机器学习从入门到放弃:梯度算法的优化

一、前言

  在真正的工程应用中,模型训练也许更为重要,特别是对于生成式模型来说,无论是 NLP 领域或者 GNN 领域所产生的内容是否适用,在直觉上我们可以可以清晰的辨别。但是具体在模型上我们怎么调整就是一个类似黑盒的概念,我们一般通过更多的特征向量,和更深层次的神经网络架构来实现我们所期望的内容。但是这也意味着更多的计算,而面对算力有限的情况,如何进行优化,则显得尤为重要,因为当一个 epoch 的训练时长久到一个星期才能完成的时候,你根本无法忍受。

  之前上一篇文章,已经介绍过梯度下降算法了,用梯度下降可以训练神经网络,可以说是最有效的方法了。但是标准的梯度下降法是很难实现的,因为标准的梯度下降法它要求的计算量是非常巨大的,靠我们现在的计算机是没有办法承担这个计算量的,所以我们需要对梯度下降算法进行优化。让它用更少的计算量,实现差不多的计算效果,这样才行。那么优化梯度下降法大概有两个思路,第一个是调整神经网络的结构,比如说增加池化层啊,或者是用 Dropout 的方法等等;第二种方法就是直接在梯度下降算法上,这个算法本身上进行优化。我们先关注第二个问题,学习一下在梯度算法的优化上,究竟有哪些通用的方法。

 

二、SGD - 随机梯度下降

  在梯度下降法中,我们求解的梯度就是损失函数 LOSS 的梯度。比如用交叉熵计算损失函数,它的表达式就是长这样子:

  其中这里的连加符号就代表了训练集里面的每一个都计算一遍,然后算出损失函数的值,然后在更新梯度。但是训练集动不动就上万个数据,而且每个数据的维度可能还非常多。比如一张图片,它的每一个像素就有三个维度,RGB 三通道。如果神经网络的层数越多,那么这里的计算量就成指数倍的增长,计算量也肯定大的惊人。这还只是训练了一次的情况,如果想要更好的结果,我们必须重复这样的计算步骤无数次。所以如果按照数学公式,一板一眼的进行计算,那么我们模型训练就会花费非常多的时间。如果不对梯度下降算法进行优化,神经网络的应用只能停留在理论层面。那么怎么进行优化呢?思路其实也比较简单,第一就是减少每一次训练的计算量,第二个就是优化一下梯度下降的路径,让其用更少的步数,更快的逼近极值点。

  而第一个思路中的减少每一次训练的计算量,就是随机梯度下降法了。上面的交叉熵计算损失时,下面还有个取平均值的 N:

  这里的 N 能省掉是因为,最后我们除与不除其实都不影响我们最后的求极值。但是只有从求期望的角度出发,去理解,我们才能去对其进行优化。期望是什么?我们应该都了解的,就是可以摆脱样本的具体数据,又能代表整个样本的特征值。从这个角度,我们在计算的时候,用了多少样本数据,是否覆盖了整个样本空间,就不再是一个关键性要素了。这里有点类似抽样统计的概念,比如说我们需要知道全地球男性和女性的比例,是否需要对整个地球上的样本进行统计呢?还是说我们进行一部分的抽样,统计出来的结果就可以逼近真实的比例值呢?依着这个思路,我们使用小批次的样本,来并发的计算梯度,就能大大的缩短训练时长了。

  在机器学习中,"epoch"(中文称为"迭代轮次"或"周期")是训练算法使用整个训练数据集的一次完整迭代。训练数据集通常被分成小批次(batches)来进行处理,每个小批次包含一组训练样本。在每个 epoch 中,算法会使用所有的训练数据集样本来更新模型的权重和参数,以最小化损失函数。

  在一个 epoch 内,模型会将每个小批次的数据输入到算法中,计算损失,并通过反向传播(backpropagation)来更新模型参数,以提高模型的性能。这个过程会重复多次,直到整个训练数据集被完整地处理了一次,即完成了一个 epoch。

  通常情况下,训练一个机器学习模型需要多个 epoch,而不仅仅是一个。通过多次迭代数据集并更新模型,模型可以逐渐学习到数据的特征和模式,从而提高性能。epoch 的数量是一个超参数,需要根据具体的问题和数据来调整,以找到适当的训练次数,以获得最佳模型性能。过多的 epoch 可能会导致过拟合,而过少的 epoch 则可能导致模型性能不足。

  在随机梯度算法中,算法研究也对其进行了证明,也就是最后求得的极值,达到的误差是如下:

  在正常情况下,标准的梯度算法它的收敛速度肯定要比随机梯度下降要更快的,但是实际上标准的梯度算法再快也不会快过 1/K , 也就是说计算了全量的数据才更新一次参数,它带来的收益和随机梯度下降差不多,但是时间却比随机梯度下降花销更多,那还不如直接用随机梯度下降。下图显示了当一个 epoch 中 batch size = 1 时,参数更新会比较慢,而 batch size = 1000 时由于可以并行计算更新参数,会发现时间花费会更少。

三、SGDM - 动量法

  SGD 的优势在于它的计算效率,因为它每次只使用一个样本进行梯度计算,这对于大规模数据集是非常有用的。然而,由于随机性,SGD 的更新可能会有一些不稳定性,损失函数可能会在训练过程中波动。所以在 SGD 的基础上我们还可以进行进一步的优化。

  首先我们先针对梯度下降的方程来进行一些参数的说明:

  • Xt:入参,也就是训练的输入数据
  • θt:神经网络需要更新的参数
  • Yt:神经网络输出的东西,如果是分类任务则是 one-hot label,如果是生成式任务则是生成的数据,如图像,文字等等
  • hatYt:则是你的期望,一般来说可以当成是测试数据集,而 Yt 和 hatYt 之间的差值组成的函数就是损失函数 LOSS

  在梯度下降的求解过程中的公式是:θt+1 = θt - lr * ▽,我们通过学习率去到达下一个点,然后再根据这个点的梯度去更新参数,从而让 LOSS 损失函数能找到 minima 一个最小值。

  如果这个过程如下,中间红色的点就是这个 LOSS 的 minima,而我们初始函数选择在了边缘的区域时,我们的更新路径就是红色的折线:

  在红色折线的没一点中也就是每一步我们所算出来的 θt+1 也就是需要更新的参数,而更新的方向则是根据红色的点在类似同心椭圆上的切线方向。那么我们可以发现在 X 轴上梯度更新方向是一直向右的,但是在 Y 轴上是忽上忽下。如果我们能忽略掉Y轴上的移动,而保持 X 轴的一路向右,那么就可以减少反复的震荡,和很多无意义的计算了。

  所以在这里的优化思路上,就是加上一个  Momentum 动量的概念。在深度学习和优化算法中,"momentum"(动量)是一种用于改进梯度下降算法的技术。它的目标是加速训练过程,特别是在处理复杂、高维度的问题时,有助于避免陷入局部极小值或收敛过慢的问题。

  动量的基本思想是在更新参数时不仅考虑当前时刻的梯度信息,还考虑之前的更新方向。它通过引入一个称为"动量项"的概念来实现这一点。

  动量项是一个累积梯度的指数加权平均。在每次迭代中,它考虑了当前的梯度和之前动量项的贡献,然后用于更新模型参数。这有助于减少参数更新的震荡和方向变化,使得梯度下降更加平滑和稳定。具体来说,动量更新规则如下:

  1. 在每次迭代中,计算当前时刻的梯度。
  2. 计算动量项,将当前梯度与之前的动量项相结合,得到新的动量项。
  3. 使用新的动量项来更新模型参数。

  如下图,在每次更新参数中,我们都计算出了梯度的方向和大小,注意看就是每一个 θ 点的红色箭头--->,因为梯度显示是LOSS增大的方向,所以我们需要往他的反方向更新,SGD就是直接在红色的箭头上进行更新梯度下降的,但是加上了动量的概念后,它还会考虑之前的梯度方向,也就是绿色划线---,他们的合并指向才是 SGDM 更新的下一个 θ 的方向。

  每一个点计算出来的梯度都需要去考虑上一个梯度的计算结果也就是如下的公式表达,因为如果当前某个 v 的计算结果梯度是 0 的时候,它可能并不会停止下来,依靠之前积累的动量能让神经网络继续学习下去冲出一个 local minima。其实这里有点类似于加权移动平均法,大家可以自行去了解比较一下。

  动量项的引入使得梯度下降具有惯性,有助于模型在参数空间中更快地前进,并且有助于跳过一些局部最小值,从而加速了收敛过程。通常,动量的值介于0和1之间,它可以被视为梯度的衰减系数。常见的动量值包括0.9、0.99等。选择合适的动量值通常取决于具体的问题和实验。除了上面提到的对梯度的动量使用类似加权移动平均来修正外,还有 Nesterov 算法也是去对梯度的方向进行修正,大家也可以去了解一下,这里就不做介绍了。

 

 

 四、AdaGrad - 自适应学习率

  我们还是需要聚焦在这个公式上:θt+1 = θt - lr * ▽。这里的学习率 lr 是固定的,我们上一篇中介绍梯度下降的时候说这是一个比较小的值,但是在现实中我们真的只使用一层不变的学习率就能保证快速找到方向吗?还是以上面的例子:    

  当红色的点更新到最内圈的椭圆时,也就是梯度下降快速更新到谷底了,依着之前的动量它会依然有一个向右运动的趋势,但是当学习率比较小的时候你会发现它只会在谷底慢慢向终点蠕动,并且有可能永远都到达不了 minima 这个点。

  所以我们的优化思路则是,能够有自适应调节的学习率让谷底的时候能后步长更大一些,让右探索,更快的的达到 minima。

  AdaGrad 的主要目标是解决传统梯度下降算法中需要手动调整学习率的问题,它根据每个参数的历史梯度信息来自动调整学习率。AdaGrad 的主要思想如下:

  1. 对每个模型参数初始化一个累积梯度平方项(accumulator),通常初始化为0。
  2. 在每个迭代中,计算当前时刻的梯度。
  3. 将梯度的平方项添加到对应参数的累积梯度平方项中。
  4. 使用学习率除以参数的累积梯度平方根来更新模型参数。

  AdaGrad 的更新规则可以表示为以下公式:

θ(t+1) = θ(t) - (η / √(G(t) + ε)) * ∇L(θ(t))
  • θ(t) 是参数在第 t 次迭代的值。
  • η 是学习率,通常是一个小正数,用于控制参数更新的步长。
  • G(t) 是参数的累积梯度平方项,用于自适应调整学习率。
  • ε 是一个小正数,用于防止分母为零,通常设置一个很小的值,如1e-8。
  • ∇L(θ(t)) 是损失函数关于参数 θ(t) 的梯度。

  在上面的例子中我们把梯度分解成 Y 轴和 X 轴方向,会发现如果 lr 除以一个 G(t) 也就是累积梯度平方项,当点处于内层盆地的时候,Y 轴方向的梯度因为震荡的原因,所以上下跳动很大,从而导致分母越大,Y轴上的学习率则越小,从而导致更新的梯度也会越来越小。而 X 方向因为每次更新从外一圈的椭圆到内一圈的椭圆,本身梯度更新变化就很小,所以当小到一个程度,进行蠕动前进的情况,小分母反而会放大学习率,让梯度以一个较大的步长往右进行探索,更新梯度。

 

四、RMSProp -  变种自适应学习率

  RMSProp(Root Mean Square Propagation)是一种自适应学习率的优化算法,用于训练机器学习模型,特别是在深度学习中的神经网络。RMSProp 旨在解决传统梯度下降算法中需要手动调整学习率的问题,并改进了 AdaGrad 算法的一些缺点。

  RMSProp 的更新规则可以表示为以下公式:

θ(t+1) = θ(t) - (η / √(G(t) + ε)) * ∇L(θ(t))

  看到这里,你有可能会说:什么嘛,这个不就是上面的 AdaGrad 嘛,简直一模一样好吗。确实我刚看的时候也发现这者并无差别,优化的出发点都是为了计算自适应的学习率,但是唯一微小的差距就是 G(t) 的计算,RMSProp计算是:

  • G(t) 是参数的累积梯度平方项,通过引入衰减因子平滑历史梯度信息

  它类似上面的 momentum 的算法,就是使用移动加权平均的方法去计算 Vt ,然后再平方开根号,这样做的目的都是为了在某个维度的梯度上进行学习率的调整,让梯度大的学习率适当小一点减少震荡,让梯度小的方向增加步长,可以快速跨过平坦的"盆地"。RMSProp 的优势在于它能够自适应地为每个参数计算学习率,通过引入衰减因子,它能够平滑历史梯度信息,避免 AdaGrad 中学习率过度衰减的问题。这使得 RMSProp 在训练中能够更好地平衡自适应性和稳定性。

  RMSProp 是深度学习中常用的优化算法之一,通常用于训练神经网络。它的性能表现良好,尤其适用于处理非平稳或具有异质性梯度的问题。虽然 RMSProp 改进了 AdaGrad 的一些问题,但后续算法如 Adam 进一步扩展了 RMSProp 的性能

 

 

五、Adam - 自适应学习率加动量优化

  Adam(Adaptive Moment Estimation)是一种自适应学习率的优化算法,用于训练机器学习模型,特别是在深度学习中的神经网络。Adam 算法结合了动量(Momentum)和 RMSProp 算法的思想,以更高效地优化模型参数。Adam 的主要思想如下:  

  1. 对每个模型参数初始化两个累积项:第一个累积项 m 用于估计梯度的一阶矩(均值),第二个累积项 v 用于估计梯度的二阶矩(均方差)。初始时,它们都被设置为0。
  2. 在每个迭代中,计算当前时刻的梯度。
  3. 更新第一个累积项 m 和第二个累积项 v,通过指数加权平均来平滑梯度信息。
  4. 使用修正后的一阶矩估计 m 和二阶矩估计 v 来计算参数的修正梯度。
  5. 使用修正梯度来更新模型参数。

  Adam 的更新规则可以表示为以下公式 :

m(t) = β₁ * m(t-1) + (1 - β₁) * ∇L(θ(t))
v(t) = β₂ * v(t-1) + (1 - β₂) * (∇L(θ(t))^2)
m_hat = m(t) / (1 - β₁^t)
v_hat = v(t) / (1 - β₂^t)
θ(t+1) = θ(t) - η * m_hat / (√(v_hat) + ε)

  其中:

  • θ(t) 是参数在第 t 次迭代的值。
  • η 是学习率,通常是一个小正数,用于控制参数更新的步长。
  • β₁ 和 β₂ 是控制一阶矩 m 和二阶矩 v 的衰减因子,通常接近于1,例如,0.9 和 0.999。
  • ∇L(θ(t)) 是损失函数关于参数 θ(t) 的梯度。
  • ε 是一个小正数,用于防止分母为零,通常设置一个很小的值,如1e-8。
  • t 是当前迭代的次数。

  你可以发现,其实 Adam 就是上面的 SGDM + RMSProp 的优化算法

  这里 adam 还引入了衰减因子的概念,在计算引入衰减因子是为了平滑和稳定梯度估计的过程,以提高算法的性能。Adam 使用两个累积项(一阶矩 m 和二阶矩 v)来估计梯度的均值和方差,这些累积项在每个迭代中都会进行指数加权平均(或者是移动加权平均)。衰减因子控制了累积项的衰减速度,使其不受历史梯度信息的过大影响。就是说很久之前的梯度对当下时刻求得的梯度影响应该更小,而最近的梯度应该应该增大,那么你可以发现 β 随着迭代的次数增多,mt_hat 的值是逐渐逼近 mt 的,这里就是所谓的”偏差修正“。

  具体来说,衰减因子 β₁ 和 β₂ 是用来控制累积项 m 和 v 的衰减速度的超参数。随着 t 增大它们通常接近于1,但不等于1。衰减因子小于1会导致较早的梯度信息被快速遗忘,从而使累积项对历史梯度信息的影响减小,增加了算法的稳定性。如果没有衰减因子,累积项可能会保留过多的历史信息,导致不稳定的更新。

  但是在计算 mt_hat 和 vt_hat 的时候有个小细节就是,当 t 比较小的时候,分母 (1 - β₂^t) 确实会接近于0,而不是接近于1。这可能导致 v_hat 在初始阶段被放大,为了解决这个问题,Adam 算法通常在分母中添加一个小正数 ε,以防止分母为零。这个小正数通常设置为一个很小的值,如1e-8,以确保分母不会过小。这个修正后的分母将保证在开始的迭代阶段 v_hat 不会被放大。

  更新规则中的修正后的分母如下所示:

v_hat_corrected = v_hat / (1 - β₂^t + ε)

   除了上面提到的优化思路,其实 Adam 还有很多变种。自从 2015 年 adam 提出以来,似乎没有任何颠覆性的优化算法出来,更多的是在 Adam 上基础上进行更多的优化和补充,比如是否需要 warm-up,具体可以参考 RAdam 等。

 

六、总结

  在梯度下降中有这么多的算法可以给我们选择,实际上 pytorch 都给我们封装好了普遍常用的一些优化。但是具体用到哪个还是需要根据具体的数据,还有工厂场景去确定的。SDG 不一定效果就会比 Adam 的差,而 Adam 也不一定会比其他算法更快收敛。优化算法的选择上,我个人总结了一下各个算法的适用场景:

  • 普通梯度下降 (Vanilla Gradient Descent): 适用于凸优化问题,通常需要较小的学习率。
  • Adam(自适应矩估计): 通常表现良好,对于大多数问题都是一个不错的选择,但需要调整超参数。
  • RMSprop: 适用于非平稳问题,可以自适应地调整学习率。
  • Momentum: 通过加入动量来帮助算法更快地收敛。
  • Nesterov Accelerated Gradient (NAG): 是Momentum的一种变体,通常更快收敛。

  在实际问题上进行实验和调优是选择优化算法的最佳方法。可以尝试不同的算法和超参数组合,通过交叉验证或验证集来评估它们的性能。

 

posted @ 2023-09-15 19:32  Blackbinbin  阅读(185)  评论(0编辑  收藏  举报