深度学习丨softmax回归

softmax回归

softmax回归用于分类问题,根据样本特征估计各类的概率,并选取概率最大的分类作为预测结果。

网络架构

softmax模型可表示为: 其中o为n维列向量,其元素分别与n种类别对应,其值代表对应类别的未规范化预测,值越大则概率越大。

W为n×m矩阵,表示每一种特征在各个类别中的权重。

x为m维列向量,表示特征。

b为n维列向量,表示偏置。

由于每个输出取决于所有输入,故softmax回归的输出层为全连接层

softmax运算

在上述模型中,o的值不一定满足概率的非负性和规范性,为了使输出能表示概率,我们需要对模型进行校准。

对输出做以下运算: 经过运算,得到的 既没有改变对应 的大小顺序,同时也满足概率的性质。因此有: 不会改变我们的预测结果。

损失函数

交叉熵常用来衡量两个概率的区别,我们用交叉熵损失作为softmax的损失函数: 在该式中,表示类别为j的真实概率,当且仅当真实类别为j时,为1,否则为0。因此,又有,其中y为真实类别。

softmax的实现

下面我们采用Fashion-MNIST数据集训练一个softmax模型,并使用模型对图像进行分类预测。

图像分类数据集

数据集获取

定义一个load_data_fashion_mnist函数来下载训练数据集和测试数据集,并将它们转化为tensor格式,装入小批量样本迭代器中。

1
2
3
4
5
6
7
8
9
def load_data_fashion_mnist(batch_size, resize=None):
trans = transforms.ToTensor()#输入的图片将转化为tensor格式
if resize:#调整图片尺寸
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)#将ToTensor和Resize(如有)封装在一起
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())#用DataLoader迭代获取小批量样本

标签转换

在模型中,使用数字标签表示分类更便于运算和存储,而在可视化时,文本标签则更为直观。因此定义一个get_fashion_mnist_labels函数,用于将数字标签转换为文本标签。

1
2
3
def get_fashion_mnist_labels(labels):
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat', 'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]

图像可视化

定义show_images函数来可视化图像,可更加直观观测预测情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
figsize = (num_cols*scale, num_rows*scale)#图表大小
#创建包含rowsXcols个子图的图表,图表大小为figsize,axes是一个包含所有子图轴的数组
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
axes = axes.flatten()#将二维数组转化为一维数组
#以子图轴为基准通过循环绘制数据(共循环num_cols*num_rows次),img的size为[28,28]
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
#如果图像数据为张量,转化为NumPy数组并显示
ax.imshow(img.numpy())
else:
#图像数据不是张量则直接显示
ax.imgshow(img)
#隐藏x和y轴标签
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
#如果提供了标题则设置子图标题
if titles:
ax.set_title(titles[i])
return axes

下面先通过迭代器获取18个样本来展示该函数的运行效果:

1
2
3
4
X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))
#图像排列为2行9列
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y))
d2l.plt.show()

可得到如下图像列表:

图像展示

模型实现

初始化模型参数

将每个样本用固定长度的向量表示,数据集中的图片为28*28像素的图像,将之展平成为长度为784的向量(将每个像素位置看作一个特征)。

1
2
3
4
num_inputs = 784#图片尺寸为28*28,将之展平为一维长度为784
num_outputs = 10#共10个类别,故输出维度为10
w = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)

定义softmax模型

首先需要定义softmax操作,即式(1.2)的代码实现。

1
2
3
4
def softmax(o):#定义softmax操作
o_exp =torch.exp(o)
partition = o_exp.sum(1, keepdim=True)#o_exp按行求和变为列向量
return o_exp / partition#这里利用了广播机制

在上述代码中,对于任何随机输入,我们将所有元素转变为非负数,并且每行总和为1。

有了softmax函数,我们就可以定义模型了:

1
2
def net(X):#定义模型
return softmax(torch.matmul(X.reshape(-1, w.shape[0]), w)+b)#即softmax(Wx+b)

定义损失函数

如前所述,我们用交叉熵损失函数来评估softmax模型损失:

1
2
def cross_entropy(y_hat ,y):#定义交叉熵损失函数
return -torch.log(y_hat[range(len(y_hat)), y])

其中y是样本的真实类别,则是每个类别的概率,用y作为的索引即可找到真实类别的预测概率,再依次取log和相反数即可得到交叉熵损失。

分类精度

分类精度即预测的正确率。如果是一个多列矩阵的话,则从每列中选取元素最大的索引(即概率最大的类别)作为预测值,并将预测值与真实类别进行比较,相等即为预测正确。以下函数的返回值为预测正确的数量。

1
2
3
4
5
def accuracy(y_hat, y):#计算预测正确的数量
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:#y为矩阵且列数>1
y_hat = y_hat.argmax(axis=1)
cmp = y_hat.type(y.dtype) == y#数据类型一致方可比较
return float(cmp.type(y.dtype).sum())

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

1
2
3
4
5
6
7
def evaluate_accuracy(net, data_iter):
if isinstance(net, torch.nn.Module):#判断net是否为torch.nn.Module类的实例
net.eval()#切换为评估模式
metric = Accumulator(2)#metric[0]累积正确数,metric[1]累积总数
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel())#累加
return metric[0]/metric[1]

这里用到了一个累加器Accumulator,同于对多个变量进行累加,具体定义如下:

1
2
3
4
5
6
7
8
9
class Accumulator:#累加器
def __init__(self, n):#初始化,创建一个包含n个元素,初始值为0.0的列表
self.data = [0.0] * n
def add(self, *args):#使列表中每个元素分别加上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, item):#索引
return self.data[item]

训练与预测

训练

首先,我们定义一个函数来进行一轮的训练,具体流程与线性回归相似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def train_epoch_ch3(net, train_iter, loss, updater):#训练模型一个迭代周期
if isinstance(net, torch.nn.Module):
net.train()#设置为训练模式
metric = Accumulator(3)#metric[0]记录损失,metric[1]记录正确数,metric[2]记录总数
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.backward()
updater.step()#做出一步优化,更新参数
metric.add(float(1)*len(y), accuracy(y_hat, y), y.size().numel())
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]#返回训练损失和训练精度

在以上代码中,updater是更新模型参数的常用函数,既可以是框架的内置优化函数,也可以是定制的优化器。我们这里采用小批量随机梯度下降进行优化,学习率设为0.1:

1
2
3
lr = 0.1#学习率
def updater(batch_size):#采用小批量随机梯度下降
return d2l.sgd([w,b], lr, batch_size)

接下来我们实现一个训练函数来实现多轮的训练。在每一轮训练中,我们都输出模型在测试集上的分类精度以及在训练集上的分类精度和损失来对模型进行评估。

1
2
3
4
5
6
7
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
for epoch in range(num_epochs):#迭代 num_epochs 次
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)#测试集精确度
print(f'epoch{epoch}_test:{test_acc}')
train_loss, train_acc = train_metrics # 训练损失和精确度
print(f'epoch{epoch}_train:loss={train_loss},acc={ train_acc}')

进行10轮训练:

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

得到输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
epoch0_test:0.7918
epoch0_train:loss=0.7882061706542969,acc=0.7462333333333333
epoch1_test:0.7963
epoch1_train:loss=0.5694080658594767,acc=0.8128833333333333
epoch2_test:0.8085
epoch2_train:loss=0.5254165397644043,acc=0.8261666666666667
epoch3_test:0.8157
epoch3_train:loss=0.5017157739003499,acc=0.8335
epoch4_test:0.8153
epoch4_train:loss=0.48570796445210773,acc=0.8378333333333333
epoch5_test:0.8239
epoch5_train:loss=0.47390937894185386,acc=0.8411833333333333
epoch6_test:0.8324
epoch6_train:loss=0.46491336631774904,acc=0.8436166666666667
epoch7_test:0.8273
epoch7_train:loss=0.4578305866241455,acc=0.8444666666666667
epoch8_test:0.8043
epoch8_train:loss=0.4522740385055542,acc=0.846
epoch9_test:0.8326
epoch9_train:loss=0.4478646834055583,acc=0.8478166666666667

可以看出随着轮次增加,分类精度总体呈上升趋势,损失呈下降趋势。

预测

训练完成后,我们使用模型对图像进行分类预测。给定几张图片,输出其实际标签(标题第一行)和模型预测(标题第二行)。

1
2
3
4
5
6
7
8
9
10
def predict_ch3(net, test_iter, n=6):
for X, y in test_iter:
break#获取一组样本
trues = get_fashion_mnist_labels(y)#正确标签
preds = 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], scale=4)
d2l.plt.show()
predict_ch3(net, test_iter)
预测结果

对于6张图像预测均正确。

总结

softmax回归的训练与线性回归十分相似,总体流程为:读取数据、定义模型和损失函数、使用优化算法训练模型。

借助softmax回归,我们可以训练多分类的模型,而线性回归则用于数量的预测。