Reconhecimento de escrita manual com Redes Neurais Convolucionais - Luís Fred
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.
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.
Leia também – Machine Learning e a matemática da aprendizagem supervisionada
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
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).
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.
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).
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.
# Como estamos trabalhando em escala de cores cinza podemos
# definir a profundidade com o valor 1.
# 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]
# Converte y_train e y_test, que são vetores de classes, para uma matriz de classe binária (one-hot vectors)
# 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).
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():
# 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.
# A camada MaxPooling2D sera nossa segunda camada onde teremos uma janela de amostragem de tamanho 2 x 2
# Uma nova camada convolucional, com 15 feature maps de tamanho 3 × 3, e função de ativação ReLU
# Uma nova subamostragem com um pooling de dimensoes 2 x 2.
# Incluimos um dropout com probabilidade de 20% (você pode experimentar outros valores)
# 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.
# Camada fully connected com 128 neuronios.
# Seguida de uma nova camada fully connected com 64 neuronios
# Seguida de uma nova camada fully connected com 32 neuronios
# 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"
# Configura todo o processo de treino da rede neural
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()
print("\nacc: %.2f%%" % (scores[1]*100))
O 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
img_pred = img2.reshape(28, 28, -1)
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_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.
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.