IT圈老男孩1 发表于 2022-3-25 21:49

7个优化器原理吐血总结+为什么有时候Adam不如SGD

SGD

输入https://www.zhihu.com/equation?tex=X%5Cin+%5Cmathrm%7BR%7D%5E%7BN%5Ctimes+m%7D, https://www.zhihu.com/equation?tex=N为样本个数 https://www.zhihu.com/equation?tex=m为特征数,对于一个样本https://www.zhihu.com/equation?tex=x 分类标签为https://www.zhihu.com/equation?tex=y,模型的输出:

https://www.zhihu.com/equation?tex=f%5Cleft%28+x+%5Cright%29+%3DW_1x_1%2BW_2x_2%2B%5Ccdots+W_mx_m+%5C%5C
损失函数为:

https://www.zhihu.com/equation?tex=J%5Cleft%28+W+%5Cright%29+%3D%5Cfrac%7B1%7D%7B2%7D%5Csum_%7Bi%3D1%7D%5EN%7B%5Cleft%28+f%5Cleft%28+x+%5Cright%29+-y+%5Cright%29+%5E2%7D+%5C%5Chttps://www.zhihu.com/equation?tex=W_j%5Cgets+W_j-%5Ceta+%5Cfrac%7B%5Cpartial+L%7D%7B%5Cpartial+W_j%7D+%5C%5Chttps://www.zhihu.com/equation?tex=%5Cbegin%7Baligned%7D+%09%5Cfrac%7B%5Cpartial+L%7D%7B%5Cpartial+W_j%7D%26%3D%5Cfrac%7B%5Cpartial%7D%7B%5Cpartial+W_j%7D%5Cfrac%7B1%7D%7B2%7D%5Cleft%28+f%5Cleft%28+x+%5Cright%29+-y+%5Cright%29+%5E2%5C%5C+%09%26%3D2%5Ccdot+%5Cfrac%7B1%7D%7B2%7D%5Cleft%28+f%5Cleft%28+x+%5Cright%29+-y+%5Cright%29+%5Cfrac%7B%5Cpartial%7D%7B%5Cpartial+W_j%7D%5Cleft%28+f%5Cleft%28+x+%5Cright%29+-y+%5Cright%29%5C%5C+%09%26%3D%5Cleft%28+f%5Cleft%28+x+%5Cright%29+-y+%5Cright%29+%5Cfrac%7B%5Cpartial%7D%7B%5Cpartial+W_j%7D%5Cleft%28+%5Csum_%7Bi%3D0%7D%5Em%7BW_ix_i-y%7D+%5Cright%29%5C%5C+%09%26%3D%5Cleft%28+f%5Cleft%28+x+%5Cright%29+-y+%5Cright%29+x_j%5C%5C+%5Cend%7Baligned%7D+%5C%5C
SGD缺点:

[*]更新频繁,一个样本更新一次
[*]训练不稳定
一个可塑性很好的容器,俯视图是一个圆形,当把它在x轴向进行拉伸,y方向不变,就会呈现x方向的z的变化很平滑的情况,也就是z在x方向的梯度变得很小,由图2也可以看出x方向的优化速度明显不如y方向的快
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np

def fun(x, y):
    return x ** 2 / 10 + y ** 2

fig = plt.figure()
ax = Axes3D(fig)
X = np.arange(-10, 10, 0.1)
Y = np.arange(-10, 10, 0.1)
X, Y = np.meshgrid(X, Y)
Z = fun(X, Y)
ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap='rainbow')
ax.contourf(X, Y, Z, zdir='z', offset=-2, cmap='rainbow')
ax.set_xlabel('x', color='r')
ax.set_ylabel('y', color='g')
ax.set_zlabel('z', color='b')
plt.show()

X = np.arange(-10, 10, 1)
Y = np.arange(-10, 10, 2)

U, V = np.meshgrid(-X/5, -2*Y)# x的负梯度,y的负梯度,指向最优点,所以是负的
fig, ax = plt.subplots()
f = ax.quiver(X, Y, U, V)
ax.quiverkey(f, X=0.1, Y=1, U=10, label="key")
plt.xlabel("x")
plt.ylabel("y")
plt.show()
将x和y优化的进程可视化出来的效果如下:
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np

def fun(x, y):
    return x ** 2 + 10 * y ** 2

def g(x, y):
    return 2 * x , 20 * y

x, y = -200, 120
x_sgd, y_sgd = ,

a = 0.9

n_step = 100
lr = 0.08
x, y = -200, 120
for _ in range(n_step):
    gx, g_y = g(x, y)
    x -= gx * lr
    y -= g_y * lr
    x_sgd.append(x)
    y_sgd.append(y)

fig = plt.figure()
X = np.arange(-240, 240, 0.1)
Y = np.arange(-130, 130, 0.1)
X, Y = np.meshgrid(X, Y)
Z = fun(X, Y)

plt.plot(x_sgd, y_sgd, color='red', marker='o', label='sgd')
plt.contourf(X, Y, Z, cmap='rainbow')
plt.legend(loc='best')
plt.show()

SGD可以看成是向着容器底部滑落,一开始梯度大,速度很快,随着逐渐接近最低点,速度最大,加速度最小,因此SGD理解为一种加速度逐渐减小的加速运动。很容易冲出最优点,因此效率低下。
Momentum

关于momentum的对于梯度更新的公式,网上有好多个版本,不同版本也只是两个正负号不同,并不是说他们写的不对。这里直接找了原论文的版本,只不过我这里换了符号名。实际上不管什么版本,最终得到的结果都是一样的。目的就是要综合考虑当前的梯度和历史的梯度情况,用历史的梯度对当前的梯度做修正。

https://www.zhihu.com/equation?tex=v+%5Cgets+%5Calpha+v+-+%5Ceta+%5Cfrac%7B%5Cpartial%7BL%7D%7D%7B%5Cpartial%7BW_j%7D%7D+%5C%5C+W_j%5Cgets+W_j+%2B+v+%5C%5C
SGD的问题是,更新方向完全依赖于当前的样本,梯度方向也完全依赖当前样本及其得到的误差,因此更新不稳定。引入momentum动量,就是在对当前样本进行梯度更新时,同时考虑历史的更新方向。就是指数平均的作用,将过去的更新方向和当前的梯度方向进行加权,Momentum梯度下降呈指数衰减。
直观点,就是原始SGD在梯度方向上直接做下降,现在把历史的梯度信息也考虑进来,因此优化时会有两个矢量相加的效果。下降方向变成了两个矢量方向的中间方向,万物皆可融合呗。
优点:

[*]效率比SGD高
[*]波动比SGD小
[*]有摆脱局部最优的能力
x, y = -200, 120
x_momentum, y_momentum = ,
a = 0.9
v_x, v_y = 0, 0
n_step = 10
lr = 0.012

for _ in range(n_step):
    g_x, g_y = g(x, y)
    v_x = a * v_x - lr * g_x
    v_y = a * v_y - lr * g_y
    x += v_x
    y += v_y
    x_momentum.append(x)
    y_momentum.append(y)

由图可见,Momentum比SGD的优化速度快了很多,同样是迭代10次,Momentum已经达到了最优附近,而SGD离得还很远。
Nesterov

由于momentum考虑了历史的梯度信息,可以加速优化的进程,但如果参数已经处于最优附近,很有可能会因为累积的梯度导致过大的动量,再一次远离最优。
因此Nesterov期望参数在快到最优解时,适当调整当前的优化距离。
更新方式:

https://www.zhihu.com/equation?tex=v%5Cgets+%5Calpha+v-%5Ceta+%5Cfrac%7B%5Cpartial%7D%7B%5Cpartial+W%7DL%5Cleft%28+W%2B%5Calpha+v+%5Cright%29++%5C%5C+W%5Cgets+W%2Bv+%5C%5C
首先按照上一步的梯度和参数,对进行一次预更新,预测当前时刻 如果按照之前的梯度会更新到哪里,参数是更加接近最优 还是会越过最优。并且求预更新之后的参数梯度https://www.zhihu.com/equation?tex=%5Cfrac%7B%5Cpartial%7D%7B%5Cpartial+W%7DL%5Cleft%28+W%2B%5Calpha+v+%5Cright%29,令其为. 如果预更新的参数越过了最优,那么参数梯度的方向是指向最优的,而且方向与是相反的,因此利用矢量相加,可以实现用对的大小方向做矫正的目的。
然后进行标准的梯度下降https://www.zhihu.com/equation?tex=W%5Cgets+W%2Bv,完成本次迭代真正的梯度更新。
从下图中可以看出,NAG相比momentum先做了路径的调整,不过在当前实验条件下优化相同次数的效果 相差不大,这和损失函数的复杂程度有关。不过看其他的关于分类任务的实验报告以及论文来看,NAG也并不会比momentum有非常明显的提高。


x_nag, y_nag = ,

a = 0.9
nv_x, nv_y = 0, 0
n_step = 10
lr = 0.01

x, y = -200, 120
for _ in range(n_step):
    x_tmp = x + a * nv_x
    y_tmp = y + a * nv_y
    g_x, g_y = g(x_tmp, y_tmp)
    nv_x = a * nv_x - lr * g_x
    nv_y = a * nv_y - lr * g_y
    x += nv_x
    y += nv_y
    x_nag.append(x)
    y_nag.append(y)Adagrad

从上图的优化路径可以看出SGD的一个问题,纵轴方向的优化速度较快,横轴方向的速度很慢,因为纵轴方向的梯度相对横轴的梯度大很多。Adagrad期望利用每个维度的梯度值大小动态调整每个维度的更新步长,就是让纵轴方向的更新慢一点或者不变,让横轴的更新快一点。Adagrad的更新方式:

https://www.zhihu.com/equation?tex=h%5Cgets+h%2B%5Cfrac%7B%5Cpartial+L%7D%7B%5Cpartial+W%7D%5Codot+%5Cfrac%7B%5Cpartial+L%7D%7B%5Cpartial+W%7D+%5C%5C+W%5Cgets+W-%5Cfrac%7B%5Ceta%7D%7B%5Cepsilon+%2B+%5Csqrt%7Bh%7D%7D%5Cfrac%7B%5Cpartial+L%7D%7B%5Cpartial+W%7D+%5C%5C
Adagrad一直在累积梯度的平方和,然后将当作全局学习率的分母,从而实现动态调整学习率。这样做,如果优化前期的梯度很大,那么累积的梯度平方和会很大,放在分母上得到的更新步长就会很小,优化后期的更新步长就会变小;如果优化前期的梯度很小,那么累积的梯度平方和也会很小,更新步长就很大。
那么问题就来了,Adagrad一直在累积梯度的平方和,更新步长就会一直减小。如果早期梯度很大,导致更新步长迅速减小,很容易出现后期优化不动的情况,因为更新步长会逐渐趋向于0.如下图所示,Adagrad的优化是真的慢,我有点怀疑是不是我代码有问题。


x, y = -2, 1
x_adagrad, y_adagrad = ,
h_adagrad_x, h_adagrad_y = 0, 0
for _ in range(n_step):
    g_x, g_y = g(x, y)
    h_adagrad_x += g_x * g_x
    h_adagrad_y += g_y * g_y
    x -= lr / (math.sqrt(h_adagrad_x) + 1e-6) * g_x
    y -= lr / (math.sqrt(h_adagrad_y) + 1e-6) * g_y
    x_adagrad.append(x)
    y_adagrad.append(y)Adadelta

Adadelta是对Adagrad的一个改进,没有选择将梯度的平方和全部累加,而是使用指数加权的形式对梯度进行累积。此外,为了实现加速更新进程的效果,Adadelta还维护了一个更新步长平方的指数加权。

https://www.zhihu.com/equation?tex=g%3D%5Cfrac%7B%5Cpartial+L%7D%7B%5Cpartial+W%7D+%5C%5C+E_g%5Cgets+%5Crho+E_g%2B%5Cleft%28+1-%5Crho+%5Cright%29+g%5E2+%5C%5C+RMS%5Cleft%28+g+%5Cright%29+%3D%5Csqrt%7BE_g%2B%5Cepsilon%7D+%5C%5C+RMS%5Cleft%28+%5CDelta+x+%5Cright%29+%3D%5Csqrt%7BE_%7B%5CDelta+x%7D%2B%5Cepsilon%7D+%5C%5C+%5CDelta+x%5Cgets+-g%5Cfrac%7BRMS%5Cleft%28+%5CDelta+x+%5Cright%29%7D%7BRMS%5Cleft%28+g+%5Cright%29%7D+%5C%5C+E_%7B%5CDelta+x%7D%3D%5Crho+E_%7B%5CDelta+x%7D%2B%5Cleft%28+1-%5Crho+%5Cright%29+%5Cleft%28+%5CDelta+x+%5Cright%29+%5E2+%5C%5C+x%5Cgets+x%2B%5CDelta+x+%5C%5C

https://www.zhihu.com/equation?tex=%5CDelta+x可以理解为SGD的更新步长,而且可以看出这里不需要指定学习率超参数,避免对学习率敏感的问题。

https://www.zhihu.com/equation?tex=%5Cfrac%7BRMS%28%5CDelta+x%29%7D%7BRMS%28g%29%7D相当于是可以自适应的学习率,而且带有Adagrad的特点,对于历史平均较大的梯度方向给予较小的更新步长,反之则给予较大的更新步长,加快更新速度。
但我做出的图,效果和Adagrad一样拉胯
x, y = -200, 120
x_adadelta, y_adadelta = ,
e_g_x, e_g_y = 0, 0
e_x, e_y = 0, 0
dx, dy = 0, 0
r = 0.9
eps = 1e-6
for _ in range(n_step):
    g_x, g_y = g(x, y)
    e_g_x = r * e_g_x + (1 - r) * g_x ** 2
    e_g_y = r * e_g_y + (1 - r) * g_y ** 2
    dx = -math.sqrt(e_x + eps) / math.sqrt(e_g_x + eps) * g_x
    dy = -math.sqrt(e_y + eps) / math.sqrt(e_g_y + eps) * g_y
    e_x = r * e_x + (1 - r) * dx ** 2
    e_y = r * e_y + (1 - r) * dy ** 2
    x += dx
    y += dy
    x_adadelta.append(x)
    y_adadelta.append(y)RMSProp

RMSProp同样是对Adagrad的改进,相比Adadelta更加直观。只维护一个历史的梯度平方的指数加权,然后用其来影响当前的梯度

https://www.zhihu.com/equation?tex=g%3D%5Cfrac%7B%5Cpartial+L%7D%7B%5Cpartial+W%7D+%5C%5C+E_g%5Cgets+%5Cbeta+E_g%2B%5Cleft%28+1-%5Cbeta+%5Cright%29+g%5E2+%5C%5C+x%5Cgets+x-%5Ceta+%5Cfrac%7Bg%7D%7B%5Csqrt%7BE_g%2B%5Cepsilon%7D%7D+%5C%5C


从图中看上去效果还不错,实际上我的迭代次数调整到了200次,不过发现一个有趣的事情,即使迭代次数到了400次,RMSProp也不会离开最优点。说明其学习率自动调整的效果非常不错的。
x, y = -200, 120
x_rmsprop, y_rmsprop = ,
e_g_x, e_g_y = 0, 0
beta = 0.9
eps = 1e-6
for _ in range(n_step):
    g_x, g_y = g(x, y)
    e_g_x = beta * e_g_x + (1 - beta) * g_x ** 2
    e_g_y = beta * e_g_y + (1 - beta) * g_y ** 2
    x += -g_x / math.sqrt(e_g_x + eps)
    y += -g_y / math.sqrt(e_g_y + eps)
    x_rmsprop.append(x)
    y_rmsprop.append(y)Adam

Adam 相当于是RMSProp的动量版,既要RMSProp自适应学习率,又要加入动量加快优化。更新方式如下:

https://www.zhihu.com/equation?tex=v%5Cgets+%5Cbeta+_1v%2B%5Cleft%28+1-%5Cbeta+_1+%5Cright%29+g+%5C%5C+h%5Cgets+%5Cbeta+_2h%2B%5Cleft%28+1-%5Cbeta+_2+%5Cright%29+g%5E2+%5C%5C+W%5Cgets+W-%5Ceta+%5Cfrac%7Bv%7D%7B%5Csqrt%7Bh%2B%5Cepsilon%7D%7D+%5C%5C
Adam认为在训练初期和初始为0,这样更新会存在偏差,因此采取矫正方法.令初始学习率为https://www.zhihu.com/equation?tex=%5Ceta_0, 更新次数为https://www.zhihu.com/equation?tex=iter次

https://www.zhihu.com/equation?tex=v%5Cgets+%5Cbeta+_1v%2B%5Cleft%28+1-%5Cbeta+_1+%5Cright%29+g+%5C%5C+h%5Cgets+%5Cbeta+_2h%2B%5Cleft%28+1-%5Cbeta+_2+%5Cright%29+g%5E2+%5C%5C+%5Ceta%5Cgets+%5Ceta_0+%5Csqrt%7B%5Cfrac%7B1-%5Cbeta_2%5E%7Biter%7D%7D%7B1-%5Cbeta_1%5E%7Biter%7D%7D%7D+%5C%5C+W%5Cgets+W-%5Ceta+%5Cfrac%7Bv%7D%7B%5Csqrt%7Bh%2B%5Cepsilon%7D%7D+%5C%5C
x, y = -7.5, 2.5
x_adam, y_adam = ,
v_x, v_y = 0, 0
h_x, h_y = 0, 0
beta_1, beta_2 = 0.9, 0.999
lr_base = 0.9
eps = 1e-6
for n in range(1, n_step+1):
    g_x, g_y = g(x, y)
    v_x = beta_1 * v_x + (1 - beta_1) * g_x
    v_y = beta_1 * v_y + (1 - beta_1) * g_y
    h_x = beta_2 * h_x + (1 - beta_2) * g_x ** 2
    h_y = beta_2 * h_y + (1 - beta_2) * g_y ** 2
    lr = lr_base * math.sqrt((1 - beta_2 ** n) / (1 - beta_1 ** n))
    x -= lr * v_x / math.sqrt(h_x + eps)
    y -= lr * v_y / math.sqrt(h_y + eps)
    x_adam.append(x)
    y_adam.append(y)放上所有优化器的对比图


在此实验设置下Adam正常收敛,但是如果把初始位置放在离最优非常远的位置,效果就很拉跨,需要增加迭代次数到300次才能基本到最优


那么问题来了,一般情况Adam的效果比SGD + momentum效果更好,因为其学习率可以自适应,总结的结论一般就是adam设置的初始学习率并不敏感,可以在很广泛的区间内优化到一个较好的参数,而SGD+momentum则是在很窄的学习率区间内可以得到非常优秀的结果,在这个区间外的结果可能就比较差。
大致的分析一下Adam的初始参数为(-200, 120)的时候效果这么差,和通常的结论相差甚远。
首先来看下离最优较近的情况,这里做了300次迭代的和更新有关的9个参数的曲线,
其中https://www.zhihu.com/equation?tex=s_x%3D%5Cfrac%7Bv_x%7D%7B%5Csqrt%7Bh_x%2B%5Cepsilon%7D%7D,https://www.zhihu.com/equation?tex=s_y同理


可以发现一阶参数如https://www.zhihu.com/equation?tex=g_x%2Cg_y%2Cv_x%2Cv_y在大约100次收敛至0,而二阶参数(开平方)https://www.zhihu.com/equation?tex=h_x%2C+h_y呈现先急速上升后缓慢下降的趋势。
对比一下初始值离最优很远的情况,


可以观察到现象,两个的初始位置虽然差了大约两个数量级,导致对应的一阶、二阶参数也差了两个数量级,但是实际更新步长的处于同样的数量级,即学习率、https://www.zhihu.com/equation?tex=SX%3D%5Cfrac%7Bv_x%7D%7B%5Csqrt%7Bh_x%2B%5Cepsilon%7D%7D和https://www.zhihu.com/equation?tex=SY都没太大变化,所以要增大迭代次数才能有收敛的迹象。
另外,这个现象对这个问题给出一个可能的比较合理的解释:为什么在一些情况下,Adam总也收敛不了,而SGD+momentum总是能收敛。
这也可以看出随机初始化方法选择的重要性。

[*] On the importance of initialization and momentum in deep learning
[*] 比Momentum更快:揭开Nesterov Accelerated Gradient的真面目 - 知乎 (zhihu.com)
[*] Deep Learning 最优化方法之AdaGrad - 知乎 (zhihu.com)
[*] 如何理解Adam算法(Adaptive Momentum Estimation)? - 知乎 (zhihu.com)
页: [1]
查看完整版本: 7个优化器原理吐血总结+为什么有时候Adam不如SGD