快速上手PyTorch——Quickstart in PyTorch
1. 简介
相信大家在学习Pytorch的过程中,对Pytorch官方网站教程的参考学习,是一个必不可少的环节,本文中作者将依据自己浅薄的理解,简要分析Pytorch/Tutorials/QuickStart,希望能为有需求的朋友们提供一些帮助,也欢迎大家批评指正。本文将依据Quickstart,使用FashionMNIST数据集,构建并训练一个简单的分类网络。
2. 代码分析
(1) 导入必要的库
在加载数据前,我们需要先导入需要用到的一些库
import torch
from torchvision import datasets
from torch.utils.data import DataLoader
from torch import nn
from torchvision.transforms import ToTensor
import matplotlib.pyplot as plt # visualize data (optional)
torch.utils.data.Datasets
和torch.utils.data.DataLoader
是Pytorch中处理数据的两个基本功能,前者包含了一些数据样本和对应的标签,后者则是把前者包裹成一个可迭代的对象,并将其传递给后续的 train 和 test 函数。
与此同时,Pytorch也提供一些具有针对性的库,如torchvision
、torchtext
、torchaudio
,它们包含了图片数据、文本信息、音频信息,本文中所采用的FashionMNIST数据集就是存储在torchvision
下的datasets
中。
torch.nn
中提供了我们在构建神经网络中所需要的一系列层,因此在构建网络的时候,我们只需要简单地调用即可。
torchvision.transforms.ToTensor
让我们能够将PIL图像转化为Tensor表征的图像。
(2) 加载数据
FashionMNIST包含有60,000个训练数据和10,000个测试数据,每一个数据都包含一个
28
p
i
x
e
l
×
28
p
i
x
e
l
28 pixel \times 28 pixel
28pixel×28pixel 的灰度图像和一个代表样本类别的标签。我们需要将torchvision.datasets
中的FashionMNIST导入进来,并使用DataLoader
将其包裹成一个可迭代的对象。
# download training data from datasets
training_data = datasets.FashionMNIST(root='data',
train=True,
download=True,
transform=ToTensor())
# download test data from datasets
test_data = datasets.FashionMNIST(root='data',
train=False,
download=True,
transform=ToTensor())
root
表示数据集的存储路径,若不存在指定的文件夹,将自行创建。
train
表明该数据是数据集中的训练数据还是测试数据。
download
表示是否下载,如果其值为True
,那么将检测root
下的文件夹是否含有数据集,如果没有,将下载数据集,如果有,不再重复下载;如果其值为False
,不下载数据集。
transform
指定了样本的转换格式。
target_transform
指定了标签的转换格式(示例代码中并没有特殊指定)。
# define batch size
batch_size = 16
# create data loaders
train_dataloader = DataLoader(training_data, batch_size=batch_size, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=batch_size, shuffle=True)
batch_size
表示在训练或者测试过程中,一次喂入的数据量,举个例子,对本例中的FashionMNIST来说,训练集中有60,000个样本数据,一次喂入16个样本,那么总共需要
60
,
000
÷
16
=
3750
60,000 \div 16 = 3750
60,000÷16=3750 个batch才能将训练数据全部喂进网络中。使用batch的一部分原因是,通过设置合理的batch_size值,我们可以提高内存的利用率,提高训练速度,并使梯度下降更为准确。
shuffle
表示打乱数据集中的数据,通常针对一个数据集,我们会遍历多次在下文所述的优化器中进行迭代,每一次称为一个epoch,为了提高网络的泛用性,降低过拟合程度,我们需要将每一个epoch中的样本打乱。
(3) 构建网络
在导入数据后,我们就可以开始着手构建我们的网络了,本文中的神经网络结构较为简单,主要就是全连接层和激活函数ReLU的堆叠。
# chose CPU or GPU for training
if torch.cuda.is_available():
device = 'cuda'
else:
device = 'cpu'
print(f'You are currently using {device}.')
torch.cuda.is_available()
判断cuda是否可用,进而选择使用GPU或CPU进行训练。
# define model
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(nn.Linear(28*28, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10),
nn.ReLU())
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits
model = NeuralNetwork().to(device)
print(model)
我们继承nn.Module
并在__init__
下定义自己的网络结构,如上代码所示,我们在__init__
下定义了一个self.flatten
和一个self.linear_relu_stack
。
self.flatten
的作用为将
m
×
n
m \times n
m×n 的图像变为
1
×
(
m
∗
n
)
1 \times (m*n)
1×(m∗n) 的图像,即将图像“摊平”,以送入全连接层。在本例中,即将
28
×
28
28 \times 28
28×28 的图像变为
1
×
784
1 \times 784
1×784 的图像。
self.linear_relu_stack
中的结构非常简单:
784 × 512 − L i n e a r ⟹ R u L U ⟹ 512 × 512 − L i n e a r ⟹ R e L U ⟹ 512 × 10 − L i n e a r ⟹ R e L U 784 \times 512 - Linear \Longrightarrow RuLU \Longrightarrow 512 \times 512 - Linear \Longrightarrow ReLU \Longrightarrow 512 \times 10 - Linear \Longrightarrow ReLU 784×512−Linear⟹RuLU⟹512×512−Linear⟹ReLU⟹512×10−Linear⟹ReLU
因为最后FashionMNIST中有10个类,因此最后的输出也为10个数。
最后,在forward
中,我们应用上述结构,构造一个前向通路。并根据先前选择的device
部署模型,打印模型结构。
(4) 损失函数和优化器
Pytorch提供了丰富的损失函数和优化器,在这里我们使用nn.CrossEntropyLoss()
作为损失函数,torch.optim.SGD()
作为优化器。
# define loss function
loss_function = nn.CrossEntropyLoss()
# define optimizer
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
注意此处的lr
表示SGD的学习率,可以根据实际情况进行修改。
(5) 训练与测试
冗长的准备工作已经完成,可以开始着手定义训练与测试了
# define train loop
def train(dataloader, model, loss_function, optimizer):
size = len(dataloader.dataset)
for batch, (X, y) in enumerate(dataloader): # batch represents batch-th
X, y = X.to(device), y.to(device) # X represents img, while y represents label
# Compute prediction error
pred = model(X)
loss = loss_function(pred, y)
# BackPropagation
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Oversee process
if batch % 100 == 0:
loss, current = loss.item(), batch * len(X)
print(f'loss:{loss:>7f} [{current:>5d}/{size:>5d}]')
size
获取数据集中所有样本的个数。
在for循环中,我们遍历dataloader
中的所有数据,获取其索引及对应数据(注意dataloader
中含有两项元素——样本及其对应的标签)。由于我们先前在train_dataloader
中定义了batch_size
,因此此处的batch
代表的enumerate
的索引值恰好表示了第几个batch。
接着我们调用loss_function
计算loss,再利用优化器进行反向传播更新网络参数。
为了监督进程,对每100个batch,我们获取它当前的loss和当前进行到第几个数据,并打印出来。
# define test loop
def test(dataloader, model, loss_function):
size = len(dataloader.dataset) # numbers of the whole test images(10,000)
num_batches = len(dataloader) # 10,000 / batch size(16) = 625
model.eval()
sum_test_loss, sum_correct = 0, 0
with torch.no_grad():
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model(X)
sum_test_loss += loss_function(pred, y).item()
sum_correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss = sum_test_loss / num_batches
correct_ratio = 100 * (sum_correct / size)
print(f'Test Error: \n Accuracy: {(correct_ratio):>0.1f}%, Avg loss: {test_loss:>8f} \n')
为了观察经过训练后的模型的表现,我们还需要测试集来评估模型的好坏。
size
获取数据集中所有样本个数。
num_batches
获取batch个数,由于FashionMNIST中的测试集有10,000个样本,且我们此前设置的batch_size
为16,因此batch为
10
,
000
÷
16
=
625
10,000 \div 16 = 625
10,000÷16=625。
model.eval()
的作用像是一种开关,由于一些层(比如Dropouts Layers、BatchNorm Layers)在训练和测试中的行为是不一样的,因此在测试时,我们需要model.eval()
将它们置为测试(非训练)状态,与之相对的则是model.train()
——置为训练状态。由于作者水平有限,其在本例中的作用尚不明确,欢迎有知道的大佬补充。
注意到在测试中,梯度信息是不需要的,因此with torch.no_grad()
可以帮助我们停止计算梯度以节省算力。
为了方便后续理解,在这里给出打印后pred的大致结构
tensor([[0.1421, 0.0000, 0.0000, 0.0000, 0.0413, 0.4401, 0.0000, 0.7510, 0.8822, 0.0000],
[0.9166, 0.6223, 0.0000, 0.9044, 1.2210, 0.0000, 0.0000, 0.0000, 0.3413, 0.0000],
[0.2356, 0.0000, 0.0000, 0.0826, 0.5078, 0.0776, 0.0000, 0.0544, 0.4084, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.2197, 0.2384, 0.0000, 0.4583, 0.9327, 0.0000],
[1.0815, 1.9916, 0.0000, 1.9508, 0.3624, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0574, 0.0000, 0.0000, 0.0000, 0.1917, 0.2527, 0.0000, 0.2819, 0.7821, 0.0000],
[1.8598, 2.3490, 0.0000, 2.5405, 0.7339, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.7836, 1.3618, 0.0000, 0.9962, 0.2862, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.5627, 0.2218, 0.0000, 0.2929, 0.1805, 0.0201, 0.0000, 0.0000, 0.0482, 0.0000],
[0.8809, 2.1661, 0.0000, 1.5107, 0.2718, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.3268, 0.5301, 0.0000, 1.5194, 1.8777, 0.0000],
[0.1255, 0.0000, 0.0000, 0.0000, 1.2370, 0.0451, 0.0000, 0.0322, 0.8576, 0.0000],
[0.7843, 0.2338, 0.0000, 0.6287, 0.6042, 0.0785, 0.0000, 0.0000, 0.2600, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.2876, 0.0000, 0.6852, 0.5046, 0.0000],
[0.4747, 0.0000, 0.0000, 0.0000, 0.6698, 0.1957, 0.0000, 0.2946, 0.9717, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.3904, 0.0000, 1.1054, 0.5773, 0.0000]])
上图展示的是一个batch中的数据,总共有625个batch。
sum_test_loss
表示所有的batch的loss的加和,一个batch的loss为该batch下16个loss的均值。
(pred.argmax(1) == y).type(torch.float)
表示返回pred的每一行最大值的索引,再与标签进行比较,相等返回1
,反之返回0
。
sum_correct
则是对上述所有的1的加和,表征了在test中模型预测正确的样本个数。
test_loss
表示平均损失,correct_ratio
表示精确度。
(6) 开始训练
# start train
epochs = 5
for t in range(epochs):
print(f'Epoch {t + 1}\n----------------------------------')
train(train_dataloader, model, loss_function, optimizer)
test(test_dataloader, model, loss_function)
为了节省时间,这里我们只训练5个epoch,实际情况下根据个人需求自行设置。根据我的个人测试,在训练了50个epoch左右时,精度能达到85%。
(7) 模型的存储、加载和预测
# save model parameters
torch.save(model.state_dict(), 'model.pth')
print('Save Pytorch Model State to model.pth')
模型训练结束后,我们可以通过上述代码将其保存为.pth
文件,注意这里仅仅保存了网络参数,不包括网络结构,若想既包含网络参数又包含网络结构可以去官方文档查找(我懒得找了)。
# load model
model = NeuralNetwork()
model.load_state_dict(torch.load('model.pth'))
加载保存在model.pth
中的网络参数。
# make predictions
classes = ['T-shirts/top', 'Trousers', 'Pullover', 'Dress', 'Coat',
'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
model.eval()
x, y = test_data[0][0], test_data[0][1]
with torch.no_grad():
pred = model(x)
predicted = classes[pred.argmax(1)]
actual = classes[y]
print(f'Predicted: "{predicted}", Actual: "{actual}"')
上述代码展示了FashionMNIST中包含的10类物体名称,并根据我们构建的网络进行预测,代码比较简单,这里不再赘述,这里我们简单挑选了FashionMNIST测试集中的第一组数据作为预测对象。