【深度学习|DAY04】神经网络深度学习笔记(下):损失函数、优化器与正则化

文章目录

  • 神经网络深度学习笔记(下):损失函数、优化器与正则化
  • [4. 损失函数](#4. 损失函数)
    • [4.1 是什么与为什么](#4.1 是什么与为什么)
    • [4.2 分类任务损失函数](#4.2 分类任务损失函数)
      • [4.2.1 多分类交叉熵损失](#4.2.1 多分类交叉熵损失)
      • [4.2.2 二分类交叉熵损失](#4.2.2 二分类交叉熵损失)
    • [4.3 回归任务损失函数](#4.3 回归任务损失函数)
      • [4.3.1 MAE 损失(L1 Loss,Mean Absolute Error)](#4.3.1 MAE 损失(L1 Loss,Mean Absolute Error))
      • [4.3.2 MSE 损失(L2 Loss,Mean Squared Error)](#4.3.2 MSE 损失(L2 Loss,Mean Squared Error))
      • [4.3.3 Smooth L1 Loss](#4.3.3 Smooth L1 Loss)
    • [4.4 Pytorch API 实现](#4.4 Pytorch API 实现)
      • [4.4.1 分类损失](#4.4.1 分类损失)
      • [4.4.2 回归损失](#4.4.2 回归损失)
      • [4.4.3 在示例网络中的代码](#4.4.3 在示例网络中的代码)
  • [5. 梯度下降与优化器](#5. 梯度下降与优化器)
    • [5.1 梯度下降的基本原理](#5.1 梯度下降的基本原理)
    • [5.2 梯度下降面临的三大困境](#5.2 梯度下降面临的三大困境)
    • [5.3 前置知识:指数加权平均](#5.3 前置知识:指数加权平均)
    • [5.4 各优化器详解](#5.4 各优化器详解)
      • [5.4.1 Momentum(动量法)](#5.4.1 Momentum(动量法))
      • [5.4.2 AdaGrad(Adaptive Gradient,自适应梯度)](#5.4.2 AdaGrad(Adaptive Gradient,自适应梯度))
      • [5.4.3 RMSProp(Root Mean Square Propagation)](#5.4.3 RMSProp(Root Mean Square Propagation))
      • [5.4.4 Adam(Adaptive Moment Estimation,自适应矩估计)](#5.4.4 Adam(Adaptive Moment Estimation,自适应矩估计))
      • [5.4.5 五种优化器对比](#5.4.5 五种优化器对比)
    • [5.5 学习率衰减策略](#5.5 学习率衰减策略)
      • [5.5.1 为什么需要](#5.5.1 为什么需要)
      • [5.5.2 三种基础衰减策略](#5.5.2 三种基础衰减策略)
    • [5.6 Pytorch API实现](#5.6 Pytorch API实现)
      • [5.6.1 优化器 API](#5.6.1 优化器 API)
      • [5.6.2 学习率衰减 API](#5.6.2 学习率衰减 API)
      • [5.6.3 调用时序](#5.6.3 调用时序)
  • [6. 正则化](#6. 正则化)
    • [6.1 L1 / L2 正则化](#6.1 L1 / L2 正则化)
      • [6.1.1 L2 正则化:weight_decay](#6.1.1 L2 正则化:weight_decay)
      • [6.1.2 L1 正则化](#6.1.2 L1 正则化)
      • [6.1.3 L1 vs L2 对比](#6.1.3 L1 vs L2 对比)
    • [6.2 Dropout(随机失活)](#6.2 Dropout(随机失活))
    • [6.3 Batch Normalization(批量归一化)](#6.3 Batch Normalization(批量归一化))
      • [6.3.1 是什么与为什么](#6.3.1 是什么与为什么)
      • [6.3.2 Batch Normalization 的使用步骤](#6.3.2 Batch Normalization 的使用步骤)
    • [6.4 Pytorch API实现](#6.4 Pytorch API实现)
      • [6.4.1 nn.Dropout API](#6.4.1 nn.Dropout API)
      • [6.4.2 nn.BatchNorm1d API](#6.4.2 nn.BatchNorm1d API)
      • [6.4.3 在示例网络中的完整代码位置](#6.4.3 在示例网络中的完整代码位置)
    • [6.5 正则化选择策略](#6.5 正则化选择策略)
  • [7. 完整训练代码](#7. 完整训练代码)

神经网络深度学习笔记(下):损失函数、优化器与正则化

上篇,继续使用同一个示例网络(3 特征输入 → 5-5 隐藏层 → 2 分类输出),深入损失函数、优化算法和正则化技术,最后给出完整可运行的训练代码。


4. 损失函数

4.1 是什么与为什么

损失函数(Loss Function)是衡量模型预测值与真实标签之间差距的函数。输入是模型的预测输出和真实标签,输出是一个标量,数字越大说明预测越差,越小说明越准。

损失函数在神经网络中承担了双重角色,是连接前向传播与反向传播的枢纽:

  1. 评估模型质量(前向传播的终点):训练过程中,通过观察 loss 的下降趋势来判断模型是否在学习;训练结束后,用 loss 量化模型的最终性能。没有损失函数,就无法判断预测的好坏。
  2. 为反向传播提供起点:梯度是从损失函数出发,沿计算图反向传播到每个参数的。没有损失函数,反向传播根本没有起点。

从这个角度看,损失函数是前向传播和反向传播的分界线 :往前是正向计算(数据→预测→loss),往后是反向求导(loss→梯度→参数更新)。它的选择直接影响反向传播第一层梯度的形态,进而影响整个训练过程。


4.2 分类任务损失函数

4.2.1 多分类交叉熵损失

公式:

L = − ∑ i = 1 C y i ⋅ log ⁡ ( y ^ i ) L = -\sum_{i=1}^{C} y_i \cdot \log(\hat{y}_i) L=−i=1∑Cyi⋅log(y^i)

其中 :

  • C C C 是类别总数。
  • y i y_i yi 是真实标签的 one-hot 编码:正确类别的位置为 1,其余全为 0。
  • y ^ i \hat{y}_i y^i 是模型预测的类别概率(经过 Softmax 后的值, y ^ i ∈ ( 0 , 1 ) \hat{y}_i \in (0,1) y^i∈(0,1), ∑ y ^ i = 1 \sum \hat{y}_i = 1 ∑y^i=1)。

因为 y i y_i yi 是 one-hot(只有正确类别对应位置才是 1),公式实际上退化成了一个非常简洁的形式:

L = − log ⁡ ( y ^ c o r r e c t ) L = -\log(\hat{y}_{correct}) L=−log(y^correct)

也就是说,交叉熵只关心模型分配了多少概率给正确的那个类别 (只最小化正确类别所对应的预测概率的负对数)。模型越确信正确答案( y ^ c o r r e c t \hat{y}{correct} y^correct 趋近于 1), − log ⁡ -\log −log 趋近于 0,损失越小;模型越不确信( y ^ c o r r e c t \hat{y}{correct} y^correct 趋近于 0), − log ⁡ -\log −log 趋近于正无穷,损失暴增。这种"只盯着正确答案"的特性,驱使我们训练时不断推高正确类别的预测概率。

与逻辑回归的关系: 逻辑回归的损失函数在数学本质上就是二分类交叉熵。将逻辑回归从二分类推广到多分类,损失函数也自然地从二元推广为多元,两者都是最大化正确类别的对数似然。所以如果你之前学过逻辑回归的对数似然损失 -[y·log(ŷ) + (1-y)·log(1-ŷ)],神经网络的多分类交叉熵就是它的 C 类版本。

在PyTorch中,nn.CrossEntropyLoss 同时包含了 Softmax + 负对数似然损失 。因此模型输出层不再需要单独加Softmax ,直接输出线性变换后的logits即可。反向传播时,梯度会正确地穿过Softmax和线性层。

4.2.2 二分类交叉熵损失

公式:

L = − y ⋅ log ⁡ ( y \^ ) + ( 1 − y ) ⋅ log ⁡ ( 1 − y \^ ) L = -y \\cdot \\log(\\hat{y}) + (1-y) \\cdot \\log(1-\\hat{y}) L=−y⋅log(y\^)+(1−y)⋅log(1−y\^)

其中:

y y y 是真实标签,取值只能是 0 或 1 ,1 表示正类,0 表示负类。

y ^ \hat{y} y^ 是模型预测为正类的概率(经过 Sigmoid 后的值)。

该公式恰好就是逻辑回归中最大化似然函数的负对数 形式。

这个公式实际上是交叉熵在二分类下的展开形式,可以分两种情况理解:

  • 当真实标签为正类( y = 1 y=1 y=1)时 : L = − log ⁡ ( y ^ ) \mathcal{L} = -\log(\hat{y}) L=−log(y^)。模型越确信该样本是正类( y ^ → 1 \hat{y} \to 1 y^→1), − log ⁡ ( y ^ ) → 0 -\log(\hat{y}) \to 0 −log(y^)→0,损失越小;模型越不确信( y ^ → 0 \hat{y} \to 0 y^→0), − log ⁡ ( y ^ ) → + ∞ -\log(\hat{y}) \to +\infty −log(y^)→+∞,损失暴增。

  • 当真实标签为负类( y = 0 y=0 y=0)时 : L = − log ⁡ ( 1 − y ^ ) \mathcal{L} = -\log(1-\hat{y}) L=−log(1−y^)。模型越确信该样本是负类( y ^ → 0 \hat{y} \to 0 y^→0,即 1 − y ^ → 1 1-\hat{y} \to 1 1−y^→1),损失趋近于 0;模型越不确信( y ^ → 1 \hat{y} \to 1 y^→1),损失暴增。

"只盯着正确答案"的驱动逻辑同样成立:对于每个训练样本,模型只被要求对真实类别做出高置信度的预测。


4.3 回归任务损失函数

回归任务预测连续数值,输出层不使用激活函数(恒等映射),衡量的是预测值和真实值之间的数值距离。

这部分L1,L2正则化的影响和特征,我们详细谈论过数学原理,详情可见

4.3.1 MAE 损失(L1 Loss,Mean Absolute Error)

公式:

L = 1 n ∑ i = 1 n ∣ y i − y ^ i ∣ L = \frac{1}{n}\sum_{i=1}^{n} |y_i - \hat{y}_i| L=n1i=1∑n∣yi−y^i∣

核心特征:梯度恒为 ±1。 不管误差是 0.01 还是 100,梯度的绝对值始终是 1。这个看似简单的特性带来了两个重要效果:

  1. 稀疏性能产生特征选择效果 :因为梯度始终是 ±1,即使误差已经很小,参数的更新力度也不衰减。这让优化器有能力将某些不重要参数精确地推向 0,产生稀疏解。当 L1 用作正则化项(加到损失中,形如 L t o t a l = L o r i g i n + λ L 1 L_{total} = L_{origin} + \lambda L_1 Ltotal=Lorigin+λL1),不重要的参数就被直接拉到 0、被"淘汰"了。

  2. 对离群点(outlier)鲁棒:离群点的误差再大,梯度也只是 ±1,不会像 MSE 那样被平方成百上千倍地放大。所以模型不会被个别极端值"牵着鼻子走"。

简单说,L1 的特点就是"一视同仁",小误差和大误差的惩罚梯度一样大,所以它在乎的是整体的中位数拟合质量,而非被个别离群点支配。代价是:(1) 在误差为 0 处不可导(实践中用次梯度解决);(2) 训练后期可能会在最优解附近震荡(因为梯度一直保持 ±1)。

4.3.2 MSE 损失(L2 Loss,Mean Squared Error)

公式:

L = 1 n ∑ i = 1 n ( y i − y ^ i ) 2 L = \frac{1}{n}\sum_{i=1}^{n} (y_i - \hat{y}_i)^2 L=n1i=1∑n(yi−y^i)2

核心特征:梯度正比于误差。 ∂ L ∂ y ^ = 2 ( y ^ − y ) \frac{\partial L}{\partial \hat{y}} = 2(\hat{y} - y) ∂y^∂L=2(y^−y),误差越大,梯度越大;误差越小,梯度越小。

这个特性同样带来双重效果:

  1. 对离群点极其敏感:对于离群点(outliers)敏感,因为平方项会放大大误差。离群点的巨大误差经过平方后被进一步放大,梯度也暴增。模型会被迫"扭曲"对其他正常样本的拟合来迁就这几个离群点。数据干净时这是优点(精确拟合每一条数据);数据有噪声时这是灾难(学到了噪声,过拟合)。

  2. 梯度爆炸风险:数学上,假设一个离群点的误差是 100,MSE 梯度就是 200。乘以学习率 η 后参数更新量为 200η。而正常误差为 1 的样本梯度仅为 2,更新量 2η。当一个大 batch 里混入离群点时,平均梯度被严重拉高,参数朝错误方向大幅跳跃。相比之下,MAE 对误差 100 和误差 1 的梯度都是 ±1,离群点不会产生不成比例的影响。

  3. 处处可导、光滑收敛:梯度随误差减小而平滑递减,接近最优解时更新幅度自然变小,收敛过程比 MAE 平稳。

当 MSE 用作正则项时(即 weight decay),它对较大的权重施加更大的约束力(梯度正比于权重值),推动所有权重整体缩小但很少精确归零,这恰是防止过拟合所需的行为:让每个特征都有贡献,但不让任何一个权重过大而主导预测。

MAE vs MSE 对比:

维度 MAE (L1) MSE (L2)
梯度数值 恒定 ±1 2 ( y ^ − y ) 2(\hat{y} - y) 2(y^−y),正比于误差
对离群点 鲁棒(梯度不放大误差) 敏感(梯度被平方放大)
0 点处 不可导 处处可导
稀疏性 有(恒定梯度易推参数到 0) 无(小误差时梯度也小,难归零)
收敛行为 后期可能震荡 平稳收敛
适用场景 数据含离群点、需要特征选择 数据干净,大多数标准回归问题

4.3.3 Smooth L1 Loss

Smooth L1 是一种结合了均方误差(MSE)和平均绝对误差(MAE)优点的损失函数。它在误差较小时表现得像 MSE(梯度平滑渐变,收敛稳定),在误差较大时表现得像 MAE(梯度恒定不爆炸,对离群点鲁棒)。

公式:

Smooth L1 ( x ) = { 0.5 x 2 , ∣ x ∣ < 1 ∣ x ∣ − 0.5 , ∣ x ∣ ≥ 1 \text{Smooth L1}(x) = \begin{cases} 0.5x^2, & |x| < 1 \\ |x| - 0.5, & |x| \geq 1 \end{cases} Smooth L1(x)={0.5x2,∣x∣−0.5,∣x∣<1∣x∣≥1

其中 x = y − y ^ x = y - \hat{y} x=y−y^ 为预测误差。

分段解读:

  • ∣ x ∣ < 1 |x| < 1 ∣x∣<1(误差小,接近收敛): 用 0.5 x 2 0.5x^2 0.5x2。梯度 = x x x,随误差减小而平滑减小,避免了 MAE 在零附近梯度突变导致的震荡。这是 MSE 的优点,让训练末期稳定收敛。
  • ∣ x ∣ ≥ 1 |x| \geq 1 ∣x∣≥1(误差大,远离目标): 用 ∣ x ∣ − 0.5 |x| - 0.5 ∣x∣−0.5。梯度 = ± 1 \pm 1 ±1(恒定),避免了 MSE 大误差时梯度暴增的问题。这是 MAE 的优点,不被离群点绑架。
  • 0.5 这个常数: 保证分段函数在 x = ± 1 x = \pm 1 x=±1 处连续且导数连续。

典型应用场景: 目标检测中的边界框回归。一张图中前景物体(正样本)和背景(负样本)数量极不平衡,且标注框可能存在噪声,用 Smooth L1 既不被标注噪声牵着走,又能在训练后期平稳收敛到精确位置。

选择建议

  • 一般回归首选MSE。
  • 数据含较多异常值(如传感器噪声)时选MAE或Smooth L1。
  • 目标检测中边框回归常使用Smooth L1。

4.4 Pytorch API 实现

所属模块

损失函数同样位于 torch.nn 模块(Neural Network)。在 PyTorch 中,损失函数以 的形态存在,它们继承自 nn.Module,可以在 __init__ 中定义。但不同于 nn.Linearnn.ReLU(在 forward 中使用),损失函数在训练循环中单独调用 loss = criterion(pred, target)

4.4.1 分类损失

  1. nn.CrossEntropyLoss 多分类交叉熵
python 复制代码
torch.nn.CrossEntropyLoss(weight=None, ignore_index=-100, reduction='mean', label_smoothing=0.0)

这个类内部做了三件事:Softmax → Log → NLLLoss。也就是说它自动完成了概率归一化、取对数和负对数似然计算的全过程。这意味着:

模型的输出层绝对不能再手动加 Softmax! 否则 Softmax 被做了两次,结果是错误的。

将 Softmax 和 Log 合并为一个操作(log-sum-exp 技巧),计算上更稳定,避免单独算 Softmax 时 e z i e^{z_i} ezi 指数溢出,也避免单独算 Log 时的除零风险。

参数 含义 默认值 如何选择
weight 各类别的权重张量,形状 (C,),用于处理类别不平衡 None 少数类给大权重,如 torch.tensor([0.5, 2.0, 1.0])
ignore_index 忽略指定类别的损失计算 -100 语义分割中通常忽略背景类(255)
reduction 'mean' 平均 / 'sum' 求和 / 'none' 逐样本返回 'mean' 正常训练用 'mean',调试需要看每个样本的 loss 时用 'none'
label_smoothing 标签平滑系数 0.0 需要额外正则化时设为 0.1(将真实标签从 0,1,0 平滑为 0.05, 0.9, 0.05,防止模型过度自信)

输入格式:

输入 形状 说明
pred(预测) (batch_size, C) 未经过 Softmax 的 logits
target(标签) (batch_size,) 类别索引 (不是 one-hot!),dtype 为 torch.long

为什么 target 用类别索引而不是 one-hot 矩阵: PyTorch 内部直接从 logits 提取正确类别位置的值来计算 − log ⁡ ( softmax ( z c o r r e c t ) ) -\log(\text{softmax}(z_{correct})) −log(softmax(zcorrect))。不需要你手动把标签 0, 2, 1 转成 [[1,0,0],[0,0,1],[0,1,0]],这既省内存,又避免了大矩阵构建,一个索引整数就定位到了该在 logits 矩阵的哪一列取值。

python 复制代码
criterion = nn.CrossEntropyLoss()
pred = model(x)                # (batch_size, 2), logits
target = torch.tensor([0, 1])  # (batch_size,), 类别索引
loss = criterion(pred, target) # 标量
  1. nn.BCEWithLogitsLoss 二分类交叉熵
python 复制代码
torch.nn.BCEWithLogitsLoss(weight=None, reduction='mean', pos_weight=None)

内部 = Sigmoid + BCE。 和 CrossEntropyLoss 一样的设计思路,将激活函数和损失计算合并,用 log-sum-exp 技巧保证数值稳定性。输入是未经过 Sigmoid 的 logits。

参数 含义 默认值 如何选择
weight 逐样本的权重张量 None 一般不用
pos_weight 正类权重,用于处理正负样本极度不平衡 None 正样本远少于负样本时,设为 负样本数 / 正样本数

输入格式: pred 形状 (batch_size, 1)(batch_size,)(logits),target 同形状(0 或 1),dtype 为 torch.float

  1. nn.BCELoss

不含 Sigmoid。使用时需要自己手动在模型输出层加 Sigmoid ,而且单独算 Sigmoid 在极端值处容易溢出。建议用 BCEWithLogitsLoss 替代。

4.4.2 回归损失

  1. nn.L1Loss :MAE
python 复制代码
torch.nn.L1Loss(reduction='mean')

参数只有 reduction。用法简单:loss = nn.L1Loss()(pred, target)

  1. nn.MSELoss :MSE
python 复制代码
torch.nn.MSELoss(reduction='mean')

同样只有 reduction 参数。MSE = (pred - target)² 的均值。

  1. nn.SmoothL1Loss :平滑 L1
python 复制代码
torch.nn.SmoothL1Loss(reduction='mean', beta=1.0)
参数 含义 默认值 如何选择
beta MSE 和 MAE 的分界阈值 1.0 默认 1.0 足够;增大 beta 让更多误差区域用 MSE(对离群点更敏感)

x < beta 时用 0.5 × x² / beta,当 x ≥ beta 时用 x - 0.5 × beta

任务 推荐损失函数 模型输出层配置 target 格式
多分类 nn.CrossEntropyLoss 不加 Softmax(Loss 内部已含) 类别索引,torch.long
二分类 nn.BCEWithLogitsLoss 不加 Sigmoid(Loss 内部已含) 0 或 1,torch.float
回归(干净数据) nn.MSELoss 恒等映射(不加激活) 实数
回归(含离群点) nn.L1Loss 恒等映射 实数
回归(鲁棒 + 平滑) nn.SmoothL1Loss 恒等映射 实数

4.4.3 在示例网络中的代码

损失函数在模型外部实例化(它没有可学习参数),在训练循环中每次迭代被调用。它是前向传播的终点、反向传播的起点

python 复制代码
criterion = nn.CrossEntropyLoss()  # 实例化

for batch_x, batch_y in dataloader:
    logits = model(batch_x)           # 前向传播 → logits(无Softmax)
    loss = criterion(logits, batch_y) # ① 前向终点:算出损失值
    loss.backward()                   # ② 反向起点:从损失出发链式求导

5. 梯度下降与优化器

5.1 梯度下降的基本原理

梯度下降的核心思想极其简单:梯度的方向是函数增长最快的方向,那往梯度的反方向走,函数值就下降最快。

对于梯度的理解,以及梯度下降的具体过程,我们详细谈论过数学原理,详情可见

对于损失函数 L ( w ) L(w) L(w),更新公式为:

w n e w = w o l d − η ⋅ ∂ L ∂ w w_{new} = w_{old} - \eta \cdot \frac{\partial L}{\partial w} wnew=wold−η⋅∂w∂L

η \eta η(学习率)和梯度的分工非常明确:梯度决定了"往哪个方向走、这个方向上有多陡",学习率决定了"这一步迈多大"。 即梯度定方向,学习率定步幅。


5.2 梯度下降面临的三大困境

在实际的深度网络训练中,朴素的梯度下降(只用当前位置的梯度,不附加任何优化)会遇到三个典型障碍:

困境 现象 根本原因
平缓区域(Plateau) 损失曲面大面积平坦,梯度值极小 → 参数更新极慢 → 训练看似停滞 高维空间参数耦合导致
鞍点(Saddle Point) 某些方向是峰、某些方向是谷,但梯度恰好为 0 → 参数完全卡住 高维优化的固有现象(维度越高鞍点越多)
局部最小值(Local Minimum) 梯度为 0 → 卡在非全局最优的解上 损失函数非凸

针对这些困境,发展出了两大类优化思路:

  • 优化梯度本身:Momentum - 不只看当前梯度,还积累历史动量,即使当前位置梯度为 0 也能靠惯性冲过去
  • 自适应学习率:AdaGrad / RMSProp - 不再给所有参数用同一个全局学习率,而是为每个参数分量独立调整学习率

5.3 前置知识:指数加权平均

指数加权平均是理解 Momentum 和 RMSProp 的关键基础。

普通算术平均:所有时刻的值权重相等。指数加权平均:离当前时刻越近的值权重越大,越远的值权重越小,权重呈指数衰减。

S t = β S t − 1 + ( 1 − β ) Y t S_t = \beta S_{t-1} + (1-\beta) Y_t St=βSt−1+(1−β)Yt

  • S t S_t St:t 时刻的指数加权平均值
  • Y t Y_t Yt:t 时刻的真实观测值
  • β \beta β:衰减系数,控制"记忆长度",默认 0.9

β \beta β 取值的直观影响:

β \beta β 效果 原因
0.99(大) 曲线非常平滑、波动很小 当前值只占 1%,历史值主导;约 100 步有效记忆
0.9(默认) 适中 当前值占 10%;约 10 步有效记忆
0.5(小) 曲线陡峭、对新数据响应极快 当前和历史各占一半

β \beta β 本质上控制的是你"多在乎过去"。

  • β \beta β 接近1(如0.9),则曲线平滑、对当前突变不敏感,反映了长期趋势;
  • β \beta β 较小,则紧跟最新值,曲线陡峭。

在优化器中,我们既要利用过去的梯度信息来平滑方向(不要太大),又要对最近的梯度变化保持敏感(不要太小),0.9 是一个通用的平衡值。


5.4 各优化器详解

5.4.1 Momentum(动量法)

是什么: Momentum 在参数更新时,不直接用当前梯度 g t g_t gt,而是用历史梯度的指数加权平均 s t s_t st 来替代,相当于给参数加上了物理中的"惯性"。

公式:

s t = β s t − 1 + ( 1 − β ) g t (梯度的指数加权平均) s_t = \beta s_{t-1} + (1-\beta) g_t \qquad \text{(梯度的指数加权平均)} st=βst−1+(1−β)gt(梯度的指数加权平均) w t = w t − 1 − η ⋅ s t (用平滑梯度替代原始梯度更新) w_t = w_{t-1} - \eta \cdot s_t \qquad \text{(用平滑梯度替代原始梯度更新)} wt=wt−1−η⋅st(用平滑梯度替代原始梯度更新)

为什么需要:

  • 当梯度下降到鞍点时,当前梯度 g t = 0 g_t = 0 gt=0,普通梯度下降完全停滞。但 Momentum 的 s t s_t st 中保留了之前累积的梯度历史,参数带着"惯性"冲过鞍点。
  • 由于 mini-batch 普通的梯度下降算法,每次选取少数的样本梯度确定前进方向,可能会出现震荡,使得训练时间变长。Momentum 使用移动加权平均,平滑了梯度的变化,使得前进方向更加平缓,有利于加快训练过程。一定程度上有利于降低 "峡谷" 问题的影响。

β \beta β 通常取 0.9 或 0.99。 0.9 意味着最近约 10 个 iteration 的梯度对当前的 s t s_t st 有显著影响,够平滑但不过度迟钝。

5.4.2 AdaGrad(Adaptive Gradient,自适应梯度)

是什么: AdaGrad 为每个参数分量分配完全独立的学习率 。核心理念:AdaGrad 的学习率总体会逐渐减小,这是因为在起初时,我们距离最优目标仍较远,可以使用较大的学习率,加快训练速度,随着迭代次数的增加,学习率逐渐下降。

计算步骤:

  1. 初始化学习率 η \eta η、参数 w w w、小常数 σ = 10 − 10 \sigma = 10^{-10} σ=10−10(防除零)
  2. 初始化梯度累积变量 s = 0 s = 0 s=0
  3. 从训练集采样一个 batch,计算梯度 g t g_t gt
  4. 累积平方梯度 : s t = s t − 1 + g t ⊙ g t s_t = s_{t-1} + g_t \odot g_t st=st−1+gt⊙gt( ⊙ \odot ⊙ 表示逐元素乘积,即各分量各自累加自己的平方)
  5. 权重更新: w t = w t − 1 − η s t + σ ⋅ g t w_t = w_{t-1} - \frac{\eta}{\sqrt{s_t} + \sigma} \cdot g_t wt=wt−1−st +ση⋅gt

核心机制解读: 每个参数都有一个独立的 s s s 值,在分母 s t \sqrt{s_t} st 的位置。历史梯度大的参数 → s t s_t st 大 → η s t \frac{\eta}{\sqrt{s_t}} st η 小 → 学习率被调低。历史梯度小的参数 → s t s_t st 小 → 学习率保持较大。

致命缺点: s t s_t st 是加法累积历史梯度平方,只增不减(单调递增)。这导致学习率只会一直降、不会升,训练后期学习率可能变得极小而几乎无法更新参数。这也是为什么我们现在几乎不会直接使用 AdaGrad,而用它的改进版 RMSProp。

5.4.3 RMSProp(Root Mean Square Propagation)

是什么: RMSProp 只对 AdaGrad 做了一处改动,把第 4 步的"平方梯度累加"换成"平方梯度的指数加权平均"。

其计算过程如下:

  1. 初始化学习率 η、初始化权重参数w、小常数 σ = 1e-10
  2. 初始化梯度累计变量 s = 0
  3. 从训练集中采样 m 个样本的小批量,计算梯度 g t g_t gt
  4. 使用指数加权平均累计历史梯度,⊙ 表示各个分量相乘,公式如下:
    s t s_t st = β s t − 1 s_{t-1} st−1 + (1-β) g t g_t gt⊙ g t g_t gt
  5. 学习率 η 的计算公式如下:
    η = η s t + σ η\over\sqrt{s_t}+σ st +ση
  6. 权重参数更新公式如下:
    w t w_t wt = w t − 1 w_{t-1} wt−1 - η s t + σ η\over\sqrt{s_t}+σ st +ση * g t g_t gt
  7. 重复 3-7 步骤

唯一改动的地方(第 4 步):

s t = β s t − 1 + ( 1 − β ) g t ⊙ g t s_t = \beta s_{t-1} + (1-\beta) g_t \odot g_t st=βst−1+(1−β)gt⊙gt

原来是 s t = s t − 1 + g t 2 s_t = s_{t-1} + g_t^2 st=st−1+gt2(加法累加),现在是 s t = β s t − 1 + ( 1 − β ) g t 2 s_t = \beta s_{t-1} + (1-\beta) g_t^2 st=βst−1+(1−β)gt2(指数加权平均)。

这一处改动解决了什么: s t s_t st 不再单调递增,过于久远的梯度平方值被 β \beta β 反复衰减后影响趋近于 0,而近期的梯度平方值权重更大。学习率不再只降不升,如果某个参数最近的梯度变小了, s t s_t st 也随之减小,学习率回升。

AdaGrad 和 RMSProp 的核心思想完全一致(梯度大→给小的学习率,梯度小→给大的学习率),区别只在 s t s_t st 的计算方式。实际训练中 RMSProp 几乎总是优于 AdaGrad。

5.4.4 Adam(Adaptive Moment Estimation,自适应矩估计)

是什么: Adam 把 Momentum (梯度指数加权平均,决定方向)和 RMSProp (梯度平方指数加权平均,为每个参数独立调整学习率)整合在一起,再加一步偏差校正

完整公式:

m t = β 1 m t − 1 + ( 1 − β 1 ) g t 一阶矩:梯度的指数加权平均(类似Momentum) m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t \qquad \text{一阶矩:梯度的指数加权平均(类似Momentum)} mt=β1mt−1+(1−β1)gt一阶矩:梯度的指数加权平均(类似Momentum)

s t = β 2 s t − 1 + ( 1 − β 2 ) g t 2 二阶矩:梯度平方的指数加权平均(类似RMSProp) s_t = \beta_2 s_{t-1} + (1-\beta_2) g_t^2 \qquad \text{二阶矩:梯度平方的指数加权平均(类似RMSProp)} st=β2st−1+(1−β2)gt2二阶矩:梯度平方的指数加权平均(类似RMSProp)

m ^ t = m t 1 − β 1 t , s ^ t = s t 1 − β 2 t 偏差校正 \hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \quad \hat{s}_t = \frac{s_t}{1 - \beta_2^t} \qquad \text{偏差校正} m^t=1−β1tmt,s^t=1−β2tst偏差校正

w t = w t − 1 − η ⋅ m ^ t s ^ t + ϵ 参数更新 w_t = w_{t-1} - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{s}_t} + \epsilon} \qquad \text{参数更新} wt=wt−1−η⋅s^t +ϵm^t参数更新

  • β 1 \beta_1 β1:一阶矩衰减系数,默认 0.9
  • β 2 \beta_2 β2:二阶矩衰减系数,默认 0.999
  • ϵ \epsilon ϵ:防止除零的小常数,默认 10 − 8 10^{-8} 10−8

为什么需要偏差校正: 训练刚开始时(t=1), m 0 = 0 m_0 = 0 m0=0, s 0 = 0 s_0 = 0 s0=0。根据公式展开:

m 1 = β 1 ⋅ 0 + ( 1 − β 1 ) ⋅ g 1 = ( 1 − β 1 ) g 1 m_1 = \beta_1 \cdot 0 + (1-\beta_1) \cdot g_1 = (1-\beta_1) g_1 m1=β1⋅0+(1−β1)⋅g1=(1−β1)g1

这意味着 m 1 m_1 m1 只有真实梯度 g 1 g_1 g1 的 10%(当 β 1 = 0.9 \beta_1=0.9 β1=0.9 时),严重偏向 0,因为历史项 m 0 = 0 m_0=0 m0=0 拖了后腿。偏差校正用 m ^ t = m t 1 − β 1 t \hat{m}_t = \frac{m_t}{1 - \beta_1^t} m^t=1−β1tmt 把它拉回到正确的尺度: t = 1 t=1 t=1 时除以 1 − 0.9 = 0.1 1-0.9=0.1 1−0.9=0.1, m 1 m_1 m1 从 0.1 g 1 0.1g_1 0.1g1 恢复到 g 1 g_1 g1,正好修正。随着 t t t 增大, 1 − β 1 t 1-\beta_1^t 1−β1t 趋近于 1,校正效果自然消失,此时 m t m_t mt 已经累积了足够多的历史梯度,不再需要校正。

为什么 betas=(0.9, 0.999) 几乎不需修改: 这是 Adam 论文在大规模实验中验证并确定的最优默认值。 β 1 = 0.9 \beta_1=0.9 β1=0.9 让一阶矩(梯度方向)记忆最近约 10 步的变化; β 2 = 0.999 \beta_2=0.999 β2=0.999 让二阶矩(学习率缩放)记忆最近约 1000 步,二阶矩需要更长的记忆周期是因为学习率的自适应调整应该基于更长期的数据统计。

Adam 在实践中通常是收敛最快、调参最省心、适用范围最广的选择。

5.4.5 五种优化器对比

维度 SGD Momentum AdaGrad RMSProp Adam
更新方向 (分子) 当前梯度 ( g_t ) 速度 ( v_t ) (历史梯度的指数平均) 当前梯度 ( g_t ) 当前梯度 ( g_t ) 一阶矩 ( m_t ) (历史梯度的指数平均)
学习率调节 (分母) 无,全局统一 无,全局统一 ( \sqrt{G_t} ) (历史梯度平方的累加和) ( \sqrt{Eg\^2_t} ) (历史梯度平方的指数平均) ( \sqrt{\hat{v}_t} ) (历史梯度平方的指数平均,经偏差校正)
学习率趋势 全程固定 全程固定 单调递减,易过早衰减至零 动态调整,避免衰减过快 动态调整,震荡更平稳
跨鞍部/逃离局部最优能力 差,易在鞍点停滞 ,靠惯性冲过 中,后期步长极小难以逃离 中,能维持一定步长 ,兼具惯性与自适应步长
偏差校正 (修正初期 ( m, v ) 偏向0的问题)
适用场景 简单任务、需要精细调参时 CV任务(如ResNet训练)的通用基准 稀疏特征(文本、推荐系统),现已较少单独使用 RNN、非平稳目标(NLP强化学习) 通用默认首选,适用绝大多数非凸优化问题
  • Momentum:调方向,不调步长

    它引入动量(速度),用历史梯度的指数平均来修正更新方向,但所有参数仍共享同一个全局学习率。

  • AdaGrad 和 RMSProp:不调方向,调步长

    它们直接用当前梯度作为方向,但利用梯度的平方和(二阶矩),为每个参数独立地调节学习率,实现自适应步长。

  • Adam:即调方向,又调步长,还加偏差校正

    它融合了 Momentum 和 RMSProp 的思路,既用一阶矩估计修正方向,又用二阶矩估计调节步长,并通过偏差校正让训练初期更稳定。


5.5 学习率衰减策略

5.5.1 为什么需要

疑问:上述学习过的学习率自适应已经在不断根据梯度调整步长,为什么还需要学习率衰减策略?那训练后期梯度变小,( v_t ) 变小,有效步长不就自然变大了吗?

思考如果没有 η \eta η 的衰减,自适应机制本身无法保证后期有效步长会减小,甚至可能反而增大(当梯度持续很小时,分母的累积也会变小,步长可能反弹回升)。这会导致"在最优解附近反复震荡"。

也就是说自适应只在参数之间做相对缩放 ,并不强制整体步长随时间单调递减 。 学习率衰减正是提供了这种单调递减的时间先验

学习率衰减做的是:在优化器的自适应调整之上,叠加一层全局性的、按训练进度规划的学习率下调策略。

进一步理解:

① 学习率调度(Learning Rate Scheduler,学习率衰减)

核心:(s(t)):衰减因子,只和迭代步数 (t) 有关,不看梯度、不看参数、不分参数,关注的就是全局时间 t

② 自适应优化器(Adam、RMSprop、AdaGrad)

以Adam参数更新形式简化写: t h e t a t + 1 = θ t − η 0 v t + ϵ ⋅ m t theta_{t+1} = \theta_t - \frac{\eta_0}{\sqrt{v_t}+\epsilon}\cdot m_t thetat+1=θt−vt +ϵη0⋅mt,改写为"等效学习率"视角: e t a t , i = η 0 v t , i + ϵ eta_{t,i} = \frac{\eta_0}{\sqrt{v_{t,i}}+\epsilon} etat,i=vt,i +ϵη0 下标 (i) = 第 i 个参数

关键区别: b o l d s y m b o l η t , i boldsymbol{\eta_{t,i}} boldsymbolηt,i 是逐参数独立的 ,依赖该参数历史梯度累积 v t , i v_{t,i} vt,i。关注的就是该参数 i 直到时间 t 的整个梯度历史,它对每个参数输出一个不同的值。
总结

  • 全局衰减:自变量是 t (时间),函数值对所有参数是常数。效果是统一的步长调节。
  • 自适应法:自变量是 历史梯度 (参数相关),函数值对每个参数是唯一的。效果是对参数空间进行逐元素的、自适应的、各向异性的缩放变形。

场景

每一次 iteration(一个batch完成前向+反向,执行参数更新θ更新)每一轮参数更新时,两层缩放因子会相乘共同作用 : η eff , i , t = η 0 ⋅ ( s ( t ) ) ⋅ 1 v t , i + ϵ \eta_{\text{eff},i,t} = \eta_0 \cdot \boldsymbol{(s(t))} \cdot \frac{1}{\sqrt{v_{t,i}}+\epsilon} ηeff,i,t=η0⋅(s(t))⋅vt,i +ϵ1

  • ( s ( t ) ) (s(t)) (s(t)):学习率调度/衰减因子(全局标量,同一轮所有参数共用同一个值)
  • 1 v t , i + ϵ \dfrac{1}{\sqrt{v_{t,i}}+\epsilon} vt,i +ϵ1:自适应优化器内部缩放(第i个参数独有,不同参数各不相同)

也就是:每次迭代更新权重这一刻,两个变换同时生效,一起缩放基础学习率 η 0 \eta_0 η0。

5.5.2 三种基础衰减策略

策略 PyTorch 类 行为 适用场景
等间隔衰减 StepLR 每隔 step_size 个 epoch,lr × gamma 训练周期长、需阶段性降温
指定间隔衰减 MultiStepLR 在预设的里程碑 epoch 处衰减 知道大概哪些 epoch 该降温
指数衰减 ExponentialLR 每个 epoch lr × gamma 希望学习率平滑持续下降
自适应衰减 ReduceLROnPlateau 监控指标不改善时自动触发 loss 不再下降时自动降温

5.6 Pytorch API实现

5.6.1 优化器 API

所属模块

torch.optim。助记:optim = Optimizer,优化器

这个模块在 loss.backward() 完成梯度计算(存入 .grad)之后,负责执行参数更新。autograd 系统管梯度计算,optim 模块管参数更新,两者职责完全分离。

所有优化器共通的调用模式

python 复制代码
optimizer = optim.SomeOptimizer(model.parameters(), lr=..., **specific_params)

第一个参数 params 传入模型的可学习参数 ,通常用 model.parameters() 拿到所有层的 W 和 b。高级用法可以传入字典列表实现不同层不同学习率:

python 复制代码
optimizer = optim.SGD([
    {'params': model.fc1.parameters(), 'lr': 0.01},
    {'params': model.fc2.parameters(), 'lr': 0.001},
])

核心 API 一览

优化器类 核心思想 关键特有参数
SGD 纯梯度下降,可选 +Momentum momentumnesterov
Adagrad 每参数独立学习率(平方梯度累加) lr_decay
RMSprop AdaGrad 的改进(指数平均替代累加) alpha(即 β)
Adam Momentum + RMSProp + 偏差校正 betaseps
  1. optim.SGD :随机梯度下降
python 复制代码
torch.optim.SGD(params, lr=0.01, momentum=0, dampening=0, weight_decay=0, nesterov=False)
参数 含义 默认值 如何选择
lr 学习率 0.01 SGD 对 lr 很敏感,通常 0.01 或 0.1 起步
momentum 动量系数 β 0 0 = 纯 SGD;0.9 是实际标配
weight_decay L2 正则化系数 λ 0 1e-4 ~ 5e-4(见第 6 章)
nesterov 是否使用 Nesterov 加速梯度 False 设为 True 时"先看一步再算梯度",有时能加速收敛
dampening 动量阻尼 0 一般不设置
  1. optim.Adagrad
python 复制代码
torch.optim.Adagrad(params, lr=0.01, lr_decay=0, weight_decay=0, eps=1e-10)
  1. optim.RMSprop
python 复制代码
torch.optim.RMSprop(params, lr=0.01, alpha=0.99, eps=1e-08, weight_decay=0, momentum=0)

注意:PyTorch 中 alpha 对应的就是公式中的 β \beta β(平滑系数),命名差异有历史原因。

  1. optim.Adam :自适应矩估计
python 复制代码
torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0, amsgrad=False)
参数 含义 默认值 如何选择
lr 基学习率 0.001 多数任务 0.001 够用;有时降到 1e-4 更稳
betas (β₁, β₂)------一阶和二阶矩的衰减系数 (0.9, 0.999) 几乎从不需修改
eps 数值稳定性常数 1e-8 默认即可;数值不稳定时增大到 1e-6
weight_decay L2 正则化系数(第 6 章) 0 1e-4 ~ 1e-2
amsgrad 是否使用 AMSGrad 变体 False 默认 False;某些极端非凸问题上 True 更稳

5.6.2 学习率衰减 API

所属模块

torch.optim.lr_scheduler,助记:lr_scheduler = Learning Rate Scheduler,学习率调度器

在每个 epoch(或 step)结束后,根据预设规则调整优化器中的学习率 lr。梯度计算归 autograd,参数更新归 optim.Optimizer,学习率调度归 lr_scheduler

所有调度器共通的调用模式

python 复制代码
scheduler = torch.optim.lr_scheduler.SomeScheduler(optimizer, **specific_params)

第一个参数传入要控制的优化器实例 ,调度器会直接修改 optimizer.param_groups 中的 lr 字段。这意味着学习率衰减和自适应优化器可以叠加使用 :Adam 负责给每个参数计算各自的等效学习率,调度器负责统一缩放所有参数的基学习率 η₀

通用的 step 调用时机:

python 复制代码
for epoch in range(num_epochs):
    train(...)          # 训练一个 epoch
    scheduler.step()    # epoch 结束后调整学习率

部分策略也支持按 batch 粒度调度(如 OneCycleLR),此时 scheduler.step() 写在每个 batch 之后。

核心 API 一览

调度器 行为 关键参数
StepLR 每隔固定 epoch,lr × gamma step_size, gamma
MultiStepLR 在预设 epoch 列表处衰减 milestones, gamma
ExponentialLR 每个 epoch,lr × gamma gamma
ReduceLROnPlateau 监控指标不改善时触发 mode, factor, patience
  1. StepLR:等间隔衰减
python 复制代码
torch.optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1)
参数 含义 如何选择
step_size 衰减周期(epoch 数) 总 epoch 的 1/3 ~ 1/5,比如 100 epoch 设 30
gamma 衰减乘法因子 0.1 是常用值(每次降一个数量级);0.5 更温和
  1. MultiStepLR :指定间隔衰减
python 复制代码
torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1)
参数 含义 如何选择
milestones 触发衰减的 epoch 列表 [30, 60, 80] 这样的经验值;或根据 loss 曲线手动设定
gamma 衰减乘法因子 同 StepLR,0.1 最常见
  1. ExponentialLR : 指数衰减
python 复制代码
torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95)
参数 含义 如何选择
gamma 每个 epoch 的乘法因子 0.95~0.99,接近 1 才合理;太小会让 lr 迅速崩塌
  1. ReduceLROnPlateau:自适应衰减
python 复制代码
torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10)
参数 含义 如何选择
mode 'min' 监控指标越小越好;'max' 越大越好 loss 用 'min';accuracy 用 'max'
factor 衰减乘法因子 0.1~0.5,比 gamma 更激进
patience 容忍多少个 epoch 不改善 5~20,太小频繁触发,太大反应迟钝
threshold 改善的最小变化量 默认 1e-4,避免微小波动误触发

与前面三种的本质区别: StepLRMultiStepLRExponentialLR 的衰减只依赖于 epoch 这个时间变量,完全不看模型的表现 ,即使 loss 还在下降,时间到了也照降不误。

ReduceLROnPlateau根据验证集指标动态决定:指标不再改善时,才触发衰减。

5.6.3 调用时序

python 复制代码
# ===== 初始化阶段 =====
model = MyModel()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)          # ① 创建优化器,指定初始学习率
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)  # ② 创建调度器,传入优化器实例

# ===== 训练阶段 =====
for epoch in range(num_epochs):
    # ===== 每个 epoch 内,遍历所有 batch =====
    for batch_idx, (data, target) in enumerate(train_loader):
        # --- batch 级别:三步一循环 ---
        optimizer.zero_grad()      # ③ 清空上一 batch 的梯度(必须在 backward 之前)
        output = model(data)       # ④ 前向传播
        loss = criterion(output, target)  # ⑤ 计算损失
        loss.backward()            # ⑥ 反向传播,梯度存入 .grad
        optimizer.step()           # ⑦ 读取 .grad,执行参数更新

    # --- epoch 级别:学习率衰减 ---
    scheduler.step()               # ⑧ 每个 epoch 结束后,调度器更新 optimizer 中的 lr

    # 打印当前学习率(验证调度器是否生效)
    current_lr = optimizer.param_groups[0]['lr']
    print(f"Epoch {epoch}: lr = {current_lr}")

6. 正则化

正则化(Regularization)是一类防止神经网络过拟合的技术。过拟合的表现很直观:训练集上的 loss 压得很低,但一到测试集/验证集上 loss 就飙高,模型不是学到了数据背后的通用规律,而是把训练集里的噪声和特性都背下来了。

6.1 L1 / L2 正则化

L1 和 L2 正则化的数学原理在损失函数第4章节中已经分析过,这里聚焦它们在神经网络代码中的实现方式。

6.1.1 L2 正则化:weight_decay

通常通过优化器的 weight_decay 参数直接实现L2正则化:

它等价于在损失函数中加入 λ ∑ w 2 \lambda \sum w^2 λ∑w2 项

python 复制代码
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)

weight_decay 做了什么: 在每次 optimizer.step() 执行参数更新时,不仅减去学习率 × 梯度,还额外减去 λ × W

W n e w = W o l d − η ⋅ ∂ L o r i g i n ∂ W − η ⋅ 2 λ ⋅ W o l d W_{new} = W_{old} - \eta \cdot \frac{\partial L_{origin}}{\partial W} - \eta \cdot 2 \lambda \cdot W_{old} Wnew=Wold−η⋅∂W∂Lorigin−η⋅2λ⋅Wold

这等价于在原始损失函数后面追加了 L2 正则项: L t o t a l = L o r i g i n + λ ∥ W ∥ 2 L_{total} = L_{origin} + \lambda \|W\|^2 Ltotal=Lorigin+λ∥W∥2。更新时多减的那一项 η ⋅ 2 λ ⋅ W o l d \eta \cdot 2 \lambda \cdot W_{old} η⋅2λ⋅Wold 相当于把所有特征的权重都被整体压制在一个较小的范围内,防止任何一个权重过大而主导整个预测。这正是 L2 防止过拟合的核心机制。

为什么 weight_decay 不设计成一个独立的 nn.Module 层?

因为它的操作发生在参数更新那一刻(optimizer 的 step 函数内部),而不是在数据流动的前向传播过程中。所以它归优化器管,不是网络层。

6.1.2 L1 正则化

PyTorch 的 weight_decay 只实现了 L2 正则。如果要加 L1 正则,需要手动在损失函数中添加

python 复制代码
l1_lambda = 1e-5
l1_reg = sum(p.abs().sum() for p in model.parameters())
loss = criterion(pred, target) + l1_lambda * l1_reg

L1 正则化的效果是产生稀疏权重矩阵,梯度恒为 ±1,不重要的参数被持续推向 0 并最终精确归零,相当于做了自动特征选择。

6.1.3 L1 vs L2 对比

维度 L1 正则化 L2 正则化(weight_decay)
效果 稀疏解,不重要参数 → 0 权重整体缩小,但很少归零
实现 手动加在 loss 中 weight_decay 参数
适合 需要特征选择 一般防止过拟合
梯度 恒定 ±1(推参数到 0) 正比于参数值(越大的权重被拉得越多)

6.2 Dropout(随机失活)

Dropout 在训练过程中 ,以概率 p p p 随机让一层中的部分神经元输出直接置为 0("失活"),而保留的神经元输出则乘以 1 1 − p \frac{1}{1-p} 1−p1 进行缩放。 注意!!!在测试/推理阶段,不执行 dropout,所有神经元都参与计算。

直觉类比: 一个团队里,如果某个关键成员总是负责处理某类任务,整个团队就会过度依赖这个人,万一他请假,团队就卡壳了。但如果每周随机抽签让一部分人休假,剩下的人不得不学会彼此的工作,团队整体的鲁棒性反而提高了。

从集成学习的角度理解: 有 n n n 个神经元的层,理论上每次 dropout 可以产生 2 n 2^n 2n 种不同的"子网络"。训练过程中每次迭代训一个不同的子网络,测试时使用完整的网络,效果上等同于这 2 n 2^n 2n 个子网络在做共同投票(ensemble)。这也是为什么 Dropout 的论文标题叫 "Dropout: A Simple Way to Prevent Neural Networks from Overfitting",它本质上用极低的计算成本模拟了集成学习的效果。

为什么缩放?

假设丢弃率 p = 0.5 p=0.5 p=0.5,那么训练时实际工作神经元只有一半,总输出约减半。若不做缩放,测试阶段全部神经元激活时,输出幅值会翻倍,造成训练-测试不一致。缩放因子 1 / ( 1 − p ) 1/(1-p) 1/(1−p) 让训练阶段的期望输出与测试阶段对齐。

为什么测试时不再Dropout?

测试阶段需要稳定、确定性的预测,因此关闭Dropout,使用全部神经元。调用 model.eval() 即可自动切换。

使用建议

  • 丢弃概率 p p p :通常 0.2~0.5。小模型用 0.2~0.3,深层网络过拟合严重时用 0.5。注意 p p p 是丢弃概率(drop probability),不是保留概率。
  • 放置位置 :全连接层的激活函数之后。Dropout 随机屏蔽的是激活后的输出。
  • 输入层和输出层通常不加 Dropout:输入层尚无学到的高级特征(丢弃无意义),输出层直接决定预测结果(丢弃太危险)。

6.3 Batch Normalization(批量归一化)

6.3.1 是什么与为什么

在神经网络的训练过程中,流经网络的数据都是一个batch,每个batch之间的数据分布变化非常剧烈,这就使得网络参数频繁的进行大的调整以适应流经网络的不同分布的数据,给模型训练带来非常大的不稳定性,使得模型难以收敛。如果我们对每一个batch的数据进行标准化之后,数据分布就变得稳定,参数的梯度变化也变得稳定,有助于加快模型的收敛。

Batch Normalization(BN)对每一层的输入数据按当前 batch 做标准化(使均值为 0、方差为 1),然后再通过两个可学习的参数 γ \gamma γ(缩放)和 β \beta β(平移)进行重构,让网络自己决定最优的输出分布。

第一步:标准化

x ^ = x − μ b a t c h σ b a t c h 2 + ϵ \hat{x} = \frac{x - \mu_{batch}}{\sqrt{\sigma_{batch}^2 + \epsilon}} x^=σbatch2+ϵ x−μbatch

其中 μ b a t c h \mu_{batch} μbatch 和 σ b a t c h 2 \sigma_{batch}^2 σbatch2 是当前 mini-batch 的均值和方差, ϵ \epsilon ϵ 是防止除零的小常数(默认 1e-5)。

标准化后数据集中在 0 附近,落在激活函数的有效区域内。以 Sigmoid 为例,远离 0 的区域导数接近 0(饱和区),标准化让数据避开这些区域,梯度的流通更通畅。即使是 ReLU,集中分布在 0 附近也能防止激活值过大导致的数值不稳定。

为什么需要,内部协方差偏移

在训练过程中,随着前面参数的每次更新,后面接收到的输入数据的分布也在不断变化,这个现象叫做内部协方差偏移(Internal Covariate Shift)

用一个流水线类比:前面的工位每天都在改变产品的摆放方式,后续的工人每天都得重新适应,效率极低。如果在两个工位之间加一个自动化的标准化工位,不管前面怎么摆,到我这里自动对齐和校正,后续工人就能高效稳定工作。

BN 就是这个"标准化工位"。既然每批次的输入分布总在变,那就强行把它标准化到一个均值为 0、方差为 1 的稳定分布上,让后面的参数能在一个稳定的数据环境中高效学习。

第二步:重构

y = γ x ^ + β y = \gamma \hat{x} + \beta y=γx^+β

  • γ \gamma γ:可学习的缩放参数(初始值为 1),恢复数据的方差自由度
  • β \beta β:可学习的平移参数(初始值为 0),恢复数据的均值自由度

为什么标准化之后还要重构? 如果强制每一层的输出永远都是均值 0、方差 1,你就剥夺了网络表达复杂分布的能力,有些任务就是需要偏斜的、不同方差的分布才能最好地表达信息。 γ \gamma γ 和 β \beta β 是可学习参数,让网络自己去决定"最好的分布是什么样子"。

如果标准化本身已经是最优状态,网络可以学出 γ = 1 , β = 0 \gamma=1, \beta=0 γ=1,β=0(恒等映射);如果任务需要不同的分布,网络通过学习调整 γ \gamma γ 和 β \beta β 来实现。换言之,重构这步让 BN 既享受了标准化的稳定性,又保留了模型的表达能力

批量归一化的作用:

  • 减少内部协方差偏移:通过对每层的输入进行标准化,减少了输入数据分布的变化,从而加速了训练过程,并使得网络在训练过程中更加稳定。

  • 加速训练:

    • 在没有批量归一化的情况下,神经网络的训练通常会很慢,尤其是深度网络。因为在每层的训练过程中,输入数据的分布(特别是前几层)会不断变化,这会导致网络学习速度缓慢。
    • 批量归一化通过确保每层的输入数据在训练时分布稳定,有效减少了这种变化,从而加速了训练过程。
  • 起到正则化作用:批量归一化可以视作一种正则化方法,因为它引入了对训练样本的噪声(不同批次的统计信息不同,批次较小的均值和方差估计会更加不准确),使得模型不容易依赖特定的输入特征,从而起到一定的正则化效果,减少了对其他正则化技术(如Dropout)的需求。

  • 提升泛化能力: 由于其正则化效果,批量归一化能帮助网络在测试集上取得更好的性能。

6.3.2 Batch Normalization 的使用步骤

  1. BN 放在线性层之后、激活函数之前:
    • 通常,BN 层会添加在卷积层 (Conv2d) 或全连接层 (Linear) 之后,激活函数之前
    • 例如:Conv2d -> BN -> ReLU 或者 Linear -> BN -> ReLU。

为什么: 标准化应该直接作用在线性层的输出上,修正它的分布,然后交给激活函数。如果在激活后做 BN,ReLU 已经把一半的值砍成 0 了,再标准化这些 0 值没有意义,反而破坏了网络的非线性表达能力。

  1. 训练时: model.train()
    • BN 层会计算当前批次的均值 μ μ μ和方差 σ 2 σ² σ2。
    • 然后,利用这两个统计量对当前批次的数据进行规范化。
    • 规范化后的数据会被缩放 γ γ γ 和平移 β β β。
    • 同时,BN 层还会维护一个全局均值全局方差的移动平均值,用于推理阶段。
  2. 推理时: model.eval()
    • 推理时,不会再使用当前批次的均值和方差,而是使用训练阶段计算的全局均值全局方差
    • 同样,规范化后的数据会被缩放 γ γ γ 和平移 β β β。

6.4 Pytorch API实现

6.4.1 nn.Dropout API

所属模块

位于 torch.nn 模块下,继承自 nn.Module,在 forward 中可像普通层一样调用(x = self.dropout(x))。

python 复制代码
torch.nn.Dropout(p=0.5, inplace=False)
参数 含义 默认值 如何选择
p 神经元被丢弃的概率(不是保留概率!) 0.5 小模型 0.2~0.3;深网 0.5
inplace 是否原地操作 False 默认即可

注意 p 的含义: p=0.3 表示 30% 的神经元随机置为 0,70% 保留并自动乘以 1 1 − 0.3 ≈ 1.43 \frac{1}{1-0.3} \approx 1.43 1−0.31≈1.43 进行补偿缩放。

python 复制代码
class MyNetWithDropout(nn.Module):
    def __init__(self, dropout_rate=0.5):
        super().__init__()
        self.fc1 = nn.Linear(3, 5)
        self.fc2 = nn.Linear(5, 5)
        self.fc3 = nn.Linear(5, 2)
        self.dropout = nn.Dropout(dropout_rate)  # Dropout层
        self._init_weights()
    
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.dropout(x)            # 激活后、下一层之前
        x = F.relu(self.fc2(x))
        x = self.dropout(x)
        x = self.fc3(x)
        return x

6.4.2 nn.BatchNorm1d API

所属模块

位于 torch.nn 模块下,继承自 nn.Module,在 forward 中可像普通层一样调用(x = self.bn1(x))。

python 复制代码
torch.nn.BatchNorm1d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
参数 含义 默认值 如何选择
num_features 输入的特征维度(= 上一层 nn.Linearout_features 必填 = 上一层神经元数
eps 防止除零的小常数 1e-5 默认即可,NaN 时增大
momentum running_mean / running_var 的更新动量 0.1 batch 小时适当增大(如 0.01)
affine 是否使用可学习的 γ \gamma γ 和 β \beta β 进行重构 True 默认开启
track_running_stats 是否在训练时累积全局统计量 True 默认开启(测试时需要)

6.4.3 在示例网络中的完整代码位置

python 复制代码
class MyNet(nn.Module):
    def __init__(self, dropout_p=0.3):
        super().__init__()
        # 线性层
        self.fc1 = nn.Linear(3, 5)
        self.fc2 = nn.Linear(5, 5)
        self.fc3 = nn.Linear(5, 2)

        # 正则化层
        self.bn1 = nn.BatchNorm1d(5)         # num_features = fc1 输出维度
        self.bn2 = nn.BatchNorm1d(5)         # num_features = fc2 输出维度
        self.dropout = nn.Dropout(p=dropout_p)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.relu(self.bn1(self.fc1(x)))   # 隐藏层1: Linear→BN→ReLU
        x = self.dropout(x)                     # 激活后 Dropout(仅训练时生效)
        x = self.relu(self.bn2(self.fc2(x)))   # 隐藏层2: Linear→BN→ReLU
        x = self.fc3(x)                         # 输出层:不加正则
        return x

记住各层在数据流中的精确顺序:

复制代码
fc1 → BN → ReLU → Dropout → fc2 → BN → ReLU → Dropout → fc3
 ↑      ↑     ↑        ↑
 │      │     │        └─ 激活后随机丢弃已激活的神经元
 │      │     └─ 非线性变换
 │      └─ 标准化线性输出,稳定分布
 └─ 加权求和

6.5 正则化选择策略

场景 推荐配置 原因
默认起步 weight_decay=1e-4 一行代码,零额外计算,对大多数任务有益无害
全连接层多、容易过拟合 + Dropout(p=0.3~0.5) 打破神经元间的相互依赖
深层网络,希望加速 + 稳定训练 + BatchNorm1d 标准化每层输入分布,可适当增大学习率
三者联合 weight_decay + BN + Dropout BN 已有轻微正则效果,Dropout 的 p 值可以适当调小

不要过度正则化,每种正则化技术都有抑制模型表达能力的代价。weight_decay 太大 → 欠拟合;Dropout 太高 → 训练信号不足;BN 在 batch size 极小时(< 8)统计量噪声过大。好的做法是从小系数开始,根据验证集 loss 曲线逐步调整。


7. 完整训练代码

以下是贯穿全文的示例网络(3→5→5→2,二分类)的完整 PyTorch 实现,集成了前述所有知识点。每个组件对应的章节在注释中标注。

python 复制代码
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
import torch.optim.lr_scheduler as lr_scheduler

# ============================================================
# 网络结构定义:第1-3章 + 第6章
# ============================================================
class MyNet(nn.Module):
    """示例网络:3输入 → 5-5隐藏层 → 2分类输出"""
    def __init__(self, dropout_p=0.3):
        super().__init__()

        # ------ 全连接层(第1章) ------
        self.fc1 = nn.Linear(3, 5)       # 隐藏层1:3输入→5神经元
        self.fc2 = nn.Linear(5, 5)       # 隐藏层2:5→5
        self.fc3 = nn.Linear(5, 2)       # 输出层:5→2(不加 Softmax)

        # ------ 参数初始化(第2章) ------
        init.kaiming_uniform_(self.fc1.weight, nonlinearity='relu')
        init.kaiming_uniform_(self.fc2.weight, nonlinearity='relu')
        init.xavier_uniform_(self.fc3.weight)
        init.zeros_(self.fc1.bias)
        init.zeros_(self.fc2.bias)
        init.zeros_(self.fc3.bias)

        # ------ 激活函数(第3章) ------
        self.relu = nn.ReLU()

        # ------ 正则化层(第6章) ------
        self.bn1 = nn.BatchNorm1d(5)     # num_features = fc1输出维度
        self.bn2 = nn.BatchNorm1d(5)
        self.dropout = nn.Dropout(p=dropout_p)

    def forward(self, x):
        # x: (batch_size, 3)
        x = self.relu(self.bn1(self.fc1(x)))     # → (batch_size, 5)
        x = self.dropout(x)                       # 训练时随机失活
        x = self.relu(self.bn2(self.fc2(x)))     # → (batch_size, 5)
        x = self.fc3(x)                           # → (batch_size, 2), logits
        return x


# ============================================================
# 训练配置:第4章(损失函数)+ 第5章(优化器+学习率衰减)+ 第6章(L2正则)
# ============================================================
model = MyNet(dropout_p=0.3)

# 损失函数(第4章):CrossEntropyLoss 内部已含 Softmax
criterion = nn.CrossEntropyLoss()

# 优化器(第5章):Adam,weight_decay 实现 L2 正则(第6章)
optimizer = optim.Adam(model.parameters(), lr=0.001,
                       betas=(0.9, 0.999), weight_decay=1e-4)

# 学习率衰减(第5章):每 30 个 epoch 学习率降为原来的 0.1 倍
scheduler = lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)


# ============================================================
# 训练循环(第1章):batch / epoch / iteration 的关系
# ============================================================
def train(model, dataloader, criterion, optimizer, scheduler, num_epochs=50):
    for epoch in range(num_epochs):                          # epoch 循环
        model.train()                                         # 训练模式(Dropout+BN 生效)
        total_loss = 0

        for batch_x, batch_y in dataloader:                   # iteration 循环
            # ① 前向传播
            logits = model(batch_x)                           # (batch_size, 2)

            # ② 计算损失
            loss = criterion(logits, batch_y)

            # ③ 清空旧梯度(PyTorch 默认累加,必须手动清零)
            optimizer.zero_grad()

            # ④ 反向传播(自动链式求导,梯度存入 .grad)
            loss.backward()

            # ⑤ 参数更新(读取 .grad,执行梯度下降)
            optimizer.step()

            total_loss += loss.item()

        # ⑥ epoch 结束后更新学习率
        scheduler.step()
        avg_loss = total_loss / len(dataloader)
        print(f'Epoch [{epoch+1}/{num_epochs}], '
              f'Loss: {avg_loss:.4f}, LR: {scheduler.get_last_lr()[0]:.6f}')


# ============================================================
# 测试 / 评估
# ============================================================
def evaluate(model, dataloader):
    model.eval()                                              # 评估模式
    correct, total = 0, 0
    with torch.no_grad():                                     # 不计算梯度
        for batch_x, batch_y in dataloader:
            logits = model(batch_x)
            _, predicted = torch.max(logits, dim=1)            # 取概率最大的类别
            total += batch_y.size(0)
            correct += (predicted == batch_y).sum().item()
    return correct / total

训练流程回顾:

复制代码
每个 iteration(一个 batch):
  zero_grad() → forward() → loss() → backward() → step()

每个 epoch:
  重复 N 个 iteration(N = 数据集样本数 / batch_size)
  调用 scheduler.step() 更新学习率

训练全过程:
  重复 num_epochs 个 epoch

上下两篇到这里全部结束。从神经元出发,理清了网络骨架和数据流动的维度变化,弄懂了前向和反向传播的完整计算过程,然后逐一深入参数初始化、激活函数、损失函数、优化器、学习率衰减和正则化。

以上为个人学习总结,旨在梳理个人理解。如有疏漏或不当之处,欢迎指正与交流。如果文章对你有帮助,别忘了点个赞、留个言,让更多的小伙伴看到~ 我们下篇再见!

相关推荐
阿图灵1 小时前
基于 GRU 的 Seq2Seq 中英机器翻译:从 Tatoeba 语料到 BLEU 0.195
深度学习·gru·nlp·机器翻译·seq2seq
砚凝霜3 小时前
软考网络工程师|第 9 章 网络诊断命令 + 故障工具 + 故障排查完整笔记
网络·笔记
今天AI了吗3 小时前
从 LLM 到 Agent Skill:把 AI 底层概念串起来
数据库·人工智能·sql·深度学习·神经网络·算法·机器学习
网络工程小王3 小时前
【HCIE-AI】4.NLP 核心任务与技术演进学习笔记
人工智能·深度学习·自然语言处理·nlp·transformer
九硕智慧建筑一体化厂家4 小时前
打破系统孤岛!IBMS 一体化平台,构建智慧楼宇全域管控体系
运维·网络·人工智能·笔记·智慧城市
黑泽明*4 小时前
LVS-Keepalived-全解析
服务器·开发语言·笔记·学习·php
大模型码小白4 小时前
AI安全前沿:AI大模型安全防护的前沿技术
java·网络·人工智能·python·深度学习·学习·安全
熊猫钓鱼>_>5 小时前
Seedance 2.0 技术深度解析:重构AI视频生成的世界模型新范式
人工智能·笔记·ai·重构·音视频·变革·sedence2.0
天天爱吃肉82185 小时前
【工程师硬件学习笔记:串口‑总线体系与三电试验室异构系统集成深度实战】
大数据·笔记·python·嵌入式硬件·学习·汽车