资讯动态

模型剪枝精度恢复难?ReCal 在剪前校准,让压缩不再开盲盒

发布时间:2026/10/15 22:12:11 来源:尧图企业网站定制
我最早接触模型剪枝那会儿最烦的其实不是剪完精度掉多少而是剪完以后的恢复像开盲盒。辛辛苦苦把模型压到一半大小结果精度直接掉了七八个点然后大家习惯性上 OPD——也就是 one-pass pruning 之后再紧接蒸馏微调这类先剪后补的路子指望靠训练阶段的补救把精度拉回来。但我在自己折腾过几次之后越来越觉得OPD 这套逻辑有个天生的短板它在剪完之后才开始处理剪坏的地方而剪枝那一刻模型已经处于一个很难回去的状态了。所以后来我在自己项目里尝试把恢复准备挪到剪枝之前走了一套我暂时叫它ReCalRecovery-Calibrated Pruning的流程效果比我预想的好很多。这篇文章不聊花里胡哨的概念只讲清楚三件事为什么事后补救有天花板ReCal 在剪枝前到底做了什么以及我自己复现这套路线时踩过的坑和调参笔记。如果你手头也有模型压缩、端侧部署、或者单纯对剪枝后精度恢复头疼的需求这篇应该能给你一些可以直接抄走的思路。小白也不用慌涉及二阶近似、Fisher 信息这些词的地方我会尽量用大白话拆开讲。1. 先剪后补的旧路线OPD 式恢复为什么总差一口气1.1 OPD 的本质剪完再回血先厘清一下概念。标题里的 OPD我指的是模型剪枝里常见的One-Pass Pruning 配合知识蒸馏的路线核心流程大致是这样先拿训练好的稠密大模型做通道剪枝把不重要的通道删掉删完之后的稀疏模型精度必然受损于是再让这个受伤的模型去模仿原始大模型的输出通过蒸馏微调让精度慢慢爬回来。这个思路听起来没毛病实操里也确实能挽回不少精度。我最早接触的时候甚至觉得它挺优雅——不需要重新训练整个大模型只用一个较小的微调成本就能把压缩后的模型救活。尤其当你手里已经有一个训练充分的大模型时OPD 几乎是成本最低的恢复方案。文献里也经常把它归类为无需从头训练的压缩手段很多部署场景都默认走这条路。但问题在于这套流程的两个阶段是割裂的剪枝阶段只关心哪些通道不重要恢复阶段只关心如何在当前稀疏结构下重新拟合。剪枝阶段没有为恢复阶段留任何余地恢复阶段也没有能力改变剪枝阶段已经造成的结构性缺失。说得直白一点OPD 是在模型已经被切掉一条腿之后再教它如何用一条腿走路。能走但很难跑。1.2 事后补救的天花板来自哪里要理解为什么 OPD 总有上限得先看剪枝那一瞬间发生了什么。拿通道剪枝举例当你把某个通道的权重置零或者从结构上移除后续所有层的前向计算结果都会变。尤其是配合 BN批量归一化的模型BN 层里存的统计量和缩放参数是基于完整通道分布算出来的通道一删这些统计量立刻失真。接下来就是连锁反应幸存下来的通道本来是在有队友帮忙的前提下学到的特征表达现在队友没了它们需要独立承担起原本由多条通路协同完成的功能。如果幸存通道之间的相关性很强、冗余度低那它们短时间内根本扛不住这种一个顶三个的活儿。演化到 loss landscape 的语言来描述剪枝相当于把模型参数从原来那个精度很高的盆地硬生生推到了一个悬崖边上。OPD 的蒸馏微调本质上是在这个悬崖边附近找一个新的局部极小但它缺少一个关键能力——翻回原来那个盆地。因为剪掉的结构不会再回来模型能走的路已经被限定死了。这也是为什么高稀疏率下 OPD 的挽回效果会断崖式下降。稀疏度 30% 的时候幸存通路勉强还能靠微调重新组织功能损失可能只有一两个点到了 60% 甚至 70% 稀疏度你复用的是同一套蒸馏逻辑却会发现怎么调学习率、怎么拉长训练轮数精度都回不到理想水位。因为它不是训练不充分的问题而是结构层面的信息丢失已经无法靠后训练弥补了。1.3 剪枝前的状态其实早就决定了恢复上限我在反复对比不同恢复方案时逐渐意识到一个道理剪完之后的恢复难度在剪枝之前就已经注定了。就像搬家前拆家具如果拆的时候没做任何标记搬完再拼装就是一场噩梦但如果你在拆之前就知道哪些零件后面要单独用提前把它们的位置和连接关系理清楚拼装就会顺畅很多。对应到模型上就是说被保留下来的那部分通路在剪枝之前就应该具备一定的独立表达能力。如果模型在完整训练阶段是高度耦合的所有通道都在互相依赖那无论事后用什么办法补救都会很费劲。反过来如果剪枝前我们能识别出哪些通道被剪掉之后最难恢复并针对性地做一点准备让幸存通路提前适应没有队友的状态那么剪枝那一刀下去模型其实已经处在离新盆地比较近的位置了。这就是 ReCal 的核心出发点把恢复准备从剪枝后提到剪枝前。2. ReCal 提前布局的三个关键动作2.1 剪枝预演在剪之前模拟剪后世界ReCal 的第一件事是先在脑子里把剪完的样子过一遍。具体做法很简单对每个候选通道做一次假剪枝——不是真正把结构删掉而是在前向计算时临时用一个置零掩码把该通道的输出屏蔽掉然后看 loss 会上升多少。这种做法有点类似 dropout但目的完全不同dropout 是训练期的随机扰动是为了增强泛化这里则是定向、逐个地模拟真实剪枝动作只为了观测模型对每一个具体通道缺失的敏感程度。我自己做的时候会把每个通道的剪枝即时损失记录下来形成一张表。但光看即时损失还不够因为有些通道虽然被剪的瞬间损失不大后面怎么微调都恢复不回来有些通道剪完瞬间波动很小而且很容易恢复。也就是说剪枝的即时伤害和长期恢复难度是两回事两者都要纳入评估。所以我在预演环节会加一步短期再训练观察对某个通道置零之后不启动完整微调只跑极少步数的梯度更新看 loss 能恢复多少、恢复到什么速度。这个信息很有价值——它类似于给每个通道做一个恢复潜力测试。恢复潜力低的通道剪枝决策时要尽量放行恢复潜力高的通道哪怕瞬时损失大一点也可以纳入剪除候选。2.2 恢复敏感度评分从谁重要到谁剪了难恢复传统剪枝的重要性判断基本围绕着谁对当前预测更重要来展开。常用的指标有 L1 范数、BN 缩放因子、泰勒一阶展开、梯度积分等等。这些指标本质上回答的是同一个问题移除哪个参数对当前 loss 影响最小。但在 ReCal 的视角下这个问题应该换成移除哪个参数之后模型最难恢复。这两个问题的答案并不总是一致。我举个例子某个通道的连接权重绝对值很小看起来删掉它无所谓但它在反向传播中可能承担着关键的梯度通路角色删掉它会让后面所有层的学习信号都变弱。按传统指标它会被剪掉实际上剪完之后模型怎么调都调不动。反过来有些通道权重很大但它所在的通路本身就存在高度冗余删掉之后其他通道迅速顶上恢复起来毫不费力。ReCal 的评分器会把这两类信息综合起来。我自己的做法是基于 Fisher 信息的对角估计加上预演中的短期恢复速度合成一个恢复敏感度分数分数越高的通道意味着剪了之后越难回来要优先保留。这里先不展开公式第三章我会把落地细节写清楚。2.3 恢复预备适配让幸存通路提前吃到蒸馏信号第三个关键动作也是 ReCal 里最有超前味道的一步在剪枝真正发生之前启动一轮蒸馏式的预备适配。做法听起来不复杂——固定住原始大模型的参数让待剪枝的学生模型去模仿大模型的软标签输出同时把一部分校准数据过一遍让 BN 统计量逐渐调整到更接近剪后分布的状态。为什么要在剪枝前做蒸馏因为剪枝后的模型本质上是一个结构更小的子网络它需要学习的目标不应该只是硬标签而应该是大模型经过完整训练后沉淀下来的知识分布。如果这段蒸馏信号留到剪枝完成之后再给那模型已经受伤了吸收能力大打折扣但如果提前在剪枝前就把蒸馏信号注入到那些注定要幸存的通路里这些通路就会提前开始学习独立表达的技能。这一步我通常安排在预演评分之后、真正执行剪枝之前。它会带来一个很有意思的副产物经过预备适配的模型剪枝完成后的初始精度会比直接从原始大模型一刀切下来的初始精度高不少后续微调的起点也更高收敛自然更快。当然这一步需要额外的计算开销后文我讲适用边界的时候再细说。3. 实操落地恢复敏感度评分器怎么设计3.1 用 Fisher 对角估计恢复难度先说数学直觉尽量不绕弯子。Fisher 信息矩阵在优化里经常被用来衡量参数对 loss 的影响程度它的对角项可以近似理解为这个参数微调一点点会引发 loss 多大的波动。对每个通道来说我们把这个通道包含的所有参数的 Fisher 对角值加起来就可以作为通道层面的敏感度。实际计算的时候不需要真的求二阶导因为 Fisher 对角项有一个等价近似在一个数据样本上计算每个参数梯度的平方。也就是说你只需要跑一遍常规反向传播收集梯度然后取平方求均值就能得到 Fisher 对角估计。这在工程上极其友好完全没有 Hessian 矩阵那种存储爆炸的烦恼。代码大概长这样def compute_fisher_diag(model, loader, num_samples1024): model.eval() fisher {} for name, param in model.named_parameters(): fisher[name] torch.zeros_like(param) count 0 for inputs, _ in loader: if count num_samples: break inputs inputs.cuda() outputs model(inputs) # 以交叉熵为例采样 logits 计算梯度 probs torch.softmax(outputs, dim-1) log_probs torch.log_softmax(outputs, dim-1) loss -(probs * log_probs).sum(dim-1).mean() model.zero_grad() loss.backward() for name, param in model.named_parameters(): if param.grad is not None: fisher[name] param.grad.detach() ** 2 count inputs.size(0) for name, param in model.named_parameters(): fisher[name] / max(1, count) return fisher这个实现里有两个经验点。第一loss 用的是预测分布自身的负对数似然期望而不是标准交叉熵因为这样算出来更贴近 Fisher 的定义。第二num_samples不要省我最低建议 1024 个样本低于这个量级评分噪声会大到你根本分不清通道之间的差别。采样样本要随机打乱最好固定随机种子保证评分可复现。拿到参数层的 Fisher 对角之后还要从参数聚合到通道。对卷积层来说一个输出通道对应一组卷积核切片把这一组切片上的 Fisher 值加总得到的就是该输出通道的恢复敏感度。对全连接层或注意力层处理方法类似按输出维度聚合即可。3.2 临时置零 短期再训练另一种更直接的预演Fisher 估计有一个局限它是基于当前权重位置的一阶梯度信息理论上是对二阶曲率的局部近似但它并没有真正模拟剪掉之后微调几步的动态过程。所以 I 在实际流程里还会加一个更暴力的预演手段——直接对候选通道临时置零然后跑几步梯度。这个预演不用耗时太长因为我们要的不是精确的恢复上限而是一个相对排序。我自己的配置如下每个候选通道置零后用校准集上的固定 batch 跑 3 到 5 步 SGD记录 loss 从置零后的峰值下降了多少。下降快的说明恢复潜力高下降慢的说明一旦剪掉就很难办。需要注意这里的置零要用掩码实现而不是真正改掉网络结构不然每评估一个通道都要重建一次模型工程上完全不可行。一个近似实现是def rehearse_channel(model, module, channel_id, loader, steps3, lr1e-3): # 给指定 channel 施加置零掩码 mask torch.ones_like(module.weight) mask[channel_id] 0.0 module.weight.data * mask losses [] optimizer torch.optim.SGD(model.parameters(), lrlr) for step in range(steps): inputs, _ next(iter(loader)) outputs model(inputs) loss F.cross_entropy(outputs, targets) optimizer.zero_grad() loss.backward() optimizer.step() losses.append(loss.item()) return losses这段代码为了展示核心逻辑做了大量简化实际工程里要更细致置零掩码要在前向传播中动态生效而不是真的把权重清零写入短期再训练的优化器参数需要固定评估完一个通道之后必须恢复原始权重确保下一个通道的评估从同一个起点出发。我一般会在评估前先把模型参数做一个深拷贝备份每个通道测完再 reload 回来虽然慢一点但保证公平。预演得到的一串损失曲线可以提取两个量置零后 loss 的峰值以及最后一步 loss 相对峰值的下降幅度。前者对应即时损失后者对应恢复速度。把这两个量和 Fisher 聚合值一起做归一化加权求和得到最终评分。我自己的权重比例大约是 Fisher 占 0.4即时损失占 0.3恢复速度占 0.3你可以根据自己的任务调整。3.3 偏向保留难恢复通道的整合策略评分算出来之后最后的剪枝决策就不是简单按分数从低到高删了而是要考虑保护名单。分数最高的那批通道意味着剪掉它们会让模型进入很难回归的状态默认进入保留名单哪怕它们的传统重要性分数并不高。这一步是 ReCal 与传统剪枝在决策层面最核心的区别。实际操作时我会先按传统重要性指标给出一个候选剪除集然后拿恢复敏感度评分把候选集里分数过高的通道踢出去再从低分候补里补足名额。这相当于给剪枝决策加了一道保险闸门。有些通道按 L1 范数看确实很弱、该剪但预演显示剪掉之后整个模型梯度通路都会瘫——这种通道必须保。宁可剪掉几个尾部低效通道也不能让关键梯度通路断掉。以下是整合后的伪代码逻辑def compute_final_priority(importance_score, fisher_score, instant_loss, recover_speed): # 三种信号统一归一化 importance_norm normalize(importance_score) # 越大越重要 fisher_norm normalize(fisher_score) # 越大越难恢复 instant_norm normalize(instant_loss) # 越大瞬时损失越大 recover_norm normalize(-recover_speed) # 恢复越慢分数越高 retention_priority ( 0.3 * importance_norm 0.4 * fisher_norm 0.2 * instant_norm 0.1 * recover_norm ) return retention_priority # 剪除顺序 retention_priority 从低到高 prune_order torch.argsort(retention_priority)为什么 Fisher 的权重我给到 0.4因为在我的实验里Fisher 对剪后恢复困难的预测稳定性最好即时损失次之短期恢复速度噪声最大。这个比例不一定是通用最优解但你至少可以从这个配置起步再针对自己的模型和任务做小范围调参比凭空设定要稳。4. 剪枝执行与快速收敛阶段的参数配置4.1 一次性裁剪还是渐进裁剪评分器落地之后下一个要决定的问题是怎么执行裁剪。业内有渐进裁剪和一次性裁剪两种主流做法。渐进裁剪是在训练过程中逐步提高稀疏度每剪一点就微调一段好处是整个过程比较平稳坏处是训练成本高、流程复杂。一次性裁剪则是训练完的模型直接一步到位剪完再统一恢复。ReCal 的场景里我更推荐一次性裁剪。原因很简单预演评分的假设是某个目标稀疏度下哪些通道幸存、哪些通道被删这要求剪枝决策基于最终稀疏度一次定下来。如果你采用渐进裁剪第一阶段剪掉的通道和第二阶段剪掉的通道所处的上下文完全不同评分器捕捉到的信息会变得混乱。我实测下来渐进裁剪在 ReCal 框架下不仅没有带来额外收益反而因为目标漂移导致恢复难度评估失真。一次性裁剪还有一个好处就是可以和预演环节共享同一份评分表。你预先算好所有通道的恢复敏感度分数按目标稀疏度划一条线线上保留、线下剪除然后整个模型只经历一次结构突变后续恢复全部交给微调阶段去解决。这种做法和 ReCal 的整体思路是自洽的。4.2 精调阶段的学习率与 BN 重置剪枝执行完之后模型的 BN 统计量一定是不准的——我之前说过BN 是基于完整通道分布算出来的通道一删必然失真。所以剪完后的第一件事不是盲目开始蒸馏而是先跑一遍校准数据重新统计每个 BN 层的均值和方差。这一步我建议在进入任何梯度更新之前完成否则模型一开始就会往错误方向跑。重置完 BN 之后再进入快速精调阶段。这个阶段的配置我基于多次实验总结了一个比较稳的基线初始学习率设为正常训练学习率的十分之一甚至更低大概在1e-4到2e-4的量级用 warmup 在几百步内线性升到目标值然后余弦退火到接近零。蒸馏温度在前面预备适配阶段可以高一点比如 5 到 6因为高温度带来的软标签分布更平滑适合让幸存通路探索新的表达空间进入剪后精调阶段后温度降到 2 到 3让模型更聚焦地拟合 teacher 的细粒度分布。训练轮数方面剪后精调不需要像完整预训练那么长。以百类图像任务为例预训练可能要几百个 epoch 才能收敛但 ReCal 的剪后精调一般 15 到 25 个 epoch 就差不多了。因为剪好的模型在剪前已经做过预备适配起点高收敛快。如果你发现 25 个 epoch 后精度还在明显上升那大概率是学习率安排不对或者蒸馏权重不平衡而不是轮数不够。4.3 与 OPD 后处理相比蒸馏的角色发生了什么变化这里要重点说清楚蒸馏在 ReCal 和 OPD 里的不同地位。在 OPD 流程里蒸馏是剪枝之后唯一的恢复主力它既要负责重新教会模型认识数据分布又要负责弥补剪枝带来的结构损伤任务很重。而在 ReCal 流程里蒸馏同时出现在剪枝前和剪枝后两个阶段但角色完全不同剪枝前的预备适配蒸馏负责把软标签知识注入幸存通路让它们在剪枝那一刻就具备独立工作的基础剪枝后的精调蒸馏更像是维持和校准让模型在已经接近目标盆地的位置上再精确调整。这个分工变化带来的直接好处是剪后精调阶段对蒸馏的依赖度大幅下降。我在实验中发现即使把剪后精调的蒸馏权重从 1.0 降到 0.6ReCal 的模型精度也没有明显下滑而同样幅度的变化放在 OPD 流程里精度会立刻掉得很明显。这说明 ReCal 的剪前准备确实替剪后阶段分担了很大一部分恢复压力。换句话说OPD 是在用剪后时间换精度ReCal 是在用剪前时间换精度而剪前时间往往更从容、收益也更可控。5. 我实测的效果、适用边界与几个容易踩的坑5.1 一组典型对比数据我在一个百类图像分类任务上用常见的残差网络结构做过一组对照实验。原模型 Top-1 精度大约在 76.4%分别按 50% 和 70% 的通道稀疏度做剪枝恢复手段分三组剪完不管、OPD 模式剪后蒸馏微调、ReCal 模式剪前预备适配 恢复敏感度评分 剪后轻量精调。示意结果如下方案50% 稀疏度70% 稀疏度剪完直接评估71.262.8OPD剪后蒸馏73.168.7ReCal剪前校准74.971.3未剪枝原模型76.476.4需要说明这是示意数据来自我自己的内部实验不是正式论文级的严谨 benchmark但趋势非常稳定。50% 稀疏度下ReCal 相比 OPD 挽回的精度多出 1.8 个点70% 稀疏度下差距拉大到 2.6 个点。稀疏度越高ReCal 的优势越明显因为结构损伤越严重事后补救的能力就越有限剪前准备的价值就越大。我后来在更小的模型上也复现过这个趋势不过幅度会收窄。5.2 什么时候 ReCal 不灵ReCal 不是万能药我在三类场景下发现它收益有限甚至可能起到反作用。第一类是模型本身冗余极低的场景。比如一个已经用神经架构搜索压缩得非常紧凑的小模型通道之间几乎没有冗余剪枝本身就没多少空间所谓的预备适配也就无从谈起。第二类是没有任何可用 teacher 的场景。前面说过预备适配依赖蒸馏信号如果你手里只有一个孤零零的小模型连一个精度更高的大模型都没有那 ReCal 就只剩评分器这一半功能收益会打折扣。第三类是交付时间极度紧张的场景。ReCal 的预演和预备适配都发生在剪枝前即使做的只是短期训练也需要几千步的前向反向验证成本比剪完直接微调高出不少。如果你的项目只求快速上线、稀疏度要求又不高直接用 OPD 可能更省时间。5.3 几个容易忽略的坑最后分享几个我在复现过程中踩过的坑每一个都花了我不少时间去排查。第一个坑是预演阶段用训练集更新 BN 统计量。一开始为了图省事我直接拿训练集跑剪枝预演结果 BN 统计量被训练集的偏见带偏剪枝评分也跟着失真。后来换成单独的校准集只在评估前用 model.eval() 模式刷新 BN问题就消失了。第二个坑是 Fisher 采样数不足。我第一次跑打分的时候只采了 128 个样本算出来的通道排序明显不稳定同一条通道重复评估的分数差异很大。加密到 1024 个样本之后排序才稳下来。所以这个环节的采样量别省得分不稳定的情况下做的所有剪枝决策都是猜。第三个坑是剪前预备适配的蒸馏权重失衡。蒸馏权重设得太高学生模型会过度模仿 teacher 的软标签反而丢掉自己在硬标签上学到的判别性特征导致剪枝前精度就开始下降。我的建议是蒸馏权重从 0.5 起步逐步往上加而不是一开始就拉到 1.0观察验证集精度有没有掉头向下。第四个坑是残差结构里的 shortcut 通道必须特殊对待。残差网络里 shortcut 承担的是梯度高速公路的角色不参与特征学习但直接影响梯度流。传统重要性指标很容易把 shortcut 判成不重要但预演会发现剪掉 shortcut 之后模型几乎无法恢复。所以我后来会把含 shortcut 的层单独拎出来默认全部跳过剪枝只在极端稀疏度要求下才考虑动它们。第五个坑比较隐晦对注意力模型和卷积模型评分器的表现差异明显。注意力模型的通道冗余更多、恢复潜力更高ReCal 的优势体现得最充分卷积模型则要看具体结构强正则化模型比如深度可分离卷积的受益幅度会小于普通卷积。所以如果你直接把自己的模型套用我的默认权重比例可能不是最优的建议先用小规模预演快速扫描几组权重比例找到相对稳定的区间再全面跑。我在实际项目中反复试过之后最大的体会是剪枝和恢复不应该当成两个割裂的阶段剪前花 10% 的精力去做预演和准备往往比剪后花 100% 的精力去补救回报更高。ReCal 这套流程是我目前自己在用的版本它不复杂核心就是换一个提问方式从哪个通道不重要换成哪个通道剪了之后回不来。如果你也在做模型压缩不妨拿这个思路去试试哪怕只加一道恢复敏感度评分也能避免不少剪完就废的尴尬局面。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑