本节我们将使用 torchvision 包,它是 PyTorch 深度学习框架的一个扩展库,主要用于构建计算机视觉模型。torchvision 主要包含以下几个部分:
torchvision.datasets
:提供加载数据集的函数和常用数据集接口;torchvision.models
:包含常用的模型结构和预训练模型,如 AlexNet、VGG、ResNet 等;torchvision.transforms
:提供常用的图像变换操作,如裁剪、旋转等;torchvision.utils
:提供其他一些有用的工具和方法。
首先导入本节需要的包或模块。
import numpy as np
import torch
from torchvision import datasets, transforms
from caesar import d2l, hex
获取数据
我们首先引入一个多类图像分类数据集,该数据集将在后续章节中多次使用,以便我们比较不同算法在模型精度和计算效率上的差异。在图像分类数据集中,手写数字识别数据集 MNIST 是最常用的。然而,大多数模型在 MNIST 上的分类精度已超过 95%。为了更直观地观察算法间的差异,我们将采用图像内容更复杂的 Fashion-MNIST 数据集 (该数据集较小,仅几十 MB,无 GPU 的电脑也能处理)。
下面,我们将使用 datasets.FashionMNIST
来下载数据集。首次调用时,它会自动从网上获取数据。我们可以通过参数 train
来指定是下载训练数据集还是测试数据集。测试数据集也称为测试集,仅用于评估模型性能,不用于训练。
此外,我们还指定了参数 transform=transforms.ToTensor()
,以将所有数据转换为 Tensor
。如果不进行转换,返回的将是 PIL 图片。transforms.ToTensor()
将尺寸为 (H x W x C) 且数据范围在 [0, 255] 的 PIL 图片或数据类型为 np.uint8
的 NumPy 数组转换为尺寸为 (C x H x W)、数据类型为 torch.float32
且数据范围在 [0.0, 1.0] 的 Tensor
。
mnist_train = datasets.FashionMNIST(
root='/data/datasets/fashion_mnist',
train=True,
download=True,
transform=transforms.ToTensor(),
)
mnist_test = datasets.FashionMNIST(
root='/data/datasets/fashion_mnist',
train=False,
download=True,
transform=transforms.ToTensor(),
)
我们可以使用 len()
函数来获取数据集的大小,并通过下标访问特定的样本。训练集和测试集中每个类别的图像数量分别为 6000 和 1000。由于共有 10 个类别,因此训练集和测试集的样本总数分别为 60000 和 10000。
type(mnist_train), len(mnist_train), len(mnist_test)
feature, label = mnist_train[0]
feature.shape, label
读取数据
Fashion-MNIST 中一共包括了 10 个类别,分别为 t-shirt(T 恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包) 和 ankle boot(短靴)。以下函数可以将数值标签转成相应的文本标签。
hex.print_method(d2l.get_fashion_mnist_labels)
下面定义一个函数,该函数可以在一行内绘制多张图像及其对应的标签。
hex.print_method(d2l.show_fashion_mnist)
X, y = [], []
for i in range(10):
X.append(mnist_train[i][0])
y.append(mnist_train[i][1])
d2l.show_fashion_mnist(X, d2l.get_fashion_mnist_labels(y))
读取小批量数据样本以供模型训练使用。在实践中,数据读取往往是训练性能的瓶颈,尤其是在模型较为简单或计算硬件性能较高的情况下。PyTorch 的DataLoader
提供了一个便利的功能,即通过多进程加速数据读取。我们可以通过设置num_workers
参数为 4 来启用这一功能,实现数据的并行读取。
batch_size = 32
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=0)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=0)
实现 softmax 运算
我们先来描述如何对多维Tensor
进行按维度操作。以下面的例子为例,给定一个Tensor
矩阵X
。我们可以只对其中同一列 (dim=0
) 或同一行 (dim=1
) 的元素求和,并在结果中保留行和列这两个维度 (keepdim=True
)。
X = torch.tensor([[1, 2, 3], [4, 5, 6]])
X.sum(dim=0, keepdim=True), X.sum(dim=1, keepdim=True)
接下来,我们定义 softmax 运算。在该函数中,矩阵X
的行数代表样本数量,列数代表输出类别的数量。softmax 运算通过以下步骤计算样本对各个输出类别的预测概率:首先,对矩阵X
中的每个元素应用exp
函数进行指数运算;然后,对exp
函数处理后的矩阵中每一行的元素求和;最后,将每行的每个元素除以该行元素的总和。这样处理后,得到的矩阵每行元素之和为 1 且均为非负值,因此每行都表示一个合法的概率分布。softmax 运算的输出矩阵中,任意一行的元素代表了一个样本在各个输出类别上的预测概率。
def softmax(X):
X_exp = X.exp()
partition = X_exp.sum(dim=1, keepdim=True)
return X_exp / partition
X = torch.rand(2, 5)
X_prob = softmax(X)
X_prob, X_prob.sum(dim=1)
初始化模型参数
我们将使用向量来表示每个样本。已知每个样本的输入图像尺寸为 28 像素高和宽,模型的输入向量长度为 。该向量的每个元素对应图像中的一个像素。由于图像分为 10 个类别,单层神经网络的输出层有 10 个输出节点,因此 softmax 回归的权重参数是一个 的矩阵,偏差参数是一个 的矩阵。
num_inputs = 28 * 28
num_outputs = 10
W = torch.tensor(
np.random.normal(0, 0.01, (num_inputs, num_outputs)),
dtype=torch.float,
requires_grad=True,
)
b = torch.zeros(num_outputs, dtype=torch.float, requires_grad=True)
W.shape, b.shape
注意:权重参数应按正态分布随机初始化,标准差不宜过大,以免初始化权重值差异过大,影响模型训练达到最优效果。
定义模型
参数初始化完成,引入了 softmax 运算,基础准备工作已经就绪,我们可以定义本节所描述的 softmax 分类模型了。这里通过view
函数将每张原始图像转换为长度为num_inputs
的向量。
def net(X):
return softmax(torch.mm(X.view(-1, num_inputs), W) + b)
定义损失函数
在 softmax 分类模型中,我们使用交叉熵损失函数。为了计算损失,我们需要标签的预测概率,这可以通过 gather 函数获得。
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = torch.LongTensor([0, 2])
y_hat.gather(1, y.view(-1, 1))
交叉熵损失函数
def cross_entropy(y_hat, y):
return -torch.log(y_hat.gather(1, y.view(-1, 1)))
计算分类准确率
给定一个类别的预测概率分布 y_hat
,我们将预测概率最大的类别作为输出类别。如果这个输出类别与真实类别 y
一致,则认为这次预测是正确的。分类准确率是指正确预测的数量与总预测数量的比值。
def eval_accuracy(data_iter, net):
l_sum, acc_sum, n = 0.0, 0.0, 0
for X, y in data_iter:
y_hat = net(X)
l_sum += loss(y_hat, y).sum().item()
acc_sum += (y_hat.argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return l_sum / n, acc_sum / n
训练模型
我们同样使用小批量随机梯度下降来优化模型的损失函数。
hex.print_method(d2l.sgd)
在训练模型时,迭代周期数 num_epochs
和学习率 lr
都是可调整的超参数。调整它们的值可能会提高模型的分类准确性。
num_epochs, lr = 5, 0.1
loss = cross_entropy
for epoch in range(num_epochs):
train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
for X, y in train_iter:
y_hat = net(X)
l = loss(y_hat, y).sum()
l.backward()
d2l.sgd([W, b], lr, batch_size)
W.grad.data.zero_()
b.grad.data.zero_()
train_l_sum += l.item()
train_acc_sum += (y_hat.argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
test_l, test_acc = eval_accuracy(test_iter, net)
print(
f'epoch: {epoch + 1}, train loss: {train_l_sum / n:4f}, train acc: {train_acc_sum / n:4f}, test loss: {test_l:4f}, test acc: {test_acc:4f}'
)
模型预测
训练完成后,现在就可以演示如何对图像进行分类了。
for X, y in test_iter:
true_labels = d2l.get_fashion_mnist_labels(y.numpy())
pred_labels = d2l.get_fashion_mnist_labels(net(X).argmax(dim=1).numpy())
titles = [f'T:{true}\nP:{pred}' for true, pred in zip(true_labels, pred_labels)]
d2l.show_fashion_mnist(X[0:10], titles[0:10])
break