Deep Learning com Keras e TensorFlow - Part 1

Entre o dias 06 e 08 de maio de 2019 participamos da QCon SP. Um dos maiores eventos de tecnologia do Brasil, com maior público senior e mais de 80 palestras técnicas. Nele apresentamos a palestra Deep Learning com Keras e TensorFlow, em breve disponível na InfoQ e descrita neste post.

Nós trabalhamos na NeuralMed, uma startup que usa Inteligência Artificial para auxiliar no diagnóstico de imagens médicas. Portanto, usar Deep Learning faz parte do nosso dia-a-dia, e é a base desse conhecimento que gostaríamos de compartilhar. Os tópicos principais que cobriremos são:

  • Como construir uma Convolutional Neural Network (CNN) usando Keras + Tensorflow

  • Como e porque usar TransferLearning

  • Alguns pontos de atenção e desafios encontrados

Por trabalharmos com imagens médicas esse será nosso caso de uso, mais especificamente imagens de raio-x de tórax. Porém, todas as redes construídas podem ser aplicadas em outros tipos de imagens, como classificação de gatos e cachorros, por exemplo.

Exemplo de raio-x de tórax

Por que Tensorflow + Keras?

Um artigo recente publicado no medium fez uma comparação entre as principais bibliotecas para Deep Learning e mostrou que Tensorflow é o que mais cresce e o que apresenta a maior demanda.

enter image description here

Resultado do estudo realizado por Jeff Hale

E porque então usar Keras? Keras roda em cima do Tensorflow e oferece uma linguagem muito mais amigável, fácil e rápida para escrever os modelos (já mencionamos que é muito mais fácil?)

O Tensorflow anunciou recentemente sua versão 2.0, que usa Keras como sua API de alto nível já integrada dentro do tf.keras. Portanto, continuaremos usando o Keras como framework para Deep Learning. (Ainda mais com esse novo mascote!)

Keras mascote: Prof Keras

Convolutional Neural Network

CNN é uma categoria de redes de deep learning normalmente aplicadas para reconhecimento de imagens.

Como elas funcionam?

São baseadas principalmente em Filtros Convolucionais que extraem características de imagens. Esses filtros já são usados há muito tempos na área de Processamento de Imagens, a ideia é que ao aplicar essas matrizes sobre a imagem se consiga obter as características desejadas. O gif abaixo mostra os principais filtros (ou kernels) para extração de bordas.

Fonte: Gentle Dive into Math Behind Convolutional Neural Networks

Nas redes convolucionais esses filtros são aplicados em várias camadas. Além disso, os valores dos filtros são aprendidos, portanto a própria rede aprende quais características são relevantes para o problema em questão.

Fonte: CS231n Convolutional Neural Networks for Visual Recognition

Vamos a prática!

Construindo uma CNN pra predizer lateralidade do raio-X

Nosso desafio é criar uma rede para aprender se um raio X é lateral ou de frente. Esse é um dos poucos desafios da área médica que nós, leigos, conseguimos validar visualmente sem o auxílio de um médico:

Para facilitar vamos dividir a criação da rede em 5 etapas:

  1. Input de dados

  2. Definição da arquitetura

  3. Compilação do Modelo

  4. Treinamento

  5. Validação

Antes de ver cada uma delas, vamos fazer todos os imports necessários para o funcionamento do código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import warnings
warnings.filterwarnings('ignore')

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

import time
import os
import datetime

from keras import datasets, Model
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.layers import Flatten
from keras.layers.core import Dropout
from keras.layers.core import Dense
from keras.optimizers import Adam
from keras import Sequential
from keras.utils import to_categorical
from keras.callbacks import ModelCheckpoint, EarlyStopping
from keras.applications import VGG16
from keras.preprocessing.image import ImageDataGenerator
from keras.models import load_model
from keras import backend
import tensorflow as tf

from keras.preprocessing.image import load_img, img_to_array

1. Input de Dados

Existem várias formas de inputar os dados para treinamento, vamos usar o Image Data Generator para ler as imagens a partir do disco.

O primeiro passo é instanciar o generator, nesta etapa existem vários parâmetros possíveis tanto para definir o formato da imagem que será lida, quanto para aplicar transformações fazendo image augmentor, todos os parâmetros podem ser consultados aqui. Neste exemplo definimos apenas que a imagem precisa ser normalizada (dividindo por 255) e que 30% dos dados serão separados para validação:

1
2
3
# aqui definimos as transformações que serão aplicadas na imagem e a % de dados 
# que serão usados para validação
data_generator = ImageDataGenerator(rescale=1./255, validation_split=0.30)

A próxima etapa é passar quais dados serão lidos. Aqui também há diferentes métodos possíveis, os mais comuns são flow_from_dataframe que lê as imagens de acordo com os caminhos específicados em um dataframe, e flow_from_directory, que lê os arquivos direto de uma pasta, cada classe deve estar em uma pasta separada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# para criar os generators precisamos definir o path da pasta raiz com as imagens e o tamanho da BATCH SIZE
path = 'images-chest-orientation/train/'
# dentro da pasta train, há uma pasta frente e outra lateral com suas respectivas imagens

BATCH_SIZE = 50

train_generator = data_generator.flow_from_directory(path, shuffle=True, seed=13,
class_mode='categorical', batch_size=BATCH_SIZE, subset="training")

validation_generator = data_generator.flow_from_directory(path, shuffle=True, seed=13,
class_mode='categorical', batch_size=BATCH_SIZE, subset="validation")

Também definimos nesta etapa o BATCH_SIZE, ou seja, quantas imagens serão lidas por bloco, se os dados serão lidos de forma aleatória (shuffle=True) e o seed. Outros parâmetros podem ser vistos na documentação do keras.

2. Definição da arquitetura

O método abaixo define a arquitetura da rede:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
model = Sequential()

# primeira camada adiciona o shape do input
# também é possível alterar a inicializacao, bias, entre outros -- https://keras.io/layers/convolutional/#conv2d
model.add(Conv2D(filters=64, kernel_size=2, activation='relu', input_shape=(256,256)))
#Tamanho do downsampling
model.add(MaxPooling2D(pool_size=2))
# Fracao das unidades que serao zeradas
model.add(Dropout(0.3))

# Segunda camada
model.add(Conv2D(filters=128, kernel_size=2, activation='relu'))
model.add(MaxPooling2D(pool_size=2))
model.add(Dropout(0.3))

# Da um reshape no output transformando em array
model.add(Flatten())

# Camada full-connected 
model.add(Dense(256, activation='relu'))

#Camada de saida com o resultado das classes
model.add(Dense(2, activation='sigmoid'))

Os modelos do keras podem ser Functional API ou Sequential. Quando estamos definindo a nossa rede usamos o Sequential para definirmos as nossas camadas de forma sequencial.

1
 model = Sequential()

A primeira camada adicionada neste exemplo é uma convolucional com 64 filtros e dimensão de 2x2, com função de ativação relu. Essa é a função de ativação tradicionalmente utilizada nas camadas intermediárias, ela ativa os neurônios que tiveram resultados maiores que 0, para outras funções disponíveis pelo Keras veja aqui. Também é na primeira camada que definimos qual o formato de entrada da rede, no caso passaremos imagens de 256x256:

1
 model.add(Conv2D(filters=64, kernel_size=2, activation='relu', input_shape=(256,256)))

Em seguida adicionamos uma camada de MaxPooling, uma camada que realiza o downsampling calculando o valor máximo de cada pool como mostrado na figura, para essa camada precisamos apenas definir o tamanho do pool.

1
 model.add(MaxPooling2D(pool_size=2))

enter image description here Exemplo de funcionamento do MaxPooling, Fonte: Google Developers: ML Practicum: Image Classification

Depois adicionamos uma camada de Dropout, uma das técnicas atualmente mais utilizadas para evitar overfitting. Ele aleatoriamente desativa uma porcentagem de neurônios durante cada época de treinamento. Precisamos apenas definir a porcentagem que queremos desativar.

1
 model.add(Dropout(0.3))

Cuidado para não exagerar na quantidade de dropouts nem na porcentagem ou acabará gerando underfitting

Após as camadas convolucionais precisamos redimensionar as features para 1 dimensão. Aqui faremos isso utilizando uma Flatten.

1
 model.add(Flatten())

Depois disso adicionamos uma camada Densa com 256 neurônios e por fim a câmada de saída com 2 neurônios (um para cada classe), e a função de ativação sigmóide, que retorna a probabilidade da instância ser daquela classe.

1
2
model.add(Dense(256, activation='relu'))   
model.add(Dense(2, activation='sigmoid'))

Essa foi a arquitetura que nós definimos para o modelo em questão, as camadas usadas e seus respectivos tamanhos são totalmente parametrizáveis, recomendamos testar o modelo com diferentes arquiteturas para analisar os resultados.

3. Compilação

Precisamos definir como a rede irá aprender, isto é, qual a função de loss e o otimizador.

1
2
3
4
# Compila o modelo definindo: otimizador, metrica e loss function
model.compile(loss='binary_crossentropy',
optimizer='adam',
metrics=['accuracy'])

Para este exemplo utilizamos o otimizador Adam, com os valores padrões (lr=0.001, beta_1=0.9, beta_2=0.999), você pode aprender mais sobre ele aqui.

Para o cálculo do loss usamos a função padrão para classificação binária: binary_crossentropy.

4. Treinamento

Como lemos os dados usando um generator, o fit do keras também será usando um fit_generator.

Também usaremos alguns callbacks:

ModelCheckPoint para salvar o modelo que tiver o melhor loss durante o treinamento e,

EarlyStop para interromper o treinamento caso a rede pare de aprender.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
checkpoint = ModelCheckpoint('chest_orientation_model.hdf5', 
monitor='val_loss', 
verbose=1, mode='min', 
save_best_only=True)

early_stop = EarlyStopping(monitor='val_loss',
min_delta=0.001,
patience=5,
mode='min',
verbose=1)

Definidos os callbacks, vamos ao treinamento em si:

1
2
3
4
5
6
7
model.fit_generator(generator=train_generator,
steps_per_epoch = train_generator.samples//BATCH_SIZE,
validation_data=validation_generator,
validation_steps=validation_generator.samples//BATCH_SIZE,
epochs= 50,
callbacks=[checkpoint, early_stop]
)

Como faremos a leitura de dados com um generator, utilizaremos o fit_generator para o treinamento. Como é possível ver no código a definição dos parâmetros é bem simples: passamos os generators, a quantidade de passos que será preciso no treinamento e na validação por época, isto é, quantos batchs são precisos para terminar de ler todos os dados, a quantidade máxima de épocas e os callbacks já criados.

O resultado do treinamento foi o seguinte:

saida

Como podemos ver, apesar de termos colocado como máximo 50 épocas, o modelo parou na 10a época devido ao early_stop. Conseguimos ver que ele parou mesmo de aprender na época 5, onde atingiu 0.9853 de acurácia na validação.

5. Avaliação:

Sempre importante separar uma quantidade de dados para testar o modelo no final. Aqui faremos apenas um teste visual para efeito de demonstração

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Carregando imagens de teste
import glob

test_set = glob.glob('images-chest-orientation/test/**/*.jpg')

# temos que fazer o load do model que teve o melhor loss
model = load_model('chest_orientation_model.hdf5')

image_test = np.array([img_to_array(load_img(image_name, target_size=(256, 256), color_mode='rgb'))/255 for image_name in test_set])

y_pred = model.predict(image_test)

Precisamos carregar o modelo salvo. Lembre-se que o modelo que treinou até a última época e estiver em memória não é necessariamente o melhor, o melhor foi salvo pelo ModelCheckpoint.

1
 model = load_model('chest_orientation_model.hdf5')

Depois carregamos as imagens de teste em memória usando os métodos do próprio keras:

1
image_test = np.array([img_to_array(load_img(image_name, target_size=(256, 256), color_mode='rgb'))/255 for image_name in test_set])

E, por fim, fazemos a predição para estas imagens:

1
 y_pred = model.predict(image_test)

Com as predições e os valores reais podemos calcular as métricas necessárias para validar nosso modelo. Geralmente utilizamos uma matriz de confusão ou calculamos métricas como precisão e sensibilidade, que são mais ou menos importantes de acordo com o problema. Porém, a fim de ilustrar melhor os resultados, iremos apenas visualizar os resultados para cada imagem.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
y_true = [0,0,0,0,0,1,1,1,1,1]
labels = ['Frente', 'Lateral']
figure = plt.figure(figsize=(20, 8))
for i in range(10):
ax = figure.add_subplot(3, 5, i + 1, xticks=[], yticks=[])
# Display each image
im = plt.imread(test_set[i])
ax.imshow(im)
predict_index = np.argmax(y_pred[i])
true_index = y_true[i]
# Set the title for each image
ax.set_title("{} ({})".format(labels[predict_index], 
labels[true_index]),
color=("green" if predict_index == true_index else "red"))

drawing

Como podemos perceber, o modelo acerta bem quase todas as imagens separadas para teste, errando apenas a terceira imagem, provavelmente por estar de cabeça para baixo. Se quisemos corrigir esse tipo de erro poderíamos usar o image augmentor e gerar mais algumas imagens em diferentes posições.