2-softmax回归

2-softmax回归

线性回归模型适用于输出为连续值的情景,在另一类场景中,模型输出可以是像图像类别这样的离散值。对于离散值预测问题,可以使用诸如softmax回归在内的分类模型。本节以softmax回归模型为例,介绍神经网络中的分类模型。

虽然这里题目是softmax回归,但其实是一个分类问题。

softmax回归模型

softmax回归和线性回归一样将输入特征和权重做线性叠加,与线性回归不同之处在于,softmax回归的输出值个数等于标签里的类别数。

如果有四种特征和三种输出动物类别,那么权重包括12个标量(带下标的ww),偏差包含三个标量(带下标的bb),对每个输入计算o1,o2,o3o_1,o_2,o_3三个输出。

o1=x1w11+x2w21+x3w31+x4w41+b1o2=x1w12+x2w22+x3w32+x4w42+b2o1=x1w13+x2w23+x3w33+x4w43+b3o_1 = x_1w_{11} + x_2w_{21} + x_3w_{31} + x_4w_{41} + b_1 \\ o_2 = x_1w_{12} + x_2w_{22} + x_3w_{32} + x_4w_{42} + b_2 \\ o_1 = x_1w_{13} + x_2w_{23} + x_3w_{33} + x_4w_{43} + b_3 \\

使用矩阵相乘有两种表示方法。

  1. 输入特征表示为列向量,则权重矩阵每行表示一个神经元,该层有多少神经元就有多少行,计算式是:wxwx
  2. 输入特征表示为行向量,则权重矩阵每列表示一个神经元,行数表示前一层输出个数,计算式是:xwxw

softmax回归是一个单层神经网络,并且softmax回归的输出层也是一个全连接层。

既然分类问题需要得到离散的预测输出,一个简单办法是将输出值oio_i当作预测类别是ii的置信度,并将值最大的输出对应的类作为预测输出。

argmaxioi\underset{i}{\arg\max}o_i

但是,直接使用输出层输出有两个问题。

  1. 由于输出层的输出值不确定,难以直观判断这些值的意义
  2. 由于真实标签是离散值,这些离散值与不确定范围的输出值之间的误差难以衡量

softmax解决了以上两个问题,它通过下式将输出值变成了值为正且和为1的概率分布。

y^1,y^2,y^3=softmax(o1,o2,o3)\widehat{y}_1,\widehat{y}_2,\widehat{y}_3 = softmax(o_1, o_2, o_3)

其中:

y^1=exp(o1)i=13exp(oi)y^2=exp(o2)i=13exp(oi)y^3=exp(o3)i=13exp(oi)\widehat{y}_1 = \frac{ exp(o_1) }{ \sum_{i=1}^3 exp(o_i) } \\ \widehat{y}_2 = \frac{ exp(o_2) }{ \sum_{i=1}^3 exp(o_i) } \\ \widehat{y}_3 = \frac{ exp(o_3) }{ \sum_{i=1}^3 exp(o_i) } \\

可以看出,y^1+y^2+y^3=1\widehat{y}_1+\widehat{y}_2+\widehat{y}_3 = 1并且0y^1,y^2,y^310 \le \widehat{y}_1,\widehat{y}_2,\widehat{y}_3 \le 1

小批量样本的矢量化

为了提高计算效率,通常会对小批量样本做向量计算。假设有一个批量样本X\bf X,特征维度为dd,样本数量为nn。此外,假设在输出中有qq个类别,那么小批量样本的特征为XRn×d\mathbf{X} \in R^{n\times d},权重为WRd×q\mathbf{W} \in R^{d\times q},偏置为bR1×q\mathbf{b} \in R^{1\times q},softmax回归的计算式为:

O=XW+bY^=softmax(O)\mathbf{ O = XW + b } \\ \mathbf{ \widehat{Y} } = softmax(\mathbf{ O} )

X\bf X中的每行表示一个样本,权重矩阵每一列代表一个神经元。

损失函数

接下来需要一个损失函数来度量预测效果。

softmax运算可以将输出变成一个合法的类别预测分布,实际上,真实标签也可以用类别分布表达,对于样本ii,构造向量yi\bf y^i,使得第y(i)y^{(i)}个元素为1,其余元素为0,这样训练目标可以设为使预测概率分布y^(i)\mathbf{\widehat{y}^{(i)} }更接近真实的标签概率分布y(i)\bf y^{(i)}

可以像线性回归那样使用均方误差损失,但是想要预测分类正确,并不需要预测概率完全等于标签概率。

例如,在图像分类中,有三个类型,如果真实标签y(i)=3y^{(i)} = 3,只需要让预测值y^3(i)\widehat{y}^{(i)}_3大于其他两个预测值即可。如果y^3(i)=0.6\widehat{y}^{(i)}_3 = 0.6,那么无论其他两个值是多少,类别预测都正确。而平方损失过于严格。

因此,交叉熵是一个常用的衡量方法。

H(y(i),y^(i))=j=1qyj(i)logy^j(i)H( \mathbf{ y^{(i)}, \widehat{y}^{(i)} } ) = -\sum_{j=1}^{q} y_{j}^{(i)} \log \widehat{y}^{(i)}_j

其中带下标的yj(i)y_{j}^{(i)}是向量y(i)\bf y^{(i)}中非0即1的元素,需要注意与样本i类别的离散数值,即不带下标的y(i)y^{(i)}区分。

在上式中,我们知道向量y(i)\bf y^{(i)}中只有第y(i)y^{(i)}个元素值为1,其余都为0,因此式子变为:

H(y(i),y^(i))=logy^y(i)(i)H( \mathbf{ y^{(i)}, \widehat{y}^{(i)} } ) = -\log \widehat{y}^{(i)} _ {y^{(i)} }

也就是说,交叉熵只关心对正确类别的预测概率,预测概率值越大,损失值越小,概率为1,则交叉熵值为0。

当然遇到一个样本有多个标签时,例如一个图像中有多个物体,则不能做这一步的简化,但即使对于这种情况,交叉熵同样只关心对图像中出现的物体类别的预测概率。

假设训练数据集样本数为n,交叉熵损失函数定义为:

l(Θ)=1ni=1nH(y(i),y^(i))\mathcal{l (\Theta) } = \frac{1}{n} \begin{matrix} \sum_{i=1}^n \end{matrix} H( \boldsymbol{ y^{(i)}, \widehat{y}^{(i)} } )

其中Θ\Theta代表模型参数,同样,如果每个样本只有一个标签,那么交叉熵损失可以简写成:

l(Θ)=1ni=1nlogy^y(i)(i)\mathcal{l (\Theta) } = -\frac{1}{n} \begin{matrix} \sum_{i=1}^n \log\widehat{y}^{(i)} _{ y^{(i)} } \end{matrix}

softmax及其导数

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

将softmax公式带入到交叉熵损失式子中,得到:

l(y,y^)=j=1qyjlogexp(oj)k=1qexp(ok)=j=1qyjlogk=1qexp(ok)j=1qyjoj=logk=1qexp(ok)j=1qyjoj\begin{aligned} l(\boldsymbol{y, \widehat{y} } ) & = - \sum_{ j=1 }^{ q } y_j \log \frac{ exp(o_j) }{ \sum_{ k=1 }^q exp(o_k) } \\ & = \sum_{j=1}^q y_j \log \sum_{k=1 }^q exp(o_k) - \sum_{ j=1 }^q y_jo_j \\ & = \log \sum_{k=1}^q exp(o_k) - \sum_{j=1}^q y_j o_j \end{aligned}

则关于预测值ojo_j的导数为:

ojl(y,y^)=exp(oj)k=1qexp(ok)yj=softmax(o)jyj\partial_{o_j} l(\boldsymbol{y, \widehat{y} }) = \frac{ exp(o_j) }{ \sum_{k=1}^q exp(o_k) } - y_j = softmax(\mathbf{o})_j - y_j

如果只预测一个类别,则上述式子应变为:

l(y,y^)=logk=1qexp(ok)ojojl(y,y^)=softmax(o)j1l(\boldsymbol{y, \widehat{y} }) = \log \sum_{k=1}^q exp(o_k) - o_j \\ \partial_{o_j} l(\boldsymbol{y, \widehat{y} }) = softmax(\mathbf{o})_j - 1

换句话说,导数是softmax模型分配概率与实际情况(one-hot向量表示)之间的差异。

模型预测和评估

在训练softmax回归模型后,给出任何样本特征,可以预测每个输出类别的概率,通常使用预测概率最高的类别作为输出类别。如果预测与实际类别一致,则预测是正确的。接下来将使用精度(accuracy)来评估模型性能,它等于正确预测数与预测总数之间比率。

图像分类数据集

在介绍softmax回归实现之前先引入一个多分类图像分类数据集,以方便观察比较算法之间在模型精度和计算效率的区别。图像分类数据集中最常见的是手写数字识别数据集MNIST,但过于简单,大部分模型在MNIST的分类精度都超过了95%,因此为了更直观观察算法之间的差异,会使用一个图像内容更复杂的数据集Fashion-MNIST,它是一个衣服类别数据集,包含了十个类别:t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。

训练集包含60000个样本,测试集包含10000个样本。

本节将使用torchvision包,主要用来构建计算机视觉模型。它主要有以下几部分组成:

  1. torchvision.datasets:一些加载数据的函数和常用的数据集接口
  2. torchvision.models:包含常用的模型结构和预训练模型,例如AlexNet,VGG等
  3. torchvision.transforms:常用的图像变换,裁剪、旋转等
  4. torchvision.utils:其他有用的方法

获取数据集

首先导入必要的包和模块。

1
2
3
4
5
6
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import time
import d2lzh_pytorch as d2l

然后,通过torchvision.datasets下载该数据集,第一次调用时会自动从网上获取数据,通过参数train来指定获取训练集或测试集。

另外,还指定了参数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

1
2
mnist_train = torchvision.datasets.FashionMNIST(root='./datasets/fashion_mnist', train=True, download=True, transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root='./datasets/fashion_mnist', train=False, download=True, transform=transforms.ToTensor())

可以通过下标访问样本,例如:

1
2
3
4
5
feature, label = mnist_train[0]
print(feature.shape, label)

# 输出
# torch.Size([1, 28, 28]) 9

变量feature对应高和宽均为28的图像,由于使用了transforms.toToTensor(),所以每个像素值都为[0.0, 1.0]之间的32位浮点数。

需要注意,feature尺寸是(C×H×W),第一维是通道数,因为是灰度图像,所以通道数为1,后面分别是高和宽。

定义函数将数值标签转换成文本标签。

1
2
3
4
def get_fashion_mnist_labels(labels): 
text_labels = ['T恤', '牛仔裤', '套衫', '连衣裙','外套',
'凉鞋', '衬衫', '运动鞋', '包', '短靴']
return [text_labels[int(i)] for i in labels]

查看训练集中前十张图片。

1
2
3
4
5
X, y = [], []
for i in range(10):
X.append(mnist_train[i][0])
y.append(mnist_train[i][1])
d2l.show_fashion_mnist(X, get_fashion_mnist_labels(y))

其中show_fashion_mnist实现代码为:

1
2
3
4
5
6
7
8
9
def show_fashion_mnist(images, labels):
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)

输出为:

output

读取小批量

mnist_traintorch.utils.data.Dataset的子类,所以可以将其传入torch.util.data.DataLoader中来创建一个读取小批量数据样本的DataLoader实例。

在实践中,数据读取经常是训练的性能瓶颈,特别是当模型较简单或硬件性能较差时,PyTorch的DataLoader允许使用多进程来借宿数据读取,使用参数num_workers来设置进程数量。

windows平台使用多进程读取会出错,因此num_workers应设为0。

1
2
3
4
batch_size =256
num_workers = 0
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=True, num_workers = num_workers)

Softmax回归的从零实现

首先导入所需库和模块。

1
2
3
4
5
6
7
8
9
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import time
import numpy as np
import d2lzh_pytorch as d2l
%matplotlib inline
plt.rcParams['font.sans-serif'] = 'SimHei'

Fashion-MNIST数据已经在上节获取到了。

初始化模型参数

使用向量表示每个样本,因为每个样本输入都是高宽均为28像素的图像,因此模型的输入向量长度为28×28=784,该向量的每个元素对应图像中每个像素,由于图像有10个类别,因此输出层输出个数为10,。

因此,softmax回归的权重和偏置参数分别为784×10和1×10的矩阵。

1
2
3
4
5
6
7
8
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_(True)
b.requires_grad_(True)

实现softmax运算

在介绍如何定义回归之前,先讲解下如何对多维Tensor按维度操作。给定一个Tensor矩阵X,可以只对同一列(dim=0)或同一行(dim=1)的元素求和,并在结果中保留行和列这两个维度(keepdim=True)。

1
2
3
4
5
6
7
X = torch.tensor([[1,2,3], [4,5,6]])
print(X.sum(dim=0, keepdim=True))
print(X.sum(dim=1, keepdim=True))
# 输出
# tensor([[5, 7, 9]])
# tensor([[ 6],
# [15]])

X.sum(dim=0, keepdim=True)输出的维度是(1,3),如果keepdim=False,则输出维度是(3,),也就是一维的,不保留列这个维度。

接下来定义softmax运算。在下面函数中,矩阵X行数是样本数,即每一行表示一个样本,列数是输出个数。

1
2
3
4
def softmax(X):
X_exp = X.exp()
partition = X_exp.sum(dim=1, keepdim=True)
return X_exp / partition

例如矩阵X维度为(256, 10),共有256个样本,首先对矩阵所有元素做指数运算,然后按行求和,得到partition,一个维度为(256,1)的向量,最后通过广播机制求得这256个样本的softmax概率分布值。

定义模型

有了softmax运算,就可以定义softmax回归模型。

1
2
def net(X): 
return softmax(torch.mm(X.view(-1, num_inputs), W) + b)

通过view函数将每张图片从28×28的矩阵改成长度为num_inputs的向量,再与权重矩阵相乘。

定义损失函数

上节中,介绍了softmax回归中使用的交叉熵损失函数,为了得到标签的猜测概率,使用gather函数。

首先介绍gather函数,gather函数作用是根据索引查找,然后将查找结果以张量形式返回。

有两个参数:

  • dim:维度,即按哪个维度来进行索引
  • index:索引矩阵

介绍到这,完全不理解gather函数。

举个例子:

1
2
3
4
5
6
7
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = torch.LongTensor([0,1]).view(1, -1)
print(y, y.shape)
y_hat.gather(1, y)
# 输出
# tensor([[0, 1]]) torch.Size([1, 2])
# tensor([[0.1000, 0.3000]])

y_hat矩阵为[0.10.30.60.30.20.5]\begin{bmatrix}0.1 & 0.3 & 0.6 \\ 0.3 & 0.2 & 0.5 \end{bmatrix},索引矩阵y[01]\begin{bmatrix}0 & 1\end{bmatrix},按dim=1,即按行索引。

但索引矩阵只有一行,维度是(1,3),那么就将y_hat矩阵中,第0行的第0个元素和第0行的第1个元素取出,也就是0.1和0.3。

下面分别给出以dim=0dim=1的几个例子。

0为开始索引。

  1. dim=0
    • 索引矩阵为[011]\begin{bmatrix}0 & 1 & 1\end{bmatrix},则将第0列的第0个元素、第1列的第1个元素、第2列的第1个元素取出,得到矩阵[0.10.20.5]\begin{bmatrix} 0.1 & 0.2 & 0.5\end{bmatrix}
    • 索引矩阵为[01]\begin{bmatrix} 0 \\ 1\end{bmatrix},按列索引,但索引矩阵只有一列,因此将第0列的第0、1个元素取出,得到矩阵[0.10.3]\begin{bmatrix} 0.1 \\ 0.3\end{bmatrix}
    • 索引矩阵为[0112]\begin{bmatrix} 0 & 1 \\ 1 & 2\end{bmatrix},按列索引,但索引矩阵第2列的第二个元素是2,表示取y_hat矩阵第2列的第2个元素,但y_hat矩阵第2列只有两个元素,因此索引越界,会出错;
    • 索引矩阵为[0110]\begin{bmatrix} 0 & 1 \\ 1 & 0\end{bmatrix},按列索引,将第0列的第0、1个元素,第1列的1、0个元素取出按索引矩阵排列,得到矩阵[0.10.20.30.3]\begin{bmatrix} 0.1 & 0.2 \\ 0.3 & 0.3\end{bmatrix}
  2. dim=1
    • 索引矩阵为[12]\begin{bmatrix}1 & 2\end{bmatrix},就将y_hat矩阵中,第0行的第1个元素和第0行的第2个元素取出,得到矩阵[0.30.6]\begin{bmatrix} 0.3 & 0.6\end{bmatrix}
    • 索引矩阵为[12]\begin{bmatrix}1 \\ 2\end{bmatrix},索引矩阵有两行,就将y_hat矩阵中,第0行的第一个元素和第1行的第2个元素取出,得到[0.30.5]\begin{bmatrix} 0.3 \\ 0.5\end{bmatrix}
    • 索引矩阵为[0112]\begin{bmatrix}0 & 1 \\ 1 & 2\end{bmatrix},就将y_hat矩阵中,第0行的第0、1个元素,第1行的第1、2个元素取出,得到矩阵[0.10.30.20.5]\begin{bmatrix}0.1 & 0.3 \\ 0.2 & 0.5\end{bmatrix}

简单来说,gather函数根据dim参数来决定按哪个维度进行索引,并根据索引矩阵取出对应的值,gather函数返回值的维度与索引矩阵维度相同。

下面就用gather定义交叉熵损失函数。

1
2
def cross_entropy(y_hat, y): 
return -torch.log(y_hat.gather(1, y.view(-1, 1)))

举个例子,如果有一个样本经过softmax回归得到矩阵:

[0.20.40.050.050.050.050.050.050.050.05]\begin{bmatrix}0.2 & 0.4 & 0.05& 0.05& 0.05& 0.05& 0.05& 0.05& 0.05& 0.05\end{bmatrix},维度为(1, 10)

真实标签为2,则y=[2]y = \begin{bmatrix}2\end{bmatrix}y.view(-1,1)确保有多个样本时,样本标签是一个列向量。

然后dim=1,按行索引,故取输出矩阵第一行(也只有一行)的第2个元素值0.4,再经过log函数,得到损失值。

计算分类准确率

分类准确率即正确预测数量与总预测数量之比。

1
2
def accuracy(y_hat, y): 
return (y_hat.argmax(dim=1) == y).float().mean().item()

举个例子,y_hat矩阵为[0.10.90.70.3]\begin{bmatrix}0.1 & 0.9 \\ 0.7 & 0.3\end{bmatrix}y_hat.argmax(dim=1)表示按行比较,找出每一行最大值对应的索引,得到矩阵[10]\begin{bmatrix}1 \\ 0\end{bmatrix},然后再和y比较,例如y=[00]y= \begin{bmatrix}0 \\ 0\end{bmatrix},则y_hat.argmax(dim=1) == y得到矩阵[01]\begin{bmatrix}0 \\ 1\end{bmatrix},然后将元素值转换成浮点数,再求平均值,(0+1)/2 = 0.5得到准确率。

类似地,可以评价模型net在数据集data_iter的准确率。

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

注意使用的是sum函数,不是mean,因为有多个batch,所以不单独算每个batch的准确率,而是遍历完后,算一个总准确率。

计算每个batch的准确预测个数,求和,n是样本总数,最后返回准确率。

训练模型

训练softmax回归的实现和线性回归实现非常相似,同样使用小批量随机梯度下降优化模型损失函数,迭代次数num_epochs和学习率lr都是可调的超参数。

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
num_epochs = 5
lr = 0.1
def train(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None, optimzer=None):
for epoch in range(1, num_epochs + 1):
train_loss_sum = 0.0
train_acc_sum = 0.0
n = 0
for X, y in train_iter:
y_hat = net(X)
l = loss(y_hat, y).sum()

# 梯度清零
# softmax回归简洁实现会用到优化器
if optimzer is not None:
optimzer.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 optimzer is None:
d2l.sgd(params, lr, batch_size)
else:
# softmax回归简洁实现会用到
optimzer.step()

train_loss_sum += l.item()
# 注意这里是sum()函数
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()

n += y.shape[0]
test_acc = evaluate_accuacy(test_iter, net)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f' % (epoch, train_loss_sum / n, train_acc_sum / n, test_acc))
train(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)

输出为:

1
2
3
4
5
epoch 1, loss 0.7866, train acc 0.749, test acc 0.785
epoch 2, loss 0.5701, train acc 0.813, test acc 0.811
epoch 3, loss 0.5268, train acc 0.824, test acc 0.820
epoch 4, loss 0.5005, train acc 0.832, test acc 0.824
epoch 5, loss 0.4856, train acc 0.836, test acc 0.825

预测

训练完成后,就可以对图像进行预测。给定一系列图像,比较真实标签和模型预测结果。

1
2
3
4
5
6
X, y = iter(test_iter).next()

true_labels = get_fashion_mnist_labels(y.numpy())
pred_labels = 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])

只显示前9张图像的预测结果。第一行是真实标签,第二行是预测标签。

输出为:

image-20221122190544423

softmax回归的简洁实现

使用PyTorch实现softmax回归,首先导入所需包和模块。

d2lzh_pytorch库封装了之前一些诸如加载图像、评估准确率等函数,这些函数在 从零实现 中全都实现过了。

在后面学习中,还会用到这个库。

1
2
3
4
5
import torch
from torch import nn
from torch.nn import init
import numpy as np
import d2lzh_pytorch as d2l

获取数据

跟上一节代码基本一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, root='./datasets/fashion_mnist/')

# 其中d2l库中的加载图像函数
def load_data_fashion_mnist(batch_size, resize=None, root):
"""Download the fashion mnist dataset and then load into memory."""
trans = []
if resize:
trans.append(torchvision.transforms.Resize(size=resize))
trans.append(torchvision.transforms.ToTensor())

transform = torchvision.transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=True, transform=transform)
mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=True, transform=transform)
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

定义和初始化模型

softmax回归输出层是一个全连接层,因此只用一个线性模块就可以。

每个batch样本X的形状是(batch_size, 1, 28, 28),所以需要先用view()函数将形状转换成(batch_size, 784)才能送入全连接层。

1
2
3
4
5
6
7
8
9
10
11
12
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):
y = self.linear(X.view(X.shape[0], -1))
return y

net = LinearNet(num_inputs, num_outputs)

X形状转换的功能定义为一个类FlattenLayer,并记录在d2l中。

1
2
3
4
5
class FlattenLayer(nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, X):
return X.view(X.shape[0], -1)

那么就可以这样来定义模型:

1
2
3
4
5
6
7
8
9
10
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的正态分布随机初始化模型的权重参数。

1
2
init.normal_(net.linear.weight, mean=0, std=0.01)
init.constant_(net.linear.bias, val=0)

softmax和交叉熵损失函数

上一节中,分别定义softmax运算和交叉熵损失函数可能会造成数值不稳定。PyTorch提供了一个包括softmax运算和交叉熵损失计算的函数,数值稳定性更好。

1
loss = nn.CrossEntropyLoss()

其中CrossEntropyLoss结合了LogSoftmaxNLLLoss

  • LogSoftmax运算:softmax运算后,取log,例如矩阵[123]\begin{bmatrix}1 & 2 & 3\end{bmatrix},二维张量

    1
    2
    3
    4
    5
    6
    logSoftmax = nn.LogSoftmax(dim=0)
    input = torch.tensor([[1,2,3]], dtype=torch.float32)
    output = logSoftmax(input)
    print(output)
    # 输出为:
    # tensor([[0., 0., 0.]])

dim=0,因此softmax按列计算,每一列只有一个元素,因此softmax运算得到矩阵[1,1,1],取log得0。

dim属性改为1,则得到输出tensor([[-2.4076, -1.4076, -0.4076]])

  • NLLLoss:简单来说,这个损失函数将预测标签中对应真实标签中的值取出,加个负号

    输入为(1, category)的情况:即只有单个样本的预测标签。

    1
    2
    3
    4
    5
    6
    7
    nllloss = nn.NLLLoss()
    pred = torch.tensor([[0.2, 0.5, 0.3]], dtype=torch.float32)
    label = torch.tensor([1], dtype=torch.long)
    output = nllloss(pred, label)
    print(output)
    # 输出为:
    # tensor(-0.5000)

    将pred张量中第1个(索引为1,顺序为2)元素取出,加个负号,得到-0.5。

    输入为(n, category),输入多个样本的预测标签。

    1
    2
    3
    4
    5
    6
    7
    nllloss = nn.NLLLoss()
    pred = torch.tensor([[0.1,0.3,0.6], [0.4,0.5,0.1]], dtype=torch.float32)
    label = torch.tensor([0, 1], dtype=torch.long)
    output = nllloss(pred, label)
    print(output)
    # 输出为:
    # tensor(-0.3000)

    第1个样本预测概率为[0.1, 0.3, 0.6],对应真实标签为0,将第0个位置值取出加负号,为-0.1

    第2个样本预测概率为[0.4, 0.5, 0.1],对应真实标签为1,将第1个位置值取出加负号,为-0.5

    然后取平均值,得到-0.3。

    可以添加属性nn.NLLLoss(reduction='sum'),这样返回的是多个样本预测概率相反数的和。

因此,CrossEntropyLoss相当于softmax + log + NLLLoss

定义优化算法

使用学习率为0.1的小批量随机梯度下降作为优化算法。

1
optimizer = torch.optim.SGD(net.parameters(), lr=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
26
27
28
def train(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:
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))

d2l中函数名为train_ch3

进行训练:

1
2
num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

输出为:

1
2
3
4
5
epoch 1, loss 0.0031, train acc 0.748, test acc 0.790
epoch 2, loss 0.0022, train acc 0.812, test acc 0.809
epoch 3, loss 0.0021, train acc 0.826, test acc 0.810
epoch 4, loss 0.0020, train acc 0.832, test acc 0.812
epoch 5, loss 0.0019, train acc 0.837, test acc 0.825

2-softmax回归
https://zhaoquaner.github.io/2022/11/23/DeepLearning/practice/2-softmax回归/
更新于
2022年11月23日
许可协议