聊聊神经网络中的梯度法

简介

在最优化问题求解的过程中,使用最多的迭代方法就是梯度法。比如针对最优化问题
m i n x f ( x ) min_{\mathbf x} f(\mathbf x) minxf(x)
采用的梯度迭代的步骤为
x = x − η ∇ f ( x ) \mathbf x = \mathbf x - \eta \nabla f(\mathbf x) x=xηf(x)
梯度法的优势在于

  • 抗"噪声"。即使每次求解的梯度并不是真实的目标函数的“梯度”,但只要保证求得的梯度“期望值”=目标函数的“梯度”,只要选择合适的步长,就可以收敛。这也就是神经网络里面使用最多的“stochastic gradient method”。因为神经网络要对所有的训练集的Loss总和求最小值,因此“真正”的梯度法应该是将所有的训练集作为一个batch,直接塞到神经网络里做梯度迭代收敛,但是由于训练集往往较大,很多情况是超过了计算机的内存的。因此只能将训练集分成多个batch,依次送入到神经网路做迭代。但只要保证我们训练集的数据是独立同分布的,那么采用多个batch总能有办法让神经网络收敛。这也是梯度法相对于其它迭代法的优势所在。
  • 对目标函数要求仅需要一阶可导就可以,甚至只需要保证目标函数是连续函数就行。因为梯度法仅需要对目标函数进行一阶求导,因此对目标函数的“光滑性”要求较低。甚至于目标函数都不需要一阶可导。比如以下函数
    在这里插入图片描述
    这种函数是连续函数,但是存在第一间断点,这种情况下,在间断点处,只要取左导数或右导数,都可以实现收敛的(只不过有可能收敛到不同的局部最小点)。

然而梯度法的问题是,收敛速度慢,尤其是遇到“等高线”比较“扁”的函数时,就会出现来回“震荡”的情况,如图
在这里插入图片描述
因此在实际梯度法迭代中,要考虑在调整 η \eta η,防止出现震荡的情况。在实际应用时,往往要时刻监视梯度收敛的情况,如果发现来回震荡,就得现调 η \eta η,这显然不是一个很好的做法。
这也是若干年在学术上有不少人研究梯度法的原因,就是为了能够找到一种自动调整 η \eta η的方法,能应对各种目标函数,能够提高梯度的收敛速度。

梯度法改进-牛顿法

最优化问题里,传统上用的最多的还是凸优化问题,即求解
m i n x f ( x ) s.t.  x ∈ C \begin{aligned} min_{\mathbf x} & f(\mathbf x) \\ \text{s.t. } & \mathbf x \in \mathbb{C} \end{aligned} minxs.t. f(x)xC
其中 f ( x ) f(\mathbf x) f(x)是关于 x \mathbf x x的凸函数, C \mathbb{C} C是一个凸集。在凸优化问题中,局部最小值=全局最小值,也就是在凸优化问题里,我们所求解的问题只有一个“坑”,只要顺着坡儿往里爬总能爬到坑底的。像Matlab之类的早早就集成了凸优化的工具,只要给出目标函数和约束条件,就能自动给求出来最优值。
如果 f ( x ) f(\mathbf x) f(x)是二阶可导的,用牛顿法进行迭代加速是很好使的。牛顿法的基本原理是把 x \mathbf x x所在的局部区域用二次函数进行近似,再求这个二次函数的最小值,即
x = x − ( ∇ 2 f ) − 1 ∇ f \mathbf x = \mathbf x - (\nabla^2f)^{-1}\nabla f x=x(2f)1f
然而牛顿法需要求解矩阵 ∇ 2 f \nabla^2f 2f,如果 x \mathbf x x N N N个分量,那么矩阵 ∇ 2 f \nabla^2f 2f就会有 N 2 N^2 N2个分量。所以在神经网络动辄就是上百万参数的情况下,牛顿法基本就不好使了,更何况这还是一次迭代的计算量呢。
神经网络不同于凸优化的问题在于,神经网络的Loss函数有大大小小的坑、鞍点(尤其是在高维空间,鞍点甚至会比坑更多)、悬崖峭壁,还需要有更合适的算法改进梯度法。因此,虽然梯度法本身用起来并不复杂,但就是这简单的算法仍然是前几年学术界的研究的热点。

梯度法改进-加momentum

思路是将梯度取一个历史的“滑动平均”,然后再用这个均值来更新 x \mathbf x x。具体的表述为
v = β v + ( 1 − β ) ∇ f ( x ) x = x − η v \begin{aligned} \mathbf v &= \beta \mathbf v + (1-\beta) \nabla f(\mathbf{x}) \\ \mathbf x &= \mathbf x - \eta \mathbf v \end{aligned} vx=βv+(1β)f(x)=xηv
其中 v \mathbf v v的初始值取0.
从表达式可以看到, v \mathbf v v相当于给真实的梯度做了“平滑”,可以消掉梯度中震荡较大的部分,这样实际的收敛就变成了以下的效果:
在这里插入图片描述
一般 β \beta β取0.9。这也就意味着相当于对最近的10个梯度求了平均值。

梯度法改进-RMSProp

RMSProp的表述为
s = β s + ( 1 − β ) ( ∇ f ( x ) ) 2 x = x − η ∇ f ( x ) s + ϵ \begin{aligned} \mathbf s &= \beta \mathbf s + (1-\beta) (\nabla f(\mathbf x))^2 \\ \mathbf x &= \mathbf x - \eta\frac{\nabla f(\mathbf x)}{\sqrt{\mathbf s + \mathbf {\epsilon}}} \end{aligned} sx=βs+(1β)(f(x))2=xηs+ϵ f(x)
其中第一个式子的平方是逐分量取平方,第二个式子为逐分量相除,在分母上加上一个较小的数 ϵ \epsilon ϵ是为了防止 s \mathbf s s接近于0时,算出来的数过大。
还是以前面的梯度迭代的图为例,如果 ∇ f ( x ) \nabla f(\mathbf x) f(x)在纵向震荡分量较大, s \mathbf s s就会大=》在第二个式子的迭代中,就会削减纵向震荡的分量。这样RMSProp也会取得和momentum类似的效果。
简单说下八卦,RMSProp本身并不是来自于学术论文,而是来自于2016年Coursera公开课上的一个讲义。

梯度法改进-Adam

Adam可以说是目前在神经网络中用的最多的算法了。像Tensorflow,Keras,Pytorch中都集成了Adam算法,一般的算法工程师搭好神经网络,在进行train的过程中,大多只需无脑配上Adam优化器,都会有不错的收敛效果。
Adam本身实现并不复杂,在Keras的源代码库中,Adam算上注释空行也就几十行代码,附上keras的源代码以供欣赏。

class Adam(Optimizer):
    """Adam optimizer.
    Default parameters follow those provided in the original paper.
    # Arguments
        learning_rate: float >= 0. Learning rate.
        beta_1: float, 0 < beta < 1. Generally close to 1.
        beta_2: float, 0 < beta < 1. Generally close to 1.
        amsgrad: boolean. Whether to apply the AMSGrad variant of this
            algorithm from the paper "On the Convergence of Adam and
            Beyond".
    # References
        - [Adam - A Method for Stochastic Optimization](
           https://arxiv.org/abs/1412.6980v8)
        - [On the Convergence of Adam and Beyond](
           https://openreview.net/forum?id=ryQu7f-RZ)
    """

    def __init__(self, learning_rate=0.001, beta_1=0.9, beta_2=0.999,
                 amsgrad=False, **kwargs):
        self.initial_decay = kwargs.pop('decay', 0.0)
        self.epsilon = kwargs.pop('epsilon', K.epsilon())
        learning_rate = kwargs.pop('lr', learning_rate)
        super(Adam, self).__init__(**kwargs)
        with K.name_scope(self.__class__.__name__):
            self.iterations = K.variable(0, dtype='int64', name='iterations')
            self.learning_rate = K.variable(learning_rate, name='learning_rate')
            self.beta_1 = K.variable(beta_1, name='beta_1')
            self.beta_2 = K.variable(beta_2, name='beta_2')
            self.decay = K.variable(self.initial_decay, name='decay')
        self.amsgrad = amsgrad

    @interfaces.legacy_get_updates_support
    @K.symbolic
    def get_updates(self, loss, params):
        grads = self.get_gradients(loss, params)
        self.updates = [K.update_add(self.iterations, 1)]

        lr = self.learning_rate
        if self.initial_decay > 0:
            lr = lr * (1. / (1. + self.decay * K.cast(self.iterations,
                                                      K.dtype(self.decay))))

        t = K.cast(self.iterations, K.floatx()) + 1
        lr_t = lr * (K.sqrt(1. - K.pow(self.beta_2, t)) /
                     (1. - K.pow(self.beta_1, t)))

        ms = [K.zeros(K.int_shape(p),
              dtype=K.dtype(p),
              name='m_' + str(i))
              for (i, p) in enumerate(params)]
        vs = [K.zeros(K.int_shape(p),
              dtype=K.dtype(p),
              name='v_' + str(i))
              for (i, p) in enumerate(params)]

        if self.amsgrad:
            vhats = [K.zeros(K.int_shape(p),
                     dtype=K.dtype(p),
                     name='vhat_' + str(i))
                     for (i, p) in enumerate(params)]
        else:
            vhats = [K.zeros(1, name='vhat_' + str(i))
                     for i in range(len(params))]
        self.weights = [self.iterations] + ms + vs + vhats

        for p, g, m, v, vhat in zip(params, grads, ms, vs, vhats):
            m_t = (self.beta_1 * m) + (1. - self.beta_1) * g
            v_t = (self.beta_2 * v) + (1. - self.beta_2) * K.square(g)
            if self.amsgrad:
                vhat_t = K.maximum(vhat, v_t)
                p_t = p - lr_t * m_t / (K.sqrt(vhat_t) + self.epsilon)
                self.updates.append(K.update(vhat, vhat_t))
            else:
                p_t = p - lr_t * m_t / (K.sqrt(v_t) + self.epsilon)

            self.updates.append(K.update(m, m_t))
            self.updates.append(K.update(v, v_t))
            new_p = p_t

            # Apply constraints.
            if getattr(p, 'constraint', None) is not None:
                new_p = p.constraint(new_p)

            self.updates.append(K.update(p, new_p))
        return self.updates

    def get_config(self):
        config = {'learning_rate': float(K.get_value(self.learning_rate)),
                  'beta_1': float(K.get_value(self.beta_1)),
                  'beta_2': float(K.get_value(self.beta_2)),
                  'decay': float(K.get_value(self.decay)),
                  'epsilon': self.epsilon,
                  'amsgrad': self.amsgrad}
        base_config = super(Adam, self).get_config()
        return dict(list(base_config.items()) + list(config.items()))

Adam算法是集成了上述momentum和RMSProp算法的思想,组合创新提出来的。具体表述为:
v t = β 1 v t − 1 + ( 1 − β 1 ) ∇ f ( x t − 1 ) s t = β 2 s t − 1 + ( 1 − β 2 ) ( ∇ f ( x t − 1 ) ) 2 v ^ t = v t 1 − β 1 t s ^ t = s t 1 − β 2 t x t = x t − 1 − η v ^ t s ^ t + ϵ \begin{aligned} \mathbf v_t &= \beta_1 \mathbf v_{t-1} + (1-\beta_1) \nabla f(\mathbf{x_{t-1}}) \\ \mathbf s_t &= \beta_2 \mathbf s_{t-1} + (1-\beta_2) (\nabla f(\mathbf x_{t-1}))^2 \\ \mathbf {\hat v}_t &= \frac{\mathbf v_t}{\sqrt{1-\beta_1^t}} \\ \mathbf {\hat s}_t &= \frac{\mathbf s_t}{\sqrt{1-\beta_2^t}} \\ \mathbf x_t &= \mathbf x_{t-1} - \eta \frac{\mathbf {\hat v}_t}{\sqrt{\mathbf {\hat s}_t + \mathbf {\epsilon}}} \end{aligned} vtstv^ts^txt=β1vt1+(1β1)f(xt1)=β2st1+(1β2)(f(xt1))2=1β1t vt=1β2t st=xt1ηs^t+ϵ v^t
可见除了归一化的操作外,其它基本就是把momentum和RMSProp的算法进行了组装,但就是这个创新,大大改善了神经网络的迭代效果。
实际使用时,Adam作者建议 β 1 = 0.9 \beta_1=0.9 β1=0.9 β 2 = 0.999 \beta_2=0.999 β2=0.999 ϵ = 1 0 − 8 \epsilon=10^{-8} ϵ=108(Keras代码中, ϵ = 1 0 − 7 \epsilon=10^{-7} ϵ=107 ϵ \epsilon ϵ在平方根号外,不过这个改变其实对性能没有太大影响), η \eta η就是所谓的learning rate,需要工程师根据具体的问题进行设置。

梯度法改进-learning rate decay

在Keras的Adam源码里,注意到还有一行之前没有提到,即

if self.initial_decay > 0:
   lr = lr * (1. / (1. + self.decay * K.cast(self.iterations,
           K.dtype(self.decay))))

即learning rate decay。其原理是如果梯度即将收敛的时候,如果learning rate取的较大,就会导致其一直在收敛区间附近来回震荡,无法收敛,因此learning rate应该“徐徐”的减少,确保迭代能收敛。因此,
η = η 1 1 + d e c a y ∗ t \eta = \eta \frac{1}{1+decay * t} η=η1+decayt1
这里 t t t可以取迭代次数,即每一个batch都减少 η \eta η,也可以取epoch数,即每一个epoch才减少 η \eta η。很显然,Keras源码采用的是第一种情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值