FA:Formulas and Algorithm,CNN:Convolutional Neural Network
1. 引言
在深度学习入门阶段,CIFAR-10 数据集是最经典的图像分类练手项目之一。它包含 10 个类别的 60000 张 32×32 彩色图片,其中 50000 张用于训练、10000 张用于测试。本文将从零开始,使用 PyTorch 搭建一个简单的卷积神经网络(CNN),完成数据加载、模型搭建、训练、测试评估以及模型保存的完整流程,并讲解model.train()与model.eval()等关键细节。
2. 网络模型设计
我们使用一个轻量级的 CNN 模型来完成 CIFAR-10 分类任务,网络结构如下:
- 输入:3×32×32 的彩色图片
- 卷积层 1:
Conv2d(3, 32, 5, 1, 2),输出 32 通道 - 最大池化层 1:
MaxPool2d(2) - 卷积层 2:
Conv2d(32, 32, 5, 1, 2) - 最大池化层 2:
MaxPool2d(2) - 卷积层 3:
Conv2d(32, 64, 5, 1, 2) - 最大池化层 3:
MaxPool2d(2) - 展平层:
Flatten(),展平后维度为 64×4×4 - 全连接层 1:
Linear(64*4*4, 64) - 全连接层 2:
Linear(64, 10),输出 10 个类别
importtorchfromtorchimportnnclassTudui(nn.Module):def__init__(self):super(Tudui,self).__init__()self.model1=nn.Sequential(nn.Conv2d(3,32,5,1,2),# 输入通道3,输出通道32,卷积核尺寸5×5,步长1,填充2nn.MaxPool2d(2),nn.Conv2d(32,32,5,1,2),nn.MaxPool2d(2),nn.Conv2d(32,64,5,1,2),nn.MaxPool2d(2),nn.Flatten(),# 展平后变成 64*4*4nn.Linear(64*4*4,64),nn.Linear(64,10))defforward(self,x):x=self.model1(x)returnx3. 使用 DataLoader 加载数据集
首先通过torchvision.datasets.CIFAR10下载并加载数据集,然后使用DataLoader将数据分批送入网络。
importtorchvisionfromtorch.utils.dataimportDataLoader# 准备数据集train_data=torchvision.datasets.CIFAR10("./dataset",train=True,transform=torchvision.transforms.ToTensor(),download=True)test_data=torchvision.datasets.CIFAR10("./dataset",train=False,transform=torchvision.transforms.ToTensor(),download=True)# 数据集长度train_data_size=len(train_data)test_data_size=len(test_data)print("训练数据集的长度:{}".format(train_data_size))print("测试数据集的长度:{}".format(test_data_size))# 利用 DataLoader 加载数据集train_dataloader=DataLoader(train_data,batch_size=64)test_dataloader=DataLoader(test_data,batch_size=64)运行结果:
Files already downloaded and verified Files already downloaded and verified 训练数据集的长度:50000 测试数据集的长度:100004. 测试网络输出尺寸
在正式训练前,先用一个随机输入验证网络输出维度是否符合预期(应为[64, 10])。
importtorchfromtorchimportnnclassTudui(nn.Module):def__init__(self):super(Tudui,self).__init__()self.model1=nn.Sequential(nn.Conv2d(3,32,5,1,2),nn.MaxPool2d(2),nn.Conv2d(32,32,5,1,2),nn.MaxPool2d(2),nn.Conv2d(32,64,5,1,2),nn.MaxPool2d(2),nn.Flatten(),nn.Linear(64*4*4,64),nn.Linear(64,10))defforward(self,x):x=self.model1(x)returnxif__name__=='__main__':tudui=Tudui()input=torch.ones((64,3,32,32))output=tudui(input)print(output.shape)# 测试输出的尺寸运行结果:
torch.Size([64, 10])5. 网络训练
接下来定义损失函数、优化器并开始训练。这里使用交叉熵损失函数和随机梯度下降(SGD)优化器,学习率设为 0.01,共训练 10 轮。
importtorchvisionimporttorchfromtorchimportnnfromtorch.utils.dataimportDataLoaderclassTudui(nn.Module):def__init__(self):super(Tudui,self).__init__()self.model1=nn.Sequential(nn.Conv2d(3,32,5,1,2),nn.MaxPool2d(2),nn.Conv2d(32,32,5,1,2),nn.MaxPool2d(2),nn.Conv2d(32,64,5,1,2),nn.MaxPool2d(2),nn.Flatten(),nn.Linear(64*4*4,64),nn.Linear(64,10))defforward(self,x):x=self.model1(x)returnx# 准备数据集train_data=torchvision.datasets.CIFAR10("./dataset",train=True,transform=torchvision.transforms.ToTensor(),download=True)test_data=torchvision.datasets.CIFAR10("./dataset",train=False,transform=torchvision.transforms.ToTensor(),download=True)train_data_size=len(train_data)test_data_size=len(test_data)print("训练数据集的长度:{}".format(train_data_size))print("测试数据集的长度:{}".format(test_data_size))train_dataloader=DataLoader(train_data,batch_size=64)test_dataloader=DataLoader(test_data,batch_size=64)# 创建网络模型tudui=Tudui()# 损失函数loss_fn=nn.CrossEntropyLoss()# 交叉熵# 优化器learning=0.01# 1e-2 就是 0.01optimizer=torch.optim.SGD(tudui.parameters(),learning)# 随机梯度下降# 记录训练的次数total_train_step=0# 训练的轮次epoch=10foriinrange(epoch):print("-----第 {} 轮训练开始-----".format(i+1))fordataintrain_dataloader:imgs,targets=data outputs=tudui(imgs)loss=loss_fn(outputs,targets)# 计算实际输出与目标输出的差距# 优化器对模型调优optimizer.zero_grad()# 梯度清零loss.backward()# 反向传播,计算损失函数的梯度optimizer.step()# 根据梯度,对网络的参数进行调优total_train_step=total_train_step+1print("训练次数:{},Loss:{}".format(total_train_step,loss.item()))运行结果(节选):
Files already downloaded and verified Files already downloaded and verified 训练数据集的长度:50000 测试数据集的长度:10000 -----第 1 轮训练开始----- 训练次数:1,Loss:2.3299059867858887 训练次数:2,Loss:2.3018362522125244 训练次数:3,Loss:2.2983856201171875 ...6. 使用 TensorBoard 可视化训练过程
为了更直观地观察训练过程,可以借助torch.utils.tensorboard.SummaryWriter记录训练损失、测试损失和测试准确率。
importtorchvisionimporttorchfromtorchimportnnfromtorch.utils.dataimportDataLoaderfromtorch.utils.tensorboardimportSummaryWriterclassTudui(nn.Module):def__init__(self):super(Tudui,self).__init__()self.model1=nn.Sequential(nn.Conv2d(3,32,5,1,2),nn.MaxPool2d(2),nn.Conv2d(32,32,5,1,2),nn.MaxPool2d(2),nn.Conv2d(32,64,5,1,2),nn.MaxPool2d(2),nn.Flatten(),nn.Linear(64*4*4,64),nn.Linear(64,10))defforward(self,x):x=self.model1(x)returnx# 准备数据集train_data=torchvision.datasets.CIFAR10("./dataset",train=True,transform=torchvision.transforms.ToTensor(),download=True)test_data=torchvision.datasets.CIFAR10("./dataset",train=False,transform=torchvision.transforms.ToTensor(),download=True)train_data_size=len(train_data)test_data_size=len(test_data)print("训练数据集的长度:{}".format(train_data_size))print("测试数据集的长度:{}".format(test_data_size))train_dataloader=DataLoader(train_data,batch_size=64)test_dataloader=DataLoader(test_data,batch_size=64)# 创建网络模型tudui=Tudui()# 损失函数loss_fn=nn.CrossEntropyLoss()# 优化器learning=0.01optimizer=torch.optim.SGD(tudui.parameters(),learning)total_train_step=0total_test_step=0epoch=10# 添加 TensorBoardwriter=SummaryWriter("logs")foriinrange(epoch):print("-----第 {} 轮训练开始-----".format(i+1))# 训练步骤开始fordataintrain_dataloader:imgs,targets=data outputs=tudui(imgs)loss=loss_fn(outputs,targets)optimizer.zero_grad()loss.backward()optimizer.step()total_train_step=total_train_step+1iftotal_train_step%100==0:print("训练次数:{},Loss:{}".format(total_train_step,loss.item()))writer.add_scalar("train_loss",loss.item(),total_train_step)# 测试步骤开始(每一轮训练后查看测试集上的 loss 情况)total_test_loss=0total_accuracy=0withtorch.no_grad():# 测试时不需要梯度fordataintest_dataloader:imgs,targets=data outputs=tudui(imgs)loss=loss_fn(outputs,targets)total_test_loss=total_test_loss+loss.item()accuracy=(outputs.argmax(1)==targets).sum()total_accuracy=total_accuracy+accuracyprint("整体测试集上的Loss:{}".format(total_test_loss))print("整体测试集上的正确率:{}".format(total_accuracy/test_data_size))writer.add_scalar("test_loss",total_test_loss,total_test_step)writer.add_scalar("test_accuracy",total_accuracy/test_data_size,total_test_step)total_test_step=total_test_step+1# 保存每一轮训练后的模型torch.save(tudui,"./model/tudui_{}.pth".format(i))print("模型已保存")writer.close()运行结果(节选):
Files already downloaded and verified Files already downloaded and verified 训练数据集的长度:50000 测试数据集的长度:10000 -----第 1 轮训练开始----- 训练次数:100,Loss:2.2990777492523193 训练次数:200,Loss:2.279019594192505 ... 整体测试集上的Loss:318.3006658554077 整体测试集上的正确率:0.26919999718666077 模型已保存 -----第 2 轮训练开始----- ... 整体测试集上的正确率:0.5320000052452087 模型已保存可以看到,随着训练轮次增加,测试集上的正确率从约 26.9% 逐步提升到约 53.2%,说明网络正在持续学习。
7. 计算正确率
在测试阶段,我们通过argmax获取每个样本预测概率最大的类别索引,再与真实标签比较,统计正确个数。
importtorch outputs=torch.tensor([[0.1,0.2],[0.05,0.4]])print(outputs.argmax(0))# 竖着看,最大值的索引print(outputs.argmax(1))# 横着看,最大值的索引preds=outputs.argmax(0)targets=torch.tensor([0,1])print((preds==targets).sum())# 对应位置相等的个数运行结果:
tensor([0, 1]) tensor([1, 1]) tensor(2)8. model.train() 与 model.eval() 的区别
model.train()和model.eval()的区别主要体现在Batch Normalization(BN)和Dropout两层:
- 训练时:如果模型中有 BN 层和 Dropout,需要调用
model.train()。model.train()保证 BN 层能够用到每一批数据的均值和方差;对于 Dropout,model.train()会随机取一部分网络连接来训练更新参数。 - 测试时:如果模型中有 BN 层和 Dropout,需要调用
model.eval()。model.eval()保证 BN 层使用全部训练数据的均值和方差,即测试过程中 BN 层的均值和方差保持不变;对于 Dropout,model.eval()会利用到所有网络连接,即不进行随机舍弃神经元。 - 注意事项:训练完 train 样本后,生成的模型 model 要用来测试样本。在
model(test)之前,需要加上model.eval(),否则即使不训练,有输入数据时也会改变权值,这是模型中含有 BN 层和 Dropout 所带来的性质。 - 特别提醒:在做 one classification 的时候,训练集和测试集的样本分布是不一样的,尤其需要注意这一点。
在训练循环中加入tudui.train()和tudui.eval()的完整代码如下:
importtorchvisionimporttorchfromtorchimportnnfromtorch.utils.dataimportDataLoaderfromtorch.utils.tensorboardimportSummaryWriterclassTudui(nn.Module):def__init__(self):super(Tudui,self).__init__()self.model1=nn.Sequential(nn.Conv2d(3,32,5,1,2),nn.MaxPool2d(2),nn.Conv2d(32,32,5,1,2),nn.MaxPool2d(2),nn.Conv2d(32,64,5,1,2),nn.MaxPool2d(2),nn.Flatten(),nn.Linear(64*4*4,64),nn.Linear(64,10))defforward(self,x):x=self.model1(x)returnx# 准备数据集train_data=torchvision.datasets.CIFAR10("./dataset",train=True,transform=torchvision.transforms.ToTensor(),download=True)test_data=torchvision.datasets.CIFAR10("./dataset",train=False,transform=torchvision.transforms.ToTensor(),download=True)train_data_size=len(train_data)test_data_size=len(test_data)print("训练数据集的长度:{}".format(train_data_size))print("测试数据集的长度:{}".format(test_data_size))train_dataloader=DataLoader(train_data,batch_size=64)test_dataloader=DataLoader(test_data,batch_size=64)tudui=Tudui()loss_fn=nn.CrossEntropyLoss()learning=0.01optimizer=torch.optim.SGD(tudui.parameters(),learning)total_train_step=0total_test_step=0epoch=10writer=SummaryWriter("logs")foriinrange(epoch):print("-----第 {} 轮训练开始-----".format(i+1))# 训练模式:BN 层和 Dropout 生效tudui.train()fordataintrain_dataloader:imgs,targets=data outputs=tudui(imgs)loss=loss_fn(outputs,targets)optimizer.zero_grad()loss.backward()optimizer.step()total_train_step=total_train_step+1iftotal_train_step%100==0:print("训练次数:{},Loss:{}".format(total_train_step,loss.item()))writer.add_scalar(