Instalado o pacote Keras

Texto original: https://www.datacamp.com/community/tutorials/keras-r-deep-learning

O primeiro passo é instalar o keras no RStudio. Para isso precisamos instalar o pacote devtools e tensorflow, através dos seguintes comandos:

install.packages(“devtools”) e install.packages(“tensorflow”)

Instalar o tensorflow requer a instalação prévia do Python.

Em seguida, instalamos o keras através do console com o comando devtools::install_github(“rstudio/keras”).

Agora podemos importar as bibliotecas:

knitr::opts_chunk$set(echo = TRUE)

#install.packages("keras")
#install.packages("tensorflow")

library(keras)
use_condaenv("r-tensorflow")
library(tensorflow)

#install_keras()
#install_tensorflow()

Carregando os dados

Para carregar os dados, usamos a função read.csv(). Nesse tutorial foram utilizados os dados fornecidos pelo UCI Machine Learning Repository.

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

Usaremos as funções abaixo para checar se a leitura dos dados foi feita corretamente:

A função head() retorna a primeira parte do data frame

head(iris)
##    V1  V2  V3  V4          V5
## 1 5.1 3.5 1.4 0.2 Iris-setosa
## 2 4.9 3.0 1.4 0.2 Iris-setosa
## 3 4.7 3.2 1.3 0.2 Iris-setosa
## 4 4.6 3.1 1.5 0.2 Iris-setosa
## 5 5.0 3.6 1.4 0.2 Iris-setosa
## 6 5.4 3.9 1.7 0.4 Iris-setosa

Já str() proporciona uma visualização compacta da estrutura interna

str(iris)
## 'data.frame':    150 obs. of  5 variables:
##  $ V1: num  5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
##  $ V2: num  3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
##  $ V3: num  1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
##  $ V4: num  0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
##  $ V5: Factor w/ 3 levels "Iris-setosa",..: 1 1 1 1 1 1 1 1 1 1 ...

Por fim, com a função dim() obtemos as dimensões

dim(iris)
## [1] 150   5

Explorando os dados

O gráfico indica a correlação (dependência) possitiva entre comprimento (length) e largura (width) para diferentes espécies da flor de Íris.

names(iris) <- c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width", "Species")

plot(iris$Petal.Length, 
     iris$Petal.Width, 
     pch=21, bg=c("red","green3","blue")[unclass(iris$Species)], 
     xlab="Petal Length", 
     ylab="Petal Width")

cor(iris$Petal.Length, iris$Petal.Width)
## [1] 0.9627571

Podemos confirmar essa informação plotando a correlação entre os atributos com a função cor().

library(corrplot)
## corrplot 0.84 loaded
M <- cor(iris[,1:4])
corrplot(M, method="circle")

Pré-processamento de Dados

A etapa de pré-processamento de dados compreende a aplicação de técnicas para realizar a limpeza, normalização e divisão (em treino e teste) dos dados.

Como observamos anteriormente, ao inspecionar os dados, a função head() e str() não indicaram nada fora do esperado. Agora usaremos a função summary() para confirmar que os dados estão suficientemente limpos.

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  
##                      
##                      
## 

Os valores dos atributos variam de 0.1 à 7.9, o que é considerado aceitável. Sendo assim, não seria necessário normalizar os dados. Entretanto, queremos estudar o efeito da normalização nos dados. A normalização busca reduzir a redundância de dados. Para isso foi feita uma função de normalização min-max, que linearmente transforma os dados para a função (x-min)/(max-min). Além da função criada, usamos o lapply() para normalizar os dados.

normalize <- function(x) {
  num <- x - min(x)
  denom <- max(x) - min(x)
  return (num/denom)
}

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

head(iris)
##   Sepal.Length Sepal.Width Petal.Length Petal.Width     Species
## 1          5.1         3.5          1.4         0.2 Iris-setosa
## 2          4.9         3.0          1.4         0.2 Iris-setosa
## 3          4.7         3.2          1.3         0.2 Iris-setosa
## 4          4.6         3.1          1.5         0.2 Iris-setosa
## 5          5.0         3.6          1.4         0.2 Iris-setosa
## 6          5.4         3.9          1.7         0.4 Iris-setosa

Para que a função normalize() seja usada no keras, é necessário garantir que estamos trabalhando com uma matriz, onde os elementos da mesma devem ser do mesmo tipo. Como verificamos anteriormente através da função str(), os valores de Species são do tipo fator, enquanto o restante dos valores é numérico. Sendo assim, vamos converter os dados para o tipo numérico através da função as.numeric(). Já para fazer a conversão dos dados para uma matriz, usamos a função as.matrix().

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

iris <- as.matrix(iris)

dimnames(iris) <- NULL

iris[,1:4] <- normalize(iris[,1:4])

summary(iris)
##        V1               V2               V3               V4         
##  Min.   :0.5385   Min.   :0.2436   Min.   :0.1154   Min.   :0.00000  
##  1st Qu.:0.6410   1st Qu.:0.3462   1st Qu.:0.1923   1st Qu.:0.02564  
##  Median :0.7308   Median :0.3718   Median :0.5449   Median :0.15385  
##  Mean   :0.7363   Mean   :0.3787   Mean   :0.4691   Mean   :0.14085  
##  3rd Qu.:0.8077   3rd Qu.:0.4103   3rd Qu.:0.6410   3rd Qu.:0.21795  
##  Max.   :1.0000   Max.   :0.5513   Max.   :0.8718   Max.   :0.30769  
##        V5   
##  Min.   :0  
##  1st Qu.:0  
##  Median :1  
##  Mean   :1  
##  3rd Qu.:2  
##  Max.   :2

Conjuntos de treino e de teste

Para que possamos começar a construção do modelo, vamos dividir os dados originais em treino e teste, isso garante que as avaliações do desempenho de seu modelo de previsão sejam honestas.

Usamos a função sample () para tirar uma amostra com um tamanho que é definido como o número de linhas do conjunto de dados, nesse caso, 150. Escolhemos de um vetor de 2 elementos e atribuimos 1 ou 2 para as 150 linhas do conjunto de dados. A atribuição dos elementos está sujeita a pesos de probabilidade de 0,67 e 0,33.

# Determinando o tamanho da amostra
ind <- sample(2, nrow(iris), replace=TRUE, prob=c(0.67, 0.33))

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

# Dividindo o atributo de classe
iris.trainingtarget <- iris[ind==1, 5]
iris.testtarget <- iris[ind==2, 5]

One-Hot Encoding

Quando modelamos problemas de classificação multi-classe com redes neurais, costuma ser uma boa prática certificar-se de que transformamos seu atributo-alvo de um vetor que contém valores para cada valor de classe para uma matriz com um booleano para cada valor de classe, independentemente de uma determinada instância possuir ou não esse valor.

A função to_categorical() do keras faz esse tratamento, que é conhecido como One-Hot Encoding (OHE):

# Valores-alvo de treinamento de OHE
iris.trainLabels <- to_categorical(iris.trainingtarget)

# Valores-alvo de teste de OHE
iris.testLabels <- to_categorical(iris.testtarget)

# Imprimindo iris.testLabels para confirmar o 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,]    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    1    0
## [34,]    0    1    0
## [35,]    0    1    0
## [36,]    0    1    0
## [37,]    0    1    0
## [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

Construindo o Modelo

Para começar a construção do modelo, é necessário primeiramente inicializar um modelo sequencial através da função keras_model_sequential(). Estamos trabalhando com dados numéricos, pré-processados e utilizamos o OHE para obter os valores da variável-alvo, onde uma flor pode ser do tipo versicolor, setosa ou virginica, o que é refletido com os valores binários 1 e 0. Tendo em vista essas características, usaremos uma rede neural totalmente conectada, a multi-layer perceptron. Com isso, buscamos construir uma pilha simples de camadas totalmente conectadas. Usaremos também a função de ativação relu, como forma de familiarização com o keras, já que essa é uma das funções mais comuns. Além disso, usaremos também a função de ativação softmax, para garantir que os valores de saída estão no intervalo de 0 e 1, podendo ser usadas como probabilidades previstas.

# Inicializando um modelo seqüencial
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')

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
## ___________________________________________________________________________

Através da função summary() e das demais funções abaixo, podemos inspecionar melhor o modelo.

# Obtendo a configuração do modelo
get_config(model)
## [{'class_name': 'Dense', 'config': {'name': 'dense_1', 'trainable': True, 'batch_input_shape': (None, 4), 'dtype': 'float32', 'units': 8, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'class_name': 'VarianceScaling', 'config': {'scale': 1.0, 'mode': 'fan_avg', 'distribution': 'uniform', 'seed': None}}, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'kernel_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}}, {'class_name': 'Dense', 'config': {'name': 'dense_2', 'trainable': True, 'units': 3, 'activation': 'softmax', 'use_bias': True, 'kernel_initializer': {'class_name': 'VarianceScaling', 'config': {'scale': 1.0, 'mode': 'fan_avg', 'distribution': 'uniform', 'seed': None}}, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'kernel_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}}]
# Obtendo a configuração das camadas
get_layer(model, index = 1)
## <keras.layers.core.Dense>
# 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)

Compile e ajuste o modelo

Para compilar o modelo, vamos configurá-lo com o otimizador adam e a função de perda categorical_crossentropy. Também vamos monitorar a acurácia durante o treino, através do argumento metrics.

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

Agora vamos ajustar o modelo aos dados. Nesse caso, treinamos o modelo por 200 iterações sobre todas as amostras em iris.training e iris.trainLabels, em 5 lotes de amostras.

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

Visualizando o histórico de treinamento do modelo

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

plot(history)

É importante saber que loss e acc indicam a perda e acurácia do modelo para os dados de treinamento, enquanto val_loss e val_acc são as mesmas métricas para os dados de teste e de validação.

# Plotando a perda do modelo para os dados de treino
plot(history$metrics$loss, main="Model Loss", xlab = "epoch", ylab="loss", col="blue", type="l")

# Plotando a perda do modelo para os dados de teste
lines(history$metrics$val_loss, col="green")

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

No gráfico anterior, plotamos a perda do modelo nos dados de treino e de teste. Agora faremos o mesmo para a acurácia:

# Plotando a acurácia para os dados de treino
plot(history$metrics$acc, main="Model Accuracy", xlab = "epoch", ylab="accuracy", col="blue", type="l")

# Plotando a acurácia para os dados de validação
lines(history$metrics$val_acc, col="green")

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

Predizendo Labels de novos dados

Agora vamos usar o modelo para prever os labels do conjunto de testes iris.test, através da função predict(). Depois, imprimimos a matriz de confusão para verificar as previsões e os labels reais dos dados de iris.test com a função table().

# Predizendo as classes para os dados de teste
classes <- model %>% predict_classes(iris.test, batch_size = 128)

# Matriz de confusão
table(iris.testtarget, classes)
##                classes
## iris.testtarget  0  1  2
##               0 20  0  0
##               1  0 17  0
##               2  0  4 11

Avaliando o modelo

Usamos a função evaluate() para avaliar o modelo.

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

#Imprime o score
print(score)
## $loss
## [1] 0.2285304
## 
## $acc
## [1] 0.9230769

Fine-tuning

Podemos observar que o modelo teve uma perda acentuada. Vamos tentar melhorar o modelo adicionando camadas, aumentando o número de hidden units e arrumando os parâmetros de otimização para a função compile(). Ao imprimir o score, o resultado era dado corretamente, mas a seguinte mensagem de alerta era dada a cada uma das 200 iterações: “UserWarning: Method on_batch_end() is slow compared to the batch update (0.128583). Check your callbacks.”. Afim de facilitar a visualização, parte do código foi comentada e os resultados obtidos colocados abaixo do código comentado.

Adicionando camadas

# Inicializando o modelo sequencial
#model <- keras_model_sequential() 

# Adicionando 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'
# )

# Ajustando o modelo aos dados
#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)

#print(score)

$loss [1] 0.2607501

$acc [1] 0.8958333

# Initialize a sequential model
model <- keras_model_sequential() 

# Add layers to the 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'
 )

# Save the training history in history
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))

Hidden Units

Agora vamos adicionar hidden units ao modelo afim de melhorá-lo.

# Inicializa um modelo sequencial
#model <- keras_model_sequential() 

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

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

# Ajustando o modelo aos dados
#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)

# Imprimindo o score
#print(score)

$loss [1] 0.2360007

$acc [1] 0.9375

# Inicializa um modelo sequencial
model <- keras_model_sequential() 

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

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

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

# Plotando a perda do modelo
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))

# Plotando a acurácia do modelo
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))

Parâmetros de otimização

Além de adicionar camadas e hidden units, podemos tentar ajustar os parâmetros de otimização.

# Inicializa um modelo sequencial
#model <- keras_model_sequential() 

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

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

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

# Ajusta o modelo aos dados de treino
#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)

# Imprimindo as métricas de perda e acurácia
#print(score)

$loss [1] 0.5610743

$acc [1] 0.6041667

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

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

# Ajusta o modelo aos dados de treino
history <- model %>% fit(
  iris.training, iris.trainLabels, 
  epochs = 200, batch_size = 5, 
  validation_split = 0.2
 )

# Plotando a perda do modelo
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))

# Plotando a acurácia do modelo
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))

Salvando, carregando ou exportando seu modelo

Podemos salvar e carregar o modelo através dos seguintes comandos: save_model_hdf5(model, “my_model.h5”) model <- load_model_hdf5(“my_model.h5”)

Também é possível salvar e carregar os pesos do modelo: save_model_weights_hdf5(“my_model_weights.h5”) model %>% load_model_weights_hdf5(“my_model_weights.h5”)

Por fim, podemos exportar as configurações do modelo para JSON ou YAML e carregá-las de volta ao workspace:

json_string <- model_to_json(model)

model <- model_from_json(json_string)

yaml_string <- model_to_yaml(model)

model <- model_from_yaml(yaml_string)