Umgang mit MNIST-Bilddaten in Tensorflow.js

Es gibt den Witz, dass 80 Prozent der Datenwissenschaftler die Daten bereinigen und 20 Prozent sich über die Bereinigung der Daten beschweren. Die Datenbereinigung macht einen viel höheren Anteil der Datenwissenschaftler aus, als ein Außenstehender erwarten würde. Tatsächlich macht das Trainieren von Modellen in der Regel einen relativ geringen Anteil (weniger als 10 Prozent) der Tätigkeiten eines Maschinenschülers oder Datenwissenschaftlers aus.

 - Anthony Goldbloom, CEO von Kaggle

Das Manipulieren von Daten ist ein entscheidender Schritt für jedes Problem des maschinellen Lernens. In diesem Artikel wird das MNIST-Beispiel für Tensorflow.js (0.11.1) verwendet und der Code durchlaufen, der das zeilenweise Laden von Daten behandelt.

MNIST-Beispiel

18 import * as tf from '@ tensorflow / tfjs';
19
20 const IMAGE_SIZE = 784;
21 const NUM_CLASSES = 10;
22 const NUM_DATASET_ELEMENTS = 65000;
23
24 const NUM_TRAIN_ELEMENTS = 55000;
25 const NUM_TEST_ELEMENTS = NUM_DATASET_ELEMENTS - NUM_TRAIN_ELEMENTS;
26
27 const MNIST_IMAGES_SPRITE_PATH =
28 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_images.png';
29 const MNIST_LABELS_PATH =
30 'https: //storage.googleapis.com/learnjs-data/model-builder/mnist_labels_uint8'; `

Zunächst importiert der Code Tensorflow (stellen Sie sicher, dass Sie Ihren Code transpilieren!) Und erstellt einige Konstanten, darunter:

  • IMAGE_SIZE - die Größe eines Bildes (Breite und Höhe von 28x28 = 784)
  • NUM_CLASSES - Anzahl der Label-Kategorien (eine Zahl kann 0-9 sein, es gibt also 10 Klassen)
  • NUM_DATASET_ELEMENTS - Anzahl der Bilder insgesamt (65.000)
  • NUM_TRAIN_ELEMENTS - Anzahl der Trainingsbilder (55.000)
  • NUM_TEST_ELEMENTS - Anzahl der Testbilder (10.000, auch bekannt als Rest)
  • MNIST_IMAGES_SPRITE_PATH & MNIST_LABELS_PATH - Pfade zu den Bildern und Etiketten

Die Bilder werden zu einem riesigen Bild verknüpft, das wie folgt aussieht:

MNISTData

Als Nächstes beginnt in Zeile 38 MnistData, eine Klasse, die die folgenden Funktionen verfügbar macht:

  • load - verantwortlich für das asynchrone Laden der Bild- und Etikettierungsdaten
  • nextTrainBatch - Lädt den nächsten Trainingsbatch
  • nextTestBatch - Lädt den nächsten Teststapel
  • nextBatch - eine generische Funktion zum Zurückgeben des nächsten Stapels, je nachdem, ob es sich um einen Trainingssatz oder einen Testsatz handelt

Zum Einstieg wird in diesem Artikel nur die Ladefunktion beschrieben.

Belastung

44 async load () {
45 // Fordere das MNIST-Sprited-Image an.
46 const img = neues Bild ();
47 const canvas = document.createElement ('canvas');
48 const ctx = canvas.getContext ('2d');

async ist eine relativ neue Sprachfunktion in Javascript, für die Sie einen Transpiler benötigen.

Das Image-Objekt ist eine native DOM-Funktion, die ein Bild im Speicher darstellt. Es bietet Rückrufe für den Zeitpunkt, zu dem das Bild geladen wird, sowie Zugriff auf die Bildattribute. canvas ist ein weiteres DOM-Element, das einfachen Zugriff auf Pixel-Arrays und die Verarbeitung über den Kontext bietet.

Da es sich bei beiden Elementen um DOM-Elemente handelt, haben Sie keinen Zugriff auf diese Elemente, wenn Sie in Node.js (oder einem Web-Worker) arbeiten. Einen alternativen Ansatz finden Sie weiter unten.

imgRequest

49 const imgRequest = new Promise ((lösen, ablehnen) => {
50 img.crossOrigin = '';
51 img.onload = () => {
52 img.width = img.naturalWidth;
53 img.height = img.naturalHeight;

Der Code initialisiert ein neues Versprechen, das aufgelöst wird, sobald das Bild erfolgreich geladen wurde. In diesem Beispiel wird der Fehlerstatus nicht explizit behandelt.

crossOrigin ist ein img-Attribut, mit dem Bilder domänenübergreifend geladen werden können. Bei der Interaktion mit dem DOM werden CORS-Probleme (Cross-Origin Resource Sharing) umgangen. naturalWidth und naturalHeight beziehen sich auf die ursprünglichen Abmessungen des geladenen Bildes und dienen dazu, bei der Durchführung von Berechnungen die Richtigkeit der Bildgröße zu gewährleisten.

55 const datasetBytesBuffer =
56 neuer ArrayBuffer (NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4);
57
58 const chunkSize = 5000;
59 canvas.width = img.width;
60 canvas.height = chunkSize;

Der Code initialisiert einen neuen Puffer, der jedes Pixel jedes Bildes enthält. Es multipliziert die Gesamtzahl der Bilder mit der Größe jedes Bildes mit der Anzahl der Kanäle (4).

Ich glaube, dass chunkSize verwendet wird, um zu verhindern, dass die Benutzeroberfläche zu viele Daten auf einmal in den Speicher lädt, obwohl ich nicht 100% sicher bin.

62 für (sei i = 0; i 

Dieser Code durchläuft jedes Bild im Sprite und initialisiert ein neues TypedArray für diese Iteration. Das Kontextbild erhält dann einen Teil des gezeichneten Bildes. Schließlich wird dieses gezeichnete Bild mit der Funktion getImageData des Kontexts in Bilddaten umgewandelt, die ein Objekt zurückgibt, das die zugrunde liegenden Pixeldaten darstellt.

72 für (sei j = 0; j 

Wir durchlaufen die Pixel und teilen sie durch 255 (den maximal möglichen Wert eines Pixels), um die Werte zwischen 0 und 1 zu begrenzen. Nur der rote Kanal ist erforderlich, da es sich um ein Graustufenbild handelt.

78 this.datasetImages = new Float32Array (datasetBytesBuffer);
79
80 resolve ();
81};
82 img.src = MNIST_IMAGES_SPRITE_PATH;
83});

Diese Zeile nimmt den Puffer auf, setzt ihn in ein neues TypedArray um, das unsere Pixeldaten enthält, und löst dann das Versprechen auf. Die letzte Zeile (Einstellung der Quelle) beginnt tatsächlich mit dem Laden des Bildes, wodurch die Funktion gestartet wird.

Was mich zunächst verwirrte, war das Verhalten von TypedArray in Bezug auf den zugrunde liegenden Datenpuffer. Möglicherweise stellen Sie fest, dass datasetBytesView in der Schleife festgelegt ist, jedoch nie zurückgegeben wird.

DatasetBytesView verweist unter der Haube auf den Puffer datasetBytesBuffer (mit dem es initialisiert wird). Wenn der Code die Pixeldaten aktualisiert, bearbeitet er indirekt die Werte des Puffers selbst, der wiederum in ein neues Float32Array in Zeile 78 umgewandelt wird.

Abrufen von Bilddaten außerhalb des DOM

Wenn Sie sich im DOM befinden, sollten Sie das DOM verwenden. Der Browser (über die Leinwand) kümmert sich darum, das Format der Bilder herauszufinden und die Pufferdaten in Pixel zu übersetzen. Wenn Sie jedoch außerhalb des DOM arbeiten (z. B. in Node.js oder einem Web-Worker), benötigen Sie einen alternativen Ansatz.

fetch stellt den Mechanismus response.arrayBuffer bereit, mit dem Sie auf den zugrunde liegenden Puffer einer Datei zugreifen können. Wir können dies verwenden, um die Bytes manuell zu lesen und das DOM vollständig zu vermeiden. Hier ist ein alternativer Ansatz zum Schreiben des obigen Codes (für diesen Code ist ein Abruf erforderlich, der in Node mit etwas wie einem isomorphen Abruf polygefüllt werden kann):

const imgRequest = fetch (MNIST_IMAGES_SPRITE_PATH) .then (resp => resp.arrayBuffer ()). then (buffer => {
  neues Versprechen zurückgeben (Beschluss => {
    const reader = neuer PNGReader (Puffer);
    return reader.parse ((err, png) => {
      const pixels = Float32Array.from (png.pixels) .map (pixel => {
        Rückgabepixel / 255;
      });
      this.datasetImages = pixels;
      Entschlossenheit();
    });
  });
});

Dies gibt einen Array-Puffer für das bestimmte Bild zurück. Beim Schreiben dieses Dokuments habe ich zuerst versucht, den eingehenden Puffer selbst zu analysieren, was ich nicht empfehlen würde. (Wenn Sie daran interessiert sind, finden Sie hier einige Informationen zum Lesen eines Array-Puffers für ein PNG.) Stattdessen habe ich pngjs gewählt, das das PNG-Parsen für Sie übernimmt. Wenn Sie mit anderen Bildformaten arbeiten, müssen Sie die Analysefunktionen selbst herausfinden.

Nur die Oberfläche kratzen

Das Verständnis der Datenmanipulation ist eine entscheidende Komponente des maschinellen Lernens in JavaScript. Wenn wir unsere Anwendungsfälle und Anforderungen verstehen, können wir einige Schlüsselfunktionen verwenden, um unsere Daten elegant und korrekt für unsere Anforderungen zu formatieren.

Das Tensorflow.js-Team ändert kontinuierlich die zugrunde liegende Daten-API in Tensorflow.js. Dies kann dazu beitragen, mehr von unseren Anforderungen zu erfüllen, wenn sich die API weiterentwickelt. Dies bedeutet auch, dass es sich lohnt, mit den Entwicklungen der API Schritt zu halten, wenn Tensorflow.js weiter wächst und verbessert wird.

Ursprünglich bei thekevinscott.com veröffentlicht

Besonderer Dank geht an Ari Zilnik.