Introdução

Nesse tutorial nós vamos aprender a construir um Multi-Layer Perceptron (MLP). Todo o conhecimento reproduzido aqui foi baseado nesse tutorial. Como sabemos, aprendizado de máquina é um subcampo da Ciência da Computação e Deep Learning pode ser enxergado como um subcampo de aprendizado de máquina baseado em um conjunto de algoritmos que foram inspirados na estrutura e funcionamento do cérebro humano, chamado frequentemente de Redes Neurais Artificiais (RNA).

Nós vamos pular algumas partes do tutorial original sobre os demais pacotes de Deep Learning e as diferenças entre kerasR e keras. Para mais detalhes, visite o tutorial.

Tópicos

O tutorial abordará os seguintes tópicos:

  • Como explorar e pré-processar os dados que você carregar do arquivo CSV: Normalizar e dividir em dados de treino e teste.

  • Construção do modelo de Deep Learning propriamente dito, Multi-Layer Perceptron (MLP) para classificação de várias classes.

  • Como compilar e ajustar o modelo para os seus dados, além de visualizar o histórico de treinamento.

  • Predizer os valores alvo baseado nos dados de teste.

  • Por fim, evoluir e refinar o seu modelo, interpretando os resultados a fim de melhorar a performance. Para isso adicionaremos camadas (ocultas ou não) e veremos como ajustar os parâmetros de otimização para obter resultados melhores.

  • Como salvar e carregar o modelo.

Inicializando o ambiente e carregando bibliotecas

Vamos primeiro instalar e carregar o keras com os seguintes comandos abaixo:

#devtools::install_github("rstudio/keras")
#install.packages("keras")
library(keras)

#install.packages("tensorflow")
library(tensorflow)
#install_tensorflow()

Carregando os dados

Vamos utilizar os dados do Iris, do repositório UCI de Machine Learning.

iris <- read.csv(url("http://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data"), header = FALSE)

Antes de tudo é preciso normalizar os dados.

# Build your own `normalize()` function
normalize <- function(x) {
  num <- x - min(x)
  denom <- max(x) - min(x)
  return (num/denom)
}

# Normalize the `iris` data
iris_norm <- as.data.frame(lapply(iris[1:4], normalize))

iris[,5] <- as.numeric(iris[,5]) -1

# Turn `iris` into a matrix
iris <- as.matrix(iris)

# Set `iris` `dimnames` to `NULL`
dimnames(iris) <- NULL

# Normalize the `iris` data
iris[,1:4] <- normalize(iris[,1:4])

Pré-processamento dos dados

Antes de tudo, vamos setar uma semente para que os resultados possam ser reproduzidos novamentes. É o que chamamos de pseudoaleatoriedade. Vamos também determinar o tamanho da amostra:

set.seed(1250)

# Determine sample size
ind <- sample(2, nrow(iris), replace=TRUE, prob=c(0.67, 0.33))

# Split the `iris` data
iris.training <- iris[ind==1, 1:4]
iris.test <- iris[ind==2, 1:4]

# Split the class attribute
iris.trainingtarget <- iris[ind==1, 5]
iris.testtarget <- iris[ind==2, 5]

One-hot encoding

Quando se trabalha com modelos que envolvem problemas de classificação multiclasse utilizando redes neurais, geralmente é uma boa prática transformar o seu atributo alvo de um vetor que contenha valores para cada valor da classe em uma matriz de booleanos para cada valor da classe e se uma dada instância tem aquele valor de classe ou não. O keras tem um pacote que faz tudo isso para você. Utilizando o to_categorical(), o retorno é exatamente essa matriz.

# One hot encode training target values
iris.trainLabels <- to_categorical(iris.trainingtarget)

# One hot encode test target values
iris.testLabels <- to_categorical(iris.testtarget)

# Print out the iris.testLabels to double check the result
print(iris.testLabels)
##       [,1] [,2] [,3]
##  [1,]    1    0    0
##  [2,]    1    0    0
##  [3,]    1    0    0
##  [4,]    1    0    0
##  [5,]    1    0    0
##  [6,]    1    0    0
##  [7,]    1    0    0
##  [8,]    1    0    0
##  [9,]    1    0    0
## [10,]    1    0    0
## [11,]    1    0    0
## [12,]    1    0    0
## [13,]    1    0    0
## [14,]    1    0    0
## [15,]    1    0    0
## [16,]    1    0    0
## [17,]    1    0    0
## [18,]    1    0    0
## [19,]    1    0    0
## [20,]    1    0    0
## [21,]    0    1    0
## [22,]    0    1    0
## [23,]    0    1    0
## [24,]    0    1    0
## [25,]    0    1    0
## [26,]    0    1    0
## [27,]    0    1    0
## [28,]    0    1    0
## [29,]    0    1    0
## [30,]    0    1    0
## [31,]    0    1    0
## [32,]    0    1    0
## [33,]    0    0    1
## [34,]    0    0    1
## [35,]    0    0    1
## [36,]    0    0    1
## [37,]    0    0    1
## [38,]    0    0    1
## [39,]    0    0    1
## [40,]    0    0    1
## [41,]    0    0    1

Podemos ver acima a matriz gerada dos booleanos.

Construindo o modelo

Utilizando a função keras_model_sequential(), inicializamos o modelo. Vamos utilizar a camada de ativação relu para ganhar um pouco de familiaridade com redes neurais, por se tratar de uma camada relativamente simples. Na camada de saída, o softmax foi escolhido a fim de garantir que os valores de saída gerados sejam entre 0 e 1 possibilitando que estes sejam usados como probabilidades.

model <- keras_model_sequential() %>%
    layer_dense(units = 8, activation = 'relu', input_shape = c(4)) %>% 
    layer_dense(units = 3, activation = 'softmax')

Observações: A camada de saída cria 3 valores de saída, um para cada classe de Iris. A primeira camada tem um atributo, input_shape igual a 4 que é o numero de colunas existentes na matriz dos dados de treino.

Por fim, podemos observar mais detalhes do modelo fazendo consultas aos seus atributos:

# Print a summary of a model
summary(model)
## ___________________________________________________________________________
## Layer (type)                     Output Shape                  Param #     
## ===========================================================================
## dense_1 (Dense)                  (None, 8)                     40          
## ___________________________________________________________________________
## dense_2 (Dense)                  (None, 3)                     27          
## ===========================================================================
## Total params: 67
## Trainable params: 67
## Non-trainable params: 0
## ___________________________________________________________________________
# Get model configuration
get_config(model)
## [{'class_name': 'Dense', 'config': {'kernel_initializer': {'class_name': 'VarianceScaling', 'config': {'distribution': 'uniform', 'scale': 1.0, 'seed': None, 'mode': 'fan_avg'}}, 'name': 'dense_1', 'kernel_constraint': None, 'bias_regularizer': None, 'bias_constraint': None, 'dtype': 'float32', 'activation': 'relu', 'trainable': True, 'kernel_regularizer': None, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'units': 8, 'batch_input_shape': (None, 4), 'use_bias': True, 'activity_regularizer': None}}, {'class_name': 'Dense', 'config': {'kernel_initializer': {'class_name': 'VarianceScaling', 'config': {'distribution': 'uniform', 'scale': 1.0, 'seed': None, 'mode': 'fan_avg'}}, 'name': 'dense_2', 'kernel_constraint': None, 'bias_regularizer': None, 'bias_constraint': None, 'activation': 'softmax', 'trainable': True, 'kernel_regularizer': None, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'units': 3, 'use_bias': True, 'activity_regularizer': None}}]
# Get layer configuration
get_layer(model, index = 1)
## <keras.layers.core.Dense>
# List the model's layers
model$layers
## [[1]]
## <keras.layers.core.Dense>
## 
## [[2]]
## <keras.layers.core.Dense>
# List the input tensors
model$inputs
## [[1]]
## Tensor("dense_1_input:0", shape=(?, 4), dtype=float32)
# List the output tensors
model$outputs
## [[1]]
## Tensor("dense_2/Softmax:0", shape=(?, 3), dtype=float32)

Compilando e ajustando o modelo

Agora que a arquitetura do modelo já foi inicializada, é hora de compilar e ajustar o modelo. Para fazer isso, vamos configurar o modelo utilizando o otimizador adam e a função de perda categorical_crossentropy. Além disso, vamos monitorar a acurácia durante o treino utilizando a métrica accuracy para os argumentos de métricas.

model %>% compile(
     loss = 'categorical_crossentropy',
     optimizer = 'adam',
     metrics = 'accuracy'
 )

Dependendo do algoritmo que será utilizado é interessante tunar determinados parâmetros. A escolha da função de perda depende de qual tarefa se quer executar.

Agora, vamos ajustar o modelo de acordo com os dados:

model %>% fit(
     iris.training, 
     iris.trainLabels, 
     epochs = 200, 
     batch_size = 5, 
     validation_split = 0.2
   )

Visualizar o histórico de treinamento de modelos

É possível visualizar o histórico de treinamento do modelo, a parte de ajuste dos dados. Pode ser feito da seguinte forma:

# Store the fitting history in `history` 
history <- model %>% fit(
     iris.training, 
     iris.trainLabels, 
     epochs = 200,
     batch_size = 5, 
     validation_split = 0.2
 )

# Plot the history
plot(history)

Uma outra coisa boa é poder visualizar os parâmetros de perda e acurácia graficamente, indicadas pelos atributos loss e acc, para os dados de treino e val_loss e val_acc para os dados de teste/validação.

Vamos visualizá-los separadamente a fim de melhor compreensão:

Primeiramente, visualizaremos o gráfico de perdas.

# Plot the model loss of the training data
plot(history$metrics$loss, main="Model Loss", xlab = "epoch", ylab="loss", col="blue", type="l")

# Plot the model loss of the test data
lines(history$metrics$val_loss, col="green")

# Add legend
legend("topright", c("train","test"), col=c("blue", "green"), lty=c(1,1))

Em seguida, visualizaremos o gráfico de acurácia:

# Plot the accuracy of the training data 
plot(history$metrics$acc, main="Model Accuracy", xlab = "epoch", ylab="accuracy", col="blue", type="l")

# Plot the accuracy of the validation data
lines(history$metrics$val_acc, col="green")

# Add Legend
legend("bottomright", c("train","test"), col=c("blue", "green"), lty=c(1,1))

Algumas ressalvas que devem ser feitas:

  • Se a acurácia dos de dados de treino continuam a melhorar enquanto a dos dados de validação pioram, isso é um sinal de overfitting, o modelo começa a se super ajustar aos dados.

  • Se a tendência para a acurácia em ambos os datasets ainda está aumentando para os últimos períodos, é possível perceber que o modelo ainda não aprendeu tudo o que deveria dos dados.

Rótulos de predição dos novos dados

Agora que o novo modelo foi criado, compilado e ajustado aos dados, chegou a hora de finalmente usar o modelo para predizer os rótulos para os dados de teste, iris.test. Para isso, vamos utilizar a função predict() aliada à matriz de confusão com uma ajuda da função table() com o intuito de melhorar a leitura.

# Predict the classes for the test data
classes <- model %>% predict_classes(iris.test, batch_size = 128)

# Confusion matrix
table(iris.testtarget, classes)
##                classes
## iris.testtarget  0  1  2
##               0 20  0  0
##               1  0 12  0
##               2  0  1  8

Evoluindo o modelo

Utilizando a função evaluate() para isto, vamos passar os dados de teste, os rótulos de teste e definir o tamanho do batch. Vamos armazenar tudo isso em uma variável de score.

# Evaluate on test data and labels
score <- model %>% evaluate(iris.test, iris.testLabels, batch_size = 128)

# Print the score
print(score)
## $loss
## [1] 0.08354811
## 
## $acc
## [1] 0.9756098

Podemos verificar o score através da função print() que nos retorna o valor de perda e a métrica selecionada (Escolhemos a acurácia lá em cima).

Tunando o modelo

É, provavelmente, uma das tarefas que mais tomará tempo durante a produção de modelos. Aperfeiçoar o modelo não é uma tarefa tão trivial quanto o do problema utilizado nesse exemplo. Existem três formas de fazer essa melhoria, e são elas: Adição de novas camadas, unidades ocultas e parâmetros de otimização.

Adição de novas camadas

Vamos ver o que acontece ao adicionar novas camadas no modelo e iremos comparar o score com o modelo obtido anteriormente.

# Initialize the sequential model
model <- keras_model_sequential() 

# Add layers to model
model %>% 
    layer_dense(units = 8, activation = 'relu', input_shape = c(4)) %>% 
    layer_dense(units = 5, activation = 'relu') %>% 
    layer_dense(units = 3, activation = 'softmax')

# Compile the model
model %>% compile(
     loss = 'categorical_crossentropy',
     optimizer = 'adam',
     metrics = 'accuracy'
 )

# Fit the model to the data
model %>% fit(
     iris.training, iris.trainLabels, 
     epochs = 200, batch_size = 5, 
     validation_split = 0.2
 )

# Evaluate the model
score <- model %>% evaluate(iris.test, iris.testLabels, batch_size = 128)

# Print the score
print(score)
## $loss
## [1] 0.3330923
## 
## $acc
## [1] 0.9512195

Unidades ocultas

Verificando o efeito das unidades ocultas na arquitetura do modelo, temos:

# Initialize a sequential model
model <- keras_model_sequential() 

# Add layers to the model
model %>% 
    layer_dense(units = 28, activation = 'relu', input_shape = c(4)) %>% 
    layer_dense(units = 3, activation = 'softmax')

# Compile the model
model %>% compile(
     loss = 'categorical_crossentropy',
     optimizer = 'adam',
     metrics = 'accuracy'
 )

# Fit the model to the data
model %>% fit(
     iris.training, iris.trainLabels, 
     epochs = 200, batch_size = 5, 
     validation_split = 0.2
 )

# Evaluate the model
score <- model %>% evaluate(iris.test, iris.testLabels, batch_size = 128)

# Print the score
print(score)
## $loss
## [1] 0.0988775
## 
## $acc
## [1] 0.9756098

Em geral essa não é a melhor otimização devido à possibilidade de overfitting para pouca quantidade de dados. Por esse motivo, é melhor utilizar uma rede pequena para datasets pequenos como este.

Vamos verificar o que acontece ao adicionar unidades ocultas.

# Initialize the sequential model
model <- keras_model_sequential() 

# Add layers to the model
model %>% 
    layer_dense(units = 28, activation = 'relu', input_shape = c(4)) %>% 
    layer_dense(units = 3, activation = 'softmax')

# Compile the model
model %>% compile(
     loss = 'categorical_crossentropy',
     optimizer = 'adam',
     metrics = 'accuracy'
 )

# Save the training history in the history variable
history <- model %>% fit(
  iris.training, iris.trainLabels, 
  epochs = 200, batch_size = 5, 
  validation_split = 0.2
 )

# Plot the model loss
plot(history$metrics$loss, main="Model Loss", xlab = "epoch", ylab="loss", col="blue", type="l")
lines(history$metrics$val_loss, col="green")
legend("topright", c("train","test"), col=c("blue", "green"), lty=c(1,1))

# Plot the model accuracy
plot(history$metrics$acc, main="Model Accuracy", xlab = "epoch", ylab="accuracy", col="blue", type="l")
lines(history$metrics$val_acc, col="green")
legend("bottomright", c("train","test"), col=c("blue", "green"), lty=c(1,1))

Otimização de parâmetros

Existem parâmetros que podem ser passados para a compilação que melhoram os resultados obtidos. Até agora utilizamos o adam, porém existem inúmeros algoritmos que podem ser utilizados. Um deles é o gradiente descendente estocástico. Vamos ver o que acontece ao utilizá-lo?

# Initialize a sequential model
model <- keras_model_sequential() 

# Build up your model by adding layers to it
model %>% 
    layer_dense(units = 8, activation = 'relu', input_shape = c(4)) %>% 
    layer_dense(units = 3, activation = 'softmax')

# Define an optimizer
sgd <- optimizer_sgd(lr = 0.01)

# Use the optimizer to compile the model
model %>% compile(optimizer=sgd, 
                  loss='categorical_crossentropy', 
                  metrics='accuracy')

# Fit the model to the training data
model %>% fit(
     iris.training, iris.trainLabels, 
     epochs = 200, batch_size = 5, 
     validation_split = 0.2
 )

# Evaluate the model
score <- model %>% evaluate(iris.test, iris.testLabels, batch_size = 128)

# Print the loss and accuracy metrics
print(score)
## $loss
## [1] 0.3108546
## 
## $acc
## [1] 0.902439

Salvar, carregar e exportar o seu modelo

As funções para salvar e carregar os modelos são bem simples: save_model_hdf5() e load_model_hdf5(). De forma adicional, também é possível salvar e carregar os pesos dos modelos com as funções save_model_weights_hdf5() e load_model_weights_hdf5().

# save_model_hdf5(model, "my_model.h5")
# model <- load_model_hdf5("my_model.h5")
# save_model_weights_hdf5("my_model_weights.h5")
# model %>% load_model_weights_hdf5("my_model_weights.h5")

Além disso, também é possível exportar o seu modelo como JSON ou YAML.

# json_string <- model_to_json(model)
# model <- model_from_json(json_string)
# 
# yaml_string <- model_to_yaml(model)
# model <- model_from_yaml(yaml_string)

Então é isto, esse foi o tutorial sobre Machine Learning. Espero que tenham gostado, pessoal! Até a próxima.