CycleGAN (Zhu, Park, Isola & Efros, 2017) performs unpaired image-to-image translation: it learns to turn horses into zebras, summer into winter, or photos into Monet-style paintings from two unrelated collections of images, with no pixel-aligned pairs. It extends the GAN framework with a second generator and a cycle-consistency constraint.
Two generators learn opposite mappings, G: X → Y and F: Y → X. Two discriminators judge realism in each domain. Adversarial losses alone only ensure G's outputs look like domain Y; they say nothing about whether the output corresponds to the input. That is the job of cycle consistency.
The full objective is
L = LGAN(G, DY) + LGAN(F, DX) + λcyc Lcyc + λid Lid
LGAN: least-squares GAN loss (LSGAN), more stable than BCE here.Lcyc = E[||F(G(x)) − x||1] + E[||G(F(y)) − y||1], typically λcyc = 10.Lid = E[||G(y) − y||1] + E[||F(x) − x||1], optional, around 0.5λcyc; helps preserve color in tasks like painting ↔ photo.The original also uses an image buffer of 50 previously generated images when updating discriminators, which reduces oscillation.
Generators follow Johnson et al.'s style-transfer network: downsample, a stack of residual blocks, upsample. The discriminator is a 70×70 PatchGAN from Pix2Pix that outputs a grid of real/fake scores. Instance normalization is used rather than batch normalization since batch size is typically 1.
import torch, torch.nn as nn, itertools
# G_XY, G_YX: ResNet generators; D_X, D_Y: PatchGAN discriminators (defined elsewhere)
mse, l1 = nn.MSELoss(), nn.L1Loss()
lam_cyc, lam_id = 10.0, 5.0
opt_G = torch.optim.Adam(itertools.chain(G_XY.parameters(), G_YX.parameters()), lr=2e-4, betas=(0.5, 0.999))
opt_D = torch.optim.Adam(itertools.chain(D_X.parameters(), D_Y.parameters()), lr=2e-4, betas=(0.5, 0.999))
def step(x, y):
# ---- generators ----
fake_y, fake_x = G_XY(x), G_YX(y)
pred_fy, pred_fx = D_Y(fake_y), D_X(fake_x)
loss_gan = mse(pred_fy, torch.ones_like(pred_fy)) + mse(pred_fx, torch.ones_like(pred_fx))
loss_cyc = l1(G_YX(fake_y), x) + l1(G_XY(fake_x), y)
loss_id = l1(G_XY(y), y) + l1(G_YX(x), x)
loss_G = loss_gan + lam_cyc * loss_cyc + lam_id * loss_id
opt_G.zero_grad(); loss_G.backward(); opt_G.step()
# ---- discriminators (LSGAN; 0.5 halves D's learning speed) ----
def d_loss(D, real, fake):
pr, pf = D(real), D(fake.detach())
return 0.5 * (mse(pr, torch.ones_like(pr)) + mse(pf, torch.zeros_like(pf)))
loss_D = d_loss(D_Y, y, fake_y) + d_loss(D_X, x, fake_x)
opt_D.zero_grad(); loss_D.backward(); opt_D.step()
return loss_G.item(), loss_D.item()
Production code adds the 50-image history buffer, linear learning-rate decay over the second half of training, and random crop / flip augmentation.
CycleGAN shines for appearance changes where geometry stays mostly the same: seasons, weather, artistic style, day/night, synthetic-to-real domain adaptation for training perception models, and some medical modality translation (with caution). If you have paired data, Pix2Pix or a conditional diffusion model will be more faithful.
Each iteration samples one image from each domain, usually random-cropped to 256×256 with batch size 1. For the generator step, G turns the horse photo x into a fake zebra G(x); D_Y scores that fake patch by patch, and the least-squares adversarial loss pushes those scores toward 1 ("real"). F then maps the fake zebra back, and the L1 distance between F(G(x)) and x is multiplied by λcyc = 10. The same happens in the other direction for a zebra y. All generator terms are summed and both generators are updated together with one Adam step.
For the discriminator step, D_Y sees a real zebra (target 1) and a fake drawn from the 50-image history buffer (target 0). The LSGAN loss for each discriminator is halved so it learns slower than the generators. The paper's schedule is Adam with learning rate 0.0002 and β1 = 0.5, constant for 100 epochs and then decayed linearly to zero over another 100.
Why the weights look like that: with λcyc too low, outputs drift toward any plausible zebra and lose the input's pose; too high, and G learns a near-identity mapping that barely changes texture. The identity loss is a nudge, not a requirement; it mostly helps when the two domains share color palettes and the generator would otherwise tint everything.
| Method | Data needed | Key idea | Trade-off |
|---|---|---|---|
| Pix2Pix (Isola et al., 2017) | Paired images | Conditional GAN + L1 to the target | Most faithful, but pairs are expensive |
| CycleGAN | Two unpaired sets | Two generators + cycle consistency | Doubles model size; favors minimal edits |
| UNIT / MUNIT | Unpaired | Shared latent space; MUNIT splits content and style | Multimodal outputs, more complex training |
| CUT (Park et al., 2020) | Unpaired | Patchwise contrastive loss, one direction only | Faster and lighter; fewer guarantees on inversion |
| Diffusion-based editing | Pretrained diffusion model | Noise the source partway, then denoise with a target prompt or model | Higher quality and flexible, but slower and harder to keep exact structure |
For new projects, diffusion-based translation often gives better visual quality, while CycleGAN remains attractive when you need a small, fast, feed-forward model that runs in real time, or when the domains are narrow (two sensor types, two scanners) and you have a few thousand images of each.
There is no ground truth for unpaired translation, so evaluation combines several imperfect signals:
See also Diffusion Models and StyleGAN.