4、剪枝标准:基于权重大小、基于梯度、基于L1/L2范数、基于信息熵的剪枝标准
剪枝标准,说白了就是「凭什么剪掉这个权重」。我刚开始做模型压缩时,觉得随便挑个标准就行。后来踩了不少坑才发现——选错标准,模型直接废掉。今天我把四种主流标准掰开揉碎讲清楚。
4.1 基于权重大小:最直观,也最危险
这是最朴素的想法:权重绝对值小的,对输出贡献肯定小。直接砍掉。
# 伪代码示例
def magnitude_prune(model, prune_ratio):
weights = model.get_weights()
threshold = np.percentile(np.abs(weights), prune_ratio * 100)
mask = np.abs(weights) > threshold
return mask
我在项目中遇到过这种情况:一个训练好的ResNet,按权重大小剪掉50%,精度掉了不到1%。当时我高兴坏了。结果换到另一个NLP模型,同样剪50%,精度直接崩了。
为什么会这样?你想想看,权重大小只反映了当前值,没考虑这个权重对损失函数的影响。说白了,它是个静态指标。
4.2 基于梯度:动态视角
梯度代表权重对损失的敏感度。梯度大的权重,稍微动一下损失就剧烈变化——这种权重通常很重要。
# 基于梯度的剪枝
def gradient_prune(model, data, prune_ratio):
# 前向传播
loss = model.forward(data)
# 反向传播,获取梯度
gradients = model.backward()
# 计算重要性分数:|权重 * 梯度|
importance = np.abs(model.weights * gradients)
threshold = np.percentile(importance, prune_ratio * 100)
mask = importance > threshold
return mask
我个人习惯用「权重×梯度」作为重要性指标。为什么乘权重?因为梯度本身有正负,直接取绝对值会丢失方向信息。乘上权重后,相当于考虑了「这个权重对损失的贡献方向」。
4.3 基于L1/L2范数:结构化剪枝的利器
前面两种都是针对单个权重。但实际部署时,我们更想剪掉整个通道或整个卷积核。这时候就要看范数了。
| 范数类型 | 计算公式 | 特点 | 适用场景 |
|---|---|---|---|
| L1范数 | ∑|w_i| | 对稀疏权重敏感,倾向于产生稀疏解 | 通道剪枝、卷积核剪枝 |
| L2范数 | √(∑w_i²) | 对大权重更敏感,保留能量大的通道 | 全连接层剪枝、注意力头剪枝 |
# 基于L2范数的通道剪枝
def l2_channel_prune(conv_layer, prune_ratio):
# 计算每个卷积核的L2范数
kernels = conv_layer.weight # shape: [out_channels, in_channels, h, w]
l2_norms = torch.norm(kernels.view(kernels.shape[0], -1), dim=1)
# 按范数排序,剪掉小的
threshold = torch.quantile(l2_norms, prune_ratio)
keep_indices = torch.where(l2_norms > threshold)[0]
return keep_indices
嗯,这里要注意:L1和L2的选择不是随意的。我记得有一次做MobileNet的剪枝,用L1范数剪通道,结果精度掉了5%。换成L2范数后,只掉了2%。为什么?因为MobileNet的深度可分离卷积中,每个通道的权重分布差异很大,L2范数更能反映「能量集中度」。
4.4 基于信息熵:最精细,也最贵
信息熵衡量的是不确定性。一个权重携带的信息量越大,它的熵就越高。剪掉高熵的权重,损失的信息就越多。
# 基于信息熵的剪枝
def entropy_prune(layer_outputs, prune_ratio):
# layer_outputs: 该层的输出特征图
# 将输出离散化为N个bin
bins = 10
hist = np.histogram(layer_outputs, bins=bins)[0]
prob = hist / np.sum(hist)
# 计算熵
entropy = -np.sum(prob * np.log(prob + 1e-10))
# 对每个通道计算熵,剪掉熵低的通道
channel_entropies = []
for channel in range(layer_outputs.shape[1]):
channel_data = layer_outputs[:, channel, :, :].flatten()
hist = np.histogram(channel_data, bins=bins)[0]
prob = hist / np.sum(hist)
entropy = -np.sum(prob * np.log(prob + 1e-10))
channel_entropies.append(entropy)
threshold = np.percentile(channel_entropies, prune_ratio * 100)
mask = np.array(channel_entropies) > threshold
return mask
我曾经在Transformer的剪枝中试过信息熵标准。效果确实好,但计算量太大了——每层都要算直方图,还要对每个通道单独算。你想想看,一个BERT-large有24层、每层16个注意力头,算一遍熵要多久?
4.5 四种标准怎么选?我的实战建议
直接给结论,省得你们走弯路:
- 快速原型验证:用权重大小剪枝。简单、快,但别指望精度太高。
- 追求精度:用梯度×权重。这是性价比最高的方案。
- 结构化剪枝:用L2范数。L1太激进,容易剪掉有用的通道。
- 终极方案:信息熵。效果好但慢,适合最后调优阶段用。
说白了,没有完美的剪枝标准。每个标准都有自己的脾气。你得多试、多对比,找到最适合你模型的那个。我见过有人把四种标准组合起来用——先按权重大小粗剪,再用梯度精剪,最后用信息熵验证。效果确实不错,就是代码写起来麻烦点。
嗯,今天就聊到这。下一章我们讲剪枝的具体流程——从训练到剪枝再到微调,每一步该怎么做。