绿色健康小清新

耐得住寂寞,守得住繁华

优化算法BGD、SGD、Momentum-SGD、Adagrad、AdaDelta、RMSProp、Adam算法及python实现

概述

一般用一个通用框架来表述优化算法
有如下定义:

  1. 待优化的参数 θ
  2. 目标函数 J ( θ )
  3. 学习率 α

有如下过程(每次迭代):

  1. 计算目标关于此时参数的梯度 ∇ θ ( J ( θ ) )
  2. 计算历史梯度的一阶动量和二阶动量
  3. 计算下降梯度 g
  4. 根据梯度进行迭代 θ = θ − g

优化算法目前有固 定 学 习 率 和自 适 应 学 习 率 两种,差别也就体现在过程的第1和第2步
固定学习率优化算法有:BGD、SGD、SGDM、NAG
自适应学习率优化算法有:AdaGrad、AdaDelta、Adam、Nadam


经验总结

现在用的最多的算法是SGD和Adam,两者各有好坏。SGD能够到达全局最优解,而且训练的最佳精度也要高于其他优化算法,但它对学习率的调节要求非常严格,而且容易停在鞍点;Adam很容易的跳过鞍点,而且不需要人为的干预学习率的调节,但是它很容易在局部最小值处震荡,存在在特殊的数据集下出现学习率突然上升,造成不收敛的情况。

算法优点缺点适用情况
BGD目标函数为凸函数时,可以找到全局最优值收敛速度慢,需要用到全部数据,内存消耗大不适用于大数据集,不能在线更新模型
SGD避免冗余数据的干扰,收敛速度加快,能够在线学习更新值的方差较大,收敛过程会产生波动,可能落入极小值(卡在鞍点),选择合适的学习率比较困难(需要不断减小学习率)适用于需要在线更新的模型,适用于大规模训练样本情况
Momentum能够在相关方向加速SGD,抑制振荡,从而加快收敛需要人工设定学习率适用于有可靠的初始化参数
Adagrad实现学习率的自动更改仍依赖于人工设置一个全局学习率,学习率设置过大,对梯度的调节太大。中后期,梯度接近于0,使得训练提前结束需要快速收敛,训练复杂网络时;适合处理稀疏梯度1
Adadelta不需要预设一个默认学习率,训练初中期,加速效果不错,很快,可以避免参数更新时两边单位不统一的问题在局部最小值附近震荡,可能不收敛需要快速收敛,训练复杂网络时
Adam速度快,对内存需求较小,为不同的参数计算不同的自适应学习率在局部最小值附近震荡,可能不收敛需要快速收敛,训练复杂网络时;善于处理稀疏梯度和处理非平稳目标的优点,也适用于大多非凸优化 - 适用于大数据集和高维空间
  • 对于稀疏数据,尽量使用学习率可自适应的优化方法,不用手动调节,而且最好采用默认值
  • SGD通常训练时间更长,但是在好的初始化和学习率调度方案的情况下,结果更可靠
  • 如果在意更快的收敛,并且需要训练较深较复杂的网络时,推荐使用学习率自适应的优化方法。
  • Adadelta,RMSprop,Adam是比较相近的算法,在相似的情况下表现差不多。
  • 在想使用带动量的RMSprop,或者Adam的地方,大多可以使用Nadam取得更好的效果


批量梯度下降BGD

每一次迭代都用到训练集的所有数据,用所有数据来计算梯度。
优劣点:迭代速度慢,全局最优解

迭代公式

θj=θjαmi=0m(hθ(xi)yi)xiθ_j = θ_j - \frac{α}{m}\sum_{i=0}^m(h_θ(x^i) - y^i)x^i

以y=x1+2*x2为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 批量梯度下降BGD
# 拟合函数为:y = theta * x
# 代价函数为:J = 1 / (2 * m) * ((theta * x) - y) * ((theta * x) - y).T;
# 梯度迭代为: theta = theta - alpha / m * (x * (theta * x - y).T);
import numpy as np


# 1、单元数据程序
# 以 y=x为例,所以正确的结果应该趋近于theta = 1
def bgd_single():
# 训练集, 单样本
x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

y = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

# 初始化
m = len(y)
theta = 0 # 参数
alpha = 0.01 # 学习率
threshold = 0.0001 # 停止迭代的错误阈值
iterations = 1500 # 迭代次数
error = 0 # 初始错误为0

# 迭代开始
for i in range(iterations):
error = 1 / (2 * m) * np.dot(((theta * x) - y).T, ((theta * x) - y))
# 迭代停止
if abs(error) <= threshold:
break

theta -= alpha / m * (np.dot(x.T, (theta * x - y)))

print('单变量:', '迭代次数: %d' % (i + 1), 'theta: %f' % theta,
'error1: %f' % error)


# 2、多元数据程序
# 以 y=x1+2*x2为例,所以正确的结果应该趋近于theta = [1,2]


def bgd_multi():
# 训练集,每个样本有2个分量
x = np.array([(1, 1), (1, 2), (2, 2), (3, 1), (1, 3), (2, 4), (2, 3), (3,
3)])
y = np.array([3, 5, 6, 5, 7, 10, 8, 9])

# 初始化
m, dim = x.shape
theta = np.zeros(dim) # 参数
alpha = 0.01 # 学习率
threshold = 0.0001 # 停止迭代的错误阈值
iterations = 1500 # 迭代次数
error = 0 # 初始错误为0

# 迭代开始
for i in range(iterations):
error = 1 / (2 * m) * np.dot((np.dot(x, theta) - y).T,
(np.dot(x, theta) - y))
# 迭代停止
if abs(error) <= threshold:
break

theta -= alpha / m * (np.dot(x.T, (np.dot(x, theta) - y)))

print('多元变量:', '迭代次数:%d' % (i + 1), 'theta:', theta, 'error:%f' % error)


if __name__ == '__main__':
bgd_single()
bgd_multi()

输出

1
2
单变量: 迭代次数: 14 theta: 0.998200 error1: 0.000062
多元变量: 迭代次数:454 theta: [1.01339617 1.98981777] error:0.000100

随机梯度下降SGD

因为BGD的迭代速度在大数据量的情况下会变得非常慢,所以提出了随机梯度下降算法,即每一次迭代只使用一个样本,根据这一个样本来计算梯度。
优劣点:迭代速度快,不是全局最优解

迭代公式

θj=θjα(hθ(xi)yi)xiθ_j = θ_j - α(h_θ(x^i) - y^i)x^i

以y=x1+2*x2为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 随机梯度下降SGD
# 以 y=x1+2*x2为例

import numpy as np


# 多元数据
def sgd():
# 训练集,每个样本有2个分量
x = np.array([(1, 1), (1, 2), (2, 2), (3, 1), (1, 3), (2, 4), (2, 3), (3, 3)])
y = np.array([3, 5, 6, 5, 7, 10, 8, 9])

# 初始化
m, dim = x.shape
theta = np.zeros(dim) # 参数
alpha = 0.01 # 学习率
threshold = 0.0001 # 停止迭代的错误阈值
iterations = 1500 # 迭代次数
error = 0 # 初始错误为0

# 迭代开始
for i in range(iterations):

error = 1 / (2 * m) * np.dot((np.dot(x, theta) - y).T, (np.dot(x, theta) - y))
# 迭代停止
if abs(error) <= threshold:
break

j = np.random.randint(0, m)

theta -= alpha * (x[j] * (np.dot(x[j], theta) - y[j]))

print('迭代次数:%d' % (i + 1), 'theta:', theta, 'error:%f' % error)


if __name__ == '__main__':
sgd()

输出:

1
迭代次数:420 theta: [1.01288885 1.99055896] error:0.000090

带动量的随机梯度下降Momentum-SGD

因为SGD只依赖于当前迭代的梯度,十分不稳定,加一个“动量”的话,相当于有了一个惯性在里面,梯度方向不仅与这次的迭代有关,还与之前一次的迭代结果有关。“当前一次效果好的话,就加快步伐;当前一次效果不好的话,就减慢步伐”;而且在局部最优值处,没有梯度但因为还存在一个动量,可以跳出局部最优值。

迭代公式

g=momentumgα(hθ(xi)yi)xig =momentum∗g - α(h_θ(x^i) - y^i)x^i

θj=θjgθ_j = θ_j - g

以y=x1+2*x2为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 随机梯度下降SGD
# 以 y=x1+2*x2为例

import numpy as np


# 多元数据
def sgd():
# 训练集,每个样本有2个分量
x = np.array([(1, 1), (1, 2), (2, 2), (3, 1), (1, 3), (2, 4), (2, 3), (3, 3)])
y = np.array([3, 5, 6, 5, 7, 10, 8, 9])

# 初始化
m, dim = x.shape
theta = np.zeros(dim) # 参数
alpha = 0.01 # 学习率
threshold = 0.0001 # 停止迭代的错误阈值
iterations = 1500 # 迭代次数
error = 0 # 初始错误为0

# 迭代开始
for i in range(iterations):

error = 1 / (2 * m) * np.dot((np.dot(x, theta) - y).T, (np.dot(x, theta) - y))
# 迭代停止
if abs(error) <= threshold:
break

j = np.random.randint(0, m)

theta -= alpha * (x[j] * (np.dot(x[j], theta) - y[j]))

print('迭代次数:%d' % (i + 1), 'theta:', theta, 'error:%f' % error)


if __name__ == '__main__':
sgd()

输出

1
迭代次数:497 theta: [1.0135894  1.99015619] error:0.000100

Adagrad

对学习率加了一个约束,但依赖于一个全局学习率。
根据迭代公式,学习率 α 乘上了一个系数,这个系数与梯度有关,且当梯度大的时候,这个系数小;当梯度小的时候,这个系数大。这样的实际表现就是在面对频繁出现的特征时,使用小学习率;在面对不频繁出现的特征时,使用大学习率。这就使得这一算法非常适合处理稀疏数据。比如在训练单词嵌入时,常用单词和不常用单词分别用小学习率和大学习率能达到更好的效果。
问题在于公式中的vtv_t会在后期变得非常大,使得矫正后的学习率趋近0,过早得结束迭代。

迭代公式

vt=vt1+gt2v_t = v_{t-1}+g_t^2

θ=θαgtvt+eθ = θ - α*\frac{g_t}{\sqrt{v_t+e}}


RMSProp

Adagrad会累加之前所有的梯度平方,而RMSProp只累加固定大小的项,并且也不直接存储这些项,仅仅是近似计算对应的平均值。不依赖全局学习率。

迭代公式

vt=βvt1+(1β)gt2v_t = βv_{t-1}+(1-β)g_t^2

θ=θαgtvt+eθ = θ - α*\frac{g_t}{\sqrt{v_t+e}}


AdaDelta

除了PMSProp算法以外,另一个常用优化算法AdaDelta算法也针对AdaGrad算法在迭代后期可能较难找到有用解的问题做了改进。有意思的是,AdaDelta算法没有学习率这一超参数。

迭代公式:

vt=βvt1+(1β)gt2v_t = βv_{t-1}+(1-β)g_t^2

gt=xt1+evt+egtg_t^{'} = \sqrt{\frac{△x_{t-1}+e}{v_t+e}}*g_t

xt=βxt1+(1β)gt2△x_t = β△x_{t-1}+(1-β)g_t^{'2}

θ=θxt1+evt+egtθ = θ - \sqrt{\frac{△x_{t-1}+e}{v_t+e}}*g_t

其中:xt△x_t的初始值也为0


如果不考虑e,AdaDelta和PMSProp算法的不同之处在于使用xt1α\sqrt{△x_{t-1}}来代替超参数α


Adam

Adam是一种自适应学习率的方法,在Momentum一阶矩估计的基础上加入了二阶矩估计,也是在Adadelta的基础上加了一阶矩。它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。为了解决Adagrad算法出现学习率趋向0的问题,还加入了偏置校正,这样每一次迭代学习率都有个确定范围,使得参数比较平稳。
优劣点:迭代速度快,效果也好,但可能不收敛。

根据下面的paper来快速描述一下Adam的algorithm:

  • 先初始化m0=0m_0=0m0m_0就是Momentum中,前一个时间点的movement

    再初始化v0=0v_0=0v0v_0就是RMSProp里计算gradient的root mean square的σ\sigma

    最后初始化t=0t=0,t用来表示时间点

  • 先算出gradient gtg_t

    gt=θft(θt1)g_t=\nabla _{\theta}f_t(\theta_{t-1})

  • 再根据过去要走的方向mt1m_{t-1}和gradient gtg_t,算出现在要走的方向 mtm_t——Momentum

    mt=β1mt1+(1β1)gtm_t=\beta_1 m_{t-1}+(1-\beta_1) g_t

  • 然后根据前一个时间点的vt1v_{t-1}和gradient gtg_t的平方,算一下放在分母的vtv_t——RMSProp

    vt=β2vt1+(1β2)gt2v_t=\beta_2 v_{t-1}+(1-\beta_2) g_t^2

  • 接下来做了一个原来RMSProp和Momentum里没有的东西,就是bias correction,它使mtm_tvtv_t都除上一个值,这个值本来比较小,后来会越来越接近于1 (原理详见paper)

    m^t=mt1β1tv^t=vt1β2t\hat{m}_t=\frac{m_t}{1-\beta_1^t} \\ \hat{v}_t=\frac{v_t}{1-\beta_2^t}

  • 最后做update,把Momentum建议你的方向mt^\hat{m_t}乘上learning rate α\alpha,再除掉RMSProp normalize后建议的learning rate分母,然后得到update的方向

    θt=θt1αm^tv^t+ϵ\theta_t=\theta_{t-1}-\frac{\alpha \cdot \hat{m}_t}{\sqrt{\hat{v}_t}+\epsilon}

以y=x1+2*x2为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# ADAM
# 以 y=x1+2*x2为例
import math
import numpy as np


def adam():
# 训练集,每个样本有三个分量
x = np.array([(1, 1), (1, 2), (2, 2), (3, 1), (1, 3), (2, 4), (2, 3), (3,
3)])
y = np.array([3, 5, 6, 5, 7, 10, 8, 9])

# 初始化
m, dim = x.shape
theta = np.zeros(dim) # 参数
alpha = 0.01 # 学习率
momentum = 0.1 # 冲量
threshold = 0.0001 # 停止迭代的错误阈值
iterations = 3000 # 迭代次数
error = 0 # 初始错误为0

b1 = 0.9 # 算法作者建议的默认值
b2 = 0.999 # 算法作者建议的默认值
e = 0.00000001 #算法作者建议的默认值
mt = np.zeros(dim)
vt = np.zeros(dim)

for i in range(iterations):
j = i % m
error = 1 / (2 * m) * np.dot((np.dot(x, theta) - y).T,
(np.dot(x, theta) - y))
if abs(error) <= threshold:
break

gradient = x[j] * (np.dot(x[j], theta) - y[j])
mt = b1 * mt + (1 - b1) * gradient
vt = b2 * vt + (1 - b2) * (gradient**2)
mtt = mt / (1 - (b1**(i + 1)))
vtt = vt / (1 - (b2**(i + 1)))
vtt_sqrt = np.array([math.sqrt(vtt[0]),
math.sqrt(vtt[1])]) # 因为只能对标量进行开方
theta = theta - alpha * mtt / (vtt_sqrt + e)

print('迭代次数:%d' % (i + 1), 'theta:', theta, 'error:%f' % error)


if __name__ == '__main__':
adam()

输出:

1
迭代次数:1987 theta: [1.01326533 1.98965863] error:0.000100
-------------本文结束感谢您的阅读-------------
六经蕴籍胸中久,一剑十年磨在手

欢迎关注我的其它发布渠道