CTC是一种端到端的语音识别技术,他避免了需要字或者音素级别的标注,只需要句子级别的标注就可以进行训练,感觉非常巧妙,也很符合神经网络浪潮人们的习惯。特别是LSTM+CTC相较于之前的DNN+HMM,LSTM能够更好的捕捉输入中的重要的点(LSTM随着状态数目增加参数呈线性增加,而HMM会平方增加),CTC打破了隐马尔科夫的假设,把整个模型从静态分类变成了序列分类。
语音识别的评价指标
在语音识别中,在数据集
S
S
S 上评价模型
h
h
h 的好坏一般用标签错误率(Label Error Rate):
L
E
R
(
h
,
S
)
=
1
∣
S
∣
∑
(
x
,
z
)
∈
S
E
D
(
h
(
x
)
,
z
)
∣
z
∣
LER(h,S)=\frac{1}{|S|}\sum_{(x,z)\in S}\frac{ED(h(x),z)}{|z|}
L E R ( h , S ) = ∣ S ∣ 1 ∑ ( x , z ) ∈ S ∣ z ∣ E D ( h ( x ) , z ) ,
E
D
(
p
,
q
)
ED(p,q)
E D ( p , q ) 表示
p
p
p 和
q
q
q 两个序列的编辑距离。
语音识别模型
在语音识别中,提取语音信号的MFCC特征
x
x
x ,经过神经网络或者GMM处理后经过一个softmax层得到一个每个音素的后验概率
y
y
y ,
y
y
y 的类别有
∣
L
∣
+
1
|L|+1
∣ L ∣ + 1 种,
L
L
L 是可能出现的字符,加1为建个符。定义
B
B
B 为简单的压缩变换,把路径
π
\pi
π (路径就是一种音素出现的路线)中相邻相同的音素合并,空音素去掉,再特征
x
x
x 下定序列
l
l
l 出现的条件概率为:
p
(
l
∣
x
)
=
∑
π
=
∈
B
−
1
(
l
)
p
(
π
∣
x
)
p(l|x)=\sum_{\pi=\in B^{-1}(l)}p(\pi|x)
p ( l ∣ x ) = π = ∈ B − 1 ( l ) ∑ p ( π ∣ x )
前向后向算法(Forward-Backward Algorithm)
定义符号
l
p
:
q
l_{p:q}
l p : q 表示符号序列KaTeX parse error: Expected '}', got 'EOF' at end of input: …..l_{q-1},l_{q} ,容易得知,要想使得路径
B
(
π
′
)
B(\pi')
B ( π ′ ) 满足一定的
l
l
l ,
π
\pi
π 路线上的状态跳转需要满组
l
′
l'
l ′ 的先后顺序,不同的符号之间可以插入blank。
定义前向变量
α
(
t
,
u
)
\alpha(t,u)
α ( t , u ) :
α
t
(
t
,
u
)
=
∑
π
∈
N
T
,
B
(
π
1
:
t
)
=
1
1
:
u
∏
t
′
=
1
t
y
π
t
′
t
′
\alpha_t(t,u)=\sum_{\pi\in N^T,B(\pi_{1:t})=1_{1:u}}\prod_{t'=1}^ty_{\pi_{t'}}^{t'}
α t ( t , u ) = ∑ π ∈ N T , B ( π 1 : t ) = 1 1 : u ∏ t ′ = 1 t y π t ′ t ′
α
(
t
,
s
)
\alpha(t,s)
α ( t , s ) 可以递推的用
α
(
t
−
1
,
s
)
,
α
(
t
−
1
,
s
−
1
)
\alpha(t-1,s),\alpha(t-1,s-1)
α ( t − 1 , s ) , α ( t − 1 , s − 1 ) 计算。
为了方便起见,我们在
l
l
l 相邻标签之间插入了空白(blank),在开始和末尾也加入了空白,这样我们用
l
′
l'
l ′ 表示这个新的标记,
l
′
l'
l ′ 的长度就为
2
∣
l
∣
+
1
2|l|+1
2 ∣ l ∣ + 1 。在计算
l
′
l'
l ′ 前缀的概率中,我们允许空白和非空白标签之间转移,那么我么有动态规划的初始条件:
α
(
1
,
1
)
=
y
b
1
\alpha(1,1)=y_b^1
α ( 1 , 1 ) = y b 1
α
(
1
,
2
)
=
y
l
1
1
\alpha(1,2)=y_{l_1}^1
α ( 1 , 2 ) = y l 1 1
α
(
1
,
u
)
=
0
,
u
>
2
\alpha(1,u)=0,u>2
α ( 1 , u ) = 0 , u > 2
α
(
t
,
0
)
=
0
\alpha(t,0)=0
α ( t , 0 ) = 0
α
(
t
,
u
)
=
0
,
u
<
U
′
−
2
(
T
−
t
)
−
1
\alpha(t,u)=0,u<U'-2(T-t)-1
α ( t , u ) = 0 , u < U ′ − 2 ( T − t ) − 1
U
′
U'
U ′ 表示序列
2
∣
l
1
:
u
∣
+
1
2|l_{1:u}|+1
2 ∣ l 1 : u ∣ + 1 。
递归算式:
KaTeX parse error: Double subscript at position 19: …pha(t,u)=y_{t'}_̲{l'_u}^t\sum_{i…
KaTeX parse error: Expected group after '\right' at end of input: …nd{array}\right
简单地说就是现在如果状态是blank,那么上一个状态有可能是blank或者是上一个字符,如果现在的状态是字符,那他上一个状态可能是相同的字符,可能是blank,也可能是上一个非blank字符,但如果现在的字符与上一个非空字符相同,那意味着现在的状态不能直接从上一个非空字符跳过来,必须隔一个blank,所以只能从blank和相同的字符跳过来。
相似的定义后向变量
β
(
t
,
u
)
\beta(t,u)
β ( t , u ) :
α
t
(
t
,
s
)
=
∑
π
∈
N
T
,
B
(
π
t
:
T
)
=
1
u
:
U
∏
t
′
=
t
+
1
T
y
π
t
′
t
′
\alpha_t(t,s)=\sum_{\pi\in N^T,B(\pi_{t:T})=1_{u:U}}\prod_{t'=t+1}^{T}y_{\pi_{t'}}^{t'}
α t ( t , s ) = ∑ π ∈ N T , B ( π t : T ) = 1 u : U ∏ t ′ = t + 1 T y π t ′ t ′
依然用动态规划来求解,动态规划的初始条件:
β
(
T
,
U
′
)
=
1
\beta(T,U')=1
β ( T , U ′ ) = 1
β
(
T
,
U
′
−
1
)
=
1
\beta(T,U'-1)=1
β ( T , U ′ − 1 ) = 1
β
(
T
,
u
)
=
0
,
u
<
U
′
−
1
\beta(T,u)=0,u<U'-1
β ( T , u ) = 0 , u < U ′ − 1
β
(
t
,
u
)
=
0
,
u
>
2
t
\beta(t, u)=0,u>2t
β ( t , u ) = 0 , u > 2 t
递归算式:
KaTeX parse error: Double subscript at position 49: …(t+1,i)y_{t'+1}_̲{l'_u}^t
KaTeX parse error: Expected group after '\right' at end of input: …nd{array}\right
在实际中,上述递归很快就会导致计算机出现下溢。避免下溢的一种好方法是先取对数,在计算结束时仅取幂以找到真实概率。在这种情况下,一个有用的等式是
l
n
(
a
+
b
)
=
l
n
a
+
l
n
(
1
+
e
l
n
b
−
l
n
a
)
ln(a+b)=ln~a+ln(1+e^{ln~b-ln~a})
l n ( a + b ) = l n a + l n ( 1 + e l n b − l n a )
这样就允许前向和后向变量相加,同时保留取对数。但是这种方法的稳定性较差,并且对于很长的序列可能会失效。
Loss Function
the CTC loss函数
L
(
S
)
L(S)
L ( S ) 被定义为训练集
S
S
S 种正确标签的negative log probability:
L
(
S
)
=
−
l
n
∏
(
x
,
z
)
∈
S
=
−
∑
(
x
,
z
)
∈
S
l
n
p
(
z
∣
x
)
L(S)=-ln\prod_{(x,z)\in S}=-\sum_{(x,z)\in S}ln~p(z|x)
L ( S ) = − l n ( x , z ) ∈ S ∏ = − ( x , z ) ∈ S ∑ l n p ( z ∣ x )
这个损失函数是可微的,方便加入到神经网络中进行反向传播。我们定义example loss为
L
(
x
,
z
)
=
−
l
n
p
(
z
∣
x
)
L(x,z)=-ln~p(z|x)
L ( x , z ) = − l n p ( z ∣ x )
那么:
L
(
S
)
=
∑
(
x
,
z
)
∈
S
L
(
x
,
z
)
L(S)=\sum_{(x,z)\in S}L(x,z)
L ( S ) = ( x , z ) ∈ S ∑ L ( x , z )
∂
L
(
S
)
∂
w
=
∑
(
x
,
z
)
∈
S
∂
L
(
x
,
z
)
∂
w
\frac{\partial L(S)}{\partial w}=\sum_{(x,z)\in S}\frac{\partial L(x,z)}{\partial w}
∂ w ∂ L ( S ) = ( x , z ) ∈ S ∑ ∂ w ∂ L ( x , z ) 。
上一小节中,我们得到了前向后向变量,我们令
l
=
z
l=z
l = z ,易知:
α
(
t
,
u
)
β
(
t
,
u
)
=
∑
π
t
=
z
u
,
B
(
π
)
=
z
∏
t
=
1
T
y
π
t
t
=
∑
π
t
=
z
u
,
B
(
π
)
=
z
p
(
π
∣
x
)
\alpha(t,u)\beta(t,u)=\sum_{\pi_t=z_u,B(\pi)=z}\prod_{t=1}^Ty_{\pi_t}^t=\sum_{\pi_t=z_u,B(\pi)=z}p(\pi|x)
α ( t , u ) β ( t , u ) = π t = z u , B ( π ) = z ∑ t = 1 ∏ T y π t t = π t = z u , B ( π ) = z ∑ p ( π ∣ x )
那么对于任意时刻
t
∈
[
1
,
T
]
t\in [1,T]
t ∈ [ 1 , T ] ,我们有
p
(
z
∣
x
)
=
∑
u
=
1
∣
z
′
∣
α
(
t
,
u
)
β
(
t
,
u
)
p(z|x)=\sum_{u=1}^{|z'|}\alpha(t,u)\beta(t,u)
p ( z ∣ x ) = u = 1 ∑ ∣ z ′ ∣ α ( t , u ) β ( t , u )
也就是说
L
(
x
,
z
)
=
−
l
n
∑
u
=
1
∣
z
′
∣
α
(
t
,
u
)
β
(
t
,
u
)
L(x,z)=-ln~\sum_{u=1}^{|z'|}\alpha(t,u)\beta(t,u)
L ( x , z ) = − l n u = 1 ∑ ∣ z ′ ∣ α ( t , u ) β ( t , u )
Loss Gradient
从前面的推导可得:
∂
L
(
x
,
z
)
∂
y
k
t
=
−
∂
l
n
p
(
z
∣
x
)
∂
y
k
t
=
−
1
p
(
z
∣
x
)
∂
p
(
z
∣
x
)
∂
y
k
t
\frac{\partial L(x,z)}{\partial y_k^t}=-\frac{\partial ln~p(z|x)}{\partial y_k^t}=-\frac{1}{p(z|x)}\frac{\partial p(z|x)}{\partial y_k^t}
∂ y k t ∂ L ( x , z ) = − ∂ y k t ∂ l n p ( z ∣ x ) = − p ( z ∣ x ) 1 ∂ y k t ∂ p ( z ∣ x )
因为网络输出不会相互影响,求解
∂
p
(
z
∣
x
)
∂
y
k
t
\frac{\partial p(z|x)}{\partial y_k^t}
∂ y k t ∂ p ( z ∣ x ) 我们只需要考虑那些在时间
t
t
t 经过标签
k
k
k 的路径。因为
k
k
k 有可能没有出现在
z
z
z 中,对于这些
k
k
k ,我们令
∂
p
(
z
∣
x
)
∂
y
k
t
=
0
\frac{\partial p(z|x)}{\partial y_k^t}=0
∂ y k t ∂ p ( z ∣ x ) = 0 。对于
k
k
k 也可能多次出现在
z
z
z 中,我们定义
B
(
z
,
k
)
=
u
∣
z
u
′
=
k
B(z,k)={u|z'_u=k}
B ( z , k ) = u ∣ z u ′ = k ,易得:
∂
p
(
z
∣
x
)
∂
y
k
t
=
1
y
k
t
∑
u
∈
B
(
z
,
k
)
α
(
t
,
u
)
β
(
t
,
u
)
\frac{\partial p(z|x)}{\partial y_k^t}=\frac{1}{y_k^t}\sum_{u\in B(z,k)}\alpha(t,u)\beta(t,u)
∂ y k t ∂ p ( z ∣ x ) = y k t 1 u ∈ B ( z , k ) ∑ α ( t , u ) β ( t , u )
那么:
∂
L
(
x
,
z
)
∂
y
k
t
=
−
1
p
(
z
∣
x
)
⋅
y
k
t
∑
u
∈
B
(
z
,
k
)
α
(
t
,
u
)
β
(
t
,
u
)
\frac{\partial L(x,z)}{\partial y_k^t}=-\frac{1}{p(z|x)\cdot y_k^t}\sum_{u\in B(z,k)}\alpha(t,u)\beta(t,u)
∂ y k t ∂ L ( x , z ) = − p ( z ∣ x ) ⋅ y k t 1 u ∈ B ( z , k ) ∑ α ( t , u ) β ( t , u )
那么对于softmax之前模型的输出
a
k
t
a_k^t
a k t 的反向传播梯度为:
∂
L
(
x
,
z
)
∂
a
k
t
=
−
∑
k
′
∂
L
(
x
,
z
)
∂
y
k
′
t
∂
y
k
′
t
∂
a
k
t
\frac{\partial L(x,z)}{\partial a_k^t}=-\sum_{k'}\frac{\partial L(x,z)}{\partial y_{k'}^t}\frac{\partial y_{k'}^t}{\partial a_k^t}
∂ a k t ∂ L ( x , z ) = − k ′ ∑ ∂ y k ′ t ∂ L ( x , z ) ∂ a k t ∂ y k ′ t
我们知道:
y
k
t
=
e
a
k
t
∑
k
′
e
a
k
′
t
y_k^t=\frac{e^{a_k^t}}{\sum_{k'}e^{a_{k'}^t}}
y k t = ∑ k ′ e a k ′ t e a k t
∂
y
k
′
t
∂
a
k
t
=
y
k
′
t
δ
k
k
′
−
y
k
′
t
y
k
t
\frac{\partial y_{k'}^t}{\partial a_k^t}=y_{k'}^t\delta_{kk'}-y_{k'}^ty_{k}^t
∂ a k t ∂ y k ′ t = y k ′ t δ k k ′ − y k ′ t y k t
综上可得:
∂
L
(
x
,
z
)
∂
a
k
t
=
y
k
t
−
1
p
(
z
∣
x
)
∑
u
∈
B
(
z
,
k
)
α
(
t
,
u
)
β
(
t
,
u
)
\frac{\partial L(x,z)}{\partial a_k^t}=y_k^t-\frac{1}{p(z|x)}\sum_{u\in B(z,k)}\alpha(t,u)\beta(t,u)
∂ a k t ∂ L ( x , z ) = y k t − p ( z ∣ x ) 1 u ∈ B ( z , k ) ∑ α ( t , u ) β ( t , u )
可以理解为现在的输出
y
k
t
y_k^t
y k t 与根据前向后向概率求出的现在应该出现的概率之差。
实验中我们可以得知输出总是spike的形式出现,大部分被blank间隔。
decoder
分类器的输出应该是在给定输入序列下的最可能的标记输出
h
(
x
)
=
a
r
g
m
a
x
l
p
(
l
∣
x
)
h(x)=argmax_{l}p(l|x)
h ( x ) = a r g m a x l p ( l ∣ x )
这是一个NP问题,下面是两个近似的算法,在实践中给出了良好的结果。
最佳路径解码(best path decoding)
基于的假设是最可能的路径会对应最可能表标签
h
(
x
)
=
B
(
π
∗
)
,
s
.
t
.
,
π
∗
=
a
r
g
m
a
x
π
p
(
π
∣
x
)
h(x)=B(\pi^*),s.t.,\pi^*=argmax_{\pi}p(\pi|x)
h ( x ) = B ( π ∗ ) , s . t . , π ∗ = a r g m a x π p ( π ∣ x )
最佳路径解码非常容易计算(计算方法如下图),但是它不能保证找到最有可能的
l
l
l (因为不同的
π
\pi
π 有可能对应相同的
l
l
l ,加起来可能与最佳路径结果不同)。
前缀查找解码(prefix search decoding)
通过修改前向后向算法(forward-backward algorithm),我们可以高效的计算出标记前缀的后继拓展的概率。前缀查找解码(prefix search decoding)是一种剪枝的算法,剪枝主要在两个方面,一是同路径不重复计算,二是不可能状态不再搜索。算法流程如下:
翻译一下:
l
∗
l^*
l ∗ 表示当前找到最好的的序列,
p
∗
p^*
p ∗ 表示以
p
∗
p^*
p ∗ 为开头的序列可能性最高,当
p
∗
p^*
p ∗ 为开头的序列可能性小于表示当前找到最好的的序列
l
∗
l^*
l ∗ 出现的可能性时停止循环,认为
l
∗
l^*
l ∗ 就是最有可能的序列。循环过程中,对于最有可能的前缀尝试后面加入每一个可能的字符,然后计算加入这个字符之后新的字符串出现的概率和以新的字符串为前缀的概率,当新的字符串出现的概率大于
l
∗
l^*
l ∗ 时更新
l
∗
l^*
l ∗ ,把以新的字符串为前缀的概率大于
l
∗
l^*
l ∗ 出现的概率的前缀加入带备选集合,循如果发现剩下字符搭配
p
∗
p^*
p ∗ 做前缀的概率之和小于
l
∗
l^*
l ∗ 出现的概率,停止循环,节约计算。循环完毕后把旧的
p
∗
p^*
p ∗ 从备选集合中剔除,再把集合中概率最大的前缀作为
p
∗
p^*
p ∗ 。
γ
(
p
n
,
t
)
\gamma(p_n,t)
γ ( p n , t ) 表示序列
p
p
p 第
t
t
t 个输出非blank,
γ
(
p
b
,
t
)
\gamma(p_b,t)
γ ( p b , t ) 表示序列
p
p
p 第
t
t
t 个输出为blank。
实验结果
tensorflow中的CTC
tf.nn.ctc_loss(labels, inputs, sequence_length)
labels: 标签,一个稀疏矩阵 inputs:模型输出的logits
decoded, _ = tf.nn.ctc_beam_search_decoder(logits, sequence_len, merge_repeated=False)
decoded, _ = tf.nn.ctc_greedy_decoder(logit, sequence_len, merge_repeated=False)
logits:模型输出的logits sequence_len:实际的长度(tensorflow静态图模型,告诉他实际的音频长度多长) merge_repeated:把连续输出相同的合并,不同于ctc_merge_repeated,ctc_merge_repeated是把中间没有blank的合并。
音识别模型小型综述
上古时期的方法:元音部分频率共振峰匹配来识别(1952年贝尔实验室的Davis等人发明的特定说话人孤立数字识别系统)
深度神经网络之前的方法:mfcc/plp/fbank->GMM+HMM->DTW(dynamic time warping)
深度学习下的方法(参考了百度语音识别技术负责人的文章):
2011-2012年:发现把GMM换成DNN有30%的提升
2013年:发现调整激活函数Maxout对数据量较少的情况有所改善,对大数据量不一定有显著帮助
2013-2014年:浅层DNN升级为浅层CNN
2014年:开始尝试开始做RNN,尤其是LSTM
2014年底2015年初:Hinton的博士后Alex Graves,把以前做手写体识别的LSTM 加CTC的系统应用在了语音识别领域,在TIMIT上做了结果,紧接着谷歌认为这个很有前途,就开始把这些技术推广在大数据上面。
2015年:研究者发现CNN在时间上做卷积和LSTM有很多相似之处但各有特点。LSTM更擅长做整个时间域的信息整合,因为CNN有很强的平移的不变性,那么鲁棒性更强
2016年:Deep CNN,WaveNet等深层CNN
工程上的一般方法是先用模型first-pass decoding,再用语言模型second-pass rescoring来提高准确率。
实现的乞丐版语音识别模型
dataset: VCTK[https://datashare.is.ed.ac.uk/download/DS_10283_2651.zip ] model: 三层lstm模型 loss function: ctc loss feature: 13阶mfcc系数+2阶动态因子(39)
git: https://github.com/RDShi/Speech2Text
速度很慢,希望有缘人指点提升数度的方法
参考文献
Graves A, Fernandez S, Gomez F, et al. Connectionist temporal classification: labelling unsegmented sequence data with recurrent neural networks[C] Proceedings of the 23rd international conference on Machine learning. ACM, 2006: 369-376.