からっぽのしょこ

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

5.3.2:Time RNNレイヤの実装【ゼロつく2のノート(実装)】

はじめに

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

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

 この記事は、5.3.2項「Time RNNレイヤの実装」の内容です。Time RNNレイヤを解説して、Pythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

5.3.2 Time RNNレイヤの実装

 $T$個の時系列データに対してRNNレイヤの処理を行うTime RNNレイヤを実装します。RNNレイヤは、前項で実装しました。そちらも参照してください。

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


・順伝播の処理の確認

 Time RNNレイヤの順伝播の処理を確認していきます。

 前項と同様に、データとパラメータの形状に関する値を指定して、入力データ$\mathbf{xs} = (\mathbf{x}_0, \mathbf{x}_1, \cdots, \mathbf{x}_{T-1})$、重み$\mathbf{W}_{\mathbf{x}},\ \mathbf{W}_{\mathbf{h}}$とバイアス$\mathbf{b}$を生成します。ここでは、入力するデータ数をバッチサイズと呼ぶことにします。

# バッチサイズ
batch_size = 1

# 時間サイズを指定
time_size = 7

# Embedレイヤの中間層のニューロン数を指定
wordvec_size = 10

# Affineレイヤの中間層のニューロン数を指定
hidden_size = 5

# (適当に)Time RNNレイヤの入力データを生成
xs = np.arange(
    batch_size * time_size * wordvec_size
).reshape((batch_size, time_size, wordvec_size))
print(xs.shape)

# パラメータを生成
Wh = np.random.randn(hidden_size, hidden_size)
Wx = np.random.randn(wordvec_size, hidden_size)
b = np.zeros(hidden_size)
print(Wh.shape)
print(Wx.shape)
print(b.shape)
(1, 7, 10)
(5, 5)
(10, 5)
(5,)


 前項で作成したRNNクラスのインスタンスを作成して、順伝播メソッドを実行し$\mathbf{h}_t$を計算します。この処理をfor文でtime_size回繰り返し行います。
 順伝播メソッドforward()の引数には、t番目の入力xs[:, t, :]と、1つ前の出力hを渡す必要があります。そこでforward()を計算結果をhとして保持しておき、それをTime RNNレイヤの出力hsに格納し、次の計算時の引数とします。ただし数式上の初回の計算では、$\mathbf{h}_0$は用いません(存在しません)。そこで実装上は、hの値を全て0にすることで計算に影響しないようにします。

 また使用したRNNのインスタンスは逆伝播の計算にも使うため、layersに格納しておきます。

# 初回(0番目のRNN)時に前の出力は存在しないため全ての値を0としておく
h = np.zeros((batch_size, hidden_size))

# 受け皿を初期化
hs = np.empty((batch_size, time_size, hidden_size))
layers = []

# Time RNNの処理
for t in range(time_size):
    # インスタンスを作成
    layer = RNN(Wx, Wh, b)
    
    # 順伝播の計算
    h = layer.forward(xs[:, t, :], h)
    
    # 計算結果を格納
    hs[:, t, :] = h
    
    # レイヤを格納
    layers.append(layer)

print(hs.shape)
print(len(layers))
(1, 7, 5)
7

 計算結果のhsは、Time Affineレイヤに順伝播します(の入力データになります)。

 以上がTime RNNレイヤの順伝播の処理です。続いて、図5-23を参考に逆伝播の処理を確認していきます。

・逆伝播の処理の確認

 Time RNNレイヤの逆伝播における入力は、$\frac{\partial L}{\partial \mathbf{hs}} = (\frac{\partial L}{\partial \mathbf{h}_0}, \frac{\partial L}{\partial \mathbf{h}_1}, \cdots, \frac{\partial L}{\partial \mathbf{h}_{T-1}})$です。この各要素が、対応する各RNNレイヤに入力します。加えて$t$番目のRNNレイヤにおいては、$t + 1$番目のRNNレイヤで求める$\frac{\partial L}{\partial \mathbf{h}_t}$も入力します。これは順伝播において$\mathbf{h}_t$が、「Time Affineレイヤ」と「次のRNNレイヤ」に分岐して出力したことに対応しています(1.3.4.2項)。この2つの和をとったものが逆伝播の入力となります。
 Time Affineレイヤから出力される$\frac{\partial L}{\partial \mathbf{hs}}$はここまでに計算されているものなので、ここではdhsとして簡易的にを作成することにします。

 また順伝播においてパラメータ$\mathbf{W}_{\mathbf{x}},\ \mathbf{W}_{\mathbf{h}},\ \mathbf{b}$は、0から$T - 1$番目までの全てのRNNレイヤに分岐して入力しています。つまり各パラメータの勾配$\frac{\partial L}{\partial \mathbf{W}_{\mathbf{x}}},\ \frac{\partial L}{\partial \mathbf{W}_{\mathbf{h}}},\ \frac{\partial L}{\partial \mathbf{b}}$は、$T$個のRNNレイヤで計算したものの総和です(1.3.4.3項)。そこで実装上は、初期値を0としたオブジェクトを作成しておき、そこに繰り返し逆伝播のメソッドbackward()の計算結果を足していきます。

# Time RNNレイヤの逆伝播の入力をランダムに生成
dhs = np.arange(batch_size * time_size * hidden_size).reshape((batch_size, time_size, hidden_size))
print(dhs.shape)

# 受け皿を初期化
dxs = np.empty((batch_size, time_size, wordvec_size))
dh = 0
grads = [0, 0, 0]

# Time RNNレイヤの処理
for t in reversed(range(time_size)): # T-1から0へ
    # t番目のRNNレイヤを取得
    layer = layers[t]
    
    # 逆伝播の計算
    dx, dh = layer.backward(dhs[:, t, :] + dh)
    
    # 計算結果を格納
    dxs[:, t, :] = dx
    
    # 各パラメータの勾配を加算
    for i, grad in enumerate(layer.grads):
        grads[i] += grad

print(dxs.shape)
for grad in grads:
    print(grad.shape)
(1, 7, 5)
(1, 7, 10)
(10, 5)
(5, 5)
(5,)

 計算結果$\frac{\partial L}{\partial \mathbf{xs}}$は、$\mathbf{xs}$と同じ形状になります。gradsには、$\frac{\partial L}{\partial \mathbf{W}_{\mathbf{x}}},\ \frac{\partial L}{\partial \mathbf{W}_{\mathbf{h}}},\ \frac{\partial L}{\partial \mathbf{b}}$の順で格納されています。これは各パラメータの更新に使います。

 以上がTime RNNレイヤの逆伝播の処理です。

・実装

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

# Time RNNレイヤの実装
class TimeRNN:
    # 初期化メソッドの定義
    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.dh = None, None # 中間オブジェクト
        self.stateful = stateful # フラグ
    
    # ネットワークの継続メソッド?
    def set_state(self, h):
        self.h = h
    
    # ネットワークの切断メソッド
    def reset_state(self):
        self.h = None
    
    # 順伝播メソッドの定義
    def forward(self, xs):
        # パラメータを取得
        Wx, Wh, b = self.params
        
        # データに関する値を取得
        N, T, D = xs.shape
        D, H = Wx.shape
        
        # オブジェクトを初期化
        self.layers = []
        hs = np.empty((N, T, H), dtype='f')
        
        # ネットワークを切断
        if not self.stateful or self.h is None:
            # hを初期化
            self.h = np.zeros((N, H), dtype='f')
        
        # 1ブロックの処理
        for t in range(T):
            # t番目のRNNレイヤを作成
            layer = RNN(*self.params)
            
            # 順伝播の計算
            self.h = layer.forward(xs[:, t, :], self.h)
            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, H = Wx.shape
        
        # オブジェクトを初期化
        dxs = np.empty((N, T, D), dtype='f')
        dh = 0
        grads = [0, 0, 0]
        
        # 1ブロックの処理
        for t in reversed(range(T)):
            # t番目のRNNレイヤを取得
            layer = self.layers[t]
            
            # 逆伝播の計算
            dx, dh = layer.backward(dhs[:, t, :] + dh)
            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

 (後で格納するオブジェクトと形状を合わせずに、スカラの0で初期化しているのが気になる。)

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

# インスタンスを作成
layer = TimeRNN(Wx, Wh, b)

# 順伝播の計算
hs = layer.forward(xs)
print(hs.shape)
 
# 逆伝播の計算
dsx = layer.backward(dhs)
print(dsx.shape)
for grad in layer.grads:
    print(grad.shape)
(1, 7, 5)
(1, 7, 10)
(10, 5)
(5, 5)
(5,)


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

参考文献

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

おわりに

 Time RNNレイヤにおいての入力データの扱い方を少し勘違いしていたため、5章の理解に少してこずってしまいました。内容自体は大半がこれまでと被っているので(これまでと比べて)難しくはないですね。ややこしくはあるけど。

 最初は、データ数と言うのでテキスト自体が複数あるものだと理解してしまいました。つまりテキスト数を$N$として、単語数を$T$と数えるのかと。これだとテキストごとに$T$が異なるぞ?となり修正。
 次に、あくまで1単語ずつ入力していくので、1単語が1データであり、つまり総単語数が$N$で、バッチサイズに区切って入力するものと。しかしここで言うRNNとは、RNNLMというモデル内の1レイヤであり、ここに届いたデータは既にバッチデータであり、この(バッチ)データ数をbatch_size$N$と呼びます。ちなみに最初にイメージしていたデータ数は、コーパス(テキスト)の総単語数でありcorpus_sizeと呼びます。あと1単語と言いつつも単語ベクトルとして扱っているので、$\mathbf{x}_t$もベクトルであるということを忘れていました。
 他にも色々勘違いしてました。例えばPTBデータセット(コーパス)には、複数の文章が含まれている訳ですが、それは全て繋がっており文の区切りが(多分)分からない状態だとか(それだと連続していない要素を連続したものとして渡してない?)。ミニバッチデータについても、これまでのようにランダムには抽出しません。時系列データなので順番を入れ変えられません。

 で最終的な理解が次です。

 コーパスと呼ぶ複数の文章を繋げたデータセットがあり、それをbatch_sizeに従い等間隔に区切ります。分割してしまうのではなく、目印を入れるようなイメージです。その区切りから、任意に指定した$T$(time_size)語ずつ抜き出してまとめます。このbatch_sizetime_size列で要素が単語IDの2次元配列が、RNNLMの入力でありTime Embedレイヤの入力です。この時点では、1単語は1スカラです。1回の学習が終わるとミニバッチを変更します。このとき区切りを1つ後にズラして、$T$語ずつ取り出します。
 Time Embedレイヤにより単語IDが単語ベクトルに変換されます。つまり次元が1つ増え、batch_size,time_size,wordvec_sizeの3次元配列がTime RNNレイヤに順伝播します。この2つのレイヤにおける入力データ(単語)はどちらも$\mathbf{x}$で表されます。しいて書き分けるなら、$\mathbf{xs} = (x_0, x_1, \cdots, x_{T-1})$と$\mathbf{xs} = (\mathbf{x}_0, \mathbf{x}_1, \cdots, \mathbf{x}_{T-1})$ですね。最初の要素を示す添字の0は、微妙に計算が異なるためなのか、Pythonのインデックスに合わせてるだけなのか?
 3次元配列の$\mathbf{xs}$がTime RNNレイヤ入力し、3次元配列の$\mathbf{hs}$に変換され、Time Affineレイヤに伝播します。$\mathbf{hs}$の各次元の要素数は、それぞれbatch_size,time_size,hidden_sizeです。hidden_sizeとは、Affineレイヤの中間層のニューロン数$H$です。ちなみにEmbedレイヤにも中間層がありますが、これは単語ベクトルに変換する役割なのでwordvec_sizeと呼ぶのだと思います。$D$で表します。
 Time Affineレイヤでは、$\mathbf{ys}$に変換されます。これはbatch_size,time_size,vocab_sizeの3次元配列です。vocab_sizeは、コーパスに含まれる単語の種類の数で語彙数とも呼び、$V$で表します。batch_sizetime_sizeのインデックスによって入力した単語を示しており、3次元方向に並ぶ要素の内一番高い値のインデックスが次の単語としての予測を示しています。NNのときはスコアとも読んでいました。
 これがTime Softmaxレイヤに伝播します。Time Softmaxレイヤでは、形状はそのままで、3次元方向の総和が1となるように正規化します。つまり入力した単語がそれぞれ持つvocab_size個の要素が確率分布(として扱えるような形)に変換されます。これが入力した単語(とそれまでに入力された単語)の次の単語の種類の予測となる訳です。
 最後にLossレイヤに伝播し、予測精度として交差エントロピー誤差(損失)を求めます。

 以上、愚痴ではなく初学者用のメモですよ?

【次節の内容】

www.anarchive-beta.com