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

Существует шутка о том, что 80 процентов науки о данных очищают данные, а 20 процентов жалуются на очистку данных… очистка данных - это гораздо более высокая доля науки о данных, чем может ожидать посторонний. На самом деле обучающие модели, как правило, составляют относительно небольшую часть (менее 10 процентов) от того, что делает обучающийся на компьютере или специалист по данным.

 - Энтони Голдблум, генеральный директор Kaggle

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

Пример MNIST

18 импортировать * как tf из «@ tenorflow / 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, класс, который предоставляет следующие функции:

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

В целях начала, эта статья будет проходить только через функцию загрузки.

нагрузка

44 асинхронная нагрузка () {
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 (или в Web Worker), у вас не будет доступа к этим элементам. Для альтернативного подхода, см. Ниже.

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 datasetBytesBuffer =
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 = new Float32Array (datasetBytesBuffer);
79
80 разрешения ();
81};
82 img.src = MNIST_IMAGES_SPRITE_PATH;
83});

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

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

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

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

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

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

const imgRequest = fetch (MNIST_IMAGES_SPRITE_PATH) .then (resp => resp.arrayBuffer ()). then (buffer => {
  вернуть новое обещание (решить => {
    const reader = новый PNGReader (буфер);
    return reader.parse ((err, png) => {
      const пикселей = Float32Array.from (png.pixels) .map (pixel => {
        обратный пиксель / 255;
      });
      this.datasetImages = пикселей;
      разрешить();
    });
  });
});

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

Просто царапая поверхность

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

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

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

Отдельное спасибо Ари Зильнику.