Datawhale X 李宏毅苹果书 AI夏令营_深度学习进阶_Datawhile_task2_自适应学习率、学习率调度、分类与CNN实践
文章目录
学习教程指路:Datawhile
一、自适应学习率
优化参数时陷入鞍点、局部极小值不一定是限制到我们的问题,有时训练网络到损失不再下降,还有可能是一直处于震荡的状态,如下图所示
这种情况出现的原因在于我们用梯度下降法更新参数时迈出一步的“步伐”过大,导致最小值处的峰比较窄的话就会一直在附近震荡,损失下不去。
我们现在训练一个网络,训练到现在参数在临界点附近,再根据特征值的正负号判断该临界点是鞍点还是局部最小值。实际上在训练的时候,要走到鞍点或局部最小值,是一件困难的事情。一般的梯度下降,其实是做不到的。用一般的梯度下降训练,往往会在梯度还很大的时候,损失就已经降了下去,这个是需要特别方法训练的。要走到一个临界点其实是比较困难的,多数时候训练在还没有走到临界点的时候就已经停止了。
最原始的梯度下降连简单的误差表面都做不好,因此需要更好的梯度下降的版本。在梯度下降里面,所有的参数都是设同样的学习率,这显然是不够的,应该要为每一个参数定制化学习率,即引入自适应学习率(adaptive learning rate)的方法,给每一个参数不同的学习率。如图 3.23 所示,如果在某一个方向上,梯度的值很小,非常平坦,我们会希望学习率调大一点;如果在某一个方向上非常陡峭,坡度很大,我们会希望学习率可以设得小一点。
我们引入三种自适应学习率方法,即:
1.AdaGrad
AdaGrad(Adaptive Gradient)是典型的自适应学习率方法,其能够根据梯度大小自动调整学习率。AdaGrad 可以做到梯度比较大的时候,学习率就减小,梯度比较小的时候,学习率就放大。
其第t+1次更新参数的时候,即
θ
t
+
1
i
←
θ
t
i
−
η
σ
t
i
g
t
i
σ
t
i
=
1
t
+
1
∑
n
=
1
t
(
g
t
i
)
2
(1)
\theta_{t+1}^i \leftarrow \theta_t^i-\frac{\eta}{\sigma_t^i}g_t^i \quad \sigma_t^i=\sqrt{\frac{1}{t+1}\displaystyle\sum_{n=1}^{t}(g_t^i)^2} \tag{1}
θt+1i←θti−σtiηgtiσti=t+11n=1∑t(gti)2(1)
η
σ
t
i
\frac{\eta}{\sigma_t^i}
σtiη当作是新的学习率来更新参数。有了
σ
t
i
\sigma_t^i
σti这一项以后,就可以随着梯度的不同,每一个参数的梯度的不同,来自动调整学习率的大小。
2. RMSProp
同一个参数需要的学习率,也会随着时间而改变。在图 3.25 中的误差表面中,如果考虑横轴方向,绿色箭头处坡度比较陡峭,需要较小的学习率,但是走到红色箭头处,坡度变得平坦了起来,需要较大的学习率。因此同一个参数的同个方向,学习率也是需要动态调整的,于是就有了一个新的方法—RMSprop(Root Mean Squared propagation)。
参数更新为
θ
t
+
1
i
←
θ
t
i
−
η
σ
t
i
g
t
i
σ
t
i
=
α
(
σ
t
i
)
2
+
(
1
−
α
)
(
g
t
i
)
2
(2)
\theta_{t+1}^i \leftarrow \theta_t^i-\frac{\eta}{\sigma_t^i}g_t^i \quad \sigma_t^i=\sqrt{\alpha(\sigma_t^i)^2+(1-\alpha)(g_t^i)^2} \tag{2}
θt+1i←θti−σtiηgtiσti=α(σti)2+(1−α)(gti)2(2)
其中
0
<
α
<
1
0<\alpha<1
0<α<1,其是一个可以调整的超参数。图 3.26 中黑线是误差表面,球就从 A 走到 B,AB 段的路很平坦,
g
g
g很小,更新参数的时候,我们会走比较大的步伐。走动BC段后梯度变大了,AdaGrad反应比较慢,而 RMSprop 会把
α
\alpha
α设小一点,让新的、刚看到的梯度的影响比较大,很快地让的值变大,很快地让步伐变小,RMSprop可以很快地“踩刹车”。如果走到CD段,CD 段是平坦的地方,可以调整
α
\alpha
α,让其比较看重最近算出来的梯度,梯度一变小,的值就变小了,走的步伐就变大了。
3.Adam
最常用的优化的策略或者优化器(optimizer)是Adam(Adaptive moment estimation)。Adam 可以看作 RMSprop 加上动量,其使用动量作为参数更新方向,并且能够自适应调整学习率。PyTorch 里面已经写好了 Adam 优化器,这个优化器里面有一些超参数需要人为决定,但是往往用 PyTorch 预设的参数就足够好了。
二、学习率调度
学习率调度是为了解决题走到图 3.27中红圈的地方,快走到终点的时候突然“爆炸”了。 σ t i \sigma_t^i σti是把过去所有的梯度拿来作平均。在AB段梯度很大,但在BC段,纵轴的方向梯度很小,因此纵轴方向累积了很小的 σ t i \sigma_t^i σti,累积到一定程度以后,步伐就变很大,但有办法修正回来。因为步伐很大,其会走到梯度比较大的地方。走到梯度比较大的地方后, σ t i \sigma_t^i σti会慢慢变大,更新的步伐大小会慢慢变小,从而回到原来的路线。
之前的学习率调整方法中
η
\eta
η是一个固定的值,而在学习率调度中
η
\eta
η跟时间有关。学习率调度中最常见的策略是学习率衰减(learning rate decay),也称为学习率退火(learning rateannealing)。随着参数的不断更新,让 η 越来越小,如图3.28 所示。如果加上学习率下降,可以很平顺地走到终点,如图 3.29 所示。虽然步伐很大,但
η
\eta
η变得非常小,步伐乘上
η
\eta
η就变小了,就可以慢慢地走到终点。
θ
t
+
1
i
←
θ
t
i
−
η
t
σ
t
i
g
t
i
(3)
\theta_{t+1}^i \leftarrow \theta_t^i-\frac{\eta_t}{\sigma_t^i}g_t^i \tag{3}
θt+1i←θti−σtiηtgti(3)
除了学习率下降以外,还有另外一个经典的学习率调度的方式———预热。预热的方法是让学习率先变大后变小,至于变到多大、变大的速度、变小的速度是超参数。残差网络里面是有预热的,在残差网络里面,学习率先设置成 0.01,再设置成 0.1,并且其论文还特别说明,一开始用 0.1 反而训练不好。除了残差网络,BERT 和 Transformer 的训练也都使用了预热。
Q:为什么需要预热?
A:当我们使用 Adam、RMSprop 或 AdaGrad 时,需要计算 σ \sigma σ。而 σ \sigma σ是一个统计的结果。从 σ \sigma σ可知某一个方向的陡峭程度。统计的结果需要足够多的数据才精准,一开始统计结果 σ \sigma σ是不精准的。一开始学习率比较小是用来探索收集一些有关误差表面的情报,先收集有关 σ \sigma σ的统计数据,等 σ \sigma σ统计得比较精准以后,再让学习率慢慢爬升。
三、分类
回归是输入一个向量x,输出 y ^ \hat{y} y^,我们希望 y ^ \hat{y} y^跟某一个标签 y y y越接近越好, y y y是要学习的目标。而分类可当作回归来看,输入x后,输出仍然是一个标量 y ^ \hat{y} y^,要让它跟正确答案的那个类越接近越好。
如果有三个类,标签 y y y就是一个三维的向量,比如类 1 是 [ 1 , 0 , 0 ] T [1,0,0]^T [1,0,0]T,类 2 是 [ 0 , 1 , 0 ] T [0,1,0]^T [0,1,0]T,类3是 [ 0 , 0 , 1 ] T [0,0,1]^T [0,0,1]T。如果每个类都用一个独热向量来表示,就没有类 1 跟类 2 比较接近,类1跟类3比较远的问题。如果用独热向量计算距离的话,类两两之间的距离都是一样的。
如果目标 y y y是一个向量,比如 y y y是有三个元素的向量,网络也要输出三个数字才行。如图 3.31 所示,输出三个数值就是把本来输出一个数值的方法,重复三次。把 a 1 , a 2 a_1,a_2 a1,a2和 a 3 a_3 a3乘上三个不同的权重,加上偏置,得到 y ^ 1 \hat{y}_1 y^1;再把 a 1 , a 2 a_1,a_2 a1,a2和 a 3 a_3 a3乘上另外三个权重,再加上另外一个偏置得到 y ^ 2 \hat{y}_2 y^2;把 a 1 , a 2 a_1,a_2 a1,a2和 a 3 a_3 a3再乘上另外一组权重,再加上另外一个偏置得到 y ^ 3 \hat{y}_3 y^3。输入一个特征向量,产生 y ^ 1 , y ^ 2 , y ^ 3 \hat{y}_1,\hat{y}_2,\hat{y}_3 y^1,y^2,y^3,希望 y ^ 1 , y ^ 2 , y ^ 3 \hat{y}_1,\hat{y}_2,\hat{y}_3 y^1,y^2,y^3跟目标越接近越好。
1、带有softmax的分类
按照上述的设定,分类实际过程是:输入 x x x,乘上 W W W,加上 b b b,通过激活函数 σ \sigma σ,乘上 W W W,再加上 b b b得到向量 y y y。但实际做分类的时候,往往会把 y y y通过softmax函数得到 y y y,才去计算 y ′ y' y′跟 y ^ \hat{y} y^之间的距离。
softmax函数如下:
y
i
′
=
e
x
p
(
y
i
)
∑
j
e
x
p
(
y
i
)
(4)
y_i'=\frac{exp(y_i)}{\displaystyle\sum_{j}^{}exp(y_i)} \tag{4}
yi′=j∑exp(yi)exp(yi)(4)
其中,
1
>
y
i
′
>
0
,
∑
i
y
i
′
=
1
1>y_i'>0,\ \displaystyle\sum_{i}^{}y_i'=1
1>yi′>0, i∑yi′=1.
四、CNN实践
引自[Datawhile](Datawhale (linklearner.com))
Step1:准备算力
- 学生认证云工开物,领取算力抵扣金(点击即可跳转)
- 链接:https://university.aliyun.com/mobile?userCode=1h9ofupt
-
- 链接:https://www.aliyun.com/activity/bigdata/pai/dsw
Step2:一键运行Notebook
启动创建的实例,等待几分钟直到启动完成,然后点击进入JupyterLab。接着,点击“Terminal”打开命令行窗口,输入以下代码并按下回车键。稍等片刻,数据集和代码文件(notebook)将会自动下载,大约需要一分钟。
git clone https://www.modelscope.cn/datasets/Datawhale/LeeDL-HW3-CNN.git
点击打开**LeeDL-HW3-CNN
**文件夹
点击**HW3-ImageClassification.ipynb
**
等待约12分钟后即可获得结果。通过单元格(cell)查看模型的训练准确率。生成的submission.csv
文件包含分类结果,可提交至Kaggle进行评估。
代码将打印出当前模型的结构,并绘制两个数据集的图像分析和分类结果分布图,以帮助更好地理解模型的表现。
Step3:运行完成后记得停止实例
一定要记得【停止实例】,否则会一直扣费消耗代金券!!!!
代码详解
卷积神经网络是深度学习中的一个非常重要的分支,本作业提供了进行图像分类任务的基本范式。
- 准备数据
- 训练模型
- 应用模型
要完成一个深度神经网络训练模型的代码,大概需要完成下面的内容:
- 导入所需要的库/工具包
- 数据准备与预处理
- 定义模型
- 定义损失函数和优化器等其他配置
- 训练模型
- 评估模型
- 进行预测
此范式不仅适用于图像分类任务,对于广泛的深度学习任务也是适用的。
1. 导入所需要的库/工具包
这段代码导入了进行图像处理和深度学习任务所需的各种Python库和模块,涵盖了数据处理、神经网络构建、数据集操作、图像转换和显示进度条等功能,为后续的模型训练和评估做好准备。
# 导入必要的库
import numpy as np
import pandas as pd
import torch
import os
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image
# “ConcatDataset” 和 “Subset” 在进行半监督学习时可能是有用的。
from torch.utils.data import ConcatDataset, DataLoader, Subset, Dataset
from torchvision.datasets import DatasetFolder, VisionDataset
# 这个是用来显示进度条的。
from tqdm.auto import tqdm
import random
此外,为了确保实验的可重复性,设置随机种子,并对CUDA进行配置以确保确定性:
# 设置随机种子以确保实验结果的可重复性
myseed = 6666
# 确保在使用CUDA时,卷积运算具有确定性,以增强实验结果的可重复性
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# 为numpy和pytorch设置随机种子
np.random.seed(myseed)
torch.manual_seed(myseed)
# 如果使用CUDA,为所有GPU设置随机种子
if torch.cuda.is_available():
torch.cuda.manual_seed_all(myseed)
2. 数据准备与预处理
数据准备包括从指定路径加载图像数据,并对其进行预处理。作业中对图像的预处理操作包括调整大小和将图像转换为Tensor格式。为了增强模型的鲁棒性,可以对训练集进行数据增强。相关代码如下:
# 在测试和验证阶段,通常不需要图像增强。
# 我们所需要的只是调整PIL图像的大小并将其转换为Tensor。
test_tfm = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
])
# 不过,在测试阶段使用图像增强也是有可能的。
# 你可以使用train_tfm生成多种图像,然后使用集成方法进行测试。
train_tfm = transforms.Compose([
# 将图像调整为固定大小(高度和宽度均为128)
transforms.Resize((128, 128)),
# TODO:你可以在这里添加一些图像增强的操作。
# ToTensor()应该是所有变换中的最后一个。
transforms.ToTensor(),
])
class FoodDataset(Dataset):
"""
用于加载食品图像数据集的类。
该类继承自Dataset,提供了对食品图像数据集的加载和预处理功能。
它可以自动从指定路径加载所有的jpg图像,并对这些图像应用给定的变换。
"""
def __init__(self, path, tfm=test_tfm, files=None):
"""
初始化FoodDataset实例。
参数:
- path: 图像数据所在的目录路径。
- tfm: 应用于图像的变换方法(默认为测试变换)。
- files: 可选参数,用于直接指定图像文件的路径列表(默认为None)。
"""
super(FoodDataset).__init__()
self.path = path
# 列出目录下所有jpg文件,并按顺序排序
self.files = sorted([os.path.join(path, x) for x in os.listdir(path) if x.endswith(".jpg")])
if files is not None:
self.files = files # 如果提供了文件列表,则使用该列表
self.transform = tfm # 图像变换方法
def __len__(self):
"""
返回数据集中图像的数量。
返回:
- 数据集中的图像数量。
"""
return len(self.files)
def __getitem__(self, idx):
"""
获取给定索引的图像及其标签。
参数:
- idx: 图像在数据集中的索引。
返回:
- im: 应用了变换后的图像。
- label: 图像对应的标签(如果可用)。
"""
fname = self.files[idx]
im = Image.open(fname)
im = self.transform(im) # 应用图像变换
# 尝试从文件名中提取标签
try:
label = int(fname.split("/")[-1].split("_")[0])
except:
label = -1 # 如果无法提取标签,则设置为-1(测试数据无标签)
return im, label
# 构建训练和验证数据集
# "loader" 参数定义了torchvision如何读取数据
train_set = FoodDataset("./hw3_data/train", tfm=train_tfm)
# 创建训练数据加载器,设置批量大小、是否打乱数据顺序、是否使用多线程加载以及是否固定内存地址
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)
# 构建验证数据集
# "loader" 参数定义了torchvision如何读取数据
valid_set = FoodDataset("./hw3_data/valid", tfm=test_tfm)
# 创建验证数据加载器,设置批量大小、是否打乱数据顺序、是否使用多线程加载以及是否固定内存地址
valid_loader = DataLoader(valid_set, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)
3. 定义模型
这段代码定义了一个图像分类器类(Classifier
),继承自PyTorch的nn.Module
。该分类器通过一系列卷积层、批归一化层、激活函数和池化层构建卷积神经网络(CNN),用于提取图像特征。随后,这些特征被输入到全连接层进行分类,最终输出11个类别的概率,用于图像分类任务。
class Classifier(nn.Module):
"""
定义一个图像分类器类,继承自PyTorch的nn.Module。
该分类器包含卷积层和全连接层,用于对图像进行分类。
"""
def __init__(self):
"""
初始化函数,构建卷积神经网络的结构。
包含一系列的卷积层、批归一化层、激活函数和池化层。
"""
super(Classifier, self).__init__()
# 定义卷积神经网络的序列结构
self.cnn = nn.Sequential(
nn.Conv2d(3, 64, 3, 1, 1), # 输入通道3,输出通道64,卷积核大小3,步长1,填充1
nn.BatchNorm2d(64), # 批归一化,作用于64个通道
nn.ReLU(), # ReLU激活函数
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0
nn.Conv2d(64, 128, 3, 1, 1), # 输入通道64,输出通道128,卷积核大小3,步长1,填充1
nn.BatchNorm2d(128), # 批归一化,作用于128个通道
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0
nn.Conv2d(128, 256, 3, 1, 1), # 输入通道128,输出通道256,卷积核大小3,步长1,填充1
nn.BatchNorm2d(256), # 批归一化,作用于256个通道
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0
nn.Conv2d(256, 512, 3, 1, 1), # 输入通道256,输出通道512,卷积核大小3,步长1,填充1
nn.BatchNorm2d(512), # 批归一化,作用于512个通道
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0
nn.Conv2d(512, 512, 3, 1, 1), # 输入通道512,输出通道512,卷积核大小3,步长1,填充1
nn.BatchNorm2d(512), # 批归一化,作用于512个通道
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0
)
# 定义全连接神经网络的序列结构
self.fc = nn.Sequential(
nn.Linear(512*4*4, 1024), # 输入大小512*4*4,输出大小1024
nn.ReLU(),
nn.Linear(1024, 512), # 输入大小1024,输出大小512
nn.ReLU(),
nn.Linear(512, 11) # 输入大小512,输出大小11,最终输出11个类别的概率
)
def forward(self, x):
"""
前向传播函数,对输入进行处理。
参数:
x -- 输入的图像数据,形状为(batch_size, 3, 128, 128)
返回:
输出的分类结果,形状为(batch_size, 11)
"""
out = self.cnn(x) # 通过卷积神经网络处理输入
out = out.view(out.size()[0], -1) # 展平输出,以适配全连接层的输入要求
return self.fc(out) # 通过全连接神经网络得到最终输出
4. 定义损失函数和优化器等其他配置
这段代码实现了图像分类模型的初始化和训练配置,目的是准备好训练环境和参数。它选择合适的设备(GPU或CPU),设置模型、批量大小、训练轮数、提前停止策略,定义了损失函数和优化器,为后续的模型训练奠定了基础。
# 根据GPU是否可用选择设备类型
device = "cuda" if torch.cuda.is_available() else "cpu"
# 初始化模型,并将其放置在指定的设备上
model = Classifier().to(device)
# 定义批量大小
batch_size = 64
# 定义训练轮数
n_epochs = 8
# 如果在'patience'轮中没有改进,则提前停止
patience = 5
# 对于分类任务,我们使用交叉熵作为性能衡量标准
criterion = nn.CrossEntropyLoss()
# 初始化优化器,您可以自行调整一些超参数,如学习率
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003, weight_decay=1e-5)
5. 训练模型
这段代码实现了一个图像分类模型的训练和验证循环,目的是通过多轮训练(epochs)逐步优化模型的参数,以提高其在验证集上的性能,并保存效果最好的模型。训练阶段通过前向传播、计算损失、反向传播和参数更新来优化模型,验证阶段评估模型在未见过的数据上的表现。如果验证集的准确率超过了之前的最好成绩,保存当前模型,并在连续多轮验证性能未提升时提前停止训练。
# 初始化追踪器,这些不是参数,不应该被更改
stale = 0
best_acc = 0
for epoch in range(n_epochs):
# ---------- 训练阶段 ----------
# 确保模型处于训练模式
model.train()
# 这些用于记录训练过程中的信息
train_loss = []
train_accs = []
for batch in tqdm(train_loader):
# 每个批次包含图像数据及其对应的标签
imgs, labels = batch
# imgs = imgs.half()
# print(imgs.shape,labels.shape)
# 前向传播数据。(确保数据和模型位于同一设备上)
logits = model(imgs.to(device))
# 计算交叉熵损失。
# 在计算交叉熵之前不需要应用softmax,因为它会自动完成。
loss = criterion(logits, labels.to(device))
# 清除上一步中参数中存储的梯度
optimizer.zero_grad()
# 计算参数的梯度
loss.backward()
# 为了稳定训练,限制梯度范数
grad_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=10)
# 使用计算出的梯度更新参数
optimizer.step()
# 计算当前批次的准确率
acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()
# 记录损失和准确率
train_loss.append(loss.item())
train_accs.append(acc)
train_loss = sum(train_loss) / len(train_loss)
train_acc = sum(train_accs) / len(train_accs)
# 打印信息
print(f"[ 训练 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {train_loss:.5f}, acc = {train_acc:.5f}")
6. 评估模型
训练完成后,需要在测试集上评估模型的性能。通过计算准确率来衡量模型在测试集上的表现。
# ---------- 验证阶段 ----------
# 确保模型处于评估模式,以便某些模块如dropout能够正常工作
model.eval()
# 这些用于记录验证过程中的信息
valid_loss = []
valid_accs = []
# 按批次迭代验证集
for batch in tqdm(valid_loader):
# 每个批次包含图像数据及其对应的标签
imgs, labels = batch
# imgs = imgs.half()
# 我们在验证阶段不需要梯度。
# 使用 torch.no_grad() 加速前向传播过程。
with torch.no_grad():
logits = model(imgs.to(device))
# 我们仍然可以计算损失(但不计算梯度)。
loss = criterion(logits, labels.to(device))
# 计算当前批次的准确率
acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()
# 记录损失和准确率
valid_loss.append(loss.item())
valid_accs.append(acc)
# break
# 整个验证集的平均损失和准确率是所记录值的平均
valid_loss = sum(valid_loss) / len(valid_loss)
valid_acc = sum(valid_accs) / len(valid_accs)
# 打印信息
print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")
# 更新日志
if valid_acc > best_acc:
with open(f"./{_exp_name}_log.txt", "a"):
print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f} -> 最佳")
else:
with open(f"./{_exp_name}_log.txt", "a"):
print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")
# 保存模型
if valid_acc > best_acc:
print(f"在第 {epoch} 轮找到最佳模型,正在保存模型")
torch.save(model.state_dict(), f"{_exp_name}_best.ckpt") # 只保存最佳模型以防止输出内存超出错误
best_acc = valid_acc
stale = 0
else:
stale += 1
if stale > patience:
print(f"连续 {patience} 轮没有改进,提前停止")
break
7. 进行预测
最后的代码构建一个测试数据集和数据加载器,以便高效地读取数据。实例化并加载预训练的分类器模型,并将其设置为评估模式。在不计算梯度的情况下,遍历测试数据,使用模型进行预测,并将预测标签存储在列表中。将预测结果与测试集的ID生成一个DataFrame,并将其保存为submission.csv
文件。
# 构建测试数据集
# "loader"参数指定了torchvision如何读取数据
test_set = FoodDataset("./hw3_data/test", tfm=test_tfm)
# 创建测试数据加载器,批量大小为batch_size,不打乱数据顺序,不使用多线程,启用pin_memory以提高数据加载效率
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=0, pin_memory=True)
# 实例化分类器模型,并将其转移到指定的设备上
model_best = Classifier().to(device)
# 加载模型的最优状态字典
model_best.load_state_dict(torch.load(f"{_exp_name}_best.ckpt"))
# 将模型设置为评估模式
model_best.eval()
# 初始化一个空列表,用于存储所有预测标签
prediction = []
# 使用torch.no_grad()上下文管理器,禁用梯度计算
with torch.no_grad():
# 遍历测试数据加载器
for data, _ in tqdm(test_loader):
# 将数据转移到指定设备上,并获得模型的预测结果
test_pred = model_best(data.to(device))
# 选择具有最高分数的类别作为预测标签
test_label = np.argmax(test_pred.cpu().data.numpy(), axis=1)
# 将预测标签添加到结果列表中
prediction += test_label.squeeze().tolist()
# 创建测试csv文件
def pad4(i):
"""
将输入数字i转换为长度为4的字符串,如果长度不足4,则在前面补0。
:param i: 需要转换的数字
:return: 补0后的字符串
"""
return "0" * (4 - len(str(i))) + str(i)
# 创建一个空的DataFrame对象
df = pd.DataFrame()
# 使用列表推导式生成Id列,列表长度等于测试集的长度
df["Id"] = [pad4(i) for i in range(len(test_set))]
# 将预测结果赋值给Category列
df["Category"] = prediction
# 将DataFrame对象保存为submission.csv文件,不保存索引
df.to_csv("submission.csv", index=False)
.cpu().data.numpy(), axis=1)
# 将预测标签添加到结果列表中
prediction += test_label.squeeze().tolist()
# 创建测试csv文件
def pad4(i):
"""
将输入数字i转换为长度为4的字符串,如果长度不足4,则在前面补0。
:param i: 需要转换的数字
:return: 补0后的字符串
"""
return "0" * (4 - len(str(i))) + str(i)
# 创建一个空的DataFrame对象
df = pd.DataFrame()
# 使用列表推导式生成Id列,列表长度等于测试集的长度
df["Id"] = [pad4(i) for i in range(len(test_set))]
# 将预测结果赋值给Category列
df["Category"] = prediction
# 将DataFrame对象保存为submission.csv文件,不保存索引
df.to_csv("submission.csv", index=False)