本文介绍的卷积神经网络(convolutional neural network,CNN)是一类强大的、为处理图像数据而设计的神经网络。 基于卷积神经网络架构的模型在计算机视觉领域中已经占主导地位,当今几乎所有的图像识别、目标检测或语义分割相关的学术竞赛和商业应用都以这种方法为基础。
现代卷积神经网络的设计得益于生物学、群论和一系列的补充实验。 卷积神经网络需要的参数少于全连接架构的网络,而且卷积也很容易用GPU并行计算。 因此卷积神经网络除了能够高效地采样从而获得精确的模型,还能够高效地计算。 久而久之,从业人员越来越多地使用卷积神经网络。即使在通常使用循环神经网络的一维序列结构任务上(例如音频、文本和时间序列分析),卷积神经网络也越来越受欢迎。 通过对卷积神经网络一些巧妙的调整,也使它们在图结构数据和推荐系统中发挥作用。
本文将介绍构成所有卷积网络主干的基本元素。 这包括卷积层本身、填充(padding)和步幅(stride)的基本细节、用于在相邻区域汇聚信息的汇聚层(pooling)、在每一层中多通道(channel)的使用。
卷积
以下对卷积是什么做出一个较为好容易的讲解,就算你看完没看懂,没关系!往下看!不影响你学卷积神经网络。
什么是卷积?你在过去不同时刻惹女朋友生气的叠加,对女朋友现在坏心情的贡献就是卷积。为什么呢?设当前时刻为t,惹女朋友生气用输入函数 p(t) 描述,女朋友心情用输出函数 h(t) 描述,假设女朋友现在不开心的结果是以前所有不开心的叠加。
注:Dirac函数 p(t) 为点源。则在 t1 时刻,惹女朋友生气为 p(t-t1) ,此时女朋友的心情为 h(t-t1) 。为什么是 t-t1 ,可这样理解,当时很生气( t1 时刻),随着时间的流逝,可能就不怎么生气了。用数学符号表示为:
C(t) 代表当前时刻的坏心情,由于是对当前的影响,因此积分上限为t。
同理,男朋友肯定会在不同时刻 ti 惹女朋友生气,表示为 p(t-ti) ,与之对应的响应为 h(t-ti) ,女朋友当前时刻的坏心情应是这些响应的叠加,则:
则卷积定义为:
图像卷积
卷积层是个错误的叫法,因为它所表达的运算其实是互相关运算(cross-correlation),而不是卷积运算。
举例一个处理二维图像数据的例子(暂时不考虑三维)。下面我们输入一个高度是3、宽度是3的二维张量(形象的表达是一个3*3的矩阵)。卷积核的高度和宽度都是2。
从[[0,1],[3,4]]开始与卷积核计算。计算完向右移动一个格子,[[1,2],[4,5]]—>[[3,4],[6,7]]—>[[4,5],[7,8]]
阴影部分是第一个输出元素:
0 * 0+ 1 * 1+3 * 2 + 4 * 3=19
其余输出元素:
1 * 0+ 2 * 1+4 * 2 + 5 * 3=25
3 * 0+ 4 * 1+6 * 2 + 7 * 3=37
4 * 0+ 5 * 1+7 * 2 + 8 * 3=19
卷积核计算python代码如下:
from mxnet import autograd, np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
def corr2d(X, K): #@save
#计算二维互相关运算
h, w = K.shape
Y = np.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
return Y
从上面我们可以看到被选中蓝色的部分每次先向右移动一次,当移动到最边缘时,从开始部位向下移动再向右移动。当没有声明步幅时默认是1,也可以进行人为的修改,下面开始说步幅。
步幅
在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。上述例子默认移动一个元素。为什么会引出步幅呢?这是为了实现高效计算或缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。
上图是垂直步幅为3,水平步幅为2的二维互相关运算。 着色部分是输出元素以及用于输出计算的输入和内核张量元素:
0 * 0 + 0 * 1 + 1 * 2 + 2 * 3 = 8
0 * 0 + 6 * 1 + 0 * 2 + 0 * 3 = 6
可以看到,为了计算输出中第一列的第二个元素和第一行的第二个元素,卷积窗口分别向下滑动三行和向右滑动两列。但是,当卷积窗口继续向右滑动两列时,没有输出,因为输入元素无法填充窗口(除非我们添加另一列填充)。
如果观察仔细,可以看到我们在原33的矩阵周围加了一圈0元素,让他变成了55的矩阵。这个操作叫做填充。
填充
在应用多层卷积时,我们常常丢失边缘像素。 由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。 但随着我们应用许多连续卷积层,累积丢失的像素数就多了。所以引出了填充的方法:在图像便捷填充元素(通常为0,也可以复制图像边界填充)。
我们将3 * 3输入填充到5 * 5,那么它的输出就增加为4 * 4。阴影部分是第一个输出元素以及用于输出计算的输入和核张量元素:
0 * 0+ 0 * 1 + 0 * 2 + 0 * 3 = 0。
总结:填充就是在图像边界添加一圈新的元素,步幅就是每次移动长度。
在实际生活中不可能只是单通道的卷积,例如彩色图像具有标准的RGB通道来代表红、绿和蓝。 但是到目前为止,我们仅展示了单个输入和单个输出通道的简化例子。 这使得我们可以将输入、卷积核和输出看作二维张量。
当我们添加通道时,我们的输入和隐藏的表示都变成了三维张量。例如,每个RGB输入图像具有3 * h * w的形状。我们将这个大小为3的轴称为通道(channel)维度。
上图以双通道单输出为例,计算方式和单通道一样,分别计算再将两个通道的元素相加得到输出结果。
但是实际情况可能不会只有一个输出通道,当我们进行分类时,可能会将不同类别的元素进行归类,这就需要多通道来解决这个问题。
用i和o分别表示输入和输出通道的数目,并让h和w为卷积核的高度和宽度。为了获得多个通道的输出,我们可以为每个输出通道创建一个形状为i * h * w的卷积核张量,这样卷积核的形状是o * i * h * w。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。
汇聚层
通常当我们处理图像时,我们希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野(输入)就越大。
而我们的机器学习任务通常会跟全局图像的问题有关(例如,“图像是否包含一只猫呢?”),所以我们最后一层的神经元应该对整个输入的全局敏感。通过逐渐聚合信息,生成越来越粗糙的映射,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层。
此外,当检测较底层的特征时,我们通常希望这些特征保持某种程度上的平移不变性。例如,如果我们拍摄黑白之间轮廓清晰的图像X,并将整个图像向右移动一个像素,即Z[i, j] = X[i, j + 1],则新图像Z的输出可能大不相同。而在现实中,随着拍摄角度的移动,任何物体几乎不可能发生在同一像素上。即使用三脚架拍摄一个静止的物体,由于快门的移动而引起的相机振动,可能会使所有物体左右移动一个像素(除了高端相机配备了特殊功能来解决这个问题)。
本节将介绍汇聚(pooling)层,它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。
与卷积层类似,汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。 然而,不同于卷积层中的输入与卷积核之间的互相关计算,汇聚层不包含参数。 相反,池运算是确定性的,我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。
上图是最大汇聚层,输出每个汇聚窗口的最大值:
max(0,1,3,4) = 4
max(1,2,4,5) = 5
max(3,4,6,7) = 7
max(4,5,7,8) = 8
平均汇聚层是将汇聚窗口的所有元素求平均,在输出。
汇聚层python代码:
from mxnet import np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = np.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
卷积神经网络(LeNet)
经过上面所有的铺垫,相信已经对卷积神经网络所有的组件都有了一定的了解,下面我们将介绍LeNet,它是最早发布的卷积神经网络之一。
上图是识别数字的一个LeNet网络图,接下来讲解LeNet的实现代码。
从上图可以看到
第一个卷积层:输入通道数为3,输出通道数为32,卷积核大小为5。他的填充和他的步幅大小,图中信息没有标出,需要我们计算一下,计算公式如下:
Hout = 32,Hin = 32, padding是未知数,dilation 默认是1,kernel_size = 5,。stride未知。
结合网络图,我们是可以看出stride其实是1,因为输入了32 * 32 的图像 输出也是 32 * 32 ,如果步幅为2的话,填充可能就会达到几十,这是不可能的,不可能把一个32 * 32 的图像四周加几十圈 0 。所以stride = 1 . 现在就只有个一padding 是未知。解方程:
32 = 32 + 2 * padding - 5 - 1
4 = 2 padding 得出padding = 2。
其余的层数计算方式相同,套公式求出padding。下边上代码
上代码之前,我要说一下,这个网络不是都是卷积层,第一层是卷积层–>最大汇聚层–>卷积层–>最大汇聚层–>卷积层–>最大池化层–>展平–>全连接层
从图像可以看到,展平就是降维,按行的方式展开。
import torch
from torch import nn
from torch.nn import Conv2d,MaxPool2d,Flatten,Linear
class buding(nn.Module):
def __init__(self):
super(buding,self).__init__() #进行初始化
# self.conv1 = Conv2d(3,32,5,padding=2)
# self.maxpool1 = MaxPool2d(2)
# self.conv2 = Conv2d(32,32,5,padding=2)
# self.maxpool2 = MaxPool2d(2)
# self.conv3 = Conv2d(32,64,5,padding=2)
# self.maxpool3 = MaxPool2d(2)
# self.flatten = Flatten()
# self.linear1 = Linear(1024,64)
# self.linear2 = Linear(64,10)
self.model1 = nn.Sequential( #开始建立网络层
Conv2d(3,32,5,padding=2),#卷积层
MaxPool2d(2),#最大汇聚层
Conv2d(32,32,5,padding=2),
MaxPool2d(2),
Conv2d(32,64,5,padding=2),
MaxPool2d(2),
Flatten(),#展平
Linear(1024,64),#全连接
Linear(64,10)
)
def forward(self,x):
x = self.model1(x)
return x
buding()
input = torch.ones((64,3,32,32))
output = buding(input)
print(output)
结果正确,说明建立的各层网络正确。
当我们了解完各层怎么工作时,对于现代的神经网络如:AlexNet,VGG等就可以直接上手使用了。