本文是对 Neural Network Programming - Deep Learning with PyTorch 系列博客的翻译与整理,英语基础比较好的同学推荐阅读原汁原味的博客。
文章目录
计算机程序通常由两个主要部分组成:代码和数据。在传统的编程中,程序员的工作是直接编写软件或代码,但是在深度学习和神经网络中,可以说软件就是网络本身,特别是在训练过程中自动产生的网络权值。数据是深度学习的主要组成部分,尽管让我们的神经网络从数据中学习是我们作为神经网络程序员的任务,但我们仍然有责任了解我们实际用于训练的数据的性质和历史。
1. 什么是MNIST数据集
MNIST数据集,全称是 Modified National Institute of Standards and Technology database,它是一个著名的手写数字数据集,通常用于训练机器学习的图像处理系统。NIST是国家标准与技术协会的缩写,M 代表修改过的,这是因为有一个原始的NIST数据集被修改为MNIST。
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 |
数据集中的部分图片如下所示:
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 对象包含一个 <image, label>
对。
> 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 ∗