让音视频学习变得简单之音频深度学习(4):声音分类,分步进行

普通语言的音频深度学习基础应用场景的端到端示例和体系结构。


布鲁斯·马斯Bruce mars)Unsplash拍摄的照片

音视频学习之音频教学知识中,声音分类是音频深度学习中应用最广泛的一种,它包括学习对声音进行分类,并预测声音的类别。这种类型的问题可以应用于许多实际情况,例如,对音乐片段进行分类以识别音乐的流派,或者通过一组说话者的短对话来进行分类,从而根据声音识别说话者。

在这篇音视频学习文章中,我们将通过一个简单的演示程序来了解用于解决这样的音频分类问题的办法。我的目标自始至终都是不仅要理解某件事情的工作原理,还要理解它的工作动机。

我的音频深度学习系列中,还有其他几篇文章与音视频学习可能有关。他们探索了该领域中其他有趣的话题,包括我们如何准备用于深度学习的音频数据,为什么我们将Mel Spectrograms用于深度学习模型以及如何生成并优化它们。

1.最先进的技术-本文(声音及其数字化方式。在日常生活中深度学习音频会遇到那些问题?什么是声谱图,为什么它们对使用语音聊天室至关重要?)

2.为什么梅尔频谱图的性能更好(在Python中处理音频数据。什么是梅尔频谱图以及如何生成它们)

3.功能的优化和增强(增强频谱图功能可通过超参数调整和数据增强实现最佳性能)

4.音频分类(端到端的示例和用于对普通声音进行分类的体系结构。适用于语音聊天室使用的各种场景。)

5.自动语音识别(语音转文本算法和体系结构,使用CTC Loss和解码来对齐序列)

音频分类

就像使用MNIST数据集对手写数字进行分类一样,对于计算机视觉来说,这是“ Hello World”类型的问题,我们可以将此应用视为音频深度学习的入门问题。

我们将从声音文件开始,将它们转换成声谱图,将它们输入到CNN加线性分类器模型中,并产生关于声音所属类的预测。


音频分类应用程序(作者提供的图像)

有许多适用于不同类型声音的数据集,这些数据集包含大量音频样本,以及每个样本的类标签,通过这些标签可以根据你要解决的问题来标记声音的类型。

这些类标签通常可以从音频样本文件名的某些部分或文件所在的子文件夹名称中获得。 另外,类标签是在单独的元数据文件中指定的,通常是TXT、JSON或CSV格式。

问题示例—对普通城市的声音进行分类

针对我们的演示,我们将使用Urban Sound 8K数据集,该数据集由从日常城市生活中录制的普通声音集组成。 声音来自10个类,例如钻探、狗吠和警笛声。 每个声音样本都用其所属的类别标记。

下载数据集后,我们看到它由两部分组成:

  • “音频”文件夹中的音频文件:它有10个子文件夹,分别名为“ fold1”至“ fold10”。 每个子文件夹包含许多“ .wav”音频样本,例如 ‘fold1 / 103074–7–1–0.wav’
  • “元数据”文件夹中的元数据:文件名为“ UrbanSound8K.csv”,其中包含有关数据集中每个音频样本的信息,例如其文件名、类别标签、“ fold”子文件夹位置等。 对于10个类中的每一个,类标签都是0到9之间的数字类ID。 例如数字0表示空调、数字1表示汽车喇叭,依此类推。

样本的长度约为4秒。这是其中一个示例的样子:

1_uc-tdNJaR1XSjMrKLQTKCw
演练的音频样本(作者提供的图像)
1_1hx7S8kUks-0O0VbXonVAA
采样率、通道数、位数和音频编码

数据集创建者的建议是使用折叠来进行10倍交叉验证,以报告指标并评估模型的性能。但是,由于本文的主要目标是作为音频深度学习示例的演示,而不是获得最佳指标,因此,我们将忽略折叠并将所有样本简单地视为一个大数据集。

准备训练数据

对于大多数深度学习问题,我们将按照以下步骤操作:

1_S2USvKfzKXYjPM4hvoUcfg
深度学习工作流(作者提供的图片)

这个问题的训练数据将非常简单:

  • 功能(X)是音频文件路径
  • 目标标签(y)是类名

由于数据集的元数据文件已经包含此信息,因此我们可以直接使用它。元数据包含每个音频文件的信息。

1_IfntZFrpUdKmkH-ON6PJ6Q

由于它是CSV文件,因此我们可以使用Pandas对其进行读取。我们可以准备函数并从元数据中标记数据。

# ----------------------------
# Prepare training data from Metadata file
# ----------------------------
import pandas as pd
from pathlib import Path

download_path = Path.cwd()/'UrbanSound8K'

# Read metadata file
metadata_file = download_path/'metadata'/'UrbanSound8K.csv'
df = pd.read_csv(metadata_file)
df.head()

# Construct file path by concatenating fold and file name
df['relative_path'] = '/fold' + df['fold'].astype(str) + '/' + df['slice_file_name'].astype(str)

# Take relevant columns
df = df[['relative_path', 'classID']]
df.head()

这为我们提供了训练数据所需的信息。

1_AHd213FyvOAzAfWDlJsnKA
带有音频文件路径和班级ID的训练数据

当元数据不可用时,扫描音频文件目录

有了元数据文件,对我们来说就很容易了。我们为什么不为不包含元数据文件的数据集准备数据呢?

因为许多数据集仅包含以文件夹结构排列的音频文件,可以从中导出类标签。为了以这种格式准备我们的训练数据,我们将执行以下操作:

1_7IPPbb5nR5vVBgLZPCmwcg
当元数据不可用时准备训练数据(作者提供的图像)

  • 扫描目录并准备所有音频文件路径的列表。
  • 从每个文件名或父子文件夹的名称中提取类标签
  • 将每个类名称从文本映射到数字类ID

使用或不使用元数据,结果将是相同的-由音频文件名列表和由类ID组成的目标标签组成的功能。

音频预处理:定义变换

带有音频文件路径的训练数据无法直接输入到模型中。我们必须从文件中加载音频数据并进行处理,以使其具有模型期望的格式。

当我们读取并加载音频文件时,所有音频预处理将在运行时动态完成,这种方法也类似于我们将要处理的图像文件。由于音频数据(如图像数据)可能非常大且占用大量内存,我们不希望提前一次将整个数据集全部读取到内存中,因此,我们在训练数据中仅保留音频文件名(或图像文件名)。

然后,在运行时,当我们一次训练一批模型时,我们将加载该批次的音频数据,并通过对音频进行一系列转换来对其进行处理。这样,我们一次只将一批音频数据保存在内存中。

对于图像数据,我们可能会有一个转换管道,在该转换过程中,我们首先将图像文件读取为像素并将其加载。然后,我们可以应用一些图像处理步骤来调整数据的形状和大小,将其裁剪为固定大小,然后将其从RGB转换为灰度。我们可能还会应用一些图像增强步骤,例如旋转、翻转等。

音频数据的处理非常相似。现在,我们仅定义功能,当我们在训练期间将数据输入模型时,它们将在稍后运行。

1_sHG9FrSI75fuX-ffGwZgHA
预处理训练数据以输入到我们的模型(作者提供的图像)

读取文件中的音频

我们首先需要做的事是以“ .wav”格式读取和加载音频文件。由于我们在此示例中使用的是Pytorch,因此下面的实现使用torchaudio进行音频处理,但是librosa也可以正常工作。

import math, random
import torch
import torchaudio
from torchaudio import transforms
from IPython.display import Audio

class AudioUtil():
  # ----------------------------
  # Load an audio file. Return the signal as a tensor and the sample rate
  # ----------------------------
  @staticmethod
  def open(audio_file):
    sig, sr = torchaudio.load(audio_file)
    return (sig, sr)

1_TZEaMk3PpUa8hyoeSpERhg
从文件加载的音频波(作者提供的图像)

转换成两个频道

一些声音文件是单声道(即1个音频通道),而大多数则是立体声(即2个音频通道)。由于我们的模型期望所有项目都具有相同的尺寸,因此我们将第一个通道复制到第二个通道,从而将单声道文件转换为立体声。

  # ----------------------------
  # Convert the given audio to the desired number of channels
  # ----------------------------
  @staticmethod
  def rechannel(aud, new_channel):
    sig, sr = aud

    if (sig.shape[0] == new_channel):
      # Nothing to do
      return aud

    if (new_channel == 1):
      # Convert from stereo to mono by selecting only the first channel
      resig = sig[:1, :]
    else:
      # Convert from mono to stereo by duplicating the first channel
      resig = torch.cat([sig, sig])

    return ((resig, sr))

标准化采样率

一些声音文件以48000Hz的采样率采样,而大多数声音文件以44100Hz的采样率采样。这意味着,对于某些声音文件,1秒音频的数组大小为48000,而对于其他声音文件,此数组的大小较小,为44100。再一次,我们必须将所有音频标准化并将其转换为相同的采样率,以使所有阵列具有相同的尺寸。

  # ----------------------------
  # Since Resample applies to a single channel, we resample one channel at a time
  # ----------------------------
  @staticmethod
  def resample(aud, newsr):
    sig, sr = aud

    if (sr == newsr):
      # Nothing to do
      return aud

    num_channels = sig.shape[0]
    # Resample first channel
    resig = torchaudio.transforms.Resample(sr, newsr)(sig[:1,:])
    if (num_channels > 1):
      # Resample the second channel and merge both channels
      retwo = torchaudio.transforms.Resample(sr, newsr)(sig[1:,:])
      resig = torch.cat([resig, retwo])

    return ((resig, newsr))

重新调整为相同长度

然后,我们可以通过以下方式将所有音频样本的大小调整为相同的长度:通过使用静音填充音频时长或将其截断来延长音频时长。我们将该方法添加到AudioUtil类中。

  # ----------------------------
  # Pad (or truncate) the signal to a fixed length 'max_ms' in milliseconds
  # ----------------------------
  @staticmethod
  def pad_trunc(aud, max_ms):
    sig, sr = aud
    num_rows, sig_len = sig.shape
    max_len = sr//1000 * max_ms

    if (sig_len > max_len):
      # Truncate the signal to the given length
      sig = sig[:,:max_len]

    elif (sig_len < max_len):
      # Length of padding to add at the beginning and end of the signal
      pad_begin_len = random.randint(0, max_len - sig_len)
      pad_end_len = max_len - sig_len - pad_begin_len

      # Pad with 0s
      pad_begin = torch.zeros((num_rows, pad_begin_len))
      pad_end = torch.zeros((num_rows, pad_end_len))

      sig = torch.cat((pad_begin, sig, pad_end), 1)
      
    return (sig, sr)

数据扩充:时移

接下来,我们可以通过应用时间偏移将音频向左或向右移动随机量来对原始音频信号进行数据增强。在本篇文章,我获得了与之有关更详细的内容以及其他数据增强技术。

1_pOT-ykG5GDMCm7uNX-hQog
声波的时移(作者提供的图像)

  # ----------------------------
  # Shifts the signal to the left or right by some percent. Values at the end
  # are 'wrapped around' to the start of the transformed signal.
  # ----------------------------
  @staticmethod
  def time_shift(aud, shift_limit):
    sig,sr = aud
    _, sig_len = sig.shape
    shift_amt = int(random.random() * shift_limit * sig_len)
    return (sig.roll(shift_amt), sr)

梅尔谱图

接着,我们将增强后的音频转换为梅尔谱图。它们捕获了音频的基本特征,并且通常是将音频数据输入到深度学习模型中的最合适方法。要获取更多背景知识,你可能需要阅读我的文章(在这里这里),这些文章用简单的词来解释什么是梅尔频谱图,为什么它们对于音频深度学习至关重要,以及它们是如何生成的,如何调整它们以获得模型的最佳性能。

  # ----------------------------
  # Generate a Spectrogram
  # ----------------------------
  @staticmethod
  def spectro_gram(aud, n_mels=64, n_fft=1024, hop_len=None):
    sig,sr = aud
    top_db = 80

    # spec has shape [channel, n_mels, time], where channel is mono, stereo etc
    spec = transforms.MelSpectrogram(sr, n_fft=n_fft, hop_length=hop_len, n_mels=n_mels)(sig)

    # Convert to decibels
    spec = transforms.AmplitudeToDB(top_db=top_db)(spec)
    return (spec)

1_juo37srmokbywcNkyKET3A
声波的梅尔频谱图(作者提供的图像)

数据扩充:时间和频率屏蔽

现在我们可以进行另一轮增强,这次是在梅尔谱图上,而不是在原始音频上。我们将使用一种称为SpecAugment的技术,这个技术使用以下两种方法:

  • 频率屏蔽-通过在频谱图上添加水平条来随机屏蔽一系列连续频率。
  • 时间掩码-与频率掩码类似,不同之处在于我们使用竖线从频谱图中随机地遮挡了时间范围。
  # ----------------------------
  # Augment the Spectrogram by masking out some sections of it in both the frequency
  # dimension (ie. horizontal bars) and the time dimension (vertical bars) to prevent
  # overfitting and to help the model generalise better. The masked sections are
  # replaced with the mean value.
  # ----------------------------
  @staticmethod
  def spectro_augment(spec, max_mask_pct=0.1, n_freq_masks=1, n_time_masks=1):
    _, n_mels, n_steps = spec.shape
    mask_value = spec.mean()
    aug_spec = spec

    freq_mask_param = max_mask_pct * n_mels
    for _ in range(n_freq_masks):
      aug_spec = transforms.FrequencyMasking(freq_mask_param)(aug_spec, mask_value)

    time_mask_param = max_mask_pct * n_steps
    for _ in range(n_time_masks):
      aug_spec = transforms.TimeMasking(time_mask_param)(aug_spec, mask_value)

    return aug_spec

1_6AqSTfzLfpHI3gvDHdjIEg
SpecAugment之后的梅尔光谱图。注意水平和垂直蒙版带(作者提供的图像)

定义自定义数据加载器

现在,我们已经定义了所有预处理转换函数,我们将定义一个自定义的Pytorch Dataset对象。

要将数据提供给使用Pytorch的模型,我们需要两个对象:

  • 一个自定义Dataset对象,该对象使用所有音频转换来预处理音频文件并一次准备一个数据项。
  • 内置的DataLoader对象,该对象使用Dataset对象来获取单个数据项并将其打包成一批数据。

使用数据加载器准备一批数据

现在已经定义了我们需要将数据输入到模型中的所有功能。

我们使用自定义数据集从Pandas数据框中加载要素和标签,然后以80:20的比例将数据随机分为训练和验证集。然后,我们使用它们来创建我们的训练和验证数据加载器。

1_pUrFw7A7G4E1eFNJ4zAQ5A
拆分数据以进行培训和验证(作者提供的图片)

from torch.utils.data import random_split

myds = SoundDS(df, data_path)

# Random split of 80:20 between training and validation
num_items = len(myds)
num_train = round(num_items * 0.8)
num_val = num_items - num_train
train_ds, val_ds = random_split(myds, [num_train, num_val])

# Create training and validation data loaders
train_dl = torch.utils.data.DataLoader(train_ds, batch_size=16, shuffle=True)
val_dl = torch.utils.data.DataLoader(val_ds, batch_size=16, shuffle=False)

当我们开始训练时,数据加载器将随机获取一批包含音频文件名列表的输入要素,并在每个音频文件上运行预处理音频转换。它还将获取一批包含类ID的相应目标Label。因此,它将一次输出一批训练数据,这些数据可以直接作为输入提供给我们的深度学习模型。


Data Loader应用转换并一次准备一批数据(作者提供的图像)

让我们从音频文件开始,逐步了解数据转换的步骤:

  • 文件中的音频被加载到Numpy形状的数组中(num_channels,num_samples)。大部分音频以44.1kHz采样,持续时间约为4秒,从而产生44,100 * 4 = 176,400个采样。如果音频具有1个通道,则阵列的形状将为(1、176,400)。同样,具有2个通道的4秒钟持续时间且以48kHz采样的音频将具有192,000个采样,形状为(2,192,000)。

  • 由于每种音频的通道和采样率不同,因此接下来的两次转换会将音频重新采样为标准的44.1kHz和标准的2个通道

  • 由于某些音频片段可能大于或小于4秒,因此我们还将音频持续时间标准化为固定的4秒长度。现在,所有项目的数组都具有相同的形状(2,176,400)

  • 现在,时移数据增强功能会随机将每个音频样本向前或向后移动。形状不变。

  • 增强后的音频将转换为梅尔频谱图,其形状为(num_channels,Mel freq_bands,time_steps)=(2,64,344)

  • SpecAugment数据增强功能将时间和频率掩码随机应用于梅尔频谱图,形状不变。

因此,每批将具有两个张量,一个用于包含梅尔频谱图的 X 特征数据,另一个用于包含数字类ID的 y 目标标签。从每个训练时期的训练数据中随机选择批次。

每个批次的形状为(batch_sz,num_channels,Mel freq_bands,time_steps)

1_71j3wKidKioxb1cbk8gjtA
一批(X,y)数据

我们可以可视化批次中的一项。我们看到带有垂直和水平条纹的梅尔频谱图显示了频率和时间屏蔽数据的增强。
1_Dpi3ZXiUfzbJ9K8u9uo6vw
现在就可以将数据输入模型了。

建立模型

我们刚刚执行的数据处理步骤是我们音频分类问题中最独特的方面。从这里开始,模型和训练过程与标准图像分类问题中常用的模型和训练过程非常相似,并且不特定于音频深度学习。

由于我们的数据现在由光谱图图像组成,因此我们建立了CNN分类架构来对其进行处理。它具有生成特征图的四个卷积块。然后将数据重新整形为我们需要的格式,以便可以将其输入到线性分类器层,该层最终输出针对10个分类的预测。


该模型获取一批预处理数据并输出类别预测(图片由作者提供)

有关模型如何处理一批数据的更多详细信息:

  • 一批图像以形状(batch_sz,num_channels,Mel freq_bands,time_steps)输入模型。(16,2,64,344)。
  • 每个CNN层都应用其滤镜以提高图像深度,即通道数。图像的宽度和高度会随着内核和步幅的减小而减小。最后,经过四个CNN层后,我们得到了输出特征图,即。(16、64、4、22)。
  • 将其合并并展平为(16,64)的形状,然后输入到“线性”层。
  • 线性层为每个类别输出一个预测分数,即 (16、10)


image

训练

现在,我们准备创建训练循环来训练模型。

我们定义了优化器、损失器和调度器的功能,以便随着训练的进行动态改变我们的学习率,这通常允许在更少的时间内训练转换。

我们针对几个时期训练模型,并在每次迭代中处理一批数据。我们跟踪一个简单的准确性指标,该指标衡量正确预测的百分比。


image

推理

通常,作为培训循环的一部分,我们还将根据验证数据评估指标。然后,我们可能会对看不见的数据进行推断,也许是从原始数据中保留测试数据集。但是,出于本演示的目的,我们将为此目的使用验证数据。

我们运行一个推理循环,小心禁用梯度更新。与模型一起执行前向传递以获取预测,但是我们不需要向后传播或运行优化器。

image

结论

现在我们已经看到了声音分类的端到端示例,它是音频深度学习中最基础的问题之一。这不仅可以用于广泛的应用中,而且我们在此介绍的许多概念和技术都将与更复杂的音频问题相关,例如自动语音识别,其中我们从人类语音入手,了解人们在说什么,以及将其转换为文本。

原文作者 Ketan Doshi

原文链接 https://towardsdatascience.com/audio-deep-learning-made-simple-sound-classification-step-by-step-cebc936bbe5

推荐阅读
作者信息
ChenYun
TA 暂未填写个人简介
文章
50
相关专栏
本专栏仅用于分享音视频相关的技术文章,与其他开发者和 Agora 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。