查看全课大纲(第 7 / 40 节)
  1. 1.从规则到学习:深度学习解决什么问题
  2. 2.表示学习与端到端训练
  3. 3.张量、计算图与自动微分
  4. 4.训练三要素:数据、模型、目标函数
  5. 5.从感知机到多层感知机
  6. 6.反向传播:手推一个两层网络
  7. 7.激活函数与初始化
  8. 8.优化器:SGD → Momentum → Adam
  9. 9.正则化:权重衰减、Dropout、BN/LN
  10. 10.卷积的直觉与参数共享
  11. 11.池化、感受野与下采样
  12. 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
  13. 13.残差连接与归一化为什么有效
  14. 14.迁移学习与特征可视化
  15. 15.RNN 与 BPTT
  16. 16.LSTM/GRU:门控如何缓解梯度问题
  17. 17.Seq2Seq 与编码器-解码器
  18. 18.注意力前史:Bahdanau 加性注意力
  19. 19.Self-Attention:QKV 与缩放点积
  20. 20.多头注意力:并行的多种关系
  21. 21.位置编码:正弦与可学习
  22. 22.残差、LayerNorm 与前馈层
  23. 23.从零实现一个最小 Transformer block
  24. 24.训练稳定性、复杂度与常见误区
  25. 25.自监督与语言建模目标
  26. 26.BERT:掩码语言模型与双向编码
  27. 27.GPT:自回归生成与缩放定律
  28. 28.指令微调与对齐:SFT → RLHF/DPO
  29. 29.参数高效微调:LoRA 与适配器
  30. 30.自编码器与 VAE
  31. 31.GAN:对抗训练与模式崩塌
  32. 32.扩散模型:前向加噪与反向去噪
  33. 33.扩散模型的应用与采样加速
  34. 34.对比学习与 CLIP
  35. 35.ViT:把图像当序列
  36. 36.多模态架构:对齐、融合与生成
  37. 37.能力边界、幻觉与评测
  38. 38.PyTorch 2.x 训练循环与混合精度
  39. 39.实验管理、评测与复现
  40. 40.推理部署与成本:量化/蒸馏概览

激活函数与初始化

激活函数:从 Sigmoid 到 GELU,非线性如何驱动深度学习?

小象实战讲义 · 深度学习基础:从神经网络到 Transformer

在上一节,我们学习了神经元的基本结构和前向传播过程。一个关键问题是:如果神经元只是对输入进行线性加权求和,那么无论堆叠多少层,整个网络依然是一个线性模型,无法学习复杂的非线性模式。本节将深入探讨激活函数——这个为神经网络注入非线性、并深刻影响其训练动态的核心机制。学完本节,你将能回答:为什么 Sigmoid 会导致梯度消失?ReLU 如何缓解此问题?现代 Transformer 为何偏爱 GELU?

💡 核心导读

  1. 激活函数的作用:为神经网络引入非线性,使其能够逼近任意复杂函数(万能逼近定理),并控制梯度流动。
  2. 从 Sigmoid 到 ReLU 的演进:Sigmoid 的“软饱和”特性导致深层网络梯度消失,是早期神经网络训练困难的主因。ReLU 在正区间的梯度恒为 1,有效缓解了此问题。
  3. ReLU 的变体:LeakyReLU、ELU 等旨在解决 ReLU 的“神经元死亡”问题(负输入梯度为零)。
  4. 现代选择:GELU:在 Transformer(如 BERT、GPT)中广泛使用,它根据输入的概率进行门控,是一种平滑、非单调的激活函数。
  5. 初始化与激活函数的协同:错误的初始化(如过大权重)会使激活值落入饱和区,导致梯度问题。需要根据激活函数特性选择匹配的初始化方法(如 Xavier/Glorot 初始化用于 Sigmoid/Tanh,Kaiming/He 初始化用于 ReLU)。

🧭 前置知识

  1. 神经元的前向计算:z=wTx+bz = \mathbf{w}^T \mathbf{x} + b,a=f(z)a = f(z),其中 ff 是激活函数。
  2. 梯度与链式法则:理解如何通过梯度更新参数。
  3. 张量的基本操作:元素级运算(element-wise)。

激活函数是什么?

激活函数 f(⋅)f(\cdot) 作用于神经元的加权求和输出 zz 上,产生该神经元的最终激活值 a=f(z)a = f(z)。它的核心作用有两点:

  1. 引入非线性:这是最关键的作用。没有非线性激活函数,多层网络等价于单层线性网络,无法学习复杂模式。
  2. 控制输出范围:例如,Sigmoid 将输出压缩到 (0,1),适合表示概率;ReLU 将负值归零,实现稀疏激活。

早期神经网络使用 Sigmoid 或 Tanh 函数,但它们存在严重的梯度消失问题。2011 年 Glorot 与 Bengio 关于稀疏整流器(ReLU)的研究之后,ReLU 随 2012 年 AlexNet 的胜利被广泛采用,极大推动了深度学习的发展。随后,一系列改进版本和新的激活函数(如 GELU)被提出,以适应更深的网络和更复杂的架构。

经典激活函数:特性与陷阱

Sigmoid 与梯度消失

Sigmoid 函数定义为:σ(z)=11+e−z\sigma(z) = \frac{1}{1 + e^{-z}}。它将输入映射到 (0, 1) 区间。 其导数为:σ′(z)=σ(z)(1−σ(z))\sigma'(z) = \sigma(z)(1 - \sigma(z))。

饱和性与梯度消失:当 ∣z∣|z| 很大时,σ(z)\sigma(z) 会非常接近 0 或 1,此时导数 σ′(z)\sigma'(z) 接近 0。这种现象称为“软饱和”。在反向传播时,梯度需要逐层乘以这些激活函数的导数。如果多层导数都很小,梯度会指数级衰减,导致深层网络的权重几乎无法更新,这就是梯度消失。这也是上世纪 90 年代至 21 世纪初神经网络(尤其是 RNN)训练困难的主要原因。

import torch
import torch.nn as nn

torch.manual_seed(0)

# 演示 Sigmoid 及其导数在极端值下的行为
z = torch.tensor([-10.0, -5.0, -1.0, 0.0, 1.0, 5.0, 10.0])
sigmoid = nn.Sigmoid()
a = sigmoid(z)
# 手动计算导数:a * (1 - a)
grad = a * (1 - a)

print("输入 z:", [round(v, 4) for v in z.tolist()])
print("Sigmoid(z):", [round(v, 6) for v in a.tolist()])
print("导数 σ'(z):", [round(v, 6) for v in grad.tolist()])
输入 z: [-10.0, -5.0, -1.0, 0.0, 1.0, 5.0, 10.0]
Sigmoid(z): [4.5e-05, 0.006693, 0.268941, 0.5, 0.731059, 0.993307, 0.999955]
导数 σ'(z): [4.5e-05, 0.006648, 0.196612, 0.25, 0.196612, 0.006648, 4.5e-05]

可以看到,当 ∣z∣>5|z| > 5 时,导数已经小于 0.01,梯度几乎消失。

ReLU:简单而有效

Rectified Linear Unit (ReLU) 定义为:ReLU(z)=max⁡(0,z)\text{ReLU}(z) = \max(0, z)。 其导数为:ReLU′(z)={1if z>00if z<0\text{ReLU}'(z) = \begin{cases} 1 & \text{if } z > 0 \\ 0 & \text{if } z < 0 \end{cases}(在 z=0z=0 处导数未定义,实现中常设为 0 或 1)。

优点:

  1. 缓解梯度消失:在正区间,梯度恒为 1,梯度可以无衰减地反向传播。
  2. 计算高效:只涉及比较和乘法,没有指数运算。
  3. 稀疏性:对零均值输入,约一半的神经元会被置零,使网络更稀疏,可能提升泛化能力。

缺点:

  1. 神经元死亡:一旦某个神经元在训练中所有样本的输入 zz 都小于 0,其梯度将永远为 0,权重不再更新,该神经元“死亡”。
  2. 输出非零中心化:输出均值恒大于零,可能影响后续层的优化动态。
import torch
import torch.nn as nn

torch.manual_seed(0)
# 使用 PyTorch 官方实现
relu = nn.ReLU()
z = torch.randn(4, 3) * 2  # 生成一些正负值
print("输入 z (shape: {}):".format(tuple(z.shape)))
for row in z.tolist():
    print([round(v, 4) for v in row])

output = relu(z)
print("\nReLU(z) (shape: {}):".format(tuple(output.shape)))
for row in output.tolist():
    print([round(v, 4) for v in row])
输入 z (shape: (4, 3)):
[3.082, -0.5869, -4.3576]
[1.1369, -2.169, -2.7972]
[0.8067, 1.6761, -1.4385]
[-0.8067, -1.1933, 0.3641]

ReLU(z) (shape: (4, 3)):
[3.082, 0.0, 0.0]
[1.1369, 0.0, 0.0]
[0.8067, 1.6761, 0.0]
[0.0, 0.0, 0.3641]

可以看到,所有负值都被置为 0。

ReLU 的变体:解决“死亡”问题

为了缓解 ReLU 的神经元死亡问题,研究者提出了几种变体:

  1. LeakyReLU:LeakyReLU(z)={zif z>0αzif z≤0\text{LeakyReLU}(z) = \begin{cases} z & \text{if } z > 0 \\ \alpha z & \text{if } z \le 0 \end{cases},其中 α\alpha 是一个小的正常数(如 0.01)。负区间有一个小的斜率,确保梯度不为零。
  2. Parametric ReLU (PReLU):与 LeakyReLU 形式相同,但斜率 α\alpha 作为一个可学习的参数,由数据决定。
  3. Exponential Linear Unit (ELU):ELU(z)={zif z>0α(ez−1)if z≤0\text{ELU}(z) = \begin{cases} z & \text{if } z > 0 \\ \alpha (e^z - 1) & \text{if } z \le 0 \end{cases}。在负区间平滑地趋近于 −α-\alpha,输出均值更接近零。
import torch
import torch.nn as nn

torch.manual_seed(0)

z = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
print("输入 z =", z.tolist())

# 使用 PyTorch 官方实现
leaky = nn.LeakyReLU(negative_slope=0.01)  # α=0.01
elu = nn.ELU(alpha=1.0)  # α=1.0

print("\nLeakyReLU(z) =", [round(v, 4) for v in leaky(z).tolist()])
print("ELU(z)       =", [round(v, 4) for v in elu(z).tolist()])
输入 z = [-2.0, -1.0, 0.0, 1.0, 2.0]

LeakyReLU(z) = [-0.02, -0.01, 0.0, 1.0, 2.0]
ELU(z)       = [-0.8647, -0.6321, 0.0, 1.0, 2.0]

现代激活函数:GELU

Gaussian Error Linear Unit (GELU) 在 Transformer 模型(如 BERT、GPT)中被广泛采用。它的设计思想基于随机正则化(如 Dropout),根据输入的概率来决定“门控”程度。

定义:GELU(z)=zΦ(z)\text{GELU}(z) = z \Phi(z),其中 Φ(z)\Phi(z) 是标准高斯分布的累积分布函数 (CDF)。

直观理解:GELU 用 Φ(z)\Phi(z) 作为门控权重,它可以理解为「一个标准正态随机变量 ZZ 不超过输入 zz 的概率」,即 P(Z≤z)P(Z \le z)——输入越大,这个概率越接近 1,门开得越大。当 zz 很大时,Φ(z)≈1\Phi(z) \approx 1,GELU(z) ≈ z;当 zz 很小时,Φ(z)≈0\Phi(z) \approx 0,GELU(z) ≈ 0。与 ReLU 的硬门控(0 或 1)不同,GELU 实现了一种平滑的软门控。

由于高斯 CDF 计算较慢,实践中常用一个双曲正切函数来近似: GELU(z)≈0.5z(1+tanh⁡[2π(z+0.044715z3)])\text{GELU}(z) \approx 0.5 z \left(1 + \tanh\left[\sqrt{\frac{2}{\pi}} \left(z + 0.044715 z^3\right) \right] \right)

PyTorch 的 nn.GELU 默认 approximate='none',即用 erf 精确计算;显式传 approximate='tanh' 才会走上面这个近似式(速度略快、数值略有差异)。

import torch
import torch.nn as nn

torch.manual_seed(0)

# 使用 PyTorch 官方实现,采用近似计算
gelu = nn.GELU(approximate='tanh')
z = torch.linspace(-3, 3, 7)
print("输入 z =", [round(v, 4) for v in z.tolist()])

output = gelu(z)
print("\nGELU(z) (近似) =", [round(v, 4) for v in output.tolist()])

# 与 ReLU 对比
relu_out = nn.functional.relu(z)
print("ReLU(z)        =", [round(v, 4) for v in relu_out.tolist()])
输入 z = [-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]

GELU(z) (近似) = [-0.0036, -0.0454, -0.1588, 0.0, 0.8412, 1.9546, 2.9964]
ReLU(z)        = [0.0, 0.0, 0.0, 0.0, 1.0, 2.0, 3.0]

可以看到,GELU 对负输入也有非零输出:它在 z≈−0.75z \approx -0.75 处取到最小值约 -0.17,再往负走则平滑趋近 0——输出有下界,不像 LeakyReLU 那样随 zz 线性下降。整条曲线处处可导,这种平滑性可能让优化过程更稳定。

初始化:与激活函数协同工作

激活函数的行为与权重初始化紧密相关。如果权重初始化得过大,线性层的输出 zz 的绝对值可能很大,容易使 Sigmoid/Tanh 进入饱和区,或使 ReLU 的输出过大。如果初始化得过小,信号可能在多层传播后衰减殆尽。

Xavier/Glorot 初始化(2010年提出)针对 Sigmoid、Tanh 等饱和激活函数设计。它假设前向传播中激活值的方差和反向传播中梯度的方差在每一层都应尽量保持稳定。对于线性层 y=Wx+b\mathbf{y} = \mathbf{W}\mathbf{x} + \mathbf{b},权重 W\mathbf{W} 从均匀分布 U[−6nin+nout,6nin+nout]U\left[-\frac{\sqrt{6}}{\sqrt{n_{in} + n_{out}}}, \frac{\sqrt{6}}{\sqrt{n_{in} + n_{out}}}\right] 或正态分布 N(0,2nin+nout)N\left(0, \frac{2}{n_{in} + n_{out}}\right) 中采样,其中 ninn_{in} 和 noutn_{out} 是输入和输出的维度。

Kaiming/He 初始化(2015年提出)专门为 ReLU 及其变体设计。它修正了 ReLU 将一半神经元置零的影响。对于 ReLU,权重从正态分布 N(0,2nin)N\left(0, \frac{2}{n_{in}}\right) 或均匀分布 U[−6nin,6nin]U\left[-\frac{\sqrt{6}}{\sqrt{n_{in}}}, \frac{\sqrt{6}}{\sqrt{n_{in}}}\right] 中采样。

这里有个常见误解要澄清:PyTorch 的默认初始化并不是「已为 ReLU 调好」的 Kaiming。nn.Linear 与 nn.Conv2d 的 reset_parameters 调用的是 kaiming_uniform_(weight, a=√5),其中 a=√5 让增益降为约 1/√3,权重标准差约为 1/√(3·n_in)——比 ReLU 所需的 √(2/n_in) 小得多。它实际是按「权重自身」保持尺度(接近 LeCun 初始化),而非按「ReLU 之后的激活方差」设计。所以换激活函数、或网络加深时,显式指定初始化方法更稳妥。

📝 动手练一练

问题:假设有一个输入维度 10、输出维度 5 的线性层。请分别用 Xavier 正态初始化、Kaiming 正态初始化(ReLU 模式)和 PyTorch 默认初始化来创建它,打印三种情况下权重的实测标准差,并与各自的公式对照——默认初始化离 ReLU 需要的尺度差多远?

👉 点击查看参考答案

思路:三种方法的目标标准差不同:Xavier 为 √(2/(n_in+n_out))、Kaiming(ReLU)为 √(2/n_in)、PyTorch 默认为 1/√(3·n_in)。用同一个 torch.manual_seed 建三层、各自打印实测 std 与理论值对照,就能看出默认初始化明显偏小。注意实测值来自 5×10=50 个样本,与理论值会有百分之几的抽样偏差,属正常。

import math
import torch
import torch.nn as nn

torch.manual_seed(0)
in_dim, out_dim = 10, 5

# 1. Xavier/Glorot 正态初始化:std = sqrt(2 / (n_in + n_out))
xavier = nn.Linear(in_dim, out_dim)
nn.init.xavier_normal_(xavier.weight)
print("Xavier  理论 std =", round(math.sqrt(2.0 / (in_dim + out_dim)), 4),
      "| 实测 std =", round(xavier.weight.std().item(), 4))

# 2. Kaiming/He 正态初始化(配合 ReLU):std = sqrt(2 / n_in)
kaiming = nn.Linear(in_dim, out_dim)
nn.init.kaiming_normal_(kaiming.weight, nonlinearity='relu')
print("Kaiming 理论 std =", round(math.sqrt(2.0 / in_dim), 4),
      "| 实测 std =", round(kaiming.weight.std().item(), 4))

# 3. PyTorch 默认初始化:kaiming_uniform_(a=sqrt(5)),不是 ReLU 专用版本
default = nn.Linear(in_dim, out_dim)
print("默认    实测 std =", round(default.weight.std().item(), 4), "(kaiming_uniform a=sqrt(5))")
Xavier  理论 std = 0.3651 | 实测 std = 0.333
Kaiming 理论 std = 0.4472 | 实测 std = 0.4071
默认    实测 std = 0.1766 (kaiming_uniform a=sqrt(5))

可以看到:Xavier 与 Kaiming 的实测值都贴着各自公式(抽样偏差在百分之几),而 PyTorch 默认值 0.1766 只有 ReLU 所需 0.4472 的四成左右。所以「默认初始化够用了」在浅层小网络上往往看不出问题,一旦换成 ReLU 堆深,信号就会逐层衰减。

常见错误

错误理解或写法现象或报错原因正确做法
在深层网络中使用 Sigmoid 作为隐藏层激活函数。网络训练极慢,损失几乎不下降,尤其是深层网络。Sigmoid 的软饱和性导致梯度消失,深层梯度无法有效回传。隐藏层使用 ReLU、GELU 等非饱和激活函数。输出层若需概率可使用 Sigmoid。
以为 PyTorch 的默认初始化已经适配 ReLU,换了激活函数也不管。浅层看不出来;网络一加深,激活值标准差逐层衰减,深层几乎学不动。nn.Linear / nn.Conv2d 的默认初始化是 kaiming_uniform_(a=√5)(标准差约 1/√(3·n_in)),并非针对 ReLU 激活后方差的 √(2/n_in)。配合 ReLU 时显式调用 nn.init.kaiming_normal_(weight, nonlinearity='relu');配 Sigmoid/Tanh 用 xavier_normal_。
误以为 GELU 就是 z * sigmoid(z)。实现的结果与标准 GELU 有偏差。GELU 的定义是 z * Φ(z),其中 Φ 是高斯 CDF,不是 Sigmoid。Sigmoid 是 Logistic 分布的 CDF。使用库函数 nn.GELU(),或确保使用正确的近似公式。

本章小结

  1. 激活函数的核心价值是引入非线性,使神经网络能够逼近复杂函数。
  2. Sigmoid/Tanh 的软饱和性是导致梯度消失、阻碍早期深度学习发展的关键原因。
  3. ReLU 通过正区间梯度为 1 的特性,有效缓解了梯度消失,但其“神经元死亡”问题催生了 LeakyReLU、ELU 等变体。
  4. GELU 基于概率的门控思想,是一种平滑的非单调激活函数,在 Transformer 模型中成为主流选择。
  5. 权重初始化必须与激活函数匹配:Xavier 用于 Sigmoid/Tanh,Kaiming 用于 ReLU 族,这是保证训练稳定性的重要一环。

📋 行动清单

  • 在代码中,将一个小型全连接网络的隐藏层激活函数从 Sigmoid 改为 ReLU,观察训练速度的变化。
  • 使用 torch.nn.init 模块,尝试对一个线性层分别进行 Xavier 初始化和 Kaiming 初始化,并打印初始化后权重值的统计量(均值、标准差)。
  • 查阅 PyTorch 官方文档中 nn.GELU 的说明,了解其 approximate 参数的不同选项。

延伸阅读

本段 PyTorch 官方文档/教程条目的框架出处:PyTorch: An Imperative Style, High-Performance Deep Learning Library(Paszke et al., 2019,arXiv:1912.01703);页面内容以 PyTorch 2.14.1 为准。

  1. Gaussian Error Linear Units (GELU)(Hendrycks & Gimpel, 2016, arXiv:1606.08415)- https://arxiv.org/abs/1606.08415
  2. Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification(He et al., 2015, arXiv:1502.01852)—— Kaiming/He 初始化原始论文 - https://arxiv.org/abs/1502.01852
  3. Understanding the difficulty of training deep feedforward neural networks(Glorot & Bengio, 2010, AISTATS 13)—— Xavier/Glorot 初始化原始论文 - https://proceedings.mlr.press/v9/glorot10a.html
  4. PyTorch 官方文档 torch.nn.GELU(2.14.1,approximate 参数说明)- https://pytorch.org/docs/stable/generated/torch.nn.GELU.html
  5. PyTorch 官方文档 torch.nn.init(2.14.1,kaiming_uniform_ 的 a 参数含义)- https://pytorch.org/docs/stable/nn.init.html

—— 小象教研组