Reconhecimento de números de aprendizado de máquina - do zero ao aplicativo
Publicados: 2022-03-11Aprendizado de máquina, visão computacional, construção de APIs poderosas e criação de belas interfaces de usuário são campos interessantes que testemunham muita inovação.
Os dois primeiros exigem matemática e ciência extensas, enquanto o desenvolvimento de API e UI se concentra no pensamento algorítmico e no design de arquiteturas flexíveis. Eles são muito diferentes, então decidir qual você quer aprender a seguir pode ser um desafio. O objetivo deste artigo é demonstrar como todos os quatro podem ser empregados na criação de um aplicativo de processamento de imagem.
O aplicativo que vamos construir é um simples reconhecedor de dígitos. Você desenha, a máquina prevê o dígito. A simplicidade é essencial porque nos permite ver o quadro geral em vez de focar nos detalhes.
Por uma questão de simplicidade, usaremos as tecnologias mais populares e fáceis de aprender. A parte de aprendizado de máquina usará Python para o aplicativo de back-end. Quanto ao lado interacional do app, vamos operar por meio de uma biblioteca JavaScript que dispensa apresentações: React.
Aprendizado de máquina para adivinhar dígitos
A parte principal do nosso aplicativo é o algoritmo que adivinha o número sorteado. O aprendizado de máquina será a ferramenta usada para obter uma boa qualidade de palpite. Esse tipo de inteligência artificial básica permite que um sistema aprenda automaticamente com uma determinada quantidade de dados. Em termos mais amplos, o aprendizado de máquina é um processo de encontrar uma coincidência ou um conjunto de coincidências nos dados para confiar neles para adivinhar o resultado.
Nosso processo de reconhecimento de imagem contém três etapas:
- Obtenha imagens de dígitos desenhados para treinamento
- Treine o sistema para adivinhar os números por meio de dados de treinamento
- Teste o sistema com dados novos/desconhecidos
Meio Ambiente
Precisaremos de um ambiente virtual para trabalhar com aprendizado de máquina em Python. Essa abordagem é prática porque gerencia todos os pacotes Python necessários, portanto, você não precisa se preocupar com eles.
Vamos instalá-lo com os seguintes comandos de terminal:
python3 -m venv virtualenv source virtualenv/bin/activate
Modelo de treinamento
Antes de começarmos a escrever o código, precisamos escolher um “professor” adequado para nossas máquinas. Normalmente, os profissionais de ciência de dados experimentam diferentes modelos antes de escolher o melhor. Vamos pular modelos muito avançados que exigem muita habilidade e prosseguir com o algoritmo de k-vizinhos mais próximos.
É um algoritmo que obtém algumas amostras de dados e as organiza em um plano ordenado por um determinado conjunto de características. Para entender melhor, vamos analisar a imagem a seguir:
Para detectar o tipo do Ponto Verde , devemos verificar os tipos de k vizinhos mais próximos onde k é o conjunto de argumentos. Considerando a imagem acima, se k for igual a 1, 2, 3 ou 4, o palpite será um triângulo preto, pois a maioria dos k vizinhos mais próximos do ponto verde são triângulos pretos. Se aumentarmos k para 5, então a maioria dos objetos são quadrados azuis, portanto o palpite será um Quadrado Azul .
Existem algumas dependências necessárias para criar nosso modelo de aprendizado de máquina:
- sklearn.neighbors.KNeighborsClassifier é o classificador que usaremos.
- sklearn.model_selection.train_test_split é a função que nos ajudará a dividir os dados em dados de treinamento e dados usados para verificar a correção do modelo.
- sklearn.model_selection.cross_val_score é a função para obter uma nota para a correção do modelo. Quanto maior o valor, melhor a correção.
- sklearn.metrics.classification_report é a função para mostrar um relatório estatístico das suposições do modelo.
- sklearn.datasets é o pacote usado para obter dados para treinamento (imagens de dígitos).
- numpy é um pacote amplamente utilizado na ciência, pois oferece uma maneira produtiva e confortável de manipular estruturas de dados multidimensionais em Python.
- matplotlib.pyplot é o pacote usado para visualizar dados.
Vamos começar instalando e importando todos eles:
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
Agora, precisamos carregar o banco de dados MNIST. O MNIST é um conjunto de dados clássico de imagens manuscritas usado por milhares de novatos no campo de aprendizado de máquina:
digits = load_digits()
Assim que os dados forem buscados e estiverem prontos, podemos passar para a próxima etapa de dividir os dados em duas partes: treinamento e teste .
Usaremos 75% dos dados para treinar nosso modelo para adivinhar dígitos e usaremos o restante dos dados para testar a correção do modelo:
(X_train, X_test, y_train, y_test) = train_test_split( digits.data, digits.target, test_size=0.25, random_state=42 )
Os dados agora estão organizados e estamos prontos para usá-los. Tentaremos encontrar o melhor parâmetro k para nosso modelo para que as estimativas sejam mais precisas. Não podemos esquecer o valor de k neste estágio, pois temos que avaliar o modelo com diferentes valores de k .
Vamos ver por que é essencial considerar um intervalo de valores de k e como isso melhora a precisão do nosso 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()
A execução deste código mostrará o gráfico a seguir descrevendo a precisão do algoritmo com diferentes valores de k .
Como você pode ver, um valor k de 3 garante a melhor precisão para nosso modelo e conjunto de dados.
Usando Flask para construir uma API
O núcleo do aplicativo, que é um algoritmo que prevê os dígitos das imagens, já está pronto. Em seguida, precisamos decorar o algoritmo com uma camada de API para disponibilizá-lo para uso. Vamos usar o popular framework web Flask para fazer isso de forma clara e concisa.
Começaremos instalando o Flask e as dependências relacionadas ao processamento de imagens no ambiente virtual:
pip install Flask Pillow scikit-image
Quando a instalação for concluída, passamos para a criação do arquivo de ponto de entrada do aplicativo:
touch app.py
O conteúdo do arquivo ficará assim:
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)
Você receberá um erro informando que PredictDigitView
e IndexView
não estão definidos. A próxima etapa é criar um arquivo que inicializará essas visualizações:
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)
Mais uma vez, encontraremos um erro sobre uma importação não resolvida. O pacote Views conta com três arquivos que ainda não temos:
- Configurações
- Repositório
- Serviço
Vamos implementá-los um por um.
Settings é um módulo com configurações e variáveis constantes. Ele armazenará o caminho para o classificador serializado para nós. Isso levanta uma questão lógica: por que preciso salvar o classificador?
Porque é uma maneira simples de melhorar o desempenho do seu aplicativo. Em vez de treinar o classificador toda vez que você receber uma solicitação, armazenaremos a versão preparada do classificador, permitindo que ele funcione imediatamente:
import os BASE_DIR = os.getcwd() CLASSIFIER_STORAGE = os.path.join(BASE_DIR, 'storage/classifier.txt')
O mecanismo de configurações — obtendo o classificador — será inicializado no próximo pacote da nossa lista, o Repo . É uma classe com dois métodos para recuperar e atualizar o classificador treinado usando o módulo pickle
integrado do 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 perto de finalizar nossa API. Agora falta apenas o módulo Service . Qual é o seu propósito?
- Obtenha o classificador treinado do armazenamento
- Transforme a imagem passada da IU para um formato que o classificador entenda
- Calcular a previsão com a imagem formatada por meio do classificador
- Devolva a previsão
Vamos 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
Aqui você pode ver que PredictDigitService
tem duas dependências: ClassifierFactory
e process_image
.
Começaremos criando uma classe para criar e treinar nosso 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
A API está pronta para ação. Agora podemos prosseguir para a etapa de processamento de imagem.
Processamento de imagem
O processamento de imagem é um método de realizar certas operações em uma imagem para melhorá-la ou extrair algumas informações úteis dela. No nosso caso, precisamos fazer uma transição suave da imagem desenhada por um usuário para o formato do modelo de aprendizado de máquina.
Vamos importar alguns auxiliares para atingir esse objetivo:
import numpy as np from skimage import exposure import base64 from PIL import Image, ImageOps, ImageChops from io import BytesIO
Podemos dividir a transição em seis partes distintas:
1. Substitua um fundo transparente por uma cor
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. Apare as bordas abertas
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. Adicione bordas de tamanho igual
def pad_image(image): return ImageOps.expand(image, border=30, fill='#fff')
4. Converta a imagem para o modo de escala de cinza
def to_grayscale(image): return image.convert('L')
5. Inverta as cores
def invert_colors(image): return ImageOps.invert(image)
6. Redimensione a imagem para o formato 8x8
def resize_image(image): return image.resize((8, 8), Image.LINEAR)
Agora você pode testar o aplicativo. Execute o aplicativo e digite o comando abaixo para enviar uma solicitação com esta imagem da iStock para a 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
Você deve ver a seguinte saída:
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
A imagem de exemplo mostrava o número 8 e nosso aplicativo o identificou corretamente como tal.
Criando um painel de desenho via React
Para inicializar rapidamente o aplicativo front-end, usaremos o clichê do CRA:
create-react-app frontend cd frontend
Depois de configurar o local de trabalho, também precisamos de uma dependência para desenhar dígitos. O pacote react-sketch atende perfeitamente às nossas necessidades:
npm i react-sketch
O aplicativo tem apenas um componente. Podemos dividir este componente em duas partes: logic e view .
A parte da vista é responsável por representar o painel de desenho, os botões Enviar e Redefinir . Quando interagimos, devemos também representar uma previsão ou um erro. Do ponto de vista da lógica, tem as seguintes funções: enviar imagens e limpar o esboço .
Sempre que um usuário clicar em Enviar , o componente extrairá a imagem do componente de esboço e apelará para a função makePrediction
do módulo da API. Se a solicitação para o back-end for bem-sucedida, definiremos a variável de estado de previsão. Caso contrário, atualizaremos o estado do erro.
Quando um usuário clicar em Redefinir , o esboço será limpo:
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 }
A lógica é suficiente. Agora podemos adicionar a interface visual a ele:
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;
O componente está pronto, teste-o executando e indo para localhost:3000
depois:
npm run start
O aplicativo de demonstração está disponível aqui. Você também pode navegar pelo código-fonte no GitHub.
Empacotando
A qualidade deste classificador não é perfeita, e não pretendo que seja. A diferença entre os dados que usamos para treinamento e os dados provenientes da interface do usuário é enorme. Apesar disso, criamos um aplicativo funcional do zero em menos de 30 minutos.
No processo, aprimoramos nossas habilidades em quatro campos:
- Aprendizado de máquina
- Desenvolvimento de back-end
- Processamento de imagem
- Desenvolvimento front-end
Não faltam casos de uso em potencial para software capaz de reconhecer dígitos manuscritos, desde software educacional e administrativo até serviços postais e financeiros.
Portanto, espero que este artigo o motive a melhorar suas habilidades de aprendizado de máquina, processamento de imagens e desenvolvimento de front-end e back-end e use essas habilidades para projetar aplicativos maravilhosos e úteis.
Se você quiser ampliar seu conhecimento sobre machine learning e processamento de imagens, confira nosso Tutorial de Adversarial Machine Learning.