【实战篇】PyTorch入门指南

本文是对 Neural Network Programming - Deep Learning with PyTorch 系列博客的翻译与整理,英语基础比较好的同学推荐阅读原汁原味的博客。

文章目录

    • 1. 什么是MNIST数据集
    • 2. 什么是Fashion-MNIST数据集
    • 3. Extract, Transform, and Load (ETL) data
    • 4. Dataset 和 DataLoader 的工作机制
    • 5. torch.nn 包
    • 6. 构建一个神经网络
    • 7. 卷积神经网络超参数
    • 8. 卷积神经网络可学习的参数
    • 9. 权重张量的形状
    • 10. torch.nn.Linear源码分析
    • 11. 实现前向传播算法
    • 12. 预测单张图片的类别
    • 13. 预测批量图片的类别
    • 14 训练一个神经网络

计算机程序通常由两个主要部分组成:代码和数据。在传统的编程中,程序员的工作是直接编写软件或代码,但是在深度学习和神经网络中,可以说软件就是网络本身,特别是在训练过程中自动产生的网络权值。数据是深度学习的主要组成部分,尽管让我们的神经网络从数据中学习是我们作为神经网络程序员的任务,但我们仍然有责任了解我们实际用于训练的数据的性质和历史。

1. 什么是MNIST数据集

  MNIST数据集,全称是 Modified National Institute of Standards and Technology database,它是一个著名的手写数字数据集,通常用于训练机器学习的图像处理系统。NIST是国家标准与技术协会的缩写,M 代表修改过的,这是因为有一个原始的NIST数据集被修改为MNIST。

【实战篇】PyTorch入门指南_第1张图片

  MNIST因其被使用的频率而闻名,常见的原因有两个:

  • 初学者使用它很容易上手

  • 研究人员使用它来基准化(比较)不同的模型。

  这个数据集包含 70,000 张手写体图片,并进行如下分割:

  • 60,000 张训练图片
  • 10,000 张测试图片

  由于 MNIST 数据集对于深度学习来说,有点太简单,所以后面有人创建了 Fashion-MNIST 数据集。

2. 什么是Fashion-MNIST数据集

  顾名思义,Fashion-MNIST是一个关于时尚产品的数据集。具体来说,该数据集有以下十类时尚项目:

Index Label
0 T-shirt/top
1 Trouser
2 Pullover
3 Dress
4 Coat
5 Sandal
6 Shirt
7 Sneaker
8 Bag
9 Ankle boot

  数据集中的部分图片如下所示:

【实战篇】PyTorch入门指南_第2张图片

  Fashion-MNIST 数据集来源于Zalando,该公司内部员工创建了此数据集,之所以名字中带MNIST,是因为他们想用Fashion-MNIST来代替MNIST,出于此原因,Fashion-MNIST 数据集被设计成尽可能接近原始MNIST数据集(60,000张训练图片,10,000张测试图片,28 * 28的灰度图),但是由于拥有比手写图像更复杂的数据而在训练中引入更高的难度。

  该数据集被设计为原始MNIST的完全替代,通过使Fashion-MNIST数据集规格与原始MNIST规格相匹配,可以顺利地实现从旧规范到新规范的转换。该论文声称,切换数据集所需的唯一更改是通过指向Fashion数据集来更改MNIST数据集的获取位置的URL。

  PyTorch 提供的 torchvision 包,可以使我们更方便地导入 Fashion-MNIST数据集。

3. Extract, Transform, and Load (ETL) data

  机器学习/深度学习工程的第一步是准备数据,我们将遵循以下的 ETL 流程:

  • 从数据源提取(extract)数据
  • 将数据转换(transform)为期望格式
  • 把数据加载(load)到合适的结构中

  在我们的项目中,该过程分别对应为:

  • Extract – 从数据源中获取Fashion-MNIST图像
  • Transform – 把数据转换为 tensor 的格式
  • Load – 将我们的数据放在DataLoader类的实例对象中,以便于访问

  基于这些目的,PyTorch 提供了以下两个类:

描述
torch.utils.data.Dataset 用于表示数据集的抽象类
torch.utils.data.DataLoader 包装数据集并提供对基础数据的访问

  抽象类 是一个Python类,它里面的方法我们必须要实现,我们可以通过创建一个子类来扩展Dataset类的功能,从而创建一个自定义数据集类,这个新的子类可以被传递到PyTorch的 DataLoader对象。

  我们将使用 torchvision 包内置的Fashion-MNIST数据集,因此我们的项目不必再重新创建一个新的子类,只需知道时尚MNIST内置的dataset类是在幕后完成这项工作的。

  torchvision 包允许我们访问以下资源:

  • Datasets (like MNIST and Fashion-MNIST)
  • Models (like VGG16)
  • Transforms
  • Utils

  我们用下面代码来获取 Fashion-MNIST 数据集:

> train_set = torchvision.datasets.FashionMNIST(
    root='./data' # 数据集保持在硬盘中的路径
    ,train=True # 是否为训练集
    ,download=True
    ,transform=transforms.Compose([transforms.ToTensor()]) # 转换操作
)

  要为我们的训练集创建一个DataLoader包装器,我们这样做:

train_loader = torch.utils.data.DataLoader(
	train_set
    ,batch_size=1000
    ,shuffle=True
)

4. Dataset 和 DataLoader 的工作机制

  • PyTorch Dataset: Working with the training set

  我们先看一下,Dataset 的实例 train_set,有哪些可以执行的操作,来探索我们的数据。

> len(train_set) # 数据集的大小
60000

# Before torchvision 0.2.2
> train_set.train_labels 
tensor([9, 0, 0, ..., 3, 0, 5])

# Starting with torchvision 0.2.2
> train_set.targets 
tensor([9, 0, 0, ..., 3, 0, 5])

  如果我们想知道,数据集中每个标签对应的样本数量,调用bincount()方法:

# Before torchvision 0.2.2
> train_set.train_labels.bincount()
tensor([6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000])

# Starting with torchvision 0.2.2
> train_set.targets.bincount()
tensor([6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000])

  要访问训练集中的单个元素,我们首先将 train_set 对象传递给Python内置的iter()函数,该函数返回一个数据流对象,然后再用Python内置的next()函数来获取数据流中的下一个元素。

> sample = next(iter(train_set))
> len(sample)
2

  我们看到返回的 sample 的长度为2,那是因为一个 sample 对象包含一个 对。

> type(image)
torch.Tensor

# Before torchvision 0.2.2
> type(label)
torch.Tensor

# Starting at torchvision 0.2.2
> type(label)
int

  我们可以再看看 image 和 label 的形状:

> image.shape
torch.Size([1, 28, 28]) 

> torch.tensor(label).shape
torch.Size([])

> image.squeeze().shape
torch.Size([28, 28])
  • PyTorch DataLoader: Working with batches of data

  我们将开始创建一个批处理大小为10的数据加载器:

> display_loader = torch.utils.data.DataLoader(
    train_set, batch_size=10
  )

  和前面从 train_set 中获取一个数据实例一样,我们从 display_loader 中获取一个 batch 数据,也是通过调用 iter()next() 函数。

# note that each batch will be different when shuffle=True
> batch = next(iter(display_loader))
> print('len:', len(batch))
len: 2

  这里 batch 的长度为2是因为 batch 由两个张量组成:

> images, labels = batch

> print('types:', type(images), type(labels))
> print('shapes:', images.shape, labels.shape)
types: <class 'torch.Tensor'> <class 'torch.Tensor'>
shapes: torch.Size([10, 1, 28, 28]) torch.Size([10])

  如果想要绘制一个 batch 中的所有图像,可以采用torchvision.utils.make_grid()函数,具体如下:

> grid = torchvision.utils.make_grid(images, nrow=10)

> plt.figure(figsize=(15,15))
> plt.imshow(np.transpose(grid, (1,2,0)))
> # plt.imshow(grid.permute(1,2,0)) # 和上面效果一样

> print('labels:', labels)
labels: tensor([9, 0, 0, 3, 0, 2, 7, 2, 5, 5])

  现在我们了解了一些 prepare the data 的方法,接下来开始第二步
build the model

5. torch.nn 包

  在PyTorch中构建神经网络,需要使用torch.nn包,这是PyTorch的神经网络(nn)库,我们通常是这样导入包的:

import torch.nn as nn

  构建神经网络所需的主要组件是layer,而PyTorch的神经网络库torch.nn 中包含一些类,可以帮助我们构建层。而神经网络中的layer,主要包含两个组件:

  • 转换操作 (code)
  • 权重参数的集合 (data)

  在torch.nn包中,有一个类叫做Module,它是所有神经网络模块的基类,包括layer。这意味着PyTorch中的所有layer都扩展了nn.Module类,并继承了PyTorch在nn.Module类中的所有内置功能。在OOP(面向对象编程)中,这个理念被称为继承。

  当我们将一个张量作为输入传递给网络时,张量通过每一层转换向前流动,直到张量到达输出层,张量通过网络向前流动的过程称为向前传递,也因此, nn.module类中提供了一个forward()方法,每个继承它的类,都必须实现这个方法,它其实也就是我们前面提到的转换操作。

  当我们在具体实现 forward() 方法时,一般需要调用 nn.functional 包中提供的函数,这个包为我们提供了许多可以用于构建层的神经网络操作。

6. 构建一个神经网络

  基于前面的学习,我们知道了构建一个网络主要分为下面几步:

  • 创建一个继承了 nn.Module 类的神经网络类
  • 在该类的构造函数中,用torch.nn中预构的层来定义网络层,作为类属性
  • 使用网络层和nn.functional中的函数来定义 forward() 函数

  我们首先来看第一步,创建一个简单的类来表示神经网络:

class Network:
    def __init__(self):
        self.layer = None

    def forward(self, t):
        t = self.layer(t)
        return t

  我们的类要继承 nn.Module,所以我们还要再做两件事情:

class Network(nn.Module): # 1. 指定nn.Module类
    def __init__(self):
        super().__init__() # 2. 对父类构造函数的调用
        self.layer = None

    def forward(self, t):
        t = self.layer(t)
        return t

  这两点小改变将我们简单的神经网络转换为PyTorch神经网络,使得我们的 Network 类有了 nn.Module 类的所有函数。

  我们再来看第二步,定义网络层作为类属性:

class Network(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
        self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)
        
        self.fc1 = nn.Linear(in_features=12 * 4 * 4, out_features=120)
        self.fc2 = nn.Linear(in_features=120, out_features=60)
        self.out = nn.Linear(in_features=60, out_features=10)
        
    def forward(self, t):
        return t

   可以看到,在我们的 Network 类中,有五个层被定义为属性。我们有两个卷积层self.conv1和self.conv2,以及三个线性层self.fc1、self.fc2和self.out。

  我们在fc1和fc2中使用缩写fc,因为linear layers也称为fully connected layers。它们还有第三个名字,叫做ldense layers。 这三种叫法都是指的同一类型的层,PyTorch使用单词 linear,因此命名为 nn.linear

7. 卷积神经网络超参数

  我们的每一层都扩展了PyTorch的nn.Module类,所以每一层中都封装了两个部分,前向传播函数和权重向量,例如下面的卷积层nn.Conv2d

self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)

  为了更好的理解我们定义的层,我们来看看层的构造函数中所包含的参数值。当我们构造一个层时,我们需要将参数值传递给层的构造函数,在我们的卷积层中有三个参数,线性层中有两个参数。

  • Convolutional layers
    • in_channels
    • out_channels
    • kernel_size
  • Linear layers
    • in_features
    • out_features

  我们先来看看需要程序员手动设定的超参数:

Paremeter Description
kernel_size 设置filter大小(filter和kernel含义相同)
out_channels 设置filter个数
out_features 设置输出张量的大小

  还有一些超参数,它的设定依赖于我们的数据流。在self.conv1层中的超参数in_channels,它的值应该等于输入图像的颜色通道数;在其后的几个卷积层的in_channels的值则需等于它上一层的out_channels;当我们从卷积层切换到全连接层时,我们需要 flatten 我们的 tensor,于是第一个全连接层的in_features的值为 12 ∗ 4 ∗ 4 12 * 4 *4 1244;最后到输出层时,我们的数据集总共有10个类别,因此我们的输出层的out_features的值应该为10。

8. 卷积神经网络可学习的参数

  可学习参数指的是在训练过程中会不断更新的参数,我们会给它们随机初始化一些值,然后在每一轮的迭代中,更新这些值。那么在我们前面设计的网络中,这些可学习参数在哪呢?

class Network(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
        self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)
        
        self.fc1 = nn.Linear(in_features=12*4*4, out_features=120)
        self.fc2 = nn.Linear(in_features=120, out_features=60)
        self.out = nn.Linear(in_features=60, out_features=10)
        
    def forward(self, t):
        # implement the forward pass
        return t

  我们的可学习参数,实际上就是神经网络中的权重,而它们就位于我们定义的网络层之中,我们先获取一个Network类的实例,再来观察我们的权重:

> network = Network()                                    

  当这段代码执行时,类构造函数__init__(self)中的代码将会被调用,我们定义的网络层会被初始化,然后再返回一个网络类的实例,在我们开始使用我们的 network 实例之前,我们先看看打印它会输出什么:

> print(network)
Network(
    (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
    (conv2): Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
    (fc1): Linear(in_features=192, out_features=120, bias=True)
    (fc2): Linear(in_features=120, out_features=60, bias=True)
    (out): Linear(in_features=60, out_features=10, bias=True)
)

  我们接下来看看如何获取我们定义的网络层:

> network.conv1
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))

> network.conv2
Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))

> network.fc1
Linear(in_features=192, out_features=120, bias=True)

> network.fc2                                    
Linear(in_features=120, out_features=60, bias=True)

> network.out
Linear(in_features=60, out_features=10, bias=True)

  下一步我们可以获取网络层的权重,通过 network.conv1.weight 这行代码,它输出的是一个 tensor.

> network.conv1.weight
Parameter containing:
tensor([[[[ 0.0692,  0.1029, -0.1793,  0.0495,  0.0619],
            [ 0.1860,  0.0503, -0.1270, -0.1240, -0.0872],
            [-0.1924, -0.0684, -0.0028,  0.1031, -0.1053],
            [-0.0607,  0.1332,  0.0191,  0.1069, -0.0977],
            [ 0.0095, -0.1570,  0.1730,  0.0674, -0.1589]]],

        [[[-0.1392,  0.1141, -0.0658,  0.1015,  0.0060],
            [-0.0519,  0.0341,  0.1161,  0.1492, -0.0370],
            [ 0.1077,  0.1146,  0.0707,  0.0927,  0.0192],
            [-0.0656,  0.0929, -0.1735,  0.1019, -0.0546],
            [ 0.0647, -0.0521, -0.0687,  0.1053, -0.0613]]],

        [[[-0.1066, -0.0885,  0.1483, -0.0563,  0.0517],
            [ 0.0266,  0.0752, -0.1901, -0.0931, -0.0657],
            [ 0.0502, -0.0652,  0.0523, -0.0789, -0.0471],
            [-0.0800,  0.1297, -0.0205,  0.0450, -0.1029],
            [-0.1542,  0.1634, -0.0448,  0.0998, -0.1385]]],

        [[[-0.0943,  0.0256,  0.1632, -0.0361, -0.0557],
            [ 0.1083, -0.1647,  0.0846, -0.0163,  0.0068],
            [-0.1241,  0.1761,  0.1914,  0.1492,  0.1270],
            [ 0.1583,  0.0905,  0.1406,  0.1439,  0.1804],
            [-0.1651,  0.1374,  0.0018,  0.0846, -0.1203]]],

        [[[ 0.1786, -0.0800, -0.0995,  0.1690, -0.0529],
            [ 0.0685,  0.1399,  0.0270,  0.1684,  0.1544],
            [ 0.1581, -0.0099, -0.0796,  0.0823, -0.1598],
            [ 0.1534, -0.1373, -0.0740, -0.0897,  0.1325],
            [ 0.1487, -0.0583, -0.0900,  0.1606,  0.0140]]],

        [[[ 0.0919,  0.0575,  0.0830, -0.1042, -0.1347],
            [-0.1615,  0.0451,  0.1563, -0.0577, -0.1096],
            [-0.0667, -0.1979,  0.0458,  0.1971, -0.1380],
            [-0.1279,  0.1753, -0.1063,  0.1230, -0.0475],
            [-0.0608, -0.0046, -0.0043, -0.1543,  0.1919]]]], 
            requires_grad=True
)

9. 权重张量的形状

  站在卷积层的角度,权重张量就在我们设定的filter之中,而在代码中,filter实际上就是权重张量自身。

  层内的卷积运算是指该层的所有输入通道的feature map与该层的filter之间的运算,这意味着我们实际上进行的是两个张量之间的运算。(一次卷积是某个卷积核对所有输入通道的同一个区域进行卷积,而不是单个输入通道

  对于第一个卷积层,我们有1个颜色通道,用6个大小为 5 ∗ 5 5*5 55的卷积核进行卷积,所以最后输出的通道数也有6个。在PyTorch中,我们不会用6个权重张量来表示每个的filter,而是集中用一个权重张量来表示,注意每一个维度所代表的含义。

> network.conv1
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))

> network.conv1.weight.shape
torch.Size([6, 1, 5, 5])

> network.conv2
Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))

> network.conv2.weight.shape
torch.Size([12, 6, 5, 5])

  我们需要记住两点:

  • 所有的filter都只用一个张量来表示
  • 每个filter都具有depth维,该值对应输入的通道数

  在全连接层中,我们的输入、输出都是一阶张量,所以我们只需要一个二阶张量对它们进行转换即可,二阶张量也常被称为权重矩阵。

> network.fc1.shape
torch.Size([120, 192])

> network.fc2.shape                                    
torch.Size([60, 120])

> network.out.shape
torch.Size([10, 60])

  由于我们的输入输出都是列向量,我们所进行的运算为:
z l = W l a l − 1 z^l=W^la^{l-1} zl=Wlal1

  于是我们权重矩阵的shape,第一个元素值对应的是out_features,第二个元素值对应的是in_features,这一点我们需要理解。在PyTorch中,矩阵乘法用tensor.matmul()函数来表示:

> weight_matrix.matmul(in_features)
tensor([30., 40., 50.])

  最后一个问题是,如果想要一次性获取到网络中的所有参数,应该怎么做呢?可以通过下面的代码

for name, param in network.named_parameters():
    print(name, '\t\t', param.shape)

conv1.weight 	 torch.Size([6, 1, 5, 5])
conv1.bias 		 torch.Size([6])
conv2.weight 	 torch.Size([12, 6, 5, 5])
conv2.bias 		 torch.Size([12])
fc1.weight 		 torch.Size([120, 192])
fc1.bias 		 torch.Size([120])
fc2.weight 		 torch.Size([60, 120])
fc2.bias 		 torch.Size([60])
out.weight 		 torch.Size([10, 60])
out.bias 		 torch.Size([10])

10. torch.nn.Linear源码分析

  我们先来看看如何用矩阵乘法,把输入的特征向量转换为输出的特征向量:

in_features = torch.tensor([1,2,3,4], dtype=torch.float32)

weight_matrix = torch.tensor([
    [1,2,3,4],
    [2,3,4,5],
    [3,4,5,6]
], dtype=torch.float32)

> weight_matrix.matmul(in_features)
tensor([30., 40., 50.])

  再来看如何用 nn.Linear类来实现上面的转换:

> fc = nn.Linear(in_features=4, out_features=3, bias=False)

> fc.weight  # 随机初始化的权重
Parameter containing:
tensor([[ 0.2845,  0.4056,  0.0574, -0.2942],
        [-0.1213, -0.2582, -0.1599,  0.3142],
        [-0.0050,  0.1562,  0.3690, -0.4962]], requires_grad=True)

  那权重矩阵是在哪里生成的呢?不急,我们来看源码进行分析:

# torch/nn/modules/linear.py (version 1.0.1)

def __init__(self, in_features, out_features, bias=True):
    super(Linear, self).__init__()
    self.in_features = in_features
    self.out_features = out_features
    self.weight = Parameter(torch.Tensor(out_features, in_features)) # 权重矩阵
    if bias:
        self.bias = Parameter(torch.Tensor(out_features))
    else:
        self.register_parameter('bias', None)
    self.reset_parameters()

  传入一个特征向量,查看输出:

> in_features = torch.tensor([1,2,3,4], dtype=torch.float32)

> fc(in_features)
tensor([ 0.0912,  0.1394, -0.5704], grad_fn=<SqueezeBackward3>)

  我们发现一件事:PyTorch的神经网络模块是可以调用的 Python 对象! 关于这一点,我们稍后详细说明。现在的问题是,线性层的输出和我们上面的例子的输出还是有差别的,这是因为我们的权重矩阵是随机初始化的,我们可以显示地指定线性层的权重矩阵。

> fc.weight = nn.Parameter(weight_matrix)  
> fc(in_features)
tensor([30., 40., 50.], grad_fn=<SqueezeBackward3>)

  现在我们的输出和矩阵乘法的结果是一致的了,接下来我们来分析,为什么PyTorch的神经网络模块像个函数一样可以被调用(如 fc(in_features)),这是因为 PyTorch 的模块类,实现了 Python 中的另一个特殊函数__call__(),如果一个类实现了该方法,则只要调用对象实例,就会执行特殊的调用方法,我们再来看看源代码:

# torch/nn/modules/module.py (version 1.0.1)

def __call__(self, *input, **kwargs):
    for hook in self._forward_pre_hooks.values():
        hook(self, input)
    if torch._C._get_tracing_state():
        result = self._slow_forward(*input, **kwargs)
    else:
        result = self.forward(*input, **kwargs)
    for hook in self._forward_hooks.values():
        hook_result = hook(self, input, result)
        if hook_result is not None:
            raise RuntimeError(
                "forward hooks should never return any values, but '{}'"
                "didn't return None".format(hook))
    if len(self._backward_hooks) > 0:
        var = result
        while not isinstance(var, torch.Tensor):
            if isinstance(var, dict):
                var = next((v for v in var.values() if isinstance(v, torch.Tensor)))
            else:
                var = var[0]
        grad_fn = var.grad_fn
        if grad_fn is not None:
            for hook in self._backward_hooks.values():
                wrapper = functools.partial(hook, self)
                functools.update_wrapper(wrapper, hook)
                grad_fn.register_hook(wrapper)
    return result

11. 实现前向传播算法

  我们先回顾一下前面定义的Network类:

class Network(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
        self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)
        
        self.fc1 = nn.Linear(in_features=12*4*4, out_features=120)
        self.fc2 = nn.Linear(in_features=120, out_features=60)
        self.out = nn.Linear(in_features=60, out_features=10)
        
    def forward(self, t):
        # implement the forward pass
        return t

  神经网络的第一层是输入层,输入层返回的就是我们输入的向量:

# (1) input layer
t = t

  第二、三层是卷积层,需要进行卷积、激活和池化等操作,激活和池化是nn.functional包中的函数:

# (2) hidden conv layer
t = self.conv1(t)
t = F.relu(t) # import nn.functional as F
t = F.max_pool2d(t, kernel_size=2, stride=2)

# (3) hidden conv layer
t = self.conv2(t)
t = F.relu(t)
t = F.max_pool2d(t, kernel_size=2, stride=2)

  第四、五层是全连接层,其中第一个全连接层要进行flatten()操作,经过前向传播之后,还要进行激活操作。

# (4) hidden linear layer
t = t.reshape(-1, 12 * 4 * 4)
t = self.fc1(t)
t = F.relu(t)

# (5) hidden linear layer
t = self.fc2(t)
t = F.relu(t)

   第六层(最后一层)是输出层,它输出的向量长度,对应我们的类别数。

# (6) output layer
t = self.out(t)
# t = F.softmax(t, dim=1) # 可选

  在神经网络内部,我们通常使用relu()作为我们的非线性激活函数,但是对于输出层,当我们试图预测一个类别时,我们使用softmax()。此函数可以为每个预测类返回一个概率值,总和为1。

  但是在我们的例子中,我们不需要使用softmax(),因为我们将使用的cross_entropy()损失函数,已经隐式地对其输入执行softmax()操作,所以我们只返回上次线性转换的结果。

  将前面代码进行汇总,即可得到我们的前向传播算法。

def forward(self, t):
    # (1) input layer
    t = t

    # (2) hidden conv layer
    t = self.conv1(t)
    t = F.relu(t)
    t = F.max_pool2d(t, kernel_size=2, stride=2)

    # (3) hidden conv layer
    t = self.conv2(t)
    t = F.relu(t)
    t = F.max_pool2d(t, kernel_size=2, stride=2)

    # (4) hidden linear layer
    t = t.reshape(-1, 12 * 4 * 4)
    t = self.fc1(t)
    t = F.relu(t)

    # (5) hidden linear layer
    t = self.fc2(t)
    t = F.relu(t)

    # (6) output layer
    t = self.out(t)
    # t = F.softmax(t, dim=1)

    return t

12. 预测单张图片的类别

  注意,我们现在的网络还是未经训练过的。 目前只是测试如何预测单张图片,检查我们的网络是否为畅通的。所以我们先关闭PyTorch的梯度计算功能,避免它在有张量流过网络时自动建立计算图,这个图主要用于计算损失函数的梯度,帮助我们后面更新网络参数。

> torch.set_grad_enabled(False)
<torch.autograd.grad_mode.set_grad_enabled object at 0x7f6bb575fcf8>

  接下来我们创建一个Network类的实例,并从训练集中获取一个 image 对象。

> network = Network()

> sample = next(iter(train_set)) 
> image, label = sample 
> image.shape 
torch.Size([1, 28, 28]) 

  需要注意的是,我们的网络期望的输入是一个batch,于是我们只需要再增加一个维度:

> image.unsqueeze(0).shape
torch.Size([1, 1, 28, 28])

  于是再将它输入到网络中,进行预测:

> pred = network(image.unsqueeze(0)) # image shape needs to be 
  									 # (batch_size × in_channels × H × W)

> pred
tensor([[0.0991, 0.0916, 0.0907, 0.0949, 0.1013, 0.0922,
		 0.0990, 0.1130, 0.1107, 0.1074]])

> pred.shape
torch.Size([1, 10])

> pred.argmax(dim=1)
tensor([7])

> label
9

  注意到我们的network也是一个可以调用的对象,原因和前面的网络层一样。另外,我们发现pred.shape的值为 [ 1 , 10 ] [1,10] [1,10],这是因为我们输入的batch中,图像的个数只有一个,第一个轴中的元素个数等于batch size,如果我们希望输出的值代表图像属于每一类的概率,采用下面的代码:

> F.softmax(pred, dim=1)
tensor([[0.1096, 0.1018, 0.0867, 0.0936, 0.1102, 0.0929, 0.1083, 0.0998, 0.0943, 0.1030]])

> F.softmax(pred, dim=1).sum()
tensor(1.)

13. 预测批量图片的类别

  整个过程和预测单张图片类别是类似的,区别在于我们需要用到DataLoader.

> data_loader = torch.utils.data.DataLoader(
     train_set, batch_size=10)

> batch = next(iter(data_loader))
> images, labels = batch

> images.shape
torch.Size([10, 1, 28, 28])

> labels.shape
torch.Size([10])

   然后将images喂到我们的网络中,输出预测结果:

> preds = network(images)

> preds.shape
torch.Size([10, 10])

> preds
tensor(
    [
        [ 0.1072, -0.1255, -0.0782, -0.1073,  0.1048,  0.1142, -0.0804, -0.0087,  0.0082,  0.0180],
        [ 0.1070, -0.1233, -0.0798, -0.1060,  0.1065,  0.1163, -0.0689, -0.0142,  0.0085,  0.0134],
        [ 0.0985, -0.1287, -0.0979, -0.1001,  0.1092,  0.1129, -0.0605, -0.0248,  0.0290,  0.0066],
        [ 0.0989, -0.1295, -0.0944, -0.1054,  0.1071,  0.1146, -0.0596, -0.0249,  0.0273,  0.0059],
        [ 0.1004, -0.1273, -0.0843, -0.1127,  0.1072,  0.1183, -0.0670, -0.0162,  0.0129,  0.0101],
        [ 0.1036, -0.1245, -0.0842, -0.1047,  0.1097,  0.1176, -0.0682, -0.0126,  0.0128,  0.0147],
        [ 0.1093, -0.1292, -0.0961, -0.1006,  0.1106,  0.1096, -0.0633, -0.0163,  0.0215,  0.0046],
        [ 0.1026, -0.1204, -0.0799, -0.1060,  0.1077,  0.1207, -0.0741, -0.0124,  0.0098,  0.0202],
        [ 0.0991, -0.1275, -0.0911, -0.0980,  0.1109,  0.1134, -0.0625, -0.0391,  0.0318,  0.0104],
        [ 0.1007, -0.1212, -0.0918, -0.0962,  0.1168,  0.1105, -0.0719, -0.0265,  0.0207,  0.0157]
    ]
)

  查看每一个图像预测结果最大值对应的类别:

> preds.argmax(dim=1)
tensor([5, 5, 5, 5, 5, 5, 4, 5, 5, 4])

> F.softmax(preds).argmax(dim=1) #  发现softmax之后的结果和原张量的结果一致
tensor([5, 5, 5, 5, 5, 5, 4, 5, 5, 4])

> labels
tensor([9, 0, 0, 3, 0, 2, 7, 2, 5, 5])

  如果我们想要判断 preds 的预测结果的准确性,采用下面的做法:

> preds.argmax(dim=1).eq(labels)
tensor([False, False, False, False, False, False, False, False,  True, False])

> preds.argmax(dim=1).eq(labels).sum() # 在Python中,True用1表示,False用0表示
tensor(1)

  所以我们可以自定义一个返回预测正确的图片个数的函数,如下所示:

def get_num_correct(preds, labels):
    return preds.argmax(dim=1).eq(labels).sum().item()

14 训练一个神经网络

  前面我们花了很大的篇幅介绍如何 build the model,主要涉及到的是网络层和前向传播算法。现在我们开始学习如何 train the model,我们将训练过程主要分为以下步骤:

  • 从 train_set 获取批量图像
  • 把批量图像输入到神经网络中
  • 计算模型损失(预测值与真实值之间的误差)
  • 计算损失关于权重参数的梯度值
  • 用梯度值来更新权重参数
  • 重复前面1-5步,直到一个epoch处理完毕
  • 重复前面1-6步,直到模型损失逼近最小值

  在进行训练过程时,我们需要打开PyTorch的梯度跟踪功能(它默认是开启的,我们在前面进行测试的时候将前关闭了)。

> torch.set_grad_enabled(True)
<torch.autograd.grad_mode.set_grad_enabled at 0x15b22d012b0>

  获取一个batch的数据:

> network = Network()

> train_loader = torch.utils.data.DataLoader(train_set, batch_size=100)
> batch = next(iter(train_loader)) # Getting a batch
> images, labels = batch

  前向传播并计算batch的损失:

> preds = network(images)
> loss = F.cross_entropy(preds, labels) # Calculating the loss

> loss.item()
2.307542085647583

  计算损失对权重参数的梯度,通过backward()方法:

> network.conv1.weight.grad # before backward() called
None

> loss.backward() # Calculating the gradients

> network.conv1.weight.grad.shape
torch.Size([6, 1, 5, 5])

  这些梯度值计算完之后,将根据我们设定的优化器,来按照对应的方式来更新模型的权重参数,优化器通过 torch.optim 来创建:

> optimizer = optim.Adam(network.parameters(), lr=0.01)
> optimizer.step() # Updating the weights

  我们可以来检查以下,更新完模型参数,同一个批次的图像所对应的损失是不是减小了:

> preds = network(images)
> loss = F.cross_entropy(preds, labels)

> loss.item() # 可以发现值变小了
2.262690782546997

  整合前面的所有步骤,我们得到了以下训练单个batch的完整代码:

network = Network()

train_loader = torch.utils.data.DataLoader(train_set, batch_size=100)
optimizer = optim.Adam(network.parameters(), lr=0.01)

batch = next(iter(train_loader)) # Get Batch
images, labels = batch

preds = network(images) # Pass Batch
loss = F.cross_entropy(preds, labels) # Calculate Loss

loss.backward() # Calculate Gradients
optimizer.step() # Update Weights

print('loss1:', loss.item())
preds = network(images)
loss = F.cross_entropy(preds, labels)
print('loss2:', loss.item())

  继续完善我们的代码,实现可以训练单个epoch中的所有batch图像,只需修改为:

network = Network()

train_loader = torch.utils.data.DataLoader(train_set, batch_size=100)
optimizer = optim.Adam(network.parameters(), lr=0.01)

total_loss = 0
total_correct = 0

for batch in train_loader: # Get Batch
    images, labels = batch 

    preds = network(images) # Pass Batch
    loss = F.cross_entropy(preds, labels) # Calculate Loss

    optimizer.zero_grad()
    loss.backward() # Calculate Gradients
    optimizer.step() # Update Weights

    total_loss += loss.item()
    total_correct += get_num_correct(preds, labels)
    
print(
    "epoch:", 0, 
    "total_correct:", total_correct, 
    "loss:", total_loss
)

  继续完善我们的代码,实现可以训练多个epoch的图像,只需一点小修改:

network = Network()

train_loader = torch.utils.data.DataLoader(train_set, batch_size=100)
optimizer = optim.Adam(network.parameters(), lr=0.01)

for epoch in range(10):
    
    total_loss = 0
    total_correct = 0
    
    for batch in train_loader: # Get Batch
        images, labels = batch 

        preds = network(images) # Pass Batch
        loss = F.cross_entropy(preds, labels) # Calculate Loss

        optimizer.zero_grad()
        loss.backward() # Calculate Gradients
        optimizer.step() # Update Weights

        total_loss += loss.item()
        total_correct += get_num_correct(preds, labels)

    print(
        "epoch", epoch, 
        "total_correct:", total_correct, 
        "loss:", total_loss
    )

  当我们执行这个代码,输出如下所示:

epoch 0 total_correct: 43301 loss: 447.59147948026657
epoch 1 total_correct: 49565 loss: 284.43429669737816
epoch 2 total_correct: 51063 loss: 244.08825492858887
epoch 3 total_correct: 51955 loss: 220.5841210782528
epoch 4 total_correct: 52551 loss: 204.73878084123135
epoch 5 total_correct: 52914 loss: 193.1240530461073
epoch 6 total_correct: 53195 loss: 184.50964668393135
epoch 7 total_correct: 53445 loss: 177.78808392584324
epoch 8 total_correct: 53629 loss: 171.81662507355213
epoch 9 total_correct: 53819 loss: 166.2412590533495

  以上就是我们利用PyTorch建立一个卷积神经网络对Fashion-MNIST数据集进行预测的全过程,我们一块砖、一片瓦的搭起了整个神经网络,了解了如何build the modeltrain the model。麻雀虽小,五脏俱全,通过本文的案例,我们了解了PyTorch底层的工作原理,希望大家能够举一反三,应用PyTorch深度学习框架去解决更多的实际问题,加油!

  
  
  
  
  
  

  
  
  
  
  
  

  
  
  
  
  
  

你可能感兴趣的:(深度学习,PyTorch,Fashion-MNIST)