Closed6

LangChainのQAボットの評価を行う

kun432kun432

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.

なるほど。こういうプロンプトになってるのね。

kun432kun432

では、このプロンプトをいじって、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

ちょっと改行が含まれてしまっているけども。

kun432kun432

上の例では事前に想定される回答を含めていたが、明確な回答のかわりに「コンテキスト」を渡して比較することもできる。その場合は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の回答と「見比べ」ながら評価できて良さそうな気がする。

kun432kun432

他の評価基準を使って評価することもできる。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の使い方がまったくわかってないので以下の記事が参考にざざっと。

https://note.com/npaka/n/n4291c091d351

https://blog.shinonome.io/huggingface-evaluate/

なので上の結果を、正解データ・評価データのフォーマットにあわせる形で変更する必要がある。

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}
kun432kun432

evaluateについては別途ためしてみるということで。

このスクラップは2023/05/21にクローズされました