Пользовательский слой в Keras возвращает NaN в качестве градиента. Каковы некоторые потенциальные проблемы, вызывающие это? - PullRequest
1 голос
/ 11 апреля 2020

Я работаю над проектом, в котором мы пытаемся восстановить 2D-изображение из примитивов геометрии c. С этой целью я разработал пользовательский слой Keras, который выводит изображение конуса с учетом его геометрии c характеристик.

Его вход представляет собой тензор формы batch_size * 5, где пять чисел представляют собой xy координаты вершины конуса, координаты xy единичного вектора, описывающего ось конуса, и угол на вершине конуса.

Цель состоит в том, чтобы использовать этот слой в качестве не обучаемого декодер в архитектуре кодер-декодер. Затем мы будем кормить нейронную сеть конусными изображениями. Ожидаемое поведение состоит в том, что нейронная сеть должна затем выучить скрытое представление, подобное описанному выше.

Когда я включаю этот уровень в большую сеть и пытаюсь оптимизировать его, неизменно некоторые веса в конечном итоге обновляются до NaN. Это происходит даже в такой простой сети, как скрытый слой с двумя нейронами без функций активации.

Я тщательно проверил свой слой. Его вывод соответствует тому, что я ожидаю. Я не могу найти ни одной тривиальной ошибки в реализации (но вы должны быть предупреждены, я все еще довольно новичок в tenorflow и keras). Я сузил вопрос до автоматической c дифференциации слоя.

Градиент, по-видимому, равен либо 0,0, либо NaN. Насколько я понимаю, некоторая численная нестабильность приводит к тому, что градиент расходится.

Вопрос состоит из двух частей:

  • в чем здесь основная причина?

  • как я могу это исправить?

Ниже приведен минимальный рабочий пример, показывающий, как градиент увеличивается до 0,0 или NaN для указанных значений c.

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))

1 Ответ

0 голосов
/ 12 апреля 2020

Прежде всего, я отвечаю на свой вопрос и оставляю его там на случай, если он может кому-то помочь в будущем. Я не знаю, является ли это общепринятым этикетом в StackOverflow.

Комментируя последовательные шаги функции вызова, я обнаружил, что проблема была в tf.math.acos. В приведенном выше коде у меня уже была проблема с acos, из-за которой я обрезал значения, которые я вводил, между -1 и 1. Числовые проблемы означали, что иногда скалярное произведение двух единичных векторов выходило за пределы этого диапазона, где acos определяется. Тем не менее, в результате я оценил acos в 1 и -1, где он не дифференцируем, следовательно, NaN в градиенте.

Чтобы исправить эту проблему, я сначала изменил свой метод для вычисления угол между двумя векторами, используя этот ответ обмена стека scicomp . Затем я обрезал диапазон, в котором я выполняю вычисления, чтобы избежать недифференцируемости sqrt в 0. Точнее, когда у меня есть c > 1.95, я округляю угол до pi, и всякий раз, когда у меня есть c < 0.05 Я округляю угол до 0.

...