LangChainのQAボットの評価を行う
LangChainでQAボットの評価を行うためにQAEvalChain
というのがあるので使ってみる。
JupyterLabでやる。
$ pip install jupyterlab ipywidgets pipenv
$ jupyter-lab --ip='0.0.0.0'
パッケージインストール
!pip install openai langchain python-dotenv
.envを読み込んでおく
from dotenv import load_dotenv
load_dotenv()
まずはシンプルなLLMChainで質問してみる。
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.llms import OpenAI
examples = [
{
"question": "ロジャーはテニスボールを5つ持っています。彼はテニスボール缶を2つ買います。1つのテニスボール缶には3つのテニスボールが入っています。彼は今テニスボールを何個持っているでしょうか?",
"answer": "11個",
},
{
"question": '次の文章の内容は正しいですか?「ジョアン・モウティーニョはNFCチャンピオンシップでスクリーンパスをキャッチした。」',
"answer": 'いいえ'
}
]
llm = OpenAI(model_name="text-davinci-003", temperature=0)
prompt = PromptTemplate(template="Question: {question}\nAnswer:", input_variables=["question"])
chain = LLMChain(llm=llm, prompt=prompt)
exampleには質問と期待する回答を入れているが、実際にはPromptTempalteはquestionしか受け取らないので、answerはまだ使われない。ここは後で使うために入れている。
では実行してみる。
predictions = chain.apply(examples)
predictions
実行結果
[{'text': ' 11個'}, {'text': ' 不正確です。'}]
内容としてはあっているけど、当然表現が異なる場合もある。temperature=0
なのでまあこの結果を元の回答に反映するというのも一つの方法ではあるが、temperature
を変えて、内容はあっててほしいけど表現は多少変化をつけたいと普通は思う。
QAEvalChain
を使う、LLMで評価を行うことでこのあたりの差異を吸収してくれる様子。temperatureを少しいじってやってみる。
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.llms import OpenAI
from langchain.evaluation.qa import QAEvalChain
examples = [
{
"question": "ロジャーはテニスボールを5つ持っています。彼はテニスボール缶を2つ買います。1つのテニスボール缶には3つのテニスボールが入っています。彼は今テニスボールを何個持っているでしょうか?",
"answer": "11個",
},
{
"question": '次の文章の内容は正しいですか?「ジョアン・モウティーニョはNFCチャンピオンシップでスクリーンパスをキャッチした。」',
"answer": 'いいえ'
}
]
llm = OpenAI(model_name="text-davinci-003", temperature=0.3)
prompt = PromptTemplate(template="Question: {question}\nAnswer:", input_variables=["question"])
chain = LLMChain(llm=llm, prompt=prompt)
eval_chain = QAEvalChain.from_llm(llm)
predictions = chain.apply(examples)
graded_outputs = eval_chain.evaluate(examples, predictions, question_key="question", prediction_key="text")
想定されるQAと実際の結果を比較する感じ。出力内容を見てみる。
for i, eg in enumerate(examples):
print(f"Example {i}:")
print("Question: " + eg['question'])
print("Real Answer: " + eg['answer'])
print("Predicted Answer: " + predictions[i]['text'])
print("Predicted Grade: " + graded_outputs[i]['text'])
print()
Example 0:
Question: ロジャーはテニスボールを5つ持っています。彼はテニスボール缶を2つ買います。1つのテニスボール缶には3つのテニスボールが入っています。彼は今テニスボールを何個持っているでしょうか?
Real Answer: 11個
Predicted Answer: 11個
Predicted Grade: CORRECT
Example 1:
Question: 次の文章の内容は正しいですか?「ジョアン・モウティーニョはNFCチャンピオンシップでスクリーンパスをキャッチした。」
Real Answer: いいえ
Predicted Answer: 不正確です。
Predicted Grade: CORRECT
ふむ、一応想定通りになる。verbose=True
をつけて詳しく見てみる。
eval_chain = QAEvalChain.from_llm(llm, verbose=True)
> Entering new QAEvalChain chain...
Prompt after formatting:
You are a teacher grading a quiz.
You are given a question, the student's answer, and the true answer, and are asked to score the student answer as either CORRECT or INCORRECT.
Example Format:
QUESTION: question here
STUDENT ANSWER: student's answer here
TRUE ANSWER: true answer here
GRADE: CORRECT or INCORRECT here
Grade the student answers based ONLY on their factual accuracy. Ignore differences in punctuation and phrasing between the student answer and true answer. It is OK if the student answer contains more information than the true answer, as long as it does not contain any conflicting statements. Begin!
QUESTION: ロジャーはテニスボールを5つ持っています。彼はテニスボール缶を2つ買います。1つのテニスボール缶には3つのテニスボールが入っています。彼は今テニスボールを何個持っているでしょうか?
STUDENT ANSWER: 11個
TRUE ANSWER: 11個
GRADE:
Prompt after formatting:
You are a teacher grading a quiz.
You are given a question, the student's answer, and the true answer, and are asked to score the student answer as either CORRECT or INCORRECT.
Example Format:
QUESTION: question here
STUDENT ANSWER: student's answer here
TRUE ANSWER: true answer here
GRADE: CORRECT or INCORRECT here
Grade the student answers based ONLY on their factual accuracy. Ignore differences in punctuation and phrasing between the student answer and true answer. It is OK if the student answer contains more information than the true answer, as long as it does not contain any conflicting statements. Begin!
QUESTION: 次の文章の内容は正しいですか?「ジョアン・モウティーニョはNFCチャンピオンシップでスクリーンパスをキャッチした。」
STUDENT ANSWER: 不正確です。
TRUE ANSWER: いいえ
GRADE:
> Finished chain.
なるほど。こういうプロンプトになってるのね。
では、このプロンプトをいじって、CORRECT/INCORRECTだけじゃなくて、点数で評価させるようにしてみる。
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.llms import OpenAI
from langchain.evaluation.qa import QAEvalChain
from langchain.prompts.prompt import PromptTemplate
_PROMPT_TEMPLATE = """You are an expert professor specialized in grading students' answers to questions.
You are grading the following question:
{query}
Here is the real answer:
{answer}
You are grading the following predicted answer:
{result}
What grade do you give from 0 to 10, where 0 is the lowest (very low similarity) and 10 is the highest (very high similarity)?
"""
PROMPT = PromptTemplate(input_variables=["query", "answer", "result"], template=_PROMPT_TEMPLATE)
examples = [
{
"question": "ロジャーはテニスボールを5つ持っています。彼はテニスボール缶を2つ買います。1つのテニスボール缶には3つのテニスボールが入っています。彼は今テニスボールを何個持っているでしょうか?",
"answer": "11個",
},
{
"question": '次の文章の内容は正しいですか?「ジョアン・モウティーニョはNFCチャンピオンシップでスクリーンパスをキャッチした。」',
"answer": 'いいえ'
}
]
llm = OpenAI(model_name="text-davinci-003", temperature=0.3)
prompt = PromptTemplate(template="Question: {question}\nAnswer:", input_variables=["question"])
chain = LLMChain(llm=llm, prompt=prompt)
eval_chain = QAEvalChain.from_llm(llm=llm, verbose=True, prompt=PROMPT)
predictions = chain.apply(examples)
graded_outputs = eval_chain.evaluate(examples, predictions, question_key="question", answer_key="answer", prediction_key="text")
結果
Example 0:
Question: ロジャーはテニスボールを5つ持っています。彼はテニスボール缶を2つ買います。1つのテニスボール缶には3つのテニスボールが入っています。彼は今テニスボールを何個持っているでしょうか?
Real Answer: 11個
Predicted Answer: 11個
Predicted Grade:
10
Example 1:
Question: 次の文章の内容は正しいですか?「ジョアン・モウティーニョはNFCチャンピオンシップでスクリーンパスをキャッチした。」
Real Answer: いいえ
Predicted Answer: 不正確です。
Predicted Grade:
8
ちょっと改行が含まれてしまっているけども。
上の例では事前に想定される回答を含めていたが、明確な回答のかわりに「コンテキスト」を渡して比較することもできる。その場合はContextQAEvalChain
を使う
まず普通にシンプルなQA。
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.llms import OpenAI
examples = [
{
"question": "私は何歳でしょうか?",
"context": "私は30歳です。ニューヨークに住んでいます。毎日の出勤は電車を使っています。",
},
{
"question": "2023年のNFCチャンピオンシップを勝ったのは?",
"context": "2023年 NFCチャンピオンシップ: フィラデルフィア・イーグルス 31, サンフランシスコ・49ers 7"
}
]
llm = OpenAI(model_name="text-davinci-003", temperature=0)
_PROMPT_TEMPLATE = "Answer the question based on the context\nContext:{context}\nQuestion:{question}\nAnswer:"
prompt = PromptTemplate(input_variables=["context", "question"], template=_PROMPT_TEMPLATE)
chain = LLMChain(llm=llm, prompt=prompt)
predictions = chain.apply(examples)
predictions
結果
[{'text': '30歳です。'}, {'text': 'フィラデルフィア・イーグルス'}]
ContextQAEvalChain
で評価してみる。
from langchain.evaluation.qa import ContextQAEvalChain
eval_chain = ContextQAEvalChain.from_llm(llm)
graded_outputs = eval_chain.evaluate(examples, predictions, question_key="question", prediction_key="text")
for i, eg in enumerate(examples):
print(f"Example {i}:")
print("Question: " + eg['question'])
print("Real context: " + eg['context'])
print("Predicted Answer: " + predictions[i]['text'])
print("Predicted Grade: " + graded_outputs[i]['text'])
print()
結果
Example 0:
Question: 私は何歳でしょうか?
Real context: 私は30歳です。ニューヨークに住んでいます。毎日の出勤は電車を使っています。
Predicted Answer: 30歳です。
Predicted Grade: CORRECT
Example 1:
Question: 2023年のNFCチャンピオンシップを勝ったのは?
Real context: 2023年 NFCチャンピオンシップ: フィラデルフィア・イーグルス 31, サンフランシスコ・49ers 7
Predicted Answer: フィラデルフィア・イーグルス
Predicted Grade: CORRECT
これ、単なる評価だけじゃなくて、VectorDBの検索結果なんかをcontextに渡してあげれば、LLMの回答と「見比べ」ながら評価できて良さそうな気がする。
他の評価基準を使って評価することもできる。HuggingFaceのevaluate
パッケージを使って比較してみる。
一番最初の例を題材にしてみる。
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.llms import OpenAI
from langchain.evaluation.qa import QAEvalChain
examples = [
{
"question": "ロジャーはテニスボールを5つ持っています。彼はテニスボール缶を2つ買います。1つのテニスボール缶には3つのテニスボールが入っています。彼は今テニスボールを何個持っているでしょうか?",
"answer": "11個",
},
{
"question": '次の文章の内容は正しいですか?「ジョアン・モウティーニョはNFCチャンピオンシップでスクリーンパスをキャッチした。」',
"answer": 'いいえ'
}
]
llm = OpenAI(model_name="text-davinci-003", temperature=0.3)
prompt = PromptTemplate(template="Question: {question}\nAnswer:", input_variables=["question"])
chain = LLMChain(llm=llm, prompt=prompt)
eval_chain = QAEvalChain.from_llm(llm)
predictions = chain.apply(examples)
graded_outputs = eval_chain.evaluate(examples, predictions, question_key="question", prediction_key="text")
display(examples)
display(predictions)
display(graded_outputs)
元データ、LLM生成結果、LLM判定結果とかはこういう形式で保持されている状態。
[{'question': 'ロジャーはテニスボールを5つ持っています。彼はテニスボール缶を2つ買います。1つのテニスボール缶には3つのテニスボールが入っています。彼は今テニスボールを何個持っているでしょうか?',
'answer': '11個'},
{'question': '次の文章の内容は正しいですか?「ジョアン・モウティーニョはNFCチャンピオンシップでスクリーンパスをキャッチした。」',
'answer': 'いいえ'}]
[{'text': ' 11個'}, {'text': ' 不正確です。'}]
[{'text': ' CORRECT'}, {'text': ' CORRECT'}]
ではevaluateを使ってみる。パッケージインストール。
!pip install evaluate
でevaluate
の使い方がまったくわかってないので以下の記事が参考にざざっと。
- evaluateではいろいろな評価指標が選択できる。
- 今回使用するのは
SQuAD(Stanford Question Answering Dataset)
-
https://huggingface.co/spaces/evaluate-metric/squad/blob/main/README.md
- ウィキペディアの記事から人間が質問と回答を作成。「読解力」という観点のデータセット。
- フォーマット
- 上記のフォーマットにあわせた正解データと、評価したいデータ、の2つを比較し、完全一致度とF値を出力する
-
https://huggingface.co/spaces/evaluate-metric/squad/blob/main/README.md
なので上の結果を、正解データ・評価データのフォーマットにあわせる形で変更する必要がある。
import copy
correct_data = copy.deepcopy(examples)
prediction_result = copy.deepcopy(predictions)
for i, cr in enumerate(correct_data):
cr['id'] = str(i)
cr['answers'] = {"text": [cr['answer']], "answer_start": [0]}
prediction_result[i]['id'] = str(i)
prediction_result[i]['prediction_text'] = prediction_result[i]['text']
for p in prediction_result:
del p['text']
new_correct_data = correct_data.copy()
for eg in new_correct_data:
del eg ['question']
del eg['answer']
exampleを変更した正解データ(new_correct_data)
[{'id': '0', 'answers': {'text': ['11個'], 'answer_start': [0]}},
{'id': '1', 'answers': {'text': ['いいえ'], 'answer_start': [0]}}]
predictionsを変更した予測データ(prediction_result)
[{'id': '0', 'prediction_text': ' 11個'},
{'id': '1', 'prediction_text': ' 不正確です。'}]
では比較
from evaluate import load
squad_metric = load("squad")
results = squad_metric.compute(
references=new_correct_result,
predictions=prediction_result,
)
results
結果
{'exact_match': 50.0, 'f1': 50.0}
evaluate
については別途ためしてみるということで。