训练一个分类器
我们这一次来从头开始,训练一个图片分类器。
1. 准备数据
1 | import numpy as np |
torchvision是独立于pytorch的关于图像操作的一些方便工具库,torchvision主要包括一下几个包:
- vision.datasets : 几个常用视觉数据集,可以下载和加载
- vision.models : 流行的模型,例如 AlexNet, VGG, and ResNet 以及训练好的参数
- vision.transforms : 常用的图像操作,例如:随机切割,旋转等
- vision.utils : 用于把形似 (3 x H x W) 的张量保存到硬盘中,给一个mini-batch的图像可以产生一个图像格网
首先我们需要导入原始的数据,这里我们使用CIFAR-10数据集,它可以通过下面一个函数导入:
1 | torchvision.datasets.CIFAR10(root, train=True, transform=None, target_transform=None, download=False) |
- root: 数据集存放或下载的路径
- train: bool, If True, creates dataset from training set, otherwise creates from test set
- transform (callable, optional) – A function/transform that takes in an PIL image and returns a transformed version.
- download (bool, optional): 如果为True, 那么如果root目录下没有想要的数据,就会自动把数据下载到root目录下
1.1 原始数据
下面举个例子,看这样可以导入数据。但是由于某些大家都知道的原因,下载速度会很慢,所以最好自己手动下载数据。
1 | # 我实现自己下载好了,download是个摆设参数 |
Using downloaded and verified file: ./data\cifar-10-python.tar.gz
可以看到导入的数据集是一个PIL对象的集合,该集合可以索引: 1
2
3 __getitem__(index):
Parameters: index (int) – Index
Returns: (image, target) where target is index of the target class.
1 | print(example) |
Dataset CIFAR10
Number of datapoints: 50000
Split: train
Root Location: ./data
Transforms (if any): None
Target Transforms (if any): None
(<PIL.Image.Image image mode=RGB size=32x32 at 0x1F8A8FFDB38>, 6)
1 | plt.imshow(example[0][0]) |
1.2 数据Transform
我们可以看到导入的原始数据是PIL的Image对象,这是无法用来训练的,所以我们必须要把原始的数据进行一些转换。
第一个转换是ToTensor
: 1
2class torchvision.transforms.ToTensor:
def __call__(pic) -> Tensor
- Converts a PIL Image or numpy.ndarray (H x W x C) to (C x H x W) Tensor
- Scale的问题
- 如果Image的模式为(L, LA, P, I, F, RGB, YCbCr, RGBA, CMYK, 1)或者ndarray的类型为
np.uint8
,则会将输入从[0, 255)
的范围自动Scale到[0, 1)
- 否则,不做Scale 这个函数可以说是专门为图像数据的读入设计的,还很贴心的自带归一化操作。
- 如果Image的模式为(L, LA, P, I, F, RGB, YCbCr, RGBA, CMYK, 1)或者ndarray的类型为
第二个转换是Normalize
,我们希望把数据范围变成[-1, 1]
,这就需要Normalize
了: 1
2class torchvision.transforms.Normalize(mean, std, inplace=False):
def __call__(tensor) -> Normalized Tensor image
解释如下:
Normalize a tensor image with mean and standard deviation.
Given mean:(M1,...,Mn)
and std:(S1,..,Sn)
for n channels, this transform will normalize each channel of the inputtorch.*Tensor
i.e. \(input[channel] = \frac{input[channel] - mean[channel]}{std[channel]}\)
我们现在的范围是[0, 1)
,所以只要把mean和std都设置为0.5,就可以把范围变换到[-1, 1)
。
为了方便使用,我们可以使用一个工具性质的类来辅助完成多次变换: 1
class torchvision.transforms.Compose(transforms: list)
现在正式导入数据:
1 | # 导入训练集 |
Files already downloaded and verified
Files already downloaded and verified
可以查看一下导入的数据:
1 | print(testset[0][0].shape, classes[testset[0][1]]) |
torch.Size([3, 32, 32]) cat
50000 10000
我们可以看到,导入后的数据是个\(C \times H \times W\)的Tensor,其中第一个维度代表了RGB。
1.3 DataLoader
到目前为止,我们似乎已经把该做的都做好了,但是我们注意到这个例子中我们的数据量还是不小的,一般而言在数据量比较大是,我们会使用mini-batch的方法来训练,所以有必要使用一个用于生成batch的工具,这就是DataLoader
。
1 | class torch.utils.data.DataLoader( |
这玩意咋用呢,看一下它的主要参数:
- dataset: 它必须是
torch.utils.data.Dataset
的子类,实现了__len__
和__getitem__
方法的重写,幸运的是我们导入的两个set已经是了 - batch_size: 一个batch里有几个样本,默认为1
- shuffle: 如果设置为True,那么每个epoch都会洗一次牌
- num_workers: 数据加载时使用多少个子进程,如果是0,代表只在main进程中加载
构造好了对象之后,可以直接进行迭代使用。
1 | trainloader = torch.utils.data.DataLoader(trainset, batch_size=10, |
2. 构造神经网络
现在让我们来构造一个卷积神经网络,首先导入nn
1 | import torch.nn as nn |
下面就是构建卷积神经网络了,注意各层的维度控制,用到的API如下: 1
2
3
4
5Conv2d(in_channels, out_channels, kernel_size,
stride=1, padding=0, dilation=1, groups=1, bias=True)
MaxPool2d(kernel_size, stride=None, padding=0,
dilation=1, return_indices=False, ceil_mode=False)
Softmax(dim=None)
在构造卷积时最麻烦的就是要注意维度的问题了,虽然很繁琐,但我觉得既然是初学有必要再自己分析一波:
- 输入图片
n x (32 x 32 x 3)
,使用6个(5 x 5 x 3)
的filter,输出结果为n x (28 x 28 x 6)
- 输入数据
n x (28 x 28 x 6)
,使用(2 x 2)
的池化单元,stride为2,输出结果为n x (16 x 16 x 6)
- 输入数据
n x (16 x 16 x 6)
,使用16个(5 x 5 x 6)
的filter,输出结果为n x (12 x 12 x 16)
- 输入数据
n x (12 x 12 x 16)
,使用(2 x 2)
的池化单元,stride为2,输出结果为n x (5 x 5 x 16)
- 进入全连接层,后面的略
另外一点,在Pytorch官方的Tutorial中,是没有最后一个SoftMax层的,而是直接输出选择最大的那个单元(也就是所谓的HardMax),虽然结果都一样。
1 | class Net(nn.Module): |
3. 训练
1 | net = Net() |
1 | criterion = nn.CrossEntropyLoss() |
1 | # 注意epoch和iteration不同,epoch是指完全训练完整个数据集 |
1 | %%time |
[1, 2000] loss: 2.151
[1, 4000] loss: 2.077
[2, 2000] loss: 2.037
[2, 4000] loss: 2.020
Finished Training
Wall time: 2min 26s
4. 预测
4.1 展示效果
首先来编写一下imshow
函数,它会接受一个经过我们之前处理的image的Tensor,然后绘制出来: - 首先进行unnormalize操作,把取值变回到[0, 1)
- 转换成numpy,进行转置,因为之前经过ToTensor
操作后,image变成了(C, H, W),现在我们变回(H, W, C) - 使用plt.imshow
,它接受代表图片的矩阵,可以是0255的整型也可以是01的浮点型
1 | def imshow(img): |
然后我们来看一看第一批batch中前五个图片和预测的情况,这里用到了这个函数: 1
2
3
4torchvision.utils.make_grid(
tensor, nrow=8, padding=2, normalize=False,
range=None, scale_each=False, pad_value=0
)
它的功能是把一系列的图片拼在一起作为一张图片展示。
1 | dataiter = iter(testloader) |
GroundTruth: cat ship ship plane frog
现在看看神经网络的输出。 其中torch.max()
1 | outputs = net(images) |
Predicted: cat ship ship plane deer
还不错!
4.2 整体预测
1 | correct = 0 |
Accuracy of the network on the 10000 test images: 44 %
1 | class_correct = list(0. for i in range(10)) |
Accuracy of plane : 48 %
Accuracy of car : 47 %
Accuracy of bird : 18 %
Accuracy of cat : 43 %
Accuracy of deer : 38 %
Accuracy of dog : 34 %
Accuracy of frog : 63 %
Accuracy of horse : 56 %
Accuracy of ship : 51 %
Accuracy of truck : 37 %
5. 使用GPU
1 | torch.cuda.is_available() |
True
1 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") |
迁移到GPU上,然后训练一下试试:
1 | net = Net().cuda() |
1 | %%time |
[1, 2000] loss: 2.159
[1, 4000] loss: 2.090
[2, 2000] loss: 2.038
[2, 4000] loss: 2.019
Finished Training
Wall time: 3min 46s
What?怎么还变慢了?实际上是因为数据量太小了,而转化成GPU的花费比节省的时间还大,如果数据量再大一些,区别就出来了!