【Python】Word2Vecの使い方

# はじめに
既に様々な場所で取り上げられているWord2Vecですが、改めてPythonを使ったWord2Vecの使い方を紹介します。
使い方と言っても特に難しい事はなく、コーパス（テキストや発話を大規模に集めてデータベース化した言語資料）からモデルを作成し、作成したモデルを様々な事例に活用するだけです。

# Word2Vecとは？
名前の通り、単語をベクトル化する手法のことです。そして、このベクトル化したものを単語の分散表現と呼びます。

2013年にGoogleが開発＆公開したもので、その仕組みについては書籍やネットなどで数多く発表されています。
最近では、朝日新聞社が自社の保有記事をコーパスとしたモデルを公開する[^1]など、自然言語処理の分野では欠かせない技術の一つになっています。

基本的には、**同じような意味や使われ方をする単語は同じような文脈の中に登場する**という考えのもと、単語をベクトル化しています。
このため、同じような意味や使われ方をする単語同士はベクトル空間上で近い場所に存在、つまりコサイン類似度が高くなります。
そして、

- 「王様」ー「男性」＋「女性」=「お姫様」
- 「日本」ー「東京」＋「ソウル」=「韓国」

というように、単語の足し算や引き算が可能になりました。

Word2Vecが登場するまでは、単語の表現方法としてはone-hotベクトルや単語文脈行列をSVD（特異値分解）で次元圧縮したベクトルなどが使われていましたが、2017年現在ではWord2Vecがデファクトスタンダードと言っても過言ではない状況になりました。

# Word2Vecのモデル作成
## コーパスの用意
まずは、モデル作成に必要なコーパスを用意する必要があります。
今回は誰でも入手可能なWikipediaのデータを使用します。

日本語のWikipediaのデータはダンプ化されたものが用意されています。
https://dumps.wikimedia.org/jawiki/latest/

Wikipediaの記事本文が欲しいので、

```
$ curl https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2 -o jawiki-latest-pages-articles.xml.bz2
```

として記事本文のXMLファイルをダウンロードします。
テキストデータですが容量が約3GBあるので、ダウンロードにかなり時間が掛かります。

そして、XMLファイルをパースして記事本文だけを取り出します。
今回は[WikiExtractor](https://github.com/attardi/wikiextractor)というGithubで公開されているフリーのソフトウェアを利用しました。
使い方はREADMEにも記載されていますが、インストールしてコマンド一発叩くだけです。

```
$ python setup.py install
$ python WikiExtractor.py jawiki-latest-pages-articles.xml.bz2
```

こちらもダウンロードと同様にかなり時間が掛かります。
結果は`text`ディレクトリ配下に

```
text/AA/wiki_00
text/AA/wiki_01
...
text/AZ/wiki_24
```

というように数千ファイルに分割されて出力されますが、

```
$ find text/ | grep wiki | awk '{system("cat "$0" >> wiki.txt")}'
```

のように`awk`と`cat`コマンドを使えば一つのファイルに一発で集約できます。
なお、WikiExtractorは英語のWikipediaのデータを想定して開発されたものだと思いますが、日本語のWikipediaのデータでも問題なくパースできました。

これでコーパスの用意は完了です。

## モデルの作成
次に、モデルを作成します。

今回は[gensim](https://radimrehurek.com/gensim/index.html)というPythonのフリーのライブラリを利用しました。
pipを使えばすぐにインストールできます。

```
$ pip install --upgrade gensim
```

gensimを使ってモデルを作成する場合、コーパスを事前に分かち書き（文章において語の区切りに空白を挟んで記述すること）にする必要があります。
これは[MeCab](http://taku910.github.io/mecab/)を使えば、すぐにできます。なお、MeCabのインストール方法は割愛します。

```
$ mecab -Owakati wiki.txt -o wiki_wakati.txt
```

ただ、原因はよく分かりませんが、このままだと`wiki_wakati.txt`にバイナリ文字が混じってしまいます。
このため、`nkf`コマンドを使ってuft-8にビシッと揃えます。

```
$ nkf -w --overwrite wiki_wakati.txt
```

コーパスを分かち書きにできれば、モデルを作成します。
[logging](https://docs.python.jp/3/howto/logging.html)を使えば、モデル作成の進捗状況が標準出力に表示されるのでお薦めです。
また、`size`, `min_count`, `window`などのパラメータの意味については[マニュアル](https://radimrehurek.com/gensim/models/word2vec.html#gensim.models.word2vec.Word2Vec)をご覧下さい。

``` python
from gensim.models import word2vec
import logging

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
sentences = word2vec.Text8Corpus('./wiki_wakati.txt')

model = word2vec.Word2Vec(sentences, size=200, min_count=20, window=15)
model.wv.save_word2vec_format("./wiki.vec.pt", binary=True)
```

`wiki.vec.pt`が作成したモデルです。
これでモデルの作成は完了です。

# Word2Vecの活用事例
## 類義語の発見
Word2Vecを使えば、類義語を見つけることができます。
例えば、`講義`の類義語を見つけたい場合、次のように数行書くだけです。

``` python
from gensim.models import KeyedVectors

wv = KeyedVectors.load_word2vec_format('./wiki.vec.pt', binary=True)
results = wv.most_similar(positive=['講義'])
for result in results:
    print(result)
```

結果は以下のようになります。
コサイン類似度が高い順に並んでおり、概ね良さそうな結果になっていますね。

```
('授業', 0.6795130968093872)
('講読', 0.6785069704055786)
('講演', 0.662272572517395)
('講話', 0.6550379991531372)
('聴講', 0.6494878530502319)
('講座', 0.6335989832878113)
('レクチャー', 0.6315985918045044)
('復習', 0.580752968788147)
('進講', 0.5763939619064331)
('大学院生', 0.5596680641174316)
```

## テキスト間の類似度
Word2Vecを使えば、類義語の発見だけでなくテキスト間の類似度も計算することができます。
これも次のように30行程度で書くことができます。

ここでは、テキストを構成する単語（名詞、動詞、形容詞に限定）のベクトルの平均を当該テキストのベクトルと見なします。
もちろん単純平均だけでなく、tf-idfを使って単語毎に重みを付けることなども可能です。

**2020/07/21追記**
重み付けの方法は色々と提案されていますが、最近の論文を幾つか読むと[SIF(smooth inverse frequency)](https://openreview.net/pdf?id=SyK00v5xx)による重み付けが精度が高くなるようです。気になる人は調べてみて下さい。

``` python
import MeCab
from gensim.models import KeyedVectors
import numpy as np

mt = MeCab.Tagger('')
wv = KeyedVectors.load_word2vec_format('./wiki.vec.pt', binary=True)

# テキストのベクトルを計算
def get_vector(text):
    sum_vec = np.zeros(200)
    word_count = 0
    node = mt.parseToNode(text)
    while node:
        fields = node.feature.split(",")
        # 名詞、動詞、形容詞に限定
        if fields[0] == '名詞' or fields[0] == '動詞' or fields[0] == '形容詞':
            sum_vec += wv[node.surface]
            word_count += 1
        node = node.next

    return sum_vec / word_count


# cos類似度を計算
def cos_sim(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))


if __name__ == "__main__":
    v1 = get_vector('昨日、お笑い番組を見た。')
    v2 = get_vector('昨夜、テレビで漫才をやっていた。')
    v3 = get_vector('昨日、公園に行った。')

    print(cos_sim(v1, v2))
    print(cos_sim(v1, v3))
```

結果は以下のようになります。
1つ目のテキストに対して、3つ目よりも2つ目のテキストの方が類似度が高くなっており、こちらも概ね良さそうな結果になっていますね。

```
0.496827236741
0.130445822764
```

## さらなる活用事例
テキストをベクトル化さえできれば、

- 口コミのポジネガ判定
- アンケート回答のクラスタリング
- コンテンツに含まれるテキストの類似性に基づくレコメンド

など様々な活用方法が考えられます。

自分たちの研究や業務ではどのような場面に活かせそうかという事を考えてみて下さい。 
そして、是非手元で動かして、Word2Vecの威力を知って頂けると嬉しいです。

[^1]: https://cl.asahi.com/api_data/wordembedding.html