模型剪枝:砍掉冗余,再微调

模型剪枝 = 训练完成之后,按”重要性”砍掉冗余参数(完整 → 残缺),再用小学习率微调几轮,让留下的参数把被砍掉的那点活接手过来,工资少发了,活基本还是干完的。


一、这份材料解决什么问题

模型上线时会撞上三堵墙:显存装不下、单次推理太慢、模型文件太大。这三堵墙的根因是同一件事,模型是过参数化(over-parameterized)的:为了让梯度下降能顺利找到好解,我们训练时故意用了远超需要的参数量,而训完之后,其中相当一部分权重对最终输出几乎没有贡献。剪枝就是干”训完之后收拾房间”这件事。

放在新闻分类项目这条流水线里,剪枝是第四章「模型压缩」四件套的第三件,位置非常清楚:BERT 微调(3.3)跑出指标最好的模型 → 它太大、Flask + Streamlit 部署吃力 → 用量化(降位宽)、蒸馏(换小结构)、剪枝(删冗余参数)、低秩分解(大矩阵换成两个小矩阵)去压。四者可以叠加,而本份材料讲的是其中最”直白”的一种:删参数。

它要回答的核心问题其实是三个:
1. “不重要”怎么定义?用”接近 0″这个代理指标(没业绩就淘汰、末位淘汰)。
2. “删”的粒度是什么?一个个删(非结构化)还是整块删(结构化)。
3. 删完之后精度掉了怎么办?微调,让剩下的参数重新分工。


二、核心结论

  1. 剪枝的本质是”加掩码”而不是”改结构”:主流实现是给权重乘一个 0/1 的 mask,被剪的参数值还在张量里、只是被置 0(”坐在工位上不给钱”)。只有把 mask 固化下来、真正换掉张量形状,参数量才会变少。
  2. 非结构化剪枝(unstructured)粒度细、精度友好,但默认不提速:参数只是变成 0,参数量、内存占用不变,PyTorch 默认不做稀疏矩阵加速;在标准实现下因为多了一层 mask 乘法,甚至可能比剪枝前更慢。
  3. 结构化剪枝(structured)整块删除神经元 / 卷积核 / 整层,留下的部分结构完整,因此框架和硬件加速支持更好、能真正提速,代价是粒度粗、同等比例下精度更敏感。
  4. “小权重不重要”只是启发式假设(heuristic),不是定理:参数之间高度耦合,理论上存在”裁到大动脉”的权重;PyTorch 官方最经典的 幅度剪枝 明确只把它当成”一种可用的剪枝策略”。剪枝 + 微调 才是完整方案,微调用很小的学习率和很少的轮次。
  5. 工业部署阶段更偏爱”简单、可控、可复现”:梯度 / 二阶(Hessian)信息是更”聪明”的判据,但计算太贵且结果依赖实现细节;所以幅度剪枝 + 微调至今仍是主流。标准压缩顺序是 蒸馏 → 剪枝 → 量化。

三、知识地图

第 1 页|用裁员讲剪枝(铺垫)
– 团队里有人干活多、有人干活少 → 裁掉干活少的,活还能干完,工资少发很多(= 剪枝的收益与代价)
– 裁完之后”再做微调” → 留下的人接手被裁掉的那点活(= 微调)
– 幻灯片标注:[掌握] 直觉类比

第 2 页|剪枝的定义与基本假设(重点)[掌握]
– 定义:训练完成后,移除”不重要的”参数
– 代理指标:”接近 0″;但”怎么定义接近、选多少、按什么结构选”有三种不同口径
– 阈值剪枝 vs 比例剪枝(两种选法)
– 关键反思:万一”裁到大动脉怎么办” → 剪枝是启发式假设,不是真理
– 剪枝之后必须微调的动机

第 3 页|重要性判据的三种”聪明程度”[掌握]

方法 判断标准 通俗理解
幅度剪枝 权重的绝对值 “这个数字小,应该不重要”
梯度 / 损失变化 移除后损失增加多少 “试试去掉它,损失会涨多少?”
二阶信息 损失函数的曲率(Hessian) “这个地方平坦还是陡峭?平坦的地方可以剪”
  • 为什么部署不用”更聪明”的:太复杂、太昂贵;部署需要”规则简单可控、精度下降可预期可复现、不必每次部署都做昂贵计算”

第 4 页|压缩流水线的组合顺序[掌握]
– 蒸馏 → 剪枝 → 量化
– 蒸馏 → 剪枝 → (微调 + QAT 同时做)→ 转换为 int8 → 量化后微调
– 可选流程:蒸馏 → 剪枝 → 微调 → 量化

第 5 页|剪枝方法的两大阵营[掌握]
– 非结构化剪枝:不关心结构,按标准决定”是否移除这个权重”;剩下的是稀疏分布;框架 / 硬件兼容性低,需要特殊软硬件支持;”每个部门都裁一点,麻烦但是精准”;实际常用,不直接改结构,只把参数置 0
– 结构化剪枝:整体移除神经元 / 卷积核 / 网络层,以 filter 或整个网络层为单位;留下部分完整,框架和硬件加速支持更好;”整个部门裁员,简单粗暴”

第 6 页|L1 范数与掩码实现细节[掌握]
– L1 范数 = 绝对值之和(给出向量 \mathbf{w} 与 \|\mathbf{w}\|_1 的公式)
– 在非结构化剪枝里,用 L1 范数实际上就是单个参数自身的绝对值 |w|
– 流程:收集所有指定参数的 |w| → 全局排序 → 找末位部分 → 生成 mask 使其失效(直接删除会破坏结构完整性)
– 非结构化为什么不加速:参数量不变、内存不变、PyTorch 默认不做稀疏加速;要真加速得用稀疏张量格式(GPU 支持有限);标准实现下甚至更慢,因为多了 mask
– 批注:”数学中定义,和我们实际略有不同”

原图清单与去向:第 1 页 1 张(裁员梗图 → §4.1)、第 2 页 3 张(阈值/比例剪枝表格 → §4.4)、第 3 页 1 张(重要性判据表 → §4.3)、第 5 页 2 张(两种剪枝的掩码示意 → §4.2)、第 6 页 2 张(向量定义与 L1 公式 → §4.3)。


四、原理详解

4.1 直觉:剪枝就是一次裁员

把这个团队问题当成优化问题看,三个角色一一对应:

裁员故事 模型剪枝
有的人干活多、有的人干活少 有的权重对输出影响大、有的几乎不影响
把干活少的人裁掉,活还能干完 把不重要的权重置 0,精度只掉一点点
工资可以少发很多 模型变小 / 变快 / 省显存
留下的人做微调,接手被裁的活 微调(fine-tune),用极小学习率、少量轮次
“裁到大动脉怎么办”的风险 参数间高度耦合,”小权重一定不重要”只是启发式假设

第1页 原图 1

图注:一张”团队干活”梗图,45 个工人(标签从 cpu 0 一直编到 cpu 44)围着一个大坑站成一圈,只有 cpu 0 一个人下到坑里挥铲子干活,其余 44 个都拄着手站在坑边围观、闲聊。用它对应第 1 页的比喻:绝大多数”工人”(参数)都在围观,实际出力的只有极少数,所以”把干活少的裁掉,团队可能还能完成工作”。图右下角带有平台水印,这张图只负责建立直觉,不含技术信息。

形式化地写,剪枝要解的问题是:给定训练好的权重 \mathbf{W} 和一个目标稀疏度 s,找一个 0/1 掩码 \mathbf{M},

\min_{\mathbf{M}} \ \mathcal{L}\big(f(\mathbf{x}; \mathbf{W}\odot\mathbf{M})\big) \quad \text{s.t.} \quad \frac{\|\mathbf{M}\|_0}{\dim(\mathbf{W})} = 1-s

其中 \odot 是逐元素乘。注意 \min 是对掩码取的,这就是为什么真正好用的剪枝流程里总带着”评估 → 剪 → 微调”的循环,而不是一次拍脑袋删完。

4.2 两大阵营:非结构化 vs 结构化

(1)非结构化剪枝:散点式置零

PPT 原文:非结构化剪枝并不关心模型结构,只按某种标准(如权重绝对值大小)决定”是否移除这个权重”。移除后剩下的权重分布是稀疏的,即大多数权重为零;留下的部分”不完整、稀疏、兼容性比较低”,通常需要特殊的硬件或软件支持才能有效利用稀疏性,”需要框架支持才行”。幻灯片上的比喻是:每个部门都裁一点,麻烦但是精准;实际工程中常用,因为它不直接去掉结构,只是告诉这个参数被置 0,”坐在工位上不给钱”。

第5页 原图 2

图注:六个 3×3 的小方格阵(3 行 × 2 列排布),每个阵里有 3~4 个格子被涂成红色,红色格子零散分布,从来不连成完整的一行或一列(有的单独出现,有的两个挨着)。这就是非结构化剪枝的形态:被处理的单位是单个权重,剪完之后留下的掩码像撒了一把点,没有任何整行/整列的规律性,因此”留下的部分不完整”。红格只是标记,图中无文字。

掩码是怎么实现的:PyTorch 的 torch.nn.utils.prune 不会真的改动张量形状,它把参数拆成 weight_orig(原始值,仍是 nn.Parameter)和 weight_mask(0/1 缓冲),再挂一个 forward 前钩子,前向时算 weight = weight_orig * weight_mask。所以官方教程里 named_buffers() 会多出一个 weight_mask,module.weight 打印出来的被剪位置就是 0。这一步是理解”非结构化剪枝不提速”的钥匙:参数量一个没少,只是每次前向多算了一次乘法。

(2)结构化剪枝:整块移除

PPT 原文:结构化剪枝整体移除整个神经元、卷积核、网络层,通常以 filter 或者整个网络层为基本单位;由于留下的部分是完整的,框架和硬件加速支持更好。比喻是:整个部门裁员,简单粗暴。

第5页 原图 1

图注:三组 3×3 方格,每组左边一个整块全部填红、右边一个整块完全空白,没有任何零散格子。对比上图的散点,它强调的差别是单位不同:这里被处理的最小单位是”一整块”,块内要么全留、要么全走,不存在”块里只走两个格子”的情况。映射到实际结构,一整块就是一个卷积核 / 一个神经元 / 一整层;正因为走得干干净净、留下的张量形状规整,后端框架和硬件才有机会真正跳过这些计算。

两张图在 PPT 上是相邻排布的一对,按图中现象与两种剪枝的定义对应:散点式置零 ↔ 非结构化,整块消失 ↔ 结构化。

(3)一个必须背下来的结论:能不能真提速

非结构化 结构化
处理单位 单个权重 神经元 / 卷积核(filter)/ 整层
剪完长什么样 稀疏、散点、不规整 规整、可直接退化成更小的张量
参数量 不变(只是变 0) 真的变少
内存占用 不变(还是存同样多的数) 变小
推理速度(PyTorch 默认) 不变,甚至更慢(多出 mask 运算) 可以真正变快
要提速的前提 稀疏张量格式 + 特殊硬件 / 框架支持(GPU 支持有限) 常规算子即可
精度友好度 高(同等比例下掉点少) 低(粒度粗,同等比例下更疼)
PPT 比喻 每个部门都裁一点,麻烦但精准 整个部门裁员,简单粗暴

补一句硬件侧的”中间路线”:为了让稀疏既能省算力又能被硬件吃下,工业界还有半结构化 / n:m 稀疏(最典型的是 2:4,即每 4 个连续权重里固定保留 2 个),NVIDIA 在较新的 GPU 架构上为这种模式提供了稀疏张量核支持,但需要相应的库和框架配合才能落到实际加速上。它的定位就是”用一点点结构化约束,换取硬件真正能利用的稀疏性”。

4.3 重要性怎么评:三种判据

第3页 原图 1

图注:PPT 第 3 页给出一张三行三列的对比表。表头是 方法 / 判断标准 / 通俗理解;三行分别是:幅度剪枝,标准是”权重的绝对值”,通俗理解”这个数字小,应该不重要”;梯度 / 损失变化,标准是”移除后损失增加多少”,通俗理解”试试去掉它,损失会涨多少?”;二阶信息,标准是”损失函数的曲率(Hessian)”,通俗理解”这个地方平坦还是陡峭?平坦的地方可以剪”。这张表是本节的主干:三者的区别就是”估一次删除的代价”用了几阶信息。

(1)幅度剪枝:只看 |w|

这是零阶(最省)的做法,也是 PyTorch 的默认套路。它的理论根子是泰勒展开:把一个权重置零相当于给参数一个扰动 \Delta w_i = -w_i,损失的变化可以写成

\Delta\mathcal{L} \approx \underbrace{g_i \,\Delta w_i}_{\text{一阶项}} + \underbrace{\tfrac{1}{2}\,H_{ii}\,(\Delta w_i)^2}_{\text{二阶项}} = -\,g_i w_i + \tfrac{1}{2}H_{ii}w_i^{2}

其中 g_i=\partial \mathcal{L}/\partial w_i,H_{ii}=\partial^2\mathcal{L}/\partial w_i^2。幅度剪枝等于把这两项全丢掉,直接拿 |w_i| 当”删除代价”的代理:|w_i| 小 → 扰动小 → 损失涨得少。梯度判据用到一阶项(g_i w_i),二阶判据(Hessian,OBD / OBS 那种思路)用到二阶项,越往后越准,也越贵,二阶项要算曲率,代价随参数量平方级增长,这就是”聪明的方法计算太贵(尤其二阶 Hessian)”这句话的技术含义。

(2)L1 范数在这里到底指什么(第 6 页的批注要点)

第6页 原图 1

图注:第 6 页的第一张公式图,定义一个向量 \mathbf{w} 的写法(n 维向量,分量记为 w_1, w_2, \dots, w_n),是为下面 L1 范数公式做铺垫的记号说明。

第6页 原图 2

图注:第 6 页的核心公式,L1 范数 = 绝对值之和,把向量各分量取绝对值后求和。这就是幻灯片上”L1 范数:绝对值之和”那句话的数学形式。

这里必须拆清楚一个容易混的点(PPT 也专门批注了”数学中定义,和我们实际略有不同”):

  • 数学上 \|\mathbf{w}\|_1=\sum_i |w_i| 是整个向量的一个标量。
  • 但在非结构化剪枝里,”用 L1 范数”实际执行的是对每个参数单独取 |w_i| 再全局排序,因为非结构化的处理单位就是单个权重,只有”一个数”才能决定”一个格子”留不留。
  • 而当你需要给一整块(一个 filter、一个神经元、一整层)打分时,才会用到”把一个向量压成一个数字”的 L1 / L2 范数:结构化剪枝是按整块的范数排序、整块删除。

所以”用 L1 还是 L2″这句话在不同语境下含义不同:非结构化语境下 L1 = 单参数 |w|;结构化语境下 L1/L2 是块级聚合口径。这是这篇材料最典型的考点之一。

(3)工程流程:全局排序 → 生成掩码

第 6 页原文把非结构化的执行步骤说得很清楚:收集所有指定参数中每个权重的绝对值 → 全局排序 → 找到末位的部分 → 生成一个 mask(掩码)使其失效。注意最后半句”直接删除会导致结构不完整”,这就是掩码式实现的由来。

4.4 两种”选多少”的口径:阈值剪枝 vs 比例剪枝

第 2 页的三张图是同一张表格被拆开的三块(左列方法名、中列判断标准、右列例子),拼起来是:

第2页 原图 1

图注:第 2 页的表格主体,两行两列:阈值剪枝 对应”移除绝对值 < 某阈值的权重”;比例剪枝 对应”移除绝对值最小的 k%(如 30%)”。

第2页 原图 2

图注:同上表格的”判断标准”一列单独抽出:上格”移除绝对值 < 某阈值的权重”,下格”移除绝对值最小的 k%(如 30%)”。可以清楚看到两者分别是绝对门槛和相对百分比。

第2页 原图 3

图注:同上表格的”通俗理解 / 例子”一列单独抽出:阈值剪枝的例子是”去掉所有 |w| < 0.01“;比例剪枝的例子是”对一层或全局,去掉最小的 30% 权重”。后半句里的 “一层或全局” 就是 local 与 global 两种打分范围,它也是 global_unstructured 这个 API 名字里”global”的含义。

两种口径的取舍很直接:

口径 怎么定 好处 坏处
阈值剪枝 卡一个绝对门槛(如 \|w\|<0.01) 规则极简、直觉友好 稀疏度不可控;不同层权重量级不同,同一个阈值可能把某层全剪光、另一层一个没剪
比例剪枝 卡一个百分比(如最小 30%) 稀疏度可控,压缩率和速度可预估 每层都硬砍 30%,可能剪掉本就很”瘦”的层的有效权重
比例 + 全局 所有层的 \|w\| 汇总排序后砍最后 30% 让”权重量级大、冗余多”的层多挨刀 可能把整层剪空 → 需要加保护(不剪某些层)

4.5 完整流程:训练 → 评估 → 剪枝 → 微调(→ 可选迭代)

① 正常训练到收敛(拿到一个高精度的大模型)
        ↓
② 重要性评估:收集目标参数的 |w|(或梯度 / Hessian 打分)
        ↓
③ 全局排序,按阈值或比例生成 mask,把末位参数置 0
        ↓
④ 微调:小学习率、少轮次,让剩余参数补偿被剪掉的功能
        ↓
⑤ 可选:重复 ②~④(迭代剪枝 / 逐层递增剪枝),或进入量化 → 部署

带数字的小例子。假设某层权重是一个 4×4 矩阵(16 个参数),我们打算全局剪掉 25%:

行 该行 4 个权重
第 1 行 0.02, -0.31, 0.87, -0.05
第 2 行 0.44, -0.009, 0.12, 0.63
第 3 行 -0.71, 0.03, -0.28, 0.005
第 4 行 0.19, -0.52, 0.07, -0.88

把所有 |w| 拉平排序(升序):0.005, 0.009, 0.02, 0.03, 0.05, 0.07, 0.12, 0.19, 0.28, 0.31, 0.44, 0.52, 0.63, 0.71, 0.87, 0.88。取最小的 4 个(= 16 的 25%)置 0,得到掩码:

[[0 1 1 1]
 [1 0 1 1]
 [1 0 1 0]
 [1 1 1 1]]

被剪掉的是 0.005, 0.009, 0.02, 0.03 这 4 个数(分布是:第 1 行掉 1 个、第 2 行掉 1 个、第 3 行掉了 2 个、第 4 行一个没掉,全局排序不保证每层分摊得均匀)。剪完之后:非零参数 12 个 / 总共 16 个,稀疏度 25%,但张量形状还是 4×4,参数量还是 16,这就是”非结构化剪枝不省参数”最直观的体现。

如果换成结构化,按行 L2 范数给”整行”打分:四行的范数分别是 0.925, 0.778, 0.764, 1.042,最小的是第 3 行(0.764),于是整行删掉。结果是:仍然剩 12 个非零参数(这里是数值上的巧合),但留下的三行范数原封不动(0.925 / 0.778 / 1.042),结构规整,第 3 行对应的那个神经元可以直接从网络里拿掉,下一层的输入维度随之减 1,这才是能被框架和硬件真正吃下的”变小”。

为什么剪完必须微调?三层理由,层层递进:

  1. 置零的瞬间损失一定上升。幅度剪枝是零阶近似,它只保证”涨得少”,不保证”不涨”。删掉的东西再小,也是原来模型输出的一部分。
  2. 参数之间是互相耦合的。一个权重的贡献依赖于与它配合的其它权重;把耦合的两端之一砍掉,剩下的部分处于一个”没见过的新结构”里,需要重新适配。
  3. 剩下的参数可以提高分工。PPT 的说法是”让其他人干多一点活”,微调就是给留下的权重一个机会,把被砍掉那部分功能学回来。因为被砍的功能本来就不多,”微调”就够了:用很小的学习率、训练少量轮次,目的是在保留原有知识的前提下适应结构变化。学习率大了会把已经学好的特征打散(等于把”裁完的团队又打散重组”),训练轮次多了会过拟合。

为什么不用更”聪明”的判据?PPT 在第 3 页直接给了答案:聪明的判据太复杂、太昂贵;而在模型部署阶段我们真正需要的是:

  • 剪枝规则简单、可控;
  • 精度下降可预期、可复现;
  • 不必每次部署都重做昂贵计算。

所以结论句是:实际工业部署中,幅度剪枝 + 微调 依然是主流。

4.6 剪枝率与精度损失的关系

这是最容易被追问、也最容易被答错的地方。它的形状不是线性的,而是一条先平后陡的曲线:

稀疏度区间(经验形状,非定律) 典型现象 对策
0 ~ 10% 几乎无损,多数模型连微调都不太需要 直接剪
10% ~ 50% 剪完立刻掉点,但微调后基本恢复到接近原水平 微调 1~2 轮,小学习率
50% ~ 90%(非结构化) 微调能救回一部分,掉点开始可见;需要迭代剪枝(每次剪一点点 + 微调)才稳 迭代式:每轮剪 10%~20% 再微调
90% 以上 进入断崖区,精度急剧崩塌,微调也救不回 到此为止,转量化 / 蒸馏

几条比数字更重要的定性规律:

  1. 同样的剪枝率,结构化比非结构化疼得多。非结构化是”每个部门裁一点”,可以精细到单个权重;结构化是”整个部门裁掉”,一刀下去损失一整个特征通道。所以实际工程里结构化剪枝的比例通常要保守得多。
  2. 一层一视同仁地砍,比”全局排序再砍”更容易出问题。参数少、本身已经很”瘦”的层(比如 BERT 的某些中间层、分类头)被硬砍 30%,往往就是掉点的主要来源。工程惯例是把这些层放进保护名单。
  3. 剪枝后的精度必须重新评估,而且要看混淆矩阵。整体准确率可能只掉 0.5 个点,但掉点可能全集中在某一个类上(比如 10 分类里的”财经”类),这种”平均看起来还行、单类已经崩了”的情况只有混淆矩阵能暴露。
  4. 曲线位置跟模型、数据、任务强相关。上面的区间只能当初始值,真正的安全剪枝率只能在自己的验证集上扫出来,这也正是 PPT 强调”精度损失可控、可复现”的原因:可复现的前提是你有一个固定的评估口径。

4.7 与其他压缩手段的关系与叠加

PPT 第 4 页给出的是组合顺序,不是单点技术:

蒸馏 → 剪枝 → 量化
蒸馏 → 剪枝 → (微调 + QAT 同时做)→ 转换为 int8 → 量化后微调
可选流程:蒸馏 → 剪枝 → 微调 → 量化

三种写法其实是同一个故事的三种节奏:

  • 蒸馏 → 剪枝 → 量化:标准顺序。先用蒸馏把”学生”结构变小(复杂 → 简单),再在这个小结构上删冗余(完整 → 残缺),最后压位宽(高清 → 模糊)。
  • 蒸馏 → 剪枝 → (微调 + QAT 同时做)→ int8 → 量化后微调:更省事的工程实现。剪枝后的微调和量化的 QAT 本来都需要”再训几轮”,两件事共用同一轮训练:在训练里插入伪量化节点,让模型一边适应”少了一批参数”、一边适应”数值精度变低”。训完转换到 int8,通常还会再做一次量化后微调(PTQ 校正)收尾。
  • 蒸馏 → 剪枝 → 微调 → 量化:最保守、最容易排错的做法,四个阶段各自独立验证,每一步都测一次精度。被问”你会怎么排”,答这条最稳。

为什么顺序不能随便换?

  • 蒸馏放最前:它解决的是”容量够不够”的问题。等你剪完、量化完才发现小模型精度上限太低,前面的工作全部白做。
  • 剪枝放中间:它改变的是结构(哪些参数存在),这会牵动后续所有步骤的形状;放在量化后面,等于剪完还要重新做一次量化校准,白白多一轮。
  • 量化放最后:量化只改数值表示、不改结构,它是”最后一层皮”。而且一旦量化到 int8,再做剪枝的可玩空间就被数值分辨率限制住了(剩下 256 个可取值,幅度排序的区分度变差)。

四件套一句话对照(配合 4.1/4.2/4.3 章的比喻记):

手段 一句话比喻 动的到底是什么 结构变不变
量化 高清 → 模糊 单个参数的位宽(float32 → int8) 不变
蒸馏 复杂 → 简单 整个模型的容量(换一个小模型去学大模型的输出) 换成另一个模型
剪枝 完整 → 残缺 参数的数量(删冗余) 结构化剪枝会变,非结构化不变
低秩分解 大矩阵 → 小矩阵 把一个大权重矩阵近似成两个小矩阵相乘 计算图变化(两层取代一层)

低秩分解值得补一句(来自 4.1「深度学习 + 线性代数极速版」的讲解):它的前提是”两者并不完全相等”,即我们接受近似,再用 SVD 或梯度下降之类的算法让两个小矩阵相乘的结果尽可能接近原来那个大矩阵,说到底是用”大体规律”去总结一个大矩阵。它和剪枝的区别在于:剪枝是把某些元素归零,低秩分解是换一种更紧凑的参数化方式去表示整块矩阵。


五、易错点与常见误解

常见说法:误解 1:剪枝能让模型推理变快。
实际上,只有结构化剪枝(并且真的固化成新结构)才可能变快;非结构化剪枝在标准 PyTorch 下不会变快,甚至可能变慢。
为什么:非结构化剪枝参数量不变、内存不变,只是很多权重变成 0;PyTorch 默认不做稀疏矩阵加速,而掩码是每次前向都要多算一遍的额外乘法。要提速必须换成稀疏张量格式,而 GPU 对这种格式的支持有限。

有人以为误解 2:剪枝就是把参数从模型里删掉。
其实掩码式实现是把参数置 0 并挂上 mask(”坐在工位上不给钱”),张量形状、参数量都不变。
原因在于,直接删除会让张量变稀疏、结构不完整,下游算子和框架无法处理。真正”删掉”需要 prune.remove() 把 mask 固化,或者在结构化剪枝里直接重建小张量。

常听到的一种说法是:误解 3:剪枝率越高越好,反正能微调回来。
真实情况是,存在断崖点,超过之后微调也救不回。
背后的道理是,剪枝是零阶近似,只在”扰动小”的区间成立;一旦被剪掉的是共享特征通道(尤其结构化剪枝),损失上升不再是微扰,微调没有足够容量去重建。

容易想岔的地方:误解 4:”L1 范数剪枝”就是拿整层的 L1 范数排序。
事实正相反,在非结构化语境下,用 L1 范数实际上就是单个参数自身的绝对值 |w|,逐参数全局排序;只有在结构化语境下才把整块聚合成一个范数标量来排序。
之所以如此,非结构化剪枝的处理单位是单个权重,必须要”一个数决定一个格子”;块级打分才需要把向量们压成标量。PPT 专门批注了”数学中定义,和我们实际略有不同”。

先说一个误解:误解 5:小权重一定不重要,所以剪掉一定没损失。
实际上,这只是一个启发式假设,”干活少的人大概率不重要”,不是绝对真理。
为什么:参数之间互相复杂关联,存在”这个 0 其实很关键”的情况(裁到大动脉)。PyTorch 官方也没声称”小权重一定不重要”,它只是提供了一种可用的剪枝策略,所以剪完必须微调兜底。

常见说法:误解 6:剪枝后不微调也差不多。
其实不微调通常能保住一部分精度,但微调是把”掉的那一点”补回来的标准步骤,不是可选项。
原因在于,置零瞬间损失一定上升,且剩余参数处于”没见过的新结构”里;微调用很小的学习率、少量轮次让它们重新分工,在保留原有知识的前提下适应结构变化(这就是第 1 页”微调”的比喻)。

有人以为误解 7:用梯度 / 二阶信息剪枝一定更好。
真实情况是,理论上更准,但太复杂、太昂贵(尤其二阶 Hessian 的计算量),且结果依赖实现细节;部署阶段优先要”规则简单可控、精度下降可预期可复现、不必每次部署重算”。
背后的道理是,部署是一次性、可重复的工程动作,不是追求极致压缩率的研究项目,所以”幅度剪枝 + 微调”至今是工业主流。

常听到的一种说法是:误解 8:剪枝和量化是一回事,都是”降精度换速度”。
事实正相反,两者动的东西完全不同:剪枝减参数数量(完整 → 残缺),量化降单个参数的位宽(高清 → 模糊)。
之所以如此,载体不同、工具不同、失败模式也不同,剪枝的失败模式是”结构被破坏 / 掉某个类”,量化的失败模式是”数值分辨率不足导致离群值失真”。

容易想岔的地方:误解 9:压缩就是”挑一个”用。
实际上,它们是可以叠加的流水线(蒸馏 → 剪枝 → 量化),压缩率会相乘;三者作用在不同维度(容量 / 参数数量 / 位宽),顺序有讲究,蒸馏最前、量化最后,剪枝夹在中间(见 §4.7)。


六、横向对比与研究脉络

6.1 剪枝方法横向对比

§4.2 的表格对比了”能不能提速”,这里补上层选型要看的三档:

维度 非结构化剪枝 结构化剪枝 半结构化(n:m,如 2:4)
粒度 单个权重 神经元 / 卷积核 filter / 整层 固定模式的分组(每 m 个里留 n 个)
推理加速 需稀疏张量格式 + 特殊硬件(GPU 支持有限) 常规算子即可 专用稀疏张量核
实现难度 低(一行 API) 中(要重建结构与后续层维度) 中高(依赖硬件与库生态)
精度损失(同比例) 小 大 介于两者之间
典型用途 研究、极限压缩率、可接受特殊运行时的场景 工业部署、端侧、延迟敏感场景 较新 GPU 上的推理加速

6.2 重要性判据横向对比

判据 用到的信息 计算成本 精度 PPT 的定位
幅度(\|w\| / L1 / L2) 只看向量本身 极低(一次排序) 够用 部署主流
梯度 / 损失变化(一阶) 反向传播的梯度 中(要算梯度) 更好 “更聪明但太贵”
二阶(Hessian、OBD/OBS 思路) 损失函数的曲率 极高(二阶导 / 逆矩阵) 最好 实际部署少用

6.3 演进脉络(只写有把握的部分)

  • 起点:过参数化模型训完仍有效,说明大量参数冗余,”能不能删掉一部分还不掉点”这个动机先于方法出现。
  • 早期:直接用幅度当判据,把”绝对值小”等同于”不重要”;同期出现了用损失曲面二阶信息判断的 OBD / OBS 思路,精度更好但代价高。
  • 分工定型:随着部署需求增长,方法分化成两条线,非结构化追压缩率和精度(粒度细、但框架不友好),结构化追真提速(粒度粗、但算子友好)。这份 PPT 第 5 页的两张掩码图就是把这两条线画清楚。
  • 工程收敛:因为部署阶段要”简单、可控、可复现”,幅度剪枝 + 微调成为实践证明最稳的组合;梯度 / 二阶判据退居研究圈。
  • 存储效率这条支线:另一个被反复验证的方向是”稀疏的子网络能不能从零开始独立训练到同等精度”(彩票假设 / lottery ticket 这一支),它解释的是”为什么剪枝后微调能恢复”的深层原因,但它不改结论,微调依然是必须的。
  • 硬件反向驱动:为了让稀疏真正变成速度,硬件侧补充了 n:m(2:4)结构化稀疏支持,把”非结构化精度好”和”结构化硬件友好”折中到同一个方案里。

七、在新闻分类项目中的落地

7.1 用在流水线的哪一步

放在 3.3 BERT 微调之后、部署(Flask + Streamlit)之前,作为第四章模型压缩的一环:

TF-IDF+随机森林 / FastText / BERT微调 / LLM(DeepSeek API)
                ↓ 选出精度最高的模型(大概率是 BERT)
        ┌── 压缩四件套 ──┐
        │ 蒸馏 → 剪枝 → 量化 │   ← 剪枝在这一步:删参数
        └──────────────┘
                ↓
        混淆矩阵 / classification_report 复评(对齐 3.1 的指标口径)
                ↓
             Flask 服务端 + Streamlit 客户端

7.2 代码:非结构化剪枝(对应 PPT 第 6 页的”全局排序 + 生成 mask”)

import torch
import torch.nn as nn
from torch.nn.utils import prune

# 1) 只挑 Linear 层的 weight 参与剪枝:embedding / LayerNorm / 分类头建议进保护名单
params_to_prune = [
    (m, "weight")
    for m in model.modules()
    if isinstance(m, nn.Linear) and m.out_features > 1
]

# 2) 全局 L1 非结构化剪枝:所有层汇总排序,砍掉绝对值最小的 30%
prune.global_unstructured(
    params_to_prune,
    pruning_method=prune.L1Unstructured,
    amount=0.30,          # 比例剪枝;也可以传 int 表示"剪掉多少个"
)

# 3) 剪完一定要看稀疏度:参数量不会变,只是权重被置 0
for module, name in params_to_prune:
    print(name, float(torch.sum(getattr(module, name) == 0)) / getattr(module, name).numel())

几个和 PPT 一一对应的细节:

  • global_unstructured 的语义就是幻灯片里的”全局排序”:它把所有指定参数的 |w| 聚合到一起再决定谁被剪,而不是每层各剪 30%。
  • 官方文档明确指出,全局剪枝只适用于非结构化方法,理由是”按块范数做全局结构化剪枝,如果不按参数尺寸做归一化就没太大意义”。所以想把通道级剪枝做成全局的,得自己写打分逻辑。
  • 剪完之后 module.weight 打印出来是 0,但参数变成 weight_orig + weight_mask 两个张量。这一步是”没变快”的直接原因。

7.3 代码:结构化剪枝(想真提速就走这条)

# 按 L2 范数整通道剪掉 20%(dim=0 对 nn.Linear 的 output 维度,diff 是下游层)
prune.ln_structured(
    module, name="weight",
    amount=0.20,
    n=2,
    dim=0,
)
# 真要让结构变小的关键一步:把 mask 固化成新的张量形状
prune.remove(module, "weight")

remove() 之后掩码被写死进权重,前向不再有多余的乘法,这是”非结构化置 0″和”结构化真变小”的分水岭。但它同时意味着:后续层的输入维度不再匹配,需要手工重建网络结构,这也是结构化剪枝在工程上比非结构化麻烦的原因。

7.4 关键参数怎么调

参数 建议起点 理由
amount(剪枝率) 全局非结构化 0.2 ~ 0.3;结构化 0.1 ~ 0.2 落在 §4.6 那条曲线的”微调可恢复区”,先验证流程跑通再往上加
按层还是全局 先用 global_unstructured 各层权重量级差异大,逐层硬剪容易毁掉”瘦”层
剪枝粒度 先非结构化验证效果,需要提速再转结构化 非结构化掉点可控、调试便宜
微调学习率 比原训练小一个量级(如 BERT 原 2e-5 → 5e-6 ~ 1e-5) PPT:用很小的学习率,避免破坏已学到的知识
微调轮次 1 ~ 2 个 epoch PPT:训练少量轮次,多了过拟合且失去”微调”的意义
迭代式剪枝 每轮剪 10%~20% + 微调,重复 3~5 轮 比一次剪 50% 稳得多,对应 §4.6 的”迭代剪枝”
保护名单 embedding、LayerNorm、分类头、bias 这些层参数少但承担关键功能,剪了掉点全砸在它们身上

7.5 实际会踩的坑

  1. 以为剪了就会快。非结构化剪枝后拿 time.time() 一测发现更慢了,因为多了 mask 乘法。被追问”非结构化剪枝不能提速”时,最好补一句”标准 PyTorch 实现下甚至可能更慢”,这句话很加分。
  2. 忘了 prune.remove(),保存的模型带着 mask。训练时 state_dict 里是 weight_orig 和 weight_mask;如果部署侧只加载普通权重,就会加载不上或权重不对。要么 remove(),要么在部署侧用同样的 pruning 代码复现结构。
  3. 保存/加载格式不一致。剪枝后的 BERT 用 torch.save(model.state_dict()) 与 save_pretrained() 的行为不同;ONNX 导出时稀疏张量支持有限,导出失败往往先怀疑剪枝实现方式。
  4. 评估口径没对齐。剪枝前后必须用同一个测试集(1w 条)+ 同一套指标(准确率 + macro F1 + 混淆矩阵),否则”掉了一个点”这个结论根本不可信,这就是 PPT 说的”精度下降可预期、可复现”。
  5. 一上来就剪 BERT 的最后一层分类头。分类头直接决定 10 个类别的分数,剪它的性价比最低、掉点最狠。

7.6 和项目里其它方案的配合

项目里的环节 与剪枝的关系
TF-IDF + 随机森林 随机森林的 feature_importances_ 就是”重要性排序”的同款思路(按重要性砍特征),被追问时拿它类比”用重要性判据删冗余”非常好讲
FastText 自带 cutoff(裁低频词/ngram)和 quantize,是最容易演示”压缩后精度掉几个点、体积省多少”的对照组,剪枝在 BERT 上讲原理,在 FastText 上做实验
BERT(transformers 微调) 剪枝的主要对象:BERT 参数量大、部署重,”剪枝 + 微调 + 量化”这套压缩正好回答”为什么你的部署方案是可行的”
LLM(DeepSeek API) 走 API 的路线完全不涉及剪枝(模型在别人机器上);它的价值是”零样本/少样本方案的成本对照”,说明”我为什么还需要一个能压缩、能本地跑的小模型”
量化 / 蒸馏 / 低秩分解 顺序固定为 蒸馏 → 剪枝 → 量化;剪枝后的微调可以和 QAT 合并成一轮训练,这就是 PPT 第 4 页第二种写法的落地

八、重点问答

Q1:什么是模型剪枝?它和量化、蒸馏有什么区别?

A:剪枝是训练完成之后,按某个”重要性”标准把不重要的参数移除掉,说到底是”完整 → 残缺”,减少的是参数的数量。量化降低的是单个参数的位宽,相当于”高清 → 模糊”;蒸馏换的是整个模型的容量,用一个小的学生模型去学大模型的输出分布,相当于”复杂 → 简单”。三者作用在不同维度,可以叠加,标准顺序是蒸馏 → 剪枝 → 量化。

Q2:非结构化剪枝和结构化剪枝的区别是什么?

A:区别在粒度和后果。非结构化剪枝按单个权重决定去留,剪完是一片散点的稀疏矩阵,参数量和内存都不变,PyTorch 默认也不做稀疏加速,所以标准实现下它不会变快、甚至因为多了掩码乘法会更慢,它的优势是同等比例下掉点最少。结构化剪枝以神经元、卷积核或整层为单位整块删除,留下的结构规整,框架和硬件都能真正跳过计算、真能提速,代价是粒度粗、同样比例下精度损失更明显。所以实际部署里结构化用来换速度,非结构化用来换压缩率。

Q3:为什么剪完之后一定要微调?

A:三个理由。第一,剪枝用的幅度判据是零阶近似,它只能保证”损失涨得少”,不能保证不涨,置零的瞬间损失一定上升。第二,参数之间是耦合的,砍掉一个以后剩下的参数处在一个没见过的新结构里,需要重新适配。第三,被剪掉的本来就只是一小部分功能,让剩下的参数重新分工就能把这点功能补回来。所以要用很小的学习率、训练少量轮次,学习率大了会把已经学好的特征打散,轮次多了会过拟合。通俗说就是”让留下的人做微调,把被裁掉的那点活接手过来”。

Q4:为什么工业界不一直用梯度或二阶信息这种更聪明的方法?

A:因为太贵。二阶方法要算 Hessian 或者它的逆,计算量随参数量急剧上升,而且结果比较依赖实现细节、不容易复现。部署阶段我们要的东西不一样:剪枝规则要简单可控,精度下降要可预期可复现,而且不能每次部署都重做一遍昂贵计算。所以实际工业部署里”幅度剪枝 + 微调”依然是主流,PyTorch 官方那套 prune API 默认也是幅度剪枝。

Q5:”L1 范数剪枝”到底在算什么?

A:分两种语境。在非结构化剪枝里,处理单位是单个权重,所以”用 L1 范数”实际就是单个参数自身的绝对值 |w|,把所有指定参数的 |w| 收集起来全局排序,砍掉末位那部分,然后生成掩码让它们失效,不是真的从张量里删掉,直接删除会破坏结构完整性。在结构化剪枝里,处理单位是整块,这时才会把一整块权重的 L1 或 L2 范数算成一个标量来排序。数学上的 L1 范数是绝对值和,和实际用法略有不同,PPT 也专门批注了这一点。

Q6:剪枝率开到多大合适?

A:这条曲线是先平后陡,所以没有通用数字,只能自己扫。经验形状是:10% 以内基本无损,10% 到 50% 掉了点但微调能恢复,50% 以上要配合迭代剪枝才稳,90% 以上基本就是断崖。另外两条比数字更重要的规律:同样的比例下结构化比非结构化疼得多;一层一视同仁地硬砍比全局排序更容易出问题,所以实际做法通常是全局排序 + 给 embedding、LayerNorm、分类头加保护名单,再用混淆矩阵看掉点集中在哪个类。

Q7:非结构化剪枝真的能提速吗?

A:标准情况下不能。因为参数量不变,只是很多权重变成 0,PyTorch 默认不做稀疏矩阵加速,内存占用也不变,还是存同样多的数。要真正加速必须换成稀疏张量格式,但 GPU 对这种格式的支持有限;而且剪枝模块的实现是 weight_orig * weight_mask 再送进前向,多了一层掩码乘法,所以在不做特殊处理的标准实现下,非结构化剪枝后模型不但不会变快,甚至可能变慢。想要速度就得走结构化,或者在硬件支持的情况下用 2:4 这种半结构化稀疏。

Q8:这些压缩手段该怎么排序?

A:蒸馏 → 剪枝 → 量化。蒸馏放最前是因为它决定”容量够不够”,如果最后才发现小模型上限太低,前面的工作全白做;剪枝放中间是因为它改结构,会牵动后续所有步骤的形状;量化放最后是因为它只改数值表示、不改结构,是最后一层皮,而且量化之后数值分辨率下降,再做剪枝的区分度就差了。工程上更省事的做法是”剪枝后微调和 QAT 合并成一轮训练”,训完转 int8 再做一次量化后微调收尾。


九、延伸阅读

官方文档与教程(本报告引用过的真实链接)
– PyTorch prune.global_unstructured API 文档:https://pytorch.org/docs/stable/generated/torch.nn.utils.prune.global_unstructured.html,含 amount 与 importance_scores 参数说明,以及”全局剪枝只限于非结构化方法”的官方说明。
– PyTorch Pruning Tutorial:https://pytorch.org/tutorials/intermediate/pruning_tutorial.html, l1_unstructured / ln_structured / random_unstructured、weight_orig 与 weight_mask、前向钩子的完整可运行示例,是理解”掩码式实现”最好的入口。

同项目内的相关材料
– 4.1模型压缩/【了解】深度学习+线性代数极速版,低秩分解”用更少的参数近似大矩阵”的讲法(SVD、梯度下降),是四件套里第四件的基础。
– 4.2模型量化/【掌握】动态静态量化-研究报告.md,量化的 QAT / PTQ / 动态量化三条路线,对应本报告 §4.7 里”量化放最后”的衔接点。
– 4.3模型蒸馏/【掌握】软硬标签-研究报告.md,蒸馏的软标签与 KL 散度,对应流水线的第一步。
– 3.1随机森林模型/【理解】混淆矩阵模型指标-研究报告.md,剪枝后精度回归检查用的指标口径。

概念线索(按名字检索即可,本材料未展开)
– 幅度剪枝(magnitude pruning)、全局剪枝 vs 逐层剪枝(global / local pruning)
– OBD / OBS(用损失曲率做最优脑损伤的早期二阶方法)
– 迭代剪枝(iterative pruning)、彩票假设(lottery ticket hypothesis)
– n:m / 2:4 结构化稀疏与稀疏张量核
– PyTorch 稀疏张量(torch.sparse)与 prune.remove() 的关系

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇