经常有人问我:现在满世界都在说AI,我想在单片机上跑个模型识别说话,怎么搜到的教程全是要上云、要连服务器的?这个问题其实很有代表性。它指向的那个方向,圈子里有个专门的名字,叫TinyML,全称是Tiny Machine Learning。简单说,就是在主频只有几十MHz、RAM只有几十到几百KB的微控制器上,把机器学习模型跑起来。关键词唤醒、振动异常检测、手势识别、环境传感分类……这些以往觉得“必须联网才能实现”的功能,现在一颗小小的MCU就能本地搞定。
这篇是“从零开始学习TinyML”系列的第一篇。我不打算一上来堆概念,而是先把TinyML是什么、为什么值得学、整条技术链路长什么样讲清楚,然后带你完整走一遍关键词唤醒项目:从数据准备、模型训练,到量化压缩、嵌入式部署,最后落在实机测试。无论你是嵌入式工程师想学AI,还是算法工程师想理解边缘部署,甚至只是创客想给作品加个“本地智能”,这篇都可以直接参考。读完你至少能知道这条路每一步在干什么、为什么这么干、会遇到哪些坑。
1. 先搞清楚TinyML到底解决什么问题
1.1 什么是TinyML:在指甲盖大的芯片上做推理
很多人第一次听说TinyML,会觉得它是个营销概念。其实这个方向边界很清晰:面向微控制器(MCU)这类资源极度受限设备的机器学习方案。MCU平时出现在哪里?洗衣机控制板、智能门锁、传感器节点、儿童玩具、助听器……这些设备的共同特征是:处理器主频通常只有几十MHz到两百MHz,RAM只有几KB到几百KB,Flash从几十KB到几MB,整板功耗要控制在毫瓦级,很多还得靠纽扣电池供电。
拿云端AI一对比就直观了。数据中心跑一个大模型,需要几百千瓦供电、几十GB内存和GPU阵列;手机跑语音助手,至少需要几GB内存、几瓦功耗;而TinyML的目标,是在指甲盖大小的芯片上,用几毫瓦功耗,把一个小而精的模型的推理跑出可用结果。它跑不了大语言模型,也没必要跑,它解决的是另一类问题:在资源极度受限的前提下,把智能判断放到最靠近数据产生的地方。
1.2 为什么很多场景必须“端侧智能”
分享几个真实场景,感受一下TinyML为什么是刚需。
第一个是语音唤醒。智能音箱你喊一句唤醒词,如果每次都要把语音发到服务器识别,不仅有网络延迟,还会不断把你的声音传出去。在设备端做关键词唤醒,延迟能压到几十毫秒,录音不出设备,隐私问题也就不存在了。第二个是工业预测性维护。把振动传感器装在转机旁边,本地识别异常振动模式,车间没有Wi-Fi覆盖也能长期运行,只需要把“是否异常”这种极小结果传出去。第三个是农业物联网。土壤、温湿度、光照传感节点可能有几千个,如果所有原始数据都上传再分析,带宽和电费都是灾难,更麻烦的是很多农田根本拉不了网线。
这三个场景的共同特点,可以总结为低延迟、数据隐私、低功耗、低带宽。这四个价值点,在云端方案和边缘网关方案里很难同时满足。云端方案延迟高、功耗大、依赖网络;边缘网关方案虽然缓解了带宽,但网关本身还是个大功耗设备,而且所有节点都绕到网关,依然有单点问题。只有在设备端本地推理,才能把功耗压到最低,同时做到数据不出本地。这就是TinyML不可替代的位置。
1.3 学TinyML的价值,以及本系列怎么安排
不同背景的人学这套东西,得到的价值不太一样。嵌入式工程师学会了,手里做的产品就不只是一个执行器,而是一个有感知能力的智能节点,项目的技术含量一下就不一样了;算法工程师学会了,才会真正理解模型在几十KB内存下要怎么设计、怎么取舍,而不是只在服务器上刷榜;创客和学生学会了,作品和项目可以脱离“必须连网才能智能”的尴尬,实现真正的离线工作。
所以这个我会分几期来写。这一篇的重点是建立认知和跑通最小闭环,后续会继续聊模型压缩的进阶技巧、多模态传感器融合、低功耗系统优化,以及如何给自有业务定制模型。大家按自己的节奏跟就行,每一篇尽量做到能独立落地。
2. 认识TinyML的完整技术链路
2.1 五个环节,两条世界线
TinyML整体链路可以拆成五个环节:数据采集与预处理、模型训练、模型压缩、格式转换与工具链集成、端侧部署与推理。这五个环节听起来不复杂,但有一个核心观念必须提前建立:模型训练在PC上,用的是浮点数;模型部署在MCU上,最终落地的是整数。
PC端可以随便跑float32的矩阵乘法,反正CPU慢一点也能等,算完再迭代就行。MCU不行,它没有足够的RAM存大模型权重,也没有足够算力做密集浮点乘法。所以训练和部署之间,必须插一层“压缩转换”,把模型变成MCU能接受的形态。很多人学TinyML栽跟头,就是栽在没理解这个“两条世界线”:训练时是科学实验,可以不断试探;部署时是工程约束,每个字节都要精打细算。从写第一行代码起,脑子里就要同时装这两套逻辑。
2.2 核心手段:量化到底做了什么
模型压缩通常有三件套:量化、剪枝、蒸馏。这一篇重点讲量化,因为它是部署链路里基本绕不开的一步,也是初学者最容易只知其然不知其所以然的部分。
量化简单说,就是把模型里连续的浮点数值,用离散的定点整数近似表达。最常见的做法是int8量化,把每个浮点值映射到[-128, 127]的整数范围,再额外保存缩放因子scale和零点偏移zero_point。推理时用整数做乘加计算,速度快,内存和Flash占用也直接降到接近原来的四分之一。
举个例子:某一层权重原始范围在-1.0到1.0之间,量化时先算scale,也就是每个整数刻度代表多少浮点数:
import numpy as np min_val, max_val = -1.0, 1.0 scale = (max_val - min_val) / 255.0 # 约等于0.00784 # 假设zero_point取0,浮点值0.35对应的量化整数约为 quantized = np.round(0.35 / scale).astype(np.int8) print(quantized) # 45推理之后如果要还原,就套公式real = scale * (int_value - zero_point)。原理听起来简单,但细节里全是坑。比如某一层权重分布非常集中,比如都在0.01到0.05之间,如果还用整个可能的浮点范围去做映射,量化误差会被放大到不可接受。这时候就需要按通道量化、动态范围裁剪,或者换更多bit来量化。所以量化不是一个“转个格式”的机械动作,它本身就是精度和性能的平衡过程。
2.3 硬件平台怎么选:先把RAM和Flash预算算清楚
新手最容易纠结的是选哪块开发板,其实硬件选型逻辑很简单:先定RAM和Flash预算,再看外设,最后看算力。
拿关键词唤醒这个项目来说,模型量化后大约几十KB,运行时还要给Tensor缓冲区、信号处理留内存,所以至少需要一百多KB RAM的MCU才算舒服。入门级的Cortex-M0/M3系列,RAM往往只有十几KB到几十KB,适合跑传感器异常检测这种极轻量的模型;主流级的Cortex-M4系列,RAM通常在128KB到512KB,可以做关键词唤醒、手势识别、振动诊断这类任务;再往上走,带NPU加速的SoC,RAM到了几MB级别,就能跑小型目标检测了。
外设也要提前想清楚。做音频要麦克风接口,做振动检测要加速度计,做图像要摄像头接口。别把模型训练好了,结果发现板子上根本没有对应的采集通道,那就白忙活了。另外,如果MCU带硬件浮点单元,处理FFT这类运算会快不少,不过现在很多嵌入式推理引擎已经全面走整数路径,FPU不是硬性必需。
2.4 工具链选择:决定开发效率的关键
工具链这件事,我入门时吃过不少亏。简单说,TinyML工具链主要由三部分组成:训练框架、转换工具、嵌入式推理引擎。
训练阶段直接用Python生态里常见的深度学习框架就够了,模型训练的方式和普通深度学习没有区别,不需要为MCU做额外动作。转换工具负责把训练好的模型,转换成嵌入式推理引擎能识别的格式,同时完成量化压缩。嵌入式推理引擎就是跑在MCU上的运行时,负责加载模型、分配内存、执行算子。
选工具链最需要盯住的一点,是算子覆盖范围。训练时可以随便用各种层,但嵌入式推理引擎支持的算子非常有限。我见过不少项目,训练出来的模型在PC上一切正常,一转换就报“不支持的算子”,要回头改模型结构,非常痛苦。所以建议项目一开始,就按嵌入式推理引擎支持的算子集合来设计模型,而不是写完了再回头迁就。这算是本系列第一个提前避坑的忠告。
3. 实操:跑通第一个关键词唤醒项目
3.1 项目目标与整体拆解
实操部分选了一个非常经典的任务:关键词唤醒。具体目标,让设备识别两个词,“上”和“下”,识别不了就归到“未知”类,没人说话则归到“静音”类,一共四个类别。
选这个任务的原因,是它麻雀虽小五脏俱全。音频是人类身边最常见的实时传感器信号,这个场景对处理延迟敏感,对功耗敏感,还要做特征提取、分类、端侧部署全链路。你把这一套跑通,再去做振动分类、环境声音识别、传感器异常检测,逻辑上基本就是平移。
整体流程走一遍大概是这样:麦克风采集音频,做预处理和分帧,提取MFCC特征,把特征喂给量化后的int8模型,推理得到四个类别概率,再按阈值判断当前是否命中唤醒词。每一步都有值得展开的细节,下面逐个说。
3.2 新手必看:MFCC特征提取,别把原始波形直接扔给模型
很多人第一次做语音分类,直觉上是把录音波形直接当模型输入。我早期也这么干过,效果非常差。
原因其实好懂。一段1秒的语音,以16kHz采样,就是16000个采样点,每个float占4字节,一共64KB。这还只是一秒的数据,MCU的RAM根本扛不住。而且原始波形里的冗余和噪声太多,模型要在原始数据上直接学特征,又慢又难。
行业中成熟的方案,是把音频转成MFCC特征。MFCC全称梅尔频率倒谱系数,可以理解为把音频从时域转到频域,再压缩成一小组系数,用来表达这一小段音频的音色特征。常规流程是这样的:预加重提升高频细节,分帧加窗每帧约30ms、帧移约20ms,做FFT得到频谱,再用梅尔刻度的滤波器组把频谱压成约40个能量值,取对数,最后做DCT变换,得到10个左右的MFCC系数。这样一秒音频会变成一个二维特征图,比如49帧乘以10个系数,数据量只有原始波形的几十分之一,而且模型学起来稳定得多。
实操里通常不需要自己手写MFCC,用现成信号处理库就能提取,但原理一定要理解。后面到了MCU上做实时特征提取,你会需要根据内存和时延重新调整帧长、帧移和滤波器数量,那时候每一个参数改了什么、为什么影响结果,都得做到心里有数。
3.3 模型训练:小卷积网络就能解决四分类
音频特征变成二维图之后,分类任务用卷积网络很自然。下面给一个我实际验证过可用的轻量模型结构,参数规模大概在几万到十几万之间,量化后Flash占用很小:
from tensorflow import keras from tensorflow.keras import layers inputs = keras.Input(shape=(49, 10, 1)) x = layers.Conv2D(filters=8, kernel_size=(3, 3), padding="same", activation="relu")(inputs) x = layers.MaxPooling2D(pool_size=(2, 2))(x) x = layers.Conv2D(filters=16, kernel_size=(3, 3), padding="same", activation="relu")(x) x = layers.MaxPooling2D(pool_size=(2, 2))(x) x = layers.Flatten()(x) x = layers.Dense(units=16, activation="relu")(x) x = layers.Dense(units=4, activation="softmax")(x) model = keras.Model(inputs, outputs) model.summary()训练阶段有几个点,是从实际项目中反复磨出来的。第一,数据划分要每类独立划分训练集、验证集、测试集,比例可以按8:1:1,尽量平衡。第二,音频要做数据增强,比如音量随机放大缩小、加随机噪声、时间微移,这些操作能把模型的泛化能力明显拉上来。第三,不要贪大。MCU不是服务器,模型参数做到千万级基本不现实,在几十万参数量级下优化准确率,才是有意义的工程方向。
按我自己跑过的相似结构,一个四分类唤醒模型在验证集上准确率能做到92%到94%之间,量化后掉点一般控制在1到2个百分点以内。如果你的数据量更充分,做到95%以上也不意外。
3.4 模型压缩与量化:把float32模型变成int8
训练完成之后,模型还是float32形态,接下来才到了关键的量化转换环节。
转换需要用专门的转换器,核心操作有两个要点:设置优化目标为默认优化,以及提供一个代表数据集。代表数据集的作用,是让转换器统计各层激活值的真实分布,从而算出合理的量化参数。它是一个生成器,里面放几十到几百条有代表性的输入数据:
converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.representative_dataset = representative_data_gen quantized_model = converter.convert() with open("model_int8.tflite", "wb") as f: f.write(quantized_model)这地方有个特别容易被忽视的坑:代表数据集一定要覆盖真实使用场景。假设你的实际环境有空调噪音、马路噪音,但代表数据集只放安静房间录的干净语音,那么量化参数里的动态范围就估算偏差,部署后模型在噪声环境下的表现会断崖式下降。做完量化之后,随手做一次对比测试,看int8模型和float32模型在同一批测试集上的准确率差多少。正常情况下差1到2个点以内,如果差很多,就需要排查是不是代表数据集覆盖不足,或者某层对量化过于敏感。
4. 上板部署与实测调优:从能跑到跑好
4.1 最小可行的部署闭环
部署端的步骤,我理成一个标准操作流,照着做不容易漏:
- 先把MCU的嵌入式开发环境搭好,确认空工程能编译、能烧录、能运行。
- 把嵌入式推理引擎的源码并入工程。它通常以源码文件形式提供,和RTOS没有强绑定,放进工程后编译。
- 把量化好的模型文件转成C语言数组,放到Flash常量区。
- 注册模型用到的所有算子。语音模型用到的卷积、池化、全连接,一般内置都有。
- 实例化解释器,分配一个全局的Tensor arena缓冲,设置好输入和输出Tensor指针。
- 在主循环里做循环推理:拿一帧数据,填充输入Tensor,调用推理,读输出Tensor,做阈值判断。
部署代码大致长这样:
#include "tensorflow/lite/micro/micro_interpreter.h" static uint8_t tensor_arena[32 * 1024]; // 根据实际模型需求调整 void setup() { tflite::MicroInterpreter interpreter( model_data, resolver, tensor_arena, sizeof(tensor_arena)); interpreter.AllocateTensors(); } void loop() { // 1. 采集一帧音频,提取MFCC特征 // 2. 将特征填充到 interpreter.input(0) // 3. 调用 interpreter.Invoke() // 4. 读取输出概率,按阈值决定动作 }这里有个容易踩的坑:模型转成C数组的时候,要确认编译器对数组做了对齐处理。MCU对不同类型数据有对齐要求,数组地址不对齐,运行到一半可能直接进硬件错误。另外,工程编译选项最好把优化开到较高等级,否则嵌入式推理引擎的体积和速度都会很差。
4.2 内存管理:Tensor arena到底该怎么设
这个坑必须单独拿出来说。嵌入式推理引擎通常不会像PC端的运行库那样动态申请内存,而是预先给你一整块内存,名字叫Tensor arena。解释器会在这一块buffer里规划所有中间张量,所以这块内存多大,直接决定你的模型能不能跑起来。
新手常用的笨办法,是先拍一个数字去试。比如先从32KB开始,AllocateTensors返回失败就翻倍,直到跑通为止。这个办法能跑,但会很浪费。更理性的方式是看工具链自带的内存估算工具,它会根据模型结构算出需要的缓冲大小,你按估算值再加一点余量就行。我自己的习惯是估算结果上浮20%左右,避免后续改输入规格时又不够用。
另外有几个省内存的设计意识要养成:模型权重能放Flash就不要复制进RAM;推理中间结果尽量复用,解释器默认会做,但你不要在模型里随意加大的中间层。比如输入特征是49乘10,中间张量不大;如果你把输入改成98乘20,中间张量直接翻四倍,RAM预算可能瞬间就爆了。设计模型时,输入尺寸、通道数这些参数,都要放在心上。
4.3 推理效果调优:别只看单次准确率
能跑通之后,真正头疼的才刚开始。在真实环境里,模型输出的四个概率通常不会像测试集里那么干净。比如一帧数据里,“上”的概率是0.35,“下”是0.3,“静音”是0.2,这种结果如果直接取最大值做判断,误报率会高到没法用。所以有三个调优项基本是必做的。
第一,置信度阈值。只有最大概率超过某个阈值,比如0.7,才算命中唤醒词;达不到阈值的帧一律当成“不确定”。阈值高,误报少但可能漏报;阈值低,灵活但容易误报。实际调试时需要根据场景来调,噪声大的环境阈值提高,安静环境可以调低一点。
第二,滑动窗口。单帧判断抖动很大,我一般会连续判断三到五帧,只有大部分结果一致才执行动作。这个方法不花任何额外内存,却能把偶发的单帧误判全部滤掉。
第三,环境适配。不同麦克风的灵敏度差很多,部署之后要先用一段静音计算底噪,再做动态增益校准。这一步别小看,很多项目实机效果差,不是模型不行,而是麦克风增益没调好,或者ADC采样值被裁掉了。我实测过一个相似结构,测试集准确率95%以上的模型,不做这三个调优直接部署,实机误报率能在10%级别;做完这三个调优之后,基本能压到1%以内。
5. 常见问题与排查速查表
5.1 一张表解决大半部署问题
下面这个速查表,是我一路调试攒下来的,日常项目里的问题基本都能对上号:
| 现象 | 可能原因 | 排查方向与解决办法 |
|---|---|---|
| 编译后Flash超限 | 模型太大、常量数组过多 | 做int8量化甚至更强压缩;剪枝;减少嵌入式端的冗余调试打印 |
| 运行内存不足 | Tensor arena太小,或输入规格太大 | 用工具估算所需buffer;缩小输入特征;减少模型层数和通道数 |
| AllocateTensors一直失败 | 算子未注册或arena不足 | 确认resolver把所有用到的算子都加上;继续加大arena或减小模型复杂度 |
| 推理结果全是0 | 输入Tensor没有正确赋值 | 检查输入归一化方式,训练时是0到1还是-1到1,部署端必须保持一致 |
| 量化后准确率骤降 | 代表数据集覆盖不够,或有量化敏感层 | 扩充代表数据集;逐层排查量化影响,必要时对敏感层保留浮点或改用量化感知训练 |
| 某个算子不支持 | 模型用了嵌入式引擎没有的算子 | 重设计模型,用支持的算子替换,把复杂结构拆成几个基础操作 |
| 推理耗时要几百毫秒 | 模型太深或没有启用硬件加速 | 减少层数通道数;优先用可分离卷积;看看MCU有没有加速指令可用 |
| 预测结果频繁抖动 | 没做滑动窗口和阈值 | 加置信度阈值,连续多帧投票一致再动作 |
5.2 排查思路:先在PC端复现,再上板调试
遇到过不少卡住很久的项目,问题本质其实不在原理,而是一步步排查的顺序不对。我的经验是,一旦MCU端结果异常,先在PC端加载同一个量化模型,用同一份输入数据跑一遍。
如果PC端结果正常而MCU端异常,说明问题定位在部署环境,比如Tensor赋值、采样问题或者内存冲突;如果PC端也不正常,那问题出在模型转换或输入处理上,直接改转换流程和预处理逻辑就行,不用在板子上反复烧录浪费时间。这个习惯能省下大量无效调试时间。
还有一个很容易翻车的点,是输入数据的维度顺序。训练时模型输入可能是(49, 10)的特征,部署端的采集和特征提取代码处理完之后,必须把数据按照完全相同的顺序和shape填进输入Tensor。我帮人排查时经常发现,数据本身对,但维度顺序反了,模型端出来的结果自然一塌糊涂。
6. 系列后续与学习路线建议
走到这里,你已经完成了一个TinyML的最小闭环:理解了技术链条,训练了一个小模型,量化后部署到MCU,还能在实机跑出稳定效果。这比单纯看资料有用得多,因为所有知识都变成过你手里跑过的东西。
6.1 三个值得深入的进阶方向
接下来想继续往前,可以往三个方向走。
一是模型压缩进阶。剪枝、蒸馏、混合量化,甚至针对目标模型做结构搜索,让模型在更小的体积里保持更高的准确率。这是在“能跑”基础上追求“跑得更好”的关键。二是硬件加速与低功耗优化。在支持DSP指令或者NPU的MCU上,调用加速算子,同时优化休眠策略和事件驱动架构,把整机功耗压到电池能撑几年的程度。这在产品化阶段几乎是必考科目。三是应用场景扩展。多麦克风阵列、IMU姿态识别、异常声音检测、电机振动诊断,本质上都是你已经掌握的这套流程在不同传感器上的复刻。
6.2 给新手的路线:先复现,再改,后调优
我经常被问到一个问题:怎么才能快速入门TinyML?我的建议是,不要上来就啃论文,也不要硬啃开源项目里那些复杂的高阶示例。正确顺序应该是,先把我这篇的代码从头到尾跑通,保证实机有响应;然后换自己的数据集,比如录你自己的指令词,或者采集家里的环境声音,把整个流程再走一遍;最后再研究优化手段,逐步深入工具链底层。
每一次成功跑通,都会给你正反馈,这是保持学习动力的最有效方式。当你已经能独立修改数据集、调整模型结构、自行部署实测的时候,再看进阶资料,理解速度会完全不一样。下一期我准备聊量化感知训练和剪枝,这两项是把一个玩具Demo变成可量产方案的分水岭。
个人体会是,TinyML的门槛其实不在算法有多深,而在于你是否真正理解了“嵌入式资源是有限的”这句话。放下服务器思维,接受几KB内存,适应int8权重,习惯在编译器、调试器、工程配置之间来回折腾,这条路就会越走越顺。最后分享一个很土的办法:每次改模型或者改部署代码,先记录一下当前版本的Flash占用、RAM占用和实机准确率,做一张简单的版本记录表。随着版本号上升,性能往哪个方向走,你心里会特别有数。