ITエンジニアのブログ

IT企業でエンジニアやってる人間の日常について

質問応答システムの実装と考察: 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 の学習時間が膨大であることを考えると、個人の趣味の範囲で効果的な機械学習モデルを構築、学習、実験するのはかなり難しい、あるいは非現実的ではないかという考えが過ぎり、暗雲が立ち込めてきました。