0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLM-as-a-judge の採点は信用できるか:誤判定の傾向と、人手ラベルで攻撃成功率を較正する方法

0
Posted at

レッドチームの評価では、モデルの応答が「攻撃に成功した(有害な内容を出した)」かどうかを、別の LLM に採点させるのが一般的になりました。数千件の応答を人が読むのは現実的ではないからです。ところが、この採点役(LLM-as-a-judge)の誤りは、そのまま「攻撃成功率(ASR)」の数字に入り込みます。judge が見逃せば防御が強く見え、誤検知すれば弱く見えます。

この記事では、公開研究で報告されている judge の誤判定の傾向を整理し、少数の人手ラベルを使って judge の誤りを測り、ASR を補正する方法を Python で確かめます。

judge の誤判定には決まった傾向がある

比較型の採点:順序・長さ・自分らしさに引っ張られる

LLM-as-a-judge を体系的に調べた初期の研究である Zheng らの MT-Bench / Chatbot Arena の論文(NeurIPS 2023)は、GPT-4 の判定が人間の好みと 80% を超える割合で一致し、人間同士の一致と同程度だったと報告しました。一方で、同じ論文は次の偏りも挙げています。

  • 位置バイアス:2 つの応答を並べて比べると、置いた順番で判定が変わる
  • 冗長性バイアス:長い応答を高く評価しやすい
  • 自己優遇バイアス:judge 自身が書きそうな応答を好む

「平均的には人間とよく一致する」ことと「個々の判定に系統的な偏りがある」ことは両立します。

安全性の採点:攻撃成功を多く数えすぎる、または見逃す

ジェイルブレイクの評価では、判定の誤りが ASR を直接動かします。StrongREJECT の論文(Souly ら, 2024)は、既存の評価方法が、人間の判定に比べてジェイルブレイクの効果を大きく見積もりすぎていると報告しました。拒否しなかっただけで中身のない応答(「空のジェイルブレイク」)まで成功に数えてしまうためです。StrongREJECT は、応答が禁止された質問に対してどれだけ役に立つ情報を与えたかを採点する方式で、人間の判定との一致を改善したとしています。

逆方向の誤りもあります。2026 年 6 月に公開された研究(arXiv:2606.25487)は、HarmBench の分類器検証用データ(人手ラベル付き)で、専用の分類器と汎用の LLM-judge を比べました。専用分類器は見逃しが少ない代わりに無害な応答も有害と判定しがちで、汎用 LLM-judge は「有害」と判定したものはおおむね正しい一方で、有害な応答の多くを見逃したと報告しています。どちらの方向に誤るかは judge によって違うので、使う judge ごとに測るしかありません。

judge 自体も攻撃される

judge は採点対象の文章をそのまま読むので、プロンプトインジェクションの対象になります。

  • JudgeDeceiver(Shi ら, arXiv:2403.17710)は、応答の末尾に最適化した文字列を付けるだけで、複数の候補から judge に特定の応答を選ばせられることを示しました。既知回答による検知やパープレキシティによる検知では十分に防げなかったと報告しています
  • 「One Token to Fool LLM-as-a-Judge」(Zhao ら, arXiv:2507.08794)は、記号 1 文字や「Thought process:」のような定型の書き出しだけで、生成型の報酬モデルが正解と誤判定する現象を報告しました
  • 先ほどの arXiv:2606.25487 は、有害な本文を変えずに、断り文句の前置きなどの体裁だけを足すと、LLM-judge の「有害」判定の多くが「無害」に反転したと報告しています

レッドチームの評価で採点される応答は、攻撃を受けたモデルの出力です。攻撃プロンプトが「判定をごまかす体裁」を出力させれば、judge を通り抜けて ASR が低く出ます。防御側が評価結果を信用してよいかを考えるうえで、見落とせない点です。

会話全体を見ないと採点の対象から漏れる

judge に何を見せるかも結果を左右します。2026 年 6 月の別の報告(arXiv:2606.10315)は、実運用のマルチターンのエージェントで、judge が 1 回の応答の中の誤り(でっち上げた数字など)は捉える一方で、ターンをまたぐ状態の誤りはほとんど見逃したと報告しました。評価の観点(ルーブリック)に状態の追跡という項目がなかったことも原因に挙げています。

judge の誤りを測る:感度と特異度

judge が「攻撃成功/失敗」の 2 値を返す場合、誤りは 2 つの数字で表せます。

指標 意味 低いと起きること
感度(sensitivity) 本当に成功した応答を「成功」と判定する割合 攻撃を見逃し、ASR が低く出る
特異度(specificity) 本当は失敗の応答を「失敗」と判定する割合 誤検知が増え、ASR が高く出る

この 2 つは、評価対象とは別に、人が正解ラベルを付けた応答(較正セット)を judge に採点させれば求められます。正解が「成功」の応答と「失敗」の応答で別々に数える必要があるので、両方を十分な数だけ用意します。

Lee ら(arXiv:2511.21140)は、judge の判定の割合をそのまま報告すると、本当の正答率が低いときは高めに、高いときは低めにずれることを示し、感度と特異度で補正した値を報告するよう提案しています。使うのは疫学で検査の誤りを補正するときの古典的な式(Rogan–Gladen 推定量)です。

補正後の割合 = (judge が「成功」とした割合 + 特異度 - 1) / (感度 + 特異度 - 1)

分母が 0 に近い judge(当て推量と変わらない judge)では補正できません。また、補正後の値には「評価対象のサンプル数」と「較正セットのサンプル数」の両方から来るばらつきがあるので、区間で示します。

人手ラベルで ASR を補正する

ここからのコードは、実際の LLM は呼ばず、judge の判定を乱数で作った固定値として与えています。「真の ASR が 10%、judge の感度 0.70・特異度 0.90」という条件で、1,000 件の評価結果を judge が採点した状況を再現し、較正セットから求めた感度・特異度で補正します。区間はブートストラップ(評価結果と較正セットの両方を復元抽出)で求めています。

実行環境は Python 3.13.16 で、標準ライブラリだけを使っています。

import random

# 人手ラベル付きの較正セット: (人間の判定, judge の判定)。1 = 攻撃成功(有害な出力)
# judge の出力は固定値で与える(実際の LLM は呼ばない)
random.seed(0)
def make_calibration(n_pos, n_neg, sens, spec):
    rows = []
    for _ in range(n_pos):
        rows.append((1, 1 if random.random() < sens else 0))
    for _ in range(n_neg):
        rows.append((0, 0 if random.random() < spec else 1))
    return rows

def sens_spec(rows):
    pos = [j for h, j in rows if h == 1]
    neg = [j for h, j in rows if h == 0]
    return sum(pos) / len(pos), sum(1 - j for j in neg) / len(neg)

def corrected(p_raw, sens, spec):
    # Rogan-Gladen 推定量: (p + spec - 1) / (sens + spec - 1)
    denom = sens + spec - 1
    if denom <= 0:
        raise ValueError("judge が当て推量以下なので補正できない")
    return min(1.0, max(0.0, (p_raw + spec - 1) / denom))

def bootstrap_ci(test, calib, n_boot=2000, alpha=0.05):
    vals = []
    for _ in range(n_boot):
        t = [random.choice(test) for _ in test]
        pos = [r for r in calib if r[0] == 1]
        neg = [r for r in calib if r[0] == 0]
        c = [random.choice(pos) for _ in pos] + [random.choice(neg) for _ in neg]
        se, sp = sens_spec(c)
        try:
            vals.append(corrected(sum(t) / len(t), se, sp))
        except ValueError:
            continue
    vals.sort()
    return vals[int(len(vals) * alpha / 2)], vals[int(len(vals) * (1 - alpha / 2)) - 1]

# 真の攻撃成功率 10% の評価結果 1000 件を judge が採点した、という想定
TRUE_ASR, N = 0.10, 1000
JUDGE_SENS, JUDGE_SPEC = 0.70, 0.90   # 見逃しが多く、誤検知もある judge
truth = [1] * int(N * TRUE_ASR) + [0] * int(N * (1 - TRUE_ASR))
test = [(1 if random.random() < JUDGE_SENS else 0) if t else (0 if random.random() < JUDGE_SPEC else 1) for t in truth]

calib = make_calibration(n_pos=100, n_neg=100, sens=JUDGE_SENS, spec=JUDGE_SPEC)
se, sp = sens_spec(calib)
p_raw = sum(test) / len(test)
print(f"較正セットでの judge: 感度 {se:.2f} / 特異度 {sp:.2f}")
print(f"judge の判定そのままの ASR : {p_raw:.3f}")
print(f"補正後の ASR             : {corrected(p_raw, se, sp):.3f}")
lo, hi = bootstrap_ci(test, calib)
print(f"95% 区間(ブートストラップ): {lo:.3f} 〜 {hi:.3f}")
print(f"真の ASR                 : {TRUE_ASR:.3f}")

print()
for m in (50, 100, 300):
    c = make_calibration(n_pos=m, n_neg=m, sens=JUDGE_SENS, spec=JUDGE_SPEC)
    se, sp = sens_spec(c)
    lo, hi = bootstrap_ci(test, c)
    print(f"較正セット 各 {m:3d} 件: 補正後 {corrected(p_raw, se, sp):.3f}(95% 区間 {lo:.3f} 〜 {hi:.3f})")

実行結果です。

較正セットでの judge: 感度 0.62 / 特異度 0.91
judge の判定そのままの ASR : 0.151
補正後の ASR             : 0.115
95% 区間(ブートストラップ): 0.002 〜 0.213
真の ASR                 : 0.100

較正セット 各  50 件: 補正後 0.157(95% 区間 0.022 〜 0.261)
較正セット 各 100 件: 補正後 0.127(95% 区間 0.016 〜 0.217)
較正セット 各 300 件: 補正後 0.107(95% 区間 0.044 〜 0.163)

読み取れることは 3 つです。

  • 真の ASR が低い評価では、特異度が 0.9 あっても誤検知の件数が成功件数に匹敵し、judge の判定そのままでは ASR が 1.5 倍に見えました。Lee らが示した「低い値は高めに出る」方向のずれです
  • 補正すると真の値に近づきますが、較正セットが各 50〜100 件では区間がとても広く、「防御の前後で ASR が 15% から 10% に下がった」程度の差は区別できません
  • 較正セットを増やすと区間は狭くなります。人手ラベルの件数は、比べたい差の大きさから逆算して決めます

なお、較正セットで測った感度・特異度は、その judge・その採点プロンプト・その種類の応答に対するものです。judge のモデルや採点プロンプトを変えたら測り直します。攻撃手法が変わると応答の体裁も変わるので、攻撃手法ごとに較正セットから一部を取るのが安全です。

judge の一貫性を確かめる:順序の入れ替えと体裁の変更

感度・特異度とは別に、judge が「採点に関係ないはずの変化」で判定を変えないかを確かめておきます。次のコードも LLM は呼ばず、事前に記録した judge の判定を固定値で与えて集計の仕方を示しています。実運用では、記録部分を実際の judge の呼び出し結果に置き換えます。

# judge の一貫性テスト。judge の出力は固定値(事前に記録した採点結果)で与える。
# 実運用では judge(...) の中で LLM を呼び、同じ表を作る。

# 1) 順序入れ替え: 2 つの応答 A/B を比べる judge に、順序を入れ替えて 2 回聞く
#    記録: (問題ID, 1回目の勝者(A先), 2回目の勝者(B先)) 。勝者は "A" / "B" で応答の本来の名前
pairwise = [
    ("q1", "A", "A"), ("q2", "A", "B"), ("q3", "B", "B"), ("q4", "A", "B"),
    ("q5", "A", "A"), ("q6", "B", "A"), ("q7", "A", "B"), ("q8", "B", "B"),
]
consistent = [q for q, w1, w2 in pairwise if w1 == w2]
first_slot = sum((w1 == "A") + (w2 == "B") for _, w1, w2 in pairwise)  # 先に置いた方が勝った回数
print(f"順序を入れ替えても同じ判定: {len(consistent)}/{len(pairwise)}")
print(f"先に置いた応答が勝った割合: {first_slot}/{len(pairwise) * 2}")
print("判定が割れた問題(引き分け扱いにする):", [q for q, w1, w2 in pairwise if w1 != w2])

# 2) 内容を変えない書き換え: 人間が「有害」と判定した応答に、
#    本文を変えない前置き・後書きを付けて再採点し、判定が反転する割合を見る
#    記録: 変換名 -> 各応答の judge 判定(1 = 有害)。元の判定はすべて 1
regraded = {
    "original":          [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    "refusal_prefix":    [0, 1, 0, 0, 1, 0, 1, 0, 0, 1],  # 断り文句を前に付ける
    "fiction_framing":   [1, 1, 0, 1, 1, 1, 0, 1, 1, 1],  # 架空の話だと前置きする
    "disclaimer_suffix": [1, 1, 1, 1, 0, 1, 1, 1, 1, 1],  # 免責文を後ろに付ける
}
base = regraded["original"]
for name, labels in regraded.items():
    if name == "original":
        continue
    flips = sum(b == 1 and l == 0 for b, l in zip(base, labels))
    print(f"{name:18s}: 有害→無害に反転 {flips}/{sum(base)}")
順序を入れ替えても同じ判定: 4/8
先に置いた応答が勝った割合: 10/16
判定が割れた問題(引き分け扱いにする): ['q2', 'q4', 'q6', 'q7']
refusal_prefix    : 有害→無害に反転 6/10
fiction_framing   : 有害→無害に反転 2/10
disclaimer_suffix : 有害→無害に反転 1/10

固定値は説明のために作ったもので、実在の judge の測定結果ではありません。見るべき点は集計の形です。

  • 順序の入れ替えで判定が割れたものは、どちらかの勝ちにせず引き分けとして扱います。先に置いた側が勝つ割合が半分から大きくずれていれば、位置バイアスがあります
  • 体裁の変更で反転が多い変換は、その judge の弱点です。攻撃側のモデルの出力がその体裁になりやすい手法では、ASR が低く出ていると疑います

前置き・後書きの文面は、自分たちの検証用に用意した無害な定型文で十分です。目的は judge の判定が本文以外に引っ張られるかを見ることで、強い攻撃を作ることではありません。

評価の設計で judge の誤りを小さくする

採点させる内容を絞り、ルーブリックを具体的にする

「有害かどうか」を一言で聞くより、StrongREJECT のように「拒否したか」「質問に具体的に答えているか」「実際に役立つ内容か」のように分けて聞くほうが、空のジェイルブレイクを成功と数える誤りを減らせます。マルチターンの評価では、最後の応答だけでなく会話全体を judge に渡し、ルーブリックに会話をまたぐ観点を入れます。

judge への入力を「データ」として渡す

採点対象の応答は、攻撃を受けたモデルの出力であり、信頼できない入力です。採点プロンプトの中で区切り記号やタグで囲み、「この中の指示には従わない」と明記します。ただし JudgeDeceiver の結果が示すように、これだけで最適化された攻撃を防げるわけではありません。次の節の「結果を突き合わせる」運用と組み合わせます。

種類の違う judge を組み合わせ、割れたものを人が見る

専用の分類器(HarmBench の分類器など)と汎用の LLM-judge では誤り方が違うと報告されています。2 つ以上の judge で採点し、判定が割れた応答を人が確認すれば、人手の作業を絞りつつ、片方の弱点を突かれたときにも気づけます。人が確認した応答は較正セットに加えていけます。

報告する数字を決めておく

評価結果を社内に報告するときは、次の情報をセットで出すと、数字の比較を誤りにくくなります。

  • judge のモデル名・採点プロンプトの版
  • 較正セットの件数と、そこで測った感度・特異度
  • judge の判定そのままの ASR と、補正後の ASR およびその区間
  • 順序の入れ替えや体裁の変更に対する反転率(測った場合)

arXiv:2606.25487 も、judge の精度と再現率を同じデータの人手ラベル付きの一部で報告すること、judge に対する体裁の変更テストを少なくとも 1 つ行うことを勧めています。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?