目录
1. 编程实现图6-1,并观察特征
2. 观察梯度方向
3. 编写代码实现算法,并可视化轨迹
4. 分析上图,说明原理
(1)为什么SGD会走“之字形”?其它算法为什么会比较平滑?
(2)Momentum、AdaGrad对SGD的改进体现在哪里?速度?方向?在图上有哪些体现?
(3)仅从轨迹来看,Adam似乎不如AdaGrad效果好,是这样么?
5. 总结SGD、Momentum、AdaGrad、Adam的优缺点
6. Adam这么好,SGD是不是就用不到了?
总结
import numpy as np
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# https://blog.csdn.net/weixin_39228381/article/details/108511882
def func(x, y):
return x * x / 20 + y * y
def paint_loss_func():
x = np.linspace(-50, 50, 100) # x的绘制范围是-50到50,从改区间均匀取100个数
y = np.linspace(-50, 50, 100) # y的绘制范围是-50到50,从改区间均匀取100个数
X, Y = np.meshgrid(x, y)
Z = func(X, Y)
fig = plt.figure() # figsize=(10, 10))
ax = Axes3D(fig)
plt.xlabel('x')
plt.ylabel('y')
ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap='rainbow')
plt.show()
paint_loss_func()
这个函数是一条弧线,两端高,中间低,有全局最小值。
这个梯度的特征是,y轴方向上大,x轴方向上小。换句话说, 就是y轴方向的坡度大,而x轴方向的坡度小。
代码:
# coding: utf-8
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
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]
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]
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]
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)
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)
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] += (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)
def f(x, y):
return x ** 2 / 20.0 + y ** 2
def df(x, y):
return x / 10.0, 2.0 * y
init_pos = (-7.0, 2.0)
params = {}
params['x'], params['y'] = init_pos[0], init_pos[1]
grads = {}
grads['x'], grads['y'] = 0, 0
optimizers = OrderedDict()
optimizers["SGD"] = SGD(lr=0.95)
optimizers["Momentum"] = Momentum(lr=0.1)
optimizers["AdaGrad"] = AdaGrad(lr=1.5)
optimizers["Adam"] = Adam(lr=0.3)
idx = 1
for key in optimizers:
optimizer = optimizers[key]
x_history = []
y_history = []
params['x'], params['y'] = init_pos[0], init_pos[1]
for i in range(30):
x_history.append(params['x'])
y_history.append(params['y'])
grads['x'], grads['y'] = df(params['x'], params['y'])
optimizer.update(params, grads)
x = np.arange(-10, 10, 0.01)
y = np.arange(-5, 5, 0.01)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)
# for simple contour line
mask = Z > 7
Z[mask] = 0
# plot
plt.subplot(2, 2, idx)
idx += 1
plt.plot(x_history, y_history, 'o-', color="red")
plt.contour(X, Y, Z) # 绘制等高线
plt.ylim(-10, 10)
plt.xlim(-10, 10)
plt.plot(0, 0, '+')
plt.title(key)
plt.xlabel("x")
plt.ylabel("y")
plt.subplots_adjust(wspace=0, hspace=0) # 调整子图间距
plt.show()
可视化:
SGD(Stochastic Gradient Descent,随机梯度下降)有缺陷,呈现之字形,是因为图像的变化并不均匀,所以y方向变化很大时,x方向变化很小,只能迂回往复地寻找,效率很低。也就是说,SGD 的缺点是,如果函数的形状非均向(anisotropic),比如呈延伸状,搜索的路径就会非常低效。因此,我们需要比单纯朝梯度方向前进的 SGD 更聪明的方法。
SGD 低效的根本原因是,梯度的方向并没有指向最小值的方向。为了改正SGD的缺点,引入了Momentum、AdaGrad、Adam这 3 种方法来取代SGD。
最简单的 BGD 以整个训练集的梯度和作为更新方向,缺点是速度慢,一个 epoch 只能更新一次模型参数。
SGD 就是用来解决这个问题的,以每个样本的梯度作为更新方向,更新次数更频繁。但有两个缺点:
Momentum 算法做出的改进主要是用来解决第一个问题。Momentum 算法则设置了动量(momentum)的概念,可以理解为惯性,使当前梯度小幅影响优化方向,而不是完全决定优化方向。也起到了减小波动的效果。体现在图上就是图像变得平滑。
AdaGrad 算法做出的改进用来解决第二个问题,其记录了每个参数的历史梯度平方和(平方是 element-wise 的),并以此表征每个参数变化的剧烈程度,继而自适应地为变化剧烈的参数选择更小的学习率。体现在图上就是图像变得平滑。
从轨迹来看是这样。原因:
一:可能不收敛
SGD没有用到二阶动量,因此学习率是恒定的(实际使用过程中会采用学习率衰减策略,因此学习率递减)。AdaGrad的二阶动量不断累积,单调递增,因此学习率是单调递减的。因此,这两类算法会使得学习率不断递减,最终收敛到0,模型也得以收敛。
但AdaDelta和Adam则不然。二阶动量是固定时间窗口内的累积,随着时间窗口的变化,遇到的数据可能发生巨变,使得可能会时大时小,不是单调变化。这就可能在训练后期引起学习率的震荡,导致模型无法收敛。
二:可能错过全局最优解
深度神经网络往往包含大量的参数,在这样一个维度极高的空间内,非凸的目标函数往往起起伏伏,拥有无数个高地和洼地。有的是高峰,通过引入动量可能很容易越过;但有些是高原,可能探索很多次都出不来,于是停止了训练。
近期Arxiv上的两篇文章谈到这个问题。
第一篇就是前文提到的吐槽Adam最狠的 The Marginal Value of Adaptive Gradient Methods in Machine Learning 。文中说到,同样的一个优化问题,不同的优化算法可能会找到不同的答案,但自适应学习率的算法往往找到非常差的答案。他们通过一个特定的数据例子说明,自适应学习率算法可能会对前期出现的特征过拟合,后期才出现的特征很难纠正前期的拟合效果。
另外一篇是 Improving Generalization Performance by Switching from Adam to SGD,进行了实验验证。他们CIFAR-10数据集上进行测试,Adam的收敛速度比SGD要快,但最终收敛的结果并没有SGD好。他们进一步实验发现,主要是后期Adam的学习率太低,影响了有效的收敛。他们试着对Adam的学习率的下界进行控制,发现效果好了很多。
SGD
优点:
缺点:
Momentum----------为SGD进行了提速(对梯度进行调整)
优点:对方向一致的参数能够加速学习,对梯度改变方向的参数能够减少其更新,因此就是momentum能够在相关方向上加速学习,抑制振荡,从而加速收敛。
缺点:比较难学习一个较好的学习率。
AdaGrad--------对学习率进行了约束
adagrad就是把每一个参数的每一次迭代的梯度取平方累加后再开方,用全局学习率除以这个数,作为学习率的动态更新。
优点:不需要对每个学习率手工地调节,学习率可以自适应的减小。
缺点:
1.从训练开始就积累梯度方差会导致有效学习率过早和过量的减小。
2.只能解决凸问题,当应用于非凸函数训练神经网络时,学习可能会到达一个局部是凸碗的区域。
3.仍依赖于人工设置一个全局学习率,学习率设置过大,对梯度的调节太大。中后期,梯度接近于0,使得训练提前结束。
Adam
简单来讲 Adam 算法就是综合了 Momentum 和 RMSProp 的一种算法,其既记录了历史梯度均值作为动量,又考虑了历史梯度平方和实现各个参数的学习率自适应调整,解决了 SGD 的上述两个问题。
优点:对内存需求较小,为不同的参数计算不同的自适应学习率
缺点:
1、可能不收敛
2、可能错过全局最优解
并不是。
在模型设计实验过程中,要快速验证新模型的效果,可以先用Adam进行快速实验优化;在模型上线或者结果发布前,可以用精调的SGD进行模型的极致优化。
先用Adam进行快速下降,而后再换到SGD进行充分的调优。
制定一个合适的学习率衰减策略。可以使用定期衰减策略,比如每过多少个epoch就衰减一次;或者利用精度或者AUC等性能指标来监控,当测试集上的指标不变或者下跌时,就降低学习率。
这次写作业的过程中看了很多好文章,对课上老师的内容有了更深的印象,对SGD、Momentum、AdaGrad、Adam这些优化算法有了更深的理解。总之,收获很多。
参考:
深度学习入门之SGD随机梯度下降法_赵孝正的博客-CSDN博客_sgd 随机梯度下降
机器学习中几种优化算法的比较(SGD、Momentum、RMSProp、Adam) - Glowming - 博客园
深度学习中的优化算法 |SGD|momentum|NAG|AdaGrad|RMSprop|AdaDelta|Adam - 知乎
adam算法效果差原因_深度学习优化器-Adam两宗罪_weixin_39536728的博客-CSDN博客
梯度下降:BGD、SGD、mini-batch GD介绍及其优缺点_Activewaste的博客-CSDN博客_bgd梯度下降
优化算法选择:SGD、SGDM、NAG、Adam、AdaGrad、RMSProp、Nadam_UtopXExistential的博客-CSDN博客_sgdm算法