深度学习入门-卷积神经网络(CNN)
卷积神经网络(Convolutional Neural Network,CNN):CNN被用于图像识别、语音识别等各种场合,在图像识别的比赛中,基于深度学习的方法几乎都以CNN为基础。
1、 整体结构
CNN和之前介绍的神经网络一样,可以通过组装层来构建。区别是,CNN中新出现了卷积层(Convolution层)和池化层(Pooling层)。
之前介绍的神经网络中,相邻层的所有神经元之间都用Affine层实现了全连接:
上图全连接的神经网络中,Affine层后面跟着激活函数ReLU层(或者Sigmoid层)。这里堆叠了4层“Affine-ReLU”组合,然后第5层是Affine层,最后由Softmax层输出最终结果(概率)。
CNN中新增了卷积层(Convolution层)和池化层(Pooling层):
如上图,CNN 的层的连接顺序是“Convolution - ReLU -(Pooling)”(Pooling层有时会被省略),靠近输出的层中使用了之前的“Affine - ReLU”组合。
即:“Affine - ReLU”连接被替换成了“Convolution - ReLU -(Pooling)”连接。
2、 卷积层(Convolution层)
2.1、全连接层(Affine层)存在的问题
全连接层会忽视形状(形状中应该含有重要的空间信息),将全部的输入数据作为相同的神经元(同一维度的神经元)处理,所以无法利用与形状相关的信息。
比如,输入数据是图像时,图像通常是高、长、通道方向上的3维形状。但是,向全连接层输入时,需要将3维数据拉平为1维数据。实际上,前面提到的使用了MNIST数据集的例子中,输入图像就是1通道、高28像素、长28像素的(1, 28, 28)形状,但却被排成1列,以784个数据的形式输入到最开始的Affine层。
卷积层可以保持形状不变。当输入数据是图像时,卷积层会以3维数据的形式接收输入数据,并同样以3维数据的形式输出至下一层。因此,在CNN中,可以(有可能)正确理解图像等具有形状的数据。、
补充:卷积层的输入数据称为输入特征图(input feature map),输出数据称为输出特征图(output feature map)
2.2、卷积层运算-滤波器(核)
卷积层进行的处理就是卷积运算,卷积运算相当于图像处理中的“滤波器运算”:
2.2.1、2维(长、高)数据的卷积运算
对于输入数据,卷积运算以一定间隔滑动滤波器的窗口(图中3 × 3的部分)并应用。如下图所示,将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和。然后,将这个结果保存到输出的对应位置。将这个过程在所有位置都进行一遍,就可以得到卷积运算的输出。
CNN中,滤波器的参数就对应之前的权重。CNN中也存在偏置(1 × 1),且这个值会被加到应用了滤波器的所有元素上:
2.2.2、卷积-填充(调整输出数据大小)
因为如果每次进行卷积运算都会缩小空间(如上例子,4 × 4 -> 2 × 2),那么在某个时刻输出大小就有可能变为 1,导致无法再应用卷积运算。为了避免出现这样的情况,就要使用填充。
如上例子,对大小为(4, 4)的输入数据应用了幅度为1的填充,最终使得卷积结束后不改变数据大小。
“幅度为1的填充”是指用幅度为1像素的0填充周围。
综上,增大填充后,输出大小会变大。
2.2.3、卷积-步幅(调整输出数据大小)
而增大步幅后,输出大小会变小。
应用滤波器的位置间隔称为步幅(stride)。之前的例子中步幅都是1,如果将步幅设为2,则如下图所示,应用滤波器的窗口的间隔变为2个元素:
如上图例子中,对输入大小为(7, 7)的数据,以步幅2应用了滤波器。通过将步幅设为2,输出大小变为(3, 3)。
所以,增大步幅后,输出大小会变小。
2.2.3、已知填充、步幅求输出数据的大小
输入大小为(H, W),滤波器大小为(FH, FW),输出大小为(OH, OW),填充为P,步幅为S。
输出大小可通过下式进行计算:
根据深度学习的框架的不同,当值无法除尽时,有时会向最接近的整数四舍五入,不进行报错而继续运行。
2.2.4、3维(长、高、通道方向)数据的卷积运算
微观
在3维数据的卷积运算中,滤波器大小可以设定为任意值。但是,每个通道的滤波器大小要全部相同;并且输入数据和滤波器的通道数要设为相同的值。
以下例子中,与2维数据相比,可以发现纵深方向(通道方向)上特征图增加了:
通道方向上有多个特征图时,会按通道进行输入数据和滤波器的卷积运算,并将结果相加,从而得到输出:
再强调一遍:在3维数据的卷积运算中,滤波器大小可以设定为任意值。但是,每个通道的滤波器大小要全部相同;并且输入数据和滤波器的通道数要设为相同的值。
宏观(牢记多维数据书写顺序)
把3维数据表示为多维数组时,书写顺序为(channel, height, width)
将数据和滤波器结合长方体的方块来考虑:
上图中,数据输出是通道数为1的特征图。如果要在通道方向上也拥有多个卷积运算的输出需要用到多个滤波器(权重):
把4维数据表示为多维数组时,书写顺序为(output_channel, input_channel, height, width)。
上图中,通过应用FN个滤波器,输出特征图也生成了FN个。如果将这FN个特征图汇集在一起,就得到了形状为(FN, OH, OW)的方块。将这个方块传给下一层,就是CNN的处理流。
如果进一步追加偏置的加法运算处理,则结果如下:
批处理-将N个输入数据打包(牢记多维数据书写顺序)
把N个3维数据表示为多维数组时,书写顺序为(batch_num, channel, height, width),数据以4维的形状在各层间传递。
把N个4维数据表示为多维数组时,书写顺序为(batch_num, output_channel, input_channel, height, width)
如上图,批处理将N次的处理汇总成了1次进行,这N个3维数据作为4维的形状在各层间传递。
3、 池化层(Pooling层)
简介
1、池化是缩小高、长方向上空间大小的运算。
2、池化分为两种:
Max池化:从目标区域中取出最大值
Average池化:计算目标区域的平均值
3、一般来说,池化的窗口大小会和步幅设定成相同的值。
特征:
1、没有要学习的参数
池化只是从目标区域中取最大值(或者平均值),所以不存在要学习的参数。
2、通道数不发生变化
池化运算是按通道独立进行的,输入数据和输出数据的通道数不会发生变化。
3、对微小的位置变化具有健壮性
输入数据发生微小偏差时,池化会吸收输入数据的偏差,仍返回相同的结果。
例子
如下例子,以2 × 2窗口大小移动2个元素的间隔从2 × 2区域中取出最大的元素,将2 × 2的区域集约成1个元素的处理,缩小空间大小:
4、 卷积层和池化层的实现
4.1、卷积层的内部实现
如果使用卷积运算,要重复好几层的for语句。这样实现的话不仅麻烦,而且NumPy中存在使用for语句后处理变慢的缺点。
所以引入将图像转换为矩阵的函数im2col(image to column:从图像到矩阵):将输入数据展开以适合滤波器(权重)。im2col会在所有应用滤波器的地方进行这个展开处理。
卷积层的内部实现流程:
宏观上如下图,im2col把包含批数量的4维输入数据转换成了2维矩阵数据:
微观上如下图,im2col将应用滤波器的输入数据区域(3维方块)横向展开为1列
如下图,使用im2col展开输入数据后,就只需将卷积层的滤波器(权重)纵向展开为1列,并计算2个矩阵的乘积即可。又因为CNN中数据会保存为4维数组,所以要将2维输出数据转换为合适的形状(reshape):
小注:上述流程为了便于观察,将步幅设置得很大,以使滤波器的应用区域不重叠。而在实际的卷积运算中,滤波器的应用区域几乎都是重叠的。在滤波器的应用区域重叠的情况下,使用im2col展开后,展开后的元素个数会多于原方块的元素个数。因此,使用im2col的实现存在比普通的实现消耗更多内存的缺点。但是,因为在计算机中矩阵计算的实现已被高度最优化,可以高速地进行大矩阵的乘法运算。因此,通过归结到矩阵计算上,可以有效地利用线性代数库。
4.2、卷积层的实现
im2col函数:
# 卷积层的内部实现-im2col函数
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):"""Parameters----------input_data : 由(数据量, 通道, 高, 长)的4维数组构成的输入数据filter_h : 滤波器的高filter_w : 滤波器的长stride : 步幅pad : 填充Returns-------col : 2维数组"""N, C, H, W = input_data.shapeout_h = (H + 2*pad - filter_h)//stride + 1out_w = (W + 2*pad - filter_w)//stride + 1img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))for y in range(filter_h):y_max = y + stride*out_hfor x in range(filter_w):x_max = x + stride*out_wcol[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)return col
使用im2col函数:
# 实际使用im2col
import numpy as np
import sys, os
sys.path.append(os.pardir)
from common.util import im2colx1 = np.random.rand(1, 3, 7, 7)
col1 = im2col(x1, 5, 5, stride=1, pad=0)
print(col1.shape) # (9, 75)x2 = np.random.rand(10, 3, 7, 7) # 10个数据
col2 = im2col(x2, 5, 5, stride=1, pad=0)
print(col2.shape) # (90, 75)
使用im2col来实现卷积层Convolution:
im2col和col2im函数:
# 卷积层的内部实现-im2col函数
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):"""Parameters----------input_data : 由(数据量, 通道, 高, 长)的4维数组构成的输入数据filter_h : 滤波器的高filter_w : 滤波器的长stride : 步幅pad : 填充Returns-------col : 2维数组"""N, C, H, W = input_data.shapeout_h = (H + 2*pad - filter_h)//stride + 1out_w = (W + 2*pad - filter_w)//stride + 1img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))for y in range(filter_h):y_max = y + stride*out_hfor x in range(filter_w):x_max = x + stride*out_wcol[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)return col# 卷积层的内部实现-im2col函数的逆处理:col2im
def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):"""Parameters----------col :input_shape : 输入数据的形状(例:(10, 1, 28, 28))filter_h :filter_wstridepadReturns-------"""N, C, H, W = input_shapeout_h = (H + 2*pad - filter_h)//stride + 1out_w = (W + 2*pad - filter_w)//stride + 1col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))for y in range(filter_h):y_max = y + stride*out_hfor x in range(filter_w):x_max = x + stride*out_wimg[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]return img[:, :, pad:H + pad, pad:W + pad]
卷积层的实现(Convolution类):
# 卷积层实现
class Convolution:# 卷积层的初始化方法将滤波器(权重)、偏置、步幅、填充作为参数接收def __init__(self, W, b, stride=1, pad=0):self.W = Wself.b = bself.stride = strideself.pad = pad# 中间数据(backward时使用)self.x = None self.col = Noneself.col_W = None# 权重和偏置参数的梯度self.dW = Noneself.db = None# 卷积层的正向传播def forward(self, x):FN, C, FH, FW = self.W.shape # 滤波器是(FN, C, FH, FW)的4维形状N, C, H, W = x.shapeout_h = 1 + int((H + 2*self.pad - FH) / self.stride)out_w = 1 + int((W + 2*self.pad - FW) / self.stride)# 补充reshape:通过在reshape时指定为-1,reshape函数会自动计算-1维度上的元素个数,以使多维数组的元素个数前后一致。# 比如,(10, 3, 5, 5)形状的数组的元素个数共有750个,指定reshape(10,-1)后,就会转换成(10, 75)形状的数组。# Convolution层的实现中的核心部分:col = im2col(x, FH, FW, self.stride, self.pad) #用im2col展开输入数据col_W = self.W.reshape(FN, -1).T #用reshape将滤波器展开为2维数组。补充:out = np.dot(col, col_W) + self.b #计算展开后的矩阵的乘积out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) #使用NumPy的transpose函数将输出大小转换为合适的形状self.x = xself.col = colself.col_W = col_Wreturn out# 卷积层的反向传播def backward(self, dout):FN, C, FH, FW = self.W.shapedout = dout.transpose(0,2,3,1).reshape(-1, FN)self.db = np.sum(dout, axis=0)self.dW = np.dot(self.col.T, dout)self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)dcol = np.dot(dout, self.col_W.T)dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)return dx
4.3、池化层的实现
池化层的实现和卷积层相同,都使用im2col展开输入数据。
卷积层(一块一块展开):
池化层(一层一层展开):
展开之后,对展开的矩阵求各行的最大值,并转换为合适的形状:
池化层的实现:
# 池化层实现
class Pooling:def __init__(self, pool_h, pool_w, stride=1, pad=0):self.pool_h = pool_hself.pool_w = pool_wself.stride = strideself.pad = padself.x = Noneself.arg_max = Nonedef forward(self, x):N, C, H, W = x.shapeout_h = int(1 + (H - self.pool_h) / self.stride)out_w = int(1 + (W - self.pool_w) / self.stride)col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)col = col.reshape(-1, self.pool_h*self.pool_w)arg_max = np.argmax(col, axis=1)out = np.max(col, axis=1)out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)self.x = xself.arg_max = arg_maxreturn outdef backward(self, dout):dout = dout.transpose(0, 2, 3, 1)pool_size = self.pool_h * self.pool_wdmax = np.zeros((dout.size, pool_size))dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()dmax = dmax.reshape(dout.shape + (pool_size,)) dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)return dx