KAPITEL 4

Entwurfsmuster für das Modelltraining

Modelle für maschinelles Lernen werden üblicherweise iterativ trainiert. Diesen iterativen Prozess nennt man formlos Trainingsschleife. In diesem Kapitel erläutern wir, wie die typische Trainingsschleife aussieht, und katalogisieren eine Reihe von Situationen, in denen Sie möglicherweise etwas anderes tun möchten.

Typische Trainingsschleife

Modelle für maschinelles Lernen lassen sich mit verschiedenen Arten der Optimierung trainieren. Entscheidungsbäume werden oft Knoten für Knoten nach einem Informationsgewinnmaß aufgebaut. Genetische Algorithmen stellen die Modellparameter als Gene dar, und die Optimierungsmethode umfasst Techniken, die auf der Evolutionstheorie fußen. Der gängigste Ansatz, um die Parameter von ML-Modellen zu bestimmen, ist jedoch der Gradientenabstieg.

Stochastischer Gradientenabstieg

Bei großen Datensätzen wird der Gradientenabstieg auf Mini-Batches der Eingabedaten angewendet, um alles – angefangen bei linearen Modellen und gewichteten Entscheidungsbäumen (Boosted Trees) bis hin zu tiefen neuronalen Netzen (Deep Neural Networks, DNNs) und Support Vector Machines (SVMs) – zu trainieren. Man spricht hierbei vom stochastischen Gradientenabstieg (SGD), und Erweiterungen von SGD (wie zum Beispiel Adam und Adagrad) sind die De-facto-Optimierer, die in modernen ML-Frameworks verwendet werden.

Da SGD verlangt, dass das Training iterativ auf kleinen Batches des Trainingsdatensatzes stattfindet, läuft das Training eines ML-Modells in einer Schleife ab. SGD findet ein Minimum, stellt aber keine geschlossene Lösung dar. Und so müssen wir ermitteln, ob die Modellkonvergenz stattgefunden hat. Deshalb muss der Fehler (Verlust genannt) auf dem Trainingsdatensatz überwacht werden. Zu einer Überanpassung (engl. Overfitting) kann es kommen, wenn die Modellkomplexität höher ist, als es Größe und Abdeckung des Datensatzes zulassen. Leider können Sie nicht wissen, ob die Modellkomplexität für einen bestimmten Datensatz zu hoch ist, bis Sie das Modell tatsächlich auf diesem Datensatz trainieren. Daher muss die Auswertung innerhalb der Trainingsschleife stattfinden, und die Fehlermetriken auf einem zurückgehaltenen Teil der Trainingsdaten – dem sogenannten Validierungsdatensatz – müssen ebenfalls überwacht werden. Da die Trainings- und Validierungsdatensätze in der Trainingsschleife verwendet worden sind, ist es notwendig, einen weiteren Teil des Trainingsdatensatzes – den sogenannten Testdatensatz – zurückzuhalten, um die tatsächlichen Fehlermetriken zu melden, die für neue und bisher ungesehene Daten zu erwarten wären. Diese Auswertung wird am Ende durchgeführt.

Keras-Trainingsschleife

Die typische Trainingsschleife sieht in Keras wie folgt aus:

model = keras.Model(...)

model.compile(optimizer=keras.optimizers.Adam(),

loss=keras.losses.categorical_crossentropy(),

metrics=['accuracy'])

history = model.fit(x_train, y_train,

batch_size=64,

epochs=3,

validation_data=(x_val, y_val))

results = model.evaluate(x_test, y_test, batch_size=128))

model.save(...)

Hier verwendet das Modell den Adam-Optimierer, um SGD auf der Kreuzentropie über dem Trainingsdatensatz auszuführen, und meldet die für den Testdatensatz erhaltene endgültige Genauigkeit. Die Modellanpassung durchläuft den Trainingsdatensatz dreimal (jeder Durchgang über dem Trainingsdatensatz wird als Epoche bezeichnet), wobei das Modell Batches, bestehend aus jeweils 64 Trainingsbeispielen, sieht. Am Ende jeder Epoche werden die Fehlermetriken auf dem Validierungsdatensatz berechnet und zur Geschichte (history) hinzugefügt. Am Ende der Anpassungsschleife wird das Modell auf dem Testdatensatz bewertet, gespeichert und potenziell für das Serving bereitgestellt, wie Abbildung 4-1 zeigt.

image

Abbildung 4-1: Eine typische Trainingsschleife, die aus drei Epochen besteht. Jede Epoche wird in Chunks von »batch_size« Beispielen verarbeitet. Am Ende der dritten Epoche wird das Modell auf dem Testdatensatz ausgewertet und für eine mögliche Bereitstellung als Webservice gespeichert.

Anstatt die vordefinierte Funktion fit() zu verwenden, können wir auch eine benutzerdefinierte Trainingsschleife schreiben, die explizit über die Batches iteriert. Aber wir werden dies für keines der in diesem Kapitel besprochenen Entwurfsmuster benötigen.

Training-Entwurfsmuster

Die in diesem Kapitel behandelten Entwurfsmuster haben alle damit zu tun, die typische Trainingsschleife in irgendeiner Weise zu modifizieren. In »Entwurfsmuster 11: Nützliche Überanpassung« verzichten wir darauf, einen Validierungs- oder Testdatensatz zu verwenden, weil wir absichtlich eine Überanpassung auf dem Trainingsdatensatz anstreben. In »Entwurfsmuster 12: Checkpoints« speichern wir den vollständigen Status des Modells periodisch, damit wir auf teilweise trainierte Modelle zugreifen können. Im Zusammenhang mit Checkpoints verwenden wir normalerweise auch virtuelle Epochen, in denen wir dazu entscheiden, die innere Schleife der Funktion fit() nicht auf dem vollständigen Trainingsdatensatz, sondern auf einer festen Anzahl von Trainingsbeispielen auszuführen. In »Entwurfsmuster 13: Transfer Learning« nehmen wir einen Teil eines zuvor trainierten Modells, frieren die Gewichte ein und integrieren diese nicht trainierbaren Schichten in ein neues Modell, das das gleiche Problem löst, allerdings auf einem kleineren Datensatz. In »Entwurfsmuster 14: Verteilungsstrategie« wird die Trainingsschleife in großem Umfang über mehrere Worker ausgeführt, oft mit Caching, Hardwarebeschleunigung und Parallelisierung. Schließlich wird in »Entwurfsmuster 15: Hyperparameter-Abstimmung« die Trainingsschleife selbst in eine Optimierungsmethode eingefügt, um den optimalen Satz von Hyperparametern für das Modell zu finden.

Entwurfsmuster 11: Nützliche Überanpassung

Nützliche Überanpassung ist ein Entwurfsmuster, bei dem wir darauf verzichten, Mechanismen zur Verallgemeinerung anzuwenden, weil wir absichtlich eine Überanpassung auf dem Trainingsdatensatz anstreben. In Situationen, in denen Überanpassung vorteilhaft sein kann, empfiehlt dieses Entwurfsmuster, maschinelles Lernen ohne Regularisierung, Dropout oder einen Validierungsdatensatz zum Early Stopping durchzuführen.

Problem

Ein Modell für maschinelles Lernen soll verallgemeinern und zuverlässige Vorhersagen mit neuen, ungesehenen Daten treffen. Wenn sich Ihr Modell an die Trainingsdaten überanpasst (zum Beispiel den Trainingsfehler über den Punkt hinaus verringert, an dem der Validierungsfehler zu steigen beginnt), kann es nicht mehr richtig verallgemeinern, und damit werden auch Ihre zukünftigen Vorhersagen schlechter. Lehrbücher mit Einführungen zum maschinellen Lernen raten dazu, Überanpassung durch Early Stopping und Techniken zur Regularisierung zu vermeiden.

Stellen Sie sich jedoch vor, Sie simulierten das Verhalten von physikalischen oder dynamischen Systemen, wie sie in der Klimaforschung, der Computerbiologie oder im Finanzwesen vorkommen. In derartigen Systemen lässt sich die Zeitabhängigkeit der Beobachtungen durch eine mathematische Funktion oder einen Satz von partiellen Differentialgleichungen (engl. Partial Differential Equation, PDE) beschreiben. Obwohl man die Gleichungen, die für viele dieser Systeme bestimmend sind, formal ausdrücken kann, haben sie keine Lösung in geschlossener Form. Stattdessen sind klassische numerische Methoden entwickelt worden, um die Lösungen für diese Systeme zu approximieren. Leider können diese Methoden bei vielen realen Anwendungen zu langsam sein, um sie in der Praxis zu verwenden.

Sehen Sie sich die Situation an, die in Abbildung 4-2 dargestellt ist. Die aus der physikalischen Umgebung gesammelten Beobachtungen dienen als Eingaben (oder anfängliche Startbedingungen) für ein physikalisch basiertes Modell, das iterative, numerische Berechnungen durchführt, um den präzisen Zustand des Systems zu berechnen. Wir nehmen an, dass die Anzahl der Möglichkeiten bei allen Beobachtungen endlich ist (zum Beispiel liegt die Temperatur zwischen 60 und 80 °C und wird in Schritten von 0,01 Grad gemessen). Es ist dann möglich, einen Trainingsdatensatz für das ML-System zu erstellen, der aus dem gesamten Eingaberaum besteht, und die Labels mithilfe des physikalischen Modells zu berechnen.

image

Abbildung 4-2: Eine Überanpassung ist in einer Situation akzeptabel, wenn der gesamte Domänenraum der Beobachtungen tabellarisch dargestellt werden kann und es ein physikalisches Modell gibt, das die genaue Lösung berechnen kann.

Das ML-Modell muss diese genau berechnete und nicht überlappende Nachschlagetabelle von Eingaben zu Ausgaben lernen. Es wäre kontraproduktiv, einen derartigen Datensatz in einen Trainingsdatensatz und einen Evaluierungsdatensatz aufzuteilen, weil wir dann erwarten würden, dass das Modell Teile des Eingaberaums lernt, die es im Trainingsdatensatz nicht gesehen hat.

Lösung

In diesem Szenario gibt es keine »ungesehenen« Daten, die verallgemeinert werden müssen, da alle möglichen Eingaben tabellarisch erfasst worden sind. Wenn man ein ML-Modell erstellt, das ein derartiges physikalisches Modell oder dynamisches System lernen soll, gibt es so etwas wie Überanpassung nicht. Das grundlegende ML-Trainingsparadigma sieht etwas anders aus. Hier gibt es ein physikalisches Phänomen, das Sie zu lernen versuchen und das durch eine darunterliegende PDE oder ein System von PDEs bestimmt wird. Maschinelles Lernen bietet lediglich einen datengesteuerten Ansatz, um die präzise Lösung anzunähern, und Konzepte wie Überanpassung müssen neu bewertet werden.

Zum Beispiel simuliert man mit einem Ray-Tracing-Ansatz Satellitenbilder, die sich aus der Ausgabe von numerischen Wettervorhersagemodellen ergeben würden. Dabei ist auch zu berechnen, welcher Anteil eines Sonnenstrahls durch die vorhergesagten Hydrometeoren (Regentropen, Schneeflocken, Hagelkörner, Eiskörner usw.) auf jeder atmosphärischen Ebene absorbiert wird. Es gibt eine endliche Anzahl möglicher Hydrometeortypen und eine endliche Anzahl von Höhen, die das numerische Modell vorhersagt. Somit muss das Ray-Tracing-Modell optische Gleichungen auf einen großen, aber endlichen Satz von Eingaben anwenden.

Die Gleichungen des Strahlungstransfers bestimmen das komplexe dynamische System der Ausbreitung elektromagnetischer Strahlung in der Atmosphäre, und Vorwärts-Strahlungsmodelle sind ein effektives Instrument, um auf den zukünftigen Zustand von Satellitenbildern zu schließen. Die klassischen numerischen Methoden, mit denen die Lösungen dieser Gleichungen berechnet werden, können jedoch einen enormen Rechenaufwand erfordern und sind zu langsam, um sie in der Praxis einzusetzen.

Hier kommt maschinelles Lernen ins Spiel. Mit maschinellem Lernen lässt sich ein Modell erstellen, das Lösungen für das Vorwärts-Strahlungstransfermodell (https://oreil.ly/IkYKm) annähert (siehe Abbildung 4-3). Der ursprünglich mit eher klassischen Methoden erreichten Lösung des Modells kann diese ML-Approximation nahe genug kommen. Der Vorteil ist, dass eine Inferenz mithilfe gelernter ML-Approximation (die lediglich eine geschlossene Formel berechnen muss) nur einen Bruchteil der Zeit für das Ray-Tracing (das numerische Methoden erfordern würde) benötigt. Gleichzeitig ist der Trainingsdatensatz zu groß (mehrere TBytes) und zu unhandlich, um ihn in der Produktion als Nachschlagetabelle zu verwenden.

image

Abbildung 4-3: Architektur, um mit einem neuronalen Netz die Lösung einer partiellen Differentialgleichung zu modellieren, die für I(r, t, n) zu lösen ist

Es gibt einen wichtigen Unterschied zwischen dem Training eines ML-Modells, das die Lösung eines derartigen dynamischen Systems annähert, und dem Training eines ML-Modells, das Babygewichte basierend auf im Laufe der Jahre gesammelten Geburtsdaten vorhersagt. Das dynamische System ist nämlich ein Satz von Gleichungen, die den Gesetzen der elektromagnetischen Strahlung unterliegen – es gibt keine unbeobachtete Variable, kein Rauschen und keine statistische Variabilität. Für einen gegebenen Satz von Eingaben gibt es nur eine genau berechenbare Ausgabe. Es gibt keine Überschneidungen zwischen verschiedenen Beispielen im Trainingsdatensatz. Aus diesem Grund können wir die Bedenken in Bezug auf die Generalisierung über Bord werfen. Wir möchten, dass sich unser Modell so perfekt wie möglich an die Trainingsdaten anpasst, also »überanpasst«.

Dies steht im Gegensatz zum typischen Trainingsansatz eines ML-Modells, bei dem Überlegungen zu Bias, Varianz und Generalisierungsfehlern eine wichtige Rolle spielen. Traditionelles Training besagt, dass ein Modell die Trainingsdaten »zu gut« lernen kann und dass das Trainieren Ihres Modells bis zu dem Punkt, an dem die Verlustfunktion gleich null ist, eher ein Warnsignal auslöst als ein Grund zum Feiern ist. Eine Überanpassung des Trainingsdatensatzes auf diese Weise führt dazu, dass das Modell falsche Vorhersagen für neue, ungesehene Datenpunkte gibt. Der Unterschied besteht darin, dass wir im Voraus wissen, dass es keine ungesehenen Daten geben wird, sodass das Modell eine Lösung für eine partielle Differentialgleichung über das gesamte Eingabespektrum approximiert. Wenn Ihr neuronales Netz in der Lage ist, einen Satz von Parametern zu lernen, wobei die Verlustfunktion null ist, dann bestimmt dieser Parametersatz die tatsächliche Lösung der fraglichen partiellen Differentialgleichung.

Warum es funktioniert

Können alle möglichen Eingaben tabellarisch dargestellt werden, trifft ein überangepasstes Modell immer noch dieselben Vorhersagen – siehe die gestrichelte Linie in Abbildung 4-4 – wie das »wahre« Modell, wenn für alle möglichen Eingabepunkte trainiert wird. Somit ist Überanpassung kein Problem. Wir müssen darauf achten, dass Inferenzen auf abgerundete Werte der Eingaben erfolgen, wobei die Rundung durch die Auflösung bestimmt wird, mit der der Eingaberaum gerastert wurde.

image

Abbildung 4-4: Überanpassung ist kein Problem, wenn für alle möglichen Eingabepunkte trainiert wird, da die Vorhersagen bei beiden Kurven gleich sind.

Ist es möglich, eine Modellfunktion zu finden, die beliebig nahe an die wahren Labels herankommt? Ein Hinweis darauf, warum dies funktioniert, ergibt sich aus dem Universal Approximation Theorem des Deep Learning, das im Wesentlichen besagt, dass jede Funktion (und ihre Ableitungen) durch ein neuronales Netz mit mindestens einer verdeckten Schicht und einer beliebigen »quetschenden« Aktivierungsfunktion wie Sigmoid angenähert werden kann. Das heißt, dass unabhängig von der gegebenen Funktion – solange sie sich einigermaßen gutartig verhält – ein neuronales Netz mit nur einer verdeckten Schicht existiert, das diese Funktion so genau wie gewünscht approximiert.1

Deep-Learning-Ansätze zum Lösen von Differentialgleichungen oder komplexen dynamischen Systemen streben danach, eine Funktion, die implizit durch eine Differentialgleichung oder ein System von Gleichungen definiert ist, mithilfe eines neuronalen Netzes darzustellen.

Überanpassung ist nützlich, wenn die beiden folgenden Bedingungen erfüllt sind:

Kompromisse und Alternativen

Wir haben Überanpassung als nützlich eingeführt, wenn der Satz der Eingaben erschöpfend aufgelistet und das genaue Label für jeden Satz von Eingaben berechnet werden kann. Wenn sich der gesamte Eingaberaum tabellarisch auflisten lässt, ist Überanpassung kein Problem, da es keine ungesehenen Daten gibt. Jedoch ist das Entwurfsmuster Nützliche Überanpassung über diesen engen Anwendungsfall hinaus nützlich. In vielen realen Situationen bleibt das Konzept gültig, dass Überanpassung nützlich sein kann, selbst wenn eine oder mehrere dieser Bedingungen gelockert werden müssen.

Interpolation und Chaostheorie

Das ML-Modell funktioniert im Wesentlichen als Approximation einer Nachschlagetabelle von Eingaben zu Ausgaben. Ist die Nachschlagetabelle klein, verwenden Sie sie einfach als Nachschlagetabelle! Es ist nicht notwendig, sie durch ein ML-Modell anzunähern. Eine ML-Approximation ist nützlich in Situationen, in denen die Nachschlagetabelle zu groß ist, um sie effektiv zu verwenden. Sollte die Nachschlagetabelle zu unhandlich sein, ist es besser, sie als Trainingsdatensatz für ein ML-Modell zu behandeln, das die Nachschlagetabelle approximiert.

Wir haben aber nun angenommen, dass die Beobachtungen eine endliche Anzahl von Möglichkeiten haben, zum Beispiel dass die Temperatur in Schritten von 0,01 °C gemessen wird und zwischen 60 °C und 80 °C liegt. Dies wird der Fall sein, wenn die Beobachtungen mit digitalen Instrumenten vorgenommen werden. Andernfalls ist das ML-Modell erforderlich, um zwischen den Einträgen in der Nachschlagetabelle zu interpolieren.

ML-Modelle interpolieren, indem sie ungesehene Werte entsprechend ihrem Abstand zu Trainingsbeispielen gewichten. Eine derartige Interpolation funktioniert nur, wenn das zugrunde liegende System nicht chaotisch ist. In chaotischen Systemen können kleine Unterschiede in den Anfangsbedingungen zu dramatisch unterschiedlichen Ergebnissen führen, selbst wenn das System deterministisch ist.

In der Praxis hat jedoch jedes spezifische chaotische Phänomen eine bestimmte Auflösungsschwelle (https://oreil.ly/F-drU), ab der es für Modelle möglich ist, das Phänomen über kurze Zeiträume vorherzusagen. Wenn also die Nachschlagetabelle genügend fein ist und die Grenzen der Auflösbarkeit bekannt sind, können sich brauchbare Approximationen ergeben.

Monte-Carlo-Methoden

In der Realität ist es nicht immer möglich, alle möglichen Eingaben tabellarisch zu erfassen. Vielleicht greifen Sie zu einem Monte-Carlo-Ansatz (https://oreil.ly/pTgS9), um eine Stichprobe aus dem Eingaberaum zu ziehen und den Satz der Eingaben zu bilden. Das gilt insbesondere dort, wo nicht alle möglichen Eingabekombinationen physikalisch möglich sind.

In derartigen Fällen ist Überanpassung technisch möglich (siehe Abbildung 4-5, wobei die leeren Kreise durch falsche Schätzungen – dargestellt durch Kreise mit einem Kreuz – approximiert werden).

image

Abbildung 4-5: Wenn der Eingaberaum abgetastet und nicht tabellarisch erfasst wird, müssen Sie darauf achten, die Modellkomplexität zu begrenzen.

Aber auch hier können Sie sehen, dass das ML-Modell zwischen bekannten Antworten interpoliert. Die Berechnung ist immer deterministisch, nur die Eingabepunkte werden zufällig ausgewählt. Demzufolge enthalten diese bekannten Antworten kein Rauschen, und weil es keine unbeobachteten Variablen gibt, sind Fehler bei nicht in der Stichprobe enthaltenen Punkten streng durch die Modellkomplexität begrenzt. Hier kommt die Gefahr der Überanpassung von der Modellkomplexität und nicht von der Anpassung an das Rauschen. Überanpassung ist nicht so sehr ein Problem, wenn die Größe des Datensatzes größer als die Anzahl der freien Parameter ist. Daher bietet eine Kombination aus Modellen geringer Komplexität und schwacher Regularisierung eine praktische Möglichkeit, inakzeptable Überanpassung bei Monte-Carlo-Auswahl des Eingaberaums zu vermeiden.

Datengesteuerte Diskretisierungen

Obwohl sich für einige PDEs eine Lösung in geschlossener Form ableiten lässt, ist es üblicher, Lösungen mit numerischen Methoden zu bestimmen. Numerische Methoden von PDEs sind bereits ein großes Forschungsgebiet, und es gibt viele Bücher (https://oreil.ly/RJWVQ), Kurse (https://oreil.ly/wcl_n) und Fachzeitschriften (https://msp.org/apde), die sich mit diesem Thema beschäftigen. Ein gängiger Ansatz ist die Verwendung von Finite-Differenzen-Methoden – ähnlich dem Runge-Kutta-Verfahren – für die Lösung gewöhnlicher Differentialgleichungen. Dies geschieht typischerweise durch Diskretisierung des Differentialoperators der PDE und dem Suchen einer Lösung für das diskrete Problem auf einem räumlich-zeitlichen Gitter des ursprünglichen Bereichs. Wenn die Dimension des Problems groß wird, scheitert dieser maschenbasierte Ansatz wegen des Fluchs der Dimensionalität dramatisch, weil der Maschenabstand des Gitters klein genug (https://oreil.ly/TxHD-) sein muss, um die kleinste Feature-Größe der Lösung zu erfassen. Um also eine 10-fach höhere Auflösung eines Bilds zu erreichen, ist die 10.000-fache Rechenleistung erforderlich, da das Maschengitter in vier Dimensionen – für Raum und Zeit – skaliert werden muss.

Es ist jedoch möglich, mit maschinellen Lernmethoden (statt Monte-Carlo-Methoden) die Stichprobenpunkte auszuwählen, um datengesteuerte Diskretisierungen von PDEs zu erzeugen. Im Paper »Learning data-driven discretizations for PDEs« (https://oreil.ly/djDkK) demonstrieren Bar-Sinai et al. die Wirksamkeit dieses Ansatzes. Die Autoren verwenden ein Gitter fester Punkte in geringer Auflösung, um sich einer Lösung anzunähern durch eine stückweise Polynominterpolation mithilfe von standardmäßigen Finite-Differenzen-Methoden sowie durch ein neuronales Netz. Die aus dem neuronalen Netz erhaltene Lösung übertrifft die numerische Simulation bei der Minimierung des absoluten Fehlers enorm und erreicht an manchen Stellen eine Verbesserung um 102 Größenordnungen. Während eine höhere Auflösung beträchtlich mehr Rechenleistung bei Finite-Differenzen-Methoden erfordert, ist das neuronale Netz in der Lage, eine hohe Performance mit nur marginalen Zusatzkosten beizubehalten. Techniken wie die Deep-Galerkin-Methode können dann Deep Learning verwenden, um eine maschenfreie Approximation der Lösung für die gegebene PDE zu liefern. Auf diese Weise wird das Lösen der partiellen Differentialgleichung auf ein verkettetes Optimierungsproblem reduziert (siehe »Entwurfsmuster 8: Kaskade« auf Seite 130).

Deep-Galerkin-Methode

Die Deep-Galerkin-Methode (https://oreil.ly/rQy4d) ist ein Deep-Learning-Algorithmus zum Lösen von partiellen Differentialgleichungen. Der Algorithmus ähnelt im Geiste den Galerkin-Methoden, die im Bereich der numerischen Analyse verwendet werden, wobei die Lösung mithilfe eines neuronalen Netzes anstelle einer Linearkombination von Basisfunktionen approximiert wird.

Unbeschränkte Domänen

Die Monte-Carlo-Methoden und die datengesteuerten Diskretisierungsmethoden gehen davon aus, dass eine Abtastung des gesamten Eingaberaums, wenn auch unvollkommen, möglich ist. Deshalb wurde das ML-Modell als Interpolation zwischen bekannten Punkten behandelt.

Verallgemeinerung und das Problem der Überanpassung lassen sich nur schwer ignorieren, wenn wir nicht in der Lage sind, Punkte im gesamten Bereich der Funktion abzutasten – zum Beispiel für Funktionen mit unbeschränkten Domänen oder Projektionen entlang einer Zeitachse in die Zukunft. In dieser Umgebung ist es wichtig, Überanpassung, Unteranpassung und Generalisierungsfehler zu berücksichtigen. Es hat sich nämlich gezeigt, dass zwar Techniken wie die Deep-Galerkin-Methode in gut abgetasteten Bereichen gut funktionieren, aber eine Funktion, die auf diese Weise gelernt wird, generalisiert nur mäßig in Bereichen außerhalb der Domäne, die in der Trainingsphase nicht abgetastet wurden. Dies kann problematisch sein, wenn partielle Differentialgleichungen durch maschinelles Lernen gelöst werden sollen, wenn diese Gleichungen auf unbeschränkten Domänen definiert sind, da es unmöglich wäre, eine repräsentative Stichprobe für das Training zu erfassen.

Wissen aus einem neuronalen Netz destillieren

Überanpassung ist auch gerechtfertigt, wenn Wissen aus einem großen ML-Modell in ein kleineres destilliert oder übertragen werden soll. Wissensdestillation ist nützlich, wenn die Lernkapazität des großen Modells nicht voll ausgeschöpft wird. In derartigen Fällen ist die rechentechnische Kapazität des großen Modells möglicherweise nicht notwendig. Es stimmt aber auch, dass kleinere Modelle schwieriger zu trainieren sind. Das kleinere Modell besitzt zwar genügend Kapazität, um das Wissen darzustellen, doch hat es vielleicht nicht genügend Kapazität, um das Wissen effizient zu lernen.

Die Lösung besteht darin, das kleinere Modell auf einer großen Menge von generierten Daten zu trainieren, die durch das größere Modell gelabelt werden. Das kleinere Modell lernt die Soft-Ausgabe des größeren Modells anstelle der tatsächlichen Labels auf realen Daten. Dies ist ein einfacheres Problem, das von dem kleineren Modell gelernt werden kann. Wie bei der Approximation einer numerischen Funktion durch ein ML-Modell soll das kleinere Modell die Vorhersagen des größeren ML-Modells getreu darstellen. Dieser zweite Trainingsschritt kann Nützliche Überanpassung verwenden.

Überanpassen eines Batches

In der Praxis erfordert das Training eines neuronalen Netzes zahlreiche Experimente. Praktiker:innen müssen viele Entscheidungen treffen, angefangen bei der Größe und der Architektur des Netzwerks bis hin zur Wahl der Lernrate, den Gewichtsinitialisierungen oder anderen Hyperparametern. Überanpassung auf einem kleinen Batch ist eine gute Plausibilitätsprüfung (https://oreil.ly/AcLtu) sowohl für den Modellcode als auch für die Dateneingabepipeline. Nur weil sich das Modell kompilieren lässt und der Code ohne Fehler läuft, heißt das nicht, dass Sie das berechnet haben, was Sie berechnet zu haben glauben, oder dass das Trainingsziel richtig konfiguriert ist. Ein ausreichend komplexes Modell sollte in der Lage sein, einen genügend kleinen Batch von Daten überanzupassen, vorausgesetzt, dass alles richtig eingerichtet ist. Wenn Sie also einen kleinen Batch mit einem Modell nicht überanpassen können, sollten Sie Ihren Modellcode, die Eingabepipeline und die Verlustfunktion noch einmal auf Fehler oder einfache Bugs überprüfen. Überanpassung auf einem Batch ist eine nützliche Technik, wenn man neuronale Netze trainiert und auf Fehler untersucht.

image

Überanpassung geht über nur einen Batch hinaus. Aus einer ganzheitlichen Perspektive folgt Überanpassung der allgemeinen Empfehlung, die in Bezug auf Deep Learning und Regularisierung häufig gegeben wird. Das am besten angepasste Modell ist ein großes Modell, das in geeigneter Weise regularisiert wurde (https://oreil.ly/A7DFC). Kurz gesagt, wenn Ihr tiefes neuronales Netz nicht zu einer Überanpassung an den Trainingsdatensatz in der Lage ist, sollten Sie ein größeres verwenden. Sobald Sie dann ein ausreichend großes Modell haben, das zu einer Überanpassung an den Trainingsdatensatz fähig ist, können Sie Regularisierung anwenden, um die Validierungsgenauigkeit zu verbessern, selbst wenn die Trainingsgenauigkeit möglicherweise abnimmt.

Ihren Keras-Modellcode können Sie auf diese Weise mit dem tf.data.Dataset testen, das Sie für Ihre Eingabepipeline geschrieben haben. Wenn Ihre Trainingsdaten-Eingabepipeline zum Beispiel trainds heißt, rufen Sie mit batch() einen einzelnen Daten-Batch ab. Den vollständigen Code für dieses Beispiel finden Sie im Repository zu diesem Buch (https://github.com/GoogleCloudPlatform/ml-design-patterns/blob/master/04_hacking_training_loop/distribution_strategies.ipynb):

BATCH_SIZE = 256

single_batch = trainds.batch(BATCH_SIZE).take(1)

Wenn Sie dann das Modell trainieren, rufen Sie in der Methode fit() nicht den gesamten trainds-Datensatz auf, sondern verwenden diesen eben erzeugten einzelnen Batch:

model.fit(single_batch.repeat(),

validation_data=evalds,

...)

Mit dem Aufruf von repeat() stellen wir sicher, dass uns die Daten nicht ausgehen, wenn wir auf diesem einzelnen Batch trainieren. Während des Trainings nehmen wir also diesen einen Batch immer und immer wieder. Alles andere (der Validierungsdatensatz, der Modellcode, die Engineered Features usw.) bleibt gleich.

image

Anstatt eine willkürliche Stichprobe des Trainingsdatensatzes auszuwählen, empfehlen wir, dass Sie eine Überanpassung an einem kleinen Datensatz vornehmen, dessen Beispiele sorgfältig auf korrekte Labels hin überprüft wurden. Entwerfen Sie die Architektur Ihres neuronalen Netzes so, dass es in der Lage ist, diesen Batch genau zu lernen und zu einem Verlust von null zu kommen. Dann nehmen Sie dasselbe Netz und trainieren es mit dem vollständigen Trainingsdatensatz.

Entwurfsmuster 12: Checkpoints

In Checkpoints speichern wir den vollständigen Status des Modells periodisch, sodass wir partiell trainierte Modelle zur Verfügung haben. Diese partiell trainierten Modelle können als endgültiges Modell dienen (im Fall eines vorzeitigen Stoppens) oder als Ausgangspunkte für das weitere Training (im Fall von Hardwareausfall und Feinabstimmung).

Problem

Je komplexer ein Modell ist (je mehr Schichten und Knoten zum Beispiel ein neuronales Netz hat), desto größer ist der erforderliche Datensatz, um das Modell effizient zu trainieren. Das liegt daran, dass komplexere Modelle in der Regel mehr abstimmbare Parameter haben. Nimmt die Modellgröße zu, dauert es auch länger, einen Batch von Beispielen anzupassen. Wenn der Umfang der Daten wächst (und eine feste Batch-Größe angenommen wird), steigt auch die Anzahl der Batches. Hinsichtlich der Rechenkomplexität bedeutet dieses zweiseitige Problem, dass das Training viel Zeit in Anspruch nimmt.

Derzeit dauert das Training eines Englisch-Deutsch-Übersetzungsmodells auf einem modernen TPU-Pod (Tensor Processing Unit) für einen relativ kleinen Datensatz ungefähr zwei Stunden (https://oreil.ly/vDRve). Für reale Datensätze, wie sie für das Training von Smart Devices verwendet werden, kann das Training mehrere Tage dauern.

Wenn ein Training so lange dauert, ist die Wahrscheinlichkeit eines Hardwareausfalls unangenehm hoch. Tritt ein Problem auf, möchten wir von einem Zwischenstadium aus fortfahren können, anstatt wieder ganz von vorn beginnen zu müssen.

Lösung

Am Ende jeder Epoche können wir den Modellstatus speichern. Wenn dann die Trainingsschleife aus irgendeinem Grund unterbrochen wird, können wir zum gespeicherten Modellzustand zurückgehen und neu starten. Allerdings müssen wir dabei darauf achten, dass wir den Zwischenzustand des Modells und nicht einfach das Modell speichern. Was heißt das?

Wenn das Training abgeschlossen ist, speichern oder exportieren wir das Modell, damit wir es für die Inferenz bereitstellen können. Ein exportiertes Modell enthält nicht den gesamten Modellstatus, sondern nur die notwendigen Informationen, um die Vorhersagefunktion zu erzeugen. Bei einem Entscheidungsbaum wären das die endgültigen Regeln für jeden Zwischenknoten und den vorhergesagten Wert für jeden Blattknoten. Für ein lineares Modell wären es die endgültigen Gewichtsund Bias-Werte. In einem vollständig verknüpften Netz müssten wir auch die Aktivierungsfunktionen und die Gewichte der versteckten Verbindungen hinzufügen.

Welche Daten zum Modellstatus, die ein exportiertes Modell nicht enthält, benötigen wir für die Wiederherstellung von einem Checkpoint aus? In einem exportierten Modell ist nicht zu sehen, welche Epochen- und Batch-Nummer das Modell gerade verarbeitet, was offensichtlich aber wichtig ist, um das Training wieder aufzunehmen. Doch es gibt noch mehr Informationen, die eine Modelltrainingsschleife enthalten kann. Für einen effektiven Gradientenabstieg könnte der Optimierer die Lernrate nach einem Zeitplan anpassen. Diese Lernrate ist in einem exportierten Modell nicht vorhanden. Darüber hinaus kann es stochastisches Verhalten im Modell geben, beispielsweise Dropouts. Dies wird im exportierten Modellstatus ebenfalls nicht erfasst. Modelle wie rekurrente neurale Netzwerke binden einen Verlauf der vorherigen Einsatzwerte ein. Im Allgemeinen kann der vollständige Modellstatus die vielfache Größe des exportierten Modells betragen.

Das Speichern des vollständigen Modellstatus, damit sich das Modelltraining von einem bestimmten Punkt aus fortsetzen lässt, wird Checkpointing genannt, und die gespeicherten Modelldateien heißen Checkpoints. Wie oft sollte man einen Checkpoint speichern? Der Modellstatus ändert sich aufgrund des Gradientenabstiegs nach jedem Batch. Wenn wir keine Arbeit verlieren wollen, sollten wir also aus technischer Sicht nach jedem Batch einen Checkpoint setzen. Allerdings sind Checkpoints riesig, und diese Ein-/Ausgabe würde einen beträchtlichen Overhead verursachen. Stattdessen bieten viele Modell-Frameworks typischerweise die Option, am Ende jeder Epoche einen Checkpoint anzulegen. Dies ist ein vernünftiger Kompromiss zwischen gar keinem Checkpointing und Checkpointing nach jedem Batch.

Um in Keras einen Modell-Checkpoint zu setzen, geben Sie in der Methode fit() einen Callback an:

checkpoint_path = '{}/checkpoints/taxi'.format(OUTDIR)

cp_callback = tf.keras.callbacks.ModelCheckpoint(checkpoint_path,

save_weights_only=False,

verbose=1)

history = model.fit(x_train, y_train,

batch_size=64,

epochs=3,

validation_data=(x_val, y_val),

verbose=2,

callbacks=[cp_callback])

Mit hinzugefügtem Checkpointing sieht die Trainingsschleife dann etwa wie in Abbildung 4-6 aus.

image

Abbildung 4-6: Checkpointing speichert den vollständigen Modellstatus am Ende jeder Epoche.

Checkpoints in PyTorch

Derzeit unterstützt PyTorch Checkpoints nicht direkt. Allerdings ist es möglich, die Status der meisten Objekte zu externalisieren. Um Checkpoints in PyTorch zu implementieren, rufen Sie die Epoche, den Modellstatus, den Status des Optimierers und alle anderen Informationen ab, die für die Wiederaufnahme des Trainings erforderlich sind, um sie zusammen mit dem Modell zu serialisieren:

torch.save({

'epoch': epoch,

'model_state_dict': model.state_dict(),

'optimizer_state_dict': optimizer.state_dict(),

'loss': loss,

...

}, PATH)

Beim Laden von einem Checkpoint aus müssen Sie die erforderlichen Klassen erstellen und sie dann aus dem Checkpoint laden:

model = ...

optimizer = ...

checkpoint = torch.load(PATH)

model.load_state_dict(checkpoint['model_state_dict'])

optimizer.load_state_dict(checkpoint['optimizer_state_dict'])

epoch = checkpoint['epoch']

loss = checkpoint['loss']

Dieser Code liegt eine Ebene unterhalb von TensorFlow, bietet aber die Flexibilität, mehrere Modelle in einem Checkpoint zu speichern und auszuwählen, welche Teile des Modellstatus geladen oder nicht geladen werden sollen.

Warum es funktioniert

TensorFlow und Keras setzen das Training automatisch von einem Checkpoint aus fort, wenn Checkpoints im Ausgabepfad gefunden werden. Um das Training von Grund auf neu zu beginnen, müssen Sie von einem neuen Ausgabeverzeichnis beginnen (oder vorherige Checkpoints aus dem Ausgabeverzeichnis löschen). Dies funktioniert, weil Frameworks für maschinelles Lernen auf Unternehmensniveau die Anwesenheit von Checkpoint-Dateien anerkennen.

Selbst wenn Checkpoints hauptsächlich in Bezug auf Robustheit konzipiert sind, eröffnet die Verfügbarkeit von partiell trainierten Modellen eine Reihe anderer Anwendungsfälle. Das liegt daran, dass die partiell trainierten Modelle in der Regel besser verallgemeinerbar sind als die in späteren Iterationen erstellten Modelle. Warum dies auftritt, lässt sich intuitiv im TensorFlow-Playground (https://oreil.ly/sRjkN) erkennen, wie Abbildung 4-7 zeigt.

image

Abbildung 4-7: Ausgangspunkt des Problems zur Klassifizierung von Spiralen. Dieses Setup können Sie sich über https://oreil.ly/ISg9X in einem Webbrowser ansehen.

Im Playground wollen wir einen Klassifizierer erstellen, um zwischen blauen und orangefarbenen Punkten zu unterscheiden. (Wenn Sie die Printausgabe dieses Buchs lesen, folgen Sie bitte dem Link in einem Webbrowser.) Die beiden Eingabe-Features sind x1 und x2, die Koordinaten der Punkte. Anhand dieser Features muss das Modell die Wahrscheinlichkeit dafür ausgeben, dass der Punkt blau ist. Das Modell beginnt mit zufälligen Gewichten, und der Hintergrund der Punkte zeigt die Modellvorhersage für jeden Koordinatenpunkt. Wie Sie sehen, bewegt sich die Wahrscheinlichkeit aufgrund der zufälligen Gewichte für alle Punkte in der Nähe des Zentrumswerts.

Wenn Sie das Training beginnen, indem Sie auf den Pfeil links oben im Bild klicken, können Sie beobachten, wie das Modell langsam mit aufeinanderfolgenden Epochen zu lernen beginnt, wie Abbildung 4-8 zeigt.

image

Abbildung 4-8: Die Lernergebnisse des Modells bei fortschreitendem Training. Die Diagramme oben stellen den Trainingsverlust und den Validierungsfehler dar, während die Bilder zeigen, wie das Modell im jeweiligen Stadium die Farbe eines Punkts bei jeder Koordinate im Raster vorhersagen würde.

Ein erster Hinweis auf das Lernen ist in Abbildung 4-8 (b) zu sehen. Abbildung 4-8 (c) zeigt, dass das Modell eine Ansicht der Daten von höherer Ebene aus gelernt hat. Von da an passt das Modell die Grenzen an, um immer mehr blaue Punkte in den mittleren Bereich zu bringen und dabei die orangefarbenen Punkte außen zu halten. Das hilft, aber nur bis zu einem gewissen Punkt. Wenn wir dann zu Abbildung 4-8 (e) kommen, beginnt die Anpassung der Gewichte, zufällige Störungen in den Trainingsdaten widerzuspiegeln. Diese sind jedoch kontraproduktiv für den Validierungsdatensatz.

Das Training können wir also in drei Phasen aufteilen. In der ersten Phase zwischen den Stufen (a) und (c) lernt das Modell die High-Level-Organisation der Daten. In der zweiten Phase zwischen den Stufen (c) und (e) lernt das Modell die Details. Wenn wir dann mit Stufe (f) zur dritten Phase kommen, findet eine Überanpassung des Modells statt. Ein partiell trainiertes Modell vom Ende von Phase 1 oder Phase 2 hat einige Vorteile, und zwar genau deshalb, weil es die High-Level-Organisation gelernt hat, aber noch nicht in den Details gefangen ist.

Kompromisse und Alternativen

Checkpoints sollen nicht nur Resilienz bieten, sondern uns auch ermöglichen, Early Stopping und Fähigkeiten zur Feinabstimmung zu implementieren.

Early Stopping

Je länger Sie trainieren, desto geringer ist im Allgemeinen der Verlust auf dem Trainingsdatensatz. Allerdings kann es sein, dass der Fehler mit dem Validierungsdatensatz ab einem bestimmten Punkt nicht mehr abnimmt. Wenn Sie beginnen, eine Überanpassung auf dem Trainingsdatensatz durchzuführen, könnte der Validierungsfehler sogar wieder ansteigen, wie in Abbildung 4-9 gezeigt.

image

Abbildung 4-9: Typischerweise nimmt der Trainingsverlust weiter ab, je länger Sie trainieren, doch sobald Überanpassung einsetzt, wird der Validierungsfehler auf einem reservierten Datensatz nach oben gehen.

In derartigen Fällen kann es hilfreich sein, einen Blick auf den Validierungsfehler am Ende jeder Epoche zu werfen und das Training anzuhalten, wenn der Validierungsfehler größer ist als der der vorherigen Epoche. In Abbildung 4-9 ist dies am Ende der vierten Epoche der Fall, dargestellt durch eine dicke gestrichelte Linie. Man spricht hier vom Early Stopping (frühes Stoppen).

image

Würden wir am Ende jedes Batches einen Checkpoint setzen, wären wir in der Lage, das wahre Minimum zu erfassen, das vielleicht etwas vor oder nach der Epochengrenze gelegen hat. Mehr dazu lesen Sie in der Diskussion zu virtuellen Epochen in diesem Abschnitt für eine Methode, bei der Checkpoints häufiger gesetzt werden.

 

Bei häufigerem Checkpointing kann es hilfreich sein, wenn Early Stopping nicht übermäßig empfindlich auf kleine Störungen im Validierungsfehler reagiert. Stattdessen können wir Early Stopping erst anwenden, nachdem sich der Validierungsfehler für mehr als N Checkpoints nicht mehr verbessert.

Checkpoint-Auswahl.Während man Early Stopping implementieren kann, indem man das Training anhält, sobald der Validierungsfehler zunimmt, empfehlen wir, länger zu trainieren und den optimalen Lauf im Rahmen der Nachbearbeitung auszuwählen. Wir schlagen vor, bis in Phase 3 hinein zu trainieren (siehe die Erläuterungen zu den drei Phasen der Trainingsschleife im vorherigen Abschnitt »Warum es funktioniert« auf Seite 177), weil es nicht ungewöhnlich ist, dass der Validierungsfehler kurzzeitig ansteigt und dann wieder abfällt. Üblicherweise kommt das daher, dass sich das Training anfangs auf häufigere Szenarios konzentriert (Phase 1) und dann erst die selteneren Situationen in Angriff nimmt (Phase 2). Da seltene Situationen durch unvollkommene Stichproben zwischen den Trainings- und Validierungsdatensätzen entstehen können, sind gelegentliche Anstiege des Validierungsfehlers während des Trainingslaufs in Phase 2 zu erwarten. Darüber hinaus gibt es Situationen, die bei großen Modellen vorherrschend sind, wobei ein tiefer doppelter Abstieg (https://oreil.ly/Kya8h) zu erwarten ist. Deshalb ist es unerlässlich, vorsichtshalber etwas länger zu trainieren.

In unserem Beispiel werden wir das Modell nicht am Ende des Trainingslaufs exportieren, sondern den vierten Checkpoint laden und unser Modell stattdessen von dort aus exportieren. Dies wird als Checkpoint-Auswahl bezeichnet, und in Tensor-Flow lässt sich das mit BestExporter (https://oreil.ly/UpN1a) erreichen.

Regularisierung.Anstatt Early Stopping oder Checkpoint-Auswahl zu verwenden, könnten Sie versuchen, dem Modell L2-Regularisierung hinzuzufügen, sodass der Validierungsfehler nicht ansteigt und das Modell nie in Phase 3 gelangt. Vielmehr sollten sowohl der Trainingsverlust als auch der Validierungsfehler ein Plateau erreichen, wie Abbildung 4-10 zeigt. Eine derartige Trainingsschleife (bei der sowohl Trainings- als auch Validierungsmetriken ein Plateau erreichen) bezeichnen wir als gutmütige Trainingsschleife.

image

Abbildung 4-10: In der idealen Situation steigt der Validierungsfehler nicht an. Stattdessen stagnieren sowohl der Trainingsverlust als auch der Validierungsfehler.

Wenn wir kein Early Stopping durchführen und nur anhand des Trainingsverlusts über die Konvergenz entscheiden, brauchen wir keinen separaten Testdatensatz beiseitezulegen. Selbst wenn wir das Training frühzeitig beenden, kann es hilfreich sein, den Fortschritt des Modelltrainings anzuzeigen, insbesondere wenn das Training des Modells lange dauert. Zwar werden Performance und Fortschritt des Modelltrainings normalerweise für den Validierungsdatensatz während der Trainingsschleife überwacht, doch dient dies lediglich der Visualisierung. Da wir keine Maßnahmen aufgrund der angezeigten Metriken ergreifen müssen, können wir die Visualisierung auf dem Testdatensatz durchführen.

Regularisierung könnte besser sein als Early Stopping, weil Sie bei der Regularisierung den gesamten Datensatz heranziehen können, um die Gewichte des Modells zu ändern, während Sie beim Early Stopping etwa 10 bis 20 % des Datensatzes verschwenden, nur um zu entscheiden, wann das Training zu stoppen ist. Andere Methoden, die Überanpassung begrenzen (wie zum Beispiel Dropout und die Verwendung von Modellen mit geringer Komplexität), sind ebenfalls gute Alternativen zum Early Stopping. Zudem deuten neuere Forschungen (https://oreil.ly/FJ_iy) darauf hin, dass doppelter Abstieg bei verschiedenartigsten Problemen des maschinellen Lernens vorkommt und es daher besser ist, länger zu trainieren, als durch Early Stopping eine suboptimale Lösung zu riskieren.

Zwei Teilungen.Steht die Empfehlung im Abschnitt zur Regularisierung nicht im Widerspruch zu den Empfehlungen in den vorherigen Abschnitten in Bezug auf Early Stopping oder Checkpoint-Auswahl? Nicht wirklich.

Wir empfehlen, dass Sie Ihre Daten zweiteilen: in einen Trainingsdatensatz und einen Evaluierungsdatensatz. Der Evaluierungsdatensatz spielt die Rolle des Testdatensatzes beim Experimentieren (wo es keinen Validierungsdatensatz gibt) und die Rolle des Validierungsdatensatzes in der Produktion (wo es keinen Testdatensatz gibt).

Je größer Ihr Trainingsdatensatz ist, desto komplexere Modelle können Sie verwenden und desto genauere Modelle erhalten. Wenn Sie mit Regularisierung statt Early Stopping oder Checkpoint-Auswahl arbeiten, können Sie einen größeren Trainingsdatensatz verwenden. In der Experimentierphase (wenn Sie verschiedene Modellarchitekturen, Trainingstechniken und Hyperparameter untersuchen) empfehlen wir, dass Sie Early Stopping abschalten und mit größeren Modellen trainieren (siehe auch »Entwurfsmuster 11: Nützliche Überanpassung« auf Seite 165). Damit soll sichergestellt werden, dass das Modell genügend Kapazität besitzt, um die Vorhersagemuster zu lernen. Überwachen Sie während dieses Prozesses die Fehlerkonvergenz auf der Trainingsteilung. Am Ende des Experimentierens können Sie mit dem Evaluierungsdatensatz diagnostizieren, wie gut Ihr Modell auf den Daten funktioniert, die es beim Training nicht kennengelernt hat.

Wenn Sie das Modell für den Einsatz in der Produktion trainieren, müssen Sie sich darauf vorbereiten, die Ergebnisse kontinuierlich zu evaluieren und das Modell erneut zu trainieren. Aktivieren Sie Early Stopping oder Checkpoint-Auswahl und überwachen Sie die Fehlermetriken auf dem Evaluierungsdatensatz. Entscheiden Sie sich zwischen Early Stopping und Checkpoint-Auswahl abhängig davon, ob Sie die Kosten kontrollieren müssen (hier würden Sie Early Stopping wählen) oder die Modellgenauigkeit priorisieren möchten (wofür die Checkpoint-Auswahl infrage käme).

Feinabstimmung

In einer gutmütigen Trainingsschleife verhält sich der Gradientenabstieg so, dass Sie entsprechend der Mehrheit Ihrer Daten schnell in die Nähe des optimalen Fehlers gelangen und dann langsam gegen den niedrigsten Fehler konvergieren, indem Sie auf Ausnahmefälle optimieren.

Stellen Sie sich nun vor, dass Sie das Modell regelmäßig mit frischen Daten neu trainieren müssen. Normalerweise würden Sie die frischen Daten betonen und nicht die Ausnahmefälle vom letzten Monat. Oftmals ist es besser, das Training nicht vom letzten Checkpoint an fortzusetzen, sondern vom Checkpoint, der in Abbildung 4-11 durch die blaue Linie markiert wird. Dies entspricht dem Beginn von Phase 2 der Modelltrainingsphasen, wie sie der Abschnitt »Warum es funktioniert« weiter oben in diesem Kapitel auf Seite 177 beschreibt. Damit stellen Sie sicher, dass Sie über eine allgemeine Methode verfügen, die Sie dann für einige Epochen nur mit den frischen Daten abstimmen können.

Wenn Sie von dem Checkpoint aus fortfahren, den die dicke senkrechte gestrichelte Linie markiert, befinden Sie sich in der vierten Epoche, sodass die Lernrate recht niedrig ist. Die frischen Daten werden also das Modell nicht dramatisch verändern. Allerdings wird sich das Modell optimal (im Kontext des größeren Modells) auf den frischen Daten verhalten, weil Sie es auf diesem kleineren Datensatz geschärft haben. Dieses Feinabstimmung genannte Verfahren erläutert der Abschnitt »Entwurfsmuster 13: Transfer Learning« auf Seite 186.

image

Abbildung 4-11: Von einem Checkpoint aus fortfahren, bevor der Trainingsverlust ein Plateau erreicht. Trainieren Sie darauffolgende Iterationen nur mit frischen Daten.

image

Die Feinabstimmung funktioniert nur, solange Sie die Modellarchitektur nicht verändern.

Es ist nicht notwendig, immer von einem früheren Checkpoint aus zu beginnen. In manchen Fällen kann der letzte Checkpoint (den das Modell schließlich verwendet) als Warmstart für eine andere Modelltrainingsiteration verwendet werden. Dennoch bietet der Start von einem früheren Checkpoint aus tendenziell eine bessere Generalisierung.

Eine Epoche neu definieren

Tutorials zum maschinellen Lernen enthalten oft Code wie diesen:

model.fit(X_train, y_train, batch_size=100, epochs=15)

Dieser Code nimmt an, dass Ihr Datensatz in den Arbeitsspeicher passt und folglich Ihr Modell 15 Epochen durchlaufen kann, ohne einen Maschinenfehler zu riskieren. Beide Annahmen sind unvernünftig – ML-Datensätze bewegen sich im Terabyte-Bereich. Und wenn ein Training Stunden dauern kann, ist die Wahrscheinlichkeit für einen Maschinenfehler hoch.

Um den obigen Code noch robuster zu machen, stellen Sie einen TensorFlow-Datensatz (https://oreil.ly/EKJ4V) – und nicht einfach ein NumPy-Array – bereit, weil der TensorFlow-Datensatz ein Out-of-Memory-Datensatz ist. Er bietet Iterationsfähigkeit und Lazy Loading. Der Code sieht nun folgendermaßen aus:

cp_callback = tf.keras.callbacks.ModelCheckpoint(...)

history = model.fit(trainds,

validation_data=evalds,

epochs=15,

batch_size=128,

callbacks=[cp_callback])

Allerdings bleibt es eine schlechte Idee, Epochen auf großen Datensätzen zu verwenden. Epochen mögen einfach zu verstehen sein, doch die Verwendung von Epochen führt in praktischen ML-Modellen zu schlechten Effekten. Um den Grund dafür zu sehen, stellen Sie sich einen Trainingsdatensatz mit einer Million Beispielen vor. Es kann verlockend sein, diesen Datensatz (zum Beispiel) einfach 15-mal zu durchlaufen, indem man die Anzahl der Epochen auf 15 setzt. Dabei gibt es mehrere Probleme:

Schritte pro Epoche.Anstatt für 15 Epochen zu trainieren, entscheiden Sie vielleicht, 143.000 Schritte zu trainieren, wobei batch_size gleich 100 ist:

NUM_STEPS = 143000

BATCH_SIZE = 100

NUM_CHECKPOINTS = 15

cp_callback = tf.keras.callbacks.ModelCheckpoint(...)

history = model.fit(trainds,

validation_data=evalds,

epochs=NUM_CHECKPOINTS,

steps_per_epoch=NUM_STEPS // NUM_CHECKPOINTS,

batch_size=BATCH_SIZE,

callbacks=[cp_callback])

In jedem Schritt sind die Gewichte basierend auf einem einzelnen Mini-Batch von Daten zu aktualisieren. Dies ermöglicht uns, bei 14,3 Epochen anzuhalten. Das beschert uns mehr Granularität, doch wir müssen eine »Epoche« als 1/15 der Gesamtzahl der Schritte definieren:

steps_per_epoch=NUM_STEPS // NUM_CHECKPOINTS,

Damit bekommen wir die richtige Anzahl von Checkpoints. Das funktioniert, solange wir gewährleisten, dass die Eingabepipeline trainds unendlich oft wiederholt wird:

trainds = trainds.repeat()

Die Methode repeat() ist erforderlich, weil wir num_epochs nicht mehr festlegen, sodass für die Anzahl der Epochen der Standardwert 1 gilt. Ohne repeat() verlässt das Modell die Trainingsschleife, sobald die Trainingsmuster nach einmaligem Lesen des Datensatzes erschöpft sind.

Erneutes Training mit mehr Daten.Was passiert, wenn wir weitere 100.000 Beispiele erhalten? Ganz einfach! Wir fügen Sie unserem Date Warehouse hinzu, aktualisieren aber nicht den Code. Unser Code wird immer noch 143.000 Schritte verarbeiten wollen und wird auch so viele Daten verarbeiten, nur dass die 10 % der Beispiele, die er sieht, neuer sind. Wenn das Modell konvergiert, großartig. Andernfalls wissen wir, dass diese neuen Datenpunkte das Problem sind, weil wir nicht länger trainieren als vorher. Indem wir die Anzahl der Schritte konstant halten, sind wir in der Lage, die Auswirkungen der neuen Daten vom Training auf mehr Daten zu trennen.

Sobald wir 143.000 Schritte trainiert haben, starten wir das Training neu und lassen es etwas länger laufen (sagen wir 10.000 Schritte). Und solange das Modell konvergiert, verlängern wir das Training weiter. Dann aktualisieren wir die Zahl 143.000 im obigen Code (in der Praxis wird das ein Parameter für den Code sein), um die neue Anzahl von Schritten widerzuspiegeln.

Das funktioniert gut, bis Sie die Hyperparameter optimieren möchten. Bei einer Feinabstimmung der Hyperparameter werden Sie auch die Batch-Größe ändern wollen. Wenn Sie aber die Batch-Größe auf 50 setzen, werden Sie leider feststellen, dass Sie nur noch halb so lange trainieren. Denn wir trainieren für 143.000 Schritte, und jeder Schritt ist nur noch halb so lang wie vorher. Offenbar ist das nicht gut.

Virtuelle Epochen.Die Lösung besteht darin, die Gesamtanzahl der Trainingsbeispiele, die dem Modell gezeigt werden (nicht die Anzahl der Schritte, siehe Abbildung 4-12), konstant zu halten:

NUM_TRAINING_EXAMPLES = 1000 * 1000

STOP_POINT = 14.3

TOTAL_TRAINING_EXAMPLES = int(STOP_POINT * NUM_TRAINING_EXAMPLES)

BATCH_SIZE = 100

NUM_CHECKPOINTS = 15

steps_per_epoch = (TOTAL_TRAINING_EXAMPLES //

(BATCH_SIZE*NUM_CHECKPOINTS))

cp_callback = tf.keras.callbacks.ModelCheckpoint(...)

history = model.fit(trainds,

validation_data=evalds,

epochs=NUM_CHECKPOINTS,

steps_per_epoch=steps_per_epoch,

batch_size=BATCH_SIZE,

callbacks=[cp_callback])

image

Abbildung 4-12: Eine virtuelle Epoche in Form der gewünschten Anzahl von Schritten zwischen Checkpoints definieren

Wenn Sie mehr Daten erhalten, trainieren Sie zuerst mit den alten Einstellungen, erhöhen dann die Anzahl der Beispiele entsprechend den neuen Daten und ändern schließlich den STOP_POINT, um widerzuspiegeln, wie oft Sie die Daten durchlaufen müssen, bis Konvergenz erreicht ist.

Diese Vorgehensweise ist nun selbst mit Hyperparameter-Feinabstimmung (siehe weiter unten in diesem Kapitel) sicher und bewahrt alle Vorteile, die Anzahl der Schritte konstant zu halten.

Entwurfsmuster 13: Transfer Learning

Beim Transfer Learning nehmen wir einen Teil eines zuvor trainierten Modells, frieren die Gewichte ein und binden diese nicht trainierbaren Schichten in ein neues Modell ein, das ein ähnliches Problem löst, allerdings auf einem kleineren Datensatz.

Problem

Das Training von benutzerdefinierten ML-Modellen auf unstrukturierten Daten erfordert äußerst große Datensätze, die nicht immer ohne Weiteres verfügbar sind. Nehmen wir ein Modell, das erkennen soll, ob das Röntgenbild eines Arms einen Knochenbruch enthält. Um hohe Genauigkeit zu erreichen, benötigen Sie Hunderttausende von Bildern, wenn nicht mehr. Bevor Ihr Modell lernt, wie ein gebrochener Knochen aussieht, muss es zuerst lernen, die Pixel, Kanten und Formen zu erkennen, die Teil der Bilder in Ihrem Datensatz sind. Das Gleiche gilt für Modelle, die auf Textdaten trainiert werden. Angenommen, wir erstellten ein Modell, das Beschreibungen von Patientensymptomen aufnimmt und die möglichen Bedingungen vorhersagt, die mit diesen Symptomen verbunden sind. Nun muss das Modell nicht nur lernen, eine Erkältung von einer Lungenentzündung zu unterscheiden, sondern auch die grundlegende Sprachsemantik und wie sich aus der Abfolge der Wörter eine Bedeutung ergibt. Zum Beispiel müsste das Modell nicht nur lernen, das Vorhandensein des Worts Fieber zu erkennen, sondern auch, dass die Sequenz kein Fieber eine andere Bedeutung hat als hohes Fieber.

Um zu sehen, wie viele Daten erforderlich sind, um hochgenaue Modelle zu trainieren, können wir uns ImageNet (https://oreil.ly/t6583) ansehen, eine Datenbank mit über 14 Millionen gelabelten Bildern. ImageNet wird häufig als Benchmark verwendet, um Frameworks für maschinelles Lernen auf unterschiedlicher Hardware zu evaluieren. Zum Beispiel greift die Benchmark-Suite MLPerf (https://oreil.ly/hDPiJ) auf ImageNet zurück, um die Zeiten zu vergleichen, die verschiedene ML-Frameworks auf unterschiedlicher Hardware benötigt haben, um eine Klassifizierungsgenauigkeit von 75,9 % zu erreichen. In den Ergebnissen von MLPerf v0.7 Training benötigte ein TensorFlow-Modell, das auf einer TPU v3 von Google lief, ungefähr 30 Sekunden, um diese Zielgenauigkeit zu erreichen.2 Mit mehr Trainingszeit können Modelle die Genauigkeit auf ImageNet noch steigern. Allerdings ist dies hauptsächlich der Größe von ImageNet zu verdanken. Die meisten Organisationen mit spezialisierten Vorhersageproblemen haben nicht annähernd so viele Daten zur Verfügung.

Da Anwendungsfälle wie die oben beschriebenen Bild- und Textbeispiele mit besonders spezialisierten Datendomänen zu tun haben, ist es auch nicht möglich, ein Standardmodell zu verwenden, um erfolgreich Knochenbrüche zu erkennen oder Krankheiten zu diagnostizieren. Ein Modell, das auf ImageNet trainiert wird, ist vielleicht in der Lage, ein Röntgenbild als Röntgenaufnahme oder medizinische Bildgebung zu labeln, aber es ist unwahrscheinlich, dass es das Bild auch als gebrochenen Oberschenkelknochen labelt. Da solche Modelle oft auf einer breiten Palette von High-Level-Label-Kategorien trainiert werden, würden wir nicht erwarten, dass sie in den Bildern die Bedingungen verstehen, die für unseren Datensatz spezifisch sind. Um dies in den Griff zu bekommen, brauchen wir eine Lösung, die es uns erlaubt, ein benutzerdefiniertes Modell zu erstellen und dabei nur die Daten zu verwenden, die wir zur Verfügung haben, und die Labels, die für uns relevant sind.

Lösung

Mit dem Entwurfsmuster Transfer Learning können wir ein Modell, das auf der gleichen Art von Daten für eine ähnliche Aufgabe trainiert wurde, auf eine spezialisierte Aufgabe mit unseren eigenen benutzerdefinierten Daten anwenden. Mit »gleiche Art von Daten« ist Datenmodalität gemeint – Bilder, Text usw. Über eine breite Kategorie wie Bilder hinaus ist es ebenfalls ideal, ein Modell zu verwenden, das auf die gleichen Bildtypen vortrainiert wurde. Verwenden Sie zum Beispiel ein Modell, das auf Fotografien vortrainiert wurde, wenn Sie es für die Klassifizierung von Fotos einsetzen möchten, und ein Modell, das auf Fernerkundungsaufnahmen vortrainiert wurde, wenn Sie damit Satellitenbilder klassifizieren wollen. Mit »ähnliche Aufgabe« beziehen wir uns auf das zu lösende Problem. Um zum Beispiel Transfer Learning für die Bildklassifizierung auszuführen, ist es besser, mit einem Modell zu beginnen, das für Bildklassifizierung und nicht für Objekterkennung trainiert wurde.

Um beim Beispiel zu bleiben: Angenommen, wir erstellten einen binären Klassifizierer, um zu bestimmen, ob ein Röntgenbild einen Knochenbruch darstellt. Wir haben nur 200 Bilder von jeder Klasse: gebrochen und nicht gebrochen. Dies ist zwar nicht genug, um ein hochwertiges Modell von Grund auf zu trainieren, aber es reicht für Transfer Learning aus. Um dieses Problem mit Transfer Learning zu lösen, müssen wir ein Modell finden, das bereits auf einem großen Datensatz für Bildklassifizierung trainiert wurde. Dann entfernen wir die letzte Schicht aus diesem Modell, frieren die Gewichte des Modells ein und setzen das Training mit unseren 400 Röntgenbildern fort. Im Idealfall finden wir ein Modell, das auf einem Datensatz mit ähnlichen Bildern wie unsere Röntgenaufnahmen trainiert worden ist, beispielsweise mit Bildern, die in einem Labor oder unter anderen kontrollierten Bedingungen aufgenommen wurden. Allerdings können wir Transfer Learning auch dann verwenden, wenn die Datensätze unterschiedlich sind, solange die Vorhersageaufgabe die gleiche ist. In diesem Fall handelt es sich um eine Bildklassifizierung.

Transfer Learning können Sie neben der Bildklassifizierung für viele andere Vorhersageaufgaben einsetzen, solange es ein bereits trainiertes Modell gibt, das zu der Aufgabe passt, die Sie mit Ihrem Datensatz durchführen möchten. Zum Beispiel wird Transfer Learning häufig für Objekterkennung, Bildstilübertragung, Bilderzeugung, Textklassifizierung, maschinelle Übersetzung und mehr eingesetzt.

image

Transfer Learning funktioniert, weil es uns erlaubt, auf den Schultern von Giganten zu stehen und Modelle zu verwenden, die bereits auf extrem großen, gelabelten Datensätzen trainiert wurden. Dass wir Transfer Learning verwenden können, ist jahrelanger Forschung und Arbeit zu verdanken, die andere in das Erstellen dieser Datensätze für uns gesteckt haben. Gleichzeitig hat dies den Stand der Technik beim Transfer Learning vorangebracht. Ein Beispiel für einen solchen Datensatz ist das Projekt ImageNet, das 2006 von Fei-Fei-Li gestartet und 2009 veröffentlicht wurde. ImageNet3 war wesentlich für die Entwicklung des Transfer Learning und ebnete den Weg für andere große Datensätze wie COCO (https://oreil.ly/mXt77) und Open Images (https://oreil.ly/QN9KU).

Hinter Transfer Learning steht die Idee, die Gewichte und Schichten von einem Modell zu übernehmen, das in derselben Domäne wie Ihre Vorhersageaufgabe trainiert wurde. In den meisten Deep-Learning-Modellen enthält die letzte Schicht die Klassifizierungs-Labels oder die Ausgaben, die für Ihre Vorhersageaufgabe spezifisch sind. Beim Transfer Learning entfernen wir diese Schicht, frieren die trainierten Gewichte des Modells ein und ersetzen die letzte Schicht durch die Ausgabe für unsere spezialisierte Vorhersageaufgabe, bevor wir das Training fortsetzen. Wie das funktioniert, zeigt Abbildung 4-13.

Die vorletzte Schicht des Modells (die Schicht, die vor der Ausgabeschicht des Modells liegt) wird typischerweise als Flaschenhalsschicht eingerichtet. Im nächsten Abschnitt erläutern wir die Flaschenhalsschicht zusammen mit verschiedenen Möglichkeiten, Transfer Learning in TensorFlow zu implementieren.

image

Abbildung 4-13: Beim Transfer Learning wird ein Modell auf einem großen Datensatz trainiert. Die »Spitze« des Modells (typischerweise nur die Ausgabeschicht) wird entfernt, und die Gewichte der übrigen Schichten werden eingefroren. Die letzte Schicht des verbleibenden Modells ist die sogenannte Flaschenhalsschicht.

Flaschenhalsschicht

In Bezug auf ein gesamtes Modell repräsentiert die Flaschenhalsschicht die Eingabe (typischerweise ein Bild- oder Textdokument) im Raum mit der niedrigsten Dimensionalität. Konkreter ausgedrückt: Wenn wir Daten in unser Modell einspeisen, sehen die ersten Schichten diese Daten nahezu in ihrer Originalform. Um zu sehen, wie das funktioniert, fahren wir mit einem Beispiel aus der medizinischen Bildgebung fort, aber dieses Mal erstellen wir ein Modell (https://oreil.ly/QfOU_) mit einem kolorektalen Histologiedatensatz (https://oreil.ly/r4HHq), um die Histologiebilder in eine von acht Kategorien zu klassifizieren.

Um das Modell zu untersuchen, das wir für das Transfer Learning verwenden werden, laden wir die VGG-Modellarchitektur herunter, die auf dem ImageNet-Datensatz vortrainiert wurde.

vgg_model_withtop = tf.keras.applications.VGG19(

include_top=True,

weights='imagenet',

)

Die Einstellung include_top=True bedeutet, dass wir das vollständige VGG-Modell laden, einschließlich der Ausgabeschicht. Bei ImageNet klassifiziert das Modell Bilder in 1.000 verschiedene Klassen, sodass die Ausgabeschicht ein 1.000-elementiges Array ist. Sehen wir uns die Ausgabe von model.summary() an, um zu verstehen, welche Schicht als Flaschenhals verwendet wird. Aus Platzgründen haben wir hier einige der mittleren Schichten weggelassen:

Model: "vgg19"

_________________________________________________________________

Layer (type) Output Shape Param #

=================================================================

input_3 (InputLayer) [(None, 224, 224, 3)] 0

_________________________________________________________________

block1_conv1 (Conv2D) (None, 224, 224, 64) 1792

...hier weitere Schichten...

_________________________________________________________________

block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808

_________________________________________________________________

block5_conv4 (Conv2D) (None, 14, 14, 512) 2359808

_________________________________________________________________

block5_pool (MaxPooling2D) (None, 7, 7, 512) 0

_________________________________________________________________

flatten (Flatten) (None, 25088) 0

_________________________________________________________________

fc1 (Dense) (None, 4096) 102764544

_________________________________________________________________

fc2 (Dense) (None, 4096) 16781312

_________________________________________________________________

predictions (Dense) (None, 1000) 4097000

=================================================================

Total params: 143,667,240

Trainable params: 143,667,240

Non-trainable params: 0

_________________________________________________________________

Wie Sie sehen, akzeptiert das VGG-Modell Bilder als Array von 224 × 224 × 3 Pixeln. Dieses 128-elementige Array wird dann über aufeinanderfolgende Schichten (von denen jede die Dimensionalität des Arrays ändern kann) geleitet, bis es in der Schicht flatten zu einem 25.088 × 1-dimensionalen Array abgeflacht wird. Schließlich wird es in die Ausgabeschicht eingespeist, die ein 1.000-elementiges Array (für jede Klasse in ImageNet) zurückgibt. In diesem Beispiel wählen wir die Schicht block5_pool als Flaschenhalsschicht aus, wenn wir dieses Modell anpassen, um es auf unseren medizinischen Histologiebildern zu trainieren. Die Flaschenhalsschicht produziert ein 7 × 7 × 512-dimensionales Array, das das Eingabebild mit weniger Dimensionen darstellt. Es behält noch genügend Informationen aus dem Eingabebild bei, um es klassifizieren zu können. Wenn wir dieses Modell auf unsere Klassifizierungsaufgabe für medizinische Bilder anwenden, hoffen wir, dass die Informationsdestillation ausreicht, um unseren Datensatz erfolgreich zu klassifizieren.

Der Histologiedatensatz enthält Bilder als (150, 150, 3)-dimensionale Arrays. Diese 150 × 150 × 3-Darstellung ist die höchste Dimensionalität. Um das VGG-Modell mit unseren Bilddaten zu verwenden, können wir es mit dem folgenden Code laden:

vgg_model = tf.keras.applications.VGG19(

include_top=False,

weights='imagenet',

input_shape=((150,150,3))

)

vgg_model.trainable = False

Mit der Einstellung include_top=False legen wir fest, dass die letzte Schicht von VGG als Flaschenhalsschicht geladen wird. Der Parameter input_shape, den wir übergeben haben, entspricht der Eingabeform unserer Histologiebilder. Eine Zusammenfassung der letzten Schichten dieses aktualisierten VGG-Modells sieht folgendermaßen aus:

block5_conv3 (Conv2D) (None, 9, 9, 512) 2359808

_________________________________________________________________

block5_conv4 (Conv2D) (None, 9, 9, 512) 2359808

_________________________________________________________________

block5_pool (MaxPooling2D) (None, 4, 4, 512) 0

=================================================================

Total params: 20,024,384

Trainable params: 0

Non-trainable params: 20,024,384

_________________________________________________________________

Die letzte Schicht ist nun unsere Flaschenhalsschicht. Wie Sie sicherlich bemerkt haben, ist die Größe von block5_pool jetzt (4, 4, 512), während sie zuvor (7, 7, 512) war. Das liegt daran, dass wir VGG mit einem Parameter input_shape instanziiert haben, um der Größe der Bilder in unserem Datensatz zu entsprechen. Zu erwähnen ist auch, dass die Einstellung include_top=False hartcodiert ist, um block5_pool als Flaschenhalsschicht zu verwenden. Wenn Sie dies aber anpassen möchten, können Sie das vollständige Modell laden und alle zusätzlichen Schichten löschen, die Sie nicht verwenden wollen.

Bevor Sie dieses Modell jedoch trainieren können, müssen Sie noch einige Schichten darübersetzen, die für Ihre Daten und die Klassifizierungsaufgabe spezifisch sind. Außerdem ist es wichtig, dass es 0 trainierbare Parameter im aktuellen Modell gibt, da wir trainable=False gesetzt haben.

image

Als allgemeine Faustregel gilt, dass die Eingabeschicht typischerweise die letzte, am wenigsten dimensionale, abgeflachte Schicht vor einer Abflachungsoperation ist.

Da beide die Features in reduzierter Dimensionalität darstellen, sind Flaschenhalsschichten konzeptionell mit Einbettungen vergleichbar. Zum Beispiel ist die Flaschenhalsschicht in einem Autoencoder-Modell mit einer Encoder-Decoder-Architektur eine Einbettung. In diesem Fall dient der Flaschenhals als mittlere Schicht des Modells, die die ursprünglichen Eingabedaten auf eine Darstellung mit niedrigerer Dimensionalität abbildet. Diese verwendet dann der Decoder (die zweite Hälfte des Netzes), um die Eingabe zurück auf ihre ursprüngliche höherdimensionale Darstellung abzubilden. Ein Schema für die Flaschenhalsschicht in einem Autoencoder zeigt Abbildung 2-13 in Kapitel 2.

Eine Einbettungsschicht ist im Wesentlichen eine Nachschlagetabelle mit Gewichten, die ein bestimmtes Feature auf eine Dimension im Vektorraum abbildet. Der Hauptunterschied besteht darin, dass die Gewichte in einer Einbettungsschicht trainiert werden können, während die Gewichte aller Schichten bis zur und einschließlich der Flaschenhalsschicht eingefroren sind. Mit anderen Worten, das gesamte Netz bis zur und einschließlich der Flaschenhalsschicht ist nicht trainierbar, und die Gewichte in den Schichten nach dem Flaschenhals sind die einzigen trainierbaren Schichten im Modell.

image

Erwähnenswert ist auch, dass die vortrainierten Einbettungen im Entwurfsmuster Transfer Learning verwendet werden können. Wenn Sie ein Modell erstellen, das eine Einbettungsschicht umfasst, können Sie entweder eine vorhandene (vortrainierte) Einbettungssuche nutzen oder Ihre eigene Einbettungsschicht von Grund auf neu trainieren.

Alles in allem ist Transfer Learning eine Lösung, die Sie nutzen können, um ein ähnliches Problem auf einem kleineren Datensatz zu lösen. Transfer Learning bedient sich immer einer Flaschenhalsschicht mit nicht trainierbaren, eingefrorenen Gewichten. Einbettungen sind eine Art der Datenrepräsentation. Letztendlich kommt es auf den Zweck an. Besteht der Zweck darin, ein ähnliches Modell zu trainieren, würden Sie Transfer Learning verwenden. Möchten Sie ein Eingabebild prägnanter darstellen, würden Sie folglich eine Einbettung nutzen. Der Code könnte genau der gleiche sein.

Transfer Learning implementieren

Transfer Learning können Sie in Keras nach einer der beiden folgenden Methoden implementieren:

Sehen wir uns zuerst an, wie Sie ein vortrainiertes Modell laden und selbst verwenden. Hierfür bauen wir auf dem weiter oben eingeführten Beispiel mit dem VGG-Modell auf. Beachten Sie, dass VGG eine Modellarchitektur ist, während ImageNet die Daten liefert, auf denen VGG trainiert wurde. Gemeinsam machen sie das vortrainierte Modell aus, das wir für das Transfer Learning verwenden. Per Transfer Learning wollen wir hier Bilder aus der kolorektalen Histologie klassifizieren. Während der ursprüngliche Datensatz 1.000 Labels enthält, wird unser resultierendes Modell nur acht von uns festgelegte Klassen zurückgeben im Gegensatz zu den Tausenden von Labels in ImageNet.

image

Ein vortrainiertes Modell zu laden, um damit die ursprünglichen Labels zu klassifizieren, auf denen das Modell trainiert wurde, ist kein Transfer Learning. Vielmehr bedeutet Transfer Learning, einen Schritt weiterzugehen und die letzten Schichten des Modells durch Ihre eigene Vorhersageaufgabe zu ersetzen.

Das VGG-Modell, das wir geladen haben, wird unser Basismodell sein. Wir müssen einige Schichten hinzufügen, um die Ausgabe unserer Flaschenhalsschicht abzuflachen, und diese abgeflachte Ausgabe in ein 8-elementiges Softmax-Array einspeisen:

global_avg_layer = tf.keras.layers.GlobalAveragePooling2D()

feature_batch_avg = global_avg_layer(feature_batch)

prediction_layer = tf.keras.layers.Dense(8, activation='softmax')

prediction_batch = prediction_layer(feature_batch_avg)

Schließlich können wir über die Sequential-API unser neues Transfer-Learning-Modell als Stapel von Schichten erstellen:

histology_model = keras.Sequential([

vgg_model,

global_avg_layer,

prediction_layer

])

Sehen Sie sich die Ausgabe von model.summary() für unser Transfer-Learning-Modell an:

_________________________________________________________________

Layer (type) Output Shape Param #

=================================================================

vgg19 (Model) (None, 4, 4, 512) 20024384

_________________________________________________________________

global_average_pooling2d (Gl (None, 512) 0

_________________________________________________________________

dense (Dense) (None, 8) 4104

=================================================================

Total params: 20,028,488

Trainable params: 4,104

Non-trainable params: 20,024,384

_________________________________________________________________

Wichtig ist hier, dass die einzigen trainierbaren Parameter nach unserer Flaschenhalsschicht kommen. In diesem Beispiel wird die Flaschenhalsschicht durch die Feature-Vektoren aus dem VGG-Modell gebildet. Nachdem Sie dieses Modell kompiliert haben, können Sie es mit dem Datensatz der Histologiebilder trainieren.

Vortrainierte Einbettungen

So wie wir in der Lage sind, ein vortrainiertes Modell in eigener Regie zu laden, können wir auch Transfer Learning implementieren, indem wir die vielen vortrainierten Modelle von TF Hub, einer Bibliothek von vortrainierten Modellen, nutzen. Diese sogenannten Module decken ein breites Spektrum von Datendomänen und Anwendungsfällen ab, einschließlich Klassifizierung, Objekterkennung, maschineller Übersetzung und mehr. In TensorFlow können Sie diese Module als Schicht laden und dann Ihre eigene Klassifizierungsschicht darüberlegen.

Um zu sehen, wie TF Hub funktioniert, erstellen wir ein Modell, das Filmkritiken entweder als positiv oder als negativ klassifiziert. Zuerst laden wir ein vortrainiertes Einbettungsmodell, das auf einem großen Korpus von Nachrichtenartikeln trainiert wurde. Dieses Modell können wir als hub.KerasLayer instanziieren:

hub_layer = hub.KerasLayer(

"https://tfhub.dev/google/tf2-preview/gnews-swivel-20dim/1",

input_shape=[], dtype=tf.string, trainable=True)

Um unseren Klassifizierer zu erstellen, können wir weitere Schichten darauf stapeln:

model = keras.Sequential([

hub_layer,

keras.layers.Dense(32, activation='relu'),

keras.layers.Dense(1, activation='sigmoid')

])

Nun können wir dieses Modell trainieren, indem wir unseren eigenen Textdatensatz als Eingabe übergeben. Die resultierende Vorhersage ist ein 1-elementiges Array, das anzeigt, ob unser Modell den gegebenen Text als positiv oder negativ einschätzt.

Warum es funktioniert

Um zu verstehen, warum Transfer Learning funktioniert, sehen wir uns zunächst eine Analogie an. Wenn Kinder ihre erste Sprache lernen, werden sie mit vielen Beispielen konfrontiert und korrigiert, sollten sie etwas falsch identifizieren. Wenn sie zum Beispiel zum ersten Mal lernen, eine Katze zu identifizieren, sehen sie, wie ihre Eltern auf die Katze zeigen und das Wort »Katze« sagen. Und durch Wiederholungen festigen sich die Bahnen in ihrem Gehirn. Sagen sie aber, es sei eine Katze, wenn sie sich auf ein Tier beziehen, das keine Katze ist, werden sie dementsprechend korrigiert. Lernt das Kind dann, wie ein Hund zu identifizieren ist, muss es nicht bei null anfangen. Es kann einen ähnlichen Erkennungsprozess wie bei der Katze verwenden, ihn aber auf eine etwas andere Aufgabe anwenden. Auf dieses Weise hat das Kind eine Grundlage für das Lernen geschaffen. Es hat nicht nur neue Dinge gelernt, sondern auch, wie es neue Dinge lernen kann. Wendet man diese Lernmethoden auf verschiedene Domänen an, ist das etwa vergleichbar damit, wie Transfer Learning funktioniert.

Wie läuft dies nun in neuronalen Netzen ab? In einem typischen Convolutional Neural Network (CNN) geschieht das Lernen hierarchisch. Die ersten Schichten lernen, die in einem Bild vorhandenen Ränder und Formen zu erkennen. Im Katzenbeispiel könnte das bedeuten, dass das Modell Bereiche in einem Bild identifizieren kann, in denen der Rand des Katzenkörpers auf den Hintergrund trifft. Die nächsten Schichten im Modell beginnen, Gruppen von Kanten zu verstehen – vielleicht gibt es zwei Kanten, die sich in der linken oberen Ecke des Bilds treffen. Die letzten Schichten eines CNN können dann diese Gruppen von Kanten zusammensetzen und ein Verständnis für verschiedene Features im Bild entwickeln. Im Katzenbeispiel könnte das Modell in der Lage sein, zwei dreieckige Formen im oberen Teil des Bilds und zwei ovale Formen darunter zu erkennen. Als Menschen wissen wir, dass diese Dreiecksformen die Ohren und die ovalen Formen die Augen sind.

Abbildung 4-14 visualisiert diesen Prozess mit einer Darstellung aus der Forschung von Zeiler und Fergus (https://oreil.ly/VzRV_) zur Dekonstruktion von CNNs, um die verschiedenen Features zu verstehen, die in jeder Schicht des Modells aktiviert wurden. Für jede Schicht in einem 5-schichtigen CNN zeigt die Abbildung die Feature-Map eines Bilds für eine bestimmte Schicht neben dem eigentlichen Bild. So können wir sehen, wie die Wahrnehmung eines Bilds durch das Modell voranschreitet, während es sich durch das Netz bewegt. Die Schichten 1 und 2 erkennen nur Kanten, Schicht 3 beginnt, Objekte herauszustellen, und die Schichten 4 und 5 können Schwerpunkte im gesamten Bild verstehen.

Für unser Modell sind das aber einfach nur Gruppierungen von Pixelwerten. Es weiß nicht, dass es sich bei den dreieckigen und ovalen Formen um Ohren und Augen handelt – es weiß nur, dass es spezifische Gruppierungen von Features mit den Labels, auf denen es trainiert wurde, assoziieren kann. Auf diese Weise unterscheidet sich der Lernprozess des Modells, welche Gruppen von Features eine Katze ausmachen, nicht wesentlich von dem Lernen der Gruppen von Features, die zu anderen Objekten gehören, beispielsweise einem Tisch, einem Berg oder sogar einem Prominenten. Für ein Modell sind dies alles nur verschiedene Kombinationen von Pixelwerten, Kanten und Formen.

image

Abbildung 4-14: Die Forschung von Zeiler und Fergus (2013) zur Dekonstruktion von CNNs hilft uns, zu visualisieren, wie ein CNN Bilder auf jeder Schicht des Netzes sieht.

Kompromisse und Alternativen

Bisher sind wir noch nicht auf Methoden eingegangen, die die Gewichte unseres ursprünglichen Modells ändern, wenn wir Transfer Learning implementieren. Hier untersuchen wir zwei Ansätze dafür: Feature-Extraktion und Feinabstimmung. Außerdem erörtern wir, warum sich Transfer Learning hauptsächlich auf Bild- und Textmodelle konzentriert, und werfen einen Blick auf die Beziehung zwischen Testsatzeinbettungen und Transfer Learning.

Feinabstimmung gegenüber Feature-Extraktion

Die Feature-Extraktion beschreibt einen Ansatz für Transfer Learning, bei dem Sie die Gewichte aller Schichten vor der Flaschenhalsschicht einfrieren und die folgenden Schichten auf Ihren eigenen Daten und Labels trainieren. Alternativ können Sie stattdessen auch die Gewichte der Schichten des vortrainierten Modells fein abstimmen. Per Feinabstimmung können Sie entweder die Gewichte jeder Schicht im vortrainierten Modell aktualisieren oder nur einige der Schichten unmittelbar vor dem Flaschenhals. Ein Transfer-Learning-Modell mithilfe von Feinabstimmung zu trainieren, dauert gewöhnlich länger als eine Feature-Extraktion. In unserem Textklassifizierungsbeispiel weiter oben ist Ihnen sicherlich aufgefallen, dass wir trainable=True gesetzt haben, als wir unsere TF-Hub-Schicht initialisierten. Dies ist ein Beispiel für Feinabstimmung.

Bei der Feinabstimmung ist es üblich, die Gewichte der anfänglichen Schichten des Modells eingefroren zu lassen, da diese Schichten daraufhin trainiert wurden, grundlegende Features zu erkennen, die oft für viele Arten von Bildern gleich sind. Um ein MobileNet-Modell fein abzustimmen, würden wir beispielsweise trainable=False nur für eine Teilmenge der Schichten im Modell setzen, anstatt jede Schicht trainierbar zu machen. Um zum Beispiel nach der 1.000sten Schicht fein abzustimmen, könnten wir folgenden Code ausführen:

base_model = tf.keras.applications.MobileNetV2(input_shape=(160,160,3),

include_top=False,

weights='imagenet')

for layer in base_model.layers[:100]:

layer.trainable = False

Um zu bestimmen, wie viele Schichten einzufrieren sind, wird die sogenannte progressive Feinabstimmung (https://oreil.ly/fAv1S) empfohlen. Bei diesem Ansatz gibt man eingefrorene Schichten iterativ nach jedem Trainingslauf wieder frei, um die ideale Anzahl von Schichten für eine Feinabstimmung zu ermitteln. Dies funktioniert am besten und ist am effizientesten, wenn Sie die Lernrate niedrig (0,001 ist üblich) und die Anzahl der Trainingsiterationen relativ klein halten. Um progressive Feinabstimmung zu implementieren, geben Sie zunächst nur die letzte Schicht Ihres transferierten Modells frei (die Schicht, die am nächsten zur Ausgabe liegt) und berechnen den Verlust des Modells nach dem Training. Dann geben Sie weitere Schichten (eine nach der anderen) frei, bis Sie die Eingabeschicht erreichen oder bis der Verlust auf einem Plateau verbleibt. Dies verwenden Sie, um die Anzahl der Schichten für die Feinabstimmung festzulegen.

Wie sollten Sie feststellen, ob Sie eine Feinabstimmung vornehmen oder alle Schichten Ihres vortrainierten Modells einfrieren? Bei einem kleinen Datensatz ist es in der Regel am besten, das vortrainierte Modell als Feature-Extraktor zu verwenden, anstatt eine Feinabstimmung vorzunehmen. Wenn Sie die Gewichte eines Modells neu trainieren, das wahrscheinlich auf Tausenden oder Millionen von Beispielen trainiert wurde, kann eine Feinabstimmung dazu führen, dass das aktualisierte Modell an Ihren kleinen Datensatz überangepasst wird und die verallgemeinerten Informationen verloren gehen, die von diesen Millionen Beispielen gelernt wurden. Es hängt zwar von Ihren Daten und der Vorhersageaufgabe ab, doch wenn wir hier »kleiner Datensatz« sagen, meinen wir Datensätze mit Hunderten oder ein paar Tausend Trainingsbeispielen.

Bei der Entscheidung für eine Feinabstimmung ist als weiterer Faktor zu berücksichtigen, wie stark sich Ihre Vorhersageaufgabe der Aufgabe des verwendeten vortrainierten Modells ähnelt. Ist die Vorhersageaufgabe ähnlich oder eine Fortsetzung des vorherigen Trainings, wie es in unserem Modell für die Stimmungsanalyse von Filmrezensionen der Fall war, kann Feinabstimmung zu genaueren Ergebnissen führen. Weicht die Aufgabe ab oder unterscheiden sich die Datensätze signifikant, ist es am besten, alle Schichten des vortrainierten Modells einzufrieren, anstatt sie fein abzustimmen. Tabelle 4-1 fasst die wichtigsten Punkte zusammen.4

Tabelle 4-1: Kriterien für die Entscheidung zwischen Feature-Extraktion und Feinabstimmung

Kriterium

Feature-Extraktion

Feinabstimmung

Wie groß ist der Datensatz?

klein

groß

Ist unsere Vorhersageaufgabe die gleiche wie die des vortrainierten Modells?

unterschiedliche Aufgaben

gleiche Aufgabe oder ähnliche Aufgabe mit der gleichen Klassenverteilung von Labels

Budget für Trainingszeit und Rechenkosten?

niedrig

hoch

In unserem Textbeispiel wurde das vortrainierte Modell auf einem Korpus aus Nachrichtentexten trainiert, unser Anwendungsfall hat aber mit Stimmungsanalyse zu tun. Da diese Aufgaben unterschiedlich sind, sollten wir das ursprüngliche Modell als Feature-Extraktor verwenden, anstatt es fein abzustimmen. Ein Beispiel für verschiedene Vorhersageaufgaben in einer Bilddomäne könnte die Verwendung unseres MobileNet-Modells sein, das auf ImageNet trainiert wurde und als Ausgangspunkt für Transfer Learning auf einem Datensatz medizinischer Bilder dienen kann. Obwohl es bei beiden Aufgaben um Bildklassifizierung geht, sind die Bilder in den beiden Datensätzen von sehr unterschiedlicher Natur.

Fokus auf Bild- und Textmodelle

Vielleicht haben Sie bemerkt, dass alle Beispiele in diesem Abschnitt mit Bild- und Textdaten zu tun hatten. Das liegt daran, dass Transfer Learning hauptsächlich für Fälle gedacht ist, in denen man eine ähnliche Aufgabe auf die gleiche Datendomäne anwenden kann. Modelle, die mit tabellarischen Daten trainiert werden, decken jedoch eine potenziell unendliche Anzahl möglicher Vorhersageaufgaben und Datentypen ab. Nun könnten Sie ein Modell auf tabellarischen Daten trainieren, um vorherzusagen, wie Sie Eintrittskarten für Ihre Veranstaltung auspreisen sollten, wie wahrscheinlich jemand einen Kredit zurückzahlen wird, wie der Umsatz Ihrer Firma im nächsten Quartal aussieht, wie lange eine Taxifahrt dauert usw. Die konkreten Daten für diese Aufgaben sind ebenfalls unglaublich vielfältig, wobei das Ticketproblem von Informationen über Künstler und Veranstaltungsorte, das Kreditproblem vom persönlichen Einkommen und die Dauer der Taxifahrt von den städtischen Verkehrsmustern abhängt. Aus diesen Gründen ist es von Haus aus nicht trivial, das Gelernte von einem tabellarischen Modell auf ein anderes zu übertragen.

Obwohl Transfer Learning bei tabellarischen Daten noch nicht so weit verbreitet ist wie bei Bild- und Textdomänen, präsentiert die Forschung in diesem Bereich mit TabNet (https://oreil.ly/HI5Xl) eine neue Modellarchitektur. Die meisten tabellarischen Modelle erfordern umfangreiches Feature Engineering im Vergleich zu Bild- und Textmodellen. TabNet bedient sich einer Technik, die erstmals mit unüberwachtem Lernen (Unsupervised Learning) die Darstellungen für tabellarische Features lernt und dann diese gelernten Darstellungen einer Feinabstimmung unterzieht, um Vorhersagen zu erzeugen. Auf diese Weise automatisiert TabNet das Feature Engineering für tabellarische Modelle.

Einbettungen von Wörtern vs. Sätzen

In unseren Erläuterungen zu Texteinbettungen haben wir uns vorrangig auf Wort-Einbettungen konzentriert. Satz-Einbettungen stellen einen anderen Typ von Texteinbettungen dar. Während Worteinbettungen einzelne Wörter in einem Vektorraum repräsentieren, stehen Satzeinbettungen für ganze Sätze. Folglich sind Worteinbettungen kontextunabhängig. Sehen Sie sich an, wie sich das beim folgenden Satz verhält:

»I’ve left you fresh baked cookies on the left side of the kitchen counter.«

In diesem Satz erscheint das Wort left zweimal, zuerst als Verb (dt. lassen, hinterlassen) und dann als Adjektiv (dt. links). Wenn wir für diesen Satz Worteinbettungen generieren, erhalten wir für jedes Wort ein eigenes Array. Bei Worteinbettungen wäre das Array für beide Instanzen des Worts left dasselbe. Mit Einbettungen auf Satzebene erhalten wir jedoch einen einzigen Vektor, der den gesamten Satz repräsentiert. Es gibt verschiedene Ansätze, um Satzeinbettungen zu erzeugen – von der Mittelung der Worteinbettungen eines Satzes bis hin zum Training eines überwachten Lernmodells auf einem großen Textkorpus, um die Einbettungen zu erzeugen. Was hat das mit Transfer Learning zu tun? Die zweite Methode – Training eines überwachten Lernmodells, um Einbettungen auf Satzebene zu generieren – ist eigentlich eine Form des Transfer Learning. Diesen Ansatz verwenden der Universal Sentence Encoder (https://oreil.ly/Y0Ry9) von Google (verfügbar in TF Hub) und BERT (https://oreil.ly/l_gQf). Diese Methoden unterscheiden sich von den Worteinbettungen dadurch, dass sie über das einfache Nachschlagen von Gewichten für einzelne Wörter hinausgehen. Stattdessen sind sie durch Training eines Modells auf einem großen Datensatz mit unterschiedlichem Text erstellt worden, um die von Wortfolgen vermittelte Bedeutung zu verstehen. Somit können sie vom Konzept her auf verschiedene Aufgaben mit natürlicher Sprache übertragen und zum Aufbau von Modellen verwendet werden, die Transfer Learning implementieren.