Sebastian Raschka 3f93d73d6d
Alt weight loading code via PyTorch (#585)
* Alt weight loading code via PyTorch

* commit additional files
2025-03-27 20:10:23 -05:00

2603 lines
136 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"id": "45398736-7e89-4263-89c8-92153baff553",
"metadata": {},
"source": [
"<table style=\"width:100%\">\n",
"<tr>\n",
"<td style=\"vertical-align:middle; text-align:left;\">\n",
"<font size=\"2\">\n",
"Supplementary code for the <a href=\"http://mng.bz/orYv\">Build a Large Language Model From Scratch</a> book by <a href=\"https://sebastianraschka.com\">Sebastian Raschka</a><br>\n",
"<br>Code repository: <a href=\"https://github.com/rasbt/LLMs-from-scratch\">https://github.com/rasbt/LLMs-from-scratch</a>\n",
"</font>\n",
"</td>\n",
"<td style=\"vertical-align:middle; text-align:left;\">\n",
"<a href=\"http://mng.bz/orYv\"><img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/cover-small.webp\" width=\"100px\"></a>\n",
"</td>\n",
"</tr>\n",
"</table>"
]
},
{
"cell_type": "markdown",
"id": "66dd524e-864c-4012-b0a2-ccfc56e80024",
"metadata": {
"id": "66dd524e-864c-4012-b0a2-ccfc56e80024"
},
"source": [
"# Chapter 5: Pretraining on Unlabeled Data"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "92b989e9-da36-4159-b212-799184764dd9",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"matplotlib version: 3.10.1\n",
"numpy version: 2.0.2\n",
"tiktoken version: 0.9.0\n",
"torch version: 2.6.0\n",
"tensorflow version: 2.18.0\n"
]
}
],
"source": [
"from importlib.metadata import version\n",
"\n",
"pkgs = [\"matplotlib\", \n",
" \"numpy\", \n",
" \"tiktoken\", \n",
" \"torch\",\n",
" \"tensorflow\" # For OpenAI's pretrained weights\n",
" ]\n",
"for p in pkgs:\n",
" print(f\"{p} version: {version(p)}\")"
]
},
{
"cell_type": "markdown",
"id": "0a3bdf9e-2ff0-4a57-abab-ede2d955a237",
"metadata": {},
"source": [
"- In this chapter, we implement the training loop and code for basic model evaluation to pretrain an LLM\n",
"- At the end of this chapter, we also load openly available pretrained weights from OpenAI into our model"
]
},
{
"cell_type": "markdown",
"id": "efd27fcc-2886-47cb-b544-046c2c31f02a",
"metadata": {},
"source": [
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/chapter-overview.webp\" width=500px>"
]
},
{
"cell_type": "markdown",
"id": "0d214765-7a73-42d5-95e9-302154b29db9",
"metadata": {},
"source": [
"- The topics covered in this chapter are shown below"
]
},
{
"cell_type": "markdown",
"id": "f67711d4-8391-4fee-aeef-07ea53dd5841",
"metadata": {},
"source": [
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/mental-model--0.webp\" width=400px>"
]
},
{
"cell_type": "markdown",
"id": "0d824183-145c-4865-89e1-1f0d0a338f19",
"metadata": {
"id": "0d824183-145c-4865-89e1-1f0d0a338f19"
},
"source": [
"## 5.1 Evaluating generative text models"
]
},
{
"cell_type": "markdown",
"id": "a3350f8c-5181-4f9b-a789-4523105e98f2",
"metadata": {},
"source": [
"- We start this section with a brief recap of initializing a GPT model using the code from the previous chapter\n",
"- Then, we discuss basic evaluation metrics for LLMs\n",
"- Lastly, in this section, we apply these evaluation metrics to a training and validation dataset"
]
},
{
"cell_type": "markdown",
"id": "bdc1cf3f-82d8-46c7-9ecc-58979ce87cdd",
"metadata": {
"id": "bdc1cf3f-82d8-46c7-9ecc-58979ce87cdd"
},
"source": [
"### 5.1.1 Using GPT to generate text"
]
},
{
"cell_type": "markdown",
"id": "5b3415fd-9f4a-4548-908e-9dfa56edc9bc",
"metadata": {},
"source": [
"- We initialize a GPT model using the code from the previous chapter"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "86000d74-624a-48f0-86da-f41926cb9e04",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "86000d74-624a-48f0-86da-f41926cb9e04",
"outputId": "ad482cfd-5a62-4f0d-e1e0-008d6457f512"
},
"outputs": [],
"source": [
"import torch\n",
"from previous_chapters import GPTModel\n",
"# If the `previous_chapters.py` file is not available locally,\n",
"# you can import it from the `llms-from-scratch` PyPI package.\n",
"# For details, see: https://github.com/rasbt/LLMs-from-scratch/tree/main/pkg\n",
"# E.g.,\n",
"# from llms_from_scratch.ch04 import GPTModel\n",
"\n",
"GPT_CONFIG_124M = {\n",
" \"vocab_size\": 50257, # Vocabulary size\n",
" \"context_length\": 256, # Shortened context length (orig: 1024)\n",
" \"emb_dim\": 768, # Embedding dimension\n",
" \"n_heads\": 12, # Number of attention heads\n",
" \"n_layers\": 12, # Number of layers\n",
" \"drop_rate\": 0.1, # Dropout rate\n",
" \"qkv_bias\": False # Query-key-value bias\n",
"}\n",
"\n",
"torch.manual_seed(123)\n",
"model = GPTModel(GPT_CONFIG_124M)\n",
"model.eval(); # Disable dropout during inference"
]
},
{
"cell_type": "markdown",
"id": "09c6cf0f-7458-48a2-97fd-aa5068d65e8c",
"metadata": {},
"source": [
"- We use dropout of 0.1 above, but it's relatively common to train LLMs without dropout nowadays\n",
"- Modern LLMs also don't use bias vectors in the `nn.Linear` layers for the query, key, and value matrices (unlike earlier GPT models), which is achieved by setting `\"qkv_bias\": False`\n",
"- We reduce the context length (`context_length`) of only 256 tokens to reduce the computational resource requirements for training the model, whereas the original 124 million parameter GPT-2 model used 1024 tokens\n",
" - This is so that more readers will be able to follow and execute the code examples on their laptop computer\n",
" - However, please feel free to increase the `context_length` to 1024 tokens (this would not require any code changes)\n",
" - We will also load a model with a 1024 `context_length` later from pretrained weights"
]
},
{
"cell_type": "markdown",
"id": "59f80895-be35-4bb5-81cb-f357ef7367fe",
"metadata": {},
"source": [
"- Next, we use the `generate_text_simple` function from the previous chapter to generate text\n",
"- In addition, we define two convenience functions, `text_to_token_ids` and `token_ids_to_text`, for converting between token and text representations that we use throughout this chapter"
]
},
{
"cell_type": "markdown",
"id": "741881f3-cee0-49ad-b11d-b9df3b3ac234",
"metadata": {},
"source": [
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/gpt-process.webp\" width=500px>"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "5e062b82-3540-48ce-8eb4-009686d0d16c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Output text:\n",
" Every effort moves you rentingetic wasnم refres RexMeCHicular stren\n"
]
}
],
"source": [
"import tiktoken\n",
"from previous_chapters import generate_text_simple\n",
"\n",
"# Alternatively:\n",
"# from llms_from_scratch.ch04 import generate_text_simple\n",
"\n",
"def text_to_token_ids(text, tokenizer):\n",
" encoded = tokenizer.encode(text, allowed_special={'<|endoftext|>'})\n",
" encoded_tensor = torch.tensor(encoded).unsqueeze(0) # add batch dimension\n",
" return encoded_tensor\n",
"\n",
"def token_ids_to_text(token_ids, tokenizer):\n",
" flat = token_ids.squeeze(0) # remove batch dimension\n",
" return tokenizer.decode(flat.tolist())\n",
"\n",
"start_context = \"Every effort moves you\"\n",
"tokenizer = tiktoken.get_encoding(\"gpt2\")\n",
"\n",
"token_ids = generate_text_simple(\n",
" model=model,\n",
" idx=text_to_token_ids(start_context, tokenizer),\n",
" max_new_tokens=10,\n",
" context_size=GPT_CONFIG_124M[\"context_length\"]\n",
")\n",
"\n",
"print(\"Output text:\\n\", token_ids_to_text(token_ids, tokenizer))"
]
},
{
"cell_type": "markdown",
"id": "e4d3249b-b2a0-44c4-b589-ae4b403b8305",
"metadata": {},
"source": [
"- As we can see above, the model does not produce good text because it has not been trained yet\n",
"- How do we measure or capture what \"good text\" is, in a numeric form, to track it during training?\n",
"- The next subsection introduces metrics to calculate a loss metric for the generated outputs that we can use to measure the training progress\n",
"- The next chapters on finetuning LLMs will also introduce additional ways to measure model quality"
]
},
{
"cell_type": "markdown",
"id": "955f9e1a-7bf7-40d8-b1fa-eacabdee8d8e",
"metadata": {},
"source": [
"<br>"
]
},
{
"cell_type": "markdown",
"id": "0f3d7ea2-637f-4490-bc76-e361fc81ae98",
"metadata": {
"id": "0f3d7ea2-637f-4490-bc76-e361fc81ae98"
},
"source": [
"### 5.1.2 Calculating the text generation loss: cross-entropy and perplexity"
]
},
{
"cell_type": "markdown",
"id": "9e1ba8aa-fb03-4d25-957f-fe8778762440",
"metadata": {},
"source": [
"- Suppose we have an `inputs` tensor containing the token IDs for 2 training examples (rows)\n",
"- Corresponding to the `inputs`, the `targets` contain the desired token IDs that we want the model to generate\n",
"- Notice that the `targets` are the `inputs` shifted by 1 position, as explained in chapter 2 when we implemented the data loader"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "6b5402f8-ec0c-4a44-9892-18a97779ee4f",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "6b5402f8-ec0c-4a44-9892-18a97779ee4f",
"outputId": "8d6fa0ff-7b37-4634-c3f0-2c050cbe81f0"
},
"outputs": [],
"source": [
"inputs = torch.tensor([[16833, 3626, 6100], # [\"every effort moves\",\n",
" [40, 1107, 588]]) # \"I really like\"]\n",
"\n",
"targets = torch.tensor([[3626, 6100, 345 ], # [\" effort moves you\",\n",
" [1107, 588, 11311]]) # \" really like chocolate\"]"
]
},
{
"cell_type": "markdown",
"id": "33dc0645-ac2c-4973-9b40-6da40515bede",
"metadata": {},
"source": [
"- Feeding the `inputs` to the model, we obtain the logits vector for the 2 input examples that consist of 3 tokens each\n",
"- Each of the tokens is a 50,257-dimensional vector corresponding to the size of the vocabulary\n",
"- Applying the softmax function, we can turn the logits tensor into a tensor of the same dimension containing probability scores "
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "e7b6ec51-6f8c-49bd-a349-95ba38b46fb6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"torch.Size([2, 3, 50257])\n"
]
}
],
"source": [
"with torch.no_grad():\n",
" logits = model(inputs)\n",
"\n",
"probas = torch.softmax(logits, dim=-1) # Probability of each token in vocabulary\n",
"print(probas.shape) # Shape: (batch_size, num_tokens, vocab_size)"
]
},
{
"cell_type": "markdown",
"id": "5c36a382-b5e2-4de6-9e65-0b69b685013b",
"metadata": {},
"source": [
"- The figure below, using a very small vocabulary for illustration purposes, outlines how we convert the probability scores back into text, which we discussed at the end of the previous chapter"
]
},
{
"cell_type": "markdown",
"id": "384d86a9-0013-476c-bb6b-274fd5f20b29",
"metadata": {},
"source": [
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/proba-to-text.webp\" width=500px>"
]
},
{
"cell_type": "markdown",
"id": "e8480efd-d419-4954-9ecc-2876055334bd",
"metadata": {},
"source": [
"- As discussed in the previous chapter, we can apply the `argmax` function to convert the probability scores into predicted token IDs\n",
"- The softmax function above produced a 50,257-dimensional vector for each token; the `argmax` function returns the position of the highest probability score in this vector, which is the predicted token ID for the given token"
]
},
{
"cell_type": "markdown",
"id": "f3b84c9f-dd08-482e-b903-a86fe44e1144",
"metadata": {},
"source": [
"- Since we have 2 input batches with 3 tokens each, we obtain 2 by 3 predicted token IDs:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "34ebd76a-16ec-4c17-8958-8a135735cc1c",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "34ebd76a-16ec-4c17-8958-8a135735cc1c",
"outputId": "ed17da47-c3e7-4775-fd00-4ec5bcda3db2"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Token IDs:\n",
" tensor([[[16657],\n",
" [ 339],\n",
" [42826]],\n",
"\n",
" [[49906],\n",
" [29669],\n",
" [41751]]])\n"
]
}
],
"source": [
"token_ids = torch.argmax(probas, dim=-1, keepdim=True)\n",
"print(\"Token IDs:\\n\", token_ids)"
]
},
{
"cell_type": "markdown",
"id": "cee4072c-21ed-4df7-8721-dd2535362573",
"metadata": {},
"source": [
"- If we decode these tokens, we find that these are quite different from the tokens we want the model to predict, namely the target tokens:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "c990ead6-53cd-49a7-a6d1-14d8c1518249",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Targets batch 1: effort moves you\n",
"Outputs batch 1: Armed heNetflix\n"
]
}
],
"source": [
"print(f\"Targets batch 1: {token_ids_to_text(targets[0], tokenizer)}\")\n",
"print(f\"Outputs batch 1: {token_ids_to_text(token_ids[0].flatten(), tokenizer)}\")"
]
},
{
"cell_type": "markdown",
"id": "a53eb8a7-070e-46d6-930c-314ba55a6ff2",
"metadata": {},
"source": [
"- That's because the model wasn't trained yet\n",
"- To train the model, we need to know how far it is away from the correct predictions (targets)"
]
},
{
"cell_type": "markdown",
"id": "ad90592f-0d5d-4ec8-9ff5-e7675beab10e",
"metadata": {},
"source": [
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/proba-index.webp\" width=500px>"
]
},
{
"cell_type": "markdown",
"id": "c7251bf5-a079-4782-901d-68c9225d3157",
"metadata": {},
"source": [
"- The token probabilities corresponding to the target indices are as follows:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "54aef09c-d6e3-4238-8653-b3a1b0a1077a",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "54aef09c-d6e3-4238-8653-b3a1b0a1077a",
"outputId": "41c946a2-c458-433e-a53d-5e7e89d9dddc"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Text 1: tensor([7.4541e-05, 3.1061e-05, 1.1563e-05])\n",
"Text 2: tensor([1.0337e-05, 5.6776e-05, 4.7559e-06])\n"
]
}
],
"source": [
"text_idx = 0\n",
"target_probas_1 = probas[text_idx, [0, 1, 2], targets[text_idx]]\n",
"print(\"Text 1:\", target_probas_1)\n",
"\n",
"text_idx = 1\n",
"target_probas_2 = probas[text_idx, [0, 1, 2], targets[text_idx]]\n",
"print(\"Text 2:\", target_probas_2)"
]
},
{
"cell_type": "markdown",
"id": "a0e89a19-73c2-4e49-93b4-861f699f1cbf",
"metadata": {},
"source": [
"- We want to maximize all these values, bringing them close to a probability of 1\n",
"- In mathematical optimization, it is easier to maximize the logarithm of the probability score than the probability score itself; this is out of the scope of this book, but I have recorded a lecture with more details here: [L8.2 Logistic Regression Loss Function](https://www.youtube.com/watch?v=GxJe0DZvydM)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "31402a67-a16e-4aeb-977e-70abb9c9949b",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "31402a67-a16e-4aeb-977e-70abb9c9949b",
"outputId": "1bf18e79-1246-4eab-efd8-12b328c78678"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tensor([ -9.5042, -10.3796, -11.3677, -11.4798, -9.7764, -12.2561])\n"
]
}
],
"source": [
"# Compute logarithm of all token probabilities\n",
"log_probas = torch.log(torch.cat((target_probas_1, target_probas_2)))\n",
"print(log_probas)"
]
},
{
"cell_type": "markdown",
"id": "c4261441-a511-4633-9c4c-67998af31b84",
"metadata": {},
"source": [
"- Next, we compute the average log probability:"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "9b003797-161b-4d98-81dc-e68320e09fec",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "9b003797-161b-4d98-81dc-e68320e09fec",
"outputId": "a447fe9c-7e27-40ed-f1fb-51210e3f7cc9"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tensor(-10.7940)\n"
]
}
],
"source": [
"# Calculate the average probability for each token\n",
"avg_log_probas = torch.mean(log_probas)\n",
"print(avg_log_probas)"
]
},
{
"cell_type": "markdown",
"id": "36d51994-ad17-4ba3-a6ec-f588b4b13585",
"metadata": {},
"source": [
"- The goal is to make this average log probability as large as possible by optimizing the model weights\n",
"- Due to the log, the largest possible value is 0, and we are currently far away from 0"
]
},
{
"cell_type": "markdown",
"id": "3de388a1-8a0a-4c94-8894-9041dc6ad514",
"metadata": {},
"source": [
"- In deep learning, instead of maximizing the average log-probability, it's a standard convention to minimize the *negative* average log-probability value; in our case, instead of maximizing -10.7722 so that it approaches 0, in deep learning, we would minimize 10.7722 so that it approaches 0\n",
"- The value negative of -10.7722, i.e., 10.7722, is also called cross-entropy loss in deep learning"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "176ddf35-1c5f-4d7c-bf17-70f3e7069bd4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tensor(10.7940)\n"
]
}
],
"source": [
"neg_avg_log_probas = avg_log_probas * -1\n",
"print(neg_avg_log_probas)"
]
},
{
"cell_type": "markdown",
"id": "84eeb868-abd8-4028-82db-107546bf7c2c",
"metadata": {},
"source": [
"- PyTorch already implements a `cross_entropy` function that carries out the previous steps"
]
},
{
"cell_type": "markdown",
"id": "5bd24b7f-b760-47ad-bc84-86d13794aa54",
"metadata": {},
"source": [
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/cross-entropy.webp?123\" width=400px>"
]
},
{
"cell_type": "markdown",
"id": "e8aaf9dd-3ee6-42bf-a63f-6e93dbfb989d",
"metadata": {},
"source": [
"- Before we apply the `cross_entropy` function, let's check the shape of the logits and targets"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "695d6f64-5084-4c23-aea4-105c9e38cfe4",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "695d6f64-5084-4c23-aea4-105c9e38cfe4",
"outputId": "43fd802a-8136-4b35-df0d-f61a5d4cb561"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Logits shape: torch.Size([2, 3, 50257])\n",
"Targets shape: torch.Size([2, 3])\n"
]
}
],
"source": [
"# Logits have shape (batch_size, num_tokens, vocab_size)\n",
"print(\"Logits shape:\", logits.shape)\n",
"\n",
"# Targets have shape (batch_size, num_tokens)\n",
"print(\"Targets shape:\", targets.shape)"
]
},
{
"cell_type": "markdown",
"id": "1d3d65f0-6566-4865-93e4-0c0bcb10cd06",
"metadata": {},
"source": [
"- For the `cross_entropy` function in PyTorch, we want to flatten these tensors by combining them over the batch dimension:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "0e17e027-ab9f-4fb5-ac9b-a009b831c122",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "0e17e027-ab9f-4fb5-ac9b-a009b831c122",
"outputId": "0b2b778b-02fb-43b2-c879-adc59055a7d8"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Flattened logits: torch.Size([6, 50257])\n",
"Flattened targets: torch.Size([6])\n"
]
}
],
"source": [
"logits_flat = logits.flatten(0, 1)\n",
"targets_flat = targets.flatten()\n",
"\n",
"print(\"Flattened logits:\", logits_flat.shape)\n",
"print(\"Flattened targets:\", targets_flat.shape)"
]
},
{
"cell_type": "markdown",
"id": "4921a57f-3a79-473e-a863-6d63b495010f",
"metadata": {},
"source": [
"- Note that the targets are the token IDs, which also represent the index positions in the logits tensors that we want to maximize\n",
"- The `cross_entropy` function in PyTorch will automatically take care of applying the softmax and log-probability computation internally over those token indices in the logits that are to be maximized "
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "62d0816e-b29a-4c8f-a9a5-a167562de978",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "62d0816e-b29a-4c8f-a9a5-a167562de978",
"outputId": "c0be634a-2c65-4ff7-a73f-1bfc2e406ba4"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tensor(10.7940)\n"
]
}
],
"source": [
"loss = torch.nn.functional.cross_entropy(logits_flat, targets_flat)\n",
"print(loss)"
]
},
{
"cell_type": "markdown",
"id": "0f15ce17-fd7b-4d8e-99da-b237523a7a80",
"metadata": {},
"source": [
"- A concept related to the cross-entropy loss is the perplexity of an LLM\n",
"- The perplexity is simply the exponential of the cross-entropy loss"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "168952a1-b964-4aa7-8e49-966fa26add54",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "168952a1-b964-4aa7-8e49-966fa26add54",
"outputId": "a0a692c1-6412-4068-8aa5-8858548141eb"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tensor(48725.8203)\n"
]
}
],
"source": [
"perplexity = torch.exp(loss)\n",
"print(perplexity)"
]
},
{
"cell_type": "markdown",
"id": "71ae26dd-d77e-41fd-b924-6bd103dd4ee7",
"metadata": {},
"source": [
"- The perplexity is often considered more interpretable because it can be understood as the effective vocabulary size that the model is uncertain about at each step (in the example above, that'd be 48,725 words or tokens)\n",
"- In other words, perplexity provides a measure of how well the probability distribution predicted by the model matches the actual distribution of the words in the dataset\n",
"- Similar to the loss, a lower perplexity indicates that the model predictions are closer to the actual distribution"
]
},
{
"cell_type": "markdown",
"id": "2ec6c217-e429-40c7-ad71-5d0a9da8e487",
"metadata": {
"id": "2ec6c217-e429-40c7-ad71-5d0a9da8e487"
},
"source": [
"### 5.1.3 Calculating the training and validation set losses"
]
},
{
"cell_type": "markdown",
"id": "530da89e-2448-436c-8f1b-28e8a31ef85c",
"metadata": {},
"source": [
"- We use a relatively small dataset for training the LLM (in fact, only one short story)\n",
"- The reasons are:\n",
" - You can run the code examples in a few minutes on a laptop computer without a suitable GPU\n",
" - The training finishes relatively fast (minutes instead of weeks), which is good for educational purposes\n",
" - We use a text from the public domain, which can be included in this GitHub repository without violating any usage rights or bloating the repository size\n",
"\n",
"\n",
"- For example, Llama 2 7B required 184,320 GPU hours on A100 GPUs to be trained on 2 trillion tokens\n",
" - At the time of this writing, the hourly cost of an 8xA100 cloud server at AWS is approximately \\\\$30\n",
" - So, via an off-the-envelope calculation, training this LLM would cost 184,320 / 8 * \\\\$30 = \\\\$690,000\n",
" \n",
"- Below, we use the same dataset we used in chapter 2"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "654fde37-b2a9-4a20-a8d3-0206c056e2ff",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import urllib.request\n",
"\n",
"file_path = \"the-verdict.txt\"\n",
"url = \"https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/main/ch02/01_main-chapter-code/the-verdict.txt\"\n",
"\n",
"if not os.path.exists(file_path):\n",
" with urllib.request.urlopen(url) as response:\n",
" text_data = response.read().decode('utf-8')\n",
" with open(file_path, \"w\", encoding=\"utf-8\") as file:\n",
" file.write(text_data)\n",
"else:\n",
" with open(file_path, \"r\", encoding=\"utf-8\") as file:\n",
" text_data = file.read()"
]
},
{
"cell_type": "markdown",
"id": "379330f1-80f4-4e34-8724-41d892b04cee",
"metadata": {},
"source": [
"- A quick check that the text loaded ok by printing the first and last 100 words"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "6kgJbe4ehI4q",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 35
},
"id": "6kgJbe4ehI4q",
"outputId": "9ff31e88-ee37-47e9-ee64-da6eb552f46f"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"I HAD always thought Jack Gisburn rather a cheap genius--though a good fellow enough--so it was no \n"
]
}
],
"source": [
"# First 100 characters\n",
"print(text_data[:99])"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "j2XPde_ThM_e",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 35
},
"id": "j2XPde_ThM_e",
"outputId": "a900c1b9-9a87-4078-968b-a5721deda5cb"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"it for me! The Strouds stand alone, and happen once--but there's no exterminating our kind of art.\"\n"
]
}
],
"source": [
"# Last 100 characters\n",
"print(text_data[-99:])"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "6b46a952-d50a-4837-af09-4095698f7fd1",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "6b46a952-d50a-4837-af09-4095698f7fd1",
"outputId": "c2a25334-21ca-486e-8226-0296e5fc6486"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Characters: 20479\n",
"Tokens: 5145\n"
]
}
],
"source": [
"total_characters = len(text_data)\n",
"total_tokens = len(tokenizer.encode(text_data))\n",
"\n",
"print(\"Characters:\", total_characters)\n",
"print(\"Tokens:\", total_tokens)"
]
},
{
"cell_type": "markdown",
"id": "a8830cb9-90f6-4e7c-8620-beeabc2d39f7",
"metadata": {},
"source": [
"- With 5,145 tokens, the text is very short for training an LLM, but again, it's for educational purposes (we will also load pretrained weights later)"
]
},
{
"cell_type": "markdown",
"id": "bedcad87-a0e8-4b9d-ac43-4e927ccbb50f",
"metadata": {},
"source": [
"- Next, we divide the dataset into a training and a validation set and use the data loaders from chapter 2 to prepare the batches for LLM training\n",
"- For visualization purposes, the figure below assumes a `max_length=6`, but for the training loader, we set the `max_length` equal to the context length that the LLM supports\n",
"- The figure below only shows the input tokens for simplicity\n",
" - Since we train the LLM to predict the next word in the text, the targets look the same as these inputs, except that the targets are shifted by one position"
]
},
{
"cell_type": "markdown",
"id": "46bdaa07-ba96-4ac1-9d71-b3cc153910d9",
"metadata": {},
"source": [
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/batching.webp\" width=500px>"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "0959c855-f860-4358-8b98-bc654f047578",
"metadata": {},
"outputs": [],
"source": [
"from previous_chapters import create_dataloader_v1\n",
"# Alternatively:\n",
"# from llms_from_scratch.ch02 import create_dataloader_v1\n",
"\n",
"# Train/validation ratio\n",
"train_ratio = 0.90\n",
"split_idx = int(train_ratio * len(text_data))\n",
"train_data = text_data[:split_idx]\n",
"val_data = text_data[split_idx:]\n",
"\n",
"\n",
"torch.manual_seed(123)\n",
"\n",
"train_loader = create_dataloader_v1(\n",
" train_data,\n",
" batch_size=2,\n",
" max_length=GPT_CONFIG_124M[\"context_length\"],\n",
" stride=GPT_CONFIG_124M[\"context_length\"],\n",
" drop_last=True,\n",
" shuffle=True,\n",
" num_workers=0\n",
")\n",
"\n",
"val_loader = create_dataloader_v1(\n",
" val_data,\n",
" batch_size=2,\n",
" max_length=GPT_CONFIG_124M[\"context_length\"],\n",
" stride=GPT_CONFIG_124M[\"context_length\"],\n",
" drop_last=False,\n",
" shuffle=False,\n",
" num_workers=0\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "f37b3eb0-854e-4895-9898-fa7d1e67566e",
"metadata": {},
"outputs": [],
"source": [
"# Sanity check\n",
"\n",
"if total_tokens * (train_ratio) < GPT_CONFIG_124M[\"context_length\"]:\n",
" print(\"Not enough tokens for the training loader. \"\n",
" \"Try to lower the `GPT_CONFIG_124M['context_length']` or \"\n",
" \"increase the `training_ratio`\")\n",
"\n",
"if total_tokens * (1-train_ratio) < GPT_CONFIG_124M[\"context_length\"]:\n",
" print(\"Not enough tokens for the validation loader. \"\n",
" \"Try to lower the `GPT_CONFIG_124M['context_length']` or \"\n",
" \"decrease the `training_ratio`\")"
]
},
{
"cell_type": "markdown",
"id": "e7ac3296-a4d1-4303-9ac5-376518960c33",
"metadata": {},
"source": [
"- We use a relatively small batch size to reduce the computational resource demand, and because the dataset is very small to begin with\n",
"- Llama 2 7B was trained with a batch size of 1024, for example"
]
},
{
"cell_type": "markdown",
"id": "a8e0514d-b990-4dc0-9afb-7721993284a0",
"metadata": {},
"source": [
"- An optional check that the data was loaded correctly:"
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "ca0116d0-d229-472c-9fbf-ebc229331c3e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train loader:\n",
"torch.Size([2, 256]) torch.Size([2, 256])\n",
"torch.Size([2, 256]) torch.Size([2, 256])\n",
"torch.Size([2, 256]) torch.Size([2, 256])\n",
"torch.Size([2, 256]) torch.Size([2, 256])\n",
"torch.Size([2, 256]) torch.Size([2, 256])\n",
"torch.Size([2, 256]) torch.Size([2, 256])\n",
"torch.Size([2, 256]) torch.Size([2, 256])\n",
"torch.Size([2, 256]) torch.Size([2, 256])\n",
"torch.Size([2, 256]) torch.Size([2, 256])\n",
"\n",
"Validation loader:\n",
"torch.Size([2, 256]) torch.Size([2, 256])\n"
]
}
],
"source": [
"print(\"Train loader:\")\n",
"for x, y in train_loader:\n",
" print(x.shape, y.shape)\n",
"\n",
"print(\"\\nValidation loader:\")\n",
"for x, y in val_loader:\n",
" print(x.shape, y.shape)"
]
},
{
"cell_type": "markdown",
"id": "f7b9b1a4-863d-456f-a8dd-c07fb5c024ed",
"metadata": {},
"source": [
"- Another optional check that the token sizes are in the expected ballpark:"
]
},
{
"cell_type": "code",
"execution_count": 23,
"id": "eb860488-5453-41d7-9870-23b723f742a0",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "eb860488-5453-41d7-9870-23b723f742a0",
"outputId": "96b9451a-9557-4126-d1c8-51610a1995ab"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Training tokens: 4608\n",
"Validation tokens: 512\n",
"All tokens: 5120\n"
]
}
],
"source": [
"train_tokens = 0\n",
"for input_batch, target_batch in train_loader:\n",
" train_tokens += input_batch.numel()\n",
"\n",
"val_tokens = 0\n",
"for input_batch, target_batch in val_loader:\n",
" val_tokens += input_batch.numel()\n",
"\n",
"print(\"Training tokens:\", train_tokens)\n",
"print(\"Validation tokens:\", val_tokens)\n",
"print(\"All tokens:\", train_tokens + val_tokens)"
]
},
{
"cell_type": "markdown",
"id": "5c3085e8-665e-48eb-bb41-cdde61537e06",
"metadata": {},
"source": [
"- Next, we implement a utility function to calculate the cross-entropy loss of a given batch\n",
"- In addition, we implement a second utility function to compute the loss for a user-specified number of batches in a data loader"
]
},
{
"cell_type": "code",
"execution_count": 24,
"id": "7b9de31e-4096-47b3-976d-b6d2fdce04bc",
"metadata": {
"id": "7b9de31e-4096-47b3-976d-b6d2fdce04bc"
},
"outputs": [],
"source": [
"def calc_loss_batch(input_batch, target_batch, model, device):\n",
" input_batch, target_batch = input_batch.to(device), target_batch.to(device)\n",
" logits = model(input_batch)\n",
" loss = torch.nn.functional.cross_entropy(logits.flatten(0, 1), target_batch.flatten())\n",
" return loss\n",
"\n",
"\n",
"def calc_loss_loader(data_loader, model, device, num_batches=None):\n",
" total_loss = 0.\n",
" if len(data_loader) == 0:\n",
" return float(\"nan\")\n",
" elif num_batches is None:\n",
" num_batches = len(data_loader)\n",
" else:\n",
" # Reduce the number of batches to match the total number of batches in the data loader\n",
" # if num_batches exceeds the number of batches in the data loader\n",
" num_batches = min(num_batches, len(data_loader))\n",
" for i, (input_batch, target_batch) in enumerate(data_loader):\n",
" if i < num_batches:\n",
" loss = calc_loss_batch(input_batch, target_batch, model, device)\n",
" total_loss += loss.item()\n",
" else:\n",
" break\n",
" return total_loss / num_batches"
]
},
{
"cell_type": "markdown",
"id": "f0691332-84d0-48b3-b462-a885ddeb4fca",
"metadata": {},
"source": [
"- If you have a machine with a CUDA-supported GPU, the LLM will train on the GPU without making any changes to the code\n",
"- Via the `device` setting, we ensure that the data is loaded onto the same device as the LLM model"
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "56f5b0c9-1065-4d67-98b9-010e42fc1e2a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Training loss: 10.98758347829183\n",
"Validation loss: 10.98110580444336\n"
]
}
],
"source": [
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
"\n",
"# Note:\n",
"# Uncommenting the following lines will allow the code to run on Apple Silicon chips, if applicable,\n",
"# which is approximately 2x faster than on an Apple CPU (as measured on an M3 MacBook Air).\n",
"# However, the resulting loss values may be slightly different.\n",
"\n",
"#if torch.cuda.is_available():\n",
"# device = torch.device(\"cuda\")\n",
"#elif torch.backends.mps.is_available():\n",
"# device = torch.device(\"mps\")\n",
"#else:\n",
"# device = torch.device(\"cpu\")\n",
"#\n",
"# print(f\"Using {device} device.\")\n",
"\n",
"\n",
"model.to(device) # no assignment model = model.to(device) necessary for nn.Module classes\n",
"\n",
"\n",
"torch.manual_seed(123) # For reproducibility due to the shuffling in the data loader\n",
"\n",
"with torch.no_grad(): # Disable gradient tracking for efficiency because we are not training, yet\n",
" train_loss = calc_loss_loader(train_loader, model, device)\n",
" val_loss = calc_loss_loader(val_loader, model, device)\n",
"\n",
"print(\"Training loss:\", train_loss)\n",
"print(\"Validation loss:\", val_loss)"
]
},
{
"cell_type": "markdown",
"id": "43875e95-190f-4b17-8f9a-35034ba649ec",
"metadata": {},
"source": [
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/mental-model-1.webp\" width=400px>"
]
},
{
"cell_type": "markdown",
"id": "b9339f8d-00cb-4206-af67-58c32bd72055",
"metadata": {
"id": "b9339f8d-00cb-4206-af67-58c32bd72055"
},
"source": [
"## 5.2 Training an LLM"
]
},
{
"cell_type": "markdown",
"id": "652a4cf4-e98f-46d9-bdec-60e7ccb8d6bd",
"metadata": {},
"source": [
"- In this section, we finally implement the code for training the LLM\n",
"- We focus on a simple training function (if you are interested in augmenting this training function with more advanced techniques, such as learning rate warmup, cosine annealing, and gradient clipping, please refer to [Appendix D](../../appendix-D/01_main-chapter-code))\n",
"\n",
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/train-steps.webp\" width=300px>"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "Mtp4gY0ZO-qq",
"metadata": {
"id": "Mtp4gY0ZO-qq"
},
"outputs": [],
"source": [
"def train_model_simple(model, train_loader, val_loader, optimizer, device, num_epochs,\n",
" eval_freq, eval_iter, start_context, tokenizer):\n",
" # Initialize lists to track losses and tokens seen\n",
" train_losses, val_losses, track_tokens_seen = [], [], []\n",
" tokens_seen, global_step = 0, -1\n",
"\n",
" # Main training loop\n",
" for epoch in range(num_epochs):\n",
" model.train() # Set model to training mode\n",
" \n",
" for input_batch, target_batch in train_loader:\n",
" optimizer.zero_grad() # Reset loss gradients from previous batch iteration\n",
" loss = calc_loss_batch(input_batch, target_batch, model, device)\n",
" loss.backward() # Calculate loss gradients\n",
" optimizer.step() # Update model weights using loss gradients\n",
" tokens_seen += input_batch.numel()\n",
" global_step += 1\n",
"\n",
" # Optional evaluation step\n",
" if global_step % eval_freq == 0:\n",
" train_loss, val_loss = evaluate_model(\n",
" model, train_loader, val_loader, device, eval_iter)\n",
" train_losses.append(train_loss)\n",
" val_losses.append(val_loss)\n",
" track_tokens_seen.append(tokens_seen)\n",
" print(f\"Ep {epoch+1} (Step {global_step:06d}): \"\n",
" f\"Train loss {train_loss:.3f}, Val loss {val_loss:.3f}\")\n",
"\n",
" # Print a sample text after each epoch\n",
" generate_and_print_sample(\n",
" model, tokenizer, device, start_context\n",
" )\n",
"\n",
" return train_losses, val_losses, track_tokens_seen\n",
"\n",
"\n",
"def evaluate_model(model, train_loader, val_loader, device, eval_iter):\n",
" model.eval()\n",
" with torch.no_grad():\n",
" train_loss = calc_loss_loader(train_loader, model, device, num_batches=eval_iter)\n",
" val_loss = calc_loss_loader(val_loader, model, device, num_batches=eval_iter)\n",
" model.train()\n",
" return train_loss, val_loss\n",
"\n",
"\n",
"def generate_and_print_sample(model, tokenizer, device, start_context):\n",
" model.eval()\n",
" context_size = model.pos_emb.weight.shape[0]\n",
" encoded = text_to_token_ids(start_context, tokenizer).to(device)\n",
" with torch.no_grad():\n",
" token_ids = generate_text_simple(\n",
" model=model, idx=encoded,\n",
" max_new_tokens=50, context_size=context_size\n",
" )\n",
" decoded_text = token_ids_to_text(token_ids, tokenizer)\n",
" print(decoded_text.replace(\"\\n\", \" \")) # Compact print format\n",
" model.train()"
]
},
{
"cell_type": "markdown",
"id": "a301b333-b9d4-4eeb-a212-3a9874e3ac47",
"metadata": {},
"source": [
"- Now, let's train the LLM using the training function defined above:"
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "3422000b-7aa2-485b-92df-99372cd22311",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "3422000b-7aa2-485b-92df-99372cd22311",
"outputId": "0e046603-908d-4093-8ae5-ef2f632639fb"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Ep 1 (Step 000000): Train loss 9.781, Val loss 9.933\n",
"Ep 1 (Step 000005): Train loss 8.111, Val loss 8.339\n",
"Every effort moves you,,,,,,,,,,,,. \n",
"Ep 2 (Step 000010): Train loss 6.661, Val loss 7.048\n",
"Ep 2 (Step 000015): Train loss 5.961, Val loss 6.616\n",
"Every effort moves you, and, and, and, and, and, and, and, and, and, and, and, and, and, and, and, and, and, and, and, and, and, and,, and, and,\n",
"Ep 3 (Step 000020): Train loss 5.726, Val loss 6.600\n",
"Ep 3 (Step 000025): Train loss 5.201, Val loss 6.348\n",
"Every effort moves you, and I had been. \n",
"Ep 4 (Step 000030): Train loss 4.417, Val loss 6.278\n",
"Ep 4 (Step 000035): Train loss 4.069, Val loss 6.226\n",
"Every effort moves you know the \"I he had the donkey and I had the and I had the donkey and down the room, I had\n",
"Ep 5 (Step 000040): Train loss 3.732, Val loss 6.160\n",
"Every effort moves you know it was not that the picture--I had the fact by the last I had been--his, and in the \"Oh, and he said, and down the room, and in\n",
"Ep 6 (Step 000045): Train loss 2.850, Val loss 6.179\n",
"Ep 6 (Step 000050): Train loss 2.427, Val loss 6.141\n",
"Every effort moves you know,\" was one of the picture. The--I had a little of a little: \"Yes, and in fact, and in the picture was, and I had been at my elbow and as his pictures, and down the room, I had\n",
"Ep 7 (Step 000055): Train loss 2.104, Val loss 6.134\n",
"Ep 7 (Step 000060): Train loss 1.882, Val loss 6.233\n",
"Every effort moves you know,\" was one of the picture for nothing--I told Mrs. \"I was no--as! The women had been, in the moment--as Jack himself, as once one had been the donkey, and were, and in his\n",
"Ep 8 (Step 000065): Train loss 1.320, Val loss 6.238\n",
"Ep 8 (Step 000070): Train loss 0.985, Val loss 6.242\n",
"Every effort moves you know,\" was one of the axioms he had been the tips of a self-confident moustache, I felt to see a smile behind his close grayish beard--as if he had the donkey. \"strongest,\" as his\n",
"Ep 9 (Step 000075): Train loss 0.717, Val loss 6.293\n",
"Ep 9 (Step 000080): Train loss 0.541, Val loss 6.393\n",
"Every effort moves you?\" \"Yes--quite insensible to the irony. She wanted him vindicated--and by me!\" He laughed again, and threw back the window-curtains, I had the donkey. \"There were days when I\n",
"Ep 10 (Step 000085): Train loss 0.391, Val loss 6.452\n",
"Every effort moves you know,\" was one of the axioms he laid down across the Sevres and silver of an exquisitely appointed luncheon-table, when, on a later day, I had again run over from Monte Carlo; and Mrs. Gis\n"
]
}
],
"source": [
"# Note:\n",
"# Uncomment the following code to calculate the execution time\n",
"# import time\n",
"# start_time = time.time()\n",
"\n",
"torch.manual_seed(123)\n",
"model = GPTModel(GPT_CONFIG_124M)\n",
"model.to(device)\n",
"optimizer = torch.optim.AdamW(model.parameters(), lr=0.0004, weight_decay=0.1)\n",
"\n",
"num_epochs = 10\n",
"train_losses, val_losses, tokens_seen = train_model_simple(\n",
" model, train_loader, val_loader, optimizer, device,\n",
" num_epochs=num_epochs, eval_freq=5, eval_iter=5,\n",
" start_context=\"Every effort moves you\", tokenizer=tokenizer\n",
")\n",
"\n",
"# Note:\n",
"# Uncomment the following code to show the execution time\n",
"# end_time = time.time()\n",
"# execution_time_minutes = (end_time - start_time) / 60\n",
"# print(f\"Training completed in {execution_time_minutes:.2f} minutes.\")"
]
},
{
"cell_type": "markdown",
"id": "2e8b86f0-b07d-40d7-b9d3-a9218917f204",
"metadata": {},
"source": [
"- Note that you might get slightly different loss values on your computer, which is not a reason for concern if they are roughly similar (a training loss below 1 and a validation loss below 7)\n",
"- Small differences can often be due to different GPU hardware and CUDA versions or small changes in newer PyTorch versions\n",
"- Even if you are running the example on a CPU, you may observe slight differences; a possible reason for a discrepancy is the differing behavior of `nn.Dropout` across operating systems, depending on how PyTorch was compiled, as discussed [here on the PyTorch issue tracker](https://github.com/pytorch/pytorch/issues/121595)"
]
},
{
"cell_type": "code",
"execution_count": 28,
"id": "0WSRu2i0iHJE",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 487
},
"id": "0WSRu2i0iHJE",
"outputId": "9d36c61b-517d-4f07-a7e8-4563aff78b11"
},
"outputs": [
{
"data": {
"image/png": "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",
"text/plain": [
"<Figure size 500x300 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import matplotlib.pyplot as plt\n",
"from matplotlib.ticker import MaxNLocator\n",
"\n",
"\n",
"def plot_losses(epochs_seen, tokens_seen, train_losses, val_losses):\n",
" fig, ax1 = plt.subplots(figsize=(5, 3))\n",
"\n",
" # Plot training and validation loss against epochs\n",
" ax1.plot(epochs_seen, train_losses, label=\"Training loss\")\n",
" ax1.plot(epochs_seen, val_losses, linestyle=\"-.\", label=\"Validation loss\")\n",
" ax1.set_xlabel(\"Epochs\")\n",
" ax1.set_ylabel(\"Loss\")\n",
" ax1.legend(loc=\"upper right\")\n",
" ax1.xaxis.set_major_locator(MaxNLocator(integer=True)) # only show integer labels on x-axis\n",
"\n",
" # Create a second x-axis for tokens seen\n",
" ax2 = ax1.twiny() # Create a second x-axis that shares the same y-axis\n",
" ax2.plot(tokens_seen, train_losses, alpha=0) # Invisible plot for aligning ticks\n",
" ax2.set_xlabel(\"Tokens seen\")\n",
"\n",
" fig.tight_layout() # Adjust layout to make room\n",
" plt.savefig(\"loss-plot.pdf\")\n",
" plt.show()\n",
"\n",
"epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))\n",
"plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)"
]
},
{
"cell_type": "markdown",
"id": "8bc83ded-5f80-4e1c-bf4d-ccb59999d995",
"metadata": {},
"source": [
"- Looking at the results above, we can see that the model starts out generating incomprehensible strings of words, whereas towards the end, it's able to produce grammatically more or less correct sentences\n",
"- However, based on the training and validation set losses, we can see that the model starts overfitting\n",
"- If we were to check a few passages it writes towards the end, we would find that they are contained in the training set verbatim -- it simply memorizes the training data\n",
"- Later, we will cover decoding strategies that can mitigate this memorization by a certain degree\n",
"- Note that the overfitting here occurs because we have a very, very small training set, and we iterate over it so many times\n",
" - The LLM training here primarily serves educational purposes; we mainly want to see that the model can learn to produce coherent text\n",
" - Instead of spending weeks or months on training this model on vast amounts of expensive hardware, we load pretrained weights later"
]
},
{
"cell_type": "markdown",
"id": "eb380c42-b31c-4ee1-b8b9-244094537272",
"metadata": {},
"source": [
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/mental-model-2.webp\" width=350px>"
]
},
{
"cell_type": "markdown",
"id": "de713235-1561-467f-bf63-bf11ade383f0",
"metadata": {},
"source": [
"**If you are interested in augmenting this training function with more advanced techniques, such as learning rate warmup, cosine annealing, and gradient clipping, please refer to [Appendix D](../../appendix-D/01_main-chapter-code)**"
]
},
{
"cell_type": "markdown",
"id": "6d5cdf2f-09a5-4eb0-a20a-d7aac5c14c2c",
"metadata": {},
"source": [
"**If you are interested in a larger training dataset and longer training run, see [../03_bonus_pretraining_on_gutenberg](../03_bonus_pretraining_on_gutenberg)**"
]
},
{
"cell_type": "markdown",
"id": "699f45fc-bf78-42f2-bd24-2355db41b28f",
"metadata": {
"id": "699f45fc-bf78-42f2-bd24-2355db41b28f"
},
"source": [
"## 5.3 Decoding strategies to control randomness"
]
},
{
"cell_type": "markdown",
"id": "6be9086e-2c27-41da-97d0-49137d0ba3c7",
"metadata": {},
"source": [
"- Inference is relatively cheap with a relatively small LLM as the GPT model we trained above, so there's no need to use a GPU for it in case you used a GPU for training it above\n",
"- Using the `generate_text_simple` function (from the previous chapter) that we used earlier inside the simple training function, we can generate new text one word (or token) at a time\n",
"- As explained in section 5.1.2, the next generated token is the token corresponding to the largest probability score among all tokens in the vocabulary"
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "2734cee0-f6f9-42d5-b71c-fa7e0ef28b6d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Output text:\n",
" Every effort moves you know,\" was one of the axioms he laid down across the Sevres and silver of an exquisitely appointed lun\n"
]
}
],
"source": [
"model.to(\"cpu\")\n",
"model.eval()\n",
"\n",
"tokenizer = tiktoken.get_encoding(\"gpt2\")\n",
"\n",
"token_ids = generate_text_simple(\n",
" model=model,\n",
" idx=text_to_token_ids(\"Every effort moves you\", tokenizer),\n",
" max_new_tokens=25,\n",
" context_size=GPT_CONFIG_124M[\"context_length\"]\n",
")\n",
"\n",
"print(\"Output text:\\n\", token_ids_to_text(token_ids, tokenizer))"
]
},
{
"cell_type": "markdown",
"id": "d25dbe31-bb7c-4893-b25b-47d0492d4aa4",
"metadata": {},
"source": [
"- Even if we execute the `generate_text_simple` function above multiple times, the LLM will always generate the same outputs\n",
"- We now introduce two concepts, so-called decoding strategies, to modify the `generate_text_simple`: *temperature scaling* and *top-k* sampling\n",
"- These will allow the model to control the randomness and diversity of the generated text"
]
},
{
"cell_type": "markdown",
"id": "4bb6f380-a798-4fd9-825c-17b7cd29a994",
"metadata": {},
"source": [
"### 5.3.1 Temperature scaling"
]
},
{
"cell_type": "markdown",
"id": "a7f4f53c-0612-43d3-aa82-52447eac50fa",
"metadata": {},
"source": [
"- Previously, we always sampled the token with the highest probability as the next token using `torch.argmax`\n",
"- To add variety, we can sample the next token using The `torch.multinomial(probs, num_samples=1)`, sampling from a probability distribution\n",
"- Here, each index's chance of being picked corresponds to its probability in the input tensor"
]
},
{
"cell_type": "markdown",
"id": "e7531bae-d5de-44c0-bc78-78fed077e22a",
"metadata": {},
"source": [
"- Here's a little recap of generating the next token, assuming a very small vocabulary for illustration purposes:"
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "01a5ce39-3dc8-4c35-96bc-6410a1e42412",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"forward\n"
]
}
],
"source": [
"vocab = { \n",
" \"closer\": 0,\n",
" \"every\": 1, \n",
" \"effort\": 2, \n",
" \"forward\": 3,\n",
" \"inches\": 4,\n",
" \"moves\": 5, \n",
" \"pizza\": 6,\n",
" \"toward\": 7,\n",
" \"you\": 8,\n",
"} \n",
"\n",
"inverse_vocab = {v: k for k, v in vocab.items()}\n",
"\n",
"# Suppose input is \"every effort moves you\", and the LLM\n",
"# returns the following logits for the next token:\n",
"next_token_logits = torch.tensor(\n",
" [4.51, 0.89, -1.90, 6.75, 1.63, -1.62, -1.89, 6.28, 1.79]\n",
")\n",
"\n",
"probas = torch.softmax(next_token_logits, dim=0)\n",
"next_token_id = torch.argmax(probas).item()\n",
"\n",
"# The next generated token is then as follows:\n",
"print(inverse_vocab[next_token_id])"
]
},
{
"cell_type": "code",
"execution_count": 31,
"id": "6400572f-b3c8-49e2-95bc-433e55c5b3a1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"forward\n"
]
}
],
"source": [
"torch.manual_seed(123)\n",
"next_token_id = torch.multinomial(probas, num_samples=1).item()\n",
"print(inverse_vocab[next_token_id])"
]
},
{
"cell_type": "markdown",
"id": "c63d0a27-830b-42b5-9986-6d1a7de04dd9",
"metadata": {},
"source": [
"- Instead of determining the most likely token via `torch.argmax`, we use `torch.multinomial(probas, num_samples=1)` to determine the most likely token by sampling from the softmax distribution\n",
"- For illustration purposes, let's see what happens when we sample the next token 1,000 times using the original softmax probabilities:"
]
},
{
"cell_type": "code",
"execution_count": 32,
"id": "b23b863e-252a-403c-b5b1-62bc0a42319f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"73 x closer\n",
"0 x every\n",
"0 x effort\n",
"582 x forward\n",
"2 x inches\n",
"0 x moves\n",
"0 x pizza\n",
"343 x toward\n",
"0 x you\n"
]
}
],
"source": [
"def print_sampled_tokens(probas):\n",
" torch.manual_seed(123) # Manual seed for reproducibility\n",
" sample = [torch.multinomial(probas, num_samples=1).item() for i in range(1_000)]\n",
" sampled_ids = torch.bincount(torch.tensor(sample), minlength=len(probas))\n",
" for i, freq in enumerate(sampled_ids):\n",
" print(f\"{freq} x {inverse_vocab[i]}\")\n",
"\n",
"print_sampled_tokens(probas)"
]
},
{
"cell_type": "markdown",
"id": "32e7d9cf-a26d-4d9a-8664-4af1efa73832",
"metadata": {},
"source": [
"- We can control the distribution and selection process via a concept called temperature scaling\n",
"- \"Temperature scaling\" is just a fancy word for dividing the logits by a number greater than 0\n",
"- Temperatures greater than 1 will result in more uniformly distributed token probabilities after applying the softmax\n",
"- Temperatures smaller than 1 will result in more confident (sharper or more peaky) distributions after applying the softmax"
]
},
{
"cell_type": "markdown",
"id": "0d2b5399",
"metadata": {},
"source": [
"- Note that the resulting dropout outputs may look different depending on your operating system; you can read more about this inconsistency [here on the PyTorch issue tracker](https://github.com/pytorch/pytorch/issues/121595)"
]
},
{
"cell_type": "code",
"execution_count": 33,
"id": "0759e4c8-5362-467c-bec6-b0a19d1ba43d",
"metadata": {},
"outputs": [],
"source": [
"def softmax_with_temperature(logits, temperature):\n",
" scaled_logits = logits / temperature\n",
" return torch.softmax(scaled_logits, dim=0)\n",
"\n",
"# Temperature values\n",
"temperatures = [1, 0.1, 5] # Original, higher confidence, and lower confidence\n",
"\n",
"# Calculate scaled probabilities\n",
"scaled_probas = [softmax_with_temperature(next_token_logits, T) for T in temperatures]"
]
},
{
"cell_type": "code",
"execution_count": 34,
"id": "2e66e613-4aca-4296-a984-ddd0d80c6578",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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9llr/fr1Dx1ft26dVbBgQctUjz/+uPXHH3/oxx999JHVvHlz/fjXX3/V14eJkiZN6moV2K5dO6tLly768bFjx6xUqVJZJho7dqyVMmVK/dvD6/idd96xqlWrZj3xxBNWr169LNO89NJLD7UXhjlz5lhVqlSxnGbNmjVWiRIlLKdgMpkfcOXufcNaqf2v6c6ePaudx3BDu9E6dero2h6mOTGKMmWUihumXDETYH+OG6a9Dx8+LC+//LKY5ujRoz67tGFGAKMTU6VMmVIuX76sH69cuVKqV6+uHydLlkxu374tJkJfgAMHDugMC/oE2Od869YtfV2baMKECbqk8O9//1u7CGKJAX+HnTt31uUp02D0jMZJ3nBs69at4jSZM2fW9w7HCPWVAsWtu3fvWosWLbLq1q1rJUmSxCpZsqQ1ceJE6/r1667v+eabb6w0adJYJp0zruhNGuk/SqVKlazq1atb586dcx3DxzVq1LAqV65smapp06Y60mjTpo2VPHly69KlS3r822+/1VkCE/Xr109HopipyJUrl/X333/r8WnTplnlypWzTJ25OHHihH6cMWNGa/fu3foxXuPp0qWzTIOZq+7duz90HMfwNVPt2bPH44bnedmyZToL8Pzzz1tOwTVqP2EdbNKkSTqKxlUnRn5o1ZknTx5d8zVN1qxZdTTapEkTvRJGtzJvL774YtB7dscE1s337t0rTjJt2jR57bXXJFeuXNqsHpDLYHd7MxXWpLGOjnP9z3/+48qy37Fjh75mTNS/f3/tnodzRrMeu+kCRtNYVzVRlixZ5MqVK/p+gdfIli1bpFixYvo+YuJGHMywvf7667Js2TIpW7asHsP7x2+//aavE1MVL178oaROKFeunEyfPl2cgtuz/ICkG7TnRNYpEhN+/fVX3Ub05ZdfapKFezKGSRcWeDPDVKaTIJEJb8DDhg0Tp8CfFqYzkdgEzzzzjCbuRTeTlmLu77//dsRru23btnoBh2ROXBx1795dnn/+edm+fbte4OFCzzT/+9//9D0PW1Lt13P79u1dF6Im+uOPPzw+R8vkjBkzOuI14o6B2g9Yy0WWLLblpEqVSvbs2aOBGgEb2wIuXbokJkHG4+OPP65bypzWv/u9997TAjMYkfrKsB89erSYwsnPM6xfv14mT56seRZff/21bt/DBR5miSpWrCimwdo0/g4x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",
"text/plain": [
"<Figure size 500x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# Plotting\n",
"x = torch.arange(len(vocab))\n",
"bar_width = 0.15\n",
"\n",
"fig, ax = plt.subplots(figsize=(5, 3))\n",
"for i, T in enumerate(temperatures):\n",
" rects = ax.bar(x + i * bar_width, scaled_probas[i], bar_width, label=f'Temperature = {T}')\n",
"\n",
"ax.set_ylabel('Probability')\n",
"ax.set_xticks(x)\n",
"ax.set_xticklabels(vocab.keys(), rotation=90)\n",
"ax.legend()\n",
"\n",
"plt.tight_layout()\n",
"plt.savefig(\"temperature-plot.pdf\")\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "d750e989-842a-4cfa-a44b-cf44d6e49163",
"metadata": {},
"source": [
"- We can see that the rescaling via temperature 0.1 results in a sharper distribution, approaching `torch.argmax`, such that the most likely word is almost always selected:"
]
},
{
"cell_type": "code",
"execution_count": 35,
"id": "e4600713-c51e-4f53-bf58-040a6eb362b8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0 x closer\n",
"0 x every\n",
"0 x effort\n",
"985 x forward\n",
"0 x inches\n",
"0 x moves\n",
"0 x pizza\n",
"15 x toward\n",
"0 x you\n"
]
}
],
"source": [
"print_sampled_tokens(scaled_probas[1])"
]
},
{
"cell_type": "markdown",
"id": "526e93cb-8e2a-42a1-b1ba-4fd5fe64c26b",
"metadata": {},
"source": [
"- The rescaled probabilities via temperature 5 are more uniformly distributed:"
]
},
{
"cell_type": "code",
"execution_count": 36,
"id": "9dfb48f0-bc3f-46a5-9844-33b6c9b0f4df",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"165 x closer\n",
"75 x every\n",
"42 x effort\n",
"239 x forward\n",
"71 x inches\n",
"46 x moves\n",
"32 x pizza\n",
"227 x toward\n",
"103 x you\n"
]
}
],
"source": [
"print_sampled_tokens(scaled_probas[2])"
]
},
{
"cell_type": "markdown",
"id": "0c83f0c4-3774-4375-ad7f-96440ba5fef7",
"metadata": {},
"source": [
"- Assuming an LLM input \"every effort moves you\", using the approach above can sometimes result in nonsensical texts, such as \"every effort moves you pizza\", 3.2% of the time (32 out of 1000 times)"
]
},
{
"cell_type": "markdown",
"id": "c6e4873e-07e4-4abb-85df-bdaedcc1a6f7",
"metadata": {},
"source": [
"### 5.3.2 Top-k sampling"
]
},
{
"cell_type": "markdown",
"id": "6d4da95a-8bb2-4f69-a9b0-a643531db5df",
"metadata": {},
"source": [
"- To be able to use higher temperatures to increase output diversity and to reduce the probability of nonsensical sentences, we can restrict the sampled tokens to the top-k most likely tokens:"
]
},
{
"cell_type": "markdown",
"id": "7ae6fffd-2730-4abe-a2d3-781fc4836f17",
"metadata": {},
"source": [
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/topk.webp\" width=500px>\n",
"\n",
"- (Please note that the numbers in this figure are truncated to two\n",
"digits after the decimal point to reduce visual clutter. The values in the Softmax row should add up to 1.0.)"
]
},
{
"cell_type": "markdown",
"id": "0ba12da5-6ff1-4008-91b8-d2d537cbc14c",
"metadata": {},
"source": [
"- In code, we can implement this as follows:"
]
},
{
"cell_type": "code",
"execution_count": 37,
"id": "2a7f908a-e9ec-446a-b407-fb6dbf05c806",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Top logits: tensor([6.7500, 6.2800, 4.5100])\n",
"Top positions: tensor([3, 7, 0])\n"
]
}
],
"source": [
"top_k = 3\n",
"top_logits, top_pos = torch.topk(next_token_logits, top_k)\n",
"\n",
"print(\"Top logits:\", top_logits)\n",
"print(\"Top positions:\", top_pos)"
]
},
{
"cell_type": "code",
"execution_count": 38,
"id": "753865ed-79c5-48b1-b9f2-ccb132ff1d2f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tensor([4.5100, -inf, -inf, 6.7500, -inf, -inf, -inf, 6.2800, -inf])\n"
]
}
],
"source": [
"new_logits = torch.where(\n",
" condition=next_token_logits < top_logits[-1],\n",
" input=torch.tensor(float(\"-inf\")), \n",
" other=next_token_logits\n",
")\n",
"\n",
"print(new_logits)"
]
},
{
"cell_type": "markdown",
"id": "dfa6fa49-6e99-459d-a517-d7d0f51c4f00",
"metadata": {},
"source": [
"> NOTE: \n",
">\n",
"> An alternative, slightly more efficient implementation of the previous code cell is the following:\n",
">\n",
"> ```python\n",
"> new_logits = torch.full_like( # create tensor containing -inf values\n",
"> next_token_logits, -torch.inf\n",
">) \n",
"> new_logits[top_pos] = next_token_logits[top_pos] # copy top k values into the -inf tensor\n",
"> ```\n",
"> <br>\n",
"> For more details, see https://github.com/rasbt/LLMs-from-scratch/discussions/326\n"
]
},
{
"cell_type": "code",
"execution_count": 39,
"id": "4844f000-c329-4e7e-aa89-16a2c4ebee43",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tensor([0.0615, 0.0000, 0.0000, 0.5775, 0.0000, 0.0000, 0.0000, 0.3610, 0.0000])\n"
]
}
],
"source": [
"topk_probas = torch.softmax(new_logits, dim=0)\n",
"print(topk_probas)"
]
},
{
"cell_type": "markdown",
"id": "56056503-a15d-4315-a3ff-46647a4c7c45",
"metadata": {},
"source": [
"### 5.3.3 Modifying the text generation function"
]
},
{
"cell_type": "markdown",
"id": "34770423-473d-46f6-a5fa-6b2979564d26",
"metadata": {},
"source": [
"- The previous two subsections introduced temperature sampling and top-k sampling\n",
"- Let's use these two concepts to modify the `generate_simple` function we used to generate text via the LLM earlier, creating a new `generate` function:"
]
},
{
"cell_type": "code",
"execution_count": 40,
"id": "8e318891-bcc0-4d71-b147-33ce55febfa3",
"metadata": {},
"outputs": [],
"source": [
"def generate(model, idx, max_new_tokens, context_size, temperature=0.0, top_k=None, eos_id=None):\n",
"\n",
" # For-loop is the same as before: Get logits, and only focus on last time step\n",
" for _ in range(max_new_tokens):\n",
" idx_cond = idx[:, -context_size:]\n",
" with torch.no_grad():\n",
" logits = model(idx_cond)\n",
" logits = logits[:, -1, :]\n",
"\n",
" # New: Filter logits with top_k sampling\n",
" if top_k is not None:\n",
" # Keep only top_k values\n",
" top_logits, _ = torch.topk(logits, top_k)\n",
" min_val = top_logits[:, -1]\n",
" logits = torch.where(logits < min_val, torch.tensor(float(\"-inf\")).to(logits.device), logits)\n",
"\n",
" # New: Apply temperature scaling\n",
" if temperature > 0.0:\n",
" logits = logits / temperature\n",
"\n",
" # Apply softmax to get probabilities\n",
" probs = torch.softmax(logits, dim=-1) # (batch_size, context_len)\n",
"\n",
" # Sample from the distribution\n",
" idx_next = torch.multinomial(probs, num_samples=1) # (batch_size, 1)\n",
"\n",
" # Otherwise same as before: get idx of the vocab entry with the highest logits value\n",
" else:\n",
" idx_next = torch.argmax(logits, dim=-1, keepdim=True) # (batch_size, 1)\n",
"\n",
" if idx_next == eos_id: # Stop generating early if end-of-sequence token is encountered and eos_id is specified\n",
" break\n",
"\n",
" # Same as before: append sampled index to the running sequence\n",
" idx = torch.cat((idx, idx_next), dim=1) # (batch_size, num_tokens+1)\n",
"\n",
" return idx"
]
},
{
"cell_type": "code",
"execution_count": 41,
"id": "aa2a0d7d-0457-42d1-ab9d-bd67683e7ed8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Output text:\n",
" Every effort moves you stand to work on surprise, a one of us had gone with random-\n"
]
}
],
"source": [
"torch.manual_seed(123)\n",
"\n",
"token_ids = generate(\n",
" model=model,\n",
" idx=text_to_token_ids(\"Every effort moves you\", tokenizer),\n",
" max_new_tokens=15,\n",
" context_size=GPT_CONFIG_124M[\"context_length\"],\n",
" top_k=25,\n",
" temperature=1.4\n",
")\n",
"\n",
"print(\"Output text:\\n\", token_ids_to_text(token_ids, tokenizer))"
]
},
{
"cell_type": "markdown",
"id": "4e2002ca-f4c1-48af-9e0a-88bfc163ba0b",
"metadata": {},
"source": [
"## 5.4 Loading and saving model weights in PyTorch"
]
},
{
"cell_type": "markdown",
"id": "0fc52676-f026-4566-a226-2a90269f9d53",
"metadata": {},
"source": [
"- Training LLMs is computationally expensive, so it's crucial to be able to save and load LLM weights\n",
"\n",
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/mental-model-3.webp\" width=400px>"
]
},
{
"cell_type": "markdown",
"id": "10e4c7f9-592f-43d6-a00e-598fa01dfb82",
"metadata": {},
"source": [
"- The recommended way in PyTorch is to save the model weights, the so-called `state_dict` via by applying the `torch.save` function to the `.state_dict()` method:"
]
},
{
"cell_type": "code",
"execution_count": 42,
"id": "3d67d869-ac04-4382-bcfb-c96d1ca80d47",
"metadata": {},
"outputs": [],
"source": [
"torch.save(model.state_dict(), \"model.pth\")"
]
},
{
"cell_type": "markdown",
"id": "90e889e0-07bf-43e5-8f92-5c5c7aeaad9e",
"metadata": {},
"source": [
"- Then we can load the model weights into a new `GPTModel` model instance as follows:"
]
},
{
"cell_type": "code",
"execution_count": 43,
"id": "9d57d914-60a3-47f1-b499-5352f4c457cb",
"metadata": {},
"outputs": [],
"source": [
"model = GPTModel(GPT_CONFIG_124M)\n",
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
"model.load_state_dict(torch.load(\"model.pth\", map_location=device, weights_only=True))\n",
"model.eval();"
]
},
{
"cell_type": "markdown",
"id": "caa81aec-9c72-4f46-8ae2-4a4fde3edbc1",
"metadata": {},
"source": [
"- It's common to train LLMs with adaptive optimizers like Adam or AdamW instead of regular SGD\n",
"- These adaptive optimizers store additional parameters for each model weight, so it makes sense to save them as well in case we plan to continue the pretraining later:"
]
},
{
"cell_type": "code",
"execution_count": 44,
"id": "bbd175bb-edf4-450e-a6de-d3e8913c6532",
"metadata": {},
"outputs": [],
"source": [
"torch.save({\n",
" \"model_state_dict\": model.state_dict(),\n",
" \"optimizer_state_dict\": optimizer.state_dict(),\n",
" }, \n",
" \"model_and_optimizer.pth\"\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 45,
"id": "8a0c7295-c822-43bf-9286-c45abc542868",
"metadata": {},
"outputs": [],
"source": [
"checkpoint = torch.load(\"model_and_optimizer.pth\", weights_only=True)\n",
"\n",
"model = GPTModel(GPT_CONFIG_124M)\n",
"model.load_state_dict(checkpoint[\"model_state_dict\"])\n",
"\n",
"optimizer = torch.optim.AdamW(model.parameters(), lr=0.0005, weight_decay=0.1)\n",
"optimizer.load_state_dict(checkpoint[\"optimizer_state_dict\"])\n",
"model.train();"
]
},
{
"cell_type": "markdown",
"id": "4194350e-0409-4a63-8ffd-d3a896509032",
"metadata": {},
"source": [
"## 5.5 Loading pretrained weights from OpenAI"
]
},
{
"cell_type": "markdown",
"id": "83eb6c38-7278-40e0-bd9f-8a2b1feac3ec",
"metadata": {},
"source": [
"- Previously, we only trained a small GPT-2 model using a very small short-story book for educational purposes\n",
"- Interested readers can also find a longer pretraining run on the complete Project Gutenberg book corpus in [../03_bonus_pretraining_on_gutenberg](../03_bonus_pretraining_on_gutenberg)\n",
"- Fortunately, we don't have to spend tens to hundreds of thousands of dollars to pretrain the model on a large pretraining corpus but can load the pretrained weights provided by OpenAI"
]
},
{
"cell_type": "markdown",
"id": "127ddbdb-3878-4669-9a39-d231fbdfb834",
"metadata": {},
"source": [
"---\n",
"\n",
"---\n",
"\n",
"\n",
"⚠️ **Note: Some users may encounter issues in this section due to TensorFlow compatibility problems, particularly on certain Windows systems. TensorFlow is required here only to load the original OpenAI GPT-2 weight files, which we then convert to PyTorch.\n",
"If you're running into TensorFlow-related issues, you can use the alternative code below instead of the remaining code in this section.\n",
"This alternative is based on pre-converted PyTorch weights, created using the same conversion process described in the previous section. For details, refer to the notebook:\n",
"[../02_alternative_weight_loading/weight-loading-pytorch.ipynb](../02_alternative_weight_loading/weight-loading-pytorch.ipynb) notebook.**\n",
"\n",
"```python\n",
"file_name = \"gpt2-small-124M.pth\"\n",
"# file_name = \"gpt2-medium-355M.pth\"\n",
"# file_name = \"gpt2-large-774M.pth\"\n",
"# file_name = \"gpt2-xl-1558M.pth\"\n",
"\n",
"url = f\"https://huggingface.co/rasbt/gpt2-from-scratch-pytorch/resolve/main/{file_name}\"\n",
"\n",
"if not os.path.exists(file_name):\n",
" urllib.request.urlretrieve(url, file_name)\n",
" print(f\"Downloaded to {file_name}\")\n",
"\n",
"gpt = GPTModel(BASE_CONFIG)\n",
"gpt.load_state_dict(torch.load(file_name, weights_only=True))\n",
"gpt.eval()\n",
"\n",
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
"gpt.to(device);\n",
"\n",
"\n",
"torch.manual_seed(123)\n",
"\n",
"token_ids = generate(\n",
" model=gpt,\n",
" idx=text_to_token_ids(\"Every effort moves you\", tokenizer).to(device),\n",
" max_new_tokens=25,\n",
" context_size=NEW_CONFIG[\"context_length\"],\n",
" top_k=50,\n",
" temperature=1.5\n",
")\n",
"\n",
"print(\"Output text:\\n\", token_ids_to_text(token_ids, tokenizer))\n",
"```\n",
"\n",
"---\n",
"\n",
"---"
]
},
{
"cell_type": "markdown",
"id": "75cab892-a165-4f43-9601-f517bc212ab6",
"metadata": {},
"source": [
"- First, some boilerplate code to download the files from OpenAI and load the weights into Python\n",
"- Since OpenAI used [TensorFlow](https://www.tensorflow.org/), we will have to install and use TensorFlow for loading the weights; [tqdm](https://github.com/tqdm/tqdm) is a progress bar library\n",
"- Uncomment and run the next cell to install the required libraries"
]
},
{
"cell_type": "code",
"execution_count": 46,
"id": "fb9fdf02-972a-444e-bf65-8ffcaaf30ce8",
"metadata": {},
"outputs": [],
"source": [
"# pip install tensorflow tqdm"
]
},
{
"cell_type": "code",
"execution_count": 47,
"id": "a0747edc-559c-44ef-a93f-079d60227e3f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"TensorFlow version: 2.18.0\n",
"tqdm version: 4.67.1\n"
]
}
],
"source": [
"print(\"TensorFlow version:\", version(\"tensorflow\"))\n",
"print(\"tqdm version:\", version(\"tqdm\"))"
]
},
{
"cell_type": "code",
"execution_count": 48,
"id": "c5bc89eb-4d39-4287-9b0c-e459ebe7f5ed",
"metadata": {},
"outputs": [],
"source": [
"# Relative import from the gpt_download.py contained in this folder\n",
"\n",
"from gpt_download import download_and_load_gpt2\n",
"# Alternatively:\n",
"# from llms_from_scratch.ch05 import download_and_load_gpt2"
]
},
{
"cell_type": "markdown",
"id": "ff76a736-6f9f-4328-872e-f89a7b70a2cc",
"metadata": {},
"source": [
"---\n",
"\n",
"**Note**\n",
"\n",
"- In very rare cases, the code cell above may result in a `zsh: illegal hardware instruction python` error, which could be due to a TensorFlow installation issue on your machine\n",
"- A reader found that installing TensorFlow via `conda` solved the issue in this specific case, as mentioned [here](https://github.com/rasbt/LLMs-from-scratch/discussions/273#discussioncomment-12367888)\n",
"- You can find more instructions in this supplementary [Python setup tutorial](https://github.com/rasbt/LLMs-from-scratch/tree/main/setup/01_optional-python-setup-preferences#option-2-using-conda)\n",
"\n",
"---\n",
"\n",
"- We can then download the model weights for the 124 million parameter model as follows:"
]
},
{
"cell_type": "code",
"execution_count": 49,
"id": "76271dd7-108d-4f5b-9c01-6ae0aac4b395",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"checkpoint: 100%|████████████████████████████████████████████████████████████████████████████████| 77.0/77.0 [00:00<00:00, 63.1kiB/s]\n",
"encoder.json: 100%|████████████████████████████████████████████████████████████████████████████| 1.04M/1.04M [00:00<00:00, 4.69MiB/s]\n",
"hparams.json: 100%|██████████████████████████████████████████████████████████████████████████████| 90.0/90.0 [00:00<00:00, 59.7kiB/s]\n",
"model.ckpt.data-00000-of-00001: 100%|████████████████████████████████████████████████████████████| 498M/498M [01:09<00:00, 7.15MiB/s]\n",
"model.ckpt.index: 100%|████████████████████████████████████████████████████████████████████████| 5.21k/5.21k [00:00<00:00, 2.32MiB/s]\n",
"model.ckpt.meta: 100%|███████████████████████████████████████████████████████████████████████████| 471k/471k [00:00<00:00, 2.19MiB/s]\n",
"vocab.bpe: 100%|█████████████████████████████████████████████████████████████████████████████████| 456k/456k [00:00<00:00, 3.47MiB/s]\n"
]
}
],
"source": [
"settings, params = download_and_load_gpt2(model_size=\"124M\", models_dir=\"gpt2\")"
]
},
{
"cell_type": "code",
"execution_count": 50,
"id": "b1a31951-d971-4a6e-9c43-11ee1168ec6a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Settings: {'n_vocab': 50257, 'n_ctx': 1024, 'n_embd': 768, 'n_head': 12, 'n_layer': 12}\n"
]
}
],
"source": [
"print(\"Settings:\", settings)"
]
},
{
"cell_type": "code",
"execution_count": 51,
"id": "857c8331-130e-46ba-921d-fa35d7a73cfe",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Parameter dictionary keys: dict_keys(['blocks', 'b', 'g', 'wpe', 'wte'])\n"
]
}
],
"source": [
"print(\"Parameter dictionary keys:\", params.keys())"
]
},
{
"cell_type": "code",
"execution_count": 52,
"id": "c48dac94-8562-4a66-84ef-46c613cdc4cd",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[-0.11010301 -0.03926672 0.03310751 ... -0.1363697 0.01506208\n",
" 0.04531523]\n",
" [ 0.04034033 -0.04861503 0.04624869 ... 0.08605453 0.00253983\n",
" 0.04318958]\n",
" [-0.12746179 0.04793796 0.18410145 ... 0.08991534 -0.12972379\n",
" -0.08785918]\n",
" ...\n",
" [-0.04453601 -0.05483596 0.01225674 ... 0.10435229 0.09783269\n",
" -0.06952604]\n",
" [ 0.1860082 0.01665728 0.04611587 ... -0.09625227 0.07847701\n",
" -0.02245961]\n",
" [ 0.05135201 -0.02768905 0.0499369 ... 0.00704835 0.15519823\n",
" 0.12067825]]\n",
"Token embedding weight tensor dimensions: (50257, 768)\n"
]
}
],
"source": [
"print(params[\"wte\"])\n",
"print(\"Token embedding weight tensor dimensions:\", params[\"wte\"].shape)"
]
},
{
"cell_type": "markdown",
"id": "466e100c-294e-4afc-a70a-2f398ac4c104",
"metadata": {},
"source": [
"- Alternatively, \"355M\", \"774M\", and \"1558M\" are also supported `model_size` arguments\n",
"- The difference between these differently sized models is summarized in the figure below:"
]
},
{
"cell_type": "markdown",
"id": "20f19d32-5aae-4176-9f86-f391672c8f0d",
"metadata": {},
"source": [
"<img src=\"https://sebastianraschka.com/images/LLMs-from-scratch-images/ch05_compressed/gpt-sizes.webp?timestamp=123\" width=500px>"
]
},
{
"cell_type": "markdown",
"id": "ea6e5076-f08d-41fc-bd8b-1cfe53538f41",
"metadata": {},
"source": [
"- Above, we loaded the 124M GPT-2 model weights into Python, however we still need to transfer them into our `GPTModel` instance\n",
"- First, we initialize a new GPTModel instance\n",
"- Note that the original GPT model initialized the linear layers for the query, key, and value matrices in the multi-head attention module with bias vectors, which is not required or recommended; however, to be able to load the weights correctly, we have to enable these too by setting `qkv_bias` to `True` in our implementation, too\n",
"- We are also using the `1024` token context length that was used by the original GPT-2 model(s)"
]
},
{
"cell_type": "code",
"execution_count": 53,
"id": "9fef90dd-0654-4667-844f-08e28339ef7d",
"metadata": {},
"outputs": [],
"source": [
"# Define model configurations in a dictionary for compactness\n",
"model_configs = {\n",
" \"gpt2-small (124M)\": {\"emb_dim\": 768, \"n_layers\": 12, \"n_heads\": 12},\n",
" \"gpt2-medium (355M)\": {\"emb_dim\": 1024, \"n_layers\": 24, \"n_heads\": 16},\n",
" \"gpt2-large (774M)\": {\"emb_dim\": 1280, \"n_layers\": 36, \"n_heads\": 20},\n",
" \"gpt2-xl (1558M)\": {\"emb_dim\": 1600, \"n_layers\": 48, \"n_heads\": 25},\n",
"}\n",
"\n",
"# Copy the base configuration and update with specific model settings\n",
"model_name = \"gpt2-small (124M)\" # Example model name\n",
"NEW_CONFIG = GPT_CONFIG_124M.copy()\n",
"NEW_CONFIG.update(model_configs[model_name])\n",
"NEW_CONFIG.update({\"context_length\": 1024, \"qkv_bias\": True})\n",
"\n",
"gpt = GPTModel(NEW_CONFIG)\n",
"gpt.eval();"
]
},
{
"cell_type": "markdown",
"id": "272f29ac-8342-4b3d-a57d-9b0166ced314",
"metadata": {},
"source": [
"- The next task is to assign the OpenAI weights to the corresponding weight tensors in our `GPTModel` instance"
]
},
{
"cell_type": "code",
"execution_count": 54,
"id": "f9a92229-c002-49a6-8cfb-248297ad8296",
"metadata": {},
"outputs": [],
"source": [
"def assign(left, right):\n",
" if left.shape != right.shape:\n",
" raise ValueError(f\"Shape mismatch. Left: {left.shape}, Right: {right.shape}\")\n",
" return torch.nn.Parameter(torch.tensor(right))"
]
},
{
"cell_type": "code",
"execution_count": 55,
"id": "f22d5d95-ca5a-425c-a9ec-fc432a12d4e9",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"def load_weights_into_gpt(gpt, params):\n",
" gpt.pos_emb.weight = assign(gpt.pos_emb.weight, params['wpe'])\n",
" gpt.tok_emb.weight = assign(gpt.tok_emb.weight, params['wte'])\n",
" \n",
" for b in range(len(params[\"blocks\"])):\n",
" q_w, k_w, v_w = np.split(\n",
" (params[\"blocks\"][b][\"attn\"][\"c_attn\"])[\"w\"], 3, axis=-1)\n",
" gpt.trf_blocks[b].att.W_query.weight = assign(\n",
" gpt.trf_blocks[b].att.W_query.weight, q_w.T)\n",
" gpt.trf_blocks[b].att.W_key.weight = assign(\n",
" gpt.trf_blocks[b].att.W_key.weight, k_w.T)\n",
" gpt.trf_blocks[b].att.W_value.weight = assign(\n",
" gpt.trf_blocks[b].att.W_value.weight, v_w.T)\n",
"\n",
" q_b, k_b, v_b = np.split(\n",
" (params[\"blocks\"][b][\"attn\"][\"c_attn\"])[\"b\"], 3, axis=-1)\n",
" gpt.trf_blocks[b].att.W_query.bias = assign(\n",
" gpt.trf_blocks[b].att.W_query.bias, q_b)\n",
" gpt.trf_blocks[b].att.W_key.bias = assign(\n",
" gpt.trf_blocks[b].att.W_key.bias, k_b)\n",
" gpt.trf_blocks[b].att.W_value.bias = assign(\n",
" gpt.trf_blocks[b].att.W_value.bias, v_b)\n",
"\n",
" gpt.trf_blocks[b].att.out_proj.weight = assign(\n",
" gpt.trf_blocks[b].att.out_proj.weight, \n",
" params[\"blocks\"][b][\"attn\"][\"c_proj\"][\"w\"].T)\n",
" gpt.trf_blocks[b].att.out_proj.bias = assign(\n",
" gpt.trf_blocks[b].att.out_proj.bias, \n",
" params[\"blocks\"][b][\"attn\"][\"c_proj\"][\"b\"])\n",
"\n",
" gpt.trf_blocks[b].ff.layers[0].weight = assign(\n",
" gpt.trf_blocks[b].ff.layers[0].weight, \n",
" params[\"blocks\"][b][\"mlp\"][\"c_fc\"][\"w\"].T)\n",
" gpt.trf_blocks[b].ff.layers[0].bias = assign(\n",
" gpt.trf_blocks[b].ff.layers[0].bias, \n",
" params[\"blocks\"][b][\"mlp\"][\"c_fc\"][\"b\"])\n",
" gpt.trf_blocks[b].ff.layers[2].weight = assign(\n",
" gpt.trf_blocks[b].ff.layers[2].weight, \n",
" params[\"blocks\"][b][\"mlp\"][\"c_proj\"][\"w\"].T)\n",
" gpt.trf_blocks[b].ff.layers[2].bias = assign(\n",
" gpt.trf_blocks[b].ff.layers[2].bias, \n",
" params[\"blocks\"][b][\"mlp\"][\"c_proj\"][\"b\"])\n",
"\n",
" gpt.trf_blocks[b].norm1.scale = assign(\n",
" gpt.trf_blocks[b].norm1.scale, \n",
" params[\"blocks\"][b][\"ln_1\"][\"g\"])\n",
" gpt.trf_blocks[b].norm1.shift = assign(\n",
" gpt.trf_blocks[b].norm1.shift, \n",
" params[\"blocks\"][b][\"ln_1\"][\"b\"])\n",
" gpt.trf_blocks[b].norm2.scale = assign(\n",
" gpt.trf_blocks[b].norm2.scale, \n",
" params[\"blocks\"][b][\"ln_2\"][\"g\"])\n",
" gpt.trf_blocks[b].norm2.shift = assign(\n",
" gpt.trf_blocks[b].norm2.shift, \n",
" params[\"blocks\"][b][\"ln_2\"][\"b\"])\n",
"\n",
" gpt.final_norm.scale = assign(gpt.final_norm.scale, params[\"g\"])\n",
" gpt.final_norm.shift = assign(gpt.final_norm.shift, params[\"b\"])\n",
" gpt.out_head.weight = assign(gpt.out_head.weight, params[\"wte\"])\n",
" \n",
" \n",
"load_weights_into_gpt(gpt, params)\n",
"gpt.to(device);"
]
},
{
"cell_type": "markdown",
"id": "4f7472cb-54dc-4311-96d8-b2694f885cee",
"metadata": {},
"source": [
"- If the model is loaded correctly, we can use it to generate new text using our previous `generate` function:"
]
},
{
"cell_type": "code",
"execution_count": 56,
"id": "1f690253-f845-4347-b7b6-43fabbd2affa",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Output text:\n",
" Every effort moves you toward finding an ideal new way to practice something!\n",
"\n",
"What makes us want to be on top of that?\n",
"\n",
"\n"
]
}
],
"source": [
"torch.manual_seed(123)\n",
"\n",
"token_ids = generate(\n",
" model=gpt,\n",
" idx=text_to_token_ids(\"Every effort moves you\", tokenizer).to(device),\n",
" max_new_tokens=25,\n",
" context_size=NEW_CONFIG[\"context_length\"],\n",
" top_k=50,\n",
" temperature=1.5\n",
")\n",
"\n",
"print(\"Output text:\\n\", token_ids_to_text(token_ids, tokenizer))"
]
},
{
"cell_type": "markdown",
"id": "6d079f98-a7c4-462e-8416-5a64f670861c",
"metadata": {},
"source": [
"- We know that we loaded the model weights correctly because the model can generate coherent text; if we made even a small mistake, the model would not be able to do that"
]
},
{
"cell_type": "markdown",
"id": "28493b9b-a1ae-4f31-87bc-c10ee4447f44",
"metadata": {},
"source": [
"- For an alternative way to load the weights from the Hugging Face Hub, see [../02_alternative_weight_loading](../02_alternative_weight_loading)\n",
"- If you are interested in seeing how the GPT architecture compares to the Llama architecture (a popular LLM developed by Meta AI), see the bonus content at [../07_gpt_to_llama](../07_gpt_to_llama)"
]
},
{
"cell_type": "markdown",
"id": "f2a66474-230d-4180-a8ff-843e04f1f1c4",
"metadata": {},
"source": [
"## Summary and takeaways"
]
},
{
"cell_type": "markdown",
"id": "fc7ed189-a633-458c-bf12-4f70b42684b8",
"metadata": {},
"source": [
"- See the [./gpt_train.py](./gpt_train.py) script, a self-contained script for training\n",
"- The [./gpt_generate.py](./gpt_generate.py) script loads pretrained weights from OpenAI and generates text based on a prompt\n",
"- You can find the exercise solutions in [./exercise-solutions.ipynb](./exercise-solutions.ipynb)"
]
}
],
"metadata": {
"accelerator": "GPU",
"colab": {
"gpuType": "A100",
"machine_shape": "hm",
"provenance": []
},
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.16"
}
},
"nbformat": 4,
"nbformat_minor": 5
}