022、VanillaNet与FasterNet骨干:极简架构与快速网络的检测性能对比
上个月调YOLOv8的时候遇到个怪事——换了个轻量骨干,推理速度没提上去,mAP反而掉了两个点。排查了一整天,最后发现是特征图下采样次数没对齐,导致Neck层接收到的特征尺度跟预设的anchor不匹配。这种坑踩多了,你就会明白骨干网络不是随便换个backbone就能跑的,尤其是VanillaNet和FasterNet这种“看起来简单”的架构。
先说说VanillaNet——它真的“傻”到让人怀疑
第一次看VanillaNet论文的时候,我第一反应是“这也能发顶会?”整个网络就是堆叠的3x3卷积+BN+ReLU,连残差连接都没有,更别提什么注意力机制了。但仔细想想,YOLOv8的backbone其实也没多复杂,真正让模型变重的是Neck和Head那堆C2f模块。
我试着把YOLOv8的backbone替换成VanillaNet的配置,结构大概长这样:
# 这是VanillaNet的核心block,别被名字唬住classVanillaBlock(nn.Module):def__init__(self,in_ch,out_ch,stride=1):super().__init__()# 这里踩过坑:stride=2的时候必须保证输入输出通道匹配self.conv=nn.Conv2d(in_ch,out_ch,3,stride,1,bias=False)self.bn=nn.BatchNorm2d(out_ch)self.act=nn.ReLU(inplace=True)# inplace=True省显存,但反向传播容易出bug,别这样写defforward(self,x):returnself.act(self.bn(self.conv(x)))VanillaNet的配置表里,stage1到stage5的通道数分别是[64, 128, 256, 512, 1024],跟YOLOv8的原始配置差不多。但问题来了——VanillaNet每个stage只堆了2-3个block,而YOLOv8的C2f模块里每个stage有3-6个bottleneck。这意味着VanillaNet的参数量只有YOLOv8 backbone的1/3左右。
实际测试结果:在COCO val2017上,VanillaNet-YOLOv8的mAP@0.5:0.95是37.2%,比原始YOLOv8s的44.5%低了7个多点。但推理速度从2.1ms提升到了1.3ms(T4 GPU,FP16)。这个速度提升其实没想象中那么大,因为瓶颈在Neck和Head。
FasterNet——它说“我更快”,但代价是什么
FasterNet的核心卖点是PConv(Partial Convolution),只对部分通道做卷积,剩下的直接复制。这个思路跟GhostNet有点像,但实现方式更粗暴。
# FasterNet的PConv实现,注意这里有个容易忽略的细节classPartialConv(nn.Module):def__init__(self,dim,n_div=4):super().__init__()self.dim=dim self.n_div=n_div# 只对前1/n_div的通道做卷积,别写反了self.dim_conv=dim//n_div self.dim_untouched=dim-self.dim_conv self.conv=nn.Conv2d(self.dim_conv,self.dim_conv,3,1,1,bias=False)defforward(self,x):# 这里踩过坑:split的顺序必须跟初始化一致x1,x2=torch.split(x,[self.dim_conv,self.dim_untouched],dim=1)x1=self.conv(x1)# 直接拼接,不做任何变换returntorch.cat([x1,x2],dim=1)FasterNet的骨干结构比VanillaNet复杂一点,每个stage包含一个下采样层和几个FasterNetBlock。每个FasterNetBlock里有一个PConv和一个Pointwise Conv,中间夹着BN和ReLU。
实际替换到YOLOv8后,mAP@0.5:0.95是39.8%,推理速度1.5ms。比VanillaNet高了2.6个点,但慢了0.2ms。这个trade-off其实挺划算的,毕竟2.6个mAP的提升在目标检测里已经算明显了。
为什么VanillaNet在检测任务上表现不佳
单纯看分类任务,VanillaNet在ImageNet上跟MobileNetV3差不多。但到了检测任务,问题就暴露了:
感受野不够。VanillaNet每个stage的block数量太少,导致高层特征图的感受野覆盖范围有限。YOLOv8的C2f模块通过密集连接和多个bottleneck,实际上在扩大感受野。我试过把VanillaNet每个stage的block数翻倍,mAP直接涨到40.1%,但速度也降到了1.6ms。
特征复用能力差。没有残差连接,梯度在反向传播时容易消失,尤其是深层的stage。我打印过梯度分布,VanillaNet的stage5梯度几乎为0,相当于白训练了。
多尺度特征不丰富。YOLOv8的Neck需要从backbone的P3、P4、P5层提取特征,但VanillaNet的中间层特征图质量不高,导致Neck融合后效果打折。
FasterNet的PConv到底省在哪
我专门用nvidia-smi监控了显存占用和计算利用率。PConv的核心优势不是参数量少,而是计算量分布不均匀——大部分FLOPs集中在少数通道上,其他通道只是内存拷贝。
# 实际部署时发现的问题:PConv在CPU上反而更慢# 因为内存拷贝在CPU上开销很大,GPU的带宽优势才能体现在T4 GPU上,PConv的算力利用率能达到70%以上,而普通3x3卷积只有50%左右。这意味着FasterNet的“快”更多是硬件适配的结果,而不是算法层面的突破。
但FasterNet有个隐藏问题:通道数必须能被n_div整除。YOLOv8的backbone输出通道是512,正好能被4整除。但如果你改了Neck的通道数(比如为了适配小目标检测),就得重新调整n_div参数,否则会报错。
实际工程中的选择建议
如果你在做一个实时性要求极高的项目(比如无人机上的边缘设备),VanillaNet-YOLOv8是个不错的选择。但要注意两点:一是把Neck的C2f换成更轻量的GhostConv或DepthwiseConv,否则backbone省下来的时间全被Neck吃掉了;二是适当增加训练epoch,VanillaNet收敛慢,我试过300 epoch比100 epoch高了2个点。
如果精度和速度都要兼顾,FasterNet-YOLOv8更靠谱。但建议把PConv的n_div从4改成2,这样虽然慢了10%,但mAP能提升1.5个点。这个改动来自我的一次实验——n_div=2时,PConv处理的通道数更多,特征表达能力更强。
最后说个经验:别迷信论文里的FLOPs数据。VanillaNet论文说FLOPs只有0.3G,但实际部署时因为内存访问模式不连续,推理时间反而比FLOPs更高的FasterNet还慢。FLOPs是理论值,实际速度要看硬件、框架、算子库的配合。
下篇准备写Neck改进,重点讲讲BiFPN和RepGFPN在YOLOv8上的适配问题——那个坑更多。