一.SGD的概述
神经网络学习阶段的目的是找到使损失函数的值尽可能小的超参。这是寻找最优参数的问题,在之前写过的minist手写数字识别的文章里面,为了找到最优参数,我们将参数的梯度作为线索。使用参数的梯度沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠近最优参数,这个过程称为随机梯度下降法(stochastic gradient descent),简称SGD。SGD虽然容易实现,但是在解决某些问题时可能没有效率。比如使用SGD作为优化器进行迭代收敛时的路径如下图所示:
根据图上可以观察到,SGD呈现出“之”字型的走法,可以看到SGD的缺点是,如果函数的形状非均向(anisotropic),比如呈延伸状,搜索的路径就会非常低效。造成这种低效的原因在于SGD计算时梯度的方向并没有指向全局最小值的方向,而是指向局部最小值。为了解决这些问题,引入了动量优化器Momentum。
二.Momentum的概述
Momentum动量优化器的灵感来源于物理学中的动量概念,在物理学中,动量是物体运动状态的量度,它与物体的质量和速度有关,动量算法在神经网络中的作用可以视作为在迭代过程中增加了一种类似物体的惯性,使得算法在迭代过程中能够根据之前的梯度信息调整当前的更新方向和学习率,从而在目标函数的曲面上更加平滑地滚动,避免在局部最小值或平坦区域停滞不前,使得优化过程更加稳定,有助于跳出局部极小值,加快收敛速度,减少震荡。
我们来回顾一下SGD的公式:
Momentum方法可以理解为是在SGD上面优化而来的,Momentum方法在更新超参之前,首先使用指数加权平均累计历史梯度来调整后续超参梯度方向和步幅,然后再进行超参的更新,用数学式表示Momentum方法,如下所示:
公式中,W表示要更新的权重参数, aL/aW表示损失函数关于W的梯度,η 表示学习率,α是动量衰减系数,通常设置在 [0,1)之间,如 0.9 或 0.99,值越大,历史梯度的影响越强,优化过程更加平滑,但也可能会导致模型收敛速度过慢,变量v由一阶变量:指数加权平均的计算获得。
三.指数加权平均理论推导
指数加权平均指的是梯度的一阶动量,是一种类似“记忆递减”机制的统计方法,用于计算梯度的加权平均,其中最近的梯度被赋予更高的权重,较远梯度的权重逐渐递减,使得优化器能够快速适应最新的梯度变化,同时保留一定的历史信息,避免过度依赖当前梯度,公式如下:
为什么叫指数加权平均?
我们可以通过公式展开来说明:
基于以上公式,我们可以假设下:
可以看到,越远的梯度权重呈现指数级衰减。
普通加权平均和指数加权平均的区别:
-
普通的加权平均,所有梯度的权重都是人为设定、不随时间衰减的固定值:
-
指数加权平均,所有梯度的权重是动态生成、随时间呈现指数衰减:
假设衰减指数为0.9,则:
最终结果为:
四.Momentum的实际公式优化
在实际的使用中,会省略(1 - beta)项,直接使用:
为什么要省略?
1.降低计算的复杂难度
移除(1−β)后,梯度计算步骤减少一次标量乘法操作,在硬件加速(如GPU)上可提升并行效率,这对大规模深度学习模型训练尤为重要。
2.降低代码实现的复杂性
移除(1−β)后,只需将学习率调整为 η′=η/(1−βt)η 即可保持参数更新幅度与原公式等效,衰减系数与学习率的关联性减弱,降低调参复杂度。实验表明,在稳定阶段(t较大时),βt趋近于0,此时η′≈η,两公式更新效果趋近一致。而当β取0.9时,η′与原η的实际差异通常在可接受范围内。故在后续的使用中,学习率依旧沿用初版公式
3.TensorFlow、PyTorch等主流框架均默认采用简化公式,通过调整默认学习率参数隐含实现补偿,确保用户无需手动处理(1−β)项
如pytorch所示:
# PyTorch中Momentum实现源码片段(无(1−β)项)
def step(self):
for p, d_p in zip(self.params, self.grads):
if p.grad is None:
continue
buf = self.state[p]
buf.mul_(self.momentum).add_(d_p) # 直接累加梯度
p.add_(-self.lr, buf)
4.与自适应方法结合
当Momentum与RMSProp等自适应学习率方法结合时(如Adam),(1−β)项的省略已被隐式吸收到自适应系数中,无需额外处理。
五.Momentum的代码实现
import numpy as np
from collections import OrderedDict
import matplotlib.pyplot as plt
class Momentum:
"""Momentum SGD"""
def __init__(self, lr=0.1, 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]
optimizers= Momentum()
init_pos = (-7.0, 2.0)
params = {}
params['x'], params['y'] = init_pos[0], init_pos[1]
grads = {}
grads['x'], grads['y'] = 0, 0
x_history = []
y_history = []
def f(x, y):
return x**2 / 20.0 + y**2
def df(x, y):
return x / 10.0, 2.0*y
for i in range(30):
x_history.append(params['x'])
y_history.append(params['y'])
grads['x'], grads['y'] = df(params['x'], params['y'])
optimizers.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
idx = 1
# 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, '+')
# colorbar()
# spring()
plt.title("Momentum")
plt.xlabel("x")
plt.ylabel("y")
plt.show()
效果如图所示:
六.Momentum的缺点
Momentum动量优化器在加速梯度下降和减少振荡方面具有优势,但也存在一些缺点:
- 对学习率敏感:动量优化器依赖于学习率的选择,若学习率设置过大可能导致更新不稳定并偏离最优解,设置过小则收敛速度缓慢。
- 可能在鞍点滞留:尽管动量机制有助于摆脱局部最小值,但在高维空间的鞍点区域,梯度方向频繁变化时,动量累积可能导致参数更新在鞍点附近振荡或停滞。
- 平缓区域收敛延迟:在损失函数的平缓区域(梯度较小),动量优化器的更新步长可能衰减,导致收敛速度变慢,尤其在初始阶段或低梯度场景下。
- 需调整动量超参数:动量系数(如β)需要额外调优,不当设置可能削弱优化效果或引入不必要的噪声,增加训练复杂度。