//

Wie man Java-Klassen in Python benutzt

15.11.2021 | 8 Minuten Lesezeit

Generell sollte man zwar für jedes Problem das passende Werkzeug nutzen. Aber oftmals wird man gezwungen, den Hammer Java zu nutzen, weil der Rest des Hauses mit diesem Hammer gebaut wurde. Eine moderne Lösung dieses Problems ist natürlich die Microservice-Architektur: unabhängige Microservices, die je eine Aufgabe erledigen und in der jeweils am besten passenden Sprache geschrieben sind.

Aber was tun, wenn der Monolith bereits besteht oder das Projekt nicht groß genug ist, um die hohe Komplexität von Microservices zu rechtfertigen? Nun, für diesen Fall, in dem hohe Kopplung unvermeidbar oder sogar erwünscht ist, möchte ich hier eine Herangehensweise vorstellen. Wir werden lernen, wie wir das Machine-Learning-Ökosystem von Python verwenden, um Reinforcement Learning auf ein in Java implementiertes System anzuwenden. Das in Python trainierte Modell können wir später wieder in Java laden und benutzen. Python wird hier also nur während des Trainings verwendet und nicht im Produktiveinsatz. Der Vorteil ist, dass sich der Data Scientist freut, seine liebsten Werkzeuge verwenden zu können.

Und da es um Python geht: Welches Beispielproblem würde sich besser eignen als das klassische Spiel Snake? (Die Antwort auf diese rhetorische Frage ist vermutlich: „Eine Anspielung auf Monty Python.“ Aber mir ist kein simples Problem eingefallen, das sich um einen fliegenden Zirkus dreht.)

Der komplette Quellcode unseres Beispiels ist auf GitHub verfügbar.

Snake in Java

Unsere Ausgangssituation ist, dass wir ein Java-Programm haben, in dem die Spiellogik von Snake implementiert ist: Es ist immer ein Stück Futter auf dem Spielfeld. Wenn die Schlange Futter erreicht, wird sie länger und neues Futter erscheint. Wenn die Schlange eine der Wände oder sich selbst beißt, ist das Spiel zuende.

Unser Ziel ist es, ein neuronales Netz zu trainieren, das die Schlange so steuert, dass sie möglichst lang ist, bevor sie einen Fehler macht und das Spiel vorbei ist. Dazu brauchen wir einen Tensor, der den aktuellen Zustand des Spiels darstellt und als Input in das neuronale Netz gefüttert wird, damit es daraus den besten nächsten Schritt vorhersagt. Um dieses Beispiel simpel zu halten, ist unser Tensor nur ein Vektor mit sieben Elementen, die entweder 0 oder 1 sein können: Die ersten vier signalisieren, ob das Futter rechts, links, vor oder hinter der Schlange ist und die nächsten drei Werte signalisieren, ob das Feld links, geradeaus oder rechts von einer Wand oder einem Teil der Schlange besetzt sind.

1public class SnakeLogic {
2    Coordinate head; // position of the snake's head
3    Coordinate food; // position of the food
4    Move headDirection; // direction in which the head points
5 
6    public boolean[] trainingState() {
7        boolean[] state = new boolean[7];
8 
9        // get the angle from the head to the food,
10        // depending on the direction of movement `headDirection`
11        double alpha = angle(head, headDirection, food);
12 
13        state[0] = isFoodFront(alpha);
14        state[1] = isFoodLeft(alpha);
15        state[2] = isFoodRight(alpha);
16        state[3] = isFoodBack(alpha);
17 
18        // check if there is danger on these sites
19        state[4] = danger(head.left(headDirection));
20        state[5] = danger(head.straight(headDirection));
21        state[6] = danger(head.right(headDirection));
22 
23        return state;
24    }
25 
26    // omitted other fields and methods for clarity
27    // find them at https://github.com/surt91/autosnake
28}
29

Einerseits müssen wir diese Methode während des Trainings des neuronales Netzes von Python aus aufrufen können. Andererseits benötigen wir sie auch später im Produktiveinsatz in unserem Java-Programm, um dem fertig trainierten Netz eine Entscheidungsgrundlage zu liefern.

Java-Klassen in Python

Hier kommt JPype ins Spiel! Das Importieren einer Klasse aus Java — ohne dass wir die Java-Seite des Codes anfassen müssten — gelingt einfach durch:

1import jpype
2import jpype.imports
3from jpype.types import *
4 
5# launch the JVM
6jpype.startJVM(classpath=['../target/autosnake-1.0-SNAPSHOT.jar'])
7 
8# import the Java module
9from me.schawe.autosnake import SnakeLogic
10 
11# construct an object of the `SnakeLogic` class ...
12width, height = 10, 10
13snake_logic = SnakeLogic(width, height)
14 
15# ... and call a method on it
16print(snake_logic.trainingState())
17

JPype startet dabei eine eigene JVM im selben Prozess, der auch Python ausführt, und lässt das Python-Programm mit ihr über das Java Native Interface (JNI) kommunizieren. Das kann man sich, etwas vereinfacht, so vorstellen wie das Aufrufen von Funktionen
aus dynamischen Bibliotheken (für eingefleischte Pythonistas ist möglicherweise der Vergleich mit dem Modul ctypes hilfreich). JPype macht dies allerdings sehr komfortabel, indem es die Abbildung von Java- und Python-Klassen aufeinander transparent übernimmt.

Es sei jedoch noch erwähnt, dass es überraschend viele Projekte mit diesem Ziel und unterschiedlichen Stärken, Schwächen und Anwendungsgebieten gibt. Stellvertretend seien Jython und Py4J erwähnt:

Jython führt einen Python-Interpreter direkt in der JVM aus, sodass die gleichen Datenstrukturen effizient von Python und Java aus manipuliert werden können. Allerdings bringt das gleichzeitig Einschränkungen mit sich, was die Nutzung nativer Python-Bibliotheken angeht — da wir numpy und tensorflow nutzen wollen, scheidet diese Option also aus.

Py4J steht eher auf der anderen Seite des Spektrums. Auf der Java-Seite startet es einen Socket, über den es mit der Python-Seite kommuniziert. Der Vorteil ist, dass sich beliebig viele Python-Prozesse mit einem lang laufenden Java-Prozess verbinden können — oder umgekehrt ein Python-Prozess mit vielen JVMs, sogar über das Netzwerk. Der Nachteil ist, dass die Kommunikation über den Socket vergleichsweise langsam ist.

Das Training

Nun, da wir aus Python Zugriff auf unsere Java-Klassen haben, können wir das Deep-Learning-Framework unserer Wahl — hier Keras — nutzen, um ein Modell zu erstellen und zu trainieren. Da wir in diesem Fall eine Schlange trainieren wollen, möglichst
viele Punkte zu sammeln, werden wir einen Reinforcement-Learning-Ansatz anwenden.

Reinforcement Learning bedeutet grundsätzlich, dass wir einen Agenten mit einem Environment interagieren lassen, ihn für gute Entscheidungen belohnen und für schlechte bestrafen. Diese Disziplin sorgt häufiger für Aufsehen, beispielsweise
durch das Spielen von klassischen Atari-Spielen oder Go .

Für unseren Fall bietet es sich an, ein Trainings-Environment zu schreiben, das sich eng an den Gyms von OpenAI orientiert, da diese für Reinforcement-Learning einen Quasi-Standard darstellen.

Dafür brauchen wir zunächst eine Methode step, die eine Aktion action entgegennimmt, einen Zeitschritt simuliert und das Ergebnis der Aktion zurückgibt. Die action ist dabei der Output des neuronalen Netzes und bestimmt, ob die Schlange sich nach links oder rechts dreht oder sich weiter geradeaus bewegt. Das zurückgegebene Ergebnis besteht aus

  1. state, dem neuen Zustand (unser siebener Vektor),
  2. reward, der Bewertung der Aktion: 1 wenn die Schlange Futter gefressen hat, -1 wenn die Schlange sich selbst oder eine Wand gebissen hat und sonst 0. Und
  3. done, ob die Partie vorbei ist, also ob die Schlange sich selbst oder eine Wand gebissen hat. Sowie
  4. einem Dictionary mit Debugging-Informationen, das wir in unserem Fall einfach leer lassen.

Außerdem benötigen wir eine Methode reset, um eine neue Partie zu starten, die ebenfalls den neuen Zustand zurückgibt.

Beide Methoden können wir dank unserer existierenden Java-Klasse sehr einfach schreiben:

1import jpype
2import jpype.imports
3from jpype.types import *
4 
5# Launch the JVM
6jpype.startJVM(classpath=['../target/autosnake-1.0-SNAPSHOT.jar'])
7 
8# import the Java module
9from me.schawe.autosnake import SnakeLogic
10 
11 
12class Snake:
13    def __init__(self):
14        width, height = 10, 10
15        # `snakeLogic` is a Java object, such that we can call
16        # all its methods. This is also the reason why we
17        # name it in camelCase instead of the snake_case
18        # convention of Python.
19        self.snakeLogic = SnakeLogic(width, height)
20 
21    def reset(self):
22        self.snakeLogic.reset()
23 
24        return self.snakeLogic.trainingState()
25 
26    def step(self, action):
27        self.snakeLogic.turnRelative(action)
28        self.snakeLogic.update()
29 
30        state = self.snakeLogic.trainingState()
31 
32        done = False
33        reward = 0
34        if self.snakeLogic.isGameOver():
35            reward = -1
36            done = True
37        elif self.snakeLogic.isEating():
38            reward = 1
39 
40        return state, reward, done, {}
41

Diese Trainingsumgebung können wir nun mit minimalem Aufwand in das erste Beispiel aus der Keras-Dokumentation für Reinforcement Learning einbauen und das leicht angepasste Skript direkt nutzen, um mit dem Training zu beginnen:


Spätestens seit Rocky wissen wir, dass ein Training nur mit eineer Trainings-Montage gut ist.

Die Schlange lernt tatsächlich dazu! Innerhalb weniger Minuten läuft sie zielstrebig auf das Futter zu und weicht Wänden aus — allerdings fängt sie sich gerne selbst. Für unsere Zwecke soll dieses Verhalten aber vorerst ausreichen.

Modell in Java laden

Um den Kreis zu schließen, laden wir unser trainiertes Modell mit deeplearning4j in Java …

1// https://deeplearning4j.konduit.ai/deeplearning4j/how-to-guides/keras-import
2public class Autopilot {
3    ComputationGraph model;
4 
5    public Autopilot(String pathToModel) {
6        try {
7            model = KerasModelImport.importKerasModelAndWeights(pathToModel, false);
8        } catch (Exception e) {
9            e.printStackTrace();
10        }
11    }
12 
13    // infer the next move from the given state
14    public int nextMove(boolean[] state) {
15        INDArray input = Nd4j.create(state).reshape(1, state.length);
16        INDArray output = model.output(input)[0];
17 
18        int action = output.ravel().argMax().getInt(0);
19 
20        return action;
21    }
22}
23

… wo wir die selben Methoden aufrufen, die wir während des Training genutzt haben, um die Schlange zu steuern.

1public class SnakeLogic {
2    Autopilot autopilot = new Autopilot("path/to/model.h5");
3 
4    public void update() {
5        int action = autopilot.nextMove(trainingState());
6        turnRelative(action);
7 
8        // rest of the update omitted
9    }
10 
11    // further methods omitted
12}
13

Fazit

Unter dem Strich ist es also überraschend einfach Java und Python gemeinsam zu nutzen, was vor allem zur Prototypen-Entwicklung sehr effizient sein kann.

Und es muss nicht direkt Deep Learning sein. Durch die sehr einfache Anwendbarkeit gibt es sicherlich auch Potential, diesen Ansatz zu wählen, um etwas explorative Datenanalyse auf der Datenbank unter Verwendung der gesamten Geschäftslogik in einem iPython Notebook zu betreiben.

Was unser Anwendungsbeispiel angeht: Dafür, dass wir keinerlei Gedanken in das Modell gesteckt haben, ist das Ergebnis überraschend gut. Für bessere Ergebnisse müsste man vermutlich das ganze Spielfeld in das neuronale Netz füttern und wir müssten uns etwas mehr Gedanken über das Modell machen. Eine kurze Google-Recherche zeigt, dass es anscheinend Modelle gibt, die ein perfektes Spiel Snake spielen können, sodass jedes einzelne Feld belegt ist. Für Snake ist es möglicherweise jedoch sinnvoller, das neuronale Netz zwischen den Ohren zu verwenden, um eine perfekte Strategie zu entwickeln. Zum Beispiel wird es immer ein perfektes Spiel, wenn die Schlange sich immer auf einem Hamilton-Pfad (ein Pfad, der alle Gitterplätze, ausgenommen die von der Schlange belegten, genau einmal besucht) zwischen Kopf und Schwanzende bewegt. Wie man effizient diese Hamilton-Pfade findet, ist dem Leser als Übung überlassen.

Beitrag teilen

Gefällt mir

0

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.