Image Generation
CLIP and Text Conditioning
Chapter 1 established that text embeddings enter the U-Net at every denoising step. It did not explain the hard part: why does an embedding of the English word
Jr Codex Generative AI Notes
Level: Intermediate Prerequisites: Chapter 1: From Diffusion to Stable Diffusion Time to complete: ~20 minutes
Table of Contents
- The Question This Chapter Answers
- CLIP — Two Encoders, One Space
- Contrastive Training
- How Text Enters the U-Net
- Classifier-Free Guidance
- Why Prompts Fail in Predictable Ways
- Summary & Next Steps
1. The Question This Chapter Answers
Chapter 1 established that text embeddings enter the U-Net at every denoising step. It did not explain the hard part: why does an embedding of the English word "fox" bias a numerical denoising process toward fox-shaped pixels?
Nothing about the two is obviously related. The answer is a model called CLIP, and it is the piece that makes text-to-image possible at all.
2. CLIP — Two Encoders, One Space
CLIP (Contrastive Language–Image Pre-training) is two separate encoders trained to place matching text and images at the same location in a shared embedding space.
The Architecture
─────────────────────────────────────────
"a red fox in snow"
│
▼
TEXT ENCODER (a Transformer) ──► vector ──┐
│
SAME SPACE
│
IMAGE ENCODER (a ViT) ──► vector ──┘
▲
│
[photo of a red fox in snow]
Training goal: these two vectors should be CLOSE.
─────────────────────────────────────────
import torch
from transformers import CLIPModel, CLIPProcessor
model = CLIPModel.from_pretrained("openai/clip-vit-large-patch14")
proc = CLIPProcessor.from_pretrained("openai/clip-vit-large-patch14")
captions = ["a red fox in snow", "a red car in snow", "a bowl of soup"]
inputs = proc(text=captions, images=fox_photo, return_tensors="pt", padding=True)
with torch.no_grad():
out = model(**inputs)
img = out.image_embeds / out.image_embeds.norm(dim=-1, keepdim=True) # NORMALISE, so
txt = out.text_embeds / out.text_embeds.norm(dim=-1, keepdim=True) # dot = cosine
for caption, score in zip(captions, (img @ txt.T)[0]):
print(f"{score:.3f} {caption}")
# 0.312 a red fox in snow ← the matching caption sits CLOSEST
# 0.241 a red car in snow ← shares colour and setting, differs in OBJECT
# 0.118 a bowl of soupThe middle score is the interesting one: "car" and "fox" share every word except the subject, so the gap between 0.312 and 0.241 is CLIP demonstrating that it encodes object identity, not just colour and setting.
This is Module 1, Chapter 3's point about conditioning made concrete: two latent spaces, deliberately fused into one. Once text and images live in the same space, a text vector is a valid description of a region of image space — and that is precisely what a conditioning signal needs to be.
3. Contrastive Training
CLIP never learned to caption or to draw. It learned only to match, on roughly 400 million image–text pairs scraped from the web.
One Training Batch
─────────────────────────────────────────
Take N image-caption pairs. Encode all of them.
Compute every image-to-text similarity: an N x N grid.
txt1 txt2 txt3 txt4
img1 [ ✓ ] x x x
img2 x [ ✓ ] x x
img3 x x [ ✓ ] x
img4 x x x [ ✓ ]
PUSH UP the N matching pairs on the diagonal.
PUSH DOWN all N² - N mismatched pairs.
─────────────────────────────────────────
Why This Simple Objective Is So Powerful
─────────────────────────────────────────
To separate "a red fox in snow" from "a red car in
snow", the encoders must genuinely represent OBJECT
IDENTITY, not just colour and setting.
Scaled to 400M web pairs, the pressure to distinguish
captions forces both encoders to learn compositional
visual semantics — style, medium, lighting, mood,
count, relation — because captions mention all of it.
Consequence: CLIP understands "in the style of a
woodcut" because thousands of captions said so.
─────────────────────────────────────────
And Why Its Biases Are Inherited
─────────────────────────────────────────
CLIP learned from uncurated web captions. Whatever
those captions associate — occupations with genders,
descriptors with ethnicities, "beautiful" with a
narrow range of appearances — becomes geometry in the
shared space.
Every image model built on CLIP inherits that
geometry. Module 6, Chapter 4 takes this up directly.
─────────────────────────────────────────
4. How Text Enters the U-Net
The mechanism is cross-attention — the same operation from DL Notes, Module 6, with the query and key/value coming from different sources.
Cross-Attention in a Denoising Block
─────────────────────────────────────────
QUERIES come from the IMAGE latent
"what should this spatial region become?"
KEYS/VALUES come from the TEXT embeddings
"here are the concepts on offer"
Each spatial position in the latent attends over the
prompt's tokens and pulls in whichever ones are
relevant to it.
─────────────────────────────────────────
What This Buys: Spatial Selectivity
─────────────────────────────────────────
Prompt: "a red fox on a blue rug"
The region that is becoming the animal attends
strongly to "red" and "fox".
The region that is becoming the floor attends
strongly to "blue" and "rug".
Different parts of the image are steered by
DIFFERENT parts of the prompt. That is why a prompt
is compositional at all, rather than a single global
style knob.
─────────────────────────────────────────
This also explains a whole class of failures. When cross-attention maps for "red" leak into the rug region, you get a red rug and a blue fox — the well-known attribute binding problem, and the reason Chapter 4's ControlNet exists as a spatial control that does not rely on attention alone.
5. Classifier-Free Guidance
Conditioning alone produces images that are only loosely on-prompt. Classifier-free guidance (CFG) amplifies the prompt's effect, and it is the single most impactful dial in image generation.
The Mechanism
─────────────────────────────────────────
At EVERY denoising step, predict the noise TWICE:
n_cond = U-Net(latent, t, prompt embedding)
n_uncond = U-Net(latent, t, EMPTY embedding)
Then extrapolate AWAY from the unconditional one:
n_final = n_uncond + g * (n_cond - n_uncond)
(n_cond - n_uncond) is literally "the direction the
prompt pulls in". g is guidance_scale.
─────────────────────────────────────────
@torch.no_grad()
def denoise_step(unet, latent, t, cond_emb, uncond_emb, guidance_scale):
"""One CFG-guided denoising step — the formula above, in code."""
both = torch.cat([uncond_emb, cond_emb]) # BATCH the two passes together
latent_in = torch.cat([latent] * 2) # so it is one U-Net call, not two
noise_uncond, noise_cond = unet(latent_in, t, encoder_hidden_states=both).sample.chunk(2)
# EXTRAPOLATE away from the unconditional prediction:
return noise_uncond + guidance_scale * (noise_cond - noise_uncond)Batching the two passes into one call is why CFG costs roughly 2x the compute rather than 2x the wall-clock time on a GPU with capacity to spare — the work doubles, the number of kernel launches does not.
Reading the Scale
─────────────────────────────────────────
g = 1 no guidance; the raw conditional prediction.
Diverse, often ignores much of the prompt.
g = 7 the usual default. Balanced.
g = 15 heavy extrapolation. Prompt adherence rises,
then saturation, contrast and artefacts rise
with it, and diversity collapses.
g = 30 burnt, oversaturated, deep-fried. The
extrapolation has left the region of latent
space where the decoder produces natural
images.
─────────────────────────────────────────
The Cost, and Why It Matters
─────────────────────────────────────────
CFG runs the U-Net TWICE per step. Generation with
guidance is ~2x the compute of generation without it.
Every "turbo", "lightning" or distilled model you see
is largely an attempt to recover this 2x — plus the
step count — by training a model that needs neither.
─────────────────────────────────────────
6. Why Prompts Fail in Predictable Ways
Understanding the mechanism turns three common frustrations from mysteries into expected behaviour.
Failure Mechanistic Cause
─────────────────────────────────────────
Long prompts lose CLIP's text encoder has a hard
their ending token limit (77 in the original
CLIP). Text beyond it is
TRUNCATED — silently.
"no cars" puts cars CLIP embeds concepts, not
in the image logical negation. "no cars"
embeds NEAR "cars". Negation
belongs in the NEGATIVE prompt
(Chapter 3), which works by
replacing the unconditional
term in the CFG formula above.
Counting fails Nothing in contrastive training
("five apples") rewards exact count. Captions
rarely enumerate, so the
embedding for "five" is weakly
grounded.
─────────────────────────────────────────
Each of these is a property of CLIP's training objective, not a prompt-writing skill issue — which is why the fixes in Chapter 3 and Chapter 4 are mechanisms, not better wording.
7. Summary & Next Steps
Key Takeaways
- CLIP trains a text encoder and an image encoder to place matching pairs at the same point in one shared space, which is what makes a text vector a usable image-space instruction.
- Contrastive matching over 400M web pairs forces genuine compositional visual semantics — and imports the biases of those captions along with them.
- Text steers the image through cross-attention, letting different spatial regions attend to different prompt tokens; leakage between them causes attribute-binding errors.
- Classifier-free guidance extrapolates along the conditional-minus-unconditional direction; it doubles compute, and pushing the scale too high leaves the natural-image region entirely.
Concept Check
- CLIP cannot generate an image or write a caption. Why is it nevertheless essential to a text-to-image model?
- Write out what
guidance_scaledoes to the noise prediction, and explain why very high values produce oversaturated images. - Why does the prompt "a street with no cars" tend to produce cars, and where does the fix actually apply?
Next Chapter
→ Chapter 3: Prompting Image Models
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to Generative AI Index