最新のChainerでそれなりに楽してAttention Sequence to Sequence

# はじめに
昔まだChainerがver1や2だった頃、Sequence to sequence (以降、seq2seq)モデルの実装に四苦八苦していました。
ましてや、Attention Seq2seqを組むなんて夢のまた夢でした。（まあそれでも組みましたが

という過去にものすごく苦戦した思い出があるのですが、最近Seq2seqモデルを利用する機会があり、せっかくなので昔のモデルを使いまわしをせずに最新のChainerの恩恵を受けた新しいモデルを作成することにしましたとさ。

# 環境など

- Python 3.7.2
- Chainer 5.3.0（投稿時ver6も出てますが、satableで最新ということで・・・）


# 通常のSeq2Seqモデル

Chainer公式からexampleが出ているので、そちらを参照されたい。
[https://github.com/chainer/chainer/tree/master/examples/seq2seq](https://github.com/chainer/chainer/tree/master/examples/seq2seq)

なお、この記事は↑のサンプルがある程度理解できる方を対象にしています。


学習でのSeq2seqモデルの要点をまとめると、

```python
class Seq2seq(chainer.Chain):
(中略)

    def forward(self, xs, ys):
（中略）
        # None represents a zero vector in an encoder.
        hx, cx, _ = self.encoder(None, None, exs)
        _, _, os = self.decoder(hx, cx, eys)
（以下略）
```

なんと、seq2seqの複雑なネットワークがたった2行で記述できてしまいます。（昔はもっと大変だったのになぁ・・・

もちろん、encoderやdecoderの定義、入力データの処理は必要ですが、それでも随分と楽になったものです。

`exs`へは要素が入力文の単語ベクトルのnumpy.arrayをミニバッチサイズ分listにしたもので、`os`には`eys`の長さに対応する隠れ層のベクトルが出力されます。

shapeっぽいのは以下の通り。（listなので.shapeは使えません）

- exs's shape -> [(input_len, word_vector) * batch_size]
- eys's shape -> [(output_len, word_vector) * batch_size] 
- os's shape -> [(output_len, hidden_layer_vector) * batch_size] 

# Attention化する

元論文（さらに大元があるけど）である[Effective Approaches to Attention-based Neural Machine Translation](https://aclweb.org/anthology/D15-1166)は、以下の3つの要素から成り立っています。

1. Global Attention
2. Local Attention
3. Input Feeding

一般的にAttentionと呼ばれるものは(1)と(2)で、(3)はおまけみたいなものです（入れようと思えば通常のSeq2seqでも導入できます）。
実際に評価では(3)を入れたり入れなかったりして性能を確かめています（なお入れたほうが性能は高い模様）。

今回は以下の理由で一番簡単なGlobal Attentionのみを実装します。

- 適応予定のデータの文長が短い（高々１０単語 → Local Attentionの効果は低い？）
- リソースの兼ね合い上、RNNのレイヤーを高く積まない（1層の予定 → Input Feedingの効果は低い？）
- **学習速度を重視したい**（すべての処理をミニバッチ単位で処理し、forループを避けたい）
- ~~できるだけ実装をさぼりたい~~


## 実際のコード

ソースコード全体は[Github](https://github.com/kazuaki-i/attention_seq2seq)に置いてあります。

```python

import numpy

import chainer
import chainer.functions as F
import chainer.links as L

UNK, EOS, BOS = 0, 1, 2

class AttentionSeq2Seq(chainer.Chain):
  def __init__(self, n_layers, n_source_vocab, n_target_vocab, n_units):
      super(AttentionSeq2Seq, self).__init__()
      with self.init_scope():
          self.embed_x = L.EmbedID(n_source_vocab, n_units, initialW=chainer.initializers.Uniform(.25), ignore_label=-1)
          self.embed_y = L.EmbedID(n_target_vocab, n_units, initialW=chainer.initializers.Uniform(.25), ignore_label=-1)
          self.encoder = L.NStepLSTM(n_layers, n_units, n_units, 0.1)
          self.decoder = L.NStepLSTM(n_layers, n_units, n_units, 0.1)
          self.Wd = L.Linear(n_units, n_target_vocab)
          self.Wa = L.Linear(n_units*2, n_units)
          self.attention_hw = L.Linear(n_units*2, 1)
          
      self.n_layers = n_layers
      self.n_units = n_units
      self.n_inf = -100000000

  def forward(self, xs, ys, train=True):
      x_len_lst = [len(x) for x in xs]
      y_len_lst = [len(y) for y in ys]
      batch_size, x_len, y_len = len(xs), max(x_len_lst), max(y_len_lst) + 1

      # shaping inputs
      xs = [self.xp.array(x[::-1], numpy.int32) for x in xs]
      eos = self.xp.array([EOS], numpy.int32)
      bos = self.xp.array([BOS], numpy.int32)
      ys_in = [F.concat([bos, self.xp.array(y, numpy.int32)], axis=0) for y in ys]
      ys_out = [F.concat([self.xp.array(y, numpy.int32), eos], axis=0) for y in ys]

      # Both xs and ys_in are lists of arrays.
      exs = sequence_embed(self.embed_x, xs)
      eys = sequence_embed(self.embed_y, ys_in)

      # basic encoder & decoder
      hx, c, xos = self.encoder(None, None, exs)
      _, _, yos = self.decoder(hx, c, eys)

      # shaping for attention
      xo = F.pad_sequence(xos, padding=0.)
      yo = F.pad_sequence(yos, padding=0.)
      xo = F.reshape(xo, (batch_size, x_len, 1, self.n_units))
      yo = F.reshape(yo, (batch_size, y_len, 1, self.n_units))

      yo = self.global_attention(xo, yo, x_len, y_len, batch_size)

      yos = [F.reshape(h[:, :len(y), :], (len(y), h.shape[-1]))
            for h, y in zip(F.split_axis(yo, batch_size, axis=0), ys_out)]

      # calculate loss
      concat_os = F.concat(yos, axis=0)
      concat_ys_out = F.concat(ys_out, axis=0)
      loss = F.sum(F.softmax_cross_entropy(
          self.Wd(concat_os), concat_ys_out, reduce='no')) / batch_size

  def global_attention(self, xo, yo, x_len, y_len, batch_size):
      def _batch_axis(v):
          return len(v.shape) - 1
      # shaping for calculate weighted average
      eh = F.repeat(xo, y_len, axis=1)
      hh = F.tile(yo, (1, x_len, 1, 1))

      h = F.concat([eh, hh], axis=-1)
      cond = self.xp.concatenate([self.masking(eh), self.masking(hh)], axis=-1)
      h = F.where(cond, F.tanh(h), self.xp.full(cond.shape, 0., self.xp.float32))

      # apply attention Liner W and softmax
      h = self.attention_hw(h, n_batch_axes=_batch_axis(h))

      h = F.reshape(h, (batch_size, x_len, y_len, 1))
      cond = self.xp.reshape(self.xp.all(cond, axis=-1), h.shape)

      h = F.where(cond, h, self.xp.full(h.shape, float(self.n_inf), self.xp.float32))
      h = F.softmax(h, axis=1)
      h = F.where(cond, h, self.xp.full(h.shape, 0., self.xp.float32))

      # Calculate weighted average
      h = h[:, :, :, :, None]
      h = F.repeat(h, self.n_units, axis=-1)

      xo = xo[:, :, None, :, :]
      xo = F.repeat(xo, y_len, axis=2)
      h = F.sum(h * xo, axis=1)

      # Apply weighted average to yo
      cond = self.xp.logical_and(self.masking(yo), self.masking(h))

      h = F.concat([yo, h], axis=-1)
      h = F.tanh(self.Wa(h, n_batch_axes=_batch_axis(h)))
      h = F.where(cond, h, self.xp.full(h.shape, 0., self.xp.float32))

      # Shaping input form
      h = F.reshape(h, (batch_size, y_len, self.n_units))

      return h

  def masking(self, x):
      # making mask for padding
      m = self.xp.sum(self.xp.absolute(x.data), axis=-1) > 0.
      m = m[:, :, :, None]
      m = self.xp.tile(m, (1, 1, 1, x.shape[-1]))
      return m


```

約110行！う～ん・・・あんまり楽じゃないかな？

ミニバッチ単位で処理しようとすると、私のコーディング力（ちから）ではこの辺が限界です。なお、一部無駄っぽいような冗長っぽいようなところは、隠れ層の次元を変えたりRNNモデルを切り替えやすくするため（要するにわざと）だったりします（詳しくはGitHubのソースを見てください）。


とはいうものの、昔よりだいぶシンプルに書くことができました。ありがたや～ありがたや～


# まとめ

最新のChainerを使ってAttention Seq2seqモデルを実装しました。

昔のChainerが公開された当時に比べると、非常にシンプルに記述できるようになりました。

今後の改良・発展にも期待しています！

# 参考

- [Chainer](https://chainer.org/)
- [Sequence-to-sequnce learning example for machine translation（Chainer 公式 Example）](https://github.com/chainer/chainer/tree/master/examples/seq2seq)
- [今更ながらchainerでSeq2Seq（2）〜Attention Model編〜](https://qiita.com/kenchin110100/items/eb70d69d1d65fb451b67)