TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

BERTで日本語の単語埋め込みを試す

京都大学が公開している日本語のWikipediaから学習したBERTのモデルを使って、単語の埋め込みを試した。

Googleが公開しているBERTのextract_features.pyを使って、Juman++v2を使って文を分かち書きして入力すると、文中の単語の埋め込みベクトルが得られる。

以下に、手順と実行結果を示す。

Juman++v2のインストール

先日の記事を参照
Juman++v2をWindowsでビルドする - TadaoYamaokaの開発日記

BERTのソースclone

BERTのソースをgit cloneする。

git clone https://github.com/google-research/bert.git

事前学習済みモデルダウンロード

ku_bert_japanese - KUROHASHI-CHU-MURAWAKI LAB
Japanese_L-12_H-768_A-12_E-30_BPE.zipをダウンロードして、適当な場所に展開する。
以下では、BERTのソースディレクトリのmodelディレクトリに展開したとする。

入力文の作成

入力する文をテキストファイルに記述して、UTF-8で保存する。

sample.txt
機動戦士ガンダムは、サンライズ制作のロボットアニメです。
ザクは、ガンダムシリーズに登場するモビルスーツです。
赤い彗星はシャア専用ザクです。
彗星は、氷や塵などでできています。
赤いイチゴは甘い。

分かち書き

入力文をJuman++v2で分かち書きする。

jumanpp_v2 --config=H:\src\jumanpp-2.0.0-rc2\model\jumandic.conf --segment r:\sample.txt -o r:\sample_s.txt

以下のように分かち書きされる。

機動 戦士 ガンダム は 、 サンライズ 制作 の ロボット アニメ です 。
ザク は 、 ガンダム シリーズ に 登場 する モビルスーツ です 。
赤い 彗星 は シャア 専用 ザク です 。
彗星 は 、 氷 や 塵 など で できて い ます 。
赤い イチゴ は 甘い 。

BERTのソース修正

BERTのソースは、そのままでは日本語の単語が1文字になってしまうため、ここの説明の通りtokenization.pyを修正する。

# text = self._tokenize_chinese_chars(text)

文と単語の埋め込みベクトルを得る

BERTのextract_features.pyを使用して、分かち書きした入力文から、文と単語の埋め込みベクトルを得る。

python extract_features.py --input_file=r:\sampel_s.txt --output_file=r:\output.json --vocab_file=model\vocab.txt --bert_config_file=model\bert_config.json --init_checkpoint=model\bert_model.ckpt --do_lower_case=False --layers=-1

引数の--layers=-1で、最終の中間層のみ出力するようにしている。
この記事によると、最後から4層までのベクトルを連結した場合が、F値が最も高くなるようだが、とりあえず最終層のみとした。
f:id:TadaoYamaoka:20190728162020p:plain

出力結果は、引数の--output_fileで指定したパスにjson形式で保存される。

output.json
{"linex_index": 0, "features": [{"token": "[CLS]", "layers": [{"index": -1, "values": [-0.593731, 0.536862, 1.262189, ...
{"linex_index": 1, "features": [{"token": "[CLS]", "layers": [{"index": -1, "values": [-0.33602, -0.053047, 0.694587, ...
{"linex_index": 2, "features": [{"token": "[CLS]", "layers": [{"index": -1, "values": [1.365805, 0.30302, 0.562358, ...
{"linex_index": 3, "features": [{"token": "[CLS]", "layers": [{"index": -1, "values": [1.169213, -0.522374, -1.460554, ...
{"linex_index": 4, "features": [{"token": "[CLS]", "layers": [{"index": -1, "values": [0.952905, 1.600134, -1.115922, ...

単語の埋め込みベクトルを取り出す

出力されたjsonファイルを読み込む。

with open(r'r:\output.json', 'r') as f:
    lines = f.readlines()

objs = []
for l in lines:
    objs.append(json.loads(l))

変数objsに各行のjsonデータが格納される。

jsonデータから文中の単語と埋め込みベクトルを抽出する。

単語の確認

単語は、jsonデータのfeaturesの単語に対応する位置の['token']に格納されている。
単語の位置の0番目は必ず[CLS]になるため、1番から始まる。

objs[0]['features'][3]['token']
Out: 'ガンダム'
すべての文の各単語埋め込みベクトルを取得

埋め込みベクトルは、featuresの単語に対応する位置の['layers'][0]['values']に格納されている。

import numpy as np

words = []
for o in objs:
    dic = {}
    for feature in o['features']:
        token = feature['token']
        dic[token] = np.array(feature['layers'][0]['values'])
    words.append(dic)

変数wordsに各行の各単語の埋め込みベクトルが格納される。

埋め込みベクトルの表示
words[0]['ガンダム']
Out: array([-9.618200e-01,  3.335200e-02,  1.011132e+00, -6.614870e-01, ...

単語のコサイン類似度の比較

BERTで取得した単語の埋め込みベクトルを使用して、単語同士の類似度を計算する。

コサイン類似度の定義

コサイン類似度を計算する関数を定義する。

def cos_sim(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))


以下に、いくつかの単語同士のコサイン類似度を計算した例を示す。

例1
cos_sim(words[0]['ガンダム'], words[1]['モビルスーツ'])
cos_sim(words[1]['ザク'], words[1]['モビルスーツ'])
cos_sim(words[0]['ロボット'], words[1]['モビルスーツ'])
cos_sim(words[4]['イチゴ'], words[1]['モビルスーツ'])
Out: 0.49544412522207004
Out: 0.5251044756373676
Out: 0.46859541657644055
Out: 0.15821825000954565

関連のある単語の類似度が高くなっている。

例2

BERTは文脈に依存した単語の意味を表現することができるので、同じ単語でも異なる文では埋め込みベクトルも異なる。

cos_sim(words[2]['彗星'], words[2]['シャア'])
cos_sim(words[3]['彗星'], words[2]['シャア'])
Out: 0.45199096516452764
Out: 0.2708140096663009

(0番から数えて)2番目の文の「彗星」の方が期待通り「シャア」と関連が高くなっている。

例3

単語の埋め込みベクトルの演算を試してみる。

cos_sim(words[1]['ザク'] + words[4]['赤い'], words[2]['シャア'])
cos_sim(words[1]['ザク'] + words[4]['甘い'], words[2]['シャア'])
Out: 0.417671520432263
Out: 0.4171861580830552

あまり期待した結果にならなかった。
単語が文脈に依存するので「甘い」が「赤い」と同じような意味になってしまったのかもしれない。
BERTの単語埋め込みベクトルは、単語の演算には適さなさそうだ。