No post de hoje, irei mostrar como implementar sua primeira Rede Neural Convolucional (Convolutional Neural Network – CNN), inspirada na conhecida arquitetura neural LeNet-5, e aplicá-la ao dataset MNIST.
Este é um tutorial focado na implementação da CNN – ou seja, estou assumindo que você tem familiaridade com a área. Por esse motivo, não entrarei em detalhes teóricos e conceitos como funções de ativação, Fully-Connected Layer, Pooling Layer, entre outros tantos.
Recomendo àqueles que estão começando ou que querem aprofundar mais nos conceitos, darem uma olhada neste artigo do site, onde eu recomendo os 3 melhores cursos online de deep learning (na minha opinião).
.
Conjunto de dados MNIST
No último post, usamos o muito conhecido conjunto de dados MNIST, que contém milhares de imagens manuscritas dos dígitos de 0-9, e criamos uma rede neural para classificá-las.
Resumidamente, cada imagem do dataset possui 28 X 28 pixels, com os valores dos pixels em escala de cinza. Como grande vantagem, os digitos já estão normalizados (tamanho) e centralizados.
Como em uma imagem em escala de cinza o valor de cada pixel é a única amostra do espaço de cores, esse valor irá variar no intervalo [0, 255], indicando a intensidade desse pixel. Para maiores detalhes sobre o dataset, veja o artigo “Redes Neurais Multicamadas com Python e Keras”.
Redes Neurais Convolucionais são um tipo de redes neurais que vêm sendo aplicadas com muito sucesso à problemas de Computer Vision.
Redes Neurais Convolucionais
Como eu disse na introdução, a nossa primeira implementação de uma CNN vai ser baseada na arquitetura da LeNet-5, primeira CNN implementada e testada com êxito!
A escolha dessa arquitetura (apesar da idade dela) é por que essa CNN é muito didática! Pequena e fácil de entender, ainda sim consegue ótimos resultados em problemas como o MNIST. Implementar a LeNet-5 é como se fosse o Hello, World! das CNN.
Arquitetura LeNet-5
Proposta por LeCun (1998) em seu paper Gradient-Based Learning Applied to Document. Recognition, a LeNet-5 tem foco no reconhecimento de dígitos, e foi pensada em reconhecer os números de CEPs em correspondências.
A Figura 2 é a imagem original do paper de LeCun. Em uma análise rápida, vemos que a imagem passada como input não é achatada (flatten), mas é passada preservando as suas dimensões. Isso é mandatório, para manter a relação espacial entre seus pixels -uma imagem achatada perderia essa informação importante.
Também dá para ver que existem três tipos de layers:
- Convolutional Layers (CONV);
- Pooling Layers (POOL);
- Fully-Connected Layers (FC).
Caso você não esteja acostumado com a representação gráfica usada na Figura 2, gosto muito da representação usada por Andrew Ng em um dos slides do curso Convolutional Neural Networks (Coursera):
Resumidamente, a arquitetura da LeNet-5 é composta por uma sequência com as seguintes camadas:
- CNN é composta por um conjunto de 6 filtros (5×5), stride=1.
- POOL (2×2), stride=2, para reduzir o tamanho espacial das matrizes resultantes.
- CNN (5×5) com 16 filtros e stride=1.
- POOL (2×2), stride=2.
- Os mapas de características são achatados (flatten), formando 400 nós (5x5x16) para a próxima camanda FC.
- FC com 120 nós.
- FC com 84 nós.
Se você pegar para ler o artigo, vai reparar que as funções de ativação referenciadas foram SIGMOID
e TANH
, entretanto eu vou usar a ativação RELU
, que nos dá uma precisão bem melhor! Outro observação, na época que o paper foi escrito, usava-se muito mais o average pooling do que max pooling. No modelo que vamos implementar, vou utilizar o max pooling.
Implementando uma CNN com Python + Keras
Antes de entrar no código propriamente dito, veja como o projeto foi dividio. Para deixar o código organizado – afinal, não tem nada pior que escrever tudo em um único arquivo! -, criei um módulo cnn
contendo a classe LeNet-5. Já a aplicação da CNN ao dataset MNIST está em um arquivo separado lenet5_mnist.py
. Segue a estrutura do projeto:
carlos$ tree . ├── cnn │ └── __init__.py └── lenet5_mnist.py 1 directory, 2 files
Ao estruturar um projeto em módulos e sub-módulos, você permite que estes sejam mais fáceis de escalar, além de adotar uma boa prática de programação e deixar tudo mais legível para os outros.
Escrevendo a classe LeNet5
Dentro do módulo cnn
, crie um arquivo __init__.py
para implementar a classe LeNet5. Importe os pacotes necessários e declare a classe:
""" Contém as implementações de arquiteturas CNN. [LeNet5] - CNN inspirada na arquitetura de LeCun [1], com algumas alterações nas funções de ativação, padding e pooling. [1] http://yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf """ # importar os pacotes necessários from keras.models import Sequential from keras.layers.core import Flatten from keras.layers.core import Dense from keras.layers.convolutional import Conv2D from keras.layers.convolutional import MaxPooling2D from keras.layers.core import Activation class LeNet5(object): """ Arquitetura LeNet5 com pequenas alterações. Com foco no reconhecimento de dígitos, esta CNN é composta por uma sequência contendo os seguintes layers: INPUT => CONV => POOL => CONV => POOL => FC => FC => OUTPUT """
Vou definir o método estático build
(usando o decorador @staticmethod )dentro da classe LeNet5
, assim não será necessário criar uma instância para chamar o método. São fornecidos ao método os argumentos referentes ao tamanho da imagem, quantidade de canais e classes, e esse empilhará os layers da CNN, retornando o modelo.
@staticmethod def build(width, height, channels, classes): """ Constroi uma CNN com arquitetura LeNet5. :param width: Largura em pixel da imagem. :param height: Altura em pixel da imagem. :param channels: Quantidade de canais da imagem. :param classes: Quantidade de classes para o output. :return: Cnn do tipo LeNet5. """ inputShape = (height, width, channels) model = Sequential() model.add(Conv2D(6, (5, 5), padding="same", input_shape=inputShape)) model.add(Activation("relu")) model.add(MaxPooling2D((2,2))) model.add(Conv2D(16, (5, 5))) model.add(Activation("relu")) model.add(MaxPooling2D((2,2))) model.add(Flatten()) model.add(Dense(120)) model.add(Activation("relu")) model.add(Dense(84)) model.add(Activation("relu")) model.add(Dense(classes)) model.add(Activation("softmax")) return model
A CNN é instanciada pela classe Sequential
, e cada layer é adicionado na sequência do outro, seguindo a arquitetura já detalhada acima.
Não esqueça que para executar a Linha 48 é preciso antes transformar o mapa de característica em um vetor com 400 neurônios executando Flatten(). Na Linha 49, o FC com 84 nós ira se conectar a cada um dos 400 nós que “achatamos”.
Na última camada, é adicionada uma camada do tipo FC de tamanho 10, que é exatamente o número de classes do problema. Usamos a função de ativação softmax
pois queremos como output a probabilidade associada a cada classe.
De maneira resumida, a arquitetura da CNN ficou assim:
INPUT => CONV => POOL => CONV => POOL =>FC => FC => OUTPUT
Só isso! A classe LeNet5 está 100% implementada, pronta para ser usada não apenas no MNIST, mas qualquer outro problema de classificação de imagens.
Aplicando a CNN no MNIST
No diretório raíz do seu projeto, crie um arquivo lenet5_mnist.py
, importe as bibliotecas que serão usadas, e não esqueça da recém-criada classe LeNet5
. Exatamente como fizemos no artigo anterior, importe e normalize o dataset MNIST:
""" Treina uma CNN com o dataset MNIST. A CNN é inspirada na arquitetura LeNet-5, com algumas alterações nas funções de ativação, padding e pooling. """ # importar pacotes necessários from keras.utils import to_categorical from keras.optimizers import SGD from keras import backend from sklearn.datasets import fetch_mldata from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report import matplotlib.pyplot as plt from cnn import LeNet5 # ESTA É A CLASSE CRIADA POR NÓS # importar e normalizar o dataset MNIST dataset = fetch_mldata("MNIST Original") labels = dataset.target data = dataset.data.astype("float32") / 255.0
Lembra da Figura 2, que mostra que o input da CNN deve ser uma imagem com largura e altura? Então, quando importamos o dataset diretamente pelo sklearn
, ele reduziu automaticamente as dimensões das imagens, transformando-as em um vetores com 784 valores.
Para a CNN funcionar adequadamente, temos que converter esse vetor em um array do tipo (28x28x1). Isso pode ser feito facilmente pelo método reshape
:
# converter as imagens de 1D para o formato (28x28x1) if backend.image_data_format() == "channels_last": data = data.reshape((data.shape[0], 28, 28, 1)) else: data = data.reshape((data.shape[0], 1, 28, 28))
Um leitor atento vai perceber que tenho duas situações possíveis, minha figura pode ser redimensionada para o shape (28x28x1) ou para (1x28x28). Isso é por que o Keras por usar tanto Theano quanto Tensorflow no backend. Como na comunidade Theano usa-se o ordenamento channels first e na comunidade Tensorflow é adotado channels last, é muito importante fazer essa verificação para garantir a compatibilidade da aplicação.
Para saber o que você está usando no backend, é só dar uma olhada no arquivo de configuração ~/.keras/keras.json
:
carlos$ cat ~/.keras/keras.json { "epsilon": 1e-07, "floatx": "float32", "image_data_format": "channels_last", "backend": "tensorflow" }
Próximo passo é dividir o conjunto de dados entre treino (75%) e teste (25%), usando o método train_test_split
, e transformar os números interior dos labels de trainY
e testY
para o formato de vetor binário, com auxílio do método to_categorical
:
# dividir o dataset entre train (75%) e test (25%) (trainX, testX, trainY, testY) = train_test_split(data, labels) # Transformar labels em vetores binarios trainY = to_categorical(trainY, 10) testY = to_categorical(testY, 10)
Treinando a CNN
Classe LeNet5 implementada, dados de entrada tratados corretamente, e agora é hora de compilar a CNN e treiná-la com os dígitos do MNIST:
# inicializar e otimizar modelo print("[INFO] inicializando e otimizando a CNN...") model = LeNet5.build(28, 28, 1, 10) model.compile(optimizer=SGD(0.01), loss="categorical_crossentropy", metrics=["accuracy"]) # treinar a CNN print("[INFO] treinando a CNN...") H = model.fit(trainX, trainY, batch_size=128, epochs=20, verbose=2, validation_data=(testX, testY))
Na Linha 39, ao passar os argumentos para o método estático LeNet5.build
, a Rede Convolucional LeNet-5 é atribuída à variável model
. Eu compilo o modelo na Linha 40 usando o algoritmo Stochastic Gradient Descent (SGD) para otimização e loss function igual a categorical_crossentropy
, dado que são múltiplas classes no output.
Na Linha 45 é iniciado o treinamento da Rede Neural Convolucional, processo que pode demorar um pouco mais caso você não esteja usando nenhuma GPU.
Avaliando a CNN
Para avaliar o desempenho da nossa CNN, chamamos o método model.predict
para gerar previsões em cima do dataset de teste. O desafio do modelo é fazer a previsão para as 17.500 imagens que compõe o conjunto de teste, atribuindo um label de 0-9 para cada uma delas:
# avaliar a CNN print("[INFO] avaliando a CNN...") predictions = model.predict(testX, batch_size=64) print(classification_report(testY.argmax(axis=1), predictions.argmax(axis=1), target_names=[str(label) for label in range(10)]))
Por fim, após o relatório de desempenho obtido, vamos querer plotar a accuracy e loss ao longo das iterações:
# plotar loss e accuracy para os datasets 'train' e 'test' plt.style.use("ggplot") plt.figure() plt.plot(np.arange(0,20), H.history["loss"], label="train_loss") plt.plot(np.arange(0,20), H.history["val_loss"], label="val_loss") plt.plot(np.arange(0,20), H.history["acc"], label="train_acc") plt.plot(np.arange(0,20), H.history["val_acc"], label="val_acc") plt.title("Training Loss and Accuracy") plt.xlabel("Epoch #") plt.ylabel("Loss/Accuracy") plt.legend() plt.savefig('cnn.png', bbox_inches='tight')
Executando a CNN LeNet5 MNIST
Eu rodei o script em uma instância p2.xlarge da AWS, e demorou cerca de 1 minuto para que a CNN fosse treinada. Essa instância P2 utiliza 1 GPU NVIDIA K80 e 4 vCPUs, ao custo de $0,90/hora. No entanto, o nosso código roda normalmente em uma máquina sem GPU, pois é uma rede pequena processando um dataset também pequeno.
Para um comparativo, com GPU da AWS e epochs=20
demorou cerca de 60 segundos para treinar a LeNet-5. Já minha máquina sem GPU (apenas CPU), demorou 360 segundos.
Vá em frente e execute python lenet5_mnist.py :
(deeplearning) ubuntu@ip-xxx-xx-xx-xxx:~/sigmoidal/cnn_lenet5$ python lenet5_mnist.py Using TensorFlow backend. [INFO] inicializando e otimizando a CNN... [INFO] treinando a CNN... Train on 52500 samples, validate on 17500 samples Epoch 1/20 9s - loss: 1.3031 - acc: 0.5689 - val_loss: 0.8836 - val_acc: 0.7410 Epoch 2/20 3s - loss: 0.3267 - acc: 0.8990 - val_loss: 0.2708 - val_acc: 0.9147 ... ... 3s - loss: 0.0499 - acc: 0.9846 - val_loss: 0.0584 - val_acc: 0.9814 [INFO] avaliando a CNN... precision recall f1-score support 0 0.99 0.99 0.99 1714 1 0.99 0.99 0.99 1958 2 0.98 0.98 0.98 1724 3 0.98 0.98 0.98 1801 4 0.98 0.98 0.98 1703 5 0.99 0.97 0.98 1564 6 0.99 0.99 0.99 1732 7 0.99 0.97 0.98 1794 8 0.97 0.98 0.97 1724 9 0.96 0.98 0.97 1786 avg / total 0.98 0.98 0.98 17500
Depois de todo esse trabalho, vamos dar uma olhada no desempenho da nossa primeira CNN e comparar com a rede neural simples que implementamos no post anterior.
Conseguimos uma precisão de 98% nas previsões feitas com a LeNet-5 treinada. Mesmo sendo uma arquitetura antiga (a primeira CNN implementada com sucesso) e sem fazer grandes alterações, ela bateu facilmente o desempenho da outra rede neural simples – que tinha conseguido 92% de precisão.
Por fim, o script forneceu o plot da accuracy e loss em função da epoch. Quando a gente treina um modelo de Deep Learning, o gráfico que se espera é basicamente desse tipo, training e validation loss com curvas bem similares, assim como ambas accuracy com comportamento similar – um padrão que indica que não há overfitting.
No entanto, esse tipo de gráfico é bem difícil de ser alcançado em problema mais complexos. O dataset MNIST foi amplamente pré-processado e normalizado – e a gente sabe que não é isso que nos espera no mundo real – e por isso gera um gráfico tão bonito assim. Ou seja, o pré-processamento do seu conjunto de dados é extremamente importante para o desempenho do algoritmo.
Resumo
A LeNet-5 é tida como uma rede shallow (rasa) quando comparada com as arquiteturas deep (profundas) modernas. Como vimos, ela possui apenas 4 camadas (2 CONV e 2 POOL), pouco para os padrões atuais. Hoje em dia, uma arquitetura classificada como estado-da-arte pode ultrapassar facilmente 100 camadas (como a ResNet).
Entretanto, vimos que mesmo essa estrutura simples de CNN foi capaz de atingir 98% de acurácia no dataset MNIST, tornando-a um ótimo exemplo inicial para implementação.
Espero que tenha gostado do artigo, e conseguido entender melhor a estrutura da clássica LeNet-5. No próximo post irei fazer um overview sobre os principais layers de uma rede convolucional.
Muito bom o artigo, rico de informações e com os códigos usados
Muito obrigado pelo feedback! Espero que aproveite bem o conteúdo do site. Abraços!