决策树内部结构
该页面描述了语音决策树聚类代码的内部结构,代码实现为非常通用的结构和算法。
有关解释整个实现算法以及工具包如何使用,请参阅“如何在Kaldi中使用决策树”一节。
EventMap类
构建决策树代码的主要概念是“事件映射”,由EventMap类型表示。不要被“事件”这个术语误导,而错误地认为是特定时间发生的事情。事件只是一组(键,值)对,没有重复键。概念上,它可以由类型std :: map <int32,int32>表示。
事实上,为了提高效率,我们通过typedef将它们表示为一对排序向量:
typedef std :: vector <std :: pair <EventKeyType,EventValueType>> EventType;
这里,EventKeyType和EventValueType是int32的别名,给它们使用不同的名称是为了代码更容易被理解。将事件(类型EventType)视为变量的集合,其中,变量的名称和值都是整数。还有EventAnswerType类型,这也是int32。它是EventMap映射的类型;实际上它是pdf标识符(声学状态索引)。使用EventType类型的函数时,需要先排序(例如:通过调用std :: sort(e.begin(),e.end()))。
EventMap类的功能下面通过一个例子来说明。
假设我们的音素上下文是a/b/c;假设我们有标准的3状态拓扑;假设我们想在这个上下文中询问音素“b”的中心状态的pdf的索引是什么。所讨论的状态将是状态1,因为我们使用基于零的索引。当我们提到“状态”时,我们正在撇开一些细节;有关更多信息,请参阅Pdf-class。假设音素a,b和c的整数索引分别为10,11和12。 “事件”将对应于映射:
(0->10),(1->11),(2->12),(-1->1)
其中0,1和2是3音素窗口“a/b/c”中的位置,-1是我们用于对状态id进行编码的特殊索引(c.f.常量kPdfClass = -1)。表示为排序向量对为:
EventType e = {{-1,1},{0,10},{1,11},{2,12}};
假设对应于这种声学状态的声学状态索引(pdf-id)恰好为1000.那么如果我们有一个表示树的EventMap“emap”,那么我们期望以下的断言不会失败:
EventAnswerType ans;
bool ret = emap.Map(e,&ans); // emap的类型是EventMap; e是EventType
KALDI_ASSERT(ret == true && ans == 1000);
所以当我们声明一个EventMap是从EventType到EventAnswerType的映射时,你可以将它大概地看作是一个从上下文相关音素到整数索引的映射。上下文相关的音素表示为一组(键值)对,原则上我们可以添加新的键并输入更多信息。请注意,EventMap::Map()函数返回bool。这是因为某些事件可能无法映射到任何答案(例如,考虑无效的音素,或者想象当EventType不包含EventMap正在查询的所有键时会发生什么)。
EventMap是一个非常被动的对象。它没有任何学习决策树的能力,它只是存储决策树的一种手段。可以将其视为从EventType到EventAnswerType的函数结构。 EventMap是一个多态的纯虚拟类(即它不能被实例化,因为它具有未实现的虚函数)。有三个具体的类来实现EventMap接口:
ConstantEventMap:将其视为决策树叶节点。 此类存储一个类型为EventAnswerType的整数,其Map函数始终返回该值。
SplitEventMap:将其视为决策树非叶节点,查询某个关键字并根据答案转到“是”或“否”子节点。 它的Map函数调用相应子节点的Map函数。 它存储一组与“是”子对应的类型为kAnswerType的整数(所有其他内容都转到“否”)。
TableEventMap:这是对特定键进行完全拆分。 一个典型的例子是:您可能首先在中心音素上完全拆分,然后为该音素的每个值分别设置一个决策树。 内部它存储一个EventMap *指针向量。 它查找与其分割的键对应的值,并调用矢量中相应位置处的EventMap的Map函数。
EventMap除了将EventType映射到EventAnswerType之外,实际上不会做很多事情。它的接口不提供允许您遍历树的功能,无论是向上还是向下。只有一个函数允许您修改EventMap,就是EventMap::Copy()函数,声明如下(作为一个类成员):
virtual EventMap * Copy(const std :: vector <EventMap *>&new_leaves)const;
这具有类似于功能组合的效果。如果您调用Copy()使用空向量“new_leaves”,那么它只会返回整个对象的深层副本,并将所有指针复制到树上。但是,如果new_leaves为非空值,则每次Copy()函数到达叶子时,如果叶子l在范围(0,new_leaves.size() - 1)和new_leaves [l]不为NULL,则Copy )函数将返回调用new_leaves [l]->Copy()的结果,而不是返回一个新的ConstantEventMap的本身的Copy()函数。一个典型的例子是你决定拆分一个特定的叶子,如叶852.你可以创建一个类型vector <EventMap *>的对象,其唯一的非空成员是位置852.它将包含一个指向一个对象的指针,类型是SplitEventMap,“yes”和“no”指针将使用具有叶值的 ConstantEventMap(例如852和1234)(我们重新使用新叶子的旧叶节点ID)。真正的建树代码不是这样没有效率的。
统计建树
用于构建音素决策树的统计数据有以下类型:
typedef std::vector<std::pair<EventType,Clusterable *>> BuildTreeStatsType;
对这种类型的对象的例子被传递给所有的决策树构建过程。 这些统计信息预计不会包含相同的EventType成员的重复项,即它们在概念上表示从EventType到Clusterable的映射。 在我们当前的代码中,Clusterable对象实际上是GaussClusterable类型,但是树形代码不知道这一点。 累积这些统计信息的程序是accrest-stats.cc。
构建树的类和功能
Questions(config class)
类Questions是一个类,它与树构建的交互,表现得像一个“配置”类。它实际上是从“key”值(类型为EventKeyType)到类型为QuestionsForKey的配置类的映射。
类 QuestionsForKey有一组“问题”,每个都是一组类型为EventValueType的整数;这些主要对应于一组音素,或者如果键为-1,则在典型情况下它们将对应于HMM状态索引的集合,即{0,1,2}的子集。 QuestionsForKey还包含一个类型为RefineClustersOptions的配置类。这样做可以控制构建树的行为,因为树构建代码将尝试迭代地在分裂的两边之间移动值(例如音素),为了最大化似然(如K-means中K = 2)。然而,这可以通过将“精简集群”的迭代次数设置为零来关闭,这对应于从固定的问题列表中选择。这似乎工作好一点。
底层函数
有关完整列表,请参阅操作统计信息和事件映射的底层函数;我们在这里总结一些重要的。
这些函数主要涉及对BuildTreeStatsType类型的对象进行操作,如上所述,它们是(EventType,Clusterable *)对的向量。最简单的是DeleteBuildTreeStats(),WriteBuildTreeStats()和ReadBuildTreeStats()。
函数PossibleValues()发现一个特定的键在一个统计信息的集合中的值(并通知用户该键是否被定义); SplitStatsByKey()将根据特定键值(例如在中心音素上分割)将类型为BuildTreeStatsType的对象拆分为向量<BuildTreeStatsType>; SplitStatsByMap()执行相同的操作,但该索引不是该键的值,而是由EventMap返回的答案提供给该函数。
SumStats()对BuildTreeStatsType对象中的统计信息(即Clusterable对象)进行求和,并返回相应的Clusterable *对象。 SumStatsVec()获取一个类型为vector <BuildTreeStatsType>的对象,并输出一些类型向量<Clusterable *>,即它像SumStats(),但是对于一个向量;在处理SplitStatsByKey()和SplitStatsByMap()的输出时很有用。
ObjfGivenMap()用给出一些统计信息和EventMap评估目标函数:它总结了每个集群内的所有统计信息(对应于EventMap::Map()函数的每个不同答案),将整个集群中的目标函数相加,返回总数。
FindAllKeys()将找到在统计信息集合中定义的所有键,并且根据参数可以找到为所有事件定义的所有键或为任何事件定义的所有键(即采取交集或一组定义的键的并集)。
中间层函数
这里列出了下一批涉及构建树的函数,对应于构建树的各个阶段。我们现在只提一些代表性的。
首先,我们指出很多这些函数都有一个参数int32 num_leaves。这个整数作为分配新叶子的计数器。在建树开始时,调用者将其设置为零。当需要一个新叶子时,构建树的代码将它当前指向的数字作为新叶子ID,然后将其递增。
一个重要的函数是GetStubMap()。此函数返回尚未拆分的树,即,pdf不依赖于上下文。该函数的输入控制所有音素是不同的还是其中一些共享决策树根,以及特定音素内的所有状态是否共享相同的决策树根。
SplitDecisionTree()函数通常对应于由GetStubMap()创建的类型的非分割“存根”决策树作为输入,并且执行决策树分解,直到达到最大叶数为止,或者从分割叶子的增益小于指定的阈值。
ClusterEventMap()函数使用EventMap和阈值,合并EventMap的叶子直到代价低于阈值即可。通常在SplitDecisionTree()之后调用此函数。此函数还有其他版本可以操作限制(例如避免合并来自不同音素的叶子)。
上层建树函数
这里列出了最上层的建树函数。这些函数直接从命令行程序调用。最重要的是BuildTree()。给一个Questions配置类,以及一些关于共享树根的音素集信息(对于每组音素),是否在单个决策树中共享pdf-class,或者为每个pdf-class有不同的决策树。它还传递了有关音素长度的信息,加上各种阈值。它构建树并返回用于构造ContextDependency对象的EventMap对象。
另一个重要函数是AutomaticallyObtainQuestions(),用于通过音素自动聚类获取问题。它将音素聚集成一棵树,对于树中的每个节点,可从该节点访问的所有树叶形成一个问题(问题相当于音素集)。这个(来自cluster-phones.cc)使我们的方法独立于人造音素集。
函数AccumulateTreeStats()累加训练树的统计信息,给定一系列transition-id的特征和对齐(参见TransitionModel使用的整数标识符)。该函数在与其他树构建相关函数(hmm / not tree /)不同的目录中定义,因为它取决于更多的代码(例如它知道TransitionModel类),我们更希望解耦核心建树代码。