Ein Leitfaden für Deeplearning4j

1. Einführung

In diesem Artikel erstellen wir ein einfaches neuronales Netzwerk mit der Bibliothek deeplearning4j (dl4j) - einem modernen und leistungsfähigen Werkzeug für maschinelles Lernen.

Bevor wir beginnen, ist es nicht erforderlich, dass dieser Leitfaden keine fundierten Kenntnisse der linearen Algebra, der Statistik, der Maschinellen Lerntheorie und vieler anderer Themen erfordert, die für einen fundierten ML-Ingenieur erforderlich sind.

2. Was ist Deep Learning?

Neuronale Netze sind Rechenmodelle, die aus miteinander verbundenen Knotenschichten bestehen.

Knoten sind neuronenähnliche Prozessoren numerischer Daten. Sie nehmen Daten von ihren Eingaben, wenden diesen Daten einige Gewichtungen und Funktionen an und senden die Ergebnisse an die Ausgänge. Ein solches Netzwerk kann mit einigen Beispielen der Quelldaten trainiert werden.

Beim Training werden im Wesentlichen einige numerische Zustände (Gewichtungen) in den Knoten gespeichert, die sich später auf die Berechnung auswirken. Trainingsbeispiele können Datenelemente mit Merkmalen und bestimmten bekannten Klassen dieser Elemente enthalten (z. B. „dieser Satz von 16 × 16 Pixeln enthält einen handgeschriebenen Buchstaben„ a “).

Nach Abschluss des Trainings kann ein neuronales Netzwerk ​​ Informationen aus neuen Daten ableiten, auch wenn diese Daten vorher noch nicht gesehen wurden . Ein gut modelliertes und gut ausgebildetes Netzwerk kann Bilder, handgeschriebene Briefe und Sprache erkennen, statistische Daten verarbeiten, um Ergebnisse für Business Intelligence zu erzielen, und vieles mehr.

In den letzten Jahren wurden tiefe neuronale Netze möglich, mit dem Fortschritt des Hochleistungs-und Parallel-Computing. Solche Netzwerke unterscheiden sich von einfachen neuronalen Netzwerken dadurch, dass sie aus mehreren zwischenliegenden (oder verborgenen) Schichten bestehen . Diese Struktur ermöglicht es Netzwerken, Daten auf viel kompliziertere Weise (rekursiv, wiederkehrend, konvolutionell usw.) zu verarbeiten und daraus mehr Informationen zu extrahieren.

3. Einrichten des Projekts

Um die Bibliothek verwenden zu können, benötigen Sie mindestens Java 7. Außerdem funktioniert es aufgrund einiger nativer Komponenten nur mit der 64-Bit-JVM-Version.

Bevor wir mit dem Leitfaden beginnen, prüfen wir, ob die Anforderungen erfüllt sind:

$ java -version
java version "1.8.0__131"
Java(TM) SE Runtime Environment (build 1.8.0__131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

Zuerst fügen wir die erforderlichen Bibliotheken unserer Maven-Datei pom.xml hinzu

Wir extrahieren die Version der Bibliothek in einen Eigenschafteneintrag (die neueste Version der Bibliotheken finden Sie unter https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org . deeplearning4j% 22[Maven Central](Repository):

<properties>
    <dl4j.version>0.9.1</dl4j.version>
</properties>

<dependencies>

    <dependency>
        <groupId>org.nd4j</groupId>
        <artifactId>nd4j-native-platform</artifactId>
        <version>${dl4j.version}</version>
    </dependency>

    <dependency>
        <groupId>org.deeplearning4j</groupId>
        <artifactId>deeplearning4j-core</artifactId>
        <version>${dl4j.version}</version>
    </dependency>
</dependencies>

Beachten Sie, dass die nd4j-native-platform -Abhängigkeit eine von mehreren verfügbaren Implementierungen ist.

Es basiert auf nativen Bibliotheken, die für viele verschiedene Plattformen verfügbar sind (Mac OS, Windows, Linux, Android usw.). Das Backend könnte auch auf nd4j-cuda-8.0-platform umgestellt werden, wenn Berechnungen auf einer Grafikkarte ausgeführt werden sollen, die das CUDA-Programmiermodell unterstützt.

4. Daten vorbereiten

4.1. Vorbereiten der DataSet-Datei

Wir schreiben die "Hallo Welt" des maschinellen Lernens - Klassifizierung des https://en.wikipedia.org/wiki/Iris flower data__set[iris Blumendatensatz]. Dies ist ein Satz von Daten, die aus den Blumen verschiedener Arten (Iris setosa, Iris versicolor und Iris virginica) gesammelt wurden.

Diese Arten unterscheiden sich in Länge und Breite der Blütenblätter und Kelchblätter. Es wäre schwierig, einen genauen Algorithmus zu schreiben, der ein Eingabedatenelement klassifiziert (d. H. Bestimmt, zu welcher Art eine bestimmte Blume gehört). Ein gut ausgebildetes neuronales Netzwerk kann es jedoch schnell und mit kleinen Fehlern klassifizieren.

Wir werden eine CSV-Version dieser Daten verwenden, wobei die Spalten 0..3 die verschiedenen Merkmale der Spezies und die Spalte 4 die Klasse des Datensatzes oder die Spezies enthalten, die mit einem Wert von 0, 1 oder 2 codiert sind:

5.1,3.5,1.4,0.2,0
4.9,3.0,1.4,0.2,0
4.7,3.2,1.3,0.2,0
...
7.0,3.2,4.7,1.4,1
6.4,3.2,4.5,1.5,1
6.9,3.1,4.9,1.5,1
...

4.2. Vektorisieren und Lesen der Daten

Wir codieren die Klasse mit einer Nummer, da neuronale Netzwerke mit Zahlen arbeiten. ** Die Umwandlung von Datenelementen aus der realen Welt in Zahlenreihen (Vektoren) wird als Vektorisierung bezeichnet.

Lassen Sie uns zuerst diese Bibliothek verwenden, um die Datei mit den vektorisierten Daten einzugeben. Beim Erstellen des CSVRecordReader können wir die Anzahl der zu überspringenden Zeilen (z. B. wenn die Datei eine Kopfzeile hat) und das Trennsymbol (in unserem Fall ein Komma) angeben:

try (RecordReader recordReader = new CSVRecordReader(0, ',')) {
    recordReader.initialize(new FileSplit(
      new ClassPathResource("iris.txt").getFile()));

   //...
}

Um die Datensätze zu durchlaufen, können wir eine der zahlreichen Implementierungen der DataSetIterator -Schnittstelle verwenden. Die Datensätze können sehr umfangreich sein, und die Möglichkeit, die Werte zu speichern oder im Cache zu speichern, kann sich als nützlich erweisen.

Unser kleines Dataset enthält jedoch nur 150 Datensätze. Lesen Sie also mit einem Aufruf von iterator.next () alle Daten gleichzeitig in den Speicher.

  • Wir geben auch den Index der Klassenspalte an, der in unserem Fall der Feature-Anzahl (4) entspricht, sowie die Gesamtzahl der Klassen ** (3).

Beachten Sie außerdem, dass die Datenmenge neu gemischt werden muss, um die Klassenreihenfolge in der Originaldatei zu beseitigen.

Wir geben einen konstanten zufälligen Startwert (42) anstelle des Standardaufrufs System.currentTimeMillis () an, damit die Ergebnisse der Umstellung immer gleich sind. Dadurch können wir jedes Mal, wenn wir das Programm ausführen, stabile Ergebnisse erzielen:

DataSetIterator iterator = new RecordReaderDataSetIterator(
  recordReader, 150, FEATURES__COUNT, CLASSES__COUNT);
DataSet allData = iterator.next();
allData.shuffle(42);

4.3. Normalisieren und Aufteilen

Eine andere Sache, die wir mit den Daten vor dem Training machen sollten, ist die Normalisierung. Die Normalisierung ist ein zweiphasiger Prozess:

  • Erfassung einiger Statistiken zu den Daten (fit)

  • die Daten auf irgendeine Weise ändern (transformieren), um sie einheitlich zu machen

  • Die Normalisierung kann für verschiedene Datentypen abweichen. **

Wenn wir beispielsweise Bilder unterschiedlicher Größe verarbeiten möchten, sollten wir zuerst die Größenstatistik erfassen und dann die Bilder auf eine einheitliche Größe skalieren.

Normalisierung bedeutet für Zahlen aber normalerweise, sie in eine sogenannte Normalverteilung umzuwandeln. Die NormalizerStandardize -Klasse kann uns dabei helfen:

DataNormalization normalizer = new NormalizerStandardize();
normalizer.fit(allData);
normalizer.transform(allData);

Jetzt, da die Daten vorbereitet sind, müssen wir den Satz in zwei Teile aufteilen.

Der erste Teil wird in einer Trainingseinheit verwendet. Wir werden den zweiten Teil der Daten (die das Netzwerk überhaupt nicht sehen würde) verwenden, um das trainierte Netzwerk zu testen.

Dies würde uns erlauben zu überprüfen, ob die Klassifizierung korrekt funktioniert.

Wir nehmen 65% der Daten (0,65) für das Training und den Rest 35% für die Prüfung:

SplitTestAndTrain testAndTrain = allData.splitTestAndTrain(0.65);
DataSet trainingData = testAndTrain.getTrain();
DataSet testData = testAndTrain.getTest();

5. Vorbereiten der Netzwerkkonfiguration

5.1. Fluent Configuration Builder

Jetzt können wir eine Konfiguration unseres Netzwerks mit einem fancy fluent builder erstellen:

MultiLayerConfiguration configuration
  = new NeuralNetConfiguration.Builder()
    .iterations(1000)
    .activation(Activation.TANH)
    .weightInit(WeightInit.XAVIER)
    .learningRate(0.1)
    .regularization(true).l2(0.0001)
    .list()
    .layer(0, new DenseLayer.Builder().nIn(FEATURES__COUNT).nOut(3).build())
    .layer(1, new DenseLayer.Builder().nIn(3).nOut(3).build())
    .layer(2, new OutputLayer.Builder(
      LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
        .activation(Activation.SOFTMAX)
        .nIn(3).nOut(CLASSES__COUNT).build())
    .backprop(true).pretrain(false)
    .build();

Trotz dieser vereinfachten, fließenden Art, ein Netzwerkmodell zu erstellen, gibt es eine Menge zu verdauen und viele Parameter zu optimieren. Lassen Sie uns dieses Modell aufschlüsseln.

5.2. Netzwerkparameter einstellen

  • Die Builder-Methode iterations () gibt die Anzahl der Optimierungsiterationen an. **

Die iterative Optimierung bedeutet, dass der Trainingssatz mehrere Durchläufe durchführt, bis das Netzwerk zu einem guten Ergebnis konvergiert.

Normalerweise verwenden wir beim Training mit realen und großen Datensätzen mehrere Epochen (vollständige Durchgänge von Daten durch das Netzwerk) und eine Iteration für jede Epoche. Da unser anfänglicher Datensatz jedoch minimal ist, werden wir eine Epoche und mehrere Iterationen verwenden.

  • Die activation () ist eine Funktion, die innerhalb eines Knotens ausgeführt wird, um die Ausgabe zu bestimmen. **

Die einfachste Aktivierungsfunktion wäre linear f (x) = x. Es stellt sich jedoch heraus, dass nur nichtlineare Funktionen es Netzwerken ermöglichen, komplexe Aufgaben mit wenigen Knoten zu lösen.

Es gibt viele verschiedene Aktivierungsfunktionen, die wir im org.nd4j.linalg.activations.Activation -Enum nachschlagen können. Bei Bedarf können wir auch unsere Aktivierungsfunktion schreiben. Wir verwenden jedoch die bereitgestellte hyperbolische Tangensfunktion (tanh).

  • Die Methode weightInit () gibt eine der vielen Möglichkeiten zum Einrichten der anfänglichen Gewichte für das Netzwerk an. ** Richtige anfängliche Gewichte können die Ergebnisse des Trainings erheblich beeinflussen. Ohne zu viel mit der Mathematik zu tun, setzen wir sie auf eine Form der Gaußschen Verteilung ( WeightInit.XAVIER ), da dies normalerweise eine gute Wahl für den Start ist.

Alle anderen Methoden zur Gewichtsinitialisierung können im org.deeplearning4j.nn.weights.WeightInit -Enum nachgeschlagen werden.

  • Die Lernrate ** ist ein entscheidender Parameter, der die Lernfähigkeit des Netzwerks entscheidend beeinflusst.

Wir könnten viel Zeit damit verbringen, diesen Parameter in einem komplexeren Fall zu optimieren. Für unsere einfache Aufgabe verwenden wir jedoch einen ziemlich signifikanten Wert von 0,1 und richten ihn mit der Builder-Methode learningRate () ein.

  • Eines der Probleme beim Training neuronaler Netze ist ein Fall von Überanpassung ** , wenn ein Netzwerk die Trainingsdaten "speichert".

Dies geschieht, wenn das Netzwerk zu hohe Gewichte für die Trainingsdaten einstellt und bei anderen Daten schlechte Ergebnisse liefert.

  • Um dieses Problem zu lösen, richten wir L2-Regularisierung ** mit der Zeile .regularization (true) .l2 (0.0001) ein. Die Regularisierung „bestraft“ das Netzwerk bei zu großen Gewichten und verhindert Überanpassung.

5.3. Erstellen von Netzwerkebenen

Als Nächstes erstellen wir ein Netzwerk aus dichten (auch als vollständig verbundenen) Schichten.

Die erste Schicht sollte dieselbe Anzahl von Knoten enthalten wie die Spalten in den Trainingsdaten (4).

Die zweite dichte Schicht enthält drei Knoten. Dies ist der Wert, den wir variieren können, aber die Anzahl der Ausgaben in der vorherigen Ebene muss gleich sein.

Die endgültige Ausgabeebene sollte die Anzahl der Knoten enthalten, die der Anzahl der Klassen entspricht (3). Die Struktur des Netzwerks ist im Bild dargestellt:

Nach erfolgreichem Training haben wir ein Netzwerk, das über seine Eingänge vier Werte empfängt und ein Signal an einen seiner drei Ausgänge sendet.

Dies ist ein einfacher Klassifikator.

Um den Aufbau des Netzwerks abzuschließen, richten wir die Rückwärtsausbreitung ein (eine der effektivsten Trainingsmethoden) und deaktivieren das Vor-Training mit der Zeile .backprop (true) .pretrain (false) .

6. Ein Netzwerk erstellen und trainieren

Lassen Sie uns nun ein neuronales Netzwerk aus der Konfiguration erstellen, initialisieren und ausführen:

MultiLayerNetwork model = new MultiLayerNetwork(configuration);
model.init();
model.fit(trainingData);

Jetzt können wir das trainierte Modell mit dem Rest des Datensatzes testen und die Ergebnisse mit Bewertungsmetriken für drei Klassen überprüfen:

INDArray output = model.output(testData.getFeatureMatrix());
Evaluation eval = new Evaluation(3);
eval.eval(testData.getLabels(), output);

Wenn wir jetzt eval.stats () ausdrucken, werden wir feststellen, dass unser Netzwerk Irisblumen gut klassifizieren kann, obwohl Klasse 1 dreimal für Klasse 2 verwechselt wurde.

Examples labeled as 0 classified by model as 0: 19 times
Examples labeled as 1 classified by model as 1: 16 times
Examples labeled as 1 classified by model as 2: 3 times
Examples labeled as 2 classified by model as 2: 15 times

==========================Scores========================================
# of classes: 3
Accuracy: 0.9434
Precision: 0.9444
Recall: 0.9474
F1 Score: 0.9411
Precision, recall & F1: macro-averaged (equally weighted avg. of 3 classes)
========================================================================

Mit dem Builder für flüssige Konfiguration können wir Netzwerkebenen schnell hinzufügen oder ändern oder einige andere Parameter anpassen, um zu sehen, ob unser Modell verbessert werden kann.

7. Fazit

In diesem Artikel haben wir mithilfe der Bibliothek deeplearning4j ein einfaches, aber leistungsfähiges neuronales Netzwerk aufgebaut.

Wie immer ist der Quellcode für den Artikel verfügbar: https://github.com/eugenp/tutorials/tree/master/deeplearning4j