神经网络剪枝与量化新手实战:模型瘦身原理与踩坑指南
聊到神经网络剪枝和量化很多新手第一反应是这又是玄学吧。作为去年才把这些东西真正落到业务模型上的人我特别理解那种感觉看文章时每个字都认识一动手模型要么直接崩要么体积小了但速度反而慢。这篇是大白话系列的第二篇没看过第一篇也不影响理解我会把剪枝和量化背后的为什么拆开揉碎再用一个模拟项目X完整走一遍流程最后把我踩过的坑按速查表的形式整理出来。剪枝是给模型减重量化是让模型用更小口径的数字表达同样的能力。这两个东西合在一起就是目前把模型塞进手机、边缘设备、浏览器里最常用的一套组合拳。这篇内容适合刚摸过深度学习、想给模型做瘦身但不知道从哪里下手的同学也适合被各种工具链绕晕、想理清原理的开发者。我会尽量不用公式堆砌能打比方的就打比方复杂的步骤会给出可复制的操作路径。1. 神经网络剪枝与量化到底在干什么用大白话把概念摆平1.1 剪枝不是把模型删没了而是把不干活的零件拆掉神经网络在训练的时候很多参数其实是在陪跑。网络为了拟合数据往往会留很多冗余就像一个大公司里有很多人虽然挂着岗位但实际产出接近于零。剪枝要做的事情就是找出这些不干活的权重或通道把它们清掉让矩阵运算变小。我在第一次接触剪枝时有个严重误解以为剪枝就是随机删参数。实际上真正有效的剪枝需要先回答一个问题凭什么判断一个权重不重要常见思路是看权重的绝对值大小——数值越接近零说明它对最终输出的贡献越小。还有看梯度、看激活值统计量等后面会展开。总之剪枝不是机械删除而是基于某种重要性度量做取舍。剪枝分两种非结构化剪枝和结构化剪枝。非结构化就是把单个权重置零产生稀疏矩阵好处是灵活、压缩率高坏处是硬件不友好计算速度很难真正提升。结构化剪枝则是把整个通道、甚至整个卷积核砍掉虽然看起来粗暴但矩阵维度真的变小了在CPU、GPU上都能跑得更快。新手我建议优先玩结构化剪枝否则你做完之后可能在部署环节被稀疏矩阵的索引问题折磨到怀疑人生。1.2 量化把高精度刻度换成低精度刻度精度损失控制在眼睛看不见的范围量化这个词听起来高深其实可以拿照片类比。一张真彩色照片每个像素用24位表示看起来细腻。如果你强行用8位去表示颜色会变少但人眼在一定尺寸下根本分辨不出来。神经网络量化也是这个思路把权重和激活值从32位浮点数FP32压缩到8位整数INT8模型体积直接缩小到原来的四分之一推理速度也可能翻倍。为什么32位浮点能压缩到8位因为网络里的数值分布往往很集中大多数权重都在一个比较窄的范围内。如果我们找到这个范围的最小值和最大值就可以把整个区间的浮点数映射到0到255的整数刻度上。推理时用整数做乘加最后再还原到浮点精度损失通常很小。但注意量化不是无损操作。如果权重本身分布很极端或者有大量离群值那么映射的时候就会把大多数正常值压缩到一个极窄区间精度就会崩。这也是为什么后面要聊校准数据集、量化感知训练这些东西——它们都是在解决如何让映射更合理的问题。1.3 这两个东西能解决什么问题谁适合读这篇先说应用场景。现在很多模型最终要部署到手机、门禁、工业相机、浏览器甚至单片机上这些场景的共同点是内存小、算力有限、对功耗有要求。你不可能在每一台手机上跑一个20GB的大模型也不可能让一个几百毫秒才能出结果的模型做实时检测。剪枝加量化就是业内最常见的部署前必经之路。对算法工程师来说训练出一个好模型只完成了三分之一。模型能不能上线、能不能在给定硬件上跑得动往往决定了项目的价值。这张系列文章就是写给那些准备进入部署环节、但还不太熟悉模型压缩的同学。我的身份是一个在某公司做过边缘视觉项目的开发者不是学术大佬所以我的经验更贴近实际工程而不是论文复现。读完这篇你会知道剪枝和量化分别做什么、顺序怎么安排、工具怎么选、遇坑怎么排查。2. 剪枝的底层逻辑与新手最容易上手的做法2.1 非结构化剪枝 vs 结构化剪枝一个像打扫卫生一个像拆隔断先把两种剪枝的区别讲透。非结构化剪枝相当于你在一个房间里把没用的杂物扔掉但房间的墙壁和门的位置没变你还是得在同样的空间里走动。放到模型上就是权重矩阵里很多位置变成了零矩阵还是原来那么大但理论上计算时可以跳过零。问题在于跳过零需要额外维护一张稀疏索引表各种硬件库对这个的支持参差不齐在GPU上甚至可能更慢。所以非结构化剪枝更适合论文刷压缩率不太适合真正部署。结构化剪枝就相当于拆房间的隔断把一整面墙都敲掉房间直接变大了。放到网络上就是整个卷积核或整个通道被删掉特征图的数量随之减少下一层的计算量也真的变少了。这种方式的压缩率可能不如非结构化高但胜在实打实加速。我在实际项目中几乎只做结构化剪枝因为它对部署的友好程度实在高太多了。在结构化剪枝里最常见的操作对象是通道数。每个卷积层会输出若干个通道每个通道相当于一个特征检测器。如果某些通道几乎没有被激活过或者它的输出对最终结果影响不大你就可以把整个通道连同前面一层对应的卷积核一起删掉。注意不是随便删一个通道就完事因为后面的层会把所有通道作为输入通道数变了后面的权重维度也要对不上。这就是后面说的通道错位问题。2.2 判断重要性的三个常用指标绝对值、激活值、BN的gamma剪枝的第一步是给每个通道打分。新手最常听说的是按权重绝对值加和做法是把一个卷积核的权重取绝对值再求和得分低的通道删掉。这个方法的直觉是权重整体都很小说明这组卷积核在输出上几乎不起作用。但它的问题是容易误伤有些通道虽然权重绝对值小但激活值被放大得厉害实际影响并不小。更稳的方法是看激活值的统计。具体来说把训练集的一部分过一遍模型记录每个通道输出的平均值或方差。如果某个通道输出的平均激活值很低说明它很少被点亮基本可以删。这个方法比较贴近实际行为但需要跑一遍前向计算量稍微大一点。还有一类方法利用BN层的缩放因子gamma。现在很多CNN里都带BN层BN会对每个通道学习一个缩放系数gamma训练时这个gamma会自适应地变大或变小。gamma大的通道对整体输出影响大gamma接近0的通道影响极小。基于BN的剪枝就是把这个gamma当作重要性分数把排序靠后的通道连同它对应的卷积核一起剪掉。这个方法的好处是训练完直接看gamma几乎不用额外计算非常符合新手友好的特征。2.3 我推荐新手的剪枝路线从BN的gamma下手如果你从来没有剪过枝我建议不要一上来就搞复杂的泰勒展开、互相信息那种高端方法先把基于BN的剪枝走通。操作路线大概是先正常训练一个模型训练时不要冻结BN的gamma更新训练完成后统计所有BN层的gamma值画个直方图然后设定一个剪枝比例比如40%把每个BN层里gamma值最小的40%通道挑出来最后把这些通道对应的前层卷积核和后层输入维度一起删掉并对模型做微调。为什么要用BN的gamma而不是权重绝对值因为我实测下来BN的gamma分布通常更干净能得到一个比较明显的双峰形状一个峰集中在0附近另一个峰在较大的值附近。这意味着很多通道确实在“躺平”把gamma接近0的通道删掉精度损失很小。而权重绝对值很多时候分布相对均匀实际删哪个都有点纠结。还有一个细节对残差结构比如带有shortcut的模块剪枝不能只考虑单个卷积层因为shortcut会把之前的输出加到后面通道数必须保持一致。所以现在很多成熟的剪枝方案会设计一种通道损失策略把shortcut支路上的卷积也一起剪掉或者做不等比通道对齐。这一块对纯新手的复杂度偏高建议你先把没有shortcut的骨架比如VGG风格练熟再去碰残差网络。3. 量化的底层逻辑与新手最容易上手的做法3.1 FP32到INT8信息是怎么被压进小盒子的先看一个最简单的公式。假设某一层权重矩阵里的数值落在[-2.0, 6.0]这个区间FP32表示的时候很自由可以在区间内取任意小数。现在要把这些值映射到[-128, 127]的整数范围内。我们需要两个参数缩放因子scale和零点zero_point。scale就是(6.0 - (-2.0)) / 255约等于0.03137表示每个整数刻度代表多少浮点值。zero_point则是浮点0对应的整数偏移。推理的时候原本的浮点乘法a×w就变成了整数乘法(q_a - zero_point_a)×(q_w - zero_point_w)最后乘上scale_a×scale_w。因为权重和激活的scale都是事先算好的常量所以一些常数可以提前合并实际计算依然是整数乘加。整数乘加在GPU、NPU上都有专门的高效指令速度自然快。这里最关键的一步是算子融合。一般会把BN层和前面的卷积层融合到一起把BN的缩放、平移参数吸收进卷积的权重和偏置里这样BN就不再是独立算子得一个纯净的卷积输出量化时只量化一个算子误差更小。这也是很多量化工具会自动帮你做的事但新手最好知道不然看到参数列表里那些奇怪的融合层会晕。3.2 两种量化方式训练后量化与量化感知训练训练后量化PTQ最简单。模型训练完成后准备一小部分校准数据也就是几百到一两千张有代表性的图像过一遍模型统计每一层激活值的min和max结合权重的min和max算出scale和zero_point然后直接导出INT8模型。整个过程可能只需要几分钟不需要重训模型。这种方式非常适合拿来快速上手。量化感知训练QAT要复杂一些。它在训练过程中就模拟量化的效果前向时把权重和激活先量化再反量化让模型适应低精度带来的噪声训练收敛后再导出。因为训练时已经见过量化误差所以最终精度通常比PTQ高一些尤其是对本来就比较敏感的模型比如检测、分割模型。但QAT需要重新训练还要小心设置量化模拟的位置时间成本高很多。我的经验是先试PTQ如果精度下降在可接受范围就直接用如果下降太多再考虑对关键层做QAT。很多工具也支持混合量化允许你指定哪些层用FP32、哪些层用INT8对精度卡点做精细化妥协。新手不要一上来就全模型QAT否则训练流程变长、调试难度增加很容易放弃。3.3 一个完整的量化参数估算例子为了不让大家觉得量化是黑盒我把一个小例子算一遍。假设我们有一个卷积层训练后权重的最小值是-3.25最大值是5.75。我们准备做对称量化也就是让浮点零正好映射到整数零这样省掉zero_point的计算对权重来说误差稍大一点但实现简单。对称量化时量化范围就是[-3.25, 5.75]这堆数值里绝对值更大的那个也就是5.75。使用INT8对称范围是[-127, 127]为了对称一般不用-128所以scale 5.75 / 127约等于0.04528。如果我们把权重值4.2量化就做round(4.2 / 0.04528) round(92.76) 93。反量化回来是93×0.04528 4.209只损失了一点点。但如果权重里有一个离群值是50那scale就变成50/127≈0.3937那么4.2量化成round(10.67)11反量化后4.328误差就大了。这就是离群值伤害量化精度的原因。遇到这种情况我们可以考虑非对称量化用min和max算零点或者对权重做裁剪把离群值先顶到上限从而保住大部分正常值的精度。激活部分的量化也是同理只不过激活的min/max需要从校准数据上统计。校准数据选得好不好直接影响激活量化范围准不准。如果校准数据太单调只覆盖了某个亮度区间的图片那么真实场景中激活分布会更广量化范围就会定窄推理时就很容易产生截断误差精度自然下降。4. 从零到一的实操记录给模拟项目X做剪枝量化4.1 实操前的环境与模型选择我这次用一个模拟项目X来做演示它本质是一个图像分类任务用的模型是一个只有几层卷积的小网络。之所以选这个小网络是因为能快速跑完方便验证流程。大模型在原理上完全一致只是训练时间更长。环境方面我用的是一台普通的工作站CPU和一张入门级显卡框架是常见的深度学习框架F你可以替换成自己熟悉的原理不相上下。建议提前装好开源工具集T它把很多剪枝、量化步骤封装成了接口虽然有时候包装多了会出问题但对新手来说还是很能救命的。数据集我用的是模拟的分类数据集几百张图片分10个类。大家自己实验也不用太大关键是能复现损失的变化趋势。4.2 第一步先做稀疏化训练别直接上来就剪我在第一次做剪枝时犯过直接对预训练模型下刀的错误结果剪完精掉得很惨。正确的做法是先在训练时加一个稀疏正则化让BN的gamma尽可能往0靠拢。具体来说就是在loss里加一项gamma的L1范数乘以稀疏系数lambda。这个lambda的取值很讲究太小了gamma不往0走太大了模型精度先崩了。我这边常用的范围是1e-4到1e-3需要简单做两三次小实验看验证集精度和gamma分布的变化。训练完成后我画出了所有BN层gamma的直方图能看到明显的长尾大量gamma集中在0附近只有少数通道比较活跃。此时先不要剪枝把模型保存好记录当前精度作为baseline。稀疏化训练可能会让精度有轻微下降这是正常的相当于给剪枝铺路。4.3 第二步对训练好的模型做基于BN的通道剪枝接下来进入剪枝核心环节。我先设定一个剪枝率比如40%。然后对每个BN层按gamma值从小到大排序找出阈值把gamma低于该阈值的通道对应的卷积核掩膜置为0。但是注意单纯的掩膜还是非结构化要真正得到结构化剪枝后的模型需要重写网络结构把卷积层的输出通道数改少删掉对应维度的权重同时把下一层的输入通道数也改少。我在实际代码里是通过工具集T的剪枝接口完成的它会在仿函数forward时记录各层输入输出通道索引关系自动逐层剥离通道。这里最需要注意的是如果网络中既有残差连接又有拼接concat工具可能无法自动识别哪些通道要同步剪只能手动维护一份需要共享剪枝索引的层列表。项目X结构简单所以我只处理了顺序链接。剪枝后先不急微调直接评测一下精度。通常会发现比baseline降了几个点这是正常的因为通道删掉后网络需要重新适应。关键是要看降的幅度是否合理如果只是下降2到5个百分点说明通道还有冗余如果下降超过10个点说明剪得太猛或稀疏化训练不够充分。4.4 第三步微调恢复精度剪完枝的模型需要微调我在这里总结了三个关键点。第一学习率不能沿用原来的大学习率我用初始学习率一般是原来的十分之一甚至更低。第二微调epoch不需要太多我这边跑10个epoch就够太多反而容易过拟合。第三一定要解冻BN层让gamma和running mean重新适配新的通道结构。微调过程中我还会把剪枝率记录到日志里每个epoch打印验证集精度。精度一般会在前几个epoch快速回升然后稳定下来。这里有一个容易踩的坑如果微调时把所有层的gamma都放开有可能gamma又会变大导致剪掉的通道在后来的语义上变得重要但已经是剪掉了所以模型精度受限。因此有些打法是在微调时再次施加稀疏正则化维持通道稀疏性让模型在稀疏结构下学习。我最后得到的模型体积大约是原来的60%精度恢复到了baseline的98%以上。注意这里98%是相对保精度率不是准确率一定到98。4.5 第四步把剪完的模型做INT8量化现在开始在剪枝后的模型上做训练后量化。我准备了一个校准数据集大约500张图像要求覆盖各类别、各种光照条件。调用量化工具时指定校准数据、量化算子配置、想要量化的层等然后工具会自动完成算子融合和参数校准导出INT8模型。这一步我遇到过一个坑如果校准数据集和真实数据分布差异太大量化后精度会明显下降。比如我只拿了白天拍的图像做校准晚上的图像激活范围统计不出来一旦推理晚上图像就崩。所以校准数据的代表性比数量更重要。量化后模型体积直接从剪枝后的基础上再缩小到四分之一加载到内存后的收益非常明显。为了在CPU上验证速度我在笔记本上跑了一下推理速度大概提升了1.8倍虽然没到理想中的4倍但考虑到模型结构、底层实现和不是所有层都被量化这个结果已经很香了。4.6 结果对比与收益分析我把完整流程的数据整理成一个表格这样可以直观看到每一步带来什么变化。模型状态参数量约体积约推理耗时约相对精度原始FP32100%完整1.0x100%稀疏训练后100%不变1.0x99%-100%剪枝后FP3260%60%0.7x96%-98%剪枝微调后FP3260%60%0.7x98%以上剪枝微调量化60%INT815%0.35x左右97%-98%可以看到剪枝和量化的收益是叠加的。光剪枝已经把参数量降下来量化又把每个参数的位数降低两者相乘体积缩减非常可观。速度方面剪枝主要通过减少计算量来提速量化则是通过利用低精度指令来提速。在部分硬件上这两个收益可以相乘到三到四倍的实际加速。5. 新手踩坑实录这些问题我几乎都遇到过5.1 剪完枝模型崩盘先查微调的初始学习率最常见的现象是剪枝完直接微调第一个epoch loss就变成NaN或者精度暴跌到个位数。我排查过好多次两个原因最扎眼。一是学习率太大剪枝后的网络拓扑已经变了原来loss曲面的平滑区域被破坏大学习率直接迈出悬崖。换成原来学习率的十分之一到二十分之一问题立刻缓解。二是训练数据没有做正确的前后顺序。如果微调时BN层还在沿用剪枝前的全局统计数据而输入数据分布因为通道删除有偏移会出现严重的协变量漂移。解决办法是先跑几个iter的纯前向让BN的running mean和running var重新估计然后再开训练。很多框架里BN默认是train模式才更新统计量如果你用eval模式去微调就会踩坑。5.2 量化后精度掉太多先查校准数据和量化边界量化后精度的下降我拆成三类排查。第一类是校准数据有问题比如只有几个类别、只有固定分辨率导致激活min/max统计不准。对策是加大校准集多样性最好从验证集中随机抽确保覆盖所有重要场景。第二类是权重离群值导致scale过大对策是尝试对权重做裁剪把超出一个合理百分位比如99.9%的极端值按边界截断很多情况下精度可以救回来。第三类是激活值分布太陡尤其是有大量接近0的激活和少量很大的激活线性量化很难兼顾对策是换用非对称量化或对激活做per-channel量化在框架里只是改个配置项但效果差异很大。还有一个容易被忽略的如果你的网络里有不妥协的算子比如某些动态分支、稀疏计算量化工具可能无法处理会退回FP32这样你以为全模型INT8实际上还有很多FP32算子速度没有达到预期。务必检查工具日志看有多少层被量化多少层被排除。5.3 通道错位、concat维度对不上剪枝不是单层的事结构化剪枝很经典的问题是下一层输入通道数和上一层输出通道数不一致。如果你手动剪枝一定要照着网络的连接关系一张表一张表地列清楚每个卷积层前面的输入来自哪一层后面的输出去哪一层。对于典型的连续卷积-卷积结构层间关系是线性的还算好办。麻烦出在shortcut和concat上。以残差块为例主分支被剪掉若干通道后要保证shortcut分支也剪掉同样多的通道否则加法时形状对不上。而concat操作是在通道维度拼接如果不同分支剪掉的通道数不一样拼接后的总通道数虽然对但通道索引语义会错乱。我的建议是对于新手期先不要对带复杂分支的网络做结构化剪枝。先用一个简单的顺序网络把流程吃透要碰残差结构时再借助工具自动生成掩膜或者手动定义统一的剪枝索引组。5.4 工具链选型的血泪教训别盲目自己写剪枝和量化的生态其实很碎片化有的工具擅长剪枝有的擅长量化有的只适配某几种硬件。我最初图省事自己手写了一套剪枝流程结果发现处理BN、处理残差、处理反量化这些杂活太多随便一个细节没注意到就出错还很难排查。后来改用主流开源工具集T虽然它也有毛病但至少社区踩坑的人多搜一搜就有解。关于选型我建议按这个顺序来第一确认你最终部署的推理引擎是什么量化操作最好用推理引擎配套的工具这样导出格式兼容性最好。第二剪枝工具优先选择能自动处理通道依赖的比如能识别shortcut和concat的能在你导出模型时生成正确网络结构的。第三不要轻信一行代码剪枝的接口很多一行代码背后隐藏了大量配置默认配置大概率不是为你的模型调的。我至今仍保持一个习惯哪怕是工具自动剪枝我仍手动打印每个BN层剪枝前后的通道数量核对一遍。6. 一些让大家少走弯路的经验与后续扩展6.1 精度、速度、体积的三角权衡你可能遇到一个让人迷惑的情况剪枝后模型的参数量变了但推理速度并没有变得更快。原因是推理速度不仅看参数量还看内存访问次数、算子调度、硬件利用率。比如通道数剪到不规整的数字比如剪完是37通道框架在底层做矩阵乘法时可能会因为维度不是对齐的而浪费计算资源。所以很多工程师会在剪枝时做通道规整把通道数对齐到16、32、64这样的数字。反过来有时参数没怎么减少但速度提升明显因为量化让内存带宽占用变低了在带宽受限的场景里收益很大。因此项目里不要只看模型体积一个指标。如果目标是上线就要拿最终推理引擎在目标硬件上跑 benchmark用延迟、吞吐、内存峰值三个数字说话。建议把精度-速度-体积做成一张三角权衡图定期回顾。单纯追求极端压缩率可能导致部署后精度不达标而追求太高精度又可能体积太大装不进设备。我一般会设置三个约束体积上限、延迟上限、最低精度阈值然后把剪枝率和量化方案当成可调节的两个旋钮在这个约束圈里找最优解。6.2 提升剪枝量化效果的小技巧稀疏训练 蒸馏如果你希望最终的压缩率更高又不想牺牲太多精度我有两个私下屡试不爽的技巧。第一个是稀疏剪枝联合训练前面说的稀疏正则化是一个基础版进阶一点可以分阶段调整稀疏系数比如前30个epoch用普通训练中间20个epoch加大稀疏正则最后10个epoch再把正则调小恢复精度。这样剪枝后的模型适应度显著改善。第二个技巧是知识蒸馏。具体来说把原始未剪枝的大模型当作教师剪枝量化后的模型当作学生用教师的软标签logits去监督学生。对大模型来说硬标签可能让学生过于自信但软标签携带了类间相似性的信息比如这看起来像猫但有一点像狗能帮小模型保留更丰富的决策边界。我做图像分类时在剪枝后微调阶段加入蒸馏损失最终精度提升了1到2个点常常能把剪枝掉的那点性能拉回来。这两个技巧可以叠加使用但注意别一次上太多tricks否则训练不稳定出了问题你都不知道是哪一步导致的。我建议先跑通最基础的剪枝量化流程再逐步加技巧。6.3 下一步可以玩的方向自动化压缩工具、基于硬件感知的量化学会手动走通剪枝和量化之后可以尝试把流程半自动化。比如写一个脚本在给定的精度和速度约束下自动调整剪枝率跑几轮小规模的搜索。这种方法不一定能找到全局最优但能在固定配置下解放人工试参时间。另一个方向是基于硬件感知的量化。不同硬件对量化格式的支持不太一样有的芯片支持per-channel量化有的只支持per-tensor有的支持INT8有的甚至支持INT4、INT16混合。你可以根据目标硬件的算子列表来定制量化策略而不是一刀切全INT8。这个方向门槛稍高但收益很大。如果你还想更深入可以研究一下结构重参数化它通过把训练时的复杂结构重构成部署时的简单结构和剪枝量化搭配效果很好。不过那是另一个话题了走完这篇的基础流程再看那些也不迟。最后分享一点个人体会。我最初在模拟项目X上走过一条完整的弯路先花两周研究各种剪枝论文再花一周写自定义剪枝代码最后发现和开源工具效果差不多。后来我学乖了先选一条最朴素的路线跑通再去深究底层原理。很多看起来高深的技巧核心思想其实特别简单只是被包装得复杂。希望这篇大白话能帮你少踩一点坑把宝贵的精力留给真正有价值的部分——理解模型为什么可以被压缩以及你的业务场景到底需要压缩到什么程度。