File size: 8,993 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 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | # -*- coding: utf-8 -*-
"""
bench_semvec.py —— 语义通道实测(路由准确率 + 确定性自证)
================================================================================
用法:
python scripts\\bench_semvec.py # 跑并写 benchmarks\\results-semvec-*.json
python scripts\\bench_semvec.py --no-write # 只打印
口径:
- 基线 = 既有 Registry.score(Jaccard + 字符 2-gram Dice + 标签命中);
- 语义 = semvec 余弦;
- 混合 = 0.5·基线 + 0.5·语义(规格要求的可调权重);
- 全部 simulated:语料是技能自述文本,不含用户数据。
"""
from __future__ import annotations
import json
import os
import sys
import time
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
from holoaeon.kernel import Kernel # noqa: E402
from holoaeon.packs import PACK_ORDER, get_pack # noqa: E402
from holoaeon.semvec import SemanticIndex, docs_from_skills, tokenize # noqa: E402
from holoaeon.skill import tokenize as base_tokenize # noqa: E402
from holoaeon.spec import VERSION # noqa: E402
INTENTS = [
("统计字数:HoloAeon 是零依赖的确定性多智能体认知框架。", "word_count"),
("患者数据统计", "basic_stats"),
("给这列数据算均值和标准差", "basic_stats"),
("计算夏普比率", "sharpe"),
("计算收益率", "returns"),
("最大回撤是多少", "drawdown"),
("读一下这篇论文的摘要", "extract_abstract"),
("筛一下相关文献", "screen_papers"),
("去重论文列表", "dedup_papers"),
("写周报总结", "weekly_review"),
("拆解一下这个任务", "task_breakdown"),
("设计实验的样本量", "sample_size"),
]
WEIGHT_SEM = 0.75
BASE_FLOOR = 0.15
def build() -> Kernel:
k = Kernel()
for key in PACK_ORDER:
get_pack(key).install(k)
return k
def vector_digest(index: SemanticIndex) -> str:
import hashlib
payload = json.dumps(
{t: [round(x, 9) for x in v] for t, v in sorted(index.vectors.items())},
ensure_ascii=False, sort_keys=True,
)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
def corpus_texts(k: Kernel) -> list:
"""训练语料:**只取技能自述**(name + description + tags),与 docs/ 文档彻底解耦。
踩过的坑:早先版本把 README/docs/*.md 的整行也塞进语料,于是每改一次文档,
语料就多几行、词表与 PMI 全部漂移,同一批 12 条意图的命中率会在 12/12 与 9/12
之间跳(实测 +15 行文档 → 跌到 9/12)。技能路由的索引只该学"技能怎么描述自己",
语料必须由代码内容唯一决定,才能做到"文档怎么改都不影响基准"。
"""
return docs_from_skills(k.skills.all())
def route(k: Kernel, index: SemanticIndex, query: str, weight: float) -> dict:
"""混合路由:基线与语义各自按本次候选集 min-max 归一,再按权重叠加。
两处必要修正(都是实测踩出来的):
1) 不做归一化时两路量纲差两个数量级,混合等于"语义被基线的尺度吞掉";
2) 只做 min-max 还不够:基线分数绝对值极低(0.02–0.05)时,噪声也能被放大成
"满分 1.0",于是 word_count/goal_smart 这类误命中反杀语义。故用 BASE_FLOOR
给基线加噪声地板:候选集基线峰值低于 BASE_FLOOR 时,按比例削弱基线话语权。
3) 平局用 (混合分, 语义分, 基线分, 技能名) 四重定序,保证逐位可复现。
"""
skills = k.skills.all()
q_tokens = base_tokenize(query)
q_vec = index.encode(query)
base_raw, sem_raw = [], []
for s in skills:
b = float(k.skills.score(s, q_tokens, query))
sv = _cos(q_vec, index.encode(" ".join([s.manifest.name,
s.manifest.description]
+ list(s.manifest.tags))))
base_raw.append(b)
sem_raw.append(sv)
def _norm(vals: list) -> list:
lo, hi = min(vals), max(vals)
if hi - lo < 1e-12:
return [0.0 for _ in vals]
return [(v - lo) / (hi - lo) for v in vals]
base_n, sem_n = _norm(base_raw), _norm(sem_raw)
peak = max(base_raw) if base_raw else 0.0
scale = 1.0 if peak <= 0 else min(1.0, peak / BASE_FLOOR)
best = {}
for tag in ("base", "sem", "hybrid"):
best[tag] = (-1.0, "")
mix_best = None
for i, s in enumerate(skills):
mix = (1.0 - weight) * scale * base_n[i] + weight * sem_n[i]
if base_raw[i] > best["base"][0]:
best["base"] = (base_raw[i], s.name)
if sem_raw[i] > best["sem"][0]:
best["sem"] = (sem_raw[i], s.name)
key = (round(mix, 9), round(sem_n[i], 9), round(base_n[i], 9), s.name)
if mix_best is None or key > mix_best[0]:
mix_best = (key, s.name)
best["hybrid"] = (mix_best[0][0], mix_best[1])
return {
"query": query,
"base": {"skill": best["base"][1], "score": round(best["base"][0], 5)},
"sem": {"skill": best["sem"][1], "score": round(best["sem"][0], 4)},
"hybrid": {"skill": best["hybrid"][1], "score": round(best["hybrid"][0], 4)},
}
def _cos(a: list, b: list) -> float:
import math
d = math.fsum(x * y for x, y in zip(a, b))
na = math.sqrt(math.fsum(x * x for x in a))
nb = math.sqrt(math.fsum(x * x for x in b))
return d / (na * nb) if na > 1e-12 and nb > 1e-12 else 0.0
def run(write: bool = True) -> dict:
k = build()
docs = corpus_texts(k)
labels = [s.name for s in k.skills.all()]
t0 = time.perf_counter()
idx = SemanticIndex()
idx.fit(docs)
fit_ms = (time.perf_counter() - t0) * 1000.0
dig1 = vector_digest(idx)
idx2 = SemanticIndex()
idx2.fit(docs)
dig2 = vector_digest(idx2)
bitwise_equal = bool(dig1 == dig2)
rows = []
for query, expect in INTENTS:
r = route(k, idx, query, WEIGHT_SEM)
r["expect"] = expect
r["base_ok"] = r["base"]["skill"] == expect
r["sem_ok"] = r["sem"]["skill"] == expect
r["hybrid_ok"] = r["hybrid"]["skill"] == expect
rows.append(r)
n = len(rows)
base_acc = sum(1 for r in rows if r["base_ok"])
sem_acc = sum(1 for r in rows if r["sem_ok"])
hyb_acc = sum(1 for r in rows if r["hybrid_ok"])
out = {
"version": VERSION,
"evidence_class": "simulated",
"corpus_docs": len(docs),
"vocab_size": len(idx.vocab),
"dim": idx.dim,
"fit_ms": round(fit_ms, 2),
"vector_digest_a": dig1,
"vector_digest_b": dig2,
"bitwise_equal": bitwise_equal,
"weight_sem": WEIGHT_SEM,
"intents": n,
"base_correct": base_acc,
"sem_correct": sem_acc,
"hybrid_correct": hyb_acc,
"base_rate": round(base_acc / n, 4),
"sem_rate": round(sem_acc / n, 4),
"hybrid_rate": round(hyb_acc / n, 4),
"rows": rows,
}
if write:
path = os.path.join(ROOT, "benchmarks", "results-semvec-%s.json" % VERSION)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "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"] = path
return out
def main() -> int:
res = run(write="--no-write" not in sys.argv)
print("=" * 72)
print("semvec 实测 corpus=%d docs vocab=%d dim=%d fit=%.2fms"
% (res["corpus_docs"], res["vocab_size"], res["dim"], res["fit_ms"]))
print("确定性 digest_a=%s digest_b=%s equal=%s"
% (res["vector_digest_a"], res["vector_digest_b"], res["bitwise_equal"]))
print("-" * 72)
print("%-28s %-16s %-16s %-16s" % ("意图", "基线", "语义", "混合%.2f" % WEIGHT_SEM))
for r in res["rows"]:
print("%-28s %-16s %-16s %-16s expect=%s"
% (r["query"][:26], r["base"]["skill"][:15],
r["sem"]["skill"][:15], r["hybrid"]["skill"][:15], r["expect"]))
print("-" * 72)
print("准确率 基线 %d/%d = %d%% | 语义 %d/%d = %d%% | 混合 %d/%d = %d%%"
% (res["base_correct"], res["intents"], int(res["base_rate"] * 100),
res["sem_correct"], res["intents"], int(res["sem_rate"] * 100),
res["hybrid_correct"], res["intents"], int(res["hybrid_rate"] * 100)))
if res.get("written"):
print("written: %s" % res["written"])
# 门禁:误路由率 ≤10% 即准确率 ≥11/12(0.9167)。留出集泛化不入门禁,只在文档披露。
ok = (res["bitwise_equal"] and res["hybrid_rate"] >= 0.9167
and res["fit_ms"] < 5000)
print("SEMVEC_EXIT=%d" % (0 if ok else 1))
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())
|