ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多层感知机MLP详解:从前向传播到反向传播的神经网络基础

多层感知机MLP详解:从前向传播到反向传播的神经网络基础

很多人第一次接触多层感知机(MLP)时,都觉得它是神经网络里最"没技术含量"的基础模块,不过就是把一堆神经元叠在一起,套个激活函数,然后交给框架训练就完事了。我自己早期也是这样想的,直到某次做分类任务,发现加了隐藏层之后效果反而变差,Loss曲线像心电图一样上下乱跳,才意识到:如果不理解 MLP 内部到底发生了什么,哪怕只是调库调参,也像是在没有仪表盘的飞机上乱按按钮。

这篇文章适合两种人看。一种是刚接触机器学习、想搞明白神经网络"为什么能工作"的新手;另一种是已经会用现成框架搭模型,但遇到不收敛、梯度爆炸、维度报错时只能到处搜答案的开发者。我会从前向传播、反向传播讲起,再用 NumPy 手写一个完整的 MLP,接着把平时最容易翻车的几个环节拆开揉碎,最后用一个小实验串起整个流程。看完之后,你应该能独立设计、调试并评估一个多层感知机模型,而不是停留在"调接口"的层面。

1. 前向传播:多层感知机凭什么画出复杂分类边界

1.1 一个神经元划的是一条"超平面"

先看最基础的计算单元。一个神经元做的事情其实非常简单:把输入向量里每个分量乘以对应权重,求和,再加上一个偏置,然后经过激活函数输出。用公式写就是:

z = w_1x_1 + w_2x_2 + ... + w_n*x_n + b a = activation(z)

理解这个公式有个很自然的类比:投票。每个输入特征像是一个投票者,权重代表它的发言权大小,偏置则是"最低门槛"。如果加权投票总分超过门槛,激活函数输出值就偏向正侧;否则偏向负侧。在二维平面上,一个神经元其实画出的是一条直线,三维是平面,高维就是超平面。它天生只能做一个线性划分,这也是"感知机"这个名称的本意——它能够感知二分类的线性边界。

我见过不少初学者把这里的 z 和 a 弄混,导致后续反向传播时梯度算错。这里先记牢:z 是线性组合的结果,a 是经过激活函数后的输出。后面的所有推导都会用到这两个量。

1.2 隐藏层在空间里做了什么事

既然单层感知机只能画直线,那为什么把神经元叠起来就可以切出任意复杂的形状?关键在于隐藏层做了两次不同的事:先对原始空间做变换,再做组合。

单一神经元画超平面是"原始特征直接投票",但隐藏层的神经元是对上一层输出再次投票。比如两个隐藏神经元分别可以理解为"是否在某个区域的上方"和"是否在某个区域的右侧",再经过下一层组合,就能表达"上方且右侧"这类逻辑与关系。通过多层组合,线性分割被拼接成凸区域,凸区域再组合成更复杂的非凸形状。异或问题就是个经典例子,单个感知机怎么调权重也分不开,但两层网络很轻松就能做到。这也是早期神经网络研究里最著名的转折点之一,后来大家才真正意识到隐藏层的价值。

我在实际调模型时还发现一个现象:隐藏层的维度并不是越大越好。隐藏单元数量决定了网络能表达的特征组合数量,但数量太多容易让模型去死记训练样本的噪声,数量太少又不足以拟合真实规律。这块需要结合数据量来权衡,后面第 5 节我会用一个具体实验展示。

1.3 前向传播各层的形状变化

一个典型的两层 MLP 前向传播可以写成:

Z1 = W1 * X + b1 A1 = ReLU(Z1) Z2 = W2 * A1 + b2 A2 = Sigmoid(Z2) 或 Softmax(Z2)

这里最容易出错的是矩阵形状。举个具体例子:假设训练数据每条样本有 4 个特征,一次送入 8 条样本,输入矩阵 X 的形状是 (8, 4)。第一层隐藏层设 16 个神经元,W1 的形状必须是 (4, 16),这样 X 乘以 W1 得到 (8, 16),偏置 b1 的形状是 (16,)。第二层隐藏层设 8 个神经元,W2 是 (16, 8),得到 (8, 8)。最终输出如果是二分类,W3 是 (8, 1),得到 (8, 1)。

我把这个形状表格列在下面,工程调试时非常实用:

层矩阵乘法形状结果形状参与运算的参数形状
输入-(8, 4)-
隐藏层1(8,4) @ (4,16)(8,16)W1:(4,16), b1:(16,)
隐藏层2(8,16) @ (16,8)(8,8)W2:(16,8), b2:(8,)
输出层(8,8) @ (8,1)(8,1)W3:(8,1), b3:(1,)

注意这里我把样本放在矩阵的行,特征放在列,这是大部分框架和 numpy 代码的习惯。碰到维度不对的报错,先检查是不是转置写反了。前向传播看起来平淡无奇,但它决定了模型对数据的"感知方式"。理解这张表,后面的反向传播就会顺很多。

2. 反向传播:梯度信号在网络里怎么一层层传回去

2.1 损失函数衡量的是误差,但也是梯度源头

网络的输出不能直接拿来调参,必须先有一个"标准答案"告诉我们当前输出错得有多离谱,这就是损失函数。回归任务常用均方误差(MSE),二分类用二元交叉熵(BCE),多分类用交叉熵(Cross-Entropy)。

为什么分类任务我更推荐交叉熵而不是 MSE?因为交叉熵的梯度形式非常干净。以二分类为例,输出层激活函数用 Sigmoid,损失用 BCE,最终输出层的梯度恰好是 (a - y),也就是预测值减真实值。这个形式意味着:预测错得越远,梯度越大;预测越接近真实,梯度越小,收敛过程很自然。而如果分类任务用 MSE 搭配 Sigmoid,由于 Sigmoid 在输出接近 0 或 1 时导数非常小,会乘积出几乎为零的梯度,训练会变得极慢甚至完全卡死。

理解损失函数不是选一个 API 就完事,它决定了梯度信号的"源头质量"。通常我会先确认任务类型,再反推损失函数和输出层激活函数的搭配。

2.2 链式法则:一层套一层的导数接力

反向传播的本质是链式法则。损失 L 对某个权重 w 的梯度,可以写成从输出到该权重的整条路径上所有偏导数的连乘。

以最简单的两层网络为例。输入 x,经过隐藏层得到 A1,再经过输出层得到 A2(即预测值)。损失 L 对 W2 的梯度是:

dL/dW2 = (dL/dA2) * (dA2/dZ2) * (dZ2/dW2)

其中 dL/dA2 来自损失函数的导数,dA2/dZ2 来自输出层激活函数的导数,dZ2/dW2 = A1 的转置。每一层都需要缓存前向传播时的 z 值和激活值,反向时从后往前逐步复用。这也是为什么框架里执行 backward 会自动把中间变量存起来,显存占用就是这么来的。

我刚开始手写反向传播时,NumPy 里的乘法到底是逐元素乘还是矩阵乘,经常搞混。这里有一个判断技巧:看涉及的量有没有明确的"样本维度"和"特征维度"两个方向。凡是逐元素的乘法,比如梯度与激活函数导数的乘法,通常都是在同一个形状的矩阵上按位做;凡是涉及对权重求导的,基本都要做矩阵乘法,并且往往需要转置。把这一点想清楚,反向传播代码基本不会有逻辑错误。

2.3 梯度消失与梯度爆炸的真实机制

梯度消失和爆炸,是训练深层 MLP 时最让人头疼的问题,本质上是链式法则连乘导致的指数效应。

先说消失。假设每层的激活函数是 Sigmoid,它的最大导数只有 0.25,而且当输入远离 0 时导数会变得更小,趋近于 0。如果网络有 20 层,反向传播中每一层都乘一个小于等于 0.25 的数,不用算完你也能感受到结果有多可怕:0.25 的 20 次方接近于 10 的 -12 次方,这几乎是零。靠近输入层的权重根本收不到任何有效梯度,于是模型根本停不下来更新这些参数,表现就是训练很慢、前几层像是被"冻结"了一样。

梯度爆炸则往往出现在权重初始化过大、输入数据量级过大时。如果每层权重矩阵的谱范数大于 1,连乘后梯度会呈指数级放大,参数瞬间变成一个超大值,Loss 直接变成 NaN。应对思路也基本固定:激活函数换 ReLU 这类导数能稳定在 1 或 0 的;权重初始化采用根据网络层数设计的方差缩放方案;输入数据做标准化。这三件事一起做,就能避开绝大多数爆炸或消失的场景。理解了这个机制,你就明白为什么网上随便搜"梯度爆炸怎么办"得到的答案都绕不开初始化、激活函数、归一化这三板斧。

3. 用 NumPy 手写一个 MLP:自己动手比调库更让人清醒

3.1 为什么我坚持让你手写一次

我不否认框架的效率,PyTorch、TensorFlow 之类的工具让深度学习工程化速度大幅提升。但在刚接触 MLP 的时候,直接调框架是最糟糕的学习方式——框架把所有步骤封装得干干净净,反向传播自动完成,改一行配置就能收敛,这反而让人误以为"神经网络本来就该这么听话"。

我建议你至少手写一次包含两层隐藏层的 MLP,用 NumPy 从零实现前向传播、反向传播、参数更新三个模块。这样做有三个实打实的好处:第一,你会亲眼看见每个梯度的形状和前向传播缓存一一对应;第二,当 Loss 不下降时,你有能力手动去查到底是哪一层出了问题,而不是无脑调学习率;第三,手写过后再切换到框架,你看官方文档时会在脑子里自动映射到具体的计算过程,排查 bug 的效率完全不同。

这里顺手说一个我自己的经历:曾经有个朋友做模拟数据分类,调框架怎么训练准确率都不超过 60%,他怀疑数据有问题。后来我让他打印中间层的激活值分布,发现隐藏层输出的值几乎全是负数,ReLU 把大部分神经元杀死了。他当时用的初始化方差非常大,导致第一层输出 z 动辄是几十,ReLU 输入为负就输出 0,梯度也跟着变 0,整个网络变成了一个"死网络"。这种问题,手写实现时很容易定位,框架封装反而把线索隐藏了。

3.2 一个可用于螺旋二分类的完整实现

下面给出一段可以直接跑通的 MLP 实现,用 NumPy 完成螺旋数据分类。螺旋数据是经典的非线性分布,单层线性模型完全搞不定,正好用来验证 MLP 的效果。

import numpy as np def make_spiral(n_samples, noise=0.2): n = n_samples // 2 t = np.linspace(0, 1, n) t = np.repeat(t, 2) x1 = t * np.cos(2 * np.pi * t) + noise * np.random.randn(2 * n) x2 = t * np.sin(2 * np.pi * t) + noise * np.random.randn(2 * n) X = np.column_stack([x1, x2]) y = np.repeat([0, 1], n) return X.astype(np.float32), y.astype(int) def relu(z): return np.maximum(0, z) def relu_derivative(a): return (a > 0).astype(float) def softmax(z): exp_z = np.exp(z - np.max(z, axis=1, keepdims=True)) return exp_z / exp_z.sum(axis=1, keepdims=True)
class MLP: def __init__(self, sizes): self.sizes = sizes self.params = {} for i in range(1, len(sizes)): fan_in = sizes[i-1] fan_out = sizes[i] self.params[f"W{i}"] = np.random.randn(fan_in, fan_out) * np.sqrt(2.0 / fan_in) self.params[f"b{i}"] = np.zeros((fan_out,)) def forward(self, X): cache = {"A0": X} z = X L = len(self.sizes) - 1 for i in range(1, L): z = z @ self.params[f"W{i}"] + self.params[f"b{i}"] cache[f"Z{i}"] = z z = relu(z) cache[f"A{i}"] = z z = z @ self.params[f"W{L}"] + self.params[f"b{L}"] probs = softmax(z) cache[f"Z{L}"] = z cache[f"A{L}"] = probs return probs, cache def backward(self, X, y, probs, cache): grads = {} L = len(self.sizes) - 1 m = y.shape[0] y_onehot = np.eye(self.sizes[L])[y] dZ = probs - y_onehot for i in reversed(range(1, L + 1)): A_prev = cache[f"A{i-1}"] grads[f"W{i}"] = A_prev.T @ dZ / m grads[f"b{i}"] = np.sum(dZ, axis=0) / m if i > 1: dA_prev = dZ @ self.params[f"W{i}"].T dZ = dA_prev * relu_derivative(cache[f"A{i-1}"]) return grads def update(self, grads, lr=0.5): for key in self.params: self.params[key] -= lr * grads[key]

这段代码有几个细节值得展开。softmax 里先减去每行最大值,是为了数值稳定,避免超大 z 导致 exp 溢出;权重初始化的乘法因子用sqrt(2 / fan_in),对应 ReLU 的推荐初始化策略,上一条里解释过梯度消失的问题;反向传播中每个 W 的梯度都要除以 m(样本数),这是求平均梯度的正常操作,否则梯度会随 batch 大小波动,训练不稳定。

训练几行代码就够了:

X, y = make_spiral(1000) mlp = MLP([2, 64, 32, 2]) for epoch in range(500): probs, cache = mlp.forward(X) loss = -np.mean(np.log(probs[range(len(y)), y])) grads = mlp.backward(X, y, probs, cache) mlp.update(grads, lr=0.5) if epoch % 50 == 0: acc = (probs.argmax(axis=1) == y).mean() print(f"epoch {epoch}, loss {loss:.4f}, acc {acc:.4f}")

跑完之后,验证集准确率通常在 95% 以上。这个结果直观说明:手写的 MLP 虽然简单,但已经具备了处理非线性分类的全部核心能力。

3.3 梯度检查:用数值差分验证反向传播是否正确

手写反向传播最容易出现笔误。比如该转置的没转置,该乘的没有乘,或者梯度符号反了。这些错误不会让程序报错,只会让 Loss 卡在某个位置不动,排查起来非常痛苦。有一个几乎万能的验证方案:数值梯度检查。

思想很简单。对某参数 theta,损失 J 在 theta 附近的数值导数可以用中心差分近似:

grad_numerical = (J(theta + eps) - J(theta - eps)) / (2 * eps)

其中 eps 取 1e-6 左右。然后用这个数值梯度和你的反向传播梯度比较,看相对误差是多少。相对误差小于 1e-6 说明反向传播基本正确,误差在 1e-4 到 1e-2 之间就要去查代码,大于 1e-2 基本可以断定反向传播写错了。

实操时,我会随机取一两个参数来做检查,因为全参数检查太慢。代码如下:

def numerical_gradient(mlp, X, y, key, eps=1e-6): orig = mlp.params[key].copy() flat = orig.ravel() grad_flat = np.zeros_like(flat) for i in range(flat.size): flat[i] += eps mlp.params[key] = flat.reshape(orig.shape) loss1 = compute_loss(mlp, X, y) flat[i] -= 2 * eps mlp.params[key] = flat.reshape(orig.shape) loss2 = compute_loss(mlp, X, y) grad_flat[i] = (loss1 - loss2) / (2 * eps) flat[i] = orig.ravel()[i] mlp.params[key] = orig return grad_flat.reshape(orig.shape)

注意做梯度检查时数据量要小一点,比如 5 个样本,否则逐参数计算代价太大。检查时网络要小,比如[2, 8, 2]这种规模。我在手写 MLP 时每写一层都会跑一次梯度检查,确保从输入到输出的每一条梯度链都是通的。这一步花的时间很少,但能帮你避开之后大量的调参迷茫。

4. 训练 MLP 最容易翻车的四个环节:维度、初始化、学习率和特征缩放

4.1 维度对齐的三种常见错误

MLP 的 bug 有相当高比例是维度问题。第一种常见错误是输入矩阵方向搞反。有人把 X 的形状写成 (features, n_samples),于是一整套网络里的矩阵乘法全部要跟着转置,代码里到处都是.T,极容易错。我的建议是统一采用 (n_samples, features) 的"行样本"约定,后续的 W 形状就能自然确定。

第二种错误是权重矩阵的形状写反。第 i 层权重矩阵的形状应当是 (fan_in, fan_out),也就是"上一层的输出维度"乘以"当前层的输出维度"。这个我一开始老记反,后来在每个矩阵乘法下面都加一行形状注释,异常报错时一眼就能看出来。

第三种错误是批归一化或 dropout 之后再倒退算梯度时,把维度弄丢了。比如某个中间层本来输出是 (batch, 16),经过某种操作变成 (16,),后面的矩阵乘法就会报维度不匹配。排查时只要把每个张量从 forward 到 backward 的形状变化写一遍,绝大多数问题都能找到。

4.2 初始化不是随便乘个随机数

权重初始化被很多人当成一个可有可无的细节,但它直接决定训练能不能起步。最典型的是把所有权重初始化成 0。如果你这么做了,每一层所有神经元的输出都会完全一致,反向传播时同一层内每个神经元的梯度也完全相同,不管网络有多宽,它实际的行为都像一个只有一个神经元的网络,这就是对称性破坏问题。正确的做法是引入随机性,让同一层的神经元朝不同方向学习。

如果只用标准正态分布初始化,放大图片数据的场景下权重和偏置的乘积动辄几十上百,激活函数和梯度都会进入饱和区,训练起不来。现在常用的是方差缩放初始化:让权重方差约等于 1 / fan_in 或 2 / (fan_in + fan_out),这样前向传播时信号方差能够保持稳定,反向传播时梯度也不会因为连乘而爆炸。我在 3.2 节的代码里就是用的这个思路,配合 ReLU,即使网络加深也能较稳定地训练。

4.3 学习率和 batch size 怎么配合

学习率是最直接、最容易见效的超参数,但也最容易走极端。学习率太大的表现是 Loss 曲线直接冲上天才或者变成 NaN,参数来回震荡根本无法收敛。学习率太小则曲线平滑但下降极慢,跑几百轮也没进展。按我的经验,Adam 优化器下初始学习率 1e-3 是一个比较稳妥的起点,如果 Loss 震荡就降到 1e-4,如果学习太慢就调到 3e-3 再试。

batch size 的作用容易被低估。batch 太小,梯度噪声大,收敛路径歪歪扭扭,但有时反而能跳出局部极小值;batch 太大,梯度平滑但每一步算得慢,而且容易陷入尖锐的局部极小值。实际调参时不要单独调一个参数,最好固定一个基准(比如 batch size 64,学习率 1e-3),跑完一轮观察曲线,再成对调整学习率和 batch。你会发现它们对训练稳定性的影响是相互制约的。

4.4 特征缩放:一个容易被忽略的前提

如果输入特征的量级不在同一个尺度,MLP 的训练会异常难受。假设一个用户分类数据里,"年收入"取值几万到几十万,"年龄"取值 20 到 70,前者对加权求和的贡献天然远大于后者。模型会把几乎全部注意力放在收入上,而年龄的梯度缩小到可以忽略,收敛就变得极其偏向某一方向。

解决办法是标准化。对每个维度的特征减去均值、除以标准差:

x_scaled = (x - mean) / std

这样每个特征的均值都是 0,方差都是 1,各维度对初始梯度的贡献就均衡了。实际代码里用 numpy 一行就能做:

mean = X_train.mean(axis=0) std = X_train.std(axis=0) X_train_scaled = (X_train - mean) / std X_test_scaled = (X_test - mean) / std

注意,标准化所用的均值和标准差必须只用训练集计算,然后应用到测试集上,不能用整个数据集的统计量去算,否则会引入信息泄露,测试集评估指标会虚高。

5. 一个可复现的 MLP 分类实验:从数据预处理到评估改进

5.1 数据集与评估指标怎么定

为了把这套东西串起来,我跑了一个手写数字分类的小实验。数据取自公开的手写数字数据集,每张图是 28x28 像素,展开成 784 维向量,任务是把图片分成 10 类。训练集 50000 张,验证集 10000 张,测试集 10000 张。

评估指标用准确率是不够的。我额外看了验证集和训练集的 loss 差值,以及分类结果的混淆矩阵。混淆矩阵能告诉我是哪一类最容易混,比如数字"4"和"9"在低分辨率下本来就很难区分。只看准确率容易自我感觉良好,结合混淆矩阵才知道模型的真实短板在哪里。

数据预处理这一步我跑了标准化,把 0-255 的像素值缩放到均值为 0、方差为 1 的范围。虽然 MLP 对像素级特征并没有特别强的假设,但标准化之后配合 He 初始化,收敛明显更稳。

5.2 网络结构、超参数与完整流程

网络结构采用两层隐藏层:输入 784 维,第一层 128 个神经元,第二层 64 个神经元,输出层 10 个神经元。激活函数隐藏层用 ReLU,输出层用 Softmax,损失函数用交叉熵,优化器用带动量的 SGD,学习率 0.01,动量 0.9,batch size 128,训练 20 轮。

直接上核心训练代码:

import numpy as np def forward(X, params): a = X for i in range(len(params) // 2): z = a @ params[f"W{i+1}"] + params[f"b{i+1}"] a = relu(z) if i < len(params) // 2 - 1 else softmax(z) return a def train_one_epoch(train_X, train_y, params, lr=0.01, momentum=0.9, batch=128): n = train_X.shape[0] perm = np.random.permutation(n) v = {k: np.zeros_like(params[k]) for k in params} for start in range(0, n, batch): idx = perm[start:start+batch] X_b, y_b = train_X[idx], train_y[idx] probs, cache = network_forward(X_b, params) grads = network_backward(X_b, y_b, probs, cache) for k in params: v[k] = momentum * v[k] + grads[k] params[k] -= lr * v[k] return params

跑完第 20 轮后,测试集准确率在 97.5% 上下。如果你自己在框架里复现,把网络结构改成 [784, 128, 64, 10],使用同样的超参数,得到的数字应该非常接近。这个结果已经明显超过简单的线性模型,证明 MLP 在图像分类上确实能学到一些非线性组合特征。这里要说明一个局限:它和优良配置的卷积网络相比仍有明显差距,30% 的差距主要来自 MLP 不能充分利用像素的空间局部性,这一点我放到第 6 节具体展开。

5.3 模型在验证集上表现不佳怎么办

实验里我发现训练集的准确率能到 99%,验证集却只有 97.5%,这说明模型出现过拟合,也就是把训练数据的细节噪声也记住了。应对手段按优先级排:

优先增大数据量,如果有更多数据,一切正则化都显得不那么必要。其次是加 Dropout,随机丢弃部分隐藏层输出,强制模型学习冗余表达;我试验将第二个隐藏层后加 Dropout,比例从 0 调到 0.2,验证集准确率提升了约 0.8%。然后是加权重衰减,也就是 L2 正则化,让权重不要太大,缓解个别特征被过度依赖。

更一般地,我养成了一个诊断习惯:每轮训练同时记录训练 loss 和验证 loss。如果训练 loss 一直在降、验证 loss 掉了又升,那就是过拟合;如果两者都高,说明欠拟合,这个时候考虑加宽网络或增加训练轮数;如果验证 loss 出现剧烈震荡,优先查学习率和 batch size 是否匹配。这一套诊断逻辑可以迁移到绝大多数 MLP 训练问题中。

6. MLP 的能力边界与它在现代架构中的位置

6.1 全连接层的参数爆炸

MLP 也有一个不争的短板,就是"全连接"三个字带来的参数规模。在 28x28 的手写数字图上,784 维输入连到第一层 128 个神经元,权重数量已经接近 10 万。如果换成 256x256 的彩色图片,输入就是 196608 维,连到一层 1024 个神经元,光第一层的权重就有约 2 亿个参数。这还只是一层,模型不仅训练极慢,还非常容易过拟合。

问题出在哪?全连接默认每个输入像素和每个隐藏神经元都有关联,但实际上图像里的强相关性是局部的,远处的像素几乎不影响局部边缘检测。卷积网络引入局部感受野和权值共享,一个滤波器的权重在整张图上共享,参数量立刻降了几个量级。MLP 之所以还能在入门阶段占重要位置,是因为它把"从特征到抽象表达"的基本逻辑讲清楚了,但图像类任务上必须转向能利用空间结构的架构。

6.2 序列建模中 MLP 缺失的机制

如果输入是自然语言句子,MLP 的问题更大。把句子切成词后,词向量的顺序对 MLP 来说没有任何意义,交换两个词的位置,除非训练时正好出现了相同样本,否则模型根本察觉不到顺序变化。这意味着它无法建模"张三打了李四"和"李四打了张三"之间的本质区别。

序列模型里我们会用到循环结构或注意力机制,核心就是在计算过程中加入"位置信息"和"词与词之间的依赖关系"。不过有意思的是,这些看似高级的架构,最后一步的分类或输出通常仍然靠一个全连接层完成。所以 MLP 并没有被淘汰,它依然承担着"特征聚合 + 最终决策"这个最朴素的职能。理解 MLP 之后,再去看注意力机制里的 QKV 矩阵变换、前馈子层,你会发现那些组件本质上还是在做线性变换、非线性激活、多层组合这套循环。

6.3 我学完 MLP 之后回头看深度学习

如果让我给建议,我会说:花一周时间手写 MLP,比直接冲到大模型的 API 调用里更有价值。手写过后你会对 loss 曲线、梯度数值、甚至内存占用都有一种"直觉",遇到训练异常时,怀疑的雷达会从"我是不是代码写错了"迅速切换到"是不是数据没洗干净、初始化不合适、学习率太激进"这些真实方向。

多层感知机这个名字听起来简单,但它包含了神经网络几乎全部核心思想:参数化表达、损失函数、梯度下降、特征变换。后面接触卷积结构、循环结构乃至各种 transformer 变体时,你会发现它们都是在 MLP 这套骨架上换不同的信息交互方式。先理解骨架,再研究器官,整个学习路径会顺畅得多。

我自己在跑完这个手写数字实验后,又把同样的代码改成回归任务去预测某模拟数据集的房价,效果也不错。说到底,MLP 是一种通用函数逼近器,只要数据规模合适、特征处理得当,它就能在很多表格类任务中发挥稳定作用。以后的深度学习路线里,你可能很少再手写这种基础网络,但回头看,那段时间踩过的坑和调过的参,会比任何一个封装好的框架都更牢靠地留在脑子里。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进