Die vielen Anwendungen des Gradientenabstiegs in TensorFlow

Veröffentlicht: 2022-03-11

TensorFlow von Google ist eines der führenden Tools zum Trainieren und Bereitstellen von Deep-Learning-Modellen. Es ist in der Lage, äußerst komplexe neuronale Netzwerkarchitekturen mit Hunderten von Millionen Parametern zu optimieren, und es wird mit einer breiten Palette von Tools für Hardwarebeschleunigung, verteiltes Training und Produktionsworkflows geliefert. Diese leistungsstarken Funktionen können es außerhalb des Bereichs des Deep Learning einschüchternd und unnötig erscheinen lassen.

Aber TensorFlow kann für einfachere Probleme, die nicht direkt mit dem Training von Deep-Learning-Modellen zusammenhängen, sowohl zugänglich als auch verwendbar sein. Im Kern ist TensorFlow nur eine optimierte Bibliothek für Tensoroperationen (Vektoren, Matrizen usw.) und die Kalküloperationen, die verwendet werden, um Gradientenabstieg bei beliebigen Folgen von Berechnungen durchzuführen. Erfahrene Data Scientists erkennen „Gradientenabstieg“ als grundlegendes Werkzeug für Computermathematik, aber es erfordert normalerweise die Implementierung von anwendungsspezifischem Code und Gleichungen. Wie wir sehen werden, kommt hier die moderne Architektur der „automatischen Differenzierung“ von TensorFlow ins Spiel.

TensorFlow-Anwendungsfälle

  • Beispiel 1: Lineare Regression mit Gradientenabstieg in TensorFlow 2.0
    • Was ist Gradientenabstieg?
  • Beispiel 2: Maximal gespreizte Einheitsvektoren
  • Beispiel 3: Generieren von gegnerischen KI-Eingaben
  • Abschließende Gedanken: Optimierung des Gradientenabstiegs
  • Gradientenabstieg in TensorFlow: Von der Suche nach Minima bis zum Angriff auf KI-Systeme

Beispiel 1: Lineare Regression mit Gradientenabstieg in TensorFlow 2.0

Beispiel 1 Notizbuch

Bevor Sie zum TensorFlow-Code kommen, ist es wichtig, sich mit Gradientenabstieg und linearer Regression vertraut zu machen.

Was ist Gradientenabstieg?

Einfach ausgedrückt ist es eine numerische Technik, um die Eingaben für ein Gleichungssystem zu finden, die seine Ausgabe minimieren. Im Kontext des maschinellen Lernens ist dieses Gleichungssystem unser Modell , die Eingaben sind die unbekannten Parameter des Modells, und die Ausgabe ist eine zu minimierende Verlustfunktion , die darstellt, wie viel Fehler zwischen dem Modell und unseren Daten besteht. Für einige Probleme (wie die lineare Regression) gibt es Gleichungen, um die Parameter direkt zu berechnen, die unseren Fehler minimieren, aber für die meisten praktischen Anwendungen benötigen wir numerische Techniken wie den Gradientenabstieg, um zu einer zufriedenstellenden Lösung zu gelangen.

Der wichtigste Punkt dieses Artikels ist, dass der Gradientenabstieg normalerweise das Auslegen unserer Gleichungen und die Verwendung von Kalkül erfordert, um die Beziehung zwischen unserer Verlustfunktion und unseren Parametern abzuleiten. Mit TensorFlow (und jedem modernen Auto-Differenzierungs-Tool) wird die Berechnung für uns erledigt, sodass wir uns auf das Entwerfen der Lösung konzentrieren können und keine Zeit für die Implementierung aufwenden müssen.

So sieht es bei einem einfachen linearen Regressionsproblem aus. Wir haben eine Stichprobe der Körpergröße (h) und des Gewichts (w) von 150 erwachsenen Männern und beginnen mit einer unvollständigen Schätzung der Steigung und Standardabweichung dieser Linie. Nach etwa 15 Iterationen des Gradientenabstiegs kommen wir zu einer nahezu optimalen Lösung.

Zwei synchronisierte Animationen. Die linke Seite zeigt ein Größen-Gewichts-Streudiagramm mit einer angepassten Linie, die weit von den Daten entfernt beginnt, sich dann schnell darauf zu bewegt und langsamer wird, bevor sie die endgültige Anpassung findet. Die rechte Größe zeigt ein Diagramm des Verlusts gegenüber der Iteration, wobei jeder Frame dem Diagramm eine neue Iteration hinzufügt. Der Verlust beginnt über dem oberen Rand des Diagramms bei 2.000, nähert sich jedoch innerhalb weniger Iterationen in einer scheinbar logarithmischen Kurve schnell der minimalen Verlustlinie.

Sehen wir uns an, wie wir die obige Lösung mit TensorFlow 2.0 erstellt haben.

Bei der linearen Regression sagen wir, dass Gewichte durch eine lineare Größengleichung vorhergesagt werden können.

w-Index-i,pred ist gleich Alpha-Punkt-Produkt h-Index-i plus Beta.

Wir wollen die Parameter α und β (Steigung und Achsenabschnitt) finden, die den durchschnittlichen quadratischen Fehler (Verlust) zwischen den Vorhersagen und den wahren Werten minimieren. Unsere Verlustfunktion (in diesem Fall der „mittlere quadratische Fehler“ oder MSE) sieht also so aus:

MSE ist gleich eins über N mal die Summe von i gleich eins zu N des Quadrats der Differenz zwischen w-Index-i,true und w-Index-i,pred.

Wir können sehen, wie der mittlere quadratische Fehler für ein paar unvollkommene Linien aussieht, und dann mit der exakten Lösung (α = 6,04, β = -230,5).

Drei Kopien desselben Größen-Gewichts-Streudiagramms, jedes mit einer anderen Anpassungslinie. Die erste hat w = 4,00 * h + -120,0 und einen Verlust von 1057,0; Die Linie liegt unter den Daten und ist weniger steil als diese. Die zweite hat w = 2,00 * h + 70,0 und einen Verlust von 720,8; Die Linie befindet sich in der Nähe des oberen Teils der Datenpunkte und ist sogar noch weniger steil. Die dritte hat w = 60,4 * h + -230,5 und einen Verlust von 127,1; Die Linie verläuft so durch die Datenpunkte, dass sie gleichmäßig um sie herum gruppiert erscheinen.

Lassen Sie uns diese Idee mit TensorFlow in die Tat umsetzen. Als erstes müssen Sie die Verlustfunktion mit Tensoren und tf.* -Funktionen kodieren.

 def calc_mean_sq_error(heights, weights, slope, intercept): predicted_wgts = slope * heights + intercept errors = predicted_wgts - weights mse = tf.reduce_mean(errors**2) return mse

Das sieht ziemlich einfach aus. Alle standardmäßigen algebraischen Operatoren sind für Tensoren überladen, also müssen wir nur sicherstellen, dass die Variablen, die wir optimieren, Tensoren sind, und wir verwenden tf.* Methoden für alles andere.

Dann müssen wir dies nur noch in eine Gradientenabstiegsschleife einfügen:

 def run_gradient_descent(heights, weights, init_slope, init_icept, learning_rate): # Any values to be part of gradient calcs need to be vars/tensors tf_slope = tf.Variable(init_slope, dtype='float32') tf_icept = tf.Variable(init_icept, dtype='float32') # Hardcoding 25 iterations of gradient descent for i in range(25): # Do all calculations under a "GradientTape" which tracks all gradients with tf.GradientTape() as tape: tape.watch((tf_slope, tf_icept)) # This is the same mean-squared-error calculation as before predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors**2) # Auto-diff magic! Calcs gradients between loss calc and params dloss_dparams = tape.gradient(loss, [tf_slope, tf_icept]) # Gradients point towards +loss, so subtract to "descend" tf_slope = tf_slope - learning_rate * dloss_dparams[0] tf_icept = tf_icept - learning_rate * dloss_dparams[1]

Nehmen wir uns einen Moment Zeit, um zu schätzen, wie ordentlich das ist. Der Gradientenabstieg erfordert die Berechnung von Ableitungen der Verlustfunktion in Bezug auf alle Variablen, die wir zu optimieren versuchen. Calculus soll involviert sein, aber wir haben eigentlich nichts davon gemacht. Die Magie liegt darin, dass:

  1. TensorFlow erstellt ein Berechnungsdiagramm jeder Berechnung, die unter einem tf.GradientTape() .
  2. TensorFlow weiß, wie die Ableitungen (Gradienten) jeder Operation berechnet werden, sodass bestimmt werden kann, wie sich eine Variable im Berechnungsdiagramm auf eine andere Variable auswirkt.

Wie sieht der Prozess aus verschiedenen Ausgangspunkten aus?

Dieselben synchronisierten Graphen wie zuvor, aber zum Vergleich auch mit einem ähnlichen Paar von Graphen darunter synchronisiert. Der Verlust-Iterations-Graph des unteren Paares ist ähnlich, scheint aber schneller zu konvergieren; Die entsprechende Anpassungslinie beginnt oberhalb der Datenpunkte und nicht unterhalb und näher an ihrem endgültigen Ruheort.

Der Gradientenabstieg kommt dem optimalen MSE bemerkenswert nahe, konvergiert jedoch in beiden Beispielen tatsächlich zu einer wesentlich anderen Steigung und einem anderen Schnittpunkt als das Optimum. In einigen Fällen ist dies einfach ein Gradientenabstieg, der zu einem lokalen Minimum konvergiert, was bei Gradientenabstiegsalgorithmen eine inhärente Herausforderung darstellt. Aber die lineare Regression hat nachweislich nur ein globales Minimum. Wie sind wir also auf der falschen Piste gelandet und haben abgefangen?

In diesem Fall besteht das Problem darin, dass wir den Code zu Demonstrationszwecken stark vereinfacht haben. Wir haben unsere Daten nicht normalisiert, und der Steigungsparameter hat eine andere Charakteristik als der Schnittpunktparameter. Winzige Änderungen in der Neigung können zu massiven Änderungen im Verlust führen, während winzige Änderungen im Schnittpunkt nur sehr geringe Auswirkungen haben. Dieser enorme Skalenunterschied der trainierbaren Parameter führt dazu, dass die Steigung die Gradientenberechnungen dominiert, wobei der Schnittpunktparameter fast ignoriert wird.

Der Gradientenabstieg findet also effektiv die beste Steigung sehr nahe an der anfänglichen Schnittpunktschätzung. Und da der Fehler so nahe am Optimum liegt, sind die Gradienten um ihn herum winzig, sodass sich jede nachfolgende Iteration nur ein winziges bisschen bewegt. Die Normalisierung unserer Daten hätte dieses Phänomen dramatisch verbessert, aber nicht beseitigt.

Dies war ein relativ einfaches Beispiel, aber wir werden in den nächsten Abschnitten sehen, dass diese „Auto-Differenzierung“-Fähigkeit einige ziemlich komplexe Dinge handhaben kann.

Beispiel 2: Maximal gespreizte Einheitsvektoren

Beispiel 2 Notizbuch

Dieses nächste Beispiel basiert auf einer unterhaltsamen Deep-Learning-Übung in einem Deep-Learning-Kurs, an dem ich letztes Jahr teilgenommen habe.

Der Kern des Problems besteht darin, dass wir einen „Variational Auto-Encoder“ (VAE) haben, der aus einem Satz von 32 normalverteilten Zahlen realistische Gesichter erzeugen kann. Zur Identifizierung von Verdächtigen möchten wir die VAE verwenden, um einen vielfältigen Satz von (theoretischen) Gesichtern zu erstellen, aus denen ein Zeuge auswählen kann, und dann die Suche eingrenzen, indem wir mehr Gesichter erstellen, die den ausgewählten ähneln. Für diese Übung wurde vorgeschlagen, den anfänglichen Vektorsatz zu randomisieren, aber ich wollte einen optimalen Anfangszustand finden.

Wir können das Problem folgendermaßen formulieren: Finden Sie in einem gegebenen 32-dimensionalen Raum eine Menge von X-Einheitsvektoren, die maximal auseinander gespreizt sind. In zwei Dimensionen ist dies leicht genau zu berechnen. Aber für drei Dimensionen (oder 32 Dimensionen!) gibt es keine einfache Antwort. Wenn wir jedoch eine geeignete Verlustfunktion definieren können, die auf ihrem Minimum ist, wenn wir unseren Zielzustand erreicht haben, kann uns vielleicht der Gradientenabstieg helfen, dorthin zu gelangen.

Zwei Grafiken. Das linke Diagramm, Anfangszustand für alle Experimente, hat einen zentralen Punkt, der mit anderen Punkten verbunden ist, die fast alle einen Halbkreis um ihn herum bilden; ein Punkt steht dem Halbkreis ungefähr gegenüber. Das rechte Diagramm, Zielzustand, ist wie ein Rad mit gleichmäßig verteilten Speichen.

Wir beginnen mit einem randomisierten Satz von 20 Vektoren, wie oben gezeigt, und experimentieren mit drei verschiedenen Verlustfunktionen, jede mit zunehmender Komplexität, um die Fähigkeiten von TensorFlow zu demonstrieren.

Lassen Sie uns zuerst unsere Trainingsschleife definieren. Wir werden die gesamte TensorFlow-Logik unter die self.calc_loss() -Methode stellen, und dann können wir diese Methode einfach für jede Technik überschreiben, indem wir diese Schleife wiederverwenden.

 # Define the framework for trying different loss functions # Base class implements loop, sub classes override self.calc_loss() class VectorSpreadAlgorithm: # ... def calc_loss(self, tensor2d): raise NotImplementedError("Define this in your derived class") def one_iter(self, i, learning_rate): # self.vecs is an 20x2 tensor, representing twenty 2D vectors tfvecs = tf.convert_to_tensor(self.vecs, dtype=tf.float32) with tf.GradientTape() as tape: tape.watch(tfvecs) loss = self.calc_loss(tfvecs) # Here's the magic again. Derivative of spread with respect to # input vectors gradients = tape.gradient(loss, tfvecs) self.vecs = self.vecs - learning_rate * gradients

Die erste auszuprobierende Technik ist die einfachste. Wir definieren eine Spread-Metrik, die der Winkel der Vektoren ist, die am nächsten beieinander liegen. Wir wollen die Streuung maximieren, aber es ist üblich, daraus ein Minimierungsproblem zu machen. Also nehmen wir einfach das Negative der Spread-Metrik:

 class VectorSpread_Maximize_Min_Angle(VectorSpreadAlgorithm): def calc_loss(self, tensor2d): angle_pairs = tf.acos(tensor2d @ tf.transpose(tensor2d)) disable_diag = tf.eye(tensor2d.numpy().shape[0]) * 2 * np.pi spread_metric = tf.reduce_min(angle_pairs + disable_diag) # Convention is to return a quantity to be minimized, but we want # to maximize spread. So return negative spread return -spread_metric

Etwas Matplotlib-Magie ergibt eine Visualisierung.

Eine Animation, die vom Anfangszustand zum Zielzustand geht. Der einsame Punkt bleibt fest, und die restlichen Speichen im Halbkreis zittern abwechselnd hin und her, breiten sich langsam aus und erreichen selbst nach 1.200 Iterationen keine Äquidistanz.

Das ist klobig (im wahrsten Sinne des Wortes!), Aber es funktioniert. Es werden jeweils nur zwei der 20 Vektoren aktualisiert, wobei der Abstand zwischen ihnen vergrößert wird, bis sie nicht mehr am nächsten sind, und dann umgeschaltet wird, um den Winkel zwischen den neuen zwei am nächsten liegenden Vektoren zu vergrößern. Das Wichtigste ist, dass es funktioniert . Wir sehen, dass TensorFlow Gradienten durch die Methode tf.reduce_min() und die Methode tf.acos() konnte, um das Richtige zu tun.

Lassen Sie uns etwas Ausgefeilteres versuchen. Wir wissen, dass bei der optimalen Lösung alle Vektoren den gleichen Winkel zu ihren nächsten Nachbarn haben sollten. Also fügen wir der Verlustfunktion „Varianz der minimalen Winkel“ hinzu.

 class VectorSpread_MaxMinAngle_w_Variance(VectorSpreadAlgorithm): def spread_metric(self, tensor2d): """ Assumes all rows already normalized """ angle_pairs = tf.acos(tensor2d @ tf.transpose(tensor2d)) disable_diag = tf.eye(tensor2d.numpy().shape[0]) * 2 * np.pi all_mins = tf.reduce_min(angle_pairs + disable_diag, axis=1) # Same calculation as before: find the min-min angle min_min = tf.reduce_min(all_mins) # But now also calculate the variance of the min angles vector avg_min = tf.reduce_mean(all_mins) var_min = tf.reduce_sum(tf.square(all_mins - avg_min)) # Our spread metric now includes a term to minimize variance spread_metric = min_min - 0.4 * var_min # As before, want negative spread to keep it a minimization problem return -spread_metric 

Eine Animation, die vom Anfangszustand zum Zielzustand geht. Die einsame Speiche bleibt nicht fixiert und bewegt sich schnell zu den restlichen Speichen im Halbkreis; Anstatt zwei Lücken zu beiden Seiten der einsamen Speiche zu schließen, schließt das Zittern jetzt eine große Lücke im Laufe der Zeit. Äquidistanz ist auch hier nach 1.200 Iterationen nicht ganz erreicht.

Dieser einsame nach Norden gerichtete Vektor gesellt sich nun schnell zu seinen Kollegen, weil der Winkel zu seinem nächsten Nachbarn riesig ist und den Varianzterm, der jetzt minimiert wird, überhöht. Aber es wird letztendlich immer noch vom global minimalen Winkel angetrieben, der nur langsam ansteigt. Ideen, die ich habe, um dies zu verbessern, funktionieren im Allgemeinen in diesem 2D-Fall, aber nicht in höheren Dimensionen.

Aber sich zu sehr auf die Qualität dieses mathematischen Versuchs zu konzentrieren, verfehlt das Wesentliche. Sehen Sie sich an, wie viele Tensoroperationen an den Mittelwert- und Varianzberechnungen beteiligt sind und wie TensorFlow jede Berechnung für jede Komponente in der Eingabematrix erfolgreich verfolgt und differenziert. Und wir mussten keine manuelle Berechnung durchführen. Wir haben einfach ein paar einfache Berechnungen angestellt, und TensorFlow hat die Berechnung für uns erledigt.

Versuchen wir zum Schluss noch etwas: eine erzwungene Lösung. Stellen Sie sich vor, dass jeder Vektor ein kleiner Planet ist, der an einen zentralen Punkt gebunden ist. Jeder Planet strahlt eine Kraft aus, die ihn von den anderen Planeten abstößt. Wenn wir eine Physiksimulation dieses Modells durchführen würden, sollten wir zu unserer gewünschten Lösung kommen.

Meine Hypothese ist, dass Gradient Descent auch funktionieren sollte. Bei der optimalen Lösung sollte sich die Tangentialkraft auf jeden Planeten von jedem anderen Planeten zu einer Netto-Nullkraft aufheben (wenn sie nicht Null wäre, würden sich die Planeten bewegen). Lassen Sie uns also die Größe der Kraft auf jedem Vektor berechnen und den Gradientenabstieg verwenden, um ihn gegen Null zu drücken.

Zuerst müssen wir die Methode definieren, die die Kraft mithilfe von tf.* -Methoden berechnet:

 class VectorSpread_Force(VectorSpreadAlgorithm): def force_a_onto_b(self, vec_a, vec_b): # Calc force assuming vec_b is constrained to the unit sphere diff = vec_b - vec_a norm = tf.sqrt(tf.reduce_sum(diff**2)) unit_force_dir = diff / norm force_magnitude = 1 / norm**2 force_vec = unit_force_dir * force_magnitude # Project force onto this vec, calculate how much is radial b_dot_f = tf.tensordot(vec_b, force_vec, axes=1) b_dot_b = tf.tensordot(vec_b, vec_b, axes=1) radial_component = (b_dot_f / b_dot_b) * vec_b # Subtract radial component and return result return force_vec - radial_component

Dann definieren wir unsere Verlustfunktion unter Verwendung der Kraftfunktion oben. Wir akkumulieren die Nettokraft auf jedem Vektor und berechnen ihre Größe. Bei unserer optimalen Lösung sollten sich alle Kräfte aufheben und wir sollten keine Kraft haben.

 def calc_loss(self, tensor2d): n_vec = tensor2d.numpy().shape[0] all_force_list = [] for this_idx in range(n_vec): # Accumulate force of all other vecs onto this one this_force_list = [] for other_idx in range(n_vec): if this_idx == other_idx: continue this_vec = tensor2d[this_idx, :] other_vec = tensor2d[other_idx, :] tangent_force_vec = self.force_a_onto_b(other_vec, this_vec) this_force_list.append(tangent_force_vec) # Use list of all N-dimensional force vecs. Stack and sum. sum_tangent_forces = tf.reduce_sum(tf.stack(this_force_list)) this_force_mag = tf.sqrt(tf.reduce_sum(sum_tangent_forces**2)) # Accumulate all magnitudes, should all be zero at optimal solution all_force_list.append(this_force_mag) # We want to minimize total force sum, so simply stack, sum, return return tf.reduce_sum(tf.stack(all_force_list)) 

Eine Animation, die vom Anfangszustand zum Zielzustand geht. Die ersten paar Frames sehen eine schnelle Bewegung in allen Speichen, und nach nur etwa 200 Iterationen ist das Gesamtbild bereits ziemlich nah am Ziel. Insgesamt werden nur 700 Iterationen angezeigt; Nach dem 300. ändern sich die Winkel nur geringfügig mit jedem Frame.

Die Lösung funktioniert nicht nur wunderbar (abgesehen von etwas Chaos in den ersten Frames), sondern der wahre Verdienst geht an TensorFlow. Diese Lösung umfasste mehrere for -Schleifen, eine if -Anweisung und ein riesiges Netz von Berechnungen, und TensorFlow hat für uns erfolgreich Gradienten durch all das verfolgt.

Beispiel 3: Generieren von gegnerischen KI-Eingaben

Beispiel 3 Notizbuch

An dieser Stelle denken die Leser vielleicht: „Hey! In diesem Beitrag sollte es nicht um Deep Learning gehen!“ Technisch gesehen bezieht sich die Einführung jedoch darauf, über das „ Training von Deep-Learning-Modellen“ hinauszugehen. In diesem Fall trainieren wir nicht , sondern nutzen stattdessen einige mathematische Eigenschaften eines vortrainierten tiefen neuronalen Netzwerks, um es dazu zu bringen, uns die falschen Ergebnisse zu liefern. Dies erwies sich als viel einfacher und effektiver als gedacht. Und alles, was es brauchte, war ein weiterer kurzer TensorFlow 2.0-Code.

Wir beginnen damit, einen anzugreifenden Bildklassifizierer zu finden. Wir werden eine der Top-Lösungen für den Dogs vs. Cats Kaggle-Wettbewerb verwenden; insbesondere die von Kaggler vorgestellte Lösung „uysimty“. Alle Ehre gebührt ihnen für die Bereitstellung eines effektiven Katze-gegen-Hund-Modells und die Bereitstellung einer großartigen Dokumentation. Dies ist ein leistungsstarkes Modell, das aus 13 Millionen Parametern in 18 neuronalen Netzwerkschichten besteht. (Leser können gerne mehr darüber im entsprechenden Notizbuch lesen.)

Bitte beachten Sie, dass das Ziel hier nicht darin besteht, einen Mangel in diesem speziellen Netzwerk hervorzuheben, sondern zu zeigen, wie anfällig jedes standardmäßige neuronale Netzwerk mit einer großen Anzahl von Eingaben ist.

Verwandte: Sound Logic und monotone KI-Modelle

Mit ein wenig Bastelei konnte ich herausfinden, wie ich das Modell laden und die damit zu klassifizierenden Bilder vorverarbeiten kann.

Fünf Beispielbilder, jedes von einem Hund oder einer Katze, mit einer entsprechenden Klassifizierung und Konfidenzstufe. Die angezeigten Konfidenzniveaus reichen von 95 Prozent bis 100 Prozent.

Das sieht nach einem wirklich soliden Klassifikator aus! Alle Probenklassifikationen sind korrekt und haben eine Konfidenz von über 95 %. Greifen wir es an!

Wir möchten ein Bild erzeugen, das offensichtlich eine Katze ist, aber den Klassifizierer entscheiden lassen, dass es sich um einen Hund mit hohem Vertrauen handelt. Wie können wir das machen?

Beginnen wir mit einem Katzenbild, das korrekt klassifiziert wird, und finden dann heraus, wie sich winzige Änderungen in jedem Farbkanal (Werte 0-255) eines bestimmten Eingabepixels auf die endgültige Ausgabe des Klassifikators auswirken. Das Ändern eines Pixels wird wahrscheinlich nicht viel bewirken, aber vielleicht erreichen die kumulativen Anpassungen aller 128 x 128 x 3 = 49.152 Pixelwerte unser Ziel.

Woher wissen wir, in welche Richtung wir jedes Pixel schieben müssen? Während des normalen neuronalen Netzwerktrainings versuchen wir, den Verlust zwischen dem Ziellabel und dem vorhergesagten Label zu minimieren, indem wir den Gradientenabstieg in TensorFlow verwenden, um alle 13 Millionen freien Parameter gleichzeitig zu aktualisieren. In diesem Fall lassen wir stattdessen die 13 Millionen Parameter unverändert und passen die Pixelwerte der Eingabe selbst an.

Was ist unsere Verlustfunktion? Nun, es ist, wie sehr das Bild wie eine Katze aussieht! Wenn wir die Ableitung des Katzenwerts in Bezug auf jedes Eingabepixel berechnen, wissen wir, auf welche Weise jedes Pixel verschoben werden muss, um die Katzenklassifikationswahrscheinlichkeit zu minimieren.

 def adversarial_modify(victim_img, to_dog=False, to_cat=False): # We only need four gradient descent steps for i in range(4): tf_victim_img = tf.convert_to_tensor(victim_img, dtype='float32') with tf.GradientTape() as tape: tape.watch(tf_victim_img) # Run the image through the model model_output = model(tf_victim_img) # Minimize cat confidence and maximize dog confidence loss = (model_output[0] - model_output[1]) dloss_dimg = tape.gradient(loss, tf_victim_img) # Ignore gradient magnitudes, only care about sign, +1/255 or -1/255 pixels_w_pos_grad = tf.cast(dloss_dimg > 0.0, 'float32') / 255. pixels_w_neg_grad = tf.cast(dloss_dimg < 0.0, 'float32') / 255. victim_img = victim_img - pixels_w_pos_grad + pixels_w_neg_grad

Matplotlib Magic hilft wieder, die Ergebnisse zu visualisieren.

Ein Original-Beispiel-Katzenbild zusammen mit 4 Iterationen mit den Klassifizierungen „Katze 99,0 %“, „Katze 67,3 %“, „Hund 71,7 %“, „Hund 94,3 %“ und „Hund 99,4 %“.

Beeindruckend! Für das menschliche Auge ist jedes dieser Bilder identisch. Doch nach vier Iterationen haben wir den Klassifizierer mit 99,4-prozentiger Sicherheit davon überzeugt, dass es sich um einen Hund handelt!

Stellen wir sicher, dass dies kein Zufall ist und auch in die andere Richtung funktioniert.

Ein Original-Beispiel-Hundebild zusammen mit 4 Iterationen mit den Klassifizierungen „Hund 98,4 %“, „Hund 83,9 %“, „Hund 54,6 %“, „Katze 90,4 %“ und „Katze 99,8 %“. Wie zuvor sind die Unterschiede mit bloßem Auge nicht sichtbar.

Erfolg! Der Klassifikator hat dies ursprünglich als Hund mit 98,4-prozentiger Sicherheit richtig vorhergesagt und glaubt nun, dass es sich mit 99,8-prozentiger Sicherheit um eine Katze handelt.

Schauen wir uns zum Schluss einen Beispiel-Image-Patch an und sehen, wie er sich verändert hat.

Drei Raster aus Pixelzeilen und -spalten, die numerische Werte für den Rotkanal jedes Pixels anzeigen. Der linke Bildfleck zeigt hauptsächlich bläuliche Quadrate, die Werte von 218 oder darunter hervorheben, wobei einige rote Quadrate (219 und darüber) in der unteren rechten Ecke gehäuft sind. Die mittlere, „geopferte“ Bildseite zeigt ein sehr ähnlich gefärbtes und nummeriertes Layout. Der rechte Bildausschnitt zeigt den numerischen Unterschied zwischen den beiden anderen, wobei die Unterschiede nur von -4 bis +4 reichen und mehrere Nullen enthalten.

Wie erwartet ist der endgültige Patch dem Original sehr ähnlich, wobei jedes Pixel nur den Intensitätswert des roten Kanals um -4 bis +4 verschiebt. Diese Verschiebung reicht für einen Menschen nicht aus, um den Unterschied zu erkennen, sondern verändert die Ausgabe des Klassifikators vollständig.

Abschließende Gedanken: Optimierung des Gradientenabstiegs

In diesem Artikel haben wir uns aus Gründen der Einfachheit und Transparenz mit der manuellen Anwendung von Farbverläufen auf unsere trainierbaren Parameter beschäftigt. In der realen Welt sollten Data Scientists jedoch direkt mit der Verwendung von Optimierern beginnen , da sie in der Regel viel effektiver sind, ohne dass Code-Bloat hinzugefügt wird.

Es gibt viele beliebte Optimierer, darunter RMSprop, Adagrad und Adadelta, aber der gebräuchlichste ist wahrscheinlich Adam . Manchmal werden sie als „adaptive Lernratenmethoden“ bezeichnet, weil sie dynamisch eine andere Lernrate für jeden Parameter aufrechterhalten. Viele von ihnen verwenden Impulsterme und näherungsweise Ableitungen höherer Ordnung mit dem Ziel, lokale Minima zu umgehen und eine schnellere Konvergenz zu erreichen.

In einer von Sebastian Ruder geliehenen Animation sehen wir den Weg verschiedener Optimierer, die eine Verlustfläche hinabsteigen. Die manuellen Techniken, die wir demonstriert haben, sind am ehesten mit „SGD“ vergleichbar. Der leistungsstärkste Optimierer ist nicht für jede Verlustfläche derselbe; Fortgeschrittenere Optimierer sind jedoch in der Regel besser als die einfacheren.

Eine animierte Höhenlinienkarte, die den Weg zeigt, den sechs verschiedene Methoden nehmen, um auf einen Zielpunkt zu konvergieren. SGD ist bei weitem am langsamsten und nimmt von seinem Ausgangspunkt aus eine stetige Kurve. Das Momentum entfernt sich zunächst vom Ziel, kreuzt dann zweimal seinen eigenen Weg, bevor es nicht ganz direkt darauf zusteuert, und scheint darüber hinauszuschießen und dann zurückzugehen. NAG ist ähnlich, weicht aber nicht ganz so weit vom Ziel ab und überkreuzt sich nur einmal, erreicht das Ziel im Allgemeinen schneller und schießt weniger darüber hinaus. Adagrad beginnt in einer geraden Linie, die am meisten vom Kurs abweicht, macht aber sehr schnell eine Haarnadelkurve in Richtung des Hügels, auf dem sich das Ziel befindet, und kurvt schneller als die ersten drei darauf zu. Adadelta hat einen ähnlichen Pfad, aber mit einer glatteren Kurve; es überholt Adagrad und bleibt nach etwa der ersten Sekunde vorne. Schließlich verfolgt Rmsprop einen sehr ähnlichen Weg wie Adadelta, nähert sich dem Ziel jedoch schon früh etwas näher; insbesondere ist sein Kurs viel stabiler, wodurch er für den größten Teil der Animation hinter Adagrad und Adadelta zurückbleibt; Im Gegensatz zu den anderen fünf scheint es gegen Ende der Animation zwei plötzliche, schnelle Sprünge in zwei verschiedene Richtungen zu haben, bevor es aufhört, sich zu bewegen, während die anderen im letzten Moment weiter langsam am Ziel vorbeikriechen.

Es ist jedoch selten nützlich, ein Experte für Optimierer zu sein – selbst für diejenigen, die daran interessiert sind, Entwicklungsdienste für künstliche Intelligenz anzubieten. Es ist eine bessere Nutzung der Entwicklerzeit, sich mit einem Paar vertraut zu machen, nur um zu verstehen, wie sie den Gradientenabstieg in TensorFlow verbessern. Danach können sie standardmäßig einfach Adam verwenden und nur dann andere ausprobieren, wenn ihre Modelle nicht konvergieren.

Für Leser, die wirklich daran interessiert sind, wie und warum diese Optimierer funktionieren, ist Ruders Überblick – in dem die Animation erscheint – eine der besten und umfassendsten Quellen zu diesem Thema.

Aktualisieren wir unsere lineare Regressionslösung aus dem ersten Abschnitt, um Optimierer zu verwenden. Das Folgende ist der ursprüngliche Gradientenabstiegscode unter Verwendung manueller Gradienten.

 # Manual gradient descent operations def run_gradient_descent(heights, weights, init_slope, init_icept, learning_rate): tf_slope = tf.Variable(init_slope, dtype='float32') tf_icept = tf.Variable(init_icept, dtype='float32') for i in range(25): with tf.GradientTape() as tape: tape.watch((tf_slope, tf_icept)) predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors**2) gradients = tape.gradient(loss, [tf_slope, tf_icept]) tf_slope = tf_slope - learning_rate * gradients[0] tf_icept = tf_icept - learning_rate * gradients[1]

Nun, hier ist derselbe Code, der stattdessen einen Optimierer verwendet. Sie werden sehen, dass es sich kaum um zusätzlichen Code handelt (geänderte Zeilen sind blau hervorgehoben):

 # Gradient descent with Optimizer (RMSprop) def run_gradient_descent (heights, weights, init_slope, init_icept, learning_rate) : tf_slope = tf.Variable(init_slope, dtype= 'float32' ) tf_icept = tf.Variable(init_icept, dtype= 'float32' ) # Group trainable parameters into a list trainable_params = [tf_slope, tf_icept] # Define your optimizer (RMSprop) outside of the training loop optimizer = keras.optimizers.RMSprop(learning_rate) for i in range( 25 ): # GradientTape loop is the same with tf.GradientTape() as tape: tape.watch( trainable_params ) predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors** 2 ) # We can use the trainable parameters list directly in gradient calcs gradients = tape.gradient(loss, trainable_params ) # Optimizers always aim to *minimize* the loss function optimizer.apply_gradients(zip(gradients, trainable_params))

Das ist es! Wir haben einen RMSprop -Optimierer außerhalb der Gradientenabstiegsschleife definiert und dann nach jeder Gradientenberechnung die Methode optimizer.apply_gradients() verwendet, um die trainierbaren Parameter zu aktualisieren. Der Optimierer wird außerhalb der Schleife definiert, da er historische Gradienten zur Berechnung zusätzlicher Terme wie Momentum und Ableitungen höherer Ordnung verfolgt.

Mal sehen, wie es mit dem RMSprop- Optimierer aussieht.

Ähnlich wie bei den vorherigen synchronisierten Animationspaaren; die angepasste Linie beginnt oberhalb ihres Ruheplatzes. Das Verlustdiagramm zeigt, dass es nach nur fünf Iterationen fast konvergiert.

Sieht großartig aus! Versuchen wir es jetzt mit dem Adam -Optimierer.

Ein weiteres synchronisiertes Streudiagramm und eine entsprechende Animation des Verlustdiagramms. Die Verlustkurve hebt sich von den anderen dadurch ab, dass sie sich nicht strikt weiter dem Minimum annähert; Stattdessen ähnelt es dem Weg eines springenden Balls. Die entsprechende Anpassungslinie im Streudiagramm beginnt über den Beispielpunkten, schwingt zu ihrem unteren Ende, dann wieder nach oben, aber nicht so hoch, und so weiter, wobei jede Richtungsänderung näher an einer zentralen Position liegt.

Wow, was ist hier passiert? Es scheint, dass die Impulsmechanik in Adam dazu führt, dass es über die optimale Lösung hinausschießt und den Kurs mehrmals umkehrt. Normalerweise hilft diese Momentum-Mechanik bei komplexen Verlustflächen, aber in diesem einfachen Fall tut sie uns weh. Dies unterstreicht den Rat, die Wahl des Optimierers zu einem der Hyperparameter zu machen, die beim Trainieren Ihres Modells optimiert werden müssen.

Jeder, der Deep Learning erforschen möchte, sollte sich mit diesem Muster vertraut machen, da es häufig in benutzerdefinierten TensorFlow-Architekturen verwendet wird, wo komplexe Verlustmechanismen erforderlich sind, die nicht einfach in den Standard-Workflow integriert werden können. In diesem einfachen TensorFlow-Beispiel für den Gradientenabstieg gab es nur zwei trainierbare Parameter, aber es ist notwendig, wenn Sie mit Architekturen arbeiten, die Hunderte Millionen Parameter enthalten, um sie zu optimieren.

Gradientenabstieg in TensorFlow: Von der Suche nach Minima bis zum Angriff auf KI-Systeme

Alle Codeschnipsel und Bilder wurden aus den Notebooks im entsprechenden GitHub-Repo erstellt. Es enthält auch eine Zusammenfassung aller Abschnitte mit Links zu den einzelnen Notizbüchern für Leser, die den vollständigen Code sehen möchten. Zur Vereinfachung der Meldung wurden viele Details weggelassen, die in der umfangreichen Inline-Dokumentation nachzulesen sind.

Ich hoffe, dieser Artikel war aufschlussreich und hat Sie dazu gebracht, darüber nachzudenken, wie Sie Gradientenabstieg in TensorFlow verwenden können. Auch wenn Sie es nicht selbst verwenden, macht es hoffentlich klarer, wie alle modernen neuronalen Netzwerkarchitekturen funktionieren – erstellen Sie ein Modell, definieren Sie eine Verlustfunktion und verwenden Sie den Gradientenabstieg, um das Modell an Ihren Datensatz anzupassen.


Google Cloud-Partner-Logo.

Als Google Cloud Partner stehen die Google-zertifizierten Experten von Toptal Unternehmen on demand für ihre wichtigsten Projekte zur Verfügung.