任务三:苹果病害模型训练与预测
- 难度/分值:中/2
打卡内容
- 参赛选手名称:AppleDoctor
- 完成日期:2023.6.11
- 任务完成情况:
- 使用的编程语言:Python,PyTorch
- 实现的功能:
- 自定义数据集读取
- 自定义CNN模型
- 模型训练与验证
- 对测试集进行预测
背景介绍
本次打卡任务是 Coggle 30 Days of ML 中的第三项任务。任务要求参赛选手利用提供的苹果病害数据集构建模型,并进行模型训练和预测。参赛选手可以选择合适的深度学习框架和模型架构,并使用训练集进行模型训练。然后,选手需要利用训练好的模型对测试集中的苹果叶片病害图像进行预测。
自定义数据集
首先,我们使用PyTorch
创建自定义数据集,以适应我们的任务。这里我定义了一个名为AppleDataset
的类。
# 定义Apple数据集的类 class AppleDataset(Dataset): def __init__(self, img_path, transform=None): """ 构造函数,初始化数据集的路径和数据增强操作 Args: - img_path: list类型,存储数据集中图像的路径 - transform: torchvision.transforms类型,数据增强操作 """ self.img_path = img_path self.transform = transform def __getitem__(self, index): """ 获取一个样本 Args: - index: 数据集中的索引 Returns: - img: Tensor类型,经过处理后的图像 - label: Tensor类型,标签 """ img = Image.open(self.img_path[index]) if self.transform is not None: img = self.transform(img) # 将类别名转换为数字标签 class_name = self.img_path[index].split('/')[-2] if class_name in ['d1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9']: label = ['d1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9'].index(class_name) else: label = -1 return img, torch.from_numpy(np.array(label)) def __len__(self): """ 获取数据集的大小 Returns: - size: int类型,数据集的大小 """ return len(self.img_path)
接下来,我们定义之前设置的数据增强方法,并应用于数据集。我们使用了以下数据增强操作:
调整图像大小:使用 transforms.Resize((224, 224)) 将图像大小调整为 224x224 像素。
随机水平翻转:使用 transforms.RandomHorizontalFlip() 以 0.5 的概率对图像进行水平翻转。
随机垂直翻转:使用 transforms.RandomVerticalFlip() 以 0.5 的概率对图像进行垂直翻转。
随机旋转:使用 transforms.RandomRotation(degrees=15) 以随机角度对图像进行旋转,角度范围为 -15 到 15 度。
转换为张量:使用 transforms.ToTensor() 将图像转换为张量。
归一化:使用 transforms.Normalize(mean, std) 对图像进行归一化处理,使用给定的均值和标准差。
# 定义图像预处理的参数 image_mean = [0.4940, 0.4187, 0.3855] image_std = [0.2048, 0.1941, 0.1932] # 定义训练集的数据增强操作 transform_train = transforms.Compose([ transforms.Resize((224,224)), # 将图像大小调整为224*224 transforms.RandomHorizontalFlip(), # 以0.5的概率对图像进行水平翻转 transforms.RandomVerticalFlip(), # 以0.5的概率对图像进行垂直翻转 transforms.RandomApply([transforms.GaussianBlur(kernel_size=3)], p=0.1), # 以0.1的概率对图像进行高斯模糊,卷积核大小为3 transforms.RandomApply([transforms.ColorJitter(brightness=0.9)],p=0.5), # 以0.5的概率调整图像的亮度,亮度因子的范围为[0.1, 1.9] transforms.RandomApply([transforms.ColorJitter(contrast=0.9)],p=0.5), # 以0.5的概率调整图像的对比度,对比度因子的范围为[0.1, 1.9] transforms.RandomApply([transforms.ColorJitter(saturation=0.9),],p=0.5), # 以0.5的概率调整图像的饱和度,饱和度因子的范围为[0.1, 1.9] transforms.ToTensor(), # 将图像转换成张量 transforms.Normalize(image_mean, image_std), # 对图像进行标准化处理 ]) # 定义验证集的数据预处理操作 transform_valid = transforms.Compose([ transforms.Resize((224,224)), # 将图像大小调整为224*224 transforms.ToTensor(), # 将图像转换成张量 transforms.Normalize(image_mean, image_std), # 对图像进行标准化处理 ])
然后,我们可以使用自定义数据集类来加载数据。
train_data = AppleDataset(train_path,transform_train) valid_data = AppleDataset(val_path,transform_valid) print("类别: {}".format(classes_name)) print("训练集: {}".format(len(train_data))) print("验证集: {}".format(len(valid_data)))
从输出结果可以看到,数据集大约有1万张图片,我将其以8:2的比例分为训练集和测试集,这样可以防止过拟合
类别: ['d1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9'] 训练集: 8165 验证集: 2046
接下来,使用PyTorch的DataLoader加载训练集和验证集的数据。你可以根据需要调整batch_size
和num_workers
参数。(win的num_workers
可能只能设为0,如果出现显存问题可以调小batch_size
)
# 使用PyTorch的DataLoader加载训练集数据 train_loader = torch.utils.data.DataLoader( train_data, batch_size=64, shuffle=True, num_workers= 8, pin_memory = True) val_loader = torch.utils.data.DataLoader( valid_data, batch_size=64, shuffle=False, num_workers= 8, pin_memory = True)
自定义CNN模型
在这个部分,我们将使用PyTorch Image Models (timm)库来快速构建CNN模型。timm是由Ross Wightman创建的深度学习库,集成了最先进的计算机视觉模型、图层、实用程序、优化器、调度程序、数据加载器、增强和训练/验证脚本。该库可以复现ImageNet训练结果,并且方便地用于构建模型并修改类别和通道等参数。
下面是一个使用timm构建resnet34模型的示例:
import timm # 使用timm库中的resnet34模型 model = timm.create_model('resnet34') # 生成一张224x224的RGB图像 x = torch.randn(1, 3, 224, 224) # 进行推理并打印输出的结果的形状 print(model(x).shape)
输出结果应为:
torch.Size([1, 1000])
输出的形状是正确的,是一个大小为[1, 1000]的张量,表示模型有1000个输出类别。然而,我们的数据集只有9个类别,并不是ImageNet的1000个类别。timm提供了一个参数来快速修改最后一个全连接层的类别数量。此外,我们还可以加载预训练的权重,以进行迁移学习。在这个例子中,我们使用resnet18作为示例,先确保整个流程正常运行:
model = timm.create_model('resnet18', num_classes=9, pretrained=True) x = torch.randn(1, 3, 224, 224) model(x).shape
输出结果应为:
Downloading model.safetensors: 100% 46.8M/46.8M [00:03<00:00, 17.2MB/s] torch.Size([1, 9])
如此一来,我们成功构建了一个resnet18模型,并将类别数设置为9。同时,我们还加载了预训练的权重,这有助于进行迁移学习。
如果需要了解更多关于timm的使用方法,请参考timm的文档和仓库:
timm文档:https://timm.fast.ai
timm仓库:https://github.com/rwightman/pytorch-image-models
模型训练与验证
在进行模型训练之前,我们首先要定义损失函数和优化器。在这个示例中,我们使用交叉熵损失函数和SGD优化器。
# 定义训练的超参数 epochs = 10 optimizer = torch.optim.SGD(model.parameters(), lr=0.05, momentum=0.9) # 初始化损失函数 if cuda: loss_fn = nn.CrossEntropyLoss().cuda() else: loss_fn = nn.CrossEntropyLoss()
接下来为了训练方便,定义了一个函数,用于计算模型的准确率:
def get_acc(outputs, label): """ 计算模型的准确率 Args: - outputs: Tensor类型,模型的输出 - label: Tensor类型,标签 Returns: - acc: float类型,模型的准确率 """ total = outputs.shape[0] probs, pred_y = outputs.data.max(dim=1) # 得到概率 correct = (pred_y == label).sum().data acc = correct / total return acc
然后,我们开始进行模型的训练,在每个epoch结束后,我们可以根据需要保存模型的状态,以便稍后进行推理或继续训练。
在训练过程中,我们还可以添加验证步骤来评估模型在验证集上的性能。
epoch_step = len(train_loader) if epoch_step == 0: raise ValueError("训练集过小,无法进行训练,请扩充数据集,或者减小batchsize") epoch_step_val = len(val_loader) if cuda: model.cuda() os.makedirs('checkpoint', exist_ok=True) best_acc = 0 for epoch in range(epochs): model.train() train_loss = 0 train_acc = 0 print('Start Train') with tqdm(total=epoch_step,desc=f'Epoch {epoch + 1}/{epochs}',postfix=dict,mininterval=0.3) as pbar: for step,(im,label) in enumerate(train_loader,start=0): with torch.no_grad(): if cuda: im = im.cuda() label = label.cuda() #----------------------# # 清零梯度 #----------------------# optimizer.zero_grad() #----------------------# # 前向传播forward #----------------------# outputs = model(im) #----------------------# # 计算损失 #----------------------# loss = loss_fn(outputs,label) #----------------------# # 反向传播 #----------------------# # backward loss.backward() # 更新参数 optimizer.step() train_loss += loss.data train_acc += get_acc(outputs,label) lr = optimizer.param_groups[0]['lr'] # 在合适的位置添加以下代码 pbar.set_postfix(**{ 'Train Loss': "{:.6f}".format(train_loss.item() / (step + 1)), 'Train Acc': "{:.6f}".format(train_acc.item() / (step + 1)), 'Lr': "{:.6f}".format(lr), 'Memory': "{:.3g}G".format(torch.cuda.memory_reserved() / 1E9) }) pbar.update(1) train_loss = train_loss.item() / len(train_loader) train_acc = train_acc.item() * 100 / len(train_loader) acc = train_acc print("Epoch: {}, Train Loss: {:.6f}, Train Acc: {:.6f}".format(epoch+1, train_loss, train_acc)) state = { 'net': model.state_dict(), 'acc': acc, 'epoch': epoch+1, 'optimizer': optimizer.state_dict(), } torch.save(state, './checkpoint/last_resnet34_ckpt.pth') # 模型保存路径 if epoch_step_val != 0: model.eval() val_loss = 0 val_acc = 0 print('Start Val') #-------------------------------- # 相同方法,同train #-------------------------------- with tqdm(total=epoch_step_val,desc=f'Epoch {epoch + 1}/{epochs}',postfix=dict,mininterval=0.3) as pbar2: for step,(im,label) in enumerate(val_loader,start=0): with torch.no_grad(): if cuda: im = im.cuda() label = label.cuda() #----------------------# # 前向传播 #----------------------# outputs = model(im) loss = loss_fn(outputs,label) val_loss += loss.data val_acc += get_acc(outputs,label) pbar2.set_postfix(**{'Val Loss': "{:.6f}".format(val_loss.item() / (step + 1)), 'Val Acc': "{:.6f}".format(val_acc.item()/(step+1)), 'Memory': "{:.3g}G".format(torch.cuda.memory_reserved() / 1E9) }) pbar2.update(1) lr = optimizer.param_groups[0]['lr'] val_acc = val_acc.item() * 100 / len(val_loader) val_loss = val_loss.item() / len(val_loader) acc = val_acc print("Epoch: {}, Val Loss: {:.6f}, Val Acc: {:.6f}".format(epoch+1, val_loss, val_acc)) # Save checkpoint. if acc > best_acc: print('Saving Best Model...') state = { 'net': model.state_dict(), 'acc': acc, 'epoch': epoch+1, 'optimizer': optimizer.state_dict(), } torch.save(state, './checkpoint/best_resnet34_ckpt.pth') best_acc = acc
Start Train Epoch 1/10: 100%|██████████| 128/128 [00:18<00:00, 7.06it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.549712, Train Loss=1.246984] Epoch: 1, Train Loss: 1.246984, Train Acc: 54.971230 Start Val Epoch 1/10: 100%|██████████| 32/32 [00:05<00:00, 5.92it/s, Memory=1.93G, Val Acc=0.765074, Val Loss=0.703293] Epoch: 1, Val Loss: 0.703293, Val Acc: 76.507372 Saving Best Model... Start Train Epoch 2/10: 100%|██████████| 128/128 [00:17<00:00, 7.37it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.745348, Train Loss=0.759731] Epoch: 2, Train Loss: 0.759731, Train Acc: 74.534816 Start Val Epoch 2/10: 100%|██████████| 32/32 [00:04<00:00, 7.71it/s, Memory=1.93G, Val Acc=0.839828, Val Loss=0.468506] Epoch: 2, Val Loss: 0.468506, Val Acc: 83.982801 Saving Best Model... Start Train Epoch 3/10: 100%|██████████| 128/128 [00:17<00:00, 7.48it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.809956, Train Loss=0.566048] Epoch: 3, Train Loss: 0.566048, Train Acc: 80.995631 Start Val Epoch 3/10: 100%|██████████| 32/32 [00:04<00:00, 6.73it/s, Memory=1.93G, Val Acc=0.873488, Val Loss=0.382040] Epoch: 3, Val Loss: 0.382040, Val Acc: 87.348789 Saving Best Model... Start Train Epoch 4/10: 100%|██████████| 128/128 [00:19<00:00, 6.66it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.839854, Train Loss=0.477816] Epoch: 4, Train Loss: 0.477816, Train Acc: 83.985364 Start Val Epoch 4/10: 100%|██████████| 32/32 [00:05<00:00, 6.09it/s, Memory=1.93G, Val Acc=0.887664, Val Loss=0.329272] Epoch: 4, Val Loss: 0.329272, Val Acc: 88.766378 Saving Best Model... Start Train Epoch 5/10: 100%|██████████| 128/128 [00:17<00:00, 7.33it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.862014, Train Loss=0.408341] Epoch: 5, Train Loss: 0.408341, Train Acc: 86.201435 Start Val Epoch 5/10: 100%|██████████| 32/32 [00:04<00:00, 6.64it/s, Memory=1.93G, Val Acc=0.924757, Val Loss=0.251357] Epoch: 5, Val Loss: 0.251357, Val Acc: 92.475742 Saving Best Model... Start Train Epoch 6/10: 100%|██████████| 128/128 [00:17<00:00, 7.43it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.876175, Train Loss=0.372916] Epoch: 6, Train Loss: 0.372916, Train Acc: 87.617451 Start Val Epoch 6/10: 100%|██████████| 32/32 [00:04<00:00, 6.86it/s, Memory=1.93G, Val Acc=0.908628, Val Loss=0.268046] Epoch: 6, Val Loss: 0.268046, Val Acc: 90.862840 Start Train Epoch 7/10: 100%|██████████| 128/128 [00:19<00:00, 6.54it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.881424, Train Loss=0.353227] Epoch: 7, Train Loss: 0.353227, Train Acc: 88.142353 Start Val Epoch 7/10: 100%|██████████| 32/32 [00:05<00:00, 6.40it/s, Memory=1.93G, Val Acc=0.936980, Val Loss=0.213115] Epoch: 7, Val Loss: 0.213115, Val Acc: 93.698019 Saving Best Model... Start Train Epoch 8/10: 100%|██████████| 128/128 [00:18<00:00, 6.91it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.890658, Train Loss=0.313763] Epoch: 8, Train Loss: 0.313763, Train Acc: 89.065802 Start Val Epoch 8/10: 100%|██████████| 32/32 [00:05<00:00, 5.44it/s, Memory=1.93G, Val Acc=0.924301, Val Loss=0.221276] Epoch: 8, Val Loss: 0.221276, Val Acc: 92.430067 Start Train Epoch 9/10: 100%|██████████| 128/128 [00:17<00:00, 7.28it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.900245, Train Loss=0.303409] Epoch: 9, Train Loss: 0.303409, Train Acc: 90.024549 Start Val Epoch 9/10: 100%|██████████| 32/32 [00:05<00:00, 6.01it/s, Memory=1.93G, Val Acc=0.926207, Val Loss=0.237639] Epoch: 9, Val Loss: 0.237639, Val Acc: 92.620653 Start Train Epoch 10/10: 100%|██████████| 128/128 [00:18<00:00, 6.80it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.907415, Train Loss=0.280949] Epoch: 10, Train Loss: 0.280949, Train Acc: 90.741462 Start Val Epoch 10/10: 100%|██████████| 32/32 [00:06<00:00, 5.20it/s, Memory=1.93G, Val Acc=0.944808, Val Loss=0.167078] Epoch: 10, Val Loss: 0.167078, Val Acc: 94.480848 Saving Best Model...
可以看到最后验证集也可以达到94.5的准确率左右,还可以继续加油冲冲冲
对测试集进行预测
接下来,我们使用训练好的模型对测试集进行预测,并生成最终的CSV文件以进行提交。
首先,定义一个函数来进行模型的推理。
from tqdm import tqdm def predict(test_loader, model): """ 进行模型的推理 Args: - test_loader: DataLoader类型,测试集数据加载器 - model: 模型 Returns: - test_pred: numpy数组类型,模型的输出结果 """ # 将模型设为评估模式 model.eval() test_pred = [] with torch.no_grad(): # 遍历测试集数据 for input, _ in tqdm(test_loader): input = input.cuda() # 进行模型的推理 output = model(input) test_pred.append(output.data.cpu().numpy()) # 将模型的输出结果转换为numpy数组类型 return np.vstack(test_pred)
接下来,加载测试集数据并进行重复10次的模型推理,并将每次推理的结果相加,这样相当于10个模型进行集成,不过是自己和自己集成。
# 获取测试集数据的路径 test_path = glob.glob(f'{root}/test/*') # 加载测试集数据 test_data = AppleDataset(test_path, transform_valid) # 使用PyTorch的DataLoader加载测试集数据 test_loader = torch.utils.data.DataLoader( test_data, batch_size=1, shuffle=False, num_workers= 0, pin_memory = True) pred = None print("-----------------Repeat 10 times-----------------") # 进行10次模型推理,并将结果相加 for _ in range(10): if pred is None: pred = predict(test_loader, model) else: pred += predict(test_loader, model)
最后,将推理结果保存在一个CSV文件中,以便提交到比赛平台。
# 创建包含标签的DataFrame submit = pd.DataFrame( { 'uuid': [x.split('/')[-1] for x in test_path], 'label': [['d1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9'][x] for x in pred.argmax(1)] } ) # 生成CSV文件的时间戳 timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") # 保存包含标签的DataFrame label_csv_file = f'submit/predictions_{timestamp}.csv' submit = submit.sort_values(by='uuid') submit.to_csv(label_csv_file, index=None) # 打印保存路径 print("Predictions saved to: {}".format(label_csv_file))
总结
在这一部分,我已顺利完成了苹果病害模型的训练与预测任务。使用Python和PyTorch作为编程语言,并根据需求自定义了一个名为AppleDataset的数据集类。在模型训练方面,我采用了自定义的CNN模型,并利用PyTorch的DataLoader加载了训练集和验证集的数据。此外,我还运用了PyTorch Image Models (timm)库来快速构建CNN模型,并使用了交叉熵损失函数和SGD优化器进行模型训练与验证。
完成这一任务后,我会进一步优化和改进模型。例如,可以尝试采用更复杂的模型架构、调整超参数或尝试使用不同的优化算法。此外,还可以尝试其他的数据增强方法,以提高模型的泛化能力。