Deep Learning em R usando o pacote Keras

Este arquivo tem como objetivo ensinar a implementar e interpretar uma rede do tipo multilayer perceptron (MLP) em R e Keras, um framework de deep learning. O tutorial original pode ser encontrado em: https://www.datacamp.com/community/tutorials/keras-r-deep-learning.

Instalando Keras

O primeiro passo é instalar o pacote Keras. Perceba que, para instalá-lo, precisamos instalar antes o pacote devtools:

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

Assim que os pacotes necessários forem instalados, já se pode importar a biblioteca Keras e instalar o Keras e o tensorflow no projeto:

# Importando pacote Keras
library(keras)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
# Instalando Keras e TensorFlow
install_keras()
## Using existing virtualenv at  ~/.virtualenvs/r-tensorflow 
## Upgrading pip ...
## Upgrading wheel ...
## Upgrading setuptools ...
## Installing TensorFlow ...
## 
## Installation complete.

Carregando os dados

O Keras possui alguns dataframe embutidos, que podem ser listados em https://github.com/keras-team/keras/tree/master/keras/dataframes, carregados e utilizados no projeto a partir do próprio pacote. Também é possível baixar dataframes no site da UCI, que contém repositórios de dados para machine learning e podem ser encontrados em http://archive.ics.uci.edu/ml/index.php.

Os dados utilizados neste tutorial podem ser acessados em http://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data e constituem o dataframe mais famoso do UCI: o Iris dataframe.

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

# Nomeando as colunas do dataframe
names(iris) <- c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width", "Species")

Explorando os dados

Iris é um gênero de flores e contém sépalas e pétalas. As sépalas são as partes semelhantes a folhas, normalmente de cor verde, que envolvem a flor, e as pétalas são normalmente coloridas. Nas flores do gênero Iris, as sépalas e pétalas são basatante parecidas, como se pode observar nas figuras abaixo:

Diferentes flores do gênero Iris

Diferentes flores do gênero Iris

A seguir, tem-se um gráfico que mostra uma correlação positiva entre o tamanho e a largura das pétalas (variáveis Petal.Length e Petal.Width) para cada espécie de Iris.

#Exibindo o gráfico de correlação entre tamanho e largura das pétalas
plot(iris$Petal.Length, 
     iris$Petal.Width, 
     pch=21, bg=c("red","green3","blue")[unclass(iris$Species)], 
     xlab="Petal Length", 
     ylab="Petal Width")

#Exibindo correlação entre o tamanho e a largura da pétala
cor(iris$Petal.Length, iris$Petal.Width)
## [1] 0.9627571

Preprocessando os dados

Antes de construir o modelo, precisamos preprocessar os dados a fim de limpá-los, normalizá-los e dividí-los em dados de treino e dados de teste.

Percebe-se, utilizando as funções summary e str que os dados estão limpos, restando normalizar e particionar esses dados.

# Exibindo sumário do dataframe
summary(iris)
##   Sepal.Length    Sepal.Width     Petal.Length    Petal.Width   
##  Min.   :4.300   Min.   :2.000   Min.   :1.000   Min.   :0.100  
##  1st Qu.:5.100   1st Qu.:2.800   1st Qu.:1.600   1st Qu.:0.300  
##  Median :5.800   Median :3.000   Median :4.350   Median :1.300  
##  Mean   :5.843   Mean   :3.054   Mean   :3.759   Mean   :1.199  
##  3rd Qu.:6.400   3rd Qu.:3.300   3rd Qu.:5.100   3rd Qu.:1.800  
##  Max.   :7.900   Max.   :4.400   Max.   :6.900   Max.   :2.500  
##             Species  
##  Iris-setosa    :50  
##  Iris-versicolor:50  
##  Iris-virginica :50  
##                      
##                      
## 
# Verificando a estrutura do dataframe
str(iris)
## 'data.frame':    150 obs. of  5 variables:
##  $ Sepal.Length: num  5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
##  $ Sepal.Width : num  3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
##  $ Petal.Length: num  1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
##  $ Petal.Width : num  0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
##  $ Species     : Factor w/ 3 levels "Iris-setosa",..: 1 1 1 1 1 1 1 1 1 1 ...

Para normalização, utilizou-se a função do pacote do keras e, para isso, precisou-se antes transformar os dados em uma matriz.

# Tranformando a variável espécies em numérica
iris[,5] <- as.numeric(iris[,5]) -1

# Tranformando os dados em matriz
iris <- as.matrix(iris)

# Tirando dimnames
dimnames(iris) <- NULL

# Normalizando os dados
iris[,1:4] <- normalize(iris[,1:4])

# Sumarizando o resultado
summary(iris)
##        V1               V2               V3               V4         
##  Min.   :0.6539   Min.   :0.2384   Min.   :0.1678   Min.   :0.01473  
##  1st Qu.:0.7153   1st Qu.:0.3267   1st Qu.:0.2509   1st Qu.:0.04873  
##  Median :0.7549   Median :0.3544   Median :0.5364   Median :0.16415  
##  Mean   :0.7516   Mean   :0.4048   Mean   :0.4550   Mean   :0.14096  
##  3rd Qu.:0.7884   3rd Qu.:0.5252   3rd Qu.:0.5800   3rd Qu.:0.19753  
##  Max.   :0.8609   Max.   :0.6071   Max.   :0.6370   Max.   :0.28042  
##        V5   
##  Min.   :0  
##  1st Qu.:0  
##  Median :1  
##  Mean   :1  
##  3rd Qu.:2  
##  Max.   :2

Particionando os dados em treino e teste

Com os dados devidamente tratados, precisamos particioná-los em dados de treino e de teste para garantir que o modelo é bom preditor.

# Setando a seed
set.seed(132)

# Determinando o tamanho da partição
partition <- sample(2, nrow(iris), replace=TRUE, prob=c(0.67, 0.33))

# Particionando os dados
iris.training <- iris[partition==1, 1:4]
iris.test <- iris[partition==2, 1:4]

# Separando a classe de espécie (a que se deseja prever)
iris.trainingtarget <- iris[partition==1, 5]
iris.testtarget <- iris[partition==2, 5]

Quando a classe que se quer predizer não é binária, é uma boa prática transformar o atributo em um vetor que contém valores para cada valor da classe e tratar cada um desses vetores como uma classe binária de cada valor da classe, independentemente de uma determinada instância possuir ou não esse valor de classe (one hot encode). Neste caso, como são três espécies de Iris, cria-se um vetor para cada espécie, totalizando três vetores. Cada vetor será uma classe binária de cada espécie diferente, informando se a espécie observada é a que ele representa ou não. O pacote keras já faz esse tratamento:

# One hot encode nos valores de treino
iris.trainLabels <- to_categorical(iris.trainingtarget)

# One hot encode nos valores de teste
iris.testLabels <- to_categorical(iris.testtarget)

# Resultado
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,]    0    1    0
## [20,]    0    1    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    1    0
## [34,]    0    1    0
## [35,]    0    1    0
## [36,]    0    1    0
## [37,]    0    0    1
## [38,]    0    0    1
## [39,]    0    0    1
## [40,]    0    0    1
## [41,]    0    0    1
## [42,]    0    0    1
## [43,]    0    0    1
## [44,]    0    0    1
## [45,]    0    0    1
## [46,]    0    0    1
## [47,]    0    0    1
## [48,]    0    0    1
## [49,]    0    0    1
## [50,]    0    0    1
## [51,]    0    0    1
## [52,]    0    0    1
## [53,]    0    0    1

Construindo o modelo

Para casos como este em que a classe a ser predita é não-binária, a rede neural mais indicada é a multi-layer perceptron (MLP), um tipo de rede que possui neurônios completamente conectados. A função de ativação a ser utilizada é a relu, ativada nas camadas ocultas da rede. Outra funcção de ativação também é utilizada: a softmax, usada na camada de saída para certificar-se de que os valores de saída estão no intervalo entre 0 e 1 e podem ser usados como probabilidades de predição.

# Inicializando um modelo sequencial
model <- keras_model_sequential() 

# Adicionando camadas ao modelo
model %>% 
    layer_dense(units = 8, activation = 'relu', input_shape = c(4)) %>% 
    layer_dense(units = 3, activation = 'softmax')

# Sumarizando o modelo
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
## ___________________________________________________________________________
# Listando as camadas do modelo
model$layers
## [[1]]
## <keras.layers.core.Dense>
## 
## [[2]]
## <keras.layers.core.Dense>
# Listando os input tensors
model$inputs
## [[1]]
## Tensor("dense_1_input:0", shape=(?, 4), dtype=float32)
# Listando os output tensors
model$outputs
## [[1]]
## Tensor("dense_2/Softmax:0", shape=(?, 3), dtype=float32)

Compilando e treinando o modelo

Para treinar o modelo com os dados de treino, utiliza-se a função compile:

# Compilando o modelo
model %>% compile(
     loss = 'categorical_crossentropy',
     optimizer = 'adam',
     metrics = 'accuracy'
 )

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

Para visualizar o histórico de treinamento do modelo em relação a acurácia e perda nos dados de treino e de teste, armazena-se em uma variável e depois exibe-a em um gráfico:

# Armazenando o histórico do treinamento 
history <- model %>% fit(
     iris.training, 
     iris.trainLabels, 
     epochs = 200,
     batch_size = 5, 
     validation_split = 0.2
 )

# Exibindo o histórico
plot(history)

Predizendo as espécies

Para predizer as espécies das flores baseando-se no tamanho e na largura das pétalas, usa-se a função predict_classes do keras e depois compara os resultados entre a espécie que o modelo preveu e a espécie real das flores:

# Predizendo as espécies dos dados de teste
classes <- model %>% predict_classes(iris.test, batch_size = 128)

# Comparando os resultados
table(iris.testtarget, classes)
##                classes
## iris.testtarget  0  1  2
##               0 18  0  0
##               1  0 18  0
##               2  0  5 12

Avaliando o modelo

Para avaliar o modelo, usa-se a função evaluate e depois exibe o score que o modelo recebeu.

# Avaliando o modelo
score <- model %>% evaluate(iris.test, iris.testLabels, batch_size = 128)

# Exibindo o score
print(score)
## $loss
## [1] 0.280623
## 
## $acc
## [1] 0.9056604

O modelo teve uma grande perda. Para contornar a situação, normalmente ajustamos o nosso modelo, modificando o número de camadas, o número de hidden units para cada camada e ajustando os parâmetros de otimização.

Adicionando camadas

Vejamos o que acontece ao adicionarmos uma nova camada ao modelo.

# Inicializando um modelo sequencial
model <- keras_model_sequential() 

# Addicionando camadas ao modelo
model %>% 
    layer_dense(units = 8, activation = 'relu', input_shape = c(4)) %>% 
    layer_dense(units = 5, activation = 'relu') %>% 
    layer_dense(units = 3, activation = 'softmax')

# Compilando o modelo
model %>% compile(
     loss = 'categorical_crossentropy',
     optimizer = 'adam',
     metrics = 'accuracy'
 )

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

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

# Exibindo o score
print(score)
## $loss
## [1] 1.162084
## 
## $acc
## [1] 0.3396226
# Save the training history in history
history <- model %>% fit(
  iris.training, iris.trainLabels, 
  epochs = 200, batch_size = 5,
  validation_split = 0.2
 )
plot(history)

Adicionando hidden units

Vejamos o que acontece ao adicionarmos mais hidden units ao modelo.

# Inicializando um modelo sequencial
model <- keras_model_sequential() 

# Addicionando camadas ao modelo
model %>% 
    layer_dense(units = 28, activation = 'relu', input_shape = c(4)) %>% 
    layer_dense(units = 3, activation = 'softmax')

# Compilando o modelo
model %>% compile(
     loss = 'categorical_crossentropy',
     optimizer = 'adam',
     metrics = 'accuracy'
 )

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

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

# Exibindo o score
print(score)
## $loss
## [1] 0.2869686
## 
## $acc
## [1] 0.9056604
# Salvando o histórico de treino
history <- model %>% fit(
  iris.training, iris.trainLabels, 
  epochs = 200, batch_size = 5,
  validation_split = 0.2
 )
# Mostrando histórico
plot(history)

Ajustando parâmetros de otimização

Vejamos o que acontece ao definirmos um novo parâmetro de otimização ao modelo.

# Inicializando um modelo sequencial
model <- keras_model_sequential() 

# Adicionando camadas 
model %>% 
    layer_dense(units = 8, activation = 'relu', input_shape = c(4)) %>% 
    layer_dense(units = 3, activation = 'softmax')

# Definindo um otimizador
sgd <- optimizer_sgd(lr = 0.01)

# Usando o otimizador para compilar o modelo
model %>% compile(optimizer=sgd, 
                  loss='categorical_crossentropy', 
                  metrics='accuracy')

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

# Avaliando o modelo
score <- model %>% evaluate(iris.test, iris.testLabels, batch_size = 128)

# Exibindo score
print(score)
## $loss
## [1] 0.6521385
## 
## $acc
## [1] 0.6792453
# Salvando o histórico de treino
history <- model %>% fit(
  iris.training, iris.trainLabels, 
  epochs = 200, batch_size = 5,
  validation_split = 0.2
 )
# Mostrando histórico
plot(history)