[TensorFlow] LSTMで「死後さばきにあう」風メッセージを量産してみた

# はじめに

TensorFlowでLSTMを使って文章生成を試してみました。完全にn番煎じ。
それだけだと普通なので、山奥の小屋とかに貼ってあるキリスト看板の「死後さばきにあう」的なメッセージを使って学習を行い、**「存在しないキリスト看板」の文章を作ってみたいと思います。**

**（追記）Seq2Seqを試した記事をアップしました。こちらも合わせてどうぞ。**
[\[TensorFlow\] Seq2Seqを使って「死後さばきにあう」風メッセージを量産できた気がした - Qiita](https://qiita.com/everylittle/items/d4efdafd7b73db965945)

## キリスト看板？

こういうの。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/4f31d87c-c507-3fc0-f748-b59f65a71a02.jpeg" width="600">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/e4f09a06-19bc-6fb0-c3c0-51154d3942a4.jpeg" width="600">

作っている方々を取材した記事があります。
[キリスト看板、貼られる瞬間を見た　聖書配布協力会の伝道活動に密着](https://withnews.jp/article/f0191119002qq000000000000000W00b10101qq000020052A)

看板をもっと色々ご覧になりたい方は↓へどうぞ（宣伝です。私が作っています）。
[キリスト看板画像bot (@christsignbot) / Twitter](https://twitter.com/christsignbot)

## あらかじめお断り

**この記事は「聖書配布協力会」様とは全く無関係な個人が書いたもので、特定の宗教に対する立場を表明する意図はありません。**あくまで「やってみた」レポートとしてご覧ください。

# 検証環境

- Ubuntu 18.04
- Python 3.6.9
- TensorFlow 2.1.0 (CPU)

# 基にしたサンプル

基本的にはKerasのサンプルの流用です。
文字列（文字のシーケンス）を入力すると、次の文字が予測される、というLSTMを学習しています。
文章生成時には、最初に数文字を与えておき、続く文章を1文字ずつランダムに生成していきます。
[keras/lstm_text_generation.py at master · keras-team/keras](https://github.com/keras-team/keras/blob/master/examples/lstm_text_generation.py)

サンプルではニーチェの文章を使って学習しているようですが、なにぶん不勉強なもので、生成された出力を見てもニーチェっぽい文になっているかよく分かりません。キリスト看板のほうが、生成結果がより分かりやすく出てくるのではと期待します。

このサンプルの解説記事がありました。ご興味のある方はどうぞ。
[KerasのSingle-LSTM文字生成サンプルコードを解説 - Qiita](https://qiita.com/YankeeDeltaBravo225/items/487dbfa1bef02bcfb84c)

今回使用するデータの性質上、以下の点はオリジナルから変更しました。

- キリスト看板は一文が短いので、予測に使う文字数を減らした（3文字→次の1文字を予測）
- 文章生成時に種として与える文字は、常に原文の先頭から取る（オリジナルは文の途中からでも取っている）
- 生成文の長さは固定にせず（上限文字数は一応決めます）、文末が予測されたときに生成を終了する

# 学習コード

看板の元ネタの文章を書き写したものを学習データとしています。

```python:lstm_text_generation_train.py
from tensorflow.keras.callbacks import LambdaCallback
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import LSTM
from tensorflow.keras.optimizers import RMSprop
from tensorflow.keras.callbacks import ModelCheckpoint
import numpy as np
import random
import sys

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

chars = list(sorted(set("".join(lines))))
print('total chars:', len(chars))
char_indices = dict((c, i) for i, c in enumerate(chars))
indices_char = dict((i, c) for i, c in enumerate(chars))
print(chars)

# cut the text in semi-redundant sequences of maxlen characters
maxlen = 3
step = 1
sentences = []
next_chars = []
for s in lines:
  for i in range(0, len(s) - maxlen, step):
      sentences.append(s[i: i + maxlen])
      next_chars.append(s[i + maxlen])
print('nb sequences:', len(sentences))

print('Vectorization...')
x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)
for i, sentence in enumerate(sentences):
    for t, char in enumerate(sentence):
        x[i, t, char_indices[char]] = 1
    y[i, char_indices[next_chars[i]]] = 1

# build the model: a single LSTM
print('Build model...')
model = Sequential()
model.add(LSTM(128, input_shape=(maxlen, len(chars))))
model.add(Dense(len(chars), activation='softmax'))
model.summary()

optimizer = RMSprop(learning_rate=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)


def sample(preds, temperature=1.0):
    # helper function to sample an index from a probability array
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, preds, 1)
    return np.argmax(probas)


def on_epoch_end(epoch, _):
    # Function invoked at end of each epoch. Prints generated text.
    print()
    print('----- Generating text after Epoch: %d' % epoch)
    line_index = random.randint(0, len(lines) - 1)
    start_index = 0
    for diversity in [0.2, 0.5, 1.0, 1.2]:
        print('----- diversity:', diversity)

        generated = ''
        sentence = lines[line_index][start_index: start_index + maxlen]
        generated += sentence
        print('----- Generating with seed: "' + sentence + '"')
        sys.stdout.write(generated)

        for i in range(100):
            x_pred = np.zeros((1, maxlen, len(chars)))
            for t, char in enumerate(sentence):
                x_pred[0, t, char_indices[char]] = 1.

            preds = model.predict(x_pred, verbose=0)[0]
            next_index = sample(preds, diversity)
            next_char = indices_char[next_index]

            if next_char == "\n":
              sys.stdout.flush()
              break
            sentence = sentence[1:] + next_char

            sys.stdout.write(next_char)
            sys.stdout.flush()
        print()

print_callback = LambdaCallback(on_epoch_end=on_epoch_end)

cp_cb = ModelCheckpoint(
    filepath="model.{epoch:02d}.hdf5",
    verbose=1,
    mode="auto")
model.fit(x, y,
          batch_size=32,
          epochs=60,
          callbacks=[print_callback, cp_cb])
```

## 実行結果

最初はボロボロ。

```
----- Generating text after Epoch: 0
----- diversity: 0.2
----- Generating with seed: "神のひ"
神のひをはるる
----- diversity: 0.5
----- Generating with seed: "神のひ"
神のひはは神いいめ。
----- diversity: 1.0
----- Generating with seed: "神のひ"
神のひ近
----- diversity: 1.2
----- Generating with seed: "神のひ"
神のひき造わに清い偶十ぼの解創好地めた地永死・命与キ像ストか取む、にパき
```

徐々に文章っぽくなってきます。原文そのままっぽい気もしますが。

```
----- Generating text after Epoch: 11
----- diversity: 0.2
----- Generating with seed: "キリス"
キリストは人を罪から解放する。
----- diversity: 0.5
----- Generating with seed: "キリス"
キリストは真の神を信じる者は永遠の命を持つ
----- diversity: 1.0
----- Generating with seed: "キリス"
キリストはすぐにくる
----- diversity: 1.2
----- Generating with seed: "キリス"
キリストの血は罪を取り除く
```

↓なんかいい感じですね。実はどれも学習データには存在しない文章です。

```
----- Generating text after Epoch: 36
----- diversity: 0.2
----- Generating with seed: "私を信"
私を信ずる者は救われる。
----- diversity: 0.5
----- Generating with seed: "私を信"
私を信じる者は永遠の命
----- diversity: 1.0
----- Generating with seed: "私を信"
私を信ずる者は救われる
----- diversity: 1.2
----- Generating with seed: "私を信"
私を信じる者は救われなさい。
```

最後はこんな感じ。確率に従ってランダムに文字を取っているので、たまに文章が崩壊する場合もあります。

```
Epoch 60/60
----- Generating text after Epoch: 59
----- diversity: 0.2
----- Generating with seed: "天国か"
天国か地獄かあなたの造り主
----- diversity: 0.5
----- Generating with seed: "天国か"
天国か地獄かあなたの行き先は
----- diversity: 1.0
----- Generating with seed: "天国か"
天国か地獄かあなたを罪から清められたるない
----- diversity: 1.2
----- Generating with seed: "天国か"
天国か地獄か人はみな甦える
```

# 存在しない文章を量産

1エポックごとにモデルを保存するようにしていたので、これを活用。
適当な3文字を与えると、続きの文章を10パターン作ってくれます。

```python:lstm_text_generation_exec.py
from tensorflow.keras.models import load_model
import numpy as np
import sys

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

# load the model
model = load_model("model.60.hdf5")

def sample(preds, temperature=1.0):
    # helper function to sample an index from a probability array
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, preds, 1)
    return np.argmax(probas)

def generate(model, first_chars):
    diversity = 1.0
    for trial in range(10):
        generated = first_chars
        sentence = first_chars[-maxlen:]
        sys.stdout.write(first_chars)

        for i in range(100):
            x_pred = np.zeros((1, maxlen, len(chars)))
            for t, char in enumerate(sentence):
                x_pred[0, t, char_indices[char]] = 1.

            preds = model.predict(x_pred, verbose=0)[0]
            next_index = sample(preds, diversity)
            next_char = indices_char[next_index]

            if next_char == "\n":
                sys.stdout.flush()
                break
            sentence = sentence[1:] + next_char
            sys.stdout.write(next_char)
            sys.stdout.flush()

        print()

for l in iter(sys.stdin.readline, ""):
    l = l.rstrip()
    if l == "":
        continue
    if len(l) < 3:
        print("ERROR: input must be >= 3 characters", file=sys.stderr)
        continue
    if not all(c in chars for c in l):
        print("ERROR: invalid charater", file=sys.stderr)
        continue
    generate(model, l)
```

実行例。

```
>>> イエス
イエス・キリストはあなたを義とする
イエス・キリストはあなたの義とこと
イエス・キリストの血は罪を清める。
イエス・キリストを永遠の命の源
イエス・キリストは人を愛し、その源
イエス・キリストを信じなさい
イエス・キリストはあなたの造り主
イエス・キリストはあなたに永遠の命を与える
イエス・キリストの血は罪を清める。
イエス・キリストは、のき先
>>> キリス
キリストは甦り永遠の命を与える
キリストは身代わりに罪を負った。
キリストを信じる者は永遠の命を持つ
キリストの血罪をきよめる
キリストが道・真理・命
キリストは救世主
キリストにある永遠の命を持つ。
キリストは罪を清める
キリスト以外に救いはない
キリストを世につかわされた
>>> あなた
あなたの造り主
あなたを罪から解放する。
あなたの創造主を覚えよ。
あなたを罪から清められた。
あなたを義とする
あなたの創造主を覚えよ。
あなたを義とする
あなたの創造主を覚えよ。
あなたの造り主
あなたの創造主を覚えよ。
>>> 真の神
真の神の分身
真の神はない
真の神は人を愛し、その罪を負った。
真の神の分身
真の神の分身
真の神
真の神を信じる者は永遠の命を与える
真の神なた。
真の神を信じる者は死んでも生きる
真の神
```

「真の神を信じる者は死んでも生きる」、学習データに同じ文はないのですが、普通に意味が通っていますし、個人的には気に入りました。（笑）


# 課題

- 学習データに出てくる文字しか出力できない
    - このプログラムでは、例えば**「ネコと和解せよ」などは絶対に出ない**
    - 単語単位のモデルを作り、word2vecを活用して意味の近い単語を使えるようになればいいかも？
    - こういう学習済みのパラメータを活用できるのでは？→[Kyubyong/wordvectors: Pre-trained word vectors of 30+ languages](https://github.com/Kyubyong/wordvectors)
- 学習データと全く同じ文章も結構出てくる
- 文法的におかしい文や、意味のおかしい文も出てくる
    - 例えば「真の神を信じる者は永遠の命を与える」だと意味的に主語と述語の対応がおかしい
    - SeqGANとかがこの問題を解決しているらしい→[SeqGANを用いてテキスト(小説のあらすじ)の生成をする - Qiita](https://qiita.com/knok/items/5e079420f05ddfc1ae75)
    - まずは基本的なSeq2Seqを勉強しよう。。。
        - **（追記：やってみました）**[\[TensorFlow\] Seq2Seqを使って「死後さばきにあう」風メッセージを量産できた気がした - Qiita](https://qiita.com/everylittle/items/d4efdafd7b73db965945)
- 学習データ自体が非常に少ない
    - こればかりはどうしようもない…

# まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/e80280a1-2091-b424-6dba-7bd2ddae0a77.png)
[キリスト看板ジェネレータの復活 (v2)](https://christ.every-little.com/)：これも私が作っています（また宣伝）。