Upload tiny_llm_from_scratch.ipynb with huggingface_hub
Browse files- tiny_llm_from_scratch.ipynb +769 -0
tiny_llm_from_scratch.ipynb
ADDED
|
@@ -0,0 +1,769 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"id": "e2785f32",
|
| 6 |
+
"metadata": {},
|
| 7 |
+
"source": [
|
| 8 |
+
"# Tiny Storyteller: building a language model from scratch\n",
|
| 9 |
+
"\n",
|
| 10 |
+
"**Author:** singhpratech, with Claude. **Runs on:** Google Colab (free T4 GPU), or a Mac with Apple Silicon, or CPU.\n",
|
| 11 |
+
"\n",
|
| 12 |
+
"## Purpose\n",
|
| 13 |
+
"Learn how a language model (an \"LLM\") is really built, by building one from nothing.\n",
|
| 14 |
+
"No pretrained weights, no fine-tuning, no magic. We start with raw PyTorch tensors,\n",
|
| 15 |
+
"build the simplest possible model, then add one idea at a time until we have a small\n",
|
| 16 |
+
"GPT-style transformer that we train ourselves.\n",
|
| 17 |
+
"\n",
|
| 18 |
+
"## Use case\n",
|
| 19 |
+
"A **bedtime story starter** for young kids. Give it the first few words, like\n",
|
| 20 |
+
"*\"Once upon a time, a little fox\"*, and it continues with a short, simple story.\n",
|
| 21 |
+
"\n",
|
| 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",
|
| 28 |
+
"| 4 | A real tokenizer (BPE) | Words instead of letters |\n",
|
| 29 |
+
"| 5 | Attention, by hand | The idea that makes transformers work |\n",
|
| 30 |
+
"| 6 | A tiny GPT | Stack the pieces into a full model |\n",
|
| 31 |
+
"| 7 | Training | Teach it with a few minutes on a GPU |\n",
|
| 32 |
+
"| 8 | Generate stories | Try the use case |\n",
|
| 33 |
+
"| 9 | Publish on Hugging Face | Save and share the model |\n",
|
| 34 |
+
"\n",
|
| 35 |
+
"Run every cell top to bottom. Each cell is small on purpose."
|
| 36 |
+
]
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"cell_type": "markdown",
|
| 40 |
+
"id": "5a109db5",
|
| 41 |
+
"metadata": {},
|
| 42 |
+
"source": [
|
| 43 |
+
"## 0. Setup\n",
|
| 44 |
+
"On Colab, go to **Runtime > Change runtime type > T4 GPU** first. Then run this cell."
|
| 45 |
+
]
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"cell_type": "code",
|
| 49 |
+
"execution_count": null,
|
| 50 |
+
"id": "00b4393c",
|
| 51 |
+
"metadata": {},
|
| 52 |
+
"outputs": [],
|
| 53 |
+
"source": [
|
| 54 |
+
"%pip install -q torch transformers datasets tokenizers huggingface_hub"
|
| 55 |
+
]
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"cell_type": "code",
|
| 59 |
+
"execution_count": null,
|
| 60 |
+
"id": "e97e5f1c",
|
| 61 |
+
"metadata": {},
|
| 62 |
+
"outputs": [],
|
| 63 |
+
"source": [
|
| 64 |
+
"import math, time, json, os\n",
|
| 65 |
+
"import torch\n",
|
| 66 |
+
"import torch.nn as nn\n",
|
| 67 |
+
"import torch.nn.functional as F\n",
|
| 68 |
+
"\n",
|
| 69 |
+
"# Pick the best device available: NVIDIA GPU (Colab), Apple GPU (Mac), or CPU.\n",
|
| 70 |
+
"device = \"cuda\" if torch.cuda.is_available() else \"mps\" if torch.backends.mps.is_available() else \"cpu\"\n",
|
| 71 |
+
"torch.manual_seed(42)\n",
|
| 72 |
+
"print(\"PyTorch\", torch.__version__, \"| device:\", device)"
|
| 73 |
+
]
|
| 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": [
|
| 94 |
+
"x = torch.tensor([[1., 2., 3.],\n",
|
| 95 |
+
" [4., 5., 6.]]) # shape (2, 3): 2 rows, 3 columns\n",
|
| 96 |
+
"print(\"shape:\", x.shape)\n",
|
| 97 |
+
"print(\"x @ x.T =\\n\", x @ x.T) # matrix multiply: the core operation of every layer\n",
|
| 98 |
+
"print(\"x * 2 =\\n\", x * 2) # elementwise ops just work"
|
| 99 |
+
]
|
| 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",
|
| 107 |
+
"PyTorch computes gradients for us with *autograd*: we mark a tensor with\n",
|
| 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": [
|
| 121 |
+
"w = torch.tensor([1.0, -1.0, 0.5], requires_grad=True) # a \"weight\" we want to learn\n",
|
| 122 |
+
"loss = ((x @ w) ** 2).sum() # some loss that depends on w\n",
|
| 123 |
+
"loss.backward() # autograd fills in w.grad\n",
|
| 124 |
+
"print(\"loss:\", loss.item())\n",
|
| 125 |
+
"print(\"d(loss)/d(w):\", w.grad)\n",
|
| 126 |
+
"\n",
|
| 127 |
+
"# One step of gradient descent: move w a little bit against the gradient.\n",
|
| 128 |
+
"with torch.no_grad():\n",
|
| 129 |
+
" w -= 0.01 * w.grad\n",
|
| 130 |
+
"print(\"w after one step:\", w)"
|
| 131 |
+
]
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"cell_type": "markdown",
|
| 135 |
+
"id": "4c3d59ca",
|
| 136 |
+
"metadata": {},
|
| 137 |
+
"source": [
|
| 138 |
+
"## 2. Data: TinyStories\n",
|
| 139 |
+
"\n",
|
| 140 |
+
"[TinyStories](https://huggingface.co/datasets/roneneldan/TinyStories) is a public dataset of\n",
|
| 141 |
+
"short children's stories written in very simple English. It was created specifically to show\n",
|
| 142 |
+
"that *small* models can learn to write coherent text if the language is simple enough.\n",
|
| 143 |
+
"\n",
|
| 144 |
+
"We stream a slice of it so the download stays small."
|
| 145 |
+
]
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"cell_type": "code",
|
| 149 |
+
"execution_count": null,
|
| 150 |
+
"id": "6af6bd64",
|
| 151 |
+
"metadata": {},
|
| 152 |
+
"outputs": [],
|
| 153 |
+
"source": [
|
| 154 |
+
"from datasets import load_dataset\n",
|
| 155 |
+
"\n",
|
| 156 |
+
"N_STORIES = 20_000 # enough for a tiny model; raise it later if you want a better one\n",
|
| 157 |
+
"\n",
|
| 158 |
+
"stream = load_dataset(\"roneneldan/TinyStories\", split=\"train\", streaming=True)\n",
|
| 159 |
+
"stories = [row[\"text\"].strip() for row in stream.take(N_STORIES)]\n",
|
| 160 |
+
"\n",
|
| 161 |
+
"print(f\"{len(stories):,} stories, {sum(len(s) for s in stories)/1e6:.1f}M characters\")\n",
|
| 162 |
+
"print(\"-\" * 60)\n",
|
| 163 |
+
"print(stories[0][:500])"
|
| 164 |
+
]
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"cell_type": "markdown",
|
| 168 |
+
"id": "ea6277bf",
|
| 169 |
+
"metadata": {},
|
| 170 |
+
"source": [
|
| 171 |
+
"## 3. The simplest language model: a bigram\n",
|
| 172 |
+
"\n",
|
| 173 |
+
"A language model answers one question: **given what came before, what comes next?**\n",
|
| 174 |
+
"\n",
|
| 175 |
+
"The simplest version looks only at the *last character* and keeps a table of\n",
|
| 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": [
|
| 189 |
+
"text = \"\\n\".join(stories)\n",
|
| 190 |
+
"chars = sorted(set(text))\n",
|
| 191 |
+
"char_vocab = len(chars)\n",
|
| 192 |
+
"stoi = {c: i for i, c in enumerate(chars)} # string -> int\n",
|
| 193 |
+
"itos = {i: c for c, i in stoi.items()} # int -> string\n",
|
| 194 |
+
"encode_c = lambda s: [stoi[c] for c in s]\n",
|
| 195 |
+
"decode_c = lambda ids: \"\".join(itos[i] for i in ids)\n",
|
| 196 |
+
"\n",
|
| 197 |
+
"print(\"character vocab size:\", char_vocab)\n",
|
| 198 |
+
"print(\"encode('hello') ->\", encode_c(\"hello\"))\n",
|
| 199 |
+
"print(\"decode back ->\", decode_c(encode_c(\"hello\")))"
|
| 200 |
+
]
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"cell_type": "code",
|
| 204 |
+
"execution_count": null,
|
| 205 |
+
"id": "9990f156",
|
| 206 |
+
"metadata": {},
|
| 207 |
+
"outputs": [],
|
| 208 |
+
"source": [
|
| 209 |
+
"class Bigram(nn.Module):\n",
|
| 210 |
+
" def __init__(self, vocab):\n",
|
| 211 |
+
" super().__init__()\n",
|
| 212 |
+
" self.table = nn.Embedding(vocab, vocab) # row i = scores for \"what follows token i\"\n",
|
| 213 |
+
"\n",
|
| 214 |
+
" def forward(self, idx, targets=None):\n",
|
| 215 |
+
" logits = self.table(idx) # (B, T) -> (B, T, vocab)\n",
|
| 216 |
+
" loss = None\n",
|
| 217 |
+
" if targets is not None:\n",
|
| 218 |
+
" # cross-entropy compares predicted scores with the true next token\n",
|
| 219 |
+
" loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))\n",
|
| 220 |
+
" return logits, loss\n",
|
| 221 |
+
"\n",
|
| 222 |
+
" @torch.no_grad()\n",
|
| 223 |
+
" def generate(self, idx, max_new_tokens):\n",
|
| 224 |
+
" for _ in range(max_new_tokens):\n",
|
| 225 |
+
" logits, _ = self(idx[:, -1:]) # only the last token matters\n",
|
| 226 |
+
" probs = F.softmax(logits[:, -1, :], dim=-1)\n",
|
| 227 |
+
" idx = torch.cat([idx, torch.multinomial(probs, 1)], dim=1)\n",
|
| 228 |
+
" return idx\n",
|
| 229 |
+
"\n",
|
| 230 |
+
"char_data = torch.tensor(encode_c(text[:2_000_000]), dtype=torch.long)\n",
|
| 231 |
+
"\n",
|
| 232 |
+
"def get_char_batch(batch_size=64, block_size=8):\n",
|
| 233 |
+
" ix = torch.randint(len(char_data) - block_size - 1, (batch_size,))\n",
|
| 234 |
+
" x = torch.stack([char_data[i:i+block_size] for i in ix])\n",
|
| 235 |
+
" y = torch.stack([char_data[i+1:i+block_size+1] for i in ix]) # the \"next character\" of each position\n",
|
| 236 |
+
" return x.to(device), y.to(device)\n",
|
| 237 |
+
"\n",
|
| 238 |
+
"bigram = Bigram(char_vocab).to(device)\n",
|
| 239 |
+
"opt = torch.optim.AdamW(bigram.parameters(), lr=1e-2)\n",
|
| 240 |
+
"\n",
|
| 241 |
+
"for step in range(2000):\n",
|
| 242 |
+
" xb, yb = get_char_batch()\n",
|
| 243 |
+
" _, loss = bigram(xb, yb)\n",
|
| 244 |
+
" opt.zero_grad(); loss.backward(); opt.step()\n",
|
| 245 |
+
" if step % 500 == 0:\n",
|
| 246 |
+
" print(f\"step {step:4d} loss {loss.item():.3f}\")\n",
|
| 247 |
+
"\n",
|
| 248 |
+
"start = torch.tensor([encode_c(\"T\")], device=device)\n",
|
| 249 |
+
"print(\"\\nBigram writes:\\n\", decode_c(bigram.generate(start, 300)[0].tolist()))"
|
| 250 |
+
]
|
| 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",
|
| 258 |
+
"but it is gibberish. It only sees one character back. To write a story, the model needs\n",
|
| 259 |
+
"two upgrades: **better tokens** and a way to **look further back**. That is the rest of the notebook."
|
| 260 |
+
]
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"cell_type": "markdown",
|
| 264 |
+
"id": "763829c4",
|
| 265 |
+
"metadata": {},
|
| 266 |
+
"source": [
|
| 267 |
+
"## 4. A real tokenizer: byte-pair encoding (BPE)\n",
|
| 268 |
+
"\n",
|
| 269 |
+
"Characters are too small; the model wastes effort learning to spell. Whole words are too\n",
|
| 270 |
+
"many (and unknown words break). BPE is the middle ground: it starts from characters and\n",
|
| 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": [
|
| 284 |
+
"from tokenizers import ByteLevelBPETokenizer\n",
|
| 285 |
+
"from transformers import PreTrainedTokenizerFast\n",
|
| 286 |
+
"\n",
|
| 287 |
+
"VOCAB_SIZE = 4096\n",
|
| 288 |
+
"EOS = \"<|endoftext|>\" # special token that marks where one story ends\n",
|
| 289 |
+
"\n",
|
| 290 |
+
"bpe = ByteLevelBPETokenizer()\n",
|
| 291 |
+
"bpe.train_from_iterator(stories, vocab_size=VOCAB_SIZE, min_frequency=2, special_tokens=[EOS])\n",
|
| 292 |
+
"\n",
|
| 293 |
+
"tokenizer = PreTrainedTokenizerFast(tokenizer_object=bpe, bos_token=EOS, eos_token=EOS, pad_token=EOS)\n",
|
| 294 |
+
"vocab_size = len(tokenizer)\n",
|
| 295 |
+
"\n",
|
| 296 |
+
"sample = \"Once upon a time, a little fox found a shiny red ball.\"\n",
|
| 297 |
+
"ids = tokenizer(sample)[\"input_ids\"]\n",
|
| 298 |
+
"print(\"vocab size:\", vocab_size)\n",
|
| 299 |
+
"print(f\"{len(sample)} characters -> {len(ids)} tokens\")\n",
|
| 300 |
+
"print(tokenizer.convert_ids_to_tokens(ids)) # 'Ġ' means 'there is a space before this piece'"
|
| 301 |
+
]
|
| 302 |
+
},
|
| 303 |
+
{
|
| 304 |
+
"cell_type": "code",
|
| 305 |
+
"execution_count": null,
|
| 306 |
+
"id": "23cb929f",
|
| 307 |
+
"metadata": {},
|
| 308 |
+
"outputs": [],
|
| 309 |
+
"source": [
|
| 310 |
+
"# Turn all stories into one long stream of token ids, with EOS between stories.\n",
|
| 311 |
+
"ids = tokenizer(EOS.join(stories) + EOS)[\"input_ids\"]\n",
|
| 312 |
+
"data = torch.tensor(ids, dtype=torch.long)\n",
|
| 313 |
+
"n = int(0.99 * len(data))\n",
|
| 314 |
+
"train_data, val_data = data[:n], data[n:]\n",
|
| 315 |
+
"print(f\"total tokens: {len(data)/1e6:.2f}M | train {len(train_data)/1e6:.2f}M | val {len(val_data)/1e3:.0f}K\")"
|
| 316 |
+
]
|
| 317 |
+
},
|
| 318 |
+
{
|
| 319 |
+
"cell_type": "markdown",
|
| 320 |
+
"id": "d2e9b068",
|
| 321 |
+
"metadata": {},
|
| 322 |
+
"source": [
|
| 323 |
+
"## 5. Attention, by hand\n",
|
| 324 |
+
"\n",
|
| 325 |
+
"This is the idea that made modern LLMs possible. Each token gets to **look at the tokens\n",
|
| 326 |
+
"before it** and decide how much to pay attention to each one.\n",
|
| 327 |
+
"\n",
|
| 328 |
+
"Every token produces three vectors:\n",
|
| 329 |
+
"- a **query** (\"what am I looking for?\"),\n",
|
| 330 |
+
"- a **key** (\"what do I contain?\"),\n",
|
| 331 |
+
"- a **value** (\"what do I pass on if someone attends to me?\").\n",
|
| 332 |
+
"\n",
|
| 333 |
+
"A token's output is a weighted average of the *values* of earlier tokens, where the weights\n",
|
| 334 |
+
"come from how well its *query* matches their *keys*. The weights are made with a softmax,\n",
|
| 335 |
+
"and a triangular mask stops tokens from peeking at the future (we are predicting it!)."
|
| 336 |
+
]
|
| 337 |
+
},
|
| 338 |
+
{
|
| 339 |
+
"cell_type": "code",
|
| 340 |
+
"execution_count": null,
|
| 341 |
+
"id": "c68c6f5c",
|
| 342 |
+
"metadata": {},
|
| 343 |
+
"outputs": [],
|
| 344 |
+
"source": [
|
| 345 |
+
"B, T, C = 1, 4, 8 # batch, time (tokens), channels (features)\n",
|
| 346 |
+
"x_demo = torch.randn(B, T, C)\n",
|
| 347 |
+
"head = 4\n",
|
| 348 |
+
"q = nn.Linear(C, head, bias=False)(x_demo)\n",
|
| 349 |
+
"k = nn.Linear(C, head, bias=False)(x_demo)\n",
|
| 350 |
+
"v = nn.Linear(C, head, bias=False)(x_demo)\n",
|
| 351 |
+
"\n",
|
| 352 |
+
"scores = q @ k.transpose(-2, -1) / math.sqrt(head) # (B, T, T): how much token i matches token j\n",
|
| 353 |
+
"mask = torch.tril(torch.ones(T, T)) # 1 = allowed to look, 0 = future\n",
|
| 354 |
+
"scores = scores.masked_fill(mask == 0, float(\"-inf\")) # future positions get -inf -> 0 after softmax\n",
|
| 355 |
+
"weights = F.softmax(scores, dim=-1)\n",
|
| 356 |
+
"out = weights @ v # (B, T, head)\n",
|
| 357 |
+
"\n",
|
| 358 |
+
"torch.set_printoptions(precision=2, sci_mode=False)\n",
|
| 359 |
+
"print(\"attention weights (row = token, column = who it looks at):\\n\", weights[0])\n",
|
| 360 |
+
"print(\"\\nRow 1 only looks at tokens 0-1, row 3 can see all four. Each row sums to 1.\")"
|
| 361 |
+
]
|
| 362 |
+
},
|
| 363 |
+
{
|
| 364 |
+
"cell_type": "markdown",
|
| 365 |
+
"id": "0da003d3",
|
| 366 |
+
"metadata": {},
|
| 367 |
+
"source": [
|
| 368 |
+
"## 6. A tiny GPT\n",
|
| 369 |
+
"\n",
|
| 370 |
+
"Now the full model. It is the same attention from above, used many times:\n",
|
| 371 |
+
"\n",
|
| 372 |
+
"1. **Token embedding**: each token id becomes a vector of `n_embd` numbers.\n",
|
| 373 |
+
"2. **Position embedding**: each position (0, 1, 2, ...) also gets a vector, so the model knows word order.\n",
|
| 374 |
+
"3. **Blocks** (repeated `n_layer` times). Each block has:\n",
|
| 375 |
+
" - multi-head attention (several attention \"heads\" in parallel, each looking for different things),\n",
|
| 376 |
+
" - a feed-forward network (a small MLP applied to every token on its own),\n",
|
| 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": [
|
| 390 |
+
"class Config:\n",
|
| 391 |
+
" vocab_size = vocab_size\n",
|
| 392 |
+
" block_size = 256 # how many tokens the model can see at once\n",
|
| 393 |
+
" n_embd = 256 # size of each token's vector\n",
|
| 394 |
+
" n_head = 4 # attention heads per block\n",
|
| 395 |
+
" n_layer = 6 # number of blocks\n",
|
| 396 |
+
" dropout = 0.0 # tiny model + lots of data -> no dropout needed\n",
|
| 397 |
+
"\n",
|
| 398 |
+
"cfg = Config()\n",
|
| 399 |
+
"\n",
|
| 400 |
+
"class CausalSelfAttention(nn.Module):\n",
|
| 401 |
+
" def __init__(self, cfg):\n",
|
| 402 |
+
" super().__init__()\n",
|
| 403 |
+
" self.n_head = cfg.n_head\n",
|
| 404 |
+
" self.qkv = nn.Linear(cfg.n_embd, 3 * cfg.n_embd) # query, key, value for all heads at once\n",
|
| 405 |
+
" self.proj = nn.Linear(cfg.n_embd, cfg.n_embd)\n",
|
| 406 |
+
" self.drop = nn.Dropout(cfg.dropout)\n",
|
| 407 |
+
"\n",
|
| 408 |
+
" def forward(self, x):\n",
|
| 409 |
+
" B, T, C = x.shape\n",
|
| 410 |
+
" q, k, v = self.qkv(x).split(C, dim=2)\n",
|
| 411 |
+
" # reshape so each head works on its own slice: (B, n_head, T, head_size)\n",
|
| 412 |
+
" q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)\n",
|
| 413 |
+
" k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)\n",
|
| 414 |
+
" v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)\n",
|
| 415 |
+
" # exactly the masked softmax(QK^T/sqrt(d))V from Part 5, done by a fast built-in kernel\n",
|
| 416 |
+
" y = F.scaled_dot_product_attention(q, k, v, is_causal=True)\n",
|
| 417 |
+
" y = y.transpose(1, 2).contiguous().view(B, T, C) # merge the heads back together\n",
|
| 418 |
+
" return self.drop(self.proj(y))\n",
|
| 419 |
+
"\n",
|
| 420 |
+
"class FeedForward(nn.Module):\n",
|
| 421 |
+
" def __init__(self, cfg):\n",
|
| 422 |
+
" super().__init__()\n",
|
| 423 |
+
" self.net = nn.Sequential(\n",
|
| 424 |
+
" nn.Linear(cfg.n_embd, 4 * cfg.n_embd),\n",
|
| 425 |
+
" nn.GELU(),\n",
|
| 426 |
+
" nn.Linear(4 * cfg.n_embd, cfg.n_embd),\n",
|
| 427 |
+
" nn.Dropout(cfg.dropout),\n",
|
| 428 |
+
" )\n",
|
| 429 |
+
" def forward(self, x):\n",
|
| 430 |
+
" return self.net(x)\n",
|
| 431 |
+
"\n",
|
| 432 |
+
"class Block(nn.Module):\n",
|
| 433 |
+
" def __init__(self, cfg):\n",
|
| 434 |
+
" super().__init__()\n",
|
| 435 |
+
" self.ln1 = nn.LayerNorm(cfg.n_embd)\n",
|
| 436 |
+
" self.attn = CausalSelfAttention(cfg)\n",
|
| 437 |
+
" self.ln2 = nn.LayerNorm(cfg.n_embd)\n",
|
| 438 |
+
" self.ff = FeedForward(cfg)\n",
|
| 439 |
+
" def forward(self, x):\n",
|
| 440 |
+
" x = x + self.attn(self.ln1(x)) # \"x +\" is the residual connection\n",
|
| 441 |
+
" x = x + self.ff(self.ln2(x))\n",
|
| 442 |
+
" return x\n",
|
| 443 |
+
"\n",
|
| 444 |
+
"class GPT(nn.Module):\n",
|
| 445 |
+
" def __init__(self, cfg):\n",
|
| 446 |
+
" super().__init__()\n",
|
| 447 |
+
" self.cfg = cfg\n",
|
| 448 |
+
" self.tok_emb = nn.Embedding(cfg.vocab_size, cfg.n_embd)\n",
|
| 449 |
+
" self.pos_emb = nn.Embedding(cfg.block_size, cfg.n_embd)\n",
|
| 450 |
+
" self.blocks = nn.Sequential(*[Block(cfg) for _ in range(cfg.n_layer)])\n",
|
| 451 |
+
" self.ln_f = nn.LayerNorm(cfg.n_embd)\n",
|
| 452 |
+
" self.head = nn.Linear(cfg.n_embd, cfg.vocab_size, bias=False)\n",
|
| 453 |
+
" self.head.weight = self.tok_emb.weight # weight tying: same matrix in and out, saves parameters\n",
|
| 454 |
+
" self.apply(self._init)\n",
|
| 455 |
+
"\n",
|
| 456 |
+
" def _init(self, m):\n",
|
| 457 |
+
" if isinstance(m, (nn.Linear, nn.Embedding)):\n",
|
| 458 |
+
" nn.init.normal_(m.weight, mean=0.0, std=0.02)\n",
|
| 459 |
+
" if isinstance(m, nn.Linear) and m.bias is not None:\n",
|
| 460 |
+
" nn.init.zeros_(m.bias)\n",
|
| 461 |
+
"\n",
|
| 462 |
+
" def forward(self, idx, targets=None):\n",
|
| 463 |
+
" B, T = idx.shape\n",
|
| 464 |
+
" pos = torch.arange(T, device=idx.device)\n",
|
| 465 |
+
" x = self.tok_emb(idx) + self.pos_emb(pos) # (B, T, n_embd)\n",
|
| 466 |
+
" x = self.ln_f(self.blocks(x))\n",
|
| 467 |
+
" logits = self.head(x) # (B, T, vocab_size)\n",
|
| 468 |
+
" loss = None\n",
|
| 469 |
+
" if targets is not None:\n",
|
| 470 |
+
" loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))\n",
|
| 471 |
+
" return logits, loss\n",
|
| 472 |
+
"\n",
|
| 473 |
+
" @torch.no_grad()\n",
|
| 474 |
+
" def generate(self, idx, max_new_tokens, temperature=0.8, top_k=40):\n",
|
| 475 |
+
" for _ in range(max_new_tokens):\n",
|
| 476 |
+
" idx_cond = idx[:, -self.cfg.block_size:] # never feed more than block_size tokens\n",
|
| 477 |
+
" logits, _ = self(idx_cond)\n",
|
| 478 |
+
" logits = logits[:, -1, :] / temperature # lower temperature = safer, more boring\n",
|
| 479 |
+
" if top_k is not None:\n",
|
| 480 |
+
" v, _ = torch.topk(logits, top_k)\n",
|
| 481 |
+
" logits[logits < v[:, [-1]]] = float(\"-inf\") # keep only the k most likely tokens\n",
|
| 482 |
+
" probs = F.softmax(logits, dim=-1)\n",
|
| 483 |
+
" next_id = torch.multinomial(probs, 1)\n",
|
| 484 |
+
" idx = torch.cat([idx, next_id], dim=1)\n",
|
| 485 |
+
" if next_id.item() == tokenizer.eos_token_id: # the model decided the story is over\n",
|
| 486 |
+
" break\n",
|
| 487 |
+
" return idx\n",
|
| 488 |
+
"\n",
|
| 489 |
+
"model = GPT(cfg).to(device)\n",
|
| 490 |
+
"n_params = sum(p.numel() for p in model.parameters())\n",
|
| 491 |
+
"print(f\"GPT with {cfg.n_layer} blocks, {cfg.n_head} heads, {cfg.n_embd}-dim vectors -> {n_params/1e6:.1f}M parameters\")\n",
|
| 492 |
+
"print(\"(for scale: GPT-2 small is 124M, Llama-3 8B is 8,000M)\")"
|
| 493 |
+
]
|
| 494 |
+
},
|
| 495 |
+
{
|
| 496 |
+
"cell_type": "markdown",
|
| 497 |
+
"id": "9c446a37",
|
| 498 |
+
"metadata": {},
|
| 499 |
+
"source": [
|
| 500 |
+
"## 7. Training\n",
|
| 501 |
+
"\n",
|
| 502 |
+
"The loop is the same one we used for the bigram, just bigger:\n",
|
| 503 |
+
"1. grab a random batch of token windows, and the same windows shifted by one (the targets),\n",
|
| 504 |
+
"2. run the model, get the loss,\n",
|
| 505 |
+
"3. backward, step the optimizer.\n",
|
| 506 |
+
"\n",
|
| 507 |
+
"Two small extras that every real training run uses:\n",
|
| 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": [
|
| 521 |
+
"BATCH_SIZE = 32\n",
|
| 522 |
+
"MAX_STEPS = 1500\n",
|
| 523 |
+
"EVAL_EVERY = 250\n",
|
| 524 |
+
"LR = 1e-3\n",
|
| 525 |
+
"WARMUP = 100\n",
|
| 526 |
+
"\n",
|
| 527 |
+
"def get_batch(split):\n",
|
| 528 |
+
" d = train_data if split == \"train\" else val_data\n",
|
| 529 |
+
" ix = torch.randint(len(d) - cfg.block_size - 1, (BATCH_SIZE,))\n",
|
| 530 |
+
" x = torch.stack([d[i:i+cfg.block_size] for i in ix])\n",
|
| 531 |
+
" y = torch.stack([d[i+1:i+cfg.block_size+1] for i in ix])\n",
|
| 532 |
+
" return x.to(device), y.to(device)\n",
|
| 533 |
+
"\n",
|
| 534 |
+
"@torch.no_grad()\n",
|
| 535 |
+
"def estimate_loss(iters=20):\n",
|
| 536 |
+
" model.eval()\n",
|
| 537 |
+
" out = {}\n",
|
| 538 |
+
" for split in (\"train\", \"val\"):\n",
|
| 539 |
+
" losses = torch.zeros(iters)\n",
|
| 540 |
+
" for i in range(iters):\n",
|
| 541 |
+
" xb, yb = get_batch(split)\n",
|
| 542 |
+
" _, loss = model(xb, yb)\n",
|
| 543 |
+
" losses[i] = loss.item()\n",
|
| 544 |
+
" out[split] = losses.mean().item()\n",
|
| 545 |
+
" model.train()\n",
|
| 546 |
+
" return out\n",
|
| 547 |
+
"\n",
|
| 548 |
+
"def lr_at(step):\n",
|
| 549 |
+
" if step < WARMUP:\n",
|
| 550 |
+
" return LR * (step + 1) / WARMUP\n",
|
| 551 |
+
" progress = (step - WARMUP) / max(1, MAX_STEPS - WARMUP)\n",
|
| 552 |
+
" return 0.1 * LR + 0.9 * LR * 0.5 * (1 + math.cos(math.pi * progress))\n",
|
| 553 |
+
"\n",
|
| 554 |
+
"optimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=0.1, betas=(0.9, 0.95))\n",
|
| 555 |
+
"use_amp = device == \"cuda\"\n",
|
| 556 |
+
"scaler = torch.amp.GradScaler(enabled=use_amp)\n",
|
| 557 |
+
"\n",
|
| 558 |
+
"history = []\n",
|
| 559 |
+
"t0 = time.time()\n",
|
| 560 |
+
"model.train()\n",
|
| 561 |
+
"for step in range(MAX_STEPS + 1):\n",
|
| 562 |
+
" for g in optimizer.param_groups:\n",
|
| 563 |
+
" g[\"lr\"] = lr_at(step)\n",
|
| 564 |
+
" if step % EVAL_EVERY == 0 or step == MAX_STEPS:\n",
|
| 565 |
+
" losses = estimate_loss()\n",
|
| 566 |
+
" history.append((step, losses[\"train\"], losses[\"val\"]))\n",
|
| 567 |
+
" print(f\"step {step:5d} | train loss {losses['train']:.3f} | val loss {losses['val']:.3f} \"\n",
|
| 568 |
+
" f\"| perplexity {math.exp(losses['val']):6.1f} | {(time.time()-t0)/60:4.1f} min\")\n",
|
| 569 |
+
" if step == MAX_STEPS:\n",
|
| 570 |
+
" break\n",
|
| 571 |
+
" xb, yb = get_batch(\"train\")\n",
|
| 572 |
+
" with torch.autocast(device_type=\"cuda\", dtype=torch.float16, enabled=use_amp):\n",
|
| 573 |
+
" _, loss = model(xb, yb)\n",
|
| 574 |
+
" optimizer.zero_grad(set_to_none=True)\n",
|
| 575 |
+
" scaler.scale(loss).backward()\n",
|
| 576 |
+
" scaler.unscale_(optimizer)\n",
|
| 577 |
+
" torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # stops rare huge gradients from wrecking training\n",
|
| 578 |
+
" scaler.step(optimizer)\n",
|
| 579 |
+
" scaler.update()"
|
| 580 |
+
]
|
| 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",
|
| 588 |
+
"`exp(loss)`: roughly, \"between how many tokens is the model guessing\". A random guess over\n",
|
| 589 |
+
"4,096 tokens would be perplexity 4,096. Anything under 10 on this data means real sentences."
|
| 590 |
+
]
|
| 591 |
+
},
|
| 592 |
+
{
|
| 593 |
+
"cell_type": "markdown",
|
| 594 |
+
"id": "284708da",
|
| 595 |
+
"metadata": {},
|
| 596 |
+
"source": [
|
| 597 |
+
"## 8. Generate stories: the use case\n",
|
| 598 |
+
"\n",
|
| 599 |
+
"Give the model the first few words of a story and let it continue.\n",
|
| 600 |
+
"Run it a few times; each run is a fresh story. Try your own prompts."
|
| 601 |
+
]
|
| 602 |
+
},
|
| 603 |
+
{
|
| 604 |
+
"cell_type": "code",
|
| 605 |
+
"execution_count": null,
|
| 606 |
+
"id": "ce19e9d9",
|
| 607 |
+
"metadata": {},
|
| 608 |
+
"outputs": [],
|
| 609 |
+
"source": [
|
| 610 |
+
"def tell_story(prompt, max_new_tokens=200, temperature=0.8):\n",
|
| 611 |
+
" model.eval()\n",
|
| 612 |
+
" idx = torch.tensor([tokenizer(prompt)[\"input_ids\"]], device=device)\n",
|
| 613 |
+
" out = model.generate(idx, max_new_tokens, temperature=temperature)\n",
|
| 614 |
+
" return tokenizer.decode(out[0].tolist(), skip_special_tokens=True)\n",
|
| 615 |
+
"\n",
|
| 616 |
+
"for prompt in [\"Once upon a time, a little fox\", \"Lily had a red balloon.\", \"One day, Tom went to the park\"]:\n",
|
| 617 |
+
" print(tell_story(prompt))\n",
|
| 618 |
+
" print(\"-\" * 60)"
|
| 619 |
+
]
|
| 620 |
+
},
|
| 621 |
+
{
|
| 622 |
+
"cell_type": "markdown",
|
| 623 |
+
"id": "a3a6cfad",
|
| 624 |
+
"metadata": {},
|
| 625 |
+
"source": [
|
| 626 |
+
"## 9. Save and publish on the Hugging Face Hub\n",
|
| 627 |
+
"\n",
|
| 628 |
+
"A model is just its **weights** (the tensors we trained), its **config** (the shape), and its\n",
|
| 629 |
+
"**tokenizer**. We save all three to a folder, write a short model card, and upload the folder\n",
|
| 630 |
+
"as a repo under your account.\n",
|
| 631 |
+
"\n",
|
| 632 |
+
"You need a Hugging Face token with *write* permission (Settings > Access Tokens).\n",
|
| 633 |
+
"On Colab, `login()` will ask for it. Locally, put `HF_TOKEN=...` in your environment."
|
| 634 |
+
]
|
| 635 |
+
},
|
| 636 |
+
{
|
| 637 |
+
"cell_type": "code",
|
| 638 |
+
"execution_count": null,
|
| 639 |
+
"id": "e5fc0838",
|
| 640 |
+
"metadata": {},
|
| 641 |
+
"outputs": [],
|
| 642 |
+
"source": [
|
| 643 |
+
"REPO_NAME = \"tiny-storyteller\" # the model will live at huggingface.co/<you>/tiny-storyteller\n",
|
| 644 |
+
"PUSH_TO_HUB = True # set False to only save locally\n",
|
| 645 |
+
"\n",
|
| 646 |
+
"save_dir = REPO_NAME\n",
|
| 647 |
+
"os.makedirs(save_dir, exist_ok=True)\n",
|
| 648 |
+
"torch.save(model.state_dict(), f\"{save_dir}/model.pt\")\n",
|
| 649 |
+
"json.dump({k: getattr(cfg, k) for k in [\"vocab_size\", \"block_size\", \"n_embd\", \"n_head\", \"n_layer\", \"dropout\"]},\n",
|
| 650 |
+
" open(f\"{save_dir}/config.json\", \"w\"), indent=2)\n",
|
| 651 |
+
"tokenizer.save_pretrained(save_dir)\n",
|
| 652 |
+
"\n",
|
| 653 |
+
"final_val = history[-1][2]\n",
|
| 654 |
+
"card = f'''---\n",
|
| 655 |
+
"license: mit\n",
|
| 656 |
+
"language: en\n",
|
| 657 |
+
"datasets: [roneneldan/TinyStories]\n",
|
| 658 |
+
"tags: [tiny, from-scratch, pytorch, educational, text-generation]\n",
|
| 659 |
+
"---\n",
|
| 660 |
+
"# Tiny Storyteller\n",
|
| 661 |
+
"\n",
|
| 662 |
+
"A {n_params/1e6:.1f}M-parameter GPT-style language model trained **from scratch** (random init, no pretrained weights)\n",
|
| 663 |
+
"on {N_STORIES:,} TinyStories, as a learning project. Use case: a bedtime story starter for young kids.\n",
|
| 664 |
+
"\n",
|
| 665 |
+
"- Architecture: {cfg.n_layer} transformer blocks, {cfg.n_head} heads, {cfg.n_embd}-dim embeddings, {cfg.block_size}-token context\n",
|
| 666 |
+
"- Tokenizer: byte-level BPE, {vocab_size:,} tokens, trained on the same data\n",
|
| 667 |
+
"- Training: {MAX_STEPS} steps, batch {BATCH_SIZE}, AdamW, cosine schedule\n",
|
| 668 |
+
"- Final validation loss {final_val:.3f} (perplexity {math.exp(final_val):.1f})\n",
|
| 669 |
+
"\n",
|
| 670 |
+
"The full build, explained step by step, is in `tiny_llm_from_scratch.ipynb` in this repo.\n",
|
| 671 |
+
"Load with plain PyTorch: `GPT(cfg).load_state_dict(torch.load(\"model.pt\"))`.\n",
|
| 672 |
+
"'''\n",
|
| 673 |
+
"open(f\"{save_dir}/README.md\", \"w\").write(card)\n",
|
| 674 |
+
"print(\"saved:\", sorted(os.listdir(save_dir)))"
|
| 675 |
+
]
|
| 676 |
+
},
|
| 677 |
+
{
|
| 678 |
+
"cell_type": "code",
|
| 679 |
+
"execution_count": null,
|
| 680 |
+
"id": "7e1ae430",
|
| 681 |
+
"metadata": {},
|
| 682 |
+
"outputs": [],
|
| 683 |
+
"source": [
|
| 684 |
+
"if PUSH_TO_HUB:\n",
|
| 685 |
+
" from huggingface_hub import HfApi, login, whoami\n",
|
| 686 |
+
" if not os.environ.get(\"HF_TOKEN\"):\n",
|
| 687 |
+
" login() # on Colab this opens a token prompt\n",
|
| 688 |
+
" api = HfApi()\n",
|
| 689 |
+
" user = whoami()[\"name\"]\n",
|
| 690 |
+
" repo_id = f\"{user}/{REPO_NAME}\"\n",
|
| 691 |
+
" api.create_repo(repo_id, repo_type=\"model\", private=True, exist_ok=True) # private by default; flip to False to share publicly\n",
|
| 692 |
+
" api.upload_folder(folder_path=save_dir, repo_id=repo_id, repo_type=\"model\")\n",
|
| 693 |
+
" nb = \"tiny_llm_from_scratch.ipynb\"\n",
|
| 694 |
+
" if os.path.exists(nb):\n",
|
| 695 |
+
" api.upload_file(path_or_fileobj=nb, path_in_repo=nb, repo_id=repo_id)\n",
|
| 696 |
+
" print(\"published at https://huggingface.co/\" + repo_id)"
|
| 697 |
+
]
|
| 698 |
+
},
|
| 699 |
+
{
|
| 700 |
+
"cell_type": "markdown",
|
| 701 |
+
"id": "34f8cabf",
|
| 702 |
+
"metadata": {},
|
| 703 |
+
"source": [
|
| 704 |
+
"## Loading it back later\n",
|
| 705 |
+
"Anyone (including you, on another machine) can rebuild the model from the Hub with the\n",
|
| 706 |
+
"`GPT` class from Part 6 and these few lines:"
|
| 707 |
+
]
|
| 708 |
+
},
|
| 709 |
+
{
|
| 710 |
+
"cell_type": "code",
|
| 711 |
+
"execution_count": null,
|
| 712 |
+
"id": "c5e86850",
|
| 713 |
+
"metadata": {},
|
| 714 |
+
"outputs": [],
|
| 715 |
+
"source": [
|
| 716 |
+
"from huggingface_hub import hf_hub_download\n",
|
| 717 |
+
"\n",
|
| 718 |
+
"def load_from_hub(repo_id):\n",
|
| 719 |
+
" c = json.load(open(hf_hub_download(repo_id, \"config.json\")))\n",
|
| 720 |
+
" class C: pass\n",
|
| 721 |
+
" for k, v in c.items(): setattr(C, k, v)\n",
|
| 722 |
+
" m = GPT(C()).to(device)\n",
|
| 723 |
+
" m.load_state_dict(torch.load(hf_hub_download(repo_id, \"model.pt\"), map_location=device))\n",
|
| 724 |
+
" return m.eval()\n",
|
| 725 |
+
"\n",
|
| 726 |
+
"# Example (uncomment after publishing):\n",
|
| 727 |
+
"# model = load_from_hub(\"singhpratech/tiny-storyteller\")\n",
|
| 728 |
+
"# print(tell_story(\"Once upon a time, a little fox\"))"
|
| 729 |
+
]
|
| 730 |
+
},
|
| 731 |
+
{
|
| 732 |
+
"cell_type": "markdown",
|
| 733 |
+
"id": "c0f44cd1",
|
| 734 |
+
"metadata": {},
|
| 735 |
+
"source": [
|
| 736 |
+
"## What we built, in one paragraph\n",
|
| 737 |
+
"Text became numbers through a tokenizer we trained ourselves. A tiny transformer turned\n",
|
| 738 |
+
"those numbers into vectors, let each token attend to the ones before it, and predicted the\n",
|
| 739 |
+
"next token. We measured how wrong it was with cross-entropy, used autograd to get gradients,\n",
|
| 740 |
+
"and nudged every weight with AdamW, thousands of times. That is the entire recipe. Big\n",
|
| 741 |
+
"models use the same recipe with more data, more layers, and more GPUs.\n",
|
| 742 |
+
"\n",
|
| 743 |
+
"## Ideas to try next\n",
|
| 744 |
+
"- Raise `N_STORIES` and `MAX_STEPS` for a noticeably better storyteller.\n",
|
| 745 |
+
"- Change `n_layer` or `n_embd` and watch how parameters, speed and loss trade off.\n",
|
| 746 |
+
"- Lower `temperature` for safer stories, raise it for stranger ones.\n",
|
| 747 |
+
"- Replace TinyStories with your own text (recipes, poems, your journal) and the same code\n",
|
| 748 |
+
" learns to write in that style."
|
| 749 |
+
]
|
| 750 |
+
}
|
| 751 |
+
],
|
| 752 |
+
"metadata": {
|
| 753 |
+
"accelerator": "GPU",
|
| 754 |
+
"colab": {
|
| 755 |
+
"gpuType": "T4",
|
| 756 |
+
"provenance": []
|
| 757 |
+
},
|
| 758 |
+
"kernelspec": {
|
| 759 |
+
"display_name": "Python 3",
|
| 760 |
+
"language": "python",
|
| 761 |
+
"name": "python3"
|
| 762 |
+
},
|
| 763 |
+
"language_info": {
|
| 764 |
+
"name": "python"
|
| 765 |
+
}
|
| 766 |
+
},
|
| 767 |
+
"nbformat": 4,
|
| 768 |
+
"nbformat_minor": 5
|
| 769 |
+
}
|