以及為什麼鳥類的聲音檢測對我們環境的未來如此重要
介紹
你聽說過自動語音識別,你聽說過音樂標籤和生成,但是你聽說過鳥的聲音檢測嗎?
大約在一年前,在我高二的時候,我第一次聽到這種音頻深度學習的用例。事實上,鳥音頻檢測是我做深度學習和計算機科學的第一個項目。我參與了一個研究項目,在北阿拉斯加的郊區用純粹的聲音來探測鳥類的存在。跳入其中,鳥的音頻檢測出現了這樣一個利基(有利可圖的形式),在本文中,我將向您展示如何在BirdVox-70k數據集上使用一個簡單的卷積神經網絡(CNN)來實現這一點。
為什麼鳥類的聲音檢測很重要呢?
多年來,鳥類音頻檢測應用中深度學習模型的使用一直在不斷發展進步,這也是一些人對此非常感興趣的原因。首先,鳥類移動速度快,體型小,已經很難追蹤。此外,不同種類的鳥都有自己獨特的發聲方式,這使得鳥類通過聲音和聽覺來檢測比通過視覺來檢測更加可取。最後,由於鳥類是生態系統的一部分,它們的存在和遷徙模式往往是任何特定地區環境健康的警示信號。
那麼,為什麼不錄製一段音頻,然後發送給人類稍後再聽呢?
手動標記音頻是昂貴的,乏味的,而且可能不接近實時。
所以,這就是深度學習和cnn發揮作用的地方。如果我們的模型足夠精確,我們可以通過在野外設置麥克風來自動記錄鳥類遷徙模式和追蹤鳥類種類。有了這些數據,我們可以更深入地了解世界的生態系統,並充分了解氣候變化對環境的影響。
那麼,上面這張圖是什麼呢?
讓我們先談談聲音。聲音基本上是由奇特的壓力波組成的,這些壓力波通過空氣進入我們的耳膜,可以被數字設備作為信號記錄下來。結果表明,我們可以將這個信號繪製如下圖:
然而,這種長線圖看起來並不是特別適合用於任何深度學習模型,更不用說CNN了。事實上,如果我現在告訴你,上面的音頻信號代表了一隻鳥的鳴叫,你會相信我嗎?你反而會認為這是我隨意創作的情節。如果你認為鳥鳴聲是在第10000 波動點 左右,如果我說鳥鳴聲是在正中間在第6000波動點?
那麼,如果我們自己都不能做出任何強有力的假設,那麼深度學習模型又如何能做到這一點呢?
我們並沒有完全陷入困境。實際上還有另一種表示聲音的方法:聲譜圖。頻譜圖是通過在一個小時間窗口中記錄頻率的存在和各自的強度,沿著x軸(用時間單位表示)堆疊,直到頻譜圖覆蓋音頻信號的總持續時間。在創建譜圖的過程中,時間窗本身會發生重疊,通常頻率強度(音量或響度)用顏色表示,或者用數字來表示高/低值。
從上面所示的完全相同的波形中鍛造出的光譜圖。x軸、y軸和標繪顏色分別表示時間單位、頻率(赫茲)和頻率強度。現在,你能清楚地分辨出鳥兒是否鳴叫以及何時鳴叫嗎?
如果有一個來自上面的信號的光譜圖,那麼鳥兒是否鳴叫以及何時鳴叫就會清楚得多。具體來說,「只是因為」,你可以看到獨特的活動大約6000赫茲在250毫秒的馬克;那是鳥的叫聲。因此,CNN音頻分類器經常以光譜圖作為輸入,鳥叫聲的音頻檢測模型也不例外。
準備
為此,我使用了BirdVox的BirdVox-70k數據集,該數據集包含半秒(500ms)的錄音/波形,其中包含或不包含鳥叫聲。數據集來自於它的大哥BirdVox-full-night,這兩種鳥都是在2015年秋天在美國紐約州的Ithaca被記錄的。每個波形的標籤包含在它的每個文件名作為最後一個數字(0或1)。儘管70 k數據集擁有大約70000的視頻數據(因此得名),我只會用三分之二的(更具體地說,50000左右)。
數據被捆綁在獨立的HDF5文件中,這意味著我計劃使用的已經減少的50,000個樣本實際上被分割成4個不同的。HDF5文件。每個波形都被存儲成一組,有它自己的「文件名」標籤,所有這些都被存儲到稱為「波形」的另一組。
即使在閱讀了h5py文檔之後,我也沒有看到像上面描述的那樣的開發。這些發現已經讓我做了兩個最糟糕的數據準備的噩夢。因為存儲空間的原因,我無法改變已經給我的數據的格式。這給我留下了最後一個選項:創建一個非常自定義的PyTorch數據集類DataSet,這樣我就可以繼續工作。
但在我繼續之前,讓我們聽一些簡短的音頻樣本,以及一些簡單的數據分析數據集(警告-降低你的音量,因為樣本可能比預期的更大):
在把所有的文件組合在一起之後,bird-positive音頻樣本的比例正好是50%,就像他們網站上承諾的那樣。每個鳥正波形的文件名還包括鳥叫聲的頻率。把所有這些整理在一起,我發現如下:
很多電話的音調會很高。大部分的樣本是這樣的,但是絕大多數實際上更接近較低的頻率(2000-3500赫茲)。很奇怪!
Dataset & Dataloader類
我們知道有一些獨立的。hdf5文件要處理成一個「數據集」,而且每個文件都有一些奇怪的數據結構。這些條件肯定需要一個自定義PyTorch Dataset類來正確加載音頻數據,以便以後進行訓練。Dataset類主要需要剩下的__init__和__getitem__函數(還有剩下的__len__,但這很簡單)。以下是我計劃要做的事情:
__init__
遍歷所有四個文件中的每個波形的每個組名,並將其所屬的文件和HDF5組追加到屬於該類的列表中。
獲取波形的標籤(文件/組名的最後一位)並將其附加到屬於該類的另一個列表中。例如,文件名unit0301271222800000_0表示沒有鳥叫聲。
準備一個變換函數,應用到波形,將其轉換為光譜圖(具體地說,mel-scaled的光譜圖),並考慮到其他增強技術。
__getitem__
為在初始化之時創建的列表提供索引
一旦通過列表接收到波形的位置,打開該波形的HDF5文件。所有的HDF5 I/O都將使用python庫h5py來處理把它變成PyTorch張量並應用任何變換,包括譜圖變換。
將項目返回給調用者
下面,Dataset類的代碼:
class BirdVox70kDS(Dataset):def __init__(self, root_dir, fnames, transforms=None): # store transforms func self.transforms = transforms # initialize storage arrays self.wave_loc = [] self.labels = [] # for each hdf5 file... for fname in fnames: # open the file fhdf5 = os.path.join(root_dir, fname) with h5py.File(fhdf5, 'r') as f: # navigate to `waveforms` group waveforms = f['waveforms'] # for each piece of data... for waveform in waveforms.keys(): # append waveform filename for later access self.wave_loc.append([fhdf5, waveform]) # (label == last digit of filename) self.labels.append(waveform[-1]) # turn them into np.arrays self.wave_loc = np.array(self.wave_loc) self.labels = np.array(self.labels) # melspec transform (similar to `librosa.feature.melspectrogram()`) self.melspec = T.MelSpectrogram(sample_rate=24000, n_fft=2048, hop_length=512) def __len__(self): # size of dataset return len(self.labels) def __getitem__(self, idx): # fetch waveform from hdf5 file & label fhdf5, waveform = self.wave_loc[idx] with h5py.File(fhdf5, 'r') as f: wave = f['waveforms'][waveform] # convert to np array for faster i/o performance wave = np.array(wave) # apply other specified transforms if self.transforms: wave = self.transforms()(wave) # convert into tensor & apply melspec wave = self.melspec(torch.Tensor(wave)) # unsqueeze adds dimension needed for pytorch's `Conv2d` wave = wave.unsqueeze(0) # parse label (still a string) label = self.labels[idx] return wave, int(label)
我們定義數據集文件的目錄和文件名本身。我決定使用4個文件中的3個作為測試數據,最後一個作為驗證/測試集來度量模型的性能,為後者留下最小的文件。由於每個文件的所有記錄設備都設置的比較近(在伊薩卡的同一個城市),我認為繞過隨機分割不會引入大量的偏差。
root_dir = '/notebooks/storage/'fnames = ['BirdVox-70k_unit03.hdf5','BirdVox-70k_unit07.hdf5', 'BirdVox-70k_unit10.hdf5']
train_ds = BirdVox70kDS(root_dir, fnames)
val_ds = BirdVox70kDS(root_dir, ['BirdVox-70k_unit01.hdf5'])
在我們的數據集之後,下面就是使用dataloaders了:
batch_size = 128
train_dl = DataLoader(train_ds, batch_size, shuffle=True, pin_memory=True)val_dl = DataLoader(val_ds, batch_size * 2, pin_memory=True)# having `num_workers > 1` will crash dataloader when working w/ h5 files
定義dataloaders,它將分批返回數據。在使用PyTorch和HDF5文件時,我嘗試過設置多個「num_workers」,但發現存在一個bug
模型
我為我的模型設置了必要的輔助函數,以便以後進行訓練:
class ModelBase(nn.Module):# defines mechanism when training each batch in dl def train_step(self, batch): xb, labels = batch outs = self(xb) loss = F.cross_entropy(outs, labels) return loss # similar to `train_step`, but includes acc calculation & detach def val_step(self, batch): xb, labels = batch outs = self(xb) loss = F.cross_entropy(outs, labels) acc = accuracy(outs, labels) return {'loss': loss.detach(), 'acc': acc.detach()} # average out losses & accuracies from validation epoch def val_epoch_end(self, outputs): batch_loss = [x['loss'] for x in outputs] batch_acc = [x['acc'] for x in outputs] avg_loss = torch.stack(batch_loss).mean() avg_acc = torch.stack(batch_acc).mean() return {'avg_loss': avg_loss, 'avg_acc': avg_acc} # print all data once done def epoch_end(self, epoch, avgs, test=False): s = 'test' if test else 'val' print(f'Epoch #{epoch + 1}, {s}_loss:{avgs["avg_loss"]}, {s}_acc:{avgs["avg_acc"]}')
定義多個函數,以後可以使用這些函數訓練繼承這個類的PyTorch模型。
def accuracy(outs, labels):_, preds = torch.max(outs, dim=1) return torch.tensor(torch.sum(preds == labels).item() / len(preds))
這個函數在上述類的val_step函數中被用來確定驗證dataloader上模型的%準確性。
並定義用於擬合/訓練模型和在驗證數據集上測試模型的主要功能
@torch.no_grad()def evaluate(model, val_dl):# eval mode model.eval() outputs = [model.val_step(batch) for batch in val_dl] return model.val_epoch_end(outputs)def fit(epochs, lr, model, train_dl, val_dl, opt_func=torch.optim.Adam): torch.cuda.empty_cache() history = [] # define optimizer optimizer = opt_func(model.parameters(), lr) # for each epoch... for epoch in range(epochs): # training mode model.train() # (training) for each batch in train_dl... for batch in tqdm(train_dl): # pass thru model loss = model.train_step(batch) # perform gradient descent loss.backward() optimizer.step() optimizer.zero_grad() # validation res = evaluate(model, val_dl) # print everything useful model.epoch_end(epoch, res, test=False) # append to history history.append(res) return history
最後,這是我們等待已久的簡單CNN模型:
class Classifier(ModelBase):def __init__(self): super().__init__() # 1 x 128 x 24 self.conv1 = nn.Conv2d(1, 4, kernel_size=3, padding=1) # 4 x 128 x 24 self.conv2 = nn.Conv2d(4, 8, kernel_size=3, padding=1) # 8 x 128 x 24 self.bm1 = nn.MaxPool2d(2) # 8 x 64 x 12 self.conv3 = nn.Conv2d(8, 8, kernel_size=3, padding=1) # 8 x 64 x 12 self.bm2 = nn.MaxPool2d(2) # 8 x 32 x 6 self.fc1 = nn.Linear(8*32*6, 64) self.fc2 = nn.Linear(64, 2) def forward(self, xb): out = F.relu(self.conv1(xb)) out = F.relu(self.conv2(out)) out = self.bm1(out) out = F.relu(self.conv3(out)) out = self.bm2(out) out = torch.flatten(out, 1) out = F.relu(self.fc1(out)) out = self.fc2(out) return out
我使用了多個卷積層,正如我們之前的理論推斷所建議的那樣,我們的模型使用了一些最大池化層,然後使用一個非常簡單的全連接網絡來進行實際的分類。令人驚訝的是,這個架構後來表現得相當好,甚至超過了我自己的預期。
利用GPU
幾乎每個人都需要GPU來訓練比一般的前饋神經網絡更複雜的東西。幸運的是,PyTorch讓我們可以很容易地利用現有GPU的能力。首先,我們將我們的cuda設備定義為關鍵詞設備,以便更容易訪問:
device = 'cuda' if torch.cuda.is_available() else 'cpu'
我們還確保如果沒有GPU, CPU會被使用。
這裡還有另一個技巧:
torch.backends.cudnn.benchmark = True
這可以幫助提高你的訓練速度(如果你的輸入在大小/形狀上沒有變化)
顯然,你可以「告訴」PyTorch在一次又一次的訓練中優化自己,只要訓練輸入在大小和形狀上保持不變。它會知道為你的特定硬體(GPU)使用最快的算法。!
然後我們定義幫助函數來移動dataloaders、tensors和我們的模型到我們的GPU設備。
def to_device(data, device):"""Move tensor(s) to chosen device""" if isinstance(data, (list, tuple)): return [to_device(x, device) for x in data] return data.to(device, non_blocking=True)class DeviceDataLoader(): """Wrap a dataloader to move data to a device""" def __init__(self, dl, device): self.dl = dl self.device = device def __iter__(self): """Yield a batch of data after moving it to device""" for b in self.dl: yield to_device(b, self.device) def __len__(self): """Number of batches""" return len(self.dl)
訓練
使用我們的設備輔助功能,我們將一切移動到我們的GPU如下:
train_dl = DeviceDataLoader(train_dl, device)val_dl = DeviceDataLoader(val_dl, device)model = to_device(Classifier(), device)
我們還指定了我們的學習速度和我們的訓練次數,我花了很長時間來找到一個好的值。
lr = 1e-5epochs = 8
在進行任何訓練之前,我們會發現模型的表現:
history = [evaluate(model, val_dl)]
[{'avg_loss': tensor(0.7133, device='cuda:0'), 'avg_acc': tensor(0.6042)}]
很顯然,我們在開始時的準確性上有點幸運,但經過多次試驗,最終結果即使不相同,也會非常相似。
接下來,我們使用之前定義的fit函數來訓練我們的簡單分類器模型實例:
history += fit(epochs, lr, model, train_dl, val_dl)
HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value='')))Epoch #1, val_loss:0.6354132294654846, val_acc:0.7176136374473572 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #2, val_loss:0.6065077781677246, val_acc:0.7439352869987488 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #3, val_loss:0.56722491979599, val_acc:0.77376788854599 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #4, val_loss:0.5528884530067444, val_acc:0.7751822471618652 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #5, val_loss:0.5130119323730469, val_acc:0.8004600405693054 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #6, val_loss:0.4849482774734497, val_acc:0.8157732486724854 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #7, val_loss:0.4655478596687317, val_acc:0.8293880224227905 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #8, val_loss:0.4765000343322754, val_acc:0.8155447244644165
在發現精確度有一點下降後,我決定再訓練一點點,讓模型重定向回到正確的方向:
history += fit(3, 1e-6, model, train_dl, val_dl)
HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value='')))Epoch #1, val_loss:0.4524107873439789, val_acc:0.8329823613166809 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #2, val_loss:0.44666698575019836, val_acc:0.8373703360557556 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #3, val_loss:0.4442901611328125, val_acc:0.8412765860557556
忽略「HBox」工件,這是tqdm提供的,請多關注準確性!
總結一下,以下是我們過去11個時期的訓練統計數據:
plt.plot([x['avg_loss'] for x in history])plt.title('Losses over epochs')plt.xlabel('epochs')plt.ylabel('loss')plt.show()
plt.plot([x['avg_acc'] for x in history])plt.title('Accuracy over epochs')plt.xlabel('epochs')plt.ylabel('acc')plt.show()
總的來說,我們的模型訓練得相當好,從它的外觀來看,我們可能已經為我們的模型的損失找到了一個相對最小的值。
等等,一個更複雜的模型或者使用不同的轉換怎麼樣?
相信我,在我的simple Classifier()第一次嘗試成功之後不久,我也嘗試過這兩種方法。我決定不包括這兩個細節,因為我發現他們的結果實際上比我們已經取得的結果更糟糕,這很奇怪。
對於額外的譜圖轉換,我嘗試了隨機時移和噪聲注入。長話短說,它似乎根本沒有提高驗證的準確性。此後,我認為是這樣,因為數據集規範明確表示,所有啾啾將位於中間的錄音,因此隨機變化的光譜圖的目的允許更好的模型泛化實際上可能已經作為損害的表現。然而,我還沒有嘗試過隨機噪聲注入。
我還嘗試訓練一個ResNet50模型,希望進一步提高驗證的準確性。這是最令人難以置信的部分:我的模型從來沒有超過50%的準確率!直到今天我寫這篇文章的時候,我還不確定我做錯了什麼,所以如果其他人能看看筆記本並幫助我,我很高興收到任何建議!
結論
總而言之,這是一個真正有趣的努力,花一些時間進行研究。首先,我得重新審視我去年夏天調查過的東西,無可否認,這有一種懷舊的感覺。更重要的是,我們學習了如何實現一個很可能用於真實場景的PyTorch數據集類,在真實場景中,數據不一定像您預期的那樣設置。最後,最終的驗證分數為84%,對於我即興創建的如此簡單的網絡架構來說,這是相當整潔的!
作者:Richard So
deephub翻譯組