TAI BUI
Back to curriculum
CapstonePython

Vision-Language Pretraining

The encoder, projection, and decoder are wired. Now train them together. Two objectives drive learning: a contrastive image-text loss (InfoNCE) that pulls matching pairs together in the joint embedding space, and a language modeling loss that asks the decoder to caption each image. Combined, they teach the network both to find the right image for a caption and to write a caption for the image.

TIME: ~90 minutesPREREQUISITES: Phase 19 lessons 30-37 (Track B foundations)

Learning Objectives

  • Implement InfoNCE contrastive loss across a batch of image-caption pairs.
  • Compose contrastive loss with autoregressive language modeling loss.
  • Synthesize a 200-pair mock image-caption corpus with no real dataset download.
  • Run a 50-step demo training loop and observe both losses decreasing.

The Problem

Vision-language pretraining trains one model on two objectives at once: ranking and generation. Ranking means that given a caption, the model finds the right image among many. Generation means that given an image, it writes a caption. Train on one skill alone and you get half a system. CLIP nailed ranking but cannot caption. GPT-4V can caption but uses a separate retrieval head for ranking. Multi-objective pretraining gets both in one pass.

InfoNCE handles the ranking half. For a batch of N pairs, the model treats the N matching pairs as positives and the N^2 - N mismatched pairs as negatives, then runs a cross-entropy loss on the resulting (N, N) similarity matrix. The LM loss handles the generation half: standard next-token prediction conditioned on the image. Both losses are differentiable and can share the vision encoder.

Warm-up0/3
1. A retrieval system is given a caption and must return the matching photo from a gallery. How does that differ from a captioning system?Warm-up
2. A batch holds 16 photos, each paired with its own caption. A model must pick the right caption for one photo from all 16. How many of those captions are wrong for that photo?Warm-up
3. Two embedding vectors are each scaled to unit length and then combined with a dot product. What does the resulting number measure?Warm-up
Check: The Problem0/2
1. A batch holds 16 image-caption pairs. How many mismatched pairs act as negatives in the InfoNCE similarity matrix?Apply
2. An image-text model trained only with InfoNCE retrieves the right image for a caption well, but it cannot write a caption for a new image. What is missing?Analyze

The Concept

Mermaid diagram

InfoNCE in one paragraph

InfoNCE is a cross-entropy loss over a similarity matrix: each image must pick out its own caption from the batch, and each caption its own image. Stack the N image embeddings as rows and the N text embeddings as rows, then L2-normalize both. Compute the N x N matrix S = exp(log_tau) * I T^T, which is the usual I T^T / tau with 1/tau = exp(log_tau) learned in log space. The diagonal entries are the matching pairs, and the off-diagonal entries are negatives. Apply cross-entropy with the target argmax running down the diagonal, so row i should have its highest entry in column i. Do the same symmetrically along columns and average the two. That is the CLIP loss in eight lines.

Temperature matters

The temperature tau controls how peaked the softmax is. Set it too small (say tau = 0.01) and the gradient comes only from the very hardest negative, which makes training noisy. Set it too large and the softmax flattens and the gradient vanishes. CLIP learns the temperature as a parameter, and the demo here does the same. In the code the parameter is log_tau (initialized to ln(1/0.07), so the scale starts near 14.3), and the tau printed during training is that multiplier, so a larger printed value means a sharper softmax.

Language modeling loss

The decoder consumes image memory tokens via cross-attention and predicts the next text token at every position. The loss is standard cross-entropy against the next-position target, with padding positions masked out.

Combining the losses

total = contrastive + lm_weight * lm, where lm_weight is a scalar (often 1.0). Both losses send gradient into the vision encoder. The projector and the text-side encoder get gradient only from the contrastive loss, and the decoder only from the LM loss, because the decoder cross-attends to the encoder's tokens directly rather than to the projected embedding. CoCa, BLIP, and SigLIP-style models all use this multi-task recipe, with various weightings.

ComponentLoss surfaceAffects
InfoNCEPair ranking in the joint spaceEncoder + projection + text-side encoder
LMToken prediction conditioned on imageEncoder + decoder
CombinedMulti-taskWhole stack

Why 50 steps is enough for a demo

The mock corpus is a synthetic 200-pair set of noisy images, each with a brightness offset set by its index, and deterministic caption ids computed from the index. After 50 Adam steps with batch size 16, both losses drop visibly, even if the absolute values stay above what a real-data model would reach. The demo has two jobs: confirm the gradient plumbing works end to end, and confirm that adding the LM loss does not destabilize the contrastive objective.

Check: The Concept0/19
1. What is InfoNCE in this lesson's setup?Define
2. What does the temperature control in the contrastive loss?Define
3. In contrastive training, what is a hard negative?Define
4. What is the LM loss on the captioning side?Define
5. What is the joint embedding space?Define
6. What do the images and captions in the mock corpus look like?Define
7. A too-small contrastive temperature makes gradients noisy and a too-large one flattens the softmax. Why is the temperature therefore usually a learned parameter?Explain
8. Why does InfoNCE apply cross-entropy along the columns as well as the rows, then average the two?Explain
9. Why does setting tau = 0.01 make contrastive training noisy?Explain
10. Which module receives gradient from both the LM loss and the contrastive loss?Explain
11. Which module in the image path is trained by the contrastive loss alone, and gets no gradient from the LM loss?Explain
12. Why does the combined loss train the whole stack when InfoNCE alone does not?Explain
13. What does ignore_index=PAD_ID accomplish in the LM cross-entropy?Explain
14. Why is a 50-step demo on the mock corpus enough for this lesson?Explain
15. With contrastive loss 2.0, LM loss 4.0 and lm_weight = 0.5, what is the combined loss?Apply
16. In the code, init_log_tau = ln(1/0.07). About what multiplier is applied to the cosine similarities at the start of training?Apply
17. You rerun training with lm_weight = 0, as in exercise 5. Which module's weights stay at their initial values?Apply
18. The contrastive loss ends at 2.4 with lm_weight = 0 but at 3.2 with lm_weight = 1, and both losses share the vision encoder. What does this comparison show?Analyze
19. You plan to add a BLIP-style matching head as a third loss. What does the lesson's author suggest doing first?Design

Build It

code/main.py implements:

  • MultimodalModel, combining a small ViT encoder, the MLP projector, a tiny text-side encoder (mean-pool over embedded ids), and the cross-attention decoder from lesson 61.
  • info_nce_loss(image_emb, text_emb, log_tau), the bidirectional CLIP-style contrastive loss, returning (loss, sim).
  • lm_loss(logits, target_ids, padding_id), masked next-token cross-entropy.
  • make_mock_corpus(seed, n_pairs, vocab_size, max_len), returning 200 deterministic (image, caption_ids) pairs.
  • A training loop running 50 steps with batch size 16, Adam optimizer, and a learned log-temperature parameter. Both losses are printed every 5 steps.

Run it:

bash
python3 code/main.py

The run is seeded (seed=0), so every run prints the same numbers. On CPU the contrastive loss goes from 3.05 at step 0 to 2.36 at step 49, and the LM loss goes from 6.50 to 4.70. Both start above their uniform-guess baselines, ln(16) = 2.77 for picking one of 16 captions and ln(512) ≈ 6.24 for picking one of 512 tokens, because the random init produces small random similarities that the temperature scale of about 14 amplifies, so the first predictions are slightly worse than guessing. The contrastive curve is noisy (it ticks up at step 10 and again at step 30), so read the endpoints, not every row. The script prints ok: both losses decreased when both endpoints fall. Real models train for millions of steps, and the dynamics are the same.

Check: Build It0/7
1. Why is ln(16) = 2.77 the contrastive loss of a model that is only guessing, with 16 pairs in the batch?Explain
2. In MultimodalModel.forward, a caption id batch has shape (16, 16). What shape of ids is passed to the decoder as its inputs?Apply
3. In make_mock_corpus, each caption has min(6 + (i % 8), max_len) real tokens, and max_len is 16. How many non-pad tokens does pair i = 10 have?Apply
4. The training loop prints when step % 5 == 0 or at the final step, and steps run 0 to 49. How many progress rows appear?Apply
5. At some step of a 16-pair run, the printed contrastive loss is about 2.77. What does that tell you?Apply
6. Contrastive loss reads 2.88 at step 5, 2.99 at step 10 and 2.89 at step 15, yet the run ends lower than it started. A teammate says training is broken. What is the sounder reading?Analyze
7. A run starts with contrastive loss 3.05 and LM loss 6.50, above ln(16) = 2.77 and ln(512) = 6.24. A teammate calls this a bug. What explains it?Analyze

Use It

The same loss recipe ships in these models:

  • CLIP (2021). Image-text contrastive only, with a separate frozen-encoder caption probe.
  • CoCa (2022). Image-text contrastive plus image-captioning LM loss in one model. This is the pattern you build here.
  • BLIP (2022) and BLIP-2. Contrastive plus LM plus an image-text matching head: three losses combined.
  • SigLIP (2023). Swaps InfoNCE for a sigmoid pair loss. The contrastive role is the same, the functional form differs.
  • LLaVA family. Two-stage training. Stage one is alignment (cosine on a frozen LM), and stage two adds LM loss with an unfrozen LM. Lesson 60 maps to stage one; this lesson maps to stage two.
Check: Use It0/4
1. How does SigLIP's loss differ from CLIP's in the recipe this lesson describes?Explain
2. How does the lesson map its recipe onto named production systems?Explain
3. A product needs both image retrieval and caption generation from one pretraining run with a shared vision encoder. Which loss recipe fits?Design
4. You want to try a SigLIP-style loss on this pipeline. SigLIP keeps the contrastive role and changes only the functional form. What is the smallest change?Design

Tests

code/test_main.py has 7 tests:

  • InfoNCE loss is symmetric across image/text rows
  • InfoNCE loss is near 0 (below 1e-3) when the embeddings are orthonormal and the scale is large, so the similarity matrix is a sharp diagonal
  • InfoNCE raises ValueError when the image and text batch sizes differ
  • LM loss correctly masks padding positions
  • model forward pass produces both losses without errors
  • a 10-step training loop ends with a combined loss no higher than the first step (within 1e-3)
  • the mock corpus has the right shapes and token ids stay inside the vocab

Run them:

bash
python3 -m unittest code/test_main.py
Check: Tests0/3
1. The test suite feeds InfoNCE orthonormal embeddings with a large scale, so the similarity matrix is a sharp diagonal. What does it assert about the loss?Apply
2. The padding test compares loss on targets [1, 2, 0, 0] (0 is the pad id) with loss on just [1, 2] under the same logits. What does it assert?Apply
3. info_nce_loss is called with 3 image embeddings and 5 text embeddings, each 4 wide. What happens?Apply

Exercises

  1. Replace InfoNCE with a SigLIP-style sigmoid pair loss and compare convergence on the mock corpus.

  2. Add a hard-negative mining step: every other batch, select the hardest off-diagonal pair from the previous batch and append it. Train and check whether the contrastive loss drops faster.

  3. Add an image-text matching binary head on top of the joint embedding (true/false: do these match?) as a third loss, replicating BLIP's three-head setup.

  4. Replace the mock corpus with caption-id sequences drawn from a Markov chain whose transition matrix is conditioned on image hash. There is now real learnable signal, so the captioning loss should drop further.

  5. Train the same model with lm_weight = 0 and again with lm_weight = 1, then compare the contrastive loss. The LM loss should not regress the ranking objective.

Key Terms

TermWhat it means
InfoNCENoise contrastive estimation: cross-entropy on a similarity matrix
TemperatureScalar that controls how peaked the contrastive softmax is
Hard negativeAn off-diagonal pair the model finds confusing, useful for sampling
LM lossStandard next-token cross-entropy on the captioning side
Joint embedding spaceThe shared space where image and text vectors live after projection
Author's Note

My bet is on reading each loss against its own baseline instead of watching the sum. The uniform-guess baselines are ln(16) = 2.77 for the contrastive loss and ln(512) ≈ 6.24 for the LM loss. The seeded run starts above both (3.05 and 6.50) and ends at 2.36 and 4.70, so those are the numbers I'd keep in front of me while the run prints every 5 steps. I'd also keep tau learned, as CLIP does, rather than fixing it, since a temperature that is too small or too large both cause trouble. Before adding a third loss like the matching head in exercise 3, I'd run exercise 5 first, so I know what the LM term does to ranking on its own.

Further Reading

  • CLIP paper for the original contrastive recipe.
  • CoCa paper for contrastive plus captioning in one model.
  • SigLIP paper for the sigmoid pair-loss variant and why it scales better.

Code