本文是对 Neural Network Programming - Deep Learning with PyTorch 系列博客的翻译与整理,英语基础比较好的同学推荐阅读原汁原味的博客。
计算机程序通常由两个主要部分组成:代码和数据。在传统的编程中,程序员的工作是直接编写软件或代码,但是在深度学习和神经网络中,可以说软件就是网络本身,特别是在训练过程中自动产生的网络权值。数据是深度学习的主要组成部分,尽管让我们的神经网络从数据中学习是我们作为神经网络程序员的任务,但我们仍然有责任了解我们实际用于训练的数据的性质和历史。
MNIST数据集,全称是 Modified National Institute of Standards and Technology database,它是一个著名的手写数字数据集,通常用于训练机器学习的图像处理系统。NIST是国家标准与技术协会的缩写,M 代表修改过的,这是因为有一个原始的NIST数据集被修改为MNIST。
MNIST因其被使用的频率而闻名,常见的原因有两个:
初学者使用它很容易上手
研究人员使用它来基准化(比较)不同的模型。
这个数据集包含 70,000
张手写体图片,并进行如下分割:
60,000
张训练图片10,000
张测试图片由于 MNIST 数据集对于深度学习来说,有点太简单,所以后面有人创建了 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 |
数据集中的部分图片如下所示:
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数据集。
机器学习/深度学习工程的第一步是准备数据,我们将遵循以下的 ETL
流程:
在我们的项目中,该过程分别对应为:
基于这些目的,PyTorch 提供了以下两个类:
类 | 描述 |
---|---|
torch.utils.data.Dataset | 用于表示数据集的抽象类 |
torch.utils.data.DataLoader | 包装数据集并提供对基础数据的访问 |
抽象类 是一个Python类,它里面的方法我们必须要实现,我们可以通过创建一个子类来扩展Dataset
类的功能,从而创建一个自定义数据集类,这个新的子类可以被传递到PyTorch的 DataLoader
对象。
我们将使用 torchvision
包内置的Fashion-MNIST数据集,因此我们的项目不必再重新创建一个新的子类,只需知道时尚MNIST内置的dataset类是在幕后完成这项工作的。
torchvision
包允许我们访问以下资源:
我们用下面代码来获取 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
)
我们先看一下,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])
我们将开始创建一个批处理大小为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
。
在PyTorch中构建神经网络,需要使用torch.nn
包,这是PyTorch的神经网络(nn)库,我们通常是这样导入包的:
import torch.nn as nn
构建神经网络所需的主要组件是layer
,而PyTorch的神经网络库torch.nn
中包含一些类,可以帮助我们构建层。而神经网络中的layer
,主要包含两个组件:
在torch.nn
包中,有一个类叫做Module
,它是所有神经网络模块的基类,包括layer
。这意味着PyTorch中的所有layer
都扩展了nn.Module类,并继承了PyTorch在nn.Module类中的所有内置功能。在OOP(面向对象编程)中,这个理念被称为继承。
当我们将一个张量作为输入传递给网络时,张量通过每一层转换向前流动,直到张量到达输出层,张量通过网络向前流动的过程称为向前传递,也因此, nn.module
类中提供了一个forward()
方法,每个继承它的类,都必须实现这个方法,它其实也就是我们前面提到的转换操作。
当我们在具体实现 forward()
方法时,一般需要调用 nn.functional
包中提供的函数,这个包为我们提供了许多可以用于构建层的神经网络操作。
基于前面的学习,我们知道了构建一个网络主要分为下面几步:
我们首先来看第一步,创建一个简单的类来表示神经网络:
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
。
我们的每一层都扩展了PyTorch的nn.Module
类,所以每一层中都封装了两个部分,前向传播函数和权重向量,例如下面的卷积层nn.Conv2d
:
self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
为了更好的理解我们定义的层,我们来看看层的构造函数中所包含的参数值。当我们构造一个层时,我们需要将参数值传递给层的构造函数,在我们的卷积层中有三个参数,线性层中有两个参数。
我们先来看看需要程序员手动设定的超参数:
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 12∗4∗4;最后到输出层时,我们的数据集总共有10个类别,因此我们的输出层的out_features
的值应该为10。
可学习参数指的是在训练过程中会不断更新的参数,我们会给它们随机初始化一些值,然后在每一轮的迭代中,更新这些值。那么在我们前面设计的网络中,这些可学习参数在哪呢?
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
)
站在卷积层的角度,权重张量就在我们设定的filter
之中,而在代码中,filter
实际上就是权重张量自身。
层内的卷积运算是指该层的所有输入通道的feature map
与该层的filter
之间的运算,这意味着我们实际上进行的是两个张量之间的运算。(一次卷积是某个卷积核对所有输入通道的同一个区域进行卷积,而不是单个输入通道)
对于第一个卷积层,我们有1个颜色通道,用6个大小为 5 ∗ 5 5*5 5∗5的卷积核进行卷积,所以最后输出的通道数也有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=Wlal−1
于是我们权重矩阵的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])
我们先来看看如何用矩阵乘法,把输入的特征向量转换为输出的特征向量:
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
我们先回顾一下前面定义的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
注意,我们现在的网络还是未经训练过的。 目前只是测试如何预测单张图片,检查我们的网络是否为畅通的。所以我们先关闭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.)
整个过程和预测单张图片类别是类似的,区别在于我们需要用到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()
前面我们花了很大的篇幅介绍如何 build the model
,主要涉及到的是网络层和前向传播算法。现在我们开始学习如何 train the model
,我们将训练过程主要分为以下步骤:
在进行训练过程时,我们需要打开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 model
和train the model
。麻雀虽小,五脏俱全,通过本文的案例,我们了解了PyTorch底层的工作原理,希望大家能够举一反三,应用PyTorch深度学习框架去解决更多的实际问题,加油!