赞
踩
pyAudioKits是基于librosa和其他库的强大Python音频工作流支持。
通过pip安装:
pip install pyAudioKits
本项目的GitHub地址,如果这个项目帮助到了你,请为它点上一颗star,谢谢你的支持!如果你在使用过程中有任何问题,请在评论区留言或在GitHub上提issue,我将持续对该项目进行维护。
import pyAudioKits.audio as ak
import numpy as np
import pyAudioKits.analyse as aly
在介绍完通用音频后,本节进一步开始对语音信号进行研究。通用音频的理论和分析方法均适用于语音信号,而语音信号又有更多进一步的属性和分析方法,以及一些特有的任务。本节将主要对语音的端点检测和语音的特征提取和识别用pyAudioKits的实现方法进行演示。其中端点检测使用双门限法,而特征提取和识别使用MFCC+DTW方法。
语音信号由三种成分构成。
浊音:具有周期性或准周期性,其频率被称为基频( F 0 F_0 F0)或音调。近似乐音。
F 0 F_0 F0 avg (Hz) | F 0 F_0 F0 min (Hz) | F 0 F_0 F0 max (Hz) | |
---|---|---|---|
男性 | 125 | 80 | 200 |
女性 | 225 | 150 | 350 |
儿童 | 300 | 200 | 500 |
清音:没有周期性,没有固定的音高。近似噪音。
静音:依然存在背景噪音。
这里的三种成分在时间上互斥,而并非是叠加的,因此语音信号可以按时间顺序进行切分,并归类到上述三种类别中。
语音是通过上节提到过的线性时不变系统产生的,即 Y ( z ) = H ( z ) X ( z ) Y(z)=H(z)X(z) Y(z)=H(z)X(z),其中 y y y是生成的语音, x x x是产生语音所需的激励,而 h h h则是我们的声道和嘴唇形成的系统。
浊音是通过迫使空气通过声门而产生的,这种情况下声门会振动,产生准周期性的气流刺激声道。这种激励有基波和多次谐波,类似于乐音。清音则是通过沿着声道的某个点形成一个收缩,迫使空气通过收缩产生紊流而产生的。浊音和清音的激励都是随机的,因此均属于随机信号。
具体的发音则由声道的共振决定,声道的共振由声门能量激发。声道可以被看作一个横截面积均匀的直管,在声门端闭合,在嘴唇处打开。当声道的形状改变时,共振也随之改变。一般存在两个共振峰,它们是发音的特征。
对于浊音来说,声门被建模为双极点系统,空气激励与双极点系统卷积产生浊音的激励;清音不通过声门,紊流直接作为激励。
产生共振峰的声道被建模为10极点系统,而嘴唇被建模为单零点系统。它们与激励卷积产生最终的语音。
声门、声道和嘴唇的动作将会使得系统零点和极点的位置随时间改变,使得发音的特性随时间改变,使得语音能够传递信息。因此,语音信号往往不满足平稳随机过程。
语音也是音频信号,因此能量、功率、自相关函数、频谱、功率谱密度等属性均适用于语音。由于语音信号往往不是平稳的,分帧加窗是语音信号分析的必经之路。
除此之外,语音信号还有两个属性过零率和谱熵,分别定义在时域和频域。过零率是音频强度曲线穿过0的频率,可以反映信号中强度最大的那些成分的频率;而谱熵表示了一段短时信号的信息量,谱熵越大信息量越小。
为了进行语音信号分析,我们首先录制一小段音频。演示所用的音频为"sample_audio/zero_to_nine(Chinese).wav"(请在GitHub项目链接中查看),它是0-9九个阿拉伯数字的中文读音。
record = ak.read_Audio("sample_audio/zero_to_nine(Chinese).wav")
record.plot()
可以看见信号集中在10处位置有显著大于0的振幅,这就是存在语音的位置,其余位置为静音。
其中“三”、“四”、“七”和“九”的开头存在清音,其余部分为浊音。
record_framed = record.framing(window="hamming")
录制的采样率为44100Hz,而人声基频一般分布在100-500Hz之间。绘制语谱图时,我们需要压缩高频的分辨率、提高低频的分辨率。其做法是在绘制时使用梅尔(mel)尺度频率轴坐标。梅尔尺度的理论基础是人耳感知的声音频率和声音的实际频率并不是线性相关的,梅尔尺度为频率区间赋予了和人儿听觉分辨率相当的分辨率。从真实频率 f f f转换为梅尔频率的公式为 f m e l = 2595 log 10 ( 1 + f / 700 ) f_{mel}=2595\log_{10}(1+f/700) fmel=2595log10(1+f/700)。
此外,对于语音幅度谱的绘制,我们还使用增益的形式。
aly.FFT(record_framed).plot(plot_type="dB",freq_scale="mel")
浊音部分的语谱呈现纵向堆叠的横向条纹,且越高频的部分条纹颜色越冷,说明强度越低。这说明了浊音是基波和谐波的叠加,而其基波决定了其音高。
清音部分则是单条纵向条纹,其频率成分分布在整个频率范围内,因此近似于白噪音。
静音部分依然存在噪音,这些噪音频率成分主要集中在低频部分。
统计短时谱熵,其计算公式为 H [ k ] = − ∑ m = k − N + 1 N / 2 p [ k , m ] log 2 p [ k , m ] w [ n − m ] H[k]=\displaystyle-\sum_{m=k-N+1}^{N/2}p[k,m]\log_2p[k,m]w[n-m] H[k]=−m=k−N+1∑N/2p[k,m]log2p[k,m]w[n−m],其中 p [ k , m ] = Y [ k , n ] ∑ l = 0 N 2 Y [ k , l ] p[k,m]=\frac{\displaystyle Y[k,n]}{\displaystyle\sum_{l=0}^{\frac{N}{2}}Y[k,l]} p[k,m]=l=0∑2NY[k,l]Y[k,n],而 Y [ k , n ] = X [ k , n ] X ∗ [ k , n ] Y[k,n]=X[k,n]X^*[k,n] Y[k,n]=X[k,n]X∗[k,n], X [ k , n ] X[k,n] X[k,n]为第 k k k帧的短时频谱。
aly.specEntropy(record_framed).plot(), record.plot()
静音的谱熵中等,清音的谱熵最大,而浊音的谱熵最小。
进行短时过零率的统计。
aly.zerocrossing(record_framed).plot(), record.plot()
静音的过零率中等,清音的过零率最大,而浊音的过零率最小。
再进行短时能量的统计。
aly.energy(record_framed).plot(), record.plot()
静音的能量几乎为0,而浊音和清音具有较高的能量。
鉴于静音部分和语音部分,以及清音部分和浊音部分,在短时能量、短时过零率和谱熵等属性上有统计意义上的区别,我们可以借助这一区别对语音进行端点检测。端点检测的目标是在一段音频信号中检测出每段语音的起始点和终止点,并在每段语音中区分出清音和浊音。
端点检测常用的方法是双门限法:
计算短时能量和短时过零率。
设置一个短时能量阈值和一个短时过零率阈值。
计算短时能量上穿短时能量阈值的点作为语音起始点,下穿短时能量阈值的点作为语音结束点。
从语音起始点开始往左,寻找短时过零率大于短时过零率阈值的最后一个点作为新的语音起始点;从语音结束点开始往右,寻找短时过零率大于短时过零率阈值的第一个点作为新的语音结束点。这样就可以区分语音和静音。
在语音段内再设置一个较高的短时能量阈值。
在每段语音段内,计算短时能量上穿短时能量阈值的点作为浊音起始点,下穿短时能量阈值的点作为浊音结束点。
从浊音起始点开始往左,寻找短时过零率小于短时过零率阈值的最后一个点作为新的浊音起始点;从浊音结束点开始往右,寻找短时过零率小于短时过零率阈值的第一个点作为新的浊音结束点。这样就可以区分浊音和清音。
import seaborn as sns
sns.distplot(aly.zerocrossing(record_framed).samples)
aly.energy(record_framed).plot(ylim=(0,0.5))
通过绘制短时过零率的直方图可以帮助我们选择短时过零率阈值。短时能量阈值则要通过观察短时能量图像后,不断调节来确定。
vad_result = alg.VAD(record, 0.05, 0.5, 400) #对录音进行端点检测,设置较低的短时能量阈值为0.05、较高的短时能量阈值为0.5、短时过零率阈值为400
vad_result.plot()
可以看到我们成功区分出了10段语音,并且区分出了“三”、“四”、“七”和“九”前的清音。
语音识别是语音信号处理的任务之一。最基本的有监督语音识别需要一个训练集,里面包含了很多段语音的特征及其对应文本的标签,用于训练模型。对于新得到的语音,我们希望机器能够识别该语音对应的文本,因此将新得到的语音作为测试集,并提取特征,将其通过模型得到标签,从而知道测试集上语音对应的文本。
为了能够进行语音识别,需要先对语音特征进行提取。语音的语谱就是可用的语音特征之一,若使用每帧内的全部样本点来进行傅里叶变换,则语谱含有语音的所有信息。但语谱是非常巨大的,对于K帧、每帧内有N个样本点的语音信号,若采用 N N N点傅里叶变换,则其语谱尺寸为 K × N 2 K\times \frac{N}{2} K×2N,若分帧的重叠率为0,则其尺寸就等于样本总数的一半。如果我们用于语音识别的算法较为简单,可以提取一些较为轻量而最大程度保持语音有用信息的特征。
梅尔倒谱系数(Mel-scale FrequencyCepstral Coefficients,简称MFCC)是依据人的听觉实验结果来分析语音的频谱,其理论基础包括梅尔尺度和第二临界带。其中梅尔尺度我们已经介绍过。第二临界带则是把进入人耳的声音频率用临界带进行划分,将语音在频域上就被划分成一系列的频率群,组成了滤波器组,即梅尔滤波器组。根据用梅尔尺度定义的分辨率,从低频到高频内按临界带宽的大小由密到疏安排一组带通滤波器,对输入信号进行滤波,将每个带通滤波器输出的信号能量作为信号的基本特征,对此特征经过进一步处理后就可以作为语音的输入特征。
在提取MFCC特征前,首先要对音频进行预加重,其方法是将音频通过一个系统函数为
H
(
z
)
=
1
−
0.97
z
−
1
H(z)=1-0.97z^{-1}
H(z)=1−0.97z−1的高通滤波器以补偿高频部分的损失(这是因为介质作为声能量的载体,在声源尺寸一定的情况下,频率越高,介质对声能量的损耗越严重)。然后对音频进行分帧加窗。再在频谱上加上梅尔滤波器组,其频率响应定义为:
H
m
[
k
]
=
{
0
,
k
<
f
(
m
−
1
)
o
r
k
≥
f
(
m
−
1
)
2
(
k
−
f
(
m
−
1
)
)
(
f
(
m
+
1
)
−
f
(
m
−
1
)
)
(
f
(
m
)
−
f
(
m
−
1
)
)
,
f
(
m
−
1
)
≤
k
≤
f
(
m
)
2
(
f
(
m
+
1
)
−
k
)
(
f
(
m
+
1
)
−
f
(
m
−
1
)
)
(
f
(
m
)
−
f
(
m
−
1
)
)
,
f
(
m
)
≤
k
≤
f
(
m
+
1
)
H_m[k]={0,k<f(m−1) or k≥f(m−1)2(k−f(m−1))(f(m+1)−f(m−1))(f(m)−f(m−1)),f(m−1)≤k≤f(m)2(f(m+1)−k)(f(m+1)−f(m−1))(f(m)−f(m−1)),f(m)≤k≤f(m+1)
通过梅尔滤波器组的滤波后,我们再计算每个滤波器组输出的对数能量: s [ m ] = ln ( ∑ k = 0 N − 1 ∣ X a [ k ] ∣ 2 H m [ k ] ) , 0 ≤ m ≤ M s[m]=\displaystyle\ln(\sum_{k=0}^{N-1}|X_a[k]|^2H_m[k]),0≤m≤M s[m]=ln(k=0∑N−1∣Xa[k]∣2Hm[k]),0≤m≤M,再经过L点离散余弦变换(DCT)得到MFCC系数: C [ n ] = ∑ m = 0 N − 1 s [ m ] cos ( π n ( m − 0.5 ) M ) , n = 1 , 2 , . . . , L C[n]=\displaystyle\sum_{m=0}^{N-1}s[m]\cos(\frac{\pi n(m-0.5)}{M}),n=1,2,...,L C[n]=m=0∑N−1s[m]cos(Mπn(m−0.5)),n=1,2,...,L。离散余弦变换后的低频部分系数对应一帧短时语音的慢变部分,低p阶(p<L)系数对应的是形成发音的系统参数(即声道和嘴唇构成的系统参数),而p阶以上的部分对应的是产生语音的激励参数(即气流通过声门产生的激励参数)。语音识别的关键就在于系统参数的确定,因此我们一般提取低p阶(12-16)系数。这样,我们就将每一帧的特征长度从 N 2 \frac{N}{2} 2N压缩到了p。
接着,继续提取动态差分参数,其计算公式为
d
[
n
]
=
{
C
[
n
+
1
]
−
C
[
n
]
,
t
<
K
o
r
t
≥
p
−
K
∑
k
=
1
K
k
(
C
[
n
+
k
]
−
C
[
n
−
k
]
)
2
∑
k
=
1
K
k
2
,
o
t
h
e
r
s
d[n]={C[n+1]−C[n],t<K or t≥p−KK∑k=1k(C[n+k]−C[n−k])√2K∑k=1k2,others
动态时间规整(DTW)算法是一个用于度量时间序列相似度的算法,且可以用于简单的语音识别任务。
在传统算法中,描述两个序列的相似度可以用余弦相似度或pearson相关系数。但是时间序列比较特殊,两段序列长度可能不同,就无法计算余弦相似度或pearson相关系数。此外,若且一个序列是另一个序列平移之后得到的,两者应该非常相似,但点对点计算的余弦相似度或pearson相关系数无法考虑这一特性。动态时间规整 (DTW) 本质上是通过动态规划来计算两个序列的相似距离。其实这和求解字符串的最长公共子串、子序列这类问题本质比较类似。
基于动态规划构建长度为
M
M
M的序列a和长度为
N
N
N的序列b的距离矩阵
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j],其中
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示序列
a
[
0
:
i
]
a[0:i]
a[0:i]和
b
[
0
:
j
]
b[0:j]
b[0:j]之间的相似距离的平方,则有:
d
p
[
i
]
[
j
]
=
{
(
a
[
0
]
−
b
[
0
]
)
2
,
i
=
0
,
j
=
0
(
a
[
0
]
−
b
[
j
]
)
2
+
d
p
[
0
]
[
j
−
1
]
,
i
=
0
(
a
[
i
]
−
b
[
0
]
)
2
+
d
p
[
i
−
1
]
[
0
]
,
j
=
0
(
a
[
i
]
−
b
[
j
]
)
2
+
min
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
j
−
1
]
[
i
]
,
d
p
[
i
−
1
]
[
j
−
1
]
)
,
i
,
j
>
0
dp[i][j]={(a[0]−b[0])2,i=0,j=0(a[0]−b[j])2+dp[0][j−1],i=0(a[i]−b[0])2+dp[i−1][0],j=0(a[i]−b[j])2+min(dp[i−1][j],dp[j−1][i],dp[i−1][j−1]),i,j>0
对于一段语音,我们将其分帧加窗,并计算得到了每一帧对应的MFCC特征,这样每一段语音的特征其实就是一个时间序列 c [ k ] , k = 0 , 1 , . . . , K c[k],k=0,1,...,K c[k],k=0,1,...,K,其中 K K K为总的帧数,而第 k k k位是一帧的MFCC特征。对于测试集上的任意一段语音提取的MFCC特征对应的时间序列 c t e s t c_{test} ctest,将其与训练集上的每一段语音 c t r a i n c_{train} ctrain使用DTW计算相似距离,则相似距离最小的一段 c t r a i n c_{train} ctrain对应的标签就是 c t e s t c_{test} ctest的标签。其中, ( c t e s t [ i ] − c t r a i n [ j ] ) 2 (c_{test}[i]-c_{train}[j])^2 (ctest[i]−ctrain[j])2计算的是长度为 3 p + 1 3p+1 3p+1的MFCC特征间的欧式距离。
下面,我们将用MFCC+DTW实现一个简单的语音拨号识别程序。由于我们之前已经录制了阿拉伯数字0-9的语音,因此直接将之前的录音作为训练集。作为测试集,我们录制一段11位的电话号码语音,演示所用的音频为"sample_audio/dial(Chinese).wav",录制的电话号码为“15330927125”。
samples = record
dial = ak.read_Audio("sample_audio/dial(Chinese).wav")
dial.plot()
对语音进行端点检测,因此统计其短时过零率和短时能量。
dial_framed = dial.framing(window="hamming")
sns.distplot(aly.zerocrossing(dial_framed).samples)
aly.energy(dial_framed).plot(ylim=(0,0.5))
vad_result_dial = alg.VAD(dial, 0.05, 0.2, 250) #对录音进行端点检测,设置较低的短时能量阈值为0.05、较高的短时能量阈值为0.2、短时过零率阈值为250
vad_result_dial.plot()
VAD类对象的slices方法可以提取出音频中所有的语音段,并返回一个列表。我们首先提取训练集中的语音段。
samples = vad_result.slices()
len(samples)
'''
outputs:
10
'''
可以看到提取出10段语音,这分别对应0-9这十个数字。
接着提取测试集中的语音段。
dials = vad_result_dial.slices()
len(dials)
'''
outputs:
12
'''
测试集中的语音是11位电话号码,但提取出了12段语音。因此我们查看每一段语音的持续时间。
for d in dials: print(d.getDuration()) ''' outputs: 0.35975056689342405 0.3447392290249433 0.4796371882086168 0.5096145124716553 0.43467120181405894 0.38970521541950115 0.28478458049886624 0.49462585034013606 0.3297732426303855 0.329750566893424 0.014988662131519274 0.2548072562358277 '''
我们发现有一段语音明显短于其他语音,因此将该段语音排除,这样就得到了测试集上的11个数字对应的语音。
dials = [dial for dial in dials if dial.getDuration() > 0.1]
len(dials)
'''
outputs:
11
'''
接下来,我们利用MFCC+DTW进行语音识别。MFCC默认阶数是13阶,我们先尝试不添加差分和能量特征。
from pyAudioKits.algorithm import dtw mfcc_samples = [aly.MFCC(sample,diff1=False,diff2=False,energy=False) for sample in samples] #使用MFCC提取训练集上每一段语音的特征 mfcc_dials = [aly.MFCC(dial,diff1=False,diff2=False,energy=False) for dial in dials] #使用MFCC提取测试集上每一段语音的特征 for dial in mfcc_dials: #在测试集上迭代 distances = [dtw(dial, sample) for sample in mfcc_samples] #将每一段语音的特征和训练集上每一段语音特征通过DTW计算距离 print(np.argmin(distances)) #DTW距离最小的测试集上语音对应的标签,就是测试集上该段语音的标签 ''' outputs: 1 5 3 3 0 9 8 7 1 2 5 '''
可以看到此时除了第7位的“2”被识别成“8”外,其余结果都是正确的。
接下来尝试添加差分和能量特征。
mfcc_samples = [aly.MFCC(sample,diff1=True,diff2=True,energy=True) for sample in samples] mfcc_dials = [aly.MFCC(dial,diff1=True,diff2=True,energy=True) for dial in dials] for dial in mfcc_dials: distances = [dtw(dial, sample) for sample in mfcc_samples] print(np.argmin(distances)) ''' outputs: 1 5 3 3 0 9 8 0 1 2 5 '''
识别准确率反而下降了,这说明添加更多特征不一定能增加识别准确率。
由于我们刚才只录制了一段训练集音频,0-9这十个数都只有一段样本作为训练集。下面我们读取更多样本,并执行相同的端点检测和MFCC特征提取操作。
samples2 = ak.read_Audio("sample_audio/zero_to_nine2(Chinese).wav")
samples3 = ak.read_Audio("sample_audio/zero_to_nine3(Chinese).wav")
vad_result_samples2 = alg.VAD(samples2, 0.05, 0.2, 300)
vad_result_samples3 = alg.VAD(samples3, 0.05, 0.2, 300)
samples2 = vad_result_samples2.slices()
samples3 = vad_result_samples3.slices()
mfcc_samples2 = [aly.MFCC(sample,diff1=True,diff2=True,energy=True) for sample in samples2]
mfcc_samples3 = [aly.MFCC(sample,diff1=True,diff2=True,energy=True) for sample in samples3]
这样,训练集扩充到了原来的3倍。让测试集上每段语音和三个训练集上每段语音均计算距离,并取距离最小的训练集语音对应的标签。这样测试集上每段语音将会得到3个标签。
result = [[] for i in range(len(mfcc_dials))] for i,dial in enumerate(mfcc_dials): distances = [dtw(dial, sample) for sample in mfcc_samples] result[i].append(np.argmin(distances)) distances = [dtw(dial, sample) for sample in mfcc_samples2] result[i].append(np.argmin(distances)) distances = [dtw(dial, sample) for sample in mfcc_samples3] result[i].append(np.argmin(distances)) result ''' ourputs: [[1, 1, 1], [5, 5, 5], [3, 3, 3], [3, 3, 3], [0, 0, 0], [9, 9, 9], [8, 2, 8], [0, 7, 7], [1, 1, 1], [2, 2, 2], [5, 5, 5]] '''
可以看见,有超过一半的训练集赋予了第八位“7”的标签,因此我们可以判断第八位是“7”。第七位的“2”依然难以分辨,但已经有一个训练集赋予其“2”的标签了,如果继续扩大训练集规模,则准确率还会继续提升。但相对地,使用DTW算法进行匹配的速度就会下降。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。