一、先把整条链放在桌上
很多教材一上来就给你一个大词:神经网络能逼近复杂函数。这个说法没有错,但太抽象。更实用的理解方式是:神经网络是一台可微的函数机器,它不断重复做四件事。
第一,拿着当前参数做一次前向传播,算出预测值。
第二,用损失函数衡量这次预测离真实答案差多远。
第三,用链式法则把这个误差沿计算图反向传回去,算出每个参数的梯度。
第四,用梯度下降或它的变种更新参数,让下一次前向传播更接近目标。
如果把一轮训练压成最短的伪流程,其实就是:
prediction = model(input)loss = criterion(prediction, target)loss.backward()optimizer.step()
RNN、CNN、Transformer 全都活在这个闭环里。它们的差异不是“会不会训练”,而是前向那张图怎么组织,信息在图里怎么流动。普通 MLP 让信息从左往右流;RNN 让隐藏表示沿时间继续流;Transformer 则让不同位置之间通过 attention 直接互相读写。
所以这一篇你真正要抓住的不是某个孤立公式,而是这条总链:
- 神经元决定单个计算单元算什么;
- 层和矩阵乘法决定很多神经元怎样一起算;
- 前向传播决定模型一次预测的计算顺序;
- 损失函数决定“什么算错”;
- 反向传播决定“错了以后每个参数怎么改”;
- 梯度下降决定“沿哪个方向动”;
- 序列模型则是在这套闭环之上重新设计信息流。
二、一个神经元到底在算什么
“神经网络”这个名字很容易让人走神,仿佛它是什么生物学模拟器。对工程理解更有帮助的说法是:一个神经元就是一个可训练的打分器,再加一个非线性门。
最小形式可以写成:
\[ z = w \cdot x + b \]
\[ h = \sigma(z) \]
第一步 w \cdot x + b 是线性打分。输入向量
x 投到权重向量 w
指定的方向上,再加偏置 b。第二步
\sigma(z)
是激活函数,它决定这个打分如何被压缩、折弯或截断。
这张图把“线性打分”这件事画得最直观:
图里每个散点是一条二维输入,绿色直线是
w·x + b = 0 这条分界线。它把平面切成两半:一半
z >= 0,一半 z < 0。红色箭头
w 表示这个神经元最敏感的方向。
这张图想说明三个事实。
第一,单个神经元本质上是在学一个方向。它问的是:“输入在这个方向上有多强?”
第二,偏置 b 决定阈值在哪里。没有
b,分界线只能穿过原点;有了
b,分界线可以平移。
第三,如果后面不接非线性,神经元只能做线性切分。它可以判断“在线的一边还是另一边”,但做不出真正复杂的弯曲边界。
这也是为什么说神经元不是一个“会思考的小脑细胞”。它更像一个方向检测器:w
决定看哪里,b
决定门槛多高,激活函数决定通过之后怎么表达。
三、为什么非线性是分水岭
只看
w \cdot x + b,你可能会以为:那我叠很多层线性变换,不就越来越强了吗?遗憾的是,不行。线性变换的复合还是线性变换。两层线性层连起来,最后仍然等价于一层更大的线性层。
也就是说,让深度网络真正变“深”的,不是层数本身,而是层与层之间插进去的非线性。没有非线性,深网络会塌缩成浅网络。
下面这张图把这个问题画得很清楚:
左边是线性打分
z = wx + b,它就是一条直线。右边是同一个
z 经过 ReLU 和 tanh
之后的输出。你可以看到,激活函数把直线“折”成了新的形状。ReLU
让负半轴直接归零,tanh 则把大值压饱和。
这一步非常关键,因为真实世界里大多数有意思的映射都不是线性的。房价不是面积的线性函数,图像标签不是像素值的线性函数,语言语义更不是 token 向量的线性函数。深度学习之所以能拟合复杂关系,靠的不是把矩阵乘法堆得更厚,而是在线性层之间不断插入非线性,让表示空间反复被折弯。
从这里开始,神经网络就不再是“一次线性打分”,而是“线性变换 + 非线性 + 线性变换 + 非线性”的函数复合机器。
四、一层、一个 batch、一个 MLP:形状怎么走
单个神经元只输出一个数,但真实任务需要一组特征。所以我们会把很多神经元并排放在一起,用一整个矩阵一次性算完一层。
如果输入维度是 d_in,这一层有
d_out 个神经元,那么:
\[ H = \sigma(XW + b) \]
这里的形状最重要。很多深度学习 bug 不是数学 bug,而是形状 bug。最常见的一组张量形状如下:
| 符号 | 含义 | 形状 |
|---|---|---|
X |
一个 batch 的输入 | B \times d_in |
W |
这一层的权重矩阵 | d_in \times d_out |
b |
这一层的偏置 | 1 \times d_out |
Z = XW + b |
激活前的线性输出 | B \times d_out |
H = sigma(Z) |
这一层的隐藏表示 | B \times d_out |
这里 B 是 batch
size。也就是说,前向传播不是“每次只算一个样本”,而是把一批样本堆成矩阵,一次矩阵乘法全算完。GPU
正是擅长这种运算,这也是为什么神经网络天然喜欢用矩阵表达。
把一层接一层串起来,就得到多层感知机(Multilayer Perceptron, MLP):
\[ Z_1 = XW_1 + b_1 \]
\[ H_1 = \tanh(Z_1) \]
\[ \hat{Y} = H_1W_2 + b_2 \]
这里 H_1
就是隐藏表示。它不是最终答案,而是网络自己造出来的中间语言。这个概念要记住,因为到了下一篇,RNN
的 h_t
本质上就是“隐藏表示沿时间传递之后的版本”。
五、前向传播:一次预测到底算了什么
现在可以把“前向传播”说具体了。以前面这个两层 MLP 为例,一次前向传播就是把输入从头算到尾。
如果是回归任务,比如本文的 toy experiment,那么输出层可以直接给一个实数:
\[ \hat{Y} = H_1W_2 + b_2 \]
如果是分类任务,最后通常不是直接拿这个输出当结果,而是把它看成 logits,再交给 softmax 变成概率分布。
为了把概念彻底钉住,我们先看一个最小的标量前向例子。取:
x = 2.0w1 = 0.60b1 = -0.20w2 = 1.40b2 = -0.10target = 0.20
那么前向传播按顺序是:
\[ z = w_1x + b_1 = 0.60 \times 2.0 - 0.20 = 1.0 \]
\[ h = \tanh(z) = \tanh(1.0) = 0.761594 \]
\[ \hat{y} = w_2h + b_2 = 1.40 \times 0.761594 - 0.10 = 0.966232 \]
你可以看到,哪怕只是最小网络,前向传播也已经不是一句“算输出”那么简单了。它是一串明确的中间结果:先有
z,再有 h,最后有
\hat{y}。而这些中间结果之所以重要,是因为反向传播必须沿着同一条链倒回来。
六、损失函数:模型到底在最小化什么
前向传播只会告诉你“这次预测是多少”,但不会告诉你“这次错得多严重”。训练要发生,必须先定义一个可微的误差尺度。这个尺度就是损失函数(loss function)。
对回归任务,一个常见选择是均方误差。为了让导数更干净,通常会在前面乘一个
1/2:
\[ L = \frac{1}{2B} \|\hat{Y} - Y\|^2 \]
这里 B 是 batch size。前面的
1/2 没有改变最优点,只是让求导时 2
被抵掉。对刚才那个标量例子,它变成:
\[ L = \frac{1}{2}(\hat{y} - y)^2 \]
代入前向结果:
\[ L = \frac{1}{2}(0.966232 - 0.20)^2 = 0.293556 \]
这个数就是“模型当前有多错”。如果是分类任务,常见的损失不是 MSE,而是交叉熵,因为输出要被解释为概率分布。不同损失函数会影响梯度形式,但训练逻辑不变:先把误差写成一个可微的标量,再让这个标量对参数求导。
七、先手算一个最小反向传播例子
“反向传播”这个词最容易把人吓住。但如果你把它压回最小例子,它其实只是链式法则按计算图从右往左走一遍。
先看图:
这张图里,前向路径是:x -> z -> h -> \hat{y} -> loss。反向路径则从
loss 往回,一层一层把梯度乘回来。
对刚才那组数值,反向传播一步一步是这样的。
第一步,先对输出求导:
\[ \frac{dL}{d\hat{y}} = \hat{y} - y = 0.966232 - 0.20 = 0.766232 \]
第二步,输出层参数 w2 的梯度:
\[ \frac{dL}{dw_2} = (\frac{dL}{d\hat{y}}) \times h = 0.766232 \times 0.761594 = 0.583558 \]
第三步,把梯度继续传回隐藏表示:
\[ \frac{dL}{dh} = (\frac{dL}{d\hat{y}}) \times w_2 = 0.766232 \times 1.40 = 1.072725 \]
第四步,经过 tanh 这层时要乘上局部导数。因为
h = \tanh(z),所以:
\[ \frac{dh}{dz} = 1 - h^2 = 1 - 0.761594^2 = 0.419974 \]
于是:
\[ \frac{dL}{dz} = (\frac{dL}{dh}) \times (1 - h^2) = 1.072725 \times 0.419974 = 0.450517 \]
第五步,再传回第一层参数:
\[ \frac{dL}{dw_1} = (\frac{dL}{dz}) \times x = 0.450517 \times 2.0 = 0.901034 \]
偏置的梯度更简单,因为 z = w_1x + b_1 对
b_1 的导数就是 1,所以:
\[ \frac{dL}{db_1} = 0.450517, \frac{dL}{db_2} = 0.766232 \]
这就是反向传播的全部:把最终误差乘上每一层的局部导数,一层层传回去。如果学习率
η = 0.1,那么这一步更新以后:
w1 <- w1 - 0.1 \times 0.901034w2 <- w2 - 0.1 \times 0.583558
参数会往损失下降的方向挪一点。下一次前向传播,\hat{y}
就会离目标更近一点。
八、把标量推广到矩阵:两层 MLP 的 batch 求导
手算标量例子是为了建立直觉,真实训练时不可能一条样本一条样本、一个参数一个参数地推。工程上我们总是按 batch、按矩阵来写。
仍然以前面的两层回归 MLP 为例:
\[ Z_1 = XW_1 + b_1 \]
\[ H_1 = \tanh(Z_1) \]
\[ \hat{Y} = H_1W_2 + b_2 \]
\[ L = \frac{1}{2B} \|\hat{Y} - Y\|^2 \]
那么反向传播可以整批写成:
\[ dY = (\hat{Y} - Y) / B \]
\[ dW_2 = H_1^T dY \]
\[ db_2 = \operatorname{row\_sum}(dY) \]
\[ dH_1 = dY W_2^T \]
\[ dZ_1 = dH_1 ⊙ (1 - H_1^2) \]
\[ dW_1 = X^T dZ_1 \]
\[ db_1 = \operatorname{row\_sum}(dZ_1) \]
这里 ⊙
表示逐元素相乘,\operatorname{row\_sum}
表示把一个 batch 在第 0
维上求和,得到偏置梯度。你会发现,矩阵版和标量版没有本质区别:
- 标量版里“乘上隐藏值”,矩阵版里变成
H_1^T dY; - 标量版里“乘上激活函数导数”,矩阵版里变成
dH_1 ⊙ (1 - H_1^2); - 标量版里“对偏置求导就是 1”,矩阵版里变成把 batch 维度上的梯度直接相加。
这也是为什么说反向传播本质上不是“深度学习黑魔法”,而是普通链式法则的批量矩阵化实现。
九、怎么实现:先自己写一遍,再看框架版
到这里,最好的办法不是继续背公式,而是自己写一遍。下面这段代码经删减,只保留同目录脚本
plot_neural_network_figures.py
里的核心训练路径,去掉了作图和快照保存。它实现的就是上面那套两层
MLP:前向、损失、反向、更新,一个不少。
import numpy as np
rng = np.random.default_rng(11)
x = np.linspace(-2.0, 2.0, 96).reshape(-1, 1)
y = np.sin(3.0 * x) + 0.35 * x + 0.08 * rng.normal(size=(96, 1))
hidden = 32
W1 = rng.normal(scale=0.8, size=(1, hidden))
b1 = np.zeros((1, hidden))
W2 = rng.normal(scale=0.35, size=(hidden, 1))
b2 = np.zeros((1, 1))
lr = 0.035
for step in range(2600):
Z1 = x @ W1 + b1
H1 = np.tanh(Z1)
\hat{Y} = H1 @ W2 + b2
dY = (\hat{Y} - y) / x.shape[0]
dW2 = H1.T @ dY
db2 = np.sum(dY, axis=0, keepdims=True)
dH1 = dY @ W2.T
dZ1 = dH1 * (1.0 - H1 ** 2)
dW1 = x.T @ dZ1
db1 = np.sum(dZ1, axis=0, keepdims=True)
W1 -= lr * dW1
b1 -= lr * db1
W2 -= lr * dW2
b2 -= lr * db2这段代码故意没有框架,也没有“自动求导”。它把神经网络训练写成了最裸露的样子:你必须自己保存中间结果,自己写导数,自己更新参数。把这一版看懂,再去看 PyTorch,就会发现框架只是替你做了两件事:记录计算图、自动算梯度。
对应的 PyTorch 版会短得多:
import torch
import torch.nn as nn
class TinyMLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(1, 32),
nn.Tanh(),
nn.Linear(32, 1),
)
def forward(self, x):
return self.net(x)
model = TinyMLP()
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-2)
criterion = nn.MSELoss()
for step in range(2600):
pred = model(x_tensor)
loss = criterion(pred, y_tensor)
optimizer.zero_grad()
loss.backward()
optimizer.step()从“自己写导数”切到“框架自动微分”,并不是原理变了,而是 bookkeeping 被封装掉了。前向还是那次前向,损失还是那个损失,梯度还是那组梯度,更新还是那步更新。
十、训练到底发生了什么
现在来看训练过程本身。梯度下降不是一句抽象口号,它在参数空间里真的走出一条路。
先看最简单的二维参数面:
这张图里,横轴是线性模型的权重 w,纵轴是偏置
b。灰色等高线表示不同损失值,橙色折线是梯度下降从起点一路往低处挪的轨迹。起点很差,离真实参数很远;更新若干步之后,参数逐渐贴近低损失区域。
这张图说明了一个经常被说得很玄的事实:训练不是“模型突然学会了”,而是参数在损失曲面上连续移动。学习率控制每一步走多远,梯度决定往哪里走。
再看本文真实跑过的两层 MLP 训练实验:
左图是同一个 MLP
在不同训练步数下的拟合结果。你能看到,刚开始几乎是一条乱线;80
步时开始有点形状;400 步时已经抓住了大致弯曲;2600
步时基本贴上目标曲线。右图则是 loss
的对数坐标曲线,它从高位一路往下掉,最后停在
0.033948。
这里还有两个值得顺手说明的点。
第一,这个 toy experiment 用的是全 batch 更新,不是 mini-batch。原因很简单:教学上这样更干净,图也更稳定。真实深度学习训练通常用 mini-batch,因为数据更大、噪声更有利于优化、GPU 也更适合按 batch 吞数据。
第二,这个实验不是在“证明神经网络很强”,而是在证明训练闭环是可工作的。只要你把前向、损失、反向和更新写对,一台很小的 MLP 就能把一条非线性曲线从不会拟合,慢慢训到会拟合。
十一、为什么普通 MLP 不适合序列
到这里,我们已经把普通神经网络从头走通了:它能表达什么,怎么前向,怎么定义损失,怎么求导,怎么训练。现在终于可以问下一个关键问题:既然 MLP 已经能拟合复杂函数,为什么还需要 RNN?
原因不是“MLP 不够强”,而是“MLP 的信息流不适合序列”。下面这张图把问题画得很清楚:
上半部分是把一段固定长度序列直接拍平成一个大向量,再交给 MLP。这样做有三个硬伤。
第一,长度固定。输入是一个大向量,就意味着你必须预先决定最多接多少个 token。短了要 padding,长了要截断。
第二,位置绑定参数。第 1 个位置和第 57 个位置虽然都可能出现形容词,但在这种设计里,它们会落到不同参数上,模型必须在每个位置重新学一遍类似规律。
第三,没有天然状态。MLP 不会在读到第 50 个 token 时自动携带前 49 个 token 的摘要。你只能把历史显式拼进输入,或者把窗口拉得很大,而这又会迅速推高参数量。
一个很典型的例子是
not good。如果模型只看当前词,good
大概率会被判成正面。要让模型理解 not 会反转
good,它就需要把前面的信息带到后面。对长度固定、位置绑定的
MLP
来说,这意味着你得为每种窗口长度和位置关系都学一套组合规则。这很笨,也很浪费。
十二、RNN 究竟是在 MLP 上加了什么
RNN 相对 MLP 的核心改动,其实只有一句话:让当前隐藏表示依赖上一时刻的隐藏表示。
把普通 MLP 写成最抽象的形式,是:
\[ h = F_θ(x) \]
而最基本的 RNN 则变成:
\[ h_t = \phi(W_x x_t + W_h h_{t-1} + b) \]
\[ y_t = W_y h_t + c \]
和普通前馈网络相比,RNN 只多出了一条来自
h_{t-1}
的边。可就是这条边,把“隐藏表示”变成了“状态”。状态让网络在读第
t 个 token 时,不只看当前输入
x_t,还看过去的总结 h_{t-1}。
从这个角度看,RNN
不是另一种神秘机器,而是把普通神经网络沿时间展开:同一套参数
W_x, W_h, W_y
在每一个时间步重复使用。也正因为它可以展开成一条很长的前馈链,所以训练它时需要把梯度沿时间往回传,这就是下一篇要讲的
BPTT(Backpropagation Through Time)。
换句话说,RNN 仍然活在本文讲的那条大闭环里:前向、损失、反向、更新,一个步骤都没少。它变的不是训练原则,而是前向图里的信息流。
十三、关键概念回顾
神经网络最小的计算单元是“线性打分 + 非线性激活”。很多这样的单元并排起来是一层,很多层串起来就是 MLP。矩阵乘法的作用,不是把公式写得好看,而是让一整个 batch 的一整层神经元一次算完。
前向传播的任务是算出预测值,损失函数的任务是把“错了多少”变成一个可微标量,反向传播的任务是沿计算图把这个误差一层层乘回来,梯度下降的任务则是让参数沿着下降方向挪一步。整套训练闭环就是这四步的重复。
手算标量例子能帮你理解链式法则,矩阵版公式则说明真实工程里为什么能高效 batch 化。框架自动微分没有改变原理,只是替你把链式法则的记账工作做掉了。
普通 MLP 的问题不在于表达力不够,而在于它天然面向固定长度向量,没有状态,也不会自动在不同位置共享序列规则。RNN 正是在这个点上做了最小但关键的改动:让隐藏表示沿时间继续流动。
十四、常见误解
14.1 “神经网络就是模拟大脑”
历史上确实有生物神经元的启发,但现代深度学习更实用的理解方式是“可微函数近似器”。把它理解成矩阵乘法、非线性、损失函数和优化器组成的计算系统,比把它理解成大脑模拟更不容易跑偏。
14.2 “只要层数够深,线性层自己就会变复杂”
不会。线性复合仍然是线性。真正让深度网络拥有复杂表达力的,是层与层之间的非线性。
14.3 “反向传播是一种特殊的 AI 黑魔法”
不是。它就是链式法则沿计算图的系统化应用。标量例子里你手算的每一步,框架都在大规模张量上重复同样的事。
14.4 “MLP 能万能逼近,所以序列任务也不需要 RNN”
万能逼近说的是固定输入维度下的表达能力,不是结构上对序列是否合适。序列长度可变、规则需要跨位置共享、历史需要形成状态,这些都是结构问题。
14.5 “有了 Transformer,就不用再学 MLP 和反向传播了”
正好相反。Transformer 每层里都有 FFN,本质就是 MLP;它的训练也完全依赖前向、损失、反向、更新这条闭环。你不理解 MLP 和反向传播,就只能把 Transformer 当成一堆会动的公式。
十五、下一步
到这里,普通神经网络这台机器已经立住了。下一篇的问题不再是“神经网络怎么训练”,而是“这台机器怎样改造之后才能处理变长序列”。
09 RNN 与序列建模
会从最基本的循环结构讲起,把
h_t = f(x_t, h_{t-1}) 这件事彻底展开,然后进入
BPTT、梯度消失爆炸、LSTM、GRU 和
Seq2Seq。你可以把它看成本文的自然下一步:把隐藏表示从一张静态计算图里的中间量,变成沿时间持续流动的状态。
再往后,10 RNN 的根本局限 会解释为什么“状态沿时间传递”这条路最终走到了天花板,以及 Transformer 为什么能从 RNN 的补丁演化成新的主角。
十六、参考文献
- McCulloch, W. S., Pitts, W. (1943). A Logical Calculus of the Ideas Immanent in Nervous Activity. Bulletin of Mathematical Biophysics. 形式神经元模型的早期源头。
- Rosenblatt, F. (1958). The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain. Psychological Review. 感知机模型。
- Rumelhart, D. E., Hinton, G. E., Williams, R. J. (1986). Learning representations by back-propagating errors. Nature. 多层网络反向传播的经典论文。
- Cybenko, G. (1989). Approximation by superpositions of a sigmoidal function. Mathematics of Control, Signals and Systems. 万能逼近定理的经典结果之一。
- Hornik, K., Stinchcombe, M., White, H. (1989). Multilayer feedforward networks are universal approximators. Neural Networks. 多层前馈网络表达能力。
- LeCun, Y., Bottou, L., Bengio, Y., Haffner, P. (1998). Gradient-Based Learning Applied to Document Recognition. Proceedings of the IEEE. 梯度学习工程化的代表工作。
- Bishop, C. M. (2006). Pattern Recognition and Machine Learning. Springer. 概率视角下的机器学习教材。
- Goodfellow, I., Bengio, Y., Courville, A. (2016). Deep Learning. MIT Press. 深度学习教材。
- Elman, J. L. (1990). Finding Structure in Time. Cognitive Science. 现代 RNN 的早期代表工作。
下一篇:09 RNN 与序列建模
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Transformer 与注意力机制】09 RNN 与序列建模:Transformer 之前的世界
在 Transformer 出现之前,序列建模属于 RNN 的世界。本文从 Vanilla RNN 讲起,经过 BPTT、梯度消失爆炸、LSTM、GRU,到 Sutskever 2014 的 Seq2Seq 框架,完整讲述 RNN 时代的故事和它留下的工程经验。
【Transformer 与注意力机制】系列总览
从《Attention Is All You Need》出发,把注意力机制、Transformer 架构、训练范式、模型变体、推理工程、可解释性与未来架构串成一条 58 篇主线加一篇桥接文的深度博客线。
【Transformer 与注意力机制】06|梯度下降与反向传播
神经网络真正会「学习」靠的是两件事:把误差变成可微分的损失函数,再沿着这个损失对参数的梯度方向一点点往下挪。本文从一维抛物线讲到多变量梯度,从两层网络的手算反向传播讲到为什么 backprop 是 O(参数量),再到 Transformer 为什么几乎一律选 Adam/AdamW,希望把「网络是怎么学的」这件事彻底讲透。
【Transformer 与注意力机制】01|为什么要从这里开始
这是【Transformer 与注意力机制】系列的第一篇,承担两件事:一是把这套五十多篇文章为谁写、解决什么问题、彼此之间是什么关系交代清楚;二是为完全没基础的读者画出一条从向量、点积、矩阵乘法走到自注意力、再走到大语言模型的爬升路径,让你在投入时间之前先知道终点在哪、路上要经过哪些坎、读完之后你会、还不会做什么事。