sora 370953729b Fix perf stats (wrong import path), per-repeat checkpoints, README
- perf_stats aggregator lives in eval/, not model/: the import failed
  silently and EVERY perf column was empty (not just ttft). Now warns
  on stderr instead of swallowing.
- repeats > 1 get their own checkpoint key (:rep2, :rep3, ...): repeat 2
  previously restored repeat 1's predictions and finished instantly with
  identical scores. rep1 keeps the legacy key (existing checkpoints still
  resume).
- repeats summary: report the MEAN score and aggregate time/tokens over
  ALL runs (was: last run only).
- README: six-benchmark command as the primary example.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-11 13:38:04 +00:00

188 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""FP-Fusion 维度 A家族归因Model Family Attribution
双路归因:
1. 词表归因(主路,无需额外依赖)—— 按探针层加权命中家族别名:
I 层自称命中 +1.5/次(含越狱/中英/直问)
K 层组织/创建者 +1.0/次
C 层拒答风格 +0.5/次(安全对齐措辞属家族化特征)
S 层风格特征词 +0.3/次
requested_family 先验 +0.8served-name 声称家族)
2. LLMmap 嵌入归因(辅路,可选)—— 对 I/K 层回答用 e5 embedding 与
60 个已知模板算距离,取 top-3 模板的家族投票。
融合:
S_fam = confidence × (1 - 0.3 × conflict)
conflict = 主路 top1 家族与 LLMmap top1 家族不一致(量化/蒸馏/伪装信号)
"""
import os
import sys
from .scorer import _families_in_text, load_aliases
# -- 风格特征词(跨语言,家族化措辞弱信号)------------------------------
# 注意:这是"说话风格"侧写,不是身份声称,权重最低。
_STYLE_TOKENS = {
# 英文:高频开场/缓冲词
'certainly!', 'let me', 'let\'s', 'absolutely!', 'great question',
'sure!', 'as an ai', 'i\'m happy to', 'i\'d be happy to',
'good question', 'that\'s a great', 'here\'s how', 'let me break',
# 中文
'当然', '让我', '好的,', '没问题', '这是一个好问题',
'首先', '其次', '最后', '总之',
}
def _lexicon_scores(records, aliases, requested_family):
"""词表归因按层加权累加家族分数。records 需含 layer/response/error。"""
scores = {}
claim_hits = 0 # I 层可解析自称次数(归一化用)
style_hits = 0
for r in records:
if r.get('error') or not r.get('response'):
continue
text = r.get('response') or ''
layer = r.get('layer')
fams = _families_in_text(text, aliases)
if layer == 'I':
if len(fams) == 1: # 明确自称
f = next(iter(fams))
scores[f] = scores.get(f, 0) + 1.5
claim_hits += 1
elif len(fams) > 1: # 多家族提及,分散加权
for f in fams:
scores[f] = scores.get(f, 0) + 1.5 / len(fams)
claim_hits += 1
elif layer == 'K':
for f in fams: # 组织/创建者弱证据
scores[f] = scores.get(f, 0) + 1.0
elif layer == 'C':
low = text.lower()
if any(s in low for s in ("i can't", "i cannot", "无法", "抱歉",
"sorry", "i apologize")):
for f in fams: # 拒答风格命中
scores[f] = scores.get(f, 0) + 0.5
if not fams:
style_hits += 1 # 拒答但未点名家族 → 中性
elif layer == 'S':
low = text.lower()
for tok in _STYLE_TOKENS:
if tok in low:
style_hits += 1
break
# 风格词与家族弱相关:仅当该回答同时提及家族才累加
if fams:
for f in fams:
scores[f] = scores.get(f, 0) + 0.3
# served-name 声称家族先验
if requested_family and requested_family in aliases:
scores[requested_family] = scores.get(requested_family, 0) + 0.8
return scores, claim_hits, style_hits
def _normalize(scores):
"""按分数排序,返回 (top1_family, confidence, per_family)。"""
if not scores:
return None, 0.0, {}
total = sum(scores.values())
order = sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
top1 = order[0][0]
conf = order[0][1] / total if total > 0 else 0.0
return top1, conf, dict(order)
def _llmmap_attribution(records, llmmap_tool):
"""LLMmap 嵌入归因辅路I/K 层回答 → 模板距离 → 家族投票。
llmmap_tool: 已加载的 LLMmap InferenceModel_open 实例(或 None
返回 {top1_family, top3: [(family, dist)], votes: {family: n}}。
"""
if llmmap_tool is None or not getattr(llmmap_tool, 'ready', False):
return None
import numpy as np
texts = []
for r in records:
if r.get('layer') not in ('I', 'K'):
continue
if r.get('error') or not r.get('response'):
continue
texts.append((r['id'], r['response']))
# LLMmap 模型一次只能吃固定 8 条 queries 的回答;这里每 8 条一批,
# 逐条与模板库比对后按 label_map 归集家族。
votes = {}
dist_rows = []
for batch_start in range(0, len(texts), 8):
batch = texts[batch_start:batch_start + 8]
answers = [t[1] for t in batch]
# 不足 8 条时补空串LLMmap __call__ 强校验数量)
answers = answers + [''] * (8 - len(answers))
try:
dists = llmmap_tool(answers)
except Exception:
continue
# open-set 距离:越小越好,取每条回答 top1 模板
order_idx = int(np.argmin(dists))
model_name = llmmap_tool.label_map[order_idx]
votes[model_name] = votes.get(model_name, 0) + 1
dist_rows.append((model_name, float(dists[order_idx])))
if not votes:
return None
top3 = sorted(dist_rows, key=lambda x: x[1])[:3]
top1_model = max(votes, key=votes.get)
return {'top1_templates': top3, 'votes': votes,
'top1_model': top1_model}
def family_attribution(records, aliases=None, requested_family=None,
llmmap_tool=None):
"""维度 A 主入口:双路归因融合。
Args:
records: fp_fusion 原始记录(含 layer/response/error/id
aliases: 家族别名表 dictNone → 默认 family_aliases.json
requested_family: served-name 声称家族None → 自动从 model 解析)。
llmmap_tool: 可选 LLMmap 实例。
Returns dict写入报告 signals.family:
enabled, method, top1_family, confidence, per_family_scores,
claims, style_hits, llmmap(可选), conflict
"""
aliases = aliases or load_aliases()
scores, claims, style_hits = _lexicon_scores(records, aliases,
requested_family)
top1, conf, per_fam = _normalize(scores)
llm = _llmmap_attribution(records, llmmap_tool) if llmmap_tool else None
conflict = False
if llm and llm.get('top1_model'):
# LLMmap 模板名 → 家族('Qwen/Qwen2-7B-Instruct' → qwen
tmpl_fams = _families_in_text(llm['top1_model'], aliases)
llm_fam = next(iter(tmpl_fams)) if len(tmpl_fams) == 1 else None
if top1 and llm_fam and llm_fam != top1:
conflict = True
s_fam = conf * (1.0 - 0.3 * int(conflict)) if top1 else 0.0
return {
'enabled': True,
'method': 'lexicon' + ('+llmmap' if llm else ''),
'top1_family': top1,
'confidence': round(conf, 4),
'per_family_scores': {k: round(v, 3) for k, v in per_fam.items()},
'claims_parsed': claims,
'style_hits': style_hits,
'llmmap': llm,
'llmmap_family': llm_fam if llm else None,
'conflict': conflict,
's_fam': round(s_fam, 4),
}