Construindo Autoencoders

Autoencoders são redes neurais treinadas com o objetivo de copiar o seu input para o seu output. Esse interesse pode parecer meio estranho, mas na prática o objetivo é aprender representações (encodings) dos dados, que podem ser usadas para redução de dimensionalidade ou até mesmo compressão de arquivos.

Basicamente, um autoencoder é dividido em duas partes:

  • um encoder que é uma função \(f(x)\) que transforma o input para uma representação \(h\)
  • um decoder que é uma função \(g(x)\) que transforma a representação \(h\) em sua reconstrução \(r\)

autoencoder Imagem do blog do Keras

Construindo o seu primeiro autoencoder

Nesse pequeno tutorial, vou usar o keras para definir e treinar os nossos autoencoders. Como base de dados vou usar algumas simulações e o banco de dados mnist (famoso para todos que já mexeram um pouco com deep learning). O mnist é um banco de dados de imagens de tamanho 28x28 de dígitos escritos à mão. Esse dataset promoveu grandes avanços na área de reconhecimento de imagens.

library(keras)

encoding_dim <- 32

# definindo o input
input <- layer_input(shape = 784)
# definindo o encoder
encoded <- layer_dense(input, encoding_dim, activation = "relu")
# definindo o decoder
decoded <- layer_dense(encoded, 784, activation = "sigmoid")

autoencoder <- keras_model(input, decoded)
encoder <- keras_model(input, encoded)

# definindo o decoder
encoded_input <- layer_input(shape = encoding_dim)
decoder_layer <- autoencoder$get_layer(index = -1L) # última camada do autoencoder
decoder <- keras_model(encoded_input, decoder_layer(encoded_input))

Com esse código definimos um modelo da seguinte forma:

\[ X = (X*W_1 + b_1)*W_2 + b_2 \]

Em que:

  • \(X\) é o nosso input com dimensão (?, 784)
  • \(W_1\) é uma matriz de pesos com dimensões (784, 32)
  • \(b_1\) é uma matriz de forma (?, 32)
  • \(W_2\) é uma matriz de pesos com dimensões (32, 784)
  • \(b_2\) é uma matriz de forma (?, 784)

Note que ? aqui é o número de observaçãoes da base de dados. Agora vamos estimar \(W_1\), \(W_2\), \(b_1\) e \(b_2\) de modo a minimizar alguma função de perda.

Inicialmente vamos usar a binary crossentropy por pixel que é definida por:

\[-\sum_{i=1}y_i*log(\hat{y}_i)\]

Isso é definido no keras usando:

autoencoder %>% compile(optimizer='adadelta', loss='binary_crossentropy')

Não vou entrar em detalhes do que é o adadelta, mas é uma variação do método de otimização conhecido como gradient descent.

Agora vamos carregar a base de dados e em seguida treinar o nosso autoencoder`.

mnist <- dataset_mnist()
# o mnist é um banco de imagens 28x28, vamos transformar cada imagem em um vetor
# de tamanho 784, cada elemento representado um pixel.
x_train <- mnist$train$x %>% apply(1, as.numeric) %>% t()
x_test <- mnist$test$x %>% apply(1, as.numeric) %>% t()
# vamos transformar as imagens p/ o intervalo 0-1 para que
# a função de perda funcione corretamente.
x_train <- x_train/255
x_test <- x_test/255

Estimamos os parâmetros desse modelo no keras fazendo:

autoencoder %>% fit(
  x_train, x_train,
  epochs = 50,
  batch_size = 256,
  shuffle = TRUE,
  validation_data = list(x_test, x_test)
)

Depois de rodar todas as iterações, você poderá usar o seu encoder e o seu decoder para entender o que eles fazem com as imagens.

Veja o exemplo a seguir em que vamos obter os encodings para as 10 primeiras imagens da base de teste e depois reconstruir a imagem usando o decoder.

encoded_imgs <- predict(encoder, x_test[1:10,])
dim(encoded_imgs)
encoded_imgs[1,] # representação vetorial de uma imagem.
## [1] 10 32
##  [1]  0.0000000 10.1513205  3.5742311  2.6635208  6.3097358  3.4840517
##  [7]  9.1041250  6.6329145  1.6385922  9.8017225  9.5529270  1.6670935
## [13]  5.7208562  4.8035479  3.9149191  0.6408147  1.2716029  3.1215091
## [19] 13.7575903  0.0000000  1.8692881  3.2142215  0.7444992  5.0728440
## [25]  8.2932110  9.9866810  2.7651572 11.1291723  5.2460670  5.6875997
## [31] 10.6097431  3.6338394

O encoder transforma a matriz de (10, 784) para uma matriz com dimensao (10, 2). Podemos reconstruir a imagem, a pardir da imagem que foi comprimida usando o nosso decoder.

predict(decoder, encoded_imgs) %>% 
  split(1:10) %>%
  lapply(matrix, ncol = 28) %>% 
  Reduce(cbind, .) %>%
  as.raster() %>%
  plot()

Compare as reconstruções com as imagens originais abaixo:

x_test[1:10,] %>%
  split(1:10) %>%
  lapply(matrix, ncol = 28) %>% 
  Reduce(cbind, .) %>%
  as.raster() %>%
  plot()

Um ponto interessante é que esse modelo faz uma aproximação da solução por componentes principais! Na verdade, a definição do quanto são parecidos é quase-equivalente. Isso quer dizer que os pesos \(W\) encontrados pelo PCA e pelo autoencoder serão diferentes, mas o sub-espaço criado pelos mesmos será equivalente.

Se são equivalentes, qual a vantagem de usar autoencoders ao invés de PCA? O PCA para por aqui, você define que serão apenas relações lineares, e você reduz dimensão apenas reduzindo o tamanho da matriz. Em autoencoders você tem diversas outras saídas para aprimorar o método.

A primeira delas é simplesmente adicionar uma condição de esparsidade nos pesos. Isso vai reduzir o tamanho do vetor latente (como é chamada a camada do meio do autoencoder) também, pois ele terá mais zeros.

Isso pode ser feito rapidamente com o keras. Basta adicionar um activity_regularizer em nossa camada de encoding. Isso vai adicionar na função de perda um termo que toma conta do valor dos outputs da camada intermediária.

# definindo o input
input <- layer_input(shape = 784)
# definindo o encoder
encoded <- layer_dense(input, encoding_dim, activation = "relu", 
                       activity_regularizer = regularizer_l1(l = 10e-5))
# definindo o decoder
decoded <- layer_dense(encoded, 784, activation = "sigmoid")

autoencoder <- keras_model(input, decoded)
autoencoder %>% compile(optimizer='adadelta', loss='binary_crossentropy')
autoencoder %>% fit(
  x_train, x_train,
  epochs = 50,
  batch_size = 256,
  shuffle = TRUE,
  validation_data = list(x_test, x_test)
)

Outra forma de melhorar o seu autoencoder é permitir que o encoder e o decoder sejam redes neurais profundas. Com isso, ao invés de tentar encontrar transformações lineares, você permitirá que o autoencoder encontre transformações não lineares.

Mais uma vez fazemos isso com o keras:

input <- layer_input(shape = 784)

encoded <- layer_dense(input, 128, activation = "relu") %>%
  layer_dense(64, activation = "relu") %>%
  layer_dense(32, activation = "relu")

decoded <- layer_dense(encoded, 64, activation = "relu") %>%
  layer_dense(128, activation = "relu") %>%
  layer_dense(784, activation = "sigmoid")


autoencoder <- keras_model(input, decoded)
autoencoder %>% compile(optimizer='adadelta', loss='binary_crossentropy')
autoencoder %>% fit(
  x_train, x_train,
  epochs = 50,
  batch_size = 256,
  shuffle = TRUE,
  validation_data = list(x_test, x_test)
)

Existem formas ainda mais inteligentes de construir esses autoencoders, mas o post iria ficar muito longo e não ia sobrar asssunto para o próximo. Se você quiser saber mais, recomendo fortemente a leitura deste artigo do blog do Keras e desse capítulo.

Uma família bem moderna de autoencoders são os VAE (Variational Autoencoders). Esses autoencoders aprendem modelos de variáveis latentes. Isso é interessante porque permite que você gere novos dados, parecidos com os que você usou para treinar o seu autoencoder. Você pode encontrar uma implementação desse modelo aqui.

É isso! Abraços

comments powered by Disqus