动手学习深度学习(4)Softmax回归

茴香豆 Lv5

就像我们从零开始实现线性回归一样,我们认为softmax回归也是重要的基础,因此也应该知道实现softmax回归的细节。

1. 回归和分类

  • 回归估计一个练习值

  • 分类预测一个离散类别

Softmax回归是一个多类分类模型,使用Softmax操作子得到每个类的预测置信度,使用交叉熵来衡量预测和标号的区别。

实现Softmax由三个步骤组成:

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

softmax(X)_{ij} = \frac{exp(X_{ij})}{ {\textstyle \sum_{k}^{}}exp(X_{ik}) }

分母或规范化常数,有时也称为配分函数(其对数称为对数-配分函数)。

2. 常见损失函数

L2 Loss

l(y,{y}') = 1/ 2 (y - {y}')^{2}

梯度变化:离原点越远,梯度值越大;离原点越近,梯度值越小。

L1 Loss

l(y,{y}') = |y - {y}'|

梯度变化:大于零,梯度为常数1;小于零,梯度为常数-1;等于零,存在梯度不平滑性,会使优化末期梯度变化不稳定。

Huber’s Robust Loss

l(y,{y}') = \begin{cases} |y-{y}'|-1/2 & \text{ if |y-{y}'|>1} \\ 1/ 2 (y - {y}')^{2} & \text{ otherwise } \end{cases}

梯度变化:在(-1, 1)梯度变化符合L2 Loss的梯度变化;在其他区间符合L1 Loss的梯度变化。

3. Softmax实现

首先我们先了解一下如何读取多类分类数据集。

图像分类数据集

MNIST数据集是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。我们将使用类似但更复杂的Fashion-MNIST数据集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
%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() # 使用svg显示图片

# 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式
# 并除以255使得所有像素的数值均在0到1之间
trans = transform.ToTensor()
mnist_train = torchvision.datasets.FasionMNIST(
root="../data", train=True, transform=trans, dawnload=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, dawnload=True)
len(mnist_train), len(mnist_test)

两个可视化数据集的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def get_fashion_mnist_labels(labels):  
"""返回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]

def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
"""绘制图像列表"""
figsize = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
# 图片张量
ax.imshow(img.numpy())
else:
# PIL图片
ax.imshow(img)
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
if titles:
ax.set_title(titles[i])
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));

读取一小批量数据,大小为batch_size

1
2
3
4
5
6
7
batch_size = 256
def get_dataloader_workers():
"""使用4个进程来读取数据"""
return 4

train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers())

最后整合所有组件,定义一个函数,用于获取和读取Fashion-MNIST数据集

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()))

从零开始实现Softmax

就像从零开始线性回归一样,你应该知道Softmax的每一个细节。

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

展开每个图像,将他们视为长度为784的向量。因为我们的数据集有10个类别,所以网络输出维度为10。因此权重将构成一个784x10的矩阵,偏置构成一个1x10的行向量。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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)# 偏置初始化为0

# 定义softmax操作
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 这里使用了广播机制
# 定义softmax模型
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
# 定义交叉熵损失函数
def cross_entropy(y_hat, y):
return -torch.log(y_hat[range(len(y_hat)), y])
# 将预测类别与真实y元素进行比较
def accuracy(y_hat, y):
'''计算预测正确的数量'''
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())
# 评估在任意模型net的精度
def evaluate_accuracy(net, data_iter):
'''计算在指定数据集上模型的精度'''
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.munel())
return metric[0] / metric[1]
# Accumulator实例中创建了2个变量,分别用于存储正确预测的数量和预测的总数量
class Accumulator:
'''在n个变量上累加'''
def __init__(self, n):
self.data = [0.0] * n
def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0.0] * len(self.data)
def __getitem__(self, idx):
return self.data[idx]

#softmax回归的训练
def train_epoch_ch3(net, train_iter, loss, updater):
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
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):
# 使用PyTorch内置的优化器和损失函数
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]

# 定义一个在动画中绘制数据的实用程序类
class Animator:
"""在动画中绘制数据"""
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)

# 训练函数
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
"""训练模型(定义见第3章)"""
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
assert train_loss < 0.5, train_loss
assert train_acc <= 1 and train_acc > 0.7, train_acc
assert test_acc <= 1 and test_acc > 0.7, test_acc
# 小批量随机梯度下降优化模型的损失函数
lr = 0.1
def updater(batch_size):
return d2l.sgd([W, b], lr, batch_size)
# 训练模型10个迭代周期
num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
# 对图像进行分类预测
def predict_ch3(net, test_iter, n=6): #@save
"""预测标签(定义见第3章)"""
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(
X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
predict_ch3(net, test_iter)

Softmax的简洁实现

和上节相同,我们继续使用Fashion-MNIST数据集,并保持批量大小为256。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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回归的输出层是一个全连接层
# 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);
# 在交叉熵损失函数中传递未规范化的预测,并同时计算softmax及其对数
loss = nn.CrossEntropyLoss(reduction='none')
# 使用学习率为0.1的小批量随机梯度下降作为优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
# 使用上一小节实现的train_ch3训练函数来训练模型
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
  • Title: 动手学习深度学习(4)Softmax回归
  • Author: 茴香豆
  • Created at : 2022-09-27 18:44:33
  • Updated at : 2022-10-28 14:49:18
  • Link: https://hxiangdou.github.io/2022/09/27/DL_4/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments