第三章 神经网络基础
一、多层网络
1.1 神经元与激活函数
神经元模型。人工神经元(Artificial Neuron)是深度网络的最小计算单元,灵感来自生物神经元的「加权求和 + 激活」。数学形式为:
y=σ (∑i=1nwixi+b)=σ(w⊤x+b) y = \sigma\!\left(\sum_{i=1}^{n} w_i x_i + b\right) = \sigma(\mathbf{w}^\top \mathbf{x} + b) y=σ(i=1∑nwixi+b)=σ(w⊤x+b)
其中 x=(x1,...,xn)\mathbf{x} = (x_1, \dots, x_n)x=(x1,...,xn) 是输入,w\mathbf{w}w 是权重向量,bbb 是偏置(bias),σ\sigmaσ 是激活函数(非线性)。没有激活函数,多层网络等价于单层线性变换------这是激活函数存在的根本原因。
历史上第一个神经元模型是 McCulloch-Pitts(1943),后由 Rosenblatt(1958)发展为感知机(Perceptron),但感知机只能解决线性可分问题,无法表示异或(XOR)。这一限制在 1969 年由 Minsky 明确指出,直接导致了 1970-80 年代神经网络的第一次低谷。直到反向传播算法 (Rumelhart 等,1986)被重新发现,加上非线性的激活函数被使用,多层网络才重新获得生命力。
激活函数的选择。激活函数决定了网络的非线性表达能力与梯度传播特性。常用选项:
| 激活函数 | 公式 | 范围 | 特点 |
|---|---|---|---|
| Sigmoid | σ(x)=11+e−x\sigma(x) = \frac{1}{1+e^{-x}}σ(x)=1+e−x1 | (0, 1) | 平滑可微;输出非零中心,使权重更新呈锯齿状(zig-zag);梯度消失严重 |
| Tanh | tanh(x)\tanh(x)tanh(x) | (-1, 1) | 零中心,比 Sigmoid 略好;仍有梯度消失 |
| ReLU | max(0,x)\max(0, x)max(0,x) | [0, ∞) | 计算简单,缓解梯度消失;死亡 ReLU 问题 |
| Leaky ReLU | max(αx,x)\max(\alpha x, x)max(αx,x)(α\alphaα 小,如 0.01) | ℝ | 解决死亡 ReLU |
| PReLU | α\alphaα 改为可学习参数 | ℝ | Leaky ReLU 的可学习版本 |
| GELU | x⋅Φ(x)x \cdot \Phi(x)x⋅Φ(x) | ℝ | BERT(2018)起成为主流,平滑近似 ReLU |
| Swish/SiLU | x⋅σ(x)x \cdot \sigma(x)x⋅σ(x) | ℝ | Google 提出,部分任务优于 ReLU |
ReLU 的关键意义 。在 ReLU 出现之前(2010 年前后),Sigmoid/Tanh 是主流,但深层网络难以训练------因为它们的导数在饱和区极小,反向传播时梯度逐层相乘后迅速消失,导致前几层几乎学不到东西。ReLU 在 x>0x > 0x>0 时导数恒为 1,梯度可以无损地传到输入端,这是 2012 年 AlexNet 成功的关键之一。
死亡 ReLU(Dying ReLU) 。当某神经元输入恒为负时,其输出恒为 0,梯度也为 0,此后该神经元永远不会被更新(因为没有梯度流过)。高学习率会显著放大这个问题。Leaky ReLU、PReLU、GELU 都是改进方案;实践中 ReLU + 合理学习率 + 适当初始化通常已经够用。
GELU 与 SiLU 在 Transformer 中的主流地位。原版 Transformer(Vaswani et al., 2017)使用 ReLU;BERT(2018)率先采用 GELU,后续 GPT-2、ViT、RoBERTa 等 Encoder 类/早期 Decoder Transformer 沿用了 GELU。2022 年起的部分大语言模型(例如 PaLM、Llama、Qwen)改用 SwiGLU/GeGLU 等门控激活,在同等参数下取得更好效果。激活函数选择上有个核心观察:GELU 在概率意义上等价于「对输入按正态分布概率 mask」,这与 Dropout 的「随机 mask」思想一致,但又是确定性的(无随机性),在训练与推理间行为更稳定------这是 GELU 在 Transformer 系列模型中流行的核心理由。
多分类输出层 。最后输出层通常不加激活(让 logits 直接流出),损失函数选用 CrossEntropyLoss,它内部会把 logits 与 Softmax 复合起来,数值上更稳定。若任务需要概率(例如采样、置信度),在外部手动 .softmax(dim=-1)。
1.2 多层感知机
多层感知机(Multilayer Perceptron, MLP)是「若干全连接层 + 激活函数」堆叠而成的网络,是最简单的深度模型。
结构。典型 MLP:
text
输入层 (in_dim)
↓
隐藏层 1 (in_dim → hidden_dim) + 激活
↓
隐藏层 2 (hidden_dim → hidden_dim) + 激活
↓
⋮
输出层 (hidden_dim → out_dim) + (可选)输出激活
为什么需要多层 。通用逼近定理(Universal Approximation Theorem,Cybenko 1989, Hornik 1991)指出:只要激活函数满足连续有界(如 Sigmoid),目标函数定义在紧集(compact set)上,单隐藏层前馈网络就能以任意精度逼近之 。但「宽度足够」在实际中可能极大,且单隐藏层网络难以学到层级化的特征。因此实践中深度比宽度更重要------深层网络以指数效率逼近某些函数(参见论文 "The Power of Depth for Feedforward Neural Networks", Eldan & Shamir 2016 等)。
PyTorch 实现:
python
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, in_dim, hidden_dims, out_dim, dropout=0.0):
super().__init__()
layers = []
prev = in_dim
for h in hidden_dims:
layers += [nn.Linear(prev, h), nn.ReLU(inplace=True)]
if dropout > 0:
layers.append(nn.Dropout(dropout))
prev = h
layers.append(nn.Linear(prev, out_dim))
self.net = nn.Sequential(*layers)
def forward(self, x):
return self.net(x)
model = MLP(in_dim=784, hidden_dims=[256, 128], out_dim=10)
表达能力与限制。MLP 在结构化数据(表格特征)上仍是基线选择,但对图像/序列/图等高维数据效率低下:
- 图像:参数量爆炸(全连接不能利用平移不变性),需 CNN。
- 序列:不能处理变长输入与长距离依赖,需 RNN/Transformer。
- 图/集合:对输入排列敏感,需 GNN/Deep Sets。
宽度选择经验。隐藏层宽度通常取 2 的幂(32、64、128、256、512、1024),便于 GPU 内存对齐;过窄(欠拟合)与过宽(过拟合 + 计算慢)都不好,需根据验证集效果调整。
二、反向传播
2.1 反向传播原理
反向传播(Backpropagation)是利用链式法则高效计算神经网络中所有参数梯度 的算法。它由两个过程组成:前向传播 (从输入到输出计算损失)和反向传播(从损失回到输入计算梯度)。
单样本示例 。考虑一个 2 层网络。令 z1=W1x+b1\mathbf{z}_1 = \mathbf{W}_1 \mathbf{x} + \mathbf{b}_1z1=W1x+b1(隐藏层 pre-activation):
h=σ(z1) \mathbf{h} = \sigma(\mathbf{z}_1) h=σ(z1)
y=W2h+b2 \mathbf{y} = \mathbf{W}_2 \mathbf{h} + \mathbf{b}_2 y=W2h+b2
L=12(y−t)2 L = \frac{1}{2}(\mathbf{y} - \mathbf{t})^2 L=21(y−t)2
前向:计算 h\mathbf{h}h、y\mathbf{y}y、LLL。
反向:用链式法则从 LLL 反推:
∂L∂y=y−t \frac{\partial L}{\partial \mathbf{y}} = \mathbf{y} - \mathbf{t} ∂y∂L=y−t
∂L∂W2=∂L∂yh⊤ \frac{\partial L}{\partial \mathbf{W}_2} = \frac{\partial L}{\partial \mathbf{y}} \mathbf{h}^\top ∂W2∂L=∂y∂Lh⊤
∂L∂h=W2⊤∂L∂y \frac{\partial L}{\partial \mathbf{h}} = \mathbf{W}_2^\top \frac{\partial L}{\partial \mathbf{y}} ∂h∂L=W2⊤∂y∂L
∂L∂z1=∂L∂h⊙σ′(W1x+b1) \frac{\partial L}{\partial \mathbf{z}_1} = \frac{\partial L}{\partial \mathbf{h}} \odot \sigma'(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1) ∂z1∂L=∂h∂L⊙σ′(W1x+b1)
∂L∂W1=∂L∂z1x⊤ \frac{\partial L}{\partial \mathbf{W}_1} = \frac{\partial L}{\partial \mathbf{z}_1} \mathbf{x}^\top ∂W1∂L=∂z1∂Lx⊤
其中 ⊙\odot⊙ 表示逐元素乘。每个参数的梯度仅与该参数附近的局部量有关,这就是「反向传播」的局部性本质。
矩阵形式的链式法则。对张量运算,链式法则的 Jacobian 形式为:
∂L∂X=∂L∂Y⋅∂Y∂X \frac{\partial L}{\partial \mathbf{X}} = \frac{\partial L}{\partial \mathbf{Y}} \cdot \frac{\partial \mathbf{Y}}{\partial \mathbf{X}} ∂X∂L=∂Y∂L⋅∂X∂Y
实际计算中,不需要显式构造完整的 Jacobian(对大矩阵不现实),而是使用向量化反向算子------每个前向算子有一个对应的反向算子,接受「上游梯度」并返回「对各输入的局部梯度」。
自动求导的两种实现:
- 符号求导(Symbolic Differentiation):对表达式做代数化简,得到显式梯度公式。优点是精确;缺点是表达式复杂后计算图爆炸,不可行。
- 数值求导(Numerical Differentiation):用有限差分近似梯度。简单但有数值误差,O(N) 复杂度(N 为参数量),不能用于训练。
- 自动求导(Automatic Differentiation, Autodiff) :
- 前向模式(Forward-mode):从输入沿计算图正向传播导数。对「输入少、输出多」的场景高效,对深度学习「输入多(参数)、输出少(loss)」则反之。
- 反向模式(Reverse-mode):从输出反向传播。深度学习专用,O(正向 + 反向) 时间,与参数数量几乎无关。
PyTorch 用的是反向模式自动求导。这也是为什么反向传播成为深度学习标准范式。
计算图视角 。在 PyTorch 中,每个 torch.autograd.Function 子类对应一个基本算子的前向与反向实现。当张量运算触发该 Function 的 forward 时,PyTorch 同时记录「该算子」「输入张量」「输出张量」到动态计算图中。backward() 时,系统从根节点(标量 loss)出发,按拓扑逆序调用各 Function 的 backward,逐层传递梯度,最终把所有叶子参数的 .grad 填好。
验证梯度正确性 。实现一个新算子或自定义 loss 时,务必用 梯度检查(gradient checking) :用数值差分近似梯度,与 autograd 的解析梯度对比,误差应在 10−510^{-5}10−5 以下。PyTorch 提供 torch.autograd.gradcheck,可对函数自动做此检查。
python
from torch.autograd import gradcheck
input = (torch.randn(3, 4, dtype=torch.double, requires_grad=True),)
test = gradcheck(my_custom_op, input, eps=1e-6, atol=1e-4)
print(test) # True 表示通过
2.2 梯度消失问题
什么是梯度消失 。在深层网络中,反向传播时梯度需要沿链式法则连续相乘。若每层的局部梯度都小于 1(如 Sigmoid 导数最大 0.25、Tanh 导数最大 1 但大部分情况远小于 1),N 层之后总梯度会以指数级衰减(∼0.25N\sim 0.25^N∼0.25N 或更小),前几层的参数几乎收不到有效梯度,训练停滞。
Sigmoid/Tanh 为何特别严重 。Sigmoid 的导数 σ′(x)=σ(x)(1−σ(x))\sigma'(x) = \sigma(x)(1 - \sigma(x))σ′(x)=σ(x)(1−σ(x)),最大值在 x=0x=0x=0 处为 0.25,远离 0 时迅速衰减到接近 0。Tanh 类似。当神经元输入落在饱和区(很大或很小的绝对值),梯度几乎为 0。深层网络堆叠后,前几层极易进入饱和。
现象识别。训练日志典型表现:
- 训练 loss 在前若干 epoch 几乎不下降。
- 各层梯度量级(可用
tensor.grad.norm()监控)随层数加深呈指数递减。 - 网络表现不优于浅层模型。
解决方案。主要有以下几类,常组合使用:
- 改用 ReLU 及其变体 。ReLU 在 x>0x > 0x>0 时导数为 1,不会逐层衰减,是从根源上解决问题。ReLU 配合 He 初始化(考虑 ReLU 的方差特性)在数十层网络上训练良好。
- 合适的初始化。Xavier(Glorot)初始化保证前向/反向方差在各层一致;He 初始化针对 ReLU 推导。两者都用方差反比于 fan_in(输入维度)的策略。
- 归一化层。BatchNorm、LayerNorm 等在层间强制把激活值拉回合理分布,即使权重较大也不易进入饱和区。
- 残差连接(Residual Connection) 。ResNet 引入:y=F(x)+x\mathbf{y} = F(\mathbf{x}) + \mathbf{x}y=F(x)+x,梯度可以通过恒等映射无损传到前层。详见第五章。
- 梯度裁剪(Gradient Clipping) 。对 RNN 训练中常见的梯度爆炸(梯度范数过大),直接限制梯度 L2 范数:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)。 - LSTM/GRU 的门控。通过 sigmoid 控制信息流,缓解 RNN 中的梯度消失。
梯度爆炸(Gradient Explosion) 。与梯度消失相反,梯度在反向传播中指数增长,导致参数被一次更新到不合理值,loss 变成 NaN。常见于 RNN 与未归一化的深层网络。torch.nn.utils.clip_grad_norm_ 是标配解决方案。
监控梯度的工具。在训练循环中加入:
python
for name, param in model.named_parameters():
if param.grad is not None:
grad_norm = param.grad.norm()
print(f'{name}: grad_norm={grad_norm:.4f}')
或用 TensorBoard / wandb 可视化。发现梯度长期接近 0 或接近 NaN,就要立即调整(换激活、改初始化、加归一化等)。
三、训练要素
3.1 损失函数
损失函数(Loss Function)度量模型预测与真实标签之间的差距,是训练信号的唯一来源。不同任务对应不同损失函数,选错损失函数是常见但隐蔽的 bug。
回归任务:
- 均方误差(MSE) :MSE=1N∑(yi−y^i)2\text{MSE} = \frac{1}{N}\sum (y_i - \hat{y}_i)^2MSE=N1∑(yi−y^i)2。对异常值敏感(平方放大),适用于误差近似正态分布的场景。
- 平均绝对误差(MAE) :MAE=1N∑∣yi−y^i∣\text{MAE} = \frac{1}{N}\sum |y_i - \hat{y}_i|MAE=N1∑∣yi−y^i∣。对异常值鲁棒,梯度恒定(可能导致训练后期震荡)。
- Huber Loss:MSE 与 MAE 的混合,误差小时用 MSE,误差大时用 MAE。是回归任务中较稳健的默认选择。
PyTorch: nn.MSELoss()、nn.L1Loss()、nn.SmoothL1Loss()(即 β=1\beta=1β=1 的 Huber 损失)、nn.HuberLoss(delta=...)(阈值可调的 Huber)。
分类任务:
- 二分类交叉熵(Binary Cross-Entropy) :BCE=−1N∑ylogy\^+(1−y)log(1−y\^)\text{BCE} = -\frac{1}{N}\sum y \\log \\hat{y} + (1-y)\\log(1-\\hat{y})BCE=−N1∑ylogy\^+(1−y)log(1−y\^)。PyTorch 提供两个版本:
nn.BCEWithLogitsLoss()(推荐 ):接受 logits(未过 Sigmoid 的原始输出),内部等价于 Sigmoid + BCELoss,但用 log-sum-exp 技巧避免数值溢出,与CrossEntropyLoss的设计原则一致。这是 PyTorch 官方推荐写法。nn.BCELoss():接受概率(模型已过 Sigmoid),单独使用易出现数值不稳定。
- 多分类交叉熵(Categorical Cross-Entropy) :CE=−1N∑ilogexp(zyi)∑jexp(zj)\text{CE} = -\frac{1}{N}\sum_i \log \frac{\exp(z_{y_i})}{\sum_j \exp(z_j)}CE=−N1∑ilog∑jexp(zj)exp(zyi)。模型输出为 logits(未归一化),内部结合 Softmax 数值稳定(对 logits 减去最大值,避免溢出)。
nn.CrossEntropyLoss()直接接受 logits,不要在外面加 Softmax。 - KL 散度(KL Divergence) :度量两个概率分布的距离,常用于知识蒸馏、VAE 中的正则项。
nn.KLDivLoss()要求输入为 log-probability。 - Focal Loss :针对类别不平衡,降低易分类样本的权重,聚焦于难样本。
FocalLoss不在torch.nn中,需自己实现或用第三方库。
标签平滑(Label Smoothing) 。把硬标签 0,1,0,... 0, 1, 0, \\dots0,1,0,... 改为软标签 ϵK,1−ϵ+ϵK,ϵK,... \\frac{\\epsilon}{K}, 1-\\epsilon+\\frac{\\epsilon}{K}, \\frac{\\epsilon}{K}, \\dotsKϵ,1−ϵ+Kϵ,Kϵ,...,在交叉熵中作为正则手段,常提升泛化。nn.CrossEntropyLoss(label_smoothing=0.1) 直接支持。
为什么不用准确率作为损失。准确率是离散指标(对参数的梯度几乎处处为 0),无法用梯度下降优化。损失必须是连续可微的;准确率只能作为监控指标。
回归 vs 分类损失的细节:
- 回归时,输出层的「激活」通常是 None(直接输出连续值)或 Tanh(限制到 -1,1)。
- 分类时,输出层无激活(多分类)或 Sigmoid(二分类),loss 内部处理 Softmax。
- 不确定时,优先用
nn.CrossEntropyLoss(reduction='mean')+ logits,这是最不容易出错的组合。
多任务损失加权。当模型同时优化多个 loss(如目标检测中的分类 + 回归),各 loss 量级差异大,直接相加会导致某一项主导。常见做法:
- 手动调权重(经验性,易调坏)。
- 不确定性加权(Kendall et al. 2018):让网络学习各 loss 的相对权重。
- GradNorm:动态平衡各 loss 的梯度量级。
3.2 优化器
优化器(Optimizer)决定如何利用梯度更新参数。深度学习中,优化器的选择对收敛速度与最终性能影响巨大。
基础:SGD。随机梯度下降(Stochastic Gradient Descent):
θt+1=θt−η∇L(θt) \theta_{t+1} = \theta_t - \eta \nabla L(\theta_t) θt+1=θt−η∇L(θt)
其中 η\etaη 是学习率(learning rate)。朴素 SGD 在损失曲面病态(狭长山谷)时收敛极慢------梯度方向与最速下降方向不一致,导致 zig-zag 路径。
带动量(Momentum)的 SGD。引入动量项,模拟物理惯性:
vt+1=βvt+∇L(θt) v_{t+1} = \beta v_t + \nabla L(\theta_t) vt+1=βvt+∇L(θt)
θt+1=θt−ηvt+1 \theta_{t+1} = \theta_t - \eta v_{t+1} θt+1=θt−ηvt+1
动量 β\betaβ 通常取 0.9 或 0.99,可以让更新方向更平滑,加速收敛。Nesterov 动量(Nesterov Accelerated Gradient)是改进版,先用动量「预看」一步,再在那个位置计算梯度,通常更快。
自适应学习率家族:AdaGrad / RMSProp / Adam。
- AdaGrad :对每个参数维护一个累积梯度平方,学习率按历史梯度反比缩放。θt+1,i=θt,i−ηGt,ii+ϵgt,i\theta_{t+1,i} = \theta_{t,i} - \frac{\eta}{\sqrt{G_{t,ii} + \epsilon}} g_{t,i}θt+1,i=θt,i−Gt,ii+ϵ ηgt,i。优点:稀疏特征学习率大,密集特征学习率小。缺点:学习率单调下降,训练后期可能过小导致提前停止。
- RMSProp:用指数移动平均代替 AdaGrad 的简单累积,避免学习率过早衰减。Hinton 2012 课程提出。
- Adam (Kingma & Ba, 2015):结合动量(梯度的指数移动平均)与 RMSProp(梯度平方的指数移动平均),并做偏差修正。现代深度学习的事实默认优化器。
mt=β1mt−1+(1−β1)gt m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t mt=β1mt−1+(1−β1)gt
vt=β2vt−1+(1−β2)gt2 v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2 vt=β2vt−1+(1−β2)gt2
m^t=mt1−β1t,v^t=vt1−β2t \hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1 - \beta_2^t} m^t=1−β1tmt,v^t=1−β2tvt
θt+1=θt−ηm^tv^t+ϵ \theta_{t+1} = \theta_t - \eta \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} θt+1=θt−ηv^t +ϵm^t
默认 β1=0.9,β2=0.999,ϵ=10−8\beta_1=0.9, \beta_2=0.999, \epsilon=10^{-8}β1=0.9,β2=0.999,ϵ=10−8。
AdamW (Loshchilov & Hutter, 2017; arXiv:1711.05101, ICLR 2019 发表):把权重衰减(weight decay)从 L2 正则中解耦,直接对参数做衰减 ,而非加到梯度上。在使用学习率调度 + 权重衰减的训练中,AdamW 几乎一致优于 Adam,是现代 Transformer 训练的标准。PyTorch torch.optim.AdamW。
LBFGS 。二阶方法(准牛顿),用损失函数的二阶导数信息构造更新方向。适合小批量、凸优化问题;深度学习大规模场景内存太大,一般不用。torch.optim.LBFGS。
选择指南:
| 场景 | 推荐优化器 |
|---|---|
| 视觉(CNN 经典) | SGD + Momentum,或 AdamW |
| Transformer(LLM) | AdamW |
| 强化学习 | Adam(部分算法 PPO 用 Adam) |
| 凸问题/小数据 | LBFGS |
| GAN | Adam(GAN 训练对动量敏感,SGD 容易不稳) |
学习率 。优化器之外,学习率本身是训练中最重要的超参数。过大→ loss 震荡或发散;过小→ 收敛过慢或卡在局部极小。建议:
- 从 1e-3 或 1e-4 起步。
- 配合学习率调度(Warmup + Cosine Decay 等,见第四章)。
- 用学习率扫描(lr finder)找区间:PyTorch Lightning、fastai 等库提供
lr_find工具,大致定位「loss 下降最快」的学习率。
梯度累积与大批次 。当显存不够大 batch 时,可以用梯度累积(accumulation):多次 forward + backward 不清梯度,等效于大 batch;或者用 torch.utils.data.BatchSampler 拼接样本。混合精度训练(见第九章)进一步缓解显存压力。
实践中的常见错误:
- 训练时忘了
optimizer.zero_grad(),导致梯度累积,loss 不下降。 - 用了
CrossEntropyLoss之前又加了 Softmax(双重 Softmax,loss 永远是错的)。 - Adam 的学习率给 1e-1(对 Adam 来说太大,典型值是 1e-3 到 1e-4);SGD 的学习率给 1e-4(从零训练时偏小,典型值是 1e-1 到 1e-2;fine-tuning 阶段 1e-4 较常见)。
- 在 fine-tuning 阶段,模型已经接近最优,却用与预训练相同的学习率(应大幅降低,如 1/10 到 1/100)。