语音识别之CTC算法理解

前言

最近在做语音识别的项目,了解一些端到端到的声学模型,大多数用的都是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的文章了,这里就只简单说一下。

先放出CTC算法论文:ftp://ftp.idsia.ch/pub/juergen/icml2006.pdf

CTC( Connectionist Temporal Classification,连接时序分类)是一种用于序列建模的工具,其核心是定义了特殊的目标函数/优化准则。其实就是用来解决时序类数据的分类问题。比如语音识别,OCR等等。

传统的语音识别的声学模型训练,对于每一帧的数据,需要知道对应的label才能进行有效的训练,在训练数据之前需要做语音对齐的预处理。而语音对齐的过程本身就需要进行反复多次的迭代,来确保对齐更准确,这本身就是一个比较耗时的工作。

而使用深度学习端到端的方法,语音识别的网络模型输出和标签长度通常是不对等的,网络模型输出可能是几百个拼音组合,而标签只有若干拼音,因此需要CTC算法将网络模型输出和标签进行对齐。如下图所示,若不进行对齐,输出为“wworrrlld!”。

二.CTC算法

1.序列问题形式化

序列问题可以形式化为以下函数:

                            N_{w}:(R^{m})^{T}\left ( R^{m} \right )^{T}\rightarrow \left ( R^{n} \right )^{T}

这个过程可以看做是对输入特征数据x做了变换N_{w}N_{w}表示神经网络模型的变换。比如CNN、RNN等。

其中序列目标为字符串,也就是输出为n维多项概率分布(经softmax处理),n为词表的个数。

我自己的项目网络输出为(batchsize,200,1422),200就可以认为是一个时间序列;1422是总共有1421个不同的音素(拼音)加上一个blank空格这些音素的概率,总和为1(softmax)。

因此网络输出为y_{k}^{t},表示t时刻发音为第k个音素的概率。

2.B变换和blank实现变长映射

上面的形式是输入到输出的一对一的映射。序列学习任务一般而言是多对多的映射关系(如语音识别中,上百帧输出可能仅对应若干音节或字符,并且每个输入和输出之间,也没有清楚的对应关系)。

比如输入一个200帧的音频数据,真实的输出是长度为5的结果。 经过神经网络处理之后,出来的还是序列长度是200的数据。比如有两个人都说了一句nihao这句话,他们的真实输出结果都是nihao这5个有序的音素,但是因为每个人的发音特点不一样,比如,有的人说的快有的人说的慢,原始的音频数据在经过神经网络计算之后,第一个人得到的结果可能是:nnnniiiiii…hhhhhaaaaaooo(长度是200),第二个人说的话得到的结果可能是:niiiiii…hhhhhaaaaaooo(长度是200)。这两种结果都是属于正确的计算结果,可以想象,长度为200的数据,最后可以对应上nihao这个发音顺序的结果是非常多的。CTC就是用在这种序列有多种可能性的情况下,计算和最后真实序列值的损失值的方法。
CTC通过引入一个特殊的blank字符(%),解决多对一的映射问题。blank的具体作用有1)标记静音区;2)分隔因素(如hello中的l-l)

首先扩展原始词表L 为 L^{'}=L\cup \left \{ BLANK \right \}。然后对于输出字符串定义变换B:1)合并连续的相同符号;2)去掉 blank 字符。

例如,对于 “aa%bb%%cc”,应用 B,则实际上代表的是字符串 “abc”。同理“%a%b%cc%” 也同样代表 “abc”。 

B\left ( aa\%bb\%\%cc \right )=abc

总之,通过引入blank 及 B变换,实现了变长的映射。

L^{'T}\rightarrow L^{\leq T}

但是要注意的是,因为这个原因,CTC只能建模输出长度小于输入长度的序列问题。

 3.似然函数和目标函数

和大多数有监督学习一样,CTC 使用最大似然标准进行训练。

 

给定输入x,输出为目标序列l的条件概率为:

p \left ( l\left \right |x \right )=\sum_{\pi \in B^{-1}(l)}p \left ( \pi\left \right |x \right )

其中,B^{-1}(l)表示了长度为 T且示经过 B变换 结果为 l字符串的集合。\pi表示一条由L中元素组成的长度为T的路径,例如当目标序列l为'nihao',以下是几个路径的例子:

\pi^{1}=\left \{ n,n,n,i,i,h,h,h,h,a,a,o \right \}

\pi^{2}=\left \{ n,i,i,i,i,i,h,a,o,o,o,o, \right \}

\pi^{3}=\left \{ n,n,n,n,n,n,i,h,h,h,a,o\right \}

则:B\left ( \pi^{1} \right )=B\left ( \pi^{2} \right )=B\left ( \pi^{3} \right )=\left \{ n.i.h.a.o \right \}

 CTC假设输出的概率是(相对于输入)条件独立的,因此路径\pi=\left \{ \pi_{1},\pi_{2},\cdots ,\pi_{T} \right \}的概率为它经过的各个时刻经过某个音素的概率相乘,也就是:

p \left ( \pi\left \right |x \right )=p \left ( \pi\left \right |y=N_{w}\left ( x \right ) \right )= \prod_{t=1}^{T}y_{\pi_{t}}^{t}

在没有对齐的情况下,目标函数应该为\left \{ \pi\left\right|B\left ( \pi \right )=l \right \}中所有路径概率之和,即上面列出的

max\; p \left ( l\left \right |x \right )=max\;\sum_{\pi \in B^{-1}(l)}p \left ( \pi\left \right |x \right )=max\;\sum_{\pi \in B^{-1}(l)}\prod_{t=1}^{T}y_{\pi_{t}}^{t}

在CTC原论文中,作者Alex Graves给出的是最小化以下目标函数,就是加了对数便于后面的梯度计算。

O^{ML}\left ( S,N_{w} \right )=-\sum _{\left ( x,l\in S \right )}ln\left (p \left ( l\left \right |x \right ) \right )

 但是需要注意的是,路径数目的计算公式为C_{T-1}^{n}(n为音素个数),量级大约为\left ( T-1 \right )^{n},这么大的路径数目是无法直接计算的。因此CTC方法中借用了HMM中的向前向后算法来计算。

4.前向后向算法

在前向及后向计算中,CTC 需要将输出字符串进行扩展。具体的,\left ( a_{1},a_{2},\cdots ,a_{m} \right )每个字符之间及首尾分别插入 blank,即扩展为 \left ( \%,a_{1},\%,a_{2},\%,\cdots \%,a_{m},\% \right )下面的 L为原始字符串,L^{'}指为扩展后的字符串。

###后面的公式太烦了我直接贴论文里面的图吧。。。###

定义:

这个代表了什么呢,可以理解为从初始到y_{s}^{t}这一段里,所有正向路径的概率值和,而且这个值可以由a_{t-1}\left ( s \right )a_{t-1}\left ( s-1 \right )得到,比如说我们上面举的例子,目标序列是(n,i,h,a,o),对于第14帧这一时刻经过y_{k}为h的所有路径可以表示为(前置项)\cdot y_{h}^{14}\cdot(后置项),所以有:

而且该值可以由a_{13}\left ( h \right )a_{13}\left ( i \right )递推得到。

容易得到:

其中b代表blank,l_{1}代表第一个音素。而且有:

 递归效果图如下(目标序列CAT):

可以看到,这里有两种情况。

Case 1:

该种情况为,1)若当前生成序列为blank,则前一时刻生成序列只能为blank或者前一个label音素(这里是a)

2)若当前生成序列为重叠音素,例如‘hello’中的‘ll’时,也就是当前生成序列与s-2音素重叠\left ( l^{'}\left ( s \right )=l^{'}\left ( s-2 \right )\right ),则不能从前一时刻的s-2序列顺接过来,因为中间必须隔着blank,且该blank在B变换中保留,因此前一时刻为blank(%a)或者仍然为当前音素(aa)

因此该种情况下有:

a_{t}\left ( s \right )=\left [ a_{t-1}\left ( s \right )+a_{t-1}\left ( s-1 \right ) \right ]\cdot y_{l_{s}^{'}}^{t}\, \, \, \, \, \, if\, l_{s}^{'}=b\, or\, l_{s}^{'}=l_{s-2}^{'}

Case 2:

 该种情况为l^{'}\left ( s \right )\neq l^{'}\left ( s-2 \right )\right时,当前时刻输出序列为音素b(如图),则可从前一时刻的前一字符(ab),blank(%b),当前字符(bb,重复字符将在B变换中消去)顺接过来。

因此该种情况下有:

a_{t}\left ( s \right )=\left [ a_{t-1}\left ( s \right )+a_{t-1}\left ( s-1 \right )+ a_{t-1}\left ( s-2 \right )\right ]\cdot y_{l_{s}^{'}}^{t}

论文中得出最后的似然值为:

这个理解的话,对于最后一个时间T时刻,对照最上面那个黑白圈的图,就是最后一个元素为最后一个标签序列值(黑圈)或者为blank(白圈)的所有路径概率之和就是整体的似然值。

 类似前向计算,定义后向计算

 

则有:

 5.梯度计算

 下面,我们利用前向、后向计算的\alpha\beta来计算梯度。为了训练能够进行,我们期望得到\large \tfrac{\partial p\left ( l|x \right )}{y_{k}^{t}},再根据反向传播得到\large \tfrac{\partial p\left ( \pi|x \right )}{\partial w}

 根据\large \alpha _{t}^{s}\large \beta _{t}^{s}定义我们易得:

则有:

 

 所以可得似然:

为计算\large \tfrac{\partial p\left ( l|x \right )}{y_{k}^{t}},观察上式右端求各项,仅有\large s=k的项包含\large y_{k}^{t},因此,其他项的偏导为0,不需要考虑,于是有:

利用除法的求导准则有:

注:\large \alpha _{t}^{k}\large \beta _{t}^{k}各包含一个y_{k}^{t}

l中可能包含多个 k 字符,它们计算的梯度要进行累加,因此,最后的梯度计算结果为:

其中,\large lab\left ( l,k \right )=\left \{ s:l_{s}^{'}=k \right \}

不过我们一般优化似然函数的对数,也就是上文提到的最小化目标函数:O^{ML}\left ( S,N_{w} \right )=-\sum _{\left ( x,l\in S \right )}ln\left (p \left ( l\left \right |x \right ) \right )

因此可以得到

在实际训练中为了计算方便,将CTC和softmax的梯度计算合并,原论文中提到一个参数u_{k}是unnormalized output,我理解的就是在网络模型在最后一层softmax层之前一层的输出,即:

y_{k}=exp\left ( u_{k} \right )/\sum _{j}exp\left ( u_{j} \right )

由此式容易求得\partial y_j / \partial u_k = y_j (\delta_{jk} - y_k),其中\delta_{jk}j=k取1,其他时候取0.

定义

\sum_{s \in lab(\mathbf{l}, k)} \frac{\alpha_t(s) \beta_t(s)} {y_k^t} = s_k

于是有p = Z = \textstyle\sum_k s_k\partial p / \partial y_k = s_k / y_k,然后即可推导:

\begin{align} \frac{\partial O}{\partial u_k} &= \sum_j \frac{\partial O}{\partial y_j} \frac{\partial y_j}{\partial u_k} \\&= \sum_j \frac{\partial (-\ln p)}{\partial y_j} \frac{\partial y_j}{\partial u_k} \\&= - \frac{1}{p} \sum_j \frac{\partial p}{\partial y_j} \frac{\partial y_j}{\partial u_k} \\&= - \frac{1}{Z} \sum_j \frac{s_j}{y_j} \cdot y_j (\delta_{jk} - y_k) \\&= - \frac{1}{Z} (s_k - \sum_j s_j y_k) \\&= y_k - \frac{s_k}{Z} \end{align}

这正是原文中的16式,这个结果似乎可以直观理解。y_k(实际上是y_k^t)是仅观察t时刻输出层时,输出符号k的概率;s_k/Z是从整体上看,所有路径中,在t时刻输出符号k的那些所占的概率比例。当网络参数取最优值时,梯度等于 0,即y_k = s_k / Z,也就是说这两个东西应该相等,也就是局部和整体一致?

三.keras中的ctc loss实现

项目框架用的是keras,keras自带ctc loss,但是由于从backend import,需要Lambda层来自定义损失函数。

看一下官方文档:

ctc_batch_cost(y_true, y_pred, input_length, label_length)

  • y_true:形如(samples,max_tring_length)的张量,包含标签的真值
  • y_pred:形如(samples,time_steps,num_categories)的张量,包含预测值或输出的softmax值
  • input_length:形如(samples,1)的张量,包含y_pred中每个batch的序列长
  • label_length:形如(samples,1)的张量,包含y_true中每个batch的序列长

可以看到,这里的标签真值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)即可啦。

 

 

 

 

 

  • 10
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值