目录
@author yuxiang.kong
FST
抛开其他的不说,我们如果想用MMI训练,可以暂且不管目标函数怎么计算的,但是需要知道导数是怎么计算的。导数无论分子还是分母是在一个FST上进行前向后向计算。所以,有必要了解一下FST的基础知识。这里只是为了更好理解MMI而了解一下FST的操作使用,如果想特别详细地了解WFST在语音中的应用,在网上可以搜到很多资料。
FST与FSA
WFST-- Weight Finite State Transducer
WFSA-- Weight Finite State Automaton
我们在kaldi中使用的基本都是有权重的,所以这里面提到的FST、FSA实际上都是WFST、WFSA。
我们最开始可能接触到WFSA更多一些,有限状态自动机,你在其他地方肯定也听过这个名字。它描述了一些有限的状态,这些状态中有开始状态有结束状态,这些状态通过有向边进行链接。如果想从一个状态跳转到另外一个状态,你需要给边一些输入,直到到达最终状态结束。
实际上就是检测你这个输入合不合法,没有输出,或者看成输出就是输入也行,不影响理解。
wfsa,权重为1,仅接受ab输入,不接受其他任何输入,没有输出,或者可以看成输入ab,输出ab。
WFST实际上就是在WFSA的基础上加上了每条边的输出。
再如:
wfst,权重为1,仅接受ab输入,并且返回cd作为输出,不接受其他任何输入。
注意,无论是wfsa还是wfst,节点并没有太多实际意义。
WFST的一些常用类型:
- Deterministic WFST:每个状态节点state对于某一个输入,只有最多一条边。
- Sequential WFST:只有一个initial state
- Functional WFST:对于一个输入序列x,WFST只有唯一确定的输出序列y
- Epsilon-Transition:有输入为空的state
- Stochastic WFST:对于每一个状态,所有以它为起点的边的权重,和为1
Semirings
半环,这是一种逻辑结构,可以参考一下其他的博客或者教程里面的介绍。我们这里不介绍太多理论东西,我们这里只是让你知道有这么个东西。
为什么需要知道它?因为可以说WFST就是个semiring,为了更方便地在WFST上进行计算,我们先来了解一下semiring的计算规则。
半环所具有的元素:数域K,自定义加法操作,自定义乘法操作,零元,一元。
懵逼了是不是?直接来看例子你就懂了。
Tropical Semiring
- 数域:R
- 自定义加法操作:min
- 自定义乘法操作:+
- 零元 0 ^ \hat{0} 0^:∞
- 一元 1 ^ \hat{1} 1^:0
也就是说,对于实数中的任何数字,它的加法操作变成了去两个数的最小值: a ⨁ b = m i n ( a , b ) a\bigoplus b = min(a,b) a⨁b=min(a,b) 。乘法变成了取两个数的和: a ⨂ b = a + b a\bigotimes b = a+b a⨂b=a+b 。零元意思是一个数圈加上零元等于自身: a ⨁ ∞ = m i n ( a , ∞ ) = a a\bigoplus \infty = min(a,\infty) = a a⨁∞=min(a,∞)=a 。一元的意思是一个数圈乘一元得到他自身: a ⨂ 0 = a + 0 = a a\bigotimes 0 = a + 0 = a a⨂0=a+0=a
至于其他的还需要满足分配律、交换律、结合律、反身性的一些东西,有兴趣的自己去查一下。
不难发现,这就是自己定义的一套规则。(我们传统意义上的加减乘除是大家普遍接受的一套规则罢了)
Log Semiring
- 数域:R
- 自定义加法操作:“log加”, x ⨁ l o g y = − l o g ( e − x + e − y ) x\bigoplus_{log} y = -log(e^{-x}+e^{-y}) x⨁logy=−log(e−x+e−y)
- 自定义乘法操作:+
- 零元 0 ^ \hat{0} 0^:∞
- 一元 1 ^ \hat{1} 1^:0
不同的Semiring可能用到的特性
-
Commutative :支持圈乘的交换律,即 x ⨂ y = y ⨂ x x\bigotimes y = y\bigotimes x x⨂y=y⨂x ,别看上面举的两个例子是支持这一操作的,实际上很多情况下是不支持的。
-
Idempotent:对于数域K中的所有元素x,满足 x ⨁ x = x x\bigoplus x= x x⨁x=x
-
k-closed: 1 ^ ⨁ a ⨁ . . . ⨁ a k = 1 ^ ⨁ a ⨁ . . . ⨁ a k ⨁ a k + 1 \hat{1}\bigoplus a \bigoplus ... \bigoplus a^k =\hat{1}\bigoplus a \bigoplus ... \bigoplus a^k\bigoplus a^{k+1} 1^⨁a⨁...⨁ak=1^⨁a⨁...⨁ak⨁ak+1 或者可以写成 ⨁ n = 0 k a n = ⨁ n = 0 k + 1 a n \bigoplus ^k_{n=0} a^n = \bigoplus ^{k+1}_{n=0} a^n ⨁n=0kan=⨁n=0k+1an
如果是0-closed,则 1 ^ = 1 ^ ⨁ a \hat{1}=\hat{1}\bigoplus a 1^=1^⨁a 。
-
Weakly-left divisible Semiring: ∀ ( x ⨁ y ! = 0 ^ ) , ∃ z ∈ K \forall (x\bigoplus y != \hat{0}), \exist z\in K ∀(x⨁y!=0^),∃z∈K使得 x = ( x ⨁ y ) ⨂ z x=(x\bigoplus y)\bigotimes z x=(x⨁y)⨂z
//不知道为啥,我打出来的不等号\neq很奇怪,所以这里用!=表示不等。 -
ZeroSum Free Semiring: x ⨁ y = 0 ^ x\bigoplus y = \hat{0} x⨁y=0^当且仅当 x = y = 0 ^ x=y=\hat{0} x=y=0^
一个从别的地方copy过来的总结表,有兴趣的可以看一下。
Tropical Semiring | Log Semiring | String Semiring | Probability Semiring | |
---|---|---|---|---|
Commutative | ✓ \checkmark ✓ | ✓ \checkmark ✓ | ✓ \checkmark ✓ | |
Idempotent | ✓ \checkmark ✓ | ✓ \checkmark ✓ | ✓ \checkmark ✓ | |
K-closed | ✓ \checkmark ✓ | ✓ \checkmark ✓ | ✓ \checkmark ✓ | |
Weakly left Divisible | ✓ \checkmark ✓ | ✓ \checkmark ✓ | ✓ \checkmark ✓ | |
Zero Sum Free | ✓ \checkmark ✓ | ✓ \checkmark ✓ | ✓ \checkmark ✓ |
WFST的基础操作(超级简略版本)
这里面只是作为了解,了解一下WFST的一些操作即可,后面方面理解kaldi中的一些数据结构,详细的版本我以后会另外写一份。
- Composition
描述:把两个transducer合并在一起。 T = T A ∘ T B T=T_{A} \circ T_{B} T=TA∘TB意味把 T A T_{A} TA和 T B T_{B} TB合并在一起。- 要求:可以合并的边要满足 T A T_{A} TA中的边输出要等与 T B T_{B} TB中边的输入。
- 作用:这样在kaldi中就可以将两种不同的level表示合并成一个,这样就可以实现不同level的映射。比如把C和LG合并在一起。同时也相当于对某一个transducer加上另一个transducer的约束。
- 例子:(图自SPEECH RECOGNITION WITH WEIGHTED FINITE-STATE TRANSDUCERS)
最下面这个图是由上面两个图经过composition的操作得到的。
- Epsilon Removal
描述:对于某些输入输出为空的边,我们可以进行简化不要了,这样可以减去一些节点和边,节省很多空间。 - Determinization
作用:实现对于一个输入x让它只有一个唯一的输出y。 - Weight-pushing
将weight重新整合,让它尽可能地在靠近初始节点的边上得到一些权重,而后面的权重为0,同样,initial节点的weight也会调整。 - minimization
用最少的state表示原wfst
后面四个都是属于优化操作,所以我写的很简略,关键是需要了解一下composition的作用。
Lattice
什么是Lattice
在说Lattice之前,我们来想一下语音识别CE的训练流程中,神经网络的输出是和什么相对应?aliment。也就是训练语料的某一帧所对应的pdf-id,这个是由decode得到的,也称为强制对齐。
那lattice是个什么东西?我们在很多地方看到了这个词,但没有人告诉你这个是什么。原因很简单,lattice在不同地方的定义是不一样的,实现方式当然也是不同的。但如果了解了Lattice的目的,问题就会简单很多了。
最开始的lattice的产生,是为了得到n-best结果而准备的,因为如果是找最优路径aliment,可能在声学上最优,但结果不一定是我们想要的结果。所以出现了n-best,在一次解码中我们不是保留一条最优路径,而是保留多很有可能的条路径。然后把这些路径存储在一个graph中。这个graph也就是wfst,实际上lattice的本质就是wfst。
在kaldi中,lattice有两种存储结构:Lattice 和 CompactLattice
Kaldi:Lattice
Kaldi中Lattice的定义是,状态级别的,通常用来描述一句话。
- 边的输入ilabel是transition-id
- 边的输出olabel是词word
- 权重weight包含两个数值:声学acoustic得分和graph得分
自定义的 ⨁ \bigoplus ⨁ 和 ⨂ \bigotimes ⨂ 的操作为:
- ( a , b ) ⨂ ( c , d ) = ( a + c , b + d ) (a,b)\bigotimes (c,d) = (a+c,b+d) (a,b)⨂(c,d)=(a+c,b+d)
- ( a , b ) ⨁ ( c , d ) = { ( a , b ) ( a + b < c + d ) o r ( ( a + b = c + d ) & ( a − b < c − d ) ) ( c , d ) o t h e r s (a,b)\bigoplus(c,d) = \left\{ \begin{array}{rcl} (a,b) & & {(a+b < c+d) or ((a+b=c+d) \& (a-b<c-d))}\\ (c,d) & & {others} \end{array} \right. (a,b)⨁(c,d)={(a,b)(c,d)(a+b<c+d)or((a+b=c+d)&(a−b<c−d))others
Kaldi:CompactLattice
和Lattice描述的内容是一致的,但是表述的方式是不一样的。
- CompactLattce是个WFSA
- ilabel和olabel都是words
- weight中包含三个部分:声学acoustic得分和graph得分以及transition-id的序列
- kaldi程序内部处理的时候用的是Lattice,但是对外输出的时候用的是CompactLattice,kaldi也把CompactLattice称为Lattice的“final form”。两者在kaldi中可以直接转换
最有趣的就是它把weight重新定义了,由三个部分组成。因此它自己也重新定义了 ⨁ \bigoplus ⨁ 和 ⨂ \bigotimes ⨂ 的操作:
- ( w 1 , s e q 1 ) ⨂ ( w 2 , s e q 2 ) = ( w 1 + w 2 , s e q 1 + s e q 2 ) (w1,seq1)\bigotimes(w2,seq2) = (w1+w2,seq1+seq2) (w1,seq1)⨂(w2,seq2)=(w1+w2,seq1+seq2)
- ( w 1 , s e q 1 ) ⨁ ( w 2. s e q 2 ) (w1,seq1)\bigoplus(w2.seq2) (w1,seq1)⨁(w2.seq2) 返回的是得分小的那个,如果两个得分相同,则返回seq长度短的那个,如果两个seq长度相同,则按照字典序返回。
lattice-align-word& lattice-align-phones
我们来看一下Lattice输出的样子。输出的实际上是CompactLattice。
我们看到会有三个权重。但是有的只有两个,这是因为tid序列要连在一起看才有意义,单看一条边的是没有意义的,个人推测是做了weightpushing的结果导致。
如果想让每条边都有其对应的对齐信息,我们可以使用lattice-align-word或lattice-align-phones。
可以看一下结果:
正常输出的Lattice:
每条边的输入和输出是word-id,这里我只把olabel映射成了汉字,ilabel没有映射,还是word-id。
phone-align-lattice:
这里我没有对应出来,但是它的ilabel和olabel都是phone-id,就是上一张图“是的”的拓展,它在拓展的时候做了一些其他的操作,所以最后看到的是只有一个结束节点。
两幅图的权重都没有画出来。
注意,生成出来的lattice可以认为是对齐,没有自环,并且每条路径上的tid序列长度和相同
Supervision
Supervision依然是一个WFST,源于Lattice但又和Lattice有些不同。具体有啥不同当我们看了流程就知道了。我这里讲到的是chain中的supervision,代码是chain-get-supervision
-
首先在生成supervision之前,需要先用lattice-align-phones,然后把phoneLattice作为输入,输入给supervision。
-
然后把Lattice转换成ProtoSupervision,函数:PhoneLatticeToProtoSupervision
在主程序执行之前,先要计算每个state_times
int CompactLatticeStateTimes(lat,&state_times)
/*
输入PhoneLattice,和state_times数组
*/
//初始化
num_states = lat.NumStates()
state_times->resize(num_states,-1)
state_times[0]=0
//统计时间
for(state in num_states){
cur_time = state_times[state]
for (arc in state){
state_times[nextstate] = cur_time + ark.num-tids
}
if state is finalstate{
检查到达finalstate的路径长度是否相同
路径长度 = cur_time + sate.weight.num-tids
保留所有路径最长的长度作为utt长度
}
}
return utt_len
总的来说就是算每个state出现的时间点,然后返回句子长度。
PhoneLatticeToProtoSupervision
//初始化
num_state = lat.NumStates()
proto_supervision.ReserveState(num_states)
num_frames = CompactLatticeStateTimes
num_frames_subsampled = num_frames/subsample_factor //这里是指subsample操作后的长度,一般来说subsample的系数是3,也就是假设原来长度是6,现在长度是2,向上取整
proto_supervision->allowed_phones.resize(num_frames_subsampled)
//allowed_phones是一个二维数组
//迭代完成protosupervision
for (state in num_states){
for (arc in state){
phone = arc.ilabel
proto_supervision->fst.AddArc(state,Arc(phone,phone,TropicalWeight::One(),arc.nextstate))
//也就是说,这里只是保留了phone信息,把所有的对齐序列都扔掉了
然后可以根据这条边开始结束节点的state_times找到这条边的subsample后的长度包括开始和结束时间点
for(time in sumbsampledtimes){
proto_supervision->allowed_phones[time].push_back(phone)
}
}
}
总结来说,protosupervision保留了lattice的phone信息,但没有保留lattice的tid对齐信息,然后里面有一个关键的allowed_phones描述的是这个句子的某一帧可能出现的phone,对后面的裁剪有帮助。
-
我们知道,Supervision实际上就是个分子图,我们计算的时候是要在分子图上跑前向算法和后向算法的,所以我们如果只有phone级别的信息是不够的,我们需要扩展到pdf-id级别。因此,最后,把protosupervision转换成supervision,函数:ProtoSupervisionToSupervision
注意:我们最后得到的是一个WFSA而不是WFST
整个流程比较简单,kaldi代码本身的注释也够看懂,如果想深入了解,还需要对FST有深入的了解才行。这里只简述一下流程即可。我们想,目前我们得到的图,实际上可以看成一个LG.fst,虽然不严谨,但是如果把一个L.fst和G.fst拼起来实际上就是phone对应到word,word之间的跳转遵循ngram。或者像论文中说的P.fst。那么我们要拼成HCLG.fst则只差CH,那我们依次先compose C,然后compose H即可。的确是这样做的。
但要注意,我们的P.fst和CH都是没有时间信息的,那我们得到的HCLG肯定是一个有很多自环的图,而我们想要的是这句话的每一帧是个什么pdf是明确清楚的。因此,我们最后得到HCLG之后还需要和时间信息compose一下。这里的时间信息,就是我们刚刚提出来的allowed_phones。
所以代码流程就是三个compose和三个project。注意,这里的project操作实际上是属于WFSA的基本操作,所以在上面没列出来。也比较简单。就是把WFST转成WFSA,Projec(input)就是把output替换为input,Project(output)就是把input替换为output。
这里就不用伪代码简述流程了。直接copy一下kaldi中的代码,里面的注释很详细。同样,只是有关键步骤,很多细节忽略掉了。
//C VectorFst<StdArc> context_dep_fst; fst::ComposeContextFst(cfst, phone_fst, &context_dep_fst); // at this point, context_dep_fst will have indexes into 'ilabels' as its // input symbol (representing context-dependent phones), and phones on its // output. We don't need the phones, so we'll project. fst::Project(&context_dep_fst, fst::PROJECT_INPUT); //H VectorFst<StdArc> *h_fst = GetHTransducer(cfst.ILabelInfo(), ctx_dep, trans_model, h_cfg, &disambig_syms_h); KALDI_ASSERT(disambig_syms_h.empty()); bool reorder = true; // more efficient in general; won't affect results. // add self-loops to the FST with transition-ids as its labels. AddSelfLoops(trans_model, disambig_syms_h, self_loop_scale, reorder, &transition_id_fst); // at this point transition_id_fst will have transition-ids as its ilabels and // context-dependent phones (indexes into ILabelInfo()) as its olabels. // Discard the context-dependent phones by projecting on the input, keeping // only the transition-ids. fst::Project(&transition_id_fst, fst::PROJECT_INPUT); //这里有个函数是AddSelfLoops,很重要,我会在后面的博客中讲到。 //时间约束 // The last step is to enforce that phones can only appear on the frames they // are 'allowed' to appear on. This will also convert the FST to have pdf-ids // plus one as the labels TimeEnforcerFst enforcer_fst(trans_model, proto_supervision.allowed_phones); ComposeDeterministicOnDemand(transition_id_fst, &enforcer_fst, &(supervision->fst)); fst::Connect(&(supervision->fst)); // at this point supervision->fst will have pdf-ids plus one as the olabels, // but still transition-ids as the ilabels. Copy olabels to ilabels. fst::Project(&(supervision->fst), fst::PROJECT_OUTPUT);
这就是分子图构建的整个流程和原理,参杂了一些个人理解在里面。欢迎交流补充。
Reference
kaldi文档:http://www.kaldi-asr.org/doc/lattices.html
Speech Recognition With Weighted Finite-State Transducers
Generating Exact Lattice in The WFST Framework