Salta al contenuto principale

70 - Autoencoder: comprimere, ricostruire e capire cos'è uno spazio latente

Un autoencoder comprime e ricostruisce senza etichette: lo confronti con la PCA, misuri il guadagno della non linearità e vedi perché non genera immagini.

Cosa ne pensi di questo articolo?

70 - Autoencoder: comprimere, ricostruire e capire cos'è uno spazio latente
Machine Learning Engineerfedericocalo.dev

In Breve

Un autoencoder comprime e ricostruisce senza etichette: lo confronti con la PCA, misuri il guadagno della non linearità e vedi perché non genera immagini.

Un autoencoder è una rete addestrata a un compito che sembra inutile: ricostruire il proprio ingresso. Il trucco sta nel fatto che nel mezzo della rete c'è un collo di bottiglia, uno strato con pochissimi numeri, attraverso il quale tutta l'informazione deve passare. Per ricostruire bene, la rete è costretta a decidere che cosa vale la pena conservare di un'immagine: impara a comprimere e, come effetto collaterale, impara una rappresentazione dei dati. È il mattone da cui nasce il VAE, il tema delle prossime lezioni, quindi vale la pena capirlo bene, con i suoi limiti. In questa lezione lo costruisci in PyTorch su cifre scritte a mano, lo confronti con una tecnica classica (la PCA) di cui scopri che è, in un caso preciso, la stessa cosa, misuri quanto aiuta la non linearità, lo usi per togliere rumore e infine scopri perché non basta per generare immagini nuove.

Le parole di questa lezione

L'encoder trasforma l'ingresso x in un vettore corto z, il codice; il decoder trasforma z in una ricostruzione x̂ che deve somigliare a x. Lo spazio latente è lo spazio dei possibili codici: «latente» significa nascosto, perché non è mai osservato direttamente nei dati ma è una rappresentazione che la rete ha scelto, e con k = 2 numeri è un piano. Il collo di bottiglia (bottleneck) è lo strato più stretto: se fosse largo quanto l'ingresso la rete potrebbe copiare senza imparare nulla. L'errore di ricostruzione è la differenza fra x e x̂, qui l'errore quadratico medio (MSE) sui pixel. La PCA trova le k direzioni lungo le quali i dati variano di più e proietta i dati su di esse, ed è lineare. Un autoencoder denoising riceve in ingresso un'immagine corrotta da rumore e deve ricostruire quella pulita. Tutto questo è apprendimento non supervisionato (o auto-supervisionato): non servono etichette, l'ingresso stesso fa da bersaglio.

L'analogia: descrivere un volto per telefono

Devi descrivere a un amico il volto di una persona usando soltanto dieci parole, e l'amico deve poi disegnarlo. Non puoi dire il colore di ogni singolo pixel: devi scegliere le caratteristiche che contano («rotondo», «occhiali», «capelli corti», «sorridente»). Le dieci parole sono il codice z; tu sei l'encoder, l'amico è il decoder. Più le parole sono poche, più il disegno sarà approssimato; più tu e l'amico vi allenate insieme, più il vostro vocabolario diventa efficiente. Il «vocabolario» che emerge è lo spazio latente. Nota però il limite: se l'amico, a caso, riceve dieci parole che nessuno ha mai detto (ad esempio «rotondo» e «quadrato» insieme), il disegno sarà senza senso. Torneremo su questo punto.

Schema di un autoencoder: l'ingresso di 64 pixel passa attraverso un encoder che lo riduce a un codice di k numeri e un decoder che lo riespande a 64 pixel; la perdita confronta la ricostruzione con l'ingresso Autoencoder: squeeze through a bottleneck, then rebuild x 64 128 z k 128 x̂ 64 ENCODER DECODER code (latent vector): k << 64 numbers loss = how different x̂ is from x (for example mean squared error over the 64 pixels) No labels needed: the input is also the target.
Schema di un autoencoder: l'ingresso di 64 pixel passa attraverso un encoder che lo riduce a un codice di k numeri e un decoder che lo riespande a 64 pixel; la perdita confronta la ricostruzione con l'ingresso

Nel disegno le immagini hanno 64 pixel e lo strato nascosto è da 128, come nel codice che segue; il codice z è l'unico strato strettissimo. Il disegno ricorda soprattutto una cosa: nessuna etichetta compare da nessuna parte, la perdita confronta la ricostruzione con l'ingresso.

La matematica

Siano E l'encoder e D il decoder, con parametri da apprendere. Per un'immagine x di d pixel (qui d = 64):

z  = E(x)                     (codice, k numeri, k ≪ d)
x̂ = D(z) = D(E(x))            (ricostruzione, d numeri)
L  = (1/d) · Σ_pixel (x_i − x̂_i)²    (MSE, mediato sulle immagini)

Non c'è nessuna etichetta: l'addestramento è una normale discesa del gradiente su L. La capacità di ricostruire dipende dal valore di k: se k è piccolo, il codice può conservare solo l'essenziale.

Un caso speciale: l'autoencoder lineare è la PCA

Se encoder e decoder sono lineari (z = W₁ x, x̂ = W₂ z), il problema diventa trovare due matrici il cui prodotto W₂W₁ ha rango al più k e minimizza l'errore quadratico di ricostruzione. È un problema classico, risolto dal teorema di Eckart-Young: la migliore approssimazione di rango k si ottiene con la decomposizione ai valori singolari (SVD) tenendo solo i k valori singolari più grandi, ed è quello che fa la PCA (la documentazione di scikit-learn la descrive come SVD dei dati centrati). Quindi un autoencoder lineare ben addestrato converge a un sottospazio equivalente a quello della PCA (risultato di Baldi e Hornik, 1989; entrambi i lavori sono citazioni bibliografiche, non consultate online per questa lezione). L'errore minimo ha una formula chiusa: la somma dei quadrati dei valori singolari scartati, divisa per il numero di numeri nel dataset:

MSE_k = ( Σ_{i>k} s_i² ) / (N · d)

Possiamo verificarlo con il codice, senza addestrare niente. Il primo blocco usa load_digits di scikit-learn al posto di MNIST: 1797 immagini 8×8 con valori da 0 a 16, divise 70/30 con stratificazione (1257 per l'addestramento, 540 per il test) e riportate in [0, 1]. Il download di MNIST non è stato eseguito per questa versione, quindi i numeri assoluti non sono confrontabili con quelli della versione precedente della lezione.

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split

torch.set_num_threads(2)
X, y = load_digits(return_X_y=True)                                # 1797 digits, 8x8 pixels, values 0..16
Xtr, Xte, ytr, yte = train_test_split(X / 16.0, y, test_size=0.3, random_state=0, stratify=y)
Xtr, Xte = torch.tensor(Xtr, dtype=torch.float32), torch.tensor(Xte, dtype=torch.float32)
D = Xtr.shape[1]                                                   # 64 pixels

# --- the best possible LINEAR compression: PCA (computed with an SVD, no training) ---
mean = Xtr.mean(dim=0)
U, S, Vt = torch.linalg.svd(Xtr - mean, full_matrices=False)       # rows of Vt = principal directions

def pca_mse(k):
    V = Vt[:k]                                                     # (k, 64)
    codes = (Xte - mean) @ V.T                                     # compress to k numbers
    recon = codes @ V + mean                                       # decompress
    return F.mse_loss(recon, Xte).item()

print(Xtr.shape, Xte.shape)
print("baseline: predict the mean image  MSE", round(F.mse_loss(mean.expand_as(Xte), Xte).item(), 4))
for k in (2, 8, 16):
    print(f"PCA with k={k:2d}:  test MSE {pca_mse(k):.4f}")
torch.Size([1257, 64]) torch.Size([540, 64])
baseline: predict the mean image  MSE 0.0739
PCA with k= 2:  test MSE 0.0528
PCA with k= 8:  test MSE 0.0245
PCA with k=16:  test MSE 0.0115

torch.linalg.svd calcola la decomposizione di X − media: le righe di Vt sono le direzioni principali. pca_mse(k) comprime con le prime k direzioni (codes = (Xte − mean) @ V.T), ricostruisce (codes @ V + mean) e misura l'MSE sul test. La baseline è predire sempre l'immagine media (MSE 0.0739): ogni compressione deve fare meglio. Con k = 2 numeri la PCA scende a 0.0528 (poco), con 8 a 0.0245, con 16 a 0.0115: una compressione di 64/16 = 4 volte con un errore pari a circa un sesto della baseline.

# PCA error from the singular values alone (Eckart-Young): MSE_k = sum of the discarded squared singular values / (N * 64)
N = Xtr.shape[0]
for k in (2, 8, 16):
    predicted = (S[k:] ** 2).sum().item() / (N * D)                # no reconstruction needed
    V = Vt[:k]
    recon = ((Xtr - mean) @ V.T) @ V + mean
    measured = F.mse_loss(recon, Xtr).item()                        # the TRAINING reconstruction error, computed directly
    print(f"k={k:2d}  from singular values {predicted:.5f}   measured {measured:.5f}")
k= 2  from singular values 0.05233   measured 0.05233
k= 8  from singular values 0.02376   measured 0.02376
k=16  from singular values 0.01093   measured 0.01093

Il codice calcola l'errore con la formula ((S[k:]**2).sum() / (N·d)) e poi, per controllo, ricostruisce davvero le immagini di addestramento e misura l'MSE: coincidono alla quinta cifra decimale. Attenzione: questi sono errori sull'addestramento e quelli del blocco precedente sul test, quindi hanno valori vicini ma non identici (0.02376 contro 0.0245 a k = 8). Ora la formula di Eckart-Young non è più un teorema da fidarsi: è un numero che hai visto uscire uguale.

L'autoencoder in PyTorch, lineare e non lineare

Costruiamo due autoencoder: uno lineare (un solo strato in discesa e uno in salita, nessuna non linearità) e uno non lineare (64 → 128 → k → 128 → 64 con ReLU e una sigmoide in uscita). Li alleniamo 300 epoche per tre valori di k, con Adam a 0.001 e mini-batch da 64, e confrontiamo gli errori sul test con quelli della PCA. Nel costruttore, se linear=True, encoder e decoder sono un solo nn.Linear ciascuno. Nel forward l'uscita passa da una sigmoide, per tenere i pixel in (0, 1), solo nel caso non lineare: una sigmoide in uscita rende la rete non lineare e romperebbe il confronto con la PCA. Il parametro noise di train_ae serve per l'esperimento successivo.

class AutoEncoder(nn.Module):
    def __init__(self, k, hidden=128, linear=False):
        super().__init__()
        self.linear = linear
        act = nn.ReLU()
        if linear:                                                 # one linear map down, one linear map up
            self.enc = nn.Linear(D, k)
            self.dec = nn.Linear(k, D)
        else:
            self.enc = nn.Sequential(nn.Linear(D, hidden), act, nn.Linear(hidden, k))
            self.dec = nn.Sequential(nn.Linear(k, hidden), act, nn.Linear(hidden, D))
    def forward(self, x):
        z = self.enc(x)                                            # the code: k numbers
        out = self.dec(z)
        return (out if self.linear else torch.sigmoid(out)), z     # nonlinear AE: pixels squashed into (0, 1)

def train_ae(k, linear=False, epochs=300, seed=0, noise=0.0):
    torch.manual_seed(seed)
    model = AutoEncoder(k, linear=linear)
    opt = torch.optim.Adam(model.parameters(), lr=1e-3)
    for _ in range(epochs):
        perm = torch.randperm(len(Xtr))
        for i in range(0, len(Xtr), 64):
            x = Xtr[perm[i:i + 64]]
            x_in = (x + noise * torch.randn_like(x)).clamp(0, 1) if noise > 0 else x   # denoising: corrupt the INPUT only
            recon, _ = model(x_in)
            loss = F.mse_loss(recon, x)                            # ... but compare with the CLEAN image
            opt.zero_grad(); loss.backward(); opt.step()
    return model

@torch.no_grad()
def test_mse(model):
    return F.mse_loss(model(Xte)[0], Xte).item()

for k in (2, 8, 16):
    lin = train_ae(k, linear=True)
    non = train_ae(k)
    print(f"k={k:2d}   linear AE {test_mse(lin):.4f}   nonlinear AE {test_mse(non):.4f}   (PCA {pca_mse(k):.4f})")
k= 2   linear AE 0.0532   nonlinear AE 0.0355   (PCA 0.0528)
k= 8   linear AE 0.0251   nonlinear AE 0.0115   (PCA 0.0245)
k=16   linear AE 0.0116   nonlinear AE 0.0058   (PCA 0.0115)

L'autoencoder lineare si avvicina alla PCA: 0.0251 contro 0.0245 (k = 8) e 0.0116 contro 0.0115 (k = 16); con k = 2 è 0.0532 contro 0.0528. Resta sempre un filo sopra la PCA, come ci si aspetta da un ottimizzatore che dopo un numero finito di epoche non ha ancora raggiunto l'ottimo esatto. L'autoencoder non lineare fa nettamente meglio: a parità di k, l'errore è più basso del 33% (0.0355 contro 0.0528 con 2 numeri), del 53% (0.0115 contro 0.0245 con 8) e del 50% (0.0058 contro 0.0115 con 16). È il guadagno della non linearità: i dati stanno su una «superficie curva» dentro lo spazio dei pixel, non su un sottospazio piatto, e una rete non lineare può seguirla. Un seme e 540 esempi di test: l'ordine PCA, lineare, non lineare è netto, la seconda cifra decimale no.

Togliere il rumore: l'autoencoder denoising

Una variante sorprendentemente utile: si corrompe l'ingresso con rumore ma si chiede di ricostruire l'immagine pulita. Poiché il rumore casuale non si può comprimere in k numeri, il collo di bottiglia lo scarta. Il rumore è gaussiano con deviazione 0.5, sommato ai pixel e poi tagliato in [0, 1]; la riga loss = F.mse_loss(recon, x) in train_ae confronta con l'immagine pulita anche quando l'ingresso è rumoroso.

# Denoising: the network sees a NOISY image and must reconstruct the CLEAN one.
torch.manual_seed(1)
noisy_test = (Xte + 0.5 * torch.randn_like(Xte)).clamp(0, 1)
plain = train_ae(16)                                  # trained on clean images
denoising = train_ae(16, noise=0.5)                   # trained on noisy inputs -> clean targets
with torch.no_grad():
    print("noisy input vs clean        MSE:", round(F.mse_loss(noisy_test, Xte).item(), 4))
    print("plain AE on noisy input     MSE:", round(F.mse_loss(plain(noisy_test)[0], Xte).item(), 4))
    print("denoising AE on noisy input MSE:", round(F.mse_loss(denoising(noisy_test)[0], Xte).item(), 4))
noisy input vs clean        MSE: 0.1175
plain AE on noisy input     MSE: 0.0877
denoising AE on noisy input MSE: 0.0317

L'immagine rumorosa ha MSE 0.1175 rispetto a quella pulita. Un autoencoder normale (addestrato su immagini pulite), a cui si dà l'immagine rumorosa, la riduce a 0.0877: aiuta un po' perché il collo di bottiglia filtra, ma non è stato addestrato a farlo. L'autoencoder denoising, addestrato con ingresso rumoroso e bersaglio pulito, arriva a 0.0317: un taglio di quasi quattro volte rispetto al rumore di partenza. La lezione è sottile: cambiando solo che cosa si dà in ingresso, la stessa rete impara un compito diverso.

Il limite: un autoencoder non è un generatore

Se il decoder è bravo a trasformare codici in immagini, perché non pescare un codice a caso e farne un'immagine nuova? Perché non sappiamo dove pescare. La rete non ha alcun vincolo sulla posizione e sulla scala dei codici: sceglie quelli che le fanno comodo. Misuriamolo con un autoencoder a codice bidimensionale (k = 2), così lo spazio latente è un piano, e guardiamo dove cadono i codici delle immagini vere e quanto è «piena» la zona intorno. La funzione calcola, per ogni punto campionato, la distanza dal codice reale più vicino (torch.cdist, poi min), e ne prende la mediana su un insieme di riferimento di 900 codici reali.

# Can we GENERATE with an autoencoder? Where do the codes of real digits live, and what lies between?
ae2 = train_ae(2)                                        # 2-dimensional code, so that we can reason about it
with torch.no_grad():
    codes = ae2.enc(Xtr)                                 # (1257, 2): where the REAL digits live in code space
print("code mean:", [f"{v:.2f}" for v in codes.mean(0)], "  std:", [f"{v:.2f}" for v in codes.std(0)])

reference = codes[:900]                                  # the "known" real codes
def median_distance_to_nearest_real_code(z):
    return torch.cdist(z, reference).min(dim=1).values.median().item()

torch.manual_seed(0)
with torch.no_grad():
    z_real = codes[900:]                                 # other real codes (never in the reference set)
    z_gauss = torch.randn(5000, 2)                       # a standard Gaussian: the prior a VAE will use
    z_box = (torch.rand(5000, 2) - 0.5) * 2 * codes.abs().max(0).values   # uniform in the bounding box of the codes
print("median distance to the nearest real code:")
print("  other real codes     :", round(median_distance_to_nearest_real_code(z_real), 3))
print("  N(0, 1) samples      :", round(median_distance_to_nearest_real_code(z_gauss), 3))
print("  uniform in the box   :", round(median_distance_to_nearest_real_code(z_box), 3))
code mean: ['2.89', '-1.14']   std: ['3.82', '4.40']
median distance to the nearest real code:
  other real codes     : 0.198
  N(0, 1) samples      : 0.244
  uniform in the box   : 1.697

I codici dei 1257 esempi di addestramento sono centrati attorno a (2.89, −1.14) con deviazione 3.8-4.4: lontano dalla scala 0-1 che di solito si usa per campionare. Un campione uniforme nella «scatola» che contiene tutti i codici ha una mediana a distanza 1.70 dal codice reale più vicino, circa otto volte la distanza fra codici reali (0.198): gran parte della scatola è vuota, e il decoder non ha mai visto che cosa fare lì. I punti con distribuzione normale standard cadono vicino alla nuvola (0.244), ma solo perché la nuvola è così grande da includere l'origine: non c'è nessun motivo, nella costruzione dell'autoencoder, perché questo succeda. Il punto è metodologico: l'autoencoder non ti dice da quale distribuzione campionare. Se non conosci la distribuzione dei codici, non puoi generare. Il VAE, nelle prossime due lezioni, risolve proprio questo: costringe lo spazio latente ad avere una forma nota (una normale standard), così che campionare da quella forma dia codici sensati.

Trade-off

La dimensione del codice è il primo compromesso: piccola significa compressione forte e perdita di dettagli, grande significa ricostruzione fedele ma rischio di copiare senza imparare una rappresentazione utile. Fra lineare e non lineare, la PCA è deterministica, istantanea (una SVD) e interpretabile; l'autoencoder non lineare comprime meglio (qui tra il 33% e il 53% di errore in meno) ma si addestra, ha iperparametri e può dare risultati diversi per seme. Infine l'MSE favorisce ricostruzioni medie e leggermente sfocate, come nella lezione sulla previsione dei frame.

Trappole reali

  1. Collo di bottiglia troppo largo. Con un codice grande quanto l'ingresso la rete impara l'identità: MSE basso e rappresentazione inutile. Una ricostruzione perfetta non è un buon segno da sola.
  2. Pensare che il codice sia interpretabile. Nessuna componente di z significa «spessore» o «inclinazione» per costruzione. Se vuoi dimensioni con un significato, servono vincoli aggiuntivi.
  3. Campionare codici a caso dall'autoencoder normale. Lo spazio latente ha buchi e una scala arbitraria; le immagini decodificate in quei punti non hanno nessuna garanzia di significato.
  4. Valutare solo con l'MSE. Una buona MSE può convivere con immagini sfocate; guarda le immagini e usa metriche aggiuntive se serve.
  5. Confrontare con la PCA un autoencoder lineare non convergito. Qui resta sempre un filo sopra la PCA (0.0532 contro 0.0528 a k = 2): la PCA è un riferimento esatto, l'autoencoder deve convergerci.
  6. Usare un autoencoder denoising con un rumore diverso da quello reale. Se in produzione il rumore ha una forma diversa da quello visto in addestramento, il modello può non ripulirlo; non l'ho misurato.

Le sei trappole hanno in comune la fiducia in un numero solo: un MSE basso, un codice che sembra ordinato, un confronto con la PCA. Ciascuna viene smascherata guardando un secondo indicatore, che sia l'immagine ricostruita, la distribuzione dei codici o il riferimento esatto della PCA.

Un esempio svolto a mano: la PCA di tre punti e la compressione perfetta

Tre punti nel piano: (1, 2), (2, 4), (3, 6). Stanno tutti sulla retta y = 2x, quindi servono davvero soltanto un numero per punto, non due. Centrare: la media è (2, 4); i punti centrati sono (−1, −2), (0, 0), (1, 2). Direzione principale: i punti centrati sono multipli di (1, 2); la direzione unitaria è u = (1, 2)/√5 = (0.447, 0.894), ed è quella che trova la PCA con k = 1.

Comprimere: il codice di ciascun punto è la proiezione su u: (−1, −2)·u = −√5 = −2.236, 0, +2.236, tre numeri invece di sei. Ricostruire: il punto ricostruito è media + codice·u; per il primo, (2, 4) + (−2.236)·(0.447, 0.894) = (1, 2), errore 0. Se i punti fossero stati (1, 2.2), (2, 3.8), (3, 6), la retta migliore sarebbe stata un'approssimazione e la ricostruzione avrebbe avuto un errore pari a ciò che sta «fuori dalla retta»; il teorema di Eckart-Young dice che nessun'altra retta fa meglio. L'autoencoder non lineare generalizza questa idea: invece di una retta può usare una curva, e comprime meglio dati che stanno su una curva, come le cifre scritte a mano.

Un paio di conti per esercizio: il fattore di compressione di un autoencoder 64 → 16 → 64 è 64/16 = 4; un autoencoder lineare 64 → 8 → 64 con bias ha 64·8 + 8 + 8·64 + 64 = 1096 parametri; e se la somma dei quadrati dei valori singolari scartati di 10 immagini a 4 pixel vale 120, l'MSE di ricostruzione è 120 / (10·4) = 3.0.

Domande frequenti

Quando ha senso usare un autoencoder invece della PCA? Quando i dati hanno struttura non lineare e ti servono codici compatti, o quando vuoi una base per un modello generativo. Se i dati sono quasi lineari, la PCA è più semplice, istantanea e interpretabile.

Si può usare l'autoencoder per trovare anomalie? È un'applicazione molto usata: si addestra su dati «normali» e si usa l'errore di ricostruzione come punteggio. Non l'ho misurato qui, quindi non do numeri.

Come scelgo k? Fai variare k e guarda la curva dell'errore: spesso c'è un «gomito» oltre il quale i miglioramenti sono piccoli. Qui l'errore PCA scende molto fra k = 2 e 8 (da 0.0528 a 0.0245) e meno fra 8 e 16 (a 0.0115).

Il codice può servire ad altro? Come ingresso di un classificatore, per cercare immagini simili, per visualizzare dati con k = 2: sono usi comuni, non risultati di questa lezione.

Un autoencoder più profondo comprime meglio? Più capacità consente di approssimare strutture più complesse, ma aumenta il rischio di imparare l'identità se il collo di bottiglia non è abbastanza stretto. Qui ho usato un solo strato nascosto da 128 e non ho provato altre profondità.

Che cosa sai fare adesso

Sai costruire un autoencoder in PyTorch e sai che comprime attraverso un collo di bottiglia senza etichette. Nel caso lineare si avvicina alla PCA (0.0251 contro 0.0245 a k = 8) e la formula di Eckart-Young dà l'errore dalla sola SVD (0.05233 a k = 2, sia da formula sia misurato sull'addestramento). La non linearità abbassa l'errore del 33-53% a parità di codice. Se corrompi l'ingresso ottieni un denoiser (da 0.1175 a 0.0317). Il limite è che lo spazio dei codici non ha una forma nota: con k = 2 i codici sono centrati in (2.89, −1.14) con deviazione 3.8-4.4, e i campioni uniformi nella loro scatola cadono in media a 1.70 dal codice reale più vicino. Il VAE, nella lezione successiva, aggiunge ciò che manca: una distribuzione sullo spazio latente.

Nella serie, la lezione precedente è FractalNet: reti profonde senza scorciatoie esplicite, con drop-path.

Fonti

  • scikit-learn, sklearn.decomposition.PCA - la PCA come SVD dei dati centrati, usata per collegare PCA e Eckart-Young; letta, consultata il 2026-11-12
  • scikit-learn, sklearn.datasets.load_digits - il dataset di cifre 8×8 usato negli esperimenti, consultata il 2026-11-12
  • PyTorch, torch.linalg.svd - la funzione usata per la SVD; pagina raggiunta (200), consultata il 2026-11-12
  • Citazioni bibliografiche non consultate online: Eckart e Young (1936), teorema sull'approssimazione di rango basso; Baldi e Hornik (1989), «Neural networks and principal component analysis: Learning from examples without local minima», Neural Networks.
  • Misure: i listati di questa lezione, eseguiti come un unico script due volte con il Python del ml-service (Python 3.12.13, PyTorch 2.12.0 CPU, scikit-learn 1.9.0, due thread), seme fisso, uscite identiche; ogni esecuzione dura alcuni minuti

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti