今日目标:掌握结构化剪枝与非结构化剪枝的核心原理,理解彩票假说,学会从零实现剪枝并对比效果
预计阅读:10分钟 | 动手操作:35分钟
一、剪枝:去掉不重要的部分
模型压缩的三大武器:
1. 量化 (Quantization): 降低精度 (FP32 → INT8)
2. 剪枝 (Pruning): 去掉冗余参数
3. 蒸馏 (Distillation): 大模型教小模型
剪枝的直觉:
神经网络有很多"冗余"参数
很多权重的值非常小(接近0)
去掉这些参数 → 模型变小,精度几乎不变
类比:修剪树枝
树 → 神经网络
枯枝 → 接近0的权重
修剪后 → 树更健康(模型更快),形状不变(精度不变)
剪枝的分类:
按粒度:
非结构化剪枝: 去掉单个权重 → 稀疏矩阵
结构化剪枝: 去掉整行/整列/整通道 → 密集矩阵
按时机:
训练后剪枝: 训练完再剪
训练中剪枝: 边训练边剪
迭代剪枝: 剪 → 训练 → 剪 → 训练 → ...
import torch
import torch.nn as nn
import torch.nn.utils.prune as prune
import numpy as np
torch.manual_seed(42)
二、核心知识点
2.1 非结构化剪枝:去掉单个权重
"""
非结构化剪枝 (Unstructured Pruning)
核心思想:去掉绝对值最小的权重
权重值越小 → 贡献越小 → 去掉影响最小
方法:基于幅度的剪枝 (Magnitude-based Pruning)
1. 计算所有权重的绝对值
2. 按绝对值从小到大排序
3. 把最小的K%的权重置为0
4. 用mask记录哪些权重被剪掉了
数学模型:
mask = |w| > threshold
w_pruned = w × mask
优点:
压缩率高(可以去掉90%+的权重)
精度损失小
缺点:
产生稀疏矩阵
需要硬件/框架支持稀疏矩阵运算
实际加速效果依赖硬件
端侧设备(如Jetson)对稀疏矩阵支持有限
"""
class MagnitudePruner:
"""
基于幅度的非结构化剪枝器
对单个权重进行剪枝,保留绝对值最大的部分
"""
def __init__(self, amount=0.3):
"""
amount: 剪枝比例 (0.3 = 去掉30%的权重)
"""
self.amount = amount
def prune(self, tensor):
"""
tensor: 任意形状的权重张量
返回:(剪枝后的张量, mask)
"""
# 计算阈值
flat = tensor.abs().flatten()
k = int(len(flat) * self.amount)
if k == 0:
return tensor, torch.ones_like(tensor)
threshold = torch.topk(flat, k, largest=False).values[-1]
# 创建mask
mask = (tensor.abs() > threshold).float()
# 剪枝
pruned = tensor * mask
return pruned, mask
def sparsity(self, tensor):
"""计算稀疏度"""
total = tensor.numel()
zeros = (tensor == 0).sum().item()
return zeros / total
def unstructured_pruning_demo():
"""演示非结构化剪枝"""
# 创建一个权重矩阵
weights = torch.randn(4, 6)
pruner = MagnitudePruner(amount=0.3)
pruned, mask = pruner.prune(weights)
sparsity = pruner.sparsity(pruned)
print("非结构化剪枝演示:")
print(f"\n原始权重 (4×6):")
print(weights.numpy())
print(f"\n剪枝后 (去掉30%):")
print(pruned.numpy())
print(f"\nMask (1=保留, 0=剪掉):")
print(mask.numpy())
print(f"\n稀疏度: {sparsity:.1%}")
print(f"非零权重: {(1-sparsity)*100:.1f}%")
print("\n观察:")
print(" 零值分散在矩阵各处 → 稀疏矩阵")
print(" 实际计算时仍需完整矩阵的存储结构")
print(" 需要用稀疏矩阵格式(CSR/CSC)才能加速")
unstructured_pruning_demo()
2.2 结构化剪枝:去掉整行/整通道
"""
结构化剪枝 (Structured Pruning)
核心思想:去掉整个结构单元(行/列/通道/卷积核)
不是去掉单个权重,而是去掉整个"神经元"
常见粒度:
1. 权重级别: 去掉单个权重 → 非结构化
2. 神经元级别: 去掉整行/整列 → 结构化
3. 通道级别: 去掉整个通道 → 结构化
4. 卷积核级别: 去掉整个卷积核 → 结构化
结构化剪枝的优势:
剪枝后的模型仍然是密集矩阵
不需要稀疏矩阵计算支持
可以直接加速!
端侧设备完美支持
结构化剪枝的方法:
1. L1-norm剪枝: 去掉L1范数最小的通道
2. L2-norm剪枝: 去掉L2范数最小的通道
3. 基于BN系数的剪枝: 去掉BN的γ很小的通道
"""
class ChannelPruner:
"""
结构化通道剪枝
基于L1-norm:去掉L1范数最小的通道
"""
def __init__(self, amount=0.3):
self.amount = amount
def prune_channels(self, conv_layer):
"""
剪枝一个卷积层的输出通道
输入通道也会被影响(下一层的输入通道)
实际中需要同时处理前后层
"""
weight = conv_layer.weight.data # (C_out, C_in, K, K)
# 计算每个输出通道的L1范数
l1_norms = weight.abs().sum(dim=(1, 2, 3)) # (C_out,)
# 选择要保留的通道
num_channels = len(l1_norms)
num_keep = int(num_channels * (1 - self.amount))
_, indices = torch.topk(l1_norms, num_keep, largest=True)
# 保留选中的通道
pruned_weight = weight[indices]
# 如果有bias,也剪枝
pruned_bias = None
if conv_layer.bias is not None:
pruned_bias = conv_layer.bias.data[indices]
return pruned_weight, pruned_bias, indices
def structured_pruning_demo():
"""演示结构化剪枝"""
conv = nn.Conv2d(64, 128, 3)
pruner = ChannelPruner(amount=0.3)
pruned_weight, pruned_bias, indices = pruner.prune_channels(conv)
print("结构化剪枝演示:")
print(f" 原始Conv: {conv.weight.shape}")
print(f" 剪枝30%通道后: {pruned_weight.shape}")
print(f" 保留的通道索引: {indices[:10].tolist()}...")
print(f" 保留通道数: {len(indices)}")
print()
print(" 结构化剪枝的优势:")
print(" 1. 输出仍然是密集矩阵")
print(" 2. 不需要稀疏矩阵计算")
print(" 3. 可以直接加速!")
print(" 4. 端侧设备完美支持")
structured_pruning_demo()
2.3 彩票假说:子网络的价值
"""
彩票假说 (Lottery Ticket Hypothesis)
论文:https://arxiv.org/abs/1803.03635
核心思想:
一个随机初始化的密集网络
包含一个"中奖彩票"子网络
这个子网络从头训练,就能达到原网络的精度
类比:
买一堆彩票 → 大网络
中奖彩票 → 关键子网络
用中奖彩票兑奖 → 子网络训练
彩票假说的步骤:
1. 随机初始化网络
2. 训练网络
3. 剪掉幅度最小的权重
4. 把剩余权重重置为初始值
5. 重新训练 → 能达到原网络精度!
实用意义:
证明了"网络中有冗余"
剪枝不是"破坏"网络,而是"发现"关键子网络
剪枝 + 微调 > 从头训练小网络
"""
def lottery_ticket_demo():
print("彩票假说 (Lottery Ticket Hypothesis):")
print()
print("步骤:")
print(" 1. 随机初始化 → 大网络")
print(" 2. 训练 → 得到训练好的权重")
print(" 3. 剪枝 → 去掉不重要的权重")
print(" 4. 重置 → 剩余权重恢复到初始值")
print(" 5. 重新训练 → 精度不输原始网络!")
print()
print("核心发现:")
print(" '中奖彩票' = 初始化的关键权重")
print(" 剪枝就是找到这些关键权重")
print(" 不是所有初始权重都重要")
print()
print("端侧AI的启示:")
print(" 大模型中有小模型的影子")
print(" 剪枝是找到小模型的方法")
print(" 剪枝+微调是目前最好的模型压缩方法之一")
lottery_ticket_demo()
三、动手实践:PyTorch剪枝API
def pytorch_pruning_demo():
"""
PyTorch官方剪枝API演示
PyTorch提供了torch.nn.utils.prune模块
支持多种剪枝策略
"""
# 创建一个简单的Linear层
linear = nn.Linear(10, 5)
print("PyTorch剪枝API演示:")
print(f" 原始权重形状: {linear.weight.shape}")
print(f" 原始参数数量: {linear.weight.numel()}")
# 1. L1非结构化剪枝 (去掉30%的权重)
prune.l1_unstructured(linear, name='weight', amount=0.3)
# 剪枝后,权重被mask包裹
# linear.weight 不是原始权重,而是被mask修饰的
# linear.weight_orig 是原始权重(用于恢复)
# linear.weight_mask 是mask
sparsity = 100. * float(torch.sum(linear.weight == 0)) / float(linear.weight.nelement())
print(f"\n 剪枝后 (L1, 30%):")
print(f" 稀疏度: {sparsity:.1f}%")
print(f" 权重: {linear.weight}")
print(f" 原始权重(weight_orig): {linear.weight_orig.shape}")
print(f" Mask: {linear.weight_mask.shape}")
# 2. 永久移除剪枝
prune.remove(linear, 'weight')
print(f"\n 移除剪枝后:")
print(f" 权重形状: {linear.weight.shape}")
print(f" 稀疏度: {100.*float(torch.sum(linear.weight==0))/linear.weight.nelement():.1f}%")
print("\nPyTorch剪枝支持:")
print(" l1_unstructured: L1范数剪枝")
print(" random_unstructured: 随机剪枝")
print(" ln_structured: 结构化剪枝")
pytorch_pruning_demo()
四、端侧AI剪枝策略
def edge_pruning_strategy():
"""
端侧AI剪枝选型
"""
guide = {
'非结构化剪枝': {
'压缩率': '90%+ (极高)',
'精度损失': '小',
'加速效果': '依赖硬件',
'端侧适用': '⚠️ 需要稀疏计算支持',
'推荐场景': '研究、极致压缩',
},
'结构化剪枝': {
'压缩率': '50-70% (中等)',
'精度损失': '中等',
'加速效果': '直接加速',
'端侧适用': '✅ 完美支持',
'推荐场景': '端侧部署首选',
},
'BN剪枝': {
'压缩率': '30-50%',
'精度损失': '小',
'加速效果': '直接加速',
'端侧适用': '✅ 完美支持',
'推荐场景': '有BN的模型',
},
'剪枝+量化': {
'压缩率': '80-90% (极高)',
'精度损失': '中等',
'加速效果': '最大',
'端侧适用': '✅ 剪枝+INT8量化',
'推荐场景': '追求极致性能',
},
}
print("端侧AI剪枝策略:")
for method, info in guide.items():
print(f"\n [{method}]")
for k, v in info.items():
print(f" {k}: {v}")
edge_pruning_strategy()
五、常见坑点
坑1:剪枝后不微调
# 剪枝会破坏模型的权重分布
# 直接推理 → 精度下降
# ❌ 错误:剪完直接部署
# model = prune(model)
# model.eval() # 直接推理
# ✅ 正确:剪枝后微调几个epoch
# model = prune(model)
# for epoch in range(3):
# train(model, dataloader) # 微调
# model.eval() # 推理
坑2:剪枝比例太高
# 剪枝比例和精度的关系(经验值):
# 10%: 几乎无损
# 30%: 轻微下降 (< 0.5%)
# 50%: 中等下降 (1-2%)
# 70%: 明显下降 (3-5%)
# 90%: 大幅下降 (5-10%)
# 端侧推荐:30-50%
# 如果精度下降 > 2% → 降低剪枝比例
坑3:结构化剪枝需要同步处理前后层
# 剪掉第i层的输出通道 → 第i+1层的输入通道也要剪
# 如果不处理 → 维度不匹配 → 报错
# 正确做法:
# 1. 确定要保留的通道索引
# 2. 剪掉当前层的输出通道
# 3. 剪掉下一层的输入通道(对应相同的索引)
# 4. 如果有BN层 → 也要剪对应的BN参数
# 5. 如果有残差连接 → 两边都要处理
坑4:剪枝和量化的顺序
# 剪枝和量化可以结合使用
# 推荐顺序:
# 1. 先剪枝(结构化剪枝)
# 2. 剪枝后微调
# 3. 再量化(INT8)
# 4. 量化后校准
# 不要反过来!量化的INT8模型不能再剪枝
# 剪枝 → 量化 效果最好
六、今日作业
- 手写剪枝器:实现基于L1范数的非结构化剪枝和结构化剪枝
- PyTorch剪枝:用PyTorch官方的prune模块对模型剪枝
- 对比实验:不同剪枝比例(10%/30%/50%)对精度的影响
- 打卡:评论区发你的理解,格式:"Day 44/100 打卡:模型剪枝已掌握!"
今日小结
今天你学会了:
✅ 剪枝的本质:去掉不重要的参数,保持精度
✅ 非结构化剪枝:去掉单个权重 → 稀疏矩阵
✅ 结构化剪枝:去掉整行/整列/整通道 → 密集矩阵
✅ L1-norm剪枝:去掉范数最小的通道
✅ 彩票假说:大网络中藏着"中奖"子网络
✅ 剪枝后必须微调(3-5个epoch)
✅ 端侧AI:结构化剪枝 + 量化 = 最强组合
✅ 4个经典坑点
明日预告
Day 45:知识蒸馏基础
软标签、温度系数、蒸馏策略、教师-学生网络
🔥 关注我,每天解锁一个端侧AI技能!
微信公众号:xxx | 小红书:xxx | CSDN:xxx
评论区打卡,一起坚持100天!
附:小红书图文版
封面标题建议:模型剪枝 | 给神经网络"瘦身",精度几乎不变!✂️
P1 — 封面
标题:模型剪枝
副标题:结构化剪枝 / 非结构化剪枝 / 彩票假说
P2 — 剪枝的直觉
神经网络有很多接近0的冗余权重
剪枝 = 去掉这些权重
类比:修剪枯枝 → 树更健康
模型变小 → 速度变快 → 精度不变
P3 — 非结构化 vs 结构化
非结构化:去掉单个权重
稀疏矩阵,需要硬件支持
结构化:去掉整行/整列/整通道
密集矩阵,直接加速!
端侧AI:结构化剪枝首选!
P4 — L1-norm剪枝
计算每个通道的L1范数
去掉范数最小的通道
保留最重要的通道
剪枝30% → 精度下降 < 1%
P5 — 彩票假说
大网络 = 一堆彩票
中奖彩票 = 关键子网络
子网络从头训练 → 精度相同!
证明了网络中有冗余
P6 — 端侧策略
剪枝比例:30-50%
剪枝后必须微调!
剪枝 + 量化 = 最强组合
先剪枝再量化,不要反过来
评论区打卡 Day 44/100
标签:#模型剪枝 #模型压缩 #彩票假说 #端侧AI
CSDN发布提示:CSDN版本建议在非结构化vs结构化部分放稀疏矩阵vs密集矩阵对比图,在L1-norm剪枝部分放通道选择示意图,在彩票假说部分放迭代剪枝流程图,在端侧策略部分放剪枝+量化组合流程图。