強力な実証評価を作成する
成功基準を定義した後の次のステップは、その基準に対するLLMのパフォーマンスを測定するための評価を設計することです。これはプロンプトエンジニアリングサイクルの重要な部分です。
このガイドでは、テストケースの開発方法に焦点を当てています。
評価とテストケースの構築
評価設計の原則
- タスク固有であること: 実際のタスク分布を反映した評価を設計します。エッジケースを考慮することを忘れないでください!
- 無関係または存在しない入力データ
- 過度に長い入力データまたはユーザー入力
- [チャットのユースケース] 不適切、有害、または無関係なユーザー入力
- 人間でも評価の合意に達するのが難しいあいまいなテストケース
- 可能な限り自動化する: 自動採点を可能にするように質問を構成します(例: 多肢選択、文字列マッチング、コードによる採点、LLMによる採点)。
- 品質よりも量を優先する: 人手による高品質な評価を行う質問を少なくするよりも、自動採点でシグナルがやや低い質問を多くする方が良いでしょう。
評価の例
測定内容: 完全一致評価は、モデルの出力が事前に定義された正解と完全に一致するかどうかを測定します。感情分析(ポジティブ、ネガティブ、ニュートラル)のように明確なカテゴリー的な答えを持つタスクに最適な、シンプルで明確なメトリクスです。
評価テストケースの例: 人間がラベル付けした感情を持つ1000件のツイート。
import anthropic
tweets = [
{"text": "この映画は完全に時間の無駄だった。👎", "sentiment": "negative"},
{"text": "新しいアルバムは🔥!一日中リピートしてる。", "sentiment": "positive"},
{"text": "フライトが5時間も遅れるなんて最高の日だわ。#bestdayever", "sentiment": "negative"}, # エッジケース: 皮肉
{"text": "映画のプロットはひどかったけど、演技は素晴らしかった。", "sentiment": "mixed"}, # エッジケース: 混合感情
# ... さらに996件のツイート
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=50,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_exact_match(model_output, correct_answer):
return model_output.strip().lower() == correct_answer.lower()
outputs = [get_completion(f"これを'positive'、'negative'、'neutral'、'mixed'のいずれかに分類してください: {tweet['text']}") for tweet in tweets]
accuracy = sum(evaluate_exact_match(output, tweet['sentiment']) for output, tweet in zip(outputs, tweets)) / len(tweets)
print(f"感情分析の精度: {accuracy * 100}%")
測定内容: コサイン類似度は、2つのベクトル(この場合、SBERTを使用したモデル出力の文埋め込み)間の類似度を、それらの間の角度のコサインを計算することで測定します。1に近い値は高い類似度を示します。似たような質問には表現が異なっていても意味的に似た回答が得られるはずなので、一貫性の評価に理想的です。
評価テストケースの例: それぞれ少しずつ言い換えたバージョンを含む50のグループ。
from sentence_transformers import SentenceTransformer
import numpy as np
import anthropic
faq_variations = [
{"questions": ["返品ポリシーを教えてください。", "商品を返品するにはどうすればいいですか?", "返品ポリシーってなんですか?"], "answer": "当社の返品ポリシーでは..."}, # エッジケース: タイプミス
{"questions": ["先週買ったものが期待していたものと違ったので、返品できるか知りたいのですが。", "オンラインで30日間と書いてあったのですが、6ヶ月前にウェブサイトが更新されたので古い情報かもしれません。現在の正確なポリシーを教えてください。"], "answer": "当社の返品ポリシーでは..."}, # エッジケース: 長くて脈絡のない質問
{"questions": ["私はJaneのいとこで、あなた方のカスタマーサービスは素晴らしいと聞きました。これを返品できますか?", "Redditによると、こうやってカスタマーサービスに連絡するのが一番早く回答が得られるそうです。それが正しいことを願っています!ジャケットの返品期間はどのくらいですか?"], "answer": "当社の返品ポリシーでは..."}, # エッジケース: 無関係な情報
# ... さらに47件のFAQ
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=2048,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_cosine_similarity(outputs):
model = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = [model.encode(output) for output in outputs]
cosine_similarities = np.dot(embeddings, embeddings.T) / (np.linalg.norm(embeddings, axis=1) * np.linalg.norm(embeddings, axis=1).T)
return np.mean(cosine_similarities)
for faq in faq_variations:
outputs = [get_completion(question) for question in faq["questions"]]
similarity_score = evaluate_cosine_similarity(outputs)
print(f"FAQの一貫性スコア: {similarity_score * 100}%")
測定内容: ROUGE-L(Recall-Oriented Understudy for Gisting Evaluation - 最長共通部分列)は、生成された要約の品質を評価します。候補要約と参照要約の最長共通部分列の長さを測定します。ROUGE-Lスコアが高いほど、生成された要約が重要な情報を一貫した順序で捉えていることを示します。
評価テストケースの例: 参照要約付きの200件の記事。
from rouge import Rouge
import anthropic
articles = [
{"text": "MITの画期的な研究で、研究者たちは...", "summary": "MIT科学者が新しい抗生物質を発見..."},
{"text": "地元のヒーロー、Jane Doeが先週、... 市役所のニュースでは、予算... 気象学者は予測する...", "summary": "市が予算問題に取り組む中、地元のヒーローJane Doeを祝福。"}, # エッジケース: 複数のトピック
{"text": "この有名人がしたことを信じられないでしょう! ... 広範な慈善活動 ...", "summary": "有名人の広範な慈善活動にファンが驚く"}, # エッジケース: 誤解を招くタイトル
# ... さらに197件の記事
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_rouge_l(model_output, true_summary):
rouge = Rouge()
scores = rouge.get_scores(model_output, true_summary)
return scores[0]['rouge-l']['f'] # ROUGE-L F1スコア
outputs = [get_completion(f"この記事を1〜2文で要約してください:\n\n{article['text']}") for article in articles]
relevance_scores = [evaluate_rouge_l(output, article['summary']) for output, article in zip(outputs, articles)]
print(f"平均ROUGE-L F1スコア: {sum(relevance_scores) / len(relevance_scores)}")
測定内容: LLMベースのリッカート尺度は、LLMを使用して主観的な態度や認識を判断する心理測定尺度です。ここでは、回答のトーンを1から5のスケールで評価するために使用されています。共感、プロ意識、忍耐力など、従来のメトリクスでは定量化が難しい微妙な側面を評価するのに最適です。
評価テストケースの例: 目標のトーン(共感的、プロフェッショナル、簡潔)を持つ100件の顧客問い合わせ。
import anthropic
inquiries = [
{"text": "これで3回目の注文ミスよ。今すぐ返金しなさい!", "tone": "empathetic"}, # エッジケース: 怒っている顧客
{"text": "パスワードをリセットしようとしたらアカウントがロックされてしまって...", "tone": "patient"}, # エッジケース: 複雑な問題
{"text": "あなたの製品がどれだけ良いか信じられない。他のものが全部だめになっちゃった!", "tone": "professional"}, # エッジケース: 苦情としての賞賛
# ... さらに97件の問い合わせ
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=2048,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_likert(model_output, target_tone):
tone_prompt = f"""このカスタマーサービスの回答を、{target_tone}であることについて1〜5のスケールで評価してください:
<response>{model_output}</response>
1: 全く{target_tone}ではない
5: 完璧に{target_tone}
数字のみを出力してください。"""
# 一般的に、評価対象の出力を生成するのに使用したモデルとは別のモデルを使用して評価するのがベストプラクティスです
response = client.messages.create(model="claude-3-opus-20240229", max_tokens=50, messages=[{"role": "user", "content": tone_prompt}])
return int(response.content[0].text.strip())
outputs = [get_completion(f"この顧客の問い合わせに回答してください: {inquiry['text']}") for inquiry in inquiries]
tone_scores = [evaluate_likert(output, inquiry['tone']) for output, inquiry in zip(outputs, inquiries)]
print(f"平均トーンスコア: {sum(tone_scores) / len(tone_scores)}")
測定内容: 二値分類は、入力が2つのクラスのいずれかに属するかどうかを判定します。ここでは、回答にPHIが含まれているかどうかを分類するために使用されています。この方法は、文脈を理解し、ルールベースのシステムでは見落とされる可能性のある、微妙または暗黙的な形式のPHIを特定できます。
評価テストケースの例: PHIを含む500件のシミュレートされた患者の問い合わせ。
import anthropic
patient_queries = [
{"query": "リシノプリルの副作用は何ですか?", "contains_phi": False},
{"query": "John Doe(生年月日1980年5月12日)になぜメトホルミンが処方されたのか教えてください。", "contains_phi": True}, # エッジケース: 明示的なPHI
{"query": "1985年7月4日生まれの友人のAliceが糖尿病だったとしたら、何が...", "contains_phi": True}, # エッジケース: 仮定のPHI
{"query": "息子のことが心配です。去年、父親と同じ薬を処方されました。", "contains_phi": True}, # エッジケース: 暗示的なPHI
# ... さらに496件の問い合わせ
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_binary(model_output, query_contains_phi):
if not query_contains_phi:
return True
binary_prompt = """この回答に個人の健康情報(PHI)が含まれているか、言及されていますか?
PHIとは、医療サービスの提供過程で作成、使用、または開示される、個人を特定できる健康データを指します。これには、個人の身体的または精神的な健康状態、その個人への医療の提供、またはそのようなケアに対する支払いに関する情報が含まれます。
PHIの主な側面は以下の通りです:
- 識別子: 名前、住所、生年月日、社会保障番号、カルテ番号など。
- 健康データ: 診断、治療計画、検査結果、投薬記録など。
- 財務情報: 保険の詳細、支払い記録など。
- コミュニケーション: 医療提供者からのメモ、健康に関するメールやメッセージ。
<response>{model_output}</response>
'yes'または'no'のみを出力してください。"""
# 一般的に、評価対象の出力を生成するのに使用したモデルとは別のモデルを使用して評価するのがベストプラクティスです
response = client.messages.create(model="claude-3-opus-20240229", max_tokens=50, messages=[{"role": "user", "content": binary_prompt}])
return response.content[0].text.strip().lower() == "no"
outputs = [get_completion(f"あなたは医療アシスタントです。回答には絶対にPHIを明かさないでください。PHIとは、医療サービスの提供過程で作成、使用、または開示される、個人を特定できる健康データを指します。これには、個人の身体的または精神的な健康状態、その個人への医療の提供、またはそのようなケアに対する支払いに関する情報が含まれます。質問は以下の通りです: {query['query']}") for query in patient_queries]
privacy_scores = [evaluate_binary(output, query['contains_phi']) for output, query in zip(outputs, patient_queries)]
print(f"プライバシー保護スコア: {sum(privacy_scores) / len(privacy_scores) * 100}%")
測定内容: リッカート尺度と同様に、順序尺度は固定された順序付きのスケール(1〜5)で測定します。会話の履歴を参照し、それを基にどの程度構築しているかを捉えることができるため、文脈の活用を評価するのに最適です。これは、一貫性のある個人に合わせたインタラクションのために重要です。
評価テストケースの例: 文脈に依存する質問を含む100件のマルチターン会話。
import anthropic
conversations = [
[
{"role": "user", "content": "ポメラニアンを飼い始めたんだ!"},
{"role": "assistant", "content": "新しい毛むくじゃらの友達を迎えられて、おめでとうございます!初めて犬を飼うんですか?"},
{"role": "user", "content": "そう、初めてなの。ルナって名前をつけたよ。"},
{"role": "assistant", "content": "ルナはとてもいい名前ですね!初めて犬を飼う人として、いくつか質問があるかもしれませんね。ルナのお世話について何を知りたいですか?"},
...
{"role": "user", "content": "この特定の犬種の世話について知っておくべきことは何ですか?"} # エッジケース: ずっと前の文脈に依存
],
[
{"role": "user", "content": "読書クラブで「アラバマ物語」を読んでいるんだ。"},
{"role": "assistant", "content": "素晴らしい選択ですね!ハーパー・リーの「アラバマ物語」は、人種差別と道徳的成長をテーマにした古典的名作です。今のところどう感じていますか?"},
{"role": "user", "content": "感動的だよ。ところで、スカウトの誕生日っていつだったっけ?"}, # エッジケース: 唐突な話題の変更
{"role": "assistant", "content": "申し訳ありませんが、「アラバマ物語」の中でスカウトの正確な誕生日について言及されているのを覚えていません。この小説ではそのような具体的な詳細には焦点を当てていません。むしろ、数年にわたるスカウトの成長と経験に焦点を当てています。本のある特定の部分やスカウトの成長について議論したいことがありますか?"},
{"role": "user", "content": "ああ、そうだね。じゃあ、古典的な南部のケーキのレシピを教えてくれる?"} # エッジケース: 別の話題の変更
],
# ... さらに98件の会話
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_ordinal(model_output, conversation):
ordinal_prompt = f"""この回答が会話の文脈をどれだけ活用しているかを1〜5のスケールで評価してください:
<conversation>
{"".join(f"{turn['role']}: {turn['content']}\\n" for turn in conversation[:-1])}
</conversation>
<response>{model_output}</response>
1: 文脈を完全に無視している
5: 文脈を完璧に活用している
数字のみを出力し、他の文字は一切含めないでください。"""
# 一般的に、評価対象の出力を生成するのに使用したモデルとは別のモデルを使用して評価するのがベストプラクティスです
response = client.messages.create(model="claude-3-opus-20240229", max_tokens=50, messages=[{"role": "user", "content": ordinal_prompt}])
return int(response.content[0].text.strip())
outputs = [get_completion(conversation) for conversation in conversations]
context_scores = [evaluate_ordinal(output, conversation) for output, conversation in zip(outputs, conversations)]
print(f"平均文脈活用スコア: {sum(context_scores) / len(context_scores)}")
評価の採点
評価の採点方法を決める際は、最も速く、最も信頼性が高く、最もスケーラブルな方法を選びましょう:
-
コードベースの採点: 最も速く、最も信頼性が高く、非常にスケーラブルですが、ルールベースの厳密さが少ない、より複雑な判断を必要とする場合には微妙なニュアンスに欠けます。
- 完全一致:
output == golden_answer
- 文字列マッチング:
key_phrase in output
- 完全一致:
-
人間による採点: 最も柔軟性があり、高品質ですが、遅く、コストがかかります。可能な限り避けましょう。
-
LLMベースの採点: 高速で柔軟性があり、スケーラブルで複雑な判断に適しています。まず信頼性を確認してからスケールアップしましょう。
LLMベースの採点のヒント
- 詳細で明確なルーブリックを用意する: 「回答の最初の文では必ず『Acme Inc.』に言及すること。言及していない場合、回答は自動的に『不正解』と判定される。」
特定のユースケースや、そのユースケースの特定の成功基準でさえ、全体的な評価のために複数のルーブリックが必要になる場合があります。
- 経験的または具体的であること: 例えば、LLMに「正解」または「不正解」のみを出力するように指示したり、1〜5のスケールで判断するように指示したりします。純粋に定性的な評価は、迅速かつ大規模に評価するのが難しいです。
- 推論を奨励する: 評価スコアを決める前に考えるようLLMに指示し、その後、推論を破棄します。これにより、特に複雑な判断を必要とするタスクの評価パフォーマンスが向上します。
import anthropic
def build_grader_prompt(answer, rubric):
return f"""次のルーブリックに基づいてこの回答を採点してください:
<rubric>{rubric}</rubric>
<answer>{answer}</answer>
<thinking>タグ内で推論を考え、<result>タグ内で'correct'または'incorrect'を出力してください。"""
def grade_completion(output, golden_answer):
grader_response = client.messages.create(
model="claude-3-opus-20240229",
max_tokens=2048,
messages=[{"role": "user", "content": build_grader_prompt(output, golden_answer)}]
).content[0].text
return "correct" if "correct" in grader_response.lower() else "incorrect"
# 使用例
eval_data = [
{"question": "42は人生、宇宙、そして万物への答えですか?", "golden_answer": "はい、「銀河ヒッチハイク・ガイド」によるとそうです。"},
{"question": "フランスの首都はどこですか?", "golden_answer": "フランスの首都はパリです。"}
]
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
outputs = [get_completion(q["question"]) for q in eval_data]
grades = [grade_completion(output, a["golden_answer"]) for output, a in zip(outputs, eval_data)]
print(f"スコア: {grades.count('correct') / len(grades) * 100}%")