动量梯度下降(Gradient Descent With Momentum),简称为动量方法(Momentum),运行速度几乎总是快于标准的梯度下降算法,并且能够解决随机梯度下降所遇到的山谷震荡以及鞍部停滞问题,这部分内容请阅读上一篇博客梯度下降算法。
根据梯度下降算法的参数更新公式:
w = w − η ∂ L ( w ) ∂ w w = w - \eta\frac{\partial L(w)}{\partial w} w=w−η∂w∂L(w)
参数的更新仅仅取决于当前位置的梯度以及步长,试想一下这样一个情境:我们将某一个物品往山谷里丢,在梯度下降算法的规则下,该物品仅仅收到当前触碰在它身上的力,而忽略其他的力,例如重力、空气阻力等等。我们可以把它想象成一张纸团。
如果此时,该物品拥有了大质量,例如是一个铁球,
在中学物理中,刻画惯性的物理量是动量,这也是该算法名字的由来。沿山谷滚下的铁球会收到沿坡道向下的力和与左右山壁碰撞的弹力。向下的力(重力)稳定不变,产生的动量不断累积,速度越来越快;左右的弹力总是在不停切换,动量累积的结果是相互抵消,减弱了球的来回震荡。因此,与随机梯度下降相比,动量方法的收敛速度更快,收敛曲线也更稳定,见下图。
相比标准的梯度下降算法,动量梯度下降是算法将动量纳入了参数更新公式中。
【计算公式】:
v t = γ v t − 1 + η ∂ L ( w ) ∂ w w = w − v t v_t = \gamma v_{t-1} + \eta \frac{\partial L(w)}{\partial w} \quad w = w - v_t vt=γvt−1+η∂w∂L(w)w=w−vt
其中,KaTeX parse error: Expected 'EOF', got '\gammda' at position 1: \̲g̲a̲m̲m̲d̲a̲ 是衰减系数,扮演阻力的作用。前进步伐 v t v_t vt 由两部分组成:
在该公式中,惯性就体现在对前一次步伐信息的利用。类比中学物理知识,当前梯度就好比当前时刻受力产生的加速度,而步长则是时间,前一次步伐好比前一时刻的速度。标准梯度下降算法在每次行动时,都忽略前一时刻的速度,而重新根据当前时刻的加速度和时间来行走,因此当加速度趋于零时就很难继续移动。而动量方法则考虑前一时刻速度和当前加速度的共同作用。
不同的文献对于动量方法的计算公式也略有不同,吴恩达老师推荐使用下述的计算公式。
v t = β v t − 1 + ( 1 − β ) ∂ L ( w ) ∂ w w = w − η v t v_t = \beta v_{t-1} + (1 - \beta)\frac{\partial L(w)}{\partial w} \\ w = w - \eta v_t vt=βvt−1+(1−β)∂w∂L(w)w=w−ηvt
至于孰优孰劣暂无定论,大家可各按喜好进行选择。
动量方法可以嵌入到标准的梯度下降算法中,例如使用动量的随机梯度下降、使用动量的批量梯度下降等等。无论对哪个梯度下降算法,加入动量后都可以加快收敛速度。对于随机梯度下降而言,还可以在一定程度上解决鞍部停滞问题。
【代码实现】:
def BatchGradientDescentM(x, y, step=0.001, iter_count=500, beta=0.9):
length, features = x.shape
# 初始化参数和动量以及整合 x'
data = np.column_stack((x, np.ones((length, 1))))
w = np.zeros((features + 1, 1))
v = np.zeros((features + 1, 1))
# 开始迭代
for i in range(iter_count):
# 计算动量
v = (beta * v + (1 - beta) * np.sum((np.dot(data, w) - y) * data, axis=0).reshape((features + 1, 1))) / length
# 更新参数
w -= step * v
return w
同样,增量方法也可以嵌入到小批量梯度下降以及随机梯度下降算法中,具体代码请参考 传送门
我们也可以将这些算法都整合到一块,通过 batch_size 的大小来判断是批量梯度下降算法,还是随机梯度下降算法。
【代码实现】:
def Momentum(x, y, step=0.01, iter_count=1000, batch_size=4, beta=0.9):
length, features = x.shape
# 初始化参数和动量以及整合 x'
data = np.column_stack((x, np.ones((length, 1))))
w = np.zeros((features + 1, 1))
v = np.zeros((features + 1, 1))
start, end = 0, batch_size
# 开始迭代
for i in range(iter_count):
v = (beta * v + (1 - beta) * np.sum((np.dot(data[start:end], w) - y[start:end]) * data[start:end], axis=0).reshape((features + 1, 1))) / length
w -= step * v
start = (start + batch_size) % length
if start > length:
start -= length
end = (end + batch_size) % length
if end > length:
end -= length
return w
# 批量梯度下降
print(Momentum(x, y, batch_size=(x.shape[0] - 1)))
# 输出:
array([[5.00311478],
[0.8307453 ]])
# 小批量梯度下降
Momentum(x, y, batch_size=5)
# 输出:
array([[4.98144568],
[1.43164128]])
# 随机梯度下降
Momentum(x, y, batch_size=1)
# 输出:
array([[4.99294353],
[0.83128473]])
受 Nesterov 加速梯度算法启发,Sutskever 提出动量方法的一个变种。与 Momentum 不同的是,Nesterov 先用当前的速度更新参数,再用更新后的临时参数计算梯度。
【代码实现】:
def Nesterov(x, y, step=0.01, iter_count=1000, batch_size=4, beta=0.9):
length, features = x.shape
data = np.column_stack((x, np.ones((length, 1))))
w = np.zeros((features + 1, 1))
v = np.zeros((features + 1, 1))
start, end = 0, batch_size
for i in range(iter_count):
# 先更新参数
w_temp = w - step * v
# 再计算梯度与速度
v = (beta * v + (1 - beta) * np.sum((np.dot(data[start:end], w_temp) - y[start:end]) * data[start:end], axis=0).reshape((features + 1, 1))) / length
w -= step * v
start = (start + batch_size) % length
if start > length:
start -= length
end = (end + batch_size) % length
if end > length:
end -= length
return w
牛顿增量相当于添加了矫正因子的 Momentum 方法,在批量梯度下降算法中能进一步缩小误差,但对于随机梯度下降而言,牛顿增量没有任何改进。