16. 判据 5 过线 + 「照抄屏幕」被治好(第 3 轮对照)
★★★
3.6k 字 ·
源文件 PROJECT.md 第 3172 行起 ·
goal ruler optim read external
#16.1 判据 5 第一次过 50%
| 跑次 | β=0 正确率 | 认字 | 留出字体 | emit | vs 纯模仿 30.2% |
|---|---|---|---|---|---|
| DE(旧) | 13.5% | 36.5% | — | 0.85 | 44.7% |
| s1(无内驱力 + 成本 0.15) | 12.5% | 54.2% | — | 0.17 | 41.4% |
| s2(成本 0.15) | 15.6% | 55.2% | 29.2% | 0.16 | 51.7% ✅ |
| s3(成本 0.40) | 2.1% | 34.4% | — | 0.08 | 7.0% |
★ s3 说明 0.40 的开门成本压过头了(门塌到 0.08、正确率掉到 2.1%)。 成本要温和;0.15 是当前最优点,但还没扫过 0.25。
#16.2 ★★ 病:它是拿屏幕当外部内存
diag_parroting.py 在 s2 权重上关掉回显(屏幕只剩目标字):
| 条件 | 整串正确率 | 认字 | d | emit |
|---|---|---|---|---|
| 基线 | 22.7% | 60.9% | 1.59 | 0.17 |
| 回显关 | 3.1% | 60.9% | 4.91 | 0.82 |
| 目标字第 4 刻消失 | 22.7% | 60.9% | 1.59 | 0.17 |
| 回显关 + 目标字消失 | 0.0% | 60.9% | 9.43 | 0.85 |
| 空白屏 | 0.0% | 8.6% | 10.66 | 0.94 |
准确说法不是"只会复述屏幕内容",而是:它把「我打到哪儿了」放在屏幕上,不放状态里。 回显一关,22.7% → 3.1%,距离 1.59 → 4.91。
★ 三个"复述"必须分开(严重程度差一个数量级):
| # | 形式 | 是不是问题 |
|---|---|---|
| ① | 复述目标字(看到「中」打 zhong) | 就是任务本身,没有别的做法 |
| ② | 复述自己的输出(每刻重看屏幕续写) | 是偷懒通路(上面这个病) |
| ③ | 复述老师(纯行为克隆,老师一走就崩) | 最严重,正是"拟合陷阱"的定义 |
#16.3 治②:训练期随机关回显(--echo-drop)
四个权重的统一对照(diag_parroting.py,n=96,同一把尺子):
| 权重 | drop | 基线整串 | 基线认字 | 无回显整串 | 抹掉目标字 | 换成别的字 |
|---|---|---|---|---|---|---|
| s2 | 0.0 | 20.8% | 59.4% | 2.1% ❌ | 11.5% | 10.4% |
| e30 | 0.3 | 23.4% | 55.5% | 25.0% ✅ | 6.2% | 10.9% |
| e50 | 0.5 | 21.9% | 51.0% | 22.9% ✅ | 5.2% | 6.2% |
| e70 | 0.7 | 16.7% | 40.6% | 16.7% | 6.2% | 7.3% |
★ 第 4 列才是分水岭(部署时本来就要关回显):2.1% -> 25.0%,12 倍。
★ 第 5、6 列是探针生效的指纹:四个权重全都低(5~12%)——
说明"抹掉/替换目标字"真的把那个字从屏幕上拿掉了。
训练期读数(2500 步,β=0 时):
| 跑次 | drop | 正确率 | 认字 | d | 门(该开/该关) | 留出字体 |
|---|---|---|---|---|---|---|
| s2 | 0.0 | 15.6% | 55.2% | 1.58 | — | 29.2% |
| e30 | 0.3 | 20.8% | 53.1% | 1.44 | 0.08/0.32 | — |
| e50 | 0.5 | 22.9% | 50.0% | 1.53 | 0.09/0.34 | — |
| e70 | 0.7 | 21.9% | 46.9% | 1.75 | 0.06/0.25 | — |
#★★ 结论:echo-drop 是一个权衡,不是纯改进
| 指标 | 随 drop 上升的变化 |
|---|---|
| 无回显正确率 | ↑ 大幅(2.1% -> 25.0%)—— 这是治好的病 |
| 认字能力 | ↓ 单调(59.4% -> 55.5% -> 51.0% -> 40.6%)—— 这是代价 |
| 有回显正确率 | 先升(15.6 -> 22.9),drop > 0.5 后回落 |
为什么"认字"会掉:回显本身是目标字之外的第二个视觉信号, 关掉它的那些回合里,编码器少了一份"这里有字"的监督。drop 越大, 这种"信息更少"的回合越多 -> 认字能力被稀释。
取舍点:0.3 ~ 0.5。 0.3 保住认字(55.5%),0.5 把无回显分数推到最高(25.0%)。 0.7 两个都受损,不要用。
★ 还有一个同向的好处:门的时机第一次分开了(该开 0.08 / 该关 0.32)。 s2 那一列的门是常开(0.85/0.94)。
为什么"关回显"不是"剥夺信息":按内外分离契约,本体感觉(82 维)里一直有 它自己上一刻发出的键。所以这是拿掉一条偷懒通路,不是把它眼睛蒙上。
#16.4 ⚠️⚠️ 这一节最贵的一个 bug(一个字符,毁掉整份诊断)
evaluate(blank_target=True) 我第一版写成了:
e._show_char = None if blank_target else (...) class="tok-com"># ✗ 错而 render() 里的约定是:
_show_char is None -> 用默认值(画目标字) ← 我以为是"不画"
_show_char == "" -> 不画 ← 这才是"不画"
_show_char == 别的字 -> 画那个字于是"目标字始终不出现"那一行什么都没改,报出 55.5% 的认字率 —— 屏幕上看不见字却认得出,与同一张表里"空白屏 6.2%"自相矛盾。
★★ 是靠这个矛盾发现的,不是靠检查代码。 教训:一张表里两个读数互相矛盾时,先怀疑工具,不要先怀疑结论。 我当时的第一反应是"它靠先验",第二反应是"上一回合的回显漏进来了", 都是错的;正确答案是"我的探针没生效"。
★ 配套教训:三态开关(默认 / 关 / 换)必须让"关"和"默认"取不同的值。 用
None表示"关"、同时用"没有这个属性"表示"默认",就是给这个 bug 留门。 正确写法是显式三分支(见evaluate现在的实现),并且加一条自检: 探针生效时像素数必须变(diag_parroting的判据 ② 就是干这个的)。
#16.5 三条可以复用的方法论
- "关掉某条通路"是比"看相关性"强得多的判别手段。
关回显、抹目标字、换目标字 —— 三个都是干预,不是观察。
认字那一列在三个条件下的分裂(55.5 / 6.2 / 10.9)一眼就能读。 - 判别器必须在同一张表里同时报"该高的高"和"该低的低"。 只报"关回显掉了多少",就读不出"它到底看没看目标字"。
- 一个探针要有"它没生效"的指纹。 这里的指纹是
目标字不出现 == 空白屏(两个都该是 6.2%)。两个本该相同的条件给出不同读数, 就是探针失效的信号。