A Conditional Variational Autoencoder (cVAE) that generates and edits celebrity face images conditioned on binary attributes, identity photos, and natural language prompts via CLIP.
Built on the CelebA dataset (202,599 face images, 40 binary attribute labels) and designed to run on Google Colab free tier (T4 GPU).
- Attribute-conditioned generation — generate a face from noise + a binary attribute vector (e.g., Smiling=1, Eyeglasses=1)
- Identity-preserving attribute swap — encode a real photo, then decode with different attributes applied while preserving identity
- Natural language control — type a prompt ("add glasses and make me look older") and CLIP maps it to the attribute vector automatically
- Cartoon style transfer — apply a pretrained anime/cartoon style on top of the cVAE output with a lightweight few-shot adapter
Text prompt
└─► CLIP encoder (frozen) ──► Linear probe (18 neurons) ──► attr vector [18]
│
Real photo ──► Photo Encoder ──► id_emb [256] │
└─────────────────────────────────────────────┐ │
▼ ▼
Noise z [128] ──────────────────────────────────────────────► Decoder ──► Face [3, 128, 128]
▲
concat(z, id_emb, attr_emb)
The model progressed through 8 development phases, each adding one capability while keeping prior phases intact.
| File | Phase | What it contains |
|---|---|---|
data_pipeline.py |
0 | CelebA dataset class, 18-attribute selection, DataLoader |
baseline_cvae.py |
1 | cVAE conditioned on labels only (no photo), 64×64 |
full_cvae.py |
2 | cVAE with photo encoder for identity preservation, 64×64 |
clip_probe.py |
3 | Frozen CLIP + trained linear probe for text-to-attribute mapping |
full_cvae_v2.py |
4 | Phase 2 rebuilt with InstanceNorm (replaces BatchNorm), 64×64 |
full_cvae_v3.py |
5+6 | Final model: 128×128, InstanceNorm, perceptual loss, KL annealing |
losses.py |
5 | Multi-scale VGG16 perceptual loss + auxiliary attribute classification loss |
style_transfer.py |
8A | Frozen AnimeGANv2 cartoonizer wrapper |
style_adapter.py |
8B–D | Residual adapter (~880 params) for few-shot style personalization |
evaluate.py |
7 | FID, SSIM/PSNR, per-attribute accuracy, latent traversal figures |
train.ipynb |
— | Main Colab training notebook (all phases) |
train_baseline.ipynb |
— | Baseline-only training notebook |
5_o_Clock_Shadow, Arched_Eyebrows, Attractive, Bags_Under_Eyes, Bald, Bangs, Black_Hair, Blond_Hair, Brown_Hair, Bushy_Eyebrows, Eyeglasses, Male, Mouth_Slightly_Open, Mustache, No_Beard, Pale_Skin, Smiling, Young
- 4-layer Conv encoder/decoder, BatchNorm, β-VAE loss
- Validates attribute-controlled generation before adding identity complexity
- Photo encoder encodes a real face to latent z
- Decoder receives
concat(z, id_emb, attr_emb)for identity-preserving attribute swap
- Frozen CLIP ViT-B/32 maps text to 512-dim embedding
- Trained 18-neuron linear probe converts embedding to soft attribute logits
- Enables prompts like "add a smile" or "make me look older"
- Replaces every BatchNorm2d with InstanceNorm2d
- Eliminates cross-sample dependencies that conflict with per-sample KL divergence
Final training config:
model: full_cvae_v3.py (5-layer InstanceNorm encoder/decoder)
optimizer: Adam, G_LR=1e-4, D_LR=5e-6, betas=(0.5, 0.999)
lr_schedule: CosineAnnealingWarmRestarts(T_0=10)
epochs: 30 (Phase 6R fine-tune from Phase 5 checkpoint)
batch_size: 128
latent_dim: 128
KL: beta annealed 0.0 → 0.1 over 15 epochs, free_bits=0.1 nats/dim
loss: 0.5·MSE + 1.0·perceptual(VGG multi-scale) + 0.05·KL + 8.0·aux + 0.5·adv + 10.0·feature_matching
discriminator: PatchDiscriminator with spectral norm, LSGAN, G:D update ratio 5:1
- FID: 10K generated vs 10K real CelebA images via
pytorch-fid - Attribute compliance: CLIP probe accuracy on generated images (target >85% per attribute)
- Reconstruction quality: SSIM and PSNR on 500 test images
- Latent traversal: attribute swept −1 → +1 with z fixed
- Frozen AnimeGANv2 (pretrained) as base cartoonizer
- Residual
StyleAdapter(880 parameters) for few-shot personalization with 5–20 images - Uncertainty-aware training: adaptive LR and FGSM perturbation scaled by output entropy
Multi-scale VGG16 perceptual loss uses three layers for full frequency coverage:
relu1_2(weight 0.25) — edges, fine texturerelu2_2(weight 0.5) — facial structure, hairrelu3_3(weight 1.0) — semantic/attribute-level features
Auxiliary attribute classification loss forces the decoder to actually respond to attribute conditioning by penalizing reconstructions that don't satisfy the target attribute vector. This closes the shortcut where the decoder ignores attr_emb and copies from id_emb.
pip install torch torchvision clip-by-openai scikit-image matplotlib pytorch-fidFor Google Colab, mount Drive and use the provided notebooks. Checkpoints are saved to Drive every 5 epochs to survive session timeouts.
CelebA download fallback: if torchvision.datasets.CelebA fails due to Google Drive limits, download manually from Kaggle (jessicali9530/celeba-dataset) and load from /content/drive/MyDrive/celeba/.
Saved to Google Drive under checkpoints/:
baseline_epoch_*.pt Phase 1
full_cvae_v2_best.pt Phase 4 (InstanceNorm, 64×64)
full_cvae_v3_best.pt Phase 5 (128×128 — sharp, weak attribute swap)
full_cvae_v3_p6r_best.pt Phase 6R (128×128 — sharp + attribute compliant)
clip_probe.pt Phase 3
- BatchNorm in VAEs causes cross-sample KL interference. InstanceNorm is the correct choice for all phases after the baseline.
- KL collapse with free_bits=0.5: sets the KL floor at
0.5 × 128 = 64 nats, effectively preventing z from encoding any information. Lowered to 0.1 in Phase 6. - VGG at non-standard resolution: running VGG16 at 128×128 instead of 224×224 produces miscalibrated feature magnitudes (~12 vs ~6). VGG must always run at 224×224.
- Discriminator dominance: with D_LR=4e-5 and G_LR=1e-5, D_loss dropped to near zero by epoch 1. Fix: D_LR=5e-6, G:D update ratio 5:1, feature matching loss.
- Aux loss swamped by perceptual loss: with
lambda_aux=2.0andperc_weight=1.0, aux was ~4% of the combined loss and ignored by the decoder. Raised tolambda_aux=8.0in Phase 6.