土法炼钢兴趣小组的算法知识备份

【Transformer 与注意力机制】08.5 神经网络基础:从 MLP 到 RNN 的最后一块地基

文章导航

分类入口
transformer
标签入口
#transformer#neural-network#mlp#backpropagation#deep-learning#rnn

源码下载

本文相关源码已整理,共 1 个文件。

打开下载目录 →

目录

一、先把整条链放在桌上

很多教材一上来就给你一个大词:神经网络能逼近复杂函数。这个说法没有错,但太抽象。更实用的理解方式是:神经网络是一台可微的函数机器,它不断重复做四件事

第一,拿着当前参数做一次前向传播,算出预测值。

第二,用损失函数衡量这次预测离真实答案差多远。

第三,用链式法则把这个误差沿计算图反向传回去,算出每个参数的梯度。

第四,用梯度下降或它的变种更新参数,让下一次前向传播更接近目标。

如果把一轮训练压成最短的伪流程,其实就是:

  1. prediction = model(input)
  2. loss = criterion(prediction, target)
  3. loss.backward()
  4. 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 变成概率分布。

为了把概念彻底钉住,我们先看一个最小的标量前向例子。取:

那么前向传播按顺序是:

\[ 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_1b_1 的导数就是 1,所以:

\[ \frac{dL}{db_1} = 0.450517, \frac{dL}{db_2} = 0.766232 \]

这就是反向传播的全部:把最终误差乘上每一层的局部导数,一层层传回去。如果学习率 η = 0.1,那么这一步更新以后:

参数会往损失下降的方向挪一点。下一次前向传播,\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 维上求和,得到偏置梯度。你会发现,矩阵版和标量版没有本质区别:

这也是为什么说反向传播本质上不是“深度学习黑魔法”,而是普通链式法则的批量矩阵化实现。

九、怎么实现:先自己写一遍,再看框架版

到这里,最好的办法不是继续背公式,而是自己写一遍。下面这段代码经删减,只保留同目录脚本 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 训练实验:

toy MLP 的拟合曲线和 loss 下降过程

左图是同一个 MLP 在不同训练步数下的拟合结果。你能看到,刚开始几乎是一条乱线;80 步时开始有点形状;400 步时已经抓住了大致弯曲;2600 步时基本贴上目标曲线。右图则是 loss 的对数坐标曲线,它从高位一路往下掉,最后停在 0.033948

这里还有两个值得顺手说明的点。

第一,这个 toy experiment 用的是全 batch 更新,不是 mini-batch。原因很简单:教学上这样更干净,图也更稳定。真实深度学习训练通常用 mini-batch,因为数据更大、噪声更有利于优化、GPU 也更适合按 batch 吞数据。

第二,这个实验不是在“证明神经网络很强”,而是在证明训练闭环是可工作的。只要你把前向、损失、反向和更新写对,一台很小的 MLP 就能把一条非线性曲线从不会拟合,慢慢训到会拟合。

十一、为什么普通 MLP 不适合序列

到这里,我们已经把普通神经网络从头走通了:它能表达什么,怎么前向,怎么定义损失,怎么求导,怎么训练。现在终于可以问下一个关键问题:既然 MLP 已经能拟合复杂函数,为什么还需要 RNN?

原因不是“MLP 不够强”,而是“MLP 的信息流不适合序列”。下面这张图把问题画得很清楚:

固定窗口 MLP 和带状态的 RNN 在序列上的信息流差异

上半部分是把一段固定长度序列直接拍平成一个大向量,再交给 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 的补丁演化成新的主角。

十六、参考文献

  1. McCulloch, W. S., Pitts, W. (1943). A Logical Calculus of the Ideas Immanent in Nervous Activity. Bulletin of Mathematical Biophysics. 形式神经元模型的早期源头。
  2. Rosenblatt, F. (1958). The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain. Psychological Review. 感知机模型。
  3. Rumelhart, D. E., Hinton, G. E., Williams, R. J. (1986). Learning representations by back-propagating errors. Nature. 多层网络反向传播的经典论文。
  4. Cybenko, G. (1989). Approximation by superpositions of a sigmoidal function. Mathematics of Control, Signals and Systems. 万能逼近定理的经典结果之一。
  5. Hornik, K., Stinchcombe, M., White, H. (1989). Multilayer feedforward networks are universal approximators. Neural Networks. 多层前馈网络表达能力。
  6. LeCun, Y., Bottou, L., Bengio, Y., Haffner, P. (1998). Gradient-Based Learning Applied to Document Recognition. Proceedings of the IEEE. 梯度学习工程化的代表工作。
  7. Bishop, C. M. (2006). Pattern Recognition and Machine Learning. Springer. 概率视角下的机器学习教材。
  8. Goodfellow, I., Bengio, Y., Courville, A. (2016). Deep Learning. MIT Press. 深度学习教材。
  9. Elman, J. L. (1990). Finding Structure in Time. Cognitive Science. 现代 RNN 的早期代表工作。

上一篇:08 嵌入:从 one-hot 到分布式表示

下一篇:09 RNN 与序列建模

回到:Transformer 与注意力机制 总览

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-15 · transformer

【Transformer 与注意力机制】系列总览

从《Attention Is All You Need》出发,把注意力机制、Transformer 架构、训练范式、模型变体、推理工程、可解释性与未来架构串成一条 58 篇主线加一篇桥接文的深度博客线。

2026-04-15 · transformer

【Transformer 与注意力机制】06|梯度下降与反向传播

神经网络真正会「学习」靠的是两件事:把误差变成可微分的损失函数,再沿着这个损失对参数的梯度方向一点点往下挪。本文从一维抛物线讲到多变量梯度,从两层网络的手算反向传播讲到为什么 backprop 是 O(参数量),再到 Transformer 为什么几乎一律选 Adam/AdamW,希望把「网络是怎么学的」这件事彻底讲透。

2026-04-15 · transformer

【Transformer 与注意力机制】01|为什么要从这里开始

这是【Transformer 与注意力机制】系列的第一篇,承担两件事:一是把这套五十多篇文章为谁写、解决什么问题、彼此之间是什么关系交代清楚;二是为完全没基础的读者画出一条从向量、点积、矩阵乘法走到自注意力、再走到大语言模型的爬升路径,让你在投入时间之前先知道终点在哪、路上要经过哪些坎、读完之后你会、还不会做什么事。


By .