卷积神经网络在计算机视觉中可以应用在图像的分类、检索、目标的检测和图像分割中,具体可以应用到人脸识别、人脸表情识别、图像生成、自动驾驶等方面。
卷积在《Deep Learning》书中定义为:卷积 是对两个实变函数的一种数学运算。卷积的运算过程如下图所示。
在输入的矩阵中,卷积核像滑动窗口一样对输入矩阵进行卷积操作。卷积操作就是卷积核中方格的值与对应输入矩阵方格中的数相乘,最后将每对方格的积求和。其中,卷积核又被成为滤波器。
在卷积过程中经常涉及到的基本概念如下:
其中padding是填充像素,当卷积核在输入矩阵上滑动时,当滑动到边界有时出现当前进行卷积操作的部分输入矩阵的大小和卷积核的大小不相同,如当前操作的矩阵大小为3*2,卷积核的大小为3*3,这样的话,就不能进行正常的卷积运算,此时可以使用填充0的方法,给原来的矩阵填充一列0,使得大小和卷积核相同,这种操作就是padding。
深度的大小和卷积核的个数一致。
其中,是输入矩阵的大小,
是卷积核的大小,stride是遍历的步长。
当图像使用了padding时,特征图大小的计算公式为:
保留了主要特征的同时减少参数和计算量,防止过度拟合,提高模型的泛化能力。一般位于卷积层与卷积层之间。主要有Max pooling(最大值池化)和Average pooling(平均值池化)。
两层之间所有的神经元都有权重的连接,通常全连接层在网络的尾部,参数量通常较大。
AlexNet包含5个卷积层和3个全连接层,结构如下图:
在AlexNet中使用非线性激活函数ReLu,为了防止产生过拟合现象,采用了Dropout和Data augmentation,并且使用双GPU实现。由上图可以看出来,网络被分为上下两层。
ZFNet在结构上和AlexNet相同,但在第一层卷积层中将感受野的大小由11*11调整为7*7,步长由4调整为2。
第三、四、五成卷积层中,卷积核个数由384,384,256调整为512,512,1024。
VGG是一个更深网络,它从AlexNet的8层加深到了16-19层。由于当时的条件限制,不足以一次性进行如此深层的网络训练,于是它首先训练了网络的前11层,在参数固定后再训练后面的层,采用分块的方法进行全层的网络训练。
GoogleNet不仅在深度上增大了模型的深度,而且在模型的结构上也进行了改进。
网络包含22个待参数的层,独立的层约有100层,并且参数是AlexNet的1/12。没有额外的全连接层,除了最后的类别输出层。
GoolgeNet添加inception模块的初衷是希望多卷积核增加特征多样性。在原始的inception中会出现堆叠导致计算复杂度过高的现象,Inception v2中插入1*1卷积核进行降维。
在Inception v3版本中,京一部对参数数量进行降低,用小的卷积核来替代大的卷积核。
使用两个3*3的卷积核来替换5*5的卷积核,通过这种方法可以降低参数的数量(5*5的卷积核的参数计算为:;替换为2个3*3的卷积核需要参数为:
)。而且可以增加非线性激活函数,使得表征能力更强,心训练的速度更快。
使用残差学习网络,深度有152层,是2015年ILSVRC竞赛冠军。
残差的思想就是去掉相同的主体部分,从而突出微小的变化。可以被用来训练非常深的网络。残差网络优势在于解决了梯度消失的现象。
这样,即使某个函数的导数为0,相对于BP算法来说,也不会出现整体为0的情况,从而解决了梯度消失的现象。
PyTorch里包含了 MNIST, CIFAR10 等常用数据集,调用 torchvision.datasets 即可把这些数据由远程下载到本地。
DataLoader提供的常用操作有:batch_size(每个batch的大小), shuffle(是否进行随机打乱顺序的操作), num_workers(加载数据的时候使用几个子进程)
input_size = 28*28 # MNIST上的图像尺寸是 28x28
output_size = 10 # 类别为 0 到 9 的数字,因此为十类
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('./data', train=True, download=True,
transform=transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])),
batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('./data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])),
batch_size=1000, shuffle=True)
class FC2Layer(nn.Module):
def __init__(self, input_size, n_hidden, output_size):
# nn.Module子类的函数必须在构造函数中执行父类的构造函数
# 下式等价于nn.Module.__init__(self)
super(FC2Layer, self).__init__()
self.input_size = input_size
# 这里直接用 Sequential 就定义了网络,注意要和下面 CNN 的代码区分开
self.network = nn.Sequential(
nn.Linear(input_size, n_hidden),
nn.ReLU(),
nn.Linear(n_hidden, n_hidden),
nn.ReLU(),
nn.Linear(n_hidden, output_size),
nn.LogSoftmax(dim=1)
)
def forward(self, x):
# view一般出现在model类的forward函数中,用于改变输入或输出的形状
# x.view(-1, self.input_size) 的意思是多维的数据展成二维
# 代码指定二维数据的列数为 input_size=784,行数 -1 表示我们不想算,电脑会自己计算对应的数字
# 在 DataLoader 部分,我们可以看到 batch_size 是64,所以得到 x 的行数是64
# 大家可以加一行代码:print(x.cpu().numpy().shape)
# 训练过程中,就会看到 (64, 784) 的输出,和我们的预期是一致的
# forward 函数的作用是,指定网络的运行过程,这个全连接网络可能看不啥意义,
# 下面的CNN网络可以看出 forward 的作用。
x = x.view(-1, self.input_size)
return self.network(x)
class CNN(nn.Module):
def __init__(self, input_size, n_feature, output_size):
# 执行父类的构造函数,所有的网络都要这么写
super(CNN, self).__init__()
# 下面是网络里典型结构的一些定义,一般就是卷积和全连接
# 池化、ReLU一类的不用在这里定义
self.n_feature = n_feature
self.conv1 = nn.Conv2d(in_channels=1, out_channels=n_feature, kernel_size=5)
self.conv2 = nn.Conv2d(n_feature, n_feature, kernel_size=5)
self.fc1 = nn.Linear(n_feature*4*4, 50)
self.fc2 = nn.Linear(50, 10)
# 下面的 forward 函数,定义了网络的结构,按照一定顺序,把上面构建的一些结构组织起来
# 意思就是,conv1, conv2 等等的,可以多次重用
def forward(self, x, verbose=False):
x = self.conv1(x)
x = F.relu(x)
x = F.max_pool2d(x, kernel_size=2)
x = self.conv2(x)
x = F.relu(x)
x = F.max_pool2d(x, kernel_size=2)
x = x.view(-1, self.n_feature*4*4)
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
x = F.log_softmax(x, dim=1)
return x
n_hidden = 8 # number of hidden units
model_fnn = FC2Layer(input_size, n_hidden, output_size)
model_fnn.to(device)
optimizer = optim.SGD(model_fnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_fnn)))
normal_fcn_train_loss,train_x = train(model_fnn)
test(model_fnn)
model_fnn
CNN网络
# Training settings
n_features = 6 # number of feature maps
model_cnn = CNN(input_size, n_features, output_size)
model_cnn.to(device)
optimizer = optim.SGD(model_cnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_cnn)))
normal_cnn_train_loss,_ = train(model_cnn)
test(model_cnn)
model_cnn
CNN在卷积与池化上具有优良特性,如果我们把图像中的像素打乱顺序,这样 卷积 和 池化 就难以发挥作用了,为了验证这个想法,我们把图像中的像素打乱顺序再试试。
可以看到卷积神经网络在训练集上误差较小,收敛较快,表现出的性能优于全连接网络。
打乱情况下全连接网络与卷积网络训练误差对比
可以看到将像素顺序打乱之后,卷积神经网络的卷积操作和池化操作的优良特性将失去作用,此时全连接网络的效果表现得较好。
打乱与非打乱情况下全连接网络训练误差对比
从结果可以看出,像素顺序是否打乱对该网络的影响较小。
打乱与非打乱情况下卷积网络训练误差对比
而对于CNN网络,打乱像素的顺序对改网络的影响较大。
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64,
shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=8,
shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
def imshow(img):
plt.figure(figsize=(8,8))
img = img / 2 + 0.5 # 转换到 [0,1] 之间
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
# 得到一组图像
images, labels = iter(trainloader).next()
# 展示图像
imshow(torchvision.utils.make_grid(images))
# 展示第一行图像的标签
for j in range(8):
print(classes[labels[j]])
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
# 网络放到GPU上
net = Net().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)
plt.figure(figsize=(24,8))#设置画板的大小
for epoch in range(10): # 重复多轮训练
loss_i = np.array([])
train_x = np.array([])
for i, (inputs, labels) in enumerate(trainloader):
inputs = inputs.to(device)
labels = labels.to(device)
# 优化器梯度归零
optimizer.zero_grad()
# 正向传播 + 反向传播 + 优化
outputs = net(inputs)
loss = criterion(outputs, labels)
loss_i = np.append(loss_i,loss.item())
loss.backward()
optimizer.step()
train_x = np.append(train_x,i)
# 输出统计信息
if i % 100 == 0:
print('Epoch: %d Minibatch: %5d loss: %.3f' %(epoch + 1, i + 1, loss.item()))
plt.plot(train_x,loss_i,label = '{i} train loss'.format(i=epoch+1))
plt.legend()#添加标注
plt.show()
print('Finished Training')
correct = 0
total = 0
for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
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))
transform_train = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))])
transform_test = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform_test)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)
testloader = torch.utils.data.DataLoader(testset, batch_size=128, shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
class VGG(nn.Module):
def __init__(self):
super(VGG, self).__init__()
self.cfg = [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M']
self.features = self._make_layers(self.cfg)
self.classifier = nn.Linear(512, 10)
def forward(self, x):
out = self.features(x)
out = out.view(out.size(0), -1)
out = self.classifier(out)
return out
def _make_layers(self, cfg):
layers = []
in_channels = 3
for x in cfg:
if x == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
layers += [nn.Conv2d(in_channels, x, kernel_size=3, padding=1),
nn.BatchNorm2d(x),
nn.ReLU(inplace=True)]
in_channels = x
layers += [nn.AvgPool2d(kernel_size=1, stride=1)]
return nn.Sequential(*layers)
plt.figure(figsize=(24,8))#设置画板的大小
for epoch in range(10): # 重复多轮训练
loss_i = np.array([])
train_x = np.array([])
for i, (inputs, labels) in enumerate(trainloader):
inputs = inputs.to(device)
labels = labels.to(device)
# 优化器梯度归零
optimizer.zero_grad()
# 正向传播 + 反向传播 + 优化
outputs = net(inputs)
loss = criterion(outputs, labels)
loss_i = np.append(loss_i,loss.item())
loss.backward()
optimizer.step()
train_x = np.append(train_x,i)
# 输出统计信息
if i % 100 == 0:
print('Epoch: %d Minibatch: %5d loss: %.3f' %(epoch + 1, i + 1, loss.item()))
plt.plot(train_x,loss_i,label = '{i} train loss'.format(i=epoch+1))
plt.legend()#添加标注
plt.show()
print('Finished Training')
对比7.3的训练结果,使用VGG网络每轮的训练误差都在降低。
correct = 0
total = 0
for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
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: %.2f %%' % (
100 * correct / total))
相比于CNN网络,本次VGG网络的分类准确度上升了20%,但同时模型的训练时间也有较为明显的差异,VGG网络的训练时间要明显长于CNN网络。
网络名称 | 训练时间 |
CNN | 1分42.8秒 |
VGG | 7分19秒 |
1. dataloader 里面 shuffle 取不同值有什么区别?
shuffle是控制加载数据集时是否打乱数据集的顺序,当shuffle为true时,数据被随即打乱,使得模型的泛化能力能强。下图是shuffle的对比,当shuffle为false时,训练误差成周期性震荡,而为true的训练误差逐渐收敛。
2. transform 里,取了不同值,这个有什么区别?
Normalize()函数的作用是将数据转换为标准高斯分布,即对图像的每个channel进行标准化(均值变为0,标准差为1),可以加快模型的收敛。
torchvision.transforms 用来对数据进行预处理,从而提高模型的泛化能力。
3. epoch 和 batch 的区别?
epoch是训练整个训练集的次数。当数据集很大时,不能一次性将所有数据送进去训练,于是使用batch,每个batch包含较小数目的数据,将这些数据送入模型中进行训练。
4. 1x1的卷积和 FC 有什么区别?主要起什么作用?
卷积跟全连接都是一个点乘的操作,区别在于卷积是作用在一个局部的区域,而全连接是对于整个输入而言,那么只要把卷积作用的区域扩大为整个输入,那就变成全连接了。
1x1的卷积核的作用有:跨通道的特征整合、特征通道升维或者降维、减少卷积核的参数。
5. residual leanring 为什么能够提升准确率?
使用残差网络可以有效解决梯度消失的问题,从而可以更好的传播误差,用来更新参数,因此能够提升准确度。
6. 代码练习二里,网络和1989年 Lecun 提出的 LeNet 有什么区别?
LeNet网络结构:
CNN网络结构:
首先,CNN的网络结构中少了一个池化层;CNN中的池化层是最大池化,LeNet的池化层中有可训练参数;CNN使用的是ReLU激活函数,LeNet使用的是sigmoid激活函数;最后LeNet使用Softmax分类,本次CNN中没有。
7. 代码练习二里,卷积以后feature map 尺寸会变小,如何应用 Residual Learning?
可以选择使用1*1卷积、填充padding等方法来调整feature map尺寸
8. 有什么方法可以进一步提升准确率?