自然言語処理をとりあえず動かしてみた

初めて記事を投稿します。
普段はPHPを使ってわちゃわちゃと開発をしております。

突然ですが私、高校生の頃に物書きになりたいと思っていた時期がありました。
その頃はライトノベルに激ハマりしていたので、将来はライトノベル作家だ！　などと夢を胸に秘めていたのです。

## なんでならなかったのか？
友人に書いたものを見せたら紙を破られ「こんなのダメに決まってんじゃん」と言われたからです。
ピュアなハートが傷ついてしまった私は物書きを諦めました。

## 一念発起
そんな私も再び小説熱が盛り返してきました、あの頃とは違う、今なら書ける、超大作がかける！ そしてウハウハになる！
自分の力以外を使って。。。

# 早速作ってもらおう

## 何を使うか
自然言語処理で使われるものには、RNNとかGRUとか色々ありますが、今回はLSTMを使って作成していこうと思います。
なんでかというとLSTMの文字を見て、qiitaのLGTMが頭に浮かんだからです。はい、直感です。
今回は自動文章生成処理ってどんなものか？　というのが目的なので、これでいいのだ（バカボン風）

## 準備
まず準備として、参考とする小説を決めます。
今回は文章の自動生成処理を動かしてみて、どのようになるかを見ることを前提としています。
ですので青空文庫様にお力をお借りして、太宰治の「[走れメロス](https://www.aozora.gr.jp/cards/000035/card1567.html)」を学習に利用します。
後はやる気とPC（Python）さえあれば準備OKです。やる気が一番準備に時間かかり、、、いえ、なんでもありません。

## テキストを整形する

さて、まずは青空文庫様より[走れメロス](https://www.aozora.gr.jp/cards/000035/card1567.html)をDLして、Pythonファイルと同一フォルダに配置し読み込みます。

```python
import re

# 走れメロスを読み込む
with open("hashire_merosu.txt", mode="r", encoding="shift-jis") as f:
	merosu_text = f.read()
```

青空文庫様のtxtファイルには不要なもの（ルビとか説明とか）があるので取り除きます。

```python
# 整形するための関数
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
```

そして、先程の文章から各文字をリストに保存します。

```python
# setで重複をなくし、各文字をリストに保存する
chars = sorted(list(set(text)))
```

```出力.
['―', '、', '。', '々', '「', '」', 'あ', 'い', 'う', 'え', 'お', 'か' ~長いので省略
```

ちゃんとリスト化されているようです。
続いて、作成した文字リストで辞書を作成していきます。

```python
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: 'あ ~省略
```

続いて、時系列データと、それから予測すべき文字を取り出すための処理を記述します。

```python
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表現で表します。

```python
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
```

インポートしてみる

```python
from keras.models import Sequential
from keras.layers import Dense, LSTM
from keras.callbacks import LambdaCallback
```

実行して怒られなければOKだと思います
ではモデルを構築していきます

```python
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
```
パラメータ数がすごい数ですね（小学生並の感想）

## 文章作成

```python
# エポック終了後に文字を出力する関数
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])
```

さて、いよいよお手並み拝見です。
私はウハウハできるのでしょうか！！！！！！！
<br><br><br><br>

```1回目出力.
Epoch 1/30
76/77 [============================>.] - ETA: 0s - loss: 5.4609エポック後 0
シード メロスは激怒した。必
メロスは激怒した。必、、。の。いた。。のた。をに、、。。。。い。た。の。い。。た。。。。のの。た。。、の。、。。
い。。を、、の。のい。。。。にのいい。の。。の。い、い。。。。。。。の。。く、。た。。。。。。、、、、。のたいいだ。、
。のの。る。。。。。い。。。たた。る。。のい。。。、、た、いのて。。。。、。。、。、、。、。い。。い。、を。たのの。。
、た。。い。。をのい。。、のた。を、、、。。のらの。。。。、。を。。、。、の、い。。。いには。。。。を。。い。。。、。
。。にだは、。。。。、の、。はい。い、の。。。。、。し。。をたのて。。。く、、。のの、。。。。。。。。。。の。。のた、
、。。いい、。。、、。。をのの。。、のいた。、。、。をの。い。。。。る。い。い。。い。。ら。。。。。。。、い、。。は。。
。た。。。。い。。をを。。な。。た、。、。い。、い、。いっ。。をいい。のいのた。。、。のい。。。い。。。、、、。の、。
。を。。。をた。。。た。。。。。。。。。。。、、の、。、い。。。、。。いい。。。。。。。。。いい、。。、。。。を、。。、。い、
。。ない。。の、、。だ、。。ら。。い。、く、、とた。。。。。。。、、。、。たいの。。、の、た。た。、。い。の。。。、。。
をの。。の。。。。ス。を。。、を。、、い。。い、の。。の、。、の。。。、。。、の。の。の。。い。。、。、、。た。たい。
い。。、な。、。。の。、。。。、。。、のた。な、、。い。の。。、。。。、の。。、。、。、。。。。。の。。。ののてる。の
。。。。。。。。、、、、。の、。。。の。。、、、。の。。、の、い。。。、。の。の。、、。るた。。。、た。。。い。。。。う。。
、を。の。。た。の。の。。い、。を。の。。。。。、なのをい。、。のい。。。い。。、い。、にを。、。。。た。。とをた。
の、た。を、。。た。。。の。、。、。。を。。い。、いの。。。、、い。、、、。
```

何を言っているのかさっぱりわかりません。
これでは私はウハウハできません。。。

今回エポック数を30にしているので、30回目の結果を見てみましょう。

```30回目出力.
Epoch 30/30
77/77 [==============================] - ETA: 0s - loss: 1.3715エポック後 29
シード メロスは激怒した。必
メロスは激怒した。必呆、メロスの市にして来るとようにああ、ああなが狂うただ。あった。私は、どっとみたなから、何もなっていいのだ。
私は、ち遠の士とした。きってもるのである。メロスは、まって行るとだろう。私は、ちちどもなわぬ。全くして、わなの心であるのだ。
私は、いまえななった。私の命は、わずなわれにもうから、何も無いのおまえになった。メロスは、悠々とと仕度を聞いた。
たの、メロスの人を殺した。「あるのだ。」としているのだ。」メロスは、ましてはりをたんだ。「あったち、あのだちだから、わしても問をされた。
「あるのだ。」「どうない。」「メロスは、まだ。そう、ども、いいまの気がりを見っていた。。
「それては、まった。「あのだ。」「どう。」メロスは、おまえに行くないない。」「どう、その男をして下さいた。」「その、いのだのだ。」「それない。」
「どう。メロスは、おまえのと婿を事いた。「れて、メロスは走った。「あるたのだ。」「どう、その心を殺して下るのだ。」
「王の、人の心を殺した。私はなのでものだ。ありない、私は、わずの市に走ったから、私は、ちまったのだ。」
「どう、私は、まのだが、それからぬば、わからは無いの顔おまえ、それなら、間いいい。おまえには、その男いのだのだ。
私は、信実のだけた。私の疲のたち、ちととっとして、きのとでであるのだ。私は、信じてくれた。私は、私の信実裏た。」
とはは、こ込ですまでの気ををついていり、ころでった。「ああ、あのた。あのだが、どうでもわしのいを、きりって、それであるのだ。
私は、信じてくれた。」として、私の心を殺してやる事だ。」「メロ、は、おまえであるた。いの人は、ちちととわしくして下さいた。
メロスは、まだよう、私を殺される。メロスは、まがられて、村のたのであるのだ。陽は、ものも無くであった。
メロスは、それかられば、このでいますのそのだって、ふのしてくれなように深いし、私は、ちんんだんだっな
```

最初に比べると大分まともに見えます！
とはいえ小説とは呼べないレベルですね。。。

## 結論
試行回数も参考資料も少ないので、ウハウハな小説にするには、まだまだ程遠いですね。
**俺はようやくのぼりはじめたばかりだからな、このはてしなく遠い男坂をよ！**

**〜完〜**


<br>
<br>
<br>


嘘です。完にしません。

## GRUでも試してみる
GRUでも同じことをしたいと思います。
記述を少し変えるだけでできるみたいなので。

```python
# 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
シード メロスは激怒した。必
メロスは激怒した。必の、、、いはい、、、、、の、、、い、の、い、、のス、、いの、、にをのはの、、、の、。、、、、。の、、、。
の、。の、、、、、、、、の、いたに、の、、の、、の、、い、ののの、、。、、、、、、。、、、の、、、いい、の、、れ、の、の、、、
の、、にのに。、に、にに、、は、にの、とは、た、のに、ののい。、、のに、、、、の、に、、のた、の、のにのにの、に、、
い、、、い、う。の、、、。、た、の、にの、の、、に、、、なのにのたい、に、、、い、。、い、、、のい、の、。。いに、、にの、
、に、に、、、、、、。、ののい、、のの、。い、にのののの、に、の、、、い、、にの、のになの、、、、、、、、のの、、、、のな、
、に、のに、、いいの。、にに、、、、、、な。、、、、の、、の、の、、に、、、いの、のの、、の、、、、、、に、の、は、の、、い、
いに、、、のの、、の、れ、の、の、、、、の、、は、の、、、、、、、の、、に、い、のに、、の、、、の、、い。、、、。の、、、、、
、、い、の、、、、、いの。、、、の、、。、の、、にの、いに、、、、、、い。い、のののロ、て、、。、、にの、に、。。、たに、、、、
。のる、、、、ののは、、の、、、、に。の、いののの、に、、、の、、っ、いの。、、、の、、、い、、の。に、い、、、、、、、に、、
にに、のい、に。の、、、、、は、、の、。た、、、に、、の、、、、、、、、、ての、、、の、、、。、、の。の、、、、、いいの、、のの。
の、、い。の、にの、、のに、、れ、、に、、は、、の、い、、、い。に、のた、に、い、、いのに、の、のい。、、い、、。のた、に、
、た、。の、、、、、、た、いの、に、たの、にの、い、、。る、。いに、、、に、、の、い、、、、ののの、のに、い、、のた、のの、
、の、の、、ののの、のに、にの、、、、の、、、に、にの、、のた、ののい、にの、、いの、、、、の、、、いの、、、、の、、、。、
```

相変わらず何を言っているのかさっぱりわかりませんね。
<br/><br/><br/><br/>

続いて同じく30回目を見ていきましょう。

```30回目.
Epoch 30/30
77/77 [==============================] - ETA: 0s - loss: 0.4364エポック後 29
シード メロスは激怒した。必
メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。メロスには政治がわからぬ。
メロスは、それから花婿の肩をたたいて、「仕度の無いのはお互さまさ。私の家にも、宝といっては、妹と羊だけだ。
他には、何も無い。全部あげよう。もう既に、メロスの弟になったことを誇ってくれ。」花婿は揉み手して、てれた。
それから、皇后さまを。それから、賢臣のアレキス様を。」「それは、そのマントを打るせて。そうして、メロスの弟になっところになっていない。
」メロスは、ます、三日目の日暮まで、ここに帰って来なかったら、あの友人を絞め殺して下さい。たの、それでも私の無い。
おまえの兄は、何もといただかりに、なんの市でしっとうろうった。「市に用事、おまえた。私の命から、皇后さまを。
それから、賢臣のアレキス様を。」「おどろいた。国王は乱心か。」「いいえ、乱心ではございませぬ。人を、信ずる事が出来ぬ、というのです。
」メロスは腕に唸りをつけてセリヌンティウスは、縄打たれた。メロスは、すぐに出発した。初夏、満天の星である。
メロスは、メロスの弟になったことを誇ってくれ。」花婿は揉み手して、てれた。その心には、神も哀れと思ったか、ついに憐愍を垂れてくれた。
押し流されつつも、見事、対岸の樹木の幹に、すがりつく事が出来たのである。ありがたい。
メロスは、それから花婿の肩をたたいて、「仕度の無いのはお互さまさ。私の家にも、宝といっては、妹と羊だけだ。
他には、何も無い。全部あげよう。もう一つ、メロスの弟になったことを誇ってくれ。」花婿は揉み手して、てれた。
その心には、ここんないなんでもった。メロスの頭は、からっぽだか。夜であるのだ。私は信頼されている。
私は、信頼に報いなければならぬ。いまはた王だ。私は王は、一気にかった。セリヌンティウスは、縄打たれた。
メロスは、それから花婿の肩をたたいて、「仕度の無いのはお互さまさ。私の家にも、宝といって
```

LSTMと見比べてみると面白いですね。こっちの方が文章としては良さそう。
参考データを増やしてみたり、エポック数を増やしてみたりすれば、もう少し精度の高い自動生成がされると思います。

今回は文章生成を実際に触ってみることをゴールとしていたので、ここまで！
正直、まだ中で何が起きているかの詳細は把握できていない。。。今後の課題。

## 今後
AIが作曲したものに、AIで歌詞つけられたら面白そう。


## 参考にしたサイト
https://qiita.com/berry-clione/items/a725186972302b634d46

https://agirobots.com/lstmgruentrance-noformula/