はじめに
『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
本の内容を1つずつ確認しながらゆっくりと組んでいきます。
この記事は、6.5節「RNNLMのさらなる改善」の内容です。Dropoutレイヤを解説して、Pythonで実装します。また改善版RNNLMの実装に関して、これまでとの変更点を確認します。
【前節の内容】
【他の節の内容】
【この節の内容】
6.5.2 Dropoutによる過学習の抑制
Dropoutについては「1巻の6.4.3項」で解説しました。この項では、Dropout後の値の調整について確認します。
# 6.5.2項で利用するライブラリ import numpy as np
・処理の確認
これまでのように、簡易的に入力データを作成します。この例ではTime LSTMレイヤの後にDropoutレイヤを入れることを想定します。どのレイヤの後に行うかに応じて、3次元方向の要素数を合わせる必要があります。
# データの形状に関する値を指定 N = 5 T = 6 H = 7 # (簡易的に)入力データを作成 xs = np.random.rand(N, T, H) * 10 print(np.round(xs[:, 0, :], 1)) print(np.round(np.sum(xs, axis=(0, 2))))
[[2. 2.1 7.6 3. 8.9 2.9 3.2]
[7.8 0.3 5.1 6.7 0.3 3.1 2. ]
[8.2 8.9 4.3 3.9 7.5 6.4 6.2]
[4.3 3.2 6.8 5.1 3.2 0.5 1.4]
[1.6 0.4 8.6 8.5 4.4 3.3 4.4]]
[156. 193. 193. 189. 194. 193.]
確認のため時刻ごとに要素の和を計算しています。
0から1の値をとる一様分布に従う乱数を利用してDropoutする要素を決めるのでした。
# Dropoutする割合を指定 dropout_ratio = 0.6 # 一様分布に従う乱数を生成 flg = np.random.rand(*xs.shape) > dropout_ratio print(flg.astype(np.float32)[:, 0, :])
[[0. 1. 0. 0. 0. 0. 1.]
[1. 1. 0. 0. 0. 1. 1.]
[0. 0. 0. 1. 1. 1. 1.]
[1. 1. 0. 1. 0. 0. 0.]
[0. 0. 0. 0. 1. 0. 1.]]
True
は1
、False
は0
として扱えることを利用して、要素を消去(値を0にして影響力をなくす)のでした。
# Dropout new_xs = xs * flg print(np.round(new_xs[:, 0, :], 1)) print(np.round(np.sum(new_xs, axis=(0, 2)))) print(np.round(np.sum(new_xs, axis=(0, 2)) / np.sum(xs, axis=(0, 2)), 2))
[[0. 2.1 0. 0. 0. 0. 3.2]
[7.8 0.3 0. 0. 0. 3.1 2. ]
[0. 0. 0. 3.9 7.5 6.4 6.2]
[4.3 3.2 0. 5.1 0. 0. 0. ]
[0. 0. 0. 0. 4.4 0. 4.4]]
[ 64. 106. 59. 102. 99. 89.]
[0.41 0.55 0.31 0.54 0.51 0.46]
ただしdropout_ratio
分の値(情報の量)も削減されてしまいます。
そこで、Dropout率を用いて値を調整します。
# 調整係数を計算 scale = 1 / (1 - dropout_ratio) mask = flg * scale print(np.round(mask[:, 0, :], 1)) # Dropout new_xs = xs * mask print(np.round(new_xs[:, 0, :], 1)) print(np.round(np.sum(new_xs, axis=(0, 2)))) print(np.round(np.sum(new_xs, axis=(0, 2)) / np.sum(xs, axis=(0, 2)), 2))
[[0. 2.5 0. 0. 0. 0. 2.5]
[2.5 2.5 0. 0. 0. 2.5 2.5]
[0. 0. 0. 2.5 2.5 2.5 2.5]
[2.5 2.5 0. 2.5 0. 0. 0. ]
[0. 0. 0. 0. 2.5 0. 2.5]]
[[ 0. 5.3 0. 0. 0. 0. 8.1]
[19.6 0.8 0. 0. 0. 7.6 4.9]
[ 0. 0. 0. 9.8 18.7 16. 15.5]
[10.8 7.9 0. 12.7 0. 0. 0. ]
[ 0. 0. 0. 0. 10.9 0. 11. ]]
[160. 265. 148. 254. 247. 222.]
[1.02 1.37 0.76 1.34 1.27 1.15]
この処理により合計値が元の水準に戻ることが期待できます。
つまり順伝播の処理では、順伝播の入力$\mathbf{xs}$の各要素に対してmask
(0かスケール値)を掛ける計算をしています。これは乗算ノード(1.3.4.1項)なので、逆伝播において$\mathbf{xs}$の勾配は、逆伝播の入力にmask
を掛けることで求められます。
・実装
処理の確認ができたので、Time Dropoutレイヤをクラスとして実装します。
# Time Dropoutレイヤの実装 class TimeDropout: def __init__(self, dropout_ratio=0.5): self.params, self.grads = [], [] self.dropout_ratio = dropout_ratio self.mask = None self.train_flg = True # 順伝播メソッドの定義 def forward(self, xs): # Dropout if self.train_flg: # 学習時のとき flg = np.random.rand(*xs.shape) > self.dropout_ratio scale = 1 / (1.0 - self.dropout_ratio) self.mask = flg.astype(np.float32) * scale return xs * self.mask else: return xs # 逆伝播メソッドの定義 def backward(self, dout): return dout * self.mask
Dropoutは学習時のみ行います。推論時は学習済みの重みをフルで使って推論します。そこで学習時かどうかをインスタンス変数train_flg
にTrue
かFalse
を指定することで制御します。詳しくは次項で確認します。(ところで、これフラグがFalse
だとbackward()
時に数値 * None
でエラーですよね?おそらく学習時にDropoutを使わないならそもそもDropoutレイヤをモデルに組み込まないという想定なんでしょうが、mask
の初期値を1
にすればいいような?)
実装したクラスを試してみましょう。Time Dropoutレイヤのインスタンスを作成して、順伝播メソッドを実行します。
# Time Dropoutレイヤのインスタンスを作成 layer = TimeDropout(dropout_ratio) # Dropout new_x = layer.forward(xs) print(np.round(new_xs[:, 0, :], 1))
[[ 0. 5.3 0. 0. 0. 0. 8.1]
[19.6 0.8 0. 0. 0. 7.6 4.9]
[ 0. 0. 0. 9.8 18.7 16. 15.5]
[10.8 7.9 0. 12.7 0. 0. 0. ]
[ 0. 0. 0. 0. 10.9 0. 11. ]]
乱数を利用しているので、実行の度に結果が変わります。
逆伝播メソッドを実行します。
# (簡易的に)逆伝播の入力を作成 dout = np.ones_like(xs) * 5 # 逆伝播の計算 dxs = layer.backward(dout) print(np.round(dxs[:, 0, :], 1))
[[12.5 0. 0. 12.5 12.5 12.5 0. ]
[12.5 0. 12.5 12.5 0. 12.5 0. ]
[ 0. 0. 0. 12.5 0. 0. 12.5]
[12.5 12.5 12.5 0. 0. 0. 12.5]
[ 0. 0. 12.5 0. 0. 0. 0. ]]
以上でTime Dropoutレイヤを実装できました。次項で重みの共有の注意点を少しだけ確認した後に、改良版RNNLMの実装に関する変更点を確認ます。
6.5.3 重みの共有
EmbeddingレイヤとAffineレイヤで同じ重みを使うことを考えます。逆伝播において、それぞれのレイヤで重みの勾配を求めることになります。同じ変数の勾配は、足し合わせる必要があります(1.3.4.2項「分岐ノード」)。その処理は、マスターデータのcommonフォルダ内のTrainer.pyファイルに実装されているremove_duplicate()
で行います。この関数の処理の確認に関しては、この資料では省略します。
6.5.4 より良いRNNLMの実装
LSTMレイヤを2層にして、各レイヤの後にDropoutレイヤを入れて、EmbedレイヤとAffineレイヤの重みを共有する改良を行った言語モデルを実装します。基本的な処理は、これまでと同様です。この資料では、これまでと異なる処理の確認を行います。
・Dropoutレイヤに関して
Dropoutは学習時のみ行います。そこで推論メソッドpredict()
の実行時に、引数の設定に従ってDropoutレイヤのインスタンス変数Train_flg
を書き換えます。
predict()
を、単語の予測(推論)を目的としてを行う際はFalse
として、損失を計算するためのスコアの計算(学習)として行う際はTrue
となるようにモデルを実装します。
・学習率に関して
確率的勾配降下法では、勾配を学習率で割り引くことでパラメータの更新(学習)の幅を調整するのでした。その際、学習率が小さすぎると極小値まで辿り着かず(あるいは時間がかかり)、大きすぎると飛び越えてしまう(あるいは発散する)のでした。詳しくは「1巻の6.1節」を参照してください。そこで、学習の進行度合いに応じて学習率を調整することを考えます。パープレキシティが改善されている間は学習率を維持して、パープレキシティが悪化した場合に学習率を小さくします。
初期値をInf
としたパープレキシティの最高値の受け皿best_ppl
を用意します。一定試行回数ごとに計測するパープレキシティppl
がbest_ppl
を下回ったとき、best_ppl
の値を更新して、学習が進んだパラメータをBetterRnnlm
のパラメータ保存メソッドsave_params()
メソッドで書き出します。逆にppl
が改善しなかったときは、学習lr
を4分の1にして、最適化手法のインスタンス変数lr
の設定を更新します。
以上で6章の内容は完了です。次章では、言語モデルを利用することを考えます。
参考文献
- 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.
おわりに
以上で6章完了です。お疲れ様でしたー。
Dropoutのscale
に関して情報の量を調整すると書きましたが、これは私が勝手に想像したものです(軽く調べたら変なサイトに引っかかって萎えて止めた)。こういうことがままあるので、本に書いてないことはあんまり妄信しないでくださいね。このブログはあくまで私の勉強中のメモ書きですので。
ところで本の章タイトルをそのまま記事タイトルにしてると、かなり盛った名付けになることがあって気が引ける。逆に、あぁ頑張った記事だからもっと検索に引っかかりやすいタイトルを付けたーいってこともままあるけどね。
【次節の内容】