Keras 中的自定义图层 returns NaN 作为渐变。造成这种情况的潜在问题有哪些?

A custom layer in Keras returns NaN as a gradient. What are some potential issues causing this?

我从事一个项目,我们尝试从几何基元重建二维图像。为此,我开发了一个自定义 Keras 层,它输出一个给定几何特征的圆锥体图像。

它的输入是一个形状为batch_size * 5的张量,其中五个数字分别是圆锥顶点的xy坐标,描述圆锥轴线的单位向量的xy坐标,以及圆锥体顶部的角度。

目标是将该层用作编码器-解码器架构中的不可训练解码器。然后我们将向神经网络提供锥体图像。预期的行为是神经网络然后应该学习类似于上述的潜在表示。

当我将这一层合并到一个更大的网络中并尝试对其进行优化时,总会有一些权重最终被更新为 NaN。即使网络像没有激活函数的双神经元隐藏层一样简单,也会发生这种情况。

我已经彻底测试了我的图层。它的输出与我的预期一致。我在实现中找不到任何微不足道的错误(但你应该被警告我对 tensorflow 和 keras 还很陌生)。我已将问题缩小到图层的自动微分。

梯度似乎等于 0.0 或 NaN。我的理解是一些数值不稳定性导致梯度发散。

问题是双重的:

下面是一个最小的工作示例,显示梯度如何最终达到 0.0 或特定值的 NaN。

import numpy as np
from tensorflow.keras import backend as K
from tensorflow.keras.layers import Layer
import tensorflow as tf
import numpy.random as rnd

class Cones(Layer):

    def __init__(self, output_dim, **kwargs):
        super(Cones, self).__init__(**kwargs)
        self.output_dim = output_dim
        coordinates = np.zeros((self.output_dim, self.output_dim, 2))
        for i in range(self.output_dim):
           for j in range(self.output_dim):
              coordinates[i,j,:] = np.array([i,j])

        coordinates = K.constant(coordinates)
        self.coordinates = tf.Variable(initial_value=coordinates, trainable=False)
        self.smooth_sign_width = tf.Variable(initial_value=output_dim, dtype=tf.float32, trainable=False)
        self.grid_width = tf.Variable(initial_value=output_dim, dtype=tf.float32, trainable=False)


    def build(self, input_shape):
        super(Cones, self).build(input_shape)

    def call(self, x):
        center = self.grid_width*x[:,:2]
        center = K.expand_dims(center, axis=1)
        center = K.expand_dims(center, axis=1)

        direction = x[:,2:4]
        direction = K.expand_dims(direction,1)
        direction = K.expand_dims(direction,1)
        direction = K.l2_normalize(direction, axis=-1)

        aperture = np.pi*x[:,4:]
        aperture = K.expand_dims(aperture)

        u = self.coordinates - center
        u = K.l2_normalize(u, axis=-1)

        angle = K.sum(u*direction, axis=-1)
        angle = K.minimum(angle, K.ones_like(angle))
        angle = K.maximum(angle, -K.ones_like(angle))

        angle = tf.math.acos(angle)


        output = self.smooth_sign(aperture-angle)

        output = K.expand_dims(output, -1)
        return output

    def smooth_sign(self, x):
        return tf.math.sigmoid(self.smooth_sign_width*x)


    def compute_output_shape(self, input_shape):
        return (input_shape[0], self.output_dim, self.output_dim, 1)

geom = K.constant([[0.34015268, 0.31530404, -0.6827047, 0.7306944, 0.8521315]])
image = Cones(Nx)(geom)

x0 = geom
y0 = image

with tf.GradientTape() as t:
    t.watch(x0)
    cone = Cones(Nx)(x0)
    error = cone-y0
    error_squared = error*error
    mse = tf.math.reduce_mean(error_squared)

print(t.gradient(mse, x0))

geom = K.constant([[0.742021, 0.25431857, 0.90899783, 0.4168009, 0.58542883]])
image = Cones(Nx)(geom)

x0 = geom
y0 = image

with tf.GradientTape() as t:
    t.watch(x0)
    cone = Cones(Nx)(x0)
    error = cone-y0
    error_squared = error*error
    mse = tf.math.reduce_mean(error_squared)

print(t.gradient(mse, x0))

首先,我回答我自己的问题并把它留在那里,以防将来对某人有所帮助。我不知道这是否是 Whosebug 上公认的礼仪。

通过评论调用函数的后续步骤,我发现问题出在 tf.math.acos。在上面的代码中,我已经遇到了 acos 的问题,这导致我将输入的值剪裁在 -1 和 1 之间。数值问题意味着有时两个单位向量的点积超出了这个范围,其中 acos 被定义。但是,通过这样做,我最终在 1 和 -1 处评估了 acos,它不可微分,因此梯度中的 NaN。

为了解决这个问题,我首先改变了我的方法来计算两个向量之间的角度,使用 this scicomp stack exchange answer。然后,我剪裁了我执行计算的范围以避免 sqrt 在 0 处的不可微性。更准确地说,每当我有 c > 1.95 时,我将角度四舍五入到 pi,并且每当我有 c < 0.05 时,我将角度四舍五入为 0。