Teil 2: In einfachen Schritten mit Deep Learning durchstarten

Schnelleinstieg in Deeplearning4j – Teil 2: Training und Verwendung

Paul Dubs

© Shutterstock / Sashkin

Dieser Artikel zeigt in zwei Teilen, wie man in kürzester Zeit den Einstieg in Deeplearning4j (DL4J) schafft. Anhand eines Beispiels, in dem vorhergesagt werden soll, ob ein Kunde seine Bank verlassen wird, wird jeder Schritt eines typischen Arbeitsablaufs betrachtet. Dieser Teil widmet sich dem Training und der Verwendung des Modells und wirft einen Blick über den Schnelleinstieg hinaus.

Im ersten Teil des Schnelleinstiegs in Deeplearning4j vermittelte ich die Grundlagen und Methoden, die nötig sind, um DL4J in das eigene Projekt zu implementieren. Im vorliegenden zweiten Teil geht es nun um das Training und die Verwendung des Modells.

Training des Modells

Bevor wir mit dem Training eines Modells beginnen können, müssen wir zunächst einmal definieren, wie das Modell aussehen soll. Im Gegensatz zu vielen anderen Machine-Learning-Methoden hat man bei Deep Learning viel mehr Einstellungsmöglichkeiten. Man muss sich entscheiden, welche Architektur das Netzwerk haben soll, wie breit die einzelnen Schichten ausfallen und welche Werte viele weitere Hyperparameter haben sollen. Wenn man Glück hat, gibt es ein bereits vortrainiertes Modell, das das eigene Problem lösen kann. Oder zumindest ein Paper, das eine Netzwerkarchitektur beschreibt, an der man sich orientieren kann.

Grundsätzlich hängt das Netzwerkdesign von dem konkreten Problem ab, mit dem man es zu tun hat. Convolutional Layer sind immer da nützlich, wo räumliche Zusammenhänge interessant sind, wie z. B. in Bildern. Recurrent Layer sind nützlich, wenn Sequenzen von Daten verarbeitet werden sollen und die Reihenfolge innerhalb der Sequenz wichtig ist. Dense Layer, auch als Fully Connected Layer bekannt, sind dann gut, wenn alle vorhandenen Daten auf einmal berücksichtigt werden sollen.

Das Finden einer guten Netzwerkarchitektur ist eine Aufgabe, die auf Experimentierfreudigkeit setzt. Man muss viele Ideen ausprobieren und viele Experimente machen, um ein tatsächlich gutes Modell zu finden. Als Orientierungshilfe kann man die Faustregel verwenden, dass man mit einfachen Modellen, d. h. solchen mit wenigen, schmalen Schichten, anfangen und nur langsam an Komplexität hinzufügen sollte. Dabei sollte man darauf achten, ob das Modell unter Verwendung eines Bruchteils der Daten, z. B. einem einzigen Mini-Batch, dazu trainiert werden kann, dass der Fehler auf 0 sinkt. Denn wenn selbst das nicht funktioniert, wird das Modell auch mit mehr Daten nicht besser funktionieren.

Für unser Beispiel verwenden wir ein einfaches Multi-Layer-Modell. Hierfür setzen wir auch wieder einen Random Seed, da die Gewichtungen des Modells mit Zufallswerten nach dem Xavier-Schema initialisiert werden. Das Setzen eines Random Seed ist, insbesondere wenn man ein Modell und seine Hyperparameter noch nicht festgemacht hat, zu empfehlen, da ansonsten gute Ergebnisse einfach nur aus Zufall passieren könnten und dann nicht mehr reproduzierbar wären.

Weiter wird für jeden Layer als Vorgabe die TANH-Aktivierungsfunktion gesetzt. Das ist eine praktische Abkürzung, um diese Einstellung nicht einzeln vornehmen zu müssen. Als Updater wird hier Adam mit einer Lernrate von 0.001 verwendet und eine L2-Regularisierung von 0.0000316.

MultiLayerConfiguration config = new NeuralNetConfiguration.Builder()
  .seed(0xC0FFEE)
  .weightInit(WeightInit.XAVIER)
  .activation(Activation.TANH)
  .updater(new Adam.Builder().learningRate(0.001).build())
  .l2(0.0000316)
  .list(
    new DenseLayer.Builder().nOut(25).build(),
    new DenseLayer.Builder().nOut(25).build(),
    new DenseLayer.Builder().nOut(25).build(),
    new DenseLayer.Builder().nOut(25).build(),
    new DenseLayer.Builder().nOut(25).build(),
    new OutputLayer.Builder(new LossMCXENT()).nOut(2).activation(Activation.SOFTMAX).build()
  )
  .setInputType(InputType.feedForward(finalSchema.numColumns() - 1))
  .build();

Die Modellarchitektur besteht in unserem Fall (Listing 1) aus fünf DenseLayer, die jeweils 25 Ausgabeeinheiten haben, und einem OutputLayer mit zwei Ausgaben. Ein OutputLayer ist selbst im Prinzip auch ein DenseLayer, der Unterschied liegt jedoch in der Loss-Funktion, die dort zusätzlich angegeben wird. Diese errechnet die Abweichung zwischen dem, was das Modell während des Trainings ausgibt, und dem Label. Mit dieser Abweichung wird dann im Weiteren das Modell angepasst, um bessere Ergebnisse zu liefern. Der OutputLayer weicht von der Voreinstellung der TANH-Aktivierungsfunktion ab und verwendet stattdessen die SOFTMAX-Aktivierungsfunktion. Dadurch wird an der Ausgabe sichergestellt, dass die Ergebnisse des Modells als eine Wahrscheinlichkeitsverteilung interpretiert werden können, d. h. dass jeder Wert zwischen 0 und 1 liegt und ihre Summe auf genau 1 kommt.

Zuletzt wird noch mit setInputType eingestellt, welche Art von Daten das Modell verarbeiten wird und wie viele Eingabespalten diese Daten je Beispiel haben. Dadurch werden bei komplexeren Modellen die evtl. notwendigen Adapter zwischen zwei Layerarten automatisch hinzugefügt und die Anzahl der Eingänge für jeden Layer automatisch berechnet.

Alle hier gewählten Parameterwerte sind durch etwas Herumprobieren zustande gekommen. So hat sich z. B. die TANH-Aktivierungsfunktion für dieses Beispiel als effektiver herausgestellt als die mittlerweile übliche RELU-Aktivierungsfunktion.

Training

Das eigentliche Training ist unspektakulär. Wir erzeugen aus der zuvor definierten Konfiguration zuerst ein neues Modell, initialisieren es und trainieren es dann mit den Trainingsdaten für 59 Epochen, d. h. die Trainingsdaten werden während des Trainings 59-mal komplett durchlaufen. Nach einiger Zeit ist das Training fertig – und das Modell kann weiterverwendet werden.

MultiLayerNetwork model = new MultiLayerNetwork(config);
  model.init();
  model.fit(trainIterator, 59);

Grundsätzlich dreht sich das eigentliche Training immer um den Aufruf der fit-Methode, von der DL4J mehrere Varianten anzubieten hat. Die im Beispiel verwendete Variante nimmt einen DataSetIterator und eine Epochenzahl entgegen. Lässt man die Epochenzahl weg, wird genau ein Datendurchlauf durchgeführt, also eine Epoche lang trainiert. Die letzte Variante ist, dass man das Durchlaufen durch den Iterator selbst in die Hand nimmt und dann den nächsten Mini-Batch eben selbst an die fit-Methode übergibt.

Die unterschiedlichen Trainingsmethoden gehen darauf zurück, was man mit dem Modell zwischen den Trainingseinheiten tun möchte. So könnte man z. B. zwischen jeder Epoche das Modell evaluieren, unterschiedliche Trainingsstände speichern oder das Modell anderweitig manipulieren.

Evaluierung

Nachdem das Modell trainiert wurde, möchte man auch evaluieren, wie gut es auf Daten generalisiert, die es noch nicht gesehen hat. Hierbei kommt die Testmenge zum Einsatz.

TransformProcessRecordReader testRecordReader = new  
TransformProcessRecordReader(new CSVRecordReader(), transformProcess);
  testRecordReader.initialize( new FileSplit(new  
File("X:/Churn_Modelling/Test/")));

Um die Testmenge zu verwenden, nutzen wir die gleiche Art des Ladens wie für die Trainingsmenge. Die Batch Size ist in diesem Fall grundsätzlich egal für das Ergebnis, daher nehmen wir der Einfachheit halber dieselbe wie für die Trainingsmenge.

RecordReaderDataSetIterator testIterator = new RecordReaderDataSetIterator.Builder(testRecordReader, batchSize)
  .classification(finalSchema.getIndexOfColumn("Exited"), 2)
  .build();

DL4J macht die Evaluierung eines Modells einfach: Es reicht in der Regel der Aufruf der evaluate-Methode auf dem Modell zusammen mit den Testdaten. Man bekommt ein Evaluation-Objekt, das meist verwendet wird, um die Evaluierungsergebnisse irgendwo auszugeben. In unserem Beispiel schreiben wir die Ergebnisse auf die Konsole (Abbildung 1):

Evaluation evaluate = model.evaluate(testIterator);
    System.out.println(evaluate.stats());
Abb. 1: Auswertung des „Evaluation“-Objekts

Abb. 1: Auswertung des „Evaluation“-Objekts

Das Evaluation-Objekt kann auch auf andere Auswertungen als die standardmäßig ausgegebenen zugreifen. In unserem Fall ist insbesondere der „Matthews Correlation Coefficient“ hervorzuheben, da dieser auch die Ungleichverteilung in den Daten berücksichtigt und somit zeigt, dass trotz einer guten Genauigkeit die Aussagekraft dieses Modells dennoch etwas eingeschränkt ist.

System.out.println("MCC: " + evaluate.matthewsCorrelation(EvaluationAveraging.Macro));

MCC: 0.530128345214995

Tuning

Da das Training, insbesondere wenn man viele Daten hat, längere Zeit dauern kann, möchte man häufig nicht blind darauf vertrauen, dass es gut vorankommen wird. Das ist gerade dann wichtig, wenn man noch nicht die optimalen Hyperparameter gewählt hat.

Als Hyperparameter werden alle Parameter bezeichnet, die die genaue Form des Modells und des Trainingsregimes vorgeben. Es wird also zwischen zwei Arten von Parametern unterschieden: die vom Modell gelernten Parameter, auch Gewichte genannt, und die Hyperparameter, die vom User vorgegeben werden. Zu den Hyperparametern gehören unter anderem die Lernrate, die Stärke der Regularisierungen, die Größe eines Mini-Batch und auch die Größe und Anzahl der Layer des Netzwerks. Die Wahl guter Hyperparameter ist im Moment immer noch mehr Kunst als Wissenschaft; es gibt jedoch Tools, die dabei helfen.

Zuerst schauen wir auf Listeners, die uns dabei helfen, das Modell nicht im völligen Blindflug trainieren zu lassen. Im Allgemeinen werden Listener mit der Methode addListeners zu einem Modell hinzugefügt. In unserem Beispiel werden zwei unterschiedliche Listener verwendet: Der ScoreIterationListener kann ohne weitere Abhängigkeiten verwendet werden und loggt einfach nur den Trainingsscore, d. h. den Wert der Loss-Funktion auf dem aktuellen Mini-Batch. Er wird damit parametrisiert, wie häufig er eine Ausgabe machen soll. In unserem Beispiel passiert das also alle 50 Iterationen, d. h. nach jeweils 50 Mini-Batches oder zweimal je Epoche.

model.addListeners(new ScoreIterationListener(50));

Der StatsListener ist viel aufwendiger und wird zusammen mit einer Weboberfläche verwendet. Um ihn verwenden zu können, wird daher noch eine weitere Abhängigkeit gebraucht. Da die Weboberfläche auf dem Play-Framework aufbaut (und das Scala verwendet), muss die zu verwendende Scala-Version in der Artefakt-ID ebenfalls angegeben werden. Wenn man sonst keine weiteren Scala-basierten Abhängigkeiten hat, kann man getrost die hier gegebene verwenden. Im Augenblick werden die Scala-Versionen 2.10 und 2.11 unterstützt.

<dependency>
  <groupId>org.deeplearning4j</groupId>
  <artifactId>deeplearning4j-ui_2.10</artifactId>
  <version>${dl4j.version}</version>
</dependency>

Wenn der StatsListener wie im Beispiel gezeigt verwendet wird, bekommt man in dem Log einen Hinweis darauf, wo die Oberfläche aufgerufen werden kann. In der Regel ist sie unter http://127.0.0.1:9000 zu finden.

UIServer uiServer = UIServer.getInstance();
  StatsStorage statsStorage = new InMemoryStatsStorage();
  uiServer.attach(statsStorage);
  model.addListeners(new StatsListener(statsStorage, 50));

  model.fit(trainIterator, 100);

Das Webinterface zeigt viele Informationen anhand derer man ablesen kann, ob das Training gut voranschreitet. Insbesondere ein tendenziell fallender Scoregraph ist wichtig. Falls er steigt, bezeichnet man das Verhalten als divergent, d. h. das Modell lernt nicht nur nichts, es wird auch zunehmend schlechter. In Abbildung 2 kann man sehen, dass der Score je Mini-Batch unterschiedlich aussehen kann. Daher sollte man nicht gleich beim ersten Anstieg von einer Divergenz ausgehen, sondern erst, wenn es tatsächlich einen anhaltend steigenden Trend gibt.

Abb. 2: Mini-Batches und Scoregraphen

Abb. 2: Mini-Batches und Scoregraphen

Eine ebenfalls wichtige Information, die hier gezeigt wird, ist die „Update:Parameter Ratios“. Hierbei ist die Faustregel, dass man auf einen Wert um -3.0 zielen sollte, was etwa durch ein Verändern der Lernrate erfolgen kann. Wenn man einen zu hohen Wert hat, z. B. -1.0, dann sollte die Lernrate gesenkt werden. Entsprechend sollte sie bei einem zu niedrigen Wert, z. B. -4.0, erhöht werden.

So nützlich die Listeners sind, muss aber auch berücksichtigt werden, dass sie sich auf die Performance auswirken. Will man ein Modell komplett unbeaufsichtigt trainieren lassen, sollte man den Prozess nicht unnötig verlangsamen indem man Informationen sammeln lässt, die nie sichtbar gemacht werden.

Manuelles Ausprobieren, welche Hyperparameter den besten Trainingsverlauf und das beste Ergebnis liefern, kann schnell viel Zeit in Anspruch nehmen. Deswegen gibt es die Arbiter-Bibliothek von DL4J. Sie unterstützt eine automatische Suche nach guten Hyperparameterkombinationen. Für den fortgeschrittenen Nutzer ist sie daher auf jeden Fall einen Blick wert.

Sichern des Modells

Auch das Speichern des Modells ist recht unspektakulär. Durch den Aufruf der saveFile-Methode wird das Modell in die angegebene Datei geschrieben. Standardmäßig wird dabei auch der Updater-Zustand gespeichert. Der ist notwendig, wenn man das gespeicherte Modell später weiter trainieren will. Wenn er weggelassen werden soll, lässt sich das über einen weiteren Parameter der saveFile-Methode regeln.

File modelSave = new File("X:/Churn_Modelling/model.bin");
  model.save(modelSave);

In der Regel muss jedoch nicht nur das Modell gespeichert werden. Wenn Normalisierungen angewendet wurden, die auf statistische Angaben angewiesen sind, ist es notwendig, auch diese für die zukünftige Verwendung zu speichern. Grundsätzlich kann man in der Modelldatei beliebig viele weitere Daten speichern. Man sollte jedoch beachten, dass diese Javas Serialisierungsfunktion verwendet. Es ist daher meist angebracht, seine Daten in ein möglichst einfaches Format zu bringen bevor man sie verwendet, um auch über Versionen hinweg ladbare Daten zu haben. In unserem Beispiel speichern wir neben dem Modell auch noch das Ergebnis der Analyse und das Schema der Eingabedaten:

ModelSerializer.addObjectToFile(modelSave, "dataanalysis", analysis.
toJson());
ModelSerializer.addObjectToFile(modelSave, "schema", finalSchema.
toJson());

Verwenden des Modells

Das Laden des Modells ist genauso einfach wie das Speichern. Wir geben wieder nur die Datei an, aus der die Daten geladen werden sollen, und laden das Modell und die zusätzlich gespeicherten Daten daraus.

File modelSave = new File("X:/Churn_Modelling/model.bin");
  MultiLayerNetwork model = ModelSerializer.restoreMultiLayerNetwork(modelSave);
  DataAnalysis analysis =   
DataAnalysis.fromJson(ModelSerializer.getObjectFromFile(modelSave,  
"dataanalysis"));
  Schema targetSchema = 
Schema.fromJson(ModelSerializer.getObjectFromFile(modelSave, "schema"));

Wir gehen davon aus, dass die Daten aus einer anderen Quelle als die Trainingsdaten kommen werden. Für das Beispiel stehen sie direkt im Quelltext.

Wie auch schon beim Training fangen wir damit an, ein Schema und einen TransformProcess zu definieren (Listing 4). Der große Unterschied zum Training ist jedoch, dass wir die Daten diesmal in einer anderen Reihenfolge bekommen und die unnötigen Daten gar nicht erst im Schema auftauchen.

Schema schema = new Schema.Builder()
  .addColumnsInteger("Age", "Tenure", "Num Of Products", "Credit Score")
  .addColumnsDouble("Balance", "Estimated Salary")
  .addColumnCategorical("Geography", "France", "Germany", "Spain")
  .addColumnCategorical("Gender", "Female", "Male")
  .addColumnCategorical("Has Credit Card", "0", "1")
  .addColumnCategorical("Is Active Member", "0", "1")
  .build();

Dadurch muss der TransformProcess auch etwas anders ausfallen. Diesmal müssen keine Daten entfernt werden aber es ist notwendig, die Spalten wieder in die Reihenfolge zu bringen, die auch beim Training verwendet wurde, da das Modell sonst nichts mit den Daten anfangen kann (Listing 5).

String[] newOrder = targetSchema.getColumnNames().stream().filter(it -> !it.equals("Exited")).toArray(String[]::new);
TransformProcess transformProcess = new TransformProcess.Builder(schema)
  .categoricalToOneHot("Geography", "Gender", "Has Credit Card", "Is Active Member")
  .integerToOneHot("Num Of Products", 1, 4)
  .normalize("Tenure", Normalize.MinMax, analysis)
  .normalize("Age", Normalize.Standardize, analysis)
  .normalize("Credit Score", Normalize.Log2Mean, analysis)
  .normalize("Balance", Normalize.Log2MeanExcludingMin, analysis)
  .normalize("Estimated Salary", Normalize.Log2MeanExcludingMin, analysis)
  .reorderColumns(newOrder)
  .build();

Da der TransformProcess auch diesmal für die Normalisierung zuständig ist, benötigt er die zuvor gespeicherte Analyse. Um die Spalten in dieselbe Reihenfolge wie beim Training zu bekommen, verwenden wir das gespeicherte Schema, bei dem wir noch die Exited-Spalte entfernen, da die im Produktiveinsatz vorhergesagt werden soll und somit nicht in unseren Daten enthalten ist.

Jetzt konvertieren wir unsere Daten in ein Record, transformieren dieses und vektorisieren das Ergebnis letztendlich, um es in ein Format zu bringen, das vom Modell angenommen wird. Die Daten sind hier genau in der Reihenfolge angegeben, wie sie auch in dem Schema angegeben wurden und stellen praktisch ein Mini-Batch der Größe 1 dar.

List record = RecordConverter.toRecord(schema, Arrays.asList(26, 8, 1, 547, 97460.1, 43093.67, "France", "Male", "1", "1")); // Unsere Daten
  List transformed = transformProcess.execute(record);
  INDArray data = RecordConverter.toArray(transformed);

Da wir mit unserem Modell eine Klassifikation durchführen wollen und seine Ausgabe mit einer SOFTMAX-Aktivierungsfunktion belegt ist, können wir das Modell mit der predict-Methode befragen und bekommen ein Integer-Array von Antworten.

int[] labelIndices = model.predict(data);
// = [0]

Das liegt daran, dass das Modell immer davon ausgeht, ein Batch von Anfragen zu bekommen. Da wir in diesem Fall nur eine einzige Anfrage gestellt haben, bekommen wir ein Array mit nur einem Element zurück. Es beinhaltet den Index des Labels als Antwort. In unserem Beispiel sind Index und Label identisch, da wir zwischen 0 und 1 unterscheiden. Wenn wir jedoch komplexere Labels hätten, müssten wir noch zwischen dem Index und seiner Bedeutung konvertieren.

Wenn man nicht nur den Index des Labels haben will, z. B. weil man das volle Ergebnis des Modells sehen möchte oder weil man keine Klassifikation, sondern eine Regression durchgeführt hat, kann man die output-Methode verwenden.

INDArray output = model.output(data, false);
// = [[    0.9844,    0.0156]]

Hierbei ist jedoch zu beachten, dass die Methode grundsätzlich im Trainingsmodus läuft, d. h. Regularisierungsmethoden wie Dropout werden auch angewendet, und man somit erst mit einem zusätzlichen Parameter in den Inferenzmodus umschalten muss. Da auch die output-Methode von ganzen Batches ausgeht, ist die Antwort auch in Batch-Form gegeben.

Da wir wissen, dass wir nur ein einziges Ergebnis haben, können wir mittels toDoubleVector-Methode das ND4J-Array in ein einfaches Double-Array konvertieren.

double[] result = model.output(data, false).toDoubleVector()
// = [0.9843524098396301, 0.015647541731595993] 

Wenn wir mehr als ein Ergebnis gehabt hätten, würde sich auch die toDoubleMatrix-Methode anbieten, um eine Double-Matrix, d. h. ein zweidimensionales Double Array, zu bekommen.

„ParallelInference“

Modelle sind in DL4J nicht threadsafe, d. h. sie können nicht von mehreren Threads gleichzeitig verwendet werden. Das ist aber z. B. im Kontext von Webanwendungen kaum zu umgehen. Man könnte zwar jedem Thread sein eigenes Modell geben – das würde aber, gerade bei komplexen Modellen, schnell zu viel Speicherbedarf führen.

Um dieses Problem zu lösen, gibt es das ParallelInference-Modul für DL4J. Das nimmt die Anfragen von mehreren Threads entgegen, sammelt sie für kurze Zeit und führt das Modell dann für alle gesammelten Anfragen aus. Da das Modell intern parallel arbeitet, werden die zur Verfügung stehenden Ressourcen dennoch voll ausgelastet.

Das Modul muss ebenfalls explizit eingebunden werden.

<dependency>
  <groupId>org.deeplearning4j</groupId>
  <artifactId>deeplearning4j-parallel-wrapper</artifactId>
  <version>${dl4j.version}</version>
</dependency>

Die Verwendung ist letztendlich einfach. Man erstellt eine ParallelInference-Instanz, die im einfachsten Fall nur das Modell als Parameter bekommt. Danach kann diese Instanz anstatt des Modells verwendet werden, um Vorhersagen von dem Modell zu bekommen.

ParallelInference wrapped = new ParallelInference.Builder(model).build();
  INDArray parOutput = wrapped.output(data);

Da ParallelInference immer zur Vorhersage mithilfe des Modells verwendet wird, ist es nicht notwendig, es erst über einen zusätzlichen Parameter zu aktivieren.

Über den Schnelleinstieg hinaus

DL4J bietet noch viel mehr Features als die hier gezeigten. So gibt es mit dem Modellzoo auch eine Vielzahl an vortrainierten Modellen. Die sind insbesondere im Zusammenhang mit dem Transfer-Learning-Feature interessant, womit die vortrainierten Modelle für eine neue, aber ihrem ursprünglichen Zweck ähnliche Aufgabe trainiert werden können.

Zudem unterstützt DL4J verteiltes Lernen mittels Spark. Die im Artikel gezeigten Grundlagen gelten dabei zum Großteil weiter. Da verteiltes Rechnen jedoch seinen eigenen Overhead hat, lohnt sich der Einsatz nur, wenn man viele Daten hat. Außerdem ist in diesem Artikel Reinforcement Learning nicht berücksichtigt worden. Im DL4J-Ökosystem gibt es aber auch RL4J, das auf DL4J aufbaut, um Reinforcement Learning mit Java zu ermöglichen.

Die zu Beginn gegebenen Abhängigkeiten decken von Anfang an schon viele DL4J-Module ab. Es ist aber natürlich auch möglich, einzelne Module anstelle von deeplearning4j-core als Abhängigkeit anzugeben. Dadurch lässt sich unter Umständen viel Speicherplatz sparen. Das ist insbesondere bei Verwendung von DL4J auf Endgeräten wie Smartphones interessant.

Fazit

Dieser Artikel hat anhand eines Beispiels gezeigt, wie man mit den Bibliotheken der Deeplearning4J-Familie von Daten zu Training und Verwendung des Modells übergeht. Die Entwickler von Deeplearning4J bieten darüber hinaus auch ein eigenes Github Repository mit vielen weiteren Beispielen an, das auf GitHub zu finden ist. Mit dem Wissen aus diesem Artikel sollte es einfach sein, den meisten dieser Beispiele zu folgen.

Sollten noch weitere Fragen auftreten, wird in der Regel im englischsprachigen Gitter-Channel schnell geholfen.

Geschrieben von
Paul Dubs
Paul Dubs
Paul hat einen Master of Science in Informatik der TU Darmstadt und verfügt über mehr als ein Jahrzehnt Berufserfahrung in der Softwareentwicklung. Als Open-Source-Entwickler arbeitet er am Deeplearning4J-Projekt mit und wenn Sie jemals eine Frage im Deeplearning4J Gitter gestellt haben, besteht eine gute Chance, dass er Ihre Frage beantwortet hat.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: