
1. 为什么我劝你别跳过MLP直接上手Transformer这两年聊神经网络有个很有趣的现象新人入门第一件事是跑通一个Transformer好像不聊注意力机制就不算搞深度学习的。上一次出现这种风气是CNN火的时候大家觉得会调几个卷积层就很厉害。但每次我带新人或者帮人排查模型问题基本都会先问一句你的MLP手写过一遍了吗得到的回答绝大多数是没写过太简单了有什么好写的。可实际排查下来Loss不下降、梯度爆炸、过拟合、激活函数饱和、权重初始化不对——这些困扰大家的问题根源全都在MLP里而且是以最纯粹、最没有遮挡的形式出现的。你在MLP里把这些问题弄明白了后面看CNN、RNN、Transformer基本就是在看不同的人在给MLP打不同的补丁。再说直白一点。Transformer里那个Feed-Forward NetworkFFN本质上就是两层带ReLU的MLP残差连接的动机来自于梯度退化LayerNorm解决的是内部协变量偏移Multi-Head Attention解决的问题是每层只做一次全连接搞不定长程依赖。你不理解MLP的前向传播和反向传播上面这些话你只能停留在背结论的层面。一旦换个结构、换个任务遇到新问题你就不知道该从哪个方向定位了。这篇文章要做的就三件事第一把MLP的结构、数学原理和训练机制讲透第二用Fashion-MNIST服装分类这个经典项目带你把一个完整的MLP从零写到训练跑通第三把我这些年训练MLP踩过的坑、总结的经验一次性倒出来。适合谁看刚入门深度学习、想在动手之前把原理弄明白的人以及训练过一些模型但总觉得哪里没搞透的人。2. 单层感知机的天花板是怎么逼出多层的2.1 感知机一个简单的二分类器MLP的前身是Rosenblatt在1958年提出的感知机Perceptron。它的逻辑非常简单把输入向量 $x$ 和权重 $w$ 做点积加上偏置 $b$然后过一个阶跃函数比如大于0输出1否则输出0就是分类结果。对于二分类问题感知机的决策边界是一条直线二维情况下。能正确分类线性可分的数据这就是感知机的全部能力。但恰恰是这个简单的结构在1969年被Minsky和Papert一篇文章打进了冷宫。文章里指出了感知机的致命缺陷它无法解决XOR异或问题。XOR的数据分布下两类点无法用一条直线分开。这个看似学术的问题直接宣告了感知机这类单层线性模型的死刑也引发了神经网络历史上第一次寒冬。2.2 打破线性隐藏层和激活函数的意义XOR问题要怎么解决答案其实是我们现在看起来非常自然的做法加一层隐藏层让每个隐藏神经元先去学习输入的某种组合特征然后在隐藏层的基础上再做一次分类。但如果你只是单纯加层不引入非线性激活函数那么不管堆多少层整个网络仍然等价于一个线性变换。道理很简单$W_2(W_1x b_1) b_2$ 展开后还是 $W_{eff}x b_{eff}$ 的形式。层数再多也只是做了个矩阵乘法的大杂烩表达能力没有任何提升。这就是激活函数存在的根本原因——引入非线性让多层结构真正做到叠加特征而不仅仅是组合线性变换。有了非线性激活一个包含至少一个隐藏层的前馈网络理论上可以逼近任意连续函数。这就是著名的万能逼近定理Universal Approximation Theorem。2.3 万能逼近定理的另一面能逼近不等于好训练这里我得泼一盆冷水。很多人学了万能逼近定理之后特别兴奋觉得既然MLP这么万能那是不是什么任务都能做理论上的确如此但实际工程完全两码事。定理只告诉你存在这样的网络并没有告诉你需要多少神经元才够用训练能不能收敛到那个理想状态需要多少数据才喂得饱这个网络。所以在实践中MLP在图像、音频、文本这类高维原始数据上的表现其实并不好原因后面会细讲。但作为理解神经网络的起点它的价值不可替代。3. 前向传播、反向传播与训练机制把每个数学符号对应到代码3.1 前向传播数据从输入到输出的流动一个标准的MLP结构就是输入层 若干隐藏层 输出层相邻两层之间全连接。第 $l$ 层的计算可以写成两行$$z^{[l]} W^{[l]} a^{[l-1]} b^{[l]}$$$$a^{[l]} g^{[l]}(z^{[l]})$$其中 $a^{[0]} x$ 是输入$W^{[l]}$是权重矩阵$b^{[l]}$是偏置$g^{[l]}$是激活函数。拿Fashion-MNIST来说一张28×28的灰度图展平后是784个像素值这就是输入维度。假设隐藏层1有256个神经元那么 $W^{[1]}$ 的形状就是256×784每个神经元对784个像素各学一个权重。数据经过每一层维度逐层变化最后输出层有10个神经元对应10个服装类别。前向传播的代码写出来非常直观# 伪代码一个3层MLP的前向传播 def forward(x, W1, b1, W2, b2, W3, b3): z1 x W1.T b1 # (batch, 256) a1 relu(z1) # (batch, 256) z2 a1 W2.T b2 # (batch, 128) a2 relu(z2) # (batch, 128) z3 a2 W3.T b3 # (batch, 10) return z3 # logits注意最后一层没有激活函数直接输出得分logits。得分本身不能直接当概率读要通过Softmax转换成概率分布。这个细节很多人一开始会忽略后面讲损失函数时会说清楚为什么。3.2 损失函数模型好还是差的标尺有了输出就需要一个数值来量化预测和真实标签差多远这个数值就是损失。分类任务最常用的是交叉熵损失Cross-Entropy Loss$$L -\frac{1}{N}\sum_{i1}^{N}\sum_{k1}^{C} y_{i,k} \log \hat{y}_{i,k}$$这里 $y_{i,k}$ 是one-hot标签$\hat{y}_{i,k}$ 是Softmax后的预测概率。直觉上交叉熵衡量的是两个概率分布的差异。注意两个特性预测概率越接近真实标签损失越小它天然对置信度低的错误惩罚更大——模型明明很确定却分错了损失会非常难看的暴涨。实战中建议直接用框架封装好的CrossEntropyLoss内部已经把Softmax和损失合并了数值上更稳定。你自己先Softmax再算log容易碰到log(0)的问题。3.3 反向传播链式法则的工程实践损失算出来之后要做的是让每个权重往损失减小的方向挪一步。这个挪的方向由梯度决定。梯度怎么算靠反向传播Backpropagation。反向传播的本质就是高数里的链式法则只是按计算图的顺序反着走一遍。举个最小化的例子假设输出层的某个权重 $w$ 影响 logits $z$而 $z$ 影响损失 $L$那么 $\frac{\partial L}{\partial w} \frac{\partial L}{\partial z} \cdot \frac{\partial z}{\partial w}$。沿着计算图从后往前每一层把损失对当前层输入的梯度传给上一层逐层求出所有权重和偏置的梯度。这也是为什么框架里只要一行loss.backward()就能把整个计算图里所有参数的梯度都算好。参数更新用梯度下降$$w : w - \eta \cdot \frac{\partial L}{\partial w}$$$\eta$ 是学习率。学习率设大了参数来回震荡不收敛设小了半天挪不动。后面调参部分会专门展开。4. 激活函数、参数初始化与优化器三个直接影响训练成败的部件4.1 激活函数选型为什么是ReLU而不是Sigmoid激活函数是整个网络里非线性的来源但不同激活函数的训练表现天差地别。直接给结论隐藏层优先用ReLU输出层分类用Softmax回归任务输出层不用激活函数。Sigmoid有个致命缺点输入绝对值稍大输出就饱和在0或1附近梯度趋近于0。梯度一旦消失梯度下降就没法更新底层的权重了——浅层网络根本学不动这就是梯度消失的核心来源。ReLU$\max(0, x)$计算简单正半轴梯度恒为1能有效缓解梯度消失是当前MLP隐藏层的默认选择。但它也有坑神经元一旦在负数区间输出恒为0梯度就会一直是0这个神经元就死了。学习率过大的时候ReLU死亡尤其常见。缓解手段包括用Leaky ReLU、控制学习率、加BatchNorm。4.2 参数初始化全零初始化为什么是灾难权重初始化表面上看是个细节实际上直接影响网络能不能训起来。最经典的错误全部权重初始化为0。这样做的话同一层所有神经元的前向输出完全一样反向传播的梯度也完全一样更新后依然一样——对称性无法打破网络等于退化成一个神经元不管多宽都没用。现在通用的做法是方差保持的初始化方法。Xavier初始化适合Sigmoid/Tanh这类饱和激活函数KaimingHe初始化针对ReLU设计。PyTorch里nn.Linear默认用的就是Kaiming均匀分布初始化除非有特殊需求否则不用额外处理。4.3 优化器选择SGD、Adam和它们背后的直觉优化器是用梯度更新参数的具体策略。传统SGD只用当前梯度乘以学习率而Adam给每个参数维护了一阶动量指数移动平均和二阶动量梯度平方的移动平均相当于自适应地为每个参数调整学习率。实操中我的经验是项目初期、想快速看到模型能不能收敛直接用Adam学习率默认0.001省心想要在收敛后把精度再往上抠可以切到SGD加Momentum配合学习率衰减常能比Adam多提升一两个点小批量训练、数据不太多的场景Adam更稳不容易中途飞掉。5. 动手实战用PyTorch搭一个MLP完成服装分类5.1 为什么选Fashion-MNIST原本的MNIST手写数字分类已经被用烂了而且任务太简单——随便一个MLP都能到98%以上根本看不出模型好坏。Fashion-MNIST是MNIST的升级版同样是28×28灰度图、10个类别但内容是衣服鞋包纹理、轮廓差异更细噪声也更大。MLP在Fashion-MNIST上大概能到88%~90%而CNN能做到94%左右这个差距恰好能让你直观体会到为什么图像任务需要卷积。数据集包含60,000张训练图和10,000张测试图10个类别分别是T恤/上衣、裤子、套头衫、连衣裙、外套、凉鞋、衬衫、运动鞋、包、短靴。5.2 数据预处理归一化为什么必须做图像数据喂给网络前先做两步操作转成Tensor、归一化。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), # 像素从[0,255]变为[0,1]的Tensor transforms.Normalize((0.5,), (0.5,)) # 映射到[-1,1] ]) train_dataset datasets.FashionMNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.FashionMNIST(root./data, trainFalse, downloadTrue, transformtransform) train_loader DataLoader(train_dataset, batch_size128, shuffleTrue) test_loader DataLoader(test_dataset, batch_size256, shuffleFalse)归一化的意义在于把不同尺度的特征拉到同一量级。如果输入像素是0到255而权重初始化范围很小前向传播的加权和会很大激活函数容易饱和梯度就不好传了。归一化之后输入分布相对稳定训练会顺畅很多。5.3 模型定义从784到10的逐层压缩class MLP(nn.Module): def __init__(self, input_dim784, hidden_dims[256, 128], num_classes10): super().__init__() layers [nn.Flatten()] # 28*28 - 784 prev_dim input_dim for h in hidden_dims: layers.append(nn.Linear(prev_dim, h)) layers.append(nn.ReLU()) prev_dim h layers.append(nn.Linear(prev_dim, num_classes)) self.net nn.Sequential(*layers) def forward(self, x): return self.net(x) model MLP() criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001)这里有个细节值得解释为什么隐藏层维度逐层递减784→256→128→10因为我们的目标是从784维的像素空间逐步提炼出更高层、更抽象的特征最后在10维上做分类决策。虽然也可以做窄-宽-窄的瓶颈结构但对这个任务来说逐层压缩是最稳的设定。还有一种常见的偷懒写法是nn.Sequential(nn.Flatten(), nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 128), nn.ReLU(), nn.Linear(128, 10))效果一样封装成类更方便后续改动和检查每层的输出。5.4 训练循环与结果分析epochs 10 for epoch in range(epochs): model.train() running_loss 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) # 前向传播 loss criterion(outputs, labels) # 计算损失 loss.backward() # 反向传播 optimizer.step() # 参数更新 running_loss loss.item() * images.size(0) epoch_loss running_loss / len(train_loader.dataset) print(fEpoch {epoch1:2d}, Loss: {epoch_loss:.4f})这个代码跑完10个epoch训练损失会从最初的0.5左右降到0.18左右测试准确率大概在89%上下。有几个观察点第1个epoch损失下降非常快说明网络在快速学习像素级的粗特征后面几个epoch损失下降变慢这是正常的越到后期学的是越细的边界如果训练损失一直在降但测试准确率停住甚至下降那就是过拟合在作祟了。我实测下来这个结构的MLP在Fashion-MNIST上大概需要3到5分钟CPU训练时间取决于机器GPU十几秒就完事适合拿来反复实验。6. 训练MLP的常见坑损失不降、梯度爆炸、过拟合的排查思路6.1 损失一直不降先看三件事遇到损失不下降新手容易直接怪网络结构其实90%的情况出在更底层的地方。我的排查顺序固定是看输入数据。有没有归一化标签和输入的对应关系对不对我曾经有一次损失降不下去最后发现是DataLoader没设shuffleTrue模型一直在按固定顺序学学成了背顺序。看损失值本身。10分类交叉熵的随机猜测损失是 $\ln(10) \approx 2.3026$。如果初始损失远远高于这个值比如5.0说明输出层的logits特别大通常和权重初始化、数据未归一化有关。如果初始损失就卡在2.3026一动不动多半是梯度根本传不回去。看梯度。打印一下第一个全连接层的梯度范数如果小到1e-8级别基本就是梯度消失了如果大到1e10级别就是梯度爆炸。6.2 过拟合的识别与手段MLP参数量大Fashion-MNIST这种6万张图的数据集很容易训练过拟合。典型信号训练损失持续下降、测试损失在第5、6个epoch开始回升。这时候按优先级做三件事加Dropout。Dropout的原理是训练时随机扔掉一部分神经元的输出强迫网络学到冗余特征而不是依赖某些特定神经元的共谋。注意Dropout加在激活函数之后、下一层之前测试时要关掉PyTorch的model.eval()会自动处理。加权重衰减L2正则。Adam优化器的weight_decay参数就是干这个的给大权重惩罚让模型更平滑。我常用0.0001到0.001之间试。早停Early Stopping。监控验证集损失连续几个epoch不降低就停止训练保存最优模型。这个手段网游上最省事效果也最直接。6.3 学习率和Batch Size的配合学习率和Batch Size不是独立变量它们共同决定了梯度更新的噪声水平。Batch Size越小每次梯度估计的噪声越大模型反而可能跳出局部最优Batch Size越大梯度越平滑但需要相应调大学习率否则收敛效率很低。经验法则如果你把Batch Size从64改成256学习率可以按约$\sqrt{4}2$倍往上调。我试过在Fashion-MNIST上用Batch Size 512加学习率0.003收敛速度和Batch Size 128加0.001差不多但训练更稳定。7. MLP的边界在哪里以及它在现代模型里留下的遗产7.1 三个绕不过去的短板把MLP吃透之后你要清楚它的局限否则容易在错误的场景里浪费大量时间。输入必须是固定维度。MLP的输入维度在定义网络结构时就锁死了处理变长序列、动态尺寸图像都很麻烦。这是它在NLP、视觉任务中被更先进结构取代的直接原因。没有利用空间/时序结构。一张图展平成一个784维向量之后像素和邻近像素的空间关系被打散了。MLP需要大量参数去重新学习相邻像素相关这个知识而卷积核天然就带了这个先验所以CNN用更少的参数就能超过MLP。参数量大、容易过拟合。全连接的参数量随层宽平方级增长。两层128个隐藏单元的MLP就有接近10万参数在6万样本的数据集上已经需要正则化手段压着了。7.2 MLP在现代架构中的角色有意思的是MLP死了吗并没有。它换了个马甲活在几乎所有主流模型里Transformer的FFN层每个注意力层后面跟的前馈网络就是一个线性层 ReLU 线性层的MLP它是Transformer中参数量最大的部分MLP-Mixer2021年有研究者直接只用MLP结构做图像分类在ImageNet上达到了接近CNN的效果证明用MLP压出空间信息并非完全不可行各类分类头Classification Head预训练模型最后接的线性层本质上就是一个单层感知机。所以你现在回头看文章开头那句话——MLP是深度学习的基石——应该知道这不是客套话了。把MLP的机制吃透等于把神经网络最底层的骨架摸了一遍之后无论你转向CNN还是Transformer学的都是骨架之上长出的不同器官。最后分享一个我自己的习惯每接触一个新的深度学习框架或新的网络结构我做的第一件事都是用它实现一个MLP跑在Fashion-MNIST上。不是因为这个任务有多难而是它能用最低的成本帮你验证环境、理解工具的工作方式、建立对正常训练过程的体感。等哪天模型出问题的时候这个体感能帮你省下大量排查时间。