BERTで日本語の文章の穴埋めをやってみる
今回使うモデルは、東北大学が提供しているBERTを日本語に対応させたもの。早速やってみよう。
MeCabをインストールする
MeCabは日本語の文章の形態素解析(トークン化)を行うツールです。
WindowsとLinuxは本家のウェッブページにインストールの方法があります。
macOSであれば、brewを使ってインストールできます。brew自体がない方は、こちらからインストールしてください。
brew install mecab
Pythonの環境を作る
まず、Pythonの環境を作り、必要なライブラリをインストールする。
mkdir workspace #適当なディレクトリを作る
cd workspace
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install transformers fugashi ipadic torch
transformersはHugging Faceが提供しているライブラリです。BERTを含めTransformerのアーキテクチャを利用したさまざまな言語モデルが使えるようになる。東北大学の日本語BERTモデルも使える。
fugashiは日本語の文章を単語などに分解するためのライブラリ(リンク)。一般にTokenizer(トークナイザー)と呼ばれるもので、MeCabをPythonでラップしたもの。また、ipadicはMeCabで使われる辞書になる(リンク)。
transformersはPyTorchとTensorFlowをサポートしているが、ここではPyTorchを使う。よってtorchをインストールした。
日本語のTokenizerを使ってみる
Pythonを起動して、東北大学の日本語BERTで使うTokenizerを読み込みむ。
from transformers import BertJapaneseTokenizer
MODEL_NAME = 'cl-tohoku/bert-base-japanese-whole-word-masking'
tokenizer = BertJapaneseTokenizer.from_pretrained(MODEL_NAME)
一部をマスクした日本語文章を用意して、トークナイズしてみる。
text = 'あれは、[MASK]へ行くのか?'
tokens = tokenizer.tokenize(text)
print(tokens)
Tokenizerで分割された文章はこうなる。分割された一つ一つをToken(トークン)と呼ぶ。
['あれ', 'は', '、', '[MASK]', 'へ', '行く', 'の', 'か', '?']
[MASK]となっている部分をモデルで予測したい。そのため、トークンをテキストから数値に変換する必要がある。
import torch
input_ids = tokenizer.encode(text, return_tensors='pt')
print(input_ids)
数値化されたトークンはこうなる。PyTorchのテンソルとして、バッチサイズが1の形式で出力される。
tensor([[ 2, 2787, 9, 6, 4, 118, 3488, 5, 29, 2935, 3]])
気づいた人もいるかもしれないが、トークンの数が9から11に増えている。
今度はトークンをテキストに戻してみる。その際に、バッチの1行目を取り出していることに注意。さもないとエラーになる。
tokens = tokenizer.convert_ids_to_tokens(input_ids[0])
print(tokens)
数値のトークンをテキストに変換した結果は以下となる。
['[CLS]', 'あれ', 'は', '、', '[MASK]', 'へ', '行く', 'の', 'か', '?', '[SEP]']
[CLS]と[SEP]が追加されている。これは特別なトークンでtokenizer.encodeを実行したときに付け加えられる。CLSはclassification tokenと呼ばれ文章の始まりに使われる。センチメント分析などの分類を行う際に、CLSの位置における最終出力を文全体の代表として扱う。SEPは複数の文を使うタスク(質問文と正解文など)で文を区別するために使われるが、ここでは文の最後についてくるだけで特に意味はない。
ちなみに、上述の数値と比べると[MASK]は数値だと4であるのがわかる。
日本語BERTを使ってみる
まず、東北大学の日本語BERTを読み込む。使うのはMasked Language Modeling(MLM:一部をマスクした文章のための言語モデル)用の事前学習済みモデルになる。
from transformers import BertForMaskedLM
model = BertForMaskedLM.from_pretrained(MODEL_NAME)
数値トークンをモデルに与えて得られるスコア(logits)を見てみる。
with torch.no_grad():
output = model(input_ids = input_ids)
scores = output.logits
print(scores)
数値の羅列が登場する。
tensor([[[ -5.3907, 5.7029, -2.3064, ..., -5.0756, -5.8780, -6.7441],
[ -3.1246, 7.7760, -3.6269, ..., -5.6002, -5.5539, -5.2073],
[ -4.6224, 6.1970, -3.3037, ..., -4.0441, -5.9045, -6.8667],
...,
[-12.5016, 7.3512, -5.5179, ..., -8.6903, -8.3691, -8.0341],
[ -8.1357, 5.8702, -2.2169, ..., -8.0937, -5.8890, -9.7132],
[ -9.4262, 4.5267, -1.8536, ..., -4.6275, -8.4868, -11.3711]]])
このShapeを見てみる。
print(scores.shape)
結果はこうなる。
torch.Size([1, 11, 32000])
これは、バッチサイズが1で、トークンの数値列に11の数値があるという意味。最後の32000はBERTが予測するトークンの種類が32000個あるということ。たとえば、2787は「あれ」だった。
32000個のトークンから最初の20個をみてみる。
tokens = tokenizer.convert_ids_to_tokens(list(range(20)))
print(tokens)
結果はこうなる。
['[PAD]', '[UNK]', '[CLS]', '[SEP]', '[MASK]', 'の', '、', 'に', '。', 'は', 'た', 'を', 'で', 'と', 'が', 'し', 'て', '1', 'な', '年']
[UNK]は不明(Unknown)の意味。[PAD]は使用されていない部分を埋める(Padding)するためのトークン。バッチサイズが二つ以上だと文章の長さ(正確にはトークン化された数列の長さ)がまちまちなので短い文章に対して使用されてない部分に[PAD]を指定して長さを調節する際に使う。
BERTはVocabulary(ボキャブラリー)にある32000個の異なるトークンから、どのトークンが最もらしいのかのスコアを計算している。上記のscoresをSoftmaxに入力すれば確率になる。
[MASK]は数値だと4だった。4がある位置におけるスコアで最大になるインデックスを取り出せば、マスクされた部分に対して最も可能性の高いトークンを選ぶことになる。
# [MASK]の位置
mask_position = input_ids[0].tolist().index(4)
# [MASK]の位置でスコアが最大になるインデックス(トークンの数値)
index = scores[0, mask_position].argmax(-1).item()
# [MASK]の位置で最も可能性が高いトークン
token = tokenizer.convert_ids_to_tokens(index)
print(token)
結果はこうなる。
どこ
つまり、「あれは、[MASK]へ行くのか?」に対して「あれは、どこへ行くのか?」が最も確率が高いとBERTは予測した。
これは納得。それでは可能性の高い順にトップ10を取り出してみる。
# PyTorchのtopkで数値の高い順から10個のインデックスを取り出す。
top10 = scores[0, mask_position].topk(10)
# 10個のインデックスをテキストのトークンに変換する。
tokens = tokenizer.convert_ids_to_tokens(top10.indices)
print(tokens)
結果はこうなる。
['どこ', 'そこ', 'ここ', '何', 'どちら', '天国', '南', '地獄', '東京', '学校']
「天国」と「地獄」の順位が高いのが興味深い。
今日はここまで。
まとめのコード
import torch
from transformers import BertJapaneseTokenizer, BertForMaskedLM
MODEL_NAME = 'cl-tohoku/bert-base-japanese-whole-word-masking'
# Tokenizer
tokenizer = BertJapaneseTokenizer.from_pretrained(MODEL_NAME)
text = 'あれは、[MASK]へ行くのか?'
tokens = tokenizer.tokenize(text)
print(tokens)
input_ids = tokenizer.encode(text, return_tensors='pt')
print(input_ids)
tokens = tokenizer.convert_ids_to_tokens(input_ids[0])
print(tokens)
# Model
model = BertForMaskedLM.from_pretrained(MODEL_NAME)
with torch.no_grad():
output = model(input_ids = input_ids)
scores = output.logits
print('scores')
print(scores)
print('scores.shape')
print(scores.shape)
# 最初の20トークン
tokens = tokenizer.convert_ids_to_tokens(list(range(20)))
print(tokens)
# [MASK]の位置
mask_position = input_ids[0].tolist().index(4)
# [MASK]の位置でスコアが最大になるインデックス(トークンの数値)
index = scores[0, mask_position].argmax(-1).item()
# [MASK]の位置で最も可能性が高いトークン
token = tokenizer.convert_ids_to_tokens(index)
print(token)
# PyTorchのtopkで数値の高い順から10個のインデックスを取り出す。
top10 = scores[0, mask_position].topk(10)
# 10個のインデックスをテキストのトークンに変換する。
tokens = tokenizer.convert_ids_to_tokens(top10.indices)
print(tokens)
この記事が気に入ったらチップで応援してみませんか?