iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0

第五篇算出每一題的 Somers' D,麻煩也跟著來了。開發集裡可能有 100 題,就會得到 100 個方向和強度都不同的 D。有人強烈正向,有人接近 0,也可能夾著幾題負值。

直接取平均很方便,卻漏掉一件事:每題實際做過的正誤比較數不一樣。

同樣一個 D,背後的比較數可能差很多

一題有 8 條正確、8 條錯誤軌跡,可以形成 64 組正誤配對。另一題有 15 條正確、1 條錯誤,只能形成 15 組。兩題都能算 D,但前者提供的題內比較多得多。

TACT 把每題的正誤配對數記成:

N_q = n_q^1 × n_q^0

是正確軌跡數,n⁰ 是錯誤軌跡數。跨題合併時,使用 van Elteren 式的分層權重:

van Elteren 式 Somers D 合併

這裡仍然保留題目邊界。每題先算自己的 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₂, ...,修正係數為:

Mann Whitney U 平手修正

沒有平手時,每個 t 都是 1,修正係數等於 1。若四條軌跡的平手群組大小為 [1, 2, 1],係數是 0.9。全部信心相同時,係數降到 0,因為排名根本不會變。

前一篇用 D=2AUC-1,所以還要把 U 的變異數換到 D 的尺度:

Somers 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 一樣:沒有正誤配對,就沒有題內辨別度可估。

合併 D 與虛無標準誤

若各題在虛無假設下彼此獨立,配對數加權平均的標準誤可以寫成:

合併 Somers D 的虛無標準誤

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),
    )

到這裡, 回答整體方向,SE₀ 描述 D=0 且題目同質時的抽樣波動。兩個數字放在一起,已經比「平均 D 是正的」可靠不少。

全域與分組 TACT 的固定預算比較

圖:固定 K 的分組實驗。資料帶有可觀測群組結構時,全域 D 可能把方向相反的子群抵銷;分組估計能保留差異。若資料真的是 i.i.d.,各種非 oracle 方法則回到同一個 SC 基準。

SE₀ 還沒看到題目之間的衝突

如果一半題目強烈正向,另一半強烈負向,兩邊可能互相抵消成接近 0;也可能平均仍是正的,但不同題型的方向差很多。SE₀ 只看每題內的排名虛無變異,沒有完整反映這種題目間異質性。

另一個邊界情況也很危險:大量信心平手會把平手修正變異數壓得很低。若程式只相信 SE₀,分母可能小到製造出誇張的 z 值。

Day 7 會補上兩層保險:逐題刪除的 jackknife 標準誤,以及 1/(2√N) 的最低標準誤。TACT 最後取三者最大值,不讓任何一個過度樂觀的估計單獨決定 γ。

論文預印本:TACT: Trust-Anchored Confidence Tempering for Self-Consistency Voting in Large Language Models


上一篇
Day 5|高信心真的比較常答對嗎?用 AUC 把方向量出來
系列文
從多數決到可信投票:30 天打造 TACT 大語言模型推理聚合方法6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言