Code Architecture — CycleGAN Notebook
Section-by-Section Breakdown
1. Setup & Imports
- Install/import PyTorch, torchvision, matplotlib, numpy, PIL.
- Set device to CUDA if available.
- Define hyperparameters: image size (128×128), batch size, learning rate, lambda_cycle, lambda_identity, epochs.
2. Dataset Preparation
- Create/load two unpaired image domains (X and Y).
- Uses synthetic data generation (e.g., geometric shapes with different styles/colors) or downloads a small public dataset.
- Custom Dataset class that samples from domain X and domain Y independently (no pairing).
- Transforms: resize, random crop, normalize to [-1, 1].
3. Generator Architecture (ResNetGenerator)
Input (3×128×128)
→ ConvBlock(3, 64, k7, s1, p3) + InstanceNorm + ReLU # c7s1-64
→ ConvBlock(64, 128, k3, s2, p1) + IN + ReLU # d128
→ ConvBlock(128, 256, k3, s2, p1) + IN + ReLU # d256
→ ResidualBlock(256) × 4 # R256 (simplified: 4 instead of 9)
→ ConvTransposeBlock(256, 128, k3, s2, p1, op1) + IN + ReLU # u128
→ ConvTransposeBlock(128, 64, k3, s2, p1, op1) + IN + ReLU # u64
→ ConvBlock(64, 3, k7, s1, p3) + Tanh # c7s1-3
Output (3×128×128)
Key classes:
ResidualBlock: two Conv2d + InstanceNorm + ReLU with skip connection.ConvBlock: Conv2d + (optional) InstanceNorm + activation.ConvTransposeBlock: ConvTranspose2d + InstanceNorm + ReLU.
4. Discriminator Architecture (PatchGAN / NLayerDiscriminator)
Input (3×128×128)
→ Conv2d(3, 64, k4, s2, p1) + LeakyReLU(0.2) # no norm on first layer
→ Conv2d(64, 128, k4, s2, p1) + IN + LeakyReLU(0.2)
→ Conv2d(128, 256, k4, s2, p1) + IN + LeakyReLU(0.2)
→ Conv2d(256, 1, k4, s1, p1) # output: 1×N×N patch map
Output (1×30×30 for 128×128 input)
- Classifies each patch as real/fake. Output is a grid of values (not a single scalar).
- Loss is MSE on real (all ones) / fake (all zeros) patch maps.
5. Loss Functions
- Adversarial loss (LSGAN): MSE between D(real) and 1, D(fake) and 0.
- Cycle consistency loss: L1(F(G(x)) - x) + L1(G(F(y)) - y), weighted by λ_cycle=10.
- Identity loss: L1(G(y) - y) for G and L1(F(x) - x) for F, weighted by λ_identity=0.5.
- This encourages the generator to preserve color when the input is already in the target domain.
6. Training Loop
For each epoch:
- Sample batch from domain X (real_A) and domain Y (real_B).
- Generators:
- fake_B = G(real_A), rec_A = F(fake_B)
- fake_A = F(real_B), rec_B = G(fake_A)
- identity_B = G(real_B), identity_A = F(real_A)
- G_loss = LSGAN(D_Y(fake_B), 1) + λ_cycle * L1(rec_A, real_A) + λ_identity * L1(identity_B, real_B)
- F_loss = LSGAN(D_X(fake_A), 1) + λ_cycle * L1(rec_B, real_B) + λ_identity * L1(identity_A, real_A)
- Backprop total generator loss.
- Discriminators:
- D_Y_loss = LSGAN(D_Y(real_B), 1) + LSGAN(D_Y(fake_B.detach()), 0)
- D_X_loss = LSGAN(D_X(real_A), 1) + LSGAN(D_X(fake_A.detach()), 0)
- Backprop each discriminator loss separately.
- Log losses, periodically save generated samples.
7. Visualization & Output
- Show side-by-side: real_A → fake_B → reconstructed_A.
- Show side-by-side: real_B → fake_A → reconstructed_B.
- Plot loss curves for G, F, D_X, D_Y.
Data Flow / Tensor Shapes
| Stage | Shape (batch=1, 128×128) | |—|—| | real_A / real_B | (1, 3, 128, 128) | | fake_B = G(real_A) | (1, 3, 128, 128) | | D_Y(fake_B) | (1, 1, 30, 30) | | rec_A = F(fake_B) | (1, 3, 128, 128) |
Deliberate Simplifications vs Full Paper
- 4 residual blocks instead of 9 (reduces model size and training time).
- 128×128 resolution instead of 256×256.
- Synthetic/small dataset instead of full horse2zebra or cityscapes.
- Fewer epochs (20-30) instead of 100-200.
- No learning rate decay (constant lr for simplicity).
- Smaller batch size to fit Kaggle GPU memory.