CHAPTER 11-Training Deep Neural Nets-part1

本篇文章是個人翻譯的,如有商業用途,請通知本人謝謝.


Vanishing/Exploding Gradients Problems

正如我們在第10章中所討論的那樣,反向傳播算法的工作原理是從輸出層到輸入層,傳播錯誤梯度。 一旦該算法已經計算了網絡中每個參數的損失函數的梯度,它就使用這些梯度來用梯度下降步驟來更新每個參數。

不幸的是,梯度往往變得越來越小,隨着算法進展到下層。 結果,梯度下降更新使得低層連接權重實際上保持不變,並且訓練永遠不會收斂到良好的解決方案。 這被稱爲消失梯度問題。 在某些情況下,可能會發生相反的情況:梯度可能變得越來越大,許多分層得到瘋狂的權重更新,算法發散。 這是梯度爆炸的問題,這在迴歸神經網絡中最常見(見第14章)。 更一般地說,深度神經網絡遭受不穩定的梯度; 不同的層次可能以不同的速度學習。

雖然這種不幸的行爲已經經過了相當長的一段時間的實驗觀察(這是造成深度神經網絡大部分時間都被拋棄的原因之一),但在2010年左右,人們纔有了明顯的進步。 Xavier Glorot和Yoshua Bengio發表的題爲“Understanding the Difficulty of Training Deep Feedforward Neural Networks”的論文發現了一些疑問,包括流行的sigmoid激活函數和當時最受歡迎的權重初始化技術的組合,即隨機初始化時使用平均值爲0,標準偏差爲1的正態分佈。簡而言之,他們表明,用這個激活函數和這個初始化方案,每層輸出的方差遠大於其輸入的方差。在網絡中前進,每層之後的變化持續增加,直到激活函數飽和在頂層。這實際上是因爲對數函數的平均值爲0.5而不是0(雙曲正切函數的平均值爲0,表現略好於深層網絡中的邏輯函數)

看一下sigmoid激活函數(參見圖11-1),可以看到當輸入變大(負或正)時,函數飽和在0或1,導數非常接近0.因此,當反向傳播開始時, 它幾乎沒有梯度通過網絡傳播回來,而且由於反向傳播通過頂層向下傳遞,所以存在的小梯度不斷地被稀釋,因此下層確實沒有任何東西可用。


Glorot和Bengio在他們的論文中提出了一種顯着緩解這個問題的方法。 我們需要信號在兩個方向上正確地流動:在進行預測時是正向的,在反向傳播梯度時是反向的。 我們不希望信號消失,也不希望它爆炸並飽和。 爲了使信號正確流動,作者認爲,我們需要每層輸出的方差等於其輸入的方差.(這裏有一個比喻:如果將麥克風放大器的旋鈕設置得太接近於零,人們聽不到聲音,但是如果將麥克風放大器設置得太接近麥克風,聲音就會飽和,人們不會理解你在說什麼。 現在想象一下這樣一個放大器的鏈條:它們都需要正確設置,以便在鏈條的末端響亮而清晰地發出聲音。 你的聲音必須以每個放大器的振幅相同的幅度出來。)而且我們也需要梯度在相反方向上流過一層之前和之後有相同的方差(如果您對數學細節感興趣,請查閱論文)。實際上不可能保證兩者都是一樣的,除非這個層具有相同數量的輸入和輸出連接,但是他們提出了一個很好的折衷辦法,在實踐中證明這個折中辦法非常好:隨機初始化連接權重必須如公式11-1所描述的那樣.其中n_inputs和n_outputs是權重正在被初始化的層(也稱爲扇入和扇出)的輸入和輸出連接的數量。 這種初始化策略通常被稱爲Xavier初始化(在作者的名字之後),或者有時是Glorot初始化。


當輸入連接的數量大致等於輸出連接的數量時,可以得到更簡單的等式我們在第10章中使用了這個簡化的策略。

使用Xavier初始化策略可以大大加快訓練速度,這是導致Deep Learning目前取得成功的技巧之一。 最近的一些論文針對不同的激活函數提供了類似的策略,如表11-1所示。 ReLU激活函數(及其變體,包括簡稱ELU激活)的初始化策略有時稱爲He初始化(在其作者的姓氏之後)。


默認情況下,fully_connected()函數(在第10章中介紹)使用Xavier初始化(具有統一的分佈)。 你可以通過使用如下所示的variance_scaling_initializer()函數來將其更改爲He初始化:

注意:本書使用tensorflow.contrib.layers.fully_connected()而不是tf.layers.dense()(本章編寫時不存在)。 現在最好使用tf.layers.dense(),因爲contrib模塊中的任何內容可能會更改或刪除,恕不另行通知。 dense()函數幾乎與fully_connected()函數完全相同。 與本章有關的主要差異是:
幾個參數被重新命名:範圍變成名字,activation_fn變成激活(類似地,_fn後綴從諸如normalizer_fn之類的其他參數中移除),weights_initializer變成kernel_initializer等等。默認激活現在是None,而不是tf.nn.relu。 它不支持tensorflow.contrib.framework.arg_scope()(稍後在第11章中介紹)。 它不支持正規化的參數(稍後在第11章介紹)。

 

he_init = tf.contrib.layers.variance_scaling_initializer()
hidden1 = tf.layers.dense(X, n_hidden1, activation=tf.nn.relu,
                          kernel_initializer=he_init, name="hidden1")

他的初始化只考慮了扇入,而不是像Xavier初始化那樣扇入和扇出之間的平均值。 這也是variance_scaling_initializer()函數的默認值,但您可以通過設置參數mode =“FAN_AVG”來更改它。


Nonsaturating Activation Functions

Glorot和Bengio在2010年的論文中的一個見解是,消失/爆炸的梯度問題部分是由於激活函數的選擇不好造成的。 在那之前,大多數人都認爲,如果大自然選擇在生物神經元中使用sigmoid激活函數,它們必定是一個很好的選擇。 但事實證明,其他激活函數在深度神經網絡中表現得更好,特別是ReLU激活函數,主要是因爲它對正值不會飽和(也因爲這樣所以計算速度很快)。

不幸的是,ReLU激活功能並不完美。 它有一個被稱爲死亡ReLUs的問題:在訓練過程中,一些神經元有效地死亡,意味着它們停止輸出0以外的任何東西。在某些情況下,你可能會發現你網絡的一半神經元已經死亡,特別是如果你使用大 學習率。 在訓練期間,如果神經元的權重得到更新,使得神經元輸入的加權和爲負,則它將開始輸出0.當發生這種情況時,由於ReLU函數的梯度爲0時,神經元不可能恢復生命當其輸入爲負。

爲了解決這個問題,你可能需要使用ReLU函數的一個變體,比如leaky ReLU。這個函數定義爲LeakyReLUα(z)= max(αz,z)(見圖11-2)。超參數α定義了函數“leaks”的程度:它是z <0的函數的斜率,通常設置爲0.01。這個小斜坡確保leaky ReLUs永不死亡;他們可能會長期昏迷,但他們有機會最終醒來。最近的一篇論文比較了幾種ReLU激活功能的變體,其中一個結論是leaky Relu總是優於嚴格的ReLU激活函數。事實上,設定α= 0.2(巨大leak)似乎導致比α= 0.01(小leak)更好的性能。他們還評估了隨機leakt ReLU(RReLU),其中α在訓練期間在給定範圍內隨機挑選,並在測試期間固定爲平均值。它表現相當好,似乎是一個正規化者(減少訓練集的過擬合風險)。最後,他們還評估了參數 leaky ReLU(PReLU),其中α被授權在訓練期間被學習(而不是超參數,它變成可以像任何其他參數一樣被反向傳播修改的參數)。據報道這在大型圖像數據集上的表現強於ReLU,但是對於較小的數據集,其具有過度擬合訓練集的風險。


最後但並非最不重要的一點是,Djork-ArnéClevert等人在2015年的一篇論文中提出了一種稱爲指數線性單元 exponential linear unit  (ELU)的新的激活函數,在他們的實驗中表現優於所有的ReLU變體:訓練時間減少,神經網絡在測試集上表現的更好。 如圖11-3所示,公式11-2給出了它的定義。


它看起來很像ReLU功能,但有一些區別,主要區別在於:

  • 首先它在z <0時取負值,這使得該單元的平均輸出接近於0.這有助於減輕消失梯度問題,如前所述。 超參數α定義當z是一個大的負數時,ELU函數接近的值。 它通常設置爲1,但是如果你願意,你可以像調整其他超參數一樣調整它。
  • 其次,它對z <0有一個非零的梯度,避免了神經元死亡的問題.
  • 第三,函數在任何地方都是平滑的,包括z = 0左右,這有助於加速梯度下降,因爲它不會像z = 0的左邊和右邊那樣反彈。
ELU激活函數的主要缺點是計算速度慢於ReLU及其變體(由於使用指數函數),但是在訓練過程中,這是通過更快的收斂速度來補償的。 然而,在測試時間,ELU網絡將比ReLU網絡慢。
那麼你應該使用哪個激活函數來處理深層神經網絡的隱藏層? 雖然你的里程會有所不同,一般ELU>leaky ReLU(及其變體)> ReLU> tanh>sigmoid。 如果您關心運行時性能,那麼您可能更喜歡ELU相對 leaky ReLU。 如果你不想調整另一個超參數,你可以使用前面提到的默認的α值(leaky ReLU爲0.01,ELU爲1)。 如果您有充足的時間和計算能力,您可以使用交叉驗證來評估其他激活功能,特別是如果您的神經網絡過擬合,則爲RReLU;如果您擁有龐大的訓練組,則爲PReLU。
TensorFlow提供了一個可以用來建立神經網絡的elu()函數。 調用fully_connected()函數時,只需設置activation_fn參數即可:
hidden1 = tf.layers.dense(X, n_hidden1, activation=tf.nn.elu, name="hidden1")
TensorFlow沒有針對leaky ReLU的預定義函數,但是很容易定義:
def leaky_relu(z, name=None):
    return tf.maximum(0.01 * z, z, name=name)

hidden1 = tf.layers.dense(X, n_hidden1, activation=leaky_relu, name="hidden1")

Batch Normalization
儘管使用HE初始化和ELU(或任何ReLU變體)可以顯着減少訓練開始階段的消失/爆炸梯度問題,但不保證在訓練期間問題不會回來。
在2015年的一篇論文中,Sergey Ioffe和Christian Szegedy提出了一種稱爲批量標準化(Batch Normalization,BN)的技術來解決消失/爆炸梯度問題,更普遍的問題是當之前的層次改變,每個層次輸入的分佈會在訓練過程中發生變化(他們稱之爲內部協變量問題)。
該技術包括在每層的激活函數之前在模型中添加操作,簡單地對輸入進行零中心和歸一化,然後使用每個層的兩個新參數(一個用於縮放,另一個用於移位)對結果進行縮放和移位。 換句話說,這個操作可以讓模型學習到每層輸入值的最佳尺度,均值。
爲了對輸入進行歸零和歸一化,算法需要估計輸入的均值和標準差。 它通過評估當前小批量輸入的均值和標準偏差(因此命名爲“批量標準化”)來實現。 整個操作在方程11-3中。


μB 是整個小批量B的經驗均值

σB 是經驗性的標準差,也是來評估整個小批量的。

mB 是小批量中的實例數量。

是以爲零中心和標準化的輸入。

γ 是層的縮放參數。

β是層的移動參數(偏移量)

ε是一個很小的數字,以避免被零除(通常爲10 ^ -3)。 這被稱爲平滑術語(拉布拉斯平滑Laplace Smoothing)。

是BN操作的輸出:它是輸入的縮放和移位版本。

在測試時,沒有小批量計算經驗均值和標準差,所以您只需使用整個訓練集的均值和標準差。 這些通常在訓練期間使用移動平均值進行有效計算。 因此,總的來說,每個批次標準化的層次都學習了四個參數:γ(標度),β(偏移),μ(平均值)和σ(標準偏差)。

作者證明,這項技術大大改善了他們試驗的所有深度神經網絡。消失梯度問題大大減少了,他們可以使用飽和激活函數,如tanh甚至sigmoid激活函數。網絡對權重初始化也不那麼敏感。他們能夠使用更大的學習速度,顯着加快了學習過程。具體地,他們指出,“適用於一個國家的最先進的圖像分類模型,批標準化實現了與14倍更少的訓練步驟相同的精度,和以顯著的優勢擊敗了原始模型。 [...]使用批量歸一化的網絡集合,我們改進了ImageNet分類上的最佳公佈結果:達到4.9%的前5個驗證錯誤(和4.8%的測試錯誤),超出了人類評估者的準確性。批量標準化也像一個正規化者一樣,減少了對其他正則化技術(如本章稍後描述的dropout).

但是,批量標準化確實增加了模型的一些複雜性(儘管它消除了對輸入數據進行標準化的需要,因爲如果批的標準化處理,第一個隱藏層將處理這個問題)。 此外,還存在運行時間的損失:由於每層所需的額外計算,神經網絡的預測速度較慢。 所以,如果你需要預測閃電般快速,你可能想要檢查普通ELU + He初始化執行之前如何執行批量規範化。

您可能會發現,訓練起初相當緩慢,而漸變下降正在尋找每層的最佳尺度和偏移量,但一旦找到合理的好值,它就會加速。


Implementing Batch Normalization with TensorFlow
TensorFlow提供了一個batch_normalization()函數,它簡單地對輸入進行居中和標準化,但是您必須自己計算平均值和標準偏差(基於訓練期間的小批量數據或測試過程中的完整數據集) 作爲這個函數的參數,並且還必須處理縮放和偏移量參數的創建(並將它們傳遞給此函數)。 這是可行的,但不是最方便的方法。 相反,你應該使用batch_norm()函數,它爲你處理所有這些。 您可以直接調用它,或者告訴fully_connected()函數使用它,如下面的代碼所示:

注意:本書使用tensorflow.contrib.layers.batch_norm()而不是tf.layers.batch_normalization()(本章寫作時不存在)。 現在最好使用tf.layers.batch_normalization(),因爲contrib模塊中的任何內容都可能會改變或被刪除,恕不另行通知。 我們現在不使用batch_norm()函數作爲fully_connected()函數的正則化參數,而是使用batch_normalization(),並明確地創建一個不同的層。 參數有些不同,特別是:

  • decay更名爲momentum
  • is_training被重命名爲training
  • updates_collections被刪除:批量標準化所需的更新操作被添加到UPDATE_OPS集合中,並且您需要在訓練期間明確地運行這些操作(請參閱下面的執行階段)
  • 我們不需要指定scale = True,因爲這是默認值。
還要注意,爲了在每個隱藏層激活函數之前運行batch norm,我們手動應用RELU激活函數,在批量規範層之後。
注意:由於tf.layers.dense()函數與本書中使用的tf.contrib.layers.arg_scope()不兼容,我們現在使用python的functools.partial()函數。 它可以很容易地創建一個my_dense_layer()函數,只需調用tf.layers.dense(),並自動設置所需的參數(除非在調用my_dense_layer()時覆蓋它們)。 如您所見,代碼保持非常相似。

import tensorflow as tf

n_inputs = 28 * 28
n_hidden1 = 300
n_hidden2 = 100
n_outputs = 10

X = tf.placeholder(tf.float32, shape=(None, n_inputs), name="X")

training = tf.placeholder_with_default(False, shape=(), name='training')

hidden1 = tf.layers.dense(X, n_hidden1, name="hidden1")
bn1 = tf.layers.batch_normalization(hidden1, training=training, momentum=0.9)
bn1_act = tf.nn.elu(bn1)

hidden2 = tf.layers.dense(bn1_act, n_hidden2, name="hidden2")
bn2 = tf.layers.batch_normalization(hidden2, training=training, momentum=0.9)
bn2_act = tf.nn.elu(bn2)

logits_before_bn = tf.layers.dense(bn2_act, n_outputs, name="outputs")
logits = tf.layers.batch_normalization(logits_before_bn, training=training,
                                       momentum=0.9)
X = tf.placeholder(tf.float32, shape=(None, n_inputs), name="X")
training = tf.placeholder_with_default(False, shape=(), name='training')
爲了避免一遍又一遍重複相同的參數,我們可以使用Python的partial()函數:
from functools import partial

my_batch_norm_layer = partial(tf.layers.batch_normalization,
                              training=training, momentum=0.9)

hidden1 = tf.layers.dense(X, n_hidden1, name="hidden1")
bn1 = my_batch_norm_layer(hidden1)
bn1_act = tf.nn.elu(bn1)
hidden2 = tf.layers.dense(bn1_act, n_hidden2, name="hidden2")
bn2 = my_batch_norm_layer(hidden2)
bn2_act = tf.nn.elu(bn2)
logits_before_bn = tf.layers.dense(bn2_act, n_outputs, name="outputs")
logits = my_batch_norm_layer(logits_before_bn)

完整代碼
from functools import partial
from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf



if __name__ == '__main__':
    n_inputs = 28 * 28
    n_hidden1 = 300
    n_hidden2 = 100
    n_outputs = 10



    mnist = input_data.read_data_sets("/tmp/data/")

    batch_norm_momentum = 0.9
    learning_rate = 0.01

    X = tf.placeholder(tf.float32, shape=(None, n_inputs), name = 'X')
    y = tf.placeholder(tf.int64, shape=None, name = 'y')
    training = tf.placeholder_with_default(False, shape=(), name = 'training')#給Batch norm加一個placeholder

    with tf.name_scope("dnn"):
        he_init = tf.contrib.layers.variance_scaling_initializer()
        #對權重的初始化

        my_batch_norm_layer = partial(
            tf.layers.batch_normalization,
            training = training,
            momentum = batch_norm_momentum
        )

        my_dense_layer = partial(
            tf.layers.dense,
            kernel_initializer = he_init
        )

        hidden1 = my_dense_layer(X ,n_hidden1 ,name = 'hidden1')
        bn1 = tf.nn.elu(my_batch_norm_layer(hidden1))
        hidden2 = my_dense_layer(bn1, n_hidden2, name = 'hidden2')
        bn2 = tf.nn.elu(my_batch_norm_layer(hidden2))
        logists_before_bn = my_dense_layer(bn2, n_outputs, name = 'outputs')
        logists = my_batch_norm_layer(logists_before_bn)

    with tf.name_scope('loss'):
        xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels = y, logits= logists)
        loss = tf.reduce_mean(xentropy, name = 'loss')

    with tf.name_scope('train'):
        optimizer = tf.train.GradientDescentOptimizer(learning_rate)
        training_op = optimizer.minimize(loss)

    with tf.name_scope("eval"):
        correct = tf.nn.in_top_k(logists, y, 1)
        accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))

    init = tf.global_variables_initializer()
    saver = tf.train.Saver()

    n_epoches = 20
    batch_size = 200
#注意:由於我們使用的是tf.layers.batch_normalization()而不是tf.contrib.layers.batch_norm()(如本書所述),
#所以我們需要明確運行批量規範化所需的額外更新操作(sess.run([ training_op,extra_update_ops],...)。
    extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)

    with tf.Session() as sess:
        init.run()
        for epoch in range(n_epoches):
            for iteraton in range(mnist.train.num_examples//batch_size):
                X_batch, y_batch = mnist.train.next_batch(batch_size)
                sess.run([training_op,extra_update_ops],
                         feed_dict={training:True, X:X_batch, y:y_batch})
            accuracy_val = accuracy.eval(feed_dict= {X:mnist.test.images,
                                                    y:mnist.test.labels})
            print(epoch, 'Test accuracy:', accuracy_val)


什麼!? 這對MNIST來說不是一個很好的準確性。 當然,如果你訓練的時間越長,準確性就越好,但是由於這樣一個淺的網絡,Batch Norm和ELU不太可能產生非常積極的影響:它們大部分都是爲了更深的網絡而發光。
請注意,您還可以訓練操作取決於更新操作:
with tf.name_scope("train"):
    optimizer = tf.train.GradientDescentOptimizer(learning_rate)
    extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
    with tf.control_dependencies(extra_update_ops):
        training_op = optimizer.minimize(loss)
這樣,你只需要在訓練過程中評估training_op,TensorFlow也會自動運行更新操作:
sess.run(training_op, feed_dict={training: True, X: X_batch, y: y_batch})

Gradient Clipping (梯度剪裁)
減少爆炸梯度問題的一種常用技術是在反向傳播過程中簡單地剪切梯度,使它們不超過某個閾值(這對於遞歸神經網絡是非常有用的;參見第14章)。 這就是所謂的漸變剪裁。一般來說,人們更喜歡批量標準化,但瞭解漸變剪裁以及如何實現它仍然是有用的。

在TensorFlow中,優化器的minimize()函數負責計算梯度並應用它們,所以您必須首先調用優化器的compute_gradients()方法,然後使用clip_by_value()函數創建一個剪輯梯度的操作,最後 創建一個操作來使用優化器的apply_gradients()方法應用剪切梯度:

threshold = 1.0

optimizer = tf.train.GradientDescentOptimizer(learning_rate)
grads_and_vars = optimizer.compute_gradients(loss)
capped_gvs = [(tf.clip_by_value(grad, -threshold, threshold), var)
              for grad, var in grads_and_vars]
training_op = optimizer.apply_gradients(capped_gvs)
像往常一樣,您將在每個訓練階段運行這個training_op。 它將計算梯度,將它們夾在-1.0和1.0之間,並應用它們。 threhold是您可以調整的超參數。




發佈了77 篇原創文章 · 獲贊 25 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章