網(wǎng)絡(luò)的城市聲音分類模型:從數(shù)據(jù)到部署全鏈路實(shí)戰(zhàn))
簡介這份PDF文獻(xiàn)面向從事音頻信號處理、環(huán)境聲音識別與深度學(xué)習(xí)應(yīng)用的研究生、算法工程師及科研人員系統(tǒng)探討了如何利用城市生態(tài)環(huán)境中的聲音信息提升分類精度。全文圍繞五種音頻特征展開包括Mel頻率倒譜系數(shù)、Mel圖譜、頻譜質(zhì)心、色度圖譜與光譜對比度并構(gòu)建深度神經(jīng)網(wǎng)絡(luò)模型完成城市環(huán)境聲音分類實(shí)驗(yàn)分類精度達(dá)88.6%優(yōu)于基于Mel頻率倒譜系數(shù)的基本方法同時(shí)提出基于卷積神經(jīng)網(wǎng)絡(luò)的聲音分類模型評估連續(xù)小卷積核網(wǎng)絡(luò)對短音頻城市環(huán)境聲音的分類潛力并與同類方法進(jìn)行對比。資源包為1個(gè)PDF文件大小約4.13MB內(nèi)容完整涵蓋研究背景、特征提取、模型設(shè)計(jì)、實(shí)驗(yàn)結(jié)果與未來工作等章節(jié)便于讀者快速把握城市聲音分類的技術(shù)路線與實(shí)驗(yàn)思路。目前已有201人學(xué)習(xí)下載適合作為深度學(xué)習(xí)與機(jī)器學(xué)習(xí)方向數(shù)據(jù)建模的參考案例。1. 城市聲音分類模型從一段街頭錄音到可部署的深度神經(jīng)網(wǎng)絡(luò)你手里有一段 10 秒的城市街頭錄音里面有汽車鳴笛、人聲嘈雜、施工電鉆、鳥叫甚至還有地鐵進(jìn)站的轟鳴。人耳能瞬間分辨出「這是路口」「這是公園」「這是工地」但要讓機(jī)器自動(dòng)打上標(biāo)簽就沒那么簡單了?;谏疃壬窠?jīng)網(wǎng)絡(luò)的城市聲音分類模型研究本質(zhì)上就是解決這個(gè)問題把非平穩(wěn)、強(qiáng)噪聲、多標(biāo)簽共存的音頻片段映射到預(yù)定義的聲學(xué)場景或事件類別上。它適合做環(huán)境監(jiān)測、智慧城市噪聲治理、助聽器場景自適應(yīng)、短視頻自動(dòng)配樂標(biāo)簽也適合作為深度學(xué)習(xí)入門到落地的完整練手項(xiàng)目。我見過太多人卡在「模型跑通但準(zhǔn)確率上不去」或者「換了數(shù)據(jù)集就崩」這兩步這篇就把從數(shù)據(jù)到推理的整條鏈路拆開講。2. 城市聲音分類的數(shù)據(jù)集與特征工程為什么你的模型總在 60% 準(zhǔn)確率徘徊2.1 常用數(shù)據(jù)集選型與標(biāo)簽體系設(shè)計(jì)城市聲音分類不是只有 UrbanSound8K 一個(gè)選擇。我一般會先看任務(wù)粒度如果只分「交通/自然/人聲/音樂」這種粗粒度UrbanSound8K 的 10 類夠用如果要細(xì)分到「警笛 vs 倒車提示音」就得上 AudioSet 的子集或者自己標(biāo)注。UrbanSound8K 有 8732 條音頻最長 4 秒采樣率 44.1kHz分 10 類空調(diào)、汽車鳴笛、兒童玩耍、狗叫、鉆孔、發(fā)動(dòng)機(jī)怠速、槍擊、手持電鉆、警笛、街頭音樂。它的官方劃分是 10 折交叉驗(yàn)證但很多人直接拿 fold1 訓(xùn)練 fold2 測試結(jié)果虛高——因?yàn)橥欢卧间浺舯磺谐闪硕鄠€(gè)片段分到不同折里就會泄漏。常見做法是先按原始錄音 ID 分組再做 train/val/test 劃分比例 7:1.5:1.5。標(biāo)簽體系上如果一條音頻里同時(shí)有狗叫和汽車聲要么做多標(biāo)簽sigmoid BCE要么按主導(dǎo)事件單標(biāo)簽。我一般會先做單標(biāo)簽因?yàn)槌鞘新曇衾镏鲗?dǎo)事件通常占能量 70% 以上單標(biāo)簽夠用且訓(xùn)練穩(wěn)定。提示UrbanSound8K 的 metadata 里有個(gè)salience字段表示該片段中目標(biāo)事件的顯著程度。訓(xùn)練時(shí)可以把 salience1 的樣本權(quán)重調(diào)高能漲 2~3 個(gè)點(diǎn)。2.2 從波形到 Mel 頻譜圖參數(shù)怎么設(shè)才不翻車深度神經(jīng)網(wǎng)絡(luò)不直接吃原始波形除非你用 WaveNet 那類一維卷積主流做法是轉(zhuǎn)成 Mel 頻譜圖。這里參數(shù)設(shè)錯(cuò)后面怎么調(diào)模型都白搭。我一般用 librosa 做前端核心參數(shù)就四個(gè)sr22050、n_fft2048、hop_length512、n_mels128。為什么是 22050因?yàn)槌鞘新曇舻闹饕芰吭?0~11kHz22050 采樣率剛好覆蓋 Nyquist 頻率再高就是浪費(fèi)算力。n_fft2048對應(yīng)約 93ms 窗長對城市聲音這種瞬態(tài)事件夠用hop_length512約 23ms 幀移保證時(shí)間分辨率。import librosa import numpy as np def extract_mel_spectrogram(file_path, sr22050, n_fft2048, hop_length512, n_mels128, duration4.0): # 統(tǒng)一加載并截?cái)?填充到 4 秒 y, _ librosa.load(file_path, srsr, durationduration) target_len int(sr * duration) if len(y) target_len: y np.pad(y, (0, target_len - len(y)), modeconstant) else: y y[:target_len] # 轉(zhuǎn) Mel 頻譜取對數(shù) dB mel librosa.feature.melspectrogram(yy, srsr, n_fftn_fft, hop_lengthhop_length, n_melsn_mels) log_mel librosa.power_to_db(mel, refnp.max) # 歸一化到 [-1, 1]這是 CNN 收斂的關(guān)鍵 log_mel (log_mel - log_mel.min()) / (log_mel.max() - log_mel.min() 1e-8) * 2 - 1 return log_mel # shape: (128, 173)這段代碼里duration4.0是跟 UrbanSound8K 對齊的如果你用自己數(shù)據(jù)建議先統(tǒng)計(jì)音頻長度分布取 95 分位數(shù)作為統(tǒng)一長度。power_to_db的refnp.max讓每條音頻獨(dú)立歸一化避免音量差異導(dǎo)致模型學(xué)偏。最后歸一化到 [-1,1] 比 [0,1] 在 BN 層前更穩(wěn)這是我踩過坑之后固定下來的習(xí)慣。2.3 數(shù)據(jù)增強(qiáng)城市噪聲場景下的三個(gè)有效手段城市聲音分類模型最容易過擬合因?yàn)闃?biāo)注數(shù)據(jù)少、背景噪聲重復(fù)。我常用的增強(qiáng)就三個(gè)加性高斯噪聲SNR 5~15dB、時(shí)間平移±20%、SpecAugment頻率掩蔽和時(shí)間掩蔽各 2 個(gè)寬度 10~20 幀。SpecAugment 在頻譜圖上直接操作比波形增強(qiáng)更貼近模型輸入分布。注意不要用音高偏移城市聲音的頻域結(jié)構(gòu)很敏感移調(diào)后警笛就不像警笛了。def spec_augment(spec, freq_mask_num2, time_mask_num2, freq_mask_width15, time_mask_width20): spec spec.copy() n_mels, n_frames spec.shape for _ in range(freq_mask_num): f np.random.randint(0, freq_mask_width) f0 np.random.randint(0, n_mels - f) spec[f0:f0f, :] 0 for _ in range(time_mask_num): t np.random.randint(0, time_mask_width) t0 np.random.randint(0, n_frames - t) spec[:, t0:t0t] 0 return spec參數(shù)上freq_mask_width15對應(yīng)約 1.2kHz 帶寬time_mask_width20約 0.46 秒這個(gè)強(qiáng)度在 UrbanSound8K 上驗(yàn)證過再大就欠擬合。增強(qiáng)只在訓(xùn)練時(shí)做驗(yàn)證和測試保持原始頻譜。3. 深度神經(jīng)網(wǎng)絡(luò)選型與訓(xùn)練CNN、CRNN 還是 Transformer3.1 從 VGG 到 CRNN城市聲音分類的骨干網(wǎng)絡(luò)對比城市聲音分類的骨干網(wǎng)絡(luò)演進(jìn)很清晰早期用 VGG 式 CNN 堆卷積后來發(fā)現(xiàn)聲音事件有時(shí)間先后關(guān)系就加了 RNN 做 CRNN再后來 Transformer 也進(jìn)來了。我實(shí)測下來在 UrbanSound8K 上一個(gè) 4 層 CNN 2 層 BiGRU 的 CRNN 能到 78~82% 準(zhǔn)確率比純 CNN 高 3~5 個(gè)點(diǎn)參數(shù)量還少。純 Transformer 需要更多數(shù)據(jù)8732 條不夠容易過擬合。網(wǎng)絡(luò)類型參數(shù)量UrbanSound8K 準(zhǔn)確率訓(xùn)練時(shí)間單卡適合場景VGG-like CNN5.2M74~77%20 min快速基線ResNet-1811.2M76~79%35 min遷移學(xué)習(xí)CRNN (CNNBiGRU)3.8M78~82%45 min推薦方案Audio Transformer25M75~80%90 min大數(shù)據(jù)集CRNN 的結(jié)構(gòu)是4 個(gè)卷積塊每個(gè)含 Conv2d BN ReLU MaxPool把 128×173 的頻譜圖壓到 8×5×256然后 reshape 成時(shí)間序列送進(jìn) BiGRUhidden128最后全連接分類。卷積塊負(fù)責(zé)提取局部頻域模式GRU 負(fù)責(zé)建模時(shí)間依賴比如警笛的周期性起伏。3.2 用 PyTorch 搭一個(gè)可復(fù)現(xiàn)的 CRNN 訓(xùn)練腳本下面這個(gè)腳本是我在多個(gè)項(xiàng)目里復(fù)用過的關(guān)鍵點(diǎn)都標(biāo)了注釋。數(shù)據(jù)集用自定義 Dataset 加載前面提取的 Mel 頻譜訓(xùn)練用 AdamW CosineAnnealing混合精度用 amp 省顯存。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader from torch.cuda.amp import autocast, GradScaler class UrbanSoundDataset(Dataset): def __init__(self, file_list, label_list, augmentFalse): self.file_list file_list self.label_list label_list self.augment augment def __len__(self): return len(self.file_list) def __getitem__(self, idx): spec extract_mel_spectrogram(self.file_list[idx]) # (128, 173) if self.augment: spec spec_augment(spec) spec torch.tensor(spec, dtypetorch.float32).unsqueeze(0) # (1, 128, 173) label torch.tensor(self.label_list[idx], dtypetorch.long) return spec, label class CRNN(nn.Module): def __init__(self, num_classes10): super().__init__() self.cnn nn.Sequential( nn.Conv2d(1, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(256, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d((2,2)), ) self.gru nn.GRU(256*8, 128, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(256, num_classes) def forward(self, x): # x: (B, 1, 128, 173) x self.cnn(x) # (B, 256, 8, 10) B, C, F, T x.shape x x.permute(0, 3, 1, 2).reshape(B, T, C*F) # (B, 10, 2048) x, _ self.gru(x) # (B, 10, 256) x x.mean(dim1) # 時(shí)間維平均池化 return self.fc(x) # 訓(xùn)練循環(huán) def train_one_epoch(model, loader, optimizer, criterion, scaler, device): model.train() total_loss 0 for spec, label in loader: spec, label spec.to(device), label.to(device) optimizer.zero_grad() with autocast(): out model(spec) loss criterion(out, label) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() total_loss loss.item() return total_loss / len(loader)關(guān)鍵參數(shù)說明GRU輸入維度是256*8因?yàn)榫矸e輸出是 256 通道、8 個(gè)頻帶把頻帶維展平后每個(gè)時(shí)間步是 2048 維。時(shí)間維平均池化比取最后一步更穩(wěn)因?yàn)槌鞘新曇舻氖录赡艹霈F(xiàn)在任意位置。優(yōu)化器用AdamW(lr1e-3, weight_decay1e-4)配合CosineAnnealingLR(T_max50)50 個(gè) epoch 足夠收斂?;旌暇扔?xùn)練在 RTX 3060 上 batch_size32 只占 4GB 顯存。3.3 訓(xùn)練曲線診斷準(zhǔn)確率不漲時(shí)先看這三處訓(xùn)練時(shí)如果驗(yàn)證準(zhǔn)確率卡在 60% 左右不動(dòng)按順序查第一看訓(xùn)練 loss 是否下降。如果訓(xùn)練 loss 也不降大概率是學(xué)習(xí)率太大或數(shù)據(jù)歸一化有問題把 lr 降到 1e-4 試試。第二看訓(xùn)練 loss 降但驗(yàn)證 loss 升這是過擬合加 dropoutGRU 后加 0.3或增強(qiáng)強(qiáng)度。第三看混淆矩陣如果某兩類互相混比如「鉆孔」和「手持電鉆」那是頻域特征太像考慮加 MFCC 的一階差分作為額外通道。我一般會在訓(xùn)練腳本里每 5 個(gè) epoch 存一次混淆矩陣圖比只看準(zhǔn)確率有用得多。4. 模型評估與推理部署從驗(yàn)證集到真實(shí)街頭錄音4.1 評估指標(biāo)準(zhǔn)確率之外必須看的兩個(gè)數(shù)城市聲音分類不能只看準(zhǔn)確率因?yàn)轭悇e不平衡很常見。UrbanSound8K 里「槍擊」只有 374 條「汽車鳴笛」有 1000 多條。我一般同時(shí)看 macro-F1 和 AUC。macro-F1 對少數(shù)類敏感AUC 看排序能力。如果 macro-F1 比準(zhǔn)確率低 10 個(gè)點(diǎn)以上說明模型偏向多數(shù)類得用類別權(quán)重或者 focal loss。推理時(shí)還要看 top-3 準(zhǔn)確率因?yàn)槌鞘新曇舫S卸鄻?biāo)簽共存top-3 能到 95% 以上就說明模型學(xué)到了有意義的表示。from sklearn.metrics import f1_score, roc_auc_score def evaluate(model, loader, device): model.eval() all_preds, all_labels, all_probs [], [], [] with torch.no_grad(): for spec, label in loader: spec spec.to(device) out model(spec) prob torch.softmax(out, dim1) all_probs.append(prob.cpu().numpy()) all_preds.extend(out.argmax(dim1).cpu().numpy()) all_labels.extend(label.numpy()) all_probs np.concatenate(all_probs, axis0) macro_f1 f1_score(all_labels, all_preds, averagemacro) auc roc_auc_score(all_labels, all_probs, multi_classovr) return macro_f1, auc4.2 導(dǎo)出 ONNX 并用 onnxruntime 推理延遲能壓到 15ms訓(xùn)練完的 PyTorch 模型要部署最順的路是導(dǎo)出 ONNX。注意導(dǎo)出時(shí)要把模型設(shè)為 eval 模式并且提供一個(gè) dummy input。ONNX 的好處是跨平臺C、Python、甚至瀏覽器都能跑。我實(shí)測 CRNN 導(dǎo)出后在 CPU 上單條 4 秒音頻推理約 15msGPU 上 3ms 以內(nèi)完全滿足實(shí)時(shí)場景。import torch.onnx model CRNN(num_classes10) model.load_state_dict(torch.load(crnn_best.pth, map_locationcpu)) model.eval() dummy_input torch.randn(1, 1, 128, 173) torch.onnx.export( model, dummy_input, crnn_urbansound.onnx, input_names[mel_spectrogram], output_names[logits], dynamic_axes{mel_spectrogram: {0: batch}, logits: {0: batch}}, opset_version11 )導(dǎo)出后一定要用 onnxruntime 跑一遍驗(yàn)證輸出和 PyTorch 一致誤差在 1e-4 以內(nèi)才算成功。opset_version11兼容性最好別用太新的版本有些推理引擎不支持。4.3 真實(shí)街頭錄音的域偏移問題與緩解驗(yàn)證集 80% 準(zhǔn)確率拿到真實(shí)街頭錄音可能掉到 50%。原因是域偏移訓(xùn)練數(shù)據(jù)是 YouTube 上剪的真實(shí)錄音有風(fēng)噪、遠(yuǎn)場混響、手機(jī)麥克風(fēng)頻響。緩解手段有三個(gè)一是推理前做譜減降噪用 noisereduce 庫二是用測試時(shí)增強(qiáng)TTA對同一條音頻做 3 次不同偏移的推理再平均三是收集少量真實(shí)場景數(shù)據(jù)做微調(diào)哪怕每類只有 20 條也能漲 10 個(gè)點(diǎn)以上。我一般會先上 TTA成本最低。5. 避坑與排查城市聲音分類模型最常見的五個(gè)翻車現(xiàn)場5.1 數(shù)據(jù)泄漏同一段錄音切分后進(jìn)了訓(xùn)練和驗(yàn)證集現(xiàn)象驗(yàn)證準(zhǔn)確率 95%測試集只有 60%。原因UrbanSound8K 的切片來自同一原始錄音隨機(jī)劃分導(dǎo)致泄漏。解決按slice_file_name里的原始 ID 分組劃分用GroupShuffleSplit。這個(gè)坑我見過至少五次每次都是「準(zhǔn)確率虛高」的元兇。5.2 采樣率不統(tǒng)一訓(xùn)練用 22050推理用 44100現(xiàn)象推理結(jié)果全是某一類softmax 輸出接近均勻分布。原因推理時(shí)沒重采樣Mel 濾波器組頻率對不上。解決推理前端強(qiáng)制librosa.load(sr22050)并在服務(wù)啟動(dòng)時(shí)打印實(shí)際采樣率做斷言。5.3 BatchNorm 在 batch_size1 時(shí)崩潰現(xiàn)象單條推理時(shí)輸出亂跳batch 推理正常。原因BN 在 eval 模式用 running_mean但如果訓(xùn)練時(shí) batch_size 太小running_mean 估計(jì)不準(zhǔn)。解決訓(xùn)練時(shí) batch_size 至少 16或者把 BN 換成 GroupNorm。我一般在 CRNN 里用 GroupNorm(8)對 batch 不敏感。5.4 頻譜圖歸一化不一致訓(xùn)練用全局推理用單條現(xiàn)象驗(yàn)證集正常部署后準(zhǔn)確率掉 20%。原因訓(xùn)練時(shí)用了數(shù)據(jù)集全局 min/max推理時(shí)用了單條音頻的 min/max。解決把訓(xùn)練集的 min/max 存成配置文件推理時(shí)加載同一組值。或者干脆都用單條歸一化但訓(xùn)練和推理必須一致。5.5 類別權(quán)重設(shè)反少數(shù)類權(quán)重過大導(dǎo)致多數(shù)類全錯(cuò)現(xiàn)象macro-F1 漲了但準(zhǔn)確率暴跌。原因focal loss 的 alpha 或類別權(quán)重給少數(shù)類太高模型把所有樣本都預(yù)測成少數(shù)類。解決權(quán)重按1/log(類別頻率)設(shè)別用1/頻率后者太激進(jìn)。我一般先用無權(quán)重跑一版看混淆矩陣再決定要不要加權(quán)。6. 進(jìn)階技巧用遷移學(xué)習(xí)和知識蒸餾把準(zhǔn)確率推到 90%如果你已經(jīng)把 CRNN 跑到 82%想再往上最劃算的路是遷移學(xué)習(xí)。拿 AudioSet 上預(yù)訓(xùn)練的 PANNsPretrained Audio Neural Networks做特征提取器只訓(xùn)練最后的分類頭在 UrbanSound8K 上能到 88~90%。PANNs 的 CNN14 在 AudioSet 上見過 200 萬條音頻頻域特征提取能力比從零訓(xùn)強(qiáng)太多。具體做法加載 PANNs 的 CNN14去掉最后的分類層輸出 2048 維嵌入接一個(gè)兩層 MLP2048→512→10只訓(xùn)練 MLP 和最后兩個(gè)卷積塊學(xué)習(xí)率設(shè) 1e-420 個(gè) epoch 就收斂。# 偽代碼示意PANNs 需從官方倉庫獲取 panns load_panns_cnn14(pretrainedTrue) for param in panns.parameters(): param.requires_grad False # 只解凍最后兩個(gè)卷積塊 for param in panns.conv_block6.parameters(): param.requires_grad True for param in panns.conv_block5.parameters(): param.requires_grad True class TransferModel(nn.Module): def __init__(self, panns, num_classes10): super().__init__() self.backbone panns self.head nn.Sequential( nn.Linear(2048, 512), nn.ReLU(), nn.Dropout(0.3), nn.Linear(512, num_classes) ) def forward(self, x): emb self.backbone(x) # (B, 2048) return self.head(emb)另一個(gè)技巧是知識蒸餾用 PANNs 當(dāng)教師模型CRNN 當(dāng)學(xué)生教師輸出的 soft label 帶溫度 T4學(xué)生同時(shí)學(xué)硬標(biāo)簽和軟標(biāo)簽損失是0.3*CE(hard) 0.7*KL(soft)。這樣學(xué)生 CRNN 能到 85%參數(shù)量只有教師的 1/6適合端側(cè)部署。我去年在一個(gè)環(huán)境監(jiān)測項(xiàng)目里就是這么干的樹莓派上跑蒸餾后的 CRNN單次推理 40ms準(zhǔn)確率 84%甲方驗(yàn)收一次過。最后說個(gè)血淚教訓(xùn)別在模型結(jié)構(gòu)上反復(fù)魔改先把數(shù)據(jù)質(zhì)量、劃分方式、歸一化一致性這三件事做對比換什么注意力機(jī)制都管用。我見過太多人花兩周調(diào)網(wǎng)絡(luò)結(jié)果發(fā)現(xiàn)是驗(yàn)證集泄漏。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取