フレーズのベクトル表現

単語をベクトルに変換してますか。
word2vecいいですよね。私は今年から機械学習と自然言語処理の勉強を始めたのですが、ベクトル化を初めて勉強した時にはちょっと感動しました。
今回はその概要や使い方は紹介しませんが、deepblueブログでも紹介されていますのでよかったら読んでみてください。
Google Colaboratoryでword2vec①
Google Colaboratoryでword2vec②

さて、その分散表現ですが、基本的には分かち書きされた単語に対して一つのベクトルを当てはめます。いくつかの単語の集まり、つまりフレーズに対して分散表現を当てはめたい場合はどうしたらいいのでしょう。日本語では、形態素解析する時点で固有名詞などは意味のあるフレーズに区切られる場合も多いですが、半角スペースを使って英語などの言語を機械的に区切る場合では結構大事な問題になりそうです。
この解決方法は極めてシンプルで、コーパス全体で、注目している単語の連なりを1つのフレーズを置換すれば大丈夫です。
例えばこんな感じに区切られているテキストを


['Charles', 'Darwin', 'によって', '自然', '選択', 'は', '体系', '化', 'さ', 'れ', 'た']

こんな風に置換します。


['Charles_Darwin', 'によって', '自然_選択', 'は', '体系', '化', 'さ', 'れ', 'た']

置換したコーパスを使ってword2vecを実行すれば、'Charles_Darwin'や'自然_選択'のベクトル化ができます。もちろんその値を使って類義語を探したり、ベクトル同士の足し算引き算をすることもできます。

自動でフレーズに置き換える

特定のフレーズについてベクトル化したい場合はこれで十分ですが、コーパス全体を通して頻出する単語の組み合わせを自動的にフレーズにしたい場合もあると思います。そのためにはコーパス全体を一度n-gramに分割して、頻出する組み合わせはフレーズに置換すればよさそうです。
しかしここで一つ問題が発生します。例えば"私 の"という組み合わせは大抵のコーパスで頻出するでしょうが、そのフレーズに果たして意味はあるでしょうか。"私 の"と"私"という2つの単語が生まれてしまい、"私"の分散表現に与えられるデータが少なくなってしまいそうです。
そこで、フレーズに変換するかどうかはそれぞれの単語の出現確率や頻度も考慮してスコアを作り、そのスコアによってフレーズに変換するかどうかを調整することになります(参考)。例えば"私 の"の場合は、"の"が"私 の"以外の組み合わせでも頻出するため、フレーズには変換されにくくなります。

gensimでやってみよう

gensimパッケージにはこのようなフレーズへの変換を助けてくれるPhrasesというクラスがあります(参考)。やってみましょう。
環境:Google Colaboratory

日本語のWikipediaをMeCabで分かち書きしたものをコーパスとして利用することにします(ja.text8)。中はスペースで分かち書きされたテキストなので、文章単位でネストされた単語のリストにしておきます。


! pip install gensim
! mkdir data
! wget https://s3-ap-northeast-1.amazonaws.com/dev.tech-sketch.jp/chakki/public/ja.text8.zip -P data/
! unzip data/ja.text8.zip -d data/

from gensim.models import Phrases
from gensim.models.word2vec import Word2Vec
import pprint

def load_data(filepath, encoding='utf-8'):
    with open(filepath, encoding=encoding) as f:
        return f.read()

text = load_data(filepath='data/ja.text8')
text_wakati = list(map(lambda x: x.split(), text.split("。")))
text[0:100]
# ちょん 掛け ( ちょん がけ 、 丁 斧 掛け ・ 手斧 掛け と も 表記 ) と は 、
# 相撲 の 決まり 手 の ひとつ で ある 。 自分 の 右 ( 左 ) 足 の 踵 を 相手 の 右

text_wakati[0:1]
# [['ちょん',  '掛け',  '(',  'ちょん',  'がけ',  '、',  '丁',  '斧',  '掛け',  '・',
# '手斧',  '掛け',  'と',  'も',  '表記',  ')',  'と',  'は',  '、',  '相撲',  'の',
# '決まり',  '手',  'の',  'ひとつ',  'で',  'ある']]

今回は2つの単語が連なったフレーズを探すことにします。Phrasesクラスで単語をくっつけてから、"_"が含まれている単語を探して、どんなフレーズができたか見てみます。


phrases_bi = Phrases(text_wakati, threshold=10.0, scoring='default')
transformed_bi = phrases_bi[text_wakati]
phrase_list = [[word for word in sentence if ("_" in word)] for sentence in transformed_bi]

print(phrase_list[0:10])
# [['決まり_手'], [], ['て_いる', 'いわ_れる'], ['小内_刈', 'ほぼ_同じ'],
# ['1944_年', '双葉_山', 'ノ_里', 'て_いる'], ['2014_年', '11_月', '2012_年', 'て_いる', '十_両', '2014_年', '2013_年', 'て_いる'],
# ['幕下_以下', 'て_おり', 'て_いる'], ['甲_越', '1579_年', '戦国_大名', '武田_勝頼', '戦国_大名', '上杉_景勝'],
# ['天正_10', '(_1582', '軍事_同盟', '戦国_時代', '後_北条', '諸_勢力'], ['戦国_時代', '武田_信虎', 'さ_れる']]

あまりフレーズらしくないものもフレーズになっちゃってますね。これはPhrasesのパラメーターを変えることで調整できます。例えばscoringでスコアの作り方を、thresholdでフレーズ変換する閾値を調整できます。具体的なパラメーターの取り方は公式ドキュメントをどうぞ。ページの一番下の方に具体的な式も書いてありナイスですね。
今回はこのままword2vecの学習にいかせてください。モデルはSkip-gramを使ってます。
ベクトル化できたら類義語を調べてみます。


model = Word2Vec(transformed_bi, size=100, window=5, sg=1)
pprint.pprint(model.wv.most_similar(['自然_選択'], topn=10))
# [('種_分化', 0.9078763723373413),
#  ('形質', 0.8996312618255615),
#  ('形成_過程', 0.8977126479148865),
#  ('パンスペルミア', 0.8969030380249023),
#  ('類縁', 0.8940705060958862),
#  ('万有引力', 0.8903927803039551),
#  ('遺伝_的', 0.8881280422210693),
#  ('微視的', 0.8880571126937866),
#  ('断続_平衡', 0.8875775337219238),
#  ('関連付け_られる', 0.88715660572052)]

フレーズに置換したことできちんと生物進化に関連する単語が類義語に出てきてました。自然淘汰がトップに来て欲しかったところですが、そもそも自然淘汰がフレーズに変換されていなかったので、もっとそれっぽいコーパスを選ばないと駄目でしたかね。

参考文献

Word2Vec For Phrases — Learning Embeddings For More Than One Word
gensim の Phrases の使い方。頻出する単語ペアを検出-python
How to incorporate phrases into Word2Vec – a text mining approach


Header photo by Pisit Heng on Unsplash