ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

EfficientNet全面解析:从复合缩放到训练部署实战

EfficientNet全面解析:从复合缩放到训练部署实战 简介聚焦图像分类任务基于Pytorch框架实现EfficientNet实战面向有一定深度学习基础、希望上手图像分类项目的开发者。包内共8个文件以5个Python脚本为核心覆盖数据集加载、模型训练、测试预测等环节另含2个编译生成的pyc文件与1个已训练好的模型权重pth文件整体大小38.27MB结构紧凑便于快速对照学习和复用。已有1205人学习下载适合作为图像分类入门项目的参考实现。透过该资源可获得一套可运行的EfficientNet图像分类流程包括训练集组织方式、模型调用写法、测试脚本及权重加载方法也能理解EfficientNet的模型结构与训练调参思路并可直接替换数据集进行迁移应用。1. EfficientNet 的复合缩放把加宽、加深、提分辨率收进一个公式如果你调过 ResNet 或者看过一些图像分类模型的演进大概率见过“模型变大就能涨点”的朴素调参思路加深度、加宽度、把输入分辨率从 224 拉到 320然后看着显存和训练时间一起失控。EfficientNet 给出的反直觉结论是——这三个维度不该独立放大而是按固定比例同时放大效果才会稳定。这个比例由一个小型网格搜索定下来之后从 B0 到 B7 都是同一套规则外推出来的不需要每换一个规模就重调一次结构。这套思路最早来自谷歌的 EfficientNet 系列论文也是这套 zip 里最值得先搞懂的部分。如果你之前只用过现成分类网络做迁移学习读完这一部分你就明白为什么 B4 在某些任务上比 B0 加三倍深度更好用也明白选型时到底该看参数量还是 FLOPs。适合正在做分类项目、想把手头模型换掉又怕掉点的人。2. MBConv 与 SE 注意力EfficientNet 的模块级设计2.1 倒残差结构为什么在轻量网络里更划算EfficientNet 的骨架由 MBConvMobile Inverted Bottleneck Convolution堆叠而成这个模块最早是从 MobileNetV2 继承过来的。普通残差块是“压缩-卷积-扩张”MBConv 反过来先用 1x1 卷积把通道数升高再在升维后的空间里做 depthwise 卷积最后用 1x1 卷积把通道数降回去。通道先扩后缩depthwise 卷积本身又不跨通道计算整个模块的 FLOPs 会比同尺寸 ResNet 块低一截。像 B0 的参数量只有 5.3M 左右和 ResNet50 的 25.6M 比只是零头但 ImageNet 精度却能压过后者原因不是单个卷积更强而是同样的算力预算内可以堆更多的层和更宽的通道。实际使用的时候torchvision 里的efficientnet_b0可以直接加载预训练权重不需要从零搭 MBConv。import torchvision.models as models model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.IMAGENET1K_V1) print(model)这段代码做的事情很简单实例化一个 B0 模型并加载 ImageNet 预训练权重。weights参数不传的话模型结构会创建但权重是随机的迁移学习场景下必须加上否则收敛速度会慢很多。打印模型是为了看features和classifier两个大模块的分界线后面做 CAM 可视化或替换分类头时要用到这个边界。2.2 Squeeze-and-Excitation 是怎么给通道加权的MBConv 里还嵌了一个 SESqueeze-and-Excitation模块。SE 做的事可以理解为对每个特征图先做全局平均池化得到一个通道描述符再通过两个全连接层算出每组通道的权重最后把权重乘回原来的特征图。这个机制的直观意义是网络不再把每个通道等量看待而是根据当前输入内容动态强调某些通道、抑制另一些通道。比如识别一辆车时和轮子、车窗纹理相关的通道会被放大和背景草地相关的通道被压小。SE 模块放在 depthwise 卷积之后、残差连接之前计算开销很小但对精度的提升在 ImageNet 上能带来一个多点。这也是 EfficientNet 能在同 FLOPs 下打赢 ResNet 的重要原因之一。如果你自己改网络结构SE 的 reduction ratio 一般设为 4 或 8B0 用的是 8缩小这个值会增加参数但未必涨点。2.3 从 B0 到 B7复合缩放的三个系数怎么配合EfficientNet 的核心创新不是 MBConv而是复合缩放。作者用神经架构搜索先找到一个基线网络 B0然后通过一组系数同时调整深度、宽度和输入分辨率。缩放公式是深度 d α^φ宽度 w β^φ分辨率 r γ^φ其中 α、β、γ 是网格搜索出来的常数约束条件是 α·β²·γ² 约等于 2φ 控制整体放大的强度。这个约束的直觉是FLOPs 的增长大体上正比于 d、w²、r² 的乘积所以让三者的乘积保持在一个可控范围内算力增长才稳定。φ0 时是 B0φ 越大模型的深度、宽度和输入尺寸都会同步变大。模型输入分辨率参数量Top-1 AccImageNet适用场景B02245.3M77.1%移动端、快速验证B12407.8M79.1%小数据集迁移学习B22609.2M80.1%中等规模数据B330012M81.6%服务器端高精度B438019M82.9%精度优先B545630M83.6%精度优先需要大显存B652843M84.0%尽量不用收益低B760066M84.3%竞赛或离线场景这个表里有一个容易被忽略的点分辨率是复合缩放的一部分而不是单独设置的外部参数。很多人直接把 B0 的输入从 224 强行改成 512结果精度不升反降原因就是宽度和深度没跟上网络没有足够容量去消化高分辨率下的细节信息。反过来B5 的默认分辨率是 456你硬缩到 224 去训练相当于浪费了模型后半部分的感受野设计。所以使用这包资源时第一步不应该是改结构而是确认你选的 B 编号和输入尺寸是配套的。提示显存不够时优先换小一档的 B 编号不要手动把分辨率砍半而保持模型不变。后者的精度损失通常更大。3. 数据管线与训练参数复现 ImageNet 级精度的关键设置3.1 预处理与增强策略的选择EfficientNet 的训练对数据增强比较敏感。原论文用的是 RMSProp 大 weight decay AutoAugment 这类策略但迁移学习场景下不需要完全复刻因为你的数据集规模一般只有几万张甚至几千张。我常用的做法是先用 RandomResizedCrop 加 RandomHorizontalFlip 做基础增强如果有余力再上 RandAugment强度别拉太高。EfficientNet 的 BN 层比较多输入分布的抖动太大会让 BN 统计量不稳定。归一化参数也要注意ImageNet 预训练权重对应的 mean 和 std 是固定的这个抄 torchvision 官方示例就可以不需要自己重新统计。数据集本身的 mean 和 std 和你任务有关但如果你用的是预训练权重归一化必须用预训练阶段的值否则第一个 batch 的特征分布就偏了。from torchvision import datasets, transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.2, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandAugment(num_ops2, magnitude9), 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]) ])这里的关键参数有三个。scale(0.2, 1.0)控制随机裁剪的面积比例0.2 意味着裁剪区域最小只占原图的 20%能模拟物体在画面中占比较小的情形适合目标占比不固定的数据集。RandAugment(num_ops2, magnitude9)中 num_ops 是每次随机应用的增强操作数magnitude 是强度迁移学习时从 9 往下调比往上调安全数据集越小强度越要低。验证集的 Resize 到 256 再 CenterCrop 224是为了和训练时的 RandomResizedCrop 输入尺度对齐同时做了轻微的放大裁剪能保留更多边缘细节。3.2 Loss、优化器与学习率调度的选择分类头要改成你自己的类别数这是迁移学习最常见的一步。B0 默认的classifier是 Linear(1280, 1000)需要替换成 Linear(1280, num_classes)。训练时我一般不用原论文的 RMSProp而是选 AdamW因为它在小数据集上的表现更稳定权重衰减不像 SGD 那样需要反复调。初始学习率在 1e-3 到 3e-3 之间batch size 越大学习率可以适当放大一些。weight decay 用 1e-4 或 5e-5 都算合理区间太大会让模型欠拟合太小则起不到正则作用。Loss 用 CrossEntropyLoss 就够多标签任务才需要换成 BCE。标签平滑在小数据集上能防止模型过自信但如果你的类别本身有重叠平滑系数不要超过 0.1。import torch.nn as nn model.classifier nn.Sequential( nn.Dropout(p0.3, inplaceTrue), nn.Linear(1280, num_classes) ) criterion nn.CrossEntropyLoss(label_smoothing0.1) optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs)label_smoothing0.1的含义是真实标签的 one-hot 向量被平滑为 [0.9, 0.1/(C-1)...] 的形式模型不再强行把目标类概率推到 1这能缓解过拟合但也会让训练集的最终准确率看起来比不加平滑时低几个点这是正常现象不要据此认为模型变差了。CosineAnnealingLR配合 warmup 使用效果更好前 5 个 epoch 学习率从 0 线性升到预设值后面按余弦曲线降到接近 0。如果你训练轮次只有 20 轮左右warmup 可以缩短到 2 到 3 轮否则模型前期收敛太慢。3.3 完整训练循环的骨架训练循环本身不复杂但有几个细节会影响结果。第一个是梯度裁剪EfficientNet 的梯度范围相对可控但用大学习率时偶尔会出现 loss 突刺clip_grad_norm_能兜底。第二个是每轮评估不只记录 val loss还要记录 top-1 和 top-5EfficientNet 前几轮验证准确率可能只有 30% 左右到了第 8 轮之后才会快速拉升不要提前停止。for epoch in range(epochs): model.train() for images, labels in train_loader: images, labels images.cuda(), labels.cuda() logits model(images) loss criterion(logits, labels) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() scheduler.step()max_norm5.0表示把所有参数的梯度 L2 范数截断到 5 以内超过的部分等比缩小。这个值设太大会失去保护作用设太小会阻碍模型中期收敛常见区间是 1.0 到 10.0。scheduler.step()放在每个 epoch 结束之后如果用了 warmup需要自己写一个分段学习率控制逻辑不能只靠 CosineAnnealingLR。数据并行方面用nn.DataParallel或DistributedDataParallel都可以跑但要注意 BN 在单卡 batch size 小于 8 时统计量会偏最好把单卡 batch size 保持在 16 以上不够就用梯度累积补。4. 训练过程监控与过拟合防线EMA、增强与梯度累积4.1 EMA 权重让验证集指标更稳定的常见做法EMA指数移动平均是分类任务里容易被人忽略的提点技巧。它的做法是在训练过程中不直接用当前 step 的权重做验证而是额外维护一组“影子权重”每个 step 按 0.999 的比例把当前权重融合进影子权重里。直观理解就是单步权重可能因为一个异常 batch 产生抖动但影子权重是所有历史权重的平均噪音小得多。EfficientNet 这类深层网络在训练后期 loss 曲线会进入平台期EMA 权重往往能比最终权重再高 0.3 到 0.5 个点这在竞赛里是常规操作。ema_decay 0.999 ema_model copy.deepcopy(model) ema_model.eval() torch.no_grad() def update_ema(model, ema_model): for (name, param), (ema_name, ema_param) in zip(model.named_parameters(), ema_model.named_parameters()): ema_param.data.mul_(ema_decay).add_(param.data, alpha1 - ema_decay)这个update_ema在每个 step 的optimizer.step()之后调用。ema_decay0.999意味着影子权重每步只吸收千分之一的新信息适合训练轮次较多的场景如果你的训练只有 20 轮0.999会让 EMA 更新太慢可以降到0.99让旧权重衰减得更快一点。ema_model只用在验证阶段不参与梯度计算所以用torch.no_grad()装饰。等训练结束后对比一下model和ema_model在验证集上的表现通常后者更稳。提示EMA 不能和 BN 的 running stats 混着用。EMA 只复制参数BN 的均值和方差统计量是独立维护的验证时用 EMA 的 BN 统计量会导致结果偏差最好选几个验证 batch 做一次 BN 统计量重估计。4.2 梯度累积单卡模拟大 batch 的正确方式EfficientNet 对 batch size 比较敏感直接表现是batch size 从 32 调到 64同样学习率下验证准确率会掉 1 个点左右因为 BN 的统计量变了。显存不够时梯度累积是替代方案。scaler.scale(loss).backward() if (step 1) % accumulation_steps 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad() update_ema(model, ema_model)accumulation_steps4表示每 4 个 batch 做一次参数更新等效 batch size 是单卡 batch size 乘以 4。注意 learning rate 是否需要相应放大如果是 AdamW 这类自适应学习率的优化器梯度累积对 lr 的影响不大如果是 SGD就需要按倍数放大。scaler是 AMP 混合精度训练用的EfficientNet 在 fp16 下能明显减少显存占用但 SE 模块里的某些 op 在 fp16 下容易溢出如果你的 loss 出现 NaN检查一下是不是scaler的 scale factor 太小或者把torch.autocast的dtype调成torch.bfloat16。4.3 经典掉点场景与排查顺序训练 EfficientNet 时最容易遇到的现象是训练 loss 一直在降但验证准确率卡住不动。这通常不是模型结构的问题而是增强强度和数据规模不匹配。下面是几个常见场景以及我平时的排查顺序现象可能原因优先排查项训练 loss 降val loss 反升增强过强或模型过大降低 RandAugment magnitude检查 label_smoothing 是否过高前 5 轮 acc 近乎 0学习率太大或 warmup 缺失把 lr 降到 3e-4加 3 轮 warmup验证集表现远低于预训练基线预处理与预训练不一致检查 Normalize 的 mean/std 是否套错loss 突刺后不再收敛梯度爆炸加梯度裁剪检查是否有个别样本标签错误EMA 和原权重几乎一样EMA decay 设置过大训练轮次少时把 decay 调小到 0.99最后一个场景容易被忽略标签噪声。如果验证集里本身有标错的样本你的模型 loss 会在某些 batch 上反复出现尖峰这时先别动模型直接用model.predict跑一遍训练集把预测概率低于 0.5 的样本筛出来人工检查。真实业务里这种样本往往能占到 1% 到 3%清洗之后准确率能立刻涨一个多点。4.4 随机深度与模型规模的权衡EfficientNet 在训练时还能用上随机深度Stochastic Depth的正则化技巧每个 MBConv 块在 forward 时有一定概率被跳过概率随深度线性增大。这样深的网络在训练时实际只走过一部分路径相当于多个子网络的集成能显著降低过拟合。但实现上需要把原模型的 forward 改掉工程量不小。替代方案是直接用小一号的模型比如 B0 换 B1、B1 换 B2。这里有个朴素的判断法则如果训练集只有 1 万张以下而 B3 以上的模型出现 val acc 波动大或反复过拟合先换小模型不要急着加正则技巧。EfficientNet 的参数量效率虽然高但它不是为小数据集设计的B0 在小数据上的表现通常已经够好B4 以上的收益边际递减明显。5. 导出 ONNX 与 Grad-CAM从验证集到可解释的推理阶段5.1 ONNX 导出时的动态轴与算子兼容训练结束后把模型导出成 ONNX是为了摆脱 PyTorch 运行时依赖方便在 CPU 推理或部署到其他框架里。EfficientNet 本身结构规整导出不像 Transformer 那样容易踩算子兼容问题但有两个点需要处理。第一个是分类头的维度导出前要固定num_classes用torch.onnx.export时传一个 dummy input尺寸最好和训练时一致不要为了省时间传一个和实际部署尺寸不一样的输入否则后续量化校准会出偏差。第二个是动态 batch 或动态分辨率时的轴标记SE 模块里有一个squeeze操作如果导出时把分辨率设成动态classifier 前的全局池化输出维度会跟着变ONNX Runtime 在部分版本上会对这类 dynamic shape 报错。import torch.onnx dummy_input torch.randn(1, 3, 224, 224).cuda() torch.onnx.export( ema_model, dummy_input, efficientnet_b0.onnx, input_names[input], output_names[logits], dynamic_axes{input: {0: batch, 2: height, 3: width}, logits: {0: batch}}, opset_version17 )dynamic_axes里把 height 和 width 设成动态部署时就能同时接受 224 和 320 的输入。但要注意ONNX Runtime 对动态分辨率的 tensor shape 推导会比固定尺寸慢而且某些 TensorRT 版本不支持全动态生产环境里如果输入尺寸固定建议直接去掉 height 和 width 的 dynamic 声明。opset_version17属于较新的算子集版本如果你的推理环境是旧版 ONNX Runtime1.12 以下需要把版本降到 13 或 11否则会遇到 unsupported operator 的报错。导出后先做一个简单验证用 ONNX Runtime 跑一遍同一样本比较输出 logits 和 PyTorch 原模型的差异。正常情况下最大误差应在 1e-4 量级如果误差到了 1e-2 或直接 nan重点检查是不是用了ema_model但 BN 统计量没有更新对。5.2 在导出前做 int8 量化时的校准集要求如果目标是边缘设备int8 量化通常是绕不开的。PyTorch 里torch.quantization.quantize_dynamic只适合线性层和 LSTM 这类算子但 EfficientNet 的主体是卷积和 BN不能直接用动态量化必须走 PTQ训练后量化的静态量化流程。PTQ 需要一个校准集几百张有代表性的图就够了关键是图片的分布要和真实场景一致。比如你的模型是在手机拍摄的照片上训练的校准集就不能全用高清网络图否则激活值的范围统计偏了量化后准确率会掉很多。int8 量化后 EfficientNet 的准确率通常会掉 0.5% 到 2%如果超过 2%优先检查校准集图片数量是否太少或有没有用错预处理参数。5.3 Grad-CAM 定位模型的真实关注区域可解释性对实际项目不是锦上添花。当模型在验证集上准确率已经很高但实际业务中某些类别频繁出错时Grad-CAM 能帮你判断模型是在看目标本身还是在看背景环境。EfficientNet 的features模块最后一层输出特征图的分辨率是 7x7输入 224 时Grad-CAM 拿这个特征图做空间加权上采样回输入尺寸就能得到一张热力图。实现上不需要引入额外库用 PyTorch 的 hook 就能完成。def grad_cam(model, input_tensor, target_class): feature_blob None gradient_blob None def forward_hook(module, input, output): nonlocal feature_blob feature_blob output def backward_hook(module, grad_input, grad_output): nonlocal gradient_blob gradient_blob grad_output[0] target_layer model.features[-1] forward_handle target_layer.register_forward_hook(forward_hook) backward_handle target_layer.register_full_backward_hook(backward_hook) logits model(input_tensor) score logits[0, target_class] score.backward() weights gradient_blob.mean(dim(-1, -2), keepdimTrue) cam (weights * feature_blob).sum(dim1, keepdimTrue).squeeze(1) cam torch.relu(cam) cam torch.nn.functional.interpolate( cam.unsqueeze(0), sizeinput_tensor.shape[-2:], modebilinear) forward_handle.remove() backward_handle.remove() return camregister_full_backward_hook在较新的 PyTorch 版本里能拿到输出梯度比register_backward_hook更稳定。weights是每个通道的梯度均值表示该通道对目标类的重要程度feature_blob是前向特征图两者相乘再求和就得到每个空间位置的重要性。interpolate把 7x7 的热力图放大到 224x224叠加到原图上。实际操作时我会把每个误分类样本的热力图和预测概率一起存成一张图观察模型是不是被背景误导。比如一个森林图像分类任务里模型把树叶阴影里的菌类识别成了树干热力图往往集中在阴影边缘而不是菌类本体这时就知道该去补这样的训练样本而不是盲目调阈值。更进一步的验证是只保留热力图高亮区域的像素把其余区域改成纯色再跑一次推理如果模型对这个“裁剪版”图片的预测仍然正确说明模型学到了鲁棒特征如果预测崩溃说明它只是记住了背景信息。这套验证方法推荐在部署前做一遍能筛选掉一批“看起来准确率高但实际不可用”的模型。本文还有配套的精品资源点击获取
返回列表