LoginSignup
6
4

More than 5 years have passed since last update.

ニューラルネットワークをつかって推理小説家をつくる2

Posted at

はじめに

推理小説家を学習したSeq2Seqを使って文章生成します。
前回:https://qiita.com/iss-f/items/9bdc67dd39da7590465f

さすがにそろそろ素晴らしい小説家が作れても良いかと思います。

前回との変更点は2点です。

まずは、学習文章長の変更です。
前回は、入力文章長と出力文章長がそれぞれ5、10となる文章のみを学習させていました。これが原因で学習データが少なくなってしまっていた可能性があります。今回は、入力、出力文章長が[5,10],[10,15],[20,25],[40,40]となる文章を学習させます。

次に、重みロードアルゴリズムの修正です。
これまでのモデルでは、学習後にテストする際、学習した重みをうまくロードできていなかったので、その修正です。

モデル

文章を単語ごとに区切り、word2vecでベクトル化し、LSTMに順番に突っ込んでいきます。

seq2seq_wv.png

文章の分割はmecab、word2vecはgensimを使用します。
LSTMはKerasを使って書きます。以下コードです。

input_dim = self.word_feat_len
output_dim = self.word_feat_len

encoder_inputs = Input(shape=(None, input_dim))
encoder_outputs, state_h, state_c = LSTM(self.latent_dim, return_state=True)(encoder_inputs)
encoder_states = [state_h, state_c]

decoder_inputs = Input(shape=(None, input_dim))
decoder_lstm = LSTM(self.latent_dim, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_inputs, initial_state=encoder_states)
decoder_dense = Dense(output_dim, activation='linear')
decoder_outputs = decoder_dense(decoder_outputs)

self.sequence_autoencoder = Model([encoder_inputs, decoder_inputs], decoder_outputs)

入出力、隠れ層の次元は、それぞれ128次元、256次元となります。

seq2seq_wv_dim.png

学習データ

word2vecに学習させるデータは、青空文庫から以下の作家の
江戸川乱歩、夢野久作、大阪圭吉、小栗虫太郎、海野十三
の全小説を用います。有名な推理小説家を選んだのですが、他におすすめあれば教えていただきたいです。

一方、Seq2Seqには、江戸川乱歩の小説のみを学習させます。word2vecへより多くの文章を学習させベクトル表現を多様化することでなにか良いことがあるんじゃないかという期待です。

データ数は、以下のようになります。

江戸川乱歩 夢野久作 大阪圭吉 小栗虫太郎 海野十三 合計
文章数 71713 56196 4702 14113 125607 272331
ユニーク単語数 34316 43809 11501 23884 53419 166929

ファイルサイズは合計で38Mあったので、学習データとしては十分かと。

ある作家の小説を全てダウンロードするプログラムが若干めんどくさかったので張っておきます。
青空文庫では、作家別にIDが振られているので、
python3 get_aozora.py -id <作家ID>
のように実行すると、その作家のファイルがzip形式でまとめてダウンロードできます。

get_aozora.py

import urllib.request
import os
import sys

def get_body(url):
    try:
        response = urllib.request.urlopen(url)
        print(">>>> get body : ",url)
        return response.read().decode('utf-8')
    except:
        print(" ----- html error",url)
        return ""


def get_cards(body):
    __cards = []
    for value in body.split("\n"):
        if ('<li>' in value) and ('.html' in value) :
            __cards.append(value.split('"')[1].split('/')[-1].split(".")[0].replace("card",""))
    return __cards


def get_zip_code(body,cardId):
    zipcode = []
    for value in body.split("\n"):
        if ('.zip' in value) and ('files' in value) and (cardId in value):
            zipcode.append(value.split('"')[1].split("/")[-1])
    if len(zipcode) != 0: return zipcode[0]
    if len(zipcode) == 0: return ""


def get_novel_body(authorId,cardId):
    url = "http://www.aozora.gr.jp/cards/"+ authorId + "/card" + cardId + ".html"
    return get_body(url)


def zero_padding(authorId):
    """ authorIDが6けたなので足りないぶんを0で埋める """
    if len(str(authorId)) < 6 :
        zero = ""
        for i in range(6 - len(authorId)):
            zero += "0"
        authorId = zero + authorId
    return authorId


def download(url,savedir,filename):
    print("download : " + filename)
    try:
        urllib.request.urlretrieve(url, savedir+filename)
        print("save ok to "+savedir+filename)
    except:
        print("html error cood",url)
        return ""


def get_path():
    return os.path.dirname(os.path.abspath(__file__))


def main():
    """
    author_id is set in a format excluding a zero.
    for example, author_id of 芥川 竜之介 is 832
    """

    if ("-id" in sys.argv) and (len(sys.argv) == 3 ):
        authorId = sys.argv[2]
    else :
        print("invalid argument.")
        print("you must set '-id' as option")
        print("and set author_id list after 'id'")
        exit(0)

    project_path = get_path()
    SAVE_DIR = "/"

    if os.path.isdir(project_path + SAVE_DIR):
        print("ok. save " + project_path + SAVE_DIR + " exist.")
    else:
        print("file no exist.")
        print("make file to ",project_path + SAVE_DIR)
        os.makedirs(project_path + SAVE_DIR)

    url = "http://www.aozora.gr.jp/index_pages/person"+authorId+".html"
    body = get_body(url)
    cardIds = get_cards(body)

    authorId = zero_padding(authorId)

    zip_code = []
    for value in cardIds:
        body = get_novel_body(authorId, value)
        zip_code.append(get_zip_code(body, value))


    for value in zip_code:
        print(authorId)
        print(value)
        url = "http://www.aozora.gr.jp/cards/" + authorId + "/files/" + value
        download(url,project_path + SAVE_DIR, value)

if __name__ == "__main__":
   main()

学習

最適化関数と損失関数は、それぞれrmsporp、最小二乗法を使います。パラメタはKerasのデフォルトを使用します。

optimizer = 'rmsprop'
loss = 'mean_squared_error'

batch_sizeは64、epoch数は1です。

batch_size:64
epoch:1

これを1stepとし、CPUで3日間、約3万stepほど学習しました。

結果

学習結果は以下のようになりました。

(5, 10)   :  [0.053629111498594284, 0.7578125]
(10, 15)  :  [0.16283731162548065 , 0.61874997615814209]
(20, 25)  :  [0.3658660352230072  , 0.52687501907348633]
(40, 40)  :  [0.44818806648254395 , 0.49375000596046448]

これ以上、回しても変わらないようだったので、学習を止めたのですが、いまいちそうな感じはします。やはり文章長が長くなるにつれ、誤差と精度はどちらも大きくなっています。

やはり、小説だと学習データとしては、不十分なのでしょうか.....。

文章生成モデル

結果はともかく、一応文章生成させて見ましょう。
文章生成用のモデルは以下となります。BOSは文章開始を表す特殊記号です。word2vecでベクトル化できるように、word2vecにBOSも学習させておきます。

seq2seq_wv2.png

文章生成時には、デコード部の入出力を変更する必要があるので、単純に重みをロードするだけではだめでした。学習済みモデルをロードし、ロードした学習済みモデルの重みを初期重みとして設定しています。もう少し良い書き方があれば教えていただきたいです.....。

コードは以下です。

input_dim = self.word_feat_len //128次元
output_dim = self.word_feat_len //128次元

// 学習済みモデルのロード
model = load_weights()
_, _, encoder_lstm_l, decoder_lstm_l, decoder_dense_l = model.layers

// エンコーダーモデル部
encoder_inputs = Input(shape=(None, input_dim))
// 学習済みモデルの重みを初期重みとして設定
_, state_h, state_c = LSTM(self.latent_dim, return_state=True, weights=encoder_lstm_l.get_weights())(encoder_inputs)
encoder_states = [state_h, state_c]
self.encoder_model = Model(encoder_inputs, encoder_states)

// デコーダー部
decoder_state_input_h = Input(shape=(self.latent_dim,))
decoder_state_input_c = Input(shape=(self.latent_dim,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]

decoder_inputs = Input(shape=(None, input_dim))
// 学習済みモデルの重みを初期重みとして設定
decoder_lstm = LSTM(self.latent_dim, return_sequences=True, return_state=True, weights=decoder_lstm_l.get_weights())
decoder_outputs, state_h, state_c = decoder_lstm(decoder_inputs, initial_state=decoder_states_inputs)

decoder_states = [state_h, state_c]
decoder_dense = Dense(output_dim, activation='linear', weights=decoder_dense_l.get_weights())
decoder_outputs = decoder_dense(decoder_outputs)

self.decoder_model = Model(
            [decoder_inputs] + decoder_states_inputs,
            [decoder_outputs] + decoder_states)

文章生成

実際に文章生成させた結果は以下になります。

(5,10)

>>  そうです。
そいつはなーるほどはずは気づきわかります。
--
>>  わかったか。
サア、かむり、、、。
--
>>  へんだなあ。
どこへあっち筈はないな。
--
>>  宗像さんですか。
中村ですう。

中村ですう。に笑います。

(10,15)

>>  次はこの三通の手紙だ。
これ、真一今さらのはのでは面白いはが、、。
--
>>  あいつの顔、ピカピカ光ったよ。
ごらん、黒いぽかり、わかっ、ね。
--
>>  二度も三度も死んだ男だ。
お前だと言うて、嘘、、、、だ。

文章として成立してないこともなくもない....

(20,25)

>>  潜航艇だよ。
こいつケレンコ、、三根、、の、、、、にげだし、魔人にげだしポチ、、、、、、、、
--
>>  ああ、明智は何をいおうとしているのでしょう。
ぼく、、は、三根三根をたすけだしを分って、、のは、でしょだろう。
--
>>  おや、どうかなすったのですかい。
背中のすっくとヒトミ鉄人、おどろいです。

この辺りから全く意味不明です。

(40,40)

>>  まあ、こちらへおいで。
正吉、正吉の、三根、、三根、三根、三根、、三根、三根、三根、立ちあがり、、、、、、三根、源一、三根、源一、ポチ、、、
--
>>  いつもの遣り口とは違っていた。
真一の、の、の、、にげだし、、、、、、、、、、、、、、、、、、話し、三根して、、、、、、、、
--
>>  いつまでにらみあっていてもはてしがないので、小林君はためしに一歩前に進んでみました。
すると、、三根、、三根三根三根三根の小林少年のにげだしの立ちあがりの立ちあがり、立ちあがっ、にげだし、にげだし、、、、にげだし、にげだしポチふりむきました。
--
>>  小林君に知恵をおさずけになったのは、むろん明智先生です。
青木の支配人、、隆夫、、、三根、三根三根三根、三根、、三根、、、三根、、三根、にげだし、、、、、、、、、、、、

単語の繰り返しが起きてます。

まとめ

Seq2Seqに推理小説を学習させ、文章生成を行わせました。
推理小説家への道はまだまだ遠いみたいです....。
次は、Attentionをつけてみるか、CNNを導入してみようかと思います。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4