Статья является продолжением первой части, в которой была обучена нейронная сеть для решения задачи соревнования Digit Recognizer на Kaggle. В предыдущей статье был использован трюк, который увеличил точность нейронной сети в контексте результатов соревнования (до 0.99 896), в результате чего позиция автора в лидерборде значительно выросла. В данной статье мы рассмотрим каким образом можно интегрировать и использовать обученную модель нейронной сети в систему для распознавания рукописных цифр.

Введение

После обучения нейронной сети, которая решает задачу из определённой предметной области, через некоторое время встаёт вопрос о том, как её использовать в своём приложении? Ведь результаты обучения рано или поздно должны выйти за пределы Google Colab или Jupyter Notebook, чтобы приносить какую-либо пользу.

Задача интеграции обученных моделей в свою систему решается довольно просто, с помощью TensorFlow. Известно, что при обучении нейронной сети корректируются её весовые коэффициенты, которые оптимизированы для выполнения конкретной задачи. TensorFlow позволяет сохранять весовые коэффициенты и переносить их в другие приложения, которые на их основе восстановят поведение нейронной сети (результат её обучения). Таким образом обучение и деплой модели нейронной сети можно осуществить с помощью одного инструмента, что значительно облегчает нам жизнь.

Можно составить последовательность действий для деплой модели:

  1. Обучение модели нейронной сети (достижение определённого результата)

  2. Сохранение весовых коэффициентов

  3. Перенос весовых коэффициентов (поближе к приложению, в которую нейронная сеть будет интегрирована)

  4. Загрузка весовых коэффициентов и использование модели при решении задач (написание бизнес-логики)

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

Разработка серверного приложения

Сохранение весовых коэффициентов

Для начала следует сохранить веса модели, но как это сделать? Довольно просто. В первой статье при обучении нейронной сети был использован колбэк ModelCheckpoint, который сохраняет наилучшие результаты модели в отдельную папку mnist-cnn.hd5 и нет необходимости в ручном сохранении. Код колбэка выглядит следующим образом:

# Колбэк для сохранения лучшего варианта работы нейронной сети
checkpoint = ModelCheckpoint('mnist-cnn.hd5',
                             monitor='val_accuracy',     # Доля правильных ответов на проверочном множестве
                             save_best_only=True,        # Сохраняем только лучший результат
                               verbose=1)                  # Вывод логовй

В случае отсутствия этого колбэка веса обученной модели также можно сохранить, для этой задачи можно использовать следующий код (взят из официальной документации):

model.save_weights('./checkpoints/my_checkpoint')

На рисунке 1 представлена файловая структура папки model-cnn.hd5 после сохранения в неё весовых коэффициентов.

Рисунок 1 - Данные сохранённой модели
Рисунок 1 - Данные сохранённой модели

Функциональные требования

Для начала составим функциональные требования к серверному приложению.

Что требуется от сервера? Прежде всего обработка одного единственного запроса на загрузку изображения и его последующую обработку с помощью нейронной сети. В общем-то, на этом всё. В сервере будет всего одна функция.

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

Файловая структура

На рисунке 2 представлена файловая структура проекта серверного приложения.

Рисунок 2 - Файловая структура проекта серверного приложения
Рисунок 2 - Файловая структура проекта серверного приложения

Можно сразу заметить, что в структуру проекта перенесены сохранённые весовые коэффициенты модели (model.h5), располагается папка для раздачи статики (public, но она скорее нужна была для тестирования и отладки загрузки изображений), а также точка входа в серверное приложение (app.py) и прочие файлы.

Особое внимание следует уделить зависимостям в requirements.txt, поскольку старые версии TensorFlow или более новые могут не работать с существующим кодом.

Немного о работе с зависимостями в Python

Для того, чтобы зафиксировать все существующие зависимости приложения в определённом списке, который затем можно использовать с целью их установки можно воспользоваться следующей командой:

pip freeze > requirements.txt

Чтобы установить все зависимости из requirements.txt можно использовать следующую команду:

python -m pip install -r requirements.txt

# Альтернатива
pip install -r requirements.txt

Если требуется удалить все зависимости, которые находятся в папке requirements.txt (такое тоже может быть необходимо), то можно воспользоваться следующей командой:

pip uninstall -y -r requirements.txt

Точка входа в серверное приложение

Для начала представлю весь исходный код точки входа в приложение:

import os
import uuid
import numpy as np
from flask import Flask, request, jsonify
import tensorflow as tf
from PIL import Image
from flask_cors import CORS

# Путь к папке public
base_path = './public/'

# Экземпляр flask-приложения
app = Flask(__name__, static_folder="public")

# Настройка CORS-политики
cors = CORS(app, origins=["http://localhost:3000"])

# Эндпоинт для распознавания цифр на изображении
@app.route('/digit-recognize', methods=['POST'])
def upload_file():
    # Получение данных о файле
    file = request.files['file']

    # Получение расширения файла
    file_ext = file.filename.rsplit('.', 1)[1].lower()

    # Генерация UUID идентификатора
    c_uuid = str(uuid.uuid4())

    # Формирование полного пути к файлу
    filename = base_path + c_uuid + '.' + file_ext

    # Сохранение файла на сервер
    file.save(filename)

    # Загрузка весов модели
    model = tf.keras.models.load_model("model.h5")

    # Загрузка изображения с конвертацией в grayscale
    img = Image.open(filename).convert("L")

    # Изменение размера изображения
    new_image = img.resize((28, 28))

    # Конвертация изображения в массив и изменение размера
    x = np.array(new_image).reshape((28, 28, 1))
    x = np.expand_dims(x, axis=0)
    images = np.vstack([x])

    # Предсказание цифры на изображении
    classes = model.predict(images, batch_size=1)

    # Выбор из результата наибольшего (выбор класса цифры)
    result = int(np.argmax(classes))

    img.close()

    # Удаление изображения
    os.remove(filename)

    # Возврат ответа
    return jsonify({'value': result})

# Запуск приложения по 5000 порту
app.run(host='0.0.0.0', port=5000)

Теперь разберём его по порядку.

Сначала мы создаём Flask приложение, определяем путь к статической папке и настраиваем CORS-политику:

# Путь к папке public
base_path = './public/'

# Экземпляр flask-приложения
app = Flask(__name__, static_folder="public")

# Настройка CORS-политики
cors = CORS(app, origins=["http://localhost:3000"])

Приложение будет работать по порту 3000, поэтому в origins при настройке CORS-политики добавляем адрес http://localhost:3000.

Далее определяем эндпоинт для выполнения бизнес-логики сервера:

# Эндпоинт для распознавания цифр на изображении
@app.route('/digit-recognize', methods=['POST'])
def upload_file():
  ...

Обработка POST-запросов для распознавания рукописных цифр будет происходить по адресу /digit-recognize. При определении обработчика используется декоратор app.route (маршрут приложения).

Двигаемся дальше, но уже по внутренней бизнес-логики обработчика POST-запроса:

# Получение данных о файле
file = request.files['file']

# Получение расширения файла
file_ext = file.filename.rsplit('.', 1)[1].lower()

# Генерация UUID идентификатора
c_uuid = str(uuid.uuid4())

# Формирование полного пути к файлу
filename = base_path + c_uuid + '.' + file_ext

# Сохранение файла на сервер
file.save(filename)

В обработчике сначала мы считываем данные о файле с помощью request.files (массив файлов полученных с запросом), получаем расширение файла, генерируем временный UUID для наименования файла и формируем полный путь к файлу, после чего его сохраняем (в папку public).

Двигаемся дальше:

# Загрузка весов модели
model = tf.keras.models.load_model("model.h5")

# Загрузка изображения с конвертацией в grayscale
img = Image.open(filename).convert("L")

# Изменение размера изображения
new_image = img.resize((28, 28))

# Конвертация изображения в массив и изменение размера
x = np.array(new_image).reshape((28, 28, 1))
x = np.expand_dims(x, axis=0)
images = np.vstack([x])

# Предсказание цифры на изображении
classes = model.predict(images, batch_size=1)

# Выбор из результата наибольшего (выбор класса цифры)
result = int(np.argmax(classes))

img.close()

# Удаление изображения
os.remove(filename)

# Возврат ответа
return jsonify({'value': result})

После того, как изображение сохранилось в папке public мы загружаем это изображение и преобразуем его в черно‑белое в градациях серого (convert(«L»)).

Затем мы меняем размер изображения на 28×28 (качество ухудшается, однако нейронная, как я покажу далее, отлично справляется с распознаванием таких изображений).

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

После конвертации изображения в массив передаём его модели для предсказания и выявляем наибольшую вероятность принадлежности к тому или иному классу цифр текущего изображения.

После всех операций удаляем загруженное изображение и возвращаем результат распознавания.

Запуск серверного приложения осуществляется с помощью следующего кода:

# Запуск приложения по 5000 порту
app.run(host='0.0.0.0', port=5000)

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

Протестируем возможности обученной модели на примере из 10-ти цифр (от 0 до 9).

В качестве основного инструмента тестирования используется Postman.

Рисунок 3 - Пример отправки файла через Postman
Рисунок 3 - Пример отправки файла через Postman

На рисунке 3 видно, что в Postman используется запрос form-data и по ключу file добавлено значение one.png, на котором нарисована единица (см. рис. 4).

Рисунок 4 - Единица
Рисунок 4 - Единица

Можно заметить, что нейронная сеть распознала единицу корректно (т.к. возвращаемое значение value равно 1). На рисунке 5 представлены результаты для распознавания всех остальных цифр.

Рисунок 5 - Результаты тестирования для распознавания цифр от 0 до 9
Рисунок 5 - Результаты тестирования для распознавания цифр от 0 до 9

Вроде бы все цифры распознались, не плохой результат. Однако с распознаванием цифры 6 очень сильно заметны проблемы. Я, конечно, подобрал такую цифру 6 чтобы она распозналась хорошо (это можно видеть по изменённому размеру тестового изображения), но всё же это ошибка нейронной сети. Думаю, что проблемы в обучающей выборке (её действительно мало) и, возможно, в трюке который был применён при обучении (но главная задача была достичь как можно более высокого значения score). Нейронная сеть в большинстве случаев при тестах цифры 6 видит цифру 5 и для того, чтобы получить больше одного случае корректного распознавания этой цифры приходится уменьшать размер изображения (чтобы при resize до 28×28 качество изображения не ухудшалось).

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

Разработка веб-приложения

Функциональные требования

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

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

Файловая структура

Файловая структура веб-приложения представлена на рисунке 6.

Рисунок 6 - Файловая структура веб-приложения
Рисунок 6 - Файловая структура веб-приложения

Структура проекта достаточно большая, поэтому я опишу лишь небольшую часть файлов и каталогов, которые представляют наибольший интерес:

  1. components/UI — в данном каталоге хранятся все UI‑компоненты веб‑приложения

  2. containers — каталог для хранения контейнеров (больших компонентов, которые включают в себя другие компоненты)

  3. context/CanvasContext — в данном каталоге находится контекст для работы с canvas и получения определённых данных о нём из любой точки приложения (контекст подключается на уровне точки входа)

  4. store — в данном каталоге находится логика для работы с Redux

  5. utils — в данном каталоге находятся полезные утилиты (для работы с изображениями например)

  6. index.js — точка входа в веб‑приложение

Начнём постепенное погружение в программную реализацию веб‑приложения.

Точка входа

Точка входа выглядит следующим образом:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./containers/App/App";
import * as serviceWorker from "./serviceWorker";
import { CanvasProvider } from "./context/CanvasContext/CanvasContext";
import { Provider } from "react-redux";
import store from "./store/store";

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <CanvasProvider>
        <App />
      </CanvasProvider>
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

serviceWorker.unregister();

В целом, здесь всё стандартно. Происходит подключение Redux хранилища через обёртку Prodiver, который в свою очередь оборачивает контекст CanvasProvider, который в свою очередь (наконец‑то) оборачивает App — главный компонент веб‑приложения.

CanvasProvider

Обёртка CanvasProvider по сути является контекстом, в рамках которого будет возможность взаимодействия с какой‑то конкретной сущностью холста (canvas) на котором пользователь сможет рисовать рукописные цифры.

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

Исходный код CanvasProdiver
import React, { useContext, useRef, useState } from "react";

// Создание элемента React-контекста
const CanvasContext = React.createContext();

// Ширина / высота холста
const width = 512;
const height = 512;

/**
 * Провайдер холста
 * @param {*} param0 Параметры провайдера
 * @returns 
 */
export const CanvasProvider = ({ children }) => {
  // Состояние рисования
  const [isDrawing, setIsDrawing] = useState(false);

  // Ссылка на холст
  const canvasRef = useRef(null);

  // Ссылка на контекст
  const contextRef = useRef(null);

  /**
   * Подготовка холста (первоначальная стилизация)
   */
  const prepareCanvas = () => {
    const canvas = canvasRef.current;
    canvas.width = width * 2;
    canvas.height = height * 2;
    canvas.style.width = `${width}px`;
    canvas.style.height = `${height}px`;
    canvas.style.border = "1px solid black";
    canvas.style.background = "black";

    const context = canvas.getContext("2d");
    context.scale(2, 2);
    context.lineCap = "round";
    context.strokeStyle = "white";
    context.lineWidth = 8;
    context.fillStyle = "black";
    contextRef.current = context;
  };

  /**
   * Начало рисования
   * @param {*} param0 Параметры
   */
  const startDrawing = ({ nativeEvent }) => {
    const { offsetX, offsetY } = nativeEvent;
    contextRef.current.beginPath();
    contextRef.current.moveTo(offsetX, offsetY);
    setIsDrawing(true);
  };

  /**
   * Завершение рисования
   */
  const finishDrawing = () => {
    contextRef.current.closePath();
    setIsDrawing(false);
  };

  /**
   * Рисование
   * @param {*} param0 Параметры
   * @returns 
   */
  const draw = ({ nativeEvent }) => {
    if (!isDrawing) {
      return;
    }
    const { offsetX, offsetY } = nativeEvent;
    contextRef.current.lineTo(offsetX, offsetY);
    contextRef.current.stroke();
  };

  /**
   * Очистка холста
   */
  const clearCanvas = () => {
    const canvas = canvasRef.current;
    const context = canvas.getContext("2d")
    context.fillStyle = "black"
    context.fillRect(0, 0, canvas.width, canvas.height)
  };

  /**
   * Конвертация снимка холста в изображение
   */
  const getImage = () => {
    return canvasRef.current.toDataURL("image/jpeg");
  };

  return (
    <CanvasContext.Provider
      value={{
        canvasRef,
        contextRef,
        prepareCanvas,
        startDrawing,
        finishDrawing,
        clearCanvas,
        draw,
        getImage
      }}
    >
      {children}
    </CanvasContext.Provider>
  );
};

export const useCanvas = () => useContext(CanvasContext);

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

 // Состояние рисования
  const [isDrawing, setIsDrawing] = useState(false);

  // Ссылка на холст
  const canvasRef = useRef(null);

  // Ссылка на контекст
  const contextRef = useRef(null);

Перед рисованием чего либо необходимо подготовить холст. За подготовку холста отвечает функция prepareCanvas:

  /**
   * Подготовка холста (первоначальная стилизация)
   */
  const prepareCanvas = () => {
    // Обращение к текущему элементу холстка
    const canvas = canvasRef.current;
    // Внесение изменений в визуал (стили HTML элемента)
    canvas.width = width * 2;
    canvas.height = height * 2;
    canvas.style.width = `${width}px`;
    canvas.style.height = `${height}px`;
    canvas.style.border = "1px solid black";
    canvas.style.background = "black";

    // Получение контекста холста
    const context = canvas.getContext("2d");
    // Внесение изменений в визуал (стили самого холста)
    context.scale(2, 2);
    context.lineCap = "round";
    context.strokeStyle = "white";
    context.lineWidth = 8;
    context.fillStyle = "black";
    contextRef.current = context;
  };

В данной функции просто идёт первоначальная инициализация стилей холста и html-элемента canvas'a.

В общем случае canvas выглядит следующим образом:

Рисунок 7 - Чёрный квадрат
Рисунок 7 - Чёрный квадрат

Да, именно так — это просто холст, который наполнен чёрным цветом. При этом кисть будет рисовать белым цветом, аналогично тем рисункам, на которых нейронная сеть была протестирована.

Далее у нас идут функции для обработки события начала рисования (зажатие мыши), затем само рисование (движение мыши), и, наконец, завершение рисования (отпускание мыши).

  /**
   * Начало рисования
   * @param {*} param0 Параметры
   */
  const startDrawing = ({ nativeEvent }) => {
    const { offsetX, offsetY } = nativeEvent;
    contextRef.current.beginPath();
    contextRef.current.moveTo(offsetX, offsetY);
    setIsDrawing(true);
  };

  /**
   * Завершение рисования
   */
  const finishDrawing = () => {
    contextRef.current.closePath();
    setIsDrawing(false);
  };

  /**
   * Рисование
   * @param {*} param0 Параметры
   * @returns 
   */
  const draw = ({ nativeEvent }) => {
    if (!isDrawing) {
      return;
    }
    const { offsetX, offsetY } = nativeEvent;
    contextRef.current.lineTo(offsetX, offsetY);
    contextRef.current.stroke();
  };

Все эти функции используются в функциональном компоненте Canvas из папки containers следующим образом:

/**
 * Функциональный компонент холста
 * @returns 
 */
const Canvas = () => {
  const {
    canvasRef,
    prepareCanvas,
    startDrawing,
    finishDrawing,
    draw,
  } = useCanvas();

  useEffect(() => {
    prepareCanvas();
  }, []);

  return (
    <canvas
      onMouseDown={startDrawing}
      onMouseUp={finishDrawing}
      onMouseMove={draw}
      ref={canvasRef}
    />
  );
}

То есть, подключаемся к контексту CanvasContext через хук useCanvas и навешиваем обработчики на сам HTML‑элемент canvas. На зажатие мыши — startDrawing, на отпускание — finishDrawing, а на рисование — draw.

В CanvasProvider есть также функция getImage, которая конвертирует текущий рисунок на холсте в изображение image/png, которое можно отправить на сервер для распознавания:

  /**
   * Конвертация снимка холста в изображение
   */
  const getImage = () => {
    return canvasRef.current.toDataURL("image/jpeg");
  };

Ну и возвращает CanvasProvider просто контекст с прикреплёнными к нему состояниями и функциями:

return (
    <CanvasContext.Provider
      value={{
        canvasRef,      // Ссылка на HTML-элемент холста
        contextRef,     // Ссылка на контекст холста
        prepareCanvas,  // Предварительная подготовка холства
        startDrawing,   // Начало рисования
        finishDrawing,  // Завершение рисования
        clearCanvas,    // Очистка холста
        draw,           // Рисование
        getImage        // Получение изображения
      }}
    >
      {children}
    </CanvasContext.Provider>
  );

Главный контейнер веб-приложения

Перейдём к рассмотрению компонента App.

Его код выглядит следующим образом:

function App() {
  // Подключение селектора
  const recognizeSelector = useAppSelector((s) => s.recognizeReducer);

  return (
    <>
      <div className={styles.container}>
        <div className={styles.paint}>
          <Canvas />
          <div className={styles.buttons}>
            <ClearCanvasButton />
            <RecognizeButton />
          </div>
          <input className={styles.input} value={recognizeSelector.value} readOnly />
        </div>
      </div>
    </>
  );
}

export default App;

В данном компоненте осуществляется просто сборка всех элементов (холст, кнопки и один input для вывода результата распознавания).

В компоненте также идёт взаимодействие с Redux хранилищем через селектор, код которого представлен ниже.

Код слайса recognizeSlize
/* Библиотеки */
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { IValueModel } from "src/models/IValueModel";

/* Локальные интерфейсы */
interface IRecognizeSlice {
  value: string;
  isLoading: boolean;
}

/* Базовое состояние текущего слайса */
const initialState: IRecognizeSlice = {
  value: "",
  isLoading: false,
};

export const recognizeSlice = createSlice({
  name: "recognize_slice",
  initialState,
  reducers: {
    loadingStart(state: IRecognizeSlice) {
      state.value = "";
      state.isLoading = true;
    },

    loadingEnd(state: IRecognizeSlice) {
      state.isLoading = false;
    },

    clear(state: IRecognizeSlice) {
      state.value = "";
      state.isLoading = false;
    },

    setValue(state: IRecognizeSlice, action: PayloadAction<IValueModel>) {
      if (action.payload) {
        state.value = action.payload.value.toString();
      }
    },
  },
});

export default recognizeSlice.reducer;

Основная логика по отправке изображения для распознавания представлена в компоненте RecognizeButton.

RecognizeButton

Данный компонент определяет кнопки, при клике на которую просто создаётся копия холста в текущий момент времени и конвертация этой копии (рисунка формата image/png) в файл:

const RecognizeButton = () => {
    // Подключаем диспетчер
    const dispatch = useAppDispatch();

    // Берём из контекста только функцию для получения текущего изображения холста
    const { getImage } = useCanvas();

    const clickHandler = () => {
        // Конвертация данных из image/png в File
        const file = dataURLToFile(getImage(), "file.png");
      
        // При успешной конвертации вызываем action recognizeImage
        file && dispatch(RecognizeAction.recognizeImage(file));
    }

    return (
        <>
            <button className={styles.button} onClick={clickHandler}>Распознать</button>
        </>
    );
}
Конвертация данных image/png в File
/**
 * Преобразование DataURL в файл
 * @param dataURL DataURL файла
 * @param filename Название файла
 * @returns {File} Файл
 */
export const dataURLToFile = (dataURL: string, filename: string) => {
  if (dataURL.length === 0) {
    return null;
  }

  let arr = dataURL.split(","),
    // @ts-ignore
    mime = arr[0].match(/:(.*?);/)[1],
    bstr = atob(arr[1]),
    n = bstr.length,
    u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  
  return new File([u8arr], filename, { type: mime });
};

При клике по кнопке "Распознать", функция-обработчик просто делегирует выполнение функции recognizeImage из actions:

/**
 * Отправка изображения для распознавания
 * @param image Изображение
 * @returns 
 */
const recognizeImage = (image: File) => async (dispatch: any) => {
  // Начинается загрузка
  dispatch(recognizeSlice.actions.loadingStart());

  try {
    // Определение данных для загрузки изображения
    const formData = new FormData();
    // Добавление файла в FormData
    formData.append("file", image);

    // Отправка запроса на распознавание
    const response = await axios.post(
      `${Api.server}${Api.digit_recognize}`,
      formData
    );

    // Обработка ошибок
    if (response.status != 200 && response.status != 201) {
      console.log(response.data.message);
      return;
    }

    // Установка ответа слайсу
    dispatch(recognizeSlice.actions.setValue(response.data));
  } catch (e: any) {
    console.log(e);
  }

  // Окончание загрузки
  dispatch(recognizeSlice.actions.loadingEnd());
};

В функции происходит работа с состоянием для регистрации загрузки (есть загрузка или нет), ну и формирование данных в формате FormData с последующей отправкой на сервер.

В общем‑то, все основные функции веб‑приложения были описаны.

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

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

Рисунок 8 - Распознавание цифры 5
Рисунок 8 - Распознавание цифры 5
Рисунок 9 - Распознавание цифры 7
Рисунок 9 - Распознавание цифры 7
Рисунок 10 - Распознавание цифры 4
Рисунок 10 - Распознавание цифры 4
Рисунок 11 - Распознавание цифры 6
Рисунок 11 - Распознавание цифры 6
Рисунок 12 - Неудачное распознавание цифры 6
Рисунок 12 - Неудачное распознавание цифры 6

Как видим, на рисунке 11 цифры 6 распозналась хорошо, а на рисунке 12 не очень.

Эмпирическое правило для этой цифры (в рамках модели с текущим уровнем обучения) — рисуем её слева, желательно хорошо. По всей видимости нейронная сеть выявила признаки для этой цифры в том, что она находится слева («спасибо» ImageDataGenerator).

Выводы

В рамках данной статьи мы разобрали каким образом можно сохранять веса обученной модели и загружать их в серверное приложение для полезного применения. Было разработано серверное приложение, которое принимает один POST‑запрос с изображением для распознавания предварительно его обработав. Также было реализовано веб‑приложение с возможностью рисования на холсте и отправкой изображения с холста на сервер, который успешно распознаёт рукописные цифры.

Список использованных источников

  1. Исходный код проекта: ссылка на код

  2. Рисование на Canvas в React.js: ссылка на видео

Комментарии (0)