4. 全局剪枝与局部剪枝对比
任务目标
在PyTorch中实现两种结构化剪枝策略——全局阈值剪枝(所有层共享一个稀疏度阈值)与逐层剪枝(每层按各自稀疏度独立裁剪),在相同总稀疏度下对比二者对模型精度的影响,并输出压缩率与精度变化。
环境与依赖
Python 3.8+,PyTorch 1.12+,torchvision,NumPy。无需额外硬件,CPU即可完成本实验。
实现思路
使用LeNet-5在MNIST上训练3个epoch作为基线,随后分别执行全局剪枝与逐层剪枝(均为非结构化细粒度剪枝),最后微调1个epoch并评估。全局剪枝将所有卷积/全连接层的权重值合并排序,按全局阈值置零;逐层剪枝则对每层单独排序,使每层稀疏度一致。两种方法保持总稀疏度相同(50%),以便公平对比。
关键完整代码
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
# ---------- 模型定义 ----------
class LeNet5(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 6, 5, padding=2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = F.max_pool2d(F.relu(self.conv1(x)), 2)
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
x = x.view(x.size(0), -1)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
return self.fc3(x)
# ---------- 数据加载 ----------
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
train_ds = datasets.MNIST('./data', train=True, download=True, transform=transform)
test_ds = datasets.MNIST('./data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)
test_loader = DataLoader(test_ds, batch_size=256)
# ---------- 训练/评估工具 ----------
def train(model, epochs=1):
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
model.train()
for _ in range(epochs):
for x, y in train_loader:
opt.zero_grad()
loss = F.cross_entropy(model(x), y)
loss.backward()
opt.step()
def evaluate(model):
model.eval()
correct = total = 0
with torch.no_grad():
for x, y in test_loader:
pred = model(x).argmax(dim=1)
correct += (pred == y).sum().item()
total += y.size(0)
return correct / total
def count_zeros(model):
total = zeros = 0
for p in model.parameters():
if p.dim() > 1: # 只统计权重,不统计bias
total += p.numel()
zeros += (p == 0).sum().item()
return zeros / total
# ---------- 剪枝实现 ----------
def global_prune(model, sparsity):
"""全局阈值剪枝:所有层合并排序后统一置零"""
weights = [p for p in model.parameters() if p.dim() > 1]
all_vals = torch.cat([w.abs().flatten() for w in weights])
threshold = torch.quantile(all_vals, sparsity)
with torch.no_grad():
for w in weights:
w.masked_fill_(w.abs() < threshold, 0)
def layerwise_prune(model, sparsity):
"""逐层剪枝:每层独立按相同稀疏度裁剪"""
with torch.no_grad():
for p in model.parameters():
if p.dim() > 1:
threshold = torch.quantile(p.abs().flatten(), sparsity)
p.masked_fill_(p.abs() < threshold, 0)
# ---------- 主流程 ----------
print("=== 训练基线模型 ===")
model_base = LeNet5()
train(model_base, epochs=3)
acc_base = evaluate(model_base)
print(f"基线精度: {acc_base:.4f}")
# 全局剪枝
model_global = LeNet5()
model_global.load_state_dict(model_base.state_dict())
global_prune(model_global, sparsity=0.5)
train(model_global, epochs=1)
acc_global = evaluate(model_global)
sp_global = count_zeros(model_global)
# 逐层剪枝
model_layer = LeNet5()
model_layer.load_state_dict(model_base.state_dict())
layerwise_prune(model_layer, sparsity=0.5)
train(model_layer, epochs=1)
acc_layer = evaluate(model_layer)
sp_layer = count_zeros(model_layer)
# ---------- 结果输出 ----------
def calc_compression(model):
"""计算压缩率:非零参数占比的倒数"""
total = nonzero = 0
for p in model.parameters():
if p.dim() > 1:
total += p.numel()
nonzero += (p != 0).sum().item()
return total / max(nonzero, 1)
print("\n=== 剪枝结果对比 ===")
print(f"全局剪枝: 实际稀疏度={sp_global:.4f}, 压缩率={calc_compression(model_global):.2f}x, 精度={acc_global:.4f}")
print(f"逐层剪枝: 实际稀疏度={sp_layer:.4f}, 压缩率={calc_compression(model_layer):.2f}x, 精度={acc_layer:.4f}")
print(f"精度差(全局-逐层): {acc_global - acc_layer:+.4f}")
运行方式
将上述代码保存为prune_compare.py,在项目目录下执行:
python prune_compare.py
首次运行会自动下载MNIST数据集(约10MB),需保持网络畅通。
验证方法与判定标准
程序输出应包含三部分:基线精度、两种剪枝策略的稀疏度/压缩率/精度。通过标准如下:
- 两种剪枝的实际稀疏度均接近0.5(误差<0.01),证明剪枝量一致;
- 全局剪枝与逐层剪枝的精度差在±0.02以内属正常(MNIST上两者差异通常较小);
- 若逐层剪枝精度显著高于全局剪枝(>0.03),说明该模型各层冗余度不均,逐层策略更优;反之则全局策略更优。
可修改sparsity参数(如0.3、0.7)重复实验,观察不同稀疏度下两种策略的精度变化趋势。
常见问题
- 稀疏度不准确:
torch.quantile在数据量小时可能有微小偏差,若需精确稀疏度可改用排序后取第k个值作为阈值。 - 精度大幅下降:当稀疏度>0.7时,LeNet-5精度会明显退化,建议在0.3~0.6范围内对比。
- bias未剪枝:代码仅对
dim>1的权重剪枝,bias保持稠密,这是结构化剪枝的常见做法,不影响对比公平性。