The Whole Book on One Page¶
Every chapter, in order, in one scroll. This edition is here for the things a chapter page cannot do: searching the whole book with your browser's own find, printing it, or reading it straight through without clicking. Use the contents on the left to jump to any chapter or section.
It is a long page, carrying all 75 figures, so give it a moment on a slow connection. The ordinary way to read the book is chapter by chapter, starting at the preface, and there is also a PDF and an EPUB.
First draft: December 17, 2025. Last updated: September 2026.
Preface: Why This Book Exists¶
There are already a lot of resources out there for learning about AI and large language models. So why write another one?
Because most of them fall into one of three traps, or they push you straight into tutorial hell.
Tutorial hell is a state of learning paralysis where you endlessly watch coding tutorials and follow along with instructors, feeling productive, but never actually building anything on your own. You run the code, it works, you feel good, but close the laptop and ask yourself why any of it worked, and you draw a blank. You've watched the chef cook a hundred times but have never touched the stove yourself.
The three traps that lead there:
- Too shallow. Copy-and-paste the code, follow along, done. You produce an output, but you don't understand any of the decisions behind it. The moment something breaks or changes, you're stuck.
- Too deep. Research-paper density, prerequisite courses in linear algebra and calculus, written for people who already have a graduate-level background. Most readers bounce off in chapter two.
- Too demanding on hardware. Great content, but assumes you have a modern GPU, a cloud computing account, and several dedicated weekends free. That rules out most people.
This book is a different approach.
The goal is a balance between theory and code, enough explanation to genuinely understand what you are building and why each piece is there, paired with real, runnable code you can execute right now on the machine in front of you.
Every chapter follows the same structure: first the idea in plain language with an analogy, then the code that implements it, then a short summary. You are never asked to accept something on faith. If a line of code does something, the chapter explains why.
Right now means on a regular laptop. No GPU. No cloud. No special hardware. This entire book was written and tested on a ThinkPad T14 Gen 1 (Intel Core i7, 32 GB RAM, released 2020), a five-year-old business laptop with no dedicated GPU. The full training run completed in about 20-30 minutes. If it runs there, it will run on yours.
Setup is minimal: Python and PyTorch. That's it. No accounts to create, no clusters to configure.
Who Is This Book For?¶
Beginners who learn by building. You know some Python, you're curious about AI, and you want to actually understand how it works, not just use someone else's model. You want to go from zero to a working language model, line by line, without getting stuck in tutorial hell.
Instructors and professors. You want classroom-ready material: concepts clear enough to teach, code that runs on a student's laptop in a single class session, and a structure that maps cleanly to a lecture. This book is designed to be that.
IT/IS professionals. You work with AI tools every day but want to understand what's happening under the hood. You don't need a PhD, you need a clear explanation of the architecture, a working example, and code you can actually read and modify.
Parents teaching their kids. AI is everywhere. If you want to introduce your child to how it actually works, not just how to use it, this book gives you a concrete, hands-on project you can work through together. Build something real. Ask questions. Break it and fix it. That's how learning sticks.
A Note on AI Assistance¶
This book was written by Truong (Jack) Luu. AI tools helped with writing plans, code drafts, and editing. All code was reviewed, edited, and tested by the author on a local machine. Every example in this book runs exactly as shown.
Truong (Jack) Luu jackluu.io
Introduction: Five Phases, One Loop¶
Before you write a line of code, it helps to know where the thing you are about to build sits. Artificial intelligence is not one invention. It is five waves, each of which kept what worked and changed one idea. The model in this book belongs to the fourth wave, and it inherits almost everything from the second.
Figure I.1: Five waves of artificial intelligence. From the second wave onward, every one of them learns the same way.
Where this book sits¶
The first phase, roughly 1950 to 1985, had people write down every rule by hand. If this, then that, thousands of times over. ELIZA, released in 1966, imitated a therapist by spotting a keyword in your sentence and turning it back into a question. It knew nothing, and people confided in it anyway. That is the oldest lesson in the field: we read understanding into anything that answers in fluent sentences.
The second phase, roughly 1985 to 2011, gave up on writing the rule. Show the machine ten thousand emails already labeled spam or not spam, and let it find the pattern itself. This is where the arithmetic of this book begins: turn everything into numbers, guess, measure how wrong the guess was, and nudge the numbers. Nothing since has changed that.
The third phase, 2012 to 2016, changed the scale and nothing else. Stack more layers, use far more data, run it on graphics cards. The result that settled the argument was an image model in 2012 that won its competition by a margin nobody could dispute, using the same loop.
The fourth phase, 2017 to 2023, changed the goal. Earlier models sorted an input into a category. These produce the next piece of it. Two ideas made that work: give every word a position in a space of numbers so that similar words sit near each other, and let the model weigh how much every other word matters before it commits to the next one. That second idea arrived in 2017 and is called attention. The rest of this book is those two ideas, built small enough to read.
The fifth phase, 2023 onward, barely changed the model at all. What changed is what we let it do: give it a goal and a set of tools, and let it plan a step, use a tool, check the result, and plan the next one. The engine underneath is still a next-word predictor. It is the one you are about to build.
Table I.1: The five phases, and what actually changed at each one.
| Phase | Roughly | What changed | What the machine does |
|---|---|---|---|
| Rules | 1950 to 1985 | People write every rule | Matches patterns, follows conditions |
| Machine learning | 1985 to 2011 | Learn the rule from examples | Guess, measure the error, adjust |
| Deep learning | 2012 to 2016 | Far more layers and data | The same loop, much bigger |
| Generative AI | 2017 to 2023 | Produce the next piece, not a label | Embeddings and attention, same loop |
| Agentic AI | 2023 onward | Give the model tools and a goal | The same model, inside a system |
One loop, running for forty years¶
Strip away the vocabulary and every phase from the second onward runs the same three steps.
Figure I.2: The loop that every model in this book runs, millions of times.
The machine guesses. Something measures how far the guess landed from the right answer. Something else adjusts the numbers so the next guess lands closer. Then it does it again, millions of times. You will build all three parts: the guess in Chapter 10, the measurement in Chapter 11, and the adjustment in Chapter 13.
Read that loop again and notice what is missing. No step in it asks whether the answer is true. The model is rewarded for producing text that fits the pattern of its training data, and a convincing invention fits that pattern exactly as well as a fact does. This is why language models state false things in clean, confident prose. It is not a defect that better engineering will remove. It is what the loop optimizes for, and you will see it directly in Chapter 17 when your own model writes Shakespeare that no one ever wrote.
A layer is a stack of small regressions¶
If you have fitted a line through a scatter plot, you have already trained a model. You picked a slope and an intercept, measured how far each point missed the line, and chose the values that made the misses smallest. That is the whole idea: a guess, a ruler, an adjustment.
A neural network is that, repeated. Each unit inside a layer multiplies its inputs by its own weights, adds them up, and adds one more number to shift the result. A slope for every input and an intercept, which is a regression. A layer runs a few hundred of those at once, as a single multiplication of two tables of numbers, and a network stacks the layers. Nothing in this book is harder than that; there is only a great deal of it. Chapter 3 covers the tables of numbers, and Chapter 8 builds the layer.
The four parts you are about to build¶
Follow one guess from a chatbot and you pass through four pieces, in this order.
- The dictionary. Words are turned into lists of numbers, positioned so that words used alike sit near each other. This is the embedding, and it is Chapter 5.
- The structure. Layers of numbers, each doing its small job and handing the result to the next. Attention lives here, in Chapters 6 through 10.
- The ruler. A single number saying how far the guess landed from the right answer. This is the loss, and it is Chapter 11.
- The messenger. The error is carried back through every layer, telling each weight which way to move. This is backpropagation, and it runs the training in Chapter 13.
Why build it by hand¶
A calculator is a fine thing if you already know arithmetic. If you never learned it, you will not notice when the calculator gives you a wrong answer, because you have nothing to check it against.
That is the argument for this book. You can already get a language model to write code, draft a memo, or explain itself. What you cannot do, without having built one, is tell when it is wrong, and say why. By the end you will have written every part of a working model, trained it on your own laptop, watched its error fall, and read the text it produces. After that, the polished paragraph on your screen stops being magic and becomes a machine you understand well enough to doubt.
Further Reading¶
Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2017). ImageNet classification with deep convolutional neural networks. Communications of the ACM, 60(6), 84–90. https://doi.org/10.1145/3065386
Rosenblatt, F. (1958). The perceptron: A probabilistic model for information storage and organization in the brain. Psychological Review, 65(6), 386–408. https://doi.org/10.1037/h0042519
Rumelhart, D. E., Hinton, G. E., & Williams, R. J. (1986). Learning representations by back-propagating errors. Nature, 323, 533–536. https://doi.org/10.1038/323533a0
Weizenbaum, J. (1966). ELIZA: A computer program for the study of natural language communication between man and machine. Communications of the ACM, 9(1), 36–45. https://doi.org/10.1145/365153.365168
Module 1: Foundations¶
Figure M1.1: Foundations in the big picture.
In this first module, we set up our workspace and prepare the raw material for our language model. For business readers, imagine you are building an assistant to draft text in your company's house style, trained on the company's archive. Think of this module as data preparation: before any system can find patterns, the text must be translated into numbers it can process.
In this module you will:
- Set up Python and PyTorch on your computer
- Understand what a language model actually does
- Learn how tensors work as the foundation for our math
- Build a tokenizer to chop text into manageable pieces
- Create embeddings that give those pieces mathematical meaning
Chapters¶
- Chapter 1: Setting Up Your Environment
- Chapter 2: What Is an LLM?
- Chapter 3: Tensors and PyTorch
- Chapter 4: Tokenization
- Chapter 5: Embeddings
Chapter 1: Environment Setup¶
Figure 1.1: Before you build, you set up your tools.
Setting up your environment ensures you have all the tools needed to build and run the model (as shown in Figure 1.1). This creates a predictable foundation so the code works the same way for you as it does in this book.
In this chapter you will:
- Install Python and a terminal to run commands.
- Download the project code using Git.
- Set up an isolated workspace for your packages.
- Download the training dataset.
Words to Know
- Terminal: a text-based window where you type commands.
- Repository: a folder of code stored online.
- Virtual Environment: an isolated toolbox for a single project's packages.
- Package Manager: a tool that downloads and installs code libraries.
In Business
Standardizing development environments in tech teams prevents the classic "it works on my machine" problem. When a company builds a house-style AI assistant to draft emails, the development team standardizes their environment. By using virtual environments and fixed package versions, companies ensure that new hires can start working immediately on the AI assistant and code runs identically on laptops and production servers.
Step 1: Install Python¶
Python is the programming language this entire tutorial uses. We need version 3.10 or newer. As seen in Figure 1.2, you can download it from the official website.
Figure 1.2: Download the latest Python installer.
Windows:
- Open your web browser and go to python.org/downloads
- Click the button to download Python 3.13.x.
- Run the downloaded
.exeinstaller. - CRITICAL: On the first screen of the installer, check the box that says "Add Python to PATH" at the very bottom. If you miss this, Python will not be found when you type commands.
- Click "Install Now".
macOS:
- Open your web browser and go to python.org/downloads
- Click the button to download Python 3.13.x.
- Run the downloaded
.pkginstaller and follow the prompts.
Step 2: Open a Terminal¶
The terminal is where you will run your code.
Windows:
Press Win + R, type cmd, and press Enter. A black window appears with a blinking cursor.
macOS:
Press Cmd + Space to open Spotlight, type Terminal, and press Enter.
Verify your Python installation by running this command:
You should see Python 3.10 or newer printed to the screen. (On macOS, if python is not found, type python3 --version instead.)
Step 3: Install Git¶
Git downloads the project code from the internet. Figure 1.3 shows the download page.
Figure 1.3: Download Git to get the project files.
Windows:
- Go to git-scm.com/download/win
- Download the "64-bit Git for Windows Setup".
- Run the installer and click "Next" through every screen.
macOS:
Open your terminal and type git --version. If it is not installed, macOS will ask if you want to install the "Command Line Developer Tools". Click Install.
Verify Git is installed (open a new terminal window on Windows):
You should see a message confirming your Git version.
Step 4: Download the Project¶
Now download the project code with a single command.
Then move into the project folder:
Step 5: Create a Virtual Environment¶
Different projects need different versions of the same library. A virtual environment gives this project its own isolated box of packages. Figure 1.4 illustrates this isolation.
Figure 1.4: A virtual environment isolates your project's libraries.
Make sure your terminal is inside the build-llm-from-zero folder, then run:
Windows:
macOS:
You will see (venv) appear at the start of your terminal prompt. You must run the activate command every time you open a new terminal window.
Step 6: Install PyTorch and Dependencies¶
With the virtual environment active, install the required libraries using pip, Python's package manager.
First, install PyTorch (we use a CPU-only version):
Then install the other libraries:
Step 7: Download the Shakespeare Dataset¶
We will train our model to write in a specific style using the complete works of Shakespeare. (Dataset credit: the Tiny Shakespeare text comes from Andrej Karpathy's char-rnn project, https://github.com/karpathy/char-rnn)
$ python src/utils/download_data.py
Dataset statistics:
Total characters : 1,115,394
Unique characters: 65
First 200 characters:
Step 8: Verify Everything Works¶
Run the setup verification script to confirm all pieces are ready:
$ python src/ch00_setup_check.py
Chapter 1: Checking your environment...
[OK] Python: 3.13.13
[OK] PyTorch: 2.6.0+cpu
[OK] NumPy: 2.5.3
[OK] Requests: 2.34.2
[OK] Shakespeare dataset found: 1,115,394 characters
[OK] Quick tensor test passed
All checks passed! You are ready to start Chapter 1.
If you see this, you are fully ready to begin!
Watch Out
If a script fails with ModuleNotFoundError: No module named 'torch', your virtual environment is not active. Run the activate command again (venv\Scripts\activate on Windows, source venv/bin/activate on macOS).
Key Takeaways¶
- You installed Python (the language), Git (code downloader), PyTorch (math library), and a terminal.
- A virtual environment keeps this project's packages isolated.
- Always activate your virtual environment before working on this project.
- With your environment ready, you can now move on to the next step in our map.
Check Your Understanding¶
- What does a virtual environment do?
- How do you know if your virtual environment is active?
- What is the command to move into the project folder?
Further Reading¶
The library you are typing into. The design argument behind the tool this book uses: write the model as ordinary Python that runs line by line, so you can print a tensor or stop in a debugger, and still get the speed of compiled code underneath. It is the reason the code in this book can be read top to bottom and still trains a real model.
Paszke, A., Gross, S., Massa, F., Lerer, A., Bradbury, J., Chanan, G., Killeen, T., Lin, Z., Gimelshein, N., Antiga, L., Desmaison, A., Köpf, A., Yang, E., DeVito, Z., Raison, M., Tejani, A., Chilamkurthy, S., Steiner, B., Fang, L., ... Chintala, S. (2019). PyTorch: An imperative style, high-performance deep learning library (arXiv:1912.01703). arXiv. https://doi.org/10.48550/arXiv.1912.01703
Note. The Tiny Shakespeare text used throughout the book comes from Andrej Karpathy's char-rnn project: https://github.com/karpathy/char-rnn{ .note }
Chapter 2: What Is an LLM?¶
Figure 2.1: Before we zoom in, let's look at what the whole system actually does.
With our environment set up in the previous chapter, we are ready to understand the model itself (Figure 2.1). A Language Model (LM) seems like a magic box that understands what you type. But under the hood, it performs a very specific, simple task.
In this chapter you will:
- Learn the single rule that drives all language models.
- See how models generate long answers one step at a time.
- Understand how reading data replaces human teachers.
Words to Know
- Token: one small piece of text, here a single character.
- Language Model: a system that predicts the next token in a sequence.
- Autoregressive: using your past outputs as inputs for your next step.
- Parameters: the numbers inside the model that adjust during training.
Theory: The Next-Token Engine¶
Imagine you are texting a friend and you type: "I am so hungry, I could eat a". Before you finish, your phone suggests: horse. Or pizza.
Figure 2.2: The core task of a language model is guessing what comes next.
As Figure 2.2 shows, your phone is doing next-word prediction. It guesses what word is most likely to come next based on everything you typed so far. A Large Language Model (LLM) does exactly the same thing. Given a sequence of tokens, it predicts what comes next. That is the whole idea. Everything else in this book is just building a machine that can do this really well.
How It Talks Back¶
If the model only predicts the next token, how does it write an essay? It uses autoregressive generation.
- You type:
"What is the capital of France?" - The model predicts the next token:
"Paris". - That output is added to the input:
"What is the capital of France? Paris". - It predicts the next token:
" is". - And repeats until it decides to stop.
Figure 2.3: In autoregressive generation, each predicted output loops back to become the next input.
As illustrated in Figure 2.3, the model never thinks about the whole answer at once. It just keeps predicting the next piece, over and over, building the sentence step by step.
What is a Token?¶
A token is the smallest unit the model works with. It could be a whole word, a piece of a word, or just a single character.
In this book, we use character-level tokens. Every single letter, space, and punctuation mark is one token. We use characters because the vocabulary is tiny (only 65 characters in our Shakespeare data) and you can understand it instantly. Real models use more complex chunks (subwords), but the math is exactly the same.
The Power of Self-Supervised Learning¶
To make a model "Large", we give it millions of parameters, which are the numbers inside the model that act like dials. Training turns these dials until the model produces good output.
But how does it learn without a teacher grading its work? Through self-supervised learning.
With language, the text itself is the answer key. If your training text is "Hello world", the model gets these practice questions automatically:
- See
H, predicte. - See
He, predictl. - See
Hel, predictl.
Figure 2.4: The text itself provides thousands of built-in practice questions.
As Figure 2.4 demonstrates, because no human labels are needed, you can train a model on millions of pages of raw text. The model just reads the data and learns the patterns.
Try It: The Numbers Game¶
Is it really just predicting characters? Let's prove it by looking at real data. If we see the letters "the ", what is the most likely next character in Shakespeare?
We wrote a tiny script to scan our training data and count every character that follows "the ".
$ python src/examples/ch02_next_char.py
What follows 'the ' in Shakespeare?
's': 573 times
'w': 459 times
'c': 448 times
'p': 426 times
'm': 376 times
This is exactly what the model learns to do, but instead of counting by hand, it uses math to predict the probabilities.
In Business
If a company builds an AI assistant to draft emails in its own house style, the concept is the same. The model trains on the company's archive of past writing. By learning what character or word typically follows another in that specific archive, the assistant learns the company's unique voice and terminology automatically.
Watch Out
It is easy to think the model "understands" the text. It does not. It is simply a math engine calculating the most probable next token based on patterns in its training data.
Key Takeaways¶
- A language model is a system that predicts the next token.
- Autoregressive generation means the model uses its own outputs as inputs for the next step.
- Tokens are the basic pieces of text, like characters or words.
- Self-supervised learning uses the raw text itself as the answer key.
- Now that we know the whole system revolves around next-token prediction, let's zoom in on the first step in our map: the math and tools we use to build it.
Check Your Understanding¶
- What does a language model actually predict?
- Why is it called "autoregressive"?
- How does self-supervised learning differ from having a teacher grade the work?
Further Reading¶
One model, many jobs, no retraining. The question was whether a model trained only to predict the next word would pick up skills nobody trained it for. Trained on a large, varied sweep of web pages, it began answering questions, summarizing and translating with no task-specific training at all, simply because the prompt made the task clear. That settled the architecture question for text generation: a decoder that predicts the next token, which is the model in Chapter 10.
Memory, before attention. Models that read a sentence one word at a time kept forgetting the beginning by the time they reached the end, because the learning signal faded as it travelled back through the steps. The fix was a cell with gates that decide what to keep, what to drop, and what to pass on. This ran almost every serious language system for twenty years. The question it answers, what should I still remember from earlier in the text, is the same question attention answers, by a completely different route.
Hochreiter, S., & Schmidhuber, J. (1997). Long short-term memory. Neural Computation, 9(8), 1735–1780. https://doi.org/10.1162/neco.1997.9.8.1735
Radford, A., Wu, J., Child, R., Luan, D., Amodei, D., & Sutskever, I. (2019). Language models are unsupervised multitask learners [Technical report]. OpenAI. https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf
Chapter 3: Tensors and PyTorch¶
Figure 3.1: Tensors are the containers that carry data through every stage.
Now that we understand the language model's goal from the previous chapter, we are ready to start building (Figure 3.1). Before we turn text into numbers, we need a way to store and manipulate those numbers efficiently. Tensors are the specialized containers that do this job, and PyTorch is the engine that processes them. (Note: the exact numbers you see may differ slightly on your computer or PyTorch version).
In this chapter you will:
- Learn what a tensor is and how it stores data.
- See how tensor shapes represent different dimensions.
- Understand how matrix multiplication transforms data.
- Turn raw scores into probabilities using Softmax.
Words to Know
- PyTorch: a library for doing math on large arrays of numbers very quickly.
- Tensor: a multi-dimensional array of numbers.
- Shape: the dimensions of a tensor (like rows and columns).
- Matrix Multiplication: combining two tensors to transform data.
- Softmax: a function that turns any numbers into probabilities that sum to 1.
Theory: The Data Containers¶
PyTorch is a Python library for doing math on large arrays of numbers, really, really fast. Without PyTorch, training would take weeks instead of minutes because pure Python is too slow for millions of calculations.
Figure 3.2: Tensors can be 1D (a list), 2D (a table), or 3D (a stack of tables).
A tensor is just a multi-dimensional array of numbers (Figure 3.2). If you have ever used a spreadsheet, you already know 2D tensors. A 3D tensor is just a stack of spreadsheets.
In this book, we work mostly with 3D tensors. Their dimensions, or shape, represent:
- B = Batch size (how many sequences we process at once)
- T = Time (how many tokens in each sequence)
- C = Channel size (how many numbers we use to represent each token)
We write shapes like (B, T, C). For example, (32, 128, 128) means "32 sequences, each 128 tokens long, each token described by 128 numbers."
Matrix Multiplication as Data Transformation¶
Figure 3.3: Matrix multiplication transforms data from one shape to another.
Matrix multiplication (written as @ in Python) is how neural networks transform data, as shown in Figure 3.3. The rule is that the inner dimensions must match: a (3, 4) tensor can multiply a (4, 5) tensor, creating a new (3, 5) tensor.
Think of (3, 4) as "3 students each with 4 test scores" and (4, 5) as "4 test scores each mapped to 5 skill ratings". Multiplying them gives you "3 students each with 5 skill ratings".
Softmax: Turning Scores into Probabilities¶
As Figure 3.4 shows, Softmax is a mathematical function that takes any list of numbers (called logits) and turns them into probabilities. The probabilities are all positive and always sum to exactly 1.0. It does this using exponential functions, which make the highest numbers stand out even more.
Figure 3.4: Softmax forces numbers into a 0-to-1 range where they total exactly 1.0.
Code: Basic Operations¶
Let's look at the basic PyTorch operations we will use.
| src/ch02_tensors.py (excerpt) | |
|---|---|
Run the file to see how PyTorch handles these:
$ python src/ch02_tensors.py
--- 1. Creating tensors ---
1D tensor: tensor([1., 2., 3., 4.])
shape: torch.Size([4])
...
...
[ 0.2303, -1.1229, -0.1863]])
...
tensor([[ 0., 1., 2., 3.],
...
...
What just happened:
- Lines 2 and 5 created 1D and 2D tensors, printing their shapes.
- Line 7 looked at a 3D tensor with a shape of
(2, 5, 8), representing(Batch, Time, Channels). - Line 9 multiplied a 3D tensor by a 2D matrix. PyTorch automatically applied the multiplication across the batch dimension. This is called batched matrix multiplication, and it saves us from writing slow Python loops.
Shape Check
Table 3.1 shows the tensor shapes before and after matrix multiplication.
Table 3.1: Tensor shapes before and after matrix multiplication.
| Tensor | Shape | Meaning |
|---|---|---|
x |
(2, 5, 8) |
2 sequences, 5 tokens each, 8 numbers per token |
W |
(8, 4) |
Transformation weights: 8 inputs to 4 outputs |
y |
(2, 5, 4) |
2 sequences, 5 tokens each, now 4 numbers per token |
Try It
Open src/ch02_tensors.py, change logits = torch.tensor([1.0, 2.0, 3.0]) to [1.0, 2.0, 10.0], and run it. Notice how the highest number takes almost 100% of the probability after Softmax.
In Business
How data is represented matters. When building our house-style email assistant, the text data is transformed into multi-dimensional tensors. The math operations we just covered are exactly how the assistant processes massive datasets to find hidden patterns in your company's writing voice.
Watch Out
A shape mismatch is the most common error in PyTorch. If you try to multiply (3, 4) and (5, 6), PyTorch will crash because the inner dimensions (4 and 5) do not match. Always check your shapes!
Key Takeaways¶
- A tensor is a multi-dimensional array of numbers.
- Shape
(B, T, C)= batch × time × channels, the standard convention in this book. @is matrix multiplication. Inner dimensions must match.- Softmax turns any numbers into probabilities that sum to 1.
- PyTorch handles batches automatically using batched matrix multiplication, with no for-loops needed.
- With tensors ready to hold our data, we can move to the next stage in our map: turning text into tokens.
Check Your Understanding¶
- What does the shape
(32, 128, 128)represent in our(B, T, C)format? - Why do the inner dimensions need to match in matrix multiplication?
- What is the difference between logits and probabilities?
Further Reading¶
The library you are typing into. The design argument behind the tool this book uses: write the model as ordinary Python that runs line by line, so you can print a tensor or stop in a debugger, and still get the speed of compiled code underneath. It is the reason the code in this book can be read top to bottom and still trains a real model.
Paszke, A., Gross, S., Massa, F., Lerer, A., Bradbury, J., Chanan, G., Killeen, T., Lin, Z., Gimelshein, N., Antiga, L., Desmaison, A., Köpf, A., Yang, E., DeVito, Z., Raison, M., Tejani, A., Chilamkurthy, S., Steiner, B., Fang, L., ... Chintala, S. (2019). PyTorch: An imperative style, high-performance deep learning library (arXiv:1912.01703). arXiv. https://doi.org/10.48550/arXiv.1912.01703
Chapter 4: Tokenization¶
Figure 4.1: Tokenization in the big picture.
Now that we have tensors to hold our data from the previous chapter, we are ready to move to the next stage in our map (Figure 4.1). Before a language model can find patterns in text, we must translate that text into a format it can understand. Neural networks only do math, which means they only eat numbers. In this chapter, we build our tokenizer: a translator that chops text into small pieces and assigns a number to each one.
In this chapter you will:
- Understand why neural networks need numbers
- Build a character-level vocabulary from the Shakespeare dataset
- Write functions to encode text into integers and decode it back
- Prepare the data for training by splitting it into two sets
Words to Know
- Token: one small piece of text, here a single character.
- Tokenizer: a function that turns text into tokens and then into numbers.
Theory¶
Neural Networks Are Number Machines¶
Figure 4.2: A tokenizer turns each character into a number.
Neural networks cannot read. You cannot feed them a string like "Hello". They expect a grid of numbers to multiply and add. To feed text into a model, we must first convert it into numbers. This process, illustrated in Figure 4.2, is called tokenization.
We need a consistent rulebook. If the letter a is the number 0, it must always be 0. We call this rulebook our vocabulary.
The Simplest Tokenizer¶
There are many ways to tokenize text. You could map each full word to a number (word-level). You could group common letters like th together (subword-level).
For our model, we use the simplest approach: character-level tokenization.
Every unique character in our dataset gets its own unique integer.
If our vocabulary is just {a: 0, b: 1, c: 2}, then the word "cab" becomes [2, 0, 1].
The Shakespeare dataset contains exactly 65 unique characters. This includes uppercase letters, lowercase letters, spaces, and punctuation. If we give each one an ID from 0 to 64, we can translate any Shakespearean sentence into a list of integers.
In Business
When you build an assistant to draft text in your company's house style, tokenization choices matter. A character-level tokenizer is simple but makes sequences long. A word-level tokenizer makes sequences short but requires a massive vocabulary. Modern business assistants use Byte-Pair Encoding (BPE), a middle ground that groups common character sequences into single tokens. We use character-level here because it keeps the math clean while our model learns from its archive (our Shakespeare dataset).
The Training and Validation Split¶
Figure 4.3: Hiding part of the data lets us test if the model actually learned.
Once we encode all of Shakespeare into one massive list of numbers, we split it into two piles (Figure 4.3):
- Training set (90%): The data the model studies to learn patterns.
- Validation set (10%): The data we hide from the model, used to test it later.
Why hide data? We want to know if the model is picking up patterns that hold across the text, or just memorizing the passages it was shown. If it performs well on the training set but fails on the validation set, it is overfitting.
Watch Out
Our tokenizer only knows the characters it saw in the training data. If you feed it a digit (like 1 or 2), an emoji, or a Chinese character, Python will raise a KeyError because that character is not in our 65-character vocabulary.
Code¶
We write our tokenizer in Python, load the dataset, and encode it.
File:
src/ch03_tokenizer.pyRun it:python src/ch03_tokenizer.py
| src/ch03_tokenizer.py (excerpt) | |
|---|---|
Here is what this code does:
- Line 3 uses
set(text)to find every unique character in the entire text. - Line 3 uses
sorted()to put them in alphabetical order so the IDs stay consistent. - Lines 6 and 7 build two dictionaries: one to go from character to ID (
char_to_id), and one to go back (id_to_char).
Figure 4.4: How the tokenizer processes the dataset.
As Figure 4.4 shows, the script processes the dataset step by step. Next, we write the translation functions:
| src/ch03_tokenizer.py (excerpt) | |
|---|---|
Line 3 converts a string into a list of integer IDs, and line 7 converts them back into a string.
Let's run the script and see what our 65 characters look like, and test a quick round-trip.
$ python src/ch03_tokenizer.py
!$&',-.3:;?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
...
--- Testing encode / decode ---
Original : 'Hello, World!'
Encoded : [20, 43, 50, 50, 53, 6, 1, 35, 53, 56, 50, 42, 2]
Decoded : 'Hello, World!'
Round-trip matches: True
--- Encoding the full dataset ---
What just happened:
- The script loaded over a million characters from the dataset.
- It found exactly 65 unique characters. Notice the space and newline characters at the start!
- It translated the string "Hello, World!" into a list of integers.
- It translated those integers back to prove no data was lost.
Finally, we encode the entire dataset into a PyTorch tensor and split it.
Training tokens : 1,003,854
Validation tokens: 111,540
Tokenizer ready! Ready for Chapter 5.
Try It¶
See how different inputs are converted into numbers. We wrote a short script that imports our tokenizer and encodes a few business phrases.
Try It
Open the terminal and run the example script. Notice how every letter, space, and punctuation mark is assigned a specific number from our vocabulary.
Key Takeaways¶
- Neural networks process numbers, not text.
- Tokenization translates text into numbers using a fixed vocabulary.
- We use a character-level tokenizer with a vocabulary of 65 characters.
encode()turns strings into lists of IDs;decode()turns IDs back into strings.- We split our data into a training set (to learn) and a validation set (to test).
Check Your Understanding¶
- Why must we convert text to numbers before feeding it to a language model?
- In our character-level tokenizer, what happens if we try to encode a character that wasn't in the training data?
- What is the purpose of the validation set?
- How many unique characters are in our vocabulary?
Further Reading¶
Why real models do not use a character vocabulary. Chapter 4 gives every character its own token, which keeps the vocabulary tiny and the code short. Production models split text into word pieces instead: common words stay whole, rare ones break into parts, and nothing is ever unknown. This paper is the method, and it is still the basis of the tokenizers shipped with today's models.
Sennrich, R., Haddow, B., & Birch, A. (2015). Neural machine translation of rare words with subword units (arXiv:1508.07909). arXiv. https://doi.org/10.48550/arXiv.1508.07909
Chapter 5: Embeddings¶
Figure 5.1: We have numbers, now we give them meaning.
In the previous chapter, we turned text into tokens (Figure 5.1). Tokens are just arbitrary numbers. To predict what comes next, the model needs to understand how characters relate to each other. In this chapter you will:
- Map simple token IDs to large vectors of numbers.
- Learn how these vectors capture meaning as coordinates.
- Add position information so the model knows the order of characters.
Words to Know
- Vector: A list of numbers that acts as a coordinate in a high-dimensional space.
- Embedding: A lookup table that maps a token ID to its vector.
Theory¶
The Problem with IDs¶
Figure 5.2: Token IDs are just arbitrary labels, not math quantities.
After tokenization, each character is an integer: A=13, B=14, a=39. But as Figure 5.2 illustrates, feeding these raw integers to a neural network creates a problem. The network learns by multiplying numbers together. It would see Z (token 38) as almost three times as big as A (token 13). It might learn that Z is more important than A just because of this accident. But token IDs are arbitrary labels.
Think of it like an employee ID number. Employee 105 is not "five times more employee" than Employee 21. The number alone carries no meaning about their role. We need a way to turn these arbitrary labels into a format the "Numbers Machine" can use to find patterns.
Vectors as Coordinates of Meaning¶
Figure 5.3: An embedding table maps each ID to a vector of numbers.
The solution, shown in Figure 5.3, is a lookup table called an embedding. It maps each token ID to a vector of floating-point numbers. In our model, we use 128 numbers for each character.
Instead of seeing 39 for a, the model sees [0.55, 0.03, -0.89, ...]. These 128 numbers act like coordinates. Characters that appear in similar contexts will gradually be moved closer together in this 128-dimensional space during training. The network learns that capital and lowercase versions of a letter behave similarly, so their vectors become similar. This table of profiles is learned from scratch.
Position Embeddings¶
Figure 5.4: The final representation combines what the token is with where it sits.
There is one more problem: the model reads all characters at exactly the same time. Without help, it cannot tell the difference between "cat" and "act" because they use the same letters. Order matters.
We fix this with positional embeddings (Figure 5.4): a second lookup table indexed by the position of the character in the sequence (0, 1, 2, ...) rather than its token ID. Position 0 gets its own 128-number vector, position 1 gets a different vector, and so on.
We then add the token embedding and the positional embedding together. The network learns to use these combined 128 numbers to represent both "what character is this?" and "where does it appear?". This combined representation completes the "Embeddings" stage in our map (Figure 5.1). The text is now fully converted into rich math vectors, ready for the next stage.
In Business
When you build an assistant that writes in your company's house style, embeddings turn your archive into a landscape of meaning. Token embeddings capture the vocabulary, while the position embeddings give the model the order of the characters, which is what lets it pick up the shape of an invoice or a polite greeting.
Code¶
We use src/ch04_embeddings.py to create and combine these two tables.
| src/ch04_embeddings.py (excerpt) | |
|---|---|
Here is what happens when we run it:
$ python src/ch04_embeddings.py
--- 2. Looking up embeddings ---
Input token_ids shape : torch.Size([2, 5])
Token embeddings shape: torch.Size([2, 5, 128])
(B=2, T=5, C=128)
...
--- 3. Positional embeddings ---
Position indices: [0, 1, 2, 3, 4]
Position embeddings shape: torch.Size([5, 128])
--- 4. Combining token + position embeddings ---
Final x shape: torch.Size([2, 5, 128])
(B=2, T=5, C=128)
What just happened:
- Line 1 created a lookup table for the 65 characters in our vocabulary.
- Line 2 translated a batch of token IDs into their embedding vectors.
- Line 3 created a second lookup table for the sequence positions.
- Line 6 added the token and position vectors to create the final input
x.
Shape Check: Table 5.1 details the dimensions of our data structures.
Table 5.1: Tensor shapes before and after the embedding layer.
| Variable | Shape | Meaning |
|---|---|---|
token_ids |
[B, T] |
Batch size by Time (sequence length). |
token_embeddings |
[B, T, C] |
C is the embedding dimension (128). |
position_embeddings |
[T, C] |
One vector per position. |
x |
[B, T, C] |
The combined input for the model. |
Try It¶
Try It
Change the batch size B or the sequence length T in src/ch04_embeddings.py. Run the script again. Notice that the output shapes scale automatically, but the parameter counts stay exactly the same. The lookup tables do not care how much text you process at once.
Watch Out
A common mistake is trying to look up a token ID that is larger than the vocabulary size. If your vocab_size is 65, the valid IDs are 0 to 64. Passing an ID of 65 will crash the program with an "index out of bounds" error.
Key Takeaways¶
- Token IDs are arbitrary labels and cannot be used directly for math.
- Embeddings are learned lookup tables that map token IDs to vectors.
- These vectors act as coordinates that capture relationships and meaning.
- Positional embeddings are added to tell the model the order of the characters.
Check Your Understanding¶
- Why do neural networks struggle with raw token IDs?
- How many numbers make up a single character's embedding vector in our model?
- Why do we need positional embeddings in addition to token embeddings?
- Does the size of the embedding table depend on the batch size?
Further Reading¶
Meaning becomes geometry. Give every word its own ID number and the model learns nothing from the numbering: "king" sits as far from "queen" as it does from "toaster". This paper trained a deliberately cheap prediction task so that words used in similar company ended up with similar vectors, and did it fast enough to run on billions of words. Embeddings, the subject of Chapter 5, start here, and so does the vector search behind modern recommendation and retrieval.
Mikolov, T., Chen, K., Corrado, G., & Dean, J. (2013). Efficient estimation of word representations in vector space (arXiv:1301.3781). arXiv. https://doi.org/10.48550/arXiv.1301.3781
Module 2: Building the Model¶
Figure M2.1: Building the Model in the big picture.
Now that we have our data ready, we build the engine. In business terms, this is constructing the logic pipeline that processes information for your house-style writing assistant. We will build the transformer architecture piece by piece, focusing on how attention allows the model to understand context.
In this module you will:
- Build self-attention so words can look at each other
- Expand to multi-head attention for multiple perspectives
- Add feed-forward layers to process what the attention found
- Combine these into a complete transformer block
- Stack the blocks into the full GPT architecture
- Enforce causal language modeling so the model cannot cheat
Chapters¶
- Chapter 6: Self-Attention (Single Head)
- Chapter 7: Multi-Head Attention
- Chapter 8: Feed-Forward and Norms
- Chapter 9: The Transformer Block
- Chapter 10: The Full GPT Architecture
- Chapter 11: Causal Language Modeling
Chapter 6: Self-Attention (Single Head)¶
Figure 6.1: Attention builds context by looking across the sequence.
Now that our tokens have meaning through embeddings (as shown in Chapter 5), reading one word at a time is not enough. To understand "it" in "the trophy didn't fit in the suitcase because it was too big", you have to connect "it" back to "trophy". In this chapter you will:
- Learn how tokens "look" at each other to build context.
- Use the Query, Key, Value (QKV) system to find relevant information.
- See how attention is just a weighted average of numbers.
- Apply a causal mask to hide the future.
Words to Know
- Self-Attention: A mechanism where tokens evaluate every other token in the sequence to gather context.
- Query (Q): A vector representing what a token is looking for.
- Key (K): A vector representing what a token contains.
- Value (V): A vector holding the actual information to be shared.
- Causal Mask: A filter that prevents tokens from seeing future tokens.
Theory¶
The Problem: Context Matters¶
If a model only considers one token at a time, it has no memory. It cannot connect subjects to verbs, or adjectives to nouns. Every token needs to "look at" the other tokens and decide which ones matter most to its own meaning.
The QKV System¶
Self-attention solves this using three vectors for every token:
- Query (Q): What am I looking for?
- Key (K): What do I offer?
- Value (V): What information do I actually contain?
Imagine you are at a networking event. You are looking for a marketing expert (your Query). Someone is wearing a badge that says "Marketing Director" (their Key). When your Query matches their Key, you start a conversation and absorb their advice (their Value).
In our model, every token creates its own Q, K, and V vectors by multiplying its embedding by learned weights. A token acts as a seeker (Query) and a source (Key and Value) at the exact same time.
Attention is a Weighted Average¶
Figure 6.2: Token 5 blends information from previous tokens.
Here is the "aha" moment: attention is nothing more than a weighted average (Figure 6.2). When token 5 calculates its final value, it does not just pick the single best token to look at. It mixes them all.
First, we multiply all Queries by all Keys (q @ k.transpose) to get an attention score for every pair. We scale these down by dividing by sqrt(head_size) (the size of each attention head, which is 32 here) so the numbers do not get too large. Then we apply softmax to turn these scores into percentages (weights) that sum to 1.0 (or 100%).
For example, when token 5 looks at the sequence, it might assign weights like this (as plotted in Figure 6.3). These values are rounded for display, so they add up to about 100% (100.1%):
- Token 0: 19.9%
- Token 1: 20.4%
- Token 2: 12.2%
- Token 3: 12.1%
- Token 4: 21.4%
- Token 5: 14.1%
Figure 6.3: A plotted bar chart of the attention weights.
Token 5's final output is simply 19.9% of the value of token 0, plus 20.4% of the value of token 1, and so on for all six tokens. The "Numbers Machine" builds context by blending the numbers of the most relevant past tokens. This context gathering completes the "Attention" stage of our map (Figure 6.1).
The Causal Mask¶
Figure 6.4: A lower triangular mask ensures tokens only see the past.
There is a catch: the model processes all tokens at once. If we let token 5 look at token 6, it would be cheating. During training, it would just copy the answer from the future instead of learning to predict it.
We apply a causal mask (a triangle of negative infinities, shown in Figure 6.4) to the scores before the softmax step. Softmax turns -infinity into exactly 0.0. This ensures that token 5 pays 0% attention to tokens 6, 7, 8, and 9. It can only see itself and the past.
In Business
When parsing a customer support chat, a simple keyword search treats every word independently. A context-aware system uses self-attention. It connects the word "refund" in message 4 back to the "broken screen" mentioned in message 1, understanding the full conversation history before it generates a reply in the company's house style.
Code¶
Figure 6.5: The Q, K, V vectors are created, scored, masked, and then multiplied.
We use src/ch05_self_attention.py to build the SingleHeadAttention class (flow illustrated in Figure 6.5).
Here is what happens when we run it:
$ python src/ch05_self_attention.py
--- Attention weights (what token 5 attends to) ---
Token 5 attends to tokens 0..5 (future tokens masked):
token 0: 0.199 #####
token 1: 0.204 ######
token 2: 0.122 ###
token 3: 0.121 ###
token 4: 0.214 ######
token 5: 0.141 ####
token 6: 0.000
token 7: 0.000
token 8: 0.000
token 9: 0.000
Self-attention done! Ready for Chapter 7.
What just happened:
- Lines 1 to 3 created independent Query, Key, and Value vectors.
- Line 7 computed the attention scores between all tokens.
- Line 10 masked the future tokens (notice tokens 6 to 9 have exactly 0.000 weight).
- Line 17 created the output (for example, token 5 created its output by taking a weighted average of tokens 0 through 5).
Shape Check: Table 6.1 lists the shapes of the attention variables.
Table 6.1: Tensor shapes during the self-attention calculation.
| Variable | Shape | Meaning |
|---|---|---|
q, k, v |
[B, T, head_size] |
Queries, Keys, and Values. head_size is 32. |
scores |
[B, T, T] |
Attention scores between every pair of tokens. |
weights |
[B, T, T] |
The softmax probabilities (summing to 1 per row). |
out |
[B, T, head_size] |
The final context-aware output vectors. |
Try It¶
Try It
Remove the scale division in src/ch05_self_attention.py by changing it to scale = 1.0. Run it again. Notice how the weights become much more extreme (some very close to 1.0, others 0.0). The scaling is critical to keep the model flexible and learning smoothly.
Watch Out
Be careful with the transpose step. We only want to swap the last two dimensions (Time and head_size) to compute the dot product properly. If you use .T, it might flip the Batch dimension too, crashing your shape calculations. Always use .transpose(-2, -1).
Key Takeaways¶
- Self-attention builds context by letting tokens "look" at other tokens.
- The QKV system works like a search engine: Queries match with Keys to retrieve Values.
- Attention is just a weighted average of the Values.
- A causal mask sets future scores to negative infinity, preventing the model from cheating.
Check Your Understanding¶
- What is the difference between a Query and a Key?
- Why do we divide the attention scores by the square root of the head size?
- What happens to a score of negative infinity when passed through softmax?
- Why is the causal mask necessary for a language model?
Further Reading¶
The first attention. Translation models of the day read the whole source sentence, squeezed it into a single fixed vector, and wrote the translation from that. Long sentences did not survive the squeeze. The fix: let the model look back over every input word and decide, at each output word, which ones matter right now. That weighted look-back is attention. The Transformer three years later kept this idea and threw out everything around it.
The architecture this book builds. Reading a sequence one step at a time is slow, because step 500 cannot start until step 499 has finished, and distant words stay hard to connect. This paper removed the step-by-step reading entirely and kept only attention, plus a note of each token's position. Every token can then be processed at once, which is what made training on very large amounts of text practical. The model you build in Chapters 6 to 10 is this design, made small.
Bahdanau, D., Cho, K., & Bengio, Y. (2014). Neural machine translation by jointly learning to align and translate (arXiv:1409.0473). arXiv. https://doi.org/10.48550/arXiv.1409.0473
Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, L., & Polosukhin, I. (2017). Attention is all you need (arXiv:1706.03762). arXiv. https://doi.org/10.48550/arXiv.1706.03762
Chapter 7: Multi-Head Attention¶
Figure 7.1: We run multiple attention heads at the same time to gather richer context.
In Chapter 6, we built one attention head. It acts as a single "reader" scanning the text. But one reader is rarely enough to catch everything. In this chapter you will:
- Run multiple attention heads in parallel.
- Understand how different heads learn to track different patterns.
- Concatenate their outputs into a single rich representation.
Words to Know
- Multi-Head Attention: Running several attention operations simultaneously.
- Concatenation: Joining multiple vectors end-to-end to form a longer vector.
- Projection: A linear layer that blends the concatenated outputs.
Theory¶
One Reader Isn't Enough¶
Figure 7.2: Every head sees the same input through its own filters, and their answers are joined rather than merged.
When you read a sentence, you track multiple things at once:
- Who is the subject?
- What action is happening?
- What is the tone, serious or sarcastic?
A single attention head can only "look" for one kind of pattern at a time. Multi-head attention fixes this by running several heads in parallel, as shown in Figure 7.2. Think of it as a team of readers, each highlighting different details, who then combine all their notes.
How It Works¶
Multi-head attention simply repeats the mechanism from Chapter 6:
- Run 4 independent
SingleHeadAttentionmodules on the exact same input. - Each head produces an output of 32 numbers (
head_size). - Glue (concatenate) all 4 outputs together:
4 heads × 32 numbers = 128 numbers. - Apply a final linear projection to blend them.
Because each head starts with different random weights (its own Q, K, and V filters), no two heads ever see the text the same way, and training pushes them further apart.
Why Four Heads and Not One Big One¶
Four readers sound expensive. They are not, and this is the part that surprises people.
A head's filters are sized by its head_size, not by the full 128 channels. Split 128 channels across four heads and each head gets filters that are a quarter as wide. Multiply it out and the totals match exactly:
| src/examples/ch07_head_budget.py | |
|---|---|
$ python src/examples/ch07_head_budget.py
4 heads of 32: 49,152 numbers
1 head of 128 : 49,152 numbers
Same budget: True
Four points of view cost the same as one. That is why every model in this family uses many heads: the split is free, so there is no reason to take one perspective when you can have four.
Do the Heads Really Differ?¶
That is the claim. Here is the check, on the model you will train in Chapter 13.
The script below loads the trained weights, feeds in a real line of Shakespeare, and prints how much attention the last character pays to each earlier character, one row per head.
| src/examples/ch07_heads_differ.py (excerpt) | |
|---|---|
$ python src/examples/ch07_heads_differ.py
Prompt: "JULIET: O Romeo"
Attention paid by the last character, one row per head:
J U L I E T : _ O _ R o m e o
head 0: 0.02 0.04 0.01 0.06 0.03 0.04 0.04 0.02 0.06 0.04 0.05 0.04 0.11 0.38 0.07
head 1: 0.02 0.02 0.03 0.03 0.04 0.08 0.12 0.06 0.05 0.07 0.02 0.10 0.16 0.14 0.06
head 2: 0.04 0.07 0.04 0.06 0.05 0.11 0.04 0.06 0.09 0.07 0.07 0.06 0.09 0.09 0.06
head 3: 0.03 0.02 0.01 0.05 0.05 0.03 0.03 0.05 0.04 0.04 0.05 0.08 0.12 0.25 0.14
Sharpest focus per head: 0->'e' (0.38) 1->'m' (0.16) 2->'T' (0.11) 3->'e' (0.25)
Read the rows, not the labels. Head 0 commits: it puts 0.38 of its attention on the single character just before the end and largely ignores the rest. Head 3 does something similar but softer, splitting between e and the final o. Head 1 spreads itself over the colon, the m and the e, holding several places at once. Head 2 is nearly flat: its largest weight is 0.11 and its smallest is 0.04, which is close to paying equal attention to everything.
So the heads do differ, and not in the tidy way the textbook story suggests. One is sharp, one is soft, one is diffuse, and one is barely committing at all. That last one is worth sitting with: in a trained model, some heads do very little. Nobody assigned these roles, and nobody can promise that head 1 is the "grammar head". They are four different filters that started from four different random draws and were shaped by the same pressure to predict the next character.
Watch Out
It is tempting to read a story into each head: this one tracks subjects, that one tracks punctuation. Sometimes a head really is that clean, and researchers have found interpretable ones in large models. Often it is not, as head 2 shows. Look at the numbers before you tell the story.
The Output Projection¶
After concatenating the 4 heads, we pass the 128 numbers through one final linear layer (the proj layer).
Why? The heads might have found redundant or conflicting information. The projection layer learns to blend the insights: "if head 1 and head 3 agree on this pattern, emphasize it; if head 2 is unsure, ignore it." It mixes the 4 separate perspectives into a single unified context vector of 128 numbers. This completes the "Attention" stage of our map (Figure 7.1).
There is a simpler thing we could have done here, and it is worth seeing why we did not. We could average the four heads instead of gluing them end to end. Averaging would give us 32 numbers rather than 128, which sounds tidy, but it throws away exactly what we paid for. Head 0's sharp focus on one character and head 2's flat spread would cancel each other into a lukewarm middle. Concatenation keeps every head's answer intact and lets the projection layer decide what each one is worth. Averaging decides in advance that they are all worth the same.
In Business
Figure 7.3: Multi-head attention gathers different perspectives.
When building an assistant to draft emails in your company's house style, you don't just look for one pattern. You track tone, structure, and vocabulary (Figure 7.3). Multi-head attention works exactly the same way: it asks 4 different "departments" to evaluate the text, then compiles a final executive summary.
Code¶
Figure 7.4: The heads run in parallel, get concatenated, and projected.
We use src/ch06_multihead_attention.py to build the MultiHeadAttention class, as flow-charted in Figure 7.4.
| src/ch06_multihead_attention.py (excerpt) | |
|---|---|
Here is what happens when we run it:
$ python src/ch06_multihead_attention.py
--- Comparing one head vs multi-head ---
Single head output shape: torch.Size([2, 10, 32]) (head_size=32)
Multi-head output shape: torch.Size([2, 10, 128]) (C=128)
Multi-head output has 4x more channels - it sees 4 perspectives at once.
Multi-head attention done! Ready for Chapter 8.
What just happened:
- Line 2 ran 4 independent attention heads at the same time.
- Line 2's result is that each head produced an output with 32 channels.
- Line 5 concatenated them, resulting in 128 channels (the original embedding dimension).
- Line 8 applied projection and dropout so the final output matches the exact shape of the input.
Shape Check: Table 7.1 outlines the shape transformations through the heads.
Table 7.1: Tensor shapes during the multi-head attention step.
| Variable | Shape | Meaning |
|---|---|---|
head_outputs |
4 × [B, T, 32] |
A list of outputs from the 4 heads. |
out (after cat) |
[B, T, 128] |
The 4 outputs joined end-to-end. |
out (final) |
[B, T, 128] |
The blended representation, ready for the next step. |
Try It¶
Try It
Change the number of heads in src/utils/config.py. Set n_heads = 8 instead of 4. Run the script again. Notice how the head_size automatically drops to 16, so the final concatenated size is still 128 (8 × 16 = 128). The model can have more perspectives, but each one has less detail.
Watch Out
For multi-head attention to work cleanly, your embedding dimension (n_embd) must be perfectly divisible by your number of heads (n_heads). If you try n_embd = 128 and n_heads = 5, the program will crash because it cannot divide the channels equally.
Key Takeaways¶
- Multi-head attention runs several single-head attention modules in parallel.
- Splitting the channels across heads costs nothing: four heads of 32 hold the same 49,152 numbers as one head of 128.
- The heads do end up different, but not in a tidy way. In our trained model one head is sharp, one is soft and one is close to flat.
- The outputs are concatenated, not averaged, so that no head's answer is diluted by the others before the projection can weigh it.
- A final linear projection blends the separate perspectives.
- The output shape
[B, T, C]is exactly the same as the input shape, making it easy to stack layers.
Check Your Understanding¶
- Why is one attention head not enough to understand complex text?
- If
n_embd = 256andn_heads = 8, what is thehead_size? - Four heads of 32 hold the same number of weights as one head of 128. Why does splitting cost nothing?
- Why do we concatenate the heads rather than average them?
- In the run above, head 2's attention was almost flat. What does that tell you about the claim that each head learns its own linguistic role?
- What is the purpose of the final projection layer?
Further Reading¶
The architecture this book builds. Reading a sequence one step at a time is slow, because step 500 cannot start until step 499 has finished, and distant words stay hard to connect. This paper removed the step-by-step reading entirely and kept only attention, plus a note of each token's position. Every token can then be processed at once, which is what made training on very large amounts of text practical. The model you build in Chapters 6 to 10 is this design, made small.
Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, L., & Polosukhin, I. (2017). Attention is all you need (arXiv:1706.03762). arXiv. https://doi.org/10.48550/arXiv.1706.03762
Chapter 8: Feed-Forward and Norms¶
Figure 8.1: We just built attention; now we process that context.
While attention (from Chapter 7) lets a token gather information from its neighbors, gathering information is only half the job. The token still needs to process that information and decide what to do with it. In this chapter you will:
- Build a feed-forward network to process context.
- Use a GELU curve to help the model learn smoothly.
- Add Layer Normalization to keep the math stable.
Words to Know
- Feed-Forward: A small neural network that processes each token independently.
- GELU: A smooth curve that replaces negative numbers with near-zero values.
- LayerNorm: A step that resets numbers to a safe size so training stays stable.
Theory¶
Thinking on Your Own¶
Attention is like listening to your team explain their ideas. Feed-forward is going back to your desk and thinking it over yourself.
Figure 8.2: The network expands to think, then compresses back to an answer.
The feed-forward layer is a small neural network applied to each token by itself (Figure 8.2). In our model, a token is a list of 128 numbers. The network first expands this to 512 numbers. This 4x expansion gives the model a wide workspace to spread out its calculations. After processing, it compresses the answer back down to 128 numbers.
The Smooth GELU Curve¶
Inside the feed-forward network, we apply a mathematical curve called GELU.
Figure 8.3: GELU replaces a hard switch with a smooth dimmer.
A neural network needs a non-linear curve to learn complex patterns. An older choice is ReLU, which acts like a hard switch: negative numbers become exactly zero, while positive numbers stay the same.
GELU is a gradual dimmer switch, as seen in Figure 8.3. Strongly negative numbers become nearly zero. Numbers near zero are gently reduced. Positive numbers pass through almost unchanged.
During training, we use calculus to figure out how to adjust the model's weights. A hard corner like ReLU can produce sudden jumps that destabilize the math. GELU's smooth curve makes training much more stable. Most modern models use it.
Keeping Numbers Safe with LayerNorm¶
As numbers flow through many layers of a neural network, they can grow very large or shrink very small. Like compound interest, small multiplications add up quickly. Extremely large or small numbers break the training math.
Layer Normalization (LayerNorm) fixes this. It re-centers and re-scales the numbers for each token. After LayerNorm, the values average to 0 and have a standard spread of 1.
Figure 8.4: LayerNorm centers messy numbers back around zero.
Think of it like resetting a runner's stopwatch after every lap (Figure 8.4). It does not change who is winning, but it keeps the numbers on the screen small and easy to read. In modern models, we apply LayerNorm before each major step. This gives the attention and feed-forward layers well-behaved numbers to work with.
Returning to the big picture map in Figure 8.1, the feed-forward network processes the context gathered by attention, and LayerNorm ensures the math remains stable before we pass the numbers to the next stage.
Code¶
| src/ch07_feedforward.py (excerpt) | |
|---|---|
Run the script to see the feed-forward layer and LayerNorm in action.
$ python src/ch07_feedforward.py
--- GELU activation ---
GELU is like ReLU (zeros out negatives) but with a smooth curve.
Input : [-3.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0, 3.0]
GELU : [-0.004, -0.159, -0.154, 0.0, 0.346, 0.841, 1.954, 2.996]
ReLU : [0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 2.0, 3.0]
--- Layer Normalization ---
LayerNorm re-centers and re-scales values at each position.
Before LayerNorm: mean=5.23, std=9.13
After LayerNorm: mean=0.0000, std=1.0039
What just happened:
- Lines 7 and 9 build a feed-forward network that expands from 128 to 512, then back to 128.
- Line 8 applies GELU, which gently reduces negative numbers instead of snapping them to zero.
- We passed messy numbers through LayerNorm and saw them neatly centered at zero with a standard spread of 1.
Shape Check:
- Input to FeedForward:
[Batch, Time, 128] - Output of FeedForward:
[Batch, Time, 128]
Try It¶
Try It
Open src/ch07_feedforward.py and change the LayerNorm input to have a massive spread: x_single = torch.randn(config.n_embd) * 1000 + 500. Run the script again. You will see that LayerNorm still perfectly tames it back to a mean of 0 and a standard spread of 1.
In Business
Imagine you are building an assistant to draft company emails in your house style. If the system is unstable, it might output gibberish. LayerNorm acts like a manager double-checking work between steps, ensuring the data never drifts too far off track before passing it to the next team.
Key Takeaways¶
- The feed-forward layer processes each token independently to build meaning.
- It expands the token's data 4x to give itself room to think.
- GELU provides a smooth mathematical curve to make training stable.
- LayerNorm resets numbers to a safe size so deep networks do not break.
Check Your Understanding¶
- Why does the feed-forward network expand its input size by 4?
- What happens to a strongly negative number when it passes through GELU?
- What is the average value of a token's numbers immediately after LayerNorm?
Further Reading¶
The line that keeps training stable. As numbers pass through a deep stack they drift, some growing, some shrinking, until learning stalls. Layer normalization rescales each token's vector back to a standard spread before the next step, using only that token's own numbers, so it works the same whatever the batch size. It is one line in Chapter 8 and one of the reasons deep Transformers train at all.
The smooth switch inside the feed-forward layer. A network needs a non-linear step, or every layer collapses into one. The common choice cut everything negative to zero, a hard switch. GELU fades instead of cutting, which gives the optimizer a gentler surface to work on and is the activation used in GPT-style models, including the one in Chapter 8.
Ba, J. L., Kiros, J. R., & Hinton, G. E. (2016). Layer normalization (arXiv:1607.06450). arXiv. https://doi.org/10.48550/arXiv.1607.06450
Hendrycks, D., & Gimpel, K. (2016). Gaussian error linear units (GELUs) (arXiv:1606.08415). arXiv. https://doi.org/10.48550/arXiv.1606.08415
Chapter 9: The Transformer Block¶
Figure 9.1: We combine our pieces into a reusable building block.
After processing information with feed-forward and normalizing the math in Chapter 8, we now have all the ingredients: multi-head attention (where tokens talk to each other), feed-forward layers (where tokens think for themselves), and LayerNorm (to keep training stable). In this chapter you will:
- Combine these parts into a single Lego brick called a Transformer block.
- Add residual connections so deep networks can learn effectively.
- Stack multiple blocks to build depth.
Words to Know
- Transformer Block: A reusable block of code containing attention, feed-forward, and normalization.
- Residual Connection: A shortcut that lets information skip a step, keeping the original signal intact.
Theory¶
Putting the Pieces Together¶
A single Transformer block combines our tools into a powerful unit. The forward pass of one block is just two simple lines of code:
x = x + self.attn(self.ln1(x)) # "listen, then add to what I know"
x = x + self.ff(self.ln2(x)) # "think, then update what I know"
Notice the x = x + ... pattern. This is the secret to making deep neural networks work.
Trick 1: The Residual Connection¶
Instead of completely replacing a token's data with the output of the attention layer, we add the new information to the original data. This is called a residual connection (or skip connection), as seen in Figure 9.2.
Figure 9.2: Residual connections provide a direct highway through the block.
Why does this matter? Imagine you are in a game of telephone. Each person translates the message and passes it on. After 10 rounds, the original message is usually unrecognizable. Residual connections are like passing the original written message alongside the game of telephone. Even if the spoken message gets mangled, the original is still there.
During training, feedback signals travel backward through the layers to adjust weights. Without residual connections, this feedback must pass through every layer in sequence. If each layer distorts the signal slightly, the signal either grows too large or shrinks to nearly nothing. The direct +x highway allows feedback to travel cleanly through dozens of stacked layers.
Trick 2: Pre-Layer Norm¶
Notice that we apply LayerNorm before each major step: attention(LayerNorm(x)).
This is the modern "pre-norm" convention used in GPT models (Figure 9.3). We normalize the data first, then do the hard work. It ensures that attention and feed-forward always receive well-behaved numbers, stabilizing training in the early stages.
Figure 9.3: Normalize the data before the hard work.
Stacking Blocks¶
One Transformer block is powerful, but not enough. We stack multiple blocks in sequence (Figure 9.4). Our model uses 4 blocks.
Each block refines the token representations further. Think of it like reading a complex document multiple times. First pass: "who are the characters?" Second pass: "what are their motivations?" Third pass: "what are the themes?" Earlier blocks capture simple patterns, while later blocks capture deeper meaning. This is why deeper models perform better.
Figure 9.4: Stacking blocks allows the model to find complex patterns.
Returning to the big picture map in Figure 9.1, these stacked Transformer blocks form the core engine of our model, ready to process the embeddings into deep, contextualized representations.
Code¶
Run the script to see a block process data and to check the parameter count.
$ python src/ch08_transformer_block.py
One TransformerBlock parameters: 197,888
MultiHeadAttention : 65,664
FeedForward : 131,712
LayerNorms (x2) : 512
Input shape: torch.Size([2, 10, 128])
Output shape: torch.Size([2, 10, 128]) (same as input)
--- Residual connection demonstration ---
The input is never lost -- it always flows through.
Original token norm : 11.470
Attention output norm: 4.599
After residual norm : 12.903 (combined)
--- Stacking multiple blocks ---
Stacking 4 blocks:
Params per block: 197,888
Total params : 791,552 (4 x 197,888)
Input shape: torch.Size([2, 10, 128])
Output shape: torch.Size([2, 10, 128]) (unchanged after 4 blocks)
What just happened:
- Lines 4 through 8 build a block containing attention, feed-forward, and LayerNorm.
- Line 12 shows the residual connection combine the original token data with the attention output.
- We stacked 4 blocks and saw the data flow through smoothly.
Shape Check:
- Input to TransformerBlock:
[Batch, Time, 128] - Output of TransformerBlock:
[Batch, Time, 128]
The block is shape-preserving. The input and output have identical shapes, which is exactly what allows us to stack them endlessly like Lego bricks.
Try It¶
Try It
Open src/ch08_transformer_block.py and change the number of layers in the stack from config.n_layers to 12. Run the script again. Notice how the total parameter count grows, but the output shape remains exactly the same.
In Business
When building software systems (like a house-style writing assistant), you want modular, scalable processes. A Transformer block is the ultimate modular unit. If your assistant is not smart enough, you do not have to invent a new architecture; you just stack more blocks and train it longer.
Key Takeaways¶
- A Transformer block combines LayerNorm, attention, and feed-forward layers.
- Residual connections (
x + ...) create a direct highway for data, allowing deep models to learn effectively. - Pre-norm applies LayerNorm before the hard work, stabilizing the math.
- Blocks preserve the shape of the data, allowing us to stack them modularly.
Check Your Understanding¶
- Why do we add the attention output to the original input (
x + attention)? - What does "pre-norm" mean?
- Why does the Transformer block output exactly the same shape it took in?
Further Reading¶
The shortcut that makes depth possible. Stacking more layers used to make networks worse, not better, because the learning signal degraded on the way down. The fix was to add the input of a block back onto its output, giving the signal a clear path through. It was shown on image models, and every Transformer block, including the one in Chapter 9, uses it.
The architecture this book builds. Reading a sequence one step at a time is slow, because step 500 cannot start until step 499 has finished, and distant words stay hard to connect. This paper removed the step-by-step reading entirely and kept only attention, plus a note of each token's position. Every token can then be processed at once, which is what made training on very large amounts of text practical. The model you build in Chapters 6 to 10 is this design, made small.
He, K., Zhang, X., Ren, S., & Sun, J. (2015). Deep residual learning for image recognition (arXiv:1512.03385). arXiv. https://doi.org/10.48550/arXiv.1512.03385
Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, L., & Polosukhin, I. (2017). Attention is all you need (arXiv:1706.03762). arXiv. https://doi.org/10.48550/arXiv.1706.03762
Chapter 10: The Full GPT Architecture¶
Figure 10.1: We connect everything to output a prediction.
Now that we have built all the core components of a Transformer model in previous chapters, we can assemble them (Figure 10.1). In this chapter you will:
- Build the full GPT model from end to end.
- See how data flows through the entire pipeline.
- Understand the final raw scores, called logits.
Words to Know
- LM Head: The final linear layer that maps internal numbers back to vocabulary words.
- Logits: The raw scores the model outputs for each possible next token.
Theory¶
The Final Assembly¶
The complete GPT model is surprisingly simple once you have the pieces.
Figure 10.2: The full forward pass of the GPT model.
It flows exactly like a production assembly line (as shown in Figure 10.2):
- Turn text into a list of numbers (Token IDs).
- Look up the meaning and position vectors (Embeddings).
- Process them through a stack of Transformer blocks.
- Tidy up the numbers one last time (Final LayerNorm).
- Map the internal 128 numbers back to the 65 possible characters (LM Head).
Tracing the Flow¶
Let's trace a concrete example. Suppose we feed the model the word "ROMEO" (5 tokens).
Step 1: Input
The input is [30, 27, 25, 17, 27]. These are the specific token IDs for the characters "ROMEO", where 'R' is 30, 'O' is 27, 'M' is 25, and 'E' is 17 according to our vocabulary.
Step 2: Embeddings The model converts these 5 IDs into 5 rich vectors (128 numbers each), combining both meaning and position.
Step 3: Transformer Blocks
The data passes through 4 blocks. After 4 blocks, the token O (at the end) has looked at all the previous letters. Its 128 numbers now encode something like "I am the last letter of a name from a famous play."
Step 4 & 5: Final Output A final LayerNorm stabilizes the numbers. Then, the LM Head (Language Model Head) takes those 128 numbers and projects them to 65 numbers. Why 65? Because our Shakespeare dataset has exactly 65 unique characters.
Figure 10.3: The LM Head projects internal data back to our vocabulary.
As Figure 10.3 illustrates, the model outputs 65 scores for every single token in the sequence. Each position independently predicts the character that comes next. The highest score at the final position is the model's best guess for what comes after "ROMEO".
What Are Logits?¶
The 65 scores the LM Head outputs are called logits.
Logit is a technical term for "raw score" (Figure 10.4). These numbers can be negative, zero, or very large. They do not add up to 1, so they are not probabilities yet. In Chapter 11, we will convert these raw scores into proper percentages to generate text. For now, just know that a higher logit means the model is more confident in that character.
Figure 10.4: Logits are the raw scores for the next character.
Returning to the big picture map in Figure 10.1, we have built everything from Text to Next-Token Scores. The model is fully assembled, but right now it only outputs random guesses. We need to train it.
Code¶
Run the script to see the parameter breakdown and a test run.
$ python src/ch09_gpt_model.py
Model parameter breakdown:
Token embedding : 8,320
Position embedding: 16,384
4 Transformer blocks: 791,552
LM head : 8,320
LayerNorm (final): 256
---
TOTAL : 824,832
Input shape: torch.Size([2, 10]) (B, T)
Output shape: torch.Size([2, 10, 65]) (B, T, vocab_size)
At each position, model outputs 65 scores.
The highest score = best guess for next character.
First position logits (top 5 scores):
token 26: 1.861
token 21: 1.195
token 44: 1.118
token 57: 1.071
token 34: 0.972
Note: these are random (untrained model).
What just happened:
- Line 1 assembles the complete GPT class.
- Line 12 creates the blocks that give us exactly 824,832 parameters (weights). The original GPT-2 Small had 117 million. Our model uses the exact same architecture, just scaled down.
- Line 38 finishes the forward pass, where the model outputs 65 logits (scores) for each position.
Shape Check:
- Input token IDs:
[Batch, Time] - Output logits:
[Batch, Time, 65](Vocab size is 65)
Try It¶
Try It
Open src/ch09_gpt_model.py and modify config.n_layers = 6 just to test. Run the script again. Watch how the parameter count increases. The blocks contain the vast majority of the model's "brain".
In Business
Building the final GPT architecture is like assembling a complete production pipeline from modular components. In our house-style writing assistant, we combine a data reader (embeddings), an analysis engine (the blocks), and an output formatter (the LM head). Because it is modular, you can upgrade the engine (add more layers) without rewriting the rest of the pipeline.
Key Takeaways¶
- The full model stacks embeddings, Transformer blocks, and a final LM head.
- The LM head is a linear layer that projects the internal representations back to the vocabulary.
- The model outputs logits, which are raw, un-normalized scores for the next token.
- Our complete model has ~825K parameters, proving that powerful architectures can be built at a small scale.
Check Your Understanding¶
- If the input sequence has 10 tokens, how many predictions does the model make?
- Why does the LM head output exactly 65 numbers per token?
- What is a logit?
Further Reading¶
One model, many jobs, no retraining. The question was whether a model trained only to predict the next word would pick up skills nobody trained it for. Trained on a large, varied sweep of web pages, it began answering questions, summarizing and translating with no task-specific training at all, simply because the prompt made the task clear. That settled the architecture question for text generation: a decoder that predicts the next token, which is the model in Chapter 10.
Radford, A., Wu, J., Child, R., Luan, D., Amodei, D., & Sutskever, I. (2019). Language models are unsupervised multitask learners [Technical report]. OpenAI. https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf
Chapter 11: Causal Language Modeling¶
Figure 11.1: The model is built. Now we define its goal: predicting the next token.
Now that we have assembled the full Transformer architecture in Chapter 10, our engine is built. But an engine is useless without a task (Figure 11.1). In this chapter, we give our model its objective: causal language modeling. This simply means guessing the next token based only on what came before it. We will also learn how to measure its performance.
In this chapter you will:
- Shift the training sequence to create input and target pairs.
- Understand cross-entropy loss as a measure of surprise.
- See how generation is just a loop of predicting and appending.
Words to Know
- Logits: The raw, unnormalized scores the model outputs before they are turned into probabilities.
- Softmax: A mathematical function that squashes a list of any numbers into positive percentages that add up to 100%.
- Cross-Entropy Loss: A mathematical way to measure how wrong the model's predictions are. Lower is better.
- Autoregressive: A process that uses its own past outputs as inputs for its next step.
Theory¶
The Training Trick: One Pass, Many Examples¶
How do we train a model to predict the next token? You might think we feed it one token, ask for the next, check the answer, and then feed it two tokens. That would be incredibly slow.
The brilliant insight of causal language modeling is that a single pass over a sequence of length T gives us T training examples all at once.
Figure 11.2: One sequence provides multiple training examples simultaneously.
As shown in Figure 11.2, the model processes the whole sequence. Because of the causal mask (from Chapter 6), position 2 cannot see position 3. Each position only sees what came before it. Therefore, at every single position, the model can make a valid prediction about what comes next.
Input and Target: The Shifted Pair¶
To implement this efficiently, we take our sequence of text and create two slightly different copies:
- Input (
x): The sequence missing its very last token. - Target (
y): The sequence missing its very first token.
This means that at any position i, the character in the input $x[i]$ is supposed to predict the character in the target $y[i]$. If our sequence is "HELLO", x is "HELL" and y is "ELLO". At position 0, "H" predicts "E". At position 1, "E" (with "H" as context) predicts "L", and so on.
Logits and Probabilities¶
When our model makes predictions, it does not immediately output a single character or a clean percentage. It outputs raw numbers called logits.
Figure 11.3: Softmax converts raw scores into valid percentages.
Logits can be any number: negative, positive, small, or large. To make sense of them, as illustrated in Figure 11.3, we pass them through a function called softmax. Softmax squashes all the logits so they are positive and sum to exactly 1.0 (or 100%). Now we have a probability distribution over our 65 possible characters.
Measuring Success: Cross-Entropy Loss¶
Once we have probabilities, we need to know how well the model is doing. We use a metric called cross-entropy loss.
Think of loss as a measure of the model's surprise. If the correct next character is "A", and the model assigned a 99% probability to "A", it is not surprised at all. The loss is very low. If it assigned a 1% probability to "A", it is highly surprised. The loss is very high.
If a model is completely untrained and guessing blindly among our 65 characters, it will assign roughly equal probability (about 1.5%) to each. The mathematical loss for this complete ignorance is about 4.17. We want our training process to push this number down.
This brings us to the end of the "Next-Token Scores" stage on our map. Our model now makes predictions and measures its own mistakes, setting the stage for it to learn.
Code¶
Figure 11.4: The model produces logits for the input, which are compared to the target to calculate loss.
Let's look at how this is implemented (following the flow in Figure 11.4).
| src/ch10_causal_lm.py (excerpt) | |
|---|---|
And how to run the full script:
$ python src/ch10_causal_lm.py
--- 1. Constructing input/target pairs ---
Sequence: [20, 17, 30, 30, 33, 1, 35, 53, 56, 30]
Input x: [20, 17, 30, 30, 33, 1, 35, 53, 56]
Target y: [17, 30, 30, 33, 1, 35, 53, 56, 30]
At each position i, x[i] predicts y[i].
--- 2. Computing cross-entropy loss ---
Logits shape : torch.Size([4, 20, 65])
Targets shape: torch.Size([4, 20])
Loss (random model): 4.3070
Expected loss for random: 4.1744
--- 3. Text generation ---
We extend a starting sequence one token at a time.
Generated IDs (first 10): [0, 54, 34, 5, 55, 18, 63, 52, 43, 10]
Output shape: torch.Size([1, 51])
After training (Chapter 13), this will produce real text!
What just happened:
- Lines 1 and 2 slice a short sequence of 10 tokens into an input
xof length 9 and a targetyof length 9 to demonstrate input/target pairs. - Line 4 passes a random batch of 20 tokens into the untrained model to get logits for the loss computation step.
- Lines 7 to 10 calculate the cross-entropy loss, which is 4.3070, very close to our expected random guessing loss of 4.1744.
- The output shows we generated 50 tokens. The machinery works; the weights are still random, so the characters are too.
Shape Check:
(Note: The shapes below are from the loss computation step, which uses 20 tokens, unlike the 10-token sequence used earlier. Table 11.1 lists the shapes at each step.)
Table 11.1: Tensor shapes for the cross-entropy loss calculation.
| Variable | Shape | Meaning |
|---|---|---|
token_ids |
[4, 20] |
4 batches of 20 tokens each. |
logits |
[4, 20, 65] |
A score for each of the 65 possible next characters, at every position. |
loss |
[] |
A single scalar number representing the average surprise. |
Try It¶
How does confidence affect the loss? We can simulate different prediction scenarios manually.
Lines 14 and 15 calculate the loss when the model is confident and correct, resulting in a much lower loss.
$ python src/examples/ch11_loss_demo.py
Scenario 1: Guessing blindly (50% / 50%)
Loss: 0.6931
Scenario 2: Confident and right (88% for 'A')
Loss: 0.1269
Scenario 3: Confident and wrong (88% for 'B')
Loss: 2.1269
Try It
Open src/examples/ch11_loss_demo.py. Change logits_right to [[5.0, 0.0]] to make the model even more confident. Run the script and see how close to zero the loss gets.
In Business
Imagine you are building an assistant to draft text in your company's house style. Causal language modeling is how the assistant learns to write like you. By reviewing thousands of past emails and reports (the target sequences), it learns which words typically follow other words in your organization's specific context. The loss tells you how close its drafts are to your actual historical data.
Watch Out
When calculating cross-entropy loss in PyTorch, the F.cross_entropy function expects the logits to be flattened. It wants a 2D tensor of shape [Total Tokens, Vocabulary Size], not a 3D tensor of [Batch, Time, Vocabulary Size]. That is why the code uses logits.view(B * T_len, config.vocab_size). Forgetting to reshape is a very common bug!
Key Takeaways¶
- A single forward pass on a sequence of length
TprovidesTseparate training examples, making training highly efficient. - The target sequence is simply the input sequence shifted one position into the future.
- The model outputs raw logits, which softmax converts into probabilities.
- Cross-entropy loss measures how "surprised" the model is by the correct answer. We want this number to be as low as possible.
- An untrained model guessing among 65 characters will have a loss of approximately 4.17.
Check Your Understanding¶
- If your sequence is "DATA", what is the input sequence
xand the target sequencey? - Why do we need softmax before we can interpret the model's output as percentages?
- If a model is perfectly confident and perfectly correct, what should its cross-entropy loss be?
Further Reading¶
Train once, reuse everywhere. Labeled examples are expensive, and every new task used to need its own pile of them. BERT trained one Transformer on ordinary text by hiding words and asking it to fill the blanks, then adapted that single model to many tasks with a small amount of labeled data each. It reads in both directions at once, which is exactly what the causal mask in Chapter 11 forbids: masking is what separates a model that fills blanks from one that writes forward.
Devlin, J., Chang, M.-W., Lee, K., & Toutanova, K. (2018). BERT: Pre-training of deep bidirectional transformers for language understanding (arXiv:1810.04805). arXiv. https://doi.org/10.48550/arXiv.1810.04805
Module 3: Training and Generation¶
Figure M3.1: Training and Generation in the big picture.
With the engine built, it is time to turn it on. For a business use case, this is where we train the house-style assistant on your company's specific archive and then deploy it to generate value. We will feed it Shakespeare, teach it to write, and explore how to control its creativity.
In this module you will:
- Prepare a dataset and data loader for training
- Write the training loop that improves the model
- Save your progress using checkpoints
- Learn how greedy and sampling generation work
- Use temperature and top-k to control text creativity
- Put it all together into a complete pipeline
Chapters¶
- Chapter 12: Dataset and DataLoader
- Chapter 13: The Training Loop
- Chapter 14: Checkpointing
- Chapter 15: Greedy and Sampling
- Chapter 16: Temperature and Top-k
- Chapter 17: Putting It All Together
Chapter 12: Dataset and DataLoader¶
Figure 12.1: We begin the training module by preparing our data pipeline.
We have our model, and we know our goal is to predict the next token (from Chapter 11). Now we need to feed data into the engine. Instead of pushing one character at a time, we will feed the model thousands of examples simultaneously. In this chapter, we build a pipeline to prepare and batch our Shakespeare dataset.
In this chapter you will:
- Use a sliding window to generate training examples.
- Understand how batches process multiple sequences in parallel.
- Build a PyTorch Dataset and DataLoader.
Words to Know
- Block Size: The maximum number of tokens the model can look at at one time (its context window).
- Batching: Grouping multiple training examples together and processing them at the same time.
- Dataset: A PyTorch class that defines how to retrieve a single training example.
- DataLoader: A PyTorch utility that automatically groups individual examples into batches and shuffles them.
Theory¶
The Sliding Window¶
In the last chapter, we saw how a single sequence provides multiple training examples. But how do we extract these sequences from a massive text file like the complete works of Shakespeare?
We use a sliding window (Figure 12.2).
Figure 12.2: A sliding window creates multiple, overlapping examples from one long text.
Imagine a window that can only see a certain number of characters at a time. This is our block_size (the maximum context the model can handle). We place this window at the beginning of the text to grab our first sequence. Then, we slide the window one character to the right to grab our second sequence. We repeat this until we reach the end of the text.
Processing in Parallel: Batches¶
If we fed each sequence to the model one by one, training would crawl. Modern computers, especially GPUs, are fantastic at doing the same math on many pieces of data at once, and they are wasted on one sequence at a time.
How much does it matter? Rather than guess, time it:
$ python src/examples/ch12_batch_speed.py
32 sequences, one at a time : 2.191 s
the same 32 as one batch : 0.245 s
batching is 9.0x faster on this machine
Nine times faster, on an ordinary laptop with no GPU. The training run in Chapter 13 takes about six and a half minutes; one sequence at a time it would run closer to an hour. On a GPU, where thousands of arithmetic units sit idle waiting for work, the gap is wider still.
Figure 12.3: A batch stacks multiple independent examples into a single block.
We group multiple sequences together into a batch, as illustrated in Figure 12.3. If our batch size is 32, we pass 32 independent sequences through the model in one go. The model processes them in parallel, calculates the loss for all 32, and averages it out.
Why 32 and Not 1, or 1,000¶
Speed is only half the reason to batch. The other half is the quality of the step the model takes.
Remember what the loss is for: it produces a direction to nudge the weights. With a batch of 1, that direction comes from a single stretch of Shakespeare, which might happen to be a stage direction, a run of dialogue, or a line of mostly spaces. The model would lurch after each one, correcting hard for whatever it just saw. Averaging the loss over 32 independent sequences cancels most of that noise out, so each step points somewhere closer to the truth for the text as a whole.
Why not 1,000, then? Two reasons. The whole batch has to fit in memory at once, and memory is the limit you hit first on a laptop. Beyond that, the returns fade: averaging 1,000 sequences gives a direction only slightly truer than averaging 32, while each step costs thirty times as much. The number 32 is not sacred, and you will see other books use 16 or 64. It is simply a size that is large enough to steady the direction and small enough to fit.
This brings us to the start of the "Training" stage on our map. With our data batched and ready, we can finally feed it into the model and start the training loop.
Code¶
To handle this efficiently, we use two built-in PyTorch tools: Dataset and DataLoader (flow shown in Figure 12.4).
Figure 12.4: The Dataset handles the sliding window, and the DataLoader stacks the examples into a batch.
And how to run the full script:
$ python src/ch11_dataloader.py
Data loaded: 1,115,394 total tokens
Train : 1,003,854 tokens
Val : 111,540 tokens
Dataset sizes:
Train examples: 1,003,726
Val examples: 111,412
DataLoader config:
Batch size : 32
Train batches per epoch: 31,367
--- Inspecting one batch ---
x_batch shape: torch.Size([32, 128]) (batch_size, block_size)
y_batch shape: torch.Size([32, 128]) (batch_size, block_size)
First example in batch:
x (input) : 'ness! serious vanity!\nMis-shapen chaos of well-seeming for...
y (target) : 'ess! serious vanity!\nMis-shapen chaos of well-seeming form...
(y is x shifted by 1 character)
DataLoader ready! Ready for Chapter 13.
What just happened:
- We loaded our text file and converted it to token IDs.
- Lines 12 and 14 implement the sliding window logic in
TextDataset, returning the sequence and its shifted target starting atidx. - Lines 21 to 23 create a
DataLoaderthat automatically batches and shuffles the training data. - We grabbed one batch and inspected it to confirm the target
yis just the inputxshifted by one character.
Shape Check:
Table 12.1 lists the shapes of the batched input and target.
Table 12.1: Tensor shapes for the batched input and target sequences.
| Variable | Shape | Meaning |
|---|---|---|
x_batch |
[32, 128] |
32 sequences, each containing 128 input characters. |
y_batch |
[32, 128] |
32 sequences, each containing 128 target characters. |
Try It¶
We can see the sliding window in action with a tiny dataset.
Lines 12 and 13 slice the array to create overlapping sequences for the input and target.
$ python src/examples/ch12_batching_demo.py
Data: [10, 20, 30, 40, 50, 60, 70]
Block size: 3
Example 1:
Input : [10, 20, 30]
Target : [20, 30, 40]
Example 2:
Input : [20, 30, 40]
Target : [30, 40, 50]
Example 3:
Input : [30, 40, 50]
Target : [40, 50, 60]
Example 4:
Input : [40, 50, 60]
Target : [50, 60, 70]
Try It
Open src/examples/ch12_batching_demo.py. Change block_size to 4 and run it again. Notice how the number of available examples decreases.
In Business
Imagine your house-style assistant needs to learn from a massive archive of 100,000 corporate documents. You wouldn't train it by showing it one word at a time. Processing data in parallel batches is like having the assistant review 32 different emails simultaneously, learning from all of them at once. It is the key to training efficiently at scale.
Watch Out
Be careful with the __len__ of your dataset. If you have 100 characters and a block_size of 10, you can only create 90 starting positions because you need 11 characters (10 for input, 1 extra for the target) for a valid example. That is why the code uses len(self.data) - self.block_size.
Key Takeaways¶
- A sliding window extracts overlapping sequences from a continuous block of text.
- Batching processes multiple independent sequences in parallel, dramatically speeding up training.
- PyTorch's
Datasetdefines how to grab a single example. - PyTorch's
DataLoaderhandles the tedious work of grouping examples into batches and shuffling them.
Check Your Understanding¶
- If you have a sequence of 1000 tokens and a block size of 100, how many examples can a sliding window extract?
- Why is batching important for training speed?
- Why do we shuffle the training data?
Further Reading¶
Why the field started building bigger. Before this, deciding how large to make a model, how much text to train it on, and how much compute to spend was guesswork. The paper measured all three and found the error falls along smooth, predictable curves across a very wide range of sizes. That turned model building into a budgeting exercise, and it is the reason the industry spent the following years scaling up. It also explains the ceiling on the model you train here: a few hundred thousand parameters and a few hundred thousand characters of Shakespeare buy a certain quality of text, and no more.
Kaplan, J., McCandlish, S., Henighan, T., Brown, T. B., Chess, B., Child, R., Gray, S., Radford, A., Wu, J., & Amodei, D. (2020). Scaling laws for neural language models (arXiv:2001.08361). arXiv. https://doi.org/10.48550/arXiv.2001.08361
Chapter 13: The Training Loop¶
Figure 13.1: The model makes predictions, measures its error, and adjusts its weights to improve.
Everything we have built so far comes down to this chapter. With our data batched and ready from Chapter 12, we have a model that can guess the next character, but right now, its guesses are no better than chance. In this chapter, we will write the loop that teaches it the patterns of the text it reads.
In this chapter you will:
- Understand gradient descent as walking down a hill.
- See what the learning rate does, by measuring three of them.
- Build the four-step training loop.
- Watch the model learn in real time, and read what its loss actually means.
Words to Know
- Optimizer: The algorithm that updates the model's weights. We use Adam, a popular and steady choice.
- Gradient: The direction we need to move our weights to increase the error. We move in the opposite direction to decrease it.
- Backpropagation: The mathematical process of calculating the gradient for every single weight in the model.
- Learning Rate: How far the optimizer moves the weights on each step. Too small and training crawls; too large and it can overshoot.
Theory¶
Walking Down the Hill¶
How does the model actually improve? Imagine you are blindfolded on a bumpy hillside, and you want to reach the very bottom (the lowest possible loss).
You can't see the whole hill, but you can feel the slope of the ground right under your feet. If the ground slopes up to your right, you know you should take a step to your left.
Figure 13.2: The optimizer takes small steps down the loss landscape to find the best weights.
This is gradient descent (Figure 13.2). The slope under your feet is the gradient (calculated by backpropagation). Taking a small step downhill is the optimizer updating the weights.
The Four-Step Loop¶
Training is a repetitive cycle that we run thousands of times (Figure 13.3):
Figure 13.3: The four steps of the training loop.
- Forward Pass: We pass a batch of data through the model to get its predictions.
- Calculate Loss: We compare the predictions to the correct targets using cross-entropy.
- Backward Pass: PyTorch's autograd engine automatically calculates the gradient for every weight in the model (
loss.backward()). - Optimizer Step: The optimizer adjusts the weights slightly in the right direction (
optimizer.step()).
How Big Should the Step Be?¶
Gradient descent tells you which way is downhill. It does not tell you how far to walk. That distance is the learning rate, and it is the one number beginners most often get wrong.
Our config sets it to 0.0003. Where does that come from? Rather than take it on faith, train the same model three times from the same starting weights, changing only the learning rate:
| src/examples/ch13_learning_rate.py | |
|---|---|
$ python src/examples/ch13_learning_rate.py
lr=0.003 start 4.33 after 150 steps 2.35
lr=0.0003 start 4.33 after 150 steps 2.60
lr=3e-05 start 4.33 after 150 steps 3.31
The bottom row is the lesson most people need. At 0.00003 the steps are so small that after 150 steps the model has barely moved: 3.31, when random guessing is 4.17. It is learning, just far too slowly to be useful. If your loss is falling but crawling, suspect the learning rate before you suspect anything else.
The top row is more interesting, because it does not say what you might expect. Ten times the learning rate learned faster here, reaching 2.35 while our chosen rate reached 2.60. So why does the book not use it?
Because 150 steps is not 3,000. A large step size is a gamble: it covers ground quickly, and it can also overshoot the bottom of the valley and bounce, or blow up entirely. Our run of 150 steps is too short to show that either way, so we will not pretend it does. What we can say is that 0.0003 is the cautious choice, it reaches a loss of 1.74 over the full run, and it got there without drama. Trying 0.003 for all 3,000 steps is a genuinely interesting experiment, and one you now have everything you need to run.
The "Aha!" Moment: It Learned¶
When we start training, the loss is around 4.3. Remember from Chapter 11 that a completely random model guessing among 65 characters expects a loss of 4.17. At the start, the model is worse than random.
But as the loop runs, the numbers start to move.
Figure 13.4: Over 3,000 steps, the model goes from blind guessing to predicting text with high confidence.
The loss plummets, as shown in Figure 13.4. By step 300, it is already at 2.7. By step 3,000, it reaches 1.74.
What the Loss Number Actually Means¶
A loss of 1.74 means nothing on its own. Here is how to read it.
Cross-entropy is the negative logarithm of the probability the model gave to the character that actually came next. Undo the logarithm and the probability comes back, which is a number you can reason about:
$ python src/examples/ch13_loss_meaning.py
random guessing loss 4.17 -> 1.5% on the right character
step 1 loss 4.33 -> 1.3% on the right character
step 300 loss 2.70 -> 6.7% on the right character
step 3000 loss 1.74 -> 17.6% on the right character
Now the numbers say something. At the start the model puts about 1.3% of its confidence on the correct character, slightly worse than the 1.5% you would get by drawing at random from 65 characters. Twelve times better than chance by the end sounds impressive, and it is. But read the absolute figure too: even fully trained, the model is wrong about the next character roughly four times in five.
Hold on to that, because it sets the right expectation for what you built. The model has learned which characters tend to follow which, how long words usually run, where the line breaks fall, and what a speaker's name looks like. It has not learned the rules of English, and it has certainly not learned to mean anything. You will see the evidence in Chapter 17, when it writes "Praviour soul to shall that that are the and not,": the shape of Shakespeare is there, the sense is not.
This completes the "Training" loop on our map. Our model is now a working engine that has learned the character patterns of its training data.
Code¶
Here is how simple the core loop is in PyTorch.
And how to run the full script:
$ python src/ch12_train.py
Chapter 13: The Training Loop
Model parameters: 824,832
Training on : cpu
Steps : 3,000
Batch size : 32
Block size : 128
Starting training... (eval every 300 steps)
step 1/3000 | train loss: 4.3280 | val loss: 4.2114 | elapsed: 3s | ETA: 7595s
step 300/3000 | train loss: 2.7016 | val loss: 2.5433 | elapsed: 55s | ETA: 496s
...
step 900/3000 | train loss: 2.2633 | val loss: 2.1889 | elapsed: 160s | ETA: 372s
step 1200/3000 | train loss: 2.1185 | val loss: 2.1053 | elapsed: 210s | ETA: 314s
step 1500/3000 | train loss: 2.0174 | val loss: 2.0078 | elapsed: 248s | ETA: 248s
What just happened:
- Line 1 creates an Adam optimizer to handle the weight updates.
- Line 3 runs 3,000 steps of training, which takes about 20-30 minutes on a standard CPU laptop.
- Lines 11 to 13 calculate the gradients and update the weights, driving the training loss down from 4.32 to 1.74.
- We saved our hard-earned weights to a file so we can load them later.
Shape Check:
Table 13.1 lists the parameter count.
Table 13.1: Total adjustable parameters in the language model.
| Variable | Shape | Meaning |
|---|---|---|
model.parameters() |
824,832 |
The total number of individual numbers (weights) the optimizer is adjusting. |
Try It¶
We can see the mechanics of PyTorch's automatic gradients on a tiny scale.
Lines 19, 26, and 27 show PyTorch computing the gradient and using it to adjust the weight toward the target.
$ python src/examples/ch13_autograd_demo.py
Initial weight: 2.00
Step 1: Output=6.00, Loss=36.00, Gradient=-36.00
Step 2: Output=11.40, Loss=0.36, Gradient=-3.60
Step 3: Output=11.94, Loss=0.00, Gradient=-0.36
Final weight: 4.00
Try It
Open src/examples/ch13_autograd_demo.py. Change the starting weight to 10.0 and watch how the gradient pulls it down instead of pushing it up, always aiming for the target output of 12.
In Business
Training is where the real investment happens. When your company trains its house-style assistant, it pays for the compute time required to run this loop billions of times across thousands of documents. The loss curve is your main dashboard metric: as long as it is going down, the assistant is getting better at mimicking your corporate voice.
Watch Out
Never forget optimizer.zero_grad() before loss.backward(). PyTorch accumulates gradients by default (adds them up). If you don't zero them out at the start of the backward pass, your model will take steps based on a mix of the current batch and all previous batches, wandering off in the wrong direction.
Key Takeaways¶
- Training is a loop of four steps: Forward, Loss, Backward, Step.
- Gradient descent finds the lowest loss by taking small steps downhill.
- We use the
torch.optim.Adamoptimizer to manage the complex math of updating the weights. - The model starts by guessing blindly (loss > 4.17), but over 3,000 steps, it learns the patterns of the text and the loss plummets.
Check Your Understanding¶
- What are the four main steps inside the training loop?
- What does
loss.backward()actually do? - Why is a dropping loss curve a good sign?
Further Reading¶
How a network learns anything at all. A network with layers in the middle had an obvious problem: when the answer came out wrong, nobody could say which of the middle weights was at fault. This paper gave the answer. Send the error backwards through the network and give each weight a share of the blame in proportion to how much it moved the result. Every model in this book learns that way, and so does every model in production today; loss.backward() in the training loop is this paper.
The optimizer on one line of your training loop. Backpropagation says which way each weight should move. It does not say how far. Adam gives every weight its own step size, adapted from how that weight has been moving recently, so rarely used weights can take larger steps and volatile ones settle down. It is the default in the training loop in Chapter 13, and the default in most training loops anywhere.
Kingma, D. P., & Ba, J. (2014). Adam: A method for stochastic optimization (arXiv:1412.6980). arXiv. https://doi.org/10.48550/arXiv.1412.6980
Rumelhart, D. E., Hinton, G. E., & Williams, R. J. (1986). Learning representations by back-propagating errors. Nature, 323, 533–536. https://doi.org/10.1038/323533a0
Chapter 14: Checkpointing¶
Figure 14.1: Where we are: we have trained the model and now we save its weights.
Training a language model takes time. Once the model learns from the data, you need to save its knowledge so you can use it later without retraining. In this chapter you will:
- Save a trained model to a file.
- Load a saved model back into memory.
- Switch the model from training mode to evaluation mode.
Words to Know
- Checkpoint: a file containing the saved state (weights and configuration) of a model at a specific point in training.
- State Dict: a PyTorch dictionary that maps each layer of the model to its learned weights.
- Dropout: A technique that randomly turns off some neurons during training to prevent memorizing the data.
Theory¶
The Checkpoint File¶
Figure 14.2: The save and load round trip: the model's structure and its learned weights are both stored in the checkpoint.
A checkpoint is the model's save file. When you train a model, you adjust its weights (the parameters). These learned parameters are stored in a dictionary called a state_dict.
However, the weights alone are not enough. If you close your program and come back tomorrow, PyTorch will not know how many layers or attention heads your model has. To bring the model back to life, you must save both its configuration (the architecture skeleton) and its state dictionary (the learned weights). We also save the current training step and validation loss so we know how well the model performed.
Restoring the Model¶
Loading a checkpoint happens in three steps:
- Load the saved dictionary from the file on disk.
- Build an empty model using the saved configuration.
- Pour the saved weights into the empty model.
Once the model is loaded, you must call model.eval(). During training, neural networks often use a technique called dropout, which randomly turns off some neurons to prevent the model from memorizing the data. Calling model.eval() turns off dropout, ensuring that all neurons are active and the model gives reliable, deterministic answers when generating text.
Figure 14.3: In evaluation mode, all neurons are active and the model is ready to generate text deterministically.
As the "where we are" map shows, saving the checkpoint captures the model after the training loop, preparing it to generate new text in the final stage.
In Business
Think of our house-style assistant. The training process analyzed your company's archive to learn its voice. If the server restarts, you don't want to re-read the entire archive. A checkpoint saves that learned company voice to a tiny file that you can load instantly on any machine.
Code¶
We use the PyTorch torch.load() function to read our checkpoint file, and model.load_state_dict() to apply the weights.
Let's see what happens when we load the checkpoint created at the end of the previous chapter.
$ python src/ch13_checkpoint.py
--- 2. Load the checkpoint ---
Loading checkpoint from: checkpoints/model.pt
Trained for : 3000 steps
Val loss : 1.7228
Model config : 4 layers, 128 embd, 4 heads
--- 3. Rebuild the model from the checkpoint ---
Model rebuilt successfully: 824,832 parameters loaded
--- 4. Verify the model works ---
Input shape : torch.Size([1, 10])
Output shape: torch.Size([1, 10, 65]) (looks good!)
--- 5. Show checkpoint file size ---
Checkpoint file size: 4.18 MB
(Small enough to share by email!)
Checkpointing done! Ready for Chapter 15.
Figure 14.4: How the checkpoint loading code flows: from file to ready model.
What just happened:
- Line 1 loaded the checkpoint
model.ptfrom disk. - Line 8 built an empty
GPTmodel using the loaded configuration. - Line 9 populated the model with the 824,832 learned parameters.
- Line 12 switched the model to evaluation mode.
- We verified the checkpoint file is tiny (just over 4 megabytes).
Shape Check¶
Table 14.1 shows the tensor shapes when verifying the loaded model.
Table 14.1: Input and output shapes for the restored model.
| Tensor | Shape | What it means |
|---|---|---|
dummy_ids |
[1, 10] |
1 sequence of 10 token IDs. |
logits |
[1, 10, 65] |
65 vocabulary scores for each of the 10 positions. |
Try It¶
Try It
You can inspect the checkpoint directly by writing a small script to load the file.
| src/examples/ch14_inspect_checkpoint.py (excerpt) | |
|---|---|
Lines 9 and 10 print the keys and the training step from the loaded checkpoint dictionary.
Key Takeaways¶
- A checkpoint saves the model's configuration and learned weights to a file.
- You rebuild the model by creating an empty network with the configuration, then loading the weights with
load_state_dict. - Always call
model.eval()after loading a model to turn off training features like dropout. - A model with 825,000 parameters takes up only about 4 MB of disk space.
Check Your Understanding¶
- Why do we need to save the model's configuration in the checkpoint along with the weights?
- What happens if you forget to call
model.eval()before generating text? - What PyTorch function do we use to apply the saved weights to the newly built model?
Further Reading¶
The library you are typing into. The design argument behind the tool this book uses: write the model as ordinary Python that runs line by line, so you can print a tensor or stop in a debugger, and still get the speed of compiled code underneath. It is the reason the code in this book can be read top to bottom and still trains a real model.
Paszke, A., Gross, S., Massa, F., Lerer, A., Bradbury, J., Chanan, G., Killeen, T., Lin, Z., Gimelshein, N., Antiga, L., Desmaison, A., Köpf, A., Yang, E., DeVito, Z., Raison, M., Tejani, A., Chilamkurthy, S., Steiner, B., Fang, L., ... Chintala, S. (2019). PyTorch: An imperative style, high-performance deep learning library (arXiv:1912.01703). arXiv. https://doi.org/10.48550/arXiv.1912.01703
Chapter 15: Greedy and Sampling¶
Figure 15.1: Where we are: we are ready to generate new text using the trained model.
Our model is trained and loaded. Now we want to use it to generate new text. But the model doesn't just output a single character; it produces a list of 65 scores (logits), one for each possible character in the vocabulary. We need a strategy to pick the winner. In this chapter you will:
- Generate text by always choosing the highest-scoring character (greedy decoding).
- Generate text by choosing characters randomly based on their probabilities (sampling).
- Compare the trade-offs between deterministic and creative text generation.
Words to Know
- Greedy Decoding: always picking the single highest-probability token.
- Sampling: choosing the next token randomly, giving higher-probability tokens a better chance to be selected.
Theory¶
Greedy Decoding¶
The simplest strategy is to always pick the character with the highest score. This is called greedy decoding.
While it seems logical to always pick the "best" answer, greedy decoding has a major flaw: it often gets stuck in loops. Imagine the model learns a common phrase. It predicts the next character, and that character becomes part of the context. The context looks familiar, so it predicts the next character of the phrase, and soon it is repeating the same phrase forever.
Greedy decoding is deterministic: if you give it the same prompt, it will always produce the exact same text.
Sampling¶
To avoid repetitive loops, we can use a strategy called sampling. Instead of automatically taking the top character, we turn the raw scores (logits) into percentages (probabilities) and draw a winner randomly.
Figure 15.2: Greedy decoding always takes the top peak; sampling draws randomly from the distribution.
If the letter "e" has a 60% probability, it will be chosen 60% of the time. If "x" has a 1% probability, it is rarely chosen, but it still has a chance. This introduces variety and breaks repetitive loops, making the text feel more natural and creative. However, it also means the output is unpredictable and changes every time you run it.
As shown on the "where we are" map, generation loops the New Text back to the Tokens step: the model predicts one character, we add it to the prompt, and the cycle repeats.
In Business
In our house-style assistant, the choice between greedy and sampling depends on the task. If the assistant is answering a factual question from a company manual, you want greedy decoding (safe, predictable, exact). If it is drafting a creative marketing email in the company voice, you want sampling (varied, creative, exploring new options).
Code¶
We implement both strategies in the same generation loop. The core difference is how next_id is chosen.
Line 7 picks the character with the single highest score for greedy decoding. Lines 17 and 18 turn the scores into probabilities and draw a winner randomly for sampling.
Figure 15.3: The generation loop predicts one character at a time and appends it to the context.
Notice ids[:, -cfg.block_size:]. We crop the context to block_size tokens before passing it to the model. The model only learned to read a specific maximum length (our block size of 128) during training, so we must feed it at most that many tokens. We also take logits[:, -1, :] because we only care about the predictions for the very last character.
Let's see the two strategies in action with the prompt ROMEO:\n.
$ python src/ch14_generate_greedy.py
--- GREEDY (always picks highest-score token) ---
ROMEO:
I will the come the some the stand the son,
And the so shall the see the shall the see the stand
...
--- SAMPLING (picks randomly from distribution) ---
ROMEO:
My must greather, but what it? whom, he ere.
EBRUTUS:
Morcy there bagainVain, I will ever may
To epated as great that the appral tankswain
...
--- Observations ---
Greedy tends to be more repetitive.
Sampling is more varied but can make unexpected choices.
Figure 15.4: Greedy always picks the tallest bar; sampling can pick any bar based on its height.
What just happened:
- The greedy approach quickly got stuck in a repetitive loop ("the shall the shall...").
- The sampling approach produced varied, non-repetitive text, but it included some strange spellings ("greather", "bagainVain") because it occasionally picked low-probability characters.
Shape Check¶
Table 15.1 details the shapes used during the character prediction step.
Table 15.1: Tensors used during the text generation loop.
| Tensor | Shape | What it means |
|---|---|---|
logits[:, -1, :] |
[1, 65] |
65 vocabulary scores for the final character in the sequence. |
next_id |
[1, 1] |
1 chosen character ID. |
Try It¶
Try It
Different starting prompts create different context for the model. Let's see how sampling handles new prompts.
Lines 6 and 9 generate new text starting from two completely different prompts.
Key Takeaways¶
- Greedy decoding always chooses the highest-scoring token, which leads to predictable but highly repetitive text.
- Sampling chooses the next token randomly based on the probability distribution, creating varied and natural text.
- The generation loop works by predicting one character, appending it to the context, and repeating the process.
- We crop the input context to the model's maximum
block_sizebefore each prediction.
Check Your Understanding¶
- Why does greedy decoding often get stuck repeating the same phrase?
- What PyTorch function do we use to randomly pick a token based on its probabilities?
- Why do we slice the logits tensor with
[:, -1, :]in the generation loop?
Further Reading¶
Why always picking the likeliest word goes wrong. Always taking the highest-scoring next token, which Chapter 15 calls greedy decoding, produces flat and repetitive text, and this paper shows why: human writing is not made of the most predictable word at every turn. Their alternative keeps the smallest set of tokens whose probabilities add up to a chosen share and samples from that. How you choose the next token matters as much as how well the model was trained.
Holtzman, A., Buys, J., Du, L., Forbes, M., & Choi, Y. (2019). The curious case of neural text degeneration (arXiv:1904.09751). arXiv. https://doi.org/10.48550/arXiv.1904.09751
Chapter 16: Temperature and Top-k¶
Figure 16.1: Where we are: we are tuning the text generation process.
Plain sampling gives us varied output, but it can be unpredictable. Sometimes the model chooses a very rare character that completely breaks the grammar or creates a nonsense word. We need controls to balance creativity with coherence. In this chapter you will:
- Use temperature to adjust how risky the model's choices are.
- Use top-k truncation to prevent the model from picking truly bad characters.
- Combine these controls for optimal text generation.
Words to Know
- Temperature: a number that scales the logits before they are turned into probabilities, controlling the randomness of the output.
- Top-k: a limit that restricts the model to only sample from the k most likely next tokens, ignoring all others.
Theory¶
Two controls sit between the raw scores the model produces and the character it finally picks. Temperature reshapes the odds. Top-k decides which characters are allowed to compete at all. They are independent, and in practice you set both.
Figure 16.2: Temperature changes the shape of the odds; top-k changes how many characters stay in the running.
Temperature¶
Temperature is a simple math trick applied to the logits before the softmax step. By dividing all the raw scores by a number (the temperature), we change the shape of the probability distribution.
Figure 16.3: A plotted chart showing probabilities: low temperature sharpens the choice, high temperature flattens it.
- Low temperature (e.g., 0.5): Dividing by a fraction mathematically stretches the scores apart. The top choice becomes overwhelmingly favored. The model plays it safe.
- High temperature (e.g., 2.0): Dividing by a large number mathematically squishes the scores together. The probabilities flatten out, meaning unusual choices become more likely. The model takes risks.
- T = 1.0: This is standard sampling. The model uses its learned probabilities exactly as they are.
Top-k¶
Even at a safe temperature, there is a tiny mathematical chance (say, 0.001%) that the model might pick a completely absurd character. If you generate 200 characters, those tiny chances add up, and a mistake is likely.
Top-k sampling solves this by putting a hard limit on the choices. If top_k = 40, we look at the 65 possible characters, keep the 40 with the highest scores, and completely discard the bottom 25 by setting their probabilities to zero. This cuts off the "long tail" of bad choices, guaranteeing that the model only samples from the most reasonable options.
Figure 16.4: Top-k keeps the highest-scoring characters and zeroes the rest. Drawn here with ten characters and k = 4; the book's code uses 65 and k = 40.
As the "where we are" map shows, these controls adjust the Next-Token Scores right before generation loops back to produce the New Text.
In Business
For the house-style assistant, you might use a low temperature (0.5) when generating compliance documentation to ensure it stays close to the safest boilerplate text. When brainstorming marketing slogans, a higher temperature (0.9) with top-k sampling (40) will produce creative, surprising slogans that still make grammatical sense.
Code¶
We apply temperature and top-k right before the softmax function in the generation loop.
Line 2 divides the scores by the temperature to adjust randomness. Line 7 discards any score below the top-k threshold by setting it to negative infinity.
Figure 16.5: The code flow applies temperature first, then top-k, and finally softmax.
In PyTorch, we use masked_fill to apply top-k. We find the score of the 40th best token (threshold), and any token with a score lower than that gets its value changed to negative infinity (-inf). When softmax calculates probabilities, anything with a score of -inf mathematically becomes exactly 0.
Let's see how different settings affect the generated text.
$ python src/ch15_generate_sampling.py
--- temp=0.5, top_k=40 ---
JULIET:
My must grace prove a son the come on my lord.
...
--- temp=0.8, top_k=40 ---
JULIET:
That will, my call thee for thee king win a be parder
The his in suppy ascented a not a say?
...
--- temp=1.0, top_k=40 ---
JULIET:
Look, do indo witned me devise thy grohed:
A give gring brothe whith ighers paison:
...
--- temp=1.5, top_k=40 ---
JULIET:
fattlyals give niet, thou stain, viful-wid thel,
Brob dayse mernion shing you fea,
...
--- temp=1.0, no top_k ---
JULIET:
Than mide smear of thou I am go vious, you me
now you scalf them frate ell, to 'till will she
...
--- Sweet spot ---
temperature=0.8 to 1.0 and top_k=40 usually gives the best results.
What just happened:
- At
temp=0.5, the text is coherent but relies heavily on common, safe words. - At
temp=0.8to1.0, the text feels more natural and varied. - At
temp=1.5, the text quickly devolves into chaos and made-up words ("fattlyals"). - The sweet spot is typically a temperature between 0.8 and 1.0, combined with
top_k=40.
Shape Check¶
Table 16.1 outlines the shapes of tensors used during top-k filtering.
Table 16.1: Tensors modified during the top-k sampling process.
| Tensor | Shape | What it means |
|---|---|---|
threshold |
[1, 1] |
The cutoff score (the 40th highest value). |
logits |
[1, 65] |
65 scores, where 25 of them have been set to -inf. |
Try It¶
Try It
Open the existing script src/examples/ch16_explore_temp.py and run it. It uses a very low temperature (0.1). You will notice it behaves almost exactly like greedy decoding, picking the safe top character every time!
Key Takeaways¶
- Temperature adjusts the spread of the probabilities. Low temperature makes the model conservative, while high temperature makes it creative and risky.
- Top-k restricts the model from ever picking the worst-scoring tokens, preventing bizarre errors.
- To combine them, first apply temperature, then apply top-k to filter out the bad options, and finally convert to probabilities with softmax.
- A combination of
temperature=0.8andtop_k=40is a widely used default for high-quality text generation.
Check Your Understanding¶
- If you set the temperature to 0.1, what happens to the gap between the highest and lowest scores?
- Why do we replace the eliminated scores in top-k with negative infinity (
-inf) instead of0? - Which step must happen first in the code: applying top-k or calculating softmax probabilities?
Further Reading¶
Where top-k sampling comes from. This paper is about writing stories from a prompt, and along the way it introduced the sampling rule you use in Chapter 16: keep only the k most likely next tokens and draw from those. It keeps the text varied without letting the model pick something absurd from the long tail.
Why always picking the likeliest word goes wrong. Always taking the highest-scoring next token, which Chapter 15 calls greedy decoding, produces flat and repetitive text, and this paper shows why: human writing is not made of the most predictable word at every turn. Their alternative keeps the smallest set of tokens whose probabilities add up to a chosen share and samples from that. How you choose the next token matters as much as how well the model was trained.
Fan, A., Lewis, M., & Dauphin, Y. (2018). Hierarchical neural story generation (arXiv:1805.04833). arXiv. https://doi.org/10.48550/arXiv.1805.04833
Holtzman, A., Buys, J., Du, L., Forbes, M., & Choi, Y. (2019). The curious case of neural text degeneration (arXiv:1904.09751). arXiv. https://doi.org/10.48550/arXiv.1904.09751
Chapter 17: Putting It All Together¶
Figure 17.1: Where we are: completing the full pipeline from text to language model.
You have built a language model from zero. Step by step, you wrote the code to tokenize text, embed it into vectors, apply self-attention, stack transformer blocks, train the weights using gradient descent, and generate new text with temperature and top-k sampling. In this final chapter you will:
- Run the complete end-to-end pipeline in a single script.
- See the full architecture in action.
- Understand how your model relates to modern, production-grade LLMs.
Words to Know
- End to End: a process that takes raw input (text) and goes through every necessary step to produce the final output (a trained model and generated text) without manual intervention.
- Instruction tuning: A post-training step that teaches the model to answer questions and follow instructions.
- RLHF: Reinforcement Learning from Human Feedback, a method to train models to behave politely and align with human preferences.
Theory¶
The Full Architecture¶
Take a look back at everything you built. This is not a "toy" architecture. You just wrote the exact same building blocks used by GPT-2, which is the architectural foundation of GPT-3, GPT-4, and many other modern Large Language Models (LLMs).
Figure 17.2: The complete system: from raw text to a trained language model.
The differences between your model and a massive production model are mostly a matter of scale:
- Vocabularies: We used 65 characters; they use 50,000+ subword tokens.
- Dimensions: We used a 128-dimensional embedding and 4 layers; they use thousands of dimensions and nearly a hundred layers.
- Data: We trained on 1 megabyte of Shakespeare; they train on terabytes of internet text.
- Hardware: We trained for a few minutes on a CPU; they train for months on thousands of specialized GPUs.
How Real Models Differ¶
While the fundamental transformer architecture (embeddings, attention, blocks, training) remains the same, modern models add refinements to optimize performance:
- Instead of simple position lookups, they might use Rotary Positional Embeddings, which help models understand relative distances between words better.
- Instead of standard multi-head attention, they might use Grouped-Query Attention, which saves memory and speeds up text generation.
- After pretraining (what we did), they undergo Instruction Tuning and RLHF (Reinforcement Learning from Human Feedback) to learn how to answer questions politely rather than just predicting the next word.
As the "where we are" map shows, the end-to-end pipeline connects every stage from text ingestion to generation, wrapping the entire system into one continuous flow.
In Business
For our house-style assistant, this end-to-end script represents the full product lifecycle. In a business environment, you run this pipeline whenever the company archive changes: training a new model overnight on updated documents, validating it, and deploying the new checkpoint to serve your marketing and compliance teams.
Code¶
We have combined every piece of code from the previous chapters into one master script. It downloads the data, tokenizes it, creates the DataLoaders, builds the 825,000-parameter model, trains it for 3,000 steps, saves a checkpoint, and generates a sample text.
Figure 17.3: The full script executes every step in sequence without manual intervention.
Let's run the smoke test.
$ python src/ch16_full_pipeline.py
[1/7] Downloading dataset...
...
[2/7] Tokenizing...
...
[3/7] Creating DataLoaders...
...
[4/7] Building model...
...
[5/7] Training for 3000 steps...
...
step 1 | train: 4.2726 | val: 4.1660 | elapsed: 2s | ETA: 6148s
...
step 3000 | train: 1.7323 | val: 1.7411 | elapsed: 474s | ETA: 0s
...
[6/7] Saving checkpoint...
...
[7/7] Generating text...
...
GENERATED TEXT (temperature=0.8, top_k=40):
...
ROMEO:
Praviour soul to shall that that are the and not,
What just happened:
- The model trained successfully and loss steadily decreased.
- It generated brand new, Shakespeare-like text based on the patterns it learned.
- While it makes some logical or grammatical mistakes, the character names, sentence structures, and vocabulary strongly mimic the training data.
Figure 17.4: The model generates new text.
Shape Check¶
Table 17.1 summarizes the parameter count of the fully assembled network.
Table 17.1: Final parameter count of the completed model.
| Tensor | Shape | What it means |
|---|---|---|
model parameters |
824,832 |
The total number of weights the model learned during training. |
Try It¶
Try It
Try testing the final model like a deployed production service. Create a script to load model_final.pt and respond to a user prompt.
Lines 6 and 8 check if the final checkpoint exists and fall back to a previous save if it doesn't.
Key Takeaways¶
- You successfully built the complete GPT architecture from scratch.
- The fundamental components (tokenization, embeddings, self-attention, transformer blocks, and gradient descent) are the core of all modern LLMs.
- Larger models mostly scale up these exact same components with more data, more parameters, and better hardware.
- Refinements like RLHF and instruction tuning are applied after this pretraining process.
Check Your Understanding¶
- What is the difference between the model we built and GPT-2?
- Why does the model output sometimes contain spelling or logic errors despite being fully trained?
- What is the purpose of an end-to-end pipeline in a business setting?
Further Reading¶
Where prompting came from. Even a pre-trained model normally had to be fine-tuned, with fresh labeled examples and a training run, before it could do a new job. At 175 billion parameters the authors found something different: write two or three examples into the prompt and the model follows the pattern, with no weights changed at all. That behavior is what people now call prompting, and it is why a language model became something you talk to rather than something you retrain.
The step that turns a text predictor into an assistant. A model trained to continue text is not the same thing as a model that does what you ask; it will happily continue your question with more questions. The authors collected human demonstrations of good answers and human rankings of competing answers, and fine-tuned on that feedback. The result matters for how you read this book: a 1.3 billion parameter model trained this way was preferred by people to the 175 billion parameter model it came from. Capability and helpfulness are different problems, and this book builds the first one.
Why the field started building bigger. Before this, deciding how large to make a model, how much text to train it on, and how much compute to spend was guesswork. The paper measured all three and found the error falls along smooth, predictable curves across a very wide range of sizes. That turned model building into a budgeting exercise, and it is the reason the industry spent the following years scaling up. It also explains the ceiling on the model you train here: a few hundred thousand parameters and a few hundred thousand characters of Shakespeare buy a certain quality of text, and no more.
Brown, T. B., Mann, B., Ryder, N., Subbiah, M., Kaplan, J., Dhariwal, P., Neelakantan, A., Shyam, P., Sastry, G., Askell, A., Agarwal, S., Herbert-Voss, A., Krueger, G., Henighan, T., Child, R., Ramesh, A., Ziegler, D. M., Wu, J., Winter, C., ... Amodei, D. (2020). Language models are few-shot learners (arXiv:2005.14165). arXiv. https://doi.org/10.48550/arXiv.2005.14165
Kaplan, J., McCandlish, S., Henighan, T., Brown, T. B., Chess, B., Child, R., Gray, S., Radford, A., Wu, J., & Amodei, D. (2020). Scaling laws for neural language models (arXiv:2001.08361). arXiv. https://doi.org/10.48550/arXiv.2001.08361
Ouyang, L., Wu, J., Jiang, X., Almeida, D., Wainwright, C. L., Mishkin, P., Zhang, C., Agarwal, S., Slama, K., Ray, A., Schulman, J., Hilton, J., Kelton, F., Miller, L., Simens, M., Askell, A., Welinder, P., Christiano, P., Leike, J., & Lowe, R. (2022). Training language models to follow instructions with human feedback (arXiv:2203.02155). arXiv. https://doi.org/10.48550/arXiv.2203.02155
Note. The Tiny Shakespeare text used throughout the book comes from Andrej Karpathy's char-rnn project: https://github.com/karpathy/char-rnn{ .note }
Glossary¶
Every term the book defines, in alphabetical order, with a plain definition and the chapter where it is first explained.
Table A.1: Glossary of terms used in this book.
| Term | Meaning | Chapter |
|---|---|---|
| Autoregressive | Using your past outputs as inputs for your next step. | 2 |
| Backpropagation | The mathematical process of calculating the gradient for every single weight in the model. | 13 |
| Batching | Grouping multiple training examples together and processing them at the same time. | 12 |
| Block Size | The maximum number of tokens the model can look at at one time (its context window). | 12 |
| Causal Mask | A filter that prevents tokens from seeing future tokens. | 6 |
| Checkpoint | A file containing the saved state (weights and configuration) of a model at a specific point in training. | 14 |
| Concatenation | Joining multiple vectors end-to-end to form a longer vector. | 7 |
| Cross-Entropy Loss | A mathematical way to measure how wrong the model's predictions are. Lower is better. | 11 |
| DataLoader | A PyTorch utility that automatically groups individual examples into batches and shuffles them. | 12 |
| Dataset | A PyTorch class that defines how to retrieve a single training example. | 12 |
| Dropout | A technique that randomly turns off some neurons during training to prevent memorizing the data. | 14 |
| Embedding | A lookup table that maps a token ID to its vector. | 5 |
| End to End | A process that takes raw input (text) and goes through every necessary step to produce the final output (a trained model and generated text) without manual intervention. | 17 |
| Feed-Forward | A small neural network that processes each token independently. | 8 |
| GELU | A smooth curve that replaces negative numbers with near-zero values. | 8 |
| Gradient | The direction we need to move our weights to increase the error. We move in the opposite direction to decrease it. | 13 |
| Greedy Decoding | Always picking the single highest-probability token. | 15 |
| Instruction tuning | A post-training step that teaches the model to answer questions and follow instructions. | 17 |
| Key (K) | A vector representing what a token contains. | 6 |
| Language Model | A system that predicts the next token in a sequence. | 2 |
| LayerNorm | A step that resets numbers to a safe size so training stays stable. | 8 |
| LM Head | The final linear layer that maps internal numbers back to vocabulary words. | 10 |
| Logits | The raw scores the model outputs for each possible next token. | 10 |
| Matrix Multiplication | Combining two tensors to transform data. | 3 |
| Multi-Head Attention | Running several attention operations simultaneously. | 7 |
| Optimizer | The algorithm that updates the model's weights. We use Adam, a popular and steady choice. | 13 |
| Package Manager | A tool that downloads and installs code libraries. | 1 |
| Parameters | The numbers inside the model that adjust during training. | 2 |
| Projection | A linear layer that blends the concatenated outputs. | 7 |
| PyTorch | A library for doing math on large arrays of numbers very quickly. | 3 |
| Query (Q) | A vector representing what a token is looking for. | 6 |
| Repository | A folder of code stored online. | 1 |
| Residual Connection | A shortcut that lets information skip a step, keeping the original signal intact. | 9 |
| RLHF | Reinforcement Learning from Human Feedback, a method to train models to behave politely and align with human preferences. | 17 |
| Sampling | Choosing the next token randomly, giving higher-probability tokens a better chance to be selected. | 15 |
| Self-Attention | A mechanism where tokens evaluate every other token in the sequence to gather context. | 6 |
| Shape | The dimensions of a tensor (like rows and columns). | 3 |
| Softmax | A function that turns any numbers into probabilities that sum to 1. | 3 |
| State Dict | A PyTorch dictionary that maps each layer of the model to its learned weights. | 14 |
| Temperature | A number that scales the logits before they are turned into probabilities, controlling the randomness of the output. | 16 |
| Tensor | A multi-dimensional array of numbers. | 3 |
| Terminal | A text-based window where you type commands. | 1 |
| Token | One small piece of text, here a single character. | 2 |
| Tokenizer | A function that turns text into tokens and then into numbers. | 4 |
| Top-k | A limit that restricts the model to only sample from the k most likely next tokens, ignoring all others. | 16 |
| Transformer Block | A reusable block of code containing attention, feed-forward, and normalization. | 9 |
| Value (V) | A vector holding the actual information to be shared. | 6 |
| Vector | A list of numbers that acts as a coordinate in a high-dimensional space. | 5 |
| Virtual Environment | An isolated toolbox for a single project's packages. | 1 |
Ten Papers That Built Generative AI¶
Every idea in this book was somebody's result first. These ten papers are the ones the rest stands on: each solved a problem that was blocking the field, and each changed what people built afterwards. They are in the order the story runs, from how a network learns at all to how a text predictor became something you can give instructions to.
You do not need the mathematics to follow them. Each entry says what was not working, what the paper did about it, and why it still matters, and then gives the reference so you can read the original. All ten are free to read.
Table B.1: The ten papers, in the order the ideas arrived.
| # | Paper | Year | What it settled |
|---|---|---|---|
| 1 | Backpropagation | 1986 | Gave every weight in a deep network its share of the blame. |
| 2 | Long short-term memory | 1997 | Gave a sequence model a memory it could keep over long text. |
| 3 | Word vectors | 2013 | Turned word meaning into distance in a vector space. |
| 4 | Attention | 2014 | Let a model look back over its input and weigh what mattered. |
| 5 | The Transformer | 2017 | Kept attention, removed the step-by-step reading. |
| 6 | Pre-training and transfer | 2018 | Trained one model on plain text, then reused it everywhere. |
| 7 | Decoder-only language models | 2019 | Showed next-token prediction alone learns many tasks. |
| 8 | Scaling laws | 2020 | Made the pay-off from more data and compute predictable. |
| 9 | Few-shot prompting | 2020 | Taught a model a task from examples in the prompt. |
| 10 | Learning from human feedback | 2022 | Turned a text predictor into something that follows instructions. |
Backpropagation (1986)¶
How a network learns anything at all. A network with layers in the middle had an obvious problem: when the answer came out wrong, nobody could say which of the middle weights was at fault. This paper gave the answer. Send the error backwards through the network and give each weight a share of the blame in proportion to how much it moved the result. Every model in this book learns that way, and so does every model in production today; loss.backward() in the training loop is this paper.
In this book: Chapter 13.
Rumelhart, D. E., Hinton, G. E., & Williams, R. J. (1986). Learning representations by back-propagating errors. Nature, 323, 533–536. https://doi.org/10.1038/323533a0
Long short-term memory (1997)¶
Memory, before attention. Models that read a sentence one word at a time kept forgetting the beginning by the time they reached the end, because the learning signal faded as it travelled back through the steps. The fix was a cell with gates that decide what to keep, what to drop, and what to pass on. This ran almost every serious language system for twenty years. The question it answers, what should I still remember from earlier in the text, is the same question attention answers, by a completely different route.
In this book: Chapter 2.
Hochreiter, S., & Schmidhuber, J. (1997). Long short-term memory. Neural Computation, 9(8), 1735–1780. https://doi.org/10.1162/neco.1997.9.8.1735
Word vectors (2013)¶
Meaning becomes geometry. Give every word its own ID number and the model learns nothing from the numbering: "king" sits as far from "queen" as it does from "toaster". This paper trained a deliberately cheap prediction task so that words used in similar company ended up with similar vectors, and did it fast enough to run on billions of words. Embeddings, the subject of Chapter 5, start here, and so does the vector search behind modern recommendation and retrieval.
In this book: Chapter 5.
Mikolov, T., Chen, K., Corrado, G., & Dean, J. (2013). Efficient estimation of word representations in vector space (arXiv:1301.3781). arXiv. https://doi.org/10.48550/arXiv.1301.3781
Attention (2014)¶
The first attention. Translation models of the day read the whole source sentence, squeezed it into a single fixed vector, and wrote the translation from that. Long sentences did not survive the squeeze. The fix: let the model look back over every input word and decide, at each output word, which ones matter right now. That weighted look-back is attention. The Transformer three years later kept this idea and threw out everything around it.
In this book: Chapter 6.
Bahdanau, D., Cho, K., & Bengio, Y. (2014). Neural machine translation by jointly learning to align and translate (arXiv:1409.0473). arXiv. https://doi.org/10.48550/arXiv.1409.0473
The Transformer (2017)¶
The architecture this book builds. Reading a sequence one step at a time is slow, because step 500 cannot start until step 499 has finished, and distant words stay hard to connect. This paper removed the step-by-step reading entirely and kept only attention, plus a note of each token's position. Every token can then be processed at once, which is what made training on very large amounts of text practical. The model you build in Chapters 6 to 10 is this design, made small.
In this book: Chapter 6, Chapter 7, Chapter 9.
Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, L., & Polosukhin, I. (2017). Attention is all you need (arXiv:1706.03762). arXiv. https://doi.org/10.48550/arXiv.1706.03762
Pre-training and transfer (2018)¶
Train once, reuse everywhere. Labeled examples are expensive, and every new task used to need its own pile of them. BERT trained one Transformer on ordinary text by hiding words and asking it to fill the blanks, then adapted that single model to many tasks with a small amount of labeled data each. It reads in both directions at once, which is exactly what the causal mask in Chapter 11 forbids: masking is what separates a model that fills blanks from one that writes forward.
In this book: Chapter 11.
Devlin, J., Chang, M.-W., Lee, K., & Toutanova, K. (2018). BERT: Pre-training of deep bidirectional transformers for language understanding (arXiv:1810.04805). arXiv. https://doi.org/10.48550/arXiv.1810.04805
Decoder-only language models (2019)¶
One model, many jobs, no retraining. The question was whether a model trained only to predict the next word would pick up skills nobody trained it for. Trained on a large, varied sweep of web pages, it began answering questions, summarizing and translating with no task-specific training at all, simply because the prompt made the task clear. That settled the architecture question for text generation: a decoder that predicts the next token, which is the model in Chapter 10.
In this book: Chapter 2, Chapter 10.
Radford, A., Wu, J., Child, R., Luan, D., Amodei, D., & Sutskever, I. (2019). Language models are unsupervised multitask learners [Technical report]. OpenAI. https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf
Scaling laws (2020)¶
Why the field started building bigger. Before this, deciding how large to make a model, how much text to train it on, and how much compute to spend was guesswork. The paper measured all three and found the error falls along smooth, predictable curves across a very wide range of sizes. That turned model building into a budgeting exercise, and it is the reason the industry spent the following years scaling up. It also explains the ceiling on the model you train here: a few hundred thousand parameters and a few hundred thousand characters of Shakespeare buy a certain quality of text, and no more.
In this book: Chapter 12, Chapter 17.
Kaplan, J., McCandlish, S., Henighan, T., Brown, T. B., Chess, B., Child, R., Gray, S., Radford, A., Wu, J., & Amodei, D. (2020). Scaling laws for neural language models (arXiv:2001.08361). arXiv. https://doi.org/10.48550/arXiv.2001.08361
Few-shot prompting (2020)¶
Where prompting came from. Even a pre-trained model normally had to be fine-tuned, with fresh labeled examples and a training run, before it could do a new job. At 175 billion parameters the authors found something different: write two or three examples into the prompt and the model follows the pattern, with no weights changed at all. That behavior is what people now call prompting, and it is why a language model became something you talk to rather than something you retrain.
In this book: Chapter 17.
Brown, T. B., Mann, B., Ryder, N., Subbiah, M., Kaplan, J., Dhariwal, P., Neelakantan, A., Shyam, P., Sastry, G., Askell, A., Agarwal, S., Herbert-Voss, A., Krueger, G., Henighan, T., Child, R., Ramesh, A., Ziegler, D. M., Wu, J., Winter, C., ... Amodei, D. (2020). Language models are few-shot learners (arXiv:2005.14165). arXiv. https://doi.org/10.48550/arXiv.2005.14165
Learning from human feedback (2022)¶
The step that turns a text predictor into an assistant. A model trained to continue text is not the same thing as a model that does what you ask; it will happily continue your question with more questions. The authors collected human demonstrations of good answers and human rankings of competing answers, and fine-tuned on that feedback. The result matters for how you read this book: a 1.3 billion parameter model trained this way was preferred by people to the 175 billion parameter model it came from. Capability and helpfulness are different problems, and this book builds the first one.
In this book: Chapter 17.
Ouyang, L., Wu, J., Jiang, X., Almeida, D., Wainwright, C. L., Mishkin, P., Zhang, C., Agarwal, S., Slama, K., Ray, A., Schulman, J., Hilton, J., Kelton, F., Miller, L., Simens, M., Askell, A., Welinder, P., Christiano, P., Leike, J., & Lowe, R. (2022). Training language models to follow instructions with human feedback (arXiv:2203.02155). arXiv. https://doi.org/10.48550/arXiv.2203.02155
The rest of the reading¶
These are the papers behind particular pieces of the model you built. Each one also appears at the end of the chapter that uses it.
Ba, J. L., Kiros, J. R., & Hinton, G. E. (2016). Layer normalization (arXiv:1607.06450). arXiv. https://doi.org/10.48550/arXiv.1607.06450
Fan, A., Lewis, M., & Dauphin, Y. (2018). Hierarchical neural story generation (arXiv:1805.04833). arXiv. https://doi.org/10.48550/arXiv.1805.04833
He, K., Zhang, X., Ren, S., & Sun, J. (2015). Deep residual learning for image recognition (arXiv:1512.03385). arXiv. https://doi.org/10.48550/arXiv.1512.03385
Hendrycks, D., & Gimpel, K. (2016). Gaussian error linear units (GELUs) (arXiv:1606.08415). arXiv. https://doi.org/10.48550/arXiv.1606.08415
Holtzman, A., Buys, J., Du, L., Forbes, M., & Choi, Y. (2019). The curious case of neural text degeneration (arXiv:1904.09751). arXiv. https://doi.org/10.48550/arXiv.1904.09751
Kingma, D. P., & Ba, J. (2014). Adam: A method for stochastic optimization (arXiv:1412.6980). arXiv. https://doi.org/10.48550/arXiv.1412.6980
Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2017). ImageNet classification with deep convolutional neural networks. Communications of the ACM, 60(6), 84–90. https://doi.org/10.1145/3065386
Paszke, A., Gross, S., Massa, F., Lerer, A., Bradbury, J., Chanan, G., Killeen, T., Lin, Z., Gimelshein, N., Antiga, L., Desmaison, A., Köpf, A., Yang, E., DeVito, Z., Raison, M., Tejani, A., Chilamkurthy, S., Steiner, B., Fang, L., ... Chintala, S. (2019). PyTorch: An imperative style, high-performance deep learning library (arXiv:1912.01703). arXiv. https://doi.org/10.48550/arXiv.1912.01703
Rosenblatt, F. (1958). The perceptron: A probabilistic model for information storage and organization in the brain. Psychological Review, 65(6), 386–408. https://doi.org/10.1037/h0042519
Sennrich, R., Haddow, B., & Birch, A. (2015). Neural machine translation of rare words with subword units (arXiv:1508.07909). arXiv. https://doi.org/10.48550/arXiv.1508.07909
Weizenbaum, J. (1966). ELIZA: A computer program for the study of natural language communication between man and machine. Communications of the ACM, 9(1), 36–45. https://doi.org/10.1145/365153.365168