AI 物見遊山 - 自然言語処理編 - 単語ベクトル

自然言語処理編 - 単語のベクトル化

アイキャッチ画像

自然言語処理の単語のベクトル化について試してゆく。 「単語のベクトル化」と、「単語の分散表現」は、ほぼ同じ意味として用いられるようだが、 「単語の分散表現」は、「単語のベクトル化」の一種であるらしい。 ソースコードは、~/jupyter-work ディレクト上で実行する前提とする。

gensim パッケージを利用する。 gensim とは、文章の意味をベクトルとして扱うためのパッケージとなる。 教師なし機械学習アルゴリズムを採用しているため、 構造化していないそのままの文章を処理出来る。 文章の集まり(コーパス)を意味を表すベクトル(セマンティックベクトル)に変換し、 ある意味のベクトルを、別の意味のベクトルに変換する方法(モデル)を定義することができる。

この記事では、単語ベクトルを扱う Word2Vec モデルと、KeyedVectors を扱う。 KeyedVectors は、Word2Vec、Doc2Vec や、fastText の単語ベクトルを格納・操作する部分を共通化したものとなる。 モデルは、学習用の情報と、単語ベクトルの両方を持っているため、追加学習が可能であるが、 KeyedVectors は、単語ベクトルのみ保持しており、追加学習ができない代わりに、モデルよりデータサイズが小さい。 KeyedVectors のみでも、単語間の類似度判定などの機能は利用できる。

参考:models.keyedvectors – Store and query word vectors

ここでは、学習済みの日本語単語ベクトルセット(KeyedVectors)である (chiVe: Sudachi による日本語単語ベクトル) を利用して、単語ベクトルの使い方を見て行くことにする。

はじめに

gensim のログ出力設定を行う。 この記事中の処理では、あまり有用ではないが、 今後は、内部処理の把握や、エラー時の対応などで、必須と言っても過言ではない。

import logging

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

単語ベクトル

まず単語「犬」のベクトルを表示してみる。

import gensim

model = gensim.models.KeyedVectors.load("../models/chive-1.3-mc5_gensim/chive-1.3-mc5.kv")
vec = model['犬']
print(vec.length)
print(vec)

この chiVe のベクトルは、300 次元となっている。 ベクトル次元が多いことは、単語をより細かく表現できるということらしい。

size:  300
[-4.34520751e-01  9.06087607e-02 -7.00369626e-02 -1.12125240e-01
 -8.08637887e-02 -1.06122769e-01 -3.61174434e-01  2.76316881e-01
 ..... 中略 .....
 -1.53031647e-01  1.85751691e-01 -2.53787078e-02 -2.46930525e-01
 -4.60487492e-02 -3.08201551e-01 -1.48632944e-01  9.46458429e-02]

単語間の類似度

単語「犬」と「人」の類似度を算出してみる。

import gensim

model = gensim.models.KeyedVectors.load("../models/chive-1.3-mc5_gensim/chive-1.3-mc5.kv")
print(model.similarity('犬', '人'))

結果は次のようになる。

0.29249397

この値は、二つのベクトルのコサインを計算しているようだ。

\[ \cos \theta = \frac{v1 \cdot v2}{|v1||v2|} \]

Python で実装してみると

import gensim
import numpy as np

model = gensim.models.KeyedVectors.load("../models/chive-1.3-mc5_gensim/chive-1.3-mc5.kv")
v1 = model['犬']
v2 = model['人']
n1 = np.linalg.norm(v1, ord=2)
n2 = np.linalg.norm(v2, ord=2)
print(np.dot(v1, v2) / (n1 * n2))

前述の値と同じになる。

0.29249397

失業 に対する感情を判定してみる。

import gensim

emotions = ['嬉しい', '腹立たしい', '悲しい', '楽しい']
model = gensim.models.KeyedVectors.load("../models/chive-1.3-mc5_gensim/chive-1.3-mc5.kv")
for emotion in emotions:
    print(str(model.similarity('失業', emotion)) + ' :' + emotion)

結果を見ると、悲しいが高く、嬉しいが低いので、想像通りだと思う。

0.05855734 :嬉しい
0.19939116 :腹立たしい
0.21386954 :悲しい
0.06767238 :楽しい

昇進 に対する感情を判定してみる。

import gensim

emotions = ['嬉しい', '腹立たしい', '悲しい', '楽しい']
model = gensim.models.KeyedVectors.load("../models/chive-1.3-mc5_gensim/chive-1.3-mc5.kv")
for emotion in emotions:
    print(str(model.similarity('昇進', emotion)) + ' :' + emotion)

結果を見ると、楽しいが最も低く、腹立たしい最も高い。 なんとなく理解できる・・・・・複雑な感じ。

0.1669954 :嬉しい
0.17801343 :腹立たしい
0.15692115 :悲しい
0.11359178 :楽しい

類似の単語を取得

単語「徳島」の類似単語を取得してみる。

import gensim

model = gensim.models.KeyedVectors.load("../models/chive-1.3-mc5_gensim/chive-1.3-mc5.kv")
print(model.most_similar('徳島', topn=5))

類似度の高いものが、候補に上がっている。

[('徳島県', 0.7955368757247925), ('愛媛', 0.7929633855819702), ('高知', 0.7896395325660706), ('徳島市', 0.7692087888717651), ('香川', 0.7571912407875061)]

アナロジー推論

Gemini 先生によれば、 「アナロジー推論とは、既知の事柄(ベース)と未知の事柄(ターゲット)の間に類似性を見出し、 その類似性に基づいて未知の事柄について推論すること」らしい。 自然言語処理の単語ベクトルでは、「単語間の関係をベクトル演算で表現し、類推問題を解く」ことらしい。

式で表すと以下のようになる。 日本と東京の関係が、アメリカとニューヨークの関係と同様であることを示しているらしい。

\[ v_{日本} - v_{東京} + v_{ニューヨーク} \simeq v_{アメリカ} \]

アメリカ日本ニューヨーク東京

では、実際に動作させてみる。

import gensim

model = gensim.models.KeyedVectors.load("../models/chive-1.3-mc5_gensim/chive-1.3-mc5.kv")
print(model.most_similar(
    positive=['日本', 'ニューヨーク'],
    negative=['東京'],
    topn=3))

次のようにアメリカが最も近い結果となった。

[('アメリカ', 0.7020306587219238), ('米国', 0.6050792932510376), ('欧米', 0.5738331079483032)]

Word Mover Distance による文章間距離

Word Mover Distance は、2 つの文章間の距離(類似度では無い)を計算する。 距離が、0 であれば、同一の文章ということ。

ソースの全文を載せる。

import logging

import spacy
from gensim.models.keyedvectors import KeyedVectors

# ログ出力設定
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)


def tokenize(doc):
    """spaCy の戻り値を分かち書き配列に変換する"""
    return [token.orth_ for token in doc]


# 分かち書きに利用するモデルを読み込む。
nlp = spacy.load('ja_ginza_bert_large')
# 単語ベクトルセットを読み込む。
model = KeyedVectors.load("../models/chive-1.3-mc5_gensim/chive-1.3-mc5.kv")

tokens1 = tokenize(nlp('私は、猫である。'))
tokens2 = tokenize(nlp('私は、犬である。'))
print(model.wmdistance(tokens1, tokens1))
print(model.wmdistance(tokens1, tokens2))

以下のような結果となる。 一つ目は、同じ文章なので、距離は 0 となる。

0.0
0.10580116662522744