基于TensorFlow Lite Micro在物联网设备上玩转TinyML之离线语音唤醒

Posted HaaS技术社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于TensorFlow Lite Micro在物联网设备上玩转TinyML之离线语音唤醒相关的知识,希望对你有一定的参考价值。

1. 案例简介

随着机器学习的发展,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语音助手 语音控制

1.1 涉及知识点

  • 唤醒词数据采集、模型训练、模型部署
  • 设备端模拟MIC声音采样
  • 设备端音频特征提取
  • TFLite-Micro推理引擎应用
  • 设备端命令识别、响应
  • 设备端喇叭播放mp3文件
  • 文件系统应用
  • OLED显示字符

2. 方案介绍

整个方案的框架如下:

 

  • 提供批量音频录制工具进行数据收集;
  • 提供TF模型训练脚本进行唤醒词训练;
  • 提供完整设备端模型部署方案;

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

 

3. 开发环境搭建

3.1 硬件准备

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

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

HaaS EDU K1硬件排线图请参考
HaaS EDU K1说明书 - IoT物联网操作系统 - 阿里云

购买链接仅供参考!!我们不负责商家发货的品质保障等问题!!

名称

数量

参考链接

HaaS EDU K1开发版

1

HaaS EDU K1购买链接

microUSB数据线

1

普通microusb线即可

模拟MIC

1

模拟MIC参考链接

喇叭

1

喇叭参考链接

杜邦线

数条

NA

3.2 环境搭建

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

3.2.1 案例代码下载

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

  • 选择解决方案: “TFLite-Micro离线语音快捷词唤醒案例”或者“tflite_micro_speech_demo”
  • 选择开发板: HaaS EDU K1

 

3.2.2 代码编译、烧录

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

3.2.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

3.2.3 打开串口

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

4. 软件架构

  • 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接口支持等功能。

4.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

4.2 设备端工作流程

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

4.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);
}

5. 案例体验

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

6. 自训练唤醒词

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

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

6.1 语料采集

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

依赖项安装

#pip install pyaudio
或者
#conda install pyaudio

录音配置

  • 语音文件长度一秒
  • 单声道、16KHz、wav格式
  • 快、中、慢三种不同速度进行录制
  • 录制次数100次以上,次数越多效果越好
  • 相对安静环境

6.1.1 唤醒词录制

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

执行命令:

#python micro_speech/train/record.py

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

6.1.2 背景噪音录制

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

执行命令:

#python micro_speech/train/record_noise.py

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

6.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_目录,训练时脚本将自动从中随机选取一秒片段作为背景噪音加入到“无声”类别中。

6.2 模型训练

6.2.1 PC端训练

PC上在VSCode中使用jupyter notbook插件打开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

6.2.2 阿里云PAI平台训练

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

  1. 登录PAI控制台
  2. 在左侧导航栏,选择模型开发和训练 > 交互式建模(DSW)。
  3. 在页面左上方,选择目标地域。
  4. 在Notebook建模服务页面,单击创建实例。
  5. 在配置实例向导页面,配置参数,镜像选择tensorflow1.15-gpu-py36-cu101-ubuntu18.04版本。

6.2.3 模型配置

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

唤醒词配置

WANTED_WORDS就是你训练的唤醒词。比如:
WANTED_WORDS="yes, on",yes/on对应于数据集dataset目录的唤醒词语料文件夹。这里跟你你要训练的唤醒词修改。

训练步数配置

如果你的唤醒词仅仅走数百条甚至数10条,那么训练的步数不用太久,修改:
TRANINGS_STEPS="1200, 300"
如果你有上千条以上,训练的步数可以增加:
TRANINGS_STEPS="15000, 3000"
为了防止训练欠拟合或者过拟合,训练的时间长短需要反复验证,找到最优的结果。

数据集配置

如果使用自己的数据集,请修改:
DATASET_DIR =  './dataset/'

6.3 模型部署

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

  1. 模型替换:将生成的模型文件model.cc替换micro_speech/micro_features/model.cc文件
  2. 标签更新:在cc替换micro_speech/micro_features/micro_model_settings.cc中,将标签名更换为你训练的快捷词,比如“打开”、“关闭”。由于标签与模型的输出张量元素是按照顺序进行匹配的,因此,需要按照将标签提供给训练脚本的顺序列出这些标签。
  3. 业务逻辑更新:在micro_speech/command_responder.cc中根据标签更新相应的业务逻辑。目前在听到“打开”后,会打开HaaS EDU K1上R/G/B LED灯。你也可以修改逻辑比如通过WiFi打开远程的风扇或灯。这里可以充分发挥你的想象力打造一些比较有意思的场景应用。

7. 总结

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

 

开发者支持

如需更多技术支持,可加入钉钉开发者群,或者关注微信公众号。

更多技术与解决方案介绍,请访问HaaS官方网站https://haas.iot.aliyun.com

 

以上是关于基于TensorFlow Lite Micro在物联网设备上玩转TinyML之离线语音唤醒的主要内容,如果未能解决你的问题,请参考以下文章

基于Zephyr在微型MCU上使用Tensor Flow Lite Micro做图像分类

在 Cadence 音频数字信号处理器上加速 TensorFlow Lite Micro

如何在基于 tensorflow lite 对象检测 android 的应用程序中添加文本转语音?

专访 | 基于LSTM与TensorFlow Lite,kika输入法是如何造就的

TensorFlow Lite - 对象检测 API YOLOv3

将 Keras 模型转换为 TensorFlow lite - 如何避免不支持的操作?