发布时间:2024-06-16 18:01
目录
1.语音识别的基本架构
2. RNN循环神经网络
3.损失函数
4. CTC Loss函数计算
5. CTC训练算法总结
6. CTC解码
6.1 CTC Prefix Search Decoding
6.2 CTC Beam Search Decoding
7 总结
假设输入的音频序列为,输出文本序列为。我们的目的是训练一个模型,最大化,也就是训练集中所有样本的后验概率最大。为什么是后验呢,因为人在说话的时候是想好了文本,然后产生的。所以是因,是果。单独拿出一个样本的后验概率使用贝叶斯可以得到:
在进行解码的时候输入O是保持不变的,目的是找到一个W使得后验概率最大,所以我们忽略分母项。再看分子,是输出词序列的概率,用语言模型来刻画。为似然概率,用声学模型来刻画。所以语音识别问题就是:
对于声学模型,我们在建模的时候往往是以音素、音节、字、词等为建模单元的,比如GMM-HMM里常常会用一个三状态hmm对音素建模,分别表示音素的起始、中间、结尾。所以我们需要进一步拆分成两部分:
其中,O表示音频特征,一般基于MFCC、FBank等方法提取特征。Q是O对应的发音字符序列,也就是刚刚所说的建模单元。这个时候,才是真正意义的声学模型,表示在词序列条件下的音素序列的概率,这就是发音词典。
在介绍CTC之前先复习一下RNN。
RNN循环神经网络,是神经网络的一种。本质是对线性层的复用。RNN主要用于处理具有序列性质的输入数据。
RNN的好处是基于上一个隐藏层输出,能把更前面的序列也建模进去,考虑了更多的上下文信息。用代码来表示这个过程就是:
h = 0
for x in X:
h = Linear(x, h)
考虑一段“我爱你中国”的输入音频序列,输出序列是音节序列“wo3 ai4 ni3 zhong1 guo2”,如果训练样本中已经“分割”好音频,并标注好它和音节的对应关系,则RNN模型如下:
对于常规的RNN模型,我们需要对每一个输出都要有label,这样才能计算loss和梯度以实现训练。
但在ASR的场景中我们很难做到这一点。比如我们拿到了一段2s的音频,按照25ms的分帧,帧间间隔为10ms的时长来算,输入有近200个音频帧,但是说话人在这2s内只说了hello这个单词,我们很难对于给每一帧去打标签来确认这一帧是hello中的h、l、l、e、o中的哪一个字母。
于是我们就有一个需求:能不能在不需要逐帧的标签的情况下,也能通过上述的模型输出和音频label来计算一个loss,并且这个loss求解函数可导使得我们的模型可以实现端到端训练?这就是我们今天要介绍的CTC,全称是Connectionist Temporal Classification。
实际情况是这样做的:对音频按照时间窗口来提取特征,比如按照每10豪ms音频提取特征得到一个N维数组,如下图:
由于人说话发音是连续的,且中间也会有“停顿”,所以输出序列中存在重复的元素,比如“wo3 wo3”,也存在表示间隔符号“_”。需从输出序列中去除掉重复的元素以及间隔符,才可得到最终的音节序列,比如,“wo3 wo3 ai4 _ ni3 _ zhong1 guo2 _” 归一处理后得到“wo3 ai4 ni3 zhong1 guo2”。因此,输出序列和最终的label之间存在多对一的映射关系,如下图:
这正是CTC的不同之处,该方法直接自动对齐输出标签与输入序列,不再像DNN-HMM那样需要提前作viteribi对齐标注。CTC在输入序列和输出序列之间直接建立多对一的映射关系,寻求最佳匹配。
输入:
: H h e e l - l o o - - w o o o r l l d d
输出: H e l l o w o r l d
如上面所示,输出序列("Hello World")的字符个数和输入序列的长度(这里是帧数20)并不相等,无法将他们直接匹配起来,但可以看到,我们通过中间的重复字符和空白字符(“-”),可以建立与输入序列一一对应的关系。在识别之后,需要去除空白字符和连续重复字符,如“o-”变为“o”,“ee”变成“e”, 最后得到精简后的输出序列。
在此基础上,定义了CTC的损失函数,并在训练过程中自动对齐并使损失函数最小化。
定义为建模单元集,建模单元可以是字符,如英文字母{},也可以是音素,如汉语普通话的声韵母。为了对静音、字母停顿、字间混淆进行建模,CTC引入额外的“空白”标签(表示为'-'),把扩展为,在识别最后需要剔除“空白”标签, 如和均表示。
假设是训练数据集,每个样本由输入序列和输出序列组成,其中,是输入序列的长度,是输出序列的长度,的每个标注都来自于建模单元集。CTC的训练目标就是,训练一个模型,使和尽量匹配,即最大化输出概率。
我们刚刚也说到,输入一段语音,对应输出标签,比如abc。而产生abc的可能路径有很多条:
a |
a |
b |
b |
b |
c |
a |
b |
b |
c |
c |
c |
a |
- |
b |
- |
- |
c |
- |
a |
- |
b |
- |
c |
- |
a |
b |
b |
- |
c |
以上路径均可以产生输出。所以为了计算,我们需要穷举这样所有可能的路径。我们把下对应所有路径放到一个集合里,于是:
其中,表示和在CTC网络下的某条对齐路径,其长度和输入序列一致,即,去除的重复字符和空白标签后得到。
而每一条路径出现的概率可以这样表示:
其中,表示路径在时刻的输出标签(中的一个),是其对应的输出概率。
我们先来看一个时刻t下从原始输入到最后输出的计算过程:
假设扩展建模单元集L’的个数为K,则CTC输出层对应K个节点。如图所示,输入经过一个RNN,隐层输出经过可以得到在时刻对应每一个字符的概率。我们用表示这个概率。对于在t时刻的输出,根据其在建模单元集的索引选择K个节点中的一个,假如选第3个,则其值为, 即.这个过程如下图所示:
这样,对于每一个时刻,我们都能算得其对应每个字符的输出概率(时刻生成的概率)。将每个时刻得到的输出概率一一相乘,就可以得到。整个过程如下图所示:
总结:
输入序列,分别通过得到隐层输出。通过softmax转换得到每个时刻的输出概率, 再将这些概率连乘得到。于是就知道怎么计算损失函数了。定义损失函数,为训练集S所有样本的负对数概率之和:
举个例子:
下面这个10维的送入。建模字符集是,对于每个时刻,经过再通过转换得到对应每个字符的概率。
比如下面这样(数值我随便取的):
第1时刻,生成的概率是 ,
第2时刻,生成的概率是
第3时刻,生成的概率是
第4时刻,生成的概率是
第5时刻,生成的概率是
第6时刻,生成的概率是
第7时刻,生成的概率是
第8时刻,生成的概率是
第9时刻,生成的概率是
第10时刻,生成的概率是
现在,如果我们的输出标签是hello,那么我们就得找到所有会产生hello的可能路径,算出这些所有可能路径的概率后一一相加,就可以计算损失函数。其中一条可能路径是he-ll-lloo,那么这条路径的生成概率就是0.3*0.1*0.2*0.1*0.3*0.2*0.3*0.1*0.3*0.1。
训练优化的目标就是使最小化,但计算的复杂度太高了,像上面那样,我们得把所有可能路径都穷举出来,然后把生成概率相加才行。为了简化这个过程, 借鉴HMM的forward-backward算法思路,利用动态规划算法来求解。
如下图,为了更形象表示问题的搜索空间,用X轴表示时间序列, Y轴表示输出序列,并把输出序列做标准化处理,输出序列中间和头尾都加上blank。原来的字符集经过补充后,变为。用表示最终标签,表示扩展后的形式,则有2|| + 1 = 2||,比如:=apple => =_a_p_p_l_e_
图中并不是所有的路径都是合法路径,所有的合法路径需要遵循一些约束,如下图:
所以,依据以上约束规则,遍历所有映射为“apple”的合法路径,最终时序T=8,标签为“apple”的全部路径如下图:
那么如何来计算这些路径的概率总和呢?可以将路径集合分为前向和后向两个部分,如下图所示:
定义在时刻经过节点s的全部前缀子路径的概率总和为:
这个前向概率即下面这个图表示的意思。
前向算法按输入序列的时间顺序,从前往后递推获得最终输出概率。
第一步,初始化:
表示第一时刻从字符序列(Y轴)的第一个字符:空字符 '-' 出发。
表示第一时刻从字符序列(Y轴)的第二个字符出发
表示第一时刻从Y轴第三个字符及以后出发的概率为0
其中,s表示y轴第几个符号的索引。表示t时刻第s个字符的概率。
也就是说,一开始只能从空字符或第一个有效字符开始转移。
第二步:递推
这要分三种情况:
1. t时刻转移第s个符号为空符号,我们想一下,一共有这俩种可能:从t-1时刻的同一个空符号转移过来,或是从t-1时刻的上一个有效字符转移过来。而不会从其他地方过来。如下面这个图画的那样,黄色位置只能从两个蓝色位置转移过来。
2. t时刻转移到第s个符号,且第s个字符和s-2个字符是相同的。这个时候,也只有俩种可能:从上一时刻的同一字符转移过来,或是从上一时刻的前一个空字符转移过来。注意,他不会从上一时刻第s-2个字符转移过来,这是为了避免与解码重复字符混淆。
我们 来看这样一条有效路径:。经过简化,可以得到apple。
第t=4时刻,是处在字符处的,t=5时刻,是转移到了-空字符。
如果在第t=5时刻,我们还转移到字符处,如下图黑色线所示,这时对应序列为, 简化后得到aple。所以说,为避免与解码重复字符混淆(最终会合并成一个,即),其实际路径表示为, 即中间加空白字符,则不能跳转。
3.既不属于情况1,也不属于情况2:
这是一般情况:从t-1刻的同一字符、空字符、上一有效字符均可能转移过来。
总结前向算法计算概率:
这样,一直往前转移,直到最后一个时刻。在最后一刻,可能会在最后一个有效字符结束,也可能会在最后一个空字符结束(因为只有这俩种情况才能把所有有效字符遍历,才会映射到标签上)。
通过动态规划求解出前向概率之后,可以用前向概率来计算CTC Loss函数,如下图所示:
类似地,我们可以定义反向概率,并用反向概率来计算CTC Loss函数,如下图:
前向后向算法总结:
损失函数计算及求导过程:
1. 通过前向和后向算法分别计算和
3.通过反向传播每层参数进行逐层优化
4.重复步骤2、3,直到CTC损失代价收敛,即可完成端到端训练。
ctc解码是基于已经训练好的ctc模型,对输入序列进行解码,得到识别结果。给定输入序列,CTC的解码目标是找到概率最大的输出序列,即:
一种启发式方法是在每个时刻t计算概率最高的输出单元,然后删除重复和空白字符得到输出序列。但是这样得到的最高概率只是来自单挑路径的最高概率,并不代表就是最后的输出序列。这是因为,该输出序列可能由多条路径组成。虽然这每一条路径都比最大概率的路径小,但他们加起来就比最大概率大。
如下图所示,解码单元为,输入序列的长度为3,栅格中的输出为t时刻对应解码单元的输出概率。
如果按启发式方法,选出每个时刻输出概率最大的解码单元。分别如下:
因此最后的解码输出为,其概率为0.5 * 0.4 = 0.2。但这个输出并不是最优结果。
实际上所有可能的解码序列及其对应概率包括:
可以看到,输出概率最大的其实是,即最优的解码序列是。
所以,CTC的解码过程并不简单,每一步都需要穷举所有路径。假设解码单元有K个,输入序列长为T,则穷举搜索的时间复杂度为(每个时刻都有K种选择)。为加快解码速度,一种方法是在每个时刻,只基于概率最大的一个前缀扩张,但这种方法只能找到次优解。
CTC Prefix Search Decoding本质是贪心算法,每一次搜索都会选取“前缀概率”最大的节点扩展,直到找到最大概率的目标label,它的核心是利用动态规划算法计算“前缀概率”。下面先通过一个简单的例子来介绍CTC Prefix Search Decoding的大致过程,如下图。
如上图例子,CTC Prefix Search搜索过程的伪代码:
Beam Search的过程非常简单,每一步搜索选取概率最大的W个节点进行扩展,W也称为Beam Width,其核心还是计算每一步扩展节点的概率。我们先从一个简单的例子来看下搜索的穷举过程,T=3,字符集为{a, b},其时间栅格表如下图
如果是穷举搜索,则每一个枝叶都要展开进行搜索,如下图所示:
但搜索复杂度太高,而Beam Search的思路很简单,每一步只选取扩展概率最大的W个节点进行扩展,如下图所示:
由此可见,Beam Search实际上是对搜索数进行了剪枝,使得每一步最多扩展W个节点,而不是随着T的增加而呈指数增长,降低了搜索复杂度。
综上所述,CTC Beam Search的算法过程如下:
CTC一般是以字为建模单元和解码单元,而且假设解码单元之间是相互独立的。因此没有考虑词和词之间的组合关系,也就是没有语言模型,这会导致解码准确率较差。为了提高准确率,可以在CTC解码过程中加上语言模型,解码目标就修改成:
其中,代表语言模型。
但是,如果加上语言模型,CTC的解码目标和训练目标就不一样了,训练的时候我们是最大化似然,而解码的时候是为了最大化,也就是说训练阶段没有同时优化语言模型,没有考虑词与词直接的依赖关系。
References
2.https://xiaodu.io/ctc-explained
3.基于CTC的语音识别基础与实现 - 知乎
4.厦门大学 《语音识别原理与应用》 洪青阳 李琳 著
在MATLAB中使用数学符号,在matlab中怎么输入特殊符号 function在MATLAB中怎么用
MindSpore报错ValueError:`padding_idx` in `Embedding超出范围的报错
Python基础知识——程序流程的三大结构(顺序、选择、循环)
有一群200w年薪的朋友是什么感觉?谈一谈入学中国科学院大学的几点感受吧
AMD代号「Cato」的RX-8125、RX-8120、A9-9820处理器信息曝光!
flutter系列之:Material主题的基础-MaterialApp
matplotlib学习 设置图片大小、windows和linux设置字体的方式、频数直方图偏移现象、normed=True无效