mypro334 发表于 2022-4-25 22:18

pytorch中常见优化器的SGD,Adagrad,RMSprop,Adam ...

前置

EMA

指数移动平均,EMA(Exponential Moving Average),是一种给予近期数据更高权重的平均方法。
Nicolas:【炼丹技巧】指数移动平均(EMA)的原理及PyTorch实现
核心公式为: https://www.zhihu.com/equation?tex=%5Clarge+y_t+%3D+%5Cbeta+y_%7Bt-1%7D+%2B+%281-%5Cbeta%29+x_t ,xt是t时刻测量值,yt是指数平均值,beta是范围在0-1之间的系数。
也可以拓展为: https://www.zhihu.com/equation?tex=%5Clarge+y_t+%3D+%5Cbeta+y_%7Bt-1%7D+%2B+%5Cgamma+x_t 或 https://www.zhihu.com/equation?tex=%5Clarge+y_t+%3D+%5Cbeta+y_%7Bt-1%7D+%2B++x_t

yt的初始值可以设置为0,这会导致当t比较小时这个平均和理想情况偏差很大,所以加入偏差修正步骤:

https://www.zhihu.com/equation?tex=%5Clarge+y_t+%3D+%5Cfrac+%7By_t%7D%7B1-%5Cbeta+%5E+t%7D
因为beta小于1,所以当t很小时,偏差修正作用比较大,使得yt接近xt的平均;当t很大时,偏差修正可以忽略

一些使用了EMA的地方:

[*]更平稳的更新模型权重的一种方式(和梯度无关)

[*]防止模型迭代更新太剧烈,利用不参与训练的影子权重(第二套模型参数),把训练权重进行指数移动平均
[*]timm库中有Model EMA的封装后的类 Model EMA (Exponential Moving Average)

[*]强化学习DQN中提出的target Network的权重更新

[*]Q网络的更新采用TD算法,其中涉及自举,很容易出现高估的偏差。所以引入另外一个模型Qtarget来降低偏差
[*]其中Qtarget的权重采用了EMA方式的更新机制

[*]优化器中,梯度的动量机制的实现

[*]很多优化器都加入了梯度动量机制,可以使梯度更平稳,不过会消耗额外的内存/显存

[*]MoCo动量对比学习中使用EMA来更新momentum encoder的权重参数

[*]https://www.zhihu.com/equation?tex=%5Clarge+%5Ctheta_k+%5Cleftarrow+m%5Ctheta_k+%2B+%281-m%29+%5Ctheta_q
[*]其中是用来产生query的encoder模型的参数,是用来产生key的momentum encoder模型的参数, m属于(0,1)
[*] 采用梯度下降方式来更新,采用EMA的方式从来缓慢更新

动量梯度

给模型每个参数记录一个不断累计更新的梯度的动量值,使用EMA方式,将最近的loss计算的梯度更新到动量值里。再使用该动量来作为更新具体每个参数的梯度。若模型的参数量为N,这个动量的记录也要再额外消耗N单位的内存/显存。

https://www.zhihu.com/equation?tex=%5CLarge+%5Cbegin+%7Balign%7D+m_t+%26%3D+%5Cbeta%2Am_%7Bt-1%7D+%2B+%281-%5Cbeta%29%2Ag_t+%5C%5C+w_t+%26%3Dw_%7Bt-1%7D+-%5Ceta+%2A+m_t+%5Cend+%7Balign%7D++    或    https://www.zhihu.com/equation?tex=%5CLarge+%5Cbegin+%7Balign%7D++m_t+%26%3D+%5Cbeta%2Am_%7Bt-1%7D+-+%5Cgamma+%2Ag_t+%5C%5C++w_t+%26%3Dw_%7Bt-1%7D+%2B+m_t+%5Cend++%7Balign%7D++
由于动量是对最近一批梯度的指数加权平均,所以用它来更新模型权重,比直接用原始梯度值,抖动更小更加稳定,也可以快速冲过error surface上的鞍点等比较平坦的区域。常见的优化器的实现里都加入了动量机制,不过默认不一定开启。
L2正则(L2 Regularization)


[*]L2正则是在损失上加一个w的二范数平方项,效果是让w尽可能不要太大
[*]L2正则一般不考虑bias参数的正则,只是计入weights的正则,因为bias参数量小,不易增加模型的复杂性,也不易带来模型输出的方差。对bias加入正则反而可能导致模型欠拟合。

https://www.zhihu.com/equation?tex=%5CLarge+%5Cbegin+%7Balign%7D++L%5E%7B%5Cprime%7D+%26%3DL%2B%5Cfrac+%7B1%7D%7B2%7D+%5Clambda+%5C%7Cw_%7Bt-1%7D%5C%7C%5E2+%5C%5C+%26%3D+L+%2B+%5Cfrac%7B1%7D%7B2%7D%5Clambda+%5Csum+w_%7Bt-1%7D%5E2+%5C%5C+%5C%5C+g%5E%7B%5Cprime%7D+%26%3D+g+%2B+%5Clambda+w_%7Bt-1%7D+%5C%5C+%5C%5C+w_t+%26%3D+w_t+-+%5Ceta+g+-+%5Ceta+%5Clambda+w_%7Bt-1%7D+%5C%5C+%26%3D+%5Cleft%281-%5Ceta+%5Clambda+%5Cright%29w_t+-+%5Ceta+g++%5Cend+%7Balign%7D

权重衰减(Weight Decay)

在更新权重时直接让权重按比例缩小。常见的优化器中都加入了权重衰减机制。

https://www.zhihu.com/equation?tex=%5CLarge+%5Cbegin+%7Balign%7D++w_t+%26%3D%5Cleft%281-+%5Clambda+%5Cright%29+w_%7Bt-1%7D++-+%5Ceta+g+%2C+%5Chspace%7B8+mm%7D+%5Clambda+%5Cin+%5Cleft%280%2C+1+%5Cright+%29++%5Cend+%7Balign%7D
或者

https://www.zhihu.com/equation?tex=%5CLarge+%5Cbegin+%7Balign%7D+g%5E%7B%5Cprime%7D+%26%3D+g+%2B+%5Clambda+w_%7Bt-1%7D+%2C+%5Chspace%7B8+mm%7D+%5Clambda+%5Cin+%5Cleft%280%2C+1+%5Cright+%29+%5C%5C+w_t+%26%3D+w_%7Bt-1%7D+-+%5Ceta+g%5E%7B%5Cprime%7D+%5Cend+%7Balign%7D
权重衰减在形式上和L2正则是一致的,但也有如下区别:

[*]两个lambda含义和数值不一样
[*]pytorch中的优化器的weight decay的默认实现/用法是不区分weights和bias,统一都decay(如何不将bias加入weightdecay的方式请见结尾部分)
常见优化器分析

SGD

pytorch中的SGD优化器融合了:

[*]动量梯度
[*]权重衰减
[*]NAG
NAG(Nesterov Accelerated Gradient)
NAG由俄罗斯数学家Yurii Nesterov在凸优化研究中提出,在使用动量梯度场景下,通过往前看一步的机制可以加速优化的收敛过程。不过这里面涉及套娃过程 ,理论对应到具体实现时比较难懂。



CM与NAG

pytorch官方文档里对SGD的算法描述如下:



pytorch文档中SGD介绍

pytorch1.6中的SGD优化器的实现核心代码如下,采用1.6版本来分析而不是更高版本是因为这个版本的实现中没有再调用到C库的优化器底层实现,全部计算细节都在python代码中了。区别于上述伪代码的是,nesterov开启后没有用 https://www.zhihu.com/equation?tex=%5Clarge+g_%7Bt-1%7D ,而是直接 https://www.zhihu.com/equation?tex=%5Clarge+g_t+%3D+g_t+%2B+%5Cmu%5Ctextbf%7Bb%7D_t ,应该是可以减少内存消耗的近似机制。



sgd.py

从官方文档中可看到:

[*]SGD开启动量梯度时,额外的内存/显存消耗只有momentum_buffer,是1倍的模型参数量;不开启动量梯度则没有额外内存/显存消耗。

Adagrad

Adagrad(Adaptive Gradient)的核心思想是,深度模型带来的稀疏性,导致模型中一些参数可能频繁获得较大梯度,另一些参数偶尔获得较大梯度,若采用统一学习率导致后者的更新会非常缓慢。基于此,可以调节模型中不同参数的学习率,而不是用统一的学习率。如果一个参数的历史累计梯度更新量大,则降低该参数的学习率;如果一个参数的历史累计梯度更新量小,则增大该参数的学习率。

[*]因为梯度有正有负所以对梯度的平方进行累计。
[*]对每个参数分别调节学习率,也可以看成调节每个参数的梯度。
[*]缺点是随着迭代次数增多,累计值越来越大,导致模型参数的实际更新越来微弱



Adagrad

pytorch官方文档里对Adagrad的算法描述如下:



pytorch文档中Adagrad介绍

从官方文档中可看到:

[*]adagrad优化器实现中还加入了学习率衰减机制、权重衰减机制
[*]该算法需要给每一个待更新的模型参数设置一个累计统计量 https://www.zhihu.com/equation?tex=%5Clarge+%5Ctext+%7Bstate_sum%7D_t ,所以额外的内存/显存消耗是,1倍的模型参数量

RMSprop

RMSProp(root mean square propagation)优化算法是祖师爷Geoff Hinton提出的,也属于自适应梯度范畴。区别于adagrad之处是它采用了EMA方式来统计每个参数的最近的累计梯度量,所以多次迭代后不会导致模型参数更新缓慢。



RMSprop

pytorch官方文档里对RMSprop的算法描述如下:



pytorch文档中RMSprop介绍

从官方文档中可看到:

[*]RMSprop优化器实现中还加入了权重衰减机制、动量梯度机制
[*]该算法需要给每一个待更新的模型参数设置一个累计梯度统计量、一个自适应梯度的动量,所以额外的内存/显存消耗是,2倍的模型参数量

Adam

Adam(Adaptive Moment Estimation)优化算法,整合了RMSprop中的自适应梯度机制和动量梯度机制。
其中的amsgrad(默认不开启)对原始adam进行了改进,多了一个max步骤:



amsgrad

pytorch官方文档里对Adam的算法描述如下:



pytorch文档中Adam介绍

从官方文档中可看到:

[*]Adam优化器实现中还加入了权重衰减机制
[*]该算法需要给每一个待更新的模型参数设置一个累计梯度统计量、一个自适应梯度的max值 https://www.zhihu.com/equation?tex=%5Clarge+%5Cwidehat%7Bv_t%7D%5E%7Bmax%7D 、一个自适应梯度的动量,所以额外的内存/显存消耗是,3倍的模型参数
pytorch1.6中Adam的核心代码如下:



adam.py

AdamW

AdamW优化器修正了Adam中权重衰减的bug,Decoupled Weight Decay Regularization.
pytorch官方文档里对AdamW的算法描述如下:



pytorch文档中AdamW介绍

从官方文档中可看到:

[*]AdamW与Adam对比,主要是修改了权重衰减计算的方式,一上来直接修改了,而不是把权重衰减放到梯度里,由梯度更新间接缩小
[*]显存/内存消耗等其他细节和Adam没有区别

总结

特点额外消耗内存/显存SGD没有自适应学习率机制,但仔细调参最终模型效果可能最好最多1倍模型参数量Adam学习率相对不敏感,减少调参试验代价,但weight decay实现不好,能用Adam的地方可以都用AdamW来代替最多3倍模型参数量AdamW相对于Adam,weight decay实现解耦,效果更好最多3倍模型参数量
模型的不同参数设置不同的优化器参数

pytorch中各个优化器的基类Optimizer中实现了param_groups的机制,每个group有自己的模型参数、优化器参数。例如,可以通过其实现让模型不同layer用不同学习率来训练的效果。



一个模型的3种参数用不同的优化器参数

权重衰减去掉bias和BN

来自yolov5源码,把模型参数分成g0,g1,g2三组,可以精细控制每组模型参数的优化器参数    g0, g1, g2 = [], [], []# optimizer parameter groups
    for v in model.modules():
      if hasattr(v, 'bias') and isinstance(v.bias, nn.Parameter):# bias
            g2.append(v.bias)
      if isinstance(v, nn.BatchNorm2d):# weight (no decay)
            g0.append(v.weight)
      elif hasattr(v, 'weight') and isinstance(v.weight, nn.Parameter):# weight (with decay)
            g1.append(v.weight)

    if opt.optimizer == 'Adam':
      optimizer = Adam(g0, lr=hyp['lr0'], betas=(hyp['momentum'], 0.999))# adjust beta1 to momentum
    elif opt.optimizer == 'AdamW':
      optimizer = AdamW(g0, lr=hyp['lr0'], betas=(hyp['momentum'], 0.999))# adjust beta1 to momentum
    else:
      optimizer = SGD(g0, lr=hyp['lr0'], momentum=hyp['momentum'], nesterov=True)
    optimizer.add_param_group({'params': g1, 'weight_decay': hyp['weight_decay']})# add g1 with weight_decay
    optimizer.add_param_group({'params': g2})# add g2 (biases)
来自timm源码def param_groups_weight_decay(model: nn.Module, weight_decay=1e-5):
    decay = []
    no_decay = []
    for name, param in model.named_parameters():
      if not param.requires_grad:
            continue
      if param.ndim <= 1 or name.endswith(".bias"):
            no_decay.append(param)
      else:
            decay.append(param)

return [
      {'params': no_decay, 'weight_decay': 0.},
      {'params': decay, 'weight_decay': weight_decay}]

rustum 发表于 2022-4-25 22:20

你好,我想请问一下,如果我想要把pytorch下的Adam优化器的上一个step的更新与当前step的更新加权求和的话要怎么修改代码呢?
页: [1]
查看完整版本: pytorch中常见优化器的SGD,Adagrad,RMSprop,Adam ...