(論文)Transformer
久しぶりにブログを更新する。
今日は「Attention Is All You Need」に関する復習。
もはや2年前の論文で、日本語でも丁寧な解説記事がたくさんある。
とっても今更感があるが、自分自身の理解の定着のためにまとめようと思う。
とりあえず、従来のRNN型のSeq2Seqモデルと何が違うのか、その辺を押さえたい。
後、理解を深めるために適宜コードも入れればと思う。
※コードに関しては以下サイトより引用させて頂いた。
towardsdatascience.com
nlp.seas.harvard.edu
Transformerモデル
論文中の図を引用する。
Decoderの入力となるOutputはターゲットは1tokenずつ右にシフトさせておく。
例えば、ターゲット文が「<BOS> 私 は サッカー が 好き <EOS>」の場合、
「<BOS> 私 は サッカー が 好き」としておく。
また、時刻tのtokenから時刻t+1のtokenを予測するためターゲットを左にシフトさせる。
(ターゲットは「私 は サッカー が 好き <EOS>」としておく。)
モデル自体は従来のSeq2Seqモデルと同じ構造。
新しく出現するキーワードは、
- Positional Encoding
- Multi-Head Attention
- Position-wise Feed-Forward Networks
の3つ。
Positional Encoding
入力の位置情報の付与。
センテンスの文脈情報を理解するためには、各単語の順序が重要となる。
このモデルはRNNのように順序を考慮しないので、位置情報を明示的に付与する必要がある。
Positional Encodingは以下の式に基づいて行う。
pos
はセンテンス中の出現位置 ,i
は次元数(単語の分散表現の次元数)。
例えば、文の先頭位置に出現する単語のPositional Encodingを考える。
論文の単語の分散表現の次元数は512なので、以下のように先頭位置を表すベクトルは得られる。
このようして得られた行列をEmbedding後の文の行列に加算することで、各単語と位置(文脈)情報を考慮した情報が得られる。
Multi-Head Attention
Multi-Head Attention層は以下のようなイメージ。
入力を複数のHeadで分割する。
入力は
各パラメータは,,
実験では,なので,としている。
入力を各重みでの線形変換したもの
をScaled dot-product attentionの入力としている。
Scaled dot-product attention
この論文でのattentionモデルであり、タイトルにもあるようにここがモデルの肝の部分。
入力の,,は先ほど計算したもの。
attentionの計算式は以下の通り。
まず、の部分。
だったので、となる。
これにでスケーリングを行った後、softmaxを適用する。
これにを掛け合わせるので、最終的には、
となる。
これを各head毎に行ったものをConcatして重みで線形変換を行う。
となり、単語数×単語の分散表現の次元数に戻される。
結局のところ、この式変形が何を行っていたかをもう少し深堀してみる。
Query, Key, Value
まず、の部分。
スケーリングを無視して考えると、オレンジのスカラー値はの赤のベクトルとの青のベクトルの内積からなることが分かる。
以下、同様となる。
要するに、各queryと各keyの類似度を計算していることに相当する。
この行列の0行目のベクトルはquery0と各keyの類似度を表現したベクトルを意味する。
これをsoftmaxで正規化しており、各行の総和が1となるような重みベクトル(attention_weight)を作り出す。
最後にこの行列との内積を取る。
各queryのattention_weightとvalueの内積を取ったものをattentionの結果として返す。
なので、最終的な単語ベクトル(オレンジ)は、valueの各ベクトルにattention_weightを掛け合わせた値となることが分かる。
Encoderでは、query, key, valueは全て同じ入力から渡される。(Self-Attention)
一方、Decoderでは、1層目のMulti-Head Attentionは全て出力から渡されるが、
2層目のQueryは出力から、key, valueはEncoderから渡される。(Source-Target-Attention)
Decoderの2層目のMulti-Head Attentionでは、出力の各queryとEncoderの各keyの類似度を取り、attention_weightを計算する。
それに出力のvalueを掛け合わせることで、Encoderのattention_weightを考慮した計算が可能となる。
もう少し具体例を挙げて考える。
Source文が「I like playing soccer」、Target文が「私/は/サッカー/が/好き/です」とする。
EncoderではSource文でのAttentionを計算する。
次に、Decoderでは、Source文とTarget文でAttentionを計算する。
query, key, valueはそれぞれ以下の通り。
※Source文からkey, valueが、Target文からQueryが作られる。
attention_weight()は以下のように計算される。
attention_weightの計算を少しだけやってみる。
例えば、attention_weight(0,0)の値は次のように計算できる。
以下、同様である。
先頭のattention_weightは、Target文の「私」とSource文の各単語の内積を計算した結果となる。
このattention_weightとvalueの行列計算を行う。
attention_weightがこのような値の場合、水色のスカラー値はvalueの「soccer」の値に強く影響される一方、その他の値は0.1でスケーリングされるため、ほとんど影響をうけなくなる。
学習がうまくいくと、以降のネットワーク計算において、最も確率の高い単語として「サッカー」が出力されるようになる。
Mask
ターゲット文が「<BOS> 私 は サッカー が 好き <EOS>」の場合、
「<BOS> 私 は サッカー が 好き」としていた。
これは、時刻tのtokenから時刻t+1のtokenを予測するためであり、
「<BOS> 私 は」の情報から次のtokenが「サッカー」であることを予測していた。
つまり「<BOS> 私 は」の時点で「サッカー」という未来の情報は知るはずがない。
学習時は文として与えられるが、推論時は未来の情報は分からない。
このため、未来の情報にMaskをすることでリークを防ぐ。
Position-wise Feed-Forward Networks
attentionが理解できれば、この層は難しくない。
residual connection
ResNetで用いられている構造。
前の層の入力を足し合わせる。
これは「Positional Encoding & Multi-Head Attention」と「Multi-Head Attention & Position-wise Feed-Forward Networks」で利用されている。
実装例は以下の通り。
class SublayerConnection(nn.Module): """ A residual connection followed by a layer norm. Note for code simplicity the norm is first as opposed to last. """ def __init__(self, size, dropout): super(SublayerConnection, self).__init__() self.norm = LayerNorm(size) self.dropout = nn.Dropout(dropout) def forward(self, x, sublayer): "Apply residual connection to any sublayer with the same size." return x + self.dropout(sublayer(self.norm(x)))
これをEncoder内部で呼ぶ。
class EncoderLayer(nn.Module): "Encoder is made up of self-attn and feed forward (defined below)" def __init__(self, size, self_attn, feed_forward, dropout): super(EncoderLayer, self).__init__() self.self_attn = self_attn self.feed_forward = feed_forward self.sublayer = clones(SublayerConnection(size, dropout), 2) self.size = size def forward(self, x, mask): "Follow Figure 1 (left) for connections." x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask)) return self.sublayer[1](x, self.feed_forward)
Positional Encodingの出力がx
となる。
これをEncoder,Decoderで6層ずつ積んでいる。
こんな感じでTransformerモデルが出来る。
今更だが、attentionとか良い勉強になった。