ARTICLE DETAIL

资讯详情

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

Deep Compression解读:剪枝、量化与哈夫曼编码实现模型极速压缩

Deep Compression解读:剪枝、量化与哈夫曼编码实现模型极速压缩 Deep Compression 是宋汉Song Han等人在 2016 年 ICLR 上拿到最佳论文的那篇工作也是我自己接触模型压缩时读的第一篇严肃论文。它的主题一句话就能说清楚把已经训练好的神经网络在不损失精度的前提下尽可能塞进更小的内存。论文用剪枝、量化和哈夫曼编码三个动作把 AlexNet 从 240MB 压到 6.9MB把 VGG-16 从 552MB 压到 11.3MB压缩比分别到了 35 倍和 49 倍。这个数字放在今天依然很能打更不用提 2016 年大家还在普遍认为“深模型就是靠大参数堆出来的”。如果读者现在正要做移动端推理、边缘设备部署或者手里有一个模型因为体积问题跑不上线这篇论文的思路依然值得从头到尾认真捋一遍。这篇文章不是论文翻译更像是一份阅读随笔加复现心得。我会先把 Deep Compression 动了哪些手脚拆开讲清楚然后给出我实际踩过的一些坑和判断依据最后聊一聊这套方法在今天的适用边界。1. 这篇论文到底在解决什么问题1.1 训练完了并不等于能上线训练完一个网络很多人第一反应是看精度第二反应是看参数量但真正做工程的人会立刻估算一件事这个模型在目标设备上能不能跑得起来。一个 VGG-16 全精度模型光权重就 552MB哪怕只往手机里放一次都费劲更别提推理过程中的内存占用和带宽压力。当时 GPU 显存和手机内存都远不如现在宽裕又恰好赶上深度学习开始往移动端铺开模型压缩就成了从“实验室能跑”到“产品能上”之间那个绕不开的坎。Deep Compression 的出发点很实际训练好的模型里有大量冗余不是每一个权重都值得用 32 位浮点存也不是每一个连接都值得保留。它解决的核心问题就是如何在尽量不伤精度的前提下把模型的存储体积压到原来的几十分之一。注意这里的重点是存储体积。它不像知识蒸馏那样重新训练一个小模型也不像低秩分解那样改变网络结构而是直接在已经训练好的大模型上做“无损减重”。1.2 为什么是剪枝量化编码的组合拳模型压缩不是新概念但 Deep Compression 的贡献在于把三个不同层面的压缩手段串成了一条流水线先用剪枝把权重数量降下来再用量化把每个权重需要的比特数降下来最后用哈夫曼编码把前两步产生的非均匀分布再榨一遍。每一步单独拿出去都有人做过但把它们按顺序组合并且在每一个阶段都配合重训练来恢复精度这个系统性的做法在当时是很有说服力的。这里面有个关键逻辑三种手段压缩的是不同维度。剪枝压缩的是“有多少个权重”量化压缩的是“每个权重占几个 bit”哈夫曼压缩的是“存储这些权重索引时能不能继续省”。如果只做剪枝非零权重还是要用 32 位浮点存体积压得有限如果只做量化模型里那些接近零的权重照样占着存储位如果只做编码原始权重分布可能不够偏编码收益不明显。三者叠加后才能出现 35 倍、49 倍这种量级的压缩比。这也是我读这篇论文时最大的一个体会不要指望单一技巧有魔法工程上的收益往往来自多个环节的叠加。Deep Compression 的每一个阶段都不算花哨但组合起来成了当年模型压缩领域绕不开的结果。2. 三步走Deep Compression 的核心方法拆解2.1 第一步剪枝先把无关紧要的连接删掉剪枝的基本思想很朴素训练完后把绝对值低于某个阈值的权重直接置零。论文里把这一步叫作“学习连接”意思是网络在训练过程中自己告诉我们哪些连接是重要的哪些是可有可无的。一个权重绝对值很小说明它对最终输出的影响大概率有限即使删掉只要后续重训练一段网络通常能把损失补回来。实际操作上剪枝不是只做一次就结束。论文的做法是迭代式的训练一个网络剪掉一部分权重再重训练然后再剪掉一部分再重训练。这样反复多轮最终的稀疏率可以做得比较高同时精度不会断崖式下跌。对比一次性把剪枝阈值拉满迭代式剪枝更稳因为网络有充足的时间去适应和调整剩余权重的分工。剪枝还有一个容易忽略的点剪完之后的权重需要用小学习率重训练一段时间。这里的重训练不是从头训练而是在稀疏结构上微调让幸存下来的权重承担起被删掉的那部分连接原本的责任。学习率太大会把已经学好的特征空间冲乱太小则恢复不了精度。我自己的经验是取原始训练学习率的十分之一到五分之一起步比较稳妥后面再用余弦或阶梯式下降。剪枝之后网络变成稀疏网络。如果直接拿普通稠密矩阵存储剪枝反而会让模型文件更臃肿因为你还得多存一份坐标信息。所以论文用了稀疏矩阵格式来存储最典型的是 CSR/CSC。简单说不存那些零值只存非零值的具体数值和它所在的位置。这也是为什么剪枝率要足够高否则索引开销会吃掉空间收益。2.2 第二步权重量化用聚类把浮点数换成索引权重量化是 Deep Compression 里技术细节最多的部分。核心思路是把每一层的权重做 k-means 聚类让同一簇内的权重共享同一个质心值然后不再保存每个权重的原始浮点数而是保存它所属簇的索引。假设一层的权重被分成 32 个簇原来每个权重需要 32bit 存储现在只需要 5bit 索引如果分成 256 个簇每个权重只需要 8bit 索引。质心表本身很小一般一层也就几百个浮点数几乎可以忽略。但这里有一个关键问题聚类完之后量化误差必然存在精度会不会掉论文里给了一个很重要的解法——对质心做“重训练”。也就是说量化不是一次性把权重钉死而是把量化后的质心当作可训练参数继续用反向传播去更新它。因为梯度可以按照簇做聚合一个簇内所有原始权重对应的梯度加起来就是这个簇质心应该更新的梯度方向。这个操作在论文里叫 trained quantization。质心的初始化方式也很有讲究。论文对比了随机初始化、密度初始化和线性初始化发现线性初始化效果最好。原因是权重分布通常两头稀疏、中间集中如果用密度初始化质心会过度集中在权重值密集的区域而把那些绝对值大但数量少的权重分得很粗糙线性初始化则均匀覆盖整个权重范围反而能保留更多极端值的信息。这个结论在我自己复现时体感非常明显换用线性初始化后量化损失立刻小了一截。选择簇数 k 的时候不建议所有层都用同一个值。全连接层参数多、冗余高可以压到 4bit 甚至更低卷积层靠近输入对量化误差更敏感一般保留 8bit 或者更多。论文里的做法是基于层敏感度分析来分配比特数而不是一刀切。如果你刚开始做可以先统一用 8bit 跑通流程再对精度敏感层逐步调大 bit 数。2.3 第三步哈夫曼编码把不均匀分布再榨一遍哈夫曼编码是最后一道工序属于无损压缩。经过剪枝和量化之后网络中同时存在几类需要存储的数据非零权重索引、质心索引、质心值、稀疏矩阵的行偏移和列坐标。这些数据的分布并不均匀比如质心索引里某些值出现频率很高某些值很少出现。哈夫曼编码正好擅长处理这种不均匀分布它给高频值分配短码字给低频值分配长码字从而在整体上进一步降低平均存储位数。很多人会小看这一层觉得哈夫曼编码是老掉牙的技术。但在 Deep Compression 的管线里编码确实还能贡献不少压缩率。尤其是量化之后索引值会出现明显的长尾分布加上偏置和特殊标记等边角数据用哈夫曼编码往往能再省掉 20% 到 30% 的空间。要注意的是哈夫曼编码是变长编码读取时需要查表解码所以在推理时不是直接读取内存里的某个数组就能用这和浮点权重数组的访问模式完全不同。这带来一个工程上的取舍模型文件体积可以压得很小但推理引擎不能直接吃哈夫曼编码后的数据。一般部署时会把编码后的模型放在磁盘加载到内存后再解成量化索引表或稀疏矩阵结构。也就是说哈夫曼编码省的是“存储空间”不一定直接省“运行时内存”。如果你只关心运行时显存/内存占用需要分别统计压缩文件大小和实际加载后的结构大小不能混为一谈。2.4 三步串联后的一次手算示例用一个简化的全连接层来体会一下压缩比是怎么攒出来的。假设这一层有 100 万个权重原始 32bit 浮点存储体积是 4MB。剪枝后只保留 10% 的非零权重那么有效权重变成 10 万个。如果不压缩这 10 万个浮点值加上索引坐标存储量大约是 10 万 × 4Byte 数值 10 万 × 4Byte 坐标也就是 80 万字节约 0.76MB比原来的 4MB 少了 5 倍左右。接着做量化把这 10 万个非零权重用 256 个质心表示。每个权重只要存一个 8bit 索引数值部分变成 10 万字节约 0.095MB。坐标部分如果用 16bit 存列号可以按列稀疏的方式压缩假设再降到 5 万字节。质心表 256 个浮点也就 1KB 左右。整体大约 0.15MB相比原始 4MB 已经压了 26 倍。最后再做哈夫曼编码索引和坐标里高频值能用更短码字整体再省 20% 左右最后可能到 0.12MB压缩比接近 33 倍。上面这个例子没有严格照搬论文里的某层数据但能说明一个道理剪枝决定数量量化决定比特数编码负责吃掉最后一点冗余。剪枝率够高后续量化收益才会明显如果剪枝率只有 50%再上量化也很难达到 30 倍以上的压缩比。3. 复现 Deep Compression 时最该注意的几个细节3.1 剪枝不是一刀切迭代式剪枝才能稳住精度我看很多初学者复现时会犯同一个错误训练完之后把权重绝对值小于某个阈值的全部置零然后直接评估发现精度掉了好几个点顿时觉得剪枝没用。这不奇怪因为一次性删太多权重网络根本没有机会重新分配信息。论文里的剪枝是一个“剪一小部分、重训练、再剪一小部分”的循环过程。具体操作上可以这样设置每一轮剪掉当前剩余权重里绝对值最低的 10% 到 20%然后重训练几个 epoch。阈值不一定要用固定绝对值也可以按每层权重的分位数来确定。因为不同层的权重尺度差异很大用一个全局阈值容易把某些层的绝大部分权重都删掉而另一些层基本没动。按分位数剪更公平也更容易控制每轮的稀疏率。迭代剪枝还有一个隐藏好处网络的稀疏结构不是一次定死的而是随着训练慢慢趋于稳定。我在复现时观察过前几轮剪枝后重训练精度会出现小波动但到后面波动越来越小说明剩下的连接已经足够表征网络的主要计算路径。这时候你再加大剪枝比例精度才开始明显下降也就说明到了当前结构的稀疏极限。3.2 量化的簇数和初始化方式直接影响结果量化阶段最常见的问题是“明明剪枝后精度很高一量化就掉了两个点”。这时候先别急着怪聚类算法先检查簇数 k 和初始化方式。我复现下来线性初始化几乎总是优于随机初始化这一点论文说得很清楚我实际做的时候也验证了。尤其是当权重分布有长尾特征时随机初始化的质心很容易“浪费”在密集区域导致极值附近的量化误差很大。簇数 k 的选择要结合层敏感性。简单粗暴的做法是先所有层统一 k256看精度变化再把精度掉得最明显的那一层的 k 调到 512 或 1024其他层可以降到 64 或 32。这种按需分配的策略比全网络统一 bit 数要高效得多。对于冗余极高的全连接层我甚至在实验里把 k 降到 16精度几乎不受影响。需要注意的是量化之后的重训练不能省。光聚类不更新质心量化误差会一直存在更新质心之后网络会重新学会用这些更少的取值表达同样的功能。实践中量化重训的 epoch 不一定要很多但学习率要小心过大会让质心浮动太厉害。我用的时候会把学习率降到原来的 0.01 倍量级效果比较稳。3.3 稀疏矩阵存储格式是压缩率的隐藏推手很多复现笔记不会仔细讲稀疏存储但这一步直接决定最终压缩文件能多大程度反映剪枝收益。CSR 格式的大致做法是用一个 values 数组存所有非零权重值用一个 col_indices 数组存每个值对应的列号再用一个 row_ptr 数组记录每一行从哪里开始。这样做的好处是不需要为每一行都保存行号因为行号其实可以由 row_ptr 推导出来。但 CSR 也有代价如果每一行非零元素都很少col_indices 的开销会相对偏高。Paper 里讨论过的还有 CSC 和其他按维度组织的存储方式。我的建议是剪枝率超过 90% 之后CSR/CSC 的索引开销会更明显这时候可以考虑用分组稀疏或者块稀疏来降低索引数量。块稀疏的意思是以固定小块为单位剪枝而不是单个权重级别剪枝这样虽然会牺牲一点稀疏率但索引更整齐硬件访问也更友好。另外有很多开源实现会把剪枝后的权重重新排列让非零权重连续分布方便向量化访问。这种重排列不改变模型语义但对推理速度很重要。如果只是追求压缩率不考虑推理速度这一步可以跳过如果想要真正部署索引内存布局和缓存友好性比纸面压缩率重要得多。3.4 训练与部署的内存口径要分清读论文时经常会看到“压缩率 35x”这种表述但要注意它指的是模型文件大小还是权重张量大小还是运行时内存占用。Deep Compression 的论文主要报告的是存储层面的压缩也就是把模型参数保存到磁盘时的大小。这个数字和前向推理时的峰值内存不是一回事。推理时你仍然需要把权重解码成可以参与计算的格式。如果我只保存量化索引那么推理时要么实时查表转成浮点要么提前展开成浮点权重这两种方案的内存占用完全不同。实时查表省内存但可能增加计算量提前展开速度快但内存又变大了。所以做工程时一定要拆开三个指标模型文件体积、加载后的参数体积、网络运行时峰值内存。三者可能差别很大不能只看论文标题里的压缩倍数。4. 效果指标、适用场景与后续影响4.1 不同网络上的压缩效果对比论文给出的最典型结果到现在也经常被人引用AlexNet 从 240MB 压到 6.9MB压缩比约 35 倍VGG-16 从 552MB 压到 11.3MB压缩比约 49 倍。两者在 ImageNet 上的精度都基本没掉。这个结果放到当时是非常震撼的因为 VGG-16 的 1.38 亿参数被压到两千多万已经接近一个小型网络的体量但精度没有明显损失。如果把三个阶段拆开看剪枝对 AlexNet 的贡献大约是把权重数压到原来的九分之一量化再把每个权重从 32bit 压到 5bit 左右最后哈夫曼编码再贡献约 20% 的额外压缩。每个阶段的贡献不是均等的具体比例取决于网络结构和冗余程度。对于冗余更高的全连接层剪枝和量化的收益会更大对于卷积层参数少一些但计算量占比高压缩时更要谨慎。4.2 什么场景最适合用 Deep Compression如果你有一个已经训练好的大模型主要瓶颈是存储空间而不是在线推理延迟那 Deep Compression 的思路非常合适。比如模型要随 App 分发给用户或者要存进嵌入式设备的 Flash 里体积就是第一优先考虑的问题。MobileNet、EfficientNet 这些本身很小的模型当然可以直接部署但如果业务要求老模型不变、体积还要降下来Deep Compression 这种“事后压缩”路线反而更省事。反过来如果你的目标是降低延迟单纯压缩权重文件帮助有限。稀疏矩阵虽然在理论上可以减少计算量但很多硬件对稀疏计算支持不好还不如直接用稠密小模型。做这类需求时我会优先考虑结构剪枝、通道剪枝或者知识蒸馏因为它们能直接改计算图。Deep Compression 的权重级剪枝更像“存储压缩”要在支持稀疏计算的自研推理引擎里才能同时吃到加速收益。4.3 它给后续模型压缩研究留下了什么Deep Compression 最深远的影响是让“训练后压缩”成为模型部署的标准思路也启发了后面一大批低比特量化工作。今天常用的 8bit 量化、混合精度量化、PTQ/QAT思想上多少都能从这篇论文里找到影子。它对“共享权重用索引存储”的处理方式后来也演进成各种基于查找表的量化方案。可以说现在工业界做模型压缩时面对的很多问题这篇论文早就用一套相对完整的框架梳理过一遍。不过也要看到它的边界论文的压缩重心在权重存储对卷积计算的加速效果没有系统讨论它对超大规模 Transformer 这类结构直接套用权重级剪枝的收益也未必有当年对 VGG 那么明显。之后的剪枝研究逐渐从“单个连接”走向“通道/注意力头/层”这种结构化剪枝目的就是为了在可编程硬件上真正获得加速。换句话说Deep Compression 的思路是“先考虑能不能存得下”而后续工作是“还要考虑能不能算得快”。5. 常见问题排查与个人复现心得5.1 问题剪枝后精度波动很大先看是不是一次性剪太多。建议把总剪枝比例拆成 5 到 10 轮每轮剪一点重训练一点。再看阈值是否按层独立设置如果某个层权重分布范围广全局阈值会把这层删空。还有一种情况是重训练学习率没调小导致已经收敛的模型被重新冲散。我自己的经验是剪枝重训阶段学习率保持原训练学习率的 1/5 到 1/10并且加一点权重衰减精度通常能稳定回来。5.2 问题量化后损失比预期高优先检查质心初始化。换成线性初始化很多时候损失直接降一半。其次检查簇数 k不同层不该用同一个 k敏感层要多给簇如果 Conv1 这种靠近输入的层掉点明显把它的 k 提到 512 甚至 1024其他层保持低位即可。还要检查量化前有没有先统一权重分布有些实现会先对权重做标准化再聚类最后反标准化这样能减少极端离群值对聚类的干扰。5.3 问题压缩后文件是小了但推理没变快这非常正常因为文件体积和推理速度是两个维度。稀疏权重如果不被硬件里的稀疏指令支持运行时还是要零填充成稠密矩阵才能计算速度自然没提升。要提速需要确认推理引擎是否支持稀疏矩阵乘或者改用结构化剪枝让稀疏模式落在固定块里。量化和哈夫曼编码也会增加解码负担尤其变长编码在 GPU/CPU 上并行度不高部署时最好先把权重解码成紧凑的定长格式再推理。5.4 我踩过的一个“索引类型”的坑最后分享一个我自己实际踩过的坑。做量化存储时我以为把索引保存成uint8就够了结果有些层的 k 是 512索引值最大到 511uint8直接溢出。一开始很难察觉因为精度只掉了一点排错排了很久。后来把所有层的索引类型统一成uint16再在上层把超过 255 的索引单独处理问题才解决。建议大家在做量化时单独写一个检查函数遍历所有层的索引最大值确保和k-1对得上。Deep Compression 这篇论文我前前后后读过很多遍每次重读都会有一些新体会。第一次读是惊叹于三个简单操作叠加带来的压缩比后来自己做复现重新去抠剪枝率、质心初始化、索引存储这些细节才开始真正理解论文里那些看似被轻轻带过的实验设置其实才是工程落地的关键。如果你也想在项目里尝试模型压缩我建议不要一上来就追最新的大模型量化方案先把 Deep Compression 这一套流程亲手跑通一遍后面再看任何优化方法都会有底得多。
返回列表