2. 基于L1范数的非结构化剪枝

任务目标

对预训练模型执行基于L1范数的非结构化剪枝:按权重值大小排序,移除最小权重的连接,使权重矩阵变为稀疏。通过比较不同剪枝率下的实际稀疏度与模型精度,验证剪枝率对模型性能的影响。

环境依赖

Python 3.8+,PyTorch 2.0+,torchvision,NumPy。数据集使用CIFAR-10,模型使用预训练的ResNet-18(torchvision.models.resnet18(pretrained=True))。

核心实现

import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

# ---------- 1. 数据加载 ----------
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = DataLoader(testset, batch_size=256, shuffle=False, num_workers=2)

# ---------- 2. 模型与评估 ----------
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = torchvision.models.resnet18(pretrained=True).to(device)
model.eval()

def evaluate(model, loader):
    correct = 0
    total = 0
    with torch.no_grad():
        for images, labels in loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
    return 100.0 * correct / total

# ---------- 3. L1范数非结构化剪枝 ----------
def prune_l1(model, prune_ratio):
    """按L1范数(值)剪枝:将值最小的prune_ratio比例权重置零"""
    for name, param in model.named_parameters():
        if 'weight' not in name:
            continue
        weight = param.data.view(-1)
        k = int(weight.numel() * prune_ratio)
        if k == 0:
            continue
        # 找到值最小的k个权重的索引
        _, indices = torch.topk(weight.abs(), k, largest=False)
        # 将对应权重置零
        mask = torch.ones_like(weight, dtype=torch.bool)
        mask[indices] = False
        param.data.mul_(mask.view(param.data.shape))

def compute_sparsity(model):
    """计算模型整体稀疏度(零权重占比)"""
    total_zeros = 0
    total_params = 0
    for name, param in model.named_parameters():
        if 'weight' in name:
            total_zeros += (param.data == 0).sum().item()
            total_params += param.data.numel()
    return 100.0 * total_zeros / total_params

# ---------- 4. 不同剪枝率对比 ----------
prune_ratios = [0.0, 0.3, 0.5, 0.7, 0.9]
print(f"{'目标剪枝率':<10} {'实际稀疏度':<12} {'精度(%)':<10}")
for ratio in prune_ratios:
    # 每次从原始预训练模型重新开始,避免累积剪枝
    model = torchvision.models.resnet18(pretrained=True).to(device)
    model.eval()
    if ratio > 0:
        prune_l1(model, ratio)
    sparsity = compute_sparsity(model)
    acc = evaluate(model, testloader)
    print(f"{ratio:<10.2f} {sparsity:<12.2f} {acc:<10.2f}")

运行方式

  1. 安装依赖:pip install torch torchvision numpy
  2. 将上述代码保存为 l1_pruning.py,直接运行:python l1_pruning.py
  3. 首次运行会自动下载CIFAR-10测试集(约163MB)和ResNet-18预训练权重。

验证方法与判定标准

程序输出5行结果,每行包含目标剪枝率、实际稀疏度和精度。判定通过的准则:

  • 稀疏度匹配:实际稀疏度与目标剪枝率基本一致(误差<1%),证明剪枝逻辑正确。
  • 精度趋势:剪枝率0%时精度约为85%(ResNet-18在CIFAR-10上的正常水平);剪枝率30%时精度下降不超过2%;剪枝率50%时精度下降不超过5%;剪枝率90%时精度显著下降(通常低于50%)。若精度变化符合此趋势,说明L1剪枝有效且评估流程正确。
  • 稀疏度单调性:剪枝率从0%到90%,实际稀疏度严格递增。

关键说明

  • 剪枝对象仅限卷积层和全连接层的weight参数,不处理bias和BN层参数,这是非结构化剪枝的标准做法。
  • 每次剪枝都从原始预训练模型重新加载,避免不同剪枝率之间的累积干扰。
  • 本案例只验证剪枝后的精度,不包含微调(fine-tuning)步骤。实际工程中剪枝后通常需要微调恢复精度,但本案例目标仅是比较剪枝率的影响,因此不做微调。