基于 Arm 的 TFLite Micro 端云一体方案

本文来自社区投稿与征集,作者 郑亚斌,Arm 中国生态技术市场经理。

概述

TensorFlow Lite Micro 是 TensorFlow Lite 针对 AIOT 的轻量级 AI 引擎,用于在微控制器和其他资源受限的设备上运行机器学习模型。端云链接从 TencentOS Tiny 开始。

1. 建立与转换模型

由于嵌入式设备存储空间有限,因此限制了深度学习的应用,同时考虑到平台算力以及算子支持等因素,因此在模型设计以及部署阶段需充分了解硬件平台资源以及预期性能。
本部分主要介绍如何将 TensorFlow 模型部署在资源受限的嵌入式设备的过程。

1.1 模型转换

将一个已训练好的 TensorFlow 模型转换为可以在嵌入式设备中运行的 TensorFlow Lite 模型可以使用 TensorFlow Lite 转换器 Python API。它能够将模型转换成 FlatBuffer 格式,减小模型规模,并修改模型及算子以支持 TensorFlow Lite 运算。

1.1.1 量化

为了获得尽可能小的模型,某些情况下可以考虑使用 训练后量化。它会降低模型中数字的精度,从而减小模型规模,比如将 FP32 转化为 Int8。不过,这种操作可能会导致模型推理准确性的下降,对于小规模模型尤为如此,所以我们需要在量化前后分析模型的准确性,以确保这种损失在可接受范围内。

训练后量化需要 representive dataset 作为参考,以下这段 Python 代码片段展示了如何使用训练后量化进行模型转换:

import tensorflow as tf
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
def representative_dataset_generator():
    for value in reference_data:
        yeild [np.array(value, dtype=np.float32, ndmin=2)]
converter.representative_dataset = representative_dataset_generator
tflite_quant_model = converter.convert()
open("converted_model.tflite", "wb").write(tflite_quant_model)

1.1.2 将模型文件转换为一个 C 数组

许多微控制器平台没有本地文件系统,从程序中使用一个模型最简单的方式是将其转换为 C 数组并将其编译进应用程序。
以下的 unix 命令会生成一个包含 TensorFlow Lite 模型的 C 源文件,其中模型数据以 char 数组形式表现:

xxd -i converted_model.tflite > model_data.cc

其输出类似如下:

unsigned char converted_model_tflite[] = {
    0x18, 0x00, 0x00, 0x00, 0x54, 0x46, 0x4c, 0x33, 0x00, 0x00, 0x0e, 0x00,
};
unsigned int converted_model_tflite_len = 18200;

生成此文件后,你可以将其包含到程序中。在嵌入式平台上,我们需要将该数组声明为 const 类型以获得更好的内存效率。

1.2 模型结构与训练

在设计一个面向微控制器的模型时,考虑模型的规模、工作负载以及模型所使用到的算子是非常重要的。

1.2.1 模型规模

一个模型必须在二进制和运行时方面都足够小,以使其编译进应用程序后满足目标设备的内存限制。
为了创建一个更小的模型,你可以在模型设计中采用少而小的层。然而,小规模的模型更易导致欠拟合问题。这意味着对于许多应用,尝试并使用符合内存限制的尽可能大的模型是有意义的。但是,使用更大规模的模型也会增加处理器工作负载。

注:在一个 Cortex M3 上,TensorFlow Lite Micro 的 core runtime 仅占约16 KB。

1.2.2 工作负载

工作负载受到模型规模,结构与复杂度的影响,大规模、复杂的模型可能会导致更高的功耗。在实际的应用场景中,功耗与热量的增加可能会带来其他问题。
1.2.3 算子支持

TensorFlow Lite Micro 目前仅支持有限的 TensorFlow 算子,因此可运行的模型也有所限制。Google 正致力于在参考实现和针对特定结构的优化方面扩展算子支持。Arm 的 CMSIS-NN 开源加速库也为算子的支持和优化提供了另一种可能。

已支持的算子在文件 all_ops_resolver.cc 中列出。

1.3 运行推断

以下部分将介绍软件包自带语音例程中的 main_functions.cc 文件,并阐述了如何使用 TensorFlow Lite Micro 来进行 AI 推理。

1.3.1 包含项

#include "tensorflow/lite/micro/kernels/micro_ops.h"
#include "tensorflow/lite/micro/micro_error_reporter.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "tensorflow/lite/version.h"

示例中还包括其他一些文件,比如:

#include "tensorflow/lite/micro/examples/micro_speech/micro_features/micro_model_settings.h"
#include "tensorflow/lite/micro/examples/micro_speech/micro_features/model.h"
  • model.h 将模型存储为 char 类型数组。阅读 “构建与转换模型” 了解如何将 TensorFlow Lite 模型转换为该格式。

1.3.2 设置日志记录

要记录日志,需要实例化 tflite::MicroErrorReporter 类:

tflite::MicroErrorReporter micro_error_reporter;
tflite::ErrorReporter* error_reporter = &micro_error_reporter;

该对象被传递到解释器 (interpreter) 中用于记录日志。由于微控制器通常具有多种日志记录机制,因此 tflite::MicroErrorReporter 在实现上考虑了设备的差异性。

1.3.3 加载模型

在以下代码中,实例化的 char 数组中包含了模型信息,g_tiny_conv_micro_features_model_data (要了解其是如何构建的,请参见 “构建与转换模型”) 。随后我们检查模型来确保其架构版本与使用版本兼容:

const tflite::Model* model =
    ::tflite::GetModel(g_tiny_conv_micro_features_model_data);
if (model->version() != TFLITE_SCHEMA_VERSION) {
    error_reporter->Report(
    "Model provided is schema version %d not equal "
    "to supported version %d.\n",
    model->version(), TFLITE_SCHEMA_VERSION);
    return  1;
}

1.3.4 实例化算子

算子(OP)实例化有两种方法:

  1. tflite::op::micro::AllOpsResolver
  2. tflite::MicroMutableOpResolver

前者包含所有 TensorFlow Lite Micro 可用算子,并将他们提供给解释器 (Interpreter)

tflite::ops::micro::AllOpsResolver resolver;

前者是一种可靠但是比较浪费资源的方法,因为给定的模型不会使用全部算子。多余的算子会占用不必要的内存空间,因此后者只需要实例化真正使用的算子。模型中包含哪些算子可以通过开源工具 Netron 查看。

//定义模型需要的算子
namespace tflite {
namespace ops {
namespace micro {
TfLiteRegistration* Register_SOFTMAX();
} //namespace micro
} //namespace ops
} //namespace tflite


tflite::MicroMutableOpResolver micro_mutable_op_resolver;
micro_mutable_op_resolver.AddBuiltin(
    tflite::BuiltinOperator_SOFTMAX,
    tflite::ops::micro::Register_SOFTMAX());

1.3.5 分配内存

我们需要预先为输入、输出以及中间变量分配一定的内存。该预分配的内存是一个大小为 tensor_arena_sizeuint8_t 数组。它将会作为 tflite::SimpleTensorAllocator 实例化的参数:

const int tensor_arena_size = 10 * 1024;
uint8_t tensor_arena[tensor_arena_size];
tflite::SimpleTensorAllocator tensor_allocator(tensor_arena,
                                               tensor_arena_size);

注:所需内存大小取决于使用的模型,可能需要通过实验来确定。

1.3.6 实例化解释器 (Interpreter)

我们创建一个 tflite::MicroInterpreter 实例并传递相关变量:

tflite::MicroInterpreter interpreter(model, resolver, &tensor_allocator,                                     error_reporter);

1.3.7 验证输入维度

MicroInterpreter 实例可以通过调用 .input(0) 返回模型输入张量的指针。其中 0 代表第一个(也是唯一的)输入张量。我们通过检查这个张量来确认它的维度与类型是否与应用匹配:

TfLiteTensor* model_input = interpreter.input(0);
if ((model_input->dims->size != 4) || (model_input->dims->data[0] != 1) ||
    (model_input->dims->data[1] != kFeatureSliceCount) ||
    (model_input->dims->data[2] != kFeatureSliceSize) ||
    (model_input->type != kTfLiteUInt8)) {
    error_reporter->Report("Bad input tensor parameters in model");
    return  1;
}

在上述代码中,变量 kFeatureSliceCountkFeatureSliceSize 与输入相关,其定义在 micro_model_settings.h 中。枚举值 kTfLiteUInt8 是对 TensorFlow Lite 某一数据类型的引用,其定义在 common.h 中。

1.3.8 运行模型

通过在 tflite::MicroInterpreter 实例上调用 Invoke() 可快速触发推理:

TfLiteStatus invoke_status = interpreter.Invoke();
if (invoke_status != kTfLiteOk) {
    error_reporter->Report("Invoke failed");
    return  1;
}

通过检查返回值 TfLiteStatus 来确定运行是否成功。在 common.h 中定义的 TfLiteStatuskTfLiteOkkTfLiteError两种状态。

1.3.9 获取输出

模型的输出张量可以通过在 tflite::MicroIntepreter 上调用 output(0) 获得,其中 0 代表第一个(也是唯一的)输出张量。

在示例中,输出是一个数组,表示输入属于不同类别(“是”(yes)、“否”(no)、“未知”(unknown) 以及“静默”(silence))的概率。由于它们是按照集合顺序排列的,我们可以使用简单的逻辑来确定概率最高的类别:

TfLiteTensor* output = interpreter.output(0);
uint8_t top_category_score = 0;
int top_category_index;
for (int category_index = 0; category_index < kCategoryCount;
    ++category_index) {
    const  uint8_t category_score = output->data.uint8[category_index];
    if (category_score > top_category_score) {
        top_category_score = category_score;
        top_category_index = category_index;
    }
}

在示例的其他部分中,使用了一个更加复杂的算法来平滑多帧的识别结果。该部分在 recognize_commands.h 中有所定义。在处理任何连续的数据流时,也可以使用相同的技术来提高可靠性和准确度。

** 2. 制作 tensorflow_lite_micro.lib**

2.1 获得 TensorFlow Lite Micro 库

构建库并从 TensorFlow master branch 中运行测试,请执行以下命令:

将 TensorFlow project 下载到本地,在终端中输入以下命令:

git clone https://github.com/tensorflow/tensorflow.git

注:由于 TensorFlow 官方仓库的更新速度较快,为了方便开发者学习 .lib 库的制作方法,作者使用 Hash ID 为 5e0ed38eb746f3a86463f19bcf7138a959ddb2d4 版本来进行演示。由于代码更新,使用 master 版本可能需要对流程或代码进行微调。

进入 clone 好的仓库:

cd tensorflow

项目中的 Makefile 能够生成包含所有源文件的独立项目,并支持导入目前成熟的嵌入式开发环境。目前支持的环境主要有 Arduino, Keil, Make 和 Mbed。

注意:我们为其中一些环境托管预建项目。参阅 支持的平台

要在 Make 中生成项目,请使用如下指令:

make -f tensorflow/lite/micro/tools/make/Makefile generate_projects

这需要几分钟下载并配制依赖。结束后,新的文件夹生成并包含源文件,例如 tensorflow/lite/micro/tools/make/gen/linux_x86_64/prj/hello_world/keil 包含了 hello world 的 Keil uVision 工程。
以下以 hello_world 工程为例,分离出与实际应用无关的 tflite_micro 源文件,用于后续 .lib 库生成。
运行本目录下的 lib_extra.py 生成 tflite micro 源文件包:

参数 含义
--tensorflow_path 下载的 TensorFlow 仓库的根目录(绝对路径)
--tflitemicro_path 生成的 TensorFlow Lite Micro路径(绝对路径)

脚本成功运行后打印 --tensorflow lite micro source file extract successful-- 信息,并在对应的 tflitemicro_path 路径下生成 Source 文件夹存放 TensorFlow Lite Micro 源文件。

2.2 将源文件加入 KEIL 工程并生成 .lib 库

2.2.1 添加文件

新建目标芯片的 KEIL 工程(本次示例以 ARM Cortex M4 为例),将 Source 目录下的 tensorflowthird_party 文件夹导入到 KEIL 工程根目录下,并添加 tensorflow 目录中除 lite/micro/kernels 以及 lite/micro/tools 文件以外的所有源文件(包含 .c 和 .cc)。如下图所示:

注意在添加 tensorflow/lite/micro/kernel 目录下的源文件时需要区分 reference 算子和 Arm CMSIS-NN 优化加速算子。tensorflow/lite/micro/kernel 文件夹内容如下图中所示:

image

根据图中显示,我们采用 CMSIS-NN 优化加速算子制作 .lib 库文件。
:CMSIS-NN 是 Arm 在 AI 领域针对 IOT 设备开发的神经网络加速库,其目的是为了让 AI 在算力和资源有限的设备上落地,更好的发挥 Arm 的生态优势。相关代码和文档已 开源。在 TensorFlow Lite Micro 框架下的 CMSIS-NN 算子与 reference 算子性能对比可参考 附录

2.2.1.1 采用 CMSIS-NN 生成 .lib 文件

需要:

  1. 添加 tensorflow/lite/micro/kernel/cmsis-nn 源文件;
  2. 添加 tensorflow/lite/micro/kernel/ 中的算子时,请不要添加 add.ccconv.ccdepthwise_conv.ccsoftmax.ccfully_connected.ccpooling.ccmul.cc 源文件;
  3. 添加 tensorflow/lite/micro/tools 文件夹下的源文件。

2.2.1.2 采用 reference 算子生成 .lib 文件

需要:

  1. 添加 tensorflow/lite/micro/kernel/ 中的全部算子;
  2. 无需添加 tensorflow/lite/micro/tools 文件夹下的源文件。

2.2.2 配制编译选项

采用 compiler version 6 编译器并关闭 Microlib :

选择 Create Library 选项并修改 .lib 库名为:tensorflow_lite_micro

配置有关的宏、包含的头文件路径并设置代码优化等级:

最后点击编译链接选项,即可在工程根目录的 Objects 文件夹下生成 ARM Cortex M4 对应的 .lib 库。其他内核型号的 tflite_micro 库以此类推。

3. 在 TencentOS Tiny 中实现端云结合

得益于 TencentOS Tiny 的物联网组件支持,嵌入式设备可以快速接入腾讯云物联网平台。本章节介绍基于 TencentOS Tiny 的物联网组件以及如何接入腾讯云物联网开发平台(IoT Explorer),实现将端侧 AI 推理结果发送至云端,实现端云结合。

3.1 qcloud-iot-sdk 软件架构

腾讯云物联网开发平台 IoT Explorer 设备端 C SDK,配合平台对设备数据模板化的定义,实现和云端基于数据模板协议的数据交互框架。开发者基于 IoT_Explorer C-SDK 数据模板框架,通过脚本自动生成模板代码,快速实现设备和平台、设备和应用之间的数据交互。

TencentOS Tiny 的 qcloud-iot-sdk 分四层设计,从上至下分别为平台服务层、核心协议层、网络层和硬件抽象层。

  • 平台服务层:在网络协议层之上,实现了包括设备接入鉴权,设备影子,数据模板,网关,动态注册,日志上报和 OTA 等功能,提供相关 API 接口给用户使用。
  • 核心协议层:设备端和 IoT 平台交互的网络协议包括 MQTT/COAP/HTTP。
  • 网络层:实现基于 TLS/SSL(TLS/DTLS) 方式,BSD_socket(TCP/UDP) 方式和 AT_socket 方式的网络协议栈,不同服务可根据需要使用不同的协议栈接口函数,本例程使用通信模组结合 AT_socket 的方式。
  • 硬件抽象层:实现对不同硬件平台底层操作的抽象封装,需要针对具体的软硬件平台开展移植,分为必须实现和可选实现两部分 HAL 层接口。

3.2 创建云端设备

开发过程如下,登录腾讯云开发平台:

3.2.1 新建产品

3.2.2 定义数据模板

设备端上传的只读数据:

  • AI 推理结果:设备端检测并发送;

设备端接收的控制指令:

  • 控制指令:用户看到有异常发生,可以控制设备端做出某种反应。

3.3 端侧开发

3.3.1 移植mbedtls

腾讯云 C SDK 对接云端时如果配置建立安全链接,将会用到 mbedtls 加密库,所以先移植 mbedtls。
1. 将 TencentOS Tiny 中已经移植适配好的 mbedtls 库作为 TencentOS Tiny 的一个组件:在 components\security\mbedtls 目录下,将此目录复制到工程目录中,复制过程中保持目录架构不变,并将其余的文件删除。

image

2. 将 mbedtls 相关的 c 文件添加到 Keil-MDK 工程中:

a. 在 Keil 工程中新建 mbedtls 目录分组。
b. 将 components\security\mbedtls\wrapper\src 目录下的移植适配文件添加至 mbedtls 目录下。
c. 将 components\security\mbedtls\3rdparty\src 目录下的源码文件添加至 mbedtls 目录下 。

image

3. 将 mbedtls 相关的头文件都添加到 Keil-MDK 工程中,移植完成。

image

3.3.2 腾讯 qcloud-iot-sdk 移植

TencentOS Tiny 已将 IoT_Explorer C SDK 移植适配完成,在 components\connectivity\qcloud-iot-explorer-sdk 目录下包含两个文件夹,其中:

  • 3rdparty:IoT_Explorer C SDK 源码。
  • port\TencentOS_tiny:移植适配文件 qcloud/port。

在使用 TencentOS Tiny 物联网操作系统时,只需要添加相关文件并修改云端设备对接信息,即可方便快捷的实现端云结合。
接下来将基于上述步骤移植成功的网络工程,讲述如何移植 C SDK。

1. 将 TencentOS Tiny 源码中 qcloud-iot-explorer-sdk 整个目录复制到工程目录中,保持原有目录架构不变并删除其余目录。

image

2. 将腾讯云 C SDK 移植到 TencentOS Tiny 的 Keil 工程。

a. 在 Keil 工程下新增 qcloud/port 目录分组。
b. 将 components\connectivity\qcloud-iot-explorer-sdk\port\TencentOS_tiny 分组下的部分文件添加至 qcloud/port 目录。

image

3. 添加腾讯云 C SDK 中 MQTT 协议相关源码。

a. 在 Keil 工程下新增 qcloud/protocol/mqtt 目录分组。
b. 将 components\connectivity\qcloud-iot-explorer-sdk\3rdparty\sdk_src\protocol\mqtt 目录下的文件添加至 qcloud/protocol/mqtt 目录。

image

4. 添加腾讯云 C SDK 中数据模板相关源码。

  1. 在 Keil 工程下新增 qcloud/services/data_template 目录分组。
  2. components\connectivity\qcloud-iot-explorer-sdk\3rdparty\sdk_src\services\data_template 目录下的文件添加至 qcloud/services/data_template 目录下。

image

5. 添加腾讯云 C SDK 中所使用到的工具源码。

  1. 在 Keil 工程下新增 qcloud/utils 目录分组。
  2. components\connectivity\qcloud-iot-explorer-sdk\3rdparty\sdk_src\utils 目录下的文件添加至 qcloud/utils 目录。

image

6. 添加腾讯云 C SDK 中所用到网络封装层源码。

  1. 在 Keil 工程下新增 qcloud/network 目录分组。
  2. components\connectivity\qcloud-iot-explorer-sdk\3rdparty\sdk_src\network 目录下的文件添加至 qcloud/network 目录。
  3. components\connectivity\qcloud-iot-explorer-sdk\3rdparty\platform\tls\mbedtls 目录下的文件添加至 qcloud/network 目录。

image

7. 添加头文件路径。

image

8. 最后添加宏定义 MBEDTLS_CONFIG_FILE=<qcloud/tls_psk_config.h>,指定 mebedtls 库的配置文件。

image

移植完成,此时编译时未发现错误信息,其中警告可暂时忽略。

3.3.3 修改端云对接信息

修改 HAL_Device_tencentos_tiny.c 文件。在 TencentOS-tiny\components\connectivity\qcloud-iot-explorer-sdk\port\TencentOS_tiny 目录中,将下图中的数据分别替换为控制台【设备详情页】中的参数并保存。

  • 产品 ID:将控制台的产品 ID ,复制到下图 sg_product_id。
  • 设备名称:将控制台的设备名称,复制到下图 sg_device_name。
  • 设备密钥:将控制台的设备密钥,复制到下图 sg_device_secret。

3.3.4 调用 TensorFlow Lite Micro

经过步骤 2.2,我们成功将 TensorFlow Lite Micro 以及 CMSIS-NN 生成 .lib 组件。在应用程序开发中,只需要包含对应的 .h 文件,即可使能相应功能。
例如,在 TencentOS Tiny 的 example 目录下包含了相关的 AI 案例。在函数中通过调用组件的头文件来使能 TensorFlow Lite Micro 以及 CMSIS-NN。

//...
#include "tensorflow/lite/micro/micro_error_reporter.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "tensorflow/lite/version.h"
//...
  • 编写线程任务函数进行AI推理:
void task1(void *arg)
{
    tflite_micro_init(); //initial
    while (1) 
    {
        result = tflite_micro(model_buffer); //Inference         
        osDelay(50);                         //delay 50ms
    }
}                       //delay 50ms    }}

3.3.5 多线程调度

TencentOS Tiny 具备多线程调度能力,因此可以将 AI 推理和云端连接在两个线程中分别实现,大大简化了代码逻辑并加快开发速度。

  • MCU 与云端之间数据传输部分的代码如下:
void task2(void *arg)
{
    printf("connect to IoT Explorer\n");
    extern int esp8266_sal_init(hal_uart_port_t uart_port);
    extern int esp8266_join_ap(const char *ssid, const char *pwd);


    esp8266_sal_init(HAL_UART_PORT_2);
    esp8266_join_ap("wifi", "password..");


    iot_thread();
}
  • 与云端连接以及收发数据的具体代码如下:
int deal_up_stream_user_logic(DeviceProperty *pReportDataList[], int *pCount)
{
    int i, j;
    //upload
    for (i = 0, j = 0; i < TOTAL_PROPERTY_COUNT; i++) {
        if(eCHANGED == sg_DataTemplate[i].state) {
            pReportDataList[j++] = &(sg_DataTemplate[i].data_property);
            sg_DataTemplate[i].state = eNOCHANGE;
        }
    }
    *pCount = j;


    return (*pCount > 0)?QCLOUD_RET_SUCCESS:QCLOUD_ERR_FAILURE;
}
  • 主函数如下:
int main(void)
{
    board_init();
    printf("Welcome to TencentOS Tiny\r\n");
    osKernelInitialize(); // TOS Tiny kernel initialize
    osThreadCreate(osThread(application_entry), NULL); // Create TOS Tiny task
    osKernelStart(); // Start TOS Tiny
}

4. TencentOS Tiny AI 开发组件

TencentOS Tiny 已将 TensorFlow Lite Micro 以及 CMSIS-NN 集成到 AI 组件中,并通过其他组件与腾讯云无缝相连,打通从云到端整条链路,助力 AI 的发展与落地。随着越来越多的厂商采用 Arm Cortex M55 和 Ethos U NPU IP,相信未来端侧 AI 的应用会更加广阔。

5. 致谢

感谢 TencentOS Tiny 团队和 TensorFlow 开源社区的支持,感谢开发者邓可笈,杨庆生和刘恒言的贡献。

如果您想在 TensorFlow 社区分享经验与用例,点击 TensorFlow 案例征集 填写相关信息,我们会尽快与您联系。

中文:TensorFlow 公众号