Introdução a classificadores Binários usando Keras

Keras é uma API de redes neurais em Python, capaz de rodar em cima das bibliotecas de tensores TensorFlow, CNTK ou Theano. Ela provê uma estrutura que permite compilar redes neurais combinando camadas de diferentes dimensões e funções de ativação, tornando o ciclo de desenvolvimento de novos modelos de aprendizado de máquina muito mais rápido.

Para instalar Keras na sua máquina, siga as instruções no link: https://keras.io/#installation

No exemplo de hoje, desenvolveremos um classificador binário que permitirá calcular a probabilidade de um paciente ter diabetes ou não.

Para isso, utilizaremos o dataset PIMA Indians, um estudo feito com índios da tribo PIMA dos Estados Unidos. Devido a mudanças dietárias os Pimas desenvolveram um alto índice de prevalência de diabetes tipo 2, sendo alvos de diversos estudos. Nosso dataset contém dados de 15000 índios e contém as seguintes características:

  • PatientId — Id do paciente
  • Pregnancies — Número de gravidezes
  • PlasmaGlucose — Concentração de Glucose no plasma após teste oral
  • DiastolicBloodPressure — Pressao Diastólica em (mm Hg)
  • TricepsThickness — Largura da pele do tríceps (mm)
  • SerumInsulin — Concentração de Insulina (mu U/ml)
  • BMI — Índice de massa corpórea (peso em kg/(altura em m)²)
  • DiabetesPedigree — Número que indica a prevalência de diabetes em antepassados
  • Age — Idade
  • Outcome — É diabético (sim / nao)

Os dados se encontram em formato .csv e podem ser obtidos no site do Kaggle: https://www.kaggle.com/uciml/pima-indians-diabetes-database

O ciclo de treino

Podemos visualizar o ciclo de treino de uma rede neural seguindo os passos abaixo:

Preparando os dados

Seleção de características

Nosso primeiro passo será carregar os dados e prepará-los para o consumo. Necessitamos selecionar quais características são relevantes para um algoritmo de aprendizado e excluir as características que não são.
No nosso caso, a característica PatientId claramente não tem correlação com o diagnóstico de diabetes, portanto ele pode ser excluído.

Normalização

A seguir, necessitamos preparar os dados para consumo. Na maioria dos casos, a inserção de dados sem pré-processamento resultará em resultados imprecisos. No caso de redes neurais é importante que as características
estejam em um range numérico padronizado, por exemplo, entre 0 a 1. Isto possibilita que o algoritmo convirja mais rápido ao resultado.
Existem diferentes tipos de normalização. No nosso caso, faremos com que todas as características tenham média 0, aplicando a seguinte formula para cada item:

Separação entre dados de treino e validação

Os dados devem ser separados em duas partes, treino e validação. Dados de treinos serão inseridos no algoritmo de classificação, e dados de teste serão usados para validar a performance de nosso algoritmo.

Abaixo apresentamos duas funções, que possibilitam a normalização e carregamento dos dados de treino e teste, normalize e load_data respectivamente:

Nosso próximo passo, será montar nossa rede neural de classificação.

A Rede Sequencial

O modelo sequencial permite inserir camadas em série, onde o output da primeira camada serve como input da segunda, e assim por diante.

Para instanciar o modelo sequencial:

from keras.models import Sequential
from keras.layers import Dense
model = Sequential()

Em seguida, adicionaremos a camada de entrada de nossa rede. Keras prove diversos tipos de camadas, porém a mais utilizada é a do tipo Dense. Ela tem como objetivo calcular uma função de ativação em conjunto com nossos dados de entrada e pesos. Formalmente, ela computa a seguinte formula:

output = activation(dot(input, kernel) + bias)

Nossa camada de entrada receberá uma matrix de dimensão 8, que são as colunas de features selecionadas em nosso dataset. A camada terá 16 neurônios hidden. O número de neurônios afeta como a rede neural interpreta nossos dados. Mais neurônios permitem mais liberdade em aprender dados mais complexos, porém torna a rede mais cara computacionalmente para ser treinada.

model.add(Dense(16, activation=’relu’, input_dim=8))

Também devemos dizer qual função de ativação será computada. No nosso caso usaremos a função “relu”, Rectified Linear Unit, que é dada pela formula:

A função de ativação decide se um neurônio da camada é disparado ou não.

Graficamente:

Para podermos criar previsões não lineares, somente uma camada não é suficiente. Adicionaremos uma camada extra semelhante a anterior. A presença de duas camadas possibilita redes neurais aprender qualquer função.

model.add(Dense(16, activation='relu'))

Para finalizar, necessitamos de uma camada de saída. A camada de saída deve ter como dimensão de output o número de classes que queremos reconhecer. No nosso caso de classificação binária, temos somente 1 dimensão de saída, true ou false para diabetes.

Para classificadores binários, estamos interessados na probabilidade de nosso dado pertencer a uma classe ou outra. Para isso mudaremos a função de ativação da camada de saída para “sigmoid”.

model.add(Dense(units=1, activation='sigmoid'))

A função sigmoid é dada pela formula:

Ou graficamente:

Para visualizar a estrutura atual de nossa rede, podemos utilizar o seguinte método

model.summary()

Ele imprimirá uma descrição de nossa arquitetura:

Layer (type)                 Output Shape              Param #
=================================================================
dense_1 (Dense) (None, 16) 144
_________________________________________________________________
dense_2 (Dense) (None, 16) 272
_________________________________________________________________
dense_3 (Dense) (None, 1) 17
=================================================================
Total params: 433
Trainable params: 433
Non-trainable params: 0

Compilacao

A compilacao serve para validar e finalizar a estrutura de nossa rede neural. Ela recebe 3 parametros:

  • optimizer: Função que define como os pesos da rede neural são atualizados. RMSProp é o mais utilizado em Keras, porém esteja livre para experimentar com outros algoritmos e decidir qual é o ideal para seu caso.
  • loss: Função de calcula a diferença entre os dados de teste e os dados de validação. Para classificadores binários, usaremos a “binary_crossentropy”.
  • metrics: métricas que devem ser guardadas para avaliação.
model.compile(optimizer=’rmsprop’, loss=’binary_crossentropy’,
metrics=[‘accuracy’])

Treino e Validação

Agora que temos nossa rede neural montada, iremos treiná-la em nosso dataset.
Necessitamos escolher um numero de épocas de treino para nossa rede. Se escolhermos um numero muito baixo de épocas, nosso algoritmo não convirjará para um mínimo global. Se escolhermos um numero muito grande, correremos o risco de nosso algoritmo se acostumar demais aos dados de teste, e terá má performance em dados reais

Usaremos uma parcela dos dados para validar nosso algoritmo, isto é, verificar se ele retorna previsões corretas de diabetes. Esses são os dados separados previamente em x_train e y_train

history = model.fit(x_train,y_train, epochs=10, batch_size=128, validation_data=(x_test, y_test))

A função fit retorna um histórico de métricas. Com isso poderemos verificar
a precisão de nosso algoritmo.

Para observar a precisão histórica de nosso algoritmo:

import matplotlib.pyplot as plt
epochs = range(1, len(history.history['acc']) + 1)
plt.plot(epochs, history.history['acc'], 'bo', label='Training acc')
plt.plot(epochs, history.history['val_acc'], 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

Precisão indica o qual próximo as classes previstas se aproximam da verdade. Podemos ver que ao longo do tempo, quanto mais épocas de treino se passam, mais preciso nosso algoritmo se torna.

Podemos fazer o mesmo com a perda, que indica o qual distante as classes previstas são da verdade. Ela pode ser considerada o inverso da precisão:

epochs = range(1, len(history.history['loss']) + 1)
plt.plot(epochs, history.history['loss'], 'bo', label='Training loss')
plt.plot(epochs, history.history['val_loss'], 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

Resultado

Após o treino, obtemos o sumário abaixo:

GeForce GTX 1060 6GB, pci bus id: 0000:01:00.0, compute capability: 6.1)
7500/7500 [==============================] - 1s 76us/step - loss: 0.5175 - acc: 0.7524 - val_loss: 0.4702 - val_acc: 0.7724
Epoch 2/10
7500/7500 [==============================] - 0s 18us/step - loss: 0.4480 - acc: 0.7847 - val_loss: 0.4358 - val_acc: 0.7883
Epoch 3/10
7500/7500 [==============================] - 0s 16us/step - loss: 0.4267 - acc: 0.7924 - val_loss: 0.4260 - val_acc: 0.7903
Epoch 4/10
7500/7500 [==============================] - 0s 16us/step - loss: 0.4173 - acc: 0.8000 - val_loss: 0.4181 - val_acc: 0.7985
Epoch 5/10
7500/7500 [==============================] - 0s 15us/step - loss: 0.4096 - acc: 0.8028 - val_loss: 0.4111 - val_acc: 0.7996
Epoch 6/10
7500/7500 [==============================] - 0s 15us/step - loss: 0.4020 - acc: 0.8055 - val_loss: 0.4028 - val_acc: 0.8047
Epoch 7/10
7500/7500 [==============================] - 0s 16us/step - loss: 0.3936 - acc: 0.8111 - val_loss: 0.3939 - val_acc: 0.8088
Epoch 8/10
7500/7500 [==============================] - 0s 19us/step - loss: 0.3853 - acc: 0.8153 - val_loss: 0.3849 - val_acc: 0.8143
Epoch 9/10
7500/7500 [==============================] - 0s 18us/step - loss: 0.3764 - acc: 0.8204 - val_loss: 0.3760 - val_acc: 0.8155
Epoch 10/10
7500/7500 [==============================] - 0s 16us/step - loss: 0.3680 - acc: 0.8245 - val_loss: 0.3668 - val_acc: 0.8231

Isto indica que nosso algorítimo obteve uma precisão de 82.45% . Melhor do que um lançar de moeda, não ?

O código completo deste exemplo pode ser encontrado no github: https://github.com/victorglt/pima_indians_neural_net

No nosso próximo artigo, veremos como utilizar uma rede neural previamente salva para gerar previsões e criaremos um webservice de previsões.