- 生成式人工智能(基于PyTorch实现)
- (美)刘焕良
- 2734字
- 2025-06-19 18:35:04
2.4 多类别分类
本节将在PyTorch中构建一个深度神经网络,借此将服装归入10个类别之一。然后,我们将使用Fashion MNIST数据集训练模型。最后,使用训练好的模型进行预测,看看预测的准确性如何。首先创建一个验证集,并定义一个早停止类,以便确定何时停止训练。
2.4.1 验证集和早停止
在构建和训练深度神经网络时,有很多超参数可供选择(如学习率和训练轮次数)。这些超参数会影响模型性能。为了找到最佳超参数,我们可以创建一个验证集来测试模型在不同超参数下的性能。
举例来说,我们将在多类别分类中创建一个验证集,以确定最佳的训练轮次数。之所以在验证集而非训练集中进行该过程,是为了避免过拟合(overfitting),即模型在训练集中表现良好,但在样本外测试(未见过的数据)中表现不佳。
下面我们将60000个观测值拆分为训练集和验证集:
train_set,val_set=torch.utils.data.random_split(\ train_set,[50000,10000])
原始训练集现在变成了两个数据集:包含50000个观测值的新训练集和包含剩余10000个观测值的验证集。
使用PyTorch utils包中的DataLoader类将训练集、验证集和测试集分批转换成3个数据迭代器,如下所示:
train_loader=torch.utils.data.DataLoader( train_set, batch_size=batch_size, shuffle=True) val_loader=torch.utils.data.DataLoader( val_set, batch_size=batch_size, shuffle=True) test_loader=torch.utils.data.DataLoader( test_set, batch_size=batch_size, shuffle=True)
接下来,定义EarlyStop()类并创建该类的实例,代码如清单2.8所示。
清单2.8 用于确定何时停止训练的EarlyStop()类
class EarlyStop: def __init__(self, patience=10): ➊ self.patience = patience self.steps = 0 self.min_loss = float('inf') def stop(self, val_loss): ➋ if val_loss < self.min_loss: ➌ self.min_loss = val_loss self.steps = 0 elif val_loss >= self.min_loss: ➍ self.steps += 1 if self.steps >= self.patience: return True else: return False stopper=EarlyStop()
➊ #将patience的默认值设置为10
➋ #定义stop()方法
➌ #如果达到一个新的最小损失,则更新min_loss的值
➍ #统计自上一次达到最小损失以来的轮次数
EarlyStop()类确定验证集的损失是否在最后的10个(patience=10)轮次中不再改善。这里将patience参数的默认值设为10,但读者可以在实例化该类时选择不同值。patience值决定了自模型上一次达到最小损失后,我们希望训练多少个轮次。stop()方法记录最小损失和自达到最小损失以来的轮次数,并将该数值与patience值进行比较。如果自达到最小损失以来的轮次数大于patience值,该方法返回True。
2.4.2 构建并训练多类别分类模型
Fashion MNIST数据集包含10个不同类别的服装,因此要构建一个多类别分类模型来对其进行分类。接下来,我们将构建并训练这样一个模型,还将使用训练好的模型进行预测,并评估预测的准确性。我们将使用PyTorch构建用于多类别分类的神经网络,代码如清单2.9所示。
清单2.9 构建多类别分类模型
model=nn.Sequential( nn.Linear(28*28,256), nn.ReLU(), nn.Linear(256,128), nn.ReLU(), nn.Linear(128,64), nn.ReLU(), nn.Linear(64,10) ➊ ).to(device) ➋
➊ #输出层包含10个神经元
➋ #对输出应用softmax激活函数
与在2.3节中构建的二分类模型相比,这里要进行一些改动。首先,输出现在包含10个值,表示数据集中10种不同类别的服装;其次,最后一个隐藏层的神经元数量从32个改为64个。构建深度神经网络的经验法则是:从上一层到下一层,逐渐增加或减少神经元数量。由于输出神经元的数量从1(二分类)增加到10(多类别分类),因此将第二层到最后一层的神经元数量从32改为64,以匹配增加的数量。不过“64”这个数字也没什么特殊之处:如果在倒数第二层使用100个神经元,也会得到类似结果。
我们将使用PyTorch的nn.CrossEntropyLoss()类作为损失函数,它可以将nn.LogSoftmax()
和nn.NLLLoss()合并到一个类中。PyTorch官方文档特别指出:“这一准则会计算输入logits与目标之间的交叉熵损失。”这就解释了为什么接下来的清单中没有应用softmax激活函数。如果在模型中使用nn.LogSoftmax()并使用nn.NLLLoss()作为损失函数,将得到完全相同的结果。
因此,nn.CrossEntropyLoss()类会对输出应用softmax激活函数,在对数运算之前将10个数字压缩到[0, 1]。在二分类中,为输出应用的激活函数首选为sigmoid;但在多类别分类中,应首选使用softmax激活函数。此外,经过softmax激活函数得到的10个数字相加的总和应该为1,这可以解释为对应于10种服装的概率。我们将使用与2.3节中二分类相同的学习率和优化器。
lr=0.001 optimizer=torch.optim.Adam(model.parameters(),lr=lr) loss_fn=nn.CrossEntropyLoss()
有关train_epoch()的定义如下所示:
def train_epoch(): tloss=0 for n,(imgs,labels) in enumerate(train_loader): imgs=imgs.reshape(-1,28*28).to(device) labels=labels.reshape(-1,).to(device) preds=model(imgs) loss=loss_fn(preds,labels) optimizer.zero_grad() loss.backward() optimizer.step() tloss+=loss.detach() return tloss/n
该函数对模型进行1个轮次的训练。相关代码与我们在二分类中看到的代码类似,只不过这一次标签共有10个(从0到9),而不再只有2个(0 和 1)。
我们还定义了一个val_epoch()函数,如下所示:
def val_epoch(): vloss=0 for n,(imgs,labels) in enumerate(val_loader): imgs=imgs.reshape(-1,28*28).to(device) labels=labels.reshape(-1,).to(device) preds=model(imgs) loss=loss_fn(preds,labels) vloss+=loss.detach() return vloss/n
该函数使用模型对验证集中的图像进行预测,并计算每批数据的平均损失。
接下来训练多类别分类器:
for i in range(1,101): tloss=train_epoch() vloss=val_epoch() print(f"at epoch {i}, tloss is {tloss}, vloss is {vloss}") if stopper.stop(vloss)==True: break
最多训练100个轮次。在每个轮次中,首先使用训练集训练模型,然后计算验证集中每批数据的平均损失。我们使用EarlyStop()类,通过观察验证集的损失来确定是否应停止训练。如果在过去的10个轮次内损失没有改进就停止训练。经过19个轮次后,训练停止。
如果使用GPU,训练耗时约5分钟,比二分类的训练过程略久些,因为现在的训练集中有更多的观测值(10种服装,而非2种)。
模型的输出是一个由10个数字组成的向量。我们可以使用torch.argmax()根据最高概率为每个观测值分配一个标签,然后将预测标签与实际标签进行比较。为了说明预测是如何工作的,我们可以看看测试集中前5张图像的预测,代码如清单2.10所示。
清单2.10 在5张图像上测试训练好的模型
plt.figure(dpi=300,figsize=(5,1)) for i in range(5): ➊ ax=plt.subplot(1,5, i + 1) img=test_set[i][0] label=test_set[i][1] img=img/2+0.5 img=img.reshape(28, 28) plt.imshow(img, cmap="binary") plt.axis('off') plt.title(text_labels[label]+f"; {label}", fontsize=8) plt.show() for i in range(5): img,label = test_set[i] ➋ img=img.reshape(-1,28*28).to(device) pred=model(img) ➌ index_pred=torch.argmax(pred,dim=1) ➍ idx=index_pred.item() print(f"the label is {label}; the prediction is {idx}") ➎
➊ #绘制测试集中的前5张图像及其标签
➋ #获取测试集中的第i张图像及其标签
➌ #用训练好的模型进行预测
➍ #用torch.argmax()方法获取预测标签
➎ #输出实际标签和预测标签
将测试集中的前5件服装绘制成1×5的网格,然后用训练好的模型对每件服装进行预测。预测是一个包含10个值的张量。torch.argmax()方法会返回张量中最高概率的位置,用它作为预测标签。最后,输出实际标签和预测标签,以比较预测是否正确。运行上述代码后,会看到如图2.3所示的图像。

图2.3 测试集中的前5件服装及其各自的标签。每件服装都有一个文本标签和一个0到9之间的数字标签
如图2.3所示,测试集中的前5件服装分别是踝靴、套头衫、长裤、长裤和衬衫,数字标签分别为9、2、1、1和6。
运行清单2.10所示的代码,得到如下输出:
the label is 9; the prediction is 9 the label is 2; the prediction is 2 the label is 1; the prediction is 1 the label is 1; the prediction is 1 the label is 6; the prediction is 6
上述输出表明,模型对所有5件衣服的预测都是正确的。
固定PyTorch中的随机状态
torch.manual_seed()方法可以固定随机状态,因此重新运行程序也能得到相同结果。然而即便使用相同的随机种子,可能依然会得到与这里完全不同的结果。这是因为不同硬件和不同版本的PyTorch处理浮点运算的方式略有不同。这种差异通常很小,就算遇到也不必奇怪。
接下来,要计算整个测试集的预测准确性。清单2.11的代码可用于测试训练好的多类别分类模型。
清单2.11 测试训练好的多类别分类模型
results=[] for imgs,labels in test_loader: ➊ imgs=imgs.reshape(-1,28*28).to(device) labels=labels.reshape(-1,).to(device) preds=model(imgs) ➋ pred10=torch.argmax(preds,dim=1) ➌ correct=(pred10==labels) ➍ results.append(correct.detach().cpu().numpy().mean()) accuracy=np.array(results).mean() ➎ print(f"the accuracy of the predictions is {accuracy}")
➊ #对测试集中的所有批次进行迭代
➋ #用训练好的模型进行预测
➌ #将概率转换为预测标签
➍ #将预测标签与实际标签进行比较
➎ #计算测试集的准确性
输出如下:
the accuracy of the predictions is 0.8819665605095541
迭代测试集中的所有服装,并使用训练好的模型进行预测,然后将预测与实际标签进行比较。在样本外测试中,准确率约为88%。鉴于随机猜测的准确率约为10%,88%的准确率已经相当高了。这表明我们在PyTorch中构建并训练了两个成功的深度学习模型!在本书后续内容中会经常用到这些技能,如在第3章所构建的判别器网络本质上也是一个二分类模型,与本章创建的模型类似。