第3章:剪枝原理——权重重要性评估方法
剪枝说白了就是「砍掉不重要的连接」。但问题来了——你怎么知道哪个连接不重要?
我刚开始做模型压缩时,天真地以为直接砍掉值小的权重就行。结果模型直接崩了。后来才明白,权重重要性评估才是剪枝的核心。这一章,我们就来聊聊三种主流方法:L1/L2范数、BN层gamma值、梯度敏感度。
3.1 基于L1/L2范数的重要性评估
这是最直观的方法。L1范数是权重值之和,L2范数是权重平方和的平方根。数值越大,说明这个权重对输出的贡献越大。
我个人习惯用L1范数做初步筛选。为什么?因为它计算简单,而且对稀疏性更敏感。你想想看,如果一个卷积核的L1范数接近0,那它基本就是个「废核」。
| 方法 | 计算公式 | 特点 | 适用场景 |
|---|---|---|---|
| L1范数 | ||W||₁ = Σ|wᵢ| | 对稀疏权重敏感,计算快 | 结构化剪枝(通道级) |
| L2范数 | ||W||₂ = √(Σwᵢ²) | 对大权重更敏感,考虑整体分布 | 非结构化剪枝(权重级) |
代码实现也很简单:
import torch
import torch.nn as nn
def l1_importance(conv_layer):
"""
计算卷积层的L1范数重要性
conv_layer: nn.Conv2d
return: 每个输出通道的重要性分数
"""
weight = conv_layer.weight.data # shape: [out_channels, in_channels, k, k]
importance = weight.abs().sum(dim=(1, 2, 3)) # 对每个输出通道求和
return importance
# 使用示例
conv = nn.Conv2d(64, 128, 3)
scores = l1_importance(conv)
print(f"Top-5 重要通道: {scores.topk(5).indices}")
3.2 基于BN层gamma值的重要性评估
YOLOv11里几乎每个卷积后面都跟着BN层。BN层的gamma参数(缩放因子)其实是个天然的重要性指标。
为什么?因为BN层的计算公式是:
y = gamma * (x - mean) / sqrt(var + eps) + beta
gamma值越小,说明这个通道的输出被压缩得越厉害。当gamma趋近于0时,这个通道的输出基本就是常数beta,对后续层几乎没有影响。
我在项目中遇到过一个问题:直接用L1范数剪枝,剪完后模型精度掉了3个点。后来换成BN gamma值排序,同样的剪枝率只掉了0.5个点。原因就是BN gamma值天然考虑了训练过程中的数据分布。
def bn_gamma_importance(bn_layer):
"""
提取BN层的gamma值作为重要性指标
bn_layer: nn.BatchNorm2d
return: 每个通道的重要性分数
"""
gamma = bn_layer.weight.data.abs() # gamma的值
return gamma
# 在YOLOv11中遍历所有BN层
def collect_gamma_scores(model):
scores = {}
for name, module in model.named_modules():
if isinstance(module, nn.BatchNorm2d):
scores[name] = bn_gamma_importance(module)
return scores
3.3 基于梯度敏感度的重要性评估
这个方法更「高级」一些。它考虑的是:如果我把某个权重置为0,损失函数会变化多少?
数学上,梯度敏感度定义为:
S(w) = |w * ∂L/∂w|
说白了,就是权重值乘以它的梯度。如果权重很大但梯度很小,说明这个权重已经收敛了,砍掉它影响不大。反过来,如果权重很小但梯度很大,说明它还在「挣扎」,砍掉它可能会让模型学不到东西。
我曾经在YOLOv11的检测头上试过梯度敏感度剪枝。效果确实比BN gamma好一点,但计算量大了不少——需要跑一次反向传播才能拿到梯度。
def gradient_sensitivity(model, dataloader, criterion):
"""
计算每个权重的梯度敏感度
需要一次前向+反向传播
"""
model.train()
sensitivity = {}
# 注册hook来捕获梯度
hooks = []
for name, param in model.named_parameters():
if param.requires_grad:
sensitivity[name] = torch.zeros_like(param)
def hook_fn(grad, n=name):
sensitivity[n] = grad.abs() * param.data.abs()
hooks.append(param.register_hook(hook_fn))
# 跑一个batch
images, targets = next(iter(dataloader))
outputs = model(images)
loss = criterion(outputs, targets)
loss.backward()
# 清理hook
for h in hooks:
h.remove()
return sensitivity
3.4 三种方法的对比与选择
我把这三种方法放在一起对比过,结果如下:
| 方法 | 计算开销 | 精度保持 | 实现难度 | 推荐场景 |
|---|---|---|---|---|
| L1/L2范数 | 低(无需额外计算) | 中等 | 低 | 快速原型验证 |
| BN gamma值 | 低(直接提取参数) | 高 | 低 | YOLO系列主流方案 |
| 梯度敏感度 | 高(需反向传播) | 最高 | 中 | 关键层精细剪枝 |
3.5 本章知识体系
下面这张图总结了三种重要性评估方法的核心逻辑: