Kaldi单音素模型 训练部分
在Kaldi中,单音素GMM的训练用的是Viterbi training,而不是Baum-Welch training。因此就不是用HMM Baum-Welch那几个公式去更新参数,也就不用计算前向概率、后向概率了。Kaldi中用的是EM算法用于GMM时的那三个参数更新公式,并且稍有改变。
Baum-Welch算法更新参数时,因为要计算前向后向概率,很费时间,因此使用Viterbi Training作为Baum-Welch算法的近似。在Baum-Welch算法中,计算前向后向概率时,要用到所有的状态路径,在Viterbi训练中,用Viterbi路径代替对所有状态路径的累积。
在Viterbi训练中,先根据上一轮的模型参数对语音特征数据进行对齐,得到每一帧的特征所对应的HMM状态(在kaldi中是transition-id),也就是forced alignment。Forced alignment的结果是对应于特征序列的状态序列。
举个例子:
当前的特征序列是o1, o2, o3, o4, o5, o6, o7.(每一帧的特征是39维MFCC)
对应的状态序列是7, 8, 8, 8, 9, 9, 10.(每个数字代表一个HMM state)
知道了特征序列和其对应的状态序列,我们就可以通过简单的数数来更新HMM的参数——转移概率矩阵A。根据对齐结果,统计每一个HMM状态总共出现了多少次(可以从transition-id得到HMM state-id),统计该状态的每一个转移出现了多少次(一般只有两个转移,转移到自身和转移到下一状态),用每一个转移的出现次数除以该状态的出现次数就得到了转移概率。HMM参数就是这样更新的。
首先应该明白,在单音素GMM训练中,每一个HMM状态有一个对应的GMM概率密度函数(pdf),所以有多少个HMM状态,就有多少个GMM,也就有多少组GMM参数。在知道了特征序列和对齐序列后,找出某一个HMM状态对应的所有观测(比如状态8对应的o2, o3, o4,在kaldi中则是找到某一transition-id对应的所有观测),也就得到了该状态对应的GMM所对应的所有观测。知道了该GMM对应的所有观测、该GMM的当前参数,就可以根据GM参数更新公式更新GMM参数了。
具体代码如下:
Usage: steps/train_mono.sh [options] <data-dir> <lang-dir>
e.g.: steps/train_mono.sh data/train.1k data/lang exp/mono<exp-dir>
- 初始化单音素模型。调用gmm-init-mono,生成0.mdl、tree。
- 编译训练时的图。调用compile-train-graph生成text中每句抄本对应的fst,存放在fsts.JOB.gz中。
- 第一次对齐数据。调用align-equal-stats-ali生成对齐状态序列,通过管道传递给gmm-acc-stats-ali,得到更新参数时用到的统计量。
- 第一次更新模型参数。调用gmm-est更新模型参数。
- 进入训练模型的主循环:在指定的对齐轮数,使用gmm-align-compiled对齐特征数据,得到新的对齐状态序列;每一轮都调用gmm-acc-stats-ali计算更新模型参数所用到的统计量,然后调用gmm-est更新模型参数,并且在每一轮中增加GMM的分量个数。
gmm-init-mono
作用:初始化单音素GMM
gmm-init-mono topo 39 mono.mdl mono.tree
- 计算所有特征数据每一维特征的全局均值、方差
- 读取topo文件,创建共享音素列表(根据$lang/phones/sets.int),根据共享音素列表创建ctx_dep(相当于tree)
- 每一组共享音素的一个状态对应一个Pdf。对每一个状态,创建只有一个分量的GMM,该GMM的均值初始化为全局均值、方差初始化为全局方差。(实际上,此时表示GMM的类是DiagGmm,该对象根据多维高斯分布的公式和对角协方差矩阵的特殊性,为了方便计算,直接保存的参数并不是均值、方差,而是方差的逆(实际就是方差矩阵每个元素求倒数)、均值×方差的逆,还提前计算并保存了公式中的常数部分(.mdl文件GMM部分的
<GCONSTS>
) - 根据ctx_dep和topo创建转移模型。将转移模型、GMM声学模型写到0.mdl
- 将ctx_dep写到tree.
compile-train-graphs
Usage: compile-train-graphs [options] <tree-in> <model-in> <lexicon-fst-in> <transcriptions-rspecifier> <graphs-wspecifier>
e.g.: compile-train-graphs tree 1.mdl lex.fst ark:train.tra ark:graphs.fsts
生成与音频特征对齐的HMM状态序列时要用到每句话的FST。
# 构造训练的网络,从源码级别分析,是每个句子构造一个phone level 的fst网络。 # $sdaba/JOB/text 中包含对每个句子的单词(words level)级别标注, L.fst是字典对于的fst表示,作用是将一串的音素(phones)转换成单词(words) # 构造monophone解码图就是先将text中的每个句子,生成一个fst(类似于语言模型中的G.fst,只是相对比较简单,只有一个句子),然后和L.fst 进行composition 形成训练用的音素级别(phone level)fst网络(类似于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进行计数,就可以得到转移概率。
align-equal-compiled
Usage: align-equal-compiled <graphs-rspecifier> <features-rspecifier> <alignments-wspecifier>
e.g.: align-equal-compiled 1.fsts scp:train.scp ark:equal.ali
# 训练时需要将标注跟每一帧特征进行对齐,由于现在还没有可以用于对齐的模型,所以采用最简单的方法 -- 均匀对齐 # 根据标注数目对特征序列进行等间隔切分,例如一个具有5个标注的长度为100帧的特征序列,则认为1-20帧属于第1个标注,21-40属于第2个... # 这种划分方法虽然会有误差,但待会在训练模型的过程中会不断地重新对齐。
gmm-acc-stats-ali
作用:Accumulate stats for GMM training.
Usage: gmm-acc-stats-ali [options] <model-in> <feature-rspecifier> <alignments-rspecifier> <stats-out> e.g.: gmm-acc-stats-ali 1.mdl scp:train.scp ark:1.ali 1.acc;
对于每一帧的特征和其对齐(transition-id):
对于转移模型(TM),累积tid出现的次数;
对于AM,由tid得到pdf-id,也就是找到对应该pdf-id的DiagGmm对象,更新与该DiagGmm对象相关的AccumDiagGmm的参数,也就是计算得到三个GMM参数更新公式的分子部分(包括每一混合分量的后验(occupancy_中保存∑nj=1γ̂ jk)、每一分量的后验乘以当前帧的特征(mean_accumulator_中保存∑nj=1γ̂ jkyj,MxD维)、每一分量的后验乘以 当前帧的特征每一维的平方(variance_accumulator_中保存∑nj=1γ̂ jky2j,MxD维))
处理完所有数据后,将TM和AM的累积量写到一个文件中:x.JOB.acc中
gmm-sum-accs
gmm-acc-stats-ali生成的累计量分散在JOB个文件中,该程序将分散的对应同一trans-id、pdf-id的累计量合并在一起。
gmm-est
作用:Do Maximum Likelihood re-estimation of GMM-based acoustic model.
主要分两部分,一部分更新TransitionModel,一部分更新GMM。 更新转移模型:根据gmm-acc-stats-ali统计的tid出现的次数,做一个除法就可以更新转移概率矩阵A。 更新GMM:gmm-acc-stats-ali已经得到了三个GMM参数更新公式的分子部分,方差累积量只需要减去更新后的均值的平方即可得到正确的方差更新公式。
分母部分也已几乎得到,做一个简单的除法就可以更新GMM的分量概率、均值、方差。
调用MleAmDiagGmmUpdate()更新GMM的参数,然后在该函数里调用MleDiaGmmUpdate()更新每一个GMM的参数,然后在后一个函数里更新每一个分量的参数。
摘要:对语音数据进行分帧和提取特征以后,语音标注是对一整段话进行标注而没有具体到某一帧,但训练系统需要有每一帧语音的具体对应标注。本文介绍了kaldi训练monophone脚本的过程,脚本中每个程序的作用以及相关参数配置对训练结果的影响。
#!/bin/bash # Copyright 2012 Johns Hopkins University (Author: Daniel Povey) # Apache 2.0 # To be run from .. # Flat start and monophone training, with delta-delta features. # This script applies cepstral mean normalization (per speaker). # Begin configuration section. nj=4 cmd=run.pl scale_opts="--transition-scale=1.0 --acoustic-scale=0.1 --self-loop-scale=0.1" num_iters=40 # Number of iterations of training 训练迭代次数 max_iter_inc=30 # Last iter to increase #Gauss on. 高斯数递增的最大次数 totgauss=1000 # Target #Gaussians. careful=false boost_silence=1.0 # Factor by which to boost silence likelihoods in alignment 强制改变某些音素的似然概率因子,见下面的代码 realign_iters="1 2 3 4 5 6 7 8 9 10 12 14 16 18 20 23 26 29 32 35 38"; config= # name of config file. stage=-4 power=0.25 # exponent to determine number of gaussians from occurrence counts norm_vars=false # deprecated, prefer --cmvn-opts "--norm-vars=false" cmvn_opts= # can be used to add extra options to cmvn. cmvn选项 # End configuration section. echo "$0 $@" # Print the command line for logging if [ -f path.sh ]; then . ./path.sh; fi . parse_options.sh || exit 1; if [ $# != 3 ]; then echo "Usage: steps/train_mono.sh [options] <data-dir> <lang-dir> <exp-dir>" echo " e.g.: steps/train_mono.sh data/train.1k data/lang exp/mono" echo "main options (for others, see top of script file)" echo " --config <config-file> # config containing options" echo " --nj <nj> # number of parallel jobs" echo " --cmd (utils/run.pl|utils/queue.pl <queue opts>) # how to run jobs." exit 1; fi data=$1 lang=$2 dir=$3 oov_sym=`cat $lang/oov.int` || exit 1;#静音符号表 # 按照任务数,将训练数据分成多份,每个任务处理一份数据。 mkdir -p $dir/log echo $nj > $dir/num_jobs sdata=$data/split$nj; [[ -d $sdata && $data/feats.scp -ot $sdata ]] || split_data.sh $data $nj || exit 1; # 特征归一化选项,这里默认指定要对variance进行归一化,还可从外部接收其他归一化选项,如果外部指定不对variance进行归一化,则外部指定生效。 $norm_vars && cmvn_opts="--norm-vars=true $cmvn_opts" echo $cmvn_opts > $dir/cmvn_opts # keep track of options to CMVN. feats="ark,s,cs:apply-cmvn $cmvn_opts --utt2spk=ark:$sdata/JOB/utt2spk scp:$sdata/JOB/cmvn.scp scp:$sdata/JOB/feats.scp ark:- | add-deltas ark:- ark:- |" example_feats="`echo $feats | sed s/JOB/1/g`"; echo "$0: Initializing monophone system." [ ! -f $lang/phones/sets.int ] && exit 1; shared_phones_opt="--shared-phones=$lang/phones/sets.int" if [ $stage -le -3 ]; then # Note: JOB=1 just uses the 1st part of the features-- we only need a subset anyway. # 获取特征的维度 if ! feat_dim=`feat-to-dim "$example_feats" - 2>/dev/null` || [ -z $feat_dim ]; then feat-to-dim "$example_feats" - echo "error getting feature dimension" exit 1; fi # Flat-start(又称为快速启动),作用是利用少量的数据快速得到一个初始化的 HMM-GMM 模型和决策树 # $lang/topo 中定义了每个音素(phone)所对应的 HMM 模型状态数以及初始时的转移概率 # --shared-phones=$lang/phones/sets.int 选项指向的文件,即$lang/phones/sets.int(该文件生成roots.txt中开头为share split的部分,表示同一行元素共享pdf,允许进行决策树分裂),文件中同一行的音素(phone)共享 GMM 概率分布。tree文件由sets.int产生。 # --train-feats=$feats subset-feats --n=10 ark:- ark:-| 选项指定用来初始化训练用的特征,一般采用少量数据,程序内部会计算这批数据的means和variance,作为初始高斯模型。sets.int中所有行的初始pdf都用这个计算出来的means和variance进行初始化。 $cmd JOB=1 $dir/log/init.log \ gmm-init-mono $shared_phones_opt "--train-feats=$feats subset-feats --n=10 ark:- ark:-|" $lang/topo $feat_dim \ $dir/0.mdl $dir/tree || exit 1; fi # 计算当前高斯数,(目标高斯数 - 当前高斯数)/ 增加高斯迭代次数 得到每次迭代需要增加的高斯数目 numgauss=`gmm-info --print-args=false $dir/0.mdl | grep gaussians | awk '{print $NF}'` incgauss=$[($totgauss-$numgauss)/$max_iter_inc] # per-iter increment for #Gauss # 构造训练的网络,从源码级别分析,是每个句子构造一个phone level 的fst网络。 # $sdaba/JOB/text 中包含对每个句子的单词(words level)级别标注, L.fst是字典对于的fst表示,作用是将一串的音素(phones)转换成单词(words) # 构造monophone解码图就是先将text中的每个句子,生成一个fst(类似于语言模型中的G.fst,只是相对比较简单,只有一个句子),然后和L.fst 进行composition 形成训练用的音素级别(phone level)fst网络(类似于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进行计数,就可以得到转移概率。 if [ $stage -le -2 ]; then echo "$0: Compiling training graphs" $cmd JOB=1:$nj $dir/log/compile_graphs.JOB.log \ compile-train-graphs $dir/tree $dir/0.mdl $lang/L.fst \ "ark:sym2int.pl --map-oov $oov_sym -f 2- $lang/words.txt < $sdata/JOB/text|" \ "ark:|gzip -c >$dir/fsts.JOB.gz" || exit 1; fi if [ $stage -le -1 ]; then echo "$0: Aligning data equally (pass 0)" $cmd JOB=1:$nj $dir/log/align.0.JOB.log \ # 训练时需要将标注跟每一帧特征进行对齐,由于现在还没有可以用于对齐的模型,所以采用最简单的方法 -- 均匀对齐 # 根据标注数目对特征序列进行等间隔切分,例如一个具有5个标注的长度为100帧的特征序列,则认为1-20帧属于第1个标注,21-40属于第2个... # 这种划分方法虽然会有误差,但待会在训练模型的过程中会不断地重新对齐。 align-equal-compiled "ark:gunzip -c $dir/fsts.JOB.gz|" "$feats" ark,t:- \| \ # 对对齐后的数据进行训练,获得中间统计量,每个任务输出到一个acc文件。 # acc中记录跟HMM 和GMM 训练相关的统计量: # HMM 相关的统计量:两个音素之间互联的边(Arc) 出现的次数。 # 如上面所述,fst.JOB.gz 中每个key对于的value保存一个句子中音素两两之间互联的边。 # gmm-acc-stats-ali 会统计每条边(例如a->b)出现的次数,然后记录到acc文件中。 # GMM 相关的统计量:每个pdf-id 对应的特征累计值和特征平方累计值。 # 对于每一帧,都会有个对齐后的标注,gmm-acc-stats-ali 可以根据标注检索得到pdf-id, # 每个pdf-id 对应的GMM可能由多个单高斯Component组成,会先计算在每个单高斯Component对应的分布下这一帧特征的似然概率(log-likes),称为posterior。 # 然后: # (1)把每个单高斯Component的posterior加到每个高斯Component的occupancy(占有率)计数器上,用于表征特征对于高斯的贡献度, # 如果特征一直落在某个高斯的分布区间内,那对应的这个值就比较大;相反,如果一直落在区间外,则表示该高斯作用不大。 # gmm-est中可以设置一个阈值,如果某个高斯的这个值低于阈值,则不更新其对应的高斯。 # 另外这个值(向量)其实跟后面GMM更新时候的高斯权重weight的计算相关。 # (2)把这一帧数据加上每个单高斯Component的posterior再加到每个高斯的均值累计值上; # 这个值(向量)跟后面GMM的均值更新相关。 # (3)把这一帧数据的平方值加上posterior再加到每个单高斯Component的平方累计值上; # 这个值(向量)跟后面GMM的方差更新相关。 # 最后将均值累计值和平方累计值写入到文件中。 gmm-acc-stats-ali --binary=true $dir/0.mdl "$feats" ark:- \ $dir/0.JOB.acc || exit 1; fi # In the following steps, the --min-gaussian-occupancy=3 option is important, otherwise # we fail to est "rare" phones and later on, they never align properly. # 根据上面得到的统计量,更新每个GMM模型,AccumDiagGmm中occupancy_的值决定混合高斯模型中每个单高斯Component的weight; # --min-gaussian-occupancy 的作用是设置occupancy_的阈值,如果某个单高斯Component的occupancy_低于这个阈值,那么就不会更新这个高斯, # 而且如果 --remove-low-count-gaussians=true,则对应得单高斯Component会被移除。 if [ $stage -le 0 ]; then gmm-est --min-gaussian-occupancy=3 --mix-up=$numgauss --power=$power \ $dir/0.mdl "gmm-sum-accs - $dir/0.*.acc|" $dir/1.mdl 2> $dir/log/update.0.log || exit 1; rm $dir/0.*.acc fi beam=6 # will change to 10 below after 1st pass # note: using slightly wider beams for WSJ vs. RM. x=1 while [ $x -lt $num_iters ]; do echo "$0: Pass $x" if [ $stage -le $x ]; then if echo $realign_iters | grep -w $x >/dev/null; then echo "$0: Aligning data" # gmm-boost-silence 的作用是让某些phones(由第一个参数指定)对应pdf的weight乘以--boost 参数所指定的数字,强行提高(如果大于1)/降低(如果小于1)这个phone的概率。 # 如果多个phone共享同一个pdf,程序中会自动做去重,乘法操作只会执行一次。 mdl="gmm-boost-silence --boost=$boost_silence `cat $lang/phones/optional_silence.csl` $dir/$x.mdl - |" # 执行force-alignment操作。 # --self-loop-scale 和 --transition-scale 选项跟HMM 状态跳转相关,前者是设置自转因子,后者是非自传因子,可以修改这两个选项控制HMM的跳转倾向。 # --acoustic-scale 选项跟GMM输出概率相关,用于平衡 GMM 输出概率和 HMM 跳转概率的重要性。 # --beam 选项用于计算对解码过程中出现较低log-likelihood的token进行裁剪的阈值,该值设计的越小,大部分token会被裁剪以便提高解码速度,但可能会在开始阶段把正确的token裁剪掉导致无法得到正确的解码路径。 # --retry-beam 选项用于修正上述的问题,当无法得到正确的解码路径后,会增加beam的值,如果找到了最佳解码路径则退出,否则一直增加指定该选项设置的值,如果还没找到,就抛出警告,导致这种问题要么是标注本来就不对,或者retry-beam也设计得太小。 $cmd JOB=1:$nj $dir/log/align.$x.JOB.log \ gmm-align-compiled $scale_opts --beam=$beam --retry-beam=$[$beam*4] --careful=$careful "$mdl" \ "ark:gunzip -c $dir/fsts.JOB.gz|" "$feats" "ark,t:|gzip -c >$dir/ali.JOB.gz" \ || exit 1; fi # 更新模型 $cmd JOB=1:$nj $dir/log/acc.$x.JOB.log \ gmm-acc-stats-ali $dir/$x.mdl "$feats" "ark:gunzip -c $dir/ali.JOB.gz|" \ $dir/$x.JOB.acc || exit 1; $cmd $dir/log/update.$x.log \ gmm-est --write-occs=$dir/$[$x+1].occs --mix-up=$numgauss --power=$power $dir/$x.mdl \ "gmm-sum-accs - $dir/$x.*.acc|" $dir/$[$x+1].mdl || exit 1; rm $dir/$x.mdl $dir/$x.*.acc $dir/$x.occs 2>/dev/null fi # 线性增加混合高斯模型的数目,直到指定数量。 if [ $x -le $max_iter_inc ]; then numgauss=$[$numgauss+$incgauss]; fi # 提高裁剪门限。 beam=10 x=$[$x+1] done ( cd $dir; rm final.{mdl,occs} 2>/dev/null; ln -s $x.mdl final.mdl; ln -s $x.occs final.occs ) utils/summarize_warnings.pl $dir/log echo Done # example of showing the alignments: # show-alignments data/lang/phones.txt $dir/30.mdl "ark:gunzip -c $dir/ali.0.gz|" | head -4
参考文章:
http://blog.csdn.net/u010731824/article/details/69668765
http://blog.csdn.net/duishengchen/article/details/52575926