青空文庫の全小説でword2vecしてみる
はじめに
word2vecの日本語学習済みベクトルというと,wikiやWebサイトのコメントなどの割と説明的な文章に対して作られたようなものが多い気がします.もう少し叙情的な分散表現を得られないかな(テキトー)と考えて青空文庫の文章全体を使ってやって見たのでその過程と得られた結果について書きます.
データセット作成
データセットは以下で作ったのを用いました.
分散表現の学習
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はべんり.
今度は作ったベクトルを画像と近づけたりしてみようと思います.
使ったコードは以下においてあります.
青空文庫内の小説を全ダウンロードして解析用にきれいにする
はじめに
青空文庫のWebページはありがたいことに小説を含む全ページのソースがgithub上に公開されています. しかしそのままデータを用いようとすると,ルビがあったりエンコーディングがshift-jisだったりで少し使いづらいです. そこで青空文庫のレポジトリ内部のデータをパースして以下のような形式で保存することを行なったのでその手順について備忘録として書こうと思います.
novels ├── 芥川龍之介 │ ├── 河童 │ ├── 蜘蛛の糸 ├── 坂口安吾 │ ├── 白痴
使ったもの
- mac
- python3
小説データの保存手順
まず青空文庫のレポジトリをクローンしてきます.
git clone git@github.com:aozorabunko/aozorabunko.git
クローンしてきたレポジトリに移動すると色々入っていますが,小説データなどは./aozorabunko/cards の中に入っています.
cards内のファイルをパースする前にエンコーディングを全てshift-jisからutf-8に変更します.
find cards -name '*.html' -exec nkf -w --overwrite {} \;
自分は全て上書きしてしまいましたが,文字化けで見れなくなるので別のところに保存した方がいいかもしれません.
次にcards内の小説データを全てパースします.自分はpythonとbeautiful soupを用いました. 主に小説のhtmlデータから著者,タイトル,メインテキストを取り出し,ルビを取り除いて保存するということを行なっています. 自分以外の環境で動くかはわかりませんが使用したコードは以下です.
from bs4 import BeautifulSoup as bs import numpy as np import re, sys, os def make_dir(dir_name): if not os.path.exists(dir_name): os.mkdir(dir_name) def save_novel(file_path, text): if not os.path.exists(file_path): print('saved', file_path) with open(file_path, 'w') as f: f.write(text) if __name__ == '__main__': pass cards = os.listdir('./cards') for card in cards: if card.isdecimal() and os.path.exists('./cards/' + card + '/files'): novels = os.listdir('./cards/' + card + '/files') for novel in novels: if novel[-4:] == 'html': with open('./cards/' + card + '/files/' + novel, 'r') as f: novel_page_html = f.read() novel_parsed = bs(novel_page_html, 'html.parser') try: for rt in novel_parsed('rt'): rt.decompose() for rp in novel_parsed('rp'): rp.decompose() except AttributeError as err: print(err) novel_title = novel_parsed.find('h1', class_='title') novel_author = novel_parsed.find('h2', class_='author') novel_content = novel_parsed.find('div', class_='main_text') if novel_author is not None and novel_author.string is not None: make_dir('./novels/' + novel_author.string) save_novel('./novels/' + novel_author.string + '/' + novel_title.string, novel_content.text)
これでおおよそ14000の小説データがnovels以下に保存されました.全部でおよそ500MB程度です. 校正を行なってくださっている方たちに頭があがらねぇ...
桜庭一樹さんの「私の男」を読んだ
桜庭一樹さんの「私の男」を読んだので印象と感想を書きます.
概要
「砂糖菓子の弾丸は打ち抜けない」,「GOSICK」などのライトノベルで人気を博した桜庭一樹さんが一般小説においてもすごいことを示す直木賞受賞作です.影のある親子の壊れた生活を振り返っていく話です.
なぜ読んだか
桜庭一樹さんのことは,少しでもサブカル系に足を踏み込んでる人なら誰でも聞いたことはある「砂糖菓子の弾丸は打ち抜けない」で知っていました.
その後,表紙がかわいい「GOSICK」を買いに行ったところ,コーナーにこの本が置いてあり,一般文芸でも描いてて直木賞まで取ってるんだ〜,読んでみよう.と思って同時に購入しました. 買ったのは4年前ぐらいなのですが,積ん読整理の過程で目についたので読み始めました.
最初の印象
「私の男」というタイトルから,ダメな夫に振り回されつつも世話を焼いてしまう妻の日常を書いたオムニバス小説と想像していました.表紙の目が虚と花で覆われて倒れ込んでいる人間は,共依存を続けることで同化し,枯れてしまった姿なのかなぁなど.
内容の感想
まさかここまでハードな内容だとは思いませんでした.あっさりとした表紙とタイトルの字体からは想像できなかったです.
結婚という一般的な人生最大の幸せを迎えているにも関わらず,心は別にあるような主人公.突然消えるように死んだ妖しい義父.苦労話が始まるのかと思ったら,実際はもっと恐ろしくて,壊れた親子生活が振り返られます.最後まで読み終わった後に,安定した人間との結婚式を迎えた主人公の気持ちを察すると,とても恐ろしいです.あれだけ人間として破滅した日常を送っていたのに,今更普通に幸せな生活をしていけるのかと.父親が死んだ今,誰にも言うことができないし,逃げることもできない.戻りたくても戻れない.父親は主人公を歪ませ,地獄を歩ませるために生きていたのかと思えるほどです.
現実的かどうかはわかりませんが,生々しく人間が書かれた小説を読むのは久しぶりでした.田舎の閉塞感や,それをけがらわしく見る主人公の視点の描写が生々しかったです.いろんな小説を読むようにしたいきたいです.