Implementación de una capa de función Bump generalizable entrenable en Keras / Tensorflow


8

Estoy tratando de codificar la siguiente variante de la función Bump , aplicada por componentes:

ecuación generalizada de la función de relieve,

donde σ es entrenable; pero no funciona (los errores se informan a continuación).


Mi intento:

Esto es lo que he codificado hasta ahora (si ayuda). Supongamos que tengo dos funciones (por ejemplo):

  def f_True(x):
    # Compute Bump Function
    bump_value = 1-tf.math.pow(x,2)
    bump_value = -tf.math.pow(bump_value,-1)
    bump_value = tf.math.exp(bump_value)
    return(bump_value)

  def f_False(x):
    # Compute Bump Function
    x_out = 0*x
    return(x_out)

class trainable_bump_layer(tf.keras.layers.Layer):

    def __init__(self, *args, **kwargs):
        super(trainable_bump_layer, self).__init__(*args, **kwargs)

    def build(self, input_shape):
        self.threshold_level = self.add_weight(name='threshlevel',
                                    shape=[1],
                                    initializer='GlorotUniform',
                                    trainable=True)

    def call(self, input):
        # Determine Thresholding Logic
        The_Logic = tf.math.less(input,self.threshold_level)
        # Apply Logic
        output_step_3 = tf.cond(The_Logic, 
                                lambda: f_True(input),
                                lambda: f_False(input))
        return output_step_3

Reporte de error:

    Train on 100 samples
Epoch 1/10
WARNING:tensorflow:Gradients do not exist for variables ['reconfiguration_unit_steps_3_3/threshlevel:0'] when minimizing the loss.
WARNING:tensorflow:Gradients do not exist for variables ['reconfiguration_unit_steps_3_3/threshlevel:0'] when minimizing the loss.
 32/100 [========>.....................] - ETA: 3s

...

tensorflow:Gradients do not exist for variables 

Además, no parece aplicarse por componentes (además del problema no entrenable). ¿Cual podría ser el problema?


¿Cuál es la dimensión de la input? ¿Es un escalar?
Vlad

1
Hola @ProbablyAHuman, ¿puedes proporcionar un código reproducible mínimo para tu escenario y especificar cómo exactamente no está funcionando?
TF_Support

1
@TF_Support Agregué detalles de mi objetivo, así como el informe de error ...
FlabbyTheKatsu

¿Sigma es entrenable?
Daniel Möller

¿Podría compartir el gráfico de lo que desea y qué puede variar en este gráfico?
Daniel Möller

Respuestas:


2

¡Estoy un poco sorprendido de que nadie haya mencionado la razón principal (y única) de la advertencia dada! Como parece, se supone que ese código implementa la variante generalizada de la función Bump; sin embargo, solo eche un vistazo a las funciones implementadas nuevamente:

def f_True(x):
    # Compute Bump Function
    bump_value = 1-tf.math.pow(x,2)
    bump_value = -tf.math.pow(bump_value,-1)
    bump_value = tf.math.exp(bump_value)
    return(bump_value)

def f_False(x):
    # Compute Bump Function
    x_out = 0*x
    return(x_out)

El error es evidente: ¡ no se puede utilizar el peso entrenable de la capa en estas funciones! Por lo tanto, no sorprende que reciba el mensaje que dice que no existe un gradiente para eso: no lo está utilizando en absoluto, ¡así que no hay gradiente para actualizarlo! Más bien, esta es exactamente la función Bump original (es decir, sin peso entrenable).

Pero, podría decir que: "al menos, usé el peso entrenable en la condición de tf.cond, ¿entonces debe haber algunos gradientes?"; Sin embargo, no es así y déjame aclarar la confusión:

  • En primer lugar, como también ha notado, estamos interesados ​​en el acondicionamiento basado en elementos. Así que en lugar de tf.condque necesita utilizar tf.where.

  • El otro concepto erróneo es afirmar que ya que tf.lessse usa como condición, y dado que no es diferenciable, es decir, no tiene gradiente con respecto a sus entradas (lo cual es cierto: no hay gradiente definido para una función con salida booleana wrt su real- entradas valiosas!), entonces eso da como resultado la advertencia dada!

    • Eso simplemente está mal! La derivada aquí se tomaría de la salida del peso entrenable de la capa wrt, y la condición de selección NO está presente en la salida. Más bien, es solo un tensor booleano que determina la rama de salida que se seleccionará. ¡Eso es! La derivada de la condición no se toma y nunca será necesaria. Entonces esa no es la razón de la advertencia dada; la razón es única y solo lo que mencioné anteriormente: no hay contribución de peso entrenable en la salida de la capa. (Nota: si el punto sobre la condición es un poco sorprendente para usted, piense en un ejemplo simple: la función ReLU, que se define como relu(x) = 0 if x < 0 else x. Si la derivada de la condición, es decirx < 0, se considera / necesita, lo que no existe, ¡entonces no podríamos usar ReLU en nuestros modelos y entrenarlos utilizando métodos de optimización basados ​​en gradientes!)

(Nota: a partir de aquí, me referiría y denotaría el valor umbral como sigma , como en la ecuación).

¡Todo bien! Encontramos la razón detrás del error en la implementación. ¿Podríamos arreglar esto? ¡Por supuesto! Aquí está la implementación de trabajo actualizada:

import tensorflow as tf
from tensorflow.keras.initializers import RandomUniform
from tensorflow.keras.constraints import NonNeg

class BumpLayer(tf.keras.layers.Layer):
    def __init__(self, *args, **kwargs):
        super(BumpLayer, self).__init__(*args, **kwargs)

    def build(self, input_shape):
        self.sigma = self.add_weight(
            name='sigma',
            shape=[1],
            initializer=RandomUniform(minval=0.0, maxval=0.1),
            trainable=True,
            constraint=tf.keras.constraints.NonNeg()
        )
        super().build(input_shape)

    def bump_function(self, x):
        return tf.math.exp(-self.sigma / (self.sigma - tf.math.pow(x, 2)))

    def call(self, inputs):
        greater = tf.math.greater(inputs, -self.sigma)
        less = tf.math.less(inputs, self.sigma)
        condition = tf.logical_and(greater, less)

        output = tf.where(
            condition, 
            self.bump_function(inputs),
            0.0
        )
        return output

Algunos puntos con respecto a esta implementación:

  • Hemos reemplazado tf.condcon el tf.wherefin de hacer un condicionamiento basado en elementos.

  • Además, como puede ver, a diferencia de su implementación, que solo verificó un lado de la desigualdad, estamos usando tf.math.less, tf.math.greatery también tf.logical_andpara averiguar si los valores de entrada tienen magnitudes menores que sigma(alternativamente, podríamos hacer esto usando solo tf.math.absy tf.math.less; no hay diferencia !). Y repitámoslo: el uso de funciones de salida booleana de esta manera no causa ningún problema y no tiene nada que ver con derivados / gradientes.

  • También estamos utilizando una restricción de no negatividad en el valor sigma aprendido por capa. ¿Por qué? Porque los valores sigma inferiores a cero no tienen sentido (es decir, el rango (-sigma, sigma)está mal definido cuando sigma es negativo).

  • Y teniendo en cuenta el punto anterior, nos encargamos de inicializar el valor sigma correctamente (es decir, a un pequeño valor no negativo).

  • Y también, ¡por favor no hagas cosas como 0.0 * inputs! Es redundante (y un poco raro) y es equivalente a 0.0; y ambos tienen un gradiente de 0.0(wrt inputs). Multiplicar cero con un tensor no agrega nada ni resuelve ningún problema existente, ¡al menos no en este caso!

Ahora, probémoslo para ver cómo funciona. Escribimos algunas funciones auxiliares para generar datos de entrenamiento basados ​​en un valor sigma fijo, y también para crear un modelo que contenga un único BumpLayercon forma de entrada de (1,). Veamos si podría aprender el valor sigma que se utiliza para generar datos de entrenamiento:

import numpy as np

def generate_data(sigma, min_x=-1, max_x=1, shape=(100000,1)):
    assert sigma >= 0, 'Sigma should be non-negative!'
    x = np.random.uniform(min_x, max_x, size=shape)
    xp2 = np.power(x, 2)
    condition = np.logical_and(x < sigma, x > -sigma)
    y = np.where(condition, np.exp(-sigma / (sigma - xp2)), 0.0)
    dy = np.where(condition, xp2 * y / np.power((sigma - xp2), 2), 0)
    return x, y, dy

def make_model(input_shape=(1,)):
    model = tf.keras.Sequential()
    model.add(BumpLayer(input_shape=input_shape))

    model.compile(loss='mse', optimizer='adam')
    return model

# Generate training data using a fixed sigma value.
sigma = 0.5
x, y, _ = generate_data(sigma=sigma, min_x=-0.1, max_x=0.1)

model = make_model()

# Store initial value of sigma, so that it could be compared after training.
sigma_before = model.layers[0].get_weights()[0][0]

model.fit(x, y, epochs=5)

print('Sigma before training:', sigma_before)
print('Sigma after training:', model.layers[0].get_weights()[0][0])
print('Sigma used for generating data:', sigma)

# Sigma before training: 0.08271004
# Sigma after training: 0.5000002
# Sigma used for generating data: 0.5

¡Sí, podría aprender el valor de sigma utilizado para generar datos! Pero, ¿está garantizado que realmente funcione para todos los valores diferentes de datos de entrenamiento e inicialización de sigma? ¡La respuesta es no! En realidad, es posible que ejecute el código anterior y obtenga nanel valor de sigma después del entrenamiento, ¡o infel valor de pérdida! ¿Entonces, cuál es el problema? ¿Por qué esto nano infvalores pueden ser producidos? Discutamos a continuación ...


Tratando con la estabilidad numérica

Una de las cosas importantes a tener en cuenta, cuando se construye un modelo de aprendizaje automático y se utilizan métodos de optimización basados ​​en gradientes para entrenarlos, es la estabilidad numérica de las operaciones y los cálculos en un modelo. Cuando una operación o su gradiente generan valores extremadamente grandes o pequeños, es casi seguro que interrumpiría el proceso de entrenamiento (por ejemplo, esa es una de las razones detrás de la normalización de los valores de píxeles de imagen en CNN para evitar este problema).

Entonces, echemos un vistazo a esta función de relieve generalizada (y descartemos el límite por ahora). Es obvio que esta función tiene singularidades (es decir, puntos donde la función o su gradiente no están definidos) en x^2 = sigma(es decir, cuándo x = sqrt(sigma)o x=-sqrt(sigma)). El siguiente diagrama animado muestra la función de relieve (la línea roja continua), su derivada wrt sigma (la línea verde punteada) x=sigmay las x=-sigmalíneas (dos líneas azules discontinuas verticales), cuando sigma comienza desde cero y se incrementa a 5:

Función de relieve generalizado cuando sigma comienza desde cero y aumenta a cinco.

Como puede ver, alrededor de la región de singularidades, la función no se comporta bien para todos los valores de sigma, en el sentido de que tanto la función como su derivada toman valores extremadamente grandes en esas regiones. Entonces, dado un valor de entrada en esas regiones para un valor particular de sigma, se generarían valores de salida y gradiente explosivos, de ahí la cuestión del infvalor de pérdida.

Aún más, hay un comportamiento problemático tf.whereque causa la emisión de nanvalores para la variable sigma en la capa: sorprendentemente, si el valor producido en la rama inactiva de tf.wherees extremadamente grande o inf, lo que con la función de relieve da como resultado infvalores extremadamente grandes o de gradiente , entonces el gradiente de tf.wheresería nan, a pesar del hecho de que infestá en la rama inactiva y ni siquiera está seleccionado (¡vea este tema de Github que trata exactamente esto!)

Entonces, ¿hay alguna solución para este comportamiento tf.where? Sí, en realidad hay un truco para resolver este problema de alguna manera que se explica en esta respuesta : básicamente podemos usar un adicional tf.wherepara evitar que la función se aplique en estas regiones. En otras palabras, en lugar de aplicar self.bump_functioncualquier valor de entrada, filtramos aquellos valores que NO están en el rango (-self.sigma, self.sigma)(es decir, el rango real al que se debe aplicar la función) y en su lugar alimentamos la función con cero (que siempre produce valores seguros, es decir es igual a exp(-1)):

     output = tf.where(
            condition, 
            self.bump_function(tf.where(condition, inputs, 0.0)),
            0.0
     )

La aplicación de esta solución resolvería por completo el problema de los nanvalores para sigma. Evaluémoslo en los valores de datos de entrenamiento generados con diferentes valores sigma y veamos cómo funcionaría:

true_learned_sigma = []
for s in np.arange(0.1, 10.0, 0.1):
    model = make_model()
    x, y, dy = generate_data(sigma=s, shape=(100000,1))
    model.fit(x, y, epochs=3 if s < 1 else (5 if s < 5 else 10), verbose=False)
    sigma = model.layers[0].get_weights()[0][0]
    true_learned_sigma.append([s, sigma])
    print(s, sigma)

# Check if the learned values of sigma
# are actually close to true values of sigma, for all the experiments.
res = np.array(true_learned_sigma)
print(np.allclose(res[:,0], res[:,1], atol=1e-2))
# True

¡Podría aprender todos los valores sigma correctamente! Eso es bueno. Esa solución funcionó! Sin embargo, hay una advertencia: se garantiza que funcionará correctamente y aprenderá cualquier valor sigma si los valores de entrada a esta capa son mayores que -1 y menores que 1 (es decir, este es el caso predeterminado de nuestra generate_datafunción); de lo contrario, todavía existe el problema del infvalor de pérdida que podría suceder si los valores de entrada tienen una magnitud mayor que 1 (consulte los puntos 1 y 2 a continuación).


Aquí hay algunos alimentos para pensar para los curiosos y la mente interesada:

  1. Se acaba de mencionar que si los valores de entrada a esta capa son mayores que 1 o menores que -1, puede causar problemas. ¿Puedes discutir por qué este es el caso? (Sugerencia: use el diagrama animado anterior y considere los casos en que sigma > 1el valor de entrada esté entre sqrt(sigma)y sigma(o entre -sigmay -sqrt(sigma)).

  2. ¿Puede proporcionar una solución para el problema en el punto n. ° 1, es decir, que la capa podría funcionar para todos los valores de entrada? (Sugerencia: al igual que la solución alternativa tf.where, piense en cómo puede filtrar aún más los valores inseguros en los que se podría aplicar la función de relieve y producir una salida / gradiente explosivo).

  3. Sin embargo, si no está interesado en solucionar este problema y desea utilizar esta capa en un modelo como está ahora, ¿cómo garantizaría que los valores de entrada para esta capa siempre estén entre -1 y 1? (Sugerencia: como una solución, hay una función de activación de uso común que produce valores exactamente en este rango y podría usarse potencialmente como la función de activación de la capa que está antes de esta capa).

  4. Si echa un vistazo al último fragmento de código, verá que lo hemos utilizado epochs=3 if s < 1 else (5 if s < 5 else 10). ¿Porqué es eso? ¿Por qué los grandes valores de sigma necesitan más épocas para aprender? (Sugerencia: nuevamente, use el diagrama animado y considere la derivada de la función para valores de entrada entre -1 y 1 a medida que aumenta el valor sigma. ¿Cuál es su magnitud?)

  5. Es lo que también necesitamos comprobar los datos de entrenamiento generados por cualquier nan, info los valores extremadamente grandes yy filtrarlos a cabo? (Sugerencia: sí, si sigma > 1y el rango de valores, es decir, min_xy max_x, quedan fuera de (-1, 1); de lo contrario, ¡no, eso no es necesario! ¿Por qué es eso? ¡Dejado como ejercicio!)


1
Buen trabajo. @ProbablyAHuman esta debería ser la respuesta aceptada.
rvinas

@hoy. Esto es genial, creo, posiblemente la respuesta más detallada / precisa / rigurosa que he visto en cualquier pila. ¡Muchas gracias!
FlabbyTheKatsu

4

Desafortunadamente, ninguna operación para verificar si xestá dentro (-σ, σ)será diferenciable y, por lo tanto, σ no se puede aprender a través de ningún método de descenso de gradiente. Específicamente, no es posible calcular los gradientes con respecto a self.threshold_levelporque tf.math.lessno es diferenciable con respecto a la condición.

Con respecto al condicional en cuanto a elementos, puede usar tf.where para seleccionar elementos de f_True(input)o f_False(input)según los valores booleanos de la condición según los componentes. Por ejemplo:

output_step_3 = tf.where(The_Logic, f_True(input), f_False(input))

NOTA: respondí en función del código proporcionado, donde self.threshold_levelno se usa f_Trueni en f_False. Si self.threshold_levelse usa en esas funciones como en la fórmula provista, la función, por supuesto, será diferenciable con respecto a self.threshold_level.

Actualizado el 19/04/2020: Gracias @today por la aclaración .


Debe haber un truco de codificación para que sea entrenable ...
FlabbyTheKatsu

Me temo que ningún truco de implementación mágica lo hará entrenable si las matemáticas no funcionan ...
rvinas

"El mensaje de error indica precisamente esto: no es posible calcular los gradientes con respecto a self.threshold_level porque tf.math.less no es diferenciable con respecto a sus entradas". -> El mensaje de advertencia no tiene nada que ver con el uso tf.math.lessen la condición y el hecho de que no es diferenciable. La condición no necesita ser diferenciable para que esto funcione. El error radica en el hecho de que el peso entrenable no se utiliza en absoluto para producir la salida de la capa (es decir, no hay rastro de ella en la salida). Consulte la primera parte de mi respuesta para obtener más información al respecto.
hoy

De acuerdo, eso no es lo que dice el mensaje de advertencia y rectificaré mi redacción. Sin embargo, el punto sigue siendo el mismo: no puede tener una operación para verificar si una variable está dentro de un rango específico y esperar que sea diferenciable con respecto a la variable límite. Dicho esto, si esta variable se usa para el cálculo de la salida (que ni siquiera noté en la fórmula, debo admitirlo), por supuesto, tendrá gradientes.
rvinas

3

Le sugiero que pruebe una distribución normal en lugar de una protuberancia. En mis pruebas aquí, esta función de relieve no se comporta bien (no puedo encontrar un error pero no lo descarto, pero mi gráfico muestra dos golpes muy agudos, lo que no es bueno para las redes)

Con una distribución normal, obtendría una protuberancia regular y diferenciable cuya altura, ancho y centro puede controlar.

Entonces, puedes probar esta función:

y = a * exp ( - b * (x - c)²)

Pruébelo en algún gráfico y vea cómo se comporta.

Para esto:

class trainable_bump_layer(tf.keras.layers.Layer):

    def __init__(self, *args, **kwargs):
        super(trainable_bump_layer, self).__init__(*args, **kwargs)

    def build(self, input_shape):

        #suggested shape (has a different kernel for each input feature/channel)
        shape = tuple(1 for _ in input_shape[:-1]) + input_shape[-1:]

        #for your desired shape of only 1:
        shape = tuple(1 for _ in input_shape) #all ones

        #height
        self.kernel_a = self.add_weight(name='kernel_a ',
                                    shape=shape
                                    initializer='ones',
                                    trainable=True)

        #inverse width
        self.kernel_b = self.add_weight(name='kernel_b',
                                    shape=shape
                                    initializer='ones',
                                    trainable=True)

        #center
        self.kernel_c = self.add_weight(name='kernel_c',
                                    shape=shape
                                    initializer='zeros',
                                    trainable=True)

    def call(self, input):
        exp_arg = - self.kernel_b * K.square(input - self.kernel_c)
        return self.kernel_a * K.exp(exp_arg)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.