からっぽのしょこ

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

6.3.0:LSTMの実装【ゼロつく2のノート(実装)】

はじめに

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

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

 この記事は、6.3節「LSTMの実装」の始めの内容です。LSTMレイヤを解説して、Pythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

6.3.0 LSTMの実装

 前節では、LSTMレイヤの処理を確認しました。この項では、LSTMレイヤを実装します。LSTMレイヤの順伝播の計算については前々回の記事、逆伝播の計算については前回の記事を参照してください。

# 6.3.0項で利用するライブラリ
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


・処理の確認

 LSTMレイヤでは、入力データ$\mathbf{x}_t,\ \mathbf{h}_{t-1}$を用いて3つのゲート値と新しい情報を計算しました。パラメータを結合することで、この4つの計算を一度の処理で行えます。

 ここでは効率化した処理を確認します。まずは、同じ形状の重みを複数個作成します。

# 重みの行数と列数を指定
row_num = 4
col_num = 3

# 重みを作成
W1 = np.ones((row_num, col_num))
W2 = np.ones((row_num, col_num)) * 2
W3 = np.ones((row_num, col_num)) * 3
print(W1)
print(W2)
print(W3)
[[1. 1. 1.]
 [1. 1. 1.]
 [1. 1. 1.]
 [1. 1. 1.]]
[[2. 2. 2.]
 [2. 2. 2.]
 [2. 2. 2.]
 [2. 2. 2.]]
[[3. 3. 3.]
 [3. 3. 3.]
 [3. 3. 3.]
 [3. 3. 3.]]


 次に、入力データを1つ作成します。

# データを作成
x = np.arange(5 * row_num).reshape((5, row_num))
print(x)
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]
 [12 13 14 15]
 [16 17 18 19]]


 入力データと各重みとの行列の積を1つずつ確認します。

# 重み付き和を計算
print(np.dot(x, W1))
print(np.dot(x, W2))
print(np.dot(x, W2))
[[ 6.  6.  6.]
 [22. 22. 22.]
 [38. 38. 38.]
 [54. 54. 54.]
 [70. 70. 70.]]
[[ 12.  12.  12.]
 [ 44.  44.  44.]
 [ 76.  76.  76.]
 [108. 108. 108.]
 [140. 140. 140.]]
[[ 12.  12.  12.]
 [ 44.  44.  44.]
 [ 76.  76.  76.]
 [108. 108. 108.]
 [140. 140. 140.]]


 np.hstack()で重みを行方向に結合します。

# 重みを結合
W = np.hstack((W1, W2, W3))
print(W)
[[1. 1. 1. 2. 2. 2. 3. 3. 3.]
 [1. 1. 1. 2. 2. 2. 3. 3. 3.]
 [1. 1. 1. 2. 2. 2. 3. 3. 3.]
 [1. 1. 1. 2. 2. 2. 3. 3. 3.]]

 3つの重みは、それぞれ0列目からcol_num列ずつ並んでいます。

 結合した重みを使って1度の処理で行列の積を計算します。

# 重み付き和を計算
A = np.dot(x, W)
print(A)
[[  6.   6.   6.  12.  12.  12.  18.  18.  18.]
 [ 22.  22.  22.  44.  44.  44.  66.  66.  66.]
 [ 38.  38.  38.  76.  76.  76. 114. 114. 114.]
 [ 54.  54.  54. 108. 108. 108. 162. 162. 162.]
 [ 70.  70.  70. 140. 140. 140. 210. 210. 210.]]

 計算結果もcol_num列ずつ行方向に並んでいることが確認できます。勿論同じ値を計算できています。これは、xから各行を取り出しWから各列を取り出して計算するためです。後のオブジェクトの列数の変化は、出力するオブジェクトの列数に影響します。

 スライス機能を使って、本来の形状のオブジェクトを取り出します。

# 計算結果を分割
print(A[:, :col_num])
print(A[:, col_num:2*col_num])
print(A[:, 2*col_num:3*col_num])
[[ 6.  6.  6.]
 [22. 22. 22.]
 [38. 38. 38.]
 [54. 54. 54.]
 [70. 70. 70.]]
[[ 12.  12.  12.]
 [ 44.  44.  44.]
 [ 76.  76.  76.]
 [108. 108. 108.]
 [140. 140. 140.]]
[[ 18.  18.  18.]
 [ 66.  66.  66.]
 [114. 114. 114.]
 [162. 162. 162.]
 [210. 210. 210.]]

 先ほど確認した個別の計算結果と同じ出力が得られました。

・実装

 処理の確認ができたので、LSTMレイヤをクラスとして実装します。

# LSTMレイヤの実装
class LSTM:
    # 初期化メソッドの定義
    def __init__(self, Wx, Wh, b):
        self.params = [Wx, Wh, b] # パラメータ
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)] # 勾配
        self.cache = None
    
    # 順伝播メソッドの定義
    def forward(self, x, h_prev, c_prev):
        # パラメータと変数の形状に関する値を取得
        Wx, Wh, b = self.params
        N, H = h_prev.shape
        
        # 結合したパラメータによる重み付き和の計算
        A = np.dot(x, Wx) + np.dot(h_prev, Wh) + b
        
        # 各ゲートの重み付き和を取得
        f = A[:, :H]      # forgetゲート
        g = A[:, H:2*H]   # 記憶セル
        i = A[:, 2*H:3*H] # inputゲート
        o = A[:, 3*H:]    # outputゲート
        
        # ゲート値に変換
        f = sigmoid(f)
        g = np.tanh(g)
        i = sigmoid(i)
        o = sigmoid(o)
        
        # 出力を計算
        c_next = f * c_prev + g * i  # 記憶セル
        h_next = o * np.tanh(c_next) # 出力データ
        
        # 逆伝播の計算用に変数を保存
        self.cache = (x, h_prev, c_prev, i, f, g, o, c_next)
        return h_next, c_next
    
    # 逆伝播メソッドの定義
    def backward(self, dh_next, dc_next):
        # 変数を取得
        Wx, Wh, b = self.params
        x, h_prev, c_prev, i, f, g, o, c_next = self.cache
        
        # 計算用に活性化記憶セルを計算
        tanh_c_next = np.tanh(c_next)
        
        # 現レイヤの記憶セルの勾配を計算
        ds = dc_next + (dh_next * o) * (1 - tanh_c_next ** 2)
        
        # 前レイヤの記憶セルの勾配を計算
        dc_prev = ds * f
        
        # 活性化後のゲートの勾配を計算
        di = ds * g
        df = ds * c_prev
        do = dh_next * tanh_c_next
        dg = ds * i
        
        # 活性化前のゲートの勾配を計算
        di *= i * (1 - i)
        df *= f * (1 - f)
        do *= o * (1 - o)
        dg *= (1 - g ** 2)
        
        # ゲートの勾配を結合
        dA = np.hstack((df, dg, di, do))
        
        # パラメータの勾配を計算
        dWh = np.dot(h_prev.T, dA)
        dWx = np.dot(x.T, dA)
        db = dA.sum(axis=0)
        
        # パラメータの勾配を格納
        self.grads[0][...] = dWx
        self.grads[1][...] = dWh
        self.grads[2][...] = db
        
        # 入力の勾配を計算
        dx = np.dot(dA, Wx.T)
        dh_prev = np.dot(dA, Wh.T)
        
        return dx, dh_prev, dc_prev

 前項で変数名をdc_nextとしていたものが、このクラスではdsとなっています(何由来のs?)。

 実装したクラスを試してみましょう。LSTMレイヤで使う変数を作成します。

# データとパラメータの形状に関する値を指定
N = 5 # バッチサイズ
D = 15 # 単語ベクトル(Embedレイヤの中間層)のサイズ
H = 10 # 隠れ状態(Affineレイヤの中間層)のサイズ

# (簡易的に)LSTMレイヤの入力データを作成
x = np.random.rand(N, D) * 2 # Embedレイヤの出力
h_prev = np.random.rand(N, H) * 2 # 1つ前のLSTMレイヤの出力
c_prev = np.random.rand(N, H) * 2 # 1つ前のLSTMレイヤの記憶セル
print(np.round(h_prev, 2))
print(np.round(c_prev, 2))

# 結合したパラメータを生成
Wx = np.random.randn(D, H * 4)
Wh = np.random.randn(H, H * 4)
b = np.zeros(H * 4)
[[0.58 0.49 0.69 0.65 1.34 0.18 0.31 1.58 0.39 1.11]
 [1.07 1.06 1.87 0.62 0.09 0.19 1.88 1.03 0.57 0.81]
 [0.47 1.33 0.26 1.21 1.5  0.54 1.95 0.67 1.98 0.71]
 [0.92 1.54 1.39 0.16 0.62 0.87 0.74 0.99 0.24 1.47]
 [0.45 1.04 1.41 0.32 1.29 1.49 0.33 1.18 1.11 1.93]]
[[1.63 0.8  1.37 0.82 0.45 0.47 0.39 0.93 1.01 0.07]
 [0.04 0.65 0.83 0.67 0.1  0.04 0.82 0.48 0.7  1.64]
 [0.02 1.4  1.49 0.15 1.09 0.31 0.38 0.49 1.11 0.  ]
 [0.85 0.63 1.44 1.37 1.57 1.86 0.99 0.03 1.84 0.16]
 [1.84 1.58 0.39 0.74 1.14 0.84 1.55 0.08 1.1  0.25]]


 LSTMレイヤのインスタンスを作成して、順伝播メソッドを実行します。

# LSTMレイヤのインスタンスを作成
layer = LSTM(Wx, Wh, b)

# 順伝播を計算
h_next, c_next = layer.forward(x, h_prev, c_prev)
print(np.round(h_next, 2))
print(np.round(c_next, 2))
[[ 0.89  0.01  0.89  0.47  0.22  0.77  0.    0.08  0.11  0.  ]
 [ 0.61  0.   -0.2   0.42  0.    0.38  0.01  0.38  0.02  0.8 ]
 [-0.73  0.    0.95  0.68  0.31  0.39  0.27  0.05 -0.01 -0.  ]
 [ 0.65  0.    0.75  0.78  0.76  0.92  0.22  0.    0.01 -0.05]
 [ 0.8   0.    0.58  0.53  0.02  0.32  0.   -0.    0.    0.  ]]
[[ 1.4   0.62  1.43  0.52  0.23  1.05  0.06  0.78  1.39  0.  ]
 [ 0.94  0.07 -0.2   0.65  0.    0.67  0.36  0.49  1.14  1.49]
 [-0.99  0.91  2.06  0.84  0.39  0.49  0.35  0.42 -0.47 -0.  ]
 [ 1.64  0.49  0.99  1.94  0.99  1.98  0.42  0.    0.77 -0.05]
 [ 1.1   0.87  0.69  0.85  0.07  0.35  0.31 -0.    0.19  0.  ]]

 h_nextc_nextは、次のLSTMレイヤに入力します。ただしTime LSTMレイヤの実装上は、同じ変数名のオブジェクト繰り返し上書きします。

 LSTMレイヤの逆伝播の入力を作成して、逆伝播メソッドを実行します。

# (簡易的に)LSTMレイヤの入力データを作成
dh_next = np.random.rand(N, H) * 2 # LSTMレイヤの出力
dc_next = np.random.rand(N, H) * 2 # LSTMレイヤの記憶セル

# 逆伝播を計算
dx, dh_prev, dc_prev = layer.backward(dh_next, dc_next)
print(np.round(dh_prev, 2))
print(np.round(dc_prev, 2))
[[-0.22 -1.34  0.5  -0.12  1.04 -0.98 -1.47 -1.31  1.82  1.81]
 [ 0.6  -1.36 -0.76  0.71  0.62 -0.09  0.43 -1.25  0.96  0.31]
 [ 0.24 -0.9   0.78 -2.77  3.11 -1.46 -0.45 -1.21 -2.87 -1.35]
 [-0.77 -1.99 -0.54 -0.94  1.8  -0.06 -0.42 -1.8   0.63  1.28]
 [ 0.25  1.46  1.74 -1.67  0.93 -1.39  0.03  2.01 -2.15 -0.89]]
[[0.3  0.   2.27 0.51 1.31 2.39 0.18 1.13 1.11 0.02]
 [1.31 0.   0.46 1.24 0.04 0.89 0.36 1.22 0.94 1.32]
 [0.58 0.01 1.55 0.31 0.86 1.03 0.98 0.4  0.   0.4 ]
 [1.53 0.   1.23 0.47 0.77 1.81 0.92 0.18 0.47 0.04]
 [0.52 0.01 0.79 1.19 0.06 1.99 0.06 0.16 1.16 0.01]]

 dxは、$T$個分の値をdxsに格納してTime Embedレイヤに入力します。dh_prevdc_prevは、順伝播と同様に再代入を繰り返すことで、簡単にTime LSTMレイヤの処理を行えます。

 以上でLSTMレイヤを実装できました。次項では、時系列データに対応したTime LSTMレイヤを実装します。

参考文献

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

おわりに

 前回と前々回で色々書いちゃったので、今回書くことがほとんどなかった。

【次節の内容】

www.anarchive-beta.com