3、机器学习基础回顾:神经网络核心概念、卷积层、池化层、全连接层、激活函数
在进入 TinyML 的模型转换与 MCU 部署之前,我们必须先建立对神经网络基本构件的清晰认知。TinyML 的核心挑战在于:如何在资源受限的微控制器上运行神经网络推理。这要求我们不仅要理解模型的功能,更要理解每一层计算对内存、算力和数据流的影响。本章将系统回顾神经网络的核心概念,重点剖析卷积层、池化层、全连接层和激活函数的数学本质与工程特性,为后续 ONNX 模型结构分析和量化部署打下坚实基础。
上图展示了神经网络中四种核心层在 TinyML 部署视角下的关键差异。从输入张量开始,数据依次经过卷积层提取特征、池化层降低空间维度、全连接层进行高级语义组合,并在每层之后(或之中)应用激活函数引入非线性。在 MCU 部署时,我们必须同时关注每一层的参数存储量、推理时的激活值内存占用以及计算复杂度,这三者共同决定了模型能否在给定 Flash 和 RAM 预算内运行。
3.1 神经网络核心概念:从神经元到张量运算
核心原理:神经网络的基本计算单元是神经元,它模拟生物神经元的“加权求和 + 非线性激活”过程。在数学上,一个神经元接收多个输入 \(x_1, x_2, ..., x_n\),每个输入对应一个权重 \(w_i\),加上偏置 \(b\),通过激活函数 \(f\) 输出结果:\(y = f(\sum_{i=1}^{n} w_i x_i + b)\)。在 TinyML 中,我们通常处理的是已经训练好的模型,因此关注点从“如何训练”转向“如何高效执行推理”。
张量(Tensor)是神经网络中数据的基本组织形式。一个典型的图像输入张量形状为 (C, H, W),其中 C 表示通道数(如 RGB 图像为 3),H 和 W 分别表示高度和宽度。在 ONNX 模型中,张量的维度顺序通常遵循 (N, C, H, W),N 为批大小(batch size)。在 MCU 部署时,批大小通常固定为 1,以节省内存。
判断方法:当我们拿到一个 ONNX 模型时,首先需要识别每一层的输入输出张量形状。这可以通过 Netron 可视化工具或 ONNX Runtime 的模型分析接口完成。关键指标包括:
- 参数量(Parameters):权重和偏置的总数,直接决定 Flash 占用。
- 激活值大小(Activation Size):每一层输出的张量尺寸,决定运行时 RAM 峰值。
- 乘加运算次数(MACs):影响推理延迟和功耗。
实战场景:假设我们有一个输入为 (1, 3, 32, 32) 的图像分类模型,第一层卷积使用 16 个 3×3 卷积核。该层参数量为 16×3×3×3 + 16 = 448 个参数(约 1.75KB 以 float32 存储),而输出激活值为 16×32×32 = 16384 个值(float32 下为 64KB)。可见,激活值内存往往远大于权重内存,这在 MCU 设计中是必须优先考虑的因素。
常见误区:初学者常误以为“模型大小”仅指权重文件大小。实际上,在 MCU 上运行时,激活值内存(RAM)往往比权重存储(Flash)更紧张。例如,一个 100KB 的权重模型可能需要 200KB 以上的 RAM 来存储中间激活值。因此,在模型选型时必须同时评估 Flash 和 RAM 预算。
3.2 卷积层:局部特征提取的基石
核心原理:卷积层通过一组可学习的卷积核(filter/kernel)在输入特征图上滑动,计算局部区域的加权和。其核心优势在于权重共享和局部连接,极大减少了参数量。一个卷积核在空间维度上共享同一组权重,因此无论输入图像多大,卷积层的参数量只与卷积核尺寸、输入通道数和输出通道数有关。
卷积输出的空间尺寸计算公式为:\(OutputSize = \lfloor (InputSize - KernelSize + 2 \times Padding) / Stride \rfloor + 1\)。其中 Padding 用于保持边界信息,Stride 控制滑动步长。在 TinyML 中,我们通常使用较小的卷积核(如 3×3)和 Stride=1,以平衡特征提取能力和计算量。
操作流程:在 ONNX 模型中,卷积层对应 Conv 算子。其关键属性包括 dilations(空洞卷积)、group(分组卷积)、kernel_shape、pads 和 strides。在部署到 MCU 前,我们需要确认这些属性是否被目标推理框架(如 TensorFlow Lite Micro、CMSIS-NN)支持。
| 卷积参数 | 典型值 | 对 MCU 部署的影响 |
|---|---|---|
| 卷积核尺寸 | 3×3 / 1×1 | 3×3 是主流选择;1×1 用于通道变换,计算量更小 |
| Stride | 1 或 2 | Stride=2 可替代池化层,但可能丢失位置信息 |
| Padding | Same / Valid | Same 保持尺寸,但增加边缘计算量 |
| 输出通道数 | 16/32/64 | 直接决定激活值内存和计算量 |
案例拆解:以 MobileNetV1 中的深度可分离卷积为例,它将标准卷积分解为深度卷积(Depthwise)和逐点卷积(Pointwise)。假设输入为 32×32×3,输出为 32×32×64:标准卷积需要 3×3×3×64 = 1728 个参数;而深度可分离卷积仅需 3×3×3 + 1×1×3×64 = 27 + 192 = 219 个参数,参数量减少约 87%。在 MCU 上,这意味着 Flash 占用大幅降低,但需要注意深度卷积在底层实现时可能不如标准卷积优化充分,需要验证推理框架的支持程度。
经验提醒:在 TinyML 模型选择时,优先考虑深度可分离卷积结构(如 MobileNet、EfficientNet-Lite)。但务必在目标 MCU 上实测推理速度,因为某些微控制器内核(如 Cortex-M0+)对深度卷积的 SIMD 优化支持较弱,可能导致实际加速效果不如预期。
3.3 池化层:空间降维与特征压缩
核心原理:池化层(Pooling)是一种非参数化的下采样操作,常用的有最大池化(Max Pooling)和平均池化(Average Pooling)。最大池化取窗口内的最大值,强调最显著的特征;平均池化取窗口内均值,保留整体统计信息。池化层的主要作用是降低特征图的空间分辨率,从而减少后续层的计算量和参数量,同时在一定程度上防止过拟合。
判断方法:在 ONNX 模型中,池化层对应 MaxPool 或 AveragePool 算子。关键属性包括 kernel_shape(池化窗口大小)、strides 和 pads。在 TinyML 中,池化层不包含可学习参数,因此不占用 Flash,但会影响激活值内存。例如,一个 32×32×16 的特征图经过 2×2 最大池化后变为 16×16×16,激活值减少为原来的 1/4。
实战场景:在关键字唤醒(KWS)模型中,输入为 49×10 的 MFCC 特征图,经过两层卷积后接一个 2×2 最大池化,将时间维度和频率维度各减半。这不仅减少了计算量,还增强了模型对时间偏移的鲁棒性。在 MCU 上实现时,池化层通常使用简单的循环遍历即可高效完成,无需特殊优化。
常见误区:不要盲目堆叠池化层。过度池化会导致空间信息丢失,尤其对于小尺寸输入(如 32×32 图像),一次 2×2 池化后尺寸变为 16×16,再池化一次变为 8×8,此时特征图已经非常小,继续池化可能损失关键细节。在 TinyML 中,建议用 Stride=2 的卷积替代部分池化,以保留更多可学习参数。
3.4 全连接层:特征组合与分类决策
核心原理:全连接层(Fully Connected Layer)将前一层的所有神经元与当前层的每个神经元进行连接。其数学本质是矩阵乘法:\(y = Wx + b\)。全连接层通常出现在网络的末端,用于将卷积层提取的局部特征组合成全局语义信息,最终输出分类概率或回归值。
全连接层的参数量极大。例如,输入为 8×8×64 = 4096 个神经元,输出为 128 个神经元,则权重矩阵大小为 4096×128 = 524,288 个参数(float32 下约 2MB)。这在 MCU 上是不可接受的。因此,TinyML 模型通常采用全局平均池化(Global Average Pooling)替代全连接层,将特征图直接降维为 1×1×C,再连接一个轻量级分类头。
操作流程:在 ONNX 模型中,全连接层对应 Gemm 或 MatMul 算子。部署时需注意:
- 确认权重矩阵的布局(是否转置),ONNX 中
Gemm的transB=1表示权重矩阵已转置。 - 检查偏置是否存在,以及是否与 BatchNorm 层融合。
- 在量化部署时,全连接层对量化误差更敏感,建议优先使用逐通道量化。
| 结构方案 | 参数量(示例) | MCU 可行性 | 适用场景 |
|---|---|---|---|
| 传统全连接层 | 4096×128 ≈ 524K | 不可行(Flash 超限) | 仅适用于大算力设备 |
| 全局平均池化 + 全连接 | 64×10 = 640 | 可行 | 大多数 TinyML 分类任务 |
| 全局平均池化 + 1×1 卷积 | 64×10 = 640 | 可行且更易量化 | MCU 上的高效分类头 |
案例拆解:以 TensorFlow Lite for Microcontrollers 官方提供的“Person Detection”模型为例,该模型采用 MobileNetV1 结构,最后使用全局平均池化将 7×7×1024 的特征图压缩为 1024 维向量,再通过一个 1024×2 的全连接层输出“人/非人”的二分类结果。整个模型参数量约 250KB(int8 量化后),在 Cortex-M4 上推理一次约需 300ms。若改用传统全连接层,参数量将膨胀至 7×7×1024×2 ≈ 100K,虽然看似不大,但加上前面的卷积层后总参数量会显著增加,且全连接层的矩阵乘法在 MCU 上难以利用 SIMD 加速。
经验提醒:在将 PyTorch/Keras 模型转换为 ONNX 时,务必检查全连接层的输入是否经过了 Flatten 操作。ONNX 中的 Flatten 算子会改变张量形状,但不会增加参数量。在 MCU 推理框架中,Flatten 通常被隐式处理,但需要确认输入张量的内存布局是连续的,否则可能导致数据拷贝开销。
3.5 激活函数:非线性表达与量化友好性
核心原理:激活函数为神经网络引入非线性,使其能够逼近任意复杂函数。没有激活函数,多层线性变换将退化为单层线性变换。常用的激活函数包括:
- ReLU:\(f(x) = max(0, x)\),计算简单,梯度不饱和,是 TinyML 中最常用的激活函数。
- Sigmoid:\(f(x) = 1 / (1 + e^{-x})\),输出范围 (0,1),但存在梯度消失和指数运算开销。
- Tanh:\(f(x) = (e^x - e^{-x}) / (e^x + e^{-x})\),输出范围 (-1,1),同样存在指数运算。
- Hard Sigmoid / Hard Swish:分段线性近似,专为移动端和嵌入式设备设计。
判断方法:在 ONNX 模型中,激活函数对应 Relu、Sigmoid、Tanh 等算子。在 MCU 部署时,ReLU 只需比较操作,几乎零开销;而 Sigmoid 需要计算指数,通常采用查表法或多项式近似。在 int8 量化模型中,ReLU 可以直接融合到前一层的输出中(即 clip 操作),不增加额外计算。
实战场景:在将模型量化为 int8 时,ReLU 的量化参数(scale 和 zero_point)可以与前一层合并,减少一次量化/反量化操作。例如,在 CMSIS-NN 中,卷积层后接 ReLU 通常被实现为带 ReLU 的卷积核函数,直接对 int32 累加结果进行 clip 操作,无需额外内存访问。
量化对比:下表展示了不同激活函数在 int8 量化下的实现复杂度:
| 激活函数 | int8 实现方式 | 额外开销 | 推荐度 |
|---|---|---|---|
| ReLU | 直接 clip 到 [0, 127] | 无 | ★★★★★ |
| Hard Sigmoid | 分段线性,查表 | 极小 | ★★★★☆ |
| Sigmoid | 查表(256 项) | 约 256 字节 Flash | ★★★☆☆ |
| Tanh | 查表(256 项) | 约 256 字节 Flash | ★★★☆☆ |
常见误区:在 TinyML 中,应避免使用 Swish 或 GELU 等复杂激活函数。虽然它们在大型模型上表现优异,但在 MCU 上,其指数运算和除法操作会显著增加推理延迟。如果模型原始使用了 Swish,建议在训练阶段将其替换为 Hard Swish(ReLU6 的变体),或在转换时手动替换为分段线性近似。
3.6 综合案例:一个 8 位 MCU 上的图像分类模型分析
为了将上述概念串联起来,我们以一个实际的 MCU 部署场景进行完整分析。假设我们有一个基于 CIFAR-10 数据集的 10 类图像分类模型,输入为 32×32×3,目标平台是 STM32F746(Cortex-M7,1MB Flash,320KB RAM)。
初始模型结构:
Conv2D (3→16, 3×3, stride=1, padding=same) + ReLU
MaxPool (2×2, stride=2)
Conv2D (16→32, 3×3, stride=1, padding=same) + ReLU
MaxPool (2×2, stride=2)
Flatten
Dense (32×8×8=2048 → 128) + ReLU
Dense (128 → 10)
参数与内存估算:
- Conv1 参数:16×3×3×3 + 16 = 448 个(float32 下 1.75KB)
- Conv2 参数:32×3×3×16 + 32 = 4640 个(float32 下 18.1KB)
- Dense1 参数:2048×128 + 128 = 262,272 个(float32 下 1MB)
- Dense2 参数:128×10 + 10 = 1290 个(float32 下 5KB)
- 总参数量:约 268KB(float32),超出 1MB Flash 预算但接近极限
- 激活值峰值:第一层卷积输出 32×32×16 = 16,384 个值(float32 下 64KB),第二层输出 16×16×32 = 8192 个值(32KB),Flatten 后为 2048 个值(8KB)
问题诊断:Dense1 层参数量占比超过 97%,且激活值峰值 64KB 加上权重 268KB 已接近 RAM 上限。若使用 float32,模型无法在 320KB RAM 中运行。
优化方案:
- 将 Dense1 替换为全局平均池化 + Dense(32→10),参数量降至 32×10 + 10 = 330 个。
- 将模型量化为 int8,权重从 float32 的 268KB 降至约 67KB,激活值从 64KB 降至 16KB。
- 将两个卷积层的 padding 改为 valid,使第二层输出尺寸变为 14×14,进一步减少激活值。
优化后结果:总参数量约 5.2KB(int8),激活值峰值约 14KB,Flash 占用约 10KB(含代码),RAM 占用约 30KB。在 STM32F746 上推理一次约需 80ms(使用 CMSIS-NN 优化)。
复盘经验:这个案例揭示了 TinyML 模型设计的核心原则:全连接层是参数大户,应尽量用全局池化替代;激活值内存往往比权重更紧张,需关注特征图尺寸;int8 量化是 MCU 部署的必由之路。在实际项目中,我们应在模型训练阶段就考虑部署约束,而不是在训练完成后再进行压缩。
总结要点:本章回顾了神经网络的核心构件。在 TinyML 中,卷积层负责高效特征提取,池化层用于空间降维,全连接层应谨慎使用,激活函数优先选择 ReLU 族。理解每一层的参数量、激活值大小和计算复杂度,是后续进行 ONNX 模型分析、量化和 MCU 部署的基础。下一章将深入探讨如何将训练好的模型导出为 ONNX 格式,并检查其结构完整性。