Python語音基礎操作--10.2隱馬爾科夫模型的孤立字識別

《語音信號處理試驗教程》(梁瑞宇等)的代碼主要是Matlab實現的,現在Python比較熱門,所以把這個項目大部分內容寫成了Python實現,大部分是手動寫的。使用CSDN博客查看幫助文件:

Python語音基礎操作–2.1語音錄製,播放,讀取
Python語音基礎操作–2.2語音編輯
Python語音基礎操作–2.3聲強與響度
Python語音基礎操作–2.4語音信號生成
Python語音基礎操作–3.1語音分幀與加窗
Python語音基礎操作–3.2短時時域分析
Python語音基礎操作–3.3短時頻域分析
Python語音基礎操作–3.4倒譜分析與MFCC係數
Python語音基礎操作–4.1語音端點檢測
Python語音基礎操作–4.2基音週期檢測
Python語音基礎操作–4.3共振峯估計
Python語音基礎操作–5.1自適應濾波
Python語音基礎操作–5.2譜減法
Python語音基礎操作–5.4小波分解
Python語音基礎操作–6.1PCM編碼
Python語音基礎操作–6.2LPC編碼
Python語音基礎操作–6.3ADPCM編碼
Python語音基礎操作–7.1幀合併
Python語音基礎操作–7.2LPC的語音合成
Python語音基礎操作–10.1基於動態時間規整(DTW)的孤立字語音識別試驗
Python語音基礎操作–10.2隱馬爾科夫模型的孤立字識別
Python語音基礎操作–11.1矢量量化(VQ)的說話人情感識別
Python語音基礎操作–11.2基於GMM的說話人識別模型
Python語音基礎操作–12.1基於KNN的情感識別
Python語音基礎操作–12.2基於神經網絡的情感識別
Python語音基礎操作–12.3基於支持向量機SVM的語音情感識別
Python語音基礎操作–12.4基於LDA,PCA的語音情感識別

代碼可在Github上下載busyyang/python_sound_open

隱馬爾科夫模型(Hidden Markov Models, HMM)作爲語音信號的一種統計模型,在語音處理中得到廣泛應用。

一個用於語音識別的HMM通常用三組模型參數M={A,B,π}\bold{M=\{A,B,\pi\}}來定義,假設某HMM一共有N個狀態{Si}i1N\{S_i\}_{i-1}^N,那麼參數的定義爲:
A\bold{A}:狀態轉移概率矩陣;
A=[a11...a1N.........aN1...aNN]A=\begin{bmatrix} a_{11}& ...& a_{1N}\\...&...&...\\a_{N1}&...&a_{NN} \end{bmatrix}

其中aija_{ij}是從狀態SiS_i到狀態SjS_j轉移時的轉移概率,並且0aij1,i=1Naij=10\leqslant a_{ij}\leqslant 1,\sum_{i=1}^Na_{ij}=1

π\pi:系統初始狀態概率的集合,{π}i=1N\{\pi\}_{i=1}^N表示初始狀態是sis_i的概率,即:πi=P[S1=si](1iN),i=1Nπij=1\pi_i=P[S_1=s_i](1\leqslant i\leqslant N),\sum_{i=1}^N\pi_{ij}=1
B\bold{B}:處處觀測值概率集合,B={bij(k)}\bold{B}=\{b_{ij}(k)\},其中bij(k)b_{ij}(k)是從狀態SiS_i到狀態SjS_j轉移時,觀測值爲k的輸出概率。根據觀察集合XX的取值可將HMM分爲連續型和離散型。

前向-後向算法

用來計算給定觀測值序列O=o1o2...oT\bold{O}=o_1o_2...o_T以及一個模型M={A,B,π}\bold{M=\{A,B,\pi\}}時,由模型MM產生出OO的概率P(OM)\bold{P(O|M)},設S1S_1是初始狀態,SNS_N是終了狀態,則前向-後向算法描述如下:
(1)前向算法
根據輸出觀察值序列重前向後遞推計算輸出序列;

符號 函數
O=o1o2...oT\bold{O}=o_1o_2...o_T 輸出觀察序列列表
P(OM)\bold{P(O\|M)} 給定模型M時,輸出符號序列O的概率
aija_{ij} 狀態SiS_i到狀態SjS_j的轉移概率
bij(ot)b_{ij}(o_t) 狀態SiS_i到狀態SjS_j的轉移時輸出oto_t的概率
αt(j)\bold{\alpha_t(j)} 輸出部分符號序列o1o2...oto_1o_2...o_t併到達狀態SjS_j的概率(前向概率)

αt(j)\bold{\alpha_t(j)}可以由遞推計算:初始化爲α0(1)=1,α0(j)=0(j1)\bold{\alpha_0(1)}=1,\bold{\alpha_0(j)}=0(j\neq 1)

遞推公式爲:
αt(j)=iαt1(i)aijbij(ot),(t=1,2,...,T;i,j=1,2,...,N)\bold{\alpha_t}(j)=\sum_i\bold{\alpha_{t-1}}(i)a_{ij}b_{ij}(o_t),(t=1,2,...,T;i,j=1,2,...,N)

最後結果:
P(OM)=αT(N)\bold{P(O|M)}=\bold{\alpha_T}(N)

t時刻的αt(j)\bold{\alpha_t}(j)等於t-1時刻的所有狀態的αt1(i)aijbij(ot)\bold{\alpha_{t-1}}(i)a_{ij}b_{ij}(o_t)的和,如果狀態SiS_i到狀態SjS_j沒有轉移時,aij=0a_{ij}=0.前向算法計算量大大減小,爲N(N+1)(T1)N(N+1)(T-1)次乘法和N(N1)(T+1)N(N-1)(T+1)次加法。
(2)後向算法
定義βt(i)\bold{\beta}_t(i)爲後向概率,即從狀態SiS_i開始到狀態SNS_N結束輸出部分符號序列爲ot+1,ot+2,...,oTo_{t+1},o_{t+2},...,o_{T}的概率,初始化:βT(N)=1,βT(j)=0,(jN)\bold{\beta_T(N)}=1,\bold{\beta_T(j)}=0,(j\neq N)
遞推公式:
βt(i)=jβt+1(j)aijbij(ot+1),(t=T,T1,...,1;i,j=1,2,...,N)\bold{\beta}_t(i)=\sum_j\beta_{t+1}(j)a_{ij}b_{ij}(o_{t+1}),(t=T,T-1,...,1;i,j=1,2,...,N)

所以:P(OM)=i=1Nβ1(i)πi=β0(1)\bold{P(O|M)}=\sum\limits_{i=1}^N\beta_1(i)\pi_i=\beta_0(1)

後向計算的計算量爲N2TN^2T,根據定義可以知道:P(OM)=i=1Nj=1Nαt(i)aijbij(ot+1)βt+1(j)\bold{P(O|M)}=\sum\limits_{i=1}^N\sum\limits_{j=1}^N\alpha_t(i)a_{ij}b_{ij}(o_{t+1})\beta_{t+1}(j)

維特比(Viterbi)算法

Viterbi解決的是給定觀察符號序列O=o1o2...oTO=o_1o_2...o_T和模型M={A,B,π}\bold{M=\{A,B,\pi\}},求出狀態序列S=s1s2...sTS=s_1s_2...s_T的問題。最佳意義上的狀態序列是使P(S,OM)P(S,O|M)最大時確定的狀態序列,即HMM輸出一個觀察值序列O=o1o2...oTO=o_1o_2...o_T時,可能通過的狀態序列有多種,這裏面使輸出概率最大的序列狀態。
初始化:α0(1)=1,α0(j)=0(j1)\alpha_0'(1)=1,\alpha_0'(j)=0(j\neq 1)
遞推公式:αt(j)=maxiαt1(j1)aijbij(ot),(t=1,2,...,T;i,j=1,2,...,N)\alpha_t'(j)=\underset{i}{\max}\alpha_{t-1}'(j-1)a_{ij}b_{ij}(o_t),(t=1,2,...,T;i,j=1,2,...,N)
最後:Pmax(S,OM)=αT(N)P_{\max}(S,O|M)=\alpha_T'(N)
每一次使αt(j)\alpha_t'(j)最大的狀態i組成的狀態序列就是所求的最佳狀態序列。求最佳狀態序列的方式爲:
1)給定每個狀態準備一個數組變量αt(j)\alpha_t'(j),初始化時,令初始狀態S1S_1的數組變量α0(1)=1\alpha_0'(1)=1,其他狀態α0(j)=0\alpha_0'(j)=0
2)根據t時刻輸出的觀察符號oto_t計算αt(j)=maxiαt1aijbij(ot)=maxi{αt1a1jb1j(ot),αt1a2jb2j(ot),...,αt1aNjbNj(ot)}\alpha_t'(j)=\underset{i}{\max}\alpha_{t-1}'a_{ij}b_{ij}(o_t)=\underset{i}{\max}\{\alpha_{t-1}'a_{1j}b_{1j}(o_t),\alpha_{t-1}'a_{2j}b_{2j}(o_t),...,\alpha_{t-1}'a_{Nj}b_{Nj}(o_t)\},當狀態SiS_i到狀態SjS_j沒有轉移時,aij=0a_{ij}=0。設計一個符號數組變量,稱爲追加狀態序列寄存器,利用這個最佳狀態序列寄存器吧每次使得αt(j)\alpha_t'(j)最大的狀態保存下來。
3)當tTt\neq T時轉移到2),否則轉移到4)。
4)把最終的狀態寄存器αT(N)\alpha_T'(N)內的值取出,則Pmax(S,OM)=αT(N)P_{\max}(S,O|M)=\alpha_T'(N)爲最佳狀態序列寄存器的值,就是所求的最佳狀態序列。

Baum-Welch算法

Baum-Welch算法解決的是HMM訓練,即HMM參數估計問題,給定一個觀察序列O=o1o2...oTO=o_1o_2...o_T,該算法能確定一個M={A,B,π}M=\{A,B,\pi\},使P(OM)P(O|M)最大,這是一個泛函極值問題。由於給定的訓練序列有限,因而不存在一個最佳的方法來估計模型,Baum-Welch算法也是利用遞歸思想,使P(OM)P(O|M)局部放大後,最後得到優化的模型參數M={A,B,π}M=\{A,B,\pi\}。利用Baum-Welch算法重估公式得到的新模型M^\hat M,一定有P(OM^)>P(OM)P(O|\hat M)>P(O|M),重複估計過程,直到P(OM^)P(O|\hat M)收斂,不再明顯增大,此時的M^\hat M即爲所求模型。

給定一個觀察值符號序列O=o1o2...oTO=o_1o_2...o_T,以及一個需要通過訓練進行重估計參數的HMM模型M={A,B,π}M=\{A,B,\pi\},按前向-後向算法,設對於符號序列,在時刻t從狀態SiS_i到狀態SjS_j的轉移概率爲γt(i,j)\gamma_t(i,j):
γt(i,j)=αt1(i)aijbij(ot)βt(j)αT(N)=αt1(i)aijbij(ot)βt(j)iαt(i)βt(i)\gamma_t(i,j)=\frac{\alpha_{t-1}(i)a_{ij}b_{ij}(o_t)\beta_t(j)}{\alpha_T(N)}=\frac{\alpha_{t-1}(i)a_{ij}b_{ij}(o_t)\beta_t(j)}{\sum_i\alpha_t(i)\beta_t(i)}

同時,對於符號序列O=o1o2...oTO=o_1o_2...o_T,在t時刻的Markov鏈處於狀態SiS_i的概率爲:
j=1Nγt(i,j)=αt(i)βt(i)iαt(i)βt(i)\sum\limits_{j=1}^N\gamma_t(i,j)=\frac{\alpha_t(i)\beta_t(i)}{\sum_i\alpha_t(i)\beta_t(i)}

這時,狀態SiS_i到狀態SjS_j轉移次數的期望爲tγt(i,j)\sum_t\gamma_t(i,j),而從狀態SiS_i轉移出去的次數期望爲jtγt(i,j)\sum_j\sum_t\gamma_t(i,j),所以重估公式爲:
a^ij=tγt(i,j)jtγt(i,j)=tαt1(i)aijbij(ot)βt(j)tαt(i)βt(i)\hat a_{ij}=\frac{\sum_t\gamma_t(i,j)}{\sum_j\sum_t\gamma_t(i,j)}=\frac{\sum_t\alpha_{t-1}(i)a_{ij}b_{ij}(o_t)\beta_t(j)}{\sum_t\alpha_t(i)\beta_t(i)}

b^ij=t:ot=kγt(i,j)tγt(i,j)=t:ot=kαt1(i)aijbij(ot)βt(j)tαt1(i)aijbij(ot)βt(j)\hat b_{ij}=\frac{\sum\limits_{t:o_t=k}\gamma_t(i,j)}{\sum_t\gamma_t(i,j)}=\frac{\sum\limits_{t:o_t=k}\alpha_{t-1}(i)a_{ij}b_{ij}(o_t)\beta_t(j)}{\sum_t\alpha_{t-1}(i)a_{ij}b_{ij}(o_t)\beta_t(j)}

得到的新模型就是M^={A^,B^,π^}\hat M=\{\hat A,\hat B,\hat\pi\}
具體的實現步驟爲:
1)適當地選擇aija_{ij}bij(k)b_{ij}(k)的初始值,常用的設定方式爲:給予從狀態i轉移出去的每條弧相等的轉移概率,即
aij=1ia_{ij}=\frac{1}{從狀態i轉移出去的弧的條數}

給予每個輸出觀察符號相等的輸出概率初始值,即:
bij(k)=1b_{ij}(k)=\frac{1}{碼本中碼字的個數}

並且每條弧上給予相同的輸出概率矩陣。
2)給定一個(訓練)觀察值符號序列O=o1o2...oTO=o_1o_2...o_T,由初始模型計算γt(i,j)\gamma_t(i,j),並且由重估公式,計算a^ij\hat a_{ij}b^ij(k)\hat b_{ij}(k).
3)再給定一個(訓練)觀察值序列O=o1o2...oTO=o_1o_2...o_T,吧前一次的a^ij\hat a_{ij}b^ij(k)\hat b_{ij}(k)作爲初始模型計算γt(i,j)\gamma_t(i,j),重新計算a^ij\hat a_{ij}b^ij(k)\hat b_{ij}(k).
4)直到a^ij\hat a_{ij}b^ij(k)\hat b_{ij}(k)收斂爲止。

語音識別一般採用從左到右的HMM,所以初始狀態概率πi\pi_i不需要顧及,總設定爲:
π1=1,πi=0,(i=2,...,N)\pi_1=1,\pi_i=0,(i=2,...,N)

from chapter3_分析實驗.mel import Nmfcc
from scipy.io import wavfile, loadmat
from hmmlearn import hmm
from sklearn.externals import joblib
import numpy as np
import os

"""
代碼來自:https://blog.csdn.net/chinatelecom08/article/details/82901480
並進行了部分更改
"""


def gen_wavlist(wavpath):
    """
    得到數據文件序列
    :param wavpath:
    :return:
    """
    wavdict = {}
    labeldict = {}
    for (dirpath, dirnames, filenames) in os.walk(wavpath):
        for filename in filenames:
            if filename.endswith('.wav'):
                filepath = os.sep.join([dirpath, filename])
                fileid = filename.strip('.wav')
                wavdict[fileid] = filepath
                label = fileid.split('_')[1]
                labeldict[fileid] = label
    return wavdict, labeldict


def compute_mfcc(file):
    """
    讀取數據並計算mfcc
    :param file: 文件名
    :return: mfcc係數
    """
    """
        有手動修改wavfile.read()函數的返回值,添加了bit_depth的返回,如果報錯,修改調用方式爲:
        fs, audio = wavfile.read(file)
        2020-3-20   Jie Y.
    """
    fs, audio, bits = wavfile.read(file)
    """
        由於部分信號太短而報錯,所以fs//2了
    """
    mfcc = Nmfcc(audio, fs // 2, 12, frameSize=int(fs // 2 * 0.025), inc=int(fs // 2 * 0.01))
    return mfcc


'''
&usage:		搭建HMM-GMM的孤立詞識別模型
參數意義:
	CATEGORY:	所有標籤的列表
	n_comp:		每個孤立詞中的狀態數
	n_mix:		每個狀態包含的混合高斯數量
	cov_type:	協方差矩陣的類型
	n_iter:		訓練迭代次數
'''


class Model:
    def __init__(self, CATEGORY=None, n_comp=3, n_mix=3, cov_type='diag', n_iter=1000):
        super(Model, self).__init__()
        self.CATEGORY = CATEGORY
        self.category = len(CATEGORY)
        self.n_comp = n_comp
        self.n_mix = n_mix
        self.cov_type = cov_type
        self.n_iter = n_iter
        # 關鍵步驟,初始化models,返回特定參數的模型的列表
        self.models = []
        for k in range(self.category):
            model = hmm.GMMHMM(n_components=self.n_comp, n_mix=self.n_mix, covariance_type=self.cov_type,
                               n_iter=self.n_iter)
            self.models.append(model)

    def train(self, tdata):
        for i in range(tdata.shape[1]):
            model = self.models[i]
            for x in range(tdata[0, i].shape[1]):
                data = tdata[0, i][0, x].squeeze()
                mfcc = Nmfcc(data, 8000, 24, 256, 80)
                model.fit(mfcc)

    def test(self, pdata):
        label = []
        result = []
        for k in range(pdata.shape[1]):
            for i in range(pdata[0, k].shape[1]):
                label.append(str(k + 1))
                data = pdata[0, k][0, i].squeeze()
                mfcc = Nmfcc(data, 8000, 24, 256, 80)
                result_one = []
                for m in range(self.category):
                    model = self.models[m]
                    re = model.score(mfcc)
                    result_one.append(re)
                result.append(self.CATEGORY[np.argmax(np.array(result_one))])
        print('識別得到結果:\n', result)
        print('原始標籤類別:\n', label)
        # 檢查識別率,爲:正確識別的個數/總數
        totalnum = len(label)
        correctnum = 0
        for i in range(totalnum):
            if result[i] == label[i]:
                correctnum += 1
        print('識別率:', correctnum / totalnum)

    def save(self, path="models.pkl"):
        joblib.dump(self.models, path)

    def load(self, path="models.pkl"):
        self.models = joblib.load(path)


tdata = loadmat('tra_data.mat')['tdata']
pdata = loadmat('rec_data.mat')['rdata']

CATEGORY = [str(i + 1) for i in range(tdata.shape[1])]
# 進行訓練
models = Model(CATEGORY=CATEGORY)
models.train(tdata)
models.test(tdata)
models.test(pdata)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章