別實(shí)戰(zhàn):從GMM到深度學(xué)習(xí)全解析)
簡介聲紋識(shí)別作為生物識(shí)別的重要分支正廣泛應(yīng)用于金融風(fēng)控、智能安防和語音交互等場景。其技術(shù)演進(jìn)經(jīng)歷了從傳統(tǒng)概率建模到深度表征學(xué)習(xí)的躍遷早期GMM通過高斯混合分布刻畫個(gè)體發(fā)聲特征GMM-UBM借助通用背景模型實(shí)現(xiàn)少量數(shù)據(jù)下的快速自適應(yīng)ivector則進(jìn)一步將變長語音壓縮為固定維度身份向量。這些經(jīng)典方法雖然實(shí)現(xiàn)簡潔卻為理解說話人建模的底層邏輯奠定了堅(jiān)實(shí)基礎(chǔ)。隨著深度學(xué)習(xí)興起ResNet、TDNN等網(wǎng)絡(luò)與ArcFace損失函數(shù)結(jié)合實(shí)現(xiàn)了端到端的聲紋特征提取大幅提升了復(fù)雜環(huán)境下的魯棒性。本文基于Python開源工程系統(tǒng)拆解GMM-UBM、ivector與深度學(xué)習(xí)聲紋識(shí)別的原理、實(shí)現(xiàn)細(xì)節(jié)及踩坑經(jīng)驗(yàn)幫助開發(fā)者快速搭建從特征提取到模型比對(duì)的完整說話人識(shí)別系統(tǒng)。 開頭先聊一個(gè)點(diǎn)很多人看到“聲紋識(shí)別”四個(gè)字第一反應(yīng)是電影里那種黑科技——對(duì)著麥克風(fēng)說句話系統(tǒng)就能判斷你是不是本人。真實(shí)項(xiàng)目當(dāng)然沒有電影夸張但也沒有想象中那么神秘。這個(gè)項(xiàng)目標(biāo)題把Python、GMM、GMM-UBM、ivector、深度學(xué)習(xí)、聲紋識(shí)別這些關(guān)鍵詞都串在了一起還帶源碼和開發(fā)文檔說明作者想做一個(gè)完整的技術(shù)閉環(huán)從經(jīng)典統(tǒng)計(jì)模型一路做到深度學(xué)習(xí)方案而不是只搭一個(gè)Demo就完事。這類項(xiàng)目我做過幾次最深的體會(huì)是聲紋識(shí)別離不了特征、模型、打分這三件事。傳統(tǒng)的GMM和GMM-UBM能幫你理解“聲紋是怎么建模的”ivector能幫你體會(huì)“怎么把變長語音壓縮成一個(gè)固定向量”深度學(xué)習(xí)則是把“自動(dòng)提特征”這件事做到極致。這篇文章我按實(shí)際工程的推進(jìn)順序來寫先講清楚三條技術(shù)路線各自解決什么問題再給出可落地的源碼實(shí)現(xiàn)思路最后把我在實(shí)操里踩過的坑整理出來。對(duì)想入門說話人識(shí)別的朋友來說這應(yīng)該比直接扔給你一倉庫代碼更有參考價(jià)值。1. 技術(shù)路線全景從GMM到深度學(xué)習(xí)這條路是怎么走過來的1.1 三條路線的核心區(qū)別你建模的到底是什么傳統(tǒng)聲紋識(shí)別和深度學(xué)習(xí)聲紋識(shí)別最根本的區(qū)別不在于“誰更準(zhǔn)”而在于看待聲音的方式。早期的GMM模型本質(zhì)上是把人聲看成一種“分布”。每個(gè)人說話時(shí)聲帶的振動(dòng)頻率、共振峰位置、氣息力度都不一樣這些差異會(huì)體現(xiàn)在頻譜特征上。GMM做的就是把某個(gè)人所有語音幀的特征收集起來用一堆高斯分布去擬合這個(gè)人聲音的分布形狀。你說一句話我提取你的MFCC特征然后在你的GMM模型上算概率概率高就是本人。GMM-UBM是GMM的進(jìn)階版。它先拿大量不同人的語音訓(xùn)練一個(gè)通用背景模型UBM這個(gè)模型代表“普通人說話大概長什么樣”。新來一個(gè)說話人我不需要從零訓(xùn)練GMM只需要在UBM的基礎(chǔ)上用這個(gè)人的少量語音去做自適應(yīng)調(diào)整得到他專屬的模型。這種做法的好處非常明顯數(shù)據(jù)少也能建模且不容易過擬合。ivector則是換了一種思路。它不再為每個(gè)說話人單獨(dú)建模而是把所有說話人的差異壓縮到一個(gè)低維向量里。這個(gè)向量既包含說話人身份信息也包含信道信息所以叫“identity vector”。就像給每個(gè)人發(fā)了一張“聲紋身份證”但這張證上不僅有你的長相還有你今天戴沒戴帽子、換沒換手機(jī)的影響。到了深度學(xué)習(xí)時(shí)代神經(jīng)網(wǎng)絡(luò)直接把“從波形到身份特征”的映射端到端學(xué)出來了。你喂進(jìn)去一段語音網(wǎng)絡(luò)輸出一個(gè)embedding向量這個(gè)向量通過訓(xùn)練被強(qiáng)約束為“說話人相關(guān)的表示”。本質(zhì)目的和ivector一樣都是把變長語音變成固定維度向量但特征提取的能力比傳統(tǒng)方法強(qiáng)了一大截。1.2 為什么先做傳統(tǒng)方法再做深度學(xué)習(xí)我給接手這個(gè)項(xiàng)目的人一個(gè)建議哪怕你最終只想用深度學(xué)習(xí)方案也一定先把GMM-UBM和ivector跑通。原因有三個(gè)。第一傳統(tǒng)方法代碼簡單能幫你從特征提取到模型訓(xùn)練、打分建立起完整的pipeline概念這些概念在深度學(xué)習(xí)里依然適用。第二傳統(tǒng)方法的失敗模式比較規(guī)律方便你排查是特征問題還是模型問題這種調(diào)參手感在深度學(xué)習(xí)中非常寶貴。第三在數(shù)據(jù)量很少的場景下傳統(tǒng)方法往往比深度學(xué)習(xí)更實(shí)用——我見過不少真實(shí)項(xiàng)目標(biāo)注數(shù)據(jù)就幾百條硬上深度學(xué)習(xí)反而不如GMM-UBM穩(wěn)。2. 核心細(xì)節(jié)解析GMM-UBM到底在做什么為什么能拿少量數(shù)據(jù)建模2.1 GMM建模一段語音怎么變成一堆高斯分布說話人識(shí)別里最常用的特征是MFCC梅爾頻率倒譜系數(shù)。每幀語音能算出幾十維特征一秒語音通常有100幀左右。假設(shè)一段語音有2000幀每幀40維MFCC那這段語音在特征空間里就呈現(xiàn)為2000個(gè)離散點(diǎn)。GMM的任務(wù)就是用K個(gè)高斯分布去擬合這2000個(gè)點(diǎn)的分布。每個(gè)高斯分布有均值、方差和權(quán)重三個(gè)參數(shù)。最終這段語音的身份特征被濃縮為這K個(gè)高斯分布的參數(shù)集合也就是一個(gè)“超向量”。在scikit-learn里用GaussianMixture就能完成GMM訓(xùn)練幾行代碼的事。但要注意GMM模型的參數(shù)量和特征維度、K值直接相關(guān)維度太高或者K值太大會(huì)導(dǎo)致參數(shù)爆炸在小數(shù)據(jù)集上極容易過擬合。在項(xiàng)目實(shí)現(xiàn)里有一個(gè)常見誤區(qū)直接對(duì)所有測試語音各訓(xùn)一個(gè)GMM然后拿來互比。這樣做的問題在于每個(gè)模型是在不同條件下訓(xùn)練的可能因?yàn)殇浺粼肼暋⒁袅坎町惖纫蛩貙?dǎo)致模型之間沒有可比性。這也是GMM-UBM出現(xiàn)的原因。2.2 UBM的思路先建?!八腥恕痹傥⒄{(diào)出“某個(gè)人”UBM的訓(xùn)練方式和GMM完全一樣只不過用的數(shù)據(jù)是幾百個(gè)不同人的語音。訓(xùn)練完成后這個(gè)UBM代表了“通用聲學(xué)空間”即人類聲音的普遍分布情況。接下來對(duì)于目標(biāo)說話人我們要做的是MAP自適應(yīng)最大后驗(yàn)概率自適應(yīng)。核心思想是不重新估計(jì)參數(shù)而是在UBM參數(shù)的基礎(chǔ)上用新說話人的語音統(tǒng)計(jì)量去調(diào)整均值。數(shù)學(xué)上可以理解為給定一段語音先計(jì)算每幀特征在每個(gè)高斯分量上的后驗(yàn)概率occupancy根據(jù)這些后驗(yàn)概率計(jì)算新說話人的均值統(tǒng)計(jì)量用插值系數(shù)把新統(tǒng)計(jì)量和UBM均值混合得到說話人的新模型。這種“先求后驗(yàn)、再插值”的過程讓目標(biāo)說話人即便只有幾十秒語音也能得到一個(gè)相對(duì)穩(wěn)定的模型不會(huì)因?yàn)閿?shù)據(jù)少而方差過大。2.3 打分策略為什么要用對(duì)數(shù)似然比當(dāng)UBM和說話人模型都準(zhǔn)備好之后識(shí)別階段要判斷一句話屬于哪個(gè)說話人。這里不能用簡單的絕對(duì)值因?yàn)椴煌Z音的長度、信道、音量都會(huì)影響似然值。標(biāo)準(zhǔn)做法是計(jì)算對(duì)數(shù)似然比對(duì)一句話分別計(jì)算在目標(biāo)說話人模型上的log-likelihood以及在UBM上的log-likelihood兩者相減得到對(duì)數(shù)似然比LLRLLR大于閾值則判定為本人否則為冒認(rèn)者。這個(gè)設(shè)計(jì)的直覺是這樣的UBM是“普通人平均水平”的基線如果在目標(biāo)模型上得分遠(yuǎn)高于UBM上的得分說明這句話和這個(gè)人的聲學(xué)特征的吻合度顯著高于普通水平。用相對(duì)值而不是絕對(duì)值得分能大幅緩解信道不同、錄音設(shè)備不同帶來的偏差。3. ivector方案怎么把一段語音壓縮成一個(gè)身份向量3.1 從超向量到總變化空間核心是降維GMM-UBM有個(gè)繞不開的痛點(diǎn)如果UBM有512個(gè)高斯分量每個(gè)MFCC特征是40維那一個(gè)說話人模型的超向量就是512402均值和方差等于40960維。這個(gè)維度做比對(duì)太費(fèi)勁而且包含大量冗余信息。ivector的提出把問題變成了一個(gè)低維因子分析問題。假設(shè)超向量s可以由一個(gè)全局均值m來自UBM參數(shù)加上一個(gè)低維矩陣T乘以一個(gè)隱變量w來表達(dá)s m T*w這里w就是ivector向量通常只有100到600維。T矩陣稱為總變化空間Total Variability Space它的作用是把高維超向量中與說話人、信道相關(guān)的信息都?jí)嚎s到低維向量中。要訓(xùn)練T矩陣需要做迭代的EM優(yōu)化。這塊自己從零手寫代碼會(huì)很繁瑣好在這個(gè)項(xiàng)目的源碼里有對(duì)應(yīng)的實(shí)現(xiàn)——核心是累積零階統(tǒng)計(jì)量、一階統(tǒng)計(jì)量然后用矩陣運(yùn)算迭代更新T。實(shí)際跑起來訓(xùn)練時(shí)間受T矩陣維度影響很大維度越高越慢。3.2 ivector的后處理從原始向量到可分向量訓(xùn)練好T矩陣后每段語音可以在給定UBM參數(shù)和T矩陣的前提下提取出一個(gè)固定維度的ivector。但直接拿這個(gè)向量做比對(duì)效果往往不夠好還需要做一系列后處理長度歸一化Length Normalization把ivector歸一化到單位長度減少因語音時(shí)長不同帶來的數(shù)值尺度差異白化Whitening讓不同維度的方差一致消除特征尺度不同的影響LDA線性判別分析在說話人維度上降維增強(qiáng)類間距離、壓縮類內(nèi)距離。實(shí)操中長度歸一化是必做的白化在數(shù)據(jù)量充足時(shí)有幫助LDA則需要有多個(gè)說話人的注冊數(shù)據(jù)才能訓(xùn)練。3.3 打分方式最常用余弦距離退而求其次用SVM或PLDAivector之間最常用的度量方式是余弦相似度計(jì)算方便且在大多數(shù)場景下表現(xiàn)穩(wěn)定。如果想更精細(xì)可以用SVM或者PLDA概率線性判別分析。PLDA雖然效果通常更好但對(duì)數(shù)據(jù)量要求更高而且訓(xùn)練和推理的代碼復(fù)雜度不小。我個(gè)人的經(jīng)驗(yàn)是小項(xiàng)目用余弦距離夠用做評(píng)測競賽或追求極限準(zhǔn)確率再上PLDA。4. 深度學(xué)習(xí)聲紋識(shí)別從“手工特征”到“端到端表征”4.1 網(wǎng)絡(luò)架構(gòu)選型為什么說TDNN和ResNet是主流深度學(xué)習(xí)聲紋識(shí)別的整體流程非常簡潔輸入語音經(jīng)過特征提取可以用Fbank或MFCC送入神經(jīng)網(wǎng)絡(luò)網(wǎng)絡(luò)輸出一個(gè)embedding向量然后用這個(gè)向量做比對(duì)。網(wǎng)絡(luò)架構(gòu)的選擇上業(yè)內(nèi)常用三類TDNN時(shí)延神經(jīng)網(wǎng)絡(luò)通過拼接不同時(shí)刻的幀上下文來建模時(shí)序信息參數(shù)量小適合低功耗場景ResNet殘差網(wǎng)絡(luò)通過殘差連接堆疊較深的卷積結(jié)構(gòu)能提取更豐富的頻譜特征準(zhǔn)確率通常比TDNN高ECAPA-TDNN算是TDNN的升級(jí)版加入了SE模塊和更深的通道維度是近年來VoxCeleb評(píng)測上的???。在項(xiàng)目里如果只是入門可以先用小型ResNet比如把ResNet18簡單改造一下把最后的全連接層輸出改成embedding維度。注意不要用ImageNet預(yù)訓(xùn)練權(quán)重初始化語音的Fbank特征分布跟圖片像素差太遠(yuǎn)。4.2 損失函數(shù)從Softmax到基于角度的約束深度學(xué)習(xí)聲紋識(shí)別里損失函數(shù)決定了embedding的分布形態(tài)。最早的思路是把說話人識(shí)別當(dāng)成分類任務(wù)直接訓(xùn)練Softmax分類器然后取倒數(shù)第二層特征作為embedding。這個(gè)做法的問題是Softmax只要求類別可分沒有顯式要求“同類內(nèi)聚”。于是有了各種margin-based損失函數(shù)Angular SoftmaxA-Softmax、AM-Softmax、ArcFace。它們共同的思想是在角度空間里增加一個(gè)margin讓同類特征更聚攏、異類特征更疏散。ArcFace是其中效果最明顯的也是目前聲紋識(shí)別里常用的選擇。用PyTorch實(shí)現(xiàn)一個(gè)ArcFace層并不難核心就是改一下交叉熵?fù)p失中目標(biāo)分?jǐn)?shù)的計(jì)算方式把余弦角度加上一個(gè)固定margin。4.3 訓(xùn)練策略與數(shù)據(jù)增強(qiáng)別忽略這些能漲分的細(xì)節(jié)訓(xùn)練數(shù)據(jù)方面源路徑常用VoxCeleb1/2開源且人群覆蓋廣。項(xiàng)目里如果只想本地快速驗(yàn)證也可以用LibriSpeech里選取一部分說話人或者自己錄幾組語音。數(shù)據(jù)增強(qiáng)值得重視的地方有幾個(gè)加噪聲是最常見的方法可以用MUSAN數(shù)據(jù)集里的噪聲和音樂進(jìn)行疊加模擬混響也可以做通過和RIR數(shù)據(jù)集里的房間脈沖響應(yīng)卷積還可以做速度擾動(dòng)把音頻變速0.9和1.1倍生成兩條新樣本。這些增強(qiáng)手段雖然簡單對(duì)魯棒性和最終準(zhǔn)確率有明顯幫助。訓(xùn)練階段通常用Adam優(yōu)化器初始學(xué)習(xí)率1e-3CosineAnnealing退火batch size越大訓(xùn)練越穩(wěn)定。我自己試下來在VoxCeleb1訓(xùn)練集上訓(xùn)練一個(gè)ResNet34聲紋模型大概需要20~30個(gè)epoch才能收斂如果只有一個(gè)GPU建議先用小模型調(diào)試通流程再擴(kuò)大規(guī)模。4.4 打分階段embedding提取與PLDA訓(xùn)練完成后推理階段很直觀把注冊語音和目標(biāo)語音都輸入網(wǎng)絡(luò)提取embedding然后算相似度。相似度可以用余弦距離也可以用PLDA得分。如果訓(xùn)模型時(shí)用的是Softmax或ArcFaceembedding的分布已經(jīng)比較緊致余弦距離一般來說足夠用。這里有一個(gè)容易踩的坑必須確認(rèn)訓(xùn)練時(shí)和測試時(shí)用的是同一套特征參數(shù)。例如訓(xùn)練時(shí)用的是80維Fbank、25ms窗長、10ms幀移測試時(shí)如果改成了40維MFCC模型的效果會(huì)大幅下降。這類不匹配問題是深度學(xué)習(xí)聲紋識(shí)別里最常見但最容易被忽略的錯(cuò)誤。5. 源碼實(shí)現(xiàn)搭一個(gè)可運(yùn)行的完整聲紋識(shí)別Demo5.1 數(shù)據(jù)準(zhǔn)備沒有VoxCeleb也能快速驗(yàn)證VoxCeleb數(shù)據(jù)集比較大完整下載需要耐心。如果你只是想先跑通流程我推薦一個(gè)輕量級(jí)方案用錄音工具錄制4~6個(gè)人、每人5~8句話作為注冊數(shù)據(jù)另錄每人3句話作為測試數(shù)據(jù)。每個(gè)人單獨(dú)放一個(gè)目錄比如data/train/zhangsan/0.wav。這樣數(shù)據(jù)量雖然少但足以驗(yàn)證代碼邏輯是否正確。在代碼中用librosa加載音頻采樣率統(tǒng)一為16kHz單聲道。需要注意的一點(diǎn)不同錄音設(shè)備和房間環(huán)境會(huì)造成信道差異如果條件允許測試語音和注冊語音盡量在不同時(shí)間錄制避免“同一條錄音既注冊又測試”的作弊現(xiàn)象。5.2 特征提取MFCC和Fbank二選一傳統(tǒng)GMM/ivector方案建議用MFCC通常40維深度學(xué)習(xí)方案建議用Fbank通常80維。直接調(diào)用librosa.feature.mfcc或者python_speech_features都可以但要注意做比較時(shí)特征參數(shù)保持一致。import librosa import numpy as np def extract_mfcc(path, n_mfcc40): y, sr librosa.load(path, sr16000) mfcc librosa.feature.mfcc(yy, srsr, n_mfccn_mfcc, n_fft1024, hop_length160, win_length400) # 加上一階差分?jǐn)U展為80維 delta librosa.feature.delta(mfcc) feature np.vstack([mfcc, delta]) return feature.T # shape: (frames, n_mfcc*2) def extract_fbank(path, n_mels80): y, sr librosa.load(path, sr16000) fbank librosa.feature.melspectrogram(yy, srsr, n_melsn_mels, n_fft1024, hop_length160, win_length400) log_fbank np.log(fbank 1e-6) return log_fbank.T # shape: (frames, n_mels)我在兩個(gè)方案里都開啟了delta差分因?yàn)镚MM-UBM對(duì)幀間動(dòng)態(tài)信息不敏感靠差分能補(bǔ)上一些上下文信息。有些開源代碼省略了這一步識(shí)別率會(huì)掉一截。5.3 GMM-UBM訓(xùn)練代碼先訓(xùn)UBM再M(fèi)AP自適應(yīng)下面這段代碼適合跑通GMM-UBM的核心流程。UBM直接用sklearn的GaussianMixture來訓(xùn)練MAP自適應(yīng)部分手動(dòng)實(shí)現(xiàn)。import os import numpy as np from sklearn.mixture import GaussianMixture def load_all_features(data_dir, n_mfcc40): feats [] for root, _, files in os.walk(data_dir): for f in files: if f.endswith(.wav): path os.path.join(root, f) feats.append(extract_mfcc(path, n_mfcc)) return np.vstack(feats) # 訓(xùn)練UBM用所有人的語音也可以用部分人的語音 all_feats load_all_features(data/train) ubm GaussianMixture(n_components256, covariance_typediag, max_iter100, random_state0) ubm.fit(all_feats) # MAP自適應(yīng)用目標(biāo)說話人的少量語音調(diào)整UBM得到GMM def map_adapt(ubm, data, relevance_factor16.0): 對(duì)單個(gè)說話人的數(shù)據(jù)進(jìn)行MAP自適應(yīng)。data為frames x dim矩陣。 # 計(jì)算每個(gè)高斯分量的后驗(yàn)概率 log_prob ubm._estimate_log_prob(data) responsibilities np.exp(log_prob - log_prob.max(axis1, keepdimsTrue)) responsibilities / responsibilities.sum(axis1, keepdimsTrue) # 零階和一階統(tǒng)計(jì)量 N_k responsibilities.sum(axis0) # (K,) F_k responsibilities.T data # (K, dim) # 歸一化后的一階統(tǒng)計(jì)量 X_hat F_k / (N_k[:, None] 1e-10) # 新均值 (alpha_k * 說話人均值 (1 - alpha_k) * UBM均值) alpha_k N_k / (N_k relevance_factor) adapt_means alpha_k[:, None] * X_hat (1 - alpha_k[:, None]) * ubm.means_ # 復(fù)制模型參數(shù)并替換均值 from copy import deepcopy adapted deepcopy(ubm) adapted.means_ adapt_means return adapted # 為每個(gè)說話人生成模型 speaker_models {} for spk in os.listdir(data/train): spk_dir os.path.join(data/train, spk) if os.path.isdir(spk_dir): feats load_all_features(spk_dir, n_mfcc40) speaker_models[spk] map_adapt(ubm, feats)這段代碼里我設(shè)置了relevance_factor16這是經(jīng)驗(yàn)值數(shù)值越大自適應(yīng)越保守模型越接近UBM數(shù)值越小越容易過度擬合單個(gè)人的少量語音。數(shù)據(jù)很少的時(shí)候建議調(diào)大一些。5.4 ivector實(shí)現(xiàn)用T矩陣投影提取身份向量ivector的完整EM訓(xùn)練代碼比較長這里給出提取部分的關(guān)鍵思路。如果你在源碼中找不到訓(xùn)練好的T矩陣文件可以先從GMM-UBM的超向量加上PCA降維做一個(gè)簡化的ivector變體。def compute_statistics(gmm, data): 計(jì)算語音數(shù)據(jù)的零階、一階統(tǒng)計(jì)量。 log_prob gmm._estimate_log_prob(data) resp np.exp(log_prob - log_prob.max(axis1, keepdimsTrue)) resp / resp.sum(axis1, keepdimsTrue) N_k resp.sum(axis0) # (K,) F_k resp.T data # (K, dim) return N_k, F_k def extract_ivector(ubm, T_matrix, data): 根據(jù)超向量求ivector: w inv(I T^T * Sigma_inv * N * T) * T^T * Sigma_inv * F N_k, F_k compute_statistics(ubm, data) dim data.shape[1] K len(N_k) # 構(gòu)造分塊對(duì)角統(tǒng)計(jì)量矩陣 N_matrix np.zeros((K*dim, K*dim)) for i in range(K): N_matrix[i*dim:(i1)*dim, i*dim:(i1)*dim] np.eye(dim) * N_k[i] F_vector F_k.flatten() sigma_inv 1.0 / (ubm.covariances_.flatten() 1e-10) A np.eye(T_matrix.shape[1]) T_matrix.T (N_matrix * sigma_inv[:, None]) T_matrix B T_matrix.T (F_vector * sigma_inv) w np.linalg.solve(A, B) return w真正的T矩陣EM訓(xùn)練比這個(gè)復(fù)雜需要多次迭代更新T矩陣的估計(jì)。如果你打算深入這塊建議先跑通上面的簡化版理解ivector是怎么從統(tǒng)計(jì)量里提取出來的再去看完整源碼里的EM實(shí)現(xiàn)。否則一開始就被矩陣求逆、分塊迭代這些細(xì)節(jié)繞進(jìn)去很難快速建立整體認(rèn)知。5.5 深度學(xué)習(xí)模型用PyTorch搭一個(gè)小型聲紋網(wǎng)絡(luò)深度學(xué)習(xí)部分我建議用一個(gè)簡化版ResNet把ArcFace損失加上訓(xùn)練出embedding后直接在測試階段用余弦相似度判斷。import torch import torch.nn as nn import torch.nn.functional as F class SimpleSpeakerNet(nn.Module): def __init__(self, in_dim80, embd_dim128, num_speakersNone): super().__init__() self.layer1 nn.Sequential( nn.Conv2d(1, 16, kernel_size3, stride1, padding1), nn.BatchNorm2d(16), nn.ReLU(), nn.MaxPool2d(2)) self.layer2 nn.Sequential( nn.Conv2d(16, 32, kernel_size3, stride1, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2)) self.layer3 nn.Sequential( nn.Conv2d(32, 64, kernel_size3, stride1, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2)) self.layer4 nn.Sequential( nn.Conv2d(64, 128, kernel_size3, stride1, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2)) self.embedding nn.Linear(128 * 5 * 5, embd_dim) self.classifier nn.Linear(embd_dim, num_speakers) def forward(self, x): # x: (batch, frames, dim) - (batch, 1, frames, dim) x x.unsqueeze(1) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) x x.flatten(1) embd F.relu(self.embedding(x)) logits self.classifier(embd) return embd, logits訓(xùn)練時(shí)輸入特征需要做歸一下處理means和vars隨機(jī)切固定長度段比如4秒不夠就循環(huán)補(bǔ)batch size可以設(shè)64或128。損失只用交叉熵就可以跑起來加ArcFace后續(xù)再優(yōu)化。測試階段提取embedding的方式是輸入整段語音的所有幀網(wǎng)絡(luò)會(huì)輸出每幀對(duì)應(yīng)分?jǐn)?shù)但我們的網(wǎng)絡(luò)是取所有幀特征整體過特征提取器的所以需要先把語音切成段、每段求embedding后求平均。這是和傳統(tǒng)ivector最大的不同之處深度學(xué)習(xí)的embedding提取通常要考慮池化策略簡單平均池化即可入門。6. 常見問題與排查技巧實(shí)錄6.1 特征維度不匹配導(dǎo)致模型訓(xùn)練報(bào)錯(cuò)這個(gè)是最常見的問題。訓(xùn)練UBM時(shí)用的MFCC是40維MAP自適應(yīng)時(shí)加載的數(shù)據(jù)如果多加了delta變成80維GMM模型不匹配直接報(bào)維度錯(cuò)誤。解決辦法統(tǒng)一封裝特征提取函數(shù)訓(xùn)練和測試走同一條路徑。建議在代碼里把特征參數(shù)寫成全局配置不要在不同腳本里各自定義。6.2 GMM訓(xùn)練過慢或存在不收斂GaussianMixture在數(shù)據(jù)量大、混合數(shù)高的情況下EM迭代非常慢。建議先用小規(guī)模數(shù)據(jù)測試比如1000幀確認(rèn)計(jì)算流程OK后再全量跑。收斂出問題通常和初始化和數(shù)據(jù)范圍有關(guān)可以檢查特征是否做了均值和方差歸一化。有些代碼提取MFCC后直接進(jìn)GMM數(shù)值范圍可能很大歸一化后能明顯改善收斂性。6.3 UBM訓(xùn)練時(shí)混合數(shù)怎么選擇UBM混合數(shù)K是一個(gè)需要權(quán)衡的超參數(shù)。K值太小模型表達(dá)能力不足K值太大參數(shù)多、訓(xùn)練慢且容易過擬合。常規(guī)配置是256或512。數(shù)據(jù)只有一到兩小時(shí)、說話人數(shù)量不多時(shí)256足夠想逼近VoxCeleb規(guī)模的評(píng)測水平512更合適。6.4 ivector訓(xùn)練時(shí)內(nèi)存占滿ivector訓(xùn)練涉及分塊矩陣,尤其是統(tǒng)計(jì)量矩陣當(dāng)UBM混合數(shù)K512、MFCC維度40時(shí)統(tǒng)計(jì)量矩陣是512*4020480維的方陣占內(nèi)存非??鋸?。這在源碼實(shí)現(xiàn)里是個(gè)容易卡住的點(diǎn)解決方法是不要一次性把所有話語統(tǒng)計(jì)量都加載進(jìn)內(nèi)存而是分批讀取邊算邊累積T矩陣更新所需的累加量。6.5 深度學(xué)習(xí)訓(xùn)練loss不下降先用很小的數(shù)據(jù)集比如每個(gè)說話人10條語音做overfit測試確認(rèn)loss能到很小值。如果小數(shù)據(jù)都上不去檢查學(xué)習(xí)率建議1e-3、網(wǎng)絡(luò)結(jié)構(gòu)是否太多層但數(shù)據(jù)不足、特征范圍。還有一個(gè)常見坑是batch-size太小導(dǎo)致BN層在batch內(nèi)部統(tǒng)計(jì)不穩(wěn)定特別是語音幀數(shù)變化大的時(shí)候建議加固定長度切幀。6.6 注冊語音與測試語音的時(shí)長差太多無論是傳統(tǒng)方法還是深度學(xué)習(xí)時(shí)長差異都會(huì)影響效果。ivector方面語音太短會(huì)導(dǎo)致統(tǒng)計(jì)量估計(jì)不穩(wěn)定embedding向量的方差變大深度學(xué)習(xí)方面embedding平均池化在短語音上不太穩(wěn)。常規(guī)做法是注冊語音盡量保持不少于3秒測試語音如果太短可以降低判決閾值來換取召回率。6.7 信道/噪聲影響識(shí)別率這是一個(gè)真實(shí)場景里的老問題。同一句話在不同手機(jī)上錄制在GMM-UBM里通常會(huì)看到log似然比下降得很厲害。增強(qiáng)方案可以放在前端做加VAD靜音檢測切掉靜音段或者做語音增強(qiáng)去噪。深度學(xué)習(xí)方法一般對(duì)輕微噪聲魯棒性更好但強(qiáng)噪聲場景下依然建議加數(shù)據(jù)增強(qiáng)讓模型在訓(xùn)練時(shí)就見過說話人帶噪聲的樣子。7. 把這套源碼真正跑起來的順序建議我第一次接觸這類開源項(xiàng)目時(shí)試圖一次把所有代碼都讀懂結(jié)果被ivector的EM訓(xùn)練和深度學(xué)習(xí)腳本同時(shí)卡住。后來總結(jié)出一個(gè)比較順暢的順序先跑GMM-UBM的最小demo用1到2個(gè)人的數(shù)據(jù)確認(rèn)數(shù)據(jù)準(zhǔn)備、特征提取、訓(xùn)練、打分整條流程能通。然后增加人數(shù)和UBM混合數(shù)驗(yàn)證識(shí)別率的變化。再進(jìn)入ivector階段先不訓(xùn)練T矩陣用PCA降維的超向量作為簡化版ivector跑通打分流程再逐步替換為真正的T矩陣EM結(jié)果。最后切換深度學(xué)習(xí)用同樣的數(shù)據(jù)跑一遍Fbank加ResNet和傳統(tǒng)方法的準(zhǔn)確率做對(duì)比。整個(gè)項(xiàng)目調(diào)試下來你獲得的不僅是一個(gè)能用的聲紋識(shí)別系統(tǒng)更重要的是親手驗(yàn)證了技術(shù)演進(jìn)的核心邏輯從概率密度模型到低維因子分析再到端到端表征學(xué)習(xí)每一步都是在解決前一步的某個(gè)痛點(diǎn)。最后提一個(gè)實(shí)用建議源碼目錄里做好版本管理數(shù)據(jù)和模型文件分開存放特征提取模塊用單獨(dú)的類封裝這樣后續(xù)無論是換數(shù)據(jù)集還是加新的深度模型改動(dòng)范圍都會(huì)很小。本文還有配套的精品資源點(diǎn)擊獲取