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.
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 direkt über pip
installiert werden:
pip install tensorflow
<aside> 📌 Um das Deployment später einfacher zu machen und Kompatibilitätsprobleme zu vermeiden, ist es immer ratsam innerhalb einer virtuellen Umgebung zu arbeiten. Python bietet hierfür viele Möglichkeiten. Wir verwenden Virtualenv bzw. das seit Python 3.3 mitgelieferte Modul venv. Eine andere bekannte Alternative ist Anaconda.
</aside>
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
.
# Load data and split into labels and features
data = pd.read_csv('../data/vw.csv', converters={
'model': str_to_category,
'transmission': str_to_category,
'fuelType': str_to_category,
})
label = data.pop('price')
Unsere Funktion, die wir auf den Strings aufrufen, ist sehr einfach, aber kann natürlich beliebig erweitert werden:
def str_to_category(string):
"""
Converts a string to a category.
"""
return string.strip(' \\t\\n').lower().replace(' ', '_').replace('-', '')
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()
.
# Encode categorical data
model_enc = preprocessing.OrdinalEncoder()
data.loc[:, 'model'] = model_enc.fit_transform(data.loc[:, ['model']])
transmission_enc = preprocessing.OrdinalEncoder()
data.loc[:, 'transmission'] = transmission_enc.fit_transform(data.loc[:, ['transmission']])
fuelType_enc = preprocessing.OrdinalEncoder()
data.loc[:, 'fuelType'] = fuelType_enc.fit_transform(data.loc[:, ['fuelType']])
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 Feature 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.
# Split data into training and test sets
train_data, test_data, train_label, test_label = train_test_split(data, label, test_size=0.2, random_state=42)
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 Feature 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.
# Normalize data
data_scaler = preprocessing.StandardScaler()
train_data = data_scaler.fit_transform(train_data)
test_data = data_scaler.transform(test_data)
# Normalize labels
label_scaler = preprocessing.StandardScaler()
train_label = label_scaler.fit_transform(train_label.values.reshape(-1, 1))
test_label = label_scaler.transform(test_label.values.reshape(-1, 1))
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 Feature besitzen, entscheiden wir uns für ein einfaches Multilayer-Perceptron (MLP) mit zwei Hidden-Layern der Größe 64 und mit ReLU-Aktivierung: