File size: 6,818 Bytes
9f075f3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | # -*- 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())
|