Classifier-Free Guidance (CFG)
Problem: How can we control the generated samples? e.g., for a diffusion-based image model, how can we generate only dogs? → need conditional control!
TODOs to clean up notes
- Clarify notation (what is , , ...)
Diffusion review
- Forward process (noise):
- The forward noising process is fixed and known:
- Reverse process (denoise):
- 💡 The true reverse noising process is what we want: , because then we could generate new samples by iteratively denoising from samples . So we learn a model that ideally matches the true reverse process as closely as possible.
Score function perspective of the diffusion reverse process
Predicting the noise added to an image is mathematically identical to estimating the gradient of the log-probability density of the data (known as the score).
- ^ Let's prove this!
0. Reparameterization trick
The reparameterization trick allows us to rewrite a random variable sampling operation () as a deterministic operation combined with an independent, standard noise source.
- 💡 can be equivalently expressed as , where
- The "reparameterization trick" is broader than just applying to Gaussian distributions - the above statement is just the instantiation applicable to these notes on diffusion models. See Wikipedia for details.
I. Forward process (adding noise)
Let be a sample from our true data distribution.
- The forward process adds Gaussian noise in discrete timesteps according to a variance schedule . The transition probability for a single step is:
- 📍 is defined with the multiplier to keep the variance of each noising step at a constant (1). Assuming the original data has been normalized so that its variance is roughly (identity matrix, meaning variance of 1 for each pixel1), let's examine the variance of : Because and are independent, the variance of their sum is the sum of their variances. Since : As shown, by scaling the mean by , we ensure that the total variance remains stable (near ) throughout the entire forward process (ultimately, we want ).
- When we have two independent Gaussian variables and , their linear combination is also Gaussian: .
- Now, we do a bunch of algebraic manipulation to show that: [Insert work for this]
- Using the reparameterization trick, we can express as a deterministic function of and a standard normal noise variable :
II. Reverse process (tractable version)
The goal of a diffusion model is to learn the reverse distribution . Directly estimating this intractable, but if we condition on the original image , the reverse process becomes tractable via Bayes' Theorem:
- Because the forward process is Markovian, . Plugging in our Gaussian definitions for each term and completing the square for yields a new Gaussian distribution: where the variance is deterministic and the mean is a linear combination of and :
- Specific algebraic steps for ^: [Fill in]
- Why directly estimating is intractable:
- Using Bayes' rule:
- We know because this is the forward diffusion step (adding Gaussian noise), which we defined explicitly.
- We do not know or , which are the marginal probabilities of seeing a specific noisy data point. To calculate , we would have to integrate over every possible data point in our training data distribution () to see how likely it is to end up at . This is impossible!
III. Connecting noise to the score function
During generation, we only have ; we do not know . However, we can express in terms of and the noise using our earlier reparameterization (): Substituting this into , we get [insert lots of algebraic manipulation]:
Our neural network attempts to match this true reverse mean (since we usually fix the variance of the noising process, so learning the mean is enough to infer the full denoising process).
- 💡 Since is known, the only unknown parameters the network must learn to predict is the noise , so our model can effectively be simplified to a noise predictor . Once we predict , we can obtain :
The score function is defined as the gradient of the log-probability density .
- Using Tweedie's Formula, the score of a Gaussian-corrupted variable directly reveals the noise that corrupted it.
- For our marginal distribution , the score is:
- Deriving how Tweedie's Formula gives us the above ^:
- Tweedie's Formula, from Wikipedia, states that:
- Let be a latent variable we don't observe, but we know it has a certain prior distribution .
- Let be an observable Gaussian-corrupted variable, where , so .
- Let be the probability density of , then the posterior mean and variance of given the observed are: Note that is just the derivative of the logarithm (i.e., ), so we can rewrite Tweedie's Formula as: [Fill in more steps]
- Tweedie's Formula, from Wikipedia, states that:
- Plugging in to our equation for reverse mean , we get:
- 💡 ^ This proves that predicting the mean of the reverse step () is mathematically equivalent to predicting the score of the data distribution ()!
Classifier guidance
We now have all the pieces to assemble the mechanisms for conditional control via guidance! How do we generate only dogs?
- We no longer want to sample from the unconditional distribution : instead, we want to sample from the conditional distribution , where is our class label ("dog").
- To do this, we need the conditional score: . Applying Bayes' rule:
- ^ This decomposes the problem into two parts:
- : the unconditional score (what standard diffusion learns)
- : the gradient of an image classifier evaluated on noisy images (hence classifier guidance)
- By substituting this conditional score back into , we get: ^ This is classifier guidance! The term is a scalar guidance scale. At every denoising step, the standard model pulls the image toward the manifold of all real images, while the classifier gradient simultaneously "pushes" the image in the specific mathematical direction that maximizes the probability of the label "dog."
The classifier is another neural network trained to be noise-aware.
- 🔑 To provide guidance at timestep , the classifier must model (where are the weights of the classifier).
- ⚠️ Because is highly corrupted by Gaussian noise at large , a standard classifier would not be helpful here. This is why we need to train a noise-aware classifier.
- Inputs: The noisy image and the continuous timestep embedding
- Architecture: Typical choices include a downsampled diffusion U-Net (encoder with ResNet blocks and self-attention) or a ViT.
- Output: A probability distribution over classes.
- During generation, we pass through this network , and calculate the cross-entropy loss against our target label , and compute the gradient of that loss w.r.t. the input pixels : This gradient is the
class_guidancein the pseudocode below!
- ⚠️ Because is highly corrupted by Gaussian noise at large , a standard classifier would not be helpful here. This is why we need to train a noise-aware classifier.
- Pseudocode for diffusion model inference using classifier guidance:
classifier_model = ... # load a pre-trained image classification model
y = 1 # we want to generate an image of class 1
guidance_scale = 7.5. # controls the strength of class guidance (higher = stronger)
# initial noisy sample x_T
x_t = get_noise(...) # randomly draws noise with the same shape as the output img from Gaussian
# reverse process (denoising loop)
for t in tqdm(scheduler.timesteps):
# 1. unconditional noise prediction
with torch.no_grad():
noise_pred = model(x_t, t).sample
# 2. compute the classifier gradient
with torch.enable_grad():
x_t.requires_grad = True
class_guidance = classifier_model.get_class_guidance(x_t, y, t)
# 3. apply classifier guidance
# fetch the variance scaling factor for the current timestep
scale_factor = torch.sqrt(1 - scheduler.alphas_cumprod[t])
noise_pred = noise_pred - (guidance_scale * scale_factor * class_guidance)
# 4. take the denoising step
x_t = scheduler.step(noise_pred, t, x_t).prev_sample
- Unconditional prediction:
with torch.no_grad(): noise_pred = model(x_t, t).sample- We pass our current noisy image and timestep into the learned denoiser. We use
.no_gradbecause we are strictly doing inference! - The model outputs its best guess for the noise that was added to the image.
- This step is entirely unaware of our target class
y
- We pass our current noisy image and timestep into the learned denoiser. We use
- Classifier gradient:
with torch.enable_grad(): x_t.requires_grad = Trueclass_guidance = classifier_model.get_class_guidance(x_t, y, t): inside this function, the classifier evaluates . It computes the cross-entropy loss between its prediction (for what class this generated image belongs to) and our target labely = 1, and backpropagates to the input image.- This yields , a tensor with the same shape as the image (e.g.,
3x256x256for RGB 256x256 image). - Every pixel value in this tensor ^ tells us: "If you change this pixel in this direction, it looks more like a cat" (if
y = 1 := cat).
- This yields , a tensor with the same shape as the image (e.g.,
- Guided noise prediction:
noise_pred = noise_pred - (guidance_scale * scale_factor * class_guidance): updating the unconditional generation with class guidancenoise_predis ,scale_factoris . Let's denote- Math equation:
- : the updated, conditionally guided noise (
noise_predon the left side) - : the unconditional noise prediction (
noise_predon the right side) - : scalar guidance weight (
scale_factor) - : the gradient of the log-probability of the classifier (
class_guidance)
- : the updated, conditionally guided noise (
- Explaining the math ^: How does increase our "label y = 1"-ness?
- Recall from earlier derivations that the mean of the reverse step, , is calculated by subtracting the predicted noise from the current noise : Let's replace the standard unconditional noise with our new guided noise : Now, has broken into two distinct parts:
- Unconditional denoising step:
- Gradient ascent step:
- By subtracting the classifier gradient from the noise prediction, we end up adding the classifier gradient to the image's pixel values.
- Adding the gradient of a function to some current position is gradient ascent → moves the pixel values of in the direction of steepest ascent for the log-probability of
- ^ this updates the image to maximize the classifier's confidence that
y = 1for the generated image
- Recall from earlier derivations that the mean of the reverse step, , is calculated by subtracting the predicted noise from the current noise : Let's replace the standard unconditional noise with our new guided noise : Now, has broken into two distinct parts:
- Denoising step:
x_t = scheduler.step(noise_pred, t, x_t).prev_sample: thenoise_predhere has been updated from Step (3) to gradient ascent toward the classifier predicting labely = 1!
Classifier-free guidance
Why CFG? The problem with classifier guidance is that it requires training a separate, noise-aware image classifier. If the classifier only knows 1000 ImageNet classes, your diffusion model can only generate those 1000 classes. Now, introducing CFG!
- 💡 The core idea of CFG is that we can turn the diffusion model into its own classifier.
- 🔑 If a single neural network learns both the unconditional distribution and the conditional distribution , we can extract a "virtual" classifier gradient from it using Bayes' Theorem. Work showing this is below!
I. Implicit classifier
By Bayes' Theorem, Taking the natural log of both sides: Take the gradient w.r.t. the noisy image pixels (to get score functions): is the gradient of the prior probability of the label (e.g., text prompt for the image = ). The prior probability of a text prompt existing in the universe is independent of the pixel values , so its derivative w.r.t. is zero: . This leaves us with: i.e., the gradient of our virtual classifier is the difference between the guided score and the unguided score. Now, we substitute this implicit classifier back into the original classifier guidance equation!
II. Assembling the CFG score
Let be our guidance scale (guidance_scale in the code). From earlier work, the guided score is: Substituting in our implicit classifier:Now, let's replace the true, intractable scores with our diffusion model approximations:
- (conditional score)
- (unconditional score, where is dropped/empty)
So, we end up with:
III. Geometry intuition of CFG
- 💡 Geometrically, CFG is essentially gradient ascent + vector extrapolation.
- : vector pointing toward the manifold of all possible images
- : vector pointing toward the manifold of images that match the text prompt
- Difference vector: : by subtracting the unconditional direction from the conditional direction, we obtain a vector that represents the semantic concept of the text prompt, isolated from the concept of "just being an image." It is a vector pointing away from generic images and strictly towards the prompt .
- 🔑 We use this difference vector as a drop-in replacement for guidance from the separate classifier in classifier guidance!
- (guidance scale): we multiply ^ that isolated semantic vector by and add it back to our base (unconditional vector)
- If , we get standard conditional generation.
- If , we are extrapolating past the model's natural distribution.
- e.g., setting (common in image generation models) tells the model to prioritize the difference vector more (i.e., move more in the direction of features it associates with "dog"), increasing prompt adherence at the cost of natural diversity ("natural" diversity as in the diversity afforded by the unconditional generation vector )
- Note that one of our intermediaries in the Section II (Assembling the CFG score) was: This format shows that CFG is simply an affine combination of the unconditional prediction and the conditional prediction (draws a straight line through two high-dimensional points).
- If , we are at the unconditional point.
- If , we are at the conditional point.
- If , we are traveling further down that exact same straight line, past the conditional point, into extrapolated territory.
IV. Pseudocode for CFG
Because we established via Tweedie's Formula that the score is directly proportional to the negative noise: the same algebraic formula applies to noise predictions:
# 1. setup & embedding
text_embeddings = clip_model.text_encode("a dog") # encode the conditional text → shape: [1, seq_len, dim]
empty_embeddings = clip_model.text_encode("") # encode empty text → shape: [1, seq_len, dim]
text_embeddings = torch.cat([empty_embeddings, text_embeddings]) # concat ^ together as the condition
# → shape: [2, seq_len, dim]
# 2. init noise to denoise w/ diffusion model
x_t = get_noise(...) # shape: [1, channels, height, width]
# denoising loop (sampling)
for t in tqdm(scheduler.timesteps):
# duplicate the noisy image to match the batch size of the text embeddings
latent_model_input = torch.cat([x_t, x_t]) # shape: [2, channels, height, width]
# 3. sample from diffusion model to get noise_pred
with torch.no_grad():
noise_pred = model(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
# ^ because neural nets process batches in parallel on the GPU,
# the diffusion model performs two entirely separate forward passes simultaneously here :0!
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
# 4. CFG equation
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# 5. take one denoising step
x_t = scheduler.step(noise_pred, t, x_t).prev_sample
- Embeddings:
text_embeddings = ...: to do CFG, we need two outputs from our model at every timestep - the conditional and unconditional output.- 🔑 During training, unconditional generation is learned by randomly dropping the text prompt (passing the embedding of an empty string). Conditional generation is learned by conditioning on the embedding of actual, meaningful text prompts ("a dog").
- Initial noisy for the diffusion model to start from:
x_t = get_noise(...); latent_model_input = torch.cat([x_t, x_t]) - Sampling from the diffusion model:
noise_pred = model(...).sample- Batch index 0: Model processes
x_tconditioned onempty_embeddings - Batch index 1: Model processes
x_tconditioned ontext_embeddings noise_predis a tensor with a batch size of 2 containing both ^ predictions.chunk(2)splitsnoise_predinto 2 equal parts along the batch dimension:noise_pred_uncond=noise_pred_text=
- Batch index 0: Model processes
- CFG math:
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond): Note that here, is back to shape[1, channels, height, width] - Scheduler denoising step:
x_t = scheduler.step(noise_pred, t, x_t).prev_sample: recall this step evaluates the reverse diffusion equation we derived earlier. For DDPM, that equation is:
(Notes above this line were guided by the following blog post: https://medium.com/@baicenxiao/understand-classifier-guidance-and-classifier-free-guidance-in-diffusion-model-via-python-e92c0c46ec18)
Fancier versions of CFG
Below are my notes from a CSAIL thesis defense in February 2026. General outline: Covers the mathematical flaws of standard CFG and proposes measure-theoretic fixes (classifier-thresholded guidance, piecewise-linear diffusion, dual-reward finetuning) to allow for logical composition (AND, NOT, XOR) without destroying the data manifold.
I. Flaws of CFG
Core intuition before we dive into the math: The central goal of a generative model is to learn some function that, given a bag of inputs (e.g., images) that has some prior distribution , learns to output , i.e. samples within this data distribution. Diffusion models approach this task by learning some function , that aims to model the distribution . If we can learn a model that perfectly models across all timesteps , then we can create new samples via iterative denoising: But how do we train a model to approximate ? The immediate answer might be - integrate the difference between the true and our model's predicted over all 's (noisy images at this timestep). This is intractable, as there are infinite 's in the universe. Instead, we can train a model to approximate the velocity field .
- Q: Why are "velocities" (scores) tractable when is not?
- A: This is the foundational "magic trick" of score-based generative modeling (JMLR 2005)!
- Complex probability distributions are often members of the exponential family, i.e. can be written as where is the partition function, representing the total volume of all possible states in the universe to ensure all probabilities sum up to 1. Calculating requires integrating over every possible image that could ever exist - is mathematically intractable to compute!
- But now let's examine the gradient of the log probability (the score function): Because is a constant w.r.t. , its gradient is 0. So we end up with: . The intractable vanishes!
- A: This is the foundational "magic trick" of score-based generative modeling (JMLR 2005)!
- Q: How can we recover new samples from a model that has learned across all timesteps ?
- A: We do numerical integration, a fancy way of saying we take tiny steps - during inference, we
- Start at timestep : generate a completely random matrix of pure Gaussian noise.
- Feed the noisy image and current time into the trained neural network: . Essentially, the model looks at the noise and outputs the score, which says - "to get a slightly more realistic image, move in this exact direction ."
- Instead of solving an integral, we use numerical integration: taking the current position, add the velocity vector multiplied by a tiny slice of time . We now have an image that is slightly less noisy: . Rinse and repeat Steps (2) & (3) until we get a clear image!
- A: We do numerical integration, a fancy way of saying we take tiny steps - during inference, we
- 💡 Essentially, diffusion models work by saying - we never need to know the shape of the entire mountain . We don't need to build the full map; we just need a reliable compass to navigate to hills within the mountain . The trained neural network is that compass!
So we've established that, while it is intractable to train a model , we can train a model . CFG essentially works by overlaying vector fields: in CFG, we take the unconditional slope and the conditional slope . We push the generation away from the generic path and further down the specific path by doing vector math: ( is notation for the CFG-guided score2, which is conceptually identical to ). During generation, the SDE/ODE solver (DDIM, Euler, etc.) acts like a blindfolded hiker. It asks the neural network , "which way is uphill?" The network gives it the CFG-modified vector, and the hiker takes a tiny step. It repeats this
num_inference_timestepsnumber of times until it reaches the top (ideally, a hill where is high, i.e. a clear image).
- ⚠️ Crucially: The solver never asks if a coherent mountain actually exists; it just blindly follow the local arrows.
- Therefore, when we artificially manipulate the arrows using in CFG, we risk creating a vector field that will lead us to run off the landscape. We need to prove that our new vector field actually corresponds to a valid, implied probability density . Let's investigate this next!
- In CFG, the modified score function at a time is a weighted mean in score space: (To unify with the notation we were using earlier: the above equation is identical to .)
- If we integrate this score function to find the probability density it corresponds to in data space, we get: By Bayes theorem, (by a factor of ), so CFG mathematically induces a probability distribution of the below form: This is the distribution our neural network is actually targeting (i.e., trying to learn) when we run the CFG code
noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
[RETURN TO THESE NOTES LATER - I'm getting stuck on the math from hereon]
- Q: Is
- Valid process: If you take a dataset, add noise to it according to the laws of physics (the heat equation) until it is pure static, and then reverse that exact physical process, you are mathematically guaranteed to reconstruct the true dataset.
- Invalid process: "Invalid" means that there is no forward physical process that starts at your clean images and
In the realm of
Standard CFG uses a guidance scale .
- too low: under-steering (ignores prompt)
- too high: over-saturation, diversity loss, generates "off the manifold"
What does off the manifold mean?
- In CFG, the modified score function at a time is a weighted mean in score space: (To unify with the notation we were using earlier: the above equation is identical to .)
- 📍 What is ?3 Imagine the universe of all possible images.
- At : is the distribution of perfectly clean, real images (e.g., the actual dataset). If we pick a random point in this high-dimensional point, is very high if the point looks like a real image (e.g., a dog!), and zero if the point looks like static noise.
- At : is the distribution of pure, unstructured Gaussian noise: .
- At intermediate : is the distribution of the dataset after it has been corrupted by exactly steps of noise.
- 🔑 Summary: given some image , the mathematical function outputs a single number answering the question: "Given the exact noise level at time , how likely is it that this () specific arrangement of pixels and static exists in our corrupted dataset?"
- Mathematically: In diffusion, is defined as the integral of the forward diffusion process applied to the true data distribution: where is the clean data, and is the Gaussian transition kernel (the formula that adds steps of noise to a clean image).
- ^ this integral says, "to find the probability of seeing this noisy image , we must check every single clean image in the universe (), multiply by the probability that the clean image could have degraded into our specific noisy image , and sum it all up." This should sound really intractable !
- 💡 In diffusion models, we never actually calculate because summing over the entire universe of images is computationally impossible. Instead, we care about its score: .
- Even though is intractable, its gradient (the score) can be learned by a neural network. When the diffusion model architecture (e.g., U-Net) predicts the noise , it is mathematically predicting this exact gradient (recall Tweedie's Formula!).
- Conceptually: the vector points toward the regions of high-dimensional space where is denser (where the real, corrupted images are).
- 📍 What is ?3 Imagine the universe of all possible images.
- \
- If we integrate this score function to find the probability density it corresponds to in data space, we get: By
- "Data space": this is the probability density landscape → every possible arrangement of pixels is a coordinate , and the altitude at any given coordinate is the probability density
- "Score space": imagine you are dropped onto the landscape ^ , but you are blindfolded - you cannot see the altitude , but you can feel the slope of the ground beneath your feet.
- Score function is a mathematical vector that points directly uphill toward the steepest ascent. The collection of all these vectors across the entire map is the score space!