1. 程式人生 > >Kaldi thchs30手札(三)單音素模型訓練(line 62-68)

Kaldi thchs30手札(三)單音素模型訓練(line 62-68)

本部分是對Kaldi thchs30 中run.sh的程式碼的line 62-68行研究和知識總結,內容為單音素模型的訓練與解碼。

概覽

先把程式碼放在這裡:

#monophone   
steps/train_mono.sh --boost-silence 1.25 --nj $n --cmd "$train_cmd" data/mfcc/train data/lang exp/mono || exit 1;  
#test monophone model
local/thchs-30_decode.sh --mono true --nj $n "steps/decode.sh" exp
/mono data/mfcc & #monophone_ali steps/align_si.sh --boost-silence 1.25 --nj $n --cmd "$train_cmd" data/mfcc/train data/lang exp/mono exp/mono_ali || exit 1;

可以看到程式碼只有三行,其中:

  1. 第一行steps/train_mono.sh 用來訓練單音素模型,主要輸出為final.mdl和tree。訓練的核心流程就是迭代對齊-統計算GMM與HMM資訊-更新引數。

  2. 第二行local/thchs-30_decode.sh是解碼和測試部分,它採用剛剛訓練得到的模型來對測試資料集進行解碼並計算準確率等資訊。

  3. 第三行steps/align_si.sh 使用src-dir中的模型對data-dir中的資料進行對齊,將結果放在align-dir中。

下面對這三行對應的程式進行詳細說明。

單音素模型訓練 steps/train_mono.sh

先說一下比較重要的幾個引數吧:

num_iters=40 | 迭代次數
max_iter_inc=30 | 指在迭代30次之後就不增加高斯數目了
totgauss=1000 | 目標高斯數
boost_silence=1.0 | 指對sil的likelihoods進行放大
realign_iters | 指迭代到哪次時進行re_ali
norm_vars=false | 不推薦,推薦–cmvn-opts “–norm-vars=false”

steps/train_mono.sh 的使用: “steps/train_mono.sh [options] ”,其中data-dir是訓練資料所在的目錄,lang-dir是語言模型所在的目錄,exp-dir是日誌檔案和最終目標檔案的輸出目錄.下面對重要/難理解的程式碼行進行說明:

line 57: 程式碼為:

    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:- |"

這裡因為Kaldi有自己的輸入輸出命令,因此較難理解,可以先看一下參考中的Kaldi中的I/O機制那裡。首先這是一個feats變數的定義,該變數作為後續其他命令的引數用來處理特徵資料。ark開頭指定是ark型別,s,cs為其引數。後面在雙引號中使用了兩個Kaldi自帶的函式apply-cmvn和add-deltas.其中apply-cmvn 的輸入3個檔案:

–utt2spk=ark:sdata/JOB/utt2spk語料和錄音人員關聯檔案,scp:sdata/JOB/cmvn.scp 說話人相關的均值和方差,scp:$sdata/JOB/feats.scp 訓練用特徵檔案.apply-cmvn是對feat.sco做CMVN….add-deltas的輸入是ark:-,即管道符前的輸出。它的輸出也表示成ark:-,利用管道符傳遞結果。其功能是為訓練資料增加差分量,比如13維的MFCC處理後變成39維(這裡不太確定,但結果確實是39維)。

line 65-75:其程式碼為

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                           
  $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 

feat-to-dim “$example_feats” - 2>/dev/null 可以獲取特徵的維度(在這裡的輸出是39)。

gmm-init-mono 是通過少量的資料快速得到一個初始化的HMM-GMM 模型和決策樹,屬於Flat-start(又稱為快速啟動)。lang/topo 中定義了每個音素(phone)所對應的HMM 模型狀態數以及初始時的轉移概率。sharedphones=$lang/phones/sets.int 選項指向的檔案,即lang/phones/sets.int(該檔案生成roots.txt中開頭為share split的部分,表示同一行元素共享pdf,允許進行決策樹分裂),檔案中同一行的音素(phone)共享 GMM 概率分佈。tree檔案由sets.int產生。
trainfeats=feats subset-feats –n=10 ark:- ark:-|$ 選項指定用來初始化訓練用的特徵,一般採用少量資料,程式內部會計算這批資料的means和variance,作為初始高斯模型。sets.int中所有行的初始pdf都用這個計算出來的means和variance進行初始化。numgauss那通過gmm-info命令即sed獲取當前高斯數,而後在incgauss計算(目標高斯數 - 當前高斯數)/ 增加高斯迭代次數,得到每次迭代需要增加的高斯數目.

line 80-86:

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 

構造訓練的網路,從原始碼級別分析,是每個句子構造一個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進行計數,就可以得到轉移概率。

line 89-94:

if [ $stage -le -1 ]; then
  echo "$0: Aligning data equally (pass 0)"
  $cmd JOB=1:$nj $dir/log/align.0.JOB.log \                                                      
    align-equal-compiled "ark:gunzip -c $dir/fsts.JOB.gz|" "$feats" ark,t:-  \| \
    gmm-acc-stats-ali --binary=true $dir/0.mdl "$feats" ark:- \
    $dir/0.JOB.acc || exit 1;
fi 

align-equal-compiled 在本步驟先對訓練資料進行初始對齊,對齊即將每一幀觀察量與標註對齊。初始時採用均勻對齊,即根據標註量和觀察量來進行均勻對齊,比如有100幀序列,5個標註,那麼就是每個標註20幀,雖然這種初始化的對齊方式誤差會很大,但在接下來的訓練步驟中會不斷的重新對齊的。

gmm-acc-stats-ali 是對對齊後的資料進行訓練,獲得中間統計量,每個任務輸出到一個.acc的檔案中。acc檔案中記錄了與HMM和GMM相關的統計量:

  1. HMM相關的統計量:兩個音素之間互聯的邊(Arc) 出現的次數。如上面所述,fst.JOB.gz 中每個key對於的value儲存一個句子中音素兩兩之間互聯的邊。gmm-acc-stats-ali 會統計每條邊(例如a->b)出現的次數,然後記錄到acc檔案中。

  2. GMM相關的統計量:每個pdf-id 對應的特徵累計值和特徵平方累計值。對於每一幀,都會有個對齊後的標註,gmm-acc-stats-ali 可以根據標註檢索得到pdf-id,每個pdf-id 對應的GMM可能由多個單高斯Component組成,會先計算在每個單高斯Component對應的分佈下這一幀特徵的似然概率(log-likes),稱為posterior。然後:

    • 把每個單高斯Component的posterior加到每個高斯Component的occupancy(佔有率)計數器上,用於表徵特徵對於高斯的貢獻度,如果特徵一直落在某個高斯的分佈區間內,那對應的這個值就比較大;相反,如果一直落在區間外,則表示該高斯作用不大。gmm-est中可以設定一個閾值,如果某個高斯的這個值低於閾值,則不更新其對應的高斯。另外這個值(向量)其實跟後面GMM更新時候的高斯權重weight的計算相關。
    • 把這一幀資料加上每個單高斯Component的posterior再加到每個高斯的均值累計值上;這個值(向量)跟後面GMM的均值更新相關。
    • 把這一幀資料的平方值加上posterior再加到每個單高斯Component的平方累計值上;這個值(向量)跟後面GMM的方差更新相關。
    • 最後將均值累計值和平方累計值寫入到檔案中。

line 99-103:

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

gmm-est這裡用上面得到的統計量來更新每個GMM模型,AccumDiagGmm中occupancy_的值決定混合高斯模型中每個單高斯Component的weight;min-gaussian-occupancy 的作用是設定occupancy_的閾值,如果某個單高斯Component的occupancy_低於這個閾值,那麼就不會更新這個高斯。而且如果remove-low-count-gaussians=true,則對應得單高斯Component會被移除。

line 106-134:

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"
      mdl="gmm-boost-silence --boost=$boost_silence `cat $lang/phones/optional_silence.csl` $dir/$x.mdl - |"
      $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  

從程式碼可以看出,這裡就是上面三步(對齊-計算統計量-更新引數)的訓練版。大體含以上無明顯改變。

其中需要注意的是對齊部分,這裡採用的不再是均勻對齊而是gmm-align-compiled,強制對齊。選項用於計算對解碼過程中出現較低log-likelihood的token進行裁剪的閾值,該值設計的越小,大部分token會被裁剪以便提高解碼速度.-acoustic-scale 選項跟GMM輸出概率相關,用於平衡 GMM 輸出概率和 HMM 跳轉概率的重要性。–beam 選項用於計算對解碼過程中出現較低log-likelihood的token進行裁剪的閾值,該值設計的越小,大部分token會被裁剪以便提高解碼速度,但可能會在開始階段把正確的token裁剪掉導致無法得到正確的解碼路徑。-retry-beam 選項用於修正上述的問題,當無法得到正確的解碼路徑後,會增加beam的值,如果找到了最佳解碼路徑則退出,否則一直增加指定該選項設定的值,如果還沒找到,就丟擲警告,導致這種問題要麼是標註本來就不對,或者retry-beam也設計得太小。

以上就是train_mono.sh的程式碼和講解了。

解碼測試 local/thchs-30_decode.sh

本部分即對應run.sh裡面的第65行。其實local/thchs-30_decode.sh這個程式碼在run.sh裡多次出現,都是負責根據得到的模型對語音進行解碼得到漢字計算準確率/似然度等資訊。開啟程式發現其核心程式碼就兩行,下面的decode phone 和word的程式碼除了出入不一樣外都一樣,而phone對應的輸入我們前面介紹過,因此:

#decode word 
utils/mkgraph.sh $opt data/graph/lang $srcdir $srcdir/graph_word  || exit 1;
$decoder --cmd "$decode_cmd" --nj $nj $srcdir/graph_word $datadir/test $srcdir/decode_test_word || exit 1

其中mkgraph.sh的作用是建立一個完全擴充套件的解碼圖(HCLG.FST),該解碼圖表示語言模型、發音字典、上下文相關性和HMM結構。其程式流程為:

  1. 由L_disambig.fst(lexicon,發音字典)和G.fst(語言模型)生成最新的$lang/tmp/LG.fst。

  2. 生成最新的$lang/tmp/CLG_1_0.fst 和 ilabels_1_0 和 disambig_ilabels_1_0.int,需要LG.fst和disambig.int。

  3. 生成最新的exp/mono/graph/Ha.fst,需要檔案treemodel.

  4. 生成最新的exp/mono/graph/HCLGa.fst。

  5. 生成最新的exp/mono/graph/HCLG.fst,呼叫程式add-self-loops。

  6. 檢查HCLG.fst是否為空。

  7. 將$lang下的一些檔案複製到exp/mono/graph下。

第二行的$decoder是傳入的引數,等價於steps/decode.sh,它的使用方式是steps/decode.sh [options] 。作用為通過呼叫gmm-latgen-faster或gmm-latgen-faster-parallel進行解碼,生成lat.JOB.gz。其中gmm-latgen-faster-parallel為gmm-latgen-faster的並行版本,可以自己去程式里根據需求設定。而後若設定變數skip_scoring為false,則呼叫local/score.sh進行打分,thchs30的解碼設定的是false。

單音素強制對齊 steps/align_si.sh

改程式的用法為:steps/align_si.sh 。其作用為使用src-dir中的模型對data-dir中的資料進行對齊,將結果放在align-dir中。簡單來說就是用訓練好的模型將資料進行強制對齊方便以後使用。這也是一般Kaldi訓練都要包含單音素的原因之一。在run.sh中的呼叫程式碼為:

steps/align_si.sh --boost-silence 1.25 --nj $n --cmd "$train_cmd" data/mfcc/train data/lang exp/mono exp/mono_ali || exit 1;

它用到的檔案為 data/train/text data/lang/oov.in exp/mono/tree exp/mono/final.mdl exp/mono/final.occs,輸出是 exp/mono_ali/ali.JOB.gz,JOB對應於任務號(1,2..)。其流程為:

  1. 先呼叫compile-train-graphs對每句話生成FST。

  2. 再呼叫gmm-align-compiled對資料進行對齊,結果放在exp/mono_ali/ali.JOB.gz。

  3. 呼叫steps/diagnostic/analyze_alignments.sh對對齊結果進行分析(thchs30裡沒用到這個)。

雜貨筆記

在學習本講的過程中,看了一些參考資料,講自己以前在書裡沒看懂或沒接觸的地方打通了一些,一下做出整理。

Kaldi單音素訓練

看名字和上面的是重複的,但實際內容是偏理論上的整體流程。之前在看書的時候都主要在強調GMM是用來訓練聲學模型,HMM是用於解碼。其中聲學模型就是用一個混合高斯分佈來擬合一個音素。HMM呢就是通過Viterbi或B-W演算法來對狀態進行解碼,給出最可能的狀態序列。但GMM與HMM間的連線卻一直不清楚。這裡嘗試對此給出流程總結:

1) 首先我們有一系列的特徵序列,o1,o2,o3,o4,o5,o6,o7,這些特徵若為MFCC,則每一幀的維度都為39.

2) 對特徵序列根據標註進行對齊。上面我們知道在初始時採用的是均勻對齊,在這裡為了理解方便我們給出幾輪迭代後的可能對齊方式,其中上面是可觀察量,下面是HMM的狀態,因此我們就可以求出HMM的引數-轉移概率:

3) 首先應該明白,在單音素GMM訓練中,每一個HMM狀態有一個對應的GMM概率密度函式(pdf),所以有多少個HMM狀態,就有多少個GMM,也就有多少組GMM引數。在知道了特徵序列和對齊序列後,找出某一個HMM狀態對應的所有觀測(比如狀態8對應的o2, o3, o4,在kaldi中則是找到某一transition-id對應的所有觀測),也就得到了該狀態對應的GMM所對應的所有觀測。知道了該GMM對應的所有觀測、該GMM的當前引數,就可以根據GMM引數更新公式更新GMM引數了,比如知道了狀態8對應的觀測o2, o3, o4,那麼將其帶入EM更新公式中即可.

HCLG

The overall picture for decoding-graph creation is that we are constructing the graph HCLG = H o C o L o G. Here

G is an acceptor (i.e. its input and output symbols are the same) that encodes the grammar or language model.
L is the lexicon; its output symbols are words and its input symbols are phones.
C represents the context-dependency: its output symbols are phones and its input symbols represent context-dependent phones, i.e. windows of N phones; see Phonetic context windows.
H contains the HMM definitions; its output symbols represent context-dependent phones and its input symbols are transition-ids, which encode the pdf-id and other information (see Integer identifiers used by TransitionModel)

If we were to summarize our approach on one line (and one line can’t capture all the details, obviously), the line would probably as follows, where asl==”add-self-loops” and rds==”remove-disambiguation-symbols”, and H’ is H without the self-loops:

HCLG = asl(min(rds(det(H’ o min(det(C o min(det(L o G))))))))

說的很好。。。我這裡用漢語總結一下:

  1. G.fst:語言模型。

  2. L.fst:詞典,輸入是Phone,輸出是word.

  3. C.fst:表示文字依賴,它的輸出的phones,輸入是文字依賴的音素,如triphone.如: vector ctx_window = { 12, 15, 21 }; 它的含義:id = 15 的 phone 為 中心 phone, left phone id = 12, right phone id = 21。

  4. H: 包括HMM definitions,其輸出 symbol 為 context-dependency phones, 其輸入 symbol 為 transitions-ids(即 對 pdf-id 和 其它資訊編碼後的 id)。粗暴的理解為把HMM的pdf-id對映到如triphone上。即擴充套件了HMM。

  5. 合體:HCLG.fst,就是把1-4步合起來,最終該fst的輸入是pdf-id,輸出為對應的片語,用圖表示為:

參考