小夕姐姐公眾號【夕小瑤的賣萌屋】的文章一直都是通俗易懂中夾帶著萌萌噠,所以也推薦大概感興趣的可以關注下哈!閉關幾個月後,其實早有繼續碼文章的打算,先後寫了一下核函數與神經網絡的一些思考、文本預處理tricks、不均衡文本分類問題、多標籤分類問題、tensorflow常用tricks、噪聲對比估算與負採樣等文章,結果全都半途而廢,寫了一半然後各種原因丟掉了就不想再接著寫。結果電腦裡稿子攢了好多,卻讓訂閱號空了這麼久。今天終於下定決心必須要碼一篇了,下午臨時決定寫一篇神經網絡調參的文章,嗯,就是這一篇啦。
雖然現在僅僅靠調參已經在深度學習領域不是香餑餑了,但是如果連參數都不會調,那可能連肉湯都喝不到的。畢竟你有再好的idea,也需要有一個漂亮的實驗結果去支撐的對不對,參數調不好,千裡馬也容易被當成騾子。
說到調參,也不得不吐槽現在行業裡論文復現難的問題。小夕曾經非常好奇AAAI2018某送審文章的性能,於是完全按照論文裡的設定去做了復現,發現跟與論文中的結果足足差了4個百分點!更神奇的是我發現按照該論文的設定,模型遠不足以擬合數據集 ╮( ̄▽ ̄"")╭ 最後實在get不到論文裡的trick了,小夕就放開了自己調!結果最後調的比該論文裡的結果還高了0.5個點,著實比較尷尬。
我們不能說某些頂會論文數據有問題,但是可以確信的是顯式或隱式的超參數很可能大大的影響實驗結果,這個超參數或許來自數據預處理,或許來自優化算法,或許來自模型本身,甚至有時候是來自輸出層的推理階段。
調參前的準備
好啦,回到正題上。在調參之前,小夕強烈建議在代碼裡完成下面幾件事:
可視化訓練過程中每個step(batch)的loss。如果是分類任務,可以順便可視化出每個batch的準確率(不均衡數據可視化F1-score)。
將訓練日誌在列印到屏幕上的同時也寫入到本地磁碟。如果能實時同步寫入那更好了(在python中可以用logging模塊可以輕鬆實現。一個handler輸出到屏幕,再設置一個handler輸出到磁碟即可)。
藉助tensorflow裡的FLAGS模塊或者python-fire工具將你的訓練腳本封裝成命令行工具。
代碼中完成tensorboard等訓練過程可視化環境的配置,最少要可視化出訓練loss曲線。
如果使用tensorflow,記得設置GPU內存動態增長(除非你只有一個GPU並且你確信一個訓練任務會消耗GPU的一大半顯存)
另外,初始調參階段記得關閉L2、Dropout等用來調高模型泛化能力的超參數吶,它們很可能極大的影響loss曲線,幹擾你的重要超參數的選取。然後根據自己的任務的量級,預估一個合理的batch size(一般來說64是個不錯的初始點。數據集不均衡的話建議使用更大一點的值,數據集不大模型又不是太小的情況下建議使用更小一些的值)。如果對網絡參數的隨機初始化策略缺乏經驗知識(知識來源於相關任務的論文實驗細節或開源項目等),可以使用He方法[1](使用ReLU激活時)或Xavier方法[2]來進行網絡參數初始化。
階段1:learning rate和num steps
這個階段是最容易的,打開tensorboard,按照指數規律設置幾組可能的學習率,小夕一般設置如下六組[1, 0.1, 0.01, 0.001, 0.0001, 0.00001]。
如果你的GPU比較多,你還可以在幾個大概率學習率附近多插幾個值,比如小夕一般喜歡再插上[0.03, 0.05, 0.003, 0.005, 0.007, 0.0005]這幾個值(最起碼在做文本分類任務時經常撞到不錯的結果哦)。
當這些任務跑完時,就可以去tensorboard裡挑選最優學習率啦。選擇原則也很簡單,選擇那條下降的又快又深的曲線所對應的學習率即可,如下圖,選擇粉色那條曲線:
選擇好學習率後,順便再觀察一下這條曲線,選擇一個差不多已經收斂的step作為我們的訓練總steps(如果數據集規模小的話也可以換算成epoch次數)。如圖
可以看到,我們的模型在迭代到4K步的時候就基本收斂了,保險起見我們可以選擇6K來作為我們訓練的總num_steps(畢竟後面改動的超參數可能使收斂延後嘛)。
細節:
如果GPU有限並且任務對顯存的消耗沒有太大,那麼可以同時在一個GPU裡掛上多組訓練任務(這時每組任務的計算速度會有損耗,但是完成全部任務所消耗的總時間大大減少了)。小夕一般先隨便設個學習率跑一下,確定一下每個任務大體消耗的顯存,然後在shell腳本裡將這若干個任務塞進GPU裡並行跑(shell腳本裡直接用&扔進後臺即可)。當然,如果代碼裡用到了時間戳,可以給時間戳加個隨機噪聲或者在shell腳本裡為任務之間加上一定的時間間隔,免得訓練任務的時間戳發生碰撞。
買不起GPU版:
曾經有一段時間小夕只有一個可以用的GPU,然而任務規模又大到每次實驗要佔用一大半的GPU顯存且要跑一天半,然而時間又特別緊,來不及像上面一樣跑十幾次實驗去選個學習率。那怎麼辦呢?
小夕get到一個trick,就是在代碼裡計算出來每次更新時的梯度更新向量的模與當前參數向量的模的比值。如果這個比值在量級附近的話說明學習率還可以,如果數量級太小,則網絡更新不動,需要增大學習率。數量級太大則每次更新對網絡的修改太大,網絡很容易發生不穩定,需要降低學習率。這樣基本跑幾個batch就能估算一次學習率,很快就能get到一個湊合使用的學習率。
階段2:batch size和momentum
帶著第一階段得到的超參數,我們來到了第二階段。
如果我們使用的是Adam這種「考慮周全」的優化器的話,動量項momentum這類優化器的超參數就基本省了。然而,不僅是小夕的經驗,業界廣泛的經驗就是Adam找到的最優點往往不如精調超參的SGD找到的超參數質量高。因此如果你想要追求更加極限的性能的話,momentum還是要會調的哦。
momentum一方面可以加速模型的收斂(減少迭代步數),另一方面還可以帶領模型逃離差勁的局部最優點(沒理解的快回去看看momentum SGD的公式)。而batch size參數似乎也能帶來類似的作用——batch size越小,噪聲越大,越容易逃離局部最優點,同時這時對梯度的估計不準確,導致需要更多的迭代步數。因此小夕一般將這兩個參數一起調。
兩個參數同時調的時候可以使用傳統的網格搜索,也可以使用大牛們提倡的隨機搜索[3]。小夕覺得嘛,GPU多又時間充裕的話就網格搜索,否則就隨機搜索啦。反正兩個超參數時使用網格搜索也不是讓人那麼無法接受。還不熟悉這兩種策略的同學可以去Ng在coursera開的深度學習課上補補哦,「超參數調節」這幾節課講的很清晰而且貌似是公開的。
另外,如果使用網格搜索並且搜索範圍小的話小夕一般直接在shell腳本裡偷懶解決:
另外,由於這兩個超參數可能涉及到模型的泛化能力,因此記得在監控loss曲線的同時也要監控開發集準確率哦。如果兩組實驗的loss曲線的形狀都很好,這時就可以根據開發集準確率來做取捨了(一般不會出現loss曲線形狀很差但是開發集準確率超好的情況)。
另外,還要記得!這一階段結束後,可能最優的loss曲線會發生很大的變化,可能第一階段我們確定的num_steps在這一階段已經變得過分冗餘了,那麼我們在這一階段結束後要記得把尾巴剪短一些哦(即減少num_steps,減少的依據跟以前一樣)。當然如果batch size低了很多,有可能之前的num_steps不足以充分訓練了,那麼要記得增加步數啦。
階段3:學習率衰減策略
相比較前面幾個超參數,學習率衰減策略就比較神奇了。有時你會發現這個超參數好像沒有什麼用,有時卻會發現它像開了掛一樣讓你看似已經收斂的網絡更進一層,帶來更低的訓練loss和更高的開發集準確率。
這個其實也很容易理解啦,如果你的模型在收斂時走到了「高原地帶」,這時其實你衰減學習率不會帶來太大改觀。而如果收斂時在「峽谷邊緣」來回跳躍,這時你衰減學習率就可能一步跨下峽谷,發現新大陸!當然啦,這也只能是我們的YY,在手頭任務中誰也不清楚這幾百萬幾千萬維度的空間裡的地形。所以不妨使用一個簡單有效的學習率衰減策略簡單一調,有用就繼續精調,沒用就算啦。
經典的學習率衰減策略要同時考慮4個東西:衰減開始的時機、衰減量級(線性衰減or指數衰減)、衰減速率以及衰減的周期。
還記得我們上個階段得到的開發集準確率曲線嗎?沒錯!這條曲線的低谷附近就是開始衰減的好時機!
衰減時機很好確定,例如上面這種狀態,最高開發集準確率在3000左右,那麼我們不妨從2700左右開始衰減學習率。
衰減量級來說,貌似大家用指數衰減更多一點。不過呢,對於指數衰減來說,衰減因子調節起來較為敏感,一旦衰減因子太小,則model往往還沒有訓練夠呢就衰減沒了。因子設置太大的話迭代好久學習率還是下不去,導致開發集的性能提升不大。考慮這些的同時還要把握好衰減的間隔(也就是每多少個steps衰減一次),如果間隔過小,則開發集準確率的波峰附近相比無衰減時更平緩,如果間隔過大,容易發現除了第一次衰減,後面的衰減都不會帶來什麼收益。不過,最最起碼的一個設計原則是,在到達原先的最高開發集準確率點的那個step時,最少衰減為初始學習率的一半才行(除非你的衰減間隔真的很短)。
是不是感覺超級麻煩哇,為了一個學習率衰減要去考慮和計算這麼多東西,感覺好麻煩哦,所以小夕個人更喜歡用下面這種懶辦法。
這種方法是從fasttext源碼裡學到的,實驗了一下發現還蠻好用的就一直用了下來。首先,開始衰減的點不用算,直接從第一步起就開始線性衰減。然後假如總迭代步數為5K,學習率為0.01,那麼我們就可以算一下每一步學習率的衰減量為
粗略算一下發現這時到達第3000步時的學習率為0.006,好像還蠻合理的誒。這樣在最後一步時,學習率也恰好衰減到0。
在這個方案裡,我們可以每個step都重新計算學習率,但是為了防止某些情況浮點下溢以及額外的計算開銷(雖然可以忽略),往往還是設置一個衰減間隔,比如每100steps衰減一次。相比經典策略,這時的衰減間隔就不那麼敏感啦,放心大膽的去設置。
使用這種懶辦法基本沒有引入任何難調的超參數,只要你在第二階段的num_steps設置的合理,這一階段的懶版學習率衰減就能往往取得不錯的效果。
當然,如果在當前任務中發現這個懶辦法也沒帶來多少收益,那這個任務可能真是地形相對平坦,對學習率衰減不太敏感,這時小夕一般不會考慮精調衰減策略。反之,如果發現這種懶辦法都帶來了明顯的收益,那麼仔細對比一下衰減策略下的開發集曲線和無衰減策略的開發集曲線,如果發現波峰後移的厲害,那可能衰減的太快了,嘗試推後衰減時機。不過,既然有明顯收益,那這時按照經典衰減策略再精調往往也不虧啦。
剩下的超參數要怎麼調呢?坐等小夕的下一篇文章咯( ̄∇ ̄)
[1] Xavier Glorot and Yoshua Bengio. 2010. Understanding the difficulty of training deep feedforward neural networks. In Proceedings of the Thirteenth International Conference on Artificial Intelligence and Statistics, volume 9 of Proceedings of Machine Learning Research, pages 249–256, Chia Laguna Resort, Sardinia, Italy. PMLR.
[2] Kaiming He, Xiangyu Zhang, Shaoqing Ren, and Jian Sun. 2015. Delving deep into rectifiers: Surpassing human-level performance on imagenet classification. CoRR, abs/1502.01852.
[3] Bergstra J, Bengio Y. Random search for hyper-parameter optimization[J]. Journal of Machine Learning Research, 2012, 13(Feb): 281-305.
推薦閱讀
一大批歷史精彩文章啦