Edited at

LSTMで桃華ちゃまと会話する

雪美響子さんに引きつづき、アイマス駆動開発第3弾です。今回は桃華ちゃまを実装します。


LSTM

よい説明がたくさんあると思うので、ここでは簡単に。

時系列データをニューラルネットワークで扱うにあたって、過去のデータを保持するために、出力をフィードバックするRNNというものがあった。ただしこれは無限に過去の情報まで持とうとするので、長期依存性や勾配消失などの問題があった。

そこで忘却ゲートなどの改良を加えたものがLSTMである。ただし内部的な構造が異なるだけで、入出力はRNN同様に扱える。


データセット


実行環境

Google ColaboratoryでGPUを使った。


文字単位

とりあえずお試しとして、文字単位でやってみる。


実装

骨組みは参考サイト(http://cedro3.com/ai/keras-lstm-text/ )からもらって、タブや改行を削除する処理を追加した。また表示する際に適宜改行を入れるようにした。

コード

# 文字単位

# http://cedro3.com/ai/keras-lstm-text/

from __future__ import print_function
from keras.callbacks import LambdaCallback
from keras.models import Sequential
from keras.layers import Dense, Activation
from keras.layers import LSTM
from keras.optimizers import RMSprop
from keras.utils.data_utils import get_file
import matplotlib.pyplot as plt # 追加
import numpy as np
import random
import sys
import io

path = './momoka.txt'
with io.open(path, encoding='utf-8') as f:
text = f.read().lower()
text = text.replace("\n","").replace(" ","").replace(" ","").replace("\t","")
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))

# cut the text in semi-redundant sequences of maxlen characters
maxlen = 8
step = 1
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

# 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)))
model.add(Activation('softmax'))

optimizer = RMSprop(lr=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, logs):
if (epoch+1) % 10 == 0:
# Function invoked at end of each epoch. Prints generated text.
print()
print('----- Generating text after Epoch: %d' % epoch)

start_index = random.randint(0, len(text) - maxlen - 1)
start_index = 0
for diversity in [0.2,0.6,1.0]: # diversity = 0.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()

if i % 30 == 0:
print()

print()
print()

print_callback = LambdaCallback(on_epoch_end=on_epoch_end)

history = model.fit(x, y,
batch_size=128,
epochs=100,
callbacks=[print_callback])

# Plot Training loss & Validation Loss
loss = history.history["loss"]
epochs = range(1, len(loss) + 1)
plt.plot(epochs, loss, "bo", label = "Training loss" )
plt.title("Training loss")
plt.legend()
plt.savefig("loss.png")
plt.close()



結果

diversity=0.6の場合を書く。


10エポック

ふふっ、今のわたく

しのちゃんと見ていただきましょう!それにはきってくださいませ
、このわたくしの家にはいっぱいと…それは…わからも、とても人
形で!びをさんってご緒に、まるかわないですわ!pちゃま、わた
くしでちょっかりして?pちゃま、いつもおと……今のでき子ーも
もつじてきたんですの?ドールですわよね?pちゃま、わたくしの
そばにいてくださいませ♪わたくしのちゃととてもろしくて!あの
…と。でも、みなさんにと楽しんではありませんのよ?それに、桜
になったかしらあち、pちゃまのお顔、ごついなおかしないですわ
。pちゃま、わたくしのそばにいえば、いのですも、どうかしまし
たの?レディのそうに、届きましたのよつリードイドの香り…わた
くしがちゃまっ!パリランで……そうですわね。うふふ、うふふ、
桃華のきのでしょね!わたくしがちゃまってください、プロデュー
サーちゃまとも、やりかせなさな、けにないように…このは、ち。
んですもの…やしく

雰囲気はすでに桃華ちゃまだが、だいぶ意味不明な単語だらけになっている。

pちゃまのお顔、ごついなおかしないですわ。

猛虎魂を感じる。


100エポック

ふふっ、今のわたく

し、ちゃんと見ていまして?もう……pちゃまが誰よりもおちろん
せんのですかおわたもしがましたわですわ、pちゃま!pちゃまの
やりとげる力、どうか、くださいませ♪お迎えがくるまで、シャボ
ン玉を…。ふふっ、キラキラ光ってきれいですわね。pちゃまとわ
はよりませ!このよのせーでも…いえば、マいスティーの疲れよれ
で…わたくし、心のからいただいましたわ♪ーちゃんともうのあら
、。ドールわ心のも、pちゃまにもられか…わたくしのことが…ま
るから?テージに桜。みせさんれると…このセー手をわたくしも、
が気になせて。pちゃまも視線をのおかかし!このステージで思い
きり遊んじゃいますけれど、みなさんもご一緒に楽しんでください
ませ♪わたくしが楽してをこまリんですのがらかるかし……まあ、
こまにならっておけれせいっわイがアイドルとできましたわ!そし
てうからしいませ♪わたくしが楽しめば楽しむほど、見てくださる
ファンの方からの感

ちょっとずつ単語がまともになってきた。


200エポック

ふふっ、今のわたく

し、ちゃんと見ていらして?皆さんの視線、独り眠りなっこもへの
、でくだけれて…なわがみなさんと一緒なら、ドキドキですわす、
好きな人なんて。こんなお話、誰にもしたことありませんわ。でも
わたくし、もう、もらうだけでは物足りませんの。もらった分は必
ずそれ以上にして、お返ししますわ。これは、レディとしての礼儀
。いいえ、好意ですわ♪明日も、明後日も、そのつぎも会ってくだ
さる?わたくしにとっても、今宵は特別な夜…パーティーは終わる
もの…。でも、わたくしたちの関係は、ずっと続きますの。pちゃ
ま、わたくしだけを見てください…。pちゃまがわたくしをそう…
わたねしたべ、でくさせしpちゃまとの思い出がまた増えましたわ
♪どちらかと言えば世話好きかもしれませんわ。苦には思いません
ものどんな時ものありたいですわ♪みなさん、ごきげんよう。わた
くしという華が咲く花園へようこそ。この舞台で一番咲き誇るべき
はわたくしなのだか

完璧とは言い難いが、ところどころ文が読み取れる。


単語単位

文字単位でやると断末魔の叫びみたいなのが入るので、もう少し意味が通るように、単語単位でやる。


実装

分かち書きにはjanomeを使った。

コード

# 単語単位


from __future__ import print_function
from keras.callbacks import LambdaCallback
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 codecs
from janome.tokenizer import Tokenizer

# path = get_file(
# 'momoka.txt',
# origin='./momoka.txt')
with io.open("momoka_small.txt", encoding='utf-8') as f:
text = f.read().lower()
text = text.replace(" ","").replace(" ","").replace("\t","")
# text = text.replace("\n","").replace(" ","").replace(" ","").replace("\t","")
# print(text)

words =Tokenizer().tokenize(text, wakati=True) # 分かち書きする
count = 0
char_indices = {} # 辞書初期化
indices_char = {} # 逆引き辞書初期化

for word in words:
if not word in char_indices: # 未登録なら
char_indices[word] = count # 登録する
count +=1
# print(count,word) # 登録した単語を表示
# 逆引き辞書を辞書から作成する
indices_char = dict([(value, key) for (key, value) in char_indices.items()])
print(len(indices_char))
# print(indices_char)

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))

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

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

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

optimizer = RMSprop(lr=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, _):
if (epoch+1) % 10 == 0:
# Function invoked at end of each epoch. Prints generated text.
print()
print('----- Generating text after Epoch: %d' % epoch)

start_index = random.randint(0, len(words) - maxlen - 1)
for diversity in [0.2]:
print('----- diversity:', diversity)

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

generated_text = ""
for i in range(400):
x_pred = np.zeros((1, maxlen, len(words)))
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_word = indices_char[next_index]

sentence = sentence[1:]
sentence.append(next_word)
generated_text += next_word

sys.stdout.write(next_word)
sys.stdout.flush()
# if i % 10 == 0:
# print()
# f.write(generated_text)

# print()
else:
print()

print_callback = LambdaCallback(on_epoch_end=on_epoch_end)

model.fit(x, y,
batch_size=128,
epochs=50,
callbacks=[print_callback])


コーパスが10万を超えると落ちるようになったので、データを少し(モバマスのカードセリフとしんげき)削った。diversityも0.2より上げるとout of indexで落ちるので、どこかにバグがあると思う。


結果

あまりに横長なので手動で適宜改行を入れた。


10エポック目

う。pちゃまの、弁護士の光と。れ、も…。pちゃまの、弁護士の鍵を。

ませ、pちゃまのお顔が終わっている?…?
わたくしのことを、楽しくお話てしたなら、のことはありますわ。
でも、この白薔薇のように、わたくしを!
みなさんの愛なら、少しですの
ふふっ、わたくしの気持ちが、ファンの方に。学びからがののなら、ここの全ても…
pちゃま、お花見のあり方のです。ね。
今のわたくし、ちゃんと見ていまして?
ふふっ、pちゃまのご一緒に!
pちゃま、お顔がすることかん…。んののは…、んののは、方ですわ…。んでも、pちゃまと一緒なら、もっと!!楽しい!
さぁ、pちゃまも…
何がよろしくて?ふふ♪
わたくしを事務所にお届けが気の♪
お仕事が終わったらわたくしと…いえ、何でもないのですけれどもご一緒とありたいと。何がありますの。
でも、この衣装、は日の光ですものね、プロデューサーのまも…くらい
家がたくさんましたの。それは、pちゃまに、何がのんですの。
わたくし、もっともっと!!…!
これはpちゃまの心の顧問弁護士ですからね♪
pちゃま、プロデューサーちゃまのご一緒に!
pちゃまのことを知るのなら、何もないんですの
お仕事が終わったらわたくしと…いえ、何でもないのですけれど?
んですの?
お仕事は終わったらわたくしと…いえ、何でもないのですけれどもいいものですのね♪

文字単位のときよりは意味がわかる。

pちゃまのお顔が終わっている?

悲しい。

お仕事は終わったらわたくしと…いえ、何でもないのですけれどもいいものですのね♪

意味深。


30エポック

全国のファン達がいるのですから、それにこたえてあげるのがアイドルの役目!

わたくしの愛を皆にプレゼントですわ!
フリフリな衣装はたくさん持っていますの!普段、桃華が着ているのはそのひとつ。
pちゃまだけに秘蔵のコレクションを見せて差し上げますわ!おどう、では、ありませんわねね
pちゃまのお顔が浮かんでくるんでしょう
みりあさんって、本当に巻き込むのがお上手…。お付き合いいたしますわ
夢中になっていたらお部屋が大変…。明日、自分たちでお片付けしなきゃ
疲れているはずなのに、心は躍って…。ドキドキが止まりませんの
いつもなら何でもないことも、一緒だと大盛り上がり。楽しいですね
pちゃまの視線を一人占めして、心を奪ってしまう桜に。
pちゃま、桜ばっかり見ていてはいけませんのね。
今日はお礼に、この舞台を雑巾がけして帰りますわ。
舞台さん、今後もよろしくお願いします。今、ピカピカのレディにして差し上げますから。
拭き方がわかりませんわ…。押し出すように?それとも手前へ…
モップは使いませんのね。
今日はお礼に、この舞台を雑巾がけして帰りますわ。
舞台さん、今後もよろしくお願いします。今、ピカピカのレディにして差し上げますから。
拭き方がわかりませんわ…。押し出すように?それとも手前へ…
モップは使いませんのね。
今日はお礼に、この舞台を雑巾がけして帰りますわ。
舞台さん、今後もよろしくお願いします。今、ピカピカのレディにして差し上げますから。
拭き方がわかりませんわ…。押し出すように?それとも手前へ

既にだいぶ元データの一部をひっぱってきてしまっている。また、たまにループしているのは、おそらく「。」の後がループポイントになってしまっている。

舞台さん、今後もよろしくお願いします。今、ピカピカのレディにして差し上げますから。

舞台さんもレディにしてくれる桃華ちゃまの愛。


感想


  • データ数が少ないので、割と早い段階で過学習になる。


    • 特に単語となると1単語の出現回数が少ないので、元データを引っ張ってきてしまう。



  • データ集めは大変。やっぱりデータは重要。

  • 半角カナは悪い文明。

  • Google Colaboratoryは神。


参考