目录
1. 图像分类数据集(Fashion-Mnist)
2. Softmax回归从0开始实现
3. Softmax回归的简洁实现
在介绍softmax回归的实现前我们先引⼊一个多类图像分类数据集。它将在后面的实验中被多次使用, 以⽅便我们观察⽐较算法之间在模型精度和计算效率上的区别。图像分类数据集中最常⽤的是⼿写数字识别数据集MNIST。但⼤部分模型在MNIST上的分类精度都超过了95%。为了更直观地观察算法之间的差异,我们将使用一个图像内容更加复杂的数据集Fashion-MNIST(这个数据集也⽐较⼩,只有⼏十M,没有GPU的电脑也能吃得消)。
本节我们将使用torchvision包,它是服务于PyTorch深度学习框架的,主要⽤来构建计算机视觉模型。 torchvision主要由以下⼏部分构成:
1)torchvision.datasets:⼀些加载数据的函数及常⽤的数据集接口;
2) torchvision.models:包含常用的模型结构(含预训练模型),例如AlexNet、VGG、ResNet等;
3)torchvision.transforms:常用的图⽚变换,例如裁剪、旋转等;
4)torchvision.utils:其他的一些有用的方法。
⾸先导⼊本节需要的包或模块:
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import time
import sys
sys.path.append(".") # 为了导入上层目录的d2lzh_pytorch.py模块
import d2lzh_pytorch as d2l
下⾯,我们通过torchvision的torchvision.dataset来下载这个数据集。第一次调⽤时会自动从⽹上获取数据。我们通过参数 train来指定获取训练数据集或测试数据集(testing data set)。测试数据集也叫测试集(testing set),只用来评价模型的表现,并不不⽤来训练模型。
另外我们还指定了参数transform = transforms.ToTensor()使所有数据转换为Tensor,如果不进行转换返回的是PIL图片。transforms.ToTensor()将尺寸为(H*W*C)且数据位于(0,255)的PIL图片或者数据类型为np.uint8的Numpy数组转换为尺寸为(C*H*W)且数据类型为torch.float32且位于(0.0,1.0)的Tensor。
注意:由于像素值为0到255的整数,所以刚好是uint8所能表示的范围,包括transforms.ToTensor()在内的⼀些关于图片的函数就默认输入的是uint8型,若不是,可能不会报错但可能得不到想要的结果。所以,如果⽤像素值(0-255整数)表示图⽚数据,那么一律将其类型设置成uint8,避免不必要的bug。
mnist_train = torchvision.datasets.FashionMNIST(root='./Datasets/FashionMNIST', train=True, download=True, transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root='./Datasets/FashionMNIST', train=False, download=True, transform=transforms.ToTensor())
上⾯的mnist_train和mnist_test都是torch.utils.data.Dataset的子类,所以我们可以用len()来获取该数据集的⼤小,还可以用下标来获取具体的⼀个样本。训练集中和测试集中的每个类别的图像数分别为6,000和1,000。因为有10个类别,所以训练集和测试集的样本数分别为60,000和 10,000。
print(type(mnist_train))
print(len(mnist_train), len(mnist_test))
我们可以通过下标来访问任意⼀个样本:
feature, label = mnist_train[0]
print(feature.shape, label) # Channel x Height x Width
变量feature对⾼和宽均为28像素的图像。由于我们使用了transforms.ToTensor(),所以每个像素的数值为[0.0, 1.0]的32位浮点数。需要注意的是,feature的尺寸是 (C x H x W) 的,⽽不是 (H x W x C)。第⼀维是通道数,因为数据集中是灰度图像,所以通道数为1。后⾯两维分别是图像的高和宽。
Fashion-MNIST中⼀共包括了10个类别,分别为t-shirt(T恤)、trouser(裤⼦子)、pullover(套衫)、 dress(连⾐衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、 bag(包)和ankle boot(短靴)。以下函数可以将数值标签转成相应的⽂本标签。
#可以把本函数保存在d2lzh包中方便以后使用
def get_fashion_mnist_labels(labels):
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]
下⾯定义一个可以在一行⾥画出多张图像和对应标签的函数。
#可以把本函数保存在d2lzh包中方便以后使用
def show_fashion_mnist(images, labels):
d2l.use_svg_display()
# 这里的_表示我们忽略(不使用)的变量
_, figs = plt.subplots(1, len(images), figsize=(12, 12))
for f, img, lbl in zip(figs, images, labels):
f.imshow(img.view((28, 28)).numpy())
f.set_title(lbl)
f.axes.get_xaxis().set_visible(False)
f.axes.get_yaxis().set_visible(False)
plt.show()
现在,我们看一下训练数据集中前9个样本的图像内容和文本标签。
X, y = [], []
for i in range(10):
X.append(mnist_train[i][0])
y.append(mnist_train[i][1])
show_fashion_mnist(X, get_fashion_mnist_labels(y))
我们将在训练数据集上训练模型,并将训练好的模型在测试数据集上评价模型的表现。前⾯说过,mnist_train是torch.utils.data.Dataset的子类,所以我们可以将其传入torch.utils.data.DataLoader来创建一个读取⼩批量数据样本的DataLoader实例。
在实践中,数据读取经常是训练的性能瓶颈,特别当模型较简单或者计算硬件性能较高时。PyTorch的DataLoader中一个很⽅便的功能是允许使用多进程来加速数据读取。这⾥我们通过参数num_workers来设置4个进程读取数据。
batch_size = 256
if sys.platform.startswith('win'):
num_workers = 0 # 0表示不用额外的进程来加速读取数据
else:
num_workers = 4
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
我们将获取并读取Fashion-MNIST数据集的逻辑封装在d2lzh_pytorch.load_data_fashion_mnist函数中供后面实验调用。该函数将返回train_iter和test_iter两个变量。随着内容的不断深入,我们会进⼀步改进该函数。
最后我们查看读取一遍训练数据需要的时间:
start = time.time()
for X, y in train_iter:
continue
print('%.2f sec' % (time.time() - start))
1)Fashion-MNIST是⼀个10类服饰分类数据集,之后的实验将使用它来检验不同算法的表现。
2)我们将高和宽为h和w像素的图像的形状记为h*w或(h,w).
这⼀节我们来动⼿实现softmax回归。首先导⼊本节实现所需的包或模块。
import torch
import torchvision
import numpy as np
import sys
sys.path.append(".") # 为了导入上层目录的d2lzh_pytorch
import d2lzh_pytorch as d2l
def load_data_fashion_mnist(batch_size):
mnist_train = torchvision.datasets.FashionMNIST(root='./Datasets/FashionMNIST', train=True, download=True, transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root='./Datasets/FashionMNIST', train=False, download=True, transform=transforms.ToTensor())
if sys.platform.startswith('win'):
num_workers = 0 # 0表示不用额外的进程来加速读取数据
else:
num_workers = 4
train_iter = torch.utils.data.DataLoader(mnist_train,batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
return train_iter,test_iter
我们将使用Fashion-MNIST数据集,并设置批量⼤小为256。
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
跟线性回归中的例子⼀样,我们将使⽤向量表示每个样本。已知每个样本输⼊是⾼和宽均为28像素的图像。模型的输⼊向量量的⻓度是28*28=784:该向量的每个元素对应图像中每个像素。由于图像有 10个类别,单层神经⽹络输出层的输出个数为10,因此softmax回归的权和偏差参数分别为784*10,1*10的矩阵。
num_inputs = 784
num_outputs = 10
W = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_outputs)), dtype=torch.float)
b = torch.zeros(num_outputs, dtype=torch.float)
同之前一样,我们需要模型参数梯度。
W.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)
在介绍如何定义softmax回归之前,我们先描述⼀下对如何对多维Tensor按维度操作。在下⾯的例子中,给定⼀个Tensor矩阵X。我们可以只对其中同一列(dim=0)或同一行(dim=1)的元素求和,并在结果中保留⾏和列这两个维度(keepdim=True).
X = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(X.sum(dim=0, keepdim=True))
print(X.sum(dim=1, keepdim=True))
下⾯我们就可以定义前面⼩节里介绍的softmax运算了。在下⾯的函数中,矩阵X的行数是样本数,列数是输出个数。为了表达样本预测各个输出的概率,softmax运算会先通过exp函数对每个元素做指数运算,再对exp矩阵同行元素求和,最后令矩阵每⾏各元素与该行元素之和相除。这样一来,最终得到的矩阵每⾏元素和为1且非负。因此,该矩阵每行都是合法的概率分布。softmax运算的输出矩阵中的任意⼀行元素代表了⼀个样本在各个输出类别上的预测概率。
def softmax(X):
X_exp = X.exp()
partition = X_exp.sum(dim=1, keepdim=True)
return X_exp / partition # 这里应用了广播机制
可以看到,对于随机输入,我们将每个元素变成了⾮负数,且每一⾏和为1。
X = torch.rand((2, 5))
X_prob = softmax(X)
print(X_prob, X_prob.sum(dim=1))
有了softmax运算,我们可以定义上节描述的softmax回归模型了。这里通过view函数将每张原始图像改成长度为num_inputs的向量:
def net(X):
return softmax(torch.mm(X.view((-1, num_inputs)), W) + b)
上⼀节中,我们介绍了softmax回归使⽤的交叉熵损失函数。为了得到标签的预测概率,我们可以使用gather函数。在下⾯面的例子中,变量是2个样本在3个类别的预测概率,变量y是这2个样本 的标签类别。通过使⽤函数,我们得到了2个样本的标签的预测概率。与上一节(softmax回归)数学表述中标签类别离散值从1开始逐一递增不同,在代码中,标签类别的离散值是从0开始逐⼀递增的。
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))
下⾯实现(softmax回归)中介绍的交叉熵损失函数。
def cross_entropy(y_hat, y):
return - torch.log(y_hat.gather(1, y.view(-1, 1)))
给定⼀个类别的预测概率分布,我们把预测概率最大的类别作为输出类别。如果它与真实类别y一致,说明这次预测是正确的。分类准确率即正确预测数量与总预测数量之比。
为了演示准确率的计算,下面定义准确率accuracy函数。其中y_hat.argmax(dim=1)返回矩阵y_hat每行最大值所在的索引,且返回结果与变量y形状一致。相等条件判断式y_hat.argmax(dim=1)==y是⼀个类型为ByteTensor的Tensor,我们⽤float()将其转换为值为0(不相等为False)或1(相等为True)的浮点型Tensor。
def accuracy(y_hat, y):
return (y_hat.argmax(dim=1) == y).float().mean().item()
让我们继续使用在演示gather函数时定义的变量y_hat和y,并将他们分别作为预测概率分布和标签。可以看到,第一个样本预测类别为2(该⾏最⼤元素0.6在本⾏的索引为2),与真实标签0不一致;第⼆个样本预测类别为2(该⾏最大元素0.5在本行的索引为2),与真实标签2一致。因此,这两个样本上的分类准确率为0.5。
print(accuracy(y_hat,y))
类似地,我们可以评价模型net在数据集data_iter上的准确率。
# 可以把本函数保存在d2lzh_pytorch包中方便以后使用。该函数将被逐步改进。
def evaluate_accuracy(data_iter, net):
acc_sum, n = 0.0, 0
for X, y in data_iter:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n
因为我们随机初始化了模型net
,所以这个随机模型的准确率应该接近于类别个数10的倒数即0.1。
print(evaluate_accuracy(test_iter,net))
训练softmax回归的实现跟“线性回归的从零开始实现”⼀节介绍的线性回归中的实现⾮常相似。我们同样使⽤⼩批量随机梯度下降来优化模型的损失函数。在训练模型时,迭代周期数num_epochs和学习率lr都是可以调的超参数。改变它们的值可能会得到分类更准确的模型。
num_epochs, lr = 5, 0.1
# 可以把本函数保存在d2lzh包中方便以后使用
def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
params=None, lr=None, optimizer=None):
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()
# 梯度清零
if optimizer is not None:
optimizer.zero_grad()
elif params is not None and params[0].grad is not None:
for param in params:
param.grad.data.zero_()
l.backward()
if optimizer is None:
d2l.sgd(params, lr, batch_size)
else:
optimizer.step() # “softmax回归的简洁实现”一节将用到
train_l_sum += l.item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
n += y.shape[0]
test_acc = evaluate_accuracy(test_iter, net)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
% (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)
训练完成后,现在就可以演示如何对图像进行分类了。给定⼀系列图像(第三⾏图像输出),我们⽐较⼀下它们的真实标签(第一行⽂本输出)和模型预测结果(第⼆行⽂本输出)。
X, y = iter(test_iter).next() #拿到一个batch
true_labels = d2l.get_fashion_mnist_labels(y.numpy())
pred_labels = d2l.get_fashion_mnist_labels(net(X).argmax(dim=1).numpy())
titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]
d2l.show_fashion_mnist(X[0:9], titles[0:9])
可以使⽤softmax回归做多类别分类。与训练线性回归相比,你会发现训练softmax回归的步骤 和它⾮常相似:获取并读取数据、定义模型和损失函数并使⽤优化算法训练模型。事实上,绝⼤多数深度学习模型的训练都有着类似的步骤。
我们在3.3节(线性回归的简洁实现)中已经了解了使⽤Pytorch实现模型的便利。下⾯,让我们再次使用Pytorch来实现一个softmax回归模型。⾸先导入所需的包或模块。
import torch
from torch import nn
from torch.nn import init
import numpy as np
import sys
sys.path.append(".")
import d2lzh_pytorch as d2l
我们仍然使⽤Fashion-MNIST数据集和上⼀节中设置的批量⼤小。
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
在(softmax回归)中提到,softmax回归的输出层是一个全连接层,所以我们⽤⼀个线性模块就可以了。因为前⾯我们数据返回的每个batch样本x的形状为(batch_size,1,28,28),所以我们要先 ⽤view()把x的形状转换成(batch_size,784)再送入全连接层。
num_inputs = 784
num_outputs = 10
class LinearNet(nn.Module):
def __init__(self, num_inputs, num_outputs):
super(LinearNet, self).__init__()
self.linear = nn.Linear(num_inputs, num_outputs)
def forward(self, x): # x shape: (batch, 1, 28, 28)
y = self.linear(x.view(x.shape[0], -1))
return y
net = LinearNet(num_inputs, num_outputs)
我们将对x的形状转换的这个功能⾃定义⼀个FlattenLayer并保存在d2lzh_pytorch中方便以后使用:
# 可以把函数保存在d2lzh_pytorch包中方便以后使用
class FlattenLayer(nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, x): # x shape: (batch, *, *, ...)
return x.view(x.shape[0], -1)
这样我们就可以更⽅便地定义我们的模型:
from collections import OrderedDict
net = nn.Sequential(
# FlattenLayer(),
# nn.Linear(num_inputs, num_outputs)
OrderedDict([
('flatten', FlattenLayer()),
('linear', nn.Linear(num_inputs, num_outputs))
])
)
然后,我们使用均值为0、标准差为0.01的正态分布随机初始化模型的权重参数。
init.normal_(net.linear.weight, mean=0, std=0.01) #也可以不自定义初始化,使用默认参数
init.constant_(net.linear.bias, val=0)
如果做了上⼀节的练习,那么你可能意识到了分开定义softmax运算和交叉熵损失函数可能会造成数值不稳定。因此,PyTorch提供了⼀个包括softmax运算和交叉熵损失计算的函数。它的数值稳定性更好。
loss = nn.CrossEntropyLoss()
我们使⽤学习率为0.1的⼩批量随机梯度下降作为优化算法。
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)
接下来,我们使⽤上一节中定义的训练函数来训练模型。
num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)
1)PyTorch提供的函数往往具有更好的数值稳定性。
2)可以使用PyTorch更简洁地实现softmax回归。