Как обрабатывать данные изображения MNIST в Tensorflow.js

машинное обучение искусственный интеллект TensorFlow JavaScript

из freeCodeCamp

Автор: Кевин Скотт

Сборник "Сердце машины"

Участие: Ли Шимэн, Лу

Очистка данных — важная часть науки о данных и машинного обучения. В этой статье показано, как обрабатывать данные изображения MNIST в Tensorflow.js (0.11.1), и построчно объясняется код.


Кто-то шутит, что 80% дата-сайентистов занимаются очисткой данных, а остальные 20% жалуются на очистку данных... В работе по науке о данных очистка данных — это гораздо больше, чем думают посторонние. В целом обучение модели обычно составляет лишь небольшую часть (менее 10%) работы машинного обучения или специалиста по данным.
— Энтони Голдблум, генеральный директор Kaggle

Обработка данных — важный шаг в решении любой задачи машинного обучения. В этой статье будет использоваться образец MNIST Tensorflow.js (0.11.1) (GitHub.com/tensorflow/…) для запуска кода обработки данных построчно.

пример MNIST

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';`

Во-первых, импортируйте TensorFlow (убедитесь, что вы транспилируете код) и установите некоторые константы, в том числе:

  • IMAGE_SIZE: Размер изображения (28*28=784)
  • NUM_CLASSES: количество классов меток (это число может быть от 0 до 9, поэтому всего 10 классов)
  • NUM_DATASET_ELEMENTS: общее количество изображений (65000)
  • NUM_TRAIN_ELEMENTS: количество изображений в тренировочном наборе (55000)
  • NUM_TEST_ELEMENTS: количество изображений в тестовом наборе (10 000, также известное как остаток).
  • MNIST_IMAGES_SPRITE_PATH&MNIST_LABELS_PATH: пути к изображениям и меткам

Объедините эти изображения в одно гигантское изображение, как показано ниже:

MNISTData

Далее, начиная со строки 38, находится MnistData, который использует следующие функции:

  • load: отвечает за асинхронную загрузку изображений и маркировку данных;
  • nextTrainBatch: загрузить следующий тренировочный пакет;
  • nextTestBatch: загрузить следующую тестовую партию;
  • nextBatch: универсальная функция, которая возвращает следующий пакет, использование которого зависит от того, находится ли он в обучающем наборе или в тестовом наборе.

Эта статья является вводной, поэтому используется только функция загрузки.

load

async load() {
 // Make a request for the MNIST sprited image.
 const img = new Image();
 const canvas = document.createElement('canvas');
 const ctx = canvas.getContext('2d');

Асинхронные функции (async) — это относительно новая языковая функция в Javascript, поэтому вам нужен транспайлер.

Объекты изображения — это встроенные функции DOM, которые представляют изображения в памяти, обеспечивая обратные вызовы для доступа к свойствам изображения при загрузке изображения. Холст — это еще один элемент модели DOM, предоставляющий простой способ доступа к пиксельным массивам, а также манипулирования ими через контекст.

Поскольку оба являются элементами DOM, вам не нужно обращаться к этим элементам, если вы используете Node.js (или Web Worker). См. ниже другие альтернативы.

imgRequest

const imgRequest = new Promise((resolve, reject) => {
 img.crossOrigin = '';
 img.onload = () => {
 img.width = img.naturalWidth;
 img.height = img.naturalHeight;

Этот код инициализирует новое обещание, которое заканчивается, когда изображение успешно загружается. В примере явно не обрабатываются состояния ошибок.

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

 const datasetBytesBuffer =
 new ArrayBuffer(NUMDATASETELEMENTS * IMAGESIZE * 4);
57
58 const chunkSize = 5000;
59 canvas.width = img.width;
60 canvas.height = chunkSize;

Этот код инициализирует новый буфер, содержащий каждый пиксель каждого изображения. Он умножает общее количество изображений на размер и количество каналов каждого изображения.

Я думаю, что полезность chunkSize заключается в том, чтобы предотвратить одновременную загрузку пользовательским интерфейсом слишком большого количества данных в память, но я не уверен на 100%.

62 for (let i = 0; i < NUMDATASETELEMENTS / chunkSize; i++) {
63 const datasetBytesView = new Float32Array(
64 datasetBytesBuffer, i * IMAGESIZE * chunkSize * 4,
 IMAGESIZE * chunkSize);
66 ctx.drawImage(
67 img, 0, i * chunkSize, img.width, chunkSize, 0, 0, img.width,
68 chunkSize);
69
70 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

Код выполняет итерацию по каждому изображению спрайта и инициализирует новый TypedArray для итерации. Далее контекстное изображение получает нарисованный блок изображения. Наконец, нарисованное изображение преобразуется в данные изображения с помощью контекстной функции getImageData, которая возвращает объект, представляющий базовые данные пикселей.

72 for (let j = 0; j < imageData.data.length / 4; j++) {
73 // All channels hold an equal value since the image is grayscale, so
74 // just read the red channel.
75 datasetBytesView[j] = imageData.data[j * 4] / 255;
76 }
77 }

Мы перебираем эти пиксели и делим на 255 (максимально возможное значение пикселя), чтобы ограничить значение от 0 до 1. Необходим только красный канал, потому что это изображение в градациях серого.

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

Эта строка создает буфер, сопоставляет его с новым массивом TypedArray, который содержит наши пиксельные данные, и завершает промис. На самом деле последняя строка (установка атрибута src) фактически запускает функцию и загружает изображение.

Сначала меня беспокоило поведение TypedArray по отношению к базовому буферу данных. Вы могли заметить, что datasetBytesView устанавливается в цикле, но никогда не возвращается.

datasetBytesView ссылается на datasetBytesBuffer буфера (используется для инициализации). Когда код обновляет данные пикселя, он косвенно редактирует значение буфера, а затем преобразует его в новый Float32Array в строке 78.

Получить данные изображения вне DOM

Если вы находитесь в DOM, просто используйте DOM, браузер (через холст) отвечает за определение формата изображения и преобразование данных буфера в пиксели. Но если вы работаете вне DOM (то есть используете Node.js или Web Workers), вам нужна альтернатива.

fetch предоставляет механизм под названием response.arrayBuffer, который дает вам доступ к основному буферу файла. Мы можем использовать этот метод для ручного чтения байтов, не избегая полностью DOM. Вот альтернативный способ написания приведенного выше кода (для этого метода требуется выборка, которая может быть поли-заполнена в Node такими методами, как isomorphic-fetch):

const imgRequest = fetch(MNISTIMAGESSPRITE_PATH).then(resp => resp.arrayBuffer()).then(buffer => {
 return new Promise(resolve => {
 const reader = new PNGReader(buffer);
 return reader.parse((err, png) => {
 const pixels = Float32Array.from(png.pixels).map(pixel => {
 return pixel / 255;
 });
 this.datasetImages = pixels;
 resolve();
 });
 });
});

Это возвращает буферизованный массив для определенного изображения. Пока я писал это, я сначала попытался разобрать входящие буферы, но я не рекомендую этого делать. Я рекомендую использовать pngjs для разбора png, если это необходимо. При работе с изображениями в других форматах вам необходимо написать свои собственные функции разбора.

быть дальше

Понимание манипулирования данными — важная часть машинного обучения с помощью JavaScript. Понимая варианты использования и требования, описанные в этой статье, мы можем форматировать данные по мере необходимости, используя только несколько ключевых функций.

Команда TensorFlow.js улучшает базовый API данных TensorFlow.js, что помогает удовлетворить больше потребностей. Это также означает, что по мере того, как TensorFlow.js продолжает совершенствоваться и развиваться, API будет продолжать развиваться, чтобы не отставать от темпов развития.