5. 静默失效清单(本项目最贵的资产)
★
7.2k 字 ·
源文件 PROJECT.md 第 435 行起 ·
goal discipline arch optim selfcorrect
以下每一条都真实发生过、且当时没被察觉。它们不报错,只让实验结果无声地变得无意义。
| # | 失效模式 | 后果 | 检测方式 | |
|---|---|---|---|---|
| 1 | 读出窗口测错(用全局时钟跨两遍定位) | 测到错误阶段,结论全废 | 任务显式声明窗口 | |
| 2 | 词分量叠加 → one-to-many | 上界假象 | 检查标签是否唯一决定激活模式 | |
| 3 | 驱动电流超阈值饱和 | 计数不反映差异 | 扫描驱动强度 | |
| 4 | 不同粒度的特征上限相同 | 误判瓶颈位置 | 同时测多种粒度 | |
| 5 | 任务输入泄露答案 | 所有延迟 100%,任务无效 | 静默区必须全零 | |
| 6 | timespans 未传/传错 | loss 停在 ln2,学不到 | 断言初始 loss ≈ 0.693 | |
| 7 | 训练集内准确率当上界(过拟合) | 误判上限 | 一律用留出集/CV | |
| 8 | 类别不平衡 / 随机基线假设错 | 数字无法解读 | 实测基线 | |
| 9 | 联合结构泄露(边缘统计量看不见) | 泄露检测全 PASS,任务已废 | 用任务语义约束(structure_actives) | |
| 10 | backbone_units=128(ncps 默认) | 所有模型都学不起来 | 见 §6.1 | |
| 11 | 答案不在输入里(被覆盖) | 闭式解上界 = 52.8%,所有模型 50% | identifiability_spec 可识别性检查 | |
| 12 | 打断改变了标签分布 | 常数预测就能拿 93.6% | 检查各条件下的标签正例比例 | |
| 13 | 多项检查共用同一 rng 流 | 0.1% 的坏运气被固定成必然事件 | 每项检查用独立 rng | |
| 14 | 余弦退火把 lr 降到 0 后锁死 | 训练中断但看不出,结果逐位重复 | lr 设下限;检查结果是否重复 | |
| 15 | 扰动过强把样本毁掉 | 字内距离 ≈ 字间距离(比值 1.05) | 量化"字内/字间"距离比 | |
| 16 | "零延迟"不是捷径 | 指令与探针从不重叠(共存 0.0%) | 直接量共存比例 | |
| 17 | 只看单次运行 | 约 12% 的种子完全失败 | ≥3 种子,报成功率 | |
| 18 | 文本编码不匹配(UTF-16 vs UTF-8) | 日志解析全部失败但不报错,正则一条都匹配不上 | 读文件前嗅探 BOM;或让实验直接写 jsonl | |
| 19 | 单一 lr 上的"能力不足"结论 | 把"学习率不对"误判成"容量不够"(代价见 §6.14) | 任何"某能力不行"必须在多个 lr 下确认 | |
| 20 | p.detach().cpu() 不是副本(对已在 CPU 的参数) | state_dict 与参数共享存储 → 存了之后改参数,存档跟着变 → "恢复"的是改后的值 | 存档一律 .clone();检查"存取"时必须同时比对"vs sd"和"vs 基准" | |
| 21 | 优化器介入"存取正确性"检查 | load_state_dict 写对后,Adam 又按自己的动量改了参数 → 参数检查 PASS、输出检查 FAIL | 检查存取时丢掉优化器(del opt);或在无优化器路径上验证 | |
| 22 | try/except 吞掉对照的失败,然后报告全称结论 | exp10 的 ALL_KINDS 有 6 个,ssm/lstm/gru 全部 NotImplementedError 被吞,产物 JSON 只有 3 个键 —— 但汇总段仍打印"没有任何模型在零输入下呈现有结构的自主活动"。一个全称否定建立在 3/6 的样本上,而且是一整条路线(§7.2)的依据 | 对照必须全部产出,任一失败即非零退出;禁止 except: continue 后继续汇总 | |
| 23 | 把"编码器偏置"当成了"零输入" —— 但结论下反了 | exp10 写的是 z = m.enc(zero),而 enc = nn.Linear(n_in, e) 默认 bias=True → z = b_enc(可学习常数向量)。我一开始指控这是"假零输入、错的"——这个指控本身错了:读 core/tasks/match.py 第 129 行,x = np.zeros((T, N_SENS)),延迟期什么都不写,所以 cell 在训练时收到的正是 b_enc。exp10 的设置恰好匹配训练分布。真正该被批评的是措辞(叫它"零输入"),而不是设置;而我后来"修正"成的 z = 0 反而更 OOD —— 实测:zero 模式的敏感性 8.50 vs task 模式 2.27,差 3.7 倍 | 描述状态动力学时必须说明输入是哪个分布的:enc(0)=b_enc(延迟期真实输入)还是 z=0(cell 从没见过)。两者结论可能相反 | |
| 24 | 拿未训练网络的动力学当架构性质的证据 | exp10_autonomous.py 没有任何训练循环(新建 → eval() → 直接跑)。所以"零输入下冻结、有效秩 5/16"是随机初始化网络的性质,而 §7.2 的真正判据含训练("任务准确率下降 <10pp")。这是类别错误,而且它支撑了两个月的路线判断 | 架构性结论必须在训练后测量;exp28 实测:同一个 cfc,未训练 retention = 0.0000,训练后(acc 0.967)retention = 0.7975 | |
| 25 | 指标方向反了,而且没人发现 | "有效秩"测的是轨迹路径维度不是状态容量:若 h 收敛到不动点,H ≈ [h₀..h_k, p, p, …, p],中心化后那 T−k 行只贡献一个方向 → eff_rank ≈ 1 + 瞬态方向数,与状态维度无关。后果:能完美保持静态记忆的系统 → eff_rank=1 → 判"冻结";注入噪声让它乱走 → eff_rank 上升 → 判"有自主活动"。它惩罚我们想要的,奖励我们不要的。 | 改用成对指标 + 校准对照:retention(跨初态散布比)+ activity(末段相对步长),并必须报出噪声(应 ≫1)/ 线性衰减 0.99^t(应 =0.049)/ 完美保持(应 =1.000)三个锚点 | |
| 26 | .bat 文件编码 ≠ 系统代码页 —— 乱码被当成命令执行 | tools/set_tdr_delay.bat 存成 UTF-8 + LF,而中文 Windows 的 cmd.exe 用 GBK(936) + 需要 CRLF 读它。某些中文字符的 UTF-8 字节被 GBK 解读后**恰好含有 & `\ | " 等特殊字节**,于是 REM 注释行被**拆成命令执行**,if (...) 块结构错乱,**reg add` 根本没跑成(注册表里键都没建)。比第 18 条更糟:18 条是"解析失败",这条是"执行垃圾**"。 | .bat 一律存 GBK + CRLF;写完必须验证:GBK 解码成功、UTF-8 解码失败、U+FFFD 计数为 0、裸 LF 计数为 0。或干脆改用 PowerShell 脚本(.ps1 是 UTF-8 友好的) |
| 27 | ★ 用与任务无关的「抽象指标」代替任务相关测量 —— 连续栽三次 | 为了判定"状态动力学是否健康",我先后用了三套抽象指标,三套全错:<br>① 有效秩(§5 第 25 条):测的是瞬态丰富度,惩罚稳定记忆、奖励漂移<br>② retention(跨初态散布比):混沌同样保持散布 —— 分不开"记忆"和"混沌"<br>③ 随机初态线性探针(h_T → h_0):把"非线性"和"信息丢失"混为一谈。实测即使 T=5(任务真实需求),R² 也只有 0.065,而同一个模型的任务准确率是 99.2%<br>后果:三套指标一致指向"状态已死",我据此差点写下"§7.2 需要 activity_loss"。而任务相关的探针显示延迟期内容保持 100% | 判据必须写成"任务需要的内容还在不在",不能写成"状态看起来活不活跃"。<br>具体做法见 §6.13:把训练好的模型冻结,只训一个线性分类器从 h_t 预测任务自己的标签(指令 / 探针),看它在各时刻的可读出性。<br>这条比前几条都重要 —— 因为它错得最隐蔽:抽象指标会给你一个看起来很科学的数字,而那个数字与你要回答的问题无关。 | |
| 28 | ★ torch 线程数超订:并行跑 N 个进程时,每个都按物理核数开线程 | 实测:6 个并行实验进程 × 默认 10 线程 = 60 个线程抢 16 个逻辑核 → CPU 100% 满载而 GPU 只有 37%。修 set_num_threads(1) 后:同一硬件、同样 6 个进程,GPU 37% → 81%。<br>项目里早就记过这件事(exp08_reproduce_winner.py 注释:"exp06 在 GPU 下 set_num_threads(1),exp07 没设(默认 10)")—— 但 exp13 没设,于是又踩了一次。 | 任何并行实验脚本都必须显式 torch.set_num_threads(1),且要在任何 torch 计算之前。判据:并行进程数 × 线程数 ≤ 逻辑核数。另外单进程也测过:threads=8 比 threads=1 更慢(173 vs 155 ms/迭代) | |
| 29 | ★ 忘了 .to(device) —— 整个诊断在 CPU 上跑,却和 GPU 的结果对比 | diag_seed_failure.train_eval 第一版建了模型就直接用(Net(...) 不带 .to(dev),model(X) 的 X 也在 CPU)。于是:<br>exp13(GPU)seed=4 → acc 0.507<br>本脚本(CPU)seed=4 → acc 0.931 / 0.931 / 0.902(三次)<br>我据此写了"同进程内连续跑多种子会泄漏状态"这个结论 —— 方向完全错了。真因是 CPU 与 GPU 是两套数值路径。<br>脚本跑通了、数字看着合理(0.507/1.000/0.858)、判读还很自信。 教科书级静默失效 | 任何"和另一个实验对比"的脚本,必须①显式 .to(device),②在输出里打印实际设备。<br>本条的代价:一整轮错误结论 + 两条错误的 §11 纪律(现已更正) |
#★ 第 20/21 条详述:一个"看起来像保存恢复坏了"的组合坑
症状(core/darlin/hybrid.py 的 tests_hybrid()):
参数层面:恢复后与 sd 差异 0.000e+00 <- 通过
参数层面:恢复后与基准差异 1.645e-01 <- 失败
输出层面:恢复后与基准差异 4.260e-01 <- 失败看起来像 load_state_dict 有 bug。
实际是两个独立的坑叠在一起:
坑 20 —— HybridNet.state_dict() 里写的是
p.detach().cpu()。对已经在 CPU 上的参数,这个表达式返回的是
共享存储的视图,不是副本。于是:
存 sd -> 打乱参数 -> sd 里的值也跟着变 -> "恢复"的是打乱后的值加 .clone() 即修复(sd 都是副本? True)。
坑 21 —— 测试 4 复用了测试 3 创建的 Adam,而 Adam 持有参数的引用
和动量状态。即使 load_state_dict 把参数写对了,后续 forward 又会
触发优化器按自己的状态更新参数。丢掉优化器后立即通过。
为什么这个组合特别危险:
| 特征 | 后果 |
|---|---|
| 只在"存了之后又动参数"时暴露 | 所有真实训练循环里都会中招 |
| 两个检查一个 PASS 一个 FAIL | 指向错误的方向(以为是 load 坏了) |
| 前向/梯度检查全 PASS | 训练时看不出任何异常,只有存模型时才发现 |
一般化的教训(与第 6、18 条同源):
凡是"接口约定没对上、而系统不报错"的地方,都要用断言钉死。 这里要钉两条:存档必须
clone();验证存取时必须隔离第三方 (优化器、随机数、缓冲)。
第 5、6、11、12 条是同一个根源的四种表现(答案的可获得性),值得合并成一个更强的检查族。 第 18 条与第 6 条(timespans 没传对)性质相同:都是"接口约定没对上,而系统不报错"。 这类问题应统一用断言处理 —— 接上就断言,不接就拒绝运行。 第 20/21 条是同一类:都是"看起来在工作,实际上没工作"。
发生了什么:experiments/tail_to_jsonl.py 用来把纯文本训练日志转成面板能读的
jsonl。它一直"运行正常"(不报错、不崩),但一行都没解析出来。
根因:results/exp14/run.log 是 UTF-16-LE —— 因为
PowerShell 的 > 重定向用系统默认编码(Windows 上是 UTF-16-LE,
带 BOM、每个字符之间夹 \x00),而 Python 的 print 输出是 UTF-8。
用 UTF-8 去读 UTF-16 文件,得到的是满屏 \x00 的垃圾:
'\x00 \x00 \x00 \x00 \x00 \x00 \x00 \x001\x00 \x00 \x00l\x00o\x00s\x00s\x00...'为什么危险:它不报错。 文件能打开、行能读到、字符串能处理 —— 只是所有模式都匹配不上,程序安静地什么都不做。
修法:读文件前嗅探 BOM。
def sniff_encoding(path):
head = open(path, class="tok-str">"rb").read(4)
if head.startswith(bclass="tok-str">"\xff\xfe"): return class="tok-str">"utf-16-le", 2
if head.startswith(bclass="tok-str">"\xfe\xff"): return class="tok-str">"utf-16-be", 2
if head.startswith(bclass="tok-str">"\xef\xbb\xbf"): return class="tok-str">"utf-8-sig", 3
return class="tok-str">"utf-8", 0更彻底的修法:让实验直接写 jsonl(core/logger.py 已经这么做了,
exp15 也已改成直接写)。绕开"文本日志 + 后处理"这一整类问题。
一般化的教训:
凡是"读别人的输出"的代码,都必须显式声明编码,不能依赖默认值。 Windows 上 PowerShell 重定向、Excel 导出、部分编辑器保存都可能产出 UTF-16。这与 §5 的其它条目同源 —— 不报错,只让结果无声地失效。
第 5、6、11、12 条是同一个根源的四种表现(答案的可获得性),值得合并成一个更强的检查族。 第 18 条与第 6 条(timespans 没传对)性质相同:都是"接口约定没对上,而系统不报错"。 这类问题应统一用断言处理 —— 接上就断言,不接就拒绝运行。
推论:core/task_base.py 的自检门必须不通过就拒绝训练,且必须做负向验证
(一个从没失败过的检查没有说服力)。