からっぽのしょこ

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

7.3.1:Encoderクラス【ゼロつく2のノート(実装)】

はじめに

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

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

 この記事は、7.3.1項「Encoderクラス」の内容です。seq2seqの入力側のRNNであるEncoderの処理を解説して、Pythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

7.3.1 Encoderクラス

 入力側のRNNであるEncoder(エンコーダー)を実装します。Encoderは、時系列データを入力して隠れ状態にエンコードします。

# 7.3.1項で利用するライブラリ
import numpy as np


 Encoderの実装には、RNNで用いるレイヤを利用します。そのため、各レイヤのクラス定義を再実行するか、次の方法で実装済みのクラスを読み込む必要があります。各レイヤのクラスは、「common」フォルダ内の「time_layers.py」ファイルに実装されています。各レイヤについては、5.4.2.1項6.3.1項を参照してください。

# 実装済みクラスの読み込み用の設定
import sys
sys.path.append('C://Users//「ユーザー名」//Documents//・・・//deep-learning-from-scratch-2-master')

# 実装済みのレイヤを読み込み
from common.time_layers import TimeEmbedding # 5.4.2.1項
from common.time_layers import TimeLSTM # 6.3.1項

 「deep-learning-from-scratch-2-master」フォルダにパスを設定しておく必要があります。

・処理の確認

 図7-6・7-9を参考にして、Encoderで行う処理を確認していきます。

・ネットワークの設定

 まずは、RNNを構築します。

 データとパラメータの形状に関する値を設定して、入力データを簡易的に作成します。

# データとパラメータの形状に関する値を指定
N = 6 # バッチサイズ
T = 7 # 時系列サイズ
V = 13 # 単語の種類数
D = 15 # 単語ベクトル(Embedレイヤの中間層)のサイズ
H = 10 # 隠れ状態(LSTMレイヤの中間層)のサイズ

# (簡易的に)入力データを作成
xs = np.random.randint(low=0, high=V, size=(N, T))
print(xs)
print(xs.shape)
[[ 5  2  9  4  5  6 12]
 [ 4  5 12  5  6  5  5]
 [12  2  7  5 12  0  3]
 [ 8  3  9  3 12 12  4]
 [ 3  9  7  3 10  4  6]
 [ 9  1  1  6  1  4  8]]
(6, 7)

 Encoderの入力データは、Tine Embedレイヤの入力$\mathbf{xs} = (x_{0,0}, \cdots, x_{N-1,T-1})$です。$n$番目のバッチデータの時刻$t$の入力データ$x_{n,t}$は、単語の種類(語彙)を示す単語IDです。単語IDは、語彙数を$V$として0から$V-1$の整数です。
 足し算データセットの場合は、文字の種類を示す文字IDであり$V = 13$です。また、Encoder側の時系列サイズは$T = 7$です。

 各レイヤの重みとバイアスの初期値をランダムに生成します。

# Time Embeddingレイヤのパラメータを初期化
embed_W = (np.random.randn(V, D) * 0.01)

# Time LSTMレイヤのパラメータを初期化
lstm_Wx = (np.random.randn(D, 4 * H) / np.sqrt(D))
lstm_Wh = (np.random.randn(H, 4 * H) / np.sqrt(H))
lstm_b = np.zeros(4 * H)

 各パラメータの形状については各レイヤの記事を、初期値の設定については1巻の6.2節を参照してください。

 作成したパラメータを渡して、各レイヤのインスタンスを作成します。

# Time Embeddingレイヤのインスタンスを作成
embed_layer = TimeEmbedding(embed_W)

# Time LSTMレイヤのインスタンスを作成
lstm_layer = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=False)


 以上でRNNを構築できました。次は順伝播の処理を確認します。

・順伝播の計算

 各レイヤの順伝播を計算します。

# Time Embeddingレイヤの順伝播を計算
xs = embed_layer.forward(xs)
print(xs.shape)

# Time LSTMレイヤの順伝播を計算
hs = lstm_layer.forward(xs)
print(hs.shape)

# T-1番目の隠れ状態をDecoderに出力
h = hs[:, -1, :]
print(np.round(h, 3))
print(h.shape)
(6, 7, 15)
(6, 7, 10)
[[-0.003 -0.001  0.005 -0.001 -0.005  0.001  0.002  0.004  0.001  0.008]
 [-0.003  0.001  0.004  0.006  0.002  0.001  0.004  0.001  0.001  0.001]
 [-0.003 -0.001 -0.002 -0.004  0.001 -0.001 -0.004  0.001 -0.002 -0.004]
 [ 0.    -0.002  0.001  0.001 -0.004 -0.002 -0.004  0.002  0.002 -0.001]
 [-0.004  0.001  0.004 -0.002  0.004  0.002 -0.001 -0.002 -0.001 -0.   ]
 [-0.002  0.002  0.001 -0.001  0.001  0.002  0.003 -0.002 -0.001 -0.004]]
(6, 10)

 Time Embedレイヤの出力は、単語ベクトル(の集合)$\mathbf{xs} = (x_{0,0,0}, \cdots, x_{N-1,T-1,D-1})$です。
 Time LSTMレイヤの出力は、隠れ状態(の集合)$\mathbf{hs} = (h_{0,0,0}, \cdots, h_{N-1,T-1,H-1})$です。

 「0から数えてT番目(最後の時刻)の隠れ状態$\mathbf{h}_{T-1} = (h_{0,0}^{(T-1)}, \cdots, h_{N-1,H-1}^{(T-1)})$」を「Decoderの0番目のLSTMレイヤ」に入力します。

 以上が順伝播の処理です。続いて、逆伝播の処理を確認します。

・逆伝播の計算

 「Decoderの0番目のLSTMレイヤ」から「Encoderの$T - 1$番目(最後の時刻)のLSTMレイヤ」に$\mathbf{h}_{T-1}$の勾配$\frac{\partial L}{\partial \mathbf{h}_{T-1}} = \Bigl( \frac{\partial L}{\partial h_{0,0}^{(T-1)}}, \cdots, \frac{\partial L}{\partial h_{N-1,H-1}^{(T-1)}} \Bigr)$が入力します。

 $\mathbf{hs}$の勾配$\frac{\partial L}{\partial \mathbf{hs}} = \Bigl( \frac{\partial L}{\partial \mathbf{h}_0}, \cdots, \frac{\partial L}{\partial \mathbf{h}_{T-1}} \Bigr)$を全ての要素を0として作成して、入力した$\frac{\partial L}{\partial \mathbf{h}_{T-1}}$を代入します。ここでは、$\frac{\partial L}{\partial \mathbf{h}_{T-1}}$を簡易的に作成します。

# (簡易的に)Decoderからの入力する隠れ状態の勾配を作成
dh = np.ones((N, H))
print(dh.shape)

# 隠れ状態を初期化
dhs = np.zeros_like(hs)
dhs[:, -1, :] = dh
print(dhs.shape)
(6, 10)
(6, 7, 10)

 代入時のスライスに最後の要素を表す-1を指定しています。値を明示的に示すのであればT-1です。

 各レイヤの逆伝播を逆順に計算します。

# Time LSTMレイヤの逆伝播を計算
dout = lstm_layer.backward(dhs)
print(dout.shape)

# Time Embeddingレイヤの逆伝播を計算
dout = embed_layer.backward(dout)
print(dout)
(6, 7, 15)
None

 Time LSTMレイヤの出力は、単語ベクトルの勾配$\frac{\partial L}{\partial \mathbf{xs}} = \Bigl( \frac{\partial L}{\partial x_{0,0,0}}, \cdots, \frac{\partial L}{\partial x_{N-1,T-1,D-1}} \Bigr)$です。
 Time Embedレイヤより先はないので、Noneを返します。入力データの勾配は返しません。

 以上がEncoderで行う処理です。

・実装

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

# エンコーダーの実装
class Encoder:
    # 初期化メソッド
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        # 変数の形状に関する値を取得
        V, D, H = vocab_size, wordvec_size, hidden_size
        
        # パラメータを初期化
        embed_W = (np.random.randn(V, D) * 0.01).astype('f')
        lstm_Wx = (np.random.randn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh = (np.random.randn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b = np.zeros(4 * H).astype('f')
        
        # レイヤのインスタンスを作成
        self.embed = TimeEmbedding(embed_W)
        self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=False)
        
        # パラメータと勾配をリストに格納
        self.params = self.embed.params + self.lstm.params # パラメータ
        self.grads = self.embed.grads + self.lstm.grads    # 勾配
        
        # LSTMレイヤの中間変数を初期化
        self.hs = None
    
    # 順伝播メソッド
    def forward(self, xs):
        # 各レイヤの順伝播を計算
        xs = self.embed.forward(xs)
        hs = self.lstm.forward(xs)
        
        # 逆伝播用に隠れ状態を保存
        self.hs = hs
        
        # T-1番目の隠れ状態をDecoderに出力
        return hs[:, -1, :]
    
    # 逆伝播メソッド
    def backward(self, dh):
        # 隠れ状態を初期化
        dhs = np.zeros_like(self.hs)
        dhs[:, -1, :] = dh # Decoderから入力
        
        # 各レイヤの逆伝播を逆順に計算
        dout = self.lstm.backward(dhs)
        dout = self.embed.backward(dout)
        return dout

 TimeEmbedの逆伝播メソッドbackward()Noneを返します。よって、Encoderの逆伝播メソッドbackward()Noneを返します。

 実装したクラスを試してみましょう。

 簡易的な入力データ$\mathbf{xs}$と、Encoderのインスタンスを作成します。

# (簡易的に)入力データを作成
xs = np.random.randint(low=0, high=V, size=(N, T))
print(xs)
print(xs.shape)

# Encoderのインスタンスを作成
encoder = Encoder(V, D, H)
[[ 9  9  7  2  1  7 11]
 [ 5  9  1  5  0  8  3]
 [ 0  0  6 11  9 11  4]
 [ 3  8  3  2  3  6  1]
 [11  3  6  9  2 12  2]
 [ 4 12  6  2  4 12 11]]
(6, 7)


 順伝播を計算します。

# 順伝播を計算
h = encoder.forward(xs)
print(np.round(h, 3))
print(h.shape)
[[-0.001 -0.011 -0.003 -0.002  0.004 -0.001  0.004 -0.003  0.    -0.001]
 [ 0.007 -0.001 -0.006 -0.001 -0.002  0.001  0.005 -0.005  0.     0.   ]
 [-0.004 -0.005 -0.004 -0.001  0.    -0.001  0.001 -0.    -0.    -0.002]
 [ 0.002 -0.004 -0.002  0.003  0.002 -0.     0.005  0.001  0.    -0.   ]
 [ 0.002 -0.004 -0.    -0.001  0.001  0.003  0.004  0.008 -0.    -0.003]
 [ 0.001 -0.007  0.    -0.003 -0.001 -0.001  0.003  0.001  0.001  0.001]]
(6, 10)

 入力データ$\mathbf{xs}$が、パラメータによって重み付けされて隠れ状態$\mathbf{h}_{T-1}$にエンコードされました。

 逆伝播の入力(Decoderの出力)を簡易的に作成して、逆伝播の計算を行います。

# (簡易的に)Decoderからの入力する隠れ状態の勾配を作成
dh = np.ones((N, H))
print(dh.shape)

# 逆伝播を計算
dout = encoder.backward(dh)
print(dout)
(6, 10)
None

 インスタンス内に各レイヤのパラメータの勾配が保存されます。確率的勾配降下法により、それぞれ勾配を用いてパラメータを更新します。

 以上でEncoderを実装できました。次項では、Decoderを実装します。

参考文献

おわりに

 seq2seqです。翻訳とかのヤツらしいくらいの認識でしたが、やってみると面白いですね。

 ところで、内容のない目次ページなどを除くとこの記事が201記事目だと思います。 2年と3か月半で200記事を越えました!漠然とした目標の年100記事ペースは下回ってますが、中々なのでは?まぁ暇なだけなんですがね。

【次節の内容】

www.anarchive-beta.com