はじめに
『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
本の内容を1つずつ確認しながらゆっくりと組んでいきます。
この記事は、6.2節「勾配消失とLSTM」の内容です。LSTM内のゲートの機能を解説して、Pythonで再現します。
【前節の内容】
【他の節の内容】
【この節の内容】
6.2 勾配消失とLSTM
RNNレイヤで生じる勾配消失問題の回避策としてゲート付きRNNを用います。6章では、ゲート付きRNNとしてLSTMを扱います。
# 6.2節で利用するライブラリ import numpy as np
LSTMレイヤの処理では、1.2.1項で実装したsigmoid()
を利用します。関数定義を再実行するか、次の方法でマスターデータから読み込む必要があります。
# 読み込み用の設定 import sys sys.path.append('C://Users//「ユーザ名」//Documents//・・・//deep-learning-from-scratch-2-master') # 実装済みの関数 from common.functions import sigmoid
これまでと同様に、$T$個の時系列データを処理するために$T$個のLSTMレイヤをまとめたレイヤを、Time LSTMレイヤと呼ぶことにします。この節では、Time LSTMレイヤ内の1つのLSTMレイヤの計算(処理)を1つずつ確認していきます。
変数についてもこれまでと同様に、Time LSTMレイヤの入力データを$\mathbf{xs} = (\mathbf{x}_0, \cdots, \mathbf{x}_{T-1})$とします。$\mathbf{xs}$は、Time Embedレイヤの出力データであり、$(N \times T \times D)$のテンソル(3次元配列)です。$\mathbf{xs}$内の$t$番目の要素$\mathbf{x}_t$が、$t$番目のLSTMレイヤに入力します。各LSTMレイヤの入力データ$\mathbf{x}_t$に対応する重みを$\mathbf{W}_{\mathbf{x}}$とします。$\mathbf{W}_{\mathbf{x}}$は、全てのLSTMレイヤで共有されるパラメータで、$(D \times H)$の行列です。
$t$番目のLSTMレイヤの出力データを$\mathbf{h}_t$とし、隠れ状態とも呼びます。$\mathbf{h}_t$は、次($t+1$番目)のLSTMレイヤに入力します。また、全てのLSTMレイヤの出力データをまとめた$\mathbf{hs} = (\mathbf{h}_0, \cdots, \mathbf{h}_{T-1})$が、Time Affineレイヤの入力データになります。$\mathbf{hs}$は、$(N \times T \times H)$のテンソルです。$\mathbf{h}_t$に対応する重みを$\mathbf{W}_{\mathbf{h}}$とします。$\mathbf{W}_{\mathbf{h}}$もLSTMレイヤ間で共有され、$(H \times H)$の行列です。
ここで$N$はバッチサイズ、$T$は時間サイズ、$D$は単語ベクトルの要素数(Embedレイヤの中間層のニューロン数)、$H$は隠れ状態の要素数(Affineレイヤの中間層のニューロン数)です。
ここまではTime RNNレイヤのときと同じです。Time LSTMレイヤでは、各LSTMレイヤにおいて$\mathbf{h}_t$の計算に用いる変数の値(情報)を調整します。この調整機能をゲートと呼び、各LSTMレイヤは3つのゲートを持ちます。また3つ目の入力として、記憶セル$\mathbf{c}_t$を導入します。$\mathbf{c}_t$は、($\mathbf{h}_t$と同じ)$(N \times H)$の行列で、Time LSTMレイヤ内だけで扱う変数です。
Time LSTMレイヤに入力する変数(データ)を作成します。
# データとパラメータの形状に関する値を指定 N = 5 # バッチサイズ T = 7 # 時間サイズ D = 12 # 単語ベクトル(Embedレイヤの中間層)のサイズ H = 10 # 隠れ状態(Affineレイヤの中間層)のサイズ # (簡易的に)Time LSTMレイヤの入力データを作成 xs = np.random.rand(N, T, D) * 2 # Time Embedレイヤの出力 hs = np.random.rand(N, T, H) * 2 # Time LSTMレイヤの出力 c_prev = np.random.rand(N, H) * 2 # Time LSTMレイヤの記憶セル print(np.round(xs[:, 0, :], 2)) # 0時刻の入力データ
[[1.49 1.38 1.38 0.75 1.34 0.68 1.15 0.65 0.89 0.12 0.49 1.94]
[0.58 1.48 0.32 1.39 1.68 1.45 0.72 1.45 0.28 0.63 0.84 1.75]
[1.39 0.78 0.09 0.88 0.74 1.19 0.27 0.46 1.63 0.53 0.82 1.87]
[1.07 1.38 0.18 0.86 0.2 0.28 0. 0.42 0.43 1.03 1.14 1.65]
[0.17 0.73 0.98 0.57 1.07 0.42 0.99 1.06 1.35 1.49 1.46 0.85]]
$\mathbf{c}_t$は、LSTMレイヤ間でやり取りされるものなので、$T$個分をまとめる必要はありません。
続いてforgetゲート、記憶セルの計算、inputゲート、outputゲートの順番に説明します。それぞれ重み$\mathbf{W}_{\mathbf{x}},\ \mathbf{W}_{\mathbf{h}}$とバイアス$\mathbf{b} = (b_0, \cdots, b_{H-1})$を持ちます。実際の流れをイメージしやすいように本とは扱う順番を変えます。
6.2.4 forgetゲート
forgetゲート(忘却ゲート)は、1つ前のLSTMレイヤから入力する記憶セル$\mathbf{c}_{t-1}$から不要な記憶を消去(値を調整)するゲートです。右肩に$(\mathbf{f})$を付けることでforgetゲートのパラメータを表すことにします。
forgetゲートの出力$\mathbf{f}$は、記憶セルの各要素の情報を残す(値を伝播する)割合を表します。次の式で計算します。
$\mathbf{f}$は$(N \times H)$の行列です。各要素の値は、sigmoid関数により0から1の値に変換されます。sigmoid関数については、「1.2.1項」または「1巻の3.2.4項」を参照してください。
時刻を指定して、指定したLSTMレイヤに入力するデータを取り出します。ただし0番目のレイヤに関しては、1つ前のレイヤが存在しないため処理が異なります。そこでここでは、0以外の値を指定することにします。
forgetゲートのパラメータを生成します。初期値についてはこれまでと同じです。
# 時刻を指定 t = 1 # t番目のLSTMレイヤの入力を取得 x = xs[:, t, :] h_prev = hs[:, t-1, :] # forgetゲートのパラメータを生成 Wx_f = np.random.randn(D, H) Wh_f = np.random.randn(H, H) b_f = np.zeros(H)
式(6.1)の計算をして$\mathbf{f}$を求めます。sigmoid関数の計算には、1.2.1項で実装したsigmoid()
を使います。
## forgetゲートの計算 # 重み付き和を計算 f = np.dot(x, Wx_f) f += np.dot(h_prev, Wh_f) f += b_f print(np.round(f, 2)) # 活性化 f = sigmoid(f) print(np.round(f, 2)) print(f.shape)
[[ -4.43 1.97 -0.93 -3.36 0.63 3.94 -1.51 6.15 -0.23 2.65]
[ -4.55 -0.98 -7.62 1.07 2.12 3.69 -2.07 8.2 0.66 1.42]
[ -5.02 3.2 -0.44 0.6 -0.63 4.12 0.1 5.57 -2.25 4.39]
[-11.58 2.34 -0.09 -7.42 0.24 5.31 0.8 5.87 -3.25 4.12]
[-11.77 6.48 1.27 -7.96 -1.75 9.86 -0.12 9.26 -0.84 1.89]]
[[0.01 0.88 0.28 0.03 0.65 0.98 0.18 1. 0.44 0.93]
[0.01 0.27 0. 0.74 0.89 0.98 0.11 1. 0.66 0.8 ]
[0.01 0.96 0.39 0.65 0.35 0.98 0.52 1. 0.1 0.99]
[0. 0.91 0.48 0. 0.56 1. 0.69 1. 0.04 0.98]
[0. 1. 0.78 0. 0.15 1. 0.47 1. 0.3 0.87]]
(5, 10)
全ての要素が0から1の値に変換されています。これは、入力$\mathbf{x}_t,\ \mathbf{h}_{t-1}$とパラメータ$\mathbf{W}_{\mathbf{x}}^{(\mathbf{f})},\ \mathbf{W}_{\mathbf{h}}^{(\mathbf{f})},\ \mathbf{b}^{(\mathbf{f})}$の情報を基にして、記憶セル$\mathbf{c}_{t-1}$から情報を抽出する割合を決めていることを意味します。
1つ前のレイヤで求めた記憶セル$\mathbf{c}_{t-1}$の各要素に、このレイヤで求めた$\mathbf{f}$の対応する要素を掛けます。割り引いた値(不要な情報を忘却した)$\mathbf{c}_{t-1}$を、このレイヤの出力データ$\mathbf{h}_t$の計算に用います。割り引いた記憶セルを$\mathbf{c}_{t-1}^{'}$で表すことにします(この項だけで用いる表現です)。
ここで$\odot$はアダマール積と言い、(行列の積ではなく)要素ごとの積を表します。つまり$\mathbf{c}_{t-1}$と$\mathbf{f}$は同じ形状の行列です。
# t番目のLSTMレイヤに入力する記憶セル(過去の記憶セル) print(np.round(c_prev, 2)) # 過去の記憶セルから不要な情報を忘却(必要な情報を抽出) c_prev *= f print(np.round(c_prev, 2)) print(c_prev.shape)
[[0.53 1.3 0.51 1.59 0.69 1.92 0.35 1.43 1.71 1.74]
[1.04 1.1 0.59 1.71 1.52 1.03 1.01 1.84 1.12 0.19]
[0.22 1.53 1.05 1.11 0.87 0.05 1.61 0.76 0.93 0.88]
[0.27 1.93 1.92 0.02 0.11 0.77 1.96 0.96 1.85 0.68]
[0.99 1.66 1.32 0.61 0.91 1.25 0.8 0.4 0.45 1.48]]
[[0.01 1.14 0.15 0.05 0.45 1.88 0.06 1.43 0.76 1.63]
[0.01 0.3 0. 1.28 1.36 1. 0.11 1.84 0.74 0.15]
[0. 1.47 0.41 0.72 0.3 0.04 0.84 0.76 0.09 0.87]
[0. 1.76 0.92 0. 0.06 0.77 1.35 0.96 0.07 0.67]
[0. 1.66 1.03 0. 0.13 1.25 0.38 0.4 0.14 1.29]]
(5, 10)
全ての要素から値を割り引く(ゲートで情報を絞る)ので、どの要素も値が小さくなります。
これが入力した記憶セル$\mathbf{c}_{t-1}$に対する処理です。次は入力データ$\mathbf{x}_t$に対する処理を説明します。
6.2.5 新しいセル
この項では、入力データ$\mathbf{x}_t$と隠れ状態(過去の情報)$\mathbf{h}_{t-1}$から、新たな記憶セル$\mathbf{c}_t$として用いる情報を抽出することを考えます。このノードで用いるパラメータは、右肩に$(\mathbf{g})$を付けることで表します。
$\mathbf{x}_t$と$\mathbf{h}_{t-1}$から抽出した情報を$\mathbf{g}$とします。$\mathbf{g}$は、このノードのパラメータ$\mathbf{W}_{\mathbf{x}}^{(\mathbf{g})},\ \mathbf{W}_{\mathbf{h}}^{(\mathbf{g})},\ \mathbf{b}^{(\mathbf{g})}$を用いた重み付き和を、tanh関数で変換して計算します。
$\mathbf{g}$も$(N \times H)$の行列です。各要素は-1から1の値をとります。
先ほどと同様に、パラメータを生成して重み付き和を計算して、np.tanh()
で活性化します。
# 新たな情報の抽出に用いるパラメータを生成 Wx_g = np.random.randn(D, H) Wh_g = np.random.randn(H, H) b_g = np.zeros(H) ## 入力データから新たな情報を抽出 # 重み付き和を計算 g = np.dot(x, Wx_g) g += np.dot(h_prev, Wh_g) g += b_g print(np.round(g, 2)) # 活性化 g = np.tanh(g) print(np.round(g, 2)) print(g.shape)
[[ 2.39 1.45 -7.02 1.53 -5.39 4.41 -4.18 -3.77 -7.03 -3.14]
[ -1.56 2.69 -4.62 -3.44 -3.72 0.84 -5.7 -0.89 -8.49 -0.36]
[ -3.09 1.58 -11.43 3.13 -10.06 4.62 -2.7 -2.35 -6.97 -3.91]
[ 5.17 5.07 -5.82 -3.32 -2.28 5.7 -8.33 -5.96 -11.1 -0.55]
[ 5.04 2.82 -8.1 -2.18 -9.7 9.48 -11.95 -6.73 -10.92 -8.31]]
[[ 0.98 0.9 -1. 0.91 -1. 1. -1. -1. -1. -1. ]
[-0.92 0.99 -1. -1. -1. 0.69 -1. -0.71 -1. -0.35]
[-1. 0.92 -1. 1. -1. 1. -0.99 -0.98 -1. -1. ]
[ 1. 1. -1. -1. -0.98 1. -1. -1. -1. -0.5 ]
[ 1. 0.99 -1. -0.97 -1. 1. -1. -1. -1. -1. ]]
(5, 10)
全ての要素が-1から1の値に変換されています。
新たな情報$\mathbf{g}$を抽出できました。次は、$\mathbf{g}$から記憶セル$\mathbf{c}_t$として残す情報を抽出する(不要な情報を落とす)ためのinputゲートを説明します。
6.2.6 inputゲート
inputゲート(入力ゲート)は、新たな情報$\mathbf{g}$から新たに記憶セル$\mathbf{c}_t$として追加する情報を抽出するためのゲートです。inputゲートで用いるパラメータは、右肩に$(\mathbf{i})$を付けることで表します。
inputゲートの出力を$\mathbf{i}$とします。$\mathbf{i}$は、$\mathbf{g}$の各要素から$\mathbf{c}_t$として追加する割合を表し、次の式で計算します。
$\mathbf{i}$も$(N \times H)$の行列で、各要素が0から1の値になります。
forgetゲートのときと同様に計算します。
# inputゲートのパラメータを生成 Wx_i = np.random.randn(D, H) Wh_i = np.random.randn(H, H) b_i = np.zeros(H) # inputゲートを計算 i = np.dot(x, Wx_i) i += np.dot(h_prev, Wh_i) i += b_i i = sigmoid(i) print(np.round(i, 2)) print(i.shape)
[[0.26 0.12 1. 0.01 0.73 0.12 0.91 0.99 0. 0.25]
[0.89 0.03 1. 0.01 0.99 0.66 0.28 1. 0. 0.01]
[0.82 0.11 0.99 0. 0.05 0. 0.95 1. 0. 0.25]
[0.99 0.21 1. 0.11 0.98 0.23 0.28 0.94 0. 0.43]
[0.31 0. 1. 0. 0.26 0.09 0.89 1. 0. 0.75]]
(5, 10)
0から1の値が出力されます。
新たな情報$\mathbf{g}$とinputゲートの出力$\mathbf{i}$の要素ごとの積を求めて、$\mathbf{c}_t^{'}$とします。
$\mathbf{c}_t^{'}$は、inputゲートによって割り引かれた追加情報です。これをforgetゲートによって割り引かれた記憶セル(過去の情報)$\mathbf{c}_{t-1}^{'}$に加えます。
$\mathbf{c}_t$がこのレイヤで得られた記憶セルであり、分岐して1つは次のLSTMレイヤに入力し、もう1つは隠れ状態$\mathbf{h}_t$の計算に用います。
# t番目のレイヤの記憶セルとして追加する情報を抽出 c_next = g * i # t番目のレイヤの記憶セルを計算 c_next += c_prev print(np.round(c_next, 2))
[[ 0.26 1.26 -0.85 0.06 -0.28 2. -0.85 0.43 0.76 1.38]
[-0.8 0.33 -1. 1.27 0.38 1.45 -0.17 1.13 0.74 0.15]
[-0.81 1.57 -0.58 0.72 0.25 0.05 -0.1 -0.22 0.09 0.62]
[ 0.99 1.97 -0.08 -0.11 -0.9 1. 1.07 0.02 0.07 0.46]
[ 0.31 1.66 0.03 -0. -0.13 1.34 -0.51 -0.6 0.14 0.54]]
各ゲートのパラメータが学習されることで、記憶セルとして残す情報も最適化されていきます。
ここまでで記憶セル$\mathbf{c}_t$が得られました。次は、$\mathbf{c}_t$から$\mathbf{h}_t$を求める計算を説明します。
6.2.3 outputゲート
outputゲート(出力ゲート)は、このレイヤの記憶セル$\mathbf{c}_t$からこのレイヤの出力データ(隠れ状態)$\mathbf{h}_t$とする情報を抽出するためのゲートです。outputゲートで用いるパラメータは、右肩に$(\mathbf{o})$を付けることでを表します。
これまでと同様に、隠れ状態の情報の抽出率$\mathbf{o}$を計算します。
$\mathbf{o}$も$(N \times H)$の行列で、各要素は0から1の値です。
これまでと同様に計算します。
# outputゲートのパラメータを生成 Wx_o = np.random.randn(D, H) Wh_o = np.random.randn(H, H) b_o = np.zeros(H) # outputゲートを計算 o = np.dot(x, Wx_o) o += np.dot(h_prev, Wh_o) o += b_o o = sigmoid(o) print(np.round(o, 2)) print(o.shape)
[[0.42 0.31 0.05 0.39 1. 0.94 0.96 1. 0.18 0.64]
[0.08 0.01 0.05 0.01 1. 1. 1. 0.99 0.81 0.78]
[0.6 0.15 0.06 0.01 1. 0.86 0.94 1. 0.02 0.27]
[0.61 0.87 0. 0.55 1. 0.92 1. 0.97 0.01 0.91]
[0.06 0.96 0. 0.55 1. 0.75 1. 0.99 0.01 0.93]]
(5, 10)
このレイヤの記憶セル$\mathbf{c}_t$をtanh関数で活性化して、$\mathbf{o}$で割り引いた値(抽出した情報)がこのレイヤの出力データ$\mathbf{h}_t$です。
$\mathbf{h}_t$も分岐して、1つは次のLSTMレイヤに入力し、もう1つはTime Affineレイヤに入力します。
# t番目のLSTMレイヤの出力データを計算 h_next = o * np.tanh(c_next) print(np.round(h_next, 2))
[[ 0.11 0.26 -0.04 0.02 -0.27 0.91 -0.67 0.41 0.11 0.56]
[-0.05 0. -0.04 0.01 0.36 0.89 -0.17 0.81 0.51 0.11]
[-0.4 0.13 -0.03 0.01 0.24 0.04 -0.09 -0.22 0. 0.15]
[ 0.46 0.84 -0. -0.06 -0.72 0.7 0.79 0.02 0. 0.39]
[ 0.02 0.9 0. -0. -0.13 0.65 -0.47 -0.53 0. 0.46]]
パラメータが学習されることによって、ここに保存される情報が有用なものになっていきます。
最後に、求めたデータを元のリストに格納してます。
# t番目のLSTMレイヤの出力を格納
hs[:, t, :] = h_next
以上がLSTMレイヤにおける順伝播の処理です。次項では、逆伝播の処理を説明します。
参考文献
- 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.
おわりに
ややこしいですがやってることは面白いですね。次項の逆伝播は少々ハードなので飛ばしてもいいかと思います。気になるのであれば、記憶セルの伝播だけ確認するのがいいのではないでしょうか。
【次節の内容】