Cos'è un AutoEncoder? Guida completa

Computer Vision 101

Cos'è un AutoEncoder? Guida completa
Condividi:

Un autoencoder è un'architettura neurale utilizzata per apprendere una rappresentazione compressa dell'input e ricostruirlo a partire da esso.

È un'architettura storica, molto usata nel mondo ML. In computer vision, prima dell'avvento dei Transformer e dei Diffusion Model, era lo standard de facto per la riduzione della dimensionalità.

L'Architettura di un AutoEncoder

E' costituito da un blocco Encoder, CNN o Transformer che sia, seguito da una componente cruciale chiamata Collo di Bottiglia (Bottleneck) e infine da un Decoder.

Ecco come interagiscono:

  1. Encoder: Prende il dato originale (ad esempio un'immagine ad alta risoluzione) e lo comprime progressivamente, riducendone le dimensioni spaziali ma aumentandone la profondità delle feature.
  2. Bottleneck (Spazio Latente): È il punto più "stretto" della rete. Qui risiede il vettore latente, una rappresentazione compressa che contiene solo le informazioni essenziali dell'input, scartando il rumore e i dettagli superflui.
  3. Decoder: Speculare all'Encoder, prende il vettore latente e tenta di ricostruire il dato originale, riportandolo alle dimensioni iniziali.

Il Processo di Addestramento

L'addestramento di un AutoEncoder avviene in modalità Self-Supervised (auto-supervisionata).

A differenza della classificazione classica, qui non abbiamo bisogno di etichette umane (label). L'obiettivo della rete è l'identità: l'input funge anche da target! Durante il training, la rete cerca di minimizzare la differenza tra l'immagine in ingresso e quella ricostruita in uscita. Costringendo i dati a passare attraverso il bottleneck, impediamo alla rete di imparare semplicemente a "copiare" pixel per pixel (memorizzazione), forzandola invece ad apprendere le feature strutturali (forme, pattern, semantica) per poter ricostruire l'immagine partendo da poche informazioni.

Casi d'Uso Pratici

Praticamente un Autoencoder può servire a:

  • Riduzione della Dimensionalità: Simile alla PCA (Principal Component Analysis), ma non lineare e molto più potente. Utile per visualizzare dati complessi in 2D o 3D. A partire dal modello originale, si considera esclusivamente l'Encoder: il bottleneck può essere utilizzato come un tensore efficiente e ricco di informazioni da unire ad un blocco per la predizione.
  • Denoising (Rimozione Rumore): Si addestra la rete fornendo in input immagini "sporche" o rumorose e imponendo come target l'immagine pulita. L'Autoencoder impara a ignorare il rumore statico.
  • Anomaly Detection: Molto usato nell'industria. Se addestri un AE solo su prodotti "sani", quando gli mostri un prodotto difettoso, non riuscirà a ricostruirlo bene (avrà un errore di ricostruzione alto), segnalando così l'anomalia.
  • Compressione Dati: Per trasmettere meno dati salvando solo il vettore latente (anche se per la compressione standard come JPEG si usano altri algoritmi, gli AE sono promettenti per la compressione semantica).

Implementazione e Loss Function di un Autoencoder

La funzione di costo (Loss Function) standard e semplice per le immagini è solitamente l'errore quadratico medio (MSE Loss), che calcola la media delle differenze al quadrato tra i pixel originali e quelli ricostruiti.

Ecco un esempio essenziale di un Autoencoder convoluzionale in PyTorch:

import torch
import torch.nn as nn

class AutoEncoder224(nn.Module):
    def __init__(self):
        super(AutoEncoder224, self).__init__()
        
        # ENCODER 
        # L'obiettivo è ridurre le dimensioni spaziali (H, W) e aumentare la profondità (Channels)
        # Input atteso: [Batch_Size, 3, 224, 224] (Immagini RGB)
        
        self.encoder = nn.Sequential(
            # Layer 1: Input [B, 3, 224, 224] -> Output [B, 32, 112, 112]
            nn.Conv2d(3, 32, kernel_size=3, stride=2, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            
            # Layer 2: Input [B, 32, 112, 112] -> Output [B, 64, 56, 56]
            nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            
            # Layer 3: Input [B, 64, 56, 56] -> Output [B, 128, 28, 28]
            nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            
            # Layer 4: Input [B, 128, 28, 28] -> Output [B, 256, 14, 14]
            nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            
            # Layer 5 (Verso il Bottleneck): Input [B, 256, 14, 14] -> Output [B, 512, 7, 7]
            nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU()
        )
        
        # BOTTLENECK 
        # A questo punto, l'immagine originale 3x224x224 (150,528 valori per pixel)
        # è stata compressa in una rappresentazione compressa di shape:
        # [Batch_Size, 512, 7, 7] (25,088 valori per "immagine compressa") !!
        # Abbiamo una compressione di circa 6 volte, mantenendo le feature più astratte.

        # DECODER 
        # L'obiettivo è fare l'operazione inversa: upsampling per tornare a 224x224.
        # Usiamo ConvTranspose2d. La combinazione kernel=3, stride=2, padding=1, output_padding=1
        # serve a raddoppiare esattamente le dimensioni spaziali.
        
        self.decoder = nn.Sequential(
            # Layer 1 (Up): Input [B, 512, 7, 7] -> Output [B, 256, 14, 14]
            nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, output_padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            
            # Layer 2 (Up): Input [B, 256, 14, 14] -> Output [B, 128, 28, 28]
            nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, output_padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            
            # Layer 3 (Up): Input [B, 128, 28, 28] -> Output [B, 64, 56, 56]
            nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, output_padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            
            # Layer 4 (Up): Input [B, 64, 56, 56] -> Output [B, 32, 112, 112]
            nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, output_padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            
            # Layer 5 (Final Output): Input [B, 32, 112, 112] -> Output [B, 3, 224, 224]
            # Nota: output_padding=1 non serve qui perché 112*2 = 224 esatto con kernel 3 e padding 1.
            # Riduciamo i canali a 3 (RGB)
            nn.ConvTranspose2d(32, 3, kernel_size=3, stride=2, padding=1, output_padding=1),
            # Usiamo Sigmoid finale per forzare l'output tra [0, 1], assumendo che l'input sia normalizzato ;)
            nn.Sigmoid() 
        )

    def forward(self, x):
        # Passaggio attraverso l'encoder
        encoded = self.encoder(x)
        # Passaggio attraverso il decoder
        decoded = self.decoder(encoded)
        return decoded

    # Metodo helper per ispezionare la shape del bottleneck senza fare un forward completo
    def get_latent_shape(self, x):
        with torch.no_grad():
            encoded = self.encoder(x)
            return encoded.shape

if __name__ == '__main__':
    # Creiamo un tensore dummy che simula un batch di 4 immagini RGB 224x224
    dummy_input = torch.randn(4, 3, 224, 224)
    
    # Istanziamo il modello
    model = Autoencoder224()
    
    # Eseguiamo un forward pass
    output = model(dummy_input)
    
    print(f"Shape dell'input originale: {dummy_input.shape}")
    
    # Verifica della shape del bottleneck
    latent_shape = model.get_latent_shape(dummy_input)
    print(f"Shape del collo di bottiglia (Spazio Latente): {latent_shape}")
    # Output atteso: torch.Size([4, 512, 7, 7])
    
    print(f"Shape dell'output ricostruito: {output.shape}")
    # Output atteso: torch.Size([4, 3, 224, 224])

    # Verifica che le dimensioni siano tornate quelle originali
    assert dummy_input.shape == output.shape
    print("\nTest superato: L'immagine ricostruita ha le stesse dimensioni dell'input.")

Curiosità sugli Autoencoder

Se rimuoviamo tutte le funzioni di attivazione non lineari (come ReLU o Sigmoid) tra i vari strati, riducendo la rete a semplici moltiplicazioni di matrici, l'Autoencoder finisce per comportarsi quasi esattamente come la PCA (Analisi delle Componenti Principali)! In pratica, sotto queste condizioni limitanti, lo spazio latente impara a proiettare i dati sugli stessi assi di varianza della PCA. Questo ci fa capire che l'Autoencoder è, di fatto, una generalizzazione non-lineare e molto più potente delle tecniche statistiche classiche.

Il loro peggior nemico è una capacità eccessiva. Se il "collo di bottiglia" (lo spazio latente) è troppo grande, ovvero ha troppe dimensioni rispetto alla complessità dei dati, la rete diventa "pigra". Invece di imparare a estrarre pattern significativi e comprimere l'informazione, impara semplicemente a copiare l'input in output (impara la funzione identità f(x)=xf(x)=x a memoria).

Gli Autoencoder un tempo erano usati non per generare immagini, ma per permettere alle reti profonde di esistere. Prima dell'invenzione di tecniche moderne come la Batch Normalization o le connessioni residuali (ResNet), era quasi impossibile addestrare reti molto profonde da zero. I ricercatori usavano quindi gli Stacked Autoencoders per addestrare la rete "a strati" (layer-wise pre-training): si addestrava un piccolo autoencoder, si congelavano i pesi, se ne aggiungeva un altro sopra, e così via. Solo alla fine si usava la rete per la classificazione. Di fatto, gli Autoencoder hanno tenuto in vita il Deep Learning durante i suoi anni più difficili :)

Mauro Sciancalepore - Notizie AI, Deep Learning e Ricerca

Resta aggiornato sulle ultime notizie di Intelligenza Artificiale e Deep Learning. Approfondimenti completi sulla ricerca e stato dell'arte.

© 2026 mauroscia.it
Tutti i diritti riservati.