文章目录
前言
回归可以用于预测多少的问题。 比如预测房屋被售出价格,或者棒球队可能获得的胜场数,又或者患者住院的天数。
事实上,我们也对分类问题感兴趣:不是问“多少”,而是问“哪一个”:
- 某个电子邮件是否属于垃圾邮件文件夹?
- 某个用户可能注册或不注册订阅服务?
- 某个图像描绘的是驴、狗、猫、还是鸡?
- 某人接下来最有可能看哪部电影?
通常,机器学习实践者用分类这个词来描述两个有微妙差别的问题: 1. 我们只对样本的“硬性”类别感兴趣,即属于哪个类别; 2. 我们希望得到“软性”类别,即得到属于每个类别的概率。 这两者的界限往往很模糊。其中的一个原因是:即使我们只关心硬类别,我们仍然使用软类别的模型。
Softmax回归
损失函数
图像分类数据集
MNIST数据集 (LeCun et al., 1998) 是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。 我们将使用类似但更复杂的Fashion-MNIST数据集 (Xiao et al., 2017)。
%matplotlib inline
import torch
import torchvision # pytorch对计算机视觉模型实现的库
from torch.utils import data # 方便读取数据小批量的一些函数
from torchvision import transforms # 对数据进行操作的库
from d2l import torch as d2l
d2l.use_svg_display() # 用svg显示图片清晰度高一点
读取数据集
我们可以通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。
# 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,
# 并除以255使得所有像素的数值均在0~1之间
trans = transforms.ToTensor() # 将图片转为tensor类型
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
Fashion-MNIST由10个类别的图像组成, 每个类别由训练数据集(train dataset)中的6000张图像 和测试数据集(test dataset)中的1000张图像组成。 因此,训练集和测试集分别包含60000和10000张图像。 测试数据集不会用于训练,只用于评估模型性能。
每个输入图像的高度和宽度均为28像素。 数据集由灰度图像组成,其通道数为1。 为了简洁起见,本书将高度
h
h
h 像素、宽度
w
w
w 像素图像的形状记为
h
×
w
h × w
h×w 或
(
h
,
w
)
(h,w)
(h,w) 。
Fashion-MNIST中包含的10个类别,分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。 以下函数用于在数字标签索引及其文本名称之间进行转换。
def get_fashion_mnist_labels(labels): #@save
"""返回Fashion-MNIST数据集的文本标签"""
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels] # 根据labels拿出对应的字符串
我们现在可以创建一个函数来可视化这些样本。
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save
"""
imgs: 需要显示的图像集合,可以是PyTorch张量或PIL图像的列表。
num_rows: 图像显示的行数。
num_cols: 图像显示的列数。
titles: 可选参数,为每张图像提供的标题列表。如果提供,每张图像下方将显示对应的标题。
scale: 控制图像显示的总大小,实际大小会乘以这个缩放因子。
"""
figsize = (num_cols * scale, num_rows * scale) # 计算图表的尺寸,使得整个图表的宽和高根据列数、行数和缩放因子scale动态调整。
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize) # 使用matplotlib的subplots函数根据指定的行数和列数创建一个子图网格,并指定整个图表的尺寸。axes是一个数组,包含了所有子图的轴对象。
axes = axes.flatten() # 如果子图超过一行,axes将是一个二维数组。通过flatten方法将其转换为一维数组,便于后续遍历。
for i, (ax, img) in enumerate(zip(axes, imgs)): # zip将axes,imgs对应的一对打包成元组,并用enumerate迭代出每一对的值和对应的索引
if torch.is_tensor(img): # 如果img是一个张量,使用img.numpy()将其转换为NumPy数组
ax.imshow(img.numpy()) # 然后用ax.imshow(img.numpy())在对应的子图轴上显示图像。
else:
# PIL图片
ax.imshow(img) # 如果img不是张量,则假定它是PIL图像,直接使用ax.imshow(img)显示。
ax.axes.get_xaxis().set_visible(False) # 隐藏了每个子图的x轴和y轴的标签和刻度,使图像显示更为简洁。
ax.axes.get_yaxis().set_visible(False)
if titles: # 如果提供了titles参数,ax.set_title(titles[i])为每个子图设置标题。
ax.set_title(titles[i])
return axes # 函数返回axes,即所有子图的轴对象,允许调用者进一步自定义图表,如添加标签或其他图形元素。
以下是训练数据集中前几个样本的图像及其相应的标签。
读取小批量
为了使我们在读取训练集和测试集时更容易,我们使用内置的数据迭代器,而不是从零开始创建。 回顾一下,在每次迭代中,数据加载器每次都会读取一小批量数据,大小为batch_size。 通过内置数据迭代器,我们可以随机打乱了所有样本,从而无偏见地读取小批量。
batch_size = 256
def get_dataloader_workers(): #@save
"""使用4个进程来读取数据"""
return 4
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True, num_workers=get_dataloader_workers())
我们看一下读取训练数据所需的时间。
整合所有组件
现在我们定义load_data_fashion_mnist函数,用于获取和读取Fashion-MNIST数据集。 这个函数返回训练集和验证集的数据迭代器。 此外,这个函数还接受一个可选参数resize,用来将图像大小调整为另一种形状。
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""
:param batch_size: 数据加载器每批次加载的图像数量。在训练深度学习模型时,批量大小可以影响模型的训练效率和性能。
:param resize: 可选参数,如果提供,所有的图像将被调整到指定的大小(宽度和高度)。这对于模型需要特定输入图像大小的情况非常有用。
:return:
"""
trans = [transforms.ToTensor()] # 这里将trans构建在列表中因为后续可能会根据条件向这个列表中添加更多的变换操作
if resize: # 如果指定了resize参数,会在trans列表的开始处插入一个transforms.Resize(resize)转换。这个转换改变图像的大小。
trans.insert(0, transforms.Resize(resize)) # 0表示插入在索引0的位置,即插在列表最开始的位置。这意味着,在对图像应用所有其他变换之前,首先应用transforms.Resize(resize)变换。
trans = transforms.Compose(trans) # 将多个变换转化到一个变换里,这里只用到了transforms.ToTensor
mnist_train = torchvision.datasets.FashionMNIST(root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(root="../data", train=False, transform=trans, download=True)
return (data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=get_dataloader_workers()))
下面,我们通过指定resize参数来测试load_data_fashion_mnist函数的图像大小调整功能。
我们现在已经准备好使用Fashion-MNIST数据集,便于下面的章节调用来评估各种分类算法。
小结
-
Fashion-MNIST是一个服装分类数据集,由10个类别的图像组成。我们将在后续章节中使用此数据集来评估各种分类算法。
-
我们将高度 h h h 像素、宽度 w w w 像素图像的形状记为 h × w h × w h×w 或 ( h , w ) (h,w) (h,w) 。
-
数据迭代器是获得更高性能的关键组件。依靠实现良好的数据迭代器,利用高性能计算来避免减慢训练过程。
softmax回归的从零开始实现
就像我们从零开始实现线性回归一样, 我们认为softmax回归也是重要的基础,因此应该知道实现softmax回归的细节。 本节我们将使用刚刚引入的Fashion-MNIST数据集, 并设置数据迭代器的批量大小为256。
import torch
from IPython import display
from d2l import torch as d2l
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""
:param batch_size: 数据加载器每批次加载的图像数量。在训练深度学习模型时,批量大小可以影响模型的训练效率和性能。
:param resize: 可选参数,如果提供,所有的图像将被调整到指定的大小(宽度和高度)。这对于模型需要特定输入图像大小的情况非常有用。
:return:
"""
trans = [transforms.ToTensor()] # 这里将trans构建在列表中因为后续可能会根据条件向这个列表中添加更多的变换操作
if resize: # 如果指定了resize参数,会在trans列表的开始处插入一个transforms.Resize(resize)转换。这个转换改变图像的大小。
trans.insert(0, transforms.Resize(resize)) # 0表示插入在索引0的位置,即插在列表最开始的位置。这意味着,在对图像应用所有其他变换之前,首先应用transforms.Resize(resize)变换。
trans = transforms.Compose(trans) # 将多个变换转化到一个变换里,这里只用到了transforms.ToTensor
mnist_train = torchvision.datasets.FashionMNIST(root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(root="../data", train=False, transform=trans, download=True)
return (data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=get_dataloader_workers()))
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
初始化模型参数
和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。 原始数据集中的每个样本都是28 × 28的图像。 本节将展平每个图像,把它们看作长度为784的向量。 在后面的章节中,我们将讨论能够利用图像空间结构的特征, 但现在我们暂时只把每个像素位置看作一个特征。
回想一下,在softmax回归中,我们的输出与类别一样多。 因为我们的数据集有10个类别,所以网络输出维度为10。 因此,权重将构成一个784 × 10的矩阵, 偏置将构成一个1 × 10的行向量。 与线性回归一样,我们将使用正态分布初始化我们的权重W,偏置初始化为0。
num_inputs = 784 # 将1*28*28的单通道图片压缩成长度为784的向量,作为输入
num_outputs = 10 # 一共有10个类型的图片,输出维度为10
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True) # w为784*10的随机正态分布
b = torch.zeros(num_outputs, requires_grad=True) # b是长度为10的向量
定义softmax操作
在实现softmax回归模型之前,我们简要回顾一下sum运算符如何沿着张量中的特定维度工作。 如之前所述, 给定一个矩阵X,我们可以对所有元素求和(默认情况下)。 也可以只求同一个轴上的元素,即同一列(轴0)或同一行(轴1)。 如果X是一个形状为(2, 3)的张量,我们对列进行求和, 则结果将是一个具有形状(3,)的向量。 当调用sum运算符时,我们可以指定保持在原始张量的轴数,而不折叠求和的维度。 这将产生一个具有形状(1, 3)的二维张量。
def softmax(X):
X_exp = torch.exp(X) # 将X每一行做指数运算
partition = X_exp.sum(1, keepdim=True) # 求X每一行指数运算的和
return X_exp / partition # 这里应用了广播机制,做归一化处理,保证和为1
正如上述代码,对于任何随机输入,我们将每个元素变成一个非负数。 此外,依据概率原理,每行总和为1。
注意,虽然这在数学上看起来是正确的,但我们在代码实现中有点草率。 矩阵中的非常大或非常小的元素可能造成数值上溢或下溢,但我们没有采取措施来防止这点。
定义模型
定义softmax操作后,我们可以实现softmax回归模型。 下面的代码定义了输入如何通过网络映射到输出。 注意,将数据传递到模型之前,我们使用reshape函数将每张原始图像展平为向量。
def net(X): # 网络模型,将X转为256*784的矩阵与w做矩阵乘法,得到256*10,再与b相加做softmax
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
定义损失函数
接下来,我们实现 交叉熵损失函数。 这可能是深度学习中最常见的损失函数,因为目前分类问题的数量远远超过回归问题的数量。
回顾一下,交叉熵采用真实标签的预测概率的负对数似然。 这里我们不使用Python的for循环迭代预测(这往往是低效的), 而是通过一个运算符选择所有元素。 下面,我们创建一个数据样本y_hat,其中包含2个样本在3个类别的预测概率, 以及它们对应的标签y。 有了y,我们知道在第一个样本中,第一类是正确的预测; 而在第二个样本中,第三类是正确的预测。 然后使用y作为y_hat中概率的索引, 我们选择第一个样本中第一个类的概率和第二个样本中第三个类的概率。
现在我们只需一行代码就可以实现交叉熵损失函数。
分类精度
给定预测概率分布y_hat,当我们必须输出硬预测(hard prediction)时, 我们通常选择预测概率最高的类。 许多应用都要求我们做出选择。如Gmail必须将电子邮件分类为“Primary(主要邮件)”、 “Social(社交邮件)”“Updates(更新邮件)”或“Forums(论坛邮件)”。 Gmail做分类时可能在内部估计概率,但最终它必须在类中选择一个。
当预测与标签分类y一致时,即是正确的。 分类精度即正确预测数量与总预测数量之比。 虽然直接优化精度可能很困难(因为精度的计算不可导), 但精度通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总会关注它。
为了计算精度,我们执行以下操作。 首先,如果y_hat是矩阵,那么假定第二个维度存储每个类的预测分数。 我们使用argmax获得每行中最大元素的索引来获得预测类别。 然后我们将预测类别与真实y元素进行比较。 由于等式运算符“==”对数据类型很敏感, 因此我们将y_hat的数据类型转换为与y的数据类型一致。 结果是一个包含0(错)和1(对)的张量。 最后,我们求和会得到正确预测的数量。
def accuracy(y_hat, y): #@save
"""计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: # 如果y_hat是二维,并且列数超过1(表示预测的样本数大于1)
y_hat = y_hat.argmax(axis=1) # 将每一行的最大预测概率的索引存入y_hat(我们只需要知道预测的具体值是否正确),这里是将二维矩阵转化为一维向量重新赋值给y_hat,在Python中,变量可以被重新赋值为不同形状或类型的数据。
cmp = y_hat.type(y.dtype) == y # 将y_hat转化为与y形状相同的张量,并于y进行比较,产生一个布尔张量,预测正确的为True
return float(cmp.type(y.dtype).sum()) # 将cmp布尔类型转化为与y相同类型(这里是整型,这里转化结果为tensor([0, 1]),0表示False,1表示True),然后计算总和(这个总数就是模型预测正确的样本数。),转化为浮点数返回(方便后面计算,并且张量类型一般为float类型)
我们将继续使用之前定义的变量y_hat和y分别作为预测的概率分布和标签。 可以看到,第一个样本的预测类别是2(该行的最大元素为0.6,索引为2),这与实际标签0不一致。 第二个样本的预测类别是2(该行的最大元素为0.5,索引为2),这与实际标签2一致。 因此,这两个样本的分类精度率为0.5。
同样,对于任意数据迭代器data_iter可访问的数据集, 我们可以评估在任意模型net的精度。
def evaluate_accuracy(net, data_iter): #@save
"""
:param net: net是要评估的模型,一个torch.nn.Module的实例。
:param data_iter: data_iter是数据迭代器,用来提供用于评估的数据批次。
:return:
"""
if isinstance(net, torch.nn.Module): # 检查net是否为torch.nn.Module的实例。这是为了确保只有PyTorch模型才执行下一步。
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # Accumulator实例将会累加两种类型的值(正确预测数、预测总数)
with torch.no_grad(): # 在评估模型时不需要进行反向传播,只需要前向传播
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel()) # 将X放入网络模型,求与真实的y之间正确预测数,y.numel()是一个PyTorch方法,返回张量中元素的总数(预测总数)
return metric[0] / metric[1] # 模型正确预测数 / 预测总数 = 预测正确的概率
这里定义一个实用程序类Accumulator,用于对多个变量进行累加。 在上面的evaluate_accuracy函数中, 我们在Accumulator实例中创建了2个变量, 分别用于存储正确预测的数量和预测的总数量。 当我们遍历数据集时,两者都将随着时间的推移而累加。
class Accumulator: #@save
"""在n个变量上累加"""
def __init__(self, n): # 接收一个参数n,表示需要累加的变量数量。
self.data = [0.0] * n # 初始化了一个长度为n的列表,列表中的每个元素都被设置为0.0。这个列表用于存储每个变量的累加值。
def add(self, *args): # 定义了一个add方法,它接收任意数量的参数(通过*args),每个参数对应于要累加到self.data中的值。
self.data = [a + float(b) for a, b in zip(self.data, args)] # 遍历self.data和args中的元素,将它们逐对相加,并将结果存回self.data中。这里使用float(b)确保加数是浮点数类型,以免类型不匹配的错误。
def reset(self): # 定义了一个reset方法,用于重置self.data中的所有值为0.0。这在开始新的累加前清零很有用。
self.data = [0.0] * len(self.data) # 通过将self.data重新设置为全是0.0的列表来实现重置,列表的长度与之前相同。
def __getitem__(self, idx): # 利用特殊方法__getitem__,使得Accumulator实例可以使用索引访问操作,类似于列表。
return self.data[idx] # 返回self.data中索引idx处的值。
由于我们使用随机权重初始化net模型, 因此该模型的精度应接近于随机猜测。 例如在有10个类别情况下的精度为0.1。
训练
在这里,我们重构训练过程的实现以使其可重复使用。 首先,我们定义一个函数来训练一个迭代周期。 请注意,updater是更新模型参数的常用函数,它接受批量大小作为参数。 它可以是d2l.sgd函数,也可以是框架的内置优化函数。
def train_epoch_ch3(net, train_iter, loss, updater): #@save
"""
:param net: 要训练的模型,一个torch.nn.Module的实例。
:param train_iter: 一个迭代器,提供训练数据的批次。
:param loss: 用于计算预测和真实标签之间差异的损失函数。
:param updater: 参数更新策略。它可以是一个PyTorch优化器(例如,torch.optim.SGD),或者是一个自定义的更新函数。
:return:
"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
net.train()
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3)
for X, y in train_iter:
# 计算梯度并更新参数
y_hat = net(X)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer): # 判断updater是否是一个PyTorch优化器(torch.optim.Optimizer优化器)
# 使用PyTorch内置的优化器和损失函数
updater.zero_grad() # 梯度清零
l.mean().backward() # 损失均值做反向传播
updater.step() # 梯度更新
else:
# 使用定制的优化器和损失函数
l.sum().backward() # l求和后反向传播求梯度
updater(X.shape[0]) # 通过将X.shape[0](当前批次的样本数量)作为参数传递给updater,自定义优化器可以根据批次大小动态调整更新策略。例如,在某些实现中,可能需要根据批次大小来平均化梯度,或者调整学习率。
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel()) # 当前批次所有样本的损失之和、预测正确的样本数量、当前批次中样本的总数
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2] # 平均损失、平均准确率
在展示训练函数的实现之前,我们定义一个在动画中绘制数据的实用程序类Animator, 它能够简化本书其余部分的代码。
class Animator: #@save
"""
xlabel, ylabel: 分别设置X轴和Y轴的标签。
legend: 图例,用于解释每条线代表的数据。
xlim, ylim: 分别设置X轴和Y轴的显示范围。
xscale, yscale: 分别设置X轴和Y轴的缩放方式(如线性、对数等)。
fmts: 设置线的格式,如实线、虚线等。
nrows, ncols: 设置子图的行数和列数。
figsize: 设置图的大小。
"""
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
figsize=(3.5, 2.5)):
# 增量地绘制多条线
if legend is None:
legend = []
d2l.use_svg_display() # 图像更清晰
self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize) # 创建的图形对象、子图对象的数组
if nrows * ncols == 1: # 创建子图只有一个时
self.axes = [self.axes, ] # 将子图对象的数组转化为列表,方便统一操作以及后面的迭代
# 使用lambda函数捕获参数
self.config_axes = lambda: d2l.set_axes(
self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts
def add(self, x, y):
# 向图表中添加多个数据点
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)
接下来我们实现一个训练函数, 它会在train_iter访问到的训练数据集上训练一个模型net。 该训练函数将会运行多个迭代周期(由num_epochs指定)。 在每个迭代周期结束时,利用test_iter访问到的测试数据集对模型进行评估。 我们将利用Animator类来可视化训练进度。
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
"""训练模型(定义见第3章)"""
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], # 设置横轴显示范围从第1轮到最后一轮num_epochs。设置纵轴显示范围为0.3到0.9
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater) # 训练一轮(训练集)
test_acc = evaluate_accuracy(net, test_iter) # 评估当前模型在测试数据集(test_iter)上的准确率。
animator.add(epoch + 1, train_metrics + (test_acc,)) # 将当前训练周期的结果(包括训练指标和测试准确率)添加到动画中。这里的epoch + 1是因为周期是从0开始的,但我们希望在图表中从1开始显示。train_metrics + (test_acc,)将测试准确率作为元组添加到train_metrics后面,以便一起传递给Animator.add方法。
train_loss, train_acc = train_metrics # 将train_metrics(包含训练损失和训练准确率)分解给train_loss和train_acc
assert train_loss < 0.5, train_loss # 检查训练损失train_loss是否小于0.5。如果不是,程序将抛出AssertionError并输出当前的train_loss值,提示训练损失过高,可能模型训练不理想。
assert train_acc <= 1 and train_acc > 0.7, train_acc # 确保训练准确率train_acc大于0.7且不超过1。这个断言验证模型在训练集上有良好的表现,并且准确率的值是合理的(不会超过100%)。
assert test_acc <= 1 and test_acc > 0.7, test_acc # 检查测试准确率test_acc也应大于0.7且不超过1,以确认模型在未见过的数据上也能有良好的表现。
作为一个从零开始的实现,我们使用小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1。
lr = 0.1 # 学习率
def updater(batch_size): # 定义优化器
return d2l.sgd([W, b], lr, batch_size) # 随机梯度下降优化器
现在,我们训练模型10个迭代周期。 请注意,迭代周期(num_epochs)和学习率(lr)都是可调节的超参数。 通过更改它们的值,我们可以提高模型的分类精度。
预测
现在训练已经完成,我们的模型已经准备好对图像进行分类预测。 给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。
def predict_ch3(net, test_iter, n=6): #@save
"""
:param net: 模型
:param test_iter: 测试数据的迭代器
:param n: 要显示的样本数量,默认为6
:return:
"""
for X, y in test_iter: # 只获取第一批测试数据
break
trues = d2l.get_fashion_mnist_labels(y) # d2l.get_fashion_mnist_labels函数将真实的标签索引(y)转换为人类可读的标签字符串。
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1)) # 首先使用模型net对输入X进行预测,得到预测结果的概率分布。然后使用.argmax(axis=1)找到概率最高的标签索引,最后同样使用d2l.get_fashion_mnist_labels函数将这些索引转换为标签字符串。
titles = [true +'\n' + pred for true, pred in zip(trues, preds)] # 将每个样本的真实标签和预测标签组合成一个字符串,中间用换行符\n分隔,这样每个图像上方将显示两行文本。
d2l.show_images(X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n]) # 将选取的图像数据重新整形为28x28的尺寸,因为原始图像数据被扁平化或具有不同的维度布局。
predict_ch3(net, test_iter)
输出结果如下:
小结
- 借助softmax回归,我们可以训练多分类的模型。
- 训练softmax回归循环模型与训练线性回归模型非常相似:先读取数据,再定义模型和损失函数,然后使用优化算法训练模型。大多数常见的深度学习模型都有类似的训练过程。
Softmax回归的简洁实现
我们发现通过深度学习框架的高级API能够使实现线性回归变得更加容易。 同样,通过深度学习框架的高级API也能更方便地实现softmax回归模型。 本节如之前一样,继续使用Fashion-MNIST数据集,并保持批量大小为256。
import torch
from torch import nn
from d2l import torch as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
初始化模型参数
如我前文所述, softmax回归的输出层是一个全连接层。 因此,为了实现我们的模型, 我们只需在Sequential中添加一个带有10个输出的全连接层。 同样,在这里Sequential并不是必要的, 但它是实现深度模型的基础。 我们仍然以均值0和标准差0.01随机初始化权重。
# PyTorch不会隐式地调整输入的形状。因此,
# nn.Flatten()是一个层,用于将输入的多维数据“展平”为一维数据。对于图像数据,例如大小为28x28的图像,nn.Flatten()将这些图像转换成784(28*28)个元素的一维数组。这个操作通常用于将多维的图像数据准备好,以便作为全连接层(nn.Linear)的输入。
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
def init_weights(m):
if type(m) == nn.Linear: # 检查传入的模块m是否是nn.Linear类型的层
nn.init.normal_(m.weight, std=0.01) # 使用均值为0(没有写出来,默认为0)、标准差为0.01的正态分布来初始化传入的全连接层的权重。第一个参数m.weight是要初始化的张量
# apply方法对模型中的每个模块(层)应用init_weights函数。这意味着init_weights函数将被递归地应用于net中定义的每一个层,包括nn.Flatten()和nn.Linear(784, 10)。
net.apply(init_weights);
重新审视Softmax的实现
"""
reduction参数控制损失的计算方式。在PyTorch中,reduction有三个可选值:'mean'(默认值)、'sum'和'none'。
当reduction='mean'时,返回所有样本损失的均值。
当reduction='sum'时,返回所有样本损失的总和。
当reduction='none'时,不进行归约,即对每个样本单独计算损失,并返回一个与样本数相同长度的向量,其中包含了每个样本的损失。
"""
loss = nn.CrossEntropyLoss(reduction='none') # 交叉熵损失函数,CrossEntropyLoss == LogSoftmax + NLLLoss
优化算法
在这里,我们使用学习率为0.1的小批量随机梯度下降作为优化算法。 这与我们在线性回归例子中的相同,这说明了优化器的普适性。
"""
net.parameters()是一个生成器,它会返回模型net中所有可训练的参数(如权重和偏置)。这些参数是SGD优化器需要更新的对象。
通过传递net.parameters()给SGD,我们告诉优化器哪些具体的参数需要在训练过程中被优化。
"""
trainer = torch.optim.SGD(net.parameters(), lr=0.1) # 随机梯度下降(SGD)优化器,学习率为0.1
训练
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
"""训练模型(定义见第3章)"""
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], # 设置横轴显示范围从第1轮到最后一轮num_epochs。设置纵轴显示范围为0.3到0.9
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater) # 训练一轮(训练集)
test_acc = evaluate_accuracy(net, test_iter) # 评估当前模型在测试数据集(test_iter)上的准确率。
animator.add(epoch + 1, train_metrics + (test_acc,)) # 将当前训练周期的结果(包括训练指标和测试准确率)添加到动画中。这里的epoch + 1是因为周期是从0开始的,但我们希望在图表中从1开始显示。train_metrics + (test_acc,)将测试准确率作为元组添加到train_metrics后面,以便一起传递给Animator.add方法。
train_loss, train_acc = train_metrics # 将train_metrics(包含训练损失和训练准确率)分解给train_loss和train_acc
assert train_loss < 0.5, train_loss # 检查训练损失train_loss是否小于0.5。如果不是,程序将抛出AssertionError并输出当前的train_loss值,提示训练损失过高,可能模型训练不理想。
assert train_acc <= 1 and train_acc > 0.7, train_acc # 确保训练准确率train_acc大于0.7且不超过1。这个断言验证模型在训练集上有良好的表现,并且准确率的值是合理的(不会超过100%)。
assert test_acc <= 1 and test_acc > 0.7, test_acc # 检查测试准确率test_acc也应大于0.7且不超过1,以确认模型在未见过的数据上也能有良好的表现。
和以前一样,这个算法使结果收敛到一个相当高的精度,而且这次的代码比之前更精简了。
小结
- 使用深度学习框架的高级API,我们可以更简洁地实现softmax回归。
- 从计算的角度来看,实现softmax回归比较复杂。在许多情况下,深度学习框架在这些著名的技巧之外采取了额外的预防措施,来确保数值的稳定性。这使我们避免了在实践中从零开始编写模型时可能遇到的陷阱。