Files
natural-memory-nm21/report_zero_overlap_phase.py
WpyQwq 643e22ecb9 Natural Memory NM2.1: 记忆路由器分叉、数据集缺陷修复与全轴评测证据
- 引入 MemoryRouterXL 与 v5/v6 流式多线程训练/编码管线
- 修复 prepare_memory_router_dataset 候选池重建缺陷(mega 家族 3568x 加速,输出逐字节相同)
- 修复 v5 被破坏的拒答与多跳标签(train 未知样本 319 -> 16319,multi_hop 平均正例 1.00 -> 2.00)
- 同存储预算下 V2-128 v6 逐轴 22/22 通过:Top-1 41.12% -> 94.62%,未知拒答 0.00% -> 100.00%
- 记录三条被实测推翻的显然优化(logits_to_keep=1 反而慢 55%、XL 容量未带来收益)
- 记忆手术跨架构可移植性 14/14,读写关闭时与原生模型逐位相同
2026-09-19 11:11:31 +08:00

164 lines
6.0 KiB
Python

"""Render the zero-overlap phase results into one comparison report.
Reads the scorecards produced for this phase and prints / writes a single table set,
so every number quoted in the write-up comes from a stored scorecard rather than from
scrollback. All rates are rendered as percentages, never bare decimals.
Usage::
python -m V2_dpskw.report_zero_overlap_phase --output zero_overlap_phase.md
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parent
#: (json path, human label, unit) -- unit is "pct", "int" or "num".
#: Paths are tuples because the threshold key itself contains a dot ("thr0.50").
#: Policy axes live under metrics["thr0.50"], matching eval_router_scorecard.py.
METRICS = [
(("metrics", "top1"), "Top-1 正确率", "pct"),
(("metrics", "recall3"), "Recall@3", "pct"),
(("metrics", "mrr"), "MRR", "pct"),
(("metrics", "all_evidence_in_top3"), "多跳证据全中(Top-3)", "pct"),
(("metrics", "hop_accuracy"), "hop 正确率", "pct"),
(("metrics", "thr0.50", "specificity_unknown_refusal"), "未知拒答率", "pct"),
(("metrics", "thr0.50", "known_question_refusal_rate"), "已知问题被误拒率", "pct"),
(("metrics", "thr0.50", "unknown_question_read_rate"), "未知问题被误读率", "pct"),
]
STORAGE_METRICS = [
(("storage", "address_bytes_per_record"), "每条记录地址字节", "int"),
(("router", "parameters"), "参数量", "int"),
]
LATENCY_METRICS = [
(("latency", "cuda", "single_query_latency_ms_p50"), "单查询延迟中位数 ms (GPU)", "num"),
(("latency", "cuda", "queries_per_second"), "路由 QPS (GPU, 单查询)", "num"),
(("latency", "cuda", "batched_qps_64"), "批量 QPS (GPU, batch=64)", "num"),
(("latency", "cuda", "batched_qps_256"), "批量 QPS (GPU, batch=256)", "num"),
]
def pluck(body: dict, path: tuple[str, ...]):
"""Read a nested path from a run body, returning None when any part is missing."""
current = body
for part in path:
if not isinstance(current, dict):
return None
current = current.get(part)
if current is None:
return None
return current
def render(value, unit: str) -> str:
if value is None:
return "-"
if unit == "pct":
return f"{100 * float(value):.2f}%"
if unit == "int":
return f"{int(value):,}"
return f"{float(value):.4f}"
def table(title: str, scorecards: list[tuple[str, Path]], note: str = "") -> list[str]:
loaded = []
for caption, path in scorecards:
if not path.exists():
continue
loaded.append((caption, path, json.loads(path.read_text(encoding="utf-8"))))
if not loaded:
return [f"## {title}", "", "_no scorecard found_", ""]
labels: list[str] = []
for _, _, data in loaded:
for label in data:
if label not in labels:
labels.append(label)
lines = [f"## {title}", ""]
if note:
lines += [note, ""]
lines.append("| 指标 | " + " | ".join(labels) + " |")
lines.append("|---" * (len(labels) + 1) + "|")
def row(nice: str, extract) -> str:
cells = []
for label in labels:
value = None
for _, _, data in loaded:
body = data.get(label)
if body is not None:
value = extract(body)
break
cells.append(value)
return f"| {nice} | " + " | ".join(cells) + " |"
for path, nice, unit in METRICS:
lines.append(row(nice, lambda body, p=path, u=unit: render(pluck(body, p), u)))
for path, nice, unit in STORAGE_METRICS:
lines.append(row(nice, lambda body, p=path, u=unit: render(pluck(body, p), u)))
for path, nice, unit in LATENCY_METRICS:
lines.append(row(nice, lambda body, p=path, u=unit: render(pluck(body, p), u)))
lines.append("")
for caption, path, data in loaded:
families = {}
for label in labels:
body = data.get(label)
if body:
for family in body.get("by_family", {}):
families[family] = True
if len(families) > 1:
lines.append(f"### {caption} — 分类别 Top-1")
lines.append("")
lines.append("| 类别 | " + " | ".join(labels) + " |")
lines.append("|---" * (len(labels) + 1) + "|")
for family in sorted(families):
cells = []
for label in labels:
body = data.get(label) or {}
block = (body.get("by_family") or {}).get(family) or {}
cells.append(render(block.get("top1"), "pct"))
lines.append(f"| {family} | " + " | ".join(cells) + " |")
lines.append("")
return lines
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", default="zero_overlap_phase.md")
args = parser.parse_args()
lines = [
"# 零重叠改写(zero-overlap paraphrase)阶段结果",
"",
"所有速率均为百分比。数据来源为各阶段保存的 scorecard JSON,不引用滚动日志。",
"",
]
lines += table(
"零重叠评测集(300 条,24 个**未见过的**改写问法,24 个同形候选,随机猜测 4.17%)",
[("基线", ROOT / "zero_overlap_baseline.json"),
("冻结策略重放", ROOT / "replay_check_zov.json")],
note="`基线` 含部署态路由器与 v6;`冻结策略重放` 为合并数据重放训练后的路由器。",
)
lines += table(
"v6 评测集(21,920 条,全部 22 轴)",
[("基线", ROOT / "forget_check_zov.json"),
("冻结策略重放", ROOT / "replay_check_v6.json")],
note="`基线` 同时包含朴素微调(仅零重叠数据)以显示灾难性遗忘,以及 v6 原始路由器。",
)
text = "\n".join(lines) + "\n"
Path(args.output).write_text(text, encoding="utf-8")
print(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())