Kaldi决策树状态绑定学习笔记(三)

建议在csdn资源页中免费下载该学习笔记的PDF版进行阅读:)点击进入下载页面

Kaldi决策树状态绑定学习笔记(三)

——EventMap及其派生类、roots文件

  到现在为止,程序acc-tree-stats累积好了构建决策树所需的统计量,程序cluster-phones和compile-questions自动生成好了构建决策树所需的问题集,那么我们就可以开始构建决策树,对三音素GMM的状态进行绑定了。但是在构建决策树之前,我们必须理解清楚决策树构建代码中一个很核心的类:EventMap,只有对EventMap及其派生类理解透彻了,才能深入理解Kaldi决策树构建代码。
  在这个笔记中,我会首先花大篇幅介绍EventMap及其派生类。因为Kaldi构建决策树除了需要累积好的统计量和问题集,还需要一个roots文件,所以接下来会对roots文件进行说明。

  建议学习Kaldi官方文档《Decision tree internals》、《How decision trees are used in Kaldi》。

目录

EventMap

  建议学习Kaldi官方文档《Decision tree internals》的Event maps部分。
  EventMap是Kaldi决策树状态绑定部分的核心,只有对EventMap理解透彻了,才能看明白构建决策树的代码到底在讲什么。
  在Kaldi决策树状态绑定学习笔记(一)里面,我们讲过EventType,在这里只简单介绍下:EventType描述三音素和HMM状态信息,其中保存着四对数,其中三对数表示三音素三个位置上的音素分别是什么,剩下的一对数表示HMM状态编号。
  来看几个int32的别名:  
  1. EventKeyType:和EventValueType成对出现;一般表示三音素的位置,当取值为0,1,2时,分别代表三音素从左到右的三个位置;当取值为-1时(一般用常量kPdfClass表示-1),其对应的EventValueType表示的是HMM的第几个状态,也就是HMM state-id。
  2. EventValueType:和EventKeyType成对出现;当EventKeyType取值为kPdfClass(-1)时,该值表示HMM state-id(一般为0,1,2);当EventKeyType取0,1,2时,该值表示三音素EventKeyType位置上的音素编号(从1开始对音素进行编号)。
  3. EventAnswerType:表示发射概率密度函数(p.d.f.)的编号pdf-id;在HMM-GMM模型中,发射概率密度函数就是混合高斯函数;每一个(三音素+HMM state-id)都能确定一个HMM状态,而每个HMM状态都有一个发射p.d.f,所以每个EventType都对应一个pdf-id。状态绑定想做的事就是使多个EventType对应到同一个pdf-id,这样就能减少参数,更好的训练模型。
  EventType就是四个<EventKeyType, EventValueType>对,其具体的定义是vector<pair<EventKeyType, EventValueType> >
  每一个EventType(三音素+HMM state-id)都能确定一个HMM状态,而每个HMM状态都有一个发射p.d.f,我们对模型中所有的p.d.f.进行编号,用不同的pdf-id表示不同的p.d.f,那么从EventType到pdf-id就有一个映射关系,怎么表示这一映射关系?这个时候EventMap就要出场了,EventMap实现了从EventType到pdf-id的映射。
  具体来讲,EventMap对象的成员方法Map()实现了从EventType到EventAnwserType的映射。
  举个例子,假设三音素是a/b/c,其音素编号分别为10,11,12,我们想知道该三音素第二个HMM状态的pdf-id是多少(假设答案是1000),下面的代码找出该pdf-id:

EventType e = { {-1, 1}, {0, 10}, {1, 11}, {2, 12} };
EventAnswerType ans;
bool ret = emap.Map(e, &ans); // emap是一个EventMap
// 此时ans为1000

  EventMap是保存决策树的一种方法,它是一个多态纯虚类,不能够被实例化。有三个具体的类继承自EventMap,实现了EventMap接口,每种类都有不同的功能,下面我逐一介绍。

ConstantEventMap

  ConstantEventMap表示决策树的叶子结点。
  我们先来思考一下决策树叶子结点的作用和其需要保存什么信息。
  给定EventType(三音素+HMM state-id,之后都用EventType代表三音素+HMM state-id),我们希望通过决策树得到什么?我们希望通过决策树得到这个EventType对应的p.d.f.的pdf-id。
  假设我们已经构建好了一个决策树,对某一个EventType,我们从决策树的树根开始问问题,比如左边的音素属于问题集1吗(每个问题集都是一些音素的集合)?右边的音素属于问题集20吗?根据对问题的回答我们就会进入决策树的不同分支,直到到达这个决策树的某一叶子结点,若该叶子结点保存着pdf-id,那么我们就得到了EventType对应的pdf-id。
  前面我们讲到,可以用EventValueType表示pdf-id,那么叶子结点就只需要保存一个EventValueType类型的变量answer_,用来保存该叶子结点对应的pdf-id即可。

SplitEventMap

  SplitEventMap表示决策树的非叶子结点。
  我们先来思考一下决策树的非叶子结点的作用和其需要保存什么信息。
  给定一个EventType(三音素+HMM state-id,假设HMM state-id是x),我们开始在中间音素的第x个HMM状态对应的决策树上查找该EventType所属的叶子结点,也就是查找该EventType对应的p.d.f.的pdf-id。对于该EventType,在决策树的每一个非叶子结点,我们都会问一个问题以决定进入该非叶子结点的哪个分支,比如“左边的音素属于问题集1吗?”、“右边的音素属于问题集20吗?”,那么我们该怎么表示“左边”、“右边”呢?根据我们对EventType的理解,可以用EventKeyType类型的变量来表示这个位置信息,我们将其命名为key_——当其取值为0时,我们是对左边的音素问问题;当其取值2时,我们是对右边的音素问问题;因为Kaldi也可以对HMM状态问问题,所以key_可以取kPdfClass(-1),并且当其取值为kPdfClass时,我们是对HMM状态问问题。
  在论文中遇见的手工制作的问题类似这样:“左边的音素是鼻音吗?”、“右边的音素是元音吗?”上面我们已经解决了如何描述“左边”、“右边”的问题,另一个问题来了,我们怎么表示“鼻音”、“元音”这些概念呢?其实鼻音就是一些音素的集合,元音也是一些音素的集合。Kaldi中不使用手工制作的问题集,而是使用自动生成的问题集,这些问题集中的每一个问题也都是一些音素的集合。于是我们发现在非叶子结点所问问题的本质其实就是一些音素的集合。这样一来,在每个非叶子结点所问的问题就等价于“第key_个位置的音素属于音素集合1吗?”、“第key_个位置的音素属于音素集合20吗?”类似于EventType那样,我们可以用EventValueType类型的变量表示一个音素,用vector类型的变量表示音素集,我们把这个变量命名为yes_set_。
  此时,我们在每个非叶子结点所问的问题可以从“左边的音素时鼻音吗?”变成等价的“第key_个位置的音素属于音素集合yes_set_吗?”
  当第key_个位置的音素属于yes_set_时,我们进入命名为yes_的孩子结点;当第key_个位置的音素不属于yes_set_时,我们进入命名为no_的孩子结点。因为孩子结点可以是叶子结点也可以是非叶子结点,所以用EventMap *来表示指向两个孩子结点的指针。(注意,属于yes_set_和不属于yes_set_两者结合起来等于全部的音素,也就是在每个非叶子结点,无论音素是什么,总能进入某一个分支)
  综上所述,表示决策树非叶子结点的SplitEventMap所需的数据成员包括:问题所问的位置key_,音素集yes_set_、问题答案属于音素集yes_set_时进入的孩子指针yes_,和问题答案不属于音素集yes_set_时进入的孩子指针no_。
  实际代码如下所示,注意ConstIntegerSet只是vector的高效表示而已,本质一样。

  EventKeyType key_;
  //  std::vector<EventValueType> yes_set_;
  ConstIntegerSet<EventValueType> yes_set_;  // more efficient Map function.
  EventMap *yes_;
  EventMap *no_;

TableEventMap

  一般来说,对每个中间音素的每个状态都要建立一棵决策树进行状态绑定,比如说有63个不同音素,每个音素3个HMM状态,则需要建立63x3=189个决策树。但是Kaldi中想把这193个决策树放进一棵大树里面,这棵大树的193个叶子结点分别是193个决策树的起点;我们随后对这193个叶子结点的每一个进行扩展,每个叶子结点都扩展成一棵决策树,整个完整的大决策树就生成了。当然,这棵大树也用EventMap表示。我个人理解TableEventMap的作用就是更高效的建立扩展之前的这棵大树。当讲到后面的GetStubMap()时应该会体会到这一点。若在每个非叶子结点SplitEventMap上对某个key_问一个问题集yes_set_对大树进行划分,63个点要划分多次才能到达一个叶子结点,可能划分至少5次才能进入一个叶子结点,我们能不能在划分第二次的时候,直接生成多个叶子结点呢?当然可以。注意在SplitEventMap上进行一次划分最多生成两个叶子结点,TableEventMap则可以直接生成多个叶子结点。
  TableEventMap的数据成员包括进行划分的位置EventKeyType key_,以及指向对其划分后的所有子结点的指针向量std::vector<EventMap*> table_

Map()

  三种EventMap(指ConstantEventMap、SplitEventMap、TableEventMap)具有相同的成员函数接口,但是其具体实现不太一样,具体实现和不同EventMap的功能有关。其中最重要的是搞清楚三种EventMap的Map()方法和Copy()方法做了什么事情。下面我们就逐一讲解。
  先不看Map()方法的不同实现,我们从统一的接口来看Map()方法完成什么工作。前面讲过,EventMap实现了从EventType到EventValueType到映射,也就是从(三音素+HMM state-id)到pdf-id的映射。Map()的作用是输入EventType,输出pdf-id,其函数声明如下,event是输入的EventType,ans是返回的pdf-id。

bool Map(const EventType &event, EventAnswerType *ans)

  在讲解不同的Map()实现之前,我们先讲一下EventMap中用到的静态方法bool Lookup(const EventType &event, EventKeyType key, EventValueType *ans),输入event和key,输出该event中位置key上保存的内容——若key是kPdfClass(-1),输出HMM state-id;或key是0,1,2,输出音素编号。
  决策树是一棵树,树这个数据结构中很重要的思想就是递归。所以关于Map()和Copy()很重要的一点就是递归调用,记住这一点会帮助你进一步理解代码。

  • SplitEventMap::Map()。前面讲过,SplitEventMap表示决策树的非叶子结点,在该结点会检查EventType第key_个位置的值(音素编号或HMM state-id)是否属于yes_set_,若属于则进入yes_孩子结点,若不属于则进入no_孩子结点。在SplitEventMap::Map()中,我们首先查找EventType第key_个位置的值value是什么,若该value在yes_set_中,则递归调用yes_孩子的Map();若该value不在yes_set_中,则递归调用no_孩子的Map()。直到孩子结点是一个叶子结点ConstantEventMap,直接返回保存pdf-id的EventAnswerType answer_。
    举个例子,对于某个中间音素x的决策树,给定中间音素是x的EventType(三音素+HMM state-id)。从该决策树的根结点我们开始进行划分,根结点SplitEventMap保存着自己的key_和yes_set_,我们查看该EventType第key_个位置的值是否在yes_set_中,若在则调用yes_孩子的Map(),若不在则调用no_孩子的Map();对每个非叶子结点重复这样的划分。假设我们一直进入yes_孩子,则一直递归调用yes_孩子的Map(),直到yes_孩子是一个叶子结点ConstantEventMap,这时就直接把ans置为该叶子结点保存的pdf-id answer_。然后一路返回到树根,这时的ans已经是该EventType对应的pdf-id了,于是我们就根据输入EventType得到了输出EventAnswerType。
  virtual bool Map(const EventType &event, EventAnswerType *ans) const {
    EventValueType value;
    if (Lookup(event, key_, &value)) {
      // if (std::binary_search(yes_set_.begin(), yes_set_.end(), value)) {
      if (yes_set_.count(value)) {
        return yes_->Map(event, ans);
      }
      return no_->Map(event, ans);
    }
    return false;
}
  • ConstantEventMap::Map()。前面讲过,ConstantEventMap表示决策树的叶子结点,其保存着该叶子结点所对应的pdf-id。Map()方法的作用就是返回EventType对应的pdf-id,所以当调用ConstantEventMap::Map()时,就直接将ans置为answer_。
    依旧以上面的例子为例,给定EventType,当对决策树的非叶子结点SplitEventMap(后简称SE)一路递归调用Map()到达决策树的叶子结点时,直接返回ConstantEventMap(后简称CE)中保存的answer_即得到了该EventType对应的pdf-id。
  virtual bool Map(const EventType &event, EventAnswerType *ans) const {
    *ans = answer_;
    return true;
     }
  • TableEventMap::Map()。在构建扩展之前的决策树时,TableEventMap(后简称TE)也可以作为非叶子结点。在TE结点的Map()方法中,会检查EventType第key_个位置的值tmp是否在table_的范围内,当table_的第tmp个元素存在且不为空时,对TE的第tmp个孩子结点,也就是table_的第tmp个元素递归调用Map()函数,直到到达叶子结点CE,返回该CE的pdf-id answer_。
  virtual bool Map(const EventType &event, EventAnswerType *ans) const {
    EventValueType tmp;   *ans = -1;  // means no answer
    if (Lookup(event, key_, &tmp) && tmp >= 0
       && tmp < (EventValueType)table_.size() && table_[tmp] != NULL) {
      return table_[tmp]->Map(event, ans);
    }
    return false;
  }

Copy()

  Copy()是修改决策树的唯一方法。作用类似于composition(我翻译成扩展)。
  在之后的例子中讲解Copy()的作用。


roots文件

  在Kaldi中构建决策树时,除了需要累积的统计量和问题集,还需要roots文件。本部分对roots文件进行说明。
  roots文件指明在决策树的聚类过程中,哪些音素应该共享树根。对放在roots文件同一行的音素共享一个树根,并且roots的每一行需指明下述两件事:

shared or not-shared

  “shared”或“not-shared”说的是,对一个音素的不同HMM状态应该建立分开的决策树树根呢,还是让这个音素的不同HMM状态共享一个决策树树根?通过对决策树状态绑定原理的学习,我们知道对一个音素的每个HMM状态都应该建立一个决策树:若一个音素都包含3个HMM状态,则其需要建立三个决策树。若指定”shared”,则Kaldi使三个HMM状态上的决策树共享同一个树根。若指定”not-shared”,则Kaldi为三个HMM状态分别建立决策树树根。
  有人可能会问:如果三个HMM状态共享同一个树根,那么在决策树的每个非叶子结点对左边的音素或右边的音素问问题之后,同一三音素的不同HMM状态不就总是进入同一叶子结点吗?这样一来某一三音素的不同HMM状态对应的pdf-id不就相同了吗?而我们知道某一三音素的不同HMM状态对应的pdf-id一般是不同的。
  Kaldi采用另外一种机制来解决这个问题:在每个非叶子结点可以对HMM状态位置问问题!若某一音素的三个HMM状态共享一个决策树树根,则在该决策树的非叶子结点可以问这种问题:“第kPdfclass(-1)位置的HMM state-id属于{0, 1}吗?”我们知道EventType的第kPdfClass位置保存的值是HMM state-id,当该EventType的HMM state-id是0、1时进入yes_分支;当HMM state-id是2时进入no_分支,这样不同HMM状态就进入不同分支,最后就会进入不同的叶子结点,从而具有不同的pdf-id。

split or not-split

  “split”或”not-split”说的是,是否应该对当前的决策树树根进行划分。如果该行指明”split”,则对决策树进行划分;如果该行指明”not-split”,则对该决策树不进行任何划分。
  下面我们看一个具体的roots文件,上面是roots.txt,下面是对应的roots.int,第一列为行号:
这里写图片描述

这里写图片描述

  以第三行为例,a1,a2,a3,a4,a5共享一个决策树根,并且该行音素的不同HMM状态也共享(”shared”)一个决策树根。
注意:roots文件中有两种“共享”。一种是roots文件中位于同一行的音素共享同一个决策树;另一种是在每一行前指定”shared”,对不同HMM状态共享同一个决策树。


作者:许开拓
日期:写于2017/04/05~04/07
联系方式:540262601@qq.com

发布了22 篇原创文章 · 获赞 22 · 访问量 3万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览