[TensorFlow] Seq2Seqを使って「死後さばきにあう」風メッセージを量産できた気がした

# はじめに

以前の記事で、文字ベースのLSTMを使って「キリスト看板」風の文章を作成する実験をしました。
[\[TensorFlow\] LSTMで「死後さばきにあう」風メッセージを量産してみた - Qiita](https://qiita.com/everylittle/items/826e147ce0b545122b26)

- 学習データに出てくる文字しか出力できない
- 学習データと全く同じ文章も結構出てくる
- 文法的におかしい文や、意味のおかしい文も出てくる
- 学習データ自体が非常に少ない

といったようにいろいろ課題が出てきたので、今度は文全体を入力とするモデルを試してみたいと思います。
最終的にはGAN系の生成モデル（SeqGAN[^1]とか）を試していければいいのでしょうが、いきなりは挫折しそうなので、まずは文単位で入出力する考え方の練習も兼ねてSeq2Seqのモデルを試してみます。

[^1]: [\[1609.05473\] SeqGAN: Sequence Generative Adversarial Nets with Policy Gradient](https://arxiv.org/abs/1609.05473)

# 検証環境

今回はGPUがないと学習が苦しいので、無料で使えるGoogle Colaboratoryにて実行します。

- Google Colaboratory
    - TensorFlow 2.2.0-rc3
    - ランタイム: **GPU**

# 戦略

機械翻訳などで使われるSeq2Seqモデルを使ってみます。
基本はこちらのチュートリアルをベースに作ります。
[Keras : Ex-Tutorials : Seq2Seq 学習へのイントロ – PyTorch](http://torch.classcat.com/2018/07/02/keras-ex-tutorials-seq2seq-intro/)
今回は入力を単語とするので、「整数シークエンスを持つ単語レベル・モデルを使用することを望む場合」を参考にコードを変更します。

Seq2Seqで文章を生成する場合、何を入力文にするのかという問題がありますが、別の記事で書かれている方法を参考に

- 最初の1文を入力して、出力を得る
- 出力された文章を次に入力し、また出力を得ることを繰り返す

という方法で使うことにします。
[Seq2Seqを利用した文章生成(訓練にWikipediaデータを使用) - Qiita](https://qiita.com/gacky01/items/064c0071e5cff87e5c08)

Seq2Seqモデルを含めたEncoder-Decoderモデルの解説は、こちらが分かりやすかったので丸投げ。
[PyTorchでEncoder-Decoder - Qiita](https://qiita.com/imaharu/items/9f206a8f470897d5458c)

この記事によると、入力データの各単語はEmbedding層によってまずベクトルに変換され、そのベクトル表現の系列を使って学習が行われます。本来は各単語のベクトル表現もデータから学習すべきところなのですが、そんなにデータはないので、学習済みのWord2Vecモデルにより代用します。

今回は、こちらのWord2Vecモデルを使いました。[^w2v]
[shiroyagicorp/japanese-word2vec-model-builder: A tool for building gensim word2vec model for Japanese.](https://github.com/shiroyagicorp/japanese-word2vec-model-builder)

[^w2v]: 日本語の学習済みWord2Vecモデルは色々あるので、形態素の単位（活用語尾を分割するとかしないとか）が学習データと合ってさえいれば他のものでも使えるはずです。→[学習済み日本語word2vecとその評価について - 株式会社ホクソエムのブログ](https://blog.hoxo-m.com/entry/2020/02/20/090000)

>A trained word2vec model is available at:
http://public.shiroyagi.s3.amazonaws.com/latest-ja-word2vec-gensim-model.zip

とあるので、このzipファイルをダウンロードして使います。

このモデル、33万語ほどの単語がそれぞれ50次元ベクトルで表されたデータが入っています。
このモデルを使うと、ここに入っている単語はどれでも出力される可能性があるわけです。
Word2Vecの表現ベースで学習するので、学習データになくても、近い意味の単語はそれなりの確率で出てくるかもしれません。**「ネコと和解せよ」も夢じゃない？**

# プログラム

まずは必要なモジュールの ``import`` から。

```python
import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dense, Activation, LSTM, Embedding
from tensorflow.keras.optimizers import RMSprop
from tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau
import numpy as np
import random
import sys
import pickle
from gensim.models import Word2Vec
```

## データ準備

ある意味いちばん大事なデータ。

**「聖書配布協力会」様の掲示している看板、および彼らが街頭で活動する際に掲げているプラカードの文章（を書き写したもの）を利用させていただきます。（前回と同じです）**

```python
text = """
ああ 世 世 世 神 の ことば を 聞け
悪 の 報い は 死 です
悪 欲 の 人 は 神 を 認め ない
悪 を まく 者 は 災 を 刈る
あなた の 神 は ただ ひとり
あなた の 造り 主 に 会う 備え を なせ
あなた の 造り 主 を 覚えよ
イエス・キリスト は あなた の 造り 主
イエス・キリスト は 永遠 の 望み を 与える
イエス・キリスト は 神 の ひとり 子
イエス・キリスト は 神 の 御子
イエス・キリスト は 唯一 の 神
イエス・キリスト は 世 を 正しく さばく
イエス・キリスト を 呼び 求める 者 は 救わ れる
永遠 の 命
永遠 の 命 の 源
永遠 の 神
永遠 の 救い の 源
永遠 へ の 希望
終り の 日 に 神 の 前 に 立つ
終り の 日 に 人 は 神 の 前 に 立つ
神 が 人類 を さばく 日 は 近い
神 と 和解 せよ
神 に対する 罪 を 悔い改め よ
神 の 国 と 神 の 正義 を 求め なさい
神 の 国 と 正義 を 求めよ
神 の 国 は 近づい た
神 の 国 は 近づい た 悔い改め よ
神 の 言 を 拒む もの は 死 を 好む
神 の さばき は 突然 に くる
神 の 正しい さばき の 日 は 近い
神 の ひとり 子 イエス・キリスト は 救世主
神 は 言っ て いる ここ で 死ぬ 定め で は ない と
神 は 心 を 見る
神 は 罪 を 罰する
神 は ひとり 子 キリスト を 世に 遣わさ れ た
神 は 御子 キリスト を 世に 遣わさ れ た
神 は 唯一
神 は 世 の 知恵 を 愚か に さ れ た
神 は 世 を さばく 日 を 定め た
神 へ の 態度 を 悔い改め よ
神 を 畏れ 敬 え
神 を 恐れ 敬 え
神 を 遠ざかる 者 は 悪 の 道 に 入る
神 を 認め よ
神 を 求めよ
考え て 下さい 死後 の 行先
今日 は 救 の 日
キリスト 以外 に 救い は ない
キリスト が 人 を さばく 日 は 近い
キリスト が 真 の 神
キリスト が 道 真理 命
キリスト の 再臨 は 近い
キリスト の 血 罪 を 清める
キリスト の 血 は 罪 を 清める
キリスト の 血 は 罪 を 清める
キリスト の 血 は 罪 を 清める
キリスト の 血 は 罪 を 取り除く
キリスト の ほか に 神 は ない
キリスト の 甦り は 救い の 確証
キリスト は あなた に 永遠 の 命 を 与える
キリスト は あなた を 義 と する
キリスト は あなた を 罪 から 解放 する
キリスト は 永遠 の 命 を 与える
キリスト は 神 の 御子
キリスト は 十字架 で 人 の 罪 を 負っ た
キリスト は 真 の 神 の 分身
キリスト は すぐ に 来る
キリスト は すぐ に くる
キリスト は 罪 を 取り消し 命 を 与える
キリスト は 罪 を 取り消す
キリスト は 罪 を 赦し 永遠 の 命 を 与える
キリスト は 墓 から よみがえっ た
キリスト は 墓 から 甦っ た
キリスト は 人 の 身代わり に 罪 を 負っ た
キリスト は 再び き て 世 を さばく
キリスト は 再び 来 て 世 を さばく
キリスト は 再び くる
キリスト は 再び 来る
キリスト は 復活 さ れ た
キリスト は 真 の 神
キリスト は 真 の 神 の 分身
キリスト は 身代わり に 罪 を 負っ た
キリスト は 道 真理 命
キリスト は 甦り 永遠 の 命 を 与える
キリスト は 甦り 死 に 打ち勝っ た
キリスト を 信じる 人 は 救わ れる
キリスト を 信じる 者 は 永遠 の 命 を 持つ
キリスト を 呼び 求める 者 は 救わ れる
悔い改め よ
悔い改め よ
偶像 崇拝 は 罪 です
心 が 清い 人 は 幸い
心から 神 を 信じ なさい
心 の 罪 も 神 は さばく
地獄 の 消え ない 火 を 逃れよ
地獄 は 永遠 の 苦しみ
地獄 は 第 二 の 死
死後 さばき が ある
死後 さばき に あう
死後 さばき に あう
死後 の 行き先 を 考え て 下さい
死後 の 行先 を 考え て 下さい
私生活 も 神 は 見 て いる
死 の 道 と 命 の 道 が ある
死 は 罪 の 報い
主 イエス キリスト の 再臨 は 近い
主 イエス キリスト の 甦り は 救い の 確証
主 イエス キリスト は 万物 の 造り 主
主 の 日 は 突然 来る
人生 は 短い 天 の 国 は 永い
正しい 人 は い ない
ただ 信ぜ よ
ただ 信ぜ よ
堕落 し た 社会 は 神 を 認め ない
地 と 人 は 神 の もの
「 造ら れ た 」 もの を 拝む な
罪 が 清め られ た 人 は 幸い
罪 から 清め られ た 人 は 幸い
罪 と 正義 と さばき について 悟 れ
罪 の まま 死ね ば 永遠 の 地獄 に 行く
罪 の まま 死ね ば 永遠 の 地獄 に 行く
罪 の 報い は 死
罪 の 報い は 死 神 の 賜物 は 永遠 の 命
罪 の 報い は 死 神 の 賜物 は キリスト に ある 永遠 の 命
罪 の ゆるし を 得よ
罪 の 赦し を 得よ
罪 の 赦し を 求めよ
罪 を 神 は 罰する
罪 を 清め られ た 人 は 幸い
罪 を 悔い改め なさい
罪 を 悔い改め よ
天国 か 地獄 か あなた の 行き先 は
天国 は 永遠 の 命 地獄 は 火 の 海
天地 が 滅び て も 私 の ことば は 滅び ない
天地 万物 の 造り 主
天 の 国 か 地獄 か 人 は みな 甦る
天 の 国 は 近い 罪 を 悔い改め なさい
ニセモノ を 警戒 せよ
初め に 神 は 天 と 地 を 造ら れ た
人 が 造っ た 物 は 神 で は ない
人 の 悪 を 取り除く
人 の 内 に 罪 が 宿る
人 の 罪 を 取り除く
人 の 道 も 行い も 神 は 見 て いる
火 の 池 が 第 二 の 死 です
不義 を 贖う
不品行 や 姦淫 を 神 は さばく
不倫 や 姦淫 を 神 は さばく
亡び の 道 と 命 の 道 が ある
曲がっ た 時代 は 神 を 認め ない
真 の 神 は 人 を 愛し その 罪 を 取り除く
真 の 神 を 信じ なさい
真 の 神 を 信じ なさい
道 真理 命
見よ 私 は すぐ に 来る
世 と 世 の 欲 は 亡び ゆく
世 の 終り は 近い
世 の 終り は 突然 に 来る
甦っ た キリスト は 永遠 の 命 を 与える
世 を 正しく さばく
災い なる かな 悪 を 善 という 者
災い なる かな 偶像 を 拝む 者
災い なる かな 酒 を 飲む こと の 英雄
災い なる かな 高ぶる 者
わたし が 道 真理 命
私 の 言葉 に 永遠 の 命 が ある
私 は 命 です
私 は いのち の パン です
私 は 命 の パン です
私 は 死 と 地獄 の 鍵 を 持つ
私 は 真理
私 は 真理 です
私 は 世 の 光
私 を 信じる 人 は 永遠 の 命 を 持つ
わたし を 信じる 者 は 永遠 の 命 を 持つ
私 を 信じる 者 は 永遠 の 命 を 持つ
私 を 信ずる 者 は 死ん で も 生きる
我 は 真理 なり 命 なり
あなた の 創造 主 を 覚え よ
イエス キリスト 以外 に 救い は ない
おそ 過ぎ ない うち に 神 を 呼び 求め よ
終わり の 日 に 人 は 神 の 前 に 立つ
隠れ た 事 も キリスト は さばく
神 が 遣わし た キリスト が 救世主
神 に 帰る なら 神 は 豊か に 赦す
神 に対する 罪 を 悔い改め よ
神 の 国 は 近づい た 悔い改め よ
神 の 裁き の 日 は 近い
神 の 賜物 は 永遠 の 命
神 は 人 を 愛し その 罪 を 取り除く
神 を 畏れ その ことば に 従い なさい
神 を 認め 畏れ 罪 を 離れ よ
キリスト が 永遠 の 命 の ことば を 持つ
キリスト による 救い を 受け入れ なさい
キリスト の 血 は 罪 を 清める
キリスト は 再臨 し 世 を 裁く
キリスト は 十字架 で 人 の 罪 を 負っ た
キリスト は 罪人 を 救う
キリスト は 人 に 永遠 の 命 を 与える
キリスト は 人 の 身代り に 罰 を 受け た
キリスト は 人 を 罪 から 解放 する
キリスト を 信じ 救わ れ なさい
キリスト を 呼び 求める 人 は 救わ れる
悔い改め て 福音 を 信ぜ よ
悔い改め なさい
心 の 底 から 新た に さ れ なさい
死後 の 行き先 を 考えよ
罪 から 清め られ た 人 は 幸い
罪 の 報い は 死
罪 の 赦し を 頂き なさい
罪 を 離れ なさい
罪 を 認め 神 に 帰り なさい
人 が 義 と さ れる ため に キリスト は 甦ら れ た
人 の 心 も 思い も 神 は 見 て いる
人 は 死後 裁き に 会う
世 と 世 の 欲 は 過ぎ去る
わたし 以外 に 神 は い ない
わたし が 道 真理 命 で ある
わたし を 信じる 者 は 永遠 の 命 を 持つ
"""

input_texts = [[w for w in s.split(" ")] for s in text.strip().split("\n")] * 10
```

ここで、``text`` には、基本的には元の文章を ``mecab -Owakati`` で解析した結果を貼り付けています。
ただし、解析結果が誤っているものを目視で修正したり、句読点や記号を消したりしています。
また、表記ゆれや活用形の関係でWord2Vecモデルに含まれない単語も変更しています。例えば

- 「わざわい」→「災い」
- 「あがなう」→「贖う」
- 「たかぶる」→「高ぶる」
- 「まゝ」「たゞ」→「まま」「ただ」
- 「悔い改めよ」→「悔い改め よ」（「よ」は命令形活用語尾なので本来は分けないのですが、「悔い改めよ」が使ったWord2Vecモデルになかったので）
 
などのパターンがありました（これで全部ではありません）。

さらに、``input_texts`` は各セットを10回繰り返したものとしています。
後で出てきますが、これらのデータが入力されたときに、この中のランダムな文を返すように学習を行います。1つの入力から10通りくらいの出力が考えられるようなデータセットを準備します。

## Word2Vecモデル

Word2Vecモデルを読み込みます。
前述のモデルを ``latest-ja-word2vec-gensim-model`` ディレクトリに展開済みとします。パスは適宜変更してください。
Google Driveにモデルを置いておき、マウントして使うのが便利だと思います。
[Google ColaboratoryをGoogle DriveにマウントしてPythonを実行する。 - Qiita](https://qiita.com/asakuraTsukazaki/items/e7eb1f0c43be1e0231c6)

```python
# 白ヤギWord2Vecモデル
model = Word2Vec.load("latest-ja-word2vec-gensim-model/word2vec.gensim.model")
words = ["<PAD>"] + model.wv.index2word
mat_embedding = np.insert(model.wv.vectors, 0, 0, axis=0)
input_token_index = target_token_index = dict((w, i) for i, w in enumerate(words))
num_encoder_tokens = num_decoder_tokens = mat_embedding.shape[0] # for Masking
max_encoder_seq_length = max(len(txt) for txt in input_texts)
max_decoder_seq_length = max_encoder_seq_length + 1 # BOS/EOS
latent_dim = mat_embedding.shape[1]
```

モデルを ``gensim`` で読み込むと、``model.wv.index2word`` と ``model.wv.vectors`` という2つの情報が得られます。

- ``model.wv.index2word`` には、モデルに含まれる語彙（単語リスト）が入っています。
- ``model.wv.vectors`` には、各単語のベクトル表現を並べた行列 (``ndarray``) が入っています。

これも後の都合で、可変長データを入力する関係上、ID: 0 をパディング（長さが足りない部分の穴埋め）のために予約しておきたいので、以下のコードで ID: 0 にダミーの単語とベクトル表現を挿入しています。これによってWord2Vecモデルと比較して単語IDが1つずれます。

```python
words = ["<PAD>"] + model.wv.index2word
mat_embedding = np.insert(model.wv.vectors, 0, 0, axis=0)
```

このモデルに含まれる語彙サイズとベクトルの次元数とで、学習するモデルのサイズが（LSTMの隠れ層（内部状態）の次元数を除いて）決まります。各変数に入るサイズは以下のようになるはずです（2020/05/01現在）。

- ``num_encoder_tokens``, ``num_decoder_tokens`` : 335477
- ``max_encoder_seq_length``: 15
- ``max_decoder_seq_length``: 16
- ``latent_dim``: 50

ここで ``max_decoder_seq_length`` が ``max_encoder_seq_length`` より1大きいのは、Decoder側を学習するときには ``<BOS>`` を先頭に追加して学習する必要があるためです。[^3]

``num_encoder_tokens`` と ``num_decoder_tokens`` はそれぞれ入出力の語彙サイズ（単語の種類）になります。翻訳タスクなどを考えると、``num_encoder_tokens`` が翻訳元、``num_decoder_tokens`` が翻訳先の語彙サイズに相当するので、一般に2つの値は異なりますが、今回は入出力が同じ言語なので同じ値になっています。

[^3]: 念のため、``<BOS>`` と ``<PAD>`` は別物です。``<BOS>`` は文の先頭を表す記号で学習対象ですが、``<PAD>`` は短いデータの後ろに詰めて単語列の長さを合わせるための便宜上の記号であり、学習対象ではありません。

## モデル定義

```python
hidden_dim = 64

# Embedding
layer_emb = Embedding(num_encoder_tokens, latent_dim, trainable=False, mask_zero=True)

# Encoder
encoder_inputs = Input(shape=(None,), dtype=tf.int32)
x = layer_emb(encoder_inputs)
_, state_h, state_c = LSTM(hidden_dim, return_sequences=True, return_state=True)(x)
encoder_states = [state_h, state_c]

# Decoder
decoder_inputs = Input(shape=(None,), dtype=tf.int32)
x = layer_emb(decoder_inputs)
x, _, _ = LSTM(hidden_dim, return_sequences=True, return_state=True)(x, initial_state=encoder_states)
decoder_outputs = Dense(num_decoder_tokens)(x)

def accuracy_masking(y_true, y_pred):
    return tf.keras.metrics.sparse_categorical_accuracy(tf.gather_nd(y_true, tf.where(y_pred._keras_mask)), tf.gather_nd(y_pred, tf.where(y_pred._keras_mask)))

model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
opt = RMSprop(lr=0.01)
model.compile(optimizer=opt, loss=lambda y_true, y_pred: tf.nn.softmax_cross_entropy_with_logits(tf.one_hot(tf.cast(y_true, tf.int32), num_decoder_tokens), y_pred),
              metrics=[accuracy_masking])
# set embedding matrix
layer_emb.set_weights([mat_embedding])
```

入力された単語ID列を ``Embedding`` レイヤーでベクトル表現に変換して、LSTMに渡しています。
この埋め込み行列（各単語のベクトル表現）は前述のように学習済みWord2Vecモデルから持ってきた値を使う[^2]ので、学習中に埋め込み行列が更新されないように ``trainable=False`` で作ります。また、ID: 0をパディング扱いするために、``mask_zero=True`` も付けておきます。

[^2]: レイヤーを作った直後は重み行列が初期化されておらず ``set_weights()`` ができないので、モデルを構築した後にセットします。

そのあとのモデルの作り方は、概ねKerasのチュートリアルと同じです。
ただし、ラベルを（One-hotベクトルではなく）単語IDで与えたいので、損失関数を自分で定義します。後で色々な単語を出やすくするためにスコアを操作したいので、モデルの出力層 (``Dense``) に Softmax を入れないようにして、それに合わせて ``tf.nn.sparse_softmax_cross_entropy_with_logits()`` で損失関数を計算します。[^4]

[^4]: スコアを操作する必要がなければ、``loss='sparse_categorical_crossentropy'`` でも構いません。その場合はモデルの出力層に Softmax を入れます。

また、学習中に表示されるAccuracyの値について、パディング扱いにすべき（無視すべき）データまで計算対象とされてしまうので、それを修正するために ``metrics`` を自作しています。``Tensor`` の ``_keras_mask`` というプロパティを参照するとマスクを取得できることを利用します。
[Masking and padding with Keras  |  TensorFlow Core](https://www.tensorflow.org/guide/keras/masking_and_padding)

## 学習用データ整形

```python
# create data
encoder_input_data = np.zeros(
    (len(input_texts), max_encoder_seq_length),
    dtype='int32')
decoder_input_data = np.zeros(
    (len(input_texts), max_decoder_seq_length),
    dtype='int32')
decoder_target_data = np.zeros(
    (len(input_texts), max_decoder_seq_length),
    dtype='int32')

output_texts = list(input_texts)
random.shuffle(output_texts)
# input one text and randomly output another text
for i, (input_text, target_text) in enumerate(zip(input_texts, output_texts)):
    for t, w in enumerate(input_text):
        encoder_input_data[i, t] = input_token_index[w]

    decoder_input_data[i, 0] = target_token_index['＠'] # BOS
    for t, w in enumerate(target_text):
        # decoder_target_data is ahead of decoder_input_data by one timestep
        decoder_input_data[i, t + 1] = target_token_index[w]
        decoder_target_data[i, t] = target_token_index[w]
    decoder_target_data[i, t + 1:] = target_token_index['。'] # EOS
```

``encoder_input_data``, ``decoder_input_data``, ``decoder_target_data`` の3つの ``ndarray`` を作成します。すべて単語ID列として準備するので、``dtype='int32'`` としています。

前述のように、``input_texts`` は1つの入力列が10回ずつ繰り返されたようなデータになっています。これをシャッフルしたものを ``output_texts`` とすることで、ある入力列に対してランダムに1つの出力列を返すような学習データを作っています（同じ入力列に最大10種類の異なる出力列が対応付けられます）。

Encoder-DecoderモデルでDecoder側を学習させる場合、入力列（のベクトル表現）より出力列のほうが時間が1単語分だけ早くなるようにします。入力側の文頭は前述のように ``<BOS>`` で始めるのですが、今回は ``'＠'`` という単語で文頭を表すことに決めます（こんなやっつけで大丈夫か…？）。逆に、出力側の文末は ``'。'`` で表すことにします。

## 学習

``fit()`` メソッドを実行してしばらく待つだけです。
Google ColabのGPU環境で10分程度かかります。

```python
batch_size = 64  # Batch size for training.
epochs = 50  # Number of epochs to train for.

cp_cb = ModelCheckpoint(
    filepath="model.{epoch:02d}.hdf5",
    verbose=1,
    mode="auto")

reduce_cb = ReduceLROnPlateau()

# Run training
model.fit([encoder_input_data, decoder_input_data], decoder_target_data,
          batch_size=batch_size,
          epochs=epochs,
          validation_split=0.2,
          callbacks=[cp_cb, reduce_cb])
```

以下のように学習が進んでいきます。

```
Epoch 1/50
28/28 [==============================] - ETA: 0s - loss: 2.7266 - accuracy_masking: 0.1383
Epoch 00001: saving model to model.01.hdf5
28/28 [==============================] - 11s 390ms/step - loss: 2.7266 - accuracy_masking: 0.1383 - val_loss: 1.9890 - val_accuracy_masking: 0.1918 - lr: 0.0100
（略）
Epoch 50/50
28/28 [==============================] - ETA: 0s - loss: 0.2723 - accuracy_masking: 0.8259
Epoch 00050: saving model to model.50.hdf5
28/28 [==============================] - 9s 329ms/step - loss: 0.2723 - accuracy_masking: 0.8259 - val_loss: 0.4014 - val_accuracy_masking: 0.7609 - lr: 1.0000e-03
```

## 推論

学習したモデルを使って、実際に文章生成をするための準備です。

```python
encoder_model = Model(encoder_inputs, encoder_states)

decoder_lstm = model.layers[4]
decoder_dense = model.layers[5]

decoder_state_input_h = Input(shape=(hidden_dim,))
decoder_state_input_c = Input(shape=(hidden_dim,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
decoder_outputs, state_h, state_c = decoder_lstm(
    layer_emb(decoder_inputs), initial_state=decoder_states_inputs)
decoder_states = [state_h, state_c]
decoder_outputs = decoder_dense(decoder_outputs)
decoder_model = Model(
    [decoder_inputs] + decoder_states_inputs,
    [decoder_outputs] + decoder_states)
```

EncoderとDecoderを別々に作ります。
Encoder側は単語列を入力に取り、入力が終わった後の状態を出力します。
Decoder側は単語列と状態を入力に取り（実際には1単語ずつしか入れませんが）、出力列と新しい状態を出力します。

```python
def decode_sequence(input_seq):
    # Encode the input as state vectors.
    states_value = encoder_model.predict([input_seq])
    # Generate empty target sequence of length 1. (batch x sample x time)
    target_seq = np.zeros((1, 1, 1), dtype=np.int32)
    # Populate the first character of target sequence with the start character.
    target_seq[0, 0, 0] = target_token_index['＠']

    # Sampling loop for a batch of sequences
    # (to simplify, here we assume a batch of size 1).
    stop_condition = False
    decoded_sentence = []
    temperature = 1.2
    while not stop_condition:
        output_tokens, h, c = decoder_model.predict(
            [target_seq] + states_value)

        # Sample a token (with temperature)
        logits = output_tokens[0, 0, :]
        prob = np.exp(logits / temperature)
        prob /= (prob.sum() * 1.00001)
        sampled_token_index = np.argmax(np.random.multinomial(1, prob))
        sampled_word = words[sampled_token_index]
        decoded_sentence.append(sampled_word)

        # Exit condition: either hit max length
        # or find stop character.
        if (sampled_word == '。' or
           len(decoded_sentence) > max_decoder_seq_length):
            stop_condition = True

        # Update the target sequence (of length 1).
        target_seq[0, 0, 0] = sampled_token_index

        # Update states
        states_value = [h, c]

    return decoded_sentence
```

先ほどの推論用Encoder, Decoderを使って入力列を処理していきます。
``states_value`` が、入力列をEncoderに入力した時のEncoderの状態にあたります。
この状態と、最初の ``<BOS>`` を表す単語 ``'＠'`` とをDecoderに与えます。入力単語は ``target_seq`` にセットするのですが、モデルがバッチ入力を前提としており、かつ1サンプル・1単語ずつ順番に出力を作っていくので、形状は ``(1, 1, 1)`` となっています。

Decoderは、入力された単語と前の状態に対して、出力単語のスコア (``Dense`` 層の出力) を返します。これをSoftmaxで確率に変換すればよいのですが、スコアの低い単語を出やすくするため、スコアを定数 ``temperature`` で割ってからSoftmaxに入れるように変更します。得られた確率に応じて1単語サンプリングして、出力とします。この出力単語が文末 ``'。'`` であればループを抜けます。

# いざ実行！

ようやく準備が整いました。思う存分生成してください！
最初の1文は適当に決めておきます。

```python
s = ["ネコ", "と", "和解", "せよ"]
for i in range(50):
    gen = decode_sequence([input_token_index[w] for w in s])
    print("".join(gen))
    s = gen
```

こんな感じで50文出力されます。

```
キリストはあなたキリストの命を与える。
神に対する罪を悔い改めよ。
偶像崇拝は罪です。
キリストの行先から罪を負った。
キリストのほかに神はない。
罪を悔い改めよ。
キリストはあなたをの日は近い。
堕落した社会は神を認めない。
悔い改めて福音を信ぜよ。
罪を離れなさい。
偶像崇拝は罪です。
天国は永遠の命地獄の賜物は。
神の賜物は永遠の命。
人が義とされるためにキリストは甦られた。
神は局所大域原理を罪を神。
初めに神は天のものを持つ。
エビアンの罪もをも主。
火が道と永遠の命を与える。
悔い改めて福音を信ぜよ。
モグラ叩きの日は突然に来る。
隠れた時代は近い。
天地が滅びても人の地獄は滅びない。
キリストは墓からよみがえった。
ニセモノが清い人は幸い。
神は唯一。
キリストは再び来る。
悪欲の人は神を認めない。
神は唯一。
キリストが真の神。
死後の行き先を考えて下さい。
アナスタシアの道と命の道がある。
私は死と地獄の鍵を持つ。
神は唯一。
神は心を見る。
神は御子キリストを世に遣わされた。
天国か地獄かあなたの行き先は。
世を正しくさばく。
今日は救の日。
神は唯一。
キリストは罪人を救う。
災いなるかな偶像を拝む者。
死後さばきにあう。
キリストは復活された。
罪を神は罰する。
人の悪を取り除く。
キリストは唯一の神。
真の神を信じなさい。
キリストの血を与える。
キリストのほかに神はない。
天地の底は死神を呼びマンデー・ナイト。
```

原文と同じような出力も結構ありますが、**学習データにない単語も出てくる**ようになっています。
**「モグラ叩きの日は突然に来る」**は笑いました。
**「天地が滅びても人の地獄は滅びない」**は救いようがない。
**「天地の底は死神を呼びマンデー・ナイト」**はお笑い芸人か誰かの持ちネタか？

学習データの出力列が看板の文言だけなので、正直、**何を入力してもそれっぽい文言が出てくる**ような感じですね。**初期値ほとんど関係ないです。多分。**それはそれで良いのかもしれません（Seq2Seqの本来の使い方ではないような気がしますが、まあいいか）。

# まとめ

前回よりはバリエーションが出てきたとはいえ、相変わらず日本語として崩壊している出力があったり、同じような文章が何回もできたりしていますね。それでも突拍子もない単語が現れてクスッとくるのは狙い通りといえます。

こんな感じで、今後も色々なモデルを楽しく体験していければよいかと思っています。長い休みなので。
ノートPCで学習しようとするとかなり厳しいですが、ColabでGPUが無料で使えるので趣味で試すだけなら間に合います。本当に太っ腹。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/9016f7e7-81df-9393-cd6d-e3b6a0f6eb26.png)
[キリスト看板ジェネレータの復活 (v2)](https://christ.every-little.com/)