質問応答システムの実装と考察: BERT for Question Answering を使ってみる
背景
日本語の質問応答システムの実装です。
前回は、機械学習を用いない、基本的な数値演算の組み合わせで、回答を選択するモデルを構築しました。しかし、精度や速度に改良すべき箇所をたくさん残しています。
↓ 前回
質問応答システムの実装と考察:BOWとTFIDFによる検索 - 燻ったエンジニアのブログ
今回は、機械学習手法を用いて、質問応答を試してみました。
BERT と呼ばれる、 Google 社が開発した自然言語処理のための機械学習手法です。
文章の一部の単語を隠し、それを予測する Masked Language Model と、2つの文章が繋がっている(ちょうど前後に位置している)かを予測する Next Sentence Prediction により文章を学習し、分類などの問題を解けるようにするというのが概要です。
言語は問わないため、日本語を学習させて、日本語の質問に答えさせてみます。
準備
私はコーパスとして Wikipedia をよく使っていますが、 BERT でこの規模の文章を学習するのはとても時間がかかり、数週間から数ヶ月ほどになるようです。したがって、自宅のパソコンで学習を行うのは、電気代や機器(特に GPU)の消耗の観点からしても非現実的です。
ですので、公開されている学習済みモデルを取得します。
↓ 参考にしたページ
BERT日本語Pretrainedモデル - KUROHASHI-CHU-MURAWAKI LAB
環境は Python 3.7.6 を使いました。 BERT を使うために、 pip で transformer を入れる必要があります。また、学習済みモデルの形態素解析器と合わせるために、 Juman を使いました。計算にpytorchも使っています。
実験
BERT の事前学習モデルを読み込みます。
bert_qa = BertForQuestionAnswering.from_pretrained("/path/to/model")
質問を用意します。
context = "..." question = "..." answer_words = "..."
形態素解析をします。
juman = Juman() def juman_tokenize(text): return [mrph.midasi for mrph in juman.analysis(text).mrph_list()] context_words = juman_tokenize(context) question_words = juman_tokenize(question) answer_words = juman_tokenize(answer)
BERT Tokenizer を用いて解析します。
tokenizer = BertTokenizer.from_pretrained("/path/to/model") tokenized = tokenizer.encode_plus( " ".join(question_words), " ".join(context_words), max_length = 128, pad_to_max_length = True, add_special_tokens = True, )
モデルに与えてみます。
input_ids = tokenized["input_ids"] attention_mask = tokenized["attention_mask"] token_type_ids = tokenized["token_type_ids"] start_scores, end_scores = bert_qa( torch.tensor([input_ids]), token_type_ids = torch.tensor([token_type_ids]) )
start_scores, end_scores から、スコアの高いものの場所を選択し、その範囲を回答と決定します。
end_scoresの高いものが後ろに来るように、スコアを調節しています。
start_index = int(torch.argmax(start_scores)) score_mask = torch.tensor([-10000] * (start_index + 1) + [0] * (128 - start_index - 1)).reshape(1, 128) end_index = torch.argmax(end_scores + score_mask)
文字に起こして回答を得ます。
response_tokens_tmp = input_ids[start_index : end_index + 1] response_tokens = tokenizer.convert_ids_to_tokens(response_tokens_tmp, skip_special_tokens = True) response = "".join(response_tokens)
response に回答の文字列が格納されます。
結果
質問1
context = "明智光秀は、織田信長を本能寺の変で討った後、山崎の戦いで豊臣秀吉に討ち取られた。" question = "本能寺の変で織田信長を討ったのは誰。" answer = "明智光秀"
回答1
、山崎の戦いて豊臣秀吉に討ち
質問2
context = "浅井家当主の長政は、織田家に追い詰められた後、自害した。" question = "長政は追い詰められたときどうした。" answer = "自害した。"
回答2
、織
質問3
context = "今日は8月25日です。昨日から今日にかけて関東では雨が降っていましたが、明日は快晴になり、気温は上昇する見込みです。" question = "明日の天気はどうなる?" answer = "快晴"
回答3
、明日は快晴になり、気温は上昇する見込み
質問4
context = "このゲームは、定価1000円ですが、7割引となっており300円で購入できます。" question = "ゲームは何円で買える?" answer = "300円"
回答4
円て買える?このケー##ムは、定価円て##すか、割引となっており
4つの質問を投げてみました。
結果としては、あまりいい回答を得られないように見られました。3つめ(質問・回答3)だけは良さそうです。
4つめについては、 BERT で tokenize した形跡が残ってしまっているので、コードに間違いがないか要調査です。
考察
以下のようなことがわかります。
- 回答候補となる文章を与えなければならない。
- 回答は与えられた文章から抽出してくるため、文章に回答が載っていないと正しく答えられない。
- 短い文章でもあまり的確に答えられない。
- 単語でも文章でも回答できる。(ファクトイド、ノンファクトイド両刀)
- 知識ベースでなくても回答できる。(明日の天気など)
私が目指している質問応答システムとは方向性が違うようですが、参考にできるものもあるかもしれません。特に、 Masked Language Model と Next Sentence Prediction の考え方は取り入れる価値がありそうです。(難しそうですが)
また、 BERT の学習時間が膨大であることを考えると、個人の趣味の範囲で効果的な機械学習モデルを構築、学習、実験するのはかなり難しい、あるいは非現実的ではないかという考えが過ぎり、暗雲が立ち込めてきました。