からっぽのしょこ

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

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

はじめに

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

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

 この記事は、6.3.1項「Time LSTMの実装」の内容です。時系列データに対応したLSTMレイヤを解説して、Pythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

6.3.1 Time LSTMの実装

 時系列データに対応したLSTMレイヤであるTime LSTMレイヤを実装します。LSTMレイヤについては、前回の記事を参照してください。

・処理の確認

 Time LSTMレイヤで使用する変数を作成します。入力データ$\mathbf{xs} = (\mathbf{x}_0, \cdots, \mathbf{x}_{T-1})$は、Time Embeddingレイヤの出力データです。またパラメータ$\mathbf{W}_{\mathbf{x}},\ \mathbf{W}_{\mathbf{h}},\ \mathbf{b}$は、3つのゲートと記憶セルの計算で用いる4つのパラメータを行方向に結合したものです。

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

# (簡易的に)LSTMレイヤの入力データを作成
xs = np.random.rand(N, T, D) * 10
print(np.round(xs[:, 0, :], 2)) # 0時刻の入力

# 結合したパラメータを生成
Wx = np.random.randn(D, H * 4)
Wh = np.random.randn(H, H * 4)
b = np.zeros(H * 4)
[[6.09 6.37 8.47 2.14 7.33 4.72 9.   7.37 5.82 6.25 1.12 5.49]
 [1.49 6.61 6.52 9.05 9.14 5.64 4.65 8.88 5.86 7.58 9.51 4.21]
 [0.42 7.69 4.26 2.81 8.21 6.   3.35 4.6  8.98 6.63 0.37 7.62]
 [9.3  3.14 6.96 6.67 5.11 3.88 8.5  7.18 1.77 1.66 3.1  0.02]
 [9.47 6.09 5.54 3.46 0.78 8.87 0.59 9.32 8.96 4.18 3.71 4.81]]


 隠れ状態$\mathbf{h}_t$と記憶セル$\mathbf{c}_t$は、1つ前のLSTMレイヤで求めたものを入力するのでした。最初(0番目)のLSTMレイヤでは、1つ前のレイヤが存在したいため、入力する隠れ状態と記憶セルも存在しません。そこで、全ての要素の初期値を0にすることで計算に影響しないように実装します。

# 隠れ状態と記憶セルを初期化
h = np.zeros((N, H))
c = np.zeros((N, H))

 初回以降は、計算結果をそれぞれ上書きして利用します。

 LSTMレイヤのインスタンスを作成して、順伝播メソッドを実行します。使用したインスタンスは、逆伝播でも使うためlayersに格納しておきます。各レイヤで求めた隠れ状態hは、Time Affineレイヤの入力にもなるのでhsに格納しておきます。

# レイヤの受け皿を初期化
layers = []

# 出力データの受け皿を初期化
hs = np.empty((N, T, H))

# 1レイヤずつ処理
for t in range(T):
    # LSTMレイヤのインスタンスを作成
    layer = LSTM(Wx, Wh, b)
    
    # 順伝播を計算
    h, c = layer.forward(xs[:, t, :], h, c)
    
    # 出力データを格納
    hs[:, t, :] = h
    
    # インスタンスを格納
    layers.append(layer)

print(np.round(hs[:, 0, :], 2)) # 0時刻の出力
print(len(layers))
[[ 0.57 -0.    0.    0.    0.43  0.   -0.6   0.76 -0.76 -0.04]
 [-0.52 -0.    0.    0.    0.76  0.   -0.26  0.76 -0.76 -0.  ]
 [ 0.01 -0.    0.76  0.    0.71  0.   -0.14  0.06 -0.73 -0.  ]
 [-0.63 -0.    0.    0.   -0.    0.55 -0.34  0.76 -0.1  -0.  ]
 [-0.08 -0.    0.   -0.    0.76  0.   -0.02  0.76 -0.    0.02]]
7

 以上で順伝播における出力データ$\mathbf{hs} = (\mathbf{h}_0, \cdots, \mathbf{h}_{T-1})$が求まりました。

 続いて、逆伝播の計算を行います。逆伝播の入力データ$\frac{\partial L}{\partial \mathbf{hs}} = (\frac{\partial L}{\partial \mathbf{h}_0}, \cdots, \frac{\partial L}{\partial \mathbf{h}_{T-1}})$を簡易的に作成します。$\frac{\partial L}{\partial \mathbf{hs}}$は、Time Affineレイヤの出力データです。

# (簡易的に)LSTMレイヤの逆伝播の入力を作成
dhs = np.ones_like(hs)


 順伝播において隠れ状態$\mathbf{h}_t$と記憶セル$\mathbf{c}_t$は、次のLSTMレイヤとTime Affineレイヤに分岐して伝播するのでした。よって逆伝播では、それぞれ分岐先で求めた勾配を足し合わせる必要があります(1.3.4.2項「分岐ノード」)。ただし最後($T-1$番目)のLSTMレイヤには次のレイヤが存在しないので、逆伝播してくる勾配も存在しません。そこで順伝播のときと同様に、初期値が0のオブジェクトを引数に渡すことで、計算に影響しないようにします。

# 隠れ状態と記憶セルの勾配を初期化
dh = 0
dc = 0

 (形状が異なるのが気になる。)

 順伝播で使用したLSTMレイヤのインスタンスをgradsから逆順に取り出して、逆伝播メソッドを実行します。逆伝播メソッドの引数には、「Time Affineレイヤから伝播した勾配dhs[:, t, :]」と「前(t+1回目)の処理で求めた勾配dh」とを加算して渡します。

 各レイヤで求めた入力データの勾配dxは、Time Embeddingレイヤに逆伝播するのでdxsに格納しておきます。

 パラメータ$\mathbf{W}_{\mathbf{x}},\ \mathbf{W}_{\mathbf{h}},\ \mathbf{b}$は、$T$個のLSTMレイヤで共有しています。よって、各レイヤで求める勾配$\frac{\partial L}{\partial \mathbf{W}_{\mathbf{x}}},\ \frac{\partial L}{\partial \mathbf{W}_{\mathbf{h}}},\ \frac{\partial L}{\partial \mathbf{b}}$を全て足し合わせる必要があります(1.3.4.3「Repeatノード」)。
 そこで、3つのパラメータの勾配を加算していくための初期値が0のリストgradsを作成しておきます。そこに各レイヤで求めた勾配を随時加算していきます。各レイヤので求めた勾配は、インスタンス変数gradsに格納されています。

# 出力データの受け皿を初期化
dxs = np.empty((N, T, D))

# 各パラメータの勾配を初期化
grads = [0, 0, 0]

# 後のレイヤから処理
for t in reversed(range(T)):
    # t番目のレイヤのインスタンスを取得
    layer = layers[t]
    
    # 逆伝播を計算
    dx, dh, dc = layer.backward(dhs[:, t, :] + dh, dc)
    
    # 入力データの勾配を格納
    dxs[:, t, :] = dx
    
    # 各パラメータの勾配を加算
    for i, grad in enumerate(layer.grads):
        grads[i] += grad

print(np.round(dxs[:, 0, :], 2)) # 0時刻の出力
for grad in grads:
    print(grad.shape)
[[-0.21 -0.11  0.58  0.05 -0.36 -0.35 -0.33  0.32 -0.16  0.16  0.39  0.3 ]
 [ 0.02  0.19 -0.21 -0.18 -0.02 -0.06  0.08  0.05 -0.08 -0.08  0.33 -0.15]
 [-0.29 -0.27  0.01 -0.13 -0.45  0.08 -0.06 -0.05  0.61  0.11  0.07  0.03]
 [ 0.28 -0.37  0.44 -0.36 -0.36 -0.13 -0.02  0.06 -0.02 -0.45  0.35  0.16]
 [-0.02 -0.   -0.17 -0.07 -0.1   0.08  0.1  -0.05  0.19 -0.07 -0.   -0.  ]]
(12, 40)
(10, 40)
(40,)

 以上で逆伝播における出力データ$\frac{\partial L}{\partial \mathbf{xs}} = (\frac{\partial L}{\partial \mathbf{x}_0}, \cdots, \frac{\partial L}{\partial \mathbf{x}_{T-1}})$が求まりました。

 以上がTime LSTMレイヤで行う処理です。

・実装

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

# Time LSTMレイヤの実装
class TimeLSTM:
    # 初期化メソッドの定義
    def __init__(self, Wx, Wh, b, stateful=False):
        self.params = [Wx, Wh, b] # パラメータ
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)] # 勾配
        self.layers = None
        self.h, self.c = None, None
        self.dh = None
        self.stateful = stateful
    
    # 順伝播メソッドの定義
    def forward(self, xs):
        # パラメータを取得
        Wx, Wh, b = self.params
        
        # 形状に関する値を取得
        N, T, D = xs.shape
        H = Wh.shape[0]
        
        # 受け皿を初期化
        self.layers = []
        hs = np.empty((N, T, H), dtype='f')
        
        # 入力を初期化
        if not self.stateful or self.h is None:
            self.h = np.zeros((N, H), dtype='f')
        if not self.stateful or self.c is None:
            self.c = np.zeros((N, H), dtype='f')
        
        # LSTMレイヤの処理
        for t in range(T):
            # LSTMレイヤのインスタンスを作成
            layer = LSTM(*self.params)
            
            # 順伝播を計算
            self.h, self.c = layer.forward(xs[:, t, :], self.h, self.c)
            
            # 出力データを格納
            hs[:, t, :] = self.h
            self.layers.append(layer)
        
        return hs
    
    # 逆伝播メソッドの定義
    def backward(self, dhs):
        # パラメータを取得
        Wx, Wh, b = self.params
        
        # 形状に関する値を取得
        N, T, H = dhs.shape
        D = Wx.shape[0]
        
        # 出力の受け皿を初期化
        dxs = np.empty((N, T, D), dtype='f')
        
        # 入力を初期化
        dh, dc = 0, 0
        
        # LSTMレイヤの処理
        grads = [0, 0, 0]
        for t in reversed(range(T)):
            # t番目のLSTMレイヤのを取得
            layer = self.layers[t]
            
            # 逆伝播の計算
            dx, dh, dc = layer.backward(dhs[:, t, :] + dh, dc)
            
            # 入力データの勾配を格納
            dxs[:, t, :] = dx
            
            # 各パラメータの勾配を加算
            for i, grad in enumerate(layer.grads):
                grads[i] += grad
        
        # パラメータの勾配を格納
        for i, grad in enumerate(grads):
            self.grads[i][...] = grad
        
        # 隠れ状態の勾配を保存
        self.dh = dh
        return dxs
    
    # ネットワークの継続メソッド?
    def set_state(self, h, c=None):
        self.h, self.c = h, c
    
    # ネットワークの切断メソッド?
    def reset_state(self):
        self.h, self.c = None, None


 実装したクラスを試してみましょう。Time LSTMレイヤのインスタンスを作成して、順伝播メソッドを実行します。

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

# 順伝播を計算
hs = layer.forward(xs)

print(np.round(hs[:, 0, :], 2)) # 0時刻の出力
[[ 0.57 -0.    0.    0.    0.43  0.   -0.6   0.76 -0.76 -0.04]
 [-0.52 -0.    0.    0.    0.76  0.   -0.26  0.76 -0.76 -0.  ]
 [ 0.01 -0.    0.76  0.    0.71  0.   -0.14  0.06 -0.73 -0.  ]
 [-0.63 -0.    0.    0.   -0.    0.55 -0.34  0.76 -0.1  -0.  ]
 [-0.08 -0.    0.   -0.    0.76  0.   -0.02  0.76 -0.    0.02]]


 続いて、逆伝播メソッドを実行します。入力dhsの値が更新されているので、初期化しておきます。

# (簡易的に)LSTMレイヤの逆伝播の入力を作成
dhs = np.ones_like(hs)

# 逆伝播を計算
dxs = layer.backward(dhs)

print(np.round(dxs[:, 0, :], 2)) # 0時刻の出力
for grad in layer.grads:
    print(grad.shape)
[[-0.21 -0.11  0.58  0.05 -0.36 -0.35 -0.33  0.32 -0.16  0.16  0.39  0.3 ]
 [ 0.02  0.19 -0.21 -0.18 -0.02 -0.06  0.08  0.05 -0.08 -0.08  0.33 -0.15]
 [-0.29 -0.27  0.01 -0.13 -0.45  0.08 -0.06 -0.05  0.61  0.11  0.07  0.03]
 [ 0.28 -0.37  0.44 -0.36 -0.36 -0.13 -0.02  0.06 -0.02 -0.45  0.35  0.16]
 [-0.02 -0.   -0.17 -0.07 -0.1   0.08  0.1  -0.05  0.19 -0.07 -0.   -0.  ]]
(12, 40)
(10, 40)
(40,)


 以上でTime LSTMレイヤを実装できました。次節では、Time LSTMレイヤを用いた言語モデルを実装します。

参考文献

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

おわりに

 やはり重複する内容ばかりな気がする。自分が内容を理解していく段階ではあれもこれも書き残さなきゃと思うのだけど、理解してしまえばあれもこれも冗長に思えて削りたくなるジレンマ。

【次節の内容】

www.anarchive-beta.com