Word2Vecで「男」から「恥」を引くとどうなるか【Python, 自然言語処理】
- 作成日: 2020-12-25
- 更新日: 2023-12-24
- カテゴリ: 自然言語処理
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
が名詞と代名詞のみの単語を抽出するようになります。
Analyzer
にtoken_filters
を渡してオブジェクト(a
)にします。
そしてAnalyzer
のanalyze()
メソッドを使って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ですので改造など自由に行ってください。
🦝 < 恥の多い人生を送ってきました
🐭 < 恥は男と女を神秘的にする