什么是卷积(convolutions)
上一小节遗留的问题就是,我们希望能够把图像一个区域与周围上下左右各个区域关联的这种特性学习到,也就是实现平移不变性,通俗来理解,一个好一点的办法就是在一个点上,把它周围的点都加起来放在这个点上,当做这个点的数据。 听起来是不是很简单,卷积就是大概实现了这个功能,我们看一下离散卷积的公式,既然这里说离散卷积,当然还有连续卷积,不过我们现在用不到,只考虑这个离散卷积就好了。
看不懂没关系,只要你理解了我上面说的那段话就好。我们把一个点周围点的数据都加到这个点上生成的新数据就表示它已经获得了它周围图像的加持。当然,从上面的公司可以看出来,这里的相加也不是简单的相加,而是有一个类似权重矩阵的东西出现,这个东西我们叫它卷积核。
比如下面这个图中,我们使用的卷积核是一个3 × 3的矩阵,中间十字上为1,四个角为0,这就意味着我们考虑的是一个元素上下左右四个像素的影响,忽略四个角上元素的影响。
定义一个卷积核,以及一个图像,这里都是伪代码
代码语言:javascript复制weight = torch.tensor([[w00, w01, w02],
[w10,w11,w12],
[w20,w21,w22]])image = torch.tensor([[i00,i01,i02,...,i0n],
i10,i11,i12,...,i1n],
[i20,i21,i22,...,i2n],
...
[im0,im1,im2,...,imn]])
这里原书中给出的计算逻辑是
我怀疑这个计算逻辑写错了,按说应该从i00开始计算,才是对中心点i11的计算,不知道我说的对不对。因为我看各种示意图和原公式都是这么算的。
计算卷积的过程,如果你把它想成一个矩阵实体,貌似有点像卷毛巾卷,这或许是它名字的由来。
或许我写的关于卷积的介绍有点粗浅,这并不影响我们使用它,我们最好能够拿最后的结果说话,如果你还想深入了解卷积的原理,可以再找一些参考文章看一下。(使用卷积神经网络实现图像分类 ImageNet Classification with Deep Convolutional Neural Networks)
让我们总结一下,使用卷积有什么作用。 1.周边元素的局部操作 2.平移不变性 3.使用很少量的模型参数
关于最后一点再说明一下,原来我们把图像转成一个向量输入进去,使用全连接网络,参数量取决于这个图片的大小和全连接网络输出的大小,一层的参数量是输入规模和输出规模的乘积,而使用卷积神经网络,一层的参数只取决于我们使用了多大的卷积核和使用了多少个卷积核。
调用卷积方法
下面让我们开始实战,用卷积改进我们之前的模型。 照例先把开头写好,数据集安排上,这些我们前面章节已经写过了,这里就不再介绍了。
代码语言:javascript复制#引用%matplotlib inlinefrom matplotlib import pyplot as pltimport numpy as npimport collectionsimport torchimport torch.nn as nnimport torch.nn.functional as Fimport torch.optim as optim
torch.set_printoptions(edgeitems=2)torch.manual_seed(123)#定义类别名称class_names = ['airplane','automobile','bird','cat','deer',
'dog','frog','horse','ship','truck']#加载CIFAR数据集 训练集from torchvision import datasets, transforms
data_path = '../data-unversioned/p1ch6/'cifar10 = datasets.CIFAR10(
data_path, train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4915, 0.4823, 0.4468),
(0.2470, 0.2435, 0.2616))
]))#加载验证集cifar10_val = datasets.CIFAR10(
data_path, train=False, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4915, 0.4823, 0.4468),
(0.2470, 0.2435, 0.2616))
]))#再把鸟和飞机摘出来,重新标注标签label_map = {0: 0, 2: 1}class_names = ['airplane', 'bird']cifar2 = [(img, label_map[label])
for img, label in cifar10 if label in [0, 2]]cifar2_val = [(img, label_map[label])
for img, label in cifar10_val if label in [0, 2]]
在torch.nn里面提供了一维二维三维的卷积方法,一维可以用于序列数据,二维可以用于图像数据,三维可以用于立体数据,调用方法如nn.Conv1d,nn.Conv2d,nn.Conv3d。我们对图像数据处理,所以这里用的是二维卷积。
代码语言:javascript复制#传入的第一个3是单个卷积核处理数据的维度,我们的图像是RGB 三通道所以是3
#传入的第二个16是卷积核的通道数,我理解就是不同卷积核的数量,每一个卷积核可能会有不同的权重值,这样可以捕获到不同的图像特征,显性的来解释,可以认为某个卷积核可能捕获边缘信息,某个卷积核捕获亮度信息,某个卷积核捕获连续特征等等
conv = nn.Conv2d(3, 16, kernel_size=3) #这里使用了一个kernel_size=3,是取巧的方法,快捷的表示3×3的矩阵,我们也可以传入一个元组来声明卷积核的大小比如kernel_size=(3,3)
conv
outs:
Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1))
看一下卷积的权重和偏置。我们前面讨论的卷积只有权重没有偏置,但是在这里使用的卷积方法是有偏置的,也是会添加一个常量值,我理解偏置的作用通常是增加稳定性。这里可以看到偏置的shape是一维的,可见对于一个卷积核,在卷积计算的最后会加上一个常量偏置。
代码语言:javascript复制conv.weight.shape, conv.bias.shapeouts:(torch.Size([16, 3, 3, 3]), torch.Size([16]))
这里我们掏出一张图来看一下,用卷积处理一下它,然后输出来跟原图对比一下。这里使用了灰度图输出,因为使用彩色图的话,你会看到很奇怪的颜色,因为你已经用公式变换了它的颜色数值。你可以输出彩色图试试。
代码语言:javascript复制img, _ = cifar2[0]
output = conv(img.unsqueeze(0))
img.unsqueeze(0).shape, output.shape
outs:(torch.Size([1, 3, 32, 32]), torch.Size([1, 16, 30, 30])) #输入和输出图片的尺寸
plt.figure(figsize=(10, 4.8)) # bookskip
ax1 = plt.subplot(1, 2, 1) # bookskip
plt.title('output') # bookskip
plt.imshow(output[0, 0].detach(), cmap='gray')
plt.subplot(1, 2, 2, sharex=ax1, sharey=ax1) # bookskip
plt.imshow(img.mean(0), cmap='gray') # bookskip
plt.title('input') # bookskip
plt.savefig('Ch8_F2_PyTorch.png') # bookskip
plt.show()
卷积中的填充(padding)
结果我们发现,诶,不对啊,虽然我看不太清图,但是很明显这输出的图怎么变小了啊,这是被谁吃掉了吗?
回头看看上面的代码,在输入输出图片尺寸那块,输入是1 × 3 × 32 × 32,到输出的尺寸怎么变成了1 × 3 × 30 × 30?
回忆一下我们的卷积操作,每计算一个输出点位,对于我们3 × 3的卷积核,需要用到一个输入点位一圈的数据,但是本来就在边缘的那些点位没有对应的一圈数据,所以就没办法算了,我们的卷积从(1,1)开始算起,到下侧和右侧也是一样的,所以就小了两圈。
要解决这个问题,这里有一个简单方法就是对原图像进行边缘填充(padding),在图像的边缘填充一圈数值,比如说都是0,从而使得我们的输出结果符合预期。填充的方案就像下图所示。
在PyTorch提供的包中,我们只需要添加一个参数就可以完成填充操作,这里需要注意的是,填充的大小跟我们使用的卷积核大小有关系,如果我们使用的卷积核为 5 × 5,那我们应该填充2格。
代码语言:javascript复制conv = nn.Conv2d(3, 1, kernel_size=3, padding=1) # 加入参数padding=1
output = conv(img.unsqueeze(0))
img.unsqueeze(0).shape, output.shape
outs:(torch.Size([1, 3, 32, 32]), torch.Size([1, 1, 32, 32])) #此时输出尺寸已经跟输入尺寸一样了
使用卷积获取特定特征
我们前面说过,一个卷积核可以认为识别其中的一种特征,但是我们现在是把卷积核作为要学习更新的权重和偏置,它最后到底能学出一个什么特征是未知数,那么有没有什么已知的卷积核能让我们检测特定的特征呢?
这当然是有的,比较常见的就是平滑。我们可以直接指定卷积核的权重和偏置,从而实现这个效果。
代码语言:javascript复制with torch.no_grad(): #偏置都置为0
conv.bias.zero_()
with torch.no_grad(): #权重填充1/9,所得到的结果就是附近一圈数值的均值
conv.weight.fill_(1.0 / 9.0)
通过上面的这个卷积操作,我们得到的每一个新的像素点就是之前这一圈所有点的均值,这时候图片就像摸匀的调色盘,出现了一种朦胧美。
代码语言:javascript复制output = conv(img.unsqueeze(0))
plt.figure(figsize=(10, 4.8)) # bookskip
ax1 = plt.subplot(1, 2, 1) # bookskip
plt.title('output') # bookskip
plt.imshow(output[0, 0].detach(), cmap='gray')
plt.subplot(1, 2, 2, sharex=ax1, sharey=ax1)
plt.imshow(img.mean(0), cmap='gray')
plt.title('input') # bookskip
plt.savefig('Ch8_F4_PyTorch.png')
plt.show()
从上图可见,我们的输出图像变得更模糊了,虽然我们本来就看不太清。
除了平滑,我们还可以这样设置卷积核
代码语言:javascript复制conv = nn.Conv2d(3, 1, kernel_size=3, padding=1)with torch.no_grad():
conv.weight[:] = torch.tensor([[-1.0, 0.0, 1.0],
[-1.0, 0.0, 1.0],
[-1.0, 0.0, 1.0]])
conv.bias.zero_()
这个卷积核的计算逻辑是用右边像素减去左边像素,你猜这有什么效果?就是让竖向的差异变得更加明显。从输出的图像可以看到一些竖直的条纹,这图片太模糊了,你可以找一些更清楚的图像来试试效果。
代码语言:javascript复制output = conv(img.unsqueeze(0))
plt.figure(figsize=(10, 4.8))
ax1 = plt.subplot(1, 2, 1)
plt.title('output')
plt.imshow(output[0, 0].detach(), cmap='gray')
plt.subplot(1, 2, 2, sharex=ax1, sharey=ax1)
plt.imshow(img.mean(0), cmap='gray')
plt.title('input') # bookskip
plt.savefig('Ch8_F5_PyTorch.png')
plt.show()
最后我们回顾一下卷积学习的过程,一张图三个通道,我们这里设定了16个卷积核,就会有16个卷积核在学习参数,当然卷积后也需要激活函数,因为卷积也是一个线性计算,每一个卷积运算之后输出的都是一种图像的特征,接下来就比较类似了,比如计算损失,更新权重。
今天就先到这了,现在讲的只能算是卷积,还算不上一个卷积神经网络(CNN),下一节我们看卷积神经网络中有哪些优化措施。