softmax回归

概念

定义

Softmax回归,也常被称为多项Logistic回归,是Logistic回归模型在多类分类问题上的推广。它主要用于处理多分类问题,其中类别数大于2。Softmax回归模型能够给出一个对象属于每个类别的概率,并且这些概率的总和为1。这使得Softmax回归成为处理多类别直接选择问题的一个自然选择。

在之前的机器学习中已经学习过logistic回归了,而logistic回归是softmax回归的一种特殊形式,即$n=2$的情况

对于输入数据$\{(x_1,y_1),(x_2,y_2),\ldots,(x_m,y_m)\}$有$k$个类别,即$y_i \in \{1,2,\ldots,k\}$,那么softmax回归主要估算输入数据$x_i$归属每一类的概率,即:

其中,$\theta_1,\theta_2,\ldots,\theta_k \in \theta$是模型的参数,乘以$\frac{1}{\sum_{j=1}^{k} e^{\theta_{j}^{T} x_i}}$是为了让概率位于$[0,1]$并且概率之和为 1,softmax 回归将输入数据 $x_i$归属于类别 $j$的概率为


矢量化

为了提高计算效率并且充分利用GPU,我们通常会对小批量样本的数据执行矢量计算。

假设我们读取了一个批量的样本$\mathbf{X}$,其中特征维度(输入数量)为$d$,批量大小为$n$。

此外,假设我们在输出中有$q$个类别。

那么小批量样本的特征为$\mathbf{X} \in \mathbb{R}^{n \times d}$,

权重为$\mathbf{W} \in \mathbb{R}^{d \times q}$,

偏置为$\mathbf{b} \in \mathbb{R}^{1\times q}$。

softmax回归的矢量计算表达式为:

相对于一次处理一个样本,小批量样本的矢量化加快了$\mathbf{X}$和$\mathbf{W}$的矩阵-向量乘法。由于$\mathbf{X}$中的每一行代表一个数据样本,那么softmax运算可以按行执行:对于$\mathbf{O}$的每一行,我们先对所有项进行幂运算,然后通过求和对它们进行标准化。

$\mathbf{X} \mathbf{W} + \mathbf{b}$的求和会使用广播机制,小批量的未规范化预测$\mathbf{O}$和输出概率$\hat{\mathbf{Y}}$都是形状为$n \times q$的矩阵。


损失函数

softmax函数给出了一个向量$\hat{\mathbf{y}}$,我们可以将其视为“对给定任意输入$\mathbf{x}$的每个类的条件概率”。

例如,$\hat{y}_1$=$P(y=\text{猫} \mid \mathbf{x})$。

假设整个数据集$\{\mathbf{X}, \mathbf{Y}\}$具有$n$个样本,其中索引$i$的样本由特征向量$\mathbf{x}^{(i)}$和独热标签向量$\mathbf{y}^{(i)}$组成。

我们可以将估计值与实际值进行比较:

根据最大似然估计,我们最大化$P(\mathbf{Y} \mid \mathbf{X})$,相当于最小化负对数似然:

其中,对于任何标签$\mathbf{y}$和模型预测$\hat{\mathbf{y}}$,损失函数为:


导数

由于softmax和相关的损失函数很常见,因此我们需要更好地理解它的计算方式。

利用softmax的定义,我们得到:

考虑相对于任何未规范化的预测$o_j$的导数,我们得到:

换句话说,导数是我们softmax模型分配的概率与实际发生的情况(由独热标签向量表示)之间的差异

从这个意义上讲,这与我们在回归中看到的非常相似,其中梯度是观测值$y$和估计值$\hat{y}$之间的差异。


图像分类数据集

1
2
3
4
5
6
7
8
%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l

d2l.use_svg_display()

通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。

读取数据集

1
2
3
4
5
6
7
8
# 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,
# 并除以255使得所有像素的数值均在0~1之间
# 这个步骤通常是对图像数据进行预处理的标准步骤,因为它有助于神经网络更好地学习图像特征。
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)

Fashion-MNIST由10个类别的图像组成, 每个类别由训练数据集(train dataset)中的6000张图像 和测试数据集(test dataset)中的1000张图像组成。 因此,训练集和测试集分别包含60000和10000张图像。 测试数据集不会用于训练,只用于评估模型性能

1
len(mnist_train), len(mnist_test)

输出结果是(60000,10000),每个输入图像的高度和宽度均为28像素。 数据集由灰度图像组成,其通道数为1。

Fashion-MNIST中包含的10个类别,分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。 以下函数用于在数字标签索引及其文本名称之间进行转换。

1
2
3
4
5
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]

现在可以创建一个函数来可视化这些样本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):  #@save
"""
绘制图像列表。

参数:
imgs: 一个图像集合,可以是PIL图像或者是PyTorch张量。
num_rows: 要显示的行数。
num_cols: 要显示的列数。
titles: (可选)一个标题列表,用于每张图像。
scale: (可选)图像显示的缩放比例,默认为1.5。
"""
# 计算整个画布的大小
figsize = (num_cols * scale, num_rows * scale)
# 创建一个子图序列
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
# 将axes数组扁平化,方便遍历
axes = axes.flatten()
# 遍历图像和对应的轴(axes)进行绘图
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
# 如果图像是PyTorch张量,则将其转换为numpy数组并显示
ax.imshow(img.numpy())
else:
# 如果图像是PIL图像,则直接显示
ax.imshow(img)
# 隐藏x和y轴的标签
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
# 如果提供了标题,为每张图像设置标题
if titles:
ax.set_title(titles[i])
# 返回绘制的轴(axes),以便于进一步的操作或调整
return axes

以下是训练数据集中前几个样本的图像及其相应的标签。

1
2
X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y));
  • X.reshape(18, 28, 28): 将获取的图像数据X重塑成18个28x28大小的图像。DataLoader返回的图像数据默认是扁平化的或有不同的维度,所以需要将其重塑回原始图像的尺寸以便于显示。
  • 2, 9: 指定在显示图像时使用2行9列的网格。因为一共有18个样本,这样的布局可以确保所有样本都能被显示出来。

image-20240220153046704


小批量读取

为了使我们在读取训练集和测试集时更容易,我们使用内置的数据迭代器,而不是从零开始创建。 回顾一下,在每次迭代中,数据加载器每次都会读取一小批量数据,大小为batch_size。 通过内置数据迭代器,我们可以随机打乱了所有样本,从而无偏见地读取小批量。

1
2
3
4
5
6
7
8
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())

读取训练数据所需的时间:

1
2
3
4
timer = d2l.Timer()
for X, y in train_iter:
continue
f'{timer.stop():.2f} sec'

3.25sec左右


整合

定义load_data_fashion_mnist函数,用于获取和读取Fashion-MNIST数据集。 这个函数返回训练集和验证集的数据迭代器。 此外,这个函数还接受一个可选参数resize,用来将图像大小调整为另一种形状。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def load_data_fashion_mnist(batch_size, resize=None):  #@save
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
trans = [transforms.ToTensor()]
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
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()))

从零实现

1
2
3
4
5
6
import torch
from IPython import display
from d2l import torch as d2l

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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 定义输入向量的维度,对于FashionMNIST数据集,每张图像是28x28像素,
# 因此当图像被展平成向量时,它的大小是784
num_inputs = 784

# 定义输出向量的维度,这里有10个输出类别,对应于FashionMNIST数据集的10种服装类别
num_outputs = 10

# 初始化权重矩阵W。
# 使用正态分布随机初始化,均值为0,标准差为0.01。
# W的形状是(num_inputs, num_outputs),这样每个输入特征都会与每个输出类别关联一个权重。
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)

# 初始化偏置项b。
# 由于每个输出类别都有一个偏置,所以b的形状是(num_outputs,)。
# 初始化为0,并且设置requires_grad=True,以便于在反向传播时计算梯度。
b = torch.zeros(num_outputs, requires_grad=True)


定义softmax操作

实现softmax由三个步骤组成:

  1. 对每个项求幂(使用exp);
  2. 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;
  3. 将每一行除以其规范化常数,确保结果的和为1。

在查看代码之前,我们回顾一下这个表达式:

1
2
3
4
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 这里应用了广播机制

参数keepdim=True意味着在求和后保持原有的维度,不会降维。即按行求和


定义模型

定义softmax操作后,我们可以实现softmax回归模型。 下面的代码定义了输入如何通过网络映射到输出。 注意,将数据传递到模型之前,我们使用reshape函数将每张原始图像展平为向量。

1
2
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
  • X.reshape((-1, W.shape[0]))将输入X重塑,以便于与权重矩阵W进行矩阵乘法。这里假设X的每一行是一个独立的样本。
  • torch.matmul(...)执行矩阵乘法。
  • + b将偏置项b加到矩阵乘法的结果上。
  • softmax(...)将线性模型的输出通过softmax函数转换成概率分布。

定义损失函数

先介绍一下pytorch中张量索引的特性:

1
2
3
y = torch.tensor([0, 2])
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y_hat[[0, 1], y]

这相当于是y_hat[[0,1],[0,2]],前一个框内是元素的行索引,后一个框是元素的列索引,最终会输出tensor([0.1000,0.5000])

1
2
3
4
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])

cross_entropy(y_hat, y)

y_hat[range(len(y_hat)), y]是一个高效的索引操作,它选择了每个样本预测概率中对应于真实标签的概率。具体来说,对于每个样本i,它选择y_hat[i, y[i]],即第i个样本的真实类别y[i]对应的预测概率。

- torch.log(...)计算每个选中概率的负对数。负对数损失对于正确分类的预测(预测概率接近1)几乎为0,而对于错误分类的预测(预测概率接近0)则非常大。这意味着模型在训练过程中被激励去增加正确类别的预测概率。


分类精度

当预测与标签分类y一致时,即是正确的。 分类精度即正确预测数量与总预测数量之比。 虽然直接优化精度可能很困难(因为精度的计算不可导), 但精度通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总会关注它。

为了计算精度,我们执行以下操作。 首先,如果y_hat是矩阵,那么假定第二个维度存储每个类的预测分数。 我们使用argmax获得每行中最大元素的索引来获得预测类别。 然后我们将预测类别与真实y元素进行比较。 由于等式运算符“==”对数据类型很敏感, 因此我们将y_hat的数据类型转换为与y的数据类型一致。 结果是一个包含0(错)和1(对)的张量。 最后,我们求和会得到正确预测的数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def accuracy(y_hat, y):  #@save
"""
计算预测正确的数量。

参数:
y_hat: 模型的预测输出。可以是每个类别的得分或概率。
y: 真实的标签。

返回:
预测正确的样本数占总样本数的比例。
"""
# 检查预测输出是否为二维且类别数大于1,
# 如果是,则使用argmax获取每个样本最高得分的类别索引作为预测类别
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)

# 比较预测类别(y_hat)和真实标签(y)是否相同,
# 结果是一个布尔型张量,其中True表示预测正确,False表示预测错误。
cmp = y_hat.type(y.dtype) == y

# 将布尔型张量转换为与y相同的数据类型(通常是整型),
# 然后计算True的数量(即预测正确的数量)。
# 最后,将其转换为float类型,以便计算准确率。
return float(cmp.type(y.dtype).sum())

argmax会缩减y_hat的维度,让其只保留最大概率的索引类别,这里的.type(y.dtype)是为了避免发生类型错误,把元素转换为和y一样的类型

同样,对于任意数据迭代器data_iter可访问的数据集, 我们可以评估在任意模型net的精度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def evaluate_accuracy(net, data_iter):  #@save
"""
计算在指定数据集上模型的精度。

参数:
net: 要评估的模型,可以是任何具有前向传播功能的PyTorch模型。
data_iter: 数据迭代器,用于提供评估数据集的批次数据。

返回:
模型在整个数据集上的准确率。
"""
# 检查是否为PyTorch的nn.Module模型。如果是,则将模型设置为评估模式。
# 评估模式会关闭模型中的Dropout和BatchNorm等层的训练行为,以保证前向传播的一致性。
if isinstance(net, torch.nn.Module):
net.eval()

# 初始化一个累加器实例用于跟踪正确预测的数量和预测的总数量。
metric = Accumulator(2) # 正确预测数、预测总数

# 不计算梯度,以节省计算资源,因为在评估模式下不需要进行反向传播。
with torch.no_grad():
for X, y in data_iter:
# 计算当前批次的准确率,并累加到metric中。
# net(X)调用模型进行预测,accuracy函数计算准确率。
metric.add(accuracy(net(X), y), y.numel())

# 计算整个数据集上的总准确率。
# metric[0]是正确预测的数量,metric[1]是预测的总数量。
return metric[0] / metric[1]

这里定义一个实用程序类Accumulator,用于对多个变量进行累加。 在上面的evaluate_accuracy函数中, 我们在Accumulator实例中创建了2个变量, 分别用于存储正确预测的数量和预测的总数量。 当我们遍历数据集时,两者都将随着时间的推移而累加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Accumulator:  #@save
"""在n个变量上累加"""

def __init__(self, n):
"""
类的初始化方法。
参数:
n (int): 指定需要累加的变量的数量。
属性:
data (list of float): 用于存储每个变量的累加值的列表,初始时每个变量的值都设置为0.0。
"""
self.data = [0.0] * n

def add(self, *args):
"""
将传入的值加到累加器的对应变量上。

参数:
*args: 可变数量的参数,每个参数对应于需要累加到的变量的增量值。

说明:
- 使用zip函数将self.data中的当前累加值和传入的增量值配对。
- 对每对值进行相加操作,并更新self.data中的累加值。
"""
self.data = [a + float(b) for a, b in zip(self.data, args)]

def reset(self):
"""
重置累加器,将所有变量的累加值重置为0.0。
"""
self.data = [0.0] * len(self.data)

def __getitem__(self, idx):
"""
使累加器支持下标访问操作。

参数:
idx (int): 需要访问的变量的索引。

返回:
float: 指定索引处变量的当前累加值。
"""
return self.data[idx]

该类非常有用于跟踪和累加如模型训练过程中的损失值、准确率计数或任何需要进行累加操作的场景。例如,在处理批量数据时累加损失值,或者统计一个周期内的正确预测数量。通过提供addreset方法,Accumulator类使得这些操作既直观又方便


训练

首先,我们定义一个函数来训练一个迭代周期。 请注意,updater是更新模型参数的常用函数,它接受批量大小作为参数。 它可以是d2l.sgd函数,也可以是框架的内置优化函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def train_epoch_ch3(net, train_iter, loss, updater):  #@save
"""
训练模型一个迭代周期。
参数:
net: 训练的神经网络模型。
train_iter: 训练数据的迭代器。
loss: 损失函数。
updater: 参数更新器,可以是PyTorch的优化器或自定义的更新函数。

返回:
训练过程中的平均损失和平均准确率。
"""
# 如果net是PyTorch的神经网络模型,设置为训练模式。
# 这对于某些特定层如Dropout和BatchNorm层在训练和评估阶段行为不同是必要的。
if isinstance(net, torch.nn.Module):
net.train()

# 使用Accumulator实例来跟踪损失和准确率的累积值,以及处理的总样本数。
metric = Accumulator(3) # 分别对应损失总和、准确度总和、样本数

# 遍历训练数据迭代器
for X, y in train_iter:
# 前向传播:计算预测值
y_hat = net(X)
# 计算损失
l = loss(y_hat, y)

# 如果updater是PyTorch优化器,则使用优化器进行反向传播和参数更新
if isinstance(updater, torch.optim.Optimizer):
updater.zero_grad() # 清除梯度
l.mean().backward() # 反向传播
updater.step() # 更新参数
else:
# 对于自定义更新器,先进行反向传播,然后调用更新函数
l.sum().backward() # 反向传播
updater(X.shape[0]) # 调用更新函数

# 更新累积器数据
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())

# 计算平均损失和平均准确率
return metric[0] / metric[2], metric[1] / metric[2]

这个函数非常重要,因为它封装了训练过程中的核心逻辑,使得对模型的训练变得清晰且易于管理。它支持使用PyTorch的标准优化器,也支持使用自定义的参数更新逻辑,提供了很好的灵活性。通过返回训练过程中的平均损失和平均准确率,这个函数还帮助我们监控模型训练的进度和效果。

在展示训练函数的实现之前,我们定义一个在动画中绘制数据的实用程序类Animator, 它能够简化本书其余部分的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Animator:  #@save
"""在动画中绘制数据"""
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类来可视化训练进度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):  #@save
"""
训练模型。

参数:
net: 要训练的神经网络模型。
train_iter: 训练数据集的迭代器。
test_iter: 测试数据集的迭代器。
loss: 使用的损失函数。
num_epochs: 训练的总周期数。
updater: 参数更新器,可以是PyTorch的优化器或自定义的更新函数。
"""
# 初始化动画生成器,用于可视化训练过程。
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[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)
# 使用动画生成器添加当前周期的训练损失、训练精度和测试精度。
animator.add(epoch + 1, train_metrics + (test_acc,))

# 训练结束后,获取最后一个周期的训练损失和训练精度。
train_loss, train_acc = train_metrics

# 断言语句用于检查训练过程是否达到预期的效果。
# 检查训练损失是否小于0.5。
assert train_loss < 0.5, train_loss
# 检查训练精度是否在合理范围内(大于0.7,小于等于1)。
assert train_acc <= 1 and train_acc > 0.7, train_acc
# 检查测试精度是否在合理范围内(大于0.7,小于等于1)。
assert test_acc <= 1 and test_acc > 0.7, test_acc

作为一个从零开始的实现,使用小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1。

1
2
3
4
lr = 0.1

def updater(batch_size):
return d2l.sgd([W, b], lr, batch_size)

现在,我们训练模型10个迭代周期。 请注意,迭代周期(num_epochs)和学习率(lr)都是可调节的超参数。 通过更改它们的值,我们可以提高模型的分类精度。

1
2
num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)

../_images/output_softmax-regression-scratch_a48321_222_0.svg

预测

现在训练已经完成,我们的模型已经准备好对图像进行分类预测。 给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def predict_ch3(net, test_iter, n=8):  #@save
"""
预测标签并显示图像及其真实标签和预测标签。

参数:
net: 训练好的神经网络模型。
test_iter: 测试数据集的迭代器。
n: 要展示的图像数量,默认为8。
"""
# 从测试数据迭代器中取出一批数据。
# 这里只取第一批数据用于展示。
for X, y in test_iter:
break

# 获取这批数据的真实标签,并转换为对应的文字标签。
trues = d2l.get_fashion_mnist_labels(y)

# 使用模型对这批数据进行预测,获取预测结果的最大值索引作为预测标签,
# 并转换为对应的文字标签。
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))

# 为每张图像准备标题,包含真实标签和预测标签。
titles = [true +'\n' + pred for true, pred in zip(trues, preds)]

# 使用d2l.show_images函数显示图像及其标题。
# 图像被重塑为28x28的大小,只展示前n张图像。
d2l.show_images(
X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])

predict_ch3(net, test_iter)

image-20240220183258116


简洁实现

1
2
3
4
5
6
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随机初始化权重。

1
2
3
4
5
6
7
8
9
# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))

def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);
1
loss = nn.CrossEntropyLoss(reduction='none')

在PyTorch中,nn.CrossEntropyLoss是一个常用的损失函数,特别适用于多类分类问题。这个函数结合了nn.LogSoftmaxnn.NLLLoss(负对数似然损失)两个操作在一个单独的类中

1
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
1
2
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

../_images/output_softmax-regression-concise_75d138_66_0.svg

和以前一样,这个算法使结果收敛到一个相当高的精度,而且这次的代码比之前更精简了。