Deep Compression解读:剪枝、量化与霍夫曼编码的模型压缩组合拳
2026/9/16 1:33:59 网站建设 项目流程

去年做某个端侧项目时,我在一块嵌入式板上尝试部署一个稍微大一点的视觉模型。板子的Flash只有8MB,模型权重一烤进去,再塞点业务代码就基本见底了。折腾了一周,最后只能靠降分辨率、抽帧这类土办法硬凑。后来回去翻资料,重新看到Song Han他们2016年ICLR那篇拿到Best Paper的《Deep Compression: Compressing Deep Neural Networks with Pruning, Trained Quantization and Huffman Coding》,才意识到当年我们踩的坑,这篇论文里其实早就给出了系统解法。这次就把这篇论文从头到尾再读一遍,把里面的方法拆开揉碎,连同我自己复现时的经验和教训一起写出来。

这篇阅读随笔适合三类人读:做端侧或移动端部署的算法工程师,正在搞模型压缩、量化、剪枝方向的学生或研究员,还有那些“模型训练完发现根本塞不进硬件”的倒霉蛋。我会把论文里的三阶段压缩流水线——剪枝、训练量化、霍夫曼编码分别讲清楚,包括它们为什么按这个顺序组合、每个阶段的数学直觉是什么、复现时容易死在哪个环节。

1. 从那次移动端部署翻车说起:为什么要读这篇2016年的老论文

先说个真实的翻车现场。当时我们想把一个分类模型塞进一块ARM Cortex-M系列的MCU上做离线推理,模型训练完精度还算能看,Top-5大约85%。结果一导权重,光参数就有十几MB。MCU的Flash总共也就那么多,不仅要放模型,还得放RTOS、协议栈、字库,最后实在没办法,只能在输入上做文章——把图像从224缩到160,帧率从10帧砍到3帧,勉强跑起来后精度掉到了81%,体验非常糟糕。

回头看,我当时的手段全是“治标不治本”:优化输入数据流、裁剪预处理逻辑、减少运行时日志,这些都没有真正触碰问题核心——模型本身携带了太多冗余参数和冗余精度。后来读到Deep Compression这篇论文,里面有一句话让我印象很深:神经网络是明显过参数化的,这意味着权重中存在大量冗余,而这些冗余恰恰是压缩的天然空间。论文用了三个简单的操作,就把AlexNet从240MB压到6.9MB,压缩率大约35倍;把VGG-16从552MB压到11.3MB,压缩率大约49倍,而且精度几乎不掉。这不是靠某一种黑科技,而是靠三种方法叠加出来的“组合拳”。

这篇论文的所有方法加起来其实就一张图,原论文里那幅pipeline图你只要记住三个阶段就行。

  • 剪枝(Pruning):把绝对值小于阈值的权重直接干掉,把密集网络变成稀疏网络。
  • 训练量化(Trained Quantization):让多个权重共享同一个数值,用聚类中心来代表它们,把每个权重从32bit降到8bit甚至更低。
  • 霍夫曼编码(Huffman Coding):针对非均匀分布的权重和索引做变长编码,把编码冗余再挤掉一部分。

这三个阶段不是随意堆叠的,它们各管一段:剪枝解决“有多少参数要存”的问题,量化解决“每个参数占多少位”的问题,霍夫曼解决“存储符号的熵冗余”问题。三段乘起来,才有几十倍的压缩效果。我后面会逐个拆,但先记住这个大框架。

2. 三阶段压缩流水线:剪枝、量化、编码,为什么偏偏是这个顺序

很多人第一次看这篇论文会想:既然量化能把32bit压到5bit,那直接量化不就完了吗?为什么前面还要做剪枝?这里有一个很容易被忽略的逻辑:先剪枝后量化,量化才有意义。

你可以把网络权重想象成一个公司的员工名单。剪枝相当于裁员——把那些常年绩效垫底、对产出几乎没贡献的员工裁掉。量化相当于把薪资等级从“每个人拿精确到分的工资”改成“所有人只分几个固定等级”——比如S、A、B、C四档,内存里只记录你属于哪一档,薪资数值本身单独放一张表。如果人数没裁过,等级表虽然能省一点,但每个员工都要登记一个档位编号,省下的有限;可如果先裁掉90%的人,剩下的人再按档位编号记录,存储量就非常可观了。

论文的pipeline顺序是严格固定的:剪枝 → 量化 → 霍夫曼编码。这个顺序背后的理由分三层。

第一层:剪枝先降低了需要存储的权重数量,后面所有阶段的作用对象都变小了。全连接层的参数量动辄上千万,剪掉95%之后,再做任何编码处理,计算量和存储量都会少一个数量级。

第二层:量化依赖剪枝后权重的分布特征。剪枝之后剩下的权重,绝对值普遍偏大且分布更集中,这时候做k-means聚类,聚类中心能更好地代表这个分布。如果反着来,先量化再剪枝,量化后的权重会引入误差,此时再做基于阈值的剪枝,很容易误伤原本重要的连接,精度损失会更大。

第三层:霍夫曼编码要依赖符号的概率分布。前面做完剪枝和量化后,模型里存的是聚类索引和少量非零权重值,这些值的分布非常不均匀——有的索引出现几百次,有的只出现几次。正是这种不均匀性给霍夫曼编码提供了压缩空间。如果顺序反过来,在密集、均匀的原始权重上直接做霍夫曼编码,编码表会非常长,几乎压不动。

所以这个三阶段结构其实是一个“层层递进”的压缩链。每一步都在降低下一步的存储成本或提高下一步的压缩收益。论文给的总压缩率不是三项压缩率相加,而是相乘的关系。大家常用的数量级感受是:剪枝通常贡献9~10倍,量化贡献约4倍(32bit到8bit),霍夫曼编码再贡献约1.1~1.3倍,乘起来就能到35~50倍的水平。

3. 剪枝阶段:看起来是“把权重变成0”,实际牵扯三个关键决策

剪枝这步的原理一句话就能说清:把每个层里绝对值低于阈值的权重置为零,然后再训练恢复精度。但真做起来,有三个问题必须处理。

3.1 阈值到底怎么定

论文里不是给所有层设同一个绝对值阈值,而是按层来做。每一层有自己的权重分布,有的层权重整体偏大,有的层偏小。给我印象最深的是:剪枝率要“分层定制”,不能一刀切。论文通过对每个层的敏感性分析来确定各自的剪枝率,有些层可以剪掉90%以上,有些层只能剪一点点。

我在复现时的实际操作是:先统计每一层权重的绝对值分布,按百分位排序,设定目标剪枝率,比如这一层要剪掉80%,就把第80百分位数作为阈值,小于它的权重全部置零。这样做比拍脑袋定一个0.001或0.01这样的绝对阈值要稳妥得多,因为不同层的参数尺度差异可能非常大。

3.2 剪完必须微调,这是精度恢复的关键

权重被置零后,模型精度必然下降,尤其是剪枝率高的层。论文做的不是“剪完就完”,而是进入一个“剪枝-微调”循环。在微调阶段,被剪掉的位置不再参与梯度更新,模型自动在剩下的稀疏连接里重新拟合,补偿被剪掉的那部分信息表达能力。

我实际跑下来,微调的学习率要调小,一般从原训练学习率的十分之一开始,迭代几个epoch就能恢复。论文里AlexNet在剪掉约90%参数量后,通过微调,Top-5准确率能恢复甚至略超剪枝前。这个结论每次我讲给别人听,大家都觉得不可思议,但真的可以复现。

3.3 稀疏连接怎么存:CSR和CSC

剪枝之后网络变稀疏了,但如果你仍然用一个稠密矩阵去存这些权重,那你一点空间都没省,只是多了很多0而已。所以论文引入了稀疏矩阵存储格式。

以CSR(Compressed Sparse Row)格式为例,它只存三样东西:

  • A:非零权重值,按行连续存放。
  • JA:每个非零权重对应的列索引。
  • IA:每一行第一个非零权重在A中的位置偏移。

这样原来用a×b个float存的全连接权重,变成了非零个数 + 非零个数 + 行数级别的存储。简单算一笔账:假设一个1000×1000的层,100万个权重用32bit存需要4MB。如果剪掉80%,非零权重只有20万个。用CSR之后,权重A占800KB,列索引JA如果也用32bit存占800KB,IA占1000×32bit约4KB,合计约1.6MB。对比原来的4MB,省了一半以上。如果索引再压缩成16bit或8bit,那空间会更小。这也是为什么论文后面还要做量化和霍夫曼编码——存储格式只解决“少存0”,不解决“每个数占多少位”的问题。

实际的剪枝在工程上还要维护一个mask张量,记录哪些位置被保留。这个mask在微调时用来屏蔽梯度,让被剪掉的权重永远保持0。这一步很多新手会漏,结果微调完后,原来被剪掉的权重又“复活”了。

4. 量化阶段:用k-means聚类实现“权重共享”,比直接降精度高明在哪

剪枝之后的权重分布虽然稀疏了,但每个权重还是一个32bit浮点数。如果直接把float32转成float8,那属于朴素量化,在直接跑的时候精度损失往往很厉害。论文用的不是这种暴力降位宽,而是训练量化,核心思想是:让多个权重共享同一个数值。

4.1 权重共享的本质:聚类中心代替单个权重

这一步用的工具是k-means聚类。具体来说:把每一个层里所有非零权重拿出来,聚成k类,k就是我们想用的量化等级数。比如k=256,那就意味着这一层的所有权重只有256个不同的取值,通常用8bit就够索引了。

我最初读到这里时有个疑惑:这跟直接用8bit表示权重有什么本质区别?后来动手做过才明白,区别在于聚类中心是“学习”出来的,而非简单的线性等比缩放。8bit直接量化是把数值范围切255段,每一段等宽;但真实世界的权重分布很少是均匀的。k-means的做法是把聚簇中心放在数据密集的地方,数据少的区域用粗糙表示,数据多的区域用精细表示。同样的8bit,信息密度完全不同。

4.2 为什么线性初始化比随机初始化更稳

论文里对比了三种聚类中心初始化方式:随机初始化、基于密度分布初始化、线性初始化。结果显示线性初始化效果最好。

我对这个结论的理解是:在剪枝后的权重分布中,数值较大的权重虽然数量少,但对网络输出的影响非常大。随机初始化可能根本没照顾到这些“少数派”;密度分布初始化则会把过多的中心分配在大批量的小权重区域,浪费了表示能力。线性初始化让中心从权重最小值到最大值均匀展开,至少保证了大权重区域有一个中心能兜住,后续的k-means迭代再自行微调。这也解释了为什么量化后网络精度损失小——少数重要的大权重得到了相对精确的表示。

4.3 量化的真正核心:梯度也要“共享”

这一部分是最容易忽略的,也是论文里“Trained Quantization”和普通“Post-Training Quantization”的本质差异。

普通量化是一刀切,模型训练完之后直接把权重映射到低bit数值,不做任何二次训练。论文的方法则是在量化之后,再对网络做一轮微调,而且微调时用的梯度是按聚类分组求和后再更新的

具体做法是:反向传播时,先算出每个权重对应的梯度,然后属于同一个聚簇的权重的梯度加在一起,得到一个聚类中心的梯度,再更新这个聚类中心的值。这相当于让所有共享同一聚类中心的权重“绑在一起学习”,既保持了低bit存储,又让聚类中心能继续适应数据。

这个机制放在工程上,效果就是量化后的精度损失肉眼可见地小。论文中AlexNet在8bit量化下Top-5精度几乎不掉,甚至有的层压到5bit还能维持不错的精度。而我曾经试过直接训练完转8bit推理的方案,精度能掉2~3个点。差距就在这个“梯度共享微调”上。

4.4 不同层用不同bit:第一层永远最矫情

论文给每一层分配的量化bit数不一样,规律是:靠近输入的层用更多bit,靠近输出的全连接层用更少bit。比如卷积第一层通常给8bit甚至更高,而全连接层可以用5bit、4bit甚至更低。

这背后的原因很直观:第一层卷积直接面对原始输入,它学到的低级特征(边缘、颜色)本身就对噪声敏感,如果这层的权重被粗糙量化,噪声会层层放大。而全连接层参数冗余度极高,就算用4bit去量化,靠前面提取好的高级语义特征,照样能输出合理的分类结果。

我在复现时验证过这个思路:如果把第一层卷积从8bit降到4bit,精度会掉很多;但全连接层从8bit降到4bit,精度几乎不变。所以论文里那句“每一层单独设置压缩策略”不是废话,而是真正的工程学问。

5. 霍夫曼编码:最后“挤一挤”的小收益,背后是熵编码的基础逻辑

前两阶段已经把存储大头解决掉了,霍夫曼编码看起来像是在压榨最后一点剩余价值。但这一阶段的存在有它独立的意义:前面量化之后,模型里保存的权重值和聚类索引,分布并不是均匀的。有的索引频繁出现,有的极少出现。既然分布不均匀,就有熵编码的空间。

用霍夫曼编码的原理说,就是给出现概率高的符号分配短的编码,给出现概率低的符号分配长的编码,最终让平均编码长度接近信息熵。对比固定8bit的方式,变长编码能省不少。

论文对三种东西做霍夫曼编码:非零权重(稀疏矩阵A中的数值)、聚类索引(JA里的索引值)、以及聚类中心表。实际压缩收益大约在10%~20%之间,相比剪枝和量化动辄几十倍的贡献,不算大,但它是“免费的午餐”——解码只需要查一张表,硬件开销很小。

我在实际复现时的一个体会是:霍夫曼编码的收益取决于“稀疏程度”和“量化比特数”的搭配。如果前面量化用的是2bit,中心只有4个,那索引分布非常集中,霍夫曼编码收益就大。如果用8bit量化,256个索引,分布相对平坦,收益就小一些。所以它是一个跟其他阶段强耦合的变量,不能孤立看待。

另外要提醒的一点是,霍夫曼编码是对“存储”的优化,不是对“计算”的优化。推理时你需要把变长编码解码成原始索引或者权重值,再送进矩阵运算。也就是说,压缩率体现在模型文件的体积和内存加载量上,计算量并不会因此下降。论文的侧重点本来就是“fit the model in memory”,而不是“make inference faster”。

6. 论文实验数据再读:35倍和49倍是怎么算出来的,背后有哪些细节值得细看

论文里最常被引用的两个数字就是AlexNet压缩35倍和VGG-16压缩49倍。但如果你只看这两个数字而不去看实验表格,很难理解这些数字是平均出来的,而不是所有层均匀压缩出来的。

6.1 不同层的压缩率差异非常大

以AlexNet为例,前面几层卷积层的参数量其实不大,但计算量大,剪枝率相对保守;后面三层全连接层参数量占绝对大头,剪枝最快,压缩率也最高。有人统计过,全连接层在剪枝后可压缩近30倍以上,再加上量化,有些层单独看能压缩上百倍。

这个现象的本质是:卷积层参数少,几乎每个值都在干活;全连接层参数多,大量连接是冗余的。所以做模型压缩时,不要指望每个层都获得平均压缩率,应该按各层敏感性和参数量分配不同的压缩力度。

6.2 压缩率与准确率的权衡

论文中AlexNet经过Deep Compression处理后,Top-5准确率从原始模型的80.3%左右变化到大约80.0%上下,基本可以看作无损。VGG-16也是类似,压缩49倍后Top-5准确率几乎不降。

这里有一个容易被忽略的重点:能实现无损压缩的前提是,原始模型本身表达力足够冗余;如果你的模型本来就欠拟合,或者训练不充分,压缩的空间和精度余量都会大幅缩小。所以论文实验里的高压缩率,不只是压缩方法厉害,也跟原始模型本身质量有关。我后来试过在一个尚未收敛好的小模型上跑剪枝,结果一切就崩,原因就是模型自己还没学够,根本没有冗余可剪。

6.3 这项技术对后续硬件的影响

论文深度压缩的后续工作也是同一拨人做的,他们把压缩后的稀疏网络和专用ASIC设计结合起来,做出了EIE(Efficient Inference Engine)。这个思路的关键在于:只在硬件里对非零权重做计算,跳过所有零值,从而把计算量也降下来。EIE能在内存受限的硬件上实现比CPU和GPU快一个数量级的推理速度,模型压缩从单纯的“省存储”升级成了“又省又快地跑”。不过这部分是硬件方向,跑题了,有机会可以另开一篇写。

7. 我自己复现Deep Compression时踩过的坑:阈值、微调、聚类初始化的实战复盘

最后这部分,聊聊我自己照着论文思路复现时真实踩过的坑,这些细节论文里一句话带过,但实际做起来非常容易翻车。

第一个坑:剪枝率设得太高,模型直接崩。我一开始天真地以为90%剪枝率很轻松,结果选了错误层一刀切,模型精度从85%直接掉到20%。原因是我在浅层卷积上设了过高的剪枝率。浅层特征图空间分辨率还很高,每个位置的卷积核都承担着一定的特征提取任务,盲目剪掉会让底层的边缘检测和纹理提取能力瞬间瘫痪。后来按层逐层跑敏感性实验,才找到合适的剪枝率区间。经验是先保守剪,观察精度掉得不多再加码,单次剪枝率变化别超过10个百分点。

第二个坑:剪完不复训,等于白剪。有一轮实验我为了赶时间,剪完权重后直接做量化,结果精度惨不忍睹。后来老老实实按照论文的pipeline,剪枝后先微调恢复精度,再量化,量化后再微调一轮,精度才回到正常水平。剪枝、量化后各加一轮微调,是整个流程里最耗时但最不能省的两步。

第三个坑:聚类中心初始化选错,低bit量化时精度波动明显。当量化bit数去到4bit甚至2bit时,随机初始化聚类中心的表现特别不稳定,有时精度掉得离谱。换成线性初始化后,虽然每次结果仍有波动,但整体稳定多了。理论上说,k-means聚类本身就可能陷入局部最优,而线性初始化给了聚类一个比较合理的起点,尤其能覆盖到少而重要的大权重。如果你的实验里量化后精度波动大,先检查聚类中心初始化方式。

第四个坑:用CSR存储但推理内核不支持稀疏计算。模型文件是变小了,但一部署到推理框架里,发现框架根本不支持稀疏矩阵的算子,又得把稀疏矩阵转回稠密矩阵,压缩了个寂寞。所以做落地时,压缩方法和推理引擎要一起考虑,要么选支持稀疏推理的框架,要么自己写稀疏矩阵乘的内核,否则论文里35倍的压缩率只是账面上的数字。

第五个坑:霍夫曼编码的解码开销在CPU上不可忽略。在MCU这种弱计算设备上,霍夫曼解码是逐符号查表的,如果模型每帧推理都要解一遍码,这个开销可能吃掉你省下的存储时间。我当时的处理方式是:在模型编译时预先解码并驻留内存,Flash里存压缩态,运行时放解压态。

写在最后:我重新理解了“深度压缩”这个名词

如果你问我,读完这篇论文最大的收获是什么,我会说:深度压缩的本质,不是把模型“变小”这个结果,而是理解神经网络参数里有大量冗余精度和冗余连接。剪枝是在剔除冗余连接,量化是在剔除冗余精度,霍夫曼编码是在剔除冗余编码。三者不是孤立的技术,而是一套完整的“信息减法”思想。

2016年到现在已经过去很多年,深度学习框架里已经有了各种自动化压缩工具,但Deep Compression这三个阶段的基本框架依然是很多现代压缩算法的基础。我自己后来的项目里,凡是遇到模型塞不进硬件的场景,第一反应就是从这三个维度去审视问题:参数数量能不能剪?位宽能不能降?存储格式能不能再优化?这套思路帮我解决过不少实际问题,也希望这篇文章能帮你少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询