使用深度學習進行音頻分類的端到端示例和解釋
聲音分類是音頻深度學習中應用最廣泛的方法之一。它包括學習對聲音進行分類并預測聲音的類別。這類問題可以應用到許多實際場景中,例如,對音樂片段進行分類以識別音樂類型,或通過一組揚聲器對短話語進行分類以根據聲音識別說話人。
在本文中,我們將介紹一個簡單的演示應用程序,以便理解用于解決此類音頻分類問題的方法。我的目標不僅僅是理解事物是如何運作的,還有它為什么會這樣運作。
音頻分類
就像使用MNIST數據集對手寫數字進行分類被認為是計算機視覺的“Hello World”類型的問題一樣,我們可以將此應用視為音頻深度學習的入門問題。
我們將從聲音文件開始,將它們轉換為聲譜圖,將它們輸入到CNN加線性分類器模型中,并產生關于聲音所屬類別的預測。

有許多合適的數據集可以用于不同類型的聲音。這些數據集包含大量音頻樣本,以及每個樣本的類標簽,根據你試圖解決的問題來識別聲音的類型。
這些類標簽通??梢詮囊纛l樣本文件名的某些部分或文件所在的子文件夾名中獲得。另外,類標簽在單獨的元數據文件中指定,通常為TXT、JSON或CSV格式。
演示-對普通城市聲音進行分類
對于我們的演示,我們將使用Urban Sound 8K數據集,該數據集包含從日常城市生活中錄制的普通聲音的語料庫。這些聲音來自于10個分類,如工程噪音、狗叫聲和汽笛聲。每個聲音樣本都標有它所屬的類。
下載數據集后,我們看到它由兩部分組成:
“Audio”文件夾中的音頻文件:它有10個子文件夾,命名為“fold1”到“fold10”。每個子文件夾包含許多。wav的音頻樣本。例如“fold1/103074 - 7 - 1 - 0. - wav”
“Metadata”文件夾中的元數據:它有一個文件“UrbanSound8K”。它包含關于數據集中每個音頻樣本的信息,如文件名、類標簽、“fold”子文件夾位置等。類標簽是10個類中的每個類從0到9的數字類ID。如。數字0表示空調,1表示汽車喇叭,以此類推。
一般音頻的長度約為4秒。下面是其中一個例子:

數據集創(chuàng)建者的建議是使用10折的交叉驗證,以便計算指標并評估模型的性能。 但是,由于本文的目標主要是作為音頻深度學習示例的演示,而不是獲得最佳指標,因此,我們將忽略分折并將所有樣本簡單地視為一個大型數據集。
準備訓練數據
對于大多數深度學習問題,我們將遵循以下步驟:

這個數據集的數據整理很簡單:
特性(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組成的目標標簽。
音頻預處理:定義變換
這種帶有音頻文件路徑的訓練數據不能直接輸入到模型中。我們必須從文件中加載音頻數據并對其進行處理,使其符合模型所期望的格式。
當我們讀取并加載音頻文件時,所有音頻預處理將在運行時動態(tài)完成。這種方法也類似于我們將要處理的圖像文件。由于音頻數據(或圖像數據)可能非常大且占用大量內存,因此我們不希望提前一次將整個數據集全部讀取到內存中。因此,我們在訓練數據中僅保留音頻文件名(或圖像文件名)。。
然后在運行時,當我們一次訓練一批數據時,我們將加載該批次的音頻數據,并通過對音頻進行一系列轉換來對其進行處理。這樣,我們一次只將一批音頻數據保存在內存中。
對于圖像數據,我們可能會有一個轉換管道,在該轉換過程中,我們首先將圖像文件讀取為像素并將其加載。然后,我們可以應用一些圖像處理步驟來調整數據的形狀和大小,將其裁剪為固定大小,然后將其從RGB轉換為灰度(如果需要)。我們可能還會應用一些圖像增強步驟,例如旋轉,翻轉等。
音頻數據的處理非常相似。現(xiàn)在我們只定義函數,當我們在訓練期間向模型提供數據時,它們將在稍后運行。

讀取文件中的音頻
我們需要做的第一件事是以“ .wav”格式讀取和加載音頻文件。 由于我們在此示例中使用的是Pytorch,因此下面的實現(xiàn)使用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))
標準化采樣率
一些聲音文件以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))
調整為相同長度
然后,我們將所有音頻樣本的大小調整為具有相同的長度,方法是通過使用靜默填充或通過截斷其長度來延長其持續(xù)時間。 我們將該方法添加到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)

數據擴充:時間和頻率屏蔽
現(xiàn)在我們可以進行另一輪擴充,這次是在Mel頻譜圖上,而不是在原始音頻上。 我們將使用一種稱為SpecAugment的技術,該技術使用以下兩種方法:
頻率屏蔽-通過在頻譜圖上添加水平條來隨機屏蔽一系列連續(xù)頻率。
時間掩碼-與頻率掩碼類似,不同之處在于,我們使用豎線從頻譜圖中隨機地遮擋了時間范圍。
- # ----------------------------
- # 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

自定義數據加載器
現(xiàn)在,我們已經定義了所有預處理轉換函數,我們將定義一個自定義的Pytorch Dataset對象。
要將數據提供給使用Pytorch的模型,我們需要兩個對象:
一個自定義Dataset對象,該對象使用所有音頻轉換來預處理音頻文件并一次準備一個數據項。
內置的DataLoader對象,該對象使用Dataset對象來獲取單個數據項并將其打包為一批數據。
- 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
使用數據加載器準備一批數據
現(xiàn)在已經定義了我們需要將數據輸入到模型中的所有函數。
我們使用自定義數據集從Pandas中加載特征和標簽,然后以80:20的比例將數據隨機分為訓練和驗證集。 然后,我們使用它們來創(chuàng)建我們的訓練和驗證數據加載器。

- 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。 因此,它將一次輸出一批訓練數據,這些數據可以直接作為輸入提供給我們的深度學習模型。

讓我們從音頻文件開始,逐步完成數據轉換的各個步驟:
文件中的音頻被加載到Numpy的數組中(numchannels,numsamples)。大部分音頻以44.1kHz采樣,持續(xù)時間約為4秒,從而產生44,100 * 4 = 176,400個采樣。如果音頻具有1個通道,則陣列的形狀將為(1、176,400)。同樣,具有2個通道的4秒鐘持續(xù)時間且以48kHz采樣的音頻將具有192,000個采樣,形狀為(2,192,000)。
每種音頻的通道和采樣率不同,因此接下來的兩次轉換會將音頻重新采樣為標準的44.1kHz和標準的2個通道。
某些音頻片段可能大于或小于4秒,因此我們還將音頻持續(xù)時間標準化為固定的4秒長度?,F(xiàn)在,所有項目的數組都具有相同的形狀(2,176,400)
時移數據擴充功能會隨機將每個音頻樣本向前或向后移動。形狀不變。
擴充后的音頻將轉換為梅爾頻譜圖,其形狀為(numchannels,Mel freqbands,time_steps)=(2,64,344)
SpecAugment數據擴充功能將時間和頻率掩碼隨機應用于梅爾頻譜圖。形狀不變。
最后我們每批得到了兩個張量,一個用于包含梅爾頻譜圖的X特征數據,另一個用于包含數字類ID的y目標標簽。 從每個訓練輪次的訓練數據中隨機選擇批次。
每個批次的形狀為(batchsz,numchannels,Mel freqbands,timesteps)

我們可以將批次中的一項可視化。 我們看到帶有垂直和水平條紋的梅爾頻譜圖顯示了頻率和時間屏蔽數據的擴充。

建立模型
我們剛剛執(zhí)行的數據處理步驟是我們音頻分類問題中最獨特的方面。 從這里開始,模型和訓練過程與標準圖像分類問題中常用的模型和訓練過程非常相似,并且不特定于音頻深度學習。
由于我們的數據現(xiàn)在由光譜圖圖像組成,因此我們建立了CNN分類架構來對其進行處理。 它具有生成特征圖的四個卷積塊。 然后將數據重新整形為我們需要的格式,以便可以將其輸入到線性分類器層,該層最終輸出針對10個分類的預測。

模型信息:
色彩圖像以形狀(batchsz,numchannels,Mel freqbands,timesteps)輸入模型。(16,2,64,344)。
每個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
訓練
現(xiàn)在,我們準備創(chuàng)建訓練循環(huán)來訓練模型。
我們定義了優(yōu)化器,損失函數和學習率的調度計劃的函數,以便隨著訓練的進行而動態(tài)地改變我們的學習率,這樣可以使模型收斂的更快。
在每輪訓練完成后。 我們跟蹤一個簡單的準確性指標,該指標衡量正確預測的百分比。
# ----------------------------
# 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)
推理
通常,作為訓練循環(huán)的一部分,我們還將根據驗證數據評估指標。 所以我們會對原始數據中保留測試數據集(被當作是訓練時看不見的數據)進行推理。 出于本演示的目的,我們將為此目的使用驗證數據。
我們禁用梯度更新并運行一個推理循環(huán)。 與模型一起執(zhí)行前向傳播以獲取預測,但是我們不需要反向傳播和優(yōu)化。
- # ----------------------------
- # 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)
結論
現(xiàn)在我們已經看到了聲音分類的端到端示例,它是音頻深度學習中最基礎的問題之一。 這不僅可以用于廣泛的應用中,而且我們在此介紹的許多概念和技術都將與更復雜的音頻問題相關,例如自動語音識別,其中我們從人類語音入手,了解人們在說什么,以及將其轉換為文本。