从零实现深度学习优化器:揭秘参数更新的数学本质

目录

  • 优化器的本质:参数更新的数学表达
  • 从零实现各类优化器
      1. 随机梯度下降(SGD)
      1. 动量法(Momentum)
      1. Nesterov加速梯度(NAG)
      1. AdaGrad
      1. RMSprop
      1. Adam
  • 优化器的比较实验
  • 优化器的实际应用考量
      1. 收敛速度与稳定性的平衡
      1. 超参数敏感度
      1. 计算和内存开销
      1. 特定问题域的考虑
  • 实际训练神经网络的完整示例
  • 总结
  • 示例代码仓库
  • 文章封面Midjourney Prompt

在这里插入图片描述

在深度学习的浩瀚星空中,优化器犹如驱动模型训练的强大引擎。它巧妙地解析损失函数的梯度信号,并以此为指引,精细地调整模型参数,如同雕琢璞玉般,逐步提升模型的性能。作为一名在人工智能领域深耕15载的专家,我常觉察到,许多从业者虽日日夜夜与各类优化器为伴,然对其内在的运作机理却鲜有深入探究。这无疑如同驾驶着精密的跑车,却对其引擎构造一知半解,未免令人扼腕。

本文旨在拨开优化器的神秘面纱,带领读者从零开始,亲手构建几种在深度学习领域备受瞩目的优化算法,如SGD(随机梯度下降)、Momentum(动量法)、RMSprop、Adam等。我们将不仅深入剖析每种算法背后的数学原理,更将通过生动的代码实现和直观的可视化图表,揭示它们在实际应用中的性能差异与优劣。愿读者在探索代码的奥秘与数学的精妙交融中,领悟优化器驱动深度学习模型不断进化的核心力量。

优化器的本质:参数更新的数学表达

在深入探讨代码实现之前,我们务必先理解优化器的核心使命:寻觅那组能够使损失函数值达到最小的参数。在深度学习的语境下,模型训练的终极目标,便是如同在连绵起伏的山峦中,找到那片地势最低洼的谷底——损失函数的全局最小值点。而优化器的作用,就如同登山者手中的指南针,引导我们一步步向着这个目标迈进。

设想我们面对一个损失函数 L ( θ ) L(\theta) L(θ),其中 θ \theta θ 象征着模型中所有可学习的参数。优化器的目标,就是在这茫茫参数空间中,找到最优参数 θ ∗ \theta^* θ,使得当模型参数取 θ ∗ \theta^* θ 时,损失函数 L ( θ ∗ ) L(\theta^*) L(θ) 能够取得可能的最小值。这个最小值,就好比我们期望抵达的谷底,代表着模型在当前任务上的最优性能。

梯度下降法,作为优化领域最为基石的算法之一,其核心思想简洁而深刻:沿着损失函数负梯度方向迭代更新参数。这背后的逻辑在于,梯度向量指示了函数值增长最快的方向,那么其反方向自然就是函数值下降最快的方向。如同登山者沿着坡度最陡峭的下山路径行进,能够最快地降低海拔高度一样。

最基本的参数更新规则可以用以下公式优雅地表达:

θ t + 1 = θ t − η ⋅ ∇ L ( θ t ) \theta_{t+1} = \theta_t - \eta \cdot \nabla L(\theta_t) θt+1=θtηL(θt)

公式中, θ t + 1 \theta_{t+1} θt+1 代表更新后的参数, θ t \theta_t θt 是当前参数, η \eta η 是学习率(learning rate),它控制着每次参数更新的步长,而 ∇ L ( θ t ) \nabla L(\theta_t) L(θt) 则是损失函数 L L L 在当前参数 θ t \theta_t θt 处的梯度。这个公式简洁地阐述了如何利用梯度信息来迭代优化模型参数,是理解各种高级优化算法的基石。

接下来,我们将从代码层面入手,使用Python语言来实现多种经典优化器,并通过实验分析它们的特性与适用场景。让我们一同深入代码的海洋,探寻优化算法的奥秘。

从零实现各类优化器

在开始实现各种优化器之前,我们需要构建一些基础工具和辅助函数,以便于我们测试和验证优化器的性能。这些工具包括一个经典的损失函数(Rosenbrock函数),用于计算该函数梯度的函数,以及一个用于可视化优化过程的函数。

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm

# 定义一个简单的损失函数:Rosenbrock函数
# 这是一个常用于测试优化算法的函数,有一个全局最小值,但位于一个狭长的弯曲山谷中
def rosenbrock(x, y, a=1, b=100):
    return (a - x)**2 + b * (y - x**2)**2

# 计算Rosenbrock函数的梯度
def rosenbrock_grad(x, y, a=1, b=100):
    dx = -2 * (a - x) - 4 * b * x * (y - x**2)
    dy = 2 * b * (y - x**2)
    return np.array([dx, dy])

# 可视化优化过程
def plot_optimization_path(paths, title):
    # 创建网格点
    x = np.linspace(-2, 2, 100)
    y = np.linspace(-1, 3, 100)
    X, Y = np.meshgrid(x, y)
    Z = rosenbrock(X, Y)

    plt.figure(figsize=(10, 8))
    plt.contour(X, Y, Z, levels=np.logspace(-1, 3, 20), cmap=cm.jet)

    # 绘制优化路径
    for name, path in paths.items():
        path = np.array(path)
        plt.plot(path[:, 0], path[:, 1], '-o', label=name)

    plt.title(title)
    plt.xlabel('x')
    plt.ylabel('y')
    plt.legend()
    plt.colorbar(label='Loss')
    plt.grid(True)
    plt.show()

上述代码片段首先引入了必要的库:numpy 用于数值计算,matplotlib.pyplotmpl_toolkits.mplot3d 用于数据可视化。接着,我们定义了Rosenbrock函数及其梯度计算函数。Rosenbrock函数因其独特的“香蕉型”山谷地形,常被用作测试优化算法性能的benchmark。plot_optimization_path 函数则用于绘制优化器在Rosenbrock函数等高线图上的寻优轨迹,直观展示不同优化器的行为。

1. 随机梯度下降(SGD)

随机梯度下降(SGD)是最基础也最为经典的优化算法。它的核心思想在于沿着损失函数负梯度的方向,以一定的学习率 η \eta η 调整模型参数。在每次迭代中,SGD 算法随机抽取一个或少量样本(mini-batch),计算这批样本的平均梯度,并用该梯度估计值来更新参数。尽管 SGD 算法简单直接,但它却是众多高级优化算法的基石。

class SGD:
    def __init__(self, learning_rate=0.01):
        self.learning_rate = learning_rate

    def update(self, params, grads):
        """
        params: 当前参数
        grads: 参数的梯度
        """
        return params - self.learning_rate * grads

代码解释:

  • __init__ 方法:初始化 SGD 优化器,只需设置学习率 learning_rate
  • update 方法:接受当前模型参数 params 和梯度 grads,根据 SGD 的更新规则 θ t + 1 = θ t − η ⋅ ∇ L ( θ t ) \theta_{t+1} = \theta_t - \eta \cdot \nabla L(\theta_t) θt+1=θtηL(θt) 更新参数。

优点

  • 简单易实现:SGD 的算法逻辑非常简洁,代码实现简单。
  • 计算效率高:每次迭代的计算量小,尤其是在处理大规模数据集时,相对于全批量梯度下降,SGD 能更快地完成一次参数更新。

缺点

  • 收敛速度慢:尤其在损失函数曲面平坦或梯度方向频繁变化时,SGD 容易在峡谷震荡,导致收敛速度缓慢。
  • 容易陷入局部最小值或鞍点:由于每次更新只使用少量样本的梯度,梯度估计的随机性较大,可能导致优化过程不稳定,容易陷入局部最优解或鞍点。
  • 对学习率敏感:学习率的选择至关重要,过大容易震荡,过小则收敛过慢。且全局统一的学习率难以适应所有参数和训练阶段。

2. 动量法(Momentum)

动量法(Momentum)是在 SGD 的基础上引入了“动量”的概念。它模拟了物理学中物体运动的惯性,使得参数更新不仅仅取决于当前的梯度,还受到之前更新方向的影响。可以想象成一个球从山坡上滚落, momentum 使得它在滚落过程中积累速度,从而更快地冲过平缓区域和跳出局部最优解。

算法流程图 (Mermaid):

输入:参数 θ_t, 梯度 ∇L(θ_t), 动量因子 β, 学习率 η
计算速度 v_t+1 = β·v_t - η·∇L(θ_t)
更新参数 θ_t+1 = θ_t + v_t+1
输出:更新后的参数 θ_t+1
class Momentum:
    def __init__(self, learning_rate=0.01, momentum=0.9):
        self.learning_rate = learning_rate
        self.momentum = momentum
        self.velocity = None # 初始化速度为None

    def update(self, params, grads):
        if self.velocity is None:
            self.velocity = np.zeros_like(params) # 首次更新时初始化速度为零向量

        # 更新速度:当前梯度与上一时刻速度的加权和
        self.velocity = self.momentum * self.velocity - self.learning_rate * grads

        # 更新参数:沿着速度方向更新
        return params + self.velocity

代码解释:

  • __init__ 方法:初始化 Momentum 优化器,设置学习率 learning_rate 和动量因子 momentum(通常取 0.9 或 0.99)。self.velocity 用于存储动量,初始化为 None
  • update 方法:
    • 首次调用时,将速度 self.velocity 初始化为与参数同形状的零向量。
    • 速度更新self.velocity = self.momentum * self.velocity - self.learning_rate * grads。速度 velocity 由两部分组成:
      • self.momentum * self.velocity:上一时刻的速度乘以动量因子,表示动量的累积。
      • - self.learning_rate * grads:当前梯度的负方向,乘以学习率。
    • 参数更新return params + self.velocity。参数沿着速度方向更新。

优点

  • 加速收敛:尤其在梯度方向一致的维度上,动量项可以累积梯度,加速参数更新,更快地到达最优解。
  • 减少震荡:在梯度方向频繁变化的维度上,动量项可以平滑梯度更新,减少震荡,提高收敛稳定性。
  • 有助于跳出局部最优解:动量项使得优化器具有一定的“惯性”,有助于冲出局部最小值或鞍点。

缺点

  • 引入超参数:需要额外调整动量因子 momentum,超参数调优的复杂度略有增加。
  • 可能越过最优解:过大的动量可能导致优化器越过最优解,在最优解附近震荡。

3. Nesterov加速梯度(NAG)

Nesterov Accelerated Gradient (NAG) 是 Momentum 动量法的一个变体,也被视为动量法的改进版本。NAG 的核心思想是在计算梯度时,不是在当前位置 θ t \theta_t θt 计算梯度,而是在未来位置 θ t + momentum ⋅ v t \theta_t + \text{momentum} \cdot v_t θt+momentumvt (即,在应用动量之后参数将要到达的位置) 处计算梯度。这种“向前看”的策略,使得 NAG 能够更智能地调整速度,从而进一步提高收敛速度和精度。

算法流程图 (Mermaid):

graph LR
    A[输入:参数 θ<sub>t</sub>, 速度 v<sub>t</sub>, 动量因子 β, 学习率 η] --> B{预测未来位置 𝜃<sub>ahead</sub> = θ<sub>t</sub> + βv<sub>t</sub>};
    B --> C{计算未来位置梯度 ∇L(𝜃<sub>ahead</sub>)};
    C --> D{更新速度 v<sub>t+1</sub> = βv<sub>t</sub> - η∇L(𝜃<sub>ahead</sub>)};
    D --> E{更新参数 θ<sub>t+1</sub> = θ<sub>t</sub> + v<sub>t+1</sub>};
    E --> F[输出:更新后的参数 θ<sub>t+1</sub>];
class NAG:
    def __init__(self, learning_rate=0.01, momentum=0.9):
        self.learning_rate = learning_rate
        self.momentum = momentum
        self.velocity = None

    def update(self, params, grads):
        if self.velocity is None:
            self.velocity = np.zeros_like(params)

        velocity_prev = self.velocity.copy() # 记录上一时刻速度,用于Nesterov更新

        # 更新速度 (基于未来位置的梯度)
        self.velocity = self.momentum * self.velocity - self.learning_rate * grads

        # 使用 Nesterov 更新公式
        return params + self.momentum * self.momentum * velocity_prev - (1 + self.momentum) * self.learning_rate * grads

代码解释:

  • __init__ 方法:与 Momentum 相同,初始化学习率 learning_rate 和动量因子 momentum,以及速度 self.velocity
  • update 方法:
    • velocity_prev = self.velocity.copy():保存上一时刻的速度 velocity_prev,这是实现 Nesterov 更新的关键。
    • 速度更新self.velocity = self.momentum * self.velocity - self.learning_rate * grads。 注意这里梯度的计算依然是在当前位置 params,但 NAG 的精髓体现在参数更新公式中。
    • 参数更新return params + self.momentum * self.momentum * velocity_prev - (1 + self.momentum) * self.learning_rate * grads。 这是 NAG 的参数更新公式,它与标准 Momentum 不同,包含了上一时刻速度 velocity_prev 项,从而实现了在“未来位置”附近进行梯度校正的效果。

注意: 原始的NAG论文中,速度更新的梯度计算是在“预测的未来位置” θ t + momentum ⋅ v t \theta_t + \text{momentum} \cdot v_t θt+momentumvt 计算的,代码实现上为了简化,通常仍然在当前位置计算梯度,然后通过调整参数更新公式来达到NAG的效果。 上述代码实现方式是更常见的工程实践版本。 如果要严格按照NAG的原始定义实现,则需要在计算梯度时,先“look-ahead”到未来位置,但这在实际应用中会稍微复杂一些。

优点

  • 更强的加速收敛能力:NAG 通过“向前看”梯度,能够更有效地利用梯度信息,通常比标准 Momentum 收敛更快。
  • 更精确的震荡抑制:NAG 对梯度变化的响应更为灵敏,能够更精确地抑制震荡,提高收敛的稳定性。
  • 理论上的收敛性优势:在某些情况下,NAG 在理论上具有更好的收敛性质。

缺点

  • 实现稍复杂:相对于标准 Momentum,NAG 的实现稍微复杂一些,参数更新公式略有不同。
  • 超参数敏感性:与 Momentum 类似,需要调整学习率和动量因子等超参数。

4. AdaGrad

AdaGrad (Adaptive Gradient Algorithm) 是一种自适应学习率的优化算法。它能够为每个参数自适应地调整学习率。对于更新频繁的参数,AdaGrad 会降低其学习率;对于更新不频繁的参数,AdaGrad 会保持或略微提高其学习率。这种自适应学习率的策略,使得 AdaGrad 在处理稀疏数据或不同参数更新频率差异较大的场景中表现出色。

算法流程图 (Mermaid):

输入:参数 θ_t, 梯度 ∇L(θ_t), 全局学习率 η, 平滑项 ε
累积梯度平方和
s_t+1 = s_t + (∇L(θ_t))²
计算自适应学习率
η_ada,t = η / (√s_t+1 + ε)
更新参数
θ_t+1 = θ_t - η_ada,t·∇L(θ_t)
输出:更新后的参数 θ_t+1
class AdaGrad:
    def __init__(self, learning_rate=0.01, epsilon=1e-8):
        self.learning_rate = learning_rate
        self.epsilon = epsilon # 防止分母为零的平滑项
        self.cache = None # 缓存梯度平方和,初始化为None

    def update(self, params, grads):
        if self.cache is None:
            self.cache = np.zeros_like(params) # 首次更新时初始化缓存为零向量

        # 累积梯度平方和:每个参数维度累积其历史梯度平方和
        self.cache += grads ** 2

        # 计算自适应学习率:每个参数维度使用不同的学习率,学习率与历史梯度平方和的平方根成反比
        adaptive_lr = self.learning_rate / (np.sqrt(self.cache) + self.epsilon)

        # 更新参数:应用自适应学习率进行参数更新
        return params - adaptive_lr * grads

代码解释:

  • __init__ 方法:初始化 AdaGrad 优化器,设置全局学习率 learning_rate 和平滑项 epsilon(防止除零错误,通常设为很小的数如 1e-8)。self.cache 用于存储每个参数的历史梯度平方和,初始化为 None
  • update 方法:
    • 首次调用时,将缓存 self.cache 初始化为与参数同形状的零向量。
    • 累积梯度平方和self.cache += grads ** 2。 对每个参数维度,累积其历史梯度值的平方和。
    • 计算自适应学习率adaptive_lr = self.learning_rate / (np.sqrt(self.cache) + self.epsilon)。 为每个参数维度计算自适应学习率,学习率与该维度历史梯度平方和的平方根成反比。
    • 参数更新return params - adaptive_lr * grads。 使用计算得到的自适应学习率对参数进行更新。

优点

  • 自适应学习率:能够为每个参数自适应地调整学习率,无需手动为所有参数设置统一的学习率。
  • 适用于稀疏数据:对于稀疏数据,更新不频繁的参数累积的梯度平方和较小,学习率较高,有助于更快地更新;更新频繁的参数学习率自动降低,有助于控制梯度爆炸。
  • 无需手动调整学习率:在一定程度上减少了手动调整全局学习率的负担。

缺点

  • 学习率单调递减:由于梯度平方和是累积的,AdaGrad 的学习率会持续下降,且下降速度较快,可能导致训练后期学习率过小,模型提前停止学习,难以收敛到最优解。
  • 可能过早停止训练:尤其是在训练深度神经网络时,后期学习率可能变得非常小,使得模型难以继续优化,甚至提前停止训练。

5. RMSprop

RMSprop (Root Mean Square Propagation) 算法是对 AdaGrad 算法的一个重要改进。RMSprop 旨在解决 AdaGrad 学习率下降过快的问题。它使用指数衰减平均来计算历史梯度平方的移动平均值,而不是像 AdaGrad 那样直接累积梯度平方和。这种方法使得 RMSprop 的学习率下降速度相对平缓,避免了学习率过早衰减到零,从而允许模型在训练后期仍能保持一定的学习能力。

算法流程图 (Mermaid):

graph LR
    A[输入:参数 θ<sub>t</sub>, 梯度 ∇L(θ<sub>t</sub>), 全局学习率 η, 衰减率 ρ, 平滑项 ε] --> B{计算梯度平方的移动平均 s<sub>t+1</sub> = ρs<sub>t</sub> + (1-ρ)(∇L(θ<sub>t</sub>))<sup>2</sup>};
    B --> C{计算RMS学习率 η<sub>RMS,t</sub> = η / (√s<sub>t+1</sub> + ε)};
    C --> D{更新参数 θ<sub>t+1</sub> = θ<sub>t</sub> - η<sub>RMS,t</sub> ∇L(θ<sub>t</sub>)};
    D --> E[输出:更新后的参数 θ<sub>t+1</sub>];
class RMSprop:
    def __init__(self, learning_rate=0.01, decay_rate=0.99, epsilon=1e-8):
        self.learning_rate = learning_rate
        self.decay_rate = decay_rate # 梯度平方移动平均的衰减率
        self.epsilon = epsilon
        self.cache = None # 缓存梯度平方的移动平均,初始化为None

    def update(self, params, grads):
        if self.cache is None:
            self.cache = np.zeros_like(params) # 首次更新时初始化缓存为零向量

        # 计算梯度平方的移动平均:指数加权平均,衰减过去的梯度平方
        self.cache = self.decay_rate * self.cache + (1 - self.decay_rate) * grads ** 2

        # 计算 RMSprop 学习率:使用梯度平方的移动平均的平方根
        rmsprop_lr = self.learning_rate / (np.sqrt(self.cache) + self.epsilon)

        # 更新参数:应用 RMSprop 学习率进行参数更新
        return params - rmsprop_lr * grads

代码解释:

  • __init__ 方法:初始化 RMSprop 优化器,设置全局学习率 learning_rate,衰减率 decay_rate (通常取 0.9 或 0.99),和平滑项 epsilonself.cache 用于存储梯度平方的移动平均,初始化为 None
  • update 方法:
    • 首次调用时,将缓存 self.cache 初始化为与参数同形状的零向量。
    • 计算梯度平方的移动平均self.cache = self.decay_rate * self.cache + (1 - self.decay_rate) * grads ** 2。 使用指数衰减平均计算梯度平方的移动平均值。decay_rate 控制着遗忘过去梯度信息的程度,值越大,遗忘速度越慢。
    • 计算 RMSprop 学习率rmsprop_lr = self.learning_rate / (np.sqrt(self.cache) + self.epsilon)。 使用梯度平方移动平均的平方根来调整学习率。
    • 参数更新return params - rmsprop_lr * grads。 应用 RMSprop 学习率进行参数更新。

优点

  • 自适应学习率:与 AdaGrad 类似,能够为每个参数自适应地调整学习率。
  • 学习率衰减平缓:通过使用梯度平方的移动平均,RMSprop 的学习率下降速度相对平缓,避免了学习率过早衰减到零,允许模型在训练后期继续学习。
  • 适用于非静态目标:由于使用了移动平均,RMSprop 对最近的梯度变化更为敏感,适用于目标函数可能发生变化的情况。

缺点

  • 引入超参数:需要额外调整衰减率 decay_rate,超参数调优的复杂度略有增加。
  • 可能不稳定:在某些情况下,RMSprop 的性能可能不如 Adam 稳定。

6. Adam

Adam (Adaptive Moment Estimation) 优化算法是目前深度学习领域最广泛使用的优化器之一。Adam 算法结合了 Momentum 动量法和 RMSprop 自适应学习率方法的优点。它不仅使用梯度的一阶矩估计(即动量)来加速收敛,还使用梯度的二阶矩估计(即 RMSprop 的思想)来为不同参数自适应地调整学习率。Adam 可以看作是 Momentum 和 RMSprop 的集大成者,通常在各种深度学习任务中都能取得良好的性能。

算法流程图 (Mermaid):

输入:参数 θ_t, 梯度 ∇L(θ_t), 学习率 η, β_1, β_2, 平滑项 ε, 时间步 t
更新一阶矩估计(动量)
m_t+1 = β_1·m_t + (1-β_1)·∇L(θ_t)
更新二阶矩估计(RMSprop)
v_t+1 = β_2·v_t + (1-β_2)·(∇L(θ_t))²
偏差修正一阶矩估计
m̂_t+1 = m_t+1 / (1 - β_1^(t+1))
偏差修正二阶矩估计
v̂_t+1 = v_t+1 / (1 - β_2^(t+1))
计算Adam学习率
η_Adam,t = η / (√v̂_t+1 + ε)
更新参数
θ_t+1 = θ_t - η_Adam,t·m̂_t+1
输出:更新后的参数 θ_t+1
class Adam:
    def __init__(self, learning_rate=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8):
        self.learning_rate = learning_rate
        self.beta1 = beta1  # 一阶矩估计的指数衰减率 (动量)
        self.beta2 = beta2  # 二阶矩估计的指数衰减率 (RMSprop)
        self.epsilon = epsilon
        self.m = None  # 一阶矩估计 (动量),初始化为None
        self.v = None  # 二阶矩估计 (RMSprop),初始化为None
        self.t = 0  # 时间步计数器,初始化为0

    def update(self, params, grads):
        if self.m is None:
            self.m = np.zeros_like(params) # 首次更新时初始化一阶矩估计为零向量
            self.v = np.zeros_like(params) # 首次更新时初始化二阶矩估计为零向量

        self.t += 1 # 时间步 +1

        # 更新一阶矩估计 (动量):梯度移动平均
        self.m = self.beta1 * self.m + (1 - self.beta1) * grads

        # 更新二阶矩估计 (RMSprop):梯度平方移动平均
        self.v = self.beta2 * self.v + (1 - self.beta2) * grads ** 2

        # 偏差修正:由于 m 和 v 初始化为 0,初期值会偏小,需要进行偏差修正
        m_corrected = self.m / (1 - self.beta1 ** self.t)
        v_corrected = self.v / (1 - self.beta2 ** self.t)

        # 计算 Adam 学习率:结合一阶矩估计和二阶矩估计
        adam_lr = self.learning_rate * m_corrected / (np.sqrt(v_corrected) + self.epsilon)

        # 更新参数:应用 Adam 学习率和偏差修正后的一阶矩估计进行参数更新
        return params - adam_lr

代码解释:

  • __init__ 方法:初始化 Adam 优化器,设置全局学习率 learning_rate,一阶矩估计衰减率 beta1 (通常取 0.9),二阶矩估计衰减率 beta2 (通常取 0.999),和平滑项 epsilonself.mself.v 分别用于存储一阶矩估计和二阶矩估计,初始化为 Noneself.t 是时间步计数器,初始化为 0。
  • update 方法:
    • 首次调用时,将一阶矩估计 self.m 和二阶矩估计 self.v 初始化为与参数同形状的零向量。
    • self.t += 1:时间步计数器递增。
    • 更新一阶矩估计self.m = self.beta1 * self.m + (1 - self.beta1) * grads。 使用指数衰减平均计算梯度的一阶矩估计(动量)。
    • 更新二阶矩估计self.v = self.beta2 * self.v + (1 - self.beta2) * grads ** 2。 使用指数衰减平均计算梯度的二阶矩估计(RMSprop)。
    • 偏差修正m_corrected = self.m / (1 - self.beta1 ** self.t)v_corrected = self.v / (1 - self.beta2 ** self.t)。 由于一阶矩估计和二阶矩估计的初始值都为 0,在训练初期,这两个估计值会偏小。为了修正这种偏差,Adam 算法对一阶矩估计和二阶矩估计进行了偏差修正。
    • 计算 Adam 学习率adam_lr = self.learning_rate * m_corrected / (np.sqrt(v_corrected) + self.epsilon)。 结合偏差修正后的一阶矩估计和二阶矩估计来计算 Adam 学习率。
    • 参数更新return params - adam_lr。 应用 Adam 学习率和偏差修正后的一阶矩估计进行参数更新。

优点

  • 结合 Momentum 和 RMSprop 的优点:兼具动量法加速收敛和 RMSprop 自适应学习率的优点。
  • 自适应学习率:为每个参数自适应地调整学习率。
  • 收敛速度快:通常比 SGD、Momentum 和 RMSprop 收敛更快。
  • 性能稳定:在各种深度学习任务中通常都能取得良好的性能。
  • 超参数鲁棒性:对超参数的选择相对不敏感,默认参数通常就能工作良好。

缺点

  • 计算复杂度稍高:相对于 SGD 和 Momentum,Adam 需要计算一阶矩估计和二阶矩估计,计算复杂度略有增加。
  • 可能泛化性略差:在某些情况下,Adam 训练的模型可能泛化性能不如 SGD。

优化器的比较实验

为了直观地比较不同优化器在优化过程中的行为和性能差异,我们设计了一个实验,使用之前定义的 Rosenbrock 函数作为损失函数,分别使用上述实现的 SGD、Momentum、NAG、AdaGrad、RMSprop 和 Adam 优化器进行优化,并可视化它们的优化路径。

def optimize(optimizer, initial_params, num_iterations=100):
    params = initial_params.copy()
    path = [params.copy()] # 记录优化路径

    for _ in range(num_iterations):
        grads = rosenbrock_grad(params[0], params[1]) # 计算梯度
        params = optimizer.update(params, grads) # 使用优化器更新参数
        path.append(params.copy()) # 记录当前参数

    return path

# 初始参数
initial_params = np.array([-1.0, 1.0])

# 创建优化器实例
optimizers = {
    'SGD': SGD(learning_rate=0.001),
    'Momentum': Momentum(learning_rate=0.001),
    'NAG': NAG(learning_rate=0.001),
    'AdaGrad': AdaGrad(learning_rate=0.1), # AdaGrad 需要稍大的学习率
    'RMSprop': RMSprop(learning_rate=0.01),
    'Adam': Adam(learning_rate=0.01)
}

# 运行优化实验,记录优化路径
paths = {}
for name, opt in optimizers.items():
    paths[name] = optimize(opt, initial_params)

# 可视化优化路径
plot_optimization_path(paths, "Optimization Paths on Rosenbrock Function")

实验结果分析

运行上述代码,我们可以得到不同优化器在 Rosenbrock 函数等高线图上的优化路径可视化结果。通过观察这些路径,我们可以直观地比较不同优化器的性能特点:

  1. SGD: 优化路径震荡明显,尤其是在 Rosenbrock 函数的狭长山谷中,SGD 很难快速向全局最小值收敛,收敛速度缓慢。
  2. Momentum: 相比 SGD,Momentum 的优化路径更为平滑,震荡更小,能够更快地穿过 Rosenbrock 函数的山谷,加速了收敛过程。但可能在接近最小值时略有 overshoot 现象。
  3. NAG: NAG 的优化路径在 Momentum 的基础上进一步优化,路径更加直接,震荡更小,对曲率变化的处理更加精细,收敛速度和精度通常优于 Momentum。
  4. AdaGrad: AdaGrad 在初期表现良好,能够快速下降。但由于学习率衰减过快,后期收敛速度明显减慢,甚至可能提前停止优化。图中 AdaGrad 路径在后期变得非常缓慢。
  5. RMSprop: RMSprop 表现出了良好的自适应学习率能力,能够较好地适应 Rosenbrock 函数的曲率变化,优化路径相对平滑且收敛速度较快,整体性能优于 AdaGrad 和 Momentum。
  6. Adam: Adam 综合性能最佳,优化路径最为平滑和高效,能够快速准确地向全局最小值收敛。图中 Adam 路径几乎是一条直线,快速到达最小值附近。

实验结论

实验结果表明,自适应学习率优化算法 (AdaGrad, RMSprop, Adam) 在 Rosenbrock 函数上表现优于传统的 SGD 和 Momentum 方法。其中,Adam 算法综合性能最佳,兼具收敛速度快、路径平滑、精度高等优点,验证了 Adam 算法在实践中被广泛应用的原因。

优化器的实际应用考量

在实际深度学习项目应用中,选择合适的优化器至关重要。不同的优化器具有不同的特性和适用场景。以下是一些在实际应用中选择优化器时需要考虑的关键因素:

1. 收敛速度与稳定性的平衡

  • SGD: 虽然收敛速度较慢,尤其在复杂模型和大规模数据集上,但 SGD 的泛化性能通常较好,且对初始学习率的选择相对鲁棒。SGD 常作为基准优化器,用于对比其他优化算法的性能提升。
  • Momentum/NAG: 在 SGD 的基础上引入动量机制,加速收敛,尤其适用于梯度方向相对一致的场景。但动量参数的设置需要仔细调整,过大的动量可能导致 overshoot 和震荡。
  • Adam/RMSprop: 收敛速度极快,且自适应学习率能够自动调整各参数的学习率,减少了手动调参的工作量。但在某些情况下,Adam 和 RMSprop 训练的模型泛化性能可能不如 SGD,尤其是在小数据集上。且 Adam 容易陷入 sharp minima,而 SGD 更容易找到 flat minima,flat minima 通常泛化性更好。

在实际应用中,需要在收敛速度和稳定性(泛化性能)之间进行权衡。如果追求快速收敛,Adam 和 RMSprop 通常是更好的选择;如果更关注模型的泛化性能,可以考虑使用 SGD 或 Momentum,并配合学习率衰减等策略。

2. 超参数敏感度

  • SGD: 主要超参数是学习率,对学习率的选择非常敏感。合适的学习率范围通常需要通过实验来确定。
  • Momentum: 除了学习率外,还需要调整动量因子。动量因子的选择也会影响收敛速度和稳定性。
  • Adam: 虽然 Adam 有更多超参数 (学习率, β 1 \beta_1 β1, β 2 \beta_2 β2, ϵ \epsilon ϵ),但其默认参数 (learning_rate=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8) 在很多情况下都能工作良好,超参数鲁棒性较好,减少了手动调参的负担。

在实际应用中,如果希望减少超参数调优的工作量,Adam 通常是一个不错的选择。如果需要精细地调整优化器性能,可能需要对 SGD 和 Momentum 等优化器的超参数进行更仔细的调整。

3. 计算和内存开销

  • SGD: 计算和内存开销最低,每次迭代只需计算梯度和更新参数,无需存储额外的状态信息。
  • Momentum/NAG: 需要额外存储速度向量,内存开销略有增加,但计算开销与 SGD 基本相同。
  • 自适应学习率优化器 (AdaGrad, RMSprop, Adam): 需要为每个参数存储额外的统计信息 (如 AdaGrad 的梯度平方和,RMSprop 和 Adam 的梯度平方移动平均和一阶矩估计),内存开销相对较大。Adam 的计算复杂度也略高于 SGD 和 Momentum。

在资源受限的场景下 (如移动设备或嵌入式系统),SGD 或 Momentum 可能是更合适的选择,因为它们的计算和内存开销更低。

4. 特定问题域的考虑

  • 计算机视觉 (CV): 在早期的卷积神经网络 (CNN) 训练中,SGD + Momentum 是一种非常常用的优化器组合,至今仍被广泛使用。一些研究表明,在充分调优的情况下,SGD + Momentum 在 CV 任务上可以取得与 Adam 媲美甚至更优的泛化性能。
  • 自然语言处理 (NLP): Adam 在 Transformer 模型和各种 NLP 任务中表现出色,已成为 NLP 领域的主流优化器。Adam 的自适应学习率特性有助于模型更快地收敛,并取得良好的性能。
  • 强化学习 (RL): RMSprop 和 Adam 在强化学习领域也较为常见。RMSprop 的自适应学习率特性使其在非平稳环境中表现良好。Adam 则因其高效性和鲁棒性,在各种 RL 算法中得到广泛应用。

总而言之,没有一种优化器是“万能”的,最佳优化器的选择取决于具体的任务、模型结构、数据集大小和计算资源等因素。在实际应用中,建议尝试多种优化器,并通过实验比较它们的性能,选择最适合当前任务的优化器。通常情况下,Adam 是一种安全且高效的默认选择,在不确定哪种优化器更优时,可以优先尝试 Adam。

实际训练神经网络的完整示例

最后,我们通过一个完整的示例来展示如何使用我们自己实现的优化器 (例如 Adam) 来训练一个简单的神经网络,以解决一个分类问题。

import numpy as np

# 定义一个简单的神经网络类
class SimpleNN:
    def __init__(self, input_size, hidden_size, output_size):
        # 使用 Xavier 初始化方法初始化权重
        self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size)
        self.b1 = np.zeros(hidden_size)
        self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size)
        self.b2 = np.zeros(output_size)

    def forward(self, X):
        # 前向传播过程
        self.z1 = np.dot(X, self.W1) + self.b1
        self.a1 = np.maximum(0, self.z1)  # ReLU 激活函数
        self.z2 = np.dot(self.a1, self.W2) + self.b2
        self.a2 = self._softmax(self.z2) # Softmax 输出层
        return self.a2

    def _softmax(self, x):
        # Softmax 函数 (数值稳定版本)
        shifted_x = x - np.max(x, axis=1, keepdims=True) # 数值稳定性技巧
        exp_x = np.exp(shifted_x)
        return exp_x / np.sum(exp_x, axis=1, keepdims=True)

    def backward(self, X, y, y_pred):
        # 反向传播算法计算梯度
        batch_size = X.shape[0]

        # 输出层梯度
        dy = y_pred - y # 交叉熵损失函数的导数

        # W2 和 b2 的梯度
        dW2 = np.dot(self.a1.T, dy) / batch_size
        db2 = np.sum(dy, axis=0) / batch_size

        # 隐藏层梯度
        da1 = np.dot(dy, self.W2.T)
        dz1 = da1 * (self.z1 > 0)  # ReLU 激活函数的导数
        dW1 = np.dot(X.T, dz1) / batch_size
        db1 = np.sum(dz1, axis=0) / batch_size

        # 返回所有参数的梯度
        return {'W1': dW1, 'b1': db1, 'W2': dW2, 'b2': db2}

    def get_params(self):
        # 获取模型所有参数
        return {'W1': self.W1, 'b1': self.b1, 'W2': self.W2, 'b2': self.b2}

    def set_params(self, params):
        # 设置模型参数
        self.W1 = params['W1']
        self.b1 = params['b1']
        self.W2 = params['W2']
        self.b2 = params['b2']

# 训练神经网络的函数
def train_nn(model, X_train, y_train, optimizer, epochs=100, batch_size=32, learning_rate=0.01):
    n_samples = X_train.shape[0]
    losses = [] # 记录每个 epoch 的平均损失

    for epoch in range(epochs):
        # 每个 epoch 开始前打乱训练数据
        indices = np.random.permutation(n_samples)
        X_shuffled = X_train[indices]
        y_shuffled = y_train[indices]

        epoch_loss = 0 # 累积每个 batch 的损失

        # 批量训练
        for i in range(0, n_samples, batch_size):
            X_batch = X_shuffled[i:i+batch_size]
            y_batch = y_shuffled[i:i+batch_size]

            # 前向传播计算预测值
            y_pred = model.forward(X_batch)

            # 计算交叉熵损失
            loss = -np.mean(np.sum(y_batch * np.log(y_pred + 1e-8), axis=1)) # 避免 log(0) 错误
            epoch_loss += loss

            # 反向传播计算梯度
            grads = model.backward(X_batch, y_batch, y_pred)

            # 获取当前模型参数
            params = model.get_params()

            # 使用优化器更新参数
            updated_params = optimizer.update(params, grads)

            # 将更新后的参数设置回模型
            model.set_params(updated_params)

        # 计算平均 epoch 损失
        avg_epoch_loss = epoch_loss / (n_samples / batch_size)
        losses.append(avg_epoch_loss)
        print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_epoch_loss:.4f}")

    return losses

# 生成一些随机训练数据
np.random.seed(0)
input_size = 10
hidden_size = 20
output_size = 2
num_samples = 1000

X_train = np.random.randn(num_samples, input_size)
y_train = np.eye(output_size)[np.random.randint(0, output_size, num_samples)] # One-hot 标签

# 创建 SimpleNN 模型实例
model = SimpleNN(input_size, hidden_size, output_size)

# 使用 Adam 优化器 (可以使用其他我们实现的优化器替换)
optimizer = Adam(learning_rate=0.01)

# 训练神经网络
losses = train_nn(model, X_train, y_train, optimizer, epochs=100, batch_size=32)

# 绘制训练损失曲线
plt.figure(figsize=(8, 6))
plt.plot(losses)
plt.title('Training Loss Curve')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.grid(True)
plt.show()

代码解释

  • SimpleNN: 定义了一个简单的两层全连接神经网络,包含一个隐藏层 (ReLU 激活) 和一个 Softmax 输出层。实现了 forward (前向传播), backward (反向传播), get_params (获取参数), set_params (设置参数) 等方法。
  • train_nn 函数: 实现了神经网络的训练循环,包括:
    • 数据打乱: 每个 epoch 开始前打乱训练数据,避免模型学习到数据的顺序性。
    • 批量训练: 将训练数据分成 mini-batch 进行训练,提高训练效率和鲁棒性。
    • 前向传播: 计算模型预测值。
    • 损失计算: 使用交叉熵损失函数计算模型输出与真实标签之间的损失。
    • 反向传播: 计算损失函数对模型参数的梯度。
    • 参数更新: 使用传入的优化器 (optimizer) 的 update 方法更新模型参数
    • 损失记录和打印: 记录每个 epoch 的平均损失,并打印训练信息。
  • 数据生成: 生成随机的训练数据 X_train 和 one-hot 标签 y_train 用于示例训练。
  • 模型和优化器实例化: 创建 SimpleNN 模型实例和 Adam 优化器实例 (这里可以替换为我们实现的任何其他优化器,如 SGD, Momentum, RMSprop 等)。
  • 模型训练: 调用 train_nn 函数训练模型,并记录训练损失。
  • 损失曲线可视化: 绘制训练过程中的损失曲线,观察模型训练收敛情况。

运行示例

运行上述代码,你将看到神经网络在随机生成的数据上进行训练,并输出每个 epoch 的训练损失。训练结束后,会绘制出训练损失曲线,你可以观察到损失值随着 epoch 增加逐渐下降,表明神经网络在不断学习和优化。

实验扩展

你可以尝试修改 train_nn 函数中的 optimizer 变量,替换为我们实现的 SGD, Momentum, RMSprop 等优化器,并观察不同优化器训练神经网络时的收敛速度和最终性能差异。你也可以尝试调整不同优化器的超参数 (如学习率,动量因子,衰减率等),进一步探索优化器的性能和调优技巧。

总结

本文深入探讨了深度学习中优化器的核心作用和数学本质,并从零开始实现了多种经典的优化算法,包括 SGD、Momentum、NAG、AdaGrad、RMSprop 和 Adam。通过实验比较和实际应用分析,我们揭示了不同优化器的特性、优缺点和适用场景。

希望通过本文的阐述,读者能够对深度学习优化器有更深刻的理解,不再仅仅停留在“调包”层面,而是能够理解优化器背后的数学原理和代码实现,从而在实际项目中更加灵活和高效地选择和使用优化器,驱动深度学习模型不断进化,在人工智能的道路上更进一步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海棠AI实验室

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值