AIって結局何なのかよく分からないので、とりあえず100日間勉強してみた Day88
経緯についてはこちらをご参照ください。
■本日の進捗
- RNN時系列対応を理解
■はじめに
今回も「ゼロから作るDeep Learning② 自然言語処理編(オライリー・ジャパン)」から学んでいきます。
今回は、再帰型ニューラルネットワークからRNN層以外にも時系列データに対応するためのその他の層を学んでいきます。
■時系列Embeddingクラス
時系列データを埋め込みベクトルに変換するためのTimeEmbedding層を実装していきます。
まずは引数として、語彙数(V)と埋め込み次元数(D)の形状を持つ埋め込み行列(W)を受け取ります。
self.paramsは埋め込み行列を格納するリストとして、self.gradsは埋め込み行列Wの勾配を格納するリストとして初期化します。
class TimeEmbedding:
    def __init__(self, W):
        self.params = [W]
        self.grads = [np.zeros_like(W)]
        self.layers = None
        self.W = W
順伝播の場合は、入力データである時系列データに対するインデックス(xs)を埋め込みベクトルに変換して、時刻(t)ごとに埋め込みを行った結果(out)を返します。この時、実装済みのEmbedding層の順伝播を用います。
    def forward(self, xs):
        N, T = xs.shape
        V, D = self.W.shape
        out = np.empty((N, T, D), dtype='f')
        self.layers = []
        for t in range(T):
            layer = Embedding(self.W)
            out[:, t, :] = layer.forward(xs[:, t])
            self.layers.append(layer)
        return out
逆伝播の場合は、出力側から流れてくる勾配(dout)を埋め込み行列(W)の勾配を求めます。ここでも、先ほどインスタンス化したEmbedding層を用いて逆伝播を計算します。
    def backward(self, dout):
        N, T, D = dout.shape
        grad = 0
        for t in range(T):
            layer = self.layers[t]
            layer.backward(dout[:, t, :])
            grad += layer.grads[0]
        self.grads[0][...] = grad
        return None
■時系列全結合層クラス
全結合層に関しても、Embedding層と同様に時系列データに対応したTimeAffineクラスを実装していきます。
まずは重み行列(W)とバイアス(b)を受け取り、これをリストに保存したら、self.gradsに勾配を格納するために重み行列とバイアスと同じ形状で0に初期化します。
class TimeAffine:
    def __init__(self, W, b):
        self.params = [W, b]
        self.grads = [np.zeros_like(W), np.zeros_like(b)]
        self.x = None
順伝播の場合は、バッチサイズ(N)、タイムステップ数(T)、特徴量の次元数(D)の形状を持つ入力(x)を引数として受け取り、入力(x)を(N×T, D)の形にリシェイプしてから全結合の計算を行います。最後に(N, T, -1)の形にリシェイプして元の形状にして返します。
    def forward(self, x):
        N, T, D = x.shape
        W, b = self.params
        rx = x.reshape(N*T, -1)
        out = np.dot(rx, W) + b
        self.x = x
        return out.reshape(N, T, -1)
逆伝播の場合は、出力に対する勾配(dout)を受け取り、順伝播と同様にリシェイプしてからそれぞれの勾配を計算します。この逆伝播の式は導出済みなのととても簡単なので割愛します。ここでも最後に元の形状(N, T, D)に戻して返り値にしたら完了です。
    def backward(self, dout):
        x = self.x
        N, T, D = x.shape
        W, b = self.params
        dout = dout.reshape(N*T, -1)
        rx = x.reshape(N*T, -1)
        db = np.sum(dout, axis=0)
        dW = np.dot(rx.T, dout)
        dx = np.dot(dout, W.T)
        dx = dx.reshape(*x.shape)
        self.grads[0][...] = dW
        self.grads[1][...] = db
        return dx
■時系列Softmaxクラス
最後に時系列データに対応したSoftmax層を実装していきます。
self.params, self.gradsは今は使わないのですが、保存用のcacheと併せて初期化します。ignore_labelは損失や勾配の計算で除外するラベルです。
class TimeSoftmaxWithLoss:
    def __init__(self):
        self.params, self.grads = [], []
        self.cache = None
        self.ignore_label = -1
順伝播の場合は、実装済みのsoftmax関数を用いて損失を計算します。バッチサイズ(N)、時系列長さ(T)、クラス数(V)の形状を持つ予測値の行列(xs)と、教師データの行列(ts)を入力として受け取ります。
ts.ndimが3の場合、教師データがone-hot化されているので、argmaxでクラスインデックスに変換します。ignore_labelを用いて無視するデータをmaskに保存して、xsの形状を(N×T, V)に変換し、tsの形状を(N×T)で1次元に変換します。
softmax関数で確率分布を求めたら損失を計算し、この結果を返します。
    def forward(self, xs, ts):
        N, T, V = xs.shape
        if ts.ndim == 3:
            ts = ts.argmax(axis=2)
        mask = (ts != self.ignore_label)
        xs = xs.reshape(N * T, V)
        ts = ts.reshape(N * T)
        mask = mask.reshape(N * T)
        ys = softmax(xs)
        ls = np.log(ys[np.arange(N * T), ts])
        ls *= mask
        loss = -np.sum(ls)
        loss /= mask.sum()
        self.cache = (ts, ys, mask, (N, T, V))
        return loss
逆伝播の場合は、出力側からの勾配(dout)を受け取り勾配を計算します。self.cacheからキャッシュデータを取得したら、正解クラスに対して1を引くことで勾配を計算します。(softmaxの出力は確率なので)
勾配は損失の平均で正規化して、無視するラベルの勾配を0にしておきます。最後に(N×T, V)の勾配を(N, T, V)に戻したら完了です。
    def backward(self, dout=1):
        ts, ys, mask, (N, T, V) = self.cache
        dx = ys
        dx[np.arange(N * T), ts] -= 1
        dx *= dout
        dx /= mask.sum()
        dx *= mask[:, np.newaxis]  # ignore_labelに該当するデータは勾配を0にする
        dx = dx.reshape((N, T, V))
        return dx
■おわりに
今回は再帰型ニューラルネットワークに用いる時系列データに対応させた層を実装しました。これらを用いて再帰型ニューラルネットワークを構築することができます。
■参考文献
- Andreas C. Muller, Sarah Guido. Pythonではじめる機械学習. 中田 秀基 訳. オライリー・ジャパン. 2017. 392p.
- 斎藤 康毅. ゼロから作るDeep Learning Pythonで学ぶディープラーニングの理論と実装. オライリー・ジャパン. 2016. 320p.
- 斎藤 康毅. ゼロから作るDeep Learning② 自然言語処理編. オライリー・ジャパン. 2018. 432p.
- ChatGPT. 4o mini. OpenAI. 2024. https://chatgpt.com/
- API Reference. scikit-learn.org. https://scikit-learn.org/stable/api/index.html
- PyTorch documentation. pytorch.org. https://pytorch.org/docs/stable/index.html
- Keiron O’Shea, Ryan Nash. An Introduction to Convolutional Neural Networks. https://ar5iv.labs.arxiv.org/html/1511.08458
- API Reference. scipy.org. 2024. https://docs.scipy.org/doc/scipy/reference/index.html