Extraindo representações com autoencoders convolucionais.

Uma abordagem prática do funcionamento de autoencoders convolucionais utilizando grandes conjuntos de dados no Keras.

Toni Esteves
15 min readMay 29, 2020

O que são autoencoders ?

"Autoencoding" é um algoritmo de compactação de dados em que as funções de compactação e descompactação são 1) específicas dos dados, 2) possuem perdas e 3) aprendidas automaticamente a partir de exemplos, em vez de projetadas por humanos. Além disso, em quase todos os contextos em que o termo "autoencoder" é utilizado, as funções de compactação e descompactação são implementadas com redes neurais, asssim:

  1. Autocodificadores são específicos para o conjunto de dados, o que significa que eles só poderão compactar dados semelhantes aos que foram treinados. Isso é diferente, digamos, do algoritmo de compactação MPEG-2 Audio Layer III (MP3), que apenas contém suposições sobre "som" em geral, mas não sobre tipos específicos de sons. Um codificador automático treinado em imagens de rostos faria um trabalho bastante ruim ao comprimir imagens de árvores, porque os recursos que aprenderia seriam específicos do rosto.
  2. Autocodificadores tem perdas, o que significa que as saídas descomprimidas serão degradadas em comparação com as entradas originais (semelhante à compactação MP3 ou JPEG). Isso difere da compressão aritmética sem perdas.
  3. Autocodificadores aprendem automaticamente a partir de exemplos de dados, o que é uma propriedade útil: significa que é fácil treinar instâncias especializadas do algoritmo que terão bom desempenho em um tipo específico de entrada. Não requer nenhuma nova engenharia, apenas dados de treinamento apropriados.
Representação gráfica abstrata de um autoencoder

Em linhas gerais, os autocodificadores são uma técnica de aprendizado não supervisionado, na qual usamos as redes neurais para a tarefa de aprendizado de representação. A idéia principal é projetar uma arquitetura de rede neural de modo a impor um gargalo na rede que força uma representação de conhecimento compactada da entrada original. Se os recursos de entrada fossem independentes um do outro, essa compressão e reconstrução subsequente seriam uma tarefa muito difícil. No entanto, se houver algum tipo de estrutura oculta nos dados (ou seja, correlações entre os recursos de entrada), essa estrutura poderá ser aprendida e consequentemente aproveitada ao forçar a entrada através do gargalo da rede.

Representação gráfica de um autoencoder convolucional.

Assim, autocodificadores geram uma representação compactada do espaço latente, e em seguida, reconstroem essa representação na saída (Figura acima).

Esse tipo de rede neural é composto por duas partes:

Codificador (Encoder): é a parte da rede que compacta a entrada em uma representação de espaço latente (codificando a entrada). Pode ser representado por uma função de codificação h = f(x).

Decodificador (Decoder): Esta parte tem como objetivo reconstruir a entrada da representação do espaço latente. Pode ser representado por uma função de decodificação r = g(h).

Os autocodificadores fornecem um dos paradigmas fundamentais para a aprendizagem não supervisionada, e abordam como as mudanças sinápticas induzidas por eventos locais podem ser coordenadas de maneira auto-organizada para produzir aprendizado global e comportamento inteligente [Baldi, 2012].

De fato, espera-se que, ao treinar o autocodificador a representação apresentada na saída x' tenha propriedades úteis. Isso pode ser conseguido criando restrições em sua execução assim, pode-se restringir x’ a ter dimensões menores que x. Ao treinar um codificador criando restrições, forçamos o autocodificador a aprender os recursos mais importantes dos dados de treinamento. Logo, o objetivo dos autocodificadores não é exclusivamente representar os dados de entrada na saída, mas sim gerar uma representação de conhecimento compactada da entrada original.

Um autocodificador é um tipo de rede neural artificial usada para aprender codificações de dados de maneira eficiente e não supervisionada. O objetivo de um autoencoder é aprender e extrair representações (codificações), identificando padrões ainda não conhecidos para um conjunto de dados.

Para que servem os autoencoders?

Eles raramente são usados ​​em aplicações práticas. Em 2012, eles encontraram brevemente um aplicativo no pré-treinamento ganancioso em camadas para redes neurais profundas convolucionais [1], mas isso rapidamente ficou fora de moda quando começamos a perceber que os esquemas de inicialização aleatória de pesos eram suficientes para treinar redes profundas a partir do zero. Em 2014, a normalização de lotes [2] começou a permitir a utilização de redes ainda mais profundas e, a partir do final de 2015, pudemos treinar redes profundas arbitrariamente a partir do zero usando aprendizado residual [3].

Hoje duas aplicações práticas interessantes dos auto-codificadores são o denoising de dados e a redução de dimensionalidade para visualização de dados. Com restrições de dimensionalidade e esparsidade apropriadas, os autoencoders podem aprender projeções de dados mais interessantes que o PCA ou outras técnicas básicas.

Uma das razões pelas quais eles atraíram tanta pesquisa e atenção é porque acredita-se que eles são uma via potencial para resolver o problema da aprendizagem não supervisionada, ou seja, a aprendizagem de representações úteis sem a necessidade de rótulos. Por outro lado, os autocodificadores não são uma verdadeira técnica de aprendizado não supervisionado (o que implicaria um processo de aprendizado completamente diferente), diz-se que são uma técnica auto-supervisionada, uma instância específica de aprendizado supervisionado em que os alvos são gerados a partir dos dados de entrada.

Para que os modelos auto-supervisionados aprendam recursos interessantes, é necessário criar uma função sintética interessante de perda e destino, e é aí que surgem problemas: apenas aprender a reconstruir sua entrada em mínimos detalhes pode não ser a escolha certa aqui. Nesse ponto, há evidências significativas de que o foco na reconstrução de uma imagem no nível de pixel, por exemplo, não conduz a aprender características abstratas interessantes do tipo que a aprendizagem supervisionada por rótulos induz (onde os alvos são conceitos bastante abstratos "inventados" por humanos como "cachorro ", "carro"). De fato, pode-se argumentar que as melhores características nesse sentido são aquelas que são as piores na reconstrução exata de entradas, enquanto alcançam alto desempenho na tarefa principal em que você está interessado (classificação, localização, etc.).

Representações práticas de autoencorders são difíceis de se encontrar e em sua grande maioria as mesmas apresentam exemplos simples utilizando o datase minist. Acontece que em sua natureza auto-supervisionada autoencoders tem uma arquitetura capsiosa de manter e entender e isso se complica caso seja necessário utilizar uma conjunto massivo de dados. A seguir apresentaremos a construção de um autoencoder o qual quebrei a cabeça alguns dias para carregar um grande volume de dados e definir sua arquitetura de forma coesa.

A matemática por trás dos autocodificadores é razoavelmente fácil de entender, por isso, analisarei brevemente. Como já sabemos, basicamente dividimos a rede em dois segmentos, o codificador e o decodificador.

A função do codificador, denotada por ϕ, mapeia os dados originais X, para um espaço latente F, presente no gargalo. A função decodificador, denotada por ψ, mapeia o espaço latente F no gargalo para a saída. A saída, nesse caso, é igual à função de entrada. Portanto, estamos basicamente tentando recriar a imagem original após alguma compressão não linear generalizada.
A rede de codificação pode ser representada pela função de rede neural padrão passada por uma função de ativação, onde z é a dimensão latente.

A rede de decodificação pode ser representada da mesma maneira, mas com diferentes funções de peso, viés e potencial de ativação sendo usadas.

Assim, a função de perda pode então ser escrita em termos dessas duas funções, e é essa função de perda que usaremos para treinar a rede neural através do procedimento padrão de retropropagação.

Como já mencionado, a entrada e a saída devem ser as mesmas imagens o que não torna esse processo um aprendizado supervisionado ou não supervisionado; portanto, normalmente chamamos isso de aprendizado auto-supervisionado. O objetivo do autocodificador é selecionar nossas funções de codificação e decodificação de forma que sejá necessário o mínimo de informações para codificar a imagem, viabilizando sua regeneração na saída da rede.

Se usarmos poucos nós na camada de "gargalo", nossa capacidade de recriar a imagem original será limitada e regeneraremos imagens borradas ou irreconhecíveis se comparadas a entrada original. Se usarmos muitos nós, há pouco sentido em usar a compactação.

O caso da compactação é bastante simples, sempre que utilizamos a Netflix, por exemplo, os dados que são enviados ao usuário final são compactados. Quando chega ao seu computador, ele é passado através de um algoritmo de descompressão e, em seguida, exibido no seu computador. Isso é análogo a como arquivos zip funcionam, exceto que no caso da Netflix esse processo é feito internamente por meio de um algoritmo de streaming.

Autoencoder e massa de dados.

Como já mencionado, sabemos que os autoencoders seguem uma abordagem dita auto-supervisionada sendo utilizados principalmente para encontrar representações em um contexto não supervisionado, onde na maioria das vezes é necessário uma quantidade enorme de dados.

Uma das minhas principais dificuldades foi carregar um conjunto de dados com cerca de 500 mil imagens através de arrays numpy e extrair representações utilizando um autoencoder. Como proceder ?

A medida que o campo do aprendizado de máquina progride, problemas como a escassez de recursos computacionais tornam-se cada vez mais comuns.

Situações em que devemos carregar um grande conjunto de dados mas não há memória suficiente na máquina são extremamente comuns no campo de visão computacional.

Hoje, esse já é um dos principais desafios no campo de visão computacional, onde grandes conjuntos de dados de imagens e arquivos de vídeo precisam ser processados com uma quantidade limitada de recursos.

Diante desse cenário, vamos nos concentrar em como criar geradores de dados para carregar e processar imagens e por que utiliza-los com o framework Keras.

Todos os exemplos de código foram atualizados para a API do Keras 2.0.

Classe ImageDataGenerator

A classe ImageDataGenerator no Keras é uma ferramenta realmente valiosa, além de ser possível utilizá-la para treinamento/validação de divisão de imagens e também é útil para aumentar dados aplicando permutações aleatórias no conjunto de dados de imagens, em um esforço para reduzir o excesso de ajustes e melhorar o desempenho generalizado de seus modelos.

A função flow_from_directory é particularmente útil, pois permite carregar lotes de imagens de uma estrutura de diretório rotulada, em que o nome de cada subdiretório é o nome da classe para uma tarefa de classificação. Por exemplo, você pode usar uma estrutura de diretórios assim para seu conjunto de dados :

Exemplo de estrutura de diretórios exigida pelo Keras.

Nesse caso, cada subdiretório do diretório data contém imagens correspondentes a cada classe específicamente (ou seja, classes a e b), divididas em conjunto de Treino, Teste e Validação. O carregamento desse conjunto de dados com o ImageDataGenerator pode ser realizado da seguinte maneira:

datagen = ImageDataGenerator()
train_data = datagen.flow_from_directory('./train')

você verá a saída assim, indicando o número de imagens e classes descobertas:

Found 1000 images belonging to 2 classes.

No entanto, para o nosso problema, esse cenário não faz muito sentido. Como já mencionado anteriormente, queremos utilizar um autoencoder para encontrar características em dados não rotulados ou seja, estamos utilizando uma especie de auto-aprendizado onde temos apenas um diretório como nossas imagens para treino, e não existem classes ou rótulos.

Em um primeiro momento nossa alternativa é apontar nosso data generator para o diretório principal com todas as nossas imagens:

datagen = ImageDataGenerator()
train_data = datagen.flow_from_directory('./train')

No entanto você receberá a seguinte mensagem:

Found 0 images belonging to 0 classes.

Não é possível encontrar nenhuma classe porque o diretório train não possui subdiretórios. Sem as devidas classes, não é possível carregar suas imagens, como você vê na saída de log acima. No entanto, existe uma solução alternativa, onde é possível especificar o diretório pai do diretório de treino com alguns parâmetros específicos.

train_dir = '/train'train_datagen = ImageDataGenerator(
rescale=1./255,
data_format=’channels_last’)
test_datagen = ImageDataGenerator(
rescale=1./255,
data_format=’channels_last’)
train_generator = train_datagen.flow_from_directory(
train_dir,
target_size=(80, 80),
shuffle=True,
seed=42,
batch_size=batch_size,
class_mode=’input’)
test_generator = test_datagen.flow_from_directory(
train_dir,
target_size=(80, 80),
shuffle=True,
seed=42,
batch_size=batch_size,
class_mode=’input’)

Vamos as explicações:

  • rescale — Quando se trabalha com Machine Learning ou Deep Learning você aprende a importancia da normalização dos dados. Os valores de pixel nas imagens devem ser redimensionados antes de fornecer as imagens como entrada para um modelo de rede neural de aprendizado profundo durante o treinamento ou a avaliação do modelo. Acontece que para um conjunto de dados muito grande(no nosso caso 606.970 imagens com dimensões 80 x 80 pixel) essa operação se torna altamente custosa, ao dividirmos cada elemento de cada matriz por 255.
  • data_format — Especifica a ordem nas dimensões de entrada com relação aos canais das imagens. channel_last corresponde a entradas com forma (tamanho do batch, altura, largura, canais) enquanto channel_first corresponde a entradas com forma (tamanho do batch, canais, altura, largura).
  • train_dir — Diretório pai do diretório onde as imagens estão armazenadas.
  • target_size — Dimensões das minhas imagens (Altura e Largura).
  • shuffle — Se vamos embralhar esses dados.
  • seed — Semente aleatória opcional para embaralhamento e transformações.
  • batch_size — Tamanho do batch que é passado para a rede e aqui vale uma observação. O grande trunfo dos generators ao trabalharem com grandes datasets é a capacidade dele carregarrem os dados em batches e passarem esses batches gradualmente para o treinamento. Em outras palavras não será necessario carregar esses dados manualmente depois normaliza-los consumindo uma infinidade de recursos.
  • class_mode — Esse parâmetro é (talvez) o mais importante para o nosso autoencoder, ele determina o tipo de matriz de etiquetas que são retornadas: — "categorical" serão etiquetas codificadas em 2D de uma só peça, — "binary" serão etiquetas binárias 1D, "sparse" serão etiquetas inteiras 1D — "input" serão imagens idênticas para inserir imagens (usadas principalmente para trabalhar com autoencoders).

O que torna o parâmetro class_mode tão importante no contexto do autoencoder, é que ele será diretamente responsável por como os dados serão formatados para entrada, evitando o seguinte erro:

keras ValueError: output of generator should be a tuple (x,y, sample_weight) or (x,y). Found: numpy.array

Caso seja atribuido None ao parâmetro class_mode, nenhum rótulo será retornado (o gerador produzirá apenas lotes de dados de imagem, o que é útil para usar com model.predict_generator()). Observe que, no caso do parâmentro class_mode ser None, os dados ainda precisam residir em um subdiretório do diretório para que funcionem corretamente.

A seguir, vamos esclarecer como passar esses dados para o autoencoder da forma que parece mais sensata para o nosso propósito.

O Keras inclui três funções separadas que podem ser usadas para treinar seus próprios modelos:

  • .fit()
  • .fit_generator()
  • .train_on_batch()

Se você é novo no Keras e está Deep Lerning, pode se sentir confuso ao tentar determinar qual função deve usar — essa confusão só aumenta caso seja necessário trabalhar com seu próprio conjunto de dados.

O método fit()

A chamada para .fit raliza duas suposições principais aqui:

  • Todo o nosso conjunto de treinamento pode caber na RAM
  • Não há aumento de dados em andamento (ou seja, não há necessidade de geradores Keras)

Em vez disso, nossa rede será treinada com os dados brutos. Os próprios dados brutos caberão na memória — não precisamos mover lotes antigos de dados da RAM e mover novos lotes de dados para a RAM. Além disso, não manipularemos os dados de treinamento em tempo real usando o aumento de dados.

Requer dois geradores, um para os dados de treinamento e outro para validação. Felizmente, os dois devem retornar um tupple (entradas, destinos) e os dois podem ser instância da classe Sequence.

O método fit_generator()

Internamente, Keras está usando o seguinte processo ao treinar um modelo com .fit_generator:

  • Keras chama a função de gerador fornecida ao .fit_generator (no nosso caso o train_generator).
  • A função do gerador gera um batch para a função .fit_generator.
  • A função .fit_generator aceita o batch de dados, executa retropropagação e atualiza os pesos em nosso modelo.
  • Esse processo é repetido até atingirmos o número desejado de épocas.

O método .train_on_batch()

Caso sejá necessário um controle mais refinado sobre o treinamento dos modelos no Keras, é possível utilizar a função train_on_batch:

  • A função train_on_batch aceita um único batch de dados, executa a retropropagação e atualiza os parâmetros do modelo.
  • O batch de dados pode ser de tamanho arbitrário (ou seja, não requer que seja fornecido um tamanho de batch explícito).
  • Os dados em si podem ser gerados da maneira que você quiser. Esses dados podem ser imagens brutas no disco ou dados que foram modificados ou aumentados de alguma maneira.

Normalmente utilizamos a função train_on_batch quando os motivos para manter seu próprio iterador de dados de treinamento forem muito explícitos, ou o processo de iteração de dados for extremamente complexo e exigir um código mais personalizado. Em 99% das situações, não será necessário um controle tão refinado sobre o treinamento de seus modelos. Em vez disso, utilizar a função fit_generator bem parametrizada provavelmente resolve o problema.

Se houver duvidas quanto a utilizar a função .train_on_batch, provavelmente você não precisará utiliza-la.

Apresentados esses conceitos podemos passar ao treino do nosso autocodifcador. De forma "incomum" ao procedimento que é amplamente empregado, os nossos dados serão fonecidos diretamente pelo nosso train_generator, empregando a estratégia de batches definida anteriormente e não por uma matriz de imagens carregada manualmente do diretório local.

model.fit_generator(train_generator, 
steps_per_epoch=1000 // batch_size,
epochs=100,
validation_data=test_generator,
validation_steps=1000 // batch_size)

Note que para o nosso autocodifcador a função fit_generator foi suficiente.

Cabe ainda ressaltar que esse o método fit_generator recebe um parâmetro chamado steps_per_epoch, a idéia é que o gerador de dados do Keras deve executar um loop infinito — ele nunca deve retornar ou sair. Como a função de treino se destina a executar um loop infinito, o Keras não tem capacidade de determinar quando uma época começa e uma nova é iniciada. Portanto, definimos o valor de steps_per_epoch como o número total de pontos de dados de treinamento dividido pelo tamanho do lote, assim quando o Keras atinge esse número de etapas, sabe que deve iniciar uma nova época.

Agora já podemos iniciar o treinamento do autocodificador e validando a saida com a entrada e monitorando nossa função de perda.

O código para o autoencoder convolucional está disponível no github.

Conclusão

A idéia geral desse artigo foi esclarecer um pouco o conceito dos autocodificadores e qual a sua aplicação prática, além de destacar em que atividades eles são mais importantes. Um outro ponto de destaque foi a utilização deles de uma forma segura e gerenciada através da propria API do Keras de forma que fosse possível carregar com grande volume de dados utilizando a estratégia de batches.

Além disso tentei ajudar àqueles, que assim como eu, cometeram alguns erros comuns na definição dessa arquitetura de rede neural, como tentar carregar todos os dados iterando uma lista ou transformá-los arbitrariamente em arrays numpy.

Finalmente, espero que esse material tenha sido útil e faça sentido pra você, principalmente aos iniciantes. Além disso nas referências do artigo é possível encontrar um material muito útil utilizado para elaboração desse artigo que pode te ajudar a ampliar seus conhecimentos na área.

Lembrando que qualquer feedback, seja positivo ou negativo é so entrar em contato através do meu twiter, linkedin ou nos comentário aqui em baixo :)

Referências

https://machinelearningmastery.com/how-to-normalize-center-and-standardize-images-with-the-imagedatagenerator-in-keras/

--

--

Toni Esteves

I’m a Computer Scientist and Quantitative researcher. This is my notepad for applied ML / DS / Deep Learning topics. Follow me on Twitter for more!