Este post apresenta um passo a passo para rodar um modelo de Deep Learning (DL) que realiza uma tarefa que até pouco tempo atrás era um desafio. A partir de uma base de dados, devidamente organizada e rotulada, contendo dezenas de milhares de dígitos (0 a 9) escritos a mão, o modelo de DL será capaz de predizer qual é o dígito da imagem de entrada. Como nem todo mundo possui uma letra tão bonita, essa tarefa não é trivial, principalmente para um computador.
Modelos desse tipo, são comumente usados para identificar de forma automática o conteúdo de texto em documentos, PDFs, imagens, entre outros. Com poucas alterações no que veremos, é possível classificar outros conteúdos de outros datasets. No final do post falamos um pouco sobre isso.
Vamos lá!? ☕
Pré-requisitos
Para executar os códigos deste post precisamos das seguintes bibliotecas do Python 3:
- Matplotlib (
pip install matplotlib
) - Numpy (
pip install numpy
) - Scikit-learn (
pip install scikit-learn
)
Dataset MNIST
Criado em 1994, o dataset MNIST (Modified National Institute of Standards and Technology) é bastante utilizado na área de visão computacional e processamento de imagens. A versão atual é composta por 60k imagens de treino e 10k de teste. Cada amostra do MNIST é uma imagem 28x28 em tons de cinza e representa um dígito manuscrito que assume valores entre 0 e 9.
Download do MNIST: Usaremos a versão do dataset MNIST disponibilizada neste site: http://yann.lecun.com/exdb/mnist/. Devemos entrar no site e baixar os 4 datasets disponibilizados, colocando-os num diretório. Para facilitar criamos uma pasta chamada datasets
e colocamos os arquivos .gz
baixados nele.
Carregando o dataset
Usaremos as seguintes funções para abrir o MNIST, pois o mesmo está no formato .gz
.
import gzip
import struct
import numpy as np
def load_dataset(path_dataset):
with gzip.open(path_dataset,'rb') as f:
magic, size = struct.unpack('>II', f.read(8))
nrows, ncols = struct.unpack('>II', f.read(8))
data = np.frombuffer(f.read(), dtype=np.dtype(np.uint8).newbyteorder('>'))
data = data.reshape((size, nrows, ncols))
return data
def load_label(path_label):
with gzip.open(path_label,'rb') as f:
magic, size = struct.unpack('>II', f.read(8))
data = np.frombuffer(f.read(), dtype=np.dtype(np.uint8).newbyteorder('>'))
return data
O código abaixo carrega as 4 partes do dataset MNIST: train-images-idx3-ubyte.gz
(conjunto de treino); train-labels-idx1-ubyte.gz
(labels do conjunto de treino); t10k-images-idx3-ubyte.gz
(conjunto de teste) e t10k-labels-idx1-ubyte.gz
(labels do conjunto de teste).
X_train = load_dataset(r'./datasets/train-images-idx3-ubyte.gz')
y_train = load_label(r'./datasets/train-labels-idx1-ubyte.gz')
X_test = load_dataset(r'./datasets/t10k-images-idx3-ubyte.gz')
y_test = load_label(r'./datasets/t10k-labels-idx1-ubyte.gz')
Após carregar os datasets, checamos se suas dimensões estão dentro do esperado.
print(X_train.shape) # valor esperado: (60000, 28, 28)
print(y_train.shape) # (60000,)
print(X_test.shape) # (10000, 28, 28)
print(y_test.shape) # (10000, )
Note que a dimensão das imagens dos conjuntos de treino e de teste é 28 x 28.
Histograma dos labels
Uma etapa importante na construção dos datasets em DL é considerar classes balanceadas. Datasets de treino com classes desbalanceadas costumam introduzir um viés, fazendo com que as predições favoreçam algumas classes ao invés de outras. Por ex., se existe uma grande quantidade de amostras do dígito 1 e poucas do dígito 7, é comum que o modelo aprenda esse padrão e tente reproduzi-lo nas predições que fizer. Nesse caso, o modelo tende a achar que "tudo" se parece mais com o dígito 1 e considera o dígito 7 como algo menos comum. Como o MNIST possui 10 classes, referentes aos dígitos de 0 a 9, então é esperado que possuam distribuições semelhantes.
Curiosidade: Na prática, o viés pode levar a falhas brutais. Por ex., um caso ocorrido em 2022 levou um inocente a ficar preso por 26 dias devido a um erro "algorítmico", causando danos e prejuízos reais. Para mais detalhes do caso clique aqui.
Para avaliar a distribuição dos labels de treino do MNIST usaremos o histograma:
import matplotlib.pyplot as plt
fig, axs = plt.subplots(figsize=(15, 5), dpi=70)
x_ticks = np.arange(min(y_train), max(y_train)+2, 1)
bins = x_ticks - 0.5
plt.hist(y_train, edgecolor='k', rwidth=0.8, bins=bins, color='#ABCDEF')
plt.title('Histograma do label de treino y_train', size=16)
plt.xlabel('Classe - Label', size=14)
plt.ylabel('Frequência', size=14)
plt.xticks(x_ticks)
plt.show()
O gráfico acima mostra que as classes do dataset MNIST estão balanceadas. Sendo assim, seguimos para a etapa de treinar e utilizar os modelos treinados para predizer dígitos manuscritos de entrada (input).
Aplicando modelos de Machine Learning e Deep Learning
Para avaliar a performance do modelo de Deep Learning que usaremos, também vamos realizar predições dos dígitos manuscritos com um modelo de Machine Learning (ML).
Nota: Não entraremos nos detalhes do que é ou não é Machine Learning e Deep Learning. No entanto, vale ter em mente que os modelos de Deep Learning costumam ser considerados como um subconjunto dos modelos de Machine Learning. Isso porque eles são modelos capazes de aprender de forma automática (sem intervenção humana) e fazer predições baseadas naquilo que aprenderam. A grande diferença entre os dois é que os modelos de DL são capazes de aprender mais detalhes. Além disso, sua formulação é mais complexa e sua arquitetura, as famosas redes neurais artificiais, geralmente possuem inúmeras camada ocultas.
O fato de um determinado modelo ser classificado como X ou Y, não mudará sua natureza intrínseca. Questões de nomenclatura/taxonomia geralmente buscam organizar e facilitar o entendimento do objeto de estudo e não criar barreiras ou complexidades extras.
Modelo SGD
O modelo SGD (Stochastic Gradient Descent) SGDClassifier
do scikit-learn
será o modelo de ML que iremos compara com o de DL. Ele realiza o ajuste de um modelo SVM (Support Vector Machine). O código a seguir treina/ajusta o SGDClassifier
sobre o dataset MNIST. Note que usamos uma etapa de pré-processamento com o StandardScaler
, que transforma a média dos dados para 0 e o desvio padrão igual a 1. Mais detalhes sobre podem ser encontrados em sua documentação.
from sklearn.linear_model import SGDClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
model_sgd_classifier = make_pipeline(StandardScaler(),
SGDClassifier(random_state=2024))
model_sgd_classifier.fit((X_train).reshape(len(X_train), 28*28),
y_train)
Após o treinamento do modelo SGDClassifier
, já podemos realizar as predições.
Modelo Multilayer Perceptron Classifier
Agora apresentaremos de fato o "Hello World!" (Olá, Mundo!) em DL. O modelo escolhido para isso é uma rede neural conhecida como Multilayer Percepetron ou MLP. Sua arquitetura geralmente possui 3 ou mais camadas ocultas. Esse modelo foi um marco, pois possui capacidade de aprender padrões não lineares e ser treinado em computadores pessoais. Além disso, suas implementações atuais, costumam ter um bom desempenho para tarefas um tanto complexas, como classificar dígitos manuscritos. Embora para os humanos, reconhecer dígitos ou letras seja algo aparentemente simples (principalmente no idioma raiz), para os computadores nem sempre foi assim. No caso do MNIST, de 1998 a 2012 houve um grande salto na performance dos modelos, passando de 88% de acerto para 99.77%.
O código abaixo treina o modelo MLPClassifier
, uma implementação de um MLP classificador. Ele também está disponível na biblioteca open-source Scikit-learn.
from sklearn.neural_network import MLPClassifier
model_mlp = MLPClassifier(max_iter=300, random_state=2024)
model_mlp.fit((X_train/255).reshape(len(X_train), 28*28), y_train)
Realizando as predições com os modelos treinados
Agora que treinamos os modelos, realizamos as predições usando o método predict
. A seguir realizamos a predição sobre o conjunto de testes (X_test
).
print(model_sgd_classifier.score(X_test.reshape(len(X_test), 28*28), y_test))
print(model_mlp.score(X_test.reshape(len(X_test), 28*28), y_test))
'''
saída esperada:
0.9027
0.9784
'''
Comparando o valor do score de ambos, notamos que o desempenho do MLPClassifier
foi maior. Vale a pena comentar que ambos modelos se saíram muito bem, com assertividades acima de 90%. No entanto, a diferença entre eles pode ser considerada alta, já que foi de 0.9784 - 0.9027 = 0.0757
. Ou seja, cerca de 7.57% .
Salvando e carregando um modelo
As etapas de treinamento podem demorar e consumir recursos como energia, vida útil da CPU ou GPU entre outros. Dessa forma, é interessante salvarmos o modelo treinado em nosso computador e carregá-lo sempre que quisermos realizar uma predição.
Para salvar o modelo utilizamos a biblioteca pickle
, responsável por serializar (passar de objeto Python/Scikit-learn para binário) e deserializar (fazer o processo contrário). Como o pickle
é uma biblioteca padrão do Python 3, não é necessário sua instalação, basta importá-la de forma usual com o import
.
import pickle
output = open('model_classifier_mnist.pkl', 'wb')
pickle.dump(model_mlp, output, -1)
output.close()
O código acima salva o modelo MLPClassifier
já treinado (objeto model_mlp
) com o nome model_classifier_mnist.pkl
. Note que usamos o método dump()
do pickle
para realizar essa tarefa.
Já para carregar o modelo salvo, utilizamos o load()
:
pickle_file = open('model_classifier_mnist.pkl', 'rb')
model_test = pickle.load(pickle_file)
pickle_file.close()
Teste com uma amostra aletória
Para finalizar, vamos realizar um simples teste que sorteia de forma aleatória um dígito manuscrito e realiza a predição com ambos modelos. Será que ambos modelos vão reconhecer o dígito manuscrito?
idx_random = np.random.randint(0, len(X_train)) # sorteia um índice
print(f'idx_random = {idx_random}')
some_digit = X_train[idx_random] # seleciona o valor dos pixels no conjunto de treino
# exibe o dígito manuscrito como uma imagem
fig, axs = plt.subplots(figsize=(29, 29), dpi=60)
plt.imshow(some_digit, cmap='binary')
plt.axis('off')
plt.show()
# realiza predição do dígito sorteado com os modelos MLP e SGD
print(model_mlp.predict([some_digit.reshape(28*28)]))
print(model_sgd_classifier.predict([some_digit.reshape(28*28)]))
'''
saída esperada:
[2]
[8]
'''
Embora a tendência seja que ambos modelos acertem, visto que escolhemos uma amostra do conjunto de treino, apenas o modelo MLP acertou a predição do dígito sorteado. Para dificultar um pouco mais, podemos escolher uma amostra do conjunto de testes ou fazer uma imagem manuscrita com nossa própria letra e usar como entrada.
Datasets semelhantes
Outros exemplos de datasets que poderíamos ter usado ao invés do MNIST:
CIFAR-10: possui 10 classes com imagens de aviões, cães, gatos, carros, caminhões, etc.
Fashion MNIST: possui 10 classes com imagens de roupas, sapatos, bolsas e outros itens semelhantes.
CIFAR-100: possui 100 classes, contendo imagens como exemplos de animais, veículos, árvores e flores.
Top comments (0)