Reconhecimento de escrita manual com Redes Neurais Convolucionais

No final deste post você terá uma visão geral sobre o funcionamento das redes neurais convolucionais e saberá como usá-las para criar um modelo capaz de fazer o reconhecimento de escrita manual, mais particularmente reconhecer dígitos escritos à mão em imagens.

O reconhecimento de escrita manual – também conhecido como handwriting recognition, do inglês – é uma aplicação de software que ainda encontra bastante demanda hoje em dia. Se você já usou o GoodNotes no iPad, então sabe do que eu estou falando. O reconhecimento de assinaturas escritas à mão a partir de documentos digitalizados/escaneados, a captura de endereços postais escritos em envelopes, ou informações monetárias escritas em cheques bancários estão entre as aplicações mais comuns desta técnica.

De um modo geral, há duas situações nas quais isto pode ser feito:

On-line: Conforme o texto estiver sendo escrito, ocorrerá a conversão do sinal obtido a partir do traçado para códigos que correspondem às letras, os quais poderão ser utilizados no computador e em aplicativos de processamento de texto. Isto corre, por exemplo, no aplicativo GoodNotes.

 

Off-line: O cerne deste tipo de tarefa está em transcrever para dados eletrônicos as informações escritas à mão em qualquer folha de papel que tenha sido digitalizada, ou mesmo textos presentes em imagens naturais.

 

Sem dúvidas, este último cenário é talvez o que mais pode se beneficiar desta técnica, pois há muitos documentos impressos com dados escritos à mão cujas informações podem possuir grande valor, inclusive estratégico. Meu objetivo neste post é abordar justamente a aplicação do reconhecimento de escrita manual neste cenário, fazendo uso de um modelo gerado a partir de uma rede neural convolucional, com base em um conjunto de dados de treino, para reconhecer caracteres numéricos presentes em imagens ainda não vistas pelo modelo. Nós utilizaremos o dataset MNIST, que já é bem conhecido, e o framework Keras. O dataset MNIST possui 60k exemplos de treino e 10k exemplos de teste, sendo que todos estes dados correspondem a dígitos no intervalo 0-9, escritos à mão e digitalizados. Cada dígito é representado por uma imagem monocromática com 28×28 pixels.

Uma vez que nosso tutorial se baseia no uso de redes neurais convolucionais, é útil ter um certo conhecimento sobre como funciona esta arquitetura. Por este motivo, irei resumir um pouco deste funcionamento, mas é importante que você tenha uma visão mais completa em torno deste assunto. Por isso, eu recomendo que você leia este post.

Leia também – Machine Learning e a matemática da aprendizagem supervisionada

Uma visão geral das Redes neurais convolucionais

Não há muita diferença entre um rede neural regular e uma convolucional (doravante denominada ConvNet). O que difere estas arquiteturas é que as ConvNets basicamente foram concebidas para extrair features a partir de dados brutos presentes nos pixels de uma imagem. Elas possuem camadas com neurônios arranjados em três dimensões: largura, altura e profundidade. Trata-se de uma sequência de três tipos de camadas principais:

  • Convolutional Layer (Camada Convolucional) – Para cada sub-região presente na imagem, esta camada aplica um conjunto de operações matemáticas (transformações lineares) para produzir um simples valor no mapa de features que é gerado como saída da camada (cada convolução pode gerar um ou mais mapas de features). Uma função de ativação do tipo ReLU (Rectified Linear Unit) é comumente aplicada com o objetivo de promover a não linearidade ao modelo, uma das funções mais populares para  esta finalidade. A camada convolucional pode gerar um ou mais mapas de features como saída.
Uma animação que ilustra como uma convolução acontece / Fonte
  • Pooling Layer (Camada de subamostragem): A camada Pooling computa o máximo de valores obtidos em cada filtro da camada convolucional, só que o resultado desta operação é apenas uma pequena amostra do que a camada recebeu como entrada (uma pequena amostra com as informações mais representativas obtidas com cada filtro na camada convolucional). Uma vantagem desta camada é que ela reduz a dimensionalidade ao mesmo tempo em que consegue manter as informações mais úteis. É um dos motivos por trás da eficiência computacional das ConvNets
  • Fully-Connected Layer (Camada densamente conectada) é a camada final de uma ConvNet, onde se coloca o classificador. Trata-se de uma camada densamente conectada, na qual cada neurônio conectado a todos os outros de uma camada anterior e posterior. Basicamente, a FCL recebe uma entrada (que pode ser a saída de uma camada convolucional, ReLU ou Pooling) e gera como saída um vetor de dimensionalidade N, onde N é o número de classes que o modelo precisa predizer.

A camada final de uma ConvNet terá reduzido todos os dados brutos da imagem a um simples vetor com os scores atribuídos às classes. Assim, esta arquitetura transforma a imagem original, camada por camada a partir dos valores dos pixels originais em scores de classes (em nosso caso, scores que representam os dígitos 0-9).

Exemplo de aplicação: Reconhecendo escrita manual

Vamos partir de um ponto em que você já tenha instalado tudo o que é preciso para que este código funcione no seu computador:

  • Anaconda –  O Anaconda é o ambiente ideal para quem trabalha com ciência de dados e Machine Learning em Python. Baixe o ambiente clicando aqui.
  • OpenCV – Library voltada para o desenvolvimento de aplicações em visão computacional. Neste caso, você precisa ter instalada a implementação em Python. Se tiver o Anaconda instalado, basta rodar este comando: $ conda install -c conda-forge opencv ou $ conda install -c conda-forge opencv=3.2.0
  • Keras – Uma library minimalista que usa a linguagem Python para nos permitir criar uma grande variedade de modelos de deep learning em cima dos frameworks TensorFlow e Theano, abstraindo a maior parte da complexidade envolvida no processo de criar modelos puramente nestes dois frameworks. Para instalar, siga as instruções presentes no próprio site da ferramenta.

Além disso, irei supor que você já saiba programar em Python e que conheça ao menos o básico do Keras.

 

Montando a rede neural

Para obter um modelo com a maior acurácia possível, usaremos duas camadas convolucionais, duas camadas de pooling e duas fully connected. Você pode baixar o código fonte que foi criado como um notebook na ferramenta Jupyter (você pode baixar o Jupyter diretamente pelo ambiente Anaconda).

from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Flatten
import numpy as np
from matplotlib import pyplot as plt
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.utils import np_utils
from keras import backend as K

K.set_image_dim_ordering('th')
import cv2
import matplotlib.pyplot as plt

Perceba que, na primeira linha, nós estamos importando o dataset MNIST, que usaremos para treinar as nossa rede neural para reconhecer dígitos. Dense, Conv2D, MaxPooling2D e Dropout são classes que possuem uma importância vital para o que queremos fazer aqui:

  • Dense – Nos permite criar camadas do tipo Fully Conected, ou Densamente conectadas.
  • Conv2D – Nos permite criar as camadas convolucionais da rede, as quais usaremos para obter nossos feature maps.
  • MaxPooling2D – Nos permite criar camadas do tipo Pooling, as quais a rede usa para extrair apenas as informações mais salientes obtidas por meio dos filtros convolucionais.
  • Dropout – Um método de regularização que vai nos ajudar a prevenir o sobre-ajuste do modelo aos dados de treino (também chamado de overfitting). O objetivo principal deste método é deliberadamente “desligar” alguns neurônios da rede de maneira aleatória (bem como suas conexões) durante o treino, de modo a evitar que eles se adaptem de mais aos dados de exemplo, o que resultaria num modelo com pouca capacidade de generalização.
#Dividimos os dados em subconjuntos de treinamento e teste.
(X_train, y_train), (X_test, y_test) = mnist.load_data()

# Como estamos trabalhando em escala de cores cinza podemos
# definir a profundidade com o valor 1.
X_train = X_train.reshape(X_train.shape[0], 1, 28, 28).astype('float32')
X_test = X_test.reshape(X_test.shape[0], 1, 28, 28).astype('float32')

# Normalizamos nossos dados de acordo com variacao da
# escala de cinza.Os valores em ponto flutuante ficam no inmtervalo [0,1], ao invés de [0,255]
X_train = X_train / 255
X_test = X_test / 255

# Converte y_train e y_test, que são vetores de classes, para uma matriz de classe binária (one-hot vectors)
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)

# Quantidade de tipos de digitos encontrados no MNIST. Neste caso, o valor é 10, correspondendo a (0,1,2,3,4,5,6,7,8,9).
num_classes = y_test.shape[1]

No MNIST, as imagens foram todas centradas em 28×28 pixels e, por isso, tanto X_train.reshape(), quanto X_test.reshape() precisam receber parâmetros em função disto. Assim, X_train.shape[0] é uma tupla no formato (28, 28) e os três parâmetros seguintes correspondem à profundidade da entrada (o valor é 1, pois estamos usando escala de cinza) e as dimensões da imagem que serão usadas para compor o tensor (28 linhas e 28 colunas). Considerando que o conjunto de treino é composto por 6000 exemplos, Xtrain possui a forma (60000, 1, 28, 28).

A partir deste ponto, nós montamos a arquitetura da rede convolucional,

def deeper_cnn_model():
    model = Sequential()

    # A Convolution2D sera a nossa camada de entrada. Podemos observar que ela possui 
    # 30 feature maps com tamanho de 5 × 5 e uma função de ativação do tipo ReLU. 
    model.add(Conv2D(30, (5, 5), input_shape=(1, 28, 28), activation='relu'))

    # A camada MaxPooling2D sera nossa segunda camada onde teremos uma janela de amostragem de tamanho 2 x 2
    model.add(MaxPooling2D(pool_size=(2, 2)))

    # Uma nova camada convolucional, com 15 feature maps de tamanho 3 × 3, e função de ativação ReLU 
    model.add(Conv2D(15, (3, 3), activation='relu'))

    # Uma nova subamostragem com um pooling de dimensoes 2 x 2.
    model.add(MaxPooling2D(pool_size=(2, 2)))
    
    # Incluimos um dropout com probabilidade de 20% (você pode experimentar outros valores)
    model.add(Dropout(0.2))

    # Precisamos converter a saída da camada convolucional, para que ela possa ser usada como entrada na camada Densamente conectada que fica logo em seguida. 
    # O que isto faz é "achatar/aplanar" a estrutura da saída das camadas convolucionais, criando um único e longo vetor de features
    # que será usado pela camada Fully Conected, ou Densamente conectada.
    model.add(Flatten())

    # Camada fully connected com 128 neuronios.
    model.add(Dense(128, activation='relu'))

    # Seguida de uma nova camada fully connected com 64 neuronios
    model.add(Dense(64, activation='relu'))
    
    # Seguida de uma nova camada fully connected com 32 neuronios
    model.add(Dense(32, activation='relu'))

    # A camada de saida possui o numero de neurônios compatível com o 
    # numero de classes a serem obtidas. Perceba que estamos usando uma função de ativação do tipo "softmax"
    model.add(Dense(num_classes, activation='softmax', name='preds'))

    # Configura todo o processo de treino da rede neural
    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    
    return model

Perceba que nós usamos o otimizador “Adam” no método compile(). Você pode, no entanto, que querer utilizar outros otimizadores e aqui está uma lista dos que você pode utilizar.

Treinando a rede

model = deeper_cnn_model()

model.summary()

model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=10, batch_size=200)

scores = model.evaluate(X_test, y_test, verbose=0)
print("\nacc: %.2f%%" % (scores[1]*100))

model.summary() exibe um resumo da arquitetura da rede que montamos, exemplo:

O método model.evaluate() é usado para medir a acurácia do modelo.

Como reconhecer dígitos novos, que não estão nos dados de treino

Depois de gerar o modelo, é útil podermos testá-lo com dígitos que ainda não foram expostos a ele. Ou seja, dados completamente inéditos. Para isto, vamos escrever o seguinte código:

# Força a imagem a ficar no mesmo padrão de tamanho dos dados de treino 28x28
if img_pred.shape != [28,28]:
    img2 = cv2.resize(img_pred, (28, 28))
    img_pred = img2.reshape(28, 28, -1)
else:
    img_pred = img_pred.reshape(28, 28, -1)

# O primeiro parâmetro, abaixo, indica que estamos submetendo apenas uma amostra. 
# Também informamos o valor para a profundidade = 1 (1 canal), número de linhas e colunas, que correspondem 28x28 da imagem.
img_pred = img_pred.reshape(1, 1, 28, 28).astype('float32')

img_pred = img_pred/255.0

# predição de classe
pred = model.predict_classes(img_pred)
pred_proba = model.predict_proba(img_pred)
pred_proba = "%.2f%%" % (pred_proba[0][pred]*100)
print(pred[0], " com confiança de ", pred_proba)

 

Em meus testes eu consegui obter uma acurácia de 99.03%, no estado da arte, com um modelo capaz de fazer boas generalizações, gerado com apenas 10 epochs.

 

Convolutional Neural Network - Handwriting recognition

Vídeo – como criar uma rede neural convolucional para reconhecer dígitos escritos à mão

No vídeo abaixo, você poderá acompanhar um tutorial completo sobre como montar uma ConvNet usando o Keras, para reconhecer dígitos escritos à mão. O vídeo começa com uma breve introdução às redes convolucionais, abordando as classes do Keras que são necessária para criar a rede e, finalmente, a criação do algorítmo no Jupyter.

 

Referências

Like