AIって結局何なのかよく分からないので、とりあえず100日間勉強してみた Day87
経緯についてはこちらをご参照ください。
■本日の進捗
- RNN層を理解
■はじめに
今回も「ゼロから作るDeep Learning② 自然言語処理編(オライリー・ジャパン)」から学んでいきます。
今回から再帰型ニューラルネットワークと呼ばれる時系列データを扱うニューラルネットワークを学んでいきます。
■再帰型ニューラルネットワーク
再帰型ニューラルネットワーク(Recurrent Neural Network:RNN)とは、時系列データや連続データを扱うためのニューラルネットワークで、入力データを独立したものとして扱うのではなく時間的依存関係をモデル化することで、過去の情報を循環して用います。
●順伝播
基本的な構造として、ネットワーク内の隠れ層の状態が、前の時間ステップから次の時間ステップに引き継ぎます。
$$ \mathrm{\boldsymbol{h}}_t = f(\mathrm{\boldsymbol{\mathrm{\boldsymbol{h}}}}_{t-1} \mathrm{\boldsymbol{W}}_{\mathrm{\boldsymbol{h}}} + \mathrm{\boldsymbol{x}}_{t} \mathrm{\boldsymbol{W}}_{\mathrm{\boldsymbol{x}}} + \mathrm{\boldsymbol{b}} )$$
ここで、htは時間tの隠れ状態、xtは時間tの入力データで、入力xを出力hに変換するための重みWx、ひとつ前のRNN層の出力を次の時刻の出力に変換するための重みWhという2つの重みを持ちます。また、活性化関数は一般にtanhやReLUが用いられます。
●逆伝播
この場合の逆伝播は簡単に導出できます。
tanhの導関数の式から、
$$ \tanh’ (z_t) = 1-(\tanh (z_t))^2 $$
隠れ状態に関する損失の勾配は、
$$ \frac{\partial L}{\partial z_t} = \frac{\partial L}{\partial h_t} \cdot (1 – h_t^2) $$
バイアスに関する勾配は、
$$ \frac{\partial L}{\partial b} = \displaystyle \sum_t \frac{\partial L}{\partial z_t} $$
重みに関する勾配はそれぞれ、
$$ \frac{\partial L}{\partial W_x} = x_t^T \cdot \frac{\partial L}{\partial b} $$
$$ \frac{\partial L}{\partial W_h} = h_{t-1}^T \cdot \frac{\partial L}{\partial b} $$
過去の隠れ状態に関する勾配は、
$$ \frac{\partial L}{\partial h_{t-1}} = \frac{\partial L}{\partial b} \cdot W_h^T $$
●通常のニューラルネットワークとの違い
これまで用いてきた通常のニューラルネットワークは、入力データを独立して処理し、固定長ベクトルに変換するため時間的で連続的な依存関係を考慮できません。例えば”I have a pen.”を(i, have, a, pen)と変換し、これは(pen, have, i, a)と同義です。
再帰型ニューラルネットワークでは循環構造を持つため可変長ベクトルのサイズに柔軟に対応できます。この場合でも時間的に繰り返し処理を行えるため、文章や音声全体を順序的な意味を保持したまま扱うことができます。
BERTやGPTなどのトランスフォーマーベースのモデルが開発されてからは自然言語処理における再帰型ニューラルネットワークのシェアは減少していますが、リソースに制約がある場合では用いられることもあります。
■RNN層の実装
先ほどの再帰型ニューラルネットワークを実装していきます。
まずは2つの重みとバイアスを引数として受け取ったら、勾配を0で、逆伝播用に情報を初期化していきます。
class RNN: def __init__(self, Wx, Wh, b): self.params = [Wx, Wh, b] self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)] self.cache = None
順伝播ではNumPyの双曲線関数を用いて順伝播の式を計算します。
$$ \mathrm{\boldsymbol{h}}_t = f(\mathrm{\boldsymbol{\mathrm{\boldsymbol{h}}}}_{t-1} \mathrm{\boldsymbol{W}}_{\mathrm{\boldsymbol{h}}} + \mathrm{\boldsymbol{x}}_{t} \mathrm{\boldsymbol{W}}_{\mathrm{\boldsymbol{x}}} + \mathrm{\boldsymbol{b}} )$$
また、先ほど初期化したcacheに保存していきます。
def forward(self, x, h_prev): Wx, Wh, b = self.params t = np.dot(h_prev, Wh) + np.dot(x, Wx) + b h_next = np.tanh(t) self.cache = (x, h_prev, h_next) return h_next
逆伝播では、次のタイムステップでの隠れ状態(hnext)に対する損失勾配としてdh_nextを計算していきます。先ほどの逆伝播の式を全て求めたら、勾配(self.grads)に入力重み(dWx)、隠れ状態重み(dWh)、バイアス(db)の勾配をそれぞれ代入して、最後に次の層(または前のタイムステップ)に伝播させる勾配(dx, dh_prev)を返り値として返します。
def backward(self, dh_next): Wx, Wh, b = self.params x, h_prev, h_next = self.cache dt = dh_next * (1 - h_next ** 2) db = np.sum(dt, axis=0) dWh = np.dot(h_prev.T, dt) dh_prev = np.dot(dt, Wh.T) dWx = np.dot(x.T, dt) dx = np.dot(dt, Wx.T) self.grads[0][...] = dWx self.grads[1][...] = dWh self.grads[2][...] = db return dx, dh_prev
■時間処理クラスの実装
先ほど実装したRNN層のクラスを用いて、ミニバッチ学習などの複数のタイムステップに対応するTimeRNNクラスを実装していきます。
まずはRNNのパラメータでもある2つの重みとバイアスを引数として受け取ります。ここで、隠れ状態を時間的に維持するかどうかをself.statefulで設定できます。
ここで、self.layersは各タイムステップで使用するRNN層を格納するリストで、self.hは隠れ状態、self.dhは逆伝播において保持される隠れ状態に関する勾配で、それぞれ初期化しておきます。
class TimeRNN: def __init__(self, Wx, Wh, b, stateful=False): self.params = [Wx, Wh, b] self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)] self.layers = None self.h, self.dh = None, None self.stateful = stateful
順伝播においては、入力データ(xs)は、バッチサイズ(N)、タイムステップ数(T)、入力次元数であり特徴量数(D)の形状を持つ行列です。
self.statefulがTrueでないか、self.hが設定されていないなら最初の隠れ状態を0で初期化してから、時系列データを各タイムステップ(t)でループ処理をし、RNN層を順番に適用します。RNNの引数(*self.params)は、self.paramsの要素を展開して渡すことなので、(Wx, Wh, b)としてRNNに渡していることと同じです。すべてのタイムステップについて処理を終えたら、隠れ状態の配列(hs)を返します。
def forward(self, xs): Wx, Wh, b = self.params N, T, D = xs.shape D, H = Wx.shape self.layers = [] hs = np.empty((N, T, H), dtype='f') if not self.stateful or self.h is None: self.h = np.zeros((N, H), dtype='f') for t in range(T): layer = RNN(*self.params) self.h = layer.forward(xs[:, t, :], self.h) hs[:, t, :] = self.h self.layers.append(layer) return hs
逆伝播においては、バッチサイズ(N)、タイムステップ数(T)、隠れ状態ベクトルの次元数(H)を形状として持つ、逆伝播で渡される隠れ状態の勾配(dhs)を引数として受け取ります。
dhを0で初期化したら、隠れ状態の勾配を累積するために時系列の逆順(reversed(range(T)))でループを回し、各タイムステップにおけるRNN層の逆伝播を行います。最後に累積された勾配をself.gradsに格納して、最終的な隠れ状態の勾配をself.dhに保持します。
def backward(self, dhs): Wx, Wh, b = self.params N, T, H = dhs.shape D, H = Wx.shape dxs = np.empty((N, T, D), dtype='f') dh = 0 grads = [0, 0, 0] for t in reversed(range(T)): layer = self.layers[t] dx, dh = layer.backward(dhs[:, t, :] + dh) dxs[:, t, :] = dx for i, grad in enumerate(layer.grads): grads[i] += grad for i, grad in enumerate(grads): self.grads[i][...] = grad self.dh = dh return dxs
最後にself.statefulがTrueの場合に隠れ状態(h)を設定し、リセットするための関数を定義します。
def set_state(self, h): self.h = h def reset_state(self): self.h = None
つまり、statefulがFalseの場合は各バッチの隠れ状態は次のバッチに引き継がれずに毎回0で初期化された隠れ状態から始まり、Trueの場合にはバッチ間で隠れ状態が引き継がれ随時次のバッチ計算に使用されます。時間的な依存関係を保持したい場合はTrueにしておくのがいいでしょう。
■おわりに
今回は再帰型ニューラルネットワークの肝となるRNN層の実装を行いました。ミニバッチ学習に対応するためにこれ以外にもいくつかの変更を加えて完成という流れになりますが、再帰型ニューラルネットワークでやりたいことは本稿でまとまったかと思います。
■参考文献
- 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