Perfekter Mix: Kotlin und Python kombinieren für Machine Learning

Im Machine Learning dominiert Python, doch auch Kotlin ist für einige ML-Bereiche besonders geeignet. Mischarchitekturen nutzen die Stärken beider Sprachen.

In Pocket speichern vorlesen Druckansicht 3 Kommentare lesen
Robot,Gives,A,Hand,To,A,Woman.,Two,Hands,In

(Bild: Willyam Bradberry/Shutterstock.com)

Lesezeit: 21 Min.
Von
  • Hauke Brammer
Inhaltsverzeichnis

Im Machine Learning ist Python als Programmiersprache absolut dominant – dennoch bietet es sich an, auch hier und da über den Tellerrand zu schauen. In den letzten Jahren hat sich rund um Kotlin ein interessantes und lebhaftes Ökosystem im Bereich Data Science und Machine Learning (ML) entwickelt. Zeit für eine Bestandsaufnahme der Möglichkeiten zur Verwendung von Kotlin für drei Bereiche des Machine-Learning-Lifecycles: die Datenerkundung in Jupyter, Modell-Architekturen und das Model Serving.

Machine Learning: Lifecycle (Abb. 1)

(Bild: Hauke Brammer)

Young Professionals schreiben für Young Professionals

Dieser Beitrag ist Teil einer Artikelserie, zu der die Heise-Redaktion junge Entwickler:innen einlädt – um über aktuelle Trends, Entwicklungen und persönliche Erfahrungen zu informieren. Bist du selbst ein "Young Professional" und willst einen (ersten) Artikel schreiben? Schicke deinen Vorschlag gern an die Redaktion: developer@heise.de. Wir stehen dir beim Schreiben zur Seite.

Bevor es ans Erkunden der Möglichkeiten und Einsatzgebiete für Kotlin im Machine Learning geht, gilt es, einen kurzen Exkurs über Machine Learning im Allgemeinen und speziell im Business Context zu unternehmen. Die Ausgangsfrage dabei ist: "Was wollen wir mit Machine Learning erreichen?"

Das Ziel ist, ein Modell mit Daten zu füttern, damit es bestimmte Vorhersagen über die Welt treffen kann. In einer Hinsicht unterscheidet sich Machine Learning im Geschäftskontext von Machine Learning zu Forschungszwecken: Ein ML-Modell allein ist nutzlos. Das ist zunächst eine überzeichnete Aussage, die aber dennoch einen realen und etwas beunruhigenden Kern hat: Etwa 90 Prozent der ML-Modelle schaffen es nicht in die Produktion. Für kommerzielle Zwecke sind die meisten ML-Projekte offensichtlich wenig brauchbar.

Zwar werden einige Data-Scientists spannende Erkenntnisse gewonnen haben und auch die Marketing-Abteilung freut sich, da sie ja auch "etwas mit KI" machen. Messbarer Mehrwert entsteht jedoch nur durch ML-Modelle, die sich in konkreten Anwendungen zur Verfügung stellen lassen.

Nur in einer produktiven Umgebung ist ein Modell nützlich. Das bedeutet aber, dass wir eigentlich nicht nur Modelle, sondern ganze "Machine-Learning-Systeme" bauen wollen (siehe Abbildung 2).

Also nicht nur ein einzelnes Artefakt, sondern eine ganze Reihe von Komponenten, die es ermöglichen, zu experimentieren, zusammenzuarbeiten und kontinuierlich neue Versionen der Modelle zu erstellen und diese dann in produktive Umgebungen zu heben. Dort angekommen müssen Unternehmen sich Gedanken über den Betrieb und die Überwachung des Modells und vor allem über ein potenzielles Nachfolgemodell machen.

Viele unterschiedliche und komplexe Teile müssen für den erfolgreichen Produktivbetrieb ineinandergreifen, und im Kern steht das Modell selbst. Aber damit ein Team es ausliefern kann, muss es das Modell in einen Webservice oder in eine App einbetten (Abbildung 2, 1).

Machine-Learning-System: Für den erfolgreichen Produktivbetrieb müssen komplexte Teile ineinandergreifen, im Kern steht das Modell (Abb. 2).

(Bild: Hauke Brammer)

Anschließend gehen von den Nutzern oder von den Umsystemen Daten ein und darauf aufbauend lassen sich Vorhersagen ausliefern (Abbildung 2, 2). Die Qualität eines Machine-Learning-Modells verschlechtert sich aufgrund sich verändernder äußerer Bedingungen und Input-Daten mit der Zeit. Daher genügt es nicht, nur ein einziges Modell zu trainieren, das für lange Zeit in Verwendung bleibt, sondern es gilt, eine Infrastruktur zu schaffen, um kontinuierlich neue Modelle zu trainieren.

Gerade das Entwickeln großer oder tiefer Deep-Learning-Modelle findet in der Regel nicht auf dem Laptop statt, sondern in einer Cloud-Umgebung oder auf einem GPU-Cluster im Rechenzentrum, wofür eine kontinuierliche Trainings-Pipeline nötig ist (Abbildung 2, 3). Die Daten für das Training stammen aus den Produktivsystemen – allerdings sind sie erst aufzubereiten, zu filtern und zu labeln. Dafür ist eine Data- oder ETL-Pipeline nötig (Abbildung 2, 4).

Wer in größeren Teams oder über mehrere Teams hinweg arbeitet, sollte die aufbereiteten Daten den anderen Teammitgliedern ebenfalls zur Verfügung stellen. Dafür ist bei kleinen Datenmengen noch ein Versionskontrollsystem ausreichend, bei größeren Projekten kommt ein Feature-Store zum Einsatz (Abbildung 2, 5). Wer kontinuierlich und mit immer neuen Daten weiter trainiert, kann auch laufend neue Modelle in Produktion bringen. Diese sind allerdings zu erfassen und zu versionieren, um einen einfachen Wechsel zwischen den Modellversionen zu ermöglichen. Würde man etwa erst in Produktion im Monitoringsystem (Abbildung 2, 6) feststellen, dass ein neues Modell schlechte Ergebnisse liefert, wäre das fatal. Kein Team kann es sich erlauben, darauf zu warten, bis die Vorgängerversion des Modells noch einmal neu trainiert wurde, während das Produktionsmodell falsche Daten liefert.

Je nach Größe stehen für die Versionierung geeignete Versionskontrollsysteme wie Git oder DVC bereit, auch eine zentrale Model Registry ist eine Option (Abbildung 2, 7).

Zur kontinuierlichen Weiterentwicklung von ML-Modellen gehört es, Experimente durchzuführen: Versuche, mit einer spezifischen Modell- und Algorithmus-Konfiguration und einem Datensatz bestimmte Ergebnisse zu erzielen. Nur wenige dieser Experimente werden es in eine produktive Umgebung schaffen. Dennoch ist die Versionierung notwendig, um sich einen Überblick darüber zu verschaffen, welche Experimente man bereits durchgeführt hat und was die Ergebnisse waren, um sie in Zukunft reproduzieren und weiterverwenden zu können.

Dafür ist ein Tool erforderlich, um die Experimente zu tracken und zu verwalten (Abbildung 2, 8). Es verhindert, dass die Data Scientists die gleichen Experimente mehrfach durchführen oder Ergebnisse verloren gehen. Für erfolgreiche und produktive Machine-Learning-Projekte ist ein Satz an Tools nötig, den man sich im Verlauf eines Projekts langsam aufbaut.

Gerade beim Zusammenspiel von Kotlin und Machine Learning ist wichtig zu bedenken, dass es am Ende nicht nur darum geht, mit TensorFlow ein Deep-Learning-Modell zusammenzustecken: Es geht darum, wie sich verlässliche, reproduzierbare und skalierbare Machine-Learning-Systeme bauen und betreiben lassen.

Am Anfang jedes Machine-Learning-Projekts steht eine umfangreiche Datenanalyse. Anschließend beginnt das Design erster Modell-Architekturen und Experimente lassen sich durchführen. Für dieses iterative Vorgehen sind Jupyter-Notebooks ideal: In ihnen können Teams Daten, Notizen, Code und Visualisierungen in einem gemeinsamen Dokument darstellen und der Code lässt sich direkt und interaktiv ausführen. Das Ausführen von Python-Code ist Teil der Standard-Installation von Jupyter. Dank dessen modularer Architektur ist durch das Installieren zusätzlicher Kernel aber auch das Verwenden anderer Sprachen in den Jupyter-Notebooks möglich.

So gibt es neben Kernels für Julia, Go, Rust und Fortran auch einen für Kotlin. Mit dem Befehl conda install -c jetbrains kotlin-jupyter-kernel lässt der Kotlin-Kernel sich in einer laufenden Jupyter-Installation installieren.

Eine alternative Möglichkeit ist das Erstellen eines Jupyter-Docker-Images mit Kotlin-Kernel, wie folgendes Listing zeigt:

FROM jupyter/base-notebook

USER root

# Install OpenJDK-8
RUN apt-get update && \
    apt-get install -y openjdk-8-jre && \
    apt-get clean;


# Setup JAVA_HOME for docker commandline
ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64/
RUN export JAVA_HOME

# Switch back to unprivileged user
USER $NB_UID

ENV JUPYTER_ENABLE_LAB=yes

# Install kotlin kernel
RUN conda install -c jetbrains kotlin-jupyter-kernel=0.11.0.95

Listing 1: Docker-Image für Jupyter mit Kotlin-Kernel

Nun lässt sich Kotlin im Jupyter-Notebook verwenden und mit dem %use-Keyword steht eine Reihe vorinstallierter Libraries bereit. Neue Libraries lassen sich mit dem Befehl @file:DependsOn(LIBRARY) herunterladen und installieren.

Die Library dataframe gehört zu den bereits vorinstallierten Libraries. Sie ermöglicht das Arbeiten auf strukturierten Daten, wie es aus der Tabellenkalkulationen und von Datenbanken her vertraut ist. Daten wie in diesem Beispiel die Passagierdaten der Titanic lassen sich mit einem einfachen df = DataFrame.read("titanic.csv") aus einer CSV-Datei laden.

Andere Dateiformate wie Excel und JSON lassen sich ebenso einlesen, und es ist auch möglich, hierarchische Strukturen abzubilden, also Zellen und Spalten ineinander zu verschachteln. Während die Daten in einer CSV-Datei zunächst untypisiert sind, weil sie als reine Textdaten vorliegen, werden sie beim Laden in einen Dataframe on-the-fly typisiert.

Dieses automatisch generierte Schema lässt sich mit df.schema() abfragen. Den Dataframe können Teams bereits dazu verwenden, um ihre Daten zu erkunden.

Dazu bietet die dataframe-Library eine mächtige, aber dennoch einfach verständliche API, die in Teilen an bekannte Befehle aus Datenbanken angelehnt ist.

Der Befehl df.select{name and age}.take(10) fragt die Spalten "name" und "age" ab und gibt dabei mit .take(10) nur die ersten zehn Zeilen aus. Jede Operation erzeugt einen neuen Dataframe und gibt ihn zurück. Der Dataframe selbst ist hingegen unveränderlich (immutable), um ein unbeabsichtigtes Verändern der Daten zu verhindern. Folgendes Listing führt weitere Operationen auf dem Dataframe auf:

// Welche Passagiere heissen 'Jones'?
df.filter{name.contains("Jones")}


// Wieviele Passagiere sind wo zugestiegen?
df.groupBy { embarked }
    .aggregate {
        count() into "count"
    }
    
// Fehlende (Null) Daten fuer den Ticket-Preis und das Alter
// auffuellen und dann sortieren
df.fillNulls{ fare and age }
.perCol{ mean() }
.sortBy { fare.desc() }

Listing 2: Datenanalyse mit Kotlin dataframe

dataframe ist ein nützliches Werkzeug zum Erkunden, Analysieren und Säubern von Daten in Jupyter-Notebooks oder auch in regulärem Kotlin-Code.

Viele Zusammenhänge in Daten lassen sich in einer tabellarischen Darstellung gut erkennen. Für manche Trends und Muster gibt es jedoch schnellere und einfachere Wege der Datenvisualisierung wie Lets-Plot.

Diese Plotting-Library für Kotlin verwendet einen Schichten-Ansatz, um Plot-Grafiken zu definieren. Mit jeder hinzugefügten Schicht wird ein anderer Aspekt der Grafik definiert: die verwendeten Daten, die Auswahl der Art des Plots oder die Farben und Formen im Plot.

Um beispielsweise für die Darstellung des Zusammenhangs zwischen Wohnfläche und Preisen von Häusern einen Punkte-Plot zu erstellen, lässt sich folgender Befehl verwenden:

letsPlot(data) {x = "size"; y = "price"} + geomPoint(size=5) + geomSmooth() + ggsize(500, 250)

Dabei gilt es, zunächst die Daten festzulegen und die verwendeten x- und y-Achsen zu definieren.

Mit geomPoint legen wir den Typ des Plots als Punkte-Plot fest, der die Punkte mit einer Größe von 5 Pixel plottet. Um einen geglätteten Mittelwert mit Abweichungen einzuzeichnen, ist noch die geomSmooth-Schicht hinzuzufügen. Als Letztes ist die Größe des Plots mit ggsize(500,250) festzulegen.

Lets-Plot bietet eine Vielzahl an Plot-Typen und Konfigurationsoptionen. Einige Beispiele zur Verwendung sind im folgenden Listing aufgeführt.

// Scatter Plot
letsPlot(data) {
    x = "size"; 
    y = "price"
} + 
geomPoint(size=5) + 
geomSmooth() + 
ggsize(500, 250)


// Dichte Plot mit Kategorien
letsPlot(moreData) + 
    geom_density(alpha=.3) {
        x="rating"; 
        fill="category"
    }

// Linienplot mit Fehlerbalken
letsPlot(study_data) {
    x="learning"; 
    color="group"
} + 
geomErrorBar(width=.1) {
    ymin="points_min"; 
    ymax="points_max"
} +
geomLine {y="points"} +
geomPoint {y="points"}

Listing 3: Plot-Grafiken mit Lets-Plot

Die Integration mit der dataframe-Library ermöglicht eine einfache Verwendung in Data-Exploration-Workflows oder in anderen Kotlin-Projekten.