自适应学习率机制:AdamW/RAdam的理论基础与代码实现细节
大家好!今天我们来深入探讨深度学习优化器中两种非常重要的自适应学习率机制:AdamW 和 RAdam。我们将从理论基础入手,逐步推导其公式,并最终通过 Python 代码实现它们的核心逻辑。
1. 自适应学习率的必要性
在传统的梯度下降法中,我们使用固定的学习率更新模型参数。然而,固定的学习率在训练过程中可能会遇到一些问题:
- 学习率过大: 可能导致模型在最优解附近震荡,无法收敛。
- 学习率过小: 可能导致训练速度过慢,甚至陷入局部最小值。
自适应学习率算法通过动态调整每个参数的学习率,来解决这些问题。它们通常会根据参数的历史梯度信息来调整学习率,从而在不同阶段和不同参数上使用更合适的学习率。
2. Adam:Adaptive Moment Estimation
Adam 是一种非常流行的自适应学习率优化器。它结合了动量法(Momentum)和 RMSProp 的优点。其核心思想是利用梯度的一阶矩估计(mean)和二阶矩估计(variance)来调整学习率。
2.1 数学原理
Adam 的更新公式如下:
-
计算梯度: 计算损失函数 L 对参数 θ 的梯度 gt = ∇L(θt-1)。
-
计算一阶矩估计(动量):
mt = β1 mt-1 + (1 – β1) gt -
计算二阶矩估计(非中心化的方差):
vt = β2 vt-1 + (1 – β2) gt2 -
偏差校正:
m̂t = mt / (1 – β1t)
v̂t = vt / (1 – β2t) -
更新参数:
θt = θt-1 – α * m̂t / (√v̂t + ε)
其中:
- θt 是 t 时刻的参数。
- gt 是 t 时刻的梯度。
- mt 是 t 时刻的一阶矩估计。
- vt 是 t 时刻的二阶矩估计。
- β1 是控制一阶矩估计衰减的系数(通常为 0.9)。
- β2 是控制二阶矩估计衰减的系数(通常为 0.999)。
- α 是初始学习率。
- ε 是一个很小的数(通常为 1e-8),用于防止分母为零。
- m̂t 和 v̂t 是经过偏差校正的一阶矩估计和二阶矩估计。
表格:Adam 参数说明
| 参数 | 含义 | 常用取值 |
|---|---|---|
| α | 初始学习率 | 0.001 |
| β1 | 一阶矩估计的指数衰减率 | 0.9 |
| β2 | 二阶矩估计的指数衰减率 | 0.999 |
| ε | 防止分母为零的小常数 | 1e-8 |
2.2 代码实现
import numpy as np
class Adam:
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.epsilon = epsilon
self.m = None
self.v = None
self.t = 0
def update(self, params, grads):
if self.m is None:
self.m = [np.zeros_like(param) for param in params]
self.v = [np.zeros_like(param) for param in params]
self.t += 1
for i in range(len(params)):
self.m[i] = self.beta1 * self.m[i] + (1 - self.beta1) * grads[i]
self.v[i] = self.beta2 * self.v[i] + (1 - self.beta2) * grads[i] ** 2
m_hat = self.m[i] / (1 - self.beta1 ** self.t)
v_hat = self.v[i] / (1 - self.beta2 ** self.t)
params[i] -= self.lr * m_hat / (np.sqrt(v_hat) + self.epsilon)
return params
代码解释:
__init__:初始化 Adam 优化器,包括学习率、β1、β2、ε 以及用于存储一阶矩和二阶矩估计的变量m和v。update:根据梯度更新参数。首先,判断m是否为空,如果为空则初始化为与参数形状相同的零数组。然后,更新一阶矩和二阶矩估计,并进行偏差校正。最后,根据更新公式更新参数。
3. AdamW:Decoupled Weight Decay
AdamW 是 Adam 的一个改进版本,主要解决了 Adam 中权重衰减(Weight Decay)的不合理之处。
3.1 问题:Adam 中的 Weight Decay
在传统的 Adam 中,权重衰减通常直接添加到梯度上,如下所示:
gt = ∇L(θt-1) + λ * θt-1
θt = θt-1 – α m̂t / (√v̂t + ε) – α λ * θt-1
其中 λ 是权重衰减系数。
这种方式存在一个问题:权重衰减与自适应学习率机制相互影响。具体来说,Adam 会根据参数的历史梯度信息调整学习率,而权重衰减会改变梯度,从而影响自适应学习率的计算。这可能导致权重衰减的效果不稳定,尤其是在使用较大的学习率时。
3.2 解决方案:解耦 Weight Decay
AdamW 的核心思想是将权重衰减从梯度计算中解耦出来,直接在参数更新时应用权重衰减:
-
计算梯度: gt = ∇L(θt-1)
-
计算一阶矩估计(动量): mt = β1 mt-1 + (1 – β1) gt
-
计算二阶矩估计(非中心化的方差): vt = β2 vt-1 + (1 – β2) gt2
-
偏差校正: m̂t = mt / (1 – β1t)
v̂t = vt / (1 – β2t) -
更新参数: θt = θt-1 – α m̂t / (√v̂t + ε) – α λ * θt-1
注意,这里权重衰减项 - α * λ * θ<sub>t-1</sub> 是在 Adam 更新之后独立应用的。
3.3 代码实现
import numpy as np
class AdamW:
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8, weight_decay=0.01):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.epsilon = epsilon
self.weight_decay = weight_decay
self.m = None
self.v = None
self.t = 0
def update(self, params, grads):
if self.m is None:
self.m = [np.zeros_like(param) for param in params]
self.v = [np.zeros_like(param) for param in params]
self.t += 1
for i in range(len(params)):
self.m[i] = self.beta1 * self.m[i] + (1 - self.beta1) * grads[i]
self.v[i] = self.beta2 * self.v[i] + (1 - self.beta2) * grads[i] ** 2
m_hat = self.m[i] / (1 - self.beta1 ** self.t)
v_hat = self.v[i] / (1 - self.beta2 ** self.t)
params[i] -= self.lr * m_hat / (np.sqrt(v_hat) + self.epsilon)
params[i] -= self.lr * self.weight_decay * params[i] # 解耦的权重衰减
return params
代码解释:
__init__:初始化 AdamW 优化器,与 Adam 类似,但增加了一个weight_decay参数。update:与 Adam 类似,计算梯度的一阶矩和二阶矩估计,并进行偏差校正。关键区别在于,在更新参数时,首先应用 Adam 更新,然后独立应用权重衰减。
4. RAdam:Rectified Adam
RAdam 旨在解决 Adam 在训练初期可能存在的问题。
4.1 问题:Adam 的早期不稳定
Adam 在训练初期,由于二阶矩估计 vt 可能不稳定,导致学习率的方差较大,从而影响模型的收敛。
4.2 解决方案:Rectified Adam
RAdam 通过引入一个修正项来解决这个问题。它基于对二阶矩估计的方差的分析,提出了一个修正项 ρt,用于调整学习率。
4.3 数学原理
-
计算梯度: gt = ∇L(θt-1)
-
计算一阶矩估计(动量): mt = β1 mt-1 + (1 – β1) gt
-
计算二阶矩估计(非中心化的方差): vt = β2 vt-1 + (1 – β2) gt2
-
计算 ρt:
L = 2 / (1 – β2) – 1
ρt = L if v_inf > v_t else (ρ_inf sqrt((v_inf-4)/(v_inf-2) (v_t/(v_inf-4*v_t/(L+1))))其中:
- v_inf = L
- ρ_inf = sqrt((v_inf-4)/(v_inf-2))
-
更新参数:
-
如果 ρt > 4: 使用修正后的 Adam 更新:
θt = θt-1 – α ρt mt / (√vt + ε) -
否则: 使用标准的随机梯度下降(SGD)更新:
θt = θt-1 – α * gt
-
公式解释:
- ρt 是一个动态调整的修正项,用于调整学习率。当二阶矩估计 vt 比较稳定时,ρt 接近于 1,Adam 的更新方式与原始 Adam 相似。当 vt 不稳定时,ρt 减小,从而降低学习率,甚至退化为 SGD。
4.4 代码实现
import numpy as np
class RAdam:
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.epsilon = epsilon
self.m = None
self.v = None
self.t = 0
self.L = 2 / (1 - beta2) - 1 #修正点
self.rho_inf = np.sqrt((self.L-4)/(self.L-2))
def update(self, params, grads):
if self.m is None:
self.m = [np.zeros_like(param) for param in params]
self.v = [np.zeros_like(param) for param in params]
self.t += 1
for i in range(len(params)):
self.m[i] = self.beta1 * self.m[i] + (1 - self.beta1) * grads[i]
self.v[i] = self.beta2 * self.v[i] + (1 - self.beta2) * grads[i] ** 2
v_t = self.v[i]
v_inf = self.L
if v_inf > v_t:
rho_t = self.rho_inf * np.sqrt(((v_inf-4)/(v_inf-2)) * (v_t/(v_inf-4*v_t/(self.L+1))))
else:
rho_t = self.L
if rho_t > 4:
params[i] -= self.lr * rho_t * self.m[i] / (np.sqrt(self.v[i]) + self.epsilon)
else:
params[i] -= self.lr * grads[i]
return params
代码解释:
__init__:初始化 RAdam 优化器,与 Adam 类似。update:计算梯度的一阶矩和二阶矩估计。关键在于计算 ρt,并根据 ρt 的值选择使用修正后的 Adam 更新或 SGD 更新。
5. 其他自适应学习率优化器
除了 AdamW 和 RAdam,还有许多其他的自适应学习率优化器,例如:
- Adagrad: 根据每个参数的历史梯度平方和来调整学习率。
- RMSProp: 类似于 Adagrad,但使用指数衰减平均来计算历史梯度平方和。
- Adamax: 基于 infinity norm 的 Adam 变体。
- NAdam: 将 Nesterov 动量引入 Adam。
6. 优化器的选择
选择合适的优化器取决于具体的任务和数据集。一般来说,AdamW 和 RAdam 在许多情况下都能取得良好的效果。以下是一些选择优化器的建议:
- AdamW: 如果需要使用权重衰减,AdamW 是一个不错的选择。
- RAdam: 如果训练初期模型不稳定,可以尝试 RAdam。
- SGD: 对于某些任务,SGD 仍然是最好的选择。可以尝试使用 SGD 配合动量。
7. 进一步的优化策略
除了选择合适的优化器之外,还可以使用一些其他的优化策略来提高模型的性能,例如:
- 学习率衰减: 随着训练的进行,逐渐减小学习率。
- 梯度裁剪: 限制梯度的范围,防止梯度爆炸。
- Warmup: 在训练初期逐渐增加学习率。
- 混合精度训练: 使用半精度浮点数来加速训练。
自适应学习率优化器的演进与差异
我们从 Adam 出发,了解了自适应学习率的优势,以及其结合动量和 RMSProp 的方式。接着,我们看到了 AdamW 如何通过解耦权重衰减来解决传统 Adam 的问题,从而实现更稳定的权重衰减效果。最后,RAdam 通过引入修正项,解决了 Adam 在训练初期可能存在的不稳定性问题。 这几种优化器都在不同的角度优化了训练的过程,最终提升模型的性能。
更多IT精英技术系列讲座,到智猿学院