CCCマーケティング データベースマーケティング研究所の Tech Blog

研究所スタッフによる格闘記録やマーケティング界隈についての記事など

KerasのLSTMで文章生成して北大路魯山人先生にランチの相談をしてみた

こんにちは、ソリューション開発の中岸です。青空文庫のテキストデータを元に、KerasのLSTMで文章生成をしてみたので、サンプルコードと共にご紹介します。

きっかけ

家に籠る時間が長くなり、毎日毎日何を食べようか悩んでいます。おいしいものを食べたいのに、何を食べたいのかわからないというある種贅沢な悩みです。自分のレパートリーや発想の範囲は決して広くなく、それだけでは驚きや刺激がないのです。
そこで、自分の考えが及ばないような内容のレコメンドが出来ないかと考えました。ということで、青空文庫にある美食家・北大路魯山人先生の著作を学習し、頭の数文字を投げるとそれに続く文章を生成する、ということをKerasのLSTMでやってみます。 魯山人先生のご神託からメニューのインスピレーションを得よう!という魂胆です。

Kerasとは

Kerasとは、TensorFlowやCNTK、Theano上で動く、ニューラルネットワークのライブラリです。TensorFlowなどよりも、より簡単にモデルを作ることができます。
Keras Documentation

LSTMとは

LSTMとは、Long Short Term Memory の略です。時系列データを扱うのに効果があり、RNN(リカレントニューラルネットワーク)より長期的な時系列の関係を学習することができます。RNNで発生していた入力重み衝突/出力重み衝突に対処する、入力ゲート/出力ゲートを持つことと、入力情報が大きく遷移した時に、リセットの役割をする忘却ゲートを持つのが特徴です。

f:id:naka06331:20200421144256j:plain
とても概念的な図

このLSTMを使って、文章生成や対話の生成、音声や映像認識を行うことができます。

今回行う文章生成は、入力された文字列(今回は10文字で行いました)を1つの時系列とし、その次にくる文字を予測する、ということを、1文字ずつずらしながら行っていきます。

『家庭料理の話』の冒頭を例にします。

世間の人は、自分の身近にある有価値な、美味いものを利用することに無頓着のようだ。

f:id:naka06331:20200421145059j:plain

サンプルコード

Kerasはサンプルコードが豊富に公開されています。今回は、こちら lstm_text_generation.py を元に手を加えます。

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 datetime

#テキストファイルを読み込み
f = open('rosanzin.txt', 'rb').read()
text = f.decode('utf-8')
print('文字数:', len(text))

テキストファイルは、ルビや注釈、ヘッダーなどを削除して、各作品を1つのファイルにまとめたものです。

#辞書作成

#重複を除いて文字をリスト化
chars = sorted(list(set(text)))
print('重複なし文字数:', len(chars))

#文字がキー、indexが値のディクショナリ型
char_indices = dict((c, i) for i, c in enumerate(chars))
#indexがキー、文字が値のディクショナリ型
indices_char = dict((i, c) for i, c in enumerate(chars))


# cut the text in semi-redundant sequences of maxlen characters

#時系列のシーケンスごとに、説明変数と、その次にくる文字(目的変数)をリスト化
maxlen = 10#1つの時系列として扱う文字数
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))

#1文字ごとにベクトル化
print('Vectorization...')
x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)#0埋めの3次元配列を作成
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)#0埋めの3次元配列を作成
for i, sentence in enumerate(sentences):#作成した配列で1になる箇所を置き換えていく
    for t, char in enumerate(sentence):
        x[i, t, char_indices[char]] = 1
    y[i, char_indices[next_chars[i]]] = 1


# LSTMのモデルを構築
print('Build model...')
model = Sequential()
model.add(LSTM(128, input_shape=(maxlen, len(chars))))
model.add(Dense(len(chars), activation='softmax'))

optimizer = RMSprop(learning_rate=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 = "春の昼食によいものは"#頭出しの文字列を定義する
        generated += sentence
        print('----- Generating with seed: "' + sentence + '"')
        sys.stdout.write(generated)

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

            sentence = sentence[1:] + next_char

            sys.stdout.write(next_char)
            sys.stdout.flush()
        dt_now = datetime.datetime.now()#現在時刻を取得して表示
        print(dt_now)
        print()

print_callback = LambdaCallback(on_epoch_end=on_epoch_end)


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



実行した環境はこちらです。元のテキストの重複なし文字数が314,387、epochs=150で約5時間半かかりました。
* MacBook Air
*CPU 1.8 GHz Intel Core i5
* メモリ 8GB
* Python 3.7.3

結果

文章生成時に設定した最初のワードは、「春の昼食によいものは」です。 エポックの経過と共に見ていきましょう。

----- Generating text after Epoch: 0
----- diversity: 0.2
----- Generating with seed: "春の昼食によいものは"
春の昼食によいものは、そうになると、そうには、その人は、その人は、その人は、その人は、その人は、その人は、その人は、その人は、その人は、その人は、その人は、その人は、その人は、その人は、これはまず、これはまず、その人は、その人は、その人は、これは、このものは、その人は、その人は、まず、そうをうまくと

「その」が多いですね。まだまだ文章っぽさはありません。

----- Generating text after Epoch: 49
----- diversity: 0.2
----- Generating with seed: "春の昼食によいものは"
春の昼食によいものは、そういうことになるかと知れたもので、これをもっとその味がよくない。というのは、このほかにないのです。しかし、それはそのうまでもなく、それはその理の料理のことである。しかし、そのこういうふうに、このくのはことはともにくり、そのことは今日のおかな、そのいうことは、このわたしは、その

「その」や「この」は多いですが、なんとなく意味のわかる文章になってきました。

----- Generating text after Epoch: 99
----- diversity: 0.2
----- Generating with seed: "春の昼食によいものは"
春の昼食によいものは、その魚は知らの上手をないの一本当のことであるが、こういうことをつくると、そうしたことを知らず、食べ物とは、まず本料理の、するの食器を食うのは、は、そうたる。私はこの料理をするを、その味いからこうなう。そうしてその本当の料理は、まず、料理は食えない。その人には、この中の一つをまた

魚もいいですね。「その」「この」は減ってきました。

----- Generating text after Epoch: 149
----- diversity: 0.2
----- Generating with seed: "春の昼食によいものは"
春の昼食によいものは、焼きの出来けばりではない。この方が美味いのは、はうにいいからとはいえないのもありますが、この三もの工の中国を美味というのは、かたらなくのであろう。その人々の中で一番美味でする。それは分らない人間にはこれをのんでする。そのてんですこでたいのではないか。であろう。それを一人らしてい

中華もいいですね。
いい感じになってきたので、Epoch: 149でdiversityの値が0.5, 1.0, 1.2 の場合も見てみましょう。

----- diversity: 0.5
春の昼食によいものはない。まず大しど来いのは金を言うと言えば、大根おのも出す。よくなるのは、その料理とはいいもらない。これではいかな食器が出るにかからなかった。としてもこのどものになることをしています。、この人々の中で一番よい見方を。しかしやかけるに、これを楽しみのいところである。しかし、まずかつお

----- diversity: 1.0
春の昼食によいものは一時間を作らない間にをやることである。それには実はよくない。これらの多自通人は、わずかに日本人も一根朝姿とか一々に生かる高利あった。かなんで自分で家だ国然地好味すむといべされが、不だののような絵画の昆ものはというようである。地の方指美生のことはひとりくじ覚のみたら中分らのっても、

----- diversity: 1.2
春の昼食によいものはばないような主人がるからずいこともあるが、もっとしかし知る。これでも小昔なうではなく、ダなら二東関僕の場わちに美味行無た訣肉ばなれ止ったずし食三種の間に品にあが金もあったおいや自工陶東でたカやから国工大発品を出つれ物ことだ。料理物ではないとはいいつかりがまく一流の料理をして心し、

diversity:0.5の冒頭で「春の昼食によいものはない。」と言われてしまいましたが……
diversityの値が小さいほど、日本語としては自然になるものの「その」「この」が多くなり、diversityの値が大きくなるほど、語彙が豊富になる一方で文章の自然さは損なわれている様子です。

ということで、Epoch: 149のdiversity:0.5で出てきた「かつお」が気になったので、買い出し時にかつおを探したところ、「かつおの生節」という商品と出会い、はじめて買ってみました。手でほぐれる柔らかさ、賞味期限も1ヶ月先で使い勝手がよさそうです。そんなわけで、ランチはかつおの生節丼になりました。魯山人先生ありがとうございました! f:id:naka06331:20200423142526j:plain



参考

こちらのサイトを参考にさせていただきました。
わかるLSTM ~ 最近の動向と共に - Qiita
kerasのLSTMを使って文章生成を実装するサンプル