
《GhostNet》这篇论文值得每个做端侧模型的人细读这个系列写到第四篇了这次想聊聊GhostNet。第一次看到这个标题More Features from Cheap Operations时我其实有点不以为然又是类似MobileNet那种把标准卷积拆成两步的轻量化结构吧。但等我真正把论文和开源代码捋完才发现它切入的角度跟MobileNet系列完全不同——MobileNet们从算子改造入手而GhostNet抓住的其实是特征图冗余这个再明显不过却很少有人直接动刀的现象。五年过去这个思路在端侧部署、模型压缩、NAS搜索里反而越来越常用。这篇笔记我从原理讲到复现尽量把当时读的时候认为值得注意的点都交代清楚。1. 为什么翻到这篇老论文轻量化模型的演进到了另一个十字路口1.1 在MobileNet和NAS夹缝中的GhostNet先把时间线摆出来。GhostNet发表在CVPR 2020那会儿大家的注意力都集中在这几个方向上MobileNetV3把NAS和手工设计结合在移动端成了事实标准EfficientNet用compound scaling把FLOPs和精度的关系捋得很明白各路NAS搜出来的网络也一个比一个卷精度。新模型都在谈我比MobileNetV3掉点更少或者同精度下我FLOPs最低。GhostNet的出身和它们都不太一样它来自华为诺亚方舟实验室更像纯粹研究驱动的一个产物。整篇论文的核心动机不是搜一个更好的结构而是先把问题说清楚神经网络的特征图里其实有大量冗余。你随便拿个训练好的CNN把某层特征图可视化出来会发现好多通道长得特别像——轮廓一样、纹理一样只是对比度或相位稍微有点差别。以前大家处理这种冗余的方式是剪枝、低秩分解、知识蒸馏这些方法都是在模型训完之后或者训练中做补救而GhostNet直接把这个冗余建模进了网络结构。这个思路在当时确实不算主流。你翻MobileNet系列的论文作者讨论的是depthwise卷积有多省算力、SE注意力放哪个位置最合适、swish激活比relu好多少。GhostNet则问了一个更底层的问题既然输出特征图有一部分是重复劳动算出来的那些冗余特征有没有可能用更cheap的方式直接生成1.2 这个点子在今天反而更值得读2024年再看GhostNet轻量化模型这条赛道已经很挤了。MobileNetV4、EfficientFormer、RepViT、FasterNet这些名字轮番登场很多结构看起来跟2019年的GhostNet毫无关系。但如果你把降低冗余计算这条主线拎出来GhostNet的建模方式几乎成了很多后续工作的默认底座。比如后来的FasterNet讨论partial convolution为什么有效本质就是在说一部分特征通道不需要复杂的空间卷积再比如很多reparameterization方法在重参数完之后会得到类似Ghost分支的结构。这些都在反复验证一个观察CNN在卷积过程中生成了大量可预测、可复用的特征。我自己是把GhostNet当作轻量化结构教科书来读的。它的篇幅很短核心方法讲得极其干净没有为了SOTA堆太多trick非常适合作为结构设计入门到进阶的一个跳板。而且它的实现复杂度很低不管是用PyTorch手写还是用TensorFlow搭基本一两天就能跑通并验证想法。2. Ghost Module的数学拆解所谓廉价操作到底廉价在哪2.1 从普通卷积的参数量算起先把概念定义清楚。给定输入特征图 $X \in R^{c \times h \times w}$一个普通卷积层生成 $n$ 个输出特征图公式是$$Y X \cdot f b$$其中 $f \in R^{c \times k \times k \times n}$ 是这层的卷积核$k$ 是卷积核大小$Y \in R^{n \times h \times w}$ 是输出。这个过程的浮点运算量大概在 $n \cdot h \cdot w \cdot c \cdot k \cdot k$ 这个量级。GhostNet的观察是$Y$ 里这 $n$ 个通道并不是都独立的。如果用聚类或者直接可视化去看大约有相当一部分通道和另一部分通道之间存在线性相关性。那么在理想情况下你其实需要生成的独立信息没有 $n$ 个那么多假设只有 $m$ 个剩下的 $n-m$ 个就能通过这 $m$ 个做某种变换得到。于是作者把输出分为两层$$Y X \cdot f$$其中 $f \in R^{c \times k \times k \times m}$$m \le n$生成的是内在特征intrinsic feature maps也就是那些信息量较高的核心特征。然后对于 $Y$ 中的每个特征图 $y_i$再用一个廉价变换生成若干个ghost特征$$y_{ij} \Phi_{ij}(y_i), \quad i1,...,m,; j1,...,s$$这里的 $\Phi_{ij}$ 就是所谓cheap operation论文里最常用的是depthwise卷积。当然每个内在特征还会留一个直接输出的分支相当于恒等变换对应 $js$ 那个位置。整个模块的最终输出拼接起来仍然是 $n$ 个通道这样就保证了Ghost Module可以像普通卷积一样插入任何网络位置不改变后续层的输入适配。2.2 参数量的压缩比到底怎么算这是Ghost Module最核心的一个数字对比。假设普通卷积核尺寸是 $k \times k$Ghost Module里内在特征用的也是 $k \times k$ 卷积但输出只有 $m n/s$ 个通道论文里经常取 $s2$然后再用 $d \times d$ 的depthwise卷积做ghost特征生成论文常用 $d3$ 或 $d5$。普通卷积的参数量$$n \cdot c \cdot k \cdot k$$Ghost Module的参数量$$\frac{n}{s} \cdot c \cdot k \cdot k (s-1) \cdot \frac{n}{s} \cdot d \cdot d$$把ratio写出来约等于$$\frac{\frac{n}{s} \cdot c \cdot k \cdot k (s-1) \cdot \frac{n}{s} \cdot d \cdot d}{n \cdot c \cdot k \cdot k} \frac{1}{s} \frac{s-1}{s} \cdot \frac{d \cdot d}{c \cdot k \cdot k}$$拿一个典型例子来算。假设输入通道 $c64$输出通道 $n128$$k3$$d3$$s2$那么参数量压缩比是$$\frac{1}{2} \frac{1}{2} \cdot \frac{9}{64 \times 9} \approx 0.5078$$也就是大约省了一半参数量。计算量也因为输出通道数少了一半、以及后续ghost变换是depthwise卷积会压得更狠——直接按FLOPs算大约能压到接近 $1/s$ 的水平。这里有个很容易忽略的细节$d$ 的取值和 $c$上一层的通道数会影响压缩率。如果上一层通道数很小比如stem后第一层 $c16$那 $d3$ 的depthwise卷积相对于 $3\times3$ 普通卷积的节省就没那么明显。这也是为什么论文里网络前几层一般不直接用Ghost Module或者会把 $s$ 调小。2.3 为什么偏偏选depthwise卷积作为廉价变换文章里作者做过一个对比实验试了三种生成ghost特征的方式一种是直接用普通的 $3 \times 3$ 卷积一种是类似跨通道的仿射变换还有一种就是depthwise卷积。结果depthwise卷积在精度和计算量的平衡上最好。我自己理解这套选择的逻辑有三个层面。第一depthwise卷积的计算量天然就是普通卷积的 $1/c$ 量级它的FLOPs是 $d \cdot d \cdot h \cdot w \cdot n$没有输入通道数这一项确实足够cheap。第二depthwise卷积保持空间结构生成的特征在视觉语义上更接近同一物体的不同底层响应也就是ghost特征和内在特征仍然共享空间位置如果换个通道级的线性变换那生成的特征就变成通道间的线性组合可表达性不够丰富。第三实现层面太友好了几乎每个推理框架都有成熟的depthwise卷积算子不会像一些刁钻的结构那样需要自定义算子才能跑。其实我更愿意把这理解成一种显式的特征复用。做神经网络的都知道$3 \times 3$ 卷积单个输出通道对应一个二维滤波器它只感受局部区域。当它要负责提取一个空间位置上的边缘、纹理、朝向等多种语义时需要好多滤波器冗余地去覆盖不同组合。Ghost Module把这种组合拆了一步先用少量滤波器把基信息提出来再用depthwise卷积做局部变换把这些基的衍生响应补齐。这有点像图像压缩里的亮度分量和色度分量——前者保留主要结构后者只需要花很少的码字来编码差值。3. 从Ghost Module到Ghost Bottleneck再到完整网络3.1 Ghost Bottleneck内部到底长什么样只有一个Module还不够要真正堆出一个网络作者仿照MobileNetV2/V3的倒残差结构设计了一个叫Ghost Bottleneck的基本单元。这个Bottleneck通常包含两个Ghost Module。第一个Ghost Module做通道扩展把输入通道先扩张成更大的通道数相当于倒残差结构里的expand部分第二个Ghost Module把通道压回去匹配输入通道方便做残差连接。中间在stride2的时候会额外插入一个stride2的depthwise卷积来做空间下采样同时在shortcut路径上也加一个下采样模块一般也是stride2的depthwise卷积保证两个分支的特征图尺寸和通道数都对齐。此外在第二个Ghost Module之后还会接一个SE模块做通道注意力。如果你把它和MobileNetV3 的bottleneck放一起对比会发现骨架几乎一模一样唯一的差别就是把MobileNetV3中的第一个 $1 \times 1$ 扩展卷积和中间的 $3 \times 3$ depthwise卷积用Ghost Module整体替代了一下。这个替代的意义在于MobileNetV3的Expand层会把通道放大4倍再压缩这中间存在大量冗余特征Ghost Module直接在生成阶段就少算了一部分冗余通道。GhostNet完整网络的结构也很直接。它按照MobileNetV3的stage配置重新设计了特征图的尺寸和通道数并把标准block替换成Ghost Bottleneck。stem仍然用一个普通的 $3 \times 3$ 卷积网络最后接一个 $1 \times 1$ 卷积展开特征再接GAP和全连接分类层。stem和最后几个关键层没用Ghost Module这个细节我放在3.3节说。3.2 一个可运行的PyTorch核心实现理论上讲再多不如一个能直接改改跑跑的实现直观。这里给出Ghost Module最核心的PyTorch代码参照官方开源的思路重新整理没有加太多面向训练的复杂分支方便理解。import torch import torch.nn as nn class GhostModule(nn.Module): def __init__(self, inp, oup, kernel_size1, ratio2, dw_size3, stride1, reluTrue): super(GhostModule, self).__init__() self.oup oup init_channels math.ceil(oup / ratio) new_channels init_channels * (ratio - 1) # 第一步普通卷积生成内在特征输出通道数只有 oup/s self.primary_conv nn.Sequential( nn.Conv2d(inp, init_channels, kernel_size, stride, kernel_size // 2, biasFalse), nn.BatchNorm2d(init_channels), nn.ReLU(inplaceTrue) if relu else nn.Sequential(), ) # 第二步对内在特征做depthwise卷积生成ghost特征 self.cheap_operation nn.Sequential( nn.Conv2d(init_channels, new_channels, dw_size, 1, dw_size // 2, groupsinit_channels, biasFalse), nn.BatchNorm2d(new_channels), nn.ReLU(inplaceTrue) if relu else nn.Sequential(), ) def forward(self, x): x1 self.primary_conv(x) x2 self.cheap_operation(x1) out torch.cat([x1, x2], dim1) return out[:, :self.oup, :, :]代码里有两个细节非常关键。第一个是ratio参数也就是论文里的 $s$它决定内在特征占总输出的比例。第二个是最后一个cat之后的裁剪操作——因为ceil的原因拼接出来的通道数可能比oup略大必须裁到oup否则后面维度就对不上了。Ghost Bottleneck组装起来也很容易基本就是两个GhostModule加一个残差class GhostBottleneck(nn.Module): def __init__(self, inp, hidden_dim, oup, kernel_size3, stride1, use_seTrue): super(GhostBottleneck, self).__init__() assert stride in [1, 2] self.conv nn.Sequential( GhostModule(inp, hidden_dim, kernel_size1, reluTrue), # 当stride2时中间额外插一个depthwise卷积做下采样 nn.Conv2d(hidden_dim, hidden_dim, kernel_size, stride, kernel_size // 2, groupshidden_dim, biasFalse) if stride 2 else nn.Sequential(), nn.BatchNorm2d(hidden_dim), GhostModule(hidden_dim, oup, kernel_size1, reluFalse), ) if use_se: self.se SqueezeExcite(oup) else: self.se nn.Sequential() if stride 1 and inp oup: self.shortcut nn.Sequential() else: self.shortcut nn.Sequential( nn.Conv2d(inp, oup, 1, 1, 0, biasFalse), nn.BatchNorm2d(oup), nn.Conv2d(oup, oup, kernel_size, stride, kernel_size // 2, groupsoup, biasFalse), nn.BatchNorm2d(oup), ) def forward(self, x): residual self.shortcut(x) x self.conv(x) x self.se(x) return x residual注意GhostModule的第二个模块设了reluFalse这是因为主分支最后输出接加法之前一般不加激活直接和shortcut融合这个设计和MobileNetV3保持一致。SqueezeExcite模块可以用一个全局平均池化加两个全连接层实现不需要额外赘述。3.3 容易被忽略的结构细节s怎么设、哪些层不能用Ghost ModuleGhostNet论文里有一个容易被快速翻过去的表就是完整网络配置表。我最初照着配置复现时踩过一个小坑——最后一个stage的通道数不是简单按照宽度系数缩放就行有些层的s被刻意调小了。从论文配置来看stem和末尾展开层用的都是普通卷积理由是前几层输入通道少、分辨率高普通卷积和Ghost Module的收益差距还不够大勉强用反而会损失精度。而中间大量Ghost Bottleneck里的ratio默认是2但最后几个stage使用了ratio2、dw_size较大的组合。其实你如果去看MobileNetV3的配置会发现一个类似现象网络最后几个stage的expand ratio往往会降一点避免在低分辨率特征图上用过大的扩展比。作者在消融实验里对比了不同的s和d。结论比较直观$s2$、$d3$ 是综合最优s再增大比如 $s4$压缩收益已经很小但精度掉得更多。原因是内在特征图的数量减少以后单个内在特征图要承担的信息变多线性变换生成ghost特征的误差也会被放大。这个规律在后面很多工作里也反复出现冗余压缩有一个临界点过了这个点压缩的边际收益会被误差的边际成本吞掉。4. 从ImageNet到COCOGhostNet的实验结果值不值得信4.1 ImageNet上的指标与同精度对比论文报告的核心指标是ImageNet top-1 accuracy。对于GhostNet-1.0x输入224x224参数量大概4.1M到6M这个量级FLOPs不到150M的情况下top-1能达到75.7%左右。这个数字放在今天不算惊艳但关键在于同时期的对比对象。MobileNetV3-Large在同参数量附近的top-1在75.2%左右FLOPs却高出不少。如果只按分类精度算GhostNet在当时确实做到了轻量级网络的一个新平衡点。比起精度数字更值得看的是论文里做的FLOPs-accuracy曲线对比。在相近FLOPs约束下GhostNet几乎全程压着MobileNetV3、FBNet、EfficientNet-B0这些结构打。这个结果说明Ghost Module带来的不是某一点上的碰巧收益而是整个结构设计上有一个稳定的效率优势。当然ImageNet结果只能说明分类能力和计算效率的关系。实际业务中大家更关心的是迁移到检测、分割等下游任务上还能不能保持这个优势。4.2 COCO检测和时延验证FLOPs之外的江湖GhostNet论文在COCO目标检测和Cityscapes语义分割上都做了实验backbone换成GhostNet之后在相似FLOPs限制下检测mAP和分割mIoU都优于MobileNetV3作为backbone的对应结果。这说明Ghost Module学到的特征并不是只在分类任务上自洽对于需要密集预测的任务同样够用。另外一个容易被人忽视的部分是作者在真实硬件上测的时延。模型大小和FLOPs毕竟只是理论指标工程落地看的是真实推理时间。论文里分别测了GPUV100和手机ARM CPUQualcomm 855上的GPU时延GhostNet在ARM CPU上的加速比要明显好于GPU。这个现象其实很合理GPU对大矩阵乘法的并行利用率更高而Ghost Module拆出来的depthwise卷积在GPU上并不能达到理论算力利用率ARM CPU上的算子简单、内存访问密度低反而更接近理论收益。这个教训后来我一直记着看轻量化模型不能只看FLOPs降了多少要看目标平台的算子实际效率。GhostNet在GPU的加速比大概是 $2\sim 3$ 倍但在ARM上可能接近 $4\sim 5$ 倍这就是平台差异。4.3 消融实验给出的设计启示论文里最有价值的消融不是精度调参而是关于变换方式的对比。作者比较了直接用普通卷积做ghost变换、用depthwise卷积做变换、以及干脆不做变换也就是把内在特征直接复制拼接之间的差别。结果只用复制identity也能保持相当不错的精度加了depthwise变换之后精度进一步提升。这说明两件事特征图冗余确实存在到复制一份都不太影响结果的程度但同时空间维度上的局部变换仍然能提供额外的有效信息。我见过有复现者在Ghost Module的cheap_operation里把depthwise卷积换成普通卷积FLOPs涨了一大截精度只高了一点点。这说明在ghost特征生成这个环节信息瓶颈并不在通道间的交互而在空间位置上的局部变换。用depthwise卷积是恰到好处的复杂度选择也算是对cheap operation命名的一个印证。5. 复现与部署中的亲身踩坑记录5.1 复现精度对不上先检查这三个地方我最早复现GhostNet跑ImageNet训练配置完全按论文来结果top-1比论文低了0.6%。排查一圈最后定位到三个问题。第一个是EMA指数移动平均权重没有加。那个年代很多轻量模型训练都会做EMA论文的Table里用的基本是EMA后的权重你直接拿最后的checkpoint测自然差一点。第二个是最后一个stage的s和通道数配比被我按照理想缩放把所有层都设成s2实际上有部分层设的是s3或别的方式配置表没看仔细导致网络表达能力跟论文不一致。第三个是数据增强的细节轻量模型对强增强普遍敏感GhostNet也一样它的最佳配置并不是把mixup、cutmix都开满。如果在自己的数据集上做finetune我建议把EMA去掉直接训练或者至少保留一份未EMA的checkpoint因为很多下游任务微调的时候EMA模型和正常模型的更新频率不一致反而不容易微调。5.2 FLOPs砍了延迟却砍不动平台算子的现实问题GhostNet真正到了部署阶段最大的坑不是精度是推理框架的算子支持。Ghost Module里那个cat操作在训练里很自然但在TensorRT或OpenVINO里如果框架不能把primary_conv的输出和cheap_operation的输出无损融合可能会引入额外的内存拷贝。而且depthwise卷积在GPU上的实际利用率常年不高导致你跑出来的延迟降低幅度远小于FLOPs降低幅度。我自己在部署一个基于GhostNet的检测模型时遇到过TensorRT对group1的depthwise卷积支持不友好、导致被替换成普通卷积的情况那一瞬间模型大小直接涨了将近一倍。解决办法是把depthwise卷积的group设到输入通道数正确的做法并在转ONNX之后仔细检查算子的mapping如果框架不支持小通道数上的depthwise可以考虑在channle数较大的层才使用Ghost Module浅层仍然用普通卷积。此外如果是量化部署Ghost Module里两个分支的量化参数差异容易导致精度掉点。因为primary_conv输出和cheap_operation输出在数值分布上差异较大量化时建议对两个分支分别统计scale和zero_point而不要统一校准。这个细节在PTQ训练后量化里尤其关键我踩过一次之后基本都会在代码里写死per-channel量化。5.3 用GhostNet替换ResNet做业务模型的一点体会我实际在一个OCR检测模型里尝试过把backbone从ResNet18换到GhostNet。同样的训练数据、同样的检测头FLOPs从将近3G降到不到1G模型大小也从40多MB降到12MB左右推理速度在GPU上提升了约一倍但在CPU上能提升近三倍。不过精度并不是直接持平而是低了0.5到1个点左右。这说明ResNet作为通用特征提取器在密集任务上的冗余度比想象中高但也确实有一部分表达能力不是白给的。后期我用知识蒸馏把ResNet18的logits蒸馏到GhostNet上精度的缺口基本补回来了。所以如果你打算在业务里替换backbone强烈建议配套一个蒸馏方案而不是期待GhostNet直接顶上还能不掉点。毕竟模型压缩的本质就是信息浓缩如果没有额外的监督信号凭空浓缩总是会损失一点。6. 抛开指标GhostNet给后续工作留下了什么6.1 从更cheap的操作到冗余结构设计GhostNet最有价值的一点是它把特征图冗余从一个经验观察变成了一个可以直接设计网络的显式机制。后来的GhostNetV2花了很大力气去解长距离依赖问题但底座仍然是Ghost Module。再往后看很多针对低功耗场景设计的分支结构都带着GhostNet的影子先用一个轻量分支处理核心特征再用另一个更cheap的分支做补充。这也引申出一个我在读论文时反复思考的问题特征图通道之间的低秩性、相似性到底从哪来一个直观的解释是自然图像本身就存在大量的自相似结构纹理基元频繁重复出现CNN在浅层学习到的是这些基元的不同组合到了深层就变成语义概念的组合。只要输入的分布还贴着自然图像这种冗余就会一直在。所以GhostNet不是针对某一类任务特调的它是对整个分布做了一个通用假设。6.2 读完GhostNet之后我实际沿用至今的设计习惯第一每次设计一个新网络我都会先拿训练好的模型把每层的特征图通道做一次可视化或聚类看看冗余度大概在什么水平然后再决定要不要上类似Ghost的压缩模块。这个习惯来自一篇论文不太直观、但实践价值很高。第二在写训练配置时尽量把s和d做成可配置的方便对不同stage设置不同压缩比而不是在代码里写死。第三所有轻量化结构替换都必须在真实目标平台上做profile而不是只拿FLOPs说事。GhostNet给我最大的启发是好结构的出发点往往不是堆叠更复杂的模块而是认真审视现有网络在哪里浪费了算力。这种先找冗余再设计压缩的思路比直接套用现成的轻量模块要可靠得多。这些年端侧模型的需求一直在变但这种从问题出发的方法论基本上没有过时。