工作台课程

CS221 · 人工智能:原理与技术

Lecture 4 · 深度学习

Lecture 4 · 深度学习

CS221: Artificial Intelligence — Principles and Techniques · Stanford · Autumn 2025
📅 Oct 1 · 💻 deep_learning.py


承上启下

上一讲 Lecture 3 · 线性分类:假设类是线性函数,决策边界只能是超平面。损失函数(交叉熵)与优化算法(梯度下降)都已就位,唯一的瓶颈是表达能力——线性模型画不出弯曲的边界。

本讲(深度学习):把假设类扩展为非线性函数(多层神经网络)。表达能力上去了,但代价是损失面变得非凸、梯度在深层网络里会指数级消失或爆炸——所以本讲的真正主线不是「更强的模型」,而是如何把更强的模型训得动:残差连接、层归一化、合适的初始化、Adam 优化器,四件套缺一不可。工具层面则从手写计算图(Lecture 1 · 张量、梯度与监督学习)切换到 PyTorch。

下一讲 Lecture 5 · 搜索算法 I:机器学习单元(Lecture 1–4)到此告一段落,课程转入第二大主题——基于搜索的问题求解。


1. PyTorch 基础

之前我们用 NumPy + 自己的计算图库来真正理解底层原理(Lecture 1 · 张量、梯度与监督学习 中手写过 backpropagation)。实践中应该用 PyTorch(或 JAX),它提供:

  • 高度优化的工业级实现(GPU 加速、算子融合);
  • 自动微分(automatic differentiation)——不必再手推每个损失的梯度;
  • 大量预定义模块(nn.Linearnn.LayerNormnn.CrossEntropyLosstorch.optim.*)。

PyTorch 的性能内幕(张量内存布局、FLOPs 核算)在 CS336 的 Lecture 2 · PyTorch 与资源核算(未找到对应页面) 中有系统展开。

1.1 NumPy vs PyTorch

# 我们的计算图库 + NumPy
x = Input("x", np.array([1, 2, 3]))
y = Input("y", np.array([4, 5, 6]))
z = DotProduct("z", x, y)
backpropagation(z)
print(x.grad, y.grad)

# PyTorch(更简洁)
x = torch.tensor([1., 2, 3], requires_grad=True)
y = torch.tensor([4., 5, 6], requires_grad=True)
z = x @ y
z.backward()
print(x.grad, y.grad)  # 直接访问 .grad 属性

关键差异

  • torch.tensor 本质上就是计算图中的节点——每次运算都在悄悄记录「这个值是怎么算出来的」,供反向传播回溯。
  • 运算符(@ / + / ** 等)与 NumPy 平行,迁移成本很低。
  • 值在节点构造时立即计算(eager execution),不需要先搭图再显式调用 forward()
  • requires_grad=True 标记需要计算梯度的参数;输入数据通常不标记。
  • 调用 .backward() 从该节点出发递归执行反向传播,梯度累积到各叶子节点的 .grad 属性上。

1.2 节点引用 vs 值引用

# 引用节点(梯度会传播)
x = torch.tensor(1., requires_grad=True)
y = x ** 2
z = y ** 2
z.backward()
print(x.grad)  # ✅ 有梯度

# 引用值(梯度不会传播)
x = torch.tensor(1., requires_grad=True)
y = x ** 2
z = y.detach() ** 2  # detach() 切断计算图
z.backward()
print(x.grad)  # ❌ None(梯度没传到 x)

使用场景

  • 测试/推理阶段只做预测、不更新参数:用 with torch.no_grad(): 包住整段前向,省去记录计算图的时间与内存。
  • 某个中间结果想当「常数」用、不希望梯度流经它:用 .detach()(例如强化学习中固定目标网络的输出)。

注意

detach() 不报错、不警告,只是安静地掐断梯度流。如果发现某些参数训练后纹丝不动,先检查计算图是不是被 detach().item().numpy() 或整数索引赋值等操作无声切断了。

1.3 完整训练循环

# 数据
x = torch.tensor([1., 2, 3, 4])
target_y = torch.tensor([0., 1, 0])  # 多分类标签(概率形式,等价于类别索引 1)

# 模型
model = nn.Linear(4, 3)  # 输入维度 4,输出 3 类

# 损失函数
loss_fn = nn.CrossEntropyLoss()

# 优化器
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

# 训练循环
for epoch in range(num_epochs):
    logits = model(x)
    loss = loss_fn(logits, target_y)

    loss.backward()          # 计算梯度
    optimizer.step()         # 更新参数
    optimizer.zero_grad()    # 梯度清零(下一轮用)

说明

nn.CrossEntropyLoss 的 target 有两种合法形式:类别索引(如 torch.tensor(1),最常用)或类别概率分布(如上面的 [0., 1, 0],PyTorch ≥ 1.10 支持,可用于软标签/标签平滑)。另外注意三连招的顺序与职责:backward()累加梯度到 .gradstep() 只按 .grad 更新参数,zero_grad() 负责清零——忘记清零,梯度会跨步累加,等效于学习率不受控地放大。


2. 为什么需要非线性?

2.1 XOR 问题

线性分类器无法解决 XOR(异或)问题:

x₁ x₂ y
0 0 0
0 1 1
1 0 1
1 1 0

没有一条直线能把 $(0,1)$ 和 $(1,0)$ 与另外两个点分开——两类样本呈对角分布,任何超平面都会切错至少一个点。

graph LR
    A["线性分类器的局限"] --> B["只能学习<br/>线性可分的模式"]
    B --> C["XOR 不可线性可分"]
    C --> D["需要非线性变换"]
    D --> E["多层神经网络"]

    style C fill:#ffcdd2,stroke:#c62828
    style E fill:#c8e6c9,stroke:#2e7d32

2.2 特征映射:手工造非线性

讲义给出的第一条出路是特征映射(feature map):先把输入非线性地变换到更高维空间,再在新空间里做线性分类。例如想要一个圆形决策边界 $x_1^2 + x_2^2 = 1$,定义

$$\phi(x) = \left[x_1,\; x_2,\; x_1^2 + x_2^2\right]$$

则在 $\phi$ 空间中取线性权重(如 $\text{logit} = -2\phi_1 - 2\phi_2 + \phi_3$)即可实现原空间中的圆形边界。

提示

高维空间中的线性边界,投影回原空间就是非线性边界。 线性模型没错,错的是坐标系——只要特征选得好,线性分类器什么形状的边界都能画。问题在于 $\phi$ 是人工设计的:每换一个任务就得重新想特征,而且你未必想得出来(图像分类需要什么特征?)。于是自然的下一问是:能不能把 $\phi$ 本身也学出来? 这正是神经网络的定义性思想——前面各层就是可学习的特征映射,最后一层是线性分类器。

2.3 多层感知机(MLP)

核心思想:堆叠多个线性变换 + 非线性激活函数。

$$h = \operatorname{ReLU}(W_1 x + b_1), \qquad \text{logits} = W_2 h + b_2$$

其中 $W_1 \in \mathbb{R}^{m \times d}$ 把 $d$ 维输入映到 $m$ 维隐藏层(hidden layer),$\operatorname{ReLU}$ 逐元素施加非线性,$W_2 \in \mathbb{R}^{K \times m}$ 把隐藏表示映到 K 个类的 logits。$h$ 就是学出来的特征 $\phi(x)$。

class MLP(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        super().__init__()
        self.layer1 = nn.Linear(input_dim, hidden_dim)
        self.layer2 = nn.Linear(hidden_dim, output_dim)

    def forward(self, x):
        h = self.layer1(x)           # 线性变换
        h = torch.relu(h)            # 非线性激活
        logits = self.layer2(h)      # 再次线性变换
        return logits

为什么必须有激活函数? 如果没有,多层线性变换塌缩回单层(矩阵乘法结合律):

$$W_2(W_1 x + b_1) + b_2 = (W_2 W_1)\,x + (W_2 b_1 + b_2) = W' x + b'$$

叠一百层也只是一个换了参数的线性模型,表达能力毫无增长。非线性激活是「层数 = 表达能力」这条等式成立的前提。


3. 激活函数

3.1 常见激活函数

名称 公式 特点
ReLU $\max(0, z)$ 最常用;计算快;负区间梯度为 0,可能「死亡」
Sigmoid $\sigma(z) = \frac{1}{1+e^{-z}}$ 输出 $(0,1)$;两端饱和,梯度消失严重
Tanh $\tanh(z) = \frac{e^{z}-e^{-z}}{e^{z}+e^{-z}}$ 输出 $(-1,1)$、零中心;仍会饱和
GELU $z \cdot \Phi(z)$,$\Phi$ 为标准正态 CDF Transformer 常用;ReLU 的平滑版
Leaky ReLU $\max(\alpha z, z)$,$\alpha \approx 0.01$ 负区间保留微小梯度,防死亡
x = torch.tensor([-2., -1, 0, 1, 2])

relu_out = torch.relu(x)        # [0, 0, 0, 1, 2]
sigmoid_out = torch.sigmoid(x)  # [0.12, 0.27, 0.5, 0.73, 0.88]
tanh_out = torch.tanh(x)        # [-0.96, -0.76, 0, 0.76, 0.96]
gelu_out = torch.nn.functional.gelu(x)

提示

激活函数的选择是在两种失败模式之间走钢丝:太「弯」(sigmoid/tanh 两端饱和)→ 深层网络里梯度连乘后消失;太「直」→ 塌缩回线性。ReLU 的高明在于它分段线性:正区间梯度恒为 1(连乘不衰减),负区间直接归零(制造稀疏与非线性)。GELU/Swish 则把 ReLU 在零点的「硬拐角」磨圆,让优化更平滑——现代 Transformer 基本都用这一族(各架构的具体选择见 CS336 的 Lecture 3 · 架构与超参数(未找到对应页面))。

3.2 死亡 ReLU 问题

如果某个神经元的输入始终为负 → ReLU 输出始终为 0 → 流经它的梯度始终为 0 → 它的参数永远不再更新 → 神经元「死亡」,成了网络里永久沉默的死权重。学习率过大或初始化不当都可能一步把神经元踢进负区间再也回不来。

解决方案

  • Leaky ReLU:负区间给一个小斜率 $\alpha$,让梯度不至于完全断流;
  • 更好的初始化(见第 7 节),让神经元一开始就大致均衡地落在正负两侧;
  • 换用 GELU/Swish 等处处有非零梯度的平滑激活。

4. 深度神经网络

4.1 万能逼近定理

理论:一个足够宽的单隐层网络可以逼近任何连续函数(Hornik et al. 1989)。

实践:多层网络往往比单层网络更好——参数更少、更容易优化、泛化能力更强。原因是深度提供组合式的表达:底层学局部简单特征,高层把它们组合成抽象概念,同样的表达能力用指数级更少的单元就能实现;而万能逼近定理对宽度的需求可能是天文数字,且完全不保证「学得到」。

# 单层宽网络
model = nn.Sequential(
    nn.Linear(input_dim, 10000),
    nn.ReLU(),
    nn.Linear(10000, output_dim)
)

# 多层窄网络(更好)
model = nn.Sequential(
    nn.Linear(input_dim, 128), nn.ReLU(),
    nn.Linear(128, 128), nn.ReLU(),
    nn.Linear(128, 128), nn.ReLU(),
    nn.Linear(128, output_dim)
)

注意

万能逼近定理只讲「存在这样的网络」,不讲「梯度下降能找到它」,也不讲「找到后能泛化」。表达能力、可优化性、泛化能力是三件独立的事——深度学习的工程难点几乎全在后两件上。

4.2 梯度消失/爆炸问题

深度网络训练的核心挑战。反向传播是链式法则的连乘:

$$\frac{\partial \mathcal{L}}{\partial h_1} = \frac{\partial \mathcal{L}}{\partial h_L} \prod_{i=1}^{L-1} \frac{\partial h_{i+1}}{\partial h_i}$$

$L$ 层网络就有 $L-1$ 个雅可比因子连乘。只要每个因子的「幅度」系统性地偏离 1,乘积就指数级偏离:

  • 梯度消失(vanishing gradients):因子幅度 < 1 → 梯度指数衰减 → 浅层参数几乎收不到更新信号;
  • 梯度爆炸(exploding gradients):因子幅度 > 1 → 梯度指数增长 → 更新步长失控、数值溢出(NaN)。

例子

前向传播同样受害:让标量输入 $x=1$ 连续乘以同一个权重 20 层——
$w = 0.5$:$0.5^{20} \approx 9.5 \times 10^{-7}$,信号几乎归零;
$w = 2.0$:$2^{20} \approx 10^6$,信号炸上天。
理想状态是权重(雅可比)的「有效幅度」贴近 1,信号才能既不衰减也不爆炸地穿过几十层。后面的四件套全部围绕这个目标设计。

flowchart TD
    A["深度网络"] --> B["链式法则:<br/>多次连乘"]
    B --> C{"权重大小"}
    C -->|"< 1"| D["梯度消失<br/>exp 衰减"]
    C -->|"> 1"| E["梯度爆炸<br/>exp 增长"]

    style D fill:#ffcdd2,stroke:#c62828
    style E fill:#ffcdd2,stroke:#c62828

解决方案(后文详述):

  1. 残差连接(residual connections)——给梯度修一条不衰减的旁路;
  2. 层归一化(layer normalization)——把每层激活幅度拉回可控范围;
  3. 合适的初始化——让训练从「幅度 ≈ 1」的状态出发;
  4. 更好的优化器(Adam)——按坐标自适应步长,对病态梯度更鲁棒。

5. 残差连接(Residual Connections)

5.1 核心思想

普通网络:$h_{i+1} = f(h_i)$
残差网络:$h_{i+1} = h_i + f(h_i)$

每层不再直接产出新表示,而是在旧表示上叠加一个修正量 $f(h_i)$(残差)。

class ResidualBlock(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.linear = nn.Linear(dim, dim)

    def forward(self, x):
        return x + torch.relu(self.linear(x))  # 关键:x +

为什么有效?

  • 对残差层求导:$\frac{\partial h_{i+1}}{\partial h_i} = I + \frac{\partial f}{\partial h_i}$。那个恒等矩阵 $I$ 保证梯度总有一条系数为 1 的直通路径,连乘再多层也不会因 $f$ 的雅可比太小而归零——梯度的「高速公路」。
  • 线性情形一目了然:若 $f(x) = wx$,普通网络每层乘 $w$($w=0.5$ 时 20 层后衰减到 $10^{-6}$ 量级),残差网络每层乘 $1 + w$,乘数天然待在 1 附近。
  • 即使 $f$ 什么都没学到(输出 ≈ 0),网络也至少是恒等映射——加深网络不会比浅网络更差,这正是 He et al. 2016 提出 ResNet 时要解决的「退化」问题(更深的普通网络训练误差反而更高)。

提示

残差连接把每层的任务从「从头重建表示」降格为「在现有表示上做增量修正」。学「修正量」比学「完整映射」容易得多——尤其当最优解接近恒等时,$f$ 只需输出接近 0,而普通层必须精确学出恒等矩阵。从 ResNet 到 Transformer,现代深层架构无一例外建立在残差流(residual stream)之上(见 CS336 的 Lecture 3 · 架构与超参数(未找到对应页面))。

5.2 视觉直觉

graph LR
    A["h_i"] --> B["f(·)"]
    B --> C["+ "]
    A -.跳跃连接.-> C
    C --> D["h_{i+1}"]

    style C fill:#e1f5fe,stroke:#0277bd

6. 层归一化(Layer Normalization)

6.1 问题

深层网络中激活值的幅度会逐层漂移:偏大则趋向爆炸/饱和,偏小则信号消失,而且每层输入的分布随训练不断变化,使得后层总在追逐一个移动的目标,学习率只能取得很保守。

6.2 解决方案

每个样本在特征维度上把激活归一化到均值 0、方差 1,再用可学习参数恢复表达自由度:

$$\mu = \frac{1}{d}\sum_{i=1}^{d} x_i, \qquad \sigma^2 = \frac{1}{d}\sum_{i=1}^{d}(x_i - \mu)^2, \qquad \operatorname{LN}(x) = \gamma \odot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta$$

其中 $x \in \mathbb{R}^d$ 是某一层的激活向量,$\gamma, \beta \in \mathbb{R}^d$ 是可学习的逐维缩放与平移,$\epsilon$(约 $10^{-5}$)防止除零,$\odot$ 为逐元素乘。

class LayerNorm(nn.Module):
    def __init__(self, dim, eps=1e-5):
        super().__init__()
        self.gamma = nn.Parameter(torch.ones(dim))   # 可学习的缩放
        self.beta = nn.Parameter(torch.zeros(dim))   # 可学习的平移
        self.eps = eps

    def forward(self, x):
        mean = x.mean(dim=-1, keepdim=True)
        var = x.var(dim=-1, keepdim=True, unbiased=False)
        x_norm = (x - mean) / torch.sqrt(var + self.eps)
        return self.gamma * x_norm + self.beta

为什么有效?

  • 无论前面的层怎么折腾,送进下一层的激活幅度总被拉回标准范围——数值上「重置」了逐层漂移;
  • 稳定的输入尺度允许更大的学习率、更快收敛;
  • 减轻对初始化精确性的依赖。

PyTorch 内置nn.LayerNorm(dim)

注意

「内部协变量偏移(internal covariate shift)」是 BatchNorm 论文当年给出的解释,后来被研究质疑(Santurkar et al. 2018 表明归一化的主要作用是平滑损失面,而非消除分布偏移)。引用这个术语时应知道它是历史动机而非定论。另外别把 LayerNorm 和 BatchNorm 弄混:LayerNorm 在单个样本的特征维度上归一化,与 batch 大小无关,训练/推理行为一致,因此成为变长序列与 Transformer 的标配;BatchNorm 在 batch 维度上归一化,小 batch 时统计量噪声大。


7. 初始化

7.1 为什么重要?

训练的起点决定信号最初的幅度。初始化太大 → 前向激活与反向梯度逐层放大、激活函数饱和;太小 → 信号逐层衰减。错误的初始化让网络还没开始学就已陷入梯度消失/爆炸。

7.2 Xavier/Glorot 初始化

对线性层 $y = Wx + b$(暂不考虑激活函数),设 $W_{ij} \sim \mathcal{N}(0, \sigma^2)$ 独立同分布、输入各维独立且方差为 $\operatorname{Var}(x)$,则输出每一维是 $d_{\text{in}}$ 项独立乘积之和,方差相加:

$$\operatorname{Var}(y_j) = d_{\text{in}} \cdot \sigma^2 \cdot \operatorname{Var}(x)$$

要让方差逐层保持($\operatorname{Var}(y) = \operatorname{Var}(x)$),令 $\sigma^2 = \frac{1}{d_{\text{in}}}$,即标准差随输入维度按 $1/\sqrt{d_{\text{in}}}$ 缩放:

# 保持方差不变:Var(y) ≈ Var(x)
std = 1 / np.sqrt(input_dim)
W = torch.randn(input_dim, output_dim) * std

讲义还提到实践细节:常用截断正态(限制在约 $\pm 3\sigma$ 内)采样,避免极端离群的初始权重。

7.3 Kaiming/He 初始化

ReLU 会把约一半的输入置零,粗略地把输出方差砍半。为补偿这一半的损失,方差再乘 2:

$$\sigma^2 = \frac{2}{d_{\text{in}}}$$

# 考虑 ReLU 的影响
std = np.sqrt(2 / input_dim)
W = torch.randn(input_dim, output_dim) * std

PyTorch 实现

nn.init.kaiming_normal_(layer.weight, mode='fan_in', nonlinearity='relu')

提示

初始化的设计原则只有一句话:让信号在初始状态下穿过任意多层,方差都不放大也不缩小。$1/\sqrt{d_{\text{in}}}$ 抵消「求和项数越多方差越大」的效应,Kaiming 的因子 2 抵消「ReLU 砍掉一半」的效应。它与残差连接、LayerNorm 目标一致——都是把网络的信号幅度钉在 1 附近,只是分别作用于起点(初始化)、路径(残差)和逐层(归一化)。


8. 优化器

8.1 从 GD 到 SGD

梯度下降(GD):每次更新用全部训练样本计算梯度——方向精确,但一次更新要过一遍全量数据,大数据集上代价不可接受。

随机梯度下降(SGD):每次更新只用一个随机 mini-batch $\mathcal{B}$:

$$\theta \leftarrow \theta - \eta \cdot \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \nabla_\theta \ell_i(\theta)$$

其中 $\theta$ 是全部参数,$\eta$ 是学习率,$\ell_i$ 是第 $i$ 个样本的损失。关键性质:batch 梯度是全量梯度的无偏估计——$\mathbb{E}[\nabla_{\mathcal{B}}] = \nabla \mathcal{L}$,因为随机抽样下每个样本入选的机会均等。单步方向带噪声,但平均而言指向正确方向;同样的计算量下能走多得多的步数,总体收敛远快于 GD。噪声还有附带的好处:帮助跳出尖锐的局部极小/鞍点。

# 将训练数据打乱后切分成 batches
batch_size = 32
random_perm = torch.randperm(len(training_data))
batches = [random_perm[i:i+batch_size] 
           for i in range(0, len(training_data), batch_size)]

for batch_indices in batches:
    batch_data = [training_data[i] for i in batch_indices]
    # 只用这个 batch 计算梯度并更新

8.2 Adam 优化器

SGD 的局限:所有参数共用一个学习率,也不利用历史梯度信息——梯度天生偏大的坐标和偏小的坐标被迫用同一步长。

AdamKingma & Ba, 2015)为每个参数维护两个指数滑动平均:一阶矩 $m_t$(动量,平滑梯度方向)与二阶矩 $v_t$(梯度平方,度量各坐标的典型幅度):

$$m_t = \beta_1 m_{t-1} + (1-\beta_1)\,g_t, \qquad v_t = \beta_2 v_{t-1} + (1-\beta_2)\,g_t^2$$

$$\theta \leftarrow \theta - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}$$

其中 $g_t$ 是当前梯度,$\hat{m}_t, \hat{v}_t$ 是偏差修正后的矩估计(消除滑动平均从 0 起步的低估),默认 $\beta_1 = 0.9$、$\beta_2 = 0.999$。除以 $\sqrt{\hat{v}_t}$ 的效果是按坐标归一化步长:历史梯度大的参数走小步,梯度小的参数走大步。

optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

实践中几乎总是用 Adam(或其变体 AdamW——把权重衰减从梯度中解耦出来,是当前训练大模型的默认选择)。它比 SGD 收敛更快、对学习率的选择更宽容。

说明

代价是显存:Adam 要为每个参数额外存 $m$ 和 $v$ 两份状态,优化器状态是参数量的 2 倍。对本课的小模型无所谓,对大语言模型则是实打实的显存开销(CS336 的 Lecture 2 · PyTorch 与资源核算(未找到对应页面) 会精确核算这笔账)。


9. 完整深度学习流程

flowchart TD
    A["定义模型架构"] --> B["初始化参数<br/>Kaiming/Xavier"]
    B --> C["选择优化器<br/>Adam"]
    C --> D["训练循环"]
    D --> E["前向传播"]
    E --> F["计算损失"]
    F --> G["反向传播<br/>.backward()"]
    G --> H["更新参数<br/>optimizer.step()"]
    H --> I["梯度清零<br/>optimizer.zero_grad()"]
    I --> J{"收敛?"}
    J -->|否| E
    J -->|是| K["评估/部署"]

    style B fill:#e1f5fe,stroke:#0277bd
    style C fill:#e1f5fe,stroke:#0277bd
    style G fill:#ffe0b2,stroke:#e65100

10. 总结

mindmap
  root((深度学习))
    PyTorch
      自动微分
      nn.Module 模块
      优化器
    非线性
      特征映射
      激活函数 ReLU 与 GELU
      解决 XOR 问题
    深度网络
      多层窄优于单层宽
      梯度消失与爆炸
    四大武器
      残差连接
      层归一化
      Kaiming 初始化
      Adam 优化器

要点

  • PyTorch = NumPy + 自动微分 + 预定义模块;张量即计算图节点,backward() 即反向传播。
  • 线性模型解决不了 XOR;特征映射能造非线性但需手工设计——神经网络的本质是把特征映射也学出来,而激活函数是多层不塌缩回线性的前提。
  • 深度网络的瓶颈不在表达能力(万能逼近定理早已保证),而在训练稳定性:链式法则连乘使梯度指数级消失/爆炸。
  • 四大技巧(缺一不可),目标一致——把信号幅度钉在 1 附近:
    1. 残差连接 $h + f(h)$:雅可比含恒等项,梯度高速公路;
    2. 层归一化:逐层把激活拉回均值 0、方差 1;
    3. Kaiming/Xavier 初始化:从方差守恒的起点出发;
    4. Adam 优化器:按坐标自适应步长。
  • 实践中用 mini-batch SGD(batch 梯度是全量梯度的无偏估计),优化器默认选 Adam/AdamW。

复习自测

题目

$W_2(W_1 x + b_1) + b_2 = (W_2 W_1)x + (W_2 b_1 + b_2)$。矩阵乘法封闭:两个线性映射的复合仍是线性映射,故任意层数的纯线性堆叠都能合并成单个 $W'x + b'$,假设类没有变大。激活函数打破这种合并,才让「深度」产生额外表达能力。

题目

残差层 $h_{i+1} = h_i + f(h_i)$ 的雅可比是 $I + \frac{\partial f}{\partial h_i}$。反向传播连乘时,每个因子都含恒等项 $I$,梯度始终保有一条系数为 1 的直通路径,不会因 $\frac{\partial f}{\partial h}$ 幅度小而指数衰减。此外当最优变换接近恒等时,$f$ 只需学到接近 0 的残差,比普通层学恒等映射容易得多。

题目

普通网络:$0.5^{20} \approx 9.5 \times 10^{-7}$,信号几乎消失。残差结构每层乘 $1 + w = 1.5$,$1.5^{20} \approx 3300$——虽然这个例子里变成了增长,但说明残差把乘数从 $w$ 移到了 $1+w$,只要 $f$ 学到的 $w$ 接近 0,乘数就贴近 1,信号稳定穿透。这也是为什么「幅度贴近 1」是所有稳定化技巧的共同目标。

题目

Xavier 的 $\sigma^2 = 1/d_{\text{in}}$ 让纯线性层的输出方差等于输入方差。但 ReLU 会把约一半(负半轴)的激活置零,输出方差近似减半;Kaiming 用 $\sigma^2 = 2/d_{\text{in}}$ 把这一半补回来,使「线性层 + ReLU」整体保持方差不变。

题目

无偏:均匀随机抽样下 $\mathbb{E}[\nabla_{\mathcal{B}}] = \nabla\mathcal{L}$,batch 梯度平均而言就是全量梯度。不用全量 GD 是因为一次精确更新的代价是过一遍全部数据,同样计算量下 SGD 能做多几个数量级的带噪更新,总体收敛快得多;噪声还有助于逃离尖锐极小与鞍点。


参考资料