Um "Hello World" em Deep Learning
Tags: deeplearning, machinelearning, python, braziliandevs
Published: 2024-05-05 13:20:22

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.

![dígitos do MNIST](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1showgv1onue4hmb6tiz.png)

<center><small><small>Imagem de amostras do MNIST.</small></small></center>

**Download do MNIST**: Usaremos a versão do dataset MNIST disponibilizada neste site: [http://yann.lecun.com/exdb/mnist/](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.

![Download MNIST](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/it6jwqmbbrbtk619fq5o.png)

<center><small><small>Site para download do MNIST. <a href="http://yann.lecun.com/exdb/mnist/">http://yann.lecun.com/exdb/mnist/</a></small></small></center>

---

## Carregando o dataset

Usaremos as seguintes funções para abrir o MNIST, pois o mesmo está no formato `.gz`.

```Python
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 <u>treino</u>); `train-labels-idx1-ubyte.gz` (<u>labels</u> do conjunto de <u>treino</u>); `t10k-images-idx3-ubyte.gz` (conjunto de <u>teste</u>) e `t10k-labels-idx1-ubyte.gz` (<u>labels</u> do conjunto de <u>teste</u>).

```Python
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.

```Python
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](https://g1.globo.com/ba/bahia/noticia/2023/09/01/com-mais-de-mil-prisoes-na-ba-sistema-de-reconhecimento-facial-e-criticado-por-racismo-algoritmico-inocente-ficou-preso-por-26-dias.ghtml) 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](https://g1.globo.com/ba/bahia/noticia/2023/09/01/com-mais-de-mil-prisoes-na-ba-sistema-de-reconhecimento-facial-e-criticado-por-racismo-algoritmico-inocente-ficou-preso-por-26-dias.ghtml).

Para avaliar a distribuição dos labels de treino do MNIST  usaremos o histograma:

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

![Histograma dos labels de treino](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pxlwpba2bgrjet82sq21.png)

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](https://en.wikipedia.org/wiki/Neural_network_(machine_learning)), geralmente possuem inúmeras camada ocultas.

![Uma rede MLP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j9t3ioth8u9egie419jz.png)

<center><small><small>Imagem de uma MLP. (Rede gerada com ajuda do site <a href="https://alexlenail.me/NN-SVG/">https://alexlenail.me/NN-SVG</a>)</small></small></center>

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](https://scikit-learn.org/stable/modules/sgd.html#sgd).

```Python
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_](https://en.wikipedia.org/wiki/Multilayer_perceptron) 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%](http://yann.lecun.com/exdb/mnist/).

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.

```Python
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`).

```Python
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, <u>cerca de 7.57% </u>.

### 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`](https://docs.python.org/3.12/library/pickle.html), 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`.

```Python
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()`:

```Python
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?

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

![Uma amostra aleatória](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vx73h5ibmgcw92v644qi.png)


```Python
# 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](https://en.wikipedia.org/wiki/CIFAR-10): possui 10 classes com imagens de aviões, cães, gatos, carros, caminhões, etc.
  
- [Fashion MNIST](https://en.wikipedia.org/wiki/Fashion_MNIST): possui 10 classes com imagens de roupas, sapatos, bolsas e outros itens semelhantes.

- [CIFAR-100](https://www.cs.toronto.edu/~kriz/cifar.html): possui 100 classes, contendo imagens como exemplos de animais, veículos, árvores e flores.

<center><h4><i>Esperamos que tenham gostado e agradecemos a leitura!</i></h4>🤖 ☕ 🗿 🦥 ♟️</center>