GRUで文章生成(keras)

# はじめに
前回、[マルコフ連鎖でレポートを自動作成してみた ](https://qiita.com/eycjur/items/2fc0b9a240306cfd3008)という記事を作成しました。このときはマルコフ連鎖を使っていたため、文章の流れを無視した文章が出来上がってしまいました。そこで今回はGRUという技術を使って文脈を意識した文章を作成しようという企画です。

# GRUとは
文章生成といわれるとRNNとかLSTMとかがよく知られていると思いますが、LSTMよりも学習時間が短くて済むという点からこれを選びました。詳しい構造は分からないので書きませんが、実用的にはLSTMとほぼ同じコードで書けます。

# 実装
それではさっそく実装していきましょう。

まずは使用するライブラリを読み込みます。

```python

import re
import MeCab
import numpy as np
from sklearn.model_selection import train_test_split
from gensim.models import word2vec

from keras.layers.core import Activation
from keras.layers.core import Dense, Dropout
from keras.layers.core import Masking
from keras.models import Sequential
from keras.layers.recurrent import GRU
from keras.layers.embeddings import Embedding
from keras.layers.normalization import BatchNormalization
from keras.utils import np_utils
from keras.optimizers import RMSprop
```


# 読み込み・形態素解析
今回利用するデータは[ 青空文庫 ](https://www.aozora.gr.jp/)のものを使います。今回は江戸川乱歩の「[怪人二十面相](https://www.aozora.gr.jp/cards/001779/card57228.html)」を利用しました。

このデータから不要なものを削除、および形態素解析する関数を定義します。

```python

def convert(text, train):
    if train:
        # ルビなど不要なものを削除
        text = re.split(r"\-{5,}", text)[2]
        text = re.split(r"底本：", text)[0]
        text = re.sub(r"《.*?》", "", text)
        text = re.sub(r"［.*?］", "", text)
        text = re.sub(r"[｜]", "", text)
        text = re.sub("(\n){2,}", "\n", text)
        text = re.sub(r"\u3000", "", text)

    # 形態素解析
    mecab = MeCab.Tagger("-Owakati")
    return mecab.parse(text).split()
```

ここでは、予測データの前処理にも使いたいので、trainというbool値の引数を用意して、訓練時のみルビなどの削除を行うようにしました。
それではこの関数を実行していきましょう。

```python

path = "text_raw/kaijin_nijumenso.txt"
with open(path, "r") as f:
    text = f.read()
text_split = convert(text, True)
```

このように読み込むと`text = 怪人二十面相\n江戸川乱歩\n\n-----------`
の様に読み込まれます。
そして`text_split = ['はし', 'が', 'き', 'その','ころ', '、',,, `のように分割されます。

#データセットの作成
まずは形態素解析した単語を数字に変換します。これは単に番号をつければよいので、以下のようにします。ここで、0はマスキング用（余ったデータを0として扱う）に使うため、`+1`する必要があります。

```python

# 単語と数字の辞書の作成
vocab_r = dict((key + 1, word) for (key, word) in enumerate(set(text_split)))
vocab = dict((word, key) for (key, word) in vocab_r.items())

text_num = list(map(lambda x:vocab[x], text_split))
```

次に学習用のデータセットを作ります。後で述べますが、入力データはEmbedding層で単語をベクトル化するので、`(n_sample, n_seq)=(入力データ数, 何個の単語で学習するか)`の次元にします。また、正解ラベルは後でone-hot化するのでこれも`(n_sample)=(入力データ数)`の次元にしておきます。

```python

n_seq = 10 #何個の単語で学習するか
num_char = len(set(text_split)) + 1 #単語の種類（含マスキング）
n_sample = len(text_num) - n_seq #入力データ数

# データの作成
input_data = np.zeros((n_sample, n_seq))
correct_data = np.zeros((n_sample))

for i in range(n_sample):
    input_data[i] = text_num[i:i + n_seq]
    correct_data[i] = text_num[i + n_seq]

x_train, x_test, y_train, y_test = train_test_split(input_data, correct_data, test_size=0.1, shuffle=True)
```

# 単語の分散表現
入力データはEmbedding層で単語をベクトル化するのですが、単純にone-hotにするとデータサイズが膨大になってしまうので分散表現というものにします。これは単語同士の関連性を考えて単語をベクトル化するものでこれを用いることで少ない次元で単語を表現することが可能になります。

```python

model_w2v = word2vec.Word2Vec([text_split], size=100, min_count=1, window=5, iter=10)
embedding_matrix = np.zeros((num_char, 100))
for w, vec in zip(model_w2v.wv.index2word, model_w2v.wv.vectors):
    embedding_matrix[vocab[w]] = vec
```

`model_w2v.wv.index2word, model_w2v.wv.vectors`を用いて

```
'はし':[ 0.04145062, -0.01713059, -0.0444374 ,,,],
'が':[ 0.554178  , -0.19353375, -0.56997895,,,]
```
のようにベクトル化することができます。これと先ほど作った単語と数字の辞書と合わせて

```
[[ 0.        ,  0.        ,  0.        , ...,  0.        ],
[ 0.00860965, -0.00045624, -0.00883528, ..., -0.00861127],
...
[ 0.00662873, -0.00499085, -0.01188819, ..., -0.01252057]]
```
のような重みベクトルを作成することができました。

# モデルの作成
それではGRUで学習していきます。モデル自体がLSTMをGRUに変えるだけでパラメーターもそのままで学習することができます。

```python

# モデルの作成
model = Sequential()
model.add(Embedding(num_char, 100, weights=[embedding_matrix], trainable=False, input_length=n_seq))
model.add(BatchNormalization(axis=-1))
model.add(Masking(mask_value=0, input_shape=(n_seq, 100)))
model.add(GRU(128, input_shape=(n_seq, 100), kernel_initializer='random_uniform'))
model.add(Dropout(0.2))
model.add(Dense(num_char))
model.add(Activation('softmax'))

model.compile(loss='categorical_crossentropy', optimizer=RMSprop(lr=0.01), metrics=['categorical_accuracy'])
model.fit(x_train, np_utils.to_categorical(y_train, num_char), batch_size=128, validation_split=0.05, epochs=100,
          shuffle=True)
```
パラメーター等は詳しくないので、[ この記事 ](https://www.atmarkit.co.jp/ait/articles/1807/05/news143.html)を参考にさせていただきました。

ではテストデータで見てみましょう。

```python

y_pred = model.predict(x_test)
print(list(map(lambda x:vocab_r[x], np.argmax(y_pred, axis=1))))
```
出力は以下のようになりました。

```
ハハハ……。」と、しきりにほめたてる"を"
を見あげました。いかにも、長針は"諸君"
君は生来の冒険児で、中学校を"、"
```
ちょっと微妙な雰囲気がしますね。

# 入力文字列で予測
入力された文字列を想定して実際にどのような文章が作成される試してみましょう。

まず単語を形態素解析して分割します。たまたまTwitterでトレンドに入ってたニュースでやってみます。

```python

input_text = "認知症の原因物質が歯周病で蓄積 九州大などが発表"
input_text_split = convert(input_text, False)
```

単語を数字に変換します。このとき、訓練データにない単語は辞書に登録されておらずエラーが出るので、0を返すような例外処理をします。

```python

def word2vec_input(x):
    """
    数字を単語に直す
    辞書にない文字は0を返す
    """
    try:
        return vocab[x]
    except:
        return 0


vocab_r[0] = "<nodata>"

# 単語を数値に変換
input_text_num = list(map(lambda x:word2vec_input(x), input_text_split))
```

予測する際は入力データが`(入力データ数, 何個の単語で学習するか) = (1, n_seq)`になるようにします。

```python

# 単語数が短かったら0埋め
if len(input_text_num) < n_seq:
    input_text_num = [0]*(n_seq - len(input_text_num)) + input_text_num

# 予測する関数
def prediction(input_text_num):
    input_test = np.zeros((1, n_seq))
    input_test[0] = input_text_num[-n_seq:]
    y_pred = model.predict(input_test)
    return np.append(input_text_num, np.where(y_pred == np.sort(y_pred)[:, -1].reshape(-1, 1))[1])  # np.argmax(y_pred))
    #確率が2番目に高い値で文章を作ったりして遊んでたのでこう書いたけど、argmaxで十分

# 予測を実行
for i in range(1000):
    input_text_num = prediction(input_text_num)
```

学習した後は数字になっているものを単語に戻して文章を作成します。

```python

test_pred = list(map(lambda x:vocab_r[x], input_text_num[len(input_text_split):])) #学習する単語数を切り出す
pred_text = input_text + "".join(test_pred) #入力はそのまま利用する
print(pred_text)
```

作成された文字列はこのようになりました。

>認知症の原因物質が歯周病で蓄積 九州大などが発表てきたのです。「きみ、きみ、どう、きみ、何の類を類て、そのの中を見てみました。「そう、きみ、どう、きみどうの類を類て、そのの見を見て、「見るに、二十面相の部下のことには、ひとりのひとりを、「のドキドキ、、そののです。「ハハハ……。」明智は、大敵の声を、「そうに、何の、「夕方……。」「ああ、は、小林は？、きみの中を、そののにぎりを見てみました。「ああ、きみはどうてきたのです。それは、その日の美術の中におさまって、そののかわいらしいを、賊のような、顔の上をしていたのです。「、。きょうのきょうの中に、は、そののです。」美術が、賊の声を、「のすると、その棚を、「と、賊は、顔色の顔をにぎりて、そののと、壮太郎氏は

だいぶ日本語がおかしくなってしまいました…


後は必要に応じてモデルを保存しておきます。

```python

#モデルの保存
model.save('test.h5')
model.save_weights('param.hdf5')

# 読み込み
from tensorflow.python.keras.models import load_model
model = load_model('model/test.h5')
model.load_weights('model/param.hdf5')
```

# 最後に
見ての通り精度が非常に低くなってしまいました。改善のアイデアがある方がいればぜひ教えていただけると嬉しいです。

# 参考文献
- [3. Pythonによる自然言語処理　1-2. コーパスの作成方法 : 青空文庫]
(https://qiita.com/y_itoh/items/fa04c1e2f3df2e807d61)
- [Word2Vecを理解する](https://qiita.com/g-k/items/69afa87c73654af49d36)
- [kerasで学習済みword2vecをembedding layerに組み込む方法](https://tksmml.hatenablog.com/entry/2019/09/01/112900)
- [Keras LSTM の文章生成を単語単位でやってみる](http://cedro3.com/ai/keras-lstm-text-word/)
- [ディープラーニングで自動筆記 － Kerasを用いた文書生成（前編）](https://www.atmarkit.co.jp/ait/articles/1807/05/news143.html)