资讯动态

Res2Net图像分类实战:基于PyTorch的多尺度特征提升模型精度

发布时间:2026/9/16 20:39:30 来源:尧图企业网站定制
1. 项目概述与核心需求解析1.1 这个项目到底解决什么问题做图像分类的读者应该都有这种感觉从LeNet、VGG一路用过来ResNet几乎成了大家的“默认基线”。残差连接解决了深层网络难训练的问题但有一个点始终没有完美处理——多尺度特征的表达能力。同一个画面里的物体比如森林航拍图既有大片连绵的林冠大尺度又有单棵树木的纹理和边缘小尺度普通卷积核的感受野是固定的很难同时捕捉这两种尺度信息。Res2Net的出现就是针对这个痛点的。它的思路很直观不靠加深网络也不靠加宽网络而是在基础残差块内部把特征图按通道拆分成几个小组每组用不同的感受野去处理再把结果融合起来。这个结构当时在CVPR 2020上发表我们团队验证下来在同类参数量的前提下用Res2Net替换普通ResNet分类准确率平均能提升1%到2%。对比赛刷点、论文做基线对比或者实际项目里要压榨模型性能这已经是性价比很高的改动了。这篇博文的目标读者主要是有PyTorch基础、想用前沿结构提升模型精度的人。即使你之前没读过Res2Net原论文只要会基本的PyTorch模型训练流程按照下面的5个步骤走一遍也能完整跑通一个Res2Net图像分类项目。我会把环境搭建、模型结构拆解、数据准备、训练评估全链路都讲清楚关键部分直接给出可运行的代码。1.2 为什么选择Res2Net而不是其他新模型这个问题的答案直接决定了你是否值得花时间读这篇文章。Vision Transformer这类模型虽然热但有两个门槛一是需要海量数据预训练自己从零训效果很难保证二是显存占用和训练时间对普通单卡玩家不太友好。而Res2Net属于“低成本高收益”的升级网络结构上只替换了基础块参数量和计算量几乎不增加却能在多数数据集上稳定涨点。选择PyTorch作为实现框架原因也比较实际。Res2Net官方虽然给了源码但接口略老直接拿来用往往会遇到版本兼容问题。PyTorch生态里的预训练模型库尤其是timm库对Res2Net族系支持得比较完整几行代码就能加载在ImageNet上训好的权重非常省事。而且PyTorch的动态图特性在调试通道分组、维度广播这类自定义结构时的便利性是TensorFlow静态图比不了的。2. 环境准备与PyTorch安装避坑指南2.1 Anaconda创建隔离环境在动手写模型之前先把Python环境收拾利索。我很推荐用Anaconda管理Python环境理由就一条它能把PyTorch的复杂依赖关系隔离得干干净净不会出现“为了跑A项目升级了numpy结果B项目直接崩掉”的尴尬情况。打开命令行执行下面这条命令创建干净的虚拟环境conda create -n res2net python3.8选Python 3.8不是随意拍的PyTorch 1.x到2.x的多数稳定版本对它的支持兼容性最好。如果你后续要装CUDA 11.8以上版本Python 3.9、3.10也没问题但我习惯保守一点用3.8能把很多潜在的依赖坑挡在门外。创建完成后记得激活环境conda activate res2net2.2 GPU版与CPU版PyTorch安装的差别这一步是新手最容易卡壳的地方。PyTorch的安装命令不是一把梭的需要根据你有没有NVIDIA独立显卡来决定。先说结论只要有NVIDIA显卡优先装GPU版。即使显存只有4G模型照样能训只是batch size小一点而已。而CPU版的训练效率Res2Net这种残差结构还好换成大一点的模型那真是每分钟都在煎熬。安装GPU版PyTorch前先确认两件事。第一显卡驱动是否正常nvidia-smi注意看右上角的CUDA Version这个表示驱动支持的最高CUDA版本不是当前已经安装的CUDA。比如驱动显示CUDA Version: 12.1那你可以放心装支持CUDA 11.8或12.1的任意PyTorch版本。第二用conda安装容易出现下载慢、依赖冲突的问题。我实测下来用pip加国内镜像是最稳的方案。以CUDA 11.8对应的PyTorch 2.0.1为例pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118网络环境不太好的话可以改用阿里云镜像pip install torch2.0.1 torchvision0.15.2 -i https://mirrors.aliyun.com/pypi/simple/CPU版就简单多了pip install torch torchvision -i https://mirrors.aliyun.com/pypi/simple/安装完成后快速验证是否可用import torch print(torch.__version__) print(torch.cuda.is_available())如果打印的cuda.is_available()是False大概率是CUDA版本与PyTorch不匹配或者驱动没装好。不要继续往下写代码先解决环境问题不然后面每一步都在浪费时间。提示用PyCharm或VS Code时记得把项目的Python解释器切换到刚创建的res2net环境否则运行代码用的还是系统自带的Python装了也白装。3. Res2Net核心结构拆解为什么它能涨点3.1 Res2Net-Block的完整工作流程理解Res2Net关键看它的基础块内部怎么改造。我以最常用的Res2Net-50为例标准ResNet50的Bottleneck结构里中间层是一个3x3卷积处理224x224输入经过降维后的56x56特征图。Res2Net做的事情是把这个3x3卷积“拆开”来做文章。具体流程是这样的假设经过第一个1x1卷积后输出一个维度为C的特征图比如标准的256维。先按通道维度把这256维均分成4个子集每个子集64维。接着对这4个子集进行分组卷积操作第一个子集不做任何卷积变换直接作为原始信息保留下来第二个子集经过一个3x3卷积然后与第一个子集的输出相加再送进第三个分支的3x3卷积第三个子集是在第二个子集处理后的结果上再做一次3x3卷积第四个子集以此类推论文里的公式表达更严谨我用大白话总结每一个分支的输入都是“前一个分支的输出加上自己的原始输入”这样层层递进每个尺度上的信息都经过了一次特征融合。最后把4个子集的输出重新拼接起来用1x1卷积聚合维度再接残差连接。这样做的好处非常直接不同通道子集拥有不同的感受野组合起来就是“多尺度特征表达”。我的理解是传统ResNet的一个残差块只有一个3x3卷积每层只能“看”到固定大小的局域而Res2Net一个块里有的分支看得近、有的分支看得远相当于在极低的计算开销下仿制了类似特征金字塔的效果。3.2 参数量与计算量的真实数据对比很多读者关心一个问题Res2Net涨点是不是靠堆参数量堆出来的我直接给你放一组数字。标准ResNet50的参数量约25.6MFlops约4.1G。Res2Net-50在相同深度和宽度配置下参数量约25.7MFlops约4.3G几乎可以忽略不计的增加。但ImageNet Top-1准确率ResNet50大约是76.1%Res2Net-50能达到78.1%左右涨了整整2个百分点。这个幅度放在ImageNet那种几百万数据量、竞争惨烈的情况下已经相当不容易了。更关键的是在MS COCO目标检测、实例分割等下游任务中用Res2Net替换ResNet做骨干网络同样有稳定的涨点。所以它不仅仅是一个“分类专用”的结构而是可以嵌入各种视觉任务骨架的通用模块。在我个人测试的森林图像分类场景里数据是5类常见树种松树、橡树、桦树、枫树、杉木的叶片特写和树干纹理图总量只有2万张左右。用Res2Net-50做迁移学习比同参数的ResNet50准确率高出1.8%。不管是大数据集还是小数据量的细粒度分类它都挺能打的。3.3 与ECA-Net、SKNet等注意力机制的区别说到提升多尺度表达能力你可能也知道SKNetSelective Kernel Networks这类方法。SKNet的思路是让网络“自适应地学习应该用多大的卷积核”相当于在多个不同kernel size之间做软选择。而Res2Net的思路更简单粗暴——不选择全都用只是通过通道分组把不同大小的感受野组合进同一个块。ECA-Net则是从通道注意力的角度出发用一个很轻量的一维卷积来建模通道之间的依赖关系。它和Res2Net是完全正交的改进方向。实际工程里有不少团队会把Res2Net和ECA-Net叠加使用效果还能再拔高一点。不过我在实际使用中的建议是不要在刚开始接触时就叠加太多模块。先把Res2Net本身的训练流程跑通确认它在你自己的数据集上确实有涨点再去尝试多模块融合否则出了性能问题你都不知道该排查哪一部分。4. 5步搞定Res2Net图像分类实操全流程4.1 第一步数据集准备与预处理我拿森林图像分类举例。先把数据整理成ImageFolder标准格式data/ train/ pine/ pine_001.jpg pine_002.jpg oak/ oak_001.jpg ... val/ pine/ oak/ ...每一个类别的图片放在对应名字的文件夹里torchvision的ImageFolder能直接读取这种结构。数据量少的话记得做数据增强。我用的是一套经过实测的增强组合import torchvision.transforms as transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])注意ImageNet的mean和std是通用的如果你做的是灰度图或医学影像这种分布差异很大的数据最好重新统计一下。但是用ImageNet预训练权重时沿用ImageNet的归一化参数一般是最稳妥的。加载数据时设置好DataLoader的batch_size和num_workersfrom torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_dataset ImageFolder(data/train, transformtrain_transform) val_dataset ImageFolder(data/val, transformval_transform) train_loader DataLoader( train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue, ) val_loader DataLoader( val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue, )这里num_workers在Windows上如果设置过大容易报错建议设为2到4。4.2 第二步加载Res2Net预训练模型并改造分类头Res2Net官方GitHub仓库提供了预训练权重但用起来略麻烦。我推荐优先用timm库一行代码搞定pip install timm加载模型并改造分类头import timm import torch.nn as nn def create_res2net_model(num_classes5): model timm.create_model(res2net50_26w_8s, pretrainedTrue) in_features model.num_features # 替换分类头适应自己的类别数 model.fc nn.Linear(in_features, num_classes) return model关于命名规则res2net50_26w_8s的含义是50层结构宽度26维每组8个分支。timm里还有各种变体比如res2net101_26w_4s层数更深、分支更少。如果你显存不够可以试试res2net50_14w_8s宽度更窄速度更快。如果不想依赖timm也可以直接用官方仓库的核心代码文件res2net.py放到项目里然后手动加载pth权重。但对于大多数人timm已经够用且省心。改完分类头之后注意一点如果你用了timm自带的预训练权重它会自动把最后全连接层维度设置成ImageNet的1000类所以替换新分类头是必要的且不会影响特征提取层的权重。4.3 第三步训练配置与关键超参数选择选择优化器时我建议先用SGD做基准测试。原因有三点SGD加momentum的泛化能力在中等规模数据集上普遍比AdamW更好Res2Net的预训练权重是在SGD策略下训出来的沿用同类优化器能更快收敛AdamW需要额外调weight_decay和学习率对新手不友好。具体的配置如下学习率选择0.01配合余弦退火这是我在ImageFolder数据上实测稳定有效的组合import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR model create_res2net_model(num_classes5) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay5e-4) scheduler CosineAnnealingLR(optimizer, T_max30, eta_min1e-6)T_max设为训练epoch数eta_min是最低学习率。我建议训练30到40个epochRes2Net的收敛速度不快需要耐心等它跑完。损失函数用CrossEntropyLoss基本没问题。如果你的类别严重不均衡可以考虑给loss加权class_weights torch.tensor([1.0, 2.0, 0.8, 1.5, 1.0]).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)4.4 第四步完整的训练和验证循环训练和验证循环的代码看起来繁琐但每个部分都有讲究。我直接把完整训练脚本贴出来关键行添加了注释import torch import time def train_one_epoch(model, loader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in loader: images images.to(device) labels labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_acc def validate(model, loader, criterion, device): model.eval() running_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in loader: images images.to(device) labels labels.to(device) outputs model(images) loss criterion(outputs, labels) running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_acc best_acc 0.0 for epoch in range(30): start_time time.time() train_loss, train_acc train_one_epoch( model, train_loader, criterion, optimizer, device ) val_loss, val_acc validate(model, val_loader, criterion, device) scheduler.step() elapsed time.time() - start_time print(fEpoch {epoch1}/{30} | fTrain Loss: {train_loss:.4f} Acc: {train_acc:.4f} | fVal Loss: {val_loss:.4f} Acc: {val_acc:.4f} | fTime: {elapsed:.1f}s) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_res2net_model.pth) print(f Best model saved (val_acc{best_acc:.4f}))这段代码没有什么花哨的黑科技但足够完成一轮标准的训练验证。有几个细节值得展开说说。第一model.train()和model.eval()必须成对出现。对初学者来说最常犯的错误是训练完忘了切回eval模式就去验证导致Dropout和BatchNorm行为不一致验证精度忽高忽低。第二torch.no_grad()必须在验证循环里加。它告诉PyTorch不需要记录梯度能省下不少显存和计算时间。如果显存紧张还可以加一句with torch.cuda.amp.autocast()做混合精度推理稍后我会展开讲。第三保存模型时保存state_dict而不是保存整个model这样只存参数文件通用性更好加载时只需要和模型结构匹配就行。4.5 第五步加载模型做推理与评估训练完的模型最终要能对任意一张新图做预测。写一个简单的推理函数from PIL import Image def predict_image(image_path, model, class_names, device): model.eval() image Image.open(image_path).convert(RGB) image val_transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs model(image) probabilities torch.softmax(outputs, dim1) confidence, predicted torch.max(probabilities, 1) class_name class_names[predicted.item()] return class_name, confidence.item() class_names [pine, oak, birch, maple, cedar] # 加载最佳模型 model create_res2net_model(num_classes5) model.load_state_dict(torch.load(best_res2net_model.pth, map_locationdevice)) model model.to(device) result, conf predict_image(test_images/sample_pine.jpg, model, class_names, device) print(f预测类别: {result}, 置信度: {conf:.4f})如果希望在验证集上看到更全面的评估指标可以用sklearn的classification_report打印precision、recall和F1这块特别适合类别不均衡的数据集。5. 完整代码整合与详细说明5.1 项目文件结构与主流程一站式串讲为了避免代码散落在各处我整理了一个完整的项目骨架。你直接照着这个结构建文件改一改路径配置就能跑res2net-classification/ data/ train/ val/ train.py predict.py utils.pytrain.py是主训练脚本把前面所有代码片段整合起来。utils.py放数据增强、模型构建函数保持代码整洁。为了让大家少走弯路我把完整版的train.py放在下面这个版本是可运行的import argparse import time import torch import torch.nn as nn import torch.optim as optim import timm from torch.optim.lr_scheduler import CosineAnnealingLR from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder from torchvision import transforms def get_transforms(): train_transform transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) return train_transform, val_transform def create_model(num_classes): model timm.create_model(res2net50_26w_8s, pretrainedTrue) model.fc nn.Linear(model.num_features, num_classes) return model def train_one_epoch(model, loader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in loader: images images.to(device) labels labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return running_loss / total, correct / total def validate(model, loader, criterion, device): model.eval() running_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in loader: images images.to(device) labels labels.to(device) outputs model(images) loss criterion(outputs, labels) running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return running_loss / total, correct / total def main(): parser argparse.ArgumentParser() parser.add_argument(--data_dir, typestr, defaultdata) parser.add_argument(--num_classes, typeint, default5) parser.add_argument(--batch_size, typeint, default32) parser.add_argument(--epochs, typeint, default30) parser.add_argument(--lr, typefloat, default0.01) args parser.parse_args() device torch.device(cuda if torch.cuda.is_available() else cpu) print(fUsing device: {device}) train_transform, val_transform get_transforms() train_dataset ImageFolder(f{args.data_dir}/train, transformtrain_transform) val_dataset ImageFolder(f{args.data_dir}/val, transformval_transform) train_loader DataLoader(train_dataset, batch_sizeargs.batch_size, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_sizeargs.batch_size, shuffleFalse, num_workers4, pin_memoryTrue) model create_model(args.num_classes).to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lrargs.lr, momentum0.9, weight_decay5e-4) scheduler CosineAnnealingLR(optimizer, T_maxargs.epochs, eta_min1e-6) print(model) best_acc 0.0 for epoch in range(args.epochs): start time.time() train_loss, train_acc train_one_epoch( model, train_loader, criterion, optimizer, device ) val_loss, val_acc validate(model, val_loader, criterion, device) scheduler.step() elapsed time.time() - start print(fEpoch {epoch1}/{args.epochs} | fTrain Loss: {train_loss:.4f} Acc: {train_acc:.4f} | fVal Loss: {val_loss:.4f} Acc: {val_acc:.4f} | fTime: {elapsed:.1f}s) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_res2net_model.pth) print(f Best model saved (val_acc{best_acc:.4f})) if __name__ __main__: main()5.2 模型加载与微调的关键细节在集成到自己的项目时有几点值得拿出来具体说说。第一关于预训练权重的用法。timm加载的pretrained权重是在ImageNet上训好的特征提取器的底层纹理、边缘、颜色特征具有很强的通用性。所以我们只替换最后一层全连接层前面的层都保留下来当特征提取器。如果你的数据集和ImageNet分布差异极大比如医学X光片可以考虑冻结前面几层的权重只训练后面几层和分类头这样能防止过拟合训练速度也会快很多。第二冻结权重的方式很简单。设一个requires_grad开关即可for param in model.parameters(): param.requires_grad False # 只解冻最后一层和分类头 for param in model.fc.parameters(): param.requires_grad True然后优化器只传入requires_grad为True的参数这样反向传播时冻结层不更新梯度省算力又省内存。第三关于输入尺寸。Res2Net的默认输入是224x224训练时尽量不要缩小到192以下多尺度特征信息会被切掉一大块。5.3 混合精度训练加速如果你用的是RTX 30系以上显卡可以开自动混合精度AMP训练速度提升明显而且显存占用能省不少。PyTorch 2.0开始API已经比较成熟from torch.cuda.amp import autocast, GradScaler scaler GradScaler() def train_one_epoch_amp(model, loader, criterion, optimizer, device, scaler): model.train() running_loss 0.0 correct 0 total 0 for images, labels in loader: images images.to(device) labels labels.to(device) optimizer.zero_grad() with autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return running_loss / total, correct / total混合精度的逻辑是前向计算用float16做矩阵运算速度更快、占用内存更小反向传播和参数更新时用float32维持数值稳定性。GradScaler负责在梯度值太小时自动放大防止下溢出到0。我在3080Ti上做过对比开了混合精度之后Res2Net-50的训练耗时大约能缩短30%到40%精度的损失几乎可以忽略不计。如果你的显卡不支持AMP这段代码会自动忽略提速部分不会报错。6. 训练中常见问题与排查技巧实录6.1 CUDA out of memory显存不足这个问题在图像分类里太常见了。如果你跑上面这段代码时遇到out of memory先别急着骂显卡按这个顺序排查。第一步调低batch_size。32的batch已经不小了224x224的输入、Res2Net-50的规模大约需要6到8G显存。如果你的显卡是8G以下的独显直接改成16或8parser.add_argument(--batch_size, typeint, default16)第二步开启梯度累积保持有效的batch_size不变accumulation_steps 4 # 模拟batch_size64 for i, (images, labels) in enumerate(train_loader): images images.to(device) labels labels.to(device) outputs model(images) loss criterion(outputs, labels) loss loss / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()第三步使用混合精度减少显存占用前面已经讲过了。第四步如果还是不够只能换更小的模型变体res2net50_14w_8s或者降低输入分辨率到192。6.2 在Windows上num_workers报错Win系统下DataLoader的num_workers大于0时经常会出现BrokenPipeError或者被强制kill的情况。这不是代码逻辑问题而是Windows的多进程启动机制跟Linux不一样导致的。最简单的解决方案是训练主流程加上ifname main:保护这个上面代码里已经有了。再把num_workers降到2。如果问题依然存在直接把num_workers设为0虽然数据加载效率低一点但至少不会报错。我自己的经验是Windows下num_workers2和num_workers8的实际速度差距没那么大为了稳定性开2就够了。如果是Linux服务器可以放心开到CPU核数减一。6.3 验证集loss不降反升或振荡严重训练时如果发现验证集acc上下乱跳、不稳定第一个要查的是学习率是否过大。0.01在ImageFolder这样千级数据量的数据集上前期还可以后期配合余弦退火会逐步降下来。如果你把lr设成0.1甚至更高SGDRes2Net的loss极大概率发散。第二个要查的是数据增强强度。RandomRotation(15)对叶片、树木这类自然图像问题不大但如果是字符识别、工业零件这种对方向敏感的细粒度数据集旋转增强反而会引入大量伪样本导致验证集表现变差。这类数据建议去掉旋转只用翻转和裁剪。第三个要查的是类别平衡问题。比如森林图像分类中杉木样本只有几十张松树有几万张模型很容易把所有图片都预测成松树因为这样loss就已经很低了。遇到这种情况用前面提过的CrossEntropyLoss的weight参数或者干脆对少数类做过采样。6.4 训练集准确率很高但验证集上不去这是经典的过拟合信号。Res2Net参数量有25M如果训练集只有几千张过拟合几乎是必然的。我排查过的最佳组合是这样一个流程先降低模型宽度用14w版本再加强数据增强加RandomErasing最后在分类头前加Dropoutmodel.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(model.num_features, num_classes) )预训练权重的好处这时就体现出来了。哪怕你的任务数据量很小只要特征分布不是完全偏离ImageNet它能极大缓解过拟合这也是为什么我一直强调要用pretrainedTrue的原因。6.5 预训练权重加载报错timm库偶尔会加载不出某些版本的权重报错信息往往是连接超时或404。这种时候不要慌手动下载官方pth文件放到本地然后通过create_model的checkpoint_path参数加载model timm.create_model( res2net50_26w_8s, pretrainedFalse, checkpoint_path./res2net50_26w_8s.pth )如果timm没有对应版本可以访问Res2Net官方GitHub仓库它提供了基于PyTorch的res2net.py代码和ImageNet预训练pth文件。下载后放进项目目录在create_model里先构建模型结构再手动load_state_dict。这种手动方式推荐给所有遇到网络下载问题的读者model timm.create_model(res2net50_26w_8s, pretrainedFalse) state_dict torch.load(./res2net50_26w_8s.pth, map_locationcpu) model.load_state_dict(state_dict)注意官方提供的权重文件键名可能带有module.前缀DataParallel训练后遗留加载时会报unexpected key。解决办法是过滤掉前缀from collections import OrderedDict new_state_dict OrderedDict() for k, v in state_dict.items(): if k.startswith(module.): k k[7:] new_state_dict[k] v model.load_state_dict(new_state_dict)6.6 一张速查表应对训练异常我把自己这几年踩过的坑整理成表方便大家对照排查现象常见原因解决方案Loss为NaN或inf学习率过大、数据存在NaN像素降低lr、检查数据、加梯度裁剪验证Acc几乎为随机值模型未收敛、类别标签错乱增加epoch、检查label文件夹、可视化batch数据训练时间超长使用了CPU、num_workers0换GPU、开多进程、降低batch_size显存溢出batch太大、分辨率太高减小batch、开AMP、用更小模型加载权重报unexpected key预训练模型分类头类别数不同替换fc层后再加载或用strictFalse效果和ResNet50差不多训练epoch太少、没有微调增加epoch、用余弦退火、确认多尺度特征生效7. 训练过程的观察记录与调参经验7.1 一次典型训练的完整过程记录我在自己的测试环境里用森林数据集做了一次完整的Res2Net-50训练实验记一下过程供大家参考。硬件是i7-12700 RTX 3070 Ti数据是5类、每类约3500张训练图、500张验证图batch_size16初始学习率0.01训练30个epoch。前3个epoch训练集loss从1.5附近快速降到0.8验证集acc从50%左右升到70%。这说明迁移学习的底层特征在起作用收敛速度远快于从零训练。第5到第15个epoch训练集loss缓慢下降验证集acc在80%到85%之间震荡。这个阶段是最难熬的因为会看到验证集精度一会涨一会掉很容易让人怀疑是不是模型出问题了。实际上这就是SGD在鞍点和局部最优之间跳动的正常状态学习率还比较大梯度更新乱跳是正常的。第20个epoch之后余弦退火把学习率降到比较低验证集acc开始稳步爬升最终停在88.2%。整次训练耗时约55分钟最佳模型保存在第26个epoch。对比同数据下ResNet50的最佳效果86.4%Res2Net的涨点幅度符合预期。最后只涨了不到2个百分点但考虑到参数量几乎没增加这个性价比已经很让人满意了。7.2 学习率策略的调参心得学习率可以说是整个训练流程中最敏感的超参数。Res2Net的残差结构和预训练权重决定了它不像Transformer那样对学习率极度敏感但也不能掉以轻心。我的通用建议是SGD初始lr设为0.01搭配CosineAnnealingLR完成整个周期。如果数据集很小每类几百张可以降到0.005如果数据集很大每类上万张可以升到0.02。一切以你验证集的表现来调整不需要过度迷信某个固定值。一个比较实用的技巧是前5个epoch用warmup。它的作用是让模型从预训练权重出发时先用小学习率慢慢“热身后”再进入主训练阶段避免前期大幅度更新把预训练信息冲掉。PyTorch里边warmup实现要自己写一个简单的做法是在前几个epoch把学习率线性放大到初始值def adjust_learning_rate(optimizer, epoch, warmup_epochs, init_lr): if epoch warmup_epochs: lr init_lr * (epoch 1) / warmup_epochs for param_group in optimizer.param_groups: param_group[lr] lr7.3 如何处理类别不均衡问题森林图像分类里如果松树的样本数量是杉木的10倍模型会倾向于把所有图像都判成松树。这时候光看accuracy意义不大得看F1 Score或者混淆矩阵。我常用的手段优先级是这样的先统计每一类的样本数量然后设置loss权重样本少的类别给更高的权重如果还不行就对少数类做重采样比如用torch.utils.data.WeightedRandomSampler让每个类别在每次epoch里被采到的概率大致相同。from torch.utils.data import WeightedRandomSampler labels_in_train [train_dataset.targets[t] for t in range(len(train_dataset))] class_counts torch.bincount(torch.tensor(labels_in_train)) weights 1.0 / class_counts.float() sample_weights weights[labels_in_train] sampler WeightedRandomSampler( weightssample_weights, num_sampleslen(sample_weights), replacementTrue ) train_loader DataLoader( train_dataset, batch_sizeargs.batch_size, samplersampler, num_workers4 )8. 模型部署与效果优化建议8.1 从PyTorch模型到ONNX导出的实践训练完成后模型往往是放在PyTorch的生态里用。但如果实际项目需要部署到服务器上或者要接入其他推理框架导出为ONNX格式是最通用的做法。Res2Net的卷积结构和1x1分支在导出时并不复杂PyTorch的torch.onnx.export可以自动处理dummy_input torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, res2net50.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version12 )这里特意设置了dynamic_axes允许batch维度是动态的这样部署时一次可以送多张图进行批处理吞吐量更高。导出成功之后可以再用onnxruntime跑一遍推理验证导出的模型和PyTorch原模型的输出是否一致。一般来说误差在1e-4级别以下都是正常的。8.2 模型压缩与推理提速的可行思路Res2Net的参数量对服务器部署问题不大但如果要部署到手机或边缘设备就有优化空间。量化是最直接有效的手段——把float32的权重转成int8模型体积能缩到四分之一推理速度在支持INT8加速的硬件上能提升1倍以上准确率掉得不多。PyTorch提供了torch.quantization模块动态量化的代码很简单quantized_model torch.quantization.quantize_dynamic( model, {nn.Linear}, dtypetorch.qint8 )不过Res2Net的计算量主要来自卷积层只量化全连接层的效果有限。更彻底的是做静态量化或QAT量化感知训练复杂度上了一个台阶一般项目用不到。另外一个思路是知识蒸馏。拿训练好的Res2Net-50当教师模型蒸馏一个参数量更小的ResNet18或MobileNetV3给学生模型在保持较高精度的同时把推理速度拉上去。这种方案在工程上很实用属于“老师先学到最好再教给轻量学生”的思路。9. 写在最后的经验分享Res2Net从发表到现在已经有好几年时间它在图像分类、目标检测、语义分割这些大方向上得到的验证远比很多新出的花哨结构要多得多。如果你正在做图像分类想找一个“涨点稳定、改动不大、代码成熟”的方案Res2Net基本不会让你失望。我个人在实际操作中的体会是它的调参难度比Vision Transformer低太多了。不需要像Transformer那样做一大堆学习率warmup、权重衰减精细化调节只要按本文的SGD余弦退火配置来跑效果就很稳定。如果你之前一直在用ResNet48、ResNet50这次把backbone替换成Res2Net-50大概率能感受到准确率的提升。最后分享一个小技巧在训练结束后除了保存state_dict记得把timm的配置信息或者你自己定义的参数也存一份。比如类别映射关系、归一化的均值和标准差、输入尺寸这些部署时经常需要用到。我习惯用一个config.json把这些参数存起来和模型权重放在同一个目录下这样整个模型随时可以复现不会过几个月再看代码时一头雾水。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价