RNNの実装
目次
はじめに
ここでは、前回勉強したRNNをPythonで実装してみようと思います。
前回の記事はこちらを参照。
実はかなり前にコードを書いてたのですが、放置状態になっていました。
勉強のログをブログに残しておくと、頭の中で考えが整理されて記憶に残るので、更新はゆっくりでも頑張って続けていきたいですね。
学習データ
- 例として、位相が遅れてノイズが乗った2つのsin波 、 から、元のsin波 を学習させる事を考えていきます。
グラフの青い線が元のsin波で、オレンジと緑が位相が遅れてノイズが乗ったsin波です。
オレンジと緑の5この時間データをサンプルデータ、青の同じ時刻の5個の時間データをターゲットデータとして学習させます。
1つのサンプルとターゲットを式で書くと以下のようになってます。
時間データ数5個、特徴量数2個のデータをRNNに入力して、時間データ数5個、クラス数1個のデータを出力させるようにします。
上のグラフですが、各サンプルとターゲットが5個の時間データを持っており、そのままグラフにプロットするのは難しいので、最終時間のデータだけプロットしています。
kerasによる実装
先ずkerasを使ってどんな感じになるのか見ていきましょう。
kerasには、SimpleRNNというクラスがあるのでそれを使います。
return_sequences=True
SimpleRNNの引数にreturn_sequencesというのがあります。return_sequencesをTrueに設定すると、時間データを全部出力します。Falseにすると最後の時間だけしか出力されません。
True時の出力は(サンプル数, 時間, ユニット数)の3階のテンソルになっていますが、Falseの時は時間インデックスが1つしか無いので省略されて(サンプル数, ユニット数)の2階のテンソルになります。
デフォルトではreturn_sequences=Falseになっています。今回はターゲットデータも5個の時間を持つようにしたいのでTrueにしておきます。
SimpleRNNの後ろには出力とターゲットのユニット数を合わせるために、調整用のDenseを入れておきます。
全部で100個あるサンプルデータの最初の80個をトレーニングデータ、後の20個を評価データとして学習させます。
- 以下lossの履歴と、学習後のprediction結果です。time 0.8 以降が評価データですが、中々いい感じで予測できてますね。
return_sequences=False
return_sequences=Falseにした場合は、最終時間しか出力されないので、SimpleRNNの出力の形は時間インデックスが無くなり2階のテンソルになります。
その後のDenseレイヤーでも2階のテンソルとして演算されるので、ターゲットデータもそれに合わせて、最終時間のみにしてやります。
- 以下lossの履歴と、学習後のprediction結果です。return_sequences=True時と比較してみました。
- return_sequences=False時は、True時よりもlossがちょっと下がっていますね。
- 学習後の予測結果では、最終時間だけ見るとreturn_sequences=FalseとTrueでそんなに違いはないみたいですね。
RNNの実装
では実際にRNNを実装していきたいと思います。
入力 → RNNブロック → Dense → 出力の全部で4層のネットワークを作成していきます。
基本的な構成は、以前多層パーセプトロンを実装した時と同じです。
多層パーセプトロンの実装に関してはこちら。
- kerasに合わせてreturn_sequencesの切り替え機能も付けたいと思います。
活性化関数と全結合
- 以下活性化関数と全結合のクラスです。多層パーセプトロン実装時と一緒です。
活性化関数
全結合
RNNブロック
順伝播の時間ループ
- 以下の順伝播の式を全ての時間で計算するため、時間でforループを回す必要があります。
- forward_prop関数内の以下の55~60行目の記述が相当します。
for t in range(T): if t==0: Z[:,t,:] = np.dot(Phi[:,t,:], self.W) + self.b else: Z[:,t,:] = np.dot(Phi[:,t,:], self.W) + np.dot(Phi_next[:,t-1,:], self.V) + self.b Phi_next[:,t,:] = self.act_func.forward_prop(Z[:,t,:])
- 戻り値は、return_sequencesで場合分けしてFalseの時は最終時間のみを出力させてます。64~67行目です。
if self.return_sequences: return Phi_next else: return Phi_next[:,-1,:]
逆伝播の時間ループ
- 以下逆伝播の式です。こちらも全ての時間で算出してやります。時間に関しても逆伝播するので、未来から過去に向かってforループを回してやります。
- back_prop関数内の87~92行目が相当する処理です。
for t in range(T-1, -1, -1): # Back Propagation Through Time if t==T-1: Delta[:,t,:] = self.act_func.back_prop(self.Z[:,t,:], dPhi_next[:,t,:]) else: Delta[:,t,:] = self.act_func.back_prop(self.Z[:,t,:], dPhi_next[:,t,:] + np.dot(Delta[:,t+1,:], self.V)) dPhi[:,t,:] = np.dot(Delta[:,t,:], self.W.T)
- 以下は重み勾配の計算式です。こちらは全ての時間について足し込んでやります。
- 94~98行目の処理が相当します。
for t in range(T): if t!=0: self.dV += np.dot(self.Phi_next[:,t-1,:].T, Delta[:,t,:])/n_samples self.dW += np.dot(self.Phi[:,t,:].T, Delta[:,t,:])/n_samples self.db += np.dot(np.ones([1, n_samples]), Delta[:,t,:])/n_samples
if self.return_sequences: dPhi_next = _dPhi_next.copy() else: dPhi_next = np.zeros([n_samples, T, n_units]) dPhi_next[:,-1,:] = _dPhi_next.copy()
- 最終時間以外はloss計算時に使ってないので微分 は全部ゼロになっているというわけです。
Dense中の時間ループ
- return_sequence=True時は時間データも含めた3階のテンソルで処理する必要があるので、全結合演算でも、forward_propとback_prop関数内で時間ループを回す必要があります。
- Denseを継承して、TimeSeriesDenseというクラスを作成し、時間ループの演算をするforward_propとback_prop関数をオーバーライドしておきます。
ネットワーク全体作成クラス
- 入力 → RNNブロック → Dense → 出力の4層ネットワークの作成と学習をまとめたクラスです。
- レイヤー作成のcreate_layers関数では、return_sequensesの値によって全結合クラスを変えています。True時はTimeSeriesDenseが呼ばれ、False時はDenseが呼ばれる様にしています。72~75行目の処理です。
if self.return_sequences: self.dense = TimeSeriesDense(units=n_classes, input_dim=self.rnn_units) else: self.dense = Dense(units=n_classes, input_dim=self.rnn_units)
順伝播関数forward_propと逆伝播関数back_propでは、各レイヤークラスのforward_propとback_prop関数をレイヤー順に呼ぶだけです。
lossに平均2乗誤差を用いるので、mean_square_error関数を用意しています。110~114行目。
def mean_square_error(self, X, Y): """平均2乗誤差""" Z, Phi = self.forward_prop(X) n_samples = Y.shape[0] return np.sum(0.5*np.power(Y - Phi[-1], 2))/n_samples
実行結果
return_sequences=True
- 以下、自作版RNNの学習実行スクリプトです。学習後はlossの履歴とpredicton結果をグラフに出力してkeras版の結果と比較しています。
- 以下lossの履歴です。自作版の方はlossの収束値が少し大きいですが、まあ許容範囲でしょう。
- 以下prediction結果です。思ったよりkeras版と一致してますね。
return_sequences=False
- 以下が自作版のreturn_sequences=Falseにした時の学習実施スクリプトです。こちらも学習後はloss履歴とprediction結果をグラフに出力してkeras版と比較しています。
- 以下loss履歴です。自作版の方がkeras版よりlossがちょっと小さくなりました。
- 以下prediction結果です。こちらもまあまあの一致。