• 神经网络的优化器


      优化器是基于梯度的用来更新可训练参数的方法。本文是参考论文《An overview of gradient descent optimization
    algorithms
    》做出的讨论。我们设置一些符号的含义如下:损失值为 ℓ \ell ,需要被更新的可训练参数为 w w w,使用 ω \omega ω 来泛指除 w w w 外的其它可训练参数,记 ℓ \ell w w w 的偏导函数为 J ( w ,   ω ) = ∂ ℓ ∂ w J(w, \, \omega)=\frac{\partial \ell}{\partial w} J(w,ω)=w,该函数的自变量为全体可训练参数,记 g t = J ( w t ,   ω t ) g_t=J(w_t, \, \omega_t) gt=J(wt,ωt),设学习率为 λ \lambda λ(常取值为 0.01 0.01 0.01),优化器迭代的次数为 t t t

    随机梯度下降

      随机梯度下降法(stochastic gradient descent, SGD)是原始 BP 算法提供的优化器,也是最早在深度学习中应用的优化器。其公式如下:
    w t + 1 = w t − λ g t w_{t+1}=w_t-\lambda g_t wt+1=wtλgt  SGD 算法面临着诸多挑战:

    • 当使用 SGD 下降到沟壑或盆地时,SGD 可能产生剧烈的抖动。一方面,抖动可能会使其跳出当前极小值,有机会找到更优的极小值;另一方面,抖动可能使得收敛速度减慢或无法收敛到极小值,此时只能通过手动降低学习率来降低抖动。研究者们最先提出了学习率计划表,为损失值设定阈值及其对应的学习率,当损失值下降到某一阈值时,启用该阈值对应的学习率。但学习率计划表,有针对性没有广泛性,对每一个数据集都需要编制其独有的学习率计划表。
    • SGD 对于所有的可训练参数使用相同的学习率是不恰当的。我们不希望以同样的程度来更新所有参数,对于那些频繁更新的参数我们希望它每次更新能有一个较小的幅度,那些更新频率较低的参数我们希望它每次更新能有一个较大的幅度。

    动量

      在沟壑中 SGD 会在沟壑两侧剧烈抖动,而在沟壑的下降方向移动十分缓慢。动量法(momentum)通过累积的方式,可以抑制在沟壑两侧方向上的抖动,在下降方向上使速度叠加。其公式如下:
    m t = α m t − 1 + λ g t m_t = \alpha m_{t-1} + \lambda g_t mt=αmt1+λgt w t + 1 = w t − m t w_{t+1} = w_t-m_t wt+1=wtmt  其中 α \alpha α 是新引入的常量参数(常取值为 α = 0.9 \alpha=0.9 α=0.9), m t m_t mt 是为了实现算法而引入的变量。当 g t − 1 g_{t-1} gt1 的符号与 g t g_t gt 的正负不同时, m t m_t mt 的累加就会使二者得到一定的抵消,即抑制抖动的作用;当 g t − 1 g_{t-1} gt1 的符号与 g t g_t gt 的正负相投时, m t m_t mt 的累加就会使二者叠加,即叠加速度的作用。
    在这里插入图片描述

      从上图可以看出 Momentum 在短时间内就将抖动抑制,而 SGD 抖动从未停止。并且 Momentum 对在沟壑下降方向上对速度的叠加效果也很明显,仅用 1426 轮迭代就走出了模型,而 SGD 使用了 14778 轮。

    内斯特洛夫加速梯度

      我们蒙着眼睛向前走时,总是伸出自己的两只手,探测自己的前方有无障碍物,以便及时更改前进方向。内斯特洛夫加速梯度(nesterov accelerated gradient,NAG)就使用了这种方法,而是使用前方的梯度来修正当前的前进方向。其公式如下:
    m t = α m t − 1 + λ J ( w t − α m t − 1 ,   ω t − α μ t − 1 ) m_t = \alpha m_{t-1} + \lambda J(w_t-\alpha m_{t-1}, \, \omega_t - \alpha \mu_{t-1}) mt=αmt1+λJ(wtαmt1,ωtαμt1) w t + 1 = w t − m t w_{t+1} = w_t-m_t wt+1=wtmt  其中 α \alpha α 是新引入的常量参数(常取值为 α = 0.9 \alpha=0.9 α=0.9), m t m_t mt 是为了实现算法而引入的变量, μ t \mu_t μt 泛指对应 ω \omega ω m t m_t mt。接下来我们将通过一幅示意图为读者介绍 NAG 的原理,以及其与 Momentum 的对比。
    在这里插入图片描述
      在上图中,Momentum 求当前点的梯度得到图中蓝色短线所示向量,然后再加上动量(图中蓝色长线所示向量)得到最终的更新向量,即图中紫色线所示向量;NAG 不再求当前点的梯度,而是求当前点加上动量所到达的点的梯度,即图中绿色短线所示向量,与动量复合即得到红色线所示的向量。最终 Momentum 将按照紫色向量更新,NAG 将按照红色向量更新。

    自适应梯度

      前面我们提到为可训练参数设置相同的学习率是不合理的。自适应梯度(adaptive gradient, AdaGrad)提供了一种为参数动态调整学习率的方法。它为频繁更新的参数设置较低的学习率,为不经常更新的参数设置较高的学习率,从而使每个参数都有自己的更新幅度。其公式如下:
    v t = v t − 1 + g t 2 v_t=v_{t-1}+g_t^2 vt=vt1+gt2 w t + 1 = w t − λ v t + ϵ ⋅ g t w_{t+1}=w_t-\frac{\lambda}{\sqrt{v_t+\epsilon}} \cdot g_t wt+1=wtvt+ϵ λgt  其中为了避免分母为零而引入的常量参数 ϵ \epsilon ϵ (常取值为 ϵ = 1 × 1 0 − 8 \epsilon=1 \times 10^{-8} ϵ=1×108), v t v_t vt 是为了实现算法而引入的变量。 v t v_t vt 一直在对 g t 2 g_t^2 gt2 做累加,如果一个参数频繁更新必然会导致 v t v_t vt 增大的幅度超乎寻常,那么 λ v t + ϵ \frac{\lambda}{\sqrt{v_t+\epsilon}} vt+ϵ λ 就会超乎寻常的相应变小。这种方式也可以抑制抖动,即让那些梯度有剧烈变化的参数有一个较小的学习率。
    在这里插入图片描述
      如图所示 AdaGrad 为 y 配置了较大的学习率,为 x 配置了较小的学习率,从而使其能够快速脱离马鞍。AdaGrad 仅迭代了 2519 轮,而 SGD 迭代了 125005 轮。
      我们看到 v t v_t vt 一直在做正数累加,总体上会使全体参数的学习率趋向无穷小,在训练的后期会使模型的收敛速度变得极慢。不可否认的是,在训练的后期是需要降低学习率,从而稳定下降到极小值,避免在极小值处抖动,即使用退火学习率。笔者推测,AdaGrad 也是出于这种考量,使用正数累加的方式从总体上来降低学习率,让模型在训练后期稳定下降。但 AdaGrad 的现实表现却不尽如人意。
    在这里插入图片描述
      我们可以看到,AdaGrad 在峡谷中十分稳定没有分毫抖动,但不断下降的学习率让它步履维艰,迭代了 100000 轮还没有走出峡谷。
      若想深入了解该方法可查阅原始文献《Adaptive Subgradient Methods for Online Learning and Stochastic Optimization

    均方根支撑

      均方根支撑(root mean square prop, RMSProp)是 Geoff Hinton 在他的课堂讲义中提出的一个尚未发表的方法。RMSProp 相对于 AdaGrad 单调减少的学习率有了很大改善,它的 v t v_t vt 不再是做正数累加,而是使用了衰减平均值,使其能够稳定在一定的范围之中。其公式如下:
    v t = β v t − 1 + ( 1 − β ) g t 2 v_t=\beta v_{t-1}+(1-\beta)g_t^2 vt=βvt1+(1β)gt2 w t + 1 = w t − λ v t + ϵ ⋅ g t w_{t+1}=w_t - \frac{\lambda}{\sqrt{v_t + \epsilon}} \cdot g_t wt+1=wtvt+ϵ λgt  其中常量参数 ϵ \epsilon ϵ 的作用及常用取值与 AdaGrad 一致,新引入常量参数 β \beta β 作为 v t v_t vt 的衰减系数(常取值为 β = 0.9 \beta=0.9 β=0.9), v t v_t vt 是为了实现算法而引入的变量。
    在这里插入图片描述
    在这里插入图片描述
      可以看到,无论是在马鞍上还是在峡谷中 RMSProp 在速度和抑制抖动方面都有着非常出色的表现。但细心观察会发现 RMSProp 在峡谷底部还是有细微的抖动,看来仅凭学习率来抑制抖动,还是无法做到根除。
      若想深入了解该方法可查阅原始文献《rmsprop: Divide the gradient by a running average of its recent magnitude

    自适应矩估计

      自适应矩估计(Adaptive Moment Estimation, Adam)是个缝合怪,它把 Momentum 和 RMSProp 缝合到了一起,使得它既有自适应调节学习率的能力,也有动量抑制抖动、叠加速度的能力。其表达式如下:
    { m t = α ⋅ m t − 1 + ( 1 − α ) g t v t = β ⋅ v t − 1 + ( 1 − β ) g t 2 \left\{

    mt=αmt1+(1α)gtvt=βvt1+(1β)gt2" role="presentation" style="position: relative;">mt=αmt1+(1α)gtvt=βvt1+(1β)gt2
    \right. {mt=αmt1+(1α)gtvt=βvt1+(1β)gt2 w t + 1 = w t − λ v t 1 − β t + ϵ ⋅ m t 1 − α t w_{t+1}=w_t - \frac{\lambda}{\sqrt{\frac{v_t}{1-\beta^t}}+\epsilon} \cdot \frac{m_t}{1-\alpha^t} wt+1=wt1βtvt +ϵλ1αtmt  其中 α \alpha α β \beta β 是用作衰减系数的常量参数(常取值为 α = 0.9 , β = 0.999 \alpha=0.9,\beta=0.999 α=0.9,β=0.999),常量参数 ϵ \epsilon ϵ 的作用及常用取值与 AdaGrad 一致, m t m_t mt v t v_t vt 是为了实现算法而引入的变量。值得注意的是 Adam 的作者对 m t m_t mt v t v_t vt 做了如下处理: m t 1 − α t \frac{m_t}{1-\alpha^t} 1αtmt v t 1 − β t \frac{v_t}{1-\beta^t} 1βtvt。是因为作者发现 m t m_t mt v t v_t vt 在初始化时为零,所以在刚开始迭代时其值很小(特别是在衰减值设置的很大的时候)。所以作者加入 m t 1 − α t \frac{m_t}{1-\alpha^t} 1αtmt v t 1 − β t \frac{v_t}{1-\beta^t} 1βtvt,在刚开始迭代时使其得到适当放大。可以看到随着迭代次数的增加 1 − α t 1-\alpha^t 1αt 1 − β t 1-\beta^t 1βt 的值逐渐趋于 1 1 1,所以迭代次数达到一定值时,二者的影响就可以忽略不计了。
    在这里插入图片描述
      通过上图可以看到可以看到 Adam 相对于 RMSProp 在马鞍上的表现更为优秀,下降曲线也比较平滑。
    在这里插入图片描述
      通过上图可以看到,虽然 Adam 的下降速度比 RMSProp 慢一些,但是在峡谷中没有像 RMSProp 一样发生抖动。
    在这里插入图片描述
      通过上图可以更直观的看出 Adam 的优势,Adam 经过 1379 轮迭代后下降到了最小值点,而 RMSProp 一直在最小值附近抖动,经过 100000 轮迭代还没有稳定下来。
      若想深入了解该方法可查阅原始文献《ADAM: A METHOD FOR STOCHASTIC OPTIMIZATION

    附录

      以下为笔者将优化器的表现可视化所使用的代码:

    from matplotlib import pyplot as plt
    from matplotlib import colors
    import numpy as np
    
    
    class Ravine:
        @staticmethod
        def get_name():
            return 'Ravine'
    
        # 模型的方程
        @staticmethod
        def function(x, y):
            return -np.cos(2 * x) * 50 + np.power(np.e, y)
    
        # 模型的梯度
        @staticmethod
        def gradient(x, y):
            return np.sin(2 * x) * 100, np.power(np.e, y)
    
        # 输出模型的范围,依次为:x 轴最小值、x 轴最大值、y 轴最小值、y 轴最大值
        @staticmethod
        def get_scope():
            return -1, 1, -5, 1
    
        # 输出优化器在本模型上梯度下降的起点
        @staticmethod
        def get_start():
            return -0.8, 0.5
    
    
    class Saddle:
        @staticmethod
        def get_name():
            return 'Saddle'
    
        @staticmethod
        def function(x, y):
            return x * x - y * y * y * y
    
        @staticmethod
        def gradient(x, y):
            return x * 2, -y * y * y * 4
    
        @staticmethod
        def get_scope():
            return -2, 2, -2, 2
    
        @staticmethod
        def get_start():
            return -1, -0.01
    
    
    class Beale:
        @staticmethod
        def get_name():
            return 'Beale'
    
        @staticmethod
        def function(x, y):
            return (1.5 - x * y)**2 + (2.25 - x - x * y * y)**2 + (2.625 - x + x * y * y * y)**2
    
        @staticmethod
        def gradient(x, y):
            gradient_x = 2 * ((1.5 - x + x * y) * (-1 + y) + (2.25 - x + x * y * y) * (-1 + y * y) + (
                        2.625 - x + x * y * y * y) * (-1 + y * y * y))
            gradient_y = 2 * ((1.5 - x + x * y) * x + (2.25 - x + x * y * y) * (2 * x * y) + (2.625 - x + x * y * y * y) * (
                        3 * x * y * y))
            return gradient_x, gradient_y
    
        @staticmethod
        def get_scope():
            return -5, 5, -5, 5
    
        @staticmethod
        def get_start():
            return 1.5, 1.2
    
    
    class SGD:
        _learning_rate = 0.01
    
        def optimize(self, gradient_w, w, t):
            w = w - self._learning_rate * gradient_w
            return w
    
    
    class Momentum:
        _learning_rate = 0.01
        _alpha = 0.9
    
        def __init__(self):
            self.m = 0
    
        def optimize(self, gradiant_w, w, t):
            self.m = self._alpha * self.m + self._learning_rate * gradiant_w
            w = w - self.m
            return w
    
    
    class NAG:
        _learning_rate = 0.01
        _alpha = 0.9
    
        def __init__(self):
            self.m = 0
    
        def get_momentum(self):
            return self._alpha * self.m
    
        def optimize(self, detection_gradiant_w, w, t):
            self.m = self._alpha * self.m + self._learning_rate * detection_gradiant_w
            w = w - self.m
            return w
    
    
    class AdaGrad:
        _learning_rate = 0.01
        _epsilon = 0.0000000001
    
        def __init__(self):
            self.v = 0
    
        def optimize(self, gradient_w, w, t):
            self.v = self.v + gradient_w**2
            w = w - self._learning_rate / np.sqrt(self.v + self._epsilon) * gradient_w
            return w
    
    
    class RMSProp:
        _learning_rate = 0.01
        _beta = 0.9
        _epsilon = 0.0000000001
    
        def __init__(self):
            self.v = 0
    
        def optimize(self, gradient_w, w, t):
            self.v = self._beta * self.v + (1 - self._beta) * gradient_w**2
            w = w - self._learning_rate / np.sqrt(self.v + self._epsilon) * gradient_w
            return w
    
    
    class AdaDelta:
        _beta = 0.9
        _epsilon = 0.0000000001
    
        def __init__(self):
            self.v = 0
            self.d = 0
    
        def optimize(self, gradient_w, w, t):
            self.v = self._beta * self.v + (1 - self._beta) * gradient_w**2
            t = np.sqrt(self.d + self._epsilon) / np.sqrt(self.v + self._epsilon) * gradient_w
            w = w - t
            self.d = self._beta * self.d + (1 - self._beta) * t**2
            return w
    
    
    class Adam:
        _learning_rate = 0.01
        _alpha = 0.9
        _beta = 0.99
        _epsilon = 0.0000000001
    
        def __init__(self):
            self.m = 0
            self.v = 0
    
        def optimize(self, gradient_w, w, t):
            self.m = self._alpha * self.m + (1 - self._alpha) * gradient_w
            self.v = self._beta * self.v + (1 - self._beta) * gradient_w**2
            w = w - self._learning_rate \
                / (np.sqrt(self.v / (1 - np.power(self._beta, t))) + self._epsilon) \
                * self.m / (1 - np.power(self._alpha, t))
            return w
    
    
    optimizers = {
        'SGD': SGD,
        'Momentum': Momentum,
        'NAG': NAG,
        'AdaGrad': AdaGrad,
        'RMSProp': RMSProp,
        'AdaDelta': AdaDelta,
        'Adam': Adam,
    }
    
    
    def experiment(axes, model, optimizer):
        scope = model.get_scope()
        x, y = np.meshgrid(np.linspace(scope[0], scope[1], 100), np.linspace(scope[2], scope[3], 100))
        z = model.function(x, y)
    #    axes.plot_surface(x, y, z, zorder=1)                                        # 在图上绘制模型
        axes.plot_surface(x, y, z, zorder=1, norm=colors.LogNorm(), cmap='jet')     # 在图上绘制模型
        axes.set_xlabel('x')
        axes.set_ylabel('y')
        axes.set_zlabel('z')
        axes.set_title(f'%s in %s' % (optimizer, model.get_name()))
    
        optimizer_x = optimizers[optimizer]()   # 为 x 生成优化器
        optimizer_y = optimizers[optimizer]()   # 为 y 生成优化器
        x, y = model.get_start()
        xa, ya = [x], [y]       # 用于记录下降过程中经过的点
        t = 1       # 记录迭代轮次
        while (t < 10
               or (t < 100000
                   and not (ya[-1] == ya[-2] and xa[-1] == xa[-2])
                   and (scope[0] < x < scope[1] and scope[2] < y < scope[3]))):
            if optimizer == 'NAG':      # 计算梯度
                gradient_x, gradient_y = model.gradient(x - optimizer_x.get_momentum(), y - optimizer_y.get_momentum())
            else:
                gradient_x, gradient_y = model.gradient(x, y)
            x = optimizer_x.optimize(gradient_x, x, t)      # 用优化器优化
            y = optimizer_y.optimize(gradient_y, y, t)      # 用优化器优化
            xa.append(x)
            ya.append(y)
            t = t + 1
    
        za = [model.function(i, j) for i, j in zip(xa, ya)]         # 生成下降时经过的点的 z 轴坐标
        axes.plot(xa, ya, za, zorder=3, label=optimizer)            # 在图上绘制下降路线
        axes.text(x, y, model.function(x, y), f'epoch=%d' % t)
        axes.legend()
    
    
    if __name__ == '__main__':
        experiment(plt.subplot(121, projection='3d'), Beale, 'RMSProp')
        experiment(plt.subplot(122, projection='3d'), Beale, 'Adam')
        plt.show()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229

      以下为生成 NAG 示意图的代码:

    from matplotlib import pyplot as plt
    import numpy as np
    
    ax = plt.subplot(111, aspect='equal')
    ax.axis('off')
    ax.arrow(0.00, 0.00, 0.02, 0.04, length_includes_head=True, color='b')
    ax.arrow(0.02, 0.04, 0.08, 0.04, length_includes_head=True, color='b')
    ax.arrow(0.00, 0.00, 0.10, 0.08, length_includes_head=True, color='m')
    ax.arrow(0.00, 0.00, 0.08, 0.04, length_includes_head=True, color='g')
    ax.arrow(0.08, 0.04, 0.02, -0.04, length_includes_head=True, color='g')
    ax.arrow(0.00, 0.00, 0.10, 0.00, length_includes_head=True, color='r')
    ax.text(-0.015, 0.02, r'$-\lambda \cdot J(w_t, \omega_t)$', color='b', size=12)
    ax.text(0.048, 0.061, r'$-\alpha m_t$', color='b', size=12)
    ax.text(0.06, 0.045, r'$-\alpha m_t-\lambda \cdot J(w_t, \omega_t)$', color='m', size=12)
    ax.text(0.083, 0.082, 'Momentum', color='m', size=16)
    ax.text(0.04, 0.027, r'$-\alpha m_t$', color='g', size=12)
    ax.text(0.045, 0.010, r'$-\lambda \cdot J(w_t - \alpha m_t, \omega_t - \alpha\mu_t)$', color='g', size=12)
    ax.text(0.02, -0.004, r'$-\alpha m_t - \lambda \cdot J(w_t - \alpha m_t, \omega_t - \alpha\mu_t)$', color='r', size=12)
    ax.text(0.102, -0.002, 'NAG', color='r', size=16)
    plt.show()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
  • 相关阅读:
    Cookie 能跨域吗?如何设置?
    创新能力 | 产品经理实践中常犯的七大错误
    WPF 设置全局静态参数
    《大话数据结构》学习记录8——查找
    pandas之数据的合并与分组
    『手写Mybatis』实现映射器的注册和使用
    【Numpy总结】强烈推荐。超实用Numpy学习目录,一篇学会Numpy
    docker安装nessus
    基于jsp+ssm的家政服务系统-计算机毕业设计
    开发板uboot与virtualbox虚拟机、windows11网络互通
  • 原文地址:https://blog.csdn.net/A_fearless_engineer/article/details/127211992