1. 基于PyTorch的LeNet-5模型剪枝与量化

基于PyTorch的LeNet-5模型剪枝与量化 学习卡片

任务目标

在MNIST数据集上完成LeNet-5的完整流程:训练基线模型 → 结构化剪枝(按卷积通道剪枝)→ 后训练量化(PTQ,8位动态量化)→ 对比剪枝/量化前后的模型大小与测试精度。

环境依赖

Python 3.8+,PyTorch 1.13+,torchvision,numpy。MNIST数据集首次运行自动下载(需网络或提前放置./data目录)。

关键完整代码

import torch
import torch.nn as nn
import torch.nn.utils.prune as prune
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

# ---------- 1. 定义LeNet-5(适配MNIST 28x28输入) ----------
class LeNet5(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 6, 5, padding=2), nn.ReLU(), nn.MaxPool2d(2),   # 6@14x14
            nn.Conv2d(6, 16, 5), nn.ReLU(), nn.MaxPool2d(2),             # 16@5x5
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(16*5*5, 120), nn.ReLU(),
            nn.Linear(120, 84), nn.ReLU(),
            nn.Linear(84, 10)
        )
    def forward(self, x):
        return self.classifier(self.features(x))

# ---------- 2. 训练/测试工具 ----------
def train(model, loader, epochs=1, lr=0.001):
    device = next(model.parameters()).device
    model.train()
    opt = torch.optim.Adam(model.parameters(), lr=lr)
    loss_fn = nn.CrossEntropyLoss()
    for _ in range(epochs):
        for x, y in loader:
            x, y = x.to(device), y.to(device)
            opt.zero_grad()
            loss = loss_fn(model(x), y)
            loss.backward()
            opt.step()

def evaluate(model, loader):
    device = next(model.parameters()).device
    model.eval()
    correct = total = 0
    with torch.no_grad():
        for x, y in loader:
            x, y = x.to(device), y.to(device)
            pred = model(x).argmax(dim=1)
            correct += (pred == y).sum().item()
            total += y.size(0)
    return correct / total

def model_size_mb(model):
    return sum(p.numel()*p.element_size() for p in model.parameters()) / 1e6

# ---------- 3. 结构化剪枝:按卷积输出通道全局阈值 ----------
def structured_prune(model, amount=0.3):
    """对两个卷积层按L2范数剪掉指定比例的整个输出通道"""
    convs = [m for m in model.features if isinstance(m, nn.Conv2d)]
    # 收集所有卷积核的L2范数
    norms = []
    for conv in convs:
        w = conv.weight.detach()  # [out_c, in_c, kh, kw]
        norms.append(w.view(w.size(0), -1).norm(dim=1))
    all_norms = torch.cat(norms)
    threshold = torch.quantile(all_norms, amount)

    for conv in convs:
        w = conv.weight.detach()
        keep = w.view(w.size(0), -1).norm(dim=1) > threshold
        # 若全部被剪则保留范数最大的一个,避免通道数为0
        if not keep.any():
            keep[w.view(w.size(0), -1).norm(dim=1).argmax()] = True
        # 用prune方法将不保留通道的权重置零(结构化掩码)
        prune.custom_from_mask(conv, name='weight', mask=keep.view(-1, 1, 1, 1).expand_as(w).float())
        prune.remove(conv, 'weight')  # 固化掩码,实际删除参数

# ---------- 4. 主流程 ----------
def main():
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    tf = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
    train_loader = DataLoader(datasets.MNIST('./data', train=True, download=True, transform=tf), batch_size=64, shuffle=True)
    test_loader = DataLoader(datasets.MNIST('./data', train=False, download=True, transform=tf), batch_size=256)

    # 训练基线
    model = LeNet5().to(device)
    train(model, train_loader, epochs=2)
    acc_base = evaluate(model, test_loader)
    size_base = model_size_mb(model)

    # 结构化剪枝(30%通道)
    structured_prune(model, amount=0.3)
    # 剪枝后微调1个epoch恢复精度
    train(model, train_loader, epochs=1, lr=0.0005)
    acc_pruned = evaluate(model, test_loader)
    size_pruned = model_size_mb(model)

    # PTQ:8位动态量化(仅量化Linear层)
    model_quant = torch.quantization.quantize_dynamic(model, {nn.Linear}, dtype=torch.qint8)
    acc_quant = evaluate(model_quant, test_loader)
    size_quant = model_size_mb(model_quant)

    print(f'基线:   精度 {acc_base:.4f}, 模型 {size_base:.3f} MB')
    print(f'剪枝后: 精度 {acc_pruned:.4f}, 模型 {size_pruned:.3f} MB')
    print(f'量化后: 精度 {acc_quant:.4f}, 模型 {size_quant:.3f} MB')

if __name__ == '__main__':
    main()

运行方式

python lenet_prune_quant.py

验证方法与通过条件

  • 精度对比:剪枝后精度相对基线下降通常<1%,量化后精度下降通常<0.5%(MNIST任务较简单,实际波动以运行为准)。
  • 模型大小:量化后模型大小应显著小于基线(动态量化主要压缩Linear层,LeNet-5中Linear层占参数量约97%,预期可减小至约1/4)。
  • 功能正确性:程序无报错跑完,输出三行精度与大小数据即视为闭环完成。

关键说明

  • 结构化剪枝实现为通道级置零并固化掩码,实际参数量未减少但权重稀疏化;如需真正减少计算量,需配合稀疏卷积后端或导出ONNX后处理。
  • 动态量化仅量化Linear层,Conv层保持FP32,因此精度损失较小。
  • MNIST训练2轮精度约99%左右,剪枝后微调1轮可恢复大部分精度损失。