gensimによるword2vecの利用例

# はじめに
最近、単語の分散表現を学び、使ったので、その際に得た知識をまとめておく。
この記事では、MeCab、gensimを用いて、夏目漱石の『こころ』に登場する単語の類似度を計算する。
GitHubにこの記事で使用する全てのコードをアップしてある。
https://github.com/hsoccer/my_word2vec
# 環境
OS X El Capitan
Python3系
# MeCabのインストール
以下のページ等を参考にインストールする。
追加の辞書であるmecab-ipadic-neologdもインストールしておく。
https://qiita.com/taroc/items/b9afd914432da08dafc8
# MeCabの辞書を強化する
デフォルトの辞書では弱いので、Wikipediaの見出語を全て辞書に加えておく。
user.dicという名前で保存しておく。
以下のページが非常に参考になる。
http://aidiary.hatenablog.com/entry/20101230/1293691668
# gensimのインストール
ターミナル上で`$ pip install gensim`でうまくいくはず。
# データのダウンロード
以下の青空文庫のサイトから『こころ』のzipファイルをダウンロードして、これ以降で書いていくコードと同じファイルに入れておく。
http://www.aozora.gr.jp/cards/000148/card773.html
zipファイルを解凍すると、全文のテキストファイルになる(kokoro.txtで保存)が、本文の前後に不要な部分があるので、削除しておく。(しなくてもそんなに変わらないかもしれない。)
# 本文を分かち書きする
分かち書きとは、各文章を文節ごとに分けた形で書きなおすことである。
もしイメージが湧かなければ、調べるか、以下のコードを実行してみると良い。

```python3:Wakati.py
import MeCab

class Wakati(object):

    """
    ==========================================================
    ファイルに存在する文章を指定の辞書を用いてMeCabによって形態素に分ける
    ==========================================================
    【関数説明】
    __init__ : コンストラクタ
    wakati : 文章を分かち書きする
    output : 結果をファイルに出力する
    """

    def __init__(self, file_dir, dic_dir=None, user_dir=None, hinshis=["動詞", "形容詞", "形容動詞", "助動詞"]):
        """
        ==========================================================
        コンストラクタ
        ==========================================================
        【変数説明】
        file_dir : 入力となる文章のディレクトリ
        dic_dir : システム辞書のディレクトリ([ex] /usr/local/lib/mecab/dic/mecab-ipadic-neologd)
        user_dir : ユーザー辞書のディレクトリ([ex] /Users/PCのユーザ名/...)
        hinshis : 活用する語
        tagger : MeCab用のtagger(詳細はMeCab自体のドキュメント参照)
        f : 入力ファイル
        splited_text : 各行を分かち書きしたもの(splited_line)をリストで格納する(Noneで初期化)
        out_dir : 出力ファイルのディレクトリ(Noneで初期化)
        """
        if dic_dir is not None and user_dir is not None:
            self.tagger = MeCab.Tagger("mecabrc -d {} -u {}".format(dic_dir, user_dir))
        elif dic_dir is not None:
            self.tagger = MeCab.Tagger("mecabrc -d {}".format(dic_dir))
        else:
            self.tagger = MeCab.Tagger("mecabrc")
        self.f = open(file_dir, 'r')
        self.hinshis = hinshis
        self.splited_text = None
        self.out_dir = None

    def wakati(self):
        """
        ==========================================================
        文章全体を分かち書きし、self.splited_textに格納する
        その際、活用された語については原形に直す
        ==========================================================
        【変数説明】
        line : 入力文章の一行(更新されていく)
        splited_line : 各行の文章を分かち書きしたもののリスト
        node : 各単語のノード
        word : 単語
        feature : 単語の情報
        hinshi : 品詞
        kata : 活用形
        genkei : 原形
        """
        line = self.f.readline()
        splited_text = []
        while line:
            node = self.tagger.parseToNode(line).next
            splited_line = []
            while node.surface:
                word = node.surface
                feature = node.feature.split(',')
                hinshi = feature[0]
                kata = feature[5]
                genkei = feature[6]
                if hinshi in self.hinshis:
                    if kata != "基本形":
                        word = genkei
                splited_line.append(word)
                node = node.next
            splited_text.append(splited_line)
            line = self.f.readline()
        self.splited_text = splited_text
        self.f.close()

    def output(self, out_dir):
        """
        ==========================================================
        self.splited_textをファイルに出力する
        ==========================================================
        【変数説明】
        out_dir : 出力ファイルのディレクトリ
        fout : 出力ファイル
        """
        assert self.splited_text is not None
        if self.out_dir is None:
            self.out_dir = out_dir
        self.fout = open(self.out_dir, 'w')
        for line in self.splited_text:
            self.fout.write(" ".join(line) + " ")
        self.fout.close()
```

```python3:wakati_text.py
from Wakati import Wakati

w = Wakati("kokoro.txt", システム辞書のディレクトリ, ユーザ辞書のディレクトリ)
w.wakati()
w.output("out.txt")
```

上記のコードを実行すると、out.txtという名前で分かち書きされたテキストファイルができる。
これを見ればやっていることが分かると思う。

【追記】 (2017/9/30)
Wakatiクラスのdic_dirのデフォルトを設定した。(それに伴って、以下のVectorizerクラスの引数も変更した。)

# word2vecを使って単語をベクトル化する
先ほどインストールしたgensimというライブラリを用いて、『こころ』に登場する単語を分散表現する。

```python3:Vectorizer.py
from Wakati import Wakati
from gensim.models import word2vec
import logging

class Vectorizer(Wakati):

    """
    ==========================================================
    Wakatiをベースとして、分かち書きしたものを学習して分散表現する
    ==========================================================
    【関数説明】
    __init__ : コンストラクタ
    vectorize : 分散表現を作る
    _train : gensimを使ってword2vecする
    save_model : 作ったモデルを保存する
    """

    def __init__(self, file_dir, dic_dir=None, user_dir=None, out_dir="out.txt", hinshis=["動詞", "形容詞", "形容動詞", "助動詞"]):
        """
        ==========================================================
        コンストラクタ
        Wakatiを使って文章を分かち書きしておく
        ==========================================================
        【変数説明】
        file_dir : 入力となる文章のディレクトリ
        dic_dir : システム辞書のディレクトリ(/usr/local/lib/mecab/dic/mecab-ipadic-neologd)
        user_dir : ユーザー辞書のディレクトリ(/Users/ユーザ名/Desktop/word2vec/user.dic)
        out_dir : 分かち書きされた文章のファイルのディレクトリ
        hinshis : 活用する語
        model : モデル(Noneで初期化)
        """
        Wakati.__init__(self, file_dir, dic_dir, user_dir, hinshis)
        self.out_dir = out_dir
        self.model = None
        self.wakati()
        self.output(self.out_dir)

    def vectorize(self, other_file_dir=None, sg=1, size=300, min_count=10, window=5, hs=0, negative=15, iter=15):
        """
        ==========================================================
        単語の分散表現を作成
        ==========================================================
        【変数説明】
        out_dir : 分かち書きされた文章のファイル
        other_file_dir : out_dirを使わない場合のファイル名(Noneで初期化)
        sentences : 分かち書きされ、空白区切の文章全文
        """
        logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
        if other_file_dir is None:
            sentences = word2vec.Text8Corpus(self.out_dir)
        else:
            sentences = word2vec.Text8Corpus(other_file_dir)
        self._train(sentences, sg, size, min_count, window, hs, negative, iter)

    def _train(self, sentences, sg=1, size=300, min_count=10, window=5, hs=0, negative=15, iter=15):
        """
        ==========================================================
        gensimによる学習
        ==========================================================
        【変数説明】
        sentences : 分かち書きされ、空白区切の文章全文
        word2vecの引数 : 詳細はgensimのドキュメント参照
        """
        self.model = word2vec.Word2Vec(sentences, sg=sg, size=size, min_count=min_count, window=window, hs=hs, negative=negative, iter=iter)

    def save_model(self, model_dir):
        """
        ==========================================================
        モデルを保存する
        ==========================================================
        【変数説明】
        model_dir : モデルを保存するディレクトリ
        """
        assert self.model is not None
        self.model.save(model_dir)

```
# 『こころ』に登場する単語を解析
ここまでで作ったモデルを用いて、『こころ』における「人間」という単語との類似度が高い単語をピックアップする。

```python3:example.py
from Vectorizer import Vectorizer

"""
==========================================================
自作のWord2Vecを使って、夏目漱石の『こころ』を解析する。
(『こころ』は青空文庫(http://www.aozora.gr.jp/cards/000148/card773.html)から
ダウンロードして同じディレクトリに入れておく)
==========================================================
【関数説明】
kokoro : 『こころ』のモデルを作成
"""

def kokoro():
    """
    ==========================================================
    『こころ』から作成されたモデルを返す
    ==========================================================
    【変数説明】
    dic_dir : システム辞書のディレクトリ(このPCでは"/usr/local/lib/mecab/dic/mecab-ipadic-neologd")
    user_dir : ユーザー辞書のディレクトリ(このPCでは"/Users/ユーザ名/Desktop/word2vec/user.dic")
    """

    #Vectorizerインスタンスを作成
    #__init__で文章が分かち書きされたファイル(out.txt)が同じディレクトリ内に作られる。
    #dic_dir、user_dirを各自のPCに合わせて設定し、Vectorizerの引数にもたせても良い。
    v = Vectorizer(file_dir="kokoro.txt")

    #単語の分散表現のモデル作成
    v.vectorize()

    #できたモデルを返す
    return v.model

if __name__ == "__main__":

    #『こころ』から作成されたモデル
    model = kokoro()

    #「人間」という単語に最も近い１０単語を表示する
    #resultは(単語, コサイン距離)からなるリスト
    result = model.most_similar(positive="人間")
    for pair in result:
        word = pair[0]
        distance = pair[1]
        print(word, distance)

    #完成したモデルを保存したければ以下のコードのコメントアウトを外す
    #v.save_model("kokoro.model")

```

このコードを実行すると、(私の環境では)以下のような結果になった。

```text:result.txt
幸福 0.7769338488578796
矛盾 0.7726519107818604
愛 0.763999342918396
変化 0.7608857154846191
判断 0.7593793869018555
嫌い 0.7444355487823486
つまり 0.7347712516784668
従妹 0.7326171398162842
苦痛 0.718117892742157
感じ 0.7159429788589478
```

本文を読まなくても、なかなか重そうな内容であることが一目で分かる。

# 最後に
この記事では、単に単語の類似度を調べただけだが、実際には、ベクトル化した単語からなる文章を時系列データとしてLSTMに突っ込む等応用が効く。
最初にも書いたが、[https://github.com/hsoccer/my_word2vec](https://github.com/hsoccer/my_word2vec)にほぼ同じコードが載っているので、合わせてご覧いただければ幸いである。
コードのコメントとして、極力丁寧に変数の説明を入れたつもりなので、いろいろと変更して、結果の違いを観察すると楽しい。
# 参考
https://qiita.com/taroc/items/b9afd914432da08dafc8
http://aidiary.hatenablog.com/entry/20101230/1293691668
http://www.aozora.gr.jp/cards/000148/card773.html
https://radimrehurek.com/gensim/models/word2vec.html