深入解析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)−η⋅(∂θ1∂f,∂θ2∂f)θi←θi−η⋅∂θi∂f,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=1∑nfi(θ)
其中 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=1∑n∇fi(θ)
如果采用梯度下降法,那么计算 ∇ 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=1∑nn1⋅∇fi(θ)=∇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(1≤b≤n) 个样本来进行个更新,即用下式去代替 ∇ 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 b1i∈B∑∇fi(θ),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[b1i∈B∑∇fi(θ)]=b21i∈B∑Vari[∇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 n1∑i=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(θ)∥2−2∇fi(θ)T∇f(θ)+∥∇f(θ)∥2]=n1i=1∑n∥∇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 e,batch 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 x←x−vΔ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=xt−1−mtΔ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+β1vt−1+β2vt−2+⋯+βt−1v1,β<1=vt+β(vt−1+βvt−2+⋯+βt−2v1)=βmt−1+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=βmt−1+gtθt=θt−1−η⋅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=βmt−1+(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(x−1)2+1.5x2 进行实验。该函数具有两个极小值点,其中一个为局部极小值,另一个为全局极小值。实验从初始点 x = − 2.5 x = -2.5 x=−2.5 开始,学习率设定为 0.01 0.01 0.01,并通过以下六组设置进行比较:
- 不使用动量和阻尼。
- 动量系数设置为 0.9 0.9 0.9,阻尼系数为 0 0 0。
- 动量系数设置为 0.9 0.9 0.9,阻尼系数为 0.5 0.5 0.5。
- 动量系数设置为 0.9 0.9 0.9,阻尼系数为 0.9 0.9 0.9。
- 动量系数设置为 0.99 0.99 0.99,阻尼系数为 0 0 0。
- 动量系数设置为 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=βmt−1+gtθt=θt−1−η⋅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=βmt−1−η⋅gtθt=θt−1+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=1∑tβt−igi,θt=θ0−ηi=1∑tmi
合并两式即可得到
θ 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=1∑tj=1∑iβi−jgj
再来推导第二个形式。不难得出
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=1∑tβt−igi,θt=θ0+i=1∑tmi
合并两式即可得到
θ 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=1∑tj=1∑iβi−jgj
由此可知,学习率固定的情况下,这两种形式是完全等价的。
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=βmt−1+gtθt=θt−1−ηt⋅mt
不难得出
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=1∑tβt−igi,θt=θ0−i=1∑tη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=θ0−i=1∑tηij=1∑iβi−jgj
第二种形式的更新公式为:
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=βmt−1−ηt⋅gtθt=θt−1+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=1∑tβt−iηigi,θt=θ0+i=1∑tmi
合并两式即可得到
θ 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=θ0−i=1∑tj=1∑iβi−jη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=mt−1+gtθt=θt−1−mt
在高维空间中,上述公式其实就是向量的加减法。下图展示了从 θ t − 1 \theta_{t-1} θt−1 到 θ 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} θt−1 处计算梯度,得到 g t = ∇ f ( θ t − 1 ) g_t=\nabla f(\theta_{t-1}) gt=∇f(θt−1)。然后用当前时刻的梯度 g t g_t gt 和上一时刻的动量 m t − 1 m_{t-1} mt−1(历史累积梯度)合成当前时刻的动量 m t m_t mt。
仔细观察, m t m_t mt 决定了第 t t t 时刻的更新方向,即当前时刻的动量决定了当前时刻的参数更新,并且参数更新均是使用动量来完成的。既然 m t m_t mt 的计算依赖于 m t − 1 m_{t-1} mt−1 和 g t g_t gt,那在计算之前,我们能不能用上一时刻的动量来提前预判当前时刻的参数更新呢?即用 m t − 1 m_{t-1} mt−1 去预判 θ t \theta_t θt 的位置。
我们在第 t t t 时刻不去计算 m t m_t mt,而是先用 m t − 1 m_{t-1} mt−1 去预判 θ 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=θt−1−mt−1,然后我们在预判的位置上重新计算梯度 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=θ~t−g~t=θt−1−mt−1−∇f(θt−1−mt−1)=θt−1−(mt−1+∇f(θt−1−mt−1))
完整的更新公式如下:
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=mt−1+∇f(θt−1−mt−1)θt=θt−1−mt

Nesterov相比原始的动量法,无非是计算梯度的位置发生了变化,原先是在 θ t − 1 \theta_{t-1} θt−1 处计算梯度,而现在是在 θ ~ t = θ t − 1 − m t − 1 \tilde{\theta}_t=\theta_{t-1}-m_{t-1} θ~t=θt−1−mt−1 处计算梯度,更具有前瞻性。
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=βmt−1−η∇f(θt−1+βmt−1)θt=θt−1+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=θt−1−ηmt=θt−1−η(βmt−1+gt)=(θt−1−ηβmt−1)−ηgt−1
于是可以推测 g t − 1 = ∇ f ( θ t − 1 − η β m t − 1 ) g_{t-1}=\nabla f(\theta_{t-1}-\eta \beta m_{t-1}) gt−1=∇f(θt−1−ηβmt−1),从而该形式下的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=βmt−1+∇f(θt−1−ηβmt−1)θt=θt−1−η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=βmt−1+∇f(θt−1)θt=θt−1−η(βmt+∇f(θt−1))
可以发现它并没有使用偏移后的参数进行梯度计算,这是为什么呢?
在 ( ∗ ) (*) (∗) 式中令 θ ~ t − 1 = θ t − 1 − η β m t − 1 \tilde{\theta}_{t-1}=\theta_{t-1}-\eta \beta m_{t-1} θ~t−1=θt−1−ηβmt−1,然后利用 ( ∗ ) (*) (∗) 式中的结果去推导 θ ~ 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=θt−1−ηmt−ηβmt=θt−1−η(βmt−1+∇f(θ~t−1))−ηβmt=(θt−1−ηβmt−1)−η∇f(θ~t−1)−ηβmt=θ~t−1−η(βmt+∇f(θ~t−1))
由此可以得知,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=βmt−1+∇f(θ~t−1)∇f(θ~t−1)=∇f(θ~t−1)+βmtθt=θt−1−η∇f(θ~t−1)
计算完成后修正参数即可。
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
相关文章:

深入解析SGD、Momentum与Nesterov:优化算法的对比与应用
目录 1. 梯度下降算法2. BGD、SGD、MBGD3. momentum与dampening3.1 另一种形式的momentum3.1.1 学习率固定3.1.2 学习率不固定 4. nesterov4.1 PyTorch中的Nesterov4.2 Polyak与Nesterov的比较 Ref 1. 梯度下降算法 先考虑一元情形。假设待更新的参数为 θ \theta θ…...

Vue2实现主内容滚动到指定位置时,侧边导航栏也跟随选中变化
需求背景: PC端项目需要实现一个有侧边导航栏,可点击跳转至对应内容区域,类似锚点导航, 同时主内容区域上下滚动时,可实现左侧导航栏选中样式能实时跟随变动的效果。 了解了一下,Element Plus 组件库 和 …...

dev containers plugins for vscode构建虚拟开发环境
0. 需求说明 自用笔记本构建一套开发环境,用docker 虚拟插件 dev containers,实现开发环境的构建,我想构建一套LLMs的环境,由于环境配置太多,不想污染本地环境,所以选择隔离技术 1. 环境准备 vscodedocker 2. 步骤…...

C++ | Leetcode C++题解之第433题最小基因变化
题目: 题解: class Solution { public:int minMutation(string start, string end, vector<string>& bank) {int m start.size();int n bank.size();vector<vector<int>> adj(n);int endIndex -1;for (int i 0; i < n; i)…...

Qt窗口——QMenuBar
文章目录 QMenuBar示例演示给菜单栏设置快捷键给菜单项设置快捷键添加子菜单添加分割线添加图标 QMenuBar Qt中采用QMenuBar来创建菜单栏,一个主窗口,只允许有一个菜单栏,位于主窗口的顶部、主窗口标题栏下面;一个菜单栏里面有多…...

python网站创建001:内容概览
内容概览: 1. Python环境搭建(Python解释器、Pycharm、环境变量等) 2. 基础语法(条件、循环、输入输出、编码等) 3. 数据类型型(整型、布尔型、字符串、列表、字典、元组、集合等) 4. 函数&…...

代码随想录第22天|
class Solution { public:int findContentChildren(vector<int>& g, vector<int>& s) {sort(g.begin(), g.end());sort(s.begin(), s.end());int index s.size() - 1; // 饼干数组的下标int result 0;for (int i g.size() - 1; i > 0; i--) { // 遍历…...

WPF-基础-02 DispatcherObject类
public abstract class DispatcherObject {protected DispatcherObject();public Dispatcher Dispatcher { get; }public bool CheckAccess();public void VerifyAccess(); }WPF中使用Dispatcher更新界面 xaml<Grid><TextBlock x:Name"tbkShow" Horizontal…...

STM32与51单片机的区别:是否应该直接学习STM32?
STM32与51单片机的区别:是否应该直接学习STM32? 在单片机的世界里,STM32和51单片机都是非常重要的角色。对于初学者来说,是否可以直接跳过51单片机,直接学习STM32,这个问题一直存在争议。让我们深入探讨这…...

【网络底层原理】I/O多路复用技术select、poll和epoll详解与比较
引言 在现代网络编程中,I/O多路复用技术是实现高性能服务器的关键。本文将详细介绍select、poll和epoll这三种技术,并比较它们的工作原理、优势与限制。 1. select 工作原理 select技术使用三个集合(读、写、异常)来跟踪需要监…...

【JavaScript】LeetCode:51-55
文章目录 51 验证二叉搜索树52 二叉搜索树中第k小的元素53 二叉树的右视图54 二叉树展开为链表55 从前序与中序遍历序列构造二叉树 51 验证二叉搜索树 递归对二叉搜索树进行中序遍历,输出节点的值是单调递增的。方法1:对二叉树进行中序遍历,将…...

Spring MVC 拦截器总结
1.简介 Spring MVC提供了拦截器方便在接口调用前后进行一些通用处理。 2.步骤 1.实现一个拦截器类,共有三处拦截时机: public class Interceptor1 implements HandlerInterceptor {//实现HandlerInterceptor接口//执行handler之前调用//编码格式处理…...

Linux——创建编写并编译一个C程序
一、使用vim编辑器 在Linux系统下,使用vim编辑器创建、编写并编译一个C程序是一个常见的做法。以下是一个详细的步骤指南,我们将创建一个简单的C程序,该程序的功能是输出“Hello, World!”到终端。 步骤 1: 打开vim编辑器并创建C程序文件 …...

window下idea中scala的配置
目录 Scala安装步骤: 1.下载scala安装包 2.配置环境变量: 3.检查scala是否安装成功: 4.idea安装scala插件 5.导入scala-sdk 6.新建scala文件 Scala安装步骤: 1.下载scala安装包 访问Scala官网:https://www.sca…...

Qt C++设计模式->享元模式
享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享相同对象来减少内存使用,尤其适合在大量重复对象的情况下。它通过将对象的可共享部分抽取出来,并在多个上下文中共享,从而避免对象的多次创建…...

前端实用技能
焦点聚焦 import Vue from vue // 插件对象(必须有 install 方法, 才可以注入到 Vue.use 中) export default {install () {Vue.directive(fofo, {inserted (el) {el el.querySelector(input)el.focus()}})} }格式化日期格式 export const formatDate (time) > {// 将xx…...

Android LiveData 数据倒灌
相关类型的文章很多,这里只做个人总结和其余的方法推荐 1.什么是数据倒灌? 所谓的“数据倒灌”:其实是类似粘性广播那样,当新的观察者开始注册观察时,会把上次发的最后一次的历史数据传递给当前注册的观察者。 一方…...

umi项目中使用mockj生成数据模拟请求调用
Mock.js简介 Mock.js 是一个轻量级且无依赖的JavaScript库,用于生成模拟数据。它可以帮助开发者在前端开发过程中模拟后端API接口,以便进行快速原型设计和测试。Mock.js 提供了丰富的API来模拟各种类型的数据,如字符串、数字、日期、数组等。…...

事件【JavaScript】
1. 事件 事件是用户或浏览器动作的表示,JavaScript 中的一切交互都是通过事件来处理的。 2. 事件冒泡(Event Bubbling) 事件冒泡是指事件从最具体的元素(即触发事件的元素)开始触发,然后逐级向上传播到较…...

【Linux】Linux基本命令
目录 文件和目录操作: ls cd pwd cp mv rm mkdir rmdir touch clear history which/whereis 文件查看和编辑: cat less head tail vi 或 vim sz/rz echo 系统信息和管理: su uname hostname df free top ps ki…...

微软宣称其新工具可纠正人工智能幻觉 但专家依然对此表示怀疑
人工智能经常胡言乱语,微软现在说它有办法解决这个问题,但我们有理由对此持怀疑态度。微软今天发布了一项名为"更正"(Correction)的服务,它可以自动修改人工智能生成的与事实不符的文本。Correction 首先会标…...

实战OpenCV之图像滤波
基础入门 图像滤波是数字图像处理中一种非常重要的技术,主要用于图像噪声去除、图像平滑、突出图像特征,或者进行图像风格的转换。它通过数学运算对图像中的像素值进行修改,以达到特定的处理目的。图像滤波可以分为两大类,分别为:线性滤波、非线性滤波。 线性滤波器通过一…...

AI学习指南深度学习篇-Adadelta的Python实践
AI学习指南深度学习篇-Adadelta的Python实践 深度学习是人工智能领域的一个重要分支,近年来在各个领域都取得了显著的成就。在深度学习的模型训练中,优化算法起着至关重要的作用,其中Adadelta是一种常用的优化算法之一。本篇博客将使用Pytho…...

go webapi上传文件 部属到linux
go厉害的地方,linux服务器上无需安装任何依赖就可以运行,大赞! 一、编译 #在Goland中cmd中执行 go env -w GOARCHamd64 go env -w GOOSlinux go build main.go # 切换回来 否则无法运行 go env -w GOOSwindows go run main.go 拷贝到linux服…...

接口加解密及数据加解密
目录 一、 加解密方式介绍 1.1 Hash算法加密 1.2. 对称加密 1.3 非对称加密 二、 我们要讲什么? 三、 接口加解密 四、 数据加解密 一、 加解密方式介绍 所有的加密方式我们可以分为三类:对称加密、非对称加密、Hash算法加密。 算法内部的具体实现…...

开创远程就可以监测宠物健康新篇章
在宠物健康监测的新纪元,智能听诊器凭借其先进技术,正逐步改变我们对宠物健康监护的传统认知。这不仅是一款监测工具,而是宠物健康管理的得力助手,为宠物主人和兽医提供前所未有的洞察力和便捷性。 深度学习算法:智能…...

二叉树的基本概念(上)
文章目录 🍊自我介绍🍊简介🍊树的定义树中的专业术语树的分类 🍊二叉树的特性讲解 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以:点赞关注评论收藏(一键四连)哦~ 🍊自我介…...

aws s3 存储桶 前端组件上传简单案例
写一个vue3 上传aws oss存储的案例 使用到的插件 npm install aws-sdk/client-s3 注意事项 : 1. 本地调试 , 需要设置在官网设置跨域 必须!!! 否则调试不了 ,前端代理是不起作用的 ,因为是插…...

【开源免费】基于SpringBoot+Vue.JS墙绘产品展示交易平台(JAVA毕业设计)
本文项目编号 T 049 ,文末自助获取源码 \color{red}{T049,文末自助获取源码} T049,文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 查…...

python爬虫初体验(四)—— 百度文库PPT的爬取
文章目录 1. 安装包2. 相关代码3. 说明4. 注意事项5. 扩展功能5.1 多页面下载5.2 输入地址下载 在Python 2中编写一个爬虫来大量下载图片,可以使用requests库来发送HTTP请求,并使用BeautifulSoup来解析HTML页面。此外,可以使用urllib2库来下载…...