最近在做语音识别的项目,了解一些端到端到的声学模型,大多数用的都是CTC算法,因此把一些学习心得记录下来分享。其中有很多是借鉴别人的博客文章,也有我自己对CTC原论文中的理解,都会分享给大家。我这几天慢慢的补,每天都会写一些。
借鉴的一些博客和文章:
https://blog.csdn.net/JackyTintin/article/details/79425866 有一些ctc细节的实现可以参考
https://distill.pub/2017/ctc/ 比较详细
https://blog.csdn.net/luodongri/article/details/80100297
https://www.zhihu.com/question/53399706/answer/134881480 论文最后公式的推导
别的博客、知乎上已经有很多介绍ctc的文章了,这里就只简单说一下。
先放出CTC算法论文:ftp://ftp.idsia.ch/pub/juergen/icml2006.pdf
CTC( Connectionist Temporal Classification,连接时序分类)是一种用于序列建模的工具,其核心是定义了特殊的目标函数/优化准则。其实就是用来解决时序类数据的分类问题。比如语音识别,OCR等等。
传统的语音识别的声学模型训练,对于每一帧的数据,需要知道对应的label才能进行有效的训练,在训练数据之前需要做语音对齐的预处理。而语音对齐的过程本身就需要进行反复多次的迭代,来确保对齐更准确,这本身就是一个比较耗时的工作。
而使用深度学习端到端的方法,语音识别的网络模型输出和标签长度通常是不对等的,网络模型输出可能是几百个拼音组合,而标签只有若干拼音,因此需要CTC算法将网络模型输出和标签进行对齐。如下图所示,若不进行对齐,输出为“wworrrlld!”。
序列问题可以形式化为以下函数:
:
这个过程可以看做是对输入特征数据做了变换
,
表示神经网络模型的变换。比如CNN、RNN等。
其中序列目标为字符串,也就是输出为n维多项概率分布(经softmax处理),n为词表的个数。
我自己的项目网络输出为(batchsize,200,1422),200就可以认为是一个时间序列;1422是总共有1421个不同的音素(拼音)加上一个blank空格这些音素的概率,总和为1(softmax)。
因此网络输出为,表示t时刻发音为第k个音素的概率。
上面的形式是输入到输出的一对一的映射。序列学习任务一般而言是多对多的映射关系(如语音识别中,上百帧输出可能仅对应若干音节或字符,并且每个输入和输出之间,也没有清楚的对应关系)。
比如输入一个200帧的音频数据,真实的输出是长度为5的结果。 经过神经网络处理之后,出来的还是序列长度是200的数据。比如有两个人都说了一句nihao这句话,他们的真实输出结果都是nihao这5个有序的音素,但是因为每个人的发音特点不一样,比如,有的人说的快有的人说的慢,原始的音频数据在经过神经网络计算之后,第一个人得到的结果可能是:nnnniiiiii…hhhhhaaaaaooo(长度是200),第二个人说的话得到的结果可能是:niiiiii…hhhhhaaaaaooo(长度是200)。这两种结果都是属于正确的计算结果,可以想象,长度为200的数据,最后可以对应上nihao这个发音顺序的结果是非常多的。CTC就是用在这种序列有多种可能性的情况下,计算和最后真实序列值的损失值的方法。
CTC通过引入一个特殊的blank字符(%),解决多对一的映射问题。blank的具体作用有1)标记静音区;2)分隔因素(如hello中的l-l)
首先扩展原始词表 为
。然后对于输出字符串定义变换
:1)合并连续的相同符号;2)去掉 blank 字符。
例如,对于 “aa%bb%%cc”,应用 ,则实际上代表的是字符串 “abc”。同理“%a%b%cc%” 也同样代表 “abc”。
总之,通过引入blank 及 变换,实现了变长的映射。
但是要注意的是,因为这个原因,CTC只能建模输出长度小于输入长度的序列问题。
和大多数有监督学习一样,CTC 使用最大似然标准进行训练。
给定输入,输出为目标序列
的条件概率为:
其中,表示了长度为
且示经过
变换 结果为
字符串的集合。
表示一条由
中元素组成的长度为
的路径,例如当目标序列
为'nihao',以下是几个路径的例子:
则:
CTC假设输出的概率是(相对于输入)条件独立的,因此路径的概率为它经过的各个时刻经过某个音素的概率相乘,也就是:
在没有对齐的情况下,目标函数应该为中所有路径概率之和,即上面列出的
在CTC原论文中,作者Alex Graves给出的是最小化以下目标函数,就是加了对数便于后面的梯度计算。
但是需要注意的是,路径数目的计算公式为(n为音素个数),量级大约为
,这么大的路径数目是无法直接计算的。因此CTC方法中借用了HMM中的向前向后算法来计算。
在前向及后向计算中,CTC 需要将输出字符串进行扩展。具体的,每个字符之间及首尾分别插入 blank,即扩展为
下面的
为原始字符串,指为扩展后的字符串。
###后面的公式太烦了我直接贴论文里面的图吧。。。###
定义:
这个代表了什么呢,可以理解为从初始到这一段里,所有正向路径的概率值和,而且这个值可以由
和
得到,比如说我们上面举的例子,目标序列是(n,i,h,a,o),对于第14帧这一时刻经过
为h的所有路径可以表示为(前置项)
(后置项),所以有:
而且该值可以由和
递推得到。
容易得到:
其中b代表blank,代表第一个音素。而且有:
递归效果图如下(目标序列CAT):
可以看到,这里有两种情况。
Case 1:
该种情况为,1)若当前生成序列为blank,则前一时刻生成序列只能为blank或者前一个label音素(这里是a)
2)若当前生成序列为重叠音素,例如‘hello’中的‘ll’时,也就是当前生成序列与s-2音素重叠,则不能从前一时刻的s-2序列顺接过来,因为中间必须隔着blank,且该blank在
变换中保留,因此前一时刻为blank(%a)或者仍然为当前音素(aa)
因此该种情况下有:
Case 2:
该种情况为时,当前时刻输出序列为音素b(如图),则可从前一时刻的前一字符(ab),blank(%b),当前字符(bb,重复字符将在
变换中消去)顺接过来。
因此该种情况下有:
论文中得出最后的似然值为:
这个理解的话,对于最后一个时间T时刻,对照最上面那个黑白圈的图,就是最后一个元素为最后一个标签序列值(黑圈)或者为blank(白圈)的所有路径概率之和就是整体的似然值。
类似前向计算,定义后向计算
则有:
下面,我们利用前向、后向计算的和
来计算梯度。为了训练能够进行,我们期望得到
,再根据反向传播得到
根据、
定义我们易得:
则有:
所以可得似然:
为计算,观察上式右端求各项,仅有
的项包含
,因此,其他项的偏导为0,不需要考虑,于是有:
利用除法的求导准则有:
注:与
各包含一个
中可能包含多个
字符,它们计算的梯度要进行累加,因此,最后的梯度计算结果为:
不过我们一般优化似然函数的对数,也就是上文提到的最小化目标函数:
因此可以得到
在实际训练中为了计算方便,将CTC和softmax的梯度计算合并,原论文中提到一个参数是unnormalized output,我理解的就是在网络模型在最后一层softmax层之前一层的输出,即:
由此式容易求得,其中当取1,其他时候取0.
定义
于是有,,然后即可推导:
这正是原文中的16式,这个结果似乎可以直观理解。(实际上是)是仅观察时刻输出层时,输出符号的概率;是从整体上看,所有路径中,在时刻输出符号的那些所占的概率比例。当网络参数取最优值时,梯度等于 0,即,也就是说这两个东西应该相等,也就是局部和整体一致?
项目框架用的是keras,keras自带ctc loss,但是由于从backend import,需要Lambda层来自定义损失函数。
看一下官方文档:
ctc_batch_cost(y_true, y_pred, input_length, label_length)
可以看到,这里的标签真值y_true是不需要进行one-hot处理的,这是因为函数内部自带稀疏处理,其实就是调用了这个函数:ctc_label_dense_to_sparse。具体的可以看源码。
此外由于数据量太大,采用数据生成的方法进行训练,也就是fit_generator函数进行训练,参数
generator:生成器函数,生成器的输出应该为:
一个形如(inputs,targets)的tuple
一个形如(inputs, targets,sample_weight)的tuple。所有的返回值都应该包含相同数目的样本。生成器将无限在数据集上循环。每个epoch以经过模型的样本数达到samples_per_epoch
时,记一个epoch结束。
这里要求从数据生成器中返回的数据为(inputs,targets),但是采用ctc损失函数时,标签真实值已经作为网络输入了,网络输入应该为(data_input,y_true,input_length,label_length),这里还要求返回一个targets,只要返回和标签尺寸一致的矩阵即可通过keras的检验,yield [x,y,input_length,label_length],np.ones(batch_size)即可啦。