在实际部署过程中,大量深度学习模型需要运行在移动端设备上。由于移动端的存储空间和计算能力极为有限,通常无法承载参数规模庞大的网络。因此,模型压缩成为关键环节。本文将系统梳理卷积神经网络压缩的主流技术,包括低秩近似、剪枝与稀疏约束、参数量化、二值化网络、知识蒸馏以及浅层/轻量网络。
众所周知,网络层数越深、参数越多、模型结构越复杂,其性能往往越优异。但在实际业务场景中,我们必须在模型性能与运行效率之间寻找平衡。神经网络压缩算法的核心目标,是将一个庞大且已经训练好的预训练模型(pre-trained model)转化为一个轻巧的小模型。根据对原始网络结构的改动程度,压缩技术可分为“前端压缩”和“后端压缩”两大类。
所谓前端压缩,指的是那些不改变原有网络结构的方法,典型代表包括知识蒸馏、设计紧凑的模型结构,以及滤波器(filter)级别的剪枝。而后端压缩则涵盖低秩近似、无限制的剪枝、参数量化、二值网络等,这些方法力求最大限度地压缩模型体积,但会对原始网络结构产生显著甚至不可逆的改变。总结来说:前端压缩基本不动原有结构(顶多减少几层或切除部分滤波器),后端压缩则大刀阔斧地改造结构,导致深度学习库和硬件往往难以兼容改动后的网络,维护成本也大幅提升。
1. 低秩近似
简单来讲,卷积神经网络的权重矩阵通常非常稠密且尺寸庞大,计算开销随之居高不下。一种解决思路是采用低秩近似技术——把稠密矩阵用若干个小规模矩阵近似重构出来。这类方法统称为低秩近似算法。
一般地,行阶梯型矩阵的秩等于它的“台阶数”——非零行的行数。
低秩近似算法能降低计算开销的原理如下:给定权重矩阵 W,如果能将其表示为若干个低秩矩阵的组合,即W = ∑Wi,其中Wi是低秩矩阵,秩为ri,且满足ri远小于矩阵的维数,那么每个低秩矩阵都可分解为小规模矩阵的乘积,Wi = Ui Vi,其中Ui ∈ ℝm×ri,Vi ∈ ℝri×n。当ri很小时,存储与计算开销就能显著降低。基于这一思想,Sindhwani等人提出了使用结构化矩阵进行低秩分解的算法,具体细节可查阅原始论文。另一种更为简便的方法是通过矩阵分解来减少权重矩阵的参数,例如Denton等人利用奇异值分解(SVD)来重构全连接层的权重。
总结
低秩近似算法在中低规模的网络模型上表现良好,但其超参数数量会随着网络层数线性增长。一旦层数增多、模型变得复杂,搜索空间就会急剧膨胀。目前该方向仍以学术研究为主,工业界实际应用并不多见。
2. 剪枝与稀疏约束
给定一个预训练好的网络,常用的剪枝算法大致遵循以下流程:
- 评估所有神经元的重要程度。
- 移除那些不重要的神经元——这一步相对更简单、更灵活。
- 对网络进行微调。剪枝必然导致精度损失,为避免分类性能严重下降,必须进行微调。不过在大规模图像数据集(如ImageNet)上微调相当耗费计算资源,因此微调的次数与策略需要仔细权衡。
- 回到第一步,循环进行下一轮剪枝。
基于这个循环框架,不同学者提出了多种方法。Han等人提出先将低于某个阈值的权重连接全部剪掉,再微调更新参数。但这种方式存在一个问题:剪枝后的网络是非结构化的,剪掉的连接在分布上缺乏连续性,这种稀疏结构会导致CPU高速缓存与内存频繁切换,实际加速效果大打折扣。于是有学者尝试将剪枝粒度提升到滤波器级别——直接丢弃整个滤波器。但如何衡量一个滤波器的重要程度呢?一种策略是观察滤波器权重的统计量,例如分别计算每个滤波器的L1或L2范数,数值大小即作为重要性依据。
利用稀疏约束来剪枝也是一个重要研究方向:在优化目标中加入权重的稀疏正则项,训练过程中让部分权重逐渐趋向于0,这些0值权重即为剪枝的对象。
总结
总体来看,剪枝是一种通用且有效的压缩技术,其核心难题在于如何准确判断单个权重对整体模型的重要性。剪枝对网络结构的破坏程度较小,如果与其他后端压缩技术结合使用,可以将模型压缩到极致。目前工业界已有应用剪枝进行模型压缩的成功案例。
3. 参数量化
相比剪枝,参数量化是一种更为常用的后端压缩技术。“量化”的含义是:从权重中归纳出若干个“代表值”,用这些代表值来表示某一类权重的具体数值。代表值存储在码本(codebook)中,而原始权重矩阵只需记录每个元素所属代表值的索引,这样存储开销就大大降低了。这一思路与经典的词包模型(bag-of-words model)非常相似。常见的量化算法有:
- 标量量化(scalar quantization)。
- 为避免标量量化带来的精度损失,许多算法采用了结构化的向量方法,其中一种是乘积量化(Product Quantization, PQ),详情可查阅相关论文。
- 以PQ为基础,Wu等人设计了一个通用的网络量化算法QCNN(quantized CNN),其核心思想是:最小化每一层网络输出的重构误差,比直接最小化量化误差更加有效。
标量量化的基本思路是:对于每个权重矩阵W,先将其拉成向量形式w,然后对w的元素进行k个簇的聚类,使用经典的k-means算法即可快速完成。这样只需将k个聚类中心(标量值)存储在码本中,而原权重矩阵仅记录每个元素所属聚类中心的索引。若不考虑码本的存储开销,该算法可将存储空间压缩到原来的log₂k/m(其中m为原始数据类型位数)。基于k-means的标量量化在很多实际应用中表现良好。参数量化与码本微调的过程如下图所示:

这三类基于聚类的参数量化算法,本质上都是将多个权重映射到同一个数值,从而实现权重共享、降低存储开销。
总结
参数量化是一种常用的后端压缩技术,可以用很小的性能损失换来非常可观的体积压缩。缺点是量化后的网络是“固定”的,难以再进行修改,同时通用性较差,需要搭配专门的深度学习库才能运行。
4. 二值化网络
二值化网络可以视为量化方法中最极端的一种:所有权重参数只能取±1,即用1个bit来存储Weight和Feature。普通神经网络一个参数为单精度浮点数,而二值化能直接将存储开销降至原来的1/32。
二值化神经网络因其高压缩率与快速前向计算,近年来备受关注,成为神经网络模型研究的热门方向。不过,真正第一篇将权重值和激活函数值同时二值化的论文,是Courbariaux等人在2016年发表的《Binarynet: Training deep neural networks with weights and activations constrained to +1 or -1》。这篇论文首次给出了如何对网络进行二值化、如何训练二值化神经网络的具体方法。
CNN的一个典型模块是卷积(Conv)→批标准化(BNorm)→激活(Activ)→池化(Pool)。对于异或神经网络,设计的模块变为:批标准化→二值化激活(BinActiv)→二值化卷积(BinConv)→池化(Pool)。这样做的原因是:批标准化能保证输入均值为0,然后进行二值化激活,数据变为+1或-1,再进行二值化卷积,可以最大程度减少特征信息的损失。二值化残差网络结构的实例代码如下:
def residual_unit(data, num_filter, stride, dim_match, num_bits=1):
"""残差块 Residual Block 定义"""
bnAct1 = bnn.BatchNorm(data=data, num_bits=num_bits)
conv1 = bnn.Convolution(data=bnAct1, num_filter=num_filter, kernel=(3, 3), stride=stride, pad=(1, 1))
convBn1 = bnn.BatchNorm(data=conv1, num_bits=num_bits)
conv2 = bnn.Convolution(data=convBn1, num_filter=num_filter, kernel=(3, 3), stride=(1, 1), pad=(1, 1))
if dim_match:
shortcut = data
else:
shortcut = bnn.Convolution(data=bnAct1, num_filter=num_filter, kernel=(3, 3), stride=stride, pad=(1, 1))
return conv2 + shortcut
4.1 二值网络的梯度下降
当前神经网络几乎都依靠梯度下降进行训练,但二值网络的权重只有±1,无法直接计算梯度,也无法更新权重。为解决这一问题,Courbariaux等人提出了二值连接(binary connect)算法——将单精度与二值相结合进行训练。这是首次给出如何对网络进行二值化以及如何训练二值化神经网络的方法。具体步骤如下:
- 权重初始化为浮点数。
- 前向传播:使用符号函数(sign(x))将权重量化为+1/-1(以0为阈值);然后用量化后的权重(仅+1/-1)计算前向传播,卷积运算仅涉及加法,得到卷积层输出。
- 反向传播:将梯度更新到浮点权重上(根据放松后的符号函数计算梯度值,再对单精度权重进行参数更新)。
- 训练结束:将权重永久转化为+1/-1,供推理阶段使用。
4.2 两个问题
网络二值化需要解决两个关键问题:如何对权重进行二值化,以及如何计算二值权重的梯度。
权重二值化通常有两种选择:
- 直接根据权重的正负进行二值化:wb = sign(w),符号函数采用标准定义。
- 进行随机的二值化,对每个权重以一定概率取±1。
二值权重的梯度为0,无法进行参数更新。解决办法是放松符号函数,用Htanh代替sign:当x在区间[-1, 1]时梯度为1,否则为0。
4.3 二值连接算法改进
之前的二值连接算法只对权重进行了二值化,中间输出值仍然是单精度。Rastegari等人对此进行了改进:用单精度对角阵与二值矩阵的乘积来近似表示原矩阵,从而提升二值网络的分类性能,弥补精度上的弱势。该算法将原卷积运算分解为:W ≈ α B,其中α是单精度缩放因子,B是二值权重矩阵。Rastegari等人认为单靠二值运算难以达到原单精度卷积的效果,因此用α对二值滤波器的卷积结果进行缩放。α通过优化目标‖W - αB‖²得到最优解。改进后的训练过程与之前大致相同,区别在于梯度的计算还考虑了α的影响。加入这个单精度缩放因子后,重构误差大幅降低,并且首次在ImageNet上达到了与AlexNet相当的精度。如下图所示:

可以看到,权重二值化神经网络(BWN)与全精度神经网络的精度几乎相同,但与异或神经网络(XNOR-Net)相比,Top-1和Top-5都有10%以上的损失。
相比权重二值化,异或神经网络将输入也转化为二进制值,因此其乘加运算变为按位异或(bitwise xnor)和统计1的个数(popcount)。
4.4 二值网络设计注意事项
- 避免使用kernel = (1, 1)的卷积(包括ResNet中的bottleneck)。在二值网络里,权重均为1bit,若再使用1×1的核,表达能力会急剧下降。
- 增大Channel数目与增大activation bit数需要协同配合。如果只增加channel而feature的bit数过低,模型容量依然会被浪费;反之亦然。
- 建议使用4bit及以下的activation bit。再提高bit数,精度收益很小,反而会显著增加推理计算量。
5. 知识蒸馏
这里仅简要介绍该领域的开篇之作——Distilling the Knowledge in a Neural Network,它属于蒸馏“logits”的方法。后续还出现了蒸馏“features”的论文。如需深入了解,可查阅相关的综述文章。
知识蒸馏是迁移学习的一种。简单来说,先训练一个大模型(teacher)和一个小模型(student),然后将大模型学习到的知识通过某种技术迁移到小模型上,使小模型获得接近大模型的性能。实验中,先训练好teacher网络,再让student网络的输出q去逼近teacher网络的输出p,因此student的损失函数为L = CrossEntropy(y, q)。但直接使用teacher的softmax输出p可能并不合适。因为网络训练好后,对正确答案的置信度会非常高,例如在MNIST中输入一个2,对2的预测概率接近1,而对3和7的预测概率可能只有10⁻⁶和10⁻⁹,这样teacher学到的数字相似信息就难以传递给student。于是论文提出了带温度参数T的softmax公式:qi = exp(zi/T) / ∑j exp(zj/T),其中zi是softmax前一层的logit。当T=1时即为普通softmax;T越大,概率分布越平滑(越“软”),相当于在迁移学习过程中引入扰动,使student学习更有效、泛化能力更强,这本质上是一种抑制过拟合的策略。
student模型的损失函数由两部分组成:
- 第一项是小模型(student)预测结果与大模型“软标签”的交叉熵;
- 第二项是小模型预测结果与普通类别标签的交叉熵。
两个损失的权重通过超参数调节,T的取值会影响最终结果,通常较大的T能获得更高的准确度。知识蒸馏的整个过程如下图所示:

student模型的实际结构与小模型相同,但损失函数包含两部分。下面是分类网络知识蒸馏的mxnet代码示例(仅给出softmax损失部分):
# -*-coding-*- : utf-8
""" 本程序没有给出具体的模型结构代码,主要给出了知识蒸馏 softmax 损失计算部分。"""
import mxnet as mx
def get_symbol(data, class_labels, resnet_layer_num, Temperature, mimic_weight, num_classes=2):
backbone = StudentBackbone(data) # Backbone 为分类网络 backbone 类
flatten = mx.symbol.Flatten(data=conv1, name="flatten")
fc_class_score_s = mx.symbol.FullyConnected(data=flatten, num_hidden=num_classes, name='fc_class_score')
softmax1 = mx.symbol.SoftmaxOutput(data=fc_class_score_s, label=class_labels, name='softmax_hard')
import symbol_resnet # Teacher model
fc_class_score_t = symbol_resnet.get_symbol(net_depth=resnet_layer_num, num_class=num_classes, data=data)
s_input_for_softmax = fc_class_score_s / Temperature
t_input_for_softmax = fc_class_score_t / Temperature
t_soft_labels = mx.symbol.softmax(t_input_for_softmax, name='teacher_soft_labels')
softmax2 = mx.symbol.SoftmaxOutput(data=s_input_for_softmax, label=t_soft_labels, name='softmax_soft', grad_scale=mimic_weight)
group = mx.symbol.Group([softmax1, softmax2])
group.sa ve('group2-symbol.json')
return group
TensorFlow代码示例:
# 将类别标签进行one-hot编码 one_hot = tf.one_hot(y, n_classes, 1.0, 0.0) # n_classes为类别总数, n为类别标签 # one_hot = tf.cast(one_hot_int, tf.float32) teacher_tau = tf.scalar_mul(1.0/args.tau, teacher) # teacher为teacher模型直接输出张量, tau为温度系数T student_tau = tf.scalar_mul(1.0/args.tau, student) # 将模型直接输出logits张量student处于温度系数T objective1 = tf.nn.sigmoid_cross_entropy_with_logits(student_tau, one_hot) objective2 = tf.scalar_mul(0.5, tf.square(student_tau - teacher_tau)) """ student模型最终的损失函数由两部分组成:第一项是由小模型的预测结果与大模型的"软标签"所构成的交叉熵(cross entroy); 第二项为预测结果与普通类别标签的交叉熵。""" tf_loss = (args.lamda * tf.reduce_sum(objective1) + (1-args.lamda) * tf.reduce_sum(objective2)) / batch_size
T的取值通常在1到20之间,参考开源代码中常见的取值为3。在开源实现中,student模型的训练有些是与teacher一起进行的,有些则是teacher训练好后直接利用其输出来指导student训练。
6. 浅层/轻量网络
浅层网络:通过设计一个更浅(层数更少)、结构更紧凑的网络来逼近复杂模型的效果。但浅层网络的表达能力很难与深层网络媲美,因此这种方法仅适用于较简单的问题,例如类别数较少的分类任务。
轻量网络:使用MobileNetV2、ShuffleNetV2等轻量网络结构作为模型的backbone,可以大幅减少模型参数数量。
