ユーニックス総合研究所

  • home
  • archives
  • python-word2vec-man-shame

Word2Vecで「男」から「恥」を引くとどうなるか【Python, 自然言語処理】

Word2Vecで「男」から「恥」を引く

人間が話す言語は「自然言語」と呼ばれます。
これを解析するのが「自然言語処理」という計算機科学のジャンルです。

自然言語処理では単語をベクトルにして解析するというのがよく行われます。
ベクトルにすることで単語同士の足し算や引き算を行ったり、近いベクトルを探すことが出来るようになります。

そのベクトルを使った解析として最近話題なのが「Word2Vec(ワード・トゥー・ベクトル)」というPythonのライブラリです。
このライブラリを使うと比較的に簡単に単語のベクトルの演算を行うことが出来ます。

今回はWord2Vecを使って「男」という単語から「恥」という単語を引き算したらどうなるか? というのをやってみました。
具体的には↓を見ていきます。

  • Word2Vecの概要
  • Janomeの概要
  • 必要モジュールのインストール
  • スクリプトの作成
  • スクリプトの実行

Word2Vecの概要

Word2VecはGoogleのトマス・ミコロフ氏率いるチームによって作成されました。
特許取得済みのハイパー技術です。

Word2Vecはニューラルネットワークを使ってモデルを構築します。
入力データであるコーパス(単語の行列)を受け取って巨大なベクトル空間を構築します。

🦝 < 巨大なベクトル空間だって!

🐭 < かっちょいい!

コーパスのそれぞれの単語は空間内のベクトルに割り当てられます。
このベクトルを使うことで単語間の演算が可能になります。

つまりWord2Vecでは↓のような前提が必要になるわけです。

  • Word2Vecの準備
  • 入力データ(コーパス)の準備
  • Word2Vecにコーパスを流し込みモデルを作成
  • モデルを使って単語ベクトルを演算

今回は入力データであるコーパスには日本語の文章を使います。
具体的には青空文庫で公開されている夏目漱石の「こころ」の文章を使います。

この文章を「Janome」という形態素解析器で解析し、単語の行列に変換します。

そしてその単語の行列をWord2Vecに渡して学習させ、モデルを出力させます。
学習には時間がかかるので、出力したモデルはファイルとして保存しておきます。
単語の演算にはこのモデルを使います。

今回作成するスクリプトは2つのコマンドを持っています。
モデルの作成を行うコマンドと演算を行うコマンドを用意しておきます。
こうすることスクリプトの利用が簡単になります。

Janomeの概要

形態素解析器としては「MeCab」が有名ですが、JanomeはそのMeCabの辞書を使った解析器です。

Janomeはpipなどのパッケージマネージャーで簡単に環境にインストールすることが出来ます。

今回は日本語の文章を単語のリストに分割したいわけなんですが、これにJanomeを使います。
英語などの文章は単語がスペースで区切られているので単語のリストへの変換が簡単です。
しかし日本語の場合はそうもいかないので、Janomeのような形態素解析器が必要になるわけです。

形態素解析は自然言語処理におけるもっとも基礎的な技術の1つと言っていいかもしれません。

必要モジュールのインストール

Word2Vecはgensimというパッケージに入っています。
そのためpipではgensimをインストールします。
JanomeはJanomeです。

↓のようにインストールします。

> pip install gensim Janome  

スクリプトの作成

スクリプトは↓になります。
ライセンスはMITです。

from janome.analyzer import Analyzer  
from janome.tokenfilter import POSKeepFilter  
from gensim.models import word2vec  
import sys  


def save_model():  
    with open('kokoro.txt', 'rt', encoding='utf-8') as fin:  
        content = fin.read()  

    token_filters = [POSKeepFilter(['名詞', '代名詞'])]  
    a = Analyzer(token_filters=token_filters)  

    sentences = [[tok.surface for tok in a.analyze(content)]]  
    sentences[0].extend(['恥', '恥', '恥', '恥', '恥'])  

    model = word2vec.Word2Vec(sentences, size=1000, min_count=5, window=5, iter=3)  
    model.save('kokoro.model')  
    print('saved at "kokoro.model"')  


def show_most_similar():  
    model = word2vec.Word2Vec.load('kokoro.model')  
    results = model.wv.most_similar(negative=['男', '恥'])  
    for result in results:  
        print(result)  


def usage():  
    print('''Sample of Word2Vec  

Usage:  

    script.py [command]  

The commands are:  

    save-model  
    most-similar''')  


def main():  
    if len(sys.argv) < 2:  
        return usage()  

    cmd = sys.argv[1]  
    if cmd == 'save-model':  
        save_model()  
    elif cmd == 'most-similar':  
        show_most_similar()  


main()  

スクリプトの解説は↓からどうぞ。

必要モジュールのインポート

スクリプトの最初の方で必要モジュールをインポートしておきます。

from janome.analyzer import Analyzer  
from janome.tokenfilter import POSKeepFilter  
from gensim.models import word2vec  
import sys  

AnalyzerというのはJanomeのモジュールで、形態素解析を行うパーサーです。
それからPOSKeepFilterというのはAnalyzerに設定できるフィルターです。これはたとえば名詞のみを抽出したい場合などにこのフィルターに名詞を設定してAnalyzerに渡します。

word2vecはWord2Vecのモジュールです。これはword2vec.Word2Vec()のようにWord2Vecクラスを使う時などに使います。

それから今回のスクリプトはコマンドラインから実行するときにコマンドを指定できるようにします。
そのためコマンドライン引数の解析のためにsysモジュールをインポートしておきます。

main関数

スクリプトはmain()関数からはじまります。

def main():  
    if len(sys.argv) < 2:  
        return usage()  

    cmd = sys.argv[1]  
    if cmd == 'save-model':  
        save_model()  
    elif cmd == 'most-similar':  
        show_most_similar()  

main()関数内ではコマンドライン引数(sys.argv)の長さをチェックし、2より下、つまりコマンドが設定されていない場合はusage()関数を呼び出してスクリプトの使い方を表示します。
引数が2以上の場合はコマンド名を取得してコマンド名がsave-modelだったらsave_model()関数を、コマンド名がmost-similarだったらshow_most_similar()関数を呼び出します。

usage関数

usage関数はスクリプトの使い方を表示します。

def usage():  
    print('''Sample of Word2Vec  

Usage:  

    script.py [command]  

The commands are:  

    save-model  
    most-similar''')  

save_model関数

save_model()関数はWord2Vecを使ってモデルを作成します。
モデルの元になる入力データであるコーパスは「こころ」の文章です。
このコーパスはkokoro.txtにあらかじめ保存されています。

def save_model():  
    with open('kokoro.txt', 'rt', encoding='utf-8') as fin:  
        content = fin.read()  

    token_filters = [POSKeepFilter(['名詞', '代名詞'])]  
    a = Analyzer(token_filters=token_filters)  

    sentences = [[tok.surface for tok in a.analyze(content)]]  
    sentences[0].extend(['恥', '恥', '恥', '恥', '恥'])  

    model = word2vec.Word2Vec(sentences, size=1000, min_count=5, window=5, iter=3)  
    model.save('kokoro.model')  
    print('saved at "kokoro.model"')  

open()関数でkokoro.txtを読み込み内容をcontentに保存します。

次にtoken_filtersというリストを作ります。これはPOSKeepFilter()のオブジェクトが入ったリストです。
POSKeepFilterにはリストで「名詞」と「代名詞」の文字列を指定しておきます。
こうすることでAnalyzerが名詞と代名詞のみの単語を抽出するようになります。

Analyzertoken_filtersを渡してオブジェクト(a)にします。
そしてAnalyzeranalyze()メソッドを使ってcontentを解析します。
analyze()メソッドは結果をトークンのリスト(正確にはジェネレーター)で返してくるので、下のようにリスト内包表記を使ってトークンのsurface属性をリストに保存します。
トークンのsurface属性は表層形とよばれるもので、これは元の文章のそのままの表記の文字列のことです。

    sentences = [[tok.surface for tok in a.analyze(content)]]  

↑のコードで注意したいのが、sentences変数は行列になっているということです。つまり2重のリストになっています。
これを1重にするとバグになるので注意してください。

sentencesを生成したらこれをword2vec.Word2Vec()クラスの引数に渡します。

    model = word2vec.Word2Vec(sentences, size=1000, min_count=5, window=5, iter=3)  
    model.save('kokoro.model')  
    print('saved at "kokoro.model"')  

Word2Vecの引数は↓のような意味になります。

  • sentencesには、学習させたいテキストデータを単語で区切った行列を渡します
  • sizeは特徴量の数を指定します
  • min_countで指定した回数以下の単語は学習モデルから破棄させます
  • windowには文脈の最大単語数を指定します
  • iterにはトレーニングの反復回数を指定します

(参考: Pythonによるword2vecの利用方法を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン

Word2Vec()でモデルを作成したらこのモデルをmodel.save('kokoro.model')とやってディスクに保存します。
これを実行するとディレクトリにkokoro.modelファイルが作成されます。

show_most_similar関数

show_most_similar()関数では作成したモデルを使って単語ベクトルの引き算を行います。

def show_most_similar():  
    model = word2vec.Word2Vec.load('kokoro.model')  
    results = model.wv.most_similar(negative=['男', '恥'])  
    for result in results:  
        print(result)  

最初にmodel = word2vec.Word2Vec.load('kokoro.model')とやってディスクからモデルをロードします。
こうするとモデルを使えるようになります。
つぎにresults = model.wv.most_similar(negative=['男', '恥'])を呼び出して、単語「男」と「恥」を引き算します。
model.wv.most_similar()の引数negativeに単語のリストを渡すと、その単語同士を引き算することが出来ます。

結果はリストで返ってきます。
そのリストをfor文で回してprint()で出力します。

スクリプトの実行

それでは作成したスクリプトを実際に使ってみます。
コマンドラインから↓のようにsave-modelコマンドを実行すると、モデルが保存されます。

> python script.py save-model  
saved at "kokoro.model"  

モデルの作成には時間がかかります。2~3分かかる場合もあります。

モデルを作成したらmost-similarコマンドを実行します。
すると↓のような結果になります。

('強情', 0.9439822435379028)  
('不幸', 0.8974537253379822)  
('明治', 0.053686320781707764)  
('日蓮', 0.04534053057432175)  
('房州', -0.0017261841567233205)  
('鯛', -0.01083311066031456)  
('雑誌', -0.02869194559752941)  
('殉死', -0.046118177473545074)  
('良心', -0.05706881359219551)  
('旅', -0.9771273136138916)  

なんと! 「男」から「恥」を引いたら「強情」「不幸」がヒットしました。
恥じらいの無い男は強情で不幸になるってことですね……。

🦝 < 殉死って

🐭 < 不幸どころか死んでますネ

ちなみに「女」から「恥」を引いたら↓のような結果になります。

('強情', 0.9439723491668701)  
('不幸', 0.8975142240524292)  
('明治', 0.05363617464900017)  
('日蓮', 0.04538388177752495)  
('房州', -0.0019384267507120967)  
('鯛', -0.010734599083662033)  
('雑誌', -0.0286957286298275)  
('殉死', -0.046195078641176224)  
('良心', -0.056991834193468094)  
('旅', -0.9771290421485901)  

「女」の場合も同様に「強情」と「不幸」が高い一致率を示しています。
女から恥じらいが無くなったら強情で不幸になるってことですね。

🦝 < 鯛にもなってる

🐭 < 鯛って

おわりに

今回はWord2Vecを使って「男」から「恥」を引いてみました。
このようにWord2Vecを使うと簡単に単語ベクトルの演算が可能です。
スクリプトのライセンスはMITですので改造など自由に行ってください。

🦝 < 恥の多い人生を送ってきました

🐭 < 恥は男と女を神秘的にする