美文网首页深度学习
Paper LeNet-5 详解

Paper LeNet-5 详解

作者: 公输睚信 | 来源:发表于2020-03-20 19:20 被阅读0次

            LeNet-5 是最早提出的卷积神经网络之一,名称来源于论文的主要作者 LeCun,本文依据其论文 Gradient-Based Learning Applied to Document Recognition (1998) 来详细的解读该网络。

    一、背景

            传统的机器学习算法通常由两个模块组成,如下图所示:

    传统模式识别的模式

    第一个模块称为特征提取器,将原始的输入变换成低维的向量或者短字符串,这样之后数据不仅容易匹配或比较,而且在不改变它们本质的变换和变形下具有相对的不变性。通常,特征提取器包含大量的先验信息,不同的任务需要进行特别的定制。而且,一般来说它是纯手工设计的,因此会耗费大量的时间和精力。基于此,这个方法的明显缺陷就是识别准确率会极大的依赖于设计者挑选特征集的能力。很多时候,对于一个特定的任务,设计者都是在比较和评估不同特征集的优劣。尽管这是一项令人畏惧的任务,但不幸的是面对一个新任务时这又是不得不重做的。第二个模块是分类器,这基本上是通用的和可训练的。早期的时候,由于分类器只局限于可分性较高的低维数据,因此特征提取器的选取尤为重要。最近几十年,由于如下 3 个原因:

    • 高性能硬件设备的出现
    • 大规模数据集的发布
    • 先进机器学习技术的提出

    使得对特征提取器的依赖逐渐削减。快速的计算单元使得依赖暴力数值计算的方法变得可行,比如,使用 GPU 可以成百上千倍的加速神经网络的计算,因此分类器处理大规模、高维度的数据已经变得越来越廉价。另一方面,大数据集的出现,使得手工设计特征提取器繁琐、低效且准确率不高的缺点变得越来越不可接受,促使设计者更着眼于实际的原始数据,进而推动机器学习算法越来越自动化,手工筛选特征集的工作已逐步由表示学习替代。而强大的机器学习新技术(比如,反向传播算法等)又让表示学习的性能异常优异,不仅让处理高维度的输入变得可能,还能产生错综复杂的决策函数,显著的拓宽了学习算法的应用范围。

            考虑最流行的梯度学习算法,以 \mathbf{z}^p 表示第 p 个输入,\mathbf{w} 表示系统的可训练参数,则系统的输出
    \mathbf{y}^p = f(\mathbf{z}^p, \mathbf{w})
    可以表示被识别到某一类的类标号得分或者概率。为了度量与真实的输出 \mathbf{d}^p 之间的差距,计算损失函数
    e^p = d(\mathbf{d}^p, f(\mathbf{z}^p, \mathbf{w})),
    以及其在标注的训练集 \{(\mathbf{z}^1, \mathbf{d}^1),\cdots, (\mathbf{z}^P, \mathbf{d}^P)\} 上的平均损失 e_{train}(\mathbf{w}) 。在最简单的情况下,学习的目标是找到使平均损失 e_{train}(\mathbf{w}) 达到最小的参数值 \mathbf{w} 。但实际上,关注训练集上的损失意义不大,更应该评估的是学习系统在实际工作时的性能,这可以通过其在与训练集无交集的测试集上的错误率来度量。

            一些理论和实验分析发现,训练集上的错误率和测试集上的错误率之间的差值近似于
    e_{test} - e_{train} = k(h/P)^\alpha,
    其中 P 是训练样本数,h 是学习系统的有效容量复杂性的一个度量,\alpha 介于 0.5 到 1 之间,k 是一个常数。显然,只要训练样本数目 P 增长时,这个差值就势必减小。但系统复杂度 h 的增长带来的结果却比这复杂。当学习系统的规模非常小时,系统几乎学习不到数据集中的模式,此时 e_{train},e_{test} 两者都比较大,因而它们的差值会较小,随着系统复杂度的增加,系统的学习能力不断加强,训练误差 e_{train} 会一直减小,而测试误差 e_{test} 则会先减小到一定程度,之后由于系统过拟合,再逐步变大。因此,持续的增加学习系统的复杂度,测试误差与训练误差的差值会先逐渐减小(或者一直比较平稳)然后再逐渐增大。为了权衡训练误差 e_{train} 的减小和差值 e_{test} - e_{train} 的变大,需要选出最优的复杂度 h 以便取得最低的泛化误差 e_{test} 。大部分的学习算法都试图同时最小化 e_{train}e_{test}-e_{train},一种正式的说法叫结构风险最小化。实际实现时,是最小化 e_{train}+\beta h(\mathbf{w}),其中 h(\mathbf{w}) 称为正则化函数\beta 是一个常数。h(\mathbf{w}) 的选择需满足对应参数空间中高容量的参数集 \mathbf{w} 它要取更大的值,这样极小化 h(\mathbf{w}) 能够有效的控制可选参数集的容量,进而控制训练误差和训练误差与测试误差之间差值的期望值之间的均衡。

            求函数极值的最简单的方法莫过于基于梯度的算法。一旦得到损失函数 e(\mathbf{w}) 之后,假设它是几乎处处可微的,那么梯度下降算法只要简单的迭代:
    \mathbf{w}_k = \mathbf{w}_{k-1} - \epsilon\frac{\partial e(\mathbf{w})}{\mathbf{w}}
    就能执行最小化的过程。在最简单的情形下,\epsilon 是一个常数,复杂一点的话也可以是变量,对角矩阵,或者牛顿法拟牛顿法中的黑塞矩阵的逆。除此之外,共轭梯度法也可以用于极小化损失函数。但这些复杂算法(牛顿法、拟牛顿法、共轭梯度法等)并不适合大型数据集上大型神经网络的训练,它们或者需要过大的运算次数,比如,假设 N 是参数个数,牛顿法在每次更新时需要执行 O(N^3) 次运算,拟牛顿法需要 O(N^2) 次运算,这对于大型网络来说是不可行的,或者运算次数不多,但实际收敛速度却不及随机梯度算法,比如,共轭梯度法仅需要 O(N) 次运算,但它的收敛速度依赖于精确的计算共轭梯度方向,而这只有在整批量(整个训练集)更新的情况下才成为可能,对于大数据集来说,收敛速度过慢(可以这么理解,因为一个整批量,即一个 epoch 才更新一次参数,而对应的随机梯度算法已经更新了成百上千次,所以对照而言收敛速度会慢)。也许你会问,小批量的共轭梯度算法呢?一些论文也已经尝试过了,但并没有证实速度会超过仔细调过参的随机梯度算法。

            基于梯度的学习算法有两种更新参数的方式,整批量梯度更新和随机梯度更新。前者对整个训练集计算梯度,而后者只对单个或少部分样本计算近似梯度,然后更新参数。随机梯度更新中的小批量样本可以随机的选取,也可以先随机化一个合适的序列然后按顺序选取。虽然随机梯度算法梯度估计是带噪声的,但它更新的频率更高,实验显示,对于有冗余数据的大型的数据集,它的收敛速度比整批量梯度算法要显著的快,有时甚至可以快几个数量级。尽管这还不能从理论上完全的理解,但直观来说,可以用下面极端的例子来解释。让我们假设有一个数据集,它由两份完全一样的样本集组成(即整个数据集由某个小数据集复制两份而来),对于整批量梯度算法,它计算梯度时要在整个数据集上计算,因此会在相同的样本上计算两次,造成计算上的冗余(因为梯度是对所有样本平均,所以在整数据集和小数据集上的结果一样),而对于随机梯度算法来说,在整个数据集上分批次更新一遍,相当于是在小数据集上迭代了两次。虽然这个极端例子中的数据集是用重复样本构造来的,但上面的想法可以进行适度的推广,即对于具有冗余样本的数据集,它尽管没有重复样本,但部分样本之间很相似,则随机更新肯定效果要好。

            基于梯度的学习算法从上世纪 五十年代就提出了,但大部分用于线性系统,它在复杂机器学习任务上超乎寻常的有用这一点,并没有一开始就被广泛的意识到,直到下面的 3 个事实让这一局面出现了扭转。首先,尽管基于梯度的方法一般只会收敛到损失函数的极小值而不是最小值,但实践显示,训练后的学习系统性能已经非常卓越。其次,对于由多个处理层组成而来的非线性复杂系统,用反向传播算法计算梯度已经非常的简单和高效。最后,实际的案例显示,反向传播算法应用到多层神经网络确实能够求解复杂的学习任务。反向传播算法的基本思路即是链式法则,比如,考虑一个由多个级联模块组成的系统,每一个模块由函数 \mathbf{x}_n=f_n(\mathbf{w}_n,\mathbf{x}_{n-1}) 表示该模块的输出向量,\mathbf{w}_n 是该模块的可训练参数,\mathbf{x}_{n-1} 是该模块的输入向量(即前一模块的输出向量),则损失 e 关于 \mathbf{w}_n\mathbf{x}_{n-1} 的梯度可以通过递归来反向计算
    \frac{\partial e}{\partial \mathbf{w}_n} = \frac{\partial f}{\partial \mathbf{w}}(\mathbf{w}_n,\mathbf{x}_{n-1})\frac{\partial e}{\partial \mathbf{x}_n},\\ \frac{\partial e}{\partial \mathbf{x}_{n-1}} = \frac{\partial f}{\partial \mathbf{x}}(\mathbf{w}_n,\mathbf{x}_{n-1})\frac{\partial e}{\partial \mathbf{x}_n},
    其中,\frac{\partial f}{\partial \mathbf{w}}(\mathbf{w}_n,\mathbf{x}_{n-1})f 关于 \mathbf{w} 在点 (\mathbf{w}_n, \mathbf{x}_{n-1})雅可比矩阵\frac{\partial f}{\partial \mathbf{x}}(\mathbf{w}_n,\mathbf{x}_{n-1}) 同理,向量函数的雅可比矩阵包含了所有输出分量关于输入分量的梯度。对于梯度学习算法,在小批量上的梯度是在该批量内每个样本上计算的梯度的平均值。上面的递归公式虽然显式的出现了雅可比矩阵与其它项的乘积,但实际实现时,是直接计算该乘积而不需要显式的计算雅可比矩阵。

            传统的神经网络是以上多级联模块系统的一个特殊例子,它的状态信息 \mathbf{x}_n 由固定大小的向量表示,模块由矩阵乘法(权重)和元素级的 sigmoid 函数(神经元)表示。多层神经网络(以及梯度算法)有效处理高维、非线性可分数据的能力,让它成为图像识别任务的一种潜在的解决方案。前面已经提过,传统模式识别模型由手工设计的特征提取器和可训练的分类器组成,前者将原始数据提取为易处理的特征向量,后者则将特征向量预测为类别。但手工设计特征提取器是一件特别繁琐、低效的事情,因此将特征提取阶段也纳入自动学习过程是更有趣的一种范式。

            以字符识别为例,我们要建立的网络应当以近乎原始的数据(即仅仅做过尺寸正规化的图像)作为输入。虽然这可以简单的通过全连接的前馈神经网络来实现,但也会存在一些问题。首先,因为输入图像的维度比较大,造成全连接层的参数数目非常大,也就是系统的容量(复杂度)非常高,这样为了避免过拟合,需要特别大的训练数据集。而且,在内存(显存)中保存过多的参数,也有可能让硬件设备承受不起。对于图像和语音识别任务来说,非结构化网络还有一个主要的缺陷,就是对于平移以及局部的变形没有内置的不变性。比如,手写数字任务,字符的大小、倾斜度、位置,以及书写风格都是多变的,这样会让可分辨特征出现在各种各样的位置。尽管一个充分大的全连接网络能够学习到对这些变化保持不变的输出,但是会导致不同位置的一些神经元具有相似的权重以保证可分辨特征出现在各种位置都能检测出来,而学习这样的权重配置则又需要规模巨大的训练样本来覆盖各种可能的变化。

            其次,全连接网络的一个缺陷是完全忽视了输入的拓扑结构,输入变量以任意(固定)的次序出现而不影响训练的结果。然而,图像(或者语音的时序表示)具有很强的 2 维局部结构:变量(或者像素)在空间或者时间上是局部高度相关的,这是在识别空间或者时间对象之前提取或者组合局部特征很有好处的原因,因为相邻变量的形状可以被分成少数几个类别(例如,边,角等)。卷积神经网络通过把隐藏结点的感受野限制到局部来强制的提取局部特征。

            卷积神经网络组合了 3 个结构性的概念来确保对平移、缩放和变形具有一定程度的不变性,它们是:

    • 局部感受野
    • 参数共享
    • 下采样

    局部连接使得每个结点都只接收前一层小领域范围内结点集的输出,来提取基本的视觉特征,比如,有向边,端点,交角等(或者其它信号,如语音谱图,的类似特征),这些特征经过后续层的组合形成更高阶的特征。如前所述,输入的变形或者平移会让一些显著特征的位置发生变化。另外,对图像中某一部位有用的基本特征检测器可能对整张图像的其它部位都有用。这些认识启发我们强制让感受野位于图像中不同位置的神经元具有相同的权重向量。权重共享的一个有趣的性质就是卷积层对于一定程度上的平移和变形是鲁棒的。一旦一个特征被检测到,那么它的精确位置就不再重要了,重要的是对于其它特征的相对位置。比如,一旦我们知道图像的左上角包含一条水平线段的端点,右上角包含一个交角,以及图像的下侧有一条近似竖直线段的端点,那么我们就知道这是一个 7 字。特征的精确位置对于识别一个字符来说不仅不相关,反而是有害的,因为同一个字符在不同样本中的位置是各不相同的。一个减小特征精确位置影响的简单做法是减小特征映射(feature map)的分辨率,这可以通过下采样来完成。下采样层对小范围内的神经元执行局部平均(或其它运算),不仅减小特征映射的分辨率,而且还可以降低对平移和变形的敏感性。

            以识别字符为例,作者们提出了经典的卷积神经网络 LeNet-5,它由卷积层和下采样层组成,这两者交错排列,形成了一种双向金字塔结构:从输入层到输出层,每一层的通道数越来越大,而空间分辨率则越来越小。通道数的增加让每层的特征表示越来越丰富,加上分辨率的减小,就让网络对输入的几何变换有了大程度上的不变性。

            下面详细的讲解 LeNet-5。

    二、LeNet-5

            LeNet-5 的结构如下:

    LeNet-5 网络结构

    它总共由 7 层组成,分别是:卷积层 C1,下采样层 S2,卷积层 C3,下采样层 S4,卷积层 C5,全连接层 F6 和输出层 OUTPUT。其中,需要注意的是,这里的下采样层是带有学习参数的,而不是通常意义下的池化层。网络的输入是分辨率为 32x32 的灰度图,由黑白两种像素组成,预处理时将白色(背景)转化成 -0.1,黑色(前景)转化成 1.175,这样大概能使输入的平均值为 0、方差为 1 以便加速训练。

            卷积层 C1,卷积核大小为 5x5,输出通道数为 6,输出分辨率为 28x28(32 - 5 + 1 = 28),总参数个数是 156 个,其中权重个数是 150 = 1 x 5 x 5 x 6 个,偏置项有 6 个,总连接次数为 122304 = 28 x 28 x 156

            下采样层 S2,不同于经典的池化层,它带有可训练的参数。对于特征映射中的每一个单元,它来源于 C1 中对应特征映射的 2x2 领域内 4 个单元的和,然后乘以一个可训练的系数,再加上一个可训练的偏置,最后传入 Sigmoid 函数作为输出(通道分离计算: C1 的第一个通道计算出 S2 的第一个通道,以此类推)。因为 2x2 的领域是不重叠的,因此 S2 的分辨率下降到了 14x14,它共有 12 = (1 + 1) x 6 个可训练参数和 5880 = 14 x 14 x 6 x (4 + 1) 个连接。

            卷积层 C3,不同于经典的卷积层,也不同于通道可分离的卷积层,它的计算非常的定制化,如下图所示:

    卷积层 C3 的连接方式

    它含有 16 个通道,第 0 个索引通道是对 S2 的前 3 个通道做 5x5 的卷积(见上图第 0 索引列),第 1 个索引通道是对 S2 的第 1,2,3 索引通道做 5x5 卷积(见上图第 1 索引列),其它类似,输出分辨率为 10 = 14 - 5 + 1。为什么不对所有通道计算卷积呢?作者们给出了两个缘由:1.非完全连接的方式可以合理的限制连接数;2.更重要的是这打破了网络的对称。因为对应的输入不同,不同的特征映射强制的去提取不同的(期望中互补的)特征。卷积层 C3 共有 1516 = 6 x (3 x 5 x 5 x 1) + 9 x (4 x 5 x 5 x 1) + 1 x (6 x 5 x 5 x 1) + 16 个可训练参数和 151600 = 10 x 10 x 1516 个连接。

            下采样层 S4, 类似于 S2,只是输出通道为 16,分辨率为 5x5,因此含有 32 = (1 + 1) x 16 个可训练参数和 2000 = 5 x 5 x 16 x (4 + 1) 个连接。

            卷积层 C5,执行普通的卷积,有 120 个通道,卷积核大小为 5x5,因此输出分辨率为 1x1,共有 48120 = 16 * 5 * 5 * 120 + 120 个可训练的参数和连接数。

            全连接层 F6,包含 84 个神经元,共有 10164 = 120 * 84 + 84 个可训练参数。该层的激活函数A\ \mathrm{tanh}(Sa),其中 A 的取值为 1.7159

            LeNet-5 在 MNIST 数据集上的错误为 0.95%(如果做了数据增强则为 0.8%),这个性能超越了当时所有的其它分类器。因为 MNIST 数据集中的图片分辨率为 28x28,因此需要背景扩边 4 个像素达到网络的输入 32x32

    三、实现

          LeNet-5 虽然结构简单,但在当时作为卷积神经网络的先驱之作已经首屈一指。随着卷积神经网络的飞速发展,它采用的部分技术也已经被遗弃,比如,带训练参数的下采样层被无训练参数的池化层替代,双曲正切的激活函数被整流线性单元(ReLU)替代。TensorFlow 官方实现的变形的 LeNet 见 lenet.py

    def lenet(images, num_classes=10, is_training=False,
              dropout_keep_prob=0.5,
              prediction_fn=slim.softmax,
              scope='LeNet'):
      """Creates a variant of the LeNet model.
      Note that since the output is a set of 'logits', the values fall in the
      interval of (-infinity, infinity). Consequently, to convert the outputs to a
      probability distribution over the characters, one will need to convert them
      using the softmax function:
            logits = lenet.lenet(images, is_training=False)
            probabilities = tf.nn.softmax(logits)
            predictions = tf.argmax(logits, 1)
      Args:
        images: A batch of `Tensors` of size [batch_size, height, width, channels].
        num_classes: the number of classes in the dataset. If 0 or None, the logits
          layer is omitted and the input features to the logits layer are returned
          instead.
        is_training: specifies whether or not we're currently training the model.
          This variable will determine the behaviour of the dropout layer.
        dropout_keep_prob: the percentage of activation values that are retained.
        prediction_fn: a function to get predictions out of logits.
        scope: Optional variable_scope.
      Returns:
         net: a 2D Tensor with the logits (pre-softmax activations) if num_classes
          is a non-zero integer, or the inon-dropped-out nput to the logits layer
          if num_classes is 0 or None.
        end_points: a dictionary from components of the network to the corresponding
          activation.
      """
      end_points = {}
    
      with tf.compat.v1.variable_scope(scope, 'LeNet', [images]):
        net = end_points['conv1'] = slim.conv2d(images, 32, [5, 5], scope='conv1')
        net = end_points['pool1'] = slim.max_pool2d(net, [2, 2], 2, scope='pool1')
        net = end_points['conv2'] = slim.conv2d(net, 64, [5, 5], scope='conv2')
        net = end_points['pool2'] = slim.max_pool2d(net, [2, 2], 2, scope='pool2')
        net = slim.flatten(net)
        end_points['Flatten'] = net
    
        net = end_points['fc3'] = slim.fully_connected(net, 1024, scope='fc3')
        if not num_classes:
          return net, end_points
        net = end_points['dropout3'] = slim.dropout(
            net, dropout_keep_prob, is_training=is_training, scope='dropout3')
        logits = end_points['Logits'] = slim.fully_connected(
            net, num_classes, activation_fn=None, scope='fc4')
    
      end_points['Predictions'] = prediction_fn(logits, scope='Predictions')
    
      return logits, end_points
    

    网络已经简化到 4 层(两个卷积层和两个全连接层),同时引入了 dropout 层来增加网络的鲁棒性。

    手机阅读可关注【公众号】:

    qrcode_for_gh_67673e6af399_258.jpg

    相关文章

      网友评论

        本文标题:Paper LeNet-5 详解

        本文链接:https://www.haomeiwen.com/subject/yplsyhtx.html