[Day 16] TFLM + BLE Sense + MP34DT05 就成了迷你智慧音箱(上)

学了半个月终於要端出「爆浆濑尿虾牛丸」了吗?要开始让大家体会一下牛肉(MCU)的鲜、濑尿虾(AI)的甜,及掺在一起比老鼠斑有过之而无不及的味道了吗?各位施主,这又是一个介於是和不是的问题了,只有耐心看完才能感受这其中的酸甜苦辣。先说结论,这个章节确实要帮大家介绍如何整合TensorFlow Lite Microcontroller(以下简称TFLM)加Arduino IDE加Arduino Nano 33 BLE Sense开发板(以下简称BLE Sense)及板上微机电麦克风MP34DT05-A来运行一个智能音箱常见的基本语音命令辨识及控制的micro_speech范例。但看完这个章节猜想你有极大可能会想要直接弃番不再追下去。不过请不要冲动,不经一番寒彻骨焉得梅花扑鼻香,後面章节还有更好的解决方案,今天就当先学个概念,後面就会操作的更得心应手了。

[Day 11] 让tinyML听见你的呼唤已初步介绍过如何透过BLE Sense收集到板子上的微机电麦克风MP34DT05-A所接收到的声音讯号。在[Day 12][Day 13]「tinyML开发框架(一):TensorFlow Lite Micro初体验」中,已分别帮大家介绍如何在TFLM上建立、训练、优化模型、准备好MCU所需格式模型参数档案,及如何使用Arduino IDEBLE Sense将备妥的模型完整运行并推论起来。没看过前面章节的朋友,麻烦先补一下课并把Arduino IDE工作环境建好,以免後面看的一头雾水。接下来我们就顺着TFLM官网介绍的微语音(micro_speech)范例来说明如何把这两样素材结合在一起,完成一个类似智能音箱的语言命令的动作,说「YES」及「NO」来控制绿色和红色LED点亮的功能。(如图Fig. 16-1所示)

TensorFlow Lite Microcontroller提供之micro_speech声音辨识范例运行结果
Fig. 16-1 TensorFlow Lite Microcontroller提供之micro_speech声音辨识范例运行结果。(图片来源

要运行的这个范例不难,假设你已照着[Day 13] tinyML开发框架(一):TensorFlow Lite Micro初体验(下) 图13-2把Arduino_TensorFlowLite函式库安装好了,此时只要新增一个micro_speech范例(如下程序所示),经过编译、上传,接着开启序列埠监控视窗,对着BLE Sense板子说出「YES」或「NO」,就能看到对应的绿色、红色LED点亮,同时也会於监控视窗上看到对应字串。如果无法顺利辨识就把板子靠近嘴巴一点,或者多说几次来改善辨识结果。

/* 
  Arduino_TensorFlowLite micro_speech.ino 范例程序  
  在Arduino IDE安装好Arduino_TensorFlowLite函式库後,
  点击主选单[档案]-[范例]-[Arduino_TensorFlowLite]-[micro_speech]产生。
*/

// 导入相关函式库头文件
#include <TensorFlowLite.h> // 导入TensorFlowLite函式库头文件
#include "main_functions.h" 
#include "audio_provider.h"
#include "command_responder.h"
#include "feature_provider.h"
#include "micro_features_micro_model_settings.h"
#include "micro_features_model.h"
#include "recognize_commands.h"
#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"

// 宣告全域变数及命名空间,初始化相关指标及变数,方便後面其它子程序引用
namespace {
tflite::ErrorReporter* error_reporter = nullptr;
const tflite::Model* model = nullptr;
tflite::MicroInterpreter* interpreter = nullptr;
TfLiteTensor* model_input = nullptr;
FeatureProvider* feature_provider = nullptr;
RecognizeCommands* recognizer = nullptr;
int32_t previous_time = 0;

// 配置记忆体空间,包含声音输入、模型参数等
constexpr int kTensorArenaSize = 10 * 1024;
uint8_t tensor_arena[kTensorArenaSize];
int8_t feature_buffer[kFeatureElementCount];
int8_t* model_input_buffer = nullptr;
}  

// 设定脚位用途及模组初始化(只在电源启动或重置时执行一次)
void setup() {
  // 建立除错日志
  static tflite::MicroErrorReporter micro_error_reporter;
  error_reporter = &micro_error_reporter;

  // 载入模型
  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;
  }

  // 不使用全操作解析器,改使用微操作解析器只加入模型有用到的元素
  // 目前只加入DepthwiseConv2D, FullyConnected, Softmax, Reshape
  // 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;
  }

  // 建立直译器以执行模型相关动作
  static tflite::MicroInterpreter static_interpreter(
      model, micro_op_resolver, tensor_arena, kTensorArenaSize, error_reporter);
  interpreter = &static_interpreter;

  // 配置张量所需记忆体
  TfLiteStatus allocate_status = interpreter->AllocateTensors();
  if (allocate_status != kTfLiteOk) {
    TF_LITE_REPORT_ERROR(error_reporter, "AllocateTensors() failed");
    return;
  }

  // 建立输出入
  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; // 宣告输入缓冲区为INT8资料型态

  // 建立特徵提供器及相关参数
  // 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; // 清除前次计时
}

// 设定无穷循环程序 (会一直依序重覆执行)
void loop() {
  // 撷取目前时间的声音图谱
  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);
  if (feature_status != kTfLiteOk) {
    TF_LITE_REPORT_ERROR(error_reporter, "Feature generation failed");
    return;
  }
  previous_time = current_time;

  // 假设没有取得任何声音就结束此次动作
  if (how_many_new_slices == 0) {
    return;
  }

  // 复制声音资料到模型输入端点
  for (int i = 0; i < kFeatureElementCount; i++) {
    model_input_buffer[i] = feature_buffer[i];
  }

  // 运行推理
  TfLiteStatus invoke_status = interpreter->Invoke();
  if (invoke_status != kTfLiteOk) {
    TF_LITE_REPORT_ERROR(error_reporter, "Invoke failed");
    return;
  }

  // 取得输出值的指标
  TfLiteTensor* output = interpreter->output(0);

  // 经由辨识结果决定输出命令字串及LED亮灯变化
  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;
  }

  // 透过序列埠回报到电脑,包含目前时间、辨识命令结果、置信分数及是否为新命令等
  RespondToCommand(error_reporter, current_time, found_command, score,
                   is_new_command);
}

但是…,事情真的有这麽简单和顺利吗?为什麽板子老是听错或听不懂「YES」、「NO」?我要怎麽才能把「YES」、「NO」改成中文的「开」、「关」或「红」、「绿」甚至是「电灯亮」「电灯灭」这样的短词呢?我不只要改变LED颜色,可以不可改成推动电气设备开关呢?这可能就要从头说起了,包括下列几大步骤。

  • 范例程序组成
  • 产生资料集
  • 训练及优化模型
  • 布署、推理及输出

不想深入了解的朋友,想让生活多点乐趣的朋友,可以先跳过下面的部份,等後面章节再来学习相同的功能但更轻松的作法。

范例程序组成

从图Fig. 16-2就可看出,这个范例不是一个ino档(主程序)就能运作的,还要有另外的22个档案才能将这个范例完整执行起来,虽然每个程序的份量不是很多,但若想深人了解的朋友可能也要花上不少时间。不过我猜想大家不一定想全盘了解,而只是关心训练好的模型怎麽放进MCU及如何控制实体家电而不只是板子上的LED。所以这里仅先点出除了主程序外另二个重点程序,arduino_command_responder.cpp及micro_features_model.cpp。其它的就留给大家自行研究了。

Arduino TensorFlowLite micro_speech范例程序组成
Fig. 16-2 Arduino TensorFlowLite micro_speech范例程序组成。(OmniXRI整理制作, 2021/9/30)

首先介绍arduino_command_responder.cpp,这段程序负责回应语音辨识的结果,即输出结果字串和控制LED点灭。在BLE Sense板子上有一个橘色LED(LED_BUILDIN)和一个RGB三色LED(LEDR, LEDG, LEDB)。但其点亮方式刚好相反,前者是给HIGH点亮,而後者则是给LOW点亮,控制时要特别注意。目前模型推论後共会产生四种结果及灯号。

  • 没有声音(Silence)或背景音。三色LED全灭,橘色LED交替闪烁。
  • 不明(unknow),对应'u'命令,点亮蓝色LED(LEDB)。
  • Yes,对应'y'命令,点亮绿色LED(LEDG)。
  • No,对应'n'命令,点亮红色LED(LEDR)。
/* 
  arduino_command_responder.cpp
  语音(命令)辨识结果输出回应(控制)程序
  将辨识结果字串从序列埠传回电脑。
  若没有声音则会反转橘色LED(LED_BUILDIN),若非「YES」「NO」则亮蓝色LED(LEDB),
  若有侦测到「YES」「NO」则点亮对应的绿色(LEDG)和红色LED(LEDR)。
*/
#include "Arduino.h"

void RespondToCommand(tflite::ErrorReporter* error_reporter,
                      int32_t current_time, const char* found_command,
                      uint8_t score, bool is_new_command) {
  static bool is_initialized = false; // 是否已初始化旗标
  
  // 若尚未初始化
  if (!is_initialized) { 
    // 设定四个LED脚位为输出
    pinMode(LED_BUILTIN, OUTPUT);
    pinMode(LEDR, OUTPUT);
    pinMode(LEDG, OUTPUT);
    pinMode(LEDB, OUTPUT);

    // 使其初始皆为不亮状态
    digitalWrite(LEDR, HIGH);
    digitalWrite(LEDG, HIGH);
    digitalWrite(LEDB, HIGH);
    
    // 设初始化旗标为真
    is_initialized = true; 
  }
  
  static int32_t last_command_time = 0; // 最後一次命令时间
  static int count = 0; // 计数器
  static int certainty = 220; // 确定值

  // 若为新命令
  if (is_new_command) {
    // 输出结果字串到序列埠,包含命令结果、置信分数和目前时间。
    TF_LITE_REPORT_ERROR(error_reporter, "Heard %s (%d) @%dms", found_command,
                         score, current_time);
                         
    // 若为Yes命令则点亮绿色LED
    if (found_command[0] == 'y') {
      last_command_time = current_time; // 记录最後时间
      digitalWrite(LEDG, LOW); // 点亮绿色LED
    }
    
    // 若为No命令则点亮红色LED
    if (found_command[0] == 'n') {
      last_command_time = current_time; // 记录最後时间
      digitalWrite(LEDR, LOW); // 点亮红色LED
    }

    // 若为不明(unknow)命令则点亮蓝色LED
    if (found_command[0] == 'u') {
      last_command_time = current_time; // 记录最後时间
      digitalWrite(LEDB, LOW); // 点亮蓝色LED
    }
  }

  // 若最後时间不为零且超过3秒,则熄灭所有LED
  if (last_command_time != 0) {
    if (last_command_time < (current_time - 3000)) {
      last_command_time = 0;
      digitalWrite(LED_BUILTIN, LOW);
      digitalWrite(LEDR, HIGH);
      digitalWrite(LEDG, HIGH);
      digitalWrite(LEDB, HIGH);
    }
    
    // 若时间未超过3秒则返回
    return;
  }

  // 若为没有声音(或背景音)则计数器加1,橘色LED反转。
  ++count; // 计数器加1
  
  if (count & 1) {
    digitalWrite(LED_BUILTIN, HIGH); // 点亮橘色LED
  } else {
    digitalWrite(LED_BUILTIN, LOW); // 熄灭橘色LED
  }
}

解释完如何将辨识结果和LED输出结果的关连後,相信大家很容易就可联想到,只要使用板子上其它通用输出入接点(GPIO)连接到继电器,再接上一般110V的家电(只有开和关的那种),就可以控制其电源开启和关闭了。

在这里补充一个使用状况,由於板上的麦克风还满灵敏的,所以当环境中有像电风扇或背景杂音较大的情况下,会发现蓝色LED一直亮着,表示有一直有收到一段声音,但听不懂是「YES」或「NO」,此时建议到比较安静的环境再测试,效果会好些。

另外关於容易听错、想换其它语音控制命令,这和资料集产生方式及模型结构、训练、调参、转换、优化很有关联,当然完成後如何布署更是我们关心的重点,这些就留待下回分解。

参考连结

TensorFlow Lite Microcontroller - Micro Speech 微语音辨识


<<:  Progressive Web App 存取通讯录联络人 (18)

>>:  Day 17 免费个资隐私盘点工具

Day02 -本机环境准备,安装Python

本机环境 OS: Windows 10 原始码编辑工具: Visual Studio Code (後...

Day-4 CLA以及bit乘法

CLA以及bit乘法 tags: IT铁人 例题答案 就简单把答案打出来罗~ 小心转换成二位元不要粗...

[Day 20] 看看看的监听器watch

大家好,希望大家今天是快乐的o(≧∀≦)o不开心的话就来看看我的文章吧~ 今天要讲的是监听器watc...

从链接/URL 下载在线影片的 5 种最佳方式

如今,YouTube、TikTok、Dailymotion 等在线网站越来越受欢迎,并吸引了全球大量...

予焦啦!结论与展望(一):Hoddarla 专案的过去、现在与未来

阮毋是喜爱虚华,阮只是环境来拖磨; 人客若叫阮,风雨嘛着行,为伊唱出留恋的情歌。 -- 流浪到淡水...