TensorFlow 實戰(zhàn)Google深度學(xué)習(xí)框架(第2版)第四章讀書筆記

第四章:深層神經(jīng)網(wǎng)絡(luò)

* 4.1深度學(xué)習(xí)與深層神經(jīng)網(wǎng)絡(luò)
? ? ? ?* 4.1.1線性模型的局限性
? ? ? ?* 4.1.2激活函數(shù)實現(xiàn)去線性化
? ? ? ?* 4.1.3多層網(wǎng)絡(luò)解決異或運(yùn)算
* 4.2損失函數(shù)定義
? ? ? ?* 4.2.1經(jīng)典損失函數(shù)
? ? ? ?* 4.2.2自定義損失函數(shù)
* 4.3神經(jīng)網(wǎng)絡(luò)優(yōu)化算法
* 4.4神經(jīng)網(wǎng)絡(luò)進(jìn)一步優(yōu)化
? ? ? ?* 4.4.1學(xué)習(xí)率的設(shè)置
? ? ? ?* 4.4.2過擬合問題
? ? ? ?* 4.4.3滑動平均模型

在這一章中,將進(jìn)一步介紹如何設(shè)計和優(yōu)化神經(jīng)網(wǎng)絡(luò),
使它能夠更好地對未知 的樣本進(jìn)行預(yù)測。

-4.1- 深度學(xué)習(xí)與深層神經(jīng)網(wǎng)絡(luò)

本節(jié)中,將介紹深度學(xué)習(xí)深層神經(jīng)網(wǎng)絡(luò)的概念,
并給出樣例來說明深層神經(jīng)網(wǎng)絡(luò)可以解決部分淺層神經(jīng)網(wǎng)絡(luò)解決不了的問題。

* 深度學(xué)習(xí)就是深層神經(jīng)網(wǎng)絡(luò)的代名詞
* 深度學(xué)習(xí)有兩個非常重要的特性一一多層和非線性

-4.1.1- 線性模型的局限性

本節(jié)將先介紹線性變換存在的問題
以及為什么要在深度學(xué)習(xí)的定義中強(qiáng)調(diào)“復(fù)雜 問題”

如果知識簡單的線性前向傳播,可以看到,雖然這個神經(jīng)網(wǎng)絡(luò)有兩層(不算輸入層),但是它和單層的神經(jīng)網(wǎng)絡(luò)井沒有區(qū)別。
以此類推,只通過線性變換,任意層的全連接神經(jīng)網(wǎng)絡(luò)和單層神經(jīng)網(wǎng)絡(luò)模型的表達(dá)能力沒有任何區(qū)別,而且它們都是線性模型 。
然而線性模型能夠解決的問題有限的 ,這就是線性模型最大的局限性,也是為什么深度學(xué)習(xí)要強(qiáng)調(diào)非線性。

通過線性模型擬合線性可分問題

上圖中,正負(fù)樣本的劃分是線性可分的,可以看到,線性模型能夠很好地區(qū)分正負(fù)樣本。

通過非線性模型擬合線性不可分問題

上圖中,正負(fù)樣本的劃分是線性不可分的,會發(fā)現(xiàn),為了較好地區(qū)分正負(fù)樣本,需要使用線性模型。在這個樣例中使用了 ReLU激活函數(shù) 。 使用其他非線性激活函數(shù)也可以得到類似的效果。

-4.1.2- 激活函數(shù)實現(xiàn)去線性化

本節(jié)中,將介紹如何實現(xiàn)去線性化
并給出 TensorFlow程序來實現(xiàn)去線性化的功能。

-4.1.3- 多層網(wǎng)絡(luò)解決異或運(yùn)算

本節(jié)將介紹具體樣例來說明深層網(wǎng)絡(luò)淺層網(wǎng)絡(luò)可以解決更多的問題。

-4.2- 損失函數(shù)定義

本節(jié)中,將介紹如何設(shè)定神經(jīng)網(wǎng)絡(luò)的優(yōu)化目標(biāo)。這個優(yōu)化目標(biāo)也就是損失函數(shù)
這一節(jié)將分別介紹分類問題和回歸問題中比較常用的幾種損失函數(shù)。
除使用經(jīng)典的損失函數(shù),在這一節(jié)中將給出一個樣例來講解如何通過對損失函數(shù)的設(shè)置,使神經(jīng)網(wǎng)絡(luò)優(yōu)化的目標(biāo)更加接近實際問題的需求。

-4.2.1- 經(jīng)典損失函數(shù)

* 分類問題常用的損失函數(shù)

如何判斷一個輸出向量和期望的向盤有多接近呢? 交叉熵(cross entropy)是常用的評判方法之 一 。交叉熵刻畫了兩個概率分布之間的距離, 它是分類問題中使用 比較廣的一種損失函數(shù)。
如何將神經(jīng)網(wǎng)絡(luò)前向傳播得到的結(jié)果也變成概率分布呢? Softmax 回歸就是一個非常常用的方法 。
交叉熵函數(shù)不是對稱的({H(p, q)}\ne{H(q,p)}),它刻畫的是 通過概率分布 q 來表達(dá)概率分布 p 的困難程度。
在 3.4.5節(jié)中,已經(jīng)通過 TensorFlow實現(xiàn)過交叉熵,其代碼實現(xiàn)如下:

import tensorflow as tf
cross_entropy = -tf . reduce_mean(
    y_ * tf.log(tf.clip_by_value(y, le-10, 1.0))
)

其中:
* tf.clip_by_value()函數(shù)可以將一個張量中 的 數(shù)值限制在一個范圍之內(nèi),這樣可以避免一些運(yùn)算錯誤(比如 log0 是無效的)。
* tf.log()函數(shù)完成了對張量中所有元素依次求對數(shù)的功能。
* *乘法,注意??兩個矩陣通過"*"相乘不是矩陣乘法,而是元素之間直接相乘。即:{matmul({W_x}, {W_y})}\ne{{W_x}*{W_y}}??
* tf.reduce_mean()函數(shù)根據(jù)交叉熵的公式,應(yīng)該將每行中的 m 個結(jié)果相加得到所有樣例的 交叉熵,然后再對這 n 行取平均得到 一個 batch 的平均交叉熵。但因為分類問題的類別數(shù) 量是不變的,所以可以直接對整個矩陣做平均而并不改變計算結(jié)果的意義 。

因為交叉熵一般會與 softmax 回歸一起使用,所以 TensorFlow 對這兩個功能進(jìn)行了統(tǒng) 一封裝,并提供了 tf.nn.softmax_cross_entropy_with_logits()函數(shù)。
比如可以直接通過以下代 碼來實現(xiàn)使用了 softmax 回歸之后的交叉熵?fù)p失函數(shù):

import tensorflow as tf
cross entropy= tf.nn.softmax_cross_entropy_with_logits( labels=y , logits=y)

在只有一個正確答案的分類問題中,TensorFlow 提供了 tf.nn.sparse_softmax_cross_entropy_with_logits() 函數(shù)來進(jìn)一步加速計算過程。
在第 5 章中將提供使用這個函數(shù)的完整樣例。

* 回歸問題常用的損失函數(shù)

對于回歸 問題,最常用的損失函數(shù)是均方誤差(MSE, mean squared error)

以下代 碼展示了如何通過 TensorFlow 實現(xiàn)均方誤差損失函數(shù) :

import tensorflow as tf 

mse = tf.reduce_mean(tf.square(y_ - y))

其中 y 代表了神經(jīng)網(wǎng)絡(luò)的輸出答案, y_代表了標(biāo)準(zhǔn)答案 。
類似 4.2.1 節(jié)中介紹的乘法操作,這里的減法運(yùn)算“-”也是兩個矩陣中對應(yīng)元素的減法。

-4.2.2- 自定義損失函數(shù)

比如如果一個商品的成本是 1元,但是利潤是 10 元,那么:
* 少預(yù)測一 個就少掙 10 元;
* 而多預(yù)測一個才少掙 1 元。
如果神經(jīng)網(wǎng)絡(luò)模型最小化的是均方誤差,
那么 很有可能此模型就無法最大化預(yù)期的利潤 。
為了最大化預(yù)期利潤,需要將損失函數(shù)和利潤 直接聯(lián)系起來 。

在 TensorFlow 中,可以通過以下代碼來實現(xiàn)這個損失函數(shù):

import tensorflow as tf 

loss= tf.reduce sum(tf . where(tf.greater(v1, v2) ,(v1 - v2) * a, (v2 - v1) * b))

注意??在TF中,除了tf.matmul()表示的是矩陣乘法,其他+-、*、/代表的都是element-wise級別的操作??

以上代碼用到了 tf.greater和 tf.where來實現(xiàn)選擇操作。
* tf.greater()的輸入是兩個張量,此函數(shù)會比較這兩個輸入張量中每一個元素的大小,并返回比較結(jié)果。 當(dāng) tf.greater()的輸入張量維度不一樣時,TensorFlow會進(jìn)行類似 NumPy廣播操作( broadcasting)的處理。
* tf.where()函數(shù)有三個參數(shù)。第一個為選擇條件根據(jù),當(dāng)選擇條件為True時,tf.where() 函數(shù)會選擇第二個參數(shù)中的值 , 否則使用第三個參數(shù)中的值。注意 tf.where 函數(shù)判斷和選擇都是在元素級別進(jìn)行

以下代碼展示了 tf.where() 函數(shù)和 tf.greater() 函數(shù)的用法 :

import tensorflow as tf

v1 = tf.constant([1.0, 2.0, 3.0, 4.0])
v2 = tf.constant([4.0, 3.0, 2.0 ,1.0])

sess = tf.IneractiveSession()
# 不需要再指定default_session

print(tf.greater(v1, v2).eval())
# 輸出:[False False True True]

print(tf.where(tf.greater(v1, v2), v1, v2).eval())
# 輸出:[4. 3. 3. 4.]

sess.close()

* 損失函數(shù)對模型訓(xùn)練結(jié)果的影響
在下面這個程序中 , 實現(xiàn)了一個擁有兩個輸入節(jié)點、 一個輸出節(jié)點,沒有隱藏層的神經(jīng)網(wǎng)絡(luò)。這個程序的主體流程和 3.4.5節(jié)中給出來的樣例基本一致,但用到了上面定義的有權(quán)重的損失函數(shù)。

import tensorflow as tf 
from bumpy.random import RandomState

batch_size = 8 

# 兩個輸入節(jié)點
x = tf.placeholder(tf.float32, shape=[None, 2], name="calculate_node_placholder_input_x")

# 一個輸出節(jié)點,回歸問題一般只有一個輸出節(jié)點
y_ = tf.placeholder(tf.float32, shape=[None, 1], name="calculate_node_placeholder_input_y")

# 定義一個單層的神經(jīng)網(wǎng)絡(luò)前向傳播過程
# 這里就是簡單的加權(quán)和
w1 = tf.Variable(tf.random_normal(shape=[2, 1], stddev=1, seed=1))
y = tf.matmul(x, w1)

# 定義預(yù)測多了or少了的成本、損失函數(shù)
loss_less = 10
loss_more = 1
loss = tf.reduce_mean(tf.where(
    tf.greater(y, y_),
    (y - y_) * loss_more,
    (y_ - y) * loss_less
))

train_step = tf.train.AdamOptimizer(0.001).minimize(loss)

# 通過隨機(jī)數(shù)生成一個模擬數(shù)據(jù)集
rdm = RandomState(1)
dataset_size = 128
X = rum.rand(dataset_size, 2)

# 設(shè)置回歸的正確值為 兩個輸入的和 + 一個隨機(jī)量
# 之所以要加上一個隨機(jī)量是為了加入不可預(yù)測的噪音
# 否則不同損失函數(shù)的意義就不大了
# 因為不同的損失函數(shù)都能會在能完全預(yù)測正確的時候最低
# 一般來說,噪音為一個均值為0的小量
# 所以這里的噪音為 -0.5 ~ +0.5 的隨機(jī)數(shù)
Y = [[x1+x2+rdm.rand()/10.0-0.5] for (x1, x2) in X]

# 訓(xùn)練神經(jīng)網(wǎng)絡(luò)
with tf.Session() as sess:
    init_op = tf.global_variables_initializer()
    sess.run(init_op)
    STEPS = 5000
    for i in range(STEPS):
        start = (i * batch_size) % dataset_size
        end = min(start+batch_size, dataset_size)
        sess.run(
            train_step,
            feed_dict = {x: X[start:end], y:Y[start:end]}
        )
    # 經(jīng)過STEP步batch迭代之后
    print(sess.run(w1))

* 運(yùn)行以上代碼會得到w1的值為[1.01934695, 1.04280889],也就是說得到的預(yù)測函數(shù)是y = 1.02x_1 + 1.04x_2,這其實是要比x_1 + x_2大的,因為在損失函數(shù)中,有指定預(yù)測少了的損失更大,所以訓(xùn)練出來的模型會傾向于預(yù)測更多。

* 如果將loss_less的值調(diào)整為1,loss_more的值調(diào)整為10,這樣的話訓(xùn)練出來的權(quán)重矩陣為[0.95525807, 0.9813394],即模型更傾向于預(yù)測少一點。
* 而如果使用均方誤差作為損失函數(shù),那么迭代之后的w1會是[0.97437561, 1.0243336]。使用mse作為損失函數(shù),會盡量讓預(yù)測值離標(biāo)準(zhǔn)答案更近。

通過這個樣例可以看出,設(shè)計不同的損失函數(shù),會對模型產(chǎn)生重要影響。

-4.3- 神經(jīng)網(wǎng)絡(luò)優(yōu)化算法

本節(jié)將更加詳細(xì)地介紹神經(jīng)網(wǎng)絡(luò)的反向傳播算法,
并且給出一個TensorFlow框架來實現(xiàn)反向傳播的過程。

本節(jié)將更加具體地介紹如何通過反向傳播算法( backpropagation)梯度下降算法 (gradient decent)調(diào)整神經(jīng)網(wǎng)絡(luò)中參數(shù)的取值。
* 梯度下降算法主要用于優(yōu)化單個參數(shù)(loss)的取值,
*反向傳播算法給出了一個高效的方式在所有參數(shù)(模型中待訓(xùn)練參數(shù)矩陣)上使用梯度下降算法,從而使神經(jīng)網(wǎng)絡(luò)模型在訓(xùn)練數(shù)據(jù)上的損失函數(shù)盡可能小。

反向傳播算法是訓(xùn)練神經(jīng)網(wǎng)絡(luò)的核心算法,它可以根據(jù)定義好的損失函數(shù)優(yōu)化神經(jīng)網(wǎng)絡(luò)中參數(shù)的取值,從而使神經(jīng)網(wǎng)絡(luò)模型在訓(xùn)練數(shù)據(jù)集上的損失函數(shù)達(dá)到一個較小值。
神經(jīng)網(wǎng)絡(luò)模型中參數(shù)的優(yōu)化過程直接決定了模型的質(zhì)量,是使用神經(jīng)網(wǎng)絡(luò)時非常重要的 一步。
在本節(jié)中,將主要介紹神經(jīng)網(wǎng)絡(luò)優(yōu)化過程的基本概念和主要思想,而略去算法的數(shù)學(xué)推導(dǎo)和證明 。
本節(jié)將給出一個具體的樣例來解釋使用梯度 下降算法優(yōu)化參數(shù)取值的過程。

* 假設(shè)用 θ表示神經(jīng)網(wǎng)絡(luò)中的參數(shù), J(θ)表示在給定的參數(shù)取值下,訓(xùn)練數(shù)據(jù)集上損失函數(shù)的大小,
* 那么整個優(yōu)化過程可以抽象為尋找一個參數(shù) θ ,使得 J(θ)最小。
* 因為目前沒 有一個通用的方法可以對任意損失函數(shù)直接求解最佳的參數(shù)取值,所以在實踐中,梯度下降算法是最常用的神經(jīng)網(wǎng)絡(luò)優(yōu)化方法。
* 梯度下降算法會以步步迭代的方式更新參數(shù) θ ,不斷沿著梯度的反方向讓參數(shù)朝著總損失更小的方向更新。

參數(shù)的梯度可以通過求偏導(dǎo)的方式計算
有了梯度,還需要定義一個學(xué)習(xí)率η(learningrate)來計算出每次參數(shù)更新的幅度。

神經(jīng)網(wǎng)絡(luò)的優(yōu)化過程可以分為兩個階段:
* 第一個階段先通過前向傳播算法計算得到預(yù)測值,井將預(yù)測值和真實值做對比得出兩者之間的差距。
* 然后在第二個階段通過反向傳播算法計算損失函數(shù)對每一個參數(shù)的梯度,再根據(jù)梯度和學(xué)習(xí)率使用梯度下降算法更新每一個參數(shù)。

此處略去反向傳播算法具體的實現(xiàn)方法和數(shù)學(xué)證明,有興趣的同學(xué)可以參考 David Rumelhart、Geoffrey Hinton和 Ronald Williams教授發(fā)表的論文 Learning representations by back-propagαtingerrors。

* 局部最優(yōu)解問題

需要注意的是,梯度下降算法并不能保證被優(yōu)化的函數(shù)達(dá)到全局最優(yōu)解。
在訓(xùn)練神經(jīng)網(wǎng)絡(luò)時,參數(shù)的初始值會很大程度影響最后得到的結(jié)果 。
只有當(dāng)損失函數(shù)為凸函數(shù)時,梯度下降算法才能保證達(dá)到全局最優(yōu)解。

* 迭代耗時,計算成本高

除了不一定能達(dá)到全局最優(yōu) ,梯度下降算法的另外一個問題就是計算時間太長 。 因為 要在全部訓(xùn)練數(shù)據(jù)上最小 化損 失,所以損失函數(shù) 冽的 是在所有訓(xùn)練數(shù)據(jù)上的損失和 。 這樣 在每一輪迭代中都需要計算在全部訓(xùn)練數(shù)據(jù)上的損失函數(shù) 。 在海量訓(xùn)練數(shù)據(jù)下,要計算所 有訓(xùn)練數(shù)據(jù)的損失函數(shù)是非常消耗時間的。

為了加速訓(xùn)練過程,可以使用隨機(jī)梯度下降的算法 Cstochastic gradient descent)。
這個算法優(yōu)化的不是在全部訓(xùn)練數(shù)據(jù)上的損失函數(shù),
而是:
在每一輪法代中,
(只讀入一條樣本?還是讀入一個batch的樣本?)
隨機(jī)優(yōu)化某一條訓(xùn)練數(shù)據(jù)上的損失函數(shù) 。
這樣每一輪參數(shù)更新的速度就大大加快了。

因為隨機(jī)梯度下降算法每次優(yōu)化的只是某一條數(shù)據(jù)上的損失函數(shù),所以它 的問題也非常明顯 :
在某 一 條數(shù)據(jù)上損失函數(shù)更小并不代表在全部數(shù)據(jù)上損失函數(shù)更小, 于是使用隨機(jī)梯度下降優(yōu)化得到的神經(jīng)網(wǎng)絡(luò)甚至可能無法達(dá)到局部最優(yōu) 。

為了綜合梯度下降算法和隨機(jī)梯度下降算法的優(yōu)缺點,在實際應(yīng)用中 一般采用這兩個 算法的折中一一每次計算一小部分訓(xùn)練數(shù)據(jù)的損失函數(shù)。
這一小部分?jǐn)?shù)據(jù)被稱之為一個 mini-batch。
通過矩陣運(yùn)算,每次在 一個 mini-batch 上優(yōu)化神經(jīng)網(wǎng)絡(luò)的參數(shù)并不會比單個數(shù)據(jù)慢太多 。
另一方面,每次使用一個 mini-batch 可以大大減小收斂所需要的迭代次數(shù),同時可以使收斂到 的結(jié)果更加接近梯度下降的效果。

* 批量梯度下降(BGD)、隨機(jī)梯度下降(SGD)以及小批量梯度下降(MBGD)
一次迭代計算loss時,用到的樣本條數(shù)
* 批量梯度下降(BGD)
* 隨機(jī)梯度下降(SGD)
* 小批量梯度下降(MBGD)

* GradientDescentOptimizer、AdamOptimizer、MomentumOptimizer
一次迭代計算完本次loss之后,沿梯度反方向更新參數(shù)時的步長計算
* GradientDescentOptimizer:使參數(shù)沿著 梯度的反方向,即總損失減小的方向移動,以固定學(xué)習(xí)率更新參數(shù);
* AdamOptimizer:通過計算梯度的一階矩估計和二階矩估計而為不同的參數(shù)設(shè)計獨立的自適應(yīng)性學(xué)習(xí)率;
* MomentumOptimizer:更新參數(shù)時,利用了超參數(shù),在梯度下降較慢的地方,加大參數(shù)更新的幅度。

以下代碼給出了在 TensorFlow 中如何實現(xiàn)神經(jīng)網(wǎng)絡(luò)的訓(xùn) 練過程。在本書的所有樣例中 ,神經(jīng)網(wǎng)絡(luò)的訓(xùn)練都大致遵循以下過程 :

import tensorflow as tf

batch_size = n

# 每次讀取一小部分?jǐn)?shù)據(jù)作為當(dāng)前的訓(xùn)練數(shù)據(jù)來執(zhí)行反向傳播算法
x = tf.placeholder(tf.float32, shape=[batch_size, 2], name="calculate_node_x_input")
y_ = tf.placeholder(tf.float32, shape=[batch_size, 1], name="calculate_node_y_input")

# 定義神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)和優(yōu)化算法
loss = ...
train_step = tf.train.AdamOptimizer(0.001).minimize(loss)

# 訓(xùn)練神經(jīng)網(wǎng)絡(luò)
with tf.Sessio() as sess:
    # 參數(shù)初始化
    ...
    
    for i in range(STEPS):
        # 準(zhǔn)備 batch_size個訓(xùn)練數(shù)據(jù)。
        # 一般將所有訓(xùn)練數(shù)據(jù)隨機(jī)打亂之后再選取
        # 可以取到更好的優(yōu)化效果
        current_x, current_y = ...
        sess.run(train_step, feed_dict={x: current_X, y_: current_Y})

-4.4- 神經(jīng)網(wǎng)絡(luò)進(jìn)一步優(yōu)化

本節(jié)將介紹在神經(jīng)網(wǎng)絡(luò)優(yōu)化中經(jīng)常遇到的幾個問題,
井且給出解決這些問題的具體方法。

本節(jié)將緊接上節(jié),繼續(xù)介紹的神經(jīng)網(wǎng)絡(luò)優(yōu)化過程中可能遇到的問題解決方法,掌握本節(jié)內(nèi)容可以幫助讀者更好地理解這些優(yōu)化方法 。
本節(jié)將繼續(xù)介紹神經(jīng)網(wǎng)絡(luò)優(yōu)化過程中可能遇到的一些問題,以及解決這些問題的常用方法。

-4.4.1- 學(xué)習(xí)率的設(shè)置

本節(jié)將介紹通過指數(shù)衰減的方法設(shè)置梯度 下降算法中的學(xué)習(xí)率。
通過指數(shù)衰減的學(xué)習(xí)率
既可以讓模型在訓(xùn)練的前期快速接近較優(yōu)解,
又可以保證模型在訓(xùn)練后期不會有太大的波動,從而更加接近局部最優(yōu)。
4.3節(jié)介紹了在訓(xùn)練神經(jīng)網(wǎng)絡(luò)時, 需要設(shè)置學(xué)習(xí)率(learningrate)控制參數(shù)更新的速度。 本節(jié)將進(jìn)一步介紹如何設(shè)置學(xué)習(xí)率。

學(xué)習(xí)率決定了參數(shù)每次更新的幅度。
如果幅度過大, 那么可能導(dǎo)致參數(shù)在極優(yōu)值的兩側(cè)來回移動。
相反,當(dāng)學(xué)習(xí)率過小時,雖然能保證收斂性,但是這會大大降低優(yōu)化速度 。會需要更多輪的法代才能達(dá)到一個比較理想的優(yōu)化效果。
綜上所述,學(xué)習(xí)率既不能過大,也不能過小。為了解決設(shè)定學(xué)習(xí)率的問題,TensorFlow提供了一種更加靈活的學(xué)習(xí)率設(shè)置方法一一指數(shù)衰減法

tf.train.exponential_decay()函數(shù)實現(xiàn)了指數(shù)衰減學(xué)習(xí)率。通過這個函數(shù),可以
*先使用較大的學(xué) 習(xí)率來快速得到一個比較優(yōu)的解,
*然后隨著迭代的繼續(xù)逐步減小學(xué)習(xí)率,使得模型在訓(xùn)練后 期更加穩(wěn)定。

exponential_decay()函數(shù)會指數(shù)級地減小學(xué)習(xí)率,它實現(xiàn)了以下代碼的功能:

import tensorflow as tf

decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)

其中:
* decayed_learning_rate 為每一輪優(yōu)化時使用的學(xué)習(xí)率
* learning_rate 為事先設(shè)定的初始學(xué)習(xí)率
* decay_rate 為衰減系數(shù)
* decay_steps 為衰減速度

圖 4-13 顯示了隨著迭代輪數(shù)的增加,學(xué)習(xí)率逐步降低的過程。

tf.train.exponential_decay()函數(shù)可以通過設(shè)置參數(shù) staircase選擇不同的衰減方式。
* staircase 的默認(rèn)值為False,這時學(xué)習(xí)率隨迭代輪數(shù)變化的趨勢如圖中灰色曲線所示。
* 當(dāng) staircase 的值被設(shè)置為True時,global_step/decay_steps 會被轉(zhuǎn)化成整數(shù)。這使得學(xué)習(xí)率成為一個階梯函數(shù)( staircase function)。圖中黑色曲線顯示了階梯狀的學(xué)習(xí)率。

在這樣的設(shè)置下,decay_steps通常代表了完整的使用一遍訓(xùn)練數(shù)據(jù)所需要的迭代輪數(shù)。
這個迭代輪數(shù)也就是總訓(xùn)練樣本數(shù)除以每一個 batch 中的訓(xùn)練樣本數(shù) 。
這種設(shè)置的常用場景是每完整地過完一遍訓(xùn)練數(shù)據(jù),學(xué)習(xí)率就減小一次。
這可以使得訓(xùn)練數(shù)據(jù)集中的所有數(shù)據(jù)對模型訓(xùn)練有相等的作用。
當(dāng)使用連續(xù)的指數(shù)衰減學(xué)習(xí)率時,不同的訓(xùn)練數(shù)據(jù)有不同的學(xué)習(xí)率,而當(dāng)學(xué)習(xí)率減小時,對應(yīng)的訓(xùn)練數(shù)據(jù)對模型訓(xùn)練結(jié)果的影響也就小了。

下面給出了一段代碼來示范如何在TensorFlow中使用tf.train.exponential_decay()函數(shù):

import tensorflow as tf 

global_step = tf.Variable(initial_value=0)

# 通過exponential_decay() 函數(shù)生成學(xué)習(xí)率:
learning_rate = tf.train.exponential_decay(
    0.1, global_step, 100, 0.96, staircase=True
)

# 使用指數(shù)衰減的學(xué)習(xí)率,
# 在minimize()函數(shù)中加入global_step將自動更新global_step這個tf.Variable
# 從而使學(xué)習(xí)率得到相應(yīng)的更新
learning_step = tf.train.GradientDecentOptimizer(learning_rate).minimize(loss, global_step=global_step)

上面這段代碼中設(shè)定了初始學(xué)習(xí)率為0.1,因為指定了staircase=True,所以每訓(xùn)練100輪后,學(xué)習(xí)率衰減0.96倍。
一般來說,初始學(xué)習(xí)率衰減系數(shù)、衰減速度都是根據(jù)經(jīng)驗設(shè)置的。而且損失函數(shù)下降的速度和迭代結(jié)束之后總損失的大小沒有必然的聯(lián)系,而僅僅是影響迭代的效率、耗時。也就是說并不能通過訓(xùn)練過程中最開始前幾輪損失函數(shù)下降的速度來判斷不同神經(jīng)網(wǎng)絡(luò)模型的好壞。

-4.4.2- 過擬合問題

本節(jié)將介紹過擬合問題。
在訓(xùn)練復(fù)雜神經(jīng)網(wǎng)絡(luò)模型時,過擬合是一個非常常見的問題。
這一節(jié)將具體介紹這個問題的影響以及解決這個問題的主要方法。

模型在訓(xùn)練數(shù)據(jù)上的表現(xiàn)并不一定代表了它在未知數(shù)據(jù)上的表現(xiàn)。
本節(jié)將介紹的過擬合問題就是可以導(dǎo)致這個差距的一個很重要因素。

在真實的應(yīng)用中,訓(xùn)練模型想要的:
* 并不是讓模型盡量模擬訓(xùn)練數(shù)據(jù)的行為,
* 而是希望通過訓(xùn)練出來的模型對未知的數(shù)據(jù)給出判斷。
所謂過擬合,指的是當(dāng)一個模型過為復(fù)雜之后,它
* 可以很好地“記憶”每一個訓(xùn)練數(shù)據(jù)中隨機(jī)噪音的部分
* 而忘記 了要去“學(xué)習(xí)”訓(xùn)練數(shù)據(jù)中通用的趨勢。
舉一個極端的例子,如果一個模型中的參數(shù)個數(shù)比訓(xùn)練樣本條數(shù)還多,
那么只要訓(xùn)練數(shù)據(jù)不沖突,這個模型完全可以記住所有訓(xùn)練數(shù)據(jù)的結(jié)果從而使得損失函數(shù)為0。
然而,過度擬合訓(xùn)練數(shù)據(jù)中的隨機(jī)噪音雖然可以得到非常小的損失函數(shù),
但是對于未知數(shù)據(jù)可能無法做出可靠的判斷。

圖 4-14顯示了模型訓(xùn)練的三種不同情況。
* 在第一種情況下,由于模型過于簡單,無法刻畫問題的趨勢。
* 第二個模型是比較合理的,它既不會過于關(guān)注訓(xùn)練數(shù)據(jù)中的噪音,又能夠比較好地刻畫問題的整體趨勢。
* 第三個模型就是過擬合了,雖然第三個模型完美地劃分了不同形狀的點,但是這樣的劃分并不能很好地對未知數(shù)據(jù)做出判斷,因為它過度擬合了訓(xùn)練數(shù)據(jù)中的噪音而忽視了問題的整體規(guī)律。比如圖中淺色方框“口”更有可能和“ X” 屬于同 一類,而不是根據(jù)圖上的劃分和“ ?!睂儆谕?一類。

* 正則化(regularization)

為了避免過擬合問題,一個非常常用的方法是正則化( regularization)。正則化的思想就是在損失函數(shù)中加入刻畫模型復(fù)雜程度的指標(biāo)。

假設(shè)用于刻畫模型在訓(xùn)練數(shù)據(jù)上表現(xiàn)的損失函數(shù)loss=J(θ)
那么在優(yōu)化時,不是直接優(yōu)化J(θ),而是優(yōu)化 J(θ)+λR(w) 。其中:
* θ是一個神經(jīng)網(wǎng)絡(luò)中所有的參數(shù),包括邊上權(quán)重w和偏置項b
* w是邊上的權(quán)重
* R(w)刻畫的是模型的復(fù)雜程度
* λ表示模型復(fù)雜損失在總損失中的比例

一般來說模型復(fù)雜度只由權(quán)重w決定 。

常用的刻畫模型復(fù)雜度的函數(shù) R(w)有兩種:
* 一種是 L1 正則化,計算公式是:

L1 正則

* 另一種是 L2 正則化,計算公式是:

L2 正則

無論是哪一種正則化方式,基本的思想都是希望通過限制權(quán)重的大小,使得模型不能任意擬合訓(xùn)練數(shù)據(jù)中的隨機(jī)噪音。
但這兩種正則化的方法也有很大的區(qū)別:
* 首先,L1正則化會讓參數(shù)變得更稀疏,而L2正則化不會。
所謂參數(shù)變得更稀疏是指:會有更多的參數(shù)變?yōu)?,這樣可以達(dá)到類似特征選取的功能。
之所以L2正則化不會讓參數(shù)變得稀疏的原因是:當(dāng)參數(shù)很小時,比如 0.001,這個參數(shù)的平方基本上就可以忽略了,于是模型不會進(jìn)一步將這 個參數(shù)調(diào)整為0。
*其次,LI正則化的計算公式不可導(dǎo),而L2正則化公式可導(dǎo)。
因為在優(yōu)化時需要計算損失函數(shù)的偏導(dǎo)數(shù),所以對含有L2正則化損失函數(shù)的優(yōu)化要更加簡潔。優(yōu)化帶LI正則化的損失函數(shù)要更加復(fù)雜,而且優(yōu)化方法也有很多種。

* 在實踐中,也可以將L1正則化L2正則化同時使用:

L1 & L2正則

4.2 節(jié)提到過 TensorFlow 可以優(yōu)化任意形式的損失函數(shù),所以 TensorFlow 自然也可以優(yōu)化帶正則化的損失函數(shù)。

以下代碼給出了 一個簡單的帶 L2 正則化的損失函數(shù)定義 :

# 此處以【回歸任務(wù)】的MSE損失為例

import tensorflow as tf

w = tf.Variable(initial_value=tf.random_normal([2, 1], stddev=2, seed=1))
y = tf.matmul(x, w)

loss = tf.reduce_mean(tf.square(y_ - y)) + tf.contrib.layers.l2regularizer(lambda)(w)

在以上程序中,loss為定義的損失函數(shù),它由兩個部分組成:
* 第一個部分是 4.2.1 節(jié)中介紹的均方誤差損失函數(shù),它刻畫了模型在訓(xùn)練數(shù)據(jù)上的表現(xiàn)(這里舉例是以回歸場景為例,所以選取了MSE這個損失計算函數(shù))。
* 第二個部分就是正則化,它防止模型過度模擬訓(xùn)練、數(shù)據(jù)中的隨機(jī)噪音。其中:
lambda參數(shù)是公式 J(θ)+λR(w)中的λ,表示正則化項在loss中的權(quán)重。
w為需要參與計算正則化損失的模型參數(shù)。

TensorFlow 提供了 tf.contrib.layers.l2_regularizer() 函數(shù),
它可以返回一個函數(shù),這個函數(shù)可以計算一個給定參數(shù) 的L2正則化項的值。
類似的,tf.contrib.layers.l1_regularizer() 可以計算L1 正則化項的值。

以下代碼給出了使用這兩個函數(shù)的樣例:

import tensorflow as tf 

weights = tf.constant([[1.0, -2.0], [-3.0, 4.0]])
with tf.Session() as sess:
    # 輸出為 (|1| + |-2| + |-3| + |4|) x 0.5 = 5。
    # 其中 0.5 為正則化項的權(quán)重
    print(sess.run(
        tf.contrib.layers.l1_regularizer(.5)(weights)
    ))
    # 輸出為 (1^2+ (-2)^2+ (-3)^2+4^2)/2 × 0.5 = 7.5
    # 為什么要 /2  ???
    print(sess.run(
        tf.contrib.layers.l2_regularizer(.5)(weights)
    ))

在簡單的神經(jīng)網(wǎng)絡(luò)中,這樣的方式就可以很好地計算帶正則的損失函數(shù)了。但當(dāng)神經(jīng)網(wǎng)絡(luò)的參數(shù)增多之后,這樣的方式:
* 首先可能導(dǎo)致?lián)p失函數(shù) loss的定義很長,可讀性差容易出錯
* 但更主要的是,當(dāng)網(wǎng)絡(luò)結(jié)構(gòu)復(fù)雜之后,定義網(wǎng)絡(luò)結(jié)構(gòu)的代碼部分和計算損失函數(shù)的代碼部分可能不在同一個函數(shù)中,這樣通過變量這種方式計算損失函數(shù)就不方便了。

*集合(Collection)

為了解決這個問題,可以使用 TensorFlow 中提供的集合(Collection)。
集合的概念在3.1節(jié)中介紹過,它可以在一個計算圖(tf.Graph)中保存一組實體(比如張量)。

以下代碼給出了通過集合(Collection)計算一個5層神經(jīng)網(wǎng)絡(luò)帶 L2 正則化的損失函數(shù)的計算方法:

import tensorflow as tf

# 獲取一層神經(jīng)網(wǎng)絡(luò)邊上的權(quán)重
# 并將這個權(quán)重的L2正則化損失加入名稱為'losses'的集合(Collection)中
def get_weight(shape, lambda):
    # 生成一個變量
    var = tf.Variable(
        initial_value=tf.random_normal(shape),
        dtype=tf.float32
    )
    # add_to_collection()函數(shù)將這個新生成的變量的L2正則化損失項加入集合
    # 這個函數(shù)的第一個參數(shù)'losses'是集合的名字,
    # 第二個參數(shù)是要加入這個集合的內(nèi)容
    tf.add_to_collection(
        'losses',
        tf.contrib.layers.l2_regularizer(lambda)(var)
    )
    # 返回生成的變量
    return var 

x = tf.placeholder(dtype=tf.float32, shape=(None, 2))
y_ = tf.placeholder(dtype=tf.float32, shape=(None, 1))
batch_size = 8

# 定義了每一層網(wǎng)絡(luò)中的節(jié)點個數(shù)
layer_dimension = [2, 10, 10, 10, 1]

# 神經(jīng)網(wǎng)絡(luò)的層數(shù)
n_layers = len(layer_dimension)

# 這個變量維護(hù)前向傳播時,最深層的節(jié)點
# 開始的時候,就是輸入層
cur_layer = x

# 當(dāng)前層的節(jié)點個數(shù)
in_dimension = layer_dimension[0]

# 通過一個循環(huán)來生成5層全聯(lián)接的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)
for i in range(n_layers):
    # layer_dimension[i]為下一層的節(jié)點數(shù)
    out_dimension = layer_dimension[i]
    # 生成當(dāng)前層中的權(quán)重變量
    # 并將這個變量的L2正則化損失加入計算圖上的'losses'集合(Collection)
    weight = get_weight(
        [in_dimension, out_dimension],
        0.001
    )
    bias = tf.Variable(tf.constant(0.1, shape=[out_dimension]))
    # 使用ReLU激活函數(shù)
    cur_layer = tf.nn.relu(
        tf.matmul(cur_layer, weight) + bias  # 這里是element-wise的加
    )
    # 進(jìn)入下一層之前,
    # 將下一層的節(jié)點個數(shù)更新為當(dāng)前層的節(jié)點個數(shù)
    in_dimension = layer_dimension[i]

# 在定義神經(jīng)網(wǎng)絡(luò)前向傳播的同時,也將所有的L2正則化損失加入了圖上的集合
# 這里只需要計算損失函數(shù)
# 該損失函數(shù)刻畫模型在訓(xùn)練數(shù)據(jù)上的表現(xiàn)
mse_loss = tf.reduce_mean(tf.square(y_ - cur_layer))

# 將均方誤差損失函數(shù)加入損失集合
tf.add_to_collection('losses', mse_loss)

# get_collection()返回一個列表
# 這個列表是所有這個集合中的元素
# 在該樣例中,這些元素就是損失函數(shù)的不同部分
# 將它們加起來,就可以得到最終的損失函數(shù)
loss = tf.add_n(
    tf.get_collection('losses')  # 返回這個名為'losses'的Collection集合中的各個L2正則項
)

從以上代碼可以看出通過使用集合的方法在網(wǎng)絡(luò)結(jié)構(gòu)比較復(fù)雜的情況下可以使代碼的可讀性更高 。
以上代碼給出的是一個只有 5 層的全連接網(wǎng)絡(luò) ,
在更加復(fù)雜的網(wǎng)絡(luò)結(jié)構(gòu)中,使用這樣的方式來計算損失函數(shù)將大大增強(qiáng)代碼的可讀性。

-4.4.3- 滑動平均模型

本節(jié)將介紹滑動平均模型。
滑動平均模型會將每一輪迭代得到的模型綜合起來,從而使得最終得到的模型更加健壯(robust)。
這一個節(jié)將介紹另外一個可以便模型在測試數(shù)據(jù)上更健壯(robust)的方法一一滑動平均模型。
在采用隨機(jī)梯度下降算法訓(xùn)練神經(jīng)網(wǎng)絡(luò)時,使用滑動平均模型在很多應(yīng)用中都可以在一定程度提高最終模型在測試數(shù)據(jù)上的表現(xiàn)。

在TensorFlow中提供了tf.train.ExponentialMovingAverage來實現(xiàn)滑動平均模型。
在初始化ExponentialMovingAverage時,需要提供一個衰減率( decay)。
這個衰減率將用于控制模型更新的速度。
ExponentialMovingAverage對每一個變量會維護(hù)一個影子變量( shadow variable),
這個影子變量的初始值就是相應(yīng)變量的初始值,

而每次運(yùn)行變量更新時,影子變量的值會更新為 :

其中:
* shadow_variable 為影子變量
* variable為待更新的變量
* decay為衰減率

從公式中可以看到,decay決定了模型更新的速度
decay越大,模型越趨于穩(wěn)定
在實際應(yīng)用中,decay一般會設(shè)成非常接近1的數(shù),比如0.999或者0.9999
為了使模型在訓(xùn)練前期可以更新得更快,ExponentialMovingAverage還提供了num_updates參數(shù)動態(tài)設(shè)置decay的大小。

如果在 ExponentialMovingAverage 初始化時提供了 num_updates參數(shù),那么每次使用的衰減率將是 :

下面通過一段代碼來解釋 ExponentialMovingAverage是如何被使用的:

import tensorflow as tf

# 定義一個變量用于計算滑動平均,這個變量的初值為0
# 注意這里手動指定了比納涼的類型為tf.float32 
# 因為所有需要計算滑動平均的變量必須是實數(shù)型
v1 = tf.Variable(0, dtype=tf.float32)
# 這里step變量模擬神經(jīng)網(wǎng)絡(luò)中的迭代論數(shù)
# 可以用于動態(tài)控制衰減率
step  = tf.Varibale(0, trainable=False)

# 定義一個滑動平均的類(class)
# 初始化時,給定了衰減率(0.99)和控制衰減率的變量step
ema = tf.train.ExponentialMovingAverage(0.99, step)

# 定義一個更新變量滑動平均的操作
# 這里需要指定一個待滑動平均的變量的列表
# 每次執(zhí)行這個操作時,這個列表里中的變量都會被更新
maintain_averages_op = ema.apply([v1])

with tf.Session() as sess:
    # 初始化所有變量
    init_op = tf.global_variable_initializer()
    sess.run(train_op)
    
    # 通過ema.average(v1)獲取滑動平均之后變量的取值
    # 在初始化之后,變量v1本身的值和v1的滑動平均都是0
    print(sess.run([v1, ema.average(v1)]))

    # 將變量v1的值指定為5
    sess.run(tf.assign(v1, 5))
    # 更新v1的滑動平均值
    # 衰減率為min{0.99, (1+step)/(10+step)= 0.1}=0.1
    # 所以v1的滑動平均會被更新為 0.1×0 + 0.9×5 = 4.5
    sess.run(maintain_averages_op)
    print(sess.run([v1, ema.average(v1)]))

    # 將step的值指定為10000
    sess.run(tf.assign(step, 10000))
    # 將v1的值指定為10
    sess.run(tf.assign(v1, 10))
    # 更新v1的滑動平均值
    # 衰減率為min{ 0.99 , (1+step)/(10+step)~0.999} = 0.99 
    # 所以v1的滑動平均值會被更新為 0.99*4.5 + 0.01*10 = 4.555
    sess.run(maintain_averages_op)
    print(sess.run([v1, ema.average(v1)]))
    # 輸出 [10.0, 4.5549998]

    # 再次更新滑動平均值
    # 得到的新滑動平均值為 0.99*4.555 + 0.01*10 = 4.60945
    sess.run(maintain_averages_op)
    print(sess.run([v1, ema.average(v1)]))
    # 輸出 [10.0 , 4.6094499]

以上代碼給出了 ExponentialMovingAverage 的簡單樣例,在第 5 章中將給出在真實應(yīng)用中使用滑動平均的樣例。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容