Skip to main content

Wio Terminal Tensorflow Lite Micro 在MCU上的语音识别 – 语音转意图

传统的使用语音进行设备控制/用户请求处理的方法是,首先将语音转录为文本,然后将文本解析为合适格式的命令/查询。虽然这种方法在词汇量和/或应用场景方面提供了很大的灵活性,但语音识别模型和专用解析器的组合不适合微控制器的受限资源。

来源:Wio Terminal、Picovoice、Tensorflow Lite

在这个项目中,我们将采用一种更高效的方法,直接将用户话语解析为意图/槽位形式的可操作输出。我们将分享训练特定领域语音转意图模型的技术,并将其部署到基于Cortex M4F的开发板上,该开发板内置麦克风,即来自Seeed Studio的Wio Terminal。

更多详细信息和视觉效果,请观看相应的视频!

有不同类型的语音识别任务——我们可以大致将它们分为三组:

  • 大词汇量连续语音识别(LVCSR)
  • 关键词检测
  • 语音转意图

关键词检测在微控制器上运行良好,使用各种可用的无代码开源工具(如Edge Impulse)相当容易训练,但无法很好地处理大词汇量。

如果我们希望设备基于语音输入执行有用的操作,我们需要结合LVCSR模型和基于文本的自然语言解析器——这种方法是稳健的,并且由于公开可用的ASR引擎丰富,实现起来相对容易,但即使在单板计算机上运行也不合适,更不用说微控制器了。

还有第三种方法,基于特定领域词汇直接将语音转换为解析的意图。让我们以智能洗衣机或智能灯为例。语音转意图模型在处理话语"Normal cycle with low-spin"时会输出解析的意图,例如

{ Intent: washClothes },
{ Slots: cycle: normal,
spin: low,
water: default }

这确实是我们能够用语音控制所述智能洗衣机所需要的全部。

语音转意图在研究中有很好的代表性,但缺乏适用于微控制器的广泛可用的开源实现。 生产就绪,非开源:

  • Picovoice
  • Fluent.ai

生产就绪,FOSS,不适用于微控制器:

  • Speechbrain.io

对于模型训练,您可以使用我们准备的Jupyter Notebook或来自Github仓库的训练脚本(在文章末尾的参考部分找到它们)。Jupyter Notebook包含一个非常基本的参考模型实现,并且对每个单元格都有解释。

模型训练完成后,将其复制到包含Wio Terminal代码的文件夹中,并在第106行中将模型名称更改为您的模型名称。让我们回顾代码中最重要的部分。它可以大致分为三个部分:

  • 音频采集
  • MFCC计算
  • 在MFCC特征上进行推理

音频采集​

为了使用 Wio Terminal 内置麦克风录制声音进行处理,我们使用 Cortex M4F MCU 的 DMA ADC 功能。DMA 代表直接内存访问,正如其名称所示——MCU 的一个特定部分称为 DMAC 或直接内存访问控制器,预先设置用于将数据从一个位置(例如内部内存、SPI、I2C、ADC 或其他接口)"传输"到另一个位置。这样,传输可以在 MCU 很少参与的情况下进行,除了初始设置。我们在这里设置传输的源和目标

descriptor.descaddr = (uint32_t)&descriptor_section[1]; // Set up a circular descriptor
descriptor.srcaddr = (uint32_t)&ADC1->RESULT.reg; // Take the result from the ADC0 RESULT register
descriptor.dstaddr = (uint32_t)adc_buf_0 + sizeof(uint16_t) * ADC_BUF_LEN; // Place it in the adc_buf_0 array
descriptor.btcnt = ADC_BUF_LEN; // Beat count
descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD | // Beat size is HWORD (16-bits)
DMAC_BTCTRL_DSTINC | // Increment the destination address
DMAC_BTCTRL_VALID | // Descriptor is valid
DMAC_BTCTRL_BLOCKACT_SUSPEND; // Suspend DMAC channel 0 after block transfer
memcpy(&descriptor_section[0], &descriptor, sizeof(descriptor)); // Copy the descriptor to the descriptor section
descriptor.descaddr = (uint32_t)&descriptor_section[0]; // Set up a circular descriptor
descriptor.srcaddr = (uint32_t)&ADC1->RESULT.reg; // Take the result from the ADC0 RESULT register
descriptor.dstaddr = (uint32_t)adc_buf_1 + sizeof(uint16_t) * ADC_BUF_LEN; // Place it in the adc_buf_1 array
descriptor.btcnt = ADC_BUF_LEN; // Beat count
descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD | // Beat size is HWORD (16-bits)
DMAC_BTCTRL_DSTINC | // Increment the destination address
DMAC_BTCTRL_VALID | // Descriptor is valid
DMAC_BTCTRL_BLOCKACT_SUSPEND; // Suspend DMAC channel 0 after block transfer
memcpy(&descriptor_section[1], &descriptor, sizeof(descriptor)); // Copy the descriptor to the descriptor section

正如我们在 DMA 描述符中使用参数 DMAC_BTCTRL_BLOCKACT_SUSPEND; 指定的那样,DMA 通道应在完整的块传输后被挂起。然后我们继续设置一个由 TC5 定时器触发的 ISR(中断服务例程):

 // Configure Timer/Counter 5
GCLK->PCHCTRL[TC5_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | // Enable perhipheral channel for TC5
GCLK_PCHCTRL_GEN_GCLK1; // Connect generic clock 0 at 48MHz
TC5->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ; // Set TC5 to Match Frequency(MFRQ) mode
TC5->COUNT16.CC[0].reg = 3000 - 1; // Set the trigger to 16 kHz: (4Mhz / 16000) - 1
while (TC5->COUNT16.SYNCBUSY.bit.CC0); // Wait for synchronization
// Start Timer/Counter 5
TC5->COUNT16.CTRLA.bit.ENABLE = 1; // Enable the TC5 timer
while (TC5->COUNT16.SYNCBUSY.bit.ENABLE); // Wait for synchronization

ISR 将在由 TC5 定时器控制的等间隔时间调用特定函数。让我们看看那个函数。

/**
* Interrupt Service Routine (ISR) for DMAC 1
*/
void DMAC_1_Handler() {

static uint8_t count = 0;

// Check if DMAC channel 1 has been suspended (SUSP)
if (DMAC->Channel[1].CHINTFLAG.bit.SUSP) {

// Debug: make pin high before copying buffer
#ifdef DEBUG
digitalWrite(debug_pin, HIGH);
#endif

// Restart DMAC on channel 1 and clear SUSP interrupt flag
DMAC->Channel[1].CHCTRLB.reg = DMAC_CHCTRLB_CMD_RESUME;
DMAC->Channel[1].CHINTFLAG.bit.SUSP = 1;

// See which buffer has filled up, and dump results into large buffer
if (count) {
audio_rec_callback(adc_buf_0, ADC_BUF_LEN);
} else {
audio_rec_callback(adc_buf_1, ADC_BUF_LEN);
}

// Flip to next buffer
count = (count + 1) % 2;

// Debug: make pin low after copying buffer
#ifdef DEBUG
digitalWrite(debug_pin, LOW);
#endif
}
}

名为 DMAC1_Handler() 的 ISR 函数检查 DMAC 通道 1 是否被挂起——这在一个信息块完成录制时发生。如果是,它调用用户定义的函数 audio_rec_callback(),在这里我们将填充的 DMA ADC 缓冲区的内容复制到用于计算 MFCC 特征的(可能)更大的缓冲区中。可选地,我们还在此步骤中应用一些声音后处理。

MFCC 计算​

用于匹配 TensorFlow MFCC Op 代码的 MFCC 特征提取借用了 ARM 仓库中用于 ARM 微控制器关键词搜索的代码。您可以在这里找到原始代码。

与 MFCC 特征计算相关的大部分工作都在 MFCC 类的 mfcc_compute(const int16_t audio_data, float mfcc_out) 方法中进行。该方法接收一个指向音频数据的指针,在我们的情况下是 320 个声音数据点,以及一个指向 MFCC 输出值数组中特定位置的指针。对于一个时间片,我们执行以下操作:

将数据标准化到 -1,1 并填充它(在我们的情况下不会发生填充,因为音频数据总是计算一个 MFCC 特征片所需的确切大小):

  //TensorFlow way of normalizing .wav data to (-1,1)
for (i = 0; i < frame_len; i++) {
frame[i] = (float)audio_data[i]/(1<<15);
}
//Fill up remaining with zeros
memset(&frame[frame_len], 0, sizeof(float) * (frame_len_padded-frame_len));

使用 ARM Math 库函数计算 RFTT 或实数快速傅里叶变换:

  //Compute FFT
arm_rfft_fast_f32(rfft, frame, buffer, 0);

将值转换为功率谱:

  //frame is stored as [real0, realN/2-1, real1, im1, real2, im2, ...]
int32_t half_dim = frame_len_padded/2;
float first_energy = buffer[0] * buffer[0],
last_energy = buffer[1] * buffer[1]; // handle this special case
for (i = 1; i < half_dim; i++) {
float real = buffer[i*2], im = buffer[i*2 + 1];
buffer[i] = real*real + im*im;
}
buffer[0] = first_energy;
buffer[half_dim] = last_energy;

然后对上一步保存在缓冲区中的数据的平方根应用 Mel 滤波器组。Mel 滤波器组在 MFCC 类实例化时在 create_mel_fbank() 方法内创建。滤波器组的数量、最小和最大频率由用户预先指定——在训练脚本和推理代码之间保持它们的一致性非常重要,否则会出现显著的精度下降。

  float sqrt_data;
//Apply mel filterbanks
for (bin = 0; bin < NUM_FBANK_BINS; bin++) {
j = 0;
float mel_energy = 0;
int32_t first_index = fbank_filter_first[bin];
int32_t last_index = fbank_filter_last[bin];
for (i = first_index; i <= last_index; i++) {
arm_sqrt_f32(buffer[i],&sqrt_data);
mel_energy += (sqrt_data) * mel_fbank[bin][j++];
}
mel_energies[bin] = mel_energy;

//avoid log of zero
if (mel_energy == 0.0)
mel_energies[bin] = FLT_MIN;
}

最后,我们对 Mel 能量数组进行离散余弦变换并将其写入 MFCC 特征输出数组。在原始脚本中,此步骤也执行了量化,但我选择使用 Tensorflow Lite for Microcontrollers 示例中的量化程序。

对 MFCC 特征进行推理​

一旦一个样本(3 秒)内的所有音频都被处理并转换为 MFCC 特征,我们将整个 MFCC 特征数组从 FLOAT32 转换为 INT8 值并将其输入神经网络。TensorFlow Lite for Microcontrollers 的初始化和推理过程已经在我之前的一篇文章中描述过,所以我不会在这里重复。

在编译草图之前,请确保您已安装所有必要的库,并且 Seeed SAMD 板定义至少是 1.8.2 版本——这对于 TensorFlow Lite 库无错误编译非常重要。编译并上传草图——如果您将 DEBUG 参数设置为 false,代码将立即开始运行,您只需按下 Wio Terminal 顶部的 C 按钮并说出数据集中的一个句子。结果将显示在屏幕上,如果 Wio Terminal 连接到计算机,也会输出到串行监视器。

虽然本课程基于 Wio Terminal,因为它非常适合探索嵌入式机器学习,但绝对可以在其他设备上实现。最简单的方法是将代码移植到其他 Cortex M4F MCU,如 Nano33 BLE Sense——这只需要调整不同的麦克风。移植到其他 ARM MCU 也应该相当简单。

移植到其他架构,例如 ESP32 或 K210 或其他架构,需要重新实现 MFCC 计算,因为它们使用来自 CMSIS-DSP 的 ARM 特定函数。

项目中的基本神经网络架构可以进行多项改进。这些改进包括:

  • 模型预训练
  • seq2seq、LSTM、注意力机制
  • 可训练滤波器
  • AutoML、合成数据

查看这个关于此主题的TinyML演讲,了解更多信息并找到论文链接!

我们鼓励您分叉代码仓库,尝试在自己的数据集上进行训练,也许尝试实现更高级的架构或模型训练技术。如果您这样做,请不要犹豫在这里给我留言或在 Github 上提交 PR!

参考资料​

Loading Comments...