如何使用梯度裁剪(Gradient Clipping)避免梯度爆炸

如何使用梯度裁剪(Gradient Clipping)避免梯度爆炸_第1张图片
给定误差函数,学习率,甚至目标变量的大小,训练神经网络可能变得不稳定。训练期间权重的较大更新会导致数值上溢或下溢,通常称为梯度爆炸(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值的预测。给定非常大的误差,然后在训练中针对权重更新计算出的误差梯度,模型权重会爆炸。传统的解决方案是使用标准化或归一化来重新调整目标变量。不过,本文使用替代方法–梯度修剪。
如何使用梯度裁剪(Gradient Clipping)避免梯度爆炸_第2张图片
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()

如何使用梯度裁剪(Gradient Clipping)避免梯度爆炸_第3张图片
该图显示了损失在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()

如何使用梯度裁剪(Gradient Clipping)避免梯度爆炸_第4张图片

你可能感兴趣的:(tensorflow,python,tensorflow,深度学习)