梯度下降的三种常见优化演示(附带代码演示与结果对比)

346 阅读3分钟
  • 在前一篇文章中,我们介绍了梯度下降的相关概念与计算实现,上文中所描述的目标函数都能通过梯度下降快速稳定地找到极小值点,然而在实际的深度学习之中,损失函数的通常为高维度非凸函数,存在大量的局部极小值和鞍点。在保证梯度下降能知道局部最优解的同时,确保收敛速度,这些都是各种梯度下降优化算法的考量指标。

那么接下来我们就介绍梯度下降的相关优化

1. 批量梯度下降BGD

  • 批量梯度下降在每一步迭代中使用整个训练集的数据计算损失函数的梯度,并更新模型参数。其目标是找到使损失函数最小化的参数值。

  • 批量梯度下降算法的步骤

    • 初始化参数:随机选择初始参数(如权重 w)。

    • 计算梯度:计算损失函数J(w)J(w)对整个训练集(m个样本)的梯度:

      J(w)=1mi=1mL(w;xi,yi)∇J(w) = \frac{1}{m}\sum_{i=1}^m∇L(w;x_i,y_i)
      • 其中L(w;xi,yi)L(w;x_i,y_i)为单个样本的损失值。
    • 更新参数:沿着负梯度的方向更新参数(ηη为学习率)

      wt+1=wtηJ(w)w_{t+1} = w_t-η·∇J(w)
    • 重复迭代:迭代至梯度收敛或者达到最大迭代次数。

  • 批量梯度下降算法的优缺点

    • 优点:
      • 理论保证收敛到全局最优(对于凸函数)或局部最优(非凸函数)。
      • 梯度计算无偏,适合小规模或中等规模数据集。
    • 缺点:
      • 大数据集下每次迭代计算慢。
      • 无法在线学习(即不能逐步加入新数据)。

2. 随机梯度下降SGD

  • 随机梯度下降在每一步迭代中使用一个随机样本的数据计算损失函数的梯度,并更新模型参数。其目标是找到使损失函数最小化的参数值。

  • 随机梯度下降算法的步骤

    • 初始化参数:随机选择初始参数(如权重 w)。

    • 随机打乱数据:在每次迭代之前,打乱训练数据的顺序,防止模型因为数据顺序引入偏差。

    • 计算梯度:计算损失函数对J(w)J(w)对当前样本的梯度:

      J(w)=L(w;xi,yi)∇J(w)=∇L(w;x_i,y_i)
    • 更新参数:沿着负梯度方向更新参数,(η 为学习率)

      wt+1=wtηJ(w)w_{t+1} = w_t-η·∇J(w)
    • 重复迭代:迭代至梯度收敛或者达到最大迭代次数。

  • 随机梯度下降算法的优缺点

    • 优点

      • 计算速度快,特别适合大规模数据集。

      • 可以逃离局部极小值(由于噪声的引入)。

      • 在线学习能力强,可以处理动态新增的数据。

    • 缺点

      • 收敛路径曲折,存在较多噪声。

      • 对学习率敏感,需要仔细调整。

      • 可能难以收敛到精确的最小值点。

3. 小批量梯度下降Mini-Batch GD

  • 小批量梯度下降它结合了批量梯度下降(BGD)和随机梯度下降(SGD)的优点。它的特点是每一步迭代中使用一个批次的数据(既不是单个样本,也不是全部数据)计算损失函数的梯度,并更新模型参数,是BGD和SGD的折中方案。

  • 小批量梯度下降算法的步骤

    • 初始化参数:随机选择初始参数(如权重 w),确定批量大小。

    • 随机打乱数据:在每次迭代之前,打乱训练数据的顺序,防止模型因为数据顺序引入偏差。

    • 计算梯度:计算损失函数对J(w)J(w)对当前批次的梯度:

      J(w)=1mi=1mL(w;xi,yi)∇J(w)= \frac{1}{m}\sum_{i=1}^m∇L(w;x_i,y_i)
      • 其中mm是当前批次的样本数量。
    • 更新参数:沿着负梯度方向更新参数,(η 为学习率)

      wt+1=wtηJ(w)w_{t+1} = w_t-η·∇J(w)
    • 重复迭代:迭代至梯度收敛或者达到最大迭代次数。

  • 小批量梯度下降算法的优缺点

    • 优点
      • 相对于BGD,MBGD减少了每次迭代的计算量,提高了训练速度。
      • 相对于SGD,MBGD的梯度估计更加稳定,减少了更新方向的波动,有助于更稳定地收敛。
      • 可以通过调整batch size来平衡计算量和梯度估计的稳定性。
    • 缺点
      • 需要通过实验与调参去确定最佳批量大小。

代码演示模拟对比三种批量下降算法

1. 批量梯度下降BGD演示

  • 首先,我们使用批量梯度下降BGD对f(x,y)=x2+y2f(x, y) = x^2+y^2进行梯度下降,该函数由于是凸函数,所以初始点的选择不会影响最后的结果,那么我们直接给出BGD算法的具体实现。

    import os
    os.environ['OMP_NUM_THREADS'] = '1'
    os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
    import torch
    import numpy as np
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D
    
    
    # 1. 定义二维函数
    def func(x, y):
        main_func = (x**2 + y**2)
    
        return main_func
    
    
    # 2. 初始化参数
    x = torch.tensor([5.0], requires_grad=True)
    y = torch.tensor([-5.0], requires_grad=True)
    
    
    # 3. 优化参数设置
    learning_rate = 0.4
    iterations = 50
    sample_size = 10  # 每次迭代评估的样本点数
    
    # 存储优化过程
    history = []
    
    # 4. 全批量梯度下降优化
    for i in range(iterations):
        # 清零梯度
        if x.grad is not None:
            x.grad.zero_()
        if y.grad is not None:
            y.grad.zero_()
    
        # 用于累积梯度的变量
        grad_x = 0
        grad_y = 0
    
        # 评估多个样本点(模拟全批量)
        for _ in range(sample_size):
            # 计算函数值(无噪声)
            z = func(x, y)
    
            # 计算梯度
            z.backward()
    
            # 累积梯度
            grad_x += x.grad.item()
            grad_y += y.grad.item()
    
            # 清零梯度以便下次计算
            x.grad.zero_()
            y.grad.zero_()
    
        # 计算平均梯度
        avg_grad_x = grad_x / sample_size
        avg_grad_y = grad_y / sample_size
    
        # 更新参数
        with torch.no_grad():
            x -= learning_rate * avg_grad_x
            y -= learning_rate * avg_grad_y
    
            # 记录当前参数和函数值
            true_z = func(x, y)
            history.append([x.item(), y.item(), true_z.item()])
    
    # 5. 可视化
    x_vals = np.linspace(-2, 2, 100)
    y_vals = np.linspace(-2, 2 , 100)
    X, Y = np.meshgrid(x_vals, y_vals)
    Z = np.zeros_like(X)
    
    for i in range(X.shape[0]):
        for j in range(X.shape[1]):
            Z[i, j] = func(torch.tensor(X[i, j]), torch.tensor(Y[i, j])).item()
    
    history = np.array(history)
    
    fig = plt.figure(figsize=(14, 6))
    
    # 3D表面图
    ax1 = fig.add_subplot(121, projection='3d')
    surf = ax1.plot_surface(X, Y, Z, cmap='coolwarm', alpha=0.7)
    ax1.scatter(history[:, 0], history[:, 1], history[:, 2], c='black', s=30)
    ax1.plot(history[:, 0], history[:, 1], history[:, 2], 'k-', lw=1.5)
    ax1.set_title('Full Batch Optimization Path')
    ax1.set_xlabel('x')
    ax1.set_ylabel('y')
    
    # 2D等高线图
    ax3 = fig.add_subplot(122)
    contour = ax3.contour(X, Y, Z, 15, cmap='coolwarm')
    ax3.scatter(history[:, 0], history[:, 1], c='black', s=20, label='Optimization path')
    ax3.plot(history[:, 0], history[:, 1], 'k-', lw=1.5)
    ax3.set_title('Contour Comparison')
    ax3.legend()
    plt.colorbar(contour, ax=ax3)
    
    plt.tight_layout()
    plt.show()
    
    # 打印最终结果
    print(f"\nFinal result:")
    print(f"x = {x.item():.4f}, y = {y.item():.4f}")
    print(f"Minimum value f(x,y) = {func(x, y).item():.4f}")
    
    • 实验结果: 在这里插入图片描述 在这里插入图片描述

      可以观察到梯度下降的结果十分完美,笔直地找到了最后结果。但是如果我们换一个非凸函数又会有什么样的结果呢?

  • 我们修改函数构造的部分,保留算法其他部分。

    修改后的函数表达式为

    f(x,y)=(sin(0.5x)+cos(0.3y)+sin(0.2xy))+0.1(x2+y2)+2sin(x+0.5y)f(x,y)=(sin(0.5·x)+cos(0.3·y)+sin(0.2·x·y))+0.1·(x^2+y^2)+2·sin(x+0.5·y)

    该函数存在多个局部极小值,我们再次运行看有什么样的结果。

    # 1. 定义新的复杂二维函数
    def func(x, y):
        # 主要函数部分 - 包含多个sin/cos项创造多个局部极值
        main_func = (torch.sin(0.5 * x) * torch.cos(0.3 * y) +
                     torch.sin(0.2 * x * y) +
                     0.1 * (x ** 2 + y ** 2) +
                     2 * torch.sin(x + 0.5 * y))
    
        return main_func
    

    并且我们在初始点的选择上分布选择(3, -3)和(5,-5)。

    • 实验结果:

      • 初始点选择(3, -3)

        在这里插入图片描述 在这里插入图片描述

        • 初始点选择(5,-5) 在这里插入图片描述 在这里插入图片描述
      • 结果分析

        我们可以清楚得到,两次初始点的选择显著影响了两次梯度下降的路径与结果。之所以造成这样的结果,是因为梯度下降是一种迭代优化算法,用于寻找目标函数的局部最小值。其核心思想是沿着当前点的梯度反方向(即函数下降最快的方向)逐步更新参数。所以,算法无法判断是否存在更优的局部最小值,只能沿着当前点的梯度方向下降。

        可见在实际生产中,如何避开此影响,找到最优结果也十分的重要。鉴于此,以后的两种算法我们统一选择使用非凸函数进行演示。

2. 随机梯度下降SGD演示

  • 接一下,我们通过引入噪声影响来模拟实际生产中随机选择样本的过程。

    函数的选择是上文的非凸函数:

    f(x,y)=(sin(0.5x)+cos(0.3y)+sin(0.2xy))+0.1(x2+y2)+2sin(x+0.5y)f(x,y)=(sin(0.5·x)+cos(0.3·y)+sin(0.2·x·y))+0.1·(x^2+y^2)+2·sin(x+0.5·y)
    import os
    os.environ['OMP_NUM_THREADS'] = '1'
    os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
    import torch
    import numpy as np
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D
    
    
    # 1. 定义新的复杂二维函数
    def func(x, y, noise=False, iteration=None):
        # 主要函数部分 - 包含多个sin/cos项创造多个局部极值
        main_func = (torch.sin(0.5 * x) * torch.cos(0.3 * y) +
                     torch.sin(0.2 * x * y) +
                     0.1 * (x ** 2 + y ** 2) +
                     2 * torch.sin(x + 0.5 * y))
    
        # 添加显著噪声模拟SGD
        if noise:
            noise_level = 0.5  # 基础噪声水平
            if iteration is not None:
                # 噪声随迭代变化
                noise_level *= (1 + 0.3 * torch.sin(torch.tensor(iteration / 3.0)))
            return main_func + noise_level * torch.randn(1) * 2
        return main_func
    
    
    # 2. 初始化参数
    x = torch.tensor([3.0], requires_grad=True)
    y = torch.tensor([-3.0], requires_grad=True)
    
    # 3. 优化参数设置
    learning_rate = 0.4
    iterations = 50
    
    # 存储优化过程
    history = []
    batch_history = []  # 用于存储带噪声的位置
    
    # 在计算带噪声梯度时,应该基于扰动后的位置
    for i in range(iterations):
        # 添加位置噪声
        noisy_x = x + torch.randn(1) * 0.5  # 位置噪声
        noisy_y = y + torch.randn(1) * 0.5
    
        # 计算带噪声的函数值
        z = func(noisy_x, noisy_y, True, i)
    
        z.backward()
    
        with torch.no_grad():
            # 普通SGD更新
            x -= learning_rate * x.grad
            y -= learning_rate * y.grad
    
            # 记录真实位置和无噪声值
            true_z = func(x, y, False)
            history.append([x.item(), y.item(), true_z.item()])
    
            # 记录带噪声的位置和值
            batch_history.append([noisy_x.item(), noisy_y.item(), z.item()])
            # 清零梯度
            x.grad.zero_()
            y.grad.zero_()
    
    # 5. 可视化
    # 准备网格数据
    x_vals = np.linspace(-5, 5, 100)
    y_vals = np.linspace(-5, 5, 100)
    X, Y = np.meshgrid(x_vals, y_vals)
    Z = np.zeros_like(X)
    
    # 计算真实函数值
    for i in range(X.shape[0]):
        for j in range(X.shape[1]):
            Z[i, j] = func(torch.tensor(X[i, j]), torch.tensor(Y[i, j]), False).item()
    
    history = np.array(history)
    batch_history = np.array(batch_history)
    
    # 创建可视化图形
    fig = plt.figure(figsize=(14, 6))
    
    # 3D表面图
    ax1 = fig.add_subplot(121, projection='3d')
    surf = ax1.plot_surface(X, Y, Z, cmap='coolwarm', alpha=0.7)
    ax1.scatter(history[:, 0], history[:, 1], history[:, 2], c='black', s=30)
    ax1.plot(history[:, 0], history[:, 1], history[:, 2], 'k-', lw=1.5)
    ax1.set_title('Optimization Path (True Function)')
    ax1.set_xlabel('x')
    ax1.set_ylabel('y')
    
    # 2D等高线图对比
    ax3 = fig.add_subplot(122)
    contour = ax3.contour(X, Y, Z, 15, cmap='coolwarm')
    ax3.scatter(history[:, 0], history[:, 1], c='black', s=20, label='True path')
    ax3.plot(history[:, 0], history[:, 1], 'k-', lw=1.5)
    ax3.set_title('Contour Comparison')
    ax3.legend()
    plt.colorbar(contour, ax=ax3)
    
    plt.tight_layout()
    plt.show()
    
    # 打印最终结果
    print(f"\nFinal result:")
    print(f"x = {x.item():.4f}, y = {y.item():.4f}")
    print(f"Minimum value f(x,y) = {func(x, y, False).item():.4f}")
    
    • 实验结果: 在这里插入图片描述 在这里插入图片描述

      • 可以看出,在其他参数条件不变的情况,SGD相较于BGD,梯度下降的路径更加曲折并且最后的结果并没有拟合到最佳位置。但是由于SGD没有BGD的复杂计算过程,所以SGD在大数据规模的数据集上运算速度有着显著的优势。

3. 小批量梯度下降Mini-Batch GD演示

  • 接下来,为了实现小批量梯度下降算法,我们在SGD的基础上添加上BGD的运算思路,依次来模拟小批量梯度下降算法的运输过程。

    函数的选择依旧是上文的非凸函数:

    f(x,y)=(sin(0.5x)+cos(0.3y)+sin(0.2xy))+0.1(x2+y2)+2sin(x+0.5y)f(x,y)=(sin(0.5·x)+cos(0.3·y)+sin(0.2·x·y))+0.1·(x^2+y^2)+2·sin(x+0.5·y)
    import os
    os.environ['OMP_NUM_THREADS'] = '1'
    os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
    import torch
    import numpy as np
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D
    
    
    # 1. 定义函数 (保持不变)
    def func(x, y, noise=False, iteration=None):
        main_func = (torch.sin(0.5 * x) * torch.cos(0.3 * y) +
                     torch.sin(0.2 * x * y) +
                     0.1 * (x ** 2 + y ** 2) +
                     2 * torch.sin(x + 0.5 * y))
    
        if noise:
            noise_level = 0.5
            if iteration is not None:
                noise_level *= (1 + 0.3 * torch.sin(torch.tensor(iteration / 3.0)))
            return main_func + noise_level * torch.randn(1) * 2
        return main_func
    
    
    # 2. 初始化参数 (保持不变)
    x = torch.tensor([3.0], requires_grad=True)
    y = torch.tensor([-3.0], requires_grad=True)
    
    # 3. 优化参数设置 - 添加小批量相关参数
    learning_rate = 0.4
    iterations = 50
    batch_size = 10  # 小批量大小
    
    # 存储优化过程
    history = []
    batch_history = []
    
    # 4. 小批量梯度下降优化
    for i in range(iterations):
        # 清零梯度
        if x.grad is not None:
            x.grad.zero_()
        if y.grad is not None:
            y.grad.zero_()
    
        # 用于累积梯度的变量
        grad_x = 0
        grad_y = 0
    
        # 处理一个小批量
        for _ in range(batch_size):
            # 添加位置噪声
            noisy_x = x + torch.randn(1) * 0.5
            noisy_y = y + torch.randn(1) * 0.5
    
            # 计算带噪声的函数值
            z = func(noisy_x, noisy_y, True, i)
    
            # 计算梯度
            z.backward()
    
            # 累积梯度
            grad_x += x.grad.item()
            grad_y += y.grad.item()
    
            # 记录带噪声的位置和值
            batch_history.append([noisy_x.item(), noisy_y.item(), z.item()])
    
            # 清零梯度以便下次计算
            x.grad.zero_()
            y.grad.zero_()
    
        # 计算平均梯度
        avg_grad_x = grad_x / batch_size
        avg_grad_y = grad_y / batch_size
    
        # 更新参数
        with torch.no_grad():
            x -= learning_rate * avg_grad_x
            y -= learning_rate * avg_grad_y
    
            # 记录真实位置和无噪声值
            true_z = func(x, y, False)
            history.append([x.item(), y.item(), true_z.item()])
    
    # 5. 可视化 (保持不变)
    x_vals = np.linspace(-5, 5, 100)
    y_vals = np.linspace(-5, 5, 100)
    X, Y = np.meshgrid(x_vals, y_vals)
    Z = np.zeros_like(X)
    
    for i in range(X.shape[0]):
        for j in range(X.shape[1]):
            Z[i, j] = func(torch.tensor(X[i, j]), torch.tensor(Y[i, j]), False).item()
    
    history = np.array(history)
    batch_history = np.array(batch_history)
    
    fig = plt.figure(figsize=(14, 6))
    
    # 3D表面图
    ax1 = fig.add_subplot(121, projection='3d')
    surf = ax1.plot_surface(X, Y, Z, cmap='coolwarm', alpha=0.7)
    ax1.scatter(history[:, 0], history[:, 1], history[:, 2], c='black', s=30)
    ax1.plot(history[:, 0], history[:, 1], history[:, 2], 'k-', lw=1.5)
    ax1.set_title('Mini-batch Optimization Path (True Function)')
    ax1.set_xlabel('x')
    ax1.set_ylabel('y')
    
    # 2D等高线图
    ax3 = fig.add_subplot(122)
    contour = ax3.contour(X, Y, Z, 15, cmap='coolwarm')
    ax3.scatter(history[:, 0], history[:, 1], c='black', s=20, label='True path')
    ax3.plot(history[:, 0], history[:, 1], 'k-', lw=1.5)
    ax3.set_title('Contour Comparison')
    ax3.legend()
    plt.colorbar(contour, ax=ax3)
    
    plt.tight_layout()
    plt.show()
    
    # 打印最终结果
    print(f"\nFinal result:")
    print(f"x = {x.item():.4f}, y = {y.item():.4f}")
    print(f"Minimum value f(x,y) = {func(x, y, False).item():.4f}")
    
    • 实验结果

      在这里插入图片描述 在这里插入图片描述

      • 显然,小批量梯度下降算法在无论是梯度下降的路径还是梯度下降的结果上都要更优于SGD算法,但是也不及BGD算法的准确性。但是小批量梯度下降算法显然在运算速度优于BGD。