文章目錄
給定誤差函數,學習率,甚至目標變量的大小,訓練神經網絡可能變得不穩定。訓練期間權重的較大更新會導致數值上溢或下溢,通常稱爲梯度爆炸(gradients exploding)。
梯度爆炸在遞歸神經網絡中更爲常見,例如LSTM,因爲梯度的累積在數百個輸入時間步長上展開。
梯度爆炸的一種常見且相對容易的解決方案是:在通過網絡向後傳播誤差並使用其更新權重之前,更改誤差的導數。兩種方法包括:給定選定的向量範數( vector norm)來重新縮放梯度;以及裁剪超出預設範圍的梯度值。這些方法一起被稱爲梯度裁剪(gradient clipping)。
1. 梯度爆炸和裁剪
使用隨機梯度下降優化算法訓練神經網絡。這首先需要在一個或多個訓練樣本上估算損失,然後計算損失的導數,該導數通過網絡反向傳播,以更新權重。使用學習率控制的反向傳播誤差的一小部分來更新權重。
權重的更新可能會很大,以至於權重的數值精度超出或低於該數值精度。權重在上溢或下溢時可以取“NaN”或“Inf”值,但網絡將毫無用處,因爲信號流過無效權重時永遠預測NaN值。權重的上溢或下溢是指網絡訓練過程的不穩定性,並且由於不穩定的訓練過程導致網絡無法進行訓練,從而導致模型實質上是無用的,因此被稱爲梯度爆炸。
在給定的神經網絡(例如卷積神經網絡或多層感知器)中,可能由於配置選擇不當而發生梯度爆炸:
- 學習率選擇不當會導致較大的權重更新。
- 準備的數據有很多噪聲,導致目標變量差異很大。
- 損失函數選擇不當,導致計算出較大的誤差值。
在遞歸神經網絡(例如長短期記憶網絡)中容易出現梯度爆炸。通常,可以通過精心配置網絡模型來避免爆炸梯度,例如,選擇較小的學習速率,按比例縮放目標變量和標準損失函數。儘管如此,對於具有大量輸入時間步長的遞歸網絡,梯度爆炸仍然是一個需要着重考慮的問題。
梯度爆炸的一種常見解決方法是先更改誤差導數,然後通過網絡反向傳播誤差導數,然後使用它來更新權重。通過重新縮放誤差導數,權重的更新也將被重新縮放,從而大大降低了上溢或下溢的可能性。更新誤差導數的主要方法有兩種:
- 梯度縮放(Gradient Scaling)
- 梯度裁剪(Gradient Clipping)
梯度縮放涉及對誤差梯度向量進行歸一化,以使向量範數大小等於定義的值,例如1.0。只要它們超過閾值,就重新縮放它們。如果漸變超出了預期範圍,則漸變裁剪會強制將漸變值(逐個元素)強制爲特定的最小值或最大值。這些方法通常簡稱爲梯度裁剪。
當傳統的梯度下降算法建議進行一個非常大的步長時,梯度裁剪將步長減小到足夠小,以至於它不太可能走到梯度最陡峭的下降方向的區域之外。
它是一種僅解決訓練深度神經網絡模型的數值穩定性,而不能改進網絡性能的方法。
梯度向量範數或預設範圍的值可以通過反覆試驗來配置,可以使用文獻中使用的常用值,也可以先通過實驗觀察通用向量範數或範圍,然後選擇一個合理的值。
對於網絡中的所有層,通常都使用相同的梯度裁剪配置。不過,在某些示例中,與隱藏層相比,輸出層中允許更大範圍的誤差梯度。
2. TensorFlow.Keras 實現
2.1 梯度範數縮放(Gradient Norm Scaling)
梯度範數縮放:在梯度向量的L2向量範數(平方和)超過閾值時,將損失函數的導數更改爲具有給定的向量範數。
例如,可以將範數指定爲1.0,這意味着,如果梯度的向量範數超過1.0,則向量中的值將重新縮放,以使向量範數等於1.0。在Keras中通過在優化器上指定 clipnorm
參數實現:
....
opt = SGD(lr=0.01, momentum=0.9, clipnorm=1.0)
2.2 梯度值裁剪(Gradient Value Clipping)
如果梯度值小於負閾值或大於正閾值,則梯度值剪切將損失函數的導數剪切爲給定值。例如,可以將範數指定爲0.5,這意味着如果梯度值小於-0.5,則將其設置爲-0.5,如果梯度值大於0.5,則將其設置爲0.5。通過在優化器上指定 clipvalue
參數實現:
...
opt = SGD(lr=0.01, momentum=0.9, clipvalue=0.5)
3. 實例
通過一個簡單的MLP迴歸問題來說明梯度裁剪的作用。
3.1 梯度爆炸 MLP
from sklearn.datasets import make_regression
from keras.layers import Dense
from keras.models import Sequential
from keras.optimizers import SGD
import matplotlib.pyplot as plt
plt.rcParams['figure.dpi'] = 150
# 構造迴歸問題數據集
X, y = make_regression(n_samples=1000, n_features=20, noise=0.1, random_state=1)
# 劃分訓練集和驗證集
n_train = 500
trainX, testX = X[:n_train, :], X[n_train:, :]
trainy, testy = y[:n_train], y[n_train:]
# 定義模型
model = Sequential()
model.add(Dense(25, input_dim=20, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(1, activation='linear'))
# 編譯模型
model.compile(loss='mean_squared_error', optimizer=SGD(lr=0.01, momentum=0.9))
# 訓練模型
history = model.fit(trainX, trainy, validation_data=(testX, testy), epochs=100, verbose=0)
# 評估模型
train_mse = model.evaluate(trainX, trainy, verbose=0)
test_mse = model.evaluate(testX, testy, verbose=0)
print('Train: %.3f, Test: %.3f' % (train_mse, test_mse))
# 繪製損失曲線
plt.title('Mean Squared Error')
plt.plot(history.history['loss'], label='train')
plt.plot(history.history['val_loss'], label='test')
plt.legend()
plt.show()
在這種情況下,該模型無法學習,從而導致對NaN值的預測。給定非常大的誤差,然後在訓練中針對權重更新計算出的誤差梯度,模型權重會爆炸。傳統的解決方案是使用標準化或歸一化來重新調整目標變量。不過,本文使用替代方法–梯度修剪。
3.2 梯度範數縮放 MLP
from sklearn.datasets import make_regression
from keras.layers import Dense
from keras.models import Sequential
from keras.optimizers import SGD
import matplotlib.pyplot as plt
plt.rcParams['figure.dpi'] = 150
# 構造迴歸問題數據集
X, y = make_regression(n_samples=1000, n_features=20, noise=0.1, random_state=1)
# 劃分訓練集和驗證集
n_train = 500
trainX, testX = X[:n_train, :], X[n_train:, :]
trainy, testy = y[:n_train], y[n_train:]
# 定義模型
model = Sequential()
model.add(Dense(25, input_dim=20, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(1, activation='linear'))
# 編譯模型
opt = SGD(lr=0.01, momentum=0.9, clipnorm=1.0)
model.compile(loss='mean_squared_error', optimizer=opt)
# 訓練模型
history = model.fit(trainX, trainy, validation_data=(testX, testy), epochs=100, verbose=0)
# 評估模型
train_mse = model.evaluate(trainX, trainy, verbose=0)
test_mse = model.evaluate(testX, testy, verbose=0)
print('Train: %.3f, Test: %.3f' % (train_mse, test_mse))
# 繪製損失曲線
plt.title('Mean Squared Error')
plt.plot(history.history['loss'], label='train')
plt.plot(history.history['val_loss'], label='test')
plt.legend()
plt.show()
該圖顯示了損失在20個epoch內從20000以上的大數值迅速下降到100以下的小數值。
3.3 梯度值裁剪 MLP
from sklearn.datasets import make_regression
from keras.layers import Dense
from keras.models import Sequential
from keras.optimizers import SGD
import matplotlib.pyplot as plt
plt.rcParams['figure.dpi'] = 150
# 構造迴歸問題數據集
X, y = make_regression(n_samples=1000, n_features=20, noise=0.1, random_state=1)
# 劃分訓練集和驗證集
n_train = 500
trainX, testX = X[:n_train, :], X[n_train:, :]
trainy, testy = y[:n_train], y[n_train:]
# 定義模型
model = Sequential()
model.add(Dense(25, input_dim=20, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(1, activation='linear'))
# 編譯模型
opt = SGD(lr=0.01, momentum=0.9, clipvalue=5.0)
model.compile(loss='mean_squared_error', optimizer=opt)
# 訓練模型
history = model.fit(trainX, trainy, validation_data=(testX, testy), epochs=100, verbose=0)
# 評估模型
train_mse = model.evaluate(trainX, trainy, verbose=0)
test_mse = model.evaluate(testX, testy, verbose=0)
print('Train: %.3f, Test: %.3f' % (train_mse, test_mse))
# 繪製損失曲線
plt.title('Mean Squared Error')
plt.plot(history.history['loss'], label='train')
plt.plot(history.history['val_loss'], label='test')
plt.legend()
plt.show()
該圖表明,該模型可以快速學習問題,僅在幾個訓練週期內就損失了不到100的MSE。
參考:
https://machinelearningmastery.com/how-to-avoid-exploding-gradients-in-neural-networks-with-gradient-clipping/