Machine Learning im Browser mit TensorFlow.js

TensorFlow.js ist eine JavaScript-Bibliothek, die das Training und das Ausführen neuronaler Netze im Browser erlaubt. WebGL zum Zugriff auf die lokale Grafikkarte sorgt für ausreichende Rechenleistung.

In Pocket speichern vorlesen Druckansicht 5 Kommentare lesen
Machine Learning im Browser mit TensorFlow.js
Lesezeit: 16 Min.
Von
  • Oliver Zeigermann
Inhaltsverzeichnis

Auf dem lezten TensorFlow Summit hat die JavaScript-Bibliothek TensorFlow.js den Sprung auf Version 1.0 vollzogen und ist seitdem noch stärker in die Plattform rund um das Machine-Learning-Framework TensorFlow integriert. Version 1.0 gibt Entwicklern Garantien über API-Stabilität und Performance, sodass sie TensorFlow.js produktiv einsetzen können.

Es gibt gute Gründe, neuronale Netze mit JavaScript im Browser zu nutzen, obwohl die dominante Programmiersprache für Machine Learning Python ist und das Training sowie die Ausführung typischerweise auf leistungsstarken Servern stattfinden.

  • Einige Entwickler fühlen sich in der Entwicklung mit JavaScript wohler als mit Python.
  • Die Browser-APIs ermöglichen einen komfortablen Zugriff auf Eingabemöglichkeiten für Video und Audio.
  • JavaScript ermöglicht interaktive Ausgaben während oder nach dem Training.* Der Browser ist eine automatisch skalierende Ausführungsumgebung für das Deployment eines komplexen Modells.

Aufgrund der verringerten Rechenleistung eignet sich der Browser vor allem für vortrainierte Modelle, die Entwickler entweder unverändert nutzen oder nachtrainieren. Dass die Daten und/oder Modelle den Browser nicht verlassen und offline verfügbar sind, hat einige Vorteile:

  • Vertrauliche Daten lassen sich lokal in ein Modell trainieren.
  • Nutzer können Bilder kontrollieren, damit vertrauliche Daten wie Ausweise oder unangebrachten Fotos nicht den Rechner verlassen.
  • Die Latenz ist gering.

Zum Lieferumfang von TensorFlow.js gehören einige Modelle, die sich relativ einfach in eigene Anwendungen einbauen lassen. Dass zahlreiche Beispiele vor allem spielerischer Natur sind, sollte Entwickler nicht davon abhalten, sie als Grundlage für ernsthafte Anwendungen herzunehmen.

Mehr Infos

WebGL

WebGL (Web Graphics Library) ist eine Browser-API, zum interaktiven Rendern von 2D- und 3D-Grafiken. Alle aktuellen Browser können die Schnittstelle verwenden, die auf OpenGL ES 2.0 aufbaut.

Die API ist somit für Grafiken ausgelegt, aber die Operationen, die man zur Berechnung von Grafiken benötigt, sind dieselben wie für neuronale Netze.

TensorFlow.js speichert dazu Tensoren (Daten) als WebGL-Texturen und die mathematischen Operationen wie Matrix-Multiplikationen darauf als WebGL Shader. Auf Wunsch kann TensorFlow.js auf der CPU arbeiten, wenn beispielsweise keine GPU vorhanden ist. Allerdings wird in dem Fall der UI-Thread blockiert, was dazu führt, dass während einer Berechnung die Nutzerschnittstelle nicht mehr reagiert.

Im Folgenden dient die Abschätzung des Unfallrisikos für Autofahrer als Fallbeispiel. Die Grundlage ist ein Datensatz mit vier Spalten, der sich folgendermaßen laden lässt, um ihn grafisch zu betrachten:

const csvDataset = await tf.data.csv(DATA_URL, {
delimiter: ";",
columnConfigs: {
group: { isLabel: true }
}
});
Mehr Infos

enterJS 2019

Vom 25. bis 28. Juni findet in Darmstadt die enterJS 2019 statt. Auf der von heise Developer, iX und dpunkt.verlag veranstalteten Konferenz zu Enterprise-JavaScript hält der Autor dieses Artikels im Rahmen des zweitägigen Vortragsprogramms einen Vortrag zu Tensorflow.js.

Neben der URL für den Datensatz und dem Trennzeichen in der CSV-Datei legt der Code fest, dass die Spalte "group" als Label dienen soll. Label heißt die Spalte, deren Wert die anderen Spalten vorhersagen sollen.

Die anderen Spalten geben die Höchstgeschwindigkeit des Fahrzeugs in Meilen pro Stunde, das Alter des Fahrers und die jährliche Laufleistung in tausend Meilen an. Sie bilden die Grundlage für die Vorhersage, ob ein Fahrer viele oder wenige Unfälle haben wird.

Zur Illustration zeigt folgende Grafik mit der Geschwindigkeit und dem Alter zwei der Werte für alle vorliegenden Datensätze. Die Farbe und das Symbol geben die Risikogruppe an, wie sie in der Legende rechts beschrieben ist. Mit bloßem Auge sind zwei Bereiche erkennbar, in denen eine oder zwei Kategorien das Bild dominieren.

Scatterplot für Risikogruppen

In JavaScript lässt sich eine Reihe für jede der drei Risikogruppen erstellen und als Scatterplot ausgeben. Dazu steht mit der TensorFlow.js-Erweiterung tfjs-vis eine komfortable API zur Verfügung.

tfvis.render.scatterplot(
{
values: [valuesAgeSpeedRed,
valuesAgeSpeedGreen,
valuesAgeSpeedYellow],

series: ["many accidents",
"few or no accidents",
"in the middle"]
});

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.

Regularisierung heißt der Vorgang, um sich dem gewünschten Resultat zu nähern. TensorFlow.js bietet dafür dieselben Mechanismen wie TensorFlow. Indem Entwickler bei jedem Trainingsdurchgang nur einen gewissen Teil der Neuronen trainieren, verringern sie zum einen die Kapazität des Modells und erzeugen zum anderen effektiv ein Ensemble schwacher Netze. Das Vorgehen hat sich im klassischen Machine Learning als sinnvoll erwiesen.

In TensorFlow.js dient dazu ein Dropout-Layer, den folgender Code mit einem relativ hohen Wert von 0,7 konfiguriert – der optimale Wert lässt sich über Experimente herausfinden. Häufig liegt es um 0,5 oder etwas darüber.

model.add(tf.layers.dropout({ rate: 0.7 }));

Neben diesem Verfahren hat sich die Normalisierung der Ausgabe eines Layers als zweckmäßig erwiesen. Die Intuition ist dabei weniger deutlich als bei Dropout. Vereinfacht gesagt fließt durch die Normalisierung ein gewisser Störfaktor in das Training ein, der dem Modell Robustheit verleiht. Folgende Zeile konfiguriert die BatchNormalization als eigenen Layer:

model.add(tf.layers.batchNormalization());

Die Konfiguration des Netzes inklusive Regularisierung besteht in den meisten Fällen mehr aus Ausprobieren als strukturiertem Vorgehen.

Damit erreicht das Modell die in Abbildung 3 gezeigte Genauigkeit von etwa 72 Prozent und weist nahezu kein Overfitting auf. Ob ein Wert gut ist, lässt sich nicht allgemein sagen: Die Daten bestimmen die Möglichkeiten und der Anwendungsfall die Notwendigkeiten. Für eine Vorhersage der Schadensklasse mag der Wert ausreichen, für das Erkennen eines Fußgängers durch ein selbstfahrendes Auto wäre das Ergebnis deutlich zu schlecht.

Das Modell lässt sich nach dem Training durch Aufruf der predict-Methode für Vorhersagen nutzen:

model.predict(tf.tensor([[100, 48, 10]])).print();

Da das Training kein deterministischer Vorgang ist, fallen die Werte bei jedem Training unterschiedlich aus. Das Modell des Autors gibt für einen 48 Jahre alten Fahrer, dessen Auto eine Höchstgeschwindigkeit von 100 Mph (160 kmh) hat und der pro Jahr 10.000 Meilen fährt, folgende Wahrscheinlichkeiten aus

  • viele Unfälle 1 Prozent,
  • wenig Unfälle 87 Prozent,
  • im mittleren Bereich 12 Prozent.

Das erscheint plausibel und somit hat das Modell seinen ersten, eher anekdotischen Test bestanden.

Neben der Kurve, die den Trainingsverlauf beschreibt, bietet TensorFlow.js weitere Möglichkeiten zur Auswertung des Trainings. Hilfreich ist eine sogenannte Confusion Matrix. Sie fasst die Verwechslungen zwischen den Kategorien zusammen. Um sie zu erstellen, gilt es zunächst eine Vorhersage für alle bekannten Daten zu treffen, und diese mit den bekannten und richtigen Bewertungen zu vergleichen. Der Code für TensorFlow.js sieht folgendermaßen aus:

// die bekannten, richtigen Bewertungen
const yTrue = tf.tensor(ys);
// die Vorhersagen
const yPred = model.predict(X).argMax([-1]);
const confusionMatrix =
await tfvis.metrics.confusionMatrix(yTrue, yPred);

Anders als oben ist nur die jeweils höchste Wahrscheinlichkeit von Interesse, da die tatsächlichen Kategorien nur in der Form angegeben sind. Im Code erledigt das der Aufruf der argMax-Methode.

Folgende Zeilen stellen die Matrix anschaulich dar:

const matrixContainer = 
document.getElementById("matrix-surface");
const classNames = ["many accidents",
"few or no accidents",
"in the middle"];
tfvis.show.confusionMatrix(matrixContainer,
confusionMatrix,
classNames);

Abbildung 4 zeigt eine solche Matrix. Im Idealfall würden nur Einträge auf der Diagonalen existieren als Zeichen dafür, dass es keine Verwechslungen zwischen den Gruppen gibt. In der Praxis schaut das Ergebnis meist anders aus und erlaubt Rückschlüsse auf die Qualität des Modells.

Die Confusion Matrix sieht in der Praxis anders aus als der Idealfall (Abb. 4).

Im konkreten Fall sehen die beiden Kategorien für viele und wenige Unfälle recht gut aus. Allerdings fällt der im Diagramm unten dargestellte mittlere Bereich deutlich ab. Es fällt auf, dass besonders viele Daten, die im unteren rechten Feld landen sollten, als "wenig Unfälle" kategorisiert sind – im Kasten in der Mitte unten.

Um die Frage zu beantworten, ob das Ergebnis auf einen Fehler bei den Trainingsdaten oder im Training hindeutet, hilft ein Blick auf den Übersichtsplot in Abbildung 1. Er zeigt, dass die beiden Bereiche stark ineinander verschränkt sind und das Modell sie wahrscheinlich nicht besser auseinanderhalten kann. Da in den vermischten Bereichen die wenigen Unfälle klar in der Überzahl sind, wird das Modell für Vorhersage diesen Bereich bevorzugen. Somit scheinen die Ergebnisse insgesamt nachvollziehbar zu sein, was den Abweichungen zum Trotz für eine gute Qualität des Modells spricht.

Somit ist das Modell reif für den Einsatz in der Produktion. Dass es dafür nicht den Browser des Nutzers verlassen muss, ist bei sensiblen Daten durchaus ein Vorteil. In der Praxis erfolgt die Überführung über die save-Methode des Modells:

https://js.tensorflow.org/api/latest/#tf.LayersModel.save

In der URL lässt sich der Speicherort des Modells angeben. localstorage ist die einfachste Option, die jedoch bei großen Modellen eventuell nicht funktioniert. Daher ist für größere Datenmengen `indexeddb die bessere Wahl:

model.save("indexeddb://insurance");

Anschließend lässt sich das Modell mit den Entwicklerwerkzeugen des Browsers betrachten. Dabei zeigt sich, dass vor allem die Parameter der Neuronen viel Platz einnehmen.

Modell im Browser, im Application Tab der Chrome Dev Tools

Wer das Modell später in einer anderen Anwendung auf demselben Host nutzen möchte, kann es wieder laden, worauf es sich wie vor dem Speichern verhält.

model = await tf.loadLayersModel('indexeddb://insurance');

Beispielhafte Anwendung zur Risikoabschätzung über das trainierte Modell (Abb. 6)

Das Modell verhält sich beim Einbetten in andere Anwendungen aus deren Sicht wie eine API, die im konkreten Beispiel die Risikoabschätzung übernimmt. Sachbearbeiter könnten die Anwendung schließlich nutzen, um reale Vorhersagen zu treffen, wie Abbildung 6 zeigt.

Der online verfügbare Crash Risk Caclulator läuft in Chrome, Safari, Firefox und vermutlich in Kürze im Edge Browser. Letzterer unterstützte beim Verfassen des Artikels den TextDecoder noch nicht, den die Anwendung zum Laden der Daten nutzt. Der komplette Quellcode des Beispiels ist auf GitHub abgelegt.

Weitere Demos, Tutorials und die API-Beschreibung finden sich auf der TensorFlow-Site. Dort existiert zudem eine Anleitung zum Umwandeln eines TensorFlow-Modells, um es mit TensorFlow.js im Browser in Produktion zu bringen. Außerdem sind einige vortrainierte Modelle verfügbar, die sich in eine bestehende JavaScript-Anwendung integrieren lassen.

Die Site ml5.js hat sich zum Ziel gesetzt, den Einsatz von TensorFlow.js weiter zu vereinfachen und bietet dazu einige gute Beispiele und Modelle. Auf Stack Overflow gibt es Antworten auf viele Fragen rund um TensorFlow.js.

Mit TensorFlow.js sind im Browser grundsätzlich alle Voraussetzungen für das Training und den Einsatz ernsthafter Anwendungen gegeben. Dennoch ersetzt der Ansatz wohl selten das Training von ML-Modellen auf dem Server, der schlicht robuster und leistungsfähiger ist.

Allerdings bietet die Arbeit im Browser deutliche Vorteile im Bereich Visualisierung, Privacy und Deployment. Zudem ist die direkte Interaktion mit den Benutzern im Browser besser. Eine denkbare Option ist, ein Modell auf dem Server vorzutrainieren und anschließend in das TensorFlow.js-Format zu wandeln, um es im Browser zu verwenden oder weiter zu trainieren.

Oliver Zeigermann
hat über Jahrzehnte in vielen unterschiedlichen Sprachen und mit vielen Techniken Software entwickelt. In den letzten Jahren ist er tief in die Analyse großer Datenmengen unter anderem auch mit Techniken des Machine Learning eingestiegen. Er arbeitet als freiberuflicher Berater und Entwickler und als Architekt bei embarc in Hamburg.
(rme)