试一试捏。
# 序
使用残差网络做树叶分类。
# 数据来源与预处理
在此处下载训练集与测试集。这是包含[latex]176[/latex]类叶子的图像数据。
构造图像数据迭代器
训练数据解压得到一个含有大量图片数据的[latex]\textbf{images文件夹}[/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, self.data_frame.iloc[idx, 0].split('/')[1])
image = Image.open(img_name)
label = self.data_frame.iloc[idx, 1]
if self.transform:
image = self.transform(image)
return image, label
其中因为记录了包含文件夹名的相对路径,与 image_dir 冗余,因此使用了 .split() 。
该方法可用来读取非测试集的图像数据,因为同时返回了 image 和 label 。
给标签编号,然后将 train.csv 分为训练集和验证集,并实例化训练集和训练集数据迭代器
# 读取train.csv为dataframe
data_df = pandas.read_csv('./leaves_class/train.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.3, shuffle=True)
# 数据转换
trans = transforms.Compose([transforms.Resize(224), transforms.ToTensor()])
trans_train = transforms.Compose([
transforms.Resize(224),
transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转
transforms.RandomVerticalFlip(p=0.5), # 除了水平竖直反转之外其他的处理方法貌似都会降低acc
transforms.ToTensor(),
])
## 构建数据迭代器
train_dataset = custom_dateset(train_df, image_dir='./leaves_class/images', transform=trans)
valid_dataset = custom_dateset(valid_df, image_dir='./leaves_class/images', transform=trans)
train_iter = data.DataLoader(train_dataset, batch_size, shuffle=True, num_workers=6)
num_batch = len(train_iter) # 得到该batch_size下有多少个batch
valid_iter = data.DataLoader(valid_dataset, batch_size, shuffle=True, num_workers=6)
注:自行调整[latex]\textit{num_workers}[/latex]。
# 训练
此处使用 PyTorch 提供的现成模型 ResNet-34 进行图像分类,最后的全连接层输出通道为叶子的总类数[latex]176[/latex]。
设定超参数并定义模型,初始化模型参数
lr, batch_size = 0.0001, 64
weight_decay = 0.001
epoch_num = 50
net = models.resnet34(weights=ResNet34_Weights.IMAGENET1K_V1)
net.fc = nn.Sequential(nn.LazyLinear(176)) # 定义最后的全连接层
# net.fc = nn.Sequential(nn.Linear(net.fc.in_features, 176))
# nn.init.xavier_uniform_(net.fc[0].weight)
net.to(device='cuda:0')
定义优化器和损失函数
optimizer = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=weight_decay)
loss = nn.CrossEntropyLoss()
注:优化器使用的是[latex]\textit{Adam}[/latex]。
开始训练
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的全体验证样本上的准确率
if valid_acc_list[-1] > best_model_acc:
best_model_acc = valid_acc_list[-1]
torch.save(net.state_dict(), './leaves_class/best_model.ckpt')
print('到达世界最高城——理塘!')
print(f'epoch_{epoch}的验证精度为{valid_acc.to(device="cpu").numpy().flatten().mean():.3f}')
注意:在训练集和验证集上计算精度时,一定不要记录梯度。这里使用了\(\textit{with torch.no_grad()}\)。
打印结果
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, self.data_frame.iloc[idx, 0].split('/')[1])
image = Image.open(img_name)
if self.transform:
image = self.transform(image)
return image
与之前读取训练集不同的是,这里只返回一个 image 。聪明的小伙伴可以把本帖中的两个数据集方法结合为一个。
读取测试集数据
# 读取测试集csv
submission_df = pandas.read_csv('./leaves_class/test.csv', header=0)
# 实例化测试集图像数据迭代器
submission_dataset = custom_testset(submission_df, image_dir='./leaves_class/images', transform=trans)
submission_iter = data.DataLoader(submission_dataset, batch_size, shuffle=False)
加载最好的模型用于推理
best_net = models.resnet34()
best_net.fc = nn.Sequential(nn.LazyLinear(176))
best_net.load_state_dict(torch.load('./leaves_class/best_model.ckpt'))
best_net.to(device='cuda:0')
需要注意的是,用于推理的模型与之前的模型在结构上必须一致。如之前的全连接输出层使用了\(\textit{nn.Sequential()}\)将其包裹,那么这里的\(\textbf{ best_net }\)也需要使用一个\(\textit{nn.Sequential()}\)将其包裹。
推理,并将得到的编码数据解码,然后输出
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)
# 总结
神经网络架构也会影响训练集准确率
尽管[latex]\textit{ResNet34}[/latex]和[latex]\textit{ResNeXt}[/latex]在训练集上的准确率均能到达[latex]100\%[/latex],但是两者在训练集上的表现却有较大差异。
在固定[latex]epoch=100[/latex],[latex]alpha=0.0001[/latex],[latex]batch_size=64[/latex],[latex]weight_decay=0.001[/latex]的情况下,[latex]\textit{ResNet34}[/latex]在训练集上的准确率最高只有[latex]88\%[/latex],而[latex]\textit{ResNeXt}[/latex]有[latex]95\%[/latex]。
使用预训练的网络极大加速收敛速度
from torchvision import models
from torchvision.models import resnext50_32x4d, ResNeXt50_32X4D_Weights
net = models.resnext50_32x4d(weights=ResNeXt50_32X4D_Weights.IMAGENET1K_V1)
net = models.resnext50_32x4d(pretrained=True) # 已弃用。等价于上面的代码
# 附录
将\(\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, 3)
image_data = numpy.moveaxis(images[10].numpy(), 0, -1) # 将通道维放到最后,并从Tensor转ndarray
axes[0].imshow(images[10][0]) # 第11张图的第1个通道
axes[1].imshow(image_data)
axes[2].plot([i for i in range(len(train_acc))], train_acc, color='green')
axes[2].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
增加数据和调参对泛化性的影响?
很多时候增加高质量数据对提升泛化性比调参有用。过多调参也只会拟合当前数据,在业务场景可能会不断增加数据。所以调参调到还行就可以了。
为什么\(w-=lr*w.grad\)可以而\(w=w-lr*w.grad\)梯度会消失?
因为后者的[latex]w[/latex]是一个新的 Tensor ,梯度没了。
为什么精度在\(0.7\)就不动了,是没做\(\text{weight_decay}\)吗?
\(\text{weight_decay}\)可能把精度从\(0.7\)提升到\(0.8\),从\(0.9\)提升到\(0.95\)。但不可能从\(0.7\)提到\(0.99\)。
工业界与学术界不同
工业界更关注数据,可能选一个稳定的模型,先调一个过得去的参数,然后不断补充新数据。之后再固定超参数,补充数据。一年调一两次参数或更久更换一次模型。而学术界更像竞赛,固定数据,换模型调参。
技术分析
改进策略 | |
---|---|
1 | 数据增强,在测试时多次使用稍弱的增强然后取平均 |
2 | 使用多个模型,最后结果加权平均 |
3 | 使用更合适的算法和学习率 |
4 | 清理数据,实践中有大量同一图片具有不同标号,需要清理 |
1. 手动去除重复图片; 2. 树叶没有方向性,且图片背景较多。可以做更强的增强,如随机旋转和更大的裁剪; 3. 跨图片增强。可以使用\(\textbf{Mixup}\)随机叠加两张图片,也可以采用\(\textbf{CutMix}\)随机组合来自不同图片的块。数据方面
1. 模型多为\(\textbf{ResNet}\)变种。如\(\textbf{DenseNet}\)、\(\textbf{ResNeXt}\)、\(\textbf{ResNeSt}\)、\(\textbf{EfficientNet}\)等; 2. 优化算法多为\(\textbf{Adam}\)及其变种; 3. 学习率一般是\(\cos\)函数,或训练不动时往下调。如采用\(\textbf{lr_scheduler}\)。模型方面