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

186 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 零重叠改写成语(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`。