Trainiere ein eigenes Modell mit TensorFlow
Dieser Teil ist eine Ergänzung zu Trainiere ein eigenes Modell mit PhotonAI, da PhotonAI für viele spezifischere Anwendungsfälle nicht geeignet ist. Wenn das eigene Problem bereits zufriedenstellend mit einem PhotonAI Modell gelöst werden konnte, kann dieses Kapitel also übersprungen werden. Andererseits wird die Erstellung eines Modells in diesem Kapitel noch knapper behandelt und es kann durchaus sinnvoll sein, zunächst das vorherige Kapitel zu lesen. Abgesehen davon handelt es sich ebenfalls nicht um eine Einführung in TensorFlow oder die Erstellung von Machine Learning Modellen allgemein. Vielmehr soll lediglich ein einfaches Modell erstellt werden, das in den folgenden Teilen als Beispiel im Deployment- und Veröffentlichungs-Prozess dient. Die TensorFlow Doku bietet jedoch zahlreiche Tutorials und Erklärungen, um mehr über die Erstellung von Machine Learning Modellen mit TensorFlow zu lernen.
Was ist TensorFlow?
TensorFlow ist ein Framework, das insbesondere die Verarbeitung mehrdimensionaler Daten und das Training von tiefen Neuronalen Netzen ermöglicht. Es werden Schnittstellen zu zahlreichen Programmiersprachen angeboten, sodass eine Verwendung der fertigen Modelle beispielsweise auch auf mobilen Endgeräten möglich ist. Durch die hohe Anzahl verfügbarer Funktionen und Layer können individuelle und sehr spezifische Anforderungen erfüllt werden. Im direkten Vergleich zu PhotonAI ist die Verwendung dadurch jedoch auch komplexer und erfordert eine längere Einarbeitungszeit. Mit der Version 2.0 wurde Keras allerdings zur Standard-API und die Benutzung vereinfacht.
Für Python kann TensorFlow im Terminal direkt über pip
installiert werden:
pip install tensorflow
Vorbereitung der Daten
In unserem Beispiel haben wir uns für einen frei verfügbaren Datensatz von Kaggle entschieden, welcher Daten und Preis zu gebrauchten Autos enthält (100,000 UK Used Car Data set).
Lade diese Datei runter und kopiere sie in den Ordner /incubaitor/1_Frameworks/1_2_Tensorflow/data/
.
Zunächst definieren wir wieder unsere Daten und Projekt-Struktur. In diesem Fall befinden wir uns mit dem Notebook im Ordner /incubaitor/1_Frameworks/1_2_Tensorflow/notebooks/
. Relativ dazu haben wir nun die Daten in den Ordner /incubaitor/1_Frameworks/1_2_Tensorflow/data/
geladen und werden unsere Modelle in /incubaitor/1_Frameworks/1_2_Tensorflow/models/
abspeichern. Somit können wir die relativen Pfade ../data/
und ../models/
nutzen. Falls es hier Probleme gibt, können auch explizite Pfade wie im PhotonAI Beispiel genutzt werden.
Wie auch schon bei PhotonAI nutzen wir zum Einlesen unserer Daten die Methode read_csv()
aus dem Python-Package Pandas. Da wir in diesem Fall auch Spalten verwenden möchten, die nicht-nummerische Werte enthalten, bereiten wir die entsprechenden Werte schon beim Einlesen vor. Über den Parameter converters
können Funktionen angegeben werden, die dann auf jeden einzelnen Wert einer Spalte angewandt werden. Hier haben wir uns eine eigene Funktion str_to_category()
definiert, die vorhandene Leerzeichen am Anfang und Ende der Strings entfernt, alles in Kleinbuchstaben konvertiert und noch ein paar andere Schritte unternimmt. Anschließend entfernen wir die Spalte price
aus unseren Daten und speichern sie stattdessen separat als label
.
Da unser Modell nur Zahlen verarbeiten kann, müssen wir auch unsere kategorischen Variablen in eben solche umwandeln. Dazu können wir den OrdinalEncoder()
aus dem Python-Package sklearn verwenden. Dieser nummeriert alle gefundenen Kategorien durch und sortiert diese im Anschluss jeweils durch ihre eindeutige Nummer. Auch wenn dies eine einfache Möglichkeit darstellt, kategorische Daten in Zahlen umzuwandeln, sollte für jeden Anwendungsfall der Sinn einer solchen Umwandlung geprüft werden, da auf diese Weise implizit Ähnlichkeiten zwischen Kategorien suggeriert werden, die nicht korrekt sind. Selbst in diesem Beispiel sind die zugeordneten Zahlen nicht ganz richtig, weil eine eindeutige Sortierung der verschiedenen Automodelle nicht existiert. Alternativ kann ein OneHotEncoder()
verwendet werden, der den Kategorien Vektoren mit jeweils nur einem Eintrag zuordnet und dadurch sicherstellt, dass die Distanz zwischen je zwei Kategorien immer gleich ist. Für unser einfaches Beispiel mit wenigen Kategorien belassen wir es jedoch bei dem OrdinalEncoder()
.
Um unser Modell am Ende bewerten zu können, müssen wir einen Teil unserer Daten im Vorfeld abspalten. Wir verwenden 20% unserer Daten nicht für das Training, um mit ihnen anschließende eine Evaluation durchführen zu können. Bei der Aufteilung ist es wichtig, die Reihenfolge der Features und Label zu erhalten, damit diese weiter korrekt zugeordnet werden können. Zum Glück hilft uns hierbei die Funktion train_test_split()
aus sklearn. Sie ordnet die einzelnen Datenpunkte zufällig dem Trainings- bzw. Test-Datensatz zu, sodass am Ende das gewünschte Größen-Verhältnis entsteht und achtet dabei darauf, die Pärchen aus Feature und Label zu erhalten. Damit wir bei mehreren Durchläufen, beispielsweise nach der Anpassung unseres Modells, immer den gleichen Test-Datensatz verwenden und so die Ergebnisse vergleichbar sind, gibt es die Möglichkeit über den Parameter random_state
einen Seed zu setzen. Voraussetzung ist natürlich, dass sich die Eingabe der Funktion dabei nicht ändert.
Als Letztes müssen wir unsere Daten und Label noch skalieren. Hierzu verwenden wir den StandardScaler()
ebenfalls aus sklearn. Durch die Skalierung können wir sicherstellen, dass nicht einzelne Features alleine durch ihre Größe das Modell dominieren. Die Parameter für den Scaler wählen wir ausschließlich anhand der Trainingsdaten (siehe fit_transform()
statt nur transform()
). Dies ist wichtig, um eine korrekte Evaluation zu gewährleisten. Später im produktiven Einsatz ist es nämlich ebenfalls nicht möglich einen Scaler anhand eines einzelnen Datenpunktes zu definieren. Zudem würden die Ergebnisse durch einen Scaler, der für einen anderen Bereich definiert wurde, verfälscht.
Erstellung und Training des Modells
Anders als bei dem PhotonAI-Beispiel werden wir keine Hyperparametersuche durchführen und auch nicht mehrere Modelle vergleichen. Wir definieren stattdessen nur ein konkretes Modell. Da wir nur acht Features besitzen, entscheiden wir uns für ein einfaches Multilayer-Perceptron (MLP) mit zwei Hidden-Layern der Größe 64 und mit ReLU-Aktivierung:
Bevor wir das Modell trainieren können, müssen wir es noch kompilieren. Dabei müssen wir auch einen Optimizer und eine Loss-Funktion angeben. Mit dem Parameter metrics
können wir weitere Metriken angeben, die wir dann während des Trainings zur Bewertung unseres Modells verwenden können. Die Methode summary()
gibt auf der Kommandozeile eine Zusammenfassung des Modells aus. Auf diese Weise kann beispielsweise die Zahl der trainierbaren Parameter nochmal kontrolliert werden.
Anschließend ist das Modell bereit für das Training. Neben der Batch Size und der Epochen Anzahl können wir hier auch die Größe eines Validation Sets angeben. Nach jeder Epoche werden der Loss und die anderen Metriken auch auf diesen Daten berechnet, um zum Beispiel Overfitting rechtzeitig erkennen zu können.
Während des Trainings werden wir in der Konsole regelmäßig über den Trainingsfortschritt informiert. Dadurch können wir einschätzen wie lange das Training noch etwa braucht und kontrollieren, dass der Loss tatsächlich abnimmt und unser Modell konvergiert.
Evaluation und Speicherung
Sobald das Training abgeschlossen ist, können wir die zuvor zur Seite gelegten Test-Daten dazu nutzen, die Qualität unseres Modells zu evaluieren. Um unabhängige und vergleichbare Ergebnisse zu bekommen, ist es wichtig, dass wir diese Daten weder direkt noch indirekt (wie die Validation-Daten als Abbruchbedingung) während des Trainings verwendet haben. Die Berechnung der Scores übernimmt TensorFlow für uns und benötigt dazu lediglich die Test-Daten zusammen mit den Ground Truth-Labeln.
Als Ausgabe erhalten wir unseren Loss sowie die Ergebnisse der anderen angegebenen Metriken. Diese Werte geben uns zwar einen guten Anhaltspunkt für die Qualität unseres Modells, mithilfe des Python-Packages matplotlib können wir die Ergebnisse jedoch auch plotten, um diese anschaulicher darzustellen. Anders als bei PhotonAI müssen wir uns hier selber um sinnvolle Darstellungen kümmern. Da wir in diesem Beispiel ein Regressions-Modell trainiert haben, ist die Darstellung als Scatterplot hilfreich. Bei einem Klassifikations-Modell wäre dagegen eine Confusion-Matrix noch einfacher lesbar.
Während des Preprocessings haben wir unseren Daten und Label normalisiert. Damit wir die Ergebnisse in unserem Plot besser einordnen können, sollten wir die Normalisierung nun rückgängig machen. Die Scaler besitzen dazu praktischerweise die Methode inverse_transform()
, die wir einfach für unsere Predictions und Ground Truth-Label aufrufen können. Zusätzlich plotten wir als Orientierung noch eine Linie, welche die optimalen Predictions angibt.
Anhand des erstellten Plots können wir einfach erkennen, dass die vorhergesagten Preise unseres Modells ungefähr den tatsächlichen Preisen entsprechen. Bei höherpreisigen Fahrzeugen nehmen die Abweichungen aufgrund von weniger Datenpunkten zwar zu, für unseren Anwendungsfall reicht uns die Qualität allerdings.
Damit wir das Modell nun zu einem späteren Zeitpunkt verwenden können ohne es neu trainieren zu müssen, ist es notwendig dieses manuell zu speichern. Doch nicht nur unser trainiertes Modell muss gespeichert werden. Damit wir die Eingabedaten korrekt vorbereiten können, brauchen wir auch alle Encoder (zur Konvertierung der kategorischen Variablen in Zahlen) und alle Scaler (zur Normalisierung unserer Feature und Label). Alle diese Objekte müssen wir speichern. Dazu können wir entweder ein neues Objekt konstruieren, dass alle benötigten Dinge enthält, oder wir speichern sie als einzelne Dateien. Wir haben uns für letztere Möglichkeit entschieden.
Verwendung des Modells
Analog zur Speicherung unserer Objekte müssen wir diese nun zunächst wieder aus den einzelnen Dateien laden:
Anschließend definieren wir eine eigene Eingabe und bereiten diese mithilfe der Encoder und Scaler für unser trainiertes MLP vor. Damit wir die verschiedenen Features nicht vertauschen, erzeugen wir wieder ein Pandas-Dataframe und benennen alle Spalten. Stattdessen könnten wir auch nur ein Numpy-Array mit den Werten anlegen. Das Modell hat abgesehen von der Reihenfolge jedoch keine Möglichkeit zu überprüfen, ob unsere Werte tatsächlich zu dem jeweiligen Feature passen. Deshalb sollte drauf geachtet werden, die Feature Reihenfolge aus dem Trainingsdatensatz zu erhalten.
Drei unserer Features enthalten zudem kategorische Daten statt Zahlen, die wir erst mithilfe der Encoder in Zahlen konvertiert haben. Bei der Erzeugung der Encoder wurden alle Kategorien aufgenommen, die in den Trainingsdaten enthalten waren. Dadurch können sie nun auch nur noch diese Kategorien in Zahlen übersetzen. Bei der Eingabe neuer Daten in das Modell, sollte dies berücksichtigt werden. Auch die genaue Schreibweise und eventuelle Leerzeichen am Anfang oder Ende führen dazu, dass die Encoder eine Kategorie nicht mehr erkennen. Für das Einlesen unserer Trainingsdaten hatten wir uns jedoch schon eine Funktion str_to_category()
definiert, die für uns viele Stolperfallen entfernt. Falls wir uns trotzdem nicht mehr sicher sind, welche Kategorien zur Auswahl stehen, können wir uns mit dem Attribut categories_ alle Kategorien anzeigen lassen, die ein Encoder kennt:
Abschließend können wir unsere kodierten und normalisierten Daten an unser Modell übergeben, welches basierend darauf eine Vorhersage berechnet. Diese Vorhersage müssen wir nur noch mit dem Label-Scaler zurückskalieren, um den Preis zu erhalten:
Als Ergebnis wird etwa 27516.51 ausgegeben, was nah an dem Ergebnis des PhotonAI-Modells liegt und ebenfalls einem realistischen Preis entspricht.
Wir konnten mit TensorFlow also ein eigenes Modell entwerfen, trainieren und nun sogar verwenden. Dafür waren zwar deutlich mehr manuelle Schritte notwendig, die uns zuvor von der PhotonAI Hyperpipe abgenommen wurden, im Ausgleich konnten wir jedoch auch kategorische Features für unsere Vorhersage nutzen.
Ordnerstruktur
Der Code, den wir in diesem Notebook aufgebaut und erklärt haben, findet sich auch in dem Ordner incubaitor/1_Framworks/1_2_Tensorflow/app
wieder. Dort finden wir drei Dateien: train.py
, test.py
und utils.py
.
In train.py
werden die Daten vorbereitet, das Modell erstellt und trainiert sowie evaluiert und gespeichert.
In test.py
finden wir dann den Code, der es uns ermöglicht, das Modell zu verwenden und zu testen.
utils.py
beinhaltet nur die Funktion str_to_category()
. Hilfsfunktionen wie diese werden üblicherweise in eine andere Datei ausgelagert, um das Skript nicht zu überfüllen.
Der Ordner incubaitor/1_Framworks/1_2_Tensorflow/app
ermöglicht es uns später das Modell über Flask und Docker zu deployen.