3. 模型轻量化技术(上):剪枝、量化与知识蒸馏
各位同学,欢迎来到第三章。前两章我们聊了边缘AI的硬件选型和部署框架,算是把「战场」和「武器」摸清了。今天开始,咱们要动真格的了——怎么把一个动辄几百兆的大模型,塞进只有几兆内存的芯片里?
说白了,就是给模型「减肥」。我这些年做过的项目,从智能门锁到工业质检,几乎每个都逃不过这一步。今天先讲三个最核心的招数:剪枝、量化、知识蒸馏。这期是上篇,咱们把原理和基础实战讲透。
3.1 剪枝:砍掉没用的神经元
剪枝这个概念,你想想看,跟种树剪枝是一个道理。树长得太密,营养跟不上,剪掉一些旁枝,主干反而长得更好。神经网络也一样,很多参数其实没啥用,砍掉它们,模型照样跑得欢。
3.1.1 非结构化剪枝
非结构化剪枝,说白了就是「零散地砍」。它会逐个检查权重参数,把那些接近0的、对输出影响微乎其微的权重直接置0。这样做的好处是精度损失极小,我试过剪掉50%的参数,精度只掉了0.3%。
但问题也很明显——稀疏矩阵。你想想,权重矩阵里到处都是0,普通硬件根本没法加速。除非你用专门的稀疏计算库,否则推理速度反而会变慢。我在一个智能音箱项目里踩过这个坑,剪完模型小了,跑起来却更慢了,气得我...嗯,后来换了结构化剪枝。
# PyTorch 非结构化剪枝示例
import torch
import torch.nn.utils.prune as prune
model = YourModel()
# 对卷积层进行 L1 非结构化剪枝,砍掉 30% 的权重
prune.l1_unstructured(module=model.conv1, name='weight', amount=0.3)
# 剪枝后需要移除重参数化掩码
prune.remove(module=model.conv1, name='weight')
3.1.2 结构化剪枝
结构化剪枝就聪明多了。它不砍单个权重,而是整行整列地砍,或者直接砍掉整个卷积核。这样做的好处是,剪完后的模型还是密集矩阵,普通硬件就能直接加速。
我个人习惯用通道剪枝。比如一个卷积层有64个输出通道,我通过评估每个通道的重要性,砍掉最不重要的16个。模型体积直接缩小25%,推理速度提升也很明显。
# 基于 BN 层 γ 值的通道剪枝思路
# 1. 训练时对 BN 层的 γ 施加 L1 正则化
# 2. 训练完成后,按 γ 值排序
# 3. 砍掉 γ 值最小的通道
def prune_channels(model, prune_ratio=0.3):
# 获取所有 BN 层的 γ 值
gamma_list = []
for module in model.modules():
if isinstance(module, torch.nn.BatchNorm2d):
gamma_list.append(module.weight.data.abs())
# 计算全局阈值
all_gamma = torch.cat([g.view(-1) for g in gamma_list])
threshold = torch.quantile(all_gamma, prune_ratio)
# 根据阈值剪枝
# ...(具体剪枝逻辑略)
3.2 量化:用更少的比特说话
量化,说白了就是「降精度」。模型训练时用 FP32(32位浮点数),推理时换成 INT8(8位整数)甚至 INT4。你想想,32位变成8位,内存占用直接降到1/4,推理速度还能翻倍。
我曾经在一个边缘盒子上部署人脸识别模型,FP32 版本跑一帧要 120ms,量化到 INT8 后只要 35ms。客户当场就竖了大拇指。
3.2.1 INT8 量化
INT8 量化是目前工业界最常用的方案。它把 FP32 的数值范围映射到 [-128, 127] 的整数范围。映射方式有两种:
| 量化方式 | 原理 | 精度损失 | 适用场景 |
|---|---|---|---|
| 对称量化 | 以0为中心,正负对称 | 较小 | 权重分布对称的模型 |
| 非对称量化 | 根据实际 min/max 映射 | 更小 | 激活值分布偏斜的模型 |
# 使用 TensorRT 进行 INT8 量化
import tensorrt as trt
# 创建 INT8 配置
config = builder.create_builder_config()
config.set_flag(trt.BuilderFlag.INT8)
# 设置校准数据集
calibrator = MyCalibrator(calibration_data)
config.int8_calibrator = calibrator
# 构建引擎
engine = builder.build_engine(network, config)
3.2.2 FP16 量化
FP16 是半精度浮点数,比 INT8 精度高,但压缩率没那么大。它特别适合那些对精度要求高、但硬件支持 FP16 加速的场景。比如 NVIDIA 的 Jetson 系列,跑 FP16 比 FP32 快一倍。
我个人建议:能上 INT8 就上 INT8,实在不行再退而求其次用 FP16。毕竟边缘设备的内存就那么点,能省一点是一点。
3.3 知识蒸馏:大模型教小模型
知识蒸馏,这个比喻特别形象。就像一个老教授(大模型)把自己的知识提炼成精华,教给小学生(小模型)。小模型学到的不是死记硬背的答案,而是老教授的「思维方式」。
具体怎么做呢?大模型输出的不是硬标签(比如「猫」),而是软标签(比如「猫 0.8,狗 0.15,兔子 0.05」)。这个软标签里包含了丰富的类间相似度信息,小模型学了这个,泛化能力会强很多。
# 知识蒸馏训练代码框架
def distillation_loss(student_logits, teacher_logits, labels, T=4, alpha=0.7):
# 硬标签损失(交叉熵)
hard_loss = F.cross_entropy(student_logits, labels)
# 软标签损失(KL散度)
soft_student = F.log_softmax(student_logits / T, dim=1)
soft_teacher = F.softmax(teacher_logits / T, dim=1)
soft_loss = F.kl_div(soft_student, soft_teacher, reduction='batchmean')
# 加权组合
return alpha * hard_loss + (1 - alpha) * T * T * soft_loss
这里有个小技巧:温度 T 是个超参数。T 越大,软标签越平滑,类间信息越丰富。我一般从 T=4 开始调,效果不好再加减。
3.4 三种技术的组合策略
在实际项目中,这三种技术往往不是单独用的。我总结了一个常用的组合流程:
- 先剪枝:用结构化剪枝砍掉 20%-30% 的通道,模型体积先瘦一圈
- 再量化:把 FP32 量化到 INT8,内存占用再降 75%
- 最后蒸馏:用原始大模型做教师,对剪枝量化后的小模型进行蒸馏,把精度拉回来
这个流程我用了很多次,基本都能把模型压缩到原来的 1/10 以内,精度损失控制在 1% 以内。
好了,这一章的内容就到这里。剪枝、量化、蒸馏,这三板斧是边缘AI模型轻量化的基本功。下期咱们接着聊更高级的轻量化技术,比如神经架构搜索和重参数化。到时候见。