
简介面向FasterNet在图像分类任务中的落地实战资料包适合有一定深度学习基础、希望复现并对比前沿网络的开发者。内容围绕FasterNet新提出的Partial卷积PConv展开突出其相比MobileViT在多个平台上的速度与精度优势覆盖数据准备、模型搭建、训练推理的主要流程。FasterNet-L在ImageNet上可达83.5%的top-1精度与Swin-B相当且GPU吞吐更高、CPU耗时更省。压缩包共包含2000个文件以2433张png图片构成图像数据集另有7个Python脚本负责数据处理与训练逻辑以及json类别配置、pth预训练权重和txt说明文件整体大小约847.88MB。目前已有1609人下载学习借助其中脚本和权重可直接搭建FasterNet分类环境快速理解PConv模块设计也可迁移至自己的图像分类项目。1. FasterNet实战图像分类任务如何从“调参跑通”走向“真的好用”同样是做图像分类用 FasterNet 的收益不是玄学开源的轻量 CNN 模型里它在 CPU 和边缘设备上的吞吐能力比同量级 MobileNet 高出一截靠的不是堆参数而是用 Partial ConvolutionPConv同时砍掉冗余计算和内存访问。实战角度讲FasterNet 是那种“训练能收敛、部署不吃亏”的网络GPU 上跑不输主流轻量 CNNCPU 上优势更明显。这篇笔记面向想把它真正用起来的人——先讲清 PConv 到底改在哪一步再给出完整的分类训练与推理代码最后把训练慢、精度上不去、加载权重报错这类翻车点逐条拆开。2. 跑通 FasterNet 之前环境选型、数据集准备与预处理管线2.1 PyTorch 环境与硬件选型CPU 也能跑通的最小前置条件FasterNet 的官方实现基于 PyTorch整个模型结构不依赖第三方算子库标准 PyTorch 就能训练。我一般建议直接装 PyTorch 2.x2.x 里的torch.compile和channels_last内存布局优化对 PConv 这种“部分通道计算、部分通道直连”的结构有天然收益编译后推理延迟能再降一些。# 新建虚拟环境后安装 CPU 版跑通流程后再补 CUDA 版 python -m venv fasternet_env source fasternet_env/bin/activate pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # GPU 机器再安装 CUDA 版以 cu121 为例CPU 版和 CUDA 版二选一 # pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121参数说明训练图像分类模型显存 8GB 以上能把 batch size 开到 128 或更大4GB 显存也能训练小规模数据集只是要把输入分辨率降到 160 附近。没有 GPU 也不影响跑通流程——FasterNet 在设计上本来就是面向 CPU 推理优化的CPU 上训练 Tiny 规模模型、跑几百个 step 验证 Loss 在下降完全可行。验证环境装好直接执行python -c import torch; print(torch.__version__)能输出版本号就说明环境没问题。2.2 从 CIFAR10 冒烟测试到自建数据集分类任务的数据加载模板图像分类实战第一步是让代码跑通而不是追求精度。我习惯先用 CIFAR10 做冒烟测试CIFAR10 下载简单、类别数量少、一个 epoch 几分钟就能跑完适合验证模型定义、前向传播、Loss 计算和保存加载逻辑有没有写错。跑通后再换自建数据集用torchvision.datasets.ImageFolder加载即可不需要改模型代码。自建数据集的目录结构长这样data/ ├── train/ │ ├── cat/ # 类别名就是文件夹名 │ │ ├── cat_001.jpg │ │ └── cat_002.jpg │ └── dog/ │ ├── dog_001.jpg │ └── dog_002.jpg └── val/ ├── cat/ └── dog/下面是通用的数据加载代码CIFAR10 和自建数据集都能用import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms def build_dataloader(data_dir, image_size224, batch_size128, num_workers8, trainTrue): if train: transform transforms.Compose([ transforms.RandomResizedCrop(image_size, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.3, 0.3, 0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) else: transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(image_size), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) dataset datasets.ImageFolder(data_dir, transformtransform) loader DataLoader( dataset, batch_sizebatch_size, shuffletrain, num_workersnum_workers, pin_memoryTrue, drop_lasttrain, # 训练时丢弃最后不完整 batch稳定性更好 ) return loader, dataset.classes逻辑说明ImageFolder按文件夹名自动生成类别索引返回值dataset.classes是有序的类别名字列表之后推理阶段做类别名映射就要靠它。shuffleTrue只在训练集开启验证集保持固定顺序。drop_lasttrain的意思是训练时如果最后一个 batch 不足batch_size就丢掉避免 BatchNorm/LayerNorm 在小 batch 上统计不稳定——FasterNet 虽然用 LayerNorm 更多但 Loss 计算和优化器更新仍然偏好稳定的 batch 大小。参数说明RandomResizedCrop的scale(0.6, 1.0)是实战里比较稳的取值。很多教程写(0.08, 1.0)那是 ImageNet 上配合 120 个 epoch 慢慢训练用的自己训练 60 个 epoch 时裁剪太狠会导致验证集精度上不去后面避坑章节会细说。num_workers在 Linux 上可以开到 8 或 16Windows 上建议开到 2避免多进程数据加载卡死。2.3 输入尺寸抉择为什么我建议直接上 224 而不是 32FasterNet 的 stem 是 4 倍下采样输入 224x224 的图像经过 stem 变成 56x56后续 Stage 再逐步降到 7x7最后一层全局池化成特征向量做分类。如果你直接把 CIFAR10 的 32x32 原图喂进去经过 stem 只剩 8x8再经过几层下采样到分类头前特征图已经退化到 2x2 甚至 1x1空间信息几乎全丢。常见做法是把 CIFAR10 先Resize到 224 再进网络。这样虽然改变了原始输入分布但作为冒烟测试完全够用而且能真实反映 FasterNet 在标准分辨率下的行为。自建数据集就不用纠结了统一按 224x224 处理Resize 到 256 后 CenterCrop 224 是 ImageNet 一脉相承的预处理方式。预处理里还有个隐蔽的坑Normalize的均值和标准差不要自己拍脑袋填。用 ImageNet 预训练权重时必须用[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]否则输入分布不匹配第一层卷积得到的特征就偏了。从零训练时用这套参数也算合理初始化不用改。3. 用 FasterNet 搭建图像分类模型PConv 原理与完整训练管线3.1 Partial Convolution 到底“省”在哪一步普通 CNN 卷积层对输入的所有通道做计算每个输出像素都要聚合全部输入通道的信息。问题是冗余计算非常多——相邻通道的特征高度相似逐通道全算一遍很多乘法结果几乎相同。FasterNet 提出的 PConvPartial Convolution改变的是这个基本操作把输入特征图在通道维度上切成两份只对其中一部分做标准 3x3 卷积另一部分原样通过最后在通道维度上拼回去。从 FLOPs 角度看PConv 只计算了 1/4 通道的卷积理论计算量降到普通卷积的 1/4。更关键的是内存访问量——普通卷积要把整个输入特征图读进计算单元PConv 只需读一部分而内存访问往往比浮点计算更拖慢实际推理速度。这解释了为什么 FasterNet 在 GPU 上的理论 FLOPs 并不惊艳但实测吞吐FPS能超过 MobileNet 同量级模型。PConv 之后还需要用 1x1 卷积Pointwise Conv做通道信息混合这样切掉的那部分通道信息不会永远隔离网络表达能力不会受损。3.2 最小可复现的 FasterNet 模型定义从 PConv 到分类头下面这个实现是我整理后的最小版本去掉官方代码里复杂的配置化工厂保留 PConv、倒残差 Block、下采样 Stage 和分类头直接复制就能训练。代码里每一层都写了注释方便对照论文结构。import torch import torch.nn as nn import torch.nn.functional as F class PartialConv3x3(nn.Module): 核心算子只对前 split 个通道做 3x3 同通道卷积其余通道恒等通过。 dim: 输入通道数 ratio: 参与卷积的通道比例默认 0.25即 1/4 通道做卷积 def __init__(self, dim, kernel_size3, stride1, padding1, ratio0.25): super().__init__() self.split max(1, int(dim * ratio)) self.conv nn.Conv2d( self.split, self.split, kernel_sizekernel_size, stridestride, paddingpadding, groupsself.split, biasFalse, ) def forward(self, x): x1 self.conv(x[:, :self.split]) # 前 split 通道走卷积 x2 x[:, self.split:] # 其余通道直连 return torch.cat([x1, x2], dim1) # 拼回原通道数 class FasterNetBlock(nn.Module): 倒残差 BlockPConv - LayerNorm - 1x1 升维 - GELU - 1x1 降维带残差。 dim: 输入输出通道数 expansion: 倒残差中间通道放大倍数 def __init__(self, dim, expansion4, pconv_ratio0.25): super().__init__() self.pconv PartialConv3x3(dim, ratiopconv_ratio) self.norm nn.LayerNorm(dim) self.pw1 nn.Conv2d(dim, dim * expansion, 1, biasFalse) self.act nn.GELU() self.pw2 nn.Conv2d(dim * expansion, dim, 1, biasFalse) def forward(self, x): identity x x self.pconv(x) b, c, h, w x.shape # LayerNorm 在通道维做归一化这里把 NCHW 转成 [B, L, C] x x.permute(0, 2, 3, 1).reshape(b, h * w, c) x self.norm(x) x x.reshape(b, h, w, c).permute(0, 3, 1, 2) x self.pw2(self.act(self.pw1(x))) return identity x class Stage(nn.Module): 每个 Stage 先做 2x2 卷积下采样并翻倍通道再堆叠多个 FasterNetBlock。 def __init__(self, dim, depth, expansion4, pconv_ratio0.25, downsampleFalse): super().__init__() self.downsample None if downsample: self.downsample nn.Conv2d(dim, dim * 2, 2, stride2, biasFalse) dim dim * 2 self.blocks nn.Sequential(*[ FasterNetBlock(dim, expansion, pconv_ratio) for _ in range(depth) ]) def forward(self, x): if self.downsample is not None: x self.downsample(x) return self.blocks(x) class MinimalFasterNet(nn.Module): 最小 FasterNet 分类模型。 stem_dim: stem 输出通道数 depths: 三个 Stage 各自的 Block 数量 num_classes: 分类类别数 def __init__(self, num_classes1000, stem_dim40, depths(2, 2, 4), expansions(4, 4, 4), ratios(0.25, 0.25, 0.25)): super().__init__() self.stem nn.Sequential( nn.Conv2d(3, stem_dim, 4, stride4, biasFalse), nn.LayerNorm(stem_dim), ) stage_dims [stem_dim, stem_dim * 2, stem_dim * 4] self.stages nn.ModuleList() for i, depth in enumerate(depths): self.stages.append(Stage( stage_dims[i], depth, expansionexpansions[i], pconv_ratioratios[i], downsample(i 0), # 第一个 Stage 不额外下采样 )) self.head nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(stage_dims[-1], num_classes), ) def forward(self, x): x self.stem(x) for stage in self.stages: x stage(x) return self.head(x)逻辑说明整个网络分三段——stem 用 4x4 步长 4 的卷积把 224x224 输入压缩到 56x56同时把通道数从 3 扩到stem_dim三个 Stage 依次下采样分辨率从 56 降到 28、14、7通道数各翻一倍最后全局平均池化接全连接层输出类别 logits。FasterNetBlock里的残差连接让梯度跨 Block 直接回传这是训练深度的保障。参数说明stem_dim40对应官方 FasterNet-T0 级别的宽度depths(2, 2, 4)是官方 Tiny 变体的深度配置。如果数据量大、算力充足可以改成stem_dim64, depths(2, 4, 6)精度会涨但参数量和计算量也会上去。expansion4表示倒残差结构会把通道临时放大 4 倍再压回来这个值直接决定 Block 里的中间特征图大小显存吃紧时可以降到 2。pconv_ratio0.25是论文里推荐的比例不建议调大——调大后 PConv 的省时优势会明显减弱。3.3 训练脚本超参数、调度器与损失的完整配置训练分类模型最省心的超参数组合是SGD 余弦退火 Label Smoothing。FasterNet 的 Block 里同时有 LayerNorm、GELU 和残差结构SGD 配 momentum 0.9 的收敛稳定性比 AdamW 更好尤其是在 60 epoch 这种中等训练长度下。下面这段训练主循环可以直接嵌进任何 PyTorch 工程里。import math import time import torch import torch.nn as nn from torch.cuda.amp import autocast, GradScaler def adjust_lr(optimizer, epoch, base_lr, warmup_epochs, total_epochs): warmup 线性升 lr之后 cosine 退火。 if epoch warmup_epochs: lr base_lr * (epoch 1) / warmup_epochs else: progress (epoch - warmup_epochs) / (total_epochs - warmup_epochs) lr base_lr * 0.5 * (1.0 math.cos(math.pi * progress)) for param_group in optimizer.param_groups: param_group[lr] lr def train_one_epoch(model, loader, criterion, optimizer, device, scalerNone): model.train() running_loss, running_correct, total 0.0, 0, 0 t0 time.time() for images, labels in loader: images, labels images.to(device), labels.to(device) with autocast(enabledscaler is not None): outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() if scaler is not None: scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() else: loss.backward() optimizer.step() running_loss loss.item() * labels.size(0) running_correct (outputs.argmax(1) labels).sum().item() total labels.size(0) avg_loss running_loss / total acc running_correct / total print(fEpoch {epoch} train loss{avg_loss:.4f} acc{acc:.4f} ftime{time.time() - t0:.1f}s lr{optimizer.param_groups[0][lr]:.5f}) return avg_loss, acc def main_train(): device torch.device(cuda if torch.cuda.is_available() else cpu) train_loader, class_names build_dataloader( data/train, image_size224, batch_size128, trainTrue) model MinimalFasterNet(num_classeslen(class_names), stem_dim64, depths(2, 2, 4)).to(device) criterion nn.CrossEntropyLoss(label_smoothing0.1) optimizer torch.optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay5e-4) total_epochs, warmup_epochs, base_lr 60, 5, 0.1 scaler GradScaler() if device.type cuda else None for epoch in range(total_epochs): adjust_lr(optimizer, epoch, base_lr, warmup_epochs, total_epochs) train_one_epoch(model, train_loader, criterion, optimizer, device, scaler) torch.save(model.state_dict(), fcheckpoints/fasternet_epoch{epoch}.pt)逻辑说明adjust_lr在训练前 5 个 epoch 把学习率从 0 线性升到 0.1之后按余弦曲线降到接近 0。warmup 的作用是避免模型一开始就用大步长在随机初始化参数上乱撞分类任务里这一步能明显降低前期 Loss 震荡概率。GradScaler只在 GPU 上开启CPU 上用混合精度没有收益反而会拖慢速度。参数说明base_lr0.1配合batch_size128是标准配置。如果你的 batch size 变成 256学习率可以按比例涨到 0.2batch size 掉到 64学习率最好减到 0.05。label_smoothing0.1会让 Loss 计算不再把正确类别的概率推到 1.0而是退到 0.9余量 0.1 摊给其他类别。这个技巧对 FasterNet 这类轻量模型特别有用能明显改善验证集泛化。weight_decay5e-4是 ImageNet 训练的标准值自建小数据集上可以降到 1e-4 甚至 5e-5防止正则过强导致欠拟合。4. 验证与单图推理别只在训练集上自嗨4.1 测试集评估准确率与 Top-5 一起看训练过程的 Loss 下降只能说明模型在拟合训练集真正决定项目能不能交付的是测试集指标。图像分类评估最低限度要看 Top-1 准确率如果类别数超过 100Top-5 也应该一起算——很多相似类别比如不同品种的狗模型分不清是正常的Top-5 能反映模型是否“知道大概是什么”。def evaluate(model, loader, device, topk(1, 5)): model.eval() correct {k: 0 for k in topk} total 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) pred outputs.topk(max(topk), dim1).indices # [B, maxk] for k in topk: # 每个样本的预测标签里是否包含真实标签 correct[k] (pred[:, :k] labels[:, None]).sum().item() total labels.size(0) return {ftop{k}_acc: correct[k] / total for k in topk} # 使用示例验证集加载时 trainFalse val_loader, class_names build_dataloader( data/val, image_size224, batch_size128, trainFalse) metrics evaluate(model, val_loader, device, topk(1, 5)) print(metrics)逻辑说明outputs.topk(max(topk), dim1)一次算出每个样本前 5 个最高概率的类别索引然后逐个检查真实标签是否落在这个集合里。这里用labels[:, None]把标签张量从[B]扩成[B, 1]与预测索引[B, k]做广播比较结果就是[B, k]的布尔张量sum()统计的是命中次数。参数说明验证时模型必须切到eval()模式LayerNorm 在训练和推理时行为不同——训练时用当前 batch 统计量推理时用累计的 running 统计量忘记切换会导致验证指标忽高忽低。另外with torch.no_grad()一定要包住整个推理循环否则每个 batch 都会为梯度计算额外分配显存batch size 一大很容易 OOM。4.2 单张图像预测把模型输出变成人能读懂的类别名测试集指标是给项目汇报用的实际使用中更多场景是输入一张图、输出前 3 个可能的类别。这里要注意datasets.ImageFolder的类别索引是字母序而非文件夹创建顺序所以必须用dataset.classes做索引到名字的映射不能用自己记忆的顺序。from PIL import Image import torchvision.transforms as transforms def predict_single(model, image_path, class_names, device): 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]), ]) image Image.open(image_path).convert(RGB) # 统一转 RGB避免 RGBA/灰度图报错 x transform(image).unsqueeze(0).to(device) # [1, 3, 224, 224] model.eval() with torch.no_grad(): logits model(x) probs torch.softmax(logits, dim1) top3 probs.topk(3, dim1) for rank in range(3): idx top3.indices[0, rank].item() score top3.values[0, rank].item() print(fTop-{rank 1}: {class_names[idx]:30s} {score * 100:.2f}%)逻辑说明softmax把 logits 转成概率分布后topk(3)返回概率最高的 3 个类别索引和对应概率值。class_names[idx]从 ImageFolder 生成的类别列表里取名字保证输出结果和训练时的标签空间一致。参数说明convert(RGB)是必须的。很多真实图片是 RGBA 四通道或单通道灰度图不转换的话transforms.ToTensor()会得到 4 通道或 1 通道张量和模型输入维度对不上直接报错。推理阶段用 256 Resize 224 CenterCrop跟验证集预处理完全一致保证输入分布统一。5. FasterNet 图像分类避坑指南五个常见翻车点与排查5.1 训练 Loss 直接变成 NaN 或前期剧烈震荡现象训练第一个 epoch 还没跑完loss 输出为nan或者从 3.0 左右剧烈震荡到负值再跳回 NaN。验证集准确率始终在随机水平附近。原因最常见的是学习率过大。SGD 在base_lr0.1以上时FasterNet 的 stem 是 4x4 步长 4 卷积参数初值方差比常规 3x3 stem 更大大步长更新很容易把 BN/LN 的统计量打飞。其次是数据问题——自建数据集里某些图片损坏或全黑Normalize后出现异常值经 GELU 放大后溢出。混合精度判断也可能出错autocast下 LayerNorm 内部统计在低精度下不稳定尤其 batch size 小于 32 时。解决先把学习率降到 0.01 跑 10 个 epoch确认 Loss 能稳定下降再逐步调回 0.1。同时检查数据集里有没有损坏图片写个小脚本遍历所有图片尝试Image.open(...).load()捕获异常文件并删除。混合精度问题先关掉scaler纯 FP32 训练。如果这三步都做过仍然 NaN检查weight_decay是否设成了类似 5e-2 这种异常值——我踩过一次正则过强导致的 Loss 发散教训深刻。5.2 训练 Loss 正常下降验证集精度却卡住不动现象训练 Loss 从 2.5 降到 0.3训练准确率接近 95%但验证集准确率在 60% 左右徘徊连续 10 个 epoch 没有提升。不同 seed 训练结果方差也很大。原因典型过拟合 数据增强不匹配的组合。RandomResizedCrop的scale(0.08, 1.0)在短训练周期下裁剪出太多难例模型把训练集“背”下来了但没学到泛化特征。另一个原因是验证集预处理和训练集差异过大——训练用了 RandomResizedCrop验证只 CenterCrop物体大小在训练里变化剧烈验证时模型没见过这种尺度的人为裁剪。解决把RandomResizedCrop的scale提到(0.6, 1.0)减少极端裁剪epoch从 60 加到 90配合label_smoothing0.1加正则。验证集预处理保持 256 Resize 224 CenterCrop 不动与训练集只差在随机增强上。如果验证集精度仍然不动检查类别分布是否均衡——少数类样本数少于 50 时可以考虑按类别权重重采样或者干脆用类别数更少的简化版数据集跑通流程再说。5.3 模型推理速度反而不如 MobileNetFasterNet 白用了现象用torch.utils.bottleneck或者简单循环计时发现 FasterNet 在 CPU 上单张推理 80ms同量级 MobileNetV3 是 60ms与论文里 FasterNet 更快的结论相反。原因大概率是没把 PConv 实现到位。常见的错误实现是用普通nn.Conv2d(dim, dim, 3, padding1)代替分组卷积再手动 mask 通道——这等于算了全通道卷积PConv 的省算量优势直接没了。还有一个原因是测试方法不对单张图片推理延迟受线程调度影响很大而且 FastetNet 的设计优势在大 batch 连续推理的吞吐量上不在单张延迟上。CPU 推理时没用channels_last内存布局也少了一大截优化空间。解决确认 PConv 实现里groupsself.split没丢这是确保只有部分通道参与卷积的关键。测速时不要单张循环应该固定 batch size 为 32 或 64连续推理 100 次取平均公式是总图片数 / 总耗时得到吞吐量。CPU 上加载模型后执行model model.to(memory_formattorch.channels_last)输入图像也同步做.to(memory_formattorch.channels_last)常见情况下能再吃下 5% 到 15% 的提速空间。5.4 加载官方预训练权重时报 size mismatch现象load_state_dict报错提示某个 Linear 层或 Conv 层的大小不匹配比如size mismatch for head.1.weight: copying a param with shape torch.Size([1000, 320]) from checkpoint, the checkpoint has shape torch.Size([1000, 256])。原因官方 FasterNet 的分类头是 1000 类 ImageNet 输出自建数据集类别数不是 1000 必然报错。另一个隐蔽原因是官方实现的 Stem 和 Stage 配置与我的MinimalFasterNet的通道数不完全一致即使 Stem 相同后续 Stage 的 depth 或 expansion 不同也会导致 shape 错位。解决只加载主干权重跳过分类头。执行ckpt torch.load(...)后过滤掉key里包含head或classifier的项再load_state_dict(ckpt, strictFalse)。如果主干也报 mismatch说明自建模型的stem_dim、depths和官方不一——要么完全对齐官方的通道配置要么放弃预训练直接从头训。自建数据集和 ImageNet 分布差异大时预训练权重的收益没有想象中高从零训练反而更可控。5.5 混合精度训练后验证精度掉点明显现象开启 AMP 后训练速度提升了 30%但验证集 Top-1 准确率比纯 FP32 低了 0.8 到 1.5 个百分点而且 Loss 曲线看起来和 FP32 差不多。原因FasterNet 里的 LayerNorm 和 GELU 对数值范围敏感。FP16 下 LayerNorm 的均值/方差计算精度不足GELU 在负半轴的指数运算在低精度下误差放大。残差连接把每个 Block 的微小误差逐层累积到分类头前面特征已偏离 FP32 轨迹。解决AMP 下强制让 LayerNorm 保持 FP32 计算。PyTorch 的autocast默认会为 LayerNorm 选择 FP32 解释器但如果手动改过torch.backends.cudnn.allow_tf32之类开关可能被子类覆盖。一个稳妥做法是训练完成后用 FP32 精度再微调 3 到 5 个 epoch把数值误差修正回来这也是模型收敛阶段一个比较实用的收尾技巧。推理阶段如果也开半精度建议对比 FP32 和 FP16 的精度差异再决定是否上线不是所有算子都适合半精度。6. 把 FasterNet 用得更狠推理延迟对比、ONNX 导出与半精度部署训练跑通、指标正常只是第一步。图像分类项目最终要落到部署部署阶段我维护一套固定的对比流程先测参考模型的吞吐量再测 FasterNet 的吞吐量最后导出 ONNX 做端侧验证。测吞吐量的代码并不复杂关键是“连续推理 多 batch 足够多次取平均”import torch, time def benchmark_throughput(model, input_shape(1, 3, 224, 224), batch_size64, repeats100, devicecpu): model.eval().to(device) if device cpu: model model.to(memory_formattorch.channels_last) x torch.randn(batch_size, *input_shape[1:], devicedevice) if device cpu: x x.to(memory_formattorch.channels_last) with torch.inference_mode(): for _ in range(10): # 预热让缓存和算子调度稳定 model(x) torch.cuda.synchronize() if device cuda else None t0 time.perf_counter() for _ in range(repeats): model(x) torch.cuda.synchronize() if device cuda else None elapsed time.perf_counter() - t0 total_images batch_size * repeats return total_images / elapsed # 单位FPS # 对比示例 # fasternet_fps benchmark_throughput(model, devicecpu) # mobilenet_fps benchmark_throughput(mobilenet, devicecpu)这段对比脚本最大的价值是让“快”变得可量化。我自己在跑对比时发现FasterNet 在 batch 32 上的 CPU 吞吐量领先 MobileNetV3 约 15% 到 25%但单张延迟只快 5% 左右所以如果你的部署场景是“请求逐个进来”的在线推理优势会被稀释如果是“批量图片离线跑”的离线任务优势就很明显。ONNX 导出是部署到 OpenVINO、ONNX Runtime 和 TensorRT 的必经环节。FasterNet 里的操作都是标准算子不需要自定义插件导出命令很干净dummy torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy, fasternet.onnx, input_names[input], output_names[logits], opset_version13, # 13 以上对 GELU 支持更完整 dynamic_axes{input: {0: batch}, logits: {0: batch}}, )导出后建议用onnxruntime跑一遍单图推理和 PyTorch 结果对比。如果差异大于 1e-3优先检查opset_version是否过低GELU 在较老版本的 ONNX 算子集里会被拆成误差较大的近似实现。半精度部署也是类似逻辑——导出 FP16 ONNX 后逐层检查精度而不是只看最终输出。半精度推理的加速收益在 CPU 上并不明显在 GPU 上配合 TensorRT 才有实际价值。我的习惯是FasterNet 这类轻量 CNNCPU 上优先保精度用 FP32GPU 上再用 FP16 做推理。FP16 模型在 TensorRT 里通常能比 FP32 快 1.5 到 2 倍但前提是 LayerNorm 的累积误差没有超过项目精度阈值。验证方法很简单跑 1000 张验证集图片统计 FP16 和 FP32 的 Top-1 差异差异小于 0.3% 就上线大于 0.5% 就退回 FP32。最后说一个我这几年调试轻量 CNN 养成的习惯拿到任何新结构先做一遍“延迟拆解”——把 Stem、每个 Stage、分类头分别计时画成柱状图。哪个阶段耗时异常再去翻对应实现。FasterNet 对新手最有价值的地方不在精度数字而在它把“内存访问也是开销”这个观点做成了可实测的模型顺着这条思路去理解其他轻量网络会顺手很多。希望这篇实战笔记能帮你在自己的数据集上少走几条弯路。本文还有配套的精品资源点击获取