Nnet3配置中的上下文和塊大小
簡介
本頁討論了nnet3配置中關於解碼和訓練的塊大小以及左右上下文的某些術語。這將有助於理解一些腳本。目前,從腳本角度來看,沒有任何關於nnet3的"概述"文檔,所以這是nnet3唯一的文檔。
基礎
如果您已閱讀了"nnet3"配置的以前的文檔,您會發現"nnet3"不僅支持簡單的前饋DNN,還可以實現在網絡內層進行時間拼接(幀拼接)的時延神經網絡(TDNN)以及帶有recurrent(循環)拓撲的RNN、LSTM、BLSTM等。所以nnet3有時間軸的概念。下面我們確定一些術語。
左右上下文
只有TDNN才需要左右上下文,LSTM/RNN不需要左右上下文
只有TDNN才需要右下文,LSTM/RNN不需要右下文;TDNN、LSTM、RNN均需要左上文
假設我們需要讓神經網絡計算特定時間索引時的輸出;比如時間t=154。如果該神經網絡在內部進行了幀的拼接(或任何其他與"t"索引相關的內容),在這種情況下,如果沒有給出當前幀的一定范圍左右上下幀,則可能無法計算當前幀的輸出。例如,如果沒有看到t = 150到t = 157這個范圍內的幀,則可能無法計算輸出。在這種情況下(忽略細節),我們會說網絡的左上下文為4、右上下文為3。上下文的實際計算有點復雜,因為它必須考慮到特殊情況,例如"t"值為奇數或偶數時。
循環拓撲,除了上述"所需"左右上下文外,在訓練或解碼時,它還需要"額外的"上下文。RNN會利用到超出"所需"上下文的上下文。在腳本中,通常會看到名為extra-left-context和extra-right-context的變量,這意味着"除了需要的內容之外,我們將提供的上下文的數量"。
在某些情況下,左上下文和右上下文意味着添加到chunk中的總的左上下文和總的右上下文,即
左上下文=模型左上下文+額外左上下文
右上下文=模型右上下文+額外右上下文
因此,在某些情況下,您需要搞清楚一個變量指的是模型的左右上下文還是數據塊的左右上下文。
在Kaldi5.0及更早版本中,數據塊中的左右上下文不受塊大小在開頭或結尾的影響;在最后我們用第一或最后一幀的副本填充輸入。這意味着對於循環拓撲,我們可能會用很多幀(最多40個左右)來填充語句的開始或結束。這沒有意義而且很奇怪。在版本5.1和更高版本中,您可以指定extra-left-context-initial和extra-right-context-final,允許話語的開始/結束具有不同的上下文量。如果您指定這些值,通常將它們都指定為0(即沒有額外的上下文)。但是,為了與舊版本兼容,它們通常默認為-1(意思是復制默認的左上方和右上方)。
Chunk大小
Chunk的大小是我們在訓練或解碼中每個數據塊所含(輸出)幀的數量。在get_egs.sh和train_dnn.py腳本中,chunk-size的也被稱為frames-per-eg(在某些上下文中,這與塊大小不同;見下文)。在解碼中,我們把它稱為frame-per-chunk。
對於非RNN、非chain模型、非TDNN
example, /eg-/, 樣例,樣本,可用其IPA音標"eg"來作為其縮寫;在傳統的語音識別中,一個example,即一幀以及標簽的二元組(frame, label)。
egs,多個eg,多個樣本。
對於不使用上下文幀以及任何時間信息的DNN,如以Sigmoid、Tanh、ReLu為神經元的DNN。chunk與egs等同。
對於非RNN、非chain模型、TDNN
對於使用交叉熵目標函數訓練的前饋網絡或TDNN等非常簡單的網絡類型,我們在幀級別上打亂整個數據集,並且我們一次只訓練一幀。為了在並行化訓練時順序地進行I/O,需要在幀級別上對數據進行預隨機化。然而,當訓練TDNN時,每幀都需要左右各10幀的上文和下文,並寫入到磁盤中,就必須知道某一幀的左右上下文具體是哪些幀,並記錄。
然而,不使用chunk,即以普通的方法生成訓練樣本時,所需的數據量可能會變成原來的20倍:
8幀,總共需要160幀的左右上下文
[-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[-9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11]
[-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ,12]
[-7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
[-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
[-4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
[-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
為了解決這個問題,將
- 某個時間范圍內的幀(大小由frame-per-eg控制,默認為8)
- 對應的標簽
- 左上下文
- 右上下文
組合為一個塊,即chunk,使得這些幀能共享左右上下文幀:
8幀,總共需要20幀的左右上下文
chunk與egs的區別:
chunk:[-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
eg:[0, 1, 2, 3, 4, 5, 6, 7]
即:chunk是顯式包含左右上下文幀的eg
這樣,chunk-width為8時,存儲的數據大小為原來的1/8;若chunk-width為20時,存儲的數據大小為原來的1/20
當訓練模型時,將以chunk為單位。
若以幀為單位進行LSTM的訓練,一整句話的所有幀能不斷地在神經網絡中進行前向傳播;
在TDNN訓練中,chunk包含frame_per_egs個有效輸出幀,以及顯式包含所需的左右上下文幀。設定大於1的frame_per_egs能減少硬盤空間消耗,但不影響訓練結果。
作用:
- 提供所需的左右上下文幀;
- 減少磁盤空間消耗;
TDNN解碼時,輸入第一幀時,需要10幀左上文和10幀右下文。而第一幀是沒有左上文的,因此,在輸入層處對第一幀拷貝10次,作為其左上文;而10幀的右下文還沒到來,因此無法輸出第一幀的輸出,需要等到第11幀到來時,才能輸出第1幀,等到第12幀到來時,輸出第2幀。
因此,時延為11*0.01=0.11秒,且只與右下文有關。
輸出到最后一幀時,沒有右下文了,將最后一幀拷貝10次,作為其右下文
不需要chunk,直接對整個語句進行解碼輸出。
left-context-max = max(left-contexti)
right-context-max = max(right-contexti)s
RNN的chunk-size
在RNN(LSTM、BLSTM等)模型或"Chain"模型中,總是訓練相當大的chunk(通常在40到150幀的范圍內),即chunk大小。當解碼時,通常在相當大的chunk(frames-per-chunk=30、50或100)上評估神經網絡。對於RNN,盡可能確保在訓練時的chunk-size或frames-per-chunk、extra-left-context、extra-right-context與解碼時的大致相同,以得到最優結果(盡管有時時解碼中的上下文值稍大一些較好)。人們可能會認為在解碼時更長的上下文能得到更好的結果,但是並不總是這樣(然而,請參見下面的looped decoding,其中提到一個更好的方法)。
在RNN訓練中,若不使用chunk,則以語句為單位進行訓練,但是,過長的語句會導致訓練過慢,且梯度沿時間傳播容易衰減甚至消失。因此,RNN的chunk用於解決訓練過慢和梯度衰減的問題。將語句分為若干個chunk,每個chunk根據left-context恢復對上一個chunk的記憶;每個chunk共享一段歷史信息。
作用:
- 加速訓練;
- 防止梯度衰減;
塊大小與frame-subsampling-factor的關系
當對輸出使用了幀降采樣(如chain模型),塊大小仍然以"t"的倍數進行測量,我們確保chunk-size是frame-subsampling factor的倍數。比如,chunk-size為90,frame-subsampling-factor為3,那么對大小為90幀的chunk估計30個的輸出(例如t = 0,t = 3 ... t = 87)。
可變的chunk大小
在Kaldi 5.1或更高版本中,有時候在訓練中使用可變chunk大小。因為當塊相當大時,會出現由於一個語句的幀數不是塊的整數倍而導致幀的丟失。這時,可以將塊大小指定為以逗號分隔的列表(例如150,120,90,75),並且生成訓練示例的命令可以創建任何這些大小的塊。指定的第一個塊大小稱為主塊大小,並且對於任何給定的話語都是"特殊的",最多允許指定兩個非主塊大小;剩余的塊必須是主塊大小。這種限制更容易得到給定長度的文件的最佳分割,並使得生成偏向於具有特定長度的塊。
Minibatch大小
nnet3-merge-egs將各個訓練樣本合並到包含許多不同樣本的minibatch中(每個原始樣本獲得不同的'n'索引)。 minibatch-size是minibatch的大小,指的是:將多個樣本的幀以及label組合為一個樣本,即eg的數量;
或對於RNN或TDNN,sequences,即chunk;進行組合的數量(例如,minibatch-size = 128)。
minibatch是以chunk為單位,一個chunk即一個樣本。
minibatch是以幀為單位,如64。
假設chunk-width=20,那么一個minibatch將橫跨3.2個chunk。短句的時長一般為3秒~4秒,設為3秒,設幀移為10ms,則1秒包含1000/10=100幀,一個短句包含300幀,如果minibatch=64,那么一句話被切分為4個minibatch=64*4=256幀,尾部的44幀被丟棄。
因此,minibatch常常也可變。
這種情況下,若minibatch=64,32
這句話就被切分為5個minibatch:64+64+64+64+32=288,尾部只有12幀被丟棄。
當塊大小可變時(如果我們設置了extra-left-context-initial和extra-right-context-final,考慮到話語開頭/結尾的上下文可能不同),需要確保minibatch中只包含"類似"的樣本;即某個樣本的上下文可用另一個樣本表示,以減小開銷。
在Kaldi版本5.1及更高版本中,nnet3-merge-egs僅將相同結構的chunk(即相同的塊大小和相同的左右上下文)合並在一起。它持續從輸入中讀取chunk,直到樣本數達到minibatch-size。在5.1之前的Kaldi版本中,通常丟棄那些無法湊足一個minibatch的樣本,現在,多種不同的chunk大小就不會丟棄太多數據)。
--egs.chunk-width
egs(examples)中每個chunk包含的幀數。注意:如果將值翻一番,則應將"--trainer.samples-per-iter"值折半。
--trainer.samples-per-iter
每個ark(archive,檔案)中包含的egs(examples,樣本)數。每個eg(example,樣本)包含'chunk_width'個幀。
chunk_width=20、samples_per_iter=20000時;相當於訓練普通DNN時每次迭代使用20*20000=400000幀。
可變的Minibatch大小
從Kaldi 5.1及更高版本開始,--minibatch-size的參數可以是一個更通用的字符串,允許用戶指定可變而非固定的的minibatch大小。 例如,可以指定--minibatch-size=64,128,這樣,對於每種類型的樣本,首先以128進行切分並輸出,直至輸入的末尾;若末尾剩下樣本數>=64,再以64為大小進行切分並輸出。 --minibatch-size也支持數字范圍,如 --minibatch-size=1:64表示首先以64幀為單位進行切分,然后將所有剩余的樣本組合為一個minibatch。 還可以為不同大小的樣本指定不同的規則(不帶參數運行nnet3-merge-egs以獲取詳細信息);這可使得GPU內存占用不容易溢出。
循環解碼(Looped decoding)
可修改"xconfig"配置文件中LSTM組件的decay-time參數,以使用循環解碼,如: "decay-time= 20"。這似乎不會降低WER,並且消除了普通解碼和循環解碼之間的差異(即,它使得網絡能夠容忍比訓練中看到的更長的上下文)。
腳本steps/nnet3/decode_looped.sh(Kaldi 5.1以上)僅接收兩個與塊或上下文相關的參數:frames-per-chunk(僅影響速度/延遲權衡,而非解碼結果),以及extra-left-context-initial,該參數應與訓練時(在最新腳本中通常為零)相同。
在撰寫本文時,尚未實現支持循環解碼的online2-wav-nnet3-latgen-faster;這是我們后續將要實現的。
