第四篇把原始信心換成題內排名,總算解決了尺度亂飄的問題。可惜排名只告訴我誰比較有把握,還沒回答最關鍵的一句:有把握的軌跡,真的比較常答對嗎?
今天會用有答案的開發集量這件事。測試題不需要答案標籤,先別被公式裡的 y 嚇到。
從同一題隨機抽一條正確軌跡和一條錯誤軌跡。若正確軌跡的信心較高,記 1 分;信心相同,兩邊各算一半;錯誤軌跡較高則記 0 分。把所有正誤配對平均起來,就是這一題的 AUC:
這裡的 AUC 不是整份測試集的答案準確率,也不是「模型有 80% 機率答對」。它只看同一題裡,信心能不能把正確軌跡排在錯誤軌跡前面。
AUC=1 代表每一條正確軌跡都排在錯誤軌跡上方;AUC=0.5 表示信心沒有辨別方向;AUC<0.5 就有意思了,錯誤軌跡反而比較有自信。
TACT 不直接使用 0 到 1 的 AUC,而是轉成 Somers' D:
這個轉換很單純,0.5 被移到 0,正向與反向的距離也變得對稱:
AUC = 1.00 → D = 1.00
AUC = 0.75 → D = 0.50
AUC = 0.50 → D = 0.00
AUC = 0.25 → D = -0.50
AUC = 0.00 → D = -1.00
第三篇看到的「高信心錯誤搶走票數」,在這裡就會留下負號。後面推導 γ 時,這個符號會決定票重要順著信心走,還是反過來走。
逐一跑正誤配對很容易懂,軌跡多時卻有點浪費。Mann–Whitney U 可以直接用第四篇算過的 midrank 得到同一個答案:
n¹ 是正確軌跡數,n⁰ 是錯誤軌跡數。Python 實作如下:
from dataclasses import dataclass
import numpy as np
from scipy.stats import rankdata
@dataclass(frozen=True)
class ItemDiscrimination:
auc: float
d: float
n_pairs: int
def item_discrimination(
confidence: list[float] | np.ndarray,
correct: list[int] | np.ndarray,
) -> ItemDiscrimination | None:
c = np.asarray(confidence, dtype=float)
y = np.asarray(correct, dtype=int)
if c.ndim != 1 or c.size == 0 or c.shape != y.shape:
raise ValueError("confidence and correct must be equal-length vectors")
if not np.all(np.isfinite(c)):
raise ValueError("confidence must be finite")
if not np.all(np.isin(y, [0, 1])):
raise ValueError("correct must contain only 0 and 1")
positive = y == 1
n1 = int(positive.sum())
n0 = int((~positive).sum())
if n1 == 0 or n0 == 0:
return None
ranks = rankdata(c, method="average")
u = float(ranks[positive].sum() - n1 * (n1 + 1) / 2)
auc = u / (n1 * n0)
return ItemDiscrimination(
auc=auc,
d=2.0 * auc - 1.0,
n_pairs=n1 * n0,
)
用三個小例子檢查正向、反向和信心平手:
examples = [
([0.80, 0.70, 0.60, 0.55], [1, 1, 0, 0]),
([0.55, 0.60, 0.80, 0.70], [1, 1, 0, 0]),
([0.80, 0.50, 0.50, 0.40], [1, 1, 0, 0]),
]
for confidence, correct in examples:
print(item_discrimination(confidence, correct))
輸出為:
ItemDiscrimination(auc=1.0, d= 1.0, n_pairs=4)
ItemDiscrimination(auc=0.0, d=-1.0, n_pairs=4)
ItemDiscrimination(auc=0.875, d= 0.75, n_pairs=4)
第三組有一組正誤軌跡同為 0.50,midrank 會自動給那個配對半分,因此 AUC 是 0.875。
一題若 16 條軌跡全部答對,就找不到錯誤軌跡配對;全部答錯也一樣。程式在這兩種情況回傳 None,不硬塞 0。
回傳 0 會暗示「信心沒有方向」,但眼前其實沒有做比較的條件。這些題目對模型整體準確率仍然有用,只是不能拿來估題內辨別度。TACT 會在開發集估 D 時略過它們,測試時照常投票。
n_pairs=n¹×n⁰ 也值得保存。一題有 8 條正確、8 條錯誤時,共有 64 組比較;15 對 1 只有 15 組。兩題都能算 D,資訊量差很多。下一篇合併多題結果時,這個數字會變成權重。
想像兩道題:簡單題的軌跡全部答對,信心落在 0.90 左右;困難題全部答錯,信心只有 0.30。若忽略題目邊界,把所有軌跡混著算,AUC 會等於 1,看起來像完美的信心通道。
實際上,任何一題內都沒有正誤軌跡可以比較。那個漂亮的 AUC 只量到「簡單題信心高、困難題信心低」,無法證明信心能改善同一題的投票。TACT 堅持在題內算 D,就是為了擋掉這種難度混淆。

圖:以 κ_c 從負值掃到正值的合成測試。κ_c 小於零代表高信心反而更常出錯;AUC 方向閘門能辨識正負方向,單向 CISC 則會在反向訊號區快速失準。
correct 需要標準答案,所以今天寫的是 TACT-dev 的估計步驟。開發集會估出一個全域信任方向,測試集只要答案、信心和這個已經決定的 γ,不會偷看測試答案。
目前還缺兩件事:每題的 D 會晃,多題合併後也要知道估計有多不確定。第六篇會用正誤配對數做 van Elteren 式合併,再處理平手修正與標準誤。只有方向還不夠穩時,γ 才有理由停在 0。
論文預印本:TACT: Trust-Anchored Confidence Tempering for Self-Consistency Voting in Large Language Models