← All Posts
ML · Transformers · Building Blocks · Part 8 of 8

Embedding Layer

From Text to Numbers

Neural networks operate on numbers. They multiply matrices, compute gradients, and adjust weights - all purely numerical operations. They have absolutely no concept of what a “word” is. So the very first question any language model must answer is: how do we turn text into numbers?

This is where the embedding layer enters the picture. It is the very first operation in a transformer - the bridge between the symbolic world of discrete tokens and the continuous numerical world where the transformer does all its reasoning. Every single input token passes through this layer before anything else happens.

The pipeline looks like this:

Raw text → Tokenizer → Token IDs (integers) → Embedding Layer → Dense vectors → Transformer layers

For example, the sentence “The cat sat” might be tokenized into three tokens - "The", "cat", "sat" - each assigned a unique integer ID by the tokenizer’s vocabulary. The embedding layer then converts each of those integer IDs into a dense, high-dimensional vector (typically 512 to 8192 dimensions). These vectors are what the transformer’s self-attention and feed-forward layers actually consume.

Without the embedding layer, the transformer has no input. It is, quite literally, where all the magic begins.

Key idea: The embedding layer converts discrete token IDs (integers like 464, 2368, 3290) into continuous dense vectors (arrays of 512–8192 floating-point numbers). This conversion is a simple table lookup, not a complex computation.

The Lookup Table Concept

Despite its importance, the embedding layer is one of the simplest components in a transformer. It is a lookup table - nothing more. Concretely, the embedding layer is a matrix E of shape (vocab_size, d_model):

E = matrix of shape (V, d) where: V = vocabulary size (e.g., 50,257 for GPT-2) d = embedding dimension (e.g., 768 for GPT-2)

Each row of this matrix is the embedding vector for one token in the vocabulary. Row 0 corresponds to token ID 0, row 1 to token ID 1, and so on. To get the embedding for a token with ID i, you simply take row i from the matrix:

embedding(token_id) = E[token_id] // That's it. Just row indexing.

This is a critical point that many explanations get wrong: the embedding lookup is NOT a matrix multiplication. It is an array index operation - equivalent to E[42] in any programming language. The cost is O(1) per token (or more precisely, O(d_model) to copy the row), not O(vocab_size).

Step-by-step: Looking Up “cat”

  1. The tokenizer converts the string "cat" into an integer token ID, say 2368.
  2. We go to the embedding matrix E (a 50,257 × 768 matrix of floating-point numbers).
  3. We take row 2368 of this matrix. That row is a vector of 768 numbers.
  4. This 768-dimensional vector is the embedding of "cat". Done.

For a sequence of n tokens, we perform n lookups and stack the resulting vectors into a matrix of shape (n, d_model). This is the input to the rest of the transformer.

Input tokens: [464, 2368, 3290] shape: (3,) Embedding: [E[464], E[2368], E[3290]] shape: (3, 768)

The values in this matrix are learned parameters. They start as random numbers and are updated during training through backpropagation, just like any other weight matrix in the network. Over millions of training steps, the model learns to place semantically similar tokens near each other in this embedding space.

One-Hot Encoding: The Bad Alternative

Before embeddings, the standard way to represent categorical data for neural networks was one-hot encoding. In one-hot encoding, each token is represented as a vector of length vocab_size, with a 1 at the position corresponding to the token’s ID and 0s everywhere else:

Vocabulary size = 50,257 "The" (ID 464): [0, 0, ..., 0, 1, 0, ..., 0] // 1 at position 464 "cat" (ID 2368): [0, 0, ..., 0, 1, 0, ..., 0] // 1 at position 2368 "sat" (ID 3290): [0, 0, ..., 0, 1, 0, ..., 0] // 1 at position 3290 Each vector has 50,257 dimensions with exactly one non-zero entry.

This is a terrible representation for language. Here’s why:

❌ One-Hot Encoding

  • Extremely sparse: a 50,257-dim vector with 99.998% zeros
  • All tokens equidistant: cosine similarity between any pair = 0
  • No semantics: “king” is as far from “queen” as from “banana”
  • Wastes memory: 50,257 floats per token = ~200 KB per token
  • Doesn’t scale: grows linearly with vocab size

✅ Dense Embeddings

  • Dense: a 768-dim vector, all values non-zero
  • Semantic distance: similar tokens have similar vectors
  • Rich semantics: “king” and “queen” are close; “banana” is far
  • Compact: 768 floats per token = ~3 KB per token
  • Fixed size: independent of vocab size

There is a useful mathematical insight here. If you multiply a one-hot vector by a weight matrix, the result is exactly one row of that matrix - the row at the position of the 1. So:

one_hot(2368) × W = W[2368]

This means a one-hot vector multiplied by a weight matrix is mathematically equivalent to a lookup. But the one-hot matmul is O(V × d) while the lookup is O(d). The embedding layer skips the useless multiplication by zeros entirely and goes straight to indexing the row. It is the sparse shortcut for what would otherwise be a very wasteful matrix multiplication.

Vocabulary and Tokenization Connection

The embedding layer’s vocabulary size is determined entirely by the tokenizer. The tokenizer is trained (or constructed) before the language model itself, and the vocabulary is fixed at training time. The embedding matrix must have exactly one row for every token in the vocabulary.

Different models use different tokenization strategies, which produce different vocabulary sizes:

Model Tokenizer Vocab Size d_model Emb. Params
GPT-2 BPE 50,257 768 38.6M
GPT-3 BPE 50,257 12,288 617.5M
BERT-base WordPiece 30,522 768 23.4M
BERT-large WordPiece 30,522 1,024 31.3M
LLaMA-7B SentencePiece (BPE) 32,000 4,096 131.1M
LLaMA-2-70B SentencePiece (BPE) 32,000 8,192 262.1M
T5-base SentencePiece (Unigram) 32,128 768 24.7M
Gemma-7B SentencePiece 256,000 3,072 786.4M

Note the massive variation - GPT-2 has 50,257 tokens while Gemma uses 256,000. Larger vocabularies can represent more words as single tokens (fewer subword splits), but the embedding matrix becomes correspondingly larger.

Special Tokens

Every vocabulary includes special tokens that have structural rather than linguistic meaning. These get their own rows in the embedding table, just like regular tokens:

Token Purpose Used In
[PAD] Padding for batch alignment (attention-masked out) BERT, T5, most encoder models
[CLS] Classification token; its final hidden state represents the whole sequence BERT
[SEP] Separator between two sentences in a pair BERT
[MASK] Masked token for masked language modeling BERT
<|endoftext|> End-of-sequence marker (also used as BOS) GPT-2, GPT-3
<s> / </s> Beginning / end of sequence LLaMA, T5
[UNK] Unknown token (fallback for out-of-vocabulary input) BERT, SentencePiece-based models

These special tokens start with randomly initialized embeddings and learn useful representations during training. For instance, BERT’s [CLS] token learns to aggregate sequence-level information, making it useful for classification tasks.

Embedding Dimension

The embedding dimension d_model is one of the most important architectural choices in a transformer. It determines how much information each token vector can carry, and it ripples through the entire model: every layer’s hidden size, every attention head’s key/value/query size, and the final output projection all depend on d_model.

d_model Model Class Example Total Params
256 Small / mobile DistilBERT-tiny, MobileBERT ~15–25M
512 Original Transformer Vaswani et al. 2017 ~65M
768 Base models BERT-base, GPT-2 (117M) ~110–125M
1,024 Large models BERT-large, GPT-2-medium ~340M
2,048 XL models GPT-2-XL ~1.5B
4,096 Large LLMs LLaMA-7B, Mistral-7B ~7B
5,120 Larger LLMs LLaMA-13B ~13B
8,192 Frontier LLMs LLaMA-65B, GPT-4 class ~65–175B+

The tradeoff is straightforward:

A rough rule of thumb: double the embedding dimension, and you roughly quadruple the model’s compute requirements (since compute scales as O(d²) in the attention and feed-forward layers). The embedding layer itself scales as O(V × d), which is linear in d.

Practical note: The embedding dimension must be divisible by the number of attention heads (so each head gets d_model / num_heads dimensions). For BERT-base: 768 / 12 = 64 dimensions per head. For LLaMA-7B: 4096 / 32 = 128 dimensions per head.

Interactive: Token-to-Vector Lookup

This animation shows exactly how the embedding layer works. We take the sentence “The cat sat”, tokenize it, and look up each token’s embedding vector from the embedding matrix. Step through to watch each token get resolved to a dense vector.

▶ Embedding Lookup Animation

Step through the token-to-vector lookup process. Each token ID selects one row from the embedding matrix.

The √dmodel Scaling

In the original “Attention Is All You Need” paper (Vaswani et al., 2017), there is a subtle but important step right after the embedding lookup: the embedding vectors are multiplied by √dmodel before adding positional encodings.

x = Embedding(token_ids) × √d_model + PositionalEncoding

Why? The reason comes down to scale matching:

The Scale Mismatch Problem

  1. Embedding initialization: Embeddings are typically initialized from N(0, 1/√dmodel). For dmodel = 512, the standard deviation is 1/√512 ≈ 0.044. So embedding values are tiny.
  2. Positional encoding values: Sinusoidal positional encodings use sin() and cos(), which have values in [-1, 1]. These are comparatively huge.
  3. The problem: If we add embeddings (values ~ 0.04) directly to positional encodings (values ~ 1.0), the position signal would completely dominate. The model would know where a token is but barely what it is.
  4. The fix: Multiply embeddings by √dmodel = √512 ≈ 22.6. Now embedding values are ~ 0.04 × 22.6 ≈ 1.0, matching the positional encoding scale.

The math works out cleanly: if each component of the embedding is drawn from N(0, 1/√dmodel), then after scaling by √dmodel, the L2 norm of the embedding vector is approximately √dmodel, giving it a magnitude comparable to the positional encoding.

Before scaling: ||emb|| ≈ √(d × (1/√d)²) = √(d × 1/d) = 1 After scaling: ||emb|| ≈ √d_model ≈ 22.6 (for d = 512) Positional enc: ||pos|| ≈ √(d/2) ≈ 16.0 (for d = 512)

Now both signals have comparable magnitude, so neither drowns out the other when added together. The model can learn to use both the identity (what token) and position (where in the sequence) information.

Note: Not all models use this scaling. GPT-2 and many modern LLMs skip the √dmodel scaling, relying instead on learned positional embeddings (which can adapt their scale during training) or different initialization schemes. The scaling is most commonly associated with the original transformer and models that use fixed sinusoidal positional encodings.

Weight Tying (Shared Embeddings)

One of the most elegant tricks in transformer design is weight tying (also called shared embeddings). The idea is simple but powerful: use the same weight matrix for both the input embedding and the output prediction layer.

The Setup

In a language model, two matrices have suspiciously similar shapes:

Input embedding: E ∈ ℝ^(V × d) maps token ID → vector Output projection: W ∈ ℝ^(d × V) maps vector → logits over vocab

Notice that W = ET would work perfectly - the output projection is just the transpose of the embedding matrix. Weight tying does exactly this: it sets the output projection (the “LM head”) to be the transpose of the input embedding matrix.

// Without weight tying: two separate matrices input_emb = E[token_id] // V × d parameters logits = hidden_state @ W_out // d × V parameters (separate) // With weight tying: one shared matrix input_emb = E[token_id] // V × d parameters logits = hidden_state @ E.T // reuses E (zero extra parameters)

Why It Works

Weight tying makes deep semantic sense. Consider what each matrix does:

This creates a beautiful symmetry: to predict "cat", the model needs to produce a hidden state that is close (in dot-product space) to the input embedding of "cat". The embedding space serves double duty as both the input representation and the output target space.

Benefits

Benefit Details
Fewer parameters Saves V × d parameters. For GPT-2: 50,257 × 768 = 38.6M saved
Regularization Constrains the model, reducing overfitting on smaller datasets
Better generalization The shared space means input and output representations are aligned
Faster convergence Embedding updates from both input and output gradients

Models That Use Weight Tying

Some models (like LLaMA) do not use weight tying. When the model is large enough, the parameter savings are relatively small (< 3% of total parameters), and having separate matrices can provide additional expressivity.

Embedding Space Geometry

Once a model is trained, the embedding matrix is no longer random noise. It has learned a rich geometric structure that captures linguistic relationships. Let’s explore what this space looks like.

Semantic Clustering

Similar words end up near each other in embedding space. If you project the high-dimensional embeddings into 2D (using t-SNE or UMAP), you’ll see clear clusters:

Within each cluster, finer-grained structure exists. “King” and “queen” are closer to each other than either is to “prince” or “princess,” reflecting their shared semantic role as rulers rather than offspring.

The Famous Analogy

The most celebrated property of embedding spaces is the ability to encode analogies as vector arithmetic:

king - man + woman ≈ queen paris - france + italy ≈ rome walking - walk + swim ≈ swimming

This works because the embedding space encodes relational differences as consistent vector offsets. The vector from “man” to “king” is approximately the same as the vector from “woman” to “queen” - both represent the concept of “royalty.”

Subword Structure

Since modern tokenizers use subword units (BPE, WordPiece), the embedding layer must also handle morphological structure. The token "un" learns an embedding that, when combined with "happy" through the transformer layers, produces the meaning of “unhappy.” Similarly, "##ing" (a WordPiece suffix) learns a representation that modifies the base verb into its progressive form.

Static vs. contextual: It is critical to understand that the embedding layer produces static vectors. The word “bank” gets the same embedding whether it means a financial institution or a river bank. The contextualization - distinguishing these meanings - happens in the transformer layers above (self-attention and feed-forward). The embedding is just the starting point.

Cosine Similarity

The standard way to measure similarity in embedding space is cosine similarity:

cos_sim(a, b) = (a · b) / (||a|| × ||b||)

Typical values for well-trained embeddings:

Initialization Strategies

How the embedding matrix is initialized before training has a measurable impact on convergence speed and final model quality. The values must be random (to break symmetry) but carefully scaled.

Random Normal Initialization

The most common strategy. Each element of the embedding matrix is drawn independently from a normal distribution:

E[i, j] ~ N(0, 1/√d_model)

The 1/√dmodel factor keeps the variance of the output manageable. For dmodel = 768, the standard deviation is 1/√768 ≈ 0.036. This means initial embedding values are very small numbers, typically between -0.1 and 0.1.

Truncated Normal

Like normal initialization, but values beyond 2σ are re-sampled. This prevents extreme outlier values in the initial embedding:

E[i, j] ~ TruncatedNormal(0, 1/√d_model, a=-2σ, b=2σ)

Used by BERT and many Google-origin models. The truncation prevents individual embedding dimensions from having extreme initial values that could cause gradient instabilities.

Xavier / Glorot Initialization

Occasionally used, drawing from a uniform distribution:

E[i, j] ~ Uniform(-√(6 / (V + d)), +√(6 / (V + d)))

Less common for embeddings specifically, but sometimes used in frameworks that apply Xavier initialization to all weight matrices uniformly.

Pretrained Initialization

For fine-tuning tasks, embeddings can be initialized from pretrained word vectors like Word2Vec or GloVe. This gives the model a head start with semantically meaningful embeddings. However, this approach has largely fallen out of favor since modern models are pretrained end-to-end on massive corpora.

Key point: Regardless of initialization, the embeddings are fully trainable. They are updated through backpropagation just like any other parameter in the model. The gradient flows from the loss through all the transformer layers, through the embedding lookup (which is differentiable since it is just selecting a row), and updates the row corresponding to each token that appeared in the batch.

Segment Embeddings and Token Type

Some transformer architectures add additional embeddings on top of the token and position embeddings. The most notable example is BERT’s segment embeddings (also called token type embeddings).

BERT’s Three-Embedding Sum

BERT processes pairs of sentences for tasks like natural language inference and question answering. It needs a way to distinguish which sentence each token belongs to. The solution is elegant: add a third embedding that encodes the sentence membership.

BERT input = Token Embedding + Position Embedding + Segment Embedding Token Embedding: E_token[token_id] shape: (d_model,) Position Embedding: E_pos[position] shape: (d_model,) Segment Embedding: E_seg[segment_id] shape: (d_model,) where segment_id ∈ {0, 1} 0 = belongs to sentence A 1 = belongs to sentence B

The segment embedding table is tiny - just 2 rows × d_model values (one for sentence A, one for sentence B). But it gives the model crucial information about sentence boundaries.

Example: BERT Input

Input: [CLS] The cat sat [SEP] It was tired [SEP]

Token [CLS]Thecatsat[SEP]Itwastired[SEP]
Position 012345678
Segment 000001111

GPT Models Don’t Need Segments

Decoder-only models like GPT process one continuous sequence at a time. There is no concept of “sentence A” vs “sentence B” - everything is just a stream of tokens. So GPT models only use token embeddings + positional embeddings (no segment embedding).

Other Embedding Types

Various models add other specialized embeddings:

All of these follow the same pattern: a small lookup table whose output is added element-wise to the main token + position embedding.

Parameter Count

The embedding layer’s parameter count is straightforward to compute:

Embedding parameters = vocab_size × d_model

But what’s surprising is how large this number can be relative to the total model:

Model Embedding Params Total Params Embedding %
BERT-base 23.4M 110M 21.3%
GPT-2 (117M) 38.6M (shared) 117M 33.0%
GPT-2-XL 103.7M (shared) 1,558M 6.7%
LLaMA-7B 131.1M 6,738M 1.9%
LLaMA-65B 262.1M 65,222M 0.4%
Gemma-7B 786.4M 8,537M 9.2%

Two clear patterns emerge:

Memory note: At FP32, GPT-2’s embedding matrix takes 38.6M × 4 bytes = 154 MB of memory. At FP16 (half precision), it’s 77 MB. For LLaMA-65B, the embedding is ~1 GB at FP16. This is often the single largest contiguous allocation in the model.

Implementation in Pseudo-Code

Here is a clean implementation of the embedding layer with all the features we’ve discussed:

// ─── Embedding Layer ───────────────────────────────────

class TokenEmbedding:
    // Parameters
    E : matrix of shape (vocab_size, d_model)   // the embedding table
    scale : float = sqrt(d_model)               // optional scaling factor

    function __init__(vocab_size, d_model, use_scaling=True):
        // Initialize from N(0, 1/sqrt(d_model))
        E = random_normal(0, 1/sqrt(d_model), shape=(vocab_size, d_model))
        scale = sqrt(d_model) if use_scaling else 1.0

    function forward(token_ids):
        // token_ids: integer tensor of shape (batch, seq_len)
        // Returns: float tensor of shape (batch, seq_len, d_model)

        embeddings = E[token_ids]    // lookup, NOT matmul
        embeddings = embeddings * scale

        return embeddings

And here’s how it fits into the full transformer input pipeline:

// ─── Full Transformer Input Pipeline ──────────────────

class TransformerInput:
    token_emb  : TokenEmbedding(vocab_size, d_model)
    pos_emb    : PositionalEncoding(max_seq_len, d_model)
    seg_emb    : SegmentEmbedding(2, d_model)   // BERT only
    dropout    : Dropout(rate=0.1)

    function forward(token_ids, segment_ids=None):
        // Step 1: Token embeddings (lookup + scale)
        x = token_emb.forward(token_ids)

        // Step 2: Add positional encoding
        x = x + pos_emb(positions)

        // Step 3: Add segment embedding (if BERT-style)
        if segment_ids is not None:
            x = x + seg_emb[segment_ids]

        // Step 4: Dropout for regularization
        x = dropout(x)

        return x   // shape: (batch, seq_len, d_model)

And for weight-tied output projection:

// ─── Weight-Tied Output Projection ────────────────────

class LMHead:
    shared_emb : reference to TokenEmbedding.E   // same matrix!

    function forward(hidden_states):
        // hidden_states: (batch, seq_len, d_model)
        // shared_emb.T:  (d_model, vocab_size)
        logits = hidden_states @ shared_emb.T
        return logits   // shape: (batch, seq_len, vocab_size)

PyTorch One-Liner

In practice, PyTorch’s nn.Embedding handles all of this:

// PyTorch implementation
embedding = nn.Embedding(num_embeddings=50257, embedding_dim=768)
x = embedding(token_ids)   // shape: (batch, seq_len, 768)

// Weight tying
lm_head = nn.Linear(768, 50257, bias=False)
lm_head.weight = embedding.weight   // shared!

Summary

The embedding layer is deceptively simple - it is just a matrix lookup - but it is the foundation on which the entire transformer is built. Here are the key takeaways:

Concept Details
What it is A matrix E of shape (vocab_size, d_model). Each row is one token's vector.
How it works Table lookup: E[token_id]. O(1) per token, NOT a matrix multiplication.
Why not one-hot One-hot is sparse, wasteful, and captures no semantics. Embeddings are dense and meaningful.
√d scaling Multiplies embeddings by √d_model to match positional encoding magnitude.
Weight tying Reuse ET as the output projection. Saves V×d parameters.
Geometry Similar tokens cluster together. Analogies encoded as vector offsets.
Initialization N(0, 1/√d_model). Fully trainable via backpropagation.
Segment embeddings BERT adds sentence membership embeddings (tiny table: 2 × d_model).
Parameter fraction 30–50% in small models, <2% in large models.

The embedding layer converts the discrete, symbolic world of language into the continuous, numerical world that neural networks can process. Every breakthrough in modern NLP - from BERT to GPT-4 - begins with this simple table lookup.