A同学上周跟我吐槽,他训练好的图像分类模型塞进手机直接内存爆掉,一张图要跑八百多毫秒,App根本带不动。我告诉他,要解决这种问题,绕不开三个词:剪枝、量化、知识蒸馏。注意,剪枝是“剪”不是“减”,很多人打成“减枝”,这里先纠正一下。这三招是业内做 AI 模型压缩最成熟的三板斧,理解透了,大多数模型“跑不动”的部署难题都能迎刃而解。
这篇文章我不打算拽名词,而是用大白话把三招的原理、适用场景、动手步骤和容易踩的坑一次讲清楚。不管你是刚入门的算法学生、做端侧部署的工程师,还是单纯好奇大模型为什么能塞进手机里的爱好者,这篇都值得收藏备用。
1. 模型为什么要减肥:先弄懂三招在解决什么问题
1.1 一个让所有落地开发头疼的部署现场
先说说模型这回事。一个训练好的深度学习模型,本质上就是一堆数字,也就是所谓的权重参数。图片进来,先做一层又一层的乘加运算,这一堆数字就是运算的系数。模型的表现好坏,基本取决于这堆数字的规模和排列组合方式。
而部署到手机、嵌入式设备上,就撞上了三座大山:体积、内存和延迟。一个视觉模型动辄上亿参数,如果每个参数用常用的 FP32 浮点数保存,占 4 字节内存,那么一亿参数就是 400MB 的模型文件。服务器上有大内存和高性能 GPU 卡可以随便造,但手机总共就那么几个 GB 内存,还要和 App 其他功能抢资源;摄像头、路由器这些嵌入式设备更惨,可用内存常常只有几百 MB。参数规模摆在那里,不是靠优化代码就能解决的,只能从模型本身下手。
1.2 为什么不直接训练一个小模型
有朋友会问:那我不训练大模型,直接训练一个小模型不就行了?听起来很直接,但实际操作会撞上一个天花板:模型容量越大,能学到的特征越丰富;小模型虽然学得快、跑得快,但表达能力有限,在复杂任务上很难达到大模型的精度。一个视觉分类任务,小模型硬训可能只能到 82%,大模型却能到 91%。
模型压缩的思路恰恰反过来:先让大模型把一个任务学到尽可能好,再想办法从中提炼出一个精简但能力接近的小模型。这就像一位资深教授花十年整理的厚讲义,压缩成几页重点笔记交给助教用。助教的水平虽然达不到教授本人,但应付日常考试绰绰有余。这个思路上的转变,是理解后面所有技术的前提。
1.3 三大神技的分工一览
在展开讲细节之前,先给个全局图,三种技术分别对应日常生活中的三种行为:
| 技术 | 生活类比 | 主打效果 | 主要副作用 |
|---|---|---|---|
| 剪枝 | 公司裁员,裁掉贡献低的员工 | 体积减小、计算量下降 | 剪太多精度会崩,需要微调恢复 |
| 量化 | 高清照片转成低色彩数图片 | 体积缩小、推理变快 | 数值精度受限,对分布极端情况敏感 |
| 知识蒸馏 | 名师带徒弟,传授火候和手感 | 小模型精度被抬高 | 需要训练一个大模型当老师,成本偏高 |
三招不是同一个问题的不同解法,而是从不同维度压缩模型:剪枝削减结构,量化压低精度,蒸馏则是在缩减体积的同时尽量保住能力。理解了这个分工,后面组合使用的时候就不会乱套。
2. 剪枝(Pruning):把神经网络里混日子的连接裁掉
2.1 神经网络为什么会“养闲人”
剪枝是所有压缩技术里直觉最直观的一种:把不重要的参数删掉。但你可能会问,为什么模型里会存在不重要的参数?
神经网络训练完成之后,你会发现大量权重的绝对值非常小,甚至接近于零。前向传播就是无数个乘加运算的叠加,如果某个权重是 0.00001,它对最终输出结果的影响几乎可以忽略。把这种权重去掉,模型输出基本不变。麻烦的是,这种“弱权重”在模型里绝非个例,而是能占到相当高的比例。
这种现象背后有个很有意思的规律:神经网络是出了名的过度参数化,参数量通常远超过任务本身所需。大网络更容易训练、泛化也更好,这不算浪费,而是它的学习策略。近年有个著名的彩票假说,大意是一个大网络里往往藏着若干小而精的子网络,把这些“中奖号码”单独提取出来训练,效果也能逼近原网络。剪枝干的事情,本质上就是寻找并保留这些中奖号码。
2.2 两类剪枝:稀疏化和结构化,部署体验天差地别
剪枝按粒度可以分为两大类,它们的部署表现差异极大,搞混了很容易白忙活一场。
第一类叫非结构化剪枝,也叫稀疏化剪枝。它把不重要的权重逐个置零,结果矩阵变得稀疏,到处是坑坑洼洼的零元素。模型文件确实变小了,但问题在于推理阶段:如果你的硬件不支持稀疏矩阵加速,计算量并没有实质下降,反而可能因为频繁判断“这个元素是不是零”而变慢。这类剪枝适合研究场景,或者配合某些专门针对稀疏张量优化过的加速硬件使用。
第二类叫结构化剪枝。它整块地剪,比如直接删掉一个卷积层的某个卷积核、某个通道或某个残差块。剪完之后,剩下的矩阵依然是形状规整的稠密矩阵,各种通用推理框架都能直接受益,因为计算量在结构层面真的减少了。
| 剪枝类型 | 剪枝粒度 | 压缩率上限 | 部署加速效果 | 推荐场景 |
|---|---|---|---|---|
| 非结构化 | 单个权重 | 高 | 依赖稀疏加速硬件 | 研究、特殊芯片 |
| 结构化 | 通道/卷积核/层 | 中等 | 通用算子直接加速 | 移动端、边缘端 |
对于手机、嵌入式这类主流场景,我的建议很明确:优先做结构化剪枝。性价比高,落地阻力小。
2.3 剪枝实操链路:训练、剪枝、微调三件套
剪枝不是简单地把参数置零就完事,标准的流程可以总结为三步:
第一步,准备一个精度达标的基准模型。别拿一个还没收敛的模型去剪,否则剪完之后你分不清是剪坏了还是模型本来就没学好。基准模型的精度和稳定性是第一优先级。
第二步,给参数排重要性。最朴素也最有效的方法是对每个通道的权重求绝对值之和(L1 范数)。一个通道的权重绝对值总和小,说明它经过运算后对输出的贡献偏弱,优先剪掉。进阶做法是用 BN 层的缩放因子 γ 作为通道重要性的代理指标,γ 越小的通道越倾向于被剪掉。
第三步,按比例剪掉一部分,然后微调。新手建议从剪 20% 到 30% 起步,先把流程跑通,再去追求高压缩率。微调时把学习率设成预训练阶段的十分之一到百分之一,让模型慢慢找回精度。
还需要强调一个观念:迭代剪枝比一次性剪枝稳定得多。策略是“剪一点、微调、再剪一点”,每次剪 20%,微调一到几个 epoch,观察精度变化,再决定是否继续。伪代码大概是这样的:
for round in range(总轮数): 按重要性排序,剪掉当前模型 20% 的通道 用低学习率微调 1-2 个 epoch 记录当前精度,如果下降幅度超出预期就停止2.4 剪枝最容易踩的坑
剪枝的坑不少,但大多可以提前避开。下面这几条是我反复踩过之后总结的:
第一,一次剪太多。新手最容易犯的毛病是奔着目标压缩率一口气剪掉 80% 甚至 90%,结果模型直接失去表达能力,后面怎么微调都救不回来。稳妥的做法是渐进式剪枝,把总目标拆成几轮小目标。
第二,剪完不微调。剪完立刻部署,精度通常掉得厉害。原因很简单:剪枝相当于强行改变了网络结构,神经元的协作方式被打乱了,必须给小模型一段适应期。微调不是可选项,是必选项。
第三,剪枝不考虑层敏感度。不同层的冗余程度差别很大,浅层学的是边缘、纹理这些基础特征,往往比较敏感,动不动就伤筋动骨;深层学的是语义信息,冗余更多,可以多剪。建议动手前先做一次逐层敏感性分析:每层单独剪掉一小部分,观察精度变化,敏感度高的层少剪,冗余度高的层多剪。
第四,非结构化剪枝的部署误区。别以为模型文件变小了,线上速度就一定会跟上。如果目标设备没有对应的稀疏算子加速,反而更慢。先查清楚部署平台的算子支持情况,再决定用非结构化还是结构化方案。
3. 量化(Quantization):用整数的思维做浮点的事
3.1 量化到底在干什么:从 FP32 到 INT8
量化是另一种完全不同的压缩思路。模型的权重默认用 FP32,也就是 32 位浮点数保存。浮点数的优点是精度高,能表达小数,适合训练时的各种梯度计算;缺点是占空间大,乘加运算也相对较慢。
量化做的事情,就是用更少的二进制位去表示一个“差不多”的数值。最经典的做法是把 FP32 变成 INT8,也就是 8 位整数。单个参数占用的内存从 4 字节变成 1 字节,模型体积直接缩到四分之一,推理时的内存占用也大幅下降。
整个映射过程不算复杂。假设一组浮点权重分布在 0 到 1 之间,我们准备 256 个整数刻度(0、1、2、……、255),每个浮点数就近映射到一个整数,这叫线性量化。实际操作时会记录两个参数:缩放因子 scale 和零点 zero_point,推理时再通过它们把整数还原成近似的浮点值。
提示:量化的本质是丢精度换资源。关键是“丢掉的精度”影响多大,以及换来多少收益。去做浮点和整数之间的转换时,最怕的不是丢精度,而是对数值分布缺乏了解导致大量值被粗暴截断。
3.2 三种量化方式怎么选:动态、静态、量化感知训练
量化的实现方式不只一种,我平时在项目里主要用三种,它们的精度、速度和实现成本差异很明显。
第一种叫训练后动态量化。操作最简单:加载模型时把权重提前量化为 INT8,但推理时激活值还是以浮点形式临时量化再算。好处是无需准备校准数据,实现成本极低,精度损失通常也能控制在可接受范围;缺点是激活的量化参数是运行时现算的,没法提前离线优化,完整整数运算的提速优势发挥不出来。
第二种叫训练后静态量化。它比动态量化多一步:准备一个几百到几千张图片的校准集,跑一遍模型的中间结果,统计每一层激活值的分布范围,提前确定量化参数。推理时激活也全程用整数运算,加速效果最明显。代价是需要额外收集校准数据,并且对激活值里的极端离群值比较敏感。
第三种叫量化感知训练,英文缩写 QAT。它在训练过程中插入模拟量化的节点,让模型前向和反向传播时“假装”自己已经被量化了,从而主动适应低精度表示。这是精度损失最小的方法,通常能逼近原模型的水平,代价是要完整重训一遍,成本高一些。
| 方式 | 是否需要重训 | 速度收益 | 精度损失 | 综合成本 |
|---|---|---|---|---|
| 动态量化 | 否 | 中 | 小 | 最低 |
| 静态量化 | 否 | 高 | 中 | 中等 |
| 量化感知训练 | 是 | 高 | 小 | 较高 |
如果你是新项目,我的建议是先动态量化试水,评估收益是否够用;不够再上静态量化;实在不行才考虑量化感知训练。有些对数值波动特别敏感的任务也可以直接跳过前两步用 QAT,省得来回折腾。
3.3 量化实战中最容易翻车的几个细节
量化看着简单,翻车的细节可不少。第一条是离群值问题。激活值偶尔会冒出一个特别大的数字,如果把量化范围定成全量程,普通数值能分到的“刻度”就被大大压缩,量化精度急剧下降。解决办法也很成熟:校准的时候不取 min/max,而是取 99.9% 分位数,把超出的极值直接裁剪。
第二条是校准集质量。静态量化的效果完全取决于校准集有没有代表性。几百张图太少不行,全是同一类样本也不行。要覆盖真实场景里的各种亮度、角度、噪声分布,尽量接近线上数据。
第三条是量化粒度的选择。可以整层整套用同一个 scale,这叫 per-tensor;也可以每个输出通道单独设 scale,这叫 per-channel。per-channel 的数值刻画更细腻,精度损失更小,但部分硬件和推理引擎不提供支持,落地前要确认清楚。
第四条是混合精度思路。不是每一层都必须塞进 INT8。对量化特别敏感的层,比如注意力机制或某些关键全连接层,可以保留 FP16 或 INT16,其他层用 INT8,灵活组合往往能达到更好的整体效果。
最后再强调一遍:量化之后的加速指标必须真机实测。某个推理引擎声称支持 INT8,不代表所有算子都被优化过,跑通之后延迟暴涨的情况我也见过不少。
4. 知识蒸馏:让大模型当师傅,小模型当学徒
4.1 蒸馏的核心原理:你要学的不是答案,是老师解题的思路
前两种技术都是直接对着模型“动手”,蒸馏则是一种训练方式的革命。知识蒸馏的思路在 2015 年的一篇经典论文里被系统地提出来,此后迅速成为工业界的标配。
思路非常朴素:拿一个能力很强的大模型当教师,去指导一个小模型学习。但关键差异在“学什么”。普通训练只给小模型一个硬标签,比如“这张图是猫”,小模型学到的是非黑即白;教师模型输出的却是一整套概率分布:0.92 的猫、0.05 的狗、0.02 的狐狸、0.01 的鸟。这套分布叫软标签。
别小看这套软标签,它把模型对“这张图有多像猫、多像狗、多像狐狸”的判断完整交代了一遍,相当于把类别之间的亲疏关系也教给了小模型。学硬标签的小模型只能记住“非猫即狗”,学软标签的小模型却能潜在地理解“猫和狗在某些场景下脸部轮廓、毛发纹理很接近”这类高阶语义。这就是蒸馏比普通训练效果好的根本原因。
打个比方,老会计带新人,不是只给一个最终答案“这笔账要扣除”,而是把判断依据、演算思路、常见陷阱一并拆开讲。新人学到了方法论,将来遇到没见过的情况也知道怎么下手。
4.2 温度参数 T:软标签的“软化开关”
软标签不是凭空生成的,它靠一个温度参数 T 来调节。具体来说,softmax 的公式会做一点变形,对每个 logit 先除以 T 再求 softmax:
qi = exp(zi / T) / sum(exp(zj / T))当 T = 1 时,这就是普通 softmax,输出的是硬朗的概率分布;T 越大,分布越平坦,原本悬殊的类别概率被压缩,那些“微弱但关键”的信息也能浮现出来。
T 选多大很讲究。T 太小,软标签退化成硬标签,蒸馏失去意义;T 太大,所有类别概率趋向均匀,等于给学生喂了一堆没有信息量的噪声。以我的经验,图像分类任务里 T 在 3 到 10 之间是个可以接受的范围,具体数值需要做个简单实验对比。
完整的蒸馏损失由两部分组成:一部分是学生模型直接学硬标签的任务损失,另一部分是学生输出分布与教师软标签之间的相似度损失,也就是 KL 散度。两项按一定比例相加,一般蒸馏损失的权重 λ 取 0.5 左右。训练时,学生模型在预测阶段同样要做除以 T 的处理,然后再计算与教师软标签的差异。
4.3 蒸馏的三种玩法和实用建议
蒸馏的落地形态有离线、在线和自蒸馏三种,按需取用。
离线蒸馏是工业界最常用的做法。先用教师模型对一批数据前向推理,把软标签全部存下来,然后用“硬标签 + 软标签”训练学生模型。好处是流程清晰、成本可控,教师只需要跑一次推理,后面训练不再依赖它。
在线蒸馏则适用于没有现成预训练大模型的场景。教师和学生同步训练,教师也随着训练过程不断更新,全程端到端。听起来更优雅,但训练稳定性要求高,超参也更敏感。
自蒸馏更有意思,一个模型自己教自己,比如用深层的输出教浅层,或者用后几轮 epoch 的参数教前期版本。操作起来便宜,常能带来一两个点的精度提升。
关于蒸馏,还有几条实用建议值得记下来:
第一,教师模型不是越大越好。教师和学生能力差距过大时,学生反而学不动。教师模型的精度和学生目标精度差距在十几个点以内往往效果最好,超过这个范围,蒸馏增益会大幅缩水。
第二,不是所有任务都适合蒸馏。如果任务标签本身非常明确,类别间几乎不存在“亲疏关系”,或者模型已经强到输出分布接近 one-hot,软标签能提供的信息有限。
第三,学生模型结构要提前规划。架构可以比教师瘦一圈,但别瘦到完全没有表达能力。否则下面再叠加剪枝和量化时,模型连基本容量都不够了。
5. 组合使用:蒸馏-剪枝-量化的一条龙方案
5.1 三招的正确顺序和搭配逻辑
三招完全可以组合使用,而且组合起来效果往往更好。我的推荐顺序是:大模型训练 → 知识蒸馏 → 结构化剪枝 → 量化。
这个顺序是有讲究的。蒸馏负责在模型还“活着”的时候把精度拉高,为后面剪枝和量化预留出精度损失预算;剪枝放在量化之前,是因为剪枝会改变模型的结构,先让它适应好结构变化再去做数值压缩,损失不会叠加;量化放在最后,因为它动的是数值表示精度,对已经精简过的模型做映射,影响最可控。
当然,这是理想链路。实际项目中如果时间紧张,完全可以先做动态量化试水,把主要瓶颈解决了,再逐步叠加剪枝和蒸馏。先挑成本最低、收益最高的做,是个朴素但高效的原则。
5.2 模拟项目:从 400MB 压缩到 50MB 的完整链路
为了让你更直观地感受三招组合的效果,我用一个模拟的图像分类项目来说说完整的链路。假设原始模型使用 FP32 保存,占用 400MB,在测试集上准确率 89.2%,单张图片推理延迟约 350ms。
先把模型做蒸馏。教师就是原始大模型,学生设计为约 120MB 的轻量结构,用软标签加硬标签混合训练,T 取 5,蒸馏损失权重 λ 取 0.6。蒸馏后准确率 88.5%,体积降到 120MB,延迟降到约 165ms。
接着做结构化剪枝。用通道 L1 范数评估重要性,每轮剪 20%,微调两个 epoch,重复四轮,最终剪掉约 40% 的通道。微调后准确率 88.1%,体积降到约 72MB,延迟降到约 105ms。
最后做 INT8 静态量化。准备 500 张有代表性的校准图,激活值取 99.5% 分位数做裁剪,个别敏感层保留 FP16。量化后准确率 87.4%,体积降到约 18MB,单张推理延迟降到约 36ms。
| 阶段 | 准确率 | 体积 | 单张延迟 |
|---|---|---|---|
| 原始 FP32 模型 | 89.2% | 400MB | 350ms |
| 蒸馏出学生模型 | 88.5% | 120MB | 165ms |
| 结构化剪枝 + 微调 | 88.1% | 72MB | 105ms |
| INT8 量化 | 87.4% | 18MB | 36ms |
整个过程准确率只下降了 1.8 个百分点,体积压缩到二十分之一,速度提升接近十倍。需要说明的是,这是模拟数据,具体数字会因任务、数据集和模型结构差异而浮动,但趋势是普遍成立的。
5.3 压缩完成后的验收与部署实战
压缩不是终点,验收才是。正式发布前,至少要把这几项跑一遍:原测试集精度、一批典型难例和边界样本的表现、单张推理延迟、内存峰值、以及设备的功耗变化。每一项指标都要和未压缩前的基线对比。
还要注意在不同设备、不同线程数、不同批量大小下分别测试。同一个模型在高端机和中低端机上的表现可能完全不同,量化算子在 A 设备上未生效但 B 设备上生效的情况也不少见。多留几版压缩中间产物,比如“剪枝后未量化版”,一旦线上出问题,可以快速定位是哪个环节导致的。
如果实测发现精度不过关,排查顺序也有讲究。先怀疑量化,试着把敏感层切回 FP16 或改用量化感知训练;再怀疑剪枝,看是不是剪枝率太高,减少剪枝比例重新微调。大多数压缩精度事故,都能在这两步里找到根因。
最后聊几句个人体会。模型压缩不是目的,让模型在真实场景里跑起来才是。我见过不少项目一上来就三招全上,折腾了一个月,精度掉了好几个点,结果回头才发现瓶颈只是内存不够,先做个动态量化就解决了八成问题。动手之前一定要想清楚自己的核心瓶颈到底是什么:体积、内存、延迟还是功耗。选了最便宜的那招先试,永远比盲目堆全套技术更靠谱。主流深度学习框架基本都内置了现成的剪枝、量化接口,蒸馏也有大量开源实现可以直接用,不必从零造轮子。把这三个技术理解透,再遇到“模型跑不动”这类问题,你至少知道该从哪儿下手了。