Как да се справим с MNIST данни от изображения в Tensorflow.js

Има шегата, че 80 процента от научните данни почистват данните, а 20 процента се оплакват от почистването на данните ... почистването на данните е много по-голям дял от науката за данни, отколкото би очаквал външен човек. Всъщност моделите за обучение обикновено са сравнително малка част (по-малко от 10 процента) от това, което прави машинен ученик или учен с данни.

 - Антъни Голдблум, изпълнителен директор на Kaggle

Манипулирането на данни е решаваща стъпка за всеки проблем с машинно обучение. Тази статия ще вземе примера на MNIST за Tensorflow.js (0.11.1) и ще премине през кода, който обработва зареждането на данни ред по ред.

MNIST пример

18 импортиране * като tf от „@ 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'; `

Първо, кодът импортира Tensorflow (уверете се, че преобразувате кода си!) И установява някои константи, включително:

  • IMAGE_SIZE - размерът на изображението (ширина и височина 28x28 = 784)
  • NUM_CLASSES - брой категории етикети (число може да бъде 0-9, така че има 10 класа)
  • NUM_DATASET_ELEMENTS - брой изображения общо (65 000)
  • NUM_TRAIN_ELEMENTS - брой тренировъчни изображения (55 000)
  • NUM_TEST_ELEMENTS - брой на тестовите изображения (10 000, известен още като остатъка)
  • MNIST_IMAGES_SPRITE_PATH & MNIST_LABELS_PATH - пътища до изображенията и етикетите

Изображенията са свързани в едно огромно изображение, което изглежда така:

MNISTData

Следващ ред, започващ от ред 38, е MnistData, клас, който излага следните функции:

  • натоварване - отговаря за асинхронното зареждане на изображението и данните за етикетиране
  • nextTrainBatch - заредете следващата тренировъчна партида
  • nextTestBatch - заредете следващата тестова партида
  • nextBatch - обща функция за връщане на следващата партида, в зависимост от това дали е в тренировъчния набор или тестовия набор

За целите на стартирането тази статия ще премине само през функцията за зареждане.

натоварване

44 async load () {
45 // Направете заявка за справочно изображение на MNIST.
46 const img = ново изображение ();
47 const canvas = document.createElement ('canvas');
48 const ctx = canvas.getContext ('2d');

async е сравнително нова езикова функция в Javascript, за която ще ви е необходим транспилатор.

Обектът Image е нативна DOM функция, която представлява изображение в паметта. Той осигурява връщане на повиквания за времето, когато изображението е заредено заедно, с достъп до атрибутите на изображението. canvas е друг DOM елемент, който осигурява лесен достъп до пикселни масиви и обработка чрез контекст.

Тъй като и двете са DOM елементи, ако работите в Node.js (или уеб работник), няма да имате достъп до тези елементи. За алтернативен подход вижте по-долу.

imgRequest

49 const imgRequest = ново обещание ((разрешаване, отхвърляне) => {
50 img.crossOrigin = '';
51 img.onload = () => {
52 img.width = img.naturalWidth;
53 img.height = img.naturalHeight;

Кодът инициализира ново обещание, което ще бъде разрешено след успешното зареждане на изображението. Този пример не обработва изрично състоянието на грешката.

crossOrigin е атрибут img, който позволява зареждането на изображения в домейни и заобикаля проблемите с CORS (кръстосано споделяне на ресурси) при взаимодействие с DOM. naturalWidth и naturalHeight се отнасят до оригиналните размери на зареденото изображение и служат за налагане на правилния размер на изображението при извършване на изчисления.

55 const набор от данниBytesBuffer =
56 нови ArrayBuffer (NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4);
57
58 const chunkSize = 5000;
59 canvas.width = img.width;
60 canvas.height = chunkSize;

Кодът инициализира нов буфер, който да съдържа всеки пиксел от всяко изображение. Той умножава общия брой изображения по размера на всяко изображение по броя на каналите (4).

Вярвам, че chunkSize се използва, за да се предотврати зареждането на твърде много данни в паметта наведнъж, въпреки че не съм 100% сигурен.

62 за (нека i = 0; i 

Този код преглежда през всяко изображение в спрайта и инициализира нов TypedArray за тази итерация. След това контекстното изображение получава част от начертаното изображение. И накрая, това изтеглено изображение се превръща в данни от изображението с помощта на функцията getImageData на контекста, която връща обект, представляващ основните данни за пикселите.

72 за (нека j = 0; j 

Преминаваме през пикселите и разделяме на 255 (максималната възможна стойност на пиксел), за да закрепим стойностите между 0 и 1. Необходим е само червеният канал, тъй като това е изображение в сиви скали.

78 this.datasetImages = нов Float32Array (набор данниBytesBuffer);
79
80 резолюция ();
81};
82 img.src = MNIST_IMAGES_SPRITE_PATH;
83});

Този ред взема буфера, преобразува го в нов TypedArray, който съхранява нашите пикселни данни, и след това разрешава Promise. Последният ред (настройване на src) всъщност започва зареждането на изображението, което стартира функцията.

Едно нещо, което ме обърка в началото, беше поведението на TypedArray по отношение на неговия основен буфер на данни. Може да забележите, че наборът данниBytesView е зададен в цикъла, но никога не се връща.

Под капака, dataBytesView препраща към набора от данни на буфераBytesBuffer (с който е инициализиран). Когато кодът актуализира пикселните данни, той индиректно редактира стойностите на самия буфер, който от своя страна се преработва в нов Float32Array на ред 78.

Извличане на данни от изображения извън DOM

Ако сте в DOM, трябва да използвате DOM. Браузърът (чрез платното) се грижи за намирането на формата на изображенията и превеждането на буферните данни в пиксели. Но ако работите извън DOM (да речем, в Node.js или Web Worker), ще ви е необходим алтернативен подход.

fetch предоставя механизъм, response.arrayBuffer, който ви дава достъп до основния буфер на файла. Можем да използваме това, за да четем байтовете ръчно, като избягваме DOM изцяло. Ето алтернативен подход за писане на горния код (този код изисква извличане, което може да бъде полифилирано в Node с нещо като isomorphic-fetch):

const imgRequest = извличане (MNIST_IMAGES_SPRITE_PATH) .then (resp => resp.arrayBuffer ()). тогава (буфер => {
  върнете ново обещание (реши = = {
    const читател = нов PNGReader (буфер);
    върнете читателя.parse ((грешка, png) => {
      const pixels = Float32Array.from (png.pixels) .map (pixel => {
        връщащ пиксел / 255;
      });
      this.datasetImages = пиксели;
      решаване ();
    });
  });
});

Това връща буфер на масив за конкретното изображение. Когато пиша това, първо се опитах да анализирам входящия буфер, който не бих препоръчал. (Ако се интересувате от това, ето малко информация за това как да четете буфер на масив за png.) Вместо това избрах да използвам pngjs, който обработва png парсинга за вас. Когато се занимавате с други формати на изображения, ще трябва сами да разберете функциите за анализ.

Просто надраскване на повърхността

Разбирането на манипулирането на данни е ключов компонент на машинното обучение в JavaScript. Разбирайки нашите случаи и изисквания за използване, можем да използваме няколко основни функции, за да форматираме правилно нашите данни правилно за нашите нужди.

Екипът на Tensorflow.js непрекъснато променя API на основните данни в Tensorflow.js. Това може да помогне за задоволяване на повече от нуждите ни с развитието на API. Това също означава, че си струва да бъдете в крак с развитието на API, тъй като Tensorflow.js продължава да расте и да се подобрява.

Първоначално публикуван в thekevinscott.com

Специални благодарности на Ари Жилник.