DL练习:CIFAR10

以前总把SDG叫成SGD,昨晚把SGD叫成了SDG。


#

使用残差网络与随机梯度下降处理[latex]\text{CIFAR-10}[/latex]分类任务。

# 数据来源与预处理

此处下载训练集与测试集。这是包含[latex]10[/latex]类实物的图像数据,每张全彩图片的分辨率为[latex]32\times 32[/latex],训练集共有[latex]50000[/latex]张。

构造图像数据迭代器

解压后分别得到储存训练集和测试集图片的文件夹,和一个记录[latex]\text{图片ID}[/latex]和标签的[latex]\textbf{train.csv文件}[/latex]。因此需要构造一个读取这些数据的类及其方法。由于方法内涉及打开图像,因此需要\(\text{from PIL import Image}\)

class custom_dateset(data.Dataset):
    def __init__(self, df, image_dir, transform=None):
        self.data_frame = df
        self.image_dir = image_dir
        self.transform = transform

    def __len__(self):
        return len(self.data_frame)

    def __getitem__(self, idx):
        if torch.is_tensor(idx):
            idx = idx.to_list()
        img_name = os.path.join(self.image_dir, str(self.data_frame.iloc[idx, 0]) + '.png')
        image = Image.open(img_name)
        label = self.data_frame.iloc[idx, 1]
        if self.transform:
            image = self.transform(image)
        return image, label

由于[latex]\textbf{train.csv文件}[/latex]仅记录了[latex]\text{图片ID}[/latex],因此代码中手动加入了 .png 文件格式后缀。

该方法可用来读取非测试集的图像数据,因为同时返回了 image  label 

给标签编号,然后将 train.csv 分为训练集和验证集,并实例化训练集和训练集数据迭代器

## 数据预处理
# 读取所有的训练集
data_df = pandas.read_csv('/workspace/data/cifar10/trainLabels.csv', header=0)
# 将字符串标签编码
label_encoder = LabelEncoder()
data_df['label'] = label_encoder.fit_transform(data_df.iloc[:, 1])
# 将训练集的数据进一步划分为训练集和验证集
train_df, valid_df = train_test_split(data_df, test_size=0.2, shuffle=True)
# 数据增强
trans_train = transforms.Compose(
    [
        transforms.Resize(40),  # 为进行随机剪裁,需要先放大图片
        transforms.RandomResizedCrop(32, scale=(0.64, 1), ratio=(1, 1)),
        transforms.RandomHorizontalFlip(),
        # transforms.RandomVerticalFlip(),
        transforms.ToTensor()
    ]
)
trans = transforms.Compose(
    [
        transforms.Resize(32),
        transforms.ToTensor()
    ]
)
## 设定构造数据迭代器所需超参数
batch_size = 128
## 构建数据迭代器
train_dataset = custom_dateset(train_df, image_dir='/workspace/data/cifar10/train', transform=trans_train)
valid_dataset = custom_dateset(valid_df, image_dir='/workspace/data/cifar10/train', transform=trans)
train_iter = data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=6)
valid_iter = data.DataLoader(valid_dataset, batch_size=batch_size, shuffle=True, num_workers=6)
num_batch = len(train_iter)

注:自行调整[latex]\textit{num_workers}[/latex]。

# 训练准备

训练前需设定一切所需超参数、实例化网络模型、损失函数等。由于本帖采用[latex]\text{随机梯度下降}[/latex]优化器,根据其收敛要求,需要进一步实例化[latex]\text{lr_scheduler}[/latex]。

设定优化器所需超参数,以及需要训练多少个\(\textit{epoch}\)

lr = 0.02
weight_decay = 0.0005
epoch_num = 200

定义网络

net = models.resnext50_32x4d(weights=ResNeXt50_32X4D_Weights.IMAGENET1K_V1)
net.fc = nn.Sequential(nn.Linear(net.fc.in_features, 10))
nn.init.xavier_uniform_(net.fc[0].weight)
net.to(device='cuda:0')

实例化优化器、\(\text{lr_scheduler}\)和损失函数

# 实例化SDG优化器。momentum是一个优化技术
optimizer = torch.optim.SGD(net.parameters(), lr=lr, momentum=0.9, weight_decay=weight_decay)
# 构造lr_scheduler.每完成4个epoch将学习率乘以0.9
lr_period = 4
lr_decay = 0.9
scheduler = torch.optim.lr_scheduler.StepLR(optimizer=optimizer, step_size=lr_period, gamma=lr_decay)
# 定义损失函数
loss = nn.CrossEntropyLoss()

训练过程记录准备

train_acc = torch.Tensor().to(device='cuda:0')  # 用于记录每一次迭代的训练集准确率
valid_acc = torch.Tensor().to(device='cuda:0')  # 记录一个epoch内每次迭代的验证集batch准确率。用于求平均,得到每个epoch的验证集准确率
valid_acc_list = []  # 记录所有epoch的验证集准确率
timer_list = []  # 记录每个epoch所用的训练时间
l_list = torch.Tensor().to(device='cuda:0')  # 记录每次迭代的损失
best_model_acc = 0.  # 记录最佳精度

# 训练

开始训练

for epoch in range(epoch_num):
    start_time = time.time()  # 计时开始
    # 转变为训练形态!
    net.train()
    for i, (X, y) in enumerate(train_iter):  # 来笔数据做iteration!
        X, y = X.to(device='cuda:0'), y.to(device='cuda:0')  # 把这笔数据丢显卡上!
        optimizer.zero_grad()  # 清空梯度!
        l = loss(net(X), y)  # 计算损失!
        l.backward()  # 反向传播!
        optimizer.step()  # 同步更新参数!
        with torch.no_grad():  # 后面两行代码不要记录梯度!
            acc_train = (net(X).argmax(dim=-1) == y).float().mean()  # 计算得到这一小笔训练数据的准确率!
            train_acc = torch.cat([train_acc, acc_train.reshape(-1)])  # 由于acc_train是零维[],需要reshape
        l_list = torch.cat([l_list, l.reshape(-1)])
        # if (i + 1) % (num_batch // 5) == 0 or i == num_batch - 1:  # 控制输出进程的时机
        print(f"epoch: {epoch} ",
              f"loss: {l.item():.6f}\t ",
              f"train accuracy: {100. * acc_train :.2f}%\t")
        # 用于debug
        # if i == 3:
        #     break
    end_time = time.time()  # 计时结束
    timer_list.append(end_time - start_time)  # 记录每个epoch所用的训练时间
    net.eval()  # 转变为评估形态!
    valid_acc = torch.Tensor().to(device='cuda:0')  # 清空valid_acc!
    for i, (X_valid, y_valid) in enumerate(valid_iter):  # 来笔数据做iteration!
        X_valid, y_valid = X_valid.to(device='cuda:0'), y_valid.to(device='cuda:0')  # 把这笔数据丢显卡上!
        with torch.no_grad():  # 下面的两行代码不要记录梯度!
            acc_valid = (net(X_valid).argmax(dim=1) == y_valid).float().mean()  # 计算得到这一小笔验证数据的准确率!
            valid_acc = torch.cat([valid_acc, acc_valid.reshape(-1)])  # 记录在每个验证集batch上的准确率
    valid_acc_list.append(valid_acc.to(device="cpu").numpy().flatten().mean())  # 记录该epoch的全体验证样本上的准确率
    scheduler.step()  # 每个epoch打一下lr_scheduler
    if valid_acc_list[-1] > best_model_acc:
        best_model_acc = valid_acc_list[-1]
        torch.save(net.state_dict(), '/workspace/data/cifar10/best_model.ckpt')
        print('到达世界最高城——理塘!')
    print(f'epoch_{epoch}的验证精度为{valid_acc.to(device="cpu").numpy().flatten().mean():.3f}')

输出更多训练信息

print(f'看看速度:{(len(train_df.iloc[:, 0]) * epoch_num) / sum(timer_list)}')  # 看看速度
print(f'看看时间:{sum(timer_list)}')
print(valid_acc_list)

# 测试集

构造测试集图像数据迭代器

class custom_testset(data.Dataset):
    def __init__(self, df, image_dir, transform=None):
        self.data_frame = df
        self.image_dir = image_dir
        self.transform = transform

    def __len__(self):
        return len(self.data_frame)

    def __getitem__(self, idx):
        if torch.is_tensor(idx):
            idx = idx.to_list()
        img_name = os.path.join(self.image_dir, str(self.data_frame.iloc[idx, 0]) + '.png')
        image = Image.open(img_name)
        if self.transform:
            image = self.transform(image)
        return image

与之前读取训练集不同的是,这里只返回一个 image 。聪明的小伙伴可以把本帖中的两个数据方法结合为一个。(虽然上一个竞赛记录帖中也是这样说的

读取测试集数据

# 读取测试集csv
submission_df = pandas.DataFrame({
    'id': [i + 1 for i in range(300000)]
})
# 实例化测试集图像数据迭代器
submission_dataset = custom_testset(submission_df, image_dir='/workspace/data/cifar10/test', transform=trans)
submission_iter = data.DataLoader(submission_dataset, batch_size, shuffle=False)

加载最好的模型用于推理

best_net = models.resnext50_32x4d()
best_net.fc = nn.Sequential(nn.Linear(best_net.fc.in_features, 10))
best_net.load_state_dict(torch.load('/workspace/data/cifar10/best_model.ckpt'))
best_net.to(device='cuda:0')

推理,并将得到的编码数据解码,然后输出

# 推理
rslt_list = torch.Tensor().to(device='cuda:0')  # 用于在gpu上存放推理结果数据
for imgs in submission_iter:
    imgs = imgs.to(device='cuda:0')
    with torch.no_grad():
        rslt_list = torch.cat([rslt_list, net(imgs).argmax(dim=1)])
labels_decoded = label_encoder.inverse_transform(rslt_list.to(device='cpu').numpy().flatten().astype(int))  # 需要转int才能转回label
submission_df['label'] = labels_decoded
submission_df.to_csv('./submission.csv', index=False)

# 总结

优化器的选择很重要

\(\text{SGD}\)和\(\text{Adam}\)是两种用于训练神经网络的优化算法,它们各有优势和劣势。\(\text{SGD}\)是一种简单且直接的优化算法,它在每次迭代中沿着负梯度方向更新模型参数。尽管简单,但它可能会受到局部最小值的困扰,并且可能需要更多的调优才能收敛到最佳解。相比之下,\(\text{Adam}\)结合了动量概念和自适应学习率,可以更快地收敛到全局最优解。它能够自适应地调整每个参数的学习率,从而更有效地更新模型参数。但是,\(\text{Adam}\)也可能会在某些情况下过度拟合或者受到噪声的影响。在实践中,通常建议先尝试Adam,因为它在许多情况下表现良好。但是,如果你的数据集较小或者你遇到了一些收敛方面的问题,你可能需要尝试\(\text{SGD}\)或者其他优化算法来比较它们的性能。最终的选择取决于你的具体情况和实验结果。

但实际上\(\text{SGD}\)有点难调,\(\text{Adam}\)无脑用就完事了。

\(\text{epoch}\)太多可能导致过拟合

由于在这个数据集上跑太快了,因此设置了[latex]epoch=666[/latex],发现分数还不如[latex]epoch=10[/latex]的时候。

考虑如何分割训练集与验证集

如果不将全体数据集分割为训练集与验证集,而将全体数据集作为训练集,得到的验证精度可能会不准确。训练集分得太少,导致训练样本不足。

# 附录

将\(\textit{RGB}\)图片以及训练过程用 axis.imshow() 以及 axis.plot() 展示出来

train_acc = train_acc.to(device='cpu')
valid_acc = valid_acc.to(device='cpu')
images, labels = next(iter(train_iter))
fig, axes = pyplot.subplots(1, 2)
image_data = numpy.moveaxis(images[10].numpy(), 0, -1)  # 将通道维放到最后,并从Tensor转ndarray
# axes[0].imshow(images[10][0])  # 第11张图的第1个通道
axes[0].imshow(image_data)
axes[1].plot([i for i in range(len(train_acc))], train_acc, color='green')
axes[1].plot([i * (len(train_acc) / len(valid_acc_list)) for i in range(len(valid_acc_list))], valid_acc_list, color='red')
# axes[2].plot([i for i in range(len(l_list))], l_list, color='grey', linestyle='--')
pyplot.show()

QA

损失函数是凸函数吗?

损失函数是凸函数,主要是追求理论意义上能够求得最优解。但神经网络是非凸的,所以追求凸没太大意义。

什么样的\(\text{scheduler}\)最好?

本文中的\(\text{lr_scheduler}\)很常用。目前\(\cos\)函数比较好。

\(\text{lr_decay}\)和\(\text{weight_decay}\)差不多吗?

不一样。前者作用在优化器上;后者作用在统计模型上,即加了个正则化项。大家发现SGD效果好,因为SGD作用了很强的正则化,有很多噪音。

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments