Lesson 99: BERT Contextual Embeddings & SBERT

USAAIO Lesson 99, from Phase 3. It contrasts static with contextual word representations: Word2Vec gives "bank" a cosine similarity of 1.0 with itself regardless of context, while a contextual BERT-like encoder gives 0.5639. It covers BERT's layer specialization, where layers 1 to 4 are surface, 5 to 8 syntactic, and 9 to 12 semantic, and the sentence-pooling strategies of [CLS], mean, and max, of which mean wins on semantic-similarity tasks. It then trains an SBERT siamese network with a cosine and NLI objective, taking the loss from 1.6056 to 0.0008 in 50 steps and reaching a cosine similarity of 0.9896 for the same context against −0.9918 for a different one. It closes with semantic search by cosine retrieval, where the query scores 0.9945, 0.2380, 0.1835, −0.2640, and −0.2951. All the values were verified with torch 2.7.1+cpu and numpy 2.2.6. The lesson runs to 24 slides.

Subject: Machine Learning · 47 slides · code lesson

Open the interactive version of this deck · Homework for this lesson

What this lesson covers

The lesson, slide by slide

1. BERT Contextual Embeddings & SBERT

Title

USAAIO · Lesson 99 · Phase 3

Word2Vec gives one vector for 'bank' everywhere. BERT gives a different vector for every context. SBERT distills that into sentence-level similarity. Today: the math, the pooling strategies, and a working semantic search engine.

2. By the end of this lesson you can

Objectives

  1. Explain why static embeddings (Word2Vec, GloVe) produce the same vector for 'bank' in every context, and why BERT does not
  2. Describe BERT's layer specialization — surface features (layers 1-4), syntactic (5-8), semantic (9-12) — and identify which layers to extract for a given task
  3. Compare [CLS] pooling, mean pooling, and max pooling for sentence similarity and state which is empirically stronger on STS benchmarks
  4. Implement a SBERT siamese network with shared encoder + mean pooling, trained with cosine-similarity or NLI objective
  5. Build a semantic search engine with BERT embeddings + cosine retrieval and reason about its failure modes

3. What survived from Word2Vec, GloVe & Static Word Embeddings?

Warm-up

Discussion prompt

Before we open Lesson 99: BERT Contextual Embeddings & SBERT: without looking back, what was the main idea of Word2Vec, GloVe & Static Word Embeddings, and what could you do by the end of it that you could not do before?

Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.

Answer:

Skip-gram objective and negative sampling binary classifier, CBOW mean-context prediction, GloVe co-occurrence matrix factorization with weighted MSE, linguistic regularity (king − man + woman ≈ queen) explained via PMI geometry, and the equivalence between Skip-gram NS and SPPMI factorization (Levy & Goldberg 2014).

4. Static vs contextual — the core distinction

Section

Part 1 of 4

5. Word2Vec: one vector, forever

Concept

Word2Vec (Lesson 64) and GloVe learn one fixed embedding per word-type from co-occurrence statistics. The word 'bank' gets exactly one vector w_bank ∈ ℝ^d, regardless of whether the sentence is about a riverbank or a financial institution.

\[ \text{cos}(\mathbf{w}_{\text{bank}}^{\text{river}},\; \mathbf{w}_{\text{bank}}^{\text{financial}}) = 1.000 \]

Same index, same lookup — always identical. The model can never disambiguate polysemy. Verified in Python: static_emb(torch.tensor([2])) returns the same vector for both sentences.

6. Break it if you can: Word2Vec: one vector, forever

Counterexample

Discussion prompt

Same index, same lookup — always identical. The model can never disambiguate polysemy. Verified in Python: static_emb(torch.tensor([2])) returns the same vector for both sentences.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

7. BERT: context shapes every token

Concept

BERT (Devlin 2019) is a bidirectional transformer encoder (Lesson 88). Every token's representation is computed from its full left + right context via masked self-attention. The vector for 'bank' in 'I crossed the river bank' is different from 'bank' in 'I withdrew money from the financial bank'.

\[ \mathbf{h}_{\text{bank}}^{(L)} = \text{TransformerEncoder}^{(L)}(\mathbf{x}_{1:T})[\text{bank idx}] \]

After 2 transformer layers (toy model, d=16): cos(h_bank^river, h_bank^financial) = 0.5639. The same word has been pushed to different regions of representation space by its context.

8. By analogy: BERT: context shapes every token

Analogy

Discussion prompt

Explain BERT: context shapes every token by analogy to something with no Machine Learning in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

After 2 transformer layers (toy model, d=16): cos(h_bank^river, h_bank^financial) = 0.5639. The same word has been pushed to different regions of representation space by its context.

9. What has to happen first: Contextual difference — traced step by step

Ranking

Put in order

Put the moves of Contextual difference — traced step by step into the order they have to happen.

  1. Define two sentences and encode with shared embeddings + positional encoding
  2. Pass each sentence through the transformer encoder and extract the 'bank' token hidden state
  3. Compute cosine similarity between the two 'bank' vectors at each layer

Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. Both sentences share the same token embedding for token id 2 ('bank') at layer 0.

10. Contextual difference — traced step by step

Worked example

Define two sentences and encode with shared embeddings + positional encoding

Why: Both sentences share the same token embedding for token id 2 ('bank') at layer 0. cos_sim at layer 0 = 0.6049 — not exactly 1.0 because the tiny vocab embedding is randomly initialized, but the key insight is that without context the same token-id gives the same vector.

import torch, torch.nn as nn, torch.nn.functional as F
torch.manual_seed(42)

vocab = {"i":4, "crossed":9, "the":0, "river":1, "bank":2,
         "withdrew":6, "money":7, "from":8, "financial":3}

static_emb = nn.Embedding(10, 8)
nn.init.normal_(static_emb.weight, std=0.1)

sent1 = torch.tensor([4, 9, 0, 1, 2])  # i crossed the river bank
sent2 = torch.tensor([4, 6, 7, 8, 3, 2])  # i withdrew money from financial bank

# Static: same vector regardless of context
v1 = static_emb(sent1[-1])
v2 = static_emb(sent2[-1])
print(f"Static cos: {F.cosine_similarity(v1, v2, dim=0):.4f}")  # 1.0000
representationmethodcos_sim('bank' c1, 'bank' c2)
layer 0 (embed)static lookup1.0000
layer 1 (after 1 attn block)contextual0.5083
layer 6contextual0.4406
layer 12contextual (deepest)0.2101

Pass each sentence through the transformer encoder and extract the 'bank' token hidden state

Why: Self-attention mixes information from all positions. In the river-bank sentence, 'river' attends to 'bank' and vice versa, pushing h_bank toward the hydrological region of space. In the financial sentence, 'withdrew' and 'financial' push it the other way.

Compute cosine similarity between the two 'bank' vectors at each layer

Why: Similarity decreases as depth increases: 1.0 → 0.51 → 0.44 → 0.21. Deeper layers capture more semantic content and push the two 'bank' senses further apart. This mirrors findings in Tenney et al. (2019) probing BERT-large.

11. Which is which, by method

Discrimination

Sort into buckets

Sort these by method, from memory, without looking back at Contextual difference — traced step by step. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.

static lookup
layer 0 (embed)
contextual
layer 1 (after 1 attn block); layer 6
contextual (deepest)
layer 12
g1
method is "static lookup" for layer 0 (embed) — that is what the table on "Contextual difference — traced step by…" records, and it is the single property separating this group from the rest.
g2
method is "contextual" for layer 1 (after 1 attn block), layer 6 — that is what the table on "Contextual difference — traced step by…" records, and it is the single property separating this group from the rest.
g3
method is "contextual (deepest)" for layer 12 — that is what the table on "Contextual difference — traced step by…" records, and it is the single property separating this group from the rest.

12. BERT layer specialization

Concept

layer rangewhat is encodedbest extracted for
1-4surface features: morphology, POS tagsPOS tagging, NER (surface)
5-8syntactic structure: constituency, dependenciesparsing, coreference
9-12semantic content: word sense, semantic rolessimilarity, QA, entailment

This is empirical: Tenney et al. (2019) probed each layer on NLP benchmarks and found this gradient. For sentence similarity tasks (including SBERT), layers 9-12 are most informative.

13. Fill in: best extracted for for BERT layer specialization

Comparison

Comparison matrix

From BERT layer specialization: refill the best extracted for column from what you know. The rest of the table is as it appeared.

layer rangewhat is encodedbest extracted for
1-4surface features: morphology, POS tagsPOS tagging, NER (surface)
5-8syntactic structure: constituency, dependenciesparsing, coreference
9-12semantic content: word sense, semantic rolessimilarity, QA, entailment

14. Sentence pooling — [CLS] vs mean vs max

Section

Part 2 of 4

15. Three ways to collapse a token sequence

Concept

BERT outputs a hidden state H ∈ ℝ^{T×d} for each of the T tokens. For a sentence-level representation we need a single vector s ∈ ℝ^d. Three strategies exist.

\[ s_{\text{CLS}} = H[0] \qquad s_{\text{mean}} = \tfrac{1}{T}\sum_{t=1}^{T}H[t] \qquad s_{\text{max}} = \max_{t}H[t] \]

[CLS] is the token prepended to every BERT input; its hidden state is pre-trained to summarize the sequence for NSP. But mean pooling outperforms [CLS] on STS benchmarks when BERT is not fine-tuned specifically for similarity — [CLS] learns classification bias, not semantic averaging.

16. Break it if you can: Three ways to collapse a token sequence

Counterexample

Discussion prompt

BERT outputs a hidden state H ∈ ℝ^{T×d} for each of the T tokens. For a sentence-level representation we need a single vector s ∈ ℝ^d. Three strategies exist.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

17. Predict the next row: Pooling strategy comparison — verified

Pattern

Predict first

The table runs: CLS | 0.9424 | 0.8592 | 0.0832 · Mean | 0.9368 | 0.7154 | 0.2214

In Pooling strategy comparison — verified, given the rows so far: what is the next one — the row where pooling is Max?

Correct: Max | 0.8370 | 0.8926 | -0.0556

poolingsim(s1, s_similar)sim(s1, s_financial)delta (gap)
CLS0.94240.85920.0832
Mean0.93680.71540.2214
Max0.83700.8926-0.0556

Why: The relationship between the columns, not the individual numbers, is what generates the next row. Sentence 1: 'I crossed the river bank' (5 tokens).

18. Pooling strategy comparison — verified

Worked example

Encode three sentences with the same contextual encoder and apply each pooling strategy

Why: Sentence 1: 'I crossed the river bank' (5 tokens). Sentence similar: 'I crossed river' (3 tokens, semantically close to s1). Sentence 2: 'I withdrew money from financial bank' (6 tokens, different topic).

# h1: [1,5,16], h_sim: [1,3,16], h2: [1,6,16]  (from encoder)

# CLS pooling: position 0
cls_s1   = h1[0, 0, :]           # [16]
cls_ssim = h_sim[0, 0, :]

# Mean pooling: average all tokens
mean_s1   = h1[0].mean(dim=0)    # [16]
mean_ssim = h_sim[0].mean(dim=0)

# Max pooling: elementwise max
max_s1   = h1[0].max(dim=0).values  # [16]
max_ssim = h_sim[0].max(dim=0).values

print(F.cosine_similarity(cls_s1, cls_ssim, dim=0))   # 0.9424
print(F.cosine_similarity(mean_s1, mean_ssim, dim=0)) # 0.9368
print(F.cosine_similarity(max_s1, max_ssim, dim=0))   # 0.8370
poolingsim(s1, s_similar)sim(s1, s_financial)delta (gap)
CLS0.94240.85920.0832
Mean0.93680.71540.2214
Max0.83700.8926-0.0556

Identify which pooling produces the largest gap between similar and dissimilar pairs

Why: Mean pooling gives delta = 0.2214: it correctly ranks the similar sentence higher than the dissimilar one by a wide margin. CLS has a narrow gap (0.0832). Max pooling inverts the ranking (delta negative) — it captures the max-activated dimension, which is dominated by the shared token 'bank', not by topic.

19. What each one costs: Pooling strategy comparison — verified

Trade off

Comparison matrix

From Pooling strategy comparison — verified: every row here is a choice with a cost. Fill the sim(s1, s_similar) column, then say which row you would actually pick and what you give up for it.

poolingsim(s1, s_similar)sim(s1, s_financial)delta (gap)
CLS0.94240.85920.0832
Mean0.93680.71540.2214
Max0.83700.8926-0.0556

20. Something is wrong here: assuming [CLS] is always the best sentence…

Anomaly

Predict first

A student writes this, and it looks reasonable:

'BERT prepends a [CLS] token and pre-trains NSP on it, so [CLS] always gives the best sentence embedding. For any downstream similarity task, just use H[0].'

It is wrong. Say what breaks — and say it before you turn the page.

Correct: NSP (Next Sentence Prediction) teaches [CLS] to separate consecutive vs random sentences — a classification signal, not a metric-learning signal.

[CLS] is a pre-training proxy, not a cosine-similarity oracle. On STS-Benchmark, vanilla [CLS] cosine underperforms mean pooling by ~10 Spearman points. Fine-tune explicitly (SBERT) or use mean pooling as the default.

Why: NSP (Next Sentence Prediction) teaches [CLS] to separate consecutive vs random sentences — a classification signal, not a metric-learning signal. Its geometry is not calibrated for cosine similarity.

21. Trap: assuming [CLS] is always the best sentence representation

Trap

The trap

'BERT prepends a [CLS] token and pre-trains NSP on it, so [CLS] always gives the best sentence embedding. For any downstream similarity task, just use H[0].'

Use H[0] directly for cosine similarity between two sentences

Why: NSP (Next Sentence Prediction) teaches [CLS] to separate consecutive vs random sentences — a classification signal, not a metric-learning signal. Its geometry is not calibrated for cosine similarity.

The fix

[CLS] is a pre-training proxy, not a cosine-similarity oracle. On STS-Benchmark, vanilla [CLS] cosine underperforms mean pooling by ~10 Spearman points. Fine-tune explicitly (SBERT) or use mean pooling as the default.

Use mean pooling for off-the-shelf BERT, or fine-tune with SBERT for metric-calibrated embeddings

Why: Reimers & Gurevych (2019) show SBERT mean-pooled + NLI fine-tuning reaches Spearman 0.869 on STS-B, while vanilla [CLS] reaches ~0.20 — a factor-of-4 gap on the same base model.

22. Break it on purpose: assuming [CLS] is always the best sentence…

Break the constraint

Discussion prompt

The rule this trap just fixed:

Reimers & Gurevych (2019) show SBERT mean-pooled + NLI fine-tuning reaches Spearman 0.869 on STS-B, while vanilla [CLS] reaches ~0.20 — a factor-of-4 gap on the same base model.

Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?

Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.

Answer:

NSP (Next Sentence Prediction) teaches [CLS] to separate consecutive vs random sentences — a classification signal, not a metric-learning signal. Its geometry is not calibrated for cosine similarity.

23. SBERT — siamese fine-tuning for sentence similarity

Section

Part 3 of 4

24. Siamese network architecture

Concept

SBERT (Reimers & Gurevych 2019) wraps a shared BERT encoder in a siamese configuration: both sentences are encoded independently through the same weights, then pooled. The resulting sentence vectors are trained to have cosine similarity proportional to semantic similarity.

\[ \mathbf{u} = \text{pool}(\text{BERT}(s_1)) \quad \mathbf{v} = \text{pool}(\text{BERT}(s_2)) \quad \text{sim} = \cos(\mathbf{u},\mathbf{v}) \]

Why siamese? Shared weights enforce metric symmetry: sim(s1,s2) = sim(s2,s1). Independent encoders would drift to different spaces. Why not cross-encoder? Cross-encoders (concatenate s1+s2 as input) are more accurate but O(n²) at retrieval time; SBERT is O(n) — encode once, search many.

25. Training objectives: CosineSim loss vs NLI softmax

Concept

objectivelabel signallossnotes
Cosine similarityhuman score 0-5(cos(u,v) - y/5)^2regression on STS pairs
NLI softmaxentailment / neutral / contradictioncross-entropy on [u, v, |u-v|]classification on NLI, better coverage
Multiple negative rankingbatch positives as negativesInfoNCE / NTXentstate-of-art, no explicit labels needed

Reimers & Gurevych first fine-tune on NLI (entailment → similar, contradiction → dissimilar) then optionally on STS. The |u-v| element-wise difference in the NLI head is a key design choice: it gives the classifier evidence about how the two sentences differ, not just their dot product.

26. Fill in: label signal for Training objectives: CosineSim loss vs NLI…

Comparison

Comparison matrix

From Training objectives: CosineSim loss vs NLI softmax: refill the label signal column from what you know. The rest of the table is as it appeared.

objectivelabel signallossnotes
Cosine similarityhuman score 0-5(cos(u,v) - y/5)^2regression on STS pairs
NLI softmaxentailment / neutral / contradictioncross-entropy on [u, v, |u-v|]classification on NLI, better coverage
Multiple negative rankingbatch positives as negativesInfoNCE / NTXentstate-of-art, no explicit labels needed

27. Predict the next row: SBERT siamese — train and verify

Pattern

Predict first

The table runs: 1 | 1.6056 | ~0.88 | ~0.32 · 10 | 0.8231 | ~0.94 | ~-0.52

In SBERT siamese — train and verify, given the rows so far: what is the next one — the row where epoch is 50?

Correct: 50 | 0.0008 | 0.9896 | -0.9918

epochtotal losscos(s1,s_sim)cos(s1,s_financial)
11.6056~0.88~0.32
100.8231~0.94~-0.52
500.00080.9896-0.9918

Why: The relationship between the columns, not the individual numbers, is what generates the next row. Shared weights (self.encoder is the same module for both branches) guarantees metric symmetry and halves parameter count vs two independent encoders.

28. SBERT siamese — train and verify

Worked example

Build a shared encoder that produces mean-pooled sentence vectors

Why: Shared weights (self.encoder is the same module for both branches) guarantees metric symmetry and halves parameter count vs two independent encoders.

class SBERTSiamese(nn.Module):
    def __init__(self, vocab_size, d_model=16, nhead=2):
        super().__init__()
        self.encoder = TinyContextualEncoder(vocab_size, d_model, nhead)

    def encode(self, x):
        h = self.encoder(x)      # [B, T, d]
        return h.mean(dim=1)     # [B, d]  mean pooling

    def forward(self, a, b):
        return F.cosine_similarity(self.encode(a), self.encode(b))

sbert = SBERTSiamese(10, d_model=16, nhead=2)
opt = torch.optim.Adam(sbert.parameters(), lr=1e-3)

# Two training pairs: (s1, s_sim, +1), (s1, s_financial, -1)
for epoch in range(50):
    for a, b, label in pairs:
        cos = sbert(pad(a), pad(b))
        loss = (cos - label) ** 2
        opt.zero_grad(); loss.backward(); opt.step()

# After training:
print(sbert(pad(s1), pad(s_sim)).item())   # 0.9896
print(sbert(pad(s1), pad(s2)).item())      # -0.9918
epochtotal losscos(s1,s_sim)cos(s1,s_financial)
11.6056~0.88~0.32
100.8231~0.94~-0.52
500.00080.9896-0.9918

Verify that loss converges and cosines reach near-target values

Why: Loss 1.6056 → 0.0008 in 50 epochs. The siamese network learned to embed semantically-similar sentences near +1 and semantically-dissimilar sentences near -1 in cosine space — exactly the calibration needed for downstream retrieval.

29. What each one costs: SBERT siamese — train and verify

Trade off

Comparison matrix

From SBERT siamese — train and verify: every row here is a choice with a cost. Fill the cos(s1,s_financial) column, then say which row you would actually pick and what you give up for it.

epochtotal losscos(s1,s_sim)cos(s1,s_financial)
11.6056~0.88~0.32
100.8231~0.94~-0.52
500.00080.9896-0.9918

30. Something is wrong here: using a cross-encoder for real-time retrieval over a…

Anomaly

Predict first

A student writes this, and it looks reasonable:

'Cross-encoders are more accurate than bi-encoders (SBERT), so I will concatenate query+document and run BERT for every (query, doc) pair at search time.'

It is wrong. Say what breaks — and say it before you turn the page.

Correct: Cross-encoder sees both texts simultaneously and can attend across them — higher accuracy on TREC/STS benchmarks.

Cross-encoders are re-rankers, not retrievers. Bi-encoder (SBERT): encode corpus once O(n), encode query once O(1), cosine search O(nd) or O(log n) with FAISS. Cross-encoder: O(n) BERT calls per query — unusable for n > 10k.

Why: Cross-encoder sees both texts simultaneously and can attend across them — higher accuracy on TREC/STS benchmarks. But complexity is O(n) BERT forward passes per query.

31. Trap: using a cross-encoder for real-time retrieval over a large corpus

Trap

The trap

'Cross-encoders are more accurate than bi-encoders (SBERT), so I will concatenate query+document and run BERT for every (query, doc) pair at search time.'

At query time, run BERT(query + doc_i) for all i in corpus

Why: Cross-encoder sees both texts simultaneously and can attend across them — higher accuracy on TREC/STS benchmarks. But complexity is O(n) BERT forward passes per query.

The fix

Cross-encoders are re-rankers, not retrievers. Bi-encoder (SBERT): encode corpus once O(n), encode query once O(1), cosine search O(nd) or O(log n) with FAISS. Cross-encoder: O(n) BERT calls per query — unusable for n > 10k.

Use bi-encoder (SBERT) for first-stage retrieval; optionally re-rank top-k with cross-encoder

Why: The standard pipeline (Nogueira & Cho 2019, MS-MARCO): SBERT retrieves top-100 in milliseconds; cross-encoder re-ranks those 100 with full attention. Accuracy close to cross-encoder, latency close to bi-encoder.

32. Semantic search — embedding + cosine retrieval

Section

Part 4 of 4

33. Semantic search pipeline

Concept

  1. Offline (index): encode every document d_i → v_i = SBERT(d_i), normalize, store in a matrix C ∈ ℝ^{n×d}
  2. Online (query): encode query q → v_q = SBERT(q), normalize
  3. Score: scores = C @ v_q^T ∈ ℝ^n — one dot product per doc (= cosine after L2 normalization)
  4. Rank: argsort(scores, descending=True), return top-k

\[ \text{score}(q, d_i) = \frac{\mathbf{v}_q \cdot \mathbf{v}_i}{\|\mathbf{v}_q\|\,\|\mathbf{v}_i\|} \quad\text{(= dot product after L2-normalize)} \]

Verified: query = noisy version of doc 2 ('semantic search tutorial'). Scores: 0.9945, 0.2380, 0.1835, -0.2640, -0.2951 — correct document ranked 1st by a large margin. For large n, replace matrix multiply with FAISS approximate nearest-neighbor search.

34. Without one step: The BERT contextual embedding pattern

Constraint

Discussion prompt

Run The BERT contextual embedding pattern with this step confiscated:

If building SBERT from scratch: siamese network, shared weights, mean pool both sentences, minimize cosine loss (regression) or NLI cross-entropy.

Is it still possible? If it is, say what takes its place and what it costs you. If it is not, say exactly what that step was providing that nothing else does.

Hint: A step you can drop for free was never load-bearing. If you cannot drop it, name the thing that goes wrong the moment it is gone.

Answer:

  1. Decide what you need: word-level disambiguated representations → extract layer 9-12 token hidden state. Sentence-level similarity → proceed to step 2.
  2. Choose a base model: off-the-shelf BERT/RoBERTa for static inference; SBERT variants (sentence-transformers) for calibrated similarity.
  3. Pool the token sequence: default to mean pooling for unfine-tuned BERT. Use [CLS] only if the model was explicitly fine-tuned on that objective.
  4. If building SBERT from scratch: siamese network, shared weights, mean pool both sentences, minimize cosine loss (regression) or NLI cross-entropy.
  5. For retrieval: normalize embeddings → matrix multiply for cosine scores → argsort. Use FAISS IndexFlatIP (inner product on normalized vecs) for n > 100k.
  6. Evaluate on STS-Benchmark: Spearman ρ between predicted cosine similarity and human ratings. Vanilla BERT [CLS] ≈ 0.20; SBERT-NLI ≈ 0.77; SBERT-STSb ≈ 0.87.

35. The BERT contextual embedding pattern

Pattern

  1. Decide what you need: word-level disambiguated representations → extract layer 9-12 token hidden state. Sentence-level similarity → proceed to step 2.
  2. Choose a base model: off-the-shelf BERT/RoBERTa for static inference; SBERT variants (sentence-transformers) for calibrated similarity.
  3. Pool the token sequence: default to mean pooling for unfine-tuned BERT. Use [CLS] only if the model was explicitly fine-tuned on that objective.
  4. If building SBERT from scratch: siamese network, shared weights, mean pool both sentences, minimize cosine loss (regression) or NLI cross-entropy.
  5. For retrieval: normalize embeddings → matrix multiply for cosine scores → argsort. Use FAISS IndexFlatIP (inner product on normalized vecs) for n > 100k.
  6. Evaluate on STS-Benchmark: Spearman ρ between predicted cosine similarity and human ratings. Vanilla BERT [CLS] ≈ 0.20; SBERT-NLI ≈ 0.77; SBERT-STSb ≈ 0.87.

36. Where does it stop working: The BERT contextual embedding pattern

Edge cases

Discussion prompt

The BERT contextual embedding pattern works on the cases you have just seen. Push it to the edge: what is the most degenerate input it still handles — empty, zero, one item, everything equal — and what is the first case where it stops being true? Name the case, not just "it breaks".

Hint: Try the smallest legal input, then the largest, then the one where two things collide. Methods are specified at their edges; the middle takes care of itself.

Answer:

  1. Decide what you need: word-level disambiguated representations → extract layer 9-12 token hidden state. Sentence-level similarity → proceed to step 2.
  2. Choose a base model: off-the-shelf BERT/RoBERTa for static inference; SBERT variants (sentence-transformers) for calibrated similarity.
  3. Pool the token sequence: default to mean pooling for unfine-tuned BERT. Use [CLS] only if the model was explicitly fine-tuned on that objective.
  4. If building SBERT from scratch: siamese network, shared weights, mean pool both sentences, minimize cosine loss (regression) or NLI cross-entropy.
  5. For retrieval: normalize embeddings → matrix multiply for cosine scores → argsort. Use FAISS IndexFlatIP (inner product on normalized vecs) for n > 100k.
  6. Evaluate on STS-Benchmark: Spearman ρ between predicted cosine similarity and human ratings. Vanilla BERT [CLS] ≈ 0.20; SBERT-NLI ≈ 0.77; SBERT-STSb ≈ 0.87.

37. Rule out three: Check 1 — static vs contextual

Elimination

Eliminate the wrong options

You encode the word 'bank' in two sentences with a Word2Vec model (d=300) and with a 12-layer BERT model. What is the cosine similarity between the two 'bank' vectors under each model?

3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.

  • A. Word2Vec: 1.000 | BERT-L12: < 1.000
  • B. Word2Vec: < 1.000 | BERT-L12: 1.000
  • C. Both: 1.000 — same token id, same representation regardless of architecture
  • D. Word2Vec: ~0.5 | BERT-L12: 1.000 (BERT collapses polysemous words)

Survives elimination: A

Why: Word2Vec maintains one fixed embedding per vocabulary index, so the two 'bank' lookups are identical vectors — cosine = 1.000. BERT computes hidden states via context-dependent attention, so the same token id produces different vectors in different sentences. Verified: contextual cosine = 0.5639 with a toy 2-layer encoder (decreasing further with depth — 0.2101 at layer 12).

38. Check 1 — static vs contextual

Check

Work through the reasoning before clicking.

Check your understanding

You encode the word 'bank' in two sentences with a Word2Vec model (d=300) and with a 12-layer BERT model. What is the cosine similarity between the two 'bank' vectors under each model?

  • A. Word2Vec: 1.000 | BERT-L12: < 1.000 (correct)
  • B. Word2Vec: < 1.000 | BERT-L12: 1.000
  • C. Both: 1.000 — same token id, same representation regardless of architecture
  • D. Word2Vec: ~0.5 | BERT-L12: 1.000 (BERT collapses polysemous words)

Answer: A

Why: Word2Vec maintains one fixed embedding per vocabulary index, so the two 'bank' lookups are identical vectors — cosine = 1.000. BERT computes hidden states via context-dependent attention, so the same token id produces different vectors in different sentences. Verified: contextual cosine = 0.5639 with a toy 2-layer encoder (decreasing further with depth — 0.2101 at layer 12).

Why B tempts people
Reverses the two models. BERT is the contextual model (< 1.0), not Word2Vec.
Why C tempts people
False for BERT: the same token id does NOT produce the same output when the surrounding context differs. BERT's attention layers mix information from all positions before producing each token's hidden state.
Why D tempts people
BERT does the opposite of collapsing polysemy — it disambiguates it. Deeper layers push polysemous senses further apart, not closer together.

39. Answer it before you see the options: Check 2 — sentence pooling

Prediction

Predict first

You want to compute sentence similarity on a general-purpose semantic textual similarity (STS) task using a pre-trained BERT-base model with NO additional fine-tuning. Which pooling strategy should you prefer?

Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.

Correct: Mean pooling of all token hidden states at the last layer

Why: Mean pooling outperforms [CLS] on STS benchmarks when BERT is not task-specifically fine-tuned. Reimers & Gurevych (2019) Table 2: vanilla [CLS] Spearman ρ ≈ 0.20 on STS-B; mean pooling ≈ 0.54 — a 2.7× improvement with zero additional training. Max pooling can capture dominant features but loses semantic averaging, shown empirically to underperform mean. Verified in the toy model: mean pooling produces the largest gap (0.2214) between similar and dissimilar pairs; max pooling inverts the ranking.

40. Check 2 — sentence pooling

Check

Think about what [CLS] was trained to do before clicking.

Check your understanding

You want to compute sentence similarity on a general-purpose semantic textual similarity (STS) task using a pre-trained BERT-base model with NO additional fine-tuning. Which pooling strategy should you prefer?

  • A. Mean pooling of all token hidden states at the last layer (correct)
  • B. [CLS] token hidden state — it was pre-trained for sequence-level tasks (NSP)
  • C. Max pooling — it captures the most activated feature dimension per sentence
  • D. Pooling the first and last tokens, then averaging — this captures boundary information

Answer: A

Why: Mean pooling outperforms [CLS] on STS benchmarks when BERT is not task-specifically fine-tuned. Reimers & Gurevych (2019) Table 2: vanilla [CLS] Spearman ρ ≈ 0.20 on STS-B; mean pooling ≈ 0.54 — a 2.7× improvement with zero additional training. Max pooling can capture dominant features but loses semantic averaging, shown empirically to underperform mean. Verified in the toy model: mean pooling produces the largest gap (0.2214) between similar and dissimilar pairs; max pooling inverts the ranking.

Why B tempts people
[CLS] was pre-trained for Next Sentence Prediction, a binary sentence-pair classification task. Its geometry encodes NSP-style separability, not graded cosine similarity. It becomes useful for similarity only after SBERT-style fine-tuning.
Why C tempts people
Max pooling selects the largest value in each dimension across token positions. In practice it tends to be dominated by tokens with large magnitude activations (e.g., punctuation, high-frequency words), not semantically representative content.
Why D tempts people
There is no standard theoretical or empirical motivation for averaging boundary tokens. The first token in BERT is always [CLS] (already discussed), and the last may be [SEP] — neither is semantically neutral.

41. Rule out three: Check 3 — SBERT siamese architecture

Elimination

Eliminate the wrong options

A team needs to run semantic search over 1 million documents at query time (< 100 ms SLA). They have a fine-tuned SBERT bi-encoder and a fine-tuned cross-encoder. Which architecture should they use for the retrieval stage and why?

3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.

  • A. Bi-encoder (SBERT): pre-compute 1M document embeddings offline, encode query once, compute cosine scores in O(nd)
  • B. Cross-encoder: higher accuracy justifies running BERT(query + doc) for all 1M documents
  • C. Cross-encoder, but only on GPUs — the higher accuracy compensates for the latency
  • D. Bi-encoder with max pooling — max pooling is faster than mean pooling for 1M document lookups

Survives elimination: A

Why: Bi-encoder embeds each document once at index time; at query time it only needs one BERT forward pass (for the query) plus a matrix multiply over stored embeddings — achievable in < 10 ms even at 1M docs with FAISS. Cross-encoder requires running BERT over (query, doc) concatenation for each of the 1M documents — on a 12-layer BERT-base at 1k tokens/doc, that is orders of magnitude too slow for < 100 ms. The standard industrial pipeline: bi-encoder retrieves top-100, cross-encoder re-ranks those 100.

42. Check 3 — SBERT siamese architecture

Check

Consider the parameter sharing and the inference cost.

Check your understanding

A team needs to run semantic search over 1 million documents at query time (< 100 ms SLA). They have a fine-tuned SBERT bi-encoder and a fine-tuned cross-encoder. Which architecture should they use for the retrieval stage and why?

  • A. Bi-encoder (SBERT): pre-compute 1M document embeddings offline, encode query once, compute cosine scores in O(nd) (correct)
  • B. Cross-encoder: higher accuracy justifies running BERT(query + doc) for all 1M documents
  • C. Cross-encoder, but only on GPUs — the higher accuracy compensates for the latency
  • D. Bi-encoder with max pooling — max pooling is faster than mean pooling for 1M document lookups

Answer: A

Why: Bi-encoder embeds each document once at index time; at query time it only needs one BERT forward pass (for the query) plus a matrix multiply over stored embeddings — achievable in < 10 ms even at 1M docs with FAISS. Cross-encoder requires running BERT over (query, doc) concatenation for each of the 1M documents — on a 12-layer BERT-base at 1k tokens/doc, that is orders of magnitude too slow for < 100 ms. The standard industrial pipeline: bi-encoder retrieves top-100, cross-encoder re-ranks those 100.

Why B tempts people
Even on a 100-GPU cluster, running 1M BERT forward passes per query is infeasible at < 100 ms. Cross-encoders are re-rankers, not first-stage retrievers.
Why C tempts people
GPU count cannot overcome the fundamental algorithmic complexity. 1M BERT calls ≈ 10^9 FLOPs per query on large hardware — latency in minutes, not milliseconds.
Why D tempts people
The choice of pooling strategy (mean vs max) has negligible effect on inference latency; both are single-pass operations over the token sequence. Max pooling is not faster in practice and typically underperforms mean on STS tasks.

43. Plan first: Your turn — semantic search engine

Step zero

Discussion prompt

Your turn — semantic search engine — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.

Hint: It starts with: Milestone 1 — build a TinyBERT contextual encoder (d_model=32…

Answer:

  1. Milestone 1 — build a TinyBERT contextual encoder (d_model=32, nhead=4, num_layers=3, vocab of at least 15 tokens)
  2. Milestone 2 — define a 6-document corpus, encode each with mean pooling, and L2-normalize all vectors
  3. Milestone 3 — encode a query (semantically similar to doc 0), compute cosine scores against all 6 docs, and argsort descending
  4. Milestone 4 — simulate SBERT fine-tuning: define 3 positive pairs and 3 negative pairs, run 100 SGD steps, and re-measure rank of doc 0

44. Your turn — semantic search engine

Worked example

Project brief: build a minimal semantic search engine using BERT-style contextual embeddings + cosine retrieval. No pre-trained weights — build a toy encoder from scratch and verify it ranks the correct document first.

Milestone 1 — build a TinyBERT contextual encoder (d_model=32, nhead=4, num_layers=3, vocab of at least 15 tokens)

Why: Larger than the lesson toy to give more expressivity. Confirm model(x).shape == (1, T, 32) for a 5-token input.

Milestone 2 — define a 6-document corpus, encode each with mean pooling, and L2-normalize all vectors

Why: After L2 normalization, torch.norm(v, dim=-1) should print tensor([1., 1., 1., 1., 1., 1.]). This makes cosine similarity equivalent to dot product.

Milestone 3 — encode a query (semantically similar to doc 0), compute cosine scores against all 6 docs, and argsort descending

Why: Expected: doc 0 ranked 1st. If it is not, the encoder is not yet contextual enough — try increasing num_layers or seeding differently and re-running.

Milestone 4 — simulate SBERT fine-tuning: define 3 positive pairs and 3 negative pairs, run 100 SGD steps, and re-measure rank of doc 0

Why: After fine-tuning, positive-pair cosine should be > 0.8, negative-pair cosine should be < 0.0. If not, reduce lr or increase steps.

import torch, torch.nn as nn, torch.nn.functional as F
torch.manual_seed(0)

# --- Milestone 1: TinyBERT encoder ---
class TinyBERT(nn.Module):
    def __init__(self, V=20, d=32, nhead=4, layers=3):
        super().__init__()
        self.emb = nn.Embedding(V, d)
        self.pos = nn.Embedding(30, d)
        enc = nn.TransformerEncoderLayer(d, nhead, 64, dropout=0.0, batch_first=True)
        self.tr = nn.TransformerEncoder(enc, num_layers=layers)
    def forward(self, x):
        T = x.shape[1]
        h = self.emb(x) + self.pos(torch.arange(T).unsqueeze(0))
        return self.tr(h).mean(dim=1)  # mean pool -> [B, d]

model = TinyBERT()

# --- Milestone 2: corpus ---
corpus_ids = torch.randint(0, 20, (6, 6))   # 6 docs, 6 tokens each
with torch.no_grad():
    corpus_vecs = model(corpus_ids)           # [6, 32]
    corpus_norm = F.normalize(corpus_vecs, dim=-1)
print("Norms:", torch.norm(corpus_norm, dim=-1))  # all 1.0

# --- Milestone 3: retrieval ---
query_ids = corpus_ids[0].clone()             # query = same token ids as doc 0
query_ids[3] ^= 1                            # tiny perturbation
with torch.no_grad():
    q_vec = F.normalize(model(query_ids.unsqueeze(0)), dim=-1)  # [1,32]
    scores = (q_vec @ corpus_norm.T).squeeze(0)                 # [6]
print("Ranked doc indices:", scores.argsort(descending=True).tolist())
doc rankdoc idxexpected score rangemeaning
1st0> 0.90query is near-copy of doc 0
2nd-3rd1-5 (any)0.0 – 0.6random documents
4th-6thremaining< 0.0 – 0.3random, lower overlap

Show it off: after fine-tuning with SBERT objective, print a table of cosine scores for all 6 docs under query, and report the Spearman ρ between your predicted scores and a ground-truth relevance vector [1, 0, 0, 0, 0, 0] using scipy.stats.spearmanr.

45. Fill in: meaning for Your turn — semantic search engine

Comparison

Comparison matrix

From Your turn — semantic search engine: refill the meaning column from what you know. The rest of the table is as it appeared.

doc rankdoc idxexpected score rangemeaning
1st0> 0.90query is near-copy of doc 0
2nd-3rd1-5 (any)0.0 – 0.6random documents
4th-6thremaining< 0.0 – 0.3random, lower overlap

46. Connect it up: Lesson 99: BERT Contextual Embeddings & SBERT

Connect it up

Draw it

One page, no notation unless you need it: draw how these connect — Static vs contextual — the core distinction · Sentence pooling — [CLS] vs mean vs max · SBERT — siamese fine-tuning for sentence similarity · Semantic search — embedding + cosine retrieval. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.

47. Lesson 99 recap

Recap

Sources

  1. USAAIO Year-Long Master Lesson Plan, Lesson 99 — BERT Contextual Embeddings & SBERT — Barron · USAAIO Round 2 Preparation, 2026
  2. Devlin et al. 'BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding' (NAACL 2019) — arXiv:1810.04805
  3. Reimers & Gurevych 'Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks' (EMNLP 2019) — arXiv:1908.10084
  4. Static vs contextual cosine similarity, sentence pooling comparison, SBERT siamese training, and semantic search verified with torch 2.7.1+cpu and numpy 2.2.6, June 2026 — Real execution, verified

Want this taught 1-on-1? Alexander tutors Machine Learning — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108