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() 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())
|
标签转换
在模型中,使用数字标签表示分类更便于运算和存储,而在可视化时,文本标签则更为直观。因此定义一个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) _, 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: ax.imgshow(img) 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)))
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 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)
|
定义softmax模型
首先需要定义softmax操作,即式(1.2)的代码实现。
1 2 3 4
| def softmax(o): o_exp =torch.exp(o) partition = o_exp.sum(1, keepdim=True) return o_exp / partition
|
在上述代码中,对于任何随机输入,我们将所有元素转变为非负数,并且每行总和为1。
有了softmax函数,我们就可以定义模型了:
1 2
| def net(X): return softmax(torch.matmul(X.reshape(-1, w.shape[0]), w)+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_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.eval() metric = Accumulator(2) 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): 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, 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) for X, y in train_iter: y_hat = net(X) l = loss(y_hat, y) if isinstance(updater, torch.optim.Optimizer): 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): 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回归,我们可以训练多分类的模型,而线性回归则用于数量的预测。