1. 项目概述:当TinyML遇见“能量自给自足”

在物联网和边缘计算的世界里,我们总在追求一个看似矛盾的平衡:让设备更“聪明”,同时让它们更“省心”。这里的“省心”,很大程度上指的是对能源的依赖。想象一下,在深海、在偏远山区、在大型结构的内部,部署一个需要定期更换电池的传感器节点,其维护成本和可行性往往成为项目落地的最大障碍。这正是我过去几年深耕TinyML领域时,反复遇到的核心痛点。

传统的思路是“节流”,即通过算法和硬件的极致优化,让设备在有限的电池容量下撑得更久。但这终究有个物理极限。一个更激进的思路是“开源”,即让设备从环境中获取能量,实现“无电池”或“能量自给自足”运行。这听起来像科幻,但随着能量收集技术的成熟,它正变得触手可及。然而,能量收集的功率通常只有毫瓦甚至微瓦级别,这对其上运行的任何计算任务都提出了近乎苛刻的要求。主流的机器学习模型,动辄需要数百MB内存和数瓦的功耗,显然与此格格不入。

因此,我们面临一个极具挑战性的目标:在仅靠环境能量收集供电的微控制器上,实现有实用价值的机器学习推理。这不仅仅是模型压缩,更是一场贯穿数据、模型、部署、能耗全链路的系统性优化。近期,我们完成了一个针对海洋鱼类图像识别的概念验证项目,成功将一套优化后的流程部署在STM32 MCU上,实现了在保持97.78%高精度的前提下,单次推理能耗仅0.57毫焦耳。这个能量水平,已经与一些水下声学能量收集装置所能提供的功率范围相匹配,为真正的无电池智能感知打开了大门。

2. 核心思路与方案选型:为何是“NAS+量化”?

面对资源受限的MCU和无电池的严苛约束,我们的优化必须多管齐下。一个完整的端到端流程,需要从数据输入开始,到模型输出结束,每一个环节都进行“瘦身”和“提效”。我们的核心方案可以概括为: 预处理降维 -> 神经架构搜索定制模型 -> 静态量化压缩 -> 专用工具链部署 。下面,我详细拆解每个环节的选型逻辑。

2.1 从数据源头“减负”:极致的预处理

模型的输入数据是计算开销的源头。对于图像任务,分辨率直接决定了后续卷积等操作的运算量。我们使用的DeepFish数据集原始图像为1920x1080的RGB三通道图像。如果直接输入,对于MCU来说是不可承受之重。

我们的做法是,将图像重采样为32x32的单通道灰度图。 这里有两个关键决策:

  1. 分辨率降低 (1920x1080 -> 32x32) :这是数量级的下降。对于许多物体检测任务,这或许会丢失细节,但对于“鱼类存在性”或“粗略分类”这样的任务,经过我们的实验验证,32x32的分辨率已经能够保留足够的判别性特征。运算量减少了约3600倍。
  2. 通道数减少 (RGB 3通道 -> 灰度 1通道) :这直接减少了3倍的输入数据量和第一层卷积的参数量。对于水下图像,颜色信息由于水体对光线的吸收和散射,本身就可能失真严重,灰度化处理在减少数据量的同时,有时反而能增强模型对形状和纹理特征的关注。

实操心得 :数据预处理是TinyML项目中性价比最高的优化步骤。在项目初期,一定要花时间分析任务对输入数据的最低要求。通过下采样、裁剪、通道选择等手段,能在模型设计之前就砍掉大部分不必要的计算。

2.2 模型架构搜索:告别“手调”,拥抱“搜优”

在轻量化模型设计上,我们放弃了手动设计或简单采用MobileNetV2、EfficientNet-Lite等现成架构。原因在于,这些通用架构虽然已经过优化,但未必是针对我们 特定任务(鱼类分类) 特定硬件(Cortex-M4内核的STM32) 的最优解。

我们选择了 ProxylessNAS 作为我们的神经架构搜索方法。与一些在大型代理数据集上搜索再迁移的方法不同,ProxylessNAS可以直接在目标数据集和目标硬件约束下进行搜索。它的核心思想是构建一个包含多种可能操作(如3x3卷积、5x5卷积、深度可分离卷积等)的“超网络”,通过可学习的架构参数来控制每条路径的重要性。在训练时,通过一种“二值门”机制,每次只激活一条路径,从而大幅降低搜索过程的内存消耗,使得在有限资源下进行NAS成为可能。

为什么是ProxylessNAS,而不是其他NAS方法?

  • 内存效率 :传统的NAS方法需要将整个庞大的搜索空间模型常驻内存,ProxylessNAS的二值化路径机制使其内存占用与单个子网络相当,非常适合在算力有限的平台上进行。
  • 直接优化 :它直接优化在目标任务上的精度,同时可以方便地将硬件延迟等指标作为损失函数的一部分,实现精度-效率的帕累托最优搜索。
  • 获得定制化模型 :最终搜出的模型结构,是专为我们这个鱼类数据集和MCU推理环境“量身定做”的,其效率往往优于直接拿来的通用轻量模型。

2.3 模型量化:从浮点到整型的“精度换效率”博弈

通过NAS得到的是一个由浮点数(通常是float32)权重和激活值构成的模型。在MCU上,浮点运算不仅速度慢,而且功耗高。 量化是将模型从高精度浮点表示转换为低精度整数表示的过程,是TinyML部署中不可或缺的一环。

我们采用 静态量化(Post-Training Static Quantization) 。与动态量化或量化感知训练相比,静态量化在推理时不需要计算每批数据的动态范围,因此运行时开销更小,更适合MCU。 其过程主要分为四步:

  1. 数据收集 :使用一部分校准数据,让模型运行并收集各层激活值的分布。
  2. 范围确定 :根据收集的分布(如最大最小值、百分位数),为每一层的权重和激活确定一个从浮点到整型的映射比例因子和零点偏移。
  3. 转换 :将浮点权重和根据上述参数计算出的激活值,转换为8位整数。
  4. 反量化 :在需要时(如层间连接处),将整数输出转换回浮点数进行后续计算(但在许多推理引擎中,整数运算会贯穿始终,仅在最后输出层反量化)。

量化后,模型大小减少约75%(从32位到8位),同时,整数运算在ARM Cortex-M系列处理器上有专门的指令集优化,速度更快,能耗更低。在我们的案例中,量化仅带来了约0.1%的精度损失,这完全在可接受范围内。

2.4 部署工具链:从TensorFlow到MCU的“桥梁”

将训练好的模型部署到MCU上,需要专门的工具链。我们选择了 ST公司的X-CUBE-AI扩展包 。它是一个将AI模型集成到STM32微控制器上的软件解决方案。

它的工作流程如下:

  1. 输入训练好的模型文件(我们使用TensorFlow Lite格式)。
  2. X-CUBE-AI会对模型进行解析、优化(如层融合、常量折叠)和代码生成。
  3. 输出纯C语言的代码文件( .h .c ),其中包含了模型权重(常量数组)和一系列针对STM32硬件优化的推理函数。
  4. 开发者将这些文件加入自己的嵌入式工程,调用生成的API即可进行推理。

选择X-CUBE-AI的原因在于其与STM32生态的无缝集成、对硬件加速器(如STM32的NNE)的支持,以及生成的代码具有高度的可预测性和可调试性,这对于资源受限和可靠性要求高的场景至关重要。

3. 实操流程详解:从数据到部署的完整链路

纸上得来终觉浅,下面我将结合我们的鱼类识别项目,一步步拆解整个实操过程,其中包含了许多在论文中一笔带过、但对工程实现至关重要的细节。

3.1 数据准备与预处理实战

我们使用DeepFish数据集。原始数据约4万张高清图像。第一步是构建一个高效的数据预处理管道。

import tensorflow as tf
import cv2
import numpy as np

def preprocess_image(image_path, target_size=(32, 32)):
    """
    预处理单张图像:读取、调整大小、灰度化、归一化。
    Args:
        image_path: 图像文件路径。
        target_size: 目标尺寸,默认为(32, 32)。
    Returns:
        预处理后的numpy数组,形状为(32, 32, 1)。
    """
    # 1. 读取图像
    img = cv2.imread(image_path)
    if img is None:
        raise ValueError(f"无法读取图像: {image_path}")

    # 2. 调整大小到目标尺寸
    img_resized = cv2.resize(img, target_size, interpolation=cv2.INTER_AREA)

    # 3. 转换为灰度图 (单通道)
    img_gray = cv2.cvtColor(img_resized, cv2.COLOR_BGR2GRAY)

    # 4. 添加通道维度并归一化到[0, 1]
    img_normalized = img_gray.astype(np.float32) / 255.0
    img_normalized = np.expand_dims(img_normalized, axis=-1)  # 形状: (H, W, 1)

    return img_normalized

# 构建TensorFlow Dataset管道
def create_dataset(image_paths, labels, batch_size=32):
    def load_and_preprocess(path, label):
        # 这里需要将上面的预处理函数用tf.numpy_function包装,以在TensorFlow图中运行
        image = tf.numpy_function(preprocess_image, [path], tf.float32)
        image.set_shape([32, 32, 1]) # 必须设置静态形状
        return image, label

    dataset = tf.data.Dataset.from_tensor_slices((image_paths, labels))
    dataset = dataset.map(load_and_preprocess, num_parallel_calls=tf.data.AUTOTUNE)
    dataset = dataset.batch(batch_size).prefetch(tf.data.AUTOTUNE)
    return dataset

注意事项 :在嵌入式部署中,预处理逻辑必须与训练时完全一致。最好将预处理步骤(如归一化系数)固定下来,并确保在MCU的C代码中精确复现Python端的操作。任何细微差别都可能导致精度大幅下降。

3.2 基于ProxylessNAS的模型搜索与训练

我们基于开源实现搭建ProxylessNAS搜索空间。搜索空间包含多种卷积类型和扩张率。关键步骤是定义包含硬件延迟的损失函数。

# 伪代码,展示核心思想
import torch # 假设使用PyTorch实现ProxylessNAS
from proxyless_nas import ProxylessNASNets

# 1. 初始化超网络
supernet = ProxylessNASNets(
    width_stages=[16, 24, 40, 80, 96, 192, 320],
    n_cell_stages=[4, 4, 4, 4, 4, 1],
    num_classes=10, # 假设10类鱼
    dropout_rate=0.2
)

# 2. 定义包含精度和估计延迟的损失函数
criterion = nn.CrossEntropyLoss()
def loss_function(predictions, targets, model):
    # 分类损失
    cls_loss = criterion(predictions, targets)
    # 估计模型在目标硬件上的延迟 (这里需要有一个预构建的查找表)
    estimated_latency = model.estimate_latency()
    # 加权总损失
    total_loss = cls_loss + 1e-5 * estimated_latency # 权重系数需要调优
    return total_loss

# 3. 交替训练网络权重和架构参数
for epoch in range(total_epochs):
    # 训练步骤:采样一条路径,更新权重
    supernet.train()
    # ... 前向传播,计算损失,反向传播更新权重

    # 架构参数更新步骤:采样两条路径,更新架构参数
    supernet.eval()
    # ... 根据ProxylessNAS论文中的梯度方法更新架构参数

# 4. 搜索完成后,导出最优子网络
best_architecture = supernet.get_best_architecture()
final_model = supernet.get_final_model(best_architecture)

关键点 :延迟估计需要事先在目标MCU上对搜索空间中的每个基础算子进行基准测试,建立一个“算子-延迟”查找表。搜索时,模型根据当前激活的路径累加各算子延迟,作为损失的一部分。这确保了搜出的模型不仅在数据上准确,在硬件上也高效。

3.3 模型量化与TensorFlow Lite转换

训练好浮点模型后,我们将其转换为TFLite格式并进行静态量化。

import tensorflow as tf

# 1. 加载训练好的Keras模型
model = tf.keras.models.load_model('proxylessnas_fish_model.h5')

# 2. 创建代表性数据集用于校准量化参数(通常使用100-500张图)
def representative_dataset():
    for image, _ in validation_dataset.take(100): # validation_dataset是tf.data.Dataset
        # 注意:提供给转换器的数据需要是float32,且不需要标签
        yield [image]

# 3. 配置转换器并量化
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT] # 启用默认优化(包含量化)
converter.representative_dataset = representative_dataset
# 确保完全量化为int8(可选,但能获得最大性能提升)
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.uint8  # 或 tf.int8
converter.inference_output_type = tf.uint8 # 或 tf.int8

# 4. 转换并保存模型
tflite_quant_model = converter.convert()
with open('fish_model_quantized.tflite', 'wb') as f:
    f.write(tflite_quant_model)

print(f"量化模型大小: {len(tflite_quant_model) / 1024:.2f} KB")

实操心得 :量化后,务必在PC端使用TFLite解释器对量化模型进行精度验证,与原始浮点模型对比。如果精度下降过多(如>1%),需要检查代表性数据集是否具有代表性,或者考虑使用“量化感知训练”在训练阶段就模拟量化效应,以获得更好的精度恢复。

3.4 使用X-CUBE-AI进行部署与集成

这是将AI模型“烧录”进MCU的关键一步。

  1. 环境准备 :安装STM32CubeIDE,并为你的目标MCU系列(如STM32L4)安装X-CUBE-AI扩展包。
  2. 模型导入 :在STM32CubeMX中创建工程,选择你的MCU型号。在“Software Packs”中选择X-CUBE-AI,并将其添加到工程。在X-CUBE-AI配置界面,导入上一步生成的 .tflite 文件。
  3. 模型分析与代码生成 :X-CUBE-AI会分析模型,报告各层内存消耗、理论推理周期数。你可以在这里进行一些优化设置,如启用CRC校验、选择内存分配策略等。然后点击“Generate Code”。
  4. 工程集成 :代码生成后,你会在工程目录中发现 Application/User/x-cube-ai 文件夹,里面包含了模型权重数组和推理API。
  5. 编写应用代码
    #include "ai_datatypes_defines.h"
    #include "fish_model.h" // X-CUBE-AI生成的头文件
    
    // 1. 声明AI模型实例和输入输出缓冲区
    static ai_handle fish_model = AI_FISH_MODEL_INSTANCE;
    static ai_u8 activations[AI_FISH_MODEL_DATA_ACTIVATIONS_SIZE];
    static ai_i8 in_data[AI_FISH_MODEL_IN_1_SIZE]; // 输入缓冲区,int8类型
    static ai_i8 out_data[AI_FISH_MODEL_OUT_1_SIZE]; // 输出缓冲区
    
    // 2. 初始化模型
    ai_error err;
    err = ai_fish_model_create(&fish_model, AI_FISH_MODEL_DATA_CONFIG);
    if (err.type != AI_ERROR_NONE) {
        // 错误处理
    }
    
    // 3. 准备输入数据 (假设已获取到32x32的灰度图像数据img_gray)
    // 注意:需要将数据量化到int8范围。如果训练时归一化到[0,1],量化参数为scale=1/255, zero_point=0
    // 那么量化公式: int8_value = round(float_value / scale) + zero_point
    for(int i=0; i<1024; i++) { // 32*32=1024
        float normalized_pixel = img_gray[i] / 255.0f;
        in_data[i] = (ai_i8)(normalized_pixel * 255); // 简化的量化,实际需按模型参数
    }
    
    // 4. 创建输入输出张量
    ai_buffer input_buffer = AI_BUFFER_INIT(
        AI_BUFFER_FORMAT_U8,
        AI_BUFFER_FMT_RAW,
        1, 1, AI_FISH_MODEL_IN_1_SIZE, in_data, NULL
    );
    ai_buffer output_buffer = AI_BUFFER_INIT(
        AI_BUFFER_FORMAT_U8,
        AI_BUFFER_FMT_RAW,
        1, 1, AI_FISH_MODEL_OUT_1_SIZE, out_data, NULL
    );
    ai_buffer* inputs = &input_buffer;
    ai_buffer* outputs = &output_buffer;
    
    // 5. 执行推理
    err = ai_fish_model_run(fish_model, &inputs, &outputs);
    if (err.type != AI_ERROR_NONE) {
        // 错误处理
    }
    
    // 6. 处理输出 (例如,找到概率最大的类别)
    int8_t* predictions = (int8_t*)outputs[0].data;
    int max_index = 0;
    for(int i=1; i<AI_FISH_MODEL_OUT_1_SIZE; i++) {
        if(predictions[i] > predictions[max_index]) {
            max_index = i;
        }
    }
    // max_index即为预测的鱼类类别
    
  6. 编译与烧录 :编译整个工程,生成二进制文件,通过ST-LINK等调试器烧录到STM32开发板中。

4. 能耗评估与性能实测:数据背后的故事

模型部署成功只是第一步,在无电池场景下,能耗才是真正的“硬通货”。我们使用 Monsoon高精度功率监测仪 对STM32L4R5开发板进行了实测。

测试方法

  1. 将Monsoon电源设置为1.9V(接近MCU的工作电压),串联接入开发板的供电回路。
  2. 编写测试固件,让MCU循环执行以下操作:进入低功耗睡眠模式 -> 被外部中断唤醒(模拟传感器触发)-> 执行一次完整的图像预处理和模型推理 -> 通过串口输出结果 -> 再次进入睡眠。
  3. 使用Monsoon配套软件,以高采样率(如10kHz)采集电压和电流波形。
  4. 分析波形,识别出一次推理周期的电流脉冲,计算其持续时间(运行时间)和积分面积(能耗)。

实测结果对比分析表

指标 原始浮点模型 (TensorFlow Lite) 优化后模型 (量化后) 优化幅度
Flash占用 1350.25 KB 483.82 KB 降低 64.17%
RAM占用 80.20 KB 70.32 KB 降低 12.31%
单次推理时间 248 ms 118 ms 降低 52.42%
平均运行功率 13.32 mW 4.83 mW 降低 63.74%
单次推理能耗 3.29 mJ 0.57 mJ 降低 82.67%
分类精度 97.88% 97.78% 下降 0.10%

结果解读与意义

  • Flash与RAM的降低 :主要归功于模型架构搜索(得到更精简的模型)和8位量化(权重从32位变为8位)。RAM降低相对较少,因为推理时的激活张量占用是主要部分,量化对其也有帮助。
  • 运行时间减半 :这得益于两方面。一是更精简的模型结构(ProxylessNAS搜出的模型FLOPs更低),二是在MCU上,8位整数运算远比32位浮点运算快,且Cortex-M4/M7内核可能支持SIMD指令进一步加速。
  • 功率与能耗的骤降 :这是最关键的成果。功率降低主要源于计算量的减少和整数运算更低的电路开关活动。能耗是功率与时间的乘积,两者同时大幅降低,带来了 82.67% 的惊人能效提升。 0.57mJ 的单次推理能耗意味着什么?假设有一个水下能量收集装置,平均收集功率为1mW,那么它每秒钟积累的能量就足以支持近2次推理。这为长期、无人值守的智能感知提供了理论可能。

5. 踩坑实录与进阶优化思考

在实际操作中,远非一切顺利。以下是几个典型的“坑”和我们的解决方案。

5.1 量化精度损失超预期

问题 :初期量化后,模型在测试集上的精度从97.8%暴跌至92%左右。 排查 :检查代表性数据集,发现我们只是随机抽取了100张训练集图片。这些图片的亮度、对比度分布可能与真实测试环境有差异。 解决 :确保代表性数据集与模型实际推理时看到的数据分布一致。我们从验证集中随机抽取了200张图片,并确保覆盖所有类别。同时,尝试调整量化的“量化-反量化”节点插入策略,并对模型输出层使用浮点数以保留更多信息。最终将精度损失控制在0.1%以内。

5.2 MCU上推理结果异常

问题 :PC端TFLite解释器推理正常,但部署到MCU后,输出全是乱码或固定值。 排查

  1. 内存对齐 :检查发现,我们自定义的输入数据缓冲区没有按照X-CUBE-AI要求进行内存对齐(通常是4字节或8字节)。
  2. 数据预处理不一致 :MCU端的预处理C代码与Python训练时的归一化公式有一个细微差别(Python是 /255.0 ,C代码误写为 /255 ,导致整数除法截断)。
  3. 量化参数未同步 :在MCU端,需要将输入数据从uint8(摄像头数据)按模型要求的缩放因子和零点进行量化,我们最初直接使用了错误的参数。 解决 :使用X-CUBE-AI提供的 ai_buffer API来管理数据;将预处理代码封装成函数,并在PC端用相同逻辑处理一张图片,与MCU输出对比;仔细核对模型头文件中定义的输入量化参数( scale zero_point ),并确保在C代码中正确应用。

5.3 能量收集与计算的任务调度

问题 :在无电池系统中,能量是断续到达的。如何确保在能量充足时才执行高耗电的推理任务? 解决 :我们在MCU固件中实现了一个简单的 能量感知调度器 。系统持续监测超级电容或储能元件的电压。

  • 低能量状态 :电压低于阈值V_low,MCU进入深度睡眠,仅维持最基本的传感器监测和能量收集电路工作。
  • 充电状态 :电压在V_low和V_high之间,MCU保持空闲或执行极低功耗的后台任务。
  • 就绪状态 :电压高于V_high,表示积累了足够进行一次推理的能量。此时,如果有传感器触发(如摄像头捕捉到运动),则立即执行完整的唤醒、数据采集、预处理、推理、发送结果流程。 这种策略避免了在能量不足时启动任务导致系统复位,提高了任务完成的可靠性。

5.4 关于模型个性化优化的思考

摘要中提到的“模型个性化”,是针对数据异构性问题。在真实海洋中,不同海域的光照、水质、鱼类种群都有差异。一个在A海域训练的模型,在B海域性能可能下降。 我们的未来思路

  1. 联邦学习微调 :多个部署节点在本地收集新数据,仅将模型更新(梯度)加密上传到边缘服务器聚合,生成全局模型后再下发。这样既能利用新数据优化模型,又保护了原始数据隐私。
  2. 在线增量学习 :在MCU上实现极轻量级的在线学习算法(如小样本学习、原型网络),让设备能够根据少数新样本快速调整模型,适应新环境。这需要算法在内存和计算上极度高效,是下一步的研究重点。

从浮点模型到0.57mJ的整数模型,从依赖电池到展望能量收集,这条路充满了工程细节的挑战和算法优化的乐趣。无电池机器学习推理不是天方夜谭,它是一系列已知技术的系统化整合与极致优化。对于从事边缘AI和物联网开发的工程师来说,理解这套从数据、模型、算法到硬件的全栈优化思维,将成为构建下一代长寿命、自维持智能设备的关键能力。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐