小説から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を利用しました。
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
前回と同じく、坊ちゃんを分かち書きします。
ローカルにダウンロードしていた坊ちゃんの情報を読み込みます。
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をリスト型に変換します。
区切り文字は半角の空白です。
#リスト = str.split(‘区切り文字’)
text_list = text.split(" ")
#リスト型になっているか確認
text_list[8]#'無鉄砲'
#分かち書きした単語数を算出
text_uniqlist= list(set(text_list))
len(text_uniqlist)#坊ちゃんの使用単語数は6832
準備として、単語のID化と、学習データの作成を行います。
#坊ちゃんに含まれる単語と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(語彙数, 分散ベクトルの次元数, 入力の系列長))となります。
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を施さなくてはならないのですが、この誤差関数を用いることで、その処理をスキップすることができます。
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)
ネットワーク構造は下記の通り。
_________________________________________________________________
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が
取得可能です。
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坊ちゃんの要約に取り組んでみようと思います。