4. 模型剪枝
任务说明
对训练好的全连接网络实施基于权重幅度的非结构化剪枝:将值低于阈值的权重置零,随后进行微调以恢复精度。本案例使用PyTorch实现完整闭环,包含剪枝、微调与验证。
环境依赖
- Python 3.8+,PyTorch 1.12+,torchvision(仅用于MNIST数据加载)
- 无其他第三方库
完整实现
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# ---------- 1. 模型定义 ----------
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.fc2 = nn.Linear(256, 128)
self.fc3 = nn.Linear(128, 10)
self.relu = nn.ReLU()
def forward(self, x):
x = x.view(x.size(0), -1)
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
return self.fc3(x)
# ---------- 2. 数据加载 ----------
def load_data(batch_size=64):
tf = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
train_ds = datasets.MNIST('./data', train=True, download=True, transform=tf)
test_ds = datasets.MNIST('./data', train=False, download=True, transform=tf)
return (DataLoader(train_ds, batch_size=batch_size, shuffle=True),
DataLoader(test_ds, batch_size=batch_size))
# ---------- 3. 训练/微调与评估 ----------
def train(model, loader, epochs=1, lr=0.001):
optimizer = optim.Adam(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
model.train()
for _ in range(epochs):
for data, target in loader:
optimizer.zero_grad()
loss = criterion(model(data), target)
loss.backward()
optimizer.step()
def evaluate(model, loader):
model.eval()
correct = 0
total = 0
with torch.no_grad():
for data, target in loader:
pred = model(data).argmax(dim=1)
correct += (pred == target).sum().item()
total += target.size(0)
return correct / total
# ---------- 4. 权重剪枝 ----------
def prune_weights(model, threshold):
"""将值低于threshold的权重置零,返回剪枝比例"""
total = 0
pruned = 0
for param in model.parameters():
if param.dim() < 2: # 跳过bias
continue
mask = param.abs() > threshold
total += param.numel()
pruned += (~mask).sum().item()
param.data.mul_(mask.float())
return pruned / total
# ---------- 5. 主流程 ----------
if __name__ == '__main__':
train_loader, test_loader = load_data()
# 训练基线模型
model = MLP()
train(model, train_loader, epochs=3, lr=0.001)
base_acc = evaluate(model, test_loader)
print(f'基线准确率: {base_acc:.4f}')
# 剪枝
threshold = 0.02
ratio = prune_weights(model, threshold)
pruned_acc = evaluate(model, test_loader)
print(f'剪枝比例: {ratio:.2%}, 剪枝后准确率: {pruned_acc:.4f}')
# 微调恢复
train(model, train_loader, epochs=2, lr=0.0005)
finetuned_acc = evaluate(model, test_loader)
print(f'微调后准确率: {finetuned_acc:.4f}')
运行方式
python prune_mnist.py
首次运行会自动下载MNIST数据集至./data目录。
验证方法
- 观察控制台输出:基线准确率通常约0.97;剪枝后准确率下降;微调后准确率回升至接近基线水平。
- 判定通过条件:剪枝比例大于50%(阈值0.02时通常可达70%以上),且微调后准确率不低于基线2个百分点以内。
- 可通过调整
threshold观察剪枝比例与精度权衡:阈值越大,剪枝越多,精度损失越大,微调恢复越困难。
关键说明
- 剪枝仅作用于权重矩阵(
dim >= 2),偏置不参与剪枝,避免破坏网络结构。 - 非结构化剪枝产生稀疏权重,但不会减少存储或计算量(需专用稀疏库支持),本案例聚焦于剪枝方法本身。
- 微调使用更低学习率(0.0005),避免破坏已保留权重的有效信息。