优化器学习小结

2025-9-10

优化器的python实现

基本原理

基本构建类:torch.optim.Optimizer

# SGD 示例
optimizer_sgd = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

# Adam 示例
optimizer_adam = torch.optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999))

zero_grad() 方法 任何 PyTorch 训练循环中的一个关键步骤是调用 optimizer.zero_grad()。在 PyTorch 中,反向传播 (loss.backward()) 期间计算的梯度默认是累积的;它们被加到每个参数的 .grad 属性上,而不是被覆盖。这种设计选择对于训练循环神经网络 (RNN) 或为模拟大批量训练实现梯度累积等场景非常有用。然而,对于标准训练,在计算当前迭代的梯度之前,必须清除上一次迭代的梯度。zero_grad() 方法通过遍历所有注册的参数并将其 .grad 属性设置为 None 或清零来完成此任务 。因此,标准的训练工作流程是三个步骤的重复序列:  

  1. optimizer.zero_grad():重置上一步的梯度。

  2. loss.backward():为当前的小批量数据计算梯度。

  3. optimizer.step():使用计算出的梯度更新模型的参数。

step() 方法 step() 方法是优化器的核心,特定算法的数学更新规则在此执行。调用此方法时,它会遍历每个参数组,并对组内的每个参数执行优化步骤。它访问参数计算出的梯度(存储在 p.grad 中),并使用它以及任何存储的状态(如动量缓冲)和超参数(如学习率)来更新参数的值 (p.data)。这正是优化理论的抽象数学公式被转化为张量上的具体计算操作的地方 。

本质上,优化器的不同在于step方法中,对于每个张量的grad属性取值的更新方式。

状态管理

现代优化器是有状态的,这意味着它们在训练迭代中维护信息以指导未来的更新。这种状态管理以及动态控制超参数的能力是通过一系列复杂的特性来处理的。

参数组 param_groups 是高级训练方案的一个关键特性。传递给优化器构造函数的参数列表在内部存储为字典列表,每个字典代表一个参数组。这种结构允许为模型的不同部分设置不同的超参数。一个在迁移学习中的常见用例是为大型预训练主干模型的参数应用较小的学习率,而为新初始化的分类器头使用较大的学习率,从而防止预训练的特征在训练早期被破坏性地改变 。同样,可以从权重衰减中排除某些参数,如偏置项。  

状态字典 (state_dict) 优化器维护着在训练过程中演变的状态。对于带动量的 SGD,这个状态是每个参数的“速度”或“动量缓冲”。对于 Adam,它包括一阶和二阶矩估计(mv)。这个状态对算法的功能至关重要,必须被保存才能正确地恢复训练。state_dict() 方法返回一个包含此信息的字典,该字典通常将内部参数 ID 映射到其对应的状态张量。相反,load_state_dict() 允许将此状态加载回优化器,确保训练过程的无缝继续 。  

学习率调度器 (lr_scheduler) 学习率可以说是最重要的超参数,并且在整个训练过程中很少保持不变。学习率调度器是根据预定计划调整学习率的实用工具。常见的策略包括 StepLR(在指定周期按因子衰减学习率)和 CosineAnnealingLR(遵循余弦曲线平滑地降低学习率)。这些调度器通过在每个步骤或周期修改其 param_groups 内的 lr 值与优化器交互,为控制收敛动态提供了强大的机制 。

Example

# 模仿pytorch结构的simpleSGD
import torch

class SimpleSGD:
    def __init__(self, params, lr=0.01):
        """
        初始化优化器。
        :param params: 一个可迭代的待优化参数。
        :param lr: 学习率。
        """
        self.params = list(params)
        self.lr = lr

    def zero_grad(self):
        """清除所有被优化参数的梯度。"""
        for p in self.params:
            if p.grad is not None:
                p.grad.detach_() # 原地操作
                p.grad.zero_()

    def step(self):
        """执行单次优化步骤。"""
        with torch.no_grad(): # 内部操作不应被 autograd 跟踪
            for p in self.params:
                if p.grad is None:
                    continue
                # 核心 SGD 更新规则
                p.data -= self.lr * p.grad.data

梯度估计,学习率,收敛轨迹

优化器设计的本质在于对数据点(样本)提供可靠的梯度估计方法,在数据集和模型上获得鲁棒的收敛轨迹。

前LLM时代的核心优化器状态:速度量 — 动量

概述

标准的动量更新规则引入了一个“速度”向量 vtv_t​,它累积了过去梯度的指数衰减移动平均。设 θt\theta_t 为时间步 t 的模型参数,gt=θL(θt1)g_t = \nabla_{\theta}L(\theta_{t-1}) 为损失函数 L 相对于前一时刻参数的梯度,η 为学习率,β[0,1)β \in [0,1) 为动量系数。更新方程为:

vt=βvt1+gtv_t​=\beta v_{t−1}​+g_t​ θt=θt1ηvtθ_t​=θ_{t−1}​−\eta v_t​

这里,v0v_0​ 初始化为零。速度 vtv_t​ 维持了过去梯度方向的“记忆”。当连续的梯度指向相似的方向时,它们对 vtv_t​ 的贡献会累积,导致在该方向上步长更大。相反,当梯度振荡时,它们的贡献倾向于相互抵消,从而抑制更新并防止不稳定的跳跃 。另一种常见的公式将速度定义为真正的指数移动平均 (EMA) :  

v_t​=\beta v_{t−1}​+(1−\beta)g_t $$​ #### Nesterov 加速度 Nesterov 加速梯度 (NAG) 是对标准动量的一种改进,在实践中通常能提供更好的收敛速度。NAG 的关键洞见在于,它不是在当前位置计算梯度,而是在一个近似的未来位置——即沿着当前动量方向的“前瞻”一步——计算梯度 1 。这使得优化器能够预测其前进方向并更快地修正路线。如果动量即将把更新带过一个最小值,那么在前瞻位置的梯度将指回最小值,提供一个修正项来减缓更新。 ### 二阶矩 -- 自适应学习率 自适应学习率方法通过为每个参数维护一个基于该参数梯度历史进行调整的逐参数学习率来自动化此过程。

主题: 优化器, 谁是Adam