概述
Pytorch官网中的Quickstart部分简要说明了使用pytorch中的FashionMNIST数据集,构建并训练一个简单的分类网络的过程。下文是对这一过程的具体实践。具体的实战部分主要包括库的引入、加载数据、构建神经网络、设置损失函数和优化器、调整训练和测试的参数、训练、模型的存储、加载和预测。
实战部分
1 导入库
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda, Compose
import matplotlib.pyplot as plt
pytorch
中包含最基本的函数库torch
。在这个库中有许多包。
torch.nn
中提供了我们在构建神经网络中所需要的一系列层。
torch.utils.data.Datasets
和 torch.utils.data.DataLoader
是两个最基础的功能包。前者包括torch中储存着的一些数据样本和对应的标签,后者在对这些数据样本和标签进行包装处理后传递给train和test函数进行交叉验证和测试。本文中只使用后者来对我们的数据集进行处理。
与此同时,Pytorch
也提供一些具有针对视频,音频,文本的数据集,如torchvision
、torchtext
、torchaudio
,它们包含了图片数据、文本信息、音频信息,本文中所采用的FashionMNIST
数据集就是存储在torchvision
下的datasets
中。
torchvision.transforms.ToTensor
让我们能够将PIL(Python Image Library)
图像转化为Tensor
张量的图像。
matplotlib.pyplot
用于让我们做数据的可视化功能。
2 加载数据
FashionMNIST
包含有60,000个训练数据和10,000个测试数据,每一个数据都包含一个 \(28 pixel\times28pixel\times28pixel\)的灰度图像和一个代表样本类别的标签。我们需要将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
指定了标签的转换格式(示例代码中并没有特殊指定)。
ToTensor()
函数官方文档如下:
class ToTensor(object):
"""Convert a ``PIL Image`` or ``numpy.ndarray`` to tensor.
Converts a PIL Image or numpy.ndarray (H x W x C) in the range
[0, 255] to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0].
"""
ToTensor()
可以将PIL图像或Numpy数组转换成FloatTensor()
,并将图像的像素密度值压缩到[0.,1.]区间。
# 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 \div 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 \times n\) 的图像变为 \(1 \times (m*n)\) 的图像,即将图像“摊平”,以送入全连接层。在本例中,即将 \(28 \times 28\) 的图像变为 \(1 \times 784\)的图像。
self.linear_relu_stack
中的结构非常简单:
\(784 \times 512 - Linear \Longrightarrow RuLU \Longrightarrow 512 \times 512 - Linear \Longrightarrow ReLU \Longrightarrow 512 \times 10 - Linear \Longrightarrow ReLU\)
因为最后FashionMNIST
中有10个类,因此最后的输出也为10个数。
最后,在forward
中,我们应用上述结构,构造一个前向通路。并根据先前选择的device
部署模型,打印模型结构。
3.1 全连接层
在 CNN 中,全连接常出现在最后几层,用于对前面设计的特征做加权和。比如 mnist
,前面的卷积和池化相当于做特征工程,后面的全连接相当于做特征加权。全连接层(fully connected layers,FC)
在整个卷积神经网络中起到“分类器”的作用。如果说卷积层、池化层和激活函数层等操作是将原始数据映射到隐层特征空间的话,全连接层则起到将学到的“分布式特征表示”映射到样本标记空间的作用。
3.2 激活函数ReLU
\[ReLU(x) = max(0, x)\]
ReLU激活函数比起其它的激活函数有以下几个优点
- 克服梯度消失的问题
- 加快训练速度
缺点
- 输入负数,则完全不激活,ReLU函数死掉。
ReLU
函数输出要么是0,要么是正数,也就是ReLU
函数不是以0为中心的函数
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)
4.1 损失函数
nn.CrossEntropyLoss
是pytorch
中一个常用的损失函数,它结合了nn.LogSoftmax()
和nn.NLLLoss()
两个函数。它在做分类训练的时候是非常有用的。在训练过程中,对于每个类分配权值,可选的参数权值应该是一个1D张量。
该函数的交叉熵并不是采用
\[H(p, q) = -\sum_x (p(x)log(q(x)) + (1-p(x))log(1-q(x)))\]
而是采用
\[H(p, q) = -\sum_x (p(x)logq(x))\]
它是交叉熵的另外一种方式。
4.2 优化器
一言以蔽之,优化器就是在深度学习反向传播过程中,指引损失函数(目标函数)的各个参数往正确的方向更新合适的大小,使得更新后的各个参数让损失函数(目标函数)值不断逼近全局最小。这里我们使用SGD(随机最速下降法)优化器,这里有几篇优化器很好的文章:
https://www.leiphone.com/category/yanxishe/c7nM342MTsWgau9f.html
https://zhuanlan.zhihu.com/p/32230623
torch.optim.SGD()
是Pytorch
中一个常用的优化器,
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 \div 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
测试集中的第一组数据作为预测对象。
本地运行
训练情况
You are currently using cpu.
NeuralNetwork(
(flatten): Flatten(start_dim=1, end_dim=-1)
(linear_relu_stack): Sequential(
(0): Linear(in_features=784, out_features=512, bias=True)
(1): ReLU()
(2): Linear(in_features=512, out_features=512, bias=True)
(3): ReLU()
(4): Linear(in_features=512, out_features=10, bias=True)
(5): ReLU()
)
)
本地运行结果告诉我们:
我们正在使用cpu(关于cpu,gpu,cudnn,cuda的简介)
展现了我们搭建的神经网络的结构:
第一层为张量范围为(1, -1)
第二层为线性ReLU激活函数的堆栈,结构如上表。
训练数据
Epoch 1
loss:2.302129 [ 0/60000]
loss:2.287403 [ 1600/60000]
loss:2.279875 [ 3200/60000]
loss:2.282575 [ 4800/60000]
loss:2.277425 [ 6400/60000]
loss:2.269187 [ 8000/60000]
loss:2.273017 [ 9600/60000]
loss:2.246931 [11200/60000]
loss:2.234808 [12800/60000]
loss:2.251579 [14400/60000]
loss:2.262180 [16000/60000]
loss:2.241689 [17600/60000]
loss:2.252061 [19200/60000]
loss:2.211784 [20800/60000]
loss:2.193383 [22400/60000]
loss:2.095272 [24000/60000]
loss:2.235866 [25600/60000]
loss:2.253394 [27200/60000]
loss:2.113588 [28800/60000]
loss:2.182225 [30400/60000]
loss:2.172397 [32000/60000]
loss:2.028497 [33600/60000]
loss:2.012153 [35200/60000]
loss:2.139345 [36800/60000]
loss:2.203317 [38400/60000]
loss:1.971179 [40000/60000]
loss:2.013982 [41600/60000]
loss:2.149199 [43200/60000]
loss:1.889025 [44800/60000]
loss:2.163815 [46400/60000]
loss:1.980354 [48000/60000]
loss:1.919316 [49600/60000]
loss:2.101635 [51200/60000]
loss:2.031067 [52800/60000]
loss:1.847806 [54400/60000]
loss:2.007991 [56000/60000]
loss:1.780223 [57600/60000]
loss:1.941932 [59200/60000]
Test Error:
Accuracy: 45.6%, Avg loss: 1.932846
Epoch 2
loss:1.997460 [ 0/60000]
loss:1.804145 [ 1600/60000]
loss:1.938063 [ 3200/60000]
loss:1.777107 [ 4800/60000]
loss:1.871894 [ 6400/60000]
loss:1.761343 [ 8000/60000]
loss:1.780803 [ 9600/60000]
loss:2.035735 [11200/60000]
loss:1.813080 [12800/60000]
loss:1.919040 [14400/60000]
loss:1.630369 [16000/60000]
loss:2.077603 [17600/60000]
loss:1.786863 [19200/60000]
loss:1.789215 [20800/60000]
loss:1.609293 [22400/60000]
loss:1.703949 [24000/60000]
loss:1.516612 [25600/60000]
loss:1.980411 [27200/60000]
loss:1.728755 [28800/60000]
loss:1.664719 [30400/60000]
loss:1.723999 [32000/60000]
loss:1.910558 [33600/60000]
loss:1.653847 [35200/60000]
loss:1.270724 [36800/60000]
loss:1.931647 [38400/60000]
loss:1.722482 [40000/60000]
loss:1.392919 [41600/60000]
loss:1.526159 [43200/60000]
loss:1.446384 [44800/60000]
loss:1.414868 [46400/60000]
loss:1.995398 [48000/60000]
loss:1.909297 [49600/60000]
loss:1.918296 [51200/60000]
loss:1.320389 [52800/60000]
loss:1.661063 [54400/60000]
loss:1.390407 [56000/60000]
loss:1.147652 [57600/60000]
loss:1.845389 [59200/60000]
Test Error:
Accuracy: 52.0%, Avg loss: 1.570699
Epoch 3
loss:1.092074 [ 0/60000]
loss:1.741982 [ 1600/60000]
loss:1.369524 [ 3200/60000]
loss:1.323857 [ 4800/60000]
loss:1.266949 [ 6400/60000]
loss:1.562171 [ 8000/60000]
loss:1.563104 [ 9600/60000]
loss:1.229966 [11200/60000]
loss:1.535066 [12800/60000]
loss:1.250649 [14400/60000]
loss:1.820632 [16000/60000]
loss:1.524231 [17600/60000]
loss:1.468110 [19200/60000]
loss:1.420336 [20800/60000]
loss:1.714278 [22400/60000]
loss:1.513260 [24000/60000]
loss:2.120431 [25600/60000]
loss:1.365088 [27200/60000]
loss:1.666167 [28800/60000]
loss:1.297498 [30400/60000]
loss:1.615333 [32000/60000]
loss:1.401120 [33600/60000]
loss:1.402781 [35200/60000]
loss:1.373979 [36800/60000]
loss:1.190076 [38400/60000]
loss:1.719997 [40000/60000]
loss:1.338256 [41600/60000]
loss:1.791476 [43200/60000]
loss:1.531789 [44800/60000]
loss:1.537996 [46400/60000]
loss:1.278385 [48000/60000]
loss:1.564274 [49600/60000]
loss:1.360564 [51200/60000]
loss:1.240304 [52800/60000]
loss:1.212397 [54400/60000]
loss:1.112574 [56000/60000]
loss:1.184957 [57600/60000]
loss:1.850699 [59200/60000]
Test Error:
Accuracy: 55.1%, Avg loss: 1.415252
Epoch 4
loss:1.147449 [ 0/60000]
loss:1.560359 [ 1600/60000]
loss:1.543317 [ 3200/60000]
loss:1.730983 [ 4800/60000]
loss:1.174177 [ 6400/60000]
loss:1.445379 [ 8000/60000]
loss:1.562520 [ 9600/60000]
loss:1.897955 [11200/60000]
loss:1.960352 [12800/60000]
loss:1.199088 [14400/60000]
loss:0.863414 [16000/60000]
loss:1.398360 [17600/60000]
loss:1.527734 [19200/60000]
loss:1.379669 [20800/60000]
loss:1.472406 [22400/60000]
loss:1.820169 [24000/60000]
loss:1.146074 [25600/60000]
loss:1.472795 [27200/60000]
loss:1.709622 [28800/60000]
loss:1.476590 [30400/60000]
loss:1.154761 [32000/60000]
loss:1.410364 [33600/60000]
loss:2.256364 [35200/60000]
loss:1.148926 [36800/60000]
loss:1.419194 [38400/60000]
loss:1.519497 [40000/60000]
loss:0.849636 [41600/60000]
loss:1.398620 [43200/60000]
loss:1.133907 [44800/60000]
loss:0.922331 [46400/60000]
loss:1.535455 [48000/60000]
loss:1.321102 [49600/60000]
loss:1.795982 [51200/60000]
loss:0.979792 [52800/60000]
loss:0.818761 [54400/60000]
loss:1.346267 [56000/60000]
loss:0.953565 [57600/60000]
loss:1.072881 [59200/60000]
Test Error:
Accuracy: 57.2%, Avg loss: 1.336358
Epoch 5
loss:0.634092 [ 0/60000]
loss:1.802971 [ 1600/60000]
loss:1.657984 [ 3200/60000]
loss:1.600910 [ 4800/60000]
loss:1.257860 [ 6400/60000]
loss:1.261429 [ 8000/60000]
loss:1.332216 [ 9600/60000]
loss:1.221099 [11200/60000]
loss:0.950734 [12800/60000]
loss:1.075305 [14400/60000]
loss:1.640876 [16000/60000]
loss:1.257362 [17600/60000]
loss:1.077084 [19200/60000]
loss:1.382569 [20800/60000]
loss:1.685493 [22400/60000]
loss:1.042982 [24000/60000]
loss:1.566217 [25600/60000]
loss:1.312096 [27200/60000]
loss:0.845461 [28800/60000]
loss:1.557899 [30400/60000]
loss:1.157883 [32000/60000]
loss:1.782711 [33600/60000]
loss:1.142777 [35200/60000]
loss:1.118635 [36800/60000]
loss:1.291814 [38400/60000]
loss:1.557206 [40000/60000]
loss:1.018585 [41600/60000]
loss:0.958620 [43200/60000]
loss:1.060240 [44800/60000]
loss:0.582739 [46400/60000]
loss:1.147862 [48000/60000]
loss:1.558747 [49600/60000]
loss:1.661504 [51200/60000]
loss:1.073796 [52800/60000]
loss:1.314006 [54400/60000]
loss:0.881447 [56000/60000]
loss:1.528572 [57600/60000]
loss:1.275195 [59200/60000]
Test Error:
Accuracy: 58.5%, Avg loss: 1.282056
Save Pytorch Model State to model.pth
Predicted: "Sandal", Actual: "Ankle boot"
总结
可以看到,我们训练的loss并不是单调递减而是有一个总体的趋势下降,这主要跟我们运用的SGD算法特性有关。最后预测的结果是失败的,因为5个epoch对于整个神经网络来说还是不够,一般来说,数据量越大准确性越高,我们的神经网络初体验也只能算是浅尝辄止,期待下一步的学习。