目次
1.はじめに
2.質問応答の基本パターン
3.社内手続きに基づく質問応答
4.課題
5.おわりに
1. はじめに
起:Chat-GPTでも使われているTrasformerを使い、社内手続きを与えて、質問応答できるか試してみました。
承:やってみたところ、原理原則的には、出来ることが分かりました。
転:しかしながら、日本語のモデルでは、安定的な結果を得ることは出来ませんでした(ちゃんと答えられないケースがありました)。
結:どうすれば、安定的に上手く行くか、今後とも、試行を続けたいと思います。
以下の違いには、注意して、お読みください。
Transformer
一般的なアーキテクチャの名前であり、シーケンスデータを処理するために設計されたモデルを指します。Transformerは、"Attention is All You Need"という論文で初めて紹介されました。
Transformersライブラリ
Hugging Face社が開発したPythonのライブラリであり、Transformerアーキテクチャを利用した様々な事前学習済みモデルを簡単に利用できるようにしたものです。
2. 質問応答の基本パターン
質問応答はQAタスクとも呼ばれ、与えられた文書と質問に対して、適切な回答を生成することが目的です。
手始めに、基本パターンから試してみましょう。
from transformers import pipeline
nlp = pipeline('question-answering', model='distilbert-base-cased-distilled-squad', tokenizer='distilbert-base-cased-distilled-squad')
context = "Hugging Face is a great company that is based in New York City. They are known for their work in natural language processing."
question = "Where is Hugging Face located?"
answer = nlp({'question': question, 'context': context})
print(answer)
出力結果は、以下の通りです。
{'score': 0.9802650809288025, 'start': 49, 'end': 62, 'answer': 'New York City'}
与えられた文脈(context
)に基づき、Where is Hugging Face located? という質問に対して、正しく New York City と回答できています。
Hugging Face社は時価総額20億ドル超のユニコーン企業です。シリコンバレーではなく、ニューヨークに本社を置いているのは、ビジネスを強く意識しているのかしれません。
3. 社内手続きに基づく、質問応答
それでは、いよいよ、社内手続きに基づく、質問応答ができるか、試してみたいと思います。
まず、日本語を扱いますので、準備をします。
# 日本語対応パッケージのインストール
pip install fugashi[unidic-lite]
pip install ipadic
ここでは、例として、架空の「休暇申請ルール」を基に、質問応答ができるかを試します。
文脈(context)を「休暇は、前日までに申請します。」とした上で、
質問(question)「休暇は、いつまでに申請しますか?」に、正しく応答できるかがポイントです。
from transformers import BertJapaneseTokenizer, AutoModelForQuestionAnswering
import torch
# モデルとトークナイザーの初期化
model = AutoModelForQuestionAnswering.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
tokenizer = BertJapaneseTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
def generate_answer(model, tokenizer, context, question):
# 推論の実行
inputs = tokenizer.encode_plus(question, context, add_special_tokens=True, return_tensors="pt")
input_ids = inputs["input_ids"].tolist()[0]
output = model(**inputs)
answer_start = torch.argmax(output.start_logits)
answer_end = torch.argmax(output.end_logits) + 1
answer = tokenizer.convert_tokens_to_string(tokenizer.convert_ids_to_tokens(input_ids[answer_start:answer_end]))
return answer
# 例として休暇の申請ルールを示す
context = "休暇は、前日までに申請します。"
question = "休暇は、いつまでに申請しますか?"
answer = generate_answer(model, tokenizer, context, question)
# 結果出力
print("質問: " + question)
print("応答: " + answer)
結果は、以下の通りです。
質問: 休暇は、いつまでに申請しますか?
応答: 前日 まで に 申請 し ます
コードの解説をします。
-
from transformers import BertJapaneseTokenizer, AutoModelForQuestionAnswering
:この行は、Hugging Faceの
transformers
ライブラリから、日本語のBERTモデルとトークナイザーを使えるようにインポートしています。 -
import torch
:PyTorchライブラリを使えるようにインポートしています。
-
model = AutoModelForQuestionAnswering.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
:'cl-tohoku/bert-base-japanese-whole-word-masking' という名前の事前学習済みの日本語BERTモデルを読み込み、
model
変数に格納しています。 -
tokenizer = BertJapaneseTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
:同様に、上記のモデルに対応するトークナイザーを読み込み、
tokenizer
変数に格納しています。 -
def generate_answer(model, tokenizer, context, question):
:generate_answer
という関数を定義しています。この関数は、与えられた文章(context
)と質問(question
)に対して回答を生成します。 -
inputs = tokenizer.encode_plus(question, context, add_special_tokens=True, return_tensors="pt")
:与えられた質問と文章をトークン化し、モデルに入力できる形に変換しています。
-
input_ids = inputs["input_ids"].tolist()[0]
:トークン化された入力をIDに変換し、リストに変換しています。
-
output = model(**inputs)
:BERTモデルに入力を与え、出力を取得しています。この出力には、回答の開始位置と終了位置の予測が含まれます。
-
answer_start = torch.argmax(output.start_logits)
およびanswer_end = torch.argmax(output.end_logits) + 1
:モデルの出力から、回答の開始位置と終了位置を取得しています。
-
answer = tokenizer.convert_tokens_to_string(tokenizer.convert_ids_to_tokens(input_ids[answer_start:answer_end]))
:トークンIDを文字列に変換し、それを結合して回答を生成しています。
-
context = "休暇は、前日までに申請します。"
question = "休暇は、いつまでに申請しますか?"
:例として、
context
には休暇の申請ルールを示す文章が、question
にはそのルールに関する質問が与えられています。 -
answer = generate_answer(model, tokenizer, context, question)
:先ほど定義した関数を使って、与えられた
context
とquestion
に基づいて回答を生成します。
4. 課題
繰り返しとなりますが、結果は、以下の通りでした。
質問: 休暇は、いつまでに申請しますか?
応答: 前日 まで に 申請 し ます
まずまずの結果ですが、何度か繰り返し実行すると、正しく応答できないケース(例:答えが返ってこないケース、[BERT]で使われている入力の頭につける[CLS]
や文章の区切りにつける[SEP]
が含まれるケース)が数多く見受けられました。
ちなみに、後述しますが、英語であれば、安定的に正しい応答が返ってきました。
この問題は、日本語の文脈においてBERTモデルが正しく動作しない可能性があることに起因するようです。
英語のモデル(distilbert-base-cased-distilled-squad
)は、SQuADデータセットなどの大量の英語の質問回答データで訓練されており、それによって比較的正確な結果を出すことができます。
一方、今回使用した日本語のモデル(cl-tohoku/bert-base-japanese-whole-word-masking
)は、日本語の質問回答タスク用に訓練されていますが、その訓練データセットやタスクの複雑さが英語のSQuADに比べて制限されている可能性があります。そのため、正しい結果を返す場合とそうでない場合があるかもしれません。
このような問題の場合、いくつかの方法で安定性を向上させることが考えられます。
-
モデルのファインチューニング
cl-tohoku/bert-base-japanese-whole-word-masking
モデルを、特定の質問回答タスクに適したデータでファインチューニングすることで、性能を向上させることができると思います。まあ、それなりに、大変そうです、、、 -
文脈の前処理
日本語の文脈をモデルが理解しやすくするために、文脈の前処理を行うことができます。例えば、文脈をより明確に表現する方法を考えることが重要だと思います。 -
他のモデルの試用
他の日本語の質問回答モデルを試すことで、どれが最も適しているかを確認することができると思います。
最終的には、タスクに最適な結果を得るためには、試行錯誤が必要だと思われます。
【参考】英語だと安定的に上手くいきます!
冒頭、英語での質問応答の例を、ご紹介しました。
当然かもしれませんが、こちらのパターンであれば、安定的に、正解が得られます。
from transformers import pipeline
nlp = pipeline('question-answering', model='distilbert-base-cased-distilled-squad', tokenizer='distilbert-base-cased-distilled-squad')
context = "Vacation must be requested by the day before."
question = "By when should vacation be requested?"
answer = nlp({'question': question, 'context': context})
print(answer)
結果は、以下の通りです。
{'score': 0.6081519722938538, 'start': 34, 'end': 44, 'answer': 'day before'}
何度やっても、By when should vacation be requested? との質問に対して、Vacation must be requested by the day before. という社内手続きに基づいて、正しく day before と、応答します。
結局、日本語のモデル構築や処理が難しいということなのかもしれません。
5. おわりに
社内手続きを与えて、質問応答できるかという今回の試みでしたが、原理的には可能なものの、(日本語においては)実用化へのハードルは、それなりに高そうです。
しっかり、学習させて、それなりのモデルを構築する必要がありそうです。
課題の解決には、今後とも、自然言語処理に精進していく必要がありそうです(笑)
「こうしたら良いよ」といった意見や感想を頂けたら、嬉しいです!
最後まで、お読みいただき、ありがとうございます。
【補足説明】応答における半角スペースについて
応答: 前日 まで に 申請 し ます
応答において、微妙に半角スペースが入っていることに、気づかれた方もいると思います。スルドイです!
これは、日本語の場合、英語のように単語が区切られていませんので、分析する場合は、形態素という固まりに分けて処理しているからだと考えられます。
名詞や助詞、助動詞に分けて処理しているので、そこに空白が入っています。