鱼书(深度学习入门):第七章 卷积神经网络
本章的主题是卷积神经网络(Convolutional Neural Network,CNN)。深度学习的方法几乎都以CNN为基础。本章将详细介绍CNN的结构,并用Python实现其处理内容。
一、整体结构
首先,来看一下CNN的网络结构,了解CNN的大致框架。CNN和之前介绍的神经网络一样,可以像乐高积木一样通过组装层来构建。不过,CNN中新出现了卷积层(Convolution层)和池化层(Pooling层)。这里我们先看一下如何组装层以构建CNN。
之前介绍的神经网络中,相邻层的所有神经元之间都有连接,这称为全连接(fully-connected)。另外,我们用Affine层实现了全连接层。如果使用这个Affine层,一个5层的全连接的神经网络就可以通过下图所示的网络结构来实现。

如上图所示,全连接的神经网络中,Affine层后面跟着激活函数ReLU层(或者Sigmoid层)。这里堆叠了4层“Affine-ReLU”组合,然后第5层是Affine层,最后由Softmax层输出最终结果(概率)。
CNN的结构如下图所示。

如上图所示,CNN中新增了Convolution层和Pooling层。CNN的层的连接顺序是“Convolution - ReLU -(Pooling)”(Pooling层有时会被省略)。这可以理解为之前的“Affine - ReLU”连接被替换成了“Convolution - ReLU -(Pooling)”连接。
还需要注意的是,在上图的CNN中,靠近输出的层中使用了之前 的“Affine - ReLU”组合。此外,最后的输出层中使用了之前的“Affine - Softmax”组合。这些都是一般的CNN中比较常见的结构。
二、卷积层
1.全连接层存在的问题
之前介绍的全连接的神经网络中使用了全连接层(Affine层)。在全连接层中,相邻层的神经元全部连接在一起,输出的数量可以任意决定。全连接层存在什么问题呢?那就是数据的形状被“忽视”了。全连接层输入时,需要将3维数据拉平为1维数据。图像是3维形状,这个形状中应该含有重要的空间信息。但是,因为全连接层会忽视形状,将全部的输入数据作为相同的神经元(同一维度的神经元)处理,所以无法利用与形状相关的信息。
而卷积层可以保持形状不变。当输入数据是图像时,卷积层会以3维数据的形式接收输入数据,并同样以3维数据的形式输出至下一层。因此, 在CNN中,可以(有可能)正确理解图像等具有形状的数据。
另外,CNN 中,有时将卷积层的输入输出数据称为特征图(feature map)。其中,卷积层的输入数据称为输入特征图(input feature map),输出数据称为输出特征图(output feature map)。
2.卷积运算
卷积层进行的处理就是卷积运算。卷积运算相当于图像处理中的“滤波器运算”。我们来看一个具体的例子。

如上图所示,卷积运算对输入数据应用滤波器。在这个例子中,输入数据是有高长方向的形状的数据,滤波器也一样,有高长方向上的维度。假设用(height, width)表示数据和滤波器的形状,则在本例中,输入大小是 (4, 4),滤波器大小是(3, 3),输出大小是(2, 2)。另外,有的文献中也会用“核” 这个词来表示这里所说的“滤波器”。
现在来解释一下上图的卷积运算的例子中都进行了什么样的计算。下图中展示了卷积运算的计算顺序。

对于输入数据,卷积运算以一定间隔滑动滤波器的窗口并应用。这里所说的窗口是指上图中灰色的3 × 3的部分。将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和(有时将这个计算称为乘积累加运算)。然后,将这个结果保存到输出的对应位置。将这个过程在所有位置都进行一遍,就可以得到卷积运算的输出。在全连接的神经网络中,除了权重参数,还存在偏置。CNN中,滤波器的参数就对应之前的权重。并且,CNN中也存在偏置。包含偏置的卷积运算的处理流如下图所示。

如上图所示,向应用了滤波器的数据加上了偏置。偏置通常只有1个 (1 × 1)(本例中,相对于应用了滤波器的4个数据,偏置只有1个),这个值会被加到应用了滤波器的所有元素上。
3.填充
在进行卷积层的处理之前,有时要向输入数据的周围填入固定的数据(比如0等),这称为填充(padding),是卷积运算中经常会用到的处理。在下图的例子中,对大小为(4, 4)的输入数据应用了幅度为1的填充。“幅度为1的填充”是指用幅度为1像素的0填充周围。

如上图所示,通过填充,大小为(4, 4)的输入数据变成了(6, 6)的形状。 然后,应用大小为(3, 3)的滤波器,生成了大小为(4, 4)的输出数据。这个例子中将填充设成了1,不过填充的值也可以设置成2、3等任意的整数。
使用填充主要是为了调整输出的大小。因为如果每次进行卷积运算都会缩小空间,那么在某个时刻输出大小就有可能变为 1,导致无法再应用卷积运算。为了避免出现这样的情况,就要使用填充。在刚才的例子中,将填充的幅度设为 1,那么相对于输入大小(4, 4),输出大小也保持为原来的(4, 4)。因此,卷积运算就可以在保持空间大小不变的情况下将数据传给下一层。
4.步幅
应用滤波器的位置间隔称为步幅(stride)。之前的例子中步幅都是1,如果将步幅设为2,则如下图所示,应用滤波器的窗口的间隔变为2个元素。

步幅可以指定应用滤波器的间隔。增大步幅后,输出大小会变小。而增大填充后,输出大小会变大。我们可以将这样的关系写成算式。接下来,我们看一下对于填充和步幅,如何计算输出大小。这里,假设输入大小为(H, W),滤波器大小为(FH, FW),输出大小为(OH, OW),填充为P,步幅为S。此时,输出大小可通过下式进行计算。

这里需要注意的是,虽然只要代入值就可以计算输出大小,但是所设定的值必须使两个式子分别可以除尽。当输出大小无法除尽时(结果是小数时),需要采取报错等对策。根据深度学习的框架的不同,当值无法除尽时,有时会向最接近的整数四舍五入,不进行报错而继续运行。
5.3维数据的卷积运算
之前的卷积运算的例子都是以有高、长方向的2维形状为对象的。但是,
图像是3维数据,除了高、长方向之外,还需要处理通道方向。这里,我们按照与之前相同的顺序,看一下对加上了通道方向的3维数据进行卷积运算的例子。
这里以3通道的数据为例, 展示了卷积运算的结果。和2维数据时相比,可以发现纵深方向(通道方向)上特征图增加了。通道方向上有多个特征图时,会按通道进行输入数据和滤波器的卷积运算,并将结果相加,从而得到输出。其运算顺序如下图所示。

需要注意的是,在3维数据的卷积运算中,输入数据和滤波器的通道数要设为相同的值。滤波器大小可以设定为任意值(不过,每个通道的滤波器大小要全部相同)。
6.结合方块思考
将数据和滤波器结合长方体的方块来考虑,3维数据的卷积运算会很容易理解。方块是如下图所示的3维长方体。把3维数据表示为多维数组时,书写顺序为(channel, height, width)。比如,通道数为C、高度为H、
长度为W的数据的形状可以写成(C, H, W)。滤波器也一样,要按(channel,
height, width)的顺序书写。比如,通道数为C、滤波器高度为FH(Filter
Height)、长度为FW(Filter Width)时,可以写成(C, FH, FW)。

在这个例子中,数据输出是1张特征图。所谓1张特征图,换句话说,就是通道数为1的特征图。那么,如果要在通道方向上也拥有多个卷积运算的输出,该怎么做呢?为此,就需要用到多个滤波器(权重)。用图表示的话, 如下图所示。

上图中,通过应用FN个滤波器,输出特征图也生成了FN个。如果将这FN个特征图汇集在一起,就得到了形状为(FN, OH, OW)的方块。将这个方块传给下一层,就是CNN的处理流。关于卷积运算的滤波器,也必须考虑滤波器的数量。因此,作为4维数据,滤波器的权重数据要按(output_channel, input_ channel, height, width)的顺序书写。比如,通道数为3、大小为5 × 5的滤波器有20个时,可以写成(20, 3, 5, 5)。
卷积运算中(和全连接层一样)存在偏置。在上图的例子中,如果进一步追加偏置的加法运算处理,则结果如下图所示。

上图中,每个通道只有一个偏置。这里,偏置的形状是(FN, 1, 1), 滤波器的输出结果的形状是(FN, OH, OW)。这两个方块相加时,要对滤波器的输出结果(FN, OH, OW)按通道加上相同的偏置值。其可以通过Numpy的广播机制实现。
7.批处理
神经网络的处理中进行了将输入数据打包的批处理。通过批处理,能够实现处理的高效化和学习时对mini-batch的对应。我们希望卷积运算也同样对应批处理。为此,需要将在各层间传递的数据保存为4维数据。具体地讲,就是按(batch_num, channel, height, width)
的顺序保存数据。例如改成对N个数据进行批处理时,其数据形状如下图所示。

上图的批处理版的数据流中,在各个数据的开头添加了批用的维度。像这样,数据作为4维的形状在各层间传递。这里需要注意的是,网络间传递的是4维数据,对这N个数据进行了卷积运算。也就是说,批处理将N次的处理汇总成了1次进行。
三、池化层
池化是缩小高、长方向上的空间的运算。比如,如下图所示,进行将2 × 2的区域集约成1个元素的处理,缩小空间大小。

上图的例子是按步幅2进行2 × 2的Max池化时的处理顺序。“Max 池化”是获取最大值的运算,“2 × 2”表示目标区域的大小。此外,这个例子中将步幅设为了2,一般来说,池化的窗口大小会和步幅设定成相同的值。
除了Max池化之外,还有Average池化等。相对于Max池化是从目标区域中取出最大值,Average池化则是计算目标区域的平均值。在图像识别领域,主要使用Max池化。因此,此处说到“池化层” 时,指的是Max池化。
池化层有以下特征。
没有要学习的参数 池化层和卷积层不同,没有要学习的参数。池化只是从目标区域中取最大值(或者平均值),所以不存在要学习的参数。
通道数不发生变化 经过池化运算,输入数据和输出数据的通道数不会发生变化。如下图所示,计算是按通道独立进行的。

对微小的位置变化具有鲁棒性(健壮)输入数据发生微小偏差时,池化仍会返回相同的结果。因此,池化对输入数据的微小偏差具有鲁棒性。如下图所示,池化会吸收输入数据的偏差(根据数据的不同,结果有可能不一致)。

四、卷积层和池化层的实现
前面我们详细介绍了卷积层和池化层,本节我们就用Python来实现这两个层。和第5章一样,也给进行实现的类赋予forward和backward方法,并使其可以作为模块使用。通过使用某
种技巧,就可以很轻松地实现卷积层和池化层。本节将介绍这种技巧,将问题简化,然后再
进行卷积层的实现。
1.4维数组
如前所述,CNN中各层间传递的数据是4维数据。所谓4维数据,比如数据的形状是(10, 1, 28, 28),则它对应10个高为28、长为28、通道为1的数据。如下所示。


CNN中处理的是4维数据,因此卷积运算的实现看上去会很复杂,但是通过使用下面要介绍的im2col这个技巧,问题就会变得很简单。
2.基于 im2col的展开
im2col是一个函数,将输入数据展开以适合滤波器(权重)。如下图所示,对3维的输入数据应用im2col后,数据转换为2维矩阵(正确地讲,是把包含批数量的4维数据转换成了2维数据)。

im2col会把输入数据展开以适合滤波器(权重)。具体地说,如下图所示,对于输入数据,将应用滤波器的区域(3维方块)横向展开为1列。im2col会在所有应用滤波器的地方进行这个展开处理。

在上图中,为了便于观察,将步幅设置得很大,以使滤波器的应用区域不重叠。在实际的卷积运算中,滤波器的应用区域几乎都是重叠的。在滤波器的应用区域重叠的情况下,使用im2col展开后,展开后的元素个数会多于原方块的元素个数。因此,使用im2col的实现存在比普通的实现消耗更 多内存的缺点。但是,汇总成一个大的矩阵进行计算,对计算机的计算颇有益处。
使用im2col展开输入数据后,之后就只需将卷积层的滤波器(权重)纵向展开为1列,并计算2个矩阵的乘积即可。这和全连接层的
Affine层进行的处理基本相同。
如下图所示,基于im2col方式的输出结果是2维矩阵。因为CNN中数据会保存为4维数组,所以要将2维输出数据转换为合适的形状。以上就是卷积层的实现流程。

3.卷积层的实现
此处提供了im2col函数,并将这个im2col函数作为黑盒(不关心内部实现)使用。im2col会考虑滤波器大小、步幅、填充,将输入数据展开为2维数组。im2col这一便捷函数具有以下接口。

下面是两个使用的例子。

现在使用im2col来实现卷积层。这里我们将卷积层实现为名为Convolution的类。
卷积层的初始化方法将滤波器(权重)、偏置、步幅、填充作为参数接收。滤波器是 (FN, C, FH, FW)的4维形状。这里用粗体字表示Convolution层的实现中的重要部分。在这些粗体字部分,用im2col展开输入数据,并用reshape将滤波器展开为2维数组。然后,计算展开后的矩阵的乘积。
这里通过reshape(FN,-1)将参数指定为-1,这是reshape的一个便利的功能。通过在reshape时指定为-1,reshape函数会自动计算-1维度上的元素个数,以使多维数组的元素个数前后一致。forward的实现中,最后会将输出大小转换为合适的形状。转换时使用了NumPy的transpose函数。transpose会更改多维数组的轴的顺序。如下图所示,通过指定从0开始的索引(编号)序列,就可以更改轴的顺序。

以上就是卷积层的forward处理的实现。通过使用im2col进行展开,基本上可以像实现全连接层的Affine层一样来实现。接下来是卷积层的反向传播的实现,因为和Affine层的实现有很多共通的地方,所以就不再介绍了。但有一点需要注意,在进行卷积层的反向传播时,必须进行im2col 的逆处理。这可以使用本书提供的col2im函数来进行。除了使用col2im这一点,卷积层的反向传播和Affi ne层的实现方式都一样。
4.池化层的实现
池化层的实现和卷积层相同,都使用im2col展开输入数据。不过,池化的情况下,在通道方向上是独立的,这一点和卷积层不同。具体地讲,如下图所示,池化的应用区域按通道单独展开。

像这样展开之后,只需对展开的矩阵求各行的最大值,并转换为合适的形状即可,如下图所示。

如上图所示,池化层的实现按下面3个阶段进行。1.展开输入数据。2.求各行的最大值。3.转换为合适的输出大小。各阶段的实现都很简单,只有一两行代码。
下面来看一下Python的实现代码。
值得注意的是,最大值的计算可以使用 NumPy 的 np.max方法。np.max可以指定axis参数,并在这个参数指定的各个轴方向上求最大值。
以上就是池化层的forward处理的介绍。如上所述,通过将输入数据展开为容易进行池化的形状,后面的实现就会变得非常简单。另外,池化层的backward处理可以参考ReLU层的实现中使用的max的反向传播。
五、CNN的实现
我们已经实现了卷积层和池化层,现在来组合这些层,搭建进行手写数字识别的CNN。这里要实现如下图所示的CNN。

如上图所示,网络的构成是“Convolution - ReLU - Pooling -Affine - ReLU - Affine - Softmax”,我们将它实现为名为SimpleConvNet的类。
首先来看一下SimpleConvNet的初始化(__init__),取下面这些参数。

这里,卷积层的超参数通过名为conv_param的字典传入。我们设想它会像{'filter_num':30,'filter_size':5, 'pad':0, 'stride':1}这样,保存必要的超参数值。
SimpleConvNet的初始化的实现稍长,我们分成3部分来说明,首先是初始化的最开始部分。
这里将由初始化参数传入的卷积层的超参数从字典中取了出来(以方便后面使用),然后,计算卷积层的输出大小。接下来是权重参数的初始化部分。
学习所需的参数是第1层的卷积层和剩余两个全连接层的权重和偏置。 将这些参数保存在实例变量的params字典中。最后,生成必要的层。
从最前面开始按顺序向有序字典(OrderedDict)的layers中添加层。只 有最后的SoftmaxWithLoss层被添加到别的变量lastLayer中。
以上就是SimpleConvNet的初始化中进行的处理。像这样初始化后,进行推理的predict方法和求损失函数值的loss方法就可以像下面这样实现。
用于推理的predict方法从头开始依次调用已添加的层,并将结果传递给下一层。在求损失函数的loss方法中,除了使用 predict方法进行的 forward处理之外,还会继续进行forward处理,直到到达最后的SoftmaxWithLoss层。
接下来是基于误差反向传播法求梯度的代码实现。
参数的梯度通过误差反向传播法(反向传播)求出,通过把正向传播和反向传播组装在一起来完成。因为已经在各层正确实现了正向传播和反向传播的功能,所以这里只需要以合适的顺序调用即可。最后,把各个权重参数的梯度保存到grads字典中。这就是SimpleConvNet的实现。
如上所述,卷积层和池化层是图像识别中必备的模块。CNN可以有效读取图像中的某种特性,在手写数字识别中,还可以实现高精度的识别。
六、CNN的可视化
本节将通过卷积层的可视化,
探索CNN中到底进行了什么处理。
1.第1层权重的可视化
这里,我们来比较一下学习前和学习后的权重,结果如下图所示。

上图中,学习前的滤波器是随机进行初始化的,所以在黑白的浓淡上没有规律可循,但学习后的滤波器变成了有规律的图像。我们发现,通过学习,滤波器被更新成了有规律的滤波器,比如从白到黑渐变的滤波器、含有块状区域(称为blob)的滤波器等。
如果要问上图中右边的有规律的滤波器在“观察”什么,答案就是它在观察边缘(颜色变化的分界线)和斑块(局部的块状区域)等。比如,左半部分为白色、右半部分为黑色的滤波器的情况下,如下图所示,会对垂直方向上的边缘有响应。

上图中显示了选择两个学习完的滤波器对输入图像进行卷积处理时的结果。我们发现“滤波器1”对垂直方向上的边缘有响应,“滤波器2”对水平方向上的边缘有响应。
由此可知,卷积层的滤波器会提取边缘或斑块等原始信息。而刚才实现的CNN会将这些原始信息传递给后面的层。
2.基于分层结构的信息提取
上面的结果是针对第1层的卷积层得出的。第1层的卷积层中提取了边缘或斑块等“低级”信息,那么在堆叠了多层的CNN中,各层中又会提取什么样的信息呢?根据深度学习的可视化相关的研究,随着层次加深,提取的信息(正确地讲,是反映强烈的神经元)也越来越抽象。
下图中展示了进行一般物体识别(车或狗等)的8层CNN。这个网络结构的名称是下一节要介绍的AlexNet。AlexNet网络结构堆叠了多层卷积层和池化层,最后经过全连接层输出结果。下图的方块表示的是中间数据, 对于这些中间数据,会连续应用卷积运算。

如上图所示,如果堆叠了多层卷积层,则随着层次加深,提取的信息也愈加复杂、抽象,这是深度学习中很有意思的一个地方。最开始的层对简单的边缘有响应,接下来的层对纹理有响应,再后面的层对更加复杂的物体部件有响应。也就是说,随着层次加深,神经元从简单的形状向“高级”信息变化。换句话说,就像我们理解东西的“含义”一样,响应的对象在逐渐变化。
七、具有代表性的 CNN
这里,我们介绍其中特别重要的两个网络,一个是在1998年首次被提出的CNN元祖LeNet,
另一个是在深度学习受到关注的2012年被提出的AlexNet。
1.LeNet
LeNet在1998年被提出,是进行手写数字识别的网络。如下图所示,它有连续的卷积层和池化层(正确地讲,是只“抽选元素”的子采样层),最后经全连接层输出结果。

和“现在的CNN”相比,LeNet有几个不同点。第一个不同点在于激活函数。LeNet中使用sigmoid函数,而现在的CNN中主要使用ReLU函数。此外,原始的LeNet中使用子采样(subsampling)缩小中间数据的大小,而现在的CNN中Max池化是主流。综上,LeNet与现在的CNN虽然有些许不同,但差别并不是那么大。想到LeNet是20多年前提出的最早的CNN,还是很令人称奇的。
2.AlexNet
在LeNet问世20多年后,AlexNet被发布出来。AlexNet是引发深度学习热潮的导火线,不过它的网络结构和LeNet基本上没有什么不同,如下图所示。

AlexNet叠有多个卷积层和池化层,最后经由全连接层输出结果。虽然结构上AlexNet和LeNet没有大的不同,但有以下几点差异。1.激活函数使用ReLU。 2.使用进行局部正规化的LRN(Local Response Normalization)层。 3.使用Dropout
如上所述,关于网络结构,LeNet和AlexNet没有太大的不同。但是,围绕它们的环境和计算机技术有了很大的进步。具体地说,现在任何人都可以获得大量的数据。而且,擅长大规模并行计算的GPU得到普及,高速进行大量的运算已经成为可能。大数据和GPU已成为深度学习发展的巨大的原动力。