Привет всем, коллеги!

Возможно, поклонники библиотеки Tensorflow, уже заметившие у нас в предзаказе эту книгу, также присматривались к возможностям машинного и глубокого обучения в браузере, тем более, что тему не обошел вниманием и сам Франсуа Шолле. Интересующихся приглашаем под кат, где рассказано, как при помощи библиотеки Tensorflow.js распознаются изображения.

TensorFlow.js – новая версия популярной опенсорсной библиотеки, обогащающей язык JavaScript возможностями глубокого обучения. Теперь разработчики могут определять, обучать и запускать модели при помощи высокоуровневого библиотечного API.

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

Tensorflow.js начиналась как фронтендовая библиотека для работы в браузере, но в этом году в нее была добавлена экспериментальная поддержка Node.js. Таким образом, TensorFlow.js можно использовать и в бэкендовых приложениях на JavaScript, что совершенно избавляет нас от необходимости прибегать к Python.

Читая об этой библиотеке, я решил испробовать ее на простой задаче…
Использовать TensorFlow.js для визуального распознавания образов на изображениях при применении JavaScript из Node.js
К сожалению, документация и примеры кода в основном описывают использование этой библиотеки в браузере, Проектные утилиты, призванные упростить загрузку и использование предобученных моделей на момент написания статьи еще не поддерживали Node.js. Мне пришлось потратить немало времени, чтобы хорошенько прочитать исходники на Typescript для этой библиотеки.

Однако, через несколько дней долбежки я все-таки это сделал! Ура!

Прежде чем перейти к подробному разбору кода, давайте поговорим о других реализациях библиотеки TensorFlow.

TensorFlow

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

Это библиотека, выпущенная Google в ноябре 2015, исходно была написана на Python. Для обучения и оценки создаваемых моделей в ней применяются вычисления на CPU или GPU. Изначально эта библиотека создавалась для работы на высокопроизводительных серверах с использованием ресурсозатратных GPU.

Последние обновления позволили оптимизировать эту библиотеку и использовать в средах с более ограниченными ресурсами – например, на мобильных устройствах и в веб-браузерах.

TensorFlow Lite

Tensorflow Lite, облегченная версия этой библиотеки для мобильных устройств и встраиваемых систем, была выпущена в мае 2017 года. Вместе с ней предоставляется новый набор предобученных глубоких моделей для задач, связанных с распознаванием образов; эта коллекция именуется MobileNet. Модели MobileNet были разработаны специально для эффективной работы в окружениях с ограниченным количеством ресурсов, например, на мобильных устройствах.

TensorFlow.js

Вслед за Tensorflow Lite в марте 2018 года была анонсирована TensorFlow.js. Эта версия библиотеки предназначена для работы в браузере и базируется на более раннем проекте под названием deeplearn.js. WebGL обеспечивает GPU-доступ к библиотеки. Разработчики используют API на JavaScript для обучения, загрузки и запуска моделей.

Позже TensorFlow.js была расширена для работы с Node.js, для этого применяется библиотечное дополнение tfjs-node.

Импорт имеющихся моделей в TensorFlow.js

Готовые модели TensorFlow и Keras можно выполнять при помощи библиотеки TensorFlow.js. Перед выполнением модели необходимо перевести в новый формат при помощи этого инструмента. Предобученные и преобразованные модели для классификации изображений, определения поз и обнаружения k-ближайших соседей доступны на Github.

Использование TensorFlow.js с Node.js

Установка библиотек TensorFlow

TensorFlow.js можно установить из реестра NPM.


npm install @tensorflow/tfjs @tensorflow/tfjs-node
// или...
npm install @tensorflow/tfjs @tensorflow/tfjs-node-gpu

В обоих расширениях для Node.js используются нативные зависимости, которые будут компилироваться по запросу.

Загрузка библиотек TensorFlow

API на JavaScript для Tensorflow предоставляется из core-библиотеки. В модулях-расширениях, обеспечивающих поддержку Node.js, дополнительные API не предоставляются.

	const tf = require('@tensorflow/tfjs')
// Загружаем привязку (вычисления CPU)
require('@tensorflow/tfjs-node')
// Или загружаем привязку (вычисления GPU)
require('@tensorflow/tfjs-node-gpu')

Загрузка моделей TensorFlow

В TensorFlow.js предоставляется библиотека NPM (tfjs-models), упрощающая загрузку предобученных и преобразованных моделей для классификации изображений, определения поз и обнаружения k-ближайших соседей.

Модель MobileNet для классификации изображений – это глубокая нейронная сеть, обученная различать 1000 различных классов изображений.

В файле README к проекту в качестве примера приведен следующий код, используемый для загрузки модели.

import * as mobilenet from '@tensorflow-models/mobilenet';

// загрузить модель
const model = await mobilenet.load();

Одна из первых проблем, с которыми мне довелось столкнуться – оказывается, этот код не работает с Node.js.

Error: browserHTTPRequest is not supported outside the web browser.

Изучив исходный код, видим, что библиотека mobilenet – это обертка для класса tf.Model. При вызове метод load() автоматически загружает нужные файлы моделей, расположенные по внешнему HTTP-адресу, и инстанцирует модель TensorFlow.

Расширение Node.js на момент написания статьи еще не поддерживало HTTP-запросы для динамического извлечения моделей. Оставалось только вручную загружать модели в файловую систему.

Однако, вчитавшись в исходный код библиотеки, я нашел обходной путь…

Загрузка моделей из файловой системы

В случае, если класс MobileNet создается вручную, можно не вызывать метод load модуля, а перезаписать автоматически генерируемую переменную path, содержащую HTTP-адрес модели, заменив этот адрес на локальный путь в файловой системе. После этого при вызове метода load в экземпляре класса будет срабатывать класса загрузчика файловой системы; в таком случае мы отказываемся от использования браузерного HTTP-загрузчика.

const path = "mobilenet/model.json"
const mn = new mobilenet.MobileNet(1, 1);
mn.path = `file://${path}`
await mn.load()

Круто, все работает!

Но откуда же берутся файлы моделей?

Модели MobileNet

Модели для TensorFlow.js состоят из файлов двух типов: файл конфигурации модели, хранимый в формате JSON, и веса моделей, хранимые в двоичном формате. Веса моделей зачастую фрагментируются на множество частей для оптимизации кэширования в браузерах.

Рассмотрев автоматический код загрузки для моделей MobileNet, видим, что модели, их конфигурации и весовые фрагменты извлекаются из общедоступного контейнера по следующему адресу.

https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v${version}_${alpha}_${size}/

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

В исходном коде указано, что только модели версии MobileNet v1 можно загружать при помощи библиотеки tensorflow-models/mobilenet.

Код извлечения по HTTP загружает файл model.json из места хранения, а затем рекурсивно выбирает все фрагменты моделей с весовыми коэффициентами, на которые стоят ссылки. Это файлы в формате groupX-shard1of1.

Скачивание моделей вручную

Если нужно сохранить все файлы моделей в файловой системе, то можно поступить так: извлечь конфигурационный файл модели, разобрать синтаксис всех весовых файлов, на которые стоят ссылки в конфигурационном файле, после чего скачать каждый весовой файл вручную.
Я собирался использовать модуль MobileNet V1 с альфа-значением 1.0 и изображение размером 224 пиксела. Так я получаю следующий URL для конфигурационного файла модели.

https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v1_1.0_224/model.json

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

$ cat model.json | jq -r ".weightsManifest[].paths[0]"
group1-shard1of1
group2-shard1of1
group3-shard1of1
...

При помощи инструмента sed можно поставить перед именем каждого элемента HTTP URL, чтобы сгенерировать URL для каждого весового файла.

$ cat model.json | jq -r ".weightsManifest[].paths[0]" | sed 's/^/https:\/\/storage.googleapis.com\/tfjs-models\/tfjs\/mobilenet_v1_1.0_224\//'
https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v1_1.0_224/group1-shard1of1
https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v1_1.0_224/group2-shard1of1
https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v1_1.0_224/group3-shard1of1
...

Команды parallel и curl позволяют затем скачать все эти файлы в мой локальный каталог.

cat model.json | jq -r ".weightsManifest[].paths[0]" | sed 's/^/https:\/\/storage.googleapis.com\/tfjs-models\/tfjs\/mobilenet_v1_1.0_224\//' |  parallel curl -O

Классификация изображений

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

const img = document.getElementById('img');

// Классифицируем изображение
const predictions = await model.classify(img);

Это не работает в Node.js из-за отсутствия поддержки DOM.

Метод classify принимает разнообразные элементы DOM (canvas, video, image) и автоматически извлекает и преобразует «картиночные» байты из этих элементов в класс tf.Tensor3D, используемый в качестве ввода модели. В качестве альтернативы вхо��ную информацию tf.Tensor3D можно передавать напрямую.

Я решил не пытаться использовать внешний пакет для имитации DOM-элемента вручную, а обнаружил, что tf.Tensor3D проще собрать вручную.

Генерируем Tensor3D из изображения

Читая исходный код метода, применяемого для преобразования элементов DOM в классы Tensor3D, находим, что для генерации класса Tensor3D используются следующие входные параметры.

const values = new Int32Array(image.height * image.width * numChannels);
// заполняем пикселы информацией пиксельных каналов, взятой с картинки 
const outShape = [image.height, image.width, numChannels];
const input = tf.tensor3d(values, outShape, 'int32');

pixels – это двухмерный массив типа (Int32Array), содержащий последовательный список канальных значений для каждого пиксела. numChannels – это количество канальных значений на пиксел.

Создание входных значений для JPEG

Библиотека jpeg-js – это JPEG-кодировщик/декодировщик для Node.js, написанный на чистом JavaScript. При помощи этой библиотеки можно извлечь RGB-значения для каждого пиксела.

const pixels = jpeg.decode(buffer, true);

В результате получим Uint8Array с четырьмя канальными значениями (RGBA) на каждый пиксел (width * height). В модели MobileNet для классификации используется всего три цветовых канала (RGB), альфа-канал игнорируется. Этот код преобразует четырехканальный массив в верную трехканальную версию.

	const numChannels = 3;
const numPixels = image.width * image.height;
const values = new Int32Array(numPixels * numChannels);

for (let i = 0; i < numPixels; i++) {
  for (let channel = 0; channel < numChannels; ++channel) {
    values[i * numChannels + channel] = pixels[i * 4 + channel];
  }
}

Требования ко входным значениям для моделей MobileNet

Используемая здесь модель MobileNet классифицирует изображения высотой и шириной по 224 пиксела. Входные тензоры должны содержать значения с плавающей точкой в диапазоне от -1 до 1 для каждого из трех канальных значений каждого пиксела.

Входные значения для изображений с другой размерностью перед классификацией нужно пересчитать в правильный размер. Кроме того, пиксельные значения, получаемые от JPEG-декодера, находятся в диапазоне 0 — 255, а не -1 — 1. Эти значения также необходимо преобразовать перед классификацией.

В TensorFlow.js есть библиотечные методы, упрощающие этот процесс, но, что еще лучше, есть специальная библиотека tfjs-models/mobilenet, автоматически решающая эту проблему!

Разработчик может передавать входные Tensor3D типа int32, а также различные размерности методу classify, который перед классификацией переводит входные значения в правильный формат. То есть, нам здесь ничего делать не приходится. Супер!

Получение прогнозов

Модели MobileNet в Tensorflow обучаются распознаванию объектов из 1000 важнейших классов из множества данных ImageNet. На выходе модели дают вероятностные значения, характеризующие, каковы шансы найти данные объекты на классифицируе��ом изображении.

Полный список обученных классов для используемой модели находится в этом файле.

Библиотека tfjs-models/mobilenet предлагает метод classify в классе MobileNet, возвращающий топ-X наиболее вероятных классов, исходя из того, что изображено на картинке.

const predictions = await mn_model.classify(input, 10);

predictions – это массив из X классов и вероятностей в следующем формате.

	{
  className: 'panda',
  probability: 0.9993536472320557
}

Пример

Итак, мы разобрались, как использовать библиотеку TensorFlow.js и модели MobileNet в Node.js, а теперь рассмотрим, как этот скрипт классифицирует изображение, заданное в качестве аргумента командной строки.

Исходный код

Сохраните этот файл скрипта и дескриптор пакета в локальных файлах.

{
	  "name": "tf-js",
	  "version": "1.0.0",
	  "main": "script.js",
	  "license": "MIT",
	  "dependencies": {
	    "@tensorflow-models/mobilenet": "^0.2.2",
	    "@tensorflow/tfjs": "^0.12.3",
	    "@tensorflow/tfjs-node": "^0.1.9",
	    "jpeg-js": "^0.3.4"
	  }
	}

const tf = require('@tensorflow/tfjs')
	const mobilenet = require('@tensorflow-models/mobilenet');
	require('@tensorflow/tfjs-node')
	

	const fs = require('fs');
	const jpeg = require('jpeg-js');
	

	const NUMBER_OF_CHANNELS = 3
	

	const readImage = path => {
	  const buf = fs.readFileSync(path)
	  const pixels = jpeg.decode(buf, true)
	  return pixels
	}
	

	const imageByteArray = (image, numChannels) => {
	  const pixels = image.data
	  const numPixels = image.width * image.height;
	  const values = new Int32Array(numPixels * numChannels);
	

	  for (let i = 0; i < numPixels; i++) {
	    for (let channel = 0; channel < numChannels; ++channel) {
	      values[i * numChannels + channel] = pixels[i * 4 + channel];
	    }
	  }
	

	  return values
	}
	

	const imageToInput = (image, numChannels) => {
	  const values = imageByteArray(image, numChannels)
	  const outShape = [image.height, image.width, numChannels];
	  const input = tf.tensor3d(values, outShape, 'int32');
	

	  return input
	}
	

	const loadModel = async path => {
	  const mn = new mobilenet.MobileNet(1, 1);
	  mn.path = `file://${path}`
	  await mn.load()
	  return mn
	}
	

	const classify = async (model, path) => {
	  const image = readImage(path)
	  const input = imageToInput(image, NUMBER_OF_CHANNELS)
	

	  const  mn_model = await loadModel(model)
	  const predictions = await mn_model.classify(input)
	

	  console.log('classification results:', predictions)
	}
	

	if (process.argv.length !== 4) throw new Error('incorrect arguments: node script.js <MODEL> <IMAGE_FILE>')
	

	classify(process.argv[2], process.argv[3])

Тестирование

Скачайте файлы модели в каталог mobilenet, следуя вышеизложенным инструкциям.
Установите зависимости проекта при помощи NPM

npm install

Скачайте образец JPEG-файла для классификации

wget http://bit.ly/2JYSal9 -O panda.jpg



Запустите скрипт, аргументами которого послужат файл модели и входное изображение.

node script.js mobilenet/model.json panda.jpg

Если все сработало верно, то в консоли должен появиться следующий вывод.

	classification results: [ {
    className: 'giant panda, panda, panda bear, coon bear',
    probability: 0.9993536472320557
} ]

Изображение верно классифицировано как содержащее панду с вероятностью 99.93%!

Заключение

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

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