Pytorch入门教程13-卷积神经网络的CIFAR-10的识别

  • A+
所属分类:Pytorch快速入门
摘要这一篇我们会使用Pytorch实现一个简单的卷积网络. 主要会介绍卷积神经网络在CIFAR-10数据集上的分类. 除了介绍完整的训练过程以外, 我们还会对卷积操作进行相应的介绍.

简介

上一篇我们介绍了全连接网络在手写数字上的识别. 这一篇我们介绍卷积神经网络CIFAR-10数据集上的分类. 除了介绍完整的训练过程以外, 我们还会对卷积操作进行相应的介绍.

 

参考资料

 

卷积的一些介绍

关于通过卷积后图像的大小

首先我们说明一下通过卷积之后图像大小的变化. 假设有以下的参数:

  • 原始图像的大小是, (N_h, N_w);
  • 卷积核大小是, (K_h, K_w);

那么此时output的大小如下:

Pytorch入门教程13-卷积神经网络的CIFAR-10的识别

接着我们加上padding, 此时在行的padding是P_h, 在列的padding是P_w, 此时的output的大小是:

Pytorch入门教程13-卷积神经网络的CIFAR-10的识别

与上面相比, 只是单纯的在height和width上加上了P_h和P_w. 通常情况下, 我们会进行如下的设置:

  • P_h = K_h - 1
  • P_w = K_w - 1

这样输出图形的大小和输入图像的大小是一样的. 于是, 当我们将kernel size的大小选择为奇数的时候, 最后padding是偶数, 这样就可以在图像上下(或是左右)进行平均分配.

我们看下面的例子, 为了简单起见, 我们将input channel和output channel都设置为1. kernel size=(5,3), 这时候我们按照上面的式子进行设置, padding应该是(4,2). 但是实际上我们4的话需要左右平分, 所以最后padding是(2,1), 这样可以保持输入图形大小和输出图形大小是一样的. 下面是详细的代码.

  1. conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
  2. X = torch.rand(size=(8, 8))
  3. conv2d(X.reshape(1,1,8,8)).shape
  4. """
  5. torch.Size([1, 1, 8, 8])
  6. """

这个时候如果我们再加上stride, 例如在height上stride是S_h, 在width上的stride是S_w. 此时输出大小是:

Pytorch入门教程13-卷积神经网络的CIFAR-10的识别

下面看一个比较复杂的例子, 此时:

  • input size, 8*8 (H*W)
  • kernel size, 5*6
  • padding, 0*1, 注意这里padding的只代表一侧的, 比如说此时W是1, 表示是在侧面加1, 因为有左右, 实际上padding=2
  • stride, 3*4

于是我们按照上面的公式进行计算, 得到下面的式子.

Pytorch入门教程13-卷积神经网络的CIFAR-10的识别

我们使用Pytorch做一下相应的实验, 结果也是和我们预期是一样的.

  1. conv2d = nn.Conv2d(1, 1, kernel_size=(5, 6), padding=(0, 1), stride=(3, 4))
  2. X = torch.rand(size=(8, 8))
  3. conv2d(X.reshape(1,1,8,8)).shape
  4. """
  5. torch.Size([1, 1, 2, 2])
  6. """

 

关于多通道的说明

input data有多个channel的时候, 例如此时是channel=c, 那么此时kernel也是拥有相应数量的通道, 是c. 我们可以将kernel想成一个立方体. 下面看一个例子.

Pytorch入门教程13-卷积神经网络的CIFAR-10的识别

此时原始数据是双通道的, 于是kernel也是双通道的. 我们就可以把其看成一个222的正方体. 比如说上图中的运算, 就是在每一个channel分别计算, 最后相加有下面的式子:

  • (1*1+2*2+3*4+5*4)+(0*0+1*1+3*2+4*3)=56.

我们使用Pytorch实现以上面的操作, 看一下最终结果是否可以预期的是一样的. 可以看到最终输出的结果与上面图中是一样的.

  1. # 得到模拟的训练样本
  2. X = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
  3.                   [[1, 2, 3], [4, 5, 6], [7, 8, 9]]], dtype=torch.float32)
  4. conv2d = nn.Conv2d(in_channels=2, out_channels=1, kernel_size=2, bias=False)
  5. # 初始化kernel的系数
  6. conv2d.weight.data = torch.tensor([[[0, 1], [2, 3]], [[1, 2], [3, 4]]], dtype=torch.float32).unsqueeze_(0)
  7. conv2d(X.unsqueeze_(0))
  8. """
  9. tensor([[[[ 56.,  72.],
  10.           [104., 120.]]]], grad_fn=<MkldnnConvolutionBackward>)
  11. """

接下来讨论当output有多个channels的时候. (通常情况下, 每一个channel都会表示不同的特征.) 于是, 这里output有多个channel的时候, 相当于有多个立方体, 每一个立方体输出是一个channel.

下面我们来看一下11的卷积, 来看一下有多个input channel和多个output channel下的情况. 11的卷积只会计算channel与channel之间的关系. 例如下图展示了input channel=3, output channel=2的情况. 这个时候1个output channel就是对应一个113的立方体, 这里因为output channel=2, 共有两个这样的立方体.

Pytorch入门教程13-卷积神经网络的CIFAR-10的识别

 

关于池化(Pooling)的一些说明

池化是为了解决图像对于位置敏感的问题. 例如现在有一个像素点是在x[i,j]的位置, 可能在别的图片中进行了位移, 在x[i+k, j+k]的位置, 如果是池化的化, 这一片输出值是相同的.

同时, 我们需要注意池化层是没有参数的, 常见的池化操作有max和average. 同时对于多通道的数据进行池化操作, 就是对每个channel进行单独操作, 并不会像卷积操作那样, 不同channel之间会有运算. 且池化操作, 输入的channel和输出的channel是相同的. 下面来看一个例子.

现在我们有如下的数据, 是一个两通道的数据.

  1. X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
  2. X = torch.cat((X, X + 1), dim=1)
  3. """
  4. tensor([[[[ 0.,  1.,  2.,  3.],
  5.           [ 4.,  5.,  6.,  7.],
  6.           [ 8.,  9., 10., 11.],
  7.           [12., 13., 14., 15.]],
  8.          [[ 1.,  2.,  3.,  4.],
  9.           [ 5.,  6.,  7.,  8.],
  10.           [ 9., 10., 11., 12.],
  11.           [13., 14., 15., 16.]]]])
  12. """

我们对其进行最大池化, 最终的结果也是2个channel, 最终结果如下所示.

  1. pool2d = nn.MaxPool2d(3, padding=0, stride=1)
  2. pool2d(X)
  3. """
  4. tensor([[[[10., 11.],
  5.           [14., 15.]],
  6.          [[11., 12.],
  7.           [15., 16.]]]])
  8. """

例如输出的14, 就是max(4,5,6,8,9,10,12,13,14)=14, 其余位置的计算均类似.

 

CIFAR-10数据集介绍

CIFAR-10数据集有10个类, 每类6000个332*32的彩色图像, 共60000个32x32 的彩色图像组成. 下面是从每一类挑出10张照片:

Pytorch入门教程13-卷积神经网络的CIFAR-10的识别

这10类图片分别是:

  1. airplane, 飞机
  2. automobile, 汽车
  3. bird, 小鸟
  4. cat, 小猫
  5. deer, 小鹿
  6. dog, 小狗
  7. frog, 青蛙
  8. horse, 小马
  9. ship, 小船
  10. trunk, 卡车

这10类中, 每一类有6000张照片, 其中有5000张在训练集中, 1000张在测试集中. 所以训练集有500010=50000张图片; 测试集中有100010=10000张图片.

 

卷积网络在CIFAR-10的识别

准备工作

这次的数据量还是比较大的, 我们使用GPU来进行训练.

  1. import torch
  2. import torch.nn as nn
  3. import torchvision
  4. import torchvision.transforms as transforms
  5. import torch.nn.functional as F
  6. import numpy as np
  7. import pandas as pd
  8. import matplotlib.pyplot as plt
  9. %matplotlib inline
  10. # Device configuration
  11. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  12. device
  13. """
  14. device(type='cuda')
  15. """

接着我们定义一下数据集中的10个类别和他们对应的名字.

  1. # 定义class
  2. classes = ('plane', 'car', 'bird', 'cat',
  3.            'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

 

数据加载与数据预处理

我们使用CIFAR-10数据集, 该数据集可以使用torchvision.datasets.CIFAR10获得. 这一阶段的任务如下所示:

  • 创建dataset
    • 加载CIFAR10数据
    • 进行数据预处理, (转换为tensor, 进行标准化)
    • 下面简单说明以下为什么标准化里的参数都是0.5, 这可以保证标准化之后的图像的像素值在-1到1之间. 这是因为: For example, the minimum value 0 will be converted to (0-0.5)/0.5=-1, the maximum value of 1 will be converted to (1-0.5)/0.5=1.
  • 创建dataloader
    • 将dataset传入dataloader, 设置batchsize

首先我们创建dataset, 同时进行数据预处理(数据预处理有两个步骤, 如上面所介绍的).

  1. # 将数据集合下载到指定目录下,这里的transform表示,数据加载时所需要做的预处理操作
  2. transform = transforms.Compose(
  3.     [transforms.ToTensor(),
  4.      transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
  5. # 加载训练集合(Train)
  6. train_dataset = torchvision.datasets.CIFAR10(root='./data',
  7.                                            train=True,
  8.                                            transform=transform,
  9.                                            download=True)
  10. # 加载测试集合(Test)
  11. test_dataset = torchvision.datasets.CIFAR10(root='./data',
  12.                                           train=False,
  13.                                           transform=transform,
  14.                                           download=True)

接着设置dataloader, 设置batchsize的大小. 这里的dataloader就是训练的时候会用到的.

  1. batch_size = 10
  2. # 根据数据集定义数据加载器
  3. train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
  4.                                            batch_size=batch_size,
  5.                                            shuffle=True)
  6. test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
  7.                                           batch_size=batch_size,
  8.                                           shuffle=False)

最后查看一下样例数据(样例图像), 注意如何查看dataloader中的数据(这里查看的时候, 我们要对图像进行反归一化):

  1. def imshow(img):
  2.     img = img / 2 + 0.5     # unnormalize
  3.     npimg = img.numpy()
  4.     plt.imshow(np.transpose(npimg, (1, 2, 0)))
  5.     plt.show()
  6. # get some random training images
  7. dataiter = iter(train_loader)
  8. images, labels = dataiter.next()
  9. # show images
  10. imshow(torchvision.utils.make_grid(images, nrow=5))
  11. # print labels
  12. print(' '.join('%5s' % classes[labels[j]] for j in range(10)))
Pytorch入门教程13-卷积神经网络的CIFAR-10的识别

 

卷积网络的构建

接下来我们定义卷积网络, 我们测试一下浅层的卷积网络.

  1. class Net(nn.Module):
  2.     def __init__(self):
  3.         super(Net, self).__init__()
  4.         self.conv1 = nn.Conv2d(3, 6, 5)
  5.         self.pool = nn.MaxPool2d(2, 2)
  6.         self.conv2 = nn.Conv2d(6, 16, 5)
  7.         self.fc1 = nn.Linear(16 * 5 * 5, 120)
  8.         self.fc2 = nn.Linear(120, 84)
  9.         self.fc3 = nn.Linear(84, 10)
  10.     def forward(self, x):
  11.         x = self.pool(F.relu(self.conv1(x))) # n*6*14*14
  12.         x = self.pool(F.relu(self.conv2(x))) # n*16*5*5
  13.         x = x.view(-1, 16 * 5 * 5) # n*400
  14.         x = F.relu(self.fc1(x)) # n*120
  15.         x = F.relu(self.fc2(x)) # n*84
  16.         x = self.fc3(x) # n*10
  17.         return x
  18. net = Net().to(device)
  19. print(net)
  20. """
  21. Net(
  22.   (conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
  23.   (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  24.   (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  25.   (fc1): Linear(in_features=400, out_features=120, bias=True)
  26.   (fc2): Linear(in_features=120, out_features=84, bias=True)
  27.   (fc3): Linear(in_features=84, out_features=10, bias=True)
  28. )
  29. """

网络定义好之后, 为了测试是否可以使用, 我们用数据集简单测试一下.

  1. # 简单测试模型的输出
  2. examples = iter(test_loader)
  3. example_data, _ = examples.next()
  4. net(example_data.to(device)).shape
  5. """
  6. torch.Size([10, 10])
  7. """

 

定义损失函数和优化器

这一步没有什么特殊的, 损失函数为交叉熵损失, 优化器为SGD优化器.

  1. criterion = nn.CrossEntropyLoss()
  2. optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

 

模型的训练与测试

接下来就是模型的训练和测试. 这一部分的代码和前面全连接部分是差不多的.

  1. num_epochs = 10
  2. n_total_steps = len(train_loader)
  3. LossList = [] # 记录每一个epoch的loss
  4. AccuryList = [] # 每一个epoch的accury
  5. for epoch in range(num_epochs):
  6.     # -------
  7.     # 开始训练
  8.     # -------
  9.     net.train() # 切换为训练模型
  10.     totalLoss = 0
  11.     for i, (images, labels) in enumerate(train_loader):
  12.         images = images.to(device) # 图片大小转换
  13.         labels = labels.to(device)
  14.         # 正向传播以及损失的求取
  15.         outputs = net(images)
  16.         loss = criterion(outputs, labels)
  17.         totalLoss = totalLoss + loss.item()
  18.         # 反向传播
  19.         optimizer.zero_grad() # 梯度清空
  20.         loss.backward() # 反向传播
  21.         optimizer.step() # 权重更新
  22.         if (i+1) % 1000 == 0:
  23.             print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'.format(epoch+1, num_epochs, i+1, n_total_steps, totalLoss/(i+1)))
  24.     LossList.append(totalLoss/(i+1))
  25.     # ---------
  26.     # 开始测试
  27.     # ---------
  28.     net.eval()
  29.     with torch.no_grad():
  30.         correct = 0
  31.         total = 0
  32.         for images, labels in test_loader:
  33.             images = images.to(device)
  34.             labels = labels.to(device)
  35.             outputs = net(images)
  36.             _, predicted = torch.max(outputs.data, 1) # 预测的结果
  37.             total += labels.size(0)
  38.             correct += (predicted == labels).sum().item()
  39.         acc = 100.0 * correct / total # 在测试集上总的准确率
  40.         AccuryList.append(acc)
  41.         print('Accuracy of the network on the {} test images: {} %'.format(total, acc))
  42. print("模型训练完成")
  43. """
  44. Accuracy of the network on the 10000 test images: 62.8 %
  45. Epoch [9/10], Step [1000/5000], Loss: 0.8864
  46. Epoch [9/10], Step [2000/5000], Loss: 0.8912
  47. Epoch [9/10], Step [3000/5000], Loss: 0.9013
  48. Epoch [9/10], Step [4000/5000], Loss: 0.9046
  49. Epoch [9/10], Step [5000/5000], Loss: 0.9081
  50. Accuracy of the network on the 10000 test images: 62.28 %
  51. Epoch [10/10], Step [1000/5000], Loss: 0.8363
  52. Epoch [10/10], Step [2000/5000], Loss: 0.8451
  53. Epoch [10/10], Step [3000/5000], Loss: 0.8562
  54. Epoch [10/10], Step [4000/5000], Loss: 0.8617
  55. Epoch [10/10], Step [5000/5000], Loss: 0.8699
  56. Accuracy of the network on the 10000 test images: 62.26 %
  57. 模型训练完成
  58. """

可以看到, 这里模型最终的准确率在60%左右. 这可能是因为模型不够复杂, 无法解决现有的问题. (关于loss和accurcy的变化, 可以查看原始notebook, 卷积神经网络的CIFAR_10的识别.ipynb)

 

分析每一类的准确率

上面我们获得了模型的一个总的准确率, 下面我们看一下模型对于每一小类的准确率.

  1. class_correct = list(0. for i in range(10)) # 每一类预测正确的个数
  2. class_total = list(0. for i in range(10)) # 每一类的总个数
  3. with torch.no_grad():
  4.     for images, labels in test_loader:
  5.         images = images.to(device)
  6.         labels = labels.to(device)
  7.         outputs = net(images)
  8.         _, predicted = torch.max(outputs, 1)
  9.         c = (predicted == labels).squeeze()
  10.         for i in range(10): # 一个batch中的个数
  11.             label = labels[i]
  12.             class_correct[label] += c[i].item()
  13.             class_total[label] += 1
  14. for i in range(10):
  15.     print('Accuracy of %5s : %2d %%' % (
  16.         classes[i], 100 * class_correct[i] / class_total[i]))
  17. """
  18. Accuracy of plane : 65 %
  19. Accuracy of   car : 81 %
  20. Accuracy of  bird : 43 %
  21. Accuracy of   cat : 25 %
  22. Accuracy of  deer : 60 %
  23. Accuracy of   dog : 66 %
  24. Accuracy of  frog : 73 %
  25. Accuracy of horse : 67 %
  26. Accuracy of  ship : 73 %
  27. Accuracy of truck : 65 %
  28. """

到这里, 我们简单看了一下如何使用Pytorch实现卷积网络, 并完成在CIFAR10数据集上的分类. 但是可以看到, 最终的分类结果不是很理想, 下面我们尝试将网络变深, 来看一下准确率的变化.

 

VGG16测试

下面的代码和上面是差不多的, 唯一的不同就是把网络的结构变得更加复杂了. 其他的训练方法, 测试方法都是一模一样的.

下面就贴一下模型的代码, 训练部分查看notebook, 卷积神经网络的CIFAR_10的识别.ipynb.

  1. class VGG16(nn.Module):
  2.     def __init__(self, num_classes=10):
  3.         super(VGG16, self).__init__()
  4.         self.features = nn.Sequential(
  5.             # 1
  6.             nn.Conv2d(3, 64, kernel_size=3, padding=1),
  7.             nn.BatchNorm2d(64),
  8.             nn.ReLU(True),
  9.             # 2
  10.             nn.Conv2d(64, 64, kernel_size=3, padding=1),
  11.             nn.BatchNorm2d(64),
  12.             nn.ReLU(True),
  13.             nn.MaxPool2d(kernel_size=2, stride=2),
  14.             # 3
  15.             nn.Conv2d(64, 128, kernel_size=3, padding=1),
  16.             nn.BatchNorm2d(128),
  17.             nn.ReLU(True),
  18.             # 4
  19.             nn.Conv2d(128, 128, kernel_size=3, padding=1),
  20.             nn.BatchNorm2d(128),
  21.             nn.ReLU(True),
  22.             nn.MaxPool2d(kernel_size=2, stride=2),
  23.             # 5
  24.             nn.Conv2d(128, 256, kernel_size=3, padding=1),
  25.             nn.BatchNorm2d(256),
  26.             nn.ReLU(True),
  27.             # 6
  28.             nn.Conv2d(256, 256, kernel_size=3, padding=1),
  29.             nn.BatchNorm2d(256),
  30.             nn.ReLU(True),
  31.             # 7
  32.             nn.Conv2d(256, 256, kernel_size=3, padding=1),
  33.             nn.BatchNorm2d(256),
  34.             nn.ReLU(True),
  35.             nn.MaxPool2d(kernel_size=2, stride=2),
  36.             # 8
  37.             nn.Conv2d(256, 512, kernel_size=3, padding=1),
  38.             nn.BatchNorm2d(512),
  39.             nn.ReLU(True),
  40.             # 9
  41.             nn.Conv2d(512, 512, kernel_size=3, padding=1),
  42.             nn.BatchNorm2d(512),
  43.             nn.ReLU(True),
  44.             # 10
  45.             nn.Conv2d(512, 512, kernel_size=3, padding=1),
  46.             nn.BatchNorm2d(512),
  47.             nn.ReLU(True),
  48.             nn.MaxPool2d(kernel_size=2, stride=2),
  49.             # 11
  50.             nn.Conv2d(512, 512, kernel_size=3, padding=1),
  51.             nn.BatchNorm2d(512),
  52.             nn.ReLU(True),
  53.             # 12
  54.             nn.Conv2d(512, 512, kernel_size=3, padding=1),
  55.             nn.BatchNorm2d(512),
  56.             nn.ReLU(True),
  57.             # 13
  58.             nn.Conv2d(512, 512, kernel_size=3, padding=1),
  59.             nn.BatchNorm2d(512),
  60.             nn.ReLU(True),
  61.             nn.MaxPool2d(kernel_size=2, stride=2),
  62.             nn.AvgPool2d(kernel_size=1, stride=1),
  63.         )
  64.         self.classifier = nn.Sequential(
  65.             # 14
  66.             nn.Linear(512, 4096),
  67.             nn.ReLU(True),
  68.             nn.Dropout(),
  69.             # 15
  70.             nn.Linear(4096, 4096),
  71.             nn.ReLU(True),
  72.             nn.Dropout(),
  73.             # 16
  74.             nn.Linear(4096, num_classes),
  75.         )
  76.         #self.classifier = nn.Linear(512, 10)
  77.     def forward(self, x):
  78.         out = self.features(x)
  79.         out = out.view(out.size(0), -1)
  80.         out = self.classifier(out)
  81.         return out
  82. # 定义当前设备是否支持 GPU
  83. net = VGG16().to(device)
  84. # 定义损失函数和优化器
  85. criterion = nn.CrossEntropyLoss()
  86. optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

最终的模型的准确率在80%左右, 可以看到在模型变深, 变复杂之后, 在测试集上的准确率得到了上升.

Pytorch入门教程13-卷积神经网络的CIFAR-10的识别

关于每一类的准确率, 请查看notebook, 卷积神经网络的CIFAR_10的识别.ipynb.

  • 微信公众号
  • 关注微信公众号
  • weinxin
  • QQ群
  • 我们的QQ群号
  • weinxin
王 茂南

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: