Künstliche Intelligenz, Machine Learning, Deep Learning

Maschinelles Lernen mit Java: Eine Einführung

Lars Schwabe

@istock/Vladyslav-Otsiatsia

Experten für Maschinelles Lernen sind gefragter denn je – spätestens seit Amazons personalisierter Online-Werbung und Spracherkennungsdiensten wie Siri, Google Now oder Cortana. Grund genug, sich das Thema auf JAXenter diese Woche etwas genauer vorzunehmen. Zu Beginn führt Lars Schwabe in das Thema ein: Wie funktioniert Machine Learning eigentlich, welche Frameworks helfen bei der Umsetzung, wohin geht der Trend in Sachen Künstlicher Intelligenz?

Maschinelles Lernen: Eine Einführung

Man muss kein Prophet sein, um den Einzug von Künstlicher Intelligenz und maschinellem Lernen (ML) in unseren Alltag vorherzusagen. Wir nutzen sie bereits heute aktiv, z. B. die Spracherkennung auf Mobiltelefonen, Spam-Filter oder die Gesichtserkennung bei der Verwaltung unserer Fotos. Oft sind wir auch im Kontakt zu ML-Systemen, ohne es zu wissen, etwa bei der personalisierten Online-Werbung. Und Chatbots sind voraussichtlich (leider) nicht mehr aufzuhalten.

Für den Neu- und Quereinsteiger mit Entwicklerhintergrund ist ML ein zunächst unübersichtliches Feld, weil es viele verschiedene konzeptuelle, methodische und theoretische Ansätze gibt. Und was ist mit den offenbar notwendigen Statistik- und Mathematikkenntnissen? Die müssen bei Bedarf auch nachgerüstet oder aufgefrischt werden. Ist die Einstiegsbarriere für Entwickler deshalb nicht viel zu hoch?

Nein, im Gegenteil. Entwickler spielen eine Schlüsselrolle beim Überführen von ML-Systemen aus der Forschungs- und Bastelecke in Produktivsysteme, die echte Mehrwerte für Nutzer bringen.

Cover_JM8_16_v5-212x300Mehr zum Thema
Das aktuelle Java Magazin beschäftigt sich ausführlich mit dem Themen-Komplex Machine Learning, Deep Learning, Künstliche Intelligenz. Lars Schwabe geht detailliert auf verschiedene ML-Mechanismen ein, Marcel Florian und Steffen Heinzl beschreiben die Funktionsweise von IBM Bluemix.
Alle Infos zum Heft finden Sie hier.

Maschinelles Lernen mit Java

Wie bei jeder Technologie bietet es sich an, zunächst ein sehr einfaches System zu verstehen. Für ML ist dies das Perzeptron. Ein Perzeptron ist eine Lernmaschine, die für eine Eingabe x eine binäre Ausgabe y liefert, und zwar durch Auswerten einer Funktion y=f(x). Die Eingaben repräsentieren Objekte, die es in zwei Klassen einzuteilen gilt. Ein gelerntes Perzeptron für zweidimensionale Eingaben ist exemplarisch durch die Java-Klasse in Listing 1 implementiert.

package me.schwabe.examples.javamag;

public class Perceptron {
    
private final static int DEFAULTDIM = 2;
    
private final float weights[];
    
public Perceptron() {
        this(DEFAULTDIM);
    }
    
public Perceptron(int dim) {
        this(new float[dim]);
    }
    
public Perceptron(float[] weights) {
        this.weights = weights;
    }
    
public float[] getWeights() { return weights; }
    
public int getDim() { return weights.length; };
    
public boolean isPositive(float[] input) throws IllegalArgumentException {
        if (input.length != getDim())
            throw new IllegalArgumentException("Dimensionality mismatch.");

        float sum = 0;
        for (int i=0; i<input.length; i++) { sum += weights[i] * input[i]; } return sum > 0f;
    }

    public boolean learn(float lrate, float[] input, boolean label) {
        boolean doLearning = isPositive(input) != label;
        if (doLearning) {
            float sign = isPositive(input) ? -1.0f : 1.0f;
            for (int i=0; i<input.length; i++) {
                weights[i] += lrate * input[i] * sign;
            }
            normalizeWeightVector();
        }
        return doLearning;
    }

    private void normalizeWeightVector() {
        double norm = 0f;
        for (int i=0; i<weights.length; i++) {
            norm += weights[i] * weights[i];
        }
        norm = Math.sqrt(norm);
        for (int i=0; i<weights.length; i++) {
            weights[i] /= norm;
        }
    }
}


Die Verwendung eines gelernten Perzeptrons wird durch die JUnit-Tests in Listing 2 demonstriert.

package me.schwabe.examples.javamag;

import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;

public class PerceptronTest {

    @Test
    public void perceptronShoudCorrectlyComputeOutput() {
        Perceptron p = new Perceptron( new float[] {1.0f, 0.0f} );
        assertTrue("Point 1 misclassified.", p.isPositive(new float[] {0.5f, 0.5f}));
        assertFalse("Point 2 misclassified.", p.isPositive(new float[] {-0.5f, 0.5f}));
        assertFalse("Point 3 misclassified.", p.isPositive(new float[] {-500f, -500f}));
    }

}


Der Code in Listing 1 nimmt eine Multiplikation der Parameter wi mit den Eingaben xi vor, bevor in Abhängigkeit des Vorzeichens die binäre Ausgabe errechnet wird. Dies erscheint vielleicht etwas willkürlich, lässt sich aber geometrisch anschaulich interpretieren, wie in Abbildung 1 gezeigt.

Das gelernte Perzeptron entscheidet eigentlich nur, ob ein Datenpunkt auf der einen oder der anderen Seite einer Geraden liegt. Wenn wir mit mehr als zwei Dimensionen bei den Eingaben arbeiten, spricht man nicht mehr von Geraden, sondern allgemein von Hyperebenen, die durch die Parametervektor w des gelernten Perzeptrons definiert sind. Genauer: Wenn wir die Parameter (oft auch Gewichte genannt) als Vektor interpretieren, steht dieser Vektor senkrecht auf der Hyperebene. Erinnerungen an die Schulmathematik können hier hilfreich sein.

schwabe_ml_1

Abb.1: Geometrische Interpretation der Perzeptron-Entscheidungsfunktion. Der Gewichtsvektor steht senkrecht auf der Trennungsebene.

Das Lernen eines Perzeptrons besteht nun darin, Werte für den Parametervektor w auf Grundlage eines Datensatzes mittels eines Lernalgorithmus zu bestimmen. Ein Datensatz besteht hier aus Eingabevektoren zusammen mit der Klassenzuordnung. Für jeden Eingabevektor ist also bekannt, ob er zu der zu lernenden Klasse des Perzeptrons gehören soll oder nicht. Dieses Szenario wird auch überwachtes Lernen genannt, weil für jeden Eingabevektor die Zielgröße bekannt ist.

Der Code in Listing 3 zeigt, wie das Lernen mittels des sogenannten Perzeptron-Lernalgorithmus geschieht. Die Klasse PerceptronTeacher wird mit einem Datensatz initialisiert. Der Aufruf der Methode createPerceptron liefert dann ein fertig gelerntes und einsatzbereites Perzeptron zurück, das nun auch auf neue Daten angewendet werden kann.

package me.schwabe.examples.javamag;

public class PerceptronTeacher {

    private static final int MINPOINTS = 3;

    private final float[][] inputs;
    private final boolean[] labels;

    private final static float LRATE = 0.1f;

    public PerceptronTeacher() {
        this( new float[][] {{0f, 0f}}, new boolean[] {true} );
    }

    public PerceptronTeacher(float[][] inputs, boolean[] labels) {
        if (inputs.length<MINPOINTS) throw new IllegalArgumentException("We need at least " + MINPOINTS + " one data point"); this.inputs = inputs; this.labels = labels; } public int getDim() { return inputs[0].length; } public Perceptron createPerceptron() { Perceptron p = new Perceptron(new float[] {1.0f, 0.0f}); int noOfErrors = inputs.length; while (noOfErrors>0) {
            noOfErrors = 0;
            for (int i=0; i<inputs.length; i++) {
                noOfErrors += p.learn(LRATE, inputs[i], labels[i]) ? 1 : 0;
            }
            System.out.println(noOfErrors);
        }

        return p;
    }

}
...
package me.schwabe.examples.javamag;

import org.junit.Test;

public class PerceptronTeacherTest {

    private static final float[][] testInputs1 = { {1.0f, 1.0f}, {1.0f, -1.0f}, {-1.0f, 1.0f}, {-1.0f, -1.0f} };
    private static final boolean[] testLabels1 = {true, true, false, false};

    private static final float[][] testInputs2 = { {1.0f, 1.0f}, {1.0f, -1.0f}, {-1.0f, 1.0f}, {-1.0f, -1.0f} };
    private static final boolean[] testLabels2 = {false, false, true, true};

    @Test
    public void perceptronShoudLearn() {
        PerceptronTeacher teacher1 = new PerceptronTeacher(testInputs1, testLabels1);
        Perceptron p1 = teacher1.createPerceptron();

        PerceptronTeacher teacher2 = new PerceptronTeacher(testInputs2, testLabels2);
        Perceptron p2 = teacher2.createPerceptron();
    }

}



Dieses einfache Beispiel zeigt bereits die wesentliche Architektur von ML-Systemen. Ein gelerntes ML-Modell (Klasse Perceptron) ist durch Parameter beschrieben (Perceptron.weights), deren Werte durch einen Lernalgorithmus (Klasse PerceptronTeacher) auf Basis eines Datensatzes bestimmt werden. Das Lernen kann in Cloud-basierten Backend-Systemen geschehen. Die Systeme in Produktion benötigen nur den Parametervektor w für den Betrieb.

Künstliche neuronale Netze

Künstliche neuronale Netze (Artificial Neural Networks, ANNs) sind eine konsequente Weiterentwicklung des Perzeptrons. Sie bestehen aus mehreren künstlichen Neuronen, die meist in Schichten organisiert werden. Die Ausgabesignale von Neuronen einer Schicht sind die Eingabesignale von Neuronen in der folgenden Schicht (Abbildung 2).

schwabe_ml_3

Abb. 2: ANNs mit mehreren Schichten und typische Transferfunktionen

Hat ein einzelnes Neuron N freie Parameter, dann hat ein ANN mit M Neuronen M x N freie Parameter. Während des Lernens gilt es nun, diese M x N freien Parameter zu bestimmen.

Und genau das übernimmt wieder ein Lernalgorithmus. Nachdem die Parameterwerte bestimmt sind, können diese persistiert und für einen Produktiveinsatz als „gelerntes ANN“ verteilt werden, z. B. an Geräte mit viel weniger Rechenpower als es zum Lernen notwendig ist, wie eingebettete Systeme mit stromsparenden Prozessoren. Die Anwendung eines gelernten ANNs auf ein Eingabemuster ist dabei recht einfach: Die Ausgaben der Neurone in der letzten (oder „obersten“) Schicht sind die Ausgabe des gesamten Netzes. Um sie zu berechnen, müssen die Ausgaben der Neurone in der vorgelagerten Schicht berechnet werden, usw.

Ein Meilenstein der ANNs war die Entwicklung des sogenannten Backpropagation-Algorithmus [1], der in vielen Update-Schritten die Parameterwerte eines ANNs für einen gegebenen Trainingsdatensatz anpasst. Die Details dieses Algorithmus sollen uns hier nicht beschäftigen. Die Struktur des Backpropagation-Algorithmus ist aber ähnlich wie das Lernen beim Perzeptron: Ein Trainingsdatensatz wird Datenpunkt für Datenpunkt iteriert, und die Parameter werden jeweils ein klein wenig angepasst (siehe Kasten).

Details für mathematisch interessierte und vorbelastete

Die Anwendung von ML läuft oft darauf hinaus, eine Kostenfunktion ED für den Datensatz D aufzustellen, die dann mit Optimierungsalgorithmen minimiert wird. Bei einem Regressionsproblem sei der Datensatz als Menge von N Paaren gegeben, D = { (x1, y1), (x2, y2), …, (xN, yN)}. Wobei xi und yi, 1 ≤ i ≤ N, hier der Einfachheit halber reelle Zahlen sein sollen.
Oft wird dann der quadratische Fehler definiert als ED(w) = ½ Σ1 ≤ i ≤ N [f(xi; w) – yi]2, um die Qualität von Netzwerkausgaben für gegebene Eingaben zu quantifizieren. Dieser Fehler ist eine Funktion der Gewichte w im ANN, wobei w ein Vektor ist. Die Funktion f(xi; w) berechnet die Ausgabe des ANNs in Abhängigkeit der Eingabe xi und der Gewichte w im ANN. Ziel ist das Finden von Gewichten w, sodass der Fehler minimiert wird.
Eine solche Minimierung wird beim Backpropagation-Algorithmus mittels Gradientenabstieg vorgenommen, d. h. es wird die Ableitung d/dwk der Fehlerfunktion ED(w) nach einem Gewicht wk (ein Eintrag in w) berechnet. Dann werden die Gewichte in einem Update-Schritt unter Verwendung des Gradienten w (ein Vektor von der gleichen Dimension wie w) geändet: wneu = walt – α w’. Hier ist α die Lernrate, d. h. eine Zahl wie beispielsweise 0,01. Wird der Fehler von einem Update-Schritt zum nächste wieder größer, hat man das Minimum überschritten.

Hier könnte man das Lernen auch abbrechen und das bis dahin ermittelte wneu als das gelernte ANN verwenden.Wenn man den Gradienten für ANNs ausrechnet, bei denen die Neurone in Schichten organisiert sind und nur Verbindungen zu nachfolgenden Schichten aufbauen, ergibt sich eine Auswertereihenfolge von Ausdrücken, die man interpretieren kann, als ob sich die künstlichen Neurone Nachrichten hin- und her senden. Das Problem dabei ist, dass das Optimierungsproblem in der Regel sehr viele lokale Minima hat und man beim Optimieren nicht weiß, ob es nicht noch bessere Werte von w gibt. Eine rechentechnische Schwierigkeit ist es, dass bei ANNs mit vielen Schichten der Gradient aufgrund der beim Ableiten anzuwendenden Kettenregel (dies führt zu Produkten mit vielen Faktoren) sehr, sehr klein werden kann (oder auch sehr, sehr groß) und das ANN damit dann nicht mehr lernt. Dieses Problem hatte erstmals Sepp Hochreiter in seiner Informatik-Diplomarbeit sauber formalisiert herausgearbeitet. Das war 1991. Und es ist als wichtige Einsicht noch immer hochaktuell. Methodische Fortschritte müssen sich mit diesem Problem auseinandersetzen. Es kann nicht durch brachiale Rechenpower gelöst werden.

Deep Learning

Deep Learning steht inzwischen stellvertretend für modernes und leistungsfähiges ML. Unternehmen wie Microsoft, Google und Facebook setzen auf Deep Learning und haben hier massiv investiert. Die Bekanntheit von Deep Learning ist größtenteils auf diese massiven Investitionen zurückzuführen. Aber was ist Deep Learning? Überspitzt formuliert: Deep Learning ist der neue Name für ANNs. Aber warum nennen wir es denn nicht weiterhin ANNs? Neben Marketing-Gründen ist eines nicht von der Hand zu weisen: Deep Learning funktioniert! Es löst das Versprechen der ANNs ein, aus komplexen Daten wie Sprach-, Text-, Bild- oder Videosignalen mittels Lernalgorithmen Strukturen zu extrahieren, um sie beispielsweise für Sprach- und Objekterkennung zu nutzen.

Der Grund dafür ist, dass Deep Learning in einigen vermeintlich nebensächlichen Details über die herkömmliche Praxis mit ANNs hinausgeht, unter anderem die Nutzung anderer Transferfunktionen und einige Heuristiken beim Lernen. Ein ganz offensichtlicher Grund ist jedoch schwer zu übersehen. Im Vergleich zu den 80er und 90er Jahren des letzten Jahrhunderts können wir beim Lernen inzwischen sehr große Datenmengen und viel mehr Rechenpower nutzen.

Derzeit gibt es mehrere leistungsfähige Bibliotheken und Frameworks, um Deep Learning auf Daten anzuwenden. Zu nennen sind hier auf jeden Fall TensorFlow, Theano, Caffe, CNTK und DL4J [2-6]. Zwar gibt es für die JVM einige relevante ML-Frameworks, mit jeweils eignen und unterschiedlichen Abstraktionen, wie die auf Spark basierenden spark.ml [7], sowie die alten Schlachtrösser Weka [8] und Mallet [9]. Bis auf DL4J setzt aber keiner der anderen Deep-Learning-Kandidaten auf die JVM als Plattform. Stattdessen wird auf native Implementierungen und Python zum Rapid Prototyping gesetzt.

Um diese Frameworks sinnvoll zu vergleichen, hängt es davon ab, was genau eigentlich ein Projekt erreichen soll. Auf jeden Fall wird aber die Unterstützung von Grafik-Prozessoren und das Lernen auf Clustern ein Kriterium auf jeder Checkliste sein. Wer über mehr Daten iterieren und mehr Netze lernen kann, hat ganz klar einen Vorteil. Auch die Einbindung in existierende Hadoop/Spark-Infrastrukturen kann ein Thema sein, weil in Unternehmen inzwischen oft in Hadoop-basierten Data Lakes die Datenschätze liegen, die es mittels Deep Learning zu heben gilt. Genau dies stellt DL4J als Alleinstellungsmerkmal heraus. Für den Java-Entwickler kann es uneingeschränkt zumindest als ersten Schritt in die Welt des Deep Learnings empfohlen werden.

Verwandte Themen:

Geschrieben von
Lars Schwabe
Lars Schwabe
Lars Schwabe hat an der TU Berlin in Informatik promoviert. Er leitet derzeit das Data Insight Lab im Technology Innovation Center der Lufthansa Industry Solutions, wo Data Scientists und ML-Experten für Industrieunternehmen neue datengetriebene Lösungen entwickeln.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: