ANNT: 卷积神经网络





5.00/5 (27投票s)
本文演示了ANNT库在创建卷积神经网络和将其应用于图像分类任务中的用法。
引言
本文继续讨论人工神经网络及其在ANNT库中的实现。第一篇文章 (第一篇文章) 从基础开始,介绍了前馈全连接神经网络及其使用随机梯度下降和误差反向传播算法的训练。然后,它演示了这种人工神经网络架构在多个任务中的应用。其中一项是从MNIST数据库中分类手写字符。尽管这是一个简单的例子,但它在测试数据集上达到了约96.5%的准确率。在本文中,我们将研究一种不同的人工神经网络架构,称为卷积神经网络(CNN)。这类网络专门为计算机视觉任务而设计,在图像识别等任务上优于经典的全连接神经网络。如另一个示例应用程序所示,我们将对手写字符分类达到约99%的准确率。
最初, 卷积神经网络 架构是由Yann LeCun于1998年发表他的研究时引入的。然而,当时它并未引起广泛关注。当使用此架构的团队赢得了 ImageNet 竞赛时,卷积网络在14年后才受到高度关注。CNNs在此之后变得非常流行,并被应用于许多计算机视觉应用,导致开发了许多基于此架构的神经网络。如今,最先进的卷积神经网络在许多图像识别任务上的准确率已经超越了人类。
理论背景
与前馈全连接人工神经网络的情况一样,卷积网络的思想也受到研究自然界——哺乳动物大脑——的启发。Hubel和Wiesel在20世纪50年代和60年代的研究表明,猫和猴子的视觉皮层中存在单独响应视觉场小区域的神经元。如果眼睛不移动,影响单个神经元放电的视觉空间区域被称为其感受野。相邻细胞具有相似且重叠的感受野。感受野的大小和位置在皮层中系统地变化,形成视觉空间的完整映射。
在他们的论文中,他们描述了大脑中两种基本的视觉神经元细胞,它们以不同的方式起作用:简单细胞和复杂细胞。例如,当简单细胞识别固定区域内的基本形状(如直线)和特定角度时,它们会被激活。复杂细胞具有更大的感受野,其输出对视野中的具体位置不敏感。这些细胞即使在视网膜上的绝对位置发生变化,也能继续对特定刺激做出响应。
1980年,一位名叫Fukushima的研究员提出了一个分层神经网络模型,该模型被称为 Neocognitron 。该模型受简单细胞和复杂细胞概念的启发。Neocognitron通过学习物体的形状来识别模式。
后来,在1998年,Yann LeCun及其同事引入了卷积神经网络。他们的第一个CNN称为LeNet-5,能够对数字手写体进行分类。
卷积网络架构
在深入了解构建卷积神经网络的细节之前,让我们先来看一些构建块,它们要么是这类网络特有的,要么是在它们出现时被推广的。正如在 上一篇文章 中所见,人工神经网络的许多概念可以实现为独立的实体,它们既执行推理阶段的计算,也执行训练阶段的计算。由于核心结构已在之前的文章中奠定,这里我们将只是在其上添加构建块,然后将它们缝合在一起。
卷积层
卷积层是卷积神经网络的核心构建块。它假定其输入具有宽度、高度和深度的三维形状。对于第一个卷积层,它通常是一个图像,其深度通常为1(灰度图像)或3(具有3个RGB通道的彩色图像)。对于后续的卷积层,输入由前一层生成的特征图集表示(这里的深度是输入特征图的数量)。目前,我们假设我们处理的输入深度为1,这将使它们变成二维结构。
那么,卷积层本质上就是对 图像卷积 以及一些 核(kernel) 。这是一个非常常见的图像处理操作,用于实现各种结果。例如,它可以使图像模糊或锐化。但这并不是卷积网络感兴趣的内容。根据所使用的核,图像卷积可用于在图像中查找特定特征 - 垂直或水平边缘、角、角度或更复杂的特征,如圆形等。回想一下视觉皮层中简单细胞的概念?
让我们看看如何计算卷积。假设我们有 n x m 的矩阵 K (核)和 I (图像)。那么,它可以写成这些矩阵的点积,其中核矩阵在水平和垂直方向上翻转。
例如,如果我们有3x3的矩阵 K 和 I ,它们的卷积可以这样计算:
上述是信号处理中卷积的定义方式。其中的核在垂直和水平方向上都会被翻转。更直接的计算方式是不进行任何翻转,直接进行 K 和 I 矩阵的正常点积。这个操作称为 互相关(cross-correlation) ,定义如下:
在信号处理中, 卷积和互相关 具有不同的性质,并用于不同的目的。然而,在图像处理和神经网络中,区别变得微妙,并且通常使用互相关代替。对于神经网络来说,这根本不重要。正如我们稍后将看到的,这些“卷积”核实际上是神经网络需要学习的权重。因此,由网络决定学习哪个核 - 翻转的还是未翻转的。考虑到这一点,我们将保持简单,然后使用互相关。 **注意**:在本文后续提到“卷积”的地方,都假定为两个矩阵的正常点积,即互相关。
好的,我们现在知道如何计算相同大小的两个矩阵或相同大小的核和图像之间的卷积。然而,在图像处理中这种情况很少见。核通常是3x3、5x5或7x7等大小的方阵。而图像可以是任何大小。那么图像卷积是如何计算的呢?为了计算图像卷积,核在整个图像上移动,并在核的每个可能位置计算加权和。在图像处理中,这个概念被称为滑动窗口。计算从图像的左上角开始,核与相应大小的图像区域之间计算卷积。然后将核向右移动一个像素,计算另一个卷积。重复此过程,直到该行的所有位置都计算了卷积。完成后,将核移到下一行像素的开头,然后继续该过程。当整个图像处理完毕后,我们就得到了一个特征图 - 每个可能位置的卷积值。
下图说明了图像卷积的计算过程。对于一个8x8大小的图像和一个3x3的核,我们得到一个6x6大小的特征图 - 只有当核完全适合图像时,才计算卷积。下图突出了源图像的几个区域及其在生成的特征图中的相应卷积值。
上述3x3的核设计用于查找对象的左边缘(或滑动窗口中心右侧存在一条直线)。生成特征图中较高的正值表示我们正在寻找的特征的存在。零表示特征的缺失。对于这个特定的例子,负值表示“反”特征的存在 - 对象的右边缘。
如上所示,当计算卷积时,输出的特征图尺寸比源图像小。并且使用的核越大,得到的特征图就越小。对于一个 n x m 大小的核,输入图像会丢失 ( n -1) x ( m -1) 的尺寸。因此,如果我们在上面的例子中使用5x5的核,那么结果特征图的尺寸将缩小到4x4。然而,在许多情况下,我们更希望得到与输入尺寸相同的输出特征图。为了实现这一点,需要对源图像进行填充(通常用零)。例如,如果源图像是8x8大小,而我们的核是5x5大小,那么我们需要填充输入,使其变为12x12大小,即在输入图像的每一侧添加4行/列。这通常是通过在输入图像的每一侧添加2行/列来完成的。
到目前为止,我们已经讨论了如何在数学上计算卷积,以及在图像处理中如何计算图像卷积。然而,我们正在进行人工神经网络的开发,因此我们需要了解以上所有内容与卷积层之间的关系。为简单起见,让我们使用上面的例子 - 一个8x8的输入图像与一个3x3的核进行卷积,得到一个6x6的特征图(输出)。在这种情况下,我们的输入层有64个节点,卷积层有36个神经元。然而,与全连接层不同的是,全连接层中的每个神经元都连接到前一层的所有神经元,卷积层中的神经元仅连接到前一层的一小部分神经元。卷积层中的每个神经元拥有的连接数等于其实现的卷积核中权重的数量,在上面的例子中是9个连接(核大小3x3)。由于卷积层假定输入具有二维形状(通常是三维,但在此示例中保持简单),因此连接是到前一层神经元的矩形组,该组的形状与所使用的核相同。然而,卷积层中每个神经元的连接组都不同,但对于相邻神经元,它们会重叠。这些连接的建立方式与在图像卷积中使用滑动窗口方法计算时选择源图像像素的方式相同。例如,查看上面演示图像卷积的图像,我们可以看到特征图上突出显示的输出与哪个输入(用相同颜色突出显示)相连接。
忽略全连接层和卷积层中的神经元具有不同数量的连接到前一层的事实,以及这些连接具有特定结构的事实,这两种层本质上都做同样的事情 - 计算输入的加权和以产生输出。但还有另一个区别。与每个神经元都有自己的权重的全连接层不同,卷积层的神经元共享权重。因此,如果一个层执行单个3x3卷积(实际上它执行的次数不止一次,但稍后会讨论),它只有一个权重集,即9个,它们在每个神经元之间共享以计算加权和。而且,尽管之前没有提到,卷积层也会在加权和中添加偏置值,这也需要共享。下表总结了全连接层和卷积层之间的区别,并为上面的例子提供了一些数字。
全连接层 | 卷积层 |
不对输入结构做任何假设 | 假定输入为二维形状(通常是三维) |
每个神经元连接到前一层的所有神经元 每个神经元连接64个 | 每个神经元连接到前一层神经元的一个小的矩形组;连接数等于卷积核中的权重数 每个神经元连接9个 |
每个神经元有自己的权重和偏置值 2304个权重和36个偏置值 | 权重和偏置值共享 9个权重和1个偏置值 |
到目前为止,我们一直保持简单,并假设卷积层的输入和输出都具有二维形状。然而,这通常不是这种情况。相反,输入和输出都具有三维形状。首先,我们从输出开始。实际上,每个卷积层计算的卷积不止一个。它执行的卷积数量是一个可配置的参数,在设计人工神经网络时设置。每个卷积都使用其自己的权重集(核)和偏置值,因此产生不同的特征图。如前所述,不同的核可以用于查找不同的特征 - 不同角度的直线、曲线、角点等。因此,通常希望获得突出显示不同特征存在的多个特征图。这些图的计算很简单 - 使用不同的核权重/偏置重复计算给定输入的卷积过程。将其转换为人工神经元的领域,我们只是在卷积层中添加了额外的神经元组,它们以与单个核相同的方式连接到输入。尽管具有相同的连接模式,但这些神经元组共享不同的权重和偏置值。回到之前描述的示例,假设我们将卷积层配置为执行5次3x3的卷积。在这种情况下,输出数量(神经元数量)是36*5=180 - 5个神经元组组织成二维形状,并重复相同的连接模式。每个神经元组共享其自己的权重/偏置集,总共为该层提供45个权重和5个偏置值。
现在我们来讨论输入的3D性质。如果我们谈论的是第一个卷积层,那么它的输入将是某种图像,很可能是。大多数时候,它将是灰度图像(2D数据)或彩色RGB图像(3D数据)。如果我们谈论后续的卷积层,那么输入的深度将等于前一层计算的特征图数量(卷积数量)。当输入深度增加时,卷积层中的神经元数量不会增加。相反,与前一层的连接数量会增加。实际上,卷积核也变成3D形状,大小为 n x m x d ,其中 d 是输入的深度。再次将其转换为神经元的领域,我们可以认为每个神经元获得了到输入包含的每个特征图的附加连接。在2D输入的情况下,每个神经元连接到输入的一个 n x m (核大小)的矩形区域。然而,在3D输入的情况下,每个神经元连接到 d 个这样的区域,它们来自相同的位置,但来自不同的输入特征图。
由于我们已经将卷积层推广到3D输入/输出,并且也提到了偏置值,我们可以更新我们的卷积公式,该公式在核在输入特征的每个可能位置( x , y )处计算。
为了暂时完成卷积层,让我们总结一下用于配置它们的参数。创建全连接层时,我们只使用两个参数 - 输入数量和输出数量(层中的神经元)。然而,在创建卷积层时,我们不需要指定输出数量。相反,我们描述输入形状 h x w x d 以及核的形状和数量 n x m @ z 。因此,我们有6个数字: w - 输入特征图(图像)的宽度, h - 输入特征图的高度, d - 输入深度(特征图数量), m - 核的宽度, n - 核的高度, z - 核的数量(输出特征图数量)。核的实际大小取决于输入规范,因此我们得到 z 个 n x m x d 大小的核。输出的大小将是 ( h - n +1) x ( w - m +1) x z (这里我们假设输入未填充,并且核仅在有效位置应用)。
当我们讨论训练它们时,我们会再次回到卷积层。然而,以上应该能让我们对在推理阶段(计算已训练网络的输出)如何计算输出有一个概念。
ReLU激活函数
要描述的下一个构建块是 ReLU激活函数 。它不是什么新鲜事物,也不是卷积神经网络特有的。然而,随着深度神经网络的兴起,它得到了极大的推广。而这通常是卷积网络的应用场景。
深度神经网络面临的一个问题称为 梯度消失问题 。在使用基于梯度学习算法和反向传播训练人工神经网络时,神经网络的每个权重都会根据误差函数对当前权重的偏导数得到更新。问题在于,在某些情况下,梯度值可能非常小,以至于实际上阻止了权重的数值改变。这个问题的原因之一是使用传统的激活函数,如sigmoid和双曲正切。这些函数在(0, 1)范围内有梯度,在函数域的大部分区域值接近零。由于误差的偏导数是使用链式法则计算的,这意味着对于一个 n 层网络,将有 n 次这些小数值的乘法,意味着梯度随着 n 指数级下降。结果是,深度网络的“前端”层训练非常缓慢,甚至不训练。
ReLU函数定义为 f( x ) = max(0, x )。其最大的优点是对于大于零的 x 值,它具有恒定的导数,等于1。因此,它允许更好的梯度传播,从而加速了更深层人工神经网络的训练。此外,它的计算效率更高,与sigmoid或双曲正切相比,计算速度更快。
![]() ReLU函数 | ![]() Sigmoid函数 |
尽管ReLU函数也有一些 潜在问题 ,但到目前为止,它似乎是深度神经网络中最成功和广泛使用的激活函数。
池化层
在卷积层之后紧跟着一个池化层是一种常见的做法。该层的目的是对前一次卷积生成的输入特征图进行下采样。通过减小输入的空间尺寸,我们也减小了神经网络中的参数量和计算量。这也有助于控制过拟合 - 参数越少,过拟合的可能性越小。
最常见的池化技术是 MAX 2x2滤波器和步长2的池化。对于 n x m 的输入特征图,它通过将输入中的每个2x2区域替换为单个值(该区域4个值中的最大值)来生成 n/2 x m/2 的图。这些区域不重叠,但彼此相邻,因为滤波器以与其大小相等的步长(stride)水平和垂直移动。下面是一个将 MAX 池化应用于6x6输入的示例(彩色单元格突出显示了MAX运算符的源值和相应的resuLT)。
MAX 池化并非唯一的池化技术。另一种常见的技术是 Average 池化,它计算源区域的平均值而不是取其最大值。
池化层也可以使用不同尺寸的滤波器和步长值进行配置。例如,一些应用使用3x3滤波器和步长2。这种配置创建了重叠的池化区域模式,因为滤波器的步长小于其尺寸。然而,步长大于滤波器尺寸是不常见的,因为某些特征可能会完全丢失。
关于池化层要提到的一点是,它们操作的是2D特征图,并且不影响输入的深度。因此,如果输入包含前一个卷积层生成的10个特征图,例如,池化会单独应用于每个图。结果是,它生成相同数量的特征图,但尺寸较小。
构建卷积神经网络
既然我们有了最常见的构建块,就可以将它们组合成一个卷积神经网络。虽然有一些网络架构完全基于卷积层,但这很少见。大多数时候,卷积网络仅以卷积层开始,执行初始特征提取,然后后跟全连接层,执行最终分类。
例如,下面是 LeNet-5 卷积神经网络的架构,该网络由Yann LeCun首次提出,并应用于手写数字的分类。它以32x32的灰度图像作为输入,并产生一个10个值的向量 - 属于特定类别的概率(0到9的数字)。下表总结了网络的架构、层输出的尺寸以及可训练参数(权重+偏置)的数量。
层类型 | 可训练参数 | 输出尺寸 |
输入图像 | 32x32x1 | |
卷积层1,6个5x5大小的核 ReLU激活 | 156 | 28x28x6 |
MAX池化1 | 14x14x6 | |
卷积层2,16个5x5x6大小的核 ReLU激活 | 2416 | 10x10x16 |
MAX池化2 | 5x5x16 | |
卷积层3,120个5x5x16大小的核 ReLU激活 | 48012 | 1x1x120 |
全连接层1,120个输入,84个输出 Sigmoid激活 | 10164 | 84 |
全连接层2,84个输入,10个输出 SoftMax激活 | 850 | 10 |
仅有61598个可训练参数,上述卷积神经网络的结构非常简单。如今,许多更复杂的深度网络正在开发中,其中包含数百万个可训练参数。
训练卷积网络
到目前为止,我们只讨论了卷积神经网络的推理部分,即计算给定输入的输出。然而,网络需要首先进行训练才能获得有意义的结果。在图像处理的卷积算子方面,核通常是手工制作的,并服务于特定目的。一些核用于查找对象的边缘,一些用于使图像锐化或模糊等。设计正确的核来执行所需任务通常是一个耗时的过程。然而,在卷积神经网络中,情况则完全不同。在设计这样的网络时,我们考虑层数、卷积的数量和大小等。但我们不设置这些卷积核。相反,网络将在训练阶段学习这些,因为本质上这些核就是权重 - 与我们在全连接层中的权重相同。
卷积网络的训练使用与训练全连接网络完全相同的算法 - 随机梯度下降和反向传播。正如在 上一篇文章 中所示,为了计算神经网络误差相对于其权重的偏导数,我们可以使用链式法则。它允许我们定义任何可训练层的权重更新的完整方程。然而,这次我们将更多地关注误差反向传播的方面,而不是提供一个包含链式法则所有部分的巨大方程,我们将提供针对神经网络每个构建块的较小方程 - 全连接层和卷积层、激活函数、损失函数等。
如果我们回顾上一篇文章中的链式法则,我们会发现神经网络的每个构建块都计算其误差梯度,即其输出相对于其输入的偏导数,并将其与来自下一个块的误差梯度相乘。请记住,我们是向后移动,因此计算从最后一个块开始,流向前面的块,即第一个块。训练阶段的最后一个块始终是损失函数,因此它计算损失(其输出)相对于神经网络输出(损失函数的输入)的误差梯度。这可以按以下方式定义:
所有其他构建块从下一个块接收误差梯度,并将其与它们自己的输出相对于输入的偏导数相乘。
在描述我们将用于卷积网络的新构建块的导数之前,让我们回顾一下我们用于全连接网络但使用新符号表示的构建块的导数。首先,我们从均方误差(MSE)损失函数相对于网络输出( yi - 网络生成的输出, ti - 目标输出)的误差梯度开始:
现在,当误差梯度向后通过sigmoid激活函数时,它会这样重新计算(这里 oi 是sigmoid的输出),这是来自下一个块的梯度(无论是什么 - 它可以是损失函数或多层网络中的另一个层)乘以sigmoid的导数:
或者,如果使用双曲正切作为激活函数,则使用其导数:
现在我们需要通过全连接层向后传播误差梯度。由于每个输入都连接到每个输出,我们得到一个偏导数之和( n 是全连接层中的神经元数量, j 是输入的索引, i 是输出/神经元的索引):
由于全连接层是可训练层,它不仅需要将误差梯度向后传递给前面的构建块/层,还需要更新其权重。使用上面定义的命名约定,权重和偏置的更新规则可以写成如下(经典SGD):
以上所有方程都是对上一篇文章中反向传播的快速回顾。为什么这很重要?首先是为了提醒基础知识。其次,是为了以不同的方式重写它,其中每个构建块定义自己的误差梯度反向传播方程,该方程独立于其他块。上一篇文章中给出权重更新方程的方式有助于理解基础知识和链式法则的工作原理。但作为一个单一方程,它一点也不通用。如果我们需要的损失函数不同于MSE怎么办?如果我们需要的激活函数是双曲正切或ReLU而不是sigmoid怎么办?本文中的呈现方式使其更加灵活,并允许以各种方式混合人工神经网络的构建块,并在不假设哪个层后面跟着哪个激活函数以及在使用哪个损失函数的情况下对其进行训练(嗯,或多或少)。此外,这种呈现方式与实际的C++实现更加同步,其中不同的构建块被实现为单独的类,负责它们在训练期间的前向传播和后向传播的计算。
**注意**:如果以上内容都不清楚,建议回顾 上一篇文章 。
交叉熵损失函数
卷积神经网络最常见的用途之一是图像分类。给定一张图像,网络需要将其分类到一个互斥的类别中。例如,可以是手写数字分类,我们有10个可能的类别对应于数字0到9。或者网络可以被训练来识别汽车、卡车、船、飞机等对象,因此我们拥有的类别数量与对象类型数量相同。这种分类的主要点是,每个输入图像只能属于一个类别,即我们不能有同时被分类为汽车和飞机的对象。
在处理多类分类问题时,设计的人工神经网络的输出数量与我们拥有的类别数量相同。在训练阶段,目标输出被 独热编码(one-hot encoded) ,即用只有一个元素设置为值“1”且索引与类别对应的零向量表示。例如,对于一个4类分类任务,我们的目标输出可能如下所示:{0, 1, 0, 0} - 第2类,{0, 0, 0, 1} - 第4类等。不允许任何目标输出有多个元素设置为“1”或另一个非零值。这可以看作是目标概率,即 {0, 1, 0, 0} 输出表示输入的样本属于第2类,概率为100%,而属于其他类的概率为0%。
然而,在训练时,实际神经网络的输出将有所不同。它可能提供类似 {0.3, 0.35, 0.25, 0.1} 的输出。这样的输出可能有不同的含义。对于已训练的网络,它可能意味着网络接收到了一个棘手的样本,它不是很清楚,但看起来更像是第2类 - 最高概率为35%。或者,如果我们刚刚开始训练,它可能几乎没有意义,只是“继续前进”。
因此,我们需要一个损失函数,它能告诉我们目标与实际输出之间的差异量,并指导神经网络的参数更新。在处理概率模型关于互斥类别时,我们处理的是预测概率和真实概率。在这种情况下,常见的选择是 交叉熵损失函数 ,它源于信息论。正如其名,通过最小化交叉熵,我们希望最小化使用估计概率 yi (可能接近但并非完全准确)编码出现概率分布 ti (目标或真实分布)的一些事件所需的额外数据(比特)量。并且为了最小化交叉熵,我们需要使我们估计的概率与真实概率相同 - 这正是我们所寻找的。
交叉熵损失函数,我们需要最小化的值,定义如下(与之前相同 - ti 是目标输出,而 yi 是神经网络提供的输出):
得到其导数,成本函数相对于神经网络输出的梯度计算如下:
现在我们有了交叉熵损失函数而不是MSE,所以我们可以继续研究其他构建块,看看误差梯度是如何反向传播的。
SoftMax激活函数
对于用于分类问题的神经网络的最后一层激活函数,我们可以使用sigmoid函数,我们已经在上一篇文章中看到过并在上面快速重复了。它的输出在(0, 1)范围内,因此可以解释为0%到100%之间的概率。当神经网络在输出层使用sigmoid进行训练时,它确实可以提供接近真实值的概率。然而,由于我们处理的是互斥类别,这可能并不总是完全有意义。例如,给定一个具有挑战性的例子,网络可能会提供一个输出向量,例如:{0.6, 0.55, 0.1, 0.1}。是的,看起来是第1类,概率为60%!但是第2类的概率也不远。另一个问题是,如果我们对这四个概率求和,我们得到1.35,即135%。
我们想解决两个问题。首先,我们肯定希望概率之和等于100%。不多不少。而且,如果我们得到一个棘手的例子,看起来像第1类,但也接近第2类,我们真的能有60%的把握说分类是正确的吗?
为了解决上述两个问题,我们可以使用一个不同的激活函数,即 SoftMax 。与sigmoid一样,它提供(0, 1)范围内的输出。但与sigmoid不同的是,它不操作输入向量的单个值,而是操作整个向量,从而确保输出向量的总和等于1。SoftMax函数定义如下:
如果我们使用SoftMax函数代替上述示例中的sigmoid(你可以使用反向sigmoid来查找源输入值),输出向量将不同,并且更有意义 - {0.316, 0.3, 0.192, 0.192}。正如我们所见,所有值的总和等于1,即100%。即使第1类似乎获胜,但其概率并不高 - 仅为31.6%。
对于任何其他激活函数,我们需要定义SoftMax函数的梯度反向传播方程。这是:
现在继续向后通过LeNet-5神经网络的架构,我们看到全连接层和sigmoid激活函数。上面已经定义了它们的方程。现在是时候解决本文中引入的其他构建块了。
ReLU激活函数
正如上面已经提到的,ReLU激活函数已成为深度神经网络非常流行的选择,因为它允许更有效的梯度传播通过网络。这都归功于其恒定的梯度,对于大于零的输入值等于1。为了完成ReLU激活,我们还需要定义其梯度反向传播的方程。
池化层
现在是时候将误差梯度向后传播通过池化层了。为简单起见,让我们假设我们使用2x2的核和步长2,并且不使用输入填充(我们只在有效位置应用池化)。考虑到这一点,这意味着输出特征图的每个值都是基于输入特征图的4个值计算的。
虽然池化层假定输入向量代表2D数据,但下面的数学运算将适用于1D向量的输入/输出。为了让一切正常工作,我们将定义一个 i2j() 函数,该函数对于输入向量的给定索引 i 返回输出向量的相应索引 j 。由于每个输出是基于4个输入值计算的,这意味着存在4个输入索引,对于这些索引 i2j() 将返回相同的输出索引。
让我们从 Max Pooling 开始。为了定义误差梯度反向传播的方程,我们需要一个额外的东西。在前向传播过程中,当计算神经网络的输出时,池化层还将填充与输出向量相同长度的 maxIndexes 向量。但是,如果输出向量包含相应输入值的最大值,则 maxIndexes 向量包含最大值的索引。有了以上所有内容,我们可以定义Max Pooling层的梯度反向传播方程:
至于 Average Pooling ,它甚至更简单 - 来自前一个块的误差梯度简单地除以池化核的大小,在本例中是4。
卷积层
最后,是时候为卷积层定义反向传播过程了。只要考虑到共享权重的这一事实,它与全连接层并没有太大区别。
那么,让我们开始更新卷积层的权重。对于全连接层,这很简单 - 误差关于权重 wi,j 的偏导数等于来自下一个块的误差梯度乘以相应的输入值 - δi(k+1)xj 。这样做的原因是,全连接层中的每个输入/输出连接都有自己的权重,这些权重不是共享的。然而,卷积层的情况并非如此。下图演示了卷积核的每个权重都用于许多输入/输出连接。在下面的例子中,突出的核权重各使用了9次 - 核在输入图像的9个不同位置应用。因此,误差关于权重的偏导数也需要有9个项 - 使用该权重的次数。
与池化层一样,我们在这里忽略了卷积层处理2D/3D数据的事实。相反,我们目前假设输入/输出/核是普通的向量/数组(最终它们在C++中就是这样)。因此,对于上面的例子,第一个核的权重(红色突出显示)应用于输入 {1, 2, 3, 5, 6, 7, 9, 10, 11, 13, 14, 15},而第四个权重应用于输入 {6,7,8,10,11,12,14,15,16}。假设我们有一个名为 weightInputsi 的向量,其中包含每个权重使用的输入索引。此外,我们将定义一个接受两个参数的函数 i2o(i,j) ,它提供第 i 个权重的输出值索引和第 j 个输入。这里是一些上面图片的例子:i2o(1,1)=1, i2o(4,6)=1, i2o(1, 11)=9 和 i2o(4,16)=9。使用上述命名约定,卷积网络的权重更新规则可以定义如下:
上面的内容有意义吗?嗯,你思考得越多,就会越觉得有意义。我们所做的只是获取所有输出的误差梯度(因为每个核的权重都用于计算所有输出),并乘以相应的输入。是的,我们有多个核。但是,它们都以相同的模式应用,所以即使我们需要更新不同核的权重, weightInputs 向量保持不变。然而, i2o(i,j) 是每个核特有的。或者可以通过额外的参数 - 核索引来扩展它。
更新偏置值要简单得多。由于每个核/偏置用于计算每个输出值,我们将所有由该核生成的特征图的误差梯度相加。
**注意**:上面的两个方程都是针对每个特征图/核的,即权重和偏置值在那里没有用核索引参数化。
现在是时候得到卷积层的最终方程了,它用于将误差梯度向后传播通过网络。这意味着计算误差相对于层输入的偏导数。每个输入元素可以被使用多次来生成特征图的输出值。它可以被使用尽可能多的次数,等于卷积核中的元素数量(权重数量)。有些输入只能用于一个输出,尽管如此。例如,那些是输入2D特征图角落的输入。但我们也需要牢记,每个输入特征图可以被不同的核处理多次,这些核会生成更多的输出图。同样,让我们暂时假装一切都是扁平的,没有2D/3D索引。那么,假设我们有另一组名为 inputOutputsi 的辅助向量,它们保存第 i 个输入贡献的输出索引。最后,我们需要 i2w(i, j) 函数,它提供用于连接第 i 个输入和第 j 个输出的权重的索引。这里再次给出上面图片的几个例子:i2w(1, 1)=1, i2w(6,1)=4, i2w(16,9)=4。有了所有这些,我们可以定义通过卷积层向后传播误差梯度的方程。
现在数学似乎完成了 - 我们拥有计算卷积网络前向传播和后向传播所需的一切。如果它仍然让你困惑、感到困惑或留下一些不确定性,请再次回顾所有内容,思考一下。或者深入代码,看看数学与实现的联系。
ANNT库
ANNT库中卷积人工神经网络的实现很大程度上基于上一篇文章中描述的全连接网络实现所设定的设计。所有核心类都保持不变,只实现了新的构建块,这些构建块允许将它们构建成卷积神经网络。下面显示了库的新类图 - 差异不大。
与之前设置的方式类似,新的构建块负责计算前向传播的输出,并在后向传播中传播误差梯度(以及在可训练层的情况下计算初始权重更新)。结果是,神经网络训练的所有代码都保持不变。
并且,与代码的其他部分一样,新的构建块在可能的地方利用SIMD指令对计算进行向量化,并使用OpenMP进行并行化。
构建代码
代码附带MSVC(2015版本)解决方案文件和GCC make文件。使用MSVC解决方案非常简单 - 每个示例的解决方案文件都包含示例本身的工程以及库。所以MSVC选项就像打开所需示例的解决方案文件并点击构建按钮一样简单。如果使用GCC,则需要先构建库,然后通过运行 make 来构建所需的示例应用程序。
使用示例
在对卷积神经网络的理论和数学进行了冗长的讨论之后,是时候付诸实践,实际构建一些用于图像分类任务的网络了 - 手写数字和各种对象,如汽车、卡车、船、飞机等。 **注意**:这些示例中的任何一个都不能声称其展示的网络架构是其任务的最佳选择。实际上,这些示例中的任何一个甚至都没有说人工神经网络是可行的方法。相反,它们唯一的目的是演示如何使用该库。
**注意**:下面的代码片段只是示例应用程序的一小部分。要查看示例的完整代码,请参阅文章随附的源代码包(其中还包括上一篇文章中描述的全连接神经网络的示例)。
MNIST手写数字分类
第一个要看的是 MNIST数据库 中手写数字的分类。该数据库包含60000个用于神经网络训练的样本,以及另外10000个用于训练后网络测试的样本。下图展示了一些不同数字的分类示例。
本例中使用的卷积神经网络的结构与上面提到的LeNet-5网络非常相似。不同之处在于,我们将使用一个稍小一些的网络(好吧,实际上小了很多,如果我们看需要训练的权重数量),它只有一个全连接网络。这是我们将使用的网络结构:
Conv(32x32x1, 5x5x6 ) -> ReLU -> AvgPool(2x2) Conv(14x14x6, 5x5x16 ) -> ReLU -> AvgPool(2x2) Conv(5x5x16, 5x5x120) -> ReLU FC(120, 10) -> SoftMax
上面的配置告诉了每个卷积层的输入尺寸以及它们执行的卷积的尺寸和数量。对于全连接层,它告诉输入和输出的数量。那么,让我们创建上述结构的卷积神经网络。
// connection table to specify wich feature maps of the first convolution layer
// to use for feature maps produced by the second layer
vector<bool> connectionTable( {
true, true, true, false, false, false,
false, true, true, true, false, false,
false, false, true, true, true, false,
false, false, false, true, true, true,
true, false, false, false, true, true,
true, true, false, false, false, true,
true, true, true, true, false, false,
false, true, true, true, true, false,
false, false, true, true, true, true,
true, false, false, true, true, true,
true, true, false, false, true, true,
true, true, true, false, false, true,
true, true, false, true, true, false,
false, true, true, false, true, true,
true, false, true, true, false, true,
true, true, true, true, true, true
} );
// prepare a convolutional ANN
shared_ptr<XNeuralNetwork> net = make_shared<XNeuralNetwork>( );
net->AddLayer( make_shared<XConvolutionLayer>( 32, 32, 1, 5, 5, 6 ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XAveragePooling>( 28, 28, 6, 2 ) );
net->AddLayer( make_shared<XConvolutionLayer>( 14, 14, 6, 5, 5, 16, connectionTable ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XAveragePooling>( 10, 10, 16, 2 ) );
net->AddLayer( make_shared<XConvolutionLayer>( 5, 5, 16, 5, 5, 120 ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XFullyConnectedLayer>( 120, 10 ) );
net->AddLayer( make_shared<XLogSoftMaxActivation>( ) );
从上面的代码来看,神经网络的上述配置如何转化为代码是很清楚的。除了一个问题 - “我们从第一个卷积层到第二个卷积层之间获得了什么样的连接表?”是的,理论部分没有提到这一点,但很容易理解。从网络的结构和代码可以看出,第一层执行6次卷积,因此生成6个特征图。而第二层执行16次卷积。在某些情况下,希望将层卷积配置为仅对输入特征图的子集进行操作。如上面的代码所示,第二层的第一个6次卷积使用第一层生成的3个特征图的不同模式。然后接下来的9次卷积使用4个特征图的不同模式。最后,最后一次卷积使用第一层的所有6个特征图。这样做是为了减少需要训练的参数数量,并确保第二层不同的特征图不是都基于相同的输入特征图。
当创建卷积网络后,我们可以执行与全连接网络相同 else - 创建一个训练上下文,指定损失函数和权重优化器,然后将所有内容传递给一个辅助类,该类运行训练/验证循环并以测试完成。
// create training context with Adam optimizer and Negative Log Likelihood cost function (since we use Log-Softmax)
shared_ptr<XNetworkTraining> netTraining = make_shared<XNetworkTraining>( net,
make_shared<XAdamOptimizer>( 0.002f ),
make_shared<XNegativeLogLikelihoodCost>( ) );
// using the helper for training ANN to do classification
XClassificationTrainingHelper trainingHelper( netTraining, argc, argv );
trainingHelper.SetValidationSamples( validationImages, encodedValidationLabels, validationLabels );
trainingHelper.SetTestSamples( testImages, encodedTestLabels, testLabels );
// 20 epochs, 50 samples in batch
trainingHelper.RunTraining( 20, 50, trainImages, encodedTrainLabels, trainLabels );
下面的应用程序样本输出显示了训练进度和最终结果 - 测试数据的分类准确率。我们获得了99.01%的准确率,这似乎比上一篇文章中的全连接神经网络有了很大的改进,后者在96.55%的准确率。
MNIST handwritten digits classification example with Convolution ANN Loaded 60000 training data samples Loaded 10000 test data samples Samples usage: training = 50000, validation = 10000, test = 10000 Learning rate: 0.0020, Epochs: 20, Batch Size: 50 Before training: accuracy = 5.00% (2500/50000), cost = 2.3175, 34.324s Epoch 1 : [==================================================] 123.060s Training accuracy = 97.07% (48536/50000), cost = 0.0878, 32.930s Validation accuracy = 97.49% (9749/10000), cost = 0.0799, 6.825s Epoch 2 : [==================================================] 145.140s Training accuracy = 97.87% (48935/50000), cost = 0.0657, 36.821s Validation accuracy = 97.94% (9794/10000), cost = 0.0669, 5.939s ... Epoch 19 : [==================================================] 101.305s Training accuracy = 99.75% (49877/50000), cost = 0.0077, 26.094s Validation accuracy = 98.96% (9896/10000), cost = 0.0684, 6.345s Epoch 20 : [==================================================] 104.519s Training accuracy = 99.73% (49865/50000), cost = 0.0107, 28.545s Validation accuracy = 99.02% (9902/10000), cost = 0.0718, 7.885s Test accuracy = 99.01% (9901/10000), cost = 0.0542, 5.910s Total time taken : 3187s (53.12min)
CIFAR10图像分类
第二个示例是对来自 CIFAR-10数据集 的彩色32x32图像进行分类。它包含60000张图像,其中50000张用于训练,另外10000张用于测试。图像被分为以下10类:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。下面是一些示例。
正如上图所示,CIFAR-10数据集比MNIST手写数字复杂得多。首先,图像是彩色的。其次,它们不那么明显。以至于如果我没有被告知这是一只狗,我不会自己说。结果是,网络的结构稍微变大了。不是说它变得更深了,而是执行的卷积和训练的权重数量在增长。下面是网络的结构:
Conv(32x32x3, 5x5x32, BorderMode::Same) -> ReLU -> MaxPool -> BatchNorm Conv(16x16x32, 5x5x32, BorderMode::Same) -> ReLU -> MaxPool -> BatchNorm Conv(8x8x32, 5x5x64, BorderMode::Same) -> ReLU -> MaxPool -> BatchNorm FC(1024, 64) -> ReLU -> BatchNorm FC(64, 10) -> SoftMax
将上述神经网络结构转换为代码会得到如下结果。 **注意**:由于ReLU(MaxPool)与MaxPool(ReLU)产生相同的结果,我们使用前者,因为它将ReLU计算减少了75%(尽管与其他网络部分相比可以忽略不计)。
// prepare a convolutional ANN
shared_ptr<XNeuralNetwork> net = make_shared<XNeuralNetwork>( );
net->AddLayer( make_shared<XConvolutionLayer>( 32, 32, 3, 5, 5, 32, BorderMode::Same ) );
net->AddLayer( make_shared<XMaxPooling>( 32, 32, 32, 2 ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XBatchNormalization>( 16, 16, 32 ) );
net->AddLayer( make_shared<XConvolutionLayer>( 16, 16, 32, 5, 5, 32, BorderMode::Same ) );
net->AddLayer( make_shared<XMaxPooling>( 16, 16, 32, 2 ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XBatchNormalization>( 8, 8, 32 ) );
net->AddLayer( make_shared<XConvolutionLayer>( 8, 8, 32, 5, 5, 64, BorderMode::Same ) );
net->AddLayer( make_shared<XMaxPooling>( 8, 8, 64, 2 ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XBatchNormalization>( 4, 4, 64 ) );
net->AddLayer( make_shared<XFullyConnectedLayer>( 4 * 4 * 64, 64 ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XBatchNormalization>( 64, 1, 1 ) );
net->AddLayer( make_shared<XFullyConnectedLayer>( 64, 10 ) );
net->AddLayer( make_shared<XLogSoftMaxActivation>( ) );
示例应用程序的其余部分遵循其他分类示例设定的相同模式 - 使用所需的损失函数和权重优化器创建训练上下文,并将其传递给辅助类以运行训练循环。下面是其输出示例。
CIFAR-10 dataset classification example with Convolutional ANN Loaded 50000 training data samples Loaded 10000 test data samples Samples usage: training = 43750, validation = 6250, test = 10000 Learning rate: 0.0010, Epochs: 20, Batch Size: 50 Before training: accuracy = 9.91% (4336/43750), cost = 2.3293, 844.825s Epoch 1 : [==================================================] 1725.516s Training accuracy = 48.25% (21110/43750), cost = 1.9622, 543.087s Validation accuracy = 47.46% (2966/6250), cost = 2.0036, 77.284s Epoch 2 : [==================================================] 1742.268s Training accuracy = 54.38% (23793/43750), cost = 1.3972, 568.358s Validation accuracy = 52.93% (3308/6250), cost = 1.4675, 76.287s ... Epoch 19 : [==================================================] 1642.750s Training accuracy = 90.34% (39522/43750), cost = 0.2750, 599.431s Validation accuracy = 69.07% (4317/6250), cost = 1.2472, 81.053s Epoch 20 : [==================================================] 1708.940s Training accuracy = 91.27% (39931/43750), cost = 0.2484, 578.551s Validation accuracy = 69.15% (4322/6250), cost = 1.2735, 81.037s Test accuracy = 68.34% (6834/10000), cost = 1.3218, 122.455s Total time taken : 48304s (805.07min)
如上所述,CIFAR-10数据集肯定更复杂。如果我们设法在MNIST数据集上获得了99%的测试准确率,那么在这里我们甚至无法接近 - 训练集上的准确率约为91%,测试/验证集上的准确率约为68-69%。此外,运行20个epoch花了13个小时。仅使用CPU肯定不足以满足卷积网络的需求。
结论
在本文中,我们涵盖了ANNT库的新扩展,这些扩展允许构建卷积神经网络。目前,它只允许构建简单的网络(或多或少),其中网络的层按顺序排列。构建更高级的流行架构,它们更像计算图,目前还不支持。然而,在此之前,还需要实现其他功能。正如CIFAR-10示例所示,一旦神经网络变得更大,它就需要更多的计算能力来进行训练。而在这里,仅使用CPU是不够的。如今,GPU支持是深度学习的必备条件。因此,这个功能将比支持复杂网络具有更高的优先级。
既然全连接和卷积神经网络都已涵盖,下一步将是介绍一些常见的循环网络架构,这是下一篇文章的主题。同时,最新的代码可以在 GitHub 上找到,随着库的不断发展,它将获得更新。