LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnCore LLM FoundationsStatic to Contextual Embeddings
📝MediumNLP Fundamentals

Static to Contextual Embeddings

Turn token IDs into vectors, learn what nearby usage captures, and see why a word such as charge needs sentence-dependent representations.

19 min read
Learning path
Step 55 of 196 in the full curriculum
BPE, WordPiece, and SentencePiecePerplexity & Model Evaluation

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

The previous lesson turned text into token IDs. Those IDs are addresses, not meanings. The same ID can appear in a billing dispute and in a device instruction:

same-id-two-sentences.txt
1"dispute the charge" -> [91, 12, 407] 2"charge the scanner" -> [407, 12, 982]

Both lines contain ID 407, yet invoice and scanner point to different uses. If both lookups start from one row, where can that difference enter? An embedding stores a vector of numbers for each ID. A contextual representation can then rewrite that vector using words around this particular occurrence.

First inspect the static table. Then watch the same token leave with two different states.

Diagram showing Sentence, Token IDs, Static row E[id], and Mix neighbors.
Sentence, Token IDs, Static row E[id], and Mix neighbors.

The first stop is the lookup. Its limitation gives context mixing a job.

A token ID selects one stored row

Let vocabulary size be V and embedding dimension be d. The model stores an embedding matrix E with shape V x d.

The table answers one narrow question: given an ID, which vector enters the model?

  • Row E[token_id] is the starting vector for that token.
  • Training moves rows so tokens used in similar situations become useful for the prediction task.
  • Before any context mixing, every occurrence of the same ID receives that same row.

A one-hot row vector of length V with a 1 at token_id selects the same row. Multiplying it by E writes the lookup as matrix algebra.

If charge is ID 0 in a tiny table, both billing and device sentences start at E[0]. A production tokenizer might use 407 instead. The rule doesn't change: the integer is an index, not a sense label.

The lab uses a three-row table so every index is visible. Before running it, predict whether the two printed rows can differ.

01-look-up-token-rows.py
1vocab = {"charge": 0, "invoice": 1, "scanner": 2} 2embedding_table = [ 3 [0.80, 0.20], # charge 4 [0.20, 1.00], # invoice 5 [1.00, 0.10], # scanner 6] 7 8billing_charge = embedding_table[vocab["charge"]] 9device_charge = embedding_table[vocab["charge"]] 10 11print("billing start:", billing_charge) 12print("device start:", device_charge) 13print("same starting row:", billing_charge == device_charge)
Output
1billing start: [0.8, 0.2] 2device start: [0.8, 0.2] 3same starting row: True

The rows are equal, so no lookup-only operation can resolve the two senses. Training can make one row useful across its uses, but only a context-dependent computation can specialize an occurrence.

Nearby words leave a fingerprint

If encoder and decoder keep seeing layer and mask, their neighborhood fingerprints start to resemble each other. The same logic puts cache near prefix when both occur with reuse language. This is the distributional hunch: usage supplies a weak signal about role.

The simplest measurement is a co-occurrence count. For each token, count words inside a fixed context window.

02-count-context-neighbors.py
1from collections import Counter, defaultdict 2 3messages = [ 4 "encoder layer uses attention mask".split(), 5 "decoder layer uses attention mask".split(), 6 "encoder block mixes token context".split(), 7 "cache hit reuses prompt prefix".split(), 8 "prefix cache skips prompt compute".split(), 9] 10 11window = 2 12neighbors = defaultdict(Counter) 13 14for message in messages: 15 for center_index, center in enumerate(message): 16 left = max(0, center_index - window) 17 right = min(len(message), center_index + window + 1) 18 for context_index in range(left, right): 19 if context_index != center_index: 20 neighbors[center][message[context_index]] += 1 21 22print("encoder context:", neighbors["encoder"].most_common(4)) 23print("decoder context:", neighbors["decoder"].most_common(4)) 24print("cache context:", neighbors["cache"].most_common(4))
Output
1encoder context: [('layer', 1), ('uses', 1), ('block', 1), ('mixes', 1)] 2decoder context: [('layer', 1), ('uses', 1)] 3cache context: [('hit', 1), ('reuses', 1), ('prefix', 1), ('skips', 1)]

The sample exposes the fingerprints: encoder and decoder share layer and uses, while cache sees prefix and reuse language. The rows are still counts, not compact coordinates.

A count row is high-dimensional and noisy. Latent semantic analysis compressed a term-document matrix with singular value decomposition (SVD).[1]Reference 1Indexing by Latent Semantic Analysishttps://doi.org/10.1002/(SICI)1097-4571(199009)41:6%3C391::AID-ASI1%3E3.0.CO;2-9

The same move works on word-context counts: keep directions that explain the main neighborhoods, drop the rest.

Raw counts can mistake a common word for an informative neighbor. Positive pointwise mutual information (PPMI) keeps pairs that occur more often than chance and floors negative values at zero.

If #(w, c) is the count for word w with context c,

PMI(w,c)=log⁡#(w,c)⋅N#(w) #(c),PPMI(w,c)=max⁡(PMI(w,c),0)\text{PMI}(w, c) = \log \frac{\#(w, c) \cdot N}{\#(w)\, \#(c)}, \qquad \text{PPMI}(w, c) = \max(\text{PMI}(w, c), 0)PMI(w,c)=log#(w)#(c)#(w,c)⋅N​,PPMI(w,c)=max(PMI(w,c),0)

where N is the total of all counts, #(w) is the row sum, and #(c) is the column sum.

After compression, cosine similarity asks whether two vectors point the same way, ignoring length:

cos⁡(u,v)=u⋅v∥u∥ ∥v∥\cos(u, v) = \frac{u \cdot v}{\|u\|\, \|v\|}cos(u,v)=∥u∥∥v∥u⋅v​

Parallel vectors score 1. Orthogonal vectors score 0. Opposite vectors score -1.

The next lab makes the geometry visible. It starts with a 4×4 fixture: architecture words (encoder, decoder) share layer and mask; serving words (cache, prefix) share hit and reuse. It converts counts to PPMI, then keeps two SVD components. The power iteration is a small stand-in for a full SVD: find the strongest direction, subtract it, then find the next one.

Predict the result before running it: which pair should have cosine 1, and which pair should be separated?

03-compress-ppmi-with-svd.py
1import math 2 3words = ["encoder", "decoder", "cache", "prefix"] 4counts = [ 5 [8, 6, 0, 0], # encoder 6 [7, 6, 0, 0], # decoder 7 [0, 0, 8, 5], # cache 8 [0, 0, 7, 6], # prefix 9] 10 11def dot(a, b): 12 return sum(x * y for x, y in zip(a, b)) 13 14def cosine(a, b): 15 return dot(a, b) / math.sqrt(dot(a, a) * dot(b, b)) 16 17def ppmi_matrix(table): 18 row_sums = [sum(row) for row in table] 19 col_sums = [sum(table[i][j] for i in range(len(table))) for j in range(len(table[0]))] 20 total = sum(row_sums) 21 result = [] 22 for i, row in enumerate(table): 23 out = [] 24 for j, count in enumerate(row): 25 expected = row_sums[i] * col_sums[j] / total 26 pmi = math.log((count + 1e-9) / (expected + 1e-9)) 27 out.append(max(pmi, 0.0)) 28 result.append(out) 29 return result 30 31def matvec(matrix, vec): 32 return [sum(row[j] * vec[j] for j in range(len(vec))) for row in matrix] 33 34def transpose(matrix): 35 return [list(col) for col in zip(*matrix)] 36 37def normalize(vec): 38 length = math.sqrt(sum(x * x for x in vec)) 39 return [x / length for x in vec] 40 41def leading_component(matrix, steps=80): 42 left = [1.0] * len(matrix) 43 right_space = transpose(matrix) 44 for _ in range(steps): 45 right = normalize(matvec(right_space, left)) 46 left = normalize(matvec(matrix, right)) 47 scaled_right = matvec(right_space, left) 48 singular = math.sqrt(sum(x * x for x in scaled_right)) 49 return left, singular 50 51def minus_outer(matrix, left, singular, right): 52 return [ 53 [matrix[i][j] - singular * left[i] * right[j] for j in range(len(right))] 54 for i in range(len(left)) 55 ] 56 57ppmi = ppmi_matrix(counts) 58left1, s1 = leading_component(ppmi) 59right1 = normalize(matvec(transpose(ppmi), left1)) 60left2, s2 = leading_component(minus_outer(ppmi, left1, s1, right1)) 61vectors = [ 62 [left1[i] * math.sqrt(s1), left2[i] * math.sqrt(s2)] 63 for i in range(len(words)) 64] 65 66print("singular values:", round(s1, 3), round(s2, 3)) 67print("encoder vs decoder:", round(cosine(vectors[0], vectors[1]), 3)) 68print("encoder vs cache:", round(cosine(vectors[0], vectors[2]), 3))
Output
1singular values: 1.418 1.348 2encoder vs decoder: 1.0 3encoder vs cache: 0.0

The architecture rows become parallel. The serving rows land on a second axis at right angles to the first. Similar neighborhoods became similar directions, which is the distributional hunch made geometric.

Count matrix, PPMI matrix, and two-component SVD for encoder, decoder, cache, and prefix. Architecture counts occupy the upper-left block, serving counts occupy the lower-right block, the first two singular values are 1.418 and 1.348, and cosine is 1.0 inside a block versus 0.0 across blocks.
The fixture's two count blocks survive PPMI. Rank-2 SVD keeps singular values 1.418 and 1.348, so encoder sits with decoder and away from cache.

The toy blocks make the split obvious. Real corpora add messy wording, rare contexts, and words that play several roles. Counting is one route to this geometry, not the only route.

Predict the neighbors instead

Counting is explicit. Word2Vec learns the same kind of static table by trying to predict neighbors:

  • CBOW (continuous bag-of-words) predicts a center token from its surrounding tokens.
  • Skip-gram predicts surrounding tokens from a center token.

There is no label saying “similar.” Each correct prediction nudges vectors so tokens that support similar predictions can share a neighborhood. Mikolov and colleagues introduced the two architectures in 2013.[2]Reference 2Efficient Estimation of Word Representations in Vector Space.https://arxiv.org/abs/1301.3781

Word2Vec on the sentence request hit cache layer today with window size two around hit. CBOW averages request, cache, and layer to predict hit. Skip-gram starts at hit and predicts those same three neighbors as separate positive pairs.
At this boundary position, window size 2 gives hit three visible neighbors. CBOW folds them into one center prediction. Skip-gram reverses the arrows and creates three positive pairs.

Skip-gram turns each window into training pairs. With window size 2, center hit produces one positive pair for each visible neighbor. The center stays first in each pair.

Count those pairs before looking at the output: the window reaches request, cache, and layer, but not today.

04-create-skipgram-pairs.py
1tokens = "request hit cache layer today".split() 2window = 2 3pairs = [] 4 5for center_index, center in enumerate(tokens): 6 left = max(0, center_index - window) 7 right = min(len(tokens), center_index + window + 1) 8 for context_index in range(left, right): 9 if context_index != center_index: 10 pairs.append((center, tokens[context_index])) 11 12hit_pairs = [pair for pair in pairs if pair[0] == "hit"] 13print(hit_pairs)
Output
1[('hit', 'request'), ('hit', 'cache'), ('hit', 'layer')]

The output confirms the boundary: today is three positions away, so it never becomes a positive pair for hit. Each listed pair supplies one local prediction target.

Scoring every vocabulary item for every pair is expensive. A Skip-gram trainer can instead treat the observed neighbor as a positive example and draw a few unobserved tokens as negative samples. Levy and Goldberg showed that Skip-gram with negative sampling is closely related to factorizing a shifted PMI matrix. That link explains why prediction geometry often agrees with the count-based SVD geometry above.[3]Reference 3Neural Word Embedding as Implicit Matrix Factorization.https://arxiv.org/abs/1405.4053

The loss below is a small, inspectable version of that update. It pushes the center toward the true neighbor and away from sampled ones. σ is the logistic sigmoid.

−log⁡σ(u⋅v+)−∑n∈Nlog⁡σ(−u⋅vn)-\log \sigma(u \cdot v_+) - \sum_{n \in \mathcal{N}} \log \sigma(-u \cdot v_n)−logσ(u⋅v+​)−n∈N∑​logσ(−u⋅vn​)

A high positive dot product lowers the first term. A negative dot product lowers each negative-sample term.

05-compare-negative-sampling-loss.py
1import math 2 3center = [1.0, 0.0] 4observed_neighbor = [1.2, 0.1] 5wrong_neighbor = [-1.0, 0.1] 6negative_samples = [[-0.9, -0.2], [-1.1, 0.0]] 7 8def dot(a, b): 9 return sum(x * y for x, y in zip(a, b)) 10 11def sigmoid(x): 12 return 1.0 / (1.0 + math.exp(-x)) 13 14def negative_sampling_loss(center_vector, positive_vector, negatives): 15 positive_loss = -math.log(sigmoid(dot(center_vector, positive_vector))) 16 negative_loss = sum( 17 -math.log(sigmoid(-dot(center_vector, sample))) 18 for sample in negatives 19 ) 20 return positive_loss + negative_loss 21 22observed_loss = negative_sampling_loss(center, observed_neighbor, negative_samples) 23wrong_loss = negative_sampling_loss(center, wrong_neighbor, negative_samples) 24 25print("observed pair loss:", round(observed_loss, 3)) 26print("wrong pair loss:", round(wrong_loss, 3)) 27print("training prefers observed pair:", observed_loss < wrong_loss)
Output
1observed pair loss: 0.892 2wrong pair loss: 1.942 3training prefers observed pair: True

The observed pair has lower loss, so training prefers it. Repeating that update puts tokens with similar neighbor predictions near one another. No analogy puzzle is required to see the signal.

Local windows are one view. GloVe starts with the global table of how often each pair occurred anywhere in the corpus.

Fit the global count table

GloVe starts from those global co-occurrence counts. It fits a weighted least-squares objective: a word vector and a context vector, plus two biases, should reconstruct the logarithm of each observed count.[4]Reference 4GloVe: Global Vectors for Word Representation.https://aclanthology.org/D14-1162.pdf

The dot product is the model's compatibility score for that word-context pair.

wi⋅w~j+bi+b~j≈log⁡Xijw_i \cdot \tilde{w}_j + b_i + \tilde{b}_j \approx \log X_{ij}wi​⋅w~j​+bi​+b~j​≈logXij​

The logarithm compresses counts that span many orders of magnitude. A weighting function then limits how much very common pairs dominate:

f(x)={(x/xmax⁡)αif x<xmax⁡1otherwisef(x) = \begin{cases} (x / x_{\max})^{\alpha} & \text{if } x < x_{\max} \\ 1 & \text{otherwise} \end{cases}f(x)={(x/xmax​)α1​if x<xmax​otherwise​

The paper's default is xmax⁡=100x_{\max} = 100xmax​=100 and α=3/4\alpha = 3/4α=3/4. Rare pairs get small weights. Very frequent pairs stop gaining extra weight past the cutoff.

The lab fixes the model score at log(30). Predict which count has zero residual, then see how weighting changes with frequency.

06-fit-glove-log-count.py
1import math 2 3def squared_glove_residual(observed_count, model_score): 4 target = math.log(observed_count) 5 return (model_score - target) ** 2 6 7def glove_weight(count, xmax=100, alpha=0.75): 8 if count < xmax: 9 return (count / xmax) ** alpha 10 return 1.0 11 12model_score = math.log(30) 13matching_error = squared_glove_residual(30, model_score) 14different_error = squared_glove_residual(3, model_score) 15 16print("log targets:", [round(math.log(count), 3) for count in (3, 30, 300)]) 17print("matching count error:", round(matching_error, 3)) 18print("different count error:", round(different_error, 3)) 19print("weights for 3, 30, 100:", [round(glove_weight(count), 3) for count in (3, 30, 100)])
Output
1log targets: [1.099, 3.401, 5.704] 2matching count error: 0.0 3different count error: 5.302 4weights for 3, 30, 100: [0.072, 0.405, 1.0]

The score matches log(30), so count 30 has zero residual. Count 3 misses that target and pays a residual of 5.302; the weighting function also gives rarer pairs less influence.

Word2Vec and GloVe still give each known spelling one static row. That makes lookup cheap, but leaves two gaps: a new spelling has no row, and an ambiguous word has only one.

Build unknown spellings from pieces

Technical text constantly produces variants: tokenize, tokenized, tokenizer, plus typos mixed into logs. FastText represents a known word with character n-grams as well as its whole-word identity, so related spellings share parameters.[5]Reference 5Enriching Word Vectors with Subword Information.https://arxiv.org/abs/1607.04606

The paper uses n-gram lengths 3 through 6 with boundary markers. The lab uses length 3 so every window is visible.

For an out-of-vocabulary (OOV) spelling, there's no learned whole-word row. FastText can still sum n-gram vectors it has already learned.

FastText character 3-grams for tokenize and tokenized. Seven windows match exactly: left-to, tok, oke, ken, eni, niz, and ize. tokenize also has ze-right and a whole-word row. tokenized also has zed and ed-right and has no whole-word row.
Exact n=3 windows share seven bucket rows. The unseen spelling omits a whole-word row but can still sum the n-gram vectors it has in common with tokenize.
07-share-character-ngrams.py
1def character_ngrams(word, n=3): 2 marked = f"<{word}>" 3 return {marked[i:i + n] for i in range(len(marked) - n + 1)} 4 5known = character_ngrams("tokenize") 6new_form = character_ngrams("tokenized") 7shared = sorted(known & new_form) 8 9print("shared pieces:", shared) 10print("new spelling can reuse pieces:", len(shared) > 0) 11print("tokenize-only pieces:", sorted(known - new_form)) 12print("tokenized-only pieces:", sorted(new_form - known))
Output
1shared pieces: ['<to', 'eni', 'ize', 'ken', 'niz', 'oke', 'tok'] 2new spelling can reuse pieces: True 3tokenize-only pieces: ['ze>'] 4tokenized-only pieces: ['ed>', 'zed']

The output isolates the reusable pieces. A trained FastText model would sum learned vectors for them. For tokenized, it would skip a whole-word row that was never stored.

Subword composition helps with unfamiliar surface forms. It still can't decide which meaning an already-known ambiguous token carries.

One static row can't choose a sense

Read these two messages:

charge-senses.txt
1"Dispute the charge on invoice 8142." 2"Charge the scanner before the lab demo."

The spelling charge is the same. A static lookup returns the same vector, even though the first case belongs near billing language and the second near device language.

Before running the lab, predict whether either similarity score can notice that difference.

08-expose-static-sense-collision.py
1import math 2 3static = { 4 "charge": [0.8, 0.2], 5 "invoice": [0.2, 1.0], 6 "scanner": [1.0, 0.1], 7} 8 9def dot(a, b): 10 return sum(x * y for x, y in zip(a, b)) 11 12def cosine(a, b): 13 return dot(a, b) / math.sqrt(dot(a, a) * dot(b, b)) 14 15billing_charge = static["charge"] 16device_charge = static["charge"] 17 18print("same charge vector:", billing_charge == device_charge) 19print("billing similarity to invoice:", round(cosine(billing_charge, static["invoice"]), 3)) 20print("device similarity to invoice:", round(cosine(device_charge, static["invoice"]), 3))
Output
1same charge vector: True 2billing similarity to invoice: 0.428 3device similarity to invoice: 0.428

Both comparisons match because the two charge vectors are identical. Every downstream decision therefore starts from the same compromise. Sentence evidence has to enter after the lookup.

Mix in a neighbor from this sentence

The lookup stays fixed, so context enters in layers above it. A Transformer can learn a weighted mix of neighboring states.

To isolate that move, take the shared charge row and average it with one clue from its sentence: billing with invoice, device with scanner.

The two starts are identical. Predict where the final states should move before running the lab.

09-mix-context-into-charge.py
1import math 2 3table = { 4 "charge": [0.8, 0.2], 5 "invoice": [0.2, 1.0], 6 "scanner": [1.0, 0.1], 7} 8 9def mix(token_vector, clue_vector): 10 return [0.5 * a + 0.5 * b for a, b in zip(token_vector, clue_vector)] 11 12def dot(a, b): 13 return sum(x * y for x, y in zip(a, b)) 14 15def cosine(a, b): 16 return dot(a, b) / math.sqrt(dot(a, a) * dot(b, b)) 17 18billing_state = mix(table["charge"], table["invoice"]) 19device_state = mix(table["charge"], table["scanner"]) 20 21print("billing charge state:", [round(x, 2) for x in billing_state]) 22print("device charge state:", [round(x, 2) for x in device_state]) 23print("same final state:", billing_state == device_state) 24print("billing vs invoice:", round(cosine(billing_state, table["invoice"]), 3)) 25print("device vs invoice:", round(cosine(device_state, table["invoice"]), 3))
Output
1billing charge state: [0.5, 0.6] 2device charge state: [0.9, 0.15] 3same final state: False 4billing vs invoice: 0.879 5device vs invoice: 0.355

The equal weights are illustrative, not learned parameters. Billing charge moves toward invoice (cosine 0.879), while device charge stays farther away (0.355). A trained attention layer learns which neighbors should contribute, and later layers can refine the state again.

Vector plot of the toy contextualizer. Both charge uses start at E[0] equals 0.8, 0.2. Averaging with invoice at 0.2, 1.0 moves billing charge to 0.5, 0.6. Averaging with scanner at 1.0, 0.1 moves device charge to 0.9, 0.15.
The shared lookup is E[0] = [0.8, 0.2]. Equal mixing with invoice or scanner sends the two occurrences to different final states.

ELMo made this context-dependent representation explicit. A bidirectional language model produces token states, then a downstream task learns a weighted mix of its layers instead of using one context-free row.[6]Reference 6Deep contextualized word representations.https://arxiv.org/abs/1802.05365

At position k, with layer states hk,0h_{k,0}hk,0​ (character CNN) through hk,2h_{k,2}hk,2​ (the top bidirectional LSTM),

ELMok=γ∑j=02sjhk,j\text{ELMo}_k = \gamma \sum_{j=0}^{2} s_j h_{k,j}ELMok​=γj=0∑2​sj​hk,j​

The task-specific weights sjs_jsj​ are normalized so they sum to 1. γ\gammaγ scales the whole mix. Lower and upper layers can therefore contribute different kinds of information.

ELMo at charge in Dispute the charge on invoice. A forward language model reads left context toward charge. A backward language model reads right context toward charge. Character-CNN state h0 and bidirectional LSTM states h1 and h2 are mixed with normalized weights s0, s1, and s2, then scaled by gamma.
At charge, each recurrent layer concatenates forward and backward states. A downstream task learns weights over h0, h1, and h2, then scales their sum by gamma.

BERT changed how deep bidirectional context is trained. Its masked language-model objective hides selected tokens and predicts them from both sides, so encoder self-attention can read evidence before and after a position.[7]Reference 7BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding.https://arxiv.org/abs/1810.04805

The binary visibility mask says who may attend. Learned attention weights say how much each allowed token contributes. Those are separate objects.

Not every model may look both ways

A BERT-style encoder token can use tokens on both sides during encoding. A GPT-style causal language model predicts the next token from the prefix, so its state at a position can't use later tokens while preserving that objective.

The original GPT work used a Transformer decoder for generative pretraining.[8]Reference 8Improving Language Understanding by Generative Pre-Training.https://cdn.openai.com/research-covers/language-unsupervised/language_understanding_paper.pdf

At the first token in charge the scanner, the right-hand clue scanner is visible to an encoder and hidden from a causal decoder state at charge. Predict which list each mask should produce.

10-compare-visibility-masks.py
1tokens = ["charge", "the", "scanner"] 2encoder_visibility = [ 3 [1, 1, 1], 4 [1, 1, 1], 5 [1, 1, 1], 6] 7causal_visibility = [ 8 [1, 0, 0], 9 [1, 1, 0], 10 [1, 1, 1], 11] 12 13def visible_tokens(mask, position): 14 return [token for token, allowed in zip(tokens, mask[position]) if allowed] 15 16print("encoder state for charge sees:", visible_tokens(encoder_visibility, 0)) 17print("causal state for charge sees:", visible_tokens(causal_visibility, 0))
Output
1encoder state for charge sees: ['charge', 'the', 'scanner'] 2causal state for charge sees: ['charge']

Both designs produce contextual states. Their visibility rules match different training objectives: one can use the whole input, while the other must preserve prefix-only prediction.

Side-by-side visibility masks for charge the scanner. The encoder matrix is all ones, so the highlighted charge row sees charge, the, and scanner. The causal lower-triangular matrix has charge row 1, 0, 0, so the first state sees only charge.
For position 0, the encoder row is [1, 1, 1] and the causal row is [1, 0, 0]. Only the encoder can use the future clue scanner at that position.

Even when two states are allowed to differ, a similarity score can still lie to you.

A shared direction can fake similarity

Embedding applications often use cosine similarity: a value near 1 means two vectors point in similar directions. That score can mislead when the space shares a large, uninformative direction.

Work on contextual representations found that token vectors can be anisotropic: many vectors share a dominant direction, so raw cosine stays high even for tokens that shouldn't be treated as alike.[9]Reference 9How Contextual are Contextualized Word Representations? Comparing the Geometry of BERT, ELMo, and GPT-2 Embeddings.https://arxiv.org/abs/1909.00512

Two states can disagree in a task-relevant component while still pointing almost the same way overall.

Two vector plots. Raw billing vector 10, 1 and device vector 10, minus 1 both point along a shared horizontal direction with cosine 0.98. Subtracting the mean 10, 0 leaves 0, 1 and 0, minus 1 with cosine minus 1.0.
The shared mean is [10, 0]. Before subtraction the two vectors differ by about 11.4°. Afterward their centered components point 180° apart.

Predict what raw cosine will say before running the lab. The two states share a large horizontal component, so raw cosine treats them as almost identical. Subtracting their shared mean exposes vertical components that point in opposite directions.

11-diagnose-shared-direction.py
1import math 2 3billing = [10.0, 1.0] 4device = [10.0, -1.0] 5 6def dot(a, b): 7 return sum(x * y for x, y in zip(a, b)) 8 9def cosine(a, b): 10 return dot(a, b) / math.sqrt(dot(a, a) * dot(b, b)) 11 12raw_similarity = cosine(billing, device) 13mean_direction = [(a + b) / 2 for a, b in zip(billing, device)] 14centered_billing = [a - m for a, m in zip(billing, mean_direction)] 15centered_device = [a - m for a, m in zip(device, mean_direction)] 16centered_similarity = cosine(centered_billing, centered_device) 17 18print("raw cosine:", round(raw_similarity, 3)) 19print("centered cosine:", round(centered_similarity, 3))
Output
1raw cosine: 0.98 2centered cosine: -1.0

The output makes the warning concrete: raw cosine is 0.98, while centered cosine is -1.0. Centering isn't a universal production recipe. Use it as a diagnostic, then measure retrieval or classification quality on held-out examples instead of trusting a similarity score in isolation.

Match the representation to the failure

Choose a representation from the failure mode and the measurement that could prove an improvement. No representation wins by name alone.

NeedUseful starting pointWhat to measure
Small fixed vocabulary, simple classifierTrainable embedding lookupHeld-out classification quality and latency
Rare spelling variants such as tokenizedCharacter or subword-aware static vectorsRecall on unseen variants
Ambiguous words such as billing/device chargeContextual token statesAccuracy on sense-dependent cases
Search over whole documentation chunksSentence or chunk embedding modelRetrieval precision and recall on real queries

Contextual token states aren't automatically good message-level retrieval vectors. Pooling and task training change what a vector means, so evaluate those choices on the retrieval task.

Later retrieval lessons pick them up.

The same geometry feeds next-token prediction. A model scores candidate tokens from the current state, then turns those scores into probabilities. The next question is how well those probabilities fit text held out from training.

Mastery check

What strong answers show

  • Foundational: You can explain why an embedding lookup returns one stored row for every occurrence of a token ID.
  • Intermediate: You can build a co-occurrence or prediction example and show why the two meanings of charge require context mixing.
  • Advanced: You can choose a representation for a measured failure case and test whether cosine geometry supports the chosen metric.

Follow-up questions

Why can't a static embedding table represent billing charge and device charge differently in these two sentences?

Answer

The tokenizer emits the same token ID for both occurrences, and a static lookup retrieves one stored row per ID. Only a later context-dependent computation can use words such as invoice or scanner to separate the senses.

What does a FastText-style character n-gram representation fix for tokenized, and what does it not fix for charge?

Answer

Character n-grams let an unseen spelling such as tokenized reuse pieces learned from related forms such as tokenize. They don't decide whether an already-known spelling such as charge means a bill or a powered device in a particular sentence.

Why should a retrieval team test cosine similarity on held-out queries before trusting contextual vectors?

Answer

Contextual vectors can share dominant directions, so unrelated token states may receive high raw cosine scores. Held-out retrieval or classification examples reveal whether the geometry supports the product decision.

When embeddings mislead

  • Treating token IDs as meaning: An ID only selects a row. Training and context create useful geometry.
  • Calling subword handling disambiguation: Character pieces help unfamiliar spellings, not multiple senses of one familiar word.
  • Trusting cosine without an evaluation set: A high score can come from shared directions rather than task-relevant similarity.
Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.Both "Dispute the charge on invoice 8142" and "Charge the scanner before the lab demo" tokenize charge to ID 407. Before any context-mixing layer, what vector is used for charge in the two messages?

Correct answer: Both occurrences use the same row E[407]; words such as invoice or scanner can separate the senses only in a later context-mixing computation.

An embedding table lookup is indexed by token ID, not by the surrounding sentence. If both occurrences have ID 407, both start from the same stored row. Sentence evidence can affect the representation only after a contextual layer mixes in neighboring tokens.

2.A word-context count table has high counts for encoder and decoder with layer and mask, and high counts for cache and prefix with hit and reuse. After PPMI and SVD compression, which outcome matches the distributional evidence?

Correct answer: encoder should land closer to decoder than to cache because they share context patterns before compression.

Co-occurrence rows record which contexts appear near each word. PPMI emphasizes word-context pairs that occur more often than chance, and SVD compresses the high-dimensional table while preserving major usage patterns. Words with similar context rows therefore tend to have nearby static vectors.

3.Using Skip-gram with window size 2 on request hit cache layer today, training pairs are written as (center, context). Which positive pairs are generated for center hit?

Correct answer: (hit, request), (hit, cache), and (hit, layer)

Skip-gram uses the center token to predict visible neighbors, so the center appears first in each pair. With window size 2, hit sees request one token left, cache one token right, and layer two tokens right; today is three positions away.

4.In a GloVe-style objective, the target for an observed word-context pair is log(count). If a model score equals log(30), which residual comparison is correct?

Correct answer: The squared residual is 0 for count 30 and positive for count 3 because log(3) differs from log(30).

GloVe fits a model score to the logarithm of the observed co-occurrence count. A score equal to log(30) exactly matches a count of 30, but it misses a count of 3 because that target is log(3). Weighting affects how much the residual counts, not what target it tries to match.

5.An embedding system saw the known word tokenize during training and later receives the unseen spelling tokenized. It also sees the known word charge in both billing and device sentences. What does a FastText-style character n-gram representation address?

Correct answer: It can build a usable tokenized vector from learned character n-gram rows shared with related spellings, but it doesn't choose the sentence-specific sense of known charge.

FastText helps with unfamiliar surface forms by summing learned character n-gram vectors, so an unseen related spelling can reuse pieces from known forms. That is different from sense disambiguation: a known ambiguous word still needs sentence context to decide which meaning is active.

6.A toy contextualizer starts both occurrences of charge at [0.8, 0.2] and then computes 0.5 * token_vector + 0.5 * clue_vector. The billing sentence supplies clue invoice = [0.2, 1.0], and the device sentence supplies clue scanner = [1.0, 0.1]. What final states result?

Correct answer: Billing charge becomes [0.5, 0.6], and device charge becomes [0.9, 0.15].

The billing state is the average of [0.8, 0.2] and [0.2, 1.0], which is [0.5, 0.6]. The device state is the average of [0.8, 0.2] and [1.0, 0.1], which is [0.9, 0.15]. The same starting row can therefore become different occurrence states after context is mixed in.

7.In charge the scanner, consider the token state at position 0, charge. Under a BERT-style encoder mask and a GPT-style causal mask, which visibility pattern is correct?

Correct answer: The encoder state can use charge, the, and scanner; the causal state at charge can use only charge.

A BERT-style encoder uses a full visibility mask, so the charge state may incorporate tokens on both sides, including scanner. A GPT-style causal state preserves next-token prediction by using only the prefix available at that position, so position 0 can't use later tokens.

8.Two contextual states are billing = [10, 1] and device = [10, -1]. Their raw cosine is about 0.98, but after subtracting their shared mean direction the centered cosine is -1. What should a retrieval team conclude before relying on raw cosine?

Correct answer: Raw cosine can be inflated by a shared dominant direction, so the team should validate retrieval or classification quality on held-out queries instead of trusting the score alone.

The large shared horizontal component makes the vectors point in nearly the same raw direction even though the informative vertical components oppose each other. This is an anisotropy warning: use held-out task metrics to check whether the geometry supports retrieval decisions.

9.A retrieval team wants semantic search over complete documentation chunks, not token-sense labels. It can train and evaluate on real query-document pairs. Which starting representation and metric fit this objective?

Correct answer: Use a sentence or chunk embedding model, then measure retrieval precision and recall on held-out queries.

Chunk search needs vectors designed or trained to represent sentences or chunks, and success should be measured on the actual retrieval task. Individual contextual token states, token lookups, and character n-grams address narrower token-level failures and aren't automatically effective chunk-level retrieval vectors.

9 questions remaining.

Next Step
Continue to Perplexity & Model Evaluation

Contextual states are what a language model turns into next-token probabilities. Next you'll measure how well those probabilities fit held-out text, and when a good fit still isn't a product decision.

PreviousBPE, WordPiece, and SentencePiece
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Indexing by Latent Semantic Analysis

Deerwester, S., et al. · 1990 · JASIS

https://doi.org/10.1002/(SICI)1097-4571(199009)41:6%3C391::AID-ASI1%3E3.0.CO;2-9

Efficient Estimation of Word Representations in Vector Space.

Mikolov, T., Chen, K., Corrado, G., & Dean, J. · 2013 · arXiv preprint

https://arxiv.org/abs/1301.3781

Neural Word Embedding as Implicit Matrix Factorization.

Levy, O. & Goldberg, Y. · 2014

https://arxiv.org/abs/1405.4053

GloVe: Global Vectors for Word Representation.

Pennington, J., Socher, R., & Manning, C. D. · 2014

https://aclanthology.org/D14-1162.pdf

Enriching Word Vectors with Subword Information.

Bojanowski, P., et al. · 2017

https://arxiv.org/abs/1607.04606

Deep contextualized word representations.

Peters, M., et al. · 2018 · NAACL 2018

https://arxiv.org/abs/1802.05365

BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding.

Devlin, J., et al. · 2019 · NAACL 2019

https://arxiv.org/abs/1810.04805

Improving Language Understanding by Generative Pre-Training.

Radford, A., et al. · 2018

https://cdn.openai.com/research-covers/language-unsupervised/language_understanding_paper.pdf

How Contextual are Contextualized Word Representations? Comparing the Geometry of BERT, ELMo, and GPT-2 Embeddings.

Ethayarajh, K. · 2019

https://arxiv.org/abs/1909.00512

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.