《Scikit-Learn與TensorFlow機器學習實用指南》 第11章 訓練深度神經網絡(上)


(第一部分 機器學習基礎)
第01章 機器學習概覽
第02章 一個完整的機器學習項目(上)
第02章 一個完整的機器學習項目(下)
第03章 分類
第04章 訓練模型
第05章 支持向量機
第06章 決策樹
第07章 集成學習和隨機森林
第08章 降維
(第二部分 神經網絡和深度學習)
第9章 啟動和運行TensorFlow
第10章 人工神經網絡
第11章 訓練深度神經網絡(上)
第11章 訓練深度神經網絡(下)
第12章 設備和服務器上的分布式 TensorFlow
第13章 卷積神經網絡
第14章 循環神經網絡
第15章 自編碼器
第16章 強化學習(上)
第16章 強化學習(下)


第 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。因此,當反向傳播開始時, 它幾乎沒有梯度通過網絡傳播回來,而且由于反向傳播通過頂層向下傳遞,所以存在的小梯度不斷地被稀釋,因此較低層確實沒有任何東西可用。

圖11-1 邏輯激活函數飽和

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

公式11-1 Xavier初始化(使用邏輯激活函數)

當輸入連接的數量大致等于輸出連接的數量時,可以得到更簡單的等式

我們在第 10 章中使用了這個簡化的策略。

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

表11-1 每種類型激活函數的初始化參數

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

he_init = tf.contrib.layers.variance_scaling_initializer()
hidden1 = fully_connected(X, n_hidden1, weights_initializer=he_init, scope="h1")

注意:本書使用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,但是對于較小的數據集,其具有過度擬合訓練集的風險。

圖11-2 Leaky ReLU 帶泄露整流函數

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

公式11-2 ELU激活函數

它看起來很像 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 中。

公式11-3 批量標準化算法
  • μB是整個小批量B的經驗均值

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

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

  • Xi是以為零中心和標準化的輸入。

  • γ是層的縮放參數。

  • β是層的便宜參數(偏移量)

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

  • z(i) 是BN操作的輸出:它是輸入的縮放和移位版本。

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

作者證明,這項技術大大改善了他們試驗的所有深度神經網絡。梯度消失問題大大減少了,他們可以使用飽和激活函數,如 tanh 甚至邏輯激活函數。網絡對權重初始化也不那么敏感。他們能夠使用更大的學習率,顯著加快了學習過程。具體地,他們指出,“應用于最先進的圖像分類模型,批標準減少了 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(),并明確地創建一個不同的層。 參數有些不同,特別是:

  • decay更名為momentum
  • is_training被重命名為training
  • 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})

梯度裁剪

減少梯度爆炸問題的一種常用技術是在反向傳播過程中簡單地剪切梯度,使它們不超過某個閾值(這對于遞歸神經網絡是非常有用的;參見第 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是您可以調整的超參數。

復用預訓練層

從零開始訓練一個非常大的 DNN 通常不是一個好主意,相反,您應該總是嘗試找到一個現有的神經網絡來完成與您正在嘗試解決的任務類似的任務,然后復用這個網絡的較低層:這就是所謂的遷移學習。這不僅會大大加快訓練速度,還將需要更少的訓練數據。

例如,假設您可以訪問經過訓練的 DNN,將圖片分為 100 個不同的類別,包括動物,植物,車輛和日常物品。 您現在想要訓練一個 DNN 來對特定類型的車輛進行分類。 這些任務非常相似,因此您應該嘗試重新使用第一個網絡的一部分(請參見圖 11-4)。

圖11-4 復用預訓練層

如果新任務的輸入圖像與原始任務中使用的輸入圖像的大小不一致,則必須添加預處理步驟以將其大小調整為原始模型的預期大小。 更一般地說,如果輸入具有類似的低級層次的特征,則遷移學習將很好地工作。

復用 TensorFlow 模型

如果原始模型使用 TensorFlow 進行訓練,則可以簡單地將其恢復并在新任務上進行訓練:

[...] # construct the original model 

with tf.Session() as sess:
    saver.restore(sess, "./my_model_final.ckpt")
    # continue training the model...

完整代碼:

n_inputs = 28 * 28  # MNIST
n_hidden1 = 300
n_hidden2 = 50
n_hidden3 = 50
n_hidden4 = 50
n_outputs = 10

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

with tf.name_scope("dnn"):
    hidden1 = tf.layers.dense(X, n_hidden1, activation=tf.nn.relu, name="hidden1")
    hidden2 = tf.layers.dense(hidden1, n_hidden2, activation=tf.nn.relu, name="hidden2")
    hidden3 = tf.layers.dense(hidden2, n_hidden3, activation=tf.nn.relu, name="hidden3")
    hidden4 = tf.layers.dense(hidden3, n_hidden4, activation=tf.nn.relu, name="hidden4")
    hidden5 = tf.layers.dense(hidden4, n_hidden5, activation=tf.nn.relu, name="hidden5")
    logits = tf.layers.dense(hidden5, n_outputs, name="outputs")

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

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

learning_rate = 0.01
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)

init = tf.global_variables_initializer()
saver = tf.train.Saver()
with tf.Session() as sess:
    saver.restore(sess, "./my_model_final.ckpt")

    for epoch in range(n_epochs):
        for iteration in range(mnist.train.num_examples // batch_size):
            X_batch, y_batch = mnist.train.next_batch(batch_size)
            sess.run(training_op, feed_dict={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)

    save_path = saver.save(sess, "./my_new_model_final.ckpt")

但是,一般情況下,您只需要重新使用原始模型的一部分(就像我們將要討論的那樣)。 一個簡單的解決方案是將Saver配置為僅恢復原始模型中的一部分變量。 例如,下面的代碼只恢復隱藏層1,2和3:

n_inputs = 28 * 28  # MNIST
n_hidden1 = 300 # reused
n_hidden2 = 50  # reused
n_hidden3 = 50  # reused
n_hidden4 = 20  # new!
n_outputs = 10  # new!

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

with tf.name_scope("dnn"):
    hidden1 = tf.layers.dense(X, n_hidden1, activation=tf.nn.relu, name="hidden1")       # reused
    hidden2 = tf.layers.dense(hidden1, n_hidden2, activation=tf.nn.relu, name="hidden2") # reused
    hidden3 = tf.layers.dense(hidden2, n_hidden3, activation=tf.nn.relu, name="hidden3") # reused
    hidden4 = tf.layers.dense(hidden3, n_hidden4, activation=tf.nn.relu, name="hidden4") # new!
    logits = tf.layers.dense(hidden4, n_outputs, name="outputs")                         # new!

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

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

with tf.name_scope("train"):
    optimizer = tf.train.GradientDescentOptimizer(learning_rate)
    training_op = optimizer.minimize(loss)
[...] # build new model with the same definition as before for hidden layers 1-3 

reuse_vars = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES,
                               scope="hidden[123]") # regular expression
reuse_vars_dict = dict([(var.op.name, var) for var in reuse_vars])
restore_saver = tf.train.Saver(reuse_vars_dict) # to restore layers 1-3

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

with tf.Session() as sess:
    init.run()
    restore_saver.restore(sess, "./my_model_final.ckpt")

    for epoch in range(n_epochs):                                      # not shown in the book
        for iteration in range(mnist.train.num_examples // batch_size): # not shown
            X_batch, y_batch = mnist.train.next_batch(batch_size)      # not shown
            sess.run(training_op, feed_dict={X: X_batch, y: y_batch})  # not shown
        accuracy_val = accuracy.eval(feed_dict={X: mnist.test.images,  # not shown
                                                y: mnist.test.labels}) # not shown
        print(epoch, "Test accuracy:", accuracy_val)                   # not shown

    save_path = saver.save(sess, "./my_new_model_final.ckpt")

首先我們建立新的模型,確保復制原始模型的隱藏層 1 到 3。我們還創建一個節點來初始化所有變量。 然后我們得到剛剛用trainable = True(這是默認值)創建的所有變量的列表,我們只保留那些范圍與正則表達式hidden [123]相匹配的變量(即,我們得到所有可訓練的隱藏層 1 到 3 中的變量)。 接下來,我們創建一個字典,將原始模型中每個變量的名稱映射到新模型中的名稱(通常需要保持完全相同的名稱)。 然后,我們創建一個Saver,它將只恢復這些變量,并且創建另一個Saver來保存整個新模型,而不僅僅是第 1 層到第 3 層。然后,我們開始一個會話并初始化模型中的所有變量,然后從原始模型的層 1 到 3中恢復變量值。最后,我們在新任務上訓練模型并保存。

任務越相似,您可以重復使用的層越多(從較低層開始)。 對于非常相似的任務,您可以嘗試保留所有隱藏的層,只替換輸出層。

復用來自其它框架的模型

如果模型是使用其他框架進行訓練的,則需要手動加載權重(例如,如果使用 Theano 訓練,則使用 Theano 代碼),然后將它們分配給相應的變量。 這可能是相當乏味的。 例如,下面的代碼顯示了如何復制使用另一個框架訓練的模型的第一個隱藏層的權重和偏置:

original_w = [...] # Load the weights from the other framework
original_b = [...] # Load the biases from the other framework

X = tf.placeholder(tf.float32, shape=(None, n_inputs), name="X")
hidden1 = fully_connected(X, n_hidden1, scope="hidden1")
[...] # # Build the rest of the model

# Get a handle on the variables created by fully_connected()
with tf.variable_scope("", default_name="", reuse=True):  # root scope
    hidden1_weights = tf.get_variable("hidden1/weights")
    hidden1_biases = tf.get_variable("hidden1/biases")

# Create nodes to assign arbitrary values to the weights and biases
original_weights = tf.placeholder(tf.float32, shape=(n_inputs, n_hidden1))
original_biases = tf.placeholder(tf.float32, shape=(n_hidden1))
assign_hidden1_weights = tf.assign(hidden1_weights, original_weights)
assign_hidden1_biases = tf.assign(hidden1_biases, original_biases)

init = tf.global_variables_initializer()

with tf.Session() as sess:
    sess.run(init)
    sess.run(assign_hidden1_weights, feed_dict={original_weights: original_w})
    sess.run(assign_hidden1_biases, feed_dict={original_biases: original_b})
    [...] # Train the model on your new task

凍結較低層

第一個 DNN 的較低層可能已經學會了檢測圖片中的低級特征,這將在兩個圖像分類任務中有用,因此您可以按照原樣重新使用這些層。 在訓練新的 DNN 時,“凍結”權重通常是一個好主意:如果較低層權重是固定的,那么較高層權重將更容易訓練(因為他們不需要學習一個移動的目標)。 要在訓練期間凍結較低層,最簡單的解決方案是給優化器列出要訓練的變量,不包括來自較低層的變量:

train_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,
                               scope="hidden[34]|outputs")
training_op = optimizer.minimize(loss, var_list=train_vars)

第一行獲得隱藏層 3 和 4 以及輸出層中所有可訓練變量的列表。 這留下了隱藏層 1 和 2 中的變量。接下來,我們將這個受限制的可列表變量列表提供給optimizerminimize()函數。現在,層 1 和層 2 被凍結:在訓練過程中不會發生變化(通常稱為凍結層)。

緩存凍結層

由于凍結層不會改變,因此可以為每個訓練實例緩存最上面的凍結層的輸出。 由于訓練貫穿整個數據集很多次,這將給你一個巨大的速度提升,因為每個訓練實例只需要經過一次凍結層(而不是每個迭代一次)。 例如,你可以先運行整個訓練集(假設你有足夠的內存):

hidden2_outputs = sess.run(hidden2, feed_dict={X: X_train})

然后在訓練過程中,不再對訓練實例建立批次,而是從隱藏層2的輸出建立批次,并將它們提供給訓練操作:

import numpy as np

n_epochs = 100
n_batches = 500

for epoch in range(n_epochs):
    shuffled_idx = rnd.permutation(len(hidden2_outputs))
    hidden2_batches = np.array_split(hidden2_outputs[shuffled_idx], n_batches)
    y_batches = np.array_split(y_train[shuffled_idx], n_batches)
    for hidden2_batch, y_batch in
        zip(hidden2_batches, y_batches):
        sess.run(training_op, feed_dict={hidden2: hidden2_batch, y: y_batch})

最后一行運行先前定義的訓練操作(凍結層 1 和 2),并從第二個隱藏層(以及該批次的目標)為其輸出一批輸出。 因為我們給 TensorFlow 隱藏層 2 的輸出,所以它不會去評估它(或者它所依賴的任何節點)。

調整,刪除或替換較高層

原始模型的輸出層通常應該被替換,因為對于新的任務來說,最有可能沒有用處,甚至可能沒有適合新任務的輸出數量。

類似地,原始模型的較高隱藏層不太可能像較低層一樣有用,因為對于新任務來說最有用的高層特征可能與對原始任務最有用的高層特征明顯不同。 你需要找到正確的層數來復用。

嘗試先凍結所有復制的層,然后訓練模型并查看它是如何執行的。 然后嘗試解凍一個或兩個較高隱藏層,讓反向傳播調整它們,看看性能是否提高。 您擁有的訓練數據越多,您可以解凍的層數就越多。

如果仍然無法獲得良好的性能,并且您的訓練數據很少,請嘗試刪除頂部的隱藏層,并再次凍結所有剩余的隱藏層。 您可以迭代,直到找到正確的層數重復使用。 如果您有足夠的訓練數據,您可以嘗試替換頂部的隱藏層,而不是丟掉它們,甚至可以添加更多的隱藏層。

Model Zoos

你在哪里可以找到一個類似于你想要解決的任務訓練的神經網絡? 首先看看顯然是在你自己的模型目錄。 這是保存所有模型并組織它們的一個很好的理由,以便您以后可以輕松地檢索它們。 另一個選擇是在模型動物園中搜索。 許多人為了各種不同的任務而訓練機器學習模型,并且善意地向公眾發布預訓練模型。

TensorFlow 在 https://github.com/tensorflow/models 中有自己的模型動物園。 特別是,它包含了大多數最先進的圖像分類網絡,如 VGG,Inception 和 ResNet(參見第 13 章,檢查model/slim目錄),包括代碼,預訓練模型和 工具來下載流行的圖像數據集。

另一個流行的模型動物園是 Caffe 模型動物園。 它還包含許多在各種數據集(例如,ImageNet,Places 數據庫,CIFAR10 等)上訓練的計算機視覺模型(例如,LeNet,AlexNet,ZFNet,GoogLeNet,VGGNet,開始)。 Saumitro Dasgupta 寫了一個轉換器,可以在 https://github.com/ethereon/caffetensorflow

無監督的預訓練

假設你想要解決一個復雜的任務,你沒有太多的標記的訓練數據,但不幸的是,你不能找到一個類似的任務訓練模型。 不要失去希望! 首先,你當然應該嘗試收集更多的有標簽的訓練數據,但是如果這太難或太昂貴,你仍然可以進行無監督的訓練(見圖 11-5)。 也就是說,如果你有很多未標記的訓練數據,你可以嘗試逐層訓練層,從最低層開始,然后上升,使用無監督的特征檢測算法,如限制玻爾茲曼機(RBM;見附錄 E)或自動編碼器(見第 15 章)。 每個層都被訓練成先前訓練過的層的輸出(除了被訓練的層之外的所有層都被凍結)。 一旦所有層都以這種方式進行了訓練,就可以使用監督式學習(即反向傳播)對網絡進行微調。

圖11-5 無監督的預訓練

這是一個相當漫長而乏味的過程,但通常運作良好。 實際上,這是 Geoffrey Hinton 和他的團隊在 2006 年使用的技術,導致了神經網絡的復興和深度學習的成功。 直到 2010 年,無監督預訓練(通常使用 RBM)是深度網絡的標準,只有在梯度消失問題得到緩解之后,純訓練 DNN 才更為普遍。 然而,當您有一個復雜的任務需要解決時,無監督訓練(現在通常使用自動編碼器而不是 RBM)仍然是一個很好的選擇,沒有類似的模型可以重復使用,而且標記的訓練數據很少,但是大量的未標記的訓練數據。(另一個選擇是提出一個監督的任務,您可以輕松地收集大量標記的訓練數據,然后使用遷移學習,如前所述。 例如,如果要訓練一個模型來識別圖片中的朋友,你可以在互聯網上下載數百萬張臉并訓練一個分類器來檢測兩張臉是否相同,然后使用此分類器將新圖片與你朋友的每張照片做比較。)

在輔助任務上預訓練

最后一種選擇是在輔助任務上訓練第一個神經網絡,您可以輕松獲取或生成標記的訓練數據,然后重新使用該網絡的較低層來完成您的實際任務。 第一個神經網絡的較低層將學習可能被第二個神經網絡重復使用的特征檢測器。

例如,如果你想建立一個識別面孔的系統,你可能只有幾個人的照片 - 顯然不足以訓練一個好的分類器。 收集每個人的數百張照片將是不實際的。 但是,您可以在互聯網上收集大量隨機人員的照片,并訓練第一個神經網絡來檢測兩張不同的照片是否屬于同一個人。 這樣的網絡將學習面部優秀的特征檢測器,所以重復使用它的較低層將允許你使用很少的訓練數據來訓練一個好的面部分類器。

收集沒有標簽的訓練樣本通常是相當便宜的,但標注它們卻相當昂貴。 在這種情況下,一種常見的技術是將所有訓練樣例標記為“好”,然后通過破壞好的訓練樣例產生許多新的訓練樣例,并將這些樣例標記為“壞”。然后,您可以訓練第一個神經網絡 將實例分類為好或不好。 例如,您可以下載數百萬個句子,將其標記為“好”,然后在每個句子中隨機更改一個單詞,并將結果語句標記為“不好”。如果神經網絡可以告訴“The dog sleeps”是好的句子,但“The dog they”是壞的,它可能知道相當多的語言。 重用其較低層可能有助于許多語言處理任務。

另一種方法是訓練第一個網絡為每個訓練實例輸出一個分數,并使用一個損失函數確保一個好的實例的分數大于一個壞實例的分數至少一定的邊際。 這被稱為最大邊際學習.


(第一部分 機器學習基礎)
第01章 機器學習概覽
第02章 一個完整的機器學習項目(上)
第02章 一個完整的機器學習項目(下)
第03章 分類
第04章 訓練模型
第05章 支持向量機
第06章 決策樹
第07章 集成學習和隨機森林
第08章 降維
(第二部分 神經網絡和深度學習)
第9章 啟動和運行TensorFlow
第10章 人工神經網絡
第11章 訓練深度神經網絡(上)
第11章 訓練深度神經網絡(下)
第12章 設備和服務器上的分布式 TensorFlow
第13章 卷積神經網絡
第14章 循環神經網絡
第15章 自編碼器
第16章 強化學習(上)
第16章 強化學習(下)


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374