🏆 Рейтинг ІТ-работодателей 2019: уже собрано более 5000 анкет. Оцените свою компанию!
×Закрыть

Прогнозування на стороні клієнта за допомогою TensorFlow.js

Всім привіт, мене звати Матвій, я працюю Data Scientist-ом. Моя робота складається з попередньої обробки даних, розвитку та розгортання моделей.

Сьогодні я поділюся з вами своїм досвідом і покажу, як розгорнути модель у такий спосіб, щоб частина розрахунків відбувалася на стороні клієнта. Ця стаття призначена для всіх, хто створив модель і бажає зменшити навантаження на сервер, передавши частину з прогнозуванням клієнтові. Особливо для Data Scientist-ів, які використовують Python щодня і погано володіють Javascript.

Вступ

Уявімо, що ви створили якусь чудову модель, яка робить круті речі і допомагає людям. Наприклад, модель прогнозує улюблений емоджі людини на основі фотографії її чашки. Ви завантажили цю модель в інтернет. Щоденне використання сягає приблизно 1000 запитів — небагато. Простий сервер може дати з цим раду, але одного дня про цю модель дізнається багато людей, і ви почнете отримувати по 100 тисяч запитів щоденно. Ваш сервер, швидше за все, «помре». Отже, ви можете або збільшити сервер і додавати щоразу більше пам’яті, або переписати прогнозування на сторону клієнта. Якщо ви виберете другий варіант, то ось для вас туторіал.

Щоб досягти мети, нам потрібні такі компоненти:

  • Backend: Flask, будь-яка бібліотека для попередньої обробки зображення в Python.
  • Frontend: TensorFlow.js

Нещодавно в TensorFlow.js з’явилася підтримка Node.js, проте ми будемо використовувати Flask, який є бібліотекою Python. Часто деяким натренованим моделям потрібна попередня обробка даних для коректної роботи. Наразі попередню обробку набагато зручніше виконувати в Python ніж в JavaScipt. Сподіваюся, що одного разу стане також можливою і попередня обробка на стороні клієнта.

Створення моделі

Ви можете тренувати модель для MNIST, запустивши train_model.py, або ж створити і натренувати будь-яку модель, яку ви хочете. Важливо зберегти топологію і навантаження. У випадку, якщо ваша модель написана на Keras, просто додайте це.

Після того, як модель збереглася, у вас з’явиться папка з таким вмістом.

Де group\*-shard\*of\* — це колекція бінарних weight файлів і model.json — це модель топології і конфігурація.

Налаштування Flask-сервера

Нам потрібно, щоб користувачі мали доступ до нашої моделі.

Поки що сервер дуже простий, нічого складного, лише один маршрут, що повертає сторінку index.html. Загальна структура системи виглядає ось так.

Створення index.html

Нам потрібна точка входу, з якою користувач буде взаємодіяти і де буде відбуватися наше прогнозування. Отож, давайте налаштуємо index.html.

Наша перша версія виглядатиме так. Єдина важлива річ тут — на рядку 6, де ми додаємо Tensorflow.js з CDN. Наступний крок — це додавання тіла HTML, щоб користувач зміг завантажувати зображення і натискати на кнопки :) Ось він.

Наступний і останній крок для частини з HTML — додати трохи стилю до нашої сторінки, відповідно присвоївши класи елементам HTML, і також створити основний main.js файл який буде містити наше магічне прогнозування. Тепер давайте глянемо на остаточну версію index.html.

Ваш index.html може відрізнятися від мого, можете додати або ж видалити деякі його частини. Тим не менше, найважливіші тут:

  • рядок 6 (додає скрипт з CDN в head — погана практика для продакшн-версії коду, проте я не буду пояснювати, що таке npm і node_modules);
  • рядок 13 (ввід, щоб користувач мав змогу завантажити зображення, можете використовувати різні типи вводу);
  • рядок 20 (наш скрипт, що відповідає за прогнозування на стороні клієнта).

Створення main.js

Настав час застосовувати магію. Передусім нам потрібно ініціалізувати кнопки, ввід, модель і функцію для прогнозування.

В рядку 3 — адреса нашої моделі, зараз вона знаходиться на моїй локальній машині, але взагалі її можна розгорнути будь-де. Також пізніше ми створимо маршрут у Flask для цієї моделі.

Тепер додамо частину, яка завантажуватиме нашу модель, братиме завантажені користувачем зображення і відправлятиме їх на сервер для попередньої обробки.

Ми надсилаємо зображення на /api/prepare/, цей шлях ми додамо пізніше. Також ми присвоюємо відповіді сервера на поле із зображенням tf.tensor2d.

Тепер потрібно добавити прогнозування для tensor, після цього візуалізувати цей прогноз і зображення для нашого користувача. Останній крок — написати функціонал для кнопки і виклик функції.

Отож, остання версія магічного скрипта з прогнозуванням на стороні клієнта буде виглядати якось так.

Ви можете переписати частину з fetch так, щоб надіслати усі файли в одному пості. Не забудьте привести повернений масив до того ж формату, за яким тренувалася модель.

Оновлення сервера Flask

Тепер нам потрібно оновити наш сервер, щоб він міг виконувати попередню обробку зображення для /api/prepare/ і також виводити модель для /model на frontend. Фінальна версія сервера виглядатиме приблизно так.

Для попередньої обробки у нас є дві функції:

  • prepare (викликається на /api/prepare/);
  • preprocessing (бере зображення й повертає змінене зображення до масиву numpy, ця функція може виконувати будь-яку попередню обробку, і все працюватиме без проблем, якщо вона повертає масив numpy).

Модель:

  • model (викликається для /model);
  • load_shards (викликається для будь-якого файлу, який можна викликати з /, ця функція використовується для завантаження бінарних weight файлів).

Чому нам потрібні дві функції та два окремі API для моделі замість одного?

В цій версії TensorFlow.js, коли ми завантажуємо модель для якогось API,

model = await tf.loadModel(modelURL);

вона спочатку завантажує модель, яка є файлом JSON, з modelURL, і після цього автоматично надсилає ще декілька POST запитів до основи домену, щоб завантажити shards (перегляньте цей запит в демо, у логах сервера). Оскільки я не хочу тримати модель в основі шляху разом із сервером, мені потрібні дві функції: одна для model.json та інша для shards.

Результат

На цьому все! Тепер ви можете передати клієнту процес прогнозування улюбленого емоджі людини на основі зображення її чашки або, як це у моєму випадку, MNIST! Якщо все зроблено правильно, то ви побачите щось таке.

Дякую що прочитали! Можете додати/переписати будь-яку частину, як вам завгодно. Насолоджуйтесь!


Цю статтю ви також можете прочитати англійською мовою.

LinkedIn

28 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.
Наразі попередню обробку набагато зручніше виконувати в Python ніж в JavaScipt. Сподіваюся, що одного разу стане також можливою і попередня обробка на стороні клієнта.

До поддержки WebCL на стороне клиента не взлетит, просто потому, что TensorFlow идет обработка или на GPU или используя спец-команды процессора. Без этого производительность на чистом js будет меньше в 1000 или 10000 раз.

WebCL считается устаревшим, так как его сменил WebGPU.
Сейчас уже есть поддержка WebGL2, WASM+WASI
Гетерогенную компиляцию под GPU можно не ожидать так как для этого нужно править сам WASM.

OpenCL во многих вещах уже можно считать несостоятельным... и его стоит заменять Vulkan’ом, или языками с соответствующими SPIR-V таргетами.

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

В целом пример в статье немного странный, так как сам TF можно вызывать напрямую с Node.js.

Бутерброд с кераса, фласка и ноды по большему счёту несостоятелен...

WebGL2, Vulkan

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

SPIR-V

По поводу SPIR-V, насколько я понял их идеологию, программа пишется на OpenCL 2.0, компилируется и исполняется на драйвером Vulcan или OpenGL. Ну такое взлетит, так как реально напоминает концепцию .NET, но писать математические задачи будут на OpenCL, графические на OpenGL, а выполнятся код сможет на всех трех движках.

WASM+WASI

Насколько я знаю, это не способно работать на GPU, просветите если я не прав.

Проблема в том, что реальная поддержка этого всего или добавлена в часть браузеров или её вообще нет, поэтому для разных браузеров нужно писать разный код, что перегружает frontend, поэтому намного проще и безбажнее получается модель если все тяжелые вычисления (кроме разве что обработки графики) мы делаем на backend и затем передаём результаты на frontend.

Рассматривайте Vulkan как более низкоуровневое API чем OpenCL, которое ещё может решать все задачи существующих графических API.

DX11/12 OpenGL и даже Metal2 можно реализовать на Vulkan.

OpenCL компилируется в SPIR, Vulkan компилируется в SPIR-V... у SPIR-V есть весь функционал SPIR, но этот IR имеет доступ ко всем средствам доступным PTX-ассемблеру видеокарты (грубо говоря GPU ASM).

У любого драйвера под капотом есть LLVM компилятор, который компилит все эти DX12 / GLSL / OpenCL приблуды в PTX-ассемблер. Вот промежуточный байткод этого компилятора стандартизирован не был, и у разработчиков не было вообще никакой возможности получить доступ непосредственно к PTX-ассемблеру, по этому Vulkan и SPIR-V можно по своему считать прорывом, хоть он и на мноооооого сложнее любого другого GPU API.

WASM на GPU гонять вполне реально, нужно реализовать соответствующий SPIR-V фронтенд для LLVM, практической пользы без нормальной поддержки со стороны WASI с этого будет не много.

WASM уже можно использовать, у многие компании отрисовуют свой сложный UI или сложные сцены в браузерах именно c WebGL таргетом под WASM. Для простых приложений этот подход абсолютно бесполезен, так как кусок рендера и лайота реализуется с нуля... и не везде есть вообще смысл переписывать и засовывать кусок браузера в приложение.

Проблема совместимости сейчас не решается так как нет поддержки столь распределённых вычислений не только со стороны интерфейсов, но и со стороны языков и компиляторов... очень много нерешаемых Data Locality проблем, а переизобретать иконографические формально верифицируемые языки программирования сейчас просто некому... (посвятил сабжу где-то лет 10)

P.S. CIL в SPIR-V транслировать нормально не получится так как там слишком сильно навязываются объектные примитивы и примитивы конкурентности, тоже самое можно сказать и про JVM байткод — они просто недостаточно гибки, и слишком избыточны, а WASM — он как LISP ;)

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

Так что на стороне клиента отрисовка, причем чем проще тем лучше, на стороне бекенда все вычисления.

что нормальная поддержка этого в браузерах будет крайне не скоро

У WASM’a уже сейчас есть нормальный SIMD, потому большая часть оптимизаторов TensorFlow уже применима, другое дело что сам tfjs Core придётся незаурядно переписать.

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

Можете тыцнуть ссылкой где на WASM можно некий код запустить сразу на нескольких ядрах CPU, нечто типа kernel в OpenCL или Parallel.For в .NET.

Так сразу почему-то и не нашел эту функциональность.

Ясно, нашел. Тоесть явной поддержки нет, есть нечто типа Task или даже Async Invoke. Это немного неудобно, так как нужно писать свою обвязку для этого. Хотя возможность создания и работы с критическими секциями есть, это неплохое дополнение к нестандартному Post.
rustwasm.github.io/...​eading-rust-and-wasm.html
dzone.com/...​s/webassembly-web-workers

В общем на клиенте реально выполнять нечто тяжелое не имеет смысла, так как сперва нужно переписать кучу библиотек (не думаю что C++ так уж легко и без глюков портируется), которые уже есть на бекенде.

Так что концепция остается вся та же, клиент отображение и графика (максимум WebGL), вся обработка на бекенде.

WebCL считается устаревшим, так как его сменил WebGPU.

Гы-гы. Хипстерство до добра не доводит. По сути, всё что ему нужно — это доступ к cuDNN.
И тензофлов убогий — он до сих пор не умеет принимать тензоры из GPU памяти, и это не говоря про его тормознутость и говнокод внутрях.

и это не говоря про его тормознутость и говнокод внутрях.

О каких конкретно изъянах дизайна идёт речь ?

А ты сам посмотри на их код внутри. А делать его полноценный разбор и критику я бесплатно не буду. Если кому нужно, могут заплатить.
Дополнительно — безумные зависимости от их калечных либ и отказ от общепринятых и отлаженных, которые еще и плывут постоянно, то одна, то другая, то зависимость от версии в 4 ее циферках и никак иначе.
Изуродовали нормальную либу Eigen и посему теперь не могут принимать на вход данные из GPU.

Ок, можно считать подобные утверждения дилетантским бредом...

Да как хочешь.
Но можешь рассказать, как в TF отдать картинку не таская ее через PCI (а да Eigen сам прекрасно умеет работать, как с GPU, так и СPU памятью).

не таская ее через PCI

Надо разобраться со всеми оптимизациями PipelineAI, не только для TF’a, так как это касается вообще всех ML решений.

Компилировать надо через XLA с AOT’ом, например TensorRT.
Типа так www.youtube.com/watch?v=ZDjLs8B387s

Подключить RDMА для видеопамяти и монопольный доступ к видеокарте через соответствующие настройки IOMMU. Можно ещё прикрутить Polly, но там с ним довольно много заморочек.
polly.llvm.org

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

спец-команды процессора

Это которые на спец-олимпиадах юзают?

Наверное человек имел ввиду SIMD’ы...

А может он чебурашек имел в виду, это не спецкоманды, это вполне обычные команду процессоров, которым хер знает сколько уже лет.

80% которых не таргетят современные компиляторы.

Уже как лет 10 или 15 умеют и прекрасно умеют. Но да, они не АИ (уровня бог) и сами проанализировать и оптимизировать весь бред от программистов не умеют. Но есть у тебя отличный инструмент в виде асма и интринсиков.

Те которые в код нужно вставлять явно, причем сперва нужно знать про их наличие и наличие в конкретной модели процессора. Видел ПО написанное немцами или турками у них, которое тупо не работало на AMD процессорах. Вот такая вот оптимизация.

К примеру Java и .NET могут использовать эти команды намного больше в вычислениях чем чистый код на C++ за счет jit компиляции под конкретную модель процессора.

Поэтому на C++ и прекращают разрабатывать, так как код написанный хорошим программистом будет хорошо работать и на C++ и Java, но хороших мало, а код написанный джуном на C++ — это боль заказчика, на Java/.Net он хоть как-то работает.

Называются Intrinsics, туда входят всяческие SIMD’ы.

software.intel.com/...​dingpage/IntrinsicsGuide

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

Особенно страдают от этого разработчики всяких ML/СV библиотек и кодеков.

В OpenCL такого изврата почти нет. Да там можно указать какую функцию вставлять нативную или оптимизированную в код, но все таки разработка и поддержка кода там намного проще. Причем переносимость выше, особенно если хостом брать мультиплатформенный язык типа Java/C#.

А OpenCL там вообще непричём.

такого изврата почти нет

Нет гарантии что результирующий SPIR IR будет нормально работать на целевой платформе... изврат там на много веселее.

поддержка кода там намного проще

Оптимизация GPGPU не имеет вообще никакого отношения к оптимизациям на СPU.
OpenCL движки на СPU чаще всего довольно криво используют существующие SIMD’ы, и толку с них не много.

Причем переносимость выше

А Overhead выше, масштабировать простой процессора — тоже весёлое занятие.

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

Если бы использовал — у нас бы не было вот этого...
llvm.org/...​/Kruse-LoopTransforms.pdf

Так речь не про код вообще, а про движок нейронок написанный Гуглом.
Получается, что там джуны работают, а не сеньоры.

Там индусы, со всеми вытекающими.

Подписаться на комментарии