PyTorch-softmax回归的从零开始实现

回归可以用于预测多少的问题。 比如预测房屋被售出价格,或者棒球队可能获得的胜场数,又或者患者住院的天数。

事实上,我们也对分类问题感兴趣:不是问“多少”,而是问“哪一个”:

  • 某个电子邮件是否属于垃圾邮件文件夹?
  • 某个用户可能注册不注册订阅服务?
  • 某个图像描绘的是驴、狗、猫、还是鸡?
  • 某人接下来最有可能看哪部电影?

通常,机器学习实践者用分类这个词来描述两个有微妙差别的问题: 1. 我们只对样本的“硬性”类别感兴趣,即属于哪个类别; 2. 我们希望得到“软性”类别,即得到属于每个类别的概率。 这两者的界限往往很模糊。其中的一个原因是:即使我们只关心硬类别,我们仍然使用软类别的模型。

所以我们引入了softmax回归。

0. 准备

导包:

1
2
3
import torch
from IPython import display
from d2l import torch as d2l

引入Fashion-MINIST数据集:

1
2
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

关于d2l包中内置的load_data_fashion_mnist函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def load_data_fashion_mnist(batch_size, resize=None):
"""Download the Fashion-MNIST dataset and then load it into memory."""
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. 初始化模型参数

这里的每个样本都将用固定长度的向量表示。 原始数据集中的每个样本都是28×2828 \times 28的图像。 在本节中,我们将展平每个图像,把它们看作长度为784的向量。 我们暂时只把每个像素位置看作一个特征。

回想一下,在softmax回归中,我们的输出与类别一样多。 因为我们的数据集有10个类别,所以网络输出维度为10。 因此,权重将构成一个784×10784 \times 10的矩阵, 偏置将构成一个1×101 \times 10的行向量。 与线性回归一样,我们将使用正态分布初始化我们的权重W,偏置初始化为0。

1
2
3
4
5
num_inputs = 784
num_outputs = 10

W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)

2. 定义softmax操作

softmax表达式:

softmax(X)ij=exp(Xij)kexp(Xik)\operatorname{softmax}(\mathbf{X})_{i j}=\frac{\exp \left(\mathbf{X}_{i j}\right)}{\sum_{k} \exp \left(\mathbf{X}_{i k}\right)}

实现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 # 这里应用了广播机制

3. 定义模型

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

O=XW+bY^=softmax(O)\begin{array}{l} \mathbf{O}=\mathbf{X W}+\mathbf{b} \\ \hat{\mathbf{Y}}=\operatorname{softmax}(\mathbf{O}) \end{array}

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

1
2
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)

4. 定义损失函数

我们引入交叉熵损失函数

softmax函数给出了一个向量y^\hat{\mathbf{y}}, 我们可以将其视为“对给定任意输入x\mathbf{x}的每个类的条件概率”。 例如y^1=P(y= 猫 x)\hat{y}_{1}=P(y=\text { 猫 } \mid \mathbf{x})。 假设整个数据集X,Y{\mathbf{X},\mathbf{Y}}具有nn个样本, 其中索引ii的样本由特征向量x(i)\mathbf{x}^{(i)}和独热标签向量y(i)\mathbf{y}^{(i)}组成。 我们可以将估计值与实际值进行比较:

P(YX)=i=1nP(y(i)x(i))P(\mathbf{Y} \mid \mathbf{X})=\prod_{i=1}^{n} P\left(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}\right)

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

logP(YX)=i=1nlogP(y(i)x(i))=i=1nl(y(i),y^(i))-\log P(\mathbf{Y} \mid \mathbf{X})=\sum_{i=1}^{n}-\log P\left(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}\right)=\sum_{i=1}^{n} l\left(\mathbf{y}^{(i)}, \hat{\mathbf{y}}^{(i)}\right)

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

l(y,y^)=j=1qyjlogy^jl(\mathbf{y}, \hat{\mathbf{y}})=-\sum_{j=1}^{q} y_{j} \log \hat{y}_{j}

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

5. 分类精度

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

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

1
2
3
4
5
6
7
8
def accuracy(y_hat, y):  #@save
"""计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)
cmp = y_hat.type(y.dtype) == y
return float(cmp.type(y.dtype).sum())

accuracy(y_hat, y) / len(y)

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

1
2
3
4
5
6
7
8
9
def evaluate_accuracy(net, data_iter):  #@save
"""计算在指定数据集上模型的精度"""
if isinstance(net, torch.nn.Module):
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # 正确预测数、预测总数
with torch.no_grad():
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]

参考

参考教程:DIVE INTO DEEP LEARNING