歡迎大家關注我的博客 pelhans.com/ ,所有文章都會第一時間發布在那裡哦~


本部分是對Kaldi thchs30 中run.sh的代碼的line 71-76 行研究和知識總結,內容為三音子模型的訓練與解碼測試

概覽

首先放代碼:

#triphone
steps/train_deltas.sh --boost-silence 1.25 --cmd "$train_cmd" 2000 10000 data/mfcc/train data/lang exp/mono_ali exp/tri1 || exit 1;

#test tri1 model
local/thchs-30_decode.sh --nj $n "steps/decode.sh" exp/tri1 data/mfcc &

#triphone_ali
steps/align_si.sh --nj $n --cmd "$train_cmd" data/mfcc/train data/lang exp/tri1 exp/tri1_ali || exit 1;

恩。。。依舊是短短的三行實際代碼:

  1. 其中第一行steps/train_deltas.sh就是三音子模型的訓練部分,三音子的訓練和單音素模型的主要區別是狀態綁定部分,也是本講的主要內容。
  2. 第二行是解碼測試部分,可以看到該代碼和單音素的解碼測試是一樣的,只是少了--mono選項,因此這裡將略過它。
  3. 第三行是利用第一行訓練得到的三因子模型來做強制對齊。代碼也是和單音素時是一樣的,只是輸入模型的變化,因此也不再贅述。

因此總結以上來看實際上我們主要關注的就是第一行的train_deltas.sh部分,我在參考中放了很多關於狀態綁定的鏈接,我在這裡只是把大體它做了什麼和原理寫出來,更深入的代碼級別解釋還請看參考。

train_deltas.sh

先看看代碼的用法:

Usage: steps/train_deltas.sh <num-leaves> <tot-gauss> <data-dir> <lang-dir> <alignment-dir> <exp-dir>

其中num-leaves是葉子節點數目,tot-gauss是總高斯數目,data-dir是數據文件夾,lang-dir是存放語言的文件夾,alignment-dir是存放之前單音素對齊後結果的文件夾,exp-dir是存放三音子模型結果的文件夾.

line 75之前都是之前介紹過的比較簡單的處理,包括設置環境、任務數,進行cmvn處理,定義feats變數等。下面講集中精力在狀態綁定的步驟上。

acc-tree-stats 與 sum-tree-stats 累積相關統計量

它的作用是為決策樹的構建累積相關的統計量。,輸入是聲學模型、特徵、對齊序列,輸出為統計量。它的執行流程為:

  1. 打開聲學模型,並從中讀取TransitionModel,打開特徵文件和對其文件。
  2. 對每一句話的特徵和對應的對齊狀態,調用程序AccumulateTreeStats()累積統計量tree_stats。
  3. 將tree_stats(累積統計量)轉移到BuildTreeStatsType類型的變數stats中,將stats寫到文件JOB.treeacc。

下面詳細寫一下這個統計量是怎麼算的。首先我們要知道三音子及HMM狀態據在Kaldi中的存儲方式:

typedef std::vector<std::pair<EventKeyType,EventValueType> > EventType;

其中EventKeyType用來表示HMM的狀態信息,長pair這樣。其中第一個數取-1來標記它是HMM的狀態信息而不是三音子的。第二個數可以取0,1,2用來表示這是該三音素是第幾個HMM的狀態.後面的EventValueType就用來表示三音子三個位置上的音素是什麼,如(0, 10), (1, 11), (2,12)就表示最左面的(0)的音素為10(音素到數字映射後的數字),中間的音素為11,最右側的是12這樣。

在強制對齊之後,從左到右掃描對齊數據,我們能從中得到(三音素及HMM狀態)和其對應的特徵向量,也就是得到一個EventType和其對應的特徵向量。在掃描過所有訓練數據後,出現的每個EventType會對應多個特徵向量。 於是,我們就可以發現,與一個EventType相關的統計量包括該EventType對應的特徵向量的個數、這些特徵向量的累加、這些特徵向量的平方的累加。這三個值,就是GuassClusterable中需要保存的統計量,並且根據這三個統計量可以計算該EventType的似然。如果把多個EventType的統計量累加在一起,就可以計算這些EventType組成的狀態集的似然,因為一個EventType實際就是一個狀態state。

在掃描對齊數據累積統計量時,一個EventType對應一個Clusterable對象(確切來說是GaussClusterable對象)。在這個GaussCluterable對象中,成員count_保存著該EventType出現的次數,成員stats_矩陣的第一行保存著該EventType對應的所有特徵向量的和,stats_矩陣的第二行保存著該EventType對應的所有特徵向量的平方之和。

在構建決策樹時,我們需要知道的所有信息就是從訓練數據的對齊中得到的所有EventType(三音素+HMM狀態id),和每個EventType對應的Clusterable對象。很自然的,我們可以把這兩者的對應關係保存成一個對pair,然後把所有的這些對保存成一個vector,所以構建決策樹所用到的統計量可以表示成:

typedef std::vector<std::pair<EventType, Clusterable*> > BuildTreeStatsType;

cluster-phones 自動生成問題集

它的作用是對多個音素或多個因素集進行聚類。輸入為決策樹相關統計量treeacc、多個音素集sets.int。輸出為自動生成的問題集(每個問題由多個音素組成)。用法為:

Usage: cluster-phones [options] <tree-stats-in> <phone-sets-in> <clustered-phones-out>

其執行的流程為:

  1. 從treeacc中讀取統計量到BuildTreeStatsType stats;讀取vector pdf_class_list,該變數指定所考慮的HMM狀態,默認為1,也就是隻考慮三狀態HMM的中間狀態;從sets.int讀取vector > phone_sets;默認的三音素參數N=3,P=1。
  2. 若指定的mode為questions,調用AutomaticallyObtainQuestions()自動生成問題集vector > phone_sets_out;若指定的model為k-means,調用KMeansClusterPhones()。thchs30裏只涉及questions模式。
  3. 將上述函數自動生成的phone_sets_out寫到questions.int

可以看到主要的問題就是如何根據相關統計量和音素集通過聚類的方式生成決策樹。以下對該問題的處理流程做簡述:

  1. 讀取sets.int中的所有音素,保存在phones中。
  2. 調用FilterStatsByKey()把stats中只屬於三音素第二個HMM狀態的統計量留下(即只保留中間音素的統計量)。
  3. 調用SplitStatsByKey(),根據三音素的中間音素對retained_stats進行劃分,把屬於每個音素的統計量放在一個BuildTreeStatsType中。由參數P指定根據三音素的第幾個音素進行劃分,因為此處P是1,所以是三音素的中間音素。舉個例子,我們實驗室的所用的音素一共有215個,假設每個音素都出現在三音素的中間位置,對retained_stats進行劃分之後,split_stats的元素個數是215,每一個元素保存著(中間音素都是x的所有三音素對應的所有統計量)。
  4. 調用SumStatsVec()把split_stats每個元素中的所有統計量加起來,得到每個中間音素的統計量,也就是summed_stats,其維數為音素個數。簡單來說,從上一步我們知道,split_stats的每一個元素保存著中間音素都是x的所有三音素對應的所有統計量,因為音素x左右音素的不同,所以split_stats這個元素中保存的統計量有很多,現在把中間音素都是x的所有三音素對應的所有統計量累加起來(就是把這些GaussClusterable的count_相加、stats_相加);對split_stats的每個元素都執行這樣的操作後,就得到了summed_stats。
  5. 根據sets.int指定的集合,累加同一個集合中音素的統計量。
  6. 調用TreeCluster(),對summed_stats_per_set進行聚類,生成相關信息。TreeClusterer是使用自頂向下的樹進行聚類的一個對象。points_中保存著初始化TreeClusterer對象時傳遞進來的每個點的統計量,該對象的聚類過程,就是為了把這些點分成一簇簇(cluster)。queue_是一個優先隊列,隊列中的每個元素是一個pair,這個pair的第二個數據保存著結點信息,這個pair的第一個數據是對該結點進行劃分時所獲得的似然的最大提升。使用優先隊列則說明,對似然提升最大的結點優先進行劃分,直到queue_為空。這個劃分不是傳統決策樹的那種直接分裂,而是採用了聚類的方式進行。
  7. 調用ObtainSetsOfPhones(),由上一步得到的信息,生成問題集。大體流程為:a. 得到每個cluster(葉子結點)中的音素集b. 將子結點的音素集加入到其父結點的音素集中(實現了「把從該結點可以到達的所有葉子結點合在一起構成一個問題」);c. 把原始的phone_set插入到問題集;

    d. 過濾問題集的重複項、空項,生成最終的問題集。

compile-question 編譯question

它的輸入為HMM的拓撲結構文件topo和上一步得到的question.txt,輸出為question.qst。當key=0,1,2時,問題是對三音素中的每個音素分別問問題。當key=-1時問題是基於HMM的某個狀態的,這和HMM的狀態數目有關,通常為三個,得到的問題集為[[0],[0,1]], 如果為5個,則為[[0],[0,1],[0,1,2], [0,1,2,3]]。

build-tree 建立決策樹tree

它的輸入為關於tree的統計量、root文件、問題集和topo文件,輸出為決策樹。它主要調用to_pdf函數,該函數由roots文件中的所有音素集首先用GetStubMap()遞歸構初始的決策樹,總的來說GetStubMap()對每一個音素集創建一個初始的葉子結點,一個音素集就是roots.int中的一行中的音素的集合,每個節點其實都是一個小決策樹的樹根,之後會進一步由這個葉子節點劃分。

對於EvenType的每一個key(-1,0,1,2),在該key對應的問題集中(之前得到的對於HMM和音素的問題集)找到一個問題,使得對葉子結點劃分後獲得的似然提升最大。而後根據該問題進行分裂,直到葉子節點的類別滿足要求或似然度提升小於閥值時停止分裂。此時每個葉子節點的音素集實現了狀態綁定。

三音素的訓練

三音素的訓練和單音素模型的訓練步驟之後就較為相似了,唯一需要注意的就是由於我們之前得到的是單音素模型的對齊序列,因此我們在使用它時要將其轉換為三音素的,這個轉換由convert-ali進行操作。根據每個單音素對齊序列中transition-id 我們可以直到表示單音素模型中哪個phone的哪個狀態,因為雖然變成三音素了,中心位置phone和第幾個狀態沒有變,根據決策樹直接換成三音素模型中transition-id,輸出新的對齊序列即可。從舊的tid轉換成新的tid的流程大致如下:

最後由convert-ali得到三音素模型的對齊後,後面的GMM參數更新就和單音素GMM一樣,採用EM演算法。

參考

決策樹在Kaldi中如何使用

Kaldi決策樹狀態綁定學習筆記(一)——如何累積相關統計量?

Kaldi決策樹狀態綁定學習筆記(二)——如何自動生成問題集?

Kaldi決策樹狀態綁定學習筆記(三)——EventMap及其派生類、roots文件

Kaldi決策樹狀態綁定學習筆記(四)——如何構建決策樹?

kaldi 學習筆記-三音素訓練1(Decision Tree)

kaldi學習筆記-三音素訓練2 訓練

Kaldi三音素GMM學習筆記


推薦閱讀:
相關文章