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

168 lines
10 KiB
Markdown
Raw 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.
# 未知拒答:从 75% 泄漏到 100% 拒答(机制 + 证据 + 边界)
这是本项目目前最差的一个数字(**未知问题泄漏 75.00%**:问库里不存在的属性时照样编一个答案),
本轮把它定位、排除了整类无效修法,并验证了一个可用的机制。
---
## 1. 先排除掉一整类无效修法(有测量支撑)
最直觉的修法是"检索分数不够高就拒答"。用**只在训练集拟合、评测集算 AUC** 的口径测了四种打分器
的分数几何(top1 / margin / top-k 均值 / 离散度 / 熵 / 近邻数,共 9 个特征):
| 打分器 | 最佳单特征 AUC | 拟合头 AUC(评测集) |
|---|---:|---:|
| cosine(冻结键余弦) | 0.6257(margin) | 0.6086 |
| 打包 `text_retriever` | 0.6076(margin) | 0.4752 |
| 50/50 混合 | 0.5446 | 0.5062 |
| 训练过的路由器 | 0.5711 | 0.5130 |
操作点同样不可用:混合打分器想标出 22% 的未知要误拒 **28.40%** 的可回答问题;
cosine 想标出 46% 要误拒 **30.00%**。
**结论:75% 的泄漏不可能靠分数阈值或分数上的分类头修好。** 这不是调参空间不够,
而是这个信号根本不存在 —— 24 条同句式候选对任何提问都"一样像"。
## 2. 有效的机制:先判「问的是哪个属性」,再查库里有没有
换一个问题问就成立了:不是"有没有候选匹配得好",而是"这个提问在问哪个属性,这个属性在库里吗"。
**封闭词表**(提问的属性在模型学过的属性集合内、但不在候选/库里):
| 指标 | 值 |
|---|---:|
| 可回答 episode 中「预测属性在候选集内」 | **100.00%**(250 条) |
| 未知 episode 中「预测属性在候选集内」 | **0.00%**(50 条) |
| **未知拒答率** | **100.00%** |
| **已知问题被误拒率** | **0.00%** |
| 混淆矩阵 | tp=50 fn=0 fp=0 tn=250 |
属性识别本身在**未见改写**上也是 **100.00%**(250 条可回答 episode 全对,24 类,随机基线 4.17%)。
注意:这是**类别式规则**,不依赖任何脆弱阈值。
## 3. 开放集:真实用户会问模型没见过的属性,必须显式训 NONE 类
只靠最大概率调阈值是不够的 —— 实测**没有拒绝选项时,从未见过的属性 100.00% 会被认成某个存在的属性**
(`closed_set_false_claim_pct: 100.00%`)。用最大 softmax 做拒绝的 AUC 是 0.9629 / 0.9760(留出 6 / 12 个属性),
可用但有取舍:留出 6 个属性时,保住 100% 已知的阈值只能拒掉 **44.64%** 的未见属性。
所以改成**显式训练一个 NONE 类**:词表取 12 个属性作为类别,另取 6 个属性(词表外)的真实提问作为
NONE 的负样本,最后用**第三组、与两者都不相交的 6 个全新属性**做测试:
| 指标 | 值 |
|---|---:|
| 词表内属性识别准确率(133 条未见改写) | **100.00%** |
| **从未见过的属性被拒绝**(52 条,与词表、NONE 训练集均不相交) | **100.00%** |
| **已知问题被误拒率** | **0.00%** |
| 随机基线(13 类) | 7.69% |
对照当前运行时:未知泄漏 **75.00%**、A 段未知拒答率 **56.67%~60.00%**。
## 4. 落地形态与必须处理的边界
**运行时规则**(三步,全部可审计、无阈值调参):
1. 属性分类头读提问的冻结键 → 输出「某个属性」或 **NONE**;
2. 若为 NONE → 拒绝(明确说不知道);
3. 若为某属性 → 查库的活跃属性集合(即 `bank.active_by_conflict` 的键)是否包含它;不含 → 拒绝。
**必须处理的边界(诚实列出,未解决)**:
* **增量写入**:头部在训练时固定了属性词表;用户**训练之后**才写下的全新属性会被判成 NONE 而误拒。
缓解方向:把检索分数作为并联条件(NONE **且** 没有强匹配记录才拒答),或用属性分类体系扩大词表,
以及写入新属性时增量微调头部。
* **词表随库变化**:生产环境属性集合是用户相关的,头部需要用「该用户当前库的属性集合」构建类别,
本实验固定为 24 个。
* **规模**:本实验 24 个属性、300 条评测;生产需要上千属性、上万条改写问法的规模验证。
* 本机制只解决**拒答**;「答成别的属性 35.00%」是另一个问题(排序),本轮未动。
## 5. 集成尝试:**失败**(已回退,须如实记录)
把覆盖门接进运行时后,端到端整体测试的结果是**灾难性**的:
| 指标 | NM2.1(无门) | NM2.1+覆盖门 |
|---|---:|---:|
| A 可回答正确率 | **100.00%** | **6.25%** |
| A 已知问题被误拒率 | 0.00% | **7.50%** |
| A 未知拒答率 | 56.67% | 43.33% |
| B 回答正确率 | 68.75% | 75.00% |
| C 未知泄漏率 | 75.00% | 62.50% |
| C 可回答正确率 | 65.00% | 65.00% |
| D 重启持久化 | 通过 | 通过 |
**门把几乎全部可回答问题都拒掉了**(可回答正确率 100.00% → 6.25%),而它本该修好的 C 段泄漏
只从 75.00% 降到 62.50%。**已把 NM2.1 的 `memory_coverage_gate` 改回 `false`**,恢复为已验证的可用状态
(52 个单元测试通过)。属性头工件保留在 `checkpoints/memory_attribute_head/`,未随包启用。
### 两个根因(离线评测掩盖了它们)
1. **输入表示不匹配**:属性头是在**裸提问文本**的冻结键上训练的,而运行时 `read()` 收到的是
`self.runtime.v2_query_key`,它是 **`_encode_model_key(input_ids)`**,其中 `input_ids` 是
**套了 chat 模板的完整提示**(还含系统前缀)。头在分布外的输入上工作,argmax 基本是随机的 ——
所以它既误拒了大量可回答问题,又没能拦住该拦的。
2. **库覆盖索引不可靠**:门要求 `user::<属性>` 出现在 `bank.active_by_conflict` 里。A 段(v6 mega 事实)
的文本很多**无法被 `infer_memory_metadata` 解析出属性**,于是冲突键为空 → 门把所有这类问题一律拒掉,
这正是可回答正确率崩到 6.25% 的原因。
**离线 100.00% / 0.00% 是真实的,但它只证明了机制在"裸提问键 + 24 个可解析属性"这个条件下成立,
并不等于接上运行时就能用。** 这一点必须在任何对外表述里讲清楚。
### 正确的下一步(按顺序,每步都有验收口径)
1. 用**运行时实际喂进来的那个表示**(chat 模板编码)重建训练集,而不是裸提问键;
验收:门开启后 A 段可回答正确率不低于 99.00%。
2. 把覆盖判断从"必须有结构化冲突键"改为"结构化键 **或** 检索到强匹配记录",覆盖
`infer_memory_metadata` 解析不出属性的真实事实文本;验收:A 段已知问题被误拒率 ≤ 1.00%。
3. 两项都达标后再开 C 段验收:未知泄漏 ≤ 5.00%(现为 75.00%)。
## 6. 集成第二轮:修好一半 —— 目标域泄漏 0.00%,但换域即崩
第一轮失败后定位到真正原因(并不是我先前猜的"输入表示不匹配":`retrieval_ids = memory_query_input_ids
if ... else input_ids`,喂给 `v2_query_key` 的就是**裸提问**,与训练一致)。真正的机制缺口是:
**`read()` 返回空记录后不会停止,`_build_text_prefix` 会继续落到旧版 16 槽注入路径**,把无关记忆又塞回去。
所以在 `_build_text_prefix` 里加了短路:覆盖门拒绝时直接返回 guard 前缀,不再回落。
效果(C 段,48 用例,24 同形候选 —— 也就是属性头训练覆盖的那个域):
| C 段指标 | NM2.1(无门) | NM2.1+门+短路 |
|---|---:|---:|
| **未知泄漏率** | 75.00% | **0.00%** |
| 可回答正确率 | 65.00% | 65.00%(无损失) |
| 答成别的属性 | 35.00% | 35.00% |
| 活跃记录 | 23/24 | 23/24 |
**但在 A 段(v6 mega 事实)上是灾难**:可回答正确率 **0.00%**、已知被误拒率 7.50%、总体 12.73%。
根因已测定:
* A 段事实里 **94.03% 是"这是普通对话噪声…不需要长期保存"**,只有 **5.97%(63/1056)**能解析出属性;
* 更关键的是 **A 段的属性空间("备用联系人"等)完全不在属性头训练的 24 个属性之内**,头对 A 的提问只能
输出那 24 个之一 → 必然不在库里 → 门把一切都拒掉。
也就是说:**这个机制目前是"域内可用、跨域不可用"**。已把 NM2.1 的 `memory_coverage_gate` 改回 `false`
(52 个单元测试通过),并在 `memory_config.json` 里记下"已验证域"与"阻塞点"。
### 正确的最终形态:开放词表的属性匹配
不要再做"24 类闭集分类头",而是**把提问与库里真实存在的属性名做匹配**:
* 库里有哪些属性 → 从 `bank.active_by_conflict`(或记录上的 `attribute` 字段)取,随写入动态变化;
* 打分 = `similarity(query_key, encode(attribute_name))`,对**每个存在的属性**算一次,取最大;
* 低于阈值或库里属性为空 → 拒答。
这样词表不需要预训练固定,新写入的属性自动纳入。验收口径不变:**先保证 A 段可回答正确率 ≥ 99.00%
且已知问题被误拒率 ≤ 1.00%,再要求 C 段未知泄漏 ≤ 5.00%。**
## 7. 产物
| 文件 | 内容 |
|---|---|
| `analyze_abstention_separability.py` / `abstention_separability.{json,md}` | 第 1 节:分数几何不可分(四打分器 AUC 与操作点) |
| `analyze_query_attribute_classifier.py` / `query_attribute_classifier.{json,md}` | 第 2 节:封闭词表 100%/0%,含按属性拆解 |
| `holdout_6.json` / `holdout_12.json` | 第 3 节:开放集最大概率 AUC 与操作点 |
| `none_class_result.json` | 第 3 节:显式 NONE 类,100.00% 拒答 / 0.00% 误拒 |
| `train_attribute_head.py` / `attribute_head_training.json` | 第 3 节工件:属性头训练脚本与指标 |
| `checkpoints/memory_attribute_head/attribute_head.pt` + `attribute_head_meta.json` | **可部署工件**(当前未在包内启用) |
| `nm2_1_gate_*.json/md`、`nm2_battery_comparison_gate.{json,md}` | 第 5 节第一轮失败证据 |
| `nm2_1_gate2_runtime_e2e.{json,md}` | 第 6 节:目标域泄漏 **0.00%** |
| `nm2_1_gate2A_e2e.{json,md}` | 第 6 节:跨域崩溃证据(A 段 0.00%) |