初めて記事を投稿します。
普段はPHPを使ってわちゃわちゃと開発をしております。
突然ですが私、高校生の頃に物書きになりたいと思っていた時期がありました。
その頃はライトノベルに激ハマりしていたので、将来はライトノベル作家だ! などと夢を胸に秘めていたのです。
なんでならなかったのか?
友人に書いたものを見せたら紙を破られ「こんなのダメに決まってんじゃん」と言われたからです。
ピュアなハートが傷ついてしまった私は物書きを諦めました。
一念発起
そんな私も再び小説熱が盛り返してきました、あの頃とは違う、今なら書ける、超大作がかける! そしてウハウハになる!
自分の力以外を使って。。。
早速作ってもらおう
何を使うか
自然言語処理で使われるものには、RNNとかGRUとか色々ありますが、今回はLSTMを使って作成していこうと思います。
なんでかというとLSTMの文字を見て、qiitaのLGTMが頭に浮かんだからです。はい、直感です。
今回は自動文章生成処理ってどんなものか? というのが目的なので、これでいいのだ(バカボン風)
準備
まず準備として、参考とする小説を決めます。
今回は文章の自動生成処理を動かしてみて、どのようになるかを見ることを前提としています。
ですので青空文庫様にお力をお借りして、太宰治の「走れメロス」を学習に利用します。
後はやる気とPC(Python)さえあれば準備OKです。やる気が一番準備に時間かかり、、、いえ、なんでもありません。
テキストを整形する
さて、まずは青空文庫様より走れメロスをDLして、Pythonファイルと同一フォルダに配置し読み込みます。
import re
# 走れメロスを読み込む
with open("hashire_merosu.txt", mode="r", encoding="shift-jis") as f:
merosu_text = f.read()
青空文庫様のtxtファイルには不要なもの(ルビとか説明とか)があるので取り除きます。
# 整形するための関数
def aozora_shaping(raw_text):
# タイトル・著者名を削除
text = re.sub(r'\A.*?\n.*?\n', '', raw_text)
# 空白・改行を削除
text = re.sub(r' | |\n|\r', '', text)
# ルビ説明文を削除
text = re.sub(r'-{55}.*?-{55}', '', text)
# 末尾の文を削除
text = re.sub(r'底本:.*\Z', '', text)
# かっこ等の記号を削除
text = re.sub(r'《.*?》|||[.*?]|〔|〕|#|※', '', text)
return text
text = aozora_shaping(merosu_text)
メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。メロスには政治がわからぬ ~長いので省略
無事不要な箇所は取り除かれているようです。
ベクトル化
続いて、文字をベクトル化していく必要がありますので、実施していきます。
設定を記述しておく
n_rnn = 10
batch_size = 128
epochs = 30
neuron = 256
そして、先程の文章から各文字をリストに保存します。
# setで重複をなくし、各文字をリストに保存する
chars = sorted(list(set(text)))
['―', '、', '。', '々', '「', '」', 'あ', 'い', 'う', 'え', 'お', 'か' ~長いので省略
ちゃんとリスト化されているようです。
続いて、作成した文字リストで辞書を作成していきます。
char_index = {}
index_char = {}
# 文字がキー、インデックスが値の辞書
for i, char in enumerate(chars):
char_index[char] = i
# 文字が値、インデックスがキーの辞書
for i, char in enumerate(chars):
index_char[i] = char
# char_index
{'―': 0, '、': 1, '。': 2, '々': 3, '「': 4, '」': 5, 'あ': 6 ~省略
# index_char
{0: '―', 1: '、', 2: '。', 3: '々', 4: '「', 5: '」', 6: 'あ ~省略
続いて、時系列データと、それから予測すべき文字を取り出すための処理を記述します。
for i in range(0, len(text) - n_rnn):
time_chars.append(text[i: i+ n_rnn])
next_chars.append(text[i+ n_rnn])
# time_chars
['メロスは激怒した。必', 'ロスは激怒した。必ず', 'スは激怒した。必ず、', 'は激怒した。必ず、か' ~省略
# next_chars
['ず', '、', 'か', 'の', '邪', '智', '暴', '虐', 'の', '王', 'を', '除', 'か', 'な', 'け', 'れ', 'ば', 'な', 'ら', 'ぬ' ~省略
ふむふむ、できてそうです。
続いて、入力と正解をone-hot表現で表します。
import numpy as np
x = np.zeros((len(time_chars), n_rnn, len(chars)), dtype=np.bool_)
t = np.zeros((len(time_chars), len(chars)), dtype=np.bool_)
for i, t_cs in enumerate(time_chars):
t[i, char_index[next_chars[i]]] = 1 # 正解
for j, char in enumerate(t_cs):
x[i, j, char_index[char]] = 1 # 入力
print(x.shape)
print(t.shape)
(9796, 10, 815) # サンプル数、時系列、文字の数
(9796, 815) # サンプル数、文字の数
LSTMモデル
続いていよいよ、LSTMのモデルを構築していきます。
Kerasを使うので、まずはKerasをインストールします。
kerasのインストール
File "~/syosetu.py", line 3, in <module>
from keras.models import Sequential
インストールしないで実施しようとすると怒られたので(当たり前)、kerasをインストールします。
tensorflowのインストールが推奨されているようなので、まずはtensorflowを入れる
pip3 install --upgrade tensorflow
続いてkerasのインストール
pip3 install keras
インポートしてみる
from keras.models import Sequential
from keras.layers import Dense, LSTM
from keras.callbacks import LambdaCallback
実行して怒られなければOKだと思います
ではモデルを構築していきます
model_lstm = Sequential()
# コンパイル
model_lstm.add(LSTM(neuron, input_shape=(n_rnn, len(chars))))
model_lstm.add(Dense(len(chars), activation="softmax"))
model_lstm.compile(loss="categorical_crossentropy", optimizer="adam")
# サマリーを出力
print(model_lstm.summary())
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm (LSTM) (None, 256) 1097728
dense (Dense) (None, 815) 209455
=================================================================
Total params: 1,307,183
Trainable params: 1,307,183
Non-trainable params: 0
_________________________________________________________________
None
パラメータ数がすごい数ですね(小学生並の感想)
文章作成
# エポック終了後に文字を出力する関数
def on_epoch_end(epoch, logs):
print("エポック後", epoch)
# 確率分布を調整する定数
beta = 5
prev_text = text[0:n_rnn]
created_text = prev_text
print("シード", created_text)
for i in range(800): # 800文字生成
# 入力をone-hot表現にする
x_pred = np.zeros((1, n_rnn, len(chars)))
for j, char in enumerate(prev_text):
x_pred[0, j, char_index[char]] = 1
# 予測を行い、次の文字を得る
y = model.predict(x_pred)
p_power = y[0] ** beta
next_index = np.random.choice(len(p_power), p=p_power/np.sum(p_power))
next_char = index_char[next_index]
created_text += next_char
# 最新の時系列が入る
prev_text = prev_text[1:] + next_char
print(created_text)
print()
# エポック終了時に実行
epock_end_callback = LambdaCallback(on_epoch_end=on_epoch_end)
# 学習させる
model = model_lstm
history_lstm = model_lstm.fit(x, t, batch_size=batch_size, epochs=epochs, callbacks=[epock_end_callback])
さて、いよいよお手並み拝見です。
私はウハウハできるのでしょうか!!!!!!!
Epoch 1/30
76/77 [============================>.] - ETA: 0s - loss: 5.4609エポック後 0
シード メロスは激怒した。必
メロスは激怒した。必、、。の。いた。。のた。をに、、。。。。い。た。の。い。。た。。。。のの。た。。、の。、。。
い。。を、、の。のい。。。。にのいい。の。。の。い、い。。。。。。。の。。く、。た。。。。。。、、、、。のたいいだ。、
。のの。る。。。。。い。。。たた。る。。のい。。。、、た、いのて。。。。、。。、。、、。、。い。。い。、を。たのの。。
、た。。い。。をのい。。、のた。を、、、。。のらの。。。。、。を。。、。、の、い。。。いには。。。。を。。い。。。、。
。。にだは、。。。。、の、。はい。い、の。。。。、。し。。をたのて。。。く、、。のの、。。。。。。。。。。の。。のた、
、。。いい、。。、、。。をのの。。、のいた。、。、。をの。い。。。。る。い。い。。い。。ら。。。。。。。、い、。。は。。
。た。。。。い。。をを。。な。。た、。、。い。、い、。いっ。。をいい。のいのた。。、。のい。。。い。。。、、、。の、。
。を。。。をた。。。た。。。。。。。。。。。、、の、。、い。。。、。。いい。。。。。。。。。いい、。。、。。。を、。。、。い、
。。ない。。の、、。だ、。。ら。。い。、く、、とた。。。。。。。、、。、。たいの。。、の、た。た。、。い。の。。。、。。
をの。。の。。。。ス。を。。、を。、、い。。い、の。。の、。、の。。。、。。、の。の。の。。い。。、。、、。た。たい。
い。。、な。、。。の。、。。。、。。、のた。な、、。い。の。。、。。。、の。。、。、。、。。。。。の。。。ののてる。の
。。。。。。。。、、、、。の、。。。の。。、、、。の。。、の、い。。。、。の。の。、、。るた。。。、た。。。い。。。。う。。
、を。の。。た。の。の。。い、。を。の。。。。。、なのをい。、。のい。。。い。。、い。、にを。、。。。た。。とをた。
の、た。を、。。た。。。の。、。、。。を。。い。、いの。。。、、い。、、、。
何を言っているのかさっぱりわかりません。
これでは私はウハウハできません。。。
今回エポック数を30にしているので、30回目の結果を見てみましょう。
Epoch 30/30
77/77 [==============================] - ETA: 0s - loss: 1.3715エポック後 29
シード メロスは激怒した。必
メロスは激怒した。必呆、メロスの市にして来るとようにああ、ああなが狂うただ。あった。私は、どっとみたなから、何もなっていいのだ。
私は、ち遠の士とした。きってもるのである。メロスは、まって行るとだろう。私は、ちちどもなわぬ。全くして、わなの心であるのだ。
私は、いまえななった。私の命は、わずなわれにもうから、何も無いのおまえになった。メロスは、悠々とと仕度を聞いた。
たの、メロスの人を殺した。「あるのだ。」としているのだ。」メロスは、ましてはりをたんだ。「あったち、あのだちだから、わしても問をされた。
「あるのだ。」「どうない。」「メロスは、まだ。そう、ども、いいまの気がりを見っていた。。
「それては、まった。「あのだ。」「どう。」メロスは、おまえに行くないない。」「どう、その男をして下さいた。」「その、いのだのだ。」「それない。」
「どう。メロスは、おまえのと婿を事いた。「れて、メロスは走った。「あるたのだ。」「どう、その心を殺して下るのだ。」
「王の、人の心を殺した。私はなのでものだ。ありない、私は、わずの市に走ったから、私は、ちまったのだ。」
「どう、私は、まのだが、それからぬば、わからは無いの顔おまえ、それなら、間いいい。おまえには、その男いのだのだ。
私は、信実のだけた。私の疲のたち、ちととっとして、きのとでであるのだ。私は、信じてくれた。私は、私の信実裏た。」
とはは、こ込ですまでの気ををついていり、ころでった。「ああ、あのた。あのだが、どうでもわしのいを、きりって、それであるのだ。
私は、信じてくれた。」として、私の心を殺してやる事だ。」「メロ、は、おまえであるた。いの人は、ちちととわしくして下さいた。
メロスは、まだよう、私を殺される。メロスは、まがられて、村のたのであるのだ。陽は、ものも無くであった。
メロスは、それかられば、このでいますのそのだって、ふのしてくれなように深いし、私は、ちんんだんだっな
最初に比べると大分まともに見えます!
とはいえ小説とは呼べないレベルですね。。。
結論
試行回数も参考資料も少ないので、ウハウハな小説にするには、まだまだ程遠いですね。
俺はようやくのぼりはじめたばかりだからな、このはてしなく遠い男坂をよ!
〜完〜
嘘です。完にしません。
GRUでも試してみる
GRUでも同じことをしたいと思います。
記述を少し変えるだけでできるみたいなので。
# GRU、Denseをインポートする
from keras.layers import Dense GRU
model_gru = Sequential()
model_gru.add(GRU(n_mid, input_shape=(n_rnn, len(chars))))
model_gru.add(Dense(len(chars), activation="softmax"))
model_gru.compile(loss="categorical_crossentropy", optimizer="adam")
model = model_gru
history_lstm = model_gru.fit(x, t, batch_size=batch_size, epochs=epochs, callbacks=[epock_end_callback])
この記述で実行します。
Epoch 1/30
77/77 [==============================] - ETA: 0s - loss: 5.4536エポック後 0
シード メロスは激怒した。必
メロスは激怒した。必の、、、いはい、、、、、の、、、い、の、い、、のス、、いの、、にをのはの、、、の、。、、、、。の、、、。
の、。の、、、、、、、、の、いたに、の、、の、、の、、い、ののの、、。、、、、、、。、、、の、、、いい、の、、れ、の、の、、、
の、、にのに。、に、にに、、は、にの、とは、た、のに、ののい。、、のに、、、、の、に、、のた、の、のにのにの、に、、
い、、、い、う。の、、、。、た、の、にの、の、、に、、、なのにのたい、に、、、い、。、い、、、のい、の、。。いに、、にの、
、に、に、、、、、、。、ののい、、のの、。い、にのののの、に、の、、、い、、にの、のになの、、、、、、、、のの、、、、のな、
、に、のに、、いいの。、にに、、、、、、な。、、、、の、、の、の、、に、、、いの、のの、、の、、、、、、に、の、は、の、、い、
いに、、、のの、、の、れ、の、の、、、、の、、は、の、、、、、、、の、、に、い、のに、、の、、、の、、い。、、、。の、、、、、
、、い、の、、、、、いの。、、、の、、。、の、、にの、いに、、、、、、い。い、のののロ、て、、。、、にの、に、。。、たに、、、、
。のる、、、、ののは、、の、、、、に。の、いののの、に、、、の、、っ、いの。、、、の、、、い、、の。に、い、、、、、、、に、、
にに、のい、に。の、、、、、は、、の、。た、、、に、、の、、、、、、、、、ての、、、の、、、。、、の。の、、、、、いいの、、のの。
の、、い。の、にの、、のに、、れ、、に、、は、、の、い、、、い。に、のた、に、い、、いのに、の、のい。、、い、、。のた、に、
、た、。の、、、、、、た、いの、に、たの、にの、い、、。る、。いに、、、に、、の、い、、、、ののの、のに、い、、のた、のの、
、の、の、、ののの、のに、にの、、、、の、、、に、にの、、のた、ののい、にの、、いの、、、、の、、、いの、、、、の、、、。、
相変わらず何を言っているのかさっぱりわかりませんね。
続いて同じく30回目を見ていきましょう。
Epoch 30/30
77/77 [==============================] - ETA: 0s - loss: 0.4364エポック後 29
シード メロスは激怒した。必
メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。メロスには政治がわからぬ。
メロスは、それから花婿の肩をたたいて、「仕度の無いのはお互さまさ。私の家にも、宝といっては、妹と羊だけだ。
他には、何も無い。全部あげよう。もう既に、メロスの弟になったことを誇ってくれ。」花婿は揉み手して、てれた。
それから、皇后さまを。それから、賢臣のアレキス様を。」「それは、そのマントを打るせて。そうして、メロスの弟になっところになっていない。
」メロスは、ます、三日目の日暮まで、ここに帰って来なかったら、あの友人を絞め殺して下さい。たの、それでも私の無い。
おまえの兄は、何もといただかりに、なんの市でしっとうろうった。「市に用事、おまえた。私の命から、皇后さまを。
それから、賢臣のアレキス様を。」「おどろいた。国王は乱心か。」「いいえ、乱心ではございませぬ。人を、信ずる事が出来ぬ、というのです。
」メロスは腕に唸りをつけてセリヌンティウスは、縄打たれた。メロスは、すぐに出発した。初夏、満天の星である。
メロスは、メロスの弟になったことを誇ってくれ。」花婿は揉み手して、てれた。その心には、神も哀れと思ったか、ついに憐愍を垂れてくれた。
押し流されつつも、見事、対岸の樹木の幹に、すがりつく事が出来たのである。ありがたい。
メロスは、それから花婿の肩をたたいて、「仕度の無いのはお互さまさ。私の家にも、宝といっては、妹と羊だけだ。
他には、何も無い。全部あげよう。もう一つ、メロスの弟になったことを誇ってくれ。」花婿は揉み手して、てれた。
その心には、ここんないなんでもった。メロスの頭は、からっぽだか。夜であるのだ。私は信頼されている。
私は、信頼に報いなければならぬ。いまはた王だ。私は王は、一気にかった。セリヌンティウスは、縄打たれた。
メロスは、それから花婿の肩をたたいて、「仕度の無いのはお互さまさ。私の家にも、宝といって
LSTMと見比べてみると面白いですね。こっちの方が文章としては良さそう。
参考データを増やしてみたり、エポック数を増やしてみたりすれば、もう少し精度の高い自動生成がされると思います。
今回は文章生成を実際に触ってみることをゴールとしていたので、ここまで!
正直、まだ中で何が起きているかの詳細は把握できていない。。。今後の課題。
今後
AIが作曲したものに、AIで歌詞つけられたら面白そう。
参考にしたサイト