2024/02/28
前言
机器学习中的五个步骤:数据 ——> 模型 ——> 损失函数 ——> 优化器 ——> 迭代训练,通过前向传播,得到模型的输出和真实标签之间的差异,也就是损失函数,有了损失函数之后,模型反向传播得到参数的梯度,接下来就是优化器根据这个梯度去更新参数,使得模型的损失不断降低,那么优化器是如何做到的呢?分别从三个方面了解一下,优化器的概念,优化器的属性和方法,常用的优化器。
pytorch的优化器:管理并更新模型中可学习参数的值,使得模型输出更接近真实标签。
在更新参数时一般使用梯度下降的方式去更新,关于梯度下降,有如下几个相关的概念:
所以梯度是一个向量,方向是导数取得最大值的方向,也就是增长最快的方向,而梯度下降是沿着梯度的负方向去变化,这样函数的下降也是最快的。所以采用梯度下降的方式来更新权值。
关于梯度下降的总结可参考:链接
pytorch中优化器的基本属性:
基本属性:
defaults:优化器超参数,存储学习率、momentum的值、衰减系数等;
state:参数的缓存,如momentum的缓存(使用前几次梯度进行平均)
param_groups:管理的参数组,这是个列表,每一个元素是一个字典,字典中key的值是真正的参数;
_step_count:记录更新的参数,学习率调整中使用,比如迭代100次之后更新学习率,这里记录100.
基本方法:
zero_grad():清空管理参数的梯度,pytorch中的参数的梯度在计算过程中是不自动清零的,所以需要这个方法将参数的梯度清零,具体实现步骤如上图所示;
step:执行一步更新
add_param_group():添加参数组,优化器可以管理很多参数,可以对这些参数分组,对不同组的参数设置不同的超参数,比如模型 finetune 中,希望前面特征提取的那些层学习率小一些,而后面新加的层学习率大一些更新快一点,就可以用这个方法
state_dict():获取优化器当前状态的字典
load_state_dict():加载状态信息字典,这两个方法用于模型断点的一个续训练,在模型训练时一般在设定epoch之后要保存当前的状态信息。
下面从人民币的二分类案例中学习优化器是如何构建的,以及是如何运行的
在train.py文件中设置断点
debug进入sgd.py文件中的SGD类:
SGD类是继承于optimizer的,所以将代码运行至父类初始化这一行,看是如何进行初始化的:
这个就是optimizer的__init__初始化部分了,可以看到它的属性和初始化方法,其中最重要的是参数组是如何添加的。另外defaults中存的超参数有:
跳出这个函数,执行完参数初始化后如下:
这就是优化器的初始化工作,初始化完之后,先进行梯度清空,然后更新梯度即可。
下面来了解一下优化器具体方法的使用
1、step():一次梯度下降更新参数
输出结果:
2、zero_grad():将梯度清零
输出结果:
使用optimizer.zero_grad()之后可看到参数的梯度就会变为0;另外可看到optimizer的param_groups中的weight地址和weight本身的地址是一样的,这说明optimizer的param_groups存储的是weight的引用,并不是复制一份,这样可以节省内存,在pytorch学习的第一节中说过叶子结点不能进行原位操作,是因为正向传播过程中的参数反向传播中是要用的,并且反向传播过程中的存的是引用,所以正向传播之后参数是不能修改的。
3.add_param_group(): 添加参数组 ,这个是在模型的迁移学习中非常实用的一个方法
输出结果:
4.state_dict()和load_state_dict()
这两个方法用于保存和加载优化器的一个状态信息,通常用在断点的续训练, 比如训练一个模型,训练了10次停电了, 那么再来电的时候就得需要从头开始训练,但是如果有了这两个方法,那么就可以再训练的时候接着上次的次数继续, 所以这两个方法也非常实用。
首先是state_dict()
输出结果:
state_dict() 方法里面保存了优化器的各种状态信息,通过 torch.save 就可以保存这些状态到文件(.pkl), 这样假设此时停电了,就可以通过 load_state_dict() 来导入这个状态信息,让优化器在这个基础上进行训练
输出结果:
可以看到保存了上一次优化器训练后的状态,这样优化器在此基础上就可以重新进行训练了。
在学习pytorch中的优化器之前,有两个重要的概念,学习率和动量,所以先来了解一下这两个的概念。
1、学习率
在梯度下降的过程中,学习率起到控制参数更新的一个步伐的作用,参数的更新公式:
假设没有学习率LR,参数更新公式为:
w
i
+
1
=
w
i
?
g
r
a
d
(
w
i
)
w_{i+1} = w_{i} - grad(w_{i})
wi+1?=wi??grad(wi?)
假设
y
=
f
(
x
)
=
4
?
x
2
y = f\left ( x \right ) = 4 * x^{2}
y=f(x)=4?x2.
y
′
=
f
′
(
x
)
=
8
?
x
y^{'} = f^{'}\left ( x \right ) = 8 * x
y′=f′(x)=8?x
那么
x
0
=
2
,
y
0
=
16
,
f
′
(
x
0
)
=
16
x_{0}=2,y_{0}=16,f^{'}\left ( x_{0} \right )=16
x0?=2,y0?=16,f′(x0?)=16
x
1
=
x
0
?
f
′
(
x
0
)
=
2
?
16
=
?
14
x_{1}=x_{0}-f^{'}\left ( x_{0} \right )=2-16=-14
x1?=x0??f′(x0?)=2?16=?14
x
1
=
?
14
,
y
1
=
784
,
f
′
(
x
1
)
=
?
112
x_{1}=-14,y_{1}=784,f^{'}\left ( x_{1} \right )=-112
x1?=?14,y1?=784,f′(x1?)=?112
x
2
=
x
1
?
f
′
(
x
1
)
=
?
14
+
112
=
98
,
y
2
=
38416
x_{2}=x_{1}-f^{'}\left ( x_{1} \right )=-14+112=98,y_{2}=38416
x2?=x1??f′(x1?)=?14+112=98,y2?=38416
可以看出随着参数的更新,y的值不降反而增大了,这是为什么呢?下面来通过代码来了解一下:
随着迭代次数的增多,loss值反而越来越大,说明在参数更新过程中,步子迈的太大,反而跳过了最优值,这时需要一个参数来控制这个跨度,这个就是学习率。将上式代码中的lr设置为0.5,0.2,0.1,0.125,0.01可观察参数的更新过程。
可以看到选择一个合适的学习率可以起到事半功倍的效果,所以学习率是一个非常重要的超参数,后面还会重点学习学习率的调整策略。
2、动量
Momentum(动量、冲量):结合当前的梯度与上一次更新的信息,用于当前更新。下面来看一个具体的栗子(图源网络):
所以在考虑动量的情况下,可以更快的走到山脚下,也就是说参数更新的更快。那动量是怎么用于参数更新的呢?
先看一下指数加权平均的概念,指数加权平均在时间序列中经常用于求取平均值的一个方法,它的思想是这样,求取当前时刻的平均值,距离当前时刻越近的那些参数值,它的参考性越大,所占的权重就越大,这个权重是随时间间隔的增大呈指数下降,所以叫做指数滑动平均。公式如下:
v
t
v_{t}
vt?是当前时刻的一个平均值,这个平均值有两项构成,一项是当前时刻的参数值
θ
t
heta_{t}
θt?, 所占的权重是1-
β
\beta
β, 这个
β
\beta
β 是个参数。另一项是上一时刻的一个平均值,权重是
β
\beta
β。
举个栗子:
上图是温度图像,横轴是天数,纵轴是温度,假设要求第100天温度的平均值,根据上式公式有:
最后一行就是这个式子的通式,可以看到,距离当前时刻越远的那些
θ
heta
θ 值,权重越小,因为
β
\beta
β小于 1, 所以间隔越远,小于 1 的这些数连乘,权重越来越小,而且是乘指数下降,因为这里是乘以
β
i
\beta ^{i}
βi的。下面通过代码来了解一下权重趋势的变化:
可以看到权重是呈指数下降的,距离当前时刻越近,权重值越大,距离当前时刻越远,权重值越小;这就是指数加权平均的思想。注意到在指数加权平均的公式中的超参数
β
\beta
β,下面看一个这个参数变化有什么影响
可以发现,beta 越小,就会发现它关注前面一段时刻的距离就越短,比如0.8, 会发现往前关注20天基本上后面的权重都是0了,意思就是说这时候是平均的过去20天的温度, 而0.98关注过去的天数会非常长,也就是说这时候平均的过去50天的温度。所以
β
\beta
β在这里控制着记忆周期的长短,或者平均过去多少天的数据对现在的影响。参数
β
\beta
β常设置为0.9,也就是
1
1
?
β
\frac{1}{1-\beta }
1?β1?等于10,关注过去10天左右的温度,如下图是不同
β
\beta
β下温度的一个变化曲线:
Momentum梯度下降:
其中
w
i
+
1
w_{i+1}
wi+1?表示第i+1次更新的参数,lr学习率,
v
i
v_{i}
vi?更新量,m是momentum系数对应指数加权平均就是
β
\beta
β,
g
(
w
i
)
g\left ( w_{i} \right )
g(wi?)是
w
i
w_{i}
wi?的梯度。假设对第100次更新,根据上式公式推导如下:
所以当前梯度的更新量会考虑到当前梯度, 上一时刻的梯度,前一时刻的梯度,这样一直往前,越往后的权重越小。下面通过代码来了解一下momentum的作用
不设置momentum时,不同学习率下,loss值的变化曲线:
可以看到大的学习率参数更新的步长会大,所以loss值达到最小值会快一些,下面对lr=0.01设置momentum0.9,结果显示如下:
可以看到设置momentum的loss值速度快了,但是前面会有震荡,这是因为这里的m太大了,这是因为当前的梯度会受到上一次更新的梯度影响,值过大就会出现波动,所以可以通过调节m的值来减少这种波动,例如设置0.63
官网链接
pytorch中的优化器可以大体分为两类:一类是基于SGD及其优化,另一类是Per-parameter adaptive learning rate methods(逐参数自适应学习率方法),如AdaGrad、RMSProp、Adam等。
梯度更新规则:BGD采用整个训练集的数据来计算 cost function 对参数的梯度
假设线性模型:
cost function:
参数更新:
缺点:
由于在一次更新中,是对整个数据集计算梯度,所以训练速度慢,如果训练集很大,需要消耗大量的内存,且全量梯度下降不能进行在线模型参数更新。
梯度更新规则:
在SGD中,每次迭代只用一个训练数据,
所以参数的更新方法为:
SGD是通过每个样本迭代更新一次,如果样本量很大的情况,那么可能只用到其中的部分样本数据参数就能更新到最优,对比BGD,一次迭代需要全部的数据,一次迭代不可能达到最优,迭代10次就需要将训练集训练10次。
缺点:
1、如果样本中噪音比较多,使得SGD并不是每次迭代向着整体最优化的方向进行;
2、SGD因为更新比较频繁,会造成 cost function 有严重的震荡;
3、可能会收敛到局部最优,但由于震荡会跳过最优。
如下图所示:
梯度更新规则:
MBGD 每次利用一小批样本,即n个样本进行计算,这样可以降低参数更新时的方差,收敛更稳定,另一方面可以利用矩阵操作来进行更有效的梯度计算。
参数更新方法为:
缺点:
1、MBGD 不能保证很好的收敛性,learning rate 如果选择太小,收敛速度慢,选择太大,会使得 cost function 在极小值附近震荡(一种解决措施是先设置大一点的learning rate,当达到某个阈值时,就减少learning rate,不过这个阈值要提前设定);
2、对所有的参数更新时应用同样的learning rate,如果数据是稀疏的,更希望对频率出现低的特征进行大一点的更新。
注:深度学习中的SGD优化算法是指mini-batch SGD(MBGD)
pytroch中SGD的实现:
用一张图来解释加入momentum的梯度下降:
说明一下这几个点(红色代表梯度下降的方向,虚线绿色代表动量的方向,蓝色代表实际移动的方向):
1、对于第一个点来说。梯度下降的方向是往右的,但是由于设置的 v0=0,所以初始时并没有动量的作用,所以此时实际移动的方向就是梯度下降的方向。
2、对于第二个点来说。梯度下降的方向是向右的,但是此时球现在还有一个向右的动量,这个动量会使小球继续往右移动。
3、对于第三个点来说。由于此时是local minima,所以此时的梯度值为0。如果对于普通的梯度下降来说,他就会卡在这个地方。但是此时还有向右的一个动量值,所以使用动量的话,实际是会向右边继续走。
4、对于第四个点来说。此时的梯度下降的方向是向左的,假设如果此处的动量值>梯度的值。此时计算,在此处小球就会朝着动量的方向继续走,他甚至可以冲出山峰,跳出local minima。
所以为了防止梯度等于0(鞍点或局部最小值),引入带有动量的随机梯度下降,从而加快梯度下降的速度。动量算法累积了之前梯度指数级衰减的移动平均,并且继续沿该方向移动。更新公式为:
其中v为动量梯度下降的动量项,且初始化为0;γ是关于动量项的超参数一般取小于等于0.9。使用上面的公式更新参数时,是将之前的梯度都联系起来,不再是每一次梯度都是独立的情况。让每一次参数的更新方向不仅仅取决于当前位置的梯度,还受到上一次参数更新方向的影响。可用下图来直观理解(图源水印)
优点:通过过去梯度信息来优化下降速度,如果当前梯度与之前梯度方向一致时候,收敛速度得到加强,反之则减弱。换句话说,加快收敛同时减小震荡。
缺点:可能在下坡过程中累计动量太大,冲过极小值点。
另外,pytorch中的 SGD with momentum 已经在optim.SGD中的参数momentum中实现。
NAG(加速梯度下降)相比于动量梯度下降的区别是,通过使用未来梯度来更新动量。即将下一次的预测梯度?θJ(θ?η?m)考虑进来。
参数更新公式为:
与普通的momentum的区别如下图
在pytorch中,通过参数nesterov=True 来实现Nesterov Momentum。
优点:
1、相对于动量梯度下降法,因为NAG考虑到了未来预测梯度,收敛速度更快(如上图)。
2、当更新幅度很大时,NAG可以抑制震荡。例如起始点在最优点的左侧←,γm对应的值在最优点的右侧→,对于动量梯度而言,叠加η?1 使得迭代后的点更加远离最优点→→。而NAG首先跳到γm对应的值→,计算梯度为正,再叠加反方向的η?2 ←,从而达到抑制震荡的目的。
AdaGrad在训练过程中动态调整学习率,对不同参数根据累计梯度平方和更新不同学习率。
参数更新公式:
其中⊙是点乘,相当于求梯度的平方。?为防止除0及维持数据稳定的极小项,一般取10^(-6)
因为s是梯度平方和的累加项,所以:
1、梯度一直变化较大的参数,学习率下降也较快,即高频特征使用较小学习率。
2、梯度一直变化较小的参数,学习率下降也较慢,即低频特征使用较大学习率。
3、因为累加性,学习率的趋势是不断衰减的,这也符合迭代后期靠近极值点时需设置较小的学习率的直观想法。
优点:每个变量都有适应自己的学习率
缺点:由于学习率的不断衰减在迭代过程早期衰减过快可能直接导致后期收敛动力不足,使得AdaGrad无法获得满意的结果。
pytroch实现:
针对于AdaGrad的学习率衰减过快缺点,RMSProp通过指数加权移动平均(累计局部梯度信息)替代累计平方梯度和来优化AdaGrad,使得远离当前点的梯度贡献小。
迭代更新公式:
其中β为RMSProp的衰减因子。s为关于梯度的指数加权移动平方和,初始值为0。⊙为点乘,即对应项乘积。
优点:在Adagrad基础上添加衰减因子,在学习率更新过程中权衡过去与当前的梯度信息,减轻了因梯度不断累计导致学习率大幅降低的影响,防止学习过早结束。
缺点:引入了超参数β,增加模型复杂性。同时依赖全局学习率η。
pytorch中的实现:
AdaDelta是针对于Adagrad的另一种优化,它相对于RMSProp,使用参数θ变化量的指数加权移动平方和替换了全局学习率η。其思想是利用一阶方法近似模拟二阶牛顿法。
sg为关于梯度的指数加权移动平方和,sΔθ是关于参数θ变化量的指数加权移动平方和。二者初始值设为0。?是维持数据稳定的常数,一般设置为10^{-6}。
在AdaDelta优化中,分子可以看成一个动量加速项,通过指数加权方式累积先前的梯度变化量。分母项则是与RMSProp一样,所以也可以将RMSProp看成是AdaDelta的一种特殊情况。
优点:
不需要人工设置学习率。
Adam融合了RMSProp及Momentum的思想,做到了学习率自适应和动量加速收敛的效果。
参数更新公式为:
其中第三和第四项是s和m的偏差修正值,使得过去的梯度权值和为1,防止值过小。超参数一般设置为β=0.999, γ=0.9, ε=10^-8?。