からっぽのしょこ

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

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

はじめに

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

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

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

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

5.4.2.1 Time Embeddingレイヤの実装

 依存関係のある$T$個のデータ(時系列データ)を処理するために、$T$個のEmbeddingレイヤをまとめたTime Embeddingレイヤを実装します。Embedレイヤについては、4章も参照してください。

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


 Time Embedレイヤの実装には、4.2.1項で実装したEmbedレイヤのクラスを利用します。クラスの定義を再実行するか、次の方法でマスターデータから読み込みます。

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

# 実装済みのクラス
from common.layers import Embedding


 また、テキストを入力データとして整形するための関数も読み込みます。2.3.1項で実装したpreprocess()を使って、テキストをコーパスに変換(単語に分割)します。

# 実装済みの関数
from common.util import preprocess


・順伝播の処理の確認

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

 まずは入力データと教師ラベルを作成します。テキストを設定して、preprocess()で単語に分割します。

# テキストを設定
text = 'You say goodbye and I say hello.'

# 谷後に分割
corpus, word_to_id, id_to_word = preprocess(text)
print(id_to_word)
print(corpus)
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
[0 1 2 3 4 1 5 6]

 各要素は単語IDで、つまり1つの要素が1つの単語に対応しています。

 ここから入力データ$\mathbf{xs} = (x_0, x_1, \cdots, x_{T-1})$と教師ラベル$\mathbf{ts} = (t_0, t_1, \cdots, t_{T-1})$を作成します。RNNでは、単語を順番に入力してその次の単語を予測するのでした。よって最後の単語は次の単語が存在しないため入力データには含めず、最初の単語は予測できないため教師ラベルには含めません。

# 学習用のデータを取得
xs = corpus[:-1] # 入力データ
ts = corpus[1:]  # 教師ラベル
print(xs)
print(ts)

# 形状に関する値を取得
vocab_size = len(word_to_id) # 語彙数(単語の種類数)
print(vocab_size)
[0 1 2 3 4 1 5]
[1 2 3 4 1 5 6]
7

 語彙数$V$(vocab_size)は、Affineレイヤでも利用します。

 最終的に、RNNLMはバッチデータ(2次元配列)を扱えるように実装します。そこでここでは、xstsを簡易的に2次元配列に変換します。変換後のデータは、バッチサイズ$N = 1$と言えます。ミニバッチの処理については、5.5.3項で扱います。

 またRNNレイヤの時系列データの数(繰り返し回数)$T$(time_size)についても、簡易的にxsの要素数(単語数)としておきます。

# バッチサイズ
batch_size = 1

# 時系列サイズ(繰り返し回数)
time_size = len(xs)

# 2次元配列に変換
xs = xs.reshape(1, time_size)
ts = ts.reshape(1, time_size)
print(xs)
print(ts)
[[0 1 2 3 4 1 5]]
[[1 2 3 4 1 5 6]]

 RNNLMに入力される前は、各単語はスカラ(単語ID)で表現されています。

 次に、Embedレイヤで用いる重みを生成します。これは4.1節における入力層の重み$\mathbf{W}_{\mathrm{in}}$のことで、行数が語彙数$V$、列数がEmbedレイヤの中間層のニューロン数$D$の2次元配列です。入力データとこの重みをEmbedレイヤで処理することで、各単語が$D$次元のベクトルに変換されます。そこでEmbedレイヤの中間層のニューロン数をwordvec_sizeと呼ぶことにします。

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

# Embedレイヤの重みを生成
W = np.random.randn(vocab_size, wordvec_size)
print(W.shape)
(7, 10)


 入力データと重みを作成できたので、順伝播の処理を行います。

 Embedレイヤのインスタンスを作成し、順伝播メソッドを実行し、出力をoutに格納する処理をtime_size回繰り返します。最終的な出力データの形状は、3次元配列になります。各次元の要素数は、$(N, T, V)$です。

 使用したEmbedレイヤのインスタンスは、逆伝播でも使うため、layersに格納しておきます。

# 受け皿を初期化
out = np.empty((batch_size, time_size, wordvec_size), dtype='f')
layers = []

# 繰り返し処理
for t in range(time_size):
    # Embedレイヤのインスタンスを作成
    layer = Embedding(W)
    
    # 順伝播の計算
    out[:, t, :] = layer.forward(xs[:, t])
    
    # t番目のレイヤを格納
    layers.append(layer)

print(np.round(out, 2))
print(out.shape)
print(len(layers))
[[[-0.23  0.79 -0.71 -0.27  1.75  1.11  0.86 -0.48  0.36 -2.33]
  [-2.13 -1.13 -0.33  1.16 -0.08  1.33 -1.48  0.46 -1.72 -0.35]
  [ 0.13 -1.21  2.36  1.83  0.08 -0.45 -0.43 -0.87  0.17  1.73]
  [-1.24  1.46  0.49  0.04  0.89 -0.67  0.61 -0.08  1.35 -0.38]
  [-0.5  -0.13  0.64 -0.35 -1.44 -0.66  0.56  0.9  -0.26 -0.02]
  [-2.13 -1.13 -0.33  1.16 -0.08  1.33 -1.48  0.46 -1.72 -0.35]
  [ 0.52  1.19 -1.47  0.65  2.11 -1.47  1.12  0.18  1.81 -0.13]]]
(1, 7, 10)
7

 単語IDから単語ベクトルに変換され、3次元方向に$D$個拡張しました。この出力outが、5.3.2項で実装したTime RNNレイヤの入力データ$\mathbf{xs}$です。
 またlayersに、time_size個のEmbeddingレイヤのインスタンスが格納されているのを確認できます。

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

・逆伝播の処理の確認

 layersに格納されているレイヤの逆伝播メソッドを順番に実行して、重みの勾配$\frac{\partial L}{\partial \mathbf{W}}$を求めます。ただし、全てのEmbedレイヤで用いる重みは同じものです。つまり1つの重みが、$T$個のEmbedレイヤに分岐して入力していると言えます(1.3.4.3項「Repeatノード」)。よって、各レイヤで求めた重みの勾配$\frac{\partial L}{\partial \mathbf{W}}$を全て足す必要があります。
 そこで実装上は、重みの勾配dWの初期値を0として、そこに各Embedレイヤで求めた値を足していきます。

 Time Embedレイヤは、逆伝播において最後のレイヤであり次に伝播する必要がないため、このレイヤの入力に関する勾配を求める必要はありません。

# (結果が分かりやすいように)逆伝播の入力を作成
dout = np.ones((batch_size, time_size, wordvec_size))

# 繰り返し処理
dW = np.zeros_like(W) # 受け皿を初期化
for t in range(time_size):
    # t番目のレイヤを取得
    layer = layers[t]
    
    # 逆伝播の計算
    layer.backward(dout[:, t, :])
    
    # 勾配を加算
    dW += layer.grads[0]

print(dW)
print(dW.shape)
print(xs)
[[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [2. 2. 2. 2. 2. 2. 2. 2. 2. 2.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
(7, 10)
[[0 1 2 3 4 1 5]]

 dWの形状は、Wと同じvocab_sizewordvec_size列になります。xsの各要素(単語ID)に対応するdWの行(語彙)に、逆伝播の入力doutの値が加算されていることが分かります。詳しくは4.1.1項を参照してください。この勾配dWを用いて、重みWを更新します。

 ここまでが逆伝播の処理です。

・実装

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

# Time Embeddingレイヤの実装
class TimeEmbedding:
    
    # 初期化メソッドの定義
    def __init__(self, W):
        self.params = [W] # パラメータ
        self.grads = [np.zeros_like(W)] # 勾配
        self.layers = None # レイヤ
        self.W = W # 重み
    
    # 順伝播メソッドの定義
    def forward(self, xs):
        # 形状に関する値を取得
        N, T = xs.shape
        V, D = self.W.shape
        
        # 受け皿を初期化
        out = np.empty((N, T, D), dtype='f')
        self.layers = []
        
        # 繰り返し処理
        for t in range(T):
            # Embedレイヤ(インスタンス)を作成
            layer = Embedding(self.W)
            
            # 順伝播の計算
            out[:, t, :] = layer.forward(xs[:, t])
            
            # t番目のレイヤを格納
            self.layers.append(layer)

        return out
    
    # 逆伝播メソッドの定義
    def backward(self, dout):
        # 形状に関する値を取得
        N, T, D = dout.shape
        
        # 繰り返し処理
        dW = np.zeros_like(self.W) # 受け皿を初期化
        for t in range(T):
            # t番目のレイヤを取得
            layer = self.layers[t]
            
            # 逆伝播の計算
            layer.backward(dout[:, t, :])
            
            # 勾配を加算
            dW += layer.grads[0]
        
        # 結果を格納
        self.grads[0][...] = dW
        return None


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

# Time Embedレイヤのインスタンスを作成
layer = TimeEmbedding(W)

# 順伝播の計算
out = layer.forward(xs)
print(np.round(out, 2))
print(out.shape)

# 逆伝播の計算
layer.backward(dout)
print(layer.grads[0])
print(layer.grads[0].shape)
[[[-0.23  0.79 -0.71 -0.27  1.75  1.11  0.86 -0.48  0.36 -2.33]
  [-2.13 -1.13 -0.33  1.16 -0.08  1.33 -1.48  0.46 -1.72 -0.35]
  [ 0.13 -1.21  2.36  1.83  0.08 -0.45 -0.43 -0.87  0.17  1.73]
  [-1.24  1.46  0.49  0.04  0.89 -0.67  0.61 -0.08  1.35 -0.38]
  [-0.5  -0.13  0.64 -0.35 -1.44 -0.66  0.56  0.9  -0.26 -0.02]
  [-2.13 -1.13 -0.33  1.16 -0.08  1.33 -1.48  0.46 -1.72 -0.35]
  [ 0.52  1.19 -1.47  0.65  2.11 -1.47  1.12  0.18  1.81 -0.13]]]
(1, 7, 10)
[[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [2. 2. 2. 2. 2. 2. 2. 2. 2. 2.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
(7, 10)


 以上でTime Embeddingレイヤを実装できました。次項では、Time Affineレイヤを実装します。

参考文献

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

おわりに

 来年はもう少し分かりやすい解説文を書けるようになりたいなぁ。あでも、いつの間にか書くこと自体は苦じゃなくなってるのは成長です。

【次節の内容】

www.anarchive-beta.com