holoaeon / scripts /bench_semvec_kfold.py
weideyouni's picture
sync: five-site DNA + B0 baseline freeze (92fd8b5)
9f075f3 verified
Raw History Blame Contribute Delete
6.82 kB
# -*- coding: utf-8 -*-
"""
bench_semvec_kfold.py —— V2 / W8 分层留出集(5 折)评估
================================================================================
用法:
python scripts\\bench_semvec_kfold.py --folds 5
python scripts\\bench_semvec_kfold.py --folds 5 --no-write
口径(务必连带读):
· 折 = 问法风格层(原句 / 口语 / 客气 / 书面 / 追加话术),每折 47 条,
覆盖全部技能各一次;索引只在技能自述上 fit,问句不进语料 → 无训练泄漏;
因此 std 读作"路由对问法风格的敏感度",不是"模型泛化能力"。
· 每折报 P@1(命中期望技能即 1),对 5 折取 mean / std(总体标准差)。
· 门禁:std ≤ 0.05 且 5 折全跑通 → 绿(0);std > 0.05 → 黄(2),
并把数值写进 docs/spec-diff0-v0.2.md §9 已知限制。
· 不调参刷分:权重与噪声地板沿用 bench_semvec.py 的 WEIGHT_SEM / BASE_FLOOR。
"""
from __future__ import annotations
import json
import math
import os
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
sys.path.insert(0, os.path.join(ROOT, "scripts"))
from bench_semvec import BASE_FLOOR, INTENTS, WEIGHT_SEM, build, corpus_texts, route # noqa: E402
from holoaeon.semvec import SemanticIndex # noqa: E402
from holoaeon.spec import DOC_VERSION, VERSION # noqa: E402
CORPUS = os.path.join(ROOT, "data", "semvec_corpus_v2.json")
STD_LIMIT = 0.05
EXIT_GREEN, EXIT_YELLOW = 0, 2
def _pstdev(values):
n = len(values)
if n == 0:
return 0.0
mean = math.fsum(values) / n
var = math.fsum((v - mean) ** 2 for v in values) / n
return math.sqrt(var)
def load_corpus(path: str = CORPUS) -> dict:
with open(path, "r", encoding="utf-8") as fh:
return json.load(fh)
def run(folds: int = 5, write: bool = True, path: str = CORPUS) -> dict:
payload = load_corpus(path)
tiers = payload["tiers"]
if folds > len(tiers):
raise SystemExit("语料只有 %d 层,无法切 %d 折" % (len(tiers), folds))
items = payload["items"]
k = build()
docs = corpus_texts(k)
idx = SemanticIndex()
idx.fit(docs)
per_fold = []
for tier in tiers[:folds]:
subset = [it for it in items if it["tier"] == tier]
base_ok = sem_ok = hyb_ok = 0
misses = []
for it in subset:
r = route(k, idx, it["query"], WEIGHT_SEM)
if r["base"]["skill"] == it["expect"]:
base_ok += 1
if r["sem"]["skill"] == it["expect"]:
sem_ok += 1
if r["hybrid"]["skill"] == it["expect"]:
hyb_ok += 1
else:
misses.append({"query": it["query"], "expect": it["expect"],
"got": r["hybrid"]["skill"]})
n = len(subset) or 1
per_fold.append({"fold": tier, "n": len(subset),
"base_p1": round(base_ok / n, 4),
"sem_p1": round(sem_ok / n, 4),
"hybrid_p1": round(hyb_ok / n, 4),
"misses": misses[:6]})
hyb = [f["hybrid_p1"] for f in per_fold]
mean = math.fsum(hyb) / len(hyb) if hyb else 0.0
std = _pstdev(hyb)
# 跨词汇锚点:V1 的 12 条意图(问句不含技能名里的规范术语,属"真改写"),
# 单独报告,用来对冲"235 条语料里含技术术语导致 P@1 虚高"的偏差。
anchor_ok = 0
for query, expect in INTENTS:
if route(k, idx, query, WEIGHT_SEM)["hybrid"]["skill"] == expect:
anchor_ok += 1
anchor = {"intents": len(INTENTS), "hybrid_correct": anchor_ok,
"hybrid_rate": round(anchor_ok / (len(INTENTS) or 1), 4)}
out = {
"version": VERSION,
"doc_version": DOC_VERSION,
"evidence_class": "simulated",
"corpus": os.path.relpath(path, ROOT).replace("\\", "/"),
"corpus_sha": payload.get("corpus_sha"),
"skills": payload["skills"],
"count": payload["count"],
"folds": len(per_fold),
"weight_sem": WEIGHT_SEM,
"base_floor": BASE_FLOOR,
"vocab_size": len(idx.vocab),
"dim": idx.dim,
"mean_p1": round(mean, 4),
"std_p1": round(std, 4),
"std_limit": STD_LIMIT,
"min_p1": round(min(hyb), 4) if hyb else 0.0,
"max_p1": round(max(hyb), 4) if hyb else 0.0,
"per_fold": per_fold,
"anchor_v1_intents": anchor,
"note": ("折=问法风格层,索引只在技能自述上 fit,问句不进语料;"
"std 读作问法风格敏感度,不是泛化能力。235 条含规范术语,"
"P@1 虚高,跨词汇能力以 anchor_v1_intents 为准。simulated。"),
}
if write:
dst = os.path.join(ROOT, "benchmarks",
"results-semvec-kfold-%s.json" % DOC_VERSION)
os.makedirs(os.path.dirname(dst), exist_ok=True)
with open(dst, "w", encoding="utf-8", newline="\n") as fh:
fh.write(json.dumps(out, ensure_ascii=False, indent=2,
sort_keys=True))
fh.write("\n")
out["written"] = dst
out["exit"] = EXIT_GREEN if std <= STD_LIMIT else EXIT_YELLOW
return out
def main(argv=None) -> int:
argv = argv if argv is not None else sys.argv[1:]
folds = 5
if "--folds" in argv:
folds = int(argv[argv.index("--folds") + 1])
res = run(folds=folds, write="--no-write" not in argv)
print("=" * 72)
print("semvec 分层留出 folds=%d skills=%d queries=%d vocab=%d dim=%d"
% (res["folds"], res["skills"], res["count"], res["vocab_size"],
res["dim"]))
print("corpus=%s sha=%s" % (res["corpus"], res["corpus_sha"]))
print("-" * 72)
print("%-14s %-6s %-8s %-8s %-9s" % ("折(问法层)", "n", "基线P@1", "语义P@1", "混合P@1"))
for f in res["per_fold"]:
print("%-14s %-6d %-8.4f %-8.4f %-9.4f"
% (f["fold"], f["n"], f["base_p1"], f["sem_p1"], f["hybrid_p1"]))
print("-" * 72)
print("混合 P@1 mean=%.4f std=%.4f min=%.4f max=%.4f (limit std<=%.2f)"
% (res["mean_p1"], res["std_p1"], res["min_p1"], res["max_p1"],
res["std_limit"]))
print("跨词汇锚点(V1 12 意图,含改写) %d/%d = %.1f%%"
% (res["anchor_v1_intents"]["hybrid_correct"],
res["anchor_v1_intents"]["intents"],
res["anchor_v1_intents"]["hybrid_rate"] * 100))
if res.get("written"):
print("written: %s" % res["written"])
print("KFOLD_EXIT=%d" % res["exit"])
return res["exit"]
if __name__ == "__main__":
raise SystemExit(main())