TAI BUI
Back to curriculum
BuildPython

Conditional GANs & Pix2Pix

The first big unlock of 2014-2017 was controlling what a GAN makes: attach a label, an image, or a sentence. Pix2Pix did the image version, and on narrow image-to-image tasks it still outperforms generic text-to-image models.

TIME: ~75 minutesPREREQUISITES: Phase 8 · 03 (GANs), Phase 4 · 06 (U-Net), Phase 3 · 07 (CNNs)

Learning Objectives

  • Explain how a conditional GAN feeds a condition `c` to both the generator and the discriminator, and why D must see the pair (x, y).
  • Describe the Pix2Pix recipe: a U-Net generator, a PatchGAN discriminator, and an adversarial plus L1 loss.
  • Build a toy 1-D conditional GAN and check its output class by class instead of only in aggregate.
  • Choose between Pix2Pix, CycleGAN, and diffusion-based approaches given paired vs unpaired data and a latency budget.

The Problem

A conditional GAN (Mirza & Osindero, 2014) is a GAN where both the generator G and the discriminator D take a condition c as an extra input. Pix2Pix (Isola et al., 2017) specialized this: the condition is a full input image, the generator is a U-Net, the discriminator is a patch-based classifier (PatchGAN), and the loss is adversarial + L1.

An unconditional GAN samples arbitrary faces. That is fine for a demo, but you get no say in what comes out. The tasks you actually want look like this: map a sketch to a photo, map a map to an aerial photo, map a daytime scene to nighttime, colorize a grayscale image. Each one gives you an input image x and asks for an output y with some semantic correspondence to it. Many plausible ys fit a single x, and mean-squared error flattens them into mush. An adversarial loss doesn't, because "looks real" is sharp.

The Pix2Pix recipe outperforms from-scratch text-to-image models on narrow image-to-image domains even in 2026, because it is trained on paired data. You have the signal you need.

Warm-up0/3
1. In a standard (unconditional) GAN, what is the discriminator's job?Warm-up
A GAN diagram: noise z goes into G, which produces a sample; a training set supplies a real sample; both kinds of sample go into D, whose output is left as a question mark.
Ordinary GAN, no condition. Drawn for this question.
2. You trained a GAN on handwritten digits and now want it to produce only 7s. What is the core obstacle?Warm-up
A generator with a noise vector as its only input, turning noise into a row of different digit images 3, 8, 0, 5, 1, 9. A note says the goal is to produce only 7s.
Digit glyphs are typed for illustration, not model output.
3. In a convolutional network, what does the receptive field of one output cell refer to?Warm-up
Three grids: a 7 by 7 input, a 5 by 5 layer 1 output and a 3 by 3 layer 2 output, joined by two 3 by 3 convolution arrows. The centre cell of the last grid is highlighted as one output cell.
Drawn for this question; valid convolutions with stride 1.
Check: The Problem0/2
1. What is Pix2Pix?Define
A sketch of a house going into a box labelled Pix2Pix, Isola et al., and coming out as a coloured photo of a house, above a table of four input and output image pairs: sketch to photo, map to aerial photo, daytime to nighttime, grayscale to colorized.
House drawings are illustrative, not model output. Task list from the lesson's Problem section.
2. Why does mean-squared error fail for a task like sketch to photo?Explain
A shirt outline sketch x next to three filled shirt images in red, green and blue, labelled photo y1, y2 and y3.
Illustrative drawing; colours are made up for display.

The Concept

Pix2Pix: U-Net generator, PatchGAN discriminator

Conditional G. G(x, z) → y. In Pix2Pix, z is dropout inside G. There is no input noise, because Isola found explicit noise got ignored.

Conditional D. D(x, y) → [0, 1]. D takes the pair (condition, output), and that is the key difference from an unconditional GAN: it has to judge whether y is consistent with x, not just whether y looks real.

U-Net generator. An encoder-decoder with skip connections across the bottleneck. The skips matter for tasks where input and output share low-level structure (edges, silhouette). Without them, high-frequency detail vanishes.

PatchGAN discriminator. Instead of a single real/fake score, D outputs an N×N grid where each cell judges a receptive field of ~70×70 pixels, and the cells are averaged. This is a Markov random field assumption: realism is local. It trains much faster, has fewer parameters, and gives sharper output.

Loss.

loss_G = -log D(x, G(x)) + λ · ||y - G(x)||_1
loss_D = -log D(x, y) - log (1 - D(x, G(x)))

The L1 term stabilizes training and pushes G toward the known target. L1 gives sharper edges than L2 (medians, not means). λ = 100 was the Pix2Pix default.

Everything above describes full Pix2Pix as published. The Build It demo implements only the conditioning half of it (see there for what it leaves out).

Check: The Concept0/9
1. What makes a GAN conditional?Define
An ordinary GAN diagram, noise into G, a sample into D, a real or fake output, above a histogram of the toy training data: class 0 clustered around minus 2 and class 1 clustered around plus 2.
Histogram of 150 samples per class drawn from the lesson's N(-2, 0.3) and N(2, 0.3).
2. What does a PatchGAN discriminator output instead of a single real/fake score?Define
One image fed to two discriminators: a usual D whose output is one real or fake score, and a PatchGAN D whose output is left as a question mark.
House drawing is illustrative.
3. What is a U-Net, as used for the Pix2Pix generator?Define
A U-shaped network: three blocks that shrink, a small bottleneck block, and three blocks that grow back, with dashed skip connections joining each shrinking block to its matching growing block.
Illustrative drawing of the Pix2Pix generator; the number of stages is not from the lesson.
4. What does Pix2Pix's discriminator receive that an unconditional GAN's discriminator does not?Explain
Two GAN diagrams: an unconditional one where noise goes through G and D receives G's output, and a Pix2Pix one where condition x goes through G to output y, with what D receives left as a question mark.
Drawn for this question.
5. Why did Isola et al. drop explicit noise z as an input to the Pix2Pix generator?Explain
A general conditional generator with inputs x and noise z producing y, above the Pix2Pix generator which takes only x and has no explicit noise input.
Drawn from the Concept section.
6. Why are U-Net skip connections useful in Pix2Pix's generator?Explain
A house sketch going through a chain of blocks that shrink to a small bottleneck and grow again, and coming out as a coloured house photo.
Illustrative drawing, not model output; the generator shown without skip connections.
7. Why does a PatchGAN train faster and give sharper output than a single global score?Explain
One image fed to a global discriminator that outputs a single score and to a PatchGAN discriminator that outputs a grid of scores which are averaged.
House drawing is illustrative; the grid is drawn 4 by 2 for display.
8. Why does Pix2Pix add an L1 term to the adversarial loss?Explain
The Pix2Pix generator loss, a negative log D term plus lambda times the L1 distance between y and G of x, above three small panels: input x, generated G(x) and paired output y.
Formula from the Concept section; house drawings are illustrative.
9. A Pix2Pix-style generator without skip connections gives the right overall layout, but edges and fine detail are gone. What fits the symptom?Analyze
An edge-map house sketch going through a generator without skips and coming out as a soft blurred house, next to the sharp target house photo; a note says the layout matches but edges and fine detail do not.
Illustrative drawing, not model output.

CycleGAN — when you don't have pairs

CycleGAN (Zhu et al., 2017) is the unpaired version of this idea. Pix2Pix needs paired (x, y) data, and CycleGAN drops that requirement at the cost of an extra loss: the cycle consistency loss. It uses two generators, G: X → Y and F: Y → X, trained so that F(G(x)) ≈ x and G(F(y)) ≈ y. That lets you translate horses to zebras or summer to winter without paired examples.

In 2026, unpaired image-to-image is mostly done with diffusion (ControlNet, IP-Adapter) rather than CycleGAN, but the cycle-consistency idea survives in many unpaired domain adaptation papers.

Check: CycleGAN — when you don't have pairs0/2
1. How does CycleGAN train without paired (x, y) data?Define
Two unconnected piles of image tiles, Set X of horse photos and Set Y of zebra photos, with a question mark between them and a note that no photo in X is matched to a photo in Y.
Illustrative tiles. Horses and zebras are the lesson's example.
2. A team has 5,000 daytime photos and 5,000 unrelated night photos with no matching pairs, and plans to train Pix2Pix. What is wrong with the plan?Analyze
Two separate piles of image tiles, 5,000 daytime photos in light blue and 5,000 night photos in darker blue, with a note that no matching pairs exist between them, and a plan box reading train Pix2Pix.
Illustrative tiles; 5,000 each is from the question.

Build It

main.py (200 lines, pure Python, hand-written backprop) implements a tiny conditional GAN on 1-D data. The condition c is a class label (0 or 1), and the task is to produce a sample from the conditional distribution for the given class: real data is N(-2, 0.3) for class 0 and N(2, 0.3) for class 1. Each network is an MLP with one hidden layer of 16 leaky-ReLU units. The snippets below are illustrative and condensed, so names and signatures differ a little from the file.

This is the conditioning half of Pix2Pix only. The file has no U-Net, no PatchGAN, no dropout, and no CycleGAN. Its default run also trains without the L1 term: update_g accepts optional l1_w and targets arguments, but main() never passes them.

Step 1: append the condition to both G and D inputs

Both networks need to see c. Without it in G, nothing steers the output; without it in D, nothing punishes a G that ignores the condition. In the file, z and x are Python lists, so + concatenates them with the one-hot vector:

python
def g_forward(z, c, G, num_classes):
    inp = z + one_hot(c, num_classes)
    ...  # linear -> leaky ReLU -> linear
 
def d_forward(x, c, D, num_classes):
    inp = x + one_hot(c, num_classes)
    ...  # linear -> leaky ReLU -> linear -> sigmoid

One-hot encoding is the simplest way to attach the condition. Larger models use learned embeddings, FiLM modulation, or cross-attention.

Step 2: train conditional

The generator has to match the real distribution for the given condition, not the marginal. Each step updates D on real and fake pairs that share a class, then updates G with fresh noise and fresh random classes:

python
for step in range(1, 601):
    reals = sample_real_conditional(batch, num_classes, rng)  # [([x], c), ...]
    cs = [c for _, c in reals]
    fakes = [(g_forward(z, c, G, num_classes)[0], c) for z, c in zip(noise, cs)]
    update_d(reals, fakes, D, num_classes, d_lr)
 
    noise = [...]                                   # fresh noise for G's update
    cs = [rng.randrange(num_classes) for _ in range(batch)]
    update_g(noise, cs, G, D, num_classes, g_lr)

Step 3: check the output class by class

A marginal check cannot tell you the condition was used, so look at each class separately. main.py contains no assert. Every 150 steps it draws 300 probes, picks a class at random for each probe (so the count n differs per class), and prints the mean generated value per class:

python
probes = {c: [] for c in range(num_classes)}
for _ in range(300):
    c = rng.randrange(num_classes)
    z = [rng.gauss(0, 1) for _ in range(z_dim)]
    probes[c].append(g_forward(z, c, G, num_classes)[0][0])

Class 0 near -2 and class 1 near +2 means G uses the condition. Both near 0 means it ignores it. At the end the script prints six raw samples per class, with no mean and no check. With the shipped seed the final means are about -2.56 and +2.47, so an assert you add yourself should use a loose tolerance (0.75, say), not a tight one.

Check: Build It0/8
1. What is FiLM conditioning?Define
A condition c equal to class 1 going into a block labelled FiLM, with a row of four hidden features above it and a row of four question marks below, standing for the features after FiLM.
Illustrative layout, not a specific model.
2. In the Build It demo, how is the class label c given to G and D?Define
The toy generator: a noise vector z of 4 numbers and a class label c in 0 or 1 both entering a one-hidden-layer MLP with 16 leaky-ReLU units that outputs 1 number; a note asks how c gets into G and D.
From the Build It section. How c enters is left out.
3. Why can a pooled check of the generated samples miss a G that ignores the condition?Explain
A histogram of the real toy data with both classes pooled into one grey shape, two humps around minus 2 and plus 2, with a note that a pooled check compares generated samples against it.
Histogram of 300 samples drawn from the lesson's two Gaussians.
4. In main.py, how can you tell the toy G is respecting the condition?Apply
A card describing the toy task in main.py: real class 0 data from N(-2, 0.3), real class 1 data from N(2, 0.3), a generator taking noise z and a one-hot c, and a run of 600 steps with a progress line every 150 steps.
From the Build It section.
5. You set num_classes = 3 in main.py but leave sample_real_conditional unchanged. Where does class 2's generated mean end up?Apply
The one-line edit setting num_classes to 3 in main.py, and a card saying sample_real_conditional is unchanged by the edit.
Code quoted from main.py.
6. With the shipped seed the final per-class means are about -2.56 and +2.47. Which tolerance on |mean - mode| would let an assert pass?Apply
A short assert with the two modes minus 2 and plus 2, the final per-class means minus 2.56 and plus 2.47, and a tolerance tol left as a question mark.
Typed code; the means are the lesson's.
7. In the shipped main.py run, which terms does G's update use?Apply
The generator half of a training step in main.py: fresh noise and random classes go through G and D into G's loss, whose terms are left as a question.
From the Build It section.
8. After training the toy cGAN, the per-class means are about -0.1 for class 0 and +0.1 for class 1. What is the most likely cause?Analyze
A number line with the real modes at minus 2 and plus 2 marked, and the two per-class generated means, class 0 at minus 0.1 and class 1 at plus 0.1, both close to zero.
Means as given in the question.

Pitfalls

The L1 bullets below describe full Pix2Pix. The Build It demo trains without an L1 term by default.

  • Condition ignored. G learns to marginalize, and D doesn't penalize it because the condition signal is weak. Fix: condition D more aggressively (early layer, not just late), or use a projection discriminator (Miyato & Koyama 2018).
  • L1 weight too low. G drifts to arbitrary real-looking outputs instead of faithful ones. Start at λ≈100 for Pix2Pix-style tasks.
  • L1 weight too high. G produces blurry outputs, because L1 is still an L_p norm. Anneal it down once training stabilizes.
  • Ground-truth leakage in D. Concatenate (x, y) as D's input, not just y. Without the pair, D cannot check consistency.
  • Mode collapse per class. Each class can collapse independently, so run class-conditional diversity checks.
Check: Pitfalls0/4
1. What happens at each extreme of the L1 weight in full Pix2Pix?Explain
The Pix2Pix generator loss with a line for the L1 weight lambda, marked low, around 100 as the default, and high.
From the Concept and Pitfalls sections.
2. Why must the Pix2Pix discriminator see the pair (x, y) rather than just y?Explain
Two discriminator designs: D(y), which receives only the output, and D(x, y), which receives the pair, each giving a score.
Drawn for this question.
3. How should you check a conditional GAN for mode collapse, per the Pitfalls section?Explain
A strip of 60 grey sample dots on a number line from a two-class conditional GAN with the classes pooled together: a spread-out group around minus 2 and a tight group around plus 2.
Simulated for illustration, not a trained model.
4. Your conditional GAN makes realistic samples, but every class's samples look alike. Which fix does the Pitfalls section suggest?Analyze
Two strips of generated samples on a number line, one for class 0 and one for class 1: both strips cover the same range and look alike, while a note says the real data sits at minus 2 for class 0 and plus 2 for class 1.
Real run of main.py's loop with G's one-hot input zeroed, 3000 steps, used only as a source of class-alike samples.

Use It

Here is the 2026 state of image-to-image tasks:

TaskBest approach
Sketch → photo, same domain, paired dataPix2Pix / Pix2PixHD (still fast, still sharp)
Sketch → photo, unpairedControlNet with a Scribble conditioning model
Semantic seg → photoSPADE / GauGAN2 or SD + ControlNet-Seg
Style transferDiffusion with IP-Adapter or LoRA; GAN methods are legacy
Depth → photoControlNet-Depth over Stable Diffusion
Super-resolutionReal-ESRGAN (GAN), ESRGAN-Plus, or SD-Upscale (diffusion)
ColorizationColTran, diffusion-based colorizers, or Pix2Pix-color
Daytime → nighttime, seasons, weatherCycleGAN or ControlNet-based

Pix2Pix is still the right tool when you have thousands of paired examples, the task is narrow and repeatable, and you need fast inference. On generic open-domain tasks, diffusion wins.

Check: Use It0/4
1. What does SPADE do?Define
A generator layer's activations going into a block labelled SPADE layer, Park et al. 2019, also known as GauGAN, and out as the next layer's activations.
Drawn for this question; names from the lesson's Key Terms and Further Reading.
2. Per the Use It table, what is the listed approach for semantic segmentation to photo?Apply
A table of eight image-to-image tasks from the Use It section, each with its approach hidden as a question mark, and the row for semantic segmentation to photo highlighted.
Task names shortened from the lesson's Use It table.
3. You have 8,000 paired sketch-to-render examples, a fixed narrow task, and a 50 ms latency budget per image. What fits best?Design
Three cards: 8,000 paired sketch to render examples, a fixed narrow task, and a budget of 50 ms per image.
From the question.
4. You want sketch-to-photo output, you have no paired sketch and photo data, and seconds of latency are acceptable. Which approach does the lesson list?Design
A row of house sketches and a separate row of house photos, with a not-equal sign between them, notes that no sketch is matched to a particular photo, and that seconds of latency are acceptable.
House drawings are illustrative.

Ship It

Save outputs/skill-img2img-chooser.md. The skill takes a task description, data availability (paired vs unpaired, N samples), and a latency/quality budget. It outputs an approach (Pix2Pix, CycleGAN, ControlNet variant, SDXL + IP-Adapter), training data requirements, inference cost, and an eval protocol (LPIPS, FID, task-specific).

Author's Note

I'd reach for Pix2Pix when all three conditions from Use It hold: thousands of paired examples, a narrow and repeatable task, and a need for fast inference. If one is missing, I'd start from the table there. For unpaired data, most of its rows point to ControlNet or other diffusion-based approaches.

My bet is that a per-class check is the habit worth building, because a marginal check can't tell you the condition was ignored. In main.py the real data sits at -2 for class 0 and +2 for class 1, and every 150 steps it prints the mean of the generated samples for each class. Those are the numbers I'd compare against.

I'd also read main.py as the conditioning half of the story. It has no U-Net and no PatchGAN, and the demo run has no L1 term: update_g accepts an L1 weight and targets, but main leaves the weight at its default of 0. To try the L1 term, I'd pass a weight and targets to update_g myself.

Exercises

  1. Easy. Modify main.py to add a third class. This is not drop-in: sample_real_conditional hardcodes c == 0 as N(-2, 0.3) and every other class as N(2, 0.3), and main() sets num_classes = 2. Give class 2 its own mode (say N(0, 0.3)) in the sampler and set num_classes = 3. The one-hot and probe code adapt on their own. Confirm each class's mean lands near its own mode.
  2. Medium. Replace L1 with a perceptual-style loss in the 1-D setting (e.g. a small frozen D acting as feature extractor). Does it change sharpness of the conditional distribution?
  3. Hard. Sketch a CycleGAN in the 1-D setting: two distributions, two generators, cycle loss. Show that it learns to map between them with no paired data.

Key Terms

TermWhat people sayWhat it actually means
Conditional GAN"GAN with labels"G(z, c), D(x, c). Both networks see the condition.
Pix2Pix"Image-to-image GAN"Paired cGAN with U-Net G and PatchGAN D + L1 loss.
U-Net"Encoder-decoder with skips"Symmetric conv network; skips preserve high-freq.
PatchGAN"Local-realism classifier"D outputs per-patch score instead of global score.
CycleGAN"Unpaired image translation"Two G's + cycle-consistency loss; no paired data.
SPADE"GauGAN"Normalizes intermediate activations with the semantic map; segmentation-to-image.
FiLM"Feature-wise linear modulation"Per-feature affine transform from the condition; cheap conditioning.

Production note: Pix2Pix as a latency-bound baseline

When you have paired data and a narrow task (sketch → render, semantic map → photo, day → night), Pix2Pix's one-shot inference beats multi-step diffusion by 40x or more on latency. The production comparison usually looks like this:

PathStepsTypical latency at 512² on a single L4
Pix2Pix (U-Net forward)1~30 ms
SD-Inpaint or SD-Img2Img20~1.2 s
SDXL-Turbo Img2Img1-4~0.15-0.35 s
ControlNet + SDXL base20-30~3-5 s

On the table above, Pix2Pix is about 40x faster than the 20-step SD paths (~30 ms vs ~1.2 s), roughly 100x or more faster than ControlNet + SDXL (~30 ms vs ~3-5 s), and about 5-12x faster than the 1-4 step SDXL-Turbo path. Pix2Pix wins on throughput in static batches, where each request costs the same FLOPs. Diffusion wins on quality and generalization. The modern play is often to ship a Pix2Pix-style distilled model for the narrow task and a diffusion fallback for tail inputs.

Check: Production note: Pix2Pix as a latency-bound baseline0/2
1. Per the Production note's table, how does Pix2Pix at 512² on an L4 compare with a 20-30 step ControlNet + SDXL pipeline?Apply
A table of four paths with steps and typical latency at 512 squared on one L4: Pix2Pix 1 step about 30 ms, SD-Inpaint or Img2Img 20 steps about 1.2 s, SDXL-Turbo 1 to 4 steps about 0.15 to 0.35 s, ControlNet plus SDXL base 20 to 30 steps about 3 to 5 s.
Copied from the lesson's Production note.
2. Your product has a narrow, repeatable task with paired data and a tight latency budget, but a few unusual inputs fall outside the training distribution. Which design fits?Design
A bell-shaped curve of request inputs with the central region shaded as common inputs and a few red points far out on either side marked rare, with notes on the narrow task, paired data, tight latency, and unusual inputs outside the training distribution.
Schematic, not measured data.

Further Reading

Code

Ships With