《语音信号处理试验教程》(梁瑞宇等)的代码主要是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\}} M={A,B,π}来定义,假设某HMM一共有N个状态 { S i } i − 1 N \{S_i\}_{i-1}^N {Si}i−1N,那么参数的定义为:
A \bold{A} A:状态转移概率矩阵;
A = [ a 11 . . . a 1 N . . . . . . . . . a N 1 . . . a N N ] A=\begin{bmatrix} a_{11}& ...& a_{1N}\\...&...&...\\a_{N1}&...&a_{NN} \end{bmatrix} A=⎣⎡a11...aN1.........a1N...aNN⎦⎤
其中 a i j a_{ij} aij是从状态 S i S_i Si到状态 S j S_j Sj转移时的转移概率,并且 0 ⩽ a i j ⩽ 1 , ∑ i = 1 N a i j = 1 0\leqslant a_{ij}\leqslant 1,\sum_{i=1}^Na_{ij}=1 0⩽aij⩽1,∑i=1Naij=1
π \pi π:系统初始状态概率的集合, { π } i = 1 N \{\pi\}_{i=1}^N {π}i=1N表示初始状态是 s i s_i si的概率,即: π i = P [ S 1 = s i ] ( 1 ⩽ i ⩽ N ) , ∑ i = 1 N π i j = 1 \pi_i=P[S_1=s_i](1\leqslant i\leqslant N),\sum_{i=1}^N\pi_{ij}=1 πi=P[S1=si](1⩽i⩽N),∑i=1Nπij=1
B \bold{B} B:处处观测值概率集合, B = { b i j ( k ) } \bold{B}=\{b_{ij}(k)\} B={bij(k)},其中 b i j ( k ) b_{ij}(k) bij(k)是从状态 S i S_i Si到状态 S j S_j Sj转移时,观测值为k的输出概率。根据观察集合 X X X的取值可将HMM分为连续型和离散型。
用来计算给定观测值序列 O = o 1 o 2 . . . o T \bold{O}=o_1o_2...o_T O=o1o2...oT以及一个模型 M = { A , B , π } \bold{M=\{A,B,\pi\}} M={A,B,π}时,由模型 M M M产生出 O O O的概率 P ( O ∣ M ) \bold{P(O|M)} P(O∣M),设 S 1 S_1 S1是初始状态, S N S_N SN是终了状态,则前向-后向算法描述如下:
(1)前向算法
根据输出观察值序列重前向后递推计算输出序列;
符号 | 函数 |
---|---|
O = o 1 o 2 . . . o T \bold{O}=o_1o_2...o_T O=o1o2...oT | 输出观察序列列表 |
P ( O ∥ M ) \bold{P(O\|M)} P(O∥M) | 给定模型M时,输出符号序列O的概率 |
a i j a_{ij} aij | 状态 S i S_i Si到状态 S j S_j Sj的转移概率 |
b i j ( o t ) b_{ij}(o_t) bij(ot) | 状态 S i S_i Si到状态 S j S_j Sj的转移时输出 o t o_t ot的概率 |
α t ( j ) \bold{\alpha_t(j)} αt(j) | 输出部分符号序列 o 1 o 2 . . . o t o_1o_2...o_t o1o2...ot并到达状态 S j S_j Sj的概率(前向概率) |
α t ( j ) \bold{\alpha_t(j)} αt(j)可以由递推计算:初始化为 α 0 ( 1 ) = 1 , α 0 ( j ) = 0 ( j ≠ 1 ) \bold{\alpha_0(1)}=1,\bold{\alpha_0(j)}=0(j\neq 1) α0(1)=1,α0(j)=0(j=1)
递推公式为:
α t ( j ) = ∑ i α t − 1 ( i ) a i j b i j ( o t ) , ( 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) αt(j)=i∑αt−1(i)aijbij(ot),(t=1,2,...,T;i,j=1,2,...,N)
最后结果:
P ( O ∣ M ) = α T ( N ) \bold{P(O|M)}=\bold{\alpha_T}(N) P(O∣M)=αT(N)
t时刻的 α t ( j ) \bold{\alpha_t}(j) αt(j)等于t-1时刻的所有状态的 α t − 1 ( i ) a i j b i j ( o t ) \bold{\alpha_{t-1}}(i)a_{ij}b_{ij}(o_t) αt−1(i)aijbij(ot)的和,如果状态 S i S_i Si到状态 S j S_j Sj没有转移时, a i j = 0 a_{ij}=0 aij=0.前向算法计算量大大减小,为 N ( N + 1 ) ( T − 1 ) N(N+1)(T-1) N(N+1)(T−1)次乘法和 N ( N − 1 ) ( T + 1 ) N(N-1)(T+1) N(N−1)(T+1)次加法。
(2)后向算法
定义 β t ( i ) \bold{\beta}_t(i) βt(i)为后向概率,即从状态 S i S_i Si开始到状态 S N S_N SN结束输出部分符号序列为 o t + 1 , o t + 2 , . . . , o T o_{t+1},o_{t+2},...,o_{T} ot+1,ot+2,...,oT的概率,初始化: β T ( N ) = 1 , β T ( j ) = 0 , ( j ≠ N ) \bold{\beta_T(N)}=1,\bold{\beta_T(j)}=0,(j\neq N) βT(N)=1,βT(j)=0,(j=N)
递推公式:
β t ( i ) = ∑ j β t + 1 ( j ) a i j b i j ( o t + 1 ) , ( t = T , T − 1 , . . . , 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) βt(i)=j∑βt+1(j)aijbij(ot+1),(t=T,T−1,...,1;i,j=1,2,...,N)
所以: P ( O ∣ M ) = ∑ i = 1 N β 1 ( i ) π i = β 0 ( 1 ) \bold{P(O|M)}=\sum\limits_{i=1}^N\beta_1(i)\pi_i=\beta_0(1) P(O∣M)=i=1∑Nβ1(i)πi=β0(1)
后向计算的计算量为 N 2 T N^2T N2T,根据定义可以知道: P ( O ∣ M ) = ∑ i = 1 N ∑ j = 1 N α t ( i ) a i j b i j ( o t + 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) P(O∣M)=i=1∑Nj=1∑Nαt(i)aijbij(ot+1)βt+1(j)
Viterbi解决的是给定观察符号序列 O = o 1 o 2 . . . o T O=o_1o_2...o_T O=o1o2...oT和模型 M = { A , B , π } \bold{M=\{A,B,\pi\}} M={A,B,π},求出状态序列 S = s 1 s 2 . . . s T S=s_1s_2...s_T S=s1s2...sT的问题。最佳意义上的状态序列是使 P ( S , O ∣ M ) P(S,O|M) P(S,O∣M)最大时确定的状态序列,即HMM输出一个观察值序列 O = o 1 o 2 . . . o T O=o_1o_2...o_T O=o1o2...oT时,可能通过的状态序列有多种,这里面使输出概率最大的序列状态。
初始化: α 0 ′ ( 1 ) = 1 , α 0 ′ ( j ) = 0 ( j ≠ 1 ) \alpha_0'(1)=1,\alpha_0'(j)=0(j\neq 1) α0′(1)=1,α0′(j)=0(j=1)
递推公式: α t ′ ( j ) = max i α t − 1 ′ ( j − 1 ) a i j b i j ( o t ) , ( 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) αt′(j)=imaxαt−1′(j−1)aijbij(ot),(t=1,2,...,T;i,j=1,2,...,N)
最后: P max ( S , O ∣ M ) = α T ′ ( N ) P_{\max}(S,O|M)=\alpha_T'(N) Pmax(S,O∣M)=αT′(N)
每一次使 α t ′ ( j ) \alpha_t'(j) αt′(j)最大的状态i组成的状态序列就是所求的最佳状态序列。求最佳状态序列的方式为:
1)给定每个状态准备一个数组变量 α t ′ ( j ) \alpha_t'(j) αt′(j),初始化时,令初始状态 S 1 S_1 S1的数组变量 α 0 ′ ( 1 ) = 1 \alpha_0'(1)=1 α0′(1)=1,其他状态 α 0 ′ ( j ) = 0 \alpha_0'(j)=0 α0′(j)=0
2)根据t时刻输出的观察符号 o t o_t ot计算 α t ′ ( j ) = max i α t − 1 ′ a i j b i j ( o t ) = max i { α t − 1 ′ a 1 j b 1 j ( o t ) , α t − 1 ′ a 2 j b 2 j ( o t ) , . . . , α t − 1 ′ a N j b N j ( o t ) } \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)\} αt′(j)=imaxαt−1′aijbij(ot)=imax{αt−1′a1jb1j(ot),αt−1′a2jb2j(ot),...,αt−1′aNjbNj(ot)},当状态 S i S_i Si到状态 S j S_j Sj没有转移时, a i j = 0 a_{ij}=0 aij=0。设计一个符号数组变量,称为追加状态序列寄存器,利用这个最佳状态序列寄存器吧每次使得 α t ′ ( j ) \alpha_t'(j) αt′(j)最大的状态保存下来。
3)当 t ≠ T t\neq T t=T时转移到2),否则转移到4)。
4)把最终的状态寄存器 α T ′ ( N ) \alpha_T'(N) αT′(N)内的值取出,则 P max ( S , O ∣ M ) = α T ′ ( N ) P_{\max}(S,O|M)=\alpha_T'(N) Pmax(S,O∣M)=αT′(N)为最佳状态序列寄存器的值,就是所求的最佳状态序列。
Baum-Welch算法解决的是HMM训练,即HMM参数估计问题,给定一个观察序列 O = o 1 o 2 . . . o T O=o_1o_2...o_T O=o1o2...oT,该算法能确定一个 M = { A , B , π } M=\{A,B,\pi\} M={A,B,π},使 P ( O ∣ M ) P(O|M) P(O∣M)最大,这是一个泛函极值问题。由于给定的训练序列有限,因而不存在一个最佳的方法来估计模型,Baum-Welch算法也是利用递归思想,使 P ( O ∣ M ) P(O|M) P(O∣M)局部放大后,最后得到优化的模型参数 M = { A , B , π } M=\{A,B,\pi\} M={A,B,π}。利用Baum-Welch算法重估公式得到的新模型 M ^ \hat M M^,一定有 P ( O ∣ M ^ ) > P ( O ∣ M ) P(O|\hat M)>P(O|M) P(O∣M^)>P(O∣M),重复估计过程,直到 P ( O ∣ M ^ ) P(O|\hat M) P(O∣M^)收敛,不再明显增大,此时的 M ^ \hat M M^即为所求模型。
给定一个观察值符号序列 O = o 1 o 2 . . . o T O=o_1o_2...o_T O=o1o2...oT,以及一个需要通过训练进行重估计参数的HMM模型 M = { A , B , π } M=\{A,B,\pi\} M={A,B,π},按前向-后向算法,设对于符号序列,在时刻t从状态 S i S_i Si到状态 S j S_j Sj的转移概率为 γ t ( i , j ) \gamma_t(i,j) γt(i,j):
γ t ( i , j ) = α t − 1 ( i ) a i j b i j ( o t ) β t ( j ) α T ( N ) = α t − 1 ( i ) a i j b i j ( o t ) β 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)} γt(i,j)=αT(N)αt−1(i)aijbij(ot)βt(j)=∑iαt(i)βt(i)αt−1(i)aijbij(ot)βt(j)
同时,对于符号序列 O = o 1 o 2 . . . o T O=o_1o_2...o_T O=o1o2...oT,在t时刻的Markov链处于状态 S i S_i Si的概率为:
∑ j = 1 N γ 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)} j=1∑Nγt(i,j)=∑iαt(i)βt(i)αt(i)βt(i)
这时,状态 S i S_i Si到状态 S j S_j Sj转移次数的期望为 ∑ t γ t ( i , j ) \sum_t\gamma_t(i,j) ∑tγt(i,j),而从状态 S i S_i Si转移出去的次数期望为 ∑ j ∑ t γ t ( i , j ) \sum_j\sum_t\gamma_t(i,j) ∑j∑tγt(i,j),所以重估公式为:
a ^ i j = ∑ t γ t ( i , j ) ∑ j ∑ t γ t ( i , j ) = ∑ t α t − 1 ( i ) a i j b i j ( o t ) β 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)} a^ij=∑j∑tγt(i,j)∑tγt(i,j)=∑tαt(i)βt(i)∑tαt−1(i)aijbij(ot)βt(j)
b ^ i j = ∑ t : o t = k γ t ( i , j ) ∑ t γ t ( i , j ) = ∑ t : o t = k α t − 1 ( i ) a i j b i j ( o t ) β t ( j ) ∑ t α t − 1 ( i ) a i j b i j ( o t ) β 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)} b^ij=∑tγt(i,j)t:ot=k∑γt(i,j)=∑tαt−1(i)aijbij(ot)βt(j)t:ot=k∑αt−1(i)aijbij(ot)βt(j)
得到的新模型就是 M ^ = { A ^ , B ^ , π ^ } \hat M=\{\hat A,\hat B,\hat\pi\} M^={A^,B^,π^}。
具体的实现步骤为:
1)适当地选择 a i j a_{ij} aij和 b i j ( k ) b_{ij}(k) bij(k)的初始值,常用的设定方式为:给予从状态i转移出去的每条弧相等的转移概率,即
a i j = 1 从 状 态 i 转 移 出 去 的 弧 的 条 数 a_{ij}=\frac{1}{从状态i转移出去的弧的条数} aij=从状态i转移出去的弧的条数1
给予每个输出观察符号相等的输出概率初始值,即:
b i j ( k ) = 1 码 本 中 码 字 的 个 数 b_{ij}(k)=\frac{1}{码本中码字的个数} bij(k)=码本中码字的个数1
并且每条弧上给予相同的输出概率矩阵。
2)给定一个(训练)观察值符号序列 O = o 1 o 2 . . . o T O=o_1o_2...o_T O=o1o2...oT,由初始模型计算 γ t ( i , j ) \gamma_t(i,j) γt(i,j),并且由重估公式,计算 a ^ i j \hat a_{ij} a^ij和 b ^ i j ( k ) \hat b_{ij}(k) b^ij(k).
3)再给定一个(训练)观察值序列 O = o 1 o 2 . . . o T O=o_1o_2...o_T O=o1o2...oT,吧前一次的 a ^ i j \hat a_{ij} a^ij和 b ^ i j ( k ) \hat b_{ij}(k) b^ij(k)作为初始模型计算 γ t ( i , j ) \gamma_t(i,j) γt(i,j),重新计算 a ^ i j \hat a_{ij} a^ij和 b ^ i j ( k ) \hat b_{ij}(k) b^ij(k).
4)直到 a ^ i j \hat a_{ij} a^ij和 b ^ i j ( k ) \hat b_{ij}(k) b^ij(k)收敛为止。
语音识别一般采用从左到右的HMM,所以初始状态概率 π i \pi_i πi不需要顾及,总设定为:
π 1 = 1 , π i = 0 , ( i = 2 , . . . , N ) \pi_1=1,\pi_i=0,(i=2,...,N) π1=1,π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)