chapter 03 / deep-learning · 预计学习时间 150-180 分钟

深度学习基础
学特征的函数

AUDIO // 本章语音导读
本章目录
  1. 回收伏笔:XOR 与线性的天花板
  2. 多层感知机与万能逼近定理
  3. 反向传播:完整推导
  4. 交互实验室:亲手训练一个神经网络
  5. 激活函数演化史
  6. 优化器演进:从 SGD 到 AdamW
  7. 初始化:起点决定命运
  8. 正则化与归一化
  9. 代码实战:NumPy 手写反向传播 vs PyTorch
  10. 章节测验

回收伏笔:XOR 与线性的天花板

第 2 章实验室里,逻辑回归在 XOR 数据上无论怎么训练都只有 50% 准确率——它的假设空间里只有直线。当时给了三条出路:手工造特征、核方法、让模型自己学特征。前两条的共同问题是:特征(或核)是人选的,选错全盘皆输。第三条路就是神经网络:

$$\text{逻辑回归:}\ \hat{p} = \sigma(\mathbf{w}^\top \mathbf{x} + b) \qquad\Longrightarrow\qquad \text{神经网络:}\ \hat{p} = \sigma\big(\mathbf{w}_2^\top\, \underbrace{g(W_1 \mathbf{x} + \mathbf{b}_1)}_{\text{学出来的新特征 } \mathbf{a}}\, + b_2\big)$$

结构上只是「先做一层线性变换+非线性 $g$,再做逻辑回归」。但意义是革命性的:隐藏层 $\mathbf{a} = g(W_1\mathbf{x}+\mathbf{b}_1)$ 就是模型自己学出来的特征。对 XOR,网络会自己发现类似「$x$ 和 $y$ 是否同侧」的两个中间特征——在这个新空间里,XOR 变成线性可分的。深度学习的全部秘密浓缩成一句话:逐层学习越来越好的表示,直到最后一层用一个线性模型收尾。

非线性 $g$ 绝对不能省。若 $g$ 是恒等函数,两层线性 $W_2(W_1\mathbf{x}) = (W_2 W_1)\mathbf{x}$ 塌缩成一层——堆一百层也还是逻辑回归。没有非线性,就没有深度。

多层感知机与万能逼近定理

把上面的结构推广:$L$ 层网络,第 $\ell$ 层 $\mathbf{a}^{(\ell)} = g\big(W^{(\ell)} \mathbf{a}^{(\ell-1)} + \mathbf{b}^{(\ell)}\big)$。理论保证来自万能逼近定理(Cybenko 1989 / Hornik 1991):单隐藏层网络只要隐藏单元足够多,就能以任意精度逼近紧集上的任意连续函数。

这个定理常被误读。它说的是「存在这样的权重」,没说梯度下降找得到、没说需要多少单元(可能指数多)、更没说能泛化。它回答了「神经网络的表达力够不够」,而深度学习真正的奇迹是另外两件事:①深而窄的网络比浅而宽的指数级高效(深度的价值:特征的层层复用);②高度非凸的损失面上,SGD 居然总能找到泛化良好的解——这件事的完整理论解释至今(2026 年)仍是开放问题。工程跑在了理论前面,这是本领域的常态。

反向传播:完整推导

训练还是老三样:模型(上面)、损失(交叉熵)、优化(梯度下降)。唯一的新问题:几百万个参数的梯度怎么算得快?答案是链式法则 + 动态规划,名字叫反向传播。

两层网络逐步推

设前向:$\mathbf{z}_1 = W_1\mathbf{x}+\mathbf{b}_1,\ \mathbf{a}_1 = g(\mathbf{z}_1),\ z_2 = \mathbf{w}_2^\top\mathbf{a}_1 + b_2,\ \hat p = \sigma(z_2)$,损失 $L$ 为交叉熵。从输出端往回,一层层用链式法则:

第一步(第 2 章的礼物,直接搬来):

$$\delta_2 \equiv \frac{\partial L}{\partial z_2} = \hat p - y$$

第二步:输出层参数的梯度。$z_2$ 对 $\mathbf{w}_2$ 的偏导就是 $\mathbf{a}_1$:

$$\frac{\partial L}{\partial \mathbf{w}_2} = \delta_2\, \mathbf{a}_1, \qquad \frac{\partial L}{\partial b_2} = \delta_2$$

第三步(关键):误差穿过权重和激活函数传回隐藏层。$z_2$ 依赖 $\mathbf{a}_1$,$\mathbf{a}_1$ 依赖 $\mathbf{z}_1$:

$$\delta_1 \equiv \frac{\partial L}{\partial \mathbf{z}_1} = \underbrace{\delta_2\, \mathbf{w}_2}_{\text{误差按权重分配}} \odot \underbrace{g'(\mathbf{z}_1)}_{\text{乘激活函数的导数}}$$

第四步:隐藏层参数的梯度,形式和第二步完全一样:

$$\frac{\partial L}{\partial W_1} = \delta_1\, \mathbf{x}^\top, \qquad \frac{\partial L}{\partial \mathbf{b}_1} = \delta_1$$

看到模式了吗?每层的梯度 = 该层的误差 $\delta$ × 该层的输入——第 1、2 章「误差×输入」的完全体。而误差 $\delta$ 的传播有一个统一的递推式(任意深的网络都适用):

$$\boxed{\ \delta^{(\ell)} = \big(W^{(\ell+1)\top} \delta^{(\ell+1)}\big) \odot g'(\mathbf{z}^{(\ell)})\ }$$

这就是「反向传播」四个字的全部内容:前向算一遍存下各层激活($O(n)$),反向用递推把 $\delta$ 从输出层传回输入层(又一个 $O(n)$)。计算全部参数的梯度只比算一次前向贵约两倍,与参数数量无关——没有这个性质,万亿参数的 GPT 根本不可能训练。PyTorch 的 loss.backward() 内部就是在计算图上跑这套递推。

VIDEO 01
What is backpropagation really doing?(反向传播到底在干什么)
3Blue1Brown · 深度学习系列 第3集 12:47
观看指南
  • 03:30 单个训练样本「希望」每个权重怎么变——梯度的民主投票直觉。
  • 07:00 误差如何按权重比例往回分配——对应递推式里的 $W^\top\delta$。
  • 09:30 mini-batch 为什么是对全量梯度的无偏估计(回扣第 1 章 SGD)。
VIDEO 02 · 进阶选看
Backpropagation calculus(反向传播的微积分)
3Blue1Brown · 深度学习系列 第4集 10:17
观看指南 · 与本章 §3 推导逐式对应
  • 02:00 链式法则在计算图上的样子——把本章四步推导可视化。
  • 06:30 多神经元情形的下标体操——看完再回读 δ 递推式会非常清晰。

交互实验室:亲手训练一个神经网络

下面是一个真的在你浏览器里跑反向传播的 2-H-1 网络(代码就是 §3 的四步推导,可读 assets/neural.js 源码对照)。必做实验:① H=1 训练 XOR——参数再多也学不会(一个隐藏单元只能折叠一次空间);② H 调到 4 再训练——看决策边界如何「弯」出两块对角区域;③ 螺旋数据 + H=8 + tanh,见证小网络的极限;④ 把激活换成 sigmoid 再训螺旋,感受梯度消失导致的龟速;⑤ 学习率拉满看损失爆炸。背景色深浅 = 模型置信度,浅色地带是它的「犹豫区」。

neural-net.train(2-H-1)

单击画布加绿点 · Shift+单击加紫点 · 改隐藏单元数/激活函数会重新初始化

epoch = 0 loss = 训练准确率 = 参数量 = 17
H=2 理论上足够解 XOR,但实验里有时训练失败、卡在 75% 左右。为什么?多试几次「重新初始化」再回答。
这就是 §2 说的「存在好解 ≠ 梯度下降找得到」的现场演示。H=2 时损失面存在坏的局部极小/平坦区域,随机初始化不走运就会掉进去;H=4 时参数空间更冗余,「下坡路」多得多,几乎每次都能成功。这是深度学习一个反直觉的重要现象:过参数化(参数比理论需要的多)反而让优化变容易——大模型不仅表达力强,还更「好训」。

激活函数演化史

函数公式问题/优势
sigmoid$1/(1+e^{-z})$两端饱和,$\sigma' \le 0.25$,深网络中梯度连乘后指数级消失;输出不以零为中心
tanh$\tanh(z)$零中心改善了 sigmoid,但依然饱和
ReLU$\max(0, z)$正区间导数恒为 1,梯度高速公路;计算近乎免费。代价:「死亡 ReLU」(神经元落入负区间后梯度为 0,永远醒不来)
GELU$z\cdot\Phi(z)$ReLU 的光滑版,GPT/BERT 的选择
SwiGLU$\text{Swish}(W_1 x)\otimes W_2 x$门控结构,LLaMA 及之后多数 LLM 的 FFN 标配(第 6 章见)

为什么 ReLU 是深度学习起飞的隐形功臣?看 $\delta$ 递推式:每往回传一层都要乘一次 $g'$。sigmoid 的 $g' \le 0.25$,传 10 层后梯度至少缩小 $4^{10} \approx$ 百万倍——底层根本学不到东西(梯度消失)。ReLU 在激活的路径上 $g' = 1$,误差无损通行。AlexNet 论文专门用一张图说明 ReLU 比 tanh 快 6 倍收敛——2012 年革命的三要素里,「算法」那一项主要就是它。

优化器演进:从 SGD 到 AdamW

朴素 SGD 的两个痛点:①损失面像狭长峡谷时,梯度在陡峭方向来回震荡、在平缓方向蠕动;②所有参数共用一个学习率,但不同参数的梯度量级可能差几个数量级。两条改进线索最终汇成 Adam:

线索一:动量(Momentum)

$$\mathbf{v}_t = \beta\, \mathbf{v}_{t-1} + (1-\beta)\, \mathbf{g}_t, \qquad \theta_{t+1} = \theta_t - \eta\, \mathbf{v}_t$$

把历史梯度做指数滑动平均:震荡方向正负抵消,一致方向越滚越快——像有质量的小球滚下山。$\beta=0.9$ 约等于平均最近 10 步。

线索二:自适应学习率(RMSProp)

$$\mathbf{s}_t = \beta_2\, \mathbf{s}_{t-1} + (1-\beta_2)\, \mathbf{g}_t^2, \qquad \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\mathbf{s}_t} + \epsilon}\, \mathbf{g}_t$$

每个参数除以自己梯度幅度的滑动均方根:梯度一向很大的参数自动减速,很小的自动加速——每个参数有了私人定制的学习率。

合流:Adam(2014)= 动量 + RMSProp + 偏差修正

$$\hat{\mathbf{m}}_t = \frac{\mathbf{m}_t}{1-\beta_1^t}, \quad \hat{\mathbf{s}}_t = \frac{\mathbf{s}_t}{1-\beta_2^t}, \qquad \theta_{t+1} = \theta_t - \eta\, \frac{\hat{\mathbf{m}}_t}{\sqrt{\hat{\mathbf{s}}_t} + \epsilon}$$

偏差修正解决冷启动问题:$\mathbf{m}_0 = 0$ 导致早期估计向零偏,除以 $(1-\beta^t)$ 校正($t$ 大了该项趋于 1,自动失效)。

AdamW(2017)是当前 LLM 训练的事实标准,与 Adam 只差一处:L2 正则不再混进梯度里被自适应分母缩放,而是解耦成独立的权重衰减步 $\theta \leftarrow (1-\eta\lambda)\theta$。在 Adam 里这两者不等价(L2 进了梯度会被 $\sqrt{\hat s}$ 除掉,正则强度被梯度大小干扰)。一行之差,泛化显著更好——读 LLM 技术报告时你会在超参表里反复见到它(典型配置:$\beta_1{=}0.9,\ \beta_2{=}0.95,\ \lambda{=}0.1$,配 warmup+余弦衰减,第 7 章细讲)。

初始化:起点决定命运

正则化与归一化

正则化:对抗过拟合的三板斧

归一化:让深网络可训练的工程支柱

$$\text{Norm}(x) = \gamma\,\frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta$$

同一个公式,区别只在对谁算 $\mu, \sigma$BatchNorm 沿 batch 维(同一通道跨样本)——视觉标配,但依赖 batch 统计:batch 小了不稳,推理时要用训练期的滑动均值,序列长度可变时很别扭。LayerNorm 沿特征维(单样本内部)——与 batch 无关、对序列天然友好,这就是 Transformer 选它的原因RMSNorm 再省掉减均值,只除均方根,LLaMA 后成为 LLM 默认(第 6 章会出现在你从零实现的 GPT 里)。它们的共同作用:把每层输入拉回稳定分布,损失面更光滑,可以用大学习率。

代码实战:NumPy 手写反向传播 vs PyTorch

左手是 §3 推导的逐行翻译,右手是工业现实——两者必须给出相同的梯度:

python · backprop_from_scratch.py
import numpy as np
rng = np.random.default_rng(0)

# XOR 数据
X = np.array([[0,0],[0,1],[1,0],[1,1]], dtype=float)
y = np.array([0.,1.,1.,0.])

H = 4
W1 = rng.normal(0, np.sqrt(2/2), (H, 2)); b1 = np.zeros(H)   # He 初始化
W2 = rng.normal(0, np.sqrt(2/H), H);      b2 = 0.0
sig = lambda z: 1/(1+np.exp(-z))

for epoch in range(5000):
    # ---- 前向(存下中间量,反向要用)----
    Z1 = X @ W1.T + b1            # (4,H)
    A1 = np.maximum(0, Z1)        # ReLU
    z2 = A1 @ W2 + b2             # (4,)
    p  = sig(z2)
    # ---- 反向:§3 的四步,逐行对应 ----
    d2  = (p - y) / len(X)        # δ₂ = p̂ - y          (第一步)
    gW2 = A1.T @ d2; gb2 = d2.sum()         #            (第二步)
    d1  = np.outer(d2, W2) * (Z1 > 0)       # δ₁ = δ₂W₂ ⊙ g'(第三步)
    gW1 = d1.T @ X;  gb1 = d1.sum(0)        #            (第四步)
    # ---- SGD 更新 ----
    lr = 0.5
    W1 -= lr*gW1; b1 -= lr*gb1; W2 -= lr*gW2; b2 -= lr*gb2

print(np.round(p, 3))   # → 约 [0, 1, 1, 0],XOR 被解开 ✓
python · 同一件事的 PyTorch 版
import torch, torch.nn as nn

X = torch.tensor([[0.,0.],[0.,1.],[1.,0.],[1.,1.]])
y = torch.tensor([[0.],[1.],[1.],[0.]])

model = nn.Sequential(nn.Linear(2, 4), nn.ReLU(), nn.Linear(4, 1))
opt = torch.optim.AdamW(model.parameters(), lr=0.05, weight_decay=0.01)
loss_fn = nn.BCEWithLogitsLoss()   # sigmoid+交叉熵融合算子(数值稳定,§2 章伏笔)

for epoch in range(2000):
    opt.zero_grad()
    loss = loss_fn(model(X), y)
    loss.backward()                # ← 自动跑 §3 的递推,所谓 autograd
    opt.step()

print(torch.sigmoid(model(X)).detach().round().squeeze())  # tensor([0.,1.,1.,0.])

收尾视频是本章的「期末大餐」:Karpathy 从一个空 Python 文件开始,手写整个自动求导引擎 micrograd——看完它,loss.backward() 对你就再无任何神秘感。这是通往第 6 章「从零实现 GPT」的第一块跳板:

VIDEO 03 · 强烈推荐完整跟写
The spelled-out intro to neural networks and backpropagation: building micrograd
Andrej Karpathy · Zero to Hero 第1集 2:25:51
观看指南 · 长视频,建议分两次看完并跟着敲代码
  • 00:00 导数的数值定义热身——一切从 (f(x+h)-f(x))/h 开始。
  • 37:00 Value 对象与计算图的构建——每个运算记住自己的输入和局部导数。
  • 51:00 手动反向传播一遍,再写 backward()——本章 §3 递推式的代码形态。
  • 1:45:00 用 micrograd 训练一个小 MLP——与本章实验室做的事完全相同。

章节测验