自然言語処理の領域で近年注目されている技術にword2vecというのがあります。
今日は、夏休みの自由研究として、スタンフォード哲学事典のデータを使って、word2vecを作ってみたいと思います。
人文系の領域でコンピューターを使った研究は、最近デジタル・ヒューマニティーズなどと呼ばれてちょっと流行しているようです。私もデジタル・ヒューマニティーズやってみたいので、手始めにとりあえずやってみます。といっても今回の試みは遊びみたいなものですが、コードと手順は残しておくので、もっと本格的な研究のとっかかりになればと思います。
コードと手順は以下に残してあります。
- コード: https://github.com/takada-at/sep_crawl
- Google Colaboratory: https://colab.research.google.com/drive/15MB_mhYbX4vfppjuz_BkQ167xFoKRDMU
ちなみに、デジタル・ヒューマニティーズにおけるword2vecの応用については、以下の記事を参考にしました。
Vector Space Models for the Digital Humanities
ただし、今回は「作る」ことがメインで、その先の「研究」の部分はほとんど何もしていません。
word2vecとは
word2vec自体の解説はちょっと大変なのでかなりスキップします。各自検索などして調べてください。
元論文はこちらです(元論文はあまり詳細を説明してくれていない)。
大雑把に言うと、単語の意味情報を反映した高次元のベクトル(単語ベクトル)を作る技術です。以下のように、学習元のテキストに現われる単語を、高次元(だいたい100次元から600次元くらい)のベクトルに変換します。その際、このベクトルにうまく意味情報を反映させます。
"kant" => [1.0, 0.0, 1.1, ...] "hegel" => [0.0, 1.0, 2.0, ...]
うまく意味情報を反映させるとどうなるかというと、似た単語はベクトル空間の中で近い位置にきます。語同士の(コーパス内の・ある観点での)類似性関係を多次元空間にマッピングしていると考えればいいと思います。
また、うまく意味情報を反映させた結果、「アナロジー」と呼ばれる謎の演算ができるようになります。これは元々制作者が意図したものではないようですが、なぜかできるようになったらしいです。
# (ベルリン - ドイツ) + フランス = パリ (v['berin'] - v['germany']) + v['france'] => 'paris'
「ベルリン - ドイツ」ってなんだよって感じですが、気持ちとしては、ベルリンからドイツをマイナスすると首都成分になり、「首都成分 + フランス」がパリになると。
この結果はわりとインパクトがあったようですが、今ひとつ根拠が不透明なのと、やっても頓珍漢な結果が返ってくることも多いのでアレです。
まあアナロジーはおまけみたいなもので、word2vecの本来の用途を考えると、文書の分類・検索のために使用するという方が正道のような気もします。
余談 + 背景解説を少しだけ
ちなみに、word2vecは作り方がちょっとおもしろくて、ある課題を解くニューラルネットワークを学習させて、その結果は使用せず、学習したパラメーターだけをベクトルとして使います。湯葉を作るために豆腐を作って、豆腐は捨てて湯葉だけ使うみたいな感じですね。
あと、個人的な関心で一点だけ。私は言語学史に少し興味があるのですが、実はword2vecや埋め込み単語モデルで使用されている語の意味についての考え方は、チョムスキー以前のアメリカ構造主義言語学の発想に近いんですね。それはどういうものかというと、「同じような文の同じような位置に現われる語は意味が似ている」という発想です。
x はドイツの哲学者である
「カント」「ヘーゲル」という語はどちらも、上のような文のXの位置に現われる確率が高いと想定されます。もちろん学習元のデータにもよりますが、「ドイツ」「哲学者」などの単語と共起する確率が高くなるはずです。
この例だと少しわかりにくいかもしれませんが、同義語(たとえば「独身者」と「結婚していない人」)の場合であれば、出現する位置はほぼ同じになるはずです。この手法では、以上のように、共起に関する特性が近い場合に、「意味が近い」と見なします*1。
実を言うと、これは構造主義言語学で「分布分析(distributional analysis)」と呼ばれていたものとほぼ同じ考え方です*2。実際、たまに参照されることはあるようで、facebookの研究者が書いた論文で、チョムスキーの師匠であるゼリッグ・ハリスの50年代の論文が参照されているのを見つけてびっくりしました。
分布分析は、行動主義が強かった時代の産物で、その後認知科学側に立つチョムスキーなどが批判したイメージがあったのですが、大規模データが使えるようになると復活してくるというのはちょっとおもしろいですね。
手順
Google Colaboratoryという便利なものがあるので、実行はすべてその上でやっていきます。こちらにあるノートのセルを1つずつ実行していくだけで、実行できる算段です(編集不可にしてあるので実行したい場合はコピーしてください)。Colaboratoryの使い方は各自調べてください。
クローラーはすでに作ったものがあるので、そちらを使って、スタンフォード哲学事典(SEP)の全記事をダウンロードします。ここが一番時間のかかる作業で、90分くらいかかります。現在全記事で1124件あるようです。
あわせて、解析しやすいように加工してあります。ドットやカンマをとりのぞき、1文1行ずつ切り出しておきます。全データで60万行程度ありました。
例.
in the philosophical literature the term abduction is used in two related but different senses in both senses the term refers to some form of explanatory reasoning
ダウンロードが終わったら、gemsimというライブラリを使って学習させます。とりあえず300次元で学習させます。ここは数分で終わりました。
sentences = LineSentence('/content/sep_crawl/data/sep/sep-entries.txt') w2v = Word2Vec(sentences, size=300)
"kant" の類似語を表示させます。解析用にすべて小文字にしてあるので人名も小文字です。ヘーゲル、ヒューム、フィヒテなどが類似語として出てきました。
print(w2v.wv.most_similar_cosmul(positive=["kant"], topn=3)) # => [('hegel', 0.8737826943397522), ('hume', 0.8399000763893127), ('fichte', 0.8334291577339172)]
良さそうです。
可視化
せっかく作ったので、少し語彙空間を探索してみましょう。まず、カントからはじめて、類似語をちょっとずつ増やしながら集めてみます。
terms = ['kant'] clusters = [0] n = 5 for i in range(15): result = w2v.wv.most_similar_cosmul(positive=terms, topn=n) terms += [r[0] for r in result] clusters += [i + 1] * n vectors = np.vstack([w2v.wv[t] for t in terms]) # >= ['kant', 'hegel', 'hume', 'fichte', 'spinoza', 'maimon', ...]
2次元に圧縮して可視化します。
tsne = TSNE(n_components=2, perplexity=50.0) matrix = np.vstack(vectors) v2d = tsne.fit_transform(matrix) x = v2d[:, 0] y = v2d[:, 1] plt.figure(figsize=(12, 10)) s = plt.scatter(x, y, c=clusters, cmap='viridis') plt.colorbar(s) plt.grid(True) for i, n in enumerate(terms): plt.annotate(n, xy=(x[i], y[i]), xytext=(5, 2), textcoords='offset points', ha='right', va='bottom') plt.show()
いえーい、できました。カントの近くに、ヘーゲル、フィヒテ、シェリングなどがいて、見えにくいですが、遠くの方にトルストイやステビングがいますね。色は、カントから何ステップで到達したかを表わしていて、色が薄くなるほど遠いです。また、平面上の距離も大まかには語同士の「遠さ」を表わしているはず。
次に、x軸をカントとの類似度、y軸をマルクスとの類似度にして可視化してみます。なぜマルクスかというと、傾向がはっきりしていておもしろかったからです。
score0 = [w2v.wv.similarity('kant', w) for w in terms] score1 = [w2v.wv.similarity('marx', w) for w in terms] plt.figure(figsize=(9, 9)) plt.xlabel('kant') plt.ylabel('marx') plt.grid(True) s = plt.scatter(score0, score1, c=clusters, cmap='viridis') for i, n in enumerate(terms): plt.annotate(n, xy=(score0[i], score1[i]), xytext=(5, 2), textcoords='offset points', ha='right', va='bottom') plt.show()
フーコー、ホルクハイマー、ヘーゲルの「マルクス度」が高いですね。一方、ヒュームやライプニッツはマルクスから遠く、カントには近いようです。
もうちょっといろいろできそうですが、今日のところはこんなもので。