TAI BUI
Back to curriculum
CapstonePython

Projection Layer for Modality Alignment

A vision encoder produces image tokens and a text decoder consumes text tokens, and the two live in different vector spaces. A small two-layer MLP projects the image tokens into the text embedding space, and a cosine alignment loss against a paired caption pulls the two spaces into agreement. That projection is the smallest piece of a vision-language model and the one that matters most for transfer.

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

Learning Objectives

  • Build a two-layer MLP projection that maps image features into the text embedding space.
  • Construct a mock text embedding table (no pretrained tokenizer, no real corpus).
  • Compute a cosine alignment loss between projected image tokens and a paired caption embedding.
  • Train the projection alone with a frozen vision encoder and a frozen text table.

The Problem

A projection layer is a small network that maps vectors from one embedding space into another. Here it maps vision-encoder tokens into a text decoder's embedding space. The encoder (lessons 58-59) produces tokens of dimension vision_hidden = 768. The decoder you want to bolt on top has embedding dimension text_hidden = 512 (any other number is just as plausible) and expects text-shaped tokens. Image tokens aren't text-shaped: they live in a basis the encoder learned during vision-only pretraining, with no relationship to the decoder's word vectors.

A two-layer MLP projection (linear, GELU, linear) bridges the gap. It has about 768 * 1024 + 1024 * 512 = 1.3M parameters, small enough to train in minutes on a single GPU, and it is the piece that has to learn during the alignment phase. The vision encoder stays frozen. The text embedding table stays frozen. Only the projection moves. LLaVA shipped this recipe in 2023, BLIP-2 reframed it as a Q-Former, and open-weight VLMs since have adopted it in some form.

Warm-up0/3
1. An image model and a language model were trained separately. Can you compare a vector from one directly with a vector from the other?Warm-up
2. Two vectors point the same way but one is three times longer. Which measure rates them as identical?Warm-up
3. You stack two linear layers with no activation function between them. What functions can the stack represent?Warm-up
Check: The Problem0/3
1. What is a projection head?Define
2. Why can the vision encoder's tokens not be fed straight into the text decoder?Explain
3. You have a frozen ViT emitting 768-dim tokens and a frozen LLM with 4096-dim embeddings. What is the minimal trainable piece to bridge them?Design

The Concept

Mermaid diagram

Pooling before projection

Pooling collapses a sequence of token vectors into one vector. The vision encoder emits 197 tokens and the text side has a single caption-level embedding, so alignment needs one image-level vector per sample. CLS pooling is the simplest: take the first token from the encoder and project it. Mean pooling over all 197 tokens is another option. Either one reduces 197 vectors to one.

Why two layers and not one

A single linear projection can rotate and rescale but cannot fix the basis if the two spaces have curvature mismatches. A GELU between two linear layers adds one non-linear bend, which is empirically enough to align CLIP-style features to language model embeddings. Deeper projections are extensions (LLaVA-NeXT used GLU, Qwen-VL used a stack of attention layers). The two-layer MLP is the canonical baseline, and it is what BLIP-2's Q-Former projection head ships with under the hood.

LayerShapeParameters
fc1(vision_hidden, projection_hidden)768 * 1024 + 1024
activationGELU0
fc2(projection_hidden, text_hidden)1024 * 512 + 512

About 1.3M parameters for a 768 -> 1024 -> 512 head.

Cosine alignment loss

Cosine alignment loss is 1 - cos_sim(image, text). Align does not mean image_emb == text_emb. It means image_emb points in the same direction as text_emb in the joint space. The loss ranges from 0 (perfectly aligned) to 2 (opposite), and training drives it toward zero per pair. Lesson 62 generalizes to a contrastive batch (InfoNCE) where each image must be closer to its own caption than to any other caption in the batch. I use the per-pair version here so the dynamics are visible.

Frozen encoder is the trick

Freezing a model means setting requires_grad=False on its parameters so training leaves them alone. The vision encoder here is a small ViT (hidden=768, depth=4, heads=8, mlp_ratio=2.0) with roughly 20M parameters, and the text table has another couple of million (4096 * 512). A production encoder like CLIP ViT-L is far larger. Training all of them from a mock corpus is a non-starter. With both frozen, the projection's 1.3M parameters are what change, and a few hundred steps on synthetic pairs is enough to drive the loss down. Adapter-based VLMs share this operational shape: the heavy parts stay frozen and the light bridge trains.

Check: The Concept0/14
1. What is modality alignment?Define
2. How is cosine similarity computed?Define
3. What does it mean for an encoder to be frozen?Define
4. What does pooling do to a sequence of token vectors?Define
5. Why is the vision encoder frozen during this alignment stage?Explain
6. Why does CLS pooling get used to produce a single image-level vector from 197 tokens?Explain
7. Why does the lesson use two linear layers with a GELU between them rather than one linear layer?Explain
8. Why does the cosine alignment loss not require image_emb to equal text_emb?Explain
9. This lesson uses a per-pair cosine loss instead of a contrastive batch loss like InfoNCE. What is the stated reason?Explain
10. What does cosine_alignment_loss(image_emb, text_emb) return when the two vectors point in opposite directions?Apply
11. image_emb = [1, 0] and text_emb = [0, 1] are two perpendicular vectors. What does 1 - cos_sim give?Apply
12. A teammate sees the projected image vector has norm 5 while the caption vector has norm 1, and predicts a large cosine loss. What is the flaw?Analyze
13. You keep the frozen 768-dim ViT but switch to a text decoder whose embedding dim is 2048. Using the lesson's 768 -> 1024 -> 512 head as the template, what must change?Design
14. You want evidence that the GELU layer helps on natural images. Which experiment fits the lesson's approach?Design

Build It

code/main.py implements:

  • MLPProjector(in_dim, hidden_dim, out_dim), two-layer linear MLP with GELU activation.
  • MockTextEmbedding(vocab_size, dim), a frozen embedding table with deterministic init from a seed.
  • make_pair(seed, vocab_size, max_len), which synthesizes one paired (image, caption) sample. Captions are short id sequences; the caption embedding is mean-pooled over token embeddings.
  • cosine_alignment_loss(image_emb, text_emb), the per-pair 1 - cos_sim objective.
  • A training loop that runs the projection for 200 steps over 32 synthetic pairs (cycled), with the vision encoder and text table frozen, and prints the loss every 25 steps.

Run it:

bash
python3 code/main.py

Expected output: the loss drops from around 1.07 to about 0.80 within 200 steps, which shows the projection alone can pull image tokens toward the text space. The final cosine similarity is printed too, as a single number: the mean over the last batch (one pair per step).

Check: Build It0/7
1. What is a mock corpus in this project?Define
2. How does the mock text table turn a padded caption of token ids into one target vector?Explain
3. With vision_hidden=768 and projection_hidden=1024, how many parameters does fc1 hold, weights and biases together?Apply
4. The run trains for 200 steps over 32 pairs, cycling through them in order from step 0. Which pair is used at the final step, step 199?Apply
5. A run ends with a final loss of about 0.80. Since the loss is 1 - cos, what is the final cosine similarity, roughly?Apply
6. A teammate skips pooling and feeds all 197 projected tokens, shape (1, 197, 512), to cosine_alignment_loss against a caption embedding of shape (1, 512). What happens?Analyze
7. A 200-step run on synthetic pairs drops the loss from about 1.07 to about 0.80. A teammate says this proves two layers beat one linear layer on natural images. What is the right reading?Analyze

Use It

The same pattern shows up across open-weight VLMs:

  • LLaVA 1.5. Two-layer GELU MLP projection from CLIP-ViT-L hidden to LLaMA embedding dim. Frozen vision encoder, frozen LLM, train only the projection (then unfreeze the LLM in stage two).
  • BLIP-2. Q-Former takes 32 learned query tokens through cross-attention against image tokens, then projects to the LLM embedding dim. The projection head at the very end of Q-Former is the analog of the MLP here.
  • MiniGPT-4. Single linear projection from BLIP-2 Q-Former output to Vicuna embedding dim.
  • Qwen-VL. Cross-attention adapter with several layers, but the final piece is again a projection to the LM embedding dim.

The shape varies but the role stays the same: pool image tokens, project to text embedding dim, train alone.

Check: Use It0/4
1. Which production system maps most directly to this lesson's pattern?Explain
2. How does BLIP-2 implement the equivalent of this lesson's projection module?Explain
3. How does MiniGPT-4's bridge compare to LLaVA's two-layer MLP?Explain
4. What does Qwen-VL use as its bridge between vision tokens and the language model?Explain

Tests

code/test_main.py holds 8 tests covering:

  • projector output shape matches the configured out_dim
  • projector gradient flows after one backward pass
  • frozen text embedding table has zero requires_grad parameters
  • text pooling ignores padding: a caption padded with id 0 pools to the same vector as the unpadded one
  • cosine loss is zero on identical vectors and is 2 on anti-parallel vectors (two tests)
  • the training loop reduces loss between its first and last step (40 steps on 4 pairs)
  • make_pair returns the expected image and token-id shapes, with ids inside the vocabulary

Run them:

bash
python3 -m unittest code/test_main.py
Check: Tests0/3
1. A projector is built with in_dim=64, hidden_dim=128, out_dim=32 and receives a batch x of shape (5, 64). What shape does proj(x) return?Apply
2. The caption [[5, 9, 3, 0, 0, 0]] is embedded by MockTextEmbedding, and so is [[5, 9, 3]]. How do the two caption embeddings compare?Apply
3. Suppose the pooling step averaged over all 16 slots, treating padding id 0 as an ordinary row of the table. What would go wrong for a caption with 4 real ids?Analyze

Exercises

  1. Replace CLS pooling with mean pooling over the 196 patch tokens and compare final loss after 200 steps. Mean pooling usually trains faster on synthetic data; CLS is more sample-efficient on natural images.

  2. Add a learned scalar temperature to the cosine loss (cos / tau) and observe what happens when tau is too small (gradient noise) or too large (loss plateaus high).

  3. Swap the two-layer MLP for a single linear layer and quantify the loss gap. The non-linearity matters more on natural image features and less on synthetic ones.

  4. Add a small L2 penalty on the projector weights and watch how it interacts with cosine alignment (cosine is scale-invariant, so the penalty mostly shrinks unused directions).

  5. Persist projector weights, then reload and run inference without the vision encoder backward pass to verify that only the projector is needed at deploy time.

Key Terms

TermWhat it means
Modality alignmentThe act of making image and text embeddings comparable in one shared space
Projection headThe small module that maps one space to another, usually a 2-layer MLP
Cosine similarityDot product divided by the product of L2 norms
Frozen encoderThe vision (or text) model has all parameters with requires_grad=False
Mock corpusSynthetic pairs used so training has no dataset download dependency
Author's Note

I'd freeze the encoder and the text table first and let only the 1.3M-parameter projection move. That keeps the experiment small enough to read, and I'd want it reading clearly before I reached for a deeper head. My bet is that when the loss stalls, a bigger projection is the wrong first change. I'd check the pooling and the loss before the architecture.

I'd also treat the synthetic run as a wiring check. A drop from about 1.07 to about 0.80 shows the projection alone can move image tokens toward the text space, and that is all I'd claim from it. I'd want natural image features before saying anything about whether two layers beat one.

Further Reading

  • LLaVA paper for the two-stage train (project, then unfreeze LM).
  • BLIP-2 paper for Q-Former as a learnable projection alternative.
  • Qwen-VL technical report for cross-attention adapters as deeper projection heads.

Code