1. 梯度下降法(Gradient Descent)
梯度下降法的计算过程就是沿梯度下降的方向求解极小值,也可以沿梯度上升方向求解最大值。 假设模型参数为 θ \theta θ,损失函数为 J ( θ ) J(\theta) J(θ),损失函数关于参数的偏导数,也就是梯度为 ▽ θ J ( θ ) \triangledown _\theta J(\theta) ▽θJ(θ),学习率为 α \alpha α ,则使用梯度下降法更新参数为:
梯度下降法目前主要分为三种方法,区别在于每次参数更新时计算的样本数据量不同:批量梯度下降法(BGD, Batch Gradient Descent),随机梯度下降法(SGD, Stochastic Gradient Descent)及小批量梯度下降法(Mini-batch Gradient Descent)。
批量梯度下降法BGD
假设训练样本总数为n,样本为 { ( x 1 , y 1 ) , ⋯ , ( x n , y n ) } \{(x^{1},y^{1}),\cdots ,(x^{n},y^{n})\} {(x1,y1),⋯,(xn,yn)},模型参数为 θ \theta θ,损失函数为 J ( θ ) J(\theta) J(θ),在第i对样本 ( x i , y i ) (x^{i},y^{i}) (xi,yi)上损失函数关于参数的梯度为 ▽ θ J i ( θ , x i , y i ) \triangledown _\theta J_i(\theta,x^{i},y^{i}) ▽θJi(θ,xi,yi), 学习率为 [公式],则使用BGD更新参数为:
由上式可以看出,每进行一次参数更新,需要计算整个数据样本集,因此导致批量梯度下降法的速度会比较慢,尤其是数据集非常大的情况下,收敛速度就会非常慢,但是由于每次的下降方向为总体平均梯度,它得到的会是一个全局最优解。
随机梯度下降法SGD
随机梯度下降法,不像BGD每一次参数更新,需要计算整个数据样本集的梯度,而是每次参数更新时,仅仅选取一个样本 ( x i , y i ) (x^{i},y^{i}) (xi,yi) 计算其梯度,参数更新公式为:
可以看到BGD和SGD是两个极端,SGD由于每次参数更新仅仅需要计算一个样本的梯度,训练速度很快,即使在样本量很大的情况下,可能只需要其中一部分样本就能迭代到最优解,由于每次迭代并不是都向着整体最优化方向,导致梯度下降的波动非常大,更容易从一个局部最优跳到另一个局部最优,准确度下降。
小批量梯度下降法
小批量梯度下降法就是结合BGD和SGD的折中,对于含有n个训练样本的数据集,每次参数更新,选择一个大小为m (m<n)的mini-batch数据样本计算其梯度,其参数更新公式如下:
小批量梯度下降法即保证了训练的速度,又能保证最后收敛的准确率,目前的SGD默认是小批量梯度下降算法。
SGD缺点:
(1) 选择合适的learning rate比较困难 ,学习率太低会收敛缓慢,学习率过高会使收敛时的波动过大
(2) 所有参数都是用同样的learning rate
(3) SGD容易收敛到局部最优,并且在某些情况下可能被困在鞍点
import numpy as np
class SGD:
"""随机梯度下降法(Stochastic Gradient Descent)"""
def __init__(self, lr=0.01):
self.lr = lr
def update(self, params, grads):
for key in params.keys():
params[key] -= self.lr * grads[key]
2. 动量优化法
动量优化方法引入物理学中的动量思想,加速梯度下降,有Momentum和Nesterov两种算法。当我们将一个小球从山上滚下来,没有阻力时,它的动量会越来越大,但是如果遇到了阻力,速度就会变小,动量优化法就是借鉴此思想,使得在梯度方向不变的维度上,参数更新变快,梯度有所改变时,更新参数变慢,这样就能够加快收敛并且减少动荡。
Momentum
momentum算法思想:参数更新时在一定程度上保留之前更新的方向,同时又利用当前batch的梯度微调最终的更新方向,简言之就是通过积累之前的动量来加速当前的梯度。假设 m t m_t mt表示 t t t时刻的动量, μ \mu μ 表示动量因子,通常取值0.9或者近似值,在SGD的基础上增加动量,则参数更新公式如下:
在梯度方向改变时,momentum能够降低参数更新速度,从而减少震荡;在梯度方向相同时,momentum可以加速参数更新, 从而加速收敛。总而言之,momentum能够加速SGD收敛,抑制震荡。
class Momentum:
"""Momentum SGD"""
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update(self, params, grads):
if self.v is None:
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]
params[key] += self.v[key]
NAG(Nesterov accelerated gradient)
momentum保留了上一时刻的梯度 ▽ θ J ( θ ) \triangledown _\theta J(\theta) ▽θJ(θ) ,对其没有进行任何改变,NAG是momentum的改进,在梯度更新时做一个矫正,具体做法就是在当前的梯度 ▽ θ J ( θ ) \triangledown _\theta J(\theta) ▽θJ(θ)上添加上一时刻的动量 μ ⋅ m t \mu \cdot m_t μ⋅mt,梯度改变为 ▽ θ J ( θ − μ ⋅ m t ) \triangledown _\theta J(\theta-\mu \cdot m_t) ▽θJ(θ−μ⋅mt) 。
加上nesterov项后,梯度在大的跳跃后,进行计算对当前梯度进行校正。
class Nesterov:
"""Nesterov's Accelerated Gradient (http://arxiv.org/abs/1212.0901)"""
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update(self, params, grads):
if self.v is None:
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
self.v[key] *= self.momentum
self.v[key] -= self.lr * grads[key]
params[key] += self.momentum * self.momentum * self.v[key]
params[key] -= (1 + self.momentum) * self.lr * grads[key]
3. 自适应学习率优化算法
在机器学习中,学习率是一个非常重要的超参数,但是学习率是非常难确定的,虽然可以通过多次训练来确定合适的学习率,但是一般也不太确定多少次训练能够得到最优的学习率,玄学事件,对人为的经验要求比较高,所以是否存在一些策略自适应地调节学习率的大小,从而提高训练速度。
为了更有效地训练模型,比较合理的一种做法是,对每个参与训练的参数设置不同的学习率,在整个学习过程中通过一些算法自动适应这些参数的学习率。目前的自适应学习率优化算法主要有:AdaGrad算法,RMSProp算法,Adam算法以及AdaDelta算法。
AdaGrad
首先设全局学习率为
α
\alpha
α,一个为了数值稳定而创建的小常量
δ
\delta
δ,建议默认取值
1
0
−
7
10^{-7}
10−7,目的是为了让分母不为0.一个梯度累计变量
r
r
r(初始化
r
r
r为0).算法的迭代更新过程如下:
(1)从训练数据集中取出包含m个样本的小批量数据
{
(
x
1
,
y
1
)
,
⋯
⋯
,
(
x
m
,
y
m
)
}
\{(x^{1},y^{1}),\cdots \cdots ,(x^{m},y^{m})\}
{(x1,y1),⋯⋯,(xm,ym)};
(2)在小批量数据的基础上计算梯度
g
←
1
m
∑
▽
θ
J
i
(
θ
,
x
i
,
y
i
)
\begin{aligned} g\leftarrow \frac{1}{m}\sum \triangledown _\theta J_i(\theta,x^i,y^i) \end{aligned}
g←m1∑▽θJi(θ,xi,yi)
(3)累计平方梯度,并刷新
r
r
r,公式为
r
←
r
+
g
⊙
g
r\leftarrow r+g\odot g
r←r+g⊙g
(4)计算参数的更新量
△
θ
=
−
α
δ
+
r
⊙
g
\begin{aligned} \triangle \theta =-\frac{\alpha }{\delta +\sqrt{r}}\odot g \end{aligned}
△θ=−δ+rα⊙g
(5)更新参数
θ
=
θ
+
△
θ
\theta = \theta+\triangle \theta
θ=θ+△θ
在该算法中,每个参数的更新量 △ θ \triangle \theta △θ都反比于其所有梯度历史平均值总和的平方根。通过这样的方式,可达到独立适应所有模型参数的学习率的目的。这样做的效果是,在参数空间中,倾斜率不大的区域也能沿着梯度方向有一个较大的参数更新。
在前期,梯度累计平方和比较小,也就是r相对较小,则约束项较大,这样就能够放大梯度, 参数更新量变大; 随着迭代次数增多,梯度累计平方和也越来越大,即r也相对较大,则约束项变小,这样能够缩小梯度,参数更新量变小。
缺点:
仍需要手工设置一个全局学习率
δ
\delta
δ , 如果
δ
\delta
δ 设置过大的话,会使regularizer过于敏感,对梯度的调节太大;
中后期,分母上梯度累加的平方和会越来越大,使得参数更新量趋近于0,使得训练提前结束,无法学习。
class AdaGrad:
"""AdaGrad"""
def __init__(self, lr=0.01):
self.lr = lr
self.h = None
def update(self, params, grads):
if self.h is None:
self.h = {}
for key, val in params.items():
self.h[key] = np.zeros_like(val)
for key in params.keys():
self.h[key] += grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
RMSProp
RMSProp算法采用了指数衰减平均的方式淡化遥远过去的历史对当前步骤参数更新量的影响。相比于AdaGrad,RMSProp算法引入了一个新的参数 ρ \rho ρ,用以控制历史梯度值的衰减速率。
算法的迭代更新过程如下:
(1)从训练数据集中取出包含m个样本的小批量数据
{
(
x
1
,
y
1
)
,
⋯
⋯
,
(
x
m
,
y
m
)
}
\{(x^{1},y^{1}),\cdots \cdots ,(x^{m},y^{m})\}
{(x1,y1),⋯⋯,(xm,ym)};
(2)在小批量数据的基础上计算梯度
g
←
1
m
∑
▽
θ
J
i
(
θ
,
x
i
,
y
i
)
\begin{aligned} g\leftarrow \frac{1}{m}\sum \triangledown _\theta J_i(\theta,x^i,y^i) \end{aligned}
g←m1∑▽θJi(θ,xi,yi)
(3)累计平方梯度,并刷新
r
r
r,注意这里使用了衰减率
ρ
\rho
ρ,公式为
r
←
ρ
r
+
(
1
−
ρ
)
g
⊙
g
\begin{aligned} r\leftarrow \rho r+(1-\rho)g\odot g \end{aligned}
r←ρr+(1−ρ)g⊙g
(4)计算参数的更新量
△
θ
=
−
α
δ
+
r
⊙
g
\begin{aligned} \triangle \theta =-\frac{\alpha }{\sqrt{\delta + r}}\odot g \end{aligned}
△θ=−δ+rα⊙g
(5)更新参数
θ
←
θ
+
△
θ
\theta \leftarrow \theta+\triangle \theta
θ←θ+△θ
class RMSprop:
"""RMSprop"""
def __init__(self, lr=0.01, decay_rate = 0.99):
self.lr = lr
self.decay_rate = decay_rate
self.h = None
def update(self, params, grads):
if self.h is None:
self.h = {}
for key, val in params.items():
self.h[key] = np.zeros_like(val)
for key in params.keys():
self.h[key] *= self.decay_rate
self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
Adam
Adam中动量直接并入了梯度一阶矩(指数加权)的估计。其次,相比于缺少修正因子导致二阶矩估计可能在训练初期具有很高偏置的RMSProp,Adam包括偏置修正,修正从原点初始化的一阶矩(动量项)和(非中心的)二阶矩估计。
首先设全局学习率为 α \alpha α,矩估计的指数衰减速率为 ρ 1 \rho_1 ρ1和 ρ 2 \rho_2 ρ2(建议分别默认为0.9和0.99),一个为了数值稳定而创建的小常量 δ \delta δ,建议默认取值 1 0 − 7 10^{-7} 10−7,目的是为了让分母不为0.初始值为0的一阶和二阶矩变量 s , r s,r s,r,以及一个时间步计数 t t t(初始化t为0).
算法的迭代更新过程如下:
(1)从训练数据集中取出包含m个样本的小批量数据
{
(
x
1
,
y
1
)
,
⋯
⋯
,
(
x
m
,
y
m
)
}
\{(x^{1},y^{1}),\cdots \cdots ,(x^{m},y^{m})\}
{(x1,y1),⋯⋯,(xm,ym)};
(2)在小批量数据的基础上计算梯度
g
←
1
m
∑
▽
θ
J
i
(
θ
,
x
i
,
y
i
)
\begin{aligned} g\leftarrow \frac{1}{m}\sum \triangledown _\theta J_i(\theta,x^i,y^i) \end{aligned}
g←m1∑▽θJi(θ,xi,yi)
(3)刷新时间步:
t
←
t
+
1
t \leftarrow t+1
t←t+1
(4)更新一阶有偏矩估计:
s
←
ρ
1
s
+
(
1
−
ρ
1
)
g
\begin{aligned} s \leftarrow \rho_1 s + (1-\rho_1)g \end{aligned}
s←ρ1s+(1−ρ1)g
(5)更新二阶有偏矩估计:
r
←
ρ
2
r
+
(
1
−
ρ
2
)
g
⊙
g
\begin{aligned} r \leftarrow \rho_2 r + (1-\rho_2)g\odot g \end{aligned}
r←ρ2r+(1−ρ2)g⊙g
(6)对一阶矩的偏差进行修正:
s
^
←
s
1
−
ρ
1
t
\begin{aligned} \hat{s}\leftarrow \frac{s}{1-\rho_1^t} \end{aligned}
s^←1−ρ1ts
(7)对二阶矩的偏差进行修正:
r
^
←
r
1
−
ρ
2
t
\begin{aligned} \hat{r}\leftarrow \frac{r}{1-\rho_2^t} \end{aligned}
r^←1−ρ2tr
(8)计算参数的更新量
△
θ
=
−
α
s
^
r
^
+
δ
\begin{aligned} \triangle \theta = -\alpha\frac{ \hat{s}} {\sqrt{\hat{r}+\delta }} \end{aligned}
△θ=−αr^+δs^
(9)更新参数
θ
←
θ
+
△
θ
\begin{aligned} \theta \leftarrow \theta+\triangle \theta \end{aligned}
θ←θ+△θ
class Adam:
"""Adam (http://arxiv.org/abs/1412.6980v8)"""
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.iter = 0
self.m = None
self.v = None
def update(self, params, grads):
if self.m is None:
self.m, self.v = {}, {}
for key, val in params.items():
self.m[key] = np.zeros_like(val)
self.v[key] = np.zeros_like(val)
self.iter += 1
lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)
for key in params.keys():
#self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
#self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
#unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
#unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
#params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)
参考文章:https://zhuanlan.zhihu.com/p/55150256