Распознавание номеров с помощью машинного обучения — от нуля до приложения

Опубликовано: 2022-03-11

Машинное обучение, компьютерное зрение, создание мощных API и создание красивых пользовательских интерфейсов — это захватывающие области, в которых много инноваций.

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

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

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

Машинное обучение угадыванию цифр

Основной частью нашего приложения является алгоритм угадывания выпавшего числа. Машинное обучение будет инструментом, используемым для достижения хорошего качества предположений. Этот тип базового искусственного интеллекта позволяет системе автоматически обучаться с заданным объемом данных. В более широком смысле машинное обучение — это процесс поиска совпадений или набора совпадений в данных, чтобы полагаться на них для угадывания результата.

Наш процесс распознавания изображений состоит из трех этапов:

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

Окружающая обстановка

Нам понадобится виртуальная среда для работы с машинным обучением в Python. Этот подход удобен, потому что он управляет всеми необходимыми пакетами Python, так что вам не нужно о них беспокоиться.

Давайте установим его с помощью следующих команд терминала:

 python3 -m venv virtualenv source virtualenv/bin/activate

Модель обучения

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

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

Изображение: Образцы данных машинного обучения, расположенные на плоскости

Чтобы определить тип Green Dot , мы должны проверить типы k ближайших соседей, где k — набор аргументов. Учитывая изображение выше, если k равно 1, 2, 3 или 4, предположение будет черным треугольником , поскольку большинство k ближайших соседей зеленой точки являются черными треугольниками. Если мы увеличим k до 5, то большинство объектов будут синими квадратами, поэтому предположение будет синим квадратом .

Для создания нашей модели машинного обучения необходимы некоторые зависимости:

  • sklearn.neighbors.KNeighborsClassifier — это классификатор, который мы будем использовать.
  • sklearn.model_selection.train_test_split — это функция, которая поможет нам разделить данные на обучающие данные и данные, используемые для проверки правильности модели.
  • sklearn.model_selection.cross_val_score — функция для получения оценки правильности модели. Чем выше значение, тем лучше правильность.
  • sklearn.metrics.classification_report — это функция для отображения статистического отчета о предположениях модели.
  • sklearn.datasets — это пакет, используемый для получения данных для обучения (изображения цифр).
  • numpy — это пакет, широко используемый в науке, поскольку он предлагает продуктивный и удобный способ манипулирования многомерными структурами данных в Python.
  • matplotlib.pyplot — это пакет, используемый для визуализации данных.

Начнем с установки и импорта их всех:

 pip install sklearn numpy matplotlib scipy from sklearn.datasets import load_digits from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import train_test_split, cross_val_score import numpy as np import matplotlib.pyplot as plt

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

 digits = load_digits()

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

Мы будем использовать 75% данных для обучения нашей модели угадыванию цифр, а остальные данные будем использовать для проверки правильности модели:

 (X_train, X_test, y_train, y_test) = train_test_split( digits.data, digits.target, test_size=0.25, random_state=42 )

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

Давайте посмотрим, почему важно учитывать диапазон значений k и как это повышает точность нашей модели:

 ks = np.arange(2, 10) scores = [] for k in ks: model = KNeighborsClassifier(n_neighbors=k) score = cross_val_score(model, X_train, y_train, cv=5) score.mean() scores.append(score.mean()) plt.plot(scores, ks) plt.xlabel('accuracy') plt.ylabel('k') plt.show()

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

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

Как видите, значение k , равное 3, обеспечивает наилучшую точность для нашей модели и набора данных.

Использование Flask для создания API

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

Мы начнем с установки Flask и зависимостей, связанных с обработкой изображений в виртуальной среде:

 pip install Flask Pillow scikit-image

Когда установка завершится, переходим к созданию файла точки входа приложения:

 touch app.py

Содержимое файла будет выглядеть так:

 import os from flask import Flask from views import PredictDigitView, IndexView app = Flask(__name__) app.add_url_rule( '/api/predict', view_func=PredictDigitView.as_view('predict_digit'), methods=['POST'] ) app.add_url_rule( '/', view_func=IndexView.as_view('index'), methods=['GET'] ) if __name__ == 'main': port = int(os.environ.get("PORT", 5000)) app.run(host='0.0.0.0', port=port)

Вы получите сообщение об ошибке, говорящее о том, что PredictDigitView и IndexView не определены. Следующий шаг — создание файла, который будет инициализировать эти представления:

 from flask import render_template, request, Response from flask.views import MethodView, View from flask.views import View from repo import ClassifierRepo from services import PredictDigitService from settings import CLASSIFIER_STORAGE class IndexView(View): def dispatch_request(self): return render_template('index.html') class PredictDigitView(MethodView): def post(self): repo = ClassifierRepo(CLASSIFIER_STORAGE) service = PredictDigitService(repo) image_data_uri = request.json['image'] prediction = service.handle(image_data_uri) return Response(str(prediction).encode(), status=200)

В очередной раз мы столкнемся с ошибкой о неразрешенном импорте. Пакет Views основан на трех файлах, которых у нас еще нет:

  • Настройки
  • Репо
  • Оказание услуг

Мы будем реализовывать их один за другим.

Настройки — это модуль с конфигурациями и постоянными переменными. Он сохранит для нас путь к сериализованному классификатору. Напрашивается логичный вопрос: зачем мне сохранять классификатор?

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

 import os BASE_DIR = os.getcwd() CLASSIFIER_STORAGE = os.path.join(BASE_DIR, 'storage/classifier.txt')

Механизм настройки — получение классификатора — будет инициализирован в следующем пакете в нашем списке — Repo . Это класс с двумя методами для извлечения и обновления обученного классификатора с помощью встроенного в Python модуля pickle :

 import pickle class ClassifierRepo: def __init__(self, storage): self.storage = storage def get(self): with open(self.storage, 'wb') as out: try: classifier_str = out.read() if classifier_str != '': return pickle.loads(classifier_str) else: return None except Exception: return None def update(self, classifier): with open(self.storage, 'wb') as in_: pickle.dump(classifier, in_)

Мы близки к завершению нашего API. Теперь ему не хватает только Сервисного модуля. Какова его цель?

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

Закодируем этот алгоритм:

 from sklearn.datasets import load_digits from classifier import ClassifierFactory from image_processing import process_image class PredictDigitService: def __init__(self, repo): self.repo = repo def handle(self, image_data_uri): classifier = self.repo.get() if classifier is None: digits = load_digits() classifier = ClassifierFactory.create_with_fit( digits.data, digits.target ) self.repo.update(classifier) x = process_image(image_data_uri) if x is None: return 0 prediction = classifier.predict(x)[0] return prediction

Здесь вы можете видеть, что PredictDigitService имеет две зависимости: ClassifierFactory и process_image .

Мы начнем с создания класса для создания и обучения нашей модели:

 from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier class ClassifierFactory: @staticmethod def create_with_fit(data, target): model = KNeighborsClassifier(n_neighbors=3) model.fit(data, target) return model

API готов к работе. Теперь мы можем перейти к этапу обработки изображения.

Обработка изображения

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

Альтернативное изображение: Преобразование нарисованных изображений в формат машинного обучения.

Давайте импортируем несколько помощников для достижения этой цели:

 import numpy as np from skimage import exposure import base64 from PIL import Image, ImageOps, ImageChops from io import BytesIO

Мы можем разделить переход на шесть отдельных частей:

1. Замените прозрачный фон цветным

Image alt: Замена фона на образце изображения.

 def replace_transparent_background(image): image_arr = np.array(image) if len(image_arr.shape) == 2: return image alpha1 = 0 r2, g2, b2, alpha2 = 255, 255, 255, 255 red, green, blue, alpha = image_arr[:, :, 0], image_arr[:, :, 1], image_arr[:, :, 2], image_arr[:, :, 3] mask = (alpha == alpha1) image_arr[:, :, :4][mask] = [r2, g2, b2, alpha2] return Image.fromarray(image_arr)

2. Обрежьте открытые границы

Изображение: обрезка границ образца изображения.

 def trim_borders(image): bg = Image.new(image.mode, image.size, image.getpixel((0,0))) diff = ImageChops.difference(image, bg) diff = ImageChops.add(diff, diff, 2.0, -100) bbox = diff.getbbox() if bbox: return image.crop(bbox) return image

3. Добавьте границы одинакового размера

Изображение: Добавление границ предустановленного и одинакового размера к образцу изображения.

 def pad_image(image): return ImageOps.expand(image, border=30, fill='#fff')

4. Преобразуйте изображение в режим оттенков серого.

 def to_grayscale(image): return image.convert('L')

5. Инвертировать цвета

Изображение: инвертирование цветов образца изображения.

 def invert_colors(image): return ImageOps.invert(image)

6. Измените размер изображения до формата 8x8.

Изображение: изменение размера образца изображения до формата 8x8.

 def resize_image(image): return image.resize((8, 8), Image.LINEAR)

Теперь вы можете протестировать приложение. Запустите приложение и введите команду ниже, чтобы отправить запрос с этим изображением iStock в API:

Изображение: стоковое изображение нарисованной от руки цифры восемь.

 export FLASK_APP=app flask run
 curl "http://localhost:5000/api/predict" -X "POST" -H "Content-Type: application/json" -d "{\"image\": \"data:image/png;base64,$(curl "https://media.istockphoto.com/vectors/number-eight-8-hand-drawn-with-dry-brush-vector-id484207302?k=6&m=484207302&s=170667a&w=0&h=s3YANDyuLS8u2so-uJbMA2uW6fYyyRkabc1a6OTq7iI=" | base64)\"}" -i

Вы должны увидеть следующий вывод:

 HTTP/1.1 100 Continue HTTP/1.0 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 1 Server: Werkzeug/0.14.1 Python/3.6.3 Date: Tue, 27 Mar 2018 07:02:08 GMT 8

На образце изображения была изображена цифра 8, и наше приложение правильно определило ее как таковую.

Создание области рисования с помощью React

Чтобы быстро загрузить внешнее приложение, мы будем использовать шаблон CRA:

 create-react-app frontend cd frontend

После настройки рабочего места нам также понадобится зависимость для отрисовки цифр. Пакет react-sketch идеально подходит для наших нужд:

 npm i react-sketch

Приложение имеет только один компонент. Мы можем разделить этот компонент на две части: логика и представление .

Часть просмотра отвечает за представление панели рисования, кнопок « Отправить » и « Сбросить ». При взаимодействии мы также должны представлять прогноз или ошибку. С точки зрения логики у него следующие обязанности: отправлять изображения и очищать скетч .

Всякий раз, когда пользователь нажимает « Отправить », компонент извлекает изображение из компонента эскиза и обращается к функции makePrediction модуля API. Если запрос к серверной части завершится успешно, мы установим переменную состояния предсказания. В противном случае мы обновим состояние ошибки.

Когда пользователь нажимает Reset , скетч очищается:

 import React, { useRef, useState } from "react"; import { makePrediction } from "./api"; const App = () => { const sketchRef = useRef(null); const [error, setError] = useState(); const [prediction, setPrediction] = useState(); const handleSubmit = () => { const image = sketchRef.current.toDataURL(); setPrediction(undefined); setError(undefined); makePrediction(image).then(setPrediction).catch(setError); }; const handleClear = (e) => sketchRef.current.clear(); return null }

Логики достаточно. Теперь мы можем добавить к нему визуальный интерфейс:

 import React, { useRef, useState } from "react"; import { SketchField, Tools } from "react-sketch"; import { makePrediction } from "./api"; import logo from "./logo.svg"; import "./App.css"; const pixels = (count) => `${count}px`; const percents = (count) => `${count}%`; const MAIN_CONTAINER_WIDTH_PX = 200; const MAIN_CONTAINER_HEIGHT = 100; const MAIN_CONTAINER_STYLE = { width: pixels(MAIN_CONTAINER_WIDTH_PX), height: percents(MAIN_CONTAINER_HEIGHT), margin: "0 auto", }; const SKETCH_CONTAINER_STYLE = { border: "1px solid black", width: pixels(MAIN_CONTAINER_WIDTH_PX - 2), height: pixels(MAIN_CONTAINER_WIDTH_PX - 2), backgroundColor: "white", }; const App = () => { const sketchRef = useRef(null); const [error, setError] = useState(); const [prediction, setPrediction] = useState(); const handleSubmit = () => { const image = sketchRef.current.toDataURL(); setPrediction(undefined); setError(undefined); makePrediction(image).then(setPrediction).catch(setError); }; const handleClear = (e) => sketchRef.current.clear(); return ( <div className="App" style={MAIN_CONTAINER_STYLE}> <div> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <h1 className="App-title">Draw a digit</h1> </header> <div style={SKETCH_CONTAINER_STYLE}> <SketchField ref={sketchRef} width="100%" height="100%" tool={Tools.Pencil} imageFormat="jpg" lineColor="#111" lineWidth={10} /> </div> {prediction && <h3>Predicted value is: {prediction}</h3>} <button onClick={handleClear}>Clear</button> <button onClick={handleSubmit}>Guess the number</button> {error && <p style={{ color: "red" }}>Something went wrong</p>} </div> </div> ); }; export default App;

Компонент готов, протестируйте его, выполнив и перейдя на localhost:3000 после:

 npm run start

Демонстрационное приложение доступно здесь. Вы также можете просмотреть исходный код на GitHub.

Подведение итогов

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

Изображение: анимация, показывающая завершенное приложение, идентифицирующее рукописные цифры.

В процессе мы оттачивали свои навыки в четырех областях:

  • Машинное обучение
  • Бэкэнд разработка
  • Обработка изображения
  • Фронтенд разработка

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

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

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