From Token IDs to Embedding Vectors
The tokenizer splits text with re.split at punctuation, the double dash, and whitespace, keeping matched punctuation through a capturing group.
Why a Full Text Is the Real Test
A tokenizer that works on one sentence has not yet been proven. The real test is whether it still behaves correctly on a much longer text. In this article we apply a basic tokenizer to an entire short story by Edith Wharton and check three things: how many tokens come out, whether the first tokens look right, and how those tokens are turned into integers.
Before you trace anything, take a guess at how many tokens the story produces. Most people count words and punctuation marks, and that is close to what the tokenizer does. The gap between a rough guess and the exact count is exactly the kind of detail a step-by-step trace can settle for you. Keep your guess in mind; the count appears later in the article.
The whole path has two stages that you should keep separate. Tokenization decides where the boundaries fall in the text. Token ID conversion decides how each resulting token is represented as a number. A problem in the first stage carries into the second, so verifying the token list before you trust the IDs built from it is a useful habit.
Tracing the Regular-Expression Split
The core of the tokenizer is a single call to re.split from Python's re module. You give it a pattern and a string, and it cuts the string wherever the pattern matches. The pattern in this tokenizer matches three kinds of boundary: a character class listing commas, periods, colons, semicolons, question marks, underscores, exclamation marks, parentheses, and quotation marks; the double dash; and any whitespace character.
The pattern uses a capturing group, which is what keeps matched punctuation in the output. Without the group, the punctuation marks that trigger a split would disappear from the result. With the group, each punctuation mark survives as its own item, so a word and the comma after it end up as separate tokens rather than one fused string.
The stages in the diagram follow the order in which the tokenizer works. First the text is cut at every boundary the pattern describes. Matched punctuation is retained through the capturing group. The raw result then goes through a second pass, which is where blank items are handled. Reading the pipeline in this order lets you check each stage separately instead of trusting the final list as a whole.
Counting Tokens and Removing Blanks
Because the split happens at every whitespace character, the raw result also contains empty strings and strings that hold only spaces. Those are not tokens. A second pass removes them, keeping only items that still contain something after stripping spaces. This is why the count you get is not simply the number of pieces the first split produced.
A quick visual check helps before you trust a long count. Print a slice of the first thirty tokens and look at the order of the items. Ask whether any word has been cut in half and whether any punctuation mark is stuck to a word. In the source's first-thirty check the output looked right: every word was separate, and the capital letter in HAD was kept exactly as it appeared in the source.
The tokenizer does not lowercase anything. Case differences therefore remain visible in the tokens, so HAD and had would be different strings. This matters later, because the token list is the input to the conversion into integers.
From Strings to Token IDs
A list of tokens is still a list of Python strings. The next step converts those tokens from strings into an integer representation, producing token IDs. The source describes this conversion as an intermediate step, sitting between the tokens and the embedding vectors that come after it. A token ID is an integer, and it is the value the embedding layer will use as a key.
A language model does not read text directly. Text is first split into tokens, and each token is represented by an integer token ID. The question this article answers is how those integer IDs become embedding vectors, which are the numeric representations a model can compute with.
The source works with a small, concrete case: four input tokens whose IDs are 2, 3, 5, and 1. The IDs are stored in a tensor, which is the data container PyTorch uses for numbers. torch.tensor() creates one from a Python list. The variable input_ids holds the integers that the embedding layer will look up.
torch.tensor()creates a tensor from a Python list of integersinput_idsholds the token IDs the embedding layer will look up
How the Embedding Layer Looks Up a Row
The embedding layer performs a lookup operation, not a calculation on the token ID. Integer token IDs enter the layer, and the layer uses each ID to select a row of its weight matrix. The selected rows leave the layer as embedding vectors. Nothing in this path performs arithmetic on the IDs; the layer's job is retrieval.
The diagram shows the lookup for the four token IDs from the opening example. Each ID points to exactly one row, and the row number equals the ID. The values inside those rows are not part of this section, because the source's matrix is not reproduced here. What matters for now is the mapping from ID to row.
The rows are counted from index zero, which is the convention Python uses. This means the row for ID 3 is the fourth row in the matrix, not the third. Keep this in mind whenever you read a row number off a matrix by eye, because counting from one is the most common source of an off-by-one mismatch.
Tracing a Single Lookup
The next step applies the embedding layer to a single token ID. The layer is named embedding_layer in this example, and calling it like a function is how you use it. The call passes a tensor containing only the ID 3. Before you run it, notice what you are asking for: one token ID goes in, and the layer must return a vector that belongs to that token.
token_idis a tensor containing only the ID 3- calling
embedding_layerlike a function performs the lookup - the printed result is the embedding vector for token ID 3
The returned result is displayed inside double square brackets. The outer pair of brackets means the value is a two-dimensional tensor with a single row. The three numbers inside that row are the components of the embedding vector for token ID 3. In this example each embedding vector has three components.
The display also includes a label named grad_fn, with the value EmbeddingBackward0. The source does not explain that label in this section, so treat it as metadata that PyTorch attaches to the result. The important part is the three numbers, because they are the values stored in the matching row of the weight matrix.
The source makes the point by comparison. The embedding vector returned for token ID 3 is identical to the fourth row of the embedding matrix from the previous discussion. Because Python starts with a zero index, token ID 3 corresponds to the row at index 3. The layer does not compute a new vector from the ID. It reads a stored row.
Common Mistakes with Token IDs and Rows
The following mistakes come up often when people first trace a tokenizer and an embedding layer. Each one is a point where the output can quietly diverge from what you expected.
Common mistake Treating the token ID and the embedding vector as two versions of the same thing
Why it failsThey are different objects with different roles. The token ID is a single integer that names a token. The embedding vector is a list of numbers the layer reads from a stored row.
Do this insteadKeep the ID as the lookup key and the vector as the retrieved value. Debug them separately.
Common mistake Counting rows from one instead of from zero
What it looks likeLooking for token ID 3 by counting to the third row
Why it failsPython starts with a zero index, so token ID 3 corresponds to the row at index 3, which is the fourth row.
Do this insteadUse the ID directly as the row index and count from zero.
Common mistake Expecting the layer to calculate a vector from the ID
Why it failsThe embedding layer performs a lookup, not a calculation on the token ID. It reads a stored row.
Do this insteadWhen a vector looks unexpected, compare it with the matching stored row rather than searching for a formula.
Common mistake Trusting the IDs without checking the token list
Why it failsA boundary error in tokenization carries into the token IDs built from it.
Do this insteadInspect the first thirty tokens for cut words and stuck punctuation before converting them to IDs.
Common mistake Forgetting that the tokenizer keeps case
Why it failsThe tokenizer does not lowercase anything, so differently cased versions of a word remain different tokens.
Do this insteadExpect capital letters to be preserved in the token list and account for them when comparing expected and actual output.
Practice: Locate the Divergence
A learner runs the tokenizer on a story and then looks up token ID 5 in the embedding layer. They expect the vector to match the fifth row counting from one. Describe, stage by stage, where the expectation diverges from what the tokenizer and layer actually do, and state which row the lookup actually reads.
Hints
- Start with the stage that produces the token list, then move to the conversion into IDs, then to the lookup.
- Remember that rows are counted from zero, so the row for ID 5 is not the same as the fifth row counted from one.
- The embedding layer reads a stored row; it does not compute a vector from the ID.
What do you think happens?
Before you look at the answer, predict which row the token ID 3 selects in the embedding weight matrix.
Reveal answer
The fourth row, at zero-based index 3.
Rows are counted from index zero, and the token ID is used directly as the row index. The vector returned for token ID 3 is identical to that stored row.
Key Takeaways
- The tokenizer uses
re.splitat punctuation, the double dash, and whitespace; a capturing group keeps matched punctuation, and a second pass removes blank and whitespace-only items before counting. - Token ID conversion turns each token into an integer. This step sits between tokenization and embedding vectors, so verify the token list before trusting the IDs.
- The embedding layer performs a lookup: each token ID selects one row of its weight matrix, counted from zero, and returns that stored row as the embedding vector.
- When a lookup looks wrong, compare the token ID, the zero-based row index, and the returned vector to find where the expectation diverges.