ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Deep Compression解读:剪枝、量化与霍夫曼编码的模型压缩组合拳

Deep Compression解读:剪枝、量化与霍夫曼编码的模型压缩组合拳 去年做某个端侧项目时我在一块嵌入式板上尝试部署一个稍微大一点的视觉模型。板子的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而已。所以论文引入了稀疏矩阵存储格式。以CSRCompressed Sparse Row格式为例它只存三样东西A非零权重值按行连续存放。JA每个非零权重对应的列索引。IA每一行第一个非零权重在A中的位置偏移。这样原来用a×b个float存的全连接权重变成了非零个数 非零个数 行数级别的存储。简单算一笔账假设一个1000×1000的层100万个权重用32bit存需要4MB。如果剪掉80%非零权重只有20万个。用CSR之后权重A占800KB列索引JA如果也用32bit存占800KBIA占1000×32bit约4KB合计约1.6MB。对比原来的4MB省了一半以上。如果索引再压缩成16bit或8bit那空间会更小。这也是为什么论文后面还要做量化和霍夫曼编码——存储格式只解决“少存0”不解决“每个数占多少位”的问题。实际的剪枝在工程上还要维护一个mask张量记录哪些位置被保留。这个mask在微调时用来屏蔽梯度让被剪掉的权重永远保持0。这一步很多新手会漏结果微调完后原来被剪掉的权重又“复活”了。4. 量化阶段用k-means聚类实现“权重共享”比直接降精度高明在哪剪枝之后的权重分布虽然稀疏了但每个权重还是一个32bit浮点数。如果直接把float32转成float8那属于朴素量化在直接跑的时候精度损失往往很厉害。论文用的不是这种暴力降位宽而是训练量化核心思想是让多个权重共享同一个数值。4.1 权重共享的本质聚类中心代替单个权重这一步用的工具是k-means聚类。具体来说把每一个层里所有非零权重拿出来聚成k类k就是我们想用的量化等级数。比如k256那就意味着这一层的所有权重只有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设计结合起来做出了EIEEfficient 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这三个阶段的基本框架依然是很多现代压缩算法的基础。我自己后来的项目里凡是遇到模型塞不进硬件的场景第一反应就是从这三个维度去审视问题参数数量能不能剪位宽能不能降存储格式能不能再优化这套思路帮我解决过不少实际问题也希望这篇文章能帮你少走点弯路。
返回列表