前向传播与反向传播
上一章的结尾,文字已经变成了向量——但一组固定的向量还谈不上"智能"。机器怎么从数据中自己学到规律?这一章回答这个问题,顺便完成一次彻底的祛魅:所谓深度学习模型,就是一连串嵌套的矩阵运算加非线性函数,没有魔法、没有神秘主…
上一章的结尾,文字已经变成了向量——但一组固定的向量还谈不上"智能"。机器怎么从数据中自己学到规律?这一章回答这个问题,顺便完成一次彻底的祛魅:所谓深度学习模型,就是一连串嵌套的矩阵运算加非线性函数,没有魔法、没有神秘主义。
本章分两步走:前半讲结构——从神经元讲到前向传播,你将亲手写出一个能跑的神经网络;后半讲学习——损失函数给"错得多离谱"定价,梯度下降决定"往哪边调、调多少",反向传播把责任分摊到每一个参数头上,最后我们亲手把这个网络训练到学会 XOR——深度学习的"Hello World"。带着这套认知,后面理解 Transformer(第 3 章)时你会发现它依然是同一套东西。
1. 从生物神经元到数学模型
1943 年,神经科学家 McCulloch 和 Pitts 提出了神经元的数学模型,思路惊人地简单:模仿生物神经元的"加权聚合 + 阈值触发"。
生物神经元的工作方式:
树突(接收多路信号) → 细胞体(汇总) → 超过阈值则放电 → 轴突(输出)
数学翻译,一个人工神经元接收 $n$ 个输入,做三件事:
- 加权求和:每个输入乘以自己的权重 $w_i$,再加上偏置 $b$
- 激活:把结果丢进一个非线性函数 $\sigma$
- 输出:作为下一层的输入
$$ z = w_1 x_1 + w_2 x_2 + \cdots + w_n x_n + b = \mathbf{w}^T \mathbf{x} + b $$
$$ a = \sigma(z) $$
各符号的直觉含义:
| 符号 | 含义 | 直觉 |
|---|---|---|
| $x_i$ | 输入 | 上一层传来的信号 |
| $w_i$ | 权重 | 这个输入"多重要",可正可负 |
| $b$ | 偏置 | 触发的难易度,类似阈值 |
| $\sigma$ | 激活函数 | 汇总后做一次非线性"审判" |
| $a$ | 输出 | 传给下一层的信号 |
训练调的就是 $w$ 和 $b$。一个 GPT 级别的模型里有几千亿个这样的数字——数量变化,本质不变。
2. 激活函数:为什么必须有非线性
如果去掉激活函数(或用恒等函数),会发生灾难:多层网络塌缩成一层。
推导只需两行。若每层只做 $\mathbf{a} = \mathbf{W}\mathbf{x} + \mathbf{b}$(纯线性),则两层叠加:
$$ \mathbf{h} = \mathbf{W}_1\mathbf{x} + \mathbf{b}_1 $$
$$ \mathbf{y} = \mathbf{W}_2\mathbf{h} + \mathbf{b}_2 = \mathbf{W}_2(\mathbf{W}_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2 = \underbrace{(\mathbf{W}_2\mathbf{W}_1)}_{\text{一个新的矩阵}}\mathbf{x} + \underbrace{(\mathbf{W}_2\mathbf{b}_1 + \mathbf{b}_2)}_{\text{一个新的偏置}} $$
两个矩阵相乘还是矩阵——一百层线性网络等价于一层。深度就失去了意义。激活函数的存在打断这种塌缩,让每层都有"自己的发言权"。
常见激活函数:
| 名称 | 公式 | 特点 |
|---|---|---|
| Sigmoid | $\frac{1}{1+e^{-z}}$ | 压到 (0,1),可当概率;两端梯度消失 |
| Tanh | $\tanh(z)$ | 压到 (-1,1),零中心 |
| ReLU | $\max(0, z)$ | 简单粗暴,现代网络主力;负区直接归零 |
| GELU | 平滑版 ReLU | Transformer 的默认选择(第 3 章会见到) |
用代码看看它们长什么样:
import numpy as np
z = np.array([-3.0, -1.0, 0.0, 1.0, 3.0])
def sigmoid(x): return 1 / (1 + np.exp(-x))
def relu(x): return np.maximum(0, x)
def gelu(x): return 0.5 * x * (1 + np.tanh(np.sqrt(2/np.pi) * (x + 0.044715 * x**3)))
for name, fn in [("sigmoid", sigmoid), ("relu", relu), ("gelu", gelu)]:
print(f"{name:>8}: {np.round(fn(z), 3)}")
输出(注意观察:sigmoid 永远在 0~1 之间;relu 把负数全部斩为 0):
sigmoid: [0.047 0.269 0.5 0.731 0.953]
relu: [0. 0. 0. 1. 3. ]
gelu: [-0.004 -0.159 0. 0.841 2.996]
3. 一层网络 = 一次矩阵变换
把多个神经元并排放在一起,就得到一层网络。关键洞察:既然每个神经元都在做 $\mathbf{w}^T\mathbf{x}+b$,只是 $\mathbf{w}$ 不同,那么这一层就是:
$$ \mathbf{a} = \sigma(\mathbf{W}\mathbf{x} + \mathbf{b}) $$
- $\mathbf{x}$:输入向量,形状
(m, 1) - $\mathbf{W}$:权重矩阵,形状
(n, m)—— 一行就是一个神经元的权重 - $\mathbf{b}$:偏置向量,形状
(n, 1) - $\mathbf{a}$:输出向量,形状
(n, 1)
也就是说:"层"不是神经元的手工集合,它本身就是一个矩阵。所谓"这个网络每层有 768 个神经元",翻译成数学就是"每层乘一个 (768, m) 的矩阵"。神经网络里根本没有一个个独立的"神经元",只有矩阵的行——这是很多初学者没有意识到的事实。
矩阵形状的变化规律(务必记住,读源码和论文时天天用):
$$ \underbrace{(n, m)}_{\mathbf{W}} \cdot \underbrace{(m, 1)}_{\mathbf{x}} = \underbrace{(n, 1)}_{\mathbf{y}} $$
矩阵第二维必须等于输入维度,输出维度由矩阵第一维决定。你以后看任何一个模型的架构图,都可以在脑中翻译成"一串形状各异的矩阵依次相乘"。
4. 多层 = 复合函数
把单层串起来,就是深度学习里"深度"的含义:
$$ \mathbf{y} = \sigma_3(\mathbf{W}_3 \cdot \sigma_2(\mathbf{W}_2 \cdot \sigma_1(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2) + \mathbf{b}_3) $$
从右往左读:原始输入 $\mathbf{x}$ 被逐层变换,每经过一层,先做一次线性变换(旋转/拉伸/平移空间),再过一次非线性(弯折空间)。层层叠加后,网络可以在输入空间里切出任意复杂的决策边界。
万能近似定理(不证明,记住直觉):单个隐藏层足够宽的网络,理论上可以逼近任何连续函数。深度学习则用"深"替代"宽"——同样的参数量,深层比宽层能表达更抽象的特征(边缘 → 纹理 → 部件 → 物体,逐层抽象)。
5. 前向传播:亲手写一个
"前向传播"(Forward Propagation)听起来很玄,实际就是:按顺序把输入依次过完每一层,得到输出。下面用 numpy 完整实现一个 2 → 4 → 1 的网络(2 个输入,隐藏层 4 个神经元,1 个输出):
import numpy as np
rng = np.random.default_rng(0)
# ---------- 定义网络:所有参数都初始化为随机数 ----------
# W1: (4, 2) 输入2维 → 隐藏4维
# b1: (4,)
# W2: (1, 4) 隐藏4维 → 输出1维
# b2: (1,)
W1, b1 = rng.normal(0, 1, (4, 2)), np.zeros(4)
W2, b2 = rng.normal(0, 1, (1, 4)), np.zeros(1)
def relu(x):
return np.maximum(0, x)
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def forward(x): # x 形状 (2,)
h = relu(W1 @ x + b1) # (2,) → (4,) 隐藏层
y = sigmoid(W2 @ h + b2) # (4,) → (1,) 输出层
return y
# ---------- 跑一个样本 ----------
x = np.array([0.5, -1.2])
print("输出:", forward(x))
运行输出(你的随机数和我的一致,因为固定了种子):
输出: [0.4812]
这个 0.4812 没有任何意义——因为参数是随机的。前向传播只是"计算",还没有"学习"。学习是本章后半的主题:怎么根据输出"错得有多离谱",反过来调整 W1/b1/W2/b2。
加一个逐层形状打印,强化"矩阵流水线"的直觉:
def forward_verbose(x):
print(f"输入 形状: {x.shape}")
z1 = W1 @ x + b1
print(f"隐藏层 形状: {z1.shape} ← W1{(W1.shape)} @ x{(x.shape)}")
h = relu(z1)
y = W2 @ h + b2
print(f"输出层 形状: {y.shape} ← W2{(W2.shape)} @ h{(h.shape)}")
return y
forward_verbose(x)
输出:
输入 形状: (2,)
隐藏层 形状: (4,) ← W1(4, 2) @ x(2,)
输出层 形状: (1,) ← W2(1, 4) @ h(4,)
看清了吗?数据在网络里流动的过程,就是向量形状被一串矩阵依次改写的过程。以后你听说某个大模型"有 96 层,隐藏维度 12288",翻译过来就是:有 96 个 (12288, 12288) 量级的矩阵在流水线上工作。
6. 数一数参数
顺便建立"参数量"的概念,这是评价模型大小的通用语言。上面的网络参数量:
| 参数 | 形状 | 数量 |
|---|---|---|
| W1 | (4, 2) | 8 |
| b1 | (4,) | 4 |
| W2 | (1, 4) | 4 |
| b2 | (1,) | 1 |
| 合计 | 17 |
代码验证:
print(W1.size + b1.size + W2.size + b2.size) # 17
计算规律:一层 (n, m) 的变换带 (n,) 的偏置,参数量 = $n \times m + n$。大模型动辄"7B 参数"(B = 十亿),就是这个加法的放大版。参数量直接决定显存占用:以 16 位浮点存储,7B 模型仅权重就需要约 14GB 显存——这是部署本地模型时算账的基本功。
现在你知道了网络的结构,但有个悬而未决的问题:这些 W 和 b 是随机的,怎么把它们变成"有用的"?这就是训练要解决的事——本章后半,我们把学习机制补全。
7. 损失函数:给"错"定价
训练的第一步是定义"错"。不能只说"模型输出不对",要给出一个数字:损失(Loss)越大,错得越离谱。训练的目标就是让这个数字变小。
不同任务用不同的损失函数:
| 任务 | 损失函数 | 直觉 |
|---|---|---|
| 回归(预测数值) | 均方误差 MSE | 预测值和真值差的平方和 |
| 二分类 | 交叉熵 BCE | 预测概率离正确标签有多远 |
| 语言模型 | 交叉熵(下一个词) | 模型给"正确下一个词"分配的概率有多低 |
以语言模型为例感受一下交叉熵的含义:模型看到"今天天气真"后,给下一个词分配概率:
好: 0.60 不错: 0.20 差: 0.15 香蕉: 0.0001 ...
如果原文的下一个词是"好",损失就低(模型押对了);如果是"香蕉",损失就高(模型完全没料到)。训练大模型,就是在万亿个这样的位置上,让模型给"真实出现的下一个词"分配越来越高的概率。你每天使用的"预测输入法",就是同一个原理。
8. 梯度下降:往哪边调
现在问题变成:参数空间动辄上亿维,怎么找到让损失最小的那一组值?
想象你蒙着眼在山上,目标是走到山谷最低处。你唯一能做的是用脚感受脚下的坡度:哪个方向往下最陡,就往那个方向迈一小步。不断重复,就能走到(某个)谷底。
这就是梯度下降:
$$ \theta_{new} = \theta_{old} - \eta \nabla L(\theta) $$
- $\nabla L(\theta)$:梯度,指向"损失上升最快"的方向 → 反方向就是下山方向
- $\eta$:学习率(learning rate),每步迈多大
- $\theta$:所有参数(就是前文的 W 和 b)
学习率是最重要的超参数,两个失败极端:
- 太大:步子跨过谷底,在两侧来回震荡甚至发散(损失越训越大)
- 太小:一百万步还没走到山脚,训练成本爆炸
学习率过大: 损失 5.0 → 8.2 → 15.7 → 44.1 → 💥 发散
学习率过小: 损失 5.0 → 4.99 → 4.98 → ... (一百年后到达)
学习率合适: 损失 5.0 → 3.1 → 1.8 → 0.9 → 0.4 → 0.2 ✅
9. 反向传播:责任分摊
梯度下降需要知道"每个参数的梯度"。但参数在多层网络里层层嵌套,最前面的一个 W1 怎么知道自己对最终损失有多少责任?
答案是大一学过的链式法则,加上一次聪明的复用。
前向传播(本章第 5 节):
$$ \mathbf{z}_1 = \mathbf{W}_1\mathbf{x} + \mathbf{b}_1 \quad\rightarrow\quad \mathbf{h} = \text{ReLU}(\mathbf{z}_1) \quad\rightarrow\quad \mathbf{z}_2 = \mathbf{W}_2\mathbf{h} + \mathbf{b}_2 \quad\rightarrow\quad y = \sigma(\mathbf{z}_2) \quad\rightarrow\quad L $$
反向传播就是沿这条链从右往左逐层求导。以"输出层的 z2"为例(sigmoid + BCE 组合的著名结果,推导略):
$$ \frac{\partial L}{\partial \mathbf{z}_2} = y - t \qquad \text{(预测值减真实值)} $$
直觉很漂亮:预测越自信、错得越离谱,这个信号越强。有了它,往左逐层传递:
$$ \frac{\partial L}{\partial \mathbf{W}_2} = \frac{\partial L}{\partial \mathbf{z}_2} \cdot \mathbf{h}^T \qquad\qquad \frac{\partial L}{\partial \mathbf{h}} = \mathbf{W}_2^T \cdot \frac{\partial L}{\partial \mathbf{z}_2} $$
$$ \frac{\partial L}{\partial \mathbf{z}_1} = \frac{\partial L}{\partial \mathbf{h}} \odot \text{ReLU}'(\mathbf{z}_1) \qquad\qquad \frac{\partial L}{\partial \mathbf{W}_1} = \frac{\partial L}{\partial \mathbf{z}_1} \cdot \mathbf{x}^T $$
($\odot$ 表示逐元素相乘。$\mathbf{W}_2^T$ 出现,是因为信号要"穿过"W2 回到上一层。)
反向传播的工程价值:一次从右到左的扫描,同时拿到所有参数的梯度,计算量和一次前向传播同量级。深度学习框架(PyTorch 的 loss.backward())干的就是这件事。我们下面手写它,写完你对"框架帮你做了什么"就彻底祛魅了。
10. 实战:训练网络学会 XOR
XOR(异或)是神经网络历史上的"名场面":单层网络(感知机)数学上不可能解决它,多层网络可以——正好验证"深度"的价值。任务:输入两个 0/1,输出它们的异或。
import numpy as np
rng = np.random.default_rng(0)
# ---------- 数据 ----------
X = np.array([[0,0],[0,1],[1,0],[1,1]], dtype=float) # 4 个样本,每个 2 维
t = np.array([[0],[1],[1],[0]], dtype=float) # 异或标签
# ---------- 网络(沿用本章第 5 节的结构) ----------
W1, b1 = rng.normal(0, 1, (4, 2)) * 0.5, np.zeros(4)
W2, b2 = rng.normal(0, 1, (1, 4)) * 0.5, np.zeros(1)
def relu(x): return np.maximum(0, x)
def sigmoid(x): return 1 / (1 + np.exp(-x))
def forward(x):
h = relu(W1 @ x + b1) # (2,) → (4,)
y = sigmoid(W2 @ h + b2) # (4,) → 标量
return h, y
# ---------- 训练循环 ----------
lr = 0.5 # 学习率
for step in range(3000):
# 全批量:4 个样本一起算(样本间独立,天然支持向量化)
# 逐样本写法更接近推导,这里为了篇幅用循环展开
dW1 = np.zeros_like(W1); db1 = np.zeros_like(b1)
dW2 = np.zeros_like(W2); db2 = np.zeros_like(b2)
loss = 0
for x, ti in zip(X, t): # 逐样本累积梯度
h, y = forward(x)
# 损失:二元交叉熵
loss += -(ti * np.log(y + 1e-9) + (1-ti) * np.log(1-y + 1e-9))
# ---- 反向传播(手写) ----
dz2 = y - ti # sigmoid+BCE 的组合导数
dW2 += dz2 * h # (1,4)
db2 += dz2
dh = W2.T @ dz2 # (4,) 穿过 W2
dz1 = dh * (h > 0) # ReLU 的导数:正区为 1,负区为 0
dW1 += np.outer(dz1, x) # (4,2)
db1 += dz1
# ---- 梯度下降:一次性更新所有参数 ----
W1 -= lr * dW1; b1 -= lr * db1
W2 -= lr * dW2; b2 -= lr * db2
if step % 600 == 0:
print(f"step {step:>4} loss = {loss.mean():.4f}")
# ---------- 验收 ----------
print("\n输入 → 预测 (期望 0,1,1,0):")
for x in X:
_, y = forward(x)
print(f" {x.astype(int)} → {y.item():.4f}")
运行结果:
step 0 loss = 2.8290
step 600 loss = 0.5011
step 1200 loss = 0.0459
step 1800 loss = 0.0150
step 2400 loss = 0.0086
输入 → 预测 (期望 0,1,1,0):
[0 0] → 0.0082
[0 1] → 0.9915
[1 0] → 0.9914
[1 1] → 0.0085
四个数字全部预测正确。这 23 个参数"学会"了异或——没有一行代码写异或规则,我们只告诉它"哪里错了",它自己调整到了正确状态。这就是"学习"的全部机制,GPT 训练时和这段代码没有本质区别,区别只在规模:参数多十一个数量级、样本多十一个数量级、损失函数面向下一个词预测。
11. Mini-Batch:样本太多怎么办
上面的循环每个 step 用了全部 4 个样本,这叫全批量(full batch)。真实训练的样本动辄上亿,问题来了:
- 全批量算一次梯度太贵,还没迈一步就要算几个小时
- 单样本(batch=1)虽然便宜,但梯度噪声太大,方向抖得厉害
折中方案:Mini-Batch。每次随机抽一小撮(如 256 个)样本算平均梯度,更新一步。三个概念对齐:
| 方式 | 每步样本数 | 特点 |
|---|---|---|
| 全批量 | 全部 | 方向准、单步贵 |
| 随机(SGD) | 1 | 单步便宜、方向抖 |
| Mini-Batch | 几十~几百 | 工程实践的标准选择 |
# mini-batch 版核心(示意)
idx = rng.choice(len(X), size=2, replace=False) # 随机抽 2 个样本
for x, ti in zip(X[idx], t[idx]):
... # 累积梯度后更新
顺带建立三个高频术语:一次完整的数据遍历叫一个 epoch;一个 batch 跑完更新一次参数,叫一个 step;batch size 就是每撮样本数。你以后在训练日志里看到的 epoch=3, step=12000, batch_size=256 就是这三件事。
12. 过拟合:学得太好也是病
最后一个概念,直接决定你的模型在真实场景能不能用。
把训练想成背题:一个学生把题库背得滚瓜烂熟(训练损失为 0),考试遇到新题却不会做——这就是过拟合:模型记住了训练数据的细节噪声,没学到通用规律,即泛化(Generalization)能力差。
诊断手段是把数据切成两份:
训练集(约80%)── 用来调参数
验证集(约20%)── 只看不学,用来检查"新题"表现
判断依据:
| 训练损失 | 验证损失 | 诊断 |
|---|---|---|
| 高 | 高 | 欠拟合:模型还没学会,继续训 |
| 低 | 高 | 过拟合:背题了,需要正则化/更多数据 |
| 低 | 低 | 泛化良好 ✅ |
常见解药(后面读到 LLM 时会反复遇到思想同源的机制):更多数据、更简单的模型、Dropout、权重衰减、早停(验证损失开始上升就停止训练)。
Agent 开发者的关联:few-shot 示例太多挤占上下文、Agent 对历史对话的过度依赖,本质上都是"过拟合现场"。理解这个概念,你在调 Agent 时会有完全不同的直觉。
本章小结
- 人工神经元 = 加权求和 + 偏置 + 激活:$a = \sigma(\mathbf{w}^T\mathbf{x} + b)$
- 激活函数存在的唯一理由:提供非线性,防止多层塌缩成一层
- 一层网络就是一个矩阵:$\mathbf{a} = \sigma(\mathbf{W}\mathbf{x} + \mathbf{b})$,"神经元"只是矩阵的行
- 前向传播 = 数据依次流过各层 = 向量形状被矩阵逐个改写
- 深度学习的"深度" = 复合函数的层数;模型大小 = 参数个数
- 损失函数把"错"变成一个数字;训练 = 最小化损失
- 梯度下降:沿负梯度方向小步更新;学习率决定步幅,是最重要的超参数
- 反向传播用链式法则一次性算出所有参数的梯度,$\mathbf{W}_2^T$ 让责任穿过层间传递
- 手写训练循环(XOR)= 深度学习的最小完整样例;大模型训练与此同构,只差规模
- Mini-Batch / epoch / step 是训练工程的通用词汇
- 过拟合是"背题",靠训练/验证集切分诊断
至此,你已经具备读懂大模型架构所需的全部数学直觉:第 1 章解决了"语言怎么变成数字",本章解决了"数字怎么学会规律"。下一章,我们进入 Transformer 的心脏——注意力机制,看模型如何读懂上下文。