別實(shí)戰(zhàn):從特征提取到嵌入式部署)
簡介基于MFCC與GMM的語音識(shí)別Matlab工程面向語音處理初學(xué)者和研究人員演示了從語音信號(hào)中提取梅爾頻率倒譜系數(shù)、以高斯混合模型建模聲學(xué)特征并完成識(shí)別/說話人辨認(rèn)的完整流程其中MFCC模擬人耳對(duì)頻率的非線性感知GMM通過多個(gè)高斯成分刻畫聲學(xué)狀態(tài)的分布工程覆蓋預(yù)加重、分幀、加窗、FFT、梅爾濾波、倒譜歸一化及EM訓(xùn)練、Viterbi解碼等關(guān)鍵環(huán)節(jié)。壓縮包共28個(gè)文件以25個(gè)m腳本為主輔以2個(gè)mat數(shù)據(jù)文件和1個(gè)說明文檔整體僅1.44MB輕量易部署同時(shí)提供現(xiàn)成的說話人模型與特征數(shù)據(jù)便于直接運(yùn)行和驗(yàn)證。已有321人學(xué)習(xí)下載適合通過Matlab動(dòng)手實(shí)踐來理解MFCC與GMM在語音識(shí)別中的協(xié)同作用。通過學(xué)習(xí)可掌握特征提取、模型訓(xùn)練和識(shí)別匹配的編碼實(shí)現(xiàn)并借助可視化工具調(diào)試參數(shù)、觀察識(shí)別效果是連接理論與工程實(shí)現(xiàn)的實(shí)用參考資料。1. 為什么MFCCGMM在2020年代仍能撐起小詞表語音識(shí)別當(dāng)你在一個(gè)離線嵌入式項(xiàng)目里收到“語音識(shí)別”需求第一反應(yīng)可能是先找神經(jīng)網(wǎng)絡(luò)推理框架。但真正落過地的工程師都知道只要詞表固定、環(huán)境相對(duì)穩(wěn)定MFCC加GMM這條路可以在一顆幾十MHz的MCU上跑起來既不依賴GPU也不依賴云端。MFCC負(fù)責(zé)把聲音變成特征向量GMM負(fù)責(zé)給每個(gè)詞的聲音特征分布建模組合出的系統(tǒng)訓(xùn)練快、推理快、失敗時(shí)還能追到是哪一步出了問題。MFCC和GMM不是“先提特征再分類”這么簡單。MFCC的幀長、濾波器和動(dòng)態(tài)拼接方式會(huì)影響GMM的協(xié)方差結(jié)構(gòu)GMM的混合數(shù)、協(xié)方差類型又決定了對(duì)特征的擬合能力。下面我把從波形到識(shí)別結(jié)果的關(guān)鍵鏈路拆開給出可直接改的實(shí)驗(yàn)參數(shù)和踩坑點(diǎn)。適合做孤立詞、命令詞、說話人確認(rèn)的讀者也適合想在端側(cè)快速驗(yàn)證語音方案的人。2. MFCC特征提取把一段錄音變成GMM能吃的1326維特征2.1 預(yù)加重、分幀、加窗三個(gè)參數(shù)決定了GMM的輸入質(zhì)量MFCC的起點(diǎn)不是直接做FFT而是先對(duì)時(shí)域信號(hào)做預(yù)處理。最容易被忽略的是預(yù)加重它用一個(gè)一階高通濾波器讓高頻段獲得更多權(quán)重系數(shù)通常取0.97。原因是發(fā)音時(shí)唇齒輻射會(huì)自然衰減高頻預(yù)加重相當(dāng)于在數(shù)學(xué)上“還原”聲音原本的形狀。系數(shù)太大容易把噪聲同時(shí)放大太小則高頻信息損失后續(xù)GMM會(huì)對(duì)輔音和摩擦音失去區(qū)分能力。分幀參數(shù)直接影響GMM的輸入分布形態(tài)。我一般用25ms幀長、10ms幀移在16kHz采樣率下就是400點(diǎn)一幀、160點(diǎn)一移。幀太長會(huì)把兩個(gè)音素揉在一起造成特征向量的方差變大幀太短則頻譜分辨率不足低頻段的信息會(huì)失真。加窗通常選漢明窗它可以抑制頻譜泄漏避免矩形窗在幀邊緣產(chǎn)生的旁瓣進(jìn)入特征。參數(shù)典型值設(shè)置理由違反后的常見現(xiàn)象預(yù)加重系數(shù)0.97補(bǔ)償高頻衰減約0.9時(shí)識(shí)別率波動(dòng)清音特征弱幀長25ms兼顧時(shí)域與頻域分辨率40ms以上元音混疊識(shí)別偏向濁音幀移10ms相鄰幀重疊保留連續(xù)性幀移過大時(shí)特征序列抖動(dòng)窗函數(shù)漢明窗降低頻譜泄漏矩形窗帶來頻譜“毛刺”FFT點(diǎn)數(shù)51216kHz下頻域分辨率約31Hz過小導(dǎo)致低頻帶混入高頻能量梅爾濾波器數(shù)40平衡低頻發(fā)射與計(jì)算量26時(shí)部分案例識(shí)別率下降約2%2.2 梅爾濾波器組與DCT為什么MFCC的維數(shù)通常取13分幀加窗后的信號(hào)經(jīng)過FFT就得到線性頻譜。人耳對(duì)頻率的感知不是線性的低頻分辨能力強(qiáng)、高頻分辨能力弱于是用梅爾刻度把線性頻譜投影到一組三角濾波器上。濾波器個(gè)數(shù)從20到80都有人用我默認(rèn)選40因?yàn)槎鄶?shù)孤立詞任務(wù)中26個(gè)會(huì)損失一部分低頻共振峰細(xì)節(jié)80個(gè)又會(huì)把高頻噪聲細(xì)節(jié)也帶進(jìn)來讓GMM協(xié)方差矩陣變得不穩(wěn)定。在濾波器組輸出的對(duì)數(shù)能量上做離散余弦變換就得到倒譜系數(shù)。取前13維是因?yàn)镈CT會(huì)天然把能量集中到低階系數(shù)上高階系數(shù)更多是聲道激勵(lì)的周期波動(dòng)和噪聲對(duì)語義區(qū)分幫助不大。但要特別注意靜態(tài)13維只描述了特征幀“當(dāng)前時(shí)刻”的狀態(tài)語音是動(dòng)態(tài)過程需要把一階差分和二階差分拼接起來形成13131339維的特征向量。這條拼接規(guī)則是MFCCGMM方案穩(wěn)定工作的關(guān)鍵很多只喂靜態(tài)特征導(dǎo)致準(zhǔn)確率上不去的系統(tǒng)缺的就是這26維動(dòng)態(tài)信息。2.3 MFCC代碼的最小實(shí)現(xiàn)用librosa三行得到特征下面這段代碼可以直接用于訓(xùn)練和識(shí)別兩個(gè)階段我在多個(gè)詞表上用它做基線特征。import numpy as np import librosa def extract_mfcc_39(audio_path, sr16000): # 加載音頻并重采樣到16kHz y, sr librosa.load(audio_path, srsr) # 提取靜態(tài)13維MFCC每幀都會(huì)有一個(gè)特征向量 mfcc librosa.feature.mfcc( yy, srsr, n_mfcc13, n_fft512, hop_length160, n_mels40, fmin0, fmax8000 ) # 一階差分和二階差分分別也是13維 delta1 librosa.feature.delta(mfcc, order1) delta2 librosa.feature.delta(mfcc, order2) # 按行拼接后轉(zhuǎn)置形狀為(幀數(shù), 39) feat np.vstack([mfcc, delta1, delta2]).T return feat代碼里hop_length160對(duì)應(yīng)10ms幀移因?yàn)閰?shù)是采樣點(diǎn)個(gè)數(shù)。n_mels40與fmin0, fmax8000限定在人的語音主要頻帶內(nèi)。np.vstack是按行把三個(gè)矩陣拼到一起因?yàn)閘ibrosa.feature.mfcc返回的形狀是(n_mfcc, 幀數(shù))所以轉(zhuǎn)置后每一行是一幀的39維特征這個(gè)方向如果搞反后面給GMM訓(xùn)練時(shí)會(huì)出現(xiàn)維度全部錯(cuò)位的問題。2.4 特征拼接和歸一化直接用原始MFCC帶來的幾個(gè)坑原始MFCC會(huì)混入信道和說話人本征差異。同一個(gè)詞用不同的麥克風(fēng)錄前幾維倒譜系數(shù)的均值就有偏移不同說話人之間倒譜均值差異可能比詞與詞之間的差異還大。常見做法是倒譜均值歸一化CMS把每個(gè)特征維度在整句時(shí)間上減去均值用來消除線性信道響應(yīng)。更穩(wěn)妥的是倒譜均值方差歸一化CMVN再除以標(biāo)準(zhǔn)差讓特征分布接近標(biāo)準(zhǔn)正態(tài)分布。要注意CMS在短命令詞上容易失效因?yàn)檎Z音本身就是短促且非平穩(wěn)的整句均值會(huì)被靜音段和邊界音素拖偏。我的做法是先做端點(diǎn)檢測去掉首尾靜音幀再對(duì)有效語音段做CMVN。還有一個(gè)坑不要在整段錄音包含靜音時(shí)計(jì)算方差否則GMM會(huì)把靜音也建模進(jìn)去識(shí)別時(shí)出現(xiàn)“靜音永遠(yuǎn)得分最高”的詭異現(xiàn)象。特征歸一化的代碼一般放在extract_mfcc_39返回之后用sklearn.preprocessing.StandardScaler按幀的方向擬合即可。3. GMM的建模邏輯從單高斯到混合模型的EM迭代3.1 為什么一個(gè)單高斯描述不了語音特征分布GMM在語音識(shí)別里常被看作一種生成式分類器但它本質(zhì)上是個(gè)軟聚類算法每個(gè)樣本都有一定概率屬于某個(gè)高斯成分。單個(gè)高斯分布假設(shè)特征是單峰對(duì)稱分布的而我們的39維MFCC特征明顯不是這樣。同一個(gè)“開燈”命令有人讀得快有人讀得慢發(fā)音時(shí)聲道狀態(tài)在共振峰位置會(huì)形成多個(gè)模態(tài)再加上不同說話人的聲道長度差異特征概率密度往往是偏態(tài)、多峰的。用單高斯去擬合會(huì)把多個(gè)聚類中心強(qiáng)行壓成一個(gè)橢球模型方差被拉大不同詞之間的類別邊界變得模糊。用多個(gè)高斯成分混合后每個(gè)成分可以專門負(fù)責(zé)特征空間里的一個(gè)局部區(qū)域比如一個(gè)成分捕捉元音段另一個(gè)成分捕捉鼻音或爆破音段?;旌蠑?shù)越多對(duì)訓(xùn)練數(shù)據(jù)擬合越精細(xì)但也要防止它把每個(gè)說話人的細(xì)節(jié)都背下來導(dǎo)致新說話人識(shí)別率下降。3.2 EM算法的E步和M步GMM參數(shù)是怎么迭代出來的給定一組特征幀我們希望估計(jì)出每個(gè)高斯成分的權(quán)重、均值向量和協(xié)方差矩陣。最大似然估計(jì)這里沒有閉式解所以用期望最大化EM迭代求近似解。E步根據(jù)當(dāng)前參數(shù)計(jì)算每一幀屬于第k個(gè)高斯成分的后驗(yàn)概率也叫責(zé)任度。M步用這些責(zé)任度重新估計(jì)參數(shù)權(quán)重是成分聚到的幀數(shù)占比均值是幀特征的加權(quán)平均協(xié)方差是加權(quán)距離外積。重復(fù)這兩步直到對(duì)數(shù)似然不再明顯增長。實(shí)際實(shí)現(xiàn)里要做兩個(gè)細(xì)節(jié)保護(hù)一是協(xié)方差矩陣取對(duì)角形式避免39維特征間的相關(guān)性導(dǎo)致行列式計(jì)算溢出二是設(shè)一個(gè)方差下限如reg_covar1e-6防止某些成分在特定維度上的方差收縮到零。提示對(duì)角協(xié)方差不是懶。在孤立詞識(shí)別中MFCC的DCT變換已經(jīng)做了去相關(guān)各維之間的相關(guān)性本來就弱強(qiáng)行建模相關(guān)性的全協(xié)方差矩陣反而需要大量訓(xùn)練樣本才能估計(jì)穩(wěn)定。3.3 用scikit-learn訓(xùn)練一個(gè)詞類的GMM下面用sklearn.mixture.GaussianMixture訓(xùn)練單個(gè)詞的GMM模型。注意這里輸入是整段發(fā)音提取出的所有幀即把所有幀當(dāng)作獨(dú)立同分布樣本。from sklearn.mixture import GaussianMixture def train_gmm_for_word(feat_matrix, n_components4, max_iter100): # feat_matrix 形狀為 (幀數(shù), 39) model GaussianMixture( n_componentsn_components, covariance_typediag, max_itermax_iter, n_init2, tol1e-3, reg_covar1e-6, random_state42 ) model.fit(feat_matrix) return modeln_components4是成數(shù)具體要根據(jù)詞表的復(fù)雜度調(diào)命令詞發(fā)音較短的按2~4個(gè)成分訓(xùn)練包含多音節(jié)或連續(xù)數(shù)字時(shí)提到8~12個(gè)。max_iter控制最大迭代次數(shù)通常100次內(nèi)能收斂若超過200次仍有警告說明數(shù)據(jù)量不足或特征有問題。n_init2表示從2個(gè)不同初始點(diǎn)開始EM最終取似然最高的結(jié)果因?yàn)镋M只保證局部最優(yōu)。模型訓(xùn)練好后model.score(S)返回所有樣本幀的平均對(duì)數(shù)似然。幀數(shù)量級(jí)也需要關(guān)注一段0.5秒的錄音經(jīng)過MFCC提取后大約41幀如果每個(gè)詞只錄10遍只有410個(gè)樣本去估計(jì)4個(gè)高斯的均值協(xié)方差。對(duì)39維對(duì)角協(xié)方差來說這樣的數(shù)據(jù)量還算夠用再少就把n_components降為2或者用半綁定GMM。3.4 混合數(shù)選擇用BIC和交叉驗(yàn)證避免過度擬合GMM的混合數(shù)選大了會(huì)過擬合訓(xùn)練說話人選小了又?jǐn)M合不了多峰分布。我在實(shí)驗(yàn)里先跑一組貝葉斯信息準(zhǔn)則找到BIC曲線拐點(diǎn)再用交叉驗(yàn)證確認(rèn)。from sklearn.mixture import GaussianMixture def select_components(feat_matrix, max_c16): bic_scores [] for n in range(2, max_c 1): model GaussianMixture(n_componentsn, covariance_typediag) model.fit(feat_matrix) bic_scores.append((n, model.bic(feat_matrix))) return sorted(bic_scores, keylambda x: x[1])[0]BIC并不是越小越好它在對(duì)數(shù)似然上增加一個(gè)與參數(shù)數(shù)量相關(guān)的懲罰項(xiàng)。我們找的是下降變緩的“拐點(diǎn)”而不是全局最小值。比如某組數(shù)據(jù)中5個(gè)成分和8個(gè)成分的BIC相差不大那就選5因?yàn)閰?shù)更少泛化更強(qiáng)。交叉驗(yàn)證可以這樣做把每個(gè)詞的多條錄音按說話人分組留出一個(gè)說話人的所有錄音做測試其余訓(xùn)練。這種留說話人交叉驗(yàn)證比隨機(jī)洗幀更貼近真實(shí)部署能看出系統(tǒng)對(duì)新說話人的適配能力。4. 搭一套基于MFCCGMM的孤立詞語音識(shí)別系統(tǒng)4.1 數(shù)據(jù)準(zhǔn)備每個(gè)詞至少錄多少條、如何組織目錄孤立詞識(shí)別任務(wù)里詞表越短越要控制訓(xùn)練均衡性。我建議每個(gè)詞至少錄30條覆蓋至少3個(gè)以上說話人如果做單一說話人專用系統(tǒng)可以降到25條但要包含不同時(shí)間、不同環(huán)境下的錄音。目錄結(jié)構(gòu)保持一個(gè)詞一個(gè)文件夾錄音文件統(tǒng)一轉(zhuǎn)為16kHz、單聲道、WAV格式這樣后面代碼遍歷時(shí)不需要特判格式。數(shù)據(jù)劃分要按文件不要按幀混洗。有人圖方便把每個(gè)詞的MFCC幀全部打亂然后用隨機(jī)劃分訓(xùn)練測試結(jié)果同一段錄音的幀同時(shí)出現(xiàn)在訓(xùn)練和測試?yán)镒R(shí)別率虛高到97%以上部署后立刻掉到70%。正確做法是按錄音文件劃分比如每個(gè)詞取70%的音頻文件訓(xùn)練30%測試。4.2 訓(xùn)練多類GMM按標(biāo)簽循環(huán)訓(xùn)練模型這里我寫一個(gè)訓(xùn)練端到端的腳本假設(shè)數(shù)據(jù)目錄格式為data/word_label/*.wav。每個(gè)詞的文件提取出MFCC特征幀后拼成一個(gè)大矩陣去擬合一個(gè)GMM。import os import pickle import librosa from sklearn.mixture import GaussianMixture from sklearn.preprocessing import StandardScaler def load_features(word_dir): X [] labels [] scaler StandardScaler() for label in os.listdir(word_dir): label_path os.path.join(word_dir, label) if not os.path.isdir(label_path): continue # 詞目錄下的所有wav先做特征提取再按文件做CMVN label_feats [] for wav in os.listdir(label_path): if not wav.endswith(.wav): continue path os.path.join(label_path, wav) feat extract_mfcc_39(path) feat scaler.fit_transform(feat) label_feats.append(feat) # 把該詞所有音頻的幀拼接成 (總幀數(shù), 39) X.append((label, np.vstack(label_feats))) return X models {} for label, feats in load_features(data/command/): models[label] GaussianMixture( n_components6, covariance_typediag, max_iter150, n_init2, reg_covar1e-6 ).fit(feats) with open(models.pkl, wb) as f: pickle.dump(models, f)需要解釋一下scaler的使用代碼里對(duì)每個(gè)音頻文件單獨(dú)做了fit_transform這等價(jià)于CMVN而不是全局標(biāo)準(zhǔn)化。這樣避免了測試集參與訓(xùn)練時(shí)的信息泄漏也符合實(shí)際部署時(shí)逐段歸一化的邏輯。如果換成對(duì)所有訓(xùn)練數(shù)據(jù)整體擬合一個(gè)全局scaler新說話人的錄音也要用這個(gè)全局參數(shù)否則特征分布不匹配。4.3 識(shí)別決策用對(duì)數(shù)似然而不是歐氏距離訓(xùn)練好的每個(gè)詞模型可以對(duì)任意輸入特征算得分。這里常見的誤區(qū)是直接用GMM的質(zhì)心均值向量算歐氏距離但這忽略了一個(gè)事實(shí)GMM每個(gè)高斯成分都有方差落在方差較大的方向上的偏移不該被懲罰得那么重。正確做法是使用概率密度函數(shù)算對(duì)數(shù)似然。實(shí)際識(shí)別時(shí)我會(huì)先對(duì)輸入音頻提取MFCC做CMVN然后依次用每個(gè)GMM調(diào)用score方法。score的結(jié)果是平均對(duì)數(shù)似然它把幀數(shù)歸一化了否則發(fā)音長的詞天然得分高系統(tǒng)會(huì)永遠(yuǎn)偏向多音節(jié)的命令詞。def recognize_one_audio(path, models): feat extract_mfcc_39(path) # 用測試時(shí)的CMVN策略直接對(duì)這段音頻做標(biāo)準(zhǔn)化 feat StandardScaler().fit_transform(feat) scores {label: model.score(feat) for label, model in models.items()} best_label max(scores, keyscores.get) best_score scores[best_label] # 完成識(shí)別返回最佳詞和得分 return best_label, best_score如果要加入拒識(shí)邏輯光比較得分不夠。最簡單的方法是在訓(xùn)練數(shù)據(jù)中混入大量非命令詞作為背景模型或者單獨(dú)訓(xùn)練一個(gè)覆蓋所有詞的全背景GMM當(dāng)best_score與背景模型得分之比低于閾值時(shí)拒絕識(shí)別。4.4 端點(diǎn)檢測和說話人歸一化讓準(zhǔn)確率提升5個(gè)百分點(diǎn)的通用做法端點(diǎn)檢測的價(jià)值常被低估。室內(nèi)錄音通常帶有空調(diào)聲、鍵盤聲這些噪聲幀在MFCC特征里形成一片低頻高能量的區(qū)域。GMM訓(xùn)練時(shí)如果不先切掉靜音就會(huì)在模型中專門分配一個(gè)或多個(gè)高斯成分去擬合噪聲削弱對(duì)語音內(nèi)容本身的建模能力。我使用librosa.effects.split它基于能量閾值找到有效語音區(qū)間然后把有效幀拼接起來再提取MFCC。說話人歸一化的另一個(gè)手段是聲道長度規(guī)整VTLN不過對(duì)GMM系統(tǒng)來說CMVN已經(jīng)能吸收大部分信道和響度差異VTLN在MFCC階段做有時(shí)會(huì)讓短詞的高頻特征變形。我的經(jīng)驗(yàn)是先用CMVN再在訓(xùn)練時(shí)不區(qū)分說話人如果模型在某一類說話人上系統(tǒng)性出錯(cuò)再去考慮性別自適應(yīng)或聲道長度估計(jì)。這個(gè)順序能避免把復(fù)雜信號(hào)處理引入不必要的不穩(wěn)定性。5. GMM識(shí)別系統(tǒng)的三個(gè)升級(jí)技巧和閉環(huán)驗(yàn)證5.1 用UBM做背景模型獲得拒識(shí)能力當(dāng)系統(tǒng)需要識(shí)別“不在詞表”的輸入時(shí)光靠GMM內(nèi)部得分排序不夠因?yàn)槿魏屋斎攵紩?huì)被強(qiáng)行分到得分最高的詞上。訓(xùn)練一個(gè)UBM通用背景模型用所有訓(xùn)練詞的全部特征幀去擬合一個(gè)成分?jǐn)?shù)更多的GMM作為“不屬于任何已知詞”的基線。識(shí)別時(shí)計(jì)算目標(biāo)詞模型的得分與UBM得分之差只有差值超過預(yù)設(shè)閾值才接受。這個(gè)差值實(shí)際上是似然比閾值在開發(fā)集上按等誤識(shí)率曲線選取。5.2 協(xié)方差下限與特征相關(guān)性陷阱如果特征里同時(shí)保留了MFCC和差分后的維度且沒有做去相關(guān)對(duì)角協(xié)方差矩陣也可能出現(xiàn)某維度方差為0的情況。這通常發(fā)生在某些詞只有固定長度的發(fā)音某一維在整段語音中完全恒定。reg_covar設(shè)得太小比如1e-12會(huì)觸發(fā)“協(xié)方差奇異”警告設(shè)在1e-4到1e-6之間可以兼顧數(shù)值穩(wěn)定和擬合精度。當(dāng)訓(xùn)練數(shù)據(jù)量少于10個(gè)文件時(shí)我會(huì)把混合數(shù)降為2同時(shí)把max_iter限制到80避免在小樣本上反復(fù)迭代出噪聲模式。5.3 從訓(xùn)練到識(shí)別的最小閉環(huán)驗(yàn)證腳本為了在部署前驗(yàn)證整條鏈路沒壞我會(huì)準(zhǔn)備一個(gè)只包含一個(gè)詞“switch”的最小測試將訓(xùn)練和識(shí)別打包到一個(gè)函數(shù)確認(rèn)輸出標(biāo)簽正確后就說明特征提取和GMM接口對(duì)接沒有錯(cuò)位。import numpy as np from sklearn.mixture import GaussianMixture # 構(gòu)造兩段模擬噪聲一套均值另一套不同均值 train_feat np.random.randn(80, 39) [1.0]*39 test_feat np.random.randn(20, 39) [1.2]*39 model GaussianMixture(n_components2, covariance_typediag).fit(train_feat) print(score on train:, model.score(train_feat)) print(score on test:, model.score(test_feat))這段代碼跑通后再把真實(shí)音頻的MFCC替換到train_feat和test_feat的位置。如果測試得分明顯下降就去查特征提取和歸一化方向如果得分反而更高則多半是訓(xùn)練集和測試集發(fā)生了文件級(jí)混疊需要重新劃分。把這個(gè)最小閉環(huán)固定成回歸用例以后改任何參數(shù)都可以先跑它再跑完整詞表驗(yàn)證。本文還有配套的精品資源點(diǎn)擊獲取