Прогнозирование на стороне клиента с помощью TensorFlow.js

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


Всем привет, меня зовут Матвей, я работаю 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, просто додайте це.

# Your training happens here
# Hundreds and hundreds of layers
# Stacked to create the greatest model ever


# don't forget saving to json format
import tensorflowjs as tfjs
tfjs.converters.save_keras_model(model, "model_js")

view raw train_model.py hosted with ? by GitHub

После того, как модель сохранилась, у вас появится папка с таким содержимым.

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

Настройки Flask-сервера

Нам нужно, чтобы пользователи имели доступ к нашей модели.

from flask_cors import CORS
from flask import Flask, render_template

app = Flask(__name__)
CORS(app)


@app.route("/", methods=["GET"])
def main():
return render_template('index.html')

if __name__ == "__main__":
app.run()

view raw app.py hosted with ? by GitHub

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

Создание index.html

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>TF JS example</title>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.11.7"></script>
</head>
<body>
</body>
</html>

view raw index.html hosted with ? by GitHub

Наша первая версия будет выглядеть так. Единственная важная вещь здесь — на строке 6, где мы добавляем Tensorflow.js с CDN. Следующий шаг — это добавление тела HTML, чтобы пользователь смог загружать изображения и нажимать на кнопки :) Вот он.

<body>
<h1>TensorflowJS client side prediction</h1>
<h2>When you first time press predict it will take more time, for model to load</h2>
<main>
<input type="file" id="file" multiple>
<label for="file">Choose files</label>
<button type="submit">Predict</button>
<button type="submit">Clear</button>
<span></span>
</main>
<div></div>
</body>

view raw index.html hosted with ? by GitHub

Следующий и последний шаг для части с HTML — добавить немного стиля к нашей страницы, соответственно присвоив классы элементам HTML, и также создать основной main.js файл который будет содержать наше магическое прогнозирования. Теперь давайте посмотрим на окончательную версию index.html.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>TF JS example</title>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.11.7"></script>
<link rel="stylesheet" href="../static/frontend/styles.css">
</head>
<body>
<h1 class="title">TensorflowJS client side prediction</h1>
<h2 class="title">When you first time press predict it will take more time, for model to load</h2>
<main class="file-form">
<input type="file" id="file" multiple class="file-form__select-file">
<label for="file" class="file-form__select-file_label">Choose files</label>
<button type="submit" class="file-form__button file-form__button_predict" id="predict">Predict</button>
<button type="submit" class="file-form__button file-form__button_clear" id="clear">Clear</button>
<span class="file-form__files" id="number-of-files"></span>
</main>
<div id="preview"></div>
<script src="../static/frontend/main.js"></script>
</body>
</html>

view raw index.html hosted with ? by GitHub

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

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

Создание main.js

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

let model;

const modelURL = 'http://localhost:5000/model';

const preview = document.getElementById("preview");
const predictButton = document.getElementById("predict");
const clearButton = document.getElementById("clear");
const numberOfFiles = document.getElementById("number-of-files");
const fileInput = document.getElementById('file');

const predict = async (modelURL) => {}

view raw main.js hosted with ? by GitHub

В строке 3 — адрес нашей модели, сейчас она находится на моей локальной машине, но вообще ее можно развернуть в любом месте. Также позже мы создадим маршрут в Flask для этой модели.

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

let model;

const modelURL = 'http://localhost:5000/model';

const preview = document.getElementById("preview");
const predictButton = document.getElementById("predict");
const clearButton = document.getElementById("clear");
const numberOfFiles = document.getElementById("number-of-files");
const fileInput = document.getElementById('file');

const predict = async (modelURL) => {
if (!model) model = await tf.loadModel(modelURL);
const files = fileInput.files;

[...files].map(async (img) => {
const data = new FormData();
data.append('file', img);

const processedImage = await fetch("/api/prepare",
{
method: 'POST',
body: data
}).then(response => {
return response.json();
}).then(result => {
return tf.tensor2d(result['image']);
});

})
};

view raw main.js hosted with ? by GitHub

Мы отправляем изображение на /api/prepare/, этот путь мы добавим позже. Также мы присваиваем ответа сервера на поле с изображением tf.tensor2d.

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

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

let model;

const modelURL = 'http://localhost:5000/model';

const preview = document.getElementById("preview");
const predictButton = document.getElementById("predict");
const clearButton = document.getElementById("clear");
const numberOfFiles = document.getElementById("number-of-files");
const fileInput = document.getElementById('file');

const predict = async (modelURL) => {
if (!model) model = await tf.loadModel(modelURL);
const files = fileInput.files;

[...files].map(async (img) => {
const data = new FormData();
data.append('file', img);

const processedImage = await fetch("/api/prepare",
{
method: 'POST',
body: data
}).then(response => {
return response.json();
}).then(result => {
return tf.tensor2d(result['image']);
});

// shape has to be the same as it was for training of the model
const prediction = model.predict(tf.reshape(processedImage, shape = [1, 28, 28, 1]));
const label = prediction.argMax(axis = 1).get([0]);
renderImageLabel(img, label);
})
};

const renderImageLabel = (img, label) => {
const reader = new FileReader();
reader.onload = () => {
preview.innerHTML += `<div class="image-block">
<img src="${reader.result}" class="image-block_loaded" id="source"/>
<h2 class="image-block__label">${label}</h2>
</div>`;

};
reader.readAsDataURL(img);
};


fileInput.addEventListener("change", () => numberOfFiles.innerHTML = "Selected " + fileInput.files.length + " files", false);
predictButton.addEventListener("click", () => predict(modelURL));
clearButton.addEventListener("click", () => preview.innerHTML = "");

view raw main.js hosted with ? by GitHub

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

Обновление сервера Flask

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

from flask_cors import CORS
from flask import Flask, request, render_template, json, jsonify, send_from_directory
import json
import cv2
import numpy as np
import io

app = Flask(__name__)
CORS(app)


@app.route("/", methods=["GET"])
def main():
return render_template('index.html')


@app.route("/api/prepare", methods=["POST"])
def prepare():
file = request.files['file']
res = preprocessing(file)
return json.dumps({"image": res.tolist()})


@app.route('/model')
def model():
json_data = json.load(open("./model_js/model.json"))
return jsonify(json_data)


@app.route('/<path:path>')
def load_shards(path):
return send_from_directory('model_js', path)


def preprocessing(file):
in_memory_file = io.BytesIO()
file.save(in_memory_file)
data = np.fromstring(in_memory_file.getvalue(), dtype=np.uint8)
img = cv2.imdecode(data, 0)
res = cv2.resize(img, dsize=(28, 28), interpolation=cv2.INTER_CUBIC)
# file.save("static/UPLOAD/img.png") # saving uploaded img
# cv2.imwrite("static/UPLOAD/test.png", res) # saving processed image
return res


if __name__ == "__main__":
app.run()

view raw app.py hosted with ? by GitHub

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

  • 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! Если все сделано правильно, то вы увидите что-то такое.

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


Источник: dou.ua

Комментарии: