Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Lernen ohne Label

Im Kontrast zu unserem bisherigen Vorgehen

Bisher haben wir ein Neuron oder ein neuronales Netz zur Klassifikation oder Regression verwendet: Eine nicht bekannte Funktion ff, für die wir manche Werte kennen (Label y=f(x)y = f(x) für Trainingsdaten (x,y)(x, y)), wollen wir mit dem Modell „schätzen“. Was, wenn wir keine Label haben?

Herleitung zur abstrakten Idee

Ohne Label müssen wir den Output des neuronalen Netzes anders bewerten. Wie Erfolg messen? Welches Ziel ist im Training noch sinnvoll?

Ein mögliches Ziel: Transformation der Eingabe in ein anderes Koordinatensystem (Feature Extraction). Das kann man rückgängig machen – und messen, wie gut das klappt. Abstrakt: ff ist nicht bekannt, aber g∘fg \circ f ist bekannt, nämlich g(f(x))=xg(f(x)) = x. Hier heißt ff Encoder und gg Decoder, und g∘fg \circ f soll die Identität sein.

Das allein wäre trivial (f=g=id⁡f = g = \operatorname{id}). Interessant wird es mit einer Einschränkung: Dimensionsreduktion. Versuche, die Dimensionalität zu reduzieren ohne Informationsverlust – transformiere die Eingabe in ein kleineres Koordinatensystem, sodass man es trotzdem rückgängig machen kann. Der Encoder f ⁣:Rn→Rkf \colon \mathbb{R}^n \to \mathbb{R}^k mit k≪nk \ll n muss dann lernen, was an den Daten wesentlich ist.

Loss-Funktion

Trainingsziel: Output so ähnlich wie möglich zum Input (Rekonstruktion). Vergleiche Input mit Output, z.B. über die mittlere quadratische Abweichung der Pixelwerte:

L(x;f,g)=1N∑i=1N∥xi−g(f(xi))∥2.L(x; f, g) = \frac1N \sum_{i=1}^N \bigl\| x_i - g(f(x_i)) \bigr\|^2 .

Der Autoencoder ist also ein neuronales Netz mit einem „Flaschenhals“ in der Mitte, trainiert mit MSE zwischen Ein- und Ausgabe – und die Label sind die Eingaben selbst. Man nennt das auch self-supervised.

Autoencoder schematisch: Encoder, Flaschenhals (latenter Vektor), Decoder.

Autoencoder schematisch: Encoder, Flaschenhals (latenter Vektor), Decoder.

Im Vergleich zur Dimensionsreduktion mit PCA: Ist der Autoencoder linear (keine Aktivierungsfunktionen), so findet er denselben Unterraum wie PCA. Mit Nichtlinearitäten kann er gekrümmte Strukturen in den Daten erfassen, die PCA verfehlt, und rekonstruiert z.B. Bilder bei gleicher Dimension des Flaschenhalses deutlich besser.

Rekonstruktion von 8×8-Ziffern aus scikit-learn durch PCA mit 4 Komponenten und durch einen kleinen Autoencoder mit L = 4 (erzeugt mit sklearn.neural_network.MLPRegressor).

Rekonstruktion von 8×8-Ziffern aus scikit-learn durch PCA mit 4 Komponenten und durch einen kleinen Autoencoder mit L=4L = 4 (erzeugt mit sklearn.neural_network.MLPRegressor).

Autoencoder für MNIST

Das Autoencoder-Layout

Noch einmal genauer: Ein Autoencoder ist eine Regression mit Input xx, einem Vektor der Länge DD. Der Encoder reduziert in mehreren Schichten von Dimension DD auf Dimension LL, der Decoder erhöht in mehreren Schichten von Dimension LL zurück auf Dimension DD. Input und Output liegen im „Datenraum“ (bei Bildern: „Pixelraum“, pixel space); die Flaschenhals-Schicht der Dimension LL ist der latente Raum (latent space).

Anwendungsbeispiel: MNIST

MNIST besteht aus 28×2828 \times 28 Graustufen-Bildern handgeschriebener Ziffern 0–9, mit Label; 60 000 Trainings- und 10 000 Testbilder. Mögliche Tasks:

Zunächst: Kompression und Klassifikation, mit den Zielen Accuracy erhöhen und Overfitten vermeiden. Variationsmöglichkeiten dafür gibt es an drei Stellen: die Daten modifizieren (putzen?), die Modellarchitektur (tiefer?), und die Trainings-Hyperparameter (wie genau wird gelernt?).

Code-Beispiel: Modellarchitektur

Ein Autoencoder in PyTorch ist ein nn.Module mit zwei nn.Sequential-Blöcken. Schematisch:

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
IMAGESIZE = 28

class Autoencoder(nn.Module):
    def __init__(self, latent_dim=12):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Linear(IMAGESIZE**2, 256), nn.ReLU(True),
            nn.Linear(256, 64), nn.ReLU(True),
            nn.Linear(64, latent_dim),
        )
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 64), nn.ReLU(True),
            nn.Linear(64, 256), nn.ReLU(True),
            nn.Linear(256, IMAGESIZE**2), nn.Sigmoid(),
        )

    def forward(self, x):
        latent = self.encoder(x)
        return latent, self.decoder(latent)

Das Sigmoid am Ende des Decoders sorgt dafür, dass die Ausgabe wie Pixelwerte in [0,1][0, 1] liegt. Man kann auch dem Encoder ein Sigmoid ans Ende setzen; nötig ist das erst für den Sparse Autoencoder weiter unten, dessen Strafterm latente Werte in [0,1][0, 1] voraussetzt. Die Architektur lässt sich mit Netron visualisieren, nachdem man das Modell mit torch.save(model, 'model.pth') gespeichert hat.

Code-Beispiel: Training

transform_mnist = transforms.Compose([
    transforms.ToTensor(),
    transforms.Lambda(lambda x: x.view(-1)),   # 28x28 -> 784
])
mnist_data = datasets.MNIST(root='./data', train=True, download=True,
                            transform=transform_mnist)
data_loader = DataLoader(dataset=mnist_data, batch_size=512,
                         num_workers=8, shuffle=True)

model = Autoencoder(latent_dim=12).to(device)
reconstruction_criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=1e-2)

for epoch in range(15):
    for img, labels in data_loader:
        img = img.to(device)
        latent, reconstructed = model(img)
        reconstruction_loss = reconstruction_criterion(reconstructed, img)
        loss = reconstruction_loss
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

Neu gegenüber dem Kapitel „Tensoren“ sind Dataset und DataLoader: Der Datensatz liefert einzelne Samples (hier mit einer transform, die das Bild in einen Vektor der Länge 784 verwandelt), der Loader fasst sie zu zufällig gemischten Mini-Batches zusammen. Die Trainingsschleife selbst ist dieselbe wie immer, nur dass das Label das Bild selbst ist.

Tricks beim Training

Label Autoencoder

Supervised: Klassifikator

Ein „klassischer“ Klassifikator-Ansatz auf Basis des Autoencoders: Encoder vom Autoencoder, dann ein linearer Layer auf 10 Dimensionen. Dessen Ausgabe sind die Logits; erst ein Softmax darauf macht daraus Wahrscheinlichkeiten, und ein argmax⁡\operatorname{argmax} dahinter die „harte“ Klassifikation.

Loss-Funktion: Negative Log-Likelihood = Kreuzentropie =^\mathrel{\hat=} Kullback-Leibler-Divergenz zwischen echten Labels und Vorhersage (siehe Kapitel „Loss-Funktionen und Maximum Likelihood“). Das ist der classification loss, im Gegensatz zum reconstruction loss. In PyTorch: nn.CrossEntropyLoss(), das Softmax und NLL in einem Schritt (numerisch stabiler) berechnet und direkt auf die Logits angewendet wird. Deshalb gehört das Softmax nicht ins Modell, wenn man diese Loss-Funktion benutzt – sonst wird zweimal normiert, die Verteilung wird flach und das Training langsam. Erst bei der Inferenz, wenn man Wahrscheinlichkeiten sehen will, wendet man torch.softmax von Hand an.

Label Autoencoder

Warum nicht beides? Encoder vom Autoencoder, darauf der lineare Layer auf 10 Dimensionen (Logits) – und daneben der Decoder vom Autoencoder. Input ist ein Bild, Output sind Rekonstruktion und Label-Vorhersage, und die Loss-Funktion ist

L=reconstruction_loss+classification_loss.L = \text{reconstruction\_loss} + \text{classification\_loss}.

Der Encoder muss jetzt einen latenten Raum finden, der beides kann: das Bild rekonstruieren und die Ziffer erkennen. Die Rekonstruktion wirkt dabei wie eine Regularisierung des Klassifikators – der latente Raum darf nichts Wichtiges wegwerfen.

Label Autoencoder: ein Encoder, zwei Köpfe, zwei Loss-Terme.

Label Autoencoder: ein Encoder, zwei Köpfe, zwei Loss-Terme.

Tricks bei der Modellarchitektur

Autoencoder-Varianten

Zur Erinnerung: Ein Autoencoder besteht aus Encoder RD→RL\mathbb{R}^D \to \mathbb{R}^L und Decoder RL→RD\mathbb{R}^L \to \mathbb{R}^D, mit dem latenten Raum der Dimension LL als Flaschenhals. In diesem Abschnitt variieren wir die Loss-Funktion und die Eingabe, und sehen, was das mit dem latenten Raum macht.

Sparse Autoencoder

Idee: die Loss-Funktion um Strafterme ergänzen. Hier: dafür sorgen, dass ein latenter Vektor dünn besetzt (sparse) ist, also die meisten Einträge nahe 0 sind.

Lösung: Unter der Annahme, dass die latenten Vektoren Einträge in [0,1][0, 1] haben (z.B. durch ein Sigmoid am Ende des Encoders), betrachten wir für jede latente Dimension jj die mittlere Aktivierung ϱ^j\hat\varrho_j über den Batch und bestrafen die Kullback-Leibler-Divergenz zwischen der Bernoulli-Verteilung mit Wahrscheinlichkeit ϱ=0,1\varrho = 0{,}1 und der Bernoulli-Verteilung mit Wahrscheinlichkeit ϱ^j\hat\varrho_j:

Lsparse=∑j=1LDKL(Ber⁡(ϱ) ∥ Ber⁡(ϱ^j))=∑j=1L(ϱln⁡ϱϱ^j+(1−ϱ)ln⁡1−ϱ1−ϱ^j).L_{\text{sparse}} = \sum_{j=1}^L D_{KL}\bigl(\operatorname{Ber}(\varrho) \,\|\, \operatorname{Ber}(\hat\varrho_j)\bigr) = \sum_{j=1}^L \left( \varrho \ln\frac{\varrho}{\hat\varrho_j} + (1 - \varrho) \ln\frac{1 - \varrho}{1 - \hat\varrho_j} \right).

Der Term ist 0 genau dann, wenn jede latente Dimension im Mittel nur zu 10 % „an“ ist. Im Code muss er ausdrücklich zum Rekonstruktions-Loss addiert werden, loss = reconstruction_loss + beta * sparsity_loss; eine Funktion, die ihn nur berechnet, ändert am Training nichts. Dense latents nutzen alle Dimensionen für jeden Datenpunkt; sparse latents verteilen die Datenpunkte auf wenige aktive Dimensionen, was die einzelnen Dimensionen interpretierbarer macht.

Latente Vektoren von fünf Datenpunkten (Zeilen), 16 Dimensionen (Spalten): dicht besetzt vs. dünn besetzt.

Latente Vektoren von fünf Datenpunkten (Zeilen), 16 Dimensionen (Spalten): dicht besetzt vs. dünn besetzt.

Denoising Autoencoder

Idee: Daten nicht putzen, sondern verschmutzen. Noise ε\varepsilon generieren, auf die Daten addieren, xnoisy=x+εx_{\text{noisy}} = x + \varepsilon, und das als Input für den Autoencoder verwenden. Die Loss-Funktion vergleicht aber dec⁡(enc⁡(xnoisy))\operatorname{dec}(\operatorname{enc}(x_{\text{noisy}})) mit dem sauberen xx:

Ldenoise=MSE⁡(x, dec⁡(enc⁡(x+ε))).L_{\text{denoise}} = \operatorname{MSE}\bigl(x,\ \operatorname{dec}(\operatorname{enc}(x + \varepsilon))\bigr).

Das zwingt das Netzwerk dazu, einzelne Pixel nicht zu stark zu gewichten und eher globale Strukturen zu suchen – ein einzelner verrauschter Pixel darf die Rekonstruktion nicht kippen.

Denoising Sparse Label Autoencoder auf MNIST (Musterlösung zu einem Übungsblatt, WiSe 2025/26): verrauschte Eingabe, Rekonstruktion, Original.

Denoising Sparse Label Autoencoder auf MNIST (Musterlösung zu einem Übungsblatt, WiSe 2025/26): verrauschte Eingabe, Rekonstruktion, Original.

Dasselbe Modell: Loss-Kurve über die Epochen.

Dasselbe Modell: Loss-Kurve über die Epochen.

Der latente Raum desselben Modells, per PCA auf zwei Dimensionen projiziert und nach Ziffer eingefärbt.

Der latente Raum desselben Modells, per PCA auf zwei Dimensionen projiziert und nach Ziffer eingefärbt.

Die drei Abbildungen oben stammen aus einem Denoising Sparse Label Autoencoder – alle Tricks auf einmal, mit SiLU, BatchNorm und Dropout: verrauschte Eingabe hinein, sauberes Bild und Ziffer heraus, dazu ein Sparsity-Strafterm auf dem latenten Vektor.

Bisher, und was es bringt

Wir haben das Autoencoder-Design eingesetzt, um Bilder zu komprimieren (mit Sparsity Constraint), Bilder zu klassifizieren, Bilder zu entrauschen – und all das zusammen. Ein ML-System, welches diese Bilder verarbeiten soll, kann nun den fertig trainierten Encoder verwenden und direkt auf den Repräsentationen im Latent Space arbeiten. Das ist Modularisierung im Training: Der Encoder wird einmal trainiert und dann als Feature-Extraktor wiederverwendet.

Frage: Welche Struktur finden wir im Latent Space bzw. in den Latent Vectors der Daten? Die Latent-Manifold-Hypothese besagt, dass die Daten in RD\mathbb{R}^D nahe einer niedrigdimensionalen gekrümmten Fläche (Mannigfaltigkeit) liegen und der latente Raum diese Fläche „flach“ parametrisiert. Dazu mehr im Kapitel „Latente Räume geometrisch“.

Später werden wir das Autoencoder-Design einsetzen, um Text (Strings) natürlicher Sprache zu kodieren, sodass die latente Repräsentation interessante Struktur aufweist – das ist word2vec (Kapitel „word2vec“).

Übung

Ein Autoencoder für MNIST: 784→128→32→128→784784 \to 128 \to 32 \to 128 \to 784, alle Layer voll verbunden mit Bias.

  1. Wie viele Parameter hat er? Wie viele davon im Encoder? Tipp: Ein Layer n→mn \to m hat n⋅m+mn\cdot m + m Parameter.

  2. Beim Denoising-Autoencoder: Auf welche der beiden Größen – Eingabe oder Zielbild – kommt das Rauschen, und was wäre der Effekt, wenn man es auf die andere legte?

Antwortprobe: Bei (1) liegt das Ergebnis zwischen 200 000 und 250 000.