1. 程式人生 > >Kaldi thchs30手札(四)三音子模型(line 71-76)

Kaldi thchs30手札(四)三音子模型(line 71-76)

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

概覽

首先放程式碼:

<code class="hljs livecodeserver">{% highlight bash %}
#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; {% endhighlight %}

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

  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<int,int>這樣。其中第一個數取-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<EventType,Clusterable>,然後把所有的這些對儲存成一個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演算法。

參考