はじめに
『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
本の内容を1つずつ確認しながらゆっくりと組んでいきます。
この記事は、5.4.2.3項「Time Softmax with Lossレイヤの実装」の内容です。Time Softmax with Lossレイヤを解説して、Pythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
5.4.2.3 Time Softmax with Lossレイヤの実装
$T$個の時系列データを処理するSofmaxレイヤとCross Entropy Errorレイヤ(Lossレイヤ)あわせたTime Sofmax with Lossレイヤを実装します。Sofmaxレイヤについては「1巻の3.5節」、交差エントロピー誤差については「1巻の4.2節」、Sofmax with Lossレイヤについては「1巻の5.6.3項」も参照してください。
# 5.4.2.3項で利用するライブラリ import numpy as np
Time Sofmax with Lossレイヤの実装には、「1.3.1項」で実装したsoftmax()
を利用します。関数定義を再実行するか、次の方法でマスターデータから読み込む必要があります。
# 読み込み用の設定 import sys sys.path.append('C://Users//「ユーザ名」//Documents//・・・//deep-learning-from-scratch-2-master') # 実装済みのクラス from common.functions import softmax
・数式による順伝播の計算の確認
まずは、Softmax関数の計算について確認します。
Time Softmaxレイヤの順伝播における入力データを$\mathbf{ss} = (\mathbf{s}_0, \mathbf{s}_1, \cdots, \mathbf{s}_{T-1})$とします。これは、Time Affineレイヤの順伝播の出力データです。$\mathbf{ss}$は3次元配列で、各次元の要素数はそれぞれバッチサイズ$N$、時間サイズ$T$、語彙数$V$です。$n,\ t$に関するインデックスは入力した単語に対応しており、3次元方向に並ぶ要素$v$は予測する次の単語に対応しています。対応しているとは、インデックスと単語IDが同じという意味です。
最初($t = 0$)に入力したミニバッチの内、0番目のデータを$\mathbf{s}_{00} = (s_{0,0,0}, s_{0,0,1}, \cdots, s_{0,0,V-1})$とすると、Softmax関数の計算は次の式で表せます(1.3.1項の式(1.6)より)。
この関数の計算は、入力データの3次元方向の要素について、次の条件を満たすように変換しています。
つまり各要素は0から1までの値となり、3次元方向の要素の和が1となるように正規化しています。これが確率の条件を満たすことから、$\mathbf{ys}$の3次元方向に並ぶ要素は、入力したデータの次の単語としての各語彙の出現確率と解釈できる。つまり確率が最大の(インデックスに対応する)語彙を予測結果とする。ただしSofmax関数による変換は大小関係が変わらないため、確率が最大の要素は変換前の$\mathbf{ss}$の時点で最大の要素である。
次に、損失関数の計算について確認します。損失として交差エントロピー誤差を用います。
教師ラベルを$\mathbf{ts} = (\mathbf{t}_0, \mathbf{t}_1, \cdots, \mathbf{t}_{T-1})$とします。時刻$t$に関して取り出すと、$\mathbf{t}_t$は$N \times V$の行列となり、各行は正解ラベルを1とするone-hotベクトルである。
これを用いて、時刻$t$のデータに対する交差エントロピー誤差は、次の式で計算できます(1.3.1項の式(1.8)より)。
$- \log y_{ntv}$を情報量と呼び、$\sum_{v=0}^{V-1}$は正解の語彙に関する項を抽出するためでした。また$\sum_{n=0}^{N-1}$はミニバッチ全体の情報量を合計し、バッチサイズ$N$で割ることで平均損失を求めています。1データあたりの損失を求めることで、値がデータ数に依存しないようにするためでした。
同様に求めた時刻$0$から$T-1$までの交差エントロピー誤差を合計することで、全ての時刻のミニバッチの損失とすることにします。ただし合計するだけだと、損失が$T$の大きさに影響してしまうため、$T$で割った値を損失$L$とします。これを式で表すと次のようになります。
まとめると、次の式で損失$L$を計算します。
・数式による逆伝播の計算の確認
入力データ$\mathbf{ss}$に関する勾配$\frac{\partial L}{\partial \mathbf{ss}}$の各項は、次の式で計算できます(1巻の付録A.2節より)。
ただし$\mathbf{ts}$は、$v$に関してone-hotベクトルでした。なので、実装上は$y_{ntv}$が正解の語彙であれば1を引いて$NT$で割ることで、正解でなければそのままの値を$NT$で割ることで求められます。
・順伝播の処理の確認
Time Sofmax with Lossレイヤの順伝播の処理を確認していきます。
まずは、ハイパーパラメータ(変数の形状に関する値)を指定して、Time Softmaxレイヤの順伝播における入力データ$\mathbf{ss}$を簡易的に作成します。これは本来ここまでに計算されているものなので、ここではnp.random.choice()
を使ってランダムに値を生成することにします。
# バッチサイズを指定 batch_size = 3 # 時間サイズ(繰り返し回数)を指定 time_size = 4 # 語彙数(単語の種類数)を指定 vocab_size = 5 # (ランダムに)Softmaxレイヤの入力データを作成 ss = np.random.choice( np.arange(0, 10, 0.1), size=batch_size*time_size*vocab_size, replace=True ).reshape((batch_size, time_size, vocab_size)) print(ss) print(ss.shape)
[[[7. 1.8 1. 1.8 1.7]
[5. 8.3 2.7 9.2 7.8]
[9. 8.2 1.9 3. 7.5]
[1.7 8.7 1.4 7.2 0.4]]
[[7.6 9.1 7. 4.3 4.1]
[2.4 7.9 5.7 4.6 7.5]
[9.8 2. 2.4 8.3 1.7]
[3.9 1.9 4. 0. 9.5]]
[[4. 9.6 5.3 5.1 9.9]
[8. 4.5 2.4 3.9 1.1]
[1.3 5.1 6.3 0.6 4.6]
[7.7 5.4 0.1 7.7 2.8]]]
(3, 4, 5)
前項のように、効率よく処理するために$N T \times V$の2次元配列に変換します。変換後の入力データを$\mathbf{rx}$と表記することにします。
# 入力データを2次元配列に変換 rs = ss.reshape((batch_size * time_size, vocab_size)) print(rs) print(rs.shape)
[[7. 1.8 1. 1.8 1.7]
[5. 8.3 2.7 9.2 7.8]
[9. 8.2 1.9 3. 7.5]
[1.7 8.7 1.4 7.2 0.4]
[7.6 9.1 7. 4.3 4.1]
[2.4 7.9 5.7 4.6 7.5]
[9.8 2. 2.4 8.3 1.7]
[3.9 1.9 4. 0. 9.5]
[4. 9.6 5.3 5.1 9.9]
[8. 4.5 2.4 3.9 1.1]
[1.3 5.1 6.3 0.6 4.6]
[7.7 5.4 0.1 7.7 2.8]]
(12, 5)
次に、教師ラベル$\mathbf{ts}$を簡易的に作成します。多項分布に従う乱数生成メソッドnp.random.multinomial()
を使って、one-hot表現のオブジェクトを作成します。試行回数引数をn=1
とすることでone-hotベクトルを出力します。
# (ランダムに)one-hot表現の教師ラベルを作成 ts = np.random.multinomial( n=1, pvals=np.repeat(1/vocab_size, vocab_size), size=batch_size*time_size ).reshape((batch_size, time_size, vocab_size)) print(ts) print(ts.shape)
[[[0 1 0 0 0]
[0 0 0 0 1]
[1 0 0 0 0]
[0 0 1 0 0]]
[[0 1 0 0 0]
[0 0 0 0 1]
[0 1 0 0 0]
[1 0 0 0 0]]
[[0 0 0 0 1]
[0 0 0 0 1]
[0 0 1 0 0]
[1 0 0 0 0]]]
(3, 4, 5)
効率的に処理するために、one-hotベクトルではなく単語IDで正解ラベルを表すように変換します。それぞれ3次元方向に並ぶ要素の内、値が1の要素のインデックスが単語IDです。そこで、np.argmax()
を使って最大値のインデックスを取得します。
# 単語IDに変換 ts = np.argmax(ts, axis=2) print(ts) print(ts.shape)
[[1 4 0 2]
[1 4 1 0]
[4 4 2 0]]
(3, 4)
値が-1
の要素を検索します。-1
以外(0から$V$の整数)であればTrue
、-1
であればFalse
として保持しておきます。(これは何の操作?)
# マスクする要素を検索 mask = ts != -1 print(mask)
[[ True True True True]
[ True True True True]
[ True True True True]]
True
は1
、False
は0
として計算に利用できます。
こちらも2次元配列に変換します。
# 1次元配列に変換 ts = ts.reshape((batch_size * time_size)) mask = mask.reshape((batch_size * time_size)) print(ts) print(ts.shape) print(mask) print(mask.shape) print(np.sum(mask))
[1 4 0 2 1 4 1 0 4 4 2 0]
(12,)
[ True True True True True True True True True True True True]
(12,)
12
mask
の総和が(計算に用いる)入力データ数(単語数)$N T$を表します。
必要な変数を作成できたので、順伝播の計算を行います。
# Softmax関数による正規化 ry = softmax(rs) print(np.round(ry, 2)) print(np.sum(ry, axis=1)) print(ry.shape)
[[0.98 0.01 0. 0.01 0. ]
[0.01 0.24 0. 0.6 0.15]
[0.6 0.27 0. 0. 0.13]
[0. 0.82 0. 0.18 0. ]
[0.16 0.73 0.09 0.01 0. ]
[0. 0.55 0.06 0.02 0.37]
[0.82 0. 0. 0.18 0. ]
[0. 0. 0. 0. 0.99]
[0. 0.42 0.01 0. 0.57]
[0.95 0.03 0. 0.02 0. ]
[0. 0.2 0.67 0. 0.12]
[0.47 0.05 0. 0.47 0. ]]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
(12, 5)
各行の和が1になるのを確認できました。正規化するだけなので、形状はそのままです。
これがTime Softmaxレイヤの出力データ$\mathbf{ys}$を行列に変換した$\mathbf{ry}$です。これがLossレイヤに出力します。
続いて、Time Lossレイヤの順伝播の処理を確認します。
np.arange(batch_size * time_size)
が全ての行、ts
が正解ラベルのインデックスです。これを添字として正解ラベルの要素だけを取り出して、np.log()
で対数をとります。この処理が$\sum_{v=0}^{V-1} t_{ntv} \log y_{ntv}$の計算です。
またmask
を掛けることで不要な要素が0
となり、損失に影響しないようにします。
# 正解の要素を抽出 print(np.round(ry[np.arange(batch_size * time_size), ts], 2)) # 損失の計算 ls = np.log(ry[np.arange(batch_size * time_size), ts]) ls *= mask print(np.round(ls, 2)) print(ls.shape)
[0.01 0.15 0.6 0. 0.73 0.37 0. 0. 0.57 0. 0.67 0.47]
[-5.22 -1.91 -0.52 -7.5 -0.31 -1. -8. -5.61 -0.57 -6.95 -0.4 -0.75]
(12,)
全ての要素の和をとり、符号を反転させます。これは$- \sum_{n=0}^{N-1} \sum_{t=0}^{T-1} \sum_{v=0}^{V-1} t_{ntv} \log y_{ntv}$の計算です。
# 損失の合計を計算 loss = - np.sum(ls) print(np.round(loss, 2))
38.73
最後にmask
の総和で割ります。これは$\frac{1}{NT}$の計算です。
# 平均損失を計算 loss /= np.sum(mask) print(np.round(loss, 2))
3.23
計算結果は、スカラになります。
ここまでがTime Sofmax with Lossレイヤの順伝播の処理です。これでRNNLMの最終的な出力である損失$L$が求まりました。続いて、逆伝播の処理を確認していきます。
・逆伝播の処理の確認
厳密に連鎖律を行うと、Time Softmax with Lossレイヤの順伝播における入力データ$\mathbf{ss}$に関する勾配は、$\frac{\partial L}{\mathbf{ss}} = \frac{\partial L}{\partial L} \frac{\partial L}{\partial \mathbf{ss}}$で計算します。$\frac{\partial L}{\partial L} = 1$であり、これがTime Sofmax with Lossレイヤ、またRNNLMの逆伝播における入力データです。
# Sofmax with Lossレイヤの逆伝播の入力 dL = 1
こちらも効率化のために、2次元配列に変換した状態で計算します。受け皿のdrs
にry
の値を代入して、正解ラベルの要素からのみ1
を引きます。これは、$y_{ntv} - t_{ntv}$の計算です。
# 括弧内の計算 drs = ry drs[np.arange(batch_size * time_size), ts] -= 1 print(np.round(drs, 2))
[[ 0.98 -0.99 0. 0.01 0. ]
[ 0.01 0.24 0. 0.6 -0.85]
[-0.4 0.27 0. 0. 0.13]
[ 0. 0.82 -1. 0.18 0. ]
[ 0.16 -0.27 0.09 0.01 0. ]
[ 0. 0.55 0.06 0.02 -0.63]
[ 0.82 -1. 0. 0.18 0. ]
[-1. 0. 0. 0. 0.99]
[ 0. 0.42 0.01 0. -0.43]
[ 0.95 0.03 0. 0.02 -1. ]
[ 0. 0.2 -0.33 0. 0.12]
[-0.53 0.05 0. 0.47 0. ]]
全ての要素にdL
を掛けます。これは、$\frac{\partial L}{\partial L} \frac{\partial L}{\partial \mathbf{rs}}$の計算です。
# 連鎖律の計算 drs *= dL print(np.round(drs, 2))
[[ 0.98 -0.99 0. 0.01 0. ]
[ 0.01 0.24 0. 0.6 -0.85]
[-0.4 0.27 0. 0. 0.13]
[ 0. 0.82 -1. 0.18 0. ]
[ 0.16 -0.27 0.09 0.01 0. ]
[ 0. 0.55 0.06 0.02 -0.63]
[ 0.82 -1. 0. 0.18 0. ]
[-1. 0. 0. 0. 0.99]
[ 0. 0.42 0.01 0. -0.43]
[ 0.95 0.03 0. 0.02 -1. ]
[ 0. 0.2 -0.33 0. 0.12]
[-0.53 0.05 0. 0.47 0. ]]
全ての要素をmask
の合計で割ります。これは、$\frac{1}{NT}$の計算です。
またマスクした要素は、順伝播していないので逆伝播もしません。よってFalse
(0
)を掛けます。
# 入力データの勾配の計算 drs /= np.sum(mask) drs *= mask[:, np.newaxis] # maskデータを0にする print(np.round(drs, 2))
[[ 0.08 -0.08 0. 0. 0. ]
[ 0. 0.02 0. 0.05 -0.07]
[-0.03 0.02 0. 0. 0.01]
[ 0. 0.07 -0.08 0.02 0. ]
[ 0.01 -0.02 0.01 0. 0. ]
[ 0. 0.05 0.01 0. -0.05]
[ 0.07 -0.08 0. 0.02 0. ]
[-0.08 0. 0. 0. 0.08]
[ 0. 0.04 0. 0. -0.04]
[ 0.08 0. 0. 0. -0.08]
[ 0. 0.02 -0.03 0. 0.01]
[-0.04 0. 0. 0.04 0. ]]
計算結果のdrs
は、rs
と同じ形状になります。
これを本来の形状に戻します。
# 元の形状に再変換 dss = drs.reshape((batch_size, time_size, vocab_size)) print(np.round(dss, 2)) print(dss.shape)
[[[ 0.08 -0.08 0. 0. 0. ]
[ 0. 0.02 0. 0.05 -0.07]
[-0.03 0.02 0. 0. 0.01]
[ 0. 0.07 -0.08 0.02 0. ]]
[[ 0.01 -0.02 0.01 0. 0. ]
[ 0. 0.05 0.01 0. -0.05]
[ 0.07 -0.08 0. 0.02 0. ]
[-0.08 0. 0. 0. 0.08]]
[[ 0. 0.04 0. 0. -0.04]
[ 0.08 0. 0. 0. -0.08]
[ 0. 0.02 -0.03 0. 0.01]
[-0.04 0. 0. 0.04 0. ]]]
(3, 4, 5)
計算結果のdss
は、ss
と同じ形状になります。
ここでが逆伝播の処理でした。
・実装
処理の確認ができたので、Time Softmax with Lossレイヤをクラスとして実装します。
# Time Softmax with Lossレイヤの実装 class TimeSoftmaxWithLoss: # 初期化メソッドの定義 def __init__(self): self.params = [] # パラメータ self.grads = [] # 勾配 self.cache = None self.ignore_label = -1 # 順伝播メソッドの定義 def forward(self, xs, ts): # 形状に関する値を取得 N, T, V = xs.shape # 2次元配列(単語ID)に変換 if ts.ndim == 3: # one-hotベクトルの場合 ts = ts.argmax(axis=2) # `-1`の要素のインデックスを取得 mask = (ts != self.ignore_label) # バッチ分と時系列分をまとめる(reshape) 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 # ignore_labelに該当するデータは損失を0にする loss = -np.sum(ls) loss /= mask.sum() # 逆伝播の処理用に変数を保存 self.cache = (ts, ys, mask, (N, T, V)) return loss # 逆伝播メソッドの定義 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
実装したクラスを試してみましょう。
# Time Softmax with Lossレイヤのインスタンスを作成 layer = TimeSoftmaxWithLoss() # 順伝播の計算 L = layer.forward(ss, ts) print(np.round(L, 2)) # 逆伝播の計算 dss = layer.backward(dout=1) print(np.round(dss, 2)) print(dss.shape)
3.23
[[[ 0.08 -0.08 0. 0. 0. ]
[ 0. 0.02 0. 0.05 -0.07]
[-0.03 0.02 0. 0. 0.01]
[ 0. 0.07 -0.08 0.02 0. ]]
[[ 0.01 -0.02 0.01 0. 0. ]
[ 0. 0.05 0.01 0. -0.05]
[ 0.07 -0.08 0. 0.02 0. ]
[-0.08 0. 0. 0. 0.08]]
[[ 0. 0.04 0. 0. -0.04]
[ 0.08 0. 0. 0. -0.08]
[ 0. 0.02 -0.03 0. 0.01]
[-0.04 0. 0. 0.04 0. ]]]
(3, 4, 5)
以上でRNNLMに必要なレイヤを全て実装できました。次節では、RNNLMモデルを実装します。
参考文献
- 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.
おわりに
適当に名付けてるわけではないのですが、本にない変数名にするとややこしいったらないね。
あとmaskしてるのって何ですか?後々出るんですよね。その時でいいです。
【次節の内容】