ディープラーニングで文章を自動生成したい!

1. 目的

はじめまして、Aidemy研修生のいとぅー(@andmohiko)です。

自然言語処理では文章の自動生成というテーマがあります。

夏目漱石っぽい文章を自動生成してみたいのでやります。

2. 手法

LSTM-RNNを使います。

LSTMについては詳しくはこちら

qiita.com

簡単に説明すると、

ある長さの文字列から次の一文字を予測する ということをひたすら繰り返すことで文章が自動生成されていくというものです。

マルコフ連鎖は前の2文字しか見ないため、LSTMを使うことで文脈に沿った文章が生成されやすくなるという利点があります。

3. データセット

青空文庫で公開されている夏目漱石の「坊ちゃん」「吾輩は猫である」「こころ」「夢十夜」を使います。

こちら→作家別作品リスト:夏目 漱石

自然言語処理はデータを集めてくるのが大変なのでとてもありがたいですね。

4. 前処理

ダウンロードしてきた本文にはルビや注釈なのど情報が含まれているため、

これらを削除し地の文のみにします。


import sys
import re
path = './bocchan.txt'
bindata = open(path, "rb")
lines = bindata.readlines()
for line in lines:
    text = line.decode('Shift_JIS')
    text = re.split(r'\r',text)[0]
    text = re.split(r'底本',text)[0]
    text = text.replace('|','')
    text = re.sub(r'《.+?》','',text)
    text = re.sub(r'[#.+?]','',text)
    print(text)
file = open('data_bocchan.txt','a',encoding='utf-8').write(text)

これを夏目漱石の他の作品についてもやり、最後に4作品を繋げて一つの長いテキストファイルにする。

5. TensorFlow + KerasでLSTMを実装

いよいよモデルを実装していきます。
Kerasを使いますが、裏側はTensorFlowのお世話になります。


from keras.models import Sequential,load_model
from keras.layers import Dense, Activation, LSTM
from keras.optimizers import RMSprop
from keras.utils.data_utils import get_file
import numpy as np
import random
import sys
path = "./data_souseki.txt"
bindata = open(path, "rb").read()
text = bindata.decode("utf-8")
print("Size of text: ",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))
#40文字の次の1文字を学習させる. 3文字ずつずらして40文字と1文字というセットを作る
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])
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
    #テキストのベクトル化
    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
#LSTMを使ったモデルを作る
model = Sequential() #連続的なデータを扱う
model.add(LSTM(128, input_shape=(maxlen,len(chars))))
model.add(Dense(len(chars)))
model.add(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)
    probs = np.random.multinomial(1, preds, 1)
    return np.argmax(probs)
#生成する
for iteration in range(1,30):
    print()
    print("-"*50)
    print("繰り返し回数: ",iteration)
    model.fit(X, y, batch_size=128, epochs=1)
    start_index = random.randint(0, len(text)-maxlen-1)
for diversity in [0.2, 0.5, 1.0, 1.2]:
    print()
    print("-----diversity", diversity)
    generated =""
    sentence = text[start_index: start_index + maxlen ]
    generated += sentence
    print("-----Seedを生成しました: " + sentence + '"')
    sys.stdout.write(generated)

    #次の文字を予測して足す
    for i in range(400):
        x = np.zeros((1,maxlen,len(chars)))
        for t,char in enumerate(sentence):
            x[0, t, char_indices[char]] = 1
    
        preds = model.predict(x, verbose =9)[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()
model.save('souseki_model.h5')
file = open('sousekigentext.txt','w+',encoding='utf-8').write(generated)

データ量が多いのでGPUを使わない場合は丸一日かかることもあります。しばらく放置しましょう。

6. 学習の様子

今回は時間の都合でepoch数を30にしています。

1回目
loss関数: 4.1994

申しょいを少しも充に落ちる語ったかど勢だ」「いらせんと従ょいに詩に至った文考を逢ってかかり方でいし出である武び少しドャ大椀屋に見て、這入ったが内にステ動りら間」
「人のく迷亭はかあした共安である。あるむを御当坊の起粧にな通ったの云き返す小ッちにも通ろ首の朝な性に学者口を高「え最がつけない」「すん、毫は食うんだ流山主人はレ一の子口学者は奥ずや。あ昔威横あが、しいくオあした 活で、学種爺とにこの作団だからパ気と云いえ面を見た。

読めない、、、

27回目
loss関数: 3.3512

大きな声を第一のはなかったから、それだから、どうかした事がない。その時の方だからしきりになっている事はないが、これからその事だから、自分でも、そのそとでない。吾輩は人間になると、吾輩には文明のごとくのはない。一るのは一際もなくなる。それなら飛び出して来たものがて来たのだが、またはなかったが、それではあいまさに、云わぬが、なるほどのところがある。ただ一人がいい。

なんとなく良さそう。
しかも夏目漱石っぽい(?)

7. 結果と今後の発展

夏目漱石っぽい文章を生成することができました。
学習されていく過程を見ると徐々に日本語らしくなっていくところが見ていて楽しかったです。

loss関数が想定よりも小さくならなかったので、次やるときはloss関数の値を小さくするようにしたいです。
27回目の学習結果でも日本語として意味が成り立っているわけではないのがもうひといきな感じですね。loss関数が小さくなるとより日本語として意味が通ったものになりそうです。
今回は1文字ずつ予測していくことで文章を生成していましたが、単語分割をして一単語ずつ予測させるとより日本語っぽくなるかなと思いました。
また、夏目漱石っぽさを評価するために、何種類かの文章を用意してどれが夏目漱石の小説を学習させた結果かをアンケートでとるなどすると客観的に評価ができると思いました。