目录
一、优化器介绍
1、梯度下降法
梯度下降法有三种不同的形式:
- BGD(Batch Gradient Descent):批量梯度下降,每次参数更新使用 所有样本
- SGD(Stochastic Gradient Descent):随机梯度下降,每次参数更新只使用 1个样本
- MBGD(Mini-Batch Gradient Descent):小批量梯度下降,每次参数更新使用小部分数据样本(mini_batch)
这三个优化算法在训练的时候虽然所采用的的数据量不同,但是他们在进行参数优化的时候,采用的方法是相同的
在训练的时候一般都是使用小批量梯度下降算法,即选择部分数据进行训练,这里把这三种算法统称为传统梯度下降法。而更优的优化算法从 梯度方面 和 学习率方面 对参数更新方式进行优化
传统梯度更新算法为最常见、最简单的一种参数更新策略。
其基本思想是:先设定一个学习率 η \eta η,参数沿梯度的反方向移动。假设需要更新的参数为 θ \theta θ,梯度为 g g g,则其更新策略可表示为:
θ ← θ − η ∗ g \theta \leftarrow \theta - \eta * g θ←θ−η∗g
优点:
- 算法简洁,当学习率取值恰当时,可以收敛到 全局最优点(凸函数) 或 局部最优点(非凸函数)。
缺点:
- 对超参数学习率比较敏感:过小导致收敛速度过慢,过大又越过极值点
- 学习率除了敏感,有时还会因其在迭代过程中保持不变,很容易造成算法被卡在鞍点的位置。
- 在较平坦的区域,由于梯度接近于0,优化算法会因误判,在还未到达极值点时,就提前结束迭代,陷入局部极小值。
1.1 一维梯度下降法
我们以 目标函数(损失函数) f ( x ) = x 2 f(x)= x^2 f(x)=x2 为例来看一看梯度下降是如何工作的。
迭代方法为:
x ← x − η ∗ g = x − η ∗ ∂ l o s s ∂ x x \leftarrow x - \eta * g = x - \eta * \frac{\partial loss}{\partial x} x←x−η∗g=x−η∗∂x∂loss
虽然我们知道最小化 f(x) 的解为x=0 ,这里依然使用这个简单函数来观察 x 是如何被迭代的
这里x为模型参数,使用x = 10 作为初始值,并设 学习率 e t a = 0.2 eta = 0.2 eta=0.2,使用梯度下降法 对x 迭代10次
import numpy as np
import matplotlib.pyplot as plt
x = 10
lr = 0.2
result = [x]
for i in range(10):
x -= lr * 2 * x
result.append(x)
f_line = np.arange(-10, 10, 0.1)
plt.plot(f_line, [x * x for x in f_line])
plt.plot(result, [x * x for x in result], '-o')
plt.title('learning rate = {}'.format(lr))
plt.xlabel('x')
plt.ylabel('f(x)')
plt.show()
大家可以尝试使用不同的学习率进行训练,会得到如下结果:
- 如果使用的学习率太小,将导致 x x x的更新非常缓慢,需要更多的迭代。
- 相反,当使用过大的学习率, x x x的迭代不能保证降低 f ( x ) f(x) f(x) 的值,例如,当学习率为 η = 1.1 \eta=1.1 η=1.1时, x x x超出了最优解 x = 0 x=0 x=0,并逐渐发散
1.2 多维梯度下降法
在对一元梯度下降有了了解之后,下面看看多元梯度下降,即考虑 X = [ x 1 , x 2 , ⋯ x d ] T X=[x_1, x_2, \cdots x_d]^T X=[x1,x2,⋯xd]T 的情况。
多元损失函数,它的梯度也是多元的,是一个由d个偏导数组成的向量:
∇ f ( X ) = [ ∂ f x ∂ x 1 , ∂ f x ∂ x 2 , ⋯ , ∂ f x ∂ x d ] T \nabla f(X) = [\frac{\partial f_x}{\partial x_1}, \frac{\partial f_x}{\partial x_2}, \cdots, \frac{\partial f_x}{\partial x_d}]^T ∇f(X)=[∂x1∂fx,∂x2∂fx,⋯,∂xd∂fx]T
然后选择合适的学率进行梯度下降:
x i ← x i − η ∗ ∇ f ( X ) x_i \leftarrow x_i - \eta * \nabla f(X) xi←xi−η∗∇f(X)
下面通过代码可视化它的参数更新过程。构造一个目标函数 f ( X ) = x 1 2 + 2 x 2 2 f(X)=x_1^2+2x_2^2 f(X)=x12+2x22,并有二维向量 X = [ x 1 , x 2 ] X = [x_1, x_2] X=[x1,x2]作为输入,标量作为输出。 损失函数的梯度为 ∇ f ( x ) = [ 2 x 1 , 4 x 2 ] T \nabla f(x) = [2x_1,4x_2]^T ∇f(x)=[2x1,4x2]T 。使用梯度下降法,观察 x 1 , x 2 x_1, x_2 x1,x2从初始位置[-5, -2] 的更新轨迹。
import numpy as np
import matplotlib.pyplot as plt
def loss_func(x1, x2): # 定义目标函数
return x1 ** 2 + 2 * x2 ** 2
x1, x2 = -5, -2
eta = 0.4
num_epochs = 20
result = [(x1, x2)]
for epoch in range(num_epochs):
gd1 = 2 * x1
gd2 = 4 * x2
x1 -= eta * gd1
x2 -= eta * gd2
result.append((x1, x2))
# print('x1:', result1)
# print('\n x2:', result2)
plt.figure(figsize=(8, 4))
plt.plot(*zip(*result), '-o', color='#ff7f0e')
x1, x2 = np.meshgrid(np.arange(-5.5, 1.0, 0.1), np.arange(-3.0, 1.0, 0.1))
plt.contour(x1, x2, loss_func(x1, x2), colors='#1f77b4')
plt.title('learning rate = {}'.format(eta))
plt.xlabel('x1')
plt.ylabel('x2')
plt.show()
2、动量(Momentum)
动量算法每下降一步都是由前面下降方向的一个累积和当前点梯度方向组合而成:
累计梯度更新: v ← α v + ( 1 − α ) g 累计梯度更新:v \leftarrow \alpha v + (1 - \alpha)g 累计梯度更新:v←αv+(1−α)g 梯度更新: x ← x − η ∗ v 梯度更新:x \leftarrow x - \eta*v 梯度更新:x←x−η∗v α 为动量参数, v 累计梯度, η 为学习率 \alpha 为动量参数,v累计梯度,\eta 为学习率 α为动量参数,v累计梯度,η为学习率
为了更好的观察动量带来的好处,我们使用一个新函数 f ( x ) = 0.1 x 1 2 + 2 x 2 2 f(x)=0.1x_1^2+2x_2^2 f(x)=0.1x12+2x22 ,这里 x 1 x_1 x1和 x 2 x_2 x2的系数分别是 0.1 和 2, 这就使得 x 1 x_1 x1和 x 2 x_2 x2 的梯度值相差一个量级,如果使用相同的学习率, x 2 x_2 x2 的更新幅度会较 x 1 x_1 x1 的更大些。
我们先使用不带动量的传统梯度下降算法观察其下降过程,学习率设置为 0.4
import numpy as np
import matplotlib.pyplot as plt
def loss_func(x1, x2): #定义目标函数
return 0.1 * x1 ** 2 + 2 * x2 ** 2
x1, x2 = -5, -2
eta = 0.4
num_epochs = 20
result = [(x1, x2)]
for epoch in range(num_epochs):
gd1 = 0.2 * x1
gd2 = 4 * x2
x1 -= eta * gd1
x2 -= eta * gd2
result.append((x1, x2))
plt.plot(*zip(*result), '-o', color='#ff7f0e')
x1, x2 = np.meshgrid(np.arange(-5.5, 1.0, 0.1), np.arange(-3.0, 1.0, 0.1))
plt.contour(x1, x2, loss_func(x1, x2), colors='#1f77b4')
plt.title('learning rate = {}'.format(eta))
plt.xlabel('x1')
plt.ylabel('x2')
plt.show()
从结果来看,和我们预料的一样:使用相同的学习率, x 2 x_2 x2 的更新幅度会较 x 1 x_1 x1 的更大些,变化快得多。
接下来,我们依然使用传统梯度下降算法,将学习率设置为 0.6。
因为 x 2 x_2 x2的更新路径如下,这会导致 x 2 x_2 x2 的值越来越发散:
x 2 = x 2 − η ∗ g d 2 = x 2 − 0.6 ∗ 4 x 2 = − 1.4 x 2 x_2 = x_2 - \eta * gd_2 = x_2 - 0.6 *4 x_2 = -1.4x_2 x2=x2−η∗gd2=x2−0.6∗4x2=−1.4x2
更新过程如下图:
这时,我们会陷入一个两难的选择:
- 如果选择较小的准确率。可以确保不会朝 x 2 x_2 x2方向发生偏离,但在 x 1 x_1 x1方向收敛会缓慢。
- 如果选择较大的准确率, x 1 x_1 x1方向会收敛很快,但在 x 2 x_2 x2方向就不会向最优点靠近。下面将
学习率从0.4调整到0.6。可以看出在X1方向会有所改善,但是整体解决方案会很差。
有没有什么方法解决这个问题呢?
下面我们尝试从改变梯度入手,将历史的梯度考虑在内:
累计梯度更新: v ← α v + ( 1 − α ) g 累计梯度更新:v \leftarrow \alpha v + (1 - \alpha)g 累计梯度更新:v←αv+(1−α)g
梯度更新: x ← x − η ∗ v 梯度更新: x \leftarrow x - \eta*v 梯度更新:x←x−η∗v
α \alpha α 为动量参数, v v v是累计梯度, η \eta η 为学习率
下面我们使用带动量的梯度算法,动量参数为0.5,将学习率设置为 0.4
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
def loss_func(x1, x2): #定义目标函数
return 0.1 * x1 ** 2 + 2 * x2 ** 2
x1, x2 = -5, -2
v1, v2 = 0, 0
eta, alpha = 0.4, 0.5
num_epochs = 20
result = [(x1, x2)]
for epoch in range(num_epochs):
v1 = alpha * v1 + (1 - alpha) * (0.2 * x1)
v2 = alpha * v2 + (1 - alpha) * (4 * x2)
x1 -= eta * v1
x2 -= eta * v2
result.append((x1, x2))
plt.plot(*zip(*result), '-o', color='#ff7f0e')
x1, x2 = np.meshgrid(np.arange(-5.5, 1.0, 0.1), np.arange(-3.0, 1.0, 0.1))
plt.contour(x1, x2, loss_func(x1, x2), colors='#1f77b4')
plt.xlabel('x1')
plt.ylabel('x2')
plt.show()
即使我们将学习率设置为0.6, x 2 x_2 x2 的梯度也不会发散了
3、Adagrad
Adagrad优化算法 被称为自适应学习率优化算法,之前我们讲的随机梯度下降对所有的参数都使用的固定的学习率进行参数更新,但是不同的参数梯度可能不一样,所以需要不同的学习率才能比较好的进行训练。
举例:
假设损失函数是 L o s s = x 1 2 + 10 x 2 2 Loss=x_1^2+10x_2^2 Loss=x12+10x22,即我们的目标是学习 x 1 x_1 x1和 x 2 x_2 x2的值,让Loss尽可能小。
显然当 x 1 = 0 , x 2 = 0 x_1=0, x_2=0 x1=0,x2=0 时,Loss取得最小值。
假设x和y的初值分别为 x 1 = 40 , x 2 = 20 x_1=40, x_2=20 x1=40,x2=20
此时对Loss函数进行求导, x 1 x_1 x1和 x 2 x_2 x2梯度分别是 ∂ l o s s ∂ x 1 = 80 , ∂ l o s s ∂ x 2 = 400 \frac{\partial loss}{\partial x_1}=80, \frac{\partial loss}{\partial x_2}=400 ∂x1∂loss=80,∂x2∂loss=400
显然 x 1 x_1 x1将要移动的距离小于 x 2 x_2 x2将要移动的距离
但是实际上 x 1 x_1 x1离最优值 l o s s = 0 loss=0 loss=0 更远,差距是40, x 2 x_2 x2离最优值 l o s s = 0 loss=0 loss=0 近一些,距离是20。
因此SGD给出的结果并不理想。
Adagrad 优化算法思想:对于不同参数,设置不同的学习率
对于每个参数,初始化一个 累计平方梯度 r 为 0,然后每次将该参数的梯度平方求和累加到这个变量 r 上:
r ← r + g 2 r \leftarrow r + g^2 r←r+g2
然后,在更新这个参数的时候,学习率就变为:
η r + δ \frac{\eta}{\sqrt {r + \delta}} r+δη
梯度更新
x ← x − η r + δ ∗ g x \leftarrow x - \frac{\eta}{\sqrt {r + \delta}}*g x←x−r+δη∗g
其中, g g g为梯度; r r r为累积平方梯度(初始为0) ; η \eta η为学习率; δ \delta δ为小参数,避免分母为0,一般取值为 1 0 − 10 10^{-10} 10−10
这样,不同的参数由于梯度不同,他们对应的 r r r大小也就不同,所以学习率也就不同,这也就实现了自适应的学习率。
总结: Adagrad 的核心想法就是,如果一个参数的梯度一直都非常大,那么其对应的学习率就变小一点,防止震荡,而一个参数的梯度一直都非常小,那么这个参数的学习率就变大一点,使得其能够更快地更新,这就是Adagrad算法加快深层神经网络的训练速度的核心。
4、RMSProp
RMSProp:Root Mean Square Propagation 均方根传播
RMSProp 是在 adagrad 的基础上,进一步在学习率的方向上优化
累计平方梯度: r ← λ r + ( 1 − λ ) g 2 r \leftarrow \lambda r + (1 - \lambda)g^2 r←λr+(1−λ)g2
梯度更新: x ← x − η r + δ ∗ g x \leftarrow x - \frac{\eta}{\sqrt {r + \delta}}*g x←x−r+δη∗g
其中, g g g为梯度, r r r为累积平方梯度(初始为0) , λ \lambda λ为衰减系数, η \eta η为学习率, δ \delta δ为小参数(避免分母为0)
5、Adam
在Grandient Descent 的基础上,做了如下几个方面的改进:
1、梯度方面增加了momentum,使用累积梯度: m ← β 1 m + ( 1 − β 1 ) g m \leftarrow \beta_1 m+(1- \beta_1)g m←β1m+(1−β1)g
2、同 RMSProp 优化算法一样, 对学习率进行优化: v ← β 2 v + ( 1 − β 2 ) g 2 v \leftarrow \beta_2 v + (1 - \beta_2)g^2 v←β2v+(1−β2)g2
3、偏差纠正: m ^ t = m 1 − β 1 t \hat{m}_t = \frac{m}{1 - \beta_1^t } m^t=1−β1tm, v ^ = v 1 − β 2 t \hat{v} = \frac{v}{1 - \beta_2^t} v^=1−β2tv
再如上3点改进的基础上,梯度更新: θ ← θ − η m ^ + δ ∗ v ^ \theta \leftarrow \theta - \frac{\eta}{\sqrt {\hat{m} + \delta}}*\hat{v} θ←θ−m^+δη∗v^
为啥要偏差纠正
t t t 为更新次数,第 t t t次更新时, m t ← β 1 m t − 1 + ( 1 − β 1 ) g m_t \leftarrow \beta_1 m_{t-1}+(1- \beta_1)g mt←β1mt−1+(1−β1)g
当 t = 1 t=1 t=1时, m 1 = β 1 ∗ m 0 + ( 1 − β 1 ) ∗ g 1 m_1=\beta_1*m_0+(1-\beta_1)*g_1 m1=β1∗m0+(1−β1)∗g1,由于 m 0 m_0 m0的初始是0,且β接近1,因此 t t t较小时, m m m的值是偏向于0的
def adam(learning_rate, beta1, beta2, epsilon, var, grad, m, v, t):
m = beta1 * m + (1 - beta1) * grad
v = beta2 * v + (1 - beta2) * grad * grad
m_hat = m / (1 - beta1 ** t)
v_hat = v / (1 - beta2 ** t)
var = var - learning_rate * m_hat / (np.sqrt(v_hat) + epsilon)
return var, m, v
7、总结:
1、Gradient Descent
梯度更新: θ ← θ − η ∗ g \theta \leftarrow \theta - \eta*g θ←θ−η∗g
2、Gradient Descent with momentum
累计梯度更新: v ← α v + ( 1 − α ) g v \leftarrow \alpha v + (1 - \alpha)g v←αv+(1−α)g
梯度更新: θ ← θ − η ∗ v \theta \leftarrow \theta - \eta*v θ←θ−η∗v
α \alpha α 为动量参数, v v v累计梯度, η \eta η为学习率
3、Adagrad
累计平方梯度: r ← r + g 2 r \leftarrow r + g^2 r←r+g2
梯度更新: θ ← θ − η r + δ ∗ g \theta \leftarrow \theta - \frac{\eta}{\sqrt {r + \delta}}*g θ←θ−r+δη∗g
其中,g为梯度,r为累积平方梯度(初始为0) , η \eta η为学习率, δ \delta δ为小参数,避免分母为0
4、RMSProp
累计平方梯度: r ← λ r + ( 1 − λ ) g 2 r \leftarrow \lambda r + (1 - \lambda)g^2 r←λr+(1−λ)g2
梯度更新: θ ← θ − η r + δ ∗ g \theta \leftarrow \theta - \frac{\eta}{\sqrt {r + \delta}}*g θ←θ−r+δη∗g
其中,g为梯度,r为累积平方梯度(初始为0) , η \eta η为学习率, δ \delta δ为小参数,避免分母为0
5、Adam
累计梯度: v ← α v + ( 1 − α ) g v \leftarrow \alpha v + (1 - \alpha)g v←αv+(1−α)g
累计平方梯度: r ← λ r + ( 1 − λ ) g 2 r \leftarrow \lambda r + (1 - \lambda)g^2 r←λr+(1−λ)g2
偏差校正: v ^ = v 1 − α t \hat{v} = \frac{v}{1 - \alpha^t} v^=1−αtv
偏差校正: r ^ = r 1 − λ t \hat{r} = \frac{r}{1 - \lambda^t} r^=1−λtr
梯度更新: θ ← θ − η r ^ + δ ∗ v ^ \theta \leftarrow \theta - \frac{\eta}{\sqrt {\hat{r} + \delta}}*\hat{v} θ←θ−r^+δη∗v^
其中:
- g为梯度, α \alpha α为动量参数, v v v为累积梯度,
- λ \lambda λ为衰减系数,r为累积平方梯度(初始为0) ,
- η \eta η为学习率, δ \delta δ为小参数,避免分母为0
二、动态修改学习率参数
修改参数的方式可以通过修改参数optimizer.params_groups或新建optimizer。新建
optimizer比较简单,optimizer 十分轻量级,所以开销很小。但是新的优化器会初始化动量
等状态信息,这对于使用动量的优化器(momentum参数的sgd)可能会造成收敛中的震
荡。所以,这一般采用修改参数optimizer.params_groups
查看优化器的参数,有8个参数
print(optimizer.param_groups[0].keys())
输出:
dict_keys(['params', 'lr', 'momentum', 'dampening', 'weight_decay', 'nesterov', 'maximize', 'foreach'])
用pycharm查看:
我们可以通过动态修改参数 “lr” 的值,来实现动态参数的调整:
比如,我们想要每 5个 epoch 修改一次学习率,改为之前学习率的 0.1 倍
if epoch % 5 == 0:
optimizer.param_groups[0]['lr'] *= 0.1