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