关键词检测(KWS)

概述

在前面的 图像分类目标检测 应用中,我们已经体验了 Nicla Vision 板卡的强大能力。现在,我们将目光转向语音激活应用,实践一个关键词检测(KWS)项目。

正如 音频分类特征工程 实践教程中介绍的,关键词检测(KWS)已集成到许多语音识别系统中,使设备能够响应特定词语或短语。这项技术不仅支撑了 Google Assistant、Amazon Alexa 等流行设备,也完全可以在小型、低功耗设备上实现。本教程将带你在配备数字麦克风的 Nicla Vision 开发板上,利用 TinyML 实现 KWS 系统。

我们的模型将识别可唤醒设备或触发特定动作的关键词,实现真正的语音激活控制。

语音助手是如何工作的?

如前所述,市面上的语音助手(如 Google Home、Amazon Echo-Dot)只有在被特定关键词唤醒时才会响应用户,比如前者的“Hey Google”和后者的“Alexa”。

图 1: Google 语音助手唤醒示意图
图 1: Google 语音助手唤醒示意图

换句话说,语音命令的识别基于多级模型或级联检测(Cascade Detection)。

图 2: 语音助手级联检测流程图
图 2: 语音助手级联检测流程图

第一阶段:Echo Dot 或 Google Home 内部的小型微处理器持续监听,等待关键词出现,使用 TinyML 模型在本地进行检测(KWS 应用)。

第二阶段:只有当第一阶段的 KWS 应用检测到关键词后,数据才会被发送到云端,由更大的模型进一步处理。

下方视频展示了在树莓派(第二阶段)上编程 Google Assistant,并用 Arduino Nano 33 BLE 作为 TinyML 设备(第一阶段)的例子。

https://youtu.be/e_OPgcnsyvM

想深入了解上述 Google Assistant 项目,请参考教程: 从零构建智能语音助手

本 KWS 项目聚焦于第一阶段(KWS 或关键词检测),我们将使用带数字麦克风的 Nicla Vision 进行关键词检测。

KWS 实践项目

下图展示了最终 KWS 应用在推理阶段的工作流程:

图 3: KWS 应用推理流程图
图 3: KWS 应用推理流程图

我们的 KWS 应用将识别四类声音:

  • YES(关键词 1)
  • NO(关键词 2)
  • NOISE(仅有背景噪声,无语音)
  • UNKNOWN(除 YES 和 NO 外的其他词)

在实际项目中,建议除了关键词外,还包含“噪声”(或背景)和“未知”类别。

机器学习工作流

KWS 应用的核心是其模型。因此,我们需要用特定关键词、噪声和其他词(“未知”)来训练模型:

图 4: KWS 项目训练流程图
图 4: KWS 项目训练流程图

数据集

任何机器学习流程的关键在于数据集。确定关键词后(本例为 YESNO),我们可以利用 Pete Warden 开发的数据集 “Speech Commands: A Dataset for Limited-Vocabulary Speech Recognition” 。该数据集包含 35 个关键词(每类 1000+ 样本),如 yes、no、stop、go 等。以 yesno 为例,每类可获得约 1500 个样本。

你可以从 Edge Studio 下载该数据集的精简版( 关键词检测预置数据集 ),其中包含本项目所需的四类样本:yes、no、noise、background。操作步骤如下:

上传数据集到 Edge Impulse Studio

在 Edge Impulse Studio(EIS)新建项目,在 Data Acquisition 区域选择 Upload Existing Data 工具,选择要上传的文件:

图 5: 数据集文件上传界面
图 5: 数据集文件上传界面

定义标签,选择 Automatically split between train and test,点击 Upload data 上传到 EIS。对所有类别重复此操作。

图 6: 数据集上传标签界面
图 6: 数据集上传标签界面

上传后,数据集会出现在 Data acquisition 区域。约 6000 个样本(每类 1500 个),自动分为训练集(4800)和测试集(1200)。

图 7: 数据集分布界面
图 7: 数据集分布界面

采集额外音频数据

尽管 Pete 的数据集已很丰富,建议还是采集一些自己的语音样本。对于加速度计等传感器,建议用同类型设备采集数据。对于声音,这不是必须,因为我们实际分类的是音频数据。

声音与音频的关键区别在于能量类型。声音是介质中的机械扰动(纵波),引起压力变化;音频则是代表声音的电信号(模拟或数字)。

当我们发出关键词时,声波需被麦克风采样为音频数据,采样率为 16 KHz,采样位宽为 16 位。

只要设备能生成 16 KHz/16 位音频数据即可,包括 NiclaV、电脑或手机。

图 8: 音频采集设备示意图
图 8: 音频采集设备示意图

使用 NiclaV 和 Edge Impulse Studio

Nicla Vision 设置 章节所述,EIS 官方支持 Nicla Vision,便于采集其传感器(包括麦克风)数据。请新建 EIS 项目并连接 Nicla,步骤如下:

  • 下载最新版 EIS 固件 并解压
  • 在电脑上打开 zip 文件,选择对应操作系统的上传工具
图 9: 固件上传工具界面
图 9: 固件上传工具界面
  • 按两次复位键,将 NiclaV 置于 Boot 模式
  • 运行对应批处理脚本,将 arduino-nicla-vision.bin 上传到开发板

进入 EIS 项目,在 Data Acquisition 标签页选择 WebUSB,弹窗中选择 Nicla is paired 并点击 [Connect]

Collect Data 区域选择 Built-in microphone,设置 label(如 yes)、采样频率 [16000Hz]、采样时长(如 [10s]),点击 Start sampling

图 10: EIS 数据采集界面
图 10: EIS 数据采集界面

Pete 数据集样本长度为 1s,录制样本为 10s,需拆分为 1s 样本。点击样本名后的“三点”,选择 Split sample

弹出分割工具窗口:

图 11: 样本分割工具界面
图 11: 样本分割工具界面

在工具中将数据分割为 1 秒(1000 ms)片段,如有需要可增删片段。对所有新样本重复此操作。

使用手机和 EI Studio

也可用电脑或手机采集音频,采样率 16 KHz,位宽 16 位。

Devices 页面扫码,手机浏览器打开数据采集应用,选择 Collecting Audio,设置 Label、采集时长和 Category

图 12: 手机音频采集界面
图 12: 手机音频采集界面

操作流程与 NiclaV 类似。

任何支持 16 KHz/16 位采样的录音软件(如 Audacity )均可用于音频采集。

创建 Impulse(预处理与模型定义)

Impulse 指将原始数据通过信号处理提取特征,再用学习模块对新数据进行分类的流程。

Impulse 设计

图 13: Impulse 设计界面
图 13: Impulse 设计界面

首先,以 1 秒窗口采集数据,进行数据增强,并以 500 ms 步长滑动窗口。注意启用 zero-pad 选项,确保不足 1 秒的样本用零填充,避免噪声和异常。

每个 1 秒音频样本需预处理并转换为图像(如 $13\times 49\times 1$)。如 音频分类特征工程 实践所述,我们采用 Audio (MFCC),即用 Mel 频率倒谱系数 提取音频特征,适合语音场景。

接着选择 Classification 模块,基于卷积神经网络(CNN)从零构建模型。

也可选用 Transfer Learning (Keyword Spotting) 模块,微调预训练 KWS 模型,适合小型关键词数据集。

预处理(MFCC)

下一步是生成用于训练的特征:

可保留默认参数,也可用 DSP 的 Autotune parameters 自动调参。

图 14: MFCC 参数自动调优界面
图 14: MFCC 参数自动调优界面

将 1 秒、16 KHz 采样的原始音频(16,000 点)用 MFCC 处理,得到 637 个特征($13\times 49$)。

图 15: MFCC 特征处理结果
图 15: MFCC 特征处理结果

结果显示,预处理仅占用 16 KB 内存,延迟 34 ms,非常优秀。例如在 Arduino Nano(Cortex-M4f @ 64 MHz)上,预处理需约 480 ms。参数如 FFT length [512] 会显著影响延迟。

保存参数后,进入 Generated features 标签页,实际生成特征。用 UMAP 进行降维,Feature explorer 可视化特征分布。

图 16: 特征可视化界面
图 16: 特征可视化界面

结果显示 yes(红色)与 no(蓝色)特征分布明显分离,unknown 更靠近 no,提示 no 更易出现误判。

深入理解

想深入了解原始声音的预处理,可参考 音频分类特征工程 章节。可下载本 notebook 在 Colab 打开 体验 MFCC 特征生成。

模型设计与训练

我们采用简单的卷积神经网络(CNN)模型,分别测试 1D 和 2D 卷积。基本结构为两组卷积 + 池化(分别为 8 和 16 个滤波器),1D dropout 为 0.25,2D 为 0.5。最后一层 Flatten 后有 4 个神经元,对应四个类别:

图 17: 1D/2D CNN 结构对比图
图 17: 1D/2D CNN 结构对比图

超参数设置:学习率 0.005,训练 100 轮,采用 SpecAugment 数据增强。1D 和 2D 架构同参对比,1D 准确率更高(90.5% vs 88%),因此采用 1D。

图 18: 训练结果与混淆矩阵
图 18: 训练结果与混淆矩阵

1D 卷积参数更少,更适合资源受限环境。

1D 混淆矩阵显示,YES 的 F1 分数为 95%,NO 为 91%。这与特征可视化结果一致(no 与 unknown 距离近)。如需提升效果,可分析错误样本。

图 19: 训练错误样本分析
图 19: 训练错误样本分析

可试听错误样本,如 YES 多为发音类似“yeh”。可采集更多样本后重新训练。

深入理解

想了解底层细节,可在 Dashboard 标签页下载预处理数据集(MFCC training data),并用本 Jupyter Notebook Colab 版本 分析训练过程,如逐轮准确率:

图 20: 训练过程可视化
图 20: 训练过程可视化

测试

用预留测试集测试模型,准确率约 76%。

图 21: 测试结果界面
图 21: 测试结果界面

F1 分数显示 YES 达 0.90,非常理想,NO 为 0.72,UNKNOWN 为 0.70。可将误判样本加入训练集后再次训练以提升效果。

实时分类

可进入下一步,也可用 NiclaV 或手机采集实时样本,测试模型部署前的效果。

部署与推理

EIS 会打包所需库、预处理函数和训练模型,下载到本地。在 Deployment 区域选择 Arduino Library,底部选择 Quantized (Int8),点击 Build

图 22: 部署界面
图 22: 部署界面

点击 Build 后会下载 zip 文件。在 Arduino IDE 的 Sketch 菜单选择 Add .ZIP Library,导入下载的 zip 文件:

图 23: Arduino IDE 安装库界面
图 23: Arduino IDE 安装库界面

现在可以完全脱离 EIS 进行推理测试。使用部署库自带的 NiclaV 示例代码。

在 Arduino IDE 的 File/Examples 菜单,找到项目,选择 nicla-vision/nicla-vision_microphone(或 nicla-vision_microphone_continuous

图 24: 代码示例选择界面
图 24: 代码示例选择界面

按两次复位键进入 boot 模式,上传代码,进行实际推理测试:

图 25: YES/NO 推理结果界面
图 25: YES/NO 推理结果界面

后处理

模型已能检测关键词,接下来修改代码,实现 NiclaV 脱机(电池、充电宝或独立 5V 供电)时的 LED 指示:

  • 检测到 YES,点亮绿灯
  • 检测到 NO,点亮红灯
  • 检测到 UNKNOWN,点亮蓝灯
  • 检测到噪声(无关键词),所有灯灭

nicla-vision_microphone_continuous 示例为例,初始化 LED:

// ...existing code...
void setup()
{
    // ...existing code...
    // Arduino NiclaV 板载 RGB LED 引脚
    pinMode(LEDR, OUTPUT);
    pinMode(LEDG, OUTPUT);
    pinMode(LEDB, OUTPUT);

    // 默认关闭所有 LED(低电平点亮,高电平熄灭)
    digitalWrite(LEDR, HIGH);
    digitalWrite(LEDG, HIGH);
    digitalWrite(LEDB, HIGH);
    // ...existing code...
}

添加 turn_off_leds() 函数关闭所有 LED:

/*
 * @brief      turn_off_leds - 关闭所有 RGB LED
 */
void turn_off_leds(){
    digitalWrite(LEDR, HIGH);
    digitalWrite(LEDG, HIGH);
    digitalWrite(LEDB, HIGH);
}

添加 turn_on_leds() 按分类结果点亮对应 LED:

/*
 * @brief     turn_on_leds - 按分类结果点亮 RGB LED
 * @param[in] pred_index
 *            no:       [0] ==> 红灯亮
 *            noise:    [1] ==> 全灭
 *            unknown:  [2] ==> 蓝灯亮
 *            yes:      [3] ==> 绿灯亮
 */
void turn_on_leds(int pred_index) {
  switch (pred_index)
  {
    case 0:
      turn_off_leds();
      digitalWrite(LEDR, LOW);
      break;
    case 1:
      turn_off_leds();
      break;
    case 2:
      turn_off_leds();
      digitalWrite(LEDB, LOW);
      break;
    case 3:
      turn_off_leds();
      digitalWrite(LEDG, LOW);
      break;
  }
}

loop()// print the predictions 部分修改如下:

// ...existing code...
if (++print_results >= (EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW)) {
    // 打印推理结果
    ei_printf("Predictions ");
    ei_printf("(DSP: %d ms., Classification: %d ms., Anomaly: %d ms.)",
        result.timing.dsp, result.timing.classification, result.timing.anomaly);
    ei_printf(": \n");
    int pred_index = 0;
    float pred_value = 0;
    for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
        if (result.classification[ix].value > pred_value){
            pred_index = ix;
            pred_value = result.classification[ix].value;
        }
    }
    ei_printf("  PREDICTION: ==> %s with probability %.2f\n",
              result.classification[pred_index].label, pred_value);
    turn_on_leds(pred_index);

#if EI_CLASSIFIER_HAS_ANOMALY == 1
    ei_printf("    anomaly score: ");
    ei_printf_float(result.anomaly);
    ei_printf("\n");
#endif

    print_results = 0;
}
// ...existing code...

完整代码见 项目 GitHub

上传代码,实际测试推理效果。检测到 YES 时绿灯亮,NO 时红灯亮,其他词蓝灯亮,噪声时全灭。类似方式也可用于触发外部设备执行动作。

https://youtu.be/25Rd76OTXLY

总结

本教程相关 notebook 和代码可在 GitHub 获取。

最后补充,声音分类不仅限于语音,还可用于以下 TinyML 场景:

  • 安防(破玻璃、枪声检测)
  • 工业(异常检测)
  • 医疗(鼾声、咳嗽、肺部疾病)
  • 自然(蜂群监控、昆虫声音、反偷猎)

参考资源

文章导航

章节内容

这是章节的内容页面。

章节概览

评论区