以前总把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作用了很强的正则化,有很多噪音。