☰
GhostNet深度解析:利用特征冗余实现的轻量级卷积网络
2026/10/5 6:04:11 网站建设 项目流程

GhostNet这篇论文,全称《GhostNet: More Features from Cheap Operations》,是华为诺亚方舟实验室2019年发表在CVPR 2020上的工作。做模型轻量化或者端侧部署的朋友,大概率都听过这个名字。它的核心思想可以用一句话概括:卷积网络输出的特征图里,有大量特征图之间存在冗余,与其老老实实把每个特征图都算出来,不如先算一小部分“本征特征图”,再用一组极廉价的线性操作把它们“补全”成其余那些高度相似的特征图。论文给出一个非常诱人的结论:理想情况下能省约一半甚至更多的计算量,而精度几乎不掉。

说实话,我读完这篇论文的第一反应是“这个思路居然到现在才有人做出来”。很多搞模型设计的人早就隐约感觉到,卷积层输出的特征图里存在大量冗余——只要随便可视化一层特征图,就能看到成对出现的相似图。但大多数人只是把这个当作一个现象聊一聊,GhostNet的作者却直接把这个现象作为整个架构设计的出发点,做出了一个可以落地的轻量网络。

今天这期孤读paper,我会按自己复现和部署时的理解,把GhostNet从设计动机、模块拆解、代码实现到工程落地,一层层展开。适合两类人精读:一类是做端侧视觉模型、天天琢磨怎么砍FLOPs的工程师;另一类是刚接触轻量网络、想搞明白GhostNet和MobileNet系列到底差在哪的研究生。老规矩,文章里凡是我自己踩过的坑,都会标注清楚。

1. 核心动机:从“冗余特征”出发的架构设计

1.1 卷积输出特征图中普遍存在的冗余现象

论文开篇用一个很直观的观察切入:把训练好的网络某一层输出的特征图拿出来看,会发现大量成对甚至成组的特征图,轮廓、纹理、语义几乎一模一样,差别可能只是某个方向上的平移、某个微小角度的旋转,或者对比度上的细微变化。而且这种冗余不是个别层才有,是普遍现象——随着网络加深、通道数增多,冗余的比例不降反升。

我自己的理解是,卷积层本质上在做一个“从输入到特征空间的展开”,而这个展开天然存在冗余。比如一张猫脸图片经过第一层卷积后,可能会同时产生“左边耳朵边缘检测图”和“右边耳朵边缘检测图”,这两张特征图在结构上高度相似,只是一个在左、一个在右。网络又不可能知道哪张图的信息更重要,它只能把所有这些可能用到的特征都算出来,让后面的层自己挑。于是,大量冗余特征图就这么被生成、传递、然后被后续层忽略或弱化。

GhostNet作者抓住这个观察,提了一个非常直接的问题:既然那些冗余特征图和某些代表性子集高度相似,能不能只生成其中一小部分“本征特征图”,然后用一些便宜的变换,把剩下那些高度相似的特征图“补出来”?这里的关键假设是:网络需要的不是一个一个单独算出来的精确特征图,而是一个信息量足够充分的特征图集合。只要这个集合的多样性够,具体每一张图是怎么来的,网络并不关心。

1.2 “先算本征部分,再廉价补全”的设计哲学

Ghost Module的思路,拆开看只有两步。

第一步,用一组普通卷积(论文里实践下来一般用1×1卷积)生成较少的本征特征图,论文里叫intrinsic feature maps,数量记作m。

第二步,对这些本征特征图做逐通道的廉价变换(cheap operation)。论文里试过很多种cheap op,最简单有效的就是3×3的深度卷积(depthwise convolution)。深度卷积只在单通道内部做空间滤波,不跨通道组合,所以计算量比普通3×3卷积少得多。这一步会生成另外一组特征图,论文里叫ghost feature maps。

最后,把第一步和第二步生成的特征图沿通道方向拼接起来,就得到了完整的n个输出通道。整个过程把一个普通卷积的“一次生成n个通道”变成了“先生成m个通道,再廉价扩展到n个通道”。

用一句话总结:普通卷积花重金把每个输出通道都算得明明白白,Ghost Module只认真计算一小部分通道,其余通道用“抠图”的方式低成本补齐。这个比喻可能不太严谨,但能帮助理解核心逻辑:不是所有输出特征都值得用完整卷积去算。

1.3 计算量对比:为什么能省这么多

我用一个直观的算式来说明。假设输入特征图大小是 h×w,通道数 c,输出 n 个通道,卷积核大小 k×k,输出特征图空间大小 h'×w'。

普通卷积的乘法计算量约为:

n · h' · w' · c · k · k

Ghost Module的计算量分两部分:

  • 第一部分(本征特征):m · h' · w' · c · k · k
  • 第二部分(廉价变换):(n - m) · h' · w' · k · k

注意,第二部分深度卷积没有 c 这个因子,因为它逐通道操作,不做跨通道组合。

如果令 m = n/s,n - m = (s-1)n/s,理论加速比约为:

r ≈ s · c / (c + s - 1)

当 c=64、s=2 时,r = 128 / 65 ≈ 1.95,接近2倍。当通道数 c 更高时,r 会更接近 s。也就是说,s=2 意味着“几乎一半的计算量被省下来”。这就是Ghost Module最核心的收益来源。

我自己复现时验证过,Ghost Module的FLOPs节省是真的省,不是那种只在纸面上好看、部署起来原形毕露的“理论优化”。当然,实际时延能不能达到理论加速,取决于硬件对深度卷积的支持程度,这个后面专门说。

1.4 为什么以前没有人这么干

很多人会问:既然这么简单,为什么早期没人想到?MobileNet已经用深度可分离卷积证明了“分解卷积”这条路可行,但它把空间卷积和通道组合彻底解耦,所有输出通道仍然要经过1×1卷积来混合。所以MobileNetV2/V3里,1×1卷积的计算量占了大头,尤其是倒残差结构中的expand层,通道数会先放大,1×1卷积的代价更高。

GhostNet表面上看也是“把卷积拆开”,但本质区别在于:它不是把“空间卷积”和“通道组合”解耦,而是在“输出通道内部”做文章——用极低成本的深度卷积在通道维度上做扩展,而不是对所有输入输出通道做组合。这个视角差异决定了它能在同FLOPs下,把节省下来的预算留给通道组合,从而保持特征表达能力。

所以GhostNet不是MobileNet的简单变体,而是一个“特征生成策略”上的创新。MobileNet的想法是“把卷积算得更高效”,GhostNet的想法是“少算一些东西,让没算的那些被便宜地推测出来”。这两者的哲学不一样。

2. 关键构建单元:Ghost Module与Ghost Bottleneck拆解

2.1 超参s的语义与工程实现里的通道配平

Ghost Module最重要的超参就是 s,论文里也叫 ratio。它表示每个本征特征图平均产生多少个输出特征图。比如 s=2,意味着 m = ceil(n/2) 个本征特征图,剩下的一半通道由cheap operation补全。

这里有个工程上很容易踩的坑:当 n 不能被 s 整除时怎么办?论文里的处理方式是 m = ceil(n/s),然后 new_channels = m × (s-1),两个分支拼接后得到一共 m + m×(s-1) 个通道,这个数可能比 n 多,也可能比 n 少。所以实际实现里,最终输出要做一次通道裁剪,只取前 n 个通道。

这个细节看起来很不起眼,但写代码时非常容易出问题。我第一次实现时偷懒,直接把 m 设为 n//2,new_channels 设为 n - m,结果在 n 为奇数时,两个分支拼接后通道数和预期不一致,后面模块的维度就对不上了。所以,老老实实用 math.ceil 和裁剪,一步都不能省。

2.2 Ghost Bottleneck:类似MobileNetV3 block的堆叠单元

单个Ghost Module做的是通道变换,但要堆出一个完整网络,还需要一个类似“倒残差块”的结构。GhostNet里把这个结构叫Ghost Bottleneck,组织方式和MobileNetV2/V3的bottleneck很像:

第一个Ghost Module负责扩展通道,比如从16扩展到64,相当于一个expand层。

如果 stride=2,中间插一个depthwise卷积做空间降采样。

可选地接一个SE模块(squeeze-and-excitation),论文里默认在某些block加上,reduction通常设为4。

第二个Ghost Module负责把通道压回原数量(或shortcut匹配的数量),注意这一层后面不加ReLU,避免信息损坏。

最后和shortcut相加。

这套结构和MobileNetV3的block放在一起看会非常眼熟,但关键区别在于:MobileNetV3的1×1 expand层和project层都是普通卷积,GhostNet把它们换成了Ghost Module。所以GhostNet本质上是“把MobileNetV3骨架里的普通1×1卷积替换成Ghost Module”,而不是发明了一个全新的block范式。这一点在做模型对比时特别重要:GhostNet强的不是block结构本身,而是对通道冗余的利用方式。

2.3 完整网络的stage配置与width multiplier

我把论文里GhostNet的网络配置大致梳理一下。它和MobileNetV3类似,第一层是标准3×3卷积(stride=2),然后接一个Ghost Bottleneck(stride=2),后面依次是几个stage,每个stage由若干个Ghost Bottleneck堆叠,最后接全局平均池化和一个1×1卷积(或全连接)输出分类。整体大约有6到7个stage,具体的block数量和通道宽度在论文附录里有完整表格。

还有一个重要的工程参数:width multiplier(宽度因子)α。这个和MobileNet里的width multiplier一样,用来控制每层通道数,从而生成不同计算量的网络版本。论文里主要给出了0.5x、1.0x、1.3x等版本,其中1.0x版本在ImageNet上的top-1精度大约75.7%,参数量约5.2M,FLOPs大约150M这个量级。这个精度和MobileNetV3 1.0x相当,但FLOPs更低、推理性价比更好。

实际做项目时,我一般会先用1.0x把基线跑通,再根据端侧延迟预算往0.5x或0.75x方向收缩。注意,直接用width multiplier缩放网络时,精度并不是线性下降的——从1.0x缩到0.75x可能只掉0.5个点,但缩到0.5x可能直接掉1.5个点以上。所以选版本时要留精度余量。

2.4 和MobileNet、ShuffleNet的选型对比

这里给出一张我平时选型用的对比表,帮助理解GhostNet的定位:

模型核心思路典型FLOPs(224输入)特点与适合场景
MobileNetV2倒残差 + depthwise卷积约300M工程生态成熟,部署案例最多
MobileNetV3NAS搜索 + 硬件友好激活约155M移动端CPU友好,精度/时延平衡
ShuffleNetV2通道切分 + 通道重排约146M考虑内存访问成本,GPU/ARM均衡
GhostNet减少冗余特征生成约150M同FLOPs下精度更高,适合理论实测兼得

GhostNet的独特优势在于,它不仅考虑FLOPs,还考虑了实际硬件的内存访问特性。Ghost Module里的concat和depthwise conv都是对硬件相对友好的操作,不需要复杂的自定义算子,这一点在部署时很关键。ShuffleNetV2虽然也强调内存访问成本,但channel shuffle操作在某些框架上支持并不好,GhostNet则没有这个问题。

3. 复现实战:Ghost Module与Ghost Bottleneck的PyTorch实现

3.1 先搭一个最简GhostModule

直接给出我在项目里实际使用的PyTorch实现,为了方便阅读,我做了一些简化:

import math import torch import torch.nn as nn class GhostModule(nn.Module): def __init__(self, in_channels, out_channels, kernel_size=1, ratio=2, dw_kernel_size=3, stride=1, use_relu=True): super().__init__() self.out_channels = out_channels init_channels = math.ceil(out_channels / ratio) new_channels = init_channels * (ratio - 1) self.primary_conv = nn.Sequential( nn.Conv2d(in_channels, init_channels, kernel_size, stride, kernel_size // 2, bias=False), nn.BatchNorm2d(init_channels), nn.ReLU(inplace=True) if use_relu else nn.Identity(), ) self.cheap_operation = nn.Sequential( nn.Conv2d(init_channels, new_channels, dw_kernel_size, 1, dw_kernel_size // 2, groups=init_channels, bias=False), nn.BatchNorm2d(new_channels), nn.ReLU(inplace=True) if use_relu else nn.Identity(), ) def forward(self, x): x1 = self.primary_conv(x) x2 = self.cheap_operation(x1) out = torch.cat([x1, x2], dim=1) return out[:, :self.out_channels]

这里最值得说的是默认参数的设置:ratio=2时精度和计算量的平衡最好,dw_kernel_size=3是论文经过消融后的最优选择。论文里试过5×5以及不同的深度卷积配置(甚至包括直接恒等映射),3×3 depthwise conv在精度和速度平衡上表现最佳。我会把dw_kernel_size作为参数暴露出来,方便做消融实验,而不是在代码里写死。

3.2 GhostBottleneck完整实现与stride处理

接下来是GhostBottleneck,这里有两个关键点:stride=2时要插一个depthwise conv做降采样,并且shortcut也要相应地下采样;SE模块只加在某些层,我用一个参数控制。

class SELayer(nn.Module): def __init__(self, channels, reduction=4): super().__init__() self.fc = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(channels, channels // reduction, 1, bias=False), nn.ReLU(inplace=True), nn.Conv2d(channels // reduction, channels, 1, bias=False), nn.Hardsigmoid(), ) def forward(self, x): return x * self.fc(x) class GhostBottleneck(nn.Module): def __init__(self, in_channels, hidden_channels, out_channels, kernel_size, stride, use_se): super().__init__() assert stride in [1, 2] self.conv = nn.Sequential( GhostModule(in_channels, hidden_channels, kernel_size=1, ratio=2, use_relu=True), nn.Conv2d(hidden_channels, hidden_channels, kernel_size, stride, kernel_size // 2, groups=hidden_channels, bias=False) if stride == 2 else nn.Identity(), nn.BatchNorm2d(hidden_channels) if stride == 2 else nn.Identity(), SELayer(hidden_channels) if use_se else nn.Identity(), GhostModule(hidden_channels, out_channels, kernel_size=1, ratio=2, use_relu=False), ) if stride == 1 and in_channels == out_channels: self.shortcut = nn.Identity() else: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, stride, bias=False), nn.BatchNorm2d(out_channels), ) def forward(self, x): return self.conv(x) + self.shortcut(x)

这段代码里有几个比较深的坑,我踩过之后说一下:

第一,第一个GhostModule的ratio不一定等于2,hidden_channels和out_channels的比例也未必是整数,所以用math.ceil处理最稳妥。第二,stride=2时,shortcut必须降采样,我习惯用1×1卷积stride=2,简单有效,某些实现会用average pooling,实测精度差异很小但前者显存略大。第三,第二个GhostModule默认不加ReLU,这一点和MobileNetV2/V3的project层完全一致,千万别顺手加个ReLU。第四,如果输出通道数不等于输入通道数且stride=1,shortcut要用1×1卷积做维度匹配。

3.3 训练配置与数据增强的实用建议

GhostNet的训练配置基本沿用了MobileNetV3那一套,但我在实际训练时做了一些调整,效果更稳定。

输入尺寸224×224,batch size 256起步,这个规模下初始学习率0.1没问题。如果你batch size减半,学习率也要跟着减半,否则前期loss会震荡得很厉害。优化器用SGD,momentum=0.9,weight decay设4e-5——不要按ResNet的习惯直接给1e-4。GhostNet的BN层多,weight decay太大会抑制BN缩放因子的学习,导致精度掉点。

学习率策略用cosine decay,总epoch数100到150。150个epoch时你会在训练日志里清楚看到,最后一轮精度还在缓慢上升。如果时间紧,100个epoch也够用,但精度大概会比150个epoch低0.2到0.3个点。

数据增强方面:random resize crop加random horizontal flip是基础,我强烈建议加label smoothing(epsilon=0.1),大约能稳定提升0.2到0.3个点。AutoAugment可加可不加,加了大概再多0.2个点,但训练时间会明显变长。另外,如果显存不够把batch size压到128,学习率相应调整后,配合EMA(指数滑动平均)一般能补回不少精度波动。

还有一个细节:Ghost Module里的cheap operation是3×3 depthwise conv,它对初始化和BN的敏感度比普通卷积更高。如果训练前几个epoch loss下降特别慢,优先检查cheap op分支的BN层是否有问题。在大batch训练时,如果因为分布式训练不得不把BN换成GroupNorm,Ghost Module的精度会显著下降——这个结构对BN的依赖比普通卷积更强,所以不太适合做大规模分布式训练中禁用BN的场景。

4. 实验结果解读:精度、速度与硬件上的真实差距

4.1 ImageNet上的精度与计算量权衡

论文中给出了一系列在ImageNet上的结果。GhostNet-1.0x(约0.15G FLOPs)取得了和MobileNetV3相近甚至略高的top-1精度,大约75.7%,参数量约5.2M。这个数据在同量级轻量网络里算是相当能打的了。

这里提醒一句:论文里的FLOPs通常按乘加次数计算,不同框架统计口径不同,对比时要留意。同一个模型,在PyTorch里用thop统计和用TensorRT报告的数字可能差不少,不是模型变了,而是统计方式变了。很多人在对比“我的模型FLOPs比GhostNet小”时,其实用的标准和论文不一致,这种对比没有意义。

4.2 不同硬件上的实测表现

FLOPs是理想值,部署时真正要看的是延迟。我实际在几种设备上测试过GhostNet-1.0x:

在ARM CPU上,比如麒麟990这类芯片,配合NCNN或MNN的depthwise卷积优化,GhostNet的推理时延比同FLOPs的MobileNetV3略低或持平。原因在于GhostNet省掉的1×1卷积在ARM CPU上开销较大,替换成depthwise conv后访存模式更友好。

在x86 CPU上,如果没有开启AVX指令集,或者底层库对depthwise conv优化不佳,GhostNet的加速比会打折扣。这不是GhostNet特有的问题,而是所有深度可分离架构的通病——kernel太小,计算密度低,访存开销占比高。

在GPU上,普通GPU跑depthwise conv的效率并不高,因为kernel太小、并行度不够,访存受限。所以GhostNet在GPU上相对ResNet的FLOPs优势并不能完全转化为延迟优势。做服务端推理时,我通常会考虑把depthwise conv替换成group conv(group=2或4)来改善访存效率,或者干脆换回普通卷积方案。

这个差异化表现,本质上是因为Ghost Module省掉的是一大堆1×1卷积的计算量,而补进来的depthwise conv在部分硬件上并不能跑满理论算力。所以选型时,绝不能只看FLOPs对比表,必须在自己目标硬件上实测。

4.3 特征图可视化:冗余假设为什么成立

论文里有一个非常直观的可视化:把某一层输出的特征图按相似度聚类,能明显看到大量特征图对仅仅存在微小的空间变换差异。这些可视化证明了“冗余假设”是成立的,同时也说明,所谓的cheap operation不需要承担语义建模的任务,只需要做特征增强和细节补充。

这个观察对做模型设计也有启发:如果某个任务的特征图冗余度没那么高,Ghost Module的收益就会下降。我在检测任务里也验证过,用GhostNet替换MobileNetV3作为backbone,在精简检测数据集上,精度下降幅度比单纯的分类任务更明显一些。这说明下游任务对特征多样性的敏感度不同。做检测或分割时,我的经验是优先用GhostNet-1.0x或更大版本,0.5x版本在小目标较多的场景下容易崩。

5. 部署与工程落地中的常见问题

5.1 通道数与shape不匹配

这是复现时最常见的报错。原因基本都在ceil和裁剪那一步。举个例子,如果out_channels=25、ratio=2,init_channels=ceil(25/2)=13,new_channels=13×(2-1)=13,concat后是26个通道,转发时最后裁剪到前25个通道。

这里的关键在于:裁剪必须放在concat之后,而不是把primary分支直接设为13、cheap分支设为12。因为primary分支是语义层的base,对它的通道做裁剪会破坏特征分布。我第一次实现时就是在这个地方偷懒,结果训练时的精度比论文低了将近1个点,后来把裁剪挪到concat之后才恢复正常。

5.2 训练掉点或精度上不去

如果你用GhostNet替换MobileNetV3后发现精度掉了超过一个点,大概率不是架构问题,而是训练细节没跟上。常见原因有四个:学习率没按batch size缩放;weight decay设成了1e-4导致BN层被过度正则化;数据增强不足,GhostNet在强增强下吃到的增益比大模型更明显;只跑了很短的epoch就下结论。GhostNet收敛速度比普通ResNet慢一些,前50个epoch看着可能差1个点,到150个epoch基本追平甚至反超。

我自己的习惯是:换架构后第一次训练,固定150个epoch、cosine decay、label smoothing三个条件不变,只在必要情况下微调学习率和weight decay。这样能快速判断是训练问题还是架构问题。

5.3 实际加速不如FLOPs预期

这个问题在GPU上尤为明显。我的排查顺序是:先确认底层库的depthwise卷积实现是否优化到位,NCNN和MNN在ARM上是OK的,PC端则需要单独确认;尝试把cheap operation的kernel size从3降到1,虽然理论上精度会掉一些,但在部分硬件上3×3深度卷积的开销可能比1×1还高;用ONNX导出并配合onnxruntime做图优化,把concat、BN这些算子尽量融合;如果目标设备支持INT8量化,GhostNet的量化误差主要来自concat后的通道裁剪,量化时校准集要覆盖足够的激活分布。

5.4 一个容易被忽略的细节:SE模块的reduction

论文里的SE reduction默认是4,但如果你直接对hidden_channels除以4,在hidden_channels小于64时,会把中间层压成个位数通道,SE效果大打折扣。我的建议是,reduction保持4不变,但加一个最小通道数下限,比如不低于4。另外,SE模块加在哪一层也有讲究。我在实现GhostBottleneck时默认加在第二个GhostModule之前,因为此时通道数是扩展后的最大值,SE在这里的注意力权重信息量最丰富。如果放在通道压缩后,信息已经被压缩,SE的效果会明显变弱。

6. 对GhostNet的深度思考与后续扩展

6.1 从论文方法到工程取舍的一些体会

初读GhostNet时,我一度认为它就是MobileNetV3的小改版,毕竟block结构太像了。后来自己动手把Ghost Module拆开做消融、部署、对比之后,才真正理解它的价值点:它对“冗余”这一现象的系统化利用,给了模型设计一个更通用的视角。

这个思想后来被很多工作沿用,比如在一些Transformer模型压缩中,也可以用类似思路去掉冗余token、用轻量映射补充表达。理解了这一点,你会发现GhostNet教给你的不是一个具体的block,而是一种思考方式:在看到“这层输出好多特征图长得很像”之后,不是简单地认为“这是网络学的不好,是过拟合”,而是去思考如何把这种冗余转化为计算效率。

6.2 将GhostNet落地到具体项目的路径建议

如果你想把GhostNet用到自己的项目里,我建议的路径是:

第一步,先在分类任务上跑通GhostNet-1.0x,确认代码实现和论文基线对齐。这是最快验证实现正确性的方式,不需要一开始就跑到目标数据集上。

第二步,再替换到目标任务的backbone上。前两个epoch观察loss下降曲线是否和MobileNetV3一致。如果不一致,优先排查学习率和BN设置,而不是调整网络结构。

第三步,做端侧部署。用NCNN、MNN或量化工具导出模型,在真实设备上测延迟,不要只看FLOPs。GhostNet这种深度可分离架构,在不同硬件上的表现差异极大。

6.3 后续可以进一步探索的实验方向

GhostNet这个方向,往深了走还有很多可以玩的地方。比如,把cheap operation从固定卷积换成可变形卷积或光流估计式的局部变换,理论上可以适应更复杂的特征模式;再比如,不同stage采用不同的ratio——浅层特征冗余少,ratio可以小一点,深层特征冗余多,ratio大一点,这个自适应配置目前公开的研究还不多;还有推理时动态决定哪些通道需要生成真特征、哪些可以直接用缓存,这个方向如果能做起来,对端侧实时推理的收益会非常大。

最后再分享一个我实际做实验时的体会:GhostModule最怕的不是被误解,而是被“无脑使用”。它适合替换通道数较大、冗余度较高的层,但不太适合网络最前面的几层——那里特征图空间分辨率大、语义信息少,强行用cheap operation会伤到底层特征。我的经验是:网络前两个stage保持普通卷积,从第三个stage开始再上Ghost Module,这样既稳又能省计算量。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询