前言

最近在看这个论文,本来想要写一个翻译,但是鉴于本人英语的渣水平,再加上论文本身一些说明,虽然能够看懂,但是很难翻译过来,所以还是写个阅读笔记好了。在这篇文章,我会跟着论文的思路大致说明论文的内容和自己的理解。

CTC解决什么问题

在许多的训练任务中,我们需要输入一个序列,这个序列有可能有噪声,并不是我们需要的信息都连续存在。比如声音的识别之类的,对于一个语音,假设我们要翻译成文字,但是声音中的信息并不是连续的,说了一个字可能要停顿个1秒才会说下个字:

  1. 我~~~~要吃饭
  2. 我要~~~~吃饭
  3. 我要吃~~~~饭

像上面这三种情况,显然结果都是我要吃饭。想要让RNN做到这一点,我们要解决两个问题,一个是,如何将RNN的输出翻译成同一句话,另一个问题,如何训练这样一个网络。CTC就这两个问题提供了一个可行的方案,使得训练这样的网络成为可能。

2.时序分类器(Temporal Classification)

论文的第二节,实际上是定义了一些基础的概念和问题,这个小标题大概是这么翻译吧……

这里提到了一大堆符号,不过有一些都是为了后面少说几个字~

S:表示训练集
χ=(Rm):输入空间,表示m维的所有序列(这里的m就是输入序列长度)
L:字符集
Z=L:输出空间,是字符集中的字符的组合序列。
(x,z):表示训练集中的一个训练样例。
x=(x1,x2,...,xT):一个训练样例中的输入的序列。
z=(z1,z2,...,zU):一个训练样例中的label的序列(就是应该输出的结果)。
UT:这里要说明的是,输出序列长度要小于等于输入序列的长度(RNN对于序列的每个元素都有个输出,但是我们要做一些操作吧这些输出转化成真正的结果)。

说了这么多,终于可以说训练的目标了,我们的目标是训练一个时序分类器h来将输入转换(映射)成我们需要的结果:

h:χZ

2.1 label的错误率计算

在论文里,评价标签的错误率使用了最小编辑距离的方法,也就是说,想要把一个串变成目标串最少需要几步(可用的操作包括添加、修改、删除一个字符)。最小编辑距离用ED(p,q)表示,这里的p,q就是要计算的两个字符串。

论文里还是用符号和公式来表达了最终测试的评价结果:

S:表示测试集
LER:标签的错误率(label error rate)

LER(h,S)=1|S|(x,z)SED(h(x),z)|z|a(1)

使用分类器h在测试集S上测试的标签错误率,计算方法是,对于每个样例,计算分类器的输出与label的最小编辑距离,然后除label的长度,把这些求和除测试集的长度~

3. Connectionist Temporal Classification

也不知道具体如何翻译,就这样吧。这一节是说,要把RNN的输出使用CTC来描述,最重要的一步是把网络的输出转化成标签上每个字符的独立的条件概率,然后网络就可以根据对应的最有可能的label来训练分类器。

3.1 从网络输出到标签

这里大概说了一下CTC网络的结构,首先是有一个softmax的输出层,输出层除了字符集L还有一个额外的单元,论文里称为’blank’,可以看作是一个特殊符号,表示这个输出不对应的字符集的元素。这些输出通过排列组合就可以得到所有可能的输出。
接下来,又是惯例的公式时间:

Nw:(Rm)T(Rn)T:这里对于一个长度为T的输入序列x,定义了一个m个输入,n个输出,权重向量为w的RNN网络作为映射函数。这里m,n指的是网络的输入输出,而T指的一个输入序列的长度,有些绕,就是一个输入到输出的定义,大概理解就好了。
y=Nw(x) :这里y就是网络的输出序列。
ytk:这个就是在t时刻,网络输出的k元素的值(概率)。
L=L{blank}:也就是说L是网络的所有输出元素的集合。
LT:一个长度为T的输出序列。

p(π|x)=t=1Tytπt,πLTa(2)
论文中说,把LT中的元素称之为paths,并用π表示。这个公式就是定义了在输入为x的时候,输出为某个路径π的概率,这个概率的算法,就是把对应的每个时刻的网络输出的对应元素的概率乘起来。

论文里说,(2)式假设了不同时刻网络的输出概率是条件独立的,并且这一点通过给定的网络的输出层与网络不存在反馈连接来保证:

This is ensured by requiring that no feedback connections exist from the output layer to itself or the network.

我看的时候还是有些疑惑的,因为RNN的输出在时间上是存在依赖性的,这里说不同时刻是独立的,总觉得哪里不太对。我的理解是,当RNN的输出确定之后,这时候对于CTC网络来说,每个时刻的输出是独立的,这样似乎可以解释得通。

然后又讲了一下从网络的输出映射为一个唯一的label的方法,其实就是把输出串的blank删掉,并且相邻的相同字符合并起来就好了。下面说一下相关的公式表示:

B:LTLT:这个就是上面说的映射函数,方法就向上面说的那样。(如:B(aab)=B(aaabb)=aab

p(l|x)=πB1(l)p(π|x)(3)
这个公式定义了在输入为x的时候,输出一个给定label的值l的概率,计算方法就是把所有可以用B函数映射为l的路径的概率相加。

3.2 构造分类器

根据上面说的,就可以很容易得出结论,最终我们的结果就是概率最大的那个labelling:

h(x)=argmaxlLT p(l|x)

不过,这个东西是没有一个好的解法的,因为要枚举所有的可能的l的话,时间复杂度就爆炸了。所以论文中给出了两个求近似的解法。
第一种:
叫作best path decoding。其实就是把每个时刻的输出的argmax作为最终的输出,用这个输出序列来求出最后的label:
h(x)B(π) where π=argmaxπNt p(π|x)(4)

第二种:
叫作prefix search decoding。这里论文讲的节奏有点不太好,论文提到了用后面的前向后向算法,用同样的方法来做一个前缀计算。这里先简单说一下前向后向算法在这里的作用,具体后面再说,这个算法利用动态规划,可以算出输出为某个前缀的概率的值,prefix search decoding的想法就是,我每次给当前的串添加一个字符(最开始是空),算出用哪个字符得到的概率最大,一个一个添上去,就得到了最终的答案:
CTC算法论文阅读笔记:Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurren

这里我还是有个疑问,就是输出串的长度是如何确定的,我猜应该是不断续上字符,然后所有的结果中取一个最大值吧。
但是呢,这么算还是有个bug,就是说,当最优解向下图中的实线部分那样分布,用这个方法是算不出来的。
CTC算法论文阅读笔记:Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurren
然后呢,论文的作者就加了一个黑科技,先把输出序列分段,在每一段的内部使用这个方法得到答案,然后把这些结果拼起来作为最终答案。而分段的依据就是通过判断每个时刻输出的blank的概率值大于某个阈值。这个方法表现的还是挺好的,但是在一个被分好的一段序列的内部再出现上面的情况,那就没办法了。

4. 训练网络

这里说了现在描述了一个用CTC来表示RNN的输出的方法,然后根据目标函数就可以开始愉快的训练了balabala~

4.1 CTC的前向后向算法

这一节讲了计算p(l|x)的算法,因为我们不可能枚举所有的可能性,所以这里采用了一个动态规划的方法来计算,作为搞过acm的人来说,动态规划就很熟悉啦,不说也知道怎么算了。这里就简单讲一下,主要是这里公式挺多的,也需要列一下。

简单来说呢,这个方法是基于这样一个结论:t时刻的一个前缀s,可以通过t-1时刻算出的前缀的结果推导出来。

下面会列一下论文中的公式和推导:

αt(s)=defπNT:B(π1:t)=l1:st=1tytπt(5)

这个公式看起来很复杂,其实也不难懂啦,其实就是定义了对于特定的l的前缀l1:s来说,αt(s)就是在t时刻可以转化成这个前缀的路径的概率总和。后面不会用到这个公式来计算啦,所以只要知道αt(s)是个什么东西就行了,它的值会用动态规划的算法算出来。

就像前面所说的:αt(s)是可以根据αt1(s)αt1(s1)来推导出来的。这个应该是比较容易理解的,如果是αt1(s)这种情况,那么只需要在后面乘上一个blank或者ls的概率就行了,如果是αt1(s1)这种情况,就要乘一个ls的概率,这两种情况的结果加起来就行了。

大概的意思就是上面所说的,但是实际实现的时候,还是有一些不同的。论文里,把给定的l做了一些修改,把每个字符之间还有前后两端插入了一个blank,得到了l,它的长度是2|l|+1。这样做的好处,一个是便于计算,另一个是可以保证每一步得到的结果是合法的,因为blank是具体区分每个字符的依据。

接下来,首先定义一下初始值:

α1(1)=y1bα1(2)=y1l1α1(s)=0,s>2

然后是状态转移方程:

αt(s)=α¯t(s)ytls(α¯t(s)+α¯t1(s2))ytlsif ls=b or ls2=lsotherwise(6)

α¯t(s)=defαt1(s)+αt1(s1)(7)

这个公式把α¯t(s)带入展开以后还是比较好懂的,需要注意的就是,当前一个字符是blank的时候,后面插入字符是任意的,但是如果目标有两个字符连在一起的时候,它们中间必须间隔一个blank,但是如果两个字符不同,就可以直接连接,另外,s的最长长度在时刻t是有限制的。

根据上面的公式,我们就能得到p(l|x)的结果了:

p(l|x)=αT(|l|)+αT(|l|1)(8)

然后,我们可以用相似的方法定义后缀的概率βt(s),这里的s指的就是从第s个字符到最后。这个公式我就不写了,直接放个图好了:
CTC算法论文阅读笔记:Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurren
接下来还有一个问题,就是这个计算有可能溢出,因为每一步都要进行乘法。所以,论文里在计算的时候加了一个缩放的操作:

Ct=defsαt(s),α^t(s)=defαt(s)Ct

在(6)(7)式中用α^代替α
后向的结果也一样:
Dt=defsβt(s),β^t(s)=defβt(s)Ct

这节的最后,提了一个最大似然误差的计算,但是我没看懂公式是怎么算出来的,后面貌似也没有用到。这里就贴个图吧,后面看懂再说。
CTC算法论文阅读笔记:Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurren

4.2 最大似然训练

这一节基本就是训练的各种公式,求导之类的。
首先,定义目标函数,涉及的符号前面都有提及:

OML(S,Nw)=(x,z)Sln(p(z|x))(12)

然后是对于每个网络(字符)的输出,可以单独地进行求导:
OML({(x,z)},Nw)ytk=ln(p(z|x))ytk(13)

接下来用上一节提到的前向后向算法来计算(13),对于一个特定的时刻t和位置s来说:

αt(s)βt(s)=πB1(l):πt=lsytlst=1Tytπt

联合公式(2),得到:
αt(s)βt(s)ytls=πB1(l):πt=lsp(π|x)

可以看出来,上面这个式子由特定的时刻和位置决定的,我们想要计算p(l|x),可以把这些情况全都加起来:
p(l|x)=t=1Ts=1|l|αt(s)βt(s)ytls(14)

在前面也说过,ctc网络输出是独立概率的,所以对于在t时刻,字符为k的一个路径的概率可以用上面的式子进行计算,也就是可以对ytk进行求导。然后需要注意的是,一个字符有可能在一个串中出现了很多次,所以我们定义一个集合来表示字符k出现的位置:lab(l,k)={s:ls=k}。当然,这个集合也有可能是空的(有些字符没有在串中出现)。然后通过(14),可以推导出来:

p(l|x)ytk=1yt2kslab(l,k)αt(s)βt(s)(15)

显然:
ln(p(l|x))ytk=1p(l|x)p(l|x)ytk

然后,论文里说令l=z,然后把(8)和(15)带入到(13)中,就可以求解了。这个的确可以推出来,但是和论文最后导出的公式不一样,不知道用了些什么骚操作,所以,这里我还是贴图吧。
CTC算法论文阅读笔记:Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurren

结语

这文章还是有一些细节没太看懂,希望了解的大佬可以讲解一下。不过整体的思路应该说的还是比较清晰的~

相关文章: