LoginSignup
4
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-06-07

雪美響子さんに引きつづき、アイマス駆動開発第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は神。

参考

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2