4. 训练后全整型量化实战
任务目标
本案例基于L1范数通道剪枝方法,对CIFAR-10分类模型进行结构化剪枝。通过计算卷积层每个通道权重张量的L1范数作为重要性指标,裁剪贡献较小的通道,实现模型压缩,并对比剪枝前后的模型参数量与精度变化。
环境依赖
Python 3.8+,PyTorch 1.10+,torchvision,NumPy。无需额外硬件,CPU即可完成本案例全部流程。
实现原理与代码
核心思路:对每个卷积层,计算其权重张量在输出通道维度上的L1范数(即每个输出通道对应卷积核的值之和),按比例保留范数较大的通道,裁剪其余通道。剪枝后需重建相邻层的输入通道维度,保持网络结构合法。
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import numpy as np
# ---------- 1. 定义简单CNN模型 ----------
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
self.conv3 = nn.Conv2d(32, 64, 3, padding=1)
self.pool = nn.MaxPool2d(2)
self.fc = nn.Linear(64 * 4 * 4, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = self.pool(F.relu(self.conv3(x)))
x = x.view(x.size(0), -1)
return self.fc(x)
# ---------- 2. L1范数通道剪枝函数 ----------
def prune_conv_layer(layer, next_layer, keep_ratio):
"""按L1范数裁剪卷积层输出通道,并调整下一层输入通道"""
# 计算每个输出通道的L1范数
l1_norm = layer.weight.abs().sum(dim=(1, 2, 3))
num_keep = max(int(l1_norm.size(0) * keep_ratio), 1)
_, indices = torch.topk(l1_norm, num_keep)
indices = torch.sort(indices).values
# 裁剪当前层
layer.weight.data = layer.weight.data[indices]
layer.bias.data = layer.bias.data[indices]
layer.out_channels = num_keep
# 调整下一层(若存在)
if next_layer is not None:
if isinstance(next_layer, nn.Conv2d):
next_layer.weight.data = next_layer.weight.data[:, indices]
next_layer.in_channels = num_keep
elif isinstance(next_layer, nn.Linear):
# 需先计算池化后的特征图尺寸
next_layer.weight.data = next_layer.weight.data.view(
next_layer.out_features, -1, 4, 4)[:, indices].view(
next_layer.out_features, -1)
next_layer.in_features = num_keep * 4 * 4
# ---------- 3. 训练/测试工具 ----------
def train(model, loader, epochs=1):
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
model.train()
for epoch in range(epochs):
for x, y in loader:
optimizer.zero_grad()
loss = criterion(model(x), y)
loss.backward()
optimizer.step()
def evaluate(model, loader):
model.eval()
correct = 0
total = 0
with torch.no_grad():
for x, y in loader:
out = model(x)
pred = out.argmax(dim=1)
correct += (pred == y).sum().item()
total += y.size(0)
return correct / total
def count_params(model):
return sum(p.numel() for p in model.parameters())
# ---------- 4. 主流程 ----------
if __name__ == "__main__":
# 数据准备(仅用少量数据演示流程)
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(testset, batch_size=64)
# 初始化并训练原始模型
model = SimpleCNN()
print("原始模型参数量:", count_params(model))
train(model, train_loader, epochs=1) # 演示用1轮,实际可增加
acc_before = evaluate(model, test_loader)
print(f"剪枝前精度: {acc_before:.4f}")
# 执行剪枝(保留50%通道)
prune_conv_layer(model.conv1, model.conv2, keep_ratio=0.5)
prune_conv_layer(model.conv2, model.conv3, keep_ratio=0.5)
prune_conv_layer(model.conv3, model.fc, keep_ratio=0.5)
print("剪枝后模型参数量:", count_params(model))
acc_after = evaluate(model, test_loader)
print(f"剪枝后精度(未微调): {acc_after:.4f}")
# 微调恢复精度
train(model, train_loader, epochs=1)
acc_finetuned = evaluate(model, test_loader)
print(f"微调后精度: {acc_finetuned:.4f}")
运行与验证
直接执行脚本即可完成全流程。首次运行会自动下载CIFAR-10数据集(约170MB)。验证通过条件:
- 程序无异常退出,输出四行指标信息;
- 剪枝后参数量显著小于原始参数量(约减少50%以上);
- 剪枝后精度下降在可接受范围(通常<5%),微调后精度回升。
关键说明
本案例为教学演示,训练仅1轮且未做数据增强,因此精度值偏低属正常现象。实际应用中应增加训练轮数(如10-20轮)以获得更高基线精度。剪枝比例可通过keep_ratio参数调节,本案例统一使用0.5。若需对全连接层之前的卷积层剪枝,需注意view操作中的特征图尺寸需与实际网络结构匹配。