ichou1のブログ

主に音声認識、時々、データ分析のことを書く

Kerasメモ(XLNet)その2

前回のつづき。

Memoryレイヤについて確認してみる。

このレイヤが生まれた背景となる問題点と、その利点については、Transformer-XLの論文で以下のとおり述べられている。

問題点。
事前定義された長さを超えるコンテキストを扱えない。

As a consequence of the fixed context length, the model cannot capture any longer-term dependency beyond the predefined context length.

利点。
長さ制限を克服する。
加えて、推論時に前の結果を再利用できる(再計算せずに済むので処理が早くなる)

Besides achieving extra long context and resolving fragmentation, another benefit that comes with the recurrence scheme is significantly faster evaluation.
Specifically, during evaluation, the representations from the previous segments can be reused instead of being computed from scratchas in the case of the vanilla model.



ソースを見てみる。
実行時のパラメータはDemoサンプル(token_embeddings_with_memory.py)を想定

このレイヤのパラメータは固定長(値は0で初期化、trainable=False)

keras_transformer_xl/memory.py
class Memory(keras.layers.Layer):
    ...
    # self.memory_len: 7 
    # self.target_len: 3
    def build(self, input_shape):
        self.memory = self.add_weight(
            shape=(self.batch_size, self.memory_len + self.target_len, self.output_dim),
            initializer='zeros',
            trainable=False,
            name='memory',
        )
        ...

レイヤのOutputの1次元目(次元は0始まりで表現)は可変長になる。

    # input_shape: [(None, 3, 768), (None, 1)]
    def compute_output_shape(self, input_shape):
        return input_shape[0][0], None, self.output_dim
        # --> (None, None, 768)

順伝播

    def call(self, inputs, **kwargs):
        ...
        # memory_length: inputs[1][0]
        # seq_len: K.shape(inputs[0])[1]

        # Build new memory
        pad = K.tile(inputs[0:1, ...], (self.batch_size - batch_size, 1, 1))
        padded = K.concatenate([inputs, pad], axis=0)
        # padded --> (self.batch_size, 3, output_dim)

        # inputを後ろに追加する
        new_memory = K.concatenate([self.memory, padded], axis=1)
        # new_memory --> (self.batch_size, 13, output_dim)

        new_memory = tf.slice(                                     
            new_memory,
            (0, 3, 0),  # <--
            (self.batch_size, 10, self.output_dim),
        )
        # new_memory --> (self.batch_size, 10, output_dim)

        # Update the value of self.memory to new_memory
        K.update(self.memory, new_memory)
        self.add_update(self.memory, inputs)

        # Build output
        old_memory = tf.slice(                                     
            new_memory,
            (0, K.maximum(0, 10 - 3 - memory_length), 0),
            (batch_size, K.minimum(7, memory_length), self.output_dim),
        )
        # old_memory --> (batch_size, memory_length, output_dim)

        return old_memory



実際に値を入れてみる。
sequenceの長さが「7」で、各sequenceの特徴量の出力が以下であるとする(本来は768次元だが、簡便のため1次元で表現)

[0.1], [0.2], [0.3], [0.4], [0.5], [0.6], [0.7]

1番目(最初)のsegment

特徴量。

[0.1], [0.2], [0.3]

memory_lengthは「0」(=new_memoryに対するsliceのサイズ)

new_memoryに対するslice

f:id:ichou1:20191116085541p:plain
tf.slice()のsizeに"0"が含まれるので、slice結果は空になる。

以下はtf.sliceのsizeに"0"が含まれているとどうなるか、挙動を確認したもの(tensorflowのバージョンは「1.14」)

まずは、sizeが全て1以上。

>>> a = np.array([[[1,2,3],[4,5,6]]])

>>> a.shape
(1, 2, 3)
>>> a
array([[[1, 2, 3],
        [4, 5, 6]]])

>>> K.eval(tf.slice(a, (0,1,0), (1, 1, 3)))
array([[[4, 5, 6]]])

sizeの1次元目を"0"にしてみると

>>> K.eval(tf.slice(a, (0,1,0), (1, 0, 3)))
array([], shape=(1, 0, 3), dtype=int64)

空になる。


2番目のsegment

特徴量。

[0.4], [0.5], [0.6]

memory_lengthは「3」(=new_memoryに対するsliceのサイズ)

new_memoryに対するslice

f:id:ichou1:20191116085855p:plain
slice結果は青枠の値になる。


3番目のsegment

特徴量。

[0.7]

memory_lengthは「6」(=new_memoryに対するsliceのサイズ)

new_memoryに対するslice

f:id:ichou1:20191116090117p:plain
slice結果は青枠の値になる。

次回に続く。