单音素训练-train-mono.sh

转载:https://www.cnblogs.com/welen/p/7505173.html

目录

0. 预备知识

1. gmm-init-mono

2. compile-train-graphs

3. align-equal-compiled 和gmm-align-compiled

4. EM Algorithm for GMM training

5. gmm-acc-stats-ali

6. gmm-est

7.Summary


0. 预备知识

单音素的训练在一个名为train-mono.sh的shell脚本里。解释之前大家需要一些储备知识。需要了解HMM,GMM以及Viterbi算法。由于代码中频繁使用Transition_Model, AmDiagGmm等,故先做一些总结如下。

kaldi对每个音素建立一个HMM模型,叫做HMMEntry,由三个state组成,定义在hmm-topology.cc 中。 如上图所示,transition state是一个整数,对应于三个变量:phone的ID号,三个hmm-state(0,1,2),global unique的pdf-id(P.d.f 为GMM模型的概率密度函数)。transition-index 对应于一个pair,也就是下一个state的ID和到下一个state的transmission/emitting probability. 而transition-ID 也对应一个pair,为transition state和transition index。

这样说可能不是很明白。再做一点解释,这些id号都是为了表达抽象的HMM转移模型,如果你需要在数学上表达一个transition,你需要知道在哪个phone上,而每个phone由多个state组成,每个state有多条边(Arc),这些边可能是self-loop,可能transmit 到下一个state,也可能emit 等等。这些边中的第几个就是transition-index,而在哪个phone,phone中哪个state,这个state的emit后的GMM概率密度表达, 这三个共同决定了transition state,因此,为了精确表达一个transition-id,需要transition state和transition-index 共同决定。

 

接下来一一讲解train-mono.sh 中的关键部分。

1. gmm-init-mono

Usage: gmm-init-mono <topology-in> <dim> <model-out> <tree-out>

输入:HMM的拓扑结构,特征维度
输出:模型文件 决策树文件

该程序首先初始化全局特征的均值和方差glob_inv_varglob_mean均为1,大小等于特征维度。如果有输入特征,另外再计算更新。

然后,读入拓扑结构HmmTopology,建立里面所有phone到该phone的Pdf class数目的映射,对于通常情况为3(在kaldi的官方英文文档中有说明In the normal case the pdf-class is the same as the HMM state index (e.g. 0, 1 or 2)but pdf classes provide a way for the user to enforce sharing)。

接着就是构建决策树了,对于单音素,不需要进行KMeans聚类,所以只需要根据set.int文件(该文件描述共享共享根结点的phone)递归的构造树就好了。

 

构建决策树的主要函数GetStubMap。 讲解该函数首先必须大致清楚kaldi里对树节点的分类,以后会在以后三音素训练中决策树构建中仔细讲解:
CE(Constant EventMap):叶子节点,直接保存该节点存放的transition ID;
SE(Split EventMap): 非叶子节点,保存左右子节点yes,no等等
TE(Table EventMap): 非叶子节点,保存节点数以及各个子节点的指针(与SE的区别在于TE的分裂更快,不只是两个分支,可以多个)

现在开始讲解GetStubMap:首先该函数输入phone_sets,所有共享的音素在一个vector中,所有的这些vector组成了phone_sets, 如果phone_sets的大小是1且其中phone都共享,返回CE,否则返回TE;如果phone_sets中每个vector的大小最大是1,并且phone_sets中vetor的数目小于最大phoneId的两倍,也返回TE;如果不是上述两种情况,进行split,phonesets分为两部分分别递归call GetStubMap,并构建SE。

 

GetStubMap讲的比较含糊,这个会在三音素训练中详细讲解。

接下来gmm-init-mono 构建GMM。 每个pdf初始化只有一个gmm,也就是单高斯,均值方差来自之前的glob_inv_var,glob_mean, weight为1,均值方差也都为1,计算常数部分gconsts 即高斯分布x为0时的概率,最后将所有gmm模型导入声学模型am_gmm中。
最后输出transition和声学模型的文件model,决策树tree

 

2. compile-train-graphs

Usage: compile-train-graphs [options] <tree-in> <model-in> <lexicon-fst-in> <transcriptions-rspecifier> <graphs-wspecifier>

输入:tree , transition_model ,L.fst(Lexicon), 训练音频文件的字幕, 输出为图 HCLG.fst
该函数比较简单,图的构建部分理论来自大佬Mohri的论文SPEECH RECOGNITION WITH WEIGHTED FINITE-STATE TRANSDUCERS

这部分在以后构图的地方仔细讲解。 只需知道由模型,决策树,词典对每个训练的音频文件说的话构建HCLG的图就好了。所用的函数是CompileGraphsFromText

 

补充:生成与音频特征对齐的HMM状态序列时要用到每句话的FST。

构造训练的网络,从源码级别分析,是每个句子构造一个phone level 的fst网络。

$sdaba/JOB/text 中包含对每个句子的单词(words level)级别标注, L.fst是字典对于的fst表示,作用是将一串的音素(phones转换成单词(words

构造monophone解码图就是先将text中的每个句子,生成一个fst(类似于语言模型中的G.fst,只是相对比较简单,只有一个句子),然后和L.fst 进行composition 形成训练用的音素级别(phone levelfst网络(类似于LG.fst)。

fsts.JOB.gz 中使用 key-value 的方式保存每个句子和其对应的fst网络,通过 key(句子) 就能找到这个句子的fst网络,value中保存的是句子中每两个音素之间互联的边(Arc),例如句子转换成音素后,标注为:"a b c d e f"

那么value中保存的其实是 a->b b->c c->d d->e e->f 这些连接(kaldi会为每种连接赋予一个唯一的id),

后面进行 HMM 训练的时候是根据这些连接的id进行计数,就可以得到转移概率。

 

3. align-equal-compiled gmm-align-compiled

这两个一起将的原因是功能比较类似,区别是前者在真正的训练前执行一次即可,后者在训练时调用。

他们的输入都是上一步得到的HCLG.fst的图,还有对音频文件进行特征提取(Feature extraction, MFCC, CMVN, etc)。而输出都是alignment,简单来说就是语音每帧对应的HMM state,在kaldi采用transition ID表示,因为之前说过transion ID是可以映射到唯一的HMM state.

对于align-equal-compiled,执行在真正训练之前,因为开始我们输入只有一个图,要得到alignment,需要对图进行viterbi 解码,找到最优路径,根据前面提到的论文中的WFST,输入即为alignment。 但是这里由于我们刚刚初始化,图里面各个转移概率都是初始化的值,没有进行更新,解码没有任何意义,因此我们直接随机产生一条路径,该路径符合以下要求:路径sequence的长度等于语音帧数,为此可能需要一定数目的self-loop作为补充,这样才能使NumOfFrames = non-selfloop’s ilabels + self-loops’s iabels。 该过程均匀对齐的具体代码见函数EqualAlign()。 最后,我们取该路径的所有ilabels作为Alignment输出。

对于gmm-align-compiled, 没有太大差别,但是这里我们使用WFST图中各个转移概率进行解码(采用faster-decoder)得到最佳路径而不是随机生成的路径。这是因为gmm-align-compiled 在训练中执行,而训练中我们不断更新WFST中各个概率参数,使得解码更加准确。 具体函数为AlignUtteranceWrapper()最后,我们取最佳路径的所有ilabels作为Alignment输出。

 

4. EM Algorithm for GMM training

接下来会讲解训练单音素的最关键部分,kaldi对transition 概率采用Viterbi training,而GMM 模型训练采用EM算法。所以这里先介绍下EM算法在GMM训练中如何使用。

大名鼎鼎的EM算法分为两部,E步和M步。也就是Expectation 和 Maximization。E步中由观测结果计算参数的估计值,M步中重新估计参数,使得Q函数最大。 Q函数为完全数据的对数似然函数的期望,也就是说M步重新估计参数,使得观测数据和未观测数据即完全数据的期望最大,在E步的基础上,我们预估了一个参数,因此得到未观测数据的分布,依此我们重新计算参数,如此迭代直到收敛。GMM的训练参数更新如下:

 

kaldi就是用以上方法对GMM进行更新,E步稍有不同,接下来马上会提到。

 

5. gmm-acc-stats-ali

Usage:  gmm-acc-stats-ali [options] <model-in> <feature-rspecifier> <alignments-rspecifier> <stats-out>

输入:模型model,特征,对齐序列alignment
输出:用于训练的统计量

首先读取transition模型 和 am-gmm模型信息。对于每个音频文件和对应的对齐序列,我们进行如下操作:遍历每一帧,因为每一帧都可以从对齐序列里找到所对应的transition-id, 用一个vector记录transitio-id出现的次数,之后用于更新转移概率。同时由transition-id找到所对应的pdf-id,建立一个vector,大小为pdf-id的数目,函数ComponentPosteriors() 计算之前图片中EM算法中E步的响应度,kaldi里调用函数LogLikelihoods()计算数据data也就是每帧语音特征在GMM中每个单高斯出现的概率,存在loglikes里面

  Vector<BaseFloat> loglikes;

  LogLikelihoods(data, &loglikes);

  BaseFloat log_sum = loglikes.ApplySoftMax();

和之前图片里E步公式稍有不同,这里不是简单取平均值,而是用SoftMax计算在这个GMM模型中,数据data分别对应每个单高斯的比例。这里也就是EM算法中的E步。

接下来还调用了函数AccumulateFromPosteriors() 这里对M步进行了一个预计算,也就是计算了上述图片中M步三个公式的分子部分,在代码里,三个分子的数据存在occupancy_mean_accumulator_covariance_accumulator_三个vector中。

上述所有计算后的统计量都放在AccumAmDiagGmm gmm_accs;中,作为*.acc 输出。

 

6. gmm-est

Usage:  gmm-est [options] <model-in> <stats-in> <model-out>

这是训练的最后一步,输入是gmm-init-mono初始化的模型或者训练时上一步训练后的模型,和gmm-acc-stat-ali计算的统计量,输出也是模型。

这步分为两个部分讲解,一个时EM算法的M步,另一个时Split,因为我们初始化中GMM只有一个单高斯,在训练的时候我们会split出多个,最后使得每个GMM模型中高斯数目等于我们想要的。

M之前还要更新模型的转移概率,还记得我们在gmm-acc-stat-ali统计好了每个transition-id出现的次数吗,我们就用它来更新,很简单,我们直接用出现的次数除以总数就得到了转移概率,具体代码见函数:

void TransitionModel::MleUpdate(const Vector<double> &stats,const MleTransition-

UpdateConfig &cfg,BaseFloat *objf_impr_out,BaseFloat *count_out)

正式进入M步。用刚才计算好的的分子除以相关度之和,也就是用每帧数据的在每个单高斯上的概率相加。具体见函数:MleDiagGmmUpdate()。 这样就完成了EM算法。

接下来讲kaldi如何在每次split出新的高斯模型,达到我们想要的gauss数目。具体看函数:

void AmDiagGmm::SplitByCount(const Vector<BaseFloat> &state_occs,int32 target_components,float perturb_factor, BaseFloat power,BaseFloat min_count);

 

该函数首先用GetSplitTargets获取了每个GMM在该轮训练结束的时候需要达到的gauss数目,具体算法如下:

struct CountStats {

  CountStats(int32 p, int32 n, BaseFloat occ)

      : pdf_index(p), num_components(n), occupancy(occ) {}

  int32 pdf_index;

  int32 num_components;

  BaseFloat occupancy;

  bool operator < (const CountStats &other) const {

    return occupancy/(num_components+1.0e-10) <

        other.occupancy/(other.num_components+1.0e-10);

  }

};

kaldi为每个GMM构建以上结构体,pdf_index也就是pdf-id, num_components就是该pdf中的gauss数目,occupancy就是在该轮训练M步结束每个GMMmodel所有gauss的weight之和,再用如下公式

BaseFloat occ = pow(state_occs(pdf_index), power);

重新计算作为该结构体的occupancy。 kaldi用一个priority_queue保存了很多个这样的结构体,occupancy/NumofGauss越大越在queue前面。 在GetSplitTarget函数中,首先构建所有的结构体放入queue中,然后进行一个循环,每次循环对列表最前面的结构体中的num_components加1(num_components之后表示这个GMM的在该轮训练结束时要达到的目标高斯数目),由于加1后priority减小,可能该结构会放到queue中其他位置,下次循环对其他的GMM的gauss数加一,如此循环,直到所有GMM中的gauss数目总数达到最终的要求。

这个算法基本思想就是优先给权重最多的GMM增加gauss数,使得所有GMM尽可能得到平均

好了,确定了每个GMM在该轮训练要增加的Gauss数目了,就要用split进行分裂了。 kaldi分裂算法如下, 具体可看函数:

void FullGmm::Split(int32 target_components, float perturb_factor,vector<int32> *history)

我做个简单介绍,我们在之前获取了每个GMM需要增加到的目标gauus数,也就是target_components ,也知道当前GMM的gauss数current_components. 我们同样进行一个循环,每个iteration里,我们先得到所占weigh最大的那个gauss,然后将那个gauss的weight变为原来的一半,增加新的gauss,新gauss的均值方差为之前加上一个perturb_factor随机扰动, weight为原gauss的一半。不断迭代直到 current_components == target_components,完成一轮训练,输出新的model。

 

7.Summary

训练开始调用gmm-init-mono得到最初的模型和决策树,由于是对单音素的训练决策树并没有发挥真正的作用,这里的模型中均值方差都是初始化值,并没有经过训练,不能用于解码,因此,在用compile-train-graphs得到每个语音的HCLG.fst后,我们训练前只能随机产生一条符合规定条件的路径进行对齐,对齐(align-equal-compiled)的目的是找到该语音每帧对应的transition-id。

之后用对齐的transition-id和提取的特征统计信息量(gmm-acc-stats-ali),该步完成了EM算法里的E部,也计算好了M步中所需的一些数据,用于之后训练。训练时用gmm-align-compiled进行对齐,这里使用faster-decoder解码得到最佳对齐路径。

用gmm-est进行模型更新,gmm-est主要是用Viterbi training,统计transition出现的次数除以总数目更新转移概率,用EM算法完成对GMM中参数的更新,结束之前用设置power和perturb_factor进行分裂,到达指定的gauss数目。如此循环迭代,完成对mono-phone的训练。

猜你喜欢

转载自blog.csdn.net/qq_18124075/article/details/82999365
.sh
SH