卷积神经网络
卷积神经网络(Convolutional Neural Network,CNN)
整体结构
CNN 中新出现了卷积层(Convolution 层)和池化层(Pooling 层),之前介绍的神经网络中,相邻层的所有神经元之间都有连接,这称为全 连接(fully-connected)
全连接层:
CNN层:
卷积层
全连接层存在的问题
数据的形状被“忽视”了。比如,输 入数据是图像时,图像通常是高、长、通道方向上的 3 维形状。但是,向全 连接层输入时,需要将 3 维数据拉平为 1 维数据,而卷积层可以保持形状不变。当输入数据是图像时,卷积层会以 3 维数据的形式接收输入数据,并同样以 3 维数据的形式输出至下一层。
CNN中,有时将卷积层的输入输出数据称为特征图(feature map)。其中,卷积层的输入数据称为输入特征图(input feature map),输出数据称为输出特征图(output feature map)。
卷积运算
卷积层进行的处理就是卷积运算。卷积运算相当于图像处理中的“滤波器运算”,例子:
将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和(有时将这个计算称为乘积 累加运算),注意:这里的乘积不是矩阵的乘积,是对应元素两个的乘积,然后再都加起来! ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
偏置:
填充
在进行卷积层的处理之前,有时要向输入数据的周围填入固定的数据(比 如 0 等),这称为填充(padding),使用填充主要是为了调整输出的大小
“幅 度为 1 的填充”是指用幅度为 1 像素的 0 填充周围,虚线里面的是0。
步幅
应用滤波器的位置间隔称为步幅(stride)
步幅为2 跳跃两个间隔的例子
对于填充和步幅,如何计算输出大小
假设输入大小为 (H, W),滤波器大小为 (FH, FW),输出大小为 (OH, OW),填充为 P,步幅为 S
如:输入大小:(28, 31);填充:2;步幅:3;滤波器大小:(5, 5)
3 维数据的卷积运算
通道方向上有多个特征图时,会按通道 进行输入数据和滤波器的卷积运算,并将结果相加,从而得到输出
结合方块思考
把 3 维数据表示为多维数组 时,书写顺序为(channel, height, width),比如,通道数为C、高度为H、 长度为 W 的数据的形状可以写成(C, H, W),滤波器也一样,要按(channel, height, width)的顺序书写。比如,通道数为 C、滤波器高度为 FH(Filter Height)、长度为 FW(Filter Width)时,可以写成(C, FH, FW)。
如果要在通道方向上也拥有多个卷积运算的输出就需要用到多个滤波器(权重),作为 4 维数据,滤波器的权重数据要按 (output_channel, input_ channel, height, width) 的顺序书写。比如,通道数为 3、大小为 5 × 5 的滤 波器有 20 个时,可以写成 (20, 3, 5, 5)。
进 一步追加偏置的加法运算处理
批处理
需要将在各层间传递的数 据保存为 4 维数据。具体地讲,就是按 (batch_num, channel, height, width) 的顺序保存数据
池化层
池化是缩小高、长方向上的空间的运算,比如,进行将2 × 2 的区域集约成 1 个元素的处理,缩小空间大小。
Max池(Max 池化”是获取最大值的运算),按步幅 2 进行 2 × 2 的 Max 池化时的处理顺序:
一般来说,池化的窗口大小会 和步幅设定成相同的值。比如,3 × 3 的窗口的步幅会设为 3,4 × 4 的窗口 的步幅会设为 4 等。
突出特点:对微小的位置变化具有鲁棒性(健壮),即输入数据发生微小偏差时,池化仍会返回相同的结果
卷积层和池化层的实现
4 维数组
# 10 个高为 28、长为 28、通道为 1 的数 据
>>> x = np.random.rand(10, 1, 28, 28) # 随机生成数据
>>> x.shape
(10, 1, 28, 28)
# 如果要访问第 1 个数据的第 1 个通道的空间数据
>>> x[0, 0] # 或者x[0][0]
基于 im2col 的展开
如果老老实实地实现卷积运算,估计要重复好几层的 for 语句,我们不使用 for 语句,而是使 用 im2col (名 称 是“image to column”的 缩 写,翻 译 过 来 就 是“从 图像到矩阵”的意思) 这个便利的函数进行简单的实现。im2col 是一个函数,将输入数据展开以适合滤波器(权重),对 3 维的输入数据应用 im2col 后,数据转换为 2 维矩阵
对于输入数据,将应用滤波器的区域(3维方块)横向展开为1列。im2col会 在所有应用滤波器的地方进行这个展开处理。
而在实际的卷积运算中,滤波器的应用区域几乎都是重叠的。在 滤波器的应用区域重叠的情况下,使用 im2col 展开后,展开后的元素个数会 多于原方块的元素个数。因此,使用 im2col 的实现存在比普通的实现消耗更 多内存的缺点,之后就只需将卷积层的滤波器(权重)纵 向展开为1列,并计算2个矩阵的乘积即可
卷积层的实现
# input_data― 由(数据量,通道,高,长)的4维数组构成的输入数据 filter_h― 滤波器的高 filter_w― 滤波器的长 stride― 步幅 pad― 填充
im2col (input_data, filter_h, filter_w, stride=1, pad=0)# 卷积层实现
class Convolution:# W(滤波器)def __init__(self, W, b, stride=1, pad=0):self.W = Wself.b = b self.stride = stride self.pad = paddef forward(self, x):FN, C, FH, FW = self.W.shapeN, C, H, W = x.shapeout_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) # 滤波器的展开为二位数组,通过在 reshape 时指定为 -1,reshape 函数会自 动计算 -1 维度上的元素个数,以使多维数组的元素个数前后一致col_W = self.W.reshape(FN, -1).T out = np.dot(col, col_W) + self.b# transpose 会更改多维数组的轴的顺序out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) return out
池化层的实现
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 = paddef 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)# 展开(1)展开输入数据col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad) col = col.reshape(-1, self.pool_h*self.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# 池化层的 backward 处理可以参考 ReLU 层的实现中使用的 max 的反向 传播
CNN 的实现
参数:
- input_dim― 输入数据的维度:(通道,高,长)
- conv_param― 卷积层的超参数(字典)。字典的关键字如下:
filter_num― 滤波器的数量
filter_size― 滤波器的大小
stride― 步幅
pad― 填充 - hidden_size― 隐藏层(全连接)的神经元数量
- output_size― 输出层(全连接)的神经元数量
- weitght_int_std― 初始化时权重的标准差
class SimpleConvNet:# conv_param超参数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['stride']input_size = input_dim[1]conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1pool_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.random.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 xdef loss(self, x, t):y = self.predict(x)return self.lastLayer.forward(y, t)# 基于误差反向传播法求梯度def gradient(self, x, t): # forwardself.loss(x, t)# backwarddout = 1dout = 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'].dbreturn grads
CNN 的可视化
探索 CNN 中到底进行了什么处理
第 1 层权重的可视化
学习前的滤波器是随机进行初始化的,所以在黑白的浓淡上 没有规律可循,但学习后的滤波器变成了有规律的图像。我们发现,通过学 习,滤波器被更新成了有规律的滤波器,比如从白到黑渐变的滤波器、含有块状区域(称为 blob)的滤波器等。右边的有规律的滤波器在“观察”边缘(颜色变化的分界线)和斑块(局部的块状区域)等,比如,左半 部分为白色、右半部分为黑色的滤波器的情况下,会对垂直 方向上的边缘有响应。
基于分层结构的信息提取
随着层次加深,提 取的信息(正确地讲,是反映强烈的神经元)也越来越抽象。下面例子中,第 1 层的神经元对边缘或斑块有响应,第 3 层对纹 理 有 响 应 , 第 5 层 对 物 体 部 件 有 响 应 , 最 后 的 全 连 接 层 对 物 体 的 类 别(狗 或 车)有 响应
具有代表性的 CNN
特别重要的两个网络,一个是在 1998 年首次被提出的 CNN 元祖 LeNet, 另一个是在深度学习受到关注的 2012 年被提出的 AlexNet。
LeNet
进行手写数字识别的网络,LeNet 有几个不同点。第一个不同点在于激活 函数。LeNet 中使用 sigmoid 函数,而现在的 CNN 中主要使用 ReLU 函数。 此外,原始的LeNet中使用子采样(subsampling)缩小中间数据的大小,而 现在的 CNN 中 Max 池化是主流。
AlexNet
AlexNet 叠有多个卷积层和池化层,最后经由全连接层输出结果。虽然结构上 AlexNet 和 LeNet 没有大的不同,但有以下几点差异。
- 激活函数使用ReLU。
- 使用进行局部正规化的LRN(Local Response Normalization)层。
- 使用Dropout
小结
- CNN在此前的全连接层的网络中新增了卷积层和池化层。
- 使用im2col函数可以简单、高效地实现卷积层和池化层。
- 通过CNN的可视化,可知随着层次变深,提取的信息愈加高级。
- LeNet和AlexNet是CNN的代表性网络。
- 在深度学习的发展中,大数据和GPU做出了很大的贡献。