Audio, Deep Learning, Embedded Systems

Deep Learning in Edge – Convolutional Neural Network for Sound Classification on ESP32

Video Demo: https://www.bilibili.com/video/BV13S4y137qU

1 Introduction

Intruduction for Module / Chip

ESP32-S2-MINI-1 and ESP32-S2-MINI-1U are two powerful, generic Wi-Fi MCU modules that have a rich set of peripherals. They are an ideal choice for a wide variety of application scenarios related to Internet of Things (IoT), such as wearable electronics and smart home.

  • Core: Xtensa® single-core 32-bit LX7 CPU, frequency up to 240MHz
  • Memories:
    • 128 KB of ROM
    • 320 KB of SRAM
    • 16 KB of RTCSRAM
    • 4 MB of Flash memory

Please refer to: module datasheet

Introduction for Dev Board

A development board based on the module above provided by EETree is used for this project. The following circuits or features are on this development board:

  • Based on ESP32-S2 WiFi core module;
  • 128*64 OLED display, SPI interface, for message, parameter and waveform displaying;
  • 4 buttons for parameter and mode selection;
  • 1-channel Mic audio input – analog circuit, a range of 0-40dB adjusted by potentiometer, with filter;
  • 1-channel audio input through headphone jack – analog circuit, a range of 0-40dB adjusted by potentiometer, with filter;
  • 2-hannel audio output, with power amplifier, specker and headphones can be driven;
  • A FM reveiver, program selection and volume adjustment by I2C interface;
  • An analog switch to select audio source (ESP32 or FM) to output devices (speaker, headphones)

Please refer to the development board’s offical website: https://www.eetree.cn/project/detail/419

Introduction for the Task

Audio Scene Classification (ASC) is one of the basic tasks in the computer acoustics field. It is expected to classify a piece of acquired sound into the correct environment labels, such as dog bark, raining. This project deployed ASC on the embedded platform ESP32, which benefits our daily life and runs at low power consumption. This post will intruduce the process for this project, including audio acquisition, feature extrachtion, neural network and deployment.

2 Audio Signal Acquisition

观察原理图可以得知,Mic接收到的信号通过运算放大器U9放大,并可以通过RV1调节增益,最终接入ESP32 ADC输入中。因此采集部分根据参考手册和编程手册,使用DIG ADC1以20kHz的采样率来测量引脚的电压,并通过DMA(直接存储器访问)直接复制到内存中而无需CPU处理。本部分驱动程序直接使用了该项目开发好的,不过因为API有更新,我强行使用了已弃用的库函数,因此编译时会有警告产生,日后可以对此部分驱动升级。
https://www.eetree.cn/project/detail/537

3 Feature Extraction

特征提取是对于音频感知来说最重要的一个步骤了。这一部分的最终目标是把一段接近一秒的音频转换为一张30×30的频谱图。接下来会讲解如何生成这个频谱图中的一列数据,

3.1 Window Function

首先我们截取采样到的音频数据中的连续的1×1024个点。这些点后续将会被进行傅里叶变换。在此之前为减少频谱泄露,我们会对这一段数据乘上一个中间大,最大为1,两边小并且逐渐接近0的函数,称为“窗函数”。我是用的窗是常用的汉宁窗(Hanning Window),具体可以参考:https://numpy.org/doc/stable/reference/generated/numpy.hanning.html

3.2 Fast Fourier Transformation (FFT)

接下来对这1024个点作快速傅里叶变换。也就是说原本的信号的横轴是时间,体现了信号在不同时间的强度。变换完之后得到的便是这段时间内音频的能量在各个频率上分布的结果。如图3.1(a)所示,左边红色的信号为变换前的时域信号,右边则为变换后的频域的能量分布。如果您对傅里叶变换不了解,在此强烈推荐该科普视频:
https://www.bilibili.com/video/av19141078

对于实现部分,这里参考了实数傅里叶变换(RFFT)的开源代码:
https://github.com/willhope/Noise-reduction/blob/master/rfft.c

3.3 Mel Spectrogram

研究表明,人类对频率的感知并不是线性的,并且对低频信号的感知要比高频信号敏感。例如,人们可以比较容易地发现500和1000Hz的区别,确很难发现7500和8000Hz的区别。然而,傅里叶变换后的频谱的横轴是线性的。这时,梅尔标度(the Mel Scale)被提出,它是Hz的非线性变换,对于以mel scale为单位的信号,可以做到人们对于相同频率差别的信号的感知能力几乎相同。图3.1(b)展示了人耳对音调的感知和它们实际频率的关系。更多介绍请参考:
https://zhuanlan.zhihu.com/p/351956040

在本项目中,我使用了30组滤波器,因此原本长度1024的数组在变换后的长度为30。实现时参考并移植了STM32官方的音频处理库,包含了特征提取部分的常用功能,包括傅里叶变换、梅尔频谱等。由于ST使用了ARM的DSP来加速浮点运算,在移植到ESP32时还要全部替换成标准的数学函数,对于缺失的函数还要手动添加它的实现,并且最后解决各种编译问题,耗费了一番功夫。ST的音频处理库地址:
https://www.st.com/zh/embedded-software/fp-ai-sensing1.html

值得一提的是,ESP官方也有DSP库,不过由于本人时间关系没能仔细研究。

Fr_O050ZUlYNwVhBWnHdPm3DqTnY
Fig 3.1 (a) Fourier Transformation (b) Mel Filterbanks 

3.4 Program Pipeline

如图3.2所示,要让程序实现上述过程,首先我创建了6个长度为256个uint16的DMA接收缓冲区。中断发生时,把当前时间以前的1024个点的数据取出,并归一化为浮点数,做快速傅里叶变换得到傅里叶频谱。之后再计算得到长度为30的数组,作为梅尔频谱图的一列。每隔512个采样点,计算1024个点得到的梅尔频谱的一列。当有40列结果计算出来后,这些数据便可以用来后面的分类。

FvkRYLR8l-AwQxiJvV5w9svlmPVO
Fig 3.2 Program pipeline for ESP32 audio acquisition and feature extraction

4 Model Training

模型训练在个人电脑中,使用python脚本和Pytorch深度学习框架完成。

4.1 Dataset

我们采用ESC-10数据集作为训练数据。ESC即环境声音分类,ESC10中包含了10个音频分类,每个分类中包含了40个音频片段。数据集下载地址:
https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/YDEPUT

此外常用的数据集还有UrbanSound8k等。下载好后使用上述的特征提取流程对其进行特征提取,得到每个音频片段的频谱图。不过在python环境中使用librosa库,仅需要两行代码便可完成傅里叶变换和梅尔频谱的操作。

4.2 Model Architecture

在特征提取后,音频分类问题就被转换成了图像分类问题。我们使用较简单的CNN卷积神经网络来对其进行分类。由于ESP32平台内存有限并且硬禾使用的是没有2M PSRAM的芯片型号,也就是只有320k的内置SRAM,因此网络的规模不能太大。一般在嵌入式平台中,内存大小限制了网络的规模,而CPU性能决定了网络能跑多快。对于CNN模型结构的可视化,推荐参考以下网站:
https://poloclub.github.io/cnn-explainer/

5 Model Deployment

模型训练好后,可以在ESP32中使用C语言实现相同的CNN模型结构。之后可以从pytorch训练好的模型中导出参数,将这些参数以数组的形式编译进程序中,就实现了模型的部署。ESP官方也推出了更加先进的深度学习库,可以直接转换并部署已经训练好的模型,并提供了一系列工具,不过由于个人时间关系还是手写了自制的模型推理代码。

6 Program Flow

程序由于使用了之前作者的代码作为框架,因此还保留了DAC信号发生器等功能。具体操作为:

  • 按键1:DAC开关(ADC关时有效);ADC数字滤波器参数设置(ADC开时)
  • 按键2:ADC开关(DAC关时有效)
  • 按键3:ADC开时:ADC模式调整(下文详细介绍);DAC开时:DAC音量调整
  • 按键4:DAC频率切换

图6重点介绍了ADC模式时的程序逻辑。当ADC打开后,ADC_Task任务会开始执行并持续响应ADC DMA中断。之后根据ADC的按键选择的4种模式之一来执行对应的代码。

FhcZEv69pJl16AMyd3xXT03i2bns
图6 ADC模式下程序执行流程图

7 Results

最终实验现象为,板子上电运行后,会不断采集音频数据,之后把分类结果和概率展示在oled屏幕上,如图7所示。

FiX-quFhZUHhAKyf0bAXolFLugh8
图7 运行展示

Appendix: Codes

神经网络推理过程代码如下,也体现了神经网络的结构。代码中调用的函数为自行实现的C语言卷积池化等操作的函数。

xSemaphoreTake( xMutexSpec, portMAX_DELAY );
conv2d_relu(&spec, 1, 30, 40, feature1, 10, conv1_1_w, conv1_1_b);
xSemaphoreGive( xMutexSpec );

conv2d_relu(feature1, 10, 28, 38, feature2, 10, conv1_2_w, conv1_2_b);
swap_feature(feature1, feature2);

max_pool2d(feature1, 10, 26, 36, feature2);
swap_feature(feature1, feature2);

conv2d_relu(feature1, 10, 13, 18, feature2, 10, conv2_1_w, conv2_1_b);
swap_feature(feature1, feature2);

conv2d_relu(feature1, 10, 11, 16, feature2, 10, conv2_2_w, conv2_2_b);
swap_feature(feature1, feature2);

max_pool2d(feature1, 10, 9, 14, feature2);
swap_feature(feature1, feature2);

flatten_fc(feature1, 10, 4, 7, out_neurons, 10, fc_w, fc_b);

神经网络推理代码,训练10轮,每轮约有2000个1秒的声音片段,batch_size为10。

for epoch in range(num_epochs):  # loop over the dataset multiple times

    running_loss = 0.0
    correct = 0
    for i, data in enumerate(asc_dataloader, 0):
        # get the inputs; data is a list of [inputs, labels]
        inputs, labels = data
        labels = torch.tensor(labels, dtype=torch.long)

        # zero the parameter gradients
        optimizer.zero_grad()

        # forward + backward + optimize
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()

        optimizer.step()

        _, preds = torch.max(outputs, 1)
        # show_images(inputs[:6], labels[:6], preds[:6])
        correct += (preds == labels).sum()

        # print statistics
        running_loss += loss.item()
        if i % 1 == 0:    # print every 2000 mini-batches
            print('Epoch [{}/{}], Step [{}/{}], Loss(Avg): {:.3f}'
                  .format(epoch + 1, num_epochs, i + 1, len(asc_dataloader), running_loss / 1))
            running_loss = 0.0
    print("Epoch {}, Acc: {}/{}".format(epoch + 1, correct, asc_dataset.__len__()))
    correct = 0

You may also like...