Ich bin ein bisschen überrascht, dass niemand den Hauptgrund (und den einzigen) für die gegebene Warnung erwähnt hat! Wie es scheint, soll dieser Code die verallgemeinerte Variante der Bump-Funktion implementieren; Schauen Sie sich jedoch die erneut implementierten Funktionen an:
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)
Der Fehler ist offensichtlich: In diesen Funktionen wird das trainierbare Gewicht der Schicht nicht verwendet! Es ist also keine Überraschung, dass Sie die Meldung erhalten, dass dafür kein Farbverlauf vorhanden ist: Sie verwenden ihn überhaupt nicht, also keinen Farbverlauf, um ihn zu aktualisieren! Dies ist vielmehr genau die ursprüngliche Bump-Funktion (dh ohne trainierbares Gewicht).
Aber man könnte sagen: "Zumindest habe ich das trainierbare Gewicht im Zustand von verwendet tf.cond
, also muss es einige Steigungen geben?!"; es ist jedoch nicht so und lassen Sie mich die Verwirrung beseitigen:
Wie Sie ebenfalls bemerkt haben, sind wir zunächst an einer elementweisen Konditionierung interessiert. Also anstatt tf.cond
Sie müssen verwenden tf.where
.
Das andere Missverständnis besteht darin, zu behaupten, dass da tf.less
als Bedingung verwendet wird und da es nicht differenzierbar ist, dh keinen Gradienten in Bezug auf seine Eingaben hat (was wahr ist: Es gibt keinen definierten Gradienten für eine Funktion mit boolescher Ausgabe in Bezug auf ihre Real- Wert Eingaben!), dann ergibt sich die gegebene Warnung!
- Das ist einfach falsch! Die Ableitung hier würde von der Ausgabe der Schicht bezüglich des trainierbaren Gewichts genommen, und die Auswahlbedingung ist NICHT in der Ausgabe vorhanden. Es ist vielmehr nur ein boolescher Tensor, der den auszuwählenden Ausgabezweig bestimmt. Das ist es! Die Ableitung der Bedingung wird nicht genommen und wird niemals benötigt. Das ist also nicht der Grund für die gegebene Warnung; Der Grund ist nur und nur das, was ich oben erwähnt habe: kein Beitrag des trainierbaren Gewichts zur Ausgabe der Schicht. (Hinweis: Wenn der Punkt über die Bedingung für Sie etwas überraschend ist, denken Sie an ein einfaches Beispiel: die ReLU-Funktion, die definiert ist als
relu(x) = 0 if x < 0 else x
. Wenn die Ableitung der Bedingung, dhx < 0
wird berücksichtigt / benötigt, was nicht existiert, dann könnten wir ReLU in unseren Modellen nicht verwenden und sie überhaupt mit gradientenbasierten Optimierungsmethoden trainieren!)
(Hinweis: Ab hier würde ich den Schwellenwert wie in der Gleichung als Sigma bezeichnen und bezeichnen ).
Gut! Wir haben den Grund für den Fehler in der Implementierung gefunden. Könnten wir das beheben? Na sicher! Hier ist die aktualisierte Arbeitsimplementierung:
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
Einige Punkte zu dieser Implementierung:
Wir haben ersetzt tf.cond
mit , tf.where
um elementweise Anlage zu tun.
Wie Sie sehen können, verwenden wir im Gegensatz zu Ihrer Implementierung, bei der nur eine Seite der Ungleichung überprüft wurde tf.math.less
, tf.math.greater
und auch tf.logical_and
, um herauszufinden, ob die Eingabewerte Größen von weniger als sigma
(alternativ können wir dies mit nur tf.math.abs
und tf.math.less
ohne Unterschied tun) !). Und lassen Sie es uns wiederholen: Die Verwendung von Booleschen Ausgabefunktionen auf diese Weise verursacht keine Probleme und hat nichts mit Ableitungen / Verläufen zu tun.
Wir verwenden auch eine Nicht-Negativitätsbeschränkung für den von der Schicht gelernten Sigma-Wert. Warum? Weil Sigma-Werte unter Null keinen Sinn ergeben (dh der Bereich (-sigma, sigma)
ist schlecht definiert, wenn Sigma negativ ist).
Und unter Berücksichtigung des vorherigen Punktes achten wir darauf, den Sigma-Wert richtig zu initialisieren (dh auf einen kleinen nicht negativen Wert).
Und bitte machen Sie auch keine Dinge wie 0.0 * inputs
! Es ist redundant (und ein bisschen komisch) und entspricht 0.0
; und beide haben einen Gradienten von 0.0
(wrt inputs
). Das Multiplizieren von Null mit einem Tensor fügt nichts hinzu oder löst kein bestehendes Problem, zumindest in diesem Fall nicht!
Testen wir es jetzt, um zu sehen, wie es funktioniert. Wir schreiben einige Hilfsfunktionen, um Trainingsdaten basierend auf einem festen Sigma-Wert zu generieren und um ein Modell zu erstellen, das eine einzelne BumpLayer
mit der Eingabeform von enthält (1,)
. Mal sehen, ob es den Sigma-Wert lernen kann, der zum Generieren von Trainingsdaten verwendet wird:
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
Ja, es könnte den Wert von Sigma lernen, der zum Generieren von Daten verwendet wird! Aber ist garantiert, dass es tatsächlich für alle unterschiedlichen Werte von Trainingsdaten und Initialisierung von Sigma funktioniert? Die Antwort ist nein! Tatsächlich ist es möglich, dass Sie den obigen Code ausführen und nan
nach dem Training den Wert von Sigma oder inf
den Verlustwert erhalten! Also, was ist das Problem? Warum könnten dies nan
oder inf
Werte erzeugt werden? Lassen Sie es uns unten diskutieren ...
Umgang mit numerischer Stabilität
Eines der wichtigsten Dinge, die beim Erstellen eines maschinellen Lernmodells und beim Verwenden gradientenbasierter Optimierungsmethoden zum Trainieren berücksichtigt werden müssen, ist die numerische Stabilität von Operationen und Berechnungen in einem Modell. Wenn durch eine Operation oder ihren Gradienten extrem große oder kleine Werte erzeugt werden, würde dies mit ziemlicher Sicherheit den Trainingsprozess stören (dies ist beispielsweise einer der Gründe für die Normalisierung der Bildpixelwerte in CNNs, um dieses Problem zu vermeiden).
Schauen wir uns also diese verallgemeinerte Bump-Funktion an (und verwerfen wir vorerst die Schwellenwertbildung). Es ist offensichtlich, dass diese Funktion Singularitäten (dh Punkte, an denen entweder die Funktion oder ihr Gradient nicht definiert ist) bei x^2 = sigma
(dh wann x = sqrt(sigma)
oder x=-sqrt(sigma)
) aufweist. Das animierte Diagramm unten zeigt die Höckerfunktion (die durchgezogene rote Linie), ihre Ableitung von Sigma (die gepunktete grüne Linie) x=sigma
und x=-sigma
Linien (zwei vertikale gestrichelte blaue Linien), wenn Sigma von Null beginnt und auf 5 erhöht wird:
Wie Sie sehen können, verhält sich die Funktion in der Region der Singularitäten nicht für alle Sigma-Werte in dem Sinne gut, dass sowohl die Funktion als auch ihre Ableitung in diesen Regionen extrem große Werte annehmen. Bei einem Eingabewert in diesen Regionen für einen bestimmten Sigma-Wert würden also explodierende Ausgabe- und Gradientenwerte erzeugt, daher das Problem des inf
Verlustwerts.
Darüber hinaus gibt es ein problematisches Verhalten, tf.where
das die Ausgabe von nan
Werten für die Sigma-Variable in der Schicht verursacht: überraschenderweise, wenn der erzeugte Wert im inaktiven Zweig von tf.where
extrem groß ist oder inf
was mit der Bump-Funktion zu extrem großen oder inf
Gradientenwerten führt , dann von der Gradient tf.where
wäre nan
, trotz der Tatsache , dass die inf
in ist inaktiv Zweig und ist nicht einmal ausgewählt (siehe diese Github Ausgabe , die diese diskutiert genau) !!
Gibt es also eine Problemumgehung für dieses Verhalten von tf.where
? Ja, tatsächlich gibt es einen Trick, um dieses Problem irgendwie zu lösen, der in dieser Antwort erläutert wird : Grundsätzlich können wir einen zusätzlichen verwenden, tf.where
um zu verhindern, dass die Funktion auf diese Regionen angewendet wird. Mit anderen Worten, anstatt self.bump_function
auf einen Eingabewert anzuwenden , filtern wir diejenigen Werte, die NICHT im Bereich liegen (-self.sigma, self.sigma)
(dh im tatsächlichen Bereich, in dem die Funktion angewendet werden soll), und speisen die Funktion stattdessen mit Null (was immer sichere Werte erzeugt, d. H. ist gleich exp(-1)
):
output = tf.where(
condition,
self.bump_function(tf.where(condition, inputs, 0.0)),
0.0
)
Das Anwenden dieses Fixes würde das Problem der nan
Werte für Sigma vollständig lösen . Lassen Sie es uns anhand von Trainingsdatenwerten bewerten, die mit verschiedenen Sigma-Werten generiert wurden, und sehen, wie es funktionieren würde:
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
Es könnte alle Sigma-Werte richtig lernen! Das ist schön. Diese Problemumgehung hat funktioniert! Es gibt jedoch eine Einschränkung: Dies funktioniert garantiert ordnungsgemäß und lernt jeden Sigma-Wert, wenn die Eingabewerte für diese Ebene größer als -1 und kleiner als 1 sind (dh dies ist der Standardfall unserer generate_data
Funktion). Andernfalls besteht immer noch das Problem des inf
Verlustwerts, der auftreten kann, wenn die Eingabewerte eine Größe von mehr als 1 haben (siehe Punkt 1 und 2 unten).
Hier sind einige Denkanstöße für Neugierige und Interessierte:
Es wurde nur erwähnt, dass wenn die Eingabewerte für diese Ebene größer als 1 oder kleiner als -1 sind, dies Probleme verursachen kann. Können Sie argumentieren, warum dies der Fall ist? (Hinweis: Verwenden Sie das obige animierte Diagramm und berücksichtigen Sie Fälle, in denen sigma > 1
der Eingabewert zwischen sqrt(sigma)
und sigma
(oder zwischen -sigma
und) liegt -sqrt(sigma)
.)
Können Sie eine Lösung für das Problem in Punkt 1 bereitstellen, dh, dass die Ebene für alle Eingabewerte funktionieren kann? (Hinweis: Überlegen Sie sich wie bei der Problemumgehung tf.where
, wie Sie die unsicheren Werte, auf die die Bump-Funktion angewendet werden könnte, weiter herausfiltern und eine explodierende Ausgabe / einen explodierenden Gradienten erzeugen können.)
Wenn Sie jedoch nicht daran interessiert sind, dieses Problem zu beheben, und diese Ebene in einem Modell wie bisher verwenden möchten, wie können Sie dann sicherstellen, dass die Eingabewerte für diese Ebene immer zwischen -1 und 1 liegen? (Hinweis: Als eine Lösung gibt es eine häufig verwendete Aktivierungsfunktion, die Werte genau in diesem Bereich erzeugt und möglicherweise als Aktivierungsfunktion der Schicht vor dieser Schicht verwendet werden kann.)
Wenn Sie sich das letzte Code-Snippet ansehen, werden Sie sehen, dass wir es verwendet haben epochs=3 if s < 1 else (5 if s < 5 else 10)
. Warum ist das so? Warum brauchen große Sigma-Werte mehr Epochen, um gelernt zu werden? (Hinweis: Verwenden Sie erneut das animierte Diagramm und betrachten Sie die Ableitung der Funktion für Eingabewerte zwischen -1 und 1, wenn der Sigma-Wert zunimmt. Wie groß sind sie?)
Sie müssen wir auch die erzeugten Trainingsdaten für jede überprüfen nan
, inf
oder extrem große Werte von y
und filtern sie aus? (Hinweis: Ja, wenn sigma > 1
und Wertebereich, dh min_x
und max_x
, außerhalb von (-1, 1)
; ansonsten nein, das ist nicht notwendig! Warum ist das so? Als Übung übrig!)
input
? ist es ein Skalar?