模型轻量化技术:让AI模型在嵌入式设备上跑起来
做嵌入式AI部署,最头疼的问题是什么?
模型太大,跑不动。算力不够,内存吃紧。我刚开始接触这个领域时,总觉得把训练好的模型直接丢到芯片上就能用。结果呢?一个几十MB的模型,在Cortex-M上根本跑不起来,连加载都加载不完。
所以,模型轻量化不是「锦上添花」,而是「生死攸关」。今天我就把这几年积累的轻量化经验,掰开揉碎了讲给你听。
模型剪枝:把「赘肉」剪掉
模型剪枝,说白了就是去掉那些不重要的参数。神经网络里有很多权重其实接近于零,留着它们只会浪费计算资源。
非结构化剪枝
这种方式比较「暴力」——直接把小权重置为零。我习惯用阈值法:设定一个阈值,低于这个值的权重全部干掉。
import torch
import torch.nn.utils.prune as prune
# 非结构化剪枝示例
model = YourModel()
# 对卷积层进行剪枝,保留20%的权重
prune.l1_unstructured(model.conv1, name='weight', amount=0.8)
# 剪枝后需要重新训练恢复精度
注意:非结构化剪枝后的模型是稀疏的。很多硬件并不支持稀疏矩阵加速,所以实际推理速度可能不降反升。我在项目中踩过这个坑,剪枝后模型小了,但跑起来反而更慢。
结构化剪枝
这个就聪明多了——直接剪掉整个通道或整个卷积核。硬件友好,加速效果明显。
# 结构化剪枝:按通道剪枝
# 计算每个通道的L1范数,保留重要的通道
def channel_prune(model, prune_ratio=0.3):
for name, module in model.named_modules():
if isinstance(module, nn.Conv2d):
# 计算每个卷积核的L1范数
l1_norm = torch.norm(module.weight.data, p=1, dim=(1,2,3))
# 找到要剪掉的通道索引
num_prune = int(module.out_channels * prune_ratio)
_, indices = torch.topk(l1_norm, num_prune, largest=False)
# 这里需要实际修改网络结构
# 具体实现略复杂,建议用torch-pruning库
我的经验:结构化剪枝一般剪掉20%-50%的参数,精度损失可以控制在1%以内。超过50%就要小心了,模型可能会「崩」。
权重量化:用更少的比特表示
量化,就是把FP32的浮点数变成INT8甚至INT4。你想想看,一个参数从32位变成8位,模型直接缩小4倍。这买卖划算吧?
INT8量化
这是目前最成熟、应用最广的方案。TensorRT、ONNX Runtime、TFLite都支持得很好。
# PyTorch量化示例
import torch.quantization as quant
model = YourModel()
model.eval()
# 配置量化
model.qconfig = quant.get_default_qconfig('fbgemm')
model = quant.prepare(model, inplace=True)
# 校准:用少量数据跑一遍,收集激活值的范围
calibrate(model, calib_loader)
# 转换量化模型
model = quant.convert(model, inplace=True)
| 量化类型 | 模型大小缩减 | 精度损失 | 硬件要求 |
|---|---|---|---|
| FP32 | 1x | 基准 | 通用 |
| INT8 | 4x | <1% | 需要INT8加速单元 |
| INT4 | 8x | 1-3% | 部分芯片支持 |
INT4量化
这个就比较激进了。我曾在某个项目中尝试INT4量化,模型小了8倍,但精度掉了3%。后来通过量化感知训练(QAT)才把精度拉回来。
关键点:INT4量化一定要做量化感知训练。后训练量化(PTQ)在INT4上基本不可用,精度损失太大。
知识蒸馏:大模型教小模型
知识蒸馏的思路很有意思——用一个大的教师模型去训练一个小学生模型。教师模型知道的知识,通过软标签(soft label)传递给学生。
# 知识蒸馏损失函数
def distillation_loss(student_logits, teacher_logits, labels, T=4, alpha=0.7):
# 软标签损失:让学生模仿教师的输出分布
soft_loss = nn.KLDivLoss()(
F.log_softmax(student_logits / T, dim=1),
F.softmax(teacher_logits / T, dim=1)
) * (T * T)
# 硬标签损失:让学生学会正确分类
hard_loss = nn.CrossEntropyLoss()(student_logits, labels)
return alpha * soft_loss + (1 - alpha) * hard_loss
温度T是个超参数。T越大,软标签越平滑,学生学到的东西越「泛化」。我一般设T=4,alpha=0.7,效果比较稳。
避坑指南:我曾经把教师模型设得太大,结果学生模型根本学不动。教师模型的大小最好是学生模型的3-5倍,太大了反而不好。
轻量化网络设计:从源头解决问题
与其在训练好的模型上做剪枝量化,不如一开始就设计轻量化的网络结构。MobileNet、ShuffleNet、EfficientNet-Lite,这三兄弟是嵌入式AI的「三驾马车」。
MobileNetV3
深度可分离卷积 + 倒残差结构 + SE注意力。我特别喜欢它的h-swish激活函数,计算量小,效果还不赖。
# MobileNetV3的核心模块
class InvertedResidual(nn.Module):
def __init__(self, inp, oup, stride, expand_ratio):
super().__init__()
hidden_dim = int(round(inp * expand_ratio))
self.use_res_connect = stride == 1 and inp == oup
layers = []
if expand_ratio != 1:
# 1x1升维
layers.append(nn.Conv2d(inp, hidden_dim, 1, bias=False))
layers.append(nn.BatchNorm2d(hidden_dim))
layers.append(nn.Hardswish())
# 3x3深度可分离卷积
layers.extend([
nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1,
groups=hidden_dim, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.Hardswish(),
# 1x1降维
nn.Conv2d(hidden_dim, oup, 1, bias=False),
nn.BatchNorm2d(oup),
])
self.conv = nn.Sequential(*layers)
ShuffleNetV2
这个网络的设计原则很实在——考虑实际硬件特性。通道混洗(channel shuffle)操作让信息在不同组之间流动,计算量小,精度高。
EfficientNet-Lite
EfficientNet的嵌入式版本。去掉了SE模块(嵌入式上跑不动),改用ReLU6激活函数。我实测过,在树莓派上比MobileNetV3快15%左右。
选型建议:
- 算力紧张(<1 GOPS):选ShuffleNetV2,极致轻量
- 算力适中(1-10 GOPS):选MobileNetV3,平衡性好
- 算力充裕(>10 GOPS):选EfficientNet-Lite,精度优先
组合拳:多种技术一起上
实际项目中,我很少只用一种技术。通常是先设计轻量化网络,然后做结构化剪枝,再INT8量化,最后用知识蒸馏微调。这一套组合拳打下来,模型能缩小10-20倍,精度损失控制在1%以内。
举个例子,我去年做的一个视觉检测项目:
- 基础模型:MobileNetV3-Large,4.2MB
- 结构化剪枝30%:降到2.9MB,精度掉0.3%
- INT8量化:降到0.73MB,精度再掉0.5%
- 知识蒸馏微调:精度恢复0.4%
最终模型0.73MB,精度只比原始模型低0.4%。在STM32H7上跑到了30fps,完美满足需求。
最后提醒一句:轻量化不是目的,部署才是。每做一步优化,都要在目标硬件上实测。我见过太多人在PC上优化得挺好,一上板子就翻车。嗯,我自己也翻过几次。