第三章 模型选型与评估:大小与精度的博弈
做嵌入式AI,说白了就是戴着镣铐跳舞。芯片资源就那么多,你不可能把几百兆的大模型塞进去。我刚开始接触这个领域时,也犯过傻——把ResNet-152直接往STM32上怼,结果编译都过不了。嗯,从那以后我学乖了,选模型前先算笔账。
3.1 模型大小与精度的权衡
这是个永恒的话题。你想想看,模型越大,参数越多,理论上精度越高。但嵌入式设备不是服务器,它没有无限的内存和算力。我个人习惯把这个问题拆成三个维度来看:
- 存储空间:Flash就那么大,模型文件不能超过可用空间
- 运行内存:推理时中间特征图占用的RAM,这个经常被忽略
- 计算能力:MCU的主频和是否有硬件加速器
我在项目中遇到过最典型的情况:一个2MB的模型,Flash够用,但推理时中间特征图需要800KB的RAM,而芯片总共只有1MB。结果就是跑不起来。所以选模型时,不光要看模型文件大小,还得算算峰值内存占用。
核心原则:精度每提升1%,如果代价是模型体积翻倍,那在嵌入式场景下通常不值得。我们追求的是「够用就好」。
3.2 常用轻量化模型
这几年轻量化模型层出不穷,我挑三个最常用的聊聊。它们各有各的绝活,也各有各的坑。
3.2.1 MobileNet系列
MobileNet的核心思想是深度可分离卷积。什么意思呢?就是把标准卷积拆成两步:先逐通道卷积,再逐点卷积。计算量能降到原来的十分之一左右。
我记得第一次在Cortex-M4上跑MobileNetV1,效果还不错,但速度还是有点慢。后来换了MobileNetV2,引入了倒残差结构和线性瓶颈,同样的精度下速度快了将近一倍。V3就更猛了,用上了NAS搜索出来的结构,但说实话,在低端MCU上V2反而更稳。
# MobileNetV2的倒残差结构示例
class InvertedResidual(nn.Module):
def __init__(self, inp, oup, stride, expand_ratio):
super().__init__()
hidden_dim = int(inp * expand_ratio)
self.use_res_connect = stride == 1 and inp == oup
layers = []
if expand_ratio != 1:
# 1x1升维
layers.append(ConvBNReLU(inp, hidden_dim, kernel_size=1))
layers.extend([
# 3x3深度可分离卷积
ConvBNReLU(hidden_dim, hidden_dim, stride=stride, groups=hidden_dim),
# 1x1降维,无激活函数
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
])
self.conv = nn.Sequential(*layers)
避坑指南:我曾经在MobileNetV2的倒残差结构中,把expand_ratio设得太大(比如6),结果中间特征图的内存占用暴涨。在资源紧张的芯片上,建议从expand_ratio=3开始试。
3.2.2 ShuffleNet系列
ShuffleNet另辟蹊径,它用分组卷积加通道混洗来减少计算量。说白了,就是把输入通道分成几组,每组独立做卷积,然后通过混洗操作让信息在组间流动。
ShuffleNetV2更绝,它提出了一条实用准则:模型的推理速度不光看FLOPs,还得看内存访问成本。这个观点我特别认同。有些模型FLOPs很低,但实际跑起来慢得要命,就是因为频繁的内存读写拖了后腿。
| 模型 | FLOPs | 参数量 | 我的评价 |
|---|---|---|---|
| ShuffleNetV1 | ~140M | ~1.3M | 分组卷积容易导致精度损失 |
| ShuffleNetV2 | ~146M | ~1.4M | 实际推理速度比V1快,推荐 |
3.2.3 EfficientNet-Lite
EfficientNet本身是用NAS搜出来的,效果确实好。但原版EfficientNet用了Swish激活函数和Sigmoid注意力机制,在嵌入式设备上部署时,这些操作没有硬件加速,反而变慢了。
EfficientNet-Lite就是针对这个问题做的优化:把Swish换成ReLU,去掉SE模块,调整分辨率。我试过在树莓派上跑,效果比MobileNetV3略好,但模型体积也大了一圈。
注意:EfficientNet-Lite虽然叫「Lite」,但它的参数量其实比MobileNetV2大。如果你的Flash空间小于1MB,建议优先考虑MobileNetV2或ShuffleNetV2。
3.3 模型评估指标
选模型不能光看精度,还得看三个硬指标:FLOPs、参数量、推理延迟。这三个指标各有侧重,我一个个说。
3.3.1 FLOPs:理论计算量
FLOPs(浮点运算次数)衡量的是模型需要做多少次乘加运算。它是个理论值,跟硬件无关。我一般用它来做快速筛选——两个模型精度差不多,FLOPs低的那个优先考虑。
但要注意,FLOPs低不代表实际跑得快。为什么?因为内存带宽、数据复用率、硬件并行度都会影响实际速度。我曾经遇到一个模型,FLOPs只有MobileNetV2的一半,但在某款芯片上跑起来反而更慢,就是因为它的内存访问模式不友好。
3.3.2 参数量:模型大小
参数量直接决定了模型文件的大小。一般来说,一个float32的权重占4字节,所以参数量乘以4就是模型文件的大致大小。比如1M参数,模型文件大约4MB。
但这里有个坑:有些框架会做模型压缩,比如把权重从float32量化到int8,那模型文件就能缩小到原来的四分之一。所以参数量只是参考,实际部署时还得看量化后的体积。
3.3.3 推理延迟:最实在的指标
说一千道一万,最终还是要看模型在目标硬件上跑一次需要多长时间。我个人习惯在选型阶段就做一次快速基准测试:
- 把候选模型都转成目标格式(比如TFLite或ONNX)
- 在目标芯片上跑100次推理,取平均值
- 记录峰值内存占用
我曾经在项目里用这个方法,发现某个号称「轻量」的模型,实际推理延迟比另一个模型高了3倍。查了半天,原来是它的某些算子没有硬件加速,全靠CPU硬算。所以,永远不要只看论文里的数据,一定要在自己硬件上跑一遍。
我的选型流程:
- 第一步:根据Flash大小筛选参数量(留30%余量给系统)
- 第二步:根据RAM大小筛选峰值内存(留50%余量给中间变量)
- 第三步:在目标硬件上跑基准测试,看延迟是否满足实时性要求
- 第四步:如果以上都满足,再对比精度,选最高的那个
嗯,模型选型这块就聊这么多。记住一句话:没有最好的模型,只有最合适的模型。下一章我们聊聊模型压缩,到时候会讲怎么把选好的模型进一步瘦身,让它能在更小的芯片上跑起来。