• 神经网络之优化算法


    摘要

    本文概述了常见的梯度下降优化算法的不同变种,分析了初始化在优化过程中的重要性以及如何初始化,最后列举出不同优化算法的具体公式,计算过程。

    优化概述

    下面概述一下常见的优化算法,优化算法的核心是梯度下降,不同优化算法改进的地方在于梯度的方向和大小。可以将优化算法粗分为两大类,一类是改变方向的 Momentum,一类是改变学习率即梯度大小的 adagrad,最常用的 Adam 结合了这两类的优点。

    • SGD:梯度下降。这里有几个概念需要辨析:GD 使用全部的样本累积梯度,用累积的梯度更新权值;SGD 每次用一个样本,计算梯度,更新权值;mini-batch SGD,使用一个 batch 的样本累积梯度,更新权值,一般 SGD 的实现用的就是一个 batch 来更新权值,而不是一个样本。
    • Momentum:保存一个称之为动量的变量,不断累加梯度到动量上,最后动量更新权值。我们可以将梯度视为力,如果每一步都朝着同个方向前进,那么累加的力就不会抵消,还会因为累加的效果可以在合力方向前进得更快,如果梯度某个方向上发生了震荡,即力的方向经常发生改变,那么力会不断抵消,因为在累加,所以这一步可能和上一步的力发生了抵消,在震荡的方向上将改变的少。可以阅读 [5] 相关章节,用了一个小球的例子,挺形象的。
    • Nesterov momentum:和 Momentum 差不多,区别在于,累加的梯度计算。Momentum 使用的梯度是当前的梯度,而 Nesterov 使用的是“假想下一步位置的梯度” ,先用动量更新权值,然后计算新的位置,在新的位置上计算梯度,最后使用这个梯度去更新原来的动量,再用更新后的动量更新权值。[1]
    • adagrad:动态调整学习率,累加梯度的平方和,再用这个值计算学习率,累加的梯度越大,学习率就越小,因此 adagrad 的问题是,学习到后面,学习率越来越小,直接学不动了。
    • RMSProp:改进了 adagrad 的问题,RMSProp 不再是简简单单直接累加梯度,而是加权求和,权值之和为 1,设置旧的累积梯度为 (eta),新的梯度为 (1 - eta)。 [2]
    • adadelta:同样是为了改进 adagrad 的问题,[3] 给出了详细的公式,adadelta 在 RMSProp 的基础上,改进了学习率的计算,这个方法甚至不需要手动设置学习率,在 pytorch 的实现中,学习率是可选的,默认是 1。
    • Adam:最常用的优化器了,结合了 Momentum 和 adagrad 的思想,计算动量和累加梯度的平方和,接着就是一顿算,根据动量算方向和大小,根据累加梯度的平方和算学习率。
    • Yogi:改进 Adam 的问题,在累加梯度的平方时,这个梯度可能很大(blows up),使到 Adam 不能收敛(even in convex setting),结合公式看一下就知道为什么了,因为这个梯度大,学习率也大,后面看公式就知道了。Yogi 改进的点在于累加梯度的平方和那个公式。链接 [4] 的公式很详细。动量和累加梯度有一个初始值,Yogi 还建议了使用一个 batch 来设置初始值。

    关于学习率的讨论

    学习率太大,不能收敛;学习率太小,学得慢或者陷入局部最小。学习率衰减,如果学习率一直都保持那么大,会在最小值周围震荡,而到达不了最小值。

    初始化

    权值初始化。[5] 这本书总结了一个最佳实践:

    当激活函数为 sigmoid 或者 tanh 等 S 型曲线函数时,权重初始化使用 Xaiver 初始值。
    当激活函数使用 ReLU 时,权重初始化使用 He 初始值;

    如果不用这两种方法,还可以使用正态分布来初始化。不能使用一个常量来初始化所有的权值,因为如果使用了常量,那么每一层节点的输出都是一样的,最后一层每个节点输出也一样。这导致了梯度也是一样的,之后每次更新权值也一样,这样之后,不管怎么更新,整个神经网络权值还是一个量。

    为什么要进行 Xaiver 初始化、He 初始化?一个原因是解决梯度消失的问题,另一个是表达力受限的问题,很多节点都输出相同的值,那么可以由一个神经元表达同样的事情。Xaiver 和 He 初始化,本质还是正态分布,只不过使用了不同的标准差。Xaiver 的标准差取决于前一层的节点数,He 还要再乘个 2。[6] 的讨论之处,权值初始化的问题,Batch Normalization 已经很好地解决了。

    正态分布初始化的问题

    下面简要分析一下正态分布初始化存在的问题,具体看 [5] 的 P178 ~ P182,[5] 将 “正态分布” 的数据在一个 “正态分布初始化” 的权值的 5 层 MLP 网络上进行了前向传播。

    • 标准差为 1,输出偏向 0 或 1,因为使用的是 Sigmoid,在输出偏向 0 或 1 时,梯度很小,想象一下 Sigmoid 的图像,如果输出接近 0 或 1,曲线是不是很平缓呢。梯度在反向传播的过程中,经过连乘,就没有了,即存在梯度消失的问题。

    • 标准差为 0.01,输出全部都集中在 0.5 附近,输出太集中,存在的问题是表达力受限,因为多个节点输出都是相同的值。

    Xaiver 初始化存在的问题

    Xaiver 在使用 ReLU 作为激活函数的场景下,存在一些问题。输出值偏向 0,在反向传播的时候,梯度也会偏向 0,随着层的加深,会出现梯度消失的问题。

    Q: 为什么输出值偏小,在反向传播的时候,梯度也会偏向 0 呢?

    我们可以假设有这样一个网络,输入 x,中间有两层。

    [a_1 = ReLU(W_1 x + b_1) ]

    [y = W_2 a_1 + b_2 ]

    我们可以计算 (W_1) 的梯度,下面的公式定性地理解,本渣渣意识到矩阵并不能随便用链式法则,不过这里为了定性分析,还是可以的,你也可以将 (W_1) 视为某一个具体的权值变量,而不是矩阵。

    [frac{partial L}{partial W_1} = frac{partial L}{partial a_1} frac{partial ReLU(W_1 x + b_1)}{partial (W_1 x + b_1)} frac{partial (W_1 x + b_1)}{partial W_1} ]

    其中最后一项微分可以得到 (x),即上一层的输出。如果上一层的输出小了,那么梯度也会小,即偏向 0。叠多几层,梯度就消失了。

    优化的公式

    当前阶段,本渣渣只是知其然,不知所以然。天知道这些公式背后隐藏着多少的道理,为什么要用动量,为什么要累积梯度的平方?不过,虽然不知道为什么,但是至少要求自己知道怎么算。

    这里先声明几个符号:

    • (W),表示网络的权值。
    • (g_t),表示第 t 步的梯度,一般公式是 (frac{partial L}{partial W}),如果是 mini-batch,那么就是一个累积的梯度。
    • (v_t),表示第 t 步的动量,一般是梯度的累积。更新公式为 $v_t = alpha v_{t-1} - eta g_t $
    • (h_t),表示第 t 步的梯度平方和的累积。一般的更新公式为,(h_t = h_{t-1} + g_t^2)

    具体的计算,最好结合代码 [7] 来看:后面很多开方、除法操作,都是对矩阵的逐元素进行。

    SGD

    梯度下降,权值朝着梯度的反方向去。

    [W leftarrow W - eta g_t ]

    Momentum

    [v_t = alpha v_{t-1} - eta g_t ]

    [W leftarrow W + v_t ]

    Nesterov momentum

    先用上一步的动量,更新权值。

    [hat{W} leftarrow W + v_t ]

    使用 (hat{W}) 进行一次前向传播,重新计算梯度 (g_t)

    更新动量:

    [v_t = alpha v_{t-1} - eta g_t ]

    再更新权值:

    [W leftarrow W + v_t ]

    adagrad

    看到了下面公式,就知道为什么 adagrad 学久了就学不动了吧,梯度累积的越多,越学不动。看公式理解 adagrad,你会发现开根号的操作,这个操作应用到张量上的意义是什么呢?其实开根号在这里是逐元素操作,即对张量中的每个元素开根号,于是对应的权重学习率就不同了。这正是 adagrad 和以往的不同之处,每个参数都一个学习率,而不是所有参数都使用一个学习率。

    [h_t leftarrow h_{t-1} + g_t^2 ]

    [W leftarrow W - eta frac{1}{sqrt{h}} g_t ]

    RMSProp

    改进梯度累积的公式。

    [h_t leftarrow eta h_{t-1} + (1 - eta) g_t^2 ]

    [W leftarrow W - eta frac{1}{sqrt{h}} g_t ]

    adadelta

    改进了梯度的计算,注意到 adadelta 中的 delta 了吧,下面的公式真有 (Delta),至于如何计算,真的建议看 [7] 的代码,简单,清晰明了。再说一次,以下矩阵的开方和除法都是逐元素操作。

    [h_t leftarrow eta h_{t-1} + (1 - eta) g_t^2 ]

    [g_t' leftarrow frac{sqrt{Delta W_{t-1} + epsilon}}{sqrt{h_t + epsilon}} odot g_t ]

    [W leftarrow W - eta frac{1}{sqrt{h}} g_t' ]

    [Delta W_{t} leftarrow ho Delta W_{t-1} + (1 - ho) g_t'^2 ]

    Adam

    结合了 Momentum 和 adagrad 的思想

    [v_t leftarrow eta_1 v_{t-1} + (1 - eta_1) g_t ]

    [h_t leftarrow eta_2 h_{t-1} + (1 - eta_2) g_t^2 ]

    [hat{v_t} leftarrow frac{v_t}{1 - eta_1^t} ]

    [hat{h_t} leftarrow frac{h_t}{1 - eta_2^t} ]

    [g_t'leftarrow frac{eta hat{v_t}}{sqrt{hat{h_t}} + epsilon} ]

    [W leftarrow W - g_t' ]

    Yogi

    在累加梯度的平方时,这个梯度可能很大(blows up),使到 Adam 不能收敛(even in convex setting),因为这个梯度大,学习率也变大,结合上面的公式看看。

    于是有人提出了下面的公式进行改进

    [h_t leftarrow h_{t-1} - (1 - eta_2) (g_t^2 - h_{t-1}) ]

    [4] 提到了上述改进的问题:

    Whenever (g_t^2) has high variance or updates are sparse, (h_t) might forget past values too quickly.

    翻译翻译,当 (g_t^2) 方差较大的时候,(h_t) 前面的值可能很快就被忘记了,意思就是 (h_t) 前面的值在 (h_t) 中所占的比重减少了。为了解决这个问题,Yogi 提出了如下的公式。

    [h_t leftarrow h_{t-1} - (1 - eta_2) g_t^2 odot sign(g_t^2 - h_{t-1}) ]

    经过这个改进之后,(h_t) 可以变大,而对应到 Adam 中 (g_t') 的学习率会变小,这解决了 Adam 存在的问题。

    总结

    如上总结了常用的优化算法,本渣渣知其然,不知所以然,背后有许多为什么等待着探索。当前阶段,搞清楚每一种优化算法的优缺点,如何计算即可。此外,权值的初始化是一个比较重要的细节,需要稍加留意,在训练太慢的时候,可以检查一下。

    参考链接

    [1] https://zhuanlan.zhihu.com/p/73264637
    [2] https://towardsdatascience.com/understanding-rmsprop-faster-neural-network-learning-62e116fcf29a
    [3] https://d2l.ai/chapter_optimization/adadelta.html
    [4] https://d2l.ai/chapter_optimization/adam.html
    [5] 深度学习入门:斋藤康毅
    [6] https://www.quora.com/Does-Xavier-initialization-work-well-when-the-activation-function-is-a-ReLu
    [7] https://github.com/Lasagne/Lasagne/blob/master/lasagne/updates.py

  • 相关阅读:
    85--spring cloud (Ribbon-Eureka注册中心)
    85--spring cloud 入门(springcloud简介)
    84--spring cloud 入门(微服务注册中心介绍)
    83--spring cloud 入门(Eureka注册中心)
    82--JT项目20(订单模块实现/ThreadLocal本地线程变量/Quartz框架)
    81--JT项目19(商品购物车/详情/用户退出)
    80--JT项目18(Dubbo负载均衡/单点登录/注册业务)
    Ajax中post与get的区别
    Process
    Java实现CURL,与把字符串结果写到json文件
  • 原文地址:https://www.cnblogs.com/zzk0/p/14988049.html
Copyright © 2020-2023  润新知