¡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.cond
que necesita utilizar tf.where
.
El otro concepto erróneo es afirmar que ya que tf.less
se 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.cond
con el tf.where
fin 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.greater
y también tf.logical_and
para averiguar si los valores de entrada tienen magnitudes menores que sigma
(alternativamente, podríamos hacer esto usando solo tf.math.abs
y 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 BumpLayer
con 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 nan
el valor de sigma después del entrenamiento, ¡o inf
el valor de pérdida! ¿Entonces, cuál es el problema? ¿Por qué esto nan
o inf
valores 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=sigma
y las x=-sigma
líneas (dos líneas azules discontinuas verticales), cuando sigma comienza desde cero y se incrementa a 5:
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 inf
valor de pérdida.
Aún más, hay un comportamiento problemático tf.where
que causa la emisión de nan
valores para la variable sigma en la capa: sorprendentemente, si el valor producido en la rama inactiva de tf.where
es extremadamente grande o inf
, lo que con la función de relieve da como resultado inf
valores extremadamente grandes o de gradiente , entonces el gradiente de tf.where
sería nan
, a pesar del hecho de que inf
está 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.where
para evitar que la función se aplique en estas regiones. En otras palabras, en lugar de aplicar self.bump_function
cualquier 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 nan
valores 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_data
función); de lo contrario, todavía existe el problema del inf
valor 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:
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 > 1
el valor de entrada esté entre sqrt(sigma)
y sigma
(o entre -sigma
y -sqrt(sigma)
).
¿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).
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).
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?)
Es lo que también necesitamos comprobar los datos de entrenamiento generados por cualquier nan
, inf
o los valores extremadamente grandes y
y filtrarlos a cabo? (Sugerencia: sí, si sigma > 1
y el rango de valores, es decir, min_x
y max_x
, quedan fuera de (-1, 1)
; de lo contrario, ¡no, eso no es necesario! ¿Por qué es eso? ¡Dejado como ejercicio!)
input
? ¿Es un escalar?