TensorFlow之DNN(二):全連接神經網絡的加速技巧(Xavier初始化、Adam、Batch Norm、學習率衰減與梯度截斷)


在上一篇博客《TensorFlow之DNN(一):構建“裸機版”全連接神經網絡》 中,我整理了一個用TensorFlow實現的簡單全連接神經網絡模型,沒有運用加速技巧(小批量梯度下降不算哦)和正則化方法,通過減小batch size,也算得到了一個還可以的結果。

那個網絡只有兩層,而且MINIST數據集的樣本量並不算太大。如果神經網絡的隱藏層非常多,每層神經元的數量巨大,樣本數量也巨大時,可能出現三個問題:

一是梯度消失和梯度爆炸問題,導致反向傳播算法難以進行下去;

二是在如此龐大的網絡中進行訓練,速度會非常緩慢;

三具有數百萬個參數的模型可能造成過擬合。

對於梯度消失的問題,我們可以通過合理地初始化權重(Xavier初始化或He初始化)、選擇更好的激活函數(relu函數)和使用Batch Normalization來緩解,對於梯度爆炸問題,可以用梯度截斷的辦法來解決。理論部分我已經整理過了,不嫌棄的話請看《深度學習之激活函數》和《深度學習之Batch Normalization》。

對於訓練速度太慢這個問題,可以用梯度下降的優化算法,調整損失函數的梯度(比如Momentum)和學習率(比如RMSProp),或者使用學習率衰減,來加快模型的訓練。理論部分可以看《深度學習之優化算法》。

對於可能造成過擬合的問題,可以用Dropout、早停、L1和L2正則化和數據增強等方法,來緩解過擬合現象。理論部分可以看《深度學習之正則化方法》。

內容還是比較多的,這篇博客先整理如何用TensorFlow實現解決梯度消失、梯度爆炸以及訓練速度太慢等問題的方法,下一篇博客再整理用TensorFlow實現神經網絡的正則化方法。

好,接下來進入到代碼環節。

 

一、解決梯度消失

1、Xavier初始化和He初始化

為了不讓梯度在正向傳播和反向傳播時消失或者爆炸,也就是要保持梯度穩定,那么我們需要神經網絡每層的輸入值的方差等於其輸出值的方差,而用Xavier初始化和He初始化可以近似地做到這一點。

(1)Xavier初始化是針對於Sigmoid函數(也叫Logistic函數)和Tanh函數的初始化方法,有兩套方案:

一套是用高斯分布來初始化權重,對於Sigmoid函數的權重,使其服從如下分布,也就是服從一個均值為0的正態分布,其方差σ2是扇入(輸入值的維度)和扇出(輸出值的維度)的平均值的倒數。

另一套是用均勻分布來初始化權重,對於Sigmoid函數的權重,使其服從[-r , r]上的均勻分布。而r滿足:

Tanh函數的權重初始化見下圖(第二行)。

(2)He初始化是針對ReLU函數的權重初始化方法,同樣可以用高斯分布或均勻分布來做。其分布的參數如下所示(第三行)。

TensorFlow中提供了 tf.variance_scaling_initializer()這個函數來做He初始化,默認是只考慮扇入(也就是ninputs),而不考慮扇出,如果想要像上面的公式那樣用扇入和扇出的平均值來初始化,就要傳入mode="FAN_AVG"這個參數。

當然我有個疑問,那就是He初始化是用在第一個隱藏層還是所有隱藏層呢?如果是用在所有的隱藏層,那么剛學習到的參數就被重新初始化了,那不就楊白勞了?不過看書中的示例代碼,是在所有隱藏層上進行He初始化。

好,這里只列出關鍵的代碼,稍候我們就用He初始化來初始化權重,應用到MINIST數據集的模型訓練中。

X = tf.placeholder(tf.float32, shape=(None, n_inputs), name="X")
he_init = tf.variance_scaling_initializer(mode =“FAN_AVG”)
hidden1 = tf.layers.dense(X, n_hidden1, activation=tf.nn.relu,
                          kernel_initializer=he_init, name="hidden1")

2、選擇更好的損失函數

Sigmoid函數和Tanh函數是兩端飽和型激活函數,容易產生梯度消失問題。而RuLU函數在正值部分的導數都是1,不會梯度飽和,而且近似於線性計算,計算速度賊快,因此隱藏層使用RuLU激活函數是標配。

可是RuLE也有不爭氣的地方,稱為死亡ReLU問題,意思是由於ReLU函數在負值區間的梯度為0,那么在訓練過程中,當神經元輸入的加權和為負值時,這個激活函數會輸出0。然后該神經元就死亡了,無法搶救,永遠沉睡,永遠輸出0。這實在是個悲傷的故事。

於是就出現了幾種ReLU函數的變體:LeakyReLU、ELU、SELU。

(1)LeakyReLU

LeakyReLU函數定義為max(γx, x),在輸入值x < 0時,也能保持一個很小的梯度γ,當神經元非激活時也能更新參數,就像是神經元在峽谷中被妲己的二技能暫時眩暈了,在以后的迭代中還能醒過來繼續carry。這個超參數γ一般設定為0.01。

遺憾的是TensorFlow中沒有定義這個激活函數,我們可以自己寫一個。

X = tf.placeholder(tf.float32, shape=(None, n_inputs), name="X")
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")

(2)ELU

第二種變體是ELU函數,叫做指數線性單元,比LeakyReLU函數效果更好,公式如下。TensorFlow中寫好了這個激活函數供我們調用,只要在定義隱藏層時傳入即可:hidden = tf.layers.dense(X, n_hidden, activation=tf.nn.elu, name="hidden")

(3)SELU

第三種變體是SELU函數,這是2017年提出來的,是ReLU家族中的新貴,效果更好。為啥?因為它與Xavier初始化、Batch Normalization的思想如出一輒。在訓練期間,由使用SELU激活函數的一堆密集層組成的神經網絡將自我歸一化每層的輸出將傾向於在訓練期間保持相同的均值和方差,這解決了消失/爆炸的梯度問題。 而通過調整超參數,使平均值保持接近0,標准差保持接近1,這不就是Batch Normalization在干的事情嗎?因此,這種激活函數顯着地優於其他激活函數。

TensofFlow也寫好了這個激活函數,傳入隱藏層即可: hidden1 = tf.layers.dense(X, n_hidden1, activation=tf.nn.selu,  name="hidden1")。

下面我們就選擇SELU激活函數,應用到MINIST數據集的模型訓練中。

3、Batch Normalization

寫到這里,內心還是非常雞凍的,這個Batch Normalization據說是解決梯度不穩定問題的核武器,廣受追捧。關於Batch Normalization的詳細說明,請移步開頭那里鄙人列出的博客,自認為講得還是蠻清楚的。

用一句話來概括,Batch Normalization就是在用小批量梯度下降算法訓練模型時,對每一個隱藏層在激活之前的輸入值進行標准歸一化(均值為0,方差為1),然后再進行縮放和平移變換,使得每個隱藏層的輸入在分布上保持一致。

操作全蘊涵在下面的公式里:

用TensorFlow來實現Batch Normalization的關鍵語法是tf.layers.batch_normalization(),但是不像定義激活函數那樣寫一行代碼,傳入一個函數就能搞定,Batch Normalization要實現起來有點麻煩。具體在完整的模型代碼中再說明。

4、選擇更好的優化算法

在之前的博客中,我整理了各種優化算法,可以移步開頭列出的博客去查看。優化算法非常多,容易把人搞暈,其實這些優化算法都是在梯度下降算法的基礎上進行優化的:

這些優化算法分別對學習率和損失函數的梯度下手,一般被分為以下三類:

第一類優化算法是在訓練過程中自動調整學習率,比如AdaGrad、RMSProp、AdaDelta,統稱為自適應學習率算法;

第二類優化算法是根據損失函數梯度的方向來改造梯度,比如Momentum、Nesterov加速梯度;

第三類優化算法是結合了前兩類的優點,同時對學習率和損失函數的梯度進行改造,比如Adam,是Momentum和RMSProp的結合。

公式我就不貼了,請移步我的博客,哈哈。

我估計有些人一開始會和我一樣(可能就我一個,因為我比較笨),對小批量梯度下降、隨機梯度下降和這些優化算法的關系不是特別明白,這里也說明一下。

首先梯度下降算法根據一次迭代傳入的樣本量的多少,可以分為批量梯度下降(Batch Gradient Descent,傳入所有訓練集樣本)、隨機梯度下降(Stochastic Gradient Descent,傳入一個訓練樣本)和小批量梯度下降(Mini-Batch Gradient Descent),公式都是上面那個計算公式,只是計算損失所用的樣本量不同。

優化算法則是對梯度下降法的學習率和梯度進行改造,可以應用於以上三種梯度下降算法。

一般而言,直接選擇Adam優化器就行。

定義這幾種優化器的TensorFlow語法整理如下。

-----------------------------------自適應學習率算法----------------------------------------------
# AdaGrad優化器,平滑項選擇默認值就行 optimizer = tf.train.AdagradOptimizer(learning_rate=learning_rate) # RMSProp優化器,衰減率默認為0.9 optimizer = tf.train.RMSPropOptimizer(learning_rate=learning_rate, momentum=0.9, decay=0.9, epsilon=1e-10) -----------------------------------梯度方向優化算法----------------------------------------------
# Momentum優化器,引入了超參數,動量belta,一般設置為0.9 optimizer = tf.train.MomentumOptimizer(learning_rate=learning_rate, momentum=0.9) # Nesterov加速梯度,傳入參數:use_nesterov=True optimizer = tf.train.MomentumOptimizer(learning_rate=learning_rate, momentum=0.9, use_nesterov=True) -----------------------------------學習率與梯度同時優化算法----------------------------------------
# Adam算法,有兩個參數,一個用來調整學習率,一個用來調整梯度 optimizer = tf.train.AdamOptimizer(learning_rate=0.001, beta1=0.9, beta2=0.999, epsilon=1e-08)

5、完整的模型代碼

好,下面就用上面這些解決梯度消失的方法,再次基於MINIST數據集,構建全連接神經網絡,看是否能提升模型的預測結果。

第一步:准備訓練集、驗證集和測試集,生成小批量樣本

這里構建一個具有兩個隱藏層(第一個隱藏層有300個神經元,第二個有100個神經元)的全連接神經網絡,實話說,這只是淺層網絡,而非深層網絡(DNN)。然后准備好訓練集、驗證集和測試集,並打亂順序,生成小批量樣本。

import tensorflow as tf
import numpy as np
from functools import partial
import time
from datetime import timedelta

# 記錄訓練花費的時間
def get_time_dif(start_time):
    end_time = time.time()
    time_dif = end_time - start_time
    #timedelta是用於對間隔進行規范化輸出,間隔10秒的輸出為:00:00:10    
    return timedelta(seconds=int(round(time_dif)))

# 定義輸入層、輸出層和中間隱藏層的神經元數量
n_inputs = 28 * 28  # MNIST
n_hidden1 = 300
n_hidden2 = 100
n_outputs = 10

# 准備訓練數據集、驗證集和測試集,並生成小批量樣本
(X_train, y_train), (X_test, y_test) = tf.keras.datasets.mnist.load_data()

X_train = X_train.astype(np.float32).reshape(-1, 28*28) / 255.0
X_test = X_test.astype(np.float32).reshape(-1, 28*28) / 255.0
y_train = y_train.astype(np.int32)
y_test = y_test.astype(np.int32)
X_valid, X_train = X_train[:5000], X_train[5000:]
y_valid, y_train = y_train[:5000], y_train[5000:]

def shuffle_batch(X, y, batch_size):
    rnd_idx = np.random.permutation(len(X))
    n_batches = len(X) // batch_size
    for batch_idx in np.array_split(rnd_idx, n_batches):
        X_batch, y_batch = X[batch_idx], y[batch_idx]
        yield X_batch, y_batch

第二步:用He初始化、Batch Norm和SELU激活函數來構建網絡層

一行行看以下的代碼,首先定義了一個Batch Norm的動量參數,設置為0.9。什么意思?Batch Norm的操作分為訓練階段和推斷階段,在推斷階段,是用整個訓練集上輸入值的均值和方差來進行標准歸一化,而這兩個全局統計量,就是對訓練階段用小批量樣本計算得到的輸入值的均值和方差,進行指數移動平均來計算的。因此這個動量參數就是用來求移動平均的。

然后下面定義了一個training,原諒我其實沒太懂這個東西,不過這應該是為了做Batch Norm而定義的,我猜是用來學習輸入值的均值、方差、縮放參數和平移參數這四個參數的(全靠猜)。

接下來對權重進行He初始化,注意定義在“dnn”這個名稱范圍之內,后面我們還會對所有的變量進行初始化,所以把He初始化定義在內部,比較好區分一些。

我們使用Python的partial()函數來構造模塊,把相同的參數放進去,避免重復的定義相同的參數。

這里構造了兩個模塊,因為Batch Norm也可以看作一個隱藏層之前的BN層,所以第一個模塊是BN層,相同的函數參數有Batch Norm,training、動量參數。

第二個模塊是隱藏層,相同的參數有全連接層和He初始化,需要注意的是tf.layers.dense()中默認activation=None,也就是說我們不傳入相應的參數,就沒有設置激活函數。這個小細節可謂相當之關鍵,因為為了在每個隱藏層的激活函數之前進行BN操作,我們需要在BN層之后再手動添加 SELU 激活函數,故在這里先不用激活函數。在代碼中可以看到,把BN操作后的輸入值再傳入到tf.nn.selu()中進行激活。

還要注意最后對輸出層的logits值進行BN操作后,並沒有用softmax激活函數求出概率分布,這是因為計算損失時用tf.nn.sparse_softmax_cross_entropy_with_logits這個函數,直接由softmax激活之前的值,計算交叉熵損失。

# 測試時,用整個訓練集的均值和標准差來進行標准化。這些通常在訓練期間使用移動平均值有效地計算,所以有個動量參數。
batch_norm_momentum = 0.9
learning_rate = 0.01

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

# 這里也有個train,不太懂什么情況。
training = tf.placeholder_with_default(False, shape=(), name='training')

with tf.name_scope("dnn"):
    he_init = tf.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.selu(my_batch_norm_layer(hidden1))
    hidden2 = my_dense_layer(bn1, n_hidden2, name="hidden2")
    bn2 = tf.nn.selu(my_batch_norm_layer(hidden2))
    logits_before_bn = my_dense_layer(bn2, n_outputs, name="outputs")
    logits = my_batch_norm_layer(logits_before_bn)

第三步:定義損失函數、Adam優化器,和模型評估指標

關鍵在“train”這一段代碼處,首先定義了Adam優化器,然后定義了一個用來更新Batch Norm參數的op:extra_update_ops。最后通過最小化交叉熵損失來求模型的參數,要基於Batch Norm的參數來定義一個op:training_op。於是這里有兩個op。

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("train"):
    optimizer = tf.train.AdamOptimizer(learning_rate)
    # 這是需要額外更新batch norm的參數
    extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
    # 模型參數的優化依賴於batch norm參數的更新
    with tf.control_dependencies(extra_update_ops):
        training_op = optimizer.minimize(loss)
    
with tf.name_scope("eval"):
    correct = tf.nn.in_top_k(logits, y, 1)
    accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))

第四步:訓練模型和保存模型

這里需要注意的有兩點,我們看 sess.run(training_op, feed_dict={training: True, X: X_batch, y: y_batch})這段代碼。

第一點是本來有兩個op要放入到sess.run()里去執行的,也就是傳入[training_op, extra_update_ops],可是由於前面讓模型的最小化損失函數的操作取決於Batch Norm的更新操作with tf.control_dependencies(extra_update_ops),所以這里只需要把training_op放進去就行。

第二點是在feed_dict中,要把{training: True}放進去,進行Batch Norm參數的學習。

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

n_epochs = 40
batch_size = 200

with tf.Session() as sess:
    init.run()
    start_time = time.time()
    for epoch in range(n_epochs):
        for X_batch, y_batch in shuffle_batch(X_train, y_train, batch_size):
            sess.run(training_op,
                     feed_dict={training: True, X: X_batch, y: y_batch})
        if epoch % 5 ==0 or epoch == 39:   
            accuracy_batch = accuracy.eval(feed_dict={X: X_batch, y: y_batch})
            accuracy_val = accuracy.eval(feed_dict={X: X_valid, y: y_valid})
            print(epoch, "Batch accuracy:", accuracy_batch,"Validation accuracy:", accuracy_val)
    time_dif = get_time_dif(start_time)
    print("\nTime usage:", time_dif)
    save_path = saver.save(sess, "./my_model_final_speed.ckpt")

耗時32秒,最后一輪的驗證精度為98.44%,還不錯。

0 Batch accuracy: 0.975 Validation accuracy: 0.962
5 Batch accuracy: 0.99 Validation accuracy: 0.9772
10 Batch accuracy: 1.0 Validation accuracy: 0.981
15 Batch accuracy: 1.0 Validation accuracy: 0.9822
20 Batch accuracy: 1.0 Validation accuracy: 0.9818
25 Batch accuracy: 1.0 Validation accuracy: 0.9828
30 Batch accuracy: 1.0 Validation accuracy: 0.9802
35 Batch accuracy: 1.0 Validation accuracy: 0.9842
39 Batch accuracy: 1.0 Validation accuracy: 0.9844

Time usage: 0:00:32

第五步:調用模型和測試模型

測試精度為98.08%,還可以。

with tf.Session() as sess:
    saver.restore(sess, "./my_model_final_speed.ckpt") # or better, use save_path
    X_test_20 = X_test[:20]
    # 得到softmax之前的輸出
    Z = logits.eval(feed_dict={X: X_test_20})
    # 得到每一行最大值的索引
    y_pred = np.argmax(Z, axis=1)
    print("Predicted classes:", y_pred)
    print("Actual calsses:   ", y_test[:20])
    # 評估在測試集上的正確率
    acc_test = accuracy.eval(feed_dict={X: X_test, y: y_test})
    print("\nTest_accuracy:", acc_test)
INFO:tensorflow:Restoring parameters from ./my_model_final_speed.ckpt
Predicted classes: [7 2 1 0 4 1 4 9 5 9 0 6 9 0 1 5 9 7 8 4]
Actual calsses:    [7 2 1 0 4 1 4 9 5 9 0 6 9 0 1 5 9 7 3 4]

Test_accuracy: 0.9808

為了和上一篇博客中的“裸機版”全連接神經網絡進行對比,同樣的,我把batch size分別設置為200、100、50、10,得到如下結果。batch size 為50時,預測結果最好,測試精度為98.44%。

batch size = 200       Test_accuracy: 0.9808
batch size = 100 Test_accuracy: 0.9811
batch size = 50 Test_accuracy: 0.9844
batch size = 10 Test_accuracy: 0.9821

“裸機版”全連接神經網絡在不同的batch size 下的測試精度如下。可見運用以上這些加速優化方法,的確提升了模型的准確性。

但是由於是淺層神經網絡,數據量也不是太大,所以提升的效果其實不太明顯。而且由於使用了SELU函數這種非線性激活函數,與使用ReLU激活函數相比,訓練速度反而下降了。

batch_size = 200    Test_accuracy: 0.9576
batch_size = 100 Test_accuracy: 0.9599 batch_size = 50 Test_accuracy: 0.9781 batch_size = 10 Test_accuracy: 0.9812

二、學習率衰減

學習率衰減一方面是為了加快訓練速度,另一方面是為了跳出局部最優解,盡量找到全局最優。

學習率衰減的想法是先從一個比較大的學習率(比如0.1)開始進行梯度下降, 此時損失下降比較快,然后一旦損失下降變得比較慢了就降低學習率。實操中可以分為幾個階段來降低學習率,迭代n步以后就降低學習率,而在每個階段內的學習率是固定的。

其中一種做法叫指數衰減,將學習率設置為步數r的函數,學習率每r步就下降10倍。

AdaGrad,RMSProp和Adam這些優化算法會自動調整學習率,所以一般不再進行學習率衰減,其他的如Momentum優化算法可以用。

用TensorFlow來實現學習率的指數衰減比較簡單,在定義損失和優化器的步驟中,可以進行如下的操作。

with tf.name_scope("train"):
    # 學習率指數衰減的初始化值為0.1
    initial_learning_rate=0.1
    # 10000步后降低學習率
    decay_steps = 10000
    # 每次都衰減10倍。
    decay_rate = 1/10
    # 用來跟蹤當前迭代次數
    global_step = tf.Variable(0, trainable=False, name="global_step")
    # 用學習率指數衰減算法來定義學習率
    learning_rate = tf.train.exponential_decay(initial_learning_rate, global_step, 
                                              decay_steps, decay_rate)
    # 用學習率指數衰減定義的學習率來構建優化器
    optimizer = tf.train.MomentumOptimizer(learning_rate,momentum=0.9)    
    training_op = optimizer.minimize(loss, global_step=global_step)

好了,那我們就在前一部分模型的基礎上,運用學習率指數衰減算法,來進一步優化模型。正如上文所說,Adam算法會自動調整學習率,就用不着再額外去做學習率衰減了,所以我們選擇Momentum優化器來做。

原諒我又要把老長的代碼再貼一遍,真的不是為了讓博客的字數更多!

把batch size分別設置為200、100、50、10,得到測試精度分別為98.45%、98.34%、98.2%、98.4%。可見取batch size=200,取得還不錯的效果。

import tensorflow as tf
import numpy as np
from functools import partial
import time
from datetime import timedelta

# 記錄訓練花費的時間
def get_time_dif(start_time):
    end_time = time.time()
    time_dif = end_time - start_time
    #timedelta是用於對間隔進行規范化輸出,間隔10秒的輸出為:00:00:10    
    return timedelta(seconds=int(round(time_dif)))

# 定義輸入層、輸出層和中間隱藏層的神經元數量
n_inputs = 28 * 28  # MNIST
n_hidden1 = 300
n_hidden2 = 100
n_outputs = 10

# 准備訓練數據集、驗證集和測試集,並生成小批量樣本
(X_train, y_train), (X_test, y_test) = tf.keras.datasets.mnist.load_data()

X_train = X_train.astype(np.float32).reshape(-1, 28*28) / 255.0
X_test = X_test.astype(np.float32).reshape(-1, 28*28) / 255.0
y_train = y_train.astype(np.int32)
y_test = y_test.astype(np.int32)
X_valid, X_train = X_train[:5000], X_train[5000:]
y_valid, y_train = y_train[:5000], y_train[5000:]

def shuffle_batch(X, y, batch_size):
    rnd_idx = np.random.permutation(len(X))
    n_batches = len(X) // batch_size
    for batch_idx in np.array_split(rnd_idx, n_batches):
        X_batch, y_batch = X[batch_idx], y[batch_idx]
        yield X_batch, y_batch

# 測試時,用整個訓練集的均值和標准差來進行標准化。這些通常在訓練期間使用移動平均值有效地計算,所以有個動量參數。

batch_norm_momentum = 0.9

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

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

with tf.name_scope("dnn"):
    he_init = tf.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.selu(my_batch_norm_layer(hidden1))
    hidden2 = my_dense_layer(bn1, n_hidden2, name="hidden2")
    bn2 = tf.nn.selu(my_batch_norm_layer(hidden2))
    logits_before_bn = my_dense_layer(bn2, n_outputs, name="outputs")
    logits = my_batch_norm_layer(logits_before_bn)
    
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("train"):

    initial_learning_rate=0.1
    decay_steps = 10000
    decay_rate = 1/10
    global_step = tf.Variable(0, trainable=False, name="global_step")
    learning_rate = tf.train.exponential_decay(initial_learning_rate, global_step, 
                                              decay_steps, decay_rate)
    # 用學習率指數衰減定義的學習率來構建優化器
    optimizer = tf.train.MomentumOptimizer(learning_rate,momentum=0.9)
    # 這是需要額外更新batch norm的參數
    extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
    # 模型參數的優化依賴與batch norm參數的更新
    with tf.control_dependencies(extra_update_ops):
        training_op = optimizer.minimize(loss)
    
with tf.name_scope("eval"):
    correct = tf.nn.in_top_k(logits, y, 1)
    accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))
    
init = tf.global_variables_initializer()
saver = tf.train.Saver()

n_epochs = 40
batch_size = 50

with tf.Session() as sess:
    init.run()
    start_time = time.time()
    for epoch in range(n_epochs):
        for X_batch, y_batch in shuffle_batch(X_train, y_train, batch_size):
            sess.run(training_op,
                     feed_dict={training: True, X: X_batch, y: y_batch})
        if epoch % 5 ==0 or epoch == 39:   
            accuracy_batch = accuracy.eval(feed_dict={X: X_batch, y: y_batch})
            accuracy_val = accuracy.eval(feed_dict={X: X_valid, y: y_valid})
            print(epoch, "Batch accuracy:", accuracy_batch,"Validation accuracy:", accuracy_val)
    time_dif = get_time_dif(start_time)
    print("\nTime usage:", time_dif)
    save_path = saver.save(sess, "./my_model_final_scheduling.ckpt")
    
with tf.Session() as sess:
    saver.restore(sess, "./my_model_final_scheduling.ckpt") # or better, use save_path
    X_test_20 = X_test[:20]
    # 得到softmax之前的輸出
    Z = logits.eval(feed_dict={X: X_test_20})
    # 得到每一行最大值的索引
    y_pred = np.argmax(Z, axis=1)
    print("Predicted classes:", y_pred)
    print("Actual calsses:   ", y_test[:20])
    # 評估在測試集上的正確率
    acc_test = accuracy.eval(feed_dict={X: X_test, y: y_test})
    print("\nTest_accuracy:", acc_test)

三、解決梯度爆炸-梯度截斷

前面用到的Batch Norm是緩解梯度爆炸問題的一個比較好的方法,而另一個比較好的方法是在反向傳播過程中對梯度進行簡單地截斷,使它們永遠不會超過某個閾值,這稱為梯度截斷(Gradient Clipping)。 這種方法簡單粗暴有效。一般來說,人們現在更喜歡用Batch Norm來解決問題。

用TensorFlow實現梯度截斷的步驟如下:

  • 先調用優化器的compute_gradients()方法來計算和獲取當前的梯度;
  • 然后使用clip_by_value()函數將梯度限制在[-1,1]的區間內;
  • 最后使用優化程序的apply_gradients()方法應用梯度截斷。
threshold = 1.0

with tf.name_scope("train"):
    optimizer = tf.train.AdamOptimizer(learning_rate)
    # 首先計算和獲取梯度
    grads_and_vars = optimizer.compute_gradients(loss)
    # 把超過閾值的梯度截斷在[-1,1]之間
    capped_gvs = [(tf.clip_by_value(grad, -threshold, threshold), var)
              for grad, var in grads_and_vars]
    # 用截斷后的梯度繼續進行訓練
    training_op = optimizer.apply_gradients(capped_gvs)

淺層神經網絡一般是不會產生梯度爆炸的問題,於是我們基於MINIST數據集構建一個包含5個隱藏層的全連接神經網絡。為了突出梯度截斷的操作,這次就不再疊加上面的各種優化技巧了。

選擇Adam優化器,batch size 取100,得到測試精度為96.68%。

import tensorflow as tf
import numpy as np
import time
from datetime import timedelta

# 記錄訓練花費的時間
def get_time_dif(start_time):
    end_time = time.time()
    time_dif = end_time - start_time
    #timedelta是用於對間隔進行規范化輸出,間隔10秒的輸出為:00:00:10    
    return timedelta(seconds=int(round(time_dif)))

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

(X_train, y_train), (X_test, y_test) = tf.keras.datasets.mnist.load_data()

X_train = X_train.astype(np.float32).reshape(-1, 28*28) / 255.0
X_test = X_test.astype(np.float32).reshape(-1, 28*28) / 255.0
y_train = y_train.astype(np.int32)
y_test = y_test.astype(np.int32)
X_valid, X_train = X_train[:5000], X_train[5000:]
y_valid, y_train = y_train[:5000], y_train[5000:]

def shuffle_batch(X, y, batch_size):
    rnd_idx = np.random.permutation(len(X))
    n_batches = len(X) // batch_size
    for batch_idx in np.array_split(rnd_idx, n_batches):
        X_batch, y_batch = X[batch_idx], y[batch_idx]
        yield X_batch, y_batch
        
X = tf.placeholder(tf.float32, shape=(None, n_inputs), name="X")
y = tf.placeholder(tf.int32, 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")
    
learning_rate = 0.01
# 把梯度截斷的閾值設置為1.0
threshold = 1.0

with tf.name_scope("train"):
    optimizer = tf.train.AdamOptimizer(learning_rate)
    # 首先計算和獲取梯度
    grads_and_vars = optimizer.compute_gradients(loss)
    # 把超過閾值的梯度截斷在[-1,1]之間
    capped_gvs = [(tf.clip_by_value(grad, -threshold, threshold), var)
              for grad, var in grads_and_vars]
    # 用截斷后的梯度繼續進行訓練
    training_op = optimizer.apply_gradients(capped_gvs)

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")
    
init = tf.global_variables_initializer()
saver = tf.train.Saver()

n_epochs = 40
batch_size = 100

with tf.Session() as sess:
    init.run()
    start_time = time.time()
    
    for epoch in range(n_epochs):
        for X_batch, y_batch in shuffle_batch(X_train, y_train, batch_size):
            sess.run(training_op, feed_dict={X: X_batch, y: y_batch})
        if epoch % 5 == 0 or epoch == 39:
            acc_batch = accuracy.eval(feed_dict={X: X_batch, y: y_batch})
            acc_val = accuracy.eval(feed_dict={X: X_valid, y: y_valid})
            print(epoch, "Batch accuracy:", acc_batch, "Val accuracy:", acc_val)
            
    time_dif = get_time_dif(start_time)
    print("\nTime usage:", time_dif)
    save_path = saver.save(sess, "./my_model_final_clip.ckpt")

with tf.Session() as sess:
    saver.restore(sess, "./my_model_final_clip.ckpt") # or better, use save_path
    X_test_20 = X_test[:20]
    # 得到softmax之前的輸出
    Z = logits.eval(feed_dict={X: X_test_20})
    # 得到每一行最大值的索引
    y_pred = np.argmax(Z, axis=1)
    print("Predicted classes:", y_pred)
    print("Actual calsses:   ", y_test[:20])
    # 評估在測試集上的正確率
    acc_test = accuracy.eval(feed_dict={X: X_test, y: y_test})
    print("\nTest_accuracy:", acc_test)

 

 

參考資料:

《Hands On Machine Learning with Scikit-Learn and TensorFlow》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM