よくわかる注意機構(Attention)

0. この記事はなに

eeic (東京大学工学部電気電子・電子情報工学科)その2 Advent Calendar 2018 - Qiitaの八日目の記事です。

この記事はNLP自然言語処理)界隈でよく使われるAttention(注意機構)の解説記事になります。
最近、Attention is All You Needの登場でAttentionがさらに話題になっています。 ここでは、上記の『Attention is All You Need』内で登場しているSelf-Attentionの前身になる2種類のAttentionについて解説したいと思います。 間違っている部分があればご指摘頂ければ幸いです。

1. Attentionとは(お気持ち的な話)

まず、技術的な話に入る前に、ふわっとしたお話をしましょう。
次のような英文を日本語に要約する事を考えます。

The lectures in eeic are very hard.

翻訳例としては、

eeicでの授業はとても大変だ。

となるでしょうか(僕は英語が苦手なので間違ってたらごめんちゃい)。
みなさんが翻訳するとき、例えば、lectures授業と訳すときは、もちろんlecturesという単語に注目していると思います。もしかしたら、その直前のTheや直後のin eeicまで見ているかもしれません。
また、hardを訳すときには、当然直前のveryにも注目すると思いますし、主語のThe lecturesにも注目するかもしれません。

このように、何かを翻訳した結果(ここでは日本語)を出力する際に、どこに注意を向けているかは異なります。
この考え方を NeuralNetwork を用いたモデルにも応用してやろうとしたのが Attention です。
こう聞くと、かなり当たり前のことをしているように思いませんか?

2. Attention とは(理屈的なお話)

さて、ここからはかなりしっかりと Attention の話をしていきたいと思います。
Attention は日本語では「注意機構」と表されます。どこに注意を向けるかを決める機構なわけです。
Atetntion は主に Encoder-Decoder モデル(seq2seq)と一緒に使われていることが多いです。というか、 Attention が現れた背景に Encoder-Decoer モデルの能力の限界があります。Encoder-Decoder モデルはここのサイトなどを読むと分かりやすいと思います。簡単に言うと、Encoder と Decoder という2つの RNN からなるモデルで、機械翻訳などによく使われています。Attention をしっかりと理解するには、(恐らく)Encoder-Decoder モデルを理解していた方がスムーズだと思います。

さて、Attention の説明をするために、まず、Attention が考案された背景を軽く説明します。
普通の Encoder-Decoder モデルでは、次図のように Decoder は Encoder から最後の隠れ層の状態だけを受け取ります。

f:id:hilinker:20181118161910p:plain

f:id:hilinker:20181118161910p:plain
[1]よりEncoder-Decoderモデル

これによって Encoder 側で入力をエンコードした情報を Decoder で利用できるのですが、実は若干の問題があります。 それは、入力を固定長のベクトルでしか表現できないということです。 どういうことかと言うと、Encoder の出力は隠れ層hなのですが、このサイズは固定です(400とか512とか)。 それゆえ、入力系列の長さが毎回違うようなデータセットだと、どうしても情報を適切に圧縮しきれなくなってきます*1
また、折角なので Encoder の最後の隠れ層の状態だけでなく、途中の隠れ層の状態も利用したくなってきます。
さあ、どうしましょう。

これが、Attention が考案された背景です。

では、この問題を解決しましょう。 入力系列に応じた隠れ層のサイズを得る手段として、Encoder の最後の隠れ層の状態だけでなく、全ての隠れ層の状態を利用します。 具体的には、Encoder の全ての隠れ層を重み付けして足し合わせます。

これが、Attention の基本的な考え方です。

この後、具体的な Attention の原理について説明しますが、実は Attention には大きく分けて2つのバリエーションがあります[3][4]。 Luong の Attention と Bahdanau の Attention です。これらの Attention の違いは、重み付けのやり方です。
もちろん、マイナーチェンジはたくさんありますが大きく枠組みの違う Attention と言うとこの2つになるでしょう。 この記事ではちゃんと両方説明します。

まとめ

  • Attention は Encoder-Decoder モデルの性能を上げたくて考案された。
  • Attention は Encoder の最後の隠れ層だけでなく、全ての隠れ層を重み付け和にして使う。

3. LuongのAttention

Luong の Attention では次のように出力系列を得ます。
まず、入力系列をEncoderに通して各時刻の隠れ層を得ます。これを、  h^e_0 , h^e_1, h^e_2, ... , h^e_n としましょう。LSTM を利用する場合、各時刻sの状態は隠れ層  h^e_s とセル c^e_s からなりますが、ここではまとめて、h^e_s と表します。ここは普通の Encoder-Decoder モデルと同じです。
次に、Decoder 側で出力系列を得ます。まず、普通の Encoder-Decoder モデルと同じように、Decoder は Encoder の最後の出力 h^e_n を受け取ります。
ここから、ようやく Attention 機構が活躍します。
Luong の Attention では、Decoder の時刻tの出力を決定するときに Encoder の隠れ層の重み付け和を利用します。この重み付け和を context vector c_t と呼びます。 最終的に Decoder で時刻tに得られる隠れ層の出力は、次の式で表されます。

 \begin{align} \tilde{h_t} = \tanh (W_c  [ c_t ; h^d_t ] ) \tag{1} \end{align}

ここで、  W_c は行列、  h^d_t は時刻tにおけるデコーダーの隠れ層です。また、; は concat (2つのベクトルを単純にくっつけること)を意味しています。つまり、 context vector c_t と、現在の Decoder の隠れ層の状態 h_t を concat したものに行列をかけて、それを tanh に通したものをその時刻における LSTM の出力  \tilde{h_t} として得ているわけですね。
tanh について一応説明しておくと、これは活性化関数の一種です。任意の実数入力を受け取り、 (-1, 1) の値域で動く関数ですね。
最終的に、ここで得られた  \tilde{h_t} を使って、出力する単語を決定します。ここは、普通の Encoder-Decoder モデルと一緒です。 concat でいいの? とか、 tanh 以外の関数じゃダメなの? とか色々と思うと思いますが、そこらへんをちょこっと変えたマイナーチェンジがいっぱいあります。それについてはここでは言及しません。

さて、では式(1)の context vector c_t はどうやって求めましょう。
context vector はその名の通り、Encoder で得られた context(文脈情報)を表現したベクトルです。文脈情報、というのがやや直感的なのですが現在の Decoder の状態に応じて、 Encoder 全体から欲しい情報を引っ張って来る、みたいな気持ちでしょうか。
context vector は、次のような計算によって求められます。


\begin{align}
c_t = \sum_{s=0}^{n} a_t(s) * h^e_s
\tag{2}
\end{align}

 h^e_s は Encoder の時刻sの隠れ層の状態で、それに重み  a_t(s) をかけて足し合わせているわけですね。
context vector はこんなに簡単な式で求まります。これにより、 Encoder の全ての時刻の状態を利用することができます。

では、重み  a_t はどうやって求めるのでしょうか。
これには色々な求め方があります。原論文[3]ですら、複数の求め方を提示しています。


\begin{align}
\begin{aligned} \boldsymbol { a } _ { t } ( s ) & = \operatorname { align } \left( \boldsymbol { h } ^ { d } _ { t } , \boldsymbol { h } ^ { e } _ { s } \right) \\ & = \frac { \exp \left( \operatorname { score } \left( \boldsymbol { h } ^ { d } _ { t } ,  \boldsymbol { h } ^ { e } _ { s } \right) \right) } { \sum _ { s ^ { \prime } } \exp \left( \operatorname { score } \left( \boldsymbol { h } ^ { d } _ { t } ,  \boldsymbol { h } ^ { e } _ { s ^ { \prime } } \right) \right) }   \end{aligned}
\tag{3}
\end{align}

\begin{align}
\operatorname { score } \left( \boldsymbol { h } ^ { d } _ { t } , \boldsymbol { h } ^ { d } _ { s } \right) = \left\{ \begin{array} { l l } { \boldsymbol { h } _ { t } ^ { d \top }  \boldsymbol { h } ^ { e } _ { s } } & { \text { dot } } \\ { \boldsymbol { h } _ { t } ^ { s \top } \boldsymbol { W } _ { a }  \boldsymbol { h } ^ { e } _ { s } } & { \text { general } } \\ { \boldsymbol { v } _ { a } ^ { \top } \tanh \left( \boldsymbol { W } _ { a } \left[ \boldsymbol { h } ^ { d } _ { t } ; \boldsymbol { h } ^ { e } _ { s } \right] \right) } & { \text { concat } } \end{array} \right.
\tag{4}
\end{align}

順番に見ていきましょう。
式(3)で  a_t(s) を求めています。align という関数に、Encoder と Decoder の隠れ層を入力していますが、要するに Encoder の全ての隠れ層について Score を計算してやり、その Score について Softmax をとっているだけです。つまり、重みの和を足して1になるようにしているわけですね。
そして、この Score 関数は式(4)で表されるように色々なものが考えられます。どれが一番いいのか、というのは原論文でも明確な断定はしていませんでしたが、general, dot, concat の順で結果が良かったそうです。差はそんなに大きくなく、やっぱりどれが一番かは決められなさそうです。*2

3. Bahdanau の Attention

お次は Bahdanau の Attention です。ほぼ同時期かちょっと Luong より早い時期に発表されました。
これも基本的な考え方は Luong の Attention と全く変わりません。
まず一点違うのは、 a_t(s) を計算するときに現在の Decoder の隠れ層  h^d_t ではなく、1つ前の時刻の Decoder の隠れ層  h^d_{t-1} を利用します。
つまり、式(3)が次のように変わります。


\begin{align}
\begin{aligned} \boldsymbol { a } _ { t-1 } ( s ) & = \operatorname { align } \left( \boldsymbol { h } ^ { d } _ { t-1 } , \boldsymbol { h } ^ { e } _ { s } \right) \\ & = \frac { \exp \left( \operatorname { score } \left( \boldsymbol { h } ^ { d } _ { t-1 } ,  \boldsymbol { h } ^ { e } _ { s } \right) \right) } { \sum _ { s ^ { \prime } } \exp \left( \operatorname { score } \left( \boldsymbol { h } ^ { d } _ { t-1 } ,  \boldsymbol { h } ^ { e } _ { s ^ { \prime } } \right) \right) }   \end{aligned}
\tag{5}
\end{align}

式(4)も同様に、 h^d_t h^d_{t-1} になるだけです。
この計算により、context vector c_t が得られるわけですが、これをどう扱うかも Luong のときと違います。
Bahdanau の Attention では、この context vector h^d_{t-1} と一緒に、次の層に入力します。少し分かりにくいですが、画像にすると次のようになります。

f:id:hilinker:20181121195334p:plain
[4]より Attention の図。下が Encoder を、上が Decoder を表している。

論文からそのまま画像を持ってきたため、変数に使っているアルファベットが少し違いますがどういう流れかは分かって頂けると思います。
Encoder の隠れ層を重み付けして足し合わせた context vector と、時刻t-1の Decoder の隠れ層の値が一緒に時刻tの隠れ層に入っています。2つの矢印が合流している場所で行なっている演算は、色々な方法が考えられますが、ここでは concat をしていると考えてください。
以上が、 Bahdanau の Attention です。

4. Luong と Bahdanau どっちがいいの?

分かりません。
google scholar で調べたところ、 Luong の論文は被引用件数は1331、Bahdanau の論文は被引用件数5159でした。
ここからは個人の感想です。
これだけ見ると Bahdanau の方が圧倒的に良さそうに見えますが、実際どっちもそこそこ見ます。軽く調べた範囲で、両者の性能を厳密に比較しているような論文は見当たらなかったので、研究者の人も結構お気持ちで使っていそうです。
両方とも Encoder 全体の情報を見ていると点で、扱える情報にはそんなに違いはないのかもしれません。

5. 最後に

参考文献[5]に、pytorch で両者の実装を行なっているチュートリアルが図解付きであります。もし言語系をやりたいのであれば、一度実装してみて損はないと思います。
冒頭述べた『Attention is all you need』内では、ここで説明したものを発展させた self-attention という機構を用いています。これが相当に強力らしく、今後は self-attention を用いた transformer と言うモデルが幅をきかせていき、Encoder-Decoder モデルは廃れていくのかなぁとも思います。
もし分からないことがあれば、私の Twitter アカウントに質問を投げてくだされば、分かる範囲でお答えするのでお気軽にどうぞ。
また、間違っている点などあればコメントなどでご指摘いただけると嬉しいです。

参考文献

[1] Encoder-decoderモデルとTeacher Forcing,Scheduled Sampling,Professor Forcing
[2] ゼロから作るDeepLearning2, 斉藤康毅, O.REILY
[3] Effective Approaches to Attention-based Neural Machine Translation, Luong et al.
[4] NEURAL MACHINE TRANSLATION BY JOINTLY LEARNING TO ALIGN AND TRANSLATE, Bahdanau et al.
[5] Translation with a Sequence to Sequence Network and Attention — PyTorch Tutorials 1.0.0.dev20181206 documentation

*1:お気持ちとしては、長い文には大きいサイズのベクトルを、短い文には小さいサイズのベクトルを割り当てるみたいなことをしたい。

*2:原論文中で、「concat はそんなに良くなかったよ!」って書いてありましたが、言うてそんな違うか? 程度の差しかなかったので、やっぱり一概には言えないかなと思いました。