学了半个月终於要端出「爆浆濑尿虾牛丸」了吗?要开始让大家体会一下牛肉(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 IDE和BLE Sense将备妥的模型完整运行并推论起来。没看过前面章节的朋友,麻烦先补一下课并把Arduino IDE工作环境建好,以免後面看的一头雾水。接下来我们就顺着TFLM官网介绍的微语音(micro_speech)范例来说明如何把这两样素材结合在一起,完成一个类似智能音箱的语言命令的动作,说「YES」及「NO」来控制绿色和红色LED点亮的功能。(如图Fig. 16-1所示)
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 = µ_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。其它的就留给大家自行研究了。
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点亮,控制时要特别注意。目前模型推论後共会产生四种结果及灯号。
/*
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)
本机环境 OS: Windows 10 原始码编辑工具: Visual Studio Code (後...
CLA以及bit乘法 tags: IT铁人 例题答案 就简单把答案打出来罗~ 小心转换成二位元不要粗...
大家好,希望大家今天是快乐的o(≧∀≦)o不开心的话就来看看我的文章吧~ 今天要讲的是监听器watc...
如今,YouTube、TikTok、Dailymotion 等在线网站越来越受欢迎,并吸引了全球大量...
阮毋是喜爱虚华,阮只是环境来拖磨; 人客若叫阮,风雨嘛着行,为伊唱出留恋的情歌。 -- 流浪到淡水...