LSTM-RNNを使って芥川龍之介っぽい文章を自動生成させてみた

LSTM-RNNを使用して、芥川龍之介っぽい文章を自動生成させるモデルを作成したので、メモ的に残します。

以下にコードと学習済みの重みデータがあるGitHubリポジトリを置いておきます。
https://github.com/odrum428/akutagawaGenerator

### 環境と使用技術
- Windows 10
- Python 3.6.8
- Keras
- Tensor flow

## 学習データのダウンロード
今回は、学習用のデータとして、青空文庫で公開されている芥川龍之介の書籍データ一覧を使用します。

書籍データは以下のリンクからダウンロードできます。
http://keison.sakura.ne.jp/agyou/agyou.html


ダウンロードしたら、解凍して適当なディレクトリに配置します。

今回は、コードと同層に`source`という名前で保存しました。

まずは持ってきたファイルを読み込んで表示させてみましょう。Pythonでファイルを開くには`open`モジュールを使ってやればＯＫ。

```python
import sys

path =  './source/春.txt'
file  =  open(path, 'r', encoding='Shift_JIS') # 日本語ファイルなのでShift_JISでエンコード
source_text =  file.read()
print(source_text)
```

これを実行することでコンソールにファイルの中身が出力されたと思います。


取得してきたテキストは、注釈や書籍の情報など余計な情報が入っているため、このままでは使えません。

まずは前処理を行って書籍データを使えるように加工しましょう。


### 前処理
前処理を行うには、対象のことを知らなければいけないので、とりあえずどんな形でデータが入っているのかを見てみましょう。

例. 
> 犬養君に就いて

>芥川龍之介

  

>　犬養君の作品は大抵読んでいるつもりである。その又僕の読んだ作品は何れも手を抜いたところはない。どれも皆丹念に出来上っている。若し欠点を挙げるとすれば余り丹念すぎる為に暗示する力を欠き易い事であろう。

>　それから又犬養君の作品はどれも皆柔かに美しいものである。こう云う柔かい美しさは一寸他の作家達には発見出来ない。僕はそこに若々しい一本の柳に似た感じを受けている。

>　いつか僕は仕事をしかけた犬養君に会った事があった。その時僕の見た犬養君の顔は（若し失礼でないとすれば）女人と交った後のようだった。僕は犬養君を思い出す度にかならずこの顔を思い出している。同時に又犬養君の作品の如何にも丹念に出来上っているのも偶然ではないと思っている。

  
  
  

>底本：「大川の水・追憶・本所両国　現代日本のエッセイ」講談社文芸文庫、講談社

>　　　1995（平成7）年1月10日第1刷発行

>底本の親本：「芥川龍之介全集　第一～九、一二巻」岩波書店

>　　　1977（昭和52）年7、9～12月、1978（昭和53）年1～4、7月発行

>入力：向井樹里

>校正：砂場清隆

>2007年2月12日作成

>青空文庫作成ファイル：

>このファイルは、インターネットの図書館、青空文庫（http://www.aozora.gr.jp/）で作られました。入力、校正、制作にあたったのは、ボランティアの皆さんです。



書籍データには、解説や出版社情報、タイトル、ルビなど不必要なデータが含まれているので、まずはこれを取り除きます。

今回、自動生成させたい文章は芥川龍之介が書いた文章なので、その部分のみを抜き出して行きます。

**タイトルや解説情報を削除する**
書籍の本文が始まる前にタイトルや解説情報などの無駄な情報が入っているので、これを削除します。

書籍データを見てみると、注釈情報は

```
-------------------------------------------------------

サンプルテキスト  

-------------------------------------------------------
```
のように、ハイフンで囲まれています。

自然な文章でハイフンが二回以上続くことは考えづらいので、ハイフンが二回以上繰り返されている部分が見つかれば、それ以前を削除すればよいと思います。

僕の力では、イケてる手法は思いつかないので、以下の手順で行います。

1.  ハイフンが二回以上続く場所を正規表現で検索して、タグとなる文字列に置換
2. splitで埋め込んだタグを検索して、タグ以降の文字列を取得

これを実装したのが以下のコードです。

```python

# 本文前の注釈にタグを埋め込んで、そこを元に本文を抽出
text_tagging_hi = re.sub(r'--+', 'タグを埋め込みます', source_text)
text_remove_tag = text_tagging_hi.split('タグを埋め込みます')[-1]
```


**ルビを取り除く。**
ルビは以下のような形で振られています。

>　支那《シナ》の上海《シャンハイ》の或《ある》町です。昼でも薄暗い或家の二階に、人相の悪い印度《インド》人の婆さんが一人、商人らしい一人の亜米利加《アメリカ》人と何か頻《しきり》に話し合っていました。


対応としては、章中にある`《○○》`の部分を正規表現で検索して、`''`で置換すればOKかと思います。

Pythonで正規表現を使って文字列を置換する場合は`re.sub`を使えばいいです。

```

置換後の文字列  =  re.sub(正規表現,  置換する文字列,  置換される文字列  [,  置換回数])
```

なので今回の場合は

```python

# 単語に振ってあるルビを削除

text_without_rubi = re.sub(r'《.+?》','', text_remove_tag)
```

とやれば、ＯＫです。



**注釈情報を削除する**
文章をよく見てみると所々に
> ［＃５字下げ］檢非違使に問はれたる媼の物語［＃「檢非違使に問はれたる媼の物語」は中見出し］

このように`[]`で囲まれた注釈情報があることが分かります。この部分も不要なので、先ほどと同様に削除します。


```python

# 本文中にある注釈や解説を削除

text_without_com = re.sub(r'［.+?］', '', text_without_rubi)
```


**出版社情報等を削除する**
末尾についている出版社や作成日などの情報は必要ないので、削除します。

これには文章中の`底本`以降を削除すれば良さそうです。

Pythonで特定の文字列以降を削除したいときは`split`を使えば簡単にできます。

やっていることはシンプルで、`底本`という文字がある場所で文字列を分割して、前半部分のテキストを取得しているだけです。

ただこれは書籍情報を記述している部分以外に`底本`という単語がないことが前提になっているので、注意が必要です。

```python

# 出版社や作成日などの情報を削除

output = text_without_com.split('底本')[0]
```

#### 抜き出した文章をつなげる
ここまでで、書籍データから必要な本文のみを抽出することができました。

次はこれを全ての書籍データに対して行い、学習用のテキストファイルにします。要は本文のみで構成された長いテキストファイルを一つ作成します。

**全ての書籍に対して前処理を行う**
書籍データは`source`ディレクトリの中に入っているので、その中に入っているすべてのファイルに対して前処理を行いたいです。

`source`ディレクトリ配下にあるファイル一覧をして取得して、for文で前処理を行います。

Pythonでファイル名やディレクトリ名の一覧を取得するには`os.listdir()`を使うと簡単に行うことができます。

`os.listdir()`は引数に指定されたディレクトリ配下にあるファイルとディレクトリ名をすべて取得してくれます。

```python

path =  './source/'
files = os.listdir(path)
# print(files)
for  file  in files:
file_path = path +  file
print(file_path)
```

これで`source`配下にあるすべてのファイルが取得できたので、あとはfor文ですべての書籍データに対して前処理を行います。


ここまでの処理をすべてまとめると以下のようなコードになります。

```python

import sys
import re
import os

path = './source/'

files = os.listdir(path)

for file in files:
    file_path = path + file
    # print(file_path)
    with open(file_path, 'r', encoding='Shift_JIS') as file:
        source_text = file.read()
        # 本文前の注釈にタグを埋め込んで、そこを元に本文を抽出
        text_tagging_hi = re.sub(r'--+', 'タグを埋め込みます', source_text)
        text_remove_tag = text_tagging_hi.split('タグを埋め込みます')[-1]
        # 単語に振ってあるルビを削除
        text_without_rubi = re.sub(r'《.+?》','', text_remove_tag)
        # 本文中にある注釈や解説を削除
        text_without_com = re.sub(r'［.+?］', '', text_without_rubi)
        # 出版社や作成日などの情報を削除
        output = text_without_com.split('底本')[0]
        output_file = open('preprocess_done.txt','a',encoding='utf-8').write(output)
```

　
### はまりポイント
以下のファイルはエンコードがshift_JISでやってもうまくいかないので、除外しました。

```
わが家の古玩.txt
廿年後之戦争.txt
```

## モデルの実装

ここまでで、学習に必要なデータが用意できたので、モデルを作成します。

今回はLSTM-RNNモデルを使います。
LSTMモデルは以下の記事を読むと大体把握できます。
https://qiita.com/t_Signull/items/21b82be280b46f467d1b

簡単に説明すると、時系列データをニューラルネットワークで扱うためのモデルです。ただ、RNNだけでは勾配消失問題が顕著になってしまうので、周辺の情報に基づいて勾配の変化をうまく調整するノードを入れたものがLSTM-RNNです。

以下がLSTM-RNNを実装したコードになります。

```python

from  __future__  import print_function
from keras.callbacks import LambdaCallback, ModelCheckpoint
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.optimizers import RMSprop

from keras.utils.data_utils import get_file
import numpy as np
import random
import sys
import io
import os

path =  './preprocess_done.txt'
with io.open(path, encoding='utf-8') as f:
    text = f.read().lower()
print('corpus length:', len(text))

chars =  sorted(list(set(text)))
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))

maxlen =  40
step =  3
sentences = []
next_chars = []

for i in  range(0, len(text) - maxlen, step):
    sentences.append(text[i: i + maxlen])
    next_chars.append(text[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

# モデルをビルドする
print('Build model...')
model = Sequential()
model.add(LSTM(128, input_shape=(maxlen, len(chars))))
model.add(Dense(len(chars), activation='softmax'))

optimizer = RMSprop(lr=0.01)

model.compile(loss='categorical_crossentropy', optimizer=optimizer)

def sample(preds, temperature=1.0):
    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, _):
    print()
    print('----- Generating text after Epoch: %d'  % epoch)
    start_index = random.randint(0, len(text) - maxlen -  1)
    for diversity in [0.2, 0.5, 1.0, 1.2]:
        print('----- diversity:', diversity)
        generated =  ''
        sentence = text[start_index: start_index + maxlen]
        generated += sentence
        print('----- Generating with seed: "'  + sentence +  '"')
        sys.stdout.write(generated)

        for i in  range(400):
            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]

            generated += next_char
            sentence = sentence[1:] + next_char
            
            sys.stdout.write(next_char)
            sys.stdout.flush()
        print()

# modelsというディレクトリを作成して、epochごとにその時点での重みを保存する
os.makedirs('models', exist_ok=True)

model_checkpoint = ModelCheckpoint(
    filepath=os.path.join('models', 'model_{epoch:02d}.h5'),
    monitor='val_loss',
    verbose=1)

print_callback = LambdaCallback(on_epoch_end=on_epoch_end)

# 学習途中での出力を確かめたい場合は、callbacksにprint_calbackを追加する
model.fit(x, y,
    batch_size=128,
    epochs=60,
    callbacks=[model_checkpoint])

# 学習が完了したら、モデルと重さを保存する
model_json_str = model.to_json()
open('complete_model_epoch_60.json', 'w').write(model_json_str)
model.save_weights('complete_model_epoch_60.h5')
```

epochごとの重さデータを`models`というディレクトリに保存してくれるようになっています。
epoch数が大きい方が性能が良いとは限らないので、そこまでのデータも保存しています。

大体僕のPCだと60epochの学習に14時間ぐらいかかりました。

### はまりポイント
**実行時にメモリ不足になる**
前処理で結合した文章のサイズが大きかったので、メモリが不足しています的なエラーが出るときがある。
その場合は、元データを分割して学習させるか、データ量を削減するなどの対応が必要です。

## 出力結果
学習が終わったら、モデルから文章を生成させて見ましょう。


以下のコードを実行することで、学習が完了したモデルを使用して、文章を生成させることができます。

```python

from keras.models import model_from_json
from keras.callbacks import LambdaCallback, ModelCheckpoint
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.optimizers import RMSprop
from keras.utils.data_utils import get_file
import numpy as np
import random
import sys
import io
import os

# 生成元の文章を読み込む
path = './preprocess_done.txt'
with io.open(path, encoding='utf-8') as f:
    text = f.read().lower()
print('corpus length:', len(text))

chars = sorted(list(set(text)))
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))

maxlen = 40
step = 3
sentences = []
next_chars = []
for i in range(0, len(text) - maxlen, step):
    sentences.append(text[i: i + maxlen])
    next_chars.append(text[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

# モデルを読み込む
model = model_from_json(open('./complete_model_epoch_60.json').read())
# 学習結果を読み込む
model.load_weights('./complete_model_epoch_60.h5')

model.summary()

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

start_index = random.randint(0, len(text) - maxlen - 1)

def sample(preds, temperature=0.9):
    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)

for diversity in [0.55]:
        print('----- diversity:', diversity)

        generated = ''
        # この文章から芥川龍之介っぽい文章を作成させてみる。
        sentence = "おはようございます。今日はいい天気ですね。今日の予定は何ですか？ごきげんよう！！"
        generated += sentence
        print('----- Generating with seed: "' + sentence + '"')
        sys.stdout.write(generated)

        for i in range(3000):
            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]

            generated += next_char
            sentence = sentence[1:] + next_char


output_file = open('akutagawa_output.txt', 'w', encoding='utf-8').write(generated)

```

この例では、`おはようございます。今日はいい天気ですね。今日の予定は何ですか？ごきげんよう！！`という文章から文章を作成させています。

以下が実際に出力された文章です。

```
おはようございます。今日はいい天気ですね。今日の予定は何ですか？ごきげんよう！！
　天主の中には、わたしの心の火を置けて、、勿論ない。それから、これは、わしの心にこう云う感ずりもある。
　そう云う言葉は、そのその上ではもないか。
　どこそこの二人はこの島のんだから、――これでもおの私は彼自身の「一もこれは時とはかったのである。その時はくごとりだけはない。それは、それを見ると、「あの女は見て、わしの心にそれを知っている。
　しかしお前はこの「何か」の女の言葉をはいていた。それが、其の思ラも今になると、を下くさうに、しばらくその男のやうな、わたしの心の間に、自らにもあれないかな？　そうです。」
　おひを得て見ると、山の上には、「やはり、」
「しかしこんな事です。」
「あの事を聞かるつて、「れぬゐるゐい。」
　置くれるとの間、をつけた、っきりとは大きなことである。わしは何かってゐる。そうして、まる事を見ると、さうして、「れているなら。」
　わたしはこの後には、」は、わしにあることを知らない事がある。
　こう云うはこの島の上に、日本人の顔をして、わしの語の下に、わしの食がて、時々れた。
　でもこれをの上に見ると、それは、この時のあたりに、お母さんの一人の名とりて、彼女自身の心をそれを見ていると、それをはずけていられとものである。
　私はこの間を黒いう手間の上が、一々の意志のあることにすることを見ている。

　　　又

　わたしはやはりはいっています。――と云うことの大我を御愛しにゐない。わたしは、あの女はこの一度に、当時の方がようなつたと云う、此時々ろんだがらその開けてなし。
　そう云う内に、一人の男を出す。
　お前はさう云ふようにも思ふとこのをの上ったかも知れなかった。
　おれはその時から、あるいは。」
「あの何も云っているような？」
　ゐるないように、一人の私には、「しかしいや、お前は御間の顔をしているのをおって御だらな。」
「おい、ただお前の御作の御」
　お母さんはわたしのお母さんの無々のである。見るとことを見た時には、何となることにこう云う言葉に一｜この間にマかもっていた。
「だからお母さんはこの時までしまいました。」
「わたしのお母さんは」
「一つあはうな。」
　そう云う女のは、わたしの心のとか、僕には一時でも少将の五六人、彼女の多いは、また一人のように、おらの年下の一人の現である。
　無情を一つ見ほどに、
「さあ、たがどもう第もの御な。しかも顔を見ている。

```

そこはかとなく芥川龍之介っぽいが出ているかなと思います。
diversityを変更すれば、生成される文章の乖離度を変更することもできるので、いろいろな値を試してみても面白いかもしれません。


## 参考になりそうなサイト一覧
このプログラムはアリス・イン・ワンダーランド（英語版）をディープラーニングで学習させ、その学習データを元に文章の生成を行います。
https://colab.research.google.com/drive/18hpMf8-lAPr9WCedHZmRXfe1h1TuAZRb#scrollTo=FO0ZwyfjlBC1

夏目漱石っぽい文章を作成してくれる
https://blog.aidemy.net/entry/2018/10/05/195404

【エヴァンゲリオン】アスカっぽいセリフをDeepLearningで自動生成してみる
https://qiita.com/S346/items/24e875e3c5ac58f55810

Pythonリハビリのために文章自動生成プログラムを作ってみた
http://o-tomox.hatenablog.com/entry/2014/11/14/190632

マルコフ連鎖による文章の自動生成
https://blog.kentarok.org/entry/20040415/1081998210

LSTMで夏目漱石ぽい文章の生成
https://qiita.com/elm200/items/6f84d3a42eebe6c47caa

Deep Learningで遊ぶ(3): LSTM-RNNで夏目漱石っぽい文章の生成にトライしてみる
https://tjo.hatenablog.com/entry/2016/11/08/190000

RNNを使った文章の自動生成
https://www.pytry3g.com/entry/2018/03/16/203414

ディープラーニングで文章を自動生成したい！
https://blog.aidemy.net/entry/2018/10/05/195404