Example: Class-Conditioned Autoencoder¶
examples/sklearn_autoencoder.py learns to compress an 8×8 digit image into
an 8-dimensional latent code and rebuild it, conditioned on the digit's
class. Understandable from one equation -- the autoencoder tries to make
reconstruction identical to the input.
Anatomy of an autoencoder¶
self.encoder = nn.Sequential(nn.Linear(64, 32), nn.LayerNorm(32), nn.ReLU(),
nn.Linear(32, 8))
self.labels = nn.Embedding(10, 8) # 8-d vector per digit class
self.decoder = nn.Sequential(nn.Linear(16, 32), nn.ReLU(),
nn.Linear(32, 64), nn.Sigmoid())
Forward: flatten the image to \mathbf{x}\in\mathbb{R}^{64}, squeeze it with the encoder to a latent \mathbf{z}\in\mathbb{R}^8, look up the class vector \mathbf{c} from the embedding, concatenate, and decode back to an image:
with \sigma the sigmoid, chosen because pixel intensities \in[0,1].
Why condition on the class?¶
A plain autoencoder has to bury "which digit is this" into the 8 latent
numbers. Conditioning on the label makes the bundle [z; c] of size 16: the
class-specific structure (c) and the instance-specific stroke detail
(z) split cleanly, which produces sharper reconstructions and a latent space
where style information is easy to read.
The targets come out of the DataLoader as (B, 1), so the example flattens
them before the embedding lookup:
c = self.labels(y.view(y.shape[0])) # (B, 1) -> (B,) -> (B, 8)
The loss: mean squared error¶
The reconstruction error, averaged over all pixels and examples:
Backprop pushes the decoder to rebuild and the encoder to keep whatever the decoder needs -- the two do a tug-of-war that converges to a working latent code. Because the objective compares pixel-averaged differences (not per-class log-probabilities), MSE is the natural loss here;
Reconstructed images converge to a mean absolute error of a few percent.¶
Exercises¶
- Drop the class
Embeddingand watch reconstructions blur into generic digits. - Interpolate between two test latents
zand see the decoded images morph.