決策樹內部結構
該頁面描述了語音決策樹聚類代碼的內部結構,代碼實現爲非常通用的結構和算法。
有關解釋整個實現算法以及工具包如何使用,請參閱“如何在Kaldi中使用決策樹”一節。
EventMap類
構建決策樹代碼的主要概念是“事件映射”,由EventMap類型表示。不要被“事件”這個術語誤導,而錯誤地認爲是特定時間發生的事情。事件只是一組(鍵,值)對,沒有重複鍵。概念上,它可以由類型std :: map <int32,int32>表示。
事實上,爲了提高效率,我們通過typedef將它們表示爲一對排序向量:
typedef std :: vector <std :: pair <EventKeyType,EventValueType>> EventType;
這裏,EventKeyType和EventValueType是int32的別名,給它們使用不同的名稱是爲了代碼更容易被理解。將事件(類型EventType)視爲變量的集合,其中,變量的名稱和值都是整數。還有EventAnswerType類型,這也是int32。它是EventMap映射的類型;實際上它是pdf標識符(聲學狀態索引)。使用EventType類型的函數時,需要先排序(例如:通過調用std :: sort(e.begin(),e.end()))。
EventMap類的功能下面通過一個例子來說明。
假設我們的音素上下文是a/b/c;假設我們有標準的3狀態拓撲;假設我們想在這個上下文中詢問音素“b”的中心狀態的pdf的索引是什麼。所討論的狀態將是狀態1,因爲我們使用基於零的索引。當我們提到“狀態”時,我們正在撇開一些細節;有關更多信息,請參閱Pdf-class。假設音素a,b和c的整數索引分別爲10,11和12。 “事件”將對應於映射:
(0->10),(1->11),(2->12),(-1->1)
其中0,1和2是3音素窗口“a/b/c”中的位置,-1是我們用於對狀態id進行編碼的特殊索引(c.f.常量kPdfClass
= -1)。表示爲排序向量對爲:
EventType e = {{-1,1},{0,10},{1,11},{2,12}};
假設對應於這種聲學狀態的聲學狀態索引(pdf-id)恰好爲1000.那麼如果我們有一個表示樹的EventMap“emap”,那麼我們期望以下的斷言不會失敗:
EventAnswerType ans;
bool ret = emap.Map(e,&ans);
// emap的類型是EventMap; e是EventType
KALDI_ASSERT(ret == true && ans == 1000);
所以當我們聲明一個EventMap是從EventType到EventAnswerType的映射時,你可以將它大概地看作是一個從上下文相關音素到整數索引的映射。上下文相關的音素表示爲一組(鍵值)對,原則上我們可以添加新的鍵並輸入更多信息。請注意,EventMap::Map()函數返回bool。這是因爲某些事件可能無法映射到任何答案(例如,考慮無效的音素,或者想象當EventType不包含EventMap正在查詢的所有鍵時會發生什麼)。
EventMap是一個非常被動的對象。它沒有任何學習決策樹的能力,它只是存儲決策樹的一種手段。可以將其視爲從EventType到EventAnswerType的函數結構。
EventMap是一個多態的純虛擬類(即它不能被實例化,因爲它具有未實現的虛函數)。有三個具體的類來實現EventMap接口:
ConstantEventMap:將其視爲決策樹葉節點。 此類存儲一個類型爲EventAnswerType的整數,其Map函數始終返回該值。
SplitEventMap:將其視爲決策樹非葉節點,查詢某個關鍵字並根據答案轉到“是”或“否”子節點。
它的Map函數調用相應子節點的Map函數。 它存儲一組與“是”子對應的類型爲kAnswerType的整數(所有其他內容都轉到“否”)。
TableEventMap:這是對特定鍵進行完全拆分。 一個典型的例子是:您可能首先在中心音素上完全拆分,然後爲該音素的每個值分別設置一個決策樹。
內部它存儲一個EventMap *指針向量。 它查找與其分割的鍵對應的值,並調用矢量中相應位置處的EventMap的Map函數。
EventMap除了將EventType映射到EventAnswerType之外,實際上不會做很多事情。它的接口不提供允許您遍歷樹的功能,無論是向上還是向下。只有一個函數允許您修改EventMap,就是EventMap::Copy()函數,聲明如下(作爲一個類成員):
virtual EventMap * Copy(const std :: vector <EventMap *>&new_leaves)const;
這具有類似於功能組合的效果。如果您調用Copy()使用空向量“new_leaves”,那麼它只會返回整個對象的深層副本,並將所有指針複製到樹上。但是,如果new_leaves爲非空值,則每次Copy()函數到達葉子時,如果葉子l在範圍(0,new_leaves.size() - 1)和new_leaves [l]不爲NULL,則Copy )函數將返回調用new_leaves [l]->Copy()的結果,而不是返回一個新的ConstantEventMap的本身的Copy()函數。一個典型的例子是你決定拆分一個特定的葉子,如葉852.你可以創建一個類型vector <EventMap *>的對象,其唯一的非空成員是位置852.它將包含一個指向一個對象的指針,類型是SplitEventMap,“yes”和“no”指針將使用具有葉值的 ConstantEventMap(例如852和1234)(我們重新使用新葉子的舊葉節點ID)。真正的建樹代碼不是這樣沒有效率的。
統計建樹
用於構建音素決策樹的統計數據有以下類型:
typedef std::vector<std::pair<EventType,Clusterable *>> BuildTreeStatsType;
對這種類型的對象的例子被傳遞給所有的決策樹構建過程。
這些統計信息預計不會包含相同的EventType成員的重複項,即它們在概念上表示從EventType到Clusterable的映射。
在我們當前的代碼中,Clusterable對象實際上是GaussClusterable類型,但是樹形代碼不知道這一點。 累積這些統計信息的程序是accrest-stats.cc。
構建樹的類和功能
Questions(config class)
類Questions是一個類,它與樹構建的交互,表現得像一個“配置”類。它實際上是從“key”值(類型爲EventKeyType)到類型爲QuestionsForKey的配置類的映射。
類 QuestionsForKey有一組“問題”,每個都是一組類型爲EventValueType的整數;這些主要對應於一組音素,或者如果鍵爲-1,則在典型情況下它們將對應於HMM狀態索引的集合,即{0,1,2}的子集。
QuestionsForKey還包含一個類型爲RefineClustersOptions的配置類。這樣做可以控制構建樹的行爲,因爲樹構建代碼將嘗試迭代地在分裂的兩邊之間移動值(例如音素),爲了最大化似然(如K-means中K
= 2)。然而,這可以通過將“精簡集羣”的迭代次數設置爲零來關閉,這對應於從固定的問題列表中選擇。這似乎工作好一點。
底層函數
有關完整列表,請參閱操作統計信息和事件映射的底層函數;我們在這裏總結一些重要的。
這些函數主要涉及對BuildTreeStatsType類型的對象進行操作,如上所述,它們是(EventType,Clusterable
*)對的向量。最簡單的是DeleteBuildTreeStats(),WriteBuildTreeStats()和ReadBuildTreeStats()。
函數PossibleValues()發現一個特定的鍵在一個統計信息的集合中的值(並通知用戶該鍵是否被定義);
SplitStatsByKey()將根據特定鍵值(例如在中心音素上分割)將類型爲BuildTreeStatsType的對象拆分爲向量<BuildTreeStatsType>;
SplitStatsByMap()執行相同的操作,但該索引不是該鍵的值,而是由EventMap返回的答案提供給該函數。
SumStats()對BuildTreeStatsType對象中的統計信息(即Clusterable對象)進行求和,並返回相應的Clusterable
*對象。 SumStatsVec()獲取一個類型爲vector <BuildTreeStatsType>的對象,並輸出一些類型向量<Clusterable
*>,即它像SumStats(),但是對於一個向量;在處理SplitStatsByKey()和SplitStatsByMap()的輸出時很有用。
ObjfGivenMap()用給出一些統計信息和EventMap評估目標函數:它總結了每個集羣內的所有統計信息(對應於EventMap::Map()函數的每個不同答案),將整個集羣中的目標函數相加,返回總數。
FindAllKeys()將找到在統計信息集合中定義的所有鍵,並且根據參數可以找到爲所有事件定義的所有鍵或爲任何事件定義的所有鍵(即採取交集或一組定義的鍵的並集)。
中間層函數
這裏列出了下一批涉及構建樹的函數,對應於構建樹的各個階段。我們現在只提一些代表性的。
首先,我們指出很多這些函數都有一個參數int32 num_leaves。這個整數作爲分配新葉子的計數器。在建樹開始時,調用者將其設置爲零。當需要一個新葉子時,構建樹的代碼將它當前指向的數字作爲新葉子ID,然後將其遞增。
一個重要的函數是GetStubMap()。此函數返回尚未拆分的樹,即,pdf不依賴於上下文。該函數的輸入控制所有音素是不同的還是其中一些共享決策樹根,以及特定音素內的所有狀態是否共享相同的決策樹根。
SplitDecisionTree()函數通常對應於由GetStubMap()創建的類型的非分割“存根”決策樹作爲輸入,並且執行決策樹分解,直到達到最大葉數爲止,或者從分割葉子的增益小於指定的閾值。
ClusterEventMap()函數使用EventMap和閾值,合併EventMap的葉子直到代價低於閾值即可。通常在SplitDecisionTree()之後調用此函數。此函數還有其他版本可以操作限制(例如避免合併來自不同音素的葉子)。
上層建樹函數
這裏列出了最上層的建樹函數。這些函數直接從命令行程序調用。最重要的是BuildTree()。給一個Questions配置類,以及一些關於共享樹根的音素集信息(對於每組音素),是否在單個決策樹中共享pdf-class,或者爲每個pdf-class有不同的決策樹。它還傳遞了有關音素長度的信息,加上各種閾值。它構建樹並返回用於構造ContextDependency對象的EventMap對象。
另一個重要函數是AutomaticallyObtainQuestions(),用於通過音素自動聚類獲取問題。它將音素聚集成一棵樹,對於樹中的每個節點,可從該節點訪問的所有樹葉形成一個問題(問題相當於音素集)。這個(來自cluster-phones.cc)使我們的方法獨立於人造音素集。
函數AccumulateTreeStats()累加訓練樹的統計信息,給定一系列transition-id的特徵和對齊(參見TransitionModel使用的整數標識符)。該函數在與其他樹構建相關函數(hmm / not tree /)不同的目錄中定義,因爲它取決於更多的代碼(例如它知道TransitionModel類),我們更希望解耦核心建樹代碼。