图神经网络中的梯度爆炸问题

Gradient exploding problem in a graph neural network

我有一个梯度爆炸的问题,试了好几天都没能解决。我在 TensorFlow 中实现了一个自定义消息传递图形神经网络,用于从图形数据中预测连续值。每个图表都与一个目标值相关联。图的每个节点由节点属性向量表示,节点之间的边由边属性向量表示。

在消息传递层中,节点属性以某种方式更新(例如,通过聚合其他 node/edge 属性),并返回这些更新的节点属性。

现在,我设法找出我的代码中出现梯度问题的位置。我有以下代码段。

to_concat = [neighbors_mean, e]
z = K.concatenate(to_concat, axis=-1)
output = self.Net(z)

这里,neighbors_mean 是两个节点属性 vivj 之间的逐元素平均值,它们形成具有边属性 e 的边。 Net是单层前馈网络。这样,训练损失在批大小为 32 的大约 30 个 epoch 后突然跳到 NaN。如果批大小为 128,梯度仍然在大约 200 个 epoch 后爆炸。

我发现,在这种情况下,梯度爆炸是因为边缘属性 e。如果我没有将 neighbors_meane 连接起来而只是使用下面的代码,就不会出现梯度爆炸。

output = self.Net(neighbors_mean)

我还可以通过一个 sigmoid 函数发送 e 来避免梯度爆炸,如下所示。但这会降低性能(最终 MAE),因为 e 中的值非线性映射到 0-1 范围。请注意,Rectified Linear Unit (ReLU) 而不是 sigmoid 不起作用。

to_concat = [neighbors_mean, tf.math.sigmoid(e)]
z = K.concatenate(to_concat, axis=-1)
output = self.Net(z)

顺便提一下,e 带有与两个对应节点之间的距离相关的单个值,该距离始终在 0.5-4 范围内。 e.

中没有大值或 NaN

我有一个自定义的损失函数来训练这个模型,但是我发现这不是损失的问题(其他损失也导致了同样的问题)。下面是我的自定义损失函数。请注意,虽然这是一个单输出回归网络,但我的 NN 的最后一层有两个神经元,与预测的均值和 log(sigma) 相关。

def robust_loss(y_true, y_pred):
  """
  Computes the robust loss between labels and predictions.
  """
  mean, sigma = tf.split(y_pred, 2, axis=-1)
  # tried limiting 'sigma' with  sigma = tf.clip_by_value(sigma,-4,1.0) but the gradients still explode
  loss =  np.sqrt(2.0) * K.abs(mean - y_true) * K.exp(-sigma)  + sigma
  return K.mean(loss)

我基本上尝试了网上建议的所有方法来避免梯度爆炸。

  1. 应用渐变裁剪 - Adam(lr, clipnorm=1, clipvalue=5)tf.clip_by_global_norm(gradients, 1.0)
  2. 我的目标变量总是按比例缩放
  3. 权重使用 glorot_uniform 分布进行初始化
  4. 对权重应用正则化
  5. 尝试了更大的批量大小(直到 256,尽管在某些时候会发生延迟梯度爆炸)
  6. 尝试降低学习率

我在这里错过了什么?我绝对知道它与连接 e 有关。但鉴于 0.5e 对我很重要。我还能做些什么来避免模型中的数值溢出?

看起来不错,因为您已经按照大多数解决方案解决了梯度爆炸问题。以下是您可以尝试的所有解决方案的列表

避免梯度爆炸问题的解决方案

  1. 适当的权重初始化:根据使用的激活函数使用适当的权重初始化。

    Initialization Activation Function
    He ReLU & variants
    LeCun SELU
    Glorot Softmax, Logistic, None, Tanh
  2. 重新设计你的神经网络:在神经网络中使用更少的层数and/or使用更小的批量大小

  3. Choosing Non Saturation activation function: 选择正确的具有降低学习率的激活函数

    • ReLU
    • 有漏洞的 ReLU
    • 随机泄漏 ReLU (RReLU)
    • 参数泄漏 ReLU (PReLU)
    • 指数线性单位 (ELU)
  4. 批量归一化:理想情况下使用批量归一化before/after每一层,基于最适合你的数据集的方法。

    • 每层后Paper reference

      model = keras.models.Sequential([
                           keras.layers.Flatten(input_shape=[28, 28]),
                           keras.layers.BatchNormalization(),
                           keras.layers.Dense(300, activation="elu", 
                           kernel_initializer="he_normal"),
                           keras.layers.BatchNormalization(),
                           keras.layers.Dense(100, activation="elu", 
                           kernel_initializer="he_normal"),
                           keras.layers.BatchNormalization(),
                           keras.layers.Dense(10, activation="softmax")
               ])
      
    • 每层前

       model = keras.models.Sequential([
                           keras.layers.Flatten(input_shape=[28, 28]),
                           keras.layers.BatchNormalization(),
                           keras.layers.Dense(300, kernel_initializer="he_normal", use_bias=False),
                           keras.layers.BatchNormalization(),
                           keras.layers.Activation("elu"),
                           keras.layers.Dense(100, kernel_initializer="he_normal", use_bias=False),
                           keras.layers.Activation("elu"),
                           keras.layers.BatchNormalization(),
                           keras.layers.Dense(10, activation="softmax")
               ])
      
  5. 梯度裁剪:好的默认值是 clipnorm=1.0 和 clipvalue=0.5

  6. 确保使用了正确的优化器:由于您使用了 Adam 优化器,请检查其他优化器是否最适合您的情况。有关可用优化器的信息,请参阅 this documentation [SGD、RMSprop、Adam、Adadelta、Adagrad、Adamax、Nadam、Ftrl]

  7. 通过时间截断反向传播:通常适用于 RNNS,请参考此 documentation

  8. 使用LSTM(RNN的解决方案)

  9. 在层上使用权重正则化器:将kernel_regularizer设置为L1或L2。 Weight regularizer document reference

有关详细信息,请参阅 Hands on Machine learning with scikit-learn, keras and tensorflow 书中的第 11 章 Aurélien

感谢这个很棒的调试工具,我解决了这个问题tf.debugging.check_numerics

我最初确定连接 e 是问题所在,然后意识到传递给 e 的值比 neighbors_mean 中的值大得多,后者与 e。一旦将它们连接起来并通过神经网络发送(Net() 在我的代码中),我观察到一些输出为数百个,并且随着训练的进行慢慢达到数千个。

这是有问题的,因为我在消息传递层中有一个 softmax 操作。请注意,softmax 计算指数 (exi/Σexj)。高于 e709 的任何值都会导致 Python 中的数值溢出。这产生了 inf 值,最终所有东西都变成了 nan 是我代码中的问题。所以,这在技术上不是梯度爆炸问题,这就是为什么它不能用梯度裁剪来解决的原因。

我是如何跟踪问题的?

我将 tf.debugging.check_numerics() 片段放在几个 layers/tensors 下,我认为会产生 nan 值。像这样:

tf.debugging.check_numerics(layerN, "LayerN is producing nans!")

一旦层输出在训练期间变为 infnan,就会产生 InvalidArgumentError

Traceback (most recent call last):
  File "trainer.py", line 506, in <module>
    worker.train_model()
  File "trainer.py", line 211, in train_model
    l, tmae = train_step(*batch)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/def_function.py", line 828, in __call__
    result = self._call(*args, **kwds)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/def_function.py", line 855, in _call
    return self._stateless_fn(*args, **kwds)  # pylint: disable=not-callable
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/function.py", line 2943, in __call__
    filtered_flat_args, captured_inputs=graph_function.captured_inputs)  # pylint: disable=protected-access
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/function.py", line 1919, in _call_flat
    ctx, args, cancellation_manager=cancellation_manager))
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/function.py", line 560, in call
    ctx=ctx)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/execute.py", line 60, in quick_execute
    inputs, attrs, num_outputs)
tensorflow.python.framework.errors_impl.InvalidArgumentError:  LayerN is producing nans! : Tensor had NaN values

现在我们知道问题出在哪里了。

如何解决问题

我将内核约束应用于神经网络权重,其输出被传递到 softmax 函数。

layers.Dense(x, name="layer1", kernel_regularizer=regularizers.l2(1e-6), kernel_constraint=min_max_norm(min_value=1e-30, max_value=1.0))

这应该确保所有权重都小于 1,并且该层不会产生大量输出。这在不降低性能的情况下解决了问题。

或者,可以使用