Smile Engineering Blog

ジェイエスピーからTipsや技術特集、プロジェクト物語を発信します

PyTorchで京大BERT日本語Pretrainedモデルを使ってみよう

自然言語処理で注目を集めるBERT

Googleによって提案されたBERTは、自然言語処理のあらゆる分野へ流用が可能で、ますます注目を集めています。自然言語処理を学んでる方でしたら、一度は触ってみたいですよね!
今日は京大から公開されている、 PyTorch & BERT日本語Pretrainedモデル を使って、単語特徴ベクトルを取り出す方法を紹介します。

nlp.ist.i.kyoto-u.ac.jp

PyTorchでBERTを扱うには?

BERTは元々はGoogleによって提案されたもの。すなわち、元々のモデルはTensorFlowで実装されています。

github.com

そしたらPyTorchでBERTは使えない!?? などということはなく、 Hugging FaceのTransformersパッケージを使用することで、PyTorchでもBERTのモデルが扱えるようになっています。

huggingface.co

今回は、Hugging FaceのTransformersを使用して、京大のBERT日本語Pretrainedモデルを呼び出して使ってみます。

特徴ベクトルの取得方法

それでは、BERTを使用して、特徴ベクトルを取得してみましょう。
入力となるテキストは『大きなのっぽの古時計を購入した。』とします。

1. まずはJUMANを使用して単語を分割します

まずは入力テキストをJUMANを使用して、単語単位に分割します。

from pyknp import Juman

text = "大きなのっぽの古時計を購入した。"

juman = Juman()
result = juman.analysis(text)
tokens = [mrph.midasi for mrph in result.mrph_list()]

print("JUMAN tokens: ", tokens)

出力:

JUMAN tokens:  ['大きな', 'のっぽの', '古', '時計', 'を', '購入', 'した', '。']

ここまではあくまで形態素解析器 JUMAN を使用して、単語を分割したのみです。まだBERTは出てきていませんね!
最近は深層学習を使用した JUMAN++ の精度がかなり上がっている印象です。JUMAN / JUMAN++ はKNPと併用することで、係り受け解析等、意義の解析が得意な形態素解析機となっています。
形態素解析器といえばMecabしか触ったことがないという方は、一度JUMAN++も触ってみると面白いかもしれませんね。

nlp.ist.i.kyoto-u.ac.jp

2. BERT用にトークナイズ

続いてBERT用に単語分割を行います。
何がBERT用なのか、まずはその出力結果を確認してみましょう。

from transformers import BertTokenizer, BertModel

bert_model_path = "./Japanese_L-12_H-768_A-12_E-30_BPE_WWM"

# for Tokenizer
vocab_file_path = bert_model_path + "/vocab.txt"
bert_tokenizer = BertTokenizer(vocab_file_path, do_lower_case=False, do_basic_tokenize=False)

bert_tokens = bert_tokenizer.tokenize(" ".join(["[CLS]"] + tokens + ["[SEP]"]))
print("BERT tokens: ", bert_tokens)

token_ids = bert_tokenizer.convert_tokens_to_ids(bert_tokens)
print("BERT token IDs: ", token_ids)

出力:

BERT tokens:  ['[CLS]', '大きな', 'の', '##っぽ', '##の', '古', '時計', 'を', '購入', 'した', '。', '[SEP]']

BERT token IDs:  [2, 522, 5, 28052, 422, 1179, 5424, 10, 1468, 20, 7, 3]

BERT用にトークナイズを行う際、文頭に '[CLS]' 、文末に '[SEP]' を挿入しています。こうすることでBERTの精度が上がるとされています。

もう一つ見過ごせないのが、下記の点ではないでしょうか。

  • 「のっぽ」→「の」+「##っぽ」+「##の」

これは、単語をsubword化しています。学習時に単語の共通部分を見つけて、それをうまいこと学習していこうというのがこの技術です。英語では going を go + ing に分けるような、そんな具合です。

最後にBERT用にトークナイズしたものを、IDへ変換しています。元のJUMANの単語分割数が8個であったのに対し、BERT用にsubword化すると、10 + 2 (CLS + SEP) 個になるということですね。
このID(token_ids)がBERTのモデルの入力となります。

3. BERTのモデルで特徴ベクトルを抽出

それでは取得したIDを使用して、特徴ベクトルを抽出してみます。

import torch

# BERT pre_trained model load
bert_model = BertModel.from_pretrained(bert_model_path)

tokens_tensor = torch.tensor(token_ids).unsqueeze(0)
outputs, _ = bert_model(tokens_tensor)
print(outputs[0], "\n (size: ", outputs[0].size(), ")")

出力:

tensor([[ 0.6360,  0.0840,  0.3619,  ..., -0.8531,  0.1429,  0.3017],
        [-0.1458,  0.1711,  1.3058,  ..., -0.7310,  0.0707, -0.2259],
        [-0.5004,  0.5406,  0.3450,  ..., -0.0714, -0.7272, -0.5982],
        ...,
        [-0.2729, -0.3282, -0.0918,  ..., -0.6031,  0.3227, -0.4769],
        [ 0.8811,  0.3096,  0.4068,  ..., -0.2815,  0.6047,  0.0074],
        [ 0.9640,  0.3325,  0.4466,  ..., -0.2990,  0.6311,  0.0432]],
       grad_fn=<SelectBackward>) 
 (size:  torch.Size([12, 768]) )

出力結果のベクトルサイズを見ると、12×768 のベクトルを取得できていることがわかります。
つまり先程BERT用に分割した12個 × 768のテンソルが特徴ベクトルとして抽出されました。
京大のBERT日本語Pretrainedモデルは、ひとつの単語(をsubword化したもの)に対して、768個のfloat型ベクトルを取得することができます。

ところでこれをどうするのか?

BERT日本語Pretrainedモデル……ということは、つまりこれは深層学習の前処理(正確には転移学習)の特徴量として使用することができます。
この後続として、別のモデルと組み合わせて使用することで、自然言語処理のあらゆる分野で活用することができるんですね!

実際使用してみようとすると・・・

正直言うと、ここまでの処理、めっちゃ遅いです! 使い方を間違えると、いつになっても学習が進まないとかいろいろ弊害が出てきます。
ちなみにどの部分が遅いのかと言うと、BERT日本語Pretrainedモデルを使用して特徴量を抽出する箇所ではなく、BERT用のトークナイズ処理部でかなりの時間を要していました。軽くソースコードを眺めてみたのですが、Pythonで長いfor文をぐるぐる回っているらしく・・・そりゃ遅いですね。。。

解決策としては、SentencePieceを使用してみるとか? そうすると京大のBERT日本語Pretrainedモデルが使用できなくなってしまい、いろいろ悩ましいですね。

あとは、、、これ、768個も本当にいる!???
アノテーション等をするとかでなければもう少し少なくてもいい気がする……)

今回のソースコード(全文)

最後に、今回書いたソースコードをそのまま貼り付けておきます。

import torch
from pyknp import Juman
from transformers import BertTokenizer, BertModel

text = "大きなのっぽの古時計を購入した。"
bert_model_path = "./Japanese_L-12_H-768_A-12_E-30_BPE_WWM"

# BERT pre_trained model load
bert_model = BertModel.from_pretrained(bert_model_path)

# for Tokenizer
vocab_file_path = bert_model_path + "/vocab.txt"
bert_tokenizer = BertTokenizer(vocab_file_path, do_lower_case=False, do_basic_tokenize=False)

"""
トークナイザー
"""
print("\n *** to Tokens ***")
juman = Juman()
result = juman.analysis(text)
tokens = [mrph.midasi for mrph in result.mrph_list()]
print("JUMAN tokens: ", tokens)

bert_tokens = bert_tokenizer.tokenize(" ".join(["[CLS]"] + tokens + ["[SEP]"]))
print("BERT tokens: ", bert_tokens)

token_ids = bert_tokenizer.convert_tokens_to_ids(bert_tokens)
print("BERT token IDs: ", token_ids)

"""
ベクトル取得
"""
print("\n *** to Vector ***")
tokens_tensor = torch.tensor(token_ids).unsqueeze(0)
outputs, _ = bert_model(tokens_tensor)
print(outputs[0], "\n (size: ", outputs[0].size(), ")")