LoginSignup
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-05-20

はじめに

昔まだ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

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

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

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は、以下の3つの要素から成り立っています。

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

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

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

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

実際のコード

ソースコード全体はGithubに置いてあります。


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が公開された当時に比べると、非常にシンプルに記述できるようになりました。

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

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2