分类目录归档:ASR

CTC的理解

CTC技术近些年搭着End2End的顺风火了起来,查了一下原文,竟然是出在2006 Pittsburgh “International Conference on Machine Learning”的会议文章。Paper很容易找,搜索一下,能找到很多可用链接。

还是郑重地给出这篇文章的名字:

Connectionist Temporal Classification: Labelling Unsegmented
Sequence Data with Recurrent Neural Networks

要想理解这篇文章,要解决几个问题:

1. 如何实现了end2end?

2. 损失函数有什么特别?

3. 前后向算法在这个算法中的作用是什么?

其实理解这篇文章之后,这三个问题的本质是一样。

第一个问题:CTC如何实现了end2end?

在ASR领域,像DNN,LSTM这样的深度模型,输出的CD-phone的state。要做后面的文字的输出,需要加入字典(Lex)、语言模型(LM),与声学模型(AM)构成解码图后才可以。CTC越过字典与语言模型,直接输出phone或者字母(英文)、字(中文),虽然在性能上达不到其他的非e2e的模型的识别率,但这肯定是未来的大趋势,毕竟技术的发展应该是越来越简单,越来越直接。而且在实现的工程上,CTC在做识别时,也会结合LM,效果会更好。

与传统的声学模型训练相比,采用CTC作为损失函数的声学模型训练,是一种完全端到端的声学模型训练,不需要预先对数据做对齐,只需要一个输入序列和一个输出序列即可以训练。这样就不需要对数据对齐和一一标注,并且CTC直接输出序列预测的概率,不需要外部的后处理。

对于一个200帧的音频数据,假设真实的输出是7。经过神经网络后(可以dnn,rnn,lstm等),出来的序列还是200。比如说“chengyu”,将chengyu扩展到200的长度,路径有许多种。扩展的原则是:将一个字符扩展成连续多个,所有字符的长度总和为200,所有以下这几种都是合理的:

ccccc…hhhh…eeeeeee…ngyu

chengyyyyyyyyyy….uuuuuuuu

这时有一个问题,那就是像hello中有两个l,怎样处理?方法是在两个l之间插入一个无意义的\epsilon,例如hh…e…lllll\epsilonlll…o..。

RNN模型本质是对 P(z|x) 建模,其中s表示输入序列,o表示输出序列,z表示最终的label,o和l存在多对一的映射关系,即:P(z│x)=P(o|x),其中o是所有映射到z的输出序列。因此,只需要穷举出所有的o,累加一起即可得到P(z│x),从而使得RNN模型对最终的label进行建模。

第二个问题:CTC的损失函数有什么特别?

不同于用于分类的cross entropy准则和回归的MSE准则,CTC的损失函数巧妙在设计为-logP(z│x)。使P(z│x)的最大,等价于损失最小,通过求log的负值,转化成一个求最小值问题。

3. 前后算法在这个算法中的作用是什么?

要想计算出每一条可能路径的得分再将所有这些得分加起来,用蛮力方式显然是不可能,巧妙地运用语音识别中的前后向算法,即baum-welch算法可以大大减少计算量,用动态规划的算法思想有效在提高了效率。

借kaldi来理解softmax

nnet-forward --feature-transform=final.feature_transform --no-softmax=false final.nnet scp:1.scp ark,t:output.ark.txt

final.feature_transform, final.nnet均由训练而来。final.feature_transform是对输入特征做转换,作为final.nnet的输入。

final.nnet是最后一层是softmax层,可以由–no-softmax这个参数来指定。

1.scp是输入特征文件的索引,为简单起见,只用一个特征文件。

ark,t:output.ark.txt将输出转成文本文件并输出到output.ark.txt中。这是一个m x n的矩阵,m是输入特征的帧数,n是final.nnet的输出状态数(这里是3000)。

选其中一帧的输出来理解softmax

sed -n '10p' output.ark.txt > 1.txt

(1)–no-softmax=false,做softmax

nnet-forward --feature-transform=final.feature_transform --no-softmax=false final.nnet scp:1.scp ark,t:output.ark.txt
sed -n '10p' output.ark.txt > 1.txt 
awk '{print NF}' 1.txt

显示是应该就是用nnet-info final.nnet看到的最后一层的值:3000

awk 'BEGIN{total=0.0}{for(i=1;i<=NF;i++)total += $i}END{print total}' 1.txt

这时应该看到就是1,说明softmax是起作用的。再看1.txt里应该是3000个特别小的数。

(2). –no-softmax=true,不做softmax

nnet-forward --feature-transform=final.feature_transform --no-softmax=true final.nnet scp:1.scp ark,t:output.ark.txt
sed -n '10p' output.ark.txt > 2.txt

再看2.txt中的内容,应该是非常大的不规整的值。大于1数有很多。

(3). 运行softmax算法将2.txt转成1.txt

算法描述:

    1. 找出3000个数中的最大值qMax。

    2. 将每一值转成Xi=exp(Xi-qMax), 并累加到qSum

    3. 将每一值转成Xi/qSum

用python实现:

import sys
import math

qList = open(sys.argv[1],'r').readline().split()
print len(qList)

qMax = 0.0
for i in range(len(qList)):
    cur = float(qList[i])
    if cur>qMax:
        qMax = cur

print qMax
qSum = 0.0
for i in range(len(qList)):
    cur = float(qList[i])
    qList[i] = math.exp(cur-qMax)
    qSum += qList[i]

print qSum
fout = open(sys.argv[2],'w')
for i in range(len(qList)):
    qList[i] = qList[i]/qSum
    fout.write('%g\t' % qList[i])

fout.write('\n')
fout.close()

以上只是为了描述算法,效率不做考虑。

python softmax.py 2.txt 2_sm.txt

(4)比较1.txt, 2_sm.txt的值,日常接近,小有差异可能是python的数据转换带来的,对于算法的正确性没有影响。


kaldi中的代码实现:

./src/matrix/kaldi-matrix.cc

template<typename Real>
Real MatrixBase<Real>::ApplySoftMax() {
  Real max = this->Max(), sum = 0.0;
  // the 'max' helps to get in good numeric range.
  for (MatrixIndexT i = 0; i < num_rows_; i++)
    for (MatrixIndexT j = 0; j < num_cols_; j++)
      sum += ((*this)(i, j) = Exp((*this)(i, j) - max));
  this->Scale(1.0 / sum);
  return max + Log(sum);
}