singhpratech commited on
Commit
45b77cb
·
verified ·
1 Parent(s): ef48c9a

Notebook: plain-words narrative + 8 interactive widgets

Browse files
Files changed (1) hide show
  1. tiny_llm_from_scratch.ipynb +677 -40
tiny_llm_from_scratch.ipynb CHANGED
@@ -2,7 +2,7 @@
2
  "cells": [
3
  {
4
  "cell_type": "markdown",
5
- "id": "e2785f32",
6
  "metadata": {},
7
  "source": [
8
  "# Tiny Storyteller: building a language model from scratch\n",
@@ -22,6 +22,7 @@
22
  "## The road map\n",
23
  "| Part | What we build | Why |\n",
24
  "|---|---|---|\n",
 
25
  "| 1 | Tensors and gradients | The two things every neural net is made of |\n",
26
  "| 2 | Data: TinyStories | Simple English stories, made for small models |\n",
27
  "| 3 | A bigram model | The simplest language model that can \"write\" |\n",
@@ -37,7 +38,277 @@
37
  },
38
  {
39
  "cell_type": "markdown",
40
- "id": "5a109db5",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  "metadata": {},
42
  "source": [
43
  "## 0. Setup\n",
@@ -47,7 +318,7 @@
47
  {
48
  "cell_type": "code",
49
  "execution_count": null,
50
- "id": "00b4393c",
51
  "metadata": {},
52
  "outputs": [],
53
  "source": [
@@ -57,7 +328,7 @@
57
  {
58
  "cell_type": "code",
59
  "execution_count": null,
60
- "id": "e97e5f1c",
61
  "metadata": {},
62
  "outputs": [],
63
  "source": [
@@ -74,20 +345,141 @@
74
  },
75
  {
76
  "cell_type": "markdown",
77
- "id": "5eecb952",
78
  "metadata": {},
79
  "source": [
80
  "## 1. Tensors and gradients: the two building blocks\n",
81
  "\n",
82
- "A **tensor** is just a grid of numbers with a shape. A vector is 1-D, a matrix is 2-D,\n",
83
- "a batch of sentences is 3-D. Everything in a neural network (the input text, the weights,\n",
84
- "the output probabilities) is a tensor."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  ]
86
  },
87
  {
88
  "cell_type": "code",
89
  "execution_count": null,
90
- "id": "3f77d7e0",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  "metadata": {},
92
  "outputs": [],
93
  "source": [
@@ -100,7 +492,7 @@
100
  },
101
  {
102
  "cell_type": "markdown",
103
- "id": "2054c860",
104
  "metadata": {},
105
  "source": [
106
  "A **gradient** tells us how much the loss would change if we nudged a weight.\n",
@@ -108,13 +500,58 @@
108
  "`requires_grad=True`, do some math, call `.backward()`, and the gradient appears in `.grad`.\n",
109
  "\n",
110
  "Training a model is only this, repeated millions of times: compute a loss, get the gradients,\n",
111
- "move every weight a tiny step against its gradient."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  ]
113
  },
114
  {
115
  "cell_type": "code",
116
  "execution_count": null,
117
- "id": "0d5918ad",
118
  "metadata": {},
119
  "outputs": [],
120
  "source": [
@@ -132,7 +569,7 @@
132
  },
133
  {
134
  "cell_type": "markdown",
135
- "id": "4c3d59ca",
136
  "metadata": {},
137
  "source": [
138
  "## 2. Data: TinyStories\n",
@@ -147,7 +584,7 @@
147
  {
148
  "cell_type": "code",
149
  "execution_count": null,
150
- "id": "6af6bd64",
151
  "metadata": {},
152
  "outputs": [],
153
  "source": [
@@ -165,7 +602,7 @@
165
  },
166
  {
167
  "cell_type": "markdown",
168
- "id": "ea6277bf",
169
  "metadata": {},
170
  "source": [
171
  "## 3. The simplest language model: a bigram\n",
@@ -176,13 +613,47 @@
176
  "\"after character A, how likely is character B\". That is a **bigram model**.\n",
177
  "It is just one `nn.Embedding` table of size *vocab x vocab*.\n",
178
  "\n",
179
- "First we need a vocabulary. For now, one token = one character."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  ]
181
  },
182
  {
183
  "cell_type": "code",
184
  "execution_count": null,
185
- "id": "9a29bbc9",
186
  "metadata": {},
187
  "outputs": [],
188
  "source": [
@@ -202,7 +673,7 @@
202
  {
203
  "cell_type": "code",
204
  "execution_count": null,
205
- "id": "9990f156",
206
  "metadata": {},
207
  "outputs": [],
208
  "source": [
@@ -251,7 +722,7 @@
251
  },
252
  {
253
  "cell_type": "markdown",
254
- "id": "658f7e72",
255
  "metadata": {},
256
  "source": [
257
  "It learned *something* (the loss dropped, and it produces word-ish shapes with spaces),\n",
@@ -261,7 +732,7 @@
261
  },
262
  {
263
  "cell_type": "markdown",
264
- "id": "763829c4",
265
  "metadata": {},
266
  "source": [
267
  "## 4. A real tokenizer: byte-pair encoding (BPE)\n",
@@ -271,13 +742,45 @@
271
  "repeatedly merges the most common pairs, so frequent words become single tokens and rare\n",
272
  "words split into pieces. Every big model (GPT, Llama, Qwen) uses some form of this.\n",
273
  "\n",
274
- "We train our own tokenizer on the stories, with a small vocabulary of 4,096 tokens."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  ]
276
  },
277
  {
278
  "cell_type": "code",
279
  "execution_count": null,
280
- "id": "8bcb2397",
281
  "metadata": {},
282
  "outputs": [],
283
  "source": [
@@ -303,7 +806,7 @@
303
  {
304
  "cell_type": "code",
305
  "execution_count": null,
306
- "id": "23cb929f",
307
  "metadata": {},
308
  "outputs": [],
309
  "source": [
@@ -317,7 +820,7 @@
317
  },
318
  {
319
  "cell_type": "markdown",
320
- "id": "d2e9b068",
321
  "metadata": {},
322
  "source": [
323
  "## 5. Attention, by hand\n",
@@ -338,7 +841,7 @@
338
  {
339
  "cell_type": "code",
340
  "execution_count": null,
341
- "id": "c68c6f5c",
342
  "metadata": {},
343
  "outputs": [],
344
  "source": [
@@ -362,7 +865,51 @@
362
  },
363
  {
364
  "cell_type": "markdown",
365
- "id": "0da003d3",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  "metadata": {},
367
  "source": [
368
  "## 6. A tiny GPT\n",
@@ -377,13 +924,59 @@
377
  " - layer norm and residual connections, which keep training stable.\n",
378
  "4. **Final layer norm + output head**: turn each vector into scores over the vocabulary.\n",
379
  "\n",
380
- "We also write a `generate` method: predict the next token, append it, repeat."
 
 
381
  ]
382
  },
383
  {
384
  "cell_type": "code",
385
  "execution_count": null,
386
- "id": "b00876c0",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  "metadata": {},
388
  "outputs": [],
389
  "source": [
@@ -494,7 +1087,7 @@
494
  },
495
  {
496
  "cell_type": "markdown",
497
- "id": "9c446a37",
498
  "metadata": {},
499
  "source": [
500
  "## 7. Training\n",
@@ -508,13 +1101,57 @@
508
  "- a **learning-rate schedule** (warm up, then cosine decay) so the start is gentle and the end is precise,\n",
509
  "- **mixed precision** on NVIDIA GPUs, which is roughly 2x faster.\n",
510
  "\n",
511
- "About 1,500 steps takes a few minutes on a T4 or an M-series Mac. Watch the loss fall."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  ]
513
  },
514
  {
515
  "cell_type": "code",
516
  "execution_count": null,
517
- "id": "222e1c51",
518
  "metadata": {},
519
  "outputs": [],
520
  "source": [
@@ -581,7 +1218,7 @@
581
  },
582
  {
583
  "cell_type": "markdown",
584
- "id": "4dee51b4",
585
  "metadata": {},
586
  "source": [
587
  "**How to read the numbers.** The loss is the average \"surprise\" per token. *Perplexity* is\n",
@@ -591,7 +1228,7 @@
591
  },
592
  {
593
  "cell_type": "markdown",
594
- "id": "284708da",
595
  "metadata": {},
596
  "source": [
597
  "## 8. Generate stories: the use case\n",
@@ -603,7 +1240,7 @@
603
  {
604
  "cell_type": "code",
605
  "execution_count": null,
606
- "id": "ce19e9d9",
607
  "metadata": {},
608
  "outputs": [],
609
  "source": [
@@ -620,7 +1257,7 @@
620
  },
621
  {
622
  "cell_type": "markdown",
623
- "id": "a3a6cfad",
624
  "metadata": {},
625
  "source": [
626
  "## 9. Save and publish on the Hugging Face Hub\n",
@@ -636,7 +1273,7 @@
636
  {
637
  "cell_type": "code",
638
  "execution_count": null,
639
- "id": "e5fc0838",
640
  "metadata": {},
641
  "outputs": [],
642
  "source": [
@@ -677,7 +1314,7 @@
677
  {
678
  "cell_type": "code",
679
  "execution_count": null,
680
- "id": "7e1ae430",
681
  "metadata": {},
682
  "outputs": [],
683
  "source": [
@@ -698,7 +1335,7 @@
698
  },
699
  {
700
  "cell_type": "markdown",
701
- "id": "34f8cabf",
702
  "metadata": {},
703
  "source": [
704
  "## Loading it back later\n",
@@ -709,7 +1346,7 @@
709
  {
710
  "cell_type": "code",
711
  "execution_count": null,
712
- "id": "c5e86850",
713
  "metadata": {},
714
  "outputs": [],
715
  "source": [
@@ -730,7 +1367,7 @@
730
  },
731
  {
732
  "cell_type": "markdown",
733
- "id": "c0f44cd1",
734
  "metadata": {},
735
  "source": [
736
  "## What we built, in one paragraph\n",
 
2
  "cells": [
3
  {
4
  "cell_type": "markdown",
5
+ "id": "980e5f0c",
6
  "metadata": {},
7
  "source": [
8
  "# Tiny Storyteller: building a language model from scratch\n",
 
22
  "## The road map\n",
23
  "| Part | What we build | Why |\n",
24
  "|---|---|---|\n",
25
+ "| 0 | The story first, plus a live flow diagram | PyTorch, models, attention and the KV cache in plain words |\n",
26
  "| 1 | Tensors and gradients | The two things every neural net is made of |\n",
27
  "| 2 | Data: TinyStories | Simple English stories, made for small models |\n",
28
  "| 3 | A bigram model | The simplest language model that can \"write\" |\n",
 
38
  },
39
  {
40
  "cell_type": "markdown",
41
+ "id": "983741fb",
42
+ "metadata": {},
43
+ "source": [
44
+ "# Before any code: the story of what we are about to build\n",
45
+ "\n",
46
+ "*This section has no code. It is for anyone, technical or not. If you understand these\n",
47
+ "five ideas, every cell below will make sense.*\n",
48
+ "\n",
49
+ "## 1. What is PyTorch?\n",
50
+ "\n",
51
+ "Imagine a kitchen built for one job: cooking with enormous grids of numbers. PyTorch is that\n",
52
+ "kitchen. It gives you the pots and pans (ways to hold grids of numbers), the stove (a fast\n",
53
+ "way to do arithmetic on millions of numbers at once, using a graphics card), and one\n",
54
+ "magical appliance that nothing else in ordinary programming has: a device that, after you\n",
55
+ "cook a dish, tells you exactly *which ingredient to adjust, and by how much, to make it\n",
56
+ "taste better next time*. That appliance is called **autograd**, and it is the reason modern AI\n",
57
+ "is possible.\n",
58
+ "\n",
59
+ "PyTorch was built by Meta's AI lab and is the tool behind most of the models you have heard\n",
60
+ "of: ChatGPT's ancestors, Llama, Stable Diffusion, Whisper. Hugging Face, where we will\n",
61
+ "publish our model, is the library and community that grew around sharing PyTorch models.\n",
62
+ "\n",
63
+ "## 2. What is a \"model\", really?\n",
64
+ "\n",
65
+ "A model is a very large spreadsheet of numbers, called **weights**, plus a fixed recipe for\n",
66
+ "how to multiply and add them. Our tiny model has about six million weights. The famous big\n",
67
+ "ones have hundreds of billions. At the start, every one of those numbers is random noise:\n",
68
+ "the model knows nothing. **Training** is the process of nudging those numbers, a little at a\n",
69
+ "time, millions of times, until the recipe produces useful output.\n",
70
+ "\n",
71
+ "Real-world picture: a new employee at a help desk who starts by guessing every answer.\n",
72
+ "Each time a customer says \"no, that's wrong\", the employee adjusts slightly. After a\n",
73
+ "million customers, the guesses become good. The employee never memorized a rulebook. They\n",
74
+ "tuned their instincts. The weights are those instincts.\n",
75
+ "\n",
76
+ "## 3. What does a language model actually do?\n",
77
+ "\n",
78
+ "One thing only: **given some text, guess the next word.** That is the whole job.\n",
79
+ "\n",
80
+ "\"Once upon a time, a little\" → probably \"girl\", maybe \"boy\", maybe \"fox\".\n",
81
+ "\n",
82
+ "Writing a story is just doing that guess over and over: guess a word, add it to the text,\n",
83
+ "guess the next one, add it, and so on. Chatting with an AI assistant is the same trick. Your\n",
84
+ "question is the text so far, and its answer is a long run of next-word guesses.\n",
85
+ "\n",
86
+ "## 4. What happens when you send a question to an LLM?\n",
87
+ "\n",
88
+ "Say you type: *\"What is the capital of France?\"* Here is the journey, step by step.\n",
89
+ "\n",
90
+ "**Step 1, chopping: text becomes tokens.** The model cannot read letters. A **tokenizer**\n",
91
+ "chops your sentence into pieces called tokens (roughly word-sized: \"What\", \" is\", \" the\",\n",
92
+ "\" capital\", \" of\", \" France\", \"?\") and swaps each for a number from its dictionary. Your\n",
93
+ "question is now a short list of integers.\n",
94
+ "\n",
95
+ "**Step 2, meaning: each token becomes a point in space.** Each number is looked up in a\n",
96
+ "table and replaced by a long list of values (in our model, 256 of them). Think of it as\n",
97
+ "placing every word somewhere on a giant map, where words with similar meaning sit close\n",
98
+ "together: \"France\" near \"Spain\", \"capital\" near \"city\". The map was learned during training.\n",
99
+ "\n",
100
+ "**Step 3, looking around: attention.** This is the heart of it, and it deserves its own\n",
101
+ "section below. In short: each word looks back at all the words before it and asks \"which of\n",
102
+ "you matter to me right now?\" The word \"capital\" looks around, finds \"France\", and pulls\n",
103
+ "some of France's meaning into itself. After this step, \"capital\" no longer means \"capital\n",
104
+ "in general\". It means \"the capital of France\". This looking-around is repeated in several\n",
105
+ "layers, so meaning gets refined again and again.\n",
106
+ "\n",
107
+ "**Step 4, the guess.** The last word's refined meaning is compared against every word in the\n",
108
+ "dictionary, producing a score for each. The scores become probabilities. \"Paris\" gets, say,\n",
109
+ "92 percent. One word is picked.\n",
110
+ "\n",
111
+ "**Step 5, repeat.** \"Paris\" is appended to the text and the whole thing runs again to pick\n",
112
+ "the next word, until the model emits a special \"I'm done\" token. You see the words appear\n",
113
+ "one by one on screen because that is literally how they are produced.\n",
114
+ "\n",
115
+ "## 5. How attention \"looks things up\", in plain words\n",
116
+ "\n",
117
+ "Picture a meeting room. Every word in the sentence is a person sitting at the table. Each\n",
118
+ "person carries three cards:\n",
119
+ "\n",
120
+ "- a **query** card: \"here is what I am looking for\" (the word \"capital\" is looking for a\n",
121
+ " country),\n",
122
+ "- a **key** card: \"here is what I can offer\" (the word \"France\" offers: I am a country),\n",
123
+ "- a **value** card: \"here is what I will hand over if you pick me\" (France's actual meaning).\n",
124
+ "\n",
125
+ "Each person holds up their query card and compares it with everyone else's key card. A good\n",
126
+ "match scores high, a poor match scores low. The scores are turned into percentages that add\n",
127
+ "up to 100, and the person collects a blend of everyone's value cards, weighted by those\n",
128
+ "percentages. \"capital\" might take 70 percent from \"France\", 20 percent from \"of\", and a\n",
129
+ "little from the rest. That blend becomes its new, more specific meaning.\n",
130
+ "\n",
131
+ "Two rules make this work for *writing*:\n",
132
+ "\n",
133
+ "- **No peeking ahead.** When the model is guessing word number 7, it may only look at words\n",
134
+ " 1 to 6. In code, this is a triangular mask: future words are blocked out.\n",
135
+ "- **Many heads.** The meeting runs several times in parallel with different cards, called\n",
136
+ " \"heads\". One head might track grammar (which noun does this verb belong to), another might\n",
137
+ " track names, another might track position. Our model uses 4 heads. Big models use dozens.\n",
138
+ "\n",
139
+ "## 6. The KV cache: why chat answers do not slow down as they get longer\n",
140
+ "\n",
141
+ "Remember Step 5: to produce each new word, the whole text is run through the model again.\n",
142
+ "Done naively, writing the 500th word means re-reading 499 words from scratch, and the 501st\n",
143
+ "means re-reading 500. The cost grows and grows.\n",
144
+ "\n",
145
+ "But notice: the key card and value card of a word that is already written **never change**.\n",
146
+ "\"France\" offers the same key and value whether the sentence is 7 words or 700 words long.\n",
147
+ "So the model simply keeps every finished word's key and value cards in a drawer, the\n",
148
+ "**KV cache** (K for key, V for value). When a new word arrives, only *its* three cards are\n",
149
+ "computed. It holds up its query, compares against all the stored keys, collects the stored\n",
150
+ "values, and done. Nothing is recomputed.\n",
151
+ "\n",
152
+ "That drawer is why a long chat stays fast, and it is also why long conversations use a lot\n",
153
+ "of GPU memory: every token ever seen keeps its cards in the drawer. When people talk about a\n",
154
+ "model's \"context window\" (how much text it can hold at once), they are partly talking about\n",
155
+ "how big that drawer is allowed to get.\n",
156
+ "\n",
157
+ "We demonstrate the cache with a few lines of code in Part 5. Our tiny model does not need it\n",
158
+ "for 200-word stories, but every production LLM has one.\n",
159
+ "\n",
160
+ "## The one-sentence version\n",
161
+ "\n",
162
+ "A language model is a huge pile of tuned numbers that turns text into tokens, places them on a\n",
163
+ "map of meaning, lets each word look back at the others to sharpen its meaning (attention),\n",
164
+ "and guesses the next word, one at a time, with a cache so it never redoes old work.\n",
165
+ "\n",
166
+ "Now let's build one."
167
+ ]
168
+ },
169
+ {
170
+ "cell_type": "markdown",
171
+ "id": "5efd4788",
172
+ "metadata": {},
173
+ "source": [
174
+ "## Live flow diagram\n",
175
+ "\n",
176
+ "Run the cell below (it only draws, it trains nothing). Press **Play** and watch the model\n",
177
+ "produce one word at a time: the text is chopped into tokens, each token gets a place on the\n",
178
+ "meaning map, the newest token looks back at the others (the orange arcs are attention), the\n",
179
+ "dictionary is scored, a word is picked, and it joins the text. On the right, the KV cache\n",
180
+ "fills up one card per token, and only the newest token ever needs new cards."
181
+ ]
182
+ },
183
+ {
184
+ "cell_type": "code",
185
+ "execution_count": null,
186
+ "id": "b619ba27",
187
+ "metadata": {},
188
+ "outputs": [],
189
+ "source": [
190
+ "from IPython.display import HTML, display\n",
191
+ "\n",
192
+ "# Everything below is plain HTML + JavaScript, no libraries. It runs inside the notebook output.\n",
193
+ "display(HTML(r\"\"\"<div id=\"llmflow\" style=\"font-family:-apple-system,Segoe UI,Helvetica,Arial,sans-serif;color:#e8e8e8;background:#1e1e1e;border-radius:12px;padding:16px;max-width:980px\">\n",
194
+ "<style>\n",
195
+ "#llmflow .stage{display:inline-block;padding:8px 12px;border-radius:8px;border:2px solid #555;margin:0 4px;min-width:110px;text-align:center;font-size:13px;transition:all .3s;background:#2a2a2a}\n",
196
+ "#llmflow .stage.on{border-color:#ffb347;background:#3a2e1a;box-shadow:0 0 12px #ffb34788}\n",
197
+ "#llmflow .arrow{display:inline-block;color:#888;font-size:18px;vertical-align:middle}\n",
198
+ "#llmflow .chip{display:inline-block;padding:4px 8px;margin:3px;border-radius:6px;background:#2f4f6f;border:1px solid #5a8ab8;font-size:13px;transition:all .3s}\n",
199
+ "#llmflow .chip.new{background:#6a4a1a;border-color:#ffb347}\n",
200
+ "#llmflow .chip.q{outline:2px solid #ffb347}\n",
201
+ "#llmflow .slot{display:inline-block;width:22px;height:30px;margin:2px;border:1px dashed #666;border-radius:4px;vertical-align:middle;font-size:9px;text-align:center;line-height:30px;color:#aaa}\n",
202
+ "#llmflow .slot.full{background:#2f6f4f;border:1px solid #6fcf97;color:#fff}\n",
203
+ "#llmflow .bar{height:14px;background:#5a8ab8;border-radius:3px;margin:2px 0;transition:width .4s}\n",
204
+ "#llmflow button{background:#333;color:#eee;border:1px solid #777;border-radius:6px;padding:6px 12px;margin-right:6px;cursor:pointer;font-size:13px}\n",
205
+ "#llmflow button:hover{background:#444}\n",
206
+ "#llmflow .panel{background:#262626;border-radius:8px;padding:10px 12px;margin-top:10px;font-size:13px;line-height:1.45}\n",
207
+ "#llmflow .lbl{font-size:11px;color:#aaa;text-transform:uppercase;letter-spacing:.05em;margin:8px 0 4px}\n",
208
+ "</style>\n",
209
+ "\n",
210
+ "<div style=\"font-size:15px;font-weight:600;margin-bottom:8px\">How a language model writes: live flow</div>\n",
211
+ "<div style=\"font-size:12px;color:#aaa;margin-bottom:10px\">Press <b>Play</b> and watch one word get produced, then the next. Attention weights are illustrative.</div>\n",
212
+ "\n",
213
+ "<div id=\"stages\">\n",
214
+ " <span class=\"stage\" data-s=\"0\">1. Text so far</span><span class=\"arrow\">&#8594;</span>\n",
215
+ " <span class=\"stage\" data-s=\"1\">2. Tokenizer<br><small>chop into tokens</small></span><span class=\"arrow\">&#8594;</span>\n",
216
+ " <span class=\"stage\" data-s=\"2\">3. Meaning map<br><small>token &#8594; vector</small></span><span class=\"arrow\">&#8594;</span>\n",
217
+ " <span class=\"stage\" data-s=\"3\">4. Attention<br><small>look back</small></span><span class=\"arrow\">&#8594;</span>\n",
218
+ " <span class=\"stage\" data-s=\"4\">5. Next-word scores</span><span class=\"arrow\">&#8617;</span>\n",
219
+ " <span class=\"stage\" data-s=\"5\">6. Append word, repeat</span>\n",
220
+ "</div>\n",
221
+ "\n",
222
+ "<div class=\"lbl\">Text so far (each chip is one token)</div>\n",
223
+ "<div id=\"chips\" style=\"min-height:36px\"></div>\n",
224
+ "<svg id=\"arcs\" width=\"940\" height=\"70\" style=\"display:block\"></svg>\n",
225
+ "\n",
226
+ "<div style=\"display:flex;gap:20px;flex-wrap:wrap\">\n",
227
+ " <div style=\"flex:1;min-width:280px\">\n",
228
+ " <div class=\"lbl\">Next-word scores (top 5)</div>\n",
229
+ " <div id=\"bars\"></div>\n",
230
+ " </div>\n",
231
+ " <div style=\"flex:1;min-width:280px\">\n",
232
+ " <div class=\"lbl\">KV cache: stored key/value cards, one per token</div>\n",
233
+ " <div id=\"cache\"></div>\n",
234
+ " <div style=\"font-size:11px;color:#aaa;margin-top:4px\">Only the newest token computes its cards. Old ones are reused.</div>\n",
235
+ " </div>\n",
236
+ "</div>\n",
237
+ "\n",
238
+ "<div class=\"panel\" id=\"explain\">Press Play.</div>\n",
239
+ "<div style=\"margin-top:10px\">\n",
240
+ " <button onclick=\"llmPlay()\">&#9654; Play</button>\n",
241
+ " <button onclick=\"llmStep()\">Step</button>\n",
242
+ " <button onclick=\"llmReset()\">Reset</button>\n",
243
+ " <label style=\"font-size:12px;color:#aaa;margin-left:8px\">Speed <input id=\"spd\" type=\"range\" min=\"1\" max=\"5\" value=\"3\" style=\"vertical-align:middle\"></label>\n",
244
+ "</div>\n",
245
+ "</div>\n",
246
+ "\n",
247
+ "<script>\n",
248
+ "(function(){\n",
249
+ "const prompt = [\"Once\",\" upon\",\" a\",\" time\",\",\",\" a\",\" little\",\" fox\"];\n",
250
+ "const gen = [\n",
251
+ " {w:\" found\", top:[[\" found\",.41],[\" was\",.22],[\" saw\",.14],[\" lived\",.09],[\" had\",.06]], att:[.05,.03,.04,.06,.02,.08,.22,.50]},\n",
252
+ " {w:\" a\", top:[[\" a\",.55],[\" the\",.18],[\" some\",.08],[\" her\",.06],[\" his\",.05]], att:[.03,.02,.05,.03,.02,.06,.12,.27,.40]},\n",
253
+ " {w:\" shiny\", top:[[\" shiny\",.31],[\" big\",.27],[\" red\",.16],[\" small\",.10],[\" little\",.06]], att:[.02,.02,.03,.02,.01,.04,.18,.33,.10,.25]},\n",
254
+ " {w:\" ball\", top:[[\" ball\",.46],[\" stone\",.15],[\" box\",.12],[\" hat\",.08],[\" key\",.05]], att:[.02,.01,.02,.02,.01,.03,.08,.28,.06,.12,.35]},\n",
255
+ " {w:\".\", top:[[\".\",.52],[\" in\",.14],[\" and\",.12],[\" on\",.08],[\" that\",.05]], att:[.02,.01,.02,.03,.02,.02,.05,.15,.05,.10,.18,.35]},\n",
256
+ "];\n",
257
+ "let tokens=[], gi=0, stage=-1, timer=null;\n",
258
+ "const $=id=>document.getElementById(id);\n",
259
+ "const txt={\n",
260
+ " 0:\"<b>1. Text so far.</b> Everything written so far, prompt plus generated words, is the input. Nothing else.\",\n",
261
+ " 1:\"<b>2. Tokenizer.</b> The text is chopped into tokens (roughly words, note the leading spaces) and each becomes a number from the model's dictionary.\",\n",
262
+ " 2:\"<b>3. Meaning map.</b> Each token number is swapped for a vector, a long list of values, placing it on a learned map where similar words sit close together.\",\n",
263
+ " 3:\"<b>4. Attention.</b> The newest token (orange outline) looks back at every earlier token. Arc thickness shows how much it attends to each. It blends their value cards into its own meaning. Future tokens do not exist yet, so there is nothing to peek at. Repeated in 6 layers with 4 heads each.\",\n",
264
+ " 4:\"<b>5. Next-word scores.</b> The refined meaning of the last token is compared with every word in the dictionary. Scores become probabilities; one word is sampled.\",\n",
265
+ " 5:\"<b>6. Append and repeat.</b> The chosen word is added to the text and its key/value cards go into the KV cache. The loop starts again from step 1, but only the new token needs computing.\",\n",
266
+ "};\n",
267
+ "function render(){\n",
268
+ " document.querySelectorAll('#llmflow .stage').forEach(s=>s.classList.toggle('on',+s.dataset.s===stage));\n",
269
+ " const c=$('chips'); c.innerHTML='';\n",
270
+ " tokens.forEach((t,i)=>{const s=document.createElement('span');s.className='chip'+(i===tokens.length-1&&gi>0&&stage<5?' new':'')+(stage===3&&i===tokens.length-1?' q':'');s.textContent=t.replace(/^ /,'␣');c.appendChild(s);});\n",
271
+ " const k=$('cache'); k.innerHTML='';\n",
272
+ " const nCached = stage>=5 ? tokens.length : Math.max(0, tokens.length-(gi>0?1:0));\n",
273
+ " tokens.forEach((t,i)=>{const s=document.createElement('span');s.className='slot'+(i<nCached?' full':'');s.textContent=i<nCached?'KV':'';k.appendChild(s);});\n",
274
+ " const b=$('bars'); b.innerHTML='';\n",
275
+ " if(stage>=4 && gi<gen.length){gen[gi].top.forEach(([w,p])=>{b.innerHTML+=`<div style=\"display:flex;align-items:center;gap:8px;font-size:12px\"><span style=\"width:60px;text-align:right\">${w.replace(/^ /,'␣')}</span><div class=\"bar\" style=\"width:${Math.round(p*260)}px\"></div><span style=\"color:#aaa\">${Math.round(p*100)}%</span></div>`;});}\n",
276
+ " else b.innerHTML='<div style=\"color:#666;font-size:12px\">(appears at step 5)</div>';\n",
277
+ " drawArcs();\n",
278
+ " $('explain').innerHTML = stage<0 ? 'Press Play.' : txt[stage];\n",
279
+ "}\n",
280
+ "function drawArcs(){\n",
281
+ " const svg=$('arcs'); svg.innerHTML='';\n",
282
+ " if(stage!==3||gi>=gen.length) return;\n",
283
+ " const chips=[...document.querySelectorAll('#chips .chip')]; if(chips.length<2) return;\n",
284
+ " const base=$('chips').getBoundingClientRect(); const last=chips[chips.length-1].getBoundingClientRect();\n",
285
+ " const x1=last.left-base.left+last.width/2; const w=gen[gi].att;\n",
286
+ " chips.slice(0,-1).forEach((ch,i)=>{const r=ch.getBoundingClientRect();const x0=r.left-base.left+r.width/2;const a=w[i]||0.02;\n",
287
+ " const p=document.createElementNS('http://www.w3.org/2000/svg','path');\n",
288
+ " p.setAttribute('d',`M${x1},2 Q${(x0+x1)/2},${20+Math.abs(x1-x0)/6} ${x0},2`);\n",
289
+ " p.setAttribute('fill','none');p.setAttribute('stroke','#ffb347');p.setAttribute('stroke-width',(0.5+a*10).toFixed(1));p.setAttribute('opacity',(0.25+a*1.5).toFixed(2));\n",
290
+ " svg.appendChild(p);\n",
291
+ " const t=document.createElementNS('http://www.w3.org/2000/svg','text');t.setAttribute('x',x0);t.setAttribute('y',60);t.setAttribute('fill','#ffb347');t.setAttribute('font-size','10');t.setAttribute('text-anchor','middle');t.textContent=Math.round(a*100)+'%';svg.appendChild(t);});\n",
292
+ "}\n",
293
+ "window.llmReset=function(){clearInterval(timer);timer=null;tokens=prompt.slice();gi=0;stage=-1;render();};\n",
294
+ "window.llmStep=function(){\n",
295
+ " if(gi>=gen.length){$('explain').innerHTML='<b>Done.</b> The model wrote: <i>'+tokens.join('')+'</i>. Press Reset to watch again.';clearInterval(timer);timer=null;return;}\n",
296
+ " stage++;\n",
297
+ " if(stage===5){tokens.push(gen[gi].w);}\n",
298
+ " if(stage>5){stage=0;gi++; if(gi>=gen.length){render();window.llmStep();return;}}\n",
299
+ " render();\n",
300
+ "};\n",
301
+ "window.llmPlay=function(){if(timer){clearInterval(timer);timer=null;return;} const ms=[0,1600,1200,900,600,350][+$('spd').value]; timer=setInterval(window.llmStep,ms); window.llmStep();};\n",
302
+ "window.llmReset();\n",
303
+ "window.addEventListener('resize',drawArcs);\n",
304
+ "})();\n",
305
+ "</script>\n",
306
+ "\"\"\"))"
307
+ ]
308
+ },
309
+ {
310
+ "cell_type": "markdown",
311
+ "id": "b33f712a",
312
  "metadata": {},
313
  "source": [
314
  "## 0. Setup\n",
 
318
  {
319
  "cell_type": "code",
320
  "execution_count": null,
321
+ "id": "649f2bf7",
322
  "metadata": {},
323
  "outputs": [],
324
  "source": [
 
328
  {
329
  "cell_type": "code",
330
  "execution_count": null,
331
+ "id": "c60680a5",
332
  "metadata": {},
333
  "outputs": [],
334
  "source": [
 
345
  },
346
  {
347
  "cell_type": "markdown",
348
+ "id": "05aa805c",
349
  "metadata": {},
350
  "source": [
351
  "## 1. Tensors and gradients: the two building blocks\n",
352
  "\n",
353
+ "A **tensor** is just a grid of numbers with a shape. Think of three everyday objects:\n",
354
+ "\n",
355
+ "- a **shopping list** is 1-D: one line of items. Shape `(4,)`. We call it a vector.\n",
356
+ "- a **spreadsheet** is 2-D: rows and columns. Shape `(3, 4)`. We call it a matrix.\n",
357
+ "- a **stack of spreadsheets**, one per customer, is 3-D. Shape `(2, 3, 4)`.\n",
358
+ "\n",
359
+ "Everything in a neural network is one of these: the input text (one row per token), the\n",
360
+ "weights (big spreadsheets), the output probabilities (one row per position). Click through\n",
361
+ "the dimensions below and hover any cell to see its address."
362
+ ]
363
+ },
364
+ {
365
+ "cell_type": "code",
366
+ "execution_count": null,
367
+ "id": "62b705cc",
368
+ "metadata": {},
369
+ "outputs": [],
370
+ "source": [
371
+ "# Interactive: 1-D, 2-D and 3-D tensors. Run this cell, then click and hover.\n",
372
+ "from IPython.display import HTML, display\n",
373
+ "display(HTML(r\"\"\"<div id=\"vt\" style=\"font-family:-apple-system,Segoe UI,Helvetica,Arial,sans-serif;color:#e8e8e8;background:#1e1e1e;border-radius:12px;padding:16px;max-width:900px\">\n",
374
+ "<style>\n",
375
+ "#vt button{background:#333;color:#eee;border:1px solid #777;border-radius:6px;padding:6px 12px;margin-right:6px;cursor:pointer}\n",
376
+ "#vt button.on{background:#6a4a1a;border-color:#ffb347}\n",
377
+ "#vt .cell{display:inline-block;width:38px;height:30px;line-height:30px;text-align:center;font-size:12px;border:1px solid #5a8ab8;background:#2f4f6f;margin:1px;border-radius:3px;cursor:crosshair}\n",
378
+ "#vt .cell:hover{background:#ffb347;color:#000}\n",
379
+ "#vt .grid{display:inline-block;padding:6px;background:#262626;border-radius:6px;margin:6px;vertical-align:top}\n",
380
+ "#vt .cap{font-size:13px;line-height:1.5;margin-top:10px;background:#262626;border-radius:8px;padding:10px}\n",
381
+ "</style>\n",
382
+ "<div style=\"font-size:15px;font-weight:600;margin-bottom:6px\">A tensor is a grid of numbers with a shape. Click a dimension, hover a cell.</div>\n",
383
+ "<div><button id=\"b1\" class=\"on\" onclick=\"vtShow(1)\">1-D: vector</button><button id=\"b2\" onclick=\"vtShow(2)\">2-D: matrix</button><button id=\"b3\" onclick=\"vtShow(3)\">3-D: batch</button></div>\n",
384
+ "<div id=\"vtArea\" style=\"margin-top:10px;min-height:120px\"></div>\n",
385
+ "<div id=\"vtHover\" style=\"font-size:12px;color:#ffb347;height:18px\"></div>\n",
386
+ "<div class=\"cap\" id=\"vtCap\"></div>\n",
387
+ "</div>\n",
388
+ "<script>\n",
389
+ "(function(){\n",
390
+ "const caps={\n",
391
+ "1:\"<b>Shape (4,)</b>: one row of numbers. Real-world: a shopping list, or one word's meaning as 4 numbers. In our model each token becomes a vector of <b>256</b> numbers.\",\n",
392
+ "2:\"<b>Shape (3, 4)</b>: rows and columns, like a spreadsheet. Real-world: one sentence, one row per token, one column per feature. Our model holds a 256-token story as a (256, 256) matrix.\",\n",
393
+ "3:\"<b>Shape (2, 3, 4)</b>: a stack of spreadsheets. Real-world: a batch of sentences processed at the same time, which is how GPUs stay busy. Training uses batches of 32 stories: shape (32, 256, 256).\"};\n",
394
+ "function grid(r,c,prefix){let h='<div class=\\\"grid\\\">';for(let i=0;i<r;i++){h+='<div>';for(let j=0;j<c;j++){const v=(Math.round((Math.sin(i*3+j*7+prefix.length)*5)*10)/10).toFixed(1);h+=`<span class=\\\"cell\\\" onmouseover=\\\"vtH('${prefix}${i},${j}', ${v})\\\" onmouseout=\\\"vtH('')\\\">${v}</span>`;}h+='</div>';}return h+'</div>';}\n",
395
+ "window.vtH=function(idx,v){document.getElementById('vtHover').textContent=idx?`index [${idx}] value ${v}`:'';};\n",
396
+ "window.vtShow=function(d){[1,2,3].forEach(k=>document.getElementById('b'+k).classList.toggle('on',k===d));\n",
397
+ " const a=document.getElementById('vtArea');\n",
398
+ " if(d===1)a.innerHTML=grid(1,4,'');\n",
399
+ " if(d===2)a.innerHTML=grid(3,4,'');\n",
400
+ " if(d===3)a.innerHTML='<span style=\"font-size:12px;color:#aaa\">sentence 0</span>'+grid(3,4,'0,')+'<span style=\"font-size:12px;color:#aaa\">sentence 1</span>'+grid(3,4,'1,');\n",
401
+ " document.getElementById('vtCap').innerHTML=caps[d];};\n",
402
+ "window.vtShow(1);\n",
403
+ "})();\n",
404
+ "</script>\n",
405
+ "\"\"\"))"
406
+ ]
407
+ },
408
+ {
409
+ "cell_type": "markdown",
410
+ "id": "690ef025",
411
+ "metadata": {},
412
+ "source": [
413
+ "### How matrix multiplication actually happens\n",
414
+ "\n",
415
+ "Almost all the work inside a neural network is one operation: multiplying two of those\n",
416
+ "spreadsheets together. People find it mysterious because textbooks show the formula first.\n",
417
+ "Here is the story instead. Two customers buy items. Two shops price those items differently.\n",
418
+ "What does each customer pay in each shop? Walk one row along one column, multiply the\n",
419
+ "pairs, add them up. That is one cell of the answer. Press **Next cell** and watch."
420
  ]
421
  },
422
  {
423
  "cell_type": "code",
424
  "execution_count": null,
425
+ "id": "9be728bd",
426
+ "metadata": {},
427
+ "outputs": [],
428
+ "source": [
429
+ "# Interactive: matrix multiplication, one output cell at a time.\n",
430
+ "from IPython.display import HTML, display\n",
431
+ "display(HTML(r\"\"\"<div id=\"vm\" style=\"font-family:-apple-system,Segoe UI,Helvetica,Arial,sans-serif;color:#e8e8e8;background:#1e1e1e;border-radius:12px;padding:16px;max-width:960px\">\n",
432
+ "<style>\n",
433
+ "#vm button{background:#333;color:#eee;border:1px solid #777;border-radius:6px;padding:6px 12px;margin-right:6px;cursor:pointer}\n",
434
+ "#vm table{border-collapse:separate;border-spacing:3px;display:inline-table;vertical-align:middle;margin:0 6px}\n",
435
+ "#vm td,#vm th{width:46px;height:34px;text-align:center;font-size:13px;border-radius:5px}\n",
436
+ "#vm td{background:#2f4f6f;border:1px solid #5a8ab8;transition:all .25s}\n",
437
+ "#vm th{color:#aaa;font-weight:normal;font-size:11px;background:transparent}\n",
438
+ "#vm td.row{background:#6a4a1a;border-color:#ffb347}\n",
439
+ "#vm td.col{background:#2f6f4f;border-color:#6fcf97}\n",
440
+ "#vm td.out{background:#ffb347;color:#000;font-weight:600}\n",
441
+ "#vm td.empty{background:#262626;border:1px dashed #555;color:#666}\n",
442
+ "#vm .sym{font-size:22px;color:#aaa;vertical-align:middle;margin:0 4px}\n",
443
+ "#vm .cap{font-size:13px;line-height:1.55;margin-top:10px;background:#262626;border-radius:8px;padding:10px;min-height:60px}\n",
444
+ "#vm .work{font-family:monospace;font-size:14px;color:#ffb347;margin-top:6px}\n",
445
+ "</style>\n",
446
+ "<div style=\"font-size:15px;font-weight:600;margin-bottom:4px\">Matrix multiplication, one cell at a time</div>\n",
447
+ "<div style=\"font-size:12px;color:#aaa;margin-bottom:10px\">Story: two customers buy items. Two shops have different prices. What does each customer pay at each shop?</div>\n",
448
+ "<div id=\"vmArea\" style=\"overflow-x:auto;white-space:nowrap\"></div>\n",
449
+ "<div class=\"work\" id=\"vmWork\"></div>\n",
450
+ "<div class=\"cap\" id=\"vmCap\"></div>\n",
451
+ "<div style=\"margin-top:10px\"><button onclick=\"vmStep()\">Next cell</button><button onclick=\"vmAuto()\">Play all</button><button onclick=\"vmReset()\">Reset</button></div>\n",
452
+ "</div>\n",
453
+ "<script>\n",
454
+ "(function(){\n",
455
+ "const A=[[2,1,3],[1,4,0]]; // customers x items (apples, bread, milk)\n",
456
+ "const B=[[1,2],[3,2],[2,4]]; // items x shops (price at shop 1, shop 2)\n",
457
+ "const items=[\"apples\",\"bread\",\"milk\"], custs=[\"Ana\",\"Ben\"], shops=[\"Shop 1\",\"Shop 2\"];\n",
458
+ "const C=[[null,null],[null,null]]; let k=-1, timer=null;\n",
459
+ "function tbl(M,rh,ch,cls){let h='<table><tr><th></th>'+ch.map(c=>`<th>${c}</th>`).join('')+'</tr>';M.forEach((r,i)=>{h+=`<tr><th>${rh[i]}</th>`+r.map((v,j)=>`<td class=\"${cls(i,j,v)}\">${v===null?'?':v}</td>`).join('')+'</tr>';});return h+'</table>';}\n",
460
+ "function render(){const i=k<0?-1:Math.floor(k/2), j=k<0?-1:k%2;\n",
461
+ " document.getElementById('vmArea').innerHTML=\n",
462
+ " tbl(A,custs,items,(r,c)=>r===i?'row':'')+'<span class=\"sym\">&times;</span>'+\n",
463
+ " tbl(B,items,shops,(r,c)=>c===j?'col':'')+'<span class=\"sym\">=</span>'+\n",
464
+ " tbl(C,custs,shops,(r,c,v)=>(r===i&&c===j)?'out':(v===null?'empty':''));\n",
465
+ " if(k<0){document.getElementById('vmWork').textContent='';document.getElementById('vmCap').innerHTML='<b>Left table:</b> how many of each item each customer buys (rows = customers, columns = items).<br><b>Right table:</b> the price of each item in each shop (rows = items, columns = shops).<br>Press <b>Next cell</b>. Each answer cell = one <span style=\"color:#ffb347\">row</span> from the left paired with one <span style=\"color:#6fcf97\">column</span> from the right: multiply matching pairs, add them up.';return;}\n",
466
+ " const terms=A[i].map((a,t)=>`${a}&times;${B[t][j]}`), prods=A[i].map((a,t)=>a*B[t][j]), tot=prods.reduce((x,y)=>x+y,0); C[i][j]=tot;\n",
467
+ " document.getElementById('vmArea').innerHTML=tbl(A,custs,items,(r,c)=>r===i?'row':'')+'<span class=\"sym\">&times;</span>'+tbl(B,items,shops,(r,c)=>c===j?'col':'')+'<span class=\"sym\">=</span>'+tbl(C,custs,shops,(r,c,v)=>(r===i&&c===j)?'out':(v===null?'empty':''));\n",
468
+ " document.getElementById('vmWork').innerHTML=`${custs[i]} at ${shops[j]}: ${terms.join(' + ')} = ${prods.join(' + ')} = <b>${tot}</b>`;\n",
469
+ " document.getElementById('vmCap').innerHTML=`Take <span style=\"color:#ffb347\"><b>${custs[i]}'s row</b></span> (what she/he buys) and <span style=\"color:#6fcf97\"><b>${shops[j]}'s column</b></span> (its prices). Walk along both at the same time: ${A[i].map((a,t)=>`${a} ${items[t]} &times; ${B[t][j]} each`).join(', ')}. Add up: <b>${tot}</b>. That is one cell of the answer.<br><br>Why it matters: a neural-network layer is exactly this. Left table = your tokens (one row per token, one column per feature). Right table = the layer's weights. The answer = the tokens' new features. GPUs exist to do this billions of times per second.`;}\n",
470
+ "window.vmStep=function(){if(k>=3){return;}k++;render();};\n",
471
+ "window.vmAuto=function(){if(timer)return;timer=setInterval(()=>{if(k>=3){clearInterval(timer);timer=null;return;}vmStep();},1400);};\n",
472
+ "window.vmReset=function(){clearInterval(timer);timer=null;k=-1;C[0]=[null,null];C[1]=[null,null];render();};\n",
473
+ "render();\n",
474
+ "})();\n",
475
+ "</script>\n",
476
+ "\"\"\"))"
477
+ ]
478
+ },
479
+ {
480
+ "cell_type": "code",
481
+ "execution_count": null,
482
+ "id": "07dc3007",
483
  "metadata": {},
484
  "outputs": [],
485
  "source": [
 
492
  },
493
  {
494
  "cell_type": "markdown",
495
+ "id": "dae67dc6",
496
  "metadata": {},
497
  "source": [
498
  "A **gradient** tells us how much the loss would change if we nudged a weight.\n",
 
500
  "`requires_grad=True`, do some math, call `.backward()`, and the gradient appears in `.grad`.\n",
501
  "\n",
502
  "Training a model is only this, repeated millions of times: compute a loss, get the gradients,\n",
503
+ "move every weight a tiny step against its gradient.\n",
504
+ "\n",
505
+ "Picture a hiker in thick fog trying to reach the bottom of a valley. They cannot see the\n",
506
+ "valley, but they can feel which way the ground slopes under their feet. That slope is the\n",
507
+ "gradient. Step downhill, feel again, step again. Drag the slider or press **Roll**."
508
+ ]
509
+ },
510
+ {
511
+ "cell_type": "code",
512
+ "execution_count": null,
513
+ "id": "e0342de2",
514
+ "metadata": {},
515
+ "outputs": [],
516
+ "source": [
517
+ "# Interactive: gradient descent as a ball rolling downhill.\n",
518
+ "from IPython.display import HTML, display\n",
519
+ "display(HTML(r\"\"\"<div id=\"vg\" style=\"font-family:-apple-system,Segoe UI,Helvetica,Arial,sans-serif;color:#e8e8e8;background:#1e1e1e;border-radius:12px;padding:16px;max-width:900px\">\n",
520
+ "<style>#vg button{background:#333;color:#eee;border:1px solid #777;border-radius:6px;padding:6px 12px;margin-right:6px;cursor:pointer}#vg .cap{font-size:13px;line-height:1.5;margin-top:8px;background:#262626;border-radius:8px;padding:10px}</style>\n",
521
+ "<div style=\"font-size:15px;font-weight:600;margin-bottom:6px\">Gradient descent: a ball rolling downhill. Drag the weight, or let it roll.</div>\n",
522
+ "<svg id=\"vgSvg\" width=\"560\" height=\"260\" style=\"background:#262626;border-radius:8px\"></svg>\n",
523
+ "<div style=\"margin-top:8px\"><label style=\"font-size:13px\">weight w <input id=\"vgW\" type=\"range\" min=\"-4\" max=\"8\" step=\"0.05\" value=\"7\" style=\"width:260px;vertical-align:middle\" oninput=\"vgDraw(+this.value)\"></label>\n",
524
+ "<button onclick=\"vgStep()\">Take one step</button><button onclick=\"vgAuto()\">Roll (auto)</button></div>\n",
525
+ "<div class=\"cap\" id=\"vgCap\"></div>\n",
526
+ "</div>\n",
527
+ "<script>\n",
528
+ "(function(){\n",
529
+ "const svg=document.getElementById('vgSvg'),W=560,H=260;const lossOf=w=>0.5*(w-2)*(w-2)+1, gradOf=w=>(w-2);\n",
530
+ "const X=w=>40+(w+4)/12*(W-80), Y=l=>H-30-(l/19)*(H-60);\n",
531
+ "let timer=null;\n",
532
+ "window.vgDraw=function(w){document.getElementById('vgW').value=w;let d='';for(let x=-4;x<=8;x+=0.1){d+=(d?'L':'M')+X(x).toFixed(1)+','+Y(lossOf(x)).toFixed(1);}\n",
533
+ " const l=lossOf(w),g=gradOf(w),lr=0.3,wn=w-lr*g;\n",
534
+ " svg.innerHTML=`<path d=\"${d}\" fill=\"none\" stroke=\"#5a8ab8\" stroke-width=\"3\"/>\n",
535
+ " <text x=\"${W/2}\" y=\"${H-6}\" fill=\"#aaa\" font-size=\"12\" text-anchor=\"middle\">weight value w</text>\n",
536
+ " <text x=\"14\" y=\"${H/2}\" fill=\"#aaa\" font-size=\"12\" transform=\"rotate(-90 14,${H/2})\" text-anchor=\"middle\">loss (how wrong)</text>\n",
537
+ " <line x1=\"${X(w)}\" y1=\"${Y(l)}\" x2=\"${X(wn)}\" y2=\"${Y(l)}\" stroke=\"#ffb347\" stroke-width=\"3\" marker-end=\"url(#ar)\"/>\n",
538
+ " <defs><marker id=\"ar\" markerWidth=\"8\" markerHeight=\"8\" refX=\"6\" refY=\"4\" orient=\"auto\"><path d=\"M0,0 L8,4 L0,8 z\" fill=\"#ffb347\"/></marker></defs>\n",
539
+ " <circle cx=\"${X(w)}\" cy=\"${Y(l)}\" r=\"9\" fill=\"#ffb347\" stroke=\"#fff\" stroke-width=\"2\"/>\n",
540
+ " <text x=\"${X(2)}\" y=\"${Y(1)+22}\" fill=\"#6fcf97\" font-size=\"12\" text-anchor=\"middle\">best weight (lowest loss)</text>`;\n",
541
+ " document.getElementById('vgCap').innerHTML=`w = <b>${w.toFixed(2)}</b> &nbsp; loss = <b>${l.toFixed(2)}</b> &nbsp; gradient = <b>${g.toFixed(2)}</b><br>The gradient is the slope under the ball. Positive slope means \"loss goes up if w grows\", so we move w <i>down</i> by learning rate &times; gradient: ${w.toFixed(2)} &minus; 0.3 &times; ${g.toFixed(2)} = <b>${wn.toFixed(2)}</b>.<br>Real-world: a hiker in fog feeling which way the ground slopes and stepping downhill. Training does this for all 6 million weights at once, thousands of times.`;\n",
542
+ " return wn;};\n",
543
+ "window.vgStep=function(){const w=+document.getElementById('vgW').value;const wn=vgDraw(w);vgDraw(Math.abs(wn-2)<0.02?2:wn);};\n",
544
+ "window.vgAuto=function(){if(timer){clearInterval(timer);timer=null;return;}timer=setInterval(()=>{const w=+document.getElementById('vgW').value;if(Math.abs(w-2)<0.03){clearInterval(timer);timer=null;return;}vgStep();},350);};\n",
545
+ "vgDraw(7);\n",
546
+ "})();\n",
547
+ "</script>\n",
548
+ "\"\"\"))"
549
  ]
550
  },
551
  {
552
  "cell_type": "code",
553
  "execution_count": null,
554
+ "id": "ab57ff9e",
555
  "metadata": {},
556
  "outputs": [],
557
  "source": [
 
569
  },
570
  {
571
  "cell_type": "markdown",
572
+ "id": "4e717021",
573
  "metadata": {},
574
  "source": [
575
  "## 2. Data: TinyStories\n",
 
584
  {
585
  "cell_type": "code",
586
  "execution_count": null,
587
+ "id": "67dfd24b",
588
  "metadata": {},
589
  "outputs": [],
590
  "source": [
 
602
  },
603
  {
604
  "cell_type": "markdown",
605
+ "id": "3758528e",
606
  "metadata": {},
607
  "source": [
608
  "## 3. The simplest language model: a bigram\n",
 
613
  "\"after character A, how likely is character B\". That is a **bigram model**.\n",
614
  "It is just one `nn.Embedding` table of size *vocab x vocab*.\n",
615
  "\n",
616
+ "First we need a vocabulary. For now, one token = one character.\n",
617
+ "\n",
618
+ "Before the code, play with the idea. The widget below counts, from a few sample sentences,\n",
619
+ "which letter tends to follow which. Click a letter. That table *is* the whole model."
620
+ ]
621
+ },
622
+ {
623
+ "cell_type": "code",
624
+ "execution_count": null,
625
+ "id": "69fb3e89",
626
+ "metadata": {},
627
+ "outputs": [],
628
+ "source": [
629
+ "# Interactive: the bigram table, counted live from a few sentences.\n",
630
+ "from IPython.display import HTML, display\n",
631
+ "display(HTML(r\"\"\"<div id=\"vb\" style=\"font-family:-apple-system,Segoe UI,Helvetica,Arial,sans-serif;color:#e8e8e8;background:#1e1e1e;border-radius:12px;padding:16px;max-width:900px\">\n",
632
+ "<style>#vb .k{display:inline-block;width:26px;height:26px;line-height:26px;text-align:center;margin:2px;border-radius:5px;background:#2f4f6f;border:1px solid #5a8ab8;cursor:pointer;font-size:13px}#vb .k.on{background:#ffb347;color:#000}#vb .bar{height:14px;background:#6fcf97;border-radius:3px}#vb .cap{font-size:13px;line-height:1.5;margin-top:8px;background:#262626;border-radius:8px;padding:10px}</style>\n",
633
+ "<div style=\"font-size:15px;font-weight:600;margin-bottom:6px\">Bigram model: click a letter to see what it thinks comes next.</div>\n",
634
+ "<div id=\"vbKeys\"></div>\n",
635
+ "<div id=\"vbOut\" style=\"margin-top:10px;min-height:120px\"></div>\n",
636
+ "<div class=\"cap\">A bigram model is nothing but this table: for every character, how often each other character followed it in the training text. It is counted here live from a few sample sentences. It cannot remember anything further back than one letter, which is why its stories are gibberish. The GPT fixes that with attention.</div>\n",
637
+ "</div>\n",
638
+ "<script>\n",
639
+ "(function(){\n",
640
+ "const corpus=\"once upon a time there was a little fox. the fox liked to play in the park with her friend tom. one day they found a shiny red ball. they were very happy and played all day. the end. lily had a big red balloon. she gave it to her mom. her mom smiled and said thank you.\";\n",
641
+ "const keys=\"abcdefghijklmnopqrstuvwxyz. \".split('');const counts={};\n",
642
+ "for(let i=0;i<corpus.length-1;i++){const a=corpus[i],b=corpus[i+1];counts[a]=counts[a]||{};counts[a][b]=(counts[a][b]||0)+1;}\n",
643
+ "const kd=document.getElementById('vbKeys');keys.forEach(k=>{const s=document.createElement('span');s.className='k';s.textContent=k===' '?'␣':k;s.onclick=()=>vbShow(k);kd.appendChild(s);});\n",
644
+ "window.vbShow=function(k){[...kd.children].forEach((s,i)=>s.classList.toggle('on',keys[i]===k));const c=counts[k]||{};const tot=Object.values(c).reduce((a,b)=>a+b,0);\n",
645
+ " const rows=Object.entries(c).sort((a,b)=>b[1]-a[1]).slice(0,6);\n",
646
+ " document.getElementById('vbOut').innerHTML=tot?`<div style=\"font-size:13px;margin-bottom:6px\">After <b>\"${k===' '?'space':k}\"</b> (seen ${tot} times), the next character was:</div>`+rows.map(([ch,n])=>`<div style=\"display:flex;align-items:center;gap:8px;font-size:13px\"><span style=\"width:40px;text-align:right\">${ch===' '?'␣':ch}</span><div class=\"bar\" style=\"width:${Math.round(n/tot*300)}px\"></div><span style=\"color:#aaa\">${Math.round(n/tot*100)}%</span></div>`).join(''):`<div style=\"font-size:13px;color:#aaa\">\"${k}\" never appears in the sample text, so the model has no idea.</div>`;};\n",
647
+ "vbShow('t');\n",
648
+ "})();\n",
649
+ "</script>\n",
650
+ "\"\"\"))"
651
  ]
652
  },
653
  {
654
  "cell_type": "code",
655
  "execution_count": null,
656
+ "id": "b6b441fb",
657
  "metadata": {},
658
  "outputs": [],
659
  "source": [
 
673
  {
674
  "cell_type": "code",
675
  "execution_count": null,
676
+ "id": "3301c521",
677
  "metadata": {},
678
  "outputs": [],
679
  "source": [
 
722
  },
723
  {
724
  "cell_type": "markdown",
725
+ "id": "073e9f1b",
726
  "metadata": {},
727
  "source": [
728
  "It learned *something* (the loss dropped, and it produces word-ish shapes with spaces),\n",
 
732
  },
733
  {
734
  "cell_type": "markdown",
735
+ "id": "07cea514",
736
  "metadata": {},
737
  "source": [
738
  "## 4. A real tokenizer: byte-pair encoding (BPE)\n",
 
742
  "repeatedly merges the most common pairs, so frequent words become single tokens and rare\n",
743
  "words split into pieces. Every big model (GPT, Llama, Qwen) uses some form of this.\n",
744
  "\n",
745
+ "We train our own tokenizer on the stories, with a small vocabulary of 4,096 tokens.\n",
746
+ "\n",
747
+ "Type your own sentence below and watch it get chopped into pieces and turned into numbers."
748
+ ]
749
+ },
750
+ {
751
+ "cell_type": "code",
752
+ "execution_count": null,
753
+ "id": "2d9f5023",
754
+ "metadata": {},
755
+ "outputs": [],
756
+ "source": [
757
+ "# Interactive: type a sentence, see tokens and ids (illustrative splitter).\n",
758
+ "from IPython.display import HTML, display\n",
759
+ "display(HTML(r\"\"\"<div id=\"vk\" style=\"font-family:-apple-system,Segoe UI,Helvetica,Arial,sans-serif;color:#e8e8e8;background:#1e1e1e;border-radius:12px;padding:16px;max-width:900px\">\n",
760
+ "<style>#vk .chip{display:inline-block;padding:4px 8px;margin:3px;border-radius:6px;background:#2f4f6f;border:1px solid #5a8ab8;font-size:13px}#vk .id{display:inline-block;padding:4px 8px;margin:3px;border-radius:6px;background:#2f6f4f;border:1px solid #6fcf97;font-size:12px;font-family:monospace}#vk input{width:95%;padding:8px;border-radius:6px;border:1px solid #777;background:#262626;color:#eee;font-size:14px}#vk .cap{font-size:13px;line-height:1.5;margin-top:8px;background:#262626;border-radius:8px;padding:10px}</style>\n",
761
+ "<div style=\"font-size:15px;font-weight:600;margin-bottom:6px\">Tokenizer: type anything and watch it get chopped into tokens, then numbers.</div>\n",
762
+ "<input id=\"vkIn\" value=\"Once upon a time, a little fox found a shiny red ball.\" oninput=\"vkRun()\">\n",
763
+ "<div style=\"font-size:12px;color:#aaa;margin:8px 0 2px\">tokens (&#9251; marks a leading space)</div><div id=\"vkTok\"></div>\n",
764
+ "<div style=\"font-size:12px;color:#aaa;margin:8px 0 2px\">token ids (what the model actually receives)</div><div id=\"vkIds\"></div>\n",
765
+ "<div class=\"cap\">A tokenizer splits text into pieces it has seen often. Common words stay whole (\"little\"), rare or long words break into parts (\"un\" + \"believ\" + \"able\"), and punctuation is its own token. Each piece has a fixed number in the dictionary. <i>This demo mimics the idea in JavaScript; the real one is trained from data in Part 4.</i></div>\n",
766
+ "</div>\n",
767
+ "<script>\n",
768
+ "(function(){\n",
769
+ "function hash(s){let h=7;for(const ch of s)h=(h*31+ch.charCodeAt(0))%4096;return h;}\n",
770
+ "function split(t){const out=[];const re=/\\s?[A-Za-z]+|\\s?\\d+|\\s?[^\\sA-Za-z\\d]/g;let m;while((m=re.exec(t))){let p=m[0];if(p.replace(/^\\s/,'').length>7){const lead=p.startsWith(' ')?' ':'';const w=p.trim();out.push(lead+w.slice(0,Math.ceil(w.length/2)));out.push(w.slice(Math.ceil(w.length/2)));}else out.push(p);}return out;}\n",
771
+ "window.vkRun=function(){const t=document.getElementById('vkIn').value;const toks=split(t);\n",
772
+ " document.getElementById('vkTok').innerHTML=toks.map(p=>`<span class=\"chip\">${p.replace(/^ /,'␣')}</span>`).join('');\n",
773
+ " document.getElementById('vkIds').innerHTML=toks.map(p=>`<span class=\"id\">${hash(p)}</span>`).join('')+`<span style=\"font-size:12px;color:#aaa;margin-left:8px\">${t.length} characters &#8594; ${toks.length} tokens</span>`;};\n",
774
+ "vkRun();\n",
775
+ "})();\n",
776
+ "</script>\n",
777
+ "\"\"\"))"
778
  ]
779
  },
780
  {
781
  "cell_type": "code",
782
  "execution_count": null,
783
+ "id": "e1d5392c",
784
  "metadata": {},
785
  "outputs": [],
786
  "source": [
 
806
  {
807
  "cell_type": "code",
808
  "execution_count": null,
809
+ "id": "b38d367d",
810
  "metadata": {},
811
  "outputs": [],
812
  "source": [
 
820
  },
821
  {
822
  "cell_type": "markdown",
823
+ "id": "3a3b651d",
824
  "metadata": {},
825
  "source": [
826
  "## 5. Attention, by hand\n",
 
841
  {
842
  "cell_type": "code",
843
  "execution_count": null,
844
+ "id": "459979b9",
845
  "metadata": {},
846
  "outputs": [],
847
  "source": [
 
865
  },
866
  {
867
  "cell_type": "markdown",
868
+ "id": "57ea4612",
869
+ "metadata": {},
870
+ "source": [
871
+ "### The KV cache, in twelve lines\n",
872
+ "\n",
873
+ "When the model writes, a new token arrives at every step. The keys and values of the tokens\n",
874
+ "already written **never change**, so we keep them in a store (the cache) and compute cards\n",
875
+ "only for the newcomer. Below: four tokens are already cached, a fifth arrives, and we check\n",
876
+ "that the cached shortcut gives exactly the same answer as recomputing all five from scratch."
877
+ ]
878
+ },
879
+ {
880
+ "cell_type": "code",
881
+ "execution_count": null,
882
+ "id": "81bf3c28",
883
+ "metadata": {},
884
+ "outputs": [],
885
+ "source": [
886
+ "Wq, Wk, Wv = nn.Linear(C, head, bias=False), nn.Linear(C, head, bias=False), nn.Linear(C, head, bias=False)\n",
887
+ "\n",
888
+ "x_old = torch.randn(1, 4, C) # 4 tokens already processed earlier\n",
889
+ "K_cache, V_cache = Wk(x_old), Wv(x_old) # their key/value cards, stored once: \"the drawer\"\n",
890
+ "\n",
891
+ "x_new = torch.randn(1, 1, C) # the 5th token arrives\n",
892
+ "q_new, k_new, v_new = Wq(x_new), Wk(x_new), Wv(x_new) # only ITS cards are computed\n",
893
+ "K_cache = torch.cat([K_cache, k_new], dim=1) # the drawer grows by one\n",
894
+ "V_cache = torch.cat([V_cache, v_new], dim=1)\n",
895
+ "\n",
896
+ "att = F.softmax(q_new @ K_cache.transpose(-2, -1) / math.sqrt(head), dim=-1) # 1 query vs 5 stored keys\n",
897
+ "out_cached = att @ V_cache\n",
898
+ "\n",
899
+ "# Check against the slow way: recompute attention for all 5 tokens from scratch, take the last row.\n",
900
+ "x_all = torch.cat([x_old, x_new], dim=1)\n",
901
+ "scores = Wq(x_all) @ Wk(x_all).transpose(-2, -1) / math.sqrt(head)\n",
902
+ "scores = scores.masked_fill(torch.tril(torch.ones(5, 5)) == 0, float(\"-inf\"))\n",
903
+ "out_full = (F.softmax(scores, dim=-1) @ Wv(x_all))[:, -1:]\n",
904
+ "\n",
905
+ "print(\"tokens stored in cache:\", K_cache.shape[1])\n",
906
+ "print(\"cached shortcut == full recompute:\", torch.allclose(out_cached, out_full, atol=1e-6))\n",
907
+ "print(\"work done this step: 1 token's cards instead of 5. For a 1,000-word chat, that is 1 instead of 1,000.\")"
908
+ ]
909
+ },
910
+ {
911
+ "cell_type": "markdown",
912
+ "id": "190d030b",
913
  "metadata": {},
914
  "source": [
915
  "## 6. A tiny GPT\n",
 
924
  " - layer norm and residual connections, which keep training stable.\n",
925
  "4. **Final layer norm + output head**: turn each vector into scores over the vocabulary.\n",
926
  "\n",
927
+ "We also write a `generate` method: predict the next token, append it, repeat.\n",
928
+ "\n",
929
+ "Click any layer in the stack below to read what it does and why it is there."
930
  ]
931
  },
932
  {
933
  "cell_type": "code",
934
  "execution_count": null,
935
+ "id": "0c74a9de",
936
+ "metadata": {},
937
+ "outputs": [],
938
+ "source": [
939
+ "# Interactive: the model's layers, bottom to top. Click a layer.\n",
940
+ "from IPython.display import HTML, display\n",
941
+ "display(HTML(r\"\"\"<div id=\"vs\" style=\"font-family:-apple-system,Segoe UI,Helvetica,Arial,sans-serif;color:#e8e8e8;background:#1e1e1e;border-radius:12px;padding:16px;max-width:900px\">\n",
942
+ "<style>#vs .box{display:block;width:300px;margin:4px auto;padding:8px;text-align:center;border-radius:8px;border:2px solid #5a8ab8;background:#2f4f6f;cursor:pointer;font-size:13px;transition:all .2s}#vs .box:hover,#vs .box.on{border-color:#ffb347;background:#3a2e1a}#vs .blk{border:2px dashed #6fcf97;border-radius:10px;padding:8px;width:340px;margin:6px auto;position:relative}#vs .tag{position:absolute;right:-70px;top:40%;color:#6fcf97;font-size:13px;font-weight:600}#vs .ar{text-align:center;color:#888;font-size:16px;line-height:14px}#vs .cap{font-size:13px;line-height:1.5;background:#262626;border-radius:8px;padding:10px;min-height:70px}</style>\n",
943
+ "<div style=\"font-size:15px;font-weight:600;margin-bottom:6px\">The tiny GPT, bottom to top. Click any layer.</div>\n",
944
+ "<div style=\"display:flex;gap:16px;flex-wrap:wrap\"><div style=\"flex:0 0 420px\">\n",
945
+ "<span class=\"box\" onclick=\"vsX(this,'out')\">next-word scores (4,096 numbers)</span><div class=\"ar\">&#8593;</div>\n",
946
+ "<span class=\"box\" onclick=\"vsX(this,'lnf')\">final LayerNorm + output head</span><div class=\"ar\">&#8593;</div>\n",
947
+ "<div class=\"blk\"><span class=\"tag\">&times; 6 blocks</span>\n",
948
+ "<span class=\"box\" onclick=\"vsX(this,'res2')\">+ add back (residual)</span><div class=\"ar\">&#8593;</div>\n",
949
+ "<span class=\"box\" onclick=\"vsX(this,'ff')\">feed-forward network</span><div class=\"ar\">&#8593;</div>\n",
950
+ "<span class=\"box\" onclick=\"vsX(this,'ln')\">LayerNorm</span><div class=\"ar\">&#8593;</div>\n",
951
+ "<span class=\"box\" onclick=\"vsX(this,'res1')\">+ add back (residual)</span><div class=\"ar\">&#8593;</div>\n",
952
+ "<span class=\"box\" onclick=\"vsX(this,'attn')\">multi-head attention (4 heads)</span><div class=\"ar\">&#8593;</div>\n",
953
+ "<span class=\"box\" onclick=\"vsX(this,'ln')\">LayerNorm</span></div>\n",
954
+ "<div class=\"ar\">&#8593;</div>\n",
955
+ "<span class=\"box\" onclick=\"vsX(this,'emb')\">token embedding + position embedding</span><div class=\"ar\">&#8593;</div>\n",
956
+ "<span class=\"box\" onclick=\"vsX(this,'tok')\">token ids (up to 256 of them)</span>\n",
957
+ "</div><div style=\"flex:1;min-width:260px\"><div class=\"cap\" id=\"vsCap\">Click a layer to read what it does and why it is there.</div></div></div>\n",
958
+ "</div>\n",
959
+ "<script>\n",
960
+ "(function(){const t={\n",
961
+ "tok:\"<b>Token ids.</b> The input: a list of up to 256 integers from the tokenizer. Real-world: the page numbers of words in a dictionary.\",\n",
962
+ "emb:\"<b>Embeddings.</b> Each id is swapped for a 256-number vector (its meaning), and a second vector for its position (1st, 2nd, 3rd...) is added, so the model knows word order. Real-world: each word gets a seat number and a name badge.\",\n",
963
+ "ln:\"<b>LayerNorm.</b> Rescales the numbers so they stay in a sensible range. It does no 'thinking'; it keeps training stable. Real-world: turning the volume to a steady level before each stage.\",\n",
964
+ "attn:\"<b>Multi-head attention.</b> Each token looks back at earlier tokens and blends in what matters. 4 heads run in parallel, each looking for something different (grammar, names, position...). This is the only place tokens talk to each other.\",\n",
965
+ "res1:\"<b>Residual (add back).</b> The attention output is <i>added</i> to the original, not replacing it. The token keeps its own meaning plus what it learned. It also gives gradients a direct path, which is what lets deep networks train at all.\",\n",
966
+ "ff:\"<b>Feed-forward network.</b> A small two-layer network applied to every token on its own, 256 &#8594; 1024 &#8594; 256. This is where facts and patterns are stored. Real-world: after listening to everyone (attention), each person goes away and thinks privately.\",\n",
967
+ "res2:\"<b>Residual again.</b> Same trick: add the feed-forward result back onto the stream.\",\n",
968
+ "lnf:\"<b>Final LayerNorm + output head.</b> One last rescale, then each token's 256-number vector is multiplied against the embedding table (shared weights, 'weight tying') to produce a score for all 4,096 dictionary entries.\",\n",
969
+ "out:\"<b>Scores.</b> 4,096 numbers per position. Softmax turns them into probabilities, and the next word is sampled from them. During training these scores are compared with the real next word to compute the loss.\"};\n",
970
+ "window.vsX=function(el,k){document.querySelectorAll('#vs .box').forEach(b=>b.classList.remove('on'));el.classList.add('on');document.getElementById('vsCap').innerHTML=t[k];};\n",
971
+ "})();\n",
972
+ "</script>\n",
973
+ "\"\"\"))"
974
+ ]
975
+ },
976
+ {
977
+ "cell_type": "code",
978
+ "execution_count": null,
979
+ "id": "cc29c0f8",
980
  "metadata": {},
981
  "outputs": [],
982
  "source": [
 
1087
  },
1088
  {
1089
  "cell_type": "markdown",
1090
+ "id": "515072d6",
1091
  "metadata": {},
1092
  "source": [
1093
  "## 7. Training\n",
 
1101
  "- a **learning-rate schedule** (warm up, then cosine decay) so the start is gentle and the end is precise,\n",
1102
  "- **mixed precision** on NVIDIA GPUs, which is roughly 2x faster.\n",
1103
  "\n",
1104
+ "About 1,500 steps takes a few minutes on a T4 or an M-series Mac. Watch the loss fall.\n",
1105
+ "\n",
1106
+ "Step through the loop below first to see the five stages and a loss curve drawing itself."
1107
+ ]
1108
+ },
1109
+ {
1110
+ "cell_type": "code",
1111
+ "execution_count": null,
1112
+ "id": "69241e04",
1113
+ "metadata": {},
1114
+ "outputs": [],
1115
+ "source": [
1116
+ "# Interactive: the training loop, stage by stage, with a loss curve.\n",
1117
+ "from IPython.display import HTML, display\n",
1118
+ "display(HTML(r\"\"\"<div id=\"vr\" style=\"font-family:-apple-system,Segoe UI,Helvetica,Arial,sans-serif;color:#e8e8e8;background:#1e1e1e;border-radius:12px;padding:16px;max-width:900px\">\n",
1119
+ "<style>#vr button{background:#333;color:#eee;border:1px solid #777;border-radius:6px;padding:6px 12px;margin-right:6px;cursor:pointer}#vr .st{display:inline-block;padding:8px 10px;margin:3px;border-radius:8px;border:2px solid #555;background:#2a2a2a;font-size:13px;min-width:120px;text-align:center;transition:all .2s}#vr .st.on{border-color:#ffb347;background:#3a2e1a}#vr .cap{font-size:13px;line-height:1.5;margin-top:8px;background:#262626;border-radius:8px;padding:10px;min-height:48px}</style>\n",
1120
+ "<div style=\"font-size:15px;font-weight:600;margin-bottom:6px\">The training loop. Step through it and watch the loss fall.</div>\n",
1121
+ "<div id=\"vrStages\">\n",
1122
+ "<span class=\"st\" data-i=\"0\">1. grab a batch<br><small>32 random story windows</small></span><span style=\"color:#888\">&#8594;</span>\n",
1123
+ "<span class=\"st\" data-i=\"1\">2. forward<br><small>model guesses next tokens</small></span><span style=\"color:#888\">&#8594;</span>\n",
1124
+ "<span class=\"st\" data-i=\"2\">3. loss<br><small>how wrong, on average</small></span><span style=\"color:#888\">&#8594;</span>\n",
1125
+ "<span class=\"st\" data-i=\"3\">4. backward<br><small>autograd: gradient per weight</small></span><span style=\"color:#888\">&#8594;</span>\n",
1126
+ "<span class=\"st\" data-i=\"4\">5. optimizer step<br><small>nudge all 6M weights</small></span><span style=\"color:#888\">&#8617;</span>\n",
1127
+ "</div>\n",
1128
+ "<div style=\"display:flex;gap:16px;flex-wrap:wrap;margin-top:10px\"><svg id=\"vrSvg\" width=\"460\" height=\"200\" style=\"background:#262626;border-radius:8px\"></svg>\n",
1129
+ "<div style=\"flex:1;min-width:240px\"><div class=\"cap\" id=\"vrCap\">Press Step, or Run 20 steps.</div>\n",
1130
+ "<div style=\"margin-top:8px\"><button onclick=\"vrStep()\">Step</button><button onclick=\"vrRun()\">Run 20 steps</button><button onclick=\"vrReset()\">Reset</button></div>\n",
1131
+ "<div style=\"font-size:12px;color:#aaa;margin-top:6px\">step <b id=\"vrN\">0</b> &nbsp; loss <b id=\"vrL\">-</b></div></div></div>\n",
1132
+ "</div>\n",
1133
+ "<script>\n",
1134
+ "(function(){\n",
1135
+ "const txt=[\"<b>Grab a batch.</b> 32 random 256-token windows from the stories, plus the same windows shifted one token right: the correct answers.\",\"<b>Forward.</b> Run the batch through the model. Out come scores for every position's next token.\",\"<b>Loss.</b> Cross-entropy compares the scores with the true next tokens. One number: average surprise. Lower is better.\",\"<b>Backward.</b> Autograd walks the computation in reverse and gives every one of the 6 million weights its gradient: 'nudge me this way to lower the loss'.\",\"<b>Optimizer step.</b> AdamW moves every weight a tiny bit against its gradient. Then the loop repeats, 1,500 times.\"];\n",
1136
+ "let stage=-1,n=0,pts=[];const svg=document.getElementById('vrSvg');let timer=null;\n",
1137
+ "function loss(k){return 1.9+6.5*Math.exp(-k/6)+ (Math.sin(k*1.7)*0.12);}\n",
1138
+ "function draw(){let d='';pts.forEach((l,i)=>{d+=(i?'L':'M')+(30+i*(410/60)).toFixed(1)+','+(180-(l-1.5)/7.5*160).toFixed(1);});\n",
1139
+ " svg.innerHTML=`<text x=\"240\" y=\"196\" fill=\"#aaa\" font-size=\"11\" text-anchor=\"middle\">training steps</text><text x=\"12\" y=\"100\" fill=\"#aaa\" font-size=\"11\" transform=\"rotate(-90 12,100)\" text-anchor=\"middle\">loss</text><path d=\"${d}\" fill=\"none\" stroke=\"#6fcf97\" stroke-width=\"2.5\"/>`+(pts.length?`<circle cx=\"${30+(pts.length-1)*(410/60)}\" cy=\"${180-(pts[pts.length-1]-1.5)/7.5*160}\" r=\"4\" fill=\"#ffb347\"/>`:'');\n",
1140
+ " document.querySelectorAll('#vr .st').forEach(s=>s.classList.toggle('on',+s.dataset.i===stage));\n",
1141
+ " document.getElementById('vrCap').innerHTML=stage<0?'Press Step, or Run 20 steps.':txt[stage];document.getElementById('vrN').textContent=n;document.getElementById('vrL').textContent=pts.length?pts[pts.length-1].toFixed(2):'-';}\n",
1142
+ "window.vrStep=function(){stage=(stage+1)%5;if(stage===2){n++;if(pts.length<60)pts.push(loss(n));}draw();};\n",
1143
+ "window.vrRun=function(){if(timer)return;let c=0;timer=setInterval(()=>{vrStep();if(++c>=100){clearInterval(timer);timer=null;}},120);};\n",
1144
+ "window.vrReset=function(){clearInterval(timer);timer=null;stage=-1;n=0;pts=[];draw();};\n",
1145
+ "draw();\n",
1146
+ "})();\n",
1147
+ "</script>\n",
1148
+ "\"\"\"))"
1149
  ]
1150
  },
1151
  {
1152
  "cell_type": "code",
1153
  "execution_count": null,
1154
+ "id": "0f607250",
1155
  "metadata": {},
1156
  "outputs": [],
1157
  "source": [
 
1218
  },
1219
  {
1220
  "cell_type": "markdown",
1221
+ "id": "e6723c30",
1222
  "metadata": {},
1223
  "source": [
1224
  "**How to read the numbers.** The loss is the average \"surprise\" per token. *Perplexity* is\n",
 
1228
  },
1229
  {
1230
  "cell_type": "markdown",
1231
+ "id": "953a841e",
1232
  "metadata": {},
1233
  "source": [
1234
  "## 8. Generate stories: the use case\n",
 
1240
  {
1241
  "cell_type": "code",
1242
  "execution_count": null,
1243
+ "id": "2cc94e4e",
1244
  "metadata": {},
1245
  "outputs": [],
1246
  "source": [
 
1257
  },
1258
  {
1259
  "cell_type": "markdown",
1260
+ "id": "75329d6b",
1261
  "metadata": {},
1262
  "source": [
1263
  "## 9. Save and publish on the Hugging Face Hub\n",
 
1273
  {
1274
  "cell_type": "code",
1275
  "execution_count": null,
1276
+ "id": "f26e61dc",
1277
  "metadata": {},
1278
  "outputs": [],
1279
  "source": [
 
1314
  {
1315
  "cell_type": "code",
1316
  "execution_count": null,
1317
+ "id": "08db0245",
1318
  "metadata": {},
1319
  "outputs": [],
1320
  "source": [
 
1335
  },
1336
  {
1337
  "cell_type": "markdown",
1338
+ "id": "b91cda98",
1339
  "metadata": {},
1340
  "source": [
1341
  "## Loading it back later\n",
 
1346
  {
1347
  "cell_type": "code",
1348
  "execution_count": null,
1349
+ "id": "c8a96007",
1350
  "metadata": {},
1351
  "outputs": [],
1352
  "source": [
 
1367
  },
1368
  {
1369
  "cell_type": "markdown",
1370
+ "id": "f2036bcb",
1371
  "metadata": {},
1372
  "source": [
1373
  "## What we built, in one paragraph\n",