深入解析SGD、Momentum与Nesterov:优化算法的对比与应用

news2024/9/25 3:32:56

目录

  • 1. 梯度下降算法
  • 2. BGD、SGD、MBGD
  • 3. momentum与dampening
    • 3.1 另一种形式的momentum
      • 3.1.1 学习率固定
      • 3.1.2 学习率不固定
  • 4. nesterov
    • 4.1 PyTorch中的Nesterov
    • 4.2 Polyak与Nesterov的比较
  • Ref

1. 梯度下降算法

先考虑一元情形。假设待更新的参数为 θ \theta θ,学习率为 η \eta η,目标函数为 f ( θ ) f(\theta) f(θ),则更新策略表示为:

θ ← θ − η ⋅ g \theta\leftarrow \theta - \eta\cdot g θθηg

其中 g = f ′ ( θ ) g=f'(\theta) g=f(θ) 是梯度,我们需要沿着梯度的反方向进行更新。

学习率 η \eta η 是我们通常所说的步长。步长过小会导致更新缓慢,步长过大会导致振荡。以 f ( θ ) = θ 2 f(\theta)=\theta^2 f(θ)=θ2 为例, θ = 0 \theta=0 θ=0 是它的极值点。我们从 θ = − 5 \theta=-5 θ=5 开始进行梯度下降,并设置不同的学习率来观察更新的效果。

import numpy as np
import matplotlib.pyplot as plt


def f(x):
    return x**2

def grad_f(x):
    return 2*x

def gradient_descent(start_x, learning_rate, steps):
    x_vals = [start_x]
    for _ in range(steps):
        grad = grad_f(x_vals[-1])
        new_x = x_vals[-1] - learning_rate * grad
        x_vals.append(new_x)
    return np.array(x_vals)


start_x = -5
steps = 10
learning_rates = [0.01, 0.05, 0.2, 0.7, 0.9, 1.01]
x_range = np.linspace(-5.5, 5.5, 400)

fig, axes = plt.subplots(2, 3, figsize=(10, 6.6))

for idx, eta in enumerate(learning_rates):
    ax = axes[idx // 3, idx % 3]
    x_vals = gradient_descent(start_x, eta, steps)
    
    ax.plot(x_range, f(x_range), color='blue')
    ax.plot(x_vals, f(x_vals), 'o-', color='orange')
    
    ax.set_title(f'Learning Rate: {eta}')
    ax.set_xlabel('x')
    ax.set_ylabel('f(x)')
    ax.legend()


plt.tight_layout()
plt.show()

从图中可以观察到,当学习率较小时,参数更新的速度会明显减缓;当学习率 > 0.5 >0.5 >0.5 时,参数更新开始出现振荡现象;而当学习率 > 1 >1 >1 时,更新过程甚至会导致参数发散。

现考虑多元情形(以二元为例)。设 θ = ( θ 1 , θ 2 ) \theta=(\theta_1,\theta_2) θ=(θ1,θ2) f ( θ ) = f ( θ 1 , θ 2 ) f(\theta)=f(\theta_1,\theta_2) f(θ)=f(θ1,θ2),则更新策略为:

θ ← θ − η ⋅ ∇ f ( θ ) ⇔ ( θ 1 , θ 2 ) ← ( θ 1 , θ 2 ) − η ⋅ ( ∂ f ∂ θ 1 , ∂ f ∂ θ 2 ) ⇔ θ i ← θ i − η ⋅ ∂ f ∂ θ i , i = 1 , 2 \begin{aligned} &\theta\leftarrow \theta -\eta\cdot \nabla f(\theta) \\ \Leftrightarrow\quad &(\theta_1,\theta_2)\leftarrow(\theta_1,\theta_2)-\eta\cdot\left(\frac{\partial f}{\partial \theta_1},\frac{\partial f}{\partial \theta_2}\right) \\ \Leftrightarrow\quad &\theta_i\leftarrow\theta_i-\eta\cdot \frac{\partial f}{\partial \theta_i},\quad i = 1,2 \end{aligned} θθηf(θ)(θ1,θ2)(θ1,θ2)η(θ1f,θ2f)θiθiηθif,i=1,2

每个参数分别更新。以 f ( θ 1 , θ 2 ) = θ 1 2 + 6 θ 2 2 f(\theta_1,\theta_2)=\theta_1^2+6\theta_2^2 f(θ1,θ2)=θ12+6θ22 为例,显然 ( 0 , 0 ) (0,0) (0,0) 是它的极值点,且 ∇ f ( θ ) = ( 2 θ 1 , 12 θ 2 ) \nabla f(\theta)=(2\theta_1,12\theta_2) f(θ)=(2θ1,12θ2)。我们从 ( − 10 , 3 ) (-10,3) (10,3) 开始进行梯度下降,并设置不同的学习率来观察更新的效果。

import numpy as np
import matplotlib.pyplot as plt

def f(x1, x2):
    return x1**2 + 6*x2**2

def grad_f(x1, x2):
    return np.array([2*x1, 12*x2])

def gradient_descent(start_x, learning_rate, steps):
    x_vals = [start_x]
    for _ in range(steps):
        grad = grad_f(*x_vals[-1])
        new_x = x_vals[-1] - learning_rate * grad
        x_vals.append(new_x)
    return np.array(x_vals)

start_x = np.array([-10, 3])
steps = 20
learning_rates = [0.01, 0.05, 0.1, 0.15, 0.2, 0.5]
x1_range = np.linspace(-12, 2, 200)
x2_range = np.linspace(-6, 6, 200)
x1_grid, x2_grid = np.meshgrid(x1_range, x2_range)
f_vals = f(x1_grid, x2_grid)

fig, axes = plt.subplots(2, 3, figsize=(10, 6.6))

for idx, eta in enumerate(learning_rates):
    ax = axes[idx // 3, idx % 3]
    x_vals = gradient_descent(start_x, eta, steps)
    
    ax.contour(x1_grid, x2_grid, f_vals, levels=15, cmap='Blues')
    ax.plot(x_vals[:, 0], x_vals[:, 1], 'o-', color='orange')
    
    ax.set_title(f'Learning Rate: {eta}')
    ax.set_xlabel(r'$x_1$')
    ax.set_ylabel(r'$x_2$')
    ax.legend()

    ax.set_xlim([-12, 2])
    ax.set_ylim([-6, 6])

plt.tight_layout()
plt.show()

从图中可以看出,多元情形依然有着相同的规律。学习率较小时,参数更新十分缓慢;当学习率增大到一定值时,开始出现振荡现象;继续增大学习率,则会直接导致发散。

据此可以总结出梯度下降法的优缺点。

优点:

  • 计算简单,易于实现。
  • 在凸优化问题中,梯度下降法有严格的收敛性保证。

缺点:

  • 梯度下降法无法保证找到全局最优解,特别是对于非凸函数时,可能会陷入局部极小值或鞍点,而难以跳出。
  • 对学习率敏感。过小的学习率会导致收敛速度过慢,过大的学习率则会使得参数更新出现振荡,甚至导致发散。
  • 对初始点的选择敏感,若初始点距离最优点较远,可能需要更多的迭代次数才能收敛,甚至可能收敛到较差的局部极小值。

2. BGD、SGD、MBGD

在深度学习场景中, θ \theta θ 通常指代模型的参数,而 f ( θ ) f(\theta) f(θ) 则指代模型在训练集上的平均损失,即

f ( θ ) = 1 n ∑ i = 1 n f i ( θ ) f(\theta)=\frac1n\sum_{i=1}^nf_i(\theta) f(θ)=n1i=1nfi(θ)

其中 n n n 是训练集的大小, f i ( θ ) f_i(\theta) fi(θ) 是模型在第 i i i 个样本上的损失。于是

∇ f ( θ ) = 1 n ∑ i = 1 n ∇ f i ( θ ) \nabla f(\theta)=\frac1n\sum_{i=1}^n\nabla f_i(\theta) f(θ)=n1i=1nfi(θ)

如果采用梯度下降法,那么计算 ∇ f ( θ ) \nabla f(\theta) f(θ) 的时间复杂度将是 O ( n ) O(n) O(n) 的。特别是当训练集特别大时,这种方法往往是不可行的。

以上方法又叫Batch Gradient Descent(BGD,批量梯度下降),即每次采用所有样本进行更新。那我们能不能不用那么多样本呢?能不能每次随机挑一个样本呢?

随机挑选时,每个样本被选中的概率为 1 n \frac1n n1,因此

E i [ ∇ f i ( θ ) ] = ∑ i = 1 n 1 n ⋅ ∇ f i ( θ ) = ∇ f ( θ ) \mathbb{E}_i[\nabla f_i(\theta)]=\sum_{i=1}^n\frac1n\cdot \nabla f_i(\theta)=\nabla f(\theta) Ei[fi(θ)]=i=1nn1fi(θ)=f(θ)

这说明 ∇ f i ( θ ) \nabla f_i(\theta) fi(θ) ∇ f ( θ ) \nabla f(\theta) f(θ) 的无偏估计,因此我们可以用 ∇ f i ( θ ) \nabla f_i(\theta) fi(θ) 代替 ∇ f ( θ ) \nabla f(\theta) f(θ) 进行梯度下降,这样一来,计算 ∇ f ( θ ) \nabla f(\theta) f(θ) 的时间复杂度就降低至 O ( 1 ) O(1) O(1) 了。这种方法又叫Stochastic Gradient Descent(SGD,随机梯度下降),即每次随机挑选一个样本进行更新。

只挑选一个样本或挑选所有样本未免有些太过极端了,有没有一种折中的办法呢?比如每次挑选 b   ( 1 ≤ b ≤ n ) b\,(1\leq b\leq n) b(1bn) 个样本来进行个更新,即用下式去代替 ∇ f ( θ ) \nabla f(\theta) f(θ)

1 b ∑ i ∈ B ∇ f i ( θ ) , B ⊂ { 1 , 2 , ⋯   , n } , ∣ B ∣ = b \frac1b \sum_{i\in \mathcal{B}} \nabla f_i(\theta),\quad \mathcal{B}\subset \{1,2,\cdots,n\},\quad|\mathcal{B}|=b b1iBfi(θ),B{1,2,,n},B=b

该式依然是 ∇ f ( θ ) \nabla f(\theta) f(θ) 的无偏估计,且由于 ∇ f i ( θ ) \nabla f_i(\theta) fi(θ) 是独立同分布的(具有相同的期望和方差),因此

Var i [ 1 b ∑ i ∈ B ∇ f i ( θ ) ] = 1 b 2 ∑ i ∈ B Var i [ ∇ f i ( θ ) ] = 1 b Var t [ ∇ f t ( θ ) ] \text{Var}_i\left[\frac1b \sum_{i\in \mathcal{B}} \nabla f_i(\theta)\right]=\frac{1}{b^2}\sum_{i\in \mathcal{B}}\text{Var}_i\left[ \nabla f_i(\theta)\right]=\frac1b\text{Var}_t\left[ \nabla f_t(\theta)\right] Vari[b1iBfi(θ)]=b21iBVari[fi(θ)]=b1Vart[ft(θ)]

📝 诸 ∇ f i ( θ ) \nabla f_i(\theta) fi(θ) 的方差均为 1 n ∑ i = 1 n ∥ ∇ f i ( θ ) ∥ 2 − ∥ ∇ f ( θ ) ∥ 2 \frac1n \sum_{i=1}^n \Vert \nabla f_i(\theta) \Vert^2-\Vert \nabla f(\theta)\Vert^2 n1i=1n∥∇fi(θ)2∥∇f(θ)2,证明如下:
Var i [ ∇ f i ( θ ) ] = E i [ ∥ ∇ f i ( θ ) − ∇ f ( θ ) ∥ 2 ] = E i [ ( ∇ f i ( θ ) − ∇ f ( θ ) ) T ( ∇ f i ( θ ) − ∇ f ( θ ) ) ] = E i [ ∥ ∇ f i ( θ ) ∥ 2 − 2 ∇ f i ( θ ) T ∇ f ( θ ) + ∥ ∇ f ( θ ) ∥ 2 ] = 1 n ∑ i = 1 n ∥ ∇ f i ( θ ) ∥ 2 − ∥ ∇ f ( θ ) ∥ 2 \begin{aligned} \text{Var}_i[\nabla f_i(\theta)]&=\mathbb{E}_i[\Vert \nabla f_i(\theta)-\nabla f(\theta)\Vert^2]=\mathbb{E}_i[(\nabla f_i(\theta)-\nabla f(\theta))^{\text{T}}(\nabla f_i(\theta)-\nabla f(\theta))] \\ &=\mathbb{E}_i[\Vert \nabla f_i(\theta) \Vert^2-2\nabla f_i(\theta)^{\text T}\nabla f(\theta)+\Vert \nabla f(\theta)\Vert^2] \\ &=\frac1n \sum_{i=1}^n \Vert \nabla f_i(\theta) \Vert^2-\Vert \nabla f(\theta)\Vert^2 \end{aligned} Vari[fi(θ)]=Ei[∥∇fi(θ)f(θ)2]=Ei[(fi(θ)f(θ))T(fi(θ)f(θ))]=Ei[∥∇fi(θ)22∇fi(θ)Tf(θ)+∥∇f(θ)2]=n1i=1n∥∇fi(θ)2∥∇f(θ)2

这说明选取的 b b b 越大,相应的方差就越小,我们对 ∇ f ( θ ) \nabla f(\theta) f(θ) 的估计就更稳定。

每次选取 b b b 个样本来进行更新的方法又叫Mini-Batch Gradient Descent(MBGD,小批量梯度下降) b b b 通常被称为batch size。

据此可以对三种方法进行总结:

  • BGD:批量梯度下降,每次采用所有样本来更新参数。
  • SGD:随机梯度下降,每次随机选择一个样本来更新参数。
  • MBGD:小批量梯度下降,每次随机选择 b b b 个样本来更新参数。

BGD、SGD可以看做是MBGD的特殊情形,一个是 b b b n n n,一个是 b b b 1 1 1


接下来对比BGD、SGD和MBGD的效果。由于BGD、SGD是MBGD的特殊情形,我们只需要实现MBGD即可。

假设训练集有 100 100 100 个样本,MBGD中的batch size设为 10 10 10,采用MSE作为损失函数,训练 5 5 5 个epoch。三种方法的更新步数如下:

  • SGD: 500 / 1 = 500 500/1=500 500/1=500 步。
  • BGD: 500 / 100 = 5 500/100=5 500/100=5 步。
  • MBGD: 500 / 10 = 50 500/10=50 500/10=50 步。

📝 在深度学习中,完整遍历一次训练集称为一个 epoch,而处理一个 batch 数据称为一个 step。设训练集包含 n n n 个样本,训练的 epoch 数为 e e ebatch size b b b,则训练过程中总的参数更新步数为: n e b \frac{ne}{b} bne

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

n_samples = 100  # 样本数量
n_features = 1   # 特征数量

# 真实参数
w_true = np.array([2])
b_true = 5

# 生成特征和标签,添加一些噪声
X = np.random.randn(n_samples, n_features)
noise = 0.5 * np.random.randn(n_samples, 1)
y = X @ w_true.reshape(-1, 1) + b_true + noise


def compute_loss(X, y, w, b):
    n = X.shape[0]
    y_pred = X @ w + b
    loss = (1 / (2 * n)) * np.sum((y_pred - y) ** 2)
    return loss


def compute_gradient(X, y, w, b):
    n = X.shape[0]
    y_pred = X @ w + b
    error = y_pred - y
    dw = (1 / n) * X.T @ error
    db = (1 / n) * np.sum(error)
    return dw, db


def mini_batch_gradient_descent(X, y, w_init, b_init, lr, epochs, batch_size):
    w = w_init.copy()
    b = b_init
    n = X.shape[0]
    w_history = [w.copy()]
    b_history = [b]
    for _ in range(epochs):
        indices = np.arange(n)
        np.random.shuffle(indices)
        X_shuffled = X[indices]
        y_shuffled = y[indices]

        for i in range(0, n, batch_size):
            Xi = X_shuffled[i:i+batch_size]
            yi = y_shuffled[i:i+batch_size]
            dw, db = compute_gradient(Xi, yi, w, b)
            w -= lr * dw
            b -= lr * db
            w_history.append(w.copy())
            b_history.append(b)
    return w, b, w_history, b_history


w_init = np.array([[0.0]])
b_init = 0.0
lr = 0.1
epochs = 5

batch_sizes = [1, 10, n_samples]
labels = ['SGD (b=1)', 'MBGD (b=10)', 'BGD (b=n)']
colors = ['r', 'g', 'b']

# 创建等高线图的数据
w_range = np.linspace(-1, 5, 50)
b_range = np.linspace(0, 10, 50)
W, B = np.meshgrid(w_range, b_range)
Loss = np.zeros_like(W)

# 计算每个网格点的损失值
for i in range(W.shape[0]):
    for j in range(W.shape[1]):
        w_ij = np.array([[W[i, j]]])
        b_ij = B[i, j]
        Loss[i, j] = compute_loss(X, y, w_ij, b_ij)


plt.figure(figsize=(8, 6))
CS = plt.contour(W, B, Loss, levels=30, cmap='viridis')
plt.clabel(CS, inline=1, fontsize=8)
plt.xlabel('w')
plt.ylabel('b')
plt.title('Parameter Descent Paths on Loss Contour')

for batch_size, label, color in zip(batch_sizes, labels, colors):
    w_final, b_final, w_history, b_history = mini_batch_gradient_descent(
        X, y, w_init, b_init, lr, epochs, batch_size)
    w_vals = np.array(w_history).flatten()
    b_vals = np.array(b_history)
    plt.plot(w_vals, b_vals, marker='o', color=color, label=label, markersize=3, linewidth=1)

plt.legend()
plt.show()

可以看出:

  • SGD方法下降的最快,但是下降的过程中波动较大,且无法及时收敛到最优解。
  • BGD方法下降的最慢,5个epoch后离最优解依然还有很远的一段距离,但是它的每一步都很稳定。
  • MBGD方法的速度介于SGD和BGD之间,且及时收敛到了最优解,并且更新过程也比较稳定。

3. momentum与dampening

在第一节的讨论中,我们提到了梯度下降容易陷入局部最优或鞍点。一个典型的例子是 y = x 3 y=x^3 y=x3,显然 x = 0 x=0 x=0 是它的鞍点。从 x = 3 x=3 x=3 处出发,使用 0.05 0.05 0.05 的学习率进行梯度下降:

import numpy as np
import matplotlib.pyplot as plt

def f(x):
    return x**3

def grad_f(x):
    return 3*x**2

def gradient_descent(start_x, learning_rate, steps):
    x_vals = [start_x]
    for _ in range(steps):
        grad = grad_f(x_vals[-1])
        new_x = x_vals[-1] - learning_rate * grad
        x_vals.append(new_x)
    return np.array(x_vals)

start_x = 3
learning_rate = 0.05
steps = 20
x_vals = gradient_descent(start_x, learning_rate, steps)

x_range = np.linspace(-5, 5, 400)
y_range = f(x_range)

plt.figure(figsize=(8, 6))
plt.plot(x_range, y_range, color='blue')
plt.plot(x_vals, f(x_vals), 'o-', color='orange', label='lr=0.05')
plt.title('Gradient Descent on $f(x) = x^3$')
plt.xlabel('x')
plt.ylabel('f(x)')
plt.legend()
plt.grid(True)
plt.show()

📝 在高维空间中,鞍点的存在往往更加普遍。

除非调大学习率,否则最后总会收敛到 x = 0 x=0 x=0 处。这一结果显然不是我们想要的。如果把梯度下降比作小球在曲线上的滚动,我们更希望小球能够越过 x = 0 x=0 x=0 这一点继续向下滚动,这也更加符合真实的物理场景。

我们已经知道梯度下降的公式为 θ ← θ − η g \theta\leftarrow\theta-\eta g θθηg,由于 η \eta η 通常是固定的,因此可以将其视为一小段时间 Δ t \Delta t Δt,这样一来, θ \theta θ 便可以视作位置, g g g 便可以视作速度,此时公式变为 x ← x − v Δ t x\leftarrow x-v\Delta t xxvΔt。在梯度下降的场景中, v v v 取决于 x x x,即 v = v ( x ) = 3 x 2 v=v(x)=3x^2 v=v(x)=3x2,当 x x x 减小时, v v v 也随之减小,说明小球在下落的过程中速度会越来越慢,这显然是不符合事实的。一个直观的想法是我们可以累积小球的历史速度,然后使用这个累积速度作为当前的速度进行下落:

m t = v 1 + v 2 + ⋯ + v t x t = x t − 1 − m t Δ t m_t=v_1+v_2+\cdots+v_t \\ x_{t}=x_{t-1}-m_t\Delta t mt=v1+v2++vtxt=xt1mtΔt

其中 m t m_t mt 就是 t t t 时刻的累积速度。

直接累加会有一个很明显的缺点,那就是 m t m_t mt 可以记住很久以前的速度。换句话说,历史任一时刻的速度对当前累积速度的贡献都是相同的,而我们更希望累积速度 m t m_t mt 具有遗忘性,即距离当前时刻越远的速度,相应的贡献就应当越少。

利用指数函数的衰减性,我们可以重新计算累积速度:

m t = β 0 v t + β 1 v t − 1 + β 2 v t − 2 + ⋯ + β t − 1 v 1 , β < 1 = v t + β ( v t − 1 + β v t − 2 + ⋯ + β t − 2 v 1 ) = β m t − 1 + v t \begin{aligned} m_t&=\beta^0v_t+\beta^1v_{t-1}+\beta^2v_{t-2}+\cdots+\beta^{t-1}v_1,\quad\beta<1 \\ &=v_t+\beta(v_{t-1}+\beta v_{t-2}+\cdots+\beta^{t-2}v_1) \\ &=\beta m_{t-1}+v_t \end{aligned} mt=β0vt+β1vt1+β2vt2++βt1v1,β<1=vt+β(vt1+βvt2++βt2v1)=βmt1+vt

于是我们得到了带动量的梯度下降公式

m t = β m t − 1 + g t θ t = θ t − 1 − η ⋅ m t m_t=\beta m_{t-1}+g_t \\ \theta_t=\theta_{t-1}-\eta\cdot m_t mt=βmt1+gtθt=θt1ηmt

其中 m t m_t mt 是动量, β \beta β 是动量系数。

设置 β = 0.9 \beta=0.9 β=0.9,更新步数为 5 5 5,我们来观察小球的运动轨迹:

def gradient_descent(start_x, learning_rate, steps):
    x_vals = [start_x]
    m = 0
    beta = 0.9
    for _ in range(steps):
        grad = grad_f(x_vals[-1])
        m = beta * m + grad
        new_x = x_vals[-1] - learning_rate * m
        x_vals.append(new_x)
    return np.array(x_vals)

可以发现此时小球成功越过了鞍点。

接下来以 y = x 2 y=x^2 y=x2 为例,比较带动量和不带动量的梯度下降法的效果:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, PillowWriter

def f(x):
    return x**2

def grad_f(x):
    return 2*x

# 带动量的梯度下降算法
def gradient_descent_with_momentum(start_x, learning_rate, steps):
    x_vals = [start_x]
    m = 0
    beta = 0.9
    for _ in range(steps):
        grad = grad_f(x_vals[-1])
        m = beta * m + grad
        new_x = x_vals[-1] - learning_rate * m
        x_vals.append(new_x)
    return np.array(x_vals)

# 不带动量的梯度下降算法
def gradient_descent_without_momentum(start_x, learning_rate, steps):
    x_vals = [start_x]
    for _ in range(steps):
        grad = grad_f(x_vals[-1])
        new_x = x_vals[-1] - learning_rate * grad
        x_vals.append(new_x)
    return np.array(x_vals)


start_x = 3
learning_rate = 0.05
steps = 50

x_vals_with_momentum = gradient_descent_with_momentum(start_x, learning_rate, steps)
x_vals_without_momentum = gradient_descent_without_momentum(start_x, learning_rate, steps)

x_range = np.linspace(-5, 5, 400)
y_range = f(x_range)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))

ax1.plot(x_range, y_range, color='blue')
point1, = ax1.plot([], [], 'o', color='orange', markersize=10, label='With Momentum')
ax1.set_title('Gradient Descent with Momentum on $f(x) = x^2$', fontsize=16)
ax1.set_xlabel('x', fontsize=14)
ax1.set_ylabel('f(x)', fontsize=14)
ax1.legend(fontsize=12)
ax1.grid(True)

ax2.plot(x_range, y_range, color='blue')
point2, = ax2.plot([], [], 'o', color='green', markersize=10, label='Without Momentum')
ax2.set_title('Gradient Descent without Momentum on $f(x) = x^2$', fontsize=16)
ax2.set_xlabel('x', fontsize=14)
ax2.set_ylabel('f(x)', fontsize=14)
ax2.legend(fontsize=12)
ax2.grid(True)


def init():
    point1.set_data([], [])
    point2.set_data([], [])
    return point1, point2


def update(frame):
    point1.set_data(x_vals_with_momentum[frame], f(x_vals_with_momentum[frame]))
    point2.set_data(x_vals_without_momentum[frame], f(x_vals_without_momentum[frame]))
    return point1, point2

ani = FuncAnimation(fig, update, frames=len(x_vals_with_momentum), init_func=init, blit=True, interval=100)
ani.save('gradient_descent_comparison.gif', writer=PillowWriter(fps=10))

plt.show()

显然左图更贴近现实世界的情况,但其收敛速度不如右图快。原因是动量法会累积历史梯度,从而导致过冲和振荡。既然我们可以控制历史累积梯度的贡献程度(即 β \beta β),我们同样希望能控制当前梯度的贡献程度,由此可以引入阻尼系数 τ \tau τ(它相当于摩擦力),从而动量的更新公式变为:

m t = β m t − 1 + ( 1 − τ ) g t m_t=\beta m_{t-1}+(1-\tau)g_t mt=βmt1+(1τ)gt

有些教程会将 τ \tau τ 固定为 β \beta β,使得 g t g_t gt 的贡献程度恒为 1 − β 1 - \beta 1β,这在一定程度上限制了灵活性。

为了充分验证动量系数和阻尼系数对优化过程的影响,我们选取函数 f ( x ) = ( x + 2 ) 2 ( x − 1 ) 2 + 1.5 x 2 f(x) = (x+2)^2(x-1)^2 + 1.5x^2 f(x)=(x+2)2(x1)2+1.5x2 进行实验。该函数具有两个极小值点,其中一个为局部极小值,另一个为全局极小值。实验从初始点 x = − 2.5 x = -2.5 x=2.5 开始,学习率设定为 0.01 0.01 0.01,并通过以下六组设置进行比较:

  1. 不使用动量和阻尼。
  2. 动量系数设置为 0.9 0.9 0.9,阻尼系数为 0 0 0
  3. 动量系数设置为 0.9 0.9 0.9,阻尼系数为 0.5 0.5 0.5
  4. 动量系数设置为 0.9 0.9 0.9,阻尼系数为 0.9 0.9 0.9
  5. 动量系数设置为 0.99 0.99 0.99,阻尼系数为 0 0 0
  6. 动量系数设置为 0.999 0.999 0.999,阻尼系数为 0 0 0
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, PillowWriter

def f(x):
    return (x + 2)**2 * (x - 1)**2 + 1.5 * x**2

def grad_f(x):
    return 2 * (x + 2) * (x - 1)**2 + 2 * (x - 1) * (x + 2)**2 + 3 * x

def gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0.9, tau=0.0):
    x_vals = [start_x]
    m = 0
    for _ in range(steps):
        grad = grad_f(x_vals[-1])
        m = beta * m + (1 - tau) * grad
        new_x = x_vals[-1] - learning_rate * m
        x_vals.append(new_x)
    return np.array(x_vals)

start_x = -2.5
learning_rate = 0.01
steps = 100

x_vals_no_momentum = gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0.0, tau=0.0)
x_vals_momentum_no_damping = gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0.9, tau=0.0)
x_vals_momentum_medium_damping = gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0.9, tau=0.5)
x_vals_momentum_high_damping = gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0.9, tau=0.9)
x_vals_momentum_0_99 = gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0.99, tau=0.0)
x_vals_momentum_0_999 = gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0.999, tau=0.0)

x_range = np.linspace(-3, 2, 400)
y_range = f(x_range)

fig, axes = plt.subplots(2, 3, figsize=(10, 6))

titles = [
    'No Momentum, No Damping', 
    'Momentum=0.9, No Damping',
    'Momentum=0.9, Damping=0.5',
    'Momentum=0.9, Damping=0.9',
    'Momentum=0.99, No Damping',
    'Momentum=0.999, No Damping'
]
colors = ['orange', 'green', 'blue', 'red', 'purple', 'brown']
x_vals_list = [
    x_vals_no_momentum,
    x_vals_momentum_no_damping,
    x_vals_momentum_medium_damping,
    x_vals_momentum_high_damping,
    x_vals_momentum_0_99,
    x_vals_momentum_0_999
]

plots = []
for i, ax in enumerate(axes.flat):
    ax.plot(x_range, y_range, color='blue')
    point, = ax.plot([], [], 'o', color=colors[i], markersize=8)
    ax.set_title(titles[i])
    ax.set_xlim([-3, 2])
    ax.set_ylim([0, 30])
    ax.set_xlabel('x')
    ax.set_ylabel('f(x)')
    ax.grid(True)
    plots.append(point)

def init():
    for plot in plots:
        plot.set_data([], [])
    return plots

def update(frame):
    for i in range(6):
        plots[i].set_data(x_vals_list[i][frame], f(x_vals_list[i][frame]))
    return plots

ani = FuncAnimation(fig, update, frames=len(x_vals_no_momentum), init_func=init, blit=True, interval=100)
ani.save('momentum_damping_experiments.gif', writer=PillowWriter(fps=10))

plt.show()

从图中可以得出以下结论:

  • 在未使用动量法(即无动量和阻尼)的情况下,或当阻尼系数较大时,优化过程更容易陷入局部极小值。
  • 动量系数过大时,对历史梯度的记忆会更加明显,可能导致优化过程中的振荡现象。

一般而言,建议设置 β = 0.9 , τ = 0 \beta=0.9,\tau=0 β=0.9,τ=0,但更多时候还是要视具体情况而定。

⚠️ 学术界关于阻尼系数的研究目前还不是很充分,大多数情况下建议直接设置为 0 0 0,即不考虑阻尼。

回顾第一节中的二元梯度下降,当学习率设置为 0.15 0.15 0.15 时,优化过程呈振荡式下降。设置合适的动量系数可以有效缓解振荡,并加速下降。

import numpy as np
import matplotlib.pyplot as plt

def f(x1, x2):
    return x1**2 + 6*x2**2

def grad_f(x1, x2):
    return np.array([2*x1, 12*x2])

def gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0):
    x_vals = [start_x]
    m = 0
    for _ in range(steps):
        grad = grad_f(*x_vals[-1])
        m = beta * m + grad
        new_x = x_vals[-1] - m * learning_rate
        x_vals.append(new_x)
    return np.array(x_vals)

start_x = np.array([-10, 3])
steps = 100
learning_rate = 0.15
beta_momentum = 0.3
x1_range = np.linspace(-12, 2, 200)
x2_range = np.linspace(-6, 6, 200)
x1_grid, x2_grid = np.meshgrid(x1_range, x2_range)
f_vals = f(x1_grid, x2_grid)

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

x_vals = gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0)
ax = axes[0]
ax.contour(x1_grid, x2_grid, f_vals, levels=15, cmap='Blues')
ax.plot(x_vals[:, 0], x_vals[:, 1], 'o-', color='orange')
ax.set_title(f'Gradient Descent (LR: {learning_rate})')
ax.set_xlabel(r'$x_1$')
ax.set_ylabel(r'$x_2$')
ax.set_xlim([-12, 2])
ax.set_ylim([-6, 6])

x_vals_momentum = gradient_descent_with_momentum(start_x, learning_rate, steps, beta=beta_momentum)
ax = axes[1]
ax.contour(x1_grid, x2_grid, f_vals, levels=15, cmap='Blues')
ax.plot(x_vals_momentum[:, 0], x_vals_momentum[:, 1], 'o-', color='orange')
ax.set_title(f'Momentum (LR: {learning_rate}, Beta: {beta_momentum})')
ax.set_xlabel(r'$x_1$')
ax.set_ylabel(r'$x_2$')
ax.set_xlim([-12, 2])
ax.set_ylim([-6, 6])

plt.tight_layout()
plt.show()

在不使用动量的情况下,每次参数更新的梯度可以分解为沿 x 1 x_1 x1 x 2 x_2 x2 方向的分量。其中,沿 x 1 x_1 x1 方向的梯度始终为正,因此参数在该方向上会持续减小;而沿 x 2 x_2 x2 方向的梯度则时而为正、时而为负,导致参数在该方向上的更新呈现出上下振荡的趋势。

动量法通过累积梯度,有效缓解了这种振荡现象。由于沿 x 2 x_2 x2 方向的梯度正负交替变化,动量累积过程中这些相反的梯度部分抵消,从而减小了沿 x 2 x_2 x2 方向的振荡幅度。与此同时,沿 x 1 x_1 x1 方向的梯度始终为正,因此动量会不断累积,导致参数沿 x 1 x_1 x1 方向下降得越来越快。

当然,在此场景中,若设置 β = 0.9 \beta=0.9 β=0.9,参数更新就会出现过冲现象。

3.1 另一种形式的momentum

3.1.1 学习率固定

我们之前提到的带动量的更新公式为:

m t = β m t − 1 + g t θ t = θ t − 1 − η ⋅ m t m_t=\beta m_{t-1}+g_t \\ \theta_t=\theta_{t-1}-\eta\cdot m_t mt=βmt1+gtθt=θt1ηmt

在不少教程中,还存在另外一种形式:

m t = β m t − 1 − η ⋅ g t θ t = θ t − 1 + m t m_t=\beta m_{t-1}-\eta \cdot g_t \\ \theta_t=\theta_{t-1}+m_t mt=βmt1ηgtθt=θt1+mt

先来推导第一个形式。不难得出

m t = ∑ i = 1 t β t − i g i , θ t = θ 0 − η ∑ i = 1 t m i m_t=\sum_{i=1}^t \beta^{t-i} g_i,\quad \theta_t=\theta_0-\eta\sum_{i=1}^t m_i mt=i=1tβtigi,θt=θ0ηi=1tmi

合并两式即可得到

θ t = θ 0 − η ∑ i = 1 t ∑ j = 1 i β i − j g j \theta_t=\theta_0-\eta\sum_{i=1}^t\sum_{j=1}^i\beta^{i-j}g_j θt=θ0ηi=1tj=1iβijgj

再来推导第二个形式。不难得出

m t = − η ∑ i = 1 t β t − i g i , θ t = θ 0 + ∑ i = 1 t m i m_t=-\eta\sum_{i=1}^t \beta^{t-i} g_i,\quad \theta_t=\theta_0+\sum_{i=1}^t m_i mt=ηi=1tβtigi,θt=θ0+i=1tmi

合并两式即可得到

θ t = θ 0 − η ∑ i = 1 t ∑ j = 1 i β i − j g j \theta_t=\theta_0-\eta\sum_{i=1}^t\sum_{j=1}^i\beta^{i-j}g_j θt=θ0ηi=1tj=1iβijgj

由此可知,学习率固定的情况下,这两种形式是完全等价的

3.1.2 学习率不固定

此时每一步的学习率都不一样,第一种形式的更新公式为:

m t = β m t − 1 + g t θ t = θ t − 1 − η t ⋅ m t m_t=\beta m_{t-1}+g_t \\ \theta_t=\theta_{t-1}-\eta_t\cdot m_t mt=βmt1+gtθt=θt1ηtmt

不难得出

m t = ∑ i = 1 t β t − i g i , θ t = θ 0 − ∑ i = 1 t η i m i m_t=\sum_{i=1}^t \beta^{t-i} g_i,\quad \theta_t=\theta_0-\sum_{i=1}^t \eta_im_i mt=i=1tβtigi,θt=θ0i=1tηimi

合并两式即可得到

θ t = θ 0 − ∑ i = 1 t η i ∑ j = 1 i β i − j g j \theta_t=\theta_0-\sum_{i=1}^t\eta_i\sum_{j=1}^i\beta^{i-j}g_j θt=θ0i=1tηij=1iβijgj

第二种形式的更新公式为:

m t = β m t − 1 − η t ⋅ g t θ t = θ t − 1 + m t m_t=\beta m_{t-1}-\eta_t \cdot g_t \\ \theta_t=\theta_{t-1}+m_t mt=βmt1ηtgtθt=θt1+mt

不难得出

m t = − ∑ i = 1 t β t − i η i g i , θ t = θ 0 + ∑ i = 1 t m i m_t=-\sum_{i=1}^t \beta^{t-i} \eta_ig_i,\quad \theta_t=\theta_0+\sum_{i=1}^t m_i mt=i=1tβtiηigi,θt=θ0+i=1tmi

合并两式即可得到

θ t = θ 0 − ∑ i = 1 t ∑ j = 1 i β i − j η j g j \theta_t=\theta_0-\sum_{i=1}^t\sum_{j=1}^i\beta^{i-j}\eta_jg_j θt=θ0i=1tj=1iβijηjgj

可以看出学习率不固定的情况下,两者并不等价

4. nesterov

Nesterov Accelerated Gradient (NAG) 是一种加速梯度下降的优化算法,最早由苏联数学家 Yurii Nesterov 提出。它是对标准的带动量梯度下降方法的改进,能够在收敛性和效率上表现得更好。与标准动量方法不同,NAG 在计算梯度时并不是根据当前的参数位置,而是根据向前预测的参数位置。这个改进让NAG可以在每一步中更具前瞻性,避免在接近局部最优点或鞍点时的惯性过冲现象。

NAG的主要思想是:利用当前的动量预测出未来参数的位置,然后在该预测位置计算梯度,再利用这个梯度来调整动量和更新参数。

这样解释或许有些抽象,为方便接下来的讲解,我们假定 β = η = 1 \beta=\eta=1 β=η=1,从而更新公式变为:

m t = m t − 1 + g t θ t = θ t − 1 − m t m_t=m_{t-1}+g_t\\ \theta_t=\theta_{t-1}-m_t mt=mt1+gtθt=θt1mt

在高维空间中,上述公式其实就是向量的加减法。下图展示了从 θ t − 1 \theta_{t-1} θt1 θ t \theta_t θt 的更新过程:

⚠️ 图中和 g , m g,m g,m 相关的均在前面加上了负号,这是因为它们原本表示的是上升的方向,加上负号才能变成下降的方向。

模型初始参数为 θ 0 \theta_0 θ0,动量初始为 m 0 = 0 m_0=0 m0=0(因为还没开始累积)。

在第 1 1 1 时刻,我们会在 θ 0 \theta_0 θ0 处计算梯度,得到 g 1 = ∇ f ( θ 0 ) g_1=\nabla f(\theta_0) g1=f(θ0),然后我们会用当前时刻的梯度 g 1 g_1 g1 和上一时刻的动量 m 0 m_0 m0(历史累积梯度)合成当前时刻的动量: m 1 = m 0 + g 1 m_1=m_0+g_1 m1=m0+g1。当前时刻的动量代表了 θ \theta θ 将要更新的方向。

以此类推,在第 t t t 时刻,我们会在 θ t − 1 \theta_{t-1} θt1 处计算梯度,得到 g t = ∇ f ( θ t − 1 ) g_t=\nabla f(\theta_{t-1}) gt=f(θt1)。然后用当前时刻的梯度 g t g_t gt 和上一时刻的动量 m t − 1 m_{t-1} mt1(历史累积梯度)合成当前时刻的动量 m t m_t mt

仔细观察, m t m_t mt 决定了第 t t t 时刻的更新方向,即当前时刻的动量决定了当前时刻的参数更新,并且参数更新均是使用动量来完成的。既然 m t m_t mt 的计算依赖于 m t − 1 m_{t-1} mt1 g t g_t gt,那在计算之前,我们能不能用上一时刻的动量来提前预判当前时刻的参数更新呢?即用 m t − 1 m_{t-1} mt1 去预判 θ t \theta_t θt 的位置。

我们在第 t t t 时刻不去计算 m t m_t mt,而是先用 m t − 1 m_{t-1} mt1 去预判 θ t \theta_t θt 的位置,假设预判的结果为 θ ~ t \tilde{\theta}_t θ~t,那么有 θ ~ t = θ t − 1 − m t − 1 \tilde{\theta}_t=\theta_{t-1}-m_{t-1} θ~t=θt1mt1,然后我们在预判的位置上重新计算梯度 g ~ t = ∇ f ( θ ~ t ) \tilde{g}_t=\nabla f(\tilde{\theta}_t) g~t=f(θ~t),于是可以得到第 t t t 时刻的更新结果:

θ t = θ ~ t − g ~ t = θ t − 1 − m t − 1 − ∇ f ( θ t − 1 − m t − 1 ) = θ t − 1 − ( m t − 1 + ∇ f ( θ t − 1 − m t − 1 ) ) \theta_t=\tilde{\theta}_t-\tilde{g}_t=\theta_{t-1}-m_{t-1}-\nabla f(\theta_{t-1}-m_{t-1})=\theta_{t-1}-(m_{t-1}+\nabla f(\theta_{t-1}-m_{t-1})) θt=θ~tg~t=θt1mt1f(θt1mt1)=θt1(mt1+f(θt1mt1))

完整的更新公式如下:

m t = m t − 1 + ∇ f ( θ t − 1 − m t − 1 ) θ t = θ t − 1 − m t m_t=m_{t-1}+\nabla f(\theta_{t-1}-m_{t-1}) \\ \theta_t=\theta_{t-1}-m_t mt=mt1+f(θt1mt1)θt=θt1mt

Nesterov相比原始的动量法,无非是计算梯度的位置发生了变化,原先是在 θ t − 1 \theta_{t-1} θt1 处计算梯度,而现在是在 θ ~ t = θ t − 1 − m t − 1 \tilde{\theta}_t=\theta_{t-1}-m_{t-1} θ~t=θt1mt1 处计算梯度,更具有前瞻性。

Nesterov原论文中的更新公式如下:

m t = β m t − 1 − η ∇ f ( θ t − 1 + β m t − 1 ) θ t = θ t − 1 + m t m_{t}=\beta m_{t-1}-\eta \nabla f(\theta_{t-1}+\beta m_{t-1}) \\ \theta_t=\theta_{t-1}+m_t mt=βmt1ηf(θt1+βmt1)θt=θt1+mt

这对应了我们之前所说的第二种形式的momentum。那第一种形式对应的Nesterov公式又是什么样的呢?首先可以知道

θ t = θ t − 1 − η m t = θ t − 1 − η ( β m t − 1 + g t ) = ( θ t − 1 − η β m t − 1 ) − η g t − 1 \begin{aligned} \theta_t&=\theta_{t-1}-\eta m_t \\ &=\theta_{t-1}-\eta(\beta m_{t-1}+g_t) \\ &=(\theta_{t-1}-\eta \beta m_{t-1})-\eta g_{t-1} \end{aligned} θt=θt1ηmt=θt1η(βmt1+gt)=(θt1ηβmt1)ηgt1

于是可以推测 g t − 1 = ∇ f ( θ t − 1 − η β m t − 1 ) g_{t-1}=\nabla f(\theta_{t-1}-\eta \beta m_{t-1}) gt1=f(θt1ηβmt1),从而该形式下的Nesterov公式为:

m t = β m t − 1 + ∇ f ( θ t − 1 − η β m t − 1 ) θ t = θ t − 1 − η m t ( ∗ ) m_{t}=\beta m_{t-1}+\nabla f(\theta_{t-1}-\eta \beta m_{t-1}) \\ \theta_t =\theta_{t-1}-\eta m_t \tag{$*$} mt=βmt1+f(θt1ηβmt1)θt=θt1ηmt()

4.1 PyTorch中的Nesterov

PyTorch中的Nesterov公式为

m t = β m t − 1 + ∇ f ( θ t − 1 ) θ t = θ t − 1 − η ( β m t + ∇ f ( θ t − 1 ) ) m_t=\beta m_{t-1}+ \nabla f(\theta_{t-1}) \\ \theta_{t}=\theta_{t-1}-\eta(\beta m_t+\nabla f(\theta_{t-1})) mt=βmt1+f(θt1)θt=θt1η(βmt+f(θt1))

可以发现它并没有使用偏移后的参数进行梯度计算,这是为什么呢?

( ∗ ) (*) () 式中令 θ ~ t − 1 = θ t − 1 − η β m t − 1 \tilde{\theta}_{t-1}=\theta_{t-1}-\eta \beta m_{t-1} θ~t1=θt1ηβmt1,然后利用 ( ∗ ) (*) () 式中的结果去推导 θ ~ t \tilde{\theta}_{t} θ~t

θ ~ t = θ t − η β m t = θ t − 1 − η m t − η β m t = θ t − 1 − η ( β m t − 1 + ∇ f ( θ ~ t − 1 ) ) − η β m t = ( θ t − 1 − η β m t − 1 ) − η ∇ f ( θ ~ t − 1 ) − η β m t = θ ~ t − 1 − η ( β m t + ∇ f ( θ ~ t − 1 ) ) \begin{aligned} \tilde{\theta}_{t}&=\theta_{t}-\eta\beta m_{t} \\ &=\theta_{t-1}-\eta m_t-\eta \beta m_t \\ &=\theta_{t-1}-\eta(\beta m_{t-1}+\nabla f(\tilde{\theta}_{t-1}))-\eta\beta m_t \\ &=(\theta_{t-1}-\eta\beta m_{t-1})-\eta \nabla f(\tilde{\theta}_{t-1})-\eta\beta m_t \\ &=\tilde{\theta}_{t-1}-\eta(\beta m_t+\nabla f(\tilde{\theta}_{t-1})) \end{aligned} θ~t=θtηβmt=θt1ηmtηβmt=θt1η(βmt1+f(θ~t1))ηβmt=(θt1ηβmt1)ηf(θ~t1)ηβmt=θ~t1η(βmt+f(θ~t1))

由此可以得知,PyTorch在实际进行计算的时候,是把参数 θ t \theta_t θt 当作是已经偏移过后的参数 θ ~ t \tilde{\theta}_t θ~t,所以其实一直更新的是 θ ~ t \tilde{\theta}_t θ~t。按理来说,在更新结束后,我们应当修正回原来的参数:

θ t = θ ~ t + η β m t \theta_t=\tilde{\theta}_t+\eta\beta m_t θt=θ~t+ηβmt

但PyTorch的源码中并未做这一修正,所以PyTorch关于Nesterov的实现实际上是一种近似。

4.2 Polyak与Nesterov的比较

📝 Polyak动量法就是传统的动量法,也叫Classical Momentum(CM)。

我们使用PyTorch中的更新方法,初始时先进行偏移,即 θ ~ 0 = θ 0 − η β m 0 = θ 0 \tilde{\theta}_0=\theta_0-\eta\beta m_0=\theta_0 θ~0=θ0ηβm0=θ0,之后的每次更新按照如下流程计算:

m t = β m t − 1 + ∇ f ( θ ~ t − 1 ) ∇ f ( θ ~ t − 1 ) = ∇ f ( θ ~ t − 1 ) + β m t θ t = θ t − 1 − η ∇ f ( θ ~ t − 1 ) m_t=\beta m_{t-1}+ \nabla f(\tilde{\theta}_{t-1}) \\ \nabla f(\tilde{\theta}_{t-1}) = \nabla f(\tilde{\theta}_{t-1}) +\beta m_t\\ \theta_{t}=\theta_{t-1}-\eta \nabla f(\tilde{\theta}_{t-1}) mt=βmt1+f(θ~t1)f(θ~t1)=f(θ~t1)+βmtθt=θt1ηf(θ~t1)

计算完成后修正参数即可。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, PillowWriter


def f(x):
    return (x + 2)**2 * (x - 1)**2 + 1.5 * x**2

def grad_f(x):
    return 2 * (x + 2) * (x - 1)**2 + 2 * (x - 1) * (x + 2)**2 + 3 * x


def gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0.9, nesterov=False):
    x_vals = [start_x]
    m = 0
    for _ in range(steps):
        grad = grad_f(x_vals[-1])
        m = beta * m + grad
        if nesterov:
            grad += beta * m
        else:
            grad = m
        new_x = x_vals[-1] - learning_rate * grad
        x_vals.append(new_x)
    if nesterov:
        x_vals[-1] += beta * learning_rate * m
    return np.array(x_vals)


start_x = -3
learning_rate = 0.01
steps = 100


x_vals_momentum = gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0.9, nesterov=False)
x_vals_nesterov = gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0.9, nesterov=True)


x_range = np.linspace(-3.5, 2, 400)
y_range = f(x_range)

fig, axes = plt.subplots(1, 2, figsize=(12, 6))

titles = ['Momentum=0.9, No Nesterov', 'Momentum=0.9, Nesterov']
x_vals_list = [x_vals_momentum, x_vals_nesterov]
colors = ['green', 'red']

plots = []
for i, ax in enumerate(axes):
    ax.plot(x_range, y_range, color='blue')
    point, = ax.plot([], [], 'o', color=colors[i], markersize=8)
    ax.set_title(titles[i])
    ax.set_xlim([-3, 2])
    ax.set_ylim([0, 30])
    ax.set_xlabel('x')
    ax.set_ylabel('f(x)')
    ax.grid(True)
    plots.append(point)

def init():
    for plot in plots:
        plot.set_data([], [])
    return plots

def update(frame):
    for i in range(2):
        plots[i].set_data(x_vals_list[i][frame], f(x_vals_list[i][frame]))
    return plots

ani = FuncAnimation(fig, update, frames=len(x_vals_momentum), init_func=init, blit=True, interval=100)
ani.save('momentum_vs_nesterov.gif', writer=PillowWriter(fps=10))

plt.show()

可以看到Nesterov明显收敛的更快。


Ref

[1] https://pytorch.org/docs/stable/generated/torch.optim.SGD.html
[2] https://d2l.ai/chapter_optimization/momentum.html
[3] https://www.bilibili.com/video/BV1jh4y1q7ua
[4] https://www.bilibili.com/video/BV1YF411n7Dr
[5] https://optimi.benjaminwarner.dev/optimizers/sgd/
[6] https://towardsdatascience.com/is-pytorchs-nesterov-momentum-implementation-wrong-5dc5f5032008
[7] https://www.bilibili.com/video/BV1kL411E7P5

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2162276.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

常见统计量与其抽样分布

什么是统计量 我们首先给出统计量的定义:设 X 1 , X 2 , ⋯ , X n X_1,X_2,\cdots,X_n X1​,X2​,⋯,Xn​ 为来自于总体X的一个样本&#xff0c; g ( X 1 , X 2 , ⋯ , X n ) g(X_1,X_2,\cdots,X_n) g(X1​,X2​,⋯,Xn​) 为关于 X 1 , X 2 , ⋯ , X n X_1,X_2,\cdots,X_n X…

React开发环境搭建以及常见错误解决

‌React开发环境搭建主要包括Node.js安装、编辑器选择、创建React项目等步骤‌。 Node.js安装‌ 从Node.js官网下载并安装最新版本的Node.js&#xff0c;安装过程中npm会自动安装。安装完成后&#xff0c;通过命令行输入node -v和npm -v检查安装是否成功。 carawang%node -v…

JS 特殊运算符有哪些?

JavaScript 特殊运算符有哪些&#xff1f; 众多编程语言之中JavaScript &#xff0c;以其强大而全面的功能深受前端开发者喜爱。其丰富的运算符集&#xff0c;不仅包括了广泛应用的算术运算符、比较运算符以及逻辑运算符&#xff0c;还蕴藏着一系列较为冷门但同样功能强大的运算…

【SSM-Day2】创建SpringBoot项目

运行本篇中的代码&#xff1a;idea专业版或者idea社区版本&#xff08;2021.1~2022.1.4&#xff09;->这个版本主要是匹配插件spring boot Helper的免费版(衰) 【SSM-Day2】创建SpringBoot项目 框架->Spring家族框架快速上手Spring Boot&#x1f4e2;用idea插件创建Sprin…

python文件读写知识简记

简单记录一下python文件读写相关知识 一、打开文件 python使用open函数打开文件&#xff0c;函数原型如下 open(file, moder, buffering-1, encodingNone, errorsNone, newline None, closefdTrue, openerNone) file 文件地址 mode 文件打开模式&#xff0c;可设定为如下的…

harmonyOS ArkTS最新跳转Navigation

文章目录 取消标题栏初始页面(load)设置为竖屏 自定义标题Tabs&TabContentTabs通过divider实现了分割线各种属性 图片下载 官方文档 Entry Component struct Index {State message: string Hello WorldState djs:number 5build() {Column(){Navigation(){}.title("g…

一文详解GB28181、RTSP、RTMP

GB28181 GB28181 即 GB/T28181—2016《公共安全视频监控联网系统信息传输、交换、控制技术要求》。它是公安部提出的公共安全行业标准&#xff0c;在视频监控领域具有重要地位。 主要目的和应用场景&#xff1a; 目的&#xff1a;解决不同厂家的视频监控设备执行各自标准&…

9.24 C++ 常成员,运算符重载

//my_string.cpp #include "my_string.h" #include <iostream> #include <cstring>using namespace std;My_string::My_string():size(15){this->ptr new char[size];this->ptr[0] \0; //表示串为空串this->len 0;}//有参构造My_…

业务数据批量插入数据库实践

业务数据如何存储一直以来都是项目开发中的一个比较重要的话题。我们要从资源的利用率&#xff0c;业务场景和技术实现多个方面考虑存储的问题。“抛开业务谈技术就是耍流氓”&#xff0c;所有技术架构都要站在实际的业务场景中分析。比如个人端的产品&#xff0c;这种就属于读…

代码随想录算法训练营Day7 | 454.四数相加Ⅱ、383.赎金信、15.三数之和、18.四数之和

454.四数相加Ⅱ 题目 454. 四数相加 II - 力扣&#xff08;LeetCode&#xff09; 给你四个整数数组 nums1、nums2、nums3 和 nums4 &#xff0c;数组长度都是 n &#xff0c;请你计算有多少个元组 (i, j, k, l) 能满足&#xff1a; 0 < i, j, k, l < nnums1[i] nums…

weblogic中间件漏洞复现

后台弱口令getshell 1.开启环境 cd vulhub-master/weblogic/weak_password docker-compose up -d docker ps 2.f访问靶场 访问/console/login/LoginForm.jsp这个目录进行登录&#xff0c; 默认账号密码&#xff1a;weblogic/Oracle123 需要注意的是单个账号进行登录时&…

C++_CH18_构造函数与析构函数

C_CH18_构造函数与析构函数 1 类的默认成员函数 在编写类的时候&#xff0c;C编译器会默认生成6个默认的函数&#xff0c;但是不显示出来&#xff1a; 需要关注以下两个方面: 第一:我们不写时&#xff0c;编译器默认生成的函数行为是什么&#xff0c;是否满足我们的需求。 …

LabVIEW界面输入值设为默认值

在LabVIEW中&#xff0c;将前面板上所有控件的当前输入值设为默认值&#xff0c;可以通过以下步骤实现&#xff1a; 使用控件属性节点&#xff1a;你可以创建一个属性节点来获取所有控件的引用。 右键点击控件&#xff0c;选择“创建” > “属性节点”。 设置属性节点为“D…

实践出真知!8个案例速通栅格系统

在现代设计中&#xff0c;栅格系统作为一种重要的布局方案&#xff0c;能够有效提升设计的秩序感。对于 UI 设计领域&#xff0c;栅格系统也广泛用于跨屏幕的响应式设计&#xff0c;帮助设计师打造更好的多端体验。本文将简要介绍栅格系统的基本概念和搭建方法&#xff0c;并提…

什么是unix中的fork函数?

一、前言 在本专栏之前的文档中已经介绍过unix进程环境相关的概念了&#xff0c;本文将开始介绍unix中一个进程如何创建出新进程&#xff0c;主要是通过fork函数来实现此功能。本文将包含如下内容&#xff1a; 1.fork函数简介 2.父进程与子进程的特征 3.如何使用fork创建新进程…

依赖不对应导致java文件不能正常显示

项目中若出现非正常显示的java文件&#xff0c;检查下是否依赖版本不对应。&#xff08;前提必须是maven项目&#xff09;

网络原理(4)——网络层(IP)、数据链路层

1. IP 协议 基本概念&#xff1a; 主机&#xff1a;配有 IP 地址&#xff0c;但是不进行路由控制的设备 路由器&#xff1a;即配有 IP 地址&#xff0c;又能进行路由控制 节点&#xff1a;主机和路由器的统称 IP 协议报头格式 1) 4 位版本&#xff1a;实际上只有两个取值&…

通义灵码AI 程序员正式发布:写代码谁还动手啊

虽然见不到面 但你已深潜我心 前几天&#xff0c;在 2024 年的杭州云栖大会上&#xff0c;随着通义大模型能力的全面提升&#xff0c;阿里云通义灵码这位中国的首位 AI 程序员也迎来重大的升级。 一年前这位 AI 程序员还只能完成基础的编程任务&#xff0c;到现在可以做到几…

Leetcode 543. 124. 二叉树的直径 树形dp C++实现

问题&#xff1a;Leetcode 543. 二叉树的直径&#xff08;边权型&#xff09; 给你一棵二叉树的根节点&#xff0c;返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。两节点之间路径的 长度 由它们之…

探索未来:MultiOn,AI的下一个革命

文章目录 探索未来&#xff1a;MultiOn&#xff0c;AI的下一个革命背景&#xff1a;为什么选择MultiOn&#xff1f;MultiOn是什么&#xff1f;如何安装MultiOn&#xff1f;简单的库函数使用方法场景应用常见问题及解决方案总结 探索未来&#xff1a;MultiOn&#xff0c;AI的下一…