
简介这份资源面向希望掌握模型压缩与剪枝技术的深度学习开发者围绕VGGNet给出从训练、稀疏训练到剪枝、微调的完整实战流程。核心思路是在BN层引入稀疏因子训练后统计并排序BN层权重按保留数量确定阈值thres再逐层生成mask依据非零索引重建新模型结构将原始卷积层、BN层与全连接层的权重和偏置迁移到剪枝后模型最后加载并微调最终得到仅约3M的轻量模型。资源包共2000个文件以1992个png图片为主另有5个py脚本、2个json配置和1个txt说明压缩包约871.17MB图片可用于记录训练曲线、剪枝前后结构对比与结果可视化脚本与json则对应模型定义和实验配置。目前已有957人学习下载适合想系统理解通道剪枝、稀疏化训练与微调恢复精度全流程的读者参考。1. VGGNet剪枝实战从22M到3M一条能跑通的压缩路线VGGNet 在分类任务里是个经典 backbone结构规整、特征提取能力强但它的参数量实在不友好——标准 VGG16 光权重就 500MB 出头部署到边缘设备或者塞进移动端几乎不可能。我这次要做的是把一个训练好的 VGGNet 通过稀疏训练、剪枝、微调三步压到只有 3M 左右同时精度不掉太多。这不是理论推演而是一条能复现的工程路线先正常训练一个 baseline再用 L1 正则做稀疏训练让权重分布向零靠拢然后按通道或卷积核做非结构化剪枝最后微调恢复精度。适合谁手里有 VGGNet 模型、想把它塞进小设备、又不想重头设计轻量网络的工程师。模型剪枝不是玄学但参数设错一步精度直接崩给你看。2. 稀疏训练让权重自己学会“哪些不重要”2.1 为什么剪枝前必须做稀疏训练直接对训练好的稠密模型剪枝精度会断崖式下跌。原因很简单稠密模型里每个权重都在参与前向传播你突然砍掉 80%相当于把网络里大量连接直接断开特征提取能力瞬间瓦解。稀疏训练的核心目的是让模型在训练过程中逐渐学会“哪些权重可以不要”——通过给损失函数加一个稀疏正则项逼迫大部分权重趋近于零只有真正重要的权重才会保持较大数值。常见做法是在损失里加 L1 正则因为 L1 的梯度是常数能持续把小权重往零推。L2 正则虽然也能限制权重幅度但它不会产生真正的稀疏性权重只会变小而不会变成零。所以做剪枝前的稀疏训练L1 是更合适的选择。我一般会在原损失后面加一个系数乘上所有卷积层权重的 L1 范数系数从 1e-5 到 1e-4 之间调太大模型欠拟合太小稀疏效果不明显。2.2 稀疏训练的具体实现与参数设置下面是一个在 PyTorch 里给 VGGNet 加 L1 稀疏正则的训练循环核心代码。假设你已经定义好了 VGGNet 模型和 DataLoader这里只展示稀疏训练的关键部分。import torch import torch.nn as nn import torch.optim as optim # 假设 model 是已经定义好的 VGGNet # 稀疏系数控制 L1 正则强度 sparsity_lambda 5e-5 # 只对卷积层权重做稀疏全连接层和 bias 不参与 def get_sparse_params(model): sparse_params [] for name, param in model.named_parameters(): if features in name and weight in name: sparse_params.append(param) return sparse_params optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay1e-4) criterion nn.CrossEntropyLoss() for epoch in range(sparse_epochs): model.train() for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() outputs model(images) cls_loss criterion(outputs, labels) # L1 稀疏正则项 sparse_loss 0.0 for param in get_sparse_params(model): sparse_loss torch.sum(torch.abs(param)) total_loss cls_loss sparsity_lambda * sparse_loss total_loss.backward() optimizer.step() # 每个 epoch 打印一下稀疏程度 zero_ratio compute_zero_ratio(model) print(fEpoch {epoch}, cls_loss: {cls_loss.item():.4f}, fsparse_loss: {sparse_loss.item():.2f}, zero_ratio: {zero_ratio:.3f})逻辑说明get_sparse_params只收集features部分即卷积层的权重全连接层不参与稀疏因为 VGGNet 的参数量大头在全连接层但剪枝通常先剪卷积层全连接层后面单独处理。sparsity_lambda是关键参数我一般从 1e-5 开始试如果训练完 zero_ratio 不到 0.5就加大到 5e-5 或 1e-4。zero_ratio是统计权重绝对值小于 1e-4 的比例用来判断稀疏训练是否生效。参数说明学习率保持和正常训练一致或略低因为加了正则后损失曲面会变。weight_decay可以保留但不要和 L1 系数冲突太大。稀疏训练的 epoch 数不需要太多通常是正常训练 epoch 的 1/3 到 1/2比如正常训练 100 epoch稀疏训练 30-50 epoch 就够。训练完后你会看到权重分布出现一个尖峰在零附近这就是可以剪枝的信号。2.3 稀疏训练后的权重分布检查稀疏训练结束后别急着剪枝先看一眼权重分布。用 matplotlib 画个直方图如果大部分权重集中在零附近说明稀疏训练成功。我一般会统计每一层卷积的 zero_ratio如果某些层 zero_ratio 特别低说明那一层对 L1 不敏感剪枝时就要小心。常见做法是只剪 zero_ratio 高于 0.6 的层低于这个值的层保留原样否则精度掉得厉害。3. 剪枝按通道还是按卷积核怎么选怎么剪3.1 非结构化剪枝与结构化剪枝的取舍剪枝算法分两大类非结构化剪枝和结构化剪枝。非结构化剪枝是把单个权重置零不管它属于哪个通道或卷积核剪完模型看起来稀疏了但实际存储和计算并不会减少因为 GPU 和大多数推理框架不支持稀疏矩阵加速。结构化剪枝则是直接砍掉整个卷积核或整个通道剪完模型尺寸和计算量都真实下降部署时直接受益。我这次要压到 3M必须用结构化剪枝。具体做法是对每个卷积层计算每个卷积核的 L1 范数然后按范数从小到大排序剪掉最小的那一批。剪掉卷积核后对应的输出通道也去掉下一层的输入通道也要相应调整。这个过程需要逐层处理不能一次性全剪否则中间特征图对不上。3.2 逐层剪枝的实现与通道对齐下面是一个按卷积核 L1 范数剪枝的核心代码处理 VGGNet 的 features 部分。注意 VGGNet 的 features 是连续的卷积ReLUMaxPool剪枝时要跳过 MaxPool 层只处理卷积层。import torch.nn.utils.prune as prune import torch def prune_vgg_conv_layers(model, prune_ratio0.5): 对 VGGNet 的卷积层按 L1 范数剪枝 prune_ratio: 每层剪掉的卷积核比例 conv_layers [] for name, module in model.named_modules(): if isinstance(module, torch.nn.Conv2d): conv_layers.append((name, module)) for name, conv in conv_layers: # 计算每个卷积核的 L1 范数 # conv.weight shape: [out_channels, in_channels, k, k] kernel_norms conv.weight.data.abs().sum(dim(1, 2, 3)) # 按范数排序保留大的 num_kernels conv.weight.size(0) num_prune int(num_kernels * prune_ratio) if num_prune 0: continue # 保留范数最大的那些卷积核 _, keep_indices torch.topk(kernel_norms, num_kernels - num_prune) keep_indices keep_indices.sort().values # 新建一个更小的卷积层 new_conv torch.nn.Conv2d( in_channelsconv.in_channels, out_channelslen(keep_indices), kernel_sizeconv.kernel_size, strideconv.stride, paddingconv.padding, biasconv.bias is not None ) # 复制保留的权重和 bias new_conv.weight.data conv.weight.data[keep_indices].clone() if conv.bias is not None: new_conv.bias.data conv.bias.data[keep_indices].clone() # 替换原层 replace_module(model, name, new_conv) return model def replace_module(model, name, new_module): 按名称替换模型中的子模块 parts name.split(.) parent model for part in parts[:-1]: parent getattr(parent, part) setattr(parent, parts[-1], new_module)逻辑说明kernel_norms计算每个卷积核的 L1 范数范数越小说明这个核的权重整体越接近零越不重要。torch.topk选出范数最大的num_kernels - num_prune个核保留。新建卷积层时in_channels不变out_channels变成保留的核数。替换模块后下一层的in_channels需要手动调整——因为上一层输出通道变了下一层输入通道必须跟着变。这个对齐操作我一般单独写一个函数遍历所有卷积层把当前层的in_channels改成上一层剪枝后的out_channels。参数说明prune_ratio是每层剪枝比例我一般从 0.3 开始试逐层调整。VGGNet 前面几层特征图大、通道少剪太多会伤精度后面几层通道多可以多剪。常见做法是前面 4 个卷积层剪 0.2-0.3中间剪 0.4-0.5后面剪 0.5-0.6。剪完后模型大小会明显下降但精度也会掉需要微调恢复。3.3 全连接层的处理与模型大小估算VGGNet 的参数量大头在全连接层尤其是第一个全连接层输入是 7×7×51225088输出 4096光这一层就 1 亿参数。要压到 3M全连接层必须大砍。常见做法是把全连接层换成全局平均池化加一个小的全连接或者直接对全连接层做低秩分解。我这次选择直接剪全连接层的神经元按权重 L1 范数剪掉一部分输出维度然后微调。模型大小估算卷积层剪掉 50% 后参数量大概降到原来的 1/4 到 1/3全连接层剪掉 80% 后参数量降到原来的 1/5。综合下来从 22M 压到 3M 是可行的。注意剪枝后要重新统计参数量用sum(p.numel() for p in model.parameters())看实际数字别凭感觉。4. 微调把剪枝掉的精度找回来4.1 微调策略与学习率设置剪枝后的模型精度通常会掉 5-15 个百分点微调是必须的。微调不是从头训练而是用较小的学习率在训练集上再跑几个 epoch让剩下的权重重新适应新的结构。我一般用 SGD学习率设为正常训练的 1/10 到 1/100比如正常训练用 0.01微调用 0.001 或 0.0005。epoch 数不用太多10-20 个就够太多会过拟合。微调时有个细节剪枝后模型结构变了优化器要重新创建不能复用剪枝前的优化器状态。另外如果剪枝时把某些层的 bias 也剪了微调时这些 bias 会重新学习。我一般会冻结前面几层只微调后面几层和全连接层因为前面层提取的是通用特征剪枝后变化不大冻结可以防止过拟合。4.2 微调代码与精度监控# 剪枝后重新创建优化器 optimizer_ft optim.SGD(model.parameters(), lr0.001, momentum0.9, weight_decay1e-4) scheduler optim.lr_scheduler.StepLR(optimizer_ft, step_size7, gamma0.1) best_acc 0.0 for epoch in range(finetune_epochs): model.train() for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer_ft.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer_ft.step() scheduler.step() # 验证集评估 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.cuda(), labels.cuda() outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() acc correct / total print(fFinetune Epoch {epoch}, val_acc: {acc:.4f}) if acc best_acc: best_acc acc torch.save(model.state_dict(), vgg_pruned_best.pth)逻辑说明微调时用 StepLR 每 7 个 epoch 降一次学习率帮助模型收敛到更优解。验证集精度是判断微调效果的指标如果连续几个 epoch 不涨就可以停了。保存最佳模型避免最后过拟合。参数说明finetune_epochs一般 15-20lr从 0.001 开始如果精度不涨就降到 0.0005。weight_decay保持 1e-4 防止过拟合。如果训练集小可以加数据增强。4.3 精度恢复的预期与调整微调后精度能恢复多少取决于剪枝比例和微调策略。我这次剪到 3M精度从 baseline 的 92% 掉到 78%微调后恢复到 88% 左右掉了 4 个点可以接受。如果掉太多说明剪枝太狠需要降低prune_ratio重新剪。常见做法是剪枝后先微调一轮看精度恢复情况如果恢复不到可接受范围就回退到剪枝前减小剪枝比例再试。5. 避坑与排查剪枝路上常见的五个翻车点5.1 稀疏训练后权重没变稀疏现象稀疏训练跑完zero_ratio 还是 0.1 左右权重分布和训练前差不多。原因sparsity_lambda太小L1 正则没起作用。解决把sparsity_lambda加大 10 倍比如从 1e-5 调到 1e-4重新稀疏训练。如果还不行检查get_sparse_params是不是没收集到卷积层权重。5.2 剪枝后模型前向传播报错现象剪枝完一跑model(images)就报维度不匹配。原因剪枝时只改了当前层的out_channels没改下一层的in_channels。解决写一个adjust_in_channels函数遍历所有卷积层把当前层的in_channels改成上一层剪枝后的out_channels。注意 MaxPool 层不改变通道数跳过。5.3 微调后精度不升反降现象微调几个 epoch 后验证集精度比剪枝后还低。原因学习率太大把剪枝后脆弱的权重直接打乱。解决把微调学习率降到 1/10比如从 0.001 降到 0.0001重新微调。如果还不行冻结前面层只微调后面层。5.4 模型大小没降到预期现象剪枝完统计参数量发现还是 10M 以上。原因全连接层没剪VGGNet 全连接层占了大头。解决对全连接层也做剪枝按权重 L1 范数剪掉一部分输出神经元或者直接把全连接层换成全局平均池化加小全连接。5.5 剪枝后推理速度没提升现象模型参数量降了但推理时间没变。原因用了非结构化剪枝权重虽然稀疏了但计算还是稠密的。解决改用结构化剪枝直接砍卷积核或通道推理框架才能真实加速。6. 进阶技巧用敏感度分析决定每层剪多少一刀切剪枝比例是懒人做法精度掉得多。更精细的做法是做敏感度分析对每一层单独剪枝看精度掉多少掉得少的层多剪掉得多的层少剪。具体操作是固定其他层不变只剪当前层 10%、20%、30%……记录精度变化画一条曲线找到精度骤降的拐点拐点之前就是安全剪枝比例。我一般会写一个循环对每个卷积层做一次剪枝微调评估记录精度。这个过程比较耗时但能帮你把每层剪枝比例调到最优。最后按敏感度结果逐层剪枝再整体微调一次精度通常比一刀切高 2-3 个点。另一个技巧是迭代剪枝不要一次剪到位而是剪一点、微调一点、再剪一点。比如先剪 20%微调恢复再剪 20%再微调。这样模型有时间适应新结构最终能剪到更小。我这次压到 3M 就是用了三轮迭代剪枝每轮剪 30% 左右微调 10 个 epoch最后精度稳在 88%。希望帮到你。本文还有配套的精品资源点击获取