羊をめぐるブログ

趣味の色々について書きます

青空文庫の全小説でword2vecしてみる

はじめに

word2vecの日本語学習済みベクトルというと,wikiやWebサイトのコメントなどの割と説明的な文章に対して作られたようなものが多い気がします.もう少し叙情的な分散表現を得られないかな(テキトー)と考えて青空文庫の文章全体を使ってやって見たのでその過程と得られた結果について書きます.

データセット作成

データセットは以下で作ったのを用いました.

serenard.hatenablog.com

分散表現の学習

gensim内のword2vecを使って分散表現の学習をします.

gesim.word2vec.Word2Vecには次のようなtokenizeした文を要素として持つリストを与えます.

[
 [...tokenizeした1文],
 ...,
 ['\u3000', '底本', 'Project Gutenberg', 'Canada', 'です', '。', 'この', '翻訳', '独自', '行く', '、', '先行', 'する', '訳', '類似', 'する', '部分', 'ある', '偶然', 'による', 'です', '。', 'しかし', '、', '大型', '宇宙船', '飛び交う', '時代', 'なる'],
 ['\u3000', 'この', '訳文', 'Creative Commons', 'CC', 'BY-SA', '公開', 'する'],
....,
]

データセット内の小説は1行に大体2文ぐらいが入っているのですが,分割が面倒なため複数文をまとめてtokenizeして利用します.tokenizerはMeCabを使いました.また,stop wordとしてSlothlibにひらがな1文字を付け加えたものを定義しています.最終的なコードは以下のような形です.

import MeCab as mecab

hiragana = [ chr(i) for i in range(12354, 12436)]
with open('stop_words.txt', 'r') as f:
    content = f.read()
    stopwords = [word for word in content.split('\n')]
stopwords.extend(hiragana)

def tokenizer(words, part_use=['名詞', '形容詞'], normalize_word=True):
    tagger = mecab.Tagger('-d /usr/local/mecab/lib/mecab/dic/mecab-ipadic-neologd ')
    tagger.parse('')
    mecab_word_nodes = tagger.parseToNode(words)
    tokenized = []
    while mecab_word_nodes:
        elements = mecab_word_nodes.feature
        word = mecab_word_nodes.surface
        element_list = elements.split(',')
        if normalize_word:
            word = element_list[6]
        part = element_list[0]

        if not word in stopwords:
            if part is None or part_use is None:
                tokenized.append(word)
            elif part in part_use and not word.encode('utf-8').isalnum():
                tokenized.append(word)

        mecab_word_nodes = mecab_word_nodes.next

    return tokenized


all_novel_lines = []
for author in os.listdir('./novel')
    for novel in os.listdir(os.oath.join('./novel', author)):
        with open(os.path.join(os.path.join('./novel', author, novel)), 'r') as f:
            novel_content = f.read()
            novel_text_lines = novel_content.split('\n')
            tokenized_novel_text_lines = [ tokenizer(text_line, part_use=None) for text_line in novel_text_lines]
            all_novel_lines.extend(tokenized_novel_text_lines)

model = word2vec.Word2Vec(all_novel_lines, iter=100)
model.save('aozora_model.model')

ハイパーパラメータは学習epoch数以外はデフォルトとしました(あまり詳しくない).

学習後のモデルの解析

学習後のモデルが普通のword2vecとどんな感じで違うのか見てみます.

比較対象としては http://www.cl.ecei.tohoku.ac.jp/~m-suzuki/jawiki_vector/ を用いました.

類義語

まずは自分が昔はまっていた江戸川乱歩さんにありがちな”怪人”に類似語を調べてみます.

通常のwikiベクトルでは,

#Wikipedia
wiki_model = KeyedVectors.load_word2vec_format('./pickle_objects/entity_vector.model.bin', binary=True)
print(wiki_model.wv.most_similar(positive=['怪人'], topn=10))
[('怪獣', 0.8180444240570068), ('ウルトラマン', 0.7956470251083374), ('魔人', 0.7853308916091919), ('[ウルトラセブン_(キャラクター)]', 0.7787925004959106), ('[ウルトラマン]', 0.7747620344161987), ('[バルタン星人]', 0.7678325176239014), ('[怪獣]', 0.7658182382583618), ('[ウルトラQの登場怪獣]', 0.7623452544212341), ('[ショッカー]', 0.759750247001648), ('宇宙人', 0.7564390897750854)]

完全にウルトラマンですね.

一方,今回学習した青空ベクトルでは,

#青空ベクトル
w2vmodel = word2vec.Word2Vec.load(MODEL_PATH2)
print(w2vmodel.wv.most_similar(positive=['走る'], topn=10))
>[('面相', 0.7113069891929626), ('怪物', 0.6929771900177002), ('透明人間', 0.6617274284362793), ('宇宙怪人', 0.6395553946495056), ('青銅の魔人', 0.6285749077796936), ('丸木', 0.6179652214050293), ('怪盗', 0.6169403195381165), ('恐怖王', 0.6136674880981445), ('骸骨', 0.6076086759567261), ('黄金仮面', 0.5900589227676392)]

懐かしいタイトルがたくさん並んでいます.

次に,走れメロスからとって,動詞の'走る'を比較して見ます.

wiki_model = KeyedVectors.load_word2vec_format('./pickle_objects/entity_vector.model.bin', binary=True)
print(wiki_model.wv.most_similar(positive=['走る'], topn=10))
>[('通る', 0.7714841365814209), ('横切る', 0.7033817172050476), ('駆け抜ける', 0.6959885358810425), ('走り抜ける', 0.6874959468841553), ('貫く', 0.6851664781570435), ('走り', 0.6783742904663086), ('歩く', 0.6761924624443054), ('通り抜ける', 0.6483071446418762), ('抜ける', 0.6402491331100464), ('走れる', 0.6378321647644043)]

類義語が多いような感じでしょうか.

w2vmodel = word2vec.Word2Vec.load(MODEL_PATH2)
print(w2vmodel.wv.most_similar(positive=['走る'], topn=10))
>[('走り出す', 0.7814075350761414), ('駆ける', 0.7684956192970276), ('駆け出す', 0.7316458225250244), ('走り去る', 0.7313833236694336), ('馳', 0.7097421884536743), ('横切る', 0.7063639163970947), ('駈ける', 0.7036117315292358), ('疾走', 0.6949977278709412), ('疾駆', 0.6659486293792725), ('はしる', 0.6658031940460205)]

類義語の中でも疾走感を付け足したような単語が並んでいる気がします.

最後に'メロス'を試して見ます.

#Wikipedia
print(wiki_model.wv.most_similar(positive=['メロス'], topn=10))
>[('[メリアドク・ブランディバック]', 0.6494805812835693), ('ニルス', 0.6397469639778137), ('クローディオ', 0.6338703632354736), ('[ベルガリオン]', 0.624444842338562), ('[エオウィン]', 0.6243359446525574), ('ローハン', 0.62108314037323), ('アベル', 0.6190121173858643), ('アンジェリカ', 0.6186339259147644), ('エリサ', 0.6166181564331055), ('エルザ', 0.6157675385475159)]

世界の児童文学作品などのキャラクターが多く出てきているようです.

#青空ベクトル
print(w2vmodel.wv.most_similar(positive=['メロス'], topn=10))
>[('素戔嗚', 0.4792671203613281), ('鞭', 0.4768710732460022), ('勇ましい', 0.47102075815200806), ('ふりほどく', 0.4657329320907593), ('力いっぱい', 0.45828065276145935), ('ハーキュリーズ', 0.4551655948162079), ('振り上げる', 0.45437756180763245), ('遮二無二', 0.452807754278183), ('セリヌンティウス', 0.45001697540283203), ('勇む', 0.4490892291069031)]

小説の情景が浮かぶような単語が出てきていそうです.セリヌンティウスもちゃんといます.

ベクトルの足し引き

'メロス'から'走る'を引いて見ます.

#青空ベクトル
merosu = w2vmodel.wv['メロス']
hashiru = w2vmodel.wv['走る']
sub = merosu - hashiru
print(w2vmodel.most_similar([sub]))
>[('黄道吉日', 0.4538549482822418), ('メロス', 0.4192773997783661), ('真心', 0.41565418243408203), ('こそ', 0.41269564628601074), ('総統', 0.4102414846420288), ('政恒', 0.4036811888217926), ('潔い', 0.403376042842865), ('心から', 0.4014996588230133), ('生殺与奪の権', 0.40138477087020874), ('得度式', 0.3940085172653198)]

メロスから走りを抜くと真心が残るようです.

まとめ

word2vecはおもしろい.gensimはべんり.

今度は作ったベクトルを画像と近づけたりしてみようと思います.

使ったコードは以下においてあります.

GitHub - sheepover96/aozora_analyzer