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

目录

  • 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 pltdef f(x):return x**2def grad_f(x):return 2*xdef 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 * gradx_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 pltdef f(x1, x2):return x1**2 + 6*x2**2def 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 * gradx_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 pltnp.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 + noisedef compute_loss(X, y, w, b):n = X.shape[0]y_pred = X @ w + bloss = (1 / (2 * n)) * np.sum((y_pred - y) ** 2)return lossdef compute_gradient(X, y, w, b):n = X.shape[0]y_pred = X @ w + berror = y_pred - ydw = (1 / n) * X.T @ errordb = (1 / n) * np.sum(error)return dw, dbdef mini_batch_gradient_descent(X, y, w_init, b_init, lr, epochs, batch_size):w = w_init.copy()b = b_initn = 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 * dwb -= lr * dbw_history.append(w.copy())b_history.append(b)return w, b, w_history, b_historyw_init = np.array([[0.0]])
b_init = 0.0
lr = 0.1
epochs = 5batch_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 pltdef f(x):return x**3def grad_f(x):return 3*x**2def 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 * gradx_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 = 0beta = 0.9for _ in range(steps):grad = grad_f(x_vals[-1])m = beta * m + gradnew_x = x_vals[-1] - learning_rate * mx_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, PillowWriterdef f(x):return x**2def grad_f(x):return 2*x# 带动量的梯度下降算法
def gradient_descent_with_momentum(start_x, learning_rate, steps):x_vals = [start_x]m = 0beta = 0.9for _ in range(steps):grad = grad_f(x_vals[-1])m = beta * m + gradnew_x = x_vals[-1] - learning_rate * mx_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 * gradx_vals.append(new_x)return np.array(x_vals)start_x = 3
learning_rate = 0.05
steps = 50x_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, point2def 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, point2ani = 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, PillowWriterdef f(x):return (x + 2)**2 * (x - 1)**2 + 1.5 * x**2def grad_f(x):return 2 * (x + 2) * (x - 1)**2 + 2 * (x - 1) * (x + 2)**2 + 3 * xdef gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0.9, tau=0.0):x_vals = [start_x]m = 0for _ in range(steps):grad = grad_f(x_vals[-1])m = beta * m + (1 - tau) * gradnew_x = x_vals[-1] - learning_rate * mx_vals.append(new_x)return np.array(x_vals)start_x = -2.5
learning_rate = 0.01
steps = 100x_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 plotsdef update(frame):for i in range(6):plots[i].set_data(x_vals_list[i][frame], f(x_vals_list[i][frame]))return plotsani = 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 pltdef f(x1, x2):return x1**2 + 6*x2**2def 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 = 0for _ in range(steps):grad = grad_f(*x_vals[-1])m = beta * m + gradnew_x = x_vals[-1] - m * learning_ratex_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, PillowWriterdef f(x):return (x + 2)**2 * (x - 1)**2 + 1.5 * x**2def grad_f(x):return 2 * (x + 2) * (x - 1)**2 + 2 * (x - 1) * (x + 2)**2 + 3 * xdef gradient_descent_with_momentum(start_x, learning_rate, steps, beta=0.9, nesterov=False):x_vals = [start_x]m = 0for _ in range(steps):grad = grad_f(x_vals[-1])m = beta * m + gradif nesterov:grad += beta * melse:grad = mnew_x = x_vals[-1] - learning_rate * gradx_vals.append(new_x)if nesterov:x_vals[-1] += beta * learning_rate * mreturn np.array(x_vals)start_x = -3
learning_rate = 0.01
steps = 100x_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 plotsdef update(frame):for i in range(2):plots[i].set_data(x_vals_list[i][frame], f(x_vals_list[i][frame]))return plotsani = 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.xdnf.cn/news/150326.html

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

相关文章

CSP-S 2024 提高级 第一轮(初赛) 阅读程序(1)

【题目】 CSP-S 2024 提高级 第一轮&#xff08;初赛&#xff09; 阅读程序&#xff08;1&#xff09; 1 #include <iostream> 2 using namespace std; 3 4 const int N 1000; 5 int c[N]; 6 7 int logic(int x, int y) { 8 return (x & y) ^ ((x ^ y)…

常见区块链数据模型介绍

除了加密技术和共识算法&#xff0c;区块链技术还依赖于一种数据模型&#xff0c;它决定了信息如何被结构化、验证和存储。数据模型定义了账户如何管理&#xff0c;状态转换如何发生&#xff0c;以及用户和开发者如何与系统交互。 在区块链技术的短暂历史中&#xff0c;数据…

鸿蒙OpenHarmony【小型系统基础内核(进程管理调度器)】子系统开发

调度器 基本概念 OpenHarmony LiteOS-A内核采用了高优先级优先 同优先级时间片轮转的抢占式调度机制&#xff0c;系统从启动开始基于real time的时间轴向前运行&#xff0c;使得该调度算法具有很好的实时性。 OpenHarmony 的调度算法将 tickless 机制天然嵌入到调度算法中&…

分区与分桶

分区 分区字段大小写&#xff1a; 在hive中&#xff0c;分区字段名是不区分大小写的&#xff0c;不过字段值是区分大小写的。我们可以来测试一下 导入数据 load data local inpath /home/hivedata/user1.txt into table part4 partition(year2018,month03,DAy21); load data …

828华为云征文|华为云Flexus云服务器X实例Windows系统部署一键短视频生成AI工具moneyprinter

在追求创新与效率并重的今天&#xff0c;我们公司迎难而上&#xff0c;决定自主搭建一款短视频生成AI工具——MoneyPrinter&#xff0c;旨在为市场带来前所未有的创意风暴。面对服务器选择的难题&#xff0c;我们经过深思熟虑与多方比较&#xff0c;最终将信任票投给了华为云Fl…

毕设基于SSM+Vue3实现设备维修管理系统四:后台框架及基础增删改查功能实现

本章介绍后端基础框架及基础的增删改查功能实现&#xff0c;创建基础的dao、service即controller层相关的基类&#xff0c;并实现基础的增删改查相关功能。 源码下载&#xff1a;点击下载 讲解视频&#xff1a; SMMVUE3实现设备维修管理系统毕设&#xff1a;后端框架搭建及表外…

知识产权增资:如何以无形资产驱动企业价值增长?

随着国家政策的不断推动和各行业技术水平的不断提升&#xff0c;知识产权的增资不仅关乎企业的技术实力展示&#xff0c;更是企业资产增值、市场竞争力增强的关键途径。 概念与意义 知识产权增资&#xff0c;是指企业通过将自身拥有的知识产权评估作价后&#xff0c;作为注册…

Xcode报错:The request was denied by service delegate (SBMainWorkspace)

Xcode报错&#xff1a;The request was denied by service delegate (SBMainWorkspace) 造成的原因: &#xff08;1&#xff09;新的M2芯片的Mac电脑 (2) 此电脑首次安装启动Xcode的应用程序 (3&#xff09;此电脑未安装Rosetta 解决方法: &#xff08;1&#xff09;打开终端…

宠物空气净化器去浮毛哪家强?希喂、美的和米家实测分享

要说养宠物后里最让我感到幸福感飙升的家电&#xff0c;必须是宠物空气净化器&#xff0c;没有之一。很多人都喜欢宠物&#xff0c;但应该没有人喜欢清扫&#xff0c;特别是家里宠物多&#xff0c;或者一群宠物在自己家聚在一起之后&#xff0c;要疯狂清除浮毛&#xff0c;真的…

Jmeter压力测试-ServerAgent-2.2.3闪退问题解决

Jmeter压力测试-ServerAgent-2.2.3闪退问题解决 1. 问题现象描述&#xff1a;2. 原因分析3. 问题解决 1. 问题现象描述&#xff1a; 使用Jmeter进行压力测试时&#xff0c;如果需要收集被测试机器的性能参数&#xff0c;需要在被测试机中启动ServerAgent。 最近在做几个项目的…

性能测试之平均负载

平均负载 除了常见命令top外&#xff0c;常使用的命令有top rootdebian:~# uptime 15:20:25 up 20 days, 23:51, 9 users, load average: 0.62, 0.13, 0.04后面三个数字分别代表 1 分钟&#xff0c;5 分钟&#xff0c;15 分钟的平均负载。 如何理解这个平均负载。 如果数…

英伟达 Blackwell平台和ASIC芯片升级助力,预计2025年液冷散热渗透率将超20%

TrendForce集邦咨询: 英伟达 Blackwell平台和ASIC芯片升级助力&#xff0c;预计2025年液冷散热渗透率将超20% 根据TrendForce集邦咨询最新调查&#xff0c;随着NVIDIA Blackwell新平台预计于2024年第四季出货&#xff0c;将推动液冷散热方案的渗透率明显增长&#xff0c;从202…

c++ std::string初始化为nullptr的问题

一.问题描述 在c std::string或者std::wstring 中是否可以使用nullptr初始化&#xff0c;首先可以说的是是可以用nullptr初始化的&#xff0c;但是程序编译没有问题&#xff0c;运行起来就挂了&#xff0c;char*是可以初始化为nullptr的&#xff0c;为啥以char*为基础的std::…

Rolling Update

滚动更新是一次只更新一小部分副本&#xff0c;成功之后在更新更多的副本&#xff0c;最终完成所有的副本的更新&#xff0c;滚动更新的最大好处是零停机&#xff0c;整个更新过程始终有副本在运行&#xff0c;从而保证了业务的连续性 部署三副本的应用&#xff0c;初始镜像为…

s3c2440——I2C

一、I2C集成电路总线 &#xff08;Inter-Integrated Circuit&#xff09;SOC与芯片间通信。 属于同步串行半双工通信方式。 1、组网模式&#xff1a; 2、电器特性 &#xff08;1&#xff09;空闲时&#xff0c;两总线都为高电平&#xff1b; &#xff08;2&#xff09;数据…

BERT训练环节(代码实现)

1.代码实现 #导包 import torch from torch import nn import dltools #加载数据需要用到的声明变量 batch_size, max_len 1, 64 #获取训练数据迭代器、词汇表 train_iter, vocab dltools.load_data_wiki(batch_size, max_len) #其余都是二维数组 #tokens, segments, vali…

geodatatool(地图资源下载工具)3.8更新

geodatatool&#xff08;地图资源下载工具&#xff09;3.8&#xff08;新&#xff09;修复更新&#xff0c;修复更新包括&#xff1a; 1.选中下载数据时显示选中个数。 其它一些BUG修复。 如您有其它问题及需求&#xff0c;也可以联系我们&#xff0c;我们将持续维护更新该工…

刷题日记_DAY1

前言 这里记录每日随机刷的错题 两个数组的交集&#xff08;模拟&#xff09; 题目描述 题目解析 题目要求返回指定的两个字符串之间的距离&#xff0c;容易想到的一种解法就是暴力遍历&#xff0c;来个双循环&#xff0c;但时间复杂度就为N^2&#xff0c;不符合题意 for(…

【最新华为OD机试E卷-支持在线评测】绘图机器(100分)多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 💻 ACM金牌🏅️团队 | 大厂实习经历 | 多年算法竞赛经历 ✨ 本系列打算持续跟新华为OD-E/D卷的多语言AC题解 🧩 大部分包含 Python / C / Javascript / Java / Cpp 多语言代码 👏 感谢大家的订阅➕ 和 喜欢�…

ESXI主机加入VCENTER现有集群提示出现常规性错误

背景&#xff1a;由于忘记了这台主机的root密码&#xff0c;所以在迁移完虚拟机后给这台主机重新安装了操作系统&#xff0c;装完操作系统加集群提示如下报错&#xff1a; 查阅了一些资料后发现主机的CPU是一样的&#xff0c;不需要开EVC&#xff1b; 也有一些说需要改这个配置…