3. CMSIS-NN 核心概念:神经网络基础回顾与量化原理
好,咱们进入第三章。这一章我打算把神经网络的基础概念、量化原理,以及 CMSIS-NN 的软件架构串起来讲。你可能会问:“这些基础东西网上到处都是,为什么还要讲?”
嗯,我刚开始做嵌入式 AI 的时候也这么想。结果呢?在 Cortex-M4 上跑一个简单的分类模型,死活跑不起来。后来才发现,我对卷积层的内存布局理解错了,导致数据搬运浪费了大量时间。所以,这一章我会结合我踩过的坑,帮你把基础打扎实。
3.1 神经网络基础回顾
咱们先快速过一遍神经网络的核心组件。别担心,我不会讲太深,够用就行。
3.1.1 卷积层(Convolution Layer)
卷积层是 CNN 的“主力”。它的作用说白了就是提取特征。比如一张图片,边缘、纹理、形状,都是靠卷积核一层层“扫”出来的。
我个人习惯把卷积操作想象成一个“滑动窗口”。一个 3x3 的卷积核,在输入特征图上从左到右、从上到下地滑动,每次做一次点积运算。结果就是输出特征图上的一个像素点。
- 输入通道(C_in):输入数据的深度,比如 RGB 图像就是 3。
- 输出通道(C_out):用了多少个卷积核,就输出多少个通道。
- 卷积核大小(K):常见 3x3、5x5。K 越大,感受野越大。
- 步长(Stride):每次滑动的步长。步长=2 会让输出尺寸减半。
- 填充(Padding):在输入边缘补 0,用来控制输出尺寸。
我在项目中遇到过一个问题:在 STM32F4 上部署一个 5x5 卷积层,发现推理时间比预期长了一倍。后来一查,是内存访问不连续导致的。CMSIS-NN 的卷积实现就针对这种场景做了优化,后面我们会细讲。
3.1.2 池化层(Pooling Layer)
池化层的作用是“降采样”。它不学参数,只是对每个通道独立操作。最常见的两种:
- 最大池化(Max Pooling):取窗口内的最大值。保留最显著的特征。
- 平均池化(Average Pooling):取窗口内的平均值。信息更平滑。
你想想看,为什么需要池化?说白了,就是为了减少计算量,同时防止过拟合。一张 224x224 的图片,经过几次池化,可能变成 28x28,数据量直接缩到原来的 1/64。
3.1.3 全连接层(Fully Connected Layer)
全连接层,也叫“密集层”。它的每个输出节点都和所有输入节点相连。说白了,就是一个矩阵乘法加上偏置。
全连接层是参数量的大头。一个 1024 输入、512 输出的全连接层,就有 1024×512 = 524,288 个参数。在 MCU 上,这很容易把 Flash 撑爆。
我记得有一次,一个客户想把一个 MobileNet 模型移植到 Cortex-M7 上。模型本身不大,但最后的全连接层有 1000 个输出。我建议他把输出降到 10 个(只识别 10 类),参数量直接少了两个数量级。效果还不错。
3.2 量化原理:int8/int16
量化,是嵌入式 AI 的核心。没有量化,模型在 MCU 上根本跑不动。
为什么?因为浮点运算在 Cortex-M 上太慢了。一个 float32 的乘法,可能需要几十个时钟周期。而 int8 的乘法,一个周期就能搞定。而且,int8 的数据只占 1 个字节,内存带宽压力也小得多。
3.2.1 量化映射公式
量化的本质,就是把浮点数映射到整数。最常见的映射方式是对称量化:
// 量化公式
int8_value = (float32_value / scale) + zero_point
// 反量化公式
float32_value = (int8_value - zero_point) * scale
其中:
- scale(缩放因子):一个浮点数,表示每个整数步长对应的浮点范围。
- zero_point(零点):整数,表示浮点 0 对应的整数值。
举个例子:假设 scale=0.1,zero_point=0。那么浮点数 1.5 量化后就是 int8(15)。反过来,int8(15) 反量化后就是 1.5。
3.2.2 int8 vs int16 的选择
CMSIS-NN 同时支持 int8 和 int16 量化。怎么选?我一般遵循这个原则:
| 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| int8 | 速度快,内存占用小 | 精度较低,动态范围窄 | 分类任务、对精度要求不高的场景 |
| int16 | 精度高,动态范围宽 | 速度慢,内存占用大 | 回归任务、语音识别、对精度敏感的场景 |
我个人习惯:如果模型精度在 int8 下能保持在 98% 以上,我就用 int8。否则,我会尝试 int16。当然,你也可以用混合精度——某些层用 int8,某些层用 int16。CMSIS-NN 支持这种灵活配置。
3.3 CMSIS-NN 软件架构
好,基础讲完了。咱们来看看 CMSIS-NN 到底是怎么组织的。
CMSIS-NN 是 ARM 官方推出的神经网络推理库,专门针对 Cortex-M 系列处理器优化。它的核心思想是:用查表法、SIMD 指令、以及精心优化的内存布局,把神经网络推理做到极致。
3.3.1 整体架构
CMSIS-NN 的架构可以分为三层:
- 应用层(Application Layer):用户代码,负责调用 CMSIS-NN 的 API。
- 中间层(Middle Layer):提供各种神经网络层的实现,比如卷积、池化、全连接。
- 底层(Low-Level Layer):提供基础数学运算,比如矩阵乘法、激活函数、数据搬运。
下面这张图,是我自己画的 CMSIS-NN 架构图。你可以看到数据是怎么从应用层流到底层的。
3.3.2 核心模块
CMSIS-NN 的核心模块包括:
- arm_convolve_s8():int8 卷积实现。支持 im2col + 矩阵乘法优化。
- arm_avgpool_s8():int8 平均池化。
- arm_maxpool_s8():int8 最大池化。
- arm_fully_connected_s8():int8 全连接层。
- arm_relu6_s8():ReLU6 激活函数。
- arm_softmax_s8():Softmax 输出层。
每个函数都针对 Cortex-M4/M7/M33 的 SIMD 指令做了手写汇编优化。你直接调用就行,不用关心底层实现。
3.3.3 内存管理
在 MCU 上,内存是稀缺资源。CMSIS-NN 提供了一套内存管理机制:
- 静态内存分配:在编译时确定缓冲区大小,避免动态分配。
- 缓冲区复用:多个层可以共享同一个缓冲区,减少内存占用。
- 对齐访问:所有缓冲区都按 16 字节对齐,确保 SIMD 指令能正常工作。
我曾经在一个项目中,因为缓冲区没有对齐,导致 CMSIS-NN 的卷积函数跑出了错误结果。查了两天才发现是内存对齐的问题。所以,我建议你在分配缓冲区时,一定要用 __attribute__((aligned(16))) 或者 arm_memory_aligned_alloc()。
3.4 本章小结
这一章,我们回顾了神经网络的基础知识,理解了量化原理,也搞清楚了 CMSIS-NN 的软件架构。这些内容,是后续实战的基础。