参考的是唐进民的《深度学习之PyTorch实战计算机视觉》6.4部分,代码部分按照原书的来会有报错,本文给出的是修改后的可以完整编译的代码。
参考书本学习过程中出现的一些报错以及修改思路可以参考PyTorch实战手写数字识别

现在来进行一个基于 PyTorch 框架使用神经网络实战手写数字识别的实例:

  • 使用提供的训练数据对搭建好的神经网络模型进行训练并完成参数优化
  • 使用优化好的模型对测试数据进行预测;
  • 对比预测值和真实值之间的损失值,同时计算出结果预测的准确率。

1 torch和torchvision

torch和torchvision是 PyTorch 中的两个核心的包。我们之前已经接触了 torch包的一部分内容,比如使用了 torch.nn 中的线性层加激活函数配合 torch.optim 完成了神经网络模型的搭建和模型参数的优化,并使用了 torch.autograd 实现自动梯度的功能(见专栏人工智能实例),接下来会介绍如何使用 torch.nn 中的类来搭建卷积神经网络。

1.1 torchvision 包

torchvision 包的主要功能是实现数据的处理、导入和预览等,所以如果需要对计算机视觉的相关问题进行处理,就可以借用在 torchvision 包中提供的大量的类来完成相应的工作。以下是torchvision的构成:

torchvision.datasets: 一些加载数据的函数及常用的数据集接口;
torchvision.models: 包含常用的模型结构(含预训练模型),例如AlexNet、VGG、ResNet等;
torchvision.transforms: 常用的图片变换,例如裁剪、旋转等;
torchvision.utils: 其他的一些有用的方法。

'''导入必要的包'''
import torch
from torchvision import datasets, transforms  #此实例只用到了 torchvision 中 datasets, transforms 这两个子包
from torch.autograd import Variable

1.2 torch.transforms

在 torch.transforms 中有大量的数据变换类,其中有很大一部分可以用于实现数据增强(Data Argumentation)。若在我们需要解决的问题上能够参与到模型训练中的图片数据非常有限,则要通过对有限的图片数据进行各种变换,来生成新的训练集,这些变换可以是缩放、水平或者垂直翻转等,都是数据增强的方法。

不过在手写数字识别的问题上可以不使用数据增强的方法,因为可用于模型训练的数据已经足够了。

在 torch.transforms 中提供了丰富的类对载入的数据进行变换,比如数据类型转换、归一化和大小缩放等。在此实例中,我们需要将图片类型转换为 Tensor 类型,且

'''定义transform'''
transform = transforms.Compose([transforms.ToTensor(),
                                transforms.Lambda(lambda x: x.repeat(3,1,1)),
                                transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))
                               ])

上面的代码中:

  • 可以将 torchvision.transforms.Compose 类看作一种容器,它能够同时对多种数据变换进行组合。传入的参数是一个列表,列表中的元素就是对载入的数据进行的各种变换操作。
  • ToTensor() 能够把图片的灰度范围从0 ~ 255 变成0 ~ 1之间,将 shape 为 (H, W, C) 的 nump.ndarray 或 img 转为 shape 为 (C, H, W) 的 tensor,其将每一个数值归一化到 [0,1],其归一化方法比较简单,直接除以 255 即可,实现数据类型转换。

  • 作标准差变换法 transforms.Normalize() 先将输入归一化到(0,1),再使用公式 “(x-mean)/std”,将每个元素分布到(-1,1)。具体地说,对每个通道而言,Normalize执行操作:原来的 0 ~ 1 最小值 0 则变成 (0-0.5)/0.5=-1,而最大值 1 则变成 (1-0.5)/0.5=1。在经过标准化变换之后,数据全部符合均值为 0、标准差为 1 的标准正态分布。

1.3 torchvision.datasets

使用 torchvision.datasets + 需要下载的数据集的名称 可以轻易实现对这些数据集的训练集和测试集的下载,比如手写数字数据集的名称是 MNIST,那么代码就是 torchvision.datasets.MNIST。其他常用的数据集(如 COCO、ImageNet、CIFCAR 等)同理。

'''实现数据集下载'''
data_train = datasets.MNIST(root = "./data/", 
                            train = True, 
                            transform = transform,                             
                            download = True)
data_test = datasets.MNIST(root="./data/",                            
                           train = False,
                           transform = transform,)

2 数据预览和数据装载

在数据下载完成并且载入(对图片的处理)后,我们还需要对数据进行装载(处理完成后,将这些图片打包好送给我们的模型进行训练,装载就是这个打包的过程)。

'''使用 torch.utils.data.DataLoader 类对数据装载'''
data_loader_train = torch.utils.data.DataLoader(dataset = data_train,
                                               batch_size = 64,
                                               shuffle = True)
data_loader_test = torch.utils.data.DataLoader(dataset = data_test,
                                               batch_size = 64,
                                               shuffle = True)

'''选取其中一个批次的数据进行预览'''
images, labels = next(iter(data_loader_train))  #使用 iter 和 next 获取其中一个批次的图片数据及标签
img = torchvision.utils.make_grid(images)  # make_grid 类方法将一个批次的图片构造成网格模式

img = img.numpy().transpose(1,2,0)  #完成原始数据类型的转换和数据维度的交换
std = [0.5,0.5,0.5]
mean = [0.5,0.5,0.5]
img = img * std + mean
print([labels[i] for i in range(64)])
plt.imshow(img)

【关于 torchvision.utils.make_grid:】

  • torchvision.utils 中的 make_grid 类方法将一个批次的图片构造成网格模式
  • 传递给它的参数数就是一个批次的装载数据
  • 每个批次的装载数据都是 4 维的:(batch_size, channel, height, weight),通过 torchvision.utils.make_grid 之后,图片的维度变成了(channel, height, weight),这个批次的图片全部被整合到了一起,所以在这个维度中对应的值也和之前不一样了,但是色彩通道数保持不变

【关于 numpy 和 transpose:】

若我们想使用 Matplotlib 将数据显示成正常的图片形式,则使用的数据首先必须是数组,其次这个数组的维度必须是(height,weight,channel),即色彩通道数在最后面。所以我们要通过 numpy 和 transpose 完成原始数据类型的转换和数据维度的交换,这样才能够使用 Matplotlib 绘制出正确的图像。

上面代码编译之后的结果是先打印输出了这个批次中的 64 张图片对应的全部标签,然后才对这个批次中的所有图片数据进行显示:

[tensor(2), tensor(9), tensor(8), tensor(8), tensor(5), tensor(0), tensor(1), tensor(3), tensor(0), tensor(7), tensor(0), tensor(1), tensor(5), tensor(1), tensor(7), tensor(3), tensor(8), tensor(7), tensor(1), tensor(8), tensor(9), tensor(0), tensor(0), tensor(5), tensor(7), tensor(4), tensor(1), tensor(6), tensor(0), tensor(8), tensor(7), tensor(9), tensor(9), tensor(5), tensor(6), tensor(4), tensor(3), tensor(7), tensor(4), tensor(4), tensor(7), tensor(7), tensor(2), tensor(3), tensor(8), tensor(0), tensor(8), tensor(6), tensor(3), tensor(6), tensor(6), tensor(1), tensor(7), tensor(3), tensor(3), tensor(9), tensor(8), tensor(1), tensor(2), tensor(1), tensor(9), tensor(9), tensor(7), tensor(4)]
<matplotlib.image.AxesImage at 0x1c887c84670>

在这里插入图片描述

3 模型搭建和参数优化

已经顺利完成了数据装载,可以开始编写卷积神经网络模型的搭建和参数优化的代码了。这个模型包含了卷积层、激活函数、池化层、全连接层(介绍在之前的深度学习计算机视觉理论基础(PyTorch)已经讲过了),所以在结构上会和之前搭建复杂神经网络同时优化参数有所区别。

不过各个部分的功能实现依然是通过 torch.nn 中的类来完成的,比如:

  • 卷积层使用 torch.nn.Conv2d 类方法来搭建;
  • 激活层使用 torch.nn.ReLU 类方法来搭建;
  • 池化层使用 torch.nn.MaxPool2d 类方法来搭建;
  • 全连接层使用 torch.nn.Linear 类方法来搭建。

3.1 搭建卷积神经网络模型

'''搭建一个在结构层次上有所简化的卷积神经网络模型'''
class Model(torch.nn.Module):
    
    def __init__(self):
        super(Model, self).__init__()
        self.conv1 = torch.nn.Sequential(torch.nn.Conv2d(1, 64, kernel_size=3, stride=1, padding=1),
                                        torch.nn.ReLU(),
                                        torch.nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
                                        torch.nn.ReLU(),
                                        torch.nn.MaxPool2d(stride=2, kernel_size=2))
        
        self.dense = torch.nn.Sequential(torch.nn.Linear(14*14*128, 1024),
                                        torch.nn.ReLU(),
                                        torch.nn.Dropout(p=0.5),
                                        torch.nn.Linear(1024, 10))
        def forward(self, x):
            x = self.conv1(x)
            x = x.view(-1, 14*14*128)
            x = self.dense(x)
            return x 

【关于torch.nn.Conv2d:】

用于搭建卷积神经网络的卷积层,主要的输入参数(都是整型)有:

  • 输入通道数、输出通道数:用于确定输入、输出数据的层数
  • 卷积核大小
  • 卷积核移动步长
  • Padding 值:值为 0 时表示不进行边界像素的填充,如果值大于 0,那么增加数字所对应的边界像素层数

【关于torch.nn.MaxPool2d:】

用于实现卷积神经网络中的最大池化层,主要的输入参数是池化窗口大小、池化窗口移动步长和 Paddingde 值(都是整型)。

【关于torch.nn.Dropout:】

torch.nn.Dropout 类用于防止卷积神经网络在训练的过程中发生过拟合,其工作原理简单来说就是在模型训练的过程中,以一定的随机概率将卷积神经网络模型的部分参数归零,以达到减少相邻两层神经连接的目的,如图所示:
在这里插入图片描述

打叉的神经节点就是被随机抽中并丢弃的神经连接,正是因为选取方式的随机性,所以在模型的每轮训练中选择丢弃的神经连接也是不同的,这样做是为了让我们最后训练出来的模型对各部分的权重参数不产生过度依赖,从而防止过拟合。
对于torch.nn.Dropout 类,我们可以对随机概率值的大小进行设置,如果不做任何设置,就使用默认的概率值 0.5。

【关于前向传播 forward 函数:】

过程如下:

  • 首先,经过 self.conv1 进行卷积处理;
  • 然后进行 x.view(−1, 1414128),对参数实现扁平化,因为之后紧接着的就是全连接层,所以如果不进行扁平化,则全连接层的实际输出的参数维度和其定义输入的维度将不匹配,程序会报错;
  • 最后,通过 self.dense 定义的全连接进行最后的分类。

3.2 训练模型、优化参数

首先,定义在训练之前使用哪种损失函数和优化函数(因为没有定
义学习速率的值,所以使用默认值):

'''定义在训练之前使用哪种损失函数和优化函数'''
model = Model()
cost = torch.nn.CrossEntropyLoss()  #计算损失值的损失函数使用的是交叉熵
optimizer = torch.optim.Adam(model.parameters())  #优化函数使用的是 Adam 自适应优化算法,需要优化的参数是在 Model 中生成的全部参数

print(model)  #查看搭建好的模型的完整结构
Model(
  (conv1): Sequential(
    (0): Conv2d(1, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU()
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (dense): Sequential(
    (0): Linear(in_features=25088, out_features=1024, bias=True)
    (1): ReLU()
    (2): Dropout(p=0.5, inplace=False)
    (3): Linear(in_features=1024, out_features=10, bias=True)
  )
)

卷积神经网络模型进行模型训练和参数优化的代码如下:

'''模型训练和参数优化'''
n_epochs = 5

for epoch in range(n_epochs):
    running_loss = 0.0
    running_correct = 0
    
    for data in data_loader_train:
        X_train, y_train = data
        X_train, y_train = Variable(X_train), Variable(y_train)
        outputs = model(X_train)
        _,pred = torch.max(outputs.data, 1)
        optimizer.zero_grad()
        loss = cost(outputs, y_train)
        
        loss.backward()
        optimizer.step()
        running_loss += loss.data
        running_correct += torch.sum(pred == y_train.data)
    
    testing_correct = 0
    
    for data in data_loader_test:
        X_test, y_test = data 
        X_test, y_test = Variable(X_test), Variable(y_test) 
        outputs = model(X_test) 
        _, pred = torch.max(outputs.data, 1) 
        testing_correct += torch.sum(pred == y_test.data)
            
    print("Epoch {}: loss = {:.4f}, train accuracy = {:.4f}%, test accuracy = {:.4f}".format(
        epoch, running_loss/len(data_train), 
        100*running_correct/len(data_train),
        100*testing_correct/len(data_test)))
Epoch 0: loss = 0.0007, train accuracy = 98.5133%, test accuracy = 98.4100
Epoch 1: loss = 0.0005, train accuracy = 98.9283%, test accuracy = 98.5900
Epoch 2: loss = 0.0004, train accuracy = 99.3133%, test accuracy = 98.8300
Epoch 3: loss = 0.0003, train accuracy = 99.3667%, test accuracy = 98.7800
Epoch 4: loss = 0.0002, train accuracy = 99.5633%, test accuracy = 98.7800

4 验证模型的准确性

为了验证我们训练的模型是不是真的已如结果显示的一样准确,则最好的方法就是随机选取一部分测试集中的图片,用训练好的模型进行预测,看看和真实值有多大的偏差,并对结果进行可视化。

data_loader_test = torch.utils.data.DataLoader(dataset=data_test,
                                              batch_size=64,
                                              shuffle=True)
X_test, y_test = next(iter(data_loader_test))
inputs = Variable(X_test)
pred = model(inputs)
_, pred = torch.max(pred, 1)

print('Predict Label is:')
for i in range(len(pred.data)):
    print(pred.data[i], end=' ')
    if (i+1) % 8 == 0:
        print('\n')
        
print('Real Label is:')
for i in range(len(y_test)):
    print(y_test.data[i], end=' ')
    if (i+1) % 8 == 0:
        print('\n')
        
img = torchvision.utils.make_grid(X_test)
img = img.numpy().transpose(1, 2, 0)

std = [0.5, 0.5, 0.5]
mean = [0.5, 0.5, 0.5]
img = img * std + mean
plt.imshow(img)

test_correct = 0
for i in range(len(pred)):
    if pred.data[i]==y_test.data[i]:
        test_correct += 1
print('test_correct:{:.4f}%'.format(100*test_correct/len(pred)))
Predict Label is:
tensor(5) tensor(9) tensor(1) tensor(1) tensor(1) tensor(1) tensor(2) tensor(7) 

tensor(9) tensor(8) tensor(1) tensor(3) tensor(1) tensor(1) tensor(8) tensor(2) 

tensor(8) tensor(5) tensor(5) tensor(9) tensor(7) tensor(4) tensor(0) tensor(0) 

tensor(5) tensor(4) tensor(2) tensor(2) tensor(1) tensor(7) tensor(8) tensor(8) 

tensor(1) tensor(2) tensor(2) tensor(3) tensor(2) tensor(7) tensor(2) tensor(8) 

tensor(9) tensor(8) tensor(5) tensor(8) tensor(1) tensor(5) tensor(8) tensor(8) 

tensor(5) tensor(0) tensor(8) tensor(4) tensor(0) tensor(4) tensor(1) tensor(1) 

tensor(6) tensor(8) tensor(4) tensor(6) tensor(3) tensor(8) tensor(5) tensor(9) 

Real Label is:
tensor(5) tensor(5) tensor(1) tensor(1) tensor(1) tensor(1) tensor(2) tensor(7) 

tensor(9) tensor(8) tensor(1) tensor(3) tensor(1) tensor(1) tensor(8) tensor(2) 

tensor(8) tensor(5) tensor(5) tensor(9) tensor(7) tensor(4) tensor(0) tensor(0) 

tensor(5) tensor(4) tensor(2) tensor(2) tensor(1) tensor(7) tensor(8) tensor(8) 

tensor(1) tensor(2) tensor(2) tensor(3) tensor(2) tensor(7) tensor(2) tensor(8) 

tensor(9) tensor(8) tensor(5) tensor(8) tensor(1) tensor(5) tensor(8) tensor(8) 

tensor(5) tensor(0) tensor(8) tensor(4) tensor(0) tensor(4) tensor(1) tensor(1) 

tensor(6) tensor(8) tensor(4) tensor(6) tensor(3) tensor(8) tensor(5) tensor(9) 

test_correct:98.4375%