Sådan håndteres MNIST-billeddata i Tensorflow.js

Der er den vittighed, at 80 procent af datavidenskaben renser dataene og 20 procent klager over rengøring af dataene… datarengøring er en meget højere andel af datavidenskaben, end en udenforstående ville forvente. Faktisk er uddannelsesmodeller typisk en relativt lille andel (mindre end 10 procent) af, hvad en maskinelev eller dataforsker gør.

 - Anthony Goldbloom, administrerende direktør for Kaggle

Manipulering af data er et vigtigt trin for ethvert maskinlæringsproblem. Denne artikel tager MNIST-eksemplet til Tensorflow.js (0.11.1) og går gennem koden, der håndterer datalastningen linje for linje.

MNIST-eksempel

18 import * som tf fra '@ 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'; `

Først importerer koden Tensorflow (sørg for at transportere din kode!) Og opretter nogle konstanter, herunder:

  • IMAGE_SIZE - størrelsen på et billede (bredde og højde på 28x28 = 784)
  • NUM_CLASSES - antal etikettekategorier (et tal kan være 0-9, så der er 10 klasser)
  • NUM_DATASET_ELEMENTS - antal billeder i alt (65.000)
  • NUM_TRAIN_ELEMENTS - antal træningsbilleder (55.000)
  • NUM_TEST_ELEMENTS - antal testbilleder (10.000, også kaldet resten)
  • MNIST_IMAGES_SPRITE_PATH & MNIST_LABELS_PATH - stier til billeder og etiketter

Billederne samles i et stort billede, der ligner:

MNISTData

Næste op, startende på linje 38, er MnistData, en klasse, der afslører følgende funktioner:

  • load - ansvarlig for asynkron indlæsning af billed- og mærkningsdata
  • nextTrainBatch - indlæs den næste træningsbatch
  • nextTestBatch - indlæs den næste testbatch
  • nextBatch - en generisk funktion til at returnere den næste batch, afhængigt af om den er i træningsættet eller testsættet

Med henblik på at komme i gang vil denne artikel kun gennemgå belastningsfunktionen.

belastning

44 async belastning () {
45 // Lav en anmodning om det MNIST-spritede billede.
46 const img = nyt billede ();
47 const canvas = document.createElement ('lærred');
48 const ctx = canvas.getContext ('2d');

async er en relativt ny sprogfunktion i Javascript, som du har brug for en transpiler til.

Billedobjektet er en indbygget DOM-funktion, der repræsenterer et billede i hukommelsen. Det giver tilbagekald til, når billedet indlæses, og adgang til billedattributterne. lærred er et andet DOM-element, der giver let adgang til pixelarrays og -behandling gennem kontekst.

Da begge disse er DOM-elementer, har du ikke adgang til disse elementer, hvis du arbejder i Node.js (eller en Web Worker). For en alternativ tilgang, se nedenfor.

imgRequest

49 const imgRequest = nyt Løfte ((løst, afvis) => {
50 img.crossOrigin = '';
51 img.onload = () => {
52 img.width = img.naturalWidth;
53 img.height = img.naturalHeight;

Koden initialiserer et nyt løfte, der løses, når billedet er indlæst med succes. Dette eksempel håndterer ikke eksplicit fejltilstanden.

crossOrigin er en img-attribut, der tillader indlæsning af billeder på tværs af domæner, og kommer omkring CORS (cross-origin resource sharing) -problemer, når de interagerer med DOM. naturalWidth og naturalHeight henviser til de originale dimensioner af det ilagte billede og tjener til at håndhæve, at billedets størrelse er korrekt, når der udføres beregninger.

55 const datasætBytesBuffer =
56 nye ArrayBuffer (NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4);
57
58 const chunkSize = 5000;
59 lærred. Bredde = img. Bredde;
60 canvas.height = chunkSize;

Koden initialiserer en ny buffer, der indeholder hver pixel i hvert billede. Det multiplicerer det samlede antal billeder med størrelsen på hvert billede med antallet af kanaler (4).

Jeg tror, ​​at chunkSize bruges til at forhindre brugergrænsefladen i at indlæse for meget data i hukommelsen på en gang, selvom jeg ikke er 100% sikker.

62 for (lad i = 0; i 

Denne kode løber gennem hvert billede i spriten og initialiserer en ny TypedArray til den iteration. Derefter får kontekstbilledet en del af tegnet billede. Endelig bliver det tegne billede omdannet til billeddata ved hjælp af kontekstens getImageData-funktion, der returnerer et objekt, der repræsenterer de underliggende pixeldata.

72 for (lad j = 0; j 

Vi går gennem pixlerne og deler med 255 (den maksimale mulige værdi af en pixel) for at klemme værdierne mellem 0 og 1. Kun den røde kanal er nødvendig, da det er et gråtonebillede.

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

Denne linje tager bufferen, omlægger den til en ny TypedArray, der indeholder vores pixeldata, og løser derefter løftet. Den sidste linje (indstilling af src) begynder faktisk indlæsning af billedet, som starter funktionen.

Én ting, der først forvirrede mig var opførelsen af ​​TypedArray i forhold til dens underliggende databuffer. Du bemærker muligvis, at datasætBytesView er indstillet i løkken, men aldrig returneres.

Under hætten refererer datasetBytesView til buffertdatasættetBytesBuffer (som det initialiseres med). Når koden opdaterer pixeldataene, redigeres den indirekte værdierne af selve bufferen, som igen omarbejdes til en ny Float32Array på linje 78.

Henter billeddata uden for DOM

Hvis du er i DOM, skal du bruge DOM. Browseren (gennem lærred) tager sig af at finde ud af formatet på billeder og oversætte bufferdata til pixels. Men hvis du arbejder uden for DOM (sige i Node.js eller en Web Worker), har du brug for en alternativ tilgang.

fetch giver en mekanisme, response.arrayBuffer, som giver dig adgang til en fils underliggende buffer. Vi kan bruge dette til at læse bytes manuelt og undgå DOM fuldstændigt. Her er en alternativ tilgang til at skrive ovennævnte kode (denne kode kræver hentning, som kan polyfyldes i Node med noget som isomorphic-fetch):

const imgRequest = hente (MNIST_IMAGES_SPRITE_PATH). derefter (resp => resp.arrayBuffer ()). derefter (buffer => {
  returner nyt løfte (resolut => {
    const reader = ny PNGReader (buffer);
    return reader.parse ((err, png) => {
      const pixels = Float32Array.from (png.pixels) .map (pixel => {
        returpixel / 255;
      });
      this.datasetImages = pixels;
      beslutte();
    });
  });
});

Dette returnerer en matrixbuffer for det bestemte billede. Da jeg skrev dette, forsøgte jeg først at analysere den indgående buffer, hvilket jeg ikke ville anbefale. (Hvis du er interesseret i at gøre det, her er nogle oplysninger om, hvordan du læser en matrixbuffer til en png.) I stedet valgte jeg at bruge pngjs, der håndterer png-parsingen til dig. Når du beskæftiger dig med andre billedformater, skal du selv finde ud af, hvordan man analyserer funktionerne.

Bare ridse overfladen

At forstå datamanipulation er en vigtig komponent i maskinlæring i JavaScript. Ved at forstå vores brugssager og krav kan vi bruge et par nøglefunktioner til elegant at formatere vores data korrekt til vores behov.

Tensorflow.js-teamet ændrer kontinuerligt det underliggende data-API i Tensorflow.js. Dette kan hjælpe med at imødekomme flere af vores behov, efterhånden som API udvikler sig. Dette betyder også, at det er værd at holde sig ajour med udviklingen i API, da Tensorflow.js fortsætter med at vokse og forbedres.

Oprindeligt offentliggjort på thekevinscott.com

Særlig tak til Ari Zilnik.