Implementieren einer trainierbaren verallgemeinerten Bump-Funktionsschicht in Keras / Tensorflow

8

Ich versuche, die folgende Variante der Bump-Funktion zu codieren , die komponentenweise angewendet wird:

verallgemeinerte Stoßfunktionsgleichung,

wo σ trainierbar ist; aber es funktioniert nicht (Fehler unten gemeldet).


Mein Versuch:

Folgendes habe ich bisher codiert (wenn es hilft). Angenommen, ich habe zwei Funktionen (zum Beispiel):

  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

Fehlermeldung:

    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 

Darüber hinaus scheint es nicht komponentenweise angewendet zu werden (abgesehen von dem nicht trainierbaren Problem). Was könnte das Problem sein?

FlabbyTheKatsu
quelle
Was ist die Dimension der input? ist es ein Skalar?
Vlad
1
Hallo @ProbablyAHuman, können Sie einen minimal reproduzierbaren Code für Ihr Szenario bereitstellen und angeben, wie genau er nicht funktioniert?
TF_Support
1
@TF_Support Ich habe Details zu meinem Ziel sowie den Fehlerbericht hinzugefügt ...
FlabbyTheKatsu
Ist Sigma trainierbar?
Daniel Möller
Könnten Sie das Diagramm teilen, was Sie wollen und was in diesem Diagramm variieren kann?
Daniel Möller

Antworten:

2

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.condSie müssen verwenden tf.where.

  • Das andere Missverständnis besteht darin, zu behaupten, dass da tf.lessals 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 < 0wird 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.condmit , tf.whereum 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.greaterund auch tf.logical_and, um herauszufinden, ob die Eingabewerte Größen von weniger als sigma(alternativ können wir dies mit nur tf.math.absund tf.math.lessohne 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 BumpLayermit 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 nannach dem Training den Wert von Sigma oder infden Verlustwert erhalten! Also, was ist das Problem? Warum könnten dies nanoder infWerte 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=sigmaund x=-sigmaLinien (zwei vertikale gestrichelte blaue Linien), wenn Sigma von Null beginnt und auf 5 erhöht wird:

Verallgemeinerte Stoßfunktion, wenn Sigma bei Null beginnt und auf fünf 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 infVerlustwerts.

Darüber hinaus gibt es ein problematisches Verhalten, tf.wheredas die Ausgabe von nanWerten für die Sigma-Variable in der Schicht verursacht: überraschenderweise, wenn der erzeugte Wert im inaktiven Zweig von tf.whereextrem groß ist oder infwas mit der Bump-Funktion zu extrem großen oder infGradientenwerten führt , dann von der Gradient tf.wherewäre nan, trotz der Tatsache , dass die infin 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.whereum zu verhindern, dass die Funktion auf diese Regionen angewendet wird. Mit anderen Worten, anstatt self.bump_functionauf 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 nanWerte 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_dataFunktion). Andernfalls besteht immer noch das Problem des infVerlustwerts, 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:

  1. 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 > 1der Eingabewert zwischen sqrt(sigma)und sigma(oder zwischen -sigmaund) liegt -sqrt(sigma).)

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

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

  4. 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?)

  5. Sie müssen wir auch die erzeugten Trainingsdaten für jede überprüfen nan, infoder extrem große Werte von yund filtern sie aus? (Hinweis: Ja, wenn sigma > 1und Wertebereich, dh min_xund max_x, außerhalb von (-1, 1); ansonsten nein, das ist nicht notwendig! Warum ist das so? Als Übung übrig!)

heute
quelle
1
Gut gemacht. @ProbablyAHuman dies sollte die akzeptierte Antwort sein.
Rvinas
@heute. Das ist großartig, denke ich, möglicherweise die detaillierteste / präziseste / schärfste Antwort, die ich je auf einem Stapel gesehen habe. Vielen Dank!
FlabbyTheKatsu
4

Leider ist keine Operation zur Überprüfung, ob sie xinnerhalb (-σ, σ)liegt, differenzierbar, und daher kann σ nicht über eine Gradientenabstiegsmethode gelernt werden. Insbesondere ist es nicht möglich, die Gradienten in Bezug auf zu berechnen, self.threshold_levelda tf.math.lessdies in Bezug auf die Bedingung nicht differenzierbar ist.

In Bezug auf die elementweise Bedingung können Sie stattdessen tf.where verwenden , um Elemente aus f_True(input)oder f_False(input)gemäß den komponentenweisen booleschen Werten der Bedingung auszuwählen . Zum Beispiel:

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

HINWEIS: Ich habe anhand des angegebenen Codes geantwortet, in dem self.threshold_levelweder verwendet f_Truenoch verwendet wird f_False. Wenn self.threshold_levelin diesen Funktionen wie in der bereitgestellten Formel verwendet wird, ist die Funktion natürlich in Bezug auf differenzierbar self.threshold_level.

Aktualisiert 19/04/2020: Vielen Dank an @today für die Klarstellung .

Rvinas
quelle
Es muss einen Codierungstrick geben, um es trainierbar zu machen ...
FlabbyTheKatsu
Ich fürchte, kein magischer Implementierungstrick wird es trainierbar machen, wenn die Mathematik nicht funktioniert ...
Rvinas
"Die Fehlermeldung zeigt genau dies an - es ist nicht möglich, die Gradienten in Bezug auf self.threshold_level zu berechnen, da tf.math.less in Bezug auf seine Eingaben nicht differenzierbar ist." -> Die Warnmeldung hat nichts mit der Verwendung tf.math.lessin der Bedingung und der Tatsache zu tun , dass sie nicht differenzierbar ist. Die Bedingung muss nicht differenzierbar sein, damit dies funktioniert. Der Fehler liegt in der Tatsache, dass das trainierbare Gewicht überhaupt nicht verwendet wird, um die Ausgabe der Schicht zu erzeugen (dh es gibt keine Spur davon in der Ausgabe). Weitere Informationen hierzu finden Sie im ersten Teil meiner Antwort.
heute
Einverstanden, das ist nicht das, was die Warnmeldung sagt, und ich werde meinen Wortlaut korrigieren. Der Punkt bleibt jedoch derselbe - Sie können keine Operation ausführen, um zu überprüfen, ob eine Variable innerhalb eines bestimmten Bereichs liegt, und erwarten, dass sie in Bezug auf die Grenzvariable differenzierbar ist. Das heißt, wenn diese Variable für die Berechnung der Ausgabe verwendet wird (was ich in der Formel nicht einmal bemerkt habe, muss ich zugeben), wird sie natürlich Gradienten haben.
Rvinas
3

Ich schlage vor, Sie versuchen eine Normalverteilung anstelle einer Beule. In meinen Tests hier verhält sich diese Bump-Funktion nicht gut (ich kann keinen Fehler finden, aber nicht verwerfen, aber meine Grafik zeigt zwei sehr scharfe Bumps, was für Netzwerke nicht gut ist).

Bei einer Normalverteilung erhalten Sie eine regelmäßige und differenzierbare Erhebung, deren Höhe, Breite und Mitte Sie steuern können.

Sie können diese Funktion also ausprobieren:

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

Probieren Sie es in einem Diagramm aus und sehen Sie, wie es sich verhält.

Dafür:

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)
Daniel Möller
quelle