Machine Learning im Browser mit TensorFlow.js

Seite 2: Das Modell betritt die Bühne

Inhaltsverzeichnis

Nun lässt sich mit den Daten ein Modell trainieren. Zuerst definieren Entwickler den Aufbau des neuronalen Netzes, das im konkreten Fall aus einer sequenziellen Abfolge von Schichten besteht:

const model = tf.sequential();

Die erste Schicht hat drei Eingaben und besteht aus 100 Neuronen. Die Eingaben für Geschwindigkeit, Alter und Laufleistung sind mit jedem der 100 Neuronen verbunden. Das Setting ist typisch für die Verarbeitung tabellarischer Daten und wird "Dense" oder "Fully Connected" genannt. Das Vorgehen ähnelt der Arbeit mit der Keras-API von TensorFlow.

model.add(
tf.layers.dense({
units: 100,
inputShape: [3]
})
);

Darauf folgen beliebig viele weitere Schichten derselben Art, typischerweise ein bis zwei. Für das Beispiel soll eine weitere Schicht genügen. Am Ende steht eine spezielle Konfiguration eines Dense-Layers, die für jede der drei Kategorien eine Wahrscheinlichkeit ausgeben kann. Das geschieht durch eine spezielle, sogenannte Aktivierungsfunktion:

model.add(
tf.layers.dense({units: 3, activation: "softmax" })
);

Damit soll das Modell nach dem Training in der Lage sein, anhand der drei Eingaben Höchstgeschwindigkeit, Alter und Laufleistung eine Vorhersage in Form von drei Wahrscheinlichkeiten für jeder der Risikoklassen abzugeben. Dabei addieren sich alle Wahrscheinlichkeiten zum Wert 1.

Der Aufbau des Modells ist damit komplett, aber es fehlt noch die Information, wie das Training des Modells erfolgen soll. Entscheidend ist dafür die sogenannte Loss-Funktion. Sie berechnet für jede Vorhersage einer Kategorie, wie nahe sie an dem bekannten Ergebnis ist. Beispielsweise beschreibt eine Vorhersage der Riskogruppe Grün – "Guter Kunde" – mit der Wahrscheinlichkeit von 100 Prozent in der passenden Kategorie und jeweils 0 Prozent in den beiden anderen. Dafür ist die Loss-Funktion sparseCategoricalCrossentropy zuständig:

model.compile({
loss: "sparseCategoricalCrossentropy",
optimizer: "adam"
});

Der sogenannte Optimizer bestimmt die Parameter im Modell, deren Anpassung den Fehler verringert. Das Thema ist komplex, und für das Beispiel genügt der "adam"-Optimizer, der ohne tieferes Wissen über seine Funktion und ohne Einstellung von Parametern gute Ergebnisse liefert. Er hat einen guten Ruf hinsichtlich seiner Robustheit. Nun kann das Training beginnen:

const history = await model.fit(X, y, {
epochs: 300,
validationSplit: 0.2
});

Die letzte Zeile mit validationSplit soll für die erste Betrachtung außen vor bleiben. Der Befehl legt das Training des Modells mit den Daten für 300 Epochen an. Das bedeutet, dass 300 mal X als Eingabe für das Modell dient und die erwartete Ausgabe y ist. Bei X handelt es sich um ein zweidimensionales Array (Tensor) mit 1500 Datensätzen, die Geschwindigkeit, Alter und Laufleistung repräsentieren. y ist ein eindimensionales Array mit den Werten 0, 1, oder 2 für die jeweiligen Risikoklasse.

Diese Art des Trainings heißt überwachtes Lernen (Supervised Learning). Anhand der oben aufgeführten Loss-Funktion findet TensorFlow.js heraus, wie falsch das Modell mit seiner Vorhersage gelegen hat, und erkennt, an welchen Parametern es drehen muss, um sie zu verbessern. Da die Vorhersage anfangs schlicht geraten ist, sollte sie aufgrund der drei Werte etwa zu 33 Prozent richtig sein. Der Wert sollte sich aber im Verlauf des Trainings verbessern.

Folgende Codezeile baut die Grafik zur Darstellung des in Abbildung 2 gezeigten Trainingsverlaufs und aktualisiert sie mit jeder Epoche. Das einfache Vorgehen ist eine der Stärken von TensorFlow.js.

tfvis.show.fitCallbacks(
document.getElementById("metrics-surface"),
["acc", "val_acc"]);

Die Darstellung des Trainingsverlaufs lässt sich interaktiv aktualisieren (Abb. 2).

Die blaue Kurve beschreibt die Genauigkeit des Modells mit jeder Epoche. Wie erwartet beginnt sie bei ungefähr 0,3 (33 %) und arbeitet sich langsam Richtung 1,0 (100 %) vor. Allerdings ist etwa bei 0,7 bereits der höchste Wert erreicht. Mit dem Modell lassen sich somit nur 70 Prozent der Daten richtig vorhersagen. Es mag überraschen, dass zwar alle Daten bekannt sind, aber es nicht möglich ist, sie komplett zu reproduzieren.

Doch das Ziel ist nicht, 100 Prozent zu erreichen, sondern ein möglichst abstraktes Modell zu schaffen, das auf Daten generalisiert, die es noch nie gesehen hat. Erst dadurch ist das Modell nützlich. Um zu simulieren, wie gut das Modell auf unbekannten Daten abschneidet, hält man einen gewissen Anteil – im konkreten Fall 20 Prozent – der Daten zurück, die man nur zum Überprüfen, nicht jedoch zum Training nutzt.

An dieser Stelle kommt der validationSplit: 0.2 zum Tragen, den die orangefarbene Kurve repräsentiert. Dass die Kurve deutlich unter der blauen bleibt, ist für das Ziel der Verallgemeinerung nicht gut: Das Modell passt sich zu sehr an die Trainingsdaten an und kann daher nicht mehr gut generalisieren – der Fachausdruck dafür ist Overfitting. Wünschensert ist eher einen Verlauf wie in Abbildung 3.

Die Kurve stellt einen gewünschten Verlauf ohne Overfitting dar (Abb. 3).

Am Anfang dürfen die Kurven durchaus auseinandergehen, aber am Ende sollte das Modell in etwa gleichermaßen auf bekannten und unbekannten Daten funktionieren.