AdaGrad、RMSProp 针对学习率进行了优化,不同的参数分量在更新时能够使用各自更适合的学习率。Momentum 则是对梯度进行了优化,可以避免碰到鞍点、局部最小值时参数无法更新的情况。
我们经常把 Adam 理解为结合了 Momentum 和 RMSProp 的优点,即:对梯度和学习率都进行了优化的梯度下降法。
1. 算法思想理解
Adam 的计算过程如下:
首先,初始化 t 变量,该变量表示当前进行的 step 是多少,为什么需要知道当前优化的步骤,这是由于我们的一阶动量和二阶动量计算都是使用的移动平均,而移动平均存在估计偏差,需要进行修正,而修正则需要使用到该变量。
然后,我们需要计算 batch 样本的梯度,当然梯度的话要理解为多个分量的梯度。我们后面的计算都需要根据梯度来进行计算。
接着,我们开始累积一阶动量、二阶动量(平方),并对其进行偏差修正。下面公式中,\(m_t\) 表示正常使用移动平均计算出的一阶动量,\(\hat{m}_t\) 表示进行偏差修正之后得到的一阶动量。\(v_t\) 表示累积的二阶动量,同理 \(\hat{v}_t\) 表示进行偏差修正之后的二阶动量。
最后,我们前面说过,Adam 优化器结合了 RMSProp 和 Momentum 的优点,这一点可以从下面的参数更新的公式看到。
并且,大家也可以发现,一阶动量如果大的话,则沿着某一方向的更新幅度就会很大。反之,则更新较小。而二阶动量则影响了学习率的变化,如果其值越大,则学习率就越小。我们也可以把上面的公式换个写法,从另外一个角度去理解:
分子是历史的梯度的均值估计,分母则是历史梯度的方差,也可以理解为历史梯度的离散程度估计。均值/方差可以理解为从整个历史中梯度的变化情况,来估计当前应该优化的方向。即:参考历史梯度的变化来对指导当前往那个方向走,因为使用的是移动平均,也是一种立足于当前以及以前对未来的走向的一种较为合理的估计,可能能够加快模型的收敛。
2. PyTorch 源码实现
def adam(params: List[Tensor], grads: List[Tensor], exp_avgs: List[Tensor], exp_avg_sqs: List[Tensor], max_exp_avg_sqs: List[Tensor], state_steps: List[int], *, amsgrad: bool, beta1: float, beta2: float, lr: float, weight_decay: float, eps: float): for i, param in enumerate(params): # 获得当前参数的梯度 grad = grads[i] # 获得上一个时刻累积的一阶动量 exp_avg = exp_avgs[i] # 获得上一个时刻累积的二阶动量 exp_avg_sq = exp_avg_sqs[i] # 获得当前的时刻表示 t step = state_steps[i] # 用于后面对一阶、二阶动量的偏差修正 bias_correction1 = 1 - beta1 ** step bias_correction2 = 1 - beta2 ** step if weight_decay != 0: grad = grad.add(param, alpha=weight_decay) # 累计当前的梯度值到一阶动量中 exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1) # 累计当前的梯度值到二阶动量中 exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2) if amsgrad: # Maintains the maximum of all 2nd moment running avg. till now torch.maximum(max_exp_avg_sqs[i], exp_avg_sq, out=max_exp_avg_sqs[i]) # Use the max. for normalizing running avg. of gradient denom = (max_exp_avg_sqs[i].sqrt() / math.sqrt(bias_correction2)).add_(eps) else: # 对二阶动量进行偏差修正,并开根号 denom = (exp_avg_sq.sqrt() / math.sqrt(bias_correction2)).add_(eps) # 下面两行可以看做:-lr * exp_avg/ bias_correction1 / denom # exp_avg/ bias_correction1 表示对一阶动量的偏差修正 # -lr / denom 可以看做通过二阶动量来调整学习率 step_size = lr / bias_correction1 param.addcdiv_(exp_avg, denom, value=-step_size)
3. PyTorch 计算演示
- 初始参数:1
- 梯度值:2
- 学习率:0.1
- 动量调节系数:β1 = 0.9,β2 = 0.8,β1 对应的是一阶动量,β2 对应的是二阶动量
- 小常数 EPS = 1e-8
import torch import torch.optim as optim if __name__ == '__main__': # 构造初始参数 param = torch.tensor([1], dtype=torch.float32) # 设置梯度值 param.grad = torch.tensor([2], dtype=torch.float32) # 使用 RMSProp 优化器 optimizer = optim.Adam([param], lr=0.1, eps=1e-8, betas=(0.9, 0.8)) # 第一次累计梯度平方 optimizer.step() print(optimizer.state) # 第二次累计梯度平方 optimizer.step() print(optimizer.state) # 第三次累计梯度平方 optimizer.step() print(optimizer.state)
代码中 Adam 的 betas 第一项相当于 momentum 的调节系数,第二项相当于 RMSProp 累计梯度时的调节系数。程序的运行结果如下:
defaultdict(<class 'dict'>, {tensor([0.9000]): {'step': 1, 'exp_avg': tensor([0.2000]), 'exp_avg_sq': tensor([0.8000])}}) defaultdict(<class 'dict'>, {tensor([0.8000]): {'step': 2, 'exp_avg': tensor([0.3800]), 'exp_avg_sq': tensor([1.4400])}}) defaultdict(<class 'dict'>, {tensor([0.7000]): {'step': 3, 'exp_avg': tensor([0.5420]), 'exp_avg_sq': tensor([1.9520])}})
大家可以根据前面的公式,来手动计算下,就可以得到上面程序的输出结果。