LLM の評価のしくみ:ベンチマークの誤差・pass@k・LLM による採点・ハルシネーション
新しい LLM が発表されるたびに、「このベンチマークで何点」という比較の表が出てきます。また、自分の業務に LLM を使うときも、「どのモデルが良いか」「プロンプトを変えて良くなったか」を判断する必要があります。しかし、LLM の評価は、分類のモデルの評価(機械学習の評価指標のしくみ)よりずっと難しい問題です。答えが文章なので正解と照合しにくく、同じ質問でも答えが毎回変わり、学習データの中に評価の問題が紛れ込んでいることもあります。
この記事では、LLM の評価でよく使われる方法と、その落とし穴を計算で確かめます。
結論を先に書くと、次のようになります。
- ベンチマークの点数には、問題の数で決まる誤差がある。200 問のベンチマークでは、本当は 3 ポイント弱いモデルが、5 回に 1 回くらいは同点以上の点を取った。2 つのモデルの差は、同じ問題どうしの対で比べる
- 答えの照合のしかたで点数が変わる。完全一致は厳しすぎ、単語の重なり(F1)は間違った答えにも部分点を与える。コードはテストに通るかで測り、pass@k は偏りのない式で計算する
- LLM に採点させると、答えの順番などのくせが入る。シミュレーションでは、本当は互角なのに、先に置いた方の勝率が 0.65 になった。順番を入れ替えて両方で聞けば、この偏りは打ち消せる
- 公開のベンチマークは、学習データへの混入で点数が上がって見えることがある。自分の用途には、自分のデータで作った評価セットを用意する
説明の途中に出てくる計算例は、Python と NumPy で実際に動かした結果です。LLM そのものは動かさず、正解・不正解や審査員のふるまいを乱数で作ったシミュレーションで、評価の方法の性質を確かめています。
環境
計算例を動かした環境です。
| 項目 | バージョン |
|---|---|
| Python | 3.13 |
| NumPy | 2.5 |
LLM の評価の種類
LLM の評価は、何を測るかで大きく分けられます。
| 種類 | 方法 | 例 |
|---|---|---|
| 選択式の問題 | 選択肢の記号が正解と一致するか | MMLU(57 分野の 4 択問題) |
| 答えが短い問題 | 正解の文字列や数値と照合する | 数学の答えの数値、質問への短い答え |
| コードの生成 | 生成したコードがテストに通るか | HumanEval、SWE-bench |
| 自由な文章 | 人や LLM が読んで良し悪しを判定する | 要約・相談への答えの比較、Chatbot Arena |
| 事実の正しさ | 答えの中の主張が、根拠と合っているか | ハルシネーションの評価、RAG の答えの根拠の確認 |
| 安全性 | 有害な依頼への反応、偏見 | 断るべき依頼を断るか、断る必要のない依頼を断りすぎないか |
ベンチマークの点数の誤差
200 問で 2 つのモデルを比べる
ベンチマークの問題は、モデルが出会うかもしれない無数の問題から選ばれた「標本」です。同じ実力のモデルでも、どの問題が選ばれたかで点数は変わります。
問題ごとに難しさが違う 200 問を、2 つのモデルに解かせるシミュレーションをします。本当の正解率は、A が約 0.70、B が約 0.67 で、A の方が 3 ポイントほど強い ように作ります。難しさは 2 つのモデルに共通なので、A が解けない問題は B も解けないことが多い、という相関があります。
import numpy as np
# ベンチマークの点数の誤差:問題ごとの難しさが違う 200 問を、2 つのモデルに解かせる
rng = np.random.default_rng(0)
n = 200
difficulty = rng.normal(0, 1.5, n) # 問題ごとの難しさ(両方のモデルに共通)
p_a = 1 / (1 + np.exp(-(1.2 - difficulty))) # モデル A の正解の確率
p_b = 1 / (1 + np.exp(-(1.0 - difficulty))) # モデル B は少しだけ弱い
a = (rng.random(n) < p_a).astype(int) # 難しさが共通なので、2 つのモデルの出来は相関する
b = (rng.random(n) < p_b).astype(int)
print("正解率 A", a.mean(), "B", b.mean(), "差", round(a.mean() - b.mean(), 3))
se_a = np.sqrt(a.mean() * (1 - a.mean()) / n)
print("A の正解率の標準誤差", round(float(se_a), 3), "95% 区間 ±", round(float(1.96 * se_a), 3))
# 別々に誤差を見積もると、差の誤差を大きく見積もりすぎる
se_unpaired = np.sqrt(a.var() / n + b.var() / n)
se_paired = (a - b).std() / np.sqrt(n) # 同じ問題どうしの差で見積もる
print("差の標準誤差:別々に見積もる", round(float(se_unpaired), 3), "対にして見積もる", round(float(se_paired), 3))
# ブートストラップ:問題を重複を許して選び直し、差の分布を作る
diffs = []
for _ in range(10000):
i = rng.integers(n, size=n)
diffs.append(a[i].mean() - b[i].mean())
lo, hi = np.percentile(diffs, [2.5, 97.5])
print("差の 95% 区間(ブートストラップ)", round(float(lo), 3), "〜", round(float(hi), 3))
print("A だけ正解", int(((a == 1) & (b == 0)).sum()), "B だけ正解", int(((a == 0) & (b == 1)).sum()))
実行結果:
正解率 A 0.62 B 0.69 差 -0.07
A の正解率の標準誤差 0.034 95% 区間 ± 0.067
差の標準誤差:別々に見積もる 0.047 対にして見積もる 0.038
差の 95% 区間(ブートストラップ) -0.145 〜 0.005
A だけ正解 23 B だけ正解 37
本当は A の方が強いのに、この 200 問では B の方が 7 ポイントも高い点数になりました。A の正解率の 95% の区間は ±0.067 もあり、200 問のベンチマークでは、数ポイントの差は誤差の範囲だということが分かります。
問題の数を増やすと
この比較を 2000 回くり返して、弱い方の B が A に並ぶか上回る割合を、問題の数を変えて調べます。
# 本当は A の方が強いのに、B が同点以上になる割合を、問題の数を変えて調べる
rng = np.random.default_rng(1)
for n in (100, 200, 1000, 5000):
flips = 0
for _ in range(2000):
difficulty = rng.normal(0, 1.5, n)
a = rng.random(n) < 1 / (1 + np.exp(-(1.2 - difficulty)))
b = rng.random(n) < 1 / (1 + np.exp(-(1.0 - difficulty)))
flips += b.mean() >= a.mean()
print(f"{n} 問: B が A に並ぶか上回る割合 {flips / 2000:.3f}")
実行結果:
100 問: B が A に並ぶか上回る割合 0.335
200 問: B が A に並ぶか上回る割合 0.214
1000 問: B が A に並ぶか上回る割合 0.043
5000 問: B が A に並ぶか上回る割合 0.000
3 ポイントの差を見分けるには、1000 問でもまだ 4% ほど逆転が起きます。誤差は問題の数の平方根に反比例するので、誤差を半分にするには問題を 4 倍に増やす必要があります。プロンプトを少し変えて「100 問で 2 ポイント上がった」というような比較は、ほとんど意味がありません。
対にして比べる
2 つのモデルを比べるときは、同じ問題どうしの差 で誤差を見積もります。上の例では、別々に見積もった差の標準誤差 0.047 に対して、対にすると 0.038 でした。難しい問題では両方とも間違え、易しい問題では両方とも正解するので、その分の揺れが打ち消されるからです。2 つのモデルの出来の相関が強いほど、対にする効果は大きくなります。
さらに、差を決めているのは「片方だけが正解した問題」だけです。上の例では、A だけ正解が 23 問、B だけ正解が 37 問でした。両方正解・両方不正解の問題は、差について何も教えてくれません。この考え方の検定がマクネマー検定で、問題を選び直すブートストラップでも区間を出せます(Miller, 2024)。
答えの照合のしかた
完全一致と単語の F1
答えが短い文章の問題では、正解の文字列との照合のしかたで点数が変わります。
# 答えの照合:完全一致と、単語の重なりの F1
def normalize(s):
for ch in "、。,.!?「」":
s = s.replace(ch, " ")
return s.lower().split()
def exact_match(pred, gold):
return float(normalize(pred) == normalize(gold))
def token_f1(pred, gold):
p, g = normalize(pred), normalize(gold)
common = sum(min(p.count(w), g.count(w)) for w in set(p))
if common == 0:
return 0.0
prec, rec = common / len(p), common / len(g)
return 2 * prec * rec / (prec + rec)
gold = "Tokyo Tower"
for pred in ("Tokyo Tower", "tokyo tower.", "It is Tokyo Tower", "Tokyo Skytree", "Kyoto Tower"):
print(f"{pred!r}: 完全一致 {exact_match(pred, gold):.0f}, F1 {token_f1(pred, gold):.2f}")
実行結果:
'Tokyo Tower': 完全一致 1, F1 1.00
'tokyo tower.': 完全一致 1, F1 1.00
'It is Tokyo Tower': 完全一致 0, F1 0.67
'Tokyo Skytree': 完全一致 0, F1 0.50
'Kyoto Tower': 完全一致 0, F1 0.50
- 大文字・小文字と句読点をそろえる 正規化 をすれば、
tokyo tower.も正解にできます - 完全一致は厳しすぎる。「It is Tokyo Tower」は正しい答えなのに 0 点です。LLM は文で答えがちなので、「答えだけを書く」ように指示するか、答えの部分を取り出してから照合します
- 単語の F1 は、間違った答えにも部分点を与える。「Kyoto Tower」は別の建物なのに 0.5 点です。文字や単語の重なりで測る指標(BLEU・ROUGE も同じ仲間)は、意味の正しさは測れません
日本語では、単語が空白で区切られていないので、単語の F1 を使うなら形態素解析や文字単位の比較が必要です(トークナイザーのしくみ)。数学の問題なら、答えの数値だけを取り出して比べるのが確実です。
コードの生成と pass@k
コードの生成では、生成したコードを実行して テストに通るか で正しさを測れます。同じ問題で何回か生成させ、「k 回のうち 1 回でも通る確率」を pass@k と呼びます。
pass@k を計算するとき、ちょうど k 回だけ生成して数えると、ばらつきが大きくなります。そこで、n 回(n > k)生成して c 回通ったときに、次の式で推定します(Chen ら, 2021)。n 個の中から k 個を選んだとき、すべてが不正解になる確率を 1 から引いたものです。
pass@k = 1 − C(n − c, k) / C(n, k)
from math import comb
# pass@k:n 回生成して c 回テストに通ったとき、k 回のうち 1 回でも通る確率の推定
def pass_at_k(n, c, k):
return 1.0 if n - c < k else 1 - comb(n - c, k) / comb(n, k)
for c in (0, 1, 5, 20):
print(f"20 回中 {c} 回正解: pass@1 {pass_at_k(20, c, 1):.3f}, pass@5 {pass_at_k(20, c, 5):.3f}, "
f"素朴な式 1-(1-c/n)^5 {1 - (1 - c / 20) ** 5:.3f}")
実行結果:
20 回中 0 回正解: pass@1 0.000, pass@5 0.000, 素朴な式 1-(1-c/n)^5 0.000
20 回中 1 回正解: pass@1 0.050, pass@5 0.250, 素朴な式 1-(1-c/n)^5 0.226
20 回中 5 回正解: pass@1 0.250, pass@5 0.806, 素朴な式 1-(1-c/n)^5 0.763
20 回中 20 回正解: pass@1 1.000, pass@5 1.000, 素朴な式 1-(1-c/n)^5 1.000
正解率 c / n を「1 回あたりの正解の確率」とみなして 1 − (1 − c/n)⁵ で計算する素朴な式は、偏りのない式より低めの値になります。pass@k を比べるときは、同じ式で計算されているかを確かめる必要があります。
LLM に採点させる
人の評価は高い
要約や相談への答えのような自由な文章は、正解の文字列と照合できません。いちばん確かなのは人が読んで判定することですが、時間もお金もかかり、判定する人によって基準もぶれます。そこで、強い LLM に審査員をさせる(LLM-as-a-judge)方法が広く使われています。2 つの答えを見せてどちらが良いかを聞く「対の比較」と、基準を示して点数を付けさせる「点数付け」があります。
順番のくせ
LLM の審査員には、中身と関係のない くせ があることが知られています(Zheng ら, 2023)。
| くせ | 内容 |
|---|---|
| 位置の偏り | 先に(または後に)示された答えを選びやすい |
| 長さの偏り | 長くて詳しそうな答えを選びやすい |
| 自分びいき | 自分(や同じ系列のモデル)が書いた答えを高く評価しやすい |
| 見た目の偏り | 箇条書きや見出しなど、整った形の答えを選びやすい |
位置の偏りの影響を、シミュレーションで確かめます。審査員は、30% の確率で中身を見ずに 1 番目の答えを選び、それ以外は正しく良い方を選ぶ、とします。
# LLM による採点の「先に出た方をひいきする」くせを、シミュレーションで確かめる
rng = np.random.default_rng(0)
n = 1000
def judge(first_is_better, bias=0.3):
"""bias の確率で、中身に関係なく 1 番目の答えを選ぶ審査員。それ以外は正しく良い方を選ぶ"""
lazy = rng.random(len(first_is_better)) < bias
return np.where(lazy, True, first_is_better) # True なら 1 番目を選んだ
for true_rate in (0.5, 0.4):
a_better = rng.random(n) < true_rate # 本当に A の方が良い質問
first_a = judge(a_better) # いつも A を 1 番目に置く
first_b = ~judge(~a_better) # B を 1 番目に置いたときに A を選んだか
both = (first_a.astype(float) + first_b) / 2 # 両方の順番で聞いて平均する
consistent = first_a == first_b # 2 回の判定が一致したものだけ
print(f"本当の A の勝率 {true_rate}: A を先に置く {first_a.mean():.3f}, B を先に置く {first_b.mean():.3f}, "
f"両方の平均 {both.mean():.3f}, 判定が入れ替わった割合 {1 - consistent.mean():.3f}")
実行結果:
本当の A の勝率 0.5: A を先に置く 0.650, B を先に置く 0.330, 両方の平均 0.490, 判定が入れ替わった割合 0.320
本当の A の勝率 0.4: A を先に置く 0.583, B を先に置く 0.284, 両方の平均 0.433, 判定が入れ替わった割合 0.299
- いつも A を先に置くと、互角なのに A の勝率が 0.650 になる。本当は B の方が良い場合(A の勝率 0.4)でも 0.583 で、勝ち負けが逆に見えて しまいます
- 順番を入れ替えて両方で聞き、平均すると、偏りは打ち消される(0.490・0.433)。ただし、くせで決まった判定は「引き分け」として平均に入るので、差は実際より小さく見えます(0.4 が 0.433 に)
- 判定が入れ替わった割合は、審査員の信頼性の目安になる。入れ替わりが多い問題は、審査員にとっても判断が難しいか、くせに頼っている問題です
審査員を使うときの工夫
| 工夫 | 内容 |
|---|---|
| 順番を入れ替える | 両方の順番で聞いて、判定が一致したものだけを数えるか、平均する |
| 基準を具体的に書く | 「正確さ」「指示に従っているか」など、何を見て判定するかを書き、理由を先に書かせてから判定させる |
| 人の判定と照らし合わせる | 一部の問題を人も判定し、審査員の判定とどれだけ一致するかを測っておく |
| 長さをそろえる・補正する | 長い答えが有利にならないよう、長さの影響を差し引く方法がある(長さを補正した勝率) |
| 自分のモデルに審査させない | 評価されるモデルと違う系列のモデルを審査員にする |
多くの対の比較の結果から、モデルの強さを 1 つの数で表すときは、LLM の学習のしくみ で説明したブラッドリー・テリーモデル(チェスのイロレーティングと同じ考え方)を使います。人が 2 つの匿名のモデルの答えを比べて投票する Chatbot Arena も、この方法でモデルの順位を付けています。
データの混入:問題を覚えていないか
LLM はインターネットの大量の文章で学習するので、公開されているベンチマークの問題と答えが、学習データに紛れ込んでいることがあります(データの混入、contamination)。そうなると、モデルは問題を解いたのではなく、答えを覚えていただけかもしれません。
| 確かめ方 | 内容 |
|---|---|
| 文字列の重なり | ベンチマークの問題と学習データの間で、長い単語の並び(13 語など)が一致する部分を探す |
| 問題を作り直す | 数値や名前を変えた同じ形の問題で、点数が大きく下がらないかを見る |
| 公開後の問題で測る | モデルの学習データの締め切りより後に作られた問題(新しいコンテストの問題など)だけで評価する |
| 非公開の問題で測る | 問題を公開しない評価セットを使う |
公開のベンチマークの点数は、モデルの大まかな実力の目安にはなりますが、自分の用途での成績の代わりにはなりません。
ハルシネーションの評価
答えの中の主張を確かめる
LLM が、もっともらしいのに事実と違う内容を答えることを ハルシネーション(幻覚)と呼びます。評価のしかたは、答えの根拠をどこに求めるかで変わります。
| 状況 | 確かめること | 方法 |
|---|---|---|
| RAG(RAG とエージェントのしくみ) | 答えの主張が、検索した文書に書かれているか(根拠づけ) | 答えを短い主張に分け、それぞれが文書から言えるかを判定する |
| 一般の知識 | 答えの主張が、世の中の事実と合っているか | 主張ごとに、信頼できる情報源で確かめる(FActScore など) |
| 分からないとき | 知らないことを「分からない」と言えるか | 答えのない質問・誤った前提の質問を混ぜ、断った割合と間違えた割合を測る |
同じ質問をくり返して、ぶれを見る
正解のデータがなくても、ハルシネーションの兆候をつかむ方法があります。同じ質問を何回か聞き、答えがぶれるかを見る 方法です(SelfCheckGPT, Manakul ら, 2023)。よく知っていることは毎回同じように答え、知らないことは毎回違うことを言いやすい、という性質を使います。答えの一致の度合いを、自信の目安として使うこともできます。
ただし、自信を持って同じ間違いをくり返すこともあるので、これだけで正しさは保証できません。
自分の用途の評価セットを作る
業務で LLM を使うときは、公開のベンチマークより、自分のデータで作った評価セット が頼りになります。
| 手順 | 内容 |
|---|---|
| 1. 実際の入力を集める | 本番で来る質問・文書を、偏りなく集める。難しい例・境界の例・断るべき例も入れる |
| 2. 合格の基準を決める | 正解の文字列、守るべき条件(形式・長さ・言ってはいけないこと)、審査員の判定の基準を書く |
| 3. 数を確保する | 上の計算のとおり、数十問では数ポイントの差は見分けられない。比べたい差の大きさに合わせて問題を増やす |
| 4. 変更のたびに回す | プロンプト・モデル・RAG の設定を変えるたびに、同じ評価セットで測り、前の版と対にして比べる |
| 5. 本番で見直す | 本番で失敗した例を評価セットに足していく。評価セットそのものを古くしない |
LLM の評価の流れ
| 年 | 手法・出来事 | ポイント |
|---|---|---|
| 2002 | BLEU | 機械翻訳の訳文を、正解の訳との単語の並びの重なりで測る |
| 2004 | ROUGE | 要約を、正解の要約との重なりで測る |
| 2016 | SQuAD | 文章から答えを抜き出す問題。完全一致と単語の F1 で測る |
| 2018・2019 | GLUE・SuperGLUE | 複数の言語理解の問題をまとめたベンチマーク |
| 2020 | MMLU | 57 分野の 4 択問題。LLM の知識の比較の定番になる |
| 2021 | HumanEval と pass@k | コードをテストで確かめ、偏りのない pass@k の式を示した |
| 2022 | HELM | 正確さだけでなく、較正・頑健さ・公平さ・効率など多くの面から評価する |
| 2023 | LLM-as-a-judge・Chatbot Arena | LLM に採点させる方法のくせを分析し、人の投票による対の比較の順位を公開した |
| 2023 | SWE-bench | 実際のソフトウェアの課題を解決できるかで、エージェントを評価する |
| 2024 | 評価の統計の見直し(Miller) | 点数に誤差の区間を付け、対にして比べることを勧めた |
まとめ
- ベンチマークの点数は問題の数で決まる誤差を持つ。200 問では、3 ポイント弱いモデルが 5 回に 1 回くらい同点以上を取った。差は、同じ問題どうしの対で比べ、区間を付けて示す
- 完全一致は厳しすぎ、単語の F1 は間違った答えにも部分点を与える。答えを取り出して照合する工夫が要る。コードはテストで確かめ、pass@k は偏りのない式で計算する
- LLM の審査員には、位置・長さ・自分びいきなどのくせがある。順番を入れ替えて両方で聞けば、位置の偏りは打ち消せる。一部は人の判定と照らし合わせておく
- 公開のベンチマークは、学習データへの混入で点数が上がって見えることがある
- ハルシネーションは、答えを主張に分けて根拠と照らし合わせて評価する。業務で使うときは、自分のデータで評価セットを作り、変更のたびに同じセットで測る
参考
- K. Papineni et al., "BLEU: a Method for Automatic Evaluation of Machine Translation", ACL 2002
- C.-Y. Lin, "ROUGE: A Package for Automatic Evaluation of Summaries", ACL Workshop 2004
- P. Rajpurkar et al., "SQuAD: 100,000+ Questions for Machine Comprehension of Text", EMNLP 2016(arXiv:1606.05250)
- D. Hendrycks et al., "Measuring Massive Multitask Language Understanding", ICLR 2021(arXiv:2009.03300)
- M. Chen et al., "Evaluating Large Language Models Trained on Code", 2021(arXiv:2107.03374)
- P. Liang et al., "Holistic Evaluation of Language Models", TMLR 2023(arXiv:2211.09110)
- L. Zheng et al., "Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena", NeurIPS 2023 Datasets and Benchmarks Track(arXiv:2306.05685)
- S. Min et al., "FActScore: Fine-grained Atomic Evaluation of Factual Precision in Long Form Text Generation", EMNLP 2023(arXiv:2305.14251)
- P. Manakul, A. Liusie and M. J. F. Gales, "SelfCheckGPT: Zero-Resource Black-Box Hallucination Detection for Generative Large Language Models", EMNLP 2023(arXiv:2303.08896)
- C. E. Jimenez et al., "SWE-bench: Can Language Models Resolve Real-World GitHub Issues?", ICLR 2024(arXiv:2310.06770)
- E. Miller, "Adding Error Bars to Evals: A Statistical Approach to Language Model Evaluations", 2024(arXiv:2411.00640)
- Y. Dubois et al., "Length-Controlled AlpacaEval: A Simple Way to Debias Automatic Evaluators", 2024(arXiv:2404.04475)