No primeiro post dessa série montamos um banco de dados de imagens que está pronto para a modelagem. Também ajustamos um classificador off-the-shelf, o xgboost nesses dados e obtivemos acerto de 91% de classificação na base de testes.

Agora vamos ajustar modelos que começam a fazer parte do mundo deep learning. Um deles é a regressão logística, que neste mundo é mais conhecida como softmax regression. Neste post vamos ajustar essa regressão logística e em seguida uma redeu neural com mais camadas.

Para treinar esses modelos vamos ustilizar o TensorFlow, uma biblioteca open source concebida pelo Google especialmente para ajustar modelos deste tipo. Já falamos dela aqui e neste post, vamos usar uma interface de maior nível, para que seja mais fácil explicitar esses modelos no código.

O pacote do R que será utilizado, tem o mesmo nome que a biblioteca feita pelo Google: tensorflow e pode ser instalada usando o comando devtools::install_github('rstudio/tensorflow'). Talvez você tenha que instalar o TensorFlow do Google antes, mas nao é nada de outro mundo.

Carregando o banco de dados

No post anterior criamos o banco de dados, portanto se você ainda não criou, volte lá.

train_data <- readRDS('train_dataset.rds')
test_data <- readRDS('test_dataset.rds')

Regressão Softmax

O TensorFlow trabalha com a definição de grafos de operações. Primeiro você define esses grafos e depois os executa por meio de um objeto de classe Session.

Sem mais detalhes, vamos ao código:

library(tensorflow)
library(magrittr)

# criar sessão
session <- tf$Session()

# definir o placeholder dos inputs
x <- tf$placeholder(tf$float32, shape = shape(NULL, 784L)) # 784 é o número de pixels de cada imagem.
y <- tf$placeholder(tf$float32, shape = shape(NULL, 10L)) # 10 é o número de classes distintas

# definir o modelo
logits <- tf$contrib$layers$linear(x, 10L)
y_ <- tf$nn$softmax(logits)

# definir a função de perda
loss <- tf$reduce_mean(tf$nn$softmax_cross_entropy_with_logits(logits, y))

# definir o método de optimização
optmizer <- tf$train$GradientDescentOptimizer(0.5)
train_step <- optmizer$minimize(loss)

init <- tf$initialize_all_variables()

Em todo bloco acima definimos o grafo do modelo que iremos treinar na sequência. Definimos:

  • x: placeholder do input com valores do tipo float e com n linhas e 784 colunas
  • y: palceholder dos labels com valores do tipo int e com n linhas e 10 colunas
  • logits: a forma de cálculo dos logitos: uma camada inteiramente ligada
  • out: a forma de cálculodas probabilidades de cada classe. Aplicando a função softmax
  • loss: a função de perda cross entropy
  • optmizer e train_step: método de otimização: Gradien Descent
  • init: operação para inicializar as variáveis

Certo! Antes de partir para a parte de ajuste do modelo vamos deixar o banco de dados exatamente da forma que precisamos para o tensorflow. Assim como para o xgboost no post anterior, vamos precisar de uma matriz com cada linha uma imagem e 784 colunas: uma para cada píxel. Para os labels, vamos aplicar a transformação one hot encode que nada mais é do que cada linha ser uma imagem e cada coluna uma das possíveis classes. Essa matriz tem valor 1 quando a imagem é daquela classe e 0 nas demais colunas. Assim como no post anterior, também vamos transformar previamente os valores de 0 a 255 para valores de -1 a 1.

train_data <- (train_data - (255/2))/(255/2)
test_data <- (test_data - (255/2))/(255/2)

train_x <- t(apply(train_data, 1, c))
train_y <- model.matrix(~ 0 + as.factor(dimnames(train_data)[[1]]))

test_x <- t(apply(test_data, 1, c))
test_y <- model.matrix(~ 0 + as.factor(dimnames(test_data)[[1]]))

Pronto! Agora podemos partir para o ajuste do algoritmo. Uma dos diferenciais dos modelos de deep learning, é a otimização por meio do Stochastic Gradient Descent, esse método permite a agilidade para treinar os algoritmos e consiste em ao invés de ajustar os pesos do modelo a cada iteração usando a base inteira, usa-se batches: pequenas parcelas dos dados. Isso faz com que sejam necessárias muito mais iterações até conseguir a convergência, no entanto cada iteração demora muito menos. Na prática o trade-off vale a pena, ou seja, fica mais rápido fazer muitas iterações pequenas do que poucas grandes.

Agora veja o código usado para treinar o modelo.

session$run(init)
n_steps <- 10000L
batch_size <- 128L
acc_loss <- NULL
indices_possiveis <- NULL

for(step in 1:n_steps){
  
  if(length(indices_possiveis) < batch_size){
    indices_possiveis <- 1:nrow(train_x)
  }
  
  indices <- sample(indices_possiveis, batch_size)
  indices_possiveis <- indices_possiveis[! indices_possiveis %in% indices]
  
  batch_x <- train_x[indices,]
  batch_y <- train_y[indices,]
  result <- session$run(list(loss, train_step), feed_dict = dict(x = batch_x, y = batch_y))
  
  acc_loss <- c(acc_loss, result[[1]])
  
  if(step %% 1000 == 0){
   cat(sprintf('Step: %04d Loss: %5.5f \n', step, mean(acc_loss))) 
  }
}
## Step: 1000 Loss: 1.47180 
## Step: 2000 Loss: 1.41761 
## Step: 3000 Loss: 1.39570 
## Step: 4000 Loss: 1.38201 
## Step: 5000 Loss: 1.37048 
## Step: 6000 Loss: 1.36435 
## Step: 7000 Loss: 1.36048 
## Step: 8000 Loss: 1.35529 
## Step: 9000 Loss: 1.34832 
## Step: 10000 Loss: 1.34424

A seguir definimos uma função que calcula a acurácia e a calculamos para as bases de treino e de teste. Basicamente, essa função pega da matriz de probabilidades, o número da coluna que possui maior probabildiade. Eem seguida compara se essa coluna é a mesma que possui o 1, na mariz de inputs verdadeiros.

accuracy <- function(x_, y){
  pred <- session$run(y_, feed_dict = dict(x = x_)) %>% 
    apply(1, function(x) {
      (1:10)[order(-x)][1]
    })
  real <- apply(y, 1, function(x) {
    (1:10)[order(-x)][1]
  })
  sum(real == pred)/length(real)
}

accuracy(train_x, train_y)
## [1] 0.8083162
accuracy(test_x, test_y)
## [1] 0.862743

O modelo de regressão logística foi treinado e está acertando neste caso 80% das iamgens na base de treino e 86% na base de teste. Esse número pode variar um pouco.

Hidden layer neural network

Agora vamos adaptar o exemplo acima para uma rede neural com uma camada oculta. Você verá que pouco muda no código! Isso deve melhorar um pouco o acerto do modelo.

# criar sessão
session <- tf$Session()

# definir o placeholder dos inputs
x <- tf$placeholder(tf$float32, shape = shape(NULL, 784L)) # 784 é o número de pixels de cada imagem.
y <- tf$placeholder(tf$float32, shape = shape(NULL, 10L)) # 10 é o número de classes distintas

# definir o modelo
layer1 <- tf$contrib$layers$fully_connected(x, 1024L, activation_fn = tf$nn$relu) # só essa e a próxima linha mudam
logits <- tf$contrib$layers$linear(layer1, 10L)
y_ <- tf$nn$softmax(logits)

# definir a função de perda
loss <- tf$reduce_mean(tf$nn$softmax_cross_entropy_with_logits(logits, y))

# definir o método de optimização
optmizer <- tf$train$GradientDescentOptimizer(0.5)
train_step <- optmizer$minimize(loss)

init <- tf$initialize_all_variables()

O código para treinar o modelo é idêntico!

session$run(init)
n_steps <- 10000L
batch_size <- 128L
acc_loss <- NULL
indices_possiveis <- NULL

for(step in 1:n_steps){
  
  if(length(indices_possiveis) < batch_size){
    indices_possiveis <- 1:nrow(train_x)
  }
  
  indices <- sample(indices_possiveis, batch_size)
  indices_possiveis <- indices_possiveis[! indices_possiveis %in% indices]
  
  batch_x <- train_x[indices,]
  batch_y <- train_y[indices,]
  result <- session$run(list(loss, train_step), feed_dict = dict(x = batch_x, y = batch_y))
  
  acc_loss <- c(acc_loss, result[[1]])
  
  if(step %% 1000 == 0){
   cat(sprintf('Step: %04d Loss: %5.5f \n', step, mean(acc_loss))) 
  }
}
## Step: 1000 Loss: 0.62297 
## Step: 2000 Loss: 0.56271 
## Step: 3000 Loss: 0.52699 
## Step: 4000 Loss: 0.49942 
## Step: 5000 Loss: 0.47542 
## Step: 6000 Loss: 0.45846 
## Step: 7000 Loss: 0.44240 
## Step: 8000 Loss: 0.42757 
## Step: 9000 Loss: 0.41236 
## Step: 10000 Loss: 0.39926

Com a mesma função calculamos a acurácia do modelo.

accuracy(train_x, train_y)
## [1] 0.9323186
accuracy(test_x, test_y)
## [1] 0.9235206

O resultado agora foi de 90% das imagens classificadas corretamente! Se comparado à regressão logística, já melhoramos 10 pontos percentuais. Ficamos também muito próximos do resultado do xgboost. No próximo post você verá como melhorar ainda mais os resultados obtidos aqui com técnicas simples como regularização e dropout.