7

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

 3 years ago
source link: https://mathpretty.com/12551.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

摘要这一篇我们会使用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. torch.Size([1, 1, 8, 8])

这个时候如果我们再加上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. torch.Size([1, 1, 2, 2])

关于多通道的说明

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. tensor([[[[ 56.,  72.],
  9.           [104., 120.]]]], grad_fn=<MkldnnConvolutionBackward>)

接下来讨论当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. tensor([[[[ 0.,  1.,  2.,  3.],
  4.           [ 4.,  5.,  6.,  7.],
  5.           [ 8.,  9., 10., 11.],
  6.           [12., 13., 14., 15.]],
  7.          [[ 1.,  2.,  3.,  4.],
  8.           [ 5.,  6.,  7.,  8.],
  9.           [ 9., 10., 11., 12.],
  10.           [13., 14., 15., 16.]]]])

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

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

例如输出的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. device(type='cuda')

接着我们定义一下数据集中的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.   (conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
  21.   (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  22.   (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  23.   (fc1): Linear(in_features=400, out_features=120, bias=True)
  24.   (fc2): Linear(in_features=120, out_features=84, bias=True)
  25.   (fc3): Linear(in_features=84, out_features=10, bias=True)

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

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

定义损失函数和优化器

这一步没有什么特殊的, 损失函数为交叉熵损失, 优化器为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. Accuracy of the network on the 10000 test images: 62.8 %
  44. Epoch [9/10], Step [1000/5000], Loss: 0.8864
  45. Epoch [9/10], Step [2000/5000], Loss: 0.8912
  46. Epoch [9/10], Step [3000/5000], Loss: 0.9013
  47. Epoch [9/10], Step [4000/5000], Loss: 0.9046
  48. Epoch [9/10], Step [5000/5000], Loss: 0.9081
  49. Accuracy of the network on the 10000 test images: 62.28 %
  50. Epoch [10/10], Step [1000/5000], Loss: 0.8363
  51. Epoch [10/10], Step [2000/5000], Loss: 0.8451
  52. Epoch [10/10], Step [3000/5000], Loss: 0.8562
  53. Epoch [10/10], Step [4000/5000], Loss: 0.8617
  54. Epoch [10/10], Step [5000/5000], Loss: 0.8699
  55. Accuracy of the network on the 10000 test images: 62.26 %
  56. 模型训练完成

可以看到, 这里模型最终的准确率在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. Accuracy of plane : 65 %
  18. Accuracy of   car : 81 %
  19. Accuracy of  bird : 43 %
  20. Accuracy of   cat : 25 %
  21. Accuracy of  deer : 60 %
  22. Accuracy of   dog : 66 %
  23. Accuracy of  frog : 73 %
  24. Accuracy of horse : 67 %
  25. Accuracy of  ship : 73 %
  26. Accuracy of truck : 65 %

到这里, 我们简单看了一下如何使用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.             nn.Conv2d(3, 64, kernel_size=3, padding=1),
  6.             nn.BatchNorm2d(64),
  7.             nn.ReLU(True),
  8.             nn.Conv2d(64, 64, kernel_size=3, padding=1),
  9.             nn.BatchNorm2d(64),
  10.             nn.ReLU(True),
  11.             nn.MaxPool2d(kernel_size=2, stride=2),
  12.             nn.Conv2d(64, 128, kernel_size=3, padding=1),
  13.             nn.BatchNorm2d(128),
  14.             nn.ReLU(True),
  15.             nn.Conv2d(128, 128, kernel_size=3, padding=1),
  16.             nn.BatchNorm2d(128),
  17.             nn.ReLU(True),
  18.             nn.MaxPool2d(kernel_size=2, stride=2),
  19.             nn.Conv2d(128, 256, kernel_size=3, padding=1),
  20.             nn.BatchNorm2d(256),
  21.             nn.ReLU(True),
  22.             nn.Conv2d(256, 256, kernel_size=3, padding=1),
  23.             nn.BatchNorm2d(256),
  24.             nn.ReLU(True),
  25.             nn.Conv2d(256, 256, kernel_size=3, padding=1),
  26.             nn.BatchNorm2d(256),
  27.             nn.ReLU(True),
  28.             nn.MaxPool2d(kernel_size=2, stride=2),
  29.             nn.Conv2d(256, 512, kernel_size=3, padding=1),
  30.             nn.BatchNorm2d(512),
  31.             nn.ReLU(True),
  32.             nn.Conv2d(512, 512, kernel_size=3, padding=1),
  33.             nn.BatchNorm2d(512),
  34.             nn.ReLU(True),
  35.             nn.Conv2d(512, 512, kernel_size=3, padding=1),
  36.             nn.BatchNorm2d(512),
  37.             nn.ReLU(True),
  38.             nn.MaxPool2d(kernel_size=2, stride=2),
  39.             nn.Conv2d(512, 512, kernel_size=3, padding=1),
  40.             nn.BatchNorm2d(512),
  41.             nn.ReLU(True),
  42.             nn.Conv2d(512, 512, kernel_size=3, padding=1),
  43.             nn.BatchNorm2d(512),
  44.             nn.ReLU(True),
  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.             nn.AvgPool2d(kernel_size=1, stride=1),
  50.         self.classifier = nn.Sequential(
  51.             nn.Linear(512, 4096),
  52.             nn.ReLU(True),
  53.             nn.Dropout(),
  54.             nn.Linear(4096, 4096),
  55.             nn.ReLU(True),
  56.             nn.Dropout(),
  57.             nn.Linear(4096, num_classes),
  58.         #self.classifier = nn.Linear(512, 10)
  59.     def forward(self, x):
  60.         out = self.features(x)
  61.         out = out.view(out.size(0), -1)
  62.         out = self.classifier(out)
  63.         return out
  64. # 定义当前设备是否支持 GPU
  65. net = VGG16().to(device)
  66. # 定义损失函数和优化器
  67. criterion = nn.CrossEntropyLoss()
  68. optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

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

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

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


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK