からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

6.5:RNNLMのさらなる改善【ゼロつく2のノート(実装)】

はじめに

 『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 本の内容を1つずつ確認しながらゆっくりと組んでいきます。

 この記事は、6.5節「RNNLMのさらなる改善」の内容です。Dropoutレイヤを解説して、Pythonで実装します。また改善版RNNLMの実装に関して、これまでとの変更点を確認します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

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.]]


 True1False0として扱えることを利用して、要素を消去(値を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_flgTrueFalseを指定することで制御します。詳しくは次項で確認します。(ところで、これフラグが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を用意します。一定試行回数ごとに計測するパープレキシティpplbest_pplを下回ったとき、best_pplの値を更新して、学習が進んだパラメータをBetterRnnlmのパラメータ保存メソッドsave_params()メソッドで書き出します。逆にpplが改善しなかったときは、学習lrを4分の1にして、最適化手法のインスタンス変数lrの設定を更新します。

 以上で6章の内容は完了です。次章では、言語モデルを利用することを考えます。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.

おわりに

 以上で6章完了です。お疲れ様でしたー。

 Dropoutのscaleに関して情報の量を調整すると書きましたが、これは私が勝手に想像したものです(軽く調べたら変なサイトに引っかかって萎えて止めた)。こういうことがままあるので、本に書いてないことはあんまり妄信しないでくださいね。このブログはあくまで私の勉強中のメモ書きですので。

 ところで本の章タイトルをそのまま記事タイトルにしてると、かなり盛った名付けになることがあって気が引ける。逆に、あぁ頑張った記事だからもっと検索に引っかかりやすいタイトルを付けたーいってこともままあるけどね。

【次節の内容】

www.anarchive-beta.com