上一期,我們一起學習了深度學習中的學習率的悲慘命運,
深度學習三人行(第6期)----深度學習之學習率的命運
今天我們一起學習下深度學習中如何避免過擬合,我們多多交流,共同進步。本期主要內容如下:
- 提前停止訓練
- L1和L2範數正則化
- DroupOut
- 最大範數正則化
- 數據增強
- 小結
我們知道,深度學習有成千上萬個參數,甚至數百萬。由於有巨大的參數,所以整個網絡有着難以置信的自由度,能夠擬合複雜的數據集。也就是說,這個巨大的靈活度意味着在訓練集上很容易過擬合。這裏,我們主要通過幾個常見的方法,來看下如何避免過擬合,主要有:過早的停止訓練,L1和L2範數的正則化,DroupOut, 最大範數正則化,數據增強等。
一. 提前停止訓練
爲了避免對訓練集造成過擬合,一個很好的方法就是在過擬合之前就停止對網絡的訓練(之前文章有介紹過)。也就是說在測試集上的性能開始下降之前的時候停止對訓練數據集的訓練。
在TensorFlow中就是在訓練的時候,通過對測試集上性能的評估,每隔一定的間隔進行保存一個當前最優的網絡,如果該網絡比上一個間隔更好,則替換掉上一個網絡。這樣在訓練結束後,將保存一個整個網絡訓練過程中出現最優的一個模型。儘管在實踐中提前停止訓練的方法能夠的很好,但是通常情況下,如果能夠結合其他正則化技術的話,能夠變現更佳。
二. L1和L2範數正則化
正如之前我們學習線性模型的時候一樣,我們也可以在神經網絡用L1和L2範數進行約束權重(一般 不對偏置項)。在TensorFlow中實現正則化還是比較簡單的,只需要在損失函數中加上合適的正則項即可。比如:假如我們的網絡只有一個隱藏層,權重爲weights1,一個輸出層,權重爲weight2。那麼我們就能對權重用L1正則化,如下:
1[...] # construct the neural network 2base_loss = tf.reduce_mean(xentropy, name="avg_xentropy") 3reg_losses = tf.reduce_sum(tf.abs(weights1)) + tf.reduce_sum(tf.abs(weights2)) 4loss = tf.add(base_loss, scale * reg_losses, name="loss")
上面的方法雖然沒問題,但是,如果我們的網絡有很多層,那麼上面的這種方法就不是太方便了。幸運的是,對於多層的正則化,TensorFlow有更好的方法。TensorFlow中有很多創建變量的函數在創建的時候都會接受一個正則化的參數。我們可以傳輸帶權重的函數作爲一個參數,並且返回相應的正則化是損失。如下:
1with arg_scope( 2 [fully_connected],weights_regularizer=tf.contrib.layers.l1_regularizer(scale=0.01)): 3 hidden1 = fully_connected(X, n_hidden1, scope="hidden1") 4 hidden2 = fully_connected(hidden1, n_hidden2, scope="hidden2") 5 logits = fully_connected(hidden2, n_outputs, activation_fn=None,scope="out")
上面的代碼創建一個有兩個隱藏層和一個輸出層的神經網絡,並且對於每一層的權重,都在圖中創建了節點和計算L1正則化損失。TensorFlow自動把所有的正則化損失加到一個特定的集合中。我們只需要將這些正則化損失加到整體損失中,如下:
1reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES) 2loss = tf.add_n([base_loss] + reg_losses, name="loss")
三. DropOut
然而,在深度神經網絡中,最流行的正則化技術則是DropOut, 這項技術在2012年由G.E.Hinton提出。後來被Nitish Srivastava發展並證明是非常成功的。即使最優秀的網絡,如果增加DropOut技術,準確率也能夠提升1~2個百分點。這看起來提高並不是太多,但是如果一個模型已經達到95%的準確率,那麼提高兩個百分點意味着錯誤率降低了40%(從5%到3%)。
其實,這是一個蠻簡單的算法:就是在training的每一步,每一個神經元(包括輸入神經元,但是不包括輸出神經元),都有一個概率p被丟棄。被丟棄的神經元,意味着在本次training中完全被放棄,但是可能在下次迭代中被激活。這個超參數p成爲DropOut率,一般設置爲50%。如下圖:
咋一看,這種粗魯的做法能夠很好的工作有些不可思議。但是事實證明確實可行!由於每一個神經元隨機的放棄,最終訓練的結果會使每一個神經元都不會過分依賴於其他的神經元,而是使努力使自己達到最優。最終網絡對輸入的輕微變化不在敏感,進而得到一個魯棒性很強的網絡。
因爲我們訓練的時候會隨機的丟棄一些神經元(概率爲p),但是預測的時候就沒辦法隨機丟棄了。如果丟棄一些神經元,這會帶來結果不穩定的問題,也就是給定一個測試數據,有時候輸出a有時候輸出b,結果不穩定,這是實際系統不能接受的,用戶可能認爲模型預測不準。那麼一種”補償“的方案就是每個神經元的權重都乘以一個(1-p),或者在訓練的時候除以(1-p),這樣在“總體上”使得測試數據和訓練數據是大致一樣的。比如一個神經元的輸出是x,那麼在訓練的時候它有(1-p)的概率參與訓練,p的概率丟棄,那麼它輸出的期望是(1-p)x+p0=(1-p)x。因此測試的時候把這個神經元的權重乘以(1-p)可以得到同樣的期望。
在TensorFlow中如何運用dropout呢?只需要簡單的在輸入層和隱藏層之前加上dropout函數即可。在training的 過程中,這個函數會隨機將一些神經元置爲0,並且自動除以(1-p)。下面代碼展示瞭如何在TensorFlow中運用dropout正則化技術,如下:
1from tensorflow.contrib.layers import dropout 2[...] 3is_training = tf.placeholder(tf.bool, shape=(), name='is_training') 4keep_prob = 0.5 5X_drop = dropout(X, keep_prob, is_training=is_training) 6hidden1 = fully_connected(X_drop, n_hidden1, scope="hidden1") 7hidden1_drop = dropout(hidden1, keep_prob, is_training=is_training) 8hidden2 = fully_connected(hidden1_drop, n_hidden2, scope="hidden2") 9hidden2_drop = dropout(hidden2, keep_prob, is_training=is_training) 10logits = fully_connected(hidden2_drop, n_outputs, activation_fn=None,scope="outputs")
正如之前batch Normalization一樣,在訓練的時候我們需要設置is_training 爲true,測試的時候設爲false。當我們觀察到模型出現過擬合的時候,我們可以增加dropout率,也就是說減小keep_prob率。相反,如果模型欠出現擬合的時候,可以增加keep_prob率,減小dropout率。通常對於大的網絡增加dropout率,小的網絡減少dropout率往往會有幫助。
然而,一般情況下,加入dropout的話,會使訓練收斂明顯放慢。但是往往會得到更好的模型,很值!
四. 最大範數正則化
另外一個神經網絡中常見的正則化技術就是最大範數正則化。對於每一個神經元的權重,都受到如下的約束:
其中||w||_2爲L2範數,r爲最大範數。通常情況下,我們通過計算w的L2範數來進行達到目的。如下:
最大範數正則化,往往能夠降低過擬合,如果不適用batch正則化的話,也可以減輕梯度消失和梯度爆炸的問題。
TensorFlow並沒有提供一個現成的最大範數正則化函數,但是實施起來也並不麻煩。如下:
1threshold = 1.0 2clipped_weights = tf.clip_by_norm(weights, clip_norm=threshold, axes=1) 3clip_weights = tf.assign(weights, clipped_weights)
上面代碼創建一個clip_weights的節點,用來調整weights變量。我們可以在每一次迭代之後加上這個操作,如下:
1with tf.Session() as sess: 2 [...] 3 for epoch in range(n_epochs): 4 [...] 5 for X_batch, y_batch in zip(X_batches, y_batches): 6 sess.run(training_op, feed_dict={X: X_batch, y: y_batch}) 7 clip_weights.eval()
我們可以通過scope來觀察權重的變化,如下:
1hidden1 = fully_connected(X, n_hidden1, scope="hidden1") 2with tf.variable_scope("hidden1", reuse=True): 3 weights1 = tf.get_variable("weights")
也可以通過root scope來得到權重,如下:
1hidden1 = fully_connected(X, n_hidden1, scope="hidden1") 2hidden2 = fully_connected(hidden1, n_hidden2, scope="hidden2") 3[...] 4with tf.variable_scope("", default_name="", reuse=True): # root scope 5 weights1 = tf.get_variable("hidden1/weights") 6 weights2 = tf.get_variable("hidden2/weights")
如果我們不知道變量的名字,那麼可以通過TensorBoard或者簡單的用global_variables()函數來查看變量名,如下:
1for variable in tf.global_variables(): 2 print(variable.name)
儘管上面的方法可行,但是顯得有些繁瑣。一個更爲簡潔的方法就是創建一個最大範數正則化的函數,就好比前面學的L1,L2範數正則化函數一樣,如下:
1def max_norm_regularizer(threshold, axes=1, name="max_norm", 2collection="max_norm"): 3 def max_norm(weights): 4 clipped = tf.clip_by_norm(weights, clip_norm=threshold, axes=axes) 5 clip_weights = tf.assign(weights, clipped, name=name) 6 tf.add_to_collection(collection, clip_weights) 7 return None # there is no regularization loss term 8 return max_norm
上面的函數返回的是一個參數化的函數max_norm(),可以向其他正則化一樣:
1max_norm_reg = max_norm_regularizer(threshold=1.0) 2hidden1 = fully_connected(X, n_hidden1, scope="hidden1",weights_regularizer=max_norm_reg)
注意到最大範數正則化不需要在全局的損失函數上增加正則項,所以max_norm()函數返回爲None。但是我們仍需要在每次迭代之後運行clip_weights,這就是爲什麼max_norm()函數中將clip_weights增加到collection中。我們需要獲取clip操作並在每次迭代之後運行,如下:
1clip_all_weights = tf.get_collection("max_norm") 2with tf.Session() as sess: 3 [...] 4 for epoch in range(n_epochs): 5 [...] 6 for X_batch, y_batch in zip(X_batches, y_batches): 7 sess.run(training_op, feed_dict={X: X_batch, y: y_batch}) 8 sess.run(clip_all_weights)
五. 數據增強
最後一個正則化技術就是數據增強,主要是認爲的從已有的數據中人爲的產生新的數據,進而來增加數據集,降低過擬合。比如,我們要對蘑菇圖片進行分類,我們可以通過輕微的旋轉,平移,縮放等方法來產生新的數據,如下圖:
這就去迫使模型能對蘑菇的位置,大小,方位等適應,也可以通過改變圖片的對比度來使得模型適應不同的光照。通過這些方法,可以使大大擴大數據集。
TensorFlow中提供了一些圖像操作的方法,比如平移,旋轉,縮放,翻轉,剪切,調整光照對比度,飽和度,色度等,這就使得對數據增強比較方便。
六. 小結
這期我們主要通過幾個常見的方法:過早的停止訓練,L1和L2範數的正則化,DroupOut, 最大範數正則化,數據增強等。來避免模型過擬合,幾種方法各有千秋,找到最適合自己模型的方法纔是王道。