【pytorch系列】优化器optimizer的使用与常用优化器

本文介绍了优化器在深度学习中的重要性,从基础的SGD优化器开始,讲解了动量优化器(SGD+Momentum)、NAG、AdaGrad、RMSProp、Adadelta以及Adam等自适应学习率的优化算法,分析了它们的优缺点和工作原理,并通过实例展示了如何在PyTorch中使用这些优化器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 optimizer类实例


1.1 介绍

属性

  • 【default】该实例的类型为 dict,元素为初始化时候的学习率等,具体的keys为
     ['lr','momentum',  'dampening',  'weight_decay',  'nesterov']
    
  • 【state】保存参数更新过程中的一些中间变量,如momentum的缓存(使用前几次的梯度进行平均)
  • 【params_groups】该实例的类型为 list,可以将神经网络中需要更新的参数分组管理,list中每个元素为一组数据,包括:优化器中需要更新的参数,以及default中超参数在当前的对应数据。数据格式为
    [
    {'lr': **, 'momentum': **,  'dampening':**,  'weight_decay':**,  'nesterov':**, 'params':[**]} ,
     ...
    ]
    

方法

  • 【step()】进行一次梯度更新
  • 【zero_grad()】清零优化器中模型参数的梯度。主要在 .step() 后使用。注意pytorch主张 张量梯度不自动清零
  • 【add_param_group()】入参类型为字典,即要添加参数组,以及对该组参数需设置的超参,如未设置默认使用default中的。添加的参数组会存放在 param_group属性当中。优化器可管理多组数据,该方法在finetune模型时经常使用。
  • 【state_dict()】获取优化器当前状态信息,我们去到源码可看到,该方法的返回值如下 在这里插入图片描述
  • 【load_state_dict()】加载状态信息。这两个方法用于模型断点的一个继续训练,所以我们在训练时,隔一段时间应保存一次该信息。

这里补充说明下参数weight_deacy

  • 当需要设置L2正则时,可不用自定义额外添加到损失函数中。只需要在实例化 optimizer时,设置weight_deacy即可,此时优化器在进行权重的更新时使用的公式为
    wi+1=wi∗(1−weight_decay)+∂Loss∂wiw_{i+1}=w_i*(1-weight\_decay)+\frac{\partial{Loss}}{\partial{w_i}}wi+1=wi(1weight_decay)+wiLoss。那么这个公式是如何等价L2正则呢?
    当损失函数中使用的是L2正则:
    Obj=Cost+RegularizationTerm=Loss+λ2∗∑iNwi2wi+1=wi−∂Obj∂wi=wi−∂Loss∂wi−λ∗wi=wi(1−λ)−∂Loss∂wi \begin{aligned} Obj&=Cost+Regularization Term \\ &=Loss+\frac{\lambda}{2}*\sum_{i}^{N}w^2_i \\ w_{i+1}&=w_i-\frac{\partial{Obj}}{\partial{w_i}} \\ &=w_i-\frac{\partial{Loss}}{\partial{w_i}}-\lambda*w_i \\ &=w_i(1-\lambda)-\frac{\partial{Loss}}{\partial{w_i}} \end{aligned}Objwi+1=Cost+RegularizationTerm=Loss+2λiNwi2=wiwiObj=wiwiLossλwi=wi(1λ)wiLoss

1.2 例子

这里使用个小例子来说明优化器的使用:

import torch
import torch.nn as nn
import torch.optim as optim

def geneWeight():
    weight = torch.randn((2,2),requires_grad=True)
    weight.grad = torch.ones((2,2))
    return weight
torch.manual_seed(0)
a = geneWeight()
b = geneWeight()

optimizer = optim.SGD([a], lr=0.1)
optimizer.add_param_group({'params': [b], 'weight_decay': 0.005})

optimizer.step()
optimizer.zero_grad()

可有以下分析

  • 1 【optimizer的属性】在实例化后进行debug,可看到【defaultparam_groupsstate
    在这里插入图片描述

  • 2 optimizer.add_param_group()运行之后的optimizer的内容如下,可以看到【.add_param_group()】的操作将新的参数组添加到优化器中
    在这里插入图片描述

    • 【optimizer.param_groups中的参数】与【变量(在工程中就是网络模型中的参数)】的关系:
      打印两者的id,可以看到是完全一致的,说明optimize.param_groups中的参数存的是网络中参数的引用,这样操作也是为了节省内存
      在这里插入图片描述
  • 3【optimizer.step()optimizer.zero_grad()

    • 使用debug模式,在该.step()命令运行前后分别打印变量的data和grad,可以看到权重进行了更新,对于SGD优化器的更新公式为【weight = weight - lr * grad】
    • .zero_grad() 命令前后运行分别打印参数信息,可以看到梯度置零
    • 可以看到,权重的更新和梯度的置零是分开操作的。当资源有限的情况下,就可以多次进行梯度计算(这里涉及到loss.backward)求梯度均值,再进行权重更新 和 梯度置零,这就是 梯度累计操作。这样在资源有限的情况下,约等于增大了前向传播的batch。
      在这里插入图片描述
  • 4【.state_dict().load_state_dict()
    这两种方法,是用于保存和加载优化器的一个状态信息,经常用于训练中间断掉后的继续训练。
    在第一次运行保存结果时的 optimizer,和第二次运行加载模型后的optimizer,可查看两次的优化器当中的 state_dict()的内容,这里不截图展示。

     import torch
     import torch.nn as nn
     import torch.optim as optim
     import os
      
     def geneWeight():
         weight = torch.randn((2,2),requires_grad=True)
         weight.grad = torch.ones((2,2))
         return weight
     
     torch.manual_seed(0)
     a = geneWeight()
     b = geneWeight()
     
     optimizer = optim.SGD([a], lr=0.1)
     optimizer.add_param_group({'params': [b]})
     
     ckpt = "optimizer_state_dict.pt"
     pretrained = True
     if pretrained and os.path.exit(ckpt):
         state_dict = torch.load(ckpt)
         optimizer.load_state_dict(state_dict)
     
     for i in range(100):
         optimizer.step()
         torch.save(optimizer.state_dict(), ckpt)
    

2 常用优化器的计算

损失函数:深度学习模型通过引入损失函数,用来计算目标预测的错误程度。根据损失函数计算得到的误差结果,需要对模型参数(即权重和偏差)进行很小的更改,以期减少预测错误。
优化器:使损失函数最小化的方式更改可训练参数,损失函数指导优化器朝正确的方向移动。


优化器的发展历程:SGD -> SGDM -> NAG —>AdaGrad -> AdaDelta -> Adam -> Nadam–>…
从AdaGrad之后提出的为 自适应学习率的优化算法。其思想:经常更新的参数,需要学习速度慢一些,偶尔更新的参数,需要学习率大一些。


2.1 SGD

BGD (Batch gradient descent) 批量梯度下降法】:每次的梯度的更新 使用所有的样本。每一次的梯度更新都使用所有样本,更新100次遍历所有数据100次

  • 优点:每次迭代都计算了全部的样本,获取到的是全局最优解
  • 缺点
    1)要对实际数据同时计算梯度,就会非常的耗时;
    2)同事实际使用中数据量都很大,无法进行一次完成所有数据的迭代。

SGD(Stochastic gradientdescent)随机梯度下降法】:每次的梯度的更新 使用一个样本。

  • 优点:速度快
  • 缺点
    1)噪声大,波动大;
    2)非常容易陷入局部最优解;
    3)结果具有随机性,因为可能只使用到部分的样本,就已经迭代到局部最优解了

MBGD(Mini-batch gradient descent)小批量梯度下降】:每次的梯度的更新 使用batch个样本。

  • 优点
    1)相较SGD收敛更稳定;
    2)另一方面可以充分地利用深度学习库中高度优化的矩阵操作来进行更有效的梯度计算。
  • 缺点
    1)SGD的学习受 学习率影响,如果lr偏大,lossfunction 会在极小值处不停地震荡;lr偏小,收敛速度就会变慢。
    这种情况一般解决方法,训练开始使用较大lr,随着迭代过程逐步降低lr,这样我们需要多尝试 lr降低策略,直到找到最优的)
    2)对于非凸函数,容易陷入局部最优解。因为在鞍点周围的所有维度的梯度都约等于0,就很容易困在这里在这里插入图片描述

但在实际的论文或工程中,所说的使用优化器SGD,其实指代的MBGD。这样强调的重点是参数更新的计算方式,而不是batch数量。所以在本博客接下来的表述中,SGD就指代MBGD。

SGD的表达式为:θt=θt−1−lr∗▽tJ(θt)\theta^{t}=\theta^{t-1}-lr*\bigtriangledown _{t}J(\theta^{t})θt=θt1lrtJ(θt)


2.2 SGD+Momentum

为了优化SGD的问题,提出了SGDM(使用动量的随机梯度下降)。
动量方法是为了加速学习(加速梯度下降),特别的是处理高曲率、小但一致的梯度,或带噪声的梯度。动量累积了之前梯度指数级衰减的移动平均,并继续沿该方向移动。
这里约等于是对动量进行了加权平均。

具体的数学表达式如下: vt=γvt−1+lr∗▽J(θt)v^{t} = \gamma v^{t-1} + lr*\bigtriangledown J(\theta^{t})vt=γvt1+lrJ(θt) θt=θt−1−vt\theta^{t}=\theta^{t-1}-v^{t}θt=θt1vt
其中,gtg^tgt为本次计算的梯度,lrlrlr 为学习率,θ\thetaθ为当前的参数。
γ\gammaγ为动量因子,通常被设置为0.9~0.99之间。

SGDM】好处:

  • 减小震荡,加速收敛: 在前后两次梯度方向改变时,momentum能够降低参数更新速度,从而减少震荡;在两次梯度方向相近时,momentum可以加速参数更新, 从而加速收敛
  • 离开鞍点,离开局部最优: 如果运行到了鞍点,不会立马停下来,即使当前的方向为水平,因为会借用上一时刻的动量,从而离开鞍点,离开局部最优值。

在pytorch中,SGDM的公式调整为vt=γvt−1+▽J(θt)v^{t} = \gamma v^{t-1} + \bigtriangledown J(\theta^{t})vt=γvt1+J(θt) θt=θt−1−lr∗vt\theta^{t}= \theta^{t-1}-lr*v^{t}θt=θt1lrvt

这里图示简单示意SGDM是如何减小震荡的。横轴为权重参数,纵轴为该操作的输出,我们想由起始点开始优化迭代,直至目标输出target。

  • 使用SGD的收敛曲线 (图一),进行了7次的梯度更新后,到达target。使用SGDM的收敛曲线 (图二),进行4次的梯度更新后,到达target。
    对与某一次的迭代,若上次和本次的梯度方向相反(夹角大于90),则会减弱本次的梯度,使其减小震荡,加速收敛 (图三);若上次和本次的梯度方向相近(夹角小于90度),动量项产生一个加速的作用,从而加速收敛(图略)。最后给出个动图模拟两者的收敛
    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    请添加图片描述

2.3 NAG

牛顿加速梯度动量优化方法(NAG, Nesterov accelerated gradient):沿着上一步的速度方向先走一小步,再看当前的梯度然后再走一步。简化的理解是:

  • SGD:在点B时,直接沿着点B的梯度进行更新即可
  • SGDM:在点B时,先沿着动量项的方向更新到B’,然后在按照点B的梯度方向更新。
  • NAG:在点B时,先沿着动量项的方向更新到 B’,然后计算B’ 位置的梯度,再沿着 B’ 的梯度方向进行更新。具体公式为θ′=θt−1−γvt−1\theta^{'}=\theta^{t-1}-\gamma v_{t-1}θ=θt1γvt1 vt=γvt−1+lr∗▽J(θ′)v_{t}=\gamma v_{t-1}+lr*\bigtriangledown J(\theta^{'})vt=γvt1+lrJ(θ) θt=θt−1−vt\theta^{t}=\theta^{t-1}-v_tθt=θt1vt在这里插入图片描述

在pytorch内,对需要训练的参数只维护一组参数的,而且是自动求导。这也就意味着,我们每次的梯度更新是需要走到θ′\theta^{'}θ,下一次的backward才能在θ′\theta^{'}θ处求导。虽然最后得到的是“多跨了一步动量”的参数,但是由于到最后靠近极值点的时候动量已经很小了,所以有一点误差也并无大碍。
经推导,上面可修改为 vt=γvt−1+▽J(θt−1′)v_t=\gamma v_{t-1} + \bigtriangledown J(\theta^{'}_{t-1})vt=γvt1+J(θt1) θt′=θt−1′−lr∗(▽J(θt−1′)+γvt)\theta^{'}_{t} = \theta^{'}_{t-1} - lr * (\bigtriangledown J(\theta^{'}_{t-1}) + \gamma v_{t})θt=θt1lr(J(θt1)+γvt)


2.4 AdaGrad

二阶动量:可以度量历史更新频率。使⽤⼀个小批量随机梯度 gtg_tgt按元素平⽅的累加变量 的所有历史累计值ntn_tnt。 可以解释为以往所有梯度值的平方和,越大表示经常跟新,越小表示不经常更新。
n0=g02n_0=g^{2}_0n0=g02
n1=g02+g12n_1=g^{2}_0+g^{2}_1n1=g02+g12
n2=g02+g12+g22n_2=g^{2}_0+g^{2}_1+g^{2}_2n2=g02+g12+g22

AdaGrad 的操作为:根据梯度的情况自适应的调整学习率,从而避免统⼀的学习率难以适应所有维度的问题。在第t次时,有公式:nt=nt−1+gt2n_{t}=n_{t-1}+g^{2}_{t}nt=nt1+gt2 Δθt=−lrnt+ε∗gt\Delta \theta_{t}=-\frac{lr}{\sqrt{n_{t}+\varepsilon }}*g_{t}Δθt=nt+εlrgt
特点:

  • ntn_tnt单调递增,lr′lr^{'}lr单调递减。
  • 前期ntn_tnt较小的时候,学习率较大,能够放大梯度。
    后期ntn_tnt 较大的时候,学习率较小,能够缩小梯度
    中后期,分母上梯度平方的累加会越来越大,使gradient→0,使得训练提前结束。

2.5 RMSProp算法 / Adadelta算法

两者都是为了解决 AdaGrad 学习率下降过快的问题,提出了 ntn_tnt的计算使用加权的方式,只关注过去一段时间内的变化。

  • 对于RMSProp,更新的梯度为 nt=γnt−1+(1−γ)gt2n_{t}=\gamma n_{t-1}+(1-\gamma)g^{2}_{t}nt=γnt1+(1γ)gt2 Δθt=−lrnt+ε∗gt\Delta \theta_{t}=-\frac{lr}{\sqrt{n_{t}+\varepsilon }}*g_{t}Δθt=nt+εlrgt
  • 对于Adadelta,没有学习率这个超参数。它会维护一个新的变量 Δxt\Delta x_{t}Δxt,初始为0,用其来带替学习率的设置。
    nt=γnt−1+(1−γ)gt2n_{t}=\gamma n_{t-1}+(1-\gamma)g^{2}_{t}nt=γnt1+(1γ)gt2 Δθt=−Δxi−1+εnt+ε∗gt\Delta \theta_{t}=-\frac{\sqrt{\Delta x_{i-1}+\varepsilon }}{\sqrt{n_{t}+\varepsilon }}*g_{t}Δθt=nt+εΔxi1+εgt Δxi=ρΔxi−1+(1−ρ)gt2\Delta x_{i}=\rho \Delta x_{i-1}+(1-\rho )g^{2}_{t}Δxi=ρΔxi1+(1ρ)gt2

2.6 Adam

Adam(Adaptive Moment Estimation)自适应矩估计。adam–> SGDM + RMSProp,也就是结合了动量加权、自适应学习率的系数加权(梯度的平方)。公式为vt=β1vt−1+(1−β1)gtv_t=\beta_{1}v_{t-1} + (1-\beta_1)g_t vt=β1vt1+(1β1)gtnt=β2nt−1+(1−β2)gt2n_t=\beta_2n_{t-1} + (1-\beta_2)g^2_tnt=β2nt1+(1β2)gt2作者发现一阶和二阶值初始训练时很小,接近为0,所以作者重新计算了个偏差进行校正,降低偏差对训练初期的影响。训练前期时,1/(1−βt)1/(1-\beta^t)1/(1βt)起到校正作用,训练后期,该项约定于1,不起作用。v^t=vt1−β1t\hat{v}_t=\frac{v_t}{1-\beta^t_1}v^t=1β1tvt n^t=nt1−β2t\hat{n}_t=\frac{n_t}{1-\beta^t_2}n^t=1β2tnt 最终的梯度更新为 θt+1=θt−lrn^t+εv^t\theta_{t+1}=\theta_t-\frac{lr}{\sqrt{\hat{n}_t+\varepsilon}}\hat{v}_tθt+1=θtn^t+εlrv^t其中 β1t\beta^t_1β1tβ2t\beta^t_2β2tβ\betaβ的t次方。超参通常选择 β1=0.9\beta_1=0.9β1=0.9β2=0.999\beta_2=0.999β2=0.999ε=108\varepsilon=10^8ε=108

adam中1/(1−βt)1/(1-\beta^t)1/(1βt)的参数的推导:
n1=(1−β2)g12n_1=(1-\beta_2)g^2_1n1=(1β2)g12
n2=β2v1+(1−β)g22n_2=\beta_2 v_1+(1-\beta)g^2_2n2=β2v1+(1β)g22
  =β2(1−β2)g12+(1−β2)g22=\beta_2(1-\beta_2)g^2_1+(1-\beta_2)g^2_2=β2(1β2)g12+(1β2)g22
  =(1−β2)(β2g12+g22)=(1-\beta_2)(\beta_2g^2_1+g^2_2)=(1β2)(β2g12+g22)
  =(1−β2)(β22−1g12+β22−2g22)=(1-\beta_2)(\beta^{2-1}_2g^2_1+\beta^{2-2}_2g^2_2)=(1β2)(β221g12+β222g22)
  =(1−β2)∑i=12β22−ig22=(1-\beta_2)\sum^2_{i=1}\beta^{2-i}_2g^2_2=(1β2)i=12β22ig22
nt=(1−β2)∑i=1tβ2t−1gi2n_t=(1-\beta_2)\sum^t_{i=1}\beta^{t-1}_2 g^2_int=(1β2)i=1tβ2t1gi2
E(nt)=(1−β2)E(∑i=1tβ2t−igi2)+ξE(n_t)=(1-\beta_2) E(\sum^{t}_{i=1}\beta^{t-i}_2 g^2_i)+\xiE(nt)=(1β2)E(i=1tβ2tigi2)+ξ
   =(1−β2)(1+β21+β22+...+β2t−1)E(gi2)+ξ=(1-\beta_2)(1+\beta^1_2+\beta^2_2+...+\beta^{t-1}_2)E(g^2_i)+\xi=(1β2)(1+β21+β22+...+β2t1)E(gi2)+ξ
   =(1−β2)(1−β2t1−β2)E(gi2)+ξ=(1-\beta_2)(\frac{1-\beta^t_2}{1-\beta_2})E(g^2_i)+\xi=(1β2)(1β21β2t)E(gi2)+ξ
   =(1−β2t)E(gi2)+ξ=(1-\beta^t_2)E(g^2_i)+\xi=(1β2t)E(gi2)+ξ
我们实际需要的是梯度的二阶矩估计E(gi2)E(g^2_i)E(gi2),但当前是对vtv_tvt求期望E(vt)E(v_t)E(vt),因此要得到 E(gi2)E(g^2_i)E(gi2),就需要除以前面的系数。
公式推导是OK的,但这里的个人理解还是有点不明确。只需要把握住:重新计算了个偏差进行校正,可以降低偏差对训练初期的影响。

### PyTorch 中 Adam 优化器的参数及其用法 PyTorch 的 `torch.optim` 模块提供了多种优化算法,其中 Adam 是一种常用的自适应学习率优化方法。以下是关于 Adam 优化器的关键参数以及如何在实际场景中使用它的详细介绍。 #### 关键参数说明 Adam 优化器的核心参数包括但不限于以下几个: 1. **lr (Learning Rate)** 学习率控制每次迭代中的步长大小,默认值为 `0.001`。它决定了模型权重更新的速度。较小的学习率可能导致收敛缓慢,而较大的学习率可能使损失函数不稳定甚至发散[^2]。 2. **betas** 这是一个元组 `(beta1, beta2)`,用于计算梯度的一阶矩估计和二阶矩估计的指数衰减率。默认值通常设置为 `(0.9, 0.999)`。这些值影响动量项和平滑梯度的效果。 3. **eps (Epsilon)** 小常数,用来防止除零错误并提高数值稳定性,默认值为 `1e-8`。 4. **weight_decay** 权重衰减系数(L2 正则化),默认值为 `0`。通过增加正则化可以有效减少过拟合的风险。 5. **amsgrad** 是否启用 AMSGrad 变体,默认为 `False`。AMSGrad 修改了原始 Adam 方法,在某些情况下能够提供更好的性能。 #### 使用示例 下面展示了一个典型的 Adam 优化器实例化的代码片段,并演示了如何将其应用于神经网络训练过程。 ```python import torch from torch import nn from torch.optim import Adam # 定义简单的全连接层网络作为示例 model = nn.Sequential( nn.Linear(10, 5), nn.ReLU(), nn.Linear(5, 1) ) # 设置初始学习率为 0.001 并应用 L2 正则化 optimizer = Adam(model.parameters(), lr=0.001, weight_decay=1e-5) # 训练循环伪代码 for epoch in range(num_epochs): for data, target in dataloader: optimizer.zero_grad() output = model(data) loss = criterion(output, target) loss.backward() optimizer.step() # 更新模型参数 ``` 上述代码展示了如何定义一个基本的线性回归模型,并利用 Adam 优化器对其进行训练。注意调用了 `zero_grad()` 清空之前累积的梯度;随后执行反向传播操作 (`loss.backward()`) 和参数更新 (`optimizer.step()`)。 #### 复杂情况下的分组管理 当面对复杂的多模块架构时,可以通过参数分组来实现更精细的调整策略。例如,对于预训练好的 BERT 层和其他新增加的部分采用不同学习速率的情况如下所示: ```python params = [ {"params": [p for n, p in model.named_parameters() if "bert" not in n]}, {"params": [p for n, p in model.named_parameters() if "bert" in n], 'lr': 2e-5} ] optimizer = Adam(params, lr=0.001) # 默认学习率仅适用于未特别指定部分 ``` 这里我们将整个网络划分为两个子集——非BERT组件沿用标准配置即 `lr=0.001` 而含BERT字样的那些层则被赋予更低水平的学习速度以保护已有的知识迁移成果[^4]。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值