Skip to content

梯度下降优化算法中,可能会碰到以下情况:

  • 碰到平缓区域,梯度值较小,参数优化变慢
  • 碰到“鞍点”,梯度为0,参数无法优化
  • 碰到局部最小值,参数不是最优

因此更加先进的优化算法,来改善参数更新策略,使得训练过程更稳定、更快速,并更容易找到更优的解,从而有效降低损失函数。


1. 指数移动加权平均 ​

1.1 公式定义 ​

指数移动加权平均(Exponential Moving Average, EMA)是一种用于计算时间序列数据平均值的统计方法,其中最近的观测值被赋予更大的权重,而较旧的观测值权重按指数方式递减。公式如下:

\[S_t = \begin{cases} Y_1, & t = 0 \\ \beta \cdot S_{t-1} + (1 - \beta) \cdot Y_t, & t > 0 \end{cases} \]

其中:

  • \(S_t\):时间 \(t\) 的指数加权平均值。
  • \(Y_t\):时间 \(t\) 的实际观测值。
  • \(\beta\):平滑因子(介于0和1之间),控制权重的衰减速度。

展开 \(S_t\):

\[\begin{align*} S_t &= \beta S_{t-1} + (1 - \beta) Y_t \\ &= \beta (\beta S_{t-2} + (1 - \beta) Y_{t-1}) + (1 - \beta) Y_t \\ &= \beta^2 S_{t-2} + \beta (1 - \beta) Y_{t-1} + (1 - \beta) Y_t \\ &= \beta^3 S_{t-3} + \beta^2 (1 - \beta) Y_{t-2} + \beta (1 - \beta) Y_{t-1} + (1 - \beta) Y_t \\ &\ \vdots \\ &= (1 - \beta) Y_t + \beta (1 - \beta) Y_{t-1} + \beta^2 (1 - \beta) Y_{t-2} + \cdots + \beta^{t-1} (1 - \beta) Y_1 + \beta^t S_0 \\ &= (1 - \beta) \sum_{k=0}^{t-1} \beta^k Y_{t-k} + \beta^t Y_0 \end{align*} \]

可以看到,每个历史观测值 \(Y_{t-k}\) 的权重是 \((1 - \beta) \beta^k\),这是一个指数递减的权重:

  • \(\beta\) 越大(接近1),过去的平均值对当前的影响越大,新的观测值的影响越小,因此平均值变化更平缓(更“平滑”)。
  • \(\beta\) 越小(接近0),新的观测值的影响越大,平均值对近期变化更敏感,波动更大。

1.2 计算示例 ​

模拟一个月(30天)的每日温度,并用指数移动平均(EMA)方法来平滑这些温度数据:

python
import torch
import matplotlib.pyplot as plt

ELEMENT_NUMBER = 30  # 定义元素数量为30

# 1. 实际平均温度
def dayTemperature():
    # 固定随机数种子
    torch.manual_seed(0)
    # 产生30天的随机温度,范围在10左右波动
    temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10
    print(temperature)
    # 绘制平均温度
    days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
    plt.plot(days, temperature, color='r', label='Daily Temperature')
    plt.scatter(days, temperature)
    plt.xlabel('Day')
    plt.ylabel('Temperature')
    plt.title('Actual Daily Temperature')
    plt.legend()
    plt.show()

# 2. 指数移动平均温度
def EMATemperature(beta=0.9):
    torch.manual_seed(0)  # 固定随机数种子
    temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10  # 产生30天的随机温度
    exp_weight_avg = []
    for idx, temp in enumerate(temperature, 1):  # 从下标1开始(实际是第0天)
        # 第一个元素的EMA值等于自身
        if idx == 1:
            exp_weight_avg.append(temp)
            continue
        # 后续元素的 EMA 值等于上一个 EMA * β + 当前气温乘以(1-β)
        new_temp = exp_weight_avg[idx - 2] * beta + (1 - beta) * temp
        exp_weight_avg.append(new_temp)
    days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
    plt.plot(days, exp_weight_avg, color='b', label='EMA Temperature')
    plt.scatter(days, temperature, color='r', label='Daily Temperature')
    plt.xlabel('Day')
    plt.ylabel('Temperature')
    plt.title(f'Exponential Moving Average (beta={beta})')
    plt.legend()
    plt.show()

# 测试
dayTemperature()  # 绘制实际温度
EMATemperature()  # 绘制EMA温度(默认beta=0.9)
EMATemperature(beta=0.5)  # 绘制EMA温度(beta=0.5)

2. 动量算法 ​

2.1 公式定义 ​

动量算法(Momentum)让优化过程逐渐趋于平缓:

  • 若梯度方向持续一致 → 动量累积 → 加速下降
  • 若梯度方向忽快忽慢 → 动量平均作用 → 减少震荡

梯度计算公式:

\[s_t = \beta s_{t-1} + (1-\beta)g_t \]
符号含义
\(s_t\)当前时刻 指数加权平均的梯度(动量)
\(s_{t-1}\)上一时刻的动量
\(g_t\)当前梯度(loss 对参数的偏导)
\(\beta\)动量系数(一般取 0.9 或 0.99),值越大,表示越信任历史加权累积的权重,惯性越强

梯度更新公式:

\[w_t = w_{t-1} - \eta s_t \]
符号含义
\(w_t\)当前权重参数
\(w_{t-1}\)上一时刻权重参数
\(\eta\)学习率(learning rate)
\(s_t\)累积动量(平滑梯度)

普通梯度下降是:

\[w_t = w_{t-1} - \eta g_t \]

但 Momentum 用的是 动量 \(s_t\) 而不是瞬时梯度 \(g_t\)。

2.2 计算示例 ​

实验:比较普通梯度下降(GD)与动量(Momentum)在一个条件数较大的二次函数上的表现:

  1. 在函数 \(f(x,y) = 0.5*(a*x^2 + b*y^2)\) 上做优化(a >> b 造成峡谷/震荡)

  2. 使用相同学习率分别运行普通 GD 和 Momentum(beta=0.9)

  3. 绘制出等高线与两条参数轨迹,以及每次迭代的 loss 曲线

python
import numpy as np
import matplotlib.pyplot as plt

# 目标函数参数(条件数较大)
a = 100.0
b = 1.0

def loss(w):
    x, y = w
    return 0.5*(a*x**2 + b*y**2)

def grad(w):
    x, y = w
    return np.array([a*x, b*y])

# 优化器实现
def grad_descent(w0, lr, steps):
    w = w0.copy()
    traj = [w.copy()]
    losses = [loss(w)]
    for t in range(steps):
        g = grad(w)
        w = w - lr * g
        traj.append(w.copy())
        losses.append(loss(w))
    return np.array(traj), np.array(losses)

def momentum(w0, lr, beta, steps):
    w = w0.copy()
    v = np.zeros_like(w)
    traj = [w.copy()]
    losses = [loss(w)]
    for t in range(steps):
        g = grad(w)
        v = beta * v + (1 - beta) * g  # 指数加权平均梯度
        w = w - lr * v
        traj.append(w.copy())
        losses.append(loss(w))
    return np.array(traj), np.array(losses)

# 初始点与超参数
w0 = np.array([-3.0, 3.0])
lr = 0.01   # 学习率(对比同一学习率下的表现)
beta = 0.9
steps = 200

traj_gd, losses_gd = grad_descent(w0, lr, steps)
traj_mom, losses_mom = momentum(w0, lr, beta, steps)

# 绘图1:等高线 + 两条轨迹
# 生成网格与等高线
xs = np.linspace(-3.5, 3.5, 400)
ys = np.linspace(-3.5, 3.5, 400)
X, Y = np.meshgrid(xs, ys)
Z = 0.5*(a*X**2 + b*Y**2)

plt.figure(figsize=(7,6))
cs = plt.contour(X, Y, Z, levels=30)
plt.plot(traj_gd[:,0], traj_gd[:,1], marker='o', markersize=3, linewidth=1, label='GD trajectory')
plt.plot(traj_mom[:,0], traj_mom[:,1], marker='x', markersize=3, linewidth=1, label='Momentum trajectory (beta=0.9)')
plt.scatter([w0[0]], [w0[1]], marker='s')  # 起点
plt.title('Trajectories on loss contours')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.axis('equal')
plt.grid(True)
plt.show()

# 绘图2:loss vs iteration
plt.figure(figsize=(7,5))
plt.semilogy(range(len(losses_gd)), losses_gd, linewidth=1, label='GD loss')
plt.semilogy(range(len(losses_mom)), losses_mom, linewidth=1, label='Momentum loss (beta=0.9)')
plt.title('Loss vs Iteration (log scale)')
plt.xlabel('Iteration')
plt.ylabel('Loss (log)')
plt.legend()
plt.grid(True)
plt.show()

# 输出一些数值比较:最终的坐标和损失
final_info = {
    'GD_final_w': traj_gd[-1].tolist(),
    'Momentum_final_w': traj_mom[-1].tolist(),
    'GD_final_loss': float(losses_gd[-1]),
    'Momentum_final_loss': float(losses_mom[-1])
}
final_info

3. RMSProp 算法 ​

3.1 公式定义 ​

RMSProp(Root Mean Square Propagation)是 AdaGrad 的改进版本。

  • AdaGrad:对每个参数维度累积梯度平方,使陡峭方向学习率变小、平缓方向学习率相对大
  • 但 AdaGrad 累积所有历史梯度平方 → 会越来越大 → 学习率会越来越小 → 最终训练停滞

RMSProp 算法不对所有历史梯度平方平均,而是使用指数加权平均(EWMA)。

首先初始化:

  • 学习率:\(\eta\)
  • 初始参数:\(w_0\)
  • 小常数:\(\epsilon = 10^{-10}\)(用于数值稳定性)
  • 梯度平方的指数加权项:\(s_0 = 0\)

从 batch 中取 m 个样本,梯度计算公式:

\[g_t = \nabla_w L(w_t) \]

指数加权平均累积梯度平方:

\[s_t = \beta s_{t-1} + (1-\beta)(g_t \odot g_t) \]
符号含义
\(s_t\)梯度平方的指数加权平均(类似“最近梯度平方”)
\(\beta\)一般取 0.9,用于平滑
\(g_t ⊙ g_t\)梯度按元素平方:即 \(g_t^2\)

计算自适应学习率(RMS 部分):

\[\sqrt{s_t + \epsilon} \]

参数更新公式:

\[w_t = w_{t-1} - \eta \frac{g_t}{\sqrt{s_t + \epsilon}} \]

3.2 计算示例 ​

比较 RMSProp 与 普通GD 的优化轨迹和收敛情况:

python
import numpy as np
import matplotlib.pyplot as plt

# 目标函数
def loss(w):
    x, y = w
    return 0.5*(100*x**2 + y**2)

def grad(w):
    x, y = w
    return np.array([100*x, y])

def gd(w0, lr, steps):
    w = w0.copy()
    traj = [w.copy()]
    losses = [loss(w)]
    for _ in range(steps):
        g = grad(w)
        w = w - lr * g
        traj.append(w.copy())
        losses.append(loss(w))
    return np.array(traj), np.array(losses)

def rmsprop(w0, lr, beta, eps, steps):
    w = w0.copy()
    s = np.zeros_like(w)
    traj = [w.copy()]
    losses = [loss(w)]
    for _ in range(steps):
        g = grad(w)
        s = beta*s + (1-beta)*(g*g)
        w = w - lr * g / (np.sqrt(s + eps))
        traj.append(w.copy())
        losses.append(loss(w))
    return np.array(traj), np.array(losses)

# 参数
w0 = np.array([-3.0, 3.0])
lr = 0.01
beta = 0.9
eps = 1e-8
steps = 200

traj_gd, loss_gd = gd(w0, lr, steps)
traj_rms, loss_rms = rmsprop(w0, lr, beta, eps, steps)

# 图1:轨迹
xs = np.linspace(-3.5, 3.5, 400)
ys = np.linspace(-3.5, 3.5, 400)
X, Y = np.meshgrid(xs, ys)
Z = 0.5*(100*X**2 + Y**2)

plt.figure(figsize=(7,6))
plt.contour(X, Y, Z, levels=30)
plt.plot(traj_gd[:,0], traj_gd[:,1], marker='o', markersize=3, linewidth=1, label="GD")
plt.plot(traj_rms[:,0], traj_rms[:,1], marker='x', markersize=3, linewidth=1, label="RMSProp")
plt.scatter([w0[0]], [w0[1]], marker='s')
plt.title("Trajectories on Loss Contours")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.axis("equal")
plt.grid(True)
plt.show()

# 图2:Loss vs Iteration
plt.figure(figsize=(7,5))
plt.semilogy(loss_gd, linewidth=1, label="GD loss")
plt.semilogy(loss_rms, linewidth=1, label="RMSProp loss")
plt.title("Loss vs Iteration (log scale)")
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.legend()
plt.grid(True)
plt.show()

{
    "GD_final_loss": float(loss_gd[-1]),
    "RMSProp_final_loss": float(loss_rms[-1]),
    "GD_final_w": traj_gd[-1].tolist(),
    "RMSProp_final_w": traj_rms[-1].tolist()
}

4. Adam 算法 ​

4.1 公式定义 ​

Adam 是目前深度学习中最常用的优化算法之一,它结合了:

  • Momentum(动量法):使用一阶矩,即梯度的指数加权平均
  • RMSProp:使用二阶矩,即梯度平方的指数加权平均
  • 偏差校正(bias correction):使指数加权平均在前期更准确

Adam 同时维护两个指数加权平均,计算了梯度的一阶矩(平均值)和二阶矩(梯度的方差)的自适应估计,从而动态调整学习率。

一阶矩计算:

\[m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t \]

二阶矩计算:

\[s_t = \beta_2 s_{t-1} + (1-\beta_2) g_t^2 \]

其中:

  • \(m_t\):一阶矩(梯度平均)
  • \(s_t\):二阶矩(平方梯度平均)
  • \(g_t\):当前梯度
  • \(\beta_{1}\) 通常取 0.9
  • \(\beta_{2}\) 通常取 0.999
  • 初始化 \(m_0 = 0, s_0 = 0\)

前期的 \(m_t\) 和 \(s_t\) 会偏向 0,因为指数加权平均初始值为 0,所以要进行偏差修正:

\[\hat{m}_t = \frac{m_t}{1-\beta_1^t} \]
\[\hat{s}_t = \frac{s_t}{1-\beta_2^t} \]

参数更新:

\[w_t = w_{t-1} - \alpha \frac{\hat{m}_t}{\sqrt{\hat{s}_t} + \epsilon} \]

其中:

  • \(\hat{m}_t\):偏差修正后的一阶矩,控制方向
  • \(\hat{s}_t\):偏差修正后的二阶矩,控制自适应学习率
  • \(\epsilon\):防止除零(一般为 1e-8)
  • \(\alpha\):初始学习率(一般为 0.001)

4.2 计算示例 ​

python
import numpy as np
import matplotlib.pyplot as plt

# 目标函数:强不等轴二次函数(常用于展示优化器差异)
def loss(w):
    x, y = w
    return 0.5 * (100*x**2 + y**2)

def grad(w):
    x, y = w
    return np.array([100*x, y])

# 普通梯度下降
def gd(w0, lr, steps):
    w = w0.copy()
    traj = [w.copy()]
    losses = [loss(w)]
    for _ in range(steps):
        g = grad(w)
        w = w - lr * g
        traj.append(w.copy())
        losses.append(loss(w))
    return np.array(traj), np.array(losses)

# Adam
def adam(w0, lr, beta1, beta2, eps, steps):
    w = w0.copy()
    m = np.zeros_like(w)
    s = np.zeros_like(w)
    traj = [w.copy()]
    losses = [loss(w)]
    for t in range(1, steps+1):
        g = grad(w)
        m = beta1*m + (1-beta1)*g
        s = beta2*s + (1-beta2)*(g*g)

        # 偏差修正
        m_hat = m / (1 - beta1**t)
        s_hat = s / (1 - beta2**t)

        w = w - lr * m_hat / (np.sqrt(s_hat) + eps)

        traj.append(w.copy())
        losses.append(loss(w))
    return np.array(traj), np.array(losses)

# 参数设置
w0 = np.array([-3.0, 3.0])
steps = 200
lr_gd = 0.01
lr_adam = 0.01
beta1 = 0.9
beta2 = 0.999
eps = 1e-8

traj_gd, loss_gd = gd(w0, lr_gd, steps)
traj_adam, loss_adam = adam(w0, lr_adam, beta1, beta2, eps, steps)

xs = np.linspace(-3.5, 3.5, 400)
ys = np.linspace(-3.5, 3.5, 400)
X, Y = np.meshgrid(xs, ys)
Z = 0.5*(100*X**2 + Y**2)

plt.figure(figsize=(7,6))
plt.contour(X, Y, Z, levels=30)
plt.plot(traj_gd[:,0], traj_gd[:,1], marker='o', markersize=3, linewidth=1, label="GD")
plt.plot(traj_adam[:,0], traj_adam[:,1], marker='x', markersize=3, linewidth=1, label="Adam")
plt.scatter([w0[0]], [w0[1]], marker='s')
plt.title("Trajectories of GD vs Adam")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.grid(True)
plt.axis("equal")
plt.show()


plt.figure(figsize=(7,5))
plt.semilogy(loss_gd, linewidth=1, label="GD loss")
plt.semilogy(loss_adam, linewidth=1, label="Adam loss")
plt.title("Loss vs Iteration (log scale)")
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.legend()
plt.grid(True)
plt.show()

{
    "GD_final_loss": float(loss_gd[-1]),
    "Adam_final_loss": float(loss_adam[-1]),
    "GD_final_w": traj_gd[-1].tolist(),
    "Adam_final_w": traj_adam[-1].tolist()
}