九、卷积神经网络(CNN)
卷积神经网络(ConvolutionalNeuralNetwork,CNN)是一种深度前馈神经网络,目前在图片分类、图片检索、目标检测、目标分割、目标跟踪、视频分类、姿态估计等图像视频相关领域中已有很多较为成功的应用。
9.1 全连接层
拉平为一个列向量
全连接层(Fully Connected Layer)可以简单地理解为前面章节中提到的神经网络的一个隐藏层,它包含权重向量W和激活函数。 具体来说,对于一张
32
?
32
?
3
32*32*3
32?32?3的图片(宽和高均为32个像素,有RGB三个通道,可以将其理解为一个的
32
?
32
?
3
32*32*3
32?32?3矩阵),要通过全连接层,首先要将其拉伸为3072*1的向量作为神经网络隐藏层的输入,然后该向量与权重向量W做点乘操作,再将点乘后的结果作为激活函数(如Sigmoid或tanh)的输入,最终,激活函数输出的结果便是全连接层的最终结果。操作过程如图所示,其中activation中蓝色圆圈的值表示所有3072个输入和10维权重向量W点乘的结果。
当完成激活(activation)后的结果为一维向量时,通常将该结果称为特征向量(或激活向量);当激活后的结果为二维向量时,通常称为特征层(feature map,有时也称为激活层,activation map)。由于后面要介绍的卷积层也需要经过激活函数,因此卷积操作得到的结果通常被称为“特征层”。
9.2 卷积层
卷积运算:两矩阵对应元素相乘相加得到的值
卷积层(Convolution Layer)与全连接层不同,它保留了输入图像的空间特征,即对于一张
32
?
32
?
3
32*32*3
32?32?3的图片而言,卷积层的输入就是的
32
?
32
?
3
32*32*3
32?32?3矩阵,不需要做任何改变。在卷积层中,我们引入了一个新的概念:卷积核kernel(常简称为卷积,有时也称为滤波器filter)。卷积的大小可以在实际需要时自定义其长和宽(常见的卷积神经网络中通常将其设置为
1
?
1
、
3
?
3
、
5
?
5
1*1、3*3、5*5
1?1、3?3、5?5等),其通道个数一般设置为与输入图片通道数量一致。
必要的概念已经介绍完毕,接下来我们讲一下卷积的过程:让卷积(核)在输入图片上依次进行滑动,滑动方向为从左到右,从上到下;每滑动一次,卷积(核)就与其滑窗位置对应的输入图片x做一次点积计算并得到一个数值。
这里需要提到另外一个概念:步长(stride)。步长是指卷积在输入图片上移动时需要移动的像素数,如步长为1时,卷积每次只移动1个像素,计算过程不会跳过任何一个像素,而步长为2时,卷积每次移动2个像素。
9.2.1 一维卷积
为方便大家理解,我们先来看一下一维卷积的情况,如图a所示,输入是一个
1
?
7
1*7
1?7维的向量及其对应的数值,我们定义一维卷积,其卷积大小为
1
?
3
1*3
1?3(数值分别为“10,5,11”),那么经过第一次卷积操作(卷积与其对应的输入做点积)后我们可以得到
10
?
5
+
5
?
2
+
11
?
6
=
126
10*5+5*2+11*6=126
10?5+5?2+11?6=126,所以这里的A对应的数值即为126。在这个例子里,我们定义步长为1,所以接下来卷积移动一个格子(在图像中一个步长可以理解为一个像素),如图b所示,可以计算得到B的数值为160。以此类推,最终得到一个
1
?
5
1*5
1?5维的向量。 卷积每次滑动覆盖的格子范围在图像处理中被称为“感受野”,这个名词在后文中还会用到。图中所示的“感受野”为
1
?
3
1*3
1?3。
接下来,我们可以扩展到如图8-3所示的步长为2的情况,同样是
1
?
7
1*7
1?7的输入向量,每次移动两个格子,即卷积从“5, 2,6”移动到“6,10,7”,然后再移动到“7,12,8”,完成所有的卷积操作之后(与步长为1不同),这里最终将得到一个
1
?
3
1*3
1?3的向量。
9.2.2 二维卷积
看完了一维卷积的计算之后,我们再来学习下二维卷积的计算。对于一个
7
?
7
7*7
7?7的图片,我们定义一个
3
?
3
3*3
3?3的卷积,步长分别为1和2,读者可以先自行思考一下其计算过程,如果你已经想好了,请参考前图和下图,看与你的想法是否一致。我们可以看出,步长为1时,输出的特征层(feature map,有时也称为激活层activation map)大小为
5
?
5
5*5
5?5,而步长为2时,则为
3
?
3
3*3
3?3。那么,当步长为3时,输出的卷积层大小是多少呢?答案是:会有错误,对于一个
7
?
7
7*7
7?7的图片不能使用步长为3的
3
?
3
3*3
3?3卷积。(做卷积的时候必须要涉及到全部的元素)
图c的步长为1
如图a所示,输入为一张
32
?
32
?
3
32*32*3
32?32?3的图,kernel大小为
5
?
5
?
3
5*5*3
5?5?3(这里的感受野为
3
?
3
3*3
3?3),那么每一次滑动都将带来卷积和输入图片
5
?
5
?
3
=
75
5*5*3=75
5?5?3=75点乘的计算量,完成整个图片的卷积后最终将生成一张
28
?
28
?
1
28*28*1
28?28?1的新图片b),即特征层(feature map)。类似地,我们再定义一个卷积(通常可以理解为不同卷积完成不同的任务),这时特征层将产生2个通道。接下来,我们连续堆叠6个不同的卷积(kerne/filter)结果,最终特征层将得到6个通道,而这就可以理解为一张
28
?
28
?
6
28*28*6
28?28?6的新图片(如图c)。
9.2.3 卷积神经网络
介绍完了卷积层,接下来我们看看什么是卷积神经网络。如下图所示,卷积神经网络是由一系列卷积层经过激活来得到的。接下来我们看一种更为通用的卷积形式,在
7
?
7
7*7
7?7的输入图片周边做1个像素的填充(pad=1),如右图所示,步长为1,kernel为
3
?
3
3*3
3?3的卷积输出的特征层将为
7
?
7
7*7
7?7。我们在这里给出通用卷积层的计算公式:输入图像为
W
1
?
H
1
?
D
1
W1*H1*D1
W1?H1?D1(字母分别表示图像的宽、高、channel),卷积层的参数中kernel大小为
F
?
F
F*F
F?F,步长为S,pad大小为P,kernel个数为K,那么经过卷积后,输出图像的宽、高、channel分别为:
W
2
=
W
1
?
F
+
2
P
S
+
1
H
2
=
H
1
?
F
?
2
P
S
+
1
D
2
=
K
W_2 = \frac{W_1 - F + 2P}{S} + 1 \\ H_2 = \frac{H_1 - F - 2P}{S} + 1 \\ D_2 = K
W2?=SW1??F+2P?+1H2?=SH1??F?2P?+1D2?=K 接下来看一个例子
- 输入为
32
?
32
?
3
32*32*3
32?32?3
- kernel个数为10,大小为
5
?
5
5*5
5?5
- 步长为1
- pad为2
那么根据以上计算公式可以得出
(
32
?
5
+
2
?
2
)
/
1
+
1
=
32
(32-5+2*2)/1+1=32
(32?5+2?2)/1+1=32,因此我们可以得知输出的特征层大小为
32
?
32
?
10
32*32*10
32?32?10。与此同时,我们也可以得到每个kernel对应的参数个数
5
?
5
?
3
+
1
=
76
5*5*3+1=76
5?5?3+1=76(+1表示bias),因此该层卷积最终的参数个数为
76
?
10
=
760
76*10=760
76?10=760 至此,卷积层的基本运算已介绍完毕,那么卷积层的参数是如何与第7章介绍的传统神经网络参数对应的呢?实际上,卷积层学习的关键就是几个kernel。在上例中,
76
?
10
=
760
76*10=760
76?10=760 可以对应到传统神经网络中的w0~wn,而输入x1~xn则是输入图片。与传统神经网络不同的是,卷积层的计算是含有空间信息的。
9.3 池化层
池化层对原始特征层的信息进行压缩
9.3.1 池化(pooling)
池化(pooling)是对图片进行压缩(降采样)的一种方法,池化的方法有很多,如(网格里面求最大值)max pooling、(求网格的平均值)average pooling等。池化层也有操作参数,我们假设输入图像为
W
1
?
H
1
?
D
1
W1*H1*D1
W1?H1?D1(字母分别表示图像的宽、高、channel),池化层的参数中,池化kernel的大小为F*F,步长为S,那么经过池化后输出的图像的宽、高、channel分别为:
W
2
=
W
1
?
F
S
+
1
H
2
=
H
1
?
F
S
+
1
D
2
=
K
W_2 = \frac{W_1 - F}{S} + 1 \\ H_2 = \frac{H_1 - F}{S} + 1 \\ D_2 = K
W2?=SW1??F?+1H2?=SH1??F?+1D2?=K
通常情况下F=2,S=2。如上图所示,一个
4
?
4
4*4
4?4的特征层经过池化filter=
2
?
2
2*2
2?2,stride=2的最大池化操作后可以得到一个
2
?
2
2*2
2?2的特征层。
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
net = Net()
9.4 批规范层
避免过拟合
让学习的过程早一点结束
批规范化层(BatchNorm层)是2015年Ioffe和Szegedy等人提出的想法,主要是为了加速神经网络的收敛过程以及提高训练过程中的稳定性。虽然深度学习被证明有效,但它的训练过程始终需要经过精心调试,比如精心设置初始化参数、使用较小的学习率等。Ioffe和Szegedy等人进行了详细的分析,并给出了BatchNorm方法,在后面的很多实验中该方法均被证明非常有效(8.2.4节中介绍的ResNet就在重复使用该结构)。这里首先介绍一下batch的概念:在使用卷积神经网络处理图像数据时,往往是几张图片(如32张、64张、128张等)被同时输入到网络中一起进行前向计算,误差也是将该batch中所有图片的误差累计起来一起回传。BatchNorm方法其实就是对一个batch中的数据根据公式(8-1)做了归一化。
x
^
k
=
x
k
?
E
[
x
k
]
V
a
r
(
x
k
)
\widehat{x}_k = \frac{x_k - E[x_k]}{\sqrt{Var(x_k)}}
x
k?=Var(xk?)
?xk??E[xk?]?
… 一些常见卷积神经网络结构
9.6 VGG16实现Cifar10分类
import torch
import torchvision
import torchvision.transforms as transforms
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]
)
trainset = torchvision.datasets.CIFAR10(root='./book/classifier_cifar10/data',
train=True,
download=True,
transform=transform
)
trainloader = torch.utils.data.DataLoader(trainset,
batch_size=4,
shuffle=True,
num_workers=2
)
testset = torchvision.datasets.CIFAR10(root='./book/classifier_cifar10/data',
train=False,
download=True,
transform=transform)
testloader = torch.utils.data.DataLoader(testset,
batch_size=4,
shuffle=False,
num_workers=2)
cifar10_classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
import math
import torch
import torch.nn as nn
import os
cfg = {'VGG16':[64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M']}
class VGG(nn.Module):
def __init__(self, net_name):
super(VGG, self).__init__()
self.features = self._make_layers(cfg[net_name])
self.classifier = nn.Sequential(
nn.Dropout(),
nn.Linear(512, 512),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(512, 512),
nn.ReLU(True),
nn.Linear(512, 10),
)
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
m.bias.data.zero_()
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x
def _make_layers(self, cfg):
layers = []
in_channels = 3
for v in cfg:
if v == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
layers += [nn.Conv2d(in_channels, v, kernel_size=3, padding=1),
nn.BatchNorm2d(v),
nn.ReLU(inplace=True)]
in_channels = v
return nn.Sequential(*layers)
net = VGG('VGG16')
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
for epoch in range(5):
train_loss = 0.0
for batch_idx, data in enumerate(trainloader, 0):
inputs, labels = data
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item()
if batch_idx % 2000 == 1999:
print('[%d, %5d] loss: %.3f' % (epoch + 1, batch_idx + 1, train_loss / 2000))
train_loss = 0.0
print('Saving epoch %d model ...' % (epoch + 1))
state = {
'net': net.state_dict(),
'epoch': epoch + 1,
}
if not os.path.isdir('checkpoint'):
os.mkdir('checkpoint')
torch.save(state, './checkpoint/cifar10_epoch_%d.ckpt' % (epoch + 1))
print('Finished Training')
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (100 * correct / total))
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs, 1)
c = (predicted == labels).squeeze()
for i in range(4):
label = labels[i]
class_correct[label] += c[i].item()
class_total[label] += 1
for i in range(10):
print('Accuracy of %5s : %2d %%' % (
cifar10_classes[i], 100 * class_correct[i] / class_total[i]))
|