HMM的拓扑结构和状态转移的建模

介绍

在这里我们将介绍在kaldi用如何表示HMM topologies和我们如何让建模和训练HMM 转移概率的。我们将简要的说下它是如何跟决策树联系的;决策树你可以在How decision trees are used in KaldiDecision tree internals这些地方看到更详细的; 对于这个里面的类和函数,你可以看Classes and functions related to HMM topology and transition modeling

HMM topologies

HmmTopology类是为用户指定到HMM音素的拓扑结构。在平常的样例里,脚本创建一个HmmTopology 对象的文本格式的文件,然后给命令行使用。为了说明这个对象包含什么,接下来的就是一个HmmTopology 对象的文本格式的文件(the 3-state Bakis model):

<Topology>
<TopologyEntry>
 <ForPhones> 1 2 3 4 5 6 7 8
 </ForPhones>
 <State> 0 <PdfClass> 0
<Transition> 0 0.5
<Transition> 1 0.5
</State> 
 <State> 1 <PdfClass> 1 
<Transition> 1 0.5
<Transition> 2 0.5
 </State>  
 <State> 2 <PdfClass> 2
<Transition> 2 0.5
<Transition> 3 0.5
 </State>   
<State> 3
 </State>   
</TopologyEntry>
 </Topology>

这里在这个特定的HmmTopology对象里有一个TopologyEntry,它覆盖音素1到8(所以在这个例子里有8个音素和他们共享相同的topology)。他们有3个发射态;每一个有个自环和一个转移到下一个状态的。这里也有一个第四个非发射态,状态3 (对于它没有 entry ) 没有任何的转移。这是一个这些topology entries的标准特征;Kaldi 把第一个状态(state zero) 看做开始状态。最后一个状态,一般都是非发射状态和没有任何的转移,但会有最终的概率的那一个。在一个HMM中,你可以把转移概率看做最后一个状态的最终概率。在这个特定的例子里所有的发射状态他们有不同的pdf's (因为PdfClass数目是不同的)。我们可以强制数目一样。在对象HmmTopology里概率是用来初始化训练的;这个训练概率对于上下文相关的HMM来说是特定的和存储在TransitionModel对象。TransitionModel用一个类名来存储HmmTopology对象,但是要注意,在初始化TransitionModel后,一般不使用在HmmTopology 对象的转移概率。这里有个特例,对于那些不是最终的非发射状态(比如:他们有转移但是没有 entry),Kaldi不训练这些转移概率,而是用HmmTopology对象里给的概率。对于不支持非发射状态的转移概率的训练,实际上简化了我们的训练机制,和由于非转移状态有转移是不正常的,所以我们就觉得这些没有很大的损失。

Pdf-classes

pdf-class是一个与HmmTopology对象有关的概念。HmmTopology对象为每个音素指定了一个 HMM拓扑结构。HMM拓扑结构的每一个状态数有一个"pdf_class"变量。如果两个状态有相同的pdf_class变量,他们将共享相同的概率分布函数(p.d.f.),如果他们在相同的音素上下文。这是因为决策树代码不能够直接“看到”HMM状态,它仅仅可以看到pdf-class。在正常的情况下,pdf-class和HMM状态的索引(e.g. 0, 1 or 2)一样,但是pdf classes为用户提供一种强制共享的方式。如果你想丰富转移模型但是想离开声学模型,这个是非常有用的,要不然就变成一样的。pdf-class的一个功能就是指定非发射状态。如果一些HMM状态的pdf-class 设定常量kNoPdf = -1,然后HMM状态是非发射的(它与pdf没有联系)。这个可以通过删除标签和相联系的数字来简化对象的文本格式。 一个特定的HMM拓扑结构的pdf-classes的设置期望是从0开始和与其邻近的(e.g. 0, 1, 2)一些值。这些是为了图建立代码的方便和不会导致任何的损失。

Transition models (the TransitionModel object)

TransitionModel对象存储转移概率和关于HMM拓扑结构的信息(它包括一个HmmTopology 对象)。图建立代码根据TransitionModel对象来获得topology 和转移概率(它也需要一个 ContextDependencyInterface对象来获得与特定音素上下文的pdf-ids)。

How we model transition probabilities in Kaldi

我们跟据下面的4件事情决定一个上下文相关的HMM状态的转移概率(你可以把他们看做4-tuple):

  • 音素(HMM里的)
  • 源HMM-state (我们解释成HmmTopology对象, i.e. normally 0, 1 or 2)
  • pdf-id (i.e.与状态相关的pdf索引)
  • 在HmmTopology对象的转移的索引

这4项可以看做在HmmTopology对象里的目标HMM状态的编码。根绝这些事情来求转移概率的原因是,这是最细粒度(the most fine-grained way)的方式,我们在不增加编译解码图的大小来对转移建模。对于训练转移概率也是非常方便的。事实上,用传统的方法来对转移尽可能精确的建模可能没有任何区别,和在单音素层面上的共享转移的HTK方式可能更加有效。

The reason for transition-ids etc.

TransitionModel对象当被初始化时,就被设置成许多整数的映射,也被其他部分的代码用作这些映射。除了上面提到的数量,也有转移标识符(transition-ids), 转移索引(这个与转移标识符不是一样的), 和转移状态。之所以要介绍这些标识符和相联系的映射,是因为我们可以用一个完整的基本FST的训练脚本。最自然的基本FST的设置是输入标签是pdf-ids。然而,考虑到我们的树建立的方法,它不是总可能从pdf-id唯一的映射到一个音素,所以那样很难从输入的标签序列映射到音素序列,那样就不是很方便了;一般很难仅仅用FST的信息来训练转移概率。基于这个原因,我们把转移标识符transition-ids作为FST的输入标签,和把这些映射到pdf-id,也可以映射到音素,也可以映射到一个prototype HMM (在HmmTopology对象里)具体的转移概率。

Integer identifiers used by TransitionModel

以下类型的标识符都用于TransitionModel接口,它们所有的都用int32类型。注意其中的一些量是从1开始索引的,也有的是从0开始索引的。我们尽可能的避免从1开始索引的,因为与C++数组索引不兼容,但是因为OpenFst把0看做特定的情况(代表特殊符号epsilon), 我们决定把那些频繁在FST中用作输入符号的从1开始索引,我们就允许从1开始。更重要的是,transition-ids 是从1开始的。因为我们没有想到作为FST的标签,pdf-ids出现很频繁。因为我们经常使用他们作为C++数组索引,我们使他们从0开始,但当pdf-ids作为FST的输入标签,我们就给pdf-ids加1。当我们阅读TransitionModel代码时,我们应该对索引从1开始的量考虑,如果是这种情况我们就减去1,如果不是就不用减去。我们记录这些声明的成员变量。在任何情况,这些代码不是公开接口的,因为那会引起很多困惑。在TransitionModel中用的许多整数量如下:

  • 音素(从1开始): 标识符的类型贯穿整个工具箱;用OpenFst符号表可以把pdf-ids转换成一个音素。这类的id不一定是邻近的(工具箱允许跳过音素索引)。
  • Hmm状态(从0开始):这是HmmTopology::TopologyEntry类型的索引,一般取值为{0, 1, 2}里的一个。
  • Pdf或者pdf-id (从0开始):这是p.d.f.的索引,由决策树的聚类来初始化(看PDF identifiers)。在一个系统里通常由几千个pdf-ids。
  • 转移状态,或者trans_state (从0开始):这是由TransitionModel 自己定义的。每个可能的三(phone, hmm-state, pdf) 映射到一个自己的转移状态。转移状态可以认为是HMM状态最细化的分割尺度,转移概率都是分别评估的。
  • 转移索引或者trans_index (从0开始):这是HmmTopology::HmmState类型中的转移数组的索引,这个索引记录了转移出某个转移状态的次数。
  • 转移标识符或者trans_id (从0开始):其中的每一个都对应转移模型中的唯一的一个转移概率。用从(transition-state, transition-index)到transition-id的映射,反之亦然。

在转移模型代码中也参考如下的概念:

  • A triple意思就是a triple (phone, hmm-state, pdf)从转移状态中映射。
  • A pair意思就是a pair (transition-state, transition-index)从转移id中映射。

Training the transition model

转移模型的训练步骤是非常简单的,我们创建的FST(训练和测试中)将transition-ids作为他们的输入标签。在训练阶段,我们做维特比解码,生成输入标签的序列,就是每一个transition-ids的序列(每个特征向量一个,意思也就是对应每帧输出有限个transition-id的序列)。训练转移概率时我们积累的统计量就是数每个transition-id在训练阶段出现了多少次(代码本身用浮点数来数,但在正常的训练时我们仅仅用整数)。函数Transition::Update()对每一个transition-state做最大似然更新。这个明显是非常有用的。这里也有一些与概率下限有关的小的问题,和如果一个特定的transition-state不知道,我们怎么做?更多的细节,请看代码。

Alignments in Kaldi

这里我们将介绍对齐的概念。说到对齐,我们意思就是类型的向量,含有transition-ids的序列,(c.f. Integer identifiers used by TransitionModel)它的长度跟需要对齐的那句话一样长。transition-ids序列可以通过对输入标签序列解码获得。对齐通常用在训练阶段的维特比训练和在测试时间的自适应阶段。因为transition-ids编码了音素的信息,所以从对齐中计算出音素的序列是可行的 (c.f. SplitToPhones() and ali-to-phones.cc)。 我们经常需要处理以句子id做索引的一系列对齐。为了方便,我们用表来读和写“对齐”,更多的信息可以看 I/O with alignments 。 函数ConvertAlignment() (c.f.命令行convert-ali) 把对齐从一个转移模型转换成其他的。这种典型的情况就是你使用一个转移模型(由特定的决策树来创建)来做对齐,和希望将其转换为不同的树的其他的转移模型。最好的就是从最原始的音素映射到新的音素集;这个特征通常不是一定要的,但是我们使用它来处理一些由减少的音素集的简化的模型。 在对齐的程序里通常使用一个后缀"-ali"。

State-level posteriors

状态级的后验概率是“对齐”这个概念的扩展,区别于每帧只有一个transition-ids,这里每帧可以有任意多的transition-ids,每个transition-ids都有权重,存储在下面的结构体里:

typedef std::vector<std::vector<std::pair<int32, BaseFloat> > > Posterior;

如果我们有个Posterior类型的对象"post",post.size()就等于一句话的帧数,和post[i]是一串pair (用向量存储), 每一个pair有一个(transition-id, posterior)构成。 在当前的程序里,有两个方式创建后验概率:

  • 用程序ali-to-post来讲对齐转换为后验,但是生成的Posterior对象很细小,每帧都有一个单元后验的transition-id
  • 用程序weight-silence-post来修改后验,通常用于减小静音的权重。

未来,当加入lattice generation,我们将会从lattice生成后验。 与"-ali"相比,读入的后验程序没有后缀。这是为了简洁;读取状态层的后验要考虑对齐信息的这类程序的默认值。

Gaussian-level posteriors

一个句子的高斯级别的后验概率用以下的形式存储:

typedef std::vector<std::vector<std::pair<int32, Vector<BaseFloat> > > > GauPost;

这个和状态级的后验概率结构体非常相似,除了原来是一个浮点数,现在是一个浮点数向量(代表状态的后验),每个值就是状态里的每个高斯分量。向量的大小跟pdf里的高斯分量的数目一样,pdf对应于转移标识符transition-id,也就是每个pair的第一个元素。 程序post-to-gpost把后验概率的结构体转换为高斯后验的结构体,再用模型和特征来计算高斯级别的后验概率。当我们需要用不同的模型或特征计算高斯级别的后验时,这个就有用了。读取高斯后验程序有一个后缀"-gpost"。

Functions for converting HMMs to FSTs

把HMMs转换为FSTs的整个函数和类的列表可以在here这里找到。 GetHTransducer() 最重要的函数就是GetHTranducer(), 声明如下:

fst::VectorFst<fst::StdArc>*
GetHTransducer(const std::vector<std::vector<int32> > &ilabel_info,
constContextDependencyInterface &ctx_dep,
constTransitionModel &trans_model,
constHTransducerConfig &config,
std::vector<int32> *disambig_syms_left);

如果没有介绍ilabel_info对象,ContextDependencyInterface接口,和fst在语音识别里的一些应用基础,这个函数的很多方面大家会很难理解。这个函数返回一个FST,输入标签是 transition-ids ,输出标签是代表上下文相关的音素(他们有对象ilabel_info的索引)。FST返回一个既有初始状态又有最终状态,所有的转移都有输出标签(CLG合理利用)。每一个转移都是一个3状态的HMM结构,和循环返回最初状态。FST返回GetHTransducer()仅仅对表示ilabel_info的上下文相关音素有效,我们称为特定。这是非常有用的,因为对于宽的上下文系统,有很多数量的上下文,大多是没有被使用。ilabel_info对象可以从ContextFst (代表C)对象获得,在合并它和一些东西,和它含有已经使用过的上下文。我们提供相同的ilabel_info对象给GetHTransducer()来获得覆盖我们需要的一个H转换器。 注意GetHTransducer()函数不包括自环。这必须通过函数AddSelfLoops()来添加;当所有的图解码优化后,仅仅添加自环是很方便的。

The HTransducerConfig configuration class

The HTransducerConfig configuration class控制了GetHTransducer行为。

  • 变量trans_prob_scale是一个转移概率尺度。当转移概率包含在图里,他们就包含了这个尺度。命令行就是–transition-scale。对尺度的合理使用和讨论,可以看Scaling of transition and acoustic probabilities。
  • 一个变量reverse和二个其他变量是可以修改的,如果"reverse"选项是true。"reverse"选项是为后向解码创建一个time-reversed FSTs。为了使这个有用,我们需要添加功能到Arpa语言模型里,我们将在以后去做。

The function GetHmmAsFst()

函数GetHmmAsFst() 需要一个音素上下文窗和返回一个用transition-ids作为符号的对应的有限状态接受器。这个在GetHTransducer()使用。函数GetHmmAsFstSimple() 有很少的选择被提供,是为了表示在原理上是怎么工作的。

AddSelfLoops()

AddSelfLoops() 函数把自环添加到一个没有自环的图中。一个典型的就是创建一个没有自环的H转换器,和CLG一起,做确定性和最小化,和然后添加自环。这样会使确定性和最小化更有效。AddSelfLoops() 函数哟选项来重新对转移排序,更多的细节可以看Reordering transitions。它也有一个转移概率尺度,"self_loop_scale",这个跟转移概率尺度不是一样的,可以看Scaling of transition and acoustic probabilities。

Adding transition probabilities to FSTs

AddTransitionProbs() 函数添加转移概率到FST。这样做时有用的,原因是我们创造了一个没有转移概率的图(i.e. without the component of the weights that arises from the HMM transitions), 和他们将在以后被添加;这样就使得训练模型的不同的迭代使用相同的图成为可能,和保持图中的转移概率更新。创造一个没有转移概率的图是使用trans_prob_scale (command-line option: –transition-scale)为0来实现的。在训练的时候,我们的脚本开始存储没有转移概率的图,然后每次我们重新调整时,我们就增加当前有效的转移概率。

Reordering transitions

AddSelfLoops()函数有一个布尔选项"reorder" ,这个选项表明重新对转移概率排序,the self-loop comes after the transition out of the state。当应用变成一个布尔命令行,比如,你可以用 –reorder=true 在创建图时重新排序。这个选项使得解码更加简单,更快和更有效(看Decoders used in the Kaldi toolkit),尽管它与kaldi的解码不兼容。 重排序的想法是,我们切换自环弧与所有其他状态出来的弧,所以自环维语每一个互相连接的弧的目标状态(The idea of reordering is that we switch the order of the self-loop arcs with all the other arcs that come out of a state, so the self-loop is located at the destination state of each of the other arcs)。为了这个,我们必须保证FST有某些特性,即一个特定状态的所有弧必须引导相同的自环(也,一个有自环的状态不能够有一个静音的输入弧,或者是开始的状态)。AddSelfLoops() 函数修改图,以确保他们有这个特性。一个相同的特性也是需要的,即使"reorder"选项设置为false。创建一个有"reorder"选项的图准确的说就相对于你解码一个句子得到的声学和转移模型概率而言,是一个非重新排序的图 。得到的对齐的transition-ids 是一个不同的顺序,但是这个不影响你使用这些对齐。

Scaling of transition and acoustic probabilities

这里有三种在kaldi中使用的尺度类型:

Name in code    Name in command-line arguments    Example value (train)    Example value (test)
acoustic_scale    –acoustic-scale=?    0.1    0.08333
self_loop_scale    –self-loop-scale=?    0.1    0.1
transition_scale    –transition-scale=?    1.0    1.0

你也许注意到这里没有用语言模型的尺度;相对于语言模型来说,任何事情都是尺度。我们不支持插入一个词的惩罚,一般来说(景观kaldi的解码不支持这个)。语言模型表示的真正的概率,一切相对于他们的尺度都是有意义的人。在训练阶段的尺度是我们在通过Viterbi 对齐解码得到的。一般而言,我们用0.1,当一个参数不是很关键和期望它是小的。当测试是很重要的和这个任务是调整的,声学尺度将被使用。现在我们来解释这三个尺度:

  • 声学尺度是应用到声学上的尺度(比如:给定一个声学状态的一帧的log似然比).
  • 转移尺度是转移概率上的尺度,但是这个仅仅适合多个转换的HMM状态,它应用到这些转移中的相对的权重。它对典型的没有影响。
  • 自环尺度是那些应用到自环的尺度。特别的是,当我们添加自环,让给定自环的概率为p,而剩下的为(1-p)。我们添加一个自环,对数概率是self_loop_scale log(p), 和对所有其他不在这个状态的对数转移概率添加(self_loop_scale log(1-p))。在典型的topologies,自环尺度仅仅是那个有关系的尺度。

我们觉得对自环应用一个不同的概率尺度,而不是正常的转移尺度有意义的原因是我们认为他们可以决定不同时间上事件的概率。稍微更加微妙的说法是,所有的转移概率可以被看成真正的概率(相对于LM 概率), 因为声学概率的相关性的问题与转移没有关系。然而,一个问题出现了,因为我们在测试时使用Viterbi 算法(在我们的例子里,训练也是)。转移概率仅仅表示当累加起来的真正的概率,在前向后向算法里。我们期望这个是自环的问题,而不是由于HMM的完全不同的路径的概率。因为后面的情况里,声学分布通常不是连接的,在前向后向算法和 Viterbi 算法里的差别很小。