Jak zacházet s obrazovými daty MNIST v Tensorflow.js

Je vtip, že 80 procent datových věd je čištění dat a 20 procent si stěžuje na čištění dat ... čištění dat je mnohem vyšší podíl datových věd, než by očekával outsider. Modely výcviku jsou ve skutečnosti relativně malým podílem (méně než 10 procent) toho, co strojový student nebo vědec s údaji dělá.

 - Anthony Goldbloom, generální ředitel společnosti Kaggle

Manipulace s daty je zásadním krokem pro jakýkoli problém se strojovým učením. Tento článek vezme příklad MNIST pro Tensorflow.js (0.11.1) a projde kódem, který zpracovává načítání dat řádek po řádku.

Příklad MNIST

18 import * jako tf z '@ tensorflow / tfjs';
19
20 konst. IMAGE_SIZE = 784;
21 konstant NUM_CLASSES = 10;
22 const NUM_DATASET_ELEMENTS = 65000;
23
24 konstant NUM_TRAIN_ELEMENTS = 55000;
25 konstant 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'; `

Nejprve kód naimportuje Tensorflow (ujistěte se, že transponujete svůj kód!) A stanoví některé konstanty, včetně:

  • IMAGE_SIZE - velikost obrázku (šířka a výška 28x28 = 784)
  • NUM_CLASSES - počet kategorií štítků (číslo může být 0-9, takže existuje 10 tříd)
  • NUM_DATASET_ELEMENTS - celkový počet obrázků (65 000)
  • NUM_TRAIN_ELEMENTS - počet tréninkových obrázků (55 000)
  • NUM_TEST_ELEMENTS - počet testovacích obrázků (10 000, tzv. Zbytek)
  • MNIST_IMAGES_SPRITE_PATH & MNIST_LABELS_PATH - cesty k obrázkům a štítkům

Obrázky jsou zřetězeny do jednoho obrovského obrazu, který vypadá takto:

MNISTData

Další, začínající na řádku 38, je MnistData, třída, která odhaluje následující funkce:

  • zatížení - odpovídá za asynchronní načítání obrazu a údajů o štítcích
  • nextTrainBatch - načte další tréninkovou dávku
  • nextTestBatch - načte další testovací dávku
  • nextBatch - obecná funkce pro vrácení další dávky v závislosti na tom, zda je v tréninkové sadě nebo testovací sadě

Pro účely zahájení bude tento článek procházet pouze funkcí načítání.

zatížení

44 asynchronní zatížení () {
45 // Vytvořte požadavek na MNIST skvrnitý obraz.
46 const img = new Image ();
47 const canvas = document.createElement ('canvas');
48 const ctx = canvas.getContext ('2d');

async je relativně nová jazyková funkce v Javascriptu, pro kterou budete potřebovat transpiler.

Objekt Image je nativní funkce DOM, která představuje obrázek v paměti. Poskytuje zpětná volání, když je obraz načten, s přístupem k atributům obrazu. canvas je další prvek DOM, který poskytuje snadný přístup k pixelovým polím a zpracování prostřednictvím kontextu.

Protože oba tyto prvky jsou DOM, pokud pracujete v Node.js (nebo Web Worker), nemáte k těmto prvkům přístup. Alternativní přístup viz níže.

imgRequest

49 const imgRequest = new Promise ((vyřešení, odmítnutí) => {
50 img.crossOrigin = '';
51 img.onload = () => {
52 img.width = img.naturalWidth;
53 img.height = img.naturalHeight;

Kód inicializuje nový slib, který bude vyřešen, jakmile bude obraz úspěšně načten. Tento příklad explicitně nezpracovává chybový stav.

crossOrigin je atribut img, který umožňuje načítání obrázků napříč doménami a při interakci s doménou obchází problémy CORS (sdílení zdrojů křížového původu). naturalWidth a naturalHeight se vztahují k původním rozměrům načteného obrazu a slouží k vynucení správnosti velikosti obrázku při provádění výpočtů.

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

Kód inicializuje novou vyrovnávací paměť, která obsahuje každý pixel každého obrázku. Vynásobí celkový počet obrázků velikostí každého obrázku počtem kanálů (4).

Věřím, že chunkSize se používá k zabránění uživatelského rozhraní v načítání příliš velkého množství dat do paměti najednou, i když si nejsem 100% jistý.

62 pro (let i = 0; i 

Tento kód prochází každým obrázkem v sprite a inicializuje nový typedArray pro tuto iteraci. Kontextový obrázek pak získá kus nakresleného obrázku. Nakonec je tento nakreslený obrázek převeden na obrazová data pomocí funkce getImageData kontextu, která vrací objekt představující podkladová obrazová data.

72 pro (let j = 0; j 

Propleteme pixely a vydělíme 255 (maximální možná hodnota pixelu), abychom hodnoty upnuli mezi 0 a 1. Je nutný pouze červený kanál, protože se jedná o obraz ve stupních šedi.

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

Tento řádek vezme vyrovnávací paměť, přepíše ji do nového TypedArray, který obsahuje naše pixelová data, a poté vyřeší Promise. Poslední řádek (nastavení src) skutečně začne načítat obrázek, který spustí funkci.

Jedna věc, která mě zpočátku zmátla, bylo chování TypedArray ve vztahu k jeho základní datové vyrovnávací paměti. Možná si všimnete, že datasetBytesView je nastaven ve smyčce, ale nikdy se nevrací.

Pod kapotou odkazuje datasetBytesView na vyrovnávací paměť datasetBytesBuffer (se kterou je inicializován). Když kód aktualizuje data pixelů, nepřímo upravuje hodnoty samotné vyrovnávací paměti, která se zase přepočítává do nového Float32Array na řádku 78.

Načítání obrazových dat mimo DOM

Pokud jste v DOMu, měli byste použít DOM. Prohlížeč (přes plátno) se stará o vymezení formátu obrázků a převedení dat vyrovnávací paměti na pixely. Ale pokud pracujete mimo DOM (řekněme v Node.js nebo Web Worker), budete potřebovat alternativní přístup.

fetch poskytuje mechanismus response.arrayBuffer, který vám dává přístup k základní vyrovnávací paměti souboru. Můžeme to použít k ručnímu přečtení bytů a zcela se vyhnout DOM. Zde je alternativní přístup k psaní výše uvedeného kódu (tento kód vyžaduje načtení, které může být v Node vyplněno něčím, jako je izomorfní načtení):

const imgRequest = fetch (MNIST_IMAGES_SPRITE_PATH) .then (resp => resp.arrayBuffer ()). (buffer => {
  vrátit nový Promise (resol => {
    const reader = nový PNGReader (buffer);
    return reader.parse ((err, png) => {
      const pixels = Float32Array.from (png.pixels) .map (pixel => {
        návratový pixel / 255;
      });
      this.datasetImages = pixely;
      odhodlání();
    });
  });
});

Tím se vrátí vyrovnávací paměť pole pro konkrétní obrázek. Když jsem to psal, poprvé jsem se pokusil analyzovat příchozí buffer sám, což bych nedoporučoval. (Pokud vás to zajímá, zde je několik informací o tom, jak číst vyrovnávací paměť pole pro png.) Místo toho jsem se rozhodl použít pngjs, který za vás zpracovává analýzu png. Když se zabýváte jinými formáty obrázků, budete muset zjistit funkce analýzy sami.

Jen škrábání povrchu

Porozumění manipulaci s daty je klíčovou součástí strojového učení v JavaScriptu. Pochopení našich případů použití a požadavků, můžeme použít několik klíčových funkcí pro elegantní formátování našich dat správně pro naše potřeby.

Tým Tensorflow.js neustále mění základní datové API v Tensorflow.js. To může pomoci vyhovět více našim potřebám s vývojem API. To také znamená, že stojí za to držet krok s vývojem API, protože Tensorflow.js neustále roste a je vylepšován.

Původně publikováno na thekevinscott.com

Zvláštní poděkování patří Ari Zilnikovi.