区分性训练与mmi(二):fst、lattice、supervision

目录

  • FST
    • FST与FSA
    • Semirings
      • Tropical Semiring
      • Log Semiring
      • 不同的Semiring可能用到的特性
      • WFST的基础操作(超级简略版本)
  • Lattice
    • 什么是Lattice
    • Kaldi:Lattice
    • Kaldi:CompactLattice
    • lattice-align-word& lattice-align-phones
  • Supervision
  • Reference

@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输入,不接受其他任何输入
wfsa,权重为1,仅接受ab输入,不接受其他任何输入,没有输出,或者可以看成输入ab,输出ab。
WFST实际上就是在WFSA的基础上加上了每条边的输出。
再如:
区分性训练与mmi(二):fst、lattice、supervision_第1张图片
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) ab=min(a,b) 。乘法变成了取两个数的和: a ⨂ b = a + b a\bigotimes b = a+b ab=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 a0=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}) xlogy=log(ex+ey)
  • 自定义乘法操作:+
  • 零元 0 ^ \hat{0} 0^:∞
  • 一元 1 ^ \hat{1} 1^:0

不同的Semiring可能用到的特性

  • Commutative :支持圈乘的交换律,即 x ⨂ y = y ⨂ x x\bigotimes y = y\bigotimes x xy=yx ,别看上面举的两个例子是支持这一操作的,实际上很多情况下是不支持的。

  • Idempotent:对于数域K中的所有元素x,满足 x ⨁ x = x x\bigoplus x= x xx=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...akak+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 (xy!=0^),zK使得 x = ( x ⨁ y ) ⨂ z x=(x\bigoplus y)\bigotimes z x=(xy)z
    //不知道为啥,我打出来的不等号\neq很奇怪,所以这里用!=表示不等。

  • ZeroSum Free Semiring: x ⨁ y = 0 ^ x\bigoplus y = \hat{0} xy=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=TATB意味把 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
      区分性训练与mmi(二):fst、lattice、supervision_第2张图片
      最下面这个图是由上面两个图经过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)&(ab<cd))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。
区分性训练与mmi(二):fst、lattice、supervision_第3张图片
我们看到会有三个权重。但是有的只有两个,这是因为tid序列要连在一起看才有意义,单看一条边的是没有意义的,个人推测是做了weightpushing的结果导致。
如果想让每条边都有其对应的对齐信息,我们可以使用lattice-align-word或lattice-align-phones。
可以看一下结果:

正常输出的Lattice:
区分性训练与mmi(二):fst、lattice、supervision_第4张图片

每条边的输入和输出是word-id,这里我只把olabel映射成了汉字,ilabel没有映射,还是word-id。

phone-align-lattice:
区分性训练与mmi(二):fst、lattice、supervision_第5张图片
这里我没有对应出来,但是它的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

你可能感兴趣的:(思考总结)