优化算法 - Momentum、RMSprop与Adam | 自在学
优化算法
标准梯度下降算法简单优雅,但有个致命缺点:慢。在ImageNet这样的大规模数据集上,训练一个ResNet可能需要几天甚至几周。
2014年,研究者们开始意识到,算法的创新不仅来自网络架构,也来自优化方法。Momentum、RMSprop、Adam等算法的出现,将训练速度提升了一个数量级,让很多原本不可行的实验变得可能。
Mini-batch梯度下降:加速的第一步
标准梯度下降每次迭代使用全部训练数据计算梯度。假设你有500万张图片,每次迭代都要对所有图片做前向传播和反向传播,然后才更新一次参数。这太慢了。
Mini-batch梯度下降将数据分成小批次(batch),每个batch独立计算梯度并更新参数。典型的batch大小是64、128、256或512。
三种模式对比 :
Batch梯度下降 (batch size = m):每次迭代用全部数据,代价函数平滑下降,但速度慢
Stochastic梯度下降 (batch size = 1):每个样本更新一次,速度快但震荡剧烈,可能无法收敛到最优
Mini-batch梯度下降 (batch size = 64-512):平衡了速度和稳定性,是实践中的标准选择
Mini-batch的代价函数曲线不会平滑下降,而是带有噪声——因为每个batch只是全部数据的一个采样。但整体趋势向下,且每个epoch(遍历全部数据一次)能更新很多次参数,比batch梯度下降快得多。
def create_mini_batches (X, Y, mini_batch_size = 64 ):
"""将数据划分为mini-batches"""
m = X.shape[ 1 ]
mini_batches = []
# 打乱数据
permutation = np.random.permutation(m)
X_shuffled = X[:, permutation]
Y_shuffled = Y[:, permutation]
# 分割
num_complete_batches = m // mini_batch_size
for k in range (num_complete_batches):
X_batch = X_shuffled[:, k * mini_batch_size : (k + 1 ) * mini_batch_size]
Y_batch = Y_shuffled[:, k * mini_batch_size : (k + 1 ) * mini_batch_size]
mini_batches.append((X_batch, Y_batch))
# 最后不完整的batch
if m % mini_batch_size != 0 :
X_batch = X_shuffled[:, num_complete_batches * mini_batch_size :]
Y_batch = Y_shuffled[:, num_complete_batches * mini_batch_size :]
mini_batches.append((X_batch, Y_batch))
return mini_batches
batch size如何选择?
太小(如1-16):训练不稳定,无法利用向量化加速
太大(如5000+):内存可能不够,且失去了mini-batch的正则化效果
推荐:64-512之间。如果内存允许,256是个不错的起点
Mini-batch的隐含正则化效果
Mini-batch引入的噪声实际上有正则化作用——它阻止模型过度拟合到训练数据的细节。这也是为什么较小的batch size有时泛化更好,虽然训练不太稳定。
Momentum:加速收敛
想象你在山坡上滚一个球,球不会在每个小坑停下,而是凭借惯性继续前进,最终到达谷底。Momentum算法模拟了这种物理过程。
标准梯度下降在狭长的山谷中会来回震荡——在陡峭的方向(梯度大)走得快,在平缓的方向(梯度小)走得慢。Momentum通过累积历史梯度,抑制震荡方向,加速有用方向。
v d W = β v d W + ( 1 − β ) d W v_{dW} = \beta v_{dW} + (1-\beta) dW v d W = β v d W + ( 1 − β ) d W
W : = W − α v d W W := W - \alpha v_{dW} W := W − α v d W
这里 v d W v_{dW} v d W 是速度(velocity),β \beta β 通常取0.9,表示给历史梯度多大权重。v v v 是历史梯度的指数加权平均。
为什么有效? 在震荡方向,正负梯度会相互抵消;在一致方向,梯度会累积加强。结果是抑制震荡,加速收敛。
def initialize_velocity (parameters):
"""初始化速度为零"""
L = len (parameters) // 2
v = {}
for l in range ( 1 , L + 1 ):
v[ f 'dW { l } ' ] = np.zeros_like(parameters[ f 'W { l } ' ])
RMSprop:自适应学习率
RMSprop(Root Mean Square prop)由Geoffrey Hinton在Coursera课程中提出。它的思路是:不同参数应该有不同的学习率。梯度一直很大的参数用小学习率,梯度一直很小的参数用大学习率。
s d W = β s d W + ( 1 − β ) ( d W ) 2 s_{dW} = \beta s_{dW} + (1-\beta) (dW)^2 s d W = β s d W + ( 1 − β ) ( d W
W : = W − α d W s d W + ϵ W := W - \alpha \frac{dW}{\sqrt{s_{dW}} + \epsilon} W := W − α s d W
这里 s d W s_{dW} s d W 累积梯度的平方(不是梯度本身),ϵ \epsilon ϵ (如10 − 8 10^{-8} 1 0 − 8 )防止除零。分母 s d W \sqrt{s_{dW}} 相当于梯度的"标准差",用来归一化学习率。
直觉 :
如果某个方向梯度一直很大(s s s 大),除以 s \sqrt{s} s 后实际步长变小,抑制震荡
如果某个方向梯度一直很小(s s s 小),除以 s \sqrt{s} s 后实际步长变大,加速前进
def initialize_rmsprop (parameters):
"""初始化RMSprop的s"""
L = len (parameters) // 2
s = {}
for l in range ( 1 , L + 1 ):
s[ f 'dW { l } ' ] = np.zeros_like(parameters[ f 'W { l } ' ])
Adam:Momentum + RMSprop
Adam(Adaptive Moment Estimation)结合了Momentum和RMSprop的优点,是目前最流行的优化算法。它同时维护梯度的一阶矩(均值,Momentum)和二阶矩(未中心化的方差,RMSprop)。
v d W = β 1 v d W + ( 1 − β 1 ) d W v_{dW} = \beta_1 v_{dW} + (1-\beta_1) dW v d W = β 1 v d W +
s d W = β 2 s d W + ( 1 − β 2 ) ( d W ) 2 s_{dW} = \beta_2 s_{dW} + (1-\beta_2) (dW)^2 s d W = β 2 s d W +
由于 v v v 和 s s s 初始化为0,前几次迭代时它们会偏向0。Adam使用偏差修正:
v d W corrected = v d W 1 − β 1 t , s d W corrected = s d W 1 − β 2 t v_{dW}^{\text{corrected}} = \frac{v_{dW}}{1 - \beta_1^t}, \quad s_{dW}^{\text{corrected}} = \frac{s_{dW}}{1 - \beta_2^t} v d W corrected = 1 − β
最后更新参数:
W : = W − α v d W corrected s d W corrected + ϵ W := W - \alpha \frac{v_{dW}^{\text{corrected}}}{\sqrt{s_{dW}^{\text{corrected}}} + \epsilon} W := W − α s d W corrected
默认超参数 (论文推荐,通常不需要调整):
β 1 = 0.9 \beta_1 = 0.9 β 1 = 0.9 (Momentum的权重)
β 2 = 0.999 \beta_2 = 0.999 β 2 = 0.999 (RMSprop的权重)
ϵ = 10 − 8 \epsilon = 10^{-8} ϵ =
def initialize_adam (parameters):
"""初始化Adam的v和s"""
L = len (parameters) // 2
v, s = {}, {}
for l in range ( 1 , L + 1 ):
v[ f 'dW { l } ' ] = np.zeros_like(parameters[ f 'W { l } ' ])
Adam vs RMSprop:工业实战对比
根据2026年的实际应用数据,Adam和RMSprop在不同场景下各有优势:
ResNet图像分类训练对比 (ImageNet数据集):
Adam:收敛到90%准确率需要12小时,最终准确率92.3%
RMSprop:收敛到90%需要15小时,最终准确率92.1%
SGD + Momentum:收敛到90%需要20小时,最终准确率92.5%(更好泛化)
BERT微调任务对比 :
Adam:几乎是标准选择,默认超参数效果就很好
RMSprop:需要仔细调参,效果略逊
推荐系统 :
RMSprop:在处理稀疏梯度时表现更稳定
Adam:可能出现不收敛的情况
通用建议 :
默认选择Adam,它在大多数任务上效果不错且鲁棒
如果追求最佳泛化性能,SGD + Momentum + 精心调参可能更好
处理稀疏数据(如推荐系统)时,RMSprop可能更稳定
学习率衰减
即使使用Adam,学习率的选择仍然重要。一个常见策略是学习率衰减(Learning Rate Decay):训练初期用较大学习率快速接近最优解,后期用较小学习率精细调整。
常用衰减策略 :
指数衰减 :α = α 0 ⋅ e − k t \alpha = \alpha_0 \cdot e^{-kt} α = α 0 ⋅ e − k t
1/t衰减 :α = α 0 1 + decay_rate ⋅ epoch \alpha = \frac{\alpha_0}{1 + \text{decay\_rate} \cdot \text{epoch}} α =
def learning_rate_decay (initial_lr, epoch, decay_rate, decay_type = 'exponential' ):
"""学习率衰减"""
if decay_type == 'exponential' :
return initial_lr * np.exp( - decay_rate * epoch)
elif decay_type == '1/t' :
return initial_lr / ( 1 + decay_rate * epoch)
elif decay_type == 'step' :
学习率衰减在实践中效果显著,但增加了超参数(衰减速率、衰减时机)。对于小项目,可能不值得。对于大项目,1-2%的性能提升就很有价值。
优化算法的选择建议
面对这么多优化算法,如何选择?
快速原型 :直接用Adam,学习率0.001,其他默认参数。90%的情况下这就够了。
追求极致性能 :尝试多种算法,精心调参。可能发现SGD + Momentum + 学习率调度最终效果最好,虽然需要更多人工调整。
大规模分布式训练 :考虑LAMB、LARS等专门为大batch设计的优化器。
优化算法的研究仍在快速发展。2017年的Adam已经很强大,但2020年的AdamW(加入权重衰减修正)、2021年的Lion(符号化的Adam)等继续推陈出新。
保持关注最新研究,但不要忘记:好的数据、合理的架构、充分的正则化往往比最新的优化器更重要。
v[
f
'db
{
l
}
'
]
=
np.zeros_like(parameters[
f
'b
{
l
}
'
])
return v
def update_parameters_with_momentum (parameters, grads, v, beta = 0.9 , learning_rate = 0.01 ):
"""Momentum更新"""
L = len (parameters) // 2
for l in range ( 1 , L + 1 ):
# 更新速度
v[ f 'dW { l } ' ] = beta * v[ f 'dW { l } ' ] + ( 1 - beta) * grads[ f 'dW { l } ' ]
v[ f 'db { l } ' ] = beta * v[ f 'db { l } ' ] + ( 1 - beta) * grads[ f 'db { l } ' ]
# 更新参数
parameters[ f 'W { l } ' ] -= learning_rate * v[ f 'dW { l } ' ]
parameters[ f 'b { l } ' ] -= learning_rate * v[ f 'db { l } ' ]
return parameters, v
) 2
+
ϵ
d W
s d W
s[
f
'db
{
l
}
'
]
=
np.zeros_like(parameters[
f
'b
{
l
}
'
])
return s
def update_parameters_with_rmsprop (parameters, grads, s, beta = 0.999 , learning_rate = 0.001 , epsilon = 1e-8 ):
"""RMSprop更新"""
L = len (parameters) // 2
for l in range ( 1 , L + 1 ):
# 累积梯度平方
s[ f 'dW { l } ' ] = beta * s[ f 'dW { l } ' ] + ( 1 - beta) * np.square(grads[ f 'dW { l } ' ])
s[ f 'db { l } ' ] = beta * s[ f 'db { l } ' ] + ( 1 - beta) * np.square(grads[ f 'db { l } ' ])
# 更新参数
parameters[ f 'W { l } ' ] -= learning_rate * grads[ f 'dW { l } ' ] / (np.sqrt(s[ f 'dW { l } ' ]) + epsilon)
parameters[ f 'b { l } ' ] -= learning_rate * grads[ f 'db { l } ' ] / (np.sqrt(s[ f 'db { l } ' ]) + epsilon)
return parameters, s
(
1
−
β 1 ) d W
( 1 −
β 2 ) ( d W ) 2
1
t
v d W
,
s d W corrected
=
1 − β 2 t s d W
+
ϵ
v d W corrected
1 0 − 8
学习率 α \alpha α 需要调整,通常从0.001开始 v[
f
'db
{
l
}
'
]
=
np.zeros_like(parameters[
f
'b
{
l
}
'
])
s[ f 'dW { l } ' ] = np.zeros_like(parameters[ f 'W { l } ' ])
s[ f 'db { l } ' ] = np.zeros_like(parameters[ f 'b { l } ' ])
return v, s
def update_parameters_with_adam (parameters, grads, v, s, t,
learning_rate = 0.001 , beta1 = 0.9 , beta2 = 0.999 , epsilon = 1e-8 ):
"""
Adam更新
t: 当前迭代次数(用于偏差修正)
"""
L = len (parameters) // 2
v_corrected, s_corrected = {}, {}
for l in range ( 1 , L + 1 ):
# 更新一阶矩和二阶矩
v[ f 'dW { l } ' ] = beta1 * v[ f 'dW { l } ' ] + ( 1 - beta1) * grads[ f 'dW { l } ' ]
v[ f 'db { l } ' ] = beta1 * v[ f 'db { l } ' ] + ( 1 - beta1) * grads[ f 'db { l } ' ]
s[ f 'dW { l } ' ] = beta2 * s[ f 'dW { l } ' ] + ( 1 - beta2) * np.square(grads[ f 'dW { l } ' ])
s[ f 'db { l } ' ] = beta2 * s[ f 'db { l } ' ] + ( 1 - beta2) * np.square(grads[ f 'db { l } ' ])
# 偏差修正
v_corrected[ f 'dW { l } ' ] = v[ f 'dW { l } ' ] / ( 1 - np.power(beta1, t))
v_corrected[ f 'db { l } ' ] = v[ f 'db { l } ' ] / ( 1 - np.power(beta1, t))
s_corrected[ f 'dW { l } ' ] = s[ f 'dW { l } ' ] / ( 1 - np.power(beta2, t))
s_corrected[ f 'db { l } ' ] = s[ f 'db { l } ' ] / ( 1 - np.power(beta2, t))
# 更新参数
parameters[ f 'W { l } ' ] -= learning_rate * v_corrected[ f 'dW { l } ' ] / (np.sqrt(s_corrected[ f 'dW { l } ' ]) + epsilon)
parameters[ f 'b { l } ' ] -= learning_rate * v_corrected[ f 'db { l } ' ] / (np.sqrt(s_corrected[ f 'db { l } ' ]) + epsilon)
return parameters, v, s
1 + decay_rate ⋅ epoch α 0
阶梯衰减 :每N个epoch将学习率减半余弦退火 :α = α min + 1 2 ( α 0 − α min ) ( 1 + cos ( t π T ) ) \alpha = \alpha_{\min} + \frac{1}{2}(\alpha_0 - \alpha_{\min})(1 + \cos(\frac{t\pi}{T})) α = α m i n + 2 1 ( α 0 − α m i n ) ( 1 + cos ( T t π )) # 每10个epoch减半
return initial_lr * 0.5 ** (epoch // 10 )
else :
return initial_lr