
简介这份资源面向希望压缩 YOLOv8 模型体积、提升推理速度的算法工程师与深度学习学习者聚焦模型剪枝这一模型轻量化关键环节。包内集成多种主流剪枝策略包括 LAMP、Slimming、Group Slimming、Group Hessian、Taylor 以及 Regularization 剪枝等代码支持一键运行并配有 md 文档说明便于对照理解各方法的实现差异与适用场景。资源共 20 个文件以 9 个 py 脚本和 9 个 yaml 配置为主前者承载剪枝与通道可视化等核心逻辑后者提供多种骨干与颈部结构的模型配置另含压缩包与说明文档整体约 1.33MB目录结构清晰。目前已有 2863 人学习下载读者可借此快速搭建剪枝实验环境对比不同策略的压缩效果并在此基础上迁移到自有数据集与检测任务中适合具备一定 PyTorch 与目标检测基础的中高级开发者参考实践。1. 从一次 RK3588 部署翻车说起yolov8 剪枝源码到底解决什么问题去年帮一个做边缘盒子的团队把 yolov8s 往 RK3588 上搬模型转 RKNN 之后推理一帧要 180ms产线要求压到 60ms 以内。第一反应是换 yolov8n精度掉了 4 个点客户不认。后来走的路子是剪枝拿训练好的 yolov8s 权重按通道重要性砍掉一批卷积核再微调 20 个 epoch模型体积从 22MB 降到 9MBRKNN 上单帧 55msmAP 只掉 1.2 个点。这套流程里最费时间的不是训练而是找一份能直接跑、策略可切换、剪完还能正常导出 ONNX 的剪枝源码。网上搜「yolov8 剪枝源码」出来的东西大致三类一类是论文复现的 demo只剪 backbone 不剪 head剪完模型结构对不上一类是通用剪枝库套在 yolov8 上通道索引和 Detect head 的 anchor 对不齐导出直接报错还有一类是「集成多种剪枝策略」的整合包但依赖锁死、配置文件写死路径换自己的数据集就崩。这篇笔记就按我自己落地过的方案把 yolov8 剪枝源码里该有的东西拆开讲结构化剪枝和非结构化剪枝怎么选、L1 与 BN scale 两种重要性评估怎么接、剪枝率怎么定、剪完怎么微调、导出 ONNX 和 RKNN 时哪些层不能碰。适合已经能跑通 yolov8 训练、想往边缘设备压模型的同学也适合手里有剪枝源码但跑不通、想搞清每一步在干什么的人。2. 剪枝策略选型结构化与非结构化在 yolov8 上的真实差别2.1 为什么 yolov8 落地几乎只选结构化剪枝剪枝分两大类。非结构化剪枝非结构化剪枝 是热搜里常出现的词是把权重矩阵里绝对值小的元素置零掩码稀疏理论压缩率高但产出的是稀疏矩阵。问题在于通用 GPU 和绝大多数边缘 NPU 对稀疏矩阵没有原生加速你得靠稀疏推理库才能吃到收益。RK3588 的 NPU、Hi3516CV610 这类芯片跑的是稠密卷积稀疏权重进去还是按稠密算等于白剪。所以做嵌入式部署结构化剪枝是主力。结构化剪枝channel pruning / filter pruning直接删掉整个卷积核或整个通道输出特征图通道数真实减少模型结构变窄任何推理后端都能吃到加速。代价是精度掉得比非结构化明显必须靠微调补回来。yolov8 的 backbone 是 C2f 结构每个 C2f 里堆了多个 Bottleneck通道之间有残差和 concat 连接剪枝时如果只按单个卷积的重要性删很容易把 concat 两侧通道数剪得不一致模型直接跑不起来。这是 yolov8 剪枝源码里第一个必须处理的问题依赖感知的通道分组。常见做法是把有 concat、add 关系的层归到一个 groupgroup 内所有层的剪枝掩码必须一致要么一起剪要么一起留。源码里一般用一个channel_groups列表维护这种约束剪枝前先做一次图分析把 yolov8 的 neck 部分尤其是 Detect head 前的三个 concat标出来。2.2 L1 范数与 BN scale两种重要性评估怎么接进源码结构化剪枝的核心是「判断哪个通道不重要」。主流两种策略依据优点缺点适用L1-norm卷积核权重绝对值之和实现简单不依赖 BN对 BN 后的缩放不敏感快速 baselineBN scaleBatchNorm 的 gamma 系数与训练耦合精度更稳必须有 BN 层推荐默认组合L1 × BN gamma兼顾两者计算稍多精度敏感场景我一般默认用 BN scale因为 yolov8 每个 Conv 后面都跟 BNgamma 直接反映该通道对输出的贡献剪掉 gamma 接近 0 的通道精度损失最小。源码里对应的是一个 hook在model.train()或model.eval()之后遍历所有nn.BatchNorm2d取abs(bn.weight)作为该通道分数。下面是一段可直接抄的最小实现作用是把 yolov8 每个 BN 层的通道分数收集起来并按依赖分组做全局排序import torch import torch.nn as nn from ultralytics import YOLO def collect_bn_scores(model): 遍历模型收集每个 BN 层的通道重要性分数 scores {} for name, module in model.named_modules(): if isinstance(module, nn.BatchNorm2d): # gamma 绝对值即通道重要性detach 避免影响计算图 scores[name] module.weight.detach().abs().clone() return scores def build_prune_groups(model): 按 concat/add 依赖把通道分组组内必须同剪同留 groups [] # 简化示意实际需遍历 graph识别 C2f 与 Detect 前的 concat # 这里把每个 C2f 内的 Bottleneck 归为一组 for name, module in model.named_modules(): if module.__class__.__name__ C2f: groups.append([f{name}.cv1, f{name}.cv2, f{name}.m]) return groups if __name__ __main__: model YOLO(yolov8s.pt).model model.eval() bn_scores collect_bn_scores(model) groups build_prune_groups(model) print(f共 {len(bn_scores)} 个 BN 层{len(groups)} 个剪枝组)逻辑说明collect_bn_scores只取nn.BatchNorm2d的 weight不取 bias因为 gamma 才是缩放因子。build_prune_groups是简化版真实源码里要用torch.fx或 ultralytics 自带的model.model图结构做拓扑分析把 concat 两侧的 Conv 归到同一组。参数上detach()必须加否则后面做全局排序时会误建计算图显存直接爆。分组数量决定了剪枝的粒度组越粗剪枝越保守精度越稳但压缩率越低。提示如果你的源码里没有分组逻辑剪枝后导出 ONNX 报Concat维度不匹配八成就是这里漏了。3. 剪枝率怎么定全局阈值、逐层阈值与敏感度分析3.1 全局排序剪枝的最小可跑流程定剪枝率是剪枝里最玄学的部分。拍脑袋设 0.5backbone 可能没事head 直接崩。我一般分三步先做全局排序再按层设上限最后跑敏感度分析微调。全局排序的做法把所有 BN 层的 gamma 分数拼成一个大 tensor排序后取分位数低于阈值的通道标记为待剪。这样能保证整体剪枝率可控而不是每层都剪一样多。下面这段代码在上一节基础上补全剪枝掩码生成import torch def global_prune_mask(bn_scores, prune_ratio0.4): 全局排序返回每层保留通道的索引 all_scores torch.cat([v.flatten() for v in bn_scores.values()]) # 取分位数作为阈值prune_ratio0.4 表示剪掉 40% 通道 threshold torch.quantile(all_scores, prune_ratio) masks {} for name, score in bn_scores.items(): keep_idx torch.where(score threshold)[0] # 至少保留 8 个通道防止某层被剪空 if len(keep_idx) 8: keep_idx torch.topk(score, 8).indices.sort().values masks[name] keep_idx return masks def apply_mask_to_conv(conv, bn, keep_idx): 按保留索引裁剪 Conv 和 BN 的权重 idx keep_idx.to(conv.weight.device) conv.weight.data conv.weight.data[idx].clone() conv.out_channels len(idx) if conv.bias is not None: conv.bias.data conv.bias.data[idx].clone() bn.weight.data bn.weight.data[idx].clone() bn.bias.data bn.bias.data[idx].clone() bn.running_mean.data bn.running_mean.data[idx].clone() bn.running_var.data bn.running_var.data[idx].clone() bn.num_features len(idx)逻辑说明global_prune_mask用torch.quantile取全局分位数prune_ratio0.4意味着分数最低的 40% 通道被剪。keep_idx强制至少保留 8 通道这是血泪经验——某些深层 BN 的 gamma 整体偏小全局阈值一卡就全剪没了模型直接输出 NaN。apply_mask_to_conv里 BN 的running_mean、running_var必须同步裁剪否则推理时统计量对不上精度崩得莫名其妙。参数说明prune_ratio是全局剪枝率不是每层剪枝率。实际每层剪多少由分数分布决定浅层通常剪得少深层剪得多。min_channels8是经验值小模型可以设 4大模型设 16。3.2 逐层上限与敏感度分析别让某一层被剪穿全局排序有个坑如果某一层 gamma 整体偏小会被剪掉一大半而这一层恰好是 Detect head 前的关键层精度直接崩。所以源码里一般再加一层逐层上限比如每层最多剪 60%。更稳的做法是敏感度分析逐层单独剪 10%、20%、30%看 mAP 掉多少掉得多的层降低剪枝率。层位置建议最大剪枝率理由backbone 浅层30%纹理信息密集剪多掉点快backbone 深层50%语义信息冗余度高neck C2f40%有 concat 依赖需分组约束Detect head20%直接决定输出最敏感敏感度分析不用跑完整训练剪完直接 eval 一次验证集看 mAP50 掉幅。掉超过 3 个点的层把该层剪枝率砍半。这一步在源码里通常是一个sensitivity_analysis.py脚本遍历每层跑一次前向耗时大概十几分钟比盲目剪完再微调省事得多。注意敏感度分析必须在model.eval()下做且要关掉 BN 的 running stats 更新否则每跑一次统计量都在变结果不可比。4. 剪枝后微调学习率、冻结策略与精度回补4.1 微调不是重训学习率与 epoch 的取值逻辑剪枝完的模型精度一般掉 3 到 8 个点必须微调回补。微调不是从头训学习率要小一般设原始训练的 1/10 到 1/20。yolov8 原始训练 lr00.01微调我一般用 0.001 到 0.0005cosine 衰减epoch 20 到 50 足够。数据增强要关掉 Mosaic 和 MixUp因为剪枝后模型容量变小强增强反而拖慢收敛。from ultralytics import YOLO # 加载剪枝后的模型结构需先把剪枝后的 state_dict 存成 pt model YOLO(yolov8s_pruned.yaml).load(yolov8s_pruned.pt) model.train( datacoco128.yaml, epochs30, imgsz640, lr00.001, # 原始 1/10 lrf0.01, # 最终 lr lr0 * lrf warmup_epochs0, # 微调不需要 warmup mosaic0.0, # 关掉 Mosaic mixup0.0, close_mosaic0, optimizerSGD, freeze10, # 冻结前 10 层先让 head 适应 )逻辑说明freeze10冻结 backbone 前 10 层先让 neck 和 head 适应新的通道数跑 5 个 epoch 后再解冻全部。mosaic0.0是关键剪枝后模型对拼接边界敏感Mosaic 的随机裁剪会让精度震荡。warmup_epochs0因为权重已经预训练过不需要再 warmup。参数说明lr0太大比如 0.01会让剪枝后的通道权重被冲乱精度先掉再涨浪费 epoch。freeze层数按模型深度调yolov8s 用 10yolov8m 用 12。epochs不用多30 个 epoch 后 mAP 基本稳定再训收益很小。4.2 精度回补的验证方法别只看 mAP50微调完不能只看 mAP50要同时看 mAP50-95 和每类 AP。剪枝容易让某些小目标类掉点严重因为浅层通道被剪后小目标特征丢失。验证时跑一次model.val()把per_class结果导出来对比剪枝前。metrics model.val(datacoco128.yaml, splitval) print(metrics.box.map) # mAP50-95 print(metrics.box.map50) # mAP50 print(metrics.box.maps) # 每类 AP如果某类 AP 掉超过 5 个点说明该类依赖的通道被剪多了回去把对应层的剪枝率调低重新剪再微调。这个迭代一般两三轮能收敛。RK3588 部署时还要注意剪枝后的模型导出 ONNX 要用opset12RKNN 工具链对 opset 13 以上的某些算子支持不全导出后转 RKNN 容易报Unsupported op。5. 避坑与排查剪枝源码跑不通的 5 个高频问题5.1 导出 ONNX 报 Concat 维度不匹配现象剪枝后model.export(formatonnx)报Concat输入维度不一致或者转 RKNN 时Add算子 shape 对不上。原因concat 两侧的 Conv 没有归到同一剪枝组各自按自己的分数剪了不同数量的通道concat 时维度对不上。yolov8 的 neck 有三个 concat分别接 backbone 的 P3、P4、P5最容易出问题。解决在剪枝前用torch.fx做一次图追踪把所有 concat 的输入节点找出来强制归到同一组组内用统一的 keep_idx。源码里一般有一个fuse_concat_groups()函数剪枝前先跑一遍。5.2 剪枝后模型输出全 NaN现象剪枝完直接 eval输出全是 NaNloss 也是 NaN。原因某一层被剪空或者 BN 的running_var被剪后出现 0除零导致 NaN。全局阈值卡太狠时深层 BN 的 gamma 整体偏小容易被全剪。解决global_prune_mask里加min_channels保护每层至少留 8 个通道。另外检查 BN 的running_var如果有 0 值加一个eps1e-5兜底。剪枝后先跑一次前向确认无 NaN 再进微调。5.3 微调精度不升反降现象微调 30 个 epochmAP 比剪枝后还低。原因学习率太大或者数据增强没关。剪枝后模型容量小大学习率会把预训练权重冲乱Mosaic 的随机拼接让模型学不到稳定特征。解决lr0降到 0.0005关掉 Mosaic 和 MixUpfreeze前 10 层先跑 5 个 epoch。如果还不升检查数据集标注是否有问题剪枝会放大标注噪声的影响。5.4 RKNN 转换报 Unsupported op现象ONNX 转 RKNN 时提示某个算子不支持常见的是SiLU或Split。原因剪枝后某些 Conv 的通道数变成非 8 的倍数RKNN 对通道对齐有要求。或者导出 opset 版本太高。解决剪枝时把每层保留通道数对齐到 8 的倍数keep_idx取完后做一次len(idx) // 8 * 8。导出用opset12。如果还报错把SiLU换成ReLU再微调几个 epoch精度掉得不多但兼容性好很多。5.5 剪枝率设了但模型体积没变现象prune_ratio0.5剪完保存的 pt 文件大小几乎没变。原因只改了out_channels属性没有真正重建模型结构。PyTorch 的state_dict里权重 tensor 还是原来的 shape保存时按原 shape 存体积自然不变。解决剪枝后必须用新的通道数重建模型重新实例化YOLO(yaml)再把裁剪后的权重 load 进去。或者用torch.nn.utils.prune的remove做永久裁剪。源码里一般有一个rebuild_model()步骤不能省。6. 进阶技巧把剪枝率搜索做成自动化流水线手动调剪枝率太慢我后来把整个流程包成了一个搜索脚本给定目标推理延迟比如 RK3588 上 60ms自动搜剪枝率。思路是二分搜索——先试 0.3转 RKNN 测延迟不够就加到 0.4超了就降到 0.35每轮微调 10 个 epoch 看 mAP 是否达标。一般 4 到 5 轮能收敛到目标。import subprocess def search_prune_ratio(target_latency60, min_map0.35): lo, hi 0.2, 0.6 best None for _ in range(5): mid (lo hi) / 2 # 剪枝 微调 导出 测延迟这里用子进程调各步骤脚本 subprocess.run([python, prune.py, --ratio, str(mid)]) subprocess.run([python, finetune.py, --epochs, 10]) latency measure_latency(yolov8s_pruned.rknn) mAP evaluate_map(yolov8s_pruned.pt) if latency target_latency and mAP min_map: best mid lo mid # 还能再剪 else: hi mid # 剪过头了 return best逻辑说明二分搜索的边界lo0.2、hi0.6是经验范围低于 0.2 压缩收益不明显高于 0.6 精度基本救不回来。每轮微调只跑 10 个 epoch 是为了快速筛最终选中的剪枝率再跑完整 30 epoch。measure_latency要在真实板子上测PC 上模拟的延迟和 RK3588 差很多。参数说明target_latency按产线要求设min_map按客户能接受的精度下限设。搜索轮数 5 轮足够再多边际收益很低。这套流水线跑一轮大概 2 小时比手动试快得多。最后说个习惯我每次剪枝前都会把原始模型的 mAP、每类 AP、推理延迟存一份 baseline剪枝后逐项对比掉点超过阈值的类单独回去查是哪层剪多了。剪枝这事没有后悔药剪错了只能重训所以敏感度分析和分组约束这两步千万别省。希望帮到你。本文还有配套的精品资源点击获取