New evalharness/fingerprint/ package (from evalstone fp_fusion v1.1, 2026-09-07 pruning final): probe battery -> concurrent collection -> five scoring views (verify/attribution/variant/adversarial/robustness), bundled family aliases + 27 reference fingerprints (12 fp_fusion schema). - CLI: 'evalharness fingerprint run ...' (REMAINDER passthrough, single source of arg definitions) + 'fingerprint list' for bundled references - imports rewritten package-relative; direct 'python3 run_fp_fusion.py' execution kept working via package bootstrap - offline analysis/collection scripts made path-independent (previously pinned to a /opt/evalscope path absent on this host) - shell scripts: hardcoded API key -> FP_API_KEY/OPENAI_API_KEY env vars - --reference accepts short names resolved against bundled references/ - pyproject: +httpx dependency, package-data references/*.json - tests/test_fingerprint.py: 10 offline tests (battery definitions, assembly counts, normalization, signals, verdict ladder, CLI wiring) - README: fingerprint section + architecture entry Verified on H20-1: tests 10/10, installed CLI OK, full-protocol run vs vectron GLM-5.3 reproduces baseline (score 0.9451, s_idn 0.846).
159 lines
6.1 KiB
Python
159 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
||
"""剪枝候选集验证(纯离线,零 API):
|
||
|
||
确定性检查之外,加 bootstrap 重跑仿真:对每个模型的 cell 答案做有放回重采样,
|
||
模拟"明天再跑一遍电池",统计 top-1 归因成功率。剪枝集与全量集成功率持平才算安全。
|
||
|
||
集合定义(依据 cell_snr.py 排行 + 弱对载荷分析):
|
||
tier1 死重: binary-*-zh ×4(9 模型全 0 valid,纯浪费 100 请求/跑)+ favorite-number:en(5/9 模型不可用)
|
||
tier2 零信号: coin-flip:en/zh、random-number-1-10:en/zh、binary-season:en
|
||
(跨模型信号 ≤0.053,全体模型收敛到同一分布,构造性无区分力)
|
||
tier3 谨慎: day-of-week:zh(不在任何弱对 top8,移除 Δ+0.001)
|
||
推荐15 = 弱对 top8 并集(11) + SNR≥2 补充(4)
|
||
"""
|
||
import json
|
||
import random
|
||
import sys
|
||
from collections import Counter, defaultdict
|
||
|
||
from pathlib import Path # noqa: E402
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[2])) # 仓库根/site-packages, 使 evalharness 包可导入
|
||
from evalharness.fingerprint.engine import (build_d_normalized, distributions_by_cell, # noqa: E402
|
||
jsd_bits, load_reference)
|
||
|
||
BFD = "/tmp/bfd"
|
||
R = str(Path(__file__).resolve().parent / 'references')
|
||
|
||
MODELS = [
|
||
("deepseek_v4_flash", "deepseek_v4_flash_fusion_reference.json", "DS"),
|
||
("deepseek_v4_flash_0731", "deepseek_v4_flash_0731_fusion_reference.json", "DS"),
|
||
("deepseek_v4_pro", "deepseek_v4_pro_fusion_reference.json", "DS"),
|
||
("glm_51", "glm_51_fusion_reference.json", "GLM"),
|
||
("glm_52", "glm52_vectron_fusion_reference.json", "GLM"),
|
||
("glm_53", "glm53_fusion_reference.json", "GLM"),
|
||
("kimi_k2_6", "kimi_k2_6_fusion_reference.json", "Kimi"),
|
||
("kimi_k2_7code", "kimi_k2_7code_fusion_reference.json", "Kimi"),
|
||
("kimi_k3", "kimi_k3_fusion_reference.json", "Kimi"),
|
||
]
|
||
NAMES = [m[0] for m in MODELS]
|
||
FAMILY = {m[0]: m[2] for m in MODELS}
|
||
|
||
samples = {}
|
||
for n, _, _ in MODELS:
|
||
with open(f"{BFD}/{n}/raw_answers.jsonl") as f:
|
||
samples[n] = build_d_normalized([json.loads(l) for l in f])
|
||
dists = {n: distributions_by_cell(s) for n, s in samples.items()}
|
||
refs = {n: load_reference(f"{R}/{rf}")["cells"] for n, rf, _ in MODELS}
|
||
|
||
ALL = sorted(set().union(*[set(r) for r in refs.values()])
|
||
| set().union(*[set(d) for d in dists.values()]))
|
||
|
||
ANS = {}
|
||
for n in NAMES:
|
||
per = defaultdict(list)
|
||
for s in samples[n]:
|
||
if s["cat"] == "valid" and s["norm"] is not None:
|
||
per[s["cell"]].append(s["norm"])
|
||
for c in ALL:
|
||
ANS[(n, c)] = per.get(c, [])
|
||
|
||
|
||
def score(dist_m, ref_cells, cells):
|
||
js = []
|
||
for c in cells:
|
||
da, db = dist_m.get(c), ref_cells.get(c)
|
||
if not da or not db:
|
||
continue
|
||
if sum(da.values()) < 10 or sum(db.values()) < 10:
|
||
continue
|
||
js.append(jsd_bits(da, db))
|
||
return sum(js) / len(js) if js else None
|
||
|
||
|
||
def state(cells, D=None):
|
||
D = dists if D is None else D
|
||
res = {}
|
||
for m in NAMES:
|
||
vals = {r: score(D[m], refs[r], cells) for r in NAMES}
|
||
vals = {r: v for r, v in vals.items() if v is not None}
|
||
if not vals:
|
||
res[m] = (None, False, False, None)
|
||
continue
|
||
best = min(vals, key=vals.get)
|
||
own = vals.get(m)
|
||
sib = [v for r, v in vals.items() if FAMILY[r] == FAMILY[m] and r != m]
|
||
mg = (min(sib) - own) if (sib and own is not None) else None
|
||
res[m] = (best, best == m, FAMILY[best] == FAMILY[m], mg)
|
||
exact = sum(1 for m in NAMES if res[m][1])
|
||
margins = [(res[m][3], m) for m in NAMES if res[m][3] is not None]
|
||
return res, exact, (min(margins) if margins else None)
|
||
|
||
|
||
KEEP15 = [
|
||
"random-animal:en", "random-animal:zh", "random-city:en", "random-city:zh",
|
||
"random-color:en", "random-color:zh", "random-letter:en", "random-letter:zh",
|
||
"random-number-1-100:en", "random-number-1-100:zh", "favorite-number:zh",
|
||
"day-of-week:en", "binary-pet:en", "binary-tea-coffee:en",
|
||
"binary-sea-mountain:en",
|
||
]
|
||
TIER12_16 = KEEP15 + ["day-of-week:zh"]
|
||
MINI3 = ["day-of-week:en", "binary-pet:en", "binary-tea-coffee:en"]
|
||
|
||
SETS = [
|
||
("全量 26 cell(现状)", ALL),
|
||
("推荐 15 cell", KEEP15),
|
||
("仅 tier1+2 剪(16 cell)", TIER12_16),
|
||
("家族分诊 mini 3 cell", MINI3),
|
||
]
|
||
|
||
out = []
|
||
def log(s=""):
|
||
print(s)
|
||
out.append(s)
|
||
|
||
B = 40
|
||
for name, cells in SETS:
|
||
res, exact, weak = state(cells)
|
||
d_req = len(cells) * 25
|
||
k3_min = (65 - 4) * d_req / 650 + 4
|
||
log(f"━━ {name} D请求 {d_req}({d_req / 691 * 100:.0f}% 原量) K3 verify 约 {k3_min:.0f} 分钟")
|
||
log(f" 确定性: 精确 top-1 {exact}/9 最弱间距 {weak[1]} {weak[0]:+.3f}")
|
||
rng = random.Random(7)
|
||
ex = {m: 0 for m in NAMES}
|
||
fam = {m: 0 for m in NAMES}
|
||
confusions = Counter()
|
||
for _ in range(B):
|
||
Dboot = {}
|
||
for m in NAMES:
|
||
d = {}
|
||
for c in cells:
|
||
ans = ANS[(m, c)]
|
||
if len(ans) >= 10:
|
||
d[c] = dict(Counter(rng.choices(ans, k=len(ans))))
|
||
Dboot[m] = d
|
||
for m in NAMES:
|
||
vals = {r: score(Dboot[m], refs[r], cells) for r in NAMES}
|
||
vals = {r: v for r, v in vals.items() if v is not None}
|
||
if not vals:
|
||
continue
|
||
best = min(vals, key=vals.get)
|
||
ex[m] += best == m
|
||
fam[m] += FAMILY[best] == FAMILY[m]
|
||
if best != m:
|
||
confusions[(m, best)] += 1
|
||
tot_ex = sum(ex.values()) / (B * len(NAMES)) * 100
|
||
tot_fam = sum(fam.values()) / (B * len(NAMES)) * 100
|
||
log(f" bootstrap 重跑仿真({B}次): 精确 {tot_ex:.0f}% 家族 {tot_fam:.0f}%")
|
||
detail = " ".join(f"{m.replace('deepseek_v4_', 'ds_').replace('kimi_', 'k')}: {ex[m] / B * 100:.0f}%"
|
||
for m in NAMES)
|
||
log(f" 逐模型精确: {detail}")
|
||
if confusions:
|
||
top = ", ".join(f"{a}→{b}×{c}" for (a, b), c in confusions.most_common(4))
|
||
log(f" 混淆集中在: {top}")
|
||
log()
|
||
|
||
with open(f"{BFD}/keepset_eval.txt", "w") as f:
|
||
f.write("\n".join(out) + "\n")
|
||
print("已写入 /tmp/bfd/keepset_eval.txt")
|