| von Marco Molitor

Mit MLflow von Databricks den gesamten Machine Learning Lifecycle abdecken

Der Machine Learning (ML) Lifecycle in Databricks mit MLflow

Der ML Lifecycle umfasst viele verschiedene Schritte: von der Konzeption und Datenhaltung übers Modelltraining und -optimierung bis hin zur Modellverwaltung und dem Deployment. Eine Plattform, die modernen Anforderungen an die Verfügbarkeit von Machine Learning-Modellen genügen möchte, muss den gesamten Lifecycle im Data Science abbilden können – es reicht nicht aus, statische Modelle in einer lokalen Umgebung analysieren zu können. Data Scientists und ML Engineers benötigen ein Angebot, das Versionierung sowie Modelltracking & -monitoring anbietet.

Mit MLflow bietet Databricks eine auf Ihre Umgebung abgestimmte und mit Apache Spark kommunizierende Bibliothek an, die Machine Learning für Big Data skalierbar und nachverfolgbar macht. Wir zeigen Ihnen, wie Sie mit MLflow in Databricks mittels Python Notebooks eine Pipeline aufbauen, die eine breite Palette an Modellen mit Hyperparameter-Optimierung trainiert und die Besten von diesen gegen bestehende Versionen testet. Nach vordefinierten Kriterien wird das beste performende Modell automatisch in die Produktion geliefert und einen Endpunkt angeboten, über den das Modell aufgerufen und Vorhersagen liefern kann.

Datenanpassung

Bevor ML-Modelle mit Daten arbeiten können, müssen diese zunächst in eine Form gebracht werden, die für numerisch arbeitende Modelle verständlich sind. Zwar gibt es ggf. Ausnahmen, aber viele ML-Modelle arbeiten strikt numerisch, bspw. die lineare oder logistische Regression. Aber auch für viele Deep Learning-Verfahren benötigen wir Inputs, die numerisch als Floating Points codiert sind. So mag es im Lakehouse optimal sein, das Bundesland eines Kunden als Text abzuspeichern, jedoch viele Algorithmen wollen es allerdings in numerischer Form erhalten. Die Lösung findet sich im Preprocessing: Hier können Textwerte numerisch kodiert werden. Für ordinale oder kategorische Variablen (wie bspw. das Bundesland eines Kunden) bietet sich häufig das One Hot Encoding an: Für deutsche Bundesländer würden bspw. 16 0/1-Dummy-Variablen erstellt, die den Wert 1 genau dann annehmen, wenn der Kunde aus dem dazugehörigen Bundesland stammt.

Diese Form des Kodierens führt häufig zum Sparse Matrix-Problem: Der Datensatz wird durch viele Spalten künstlich aufgeblasen, die für sich jeweils kaum Informationen enthalten. Wenn wir z.B. nicht nach deutschen Bundesländern, sondern nach Kommunen kodieren wollen, würden wir etwa 11.000 Dummy-Spalten erhalten, die fast immer nur den Wert „0“ enthielten, da jeder Kunde nur einer einzelnen Kommune zugeordnet ist. Databricks und MLflow helfen uns bei diesem Problem, indem sie diese Daten als sparse matrix abspeichern: Es wird nicht die gesamte Spalte abgespeichert, sondern eine Repräsentation, die erfasst, in welcher Spalte & Zeile ein "nicht-0-Wert" stehen sollte (sowie welcher Wert). So erzielt Databricks mit Spark eine enorme Effizienzsteigerung und unser Datensatz behält eine auch bei sehr großen Datenmengen überschaubare Größe.

Auch an anderen Stellen sollten wir unseren Datensatz auf Herz und Nieren überprüfen: Wenn wir die Lehren des Lakehouse beherzigt und ein Gold-Level an Standardisierung erreicht haben, sollten die Daten aus unserem Core Lakehouse sauber und standardisiert sein. Dennoch lohnt es sich, die Daten zu bereinigen, gegebenenfalls fehlende Werte zu imputieren, und neue Features zu kodieren, sofern wir diese benötigen. So kann es oft Sinn ergeben, Kundenalter nicht als fortlaufende numerische Variable zu erfassen, sondern als kategorische Variable: bspw. die berühmte 18-bis-49-Zielgruppe sowie die Alterskohorte jenseits davon. So verdichten wir Information und steigern die Güte an Präzision, die später durch unsere trainierten Modelle erreicht wird. Auch hier gilt der Grundsatz: more data trumps less data, but better data trumps more data.

Feature Store
Abbildung 1: Der Feature Store erleichtert zentrale Daten für ML

Sobald wir mit der Form der Daten zufrieden sind, können diese im Databricks Feature Store hinterlegt werden. Dort liegen Daten zentralisiert in direkt nutzbarer Form vor, sodass ein Data Science Team mit den gleichen, aktuellen Daten arbeiten kann, ohne regelmäßig auf das Core Lakehouse zugreifen zu müssen. Auf diese Weise stehen hochwertige, kuratierte Daten zentral zum Modelltraining – auch in unterschiedlichen Kontexten! – zur Verfügung und können in regelmäßigen Abständen bspw. mittels Databricks Workflows upgedatet werden. Dies entkoppelt das Preprocessing vom eigentlichen Modelltraining und ist daher sehr wertvoll.

Modelltraining

1.1 Modelltraining Basics

ML-Runtime auf Cluster
Abbildung 2: Nutzen der ML-Runtime auf dem Cluster

Wenn ein Datensatz zufriedenstellend als PySpark DataFrame vorliegt, können wir beginnen, Modelle zu importieren und zu trainieren. Databricks bietet uns dafür mit MLflow eine moderne und elegante Bibliothek, die alle Bereiche des Modelltrainings für uns übernimmt und automatisch in Databricks integriert. Um die MLflow-Bibliothek nutzen zu können, benötigen wir einen Cluster, auf dem die jeweilige ML Runtime-Version installiert ist!

MLflow bietet auf Apache Spark abgestimmte Varianten beliebter Python-Bibliotheken: Es gibt Integrationen für bspw. Scikit Learn, aber auch PyTorch und TensorFlow. So lassen sich nicht nur viele klassische Algorithmen wie die Logistische Regression, ein Random Forest, oder ein Gradient Boosting-Modell implementieren, sondern auch große Deep Learning-Modelle bis hin zu Transformern für Natural Language Processing oder zur Zeitreihenvorhersage.

Die Besonderheit ist hier, dass diese Modelle mittels der Implementierung durch MLflow das parallel processing von Apache Spark nutzen, um das Modelltraining verteilt und somit effizienter zu gestalten. Dies ist ein schlagender Vorteil gegenüber klassischem, statischen Data Science aus lokalen Arbeitsumgebungen heraus, die bei Big Data schnell an ihre Grenzen stoßen. Dank Apache Spark in Databricks können wir riesige Datenmengen für das Modelltraining nutzen und so die Information aus gigantischen Datenbergen konzentriert für die Inferenz verwenden.

Im Folgenden Beispiel nutzen wir einen KNN Classifier, den wir aus der mlflow.sklearn-Bibliothek verwenden. Diese Implementierung funktioniert so, wie wir sie aus Scikit-Learn kennen, und bietet daher eine dankbare Oberfläche für Data Scientists, die ihre bekannten Bibliotheken nicht missen möchten.

Wir beginnen den Codeblock hier mit "with mlflow.start_run():", um den gesamten folgenden Code an MLFlow zu übergeben (Beispiel findet sich weiter unten). So können wir jetzt im MLflow Run das Modelltraining aufrufen und Metriken wie bspw. den MSE oder, für Klassifikation, Accuracy, Recall, Precision, etc. überprüfen. Abschließend loggen wir das trainierte Modell als Artefakt und können dieses ab sofort im Model Registry hinterlegt finden. Wir können für jedes Modell mehrere Versionen abspeichern und diese auch durch Tags bestimmten Stages zuweisen: bspw. „Production“ oder „Archived“. So behalten wir stets einen guten Überblick über unsere trainierten Modelle.

Geloggte Metriken für KNN-Classifier
Abbildung 3: Metriken & Parameter werden unter Experiments & im Model Registry gespeichert

1.2. Hyperparameter-Tuning

Die meisten Machine Learning-Algorithmen enthalten bestimmte voreingestellte Hyperparametern, die sich justieren lassen. Abhängig davon, welche Hyperparameter-Kombination wir gewählt haben, wird das finale Modell besser oder schlechter performen. Es ist daher gängige Praxis, das Hyperparameter-Tuning in das Modelltraining aufzunehmen. Für einen Random Forest Classifier lässt sich bspw. justieren, wie viele einzelne Entscheidungsbäume als weak learner zugrunde liegen sollen und wie hoch die maximale Tiefe eines jeden solchen Entscheidungsbaumes sein soll. Ähnliche Parameter gibt es für populäre Boosting-Algorithmen wie LGBM, XGBoost, oder CatBoost, die darüber hinaus noch eine Learning-Rate als Parameter benötigen.

Ein Problem entsteht dann, wenn wir eine große Anzahl an Hyperparametern vollständig abdecken wollen. Dieser als GridSearch bekannte Ansatz, in dem wirklich jede mögliche Kombination aus vordefinierten Hyperparametern getestet wird, stößt schnell auf das Problem, dass die Anzahl an notwendigen Modelltrainings gigantische Ausmaße erreicht. Deshalb ist es oft etablierte Praxis, stattdessen die RandomizedSearch-Variante zu verwenden: Aus allen möglichen Hyperparameter Kombinationen wird eine kleinere Anzahl zufällig gezogener Kombinationen trainiert. So gelangt man ggf. nicht zur absolut besten Kombination, aber für gewöhnlich zu einer hinreichend guten, die zufriedenstellend ist.

Databricks bietet darüber hinaus noch eine Optimierung an, die sich die Einsichten bayes’scher Statistik zunutze macht: den sogenannten Tree-Parzen-Estimator. Mittels mathematischer Optimierung wird sequenziell anhand der Trainingsgüte vorheriger Hyperparameter Kombinationen bestimmt, welche Kombination als nächstes getestet werden sollte. Da dieser Ansatz nicht leicht verständlich ist, soll er hier mit einer Analogie erläutert werden.

Stellen Sie sich vor, Sie wollen in Ihrer Wohnung bestimmen, welcher Punkt am wärmsten ist. GridSearch zerlegt die Wohnfläche in 1x1m^2 große Quadrate und testet jedes Einzelne. Es führt mit Sicherheit zum wärmsten Punkt, aber für eine große Villa kann das sehr lange dauern – und wirklich rechenintensiv werden!

RandomizedSearch nimmt sich dasselbe Raster, überprüft aber nur zufällige 10% der Fläche. Es kommt vielleicht nicht am wärmsten Punkt heraus, aber doch immerhin in dessen Nähe. Sofern wir nicht auf Temperaturunterschiede der dritten Nachkommastelle angewiesen sind, kann das schon gut genug sein!

Der Tree-Parzen-Estimator verbessert diesen Ansatz noch einmal. Zunächst einmal nimmt man sich bspw. drei zufällige Punkte im Haus: einen im Keller, in der Küche, und im Wohnzimmer, und bestimmt von diesen dreien den wärmsten Punkt. Nehmen wir an, dieser liegt im Wohnzimmer. Als Nächstes sucht man in dieser Umgebung erneut nach dem wärmsten Punkt: in der Nähe vom TV, vom Sofa, etc. Dieses Spiel führt man einige Male durch und baut jedes Mal auf der Information vom vorherigen Durchlauf auf: Man sucht dort weiter, wo man den besten Punkt vermutet. So geraten wir zuverlässiger und effizienter ans Ziel als mit RandomizedSearch.

Dank MLflow gibt es eine sehr gute Integration für die Hyperparameter-Optimierung mit hyperopt. Wir beginnen mit einem fmin-Call, der das Minimum einer Funktion sucht. Diese Funktion definieren wir selbst, bspw. suchen wir in einer Regression das Modell, unter dem der Mean-Squared-Error minimal ist. Wollen wir in anderen Fällen etwas maximieren (z.B. Accuracy), so können wir einfach eine Funktion schreiben, die diese Metrik mit -1 multipliziert zurückgibt. Merke: das Maximum von f(x) ist das Minimum von -f(x)!

Objective Function für fmin()
Abbildung 4 Objective Function für fmin() - Selbstdefinierte Minimierungsfunktionen

Unter hyperopt.tpe.suggest können wir den Suggestion Algorithmus des Tree-Parzen-Estimators erhalten, sodass wir in den fmin-Call als objective function unsere selbstgeschriebene Funktion setzen, den tpe.suggest-Algorithmus dafür spezifizieren, einen vordefinierten Search-Space an Hyperparametern mitgeben, eine maximale Anzahl an Evaluierungen definieren, und zuletzt ein SparkTrials-Argument mitgeben, damit das Hyperparameter-Tuning parallelisiert werden kann. Dabei ist zu beachten, dass MLflow ebenfalls Bibliotheken liefert, die bereits nativ parallelisiert sind. In diesem Fall müssen wir kein Trials-Argument mitgeben, da dieses von Spark automatisch übernommen wird.

Search Space
Abbildung 5: Hyperparameter werden für HyperOpt als Range übergeben

Das Besondere dabei ist, wenn wir dies ausführen, können wir später im Experiments-Bereich von Databricks alle Tuning-Versuche nachverfolgen und uns zentrale Metriken ausgeben lassen. So finden wir heraus, wie der Goodness-of-Fit aller geprüften Hyperparameter war. Databricks bietet außerdem ein Feature, mit dem wir automatisch die besten Hyperparameter ausgeben lassen können, um diese als Modell zu registrieren und später zu deployen.

Hyperparameter
Abbildung 6: Ein einfacher Run für das Hyperparameter-Training

Model Registry & Deployment

All unsere oben genannten Training Runs können wir unter dem Reiter Experiments nachvollziehen. Jedem Experiment wird ein Run Name zugewiesen: Dieser besteht aus einem Adjektiv, einem Substantiv, und einer zufälligen Zahl:

Databricks Experiments
Abbildung 7: Unter Experiments sehen wir alle vorhergehenden Runs

Das eigentliche Interesse liegt in den tatsächlich registrierten und hinterlegten Modellen. Diese finden wir im Model Registry. Wir können aus einem Notebook heraus die besten Modelle automatisch registrieren und mit Tags & Beschreibungen versehen. So können wir bspw. ein Random Forest-Modell mit einer Beschreibung versehen, die enthält, welche Hyperparameter Kombination verwendet wurde und welche neuen Daten verwendet wurden. Dann können wir dieses Modell taggen – bspw. als „Development“ – wenn wir es gegen das Modell abgleichen wollen, das aktuell im Deployment ist.

Das Model Registry hilft uns sehr, wenn es darum geht, einen Überblick über unsere trainierten Modelle zu behalten und diese zu versionieren und ggf. Rollbacks durchzuführen, wenn ein neues Modell in der produktiven Umgebung nicht so gut performt, wie man es sich erhofft hat oder aber zu Fehlern führt.

Sind wir zufrieden mit der Performance eines Modells, so können wir im Bereich Serving einen Endpunkt einrichten, an dem wir Modelle hinterlegen können, die über eine API angesprochen werden können. So können bspw. Echtzeitvorhersagen in eine App integriert werden, ohne dass eine aufwendige Integration in diese vonnöten wäre – man schickt schlicht den jeweiligen Input, der vom Modell bewertet werden soll, an den Endpunkt und dieser gibt den gewünschten Output blitzschnell zurück. Eine Besonderheit von MLflow ist, dass es uns ebenfalls erlaubt, custom models zu schreiben, bei denen individuelle Modelle zusätzlich individualisiertes pre- bzw. post-processing erfahren. So lassen sich auch vorgefertigte Modelle noch stärker individualisieren und der Output des Endpunkts anpassen, damit er exakt die Antwort liefert, die von der anfragenden Schnittstelle, bspw. einer App, erwartet wird.

Databricks Endpunkt
Abbildung 8: Unter Serve konfigurieren wir eigene Endpunkte

Auf diese Art und Weise können Sie für Ihre Anforderungen ML Modelle in der Cloud arbeiten lassen, ohne dass diese Ihre lokalen Umgebungen überfordern. Anwendungen bleiben schlank und Sie nutzen die Geschwindigkeitsvorteile der Cloud bei geringen Kosten.

 

Zusammenfassung

So lässt sich Ihr gesamter ML-Lifecycle in Databricks automatisieren: Mit Databricks Workflows können Sie regelmäßig die nötigen Daten aus dem Lakehouse ziehen, umformen, und im Feature Store hinterlegen, sodass immer die aktuellen Daten passgenau für das Modelltraining vorliegen. Daneben steht eine zweite Pipeline, die aus dem Feature Store Daten zieht und über eine ML-Pipeline mit Hyperparameter-Optimierung neueste Modelle nach Ihren Vorgaben automatisch trainiert und im Model Registry abspeichert. Abschließend können Sie die besten von Ihnen trainierten Modelle anpassen und an einem Endpunkt automatisch hinterlegen – und updaten – lassen, damit Ihre Apps immer mit den neuesten Modellen automatisch kommunizieren können.

Quellen

https://docs.databricks.com/en/mlflow/index.html
https://docs.databricks.com/en/machine-learning/feature-store/index.html
https://docs.databricks.com/en/machine-learning/automl-hyperparam-tuning/hyperopt-spark-mlflow-integration.html
https://proceedings.neurips.cc/paper_files/paper/2011/file/86e8f7ab32cfd12577bc2619bc635690-Paper.pdf
https://docs.databricks.com/en/machine-learning/model-serving/index.html

 

Teile diesen Artikel mit anderen

Über den Autor

Marco ist Data Scientist und als Associate Consultant bei der Woodmark tätig. Dabei betreut er insbesondere Data-Science- und Data- Engineering-Projekte mit einem Fokus auf AWS & Databricks.

Zur Übersicht Blogbeiträge