March 19, 2026
The VAE asks us to do a lot of things simultaneously:
The first four are the primary objective. Generation from random draws is somewhat secondary — it works because KL regularization creates a nice latent space, but it’s not what the loss function directly optimizes.
This is why reconstructions look decent but generations from \(\mathbf{z} \sim \mathcal{N}(\mathbf{0}, \mathbf{I})\) are blurry and samey.
What if we only cared about generation?
Start with an easy-to-sample distribution: \(\mathbf{z} \sim P(\mathbf{z}) = \mathcal{N}(\mathbf{0}, \mathbf{I})\) in \(\mathbb{R}^{256}\)
Learn a mapping \(g_\theta(\mathbf{z})\) such that when \(\mathbf{z} \sim P(\mathbf{z})\), the output \(g_\theta(\mathbf{z})\) is a coherent image of a dog.
This is just a decoder. A neural network that takes a random vector and outputs an image.
The idea tracks. But how do we judge quality?
Since \(P(\mathbf{z})\) is fixed (\(\mathcal{N}(\mathbf{0}, \mathbf{I})\), chosen by us), we need to find \(\theta\) such that \(g_\theta\) maps \(P(\mathbf{z})\) to \(P(\mathbf{x})\) — the distribution of all dog images in pixel space.
\(P(\mathbf{x})\) is a tiny, thin manifold in \(\mathbb{R}^{H \times W \times C}\).
Almost all of pixel space is garbage. The set of “images that look like dogs” is vanishingly small.
But it’s there. And we have samples from it — our training data.
If we just train \(g_\theta(\mathbf{z})\) to return the images in our training data, we get a deterministic autoencoder again.
The training set is not a fully characterized empirical distribution, even with billions of images — the curse of dimensionality means we’re always seeing a sparse sample of the true manifold.
We need to use \(\mathbf{X}\) to learn about \(P(\mathbf{x})\) without ever computing \(P(\mathbf{x})\) directly.
How?
Assume we have a CNN decoder parameterized by \(\theta\) that maps \(P(\mathbf{z})\) to the input space.
Goal: Make \(P(\mathbf{x})\) and \(Q(\mathbf{x})\) equivalent.
Formal equivalence:
\[\frac{P(\mathbf{x})}{Q(\mathbf{x})} = 1 \quad \forall \quad \mathbf{x}\]
The density ratio equals 1 everywhere.
Take the log:
\[\log \frac{P(\mathbf{x})}{Q(\mathbf{x})} = 0 \quad \iff \quad P = Q\]
The further from 0 the log density ratio is, the further the approximation is from the truth.
(KL divergence alarm bells should be ringing — we’ll come back to this.)
If I knew \(P(\mathbf{x})\), I could find \(\theta\) that minimizes the log density ratio everywhere.
I know \(Q(\mathbf{x})\) — it’s defined by my generator and the known prior \(P(\mathbf{z})\). \(\theta\) dictates its structure. Pick \(\theta\) to minimize the the expected log density ratio.
Note: There is a way to do this directly with distributions due to the Jacobian change of variable formula. We just won’t need to deal with this!
But, I don’t know \(P(\mathbf{x})\).
How can I assess the density ratio using only samples?
I have \(N\) samples from \(P(\mathbf{x})\) — the training images. Real.
Since I know \(P(\mathbf{z})\), I can take \(N\) samples from \(P(\mathbf{z})\) and transform them through \(g_\theta\) to get \(N\) samples from \(Q(\mathbf{x})\). Fake.
Our full collection: \(\{\mathbf{X}_1, \mathbf{X}_2, \ldots, \mathbf{X}_{2N}\}\)
Each image has \(H \times W \times C\) pixels.
Each image has a label:
\[y_i = \begin{cases} 1 & \text{real (generated by the world)} \\ 0 & \text{fake (generated by us)} \end{cases}\]
Given our labeling scheme, we can rewrite the density ratio at any \(\mathbf{x}\):
\[\frac{P(\mathbf{x})}{Q(\mathbf{x})} = \frac{P(\mathbf{x} \,|\, y = 1)}{P(\mathbf{x} \,|\, y = 0)}\]
This is always true
\(P(\mathbf{x})\) generates real images (\(y = 1\))
\(Q(\mathbf{x})\) generates fake images (\(y = 0\)).
Rewrite each conditional using Bayes’ rule:
\[\frac{P(\mathbf{x} \,|\, y=1)}{P(\mathbf{x} \,|\, y=0)} = \frac{P(y=1 \,|\, \mathbf{x}) \; P(\mathbf{x})}{P(y=1)} \cdot \frac{P(y=0)}{P(y=0 \,|\, \mathbf{x}) \; P(\mathbf{x})}\]
Cancel \(P(\mathbf{x})\) (e.g. the marginal likelihood a.k.a. the bane of my and now your existence):
\[\frac{P(\mathbf{x})}{Q(\mathbf{x})} = \frac{P(y=1 \,|\, \mathbf{x})}{P(y=0 \,|\, \mathbf{x})} \cdot \frac{P(y=0)}{P(y=1)}\]
Since we always draw \(N\) samples from \(P\) and \(N\) samples from \(Q\):
\[P(y = 0) = P(y = 1) = \frac{1}{2}\]
The prior ratio is 1. It drops out:
\[\frac{P(\mathbf{x})}{Q(\mathbf{x})} = \frac{P(y=1 \,|\, \mathbf{x})}{P(y=0 \,|\, \mathbf{x})}\]
And since this is a two-class problem by construction:
\[\frac{P(\mathbf{x})}{Q(\mathbf{x})} = \frac{P(y=1 \,|\, \mathbf{x})}{1 - P(y=1 \,|\, \mathbf{x})}\]
\[\frac{P(\mathbf{x})}{Q(\mathbf{x})} = \frac{P(y=1 \,|\, \mathbf{x})}{1 - P(y=1 \,|\, \mathbf{x})}\]
\(P(y = 1 \,|\, \mathbf{x})\) is a discriminator. A classifier that takes an image and outputs the probability that it’s real.
If I can train a classifier to tell which draws came from \(P(\mathbf{x})\) and which came from \(Q(\mathbf{x})\), I can compute the density ratio for free.
No assumption about the form of \(P(\mathbf{x})\).
No integral to solve.
No likelihood to compute.
Just a classifier — something we know how to train.
Remember the VAE’s problem? We wanted \(P(\mathbf{x} \,|\, \theta) = \int P(\mathbf{x} \,|\, \mathbf{z}, \theta) P(\mathbf{z}) \, d\mathbf{z}\) and couldn’t compute the integral.
We spent an entire lecture deriving the ELBO to get around it.
The GAN sidesteps the integral entirely. We never compute \(P(\mathbf{x})\). We never even approximate it. We just train a classifier on samples from both distributions and let the density ratio tell us how good our generator is.
If our goal is to find the classifier that best discriminates real from fake, we maximize the Bernoulli log-likelihood:
\[V(P, Q) = \max_\phi \; \mathbb{E}\left[y \log D_\phi(\mathbf{x}) + (1 - y) \log(1 - D_\phi(\mathbf{x}))\right]\]
where \(D_\phi(\mathbf{x}) = P(y = 1 \,|\, \mathbf{x})\) is a neural network classifier. \(\phi\) denotes the parameters for this classifier!
In neural network terms: find \(\phi\) that minimizes the negative log-likelihood. Standard binary cross-entropy. We’ve done this a hundred times.
The Bernoulli likelihood has two pieces — one active when \(y = 1\) (real) and one active when \(y = 0\) (fake).
\[V(P, Q) = \max_\phi \; \mathbb{E}\left[\log D_\phi(\mathbf{x}) \,|\, y = 1\right] + \mathbb{E}\left[\log(1 - D_\phi(\mathbf{x})) \,|\, y = 0\right]\]
By construction, all real images come from \(P(\mathbf{x})\) and all fake images come from \(Q(\mathbf{x})\). Substitute:
\[V(P, Q) = \max_\phi \; \frac{1}{2}\mathbb{E}_{P(\mathbf{x})}\left[\log D_\phi(\mathbf{x})\right] + \frac{1}{2}\mathbb{E}_{Q(\mathbf{x})}\left[\log(1 - D_\phi(\mathbf{x}))\right]\]
The \(\frac{1}{2}\) comes from \(P(y = 1) = P(y = 0) = \frac{1}{2}\).
Law of the Unconscious Statistician again!!!!
Since \(P(\mathbf{x})\) and \(Q(\mathbf{x})\) have the same support, place both expectations under one integral:
\[V(P,Q) = \max_\phi \; \frac{1}{2} \int_{\mathbf{x}} P(\mathbf{x}) \log D_\phi(\mathbf{x}) + Q(\mathbf{x}) \log(1 - D_\phi(\mathbf{x})) \; d\mathbf{x}\]
An integral is a continuous sum. The maximum of an integral equals the integral of the pointwise maxima. We can shove the max operator inside the integral:
\[V(P,Q) = \frac{1}{2} \int_{\mathbf{x}} \max_{\phi | \mathbf x} \; P(\mathbf{x}) \log D_\phi(\mathbf{x}) + Q(\mathbf{x}) \log(1 - D_\phi(\mathbf{x})) \; d\mathbf{x}\]
So we need \(D_{\phi}(\mathbf{x})\) that maximizes the integrand at each \(\mathbf{x}\).
For each \(\mathbf{x}\), we need to maximize:
\[P(\mathbf{x}) \log D(\mathbf{x}) + Q(\mathbf{x}) \log(1 - D(\mathbf{x}))\]
This is strictly concave in \(D(\mathbf{x})\):
A unique maximum exists.
\[D^*(\mathbf{x}) = \frac{P(\mathbf{x})}{P(\mathbf{x}) + Q(\mathbf{x})}\]
If we knew \(P(\mathbf{x})\) and \(Q(\mathbf{x})\), we wouldn’t need to train a model at all. This is the optimal classifier under cross-entropy loss.
But we don’t know \(P(\mathbf{x})\). So we train a neural network to approximate this optimal critic. The better the network, the closer it gets to \(D^*\).
Since \(D^*(\mathbf{x}) = \frac{P}{P + Q}\), we also know \(1 - D^*(\mathbf{x}) = \frac{Q}{P + Q}\).
Plug both into the \(V(P, Q)\) formula:
\[V(P, Q) = \frac{1}{2}\mathbb{E}_{P(\mathbf{x})}\left[\log \frac{P(\mathbf{x})}{P(\mathbf{x}) + Q(\mathbf{x})}\right] + \frac{1}{2}\mathbb{E}_{Q(\mathbf{x})}\left[\log \frac{Q(\mathbf{x})}{P(\mathbf{x}) + Q(\mathbf{x})}\right]\]
Multiply and divide inside each log by \(\frac{1}{2}\):
\[V(P, Q) = \frac{1}{2}\mathbb{E}_{P}\left[\log \frac{P(\mathbf{x})}{\frac{1}{2}(P(\mathbf{x}) + Q(\mathbf{x}))}\right] + \frac{1}{2}\mathbb{E}_{Q}\left[\log \frac{Q(\mathbf{x})}{\frac{1}{2}(P(\mathbf{x}) + Q(\mathbf{x}))}\right] - \log 2\]
Recognize these expectations?
\[D_{KL}(P \| R) = \mathbb{E}_P\left[\log \frac{P(\mathbf{x})}{R(\mathbf{x})}\right]\]
where \(R = \frac{1}{2}(P + Q)\) is the midpoint of the two distributions.
\[V(P, Q) = \frac{1}{2}D_{KL}\left(P \;\Big\|\; \frac{P + Q}{2}\right) + \frac{1}{2}D_{KL}\left(Q \;\Big\|\; \frac{P + Q}{2}\right) - \log 2\]
This is the Jensen-Shannon divergence between \(P(\mathbf{x})\) and \(Q(\mathbf{x})\).
Like KL, it measures how far two distributions are from one another.
Unlike KL, it’s symmetric: \(\text{JSD}(P, Q) = \text{JSD}(Q, P)\).
Just another distance to minimize
Let’s make this concrete.
We have two distributions — real images from \(P(\mathbf{x})\) and fake images from \(Q(\mathbf{x})\).
The Jensen-Shannon divergence between these two distributions is exactly equal to the Bernoulli log-likelihood of the optimal binary classifier trained to separate them.
The better the classifier performs \(\to\) the higher its log-likelihood \(\to\) the lower the cross-entropy loss \(\to\) the larger the JSD \(\to\) the further apart the real and fake distributions are.
If the classifier achieves perfect accuracy, the distributions are completely separated. If the classifier is at chance (50/50), the distributions are identical.
So for a fixed generator \(g_\theta\):
A high-performing classifier = large JSD = easy to tell apart = bad generator.
A struggling classifier = small JSD = hard to tell apart = good generator.
We can measure how good our generator is without ever computing \(P(\mathbf{x})\).
We want to find a generator function \(g_\theta\) that maps random draws from \(\mathcal{N}(\mathbf{0}, \mathbf{I})\) to images that are as similar as possible to real images from \(P(\mathbf{x})\).
“As similar as possible” now has a precise meaning: minimize the Jensen-Shannon divergence between the real distribution and the fake distribution.
Generator \(g_\theta\): takes a random vector \(\mathbf{z} \sim \mathcal{N}(\mathbf{0}, \mathbf{I})\) and produces an image of the same size as the training data (\(H \times W \times C\)). A neural network parameterized by \(\theta\).
Discriminator \(D_\phi\): takes an image — either real from the training set or fake from \(g_\theta(\mathbf{z})\) — and outputs the probability that it’s real. A binary classifier parameterized by \(\phi\).
The discriminator’s job: given \(N\) fake images from \(g_\theta(\mathbf{z})\) and \(N\) real images, minimize cross-entropy — maximize its ability to tell real from fake.
The generator’s job: produce images that maximize the discriminator’s cross-entropy — minimize the JSD.
\[\min_\theta \max_\phi \; \frac{1}{2}\mathbb{E}_{P(\mathbf{x})}[\log D_\phi(\mathbf{x})] + \frac{1}{2}\mathbb{E}_{Q(\mathbf{z})}[\log(1 - D_\phi(g_\theta(\mathbf{z})))]\]
\(\phi\) wants to maximize this — find every difference between real and fake.
\(\theta\) wants to minimize this — make the fake distribution indistinguishable from real.
We can’t optimize both simultaneously — each depends on the other.
Step 1 — Train the discriminator (generator frozen):
Step 2 — Train the generator (discriminator frozen):
At equilibrium:
The generator wins when the world’s best discriminator for picking out real and fake dogs assigns:
\[P(y = 1 \,|\, \text{fake image}) = \frac{1}{2}\]
The minimax objective splits into two separate losses — one for each network.
Discriminator loss — maximize the ability to tell real from fake. Given N draws from the real distribution and N draws from the fake distribution under BCE loss:
\[\mathcal{L}_D(\phi) = -\frac{1}{2N}\left[ \sum_{i = 1}^N \log P(y = 1 | \mathbf x_i) + \sum_{i = 1}^N \log P(y = 0 | g_{\theta}(\mathbf z_i))\right]\]
where \(P(y = 1 | \mathbf{x}) = D_\phi(\mathbf{x})\) — the discriminator’s output.
Minimize \(\mathcal{L}_D\) — push \(D\) to score real images high and fake images low.
The minimax objective splits into two separate losses — one for each network.
Generator loss — fool the discriminator. Try to maximize the probability that the discriminator thinks that the fake images are real:
\[\mathcal{L}_G(\theta) = -\frac{1}{N}\left[ \sum_{i = 1}^N \log P(y = 1 | g_\theta(\mathbf{z}_i))\right]\]
Minimize \(\mathcal{L}_G\) — push \(D\) to score fake images high.
This is different than anything you’ve seen before - there are two separate optimization problems happening in sequence!
The generator and discriminator are separate networks. How does the generator learn from the discriminator’s judgment?
During the discriminator step, we freeze the generator, generate N draws from the prior, and only update the discriminator
During the generator step, the computation graph is:
\[\mathbf{z} \;\xrightarrow{g_\theta}\; \hat{\mathbf{x}} \;\xrightarrow{D_\phi}\; \text{score} \;\rightarrow\; \mathcal{L}_G\]
The loss depends on the score. The score depends on \(\hat{\mathbf{x}}\). And \(\hat{\mathbf{x}}\) depends on \(\theta\).
When we call loss.backward(), gradients flow backward through \(D_\phi\) (frozen — gradients pass through but don’t update \(\phi\)) into \(\hat{\mathbf{x}}\), then into \(g_\theta\).
This is the same principle as LPIPS — a frozen network whose gradients flow through to update the generator.
Normally, this is the point where we would implement the concepts we’ve discussed in a practical example.
Not yet, though.
In theory, the minimax game converges to an equilibrium state. In practice, the original BCE loss breaks down early in training.
At the start of training, the generator produces random noise. The discriminator easily classifies everything correctly with high confidence:
\[D(G(\mathbf{z})) \approx 0 \quad \text{for all fakes}\]
The generator’s loss: \(\log(1 - D(G(\mathbf{z}))) \approx \log(1 - 0) = \log(1) = 0\)
Gradient \(\approx\) 0. The generator is stuck exactly when it needs the most help.
Early in training, the real and fake distributions may have no overlap in pixel space. Real dog images live on one manifold, random noise from an untrained generator lives somewhere completely different.
The JSD between two distributions with non-overlapping support is a constant: \(\log 2\).
A constant has zero gradient. No matter what the generator does, the JSD doesn’t change. The generator receives no signal about which direction to move.
JSD saturates when distributions are far apart — exactly when we need gradients most.
Much like many college students in the ChatGPT age, an optimizer given the opportunity to cheat and take the easy way out will do so.
The easiest way to beat the discriminator is to produce one fake image that fools the discriminator
Then repeat that image \(N\) times!
The same image repeated over and over again will fool the discriminator since it looks real!
Replace JSD with the Earth Mover’s Distance (Wasserstein-1 distance):
\[W(P, Q) = \inf_{\gamma \in \Pi(P, Q)} \mathbb{E}_{(\mathbf{x}, \mathbf{y}) \sim \gamma}\left[\|\mathbf{x} - \mathbf{y}\|\right]\]
Intuition: if \(P\) and \(Q\) are piles of dirt, \(W(P, Q)\) is the minimum amount of work to reshape one pile into the other. “How much dirt do I need to move, and how far?”
COMPLICATED LOSS BUT SIMPLE IMPLEMENTATION
Key property: \(W(P, Q)\) provides gradients everywhere, even when the distributions don’t overlap.
Wasserstein distance grows linearly with the distance between disjoint distributions — always a gradient, always a direction to move. (Arjovsky et al., 2017)
When the real and fake distributions are far apart (early training), JSD is flat and useless. Wasserstein gives a clear gradient: “move in this direction to get closer.”
The only architectural change: replace the sigmoid at the discriminator’s output with a linear activation (i.e., no activation at all).
\[D_{\text{BCE}}(\mathbf{x}) = \sigma(f_\phi(\mathbf{x})) \in [0, 1] \quad \longrightarrow \quad D_{\text{WGAN}}(\mathbf{x}) = f_\phi(\mathbf{x}) \in \mathbb{R}\]
The discriminator becomes a critic — it outputs a real-valued score instead of a probability. Positive = “looks real.” Negative = “looks fake.”
The losses simplify accordingly — no log, no sigmoid, just raw scores:
Critic loss: maximize the gap between real and fake scores
\[\mathcal{L}_D = \mathbb{E}[D(G(\mathbf{z}))] - \mathbb{E}[D(\mathbf{x}_{\text{real}})]\]
Generator loss: maximize the score the critic assigns to fakes
\[\mathcal{L}_G = -\mathbb{E}[D(G(\mathbf{z}))]\]
Everything upstream of the final activation is identical — same convolutional layers, same feature extraction. We’re just removing the sigmoid bottleneck that squashes everything into \([0, 1]\) and causes the gradient saturation.
There’s a catch. The Wasserstein distance is only valid if the critic is Lipschitz continuous — it can’t change too fast as a function of its input.
\[|D(\mathbf{x}_1) - D(\mathbf{x}_2)| \leq K \|\mathbf{x}_1 - \mathbf{x}_2\|\]
Without this constraint, the critic can assign \(+\infty\) to real and \(-\infty\) to fakes — the estimate blows up and training diverges.
Three approaches to enforce this:
Weight clipping (original WGAN): clip all critic weights to \([-c, c]\). Crude — limits the critic’s expressivity.
Gradient penalty (WGAN-GP): add \(\lambda \, \mathbb{E}[(\|\nabla_{\mathbf{x}} D(\mathbf{x})\| - 1)^2]\) to the critic loss. More expressive, more expensive.
Spectral normalization: divide each weight matrix by its spectral norm. Guarantees Lipschitz constant \(\leq 1\) by construction. Most common in modern practice.
In practice, most modern GANs use hinge loss rather than pure Wasserstein:
\[\mathcal{L}_D = \mathbb{E}[\text{ReLU}(1 - D(\mathbf{x}_{\text{real}}))] + \mathbb{E}[\text{ReLU}(1 + D(\mathbf{x}_{\text{fake}}))]\]
\[\mathcal{L}_G = -\mathbb{E}[D(G(\mathbf{z}))]\]
The ReLU creates a margin: once D classifies correctly by a margin of 1, the gradient turns off. The discriminator stops getting stronger once it’s confident enough.
This prevents D from dominating G — the exact problem that plagues both the original GAN and WGAN without careful tuning.
The generator loss is the same as WGAN — no saturation, no log, always a gradient.
def discriminator_loss(real_logits, fake_logits):
loss_real = torch.mean(F.relu(1.0 - real_logits))
loss_fake = torch.mean(F.relu(1.0 + fake_logits))
return loss_real + loss_fake
def generator_loss(fake_logits):
return -torch.mean(fake_logits)real_logits should be large positive \(\to\) 1 - large_positive is negative \(\to\) ReLU kills it \(\to\) no loss
fake_logits should be large negative \(\to\) 1 + large_negative is negative \(\to\) ReLU kills it \(\to\) no loss
Only contributes loss when D is wrong or not confident enough (margin < 1).
Instead of one real/fake score per image, output a grid of scores — one per image patch.
A standard discriminator: “this whole image looks fake” \(\to\) one gradient signal for the entire image
A PatchGAN: “the texture in the top-left looks fake, the fur in the center looks real, the background looks artificial” \(\to\) spatially localized feedback
class PatchDiscriminator(nn.Module):
def __init__(self):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(3, 64, 4, stride=2, padding=1), # 128→64
nn.LeakyReLU(0.2),
nn.Conv2d(64, 128, 4, stride=2, padding=1), # 64→32
nn.GroupNorm(32, 128),
nn.LeakyReLU(0.2),
nn.Conv2d(128, 256, 4, stride=2, padding=1), # 32→16
nn.GroupNorm(32, 256),
nn.LeakyReLU(0.2),
nn.Conv2d(256, 1, 4, stride=1, padding=1), # 16→15
)
def forward(self, x):
return self.model(x)LeakyReLU (not ReLU) — the discriminator needs gradients for “obviously fake” regions (negative activations). Standard ReLU would kill those gradients.
GroupNorm in the discriminator — same reasoning as the VAE. Per-image normalization, no batch dependence.
Spectral normalization on D: constrain how fast D can change — prevents arbitrarily sharp decisions
Higher LR for D: TTUR standard is 4x for the discriminator
Gradient clipping on G: occasional large gradients from D can destabilize G — clip_grad_norm_(G.parameters(), max_norm=10.0)
Differential Augmentation: apply random flips and crops to real and generated image before passing through the discriminator. This helps the discriminator not overfit to the training batches!
Even with all of these tricks, GANs are still notoriously unstable.
Training is super picky
Even sneezing in the wrong direction can destabilize training!
We didn’t have that issue with the VAE…
Our example today is perfect evidence of this
GAN losses don’t converge like VAE losses. They oscillate.
D_loss bounces around — when D improves, G catches up. When G improves, D adapts.
G_loss also bounces. Not monotonically decreasing.
You cannot tell from the loss curves whether the model is improving. You have to look at generated samples.
This is fundamentally different from the VAE, where lower ELBO reliably means better reconstructions.
The discriminator only asks: “does this look real?”
It doesn’t ask: “does this match a specific target pixel-by-pixel?”
There’s no MSE. No pixel averaging. No Gaussian decoder assumption.
A sharp image that looks slightly different from any training image gets a high score. A blurry image that’s the pixel-wise average of many training images gets a low score — the discriminator has learned that blur = fake.
NOW, let’s actually generate some pets!
Modern responsive Gen AI architectures can’t use GANs alone!
No reconstruction: We can’t encode a real image. There’s no encoder.
No structured latent space: Interpolating between \(\mathbf{z}\) vectors doesn’t produce meaningful transitions. No KL regularization.
Modern responsive Gen AI architectures can’t use GANs alone!
No editing: We can’t encode \(\to\) modify \(\to\) decode. One-way generation only.
No easy conditioning: Requires conditional batch norm, projection discriminators — really expensive and not possible with current hardware
| VAE | GAN | |
|---|---|---|
| Image quality | Blurry | Sharp |
| Reconstruction | Yes | No |
| Latent interpolation | Smooth, meaningful | Unstructured |
| Image editing | Encode \(\to\) edit \(\to\) decode | No encoder |
| Conditioning | Trivial (concatenate) | Architectural tricks |
| Training | Stable (ELBO) | Unstable (minimax) |
We want the VAE’s structure with the GAN’s sharpness.
What if the VAE’s decoder received a third gradient signal?
Reconstruction loss: “match the input image” \(\to\) keeps the decoder honest
Perceptual loss: “look good” \(\to\) keeps the decoder realistic
GAN loss: “look realistic” \(\to\) keeps the decoder sharp
We keep the encoder, the latent space, the ELBO — everything that makes the VAE useful for editing and control. We just add a discriminator that judges the decoder’s output.
The discriminator is a differentiable loss function — same principle as LPIPS, same principle as the gradient flow we just discussed. One more signal flowing into the decoder.
Next lecture: how to make this work in practice.