Building my own language model: Embedding layers (Part 3)

It has been a while since I had time to work on this project. Actually, the code was written a while back, but I just did not have time to write about it. Interestingly enough the initial code was co-created using GPT 4.1 and although it technically worked, the results were not great.

Therefore, while coming back to the blog, I decided to upgrade the code, using GPT 5.

Recap

I decided to build a very simple GPT model, not one that actually competes with any LLM or SLM out there, but one that helps me better understand and experience how to build a model.

  • As a tokenizer I use a simple word based tokenizer
  • For the tokenizer and training I use Wikitext2
  • For finetuning I will use the open assist dataset, to train it on actual conversations

The crucial parts

There are a few parts needed to make this all work.

  • A set of configuration parameters
  • A model
  • A dataset
  • A training function

Configuration

I will go into more details later, but these are the configurations used at the moment.

EMBED_DIM    = 128     # size of each token/position embedding vector (hidden dimension)
NUM_HEADS    = 4       # number of attention heads per transformer layer
NUM_LAYERS   = 4       # number of stacked transformer encoder layers
SEQ_LEN      = 128     # maximum sequence length (context window)

BATCH_SIZE   = 32      # number of sequences per training batch
EPOCHS       = 1       # how many passes over the dataset to train

LR           = 3e-4    # learning rate for optimizer (step size in weight updates)
WEIGHT_DECAY = 0.01    # L2 regularization strength to prevent overfitting
GRAD_CLIP    = 1.0     # max gradient norm (prevents exploding gradients)

DROPOUT      = 0.1     # dropout probability for regularization inside the network

Model

Let’s first look at the model architecture. I realized while writing the blog post that this part alone will require a few blog posts. Therefore, let’s explore the overall architecture and then dive deeper into the individual parts.

Token embeddings

Before our model can do anything clever, it has to turn our token IDs into something continuous a neural net can reason about. That’s the job of the token embedding. With a vocabulary of ~30,000 tokens and EMBED_DIM = 128, the layer is literally a table of size 30,000 × 128: one row per token, 128 numbers per row, think of “ID cards” for tokens.

# token "ID cards": one 128-D vector per vocab item
self.token_emb = nn.Embedding(vocab_size, 128)  # shape: [vocab_size, 128]

When we pass a token ID like 42, the model grabs row 42 from that table and hands you its 128-number ID card. It feels like a lookup. But during training, those numbers aren’t fixed at all, they start random and get nudged by gradient descent like any other layer. Over time, the model pushes similar words closer together (their vectors align) and pulls apart words that should behave differently. In other words, the “lookup table” is actually a learned map of meaning.

Position embeddings

Knowing who a token is isn’t enough, the model also needs to know where it is. Transformers don’t come with order built in, so we give each position its own ID card too. With SEQ_LEN = 128, that’s a second table sized 128 × 128, one row per position, each a 128-D vector.

# position "seat numbers": one 128-D vector per position in the window
self.pos_emb = nn.Embedding(128, 128)  # shape: [max_pos=128, 128]

Let’s play this through

1. Raw input = token IDs

The tokenizer maps words into integers (IDs in the vocab):

"the cat sat" → [42, 7, 913]

This is the raw input: just integers.

If SEQ_LEN = 128, this example sequence only uses the first 3 slots out of the available 128.

2. Token embeddings (identity)

We look these up in the token embedding table ([30,000 × 128]).

Each ID gets replaced with a 128-dimensional vector:

W_token[42]  → [128 numbers]  # "the"
W_token[7]   → [128 numbers]  # "cat"
W_token[913] → [128 numbers]  # "sat"

So now we have:

[3, 128]  → 3 tokens, each with 128 features

3. Position embeddings (order)

Next, we look up the position IDs: [0, 1, 2]. From the position embedding table ([128 × 128]):

pos_emb[0] → [128 numbers]  # slot 0
pos_emb[1] → [128 numbers]  # slot 1
pos_emb[2] → [128 numbers]  # slot 2

4. Combine them

We add the vectors elementwise:

"the @ pos 0" = W_token[42]  + pos_emb[0]
"cat @ pos 1" = W_token[7]   + pos_emb[1]
"sat @ pos 2" = W_token[913] + pos_emb[2]

In short

  • Raw input = integers (token IDs).
  • Token embedding = “who I am” (vector lookup).
  • Position embedding = “where I sit” (vector lookup).
  • Add them = identity + order, same size [seq_len, embed_dim].
  • Feed into Transformer blocks to learn patterns and make predictions.

A quick sense of scale

  • Token embedding: ~30,000 × 128 ≈ 3.8M parameters, the biggest chunk in our tiny GPT.
  • Position embedding: 128 × 128 = 16,384 parameters, tiny by comparison, but important for word order.

Conclusion

Personally, I found it interesting to learn how embeddings actually work, I did realize though I had many questions. I’m not sure if the blog posts does enough justice to the complexity. But with the following blog posts everything should come together and will (hopefully) make sense.

Published
Categorized as AI

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.