§2.1–2.4Embeddings, Tokens, and Token IDs

Part I pp. 7–12 · ~7 min read

  • embedding
  • word embedding
  • tokenization
  • vocabulary
  • token ID
  • special token

A model multiplies numbers, not letters. This section builds the first two links of the chain that turns a raw string into something a GPT can consume: text → tokens → token IDs. (The next sections add sub-word tokenization and, finally, the embedding vectors themselves.)

2.1 Understanding word embeddings

An embedding maps discrete things — here, words — to points in a continuous vector space, arranged so that related items land near each other. A word embedding is the per-word case (you can also embed sentences or whole documents). Embeddings can have anywhere from one to thousands of dimensions; below we squash them to 2-D just to look at them.

first dimension →second dimension →birdseagleduckgoosesquirrelGermanyBerlinEnglandLondonlonglongerlongest
In a good embedding space, bird words cluster, each country sits by its capital, and long/longer/longest lie along a line. Directions and distances carry meaning.

2.2 Tokenizing text

Tokenization is the first concrete step: splitting the raw string into tokens. A simple word/punctuation tokenizer is just a regular expression that splits on whitespace and keeps punctuation as its own tokens.

</> A regex word/punctuation tokenizer
import re

text = "Hello, world. This, is a test."
result = re.split(r'([,.:;?_!"()\']|--|\s)', text)   #A
result = [item.strip() for item in result if item.strip()]   #B
print(result)
# ['Hello', ',', 'world', '.', 'This', ',', 'is', 'a', 'test', '.']
  • #A Split on punctuation, em-dashes (--), and whitespace. Wrapping the pattern in ( ) keeps the delimiters as tokens instead of discarding them.
  • #B Drop pure-whitespace fragments and strip stray spaces, leaving clean word/punctuation tokens.

2.3 Converting tokens into token IDs

Tokens are still strings. To get numbers, collect every unique token into a vocabulary that maps each token to a unique integer — its token ID . The vocabulary is usually sorted, so IDs are assigned alphabetically.

Training text The quick brown fox ... Tokens Thequickbrown... Vocabulary brown0dog1fox2the7 Token IDs 701 ...
1

1 · Tokenize the training text

Break the complete training text into individual tokens with the regex tokenizer.

1 / 4

Building a vocabulary and encoding text to token IDs (book Figure, p. 10).

The book wraps this into a SimpleTokenizerV1 class with two methods: encode (text → IDs) and decode (IDs → text, via the inverse vocabulary).

</> SimpleTokenizerV1
class SimpleTokenizerV1:
  def __init__(self, vocab):
      self.str_to_int = vocab                              #A
      self.int_to_str = {i: s for s, i in vocab.items()}   #B

  def encode(self, text):                                  #C
      preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', text)
      preprocessed = [item.strip() for item in preprocessed if item.strip()]
      ids = [self.str_to_int[s] for s in preprocessed]
      return ids

  def decode(self, ids):                                   #D
      text = " ".join([self.int_to_str[i] for i in ids])
      text = re.sub(r'\s+([,.?!"()\'])', r'\1', text)      #E
      return text
  • #A str_to_int is the vocabulary: token → ID (used by encode).
  • #B int_to_str is the inverse vocabulary: ID → token (used by decode).
  • #C encode: tokenize the text the same way, then map each token to its ID.
  • #D decode: map each ID back to its token and join with spaces.
  • #E Tidy up: remove the space the join() inserted before punctuation.

Encoding a sentence gives a plain list of integers — for example "the quick brown" might become:

token IDs (1×3)
7
6
0
thequickbrown
One token ID per token. Hover a cell to see its index. These integers are what §2.7 will turn into embedding vectors.

2.4 Adding special context tokens

SimpleTokenizerV1 has a fatal flaw: feed it a word that isn’t in the vocabulary and it throws a KeyError. Real text also needs markers for structure — where one document ends and another begins. Both are solved with special tokens .

TokenPurpose
<|endoftext|>Marks the boundary between independent documents concatenated for training.+
<|unk|>Stands in for any token missing from the vocabulary.+
[BOS]Beginning of sequence — marks where a text starts.
[EOS]End of sequence — marks where a text ends (e.g. between concatenated examples).
[PAD]Padding — fills short sequences so every sequence in a batch has equal length.
Common special tokens. GPT-2's BPE tokenizer (next section) actually uses only <|endoftext|> — because byte-pair encoding can represent any word, it never needs <|unk|>.

SimpleTokenizerV2 adds <|unk|> and <|endoftext|> to the vocabulary and, in encode, replaces any out-of-vocabulary token with <|unk|>:

</> SimpleTokenizerV2 — handles unknowns
all_tokens = sorted(set(preprocessed))
all_tokens.extend(["<|endoftext|>", "<|unk|>"])                 #A
vocab = {token: integer for integer, token in enumerate(all_tokens)}

class SimpleTokenizerV2:
  def __init__(self, vocab):
      self.str_to_int = vocab
      self.int_to_str = {i: s for s, i in vocab.items()}

  def encode(self, text):
      preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', text)
      preprocessed = [item.strip() for item in preprocessed if item.strip()]
      preprocessed = [
          item if item in self.str_to_int else "<|unk|>"        #B
          for item in preprocessed
      ]
      ids = [self.str_to_int[s] for s in preprocessed]
      return ids

  def decode(self, ids):
      text = " ".join([self.int_to_str[i] for i in ids])
      text = re.sub(r'\s+([,.:;?!"()\'])', r'\1', text)
      return text
  • #A Append the two special tokens to the end of the vocabulary, giving them the two highest IDs.
  • #B The one substantive change vs. V1: any token not in the vocabulary is replaced by <|unk|> before ID lookup, so encode() never crashes.

Joining two texts with " <|endoftext|> ".join((text1, text2)) and encoding them shows both special tokens in action — unknown words map to the <|unk|> ID, and the <|endoftext|> ID marks the seam between the two texts.

Key idea

A tokenizer is just two lookup tables — str→int for encode, its inverse int→str for decode. Everything hard about real tokenizers (unknown words, document boundaries) is handled by adding a few reserved special tokens to that vocabulary. Next, §2.5 replaces this brittle word-level scheme with byte pair encoding, which never meets a word it can’t encode.

📝 Check yourself: text → token IDs

0 / 5
  1. 1.Why must text be turned into embeddings before a neural network can use it?

  2. 2.In the tokenize → vocabulary → token-ID pipeline, what exactly is a token ID?

  3. 3.SimpleTokenizerV1 crashes when it meets a word that isn't in its vocabulary. How does SimpleTokenizerV2 fix this?

  4. 4.What is the <|endoftext|> special token used for?

  5. 5.The tokenizer's decode(ids) method is described as using the 'inverse vocabulary'. What is that?