Skip to content

Chapter 2 · Main chapter

Text to Tensors

Before a language model can learn anything, its input has to become numbers — then vectors. Watch a paragraph turn into the exact tensors GPT trains on.

Adapted from the original notebook by Sebastian Raschka · the Python shown is the book's; the demos run a faithful JavaScript port in your browser

Act 1 · Words become IDs

It's the last he painted, you know

01 / 06

A model can't read. The first job of a tokenizer is to cut text into pieces and give each piece a number. Start with one sentence from the story.

From here we hand the job to GPT-2's real byte pair encoder, which never meets a word it can't split. The story becomes one stream of 5,145 token IDs — ready to be sliced into training examples.

Act 2 · The sliding window

the story · 5,145 token IDs

I40
·H367
AD2885
·always1464
·thought1807
·Jack3619
·G402
is271
burn10899
… +5,136 more

One story → one flat sequence of integers. A network trains on examples, though — so we have to carve them out.

01 / 06

Byte pair encoding turns the whole story into 5,145 token IDs — one long sequence. But a network trains on examples. Where do they come from?

Those windows are still just integers. The last step turns each ID into something a network can actually learn from — a vector.

Act 3 · IDs become vectors

ids
2
3
5
1
5 ≠ 1

an ID is an index, not a quantity

01 / 05

The data loader emits token IDs, but an ID is a bare index — token 5 isn't greater than token 1. The model needs a vector it can learn to shape.

The lab

Three sliders and the real token stream: build the exact inputs and targets tensors the data loader hands the model, and step through them batch by batch.

Data loader explorer

Slide the knobs and step through the batches the GPT data loader builds from the real token stream.

batch 1 / 160

inputs · 8 × 4

40
367
2885
1464
1807
3619
402
271
10899
2138
257
7026
15632
438
2016
257
922
5891
1576
438
568
340
373
645
1049
5975
284
502
284
3285
326
11

targets · shifted +1

367
2885
1464
1807
3619
402
271
10899
2138
257
7026
15632
438
2016
257
922
5891
1576
438
568
340
373
645
1049
5975
284
502
284
3285
326
11
287

1,286 windows from 5,145 tokens · 160 full batches · drop_last=true, shuffle=false

And the naive tokenizer from Act 1, loaded live. Type anything — watch which words it knows, and how everything unfamiliar collapses to one <|unk|>.

Word-tokenizer playground

The naive tokenizer, vocab 1,130. Unknown words turn amber and collapse to <|unk|>.

<|unk|>1131,5do355you1126like628tea975?10<|endoftext|>1130In55the988sunlit956terraces984of722the988<|unk|>1131.7
tokens: 16unknown: 2

decoded back
<|unk|>, do you like tea? <|endoftext|> In the sunlit terraces of the <|unk|>.

The real code

The demos above run TypeScript ports; here is the Python they mirror, from the original notebook. The sliding-window data loader is the heart of the chapter — the target is the input shifted by one:

ch02.ipynb
class GPTDatasetV1(Dataset):
    def __init__(self, txt, tokenizer, max_length, stride):
        self.input_ids, self.target_ids = [], []
        token_ids = tokenizer.encode(txt, allowed_special={"<|endoftext|>"})
 
        # Slide a window across the token stream, stepping by `stride`.
        for i in range(0, len(token_ids) - max_length, stride):
            input_chunk = token_ids[i : i + max_length]
            target_chunk = token_ids[i + 1 : i + max_length + 1]
            self.input_ids.append(torch.tensor(input_chunk))
            self.target_ids.append(torch.tensor(target_chunk))
 
    def __len__(self):
        return len(self.input_ids)
 
    def __getitem__(self, idx):
        return self.input_ids[idx], self.target_ids[idx]

Turning IDs into vectors is two embedding tables and a sum — token identity plus position:

ch02.ipynb
token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
pos_embedding_layer   = torch.nn.Embedding(context_length, output_dim)
 
token_embeddings = token_embedding_layer(inputs)              # (batch, length, dim)
pos_embeddings   = pos_embedding_layer(torch.arange(context_length))
input_embeddings = token_embeddings + pos_embeddings          # what the model reads
Show the naive word tokenizer and the data-loader factory
ch02.ipynb
import re
 
 
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()]
        # Any word not in the fixed vocabulary becomes <|unk|>.
        preprocessed = [
            item if item in self.str_to_int else "<|unk|>" for item in preprocessed
        ]
        return [self.str_to_int[s] for s in preprocessed]
 
    def decode(self, ids):
        text = " ".join([self.int_to_str[i] for i in ids])
        return re.sub(r'\s+([,.:;?!"()\'])', r'\1', text)  # tidy spaces before punctuation
 
 
def create_dataloader_v1(txt, batch_size=4, max_length=256,
                         stride=128, shuffle=True, drop_last=True, num_workers=0):
    tokenizer = tiktoken.get_encoding("gpt2")
    dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
    return DataLoader(
        dataset, batch_size=batch_size, shuffle=shuffle,
        drop_last=drop_last, num_workers=num_workers,
    )

Three things to remember

  1. Tokenizing is lookup, and a fixed vocabulary is brittle. The naive tokenizer must reserve <|unk|> for everything it never saw — which is why real models use byte pair encoding instead.
  2. Training data is manufactured, not given. A sliding window over one long token stream produces every (input, target) pair; the target is just the input shifted one step right.
  3. IDs carry identity; positions carry order. Token embeddings and positional embeddings are summed into the vectors the transformer actually reads.

Adapted from Build a Large Language Model (From Scratch) by Sebastian Raschka — original notebook (Apache 2.0). The token IDs and data-loader batches on this page are its real printed outputs; the token and positional embedding values are torch's seed-123 output — verified number-for-number against the Python even where the notebook itself doesn't print them (its positional example is 4×256).