Як поводитися з 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 - шляхи до зображень та міток

Зображення об'єднані в одне величезне зображення, яке виглядає так:

MNISTДані

Наступним, починаючи з рядка 38, є MnistData, клас, який має такі функції:

  • навантаження - відповідає за асинхронне завантаження зображення та даних маркування
  • nextTrainBatch - завантажте наступну навчальну партію
  • nextTestBatch - завантажте наступний тестовий пакет
  • nextBatch - загальна функція для повернення наступної партії, залежно від того, знаходиться вона у навчальному наборі чи тестовому наборі

Для початку роботи ця стаття буде проходити лише через функцію навантаження.

навантаження

44 async load () {
45 // Зробіть запит на спрацьоване зображення MNIST.
46 const img = new Image ();
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 databaseBytesBuffer =
56 нових ArrayBuffer (NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4);
57
58 const chunkSize = 5000;
59 canvas.width = img.width;
60 canvas.height = шматокSize;

Код ініціалізує новий буфер, щоб містити кожен піксель кожного зображення. Він помножує загальну кількість зображень на розмір кожного зображення на кількість каналів (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, який містить наші піксельні дані, а потім вирішує Обіцяння. Останній рядок (встановлення src) фактично починає завантажувати зображення, яке запускає функцію.

Одне, що мене спочатку збентежило, - це поведінка TypedArray щодо його буфера даних. Ви можете помітити, що databaseBytesView встановлюється в циклі, але ніколи не повертається.

Під кришкою набір данихBytesView посилається на набір даних буфераBytesBuffer (з яким він ініціалізується). Коли код оновлює дані пікселів, він опосередковано редагує значення самого буфера, який, у свою чергу, переробляється на новий Float32Array у рядку 78.

Отримання даних зображення поза DOM

Якщо ви перебуваєте в домені, вам слід використовувати домен. Веб-переглядач (через полотно) дбає про те, щоб визначити формат зображень та перевести дані буфера в пікселі. Але якщо ви працюєте за межами DOM (скажімо, в Node.js або веб-працівнику), вам знадобиться альтернативний підхід.

fetch забезпечує механізм response.arrayBuffer, який надає вам доступ до основного буфера файлу. Ми можемо використовувати це для читання байтів вручну, уникаючи DOM повністю. Ось альтернативний підхід до написання вищевказаного коду (цей код вимагає отримання, який може бути заповнений у Вузлі чимось на зразок ізоморфним-вибором):

const imgRequest = fetch (MNIST_IMAGES_SPRITE_PATH) .then (resp => resp.arrayBuffer ()). тоді (буфер => {
  повернути нове Обіцяння (вирішити => {
    const читач = новий PNGReader (буфер);
    return reader.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

Особлива подяка Арі Зільнику.