小説からRNNを用いて文章自動生成2(勉強メモ)

#小説からRNNを用いて文章自動生成
文豪の小説を学習させ、文章の自動生成に取り組んでみた勉強メモ2回目。

夏目漱石の"坊ちゃん"をリカレントニューラルネットワークで学習させ、
坊ちゃんのような軽快な文章の自動生成に挑戦する。


前回の記事"https://qiita.com/umez/items/3f17bab84e61508aed6c"
では、疎な行列のままRNNにいれ、文章にならない文章を作成してしまいました。

今回は単語の分散表現を実装し、結果を確認してみます。


##参考URL
【Python】機械学習で文章を自動生成する方法
https://sleepless-se.net/2018/08/20/python-machine-learning/
keras Embedding層
https://keras.io/ja/layers/embeddings/
keras 目的関数
https://keras.io/ja/objectives/

##準備

ニューラルネットワークはkerasでモデル作成します。
文章の解析にはjanomeを利用しました。

```python
from keras.layers import Dense, Activation, SimpleRNN
from keras.models import Sequential
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import janome
import numpy as np
```

前回と同じく、坊ちゃんを分かち書きします。
ローカルにダウンロードしていた坊ちゃんの情報を読み込みます。

```python
FILE_PATH = "./bocchan.txt"
text=""
with open(FILE_PATH, 'r',encoding="Shift-JIS") as f:
    for line in f:
        t = Tokenizer(wakati=True)
        lines = t.tokenize(line)
        text += " ".join(lines)
text[0:500]
```

'坊っちゃん夏目 漱石+ 目次一親 譲 お やゆ ずり の 無鉄砲 むてっぽうで 小 供 の 時 から 損 ばかり し て いる 。 小学校 に 居る 時分 学校 の 二 階 から 飛び降り て 一 週間 ほど 腰 こし を 抜 ぬかし た 事 が ある 。 なぜ そんな 無闇 むやみ を し た と 聞く 人 が ある かも 知れ ぬ 。 別段 深い 理由 で も ない 。 新築 の 二 階 から 首 を 出し て い たら 、 同級生 の 一 人 が 冗談 じ ょうだんに 、 いくら 威張 いばっ て も 、 そこ から 飛び降りる 事 は 出来 まい 。 弱虫 や ー い 。 と 囃 は やし た から で ある 。 小使 こづかい に 負ぶさっ て 帰っ て 来 た 時 、 おやじ が 大きな 眼 め を し て 二 階 ぐらい から 飛び降り て 腰 を 抜かす 奴 やつ が ある か と 云い っ た から 、 この 次 は 抜かさ ず に 飛ん で 見せ ます と 答え た 。親類 の もの から 西洋 製 の ナイフ を 貰 もらっ て 奇麗 きれい な 刃 は を 日 '

strをリスト型に変換します。
区切り文字は半角の空白です。

```python
#リスト = str.split(‘区切り文字’)
text_list = text.split(" ")
#リスト型になっているか確認
text_list[8]#'無鉄砲'

#分かち書きした単語数を算出
text_uniqlist= list(set(text_list))
len(text_uniqlist)#坊ちゃんの使用単語数は6832
```

準備として、単語のID化と、学習データの作成を行います。

```python
#坊ちゃんに含まれる単語とIDを辞書形式で保存する
ID2word = dict((i,c) for i, c in enumerate(text_uniqlist))
word2ID = dict((c,i) for i, c in enumerate(text_uniqlist))

#学習データの作成
SEQLEN = 10 #文書の次元数
xtrain_chars = []
ytrain_chars = []

for i in range( 0,len(text_list) - SEQLEN):
    xtrain_chars.append(text_list[i:i+SEQLEN])  #学習対象のデータ一覧
    ytrain_chars.append(text_list[i+SEQLEN])  #予測対象
```


単語IDを分散表現していきます。
これは、単語IDを固定次元の密ベクトルに変換する作業になります。
本記事では、kerasのEmbedding層(https://keras.io/ja/layers/embeddings/)
を用います。

Embedding層の定義方法ですが、
Embedding(語彙数, 分散ベクトルの次元数, 入力の系列長))となります。

```python
model.add (Embedding(num_word, OUTPUT_DIM,input_length=SEQLEN))
```

　語彙数:"num_word" 
　　：坊ちゃんの使用単語数(6832単語)
　分散ベクトルの次元数:"OUTPUT_DIM"　
　　：単語IDを何次元のベクトルに変換するか。1つの単語IDが"OUTPUT_DIM"次元の固定密ベクトルに変換されます。
　入力の系列長:"input_length"
　　：単語の並びの数(先ほどの"#学習データの作成時"にSEQLEN(=10)単語で定義)

ちなみにEmbeddingレイヤーはモデルの最初のレイヤーとしてのみ利用できます。
Embedding層の入力・出力のサイズは下記の通り。
この層が出力したテンソルを、SimpleRNN層に引き渡していきます。

　入力：(BATCH_SIZE , SEQLEN)
　出力：(BATCH_SIZE , SEQLEN, OUTPUT_DIM)

誤差関数lossは、"sparse_categorical_crossentropy"を使用します。
慣習的な方法では、ラベルにOne-hot encodingを施さなくてはならないのですが、この誤差関数を用いることで、その処理をスキップすることができます。

```python
EPISODE = 5
HIDDEN_SIZE = 128
BATCH_SIZE = 32
OUTPUT_DIM = 300
num_word = len(text_uniqlist)#6832単語

#学習モデルの構築
model = Sequential()
model.add (Embedding(num_word, OUTPUT_DIM,input_length=SEQLEN))#語彙数, 分散ベクトルの次元数, 入力の系列長
model.add (SimpleRNN( HIDDEN_SIZE , return_sequences = False , input_shape = ( SEQLEN, OUTPUT_DIM ),unroll = True ))
model.add ( Dense(num_word) )
model.add ( Activation("softmax"))
model.compile( loss = "sparse_categorical_crossentropy" , optimizer = "rmsprop" )#categorical_crossentropyと同じですが，スパースラベルを取る点で違います

model.summary()

for epoch in range(EPISODE):
    model.fit(np.array(xtrain_chars),np.array(ytrain_chars),batch_size=BATCH_SIZE)
```

ネットワーク構造は下記の通り。

```python
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (None, 10, 300)           2049600   
_________________________________________________________________
simple_rnn_1 (SimpleRNN)     (None, 128)               54912     
_________________________________________________________________
dense_1 (Dense)              (None, 6832)              881328    
_________________________________________________________________
activation_1 (Activation)    (None, 6832)              0         
=================================================================
```

続いて学習したモデルを作成して、文章を生成していきます。

予測した単語IDを取得するために、predict_classesメソッドを用いています。
predictメソッドですと、Embeddingした単語ベクトルが取得されてしまいます。
predict_classesメソッドを使用することで、Embeddingする前の単語IDが
取得可能です。

```python
NUM_PREDS_PER_EPOCH = 60

test_index = np.random.randint(len(xtrain_chars))
test_chars =[]
test_chars = text_list_id[test_index:test_index+SEQLEN]
test_chars = np.array(test_chars)
test_chars = test_chars[np.newaxis, :]


print("(元となる文章):",end="")
for i in range(len(test_chars[0])):
    print(ID2word[test_chars[0][i]],end=" ")

for i in range (NUM_PREDS_PER_EPOCH):
    pred = model.predict(np.array(test_chars)) #予測した単語ベクトルを取得
    pred_label = model.predict_classes(np.array(test_chars)) #予測した単語IDを取得
    test_chars=np.delete(test_chars, 0)
    test_chars=np.append( test_chars, pred_label )
    test_chars = test_chars[np.newaxis, :]
    print()
    print(' ======================== ')
    for i in range(len(test_chars[0])):
        print(ID2word[test_chars[0][i]],end=" ")

```


(元となる文章):人 を 驚 ろか し や がっ て 、 どう 
 ======================== 
を 驚 ろか し や がっ て 、 どう し 
 ======================== 
驚 ろか し や がっ て 、 どう し て 
 ======================== 
ろか し や がっ て 、 どう し て いる 
 ======================== 
し や がっ て 、 どう し て いる 。 
 ======================== 
や がっ て 、 どう し て いる 。 おれ 
 ======================== 
がっ て 、 どう し て いる 。 おれ は 
 ======================== 
て 、 どう し て いる 。 おれ は は 
 ======================== 
、 どう し て いる 。 おれ は は ない 
 ======================== 
どう し て いる 。 おれ は は ない 。 
 ======================== 
し て いる 。 おれ は は ない 。 おれ 
 ======================== 
て いる 。 おれ は は ない 。 おれ は 
 ======================== 
いる 。 おれ は は ない 。 おれ は は 
 ======================== 
。 おれ は は ない 。 おれ は は ない 
 ======================== 
（以下略）


"おれ は は ない 。"を延々と繰り返していく結果となりました。

次回の記事では、学習アルゴリズムを変更or坊ちゃんの要約に取り組んでみようと思います。