Blog

Python Summit 2020
Das große Trainingsevent für Python
22. - 24. April 2020 | München
24
Feb

Auf pythonischen Spuren: Schließt Tribuo den Feature-Gap zwischen Python und Java?

Nicht zuletzt ob der breiten Verfügbarkeit von Bibliotheken hat sich Python im Laufe der letzten Jahre als Quasistandard im Bereich Machine Learning etabliert. Logisch, dass man bei Oracle diesem Trend nicht wirklich gern zusah – schließlich und endlich muss Java ja große Verbreitung haben, möchte man am Produkt ernsthaft Geld verdienen. Vor einiger Zeit stellte Oracle deshalb die hauseigene Bibliothek Tribuo unter eine quelloffene Lizenz.

Im Prinzip handelt es sich bei Tribuo um ein ML-System, das den Feature-Gap zwischen Python und Java im Bereich der künstlichen Intelligenz zumindest bis zu einem gewissen Grad schließen helfen soll.

Das unter die (sehr liberale) Apache-Lizenz gestellte Produkt kann laut der Ankündigung auf eine mehrjährige Anwendungsgeschichte innerhalb Oracles zurückblicken. Man erkennt das unter anderem daran, dass die Bibliothek sehr umfangreiche Funktionen anbietet – neben der Erzeugung „eigener“ Modelle gibt es auch Interfaces für verschiedene andere Bibliotheken, darunter auch TensorFlow.

Autor und Redaktion ist klar, dass man Machine Learning nicht in einem Artikel eines Fachmagazins unterbringen kann. Schon ob der Wichtigkeit dieser Vorstellung wollen wir Ihnen allerdings ein wenig zeigen, wie man mit Tribuo herumspielen kann.

Modulare Bibliotheksstruktur

Machine-Learning-Anwendungen zählen normalerweise nicht zu den Aufgabenstellungen, die man auf ressourcenbeschränkten Systemen ausführt – IoT-Edge-Systeme, die ML Payloads ausführen, haben meist einen Hardwarebeschleuniger, wie beispielsweise den SiPeed MAiX. Trotzdem wird die Tribuo-Bibliothek von Oracle modularisiert angeboten, weshalb man als Entwickler in der Theorie nur jene Teile des Projekts in seine Solutions einbinden kann, die man auch wirklich benötigt. Unter [1] findet sich eine Übersicht, welche Funktionen in den einzelnen Paketen bereitgestellt werden.

Wir wollen uns in diesem Einführungsartikel allerdings nicht weiter mit Modularisierung auseinandersetzen, sondern stattdessen ein wenig drauflos programmieren. Eine Tribuo-basierte Anwendung der künstlichen Intelligenz weist im Allgemeinen immer die in Abbildung 1 gezeigte Struktur auf.

Abb. 1: Tribuo versucht, eine Full-Stack-Lösung zur künstlichen Intelligenz zu sein (Bildquelle: Oracle)

Die Abbildung informiert uns darüber, dass Tribuo die Informationen von A bis Z selbst zu verarbeiten sucht. Ganz links steht dabei ein DataSource-Objekt, das die per künstlicher Intelligenz zu verarbeitenden Informationen einsammelt und in das Tribuo-eigene Speicherformat namens Example konvertiert. Diese Example-Objekte werden dann in Form einer Dataset-Instanz vorgehalten, die – im Allgemeinen wie gewohnt – in Richtung eines Modells weiterwandern, das zu guter Letzt Vorhersagen liefert. Eine als Evaluator bezeichnete Klasse kann dann anhand dieser meist recht allgemein bzw. wahrscheinlichkeitsbezogen gehaltenen Informationen konkrete Entscheidungen treffen.

Ein in diesem Zusammenhang interessanter Aspekt des Frameworks ist, dass viele Tribuo-Klassen ein mehr oder weniger generisches System zum Setzen von Konfigurationseinstellungen mitbringen. Im Prinzip wird dabei eine Annotation vor die Attribute gestellt, deren Verhalten angepasst werden kann:

public class LinearSGDTrainer implements Trainer<Label>, WeightedExamples { @Config(description="The classification objective function to use.") private LabelObjective objective = new LogMulticlass();

Das unter [2] im Detail beschriebene Oracle Labs Configuration and Utilities Toolkit (OLCUT) kann diese Informationen dann aus XML-Dateien einlesen – im Fall unseres soeben angelegten Propertys könnte die Parametrierung beispielsweise nach folgendem Schema erfolgen:

<config> <component name="logistic" type="org.tribuo.classification.sgd.linear.LinearSGDTrainer"> <property name="objective" value="log"/>

Sinn dieser auf den ersten Blick akademisch klingenden Vorgehensweise ist, dass das Verhalten von ML-Systemen stark von den in den diversen Elementen enthaltenen Parametern abhängig ist. Durch die Implementierung von OLCUT kann der Entwickler dem User die Möglichkeit geben, diese Einstellungen zu dumpen, oder ein System mit geringem Aufwand in einen definierten Zustand zurückzuversetzen.

Einbindung von Tribuo

Nach diesen einführenden Überlegungen ist es nun an der Zeit, erste Experimente mit Tribuo durchzuführen. Auch wenn der Quellcode der Bibliothek in GitHub zum Selbstkompilieren bereitsteht, empfiehlt sich für erste Gehversuche die Verwendung eines fertigen Pakets.

Oracle unterstützt dabei sowohl Maven als auch Gradle und bietet im Gradle-Sonderfall sogar (teilweise) Unterstützung für Kotlin. Wir wollen in den folgenden Schritten allerdings mit klassischen Werkzeugen arbeiten, weshalb wir uns eine Eclipse-Instanz greifen und diese durch Anklicken von New | New Maven project zum Erzeugen eines neuen, auf Maven basierten Projektskeletts animieren.

Im ersten Schritt des Generators fragt die IDE, ob sie als Archetyp bezeichnete Vorlagen laden wollen. Bitte selektieren Sie die Checkbox Create a simple project (skip archetype selection), um die IDE zum Anlegen eines primitiven Projektskeletts zu animieren. Im nächsten Schritt öffnen wir die Datei pom.xml, um eine Einfügung der Bibliothek nach dem Schema in Listing 1 zu befehligen.

Listing 1

 <name>tribuotest1</name> <dependencies> <dependency> <groupId>org.tribuo</groupId> <artifactId>tribuo-all</artifactId> <version>4.0.1</version> <type>pom</type> </dependency> </dependencies> </project>

Zur Überprüfung der erfolgreichen Einbindung befehligen wir danach eine Rekompilation der Solution – bei bestehender Internetverbindung wird Sie die IDE darüber informieren, dass die benötigten Komponenten aus dem Internet auf Ihre Workstation wandern.

Ob der Nähe zum klassischen ML-Ökosystem dürfte es niemanden verwundern, dass die Tribuo-Beispiele im Großen und Ganzen in Form von Jupiter Notebooks vorliegen – eine insbesondere im Forschungsbereich weitverbreitete Darbietungsform, die für den seriellen Einsatz in der Produktion zumindest nach Meinung des Autors nicht wirklich geeignet ist.

Wir wollen in den folgenden Schritten deshalb auf klassische Java-Programme setzen. Als erstes Problem wollen wir uns dabei der Klassifikation zuwenden. Darunter versteht man in der Welt der ML, dass die angelieferten Informationen in eine Gruppe von als Klasse bezeichneten Kategorien oder Histogramm-Bins eingeordnet werden. Im Bereich des Machine Learnings hat sich eine Gruppe von als Data Set bezeichneten Beispielen (Sample Data Set) etabliert. Dabei handelt es sich um vorgefertigte Datenbanken, die konstant sind und zur Evaluation verschiedener Modellhierarchien und Trainingsstufen herangezogen werden können. In den folgenden Schritten wollen wir auf das unter [3] bereitstehende Iris-Data-Set setzen. Hinter dem Begriff Iris verbirgt sich witzigerweise nicht ein Teil des Auges, sondern eine Pflanzenart.

Freundlicherweise steht das Data Set in einer für Tribuo direkt verwendbaren Form zur Verfügung. Der unter Linux arbeitende Autor öffnet aus diesem Grund im ersten Schritt ein Terminalfenster, in dem er danach ein neues Arbeitsverzeichnis anlegt und die Informationen zu guter Letzt per wget vom Server herunterlädt:

t@T18:~$ mkdir tribuospace t@T18:~$ cd tribuospace/ t@T18:~/tribuospace$ wget https://archive.ics.uci.edu/ml/machine-learning-databases/iris/bezdekIris.data

Als nächsten Akt fügen wir unserem Eclipse-Projektskelett im ersten Schritt eine Klasse hinzu, die eine Main-Methode aufnimmt. Platzieren Sie in Ihr den Code aus Listing 2.

Listing 2

import org.tribuo.classification.Label; import java.nio.file.Paths;   public class ClassifyWorker { public static void main(String[] args) { var irisHeaders = new String[]{"sepalLength", "sepalWidth", "petalLength", "petalWidth", "species"}; DataSource<Label> irisData = new CSVLoader<>(new LabelFactory()).loadDataSource(Paths.get("bezdekIris.data"), irisHeaders[4], irisHeaders);

Die auf den ersten Blick einfach erscheinende Routine hat es in mehrerlei Hinsicht faustdick hinter den Ohren. Erstens bekommen wir es hier mit einer Klasse namens Label zu tun – je nach Konfiguration ihrer Eclipse-Arbeitsumgebung wird die IDE vielleicht sogar Dutzende von Label-Kandidatenklassen anbieten. Wichtig ist, dass Sie sich unbedingt für den hier gezeigten Import org.tribuo.classification.Label entscheiden – ein Label ist in Tribuo eine Katalogisierungskategorie.

Die mit var beginnende Syntax ermöglicht der IDE dann das Erreichen einer aktuellen Java-Version – wer Tribuo (effektiv) verwenden möchte, muss zumindest JDK 8, besser aber JDK 10 einsetzen. Die in dieser Version eingeführte var-Syntax findet sich nämlich in so gut wie jedem Codebeispiel.

Aus der Logik folgt, dass Sie – je nach Systemkonfiguration – an dieser Stelle mehr oder weniger umfangreiche Anpassungen durchführen müssen. Der unter Ubuntu 18.04 arbeitende Autor musste beispielsweise erst ein kompatibles JDK bereitstellen:

tamhan@TAMHAN18:~/tribuospace$ sudo apt-get install openjdk-11-jdk

Beachten Sie, dass Eclipse den neuen Installationspfad mitunter nicht selbsttätig erreichen kann – im vom Autor verwendeten Paket lautete das korrekte Verzeichnis /usr/lib/jvm/java-11-openjdk-amd64/bin/java.

Nach der erfolgreichen Anpassung der Java-Ausführungskonfiguration sind wir jedenfalls in der Lage, unsere Applikation zu kompilieren – eventuell müssen Sie noch das NIO-Paket zur Maven-Konfiguration hinzufügen, weil die Tribuo-Bibliothek im Interesse besserer Performance quer durch den Gemüsegarten auf diese neuartige EA-Bibliothek setzt.

Nachdem wir unser Programmskelett zum Laufen gebracht haben, wollen wir es uns der Reihe nach ansehen, um mehr über den inneren Aufbau von Tribuo-Applikationen zu erfahren. Als Erstes müssen wir uns – denken Sie an Abbildung 1 ganz links – mit dem Laden der Daten auseinandersetzen (Listing 3).

Listing 3

public static void main(String[] args) { try { var irisHeaders = new String[]{"sepalLength", "sepalWidth", "petalLength", "petalWidth", "species"}; DataSource<Label> irisData; irisData = new CSVLoader<>(new LabelFactory()).loadDataSource( Paths.get("/home/tamhan/tribuospace/bezdekIris.data"), irisHeaders[4], irisHeaders);

Der bereitgestellte Datensatz enthält vergleichsweise wenige Informationen – Kopfzeilen und Co. sucht man hier vergebens. Aus diesem Grund müssen wir der CSVLoader-Klasse ein Array übergeben, das Sie über die Spaltennamen des Datensatzes informiert. Die Vergabe von irisHeaders [4] sorgt dann dafür, dass die Zielvariable des Modells separate Bekanntgabe erfährt.

Im nächsten Schritt müssen wir uns darum kümmern, unseren Datensatz in eine Trainings- und eine Testgruppe aufzuteilen. Dabei handelt es sich um ein im Bereich des Machine Learnings durchaus übliches Verfahren: Man teilt die Informationen in zwei Gruppen auf. Die Testdaten dienen dem trainierten Modell dabei zur „Verifikation“, während die eigentlichen Trainingsdaten zur Anpassung und Verbesserung der Parameter dienen. Im Fall unseres Programms wollen wir eine 70-30-Teilung zwischen Trainings- und sonstigen Daten vornehmen, was zu folgendem Code führt:

 var splitIrisData = new TrainTestSplitter<>(irisData, 0.7, 1L); var trainData = new MutableDataset<>(splitIrisData.getTrain()); var testData = new MutableDataset<>(splitIrisData.getTest());

Aufmerksame Leser wundern sich an dieser Stelle, warum der zusätzliche Parameter 1L übergeben wird. Tribuo arbeitet intern mit einem Zufallsgenerator. Wie bei allen oder zumindest den meisten Pseudozufallsgeneratoren gilt auch hier, dass er sich zu einem mehr oder weniger deterministischen Verhalten animieren lässt, wenn man denn als Seed bezeichneten Startwert auf eine Konstante legt. Der Konstruktor der Klasse TrainTestSplitter exponiert dieses Seed-Feld – wir übergeben hier den konstanten Wert eins, um ein reproduzierbares Verhalten der Klasse zu erreichen.

An dieser Stelle sind wir für einen ersten Trainingslauf bereit. Im Bereich des Trainierens von Machine-Learning-basierten Modellen hat sich eine Gruppe von Verfahren herauskristallisiert, die von Entwicklern im Allgemeinen zusammengefasst werden. Der schnellste Weg zu einem lauffähigen Trainingssystem ist die Verwendung der Klasse LogisticRegressionTrainer, die von Haus aus eine Gruppe vordefinierter Einstellungen lädt:

 var linearTrainer = new LogisticRegressionTrainer(); Model<Label> linear = linearTrainer.train(trainData);

Der Aufruf der Methode Train kümmert sich dann darum, dass das Framework den Trainingsprozess auslöst und sich für das Ausgeben von Vorhersagen bereitmacht. Aus der Logik folgt, dass unsere nächste Aufgabe das Anfordern einer derartigen Vorhersage ist, die im nächsten Schritt in Richtung eines Evaluators weitergeleitet wird. Zu guter Letzt müssen wir seine Ergebnisse dann noch in Richtung der Kommandozeile ausgeben:

Prediction<Label> prediction = linear.predict(testData.getExample(0)); LabelEvaluation evaluation = new LabelEvaluator().evaluate(linear,testData); double acc = evaluation.accuracy(); System.out.println(evaluation.toString());

An dieser Stelle ist unser Programm für einen ersten kleinen Testlauf bereit – Abbildung 2 zeigt, wie sich die Ergebnisse des Iris-Data-Sets auf der Workstation des Autors präsentieren.

Abb. 2: Es geht: Machine-Learning ohne Python!

Mehr über Elemente erfahren

Nachdem wir unseren kleinen Tribuo-Klassifikator im ersten Schritt zur Durchführung einer Klassifikation gegen das Iris-Data-Set befähigt haben, wollen wir uns einige fortgeschrittene Eigenschaften und Funktionen der verwendeten Klassen im Detail ansehen.

Interessantes Feature Numero eins ist, die Arbeit der TrainTestSplitter-Klasse zu überprüfen. Hierzu reicht es aus, folgenden Code an eine bequem zugängliche Stelle in der Main-Methode zu platzieren:

System.out.println(String.format("data size = %d, num features = %d, num classes = %d",trainingDataset.size(),trainingDataset.getFeatureMap().size(),trainingDataset.getOutputInfo().size()));

Das Data Set exponiert eine Gruppe von Member-Funktionen, die zusätzliche Informationen über die in ihnen enthaltenen Tupel ausgeben. Die Ausführung des hier vorliegenden Codes würde beispielsweise darüber informieren, wie viele Datensätze, wie viele Features und wie viele zu unterteilende Klassen vorliegen.

Die Klasse LogisticRegressionTrainer ist außerdem nur eines von mehreren Verfahren, über das Sie das Training des Modells durchführen können. Möchten wir stattdessen auf den CART-Prozess setzen, so bietet sich eine Anpassung nach folgendem Schema an:

var cartTrainer = new CARTClassificationTrainer(); Model<Label> tree = cartTrainer.train(trainData); Prediction<Label> prediction = tree.predict(testData.getExample(0)); LabelEvaluation evaluation = new LabelEvaluator().evaluate(tree,testData);

Wenn Sie das Programm mit der geänderten Klassifikation abermals ausführen, öffnet sich ein neues Konsolenfenster mit zusätzlichen Informationen – es zeigt sich, dass Tribuo auch zur Exploration von Machine-Learning-Prozessen geeignet ist. Angesichts des vergleichsweise einfachen und eindeutigen Aufbaus des Data Sets kommt es übrigens bei beiden Klassifikatoren zum selben Ergebnis.

Erkennen nach Zahlen

Ein weiterer im ML-Bereich weit verbreiteter Datensatz ist das MNIST-Handschrift-Sample. Dahinter verbirgt sich eine von der MNIST durchgeführte Erhebung des Verhaltens, wie Personen Zahlen schreiben. Aus der Logik folgt, dass sich ein derartiges Modell dann beispielsweise zur Erkennung von Postleitzahlen einspannen lässt – ein Verfahren, das zum Beispiel bei der Sortierung von Briefsendungen wertvolle Mannstunden und somit naturgemäß Geld sparen hilft.

Da wir uns soeben mit grundlegender Klassifizierung anhand eines mehr oder weniger synthetischen Datensatzes auseinandergesetzt haben, wollen wir im nächsten Schritt mit diesen doch etwas realeren Informationen arbeiten. Auch sie müssen im ersten Schritt auf die Workstation wandern, was unter Linux abermals durch einen Aufruf von wget erfolgt:

tamhan@TAMHAN18:~/tribuospace$ wget http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz tamhan@TAMHAN18:~/tribuospace$ wget http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz

Interessant ist an der hier vorgegebenen Struktur vor allem, dass jedes wget-Kommando zwei Dateien herunterlädt – sowohl die Trainings- als auch die eigentlichen Arbeitsdateien liegen in separaten Archiven je mit Label- und Bildinformationen vor.

Das MNIST-Data-Set liegt von Haus aus in einem als IDX bezeichneten Datenformat vor, dass in vielen Fällen per GZIP-Kompression komprimiert und somit einfacher zu handhaben ist. Wer die unter [4] bereitstehende Dokumentation der Tribuo-Bibliothek studiert, stellt fest, dass mit der Klasse IDXDataSource ein dafür vorgesehenes Ladewerkzeug zur Verfügung steht, das die Daten bei Bedarf sogar on the fly dekomprimiert.

Aus der Logik folgt, dass unsere nächste Aufgabe das Integrieren der IDXDataSource-Klasse in unseren Programmworkflow darstellt (Listing 4).

Listing 4

public static void main(String[] args) { try { LabelFactory myLF = new LabelFactory(); DataSource<Label> ids1; ids1 = new IDXDataSource<Label>(Paths.get("/home/tamhan/tribuospace/t10k-images-idx3-ubyte.gz"), Paths.get("/home/tamhan/tribuospace/t10k-labels-idx1-ubyte.gz"), myLF);

Aus technischer Sicht unterscheidet sich die IDXDataSource nicht wesentlich von der weiter oben verwendeten CSVDataSource. Von besonderem Interesse ist nur, dass wir hier zwei Dateipfade übergeben, weil das MNIST-Data-Set ja in Form eines separaten Trainings- und Arbeitsdatensatzes vorliegt.

Mit anderen IO-Bibliotheken aufgewachsene Entwickler haben beim Konstruktor übrigens eine kleine Anfängerfalle zu beachten: Die in Tribuo enthaltenen Klassen erwarten durch die Bank nicht einen String, sondern schon eine fertige Path-Instanz. Diese lässt sich allerdings durch den unbürokratischen Aufruf von Paths.get() erzeugen.

Sei dem, wie es sei, die nächste Aufgabe unseres Programms ist die Erzeugung einer zweiten DataSource, die sich nun um die Trainingsdaten kümmert. Ihr Konstruktor unterscheidet sich deshalb – logischerweise – nur im Dateinamen für die Quellinformationen (Listing 5).

Listing 5

DataSource<Label> ids2; ids2 = new IDXDataSource<Label>(Paths.get("/home/tamhan/tribuospace/train-images-idx3-ubyte.gz"), Paths.get("/home/tamhan/tribuospace/train-labels-idx1-ubyte.gz"), myLF); var trainData = new MutableDataset<>(ids1); var testData = new MutableDataset<>(ids2); var evaluator = new LabelEvaluator();

Die meisten in Tribuo enthaltenen Datenklassen sind in Bezug auf das Ausgabeformat zumindest bis zu einem gewissen Grad generisch. Das betrifft uns hier insofern, als wir permanent eine LabelFactory übergeben müssen, um den Konstruktor über das gewünschte Ausgabeformat und die zu seiner Erzeugung zu verwendenden Factory-Klasse zu informieren.

Als nächste Aufgabe kümmern wir uns darum, die Informationen in Data Sets umzuwandeln und einen Emulator sowie eine CARTClassificationTrainer-Trainingsklasse ins Leben zu rufen:

var cartTrainer = new CARTClassificationTrainer(); Model<Label> tree = cartTrainer.train(trainData);

Der Rest des Codes – hier aus Platzgründen nicht abgedruckt – ist dann eine Eins-zu-eins-Abwandlung der weiter oben verwendeten Modellverarbeitung. Bringen Sie das Programm auf der Workstation zur Ausführung, sehen Sie das in Abbildung 3 gezeigte Ergebnis. Wundern Sie sich übrigens nicht, wenn sich Tribuo hier etwas Zeit lässt – der MNIST-Datensatz ist ja doch etwas umfangreicher als der in oben verwendeter Tabelle.

Abb. 3: Für einen ersten Anlauf nicht so schlecht: die Ergebnisse unseres CART-Classifiers

Da die erreichte Genauigkeit doch nicht allzu großartig ist, wollen wir hier noch einen anderen Trainieralgorithmus verwenden. Der in Abbildung 1 gezeigte und auf Standard-Interfaces basierende Aufbau von Tribuo hilft uns an dieser Stelle insofern, als eine derartige Änderung mit vergleichsweise wenig Code vor sich gehen kann.

Als Ziel wollen wir diesmal die Klasse LinearSGDTrainer verwenden, die wir durch den Befehl import org.tribuo.classification.sgd.linear.LinearSGDTrainer; in das Programm einbinden. Die explizite Erwähnung desselben ist übrigens keine Pedanterie des Verlags – neben der hier verwendeten Classification-Klasse gibt es nämlich auch einen LinearSGDTrainer, der allerdings für Regressionsaufgaben vorgesehen ist und uns an dieser Stelle nicht weiterhelfen kann.

Da die LinearSGDTrainer-Klasse keinen Convenience-Konstruktor mitbringt, der den Trainer mit einer Gruppe von (mehr oder weniger gut funktionierenden) Standardwerten initialisiert, müssen wir hier kleine Änderungen vornehmen:

var cartTrainer = new LinearSGDTrainer(new LogMulticlass(), SGD.getLinearDecaySGD(0.2),10,200,1,1L); Model<Label> tree = cartTrainer.train(trainData);

An dieser Stelle ist auch diese Version des Programms einsatzbereit – da LinearSGD mehr Rechenleistung benötigt, wird die Abarbeitung noch etwas mehr Zeit in Anspruch nehmen.

Exkurs: Automatische Konfiguration

LinearSGD mag ein durchaus leistungsfähiger Algorithmus des maschinellen Lernens sein – der Konstruktor ist allerdings erstens lang und ob der hohen Parameterzahl insbesondere ohne IntelliSense-Unterstützung nur schwer ersichtlich.

Da man bei ML-Systemen im Allgemeinen mit einem Data Scientist zusammenarbeitet, dessen Java-Kenntnisse meist eher eingeschränkt sind, wäre es schön, wenn man die Modellinformationen als JSON- oder XML-Datei anbieten könnte.

An dieser Stelle schlägt die Stunde des weiter oben erwähnten OLCUT-Konfigurationssystems. Wenn wir es in unsere Solution einbinden, können wir die Parametrisierung der Klasse nach dem in Listing 6 gezeigten Schema in JSON vornehmen.

Listing 6

"name" : "cart", "type" : "org.tribuo.classification.dtree.CARTClassificationTrainer", "export" : "false", "import" : "false", "properties" : { "maxDepth" : "6", "impurity" : "gini", "seed" : "12345", "fractionFeaturesInSplit" : "0.5" }

Schon auf den ersten Blick ist auffällig, dass Parameter wie der für den Zufallsgenerator zu verwendenden Samen hier direkt ansprechbar sind – die Zeile „seed“ : „12345“ sollte auch jemand finden, der mit Java schwer herausgefordert ist.

OLCUT ist dabei übrigens nicht auf das lineare Erzeugen von Objektinstanzen beschränkt – das System ist auch in der Lage, verschachtelte Klassenstrukturen zu erzeugen. In unserem soeben abgedruckten Markup ist das Attribut „impurity“ : „gini“ ein exzellentes Beispiel dafür – es lässt sich nach folgendem Schema ausdrücken, um eine Instanz der GiniIndex-Klasse zu generieren:

"name" : "gini", "type" : "org.tribuo.classification.dtree.impurity.GiniIndex",

Haben wir uns einer derartigen Konfigurationsdatei bemächtigt, können wir wie folgt eine Instanz der ConfigurationManager-Klasse heraufbeschwören:

ConfigurationManager.addFileFormatFactory(new JsonConfigFactory()) String configFile = "example-config.json"; String.join("\n",Files.readAllLines(Paths.get(configFile)))

OLCUT ist im Bereich der verwendeten Dateiformate agnostisch – die eigentliche Logik für das Einlesen der im Dateisystem bzw. im Konfigurationsfile vorliegenden Daten lässt sich durch eine Adapterklasse einschreiben. Im Fall unseres vorliegenden Codes verwenden wir das JSON-Dateiformat, weshalb wir über die Methode addFileFormatFactory eine Instanz von JsonConfigFactory anmelden.

Als Nächstes können wir auch schon damit beginnen, die Elemente unserer Maschine-Learning-Applikation unter Nutzung von ConfigurationManager zu parametrieren:

var cm = new ConfigurationManager(configFile); DataSource<Label> mnistTrain = (DataSource<Label>) cm.lookup("mnist-train");

Die lookup-Methode übernimmt dabei einen String, der gegen die in der Wissensbasis befindlichen Name-Attribute verglichen wird. Sofern das Konfigurationssystem an dieser Stelle ein Match findet, beschwört es automatisch die in der Datei beschriebene Klassenstruktur.

ConfigurationManager ist dabei extrem leistungsfähig. Oracle bietet unter [5] ein umfangreiches Beispiel an, dass das Konfigurationssystem – leider wieder unter Verwendung von Jupiter Notebooks – zur Erzeugung einer komplexen Machine-Learning-Toolchain einspannt.

Diese auf den ersten Blick sinnlose erscheinende Maßnahme ist nach Meinung des Autors übrigens durchaus sinnvoll. Machine-Learning-Systeme leben und sterben nämlich sowohl mit der Qualität der Trainingsdaten als auch mit der Parametrisierung – lassen sich diese Parameter bequem extern ansprechen, so ist es einfacher, das System an die vorliegenden Bedürfnisse anzupassen.

Gruppierung mit synthetischen Datensätzen

Im Bereich des Machine Learnings gibt es auch bei der Auswahl des Verfahrens eine Gruppe universeller Vorgehensweisen, die sich auf verschiedene Detailprobleme gleichermaßen anwenden lassen. Nachdem wir uns bisher mit der Klassifikation auseinandergesetzt haben, folgt nun das Clustering. Dabei steht, wie in Abbildung 4 gezeigt, der Gedanke im Mittelpunkt, einer wilden Datenhorde Gruppen einzuschreiben, um Trends oder Zusammengehörigkeit leichter erkennen zu können.

Abb. 4: Die Einschreibung der Trennlinien teilt den Datenbestand (Bildquelle: Wikimedia Commons/ChiRe)

Aus der Logik folgt, dass wir auch für Regressionsexperimente eine Datenbasis benötigen. Tribuo hilft uns an dieser Stelle mit der ClusteringDataGenerator-Klasse, die die aus der Mathematik und Wahrscheinlichkeitsrechnung bekannte Gaußsche Verteilung zur Erzeugung von Testdatensätzen einspannt. Fürs Erste wollen wir nach folgendem Schema zwei Testdatenfelder bevölkern:

public static void main(String[] args) { try { var data = ClusteringDataGenerator.gaussianClusters(500, 1L); var test = ClusteringDataGenerator.gaussianClusters(500, 2L);

Die als zweiter Parameter übergebenen Zahlen legen dabei fest, welche Zahl als Initialwert für den Zufallsgenerator herangezogen werden soll. Da wir hier zwei verschiedene Werte übergeben, wird der PRNG zwei zwar von der Folge hier unterschiedliche Zahlenfolgen generieren, die allerdings immer den Regeln der Gaußschen Normalverteilung folgen.

Im Bereich der Clustering-Verfahren hat sich der K-Means-Prozess sehr gut etabliert. Schon aus diesem Grund wollen wir ihn in unserem Tribuo-Beispiel abermals einsetzen. Wer die Struktur des verwendeten Codes sorgfältig ansieht, bemerkt, dass der standardisierte Aufbau auch hier zum Vorschein kommt:

var trainer = new KMeansTrainer(5,10,Distance.EUCLIDEAN,1,1); var model = trainer.train(data); var centroids = model.getCentroidVectors(); for (var centroid : centroids) { System.out.println(centroid); }

Von besonderem Interesse ist hier eigentlich nur der übergebene Distance-Wert, der festlegt, welche Methode zur Berechnung bzw. Gewichtung der zweidimensionalen Abstände zwischen den Elementen verwendet werden soll. Er ist insofern kritisch, als die Tribuo-Bibliothek mehrere Distance Enums mitbringt – die von uns benötigte liegt im Namespace org.tribuo.clustering.kmeans.KMeansTrainer.Distance.

Nun bietet sich eine abermalige Ausführung des Programms an – Abbildung 5 zeigt die generierten Mittelpunktmatrizen.

Abb. 5: Tribuo informiert uns über den Aufenthaltsort der Cluster

Die ebenfalls zu sehenden roten Meldungen sind Fortschrittsberichte: Der Großteil der in Tribuo enthaltenen Klassen enthält Logik, um den User bei lang laufenden Prozessen in der Konsole über den Fortschritt zu informieren. Da diese Operationen allerdings ebenfalls Rechenleistung verbrauchen, gibt es in vielen Konstruktoren eine Möglichkeit zur Beeinflussung der Arbeitsfrequenz der Ausgabelogik.

Sei dem wie es sei: Zur Beurteilung der Ergebnisse ist es hilfreich, die von der Funktion ClusteringDataGenerator verwendeten Gauß-Parameter zu kennen. Oracle verrät sie witzigerweise nur im Tribuo-Tutorial-Beispiel, die Werte für die drei Parametervariablen lauten jedenfalls folgendermaßen:

N([ 0.0,0.0], [[1.0,0.0],[0.0,1.0]]) N([ 5.0,5.0], [[1.0,0.0],[0.0,1.0]]) N([ 2.5,2.5], [[1.0,0.5],[0.5,1.0]]) N([10.0,0.0], [[0.1,0.0],[0.0,0.1]]) N([-1.0,0.0], [[1.0,0.0],[0.0,0.1]])

Da eine genaue Besprechung der im Hintergrund stattfindenden mathematischen Prozesse den Rahmen dieses Artikels sprengen würde, wollen wir die Bewertung der Ergebnisse stattdessen ebenfalls an Tribuo abtreten. Werkzeug der Wahl ist hier – logischerweise – abermals ein auf dem Evaluator-Prinzip basierendes Element. Da wir uns diesmal aber mit Clustering auseinandersetzen, sieht der notwendige Code folgendermaßen aus:

ClusteringEvaluator eval = new ClusteringEvaluator(); var mtTestEvaluation = eval.evaluate(model,test); System.out.println(mtTestEvaluation.toString());

Wenn Sie das vorliegende Programm zur Ausführung bringen, bekommen Sie ein für Menschen lesbares Ergebnis zurückgeliefert – die Tribuo-Bibliothek kümmert sich darum, die in ClusteringEvaluator enthaltenen Ergebnisse für eine bequeme Ausgabe auf der Kommandozeile oder im Terminal vorzubereiten:

Clustering Evaluation Normalized MI = 0.8154291916732408 Adjusted MI = 0.8139169342020222

Exkurs: Schneller, wenn parallelisiert

Aufgaben der künstlichen Intelligenz neigen dazu, immense Mengen an Rechenleistung zu verbrauchen – wer sie nicht parallelisiert, hat das Nachsehen.

Teile der Tribuo-Bibliothek sind von Oracle von Haus aus mit dem nötigen Tooling ausgestattet, das die zu erledigenden Aufgaben automatisch auf mehrere Kerne einer Workstation verteilt.

Der hier vorliegende Trainer ist ein exzellentes Beispiel dafür. Als erste Aufgabe wollen wir nach folgendem Schema dafür sorgen, dass sowohl die Trainings- als auch die Nutzdaten wesentlich umfangreicher ausfallen:

var data = ClusteringDataGenerator.gaussianClusters(50000, 1L); var test = ClusteringDataGenerator.gaussianClusters(50000, 2L);

Im nächsten Schritt reicht es aus, nach dem folgenden Schema einen Wert im Konstruktor der KMeansTrainer-Klasse zu verändern – wer hier acht übergibt, weist die Engine dazu an, acht Prozessorkerne gleichzeitig unter Feuer zu nehmen:

var trainer = new KMeansTrainer(5,10,Distance.EUCLIDEAN,8,1);

An dieser Stelle können Sie das Programm abermals zum Test freigeben – bei Überwachung des Gesamtrechenleistungsverbrauchs mit einem Werkzeug wie Top sollten Sie ein kurzes Aufblitzen der Auslastung aller Kerne sehen.

Regression mit Tribuo

Spätestens an dieser Stelle sollte auffallen, dass einer der wichtigsten USPs der Tribuo-Bibliothek ihre Fähigkeit ist, mehr oder weniger unterschiedliche Verfahren der künstlichen Intelligenz über ein gemeinsames, in Abbildung 1 gezeigtes Denk-, Prozess- und Implementierungsschema abzubilden. Als letzte Aufgabe in diesem Bereich wollen wir uns der Regression zuwenden – darunter versteht man in der Welt der künstlichen Intelligenz die Analyse eines Datensatzes, um den Zusammenhang zwischen Variablen in einem Eingangsset und einem Ausgangsset zu ermitteln. Witzigerweise handelt es sich dabei – Stichwort: frühe neuronale Netzwerke im Bereich der KI für Spiele – und den Bereich, den man als nichtinitiierter Entwickler am meisten von KI erwartet.

Für diese Aufgabe wollen wir ein Weinqualitäts-Data-Set verwenden: Das unter [6] im Detail bereitstehende Daten-Sample setzt Weinbewertungen in Korrelation zu diversen chemischen Analysewerten. Sinn des entstehenden Systems ist also, nach Fütterung mit diesen chemischen Informationen ein – mehr oder weniger genaues – Urteil darüber abzugeben, welche Qualität bzw. welche Bewertungen der erzeugte Wein am Ende erhalten wird.

Erste Aufgabe ist wie immer das Bereitstellen der Testdaten, das wir (natürlich) abermals per wget erledigen:

tamhan@TAMHAN18:~/tribuospace$ wget https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv

Unsere erste Amtshandlung ist auch hier das Laden der soeben bereitgestellten Beispielinformationen – eine Aufgabe, die hier wieder über eine CSVLoader-Klasse erfolgt:

try { var regressionFactory = new RegressionFactory(); var csvLoader = new CSVLoader<>(';',regressionFactory); var wineSource = csvLoader.loadDataSource(Paths.get("/home/tamhan/tribuospace/winequality-red.csv"),"quality");

Neu gegenüber dem bisher besprochenen Code ist zweierlei: Erstens bekommt der CSVLoader nun zusätzlich ein Semikolon als Parameter übergeben. Dieser String informiert ihn darüber, dass die vorgelegte Beispieldatei ein „spezielles“ Datenformat aufweist, in dem die einzelnen Informationen nicht wie üblich durch Kommata getrennt sind. Besonderheit Numero zwei ist, dass wir als Factory-Instanz nun eine Instanz von RegressionFactory verwenden – die bisher verwendete LabelFactory ist für Regressionsanalysen nicht wirklich geeignet.

Das Wein-Data-Set ist nicht in Trainings- und Arbeitsdaten unterteilt. Aus diesem Grund kommt die von zuvor bekannte TrainTestSplitter-Klasse zu neuen Ehren; wir nehmen abermals 30 Prozent Trainings- und 70 Prozent Nutzinformationen an:

var splitter = new TrainTestSplitter<>(wineSource, 0.7f, 0L); Dataset<Regressor> trainData = new MutableDataset<>(splitter.getTrain()); Dataset<Regressor> evalData = new MutableDataset<>(splitter.getTest());

Im nächsten Schritt benötigen wir logischerweise abermals einen Trainer und einen Evaluator (Listing 7).

Listing 7

var trainer = new CARTRegressionTrainer(6); Model<Regressor> model = trainer.train(trainData); RegressionEvaluator eval = new RegressionEvaluator(); var evaluation = eval.evaluate(model,trainData); var dimension = new Regressor("DIM-0",Double.NaN); System.out.printf("Evaluation (train):%n RMSE %f%n MAE %f%n R^2 %f%n", evaluation.rmse(dimension), evaluation.mae(dimension), evaluation.r2(dimension));

Neu ist an diesem Code zweierlei: Erstens müssen wir nun einen Regressor anlegen, der einen der im Datensatz enthaltenen Werte als relevant markiert. Zweitens führen wir hier schon im Rahmen der Modellvorbereitung eine Evaluation gegen den zum Training verwendeten Datensatz durch. Diese Vorgehensweise kann zwar zu Übertraining führen, liefert bei intelligenter Anwendung aber durchaus interessante Parameter.

Führen Sie unser Programm im vorliegenden Zustand aus, sehen Sie in der Kommandozeile die folgende Ausgabe:

Evaluation (train): RMSE 0.545205 MAE 0.406670 R^2 0.544085

RMSE und MAE sind dabei beides Parameter, die die Güte des Modells beschreiben. Für beide gilt, dass ein geringerer Wert (logischerweise) auf ein genauer arbeitendes Modell hinweist.

Als letzte Aufgabe müssen wir uns dann noch darum kümmern, eine weitere Evaluation durchzuführen, die nun aber nicht mehr die Trainingsdaten als Vergleich bekommt. Hierzu müssen wir einfach die an die Evaluate-Methode übergebenen Werte anpassen:

evaluation = eval.evaluate(model,evalData); dimension = new Regressor("DIM-0",Double.NaN); System.out.printf("Evaluation (test):%n RMSE %f%n MAE %f%n R^2 %f%n", evaluation.rmse(dimension), evaluation.mae(dimension), evaluation.r2(dimension));

Lohn der Mühen ist das in Abbildung 6 gezeigte Schirmbild – da wir nun ja nicht mehr die Originaltrainingsdaten evaluieren, ist die Genauigkeit des entstandenen Systems schlechter geworden.

Abb. 6: Der Test gegen reale Daten liefert weniger genaue Ergebnisse

Dieses Programmverhalten ist allerdings logisch: Wer ein Modell gegen neue Daten trainiert, bekommt naturgemäß schlechtere Daten, als wenn er es mit einer schon vorhandenen Informationsquelle kurzschließt. Achten Sie an dieser Stelle allerdings darauf, dass Übertraining insbesondere im finanzwirtschaftlichen Bereich ein klassisches Antipattern ist, das mehr als nur einen algorithmischen Trader in der Vergangenheit viel Geld gekostet hat.

Damit sind wir am Ende unserer Reise durch die Welt der künstlichen Intelligenz mit Tribuo angekommen. Die Bibliothek unterstützt als viertes Design Pattern auch noch die Anomalieerkennung, die wir hier aber nicht auch noch besprechen wollen. Unter [7] findet sich ein kleines, durchgearbeitetes Tutorial – der Unterschied zwischen den drei bisher vorgestellten Verfahren und der neuen Methode besteht allerdings nur darin, dass die neue Methode abermals mit anderen Klassen arbeitet.

Fazit

Hand aufs Herz: Wer sich gut mit Java auskennt, kann Python – zwar unter fluchen, aber doch – problemlos lernen: Es handelt sich also mehr um eine Frage des Nichtwollens als um ein Problem des Nichtkönnens.

Andererseits steht auch außer Frage, dass sich Java Payloads in diverse Enterprise-Prozesse und Enterprise-Toolchains wesentlich einfacher einbinden lassen als ihre Python-basierten Kollegen. Die Verwendung der Tribuo-Bibliothek schafft an dieser Stelle schon insofern Abhilfe, als man sich nicht mit dem manuellen Makeln der Werte zwischen den Python- und den Java-Teilen der Applikation herumärgern muss.

Wenn Oracle die Dokumentation ein wenig verbessern und noch mehr Beispiele anbieten würde, spräche absolut nichts gegen das System. So gilt, dass die Umstiegshürde etwas höher ist: Wer noch nie mit ML gearbeitet hat, lernt die Grundlagen mit Python schneller, weil er mehr schlüsselfertige Beispiele findet. Andererseits gilt allerdings auch, dass die Arbeit mit Tribuo ob der diversen Komfortfunktionen in vielen Fällen schneller von der Hand geht. Unterm Strich gelingt Oracle mit Tribuo also ein durchaus großer Wurf, der Java – zumindest aus Sicht eines Ecosystem Plays – eine gute Chance im Bereich des Machine Learnings freischaufeln sollte.

Tam Hanna befasst sich seit der Zeit des Palm IIIc mit der Programmierung und Anwendung von Handcomputern. Er entwickelt Programme für diverse Plattformen, betreibt Onlinenewsdienste zum Thema und steht für Fragen, Trainings und Vorträge gern zur Verfügung.
Alle Updates zum Python Summit!