Reconnaissance des nombres par apprentissage automatique - De zéro à l'application

Publié: 2022-03-11

L'apprentissage automatique, la vision par ordinateur, la création d'API puissantes et la création de superbes interfaces utilisateur sont des domaines passionnants qui connaissent de nombreuses innovations.

Les deux premiers nécessitent des mathématiques et des sciences approfondies, tandis que le développement d'API et d'interface utilisateur se concentre sur la pensée algorithmique et la conception d'architectures flexibles. Ils sont très différents, il peut donc être difficile de décider lequel vous voulez apprendre ensuite. Le but de cet article est de démontrer comment les quatre peuvent être utilisés dans la création d'une application de traitement d'image.

L'application que nous allons construire est un simple outil de reconnaissance de chiffres. Vous dessinez, la machine prédit le chiffre. La simplicité est essentielle car elle nous permet d'avoir une vue d'ensemble plutôt que de nous concentrer sur les détails.

Par souci de simplicité, nous utiliserons les technologies les plus populaires et les plus faciles à apprendre. La partie apprentissage automatique utilisera Python pour l'application back-end. Quant au côté interactionnel de l'application, on fonctionnera via une bibliothèque JavaScript qu'on ne présente plus : React.

Apprentissage automatique pour deviner les chiffres

La partie centrale de notre application est l'algorithme qui devine le nombre tiré. L'apprentissage automatique sera l'outil utilisé pour obtenir une bonne qualité d'estimation. Ce type d'intelligence artificielle de base permet à un système d'apprendre automatiquement avec une quantité donnée de données. En termes plus larges, l'apprentissage automatique est un processus consistant à trouver une coïncidence ou un ensemble de coïncidences dans les données pour s'appuyer sur elles pour deviner le résultat.

Notre processus de reconnaissance d'image contient trois étapes :

  • Obtenir des images de chiffres dessinés pour la formation
  • Entraînez le système à deviner les nombres via les données d'entraînement
  • Tester le système avec des données nouvelles/inconnues

Environnement

Nous aurons besoin d'un environnement virtuel pour travailler avec l'apprentissage automatique en Python. Cette approche est pratique car elle gère tous les packages Python requis, vous n'avez donc pas à vous en soucier.

Installons-le avec les commandes de terminal suivantes :

 python3 -m venv virtualenv source virtualenv/bin/activate

Modèle de formation

Avant de commencer à écrire le code, nous devons choisir un « professeur » approprié pour nos machines. Habituellement, les professionnels de la science des données essaient différents modèles avant de choisir le meilleur. Nous allons ignorer les modèles très avancés qui nécessitent beaucoup de compétences et continuer avec l'algorithme des k plus proches voisins.

C'est un algorithme qui obtient des échantillons de données et les organise sur un plan ordonné par un ensemble donné de caractéristiques. Pour mieux comprendre, examinons l'image suivante :

Image : Échantillons de données d'apprentissage automatique disposés sur un plan

Pour détecter le type du point vert , nous devons vérifier les types de k plus proches voisins où k est l'ensemble d'arguments. Considérant l'image ci-dessus, si k est égal à 1, 2, 3 ou 4, la supposition sera un triangle noir car la plupart des k voisins les plus proches du point vert sont des triangles noirs. Si nous augmentons k à 5, alors la majorité des objets sont des carrés bleus, donc la supposition sera un carré bleu .

Certaines dépendances sont nécessaires pour créer notre modèle d'apprentissage automatique :

  • sklearn.neighbors.KNeighborsClassifier est le classificateur que nous utiliserons.
  • sklearn.model_selection.train_test_split est la fonction qui nous aidera à diviser les données en données d'entraînement et en données utilisées pour vérifier l'exactitude du modèle.
  • sklearn.model_selection.cross_val_score est la fonction pour obtenir une note pour l'exactitude du modèle. Plus la valeur est élevée, meilleure est la justesse.
  • sklearn.metrics.classification_report est la fonction permettant d'afficher un rapport statistique des suppositions du modèle.
  • sklearn.datasets est le package utilisé pour obtenir des données pour la formation (images de chiffres).
  • numpy est un package largement utilisé en science car il offre un moyen productif et confortable de manipuler des structures de données multidimensionnelles en Python.
  • matplotlib.pyplot est le package utilisé pour visualiser les données.

Commençons par les installer et les importer :

 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

Maintenant, nous devons charger la base de données MNIST. MNIST est un jeu de données classique d'images manuscrites utilisé par des milliers de novices dans le domaine de l'apprentissage automatique :

 digits = load_digits()

Une fois les données récupérées et prêtes, nous pouvons passer à l'étape suivante consistant à diviser les données en deux parties : formation et test .

Nous utiliserons 75 % des données pour entraîner notre modèle à deviner les chiffres et nous utiliserons le reste des données pour tester l'exactitude du modèle :

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

Les données sont maintenant organisées et nous sommes prêts à les utiliser. Nous essaierons de trouver le meilleur paramètre k pour notre modèle afin que les suppositions soient plus précises. Nous ne pouvons pas oublier la valeur k à ce stade, car nous devons évaluer le modèle avec différentes valeurs k .

Voyons pourquoi il est essentiel de considérer une plage de valeurs de k et comment cela améliore la précision de notre modèle :

 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()

L'exécution de ce code vous montrera le tracé suivant décrivant la précision de l'algorithme avec différentes valeurs de k .

Image : Tracé utilisé pour tester la précision de l'algorithme avec différentes valeurs de k.

Comme vous pouvez le voir, une valeur k de 3 garantit la meilleure précision pour notre modèle et notre ensemble de données.

Utiliser Flask pour créer une API

Le noyau de l'application, qui est un algorithme prédisant les chiffres à partir d'images, est maintenant prêt. Ensuite, nous devons décorer l'algorithme avec une couche API pour le rendre disponible à l'utilisation. Utilisons le framework Web populaire Flask pour le faire de manière propre et concise.

Nous commencerons par installer Flask et les dépendances liées au traitement d'image dans l'environnement virtuel :

 pip install Flask Pillow scikit-image

Lorsque l'installation est terminée, nous passons à la création du fichier de point d'entrée de l'application :

 touch app.py

Le contenu du fichier ressemblera à ceci :

 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)

Vous obtiendrez une erreur indiquant que PredictDigitView et IndexView ne sont pas définis. L'étape suivante consiste à créer un fichier qui initialisera ces vues :

 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)

Une fois de plus, nous rencontrerons une erreur concernant une importation non résolue. Le package Views repose sur trois fichiers que nous n'avons pas encore :

  • Réglages
  • Repo
  • Service

Nous les implémenterons un par un.

Settings est un module avec des configurations et des variables constantes. Il stockera le chemin vers le classificateur sérialisé pour nous. Cela soulève une question logique : pourquoi ai-je besoin de sauvegarder le classificateur ?

Parce que c'est un moyen simple d'améliorer les performances de votre application. Au lieu de former le classificateur chaque fois que vous recevez une demande, nous stockons la version préparée du classificateur, lui permettant de fonctionner immédiatement :

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

Le mécanisme de paramétrage - obtenir le classificateur - sera initialisé dans le prochain package de notre liste, le Repo . C'est une classe avec deux méthodes pour récupérer et mettre à jour le classificateur formé à l'aide du module pickle intégré de Python :

 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_)

Nous sommes sur le point de finaliser notre API. Maintenant, il ne manque que le module Service . Quel est son but ?

  • Obtenir le classificateur formé à partir du stockage
  • Transformer l'image transmise de l'interface utilisateur à un format que le classificateur comprend
  • Calculer la prédiction avec l'image formatée via le classifieur
  • Renvoyer la prédiction

Codons cet algorithme :

 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

Ici, vous pouvez voir que PredictDigitService a deux dépendances : ClassifierFactory et process_image .

Nous allons commencer par créer une classe pour créer et entraîner notre modèle :

 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

L'API est prête à l'action. Nous pouvons maintenant passer à l'étape de traitement de l'image.

Traitement d'image

Le traitement d'image est une méthode consistant à effectuer certaines opérations sur une image pour l'améliorer ou en extraire des informations utiles. Dans notre cas, nous devons faire passer en douceur l'image dessinée par un utilisateur au format du modèle d'apprentissage automatique.

Image alt : Transformer des images dessinées en un format d'apprentissage automatique.

Importons quelques assistants pour atteindre cet objectif :

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

Nous pouvons diviser la transition en six parties distinctes :

1. Remplacer un fond transparent par une couleur

Image alt : remplacement de l'arrière-plan sur un exemple d'image.

 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. Coupez les bordures ouvertes

Image : rognage des bordures d'un exemple d'image.

 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. Ajoutez des bordures de taille égale

Image : Ajout de bordures d'une taille prédéfinie et égale à un exemple d'image.

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

4. Convertissez l'image en mode niveaux de gris

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

5. Inverser les couleurs

Image : Inversion des couleurs de l'image de l'échantillon.

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

6. Redimensionnez l'image au format 8x8

Image : redimensionnement de l'exemple d'image au format 8 x 8.

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

Vous pouvez maintenant tester l'application. Exécutez l'application et saisissez la commande ci-dessous pour envoyer une requête avec cette image iStock à l'API :

Image : Image d'un numéro huit dessiné à la main.

 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

Vous devriez voir la sortie suivante :

 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

L'exemple d'image représentait le chiffre 8 et notre application l'a correctement identifié comme tel.

Créer un volet de dessin via React

Pour amorcer rapidement l'application frontale, nous utiliserons le passe-partout CRA :

 create-react-app frontend cd frontend

Après avoir configuré le lieu de travail, nous avons également besoin d'une dépendance pour dessiner des chiffres. Le package react-sketch correspond parfaitement à nos besoins :

 npm i react-sketch

L'application n'a qu'un seul composant. Nous pouvons diviser ce composant en deux parties : logique et vue .

La partie vue est chargée de représenter le volet de dessin, les boutons Soumettre et Réinitialiser . Lorsqu'ils interagissent, nous devrions également représenter une prédiction ou une erreur. Du point de vue logique, il a les devoirs suivants : soumettre des images et effacer le croquis .

Chaque fois qu'un utilisateur clique sur Soumettre , le composant extrait l'image du composant d'esquisse et fait appel à la fonction makePrediction du module API. Si la demande au back-end réussit, nous définirons la variable d'état de prédiction. Sinon, nous mettrons à jour l'état d'erreur.

Lorsqu'un utilisateur clique sur Réinitialiser , l'esquisse s'efface :

 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 }

La logique est suffisante. Maintenant, nous pouvons y ajouter l'interface visuelle :

 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;

Le composant est prêt, testez-le en exécutant et en allant sur localhost:3000 après :

 npm run start

L'application de démonstration est disponible ici. Vous pouvez également parcourir le code source sur GitHub.

Emballer

La qualité de ce classificateur n'est pas parfaite, et je ne prétends pas qu'elle le soit. La différence entre les données que nous avons utilisées pour la formation et les données provenant de l'interface utilisateur est énorme. Malgré cela, nous avons créé une application fonctionnelle à partir de zéro en moins de 30 minutes.

Image : Animation montrant l'application finalisée identifiant les chiffres écrits à la main.

Ce faisant, nous avons perfectionné nos compétences dans quatre domaines :

  • Apprentissage automatique
  • Développement back-end
  • Traitement d'image
  • Développement front-end

Les cas d'utilisation potentiels ne manquent pas pour les logiciels capables de reconnaître les chiffres manuscrits, allant des logiciels éducatifs et administratifs aux services postaux et financiers.

Par conséquent, j'espère que cet article vous motivera à améliorer vos capacités d'apprentissage automatique, de traitement d'image et de développement front-end et back-end, et à utiliser ces compétences pour concevoir des applications merveilleuses et utiles.

Si vous souhaitez approfondir vos connaissances sur l'apprentissage automatique et le traitement d'images, vous pouvez consulter notre didacticiel d'apprentissage automatique contradictoire.