Adam 优化器原理

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. 初始参数:1
  2. 梯度值:2
  3. 学习率:0.1
  4. 动量调节系数:β1 = 0.9,β2 = 0.8,β1 对应的是一阶动量,β2 对应的是二阶动量
  5. 小常数 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])}})

大家可以根据前面的公式,来手动计算下,就可以得到上面程序的输出结果。

未经允许不得转载:一亩三分地 » Adam 优化器原理