DL 笔记:高级梯度下降算法
DL笔记:高级梯度下降算法的应用
用于训练神经网络的主要优化算法,在Python中从头解释和实现
在我的关于梯度下降的先前文章中,我解释了其背后的基本概念,并总结了这种优化的主要挑战。
然而,我只涵盖了随机梯度下降(SGD)以及梯度下降的“批量”和“小批量”实现。
其他算法在收敛速度、对“地形”特征(梯度消失问题)的稳健性以及在选择学习率方面实现良好性能方面具有优势。
因此,今天我将讨论更高级的优化算法,并在Python中从头实现它们,并通过动画可视化进行比较。
- AI中的爬山算法是什么?
- Meta 在 Facebook 和 Instagram 上推出了新的 AI 功能
- 这项AI研究推出了CoDi-2:一种突破性的多模态大型语言模型,改变了交错指令处理和多模态输出生成的领域
我还列出了我用来学习这些算法的资源。它们非常适合深入了解形式概念。
使用简单目标函数比较算法
在本文中,我将展示如何在Python中实现不同的算法。
我创建了一个Jupyter Notebook,您可以在GitHub上访问或直接在Google Colab上访问,以查看用于创建此处显示的图形的所有代码。
为了生成动画,我使用了我之前关于在Python中创建动画梯度下降图形的方法。
函数定义假定以下代码已经包含在其中,因为它们使用了numpy
类和方法,并调用了函数f
及其梯度grad_f
。
import numpy as np# 创建计算曲面的函数def f(theta): x = theta[0] y = theta[1] return x**2 - y**2# 定义计算梯度的函数def grad_f(theta): returnValue = np.array([0.,0.]) x = theta[0] y = theta[1] returnValue[0] += 2*x returnValue[1] += - 2*y return returnValue
动量
我们可以将优化算法与球沿着山下滚动进行比较。
如果“球”像现实生活中一样具有动量,它在全速下坡后不太可能停留在局部最小值处。
这就是人们在处理梯度下降陷入局部最小值问题时意识到的。
从高中物理知识上看,平移动量通过物体的质量和速度的乘积定义:
我们还知道物体的质量 m 的重力势能与其放置的高度 h 成正比:
此外,物体的势能与施加在它上面的力之间存在直接的关系
p 和 U 之间的关系可以由牛顿第二定律推导出来:
物体的运动变化与施加的力成正比,且沿着施加力的直线方向进行变化。
💡 实际上,这个物理类比过于简化,无法涵盖添加动量到梯度下降优化中的所有优点和缺点。要了解全貌,我建议你查看为什么动量真的有效?
如何添加动量?
当我们初始化优化算法时,将“球”放置在高度 h 处,给予其势能 U。
施加在球上的力与该势能的梯度成正比。如同我们正在优化的函数的梯度(我们正在移动的表面)。
动量对优化的作用方式是使用梯度来改变“粒子”的“速度”,从而改变其位置。
由于速度项,该“粒子”在具有一致梯度的任何方向上积累速度。
我将其实现为以下的 Python 函数:
def gradient_descent(x_init, y_init, step_size, n_iters, momentum): eta = step_size mu = momentum # 注意,如果 mu = 0,则该算法就是 SGD # 初始化数组以存储结果 theta = np.tile([x_init, y_init], (n_iters,1) ) z = np.tile([f(theta[0])], n_iters ) # 初始化速度项 v_t = np.array([0,0]) for k in range (1, n_iters): # 更新速度 v_t = mu*v_t - eta*grad_f(theta[k-1]) # 更新位置 theta[k] = theta[k-1] + v_t z[k] = f(theta[k]) # 存储位置坐标 dataset = np.stack((theta[:,0], theta[:,1], z), 1) return dataset
动量更新既加速了低曲率方向上的优化,又平滑了由“地形”特征或噪声数据引起的震荡[3]。
有人认为,动量更新实际上更符合摩擦系数的物理效应,因为它减少了系统的动能 [2]。
另一种解释是它给了优化过程“短期”记忆。
动量参数小于1, acts like an exponentially weighted sum是先前梯度的以指数形式加权的和,速度更新可以重写为 [3][5]:
其中 g 是瞬时梯度,v 是平滑梯度估计值。
参数 β 控制我们给予瞬时梯度的新值与先前值的权重。
通常取 0.9,但有时会进行“调度”,即在迭代进行中逐渐从0.5增加到0.99。
Nesterov’s Accelerated Gradient (NAG)
由Nesterov, Y.在1983年提出。
Nesterov 更新引入了“lookahead”功能,以提高Momentum在凸函数上的稳定性和收敛速度。
当 Momentum 使用当前位置更新梯度时,NAG 首先对当前位置进行部分更新,因为它意识到
要将其实现为 Python 函数,只需对之前代码中的“速度更新”进行以下修改:
# Update velocity# Momentumv_t = mu*v_t - eta *grad_f(theta[k-1])# NAGv_t = mu*v_t - eta *grad_f(theta[k-1] + mu * v_t)
这种部分更新有助于提高优化的准确性。从实际角度来看,与 Momentum 相比,它转化为在局部最小值周围更少的振荡。
下图显示了两者的差异。
两种优化器初始坐标相同,使用相同的动量参数(0.95,固定)。
下面的动画还帮助我们看到调度或“退火”动量参数的直觉。
一开始,一点动量可以有助于摆脱消失梯度。当接近局部最小值时,具有较大动量值可以减弱我们看到的振荡,提高收敛速度。
自适应方法
上面动画中展示的其他优化算法都属于自适应方法,我将在本节中进行描述。
从这个简单的例子中可以看出,动量法和NAG可能比其他方法更优越。然而,自适应算法更加健壮。我将在另一篇文章中通过实际例子进行展示。
自适应梯度算法(AdaGrad)
AdaGrad是一类用于随机优化的子梯度算法,由John Duchi、Elad Hazan和Yoram Singer于2011年提出。
他们提出通过将梯度的历史纳入权重的每一次更新,来改进基于梯度的学习。
AdaGrad不像动量法那样调整梯度本身,而是为目标函数的每个参数动态且独立地修改学习率。
这意味着我们对每个模型权重使用不同的学习率。学习率的调整基于梯度的稳定性。
为了实现这一点,梯度估计的序列存储如下:
如果我们正在优化一个具有n个坐标或参数的函数,gₜ将是一个具有n个元素的向量,Gₜ也是如此。
然后,更新规则如下:
参数ε用于避免除以零,并且通常设置为一个小值,如1e-08。
有趣的是,Gₜ的定义类似于(零均值的)梯度分布的方差。
方差是分布的离散能量的度量。
因此,对于每个参数θᵢ,学习率按照其梯度方差的倒数来进行适应。
考虑到这一点,我们可以说在梯度分布中具有更大离散性的参数将以更大的因子缩小学习率,而具有更一致梯度(较低的方差)的参数将具有更大的学习率。
AdaGrad还自动实现学习率衰减,基于时间(之前梯度的累积)和目标函数的曲率(梯度方差较低的“区域”将被分配较小的步长)。这提高了算法的收敛速度。
我已经实现了一个Python函数来执行AdaGrad算法,如下所示:
def Adagrad(x_init, y_init, step_size, n_iters): eta = step_size G_t = 0 eps = 1e-8 theta = np.tile([x_init, y_init], (n_iters,1) ) z = np.tile([f(theta[0])], n_iters ) for k in range (1, n_iters): # 计算梯度 g_t = grad_f(theta[k-1]) # 累积平方梯度 G_t += g_t**2 # 更新位置 theta[k] = theta[k-1] - eta * g_t / (np.sqrt(G_t) + eps) z[k] = f(theta[k]) # 存储位置坐标 dataSet = np.stack((theta[:,0], theta[:,1], z), 1) return dataSet
AdaGrad的一个缺点是,在训练期间学习率的衰减可能过于激进,导致在训练人工神经网络时学习停止得太早。每次参数更新都是稳健的,但是变化朝向最优点的速率可能会减少得太多。
另一个缺点是,虽然学习率在学习过程中进行了自我调整,但AdaGrad仍然对初始条件敏感。如果优化开始时梯度很大,则剩余训练时学习率将变低。
我们可以在动画图中看到这一点。AdaGrad很快脱离对称性,但与其他算法相比,学习速度非常慢。
为了弥补这一点,可能需要将学习率调整为更高的值,这在一定程度上背离了自我调整的特点。
均方根传播(RMSprop)
未发表的方法,但在课程《神经网络的机器学习》第6讲幻灯片中提到,该课程由Geoffrey Hinton教授讲授。
该算法的概念与动量类似。它还将梯度的幅度的短期历史纳入权重更新中。
然而,与AdaGrad类似,RMSprop修改的是学习率而不是梯度。
为此,学习率除以最近梯度幅度的平均值的平方根。
首先,算法计算代价值及其先前值的加权和:
这类似于短期均值,其中参数β调整对最新代价值与较旧代价值的权重。
这类似于我之前提到的动量的改写形式,但应用于平方代价而不是梯度。
下一步是将学习率除以这个移动平均值的平方根。
这样,步长取决于梯度幅度的历史(短期记忆)。
注意,计算加权和(或加权平均值)的平方根等效于计算这些值的均方根(RMS)。
信号的RMS表示其总能量(与方差相反,方差表示其离散能量)[1]。
因此,使用RMSProp,学习率根据成本函数的梯度及其先前值的总能量调节。这种调整是动态的,并针对损失函数的每个方向或分量(每个权重!)进行。
目标是通过在这些情况下减小步长来减少梯度变化引起的波动。
这也有助于消失梯度问题,因为当存在非常小的梯度趋势时我们会采取更大的步长。
下面是我将其编码为Python函数的方式:
def RMSProp(x_init, y_init, step_size, n_iters, decay): beta = decay # 0.8, 0.9, ..., 0.99 eta = step_size eps = 1e-8 MSQ = 0 theta = np.tile([x_init, y_init], (n_iters,1) ) z = np.tile([f(theta[0])], n_iters ) for k in range (1, n_iters): # 计算梯度 g_t = grad_f(theta[k-1]) # 计算加权平方值的均值 MSQ = beta * MSQ + (1 - beta) * g_t**2 # 更新位置(除以RMS后的η) theta[k] = theta[k-1] - eta * g_t / (np.sqrt(MSQ) + eps) z[k] = f(theta[k]) # 存储位置坐标 dataSet = np.stack((theta[:,0], theta[:,1], z), 1) return dataSet
RMSprop对学习率的初始选择具有鲁棒性,并且还实现了自动学习率衰减。然而,由于它是基于梯度值的短期历史,衰减比AdaGrad要缓和。
AdaDelta
这种方法的开发旨在克服AdaGrad的主要限制:连续衰减的学习率会导致提前停止,并且需要手动调整“全局”学习率。
为了克服连续的学习率衰减,算法在一个固定大小的窗口上累积过去梯度的历史记录。
在实践中,这意味着将学习率除以固定大小的窗口内先前梯度的均方根,就像RMSprop那样:
AdaGrad的下一个修改是纠正优化更新的单位。
在AdaGrad(以及我到目前为止描述的所有其他优化算法)中,优化步骤的单位与我们修改以优化成本函数的参数的单位不匹配[9]:
我们从学校知道不能加上苹果和橙子。但是使用这些优化算法,就好像我们一直在添加“苹果”(当前参数值,θₜ)和一些未知数量(优化步骤Δθ) ,数学上可以将它们相加得到新的苹果(更新的参数θₜ ₊₁)。它确实起作用,但在现实生活中没有意义。
Zeiler决定纠正单位,重新排列牛顿法的更新项,并假设损失函数的曲率可以近似为一个对角线矩阵:
将这个观察结果与类似于RMSProp的更新规则进行比较,Zeiler确定了保留正确单位的更新项的正确形式。
这个想法在原始论文中有更好的解释,但在实践中,它导致将先前更新值的指数加权平均的平方根添加到更新项的分子中:
这基本上是假设损失函数在大小为w的小窗口内是平滑的(低曲率),以便可以用先前值的指数RMS近似Δθₜ。
如果将其作为Python函数实现,该算法如下:
def AdaDelta(x_init, y_init, step_size, n_iters, decay): eta = step_size G_t = 0 eps = 1e-8 E_gsq = 0 E_xsq = 0 theta = np.tile([x_init, y_init], (n_iters,1) ) z = np.tile([f(theta[0])], n_iters ) for k in range (1, n_iters): g_t = grad_f(theta[k-1]) E_gsq = decay * E_gsq + (1 - decay) * g_t**2 delta = - np.sqrt(E_xsq + eps) / np.sqrt(E_gsq + eps) * g_t E_xsq = decay * E_xsq + (1 - decay) * delta**2 theta[k] = theta[k-1] + delta z[k] = f(theta[k]) # 设置动画的数据集 dataSet = np.stack((theta[:,0], theta[:,1], z), 1) return dataSet
AdaDelta结合了其构建所基于的优化方法的优点。
例如,分子中先前参数更新的短期记忆类似于动量,并具有加速梯度下降的效果。
分母提供了AdaGrad的每个维度准确性,但没有过多的学习率衰减(就像RMSProp一样)。
此外,AdaDelta对突然的梯度变化更稳健,并且对初始学习率的选择也很稳健(请参见本文最后一节中的实际示例)。
Adam(自适应动量)
这是今天最流行的算法之一。
它由Diederik P. Kingma和Jimmy Lei Ba于2014年提出,因其计算效率和在具有大量数据和参数的问题上表现出色而变得非常流行。
Adam就像是动量和RMSprop的组合,因为它动态更改了损失函数的梯度和用于缩放这种梯度以更新权重的学习率。
为此,算法包括了计算两个在本文前几节中会感到熟悉的术语。
首先,有动量的项,代表成本函数先前梯度的指数加权和(这类似于加权方差):
然后,有RMSprop的项,代表平方梯度的指数加权移动平均值。
将这两个术语与SGD算法相结合,将过去梯度的信息包含在更新步骤中。它们在短窗口内的总能量(RMS)用于缩放学习率,它们的分散性(方差)有助于调整用于更新权重的当前梯度值。
带有帽子(~)的值对应于偏差校正术语,这些术语被引入以减少学习进程中来自m和v的初始值的贡献:
t = 当前训练时期。
与AdaDelta不同,Adam确实需要对一些超参数进行调整,但这些超参数很容易解释。
术语β₁和β₂是梯度和平方梯度的指数移动平均数的衰减率。
较大的值将给先前梯度分配更大的权重,使其更平滑,并且对最近的变化反应较小。接近零的值给最近梯度变化更大的权重。典型的值是β₁ = 0.9和β₂ = 0.999。
ε是一个常数,像之前的情况一样,添加以避免被零除,通常设置为1e-8。
尽管有一堆额外的术语和显著的优势,实现Adam很容易:
def Adam(x_init, y_init, step_size, n_iters, beta_1 = 0.9, beta_2 = 0.999): eps = 1e-8 eta = step_size # 初始化向量 m_t = np.array([0,0]) v_t = np.array([0,0]) theta = np.tile([x_init, y_init], (n_iters,1) ) z = np.tile([f(theta[0])], n_iters ) for k in range (1, n_iters): # 计算梯度 g_t = grad_f(theta[k-1]) # 计算“动量类似”的项(加权平均) m_t = beta_1 * m_t + (1 - beta_1)*g_t # 计算平方梯度值的均值 v_t = beta_2 * v_t + (1 - beta_2)*g_t**2 # 初始化偏差校正项 m_t_hat = m_t/(1 - beta_1**k) v_t_hat = v_t/(1 - beta_2**k) # 使用调整后的梯度和学习率更新位置 theta[k] = theta[k-1] - eta * m_t_hat/(np.sqrt(v_t_hat)+ eps) z[k] = f(theta[k]) # 存储位置坐标 dataSet = np.stack((theta[:,0], theta[:,1], z), 1) return dataSet
有趣的是,这篇论文的作者指出,术语
与信噪比(SNR)的定义相似:
因此,我们可以说对于较小的SNR值,参数更新将接近于零。这意味着当我们不能确定是否朝着真正的梯度方向移动时,我们不会进行大规模的更新。
在训练深度学习模型时,Adam及其变种通常表现优于其他算法,特别是当梯度非常嘈杂和稀疏时。
不同学习率的性能
我决定比较不同优化器在不同“全局”学习率下的性能。
这只是一个相对简单的例子,但它实际演示了学习率选择对这些方法的影响。
可以看出,AdaDelta对全局学习率的设置非常稳健,且在所有三种情况下“下降”速度相同。我们还可以看到,在这种情况下,AdaGrad需要更大的学习率才能达到与AdaDelta相当的性能。
对于较小的学习率,显然Adam和RMSProp类似,并且优于动量法和SGD。
然而,对于较大的学习率,RMSProp在优化x值(x = 0)周围显示出持续的振荡,而Adam在初始瞬态之后稳定下来,这要归功于分子中动量项的阻尼效果。
自适应算法早于SGD和动量法破除了对称性,但与全局学习率为0.1的情况相比,动量法和NAG优于AdaDelta。
再次强调,这些观察结果仅适用于特定的场景。
结论
当我们将这些优化算法应用于简单函数(如上述鞍点示例)时,这些优化算法的优势并不完全明显。
对于其他小规模模型或数据集的情况,甚至SGD可能效果更好,因此了解每种类型的优化器在哪些情况下效果最好非常重要。
在训练神经网络时,我们优化损失函数,但在任何时刻都没有精确的梯度值,只有其近似值。这就是为什么像Adam和AdaDelta这样的方法,在梯度中存在噪音和稀疏性时,在数据科学界被广泛使用的原因。
此外,我们可以处理大量模型权重,而不仅仅是x和y坐标。在这些情况下,获得每个参数的学习率是有益的。
在将来的文章中,我将在另一篇文章中使用人工神经网络展示这些方法的更加实际的比较。
进一步阅读
参考资料
除非另有说明,所有图表均由作者创建。
[1] 在线课程 深度学习的深度理解,Mike X Cohen著(sincxpress.com)
[3] Goh著。”为什么动量方法真正有效”,Distill,2017。 http://doi.org/10.23915/distill.00006
[4] Villalarga, D. “AdaGrad”。发表于康奈尔大学计算优化开放教材-优化维基。
[5] Bengio, Yoshua著。“深度体系结构梯度训练的实践建议。”《神经网络:行业的技巧:第二版》。柏林,海德堡:斯普林格柏林海德堡,437–478,2012年。在线:arXiv:1206.5533 [cs.LG]
[6] Sutskever, I., Martens, J., Dahl, G. & Hinton, G. “关于初始化和动量在深度学习中的重要性”。机器学习研究论文集,28(3):1139–1147,2013。来源:https://proceedings.mlr.press/v28/sutskever13.html
[7] Duchi, J., Hazan, E., Singer, Y.,“自适应子梯度方法用于在线学习和随机优化”。机器学习研究期刊,12(61):2121−2159,2011。来源:https://jmlr.org/papers/v12/duchi11a.html
[8] Jason Brownlee,从头开始使用AdaGrad的梯度下降。2021
[9] Zeiler, M. “ADADELTA:一种自适应学习率方法”,2012年。 arXiv:1212.5701v1 [cs.LG]
[10] Kingma, D., Ba, J. “Adam:一种用于随机优化的方法”,2014年。 arXiv:1412.6980 [cs.LG]
首次发布于https://www.makerluis.com,2023年12月5日。