不用反向传播,能走多远?——我在 MNIST 上把零梯度网络做到了 97.48%
先说结论:97.48%。
一个普通的反向传播 CNN 在 MNIST 上能到 99.37%(对照组代码在 mnist.py,整个仓库里唯一用反传的文件)。我最好的无反传网络是 97.48%。这 1.9 个点我追了很久,没追上。但为了追它,我把五条路线各跑了一遍全程,其中一些发现比那 1.9 个点有意思得多。
MNIST 是深度学习界的"Hello World":7 万张 28×28 的手写数字图片,任务是把每张图分类成 0 到 9 中的一个数字。听起来简单,但正因为简单,谁几分一目了然,是个理想的实验场。
先补课:反向传播是什么,为什么大家离不开它
你训练神经网络时,模型会先猜一个答案,猜错了,然后呢?它得知道"错在哪、每个参数该背多少锅"。
反向传播就是这个"背锅分摊"算法:它从最终的错误出发,一层一层往回算,算出每个参数对错误负多大责任(这个责任值叫"梯度"),然后让每个参数朝能减小错误的方向挪一小步。重复几万次,网络就学会了。深度学习的全部繁荣,基本都建立在这一个算法上。
那不用它行不行?这就是我给整个项目定的规矩:一行反向传播都不写。 没有梯度,没有优化器,没有训练循环。参数要么随机生成后直接冻住,要么用更"生物"的局部规则算出来。
听起来像自废武功。但有一点诱惑:人脑也不用反向传播,神经元只接收本地信号,照样学会了看东西。所以这个实验顺便回答了一个问题:不靠全局的"背锅分摊",纯靠本地规则和统计规律,到底能走多远?
另外我给自己立了一条实验纪律:每个实验跑之前先写好判读标准,比如"如果 A 没超过 B 两个点,就判 A 不成立",跑完不许改。这救了我好几次,人太容易在看到数字之后给结果找说法。
爬梯子的过程:从 71.9% 到 88.38%
起点很难看。最原始的配置只有 71.9%。然后一级一级爬:
| 改动 | 精度 | 给入门者的一句话解释 | 代码 |
|---|---|---|---|
| 原始配置 | 71.9% | 图片像素直接扔进投票系统 | threshold_net.py |
| 随机卷积核 + 池化 | 82.99% | 用一堆"随机滤镜"找边缘、找纹理 | conv_net.py |
| 竞争学习核 | 85.58% | 滤镜不再随机,改成从数据里"学"出来 | kmeans_conv.py |
| ZCA 白化 | 88.38% | 先把数据"校准"再学,效果大幅提升 | kmeans_conv2.py |
| 单层 64 核基线 | 89.16% | 后续所有实验的对照平台 | readout_capacity.py |
这一节反复出场的投票系统本体是 neuro_net.py,全部图像实验共用的读出引擎。
逐个解释一下这三级台阶。
随机卷积核:卷积核你可以理解成一个小小的 7×7 滤镜,在图片上滑动扫描,对每个局部区域打分。训练 CNN 时这些滤镜的参数靠反向传播学;而我直接用随机数生成,生成完就冻结,一个参数都不更新。哪怕滤镜是随机的,只要配上池化(把相邻区域的最高分留下来,丢掉细节),网络就能提取出"这块有笔画"之类的粗略特征,精度直接跳到 82.99%。
竞争学习:随机滤镜毕竟瞎猫碰死耗子。能不能让滤镜从数据里学点东西,但又不搞反向传播?我用了球面 k-means,思路很朴素:从训练图片上随机剪几十万个小图块,然后做"归类"——每个滤镜(此时叫"核")认领一批长得最像自己的图块,然后挪过去变成这批图块的平均样子,反复几轮,就像把相似的照片扔进同一个抽屉,再把抽屉标签更新成这批照片的平均脸。核与图块之间"认领—平均"的争夺就是"竞争学习"。这一改动让精度到 85.58%。
ZCA 白化:这一步最违反直觉,收益却最大。白化是一种数据预处理:原始图片里相邻像素高度相关(亮的地方周围也亮),而且亮度整体忽高忽低。白化相当于把每张照片的对比度拉平、把像素间的关联解耦,让数据变成"干净均匀的原料"。很多教材一笔带过这一步,但我实测它带来了 3 个点的提升。原料干净,后面随便怎么加工都更香。
每一级台阶背后都躺着一排死掉的尝试。比如数据增强(把图片旋转、平移、加噪声来扩充训练集)没涨点反而掉了 3 个点——它买来的只是抗平移能力,代价是原始精度。比如核数量从 64 加到 256,精度反而微降。到这里我停了很长时间。88.38%,不上不下。
关键一跳:不换特征,换"读出"
要理解这一跳,先看这套系统怎么"读出"答案。
我的网络没有 softmax,没有输出层权重。它的读出方式是统计投票:几千个神经元,每个都先看一遍训练数据,记住"我见到数字 5 时平均放电多少、见到别的数字时平均放电多少",取个中间值当阈值。推理时每个神经元举牌:过阈值举 +1,不过举 -1。几千张牌加起来,哪个数字得票最高就输出哪个。
这套投票系统(我沿用了一个叫 NeuroNet 的开源项目的设计)简单、快、无梯度,但表达粗糙。于是我做了一件出乎意料有效的事:特征一个字不动,只把投票换成一个叫 LDA/ridge 的经典线性分类器。
给入门者解释一下这个东西:训练一个线性分类器,本质是找一条直线(或超平面)把不同类的数据分开。反向传播的做法是随机起点、一点点挪、试错几万步。而 LDA 和 ridge 这两个诞生于深度学习之前几十年的统计方法,可以直接套公式一步解出答案——就像解一元一次方程,移项就完事,不需要任何迭代。所以它依然符合我的"无反传"规矩:没有梯度,只有一次矩阵求解。
结果:88.38% 变成 97.48%。涨了 9.1 个点。这一跳的完整消融(投票 vs ridge vs LDA)在 closed_readout.py 里。
同一种特征,同一批数据,只换了读出方式。这是我从整个项目里得到的最重要的认知:大家(包括当时的我)总盯着特征提取不放,但读出质量才是最大的杠杆。打个比方,特征是米,读出是厨艺,我之前一直在疯狂换米,锅还是那口锅。
作为对照,Hinton(反向传播之父)晚年提出的 Forward-Forward 算法——一个专门为"告别反向传播"设计的精巧学习规则——在同一套框架下做到 97.22%。说实话这个数字出来时我心情复杂:一位大师的精巧规则,被我"随机核+解一次方程"朴素地追平还反超了一点。
五个思路,五个交代
爬完梯子,第二阶段我列了五个更有野心的思路,每个都写代码、跑全量、按事先写好的判据给结论。总表如下(后三个实验换了个任务:给莎士比亚剧本文本预测下一个字符,检验无反传方法在语言上的表现):
| 思路 | 结果 | 判读 | 代码 |
|---|---|---|---|
| 闭式线性读出(上一节讲的) | 97.48% | 成立 | closed_readout.py |
| Forward-Forward | 97.22% | 证伪(没超过更简单的闭式读出) | ff_net.py |
| Hopfield 检索式语言模型 | 0.4878 | 机制成立,零增益 | hopfield_lm.py |
| eligibility trace 三因素规则 | 0.2668 | 机制成立,软选择证伪 | eligibility_lm.py |
| 逐层贪婪深度堆叠 | 90.23% | 成立 | greedy_deep.py |
挑两个讲讲,正好都是入门者不太会碰到的东西。
Hopfield 网络与"联想记忆":Hopfield 网络是 1980 年代的经典模型,思路完全不同于现代网络——它不计算,而是"记忆+联想"。像你听到"床前明月光",脑中自动补出"疑是地上霜"。我的实验是:把训练文本里所有"前 4 个字符 → 下 1 个字符"的组合全部存起来(89 万条),预测时拿着当前 4 个字符当线索,去记忆里"联想"下一个字符。这类检索由一个叫 β 的参数控制软硬:β 越大,越死板地只找完全匹配的记忆。结果 β=8 的"软联想"精确追平了老老实实做统计的 n-gram 查表(n-gram 就是统计"上一个字是什么时,下一个字最常见是什么"的频率表)。听起来该高兴?其实是零增益——费劲证明白的只是"联想检索等价于一次平滑的查表"。更打脸的是 β=64 的"硬联想"反而掉了 1.4 个点:一旦记忆库里没有相似条目,模型会硬抓一个不相干的记忆来凑数。经典版 Hopfield 全军覆没,预测 100% 塌缩成同一个字符。
逐层贪婪:这是五条思路里唯一超过单层基线的,也是唯一"成功加深"的。思路来自 2006 年 Hinton 的深度学习黎明期:第一层先用竞争学习学好"笔画字典"然后冻结;第二层不碰原始图片,只在第一层产出的特征图上再学一个"由笔画组成的更复杂部件"的字典。90.23%,过了我事先定的预注册线。但这组实验真正值钱的结论是对照出来的:同样的两层结构,第二层如果连接方式是随机的,只有 87%——这个对照出自 unit_soup2.py,深度实验本身在 greedy_deep.py。先学底层、冻住、再在上层学更抽象的东西,这个顺序是深度红利能否兑现的开关。而且深度不是白来的:把图片平移 2 个像素再测,鲁棒性从 52.25% 掉到 46.07%。"精度换鲁棒性"这个交换在项目里出现了三次,每次方向都一样。
负结果也值钱
把证伪的部分单独列出来,因为我觉得这部分比成功案例更稀缺——网上极少有人公布失败的尝试,但对你可能最有用:
- 数据增强在随机投影网络下降低原始精度(80.49% → 77.17%)——
augment.py - FFT 幅度谱做特征,判别力暴跌(简单说:把图片转成"频率成分"表示,虽然位置怎么挪都不变,但丢了关键的"笔画在哪"信息)——
fft_features.py - 随机局部感受野,加到 8192 个神经元也只有 67.8%——
local_net.py - 二阶统计读出,精细的贝叶斯阈值反而比中点基线差 3.4 个点——
second_order.py - 经典 Hopfield 的两种编码方式全部失败——
hopfield_lm.py - 加大核数(64→256)、加宽神经元(8192→16384),两个方向都饱和了——
kmeans_conv2.py/readout_capacity.py
每个负结果对应的代码都在仓库里,跑一遍就能看到它们是怎么死掉的。
如果你在自己项目里碰到类似的数字,希望这张清单帮你省几次试错。
三条贯穿全部实验的结论
读出质量 > 表示深度 > 学习规则复杂度。 三者对最终精度的影响就是这个排序。换读出一跳 9 个点,堆深度 1 个点,而更复杂的学习规则(Forward-Forward、三因素规则)净贡献是零或负。先把读出做好,再谈别的。
位置信息是序列的货币。 做语言实验时,一旦模型"看见字符但看不见字符的先后顺序"(比如只看出现了哪些字符),精度从 0.49 崩到 0.07。两次独立实验,同一个结论:对序列任务来说,字符在哪比字符是什么还重要。
可行性和增益是两个问题。 Hopfield 检索、生物启发的三因素规则,机制都跑通了,一个都没带来增益。机制成立只是入场券,不保证有增长。
顺手拿到的三个特性
无反传架构附赠了一些反向传播网络不太容易有的性质。
零遗忘:学一个新数字,只需要 5 张图,旧类精度纹丝不动。原因很直接——学新类时只是新增一些"评委",已有评委的数据一个都不动,而反向传播训练时要改所有参数,容易把旧的冲掉(这在大模型圈是个正经的研究难题,叫"灾难性遗忘")。复现在 few_shot.py。
睡眠剪枝:训练后让网络自己跑几轮重组(模拟睡眠),精度再涨 3.8 个点。复现在 wake_sleep.py。
选择性预测:网络不但给答案,还能给自己打分。用投票的领先幅度当"把握值",把测试题分成"有把握"和"没把握"两堆,有把握的那 60% 的题上精度 99.1%。不确信的题目可以让它说"我不知道",这在医疗、自动驾驶等高风险场景是实打实的能力。复现在 selective_pred.py。
最后,诚实的那部分
97.48% 和 99.37% 之间差的 1.9 个点,我不打算假装它不存在。在 MNIST 上,反向传播依然是天花板最高的方法,没有悬念。这个项目证明的不是"无反传更好",而是两件更微妙的事:无反传离天花板比你想象的近得多;以及,在"几分"这个问题之外,还有一整套关于表示、读出和位置信息的结论值得挖——这些结论对用不用反向传播的你,都有参考价值。
文中出现的全部实验代码都放在配套仓库里(threshold_net.py 到 hopfield_lm.py,共二十来个脚本),每个文件顶部写明了设计动机、事先定好的判读标准和预期数字,克隆下来 python 文件名 就能复现,包括那些失败的实验。仓库里还有一份完整的实验笔记 notes.md,约二十个实验的全部数字和失败机理分析都在里面。
代码仓库:https://gitee.com/hylab/net-lab
这是系列第一篇。后面计划拆开写几篇单篇:闭式读出那 9 个点的细节、逐层贪婪的路由对照实验、Hopfield 那场教科书级的塌缩。如果你对哪一篇最感兴趣,或者你在无反传方向踩过别的坑,评论区告诉我。