双方向LSTMを使用したAttention付きseq2seq

「ゼロから作るDeep Learning自然言語処理編」を読み終えました。
この本の後半に双方向LSTM(TimeBiLSTM)を使用してAttention付きseq2seqを作ってみてねとありましたので、作ってみました。

まず、AttentionBiSeq2Seq, AttentionBiEncoder, AttentionBiDecoderというクラスを
作成し、AttentionBiSeq2Seqを動かすためのtrain_bi_seq.pyファイルを作成しました。
間違っているところがあるかも知れませんが、一応動きます(汗)ので、勉強メモを兼ねて掲載します。

まず、attention_bi_seq2seq.pyです。AttentionBiSeq2Seq, AttentionBiEncoder, AttentionBiDecoderの三つのクラスを持ちます。
詳しくは元のサンプルソース attention_seq2seq.pyと比較してみて下さい。

# coding: utf-8
import sys
sys.path.append('..')
from common.time_layers import *
from ch07.seq2seq import Encoder, Seq2seq
from ch08.attention_seq2seq import *

class AttentionBiSeq2seq(Seq2seq):
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        args = vocab_size, wordvec_size, hidden_size
        self.encoder = AttentionBiEncoder(*args)
        self.decoder = AttentionBiDecoder(*args)
        self.softmax = TimeSoftmaxWithLoss()

        self.params = self.encoder.params + self.decoder.params
        self.grads = self.encoder.grads + self.decoder.grads

class AttentionBiEncoder(AttentionEncoder):
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        embed_W = (rn(V, D) / 100).astype('f')

        # TimeBiLSTMを使うための設定。双方向だから、重みの数が普通のLSTMの二倍。
        lstm_Wx1 = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh1 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b1 = np.zeros(4 * H).astype('f')

        lstm_Wx2 = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b2 = np.zeros(4 * H).astype('f')

        self.embed = TimeEmbedding(embed_W)
        self.lstm = TimeBiLSTM(lstm_Wx1, lstm_Wh1, lstm_b1, lstm_Wx2, lstm_Wh2, lstm_b2, stateful=False)

        self.params = self.embed.params + self.lstm.params
        self.grads = self.embed.grads + self.lstm.grads
        self.hs = None  

class AttentionBiDecoder(AttentionDecoder):
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        embed_W = (rn(V, D) / 100).astype('f')

        # Encoderからの入力が2倍になるため、重みの数も調整。
        lstm_Wx = (rn(D, 2 * 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh = (rn(2 * H, 2 * 4 * H) / np.sqrt(2 * H)).astype('f')
        lstm_b = np.zeros(2 * 4 * H).astype('f')
        affine_W = (rn(2 * 2 * H, V) / np.sqrt(2 * 2 * H)).astype('f')
        affine_b = np.zeros(V).astype('f')

        self.embed = TimeEmbedding(embed_W)
        self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True)
        self.attention = TimeAttention()
        self.affine = TimeAffine(affine_W, affine_b)
        layers = [self.embed, self.lstm, self.attention, self.affine]

        self.params, self.grads = [], []
        for layer in layers:
            self.params += layer.params
            self.grads += layer.grads        

次はtrain_bi_seq.pyです。元のサンプルソースtrain_seq.pyとの違いはimportとAttentionBiSeq2seqのところだけです。

# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
import matplotlib.pyplot as plt
from dataset import sequence
from common.optimizer import Adam
from common.trainer import Trainer
from common.util import eval_seq2seq
from attention_bi_seq2seq import AttentionBiSeq2seq
from ch07.seq2seq import Seq2seq
#from ch07.peeky_seq2seq import PeekySeq2seq


# データの読み込み
(x_train, t_train), (x_test, t_test) = sequence.load_data('date.txt')
char_to_id, id_to_char = sequence.get_vocab()

# 入力文を反転
x_train, x_test = x_train[:, ::-1], x_test[:, ::-1]

# ハイパーパラメータの設定
vocab_size = len(char_to_id)
wordvec_size = 16
hidden_size = 256
batch_size = 128
max_epoch = 10
max_grad = 5.0

model = AttentionBiSeq2seq(vocab_size, wordvec_size, hidden_size)
# model = Seq2seq(vocab_size, wordvec_size, hidden_size)
# model = PeekySeq2seq(vocab_size, wordvec_size, hidden_size)

optimizer = Adam()
trainer = Trainer(model, optimizer)

acc_list = []
for epoch in range(max_epoch):
    trainer.fit(x_train, t_train, max_epoch=1,
                batch_size=batch_size, max_grad=max_grad)

    correct_num = 0
    for i in range(len(x_test)):
        question, correct = x_test[[i]], t_test[[i]]
        verbose = i < 10
        correct_num += eval_seq2seq(model, question, correct,
                                    id_to_char, verbose, is_reverse=True)

    acc = float(correct_num) / len(x_test)
    acc_list.append(acc)
    print('val acc %.3f%%' % (acc * 100))


model.save_params()

# グラフの描画
x = np.arange(len(acc_list))
plt.plot(x, acc_list, marker='o')
plt.xlabel('epochs')
plt.ylabel('accuracy')
plt.ylim(-0.05, 1.05)
plt.show()

要は、本で紹介しているTimeBiLSTMは、Encoderで呼ばれて、
翻訳前の言葉に対する左方向のLSTM結果と右方向のLSTM結果を結合(concatenate)し、
Axis:0のサイズが通常Encoderの2倍のhsを返します。
Decoderではこのhsのサイズに合わせて重みのサイズを調整する必要があります。

もし、Encoderで左右LSTMの結果を結合(concatenate)せず、平均値を出力するなら、重みのサイズは変更せず、済んだでしょう。

前編の「ゼロから作るDeep Learning: Pythonで学ぶディープラーニングの理論と実装」もよかったですが、
自然言語処理編もとても分かりやすくてよかったです。
ダウンロードしたソースコードも問題なく動きました。

これまでDeep Learning関連の本(数学・統計なども含めて)を十冊以上読でいて、
少し分かる気はしますが、まだまだ足りないと思うので、今はKeras関連の本を買って勉強しているところです。