3.0 softmax回归
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 | %matplotlib inline |
通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。
读取数据集
1 | # 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式, |
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 | def get_fashion_mnist_labels(labels): #@save |
现在可以创建一个函数来可视化这些样本。
1 | def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save |
以下是训练数据集中前几个样本的图像及其相应的标签。
1 | X, y = next(iter(data.DataLoader(mnist_train, batch_size=18))) |
X.reshape(18, 28, 28)
: 将获取的图像数据X
重塑成18个28x28大小的图像。DataLoader
返回的图像数据默认是扁平化的或有不同的维度,所以需要将其重塑回原始图像的尺寸以便于显示。2, 9
: 指定在显示图像时使用2行9列的网格。因为一共有18个样本,这样的布局可以确保所有样本都能被显示出来。
小批量读取
为了使我们在读取训练集和测试集时更容易,我们使用内置的数据迭代器,而不是从零开始创建。 回顾一下,在每次迭代中,数据加载器每次都会读取一小批量数据,大小为batch_size
。 通过内置数据迭代器,我们可以随机打乱了所有样本,从而无偏见地读取小批量。
1 | batch_size = 256 |
读取训练数据所需的时间:
1 | timer = d2l.Timer() |
3.25sec左右
整合
定义load_data_fashion_mnist
函数,用于获取和读取Fashion-MNIST数据集。 这个函数返回训练集和验证集的数据迭代器。 此外,这个函数还接受一个可选参数resize
,用来将图像大小调整为另一种形状。
1 | def load_data_fashion_mnist(batch_size, resize=None): #@save |
从零实现
1 | import torch |
初始化模型参数
原始数据集中的每个样本都是28×28的图像。 本节将展平每个图像,把它们看作长度为784的向量
在softmax回归中,我们的输出与类别一样多。 因为我们的数据集有10个类别,所以网络输出维度为10。 因此,权重将构成一个784×10的矩阵, 偏置将构成一个1×10的行向量。 与线性回归一样,我们将使用正态分布初始化我们的权重W
,偏置初始化为0。
1 | # 定义输入向量的维度,对于FashionMNIST数据集,每张图像是28x28像素, |
定义softmax操作
实现softmax由三个步骤组成:
- 对每个项求幂(使用
exp
); - 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;
- 将每一行除以其规范化常数,确保结果的和为1。
在查看代码之前,我们回顾一下这个表达式:
1 | def softmax(X): |
参数keepdim=True
意味着在求和后保持原有的维度,不会降维。即按行求和
定义模型
定义softmax操作后,我们可以实现softmax回归模型。 下面的代码定义了输入如何通过网络映射到输出。 注意,将数据传递到模型之前,我们使用reshape
函数将每张原始图像展平为向量。
1 | def net(X): |
X.reshape((-1, W.shape[0]))
将输入X
重塑,以便于与权重矩阵W
进行矩阵乘法。这里假设X
的每一行是一个独立的样本。torch.matmul(...)
执行矩阵乘法。+ b
将偏置项b
加到矩阵乘法的结果上。softmax(...)
将线性模型的输出通过softmax函数转换成概率分布。
定义损失函数
先介绍一下pytorch中张量索引的特性:
1 | y = torch.tensor([0, 2]) |
这相当于是y_hat[[0,1],[0,2]]
,前一个框内是元素的行索引,后一个框是元素的列索引,最终会输出tensor([0.1000,0.5000])
1 | def 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 | def accuracy(y_hat, y): #@save |
argmax
会缩减y_hat
的维度,让其只保留最大概率的索引类别,这里的.type(y.dtype)
是为了避免发生类型错误,把元素转换为和y
一样的类型
同样,对于任意数据迭代器data_iter
可访问的数据集, 我们可以评估在任意模型net
的精度
1 | def evaluate_accuracy(net, data_iter): #@save |
这里定义一个实用程序类Accumulator
,用于对多个变量进行累加。 在上面的evaluate_accuracy
函数中, 我们在Accumulator
实例中创建了2个变量, 分别用于存储正确预测的数量和预测的总数量。 当我们遍历数据集时,两者都将随着时间的推移而累加
1 | class Accumulator: #@save |
该类非常有用于跟踪和累加如模型训练过程中的损失值、准确率计数或任何需要进行累加操作的场景。例如,在处理批量数据时累加损失值,或者统计一个周期内的正确预测数量。通过提供add
和reset
方法,Accumulator
类使得这些操作既直观又方便
训练
首先,我们定义一个函数来训练一个迭代周期。 请注意,updater
是更新模型参数的常用函数,它接受批量大小作为参数。 它可以是d2l.sgd
函数,也可以是框架的内置优化函数。
1 | def train_epoch_ch3(net, train_iter, loss, updater): #@save |
这个函数非常重要,因为它封装了训练过程中的核心逻辑,使得对模型的训练变得清晰且易于管理。它支持使用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 | def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save |
作为一个从零开始的实现,使用小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1。
1 | lr = 0.1 |
现在,我们训练模型10个迭代周期。 请注意,迭代周期(num_epochs
)和学习率(lr
)都是可调节的超参数。 通过更改它们的值,我们可以提高模型的分类精度。
1 | num_epochs = 10 |
预测
现在训练已经完成,我们的模型已经准备好对图像进行分类预测。 给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。
1 | def predict_ch3(net, test_iter, n=8): #@save |
简洁实现
1 | import torch |
softmax回归的输出层是一个全连接层。 因此,为了实现我们的模型, 我们只需在Sequential
中添加一个带有10个输出的全连接层。 同样,在这里Sequential
并不是必要的, 但它是实现深度模型的基础。 我们仍然以均值0和标准差0.01随机初始化权重。
1 | # PyTorch不会隐式地调整输入的形状。因此, |
1 | loss = nn.CrossEntropyLoss(reduction='none') |
在PyTorch中,nn.CrossEntropyLoss
是一个常用的损失函数,特别适用于多类分类问题。这个函数结合了nn.LogSoftmax
和nn.NLLLoss
(负对数似然损失)两个操作在一个单独的类中
1 | trainer = torch.optim.SGD(net.parameters(), lr=0.1) |
1 | num_epochs = 10 |
和以前一样,这个算法使结果收敛到一个相当高的精度,而且这次的代码比之前更精简了。