ProjE: Embedding Projection for Knowledge Graph Completionを実装,実験してみた
はじめに
ナレッジグラフ(KG)の埋め込み手法であるProjEを実装,実験してみました. 他の手法に比べると引用数が少ないですが,結構シンプルで精度も出て,リンクの予測ができたりベクトルの事前学習も必要ないなどいろいろな利点が存在するようです. 論文は以下になります.バージョンが2つあり,訓練のアルゴリズムは古い方のものにだけ書いてあります.
[1611.05425] ProjE: Embedding Projection for Knowledge Graph Completion
ナレッジグラフの埋め込みでは,Tramp is Presidentのような主語,述語, 目的語のセット(RDFトリプル) に対して
を満たす空間を設計することを目的としています. 有名な手法としてはTransEなどがあります.
Translating Embeddings for Modeling Multi-relational Data
本論文ではそれらの従来手法よりパラメータ数を減らしつつ,高精度なエンティティ,リンク予測を 行える埋め込み空間を設計する手法を提案しています. ただ,エンティティ予測の際,TransEなどが埋め込みベクトルの類似度のみでそれを行なっている のに対して,本手法では予測のための重みを掛けてから類似度をはかるということをしています. なので単純なエンベディングとはちょっと違うかもしれません.
手法の概要
まず,エンティティベクトルとリレーションベクトルを足し合わせる演算として次を定義します.
ここでとは,ベクトルの次元を縦横のサイズとして持つ対角行列です. つまりベクトルの各要素を定数倍するだけのものになります.ここで普通のdenseな行列でええやろとしていたら,精度があまり出ませんでした.
次に,この関係を満たすエンティティを探すため, 候補エンティティのベクトルを列として持つ行列に対して次の演算を行います.
式中のf, gはsigmoid,tanhなどの活性化関数です.ここでは,で得られたベクトルと, 全ての候補エンティティとの類似度計算をしています.の番目の要素は 候補との類似度ということになります.
最後に損失関数の部分になります. 損失としては関係を満たすエンティティと満たさないエンティティを分類するpointwise lossと, 候補エンティティが正解となる確率について負の対数尤度を最小化するlistwise,listwiseに重みを追加したwlistwiseが提案されています. point wiseの式は以下になります.
ほぼBinary Cross Entropyです.第一項は正例を評価していて,第二項は負例を評価しています. 負例は,e+rを満たさないエンティティから二項分布でサンプリングしてきます.
そしてwlistwiseは以下になります.
softmaxで出力を確信度の形式にしてから,負の対数尤度を取ります. wlistwiseでは対数尤度の足し合わせの際,(e,r)の関係を満たすエンティティの総数の逆数を 重みとして掛け合わせます.通常のlistwiseの際は重みを利用しません.
実装・実験
実装はPytorchで行い,実験はQuadro P6000を用いました. 全然整理できていないのですが,コードは以下になります.
GitHub - sheepover96/ProjE.torch: pytorch implementation of ProjE: Embedding Projection for KGC
また,tensorflowによる公式実装は以下になります.
GitHub - bxshi/ProjE: Embedding Projection for Knowledge Graph Completion
公式実装は学習結果の表示が綺麗です.あと公式なので多分正確です. 私の実装の方は公式に比べ学習が数倍早いのですが,メモリをめちゃくちゃ食います. これはネガティブサンプリングのための候補をキャッシュしているかどうかの違いだと思います.
データセットはFB15KとWN18RRを利用しました.
Relation Prediction | NLP-progress
評価は,論文中で利用されているMean RankとHIT@10を用いました. Mean Rankは,目標のエンティティが現れるまでのランクの平均で, HIT@10は,目標のエンティティが10位以内にあらわれる割合です.
結果は次のようになりました.オレオレの方はtailの予測を行なった結果ですが, 公式の方はtail, head予測どちらをやってるのかわかりませんでした.また, 実験結果はwlistwiseのものだけを示しています.
実装 | MeanRank | HITS@10 |
---|---|---|
論文 | 124 | 54.7 |
公式(tail) | 182.9 | 49.4 |
公式(head) | 275 | 41.5 |
オレオレ(tail) | 153 | 57.8 |
オレオレ(head) | 252 | 49.9 |
公式実装の方は,以前動かした時は論文ぐらいの精度だったのですが,なぜか今回は調子が悪いです. オレオレ実装は,部分的に論文に勝ったり負けたりしていますが,それっぽい結果は出ているんじゃないかと思います.
重松清さんの「疾走」を読んだ
重松清さんの「疾走」を読んだので感想を書きます.
概要
インターネッツ上では重松清さんの異作かつ名作として有名なようです. 直木賞を受賞した「ビタミンF」では家族の生々しい現実としかしそこに残る暖かい関係というのが書かれていた気がしますが, そういうものとはかけ離れていたように思えます. 私は悲壮を感じるのがとても好きなので,この作品もとても好きになれそうでした.読むのに熱中して朝日が上るのを目撃してしまいました.
なぜ読んだか
名前も顔も,そもそも存在するかもわからないけど間違いなくいい人に,おすすめの作家として重松清さんを教えてもらいました. 最初は有名な「ビタミンF」を読んでいたのですが,作中のテーマだと思われる家族の絆が私には非常に現実的かつ, 生々しく感じられてしまいました.私は母や父が苦労しつつも注いでくれる精一杯の愛というようなものに,なぜだか 私が好む悲壮とは全く反対の悲しさを見てしまいます.自分が親の人生を阻害してしまっているという申し訳なさが 浮かぶからかもしれません.なんにせよ,「ビタミンF」を読んでいると辛い気持ちになりました. なのでもっと絶望に近いような作品はないか,ということで「疾走」を読み始めました.
最初の印象
裏表紙のあらすじやネット上に書かれたストーリーの概要も見ずに読み始めたので,話に関する先入観は, 絶望の予感以外にはありませんでした.衝撃的な表紙からも,人生をなくし,行くあてもなく疾走する少年の絶望のようなものが 感じられました
内容の感想
期待を超える衝撃を常に与えてくれる小説でした.不穏な空気が漂う家族と,ビー玉越しの景色のように歪みつつも 綺麗だったりする町.それらが止まることなく壊れていく様子.素晴らしい人間になれただろうに,周りの崩壊に 合わせてどんどん壊れていく主人公.全てに期待せず別世界で生きているようなヒロイン. 心の救いであり,祈り続けてくれる神父様.それらを書く疾走感のある文章.読んでいる途中も,読み終わった後も, すげぇなぁと思いました.私はこの小説のように,人間の感情と,それによる出来事の連鎖からなる,人の人生そのもの を表すような作品がとても好きです.特にこの小説は,登場人物の環境,心情の絡みが空気感も合わせてそのまま表現 されているようで,自分が人生を追体験しているように思えました.
小説中にはよく聖書の引用が出てきます.私は,宗教というものがよくわかりませんでした. でも,どんな人生の先にも誰かの祈りと,一筋の救いがあると,信じるためにあるのかと何と無く納得しました. 人生には間違いなくどうしようもないことがたくさんあって,それは人間そのものを破壊しようとしてきます. 私が今普通に生きれているのも,そんなどうしようもないことに出会う機会がたまたま少なかったからです. 自分は運よく生きれていて,それがいつ壊れるかもわからない.もし壊れてしまってもどこかに希望や救いがある. 与えられたものを楽しみ,精一杯生きていこうと思いました.
西加奈子さんの「サラバ!」を読んだ
概要
西加奈子さんの直木賞受賞作です.出たのはもう4,5年前ですが,今でもよく本屋で見かけるので,そういうことだと思います. そういうことなので私は徹夜で読んでしまいました.
なぜ読んだか
数年前,塾でバイトをしていた時に,本好きの生徒さんがいて,おすすめとして紹介してもらいました. 私は普段,流行に乗っているようなものを読むことはほとんどないのですが,その勧めてくれた人とはいろんな趣味が あって,また人間的にもすごく立派な方で,尊敬していたので,その人の一押しなら間違いないと思いました. 残念なことに,これを勧めてもらった時の私はお金のためにバイトを詰め込んでいて,精神的余裕がありませんでした. 結局バイトを辞めて地元を出るまで,これに手をつけることはなかったのですが,それから数年が経ち,読み終わった今,すごく後悔しています.
最初の印象
父親の事情で海外移住を繰り返す主人公が,エジプトで大切な友達と出会う...
裏に書かれたあらすじを見た時,私はこの小説のテーマが私の精神と対局にあるように思えました. 家に引きこもって一人で楽しんでいるような人間が,見知らぬ土地で言葉も通じぬ人間と心を通わす などという事象に同調するのは非常に難しいのです.同調するような心が自分にあることを認めたくはないのです. 私にはない幸せな心の交流を燦々と見せつけられ,うんざりしそうだなとか思っていました.
内容の感想
とても予想を裏切られました.だいたい裏切られます.幸せな黄色で埋め尽くされていると思っていた小説内の空気は, 実はその多くがどんよりとしたもので覆われていました.汚い環境,不穏な家庭,格差,嫉妬とプライド, 後悔,そういった暗いものが常につきまとっていました.私が人生で感じた,どうしようもないネガティブな 感情というものが多く表れており,最高に同調しました. しかし,そういうものに心を魅了されたから こういうのを書いているわけではありません.私が,すごいとなったのは,そんな暗い空気の中を優しく,力強く 生きている主人公の周りの人間たちです.読んでいる時,私は,自分が生きているのが恥ずかしくなりました. 立場,能力,目標そんな自分ごとにばかり目を取られ,己のダメさばかり呪っていた自身が,周りの人間にいかに恵まれていたか, そしていかに多くのものを切り捨ててきたかを分かった気がします.
人間関係を軽視していた私が,もっと人と,心から温かく交流していきたいなぁと思える小説でした. 勧めてくれた人と会って話したいです.
青空文庫の全小説で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年前ぐらいなのですが,積ん読整理の過程で目についたので読み始めました.
最初の印象
「私の男」というタイトルから,ダメな夫に振り回されつつも世話を焼いてしまう妻の日常を書いたオムニバス小説と想像していました.表紙の目が虚と花で覆われて倒れ込んでいる人間は,共依存を続けることで同化し,枯れてしまった姿なのかなぁなど.
内容の感想
まさかここまでハードな内容だとは思いませんでした.あっさりとした表紙とタイトルの字体からは想像できなかったです.
結婚という一般的な人生最大の幸せを迎えているにも関わらず,心は別にあるような主人公.突然消えるように死んだ妖しい義父.苦労話が始まるのかと思ったら,実際はもっと恐ろしくて,壊れた親子生活が振り返られます.最後まで読み終わった後に,安定した人間との結婚式を迎えた主人公の気持ちを察すると,とても恐ろしいです.あれだけ人間として破滅した日常を送っていたのに,今更普通に幸せな生活をしていけるのかと.父親が死んだ今,誰にも言うことができないし,逃げることもできない.戻りたくても戻れない.父親は主人公を歪ませ,地獄を歩ませるために生きていたのかと思えるほどです.
現実的かどうかはわかりませんが,生々しく人間が書かれた小説を読むのは久しぶりでした.田舎の閉塞感や,それをけがらわしく見る主人公の視点の描写が生々しかったです.いろんな小説を読むようにしたいきたいです.