在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

