模型剪枝经典论文精读:Learning Efficient Convolutional Networks through Network Slimming
一、论文基本信息
论文题目:Learning Efficient Convolutional Networks through Network Slimming
作者:Zhuang Liu、Jianguo Li、Zhiqiang Shen、Gao Huang、Shoumeng Yan、Changshui Zhang
发表信息:ICCV 2017
DOI:10.1109/ICCV.2017.298
论文链接:
arXiv:Learning Efficient Convolutional Networks through Network Slimming
CVF Open Access:ICCV 2017 论文页面
PyTorch 实现:https://github.com/Eric-mingjie/network-slimming
这篇论文发表于 ICCV 2017,DBLP 记录显示其页码为2755–2763,DOI 为10.1109/ICCV.2017.298。官方 GitHub 仓库说明该工作对应论文Learning Efficient Convolutional Networks through Network Slimming,并且原始代码基于 Torch / fb.resnet.torch;另一个 PyTorch 实现支持 VGG、ResNet 和 DenseNet。
这篇论文的核心思想非常清晰:利用 Batch Normalization 中的缩放因子 (\gamma) 表示每个通道的重要性,在训练时对 (\gamma) 加 L1 稀疏正则,让不重要通道的 (\gamma) 自动趋近于 0,训练后再根据 (\gamma) 大小进行通道剪枝。论文明确提出在 BN scaling factors 上施加 L1 regularization,从而识别不重要通道,并在剪枝后 fine-tune 得到紧凑网络。
二、论文要解决的问题
在前面几篇论文中,我们已经看到几类典型通道剪枝思路。
Pruning Filters for Efficient ConvNets直接用 filter 的 L1 范数判断通道重要性。它简单有效,但属于事后启发式评分。
ThiNet和Channel Pruning for Accelerating Very Deep Neural Networks则从特征重构角度出发,判断删除某些通道后,下一层或当前层输出能否被较好重构。它们比 L1 filter pruning 更精细,但需要额外采样特征、构建优化问题,并且剪枝流程相对复杂。
Network Slimming 想解决的是另一个问题:
能不能在训练阶段就让网络自己学出哪些通道重要、哪些通道不重要,而不是训练后再额外设计复杂的通道评分或重构算法?
论文的回答是:可以。具体做法是利用 BN 层里的缩放因子 (\gamma)。
在带 BN 的 CNN 中,每个通道通常都有一个对应的缩放参数 (\gamma)。如果某个通道的 (\gamma) 很小,那么该通道经过 BN 后的输出幅度也会被压得很小。因此,(\gamma) 可以自然地作为通道重要性的代理指标。
Network Slimming 的关键不是简单地“观察 (\gamma)”,而是在训练时主动让一部分 (\gamma) 变小:
正常训练损失 + BN gamma 的 L1 稀疏正则这样,网络在学习分类任务的同时,也会学习一个更稀疏的通道结构。
三、核心思想
Network Slimming 的核心思想可以概括为一句话:
在训练过程中对 BN 层的缩放因子 (\gamma) 加 L1 正则,使部分通道的 (\gamma) 自动趋近于 0,然后根据 (\gamma) 的大小删除对应通道。
普通 BN 的形式是:
其中:
:通道缩放因子;
:通道平移因子;
:归一化后的特征;
y:BN 输出。
对于一个卷积通道来说,如果它对应的 (\gamma) 很小,那么这个通道的输出会被整体压低。此时即使该通道仍然存在,它对后续网络的影响也可能很弱。
因此,论文将当作通道选择因子:
gamma 大:通道重要,保留 gamma 小:通道不重要,删除这和前几篇论文的区别很明显:
L1 Filter Pruning: 看 filter 权重大小 ThiNet: 看下一层输出重构误差 Channel Pruning: 用 LASSO 选通道,再做最小二乘重建 Network Slimming: 训练时直接让 BN gamma 稀疏化,用 gamma 选通道论文 Figure 1 中也明确说明:每个卷积通道对应一个 scaling factor,训练时对这些 scaling factors 加稀疏正则,小 scaling factor 对应的通道会被剪掉,剪枝后得到 compact network,并通过 fine-tuning 恢复精度。
四、方法细节
4.1 为什么选择 BN gamma?
Network Slimming 最巧妙的地方在于:它没有额外引入新的通道选择参数,而是直接复用 BN 层已有的 (\gamma)。
对于带 BN 的卷积网络来说,卷积层后通常接 BN 层。BN 层中每个通道都有一个可学习的缩放因子 (\gamma)。这个参数天然是一维的,且和通道一一对应,非常适合作为通道重要性指标。
论文也指出,直接利用 BN 中的 (\gamma) 有一个重要优势:不需要改变现有 CNN 架构,也不会引入额外推理开销。相比单独插入 scaling layer,复用 BN 参数更自然,因为 BN 本来就包含 channel-wise scaling/shifting。
4.2 训练目标:任务损失 + gamma 稀疏正则
Network Slimming 的训练目标写作:
论文实验中选择:
也就是对所有 BN 的加 L1 正则。L1 正则会鼓励一部分
变小甚至接近 0,从而形成通道级稀疏。论文中给出的公式和说明明确指出,训练目标由正常 CNN loss 和 BN scaling factors 的 sparsity-induced penalty 组成,并在实验中采用 L1 penalty。
4.3 稀疏训练阶段
训练阶段和普通训练非常接近,只是损失函数多了一项:
loss = classification_loss + lambda * sum(|BN_gamma|)也就是说,训练时不仅希望分类准确,还希望网络尽量使用更少的有效通道。
这一步非常重要。Network Slimming 不是训练好模型后再临时找通道,而是在训练过程中让网络自动形成通道稀疏性。
直观上可以理解为:
如果某个通道确实重要, 分类损失会阻止它的 gamma 被压到 0。 如果某个通道不重要或可被其他通道替代, L1 正则会把它的 gamma 推向 0。因此,训练结束后,很多 BN (\gamma) 会呈现明显的大小差异,这就给后续剪枝提供了依据。
4.4 剪枝阶段:根据 gamma 全局阈值删通道
训练完成后,Network Slimming 根据 (\gamma) 的大小剪通道。
论文采用的是全局阈值策略,而不是每层单独设置剪枝率。具体来说,把所有 BN scaling factors 放在一起排序,然后根据目标剪枝比例选择一个 percentile threshold。例如,如果希望剪掉 70% channels,就选择所有 (\gamma) 中第 70 百分位作为阈值,低于该阈值的通道被删除。论文明确说明使用 global pruning threshold,并以 percentile 方式确定阈值,例如剪掉 40% 或 60% channels。
剪枝时会删除:
1. 当前 BN 中对应的 gamma / beta / running mean / running var; 2. 当前卷积层对应的输出 filter; 3. 下一层卷积中对应的输入 channel; 4. 如果是全连接层,也要同步删除对应输入维度。剪枝后得到的不是稀疏 mask 网络,而是一个真正更窄的 dense network。论文也强调,通道剪枝后可以直接得到 narrow network,不需要特殊稀疏计算库。
4.5 Fine-tuning 阶段
剪枝会导致网络性能暂时下降,尤其是剪枝比例较高时。因此论文在剪枝后会对紧凑模型进行 fine-tuning。
整体流程是:
训练带 gamma 稀疏正则的大网络 ↓ 统计所有 BN gamma ↓ 根据全局阈值删除小 gamma 通道 ↓ 构建更窄的 compact network ↓ fine-tune 紧凑模型论文 Figure 2 给出了这一流程:先 train with channel sparsity regularization,然后 prune channels with small scaling factors,最后 fine-tune pruned network。
4.6 Multi-pass Network Slimming
Network Slimming 还提出了 multi-pass 版本。
单次 slimming 流程是:
稀疏训练 → 剪枝 → fine-tuningmulti-pass 则是把这个流程重复多次:
稀疏训练 → 剪枝 → fine-tuning ↓ 继续对剪枝后的网络稀疏训练 ↓ 再次剪枝 ↓ 再次 fine-tuning论文指出,剪枝得到的窄网络可以再次应用整个 slimming 训练流程,从而进一步得到更紧凑的模型。实验中,multi-pass scheme 可以带来更高压缩率。
4.7 ResNet / DenseNet 中的特殊处理
对于 VGG 这类串行网络,剪通道相对简单:
conv -> BN -> ReLU -> conv -> BN -> ReLU只要删除当前层输出通道,并同步删除下一层输入通道即可。
但 ResNet 和 DenseNet 有跨层连接。某一层输出可能会被多个后续层使用。如果直接物理删除通道,可能破坏残差相加或 dense concatenation 的维度一致性。
因此论文指出,对于 ResNet、DenseNet 这类带 cross-layer connections 和 pre-activation 结构的网络,需要额外适配。它们往往在卷积前放 BN,此时稀疏性体现在某层输入端,即该层选择性使用输入通道。为了在测试阶段获得参数和计算节省,需要放置 channel selection layer 来 mask 掉不重要通道。
这也是 Network Slimming 在工程实现中比论文公式更复杂的地方。VGG 很容易物理删通道,但 ResNet / DenseNet 需要依赖结构约束或 channel selection layer。
""" Network Slimming toy demo: 5-layer CNN with BatchNorm gamma sparsity + channel pruning. 对应论文: Learning Efficient Convolutional Networks through Network Slimming Zhuang Liu et al., ICCV 2017 这份代码是“理解性代码”,不是完整论文工程复现。 它演示 Network Slimming 的核心流程: 1. 构造一个带 BN 的 5 层 CNN。 2. 随机生成一组图片和标签。 3. 训练时给所有 BN gamma 加 L1 正则: loss = cross_entropy + lambda * sum(|gamma|) 4. 根据所有 BN gamma 的绝对值做全局排序。 5. 删除 gamma 较小的通道。 6. 物理构建一个更窄的新模型。 7. 检查剪枝前后输出 shape 和参数量。 核心思想: BN 层输出为: y = gamma * x_hat + beta 如果某个通道的 gamma 很小, 说明这个通道的输出被整体压得很弱, 因此可以把 gamma 当作通道重要性指标。 运行: python network_slimming_toy_5conv_bn.py """ from __future__ import annotations from dataclasses import dataclass from typing import Dict, Tuple import torch import torch.nn as nn import torch.nn.functional as F # ============================================================ # 1. 定义一个带 BN 的 5 层 CNN # ============================================================ class Slimming5ConvNet(nn.Module): """ 一个用于理解 Network Slimming 的 5 层 CNN。 默认结构: conv1: 3 -> 16, bn1 conv2: 16 -> 24, bn2 conv3: 24 -> 32, bn3 conv4: 32 -> 32, bn4 conv5: 32 -> 16, bn5 global average pooling fc: 16 -> num_classes 每个 BN 层都有一个 gamma,也就是 PyTorch 中的 bn.weight。 Network Slimming 正是对这些 gamma 加 L1 正则。 """ def __init__( self, channels: Tuple[int, int, int, int, int] = (16, 24, 32, 32, 16), num_classes: int = 10, ) -> None: super().__init__() c1, c2, c3, c4, c5 = channels # Conv 后面接 BN,因此 Conv 不使用 bias。 self.conv1 = nn.Conv2d(3, c1, kernel_size=3, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(c1) self.conv2 = nn.Conv2d(c1, c2, kernel_size=3, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(c2) self.conv3 = nn.Conv2d(c2, c3, kernel_size=3, padding=1, bias=False) self.bn3 = nn.BatchNorm2d(c3) self.conv4 = nn.Conv2d(c3, c4, kernel_size=3, padding=1, bias=False) self.bn4 = nn.BatchNorm2d(c4) self.conv5 = nn.Conv2d(c4, c5, kernel_size=3, padding=1, bias=False) self.bn5 = nn.BatchNorm2d(c5) self.pool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Linear(c5, num_classes) def forward(self, x: torch.Tensor) -> torch.Tensor: x = F.relu(self.bn1(self.conv1(x))) x = F.relu(self.bn2(self.conv2(x))) x = F.relu(self.bn3(self.conv3(x))) x = F.relu(self.bn4(self.conv4(x))) x = F.relu(self.bn5(self.conv5(x))) # [B, C, H, W] -> [B, C, 1, 1] -> [B, C] x = self.pool(x).flatten(1) x = self.fc(x) return x # ============================================================ # 2. 工具函数:参数量、结构、BN gamma # ============================================================ def count_params(model: nn.Module) -> int: """统计模型总参数量。""" return sum(p.numel() for p in model.parameters()) def get_conv_bn_pairs(model: Slimming5ConvNet): """ 按顺序返回 5 组 Conv-BN。 """ return [ ("layer1", model.conv1, model.bn1), ("layer2", model.conv2, model.bn2), ("layer3", model.conv3, model.bn3), ("layer4", model.conv4, model.bn4), ("layer5", model.conv5, model.bn5), ] def print_model_structure(model: Slimming5ConvNet, title: str) -> None: """打印模型每层通道数。""" print(f"\n[{title}]") for name, conv, bn in get_conv_bn_pairs(model): print( f"{name}: conv {conv.in_channels:2d} -> {conv.out_channels:2d}, " f"BN features = {bn.num_features:2d}" ) print(f"fc: {model.fc.in_features:2d} -> {model.fc.out_features:2d}") print(f"params: {count_params(model):,}") def bn_gamma_l1(model: Slimming5ConvNet) -> torch.Tensor: """ 计算所有 BN gamma 的 L1 和。 在 PyTorch 中: BatchNorm2d.weight 就是 gamma BatchNorm2d.bias 就是 beta """ loss = 0.0 for _, _, bn in get_conv_bn_pairs(model): loss = loss + bn.weight.abs().sum() return loss @torch.no_grad() def collect_all_bn_gamma_abs(model: Slimming5ConvNet) -> torch.Tensor: """收集所有 BN gamma 的绝对值,用于全局排序。""" gammas = [] for _, _, bn in get_conv_bn_pairs(model): gammas.append(bn.weight.detach().abs().cpu()) return torch.cat(gammas, dim=0) @torch.no_grad() def print_gamma_statistics(model: Slimming5ConvNet, title: str) -> None: """打印每个 BN 层 gamma 的统计信息。""" print(f"\n[{title}]") for name, _, bn in get_conv_bn_pairs(model): g = bn.weight.detach().abs().cpu() print( f"{name}: " f"min={g.min().item():.6f}, " f"mean={g.mean().item():.6f}, " f"max={g.max().item():.6f}, " f"num={g.numel()}" ) all_g = collect_all_bn_gamma_abs(model) print( f"all gamma: min={all_g.min().item():.6f}, " f"mean={all_g.mean().item():.6f}, " f"max={all_g.max().item():.6f}, " f"num={all_g.numel()}" ) # ============================================================ # 3. 随机数据训练:task loss + gamma L1 # ============================================================ def train_with_gamma_l1( model: Slimming5ConvNet, images: torch.Tensor, labels: torch.Tensor, steps: int = 40, lr: float = 0.03, gamma_l1_lambda: float = 1e-3, ) -> None: """ 在随机数据上做一个小训练,演示 gamma L1 正则。 注意: 随机数据和随机标签没有真实精度意义。 这里只是演示 Network Slimming 的训练机制。 loss: cross_entropy + gamma_l1_lambda * sum(abs(BN gamma)) """ model.train() optimizer = torch.optim.SGD( model.parameters(), lr=lr, momentum=0.9, weight_decay=5e-4, ) for step in range(1, steps + 1): logits = model(images) task_loss = F.cross_entropy(logits, labels) sparse_loss = bn_gamma_l1(model) loss = task_loss + gamma_l1_lambda * sparse_loss optimizer.zero_grad() loss.backward() optimizer.step() if step == 1 or step % 10 == 0 or step == steps: all_g = collect_all_bn_gamma_abs(model) print( f"step {step:03d} | " f"task_loss={task_loss.item():.4f} | " f"gamma_l1={sparse_loss.item():.4f} | " f"mean|gamma|={all_g.mean().item():.6f} | " f"min|gamma|={all_g.min().item():.6f}" ) # ============================================================ # 4. 根据 gamma 全局排序,生成每层保留通道 # ============================================================ @dataclass class PrunePlan: """ 剪枝计划。 keep_indices: 每层保留下来的输出通道编号。 threshold: 全局 gamma 阈值。 prune_ratio: 目标剪枝比例。 """ keep_indices: Dict[str, torch.Tensor] threshold: float prune_ratio: float @torch.no_grad() def make_prune_plan_by_global_gamma( model: Slimming5ConvNet, prune_ratio: float = 0.5, min_channels_per_layer: int = 2, ) -> PrunePlan: """ 根据所有 BN gamma 的绝对值做全局剪枝计划。 论文中的思想: 把所有 BN gamma 放在一起排序; 选择一个全局阈值; 小于阈值的通道删除。 参数: prune_ratio: 希望删除多少比例的通道。 min_channels_per_layer: 每层至少保留几个通道。 这是工程保护,避免某一层被全部剪空。 """ all_g = collect_all_bn_gamma_abs(model) total_channels = all_g.numel() prune_count = int(total_channels * prune_ratio) prune_count = max(0, min(prune_count, total_channels - 1)) sorted_g, _ = torch.sort(all_g) threshold = sorted_g[prune_count].item() keep_indices: Dict[str, torch.Tensor] = {} print("\n[Global gamma threshold]") print(f"total channels: {total_channels}") print(f"target prune ratio: {prune_ratio:.2f}") print(f"threshold: {threshold:.6f}") for name, _, bn in get_conv_bn_pairs(model): g = bn.weight.detach().abs().cpu() keep = torch.nonzero(g > threshold, as_tuple=False).flatten() # 防止某层被剪空。 if keep.numel() < min_channels_per_layer: keep = torch.topk(g, k=min_channels_per_layer, largest=True).indices keep = torch.sort(keep).values.long() keep_indices[name] = keep print( f"{name}: keep {keep.numel():2d} / {g.numel():2d}, " f"indices = {keep.tolist()}" ) return PrunePlan( keep_indices=keep_indices, threshold=threshold, prune_ratio=prune_ratio, ) # ============================================================ # 5. 根据保留通道物理构建更窄模型 # ============================================================ @torch.no_grad() def copy_pruned_conv( old_conv: nn.Conv2d, new_conv: nn.Conv2d, keep_in: torch.Tensor, keep_out: torch.Tensor, ) -> None: """ 复制剪枝后的卷积权重。 old_conv.weight shape: [old_out_channels, old_in_channels, k, k] 剪枝后要保留: 输出通道 keep_out 输入通道 keep_in new_conv.weight shape: [len(keep_out), len(keep_in), k, k] """ w = old_conv.weight.detach().cpu() w = w[keep_out][:, keep_in].clone() new_conv.weight.copy_(w) @torch.no_grad() def copy_pruned_bn( old_bn: nn.BatchNorm2d, new_bn: nn.BatchNorm2d, keep: torch.Tensor, ) -> None: """ 复制剪枝后的 BN 参数。 BN 中与通道相关的量包括: gamma: old_bn.weight beta: old_bn.bias running_mean running_var """ keep = keep.cpu().long() new_bn.weight.copy_(old_bn.weight.detach().cpu()[keep]) new_bn.bias.copy_(old_bn.bias.detach().cpu()[keep]) new_bn.running_mean.copy_(old_bn.running_mean.detach().cpu()[keep]) new_bn.running_var.copy_(old_bn.running_var.detach().cpu()[keep]) new_bn.num_batches_tracked.copy_(old_bn.num_batches_tracked.detach().cpu()) @torch.no_grad() def build_pruned_model( old_model: Slimming5ConvNet, plan: PrunePlan, ) -> Slimming5ConvNet: """ 根据剪枝计划构建一个真正变窄的新模型。 原始结构: conv1: 3 -> 16 conv2: 16 -> 24 conv3: 24 -> 32 conv4: 32 -> 32 conv5: 32 -> 16 fc: 16 -> num_classes 假设每层保留通道数为: k1, k2, k3, k4, k5 剪枝后结构: conv1: 3 -> k1 conv2: k1 -> k2 conv3: k2 -> k3 conv4: k3 -> k4 conv5: k4 -> k5 fc: k5 -> num_classes """ keep1 = plan.keep_indices["layer1"] keep2 = plan.keep_indices["layer2"] keep3 = plan.keep_indices["layer3"] keep4 = plan.keep_indices["layer4"] keep5 = plan.keep_indices["layer5"] new_channels = ( keep1.numel(), keep2.numel(), keep3.numel(), keep4.numel(), keep5.numel(), ) new_model = Slimming5ConvNet( channels=new_channels, num_classes=old_model.fc.out_features, ) rgb_keep = torch.tensor([0, 1, 2], dtype=torch.long) copy_pruned_conv(old_model.conv1, new_model.conv1, rgb_keep, keep1) copy_pruned_bn(old_model.bn1, new_model.bn1, keep1) copy_pruned_conv(old_model.conv2, new_model.conv2, keep1, keep2) copy_pruned_bn(old_model.bn2, new_model.bn2, keep2) copy_pruned_conv(old_model.conv3, new_model.conv3, keep2, keep3) copy_pruned_bn(old_model.bn3, new_model.bn3, keep3) copy_pruned_conv(old_model.conv4, new_model.conv4, keep3, keep4) copy_pruned_bn(old_model.bn4, new_model.bn4, keep4) copy_pruned_conv(old_model.conv5, new_model.conv5, keep4, keep5) copy_pruned_bn(old_model.bn5, new_model.bn5, keep5) # fc 输入维度等于 conv5 输出通道数,所以也要同步裁剪。 new_model.fc.weight.copy_(old_model.fc.weight.detach().cpu()[:, keep5]) new_model.fc.bias.copy_(old_model.fc.bias.detach().cpu()) return new_model # ============================================================ # 6. 主函数:完整跑一遍 # ============================================================ def main() -> None: torch.manual_seed(42) # 随机图片和随机标签。 # 这里只是演示流程,不看真实精度。 batch_size = 16 num_classes = 10 images = torch.randn(batch_size, 3, 32, 32) labels = torch.randint(0, num_classes, size=(batch_size,)) model = Slimming5ConvNet(num_classes=num_classes) print_model_structure(model, "Original model") print_gamma_statistics(model, "BN gamma before sparse training") print("\n[Sparse training with BN gamma L1]") train_with_gamma_l1( model=model, images=images, labels=labels, steps=40, lr=0.03, gamma_l1_lambda=1e-3, ) print_gamma_statistics(model, "BN gamma after sparse training") plan = make_prune_plan_by_global_gamma( model=model, prune_ratio=0.5, min_channels_per_layer=2, ) pruned_model = build_pruned_model(model, plan) print_model_structure(pruned_model, "Pruned compact model") model.eval() pruned_model.eval() with torch.no_grad(): old_out = model(images) new_out = pruned_model(images) print("\n[Forward check]") print(f"original output shape: {tuple(old_out.shape)}") print(f"pruned output shape: {tuple(new_out.shape)}") print(f"output MSE before fine-tuning: {F.mse_loss(new_out, old_out).item():.8f}") print("\n[Summary]") print("This demo shows the Network Slimming pipeline:") print(" 1. Train with BN gamma L1 sparsity.") print(" 2. Sort all |gamma| values globally.") print(" 3. Prune channels with small gamma.") print(" 4. Build a physically narrower CNN.") print(" 5. In real experiments, fine-tune the pruned model next.") if __name__ == "__main__": main()五、关键公式
5.1 BN 层缩放
其中是每个通道对应的缩放因子。
5.2 Network Slimming 训练目标
目标可以写成:
这就是整篇论文最核心的公式。
六、实验设置
6.1 数据集
论文在多个数据集上验证 Network Slimming,包括:
CIFAR-10 CIFAR-100 SVHN ImageNet MNIST论文描述中,CIFAR-10 / CIFAR-100 都是 (32 \times 32) 自然图像数据集,训练集 50,000 张、测试集 10,000 张;ImageNet 包含 1.2M 训练图像、50,000 验证图像和 1000 个类别;MNIST 用于测试该方法在全连接网络上的效果。
6.2 网络结构
论文使用了多种网络:
VGGNet ResNet-164 DenseNet-40 VGG-A on ImageNet 3-layer fully-connected network on MNIST其中,CIFAR 和 SVHN 上使用 VGGNet、ResNet-164 和 DenseNet-40;ImageNet 上使用带 BN 的 11 层 VGG-A;MNIST 上使用三层全连接网络,并把每个 neuron 视为 (1 \times 1) 空间尺寸的 channel。
6.3 训练、剪枝和微调
论文使用 SGD 训练。CIFAR 和 SVHN 上 batch size 为 64,CIFAR 训练 160 epochs,SVHN 训练 20 epochs;ImageNet 和 MNIST 使用 batch size 256,分别训练 60 和 30 epochs。训练中使用 (10^{-4}) weight decay 和 0.9 Nesterov momentum。
稀疏正则强度 (\lambda) 通过网格搜索确定。论文在 CIFAR 和 SVHN 上从 (10^{-3}, 10^{-4}, 10^{-5}) 中搜索;VGGNet 选择 (\lambda=10^{-4}),ResNet 和 DenseNet 选择 (\lambda=10^{-5}),ImageNet 上的 VGG-A 也设置为 (\lambda=10^{-5})。
剪枝时,论文使用所有 scaling factors 的全局 percentile threshold;剪枝后构建新的窄网络,并复制保留通道对应权重。ImageNet 上由于时间限制,论文只对剪枝后的 VGG-A 以 (10^{-3}) 学习率 fine-tune 5 个 epochs。
七、实验结果解读
7.1 CIFAR-10 上的结果
论文在 CIFAR-10 上报告了 VGGNet、DenseNet-40 和 ResNet-164 的结果:
| 模型 | Test Error | 参数量 | 参数剪枝 | FLOPs | FLOPs 剪枝 |
|---|---|---|---|---|---|
| VGGNet Baseline | 6.34% | 20.04M | - | (7.97 \times 10^8) | - |
| VGGNet 70% Pruned | 6.20% | 2.30M | 88.5% | (3.91 \times 10^8) | 51.0% |
| DenseNet-40 Baseline | 6.11% | 1.02M | - | (5.33 \times 10^8) | - |
| DenseNet-40 40% Pruned | 5.19% | 0.66M | 35.7% | (3.81 \times 10^8) | 28.4% |
| DenseNet-40 70% Pruned | 5.65% | 0.35M | 65.2% | (2.40 \times 10^8) | 55.0% |
| ResNet-164 Baseline | 5.42% | 1.70M | - | (4.99 \times 10^8) | - |
| ResNet-164 60% Pruned | 5.27% | 1.10M | 35.2% | (2.75 \times 10^8) | 44.9% |
这个结果非常关键:VGGNet 剪掉 70% channels 后,测试错误率从 6.34% 降到 6.20%,参数减少 88.5%,FLOPs 减少 51.0%。DenseNet-40 和 ResNet-164 在适度剪枝时也出现了错误率下降,说明 BN gamma 稀疏正则本身具有一定正则化效果。
7.2 CIFAR-100 与 SVHN 上的结果
在 CIFAR-100 上,VGGNet 50% Pruned 的测试错误率从 baseline 的 26.74% 变为 26.52%,参数减少 75.1%,FLOPs 减少 37.1%;ResNet-164 40% Pruned 的测试错误率从 23.37% 降到 22.87%,同时 FLOPs 减少 33.3%。在 SVHN 上,VGGNet 60% Pruned 的错误率从 2.17% 降到 2.06%,参数减少 84.8%,FLOPs 减少 50.1%。
这些结果说明,Network Slimming 不只是适用于某一个数据集或某一个模型。在多个数据集上,它都能在保持甚至提升精度的情况下减少参数和计算量。
7.3 为什么有些剪枝后精度更高?
论文中一个有意思的现象是:剪枝后模型有时比 baseline 更准确。
原因可以从两个角度理解。
第一,训练时对 BN (\gamma) 加 L1 正则,相当于增加了一种结构化正则化。它会抑制冗余通道,减少过拟合。
第二,剪枝后 fine-tuning 进一步让紧凑模型适应新的结构。对于本来过宽、冗余较多的模型,剪掉冗余通道反而可能改善泛化。
论文也提到,适度剪枝有时会降低 test error,而剪枝比例达到 60% 或更高时,通常仍能保持相近精度。
7.4 Multi-pass 结果
multi-pass 是这篇论文很重要的扩展。论文在 CIFAR 上使用 VGGNet 测试多轮 slimming。CIFAR-10 上,第 3 轮时 fine-tuned error 为 6.10%,参数已剪掉 91.4%,FLOPs 剪掉 63.1%;第 5 轮时参数剪掉 98.3%,FLOPs 剪掉 88.7%,但错误率上升到 7.73%。
这说明 multi-pass 可以进一步提升压缩率,但也存在明显边界:
前几轮:压缩率提高,精度基本可控 过多轮:通道过少,表达能力不足,精度明显下降因此,multi-pass 不是越多越好,而是需要根据目标压缩率和精度损失进行折中。
八、方法优点
8.1 实现非常简单
Network Slimming 的最大优点是简单。
它不需要复杂的二阶信息,不需要特征重构,不需要 LASSO,也不需要额外训练一个评分网络。只需要在训练 loss 中加入:
这使得它非常容易加入现有 CNN 训练代码。
8.2 通道重要性是训练出来的
L1 filter pruning 是事后根据权重大小判断重要性;ThiNet 和 Channel Pruning 是事后根据特征重构判断重要性。
Network Slimming 则是在训练阶段就让网络自己决定哪些通道重要。
这意味着通道选择和任务训练是耦合的,而不是训练完成后再额外打分。
8.3 剪枝后是规则稠密网络
Network Slimming 删除的是整个通道,因此得到的是更窄的 dense network。
这和非结构化 weight pruning 不同。后者虽然参数中有很多 0,但不一定能在普通硬件上直接加速。Network Slimming 得到的模型不需要特殊稀疏存储或计算操作,可以直接使用常规深度学习库推理。论文也明确指出,该方法能在 conventional hardware 和 deep learning packages 上实现压缩和加速,因为剪枝后的窄模型不依赖 sparse storing format 或 sparse computation。
8.4 全局阈值简单有效
相比每层手动设置剪枝率,Network Slimming 使用全局 percentile threshold。
这有一个好处:
网络自己决定哪些层该多剪,哪些层该少剪。如果某一层大量通道的 (\gamma) 都很小,就会被剪得更多;如果某一层大多数通道 (\gamma) 较大,就会保留更多。
这种全局排序思想后来在很多剪枝方法中也被广泛使用。
8.5 可以扩展到多种结构
论文不仅测试 VGGNet,还测试了 ResNet-164 和 DenseNet-40,并讨论了 cross-layer connections 和 pre-activation 结构下如何通过 channel selection layer 处理剪枝。
这说明 Network Slimming 不只是 VGG 上的简单技巧,而是可以推广到更复杂 CNN 结构的通道稀疏训练框架。
九、方法局限
9.1 依赖重新训练
Network Slimming 不是纯 post-training pruning。它需要在训练过程中加入 BN (\gamma) 的 L1 正则。
如果只有一个已经训练好的普通模型,而没有重新训练资源,那么 Network Slimming 不能像 L1 filter pruning 那样直接剪。
9.2 对 BN 结构依赖较强
这篇论文的核心依赖 BN 的 (\gamma)。如果网络没有 BN,或者 BN 的位置和结构不适合通道选择,那么方法需要额外改造。
论文中也讨论了为什么复用 BN gamma 比额外插入 scaling layer 更合适:如果 scaling layer 放在 BN 前,其缩放效果会被 BN 归一化抵消;如果放在 BN 后,又会出现连续两个缩放因子。
9.3 ResNet / DenseNet 需要特殊结构处理
对于 VGG 这种串行结构,剪通道很直接。但在 ResNet、DenseNet 中,一个通道可能参与残差相加、跨层连接或拼接。
如果不处理这些依赖关系,直接剪通道可能造成维度不匹配。
因此,Network Slimming 在复杂结构上的工程实现并不是简单地“删掉所有小 gamma 通道”,还需要考虑结构依赖、channel selection layer 或一致性约束。
9.4 剪枝比例需要调节
(\lambda) 控制稀疏强度,剪枝 percentile 控制通道删除比例。二者都需要实验调节。
如果 (\lambda) 太小,(\gamma) 不够稀疏,剪枝效果有限;如果 (\lambda) 太大,重要通道也可能被压小,训练精度会受影响。
如果剪枝比例太高,fine-tuning 也难以完全恢复性能。
9.5 不直接适用于无 BN 的 Transformer / LLM
Network Slimming 是 CNN + BN 时代非常自然的方法。但在 Transformer、ViT、LLM 中,常见归一化层是 LayerNorm,而不是 BatchNorm;LayerNorm 的缩放参数与通道维度、token 维度、隐藏维度的关系也不同。
因此,Network Slimming 的思想可以迁移,例如对 LayerNorm scale、MLP hidden dimension 或 attention head 引入稀疏门控,但原方法不能直接照搬到所有现代大模型上。
十、后续影响
Network Slimming 对后续结构化剪枝有很大影响。
第一,它把通道剪枝从“训练后评分”推进到了“训练中学习”。通道重要性不再完全依赖人工设计的指标,而是通过稀疏正则在训练中形成。
第二,它强化了 BN scaling factor 作为通道重要性指标的思路。后续大量 CNN 剪枝方法都会利用 BN (\gamma)、scale parameter 或类似 gating variable 来判断通道重要性。
第三,它推动了“稀疏训练 + 结构化剪枝 + fine-tuning”的经典流程:
训练时加结构稀疏约束 ↓ 根据稀疏参数剪通道 ↓ 构建紧凑模型 ↓ fine-tuning 恢复精度第四,它对后续可学习剪枝、可微门控、自动通道选择和 slimmable network 都有影响。官方代码仓库也明确给出了训练带稀疏正则、prune、convert compact network、fine-tune 的完整流程。
从专栏脉络上看,这篇论文可以放在这里:
Pruning Filters for Efficient ConvNets ↓ ThiNet ↓ Channel Pruning for Accelerating Very Deep Neural Networks ↓ Network Slimming ↓ FPGM / HRank / EagleEye ↓ AutoSlim / Slimmable Networks / Hardware-aware Pruning ↓ ViT / LLM / VLM structured pruning如果说前几篇论文主要回答的是:
训练后怎么判断哪些通道可以剪?那么 Network Slimming 回答的是:
能不能在训练时就让通道重要性自己显现出来?它的答案就是:
给 BN gamma 加 L1 稀疏正则。十一、一句话总结
《Learning Efficient Convolutional Networks through Network Slimming》通过对 BN 缩放因子 (\gamma) 加 L1 稀疏正则,让网络在训练过程中自动学习通道重要性,再根据小 (\gamma) 删除冗余通道,是“稀疏训练驱动结构化剪枝”的经典代表工作。