【李沐 | 动手学深度学习】 22 Classify-Leaves 任务实现

📅 2026/7/15 19:30:41
【李沐 | 动手学深度学习】 22 Classify-Leaves 任务实现
这是一个五年前的竞赛居然如此久远~~~前段时间忙着毕设提交一堆报告终于结束啦~~~啊啊没办法笔者只能单线程工作。。。1 竞赛介绍任务是预测叶子图像的类别。该数据集包含176个类别18353张训练图像8800张测试图像。每个类别至少有50张训练图像。测试集平均分为公开排行榜和私有排行榜。ImageNet 中有包含大量树叶图像我们采用迁移学习策略——用 ImageNet 上预训练好的模型 resnet50 作为基础模型具备通用的图像数据特征学习能力按理来说可以达到不错的效果。2 代码实现2.1 ResNet 50 Baseline1) 全局设置导入要用到的包import torch import torch.nn as nn import torch.optim as optim import pandas as pd import os from PIL import Image from torch.utils.data import Dataset, DataLoader from torchvision import transforms, models from tqdm import tqdm from sklearn.model_selection import train_test_split from Kaggle.network import PlantDataset设置全局配置 CONFIGresnet_model下载地址https://www.kaggle.com/datasets/tsingxue/resnet50-0676ba61-pth也可以在线下载由于csv文件中的 image列图像名称已经包含“imaegs”字段因此这里设置的img_root为数据集根目录。# 全局配置 CONFIG { img_root: ./data/classify-leaves, train_csv: ./data/classify-leaves/train.csv, resnet_model: resnet50-0676ba61.pth, model_path: best_model.pth, batch_size: 64, # 稍微减小 batch_size 有助于收敛 epochs: 10, # 建议至少 10 个 Epoch 看看效果 learning_rate: 1e-4, num_workers: 4, device: torch.device(cuda if torch.cuda.is_available() else cpu) }2) LeavesDataset 类定义为了方便读取数据这里定义了 LeavesDataset 数据集类# 数据集类定义 class LeavesDataset(Dataset): def __init__(self, df, img_dir, transformNone, class_to_idxNone): self.df df self.img_dir img_dir self.transform transform # 标签处理给标签名映射编号 label_col df.columns[1] if class_to_idx is None: self.classes sorted(df[label_col].unique()) self.class_to_idx {cls_name: i for i, cls_name in enumerate(self.classes)} else: self.class_to_idx class_to_idx def __len__(self): return len(self.df) def __getitem__(self, idx): img_name self.df.iloc[idx][0] img_path os.path.join(self.img_dir, img_name) image Image.open(img_path).convert(RGB) label self.class_to_idx[self.df.iloc[idx][1]] # 图像增广处理 if self.transform: image self.transform(image) return image, label3) 数据集划分为了提升模型的鲁棒性对图像做增广处理# 图像增广处理 # 训练集加入随机性数据增强 train_transform transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) # 验证集保持固定模拟真实推理环境 val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])train.csv按73划分训练集和验证集并采用分层抽样策略确保训练集与验证集数据分布一致。shuffle 的设置训练集的 shuffleTrue目的是防止模型“作弊”或产生“记忆倾向”。原理如果数据按某种顺序排列比如前100个是枫叶后100个是银杏不打乱的话模型在每个 Batch 里只能看到单一品种。这会导致梯度下降的方向极其不稳定甚至让模型记住数据的顺序而不是特征。打乱的作用确保每个 Batch 的数据分布都是随机且均匀的让模型通过随机样本学习到更具泛化性的特征。验证集的 shuffleFalse的目的是保证评估的稳定性和可比性。原理验证集是用来“考试”的。对于考试来说题目顺序并不影响得分也没必要打乱。实际用途如果不打乱数据顺序固定我们可以很方便地对比模型在不同 Epoch 对同一张特定图片的预测结果。此外在生成最终预测报告如提交 Kaggle 结果时固定的顺序可以确保预测结果与文件名一一对应。pin_memory 是什么另外此处设置的 pin_memoryTrue —— 这是一个提升数据传输效率的“加速开关”。另外这里stratify1表示分层抽样在训练集和验证集分别放置类别成比例的数据量比如73——类别为x的数据共有10个就在训练集和验证集分配数量7、3使得数据分布具有一般性。# 划分数据 full_df pd.read_csv(CONFIG[train_csv]) train_df, val_df train_test_split(full_df, test_size0.3, random_state42, stratifyfull_df.iloc[:, 1]) # 准备 Loader # 建立统一的类别映射 leaves_ds LeavesDataset(full_df, CONFIG[img_root]) class_to_idx leaves_ds.class_to_idx num_classes len(leaves_ds.classes) # 数据加载器 train_loader DataLoader(LeavesDataset(train_df, CONFIG[img_root], train_transform, class_to_idx), batch_sizeCONFIG[batch_size], shuffleTrue, num_workersCONFIG[num_workers], pin_memoryTrue) val_loader DataLoader(LeavesDataset(val_df, CONFIG[img_root], val_transform, class_to_idx), batch_sizeCONFIG[batch_size], shuffleFalse, num_workersCONFIG[num_workers], pin_memoryTrue)4) 网络定义使用预训练模型采用全量微调注意修改最后一层的输出维度。# 模型定义 # net models.resnet50(pretrainedTrue) net models.resnet50() net.load_state_dict(torch.load(CONFIG[resnet_model])) net.fc nn.Linear(net.fc.in_features, num_classes) net net.to(deviceCONFIG[device])5) 训练- 单个 epoch 计算下面是一个 epoch的处理对于 train 阶段需要设置 model.train()以及梯度计算对于 test 阶段设置 model.eval()最终返回平均损失和准确率。# 一个 epoch 的处理 def run_epoch(model, loader, criterion, optimizer): is_train optimizer is not None # 训练/推理 model.train() if is_train else model.eval() total_loss 0.0 correct, total 0, 0 pbar tqdm(loader, leaveFalse) # 进度条 with torch.set_grad_enabled(is_train): for images, labels in pbar: images, labels images.to(CONFIG[device]), labels.to(CONFIG[device]) outputs model(images) # 预测 loss criterion(outputs, labels) # 计算损失 # 训练模式下计算梯度 if is_train: optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() correct (outputs.argmax(1) labels).sum().item() total labels.size(0) # 平均损失准确率 return total_loss / len(loader), correct / total- 绘图函数这里定义了一个绘图函数将训练过程中的 train_acc、test_acc记录下来import matplotlib.pyplot as plt # 新增引用 def plot_history(history): epochs range(1, len(history[train_loss]) 1) plt.figure(figsize(12, 5)) # 子图1Loss 变化 plt.subplot(1, 2, 1) plt.plot(epochs, history[train_loss], bo-, labelTraining Loss) plt.title(Training Loss) plt.xlabel(Epochs) plt.ylabel(Loss) plt.legend() # 子图2准确率变化 plt.subplot(1, 2, 2) plt.plot(epochs, history[train_acc], ro-, labelTrain Acc) plt.plot(epochs, history[val_acc], go-, labelVal Acc) plt.title(Training and Validation Accuracy) plt.xlabel(Epochs) plt.ylabel(Accuracy) plt.legend() plt.tight_layout() plt.savefig(training_results.png) # 自动保存为图片 plt.show()- 训练主循环迭代 num_epochs次每次 batchsize 的批量每一轮验证一次模型效果保留表现最好的模型。def func(net, train_loader, val_loader, criterion, optimizer): best_acc 0.0 # --- 1. 初始化记录列表 --- history { train_loss: [], train_acc: [], val_acc: [] } for epoch in range(CONFIG[epochs]): # 训练模型 train_loss, train_acc run_epoch(net, train_loader, criterion, optimizer) # 验证模型 v_loss, v_acc run_epoch(net, val_loader, criterion, None) # --- 2. 记录数据 --- history[train_loss].append(train_loss) history[train_acc].append(train_acc) history[val_acc].append(v_acc) print(fEpoch {epoch 1:02d} | Train Loss: {train_loss:.3f} | Val Acc: {v_acc:.2%}) # 留下表现最好的模型 if v_acc best_acc: best_acc v_acc torch.save(net.state_dict(), CONFIG[save_path]) print(f⭐ Model Saved!) # --- 3. 绘制可视化图表 --- plot_history(history) print(f\n训练结束最高验证集准确率: {best_acc:.2f}%)使用交叉熵损失并且设置了标签平滑 (Label Smoothing)为0.1.这样做的原因是默认的分类让模型去“死记硬背”某个标签是 1.0其他是 0。这容易导致模型过度自信产生过拟合。标签平滑会让目标变成类似 0.9留出 0.1 给其他类提高容错性。# 损失函数/优化器设置 criterion nn.CrossEntropyLoss(label_smoothing0.1) # optimizer optim.Adam(net.parameters(), lrCONFIG[learning_rate]) optimizer optim.Adam([ {params: model.backbone.parameters(), lr: 1e-5}, {params: model.fc.parameters(), lr: 1e-4} ]) # 训练 func(net, train_loader, val_loader, criterion, optimizer)//输出对于下面结果训练的时候优化器直接用的统一学习率lr1e-4之前没注意到因为训练一次时间-25min左右太久了就不重来一遍了随着迭代次数的增加train_loss下降逐渐缓慢而准确率逐渐提升有意思的是 验证集的准确率始终比训练集的高说明模型的泛化性能不错可以到90%以上整体性能还可以。// kaggle 提交结果如下之后主包加载本地模型的时候没注意设置为了之前保存好的 best_model再次训练约等于继续“微调”-其实可以说相当于再跑了10个epoch准确度可以到94%引入5折交叉验证 (5-Fold Cross Validation)是提升模型稳定性和评估准确性的重要手段。核心逻辑变化将整个训练集平均分成 5 份Folds。每次取其中 1 份做验证其余 4 份做训练。这样会训练出 5 个模型最终的性能是这 5 次结果的平均值。本来想要引入这个跑一下的但是时间太长用kaggle跑了两轮都中途失败了设备不行啊旨在理解这个过程吧~------------------------------------------------------------二编沐老师的树叶分类竞赛技术总结43 树叶分类竞赛技术总结【动手学深度学习v2】_哔哩哔哩_bilibili(1) 技术分析相较于课程介绍的代码大家做的增强如下① 数据增强中心点crop、四角crop、旋转...在测试时多次使用稍弱的增强然后取平均② 使用多个模型预测最后结果加权平均③ 训练算法和学习率④ 清理数据数据本身存在噪声同一张图片对应多个标签有歧义的数据清理掉。(2) 数据方面跨图片增强传统的图像增强如旋转、裁剪、色彩抖动都是在单张图片内部进行的。而跨图片则是打破单张图片的限制将两张或多张不同的图片以某种方式融合在一起生成全新的训练样本。这种方法在现代深度学习尤其是全监督、半监督学习和各类CV比赛中被广泛应用。Mixup和CutMix是两种经典的跨图片增强技术。① Mixup概念Mixup是由Facebook AI Research 在2018年提出的一种极其简单却异常强大的像素级混合方法。核心原理Mixup通过对两张随机选择的输入图片以及它们对应的标签进行线性插值按比例融合从而产生新的样本。//示例假设我们随机抽取了图片A一张猫的图片和图片B一张狗的图片它们的像素矩阵分别为、标签One-hot编码分别为、。通过从一个β分布中随机采样的比例因子生成新样本的公式如下直观理解生成的图片看起来就像是两张图形成了“半透明的重叠影”类似摄影中的双重曝光。标签变化如果那么新图片看起来70%像猫30%像狗。它的标签也不再是绝对的0/1而是软标签。Mixup为什么有效① 平滑决策边界传统的训练让模型在“猫”和“狗”之间画一条非黑即白的硬边界。Mixup强迫模型在中间过渡地带也做出线性的预测从而增强模型的泛化能力。② 抗噪与防过拟合它充当了一种极强的正则化手段能有效缓解模型对错误标签的敏感度。③ 增加数据数量任意两张图片组合使得理论组合数量巨大。② CutMix尽管Mixup很强但存在明显的缺点融合后的图片往往虚无缥缈不符合现实世界的物理规律有时会误导模型。为了解决这个问题2019年诞生了 CutMix。核心原理CutMix直接“剪切与粘贴”——从图片A中随机裁剪掉一个矩形区域然后用图片B对应位置的区域把它填满覆盖。同样它的标签融合比例取决于剪切区域的面积占整张图片的比例直观理解一张图绝大部分是“猫”但中间正方形盒子里突然变成了“狗的毛皮”或“狗头”。边界是清晰的没有半透明融合。标签变化如果狗的区域占了整张图的 20%那么新标签就是Cutup为什么有效① 强迫模型关注局部特征传统的分类网络很容易偷懒比如分类一辆车它可能只看车轮。如果用CutMix把车轮裁剪掉替换成了别的模型就不得不去寻找车窗、车灯等其他微弱特征来判断。这大大提升了模型的特征定位能力。② 无信息浪费相比于另一种 Cutout直接挖掉一块的技术CutMix用有意义的图片信息填补了空缺训练效率更高。实战Tips在 Kaggle 等图像竞赛中Mixup 和 CutMix 几乎是标配。通常的做法是在训练每一个 Batch 时以一定的概率例如p0.5随机触发这两种增强中的一种。由于它们改变了标签形式变成了 Soft Label在计算损失函数时需要配合支持软标签的交叉熵损失Cross Entropy Loss进行训练。(3) 模型方面一、ResNet变种自从 2015 年 ResNet 诞生并解决深层网络退化问题后它就成了 CV 界的“通用骨架”。后面演化出的一系列变种核心目的都是在不显著增加计算量的前提下进一步提升模型的特征提取能力和泛化性。1. DenseNet密集连接网络核心思想传统的 ResNet 是跨层“相加”而 DenseNet 则是跨层“拼接”Concatenate。在 DenseNet 的一个 Block 内每一层都直接与它后面的所有层相连。优势极大地加强了特征传播鼓励特征复用网络在深层依然能直接获取到浅层的低级特征如边缘、纹理在小数据集上表现尤为出色。2. ResNeTx多分支ResNet核心思想引入了基数Cardinality的概念。传统的 ResNet 是单通道通过卷积层而 ResNeXt 把通道分成了多个并行的“小组”称为 Group Convolution分组卷积每个小组独立进行卷积最后再拼接起来。优势证明了“增加网络的并行分支数基数比单纯增加网络深度或宽度更能提升模型准确率”性价比极高。3. ResNeSt分裂注意力网络核心思想由亚马逊张航等人在 2020 年提出将注意力机制Attention无缝融合到了 ResNet 的分组卷积中设计了Split-Attention分裂注意力块。优势它在当时横扫了多个视觉任务是打比赛的“屠榜神器”提取全局和局部混合特征的能力极强。4. EfficientNet (高效网络)核心思想过去大家改进网络很盲目要么调深、要么调宽、要么提高输入分辨率。EfficientNet 提出了一种复合缩放Compound Scaling方法用一个特定的比例系数同时、均衡地去缩放深度、宽度和分辨率。优势精度奇高且参数量和计算量FLOPs极小。对于算力受限的场景EfficientNet 往往是首选。二、优化算法在深度学习中优化算法Optimizer决定了模型如何根据梯度去更新权重。1. 传统 SGD (随机梯度下降) 与 Adam 的权衡SGD (with Momentum)像一个在山谷里滚落的铁球。虽然训练收敛慢需要精细调参但一旦收敛泛化能力极强往往能在测试集上拿到最高的分数。Adam (自适应矩估计)结合了动量和自适应学习率。它能为每个参数动态调整学习率。最大的特点是“省心”即使学习率设得不是最完美它也能快速收敛。2. 现代变种AdamW最推荐一种典型的变种是AdamW。传统的 Adam 在加入正则化权重衰减 Weight Decay时由于自适应梯度的干扰其效果等同于失效。AdamW 改变了计算顺序将权重衰减从梯度更新中剥离出来直接作用于权重本身。这使得它既有 Adam 的快速收敛又具备了 SGD 的强泛化性。目前绝大多数 Vision Transformer (ViT) 和现代卷积神经网络默认都用 AdamW。三、学习率策略学习率Learning Rate, LR是训练神经网络最敏感的超参数。一成不变的学习率是无法训练出好模型的必须进行“衰减”LR Decay。1. Cosine (余弦退火衰减)原理学习率按照余弦函数曲线缓慢下降。在训练初期学习率较大有时会配合几个 Epoch 的 Warmup 预热在中后期学习率像滑滑梯一样平滑下降到训练末期学习率变得非常微小让模型在最优解附近进行微调。为什么好用相比于生硬的阶梯式下降余弦退火更加平滑能让模型更自然地滑入并锁定局部最小值。2. 训练不动时往下调 (ReduceLROnPlateau)原理监控一个指标如验证集 Loss 或 Accuracy。如果连续 N 个 Epoch称为 patience 容忍度该指标都没有提升就证明模型卡在瓶颈期“训练不动了”此时手动或自动将学习率乘以一个因子如 0.1强行下调。为什么好用非常符合人类直觉。当大步走无法前进时原地踏步几次然后改成“迈小步”继续探索。(4) AutoGluonAutoGluon是由AWS开源的深度学习自动化工具箱AutoML。它的核心理念是“极其简单、极其强大”。相比于其他 AutoML 框架AutoGluon 的独特之处在于它放弃了传统“耗时且不一定有效”的超参数搜索HPO转而采用多层模型集成Ensemble与堆叠Stacking技术在短时间内就能刷爆 Kaggel 等数据竞赛的排行榜。一、核心设计理念与优势在过去机器学习管道Pipeline的搭建极其繁琐特征工程 - 模型选择 - 超参数调优 - 模型融合每一个环节都需要资深专家的经验。AutoGluon 将这一切彻底自动化并带来以下优势真正的一键式驱动只需要一行fit()自动完成端到端的所有工作。不搞虚的超参数搜索传统的 AutoML 把 90% 的时间花在调整 SVM 或是 XGBoost 的微小参数上。AutoGluon 认为“与其把时间花在压榨单个模型的极限不如把时间花在训练更多不同类型的模型并融合它们”。多模态支持完美支持表格数据Tabular、文本Text、图像Image以及多模态混合数据。鲁棒性极强自动处理缺失值、异常值、类别不均衡和文本字段几乎不会因为数据问题报错崩溃。二、核心底层架构多层集成策略AutoGluon 能够在短时间内取得极高精度的秘密武器在于其独特的多层模型堆叠Multi-layer Stacking和多层交叉验证Repeated Cross-Validation。1. 基础层模型并行训练Base Models在一开始AutoGluon 会同时训练多个完全不同类型的基模型包括LightGBM、XGBoost、CatBoost擅长表格结构化数据深度学习模型基于 PyTorch 的神经网络、FastAI传统机器学习随机森林、极度随机树、K近邻2. 多层堆叠Multi-layer Stacking第一层所有的基模型对原始特征进行预测。第二层及更高层将前一层所有模型的预测结果作为“新特征”与原始特征拼接在一起再送入下一组模型中训练。为了防止过拟合这个过程全程伴随着内嵌的k折交叉验证Out-Of-Fold, OOF 预测。3. 加权集成Weighted Ensemble在所有堆叠层训练完成后AutoGluon 会使用 Caruana 规则一种贪心算法选择表现最好的一组模型组合并赋予它们不同的权重融合成最终的输出。这种方法让它天生具备极强的泛化能力。