ARTICLE DETAIL

资讯详情

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

从分组卷积到通道剪枝:轻量化模型的通道设计与推理加速

从分组卷积到通道剪枝:轻量化模型的通道设计与推理加速 6. 分组卷积、深度可分离卷积与通道的恩怨前面几节的通道操作无论怎么折腾本质上还是在所有通道互相全连接的框架里做文章。而真正让通道维度产生结构性变革的是分组卷积这条线。理解了它你才能明白为什么现在手机上的模型能跑得动为什么有些网络把通道拆成好几组反而效果不降。6.1 分组卷积把通道切成几份各管各的标准卷积里每一个输出通道都会看全部输入通道。分组卷积直接把输入通道切成 g 组每组只在自己那一组里做卷积输出也相应地分成 g 组组和组之间互不干涉最后再把结果拼起来。拿具体数字说。输入 32 通道输出 64 通道分成 4 组。那么每组处理 32/4 8 个输入通道也负责产出 64/4 16 个输出通道。第 1 组只用输入的 1 到 8 通道卷出输出的 1 到 16 通道第 2 组用输入 9 到 16 通道卷出输出的 17 到 32 通道以此类推。注意这里输出通道的分配方式是按组分段的不是按奇偶交错。参数量立刻掉下来。标准卷积的参数量是K×K×C_in×C_out分组卷积变成K×K×(C_in/g)×(C_out/g)×g也就是原来的 g 分之一。上面这个例子分组卷积的参数量只有标准卷积的四分之一计算量也降四倍。这个操作最早出现在 AlexNet 里当年是为了把模型拆到两张显卡上跑硬生生把通道分了两组一张卡管一半。后来大家发现分组本身还挺有用通道之间适当解耦能减少冗余还能起到类似正则化的作用。但要提醒一句g 不是越大越好。极端情况下 g 等于 C_in 也等于 C_out那就退化成深度卷积了通道之间完全不通气表达能力会受限。注意分组数 g 必须能同时整除输入通道数和输出通道数否则 PyTorch 直接报错。设计网络时这点一定要提前确认我见过有人设 g3 结果通道数写的是 64跑起来就崩。6.2 深度可分离卷积分组卷积极端化的产物深度可分离卷积是把分组卷积推到极限的做法也是 MobileNet 系列的核心。它把标准卷积拆成两个独立步骤第一步叫深度卷积groups 设成等于输入通道数每个通道配一个专属的 K×K 卷积核自己卷自己不和其他通道混合。输入 32 通道就输出 32 通道通道数不变。这一步只提取空间特征不管通道间的信息交流。第二步叫逐点卷积就是一个 1×1 的标准卷积把上一步得到的 32 通道重新组合成想要的输出通道数比如升到 64。这一步专门负责通道维度的信息融合。两步拆开之后参数量对比一下就很惊人。假设输入 32 通道输出 64 通道卷积核 3×3。标准卷积3×3×32×64 18432个参数。深度可分离卷积深度卷积部分是3×3×32 288逐点卷积部分是1×1×32×64 2048加起来 2336 个参数。只有标准卷积的八分之一左右。用代码实现很直接class DepthwiseSeparable(nn.Module): def __init__(self, in_ch, out_ch, stride1): super().__init__() self.depthwise nn.Conv2d( in_ch, in_ch, kernel_size3, stridestride, padding1, groupsin_ch, biasFalse ) self.pointwise nn.Conv2d( in_ch, out_ch, kernel_size1, biasFalse ) def forward(self, x): x self.depthwise(x) x self.pointwise(x) return x深度卷积那行groupsin_ch是关键它让每个通道独立处理。逐点卷积再负责通道重组。这两步的分工要记清楚深度卷积管空间逐点卷积管通道。这个分工背后是有道理的空间相关性和通道相关性其实是两类不同的关系强行用一个大卷积一起处理参数和计算都浪费了拆开各司其职效率更高。实测下来MobileNet 在移动端同样的精度下计算量能压到普通卷积网络的十分之一量级代价是内存访问模式变得不连续某些硬件上实际推理速度提升没计算量降得那么明显。这也是个容易被忽视的坑理论 FLOPs 低了不代表跑得快部署时要用真实设备测一遍。7. 通道剪枝把没用的通道砍掉模型训练完了你会发现很多通道其实是冗余的对于当前任务根本没什么贡献。通道剪枝做的事就是找出这些划水的通道直接砍掉让模型变小变快。7.1 剪枝的核心判断标准怎么判断一个通道有没有用最常用的办法是看它输出权重的数值大小。一个 BN 层里的缩放系数 gamma 如果接近 0说明这个通道的输出被压制得很狠对最终结果贡献很小。这背后的逻辑是BN 层的 gamma 决定了特征被缩放多少倍gamma 越接近 0该通道的信息传到下一层时越微弱删掉它对整体影响就小。判断标准定好之后剪枝流程大致是先正常训练一个网络然后统计所有 BN 层的 gamma 值按从小到大排序剪掉最小的那一部分通道再对剪枝后的网络做微调恢复精度。import torch.nn.utils.prune as prune def collect_bn_scales(model): scales [] for name, module in model.named_modules(): if isinstance(module, torch.nn.BatchNorm2d): scales.append((name, module.weight.detach().abs().mean().item())) scales.sort(keylambda x: x[1]) return scales这段代码把所有 BN 层按 gamma 均值从小到大排好了序后面按顺序剪就行。剪的比例一般从 10% 到 30% 起步剪太多精度会掉得厉害微调也救不回来。7.2 结构化剪枝和非结构化剪枝的区别这里必须分清楚两类剪枝很多教程把这两个概念混着讲容易把人带偏。非结构化剪枝是把权重矩阵里某些具体的值设成 0网络结构不变。这种做法能大幅压缩存储配合稀疏存储格式但普通的硬件跑起来速度不会变快因为计算还是按密集矩阵做的。它图的是省空间不是省时间。结构化剪枝是直接删掉整个通道、整个卷积核网络的实际结构变小了。这种做法既省空间又省时间因为通道真的没了后续计算量实打实下降。代价是粒度粗精度的调整空间没那么细。做工程落地绝大多数场景要的是结构化剪枝因为你要的是推理加速。非结构化剪枝更多出现在研究性质的工作里。提示剪枝后的通道数往往不是 8 的整数倍这在普通框架上没问题但在某些针对特定硬件优化过的推理引擎上会掉效率。剪枝方案定好后最好把通道数凑到硬件友好的数字上比如保持每个 block 的输出通道是 8 的倍数。7.3 剪枝里最容易被忽略的一步剪枝不是剪完就结束微调才是决定成败的环节。我见过太多人剪了之后精度掉了两个点直接放弃其实是没微调到位。微调时学习率要设得比正常训练小因为网络结构已经变了大学习率会让之前学到的有用特征全部打乱。一般用原学习率的十分之一起步训练几个 epoch 看恢复情况。另外微调用的是原始训练集还是只拿一小部分做校准也影响结果。只拿小样本校准速度快但恢复的精度上限低用全量数据微调慢但效果更稳。还有一个坑是剪枝和残差连接的关系。如果你的网络里有残差块剪掉某个通道之后残差那一路的对应通道也得同步剪掉否则相加的时候维度对不上直接报错。这个道理和分组卷积里的通道对齐一样凡是涉及通道拼接或相加的地方两边的通道数必须严格一致。8. 通道在不同硬件和数据形态下的表现通道这个概念不只是算法层面的事落到工程上它和硬件、数据组织方式都有很难掰扯清楚的联系。8.1 为什么通道数喜欢取 8 的倍数因为 GPU 做并行计算时内存访问是按块读的。很多涉密场景的推理芯片和主流加速卡向量化宽度是 8 或者 16。通道数如果不是这些数的整数倍最后几个通道就得单独处理出现尾块效率掉得很明显。实践中常见的做法是把通道数往 8 的倍数上凑32、64、128、256 这些数字之所以到处都是除了设计习惯也有硬件友好的考虑。你自己设计网络时如果通道数做到了 30、60 这种可以往上凑到 32、64性能提升通常比多出那两个通道带来的效果更大。8.2 数据通道和模型通道的对应关系还有个容易被忽略的点输入数据的通道顺序和后端框架的期望常常对不上。视频数据可能是 (N, T, C, H, W) 五维的通道在第三位图片是 (N, C, H, W)通道在第二位点云或者序列数据可能压根没有空间维通道的含义又变了。处理这类问题的方法很简单但需要养成习惯接任何数据进来先把形状打印出来看一眼。我自己的做法是在数据处理流程里加一个断言明确期望的通道维位置一旦对不上立刻报错而不是等到卷积层里报一个很难读的维度不匹配错误再去倒推。前面已经踩过的坑就别让它在下一个项目里再出现一次。通道拆开看无非是一个维度。但这个维度承载的信息量、它对模型规模的影响、它在硬件上的行为全都比表面看起来要深一层。把这一维理解透了后面不管遇到什么新结构你都能很快定位它在哪里动通道、为什么这么动。
返回列表