————————————————————————————————————
优化算法
前言
参考:https://zhuanlan.zhihu.com/p/376387915
首先应该知道优化算法的目的是什么:既然在变量空间的某一点处,函数沿梯度方向具有最大的变化率,那么在优化目标函数的时候,沿着负梯度方向去减小函数值,以此达到我们的优化目标。
梯度的定义如下:
g
r
a
d
f
(
x
0
,
x
1
,
.
.
.
,
x
n
)
=
(
∂
f
∂
x
0
,
∂
f
∂
x
1
,
.
.
.
,
∂
f
∂
x
n
)
gradf(x_0,x_1,...,x_n) = (\frac{\partial f}{\partial x_0},\frac{\partial f}{\partial x_1},...,\frac{\partial f}{\partial x_n})
gradf(x0,x1,...,xn)=(∂x0∂f,∂x1∂f,...,∂xn∂f)
梯度的提出只为回答一个问题:
函数在变量空间的某一点处,沿着哪一个方向有最大的变化率?(针对这一点,我们都知道对于神经网络而言,神经元节点可看做变量x,而每个变量x都有自己的参数
w
w
w,在某一隐藏层,可能存在256的神经元节点,每个神经元节点对于损失函数来说都有属于自己的方程,我们实质性求得梯度,是基于损失函数在该神经元节点的导数,可能又出现一个问题是众多的偏导数和梯度有什么关系,这也就回到了梯度的定义上,一个空间中存在多个变量,而沿着哪一方向有最大的变化率,则为梯度,而这个变化率就是所求的导)
梯度定义如下:
函数在某一点的梯度是一个向量,它的方向与取得最大方向导数的方向一致,而它的模为方向导数的最大值。
这里注意三点:
1)梯度是一个向量,即有方向有大小
2)梯度的方向是最大方向导数的方向
3)梯度的值是最大方向导数的值
参考文章:https://zhuanlan.zhihu.com/p/32230623
首先定义:待优化的参数:
w
w
w,目标函数:
f
(
w
)
f(w)
f(w),初始学习率:
l
r
l_r
lr
而后,开始进行迭代优化。在每个
e
p
o
c
h
epoch
epoch:
- 1 计算目标函数关于当前参数的梯度(可以理解为计算损失函数关于当前网络层参数的梯度): g t = ∇ f ( w t ) g_t = \nabla f(w_t) gt=∇f(wt)
- 2 根据历史梯度计算一阶动量和二阶动量: m t = ϕ ( g 1 , g 2 . . . . g t ) m_t = \phi(g_1,g_2....g_t) mt=ϕ(g1,g2....gt), V t = φ ( g 1 , g 2 . . . . g t ) V_t = \varphi(g_1,g_2....g_t) Vt=φ(g1,g2....gt)
- 3 计算当前时刻的下降梯度: η t = l r × m t V t \eta_t = \frac{l_r \times m_t}{\sqrt V_t} ηt=Vtlr×mt
- 4 根据下降梯度进行最优参数的更新:
w
t
+
1
=
w
t
−
η
t
w_{t+1} = w_t - \eta_t
wt+1=wt−ηt
掌握了这个框架,你可以轻轻松松设计自己的优化算法。
SGD理解1
SGD是每一次迭代计算mini-batch的梯度。
SGD没有动量的概念,也就是说:
m
t
=
g
t
m_t = g_t
mt=gt,
V
t
=
I
2
V_t = I^2
Vt=I2
代入步骤3,可以看到下降梯度就是最简单的:
η
t
=
l
r
×
g
t
\eta_t = l_r\times g_t
ηt=lr×gt
代入步骤4,可以得到梯度更新公式为:
w
t
+
1
=
w
t
−
l
r
×
g
t
w_{t+1} = w_t - l_r\times g_t
wt+1=wt−lr×gt
SGD最大的缺点是下降速度慢,而且可能会在沟壑的两边持续震荡,停留在一个局部最优点。
g
t
g_t
gt是当前batch的梯度,所以
l
r
×
g
t
l_r\times g_t
lr×gt可理解为允许当前batch的梯度多大程度影响参数更新
SGD with Momentum
为了抑制SGD的震荡,SGDM(SGD with momentum)认为梯度下降过程可以加入惯性。下坡的时候,如果发现是陡坡,那就利用惯性跑的快一些。在SGD基础上引入了一阶动量:
m
t
=
β
1
×
m
t
−
1
+
g
t
m_t = \beta_1\times m_{t-1}+g_t
mt=β1×mt−1+gt
也就是说,t时刻的下降方向,不仅由当前点的梯度方向决定,而且由此前累积的下降方向决定。
β
1
\beta_1
β1的经验值为0.9,这就意味着下降方向主要是此前累积的下降方向,并略微偏向当前时刻的下降方向。
代入步骤4,可以得到梯度更新公式为:
w
t
+
1
=
w
t
−
l
r
(
×
β
1
×
m
t
−
1
+
(
1
−
β
1
)
×
g
t
)
V
t
w_{t+1} = w_t - \frac {l_r(\times \beta_1\times m_{t-1}+(1-\beta_1)\times g_t)}{\sqrt V_t}
wt+1=wt−Vtlr(×β1×mt−1+(1−β1)×gt)
SGD理解2
原理:
模型每次反向传导都会给各个可学习参数
w
w
w计算出一个偏导数
g
t
g_{t}
gt,用于更新对应的参数
w
w
w。通常偏导数
g
t
g_{t}
gt不会直接作用到对应的可学习参数
w
w
w上,而是通过优化器做一下处理,得到一个新的值
g
^
t
\hat{g}_t
g^t,处理过程用函数F表示(不同的优化器对应的F的内容不同),即
g
t
^
=
F
(
g
t
)
\hat{g_t}=F(g_{t})
gt^=F(gt),然后和学习率
l
r
l_r
lr一起用于更新可学习参数
w
w
w,即
w
=
w
−
l
r
×
g
t
^
w = w - l_r\times \hat{g_t}
w=w−lr×gt^。
在pytorch中,SGD定义为:
torch.optim.SGD(params,
lr=<required parameter>,
momentum=0,
dampening=0,
weight_decay=0,
nesterov=False)
params
:模型中需要被更新的可学习参数
lr
:学习率
momentum
:动量—通过上一次的
v
t
−
1
v_{t-1}
vt−1和当前的偏导数
g
t
g_t
gt,得到本次的
v
t
v_t
vt,即
v
t
=
v
t
−
1
×
m
o
m
e
n
t
u
m
+
g
t
v_{t}=v_{t-1}\times momentum+g_{t}
vt=vt−1×momentum+gt,这个就是上述的函数F。
dampening
:是乘到偏导数g上的一个数,即:
v
t
=
v
t
−
1
×
m
o
m
e
n
t
u
m
+
g
t
×
(
1
−
d
a
m
p
e
n
i
n
g
)
v_{t}=v_{t-1}\times momentum+g_{t}\times (1-dampening)
vt=vt−1×momentum+gt×(1−dampening)。注意:dampening在优化器第一次更新时,不起作用。
weight_decay
:权重衰减参数:其直接作用于
g
t
g_t
gt,即:
g
t
=
g
t
+
(
g
t
−
1
×
w
e
i
g
h
t
d
e
c
a
y
)
g_t = g_t + (g_{t-1}\times weight_{decay})
gt=gt+(gt−1×weightdecay)
流程计算
:
1.先计算: g t = g t + ( w × w e i g h t d e c a y ) g_t = g_t + (w\times weight_{decay}) gt=gt+(w×weightdecay) 得到当前轮关于 w w w的导数 g t g_t gt,在第一轮训练中, w w w为初始化的随机梯度值,或者加载的预训练权值。
2.在计算: v t = v t − 1 × m o m e n t u m + g t × ( 1 − d a m p e n i n g ) v_{t}=v_{t-1}\times momentum+g_{t}\times (1-dampening) vt=vt−1×momentum+gt×(1−dampening) 此时的 v 0 = 0 ; v 1 = g 1 v_0=0 ; v_1=g_1 v0=0;v1=g1
3.参数更新: w t = w t − l r × v t w_t = w_t - l_r\times v_t wt=wt−lr×vt
例:
假设函数 Y = W 2 Y = W^2 Y=W2, W W W的随机梯度初始值为100,则 Y Y Y的导为 2 W 2W 2W,假设 w e i g h t d e c a y weight_{decay} weightdecay为0.1,假设 m o m e n t u m momentum momentum为0.9,假设 d a m p e n i n g dampening dampening为0.5, l r l_r lr为0.01。第一次更新SGD默认不使用 d a m p e n i n g dampening dampening。
第一轮:
g 1 = g 1 + ( w × w e i g h t d e c a y ) = 200 + ( 100 ∗ 0.1 ) = 210 g_1 = g_1 + (w\times weight_{decay}) = 200 + (100*0.1)= 210 g1=g1+(w×weightdecay)=200+(100∗0.1)=210
v 1 = v 0 × m o m e n t u m + g 1 × ( 1 − d a m p e n i n g ) = 210 v_{1}=v_{0}\times momentum+g_{1}\times (1-dampening) = 210 v1=v0×momentum+g1×(1−dampening)=210
w = w − l r × v 1 = 100 − 0.01 ∗ 210 = 97.9 w = w - l_r\times v_1 = 100 - 0.01*210 = 97.9 w=w−lr×v1=100−0.01∗210=97.9
第二轮:
g 2 = g 2 + ( w × w e i g h t d e c a y ) = 195.8 + ( 97.9 ∗ 0.1 ) = 205.59 g_2 = g_2 + (w\times weight_{decay}) = 195.8 + (97.9*0.1)= 205.59 g2=g2+(w×weightdecay)=195.8+(97.9∗0.1)=205.59
v 2 = v 1 × m o m e n t u m + g 2 × ( 1 − d a m p e n i n g ) = 210 ∗ 0.9 + 205.59 ∗ ( 1 − 0.5 ) = 189 + 102.795 = 291.795 v_{2}=v_{1}\times momentum+g_{2}\times (1-dampening) = 210*0.9+205.59*(1-0.5)= 189+102.795=291.795 v2=v1×momentum+g2×(1−dampening)=210∗0.9+205.59∗(1−0.5)=189+102.795=291.795
w = w − l r × v 2 = 97.9 − 0.01 ∗ 291.795 = 97.9 − 2.92 = 94.98 w = w - l_r\times v_2 = 97.9- 0.01*291.795 = 97.9 -2.92=94.98 w=w−lr×v2=97.9−0.01∗291.795=97.9−2.92=94.98
为了验证这一流程的正确项,使用pytorch定义这个函数,并反向传播验证所计算得到的梯度值:
import torch
def test_sgd():
# 定义一个可学习参数w,初值是100
w = torch.tensor(data=[100], dtype=torch.float32, requires_grad=True)
# 定义SGD优化器,nesterov=False,其余参数都有效
optimizer = torch.optim.SGD(params=[w], lr=0.01, momentum=0.9, dampening=0.5, weight_decay=0.1, nesterov=False)
# 进行5次优化
for i in range(5):
y = w ** 2 # 优化的目标是让w的平方,即y尽可能小
optimizer.zero_grad() # 让w的偏导数置零
y.backward() # 反向传播,计算w的偏导数
optimizer.step() # 根据上述两个公式,计算一个v,然后作用到w
print('grad=%.2f, w=%.2f' % (w.grad, w.data)) # 查看w的梯度和更新后的值
if __name__=="__main__":
test_sgd()
'''
输入日志如下:
grad=200.00, w=97.90
grad=195.80, w=94.98
grad=189.96, w=91.36
grad=182.72, w=87.14
grad=174.28, w=82.42
'''
#------------------神经网络模型损失优化器定义---------------------
model = MyModel()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=1e-4)
for epoch in range(1, epochs):
for i, (inputs, labels) in enumerate(train_loader):
output= model(inputs) #---这就是上面的 y=w*2 函数
loss = criterion(output, labels) #---损失计算
optimizer.zero_grad() #---梯度清零
loss.backward() #---误差反向传播
optimizer.step() #梯度更新
Adam 优化算法
原理:
模型每次反向传导都会给各个可学习参数
w
w
w计算出一个偏导数
g
t
g_t
gt,用于更新对应的参数
w
w
w。通常偏导数
g
t
g_t
gt不会直接作用到对应的可学习参数
w
w
w上,而是通过优化器做一下处理,得到一个新的值
g
t
^
\hat{g_t}
gt^,处理过程用函数
F
F
F表示(不同的优化器对应的F的内容不同),即
g
t
^
=
F
(
g
t
)
\hat{g_t}=F(g_t)
gt^=F(gt),然后和学习率
l
r
l_r
lr一起用于更新可学习参数
w
w
w,即
w
=
w
−
l
r
×
g
t
^
w = w - l_r \times \hat{g_t}
w=w−lr×gt^。
在pytorch中,Adam定义为:
torch.optim.Adam(params,
lr=0.001,
betas=(0.9, 0.999),
eps=1e-08,
weight_decay=0,
amsgrad=False)
params
:模型里需要被更新的可学习参数
lr
:学习率
betas
:平滑常数
β
1
\beta_1
β1和
β
2
\beta_2
β2
eps
:
ϵ
\epsilon
ϵ非常小的数,预防分母为0
weight-decay
:权值(可学习参数)衰减系数,用于修改偏导数:
g
t
=
g
t
+
(
w
×
w
e
i
g
h
t
d
e
c
a
y
)
g_t = g_t + (w\times weight_{decay})
gt=gt+(w×weightdecay),其中
g
t
g_t
gt为可学习参数
w
w
w的偏导数。
amsgrad
:amsgrad和Adam并无直接关系。
计算过程如下:
初始1:学习率 lr
初始2:平滑常数(或者叫做衰减速率)
β
1
,
β
2
\beta_1,\beta_2
β1,β2,分别用于平滑
m
m
m和
v
v
v。
初始3:可学习参数
w
w
w
初始4:
m
0
=
0
;
v
0
=
0
;
t
=
0
m_0 = 0;v_0 = 0;t = 0
m0=0;v0=0;t=0
while 是否进行训练:
训练次数更新:t = t + 1
计算梯度:
g
t
g_t
gt(所有的可学习参数都有自己的梯度,因此
g
t
g_t
gt表示的是全部梯度的集合)
累计梯度:
m
t
=
β
1
×
m
t
−
1
+
(
1
−
β
1
)
×
g
t
m_t = \beta_1\times m_{t-1}+(1-\beta_1)\times g_t
mt=β1×mt−1+(1−β1)×gt(每个导数对应一个m,因此m也是个集合)
累计梯度的平方:
v
t
=
β
2
×
v
t
−
1
+
(
1
−
β
2
)
×
(
g
t
)
2
v_t = \beta_2\times v_{t-1}+(1-\beta2)\times(g_t)^2
vt=β2×vt−1+(1−β2)×(gt)2(每个导数对应一个v,因此v也是个集合)
偏差纠正
m
^
\hat{m}
m^:
m
t
^
=
m
t
1
−
(
β
1
)
t
\hat{m_t}=\frac{m_t}{1-(\beta_1)^t}
mt^=1−(β1)tmt
偏差纠正
v
^
\hat{v}
v^:
v
t
^
=
v
t
1
−
(
β
2
)
t
\hat{v_t}=\frac{v_t}{1-(\beta_2)^t}
vt^=1−(β2)tvt
更新参数
w
w
w:
w
=
w
−
l
r
×
m
t
^
v
t
^
+
ϵ
w = w - l_r \times \frac{\hat{m_t}} {\sqrt{\hat{v_t}}+\epsilon}
w=w−lr×vt^+ϵmt^
end while
例:
假设函数 Y = W 2 Y = W^2 Y=W2, W W W的随机梯度初始值为100,则 Y Y Y的导为 2 W 2W 2W,假设 w e i g h t d e c a y weight_{decay} weightdecay为0.1,假设 b e t a s = ( β 1 , β 2 ) betas= (\beta_1,\beta_2) betas=(β1,β2)为(0.9,0.999), l r l_r lr为0.01。
第一轮:t=1
w = 100 , g 1 = 2 w = 200 w=100,g_1 = 2w = 200 w=100,g1=2w=200, m 1 = β 1 × m 0 + ( 1 − β 1 ) × g 1 = 0.9 ∗ 0 + ( 1 − 0.9 ) 200 = 20 m_1 = \beta_1\times m_{0}+(1-\beta_1)\times g_1=0.9*0+(1-0.9)200=20 m1=β1×m0+(1−β1)×g1=0.9∗0+(1−0.9)200=20
v 1 = β 2 × v 0 + ( 1 − β 2 ) × ( g 1 ) 2 = 0.999 ∗ 0 + ( 1 − 0.999 ) ∗ 40000 = 40 v_1 = \beta_2\times v_{0}+(1-\beta2)\times(g_1)^2=0.999*0+(1-0.999)*40000=40 v1=β2×v0+(1−β2)×(g1)2=0.999∗0+(1−0.999)∗40000=40
m 1 ^ = m 1 1 − ( β 1 ) t = 20 / 0.1 = 200 \hat{m_1}=\frac{m_1}{1-(\beta_1)^t} = 20/0.1=200 m1^=1−(β1)tm1=20/0.1=200
v 1 ^ = v 1 1 − ( β 2 ) t = 40000 \hat{v_1}=\frac{v_1}{1-(\beta_2)^t} = 40000 v1^=1−(β2)tv1=40000
w = w − l r × m t ^ v t ^ + ϵ = 100 − 0.01 ∗ 1 = 99.99 w = w - l_r \times \frac{\hat{m_t}} {\sqrt{\hat{v_t}}+\epsilon}=100 - 0.01*1=99.99 w=w−lr×vt^+ϵmt^=100−0.01∗1=99.99
第二轮:t = 2
w = 99.99 , g 2 = 2 w = 199.98 w=99.99,g_2 = 2w = 199.98 w=99.99,g2=2w=199.98, m 2 = β 1 × m 1 + ( 1 − β 1 ) × g 2 = 0.9 ∗ 20 + 0.1 ∗ 199.98 = 37.998 m_2 = \beta_1\times m_{1}+(1-\beta_1)\times g_2=0.9*20+0.1*199.98=37.998 m2=β1×m1+(1−β1)×g2=0.9∗20+0.1∗199.98=37.998
v 2 = β 2 × v 1 + ( 1 − β 2 ) × ( g 2 ) 2 = 0.999 ∗ 40 + ( 1 − 0.999 ) ∗ 199.98 ∗ 199.98 = 79.68 v_2 = \beta_2\times v_{1}+(1-\beta2)\times(g_2)^2=0.999*40+(1-0.999)*199.98*199.98=79.68 v2=β2×v1+(1−β2)×(g2)2=0.999∗40+(1−0.999)∗199.98∗199.98=79.68
m 2 ^ = m 2 1 − ( β 1 ) t = 37.998 / 0.01 = 3799.8 \hat{m_2}=\frac{m_2}{1-(\beta_1)^t} = 37.998/0.01=3799.8 m2^=1−(β1)tm2=37.998/0.01=3799.8
v 2 ^ = v 2 1 − ( β 2 ) t = 79.68 / 0.000001 = 79680000 \hat{v_2}=\frac{v_2}{1-(\beta_2)^t} = 79.68/0.000001=79680000 v2^=1−(β2)tv2=79.68/0.000001=79680000
w = w − l r × m t ^ v t ^ + ϵ = 99.99 − 0.01 ∗ 3799.8 / 8926 = 99.98 w = w - l_r \times \frac{\hat{m_t}} {\sqrt{\hat{v_t}}+\epsilon}=99.99-0.01*3799.8/8926=99.98 w=w−lr×vt^+ϵmt^=99.99−0.01∗3799.8/8926=99.98
实践是检验真理的唯一标准:
import torch
def test_sgd():
# 定义一个可学习参数w,初值是100
w = torch.tensor(data=[100], dtype=torch.float32, requires_grad=True)
# 定义SGD优化器,nesterov=False,其余参数都有效
#optimizer = torch.optim.SGD(params=[w], lr=0.01, momentum=0.9, dampening=0.5, weight_decay=0.1, nesterov=False)
optimizer = torch.optim.Adam(params=[w],lr=0.01,betas=(0.9, 0.999),eps=1e-08,weight_decay=0.1,amsgrad=False)
# 进行5次优化
for i in range(5):
y = w ** 2 # 优化的目标是让w的平方,即y尽可能小
optimizer.zero_grad() # 让w的偏导数置零
y.backward() # 反向传播,计算w的偏导数
optimizer.step() # 根据上述两个公式,计算一个v,然后作用到w
print('grad=%.2f, w=%.2f' % (w.grad, w.data)) # 查看w的梯度和更新后的值
if __name__=="__main__":
test_sgd()
'''
输入日志如下:
grad=200.00, w=99.99
grad=199.98, w=99.98
grad=199.96, w=99.97
grad=199.94, w=99.96
grad=199.92, w=99.95
'''
代码例子来自:https://blog.csdn.net/qq_37541097 非常厉害的一位博主,跟着他的博客和视频学习到很多,再次表示感谢❤❤❤