优化算法

  1. SGD 就是随机梯度下降
  2. momentum 动量加速,在SGD函数里指定 momentum 的值即可
    1. 指数加权移动平均
    2. 由指数加权移动平均理解动量法
  • AdaGrad 算法
  • RMSprop 指定参数 alpha
  • AdaDelta 算法
  • Adam 参数 betas = (0.9, 0.99)
  • 训练一个复杂的深度学习模型可能需要数小时、数日,甚至数周时间,而优化算法的表现直接影响模型的训练效率;另一方面,理解各种优化算法的原理以及其中超参数的意义将有助于我们更有针对性地调参,从而使深度学习模型表现更好。
    虽然优化为深度学习提供了最小化损失函数的方法,但本质上,优化与深度学习的目标是有区别的。
    首先区分训练误差和泛化误差。 由于优化算法的目标函数通常是一个基于训练数据集的损失函数,优化的目标在于降低训练误差。 而深度学习的目标在于降低泛化误差。为了降低泛化误差,除了使用优化算法降低训练误差以外,还需要注意应对过拟合。

    以下论述关注优化算法在最小化目标函数上的表现,而不关注模型的泛化误差。

    SGD 就是随机梯度下降

    鞍点和局部最优的理解

    随机梯度的计算公式:$X = X - \eta \nabla f(x)$
    泰勒展开:$f(x + \epsilon) = f(x) + \frac {f’(x)}{1}\epsilon + o(\epsilon^2)$
    令 $\epsilon = - \eta f’(x)$
    得 $f(x - \eta f’(x)) = f(x) - \eta f’(x)^2 + o(\epsilon^2)$
    $\eta$ 为正数,假设余项很小舍去,则 f(x) 会越来越小
    前提条件,观察余项
    所以学习率不能太大,一阶导数也不能太大

    # scratch version
    def sgd(params, hyperparams):
        for p in params:
            p[:] -= hyperparams['lr'] * p.grad
    
    # pytorch version
    opt_SGD = torch.optim.SGD(net_SGD.parameters(), lr=LR)
    

    momentum 动量加速,在SGD函数里指定 momentum 的值即可

    目标函数有关自变量的梯度代表了目标函数在自变量当前位置下降最快的方向。因此,梯度下降也叫作最陡下降(steepest descent)。在每次迭代中,梯度下降根据自变量当前位置,沿着当前位置的梯度更新自变量。然而,如果自变量的迭代方向仅仅取决于自变量当前位置,这可能会带来一些问题。
    动量法的提出是为了解决梯度下降的上述问题。
    $$
    \begin{aligned}
    \boldsymbol{v}_t &\leftarrow \gamma \boldsymbol{v}_{t-1} + \eta_t \boldsymbol{g}_t, \
    \boldsymbol{x}_t &\leftarrow \boldsymbol{x}_{t-1} - \boldsymbol{v}_t,
    \end{aligned}
    $$

    指数加权移动平均

    $v_t = \gamma v_{t-1} + \eta_t grad$

    为了从数学上理解动量法,让我们先解释一下指数加权移动平均(exponentially weighted moving average)。给定超参数$0 \leq \gamma < 1$,当前时间步$t$的变量$y_t$是上一时间步$t-1$的变量$y_{t-1}$和当前时间步另一变量$x_t$的线性组合:

    $$y_t = \gamma y_{t-1} + (1-\gamma) x_t.$$

    $$
    y^{(20)} = 0.05x^{(20)} + 0.95y^{(19)}
    = 0.05x^{(20)} + 0.05*0.95x^{(19)} + 0.95^2 y^{(18)}

    $$

    $x^{\frac {1}{1-x}} \approx \frac {1}{e}$
    $0.95^{20} \approx \frac {1}{e} = 0.3678$
    代表对过去的 20 计算 EMA

    我们可以对$y_t$展开:

    $$
    \begin{aligned}
    y_t &= (1-\gamma) x_t + \gamma y_{t-1}\
    &= (1-\gamma)x_t + (1-\gamma) \cdot \gamma x_{t-1} + \gamma^2y_{t-2}\
    &= (1-\gamma)x_t + (1-\gamma) \cdot \gamma x_{t-1} + (1-\gamma) \cdot \gamma^2x_{t-2} + \gamma^3y_{t-3}\
    &\ldots
    \end{aligned}
    $$

    令$n = 1/(1-\gamma)$,那么 $\left(1-1/n\right)^n = \gamma^{1/(1-\gamma)}$。因为

    $$ \lim_{n \rightarrow \infty} \left(1-\frac{1}{n}\right)^n = \exp(-1) \approx 0.3679,$$

    所以当$\gamma \rightarrow 1$时,$\gamma^{1/(1-\gamma)}=\exp(-1)$,如$0.95^{20} \approx \exp(-1)$。如果把$\exp(-1)$当作一个比较小的数,我们可以在近似中忽略所有含$\gamma^{1/(1-\gamma)}$和比$\gamma^{1/(1-\gamma)}$更高阶的系数的项。例如,当$\gamma=0.95$时,

    $$y_t \approx 0.05 \sum_{i=0}^{19} 0.95^i x_{t-i}.$$

    因此,在实际中,我们常常将$y_t$看作是对最近$1/(1-\gamma)$个时间步的$x_t$值的加权平均。例如,当$\gamma = 0.95$时,$y_t$可以被看作对最近20个时间步的$x_t$值的加权平均;当$\gamma = 0.9$时,$y_t$可以看作是对最近10个时间步的$x_t$值的加权平均。而且,离当前时间步$t$越近的$x_t$值获得的权重越大(越接近1)。

    features, labels = d2l.get_data_ch7()
    
    def init_momentum_states():
        v_w = nd.zeros((features.shape[1], 1))
        v_b = nd.zeros(1)
        return (v_w, v_b)
    
    def sgd_momentum(params, states, hyperparams):
        for p, v in zip(params, states):
            v[:] = hyperparams['momentum'] * v + hyperparams['lr'] * p.grad
            p[:] -= v
    

    由指数加权移动平均理解动量法

    现在,我们对动量法的速度变量做变形:

    $$\boldsymbol{v}_t \leftarrow \gamma \boldsymbol{v}_{t-1} + (1 - \gamma) \left(\frac{\eta_t}{1 - \gamma} \boldsymbol{g}_t\right). $$

    由指数加权移动平均的形式可得,速度变量$\boldsymbol{v}_t$实际上对序列${\eta_{t-i}\boldsymbol{g}_{t-i} /(1-\gamma):i=0,\ldots,1/(1-\gamma)-1}$做了指数加权移动平均。换句话说,相比于小批量随机梯度下降,动量法在每个时间步的自变量更新量近似于将前者对应的最近$1/(1-\gamma)$个时间步的更新量做了指数加权移动平均后再除以$1-\gamma$。所以,在动量法中,自变量在各个方向上的移动幅度不仅取决当前梯度,还取决于过去的各个梯度在各个方向上是否一致。在本节之前示例的优化问题中,所有梯度在水平方向上为正(向右),而在竖直方向上时正(向上)时负(向下)。这样,我们就可以使用较大的学习率,从而使自变量向最优解更快移动。

    opt_Momentum = torch.optim.SGD(net_Momentum.parameters(), lr=LR, momentum=0.8)
    

    AdaGrad 算法

    AdaGrad算法会使用一个小批量随机梯度$\boldsymbol{g}_t$按元素平方的累加变量$\boldsymbol{s}_t$。在时间步0,AdaGrad将$\boldsymbol{s}_0$中每个元素初始化为0。在时间步$t$,首先将小批量随机梯度$\boldsymbol{g}_t$按元素平方后累加到变量$\boldsymbol{s}_t$:

    $$\boldsymbol{s}_t \leftarrow \boldsymbol{s}_{t-1} + \boldsymbol{g}_t \odot \boldsymbol{g}_t,$$

    其中$\odot$是按元素相乘。接着,我们将目标函数自变量中每个元素的学习率通过按元素运算重新调整一下:

    $$\boldsymbol{x}_t \leftarrow \boldsymbol{x}_{t-1} - \frac{\eta}{\sqrt{\boldsymbol{s}_t + \epsilon}} \odot \boldsymbol{g}_t,$$

    其中$\eta$是学习率,$\epsilon$是为了维持数值稳定性而添加的常数,如$10^{-6}$。这里开方、除法和乘法的运算都是按元素运算的。这些按元素运算使得目标函数自变量中每个元素都分别拥有自己的学习率。

    features, labels = d2l.get_data_ch7()
    
    def init_adagrad_states():
        s_w = nd.zeros((features.shape[1], 1))
        s_b = nd.zeros(1)
        return (s_w, s_b)
    
    def adagrad(params, states, hyperparams):
        eps = 1e-6
        for p, s in zip(params, states):
            s[:] += p.grad.square()
            p[:] -= hyperparams['lr'] * p.grad / (s + eps).sqrt()
    

    RMSprop 指定参数 alpha

    我们在动量法里介绍过指数加权移动平均。不同于AdaGrad算法里状态变量$\boldsymbol{s}_t$是截至时间步$t$所有小批量随机梯度$\boldsymbol{g}_t$按元素平方和,RMSProp算法将这些梯度按元素平方做指数加权移动平均。具体来说,给定超参数$0 \leq \gamma < 1$,RMSProp算法在时间步$t>0$计算

    $$\boldsymbol{s}_t \leftarrow \gamma \boldsymbol{s}_{t-1} + (1 - \gamma) \boldsymbol{g}_t \odot \boldsymbol{g}_t. $$

    和AdaGrad算法一样,RMSProp算法将目标函数自变量中每个元素的学习率通过按元素运算重新调整,然后更新自变量

    $$\boldsymbol{x}_t \leftarrow \boldsymbol{x}_{t-1} - \frac{\eta}{\sqrt{\boldsymbol{s}_t + \epsilon}} \odot \boldsymbol{g}_t, $$

    其中$\eta$是学习率,$\epsilon$是为了维持数值稳定性而添加的常数,如$10^{-6}$。因为RMSProp算法的状态变量$\boldsymbol{s}_t$是对平方项$\boldsymbol{g}_t \odot \boldsymbol{g}_t$的指数加权移动平均,所以可以看作是最近$1/(1-\gamma)$个时间步的小批量随机梯度平方项的加权平均。如此一来,自变量每个元素的学习率在迭代过程中就不再一直降低(或不变)。

    照例,让我们先观察RMSProp算法对目标函数$f(\boldsymbol{x})=0.1x_1^2+2x_2^2$中自变量的迭代轨迹。回忆在“AdaGrad算法”一节使用的学习率为0.4的AdaGrad算法,自变量在迭代后期的移动幅度较小。但在同样的学习率下,RMSProp算法可以更快逼近最优解。
    opt_RMSprop = torch.optim.RMSprop(net_RMSprop.parameters(), lr=LR, alpha=0.9)

    def rmsprop_2d(x1, x2, s1, s2):
        g1, g2, eps = 0.2 * x1, 4 * x2, 1e-6
        s1 = gamma * s1 + (1 - gamma) * g1 ** 2
        s2 = gamma * s2 + (1 - gamma) * g2 ** 2
        x1 -= eta / math.sqrt(s1 + eps) * g1
        x2 -= eta / math.sqrt(s2 + eps) * g2
        return x1, x2, s1, s2
    
    def f_2d(x1, x2):
        return 0.1 * x1 ** 2 + 2 * x2 ** 2
    
    eta, gamma = 0.4, 0.9
    d2l.show_trace_2d(f_2d, d2l.train_2d(rmsprop_2d))
    

    AdaDelta 算法

    AdaDelta算法也像RMSProp算法一样,使用了小批量随机梯度$\boldsymbol{g}_t$按元素平方的指数加权移动平均变量$\boldsymbol{s}_t$。在时间步0,它的所有元素被初始化为0。给定超参数$0 \leq \rho < 1$(对应RMSProp算法中的$\gamma$),在时间步$t>0$,同RMSProp算法一样计算

    $$\boldsymbol{s}_t \leftarrow \rho \boldsymbol{s}_{t-1} + (1 - \rho) \boldsymbol{g}_t \odot \boldsymbol{g}_t. $$

    与RMSProp算法不同的是,AdaDelta算法还维护一个额外的状态变量$\Delta\boldsymbol{x}t$,其元素同样在时间步0时被初始化为0。我们使用$\Delta\boldsymbol{x}{t-1}$来计算自变量的变化量:

    $$ \boldsymbol{g}_t’ \leftarrow \sqrt{\frac{\Delta\boldsymbol{x}_{t-1} + \epsilon}{\boldsymbol{s}_t + \epsilon}} \odot \boldsymbol{g}_t, $$

    其中$\epsilon$是为了维持数值稳定性而添加的常数,如$10^{-5}$。接着更新自变量:

    $$\boldsymbol{x}_t \leftarrow \boldsymbol{x}_{t-1} - \boldsymbol{g}’_t. $$

    最后,我们使用$\Delta\boldsymbol{x}_t$来记录自变量变化量$\boldsymbol{g}’_t$按元素平方的指数加权移动平均:

    $$\Delta\boldsymbol{x}_t \leftarrow \rho \Delta\boldsymbol{x}_{t-1} + (1 - \rho) \boldsymbol{g}’_t \odot \boldsymbol{g}’_t. $$

    可以看到,如不考虑$\epsilon$的影响,AdaDelta算法与RMSProp算法的不同之处在于使用$\sqrt{\Delta\boldsymbol{x}_{t-1}}$来替代超参数$\eta$。

    features, labels = d2l.get_data_ch7()
    
    def init_adadelta_states():
        s_w, s_b = nd.zeros((features.shape[1], 1)), nd.zeros(1)
        delta_w, delta_b = nd.zeros((features.shape[1], 1)), nd.zeros(1)
        return ((s_w, delta_w), (s_b, delta_b))
    
    def adadelta(params, states, hyperparams):
        rho, eps = hyperparams['rho'], 1e-5
        for p, (s, delta) in zip(params, states):
            s[:] = rho * s + (1 - rho) * p.grad.square()
            g = ((delta + eps).sqrt() / (s + eps).sqrt()) * p.grad
            p[:] -= g
            delta[:] = rho * delta + (1 - rho) * g * g
    

    Adam 参数 betas = (0.9, 0.99)

    Adam 结合了两个想法来改善收敛性:每个参数更新可加快收敛速度;动量可避免卡在鞍点上。
    Adam 算法使用了动量变量$\boldsymbol{v}_t$和RMSProp算法中小批量随机梯度按元素平方的指数加权移动平均变量$\boldsymbol{s}_t$,并在时间步0将它们中每个元素初始化为0。给定超参数$0 \leq \beta_1 < 1$(算法作者建议设为0.9),时间步$t$的动量变量$\boldsymbol{v}_t$即小批量随机梯度$\boldsymbol{g}_t$的指数加权移动平均:

    $$\boldsymbol{v}_t \leftarrow \beta_1 \boldsymbol{v}_{t-1} + (1 - \beta_1) \boldsymbol{g}_t. $$

    和RMSProp算法中一样,给定超参数$0 \leq \beta_2 < 1$(算法作者建议设为0.999),
    将小批量随机梯度按元素平方后的项$\boldsymbol{g}_t \odot \boldsymbol{g}_t$做指数加权移动平均得到$\boldsymbol{s}_t$:

    $$\boldsymbol{s}_t \leftarrow \beta_2 \boldsymbol{s}_{t-1} + (1 - \beta_2) \boldsymbol{g}_t \odot \boldsymbol{g}_t. $$

    由于我们将$\boldsymbol{v}_0$和$\boldsymbol{s}_0$中的元素都初始化为0,
    在时间步$t$我们得到$\boldsymbol{v}_t = (1-\beta_1) \sum_{i=1}^t \beta_1^{t-i} \boldsymbol{g}_i$。将过去各时间步小批量随机梯度的权值相加,得到 $(1-\beta_1) \sum_{i=1}^t \beta_1^{t-i} = 1 - \beta_1^t$。需要注意的是,当$t$较小时,过去各时间步小批量随机梯度权值之和会较小。例如,当$\beta_1 = 0.9$时,$\boldsymbol{v}_1 = 0.1\boldsymbol{g}_1$。为了消除这样的影响,对于任意时间步$t$,我们可以将$\boldsymbol{v}_t$再除以$1 - \beta_1^t$,从而使过去各时间步小批量随机梯度权值之和为1。这也叫作偏差修正。在Adam算法中,我们对变量$\boldsymbol{v}_t$和$\boldsymbol{s}_t$均作偏差修正:

    $$\hat{\boldsymbol{v}}_t \leftarrow \frac{\boldsymbol{v}_t}{1 - \beta_1^t}, $$

    $$\hat{\boldsymbol{s}}_t \leftarrow \frac{\boldsymbol{s}_t}{1 - \beta_2^t}. $$

    接下来,Adam算法使用以上偏差修正后的变量$\hat{\boldsymbol{v}}_t$和$\hat{\boldsymbol{s}}_t$,将模型参数中每个元素的学习率通过按元素运算重新调整:

    $$\boldsymbol{g}_t’ \leftarrow \frac{\eta \hat{\boldsymbol{v}}_t}{\sqrt{\hat{\boldsymbol{s}}_t} + \epsilon},$$

    其中$\eta$是学习率,$\epsilon$是为了维持数值稳定性而添加的常数,如$10^{-8}$。和AdaGrad算法、RMSProp算法以及AdaDelta算法一样,目标函数自变量中每个元素都分别拥有自己的学习率。最后,使用$\boldsymbol{g}_t’$迭代自变量:

    $$\boldsymbol{x}_t \leftarrow \boldsymbol{x}_{t-1} - \boldsymbol{g}_t’. $$

    # scratch version
    features, labels = d2l.get_data_ch7()
    
    def init_adam_states():
        v_w, v_b = nd.zeros((features.shape[1], 1)), nd.zeros(1)
        s_w, s_b = nd.zeros((features.shape[1], 1)), nd.zeros(1)
        return ((v_w, s_w), (v_b, s_b))
    
    def adam(params, states, hyperparams):
        beta1, beta2, eps = 0.9, 0.999, 1e-6
        for p, (v, s) in zip(params, states):
            v[:] = beta1 * v + (1 - beta1) * p.grad
            s[:] = beta2 * s + (1 - beta2) * p.grad.square()
            v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
            s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
            p[:] -= hyperparams['lr'] * v_bias_corr / (s_bias_corr.sqrt() + eps)
        hyperparams['t'] += 1
    # pytorch version
    opt_Adam = torch.optim.Adam(net_Adam.parameters(), lr=LR, betas=(0.9, 0.99))
    
    • 当使用小批量梯度下降时,为什么打乱数据很重要?
      如果不打乱数据的顺序,那么假设我们训练一个神经网络分类器,且有两个类别:A和B,那么各个epoch中的所有小批量都会完全相同,这会导致收敛速度变慢,甚至导致神经网络对数据的顺序产生倾向性。
    • 说明为何L2正则化可以解释为一种权重衰减。
      $w = w -grad(C)(w) — 2cw = (1–2c)w — grad(C)(w)$
      其中,1-2c < 0,则 w 乘数因子 < 1,所以 w 会变小。

    请多多指教。

    文章标题:优化算法

    本文作者:顺强

    发布时间:2020-03-23, 23:59:00

    原始链接:http://shunqiang.ml/cnn-optimization-algorithm/

    版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

    目录
    ×

    喜欢就点赞,疼爱就打赏