10686 一次 CTC-RNN 調參經歷

  這個月花了將近一整個月的時間,調整了一個循環神經網路(RNN)的超參數,經歷了各種挫折,最終取得了成功。在我最迷茫的時候,我曾經試圖在網上搜索有針對性的調參經驗,但一無所獲。為了給後來人排雷,我就把我的調參經歷記錄在這裡。

  要讀懂這篇文章,你需要了解 RNN、LSTM、CTC,最好有過訓練 CTC 網路的體驗。

零、任務描述

  任務背景其實並不重要。如果你不想詳細看,可以只看這一句:我用隨機梯度下降法(SGD)訓練了一個使用 CTC 輸出層的 RNN,可調的超參數包括梯度裁剪閾值和初始學習率。

  我訓練的 RNN 是一個語音識別任務的聲學模型。它的輸入是 40 維的濾波器組特徵序列,輸出是音素序列。網路並不負責把音素序列進一步轉換成單詞序列,所以它只是一個聲學模型,並不是一個完整的語音識別器。

  訓練使用的數據為 TEDLIUM v1,其內容為 TED 講座。其中訓練數據有 206 小時,開發數據有 1.7 小時,測試數據有 3.1 小時。由於種種原因,實際中我使用了 95% 的訓練數據進行訓練,5% 的訓練數據進行交叉驗證,開發集和測試集沒有用到。

  我一共訓練了兩個版本的網路。第一個版本與 EESEN 中的示例 相同。它含有 5 個雙向 LSTM 隱藏層,每層每個方向有 320 個神經元;CTC 輸出層含有 40 個神經元(39 個音素 + 1 個空白),放在一個 softmax 組中。訓練時的目標函數為平均每幀的 negative log-likelihood。訓練演算法為隨機梯度下降(SGD),每個 batch 含有 2 萬幀,每個 epoch(即掃描完一趟所有訓練數據)約為 2,000 個 batch。使用的超參數為:梯度裁剪至 10^{-4};初始學習率為 3,在前 12 個 epoch 保持恆定,之後每個 epoch 折半,直到 24 個 epoch;訓練使用了 Nesterov momentum,係數為 0.9。解碼時採用樸素的 best-path decoding 得到輸出音素序列,網路性能用音素錯誤率(PER)衡量。這個網路的訓練沒打什麼麻煩,1 個 epoch 過後,訓練集上的錯誤率就降到了 28.8%,交叉驗證集為 30.2%;用 6 天時間跑完全部 24 個 epoch 後,最終的性能為:訓練集 4.8%,交叉驗證集 15.4%。

  我真正想訓練的是第二個版本 ——「弱監督」版本。在這個版本中,對於每一條訓練語音,我不再告訴網路它對應的音素串是什麼,而只告訴它每個音素出現了幾次。網路的隱藏層結構都不變,但輸出層現在只有 39 個神經元,均使用 sigmoid 激活函數。這可以看成是使用了 39 個平行的 CTC 輸出層,每個輸出層只負責識別一個音素;在計算目標函數時,每個 CTC 層的目標序列就是它所負責的音素重複指定的次數。訓練方法、解碼方法、性能指標均與「強監督」版本相同。為了控制超參數空間的規模,我只調整梯度裁剪閾值初始學習率這兩個超參數。我的預期是這個網路仍能進行音素識別,但學習過程可能會慢一些,性能也會比「強監督」版本差一些。

一、出師不利

  首先,我把「強監督」版本的超參數(梯度裁剪閾值 10^{-4}、初始學習率 3)直接應用到了「弱監督」網路上。結果,經過了好幾個 epoch,網路都沒有輸出任何音素,兩個數據集上的錯誤率都是 100%。對比「強監督」版本在 1 個 epoch 後就達到了 30% 左右的錯誤率,「弱監督」版本顯然出問題了。

二、誤入歧途

  我猜想,「弱監督」網路之所以學不到東西,是因為它的目標函數跟「強監督」版本不同,造成最優超參數發生了偏移。但我並不確定應該把超參數調大還是調小。於是,我對梯度裁剪閾值 limit、初始學習率 lr 兩個參數進行了 grid search,limit 在每個數量級內取一個值,lr 在每個數量級內取兩個值。由於訓練比較費時間(1 個 epoch 需要 8 個小時),對每組超參數,我只訓練了 50 個 batch(相當於 1/40 個 epoch),並觀察此時在交叉驗證集中某 10 條語音上的錯誤率。如果訓練正常,這個錯誤率應該小於 100%;如果訓練異常,這個錯誤率可能等於 100%,也可能發生梯度爆炸導致程序崩潰。

  Grid search 的結果如下圖所示:

(圖 1:初次 grid search 的結果)

  這個圖需要好好消化一下。圖的縱軸是梯度裁剪閾值,橫軸是初始學習率。首先注意到,最低的錯誤率(藍色)分布在對角線上。這很容易理解:當梯度真的被裁剪到了的時候,裁剪閾值縮小一個數量級,就需要學習率放大一個數量級,才會讓網路參數發生相同的變化。注意到上圖正中的那個 89.3,它以及周圍的一些數字向右下方向延伸時毫無變化,這說明梯度裁剪已經非常「狠」了,梯度中絕大多數的分量都被裁剪得只剩下符號。這並不是我想要的。而向左上方延伸時,數值稍有不同,這說明左上這片區域中,梯度裁剪的力度適中,梯度的有些分量被裁剪了,有些則沒有。

  當超參數的組合偏離對角線時,錯誤率就開始增大了。在對角線左側,學習率太小,所以學得慢;在對角線右側,學習率太大,容易造成梯度爆炸(鮮紅色)。左下角和右上角的兩片區域我並沒有嘗試,因為這兩片區域中顯然不會出現更低的錯誤率。

  整張圖中最低的錯誤率是左上角的 88.3 和 88.5,它們對應的 limit 分別為 10^{-3}10^{-4},接近我最初嘗試的值(10^{-4});而 lr 則是 0.01 和 0.03,比我最初嘗試的值(3)小了兩個數量級。於是我覺得,第一節中網路沒有輸出的原因是學習率太大了,網路可能陷入了一個「什麼也不輸出」的局部最優解,出不來了。

  我用剛剛找到的最優超參數以及它附近的幾組超參數重新訓練網路,結果發現,若在每個 epoch 後測量錯誤率,仍然全都是 100%。實際上,對於 (limit = 10^{-3},lr = 0.01) 這組超參數,「50 個 batch 之後」恰恰是錯誤率最低的時機。此後,錯誤率會逐漸回升,並在 1 個 epoch 以內回升到 100% 並保持。難道說用弱監督真的什麼也學不到?我不甘心。

三、曲徑通幽

  為了弄清楚網路到底在幹什麼,我決定畫出在某一條語音上網路各層的輸出,看看能否觀察出端倪。

(圖 2:弱監督網路訓練 50 個 batch 後各層的輸出)

  上圖是「弱監督」網路使用最初的超參數(limit = 10^{-4},lr = 3)訓練 50 個 batch 後,在交叉驗證集里一條語音上各層的輸出。這條語音共有 85 幀。圖中從左到右、從上到下依次是輸入的濾波器組特徵、5 個隱藏層的輸出,以及最後預測的每個音素的概率。由於每個隱藏層有 640 個神經元,圖中畫不下,所以我只畫了向前方向的 40 個神經元的輸出。可以看出,各層的輸出越來越「糊」,即在時間方向上的變化越來越不明顯,到了第 3 個隱藏層,輸出就基本是常數了。當然,在最開頭的幾幀,輸出還是略有不同的。我覺得,這表示 RNN 陷入了某種「死循環」,除了開頭的幾幀,RNN 就陷入了「不管輸入是什麼,輸出都不變」的死亡狀態了。上面的圖中畫的是訓練 50 個 batch 之後的情景,但哪怕訓練到 100 個 batch,結果都是類似的。

  我試圖分析網路參數(包括權重和偏置)的變化,以及 LSTM 各個門的開閉情況,都沒能得出明確的結論。於是,我準備也畫出「強監督」網路在同一條語音上各層的輸出,與「弱監督」網路對比。

(圖 3:強監督網路訓練 10 個 batch 後各層的輸出)

  上圖是「強監督」網路在訓練 10 個 batch 後各層的輸出。我們同樣看到,從第 4 個隱藏層開始,輸出也「糊」掉了。但!是!隨著訓練的進行,這種「糊」掉的現象得以改善。比如,在訓練 200 個 batch 之後,各層的輸出是下面這樣的。可以看到,所有隱藏層的輸出都開始隨著時間有變化,而且變化與輸入特徵中的突變基本同步。網路開始學到東西了!

(圖 4:強監督網路訓練 200 個 batch 後各層的輸出)

   對「強監督」網路的觀察推翻了我對於「弱監督」網路陷入「死循環」的猜想。原來,網路並不是「死了」、已經學不到東西了,而是還沒有開始「活」。我想起了 CTC 網路在訓練的初始階段,會有一個「熱身」(warm-up)的過程,此時「空白」符號的概率占絕對優勢,網路什麼也不會輸出。而在「熱身」階段之前,其實還會有一個「預熱身」階段,在此階段,「空白符號的概率占絕對優勢」這一秩序尚未建立,網路可能會輸出一些隨機的符號(音素)。觀察圖 2 的最後一個子圖,可以看到有 3 個音素具有較高的概率,網路會把它們輸出出來。如果正好某一個音素也出現在標準答案中,PER 就會小於 100% 了。如果觀察 PER 在 CTC 訓練過程中的變化,會是這個樣子:

(圖 5:CTC 訓練過程中各個階段錯誤率的變化)

  原來,圖 1 中所有小於 100% 的錯誤率,其實都對應著「預熱身階段」。它們在一段時間後回升到 100%,不代表著網路的死亡,而是代表進入了「熱身階段」。為了促使網路儘快脫離熱身階段、進入學習階段(下文稱為「啟動」),不應該把學習率調低,反而應該調高。在第二節,我們調參的方向正好反了。

  當然,如果只調高學習率,不調整梯度裁剪閾值,就會導致梯度爆炸;所以需要相應地調低梯度裁剪閾值。如果放在圖 1 中來看,就是說,最優參數很可能不位於圖 1 的左上方,而是位於右下方。咦?等等,在圖 1 的右下方,只要保持 limit * lr 恆定,錯誤率都一樣呀?其實並不是這樣 ——「錯誤率都一樣」只是預熱身階段的現象。隨著訓練的進行,梯度的絕對值會越來越小,等到梯度裁剪不起作用的時候,就只剩下學習率決定訓練進展的速度了。這說明我們應當調高學習率,同時調低梯度裁剪閾值。另外,最優參數不一定位於圖 1 中錯誤率最低的對角線上,因為預熱身階段的錯誤率並不能說明學習階段的表現。我們需要在圖 1 右下方區域內重新進行 grid search,這次要觀察的不是一定時間後的錯誤率,而是需要多久網路才能「啟動」。

四、昂首闊步

  第二次 grid search 的結果如下圖。我選擇的超參數都位於圖 1 中對角線偏右側的區域,因為我觀察到,只有這片區域內的超參數才能使網路在 100 個 batch 之內脫離預熱身階段,而我的目的正是尋找讓網路訓練進展更快的超參數。對於每組超參數,我訓練了 10,000 個 batch,相當於 5 個 epoch,用時接近 2 天。每過 200 個 batch,我測量了交叉驗證集中 10 條語音上的錯誤率,圖中記錄的是網路「啟動」的時間和發生梯度爆炸的時間。

(圖 6:第二次 grid search 的結果)

  訓練 10,000 個 batch 後,我們看到了與圖 1 不一樣的風景。原來,當我們「昂首闊步」地把學習率調高好幾個數量級之後,網路還是可以在合理的時間內啟動的。使網路啟動最快的超參數出現在 limit = 10^{-8},lr = 10^4,它只需要 1,200 個 batch 就能啟動。到 5 個 epoch 時,在隨機選擇的 10 條交叉驗證語音上的錯誤率已經達到了 55.6%,網路果然學到東西了!

五、寧靜致遠

  於是,我用 limit = 10^{-8},lr = 10^4 這組參數進行了正式的實驗,每個 epoch 後在整個交叉驗證集上測量錯誤率。令我驚喜的是,在 5 個 epoch 後,整個驗證集上的錯誤率實際上已經降到了 44.3%。但是,到第 8 個 epoch 時,發生了梯度爆炸,訓練崩潰了。

  看來,上一節中使用的學習率還是「步子太大,扯到蛋了」。雖然啟動很快,但當梯度小到裁剪不起作用的時候,學習率太大的壞處就體現出來了。這意味著學習率不應該調得這麼大,走得慢一點,才能走得遠。當然,還有一種解決方案是在第 8 個 epoch 之前開始降低學習率,不過如果去試驗這一點,就意味著把學習率的 schedule(包括在什麼時候開始衰減、衰減多快)也納入待調整的超參數,超參數空間一下子又升了兩維,這並不是我想要的。於是,我在圖 6 中試驗過的超參數的左側又進行了第三次 grid search。這次 grid search 是正式的 —— 我跑完了全部 24 個 epoch,並在每個 epoch 後在整個交叉驗證集上測量錯誤率。下圖展示了結果。綠色背景的單元格表示成功跑完 24 個 epoch 的超參數,其中的數值是最終交叉驗證集上的錯誤率;橙色背景的單元格表示發生梯度爆炸的超參數,其中記錄了發生爆炸的 epoch 數,以及前一個 epoch 的錯誤率。如果 6 個 epoch 後網路仍未啟動,我則沒有讓它們繼續跑下去,相應的單元格塗成紅色。

(圖 7:第三次 grid search 的結果)

  最終得到的最優超參數為 limit = 10^{-6},lr = 100。在 24 個 epoch 之後,它在訓練集和交叉驗證集上的錯誤率分別為 30.1% 和 34.5%,高於「強監督」網路的 4.8% 和 15.4%。這說明在「弱監督」下網路仍能學到東西,但不如在「強監督」下學得那麼快、那麼好,完美驗證了我的預期。

六、經驗總結

  1. CTC 在訓練過程中會經歷預熱身階段、熱身階段、學習階段。在熱身階段,網路沒有任何輸出,錯誤率會是 100%;在預熱身階段,網路會有少量隨機輸出,錯誤率略低於 100%。如果只觀察錯誤率,容易把預熱身階段誤當成學習階段。
  2. 如果觀察到 RNN 中各隱藏層的輸出不隨時間變化,並不意味著網路已經陷入了「死循環」,往往只是網路還沒有開始學習。此時應加大學習率,促使網路儘快進入學習階段。
  3. 在對 CTC 網路的超參數進行 grid search 時,如果為了省時間而只跑較少的 batch 數,往往欲速則不達:你可能只觀察到了預熱身階段;也有可能在學習階段的初期學得很快的超參數會導致後期梯度爆炸。最終的最優超參數還是要跑完整個訓練過程才能確定。

推薦閱讀:

什麼是自編碼 Autoencoder
集成學習(Ensemble Learning)
通俗理解激活函數作用的另一種解釋
深度學習一行一行敲faster rcnn-keras版(目錄)

TAG:机器学习 | 深度学习DeepLearning | 神经网络 |