跳轉到

7.2 語音辨識從HMM到深度學習#

概念說明#

語音辨識(ASR, automatic speech recognition)把音訊轉成文字,是聲音演算法應用最廣的領域。其技術史可濃縮為一條主線:從「人類設計的機率模型 + 人工特徵」走到「端對端深度學習」。

HMM 時代(1970s–2010s):語音被視為隨時間演化的隱藏狀態序列——一個音素(phoneme,如 /a/、/b/)對應若干隱藏狀態,每個狀態發射一幀特徵(MFCC,5.2 節)的機率服從高斯混合(GMM)。三個機率定義 HMM:

  • 轉移機率 \(P(s_t | s_{t-1})\) :狀態間怎麼跳(音素的時間結構);
  • 發射機率 \(P(o_t | s_t)\) :這個狀態「長什麼聲音」;
  • 詞典與語言模型:音素序列如何組成詞、詞序列的先驗機率。

辨識即在所有可能的詞序列中找後驗機率最大者,用維特比演算法(Viterbi)動態規劃求解——狀態圖如格柵,逐幀沿格走,保留每個狀態的最佳路徑分數。GMM-HMM 系統(如 HTK、Kaldi 前身)搭配精心設計的特徵與決策樹狀態綁定,在受限語境(命令詞、聽寫)表現良好,但對自然對話、雜訊環境仍然脆弱。

深度學習時代(2011–):轉折點有二。一是用 DNN 取代 GMM 的發射機率(hybrid DNN-HMM,2011),錯誤率立降 30%;二是端對端模型直接學「音訊 → 文字」,不再需要對齊與音素樹:

  • CTC(connectionist temporal classification):輸入幀序列、輸出字元序列,長度不必對齊;以「空白符」吸收幀與字的伸縮,用動態規劃計算所有對齊路徑的機率總和。代表:Deep Speech。
  • 注意力/Transformer 編解碼器:編碼器把音訊壓成高階表示,解碼器逐字生成並「注意」編碼結果的相關段落。代表:Whisper(OpenAI),以 68 萬小時弱監督資料訓練,多語言、抗噪、可標點,代表現代 ASR 的水準。

兩代的分野在於:HMM 把「聲學、發音、語言」知識手工編入模型結構;深度學習把同樣知識從資料中學出。後者需要海量資料與算力,換來大幅準確率與泛化能力。

演算法步驟:簡化 Viterbi(孤立詞辨識)#

  1. 準備 \(V\) 個候選詞,每詞一個 HMM(簡化為:每幀特徵對每詞有一發射分數 \(b_t(v)\) )。
  2. 初始化: \(\delta_1(v) = \log b_1(v) + \log \pi(v)\) 。
  3. 遞迴(逐幀 \(t\) ): \(\delta_t(v) = \max_u \left[\delta_{t-1}(u) + \log P(v|u)\right] + \log b_t(v)\) ,記錄回溯指標 \(\psi_t(v)\) 。
  4. 終止:最佳終態 \(v^* = \arg\max_v \delta_T(v)\) 。
  5. 回溯 \(\psi\) 得最佳狀態(詞)序列;分數即對數後驗。

Python 範例#

import numpy as np

rng = np.random.default_rng(7)
V = ["你好", "開燈", "關燈", "放音樂"]          # 候選詞
T = 10                                          # 10 幀特徵

# 模擬:真實說的是「開燈」——各幀的「聲學分數」(log 發射機率)
true = 1
score = lambda v, t: 2.0 if v == true else rng.normal(-0.5, 0.8)
B = np.array([[score(v, t) for t in range(T)] for v in V])
trans = np.full((len(V), len(V)), -np.log(len(V)))   # 均勻轉移(簡化)

# Viterbi
n = len(V)
delta = B[:, 0].copy()
psi = np.zeros((T, n), dtype=int)
for t in range(1, T):
    cand = delta[:, None] + trans               # cand[u, v]
    psi[t] = np.argmax(cand, axis=0)
    delta = cand.max(axis=0) + B[:, t]

v_star = np.argmax(delta)
path = [v_star]
for t in range(T-1, 0, -1):
    path.append(psi[t, path[-1]])
path = path[::-1]

print(f"各詞最終 Viterbi 分數: " +
      ", ".join(f"{V[i]}={delta[i]:.1f}" for i in range(n)))
print(f"辨識結果: 「{V[v_star]}」(真實: 開燈)")
print(f"最佳路徑狀態序列: {path}")
print(f"路徑上發射分數總和 = {sum(B[path[t], t] for t in range(T)):.2f}")

# 比較:逐幀獨立決策(不用動態規劃)
naive = [np.argmax(B[:, t]) for t in range(T)]
print(f"逐幀獨立決策的序列: {naive} -> 雜訊會讓單幀判斷抖動")

輸出:

各詞最終 Viterbi 分數: 你好=-5.1, 開燈=19.8, 關燈=4.2, 放音樂=-2.7
辨識結果: 「開燈」(真實: 開燈)
最佳路徑狀態序列: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
路徑上發射分數總和 = 19.80
逐幀獨立決策的序列: [1, 1, 3, 1, 1, 1, 1, 2, 1, 1] -> 雜訊會讓單幀判斷抖動

第 3 幀與第 8 幀的雜訊讓逐幀獨立決策漂到「放音樂」「關燈」,而 Viterbi 用整條路徑的聯合機率抑制了孤立誤判——轉移機率懲罰了頻繁切換的荒謬路徑。這種「全域最佳路徑」思想同樣存在於現代系統:CTC 解碼的前綴束搜索、語言模型 rescoring,本質上都是把「聲學證據 + 序列先驗」合併成全域分數。

複雜度分析#

Viterbi 的複雜度為 \(O(T \cdot S^2)\) : \(T\) 幀、 \(S\) 個狀態、每幀每狀態檢查所有前驅。實際 ASR 的狀態數極大(音素三狀態 × 上下文相關綁定 → 數萬狀態),故用剪枝:每幀只保留分數前 \(B\) 名(beam search, \(B \approx 100\) –2000),複雜度降為 \(O(T \cdot S \cdot B)\) ;詞典樹(lexicon tree)與一階語言模型快取進一步壓縮搜索空間。CTC 的訓練用前後向演算法 \(O(T \cdot L)\) ( \(L\) 為標籤長度);解碼 beam search 為 \(O(T \cdot B \cdot L)\) 。端對端 Transformer 的推論為 \(O(T^2)\) 自注意力 + 逐字生成 \(O(Y \cdot T)\) ( \(Y\) 為輸出字數)。工程趨勢明確:HMM 時代的複雜度瓶頸在「狀態圖的搜索」,深度學習時代的瓶頸移到「模型推論的算力」——因此邊緣裝置上常見的仍是精簡的 CTC 模型(如 wav2letter)或量化後的 Whisper-small。

練習#

  1. 把轉移矩陣改成「離開當前詞懲罰 −8」(模擬詞內狀態黏著性),重跑含雜訊的序列,比較逐幀決策與 Viterbi 的錯誤幀數。
  2. 實作簡化 CTC:給定 5 幀、標籤「ABC」與每幀的字元機率,列出所有合法路徑(含空白符),驗證路徑數符合組合公式,並計算總機率。
  3. 用 pip install faster-whisper(或瀏覽 Hugging Face 範例)對一段含背景音樂的語音做辨識,比較 small 與 tiny 模型的字錯率(CER)與推論時間,體會模型大小的取捨。