京都大学が公開している日本語のWikipediaから学習したBERTのモデルを使って、単語の埋め込みを試した。
Googleが公開しているBERTのextract_features.pyを使って、Juman++v2を使って文を分かち書きして入力すると、文中の単語の埋め込みベクトルが得られる。
以下に、手順と実行結果を示す。
Juman++v2のインストール
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値が最も高くなるようだが、とりあえず最終層のみとした。
出力結果は、引数の--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の単語埋め込みベクトルは、単語の演算には適さなさそうだ。