基于TensorFlow Lite Micro在HaaS EDU K1物联网开发板上玩转TinyML之离线语音唤醒

发布人:罗奎 技术专家,阿里巴巴

案例简介

随着机器学习的发展,TinyML(微型机器学习)已在你的家里、车里、甚至口袋里工作了。什么是 TinyML 呢?它是属于机器学习的一个子领域,包含了算法、硬件和软件,能够基于传感器数据分析,并在极低功耗的设备上运行。比如家里的天猫精灵、苹果的 Siri 和亚马逊的 Alexa 等语音助手,都属于 TinyML 的应用,它们提供了语音用户接口 (AUI),让用户不需要操作屏幕、鼠标或键盘就可以进行交互,给用户提供了一种全新的交互方式,这些语音助手几乎无处不在。从独立的智能音箱到几乎所有手机都内置了某种语音助手。

在大部分情况下,语音识别、自然语言处理以及语音合成等繁重工作都是在云端完成的,由性能强大的服务器运行大型机器学习模型。当用户提出问题时,将以 wav 或其他音频流的形式被发送到云端。云端识别出音频流的含义并进行回复响应。语音助手真正需要的音频是唤醒设备后的数据。如果能在不发送数据的情况下检测到这个唤醒词,并在听到唤醒词之后才开始音频流的传输,这样既能够保护用户的隐私、节省电池电量和带宽,而且可以在没有网络的情况下唤醒。

这也是 TinyML 的用武之地。要在低功耗的芯片上运行,意味着要训练一个监听唤醒词的微型模型,嵌入到微控制设备中,它就可以一直监听唤醒词,当检测到唤醒词后通知操作系统开始捕获音频并发送到云端。

在本文章中,将教大家如何使用预先训练的检测模型,在 HaaS EDU K1 上进行唤醒词检测模型的部署,并使用 Tensorflow Lite Micro 推理引擎进行推理。随后将教大家如何使用 Tensorflow 训练脚本训练一个自己的唤醒词,再进行设备端部署。本案例主要由三个部分组成:

1. 语音采集:接入模拟麦克风(Mic1 输入);
2. 语音识别:说出“打开”和“关闭”识别后,OLED 将显示 “Turn on…” 和 “Turn off”;
3. 语音播报:执行指令的同时,播报本地 TTS (mp3)。

开始学习之前我们先看一下案例效果:

HaaS 语音助手 语音控制

涉及知识点

● 唤醒词数据采集、模型训练、模型部署

● 设备端模拟 MIC 声音采样

● 设备端音频特征提取

● TFLite-Micro 推理引擎应用

● 设备端命令识别、响应

● 设备端喇叭播放 mp3 文件

● 文件系统应用

● OLED 显示字符

方案介绍

整个方案的框架如下:

● 提供批量音频录制工具进行数据收集;

● 提供 TF 模型训练脚本进行唤醒词训练;

● 提供完整设备端模型部署方案;

基于该方案,你将学习到 TinyML 的整个生命周期:

开发环境搭建

1. 硬件准备

如果有 HaaS 语音扩展板,直接插入即可:

如果没有 HaaS 语音扩展板,请按照如下步骤接入麦克风和喇叭:

HaaS EDU K1 硬件排线图请参考

HaaS EDU K1 说明书 - IoT 物联网操作系统 - 阿里云

购买链接仅供参考,无法保障商家发货的品质等问题。

名称 数量 参考链接
HaaS EDU K1 开发版 1 HaaS EDU K1 购买链接
Type-C USB 数据线 1 普通 Type-C USB 线即可
模拟 MIC 1 模拟 MIC 参考链接
喇叭 1 喇叭参考链接
杜邦线 数条 NA

2.环境搭建

参考 《HaaS EDU K1 快速开始》 中 HaaS Studio 章节完成 AliOS Things 开发环境搭建。

2.1 案例代码下载

该案例相关的源代码下载可参考 《创建工程》,该案例是 C/C++ 案例。

其中:

● 选择解决方案: “TFLite-Micro 离线语音快捷词唤醒案例”或者 “tflite_micro_speech_demo”

● 选择开发板: HaaS EDU K1

2.2 代码编译、烧录

参考 《HaaS EDU K1 快速开始》 完成代码的编译及烧录,在烧录前,请先完成 2.1 的步骤,再进行编译烧录。

文件件系统烧录

本组件例子中使用到的本地语料存放在代码中 hardware/chip/haas1000/prebuild/data/ 目录下 mp3 目录,除烧录 tflite_micro_speech_demo image 外,需烧录 littlefs 文件系统,请将 hardware/chip/haas1000/package.yaml 文件中以下代码段的注释打开后重新编译:

program_data_files:
    - filename: release/write_flash_tool/ota_bin/littlefs.bin
      address: 0xB32000

2.3 打开串口

参考 《HaaS EDU K1 快速开始》打开串口进行 LOG 查看。

软件架构

● KWS Demo 应用程序: 主要打通实现 AI 语音引擎的初始化,欢迎语播报。

● ai_agent 组件:是 AliOS Things 上的 AI 引擎核心模块,后端接入不同的推理引擎,本案例中使用了 TFLite-Micro 推理引擎。

● uVoice 组件:是 AliOS Things 上智能语音解决方案的核心组件,提供了本地音频,URL 音频,TTS 合成等基础功能,音频格式支持 mp3, m4a, wav, opus 等主流格式,本案例中使用它来进行本地 mp3 语料的响应播报。

● A2SA 组件:是 AliOS Things 上音频服务框架,兼容 ALSA 应用接口访问,支持音频硬件驱动抽象,多音频驱动加载/卸载,VFS 接口支持等功能。

1.代码结构

├── cp_resources.py     # 拷贝本地语料到/prebuild/data目录,编译进文件系统
├── main.c
├── maintask.c
├── Makefile
├── micro_speech        # 语音识别程序
├── oled                # OLED显示程序
│   ├── oled.c
│   └── oled.h
├── package.yaml        # 编译系统配置文件
├── player              # 播放器程序
│   ├── player.c
│   └── player.h
├── README.md
├── recorder            # 录音程序
│   ├── recorder.c
│   └── recorder.h
├── resources
│   └── mp3             # 本地mp3语料
├── SConstruct

2.设备端工作流程

在 HaaS EDU K1 上的整个工作流程如下图:

3.程序主体

以下代码是执行唤醒词识别主体,setup 对 TFLite-Micro 模型推理引擎进行初始化,loop 中执行上图中整个流程,从音频采集到命令响应的全部流程在该函数中实现,详细逻辑请参考代码。

// The name of this function is important for Arduino compatibility.
void setup()
{
    //   tflite::InitializeTarget();
    //RegisterDebugLogCallback(callback);
    // Set up logging. Google style is to avoid globals or statics because of
    // lifetime uncertainty, but since this has a trivial destructor it's okay.
    // NOLINTNEXTLINE(runtime-global-variables)
    static tflite::MicroErrorReporter micro_error_reporter;
    error_reporter = &micro_error_reporter;
    // Map the model into a usable data structure. This doesn't involve any
    // copying or parsing, it's a very lightweight operation.
    model = tflite::GetModel(g_model);
    if (model->version() != TFLITE_SCHEMA_VERSION)
    {
        TF_LITE_REPORT_ERROR(error_reporter,
                             "Model provided is schema version %d not equal "
                             "to supported version %d.",
                             model->version(), TFLITE_SCHEMA_VERSION);
        return;
    }
    // Pull in only the operation implementations we need.
    // This relies on a complete list of all the ops needed by this graph.
    // An easier approach is to just use the AllOpsResolver, but this will
    // incur some penalty in code space for op implementations that are not
    // needed by this graph.
    //
    // tflite::AllOpsResolver resolver;
    // NOLINTNEXTLINE(runtime-global-variables)
    static tflite::MicroMutableOpResolver<4> micro_op_resolver(error_reporter);
    if (micro_op_resolver.AddDepthwiseConv2D() != kTfLiteOk)
    {
        return;
    }
    if (micro_op_resolver.AddFullyConnected() != kTfLiteOk)
    {
        return;
    }
    if (micro_op_resolver.AddSoftmax() != kTfLiteOk)
    {
        return;
    }
    if (micro_op_resolver.AddReshape() != kTfLiteOk)
    {
        return;
    }
    // Build an interpreter to run the model with.
    static tflite::MicroInterpreter static_interpreter(
        model, micro_op_resolver, tensor_arena, kTensorArenaSize, error_reporter);
    interpreter = &static_interpreter;
    // Allocate memory from the tensor_arena for the model's tensors.
    TfLiteStatus allocate_status = interpreter->AllocateTensors();
    if (allocate_status != kTfLiteOk)
    {
        TF_LITE_REPORT_ERROR(error_reporter, "AllocateTensors() failed");
        return;
    }
    // Get information about the memory area to use for the model's input.
    model_input = interpreter->input(0);
    if ((model_input->dims->size != 2) || (model_input->dims->data[0] != 1) ||
        (model_input->dims->data[1] !=
         (kFeatureSliceCount * kFeatureSliceSize)) ||
        (model_input->type != kTfLiteInt8))
    {
        TF_LITE_REPORT_ERROR(error_reporter,
                             "Bad input tensor parameters in model");
        return;
    }
    model_input_buffer = model_input->data.int8;
    // Prepare to access the audio spectrograms from a microphone or other source
    // that will provide the inputs to the neural network.
    // NOLINTNEXTLINE(runtime-global-variables)
    static FeatureProvider static_feature_provider(kFeatureElementCount,
                                                   feature_buffer);
    feature_provider = &static_feature_provider;
    static RecognizeCommands static_recognizer(error_reporter);
    recognizer = &static_recognizer;
    previous_time = 0;
    RespondCommandThreadInit();
}
// The name of this function is important for Arduino compatibility.
void loop()
{
    // Fetch the spectrogram for the current time.
    const int32_t current_time = LatestAudioTimestamp();
    int how_many_new_slices = 0;
    TfLiteStatus feature_status = feature_provider->PopulateFeatureData(
        error_reporter, previous_time, current_time, &how_many_new_slices);
    // LOG("current_time: %d, previous_time: %d, how_many_new_slices: %d\n", current_time, previous_time, how_many_new_slices);
    if (feature_status != kTfLiteOk)
    {
        TF_LITE_REPORT_ERROR(error_reporter, "Feature generation failed");
        return;
    }
    previous_time = current_time;
    // If no new audio samples have been received since last time, don't bother
    // running the network model.
    if (how_many_new_slices == 0)
    {
        //LOG("[lk added]how_many_new_slices is 0\n");
        return;
    }
    // Copy feature buffer to input tensor
    for (int i = 0; i < kFeatureElementCount; i++)
    {
        model_input_buffer[i] = feature_buffer[i];
    }
    // Run the model on the spectrogram input and make sure it succeeds.
    TfLiteStatus invoke_status = interpreter->Invoke();
    if (invoke_status != kTfLiteOk)
    {
        TF_LITE_REPORT_ERROR(error_reporter, "Invoke failed");
        return;
    }
    // Obtain a pointer to the output tensor
    TfLiteTensor *output = interpreter->output(0);
    // Determine whether a command was recognized based on the output of inference
    const char *found_command = nullptr;
    uint8_t score = 0;
    bool is_new_command = false;
    TfLiteStatus process_status = recognizer->ProcessLatestResults(
        output, current_time, &found_command, &score, &is_new_command);
    if (process_status != kTfLiteOk)
    {
        TF_LITE_REPORT_ERROR(error_reporter,
                             "RecognizeCommands::ProcessLatestResults() failed");
        return;
    }
    // Do something based on the recognized command. The default implementation
    // just prints to the error console, but you should replace this with your
    // own function for a real application.
    RespondToCommand(error_reporter, current_time, found_command, score,
                     is_new_command);
}

案例体验

当程序烧录完成后,直接喊出“打开"/“关闭”,就可以看到视频所示的效果。目前只支持近场唤醒,唤醒距离 1 米左右。由于这个“打开”/“关闭”唤醒的语料有限,唤醒效果因人不同有差异。建议按照章节 6 中自己训练一个唤醒词或者使用数据集中的英文语料 “on/off” 试试。

自训练唤醒词

本案例是自训练了一个“打开”/“关闭”快捷唤醒词。本小节将带你训练一个新的快捷唤醒词。

从录音采集到部署到 HaaS EDU K1 的整个详细流程如下:

1.语料采集

语料采集是一个比较耗费人力的事情,通常商业化工程中语料收集有专人或专门的数据公司收集整理,这里提供了一个使用 Python 写的录音工具,方便你快速录音。

依赖项安装

#pip install pyaudio
或者
#conda install pyaudio

录音配置

● 语音文件长度一秒

● 单声道、16KHz、wav 格式

● 快、中、慢三种不同速度进行录制

● 录制次数 100 次以上,次数越多效果越好

● 相对安静环境

1.1 唤醒词录制

录制时看到“开始录音,请说话…”即可立即说出唤醒词,比如“打开”、“关闭”。由于我们检测一秒的唤醒词,所以注意要在一秒内说完整整个唤醒词,录制一次后会自动回放确认是否录制完整,如果录制完整,按回车键继续下一次录制,如果录制不完整或有其他杂音,按其他任意键删除刚才的录音再继续下一次录制。

执行命令:

#python micro_speech/train/record.py

毫无疑问,这个教学案例是教你如何录制一个人的声音,如果想要达到商业化的识别率,就至少需要 500 人以上的声音录制。如果仅仅录制你一个人的唤醒词,那么仅识别你的声音是可以的,但其他人在唤醒时的成功率就会低很多。这个案例重点是教你了解唤醒词训练部署的原理。

1.2 背景噪音录制

为了更好的识别,需要录制一些背景噪音,模型训练时会学习唤醒词和背景噪音的差别。背景噪音可以录制 1~2 分钟。模型训练时会自动从中随机选择片段作为背噪加入唤醒词中进行学习。

执行命令:

#python micro_speech/train/record_noise.py

录制背景噪音,放到 dataset/background_noise 目录。

1.3 创建自己的数据集

训练脚本中默认采样的预训练数据集是 Google 发布的 Speech Commands(语音命令)数据集,该数据集是英文数据集。这里我们以录制中文的“打开”/“关闭”为例,每个词录制 100 次。录制完成后分别命名为 dakai 和 guanbi 两个文件夹放入自定义的 my_dataset 目录,然后从 Speech Commands 中选择几个单词 house、marvin、wow 等唤醒词作为“未知”类别放入到 my_only_dataset 目录,它的作用是模型训练时能够从这些唤醒词中识别想要的 dakai 和 guanbi 命令,dakai 和 guanbi 可以理解为正面示例,“未知”类别为反面示例。整个命令词个数尽量限制在十个以下,这样训练的时间不会过久。如果你有其他同样长度且与录音配置中格式一样的唤醒词,也可以加入进来。另外如果录制的是 100 次唤醒词,那么其他作为“未知”类别的唤醒词的录音示例个数也尽量在 100 左右。录制的背景噪音放入到 background_noise 目录,训练时脚本将自动从中随机选取一秒片段作为背景噪音加入到“无声”类别中。

2.模型训练

2.1 PC 端训练

PC 上在 VSCode 中使用 jupyter notebook 插件打开 tflite_micro_speech_demo/micro_speech/train/train_micro_speech_model.ipynb

进行其他唤醒词的训练。

前提:

参考 《HaaS AI 之 VSCode 中搭建 Python 虚拟环境》 搭建完开发环境后,安装 tensorflow 1.15 版本:

#conda create --name tf python=3.6
#conda activate tf
#conda install tensorflow=1.15

2.2 阿里云 PAI 平台训练

如果 PC 性能有限,使用阿里云 PAI 平台进行训练也是一个不错的选择,PAI-DSW 是一款云端机器学习开发 IDE,为您提供交互式编程环境,适用于不同水平的开发者。你可以根据需要选择个人版、GPU 特价版或 探索者版(免费),相关使用手册 DSW 新手使用手册

以使用 DSW 个人版为例:

1. 登录 PAI 控制台

  • 登录 PAI 控制台

2. 在左侧导航栏,选择模型开发和训练 > 交互式建模 (DSW)。

3. 在页面左上方,选择目标地域。

4. 在 Notebook 建模服务页面,单击创建实例。

5. 在配置实例向导页面,配置参数,镜像选择 tensorflow1.15-gpu-py36-cu101-ubuntu18.04 版本。

2.3 模型配置

无论在什么平台上进行训练,脚本中需要对训练的参数进行一定的配置:

唤醒词配置

WANTED_WORDS 就是你训练的唤醒词。比如:

WANTED_WORDS=“yes, on”,yes/on 对应于数据集 dataset 目录的唤醒词语料文件夹。这里根据你要训练的唤醒词修改。

训练步数配置

如果你的唤醒词仅仅数百条甚至数 10 条,那么训练的步数不用太久,修改:

TRANINGS_STEPS=“1200, 300”

如果你有上千条以上,训练的步数可以增加。

TRANINGS_STEPS=“15000, 3000”

为了防止训练欠拟合或者过拟合,训练的时间长短需要反复验证,找到最优的结果。

数据集配置

如果使用自己的数据集,请修改:

DATASET_DIR = ‘./dataset/’

3.模型部署

模型部署在 HaaS EDU K1 上,主要有三个步骤:

1. 模型替换:将生成的模型文件 model.cc 替换 micro_speech/micro_features/model.cc 文件

2. 标签更新:在 micro_speech/micro_features/micro_model_settings.cc 中修改 kCategoryLabels 标签内容,将标签名更换为你训练的快捷词,比如“打开”、“关闭”。由于标签与模型的输出张量元素是按照顺序进行匹配的,因此,需要按照将标签提供给训练脚本的顺序列出这些标签。

3. 业务逻辑更新:在 micro_speech/command_responder.cc 中根据标签更新相应的业务逻辑。目前在听到“打开”后,会打开 HaaS EDU K1 上 R/G/B LED 灯。你也可以修改逻辑比如通过 WiFi 打开远程的风扇或灯。这里可以充分发挥你的想象力打造一些比较有意思的场景应用。

总结

本案例在 HaaS EDU K1 上基于 TFLite-Micro 推理引擎进行语音唤醒词的部署。也提供了从唤醒词采集到模型训练、模型部署的全链路开发流程,帮助您深入理解在低功耗 MCU 上如何进行离线语音识别的开发部署,期待您打造更多属于你的离线唤醒词应用。

中文:TensorFlow 公众号