Files
natural-memory-nm21/ZERO_OVERLAP_FINDINGS.md
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

12 KiB
Raw Permalink Blame History

零重叠改写成语(zero-overlap paraphrase)阶段报告

本阶段针对 E2E_FINDINGS.md 定位到的唯一泛化缺口:查询与事实之间没有任何独特字重叠时的语义匹配。 所有速率均为百分比。所有数字均来自磁盘上保存的 scorecard / 评测 JSON,不引用滚动日志。 逐轴对照表见 zero_overlap_phase.md(由 report_zero_overlap_phase.py 从 scorecard JSON 渲染)。


1. 结果摘要

新增了一个可验证的零重叠改写数据集、一个合并特征库,以及一个重放微调后的路由器 REPLAY-128(2,037,774 参数,128 维,每条记录地址 512 字节 —— 与现网路由器几何完全一致)。

评测 指标 v6 最终版 REPLAY-128 变化
零重叠(300 条,24 个未见过问法) Top-1 正确率 18.40% 59.60% +41.20pp
零重叠 Recall@3 36.00% 83.20% +47.20pp
零重叠 MRR 35.07% 73.06% +37.99pp
v6 全集(21,920 条,全部 22 轴) Top-1 正确率 94.37% 94.14% −0.23pp
v6 全集 Recall@3 96.40% 96.48% +0.08pp
v6 全集 多跳证据全中(Top-3) 95.95% 96.22% +0.27pp
v6 全集 hop 正确率 100.00% 100.00% 0
v6 全集 未知拒答率 100.00% 100.00% 0
v6 全集 已知问题被误拒率 0.00% 0.00% 0

零重叠评测集随机猜测基线为 4.17%(24 个同形候选),因此 59.60% 约为随机水平的 14 倍。 v6 全集的全部策略轴逐位不变(见第 3 节的冻结机制),检索轴变化在 ±0.27pp 以内。

部署契约:check_router_swap.py 判定 DROP-IN REPLACEMENT OK —— 16/16 键匹配、无缺失/多余、 无形状不符、分数有限,且实测可驱动 PagedMemoryBankV2 路由(bank_routed_records: 3)。

延迟(bench_router_latency.py,7 轮交错取中位数):单查询 1.2042 ms → 1.2087 ms(+0.37%), batch=256 QPS 190,103 → 192,600(+1.3%)。batch=64 QPS 读数低 9.2%,但该轮自身离散度为 17.4%,且同架构同参数同 FLOPs 下 batch=256 反而更快 —— 方向不一致,判为测量噪声而非回归。


2. 数据集:属性由构造保证,而非相信手写意图

make_zero_overlap_paraphrase_data.py 生成 24 个属性 × 3 个改写问法:

  • 每条查询与其自身目标事实的独特字重叠必须为 0(属性字符、停用字除外);
  • 前 2 个改写用于训练(48 个问法),第 3 个留出用于评测(24 个问法)——两个划分的 查询字符串完全不相交,因此评测衡量的是"换一种说法"的泛化,而不是记住训练字符串;
  • 候选是 24 条同句式、不同属性的事实,值为随机码,只出现在一条事实里(可检测证据混用);
  • 15% 为不可回答 episode(改写指向候选中不存在的属性)。

关键设计判断:改写与其他无关属性的字符重叠被刻意保留(生成器统计 54.53% 的 episode 至少 含一个这类"陷阱"候选)。因为命中错误候选会得到错误答案,这只会让任务更难,不会提供捷径; 真正必须为 0 的是与自身目标的重叠。最初把二者混为一谈时,24 个属性中有 7 个因为 "起床时间"的"时"、"办公城市"的"公"等高频字而被判为"无可用的改写"。

verify_zero_overlap_dataset.py 用独立重写的停用字集合与重叠算法复核(不 import 生成器):

  • 结构问题 0,查询与自身目标重叠 0,两个划分共享查询字符串 0;
  • 每条的答案码只出现在唯一一条候选中,候选属性标签唯一;
  • answerable / need_memory / hop 与正例索引一致。

(生成器报的陷阱比例 54.53% 与校验器报的 44.00%/50.00% 不同,因为两者用不同的停用字集合 定义"独特字";这是定义差异,不是矛盾。)


3. 两次微调的对照:朴素微调会造成灾难性遗忘

先做朴素微调(只用零重叠数据,全部损失项默认权重,从 v6 最终版权重出发,6000 步):

  • 零重叠 Top-1 18.40% → 66.80%,但 v6 全集同时退化: random_position MRR 99.02% → 93.37%、多跳证据全中 100.00% → 95.65%、 hop 100.00% → 87.10%、已知问题被误拒率 0.00% → 13.85%、unknown Top-1 58.99% → 47.52%。
  • 训练日志还显示策略门发生了"用误拒换拒答"的交换:未知拒答率升到 40.00%,代价是 38.00% 的可回答问题被误拒。

这就是单指标视角会误判为胜利、而多轴记分卡会否掉的典型情形。

修正方案由两点组成:

  1. 冻结策略头(--need-loss-weight 0 --hop-loss-weight 0)。在零重叠集合上,need_memory 门只看查询、看不到候选,而"不可回答"的改写与可回答的改写句式完全一样,因此该门在此数据上 原理上不可学;强行训练只会破坏 v6 学到的门。实测证明冻结生效:合并评测的 need_specificity 在每一次 eval 中都是 0.9879692011549567,逐位不变。
  2. 重放(把零重叠数据并入 v6 数据一起训练),避免排序通路被新分布带偏。

4. 合并语料:复制已验证的行,而不是重新编码

build_replay_corpus.py 把 v6 库(2,124,552 行)与零重叠库(36,072 行)合并为 2,160,624 行。没有重新编码 v6 特征 —— 那既耗时数小时,又会让历史测量所依赖的参考特征 发生无声漂移。合并的强制不变量:

  • 每个源库的每一行都恰好被复制一次,或因文本完全相同而跳过(本次重复 0 行);
  • index 条目数 == 库行数(训练器会拒绝不一致的库);
  • 合并后 train/eval 引用的每一个文本都能解析到一行(缺失 0);
  • 2000 行抽查逐字节相等;全库扫描全零行 0;
  • manifest 携带合并后数据文件的 sha256(训练器会据此校验冻结输入)。

合并语料:train 88,355 episode / eval 22,220 episode; data/router_replay_v7/、H:\Memory\nm_cache\nm_replay_v7\feature_cache。


5. 关键负面结论:路由器排序不影响端到端答案

REPLAY-128 与 V2-128-v6 在 eval_router_critical_e2e.py(16 个零重叠用例)上的结果逐位相同:

top_k_records 路由器 回答正确率 答成别的属性 平均选中记录
默认 V2-128-v6 / REPLAY-128 37.50% / 37.50% 25.00% / 25.00% 5.88 / 5.88
1 V2-128-v6 / REPLAY-128 12.50% / 12.50% 43.75% / 43.75% 0.94 / 0.94
3 V2-128-v6 / REPLAY-128 31.25% / 31.25% 25.00% / 31.25% 2.44 / 2.44

路由器级 Top-1 提升了 41.20pp,端到端却一个用例都没变。因此做了一个判决性实验:保留 need_memory / hop_controller / head_gate 全部原权重,只把排序通路 (query_projection、key_projection、pair_scorer)替换为同形状随机权重 (checkpoints/router_v6_v2_128/ranking_scrambled_probe.pt,非有限值 0 个)。 结果在默认 Top-K 与 Top-K=1 下都完全不变(37.50% / 12.50%,选中记录数相同)。

机制(已逐项实测,含两次被自己数据推翻的中间假设):

  • memory_os_v2.py:1671 — 带 semantic_key(张量)的记录,其 _score_candidates()(即 MemoryRouterV2.projected_scores)分数会被 self.record_scorer 逐位置覆盖;实测残差与 text_retriever 匹配 18/18,与 memory_router_v2 匹配 0/18。
  • qwen_integration.py:1359 _score_semantic_memory_records — 该回调走 self.text_retriever。注意:最初以为"部署目录没有 text_retriever.pt 所以走余弦兜底" 是错的 —— 重排器被烘焙进合并包(safetensors 内有 6 个 dynamic_memory.text_retriever.* 张量,1,573,377 参数), _text_retriever_ready 实测为 True,余弦兜底分支根本不执行。
  • 路由器在此配置下唯一实际生效的杠杆是 need_memory 门:部署权重下门开启率 68.75%(11/16),而三份随机初始化的路由器只有 37.5%(6/16)。判决性探针只随机化了 排序通路、保留了门控头,所以"端到端零变化"是由构造保证的,而不是巧合。
  • 这 16 个用例里 93.75%(15/16)实际注入的是旧版 16 槽路径的证据 (legacy_slot + text_memory_top_k=2),而不是 V2 库记录;地址命中 0/16, 直接词面命中 6/16。skip_semantic_expansion 在这 6 例里直接跳过整个语义 hop 循环。
  • 更严重的是:一次写入 20 条不同属性的事实后,只剩 12 条 active,8 条在查询前就已被 retract(16 个用例的 8 个里,目标记录本身就在被删的那批里)。任何排序器都救不回一条 已经不存在的记录,因此端到端正确率存在 50% 的硬上限。

结论分两层,不要混为一谈:

  1. 路由器记分卡上 22/22 的统治性和本次 +41.20pp 的零重叠提升,都是路由器工件真实的增益;
  2. 但在这套运行时里,记录级顺序由打包的 text_retriever 决定,路由器分数被覆盖,且相当比例的 读取根本不走 V2 库、目标记录还会被写入路径删除。所以路由器增益不会传递到最终答案。

要让增益落地,需要的不是继续加大路由器,而是:

  1. 修写入路径:不要让 20 条不同属性的事实互相淘汰(当前最大瓶颈,直接决定 50% 上限);
  2. 让 V2 库记录真正参与读取:目前 93.75% 的用例走旧版 16 槽路径;
  3. 训练/替换 text_retriever(记录级重排器)—— 零重叠数据集可直接复用为它的训练/评测集;
  4. 让路由器分数对 semantic_key 记录也参与排序(改动 memory_os_v2.py:1671 的覆盖逻辑)。 (曾据此改过一版,实测在部署配置下不生效,已回退并留注释。)

6. 产物清单

文件 作用
make_zero_overlap_paraphrase_data.py 生成零重叠数据集(属性由构造保证,划分字符串不相交)
verify_zero_overlap_dataset.py 独立复核数据集(不共享生成器代码)
build_replay_corpus.py 合并两个数据集与两个特征库,含逐字节抽查
report_zero_overlap_phase.py 由 scorecard JSON 渲染对照表(zero_overlap_phase.md)
data/zero_overlap/ train 1,200 / eval 300(含 manifest.json、zero_overlap_verification.json)
data/router_replay_v7/ 合并语料 train 88,355 / eval 22,220
H:\Memory\nm_cache\nm_replay_v7\feature_cache 合并特征库 2,160,624 行
checkpoints/router_replay_v7_v2_128/ REPLAY-128 最终版权重(可 drop-in)
compare_record_rankers.py 在同一批冻结特征上对比"余弦 / 各路由器"的排序能力
measure_write_path_updates.py 量写入路径的更新判定(证明重排器打分不是 retract 的原因)
probe_record_selection.py 逐记录拆解打分项、门控扫描、目标记录存活状态
migrate_bank_paths.py 把特征库路径从 D: 迁移到 H:\Memory\nm_cache(含校验)
zero_overlap_phase.md 本阶段的逐轴对照表

特征库现存于 H:\Memory\nm_cache\(nm_router_v6 / nm_router_v5 / nm_zero_overlap / nm_replay_v7 / nm_probe_bf16,共约 35.9 GB;D 盘已不再保存项目数据)。迁移后用 verify_feature_bank.py 全盘复扫(2,124,552 行、全零行 0、complete=true)并在新路径下 复算了排序器对比(59.60 / 30.40 逐位一致)。

历史运行记录(checkpoints/*/metrics.jsonl、*/training_stdout.log、 */router_v5_training.json、feature_bank_verification.json)刻意保留原始 D: 路径, 因为那是当时真实的运行事实,改写它们等于篡改记录。

对照用的负面结果与判决性探针:checkpoints/router_zov_v2_128/(朴素微调)、 checkpoints/router_v6_v2_128/ranking_scrambled_probe.pt(随机排序探针)、 forget_check_zov.json、router_critical_e2e_scramble*.json。