这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【瑞萨RA8D1测评】TFLM端侧部署

共1条 1/1 1 跳转至

【瑞萨RA8D1测评】TFLM端侧部署

菜鸟
2026-09-19 10:28:18     打赏



上一篇把 tinyML 的采集和训练走通了,这篇继续——把训练好的模型塞进 RA8D1 里。

首先推理时不该留 BatchNorm。BN 在推理阶段就退化成一个逐通道的仿射变换,而它前面的卷积也是线性的,两个线性变换合成一个:

image.png

作用在卷积的输出通道轴上。

折完立刻拿原模型对拍,把两边的 logits 逐条减:

折叠后 logits 最大误差 1.907e-06 预测一致率 100.00%  (无损)

1.9e-6 就是 fp32 的舍入噪声。

训练时输入做了归一化——加速度除 4000,角速度除 500000,不这么做,陀螺 mdps 比加速度 mg 大 125 倍,第一层几乎全被陀螺通道支配。既然 BN 能折,归一化按理也能,因为卷积对输入也是线性的:

image.png

把 conv1.weight 的第 ccc 个输入通道整个除以 scs_csc,归一化就消失了,设备端直接喂原始整数。

折叠后是2000 多个参数,8.4 KB

.tflite 的转换器必须在 TensorFlow 里,必须装 TF。要注意主环境里 numpy 版本,新版 TF 要求更高的 numpy。

单开一个转换专用的 venv,两边用 .npz 传数据:

uv venv --python 3.11 .venv-tfliteuv pip install --python .venv-tflite/bin/python tensorflow-cpu

PyTorch 是 channels-first([6, 300]),Keras/TFLite 是 channels-last([300, 6])。

channels-last 正是数据在设备上到达的方式——IMU 每个时刻吐 6 个值,天然就是 [时刻, 6]。原来 Dataset 里那句 x = x.T 的转置,到这儿自然就消失了,一行 C 都不用写

权重布局跟着换:

PyTorch Conv1d weight [out, in, k]  →  Keras Conv1D kernel [k, in, out]
PyTorch Linear weight [out, in]     →  Keras Dense  kernel [in, out]


翻大车:int8 量化后准确率从 100% 掉到 75%

转 fp32 的 TFLite,对拍 100% 一致,顺得很。接着转 int8,想着归一化都折进权重了,设备端喂原始 mg/mdps 多干净——就按这个建了模型,量化,对拍。

结果:

从 100% 掉到 75%,掉了整整 24 个点

第一反应是怪量化本身——"int8 精度不够"。但 fp32 明明是无损的,8 位不至于崩成这样。

问题就在前面那个"折叠"上。fp32 下折进权重是无损的,int8 下per-tensor 量化要求整个张量数值范围接近,把陀螺的 50 万和加速度的 4000 塞进同一个张量,加速度直接被量化台阶碾平。

把两个变体都建出来实测:

变体大小与 PyTorch 预测一致数据集准确率




TFLite fp3212.9 KB100%100%
TFLite int8(归一化输入)8.5 KB100%100%
int8(原始量纲输入)8.5 KB75.68%75.68%

所以fp32 和 int8 应该走相反的折叠策略


fp32 路线 → 归一化折进权重 → 设备喂原始值 → 少一步,不会错

int8 路线 → 归一化留在设备端 → 六个通道都在 [-1,1] → per-tensor 量化才公平

量纲的坑填完,导出算子清单,又愣了一下:

CONV_2D  MAX_POOL_2D  FULLY_CONNECTED  MEAN  EXPAND_DIMS  RESHAPE

我明明写的是一维卷积,清单里一个 CONV_1D 都没有,凭空多出 CONV_2D 和几个 EXPAND_DIMS/RESHAPE。

查了才知道:TFLite 根本没有原生 Conv1D,转换器把它映射成 H=1 的 CONV_2D,前后配 EXPAND_DIMS/RESHAPE 把形状凑出来。这正是我之前犹豫要不要走框架路线时担心的事——转换器会不会悄悄改了我的图。好在这次它处理得干净,数值逐条一致,虚惊一场。

但这一下也把框架路线的问题暴露了:每一次格式转换,都是希望它对了 这个模型小、算子简单,转换器没出岔子;模型再复杂点.........

( ´_ゝ`)

坑都填完,导出最终产物:imu_net_int8.tflite,8752 字节

然后转换成.c.h就结束了


然后打开e2studio,需要加TFLM相关组件

(我这个工程使用的是github.com/renesas/cpk_examples/tree/main/cpkexp_ekra8x1/mipi_cpkexp_ra8d1_ep,官方给的mipi屏幕的例程)

屏幕截图 2026-09-18 225229.png

因为 TFLM 是用 C++ 写的,它的接口(API)也是 C++ 的——而 C++ 代码只能由 C++ 文件来调,所以如果RA工程是C的话,需要转成C++工程

右键项目,选择新建-其他

屏幕截图 2026-09-18 223710.png

选择转换成C/C++项目

屏幕截图 2026-09-18 224108.png

点击完成

image.png

如果这时候编译,就会出现一堆错误,因为这个工程的底座是官方mipi屏幕的例程,而这个例程的BSP 和 HAL 全是 5.3.0,而 TFLM 和 Flatbuffers 是 6.5.0 的。这个工程是 FSP 5.3.0 的底座,被塞进了 FSP 6.5.0 的 TFLM。

本地装了哪些包我查了:

Arm.CMSIS-NN.4.1.0+fsp.5.3.0.pack          ← 配 5.3.0 的
Arm.CMSIS-NN.7.0.0+fsp.6.5.0.pack          ← 配 6.5.0 的
Google.TFLM-Core-Lib.25.2.0+renesas.0.fsp.6.5.0.pack
Google.TFLM-CMSIS-NN-Kernel.25.2.0+renesas.0.fsp.6.5.0.pack
Google.TFLM-Ethos-U-Kernel.25.2.0+renesas.0.fsp.6.5.0.pack

TFLM 只有 6.5.0 版本,没有 5.3.0 版本。而 TFLM 25.2 是照 CMSIS-NN 7.x 的 API 写的。在 5.3.0 工程里能勾到的 CMSIS-NN 只有 4.1.0——头文件是能找到了,但 arm_convolve_wrapper_s8 这类函数从 4.x 到 7.x 签名和 context/buffer 约定都改过,接着就会在参数上报一堆新错。从一个错换成二十个错。

所以需要在bsp里更新fsp版本到最新的6.5.0

这个方案风险巨大,我不太清楚fsp版本更替兼容性融合,但是跨三个大版本,mipi屏幕显示有翻车风险

还需要做的就是在这里将C++11改成C++17

屏幕截图 2026-09-19 101036.png

这些全做完再编译应该就不会再出错了

编译过了之后,写推理胶水层,把模型跑起来:

#include <new>
#include "imu_net_model.h" 
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/schema/schema_generated.h"

#define WINDOW   300
#define CHANNELS 6
#define CLASSES  3
#define ACCEL_DIV  4000.0f
#define GYRO_DIV   500000.0f


alignas(16) static uint8_t s_arena[40 * 1024];
alignas(8)  static uint8_t s_interp_buf[sizeof(tflite::MicroInterpreter)];

/* 导出的 6 个算子 + 顺带几个 shape op,省得漏了报 AllocateTensors 错 */
static tflite::MicroMutableOpResolver<8> s_resolver;
static tflite::MicroInterpreter *sp_interp = nullptr;

static const char *sp_names[CLASSES] = { "still", "shake", "pickup" };

bool imu_infer_init(void)
{
    const tflite::Model *model = tflite::GetModel(imu_net_model);
    if (model->version() != TFLITE_SCHEMA_VERSION) return false;

    s_resolver.AddConv2D();
    s_resolver.AddMaxPool2D();
    s_resolver.AddFullyConnected();
    s_resolver.AddMean();
    s_resolver.AddExpandDims();
    s_resolver.AddReshape();
    s_resolver.AddRelu();
    s_resolver.AddSoftmax();

    sp_interp = new (s_interp_buf)
                tflite::MicroInterpreter(model, s_resolver, s_arena, sizeof(s_arena));

    return sp_interp->AllocateTensors() == kTfLiteOk;
}

/* 零点为 0,量化就是除量纲 + 饱和 */
static int8_t sat(float v)
{
    v = (v >= 0) ? v + 0.5f : v - 0.5f;
    if (v > 127)  return 127;
    if (v < -128) return -128;
    return (int8_t)v;
}
int8_t imu_quant_accel(int32_t mg)   { return sat(mg / ACCEL_DIV); }
int8_t imu_quant_gyro (int32_t mdps) { return sat(mdps / GYRO_DIV); }

/* p_input: 已量化好的 [300,6] int8;返回类别下标 */
int32_t imu_infer_run(const int8_t *p_input, int8_t *p_out)
{
    int8_t *dst = sp_interp->input(0)->data.int8;
    for (int i = 0; i < WINDOW * CHANNELS; i++) dst[i] = p_input[i];

    if (sp_interp->Invoke() != kTfLiteOk) return -1;

    const int8_t *src = sp_interp->output(0)->data.int8;
    int32_t best = 0;
    for (int i = 1; i < CLASSES; i++)
        if (src[i] > src[best]) best = i;

    if (p_out)
        for (int i = 0; i < CLASSES; i++) p_out[i] = src[i];

    return best;
}

const char *imu_infer_class_name(int32_t c)
{
    return (c >= 0 && c < CLASSES) ? sp_names[c] : "?";
}

然后写了个测试脚本进行测试

00> [2] capturing 300 samples at 100 Hz (3 s) - move the board now
00>   captured 300 samples, overruns 0
00>   sample[0]: ax=-47 ay=-43 az=1081 mg, gx=-3251 gy=1992 gz=1236 mdps
00>   quantised[0..5]: -1 -1 34 -1 1 0
00>   live -> pickup  q=[-31,-12,37]  18376 us

00> [2] capturing 300 samples at 100 Hz (3 s) - move the board now
00>   captured 300 samples, overruns 0
00>   sample[0]: ax=-234 ay=-240 az=1092 mg, gx=18671 gy=22000 gz=10496 mdps
00>   quantised[0..5]: -7 -8 35 5 6 3
00>   live -> shake  q=[-118,108,-9]  18374 us

00> [2] capturing 300 samples at 100 Hz (3 s) - move the board now
00>   captured 300 samples, overruns 0
00>   sample[0]: ax=-2 ay=28 az=1075 mg, gx=-3335 gy=1946 gz=1290 mdps
00>   quantised[0..5]: 0 1 34 -1 0 0
00>   live -> still  q=[7,0,-12]  18374 us

这是三次不同动作的采集加推理,可以看到,三次均准确识别了,平均耗时18ms

至此,从采集、训练到端侧部署的完整链路全部跑通。


共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]