第 10 章介紹了人工神經網絡,并訓練了我們的第一個深度神經網絡。 但它是一個非常淺的 DNN,隻有兩個隐藏層。 如果你需要解決非常複雜的問題,例如檢測高分辨率圖像中的數百種類型的對象,該怎麼辦? 你可能需要訓練更深的 DNN,也許有 10 層,每層包含數百個神經元,通過數十萬個連接配接來連接配接。 這不會是閑庭信步:
首先,你将面臨棘手的梯度消失問題(或相關的梯度爆炸問題),這會影響深度神經網絡,并使較低層難以訓練。


第三,具有數百萬參數的模型将會有嚴重的過拟合訓練集的風險。
在本章中,我們将依次讨論這些問題,并提出解決問題的技巧。 我們将從解釋梯度消失問題開始,并探讨解決這個問題的一些最流行的解決方案。 接下來我們将看看各種優化器,與普通梯度下降相比,它們可以加速大型模型的訓練。 最後,我們将浏覽一些流行的大型神經網絡正則化技術。
使用這些工具,你将能夠訓練非常深的網絡:歡迎來到深度學習的世界!
梯度消失/爆炸問題
正如我們在第 10 章中所讨論的那樣,反向傳播算法的工作原理是從輸出層到輸入層,傳播誤差的梯度。 一旦該算法已經計算了網絡中每個參數的損失函數的梯度,它就使用這些梯度來用梯度下降步驟來更新每個參數。
不幸的是,梯度往往變得越來越小,随着算法進展到較低層。 結果,梯度下降更新使得低層連接配接權重實際上保持不變,并且訓練永遠不會收斂到良好的解決方案。 這被稱為梯度消失問題。 在某些情況下,可能會發生相反的情況:梯度可能變得越來越大,許多層得到了非常大的權重更新,算法發散。這是梯度爆炸的問題,在循環神經網絡中最為常見(見第 14 章)。 更一般地說,深度神經網絡受梯度不穩定之苦; 不同的層次可能以非常不同的速度學習。
雖然這種不幸的行為已經經過了相當長的一段時間的實驗觀察(這是造成深度神經網絡大部分時間都被抛棄的原因之一),但直到 2010 年左右,人們才有了明顯的進步。 Xavier Glorot 和 Yoshua Bengio 發表的題為《Understanding the Difficulty of Training Deep Feedforward Neural Networks》的論文發現了一些疑問,包括流行的 sigmoid 激活函數和當時最受歡迎的權重初始化技術的組合,即随機初始化時使用平均值為 0,标準差為 1 的正态分布。簡而言之,他們表明,用這個激活函數和這個初始化方案,每層輸出的方差遠大于其輸入的方差。網絡正向,每層的方差持續增加,直到激活函數在頂層飽和。這實際上是因為logistic函數的平均值為 0.5 而不是 0(雙曲正切函數的平均值為 0,表現略好于深層網絡中的logistic函數)
看一下logistic 激活函數(參見圖 11-1),可以看到當輸入變大(負或正)時,函數飽和在 0 或 1,導數非常接近 0。是以,當反向傳播開始時, 它幾乎沒有梯度通過網絡傳播回來,而且由于反向傳播通過頂層向下傳遞,是以存在的小梯度不斷地被稀釋,是以較低層确實沒有任何東西可用。
Glorot 和 Bengio 在他們的論文中提出了一種顯著緩解這個問題的方法。 我們需要信号在兩個方向上正确地流動:在進行預測時是正向的,在反向傳播梯度時是反向的。 我們不希望信号消失,也不希望它爆炸并飽和。 為了使信号正确流動,作者認為,我們需要每層輸出的方差等于其輸入的方差。(這裡有一個比喻:如果将麥克風放大器的旋鈕設定得太接近于零,人們聽不到聲音,但是如果将麥克風放大器設定得太大,聲音就會飽和,人們就會聽不懂你在說什麼。 現在想象一下這樣一個放大器的鍊條:它們都需要正确設定,以便在鍊條的末端響亮而清晰地發出聲音。 你的聲音必須以每個放大器的振幅相同的幅度出來。)而且我們也需要梯度在相反方向上流過一層之前和之後有相同的方差(如果您對數學細節感興趣,請查閱論文)。實際上不可能保證兩者都是一樣的,除非這個層具有相同數量的輸入和輸出連接配接,但是他們提出了一個很好的折衷辦法,在實踐中證明這個折中辦法非常好:随機初始化連接配接權重必須如公式 11-1 所描述的那樣。其中
n_inputs
和
n_outputs
是權重正在被初始化的層(也稱為扇入和扇出)的輸入和輸出連接配接的數量。 這種初始化政策通常被稱為Xavier初始化(在作者的名字之後),或者有時是 Glorot 初始化。
當輸入連接配接的數量大緻等于輸出連接配接的數量時,可以得到更簡單的等式
我們在第 10 章中使用了這個簡化的政策
使用 Xavier 初始化政策可以大大加快訓練速度,這是導緻深度學習目前取得成功的技巧之一。 最近的一些論文針對不同的激活函數提供了類似的政策,如表 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")
He 初始化隻考慮了扇入,而不是像 Xavier 初始化那樣扇入和扇出之間的平均值。 這也是
variance_scaling_initializer()
函數的預設值,但您可以通過設定參數
mode ="FAN_AVG"
來更改它。
非飽和激活函數
Glorot 和 Bengio 在 2010 年的論文中的一個見解是,消失/爆炸的梯度問題部分是由于激活函數的選擇不好造成的。 在那之前,大多數人都認為,如果大自然選擇在生物神經元中使用 sigmoid 激活函數,它們必定是一個很好的選擇。 但事實證明,其他激活函數在深度神經網絡中表現得更好,特别是 ReLU 激活函數,主要是因為它對正值不會飽和(也因為它的計算速度很快)。
不幸的是,ReLU激活功能并不完美。 它有一個被稱為 “ReLU 死區” 的問題:在訓練過程中,一些神經元有效地死亡,意味着它們停止輸出 0 以外的任何東西。在某些情況下,你可能會發現你網絡的一半神經元已經死亡,特别是如果你使用大學習率。 在訓練期間,如果神經元的權重得到更新,使得神經元輸入的權重和為負,則它将開始輸出 0 。當這種情況發生時,由于當輸入為負時,ReLU函數的梯度為0,神經元不可能恢複生機。
為了解決這個問題,你可能需要使用 ReLU 函數的一個變體,比如 leaky ReLU。這個函數定義為
LeakyReLUα(z)= max(αz,z)
(見圖 11-2)。超參數
α
定義了函數“leaks”的程度:它是
z < 0
時函數的斜率,通常設定為 0.01。這個小斜坡確定 leaky ReLU 永不死亡;他們可能會長期昏迷,但他們有機會最終醒來。最近的一篇論文比較了幾種 ReLU 激活功能的變體,其中一個結論是 leaky Relu 總是優于嚴格的 ReLU 激活函數。事實上,設定
α= 0.2
(巨大 leak)似乎導緻比
α= 0.01
(小 leak)更好的性能。他們還評估了随機化 leaky 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。 如果您關心運作時性能,那麼您可能喜歡 leaky ReLU超過ELU。 如果你不想調整另一個超參數,你可以使用前面提到的預設的
α
值(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")
批量标準化
盡管使用 He初始化和 ELU(或任何 ReLU 變體)可以顯著減少訓練開始階段的梯度消失/爆炸問題,但不保證在訓練期間問題不會回來。
在 2015 年的一篇論文中,Sergey Ioffe 和 Christian Szegedy 提出了一種稱為批量标準化(Batch Normalization,BN)的技術來解決梯度消失/爆炸問題,每層輸入的分布在訓練期間改變的問題,更普遍的問題是目前一層的參數改變,每層輸入的分布會在訓練過程中發生變化(他們稱之為内部協變量偏移問題)。
該技術包括在每層的激活函數之前在模型中添加操作,簡單地對輸入進行zero-centering和規範化,然後每層使用兩個新參數(一個用于尺度變換,另一個用于偏移)對結果進行尺度變換和偏移。 換句話說,這個操作可以讓模型學習到每層輸入值的最佳尺度,均值。為了對輸入進行歸零和歸一化,算法需要估計輸入的均值和标準差。 它通過評估目前小批量輸入的均值和标準差(是以命名為“批量标準化”)來實作。 整個操作在方程 11-3 中。
在測試時,沒有小批量計算經驗均值和标準差,是以您隻需使用整個訓練集的均值和标準差。 這些通常在訓練期間使用移動平均值進行有效計算。 是以,總的來說,每個批次标準化的層次都學習了四個參數:
γ
(标度),
β
(偏移),
μ
(平均值)和
σ
(标準差)。
作者證明,這項技術大大改善了他們試驗的所有深度神經網絡。梯度消失問題大大減少了,他們可以使用飽和激活函數,如 tanh 甚至 sigmoid 激活函數。網絡對權重初始化也不那麼敏感。他們能夠使用更大的學習率,顯著加快了學習過程。具體地,他們指出,“應用于最先進的圖像分類模型,批标準化用少了 14 倍的訓練步驟實作了相同的精度,以顯著的優勢擊敗了原始模型。[...] 使用批量标準化的網絡集合,我們改進了 ImageNet 分類上的最佳公布結果:達到4.9% 的前5個驗證錯誤(和 4.8% 的測試錯誤),超出了人類評估者的準确性。批量标準化也像一個正則化項一樣,減少了對其他正則化技術的需求(如本章稍後描述的 dropout).
然而,批量标準化的确會增加模型的複雜性(盡管它不需要對輸入資料進行标準化,因為第一個隐藏層會照顧到這一點,隻要它是批量标準化的)。 此外,還存在運作時間的損失:由于每層所需的額外計算,神經網絡的預測速度較慢。 是以,如果你需要預測閃電般快速,你可能想要檢查普通ELU + He初始化執行之前如何執行批量标準化。
您可能會發現,訓練起初相當緩慢,而漸變下降正在尋找每層的最佳尺度和偏移量,但一旦找到合理的好值,它就會加速。
使用 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()
,并明确地建立一個不同的層。 參數有些不同,特别是:
【翻譯】Sklearn 與 TensorFlow 機器學習實用指南 —— 第11章 訓練深層神經網絡(上)
decay
更名為
momentum
【翻譯】Sklearn 與 TensorFlow 機器學習實用指南 —— 第11章 訓練深層神經網絡(上)
is_training
被重命名為
training
【翻譯】Sklearn 與 TensorFlow 機器學習實用指南 —— 第11章 訓練深層神經網絡(上)
updates_collections
被删除:批量标準化所需的更新操作被添加到
UPDATE_OPS
集合中,并且您需要在訓練期間明确地運作這些操作(請參閱下面的執行階段)

我們不需要指定
scale = True
,因為這是預設值。
還要注意,為了在每個隐藏層激活函數之前運作批量标準化,我們手動應用 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 來說不是一個很好的準确性。 當然,如果你訓練的時間越長,準确性就越好,但是由于這樣一個淺的網絡,批量範數和 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})
原文釋出時間為:2018-06-23
本文作者:ApacheCN【翻譯】
本文來自雲栖社群合作夥伴“
Python愛好者社群”,了解相關資訊可以關注“
”。