音频深度学习的基础应用场景的端到端示例和架构。
Bruce mars 发布在 Unsplash 上的图片
声音分类应用是最常见的音频深度学习应用,包括学习声音分类、预测声音的类别。声音分类可以应用于多种实际场景,例如,对音乐片段进行分类以识别音乐的流派,或者分析一组对话然后根据声音识别说话者。
本文将通过一个简单的 demo 应用向大家介绍这些音频分类办法。我们不仅要理解音频深度学习是怎么工作的,还要理解音频深度学习的工作原理。
下面列出了音频深度学习系列的其他几篇文章,这些文章的主题包括如何准备用于深度学习的音频数据、为什么把梅尔声谱图用于深度学习模型、以及如何生成并优化梅尔声谱图等。
1.前沿技术(什么是声音?声音是如何被数字化的?音频深度学习解决了我们日常生活中的哪些问题?什么是声谱图以及它为什么这么重要?)
2.为什么梅尔声谱图性能更佳(在 Python 中处理音频数据、什么是梅尔声谱图以及如何生成梅尔声谱图?)
3.数据准备和增强(本文)(通过超参数调整和数据增强来增强声谱图的功能,从而获得最佳性能)
4.自动语音识别(语音转文本算法和架构、使用 CTC Loss 和解码来对齐序列)
5.集束搜索(语音转文本和 NLP 应用中常用的增强预测的算法)
音频分类
在计算机视觉领域,使用 MNIST 数据集对手写的数字进行分类是个“Hello World”类的问题,与此类似,音频分类应用是音频深度学习的入门应用。
我们从声音文件开始,先把声音文件转换成声谱图,然后把声谱图输入到 CNN 加线性分类器模型中,最后,预测声音所属类别。
音频分类应用(图片来自作者)
我们有适用于各种声音类型的数据集,数据集包含大量音频样本,每个样本有一个类别标签,该类别标签基于我们想要解决的问题对声音类型进行标记。
一般来说,类别标签是音频样本文件名或文件所在的子文件夹名称,另外,类别标签可以由单独的元数据文件指定,通常是 TXT、JSON 或 CSV 格式。
问题示例——日常城市声音
我们的 demo 使用 Urban Sound 8K 数据集,该数据集是在日常城市生活中录制的声音集,主要有 10 种声音,例如钻孔声、狗吠和汽笛声。 每个声音样本都有类别标签。
下载数据集后,我们看到它由两部分组成:
-
“音频”文件夹中的音频文件:它有 10 个子文件夹,文件名依次从“ fold1”排到“ fold10”。 每个子文件夹包含若干“ .wav” 格式的音频样本,例如: ‘fold1 / 103074–7–1–0.wav’
-
“元数据”文件夹中的元数据:其中有一个“ UrbanSound8K.csv”文件,该文件包含数据集中每个音频样本的信息,例如其文件名、类别标签、“ fold”子文件夹位置等。这 10 类文件的类别标签由 0~9 的数字 ID表示。 例如数字 0 表示空调、数字 1 表示汽车喇叭等。
每个样本的长度大约为 4 秒。下图为其中一个样本:
钻孔声的音频样本(图片来自作者)
采样率、频道数、比特数和音频编码
数据集创建者建议使用 10 折交叉验证来报告指标并评估模型的性能。但是,本文的目的是搭建一个音频深度学习示例的 demo,并非为了获得最佳指标,因此,本文不考虑折叠,把所有样本当作一个大数据集。
准备训练数据
对于大多数深度学习问题,我们将按照以下步骤操作:
深度学习工作流(图片来自作者)
这个问题的训练数据非常简单:
-
特性(X)是音频文件路径
-
目标标签(y)是类别名称
因为数据集的元数据文件已经包含这些信息,所以我们可以直接使用元数据文件。元数据包含每个音频文件的信息。
元数据文件是 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()
我们从中获得训练数据所需要的信息:
带有音频文件路径和类别 ID 的训练数据
当元数据不可用时,扫描音频文件目录
如果数据集有元数据文件,一切就方便多了。但如果数据集里不包含元数据文件,我们怎么准备数据呢?
许多数据集仅包含以文件夹结构排列的音频文件,我们可以从文件夹结构中导出类别标签。为了以这种格式准备我们的训练数据,我们将执行以下操作:
在元数据不可用的情况下准备训练数据(图片来自作者)
-
扫描目录并准备一个音频文件路径的列表。
-
从文件名或父子文件夹的名称中提取类别标签
-
将每个类别名称的文本映射到数字类 ID
无论有没有元数据,目标都是一样的——获得由音频文件名列表组成的特性和由类别 ID 组成的目标标签。
音频预处理:定义变换
音频文件路径的训练数据无法直接输入到模型中。我们必须从文件中加载音频数据并将其处理成模型兼容的格式。
读取并加载音频文件的同时会进行音频预处理,这与处理图像文件的方法类似。另外,与图像数据一样,音频数据可能会非常大,会占用大量内存,所以我们不想把整个数据集一次性全部提前读取到内存中,我们在训练数据中仅保留音频文件名(或图像文件名)。
我们每训练一批模型时,就加载该批次的音频数据,并对音频进行一系列的转换处理,一次只在内存中保存一批音频数据。
我们使用转换通道来处理图像数据,先将图像文件读取为像素并加载,采用一些图像处理操作来调整数据的形状和大小,将其裁剪为固定尺寸,然后将其从 RGB 模式转换为灰度模式。我们可能还会使用一些图像增强操作,例如旋转、翻转等。
音频数据的处理与之类似,我们现在只定义函数,训练开始后,向模型提供数据后函数就会开始运行。
预处理训练数据以便向模型输入数据(图片来自作者)
读取文件中的音频
首先,读取和加载“ .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 个音频通道),但大多数则是立体声(即 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))
标准化采样率
一些声音文件是按照 48000 赫兹的采样率进行采样的,但大多数声音文件以 44100 赫兹的采样率进行采样,即有些声音文件的 1 秒音频的数组大小为 48000,而另一些声音文件的 1 秒音频的数组大小较小,为 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)
数据增强:时间移位
接下来,我们可以通过时间移位将音频向左或向右随机移动,从而增强原始音频信号数据。我在音频深度学习(第三部分):数据准备和增强中讨论了数据增强技术。
声波的时间移位(图像来自作者)
# ----------------------------
# 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)
声波的梅尔声谱图(图像来自作者)
数据增强:时间和频域掩蔽
现在可以在梅尔声谱图上进行一轮增强。这轮增强使用 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
SpecAugment 之后的梅尔声谱图。注意水平和垂直掩码带(图片来自作者)
自定义数据加载器
我们已经定义了所有预处理转换函数,接下来,我们需要定义一个定制的 Pytorch 数据集对象。
要将数据提供给使用 Pytorch 的模型,需要两个对象:
-
一个定制数据集对象,该对象使用音频转换来预处理音频文件,每次准备一个数据项。
-
一个内置的 Dataloader 对象,该对象使用数据集对象来获取单个数据项并将其打包成批数据。
from torch.utils.data import DataLoader, Dataset, random_split
import torchaudio
# ----------------------------
# Sound Dataset
# ----------------------------
class SoundDS(Dataset):
def __init__(self, df, data_path):
self.df = df
self.data_path = str(data_path)
self.duration = 4000
self.sr = 44100
self.channel = 2
self.shift_pct = 0.4
# ----------------------------
# Number of items in dataset
# ----------------------------
def __len__(self):
return len(self.df)
# ----------------------------
# Get i'th item in dataset
# ----------------------------
def __getitem__(self, idx):
# Absolute file path of the audio file - concatenate the audio directory with
# the relative path
audio_file = self.data_path + self.df.loc[idx, 'relative_path']
# Get the Class ID
class_id = self.df.loc[idx, 'classID']
aud = AudioUtil.open(audio_file)
# Some sounds have a higher sample rate, or fewer channels compared to the
# majority. So make all sounds have the same number of channels and same
# sample rate. Unless the sample rate is the same, the pad_trunc will still
# result in arrays of different lengths, even though the sound duration is
# the same.
reaud = AudioUtil.resample(aud, self.sr)
rechan = AudioUtil.rechannel(reaud, self.channel)
dur_aud = AudioUtil.pad_trunc(rechan, self.duration)
shift_aud = AudioUtil.time_shift(dur_aud, self.shift_pct)
sgram = AudioUtil.spectro_gram(shift_aud, n_mels=64, n_fft=1024, hop_len=None)
aug_sgram = AudioUtil.spectro_augment(sgram, max_mask_pct=0.1, n_freq_masks=2, n_time_masks=2)
return aug_sgram, class_id
使用数据加载器准备批数据
我们已经定义了将数据输入到模型中的所有函数。
现在,使用定制数据集从 Pandas 数据框中加载特色和标签,然后按照 80:20 的比例将数据随机分为训练和验证集。然后,使用它们创建训练和验证数据加载器。
拆分数据以进行培训和验证(图片来自作者)
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 的目标标签。因此,加载器每次可以输出一批训练数据,这批数据可以直接输入进深度学习模型。
数据加载器应用转换,每次准备一批数据(图像来自作者)
从音频文件开始了解数据转换的步骤:
-
文件中的音频被加载到 Numpy 形状的数组中(num_channels,num_samples)。大部分音频以 44100 赫兹采样,持续时间约为 4 秒,产生 44,100 * 4 = 176,400 个采样。如果音频只有 1 个频道,则数组的形状将为(1,176,400),以此类推,以 48000 赫兹采样的持续 4 秒的音频有 192,000 个采样,如果这个音频有 2 个频道,那么数组的形状为(2,192,000)。
-
由于每个音频的频道数和采样率不同,因此接下来的两次转换会将音频统一采样为 44100 赫兹和 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)
一批(X,y)数据
下图是该批次中的一项,带有垂直和水平掩码带的梅尔声谱图显示了频率和时间屏蔽数据的增强。
现在就可以将数据输入模型了。
创建模型
上一节的数据处理步骤是音频分类问题中最特别的一步,接下来的模型和训练过程与图像分类问题中常用的模型和训练过程非常相似。
现在,我们的数据由声谱图组成,所以我们建立了 CNN 分类架构对其进行处理。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)
import torch.nn.functional as F
from torch.nn import init
# ----------------------------
# Audio Classification Model
# ----------------------------
class AudioClassifier (nn.Module):
# ----------------------------
# Build the model architecture
# ----------------------------
def __init__(self):
super().__init__()
conv_layers = []
# First Convolution Block with Relu and Batch Norm. Use Kaiming Initialization
self.conv1 = nn.Conv2d(2, 8, kernel_size=(5, 5), stride=(2, 2), padding=(2, 2))
self.relu1 = nn.ReLU()
self.bn1 = nn.BatchNorm2d(8)
init.kaiming_normal_(self.conv1.weight, a=0.1)
self.conv1.bias.data.zero_()
conv_layers += [self.conv1, self.relu1, self.bn1]
# Second Convolution Block
self.conv2 = nn.Conv2d(8, 16, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
self.relu2 = nn.ReLU()
self.bn2 = nn.BatchNorm2d(16)
init.kaiming_normal_(self.conv2.weight, a=0.1)
self.conv2.bias.data.zero_()
conv_layers += [self.conv2, self.relu2, self.bn2]
# Second Convolution Block
self.conv3 = nn.Conv2d(16, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
self.relu3 = nn.ReLU()
self.bn3 = nn.BatchNorm2d(32)
init.kaiming_normal_(self.conv3.weight, a=0.1)
self.conv3.bias.data.zero_()
conv_layers += [self.conv3, self.relu3, self.bn3]
# Second Convolution Block
self.conv4 = nn.Conv2d(32, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
self.relu4 = nn.ReLU()
self.bn4 = nn.BatchNorm2d(64)
init.kaiming_normal_(self.conv4.weight, a=0.1)
self.conv4.bias.data.zero_()
conv_layers += [self.conv4, self.relu4, self.bn4]
# Linear Classifier
self.ap = nn.AdaptiveAvgPool2d(output_size=1)
self.lin = nn.Linear(in_features=64, out_features=10)
# Wrap the Convolutional Blocks
self.conv = nn.Sequential(*conv_layers)
# ----------------------------
# Forward pass computations
# ----------------------------
def forward(self, x):
# Run the convolutional blocks
x = self.conv(x)
# Adaptive pool and flatten for input to linear layer
x = self.ap(x)
x = x.view(x.shape[0], -1)
# Linear layer
x = self.lin(x)
# Final output
return x
# Create the model and put it on the GPU if available
myModel = AudioClassifier()
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
myModel = myModel.to(device)
# Check that it is on Cuda
next(myModel.parameters()).device
训练
现在,我们创建训练循环来训练模型。
定义优化器、损失器和调度器的函数,以便随着训练来动态地改变学习率,这样做通常可以以更少的时间完成训练转换。
我们训练几个时期的模型,并在每次迭代中处理一批数据,记录简单的准确性指标,该指标衡量正确预测的百分比。
# ----------------------------
# Training Loop
# ----------------------------
def training(model, train_dl, num_epochs):
# Loss Function, Optimizer and Scheduler
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),lr=0.001)
scheduler = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr=0.001,
steps_per_epoch=int(len(train_dl)),
epochs=num_epochs,
anneal_strategy='linear')
# Repeat for each epoch
for epoch in range(num_epochs):
running_loss = 0.0
correct_prediction = 0
total_prediction = 0
# Repeat for each batch in the training set
for i, data in enumerate(train_dl):
# Get the input features and target labels, and put them on the GPU
inputs, labels = data[0].to(device), data[1].to(device)
# Normalize the inputs
inputs_m, inputs_s = inputs.mean(), inputs.std()
inputs = (inputs - inputs_m) / inputs_s
# Zero the parameter gradients
optimizer.zero_grad()
# forward + backward + optimize
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
scheduler.step()
# Keep stats for Loss and Accuracy
running_loss += loss.item()
# Get the predicted class with the highest score
_, prediction = torch.max(outputs,1)
# Count of predictions that matched the target label
correct_prediction += (prediction == labels).sum().item()
total_prediction += prediction.shape[0]
#if i % 10 == 0: # print every 10 mini-batches
# print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 10))
# Print stats at the end of the epoch
num_batches = len(train_dl)
avg_loss = running_loss / num_batches
acc = correct_prediction/total_prediction
print(f'Epoch: {epoch}, Loss: {avg_loss:.2f}, Accuracy: {acc:.2f}')
print('Finished Training')
num_epochs=2 # Just for demo, adjust this higher.
training(myModel, train_dl, num_epochs)
推理
通常我们还会根据验证数据来评估指标。我们可以从原始数据中划分出测试数据集来对不可见的数据进行推断。但根据本 demo 的目标,我们使用验证数据。
我们运行一个推理循环,关闭梯度更新,与模型一起执行前向传递以获取预测,不需要反向传播或运行优化器。
# ----------------------------
# Inference
# ----------------------------
def inference (model, val_dl):
correct_prediction = 0
total_prediction = 0
# Disable gradient updates
with torch.no_grad():
for data in val_dl:
# Get the input features and target labels, and put them on the GPU
inputs, labels = data[0].to(device), data[1].to(device)
# Normalize the inputs
inputs_m, inputs_s = inputs.mean(), inputs.std()
inputs = (inputs - inputs_m) / inputs_s
# Get predictions
outputs = model(inputs)
# Get the predicted class with the highest score
_, prediction = torch.max(outputs,1)
# Count of predictions that matched the target label
correct_prediction += (prediction == labels).sum().item()
total_prediction += prediction.shape[0]
acc = correct_prediction/total_prediction
print(f'Accuracy: {acc:.2f}, Total items: {total_prediction}')
# Run inference on trained model with the validation set
inference(myModel, val_dl)
结论
通过本文,我们对音频深度学习的一个基础问题——声音分类的端到端示例有所了解。声音分类不仅广泛应用于各类应用程序,而且它的许多概念和技术都与更复杂的音频问题相关,例如自动语音识别。我们可以用自动语音识别分析人类语言,理解说话内容,从而把语音转换成文本。
原文作者 Ketan Doshi
原文链接 https://towardsdatascience.com/audio-deep-learning-made-simple-sound-classification-step-by-step-cebc936bbe5