Reconocimiento de números mediante aprendizaje automático: de cero a la aplicación
Publicado: 2022-03-11El aprendizaje automático, la visión por computadora, la creación de potentes API y la creación de hermosas interfaces de usuario son campos emocionantes que presencian una gran cantidad de innovación.
Los dos primeros requieren matemáticas y ciencias extensas, mientras que el desarrollo de API y UI se centra en el pensamiento algorítmico y el diseño de arquitecturas flexibles. Son muy diferentes, por lo que decidir cuál quieres aprender a continuación puede ser un desafío. El propósito de este artículo es demostrar cómo se pueden emplear los cuatro para crear una aplicación de procesamiento de imágenes.
La aplicación que vamos a construir es un simple reconocedor de dígitos. Tú dibujas, la máquina predice el dígito. La simplicidad es esencial porque nos permite ver el panorama general en lugar de centrarnos en los detalles.
En aras de la simplicidad, utilizaremos las tecnologías más populares y fáciles de aprender. La parte de aprendizaje automático utilizará Python para la aplicación de back-end. En cuanto al lado interactivo de la aplicación, operaremos a través de una biblioteca de JavaScript que no necesita presentación: React.
Aprendizaje automático para adivinar dígitos
La parte central de nuestra aplicación es el algoritmo que adivina el número sorteado. El aprendizaje automático será la herramienta utilizada para lograr una buena calidad de conjetura. Este tipo de inteligencia artificial básica permite que un sistema aprenda automáticamente con una cantidad determinada de datos. En términos más amplios, el aprendizaje automático es un proceso de encontrar una coincidencia o un conjunto de coincidencias en los datos para confiar en ellos para adivinar el resultado.
Nuestro proceso de reconocimiento de imágenes consta de tres pasos:
- Obtén imágenes de dígitos dibujados para entrenar
- Entrene al sistema para adivinar los números a través de datos de entrenamiento
- Probar el sistema con datos nuevos/desconocidos
Ambiente
Necesitaremos un entorno virtual para trabajar con aprendizaje automático en Python. Este enfoque es práctico porque administra todos los paquetes de Python necesarios, por lo que no debe preocuparse por ellos.
Vamos a instalarlo con los siguientes comandos de terminal:
python3 -m venv virtualenv source virtualenv/bin/activate
Modelo de Entrenamiento
Antes de comenzar a escribir el código, debemos elegir un "maestro" adecuado para nuestras máquinas. Por lo general, los profesionales de la ciencia de datos prueban diferentes modelos antes de elegir el mejor. Omitiremos modelos muy avanzados que requieren mucha habilidad y procederemos con el algoritmo de k-vecinos más cercanos.
Es un algoritmo que obtiene algunas muestras de datos y las organiza en un plano ordenado por un conjunto determinado de características. Para entenderlo mejor, repasemos la siguiente imagen:
Para detectar el tipo del punto verde , debemos verificar los tipos de k vecinos más cercanos donde k es el conjunto de argumentos. Teniendo en cuenta la imagen de arriba, si k es igual a 1, 2, 3 o 4, la conjetura será un Triángulo negro , ya que la mayoría de los vecinos k más cercanos del punto verde son triángulos negros. Si aumentamos k a 5, entonces la mayoría de los objetos son cuadrados azules, por lo que la conjetura será un cuadrado azul .
Se necesitan algunas dependencias para crear nuestro modelo de aprendizaje automático:
- sklearn.neighbors.KNeighborsClassifier es el clasificador que usaremos.
- sklearn.model_selection.train_test_split es la función que nos ayudará a dividir los datos en datos de entrenamiento y datos utilizados para verificar la corrección del modelo.
- sklearn.model_selection.cross_val_score es la función para obtener una calificación de la corrección del modelo. Cuanto mayor sea el valor, mejor será la corrección.
- sklearn.metrics.classification_report es la función para mostrar un informe estadístico de las conjeturas del modelo.
- sklearn.datasets es el paquete utilizado para obtener datos para el entrenamiento (imágenes de dígitos).
- numpy es un paquete ampliamente utilizado en la ciencia, ya que ofrece una forma productiva y cómoda de manipular estructuras de datos multidimensionales en Python.
- matplotlib.pyplot es el paquete utilizado para visualizar datos.
Comencemos instalando e importando todos ellos:
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
Ahora, necesitamos cargar la base de datos MNIST. MNIST es un conjunto de datos clásico de imágenes escritas a mano que utilizan miles de novatos en el campo del aprendizaje automático:
digits = load_digits()
Una vez que los datos se obtienen y están listos, podemos pasar al siguiente paso de dividir los datos en dos partes: entrenamiento y prueba .
Usaremos el 75% de los datos para entrenar nuestro modelo para adivinar dígitos y usaremos el resto de los datos para probar la corrección del modelo:
(X_train, X_test, y_train, y_test) = train_test_split( digits.data, digits.target, test_size=0.25, random_state=42 )
Los datos ahora están organizados y estamos listos para usarlos. Intentaremos encontrar el mejor parámetro k para nuestro modelo para que las conjeturas sean más precisas. No podemos dejar de pensar en el valor de k en esta etapa, ya que tenemos que evaluar el modelo con diferentes valores de k .
Veamos por qué es esencial considerar un rango de valores de k y cómo esto mejora la precisión de nuestro modelo:
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()
Ejecutar este código le mostrará el siguiente gráfico que describe la precisión del algoritmo con diferentes valores de k .
Como puede ver, un valor k de 3 garantiza la mejor precisión para nuestro modelo y conjunto de datos.
Usando Flask para construir una API
El núcleo de la aplicación, que es un algoritmo que predice los dígitos de las imágenes, ya está listo. A continuación, debemos decorar el algoritmo con una capa API para que esté disponible para su uso. Usemos el popular marco web Flask para hacer esto de manera limpia y concisa.
Comenzaremos instalando Flask y las dependencias relacionadas con el procesamiento de imágenes en el entorno virtual:
pip install Flask Pillow scikit-image
Cuando se completa la instalación, pasamos a la creación del archivo de punto de entrada de la aplicación:
touch app.py
El contenido del archivo se verá así:
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)
Recibirá un error que indica que PredictDigitView
e IndexView
no están definidos. El siguiente paso es crear un archivo que inicialice estas vistas:
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)
Una vez más, encontraremos un error sobre una importación no resuelta. El paquete Vistas se basa en tres archivos que aún no tenemos:
- Ajustes
- Repo
- Servicio
Los implementaremos uno por uno.
Settings es un módulo con configuraciones y variables constantes. Almacenará la ruta al clasificador serializado para nosotros. Plantea una pregunta lógica: ¿Por qué necesito guardar el clasificador?
Porque es una forma sencilla de mejorar el rendimiento de tu aplicación. En lugar de entrenar el clasificador cada vez que recibe una solicitud, almacenaremos la versión preparada del clasificador, lo que le permitirá funcionar de inmediato:
import os BASE_DIR = os.getcwd() CLASSIFIER_STORAGE = os.path.join(BASE_DIR, 'storage/classifier.txt')
El mecanismo para la configuración (obtener el clasificador) se inicializará en el siguiente paquete de nuestra lista, el Repo . Es una clase con dos métodos para recuperar y actualizar el clasificador entrenado usando el módulo pickle
integrado 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_)
Estamos cerca de finalizar nuestra API. Ahora solo le falta el módulo de Servicio . ¿Cuál es su propósito?
- Obtenga el clasificador entrenado del almacenamiento
- Transforme la imagen pasada desde la interfaz de usuario a un formato que el clasificador entienda
- Calcule la predicción con la imagen formateada a través del clasificador
- Devolver la predicción
Vamos a codificar este algoritmo:

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
Aquí puede ver que PredictDigitService
tiene dos dependencias: ClassifierFactory
y process_image
.
Comenzaremos creando una clase para crear y entrenar nuestro modelo:
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
La API está lista para la acción. Ahora podemos continuar con el paso de procesamiento de imágenes.
Procesamiento de imágenes
El procesamiento de imágenes es un método para realizar ciertas operaciones en una imagen para mejorarla o extraer información útil de ella. En nuestro caso, necesitamos hacer una transición suave de la imagen dibujada por un usuario al formato del modelo de aprendizaje automático.
Importemos algunos ayudantes para lograr ese objetivo:
import numpy as np from skimage import exposure import base64 from PIL import Image, ImageOps, ImageChops from io import BytesIO
Podemos dividir la transición en seis partes distintas:
1. Reemplace un fondo transparente con un color
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. Recorte los bordes abiertos
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. Agrega bordes del mismo tamaño
def pad_image(image): return ImageOps.expand(image, border=30, fill='#fff')
4. Convierta la imagen al modo de escala de grises
def to_grayscale(image): return image.convert('L')
5. Invertir colores
def invert_colors(image): return ImageOps.invert(image)
6. Cambiar el tamaño de la imagen a formato 8x8
def resize_image(image): return image.resize((8, 8), Image.LINEAR)
Ahora puedes probar la aplicación. Ejecute la aplicación e ingrese el siguiente comando para enviar una solicitud con esta imagen de iStock a la 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
Debería ver el siguiente resultado:
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
La imagen de muestra mostraba el número 8 y nuestra aplicación lo identificó correctamente como tal.
Crear un panel de dibujo a través de React
Para arrancar rápidamente la aplicación frontend, usaremos el modelo estándar de CRA:
create-react-app frontend cd frontend
Después de configurar el lugar de trabajo, también necesitamos una dependencia para dibujar dígitos. El paquete react-sketch se adapta perfectamente a nuestras necesidades:
npm i react-sketch
La aplicación tiene un solo componente. Podemos dividir este componente en dos partes: lógica y vista .
La parte de la vista es responsable de representar el panel de dibujo, los botones Enviar y Restablecer . Cuando interactuamos, también deberíamos representar una predicción o un error. Desde la perspectiva lógica, tiene las siguientes funciones: enviar imágenes y borrar el boceto .
Siempre que un usuario haga clic en Enviar , el componente extraerá la imagen del componente de boceto y recurrirá a la función makePrediction
del módulo API. Si la solicitud al back-end tiene éxito, estableceremos la variable de estado de predicción. De lo contrario, actualizaremos el estado de error.
Cuando un usuario hace clic en Restablecer , el boceto se borrará:
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 lógica es suficiente. Ahora podemos agregarle la interfaz visual:
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;
El componente está listo, pruébelo ejecutando y yendo a localhost:3000
después de:
npm run start
La aplicación de demostración está disponible aquí. También puede buscar el código fuente en GitHub.
Terminando
La calidad de este clasificador no es perfecta, y no pretendo que lo sea. La diferencia entre los datos que usamos para el entrenamiento y los datos que provienen de la interfaz de usuario es enorme. A pesar de eso, creamos una aplicación funcional desde cero en menos de 30 minutos.
En el proceso, perfeccionamos nuestras habilidades en cuatro campos:
- Aprendizaje automático
- desarrollo de fondo
- Procesamiento de imágenes
- Desarrollo front-end
No hay escasez de posibles casos de uso para software capaz de reconocer dígitos escritos a mano, que van desde software educativo y administrativo hasta servicios postales y financieros.
Por lo tanto, espero que este artículo lo motive a mejorar sus habilidades de aprendizaje automático, procesamiento de imágenes y desarrollo de front-end y back-end, y use esas habilidades para diseñar aplicaciones maravillosas y útiles.
Si desea ampliar sus conocimientos sobre el aprendizaje automático y el procesamiento de imágenes, puede consultar nuestro Tutorial de aprendizaje automático antagónico.