O que é geração de dados com autoencoders
Geração de dados é uma tarefa não supervisionada fascinante. Ela cria novos exemplos similares aos dados originais. Primeiramente, o modelo aprende a estrutura escondida nos dados. Em seguida, ele gera amostras sintéticas realistas. Por exemplo, podemos criar novas imagens de dígitos manuscritos. Autoencoders são redes neurais ideais para essa finalidade. Eles comprimem dados em uma representação menor (codificação). Depois, reconstroem os dados a partir dessa codificação. O autoencoder vanilla é a versão mais simples dessa arquitetura. Ele tem apenas uma camada oculta no codificador e decodificador. Diferentemente de modelos complexos, ele é fácil de entender. Além disso, seu treinamento é estável e rápido.
Essa técnica tem várias aplicações práticas importantes. Primeiramente, ela pode aumentar conjuntos de dados pequenos. Outra aplicação é a remoção de ruído de imagens. Além disso, autoencoders detectam anomalias por erro de reconstrução. Por conseguinte, eles são usados em segurança e medicina. A geração de dados também ajuda em privacidade diferencial. Portanto, autoencoders são ferramentas versáteis e poderosas. Este guia foca no autoencoder vanilla para iniciantes. Vamos explorar sua arquitetura, hiperparâmetros e fórmulas. No final, um exemplo prático com o dataset MNIST será apresentado.
Arquitetura do autoencoder vanilla
O autoencoder vanilla tem duas partes principais simétricas. Primeiramente, o codificador comprime a entrada em um código. Ele é uma rede feedforward com uma camada oculta. A função de ativação ReLU é comumente usada no codificador. A camada de gargalo (bottleneck) tem menos neurônios que a entrada. Essa compressão força o modelo a aprender características essenciais. A fórmula da codificação é: \(h = f(W_e x + b_e)\). Aqui, \(f\) é a função de ativação escolhida. Em seguida, o decodificador reconstrói os dados a partir de \(h\). Ele também tem uma camada oculta com ativação ReLU. A saída final usa ativação sigmoide para dados normalizados. A reconstrução é calculada como: \(\hat{x} = g(W_d h + b_d)\). O objetivo é minimizar a diferença entre \(x\) e \(\hat{x}\). Essa arquitetura é treinada com retropropagação normal. Portanto, é um ótimo ponto de partida para iniciantes.
O autoencoder vanilla é chamado de “vanilla” por ser básico. Ele não possui regularizações como dropout ou esparsidade. Contudo, sua simplicidade facilita o entendimento do conceito. A camada de gargalo é o coração do autoencoder. Quanto menor o gargalo, maior a compressão forçada. Isso pode levar a perda de informações relevantes. Por outro lado, um gargalo muito grande não comprime bem. O equilíbrio é encontrado experimentalmente para cada problema. A simetria entre codificador e decodificador é comum. Porém, não é obrigatória para o funcionamento do modelo. Autoencoders mais avançados usam camadas convolucionais. Para imagens, eles capturam melhor padrões espaciais. O vanilla, entretanto, trata cada pixel como uma característica. Isso funciona bem para dados tabulares e imagens pequenas. O MNIST (28×28) é um exemplo clássico de aplicação.
Hiperparâmetros e fórmulas matemáticas
Vários hiperparâmetros controlam o autoencoder vanilla. Primeiramente, o tamanho do gargalo é o mais crítico. Ele define quantas características latentes serão aprendidas. A taxa de aprendizado afeta a convergência do treinamento. Além disso, o número de épocas determina o tempo de treino. O tamanho do lote (batch) influencia a estabilidade do gradiente. A escolha da função de ativação também é importante. ReLU é comum nas camadas ocultas do codificador e decodificador. A saída do decodificador usa sigmoide para dados entre 0 e 1. A regularização L2 pode ser aplicada para evitar overfitting. Outro hiperparâmetro é o otimizador (Adam, SGD, etc.). Adam é geralmente a melhor escolha para autoencoders. A inicialização dos pesos afeta a qualidade final. Portanto, todos esses parâmetros precisam ser ajustados. A validação cruzada ajuda a encontrar os melhores valores.
A função de perda mais comum é o erro quadrático médio: \(MSE = \frac{1}{N}\sum_{i=1}^{N} ||x_i – \hat{x}_i||^2\). Para dados binários, a entropia cruzada binária é preferida: \(L = -\frac{1}{N}\sum_{i=1}^{N} [x_i \log(\hat{x}_i) + (1-x_i)\log(1-\hat{x}_i)]\). A retropropagação calcula os gradientes dessa perda. A atualização dos pesos segue a regra do gradiente descendente: \(w_{novo} = w_{velho} – \eta \frac{\partial L}{\partial w}\). O treinamento continua até a perda estabilizar. Após o treinamento, o decodificador pode gerar novos dados. Basta amostrar pontos aleatórios no espaço latente. Contudo, o autoencoder vanilla não garante um espaço contínuo. Para geração mais controlada, Variational Autoencoders são melhores. Ainda assim, o vanilla é um excelente primeiro passo. Ele ensina os conceitos fundamentais de compressão e reconstrução.
Enunciado do exemplo clássico (dígitos MNIST)
Você recebeu imagens de dígitos manuscritos do MNIST. Cada imagem tem 28×28 pixels em escala de cinza. Seu objetivo é treinar um autoencoder vanilla para gerar novos dígitos. Primeiramente, use 10 neurônios no gargalo (espaço latente). Em seguida, treine o modelo para reconstruir as imagens originais. Depois, visualize 10 imagens originais e suas reconstruções. Por fim, gere novos dígitos amostrando do espaço latente. Use duas amostras aleatórias para interpolação linear. Exiba os resultados em dois gráficos organizados. O código abaixo resolve este enunciado completamente. Ele roda no Google Colab sem necessidade de ajustes. Portanto, boa prática! Autoencoders geram dados sintéticos realistas.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
import numpy as np import matplotlib.pyplot as plt import tensorflow as tf from tensorflow.keras.layers import Input, Dense from tensorflow.keras.models import Model from tensorflow.keras.datasets import mnist from tensorflow.keras.optimizers import Adam # Carrega e pré-processa o MNIST (x_train, _), (x_test, _) = mnist.load_data() x_train = x_train.astype('float32') / 255.0 x_test = x_test.astype('float32') / 255.0 x_train = x_train.reshape(-1, 784) x_test = x_test.reshape(-1, 784) print(f"Treino: {x_train.shape[0]} imagens") print(f"Teste: {x_test.shape[0]} imagens") print(f"Cada imagem tem {x_train.shape[1]} pixels") # Hiperparâmetros tamanho_gargalo = 10 taxa_aprendizado = 0.001 epocas = 30 tamanho_lote = 128 # Arquitetura do autoencoder vanilla # Codificador entrada = Input(shape=(784,), name='entrada') codificado = Dense(256, activation='relu', name='encoder_1')(entrada) gargalo = Dense(tamanho_gargalo, activation='relu', name='bottleneck')(codificado) # Decodificador decodificado = Dense(256, activation='relu', name='decoder_1')(gargalo) saida = Dense(784, activation='sigmoid', name='saida')(decodificado) # Modelo completo autoencoder = Model(entrada, saida, name='autoencoder_vanilla') autoencoder.compile(optimizer=Adam(learning_rate=taxa_aprendizado), loss='binary_crossentropy') # Resumo do modelo print("\n" + "=" * 60) print("Arquitetura do Autoencoder Vanilla:") print("=" * 60) autoencoder.summary() # Treinamento print("\n" + "=" * 60) print("Iniciando treinamento...") print("=" * 60) historico = autoencoder.fit(x_train, x_train, epochs=epocas, batch_size=tamanho_lote, validation_data=(x_test, x_test), verbose=1) # Visualização das reconstruções n = 10 # número de dígitos para mostrar imagens_teste = x_test[:n] reconstrucoes = autoencoder.predict(imagens_teste) plt.figure(figsize=(20, 4)) for i in range(n): # Original ax = plt.subplot(2, n, i + 1) plt.imshow(imagens_teste[i].reshape(28, 28), cmap='gray') plt.title(f"Original {i}") plt.axis('off') # Reconstrução ax = plt.subplot(2, n, i + 1 + n) plt.imshow(reconstrucoes[i].reshape(28, 28), cmap='gray') plt.title(f"Reconstruído {i}") plt.axis('off') plt.suptitle('Autoencoder Vanilla: Originais vs Reconstruções', fontsize=14) plt.tight_layout() plt.show() # Geração de novos dígitos via interpolação no espaço latente # Extrai o modelo do codificador e decodificador separadamente codificador = Model(entrada, gargalo, name='codificador') # Decodificador separado decodificador_input = Input(shape=(tamanho_gargalo,), name='dec_input') decodificador_output = autoencoder.layers[-2](decodificador_input) decodificador_output = autoencoder.layers[-1](decodificador_output) decodificador = Model(decodificador_input, decodificador_output, name='decodificador') # Escolhe dois dígitos reais e interpola no espaço latente indices = [0, 5] # dois dígitos diferentes ponto_a = codificador.predict(imagens_teste[indices[0]].reshape(1, 784), verbose=0) ponto_b = codificador.predict(imagens_teste[indices[1]].reshape(1, 784), verbose=0) # Interpolação linear entre os dois pontos interpolacoes = [] alphas = np.linspace(0, 1, 10) for alpha in alphas: ponto_interpolado = (1 - alpha) * ponto_a + alpha * ponto_b imagem_gerada = decodificador.predict(ponto_interpolado, verbose=0) interpolacoes.append(imagem_gerada.reshape(28, 28)) # Visualização da interpolação plt.figure(figsize=(15, 4)) for i, img in enumerate(interpolacoes): plt.subplot(1, 10, i + 1) plt.imshow(img, cmap='gray') plt.title(f"{alphas[i]:.1f}", fontsize=9) plt.axis('off') plt.suptitle('Interpolação no espaço latente entre dois dígitos reais', fontsize=14) plt.tight_layout() plt.show() # Geração aleatória a partir de ruído gaussiano print("\n" + "=" * 60) print("Gerando novos dígitos a partir de ruído aleatório:") print("=" * 60) n_gerados = 10 pontos_aleatorios = np.random.randn(n_gerados, tamanho_gargalo) * 2.0 imagens_geradas = decodificador.predict(pontos_aleatorios, verbose=0) plt.figure(figsize=(15, 4)) for i in range(n_gerados): plt.subplot(1, n_gerados, i + 1) plt.imshow(imagens_geradas[i].reshape(28, 28), cmap='gray') plt.axis('off') plt.suptitle('Dígitos gerados aleatoriamente a partir do espaço latente', fontsize=14) plt.tight_layout() plt.show() # Gráfico da perda durante o treinamento plt.figure(figsize=(10, 5)) plt.plot(historico.history['loss'], label='Treino', linewidth=2) plt.plot(historico.history['val_loss'], label='Validação', linewidth=2) plt.xlabel('Época') plt.ylabel('Perda (entropia cruzada binária)') plt.title('Curva de aprendizado do autoencoder vanilla') plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() # Análise final print("\n" + "=" * 60) print("Análise dos resultados:") print("=" * 60) perda_final = historico.history['val_loss'][-1] print(f"Perda final na validação: {perda_final:.4f}") print("\nObservações:") print("- O autoencoder aprendeu a comprimir dígitos em 10 números.") print("- As reconstruções são reconhecíveis, mas um pouco borradas.") print("- A interpolação gera transições suaves entre dois dígitos.") print("- A geração aleatória produz novos dígitos plausíveis.") print("- Para qualidade superior, use Variational Autoencoder (VAE).") |
Esse código treina um autoencoder vanilla no dataset MNIST. Primeiramente, as imagens são normalizadas entre 0 e 1. O gargalo tem apenas 10 neurônios para forçar compressão. As reconstruções mostram que o modelo aprendeu bem. A interpolação entre dois dígitos gera transições suaves. Além disso, a geração aleatória produz novos dígitos plausíveis. A curva de perda confirma que não houve overfitting severo. Contudo, as imagens geradas são um pouco borradas. Para melhor qualidade, Variational Autoencoders são recomendados. Portanto, o autoencoder vanilla é um excelente ponto de partida. Parabéns por gerar novos dados sintéticos com sucesso!