目录
第7章 卷积神经网络
本章的主题是卷积神经网络(CNN)。CNN被用于图像识别、语音识别等各种场合,在图像识别的比赛中,基于深度学习的方法几乎都以CNN为基础。
7.1 整体结构
CNN和之前介绍的神经网络一样,可以像乐高积木一样通过组装层来构建。不过,CNN中出现了卷积层和池化层。
之前介绍的神经网络中,相邻层的所有神经元之间都有连接,这称为全连接。另外,我们用Affine层实现了全连接层。
那么,CNN会是什么样的结构呢?
在上图中,靠近输出的层中使用了之前的“Affine-ReLU”组合,此外,最后的输出层中使用了之前的“Affine-Softmax”组合。这些都是一般的CNN中比较常见的结构。
7.2 卷积层
CNN中出现了一些特有的术语,比如填充、步幅等,此外,各层中传递的数据是有形状的数据,这与之前的全连接网络不同。
7.2.1 全连接层存在的问题
全连接层存在什么问题呢?那就是数据的形状被“忽视”了。比如,输入数据是图像时,图像通常是高、长、通道方向上的3维形状,但是,向全连接层输入时,需要将3维数据拉平为1维数据。
图像是3维,这个形状中应该含有重要的空间信息。比如,空间上邻近的像素为相似的值、RGB的各个通道之间分别有密切的关联性、相距较远的像素之间没有什么关联等,3维形状中可能隐藏有值得提取的本质模式,但是,因为全连接层会忽视形状,将全部的输入数据作为相同的神经元(同一维度的神经元)处理,所以无法利用与形状相关的信息。
而卷积层可以保持形状不变,当输入数据是图像时,卷积层会以3维数据的形式接收输入数据,并同样以3维数据的形式输出至下一层。因此,在CNN中,可以(有可能)正确理解图像等具有形状的数据。
另外,CNN中,有时将卷积层的输入输出数据称为特征图。其中,卷积层的输入数据称为输入特征图,输出数据称为输出特征图。
7.2.2 卷积运算
卷积层进行的处理就是卷积运算,卷迹运算相当于图像处理中的“滤波器运算”。
对于输入数据,卷积运算以一定间隔滑动滤波器的窗口并应用。如图所示,将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和(有时将这个计算称为乘积累加运算)。然后,将这个结果保存到输出的对应位置,将这个过程在所有位置都进行一遍,就可以得到卷积运算的输出。
在全连接的神经网络中,除了权重参数,还存在偏置。CNN中,滤波器的参数就对应之前的权重,并且,CNN中也存在偏置。
7.2.3 填充
在进行卷积层的处理之前,有时要向输入数据的周围填入固定的数据(比如0等),这称为填充,是卷积运算中经常会用到的处理。
使用填充主要是为了调整输出的大小。比如,对大小的为(4,4)的输入数据应用(3,3)的滤波器时,输出大小变为(2,2),相当于输出大小比输入大小缩小了2个元素。这在反复进行多次卷积运算的深度网络中会成为问题。为什么呢?因为如果每次进行卷积运算都会缩小空间,那么在某个时刻输出大小就有可能变为1,导致无法再应用卷积运算。为了避免出现这样的情况,就要使用填充。在刚才的例子中,将填充的幅度设为1,那么相对于输入大小(4,4),输出大小也保持为原来的(4,4)。因此,卷积运算就可以在保持空间大小不变的情况下将数据传给下一层。
7.2.4 步幅
应用滤波器的位置间隔称为步幅。
增大步幅后,输出大小会变小,而增大填充后,输出大小会变大。
当输出大小无法除尽时(结果是小数),需要采取报错等对策。顺便说一下,根据深度学习的框架的不同,当值无法除尽时,有时会向最接近的整数四舍五入,不进行报错而继续运行。
7.2.5 3维数据的卷积运算
通道方向上有多个特征图时,会按通道进行输入数据和滤波器的卷积运算,并将结果相加,从而得到输出。
在3维数据的卷积运算中,输入数据和滤波器的通道数要设为相同的值,滤波器大小可以设定为任意值。
7.2.6 结合方块思考
在这个例子里,数据输出是1张特征图。所谓1张特征图,换句话说,就是通道数为1的特征图。那么,如果要在通道方向上也拥有多个卷积运算的输出,该怎么做呢?为此,就需要用到多个滤波器(权重)。
7.2.7 批处理
神经网络的处理中进行了将输入数据打包的批处理,之前的全连接神经网络的实现也对应了批处理,通过批处理,能够实现处理的高效化和学习时对mini-batch的对应。
我们希望卷积运算也同样对应批处理,为此,需要将在各层间传递的数据保存为4维数据。具体地讲,就是按(batch_num, channel, height, width)的顺序保存数据。
7.3 池化层
池化是缩小高、长方向上的空间的运算。
“Max池化”是获取最大值的运算,“2x2”表示目标区域的大小。一般来说,池化的串口大小会和步幅设定成相同的值。
除了Max池化之外,还有Average池化等。相对于Max池化是从目标区域中取出最大值,Average池化则是计算目标区域的平均值。在图像识别领域,主要使用Max池化。因此,本书中说到“池化层”时,指的是Max池化。
池化层的特征
1.没有要学习的参数
池化层和卷积层不同,没有要学习的参数。池化只是从目标区域中取最大值(或者平均值),所以不存在要学习的参数。
2.通道数不发生变化
经过池化运算,输入数据和输出数据的通道数不会发生变化。
如上图所示,计算是按通道独立进行的。
3.对微小的位置变化具有鲁棒性(健壮)
输入数据发生微笑偏差时,池化仍会返回相同的结果。因此,池化对输入数据的微小偏差具有鲁棒性。
如上图所示,池化会吸收输入数据的偏差(根据数据的不同,结果有可能不一致) 。
7.4 卷积层和池化层的实现
7.4.1 4维数组
import numpy as np
x = np.random.rand(10, 1, 28, 28)
print(x.shape)
print(x[0].shape)
print(x[1].shape)
print(x[0, 0])
print(x[0][0])
(10, 1, 28, 28)
(1, 28, 28)
(1, 28, 28)
[[0.78366725 0.04926278 0.19718404 0.57338282 0.39769665 0.01094044
0.824495 0.91850354 0.73099492 0.71527378 0.35505084 0.0211244
0.73428751 0.43283415 0.62773291 0.04168173 0.53651204 0.27816881
0.16277058 0.49880375 0.26565717 0.03620092 0.71409177 0.40914321
0.47988124 0.3231364 0.92026101 0.57861158]
[0.79113883 0.40655301 0.42514531 0.26880885 0.80510278 0.13539557
0.53198892 0.19315611 0.55525788 0.68411349 0.01468148 0.951923
0.50952119 0.39191196 0.8592221 0.88079757 0.14027045 0.41493338
0.3364961 0.21514096 0.81014103 0.4290779 0.84423908 0.5519229
0.87218471 0.96769902 0.93064515 0.21332042]
......
7.4.2 基于im2col的展开
im2col会把输入数据展开以适合滤波器(权重),对于输入数据,将应用滤波器的区域(3维方块)横向展开为1列,im2col会在所有应用滤波器的地方进行这个展开处理。
7.4.3 卷积层的实现
本书提供了im2col函数,并将这个im2col函数作为黑盒(不关心内部实现)使用。
im2col会考虑滤波器大小、步幅、填充,将输入数据展开为2维数组。
现在使用im2col来实现卷积层,这里我们将卷积层实现为名为Convolution的类。
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = int(1 + (H + 2*self.pad - FH) / self.stride)
out_w = int(1 + (W + 2*self.pad - FW) / self.stride)
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1)
out = np.dot(col, col_W) + self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
return out
7.4.4 池化层的实现
池化层的实现和卷积层相同,都使用im2col展开输入数据。不过,池化的情况下,在通道方向上是独立的,这一点和卷积层不同。具体地讲,池化的应用区域按通道单独展开。
class Pooling:
def __init__(self, pool_h, pool_w, stride=1, pad=0):
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
def forward(self, x):
N, C, H, W = x.shape
out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / self.stride)
# 展开(1)
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h*self.pool.pool_w)
# 最大值(2)
out = np.max(col, axis=1)
# 转换(3)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
return out
池化层的实现按下面3个阶段进行:
1.展开输入数据;
2.求各行的最大值;
3.转换为合适的输出大小。
7.5 CNN的实现
我们已经实现了卷积层和池化层,现在来组合这些层,搭建进行手写数字识别的CNN。
class SimpleConvNet:
def __init__(self, input_dim=(1, 28, 28), conv_param = {'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1}, hidden_size=100, output_size=10, weight_init_std=0.01):
filter_num = conv_param['filter_num']
filter_size = conv_param['filter_size']
filter_pad = conv_param['pad']
filter_stride = conv_param
input_size = input_dim[1]
conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1
pool_output_size = int(filter_num * (conv_output_size/2) * (conv_output_size/2))
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
self.params['b1'] = np.zeros(filter_num)
self.params['W2'] = weight_init_std * np.random.randn(pool_output_size, hidden_size)
self.params['b2'] = np.zeros(hidden_size)
self.params['W3'] = weight_init_std * np.ramdon.randn(hidden_size, output_size)
self.params['b3'] = np.zeros(output_size)
self.layers = OrderedDict()
self.layers['Conv1'] = Convolution(self.params['W1'],
self.params['b1'],
conv_param['stride'],
conv_param['pad'])
self.layers['Relu1'] = Relu()
self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
self.layers['Affine1'] = Affine(self.params['W2'], self.params['b2'])
self.layers['Relu2'] = Relu()
self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3'])
self.last_layer = SoftmaxWithLoss()
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss(self, x, t):
y = self.predict(x)
return self.lastLayer.forward(y, t)
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.lastLayer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# 设定
grads = {}
grads['W1'] = self.layers['Conv1'].dW
grads['b1'] = self.layers['Conv1'].db
grads['W2'] = self.layers['Affine1'].dW
grads['b2'] = self.layers['Affine1'].db
grads['W3'] = self.layers['Affine2'].dW
grads['b3'] = self.layers['Affine2'].db
return grads
卷积层和池化层是图像识别中必备的模块,CNN可以有效读取图像中的某种特性,在手写数字识别中,还可以实现高精度的识别。
7.6 CNN的可视化
7.6.1 第1层权重的可视化
在上图中,学习前的滤波器是随机进行初始化的,所以在黑白的浓淡上没有规律可循,但学习后的滤波器变成了有规律的图像。我们发现,通过学习,滤波器被更新成了有规律的滤波器,比如从白到黑渐变的滤波器、含有块状区域(称为blob) 的滤波器等。
上图显示了选择两个学习完的滤波器对输入图像进行卷积处理时的结果。我们发现“滤波器1”对垂直方向上的边缘有响应,“滤波器2”对水平方向上的边缘有响应。
由此可知,卷积层的滤波器会提取边缘或斑块等原始信息,而CNN会将这些原始信息传递给后面的层。
7.6.2 基于分层结构的信息提取
根据深度学习的可视化相关的研究,随着CNN的层次加深,提取的信息(正确地讲,是反映强烈的神经元)也越来越抽象。
如上图所示,如果堆叠了多层卷积层,则随着层次加深,提取的信息也愈加复杂、抽象,这是深度学习中很有意思的一个地方。最开始的层对简单的边缘有响应,接下来的层对纹理有响应,再后面的层对更加复杂的物体部件有响应。也就是说,随着层次加深,神经元从简单的形状向“高级”信息变化。换句话说,就像我们理解东西的“含义”一样,响应的对象在逐渐变化。
7.7 具有代表性的CNN
7.7.1 LeNet
LeNet具有连续的卷积层和池化层(正确地讲,是只“抽选元素”的子采样层) ,最后经全连接层输出结果。
7.7.2 AlexNet
AlexNet叠有多个卷积层和池化层,最后经由全连接层输出结果。虽然结构上AlexNet和LeNet没有大的不同,但有以下几点差异:
1.激活函数使用ReLU;
2.使用进行局部正规化的LRN层;
3.使用Dropout。
关于网络结构,LexNet和AlexNet没有太大的不同,但是,围绕它们的环境和计算机技术有了很大的进步。具体地说,现在任何人都可以获得大量的数据。而且,擅长大规模并行计算的GPU得到普及,高速进行大量的运算已经成为可能,大数据和GPU已成为深度学习发展的巨大的原动力。
大多数情况下,深度学习(加深了层次的网络)存在大量的参数。因此,学习需要大量的计算,并且需要使那些参数“满意”的大量数据,可以说是GPU和大数据给这些课题带来了希望。
7.8 小结
本章介绍了CNN。
构成CNN的基本模块的卷积层和池化层虽然有些复杂,但是一旦理解了,之后就只是如何使用它们的问题了。
1.CNN在此前的全连接层的网络中新增了卷积层和池化层;
2.使用im2col函数可以简单、高效地实现卷积层和池化层;
3.通过CNN的可视化,可知随着层次变深,提取的信息愈加高级;
4.LeNet和AlexNet是CNN的代表网络;
5.在深度学习的发展中,大数据和GPU做出了很大的贡献。