在上一篇博客《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》