{ "cells": [ { "cell_type": "markdown", "id": "d6dbef8d", "metadata": {}, "source": [ "# Tiny Storyteller: building a language model from scratch\n", "\n", "**Author:** singhpratech. **Runs on:** Google Colab (free T4 GPU), or a Mac with Apple Silicon, or CPU.\n", "\n", "## Purpose\n", "Learn how a language model (an \"LLM\") is really built, by building one from nothing.\n", "No pretrained weights (dials borrowed from a model someone else trained), no fine-tuning\n", "(nudging borrowed dials), no magic. We start with raw PyTorch tensors, build the simplest\n", "possible model, then add one idea at a time until we have a small GPT-style transformer that\n", "we train ourselves. If those words mean nothing yet, good: the next section explains every\n", "one of them.\n", "\n", "## Use case\n", "A **bedtime story starter** for young kids. Give it the first few words, like\n", "*\"Once upon a time, a little monkey\"*, and it continues with a short, simple story.\n", "\n", "## Two characters, one story\n", "The whole notebook is told through **Momo**, a young monkey who is learning to tell stories,\n", "and **Ella**, an elephant who never forgets and reads to him every night. Momo is the model.\n", "Ella is the teacher and the memory. Every idea below, and every clickable picture, uses the\n", "same two characters, so nothing has to be re-learned from scratch.\n", "\n", "## The road map\n", "| Part | What we build | Why |\n", "|---|---|---|\n", "| 0 | Momo and Ella's story, a word list, a live flow diagram, then setup | Every idea and every strange word in plain language |\n", "| 1 | Tensors and gradients | The two things every neural net is made of |\n", "| 2 | Data: TinyStories | Simple English stories, made for small models |\n", "| 3 | A bigram model | The simplest language model that can \"write\" |\n", "| 4 | A real tokenizer (BPE) | Words instead of letters |\n", "| 5 | Attention, by hand | The idea that makes transformers work |\n", "| 6 | A tiny GPT | Stack the pieces into a full model |\n", "| 7 | Training | Teach it with a few minutes on a GPU |\n", "| 8 | Generate stories | Try the use case |\n", "| 9 | Publish on Hugging Face | Save the model, privately at first |\n", "| 10 | Load it back, and what we built | Use Momo anywhere |\n", "\n", "Run every cell top to bottom. Each cell is small on purpose." ] }, { "cell_type": "markdown", "id": "52bce3d0", "metadata": {}, "source": [ "## 0. Before any code: Momo, Ella, and what we are about to build\n", "\n", "*No code in this section. It is written so that a five-year-old could follow the story and a\n", "grown-up could follow the meaning. Every strange word is explained the first time it appears,\n", "and all of them are collected in the word list at the end of this section.*" ] }, { "cell_type": "markdown", "id": "18badc67", "metadata": {}, "source": [ "### Meet the characters\n", "\n", "**Momo** is a young monkey. He is quick and curious and wants to learn to tell bedtime\n", "stories. Inside Momo's head are millions of tiny **dials**. Right now every dial points\n", "somewhere random, so when Momo tries to tell a story it comes out as nonsense.\n", "\n", "**Ella** is an elephant, and elephants never forget. Every night Ella reads a story to Momo\n", "one word at a time. Before each word she stops and lets Momo guess what comes next. Then she\n", "tells him how wrong he was. Ella also keeps a shelf where she stores everything already said,\n", "so nobody has to work it out twice.\n", "\n", "In grown-up words: Momo is the **model**. His dials are the **weights**, also called\n", "**parameters**: the numbers that get adjusted during learning. Ella's stories are the\n", "**training data**. Her \"how wrong were you\" is the **loss**. Her shelf is the **KV cache**.\n", "Ella plays two roles on purpose: the teacher while Momo learns (training), and the memory\n", "while Momo writes (generation). Each of these gets its own section below." ] }, { "cell_type": "markdown", "id": "c6b124e5", "metadata": {}, "source": [ "### 0.1 What is PyTorch?\n", "\n", "Momo needs a place to keep his dials and do his sums very fast. That place is **PyTorch**,\n", "Momo's workshop. It has three things:\n", "\n", "- **Boxes for numbers**, called **tensors**. A tensor is a grid of numbers with a shape: a\n", " row of baskets, a tray of baskets, or a stack of trays. Everything in Momo's head is a\n", " tensor: the words coming in, the dials, the guesses going out.\n", "- **A very fast counting table**, the **GPU** (graphics processing unit: a chip first built\n", " for video games that can do millions of small sums at the same time). On Colab we borrow\n", " one called a T4.\n", "- **A tape recorder**, called **autograd** (automatic gradients). While Momo works it writes\n", " down every step. Afterwards it reads the tape backwards and tells Momo which way to turn\n", " every dial, and by how much. Ordinary programs do not do this, and it is the reason modern AI\n", " is practical at all. Part 1 has a game that shows exactly how the tape is read.\n", "\n", "PyTorch was built at Meta's AI lab and is behind most famous models (the GPT family, Llama,\n", "Stable Diffusion, Whisper). **Hugging Face**, where we publish at the end, is the public\n", "library where people share models built this way." ] }, { "cell_type": "markdown", "id": "a8557a5e", "metadata": {}, "source": [ "### 0.2 What is a model, really?\n", "\n", "A **model** is Momo: a big pile of dials plus a fixed recipe for how to use them. Our Momo\n", "has about 5.9 million dials. Famous big models have hundreds of billions. At the start every\n", "dial is random. **Training** means turning the dials a tiny bit at a time, thousands of\n", "times, until the recipe gives good guesses. Momo never writes down a rulebook. He tunes his\n", "instincts. The dials *are* the instincts. (He can still end up repeating sentences he has\n", "heard many times, as we will see in Part 8.)" ] }, { "cell_type": "markdown", "id": "f05878b3", "metadata": {}, "source": [ "### 0.3 What does a language model actually do?\n", "\n", "One thing: **given some words, guess the next word.** That is the whole job.\n", "\n", "Ella reads: \"Once upon a time, a little...\" Momo guesses: \"monkey!\" (or \"girl\", or \"fox\").\n", "\n", "Telling a whole story is just that guess over and over: guess a word, add it, guess the next\n", "word, add it. Chatting with an AI assistant is the same trick. Your question is the words so\n", "far, and the answer is a long run of next-word guesses.\n", "\n", "A **language model** is any machine that does this. An **LLM** is a *large* language model,\n", "one with billions of dials. Ours is tiny, but it works the same way." ] }, { "cell_type": "markdown", "id": "8774b740", "metadata": {}, "source": [ "### 0.4 What happens when you ask Momo a question?\n", "\n", "Say you ask: *\"What is the capital of France?\"* Here is the journey.\n", "\n", "**Step 1, biting: words become tokens.** Momo cannot swallow a whole sentence. He bites it\n", "into pieces: \"What\", \" is\", \" the\", \" capital\", \" of\", \" France\", \"?\". Each kind of piece\n", "has a number painted on it. A piece is called a **token**, the biter is the **tokenizer**,\n", "and the set of all pieces Momo knows is his **vocabulary** (ours has 4,096 pieces). After\n", "this step your question is a short list of numbers.\n", "\n", "**Step 2, the meaning map: each token becomes a point.** Momo keeps a big map. Every token\n", "has a spot on it, and tokens that mean similar things sit close together: \"France\" near\n", "\"Spain\", \"capital\" near \"city\". A spot on the map is written as a list of numbers (256 of\n", "them in our model). That list is called an **embedding**, and the whole map is learned\n", "during training.\n", "\n", "**Step 3, looking around: attention.** This is the heart of it. Each word looks back at all\n", "the words before it (and at itself) and asks \"which of you matter to me right now?\" The word\n", "\"capital\" looks around, finds \"France\", and pulls some of France's meaning into itself. Now\n", "\"capital\" means \"the capital of France\". This looking-around is called **attention**. It is\n", "repeated in several rounds, called **layers**. Picture a tall jungle tree that Momo climbs:\n", "each rung is one round of looking around, and the meaning gets sharper at every rung. Our\n", "Momo's tree has 6 rungs.\n", "\n", "**Step 4, the guess.** The last word's sharpened meaning is compared with every token in\n", "the vocabulary, giving each a score. The raw scores are called **logits**. They are squashed\n", "into percentages that add up to 100, and that squashing is called **softmax**. \"Paris\" gets,\n", "say, 92 percent. One token is picked.\n", "\n", "**Step 5, repeat.** \"Paris\" is added to the words so far and the whole journey runs again for\n", "the next token, until Momo picks a special \"the story is over\" token or reaches the length limit. You see words appear one by one\n", "on screen because that is literally how they are made." ] }, { "cell_type": "markdown", "id": "5a0ae3aa", "metadata": {}, "source": [ "### 0.5 How attention looks things up: the story circle\n", "\n", "Picture the jungle story circle. Every word in the sentence is an animal sitting in the\n", "circle. Each animal holds three cards:\n", "\n", "- a **query** card: \"here is what I am looking for\" (\"capital\" is looking for a country),\n", "- a **key** card: \"here is what I am\" (\"France\" says: I am a country),\n", "- a **value** card: \"here is what I will tell you if you pick me\" (France's actual meaning).\n", "\n", "Every animal holds up its query card and compares it with the key cards of the animals\n", "before it, and with its own. A good match scores high, a poor match scores low. The scores\n", "are squashed into percentages (softmax again) and the animal collects a blend of everyone's\n", "value cards, weighted by those percentages. \"capital\" might take 70 percent from \"France\",\n", "20 percent from \"of\", and crumbs from the rest. That blend becomes its new, sharper meaning.\n", "\n", "One more thing. While Ella is teaching, every animal does this at the same time. While Momo\n", "is writing, only the newest animal needs to.\n", "\n", "Two rules make this work for *writing*:\n", "\n", "- **No peeking ahead.** When Momo is guessing word 7 he may only look at words 1 to 6. In\n", " code this is a triangle-shaped **mask** that blocks out the future.\n", "- **Many heads.** The circle runs several times at once with different cards. Each run is a\n", " **head**. One head might track grammar, another names, another position. Momo has 4 heads.\n", " Big models have dozens." ] }, { "cell_type": "markdown", "id": "e786fabb", "metadata": {}, "source": [ "### 0.6 Ella's shelf: the KV cache\n", "\n", "Remember Step 5: for each new word the whole journey runs again. Done naively, writing the\n", "500th word means re-reading 499 words, and the 501st means re-reading 500. Slower and slower.\n", "\n", "But the key card and value card of a word already said **never change**. \"France\" holds the\n", "same cards whether the story is 7 words or 700 words long. So Ella, who never forgets, keeps\n", "every finished word's key and value cards on her shelf. That shelf is the **KV cache** (K for\n", "key, V for value; a cache is a place where things are kept for quick reuse). When a new word\n", "arrives only *its* cards are made. It compares its query with all the keys on the shelf,\n", "collects the values, done. Nothing old is redone.\n", "\n", "The shelf is why a long chat stays fast, and why long chats need a lot of GPU memory: every\n", "token keeps its cards there. A model's **context window** (how much text it can hold at\n", "once) is partly how big the shelf is allowed to grow.\n", "\n", "Part 5 shows the shelf in about a dozen lines of code. Two honest notes: the shelf is emptied at the\n", "start of every new story (it is a memory *within* one piece of writing, not between stories),\n", "and Momo's own `generate` in Part 6 does not use it, to keep the code short. It redoes the\n", "whole journey for every word, which is fine for 200-word stories." ] }, { "cell_type": "markdown", "id": "b4e0da60", "metadata": {}, "source": [ "### 0.7 The strange names, decoded\n", "\n", "| Word | In Momo's world | Grown-up meaning |\n", "|---|---|---|\n", "| **Model** | Momo | A pile of dials plus a recipe for using them |\n", "| **Weights / parameters** | Momo's dials | The numbers that training adjusts |\n", "| **Training data** | Ella's bedtime stories | The text the model learns from |\n", "| **Token / tokenizer / vocabulary** | A bite / the biter / all the bites Momo knows | Text pieces, the splitter, the set of pieces |\n", "| **Embedding** | A spot on Momo's meaning map | A list of numbers standing for one token |\n", "| **Attention** | Looking around the story circle | Each token blending in earlier tokens by relevance |\n", "| **Query, key, value** | The three cards each animal holds | What I seek, what I am, what I give |\n", "| **Head** | One run of the circle | One attention pattern; several run in parallel |\n", "| **Mask** | No peeking ahead | Blocks future tokens |\n", "| **Softmax** | Turning scores into shares of 100 | Raise e to the power of each score, then divide so they add up to 1 |\n", "| **Logits** | Raw scores before the squash | Unnormalised outputs of the last layer |\n", "| **KV cache** | Ella's shelf | Stored keys and values of past tokens |\n", "| **Context window** | How long Ella's shelf can get | Maximum tokens the model holds at once |\n", "| **GPT** | Momo's kind of model | Generative Pre-trained Transformer: makes text, learned from text first, built from attention |\n", "| **Pre-training / fine-tuning** | Ella's bedtime stories / a few extra lessons on one topic | Training from scratch on lots of text / continuing to train an already-trained model on a small special set |\n", "| **Transformer** | The tall tree of story-circle rungs | The 2017 design that stacks attention layers |\n", "| **Neural network** | Any pile of dials in layers | The general family transformers belong to |\n", "| **Layer / block** | One rung of the tree | One attention round plus a think-alone round |\n", "| **Feed-forward network** | Thinking alone on a twig after the circle | A small two-layer network applied per token |\n", "| **`nn.Linear`** | One tray of dials | Multiplies its input by a weight matrix (the fruit widget in Part 1) |\n", "| **LayerNorm** | Ella steadying the volume | Rescales numbers so training stays stable |\n", "| **Residual connection** | Adding thoughts instead of replacing them | Each block adds to its input |\n", "| **Loss / cross-entropy** | Ella's \"how wrong were you\" | How surprised the model was by the true next token |\n", "| **Gradient** | The slope under Momo's feet | For each dial, how the loss changes if the dial moves |\n", "| **Gradient descent** | Walking downhill one stride at a time | Repeatedly step every weight against its gradient |\n", "| **Autograd** | Momo's tape recorder | PyTorch recording every step so it can replay it backwards |\n", "| **Backpropagation** | Reading the tape backwards, the blame game | Chain rule applied from the loss back to every weight |\n", "| **Chain rule** | Multiply by how hard each step pushes | The slope through a chain of steps is the product of each step's slope |\n", "| **Optimizer / AdamW** | The rule for turning dials | Uses gradients plus a memory of recent slopes |\n", "| **Weight decay** (the W in AdamW) | Ella nudging every dial back toward the middle | Shrinks every weight a little each step so none grows wild |\n", "| **Learning rate** | Momo's stride | Step size per update |\n", "| **Warm-up / cosine decay** | Tiptoe, stride, tiptoe | Learning-rate schedule: small, then big, then small |\n", "| **Batch** | How many story pieces per lesson | Examples processed before one update |\n", "| **Validation set** | The stories Ella hides for the quiz | Held-out text the model never trains on; shows real learning, not memorising |\n", "| **Step / epoch** | One dial turn / one pass over every story | One update / one pass over the data |\n", "| **Perplexity** | How many tokens Momo is choosing between | exp(loss); 1 is perfect, vocabulary size is random |\n", "| **Temperature / top-k** | How wild Momo is when picking | Sampling knobs at generation time |\n", "| **BPE** | How Momo decided where to bite | Byte-pair encoding: merge the most common pairs repeatedly |\n", "| **GPU** | The fast counting table | Graphics chip doing millions of sums at once |\n", "| **Hugging Face Hub / Colab** | The jungle library / a borrowed workshop | Public model library / Google's free GPU notebooks |\n", "| **Access token** | Momo's library card | A secret key that lets you upload; nothing to do with text tokens |" ] }, { "cell_type": "markdown", "id": "91659f55", "metadata": {}, "source": [ "### The one-sentence version\n", "\n", "Momo bites text into tokens, places them on his meaning map, lets each word look around the\n", "story circle to sharpen its meaning (attention), guesses the next token, and Ella keeps the\n", "cards on her shelf so nothing is redone.\n", "\n", "Now let's build Momo." ] }, { "cell_type": "markdown", "id": "eabf2c36", "metadata": {}, "source": [ "## Live flow diagram\n", "\n", "Run the cell below (it only draws, it trains nothing). Press **Play** and watch Momo produce\n", "one word at a time: the text is bitten into tokens, each token gets a spot on the meaning\n", "map, the newest token looks around the story circle (the orange arcs are attention), the\n", "vocabulary is scored, a word is picked, and it joins the text. On the right, Ella's shelf\n", "(the KV cache) fills up one card per token, and only the newest token ever needs new cards.\n", "One honest note: the diagram shows the shelf because real models use it. Our own short\n", "`generate` in Part 6 skips the shelf and redoes the whole journey for every word." ] }, { "cell_type": "code", "execution_count": null, "id": "1baea7bb", "metadata": {}, "outputs": [], "source": [ "from IPython.display import HTML, display\n", "\n", "# Everything below is plain HTML + JavaScript, no libraries. It runs inside the notebook output.\n", "display(HTML(r\"\"\"