Optimization

优化(Optimization) #

机器学习和深度学习中的 优化问题(Optimization) 指的是在给定的模型结构和数据集下,通过调整模型的参数,使目标函数(Objective func-tion, or criterion)达到最小化 或最大化的过程。目标函数通常衡量模型预测值与真实值之间的偏差,例如均方误差或交叉熵。在优化过程中,参数更新依赖于目标函数对参数的梯度信息,通过迭代计算逐步逼近最优解。

  • 凸优化(Convex Optimization) 是指目标函数为凸函数的优化问题,凸函数满足以下性质:
    • 任意两点之间的连线上的函数值不会超过这两点的函数值。
    • 数学形式:对于任意 \(x_1, x_2 \in \mathbb{R}^n\) \(\theta \in [0, 1]\) ,有 \[ f(\theta x_1 + (1-\theta)x_2) \leq \theta f(x_1) + (1-\theta)f(x_2) \]
    • 目标函数特点:
      • 单一的全局最优解(Global Minimum)
      • 常见目标函数形式:二次函数(如 \(f(x) = x^2\) )、对数函数、指数函数等。
      • 使用简单的梯度下降或更高效的二阶方法(如牛顿法)即可快速收敛
  • 非凸优化(Non-Convex Optimization) 非凸优化是指目标函数为非凸函数的优化问题,非凸函数可能存在多个局部最优解(Local Minimum),不满足凸函数的性质。
    • 目标函数特点:
      • 通常为复杂的多峰形状,可能存在多个局部最优解、鞍点甚至平坦区域
      • 深度学习中的损失函数(如交叉熵、均方误差)大多属于此类。
    • 解决策略:
      • 启发式算法: 使用随机梯度下降(SGD)及其变种(如 Adam、RMSProp)通过随机性帮助跳出局部最优解。
      • 正则化技巧: 添加 L1/L2 正则项以平滑损失函数,减少极值点的数量。
      • 预训练与迁移学习: 通过初始化参数靠近全局最优区域来提高收敛效率。

基于梯度的优化(Gradient-Based Optimization) #

在函数 \(y = f(x)\) 中(其中 \(x\) \(y\) 都是实数),导数 \(f'(x)\) (或者表示为 \(\frac{\partial y}{\partial x}\) ) 表示函数在点 \(x\) 处的斜率(Slope)。它描述了输入 \(x\) 的一个微小变化如何引起输出 \(y\) 的相应变化,用以下公式近似表示: \[ f(x + \epsilon) \approx f(x) + \epsilon f'(x) \] 导数在函数优化中非常有用,因为它指示了如何调整 \(x\) 以使 \(y\) 取得小幅改善。例如,当我们沿着导数的反方向移动时,函数值会减小,即: \[ f(x - \epsilon \cdot \text{sign}(f'(x))) < f(x) \] 这种基于导数的优化技术被称为梯度下降法(Gradient Descent)

关键概念 #

  1. 临界点与极值
    • \(f'(x) = 0\) 时,称为临界点(Critical points,或者 stationary points)
    • 局部极小值(Local minimum):周围点中 \(f(x)\) 最小。
    • 局部极大值(local maximum):周围点中 \(f(x)\) 最大。
    • 鞍点(Saddle points):既非局部极小值也非局部极大值的临界点,其中正交方向的斜率(导数)全部为零(临界点),但不是函数的局部极值。在多维空间中,不必具有 0 的特征值才能得到鞍点:只需具有正和负的特征值即可。
    • 全局极小值(Global minimum):函数 \(f(x)\) 在整个定义域内的最小值
    • 平坦区域(Plateaus):平坦区域是指目标函数的梯度几乎为零的区域,网络在这些区域中移动缓慢,几乎没有任何有效的方向引导优化过程。
    • 悬崖结构区域(Cliffs):悬崖结构区域指的是目标函数中梯度变化非常剧烈的区域,即梯度几乎呈现出极为陡峭的下降趋势。在这种区域内,损失函数对于某些参数的变化非常敏感,导致小的参数更新可能引起损失值的剧烈波动。
  2. 多维输入优化:当函数有多个输入( \(f: \mathbb{R}^n \to \mathbb{R}\) ),优化需要使用梯度(Gradient)的概念。梯度是包含所有偏导数的向量,定义为: \[ \nabla_x f(x) = \left[\frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, \dots, \frac{\partial f}{\partial x_n}\right] \] 在多维空间中,临界点是所有梯度分量为零的点,即 \(\nabla_x f(x) = 0\)
  3. 学习率的选择:学习率 \(\epsilon\) 决定了每一步更新的幅度,常见选择方式包括:
    • 固定的小常数。
    • 使用线搜索(Line Search),在多种步长中选择目标函数值最小的步长。
  4. 梯度下降的收敛:当梯度的所有梯度的分量接近零时,梯度下降算法收敛。实际上,由于优化问题的复杂性,尤其是在深度学习中,我们通常寻找“足够低”的函数值,而非严格的全局极小值

全批量梯度下降(Batch Gradient Descent) #

Batch Gradient Descent 是一种优化算法,每次使用整个数据集来计算目标函数的梯度,并基于梯度更新模型参数。这种方法适用于目标函数是所有样本损失的平均值或总和的情况。每次迭代计算所有训练样本的损失函数梯度。公式为: \[ \theta = \theta - \eta \cdot \nabla J(\theta) \] 其中, \(\nabla J(\theta)\) 是基于整个数据集的梯度。

工作流程一般为:

  1. 初始化模型参数 \(\theta\) 为随机值或设定初值。
  2. 重复以下步骤,直到达到停止条件(如梯度足够小或迭代次数用尽):
    • 计算当前所有样本的目标函数值和梯度 \(\nabla J(\theta)\)
    • 使用梯度更新模型参数: \[ \theta = \theta - \eta \cdot \nabla J(\theta) \]
  3. 输出最终优化的参数。
  • 优点
    1. 精确性高:每次更新都基于完整的数据集,提供了目标函数梯度的精确估计,使得更新过程稳定可靠。
    2. 容易收敛到局部或全局最优:因为梯度估计噪声较小,参数更新方向更明确。在凸优化问题中,Batch Gradient Descent 的收敛轨迹通常表现为朝向最优解的一条平滑路径,梯度的更新方向明确,不会因为噪声而偏离轨道。但是由于梯度计算使用了整个数据集,优化轨迹通常稳定地沿着梯度方向下降,容易陷入一个局部最优点或停留在鞍点上。
  • 缺点
    1. 计算资源消耗大:每次迭代需要对整个数据集计算梯度,在数据量大时计算成本高,不适合分布式或在线训练。
    2. 存储限制:对于大规模数据集,可能需要更多的内存或存储资源来一次性加载数据。
    3. 收敛速度慢:尤其在每次迭代中,如果数据集中样本的梯度信息存在冗余,则更新过程可能很低效。
  • 全批量梯度下降(Batch Gradient Descent)代码实现
    import numpy as np
    # 定义目标函数及其梯度
    def loss_function(w, data):
        """目标函数: f(w) = 0.5 * (w - 3)^2"""
        return 0.5 * np.sum((data - w) ** 2)
    
    def gradient(w, data):
        """目标函数的梯度: f'(w) = w - 3"""
        return np.sum(data - w)
    
    # Batch Gradient Descent 优化过程
    for epoch in range(epochs):
        # 计算梯度
        grad = gradient(w, data)
        # 更新参数
        w -= learning_rate * grad / len(data)  # 除以数据集大小,以计算平均梯度
        # 记录损失值
        loss = loss_function(w, data)
    

随机梯度下降(Stochastic Gradient Descent) #

SGD是一种优化算法,用于通过梯度下降更新模型参数,以最小化损失函数。它的核心思想是在每次迭代中,随机选择一个样本计算梯度,而不是使用整个数据集。这种方式极大地降低了每次更新的计算成本。更新公式为: \[ \theta = \theta - \eta \cdot \nabla J(\theta; x_i, y_i) \]

  • 优点
    1. 高效性:单次梯度计算只涉及一个样本,计算速度快,对内存需求低。
    2. 跳出局部最优解:随机梯度的噪声可以避免陷入平滑函数中的局部最优点(local minima),尤其适合非凸优化问题。
    3. 在线学习能力:在模型需要不断更新时(如实时场景),SGD可以随着数据流实时调整参数。

Note: 随机梯度下降在每次迭代中仅使用一个样本计算梯度,因此梯度估计会带有噪声。这种噪声主要表现为梯度方向的不确定性,使得优化过程中的参数更新具有一定的随机性和波动性。随机噪声使得优化路径不完全按照损失函数表面的梯度方向前进,而是以一种“抖动”的方式探索参数空间。当优化路径接近某个局部最优点时,全批量梯度可能因所有样本的梯度方向一致而停留在该点;而随机梯度的波动可能使路径偏离局部最优,继续搜索全局最优解

在深度学习中的目标函数通常具有非凸性质,随机梯度的噪声可以帮助模型训练找到性能更优的解,从而避免陷入次优状态。此外研究表明,在高维空间中,随机梯度的波动尤其有助于突破鞍点,因为鞍点在高维空间中比局部最优点更常见。

  • 缺点
    1. 梯度估计噪声大:由于每次迭代仅基于单个样本,梯度方向可能偏离真正的最优方向,导致优化过程不稳定。
    2. 收敛速度慢:需要更多迭代次数才能达到较优解,与Batch Gradient Descent相比,收敛速度可能较慢
    3. 对学习率敏感不适当的学习率可能导致震荡或过早停止收敛,往往可能需要更小的学习率( \(\eta \) )或动态调整以避免振荡。因此学习率需要仔细调节或动态调整。
  • 随机梯度下降(Stochastic Gradient Descent)代码实现
    import numpy as np
    
    # 定义目标函数及其梯度
    def loss_function(w):
        """目标函数: f(w) = 0.5 * (w - 3)^2"""
        return 0.5 * (w - 3) ** 2
    
    def gradient(w):
        """目标函数的梯度: f'(w) = w - 3"""
        return w - 3
    
    # SGD 优化过程
    for epoch in range(epochs):
        # 随机选取数据点
        idx = np.random.choice(len(data), batch_size=1, replace=False)
        x_sample = data[idx]
        # 计算梯度
        grad = gradient(w)
        # 更新参数
        w -= learning_rate * grad
        # 记录损失值
        loss = loss_function(w)
    

小批量随机梯度下降(Minibatch Stochastic Gradient Descent) #

Minibatch SGD 是一种在每次迭代中使用一小批数据(称为 Minibatch)计算梯度并更新参数的优化方法。它结合了 Batch Gradient Descent 和 Stochastic Gradient Descent 的优点,能够在计算效率和收敛稳定性之间找到平衡

  • 核心工作流程一般为:
    1. 数据划分: 将训练数据集分成若干小批量(Minibatches),每个批次包含 \(m\) 个样本。 Minibatch 大小 \(m \) 一般为 16, 32, 64, 128,根据硬件资源和模型大小调节。
    2. 梯度计算与更新:对于每个 Minibatch ( \(X_{minibatch}, Y_{minibatch} \) ),计算目标函数在该批次上的梯度并更新参数: \[ \theta = \theta - \eta \cdot \nabla J(\theta; X_{minibatch}, Y_{minibatch}) \] 较大的 Minibatch 通常需要较大的学习率。可结合学习率衰减策略(如 Step Decay、Exponential Decay、Warm Restarts)来平衡收敛速度与准确性。
    3. 迭代更新: 对每个 Minibatch 重复上述步骤,直到遍历整个数据集(称为一个 epoch)。根据收敛情况执行多个 epoch。

Note:为了确保梯度估计的无偏性(unbiasedness),Minibatch的样本必须独立随机抽样。如果数据集的自然排列存在相关性(如医疗数据按患者排序),在选择Minibatch前需要对数据集进行随机打乱(shuffle)。所以我们需要在每个 epoch 开始时随机打乱数据,避免梯度计算因样本顺序产生偏差。

  • Minibatch大小的选择
    • 梯度估计的准确性:批量越大,梯度估计越精确,但收益递减(标准误差与样本数量的平方根成反比)。
    • 计算资源限制:较小的批量可能导致多核硬件或GPU利用率不足;较大的批量需要更多内存。
    • 硬件优化:GPU通常在批量大小为2的幂(如32, 64, 128)时性能最佳。
    • 正则化效果:较小批量可以引入噪声,具有正则化作用,但需要调整较小的学习率。

Note:Minibatch(小批量)在计算上具有优势(相比于大批量)的原因是,小批量数据(如 32 或 64 个样本)可以被完全加载到 GPU 中进行高效的并行计算。GPU 的计算效率在处理适量数据时达到峰值。与此同时,在大批量中需要对更多样本进行梯度计算和聚合,梯度计算过程更复杂,占用更多时间。例如,矩阵计算的开销随数据规模呈非线性增长

  • 训练速度比较

    • SGD 的单次更新虽然快,但更新频率极高,导致整体时间长。
    • Minibatch SGD 在更新频率和计算量之间取得了平衡,往往在一个 epoch 的整体速度最快,是实际应用中的首选。
    • Batch Gradient Descent 由于每次更新都需要遍历整个数据集,在大规模数据上效率低。
  • 小批量随机梯度下降(Minibatch Stochastic Gradient Descent)代码实现

    import numpy as np
    # 定义目标函数及其梯度
    def loss_function(w):
        """目标函数: f(w) = 0.5 * (w - 3)^2"""
        return 0.5 * (w - 3) ** 2
    
    def gradient(w):
        """目标函数的梯度: f'(w) = w - 3"""
        return w - 3
    
    # Mini-batch SGD 优化过程
    for epoch in range(epochs):
        # 每个 epoch 中分成多个 mini-batch
        np.random.shuffle(data)  # 打乱数据集
        for i in range(0, len(data), batch_size):
            # 从数据集中取出一个 mini-batch
            x_sample = data[i:i+batch_size]
            # 计算梯度
            grad = gradient(w)  # 这里假设梯度是目标函数的梯度,针对每个样本计算
            # 更新参数
            w -= learning_rate * grad
    

优化深度神经网络的挑战(Challenges in Neural Network Optimization) #

优化深度神经网络面临诸多挑战,因为网络的目标函数通常是非凸函数,即包含多个局部极值点、鞍点和平坦区域等复杂结构。此外,即便是凸优化问题,也会因为高维度和数据特性而复杂化。


局部极小值(Local Minima) #

深度神经网络的目标函数(例如分类或回归问题中的损失函数)通常是非凸的。非凸函数意味着它可能有多个局部极小值、鞍点、以及平坦区域。深度网络的复杂结构使得这些局部极小值的位置和数量变得更加复杂和难以预测。虽然局部极小值可能在低维问题中更常见,但在高维空间中,局部极小值的影响通常被更复杂的平坦区域或鞍点替代。

  • 局部极小值的影响
    1. 训练停滞:局部极小值会导致优化算法的停滞,尤其是梯度下降算法。当优化器在一个局部极小值附近时,梯度变得非常小或几乎为零,参数更新几乎无法继续,导致训练过程无法继续进行。
    2. 降低训练效率:即使局部极小值的影响并不完全阻止训练过程,停滞在局部极小值附近也会显著增加训练时间。优化器可能需要很长时间才能逃脱这些局部极小值,增加了收敛的时间。
    3. 全局最优解的错失:如果优化器陷入局部极小值,则无法继续向全局最优解前进。尤其在复杂的神经网络中,局部极小值可能位于一个比较低的损失值附近,因此模型在训练过程中可能会错过更优的解
  • 优化过程中诊断局部极小值问题:
    • 通过实时监控训练和验证损失曲线,可以快速定位优化是否正常进行。如果损失曲线在训练过程中停滞不前,长时间保持在一个较高的值,我们可以推测优化遇到了局部最小值的问题。

平坦区域和鞍点(Plateaus and Saddle Points) #

深度神经网络包含大量的参数,尤其是深度模型中,每一层的参数都会在目标函数的定义中增加维度。随着维度的增加,非凸优化问题的复杂性大大增加。鞍点和梯度为零的平坦区域在高维空间中更为常见,且它们在这些维度上的“上升”和“下降”趋势很容易相互作用,导致目标函数在这些点附近的行为变得难以预测。鞍点的存在表明,在优化过程中,目标函数的某些方向上可能是上升的,而其他方向上可能是下降的。这种局部结构使得优化过程充满不确定性。

  • 平坦区域和鞍点的影响
    1. 梯度更新变慢:在平坦区域,梯度接近零,意味着优化算法的更新非常缓慢。无论是在平坦区域还是鞍点附近,梯度下降算法(如 SGD)都可能因为梯度过小而在这些区域停滞不前,导致收敛速度变慢,甚至完全停滞。
    2. 优化过程的非稳定性:鞍点区域的存在导致梯度下降算法可能会 “卡在”某些不理想的位置,无法有效地向全局最优解逼近。这些点的复杂几何性质使得简单的梯度下降方法无法高效逃离它们,因此可能长时间停留在鞍点区域或平坦区域,无法有效继续优化。
    3. 参数更新难度增加:在优化过程中,深度神经网络的参数更新依赖于梯度信息。如果网络被困在平坦区域或鞍点,它将无法获得有效的梯度信息,从而使得参数的更新难以进行。

Note:常见应对局部极小值,平坦区域和鞍点的解决方法

  1. 使用随机初始化:随机初始化可以帮助模型从不同的起点开始优化过程,从而增加逃离局部极小值的可能性。如果某次训练陷入局部极小值,其他随机初始化可能会找到更优的解。
  2. 引入动量(Momentum):动量方法通过累积之前的梯度方向,可以加速沿低曲率方向的收敛,并帮助优化器跨越较浅的局部极小值。
  3. 调整学习率:学习率过小可能导致优化器在鞍点附近徘徊;适当增大学习率有助于跳出鞍点。
  4. 自适应优化算法:Adam 和 RMSProp 等算法通过对梯度变化的跟踪,能够更快地从鞍点中脱离。

梯度爆炸(Exploding Gradients) #

梯度爆炸(Exploding Gradients) 是指在反向传播过程中,梯度值在某些层中异常增大,导致权重更新时出现非常大的步长,从而导致训练过程中参数的值急剧增大,甚至溢出。

  • 梯度爆炸的原因
    1. 链式法则:在深度神经网络中,反向传播过程通过链式法则计算梯度。当网络深度较大时,梯度的计算会依赖于多个层的梯度乘积。如果某些层的梯度值较大,它们会在反向传播过程中不断放大,导致最终梯度值急剧增大,这就造成了梯度爆炸。
    2. 不合理的权重初始化:如果模型的权重初始化不当(如权重值太大),会使得每一层的激活值非常大,导致反向传播时梯度的放大效应
    3. 不适当的激活函数:某些激活函数,如 ReLU,在特定情况下可能间接导致梯度爆炸。尤其是当网络的输入值过大时,ReLU 激活函数会输出较大的值,这样在反向传播时,梯度可能会累积放大。如果网络较深且没有采取有效的梯度抑制措施,这种累积放大会导致梯度爆炸。

Note:激活值本身并不会直接被用于反向传播,而是激活值的导数和前一层的梯度共同作用于权重的更新。在反向传播中,计算每一层的梯度时需要用到激活函数的导数,而不是激活值本身。激活值是当前层权重和偏置作用下的输出,作为下一层的输入,间接影响了梯度计算。如果激活值较大,可能导致下一层的输入过大,从而使激活函数进入饱和区,导致梯度消失;或者在某些情况下,激活函数导数恒定(如 ReLU 的正区间),结合较大的权重累积,会导致梯度放大的问题

  • 梯度爆炸的影响
    • 梯度爆炸会导致模型参数的值变得异常大,进而导致数值溢出或数值不稳定。具体表现为:训练过程中,损失函数的值出现波动甚至爆炸,模型的参数值可能变得非常大,难以更新,甚至会导致训练停止。
    • 更严重的是,梯度爆炸可能完全破坏训练过程,导致模型无法收敛,并可能使得计算资源的浪费加剧。
  • 优化过程中诊断梯度爆炸问题:
    • 如果损失在训练中突然暴增到无穷大(NaN 或 Inf),我们可以推测优化遇到了梯度爆炸的问题。
    • 如果权重值快速变大,远超合理范围,或者发现激活值过大(尤其是 ReLU 输出远超预期范围)时,也可以推断出现了梯度爆炸。

Note:常见应对梯度爆炸的解决方法

  1. 自适应优化算法:自适应优化算法(如 RMSProp、Adam)能够缓解梯度爆炸问题,这些优化器会动态调整学习率,使梯度变化较为平稳。
  2. 正则化方法:在损失函数中加入权重惩罚项,限制参数的大小。限制梯度值的放大,防止权重过大。
  3. 权重初始化:恰当的权重初始化能够有效防止梯度过大:如 Xavier 初始化(适用于 sigmoid 或 tanh 激活函数),He 初始化(适用于 ReLU 激活函数)。
  4. 归一化技术:批量归一化(Batch Normalization)在每一层将激活值归一化,使其均值为 0,方差为 1。在更新前,将梯度归一化为固定尺度。缓解梯度爆炸,同时加速收敛。

梯度消失(Long-Term Dependencies and Gradient Vanishing) #

梯度消失问题发生在反向传播(backpropagation)过程中,当网络中的梯度值在传播时逐渐变小,最终接近零。由于梯度变得非常小,权重更新的步伐变得非常缓慢,导致训练变得非常困难甚至无法收敛。

  • 梯度消失的原因

    1. 链式法则:反向传播算法依赖链式法则来计算梯度,即通过每一层的梯度传递,最终得到损失函数对每个参数的梯度。然而,在深层神经网络或者长序列的网络中,梯度的传递通过多个层或时间步进行叠加。如果每一层或每一步的梯度值都小于1(通常是通过激活函数计算的),那么多次乘积会导致梯度指数级下降,最终变得非常小。
    2. 激活函数的性质:常见的激活函数(如sigmoid、tanh)会在其饱和区间(即输入非常大或非常小的时候)将梯度压缩到接近零。(e.g. 当输入值非常大或非常小时,sigmoid函数的梯度接近于零,这就导致了反向传播过程中梯度的快速衰减。)
    3. 深层网络和长时间序列:在深层神经网络(特别是RNN或LSTM)中,层数过多或时间步过长时,梯度必须沿着多个路径传播。这使得梯度在每一步都逐渐缩小,导致梯度几乎无法传递到网络的最早层,尤其是在处理长时间依赖时尤为严重。
  • 梯度消失的影响

    • 学习效率低下:当梯度消失时,网络的参数几乎不更新,尤其是接近输入层的参数。这样,网络在训练过程中无法有效地学习到有用的特征,特别是长时间依赖的特征(例如,RNN中用于记忆先前输入的长期依赖关系)。
    • 训练停滞或失败:对于长序列数据,梯度消失会导致模型无法捕捉到远程依赖关系。训练可能会停滞,模型的性能无法提高,导致训练过程失败或收敛到不理想的结果。
  • 优化过程中诊断梯度消失问题:

    • 如果损失缓慢下降甚至趋于平坦,优化进度显著减慢,我们可以推测优化遇到了梯度消失的问题。
    • 如果激活值过小或趋于饱和(如 Sigmoid 输出接近 0 或 1),或者权重更新幅度很小,长期接近初始值,也可以推断出现了梯度消失。

Note:常见应对梯度消失的解决方法

  1. 使用合适的激活函数
  2. 权重初始化:恰当的权重初始化能够有效防止梯度过大:如 Xavier 初始化(适用于 sigmoid 或 tanh 激活函数),He 初始化(适用于 ReLU 激活函数)。
  3. 归一化技术:批量归一化(Batch Normalization)在每一层将激活值归一化,使其均值为 0,方差为 1。在更新前,将梯度归一化为固定尺度。缓解梯度爆炸,同时加速收敛。

不精确的梯度(Inexact Gradients) #

在训练深度神经网络时,不精确的梯度指的是在某些情况下,计算出的梯度并不是目标函数的准确梯度,而是经过近似或估算的。这种不精确的梯度通常出现在基于小批量(mini-batch)梯度下降方法的训练过程中,也可能出现在其他优化方法中,如随机梯度下降(SGD)。这些不精确的梯度可能会影响优化的稳定性和收敛速度。

  • 不精确的梯度的原因

    • 小批量梯度计算:因为每个小批量的数据量有限,因此计算出的梯度只是目标函数在该小批量数据上的估计值,而不是在整个训练数据集上的真实梯度。通常情况下,计算出的梯度存在一些随机噪声。这意味着每次梯度更新时,参数并未沿着真正的目标函数梯度方向进行更新,而是被噪声扰动,导致偏离最优方向。这种偏差特别是在训练初期更为显著,因为网络的参数尚未经过充分训练,梯度计算更容易受到数据和噪声的影响。
  • 不精确的梯度的影响

    1. 收敛速度减慢:因为不精确的梯度会引入噪声和偏差,优化过程会变得不稳定,导致梯度下降算法的收敛速度减慢。具体来说,不精确的梯度可能导致模型参数朝错误的方向更新,甚至陷入局部最优解,而不能快速接近全局最优解。
    2. 过度依赖随机性:不精确的梯度也使得优化算法过度依赖随机性。虽然这种随机性在一定程度上能够帮助优化过程跳出局部最小值,但过度的随机性可能导致算法无法有效地找到全局最优解,并且在不稳定的情况下表现得更加差劲。

动量(Momentum) #

Momentum(动量)是一种基于梯度下降法(Stochastic Gradient Descent)的优化算法改进,它通过引入动量的概念,在参数更新中融入历史梯度的信息,从而加速优化过程,尤其是在复杂的非凸损失表面中表现出色。Momentum 通常作为 SGD 的扩展版本使用,可以与 Mini-Batch SGD 结合

Momentum 模仿物理学中的动量概念:

  • 在物理中,动量是物体的质量与速度的乘积。物体的运动状态会受到惯性影响,越大的动量意味着越难改变方向
  • 在优化中,Momentum 会记录梯度下降的更新方向,并累积这些更新,以使得参数在一致的方向上更新得更快,而在噪声较大的方向上减少振荡。

Momentum 基本算法的更新规则如下: \[ v_t = \gamma v_{t-1} + \eta \nabla J(\theta_{t-1}) \] \[ \theta_t = \theta_{t-1} - v_t \]

  1. 动量项 \(\gamma v_{t-1}\)
    • 表示上一次更新的方向,具有惯性,当前更新会参考之前的方向。
    • \(\gamma\) :动量系数,通常取值在 [0.8, 0.99] 之间。
  2. 梯度项 \(\eta \nabla J(\theta_{t-1})\)
    • 当前的梯度信息,驱动参数向最小值方向移动。
  3. 参数更新 \(\theta_t\)
    • 综合了惯性和当前梯度,更新参数。

Note:SGD 每次仅使用一个样本或小批量样本来估计梯度,随机梯度的变化使得优化路径在复杂非凸损失表面上具有探索能力,有一定概率跳出局部最优解或鞍点。但由于梯度估计不稳定,优化路径可能会呈现高频振荡,尤其是在陡峭方向或平坦区域,导致收敛速度较慢。

Momentum 可以被看作对梯度信息的一种“平滑操作”,减少不必要的小幅度方向变化,优化路径更趋于稳定。但Momentum 并没有完全消除随机性。因为梯度更新仍基于 Minibatch SGD 或 SGD,随机性依然存在,只是短期的高频随机波动被抑制了。

对比标准SGD的更新公式: \[ \theta_t = \theta_{t-1} - \eta \nabla J(\theta_{t-1}) \]

和 Momentum的更新公式: \[ \theta_t = \theta_{t-1} - \gamma v_{t-1} - \eta \nabla J(\theta_{t-1}) \]

Momentum 保留了 SGD 的随机特性,同时通过动量的累积减少了噪声对更新路径的干扰


Momentum 的主要改进与目标问题 #

Momentum 主要针对以下的问题进行优化:

  1. 问题一:收敛速度慢。在陡峭方向(梯度较大)和较平坦方向(梯度较小)上,SGD 的更新幅度相差较大。在较平坦方向上,梯度小导致更新缓慢,而在陡峭方向上,反复振荡消耗了时间
    • Momentum 如何解决
      • 动量累积历史梯度:Momentum 会在参数更新时累积之前的梯度信息,形成一个惯性(动量),从而增强优化的方向性。
      • 加速沿低梯度方向的更新:对于较平坦的方向,由于动量项的累积作用,更新步长会逐渐加快
      • 平滑陡峭方向的振荡:在陡峭方向上,梯度的更新方向会相互抵消(正负方向交替),动量能够有效减少振荡幅度,使得更新更平稳。
  2. 问题二:局部振荡问题。在目标函数中接近局部最优点或鞍点时,SGD 可能因为随机梯度引入的噪声而反复振荡,无法稳定收敛。振荡现象在非凸优化问题中尤为严重,常导致收敛效率低。
    • Momentum 如何解决
      • 动量的平滑作用:Momentum 会对当前的梯度值和之前的梯度值进行加权平均,从而减少梯度噪声的影响。
      • 更新方向的稳定性:动量累积了一段时间内梯度的总体方向,使得参数更新不易受到局部梯度噪声的干扰,避免振荡。
  3. 问题三:避免陷入局部最优点。在复杂的高维非凸函数中,优化算法可能会陷入局部最优点,难以找到全局最优解。SGD 由于其更新完全依赖当前梯度,缺乏全局视角,更容易陷入局部最优。
    • Momentum 如何解决
      • 动量的历史信息积累:Momentum 通过累积多次梯度更新信息,使得优化过程具有更强的惯性,能够 “冲出”某些局部最优点或平坦区域
      • 优化路径的惯性推动:当优化路径接近局部最优时,动量机制仍会保持一定的前进趋势,避免算法在局部区域停滞。
  • Momentum 代码实现
    import numpy as np
    # 定义目标函数及其梯度
    def loss_function(w):
        """目标函数: f(w) = 0.5 * (w - 3)^2"""
        return 0.5 * (w - 3) ** 2
    
    def gradient(w):
        """目标函数的梯度: f'(w) = w - 3"""
        return w - 3
    
    # 初始化参数
    w = np.random.randn()  # 初始化权重,假设是一个标量
    learning_rate = 0.1    # 学习率
    epochs = 100           # 训练的总轮次
    momentum = 0.9         # 动量系数
    v = 0                   # 初始化动量项
    
    # SGD with Momentum优化过程
    for epoch in range(epochs):
        # 计算梯度
        grad = gradient(w)
        # 更新动量(标准动量公式)
        v = momentum * v + learning_rate * grad  # v是历史动量和当前梯度的加权和
        # 更新参数
        w -= v  # 更新参数,减去动量
    

Nesterov Momentum #

Nesterov Momentum(Nesterov加速梯度)是对传统Momentum方法的一种改进。它被设计用来加速优化过程并提高收敛速度。其关键思想是,在更新参数之前,预先计算梯度,并且利用当前的 “预估”位置 来指导下一步的更新。与传统Momentum方法相比,Nesterov Momentum通常可以获得更好的收敛性能。因为它能够提前“看到”更远的方向,从而更精确地更新参数。它更能利用 动量的“提前预见”来加速收敛。具体来说,Nesterov Momentum方法的步骤如下:

  1. 计算预估位置:在更新之前,首先通过动量项预测出一个“预估”位置。即: \[ \theta_{\text{temp}} = \theta_t - \gamma v_{t-1} \] 其中, \(\theta\) 是当前的参数, \(\gamma\) 是动量系数, \(v\) 是上一轮的动量。
  2. 计算梯度:使用这个预估位置 \(\theta_{\text{temp}}\) 来计算梯度: \[ \nabla J(\theta_t - \gamma v_{t-1}) \]
  3. 更新动量和参数:然后基于计算得到的梯度,更新动量和参数: \[ v_t = \gamma v_{t-1} + \eta \nabla J(\theta_t - \mu v_{t-1}) \] \[ \theta_{t+1} = \theta_t - v_t \]

Nesterov Momentum的优势在于,它能够在更新之前“预测”到未来的趋势,避免了传统Momentum方法中的滞后效应。传统Momentum通过当前的梯度来更新动量,这可能导致参数更新的方向滞后于实际的优化目标。

  • 提前获得信息:由于在更新参数时使用的是当前动量的预估位置,这使得梯度下降方向更加“前瞻性”,有时比直接使用当前梯度更有效。具体来说,Nesterov Momentum在梯度计算时,不是简单地使用当前参数 \(\theta_t\) ,而是使用预估的参数 \(\theta_t - \mu v_{t-1}\) ,即“预测”的位置。

  • 减少滞后:传统Momentum容易受到过去梯度的影响,尤其是在损失函数具有不规则曲线时。例如,当优化过程进入谷底(参数值接近最优解时),新的梯度非常小(接近0),但是动量项会继续沿着之前的方向更新,这就是所谓的“滞后”现象。由于动量项是对历史梯度的加权平均,它不会立刻根据当前的梯度信息来调整方向,而是会继续沿着之前的方向更新参数,直到动量被新的梯度信息所替代。

    Momentum的本质是“惯性”,它通过加权之前的梯度来推测更新的方向。在梯度发生变化的区域,Momentum仍然会继续沿着先前的方向进行更新。这会导致在梯度发生急剧变化时,动量更新的方向滞后于实际的需求。Nesterov Momentum通过提前估计未来的梯度变化,减少了这种滞后,通常会加速收敛。

Note:在Momentum中,动量是直接根据当前点的梯度来更新的,而没有提前预测。

在Nesterov Momentum中,动量的更新是通过先计算出一个预估位置(基于上一步的动量),然后在这个预估位置计算梯度,再利用这个梯度来更新动量(提前往前走一步)。这样一来,Nesterov Momentum就“提前”计算了梯度,从而可以更准确地引导下一步的更新。因此,Nesterov Momentum并不是完全“准确”的梯度,它是基于当前动量的预测来计算的一个梯度,这个梯度的计算位置是偏向于未来的(即梯度的计算基于预估位置)。

  • Nesterov Momentum 代码实现
    import numpy as np
    
    # 定义目标函数及其梯度
    def loss_function(w):
        """目标函数: f(w) = 0.5 * (w - 3)^2"""
        return 0.5 * (w - 3) ** 2
    
    def gradient(w):
        """目标函数的梯度: f'(w) = w - 3"""
        return w - 3
    
    # 超参数设置
    learning_rate = 0.1
    momentum = 0.9
    epochs = 100
    w = np.random.randn()  # 初始参数
    v = 0  # 初始化动量
    
    # Nesterov Momentum优化过程
    for epoch in range(epochs):
        # 在更新前,先利用当前动量预测参数的位置
        w_nesterov = w - momentum * v  # Nesterov的预估位置
        # 计算梯度
        grad = gradient(w_nesterov)
        # 更新动量(Nesterov公式)
        v = momentum * v + learning_rate * grad  # v是历史动量和当前梯度的加权和
        # 更新参数
        w -= v  # 更新参数,减去动量
    

参数初始化策略(Parameter Initialization Strategies) #

在深度学习中,参数初始化是训练神经网络的重要一步。良好的初始化可以加速收敛,避免梯度消失或梯度爆炸的问题,同时提高最终模型的性能。而不当的初始化则可能导致网络训练缓慢、不稳定,甚至完全无法收敛。

  1. 避免梯度消失与梯度爆炸:当网络层数较深时,若初始化不当,前向传播中的激活值或反向传播中的梯度可能会指数级地缩小或增大,导致:
    • 梯度消失:参数更新几乎停止,无法学习深层特征。
    • 梯度爆炸:梯度值过大,导致参数更新不稳定。
  2. 提高训练速度:良好的初始化使得激活值和梯度的分布在整个网络中保持适中,从而提高收敛速度。
  3. 避免对称性问题:若所有权重初始值相同,反向传播时每个神经元将计算出相同的梯度,导致网络无法打破对称性,降低模型的表达能力。
    • 初始化需要确保不同的隐藏单元计算不同的函数。
    • 如果多个隐藏单元具有相同的输入和初始化参数,它们将始终更新为相同的值,丧失多样性。
    • 随机初始化通常用于打破对称性,避免输入模式或梯度模式丢失。

Note对称性问题指的是网络中不同神经元的参数更新路径完全相同,从而导致它们学习的特征也完全相同,失去了网络的表达能力。这种现象被称为对称性破坏不足。如果多个神经元初始化时的权重完全相同(例如都为 0 或相同的常数),并且它们接收到完全相同的输入数据,那么:

  • 它们的前向传播计算结果相同。
  • 它们的梯度在反向传播时也完全相同。
  • 经过多轮更新后,这些神经元仍然保持相同的权重值,无法学到不同的特征。

通过给每个神经元分配不同的初始权重值(如随机初始化),可以破坏对称性,使得每个神经元学到的特征变得多样化。这是因为:

  1. 前向传播阶段
    • 相同的输入 \(x\) ,在与不同的权重 \(w_1, w_2, \dots\) 相乘相加后,会生成不同的中间值
    • 这些不同的中间值通过激活函数(如 ReLU、sigmoid)映射后,输入到下一层神经元时已经变得完全不同
  2. 反向传播阶段
    • 不同权重初始化会导致每个神经元接收到的梯度不同
    • 即便它们的输入和输出在一开始类似,梯度更新的路径会逐渐让它们的参数走向不同的方向。

常用初始化方法 #

随机初始化(Random Initialization) #

  • 通常从高斯分布或均匀分布中随机采样权重。
    • 均匀分布:如 \(W \sim U(-a, a)\)
    • 正态分布:如 \(W \sim N(0, \sigma^2)\)
  • 分布类型对结果影响较小,但分布的 尺度(scale) 对优化效果和泛化能力有较大影响。
    • 权重过大:前向传播时,可能导致值爆炸。反向传播时,可能导致梯度爆炸或混沌行为(如RNN)。激活函数饱和,梯度完全丢失。
    • 权重过小:信号在前向传播中消失,导致网络无法学习。
  • 随机初始化(Random Initialization)代码实现
    import numpy as np
    
    # 随机初始化函数
    def random_initialize(input_size, output_size):
        # 权重初始化:均匀分布
        W = np.random.randn(output_size, input_size) * 0.01  
        # 乘以一个小常数(通常是0.01)来避免较大的权重值
        b = np.zeros((output_size, 1))  # 偏置初始化为0
        return W, b
    

Xavier 初始化(Xavier Initialization) #

对权重 \(W\) 的每个元素,从以下分布中采样:

  • 均匀分布: \[ W_{i,j} \sim U\left(-\sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}}, \sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}}\right) \]
  • 正态分布: \[ W_{i,j} \sim N\left(0, \frac{2}{n_{\text{in}} + n_{\text{out}}}\right) \] 其中, \(n_{\text{in}}\) 是输入单元数, \(n_{\text{out}}\) 是输出单元数。

Xavier 初始化适用于激活函数是 sigmoid 或 tanh 的网络,或者网络层较浅或中等深度的场景。他的优点在于:

  • 平衡输入和输出信号的方差,避免激活值和梯度值的极端变化
  • 避免信号在网络中消失或爆炸
  • 缺点:不适用于ReLU类激活函数。

NoteXavier 初始化适用于激活函数是 sigmoid 或 tanh 的网络的原因是这些激活函数的导数容易趋于零,尤其是在输入值落入激活函数的饱和区(Sigmoid 的两侧平坦区域)。如果权重初始化过大,输入会快速进入饱和区,导致梯度消失。如果权重初始化过小,输出信号会逐层衰减,最终导致梯度消失。

Xavier 的初始化方法将权重分布限定在一个较小的范围内,使输入值主要分布在 Sigmoid 和 Tanh 的线性区,避免梯度消失。在较深的网络中,信号可能仍会因为层数的累积效应导致衰减或放大。

  • Xavier 初始化代码实现
    import numpy as np
    
    def xavier_initialization(input_size, output_size):
        # Xavier初始化公式:使用均匀分布来初始化权重
        # 计算权重初始化的范围
        limit = np.sqrt(6 / (input_size + output_size))
        # 均匀分布初始化权重
        weights = np.random.uniform(-limit, limit, (input_size, output_size))
        return weights
    
    # 示例:假设输入层有3个神经元,输出层有2个神经元
    input_size = 3
    output_size = 2
    weights = xavier_initialization(input_size, output_size)
    

He 初始化(He Initialization) #

具体公式如下:

  • 均匀分布: \[ W_{i,j} \sim U\left(-\sqrt{\frac{6}{n_{\text{in}}}}, \sqrt{\frac{6}{n_{\text{in}}}}\right) \]
  • 正态分布: \[ W_{i,j} \sim N\left(0, \frac{2}{n_{\text{in}}}\right) \] 其中, \(n_{\text{in}}\) 是输入单元数。

He 初始化适用于激活函数是ReLU及其变种(如Leaky ReLU、Parametric ReLU),或者深层网络的场景。他的优点在于:

  • 专为ReLU类激活函数设计,能够更好地传递正向和反向信号。
  • 缺点:对非ReLU激活函数可能效果较差。

NoteHe 初始化适用于激活函数是ReLU及其变种的原因是对于 ReLU,当输入为负时,输出恒为 0;当输入为正时,输出为原值。由于一部分神经元输出会被截断为 0,导致有效的参与计算的神经元数量减少(称为“稀疏激活”现象)。如果初始化权重过小,信号会迅速减弱,导致梯度消失;而如果权重过大,信号会迅速放大,导致梯度爆炸。

He 初始化通过设定较大的方差,补偿了 ReLU 截断负值导致的信号损失。这样可以让激活值的分布更均衡,避免信号快速衰减或放大。He 初始化根据输入层大小调整权重的方差,使每层的输出方差保持相对稳定,即使网络层数增加,信号也不会显著衰减或爆炸。

  • He 初始化代码实现
    import numpy as np
    
    def he_initialization(input_size, output_size):
        # 权重初始化
        weights = np.random.randn(output_size, input_size) * np.sqrt(2. / input_size)
        # 偏置初始化为 0
        biases = np.zeros((output_size, 1))
        return weights, biases
    
    # 示例:初始化一个有 3 个输入节点和 2 个输出节点的层
    input_size = 3
    output_size = 2
    weights, biases = he_initialization(input_size, output_size)
    

现代优化中的初始化改进 #

  1. 学习率与初始化的协同
    • 初始化与学习率的选择需要协同设计。
    • 比如,较大的初始化可能需要较小的学习率,反之亦然。
  2. Batch Normalization 的引入
    • 批量归一化(Batch Normalization)可以在一定程度上缓解初始化不当带来的问题,使得更宽泛的初始化策略能够被有效利用。
  3. 自适应优化算法
    • 优化算法如 Adam 和 RMSProp 可以通过动态调整学习率,减少对初始化的敏感性。

批量标准化(Batch Normalization) #

具体细节详见 正则化章节的 Batch Normalization

Note:Batch Normalization 在 正则化(Regularization) 中的作用体现在 防止过拟合,提高模型泛化能力。通过小批量统计引入噪声,减轻对特定神经元的依赖,间接限制权重的自由度,使模型更加简洁和泛化性更强。

Batch Normalization 在 优化(Optimization) 中的作用体现在 加速收敛并提高稳定性。归一化减少 Internal Covariate Shift,并限制激活值范围,缓解梯度消失和爆炸问题,从而允许更大的学习率。

Batch Normalization 在两个方面的作用相辅相成,但本质上是优化导向的技术,正则化效果是其附带的一个益处


Batch Normalization 在优化中的作用及原因 #

  1. 减少 Internal Covariate Shift:在训练过程中,每层的输入分布可能会随着前层参数更新而改变,这被称为 Internal Covariate Shift。这种分布变化会导致:训练变得困难,梯度传播不稳定,学习率难以设置。Batch Normalization 通过强制每一层的输入分布保持稳定(即归一化到零均值和单位方差),有效减小了 Internal Covariate Shift,使模型更容易选择较大的学习率

  2. 缓解梯度消失和梯度爆炸问题:深度网络中的梯度可能因为激活函数(如 sigmoid 或 tanh)的饱和区域而迅速缩小或增长,导致:梯度消失(权重更新速度慢)或者梯度爆炸(权重更新不稳定)。

    通过将输入归一化,BN 限制了激活值的范围,避免了梯度过大或过小。归一化后的输入值分布更接近零均值和单位方差,减少了激活函数饱和的概率。梯度在反向传播中更稳定,使得优化过程更加平滑。

  3. 提高优化效率:BN 允许使用更大的学习率,从而加速训练。BN 的归一化过程对激活值和梯度值进行了平滑化处理,即使学习率较大,梯度更新仍然稳定。这种特性让优化过程中的收敛速度显著提高。

  4. 改善权重初始值的鲁棒性:传统的神经网络对权重初始化非常敏感,糟糕的初始化会导致:训练时间变长,模型性能变差。BN 减轻了对权重初始化的依赖,因为归一化后的输入分布消除了初始化权重引起的输入偏移。在每一层中,输入经过归一化后分布固定,减少了初始化权重对训练初期表现的影响。


具有自适应学习率的算法(Algorithms with Adaptive Learning Rates) #

学习率(Learning Rate) 在神经网络训练中是最重要的超参数之一,它控制着参数更新的步幅。选择合适的学习率对于训练过程至关重要,但它往往是 最难设置的超参数之一。原因主要有以下几点:

  1. 不同方向的灵敏度:在参数空间的某些方向,代价函数对参数的敏感度较高(即梯度较大),而在其他方向上则不敏感(即梯度较小)。这种非均匀的敏感度使得在所有方向上使用统一的学习率变得困难。即使在同一方向上,代价函数的变化幅度也可能不同,这导致在训练过程中很难确定合适的步长。
  2. 局部极小值或鞍点:神经网络的损失函数通常具有多个局部极小值或鞍点,学习率过大可能导致模型错过最优解,而学习率过小则可能导致训练过慢,甚至陷入局部极小值。因此,选择合适的学习率是一个平衡问题,过大可能导致发散,过小可能导致收敛过慢。
  3. 需要在训练过程中动态调整:固定的学习率在整个训练过程中可能并不适用,因为随着训练的进行,参数的更新越来越小,需要逐渐减小学习率才能更精细地搜索最优解。因此,动态调整学习率成为优化问题中的重要部分。

为了应对学习率调节的难题,研究者们提出了许多自适应学习率的方法,这些方法通过根据梯度的变化来自动调整每个参数的学习率。这些方法的核心思想是,如果某个方向上的梯度信息发生变化,那么相应的学习率也应该做出相应调整,从而更高效地进行参数更新。


AdaGrad #

AdaGrad的核心理念是通过调整每个参数的学习率来加速收敛,尤其是对于那些参数更新频繁的方向,给它们一个较小的学习率,而对于那些更新较少的方向,给它们一个较大的学习率。这是通过累积每个参数的梯度平方来实现的。每个参数的学习率会随着它的历史梯度大小的变化而逐步调整,从而使得学习率能够适应参数的梯度信息。AdaGrad算法的步骤可以总结为:

  1. 初始化:初始化每个参数的学习率 \(\eta_0\) (通常为一个小的常数),并初始化梯度累积项 \(G_i = 0\) ,其中 \(G_i\) 是该参数的梯度平方和的累积。
  2. 计算梯度:在每次迭代中,根据当前的损失函数计算每个参数 \(\theta_i\) 的梯度 \(g_i(t) = \nabla_{\theta} L(\theta_t)\)
  3. 累积梯度的平方:对每个参数 \(\theta_i\) ,累积梯度的平方,得到 \(G_t\) ,表示每个参数历史梯度的平方和。 \[ G_i(t) = G_i(t-1) + g_i(t)^2 \]
  4. 更新参数:然后,根据每个参数的累积梯度平方来调整学习率,并更新参数。AdaGrad的更新规则为: \[ \theta_i(t) = \theta_i(t-1) - \frac{\eta}{\sqrt{G_i(t)} + \epsilon} \cdot g_i(t) \] 其中:
    • \(\eta\) 是全局学习率(一个小的常数)。
    • \(G_t\) 是梯度的平方和。
    • \(\epsilon\) 是为了避免除以0而加上的一个小常数,通常设置为 \(10^{-8}\)

AdaGrad的特点与优势

  • 自适应调整学习率:AdaGrad 的主要特点是自适应调整每个参数的学习率。它根据每个参数的梯度历史调整学习率,对于频繁更新的参数,学习率会变得较小,而对于较少更新的参数,学习率则较大。
  • 适应稀疏数据:AdaGrad 特别适用于处理稀疏数据(如文本数据或大规模稀疏矩阵)。因为稀疏特征的梯度较少,AdaGrad 会给予这些特征更大的学习率,从而有效地加快收敛。
  • 无需手动调整学习率:由于 AdaGrad 会根据历史梯度自动调整学习率,它减少了手动调节学习率的需求。

AdaGrad的缺点

  • 学习率下降过快:AdaGrad 的最大缺点是其学习率会随着训练的进行不断减小,尤其是在训练的早期,梯度的累积效应可能导致学习率迅速下降到非常小的值,进而影响后续的训练。这意味着在训练过程中,模型可能会在某些参数上收敛得过快,无法再进一步优化。

  • 不适合长时间训练:由于学习率的衰减,AdaGrad 在长时间训练过程中可能会导致模型在后期停止更新参数,从而影响最终的收敛效果。

  • AdaGrad代码实现

    import numpy as np
    # 定义目标函数及其梯度
    def loss_function(w):
        """目标函数: f(w) = 0.5 * (w - 3)^2"""
        return 0.5 * (w - 3) ** 2
    
    def gradient(w):
        """目标函数的梯度: f'(w) = w - 3"""
        return w - 3
    
    # AdaGrad 优化过程
    def adagrad_optimizer(learning_rate, epochs, initial_w, epsilon=1e-8):
        w = initial_w  # 初始化参数
        G = 0  # 初始化梯度的平方和
    
        for epoch in range(epochs):
            grad = gradient(w)  # 计算梯度
            # 更新梯度的平方和
            G += grad ** 2
            # 计算每个参数的学习率
            adjusted_lr = learning_rate / (np.sqrt(G) + epsilon)
            # 更新参数
            w -= adjusted_lr * grad
            # 计算并记录损失值
            loss = loss_function(w)
    
        return w
    

RMSProp #

RMSProp 的主要思想是通过维护每个参数梯度的平方的指数衰减平均值来调整每个参数的学习率。与 AdaGrad 相比,RMSProp 引入了一个指数加权平均(Exponential Moving Average, EMA) 来控制梯度平方的累积,从而防止学习率在训练过程中过快衰减。RMSProp算法的步骤可以总结为:

  1. 初始化:与其他优化算法类似,初始化参数 \(\theta_i\) ,并设定初始学习率 \(\eta\) 和衰减因子 \(\gamma\) 。此外,初始化一个梯度平方的累积值 \(G_i = 0\) ,用于存储每个参数的梯度平方的加权平均。
  2. 计算梯度:在每次迭代中,计算每个参数 \(\theta_i\) 对应的梯度 \(g_i(t) = \nabla_{\theta} L(\theta_t)\) (即损失函数相对于该参数的偏导数)。
  3. 更新梯度平方的加权平均:使用指数加权平均来更新梯度平方的值: \[ G_i(t) = \gamma G_i(t-1) + (1 - \gamma) g_i(t)^2 \] 其中, \(\gamma\) 是衰减因子(通常设置为接近 1,如 0.9),它控制了历史梯度信息对当前梯度平方值的影响。较小的 \(\gamma\) 会让历史梯度信息对当前更新影响较小,而较大的 \(\gamma\) 会保留更多历史信息
  4. 更新参数:使用更新后的梯度平方的加权平均值来计算参数更新: \[ \theta_i(t) = \theta_i(t-1) - \frac{\eta}{\sqrt{G_i(t)} + \epsilon} \cdot g_i(t) \] 其中, \(\eta\) 是全局学习率, \(G_i(t)\) 是当前的梯度平方加权平均值, \(\epsilon\) 是为了避免除零错误而加上的一个小常数(通常设置为 \(10^{-8}\) )。

RMSProp的特点与优势

  • 更稳定的学习率调整:相比 AdaGrad,RMSProp 在训练过程中保持了较为稳定的学习率,不会因梯度过大或过小导致训练过程不稳定。
  • 适用于递归神经网络和强化学习:由于 RMSProp 在处理梯度变化较大的情况时非常有效,因此它广泛应用于递归神经网络(RNNs)和强化学习等任务。

RMSProp的缺点

  • 依赖衰减因子选择:RMSProp 的性能依赖于衰减因子 \(\gamma\) 的选择。虽然常见的 \(\gamma = 0.9\) 或 0.99 在很多任务中表现良好,但对于不同的任务,合适的衰减因子可能有所不同。

  • RMSProp代码实现

    import numpy as np
    
    # 定义目标函数及其梯度
    def loss_function(w):
        """目标函数: f(w) = 0.5 * (w - 3)^2"""
        return 0.5 * (w - 3) ** 2
    
    def gradient(w):
        """目标函数的梯度: f'(w) = w - 3"""
        return w - 3
    
    # RMSProp 优化过程
    def rmsprop_optimizer(learning_rate, epochs, initial_w, beta=0.9, epsilon=1e-8):
        w = initial_w  # 初始化参数
        v = 0  # 初始化梯度平方的移动平均
        for epoch in range(epochs):
            grad = gradient(w)  # 计算梯度
            # 更新梯度平方的移动平均
            v = beta * v + (1 - beta) * grad ** 2
            # 计算每个参数的学习率
            adjusted_lr = learning_rate / (np.sqrt(v) + epsilon)
            # 更新参数
            w -= adjusted_lr * grad
            # 计算并记录损失值
            loss = loss_function(w)
    
        return w
    

Adam #

Adam 通过计算梯度的一阶矩(梯度的均值,表示动量)和二阶矩(梯度的平方的均值,表示梯度的方差)来动态调整每个参数的学习率。具体来说,Adam 通过下面两个过程来更新参数:

  • 一阶矩估计(动量):这部分计算的是梯度的指数加权平均。它帮助优化器记住过去梯度的趋势,减少振荡和加速收敛。
  • 二阶矩估计(自适应学习率):这部分计算的是梯度平方的指数加权平均。它根据梯度的方差调整每个参数的学习率,使得模型能够对不同的梯度大小做出不同的响应。

Adam 的更新过程通过结合这两个矩估计(即梯度的一阶矩和二阶矩)来计算每个参数的自适应学习率,并更新参数。Adam 的更新过程可以分为以下几个步骤:

  1. 初始化参数
    • 参数 \(\theta\) (模型的可学习参数)
    • 学习率 \(\eta\)
    • 衰减因子 \(\beta_1\) \(\beta_2\) ,分别用于计算一阶矩和二阶矩的指数加权平均(通常设置为 \(\beta_1 = 0.9\) \(\beta_2 = 0.999\)
    • 偏置修正常数 \(\epsilon\) (通常为 \(10^{-8}\)
  2. 计算梯度:计算损失函数 \(L(\theta)\) 相对于参数 \(\theta\) 的梯度 \(g_t\) ,即 \(g_t = \nabla_{\theta} L(\theta_t)\)
  3. 更新一阶矩和二阶矩估计\[ m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot g_t \] \[ v_t = \beta_2 \cdot v_{t-1} + (1 - \beta_2) \cdot g_t^2 \] 其中, \(m_t\) 是梯度的动量, \(v_t\) 是梯度的平方的加权平均。
  4. 偏置修正:由于 \(m_t\) \(v_t\) 在训练的初期会有偏置,因此需要对它们进行修正: \[ \hat{m}_t = \frac{m_t}{1 - \beta_1^t} \] \[ \hat{v}_t = \frac{v_t}{1 - \beta_2^t} \] 这两个修正是为了消除训练初期 \(m_t\) \(v_t\) 的偏置。
  5. 更新参数\[ \theta_t = \theta_{t-1} - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \cdot \hat{m}_t \] 最终,通过带有自适应学习率的梯度更新每个参数。

Adam的特点与优势

  • 自适应学习率:Adam 自动调整每个参数的学习率,避免了手动调整学习率的需求,尤其是在复杂的深度学习任务中。不同的参数可能具有不同的梯度尺度和变化,因此使用自适应学习率有助于提升训练效率。
  • 动量优化:通过引入动量,Adam 能够加速梯度下降过程,减少振荡,从而更快地收敛。
  • 计算效率高:尽管 Adam 使用了两个矩的估计(动量和方差),但它只需要存储两个额外的向量( \(m_t\) \(v_t\) ),因此计算量相对较小,且能够适用于大规模数据集。

Adam的缺点

  • 超参数设置问题:尽管 Adam 在许多任务中表现得很出色,但它的超参数(如 \(\beta_1\) \(\beta_2\) \(\epsilon\) )对最终结果仍有较大影响。尽管默认设置通常有效,但对于特定问题,可能仍需要进行调优。

  • 偏置修正的延迟:尽管偏置修正能消除初期的偏差,但在非常长的训练过程中,仍可能出现一些偏差,影响训练的稳定性。

  • 可能导致过拟合:由于 Adam 在训练过程中可以快速适应数据集特性,它可能会导致过拟合,尤其是在数据较少或模型过于复杂的情况下。因此,在某些任务中,需要结合正则化技术(如 dropout)来避免过拟合。

  • Adam代码实现

    import numpy as np
    
    # 定义目标函数及其梯度
    def loss_function(w):
        """目标函数: f(w) = 0.5 * (w - 3)^2"""
        return 0.5 * (w - 3) ** 2
    
    def gradient(w):
        """目标函数的梯度: f'(w) = w - 3"""
        return w - 3
    
    # Adam 优化过程
    def adam_optimizer(learning_rate, epochs, initial_w, beta1=0.9, beta2=0.999, epsilon=1e-8):
        w = initial_w  # 初始化参数
        m = 0  # 一阶矩的初始化
        v = 0  # 二阶矩的初始化
        t = 0  # 时间步
        for epoch in range(epochs):
            t += 1
            grad = gradient(w)  # 计算梯度
            # 更新一阶矩(动量)
            m = beta1 * m + (1 - beta1) * grad
            # 更新二阶矩(梯度平方的移动平均)
            v = beta2 * v + (1 - beta2) * grad ** 2
            # 偏差修正(为了抵消初始化时 m 和 v 的偏差)
            m_hat = m / (1 - beta1 ** t)
            v_hat = v / (1 - beta2 ** t)
            # 计算每个参数的更新量
            w -= learning_rate * m_hat / (np.sqrt(v_hat) + epsilon)
            # 计算并记录损失值
            loss = loss_function(w)
    
        return w
    

学习率调度器(Learning Rate Scheduling) #

Learning Rate Scheduling 是指在训练过程中逐步调整学习率的策略。其核心思想是在训练过程中随着时间的推移,逐步减小学习率,使得模型能够更细致地调整参数,从而获得更好的收敛性能。Learning Rate Scheduling 提供了以下优势

  • 避免梯度爆炸或梯度消失:通过在训练的不同阶段动态调整学习率,避免在训练的初期学习率过大导致梯度爆炸,或者在后期学习率过小导致收敛过慢。
  • 提高收敛速度:随着训练的进行,减小学习率可以让模型在更精细的层面进行参数调整,提高最终收敛的精度。
  • 避免过拟合:逐步减小学习率有助于模型在后期避免过拟合,尤其是当模型已经接近全局最优解时。

Note:尽管自适应学习率和学习率调度都涉及到调整学习率,但它们之间有本质的区别:

  • 自适应学习率(如 AdaGrad、Adam)是基于每个参数的历史梯度信息来调整学习率。这是一个动态调整过程,能够针对每个参数的训练需求进行个性化调整。它在优化过程中有自动调整的优势,但它通常没有显式的学习率衰减策略
  • 学习率调度(如 Step Decay)是针对全局学习率的调整,它遵循某种预设的规则,并通常在整个训练过程中逐步减小学习率。它更关注整体训练过程的学习率变化,而非单个参数的调节。

常见的 Learning Rate Scheduling 策略 #

  1. 固定学习率(Constant Learning Rate): 这是最简单的学习率策略。在整个训练过程中,学习率保持不变,通常适用于模型较为简单的任务。虽然这种策略的优点是实现简单,但往往不能充分利用训练过程中的信息。

  2. 逐步衰减(Step Decay): 逐步衰减是一种经典的学习率调度策略,其基本思想是按照一定的间隔将学习率降低。通常在每训练几个epoch后,将学习率按比例衰减\[ \eta_t = \eta_0 \cdot \gamma^{\lfloor \frac{t}{\text{step\_size}} \rfloor} \] 其中:

    • \(\eta_t\) 是当前的学习率,
    • \(\eta_0\) 是初始学习率,
    • \(\gamma\) 是衰减因子(通常 \(0.1 \leq \gamma \leq 0.5\) ),
    • \(\text{step\_size}\) 是衰减的步长(通常以 epoch 为单位)。
  3. 指数衰减(Exponential Decay): 在指数衰减中,学习率在每次更新时按照指数方式衰减,而不是按照固定的步长进行调整。这种策略在训练的后期可以逐步减小学习率,使得模型更加精细地调整参数。 \[ \eta_t = \eta_0 \cdot e^{-\lambda t} \] 其中:

    • \(\eta_t\) 是当前的学习率,
    • \(\eta_0\) 是初始学习率,
    • \(\lambda\) 是衰减率,控制衰减的速度,
    • \(t\) 是当前 epoch 数。

Note:在实际的深度学习训练中,自适应学习率(如 Adam、RMSProp)和学习率调度(Learning Rate Scheduling)可以结合使用,也可以单独使用

自适应学习率算法(如 Adam)自动调整每个参数的学习率,可以减少手动调参的工作量,非常适合快速验证模型设计或算法选择。自适应学习率通常表现出较快的初期收敛速度,但在训练后期可能难以找到最优解。因此,当更关注模型能快速达到“足够好”的性能时,仅使用自适应学习率可能已经足够。

但在对模型最终性能要求较高,或在大规模数据集上训练深层网络时,自适应学习率可以快速收敛,而学习率调度可以避免在后期震荡。这时结合自适应学习率和学习率调度更为常见。例如,使用 Adam 优化器加上 学习率逐步降低(如 Step Decay)。这样既能快速优化,又能在训练后期稳定收敛。

  • 学习率调度器代码实现
    import torch
    import torch.nn as nn
    import torch.optim as optim
    from torch.optim.lr_scheduler import StepLR
    
    # 模型、损失函数和优化器
    model = SimpleNN()
    criterion = nn.MSELoss()
    optimizer = optim.SGD(model.parameters(), lr=0.1)
    
    # StepLR 学习率调度器:每 10 个 epoch 将学习率减少为原来的 0.1
    scheduler = StepLR(optimizer, step_size=10, gamma=0.1)
    # scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.9)
    # scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)
    
    # 训练过程
    epochs = 50
    for epoch in range(epochs):
        model.train()
        optimizer.zero_grad()
        # 前向传播
        outputs = model(data)
        # 计算损失
        loss = criterion(outputs, targets)
        # 反向传播和优化
        loss.backward()
        optimizer.step()
        # 每个 epoch 后更新学习率
        scheduler.step()
    

优化方法的综合运用 #

在深度学习训练中,优化流程通常分为三个阶段,每个阶段结合多种方法以逐步提升模型性能。

  1. 初期探索阶段

初期探索阶段的目标是快速验证模型设计是否合理,通常使用 Adam 优化器,其默认设置适合大多数任务,能快速收敛。此时使用 He 初始化(针对 ReLU 激活函数)或 Xavier 初始化(针对 Sigmoid 或 Tanh),并引入 Batch Normalization 来稳定激活值分布。为了防止过拟合,可以选择适量的 Dropout。同时,将输入数据标准化为均值 0、方差 1,选择小批量大小(通常 32 ~ 128)以平衡训练稳定性与效率。

  1. 中期优化阶段

中期优化阶段旨在进一步提升模型性能并解决可能出现的优化挑战。在这个阶段,可以继续使用 Adam 优化器,或者切换到 SGD with Momentum,以获得更稳定的优化过程。同时,结合 学习率调度器(如 ReduceLROnPlateau),动态调整学习率,避免训练停滞。对于梯度爆炸问题,启用 梯度裁剪 限制梯度范围;对稀疏特征或大型模型,则增加 Dropout 比例和 L2 正则化力度,进一步防止过拟合。

  1. 后期精调阶段

后期精调阶段的重点是通过微调参数实现最终的性能优化。此时,通常切换到 SGD with Momentum 并降低学习率到较小值(如 \(1e^{-5}\) ),结合固定步长下降(Step Decay)或动态调度策略(如 ReduceLROnPlateau)控制学习率变化。同时,可以逐渐减少或停用 Dropout,以提高模型的表达能力,仅保留少量的 L2 正则化。在验证集上增加评估频率,通过 早停(Early Stopping) 策略监控性能并防止过拟合。