29.1 Lineáris és nemlineáris autoencoder

29.1.1 Cél, előismeret és induló kérdés

Ebben az alfejezetben az autoenkódolót (autoencoder) mint adatvezérelt rekonstrukciós modellt vezetjük be. A korábbi 25.1. Teljes képosztályozó adatfolyam osztályozási adatfolyam a képet címkévé alakította; itt a cél maga a bemenet visszaállítása egy szűk reprezentáción keresztül. A 28.1. Embedding, távolság és normalizálás embedding-tanulásából tudjuk, hogy a reprezentáció hasznossága a downstream céltól függ; ez az autoencoder esetében is fontos korlát lesz. Előismeretként a 7.4. Numerikus stabilitás és jelentésmegőrző implementáció stabil veszteségszámítást, a 15.4. Független gradiensellenőrzési protokoll gradiensellenőrzést, a 23.4. Hibaszeletek és alkalmazási határ hibaszeleteket és a 27.4. Instance mask és összetett feladat encoder–decoder szerkezetű látásmódját idézzük fel. Induló kérdés: ha a GlyphLab egy képét d=576d=576 dimenziós vektorként fogjuk fel, és k=72k=72 dimenziós kódba tömörítjük lineárisan, mekkora a minimális átlagos négyzetes rekonstrukciós hiba? Sejtésünk: az elhagyott variancia; pontos képletét azonnal levezetjük.

29.1.2 Definíciók és jelölések

Legyen a bemeneti tér ℝd\mathbb R^d, a latens (rejtett) kód tere ℝk\mathbb R^k. Egy batch X∈ℝN×dX\in\mathbb R^{N\times d}, egy megfigyelés x∈ℝdx\in\mathbb R^d oszlopvektor. Az encoder (kódoló) egy fθ:ℝd→ℝkf_\theta:\mathbb R^d\to\mathbb R^k paraméteres függvény, amely a

z=fθ(x)∈ℝkz=f_\theta(x)\in\mathbb R^k

latens reprezentációt állítja elő. A decoder (dekódoló) gϕ:ℝk→ℝdg_\phi:\mathbb R^k\to\mathbb R^d a rekonstrukciót adja:

x̂=gϕ(z)∈ℝd.\hat x=g_\phi(z)\in\mathbb R^d.

A szűk keresztmetszet (bottleneck) feltétele k<dk<d; enélkül elegendő kapacitású modell triviálisan megtanulhatná az identitást. A tanítási cél az átlagos négyzetes rekonstrukciós hiba:

Lrec(θ,ϕ)=1N∑n=1N∥xn−gϕ(fθ(xn))∥22.L_{\mathrm{rec}}(\theta,\phi)=\frac{1}{N}\sum_{n=1}^N\|x_n-g_\phi(f_\theta(x_n))\|_2^2.

A könyvtári MSELoss reduction='mean' esetén a pixeldimenzión is átlagol, ezért a laborban a (1/(Nd))(1/(Nd)) skálát használjuk.

Lineáris eset. Ha ff és gg affin leképezés bias nélkül, akkor a 3.2. Mátrixszorzás és affín réteg rétegkonvenciója szerint We∈ℝk×dW_e\in\mathbb R^{k\times d}, Wd∈ℝd×kW_d\in\mathbb R^{d\times k} mellett

Z=XWe⊤∈ℝN×k,X̂=ZWd⊤=X(WdWe)⊤∈ℝN×d.Z=XW_e^\top\in\mathbb R^{N\times k},\qquad \hat X=ZW_d^\top=X(W_dW_e)^\top\in\mathbb R^{N\times d}.

A centrálást külön lépésként kezeljük, mert a PCA-kapcsolathoz nulla átlagú adatra van szükség.

29.1.3 A lineáris autoencoder és a PCA kapcsolata

Legyen X∈ℝN×dX\in\mathbb R^{N\times d} centrált, azaz 1N∑nxn=0\frac1N\sum_n x_n=0. Legyen a tapasztalati kovarianciamátrix Σ=1NX⊤X\Sigma=\frac1N X^\top X, sajátértékei λ1≥⋯≥λd≥0\lambda_1\ge\cdots\ge\lambda_d\ge0, a hozzájuk tartozó ortonormált sajátvektorok v1,…,vdv_1,\dots,v_d. Rögzítsünk k<dk<d értéket.

Tétel (lineáris autoencoder globális optimuma). Legyen X∈ℝN×dX\in\mathbb R^{N\times d} centrált, r=rank(X)r=\operatorname{rank}(X), és tegyük fel k<rk<r. A

minWe,Wd1N∥X−X(WdWe)⊤∥F2\min_{W_e,W_d}\ \frac1N\left\|X-X(W_dW_e)^\top\right\|_F^2

feladat optimuma ∑i=k+1rλi\sum_{i=k+1}^{r}\lambda_i. Ha λk>λk+1\lambda_k>\lambda_{k+1} és rank(X)=d\operatorname{rank}(X)=d, akkor az optimális WdWeW_dW_e szorzat egyértelmű, mégpedig WdWe=VkVk⊤W_dW_e=V_kV_k^\top, ahol Vk=[v1,…,vk]V_k=[v_1,\dots,v_k].

Bizonyításvázlat. A rekonstrukció X̂=XC\hat X=XC, ahol C=(WdWe)⊤∈ℝd×dC=(W_dW_e)^\top\in\mathbb R^{d\times d} és rankC≤k\operatorname{rank} C\le k, ezért X̂\hat X legfeljebb kk rangú mátrix. A klasszikus Eckart–Young-tétel szerint minden legfeljebb kk rangú BB mátrixra ∥X−B∥F2≥∑i=k+1rσi2\|X-B\|_F^2\ge\sum_{i=k+1}^{r}\sigma_i^2, ahol σi\sigma_i az XX szinguláris értékei; egyenlőség a csonkolt SVD-vel, B=UkΣkVk⊤B=U_k\Sigma_k V_k^\top mellett teljesül. Mivel σi2/N=λi\sigma_i^2/N=\lambda_i, az alsó korlát az állított sajátértékösszeg. A C=VkVk⊤C=V_kV_k^\top választással X̂=XVkVk⊤=UkΣkVk⊤\hat X=XV_kV_k^\top=U_k\Sigma_kV_k^\top, tehát a korlát elérhető. A sajátérték-rés miatt az altér egyértelmű; teljes rangú XX esetén a CC mátrix, így WdWeW_dW_e is egyértelmű. ∎

Az Eckart–Young-tételt itt idézett eredményként használjuk; bizonyítását nem ismételjük, mert az SVD-tulajdonságokra épül. A 3.4. Sajátérték, SVD és spektrális határ egységben felépített SVD eszköztár alkalmas a nyomon követésére. A tétel tartalmát a megadott lineáris algebrai feltételek szerint használjuk, az itt adott levezetés önálló.

Az optimális We,WdW_e,W_d páros nem egyértelmű. Ha We,WdW_e,W_d optimum, akkor tetszőleges invertálható C∈ℝk×kC\in\mathbb R^{k\times k} mellett We′=CWeW_e'=CW_e, Wd′=WdC−1W_d'=W_dC^{-1} is optimum, mert Wd′We′=WdWeW_d'W_e'=W_dW_e. A latens térbeli koordináták tetszőleges invertálható lineáris keveredéséig határozottak meg; a rekonstrukciós altér azonban egyértelmű.

29.1.4 Eredeti kézi kis példa

Tekintsük a centrált adatot

X=(20−20010−1).X=\begin{pmatrix}2&0\\-2&0\\0&1\\0&-1\end{pmatrix}.

Itt N=4N=4. A kovariancia:

X⊤X=(4000)+(4000)+(0001)+(0001)=(8002),X^\top X=\begin{pmatrix}4&0\\0&0\end{pmatrix}+\begin{pmatrix}4&0\\0&0\end{pmatrix}+\begin{pmatrix}0&0\\0&1\end{pmatrix}+\begin{pmatrix}0&0\\0&1\end{pmatrix}=\begin{pmatrix}8&0\\0&2\end{pmatrix},

tehát Σ=14X⊤X=diag(2,0.5)\Sigma=\frac14X^\top X=\operatorname{diag}(2,0.5). A k=1k=1 esetben az első sajátvektor v1=(1,0)⊤v_1=(1,0)^\top, a második elhagyott sajátérték λ2=0.5\lambda_2=0.5. Válasszuk az optimális paramétereket We=v1⊤=(1,0)W_e=v_1^\top=(1,0) és Wd=v1=(1,0)⊤W_d=v_1=(1,0)^\top alakban. Ekkor

Z=XWe⊤=(2−200),X̂=ZWd⊤=(20−200000).Z=XW_e^\top=\begin{pmatrix}2\\-2\\0\\0\end{pmatrix},\qquad \hat X=ZW_d^\top=\begin{pmatrix}2&0\\-2&0\\0&0\\0&0\end{pmatrix}.

A hibák az utolsó két sorban 11 és 11, ezért a batch-átlag 14(0+0+1+1)=12=λ2\frac14(0+0+1+1)=\frac12=\lambda_2, pontosan a tétel állítása szerint.

29.1.5 Nemlineáris autoencoder és a labor CNN modellje

A nemlineáris esetben fθf_\theta és gϕg_\phi általában ReLU- és konvolúciós rétegekből áll; a cél nem konvex, és nincs PCA-hoz hasonló zárt megoldás. A rekonstrukciós hiba csökkentése itt sem garantál hasznos downstream reprezentációt. A laborban egy kis CNN-autoencodert vetünk össze a lineáris, PCA-alapú rekonstrukciós baseline-nal a GlyphLab adaton.

A CNN encoder kimenete a 8×3×3=728\times3\times3=72 dimenziós latens; ez a szűk keresztmetszet. A decoder transzponált konvolúciókkal állítja vissza a 1×24×241\times24\times24 méretet. A transzponált konvolúció itt a megfelelő lineáris operátor adjungáltja, nem általános inverz; a rétegek a 15.3. Transposed convolution mint adjungált alfejezetben bevezetett értelemben nem az encoder inverzét számolják.

A labor a lab_autoencoder munkakönyvtárban fut. A GlyphLab splitje a metadata.csv-ből jön; a tanítás a train, az epoch-választás a val, a végső összehasonlítás a test halmazon történik. A lineáris baseline a train adatból számított SVD-vel zárt alakú; a CNN-autoencodert Adam optimalizálóval tanítjuk. A kézirat írásakor a szkriptet nem futtattuk, ezért a kimeneti MSE-számok jelzett, nem mért eredmények.

# lab_autoencoder/autoencoder_lab.py
import argparse, csv, json
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

def split_indices(meta_path):
    result = {"train": [], "val": [], "test": []}
    with open(meta_path, newline="") as f:
        for row in csv.DictReader(f):
            result[row["split"]].append(int(row["index"]))
    return result

class CNNAutoencoder(nn.Module):
    def __init__(self):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Conv2d(1, 16, 3, stride=2, padding=1), nn.ReLU(),
            nn.Conv2d(16, 32, 3, stride=2, padding=1), nn.ReLU(),
            nn.Conv2d(32, 8, 3, stride=2, padding=1), nn.ReLU(),
        )
        self.decoder = nn.Sequential(
            nn.ConvTranspose2d(8, 32, 4, stride=2, padding=1), nn.ReLU(),
            nn.ConvTranspose2d(32, 16, 4, stride=2, padding=1), nn.ReLU(),
            nn.ConvTranspose2d(16, 1, 4, stride=2, padding=1), nn.Sigmoid(),
        )
    def forward(self, x):
        return self.decoder(self.encoder(x))

def linear_pca(images, idx_train, idx_test, k):
    Xtr = images[idx_train].reshape(len(idx_train), -1).astype(np.float32)
    Xte = images[idx_test].reshape(len(idx_test), -1).astype(np.float32)
    mu = Xtr.mean(axis=0, keepdims=True)
    U, S, Vt = np.linalg.svd(Xtr - mu, full_matrices=False)
    Vk = Vt[:k].T
    Xtr_hat = (Xtr - mu) @ Vk @ Vk.T + mu
    Xte_hat = (Xte - mu) @ Vk @ Vk.T + mu
    return float(np.mean((Xtr - Xtr_hat) ** 2)), float(np.mean((Xte - Xte_hat) ** 2))

def main():
    p = argparse.ArgumentParser()
    p.add_argument("--data", required=True)
    p.add_argument("--meta", required=True)
    p.add_argument("--latent", type=int, default=72)
    p.add_argument("--epochs", type=int, default=20)
    p.add_argument("--batch", type=int, default=32)
    p.add_argument("--lr", type=float, default=1e-3)
    p.add_argument("--seed", type=int, default=123)
    args = p.parse_args()

    torch.manual_seed(args.seed)
    np.random.seed(args.seed)
    device = torch.device("cpu")
    images = np.load(args.data).astype(np.float32)          # (360,1,24,24)
    idx = split_indices(args.meta)

    lin_train, lin_test = linear_pca(images, idx["train"], idx["test"], args.latent)

    model = CNNAutoencoder().to(device)
    opt = torch.optim.Adam(model.parameters(), lr=args.lr)
    lossfn = nn.MSELoss()
    tr = torch.from_numpy(images[idx["train"]])
    va = torch.from_numpy(images[idx["val"]])
    te = torch.from_numpy(images[idx["test"]])
    tr_loader = DataLoader(TensorDataset(tr, tr), batch_size=args.batch, shuffle=True)

    best_val, best_state = float("inf"), None
    for _ in range(args.epochs):
        model.train()
        for xb, _ in tr_loader:
            opt.zero_grad()
            loss = lossfn(model(xb.to(device)), xb.to(device))
            loss.backward()
            opt.step()
        model.eval()
        with torch.no_grad():
            vloss = lossfn(model(va.to(device)), va.to(device)).item()
            if vloss < best_val:
                best_val, best_state = vloss, {k: v.detach().clone() for k, v in model.state_dict().items()}

    model.load_state_dict(best_state)
    model.eval()
    with torch.no_grad():
        cnn_test = lossfn(model(te.to(device)), te.to(device)).item()

    print(json.dumps({"linear_train_mse": lin_train, "linear_test_mse": lin_test,
                      "cnn_val_mse": best_val, "cnn_test_mse": cnn_test}))

if __name__ == "__main__":
    main()

Futtatási parancs:

python autoencoder_lab.py --data ../lab/data/images.npy --meta ../lab/data/metadata.csv --latent 72 --epochs 20 --batch 32 --lr 0.001 --seed 123

Az ellenőrzés a shape-ekre: a lineáris PCA bemenete (240,576)(240,576), a teszt (60,576)(60,576); a CNN bemenete (N,1,24,24)(N,1,24,24), az encoder kimenete (N,8,3,3)(N,8,3,3), a dekódolóé (N,1,24,24)(N,1,24,24). A linear_test_mse a PCA-altérre vetítés hibája a testen; a cnn_test_mse csak a fenti parancs tényleges futtatása után állítható. A várható összehasonlítás: a CNN több nemlineáris kapacitással alacsonyabb train hibát érhet el, de a test-összehasonlításból a val-epocha-választás és a véges testméret miatt óvatosan szabad következtetni.

29.1.6 Gyakori tévedés ellenpéldával

A leggyakoribb tévedés, hogy „minden autoencoder PCA-t számol, és a rekonstrukció garantálja a hasznos downstream reprezentációt”. Ez két ponton is hamis. Egyrészt lineáris esetben is csak a rekonstrukciós altér egyértelmű; a latens koordináták az invertálható CC keveredés miatt nem feltétlenül a főkomponens-koordináták. Másrészt a varianciaközpontú PCA elhagyhat éppen olyan kis szórású jellemzőt, amely a downstream döntéshez elengedhetetlen. Példa: legyen x=(xc,xf)x=(x_c,x_f), ahol xc∈{−2,2}x_c\in\{-2,2\} nagy szórású háttérváltozó, xf∈{−0.1,0.1}x_f\in\{-0.1,0.1\} pedig a címkét meghatározó jellemző. A k=1k=1 lineáris autoencoder a főkomponens irányát, xcx_c-t tartja meg; a rekonstrukciós hiba kicsi, de a latensből a címke nem állítható helyre. A rekonstrukciós cél tehát adattömörítő, nem automatikusan szemantikai jellemzőtanuló.

29.1.7 Önellenőrzés és visszahívás

Ellenőrizzük magunkat: (a) mi változik a lineáris optimumban, ha a bias nem nulla? (b) miért nem egyértelműek a lineáris súlyok? (c) mikor nulla a minimális hiba? A válasz: (a) a centrálást a train átlaggal kell kezelni, mert a bias nélküli levezetés nulla átlagot feltételez; (b) a latens invertálható lineáris keverése az altérre vetítést változatlanul hagyja; (c) ha az adat már eleve egy kk-dimenziós altérben van. Visszahívás: a 17.1. LeNet és a lokális megosztott reprezentáció alfejezetben látott konvolúciós építőelemek itt az encoder–decoderben térnek vissza; a 28.1. Embedding, távolság és normalizálás alfejezetben tanultakat pedig úgy alkalmazzuk, hogy a reprezentáció minősége csak a megcélzott feladaton mérhető.

29.1.8 Feladatok

F1 (fogalmi, nehézség 1). Fogalmazza meg, miért nem állítható, hogy minden autoencoder PCA-t számol. Adjon két különböző okot, egy lineáris és egy nemlineáris példával. Átadandó: legfeljebb fél oldalas indoklás az altér és a koordináta megkülönböztetésével.

F2 (levezetési, nehézség 2). A fenti kézi példa XX mátrixára SVD-könyvtár nélkül, a kovarianciamátrixból vezesse le a k=1k=1 lineáris autoencoder optimális irányát és minimális átlagos hibáját. Átadandó: a kovarianciamátrix, a sajátértékek, az optimális irány és a hiba.

F3 (implementációs, nehézség 1). Írjon NumPy-függvényt pca_reconstruct(X_train, X_test, k) aláírással, amely a train átlaggal centrál, SVD-vel kiszámítja a top kk altérre vetítést, és visszaadja a test rekonstrukciót. Ellenőrizze a kézi példán: a k=1k=1 hiba 0.50.5 legyen 10−610^{-6} tűrésen belül. Átadandó: futtatható kód és a hiba.

F4 (hibakeresési, nehézség 2). Egy hallgató a CNN-autoencoder lossát F.mse_loss(pred, target, reduction='sum') módon számolja, majd összehasonlítja a lineáris baseline reduction='mean' értékével. Mi a hiba, mi a következménye, és hogyan javítja? Átadandó: javított kódrészlet és a skálakapcsolat.

F5 (kísérleti tervezés, nehézség 3). Tervezzen kísérletet annak eldöntésére, hogy a CNN-autoencoder latens reprezentációja hasznosabb-e a GlyphLab orientációs osztályozásához, mint az azonos dimenziójú PCA-latens. Átadandó: split, baseline, befagyasztási protokoll, metrika és érvényességi korlát.

29.1.9 Megoldások és indoklás

F1. Az állítás azért hamis, mert (a) még a lineáris esetben sem maguk a főkomponens-koordináták egyértelműek: tetszőleges invertálható CC keverés optimális marad, miközben a latens koordináták megváltoznak; (b) a nemlineáris autoencoder nemlineáris sokaságra vetít, így az optimum általában nem írható le lineáris altérrel. A pontos állítás: lineáris, centrált, négyzetes hibájú, k<dk<d esetben az optimális rekonstrukciós altér a top kk főkomponens altere.

F2. Az X⊤X=diag(8,2)X^\top X=\operatorname{diag}(8,2) mátrixot már kiszámoltuk, így Σ=diag(2,0.5)\Sigma=\operatorname{diag}(2,0.5). A sajátértékek 2≥0.52\ge0.5; az első sajátvektor v1=(1,0)⊤v_1=(1,0)^\top. A k=1k=1 optimum We=v1⊤W_e=v_1^\top, Wd=v1W_d=v_1. A rekonstrukciós hibák az utolsó két sorban 11 és 11; az átlag 14(0+0+1+1)=0.5=λ2\frac14(0+0+1+1)=0.5=\lambda_2, ahogy a tétel állítja. Szélsőeset: ha k=2k=2, a hiba 00, mert az adat két dimenziós altérben van.

F3. A függvény:

def pca_reconstruct(X_train, X_test, k):
    mu = X_train.mean(axis=0, keepdims=True)
    U, S, Vt = np.linalg.svd(X_train - mu, full_matrices=False)
    Vk = Vt[:k].T
    X_hat = (X_test - mu) @ Vk @ Vk.T + mu
    return X_hat

X = np.array([[2.,0.],[-2.,0.],[0.,1.],[0.,-1.]], dtype=np.float64)
Xh = pca_reconstruct(X, X, 1)
print(np.mean((X - Xh) ** 2))   # 0.5

Az ellenőrző érték 0.50.5, a <10−6<10^{-6} abszolút tolerancia teljesül. A kimeneti shape megegyezik a bemeneti shape-szel.

F4. A hiba a reduction meg nem egyezése: a sum-hányados N⋅dN\cdot d-szerese a mean-nek. Így a CNN hiba számszerűen nagyobbnak látszik, és a két modell nem összehasonlítható. Javítás: mindkét ágon azonos reduction='mean'-t használunk; például a CNN-hez is F.mse_loss(pred, target, reduction='mean'), és a lineáris baseline-ban np.mean((X-Xh)**2). Visszaellenőrzés: egy 32×24×24 batchre a sum/mean hányados 1843218432 legyen.

F5. Rögzített train/val/test GlyphLab split. Baseline: PCA-latens (72 dimenzió) fölött logisztikus regresszió. Vizsgált modell: a labor CNN-autoencoder befagyasztott encoderének kimenete fölött ugyanolyan lineáris fej. A fejet csak a train latenseken tanítjuk, a val a korai leállításra szolgál; az autoencoder súlyait sem a val, sem a test nem finomhangolja. Metrika: osztályonkénti accuracy, a bizonytalanságot scene_id szerint rétegzett bootstrap adja. Korlát: a rekonstrukciós előtanítás a címkét nem látta, ezért a kísérlet azt ellenőrzi, hogy a rekonstrukció megőrizte-e az orientációhoz szükséges információt; az eredmény nem általánosítható természetes képekre.

29.1.10 Rövid összefoglaló

Az autoencoder egy encodert, egy latens kódot és egy decodert kapcsol össze; a cél a bemenet rekonstrukciója. A k<dk<d szűk keresztmetszet tömörítésre kényszerít, de önmagában nem garantál hasznos reprezentációt. Centrált adatra, lineáris leképezésekkel és átlagos négyzetes hibával a globális optimum rekonstrukciós altere a top kk főkomponens altere; a hibát az elhagyott sajátértékek összege adja. A latens koordináták invertálható lineáris keveredésig egyértelműek. A nemlineáris CNN-autoencodernek nincs ilyen zárt megoldása. A labor a PCA-alapú lineáris baseline-t és egy kis CNN-autoencodert veti össze a GlyphLab rekonstrukciós hibáján.

29.1.11 Következő alkalmazás és források

A 29.2. Denoising és korrupciós modell egységben a denoising autoencodert vizsgáljuk: ott a bemenet zajos, a cél a zajmentes kép, így a rekonstrukciós cél kifejezetten a zajmodellre irányul.