第五篇算出每一題的 Somers' D,麻煩也跟著來了。開發集裡可能有 100 題,就會得到 100 個方向和強度都不同的 D。有人強烈正向,有人接近 0,也可能夾著幾題負值。
直接取平均很方便,卻漏掉一件事:每題實際做過的正誤比較數不一樣。
一題有 8 條正確、8 條錯誤軌跡,可以形成 64 組正誤配對。另一題有 15 條正確、1 條錯誤,只能形成 15 組。兩題都能算 D,但前者提供的題內比較多得多。
TACT 把每題的正誤配對數記成:
N_q = n_q^1 × n_q^0
n¹ 是正確軌跡數,n⁰ 是錯誤軌跡數。跨題合併時,使用 van Elteren 式的分層權重:
這裡仍然保留題目邊界。每題先算自己的 D,再把結果合起來;不會把所有軌跡倒進同一池,重演第五篇談過的難度混淆。
假設開發集目前只有三題:
題目 D_q 正確×錯誤 N_q
q1 0.8125 8×8 64
q2 -0.2000 15×1 15
q3 0.3750 4×4 16
不加權平均是 0.329。配對數加權後則是:
(64×0.8125 + 15×(-0.2) + 16×0.375) / 95
= 55 / 95
= 0.579
q2 的負值沒有被刪掉,只是它能提供的正誤比較比較少。D̂=0.579 表示整體信心通道偏正向,還不能直接宣布「證據夠強」。下一個問題是這個數字會晃多大。
若信心與正確性無關,D 的期望值是 0。即使如此,有限樣本還是會跑出正值或負值。要判斷 0.579 究竟明不明顯,得先知道虛無假設下的變異數。
Mann–Whitney U 的一般公式假設沒有信心平手。LLM 信心常被四捨五入成兩位小數,平手並不少見;照抄無平手公式會把實際的排名變化算錯。
假設同一題裡,每個平手群組的大小是 t₁, t₂, ...,修正係數為:
沒有平手時,每個 t 都是 1,修正係數等於 1。若四條軌跡的平手群組大小為 [1, 2, 1],係數是 0.9。全部信心相同時,係數降到 0,因為排名根本不會變。
前一篇用 D=2AUC-1,所以還要把 U 的變異數換到 D 的尺度:
下面的函式把 Day 5 的 D 和今天需要的配對數、平手修正變異數包在一起:
from dataclasses import dataclass
import numpy as np
from scipy.stats import rankdata
@dataclass(frozen=True)
class ItemStat:
d: float
n_pairs: float
var0_d: float
def item_stat(
confidence: list[float] | np.ndarray,
correct: list[int] | np.ndarray,
) -> ItemStat | None:
c = np.asarray(confidence, dtype=float)
y = np.asarray(correct, dtype=int)
if c.ndim != 1 or c.shape != y.shape:
raise ValueError("confidence and correct must have the same shape")
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
negative = y == 0
n1 = int(positive.sum())
n0 = int(negative.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)
d = 2.0 * auc - 1.0
_, tie_counts = np.unique(c, return_counts=True)
m = c.size
tie_term = float(np.sum(tie_counts ** 3 - tie_counts))
correction = 1.0 - tie_term / max(m ** 3 - m, 1)
var0_u = n1 * n0 * (m + 1) / 12.0 * correction
var0_d = 4.0 * var0_u / (n1 * n0) ** 2
return ItemStat(
d=d,
n_pairs=float(n1 * n0),
var0_d=var0_d,
)
這份程式故意讓全對、全錯題回傳 None,理由和 Day 5 一樣:沒有正誤配對,就沒有題內辨別度可估。
若各題在虛無假設下彼此獨立,配對數加權平均的標準誤可以寫成:
Python 只需要幾行:
@dataclass(frozen=True)
class PooledD:
d_hat: float
se0: float
n_pairs_total: float
n_items: int
def pool_item_stats(stats: list[ItemStat]) -> PooledD | None:
if not stats:
return None
n = np.array([s.n_pairs for s in stats], dtype=float)
d = np.array([s.d for s in stats], dtype=float)
var0 = np.array([s.var0_d for s in stats], dtype=float)
total = float(n.sum())
d_hat = float(np.sum(n * d) / total)
se0 = float(np.sqrt(np.sum(n ** 2 * var0)) / total)
return PooledD(
d_hat=d_hat,
se0=se0,
n_pairs_total=total,
n_items=len(stats),
)
到這裡,D̂ 回答整體方向,SE₀ 描述 D=0 且題目同質時的抽樣波動。兩個數字放在一起,已經比「平均 D 是正的」可靠不少。

圖:固定 K 的分組實驗。資料帶有可觀測群組結構時,全域 D 可能把方向相反的子群抵銷;分組估計能保留差異。若資料真的是 i.i.d.,各種非 oracle 方法則回到同一個 SC 基準。
如果一半題目強烈正向,另一半強烈負向,兩邊可能互相抵消成接近 0;也可能平均仍是正的,但不同題型的方向差很多。SE₀ 只看每題內的排名虛無變異,沒有完整反映這種題目間異質性。
另一個邊界情況也很危險:大量信心平手會把平手修正變異數壓得很低。若程式只相信 SE₀,分母可能小到製造出誇張的 z 值。
Day 7 會補上兩層保險:逐題刪除的 jackknife 標準誤,以及 1/(2√N) 的最低標準誤。TACT 最後取三者最大值,不讓任何一個過度樂觀的估計單獨決定 γ。
論文預印本:TACT: Trust-Anchored Confidence Tempering for Self-Consistency Voting in Large Language Models