{ "cells": [ { "cell_type": "markdown", "id": "c024bfa4-1a7a-4751-b5a1-827225a3478b", "metadata": { "id": "c024bfa4-1a7a-4751-b5a1-827225a3478b" }, "source": [ "\n", "\n", "\n", "\n", "\n", "
\n", "\n", "Supplementary code for the Build a Large Language Model From Scratch book by Sebastian Raschka
\n", "
Code repository: https://github.com/rasbt/LLMs-from-scratch\n", "
汉化的库: https://github.com/GoatCsu/CN-LLMs-from-scratch.git\n", "
\n", "
\n", "\n", "
\n" ] }, { "cell_type": "markdown", "id": "bfabadb8-5935-45ff-b39c-db7a29012129", "metadata": { "id": "bfabadb8-5935-45ff-b39c-db7a29012129" }, "source": [ "# 第 6 章:进行文本分类的微调" ] }, { "cell_type": "code", "execution_count": 1, "id": "5b7e01c2-1c84-4f2a-bb51-2e0b74abda90", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "5b7e01c2-1c84-4f2a-bb51-2e0b74abda90", "outputId": "9495f150-9d79-4910-d6e7-6c0d9aae4a41" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "matplotlib version: 3.7.2\n", "numpy version: 1.24.3\n" ] }, { "ename": "PackageNotFoundError", "evalue": "No package metadata was found for tiktoken", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mStopIteration\u001b[0m Traceback (most recent call last)", "File \u001b[0;32m~/Work/anaconda3/lib/python3.11/importlib/metadata/__init__.py:563\u001b[0m, in \u001b[0;36mDistribution.from_name\u001b[0;34m(cls, name)\u001b[0m\n\u001b[1;32m 562\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 563\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mnext\u001b[39m(\u001b[38;5;28mcls\u001b[39m\u001b[38;5;241m.\u001b[39mdiscover(name\u001b[38;5;241m=\u001b[39mname))\n\u001b[1;32m 564\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mStopIteration\u001b[39;00m:\n", "\u001b[0;31mStopIteration\u001b[0m: ", "\nDuring handling of the above exception, another exception occurred:\n", "\u001b[0;31mPackageNotFoundError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[1], line 11\u001b[0m\n\u001b[1;32m 3\u001b[0m pkgs \u001b[38;5;241m=\u001b[39m [\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmatplotlib\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 4\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnumpy\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 5\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtiktoken\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mpandas\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;66;03m# 用于加载数据集的库\u001b[39;00m\n\u001b[1;32m 9\u001b[0m ]\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m p \u001b[38;5;129;01min\u001b[39;00m pkgs:\n\u001b[0;32m---> 11\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mp\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m version: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mversion(p)\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n", "File \u001b[0;32m~/Work/anaconda3/lib/python3.11/importlib/metadata/__init__.py:1008\u001b[0m, in \u001b[0;36mversion\u001b[0;34m(distribution_name)\u001b[0m\n\u001b[1;32m 1001\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mversion\u001b[39m(distribution_name):\n\u001b[1;32m 1002\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Get the version string for the named package.\u001b[39;00m\n\u001b[1;32m 1003\u001b[0m \n\u001b[1;32m 1004\u001b[0m \u001b[38;5;124;03m :param distribution_name: The name of the distribution package to query.\u001b[39;00m\n\u001b[1;32m 1005\u001b[0m \u001b[38;5;124;03m :return: The version string for the package as defined in the package's\u001b[39;00m\n\u001b[1;32m 1006\u001b[0m \u001b[38;5;124;03m \"Version\" metadata key.\u001b[39;00m\n\u001b[1;32m 1007\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 1008\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m distribution(distribution_name)\u001b[38;5;241m.\u001b[39mversion\n", "File \u001b[0;32m~/Work/anaconda3/lib/python3.11/importlib/metadata/__init__.py:981\u001b[0m, in \u001b[0;36mdistribution\u001b[0;34m(distribution_name)\u001b[0m\n\u001b[1;32m 975\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdistribution\u001b[39m(distribution_name):\n\u001b[1;32m 976\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Get the ``Distribution`` instance for the named package.\u001b[39;00m\n\u001b[1;32m 977\u001b[0m \n\u001b[1;32m 978\u001b[0m \u001b[38;5;124;03m :param distribution_name: The name of the distribution package as a string.\u001b[39;00m\n\u001b[1;32m 979\u001b[0m \u001b[38;5;124;03m :return: A ``Distribution`` instance (or subclass thereof).\u001b[39;00m\n\u001b[1;32m 980\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 981\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m Distribution\u001b[38;5;241m.\u001b[39mfrom_name(distribution_name)\n", "File \u001b[0;32m~/Work/anaconda3/lib/python3.11/importlib/metadata/__init__.py:565\u001b[0m, in \u001b[0;36mDistribution.from_name\u001b[0;34m(cls, name)\u001b[0m\n\u001b[1;32m 563\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mnext\u001b[39m(\u001b[38;5;28mcls\u001b[39m\u001b[38;5;241m.\u001b[39mdiscover(name\u001b[38;5;241m=\u001b[39mname))\n\u001b[1;32m 564\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mStopIteration\u001b[39;00m:\n\u001b[0;32m--> 565\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m PackageNotFoundError(name)\n", "\u001b[0;31mPackageNotFoundError\u001b[0m: No package metadata was found for tiktoken" ] } ], "source": [ "from importlib.metadata import version\n", "\n", "pkgs = [\"matplotlib\",\n", " \"numpy\",\n", " \"tiktoken\",\n", " \"torch\",\n", " \"tensorflow\", # 用于加载OpenAI的预训练权重的TensorFlow库\n", " \"pandas\" # 用于加载数据集的库\n", " ]\n", "for p in pkgs:\n", " print(f\"{p} version: {version(p)}\")\n", "#查看版本号Ω" ] }, { "cell_type": "markdown", "id": "a445828a-ff10-4efa-9f60-a2e2aed4c87d", "metadata": {}, "source": [ "" ] }, { "cell_type": "code", "execution_count": null, "id": "946c3e56-b04b-4b0f-b35f-b485ce5b28df", "metadata": {}, "outputs": [], "source": [ "# 防止某些单元格执行两次\n", "from IPython.core.magic import register_line_cell_magic\n", "\n", "executed_cells = set()\n", "#使用一个 set 数据结构来存储已经执行过的单元格标识符\n", "@register_line_cell_magic\n", "#注册了一个名为 run_once 的魔法命令\n", "def run_once(line, cell):\n", " if line not in executed_cells:\n", " get_ipython().run_cell(cell)\n", " executed_cells.add(line)\n", " else:\n", " print(f\"Cell '{line}' has already been executed.\")" ] }, { "cell_type": "markdown", "id": "3a84cf35-b37f-4c15-8972-dfafc9fadc1c", "metadata": { "id": "3a84cf35-b37f-4c15-8972-dfafc9fadc1c" }, "source": [ "## 6.1 不同类型的微调" ] }, { "cell_type": "markdown", "id": "ede3d731-5123-4f02-accd-c670ce50a5a3", "metadata": { "id": "ede3d731-5123-4f02-accd-c670ce50a5a3" }, "source": [ "- 本章节没有代码" ] }, { "cell_type": "markdown", "id": "ac45579d-d485-47dc-829e-43be7f4db57b", "metadata": {}, "source": [ "- 常见微调大语言模型的方法包括:指令微调( instructionfinetuning)和分类微调(classification finetuning)\n", "- 如下是:指令微调,也是下一章节所要讲的内容" ] }, { "cell_type": "markdown", "id": "6c29ef42-46d9-43d4-8bb4-94974e1665e4", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "a7f60321-95b8-46a9-97bf-1d07fda2c3dd", "metadata": {}, "source": [ "- 如果您具有机器学习的背景,对于分类微调您可能已经熟悉. 举个例子,分类微调类似于训练卷积网络来对手写数字进行分类的过程\n", "- 在分类微调中,模型可以输出特定的分类标签(例如,“spam”和“not spam”)\n", "- 分类微调模型只能预测它在训练期间所熟知的类别标签(例如,“垃圾邮件”或“非垃圾邮件”),而指令微调模型通常可以执行更广泛的任务\n", "- 我们可以将分类微调模型视为高度专业化的模型;在实践中,开发专业化的模型通常比开发在许多不同任务上表现良好的通用模型要容易得多" ] }, { "cell_type": "markdown", "id": "0b37a0c4-0bb1-4061-b1fe-eaa4416d52c3", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "8c7017a2-32aa-4002-a2f3-12aac293ccdf", "metadata": { "id": "8c7017a2-32aa-4002-a2f3-12aac293ccdf" }, "source": [ "## 6.2 准备数据集" ] }, { "cell_type": "markdown", "id": "5f628975-d2e8-4f7f-ab38-92bb868b7067", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "9fbd459f-63fa-4d8c-8499-e23103156c7d", "metadata": { "id": "9fbd459f-63fa-4d8c-8499-e23103156c7d" }, "source": [ "- 本节准备我们用于分类微调的数据集\n", "- 我们使用由垃圾邮件和非垃圾邮件组成的数据集来对 LLM 进行分类微调\n", "- 首先,我们下载并解压缩数据集" ] }, { "cell_type": "code", "execution_count": null, "id": "def7c09b-af9c-4216-90ce-5e67aed1065c", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "def7c09b-af9c-4216-90ce-5e67aed1065c", "outputId": "424e4423-f623-443c-ab9e-656f9e867559" }, "outputs": [], "source": [ "import urllib.request\n", "import zipfile\n", "import os\n", "from pathlib import Path\n", "\n", "url = \"https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip\"\n", "zip_path = \"sms_spam_collection.zip\"\n", "extracted_path = \"sms_spam_collection\"\n", "data_file_path = Path(extracted_path) / \"SMSSpamCollection.tsv\"\n", "#完成导入数据\n", "def download_and_unzip_spam_data(url, zip_path, extracted_path, data_file_path):\n", " if data_file_path.exists():\n", " print(f\"{data_file_path} already exists. Skipping download and extraction.\")\n", " return\n", "\n", " # 下载文件\n", " with urllib.request.urlopen(url) as response:\n", " with open(zip_path, \"wb\") as out_file:\n", " out_file.write(response.read())\n", "\n", " # 解压文件\n", " with zipfile.ZipFile(zip_path, \"r\") as zip_ref:\n", " zip_ref.extractall(extracted_path)\n", " # 添加.tsv文件扩展名\n", " original_file_path = Path(extracted_path) / \"SMSSpamCollection\"\n", " os.rename(original_file_path, data_file_path)\n", " print(f\"File downloaded and saved as {data_file_path}\")\n", "\n", "download_and_unzip_spam_data(url, zip_path, extracted_path, data_file_path)" ] }, { "cell_type": "markdown", "id": "6aac2d19-06d0-4005-916b-0bd4b1ee50d1", "metadata": { "id": "6aac2d19-06d0-4005-916b-0bd4b1ee50d1" }, "source": [ "- 数据集保存为一个以制表符分隔的文本文件SMSSpamCollection.tsv,位于 SMSSpamCollection 文件夹中。我们可以使用以下代码将其加载到 pandas DataFrame 中" ] }, { "cell_type": "code", "execution_count": null, "id": "da0ed4da-ac31-4e4d-8bdd-2153be4656a4", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 423 }, "id": "da0ed4da-ac31-4e4d-8bdd-2153be4656a4", "outputId": "a16c5cde-d341-4887-a93f-baa9bec542ab" }, "outputs": [], "source": [ "import pandas as pd\n", "\n", "df = pd.read_csv(data_file_path, sep=\"\\t\", header=None, names=[\"Label\", \"Text\"])\n", "df" ] }, { "cell_type": "markdown", "id": "e7b6e631-4f0b-4aab-82b9-8898e6663109", "metadata": { "id": "e7b6e631-4f0b-4aab-82b9-8898e6663109" }, "source": [ "- 执行如下代码来查看数据集中的数据类别分布,会发现数据中“非垃圾消息(ham)”的出现频率远高于“垃圾消息(spam)”" ] }, { "cell_type": "code", "execution_count": null, "id": "495a5280-9d7c-41d4-9719-64ab99056d4c", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "495a5280-9d7c-41d4-9719-64ab99056d4c", "outputId": "761e0482-43ba-4f46-f4b7-6774dae51b38" }, "outputs": [], "source": [ "print(df[\"Label\"].value_counts())" ] }, { "cell_type": "markdown", "id": "f773f054-0bdc-4aad-bbf6-397621bf63db", "metadata": { "id": "f773f054-0bdc-4aad-bbf6-397621bf63db" }, "source": [ "- 为了简化处理,并且出于教学目的考虑,我们选择使用小规模数据集,这有助于更快地对大语言模型进行微调。因此,我们对数据集进行了下采样,使每个类别包含 747 个实例。\n", "- (除了下采样之外,还有其他几种方法可以处理类别不平衡,但这些超出了本书的范围; 你可以在 imbalanced-learn 中找到示例和更多信息)[`imbalanced-learn` 用户指南](https://imbalanced-learn.org/stable/user_guide.html)" ] }, { "cell_type": "code", "execution_count": null, "id": "7be4a0a2-9704-4a96-b38f-240339818688", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "7be4a0a2-9704-4a96-b38f-240339818688", "outputId": "396dc415-cb71-4a88-e85d-d88201c6d73f" }, "outputs": [], "source": [ "%%run_once balance_df\n", "#单元模型启动\n", "\n", "def create_balanced_dataset(df):\n", " \n", " num_spam = df[df[\"Label\"] == \"spam\"].shape[0]\n", " #算一下类别为spam的样本出现的次数\n", " ham_subset = df[df[\"Label\"] == \"ham\"].sample(num_spam, random_state=123)\n", " #随机采样 “ham” 使其数量与“spam”样本数量一致\n", " balanced_df = pd.concat([ham_subset, df[df[\"Label\"] == \"spam\"]])\n", " #合并 “spam” 和采样后的 “ham”数据\n", " return balanced_df\n", "\n", "\n", "balanced_df = create_balanced_dataset(df)\n", "print(balanced_df[\"Label\"].value_counts())" ] }, { "cell_type": "markdown", "id": "d3fd2f5a-06d8-4d30-a2e3-230b86c559d6", "metadata": { "id": "d3fd2f5a-06d8-4d30-a2e3-230b86c559d6" }, "source": [ "- 随后我们把标签 \"ham\" 和 \"spam\" 转换为整数类标签“0”和“1”:" ] }, { "cell_type": "code", "execution_count": null, "id": "c1b10c3d-5d57-42d0-8de8-cf80a06f5ffd", "metadata": { "id": "c1b10c3d-5d57-42d0-8de8-cf80a06f5ffd" }, "outputs": [], "source": [ "%%run_once label_mapping\n", "balanced_df[\"Label\"] = balanced_df[\"Label\"].map({\"ham\": 0, \"spam\": 1}) " ] }, { "cell_type": "code", "execution_count": null, "id": "e6f7f062-ef4e-4020-8275-71990cab4414", "metadata": {}, "outputs": [], "source": [ "balanced_df" ] }, { "cell_type": "markdown", "id": "5715e685-35b4-4b45-a86c-8a8694de9d6f", "metadata": { "id": "5715e685-35b4-4b45-a86c-8a8694de9d6f" }, "source": [ "- 现在自定义一个函数,用于把数据集随机划分为训练集、验证集、测试集" ] }, { "cell_type": "code", "execution_count": null, "id": "uQl0Psdmx15D", "metadata": { "id": "uQl0Psdmx15D" }, "outputs": [], "source": [ "def random_split(df, train_frac, validation_frac):\n", " # 把数据集Dataframe打乱\n", " df = df.sample(frac=1, random_state=123).reset_index(drop=True)\n", "\n", " # 计算分割系数\n", " train_end = int(len(df) * train_frac)\n", " validation_end = train_end + int(len(df) * validation_frac)\n", "\n", " # 分割数据集Dataframe\n", " train_df = df[:train_end]\n", " validation_df = df[train_end:validation_end]\n", " test_df = df[validation_end:]\n", "\n", " return train_df, validation_df, test_df\n", "\n", "train_df, validation_df, test_df = random_split(balanced_df, 0.7, 0.1)\n", "# 剩余部分为测试集,占总数据集的比例为 0.2\n", "\n", "train_df.to_csv(\"train.csv\", index=None)\n", "validation_df.to_csv(\"validation.csv\", index=None)\n", "test_df.to_csv(\"test.csv\", index=None)\n", "#DataFrame 被随机划分为训练集、验证集和测试集并保存" ] }, { "cell_type": "markdown", "id": "a8d7a0c5-1d5f-458a-b685-3f49520b0094", "metadata": {}, "source": [ "## 6.3 创建数据加载器" ] }, { "cell_type": "markdown", "id": "7126108a-75e7-4862-b0fb-cbf59a18bb6c", "metadata": { "id": "7126108a-75e7-4862-b0fb-cbf59a18bb6c" }, "source": [ "- 由于文本消息长度随机,因此在批量化组合训练数据之前要做数据归一化,我们有两种操作可供选择\n", " 1. 将所有消息截断到数据集中最短消息的长度或批次长度\n", " 2. 将所有消息填充到数据集中最长消息的长度或批次长度\n", "\n", "- 这里我们选择操作2,填充数据\n", "- 并且,我们使用`<|endoftext|>` 作为填充标识符" ] }, { "cell_type": "markdown", "id": "0829f33f-1428-4f22-9886-7fee633b3666", "metadata": {}, "source": [ "" ] }, { "cell_type": "code", "execution_count": null, "id": "74c3c463-8763-4cc0-9320-41c7eaad8ab7", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "74c3c463-8763-4cc0-9320-41c7eaad8ab7", "outputId": "b5b48439-32c8-4b37-cca2-c9dc8fa86563" }, "outputs": [], "source": [ "import tiktoken\n", "\n", "tokenizer = tiktoken.get_encoding(\"gpt2\")\n", "# 打印<|endoftext|>对应的词元id\n", "print(tokenizer.encode(\"<|endoftext|>\", allowed_special={\"<|endoftext|>\"}))" ] }, { "cell_type": "markdown", "id": "04f582ff-68bf-450e-bd87-5fb61afe431c", "metadata": { "id": "04f582ff-68bf-450e-bd87-5fb61afe431c" }, "source": [ "- 下面我们首先将 `SpamDataset` 训练数据集中最长的序列,然后将填充token(<|endoftext|>)添加到其他序列末端以匹配该序列长度" ] }, { "cell_type": "code", "execution_count": null, "id": "d7791b52-af18-4ac4-afa9-b921068e383e", "metadata": { "id": "d7791b52-af18-4ac4-afa9-b921068e383e" }, "outputs": [], "source": [ "import torch\n", "from torch.utils.data import Dataset\n", "\n", "\n", "class SpamDataset(Dataset):\n", " def __init__(self, csv_file, tokenizer, max_length=None, pad_token_id=50256):\n", " self.data = pd.read_csv(csv_file)\n", " #先读入文本\n", " self.encoded_texts = [\n", " tokenizer.encode(text) for text in self.data[\"Text\"]\n", " ]\n", " #编码成对应的词元id\n", " if max_length is None:\n", " self.max_length = self._longest_encoded_length()\n", " else:\n", " self.max_length = max_length\n", " #如果序列长度超过 max_length,则进行截断\n", " self.encoded_texts = [\n", " encoded_text[:self.max_length]\n", " for encoded_text in self.encoded_texts\n", " ]\n", "\n", " # 填充到最长序列的长度\n", " self.encoded_texts = [\n", " encoded_text + [pad_token_id] * (self.max_length - len(encoded_text))\n", " for encoded_text in self.encoded_texts\n", " ]\n", " def __getitem__(self, index):\n", " #获取指定索引的数据样本\n", " encoded = self.encoded_texts[index]\n", " label = self.data.iloc[index][\"Label\"]\n", " return (\n", " torch.tensor(encoded, dtype=torch.long),\n", " torch.tensor(label, dtype=torch.long)\n", " )\n", " def __len__(self):\n", " return len(self.data)\n", " #输出序列长度\n", " def _longest_encoded_length(self):\n", " max_length = 0\n", " for encoded_text in self.encoded_texts:\n", " encoded_length = len(encoded_text)\n", " if encoded_length > max_length:\n", " max_length = encoded_length\n", " return max_length" ] }, { "cell_type": "code", "execution_count": null, "id": "uzj85f8ou82h", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "uzj85f8ou82h", "outputId": "d08f1cf0-c24d-445f-a3f8-793532c3716f" }, "outputs": [], "source": [ "train_dataset = SpamDataset(\n", " csv_file=\"train.csv\",\n", " max_length=None,\n", " tokenizer=tokenizer\n", ")\n", "\n", "print(train_dataset.max_length)" ] }, { "cell_type": "markdown", "id": "15bdd932-97eb-4b88-9cf9-d766ea4c3a60", "metadata": {}, "source": [ "- 我们还将验证和测试集填充到最长的训练序列\n", "- 在本节代码中,任何超过最长训练示例长度的验证集和测试集样本都将使用之前定义的 `SpamDataset` 中的代码 `encoded_text[:self.max_length]`进行截断\n", "- 此行为完全是可选的,如果我们在验证和测试集情况下都设置 `max_length=Non`,代码也能正常运行" ] }, { "cell_type": "code", "execution_count": null, "id": "bb0c502d-a75e-4248-8ea0-196e2b00c61e", "metadata": { "id": "bb0c502d-a75e-4248-8ea0-196e2b00c61e" }, "outputs": [], "source": [ "val_dataset = SpamDataset(\n", " csv_file=\"validation.csv\",\n", " max_length=train_dataset.max_length,\n", " tokenizer=tokenizer\n", ")\n", "test_dataset = SpamDataset(\n", " csv_file=\"test.csv\",\n", " max_length=train_dataset.max_length,\n", " tokenizer=tokenizer\n", ")\n", "#将验证集和测试集数据填充到最长序列的长度" ] }, { "cell_type": "markdown", "id": "20170d89-85a0-4844-9887-832f5d23432a", "metadata": {}, "source": [ "- 接下来,我们使用数据集来实例化数据加载器,这与前几章中创建数据加载器类似" ] }, { "cell_type": "markdown", "id": "64bcc349-205f-48f8-9655-95ff21f5e72f", "metadata": {}, "source": [ "" ] }, { "cell_type": "code", "execution_count": null, "id": "8681adc0-6f02-4e75-b01a-a6ab75d05542", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "8681adc0-6f02-4e75-b01a-a6ab75d05542", "outputId": "3266c410-4fdb-4a8c-a142-7f707e2525ab" }, "outputs": [], "source": [ "from torch.utils.data import DataLoader\n", "\n", "num_workers = 0\n", "batch_size = 8\n", "\n", "# 设置种子确保可复现\n", "torch.manual_seed(123)\n", "# 初始化数据加载器\n", "train_loader = DataLoader(\n", " dataset=train_dataset,\n", " batch_size=batch_size,\n", " shuffle=True,\n", " num_workers=num_workers,\n", " drop_last=True,\n", ")\n", "\n", "val_loader = DataLoader(\n", " dataset=val_dataset,\n", " batch_size=batch_size,\n", " num_workers=num_workers,\n", " drop_last=False,\n", ")\n", "\n", "test_loader = DataLoader(\n", " dataset=test_dataset,\n", " batch_size=batch_size,\n", " num_workers=num_workers,\n", " drop_last=False,\n", ")\n", "# 对齐超参数,但是dataset需要区别一下训练集、验证集和测试集" ] }, { "cell_type": "markdown", "id": "ab7335db-e0bb-4e27-80c5-eea11e593a57", "metadata": {}, "source": [ "- 作为验证步骤,我们遍历数据加载器,并确保每个批次包含 8 个训练样本,其中每个训练样本由 120 个 token 组成" ] }, { "cell_type": "code", "execution_count": null, "id": "4dee6882-4c3a-4964-af15-fa31f86ad047", "metadata": {}, "outputs": [], "source": [ "print(\"Train loader:\")\n", "for input_batch, target_batch in train_loader:\n", " pass\n", " #如果这个数据在训练集出现过,则跳过\n", "\n", "print(\"Input batch dimensions:\", input_batch.shape)\n", "print(\"Label batch dimensions\", target_batch.shape)" ] }, { "cell_type": "markdown", "id": "5cdd7947-7039-49bf-8a5e-c0a2f4281ca1", "metadata": {}, "source": [ "- 最后,让我们通过打印来查看每个数据集的批次数目" ] }, { "cell_type": "code", "execution_count": null, "id": "IZfw-TYD2zTj", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "IZfw-TYD2zTj", "outputId": "6934bbf2-9797-4fbe-d26b-1a246e18c2fb" }, "outputs": [], "source": [ "print(f\"{len(train_loader)} training batches\")\n", "print(f\"{len(val_loader)} validation batches\")\n", "print(f\"{len(test_loader)} test batches\")" ] }, { "cell_type": "markdown", "id": "d1c4f61a-5f5d-4b3b-97cf-151b617d1d6c", "metadata": { "id": "d1c4f61a-5f5d-4b3b-97cf-151b617d1d6c" }, "source": [ "## 6.4 用预训练权重初始化模型" ] }, { "cell_type": "markdown", "id": "97e1af8b-8bd1-4b44-8b8b-dc031496e208", "metadata": {}, "source": [ "- 在本节中,我们将初始化在上一章中使用的预训练模型\n", "\n", "" ] }, { "cell_type": "code", "execution_count": null, "id": "2992d779-f9fb-4812-a117-553eb790a5a9", "metadata": { "id": "2992d779-f9fb-4812-a117-553eb790a5a9" }, "outputs": [], "source": [ "CHOOSE_MODEL = \"gpt2-small (124M)\"\n", "INPUT_PROMPT = \"Every effort moves\"\n", "\n", "BASE_CONFIG = {\n", " \"vocab_size\": 50257, # 词表大小\n", " \"context_length\": 1024, # 上下文长度\n", " \"drop_rate\": 0.0, # Dropout率\n", " \"qkv_bias\": True # 查询-键-值偏置\n", "}\n", "# 输入超参数\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", "BASE_CONFIG.update(model_configs[CHOOSE_MODEL])\n", "\n", "assert train_dataset.max_length <= BASE_CONFIG[\"context_length\"], (\n", " f\"Dataset length {train_dataset.max_length} exceeds model's context \"\n", " f\"length {BASE_CONFIG['context_length']}. Reinitialize data sets with \"\n", " f\"`max_length={BASE_CONFIG['context_length']}`\"\n", ")\n", "# 断言语句,用于验证 train_dataset.max_length 是否符合模型的上下文长度(context_length)要求。\n", "# 如果不符合,将抛出一个异常的讯号" ] }, { "cell_type": "code", "execution_count": null, "id": "022a649a-44f5-466c-8a8e-326c063384f5", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "022a649a-44f5-466c-8a8e-326c063384f5", "outputId": "7091e401-8442-4f47-a1d9-ecb42a1ef930" }, "outputs": [], "source": [ "from gpt_download import download_and_load_gpt2\n", "from previous_chapters import GPTModel, load_weights_into_gpt\n", "\n", "model_size = CHOOSE_MODEL.split(\" \")[-1].lstrip(\"(\").rstrip(\")\")\n", "settings, params = download_and_load_gpt2(model_size=model_size, models_dir=\"gpt2\")\n", "# 引入超参数跟模型设置\n", "model = GPTModel(BASE_CONFIG)\n", "load_weights_into_gpt(model, params)\n", "model.eval()" ] }, { "cell_type": "markdown", "id": "ab8e056c-abe0-415f-b34d-df686204259e", "metadata": {}, "source": [ "- 为了确保模型已正确加载,让我们仔细检查它是否生成了连贯的文本" ] }, { "cell_type": "code", "execution_count": null, "id": "d8ac25ff-74b1-4149-8dc5-4c429d464330", "metadata": {}, "outputs": [], "source": [ "from previous_chapters import (\n", " generate_text_simple,\n", " text_to_token_ids,\n", " token_ids_to_text\n", ")\n", "\n", "\n", "text_1 = \"Every effort moves you\"\n", "\n", "token_ids = generate_text_simple(\n", " model=model,\n", " idx=text_to_token_ids(text_1, tokenizer),\n", " max_new_tokens=15,\n", " context_size=BASE_CONFIG[\"context_length\"]\n", ")\n", "# 生成文本\n", "print(token_ids_to_text(token_ids, tokenizer))" ] }, { "cell_type": "markdown", "id": "69162550-6a02-4ece-8db1-06c71d61946f", "metadata": {}, "source": [ "- 在我们将模型微调为分类器之前,让我们看看该模型是否已经可以通过提示对垃圾邮件进行分类" ] }, { "cell_type": "code", "execution_count": null, "id": "94224aa9-c95a-4f8a-a420-76d01e3a800c", "metadata": {}, "outputs": [], "source": [ "text_2 = (\n", " \"Is the following text 'spam'? Answer with 'yes' or 'no':\"\n", " \" 'You are a winner you have been specially\"\n", " \" selected to receive $1000 cash or a $2000 award.'\"\n", ")\n", "\n", "token_ids = generate_text_simple(\n", " model=model,\n", " idx=text_to_token_ids(text_2, tokenizer),\n", " max_new_tokens=23,\n", " context_size=BASE_CONFIG[\"context_length\"]\n", ")\n", "\n", "print(token_ids_to_text(token_ids, tokenizer))" ] }, { "cell_type": "markdown", "id": "1ce39ed0-2c77-410d-8392-dd15d4b22016", "metadata": {}, "source": [ "- 正如我们所看到的,该模型不太擅长遵循指示\n", "- 这是意料之中的,因为它只是经过了预训练,还没有经过指令微调(指令微调将在下一章中介绍)" ] }, { "cell_type": "markdown", "id": "4c9ae440-32f9-412f-96cf-fd52cc3e2522", "metadata": { "id": "4c9ae440-32f9-412f-96cf-fd52cc3e2522" }, "source": [ "## 6.5 添加分类头" ] }, { "cell_type": "markdown", "id": "d6e9d66f-76b2-40fc-9ec5-3f972a8db9c0", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "217bac05-78df-4412-bd80-612f8061c01d", "metadata": {}, "source": [ "- 在本节中,我们将修改预训练的 LLM,为分类微调做准备\n", "- 首先,让我们先看一下模型的架构" ] }, { "cell_type": "code", "execution_count": null, "id": "b23aff91-6bd0-48da-88f6-353657e6c981", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "1d8f7a01-b7c0-48d4-b1e7-8c12cc7ad932", "outputId": "b6a5b9b5-a92f-498f-d7cb-b58dd99e4497" }, "outputs": [], "source": [ "print(model)" ] }, { "cell_type": "markdown", "id": "3f640a76-dd00-4769-9bc8-1aed0cec330d", "metadata": {}, "source": [ "- 我们可以看到,在第 4 章中实现的架构\n", "- 我们的目标是替换和微调输出层\n", "- 为了实现这一点,我们首先冻结模型,这意味着我们使所有层都是不可训练的" ] }, { "cell_type": "code", "execution_count": null, "id": "fkMWFl-0etea", "metadata": { "id": "fkMWFl-0etea" }, "outputs": [], "source": [ "for param in model.parameters():\n", " param.requires_grad = False\n", "# 返回 model 中所有的参数,但是不参与反向传播" ] }, { "cell_type": "markdown", "id": "72155f83-87d9-476a-a978-a15aa2d44147", "metadata": {}, "source": [ "- 然后,我们替换输出层`(model.out_head)`,它最初将层输入映射到 50,257 个维度(词汇表的大小)\n", "- 由于我们对二元分类模型进行了微调(预测 2 个类别,“spam”和“not spam”),因此我们可以替换如下所示的输出层,默认情况下它是可训练的\n", "- 我们使用 `BASE_CONFIG[\"emb_dim\"]`(在 “gpt2-small (124M)” 模型中为 768)来保持下面的代码更通用" ] }, { "cell_type": "code", "execution_count": null, "id": "7e759fa0-0f69-41be-b576-17e5f20e04cb", "metadata": {}, "outputs": [], "source": [ "torch.manual_seed(123)\n", "\n", "num_classes = 2\n", "model.out_head = torch.nn.Linear(in_features=BASE_CONFIG[\"emb_dim\"], out_features=num_classes)" ] }, { "cell_type": "markdown", "id": "30be5475-ae77-4f97-8f3e-dec462b1339f", "metadata": {}, "source": [ "- 从技术上讲,只需训练输出层就足够了\n", "- 但是,正如在 [Finetuning Large Language Models](https://magazine.sebastianraschka.com/p/finetuning-large-language-models)中表明,微调其他层可以显著提高性能\n", "- 因此,我们还使最后一个 transformer 模块和最后一个 `LayerNorm` 模块连接起来,将最后一个 transformer 模块连接到输出层,使其可训练" ] }, { "cell_type": "markdown", "id": "0be7c1eb-c46c-4065-8525-eea1b8c66d10", "metadata": {}, "source": [ "" ] }, { "cell_type": "code", "execution_count": null, "id": "2aedc120-5ee3-48f6-92f2-ad9304ebcdc7", "metadata": { "id": "2aedc120-5ee3-48f6-92f2-ad9304ebcdc7" }, "outputs": [], "source": [ "for param in model.trf_blocks[-1].parameters():\n", " param.requires_grad = True\n", "\n", "for param in model.final_norm.parameters():\n", " param.requires_grad = True\n", "# 对于需要更新权重则设将该参数设置为True" ] }, { "cell_type": "markdown", "id": "f012b899-8284-4d3a-97c0-8a48eb33ba2e", "metadata": {}, "source": [ "- 即使我们添加了新的输出层并标记了某些层为可训练或不可训练,我们仍然可以像之前一样使用这个模型\n", "- 例如,我们输入点文本" ] }, { "cell_type": "code", "execution_count": null, "id": "f645c06a-7df6-451c-ad3f-eafb18224ebc", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "f645c06a-7df6-451c-ad3f-eafb18224ebc", "outputId": "27e041b1-d731-48a1-cf60-f22d4565304e" }, "outputs": [], "source": [ "inputs = tokenizer.encode(\"Do you have time\")\n", "inputs = torch.tensor(inputs).unsqueeze(0)\n", "print(\"Inputs:\", inputs)\n", "print(\"Inputs dimensions:\", inputs.shape)\n", "# 编码并输出" ] }, { "cell_type": "markdown", "id": "fbbf8481-772d-467b-851c-a62b86d0cb1b", "metadata": {}, "source": [ "- 与前几章相比,它现在有两个输出维度,而不再是 50,257 个" ] }, { "cell_type": "code", "execution_count": null, "id": "48dc84f1-85cc-4609-9cee-94ff539f00f4", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "48dc84f1-85cc-4609-9cee-94ff539f00f4", "outputId": "9cae7448-253d-4776-973e-0af190b06354" }, "outputs": [], "source": [ "with torch.no_grad():\n", " outputs = model(inputs)\n", "\n", "print(\"Outputs:\\n\", outputs)\n", "print(\"Outputs dimensions:\", outputs.shape) # 形状:(批次大小,词元数量)" ] }, { "cell_type": "markdown", "id": "75430a01-ef9c-426a-aca0-664689c4f461", "metadata": {}, "source": [ "- 如前几章所述,对于每个 input token,模型都会返回一个 output vector\n", "- 由于我们向模型提供了具有 4 个输入 token 的文本样本,因此输出由上面的 4 个 2 维输出向量组成" ] }, { "cell_type": "markdown", "id": "7df9144f-6817-4be4-8d4b-5d4dadfe4a9b", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "e3bb8616-c791-4f5c-bac0-5302f663e46a", "metadata": {}, "source": [ "- 在第 3 章中,我们讨论了attention机制,它将每个输入 token 连接到另一个输入 token\n", "- 在第 3 章中,我们还介绍了类 GPT 模型中使用的因果注意力掩码(causal attention mask);这种掩码让当前token只关注当前和先前出现过的位置\n", "- 基于这种因果注意力机制,第 4 个(最后一个)token在所有token中包含最多的信息,因为唯一包含所有其他全部信息只有它\n", "- 因此,我们在微调过程中格外关注这个最后的词元" ] }, { "cell_type": "code", "execution_count": null, "id": "49383a8c-41d5-4dab-98f1-238bca0c2ed7", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "49383a8c-41d5-4dab-98f1-238bca0c2ed7", "outputId": "e79eb155-fa1f-46ed-ff8c-d828c3a3fabd" }, "outputs": [], "source": [ "print(\"Last output token:\", outputs[:, -1, :])" ] }, { "cell_type": "markdown", "id": "8df08ae0-e664-4670-b7c5-8a2280d9b41b", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "32aa4aef-e1e9-491b-9adf-5aa973e59b8c", "metadata": {}, "source": [ "## 6.6 计算分类损失和准确率" ] }, { "cell_type": "markdown", "id": "669e1fd1-ace8-44b4-b438-185ed0ba8b33", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "7a7df4ee-0a34-4a4d-896d-affbbf81e0b3", "metadata": {}, "source": [ "- 在解释损失计算之前,让我们简单了解一下模型输出是如何转换为类标签的" ] }, { "cell_type": "markdown", "id": "557996dd-4c6b-49c4-ab83-f60ef7e1d69e", "metadata": {}, "source": [ "" ] }, { "cell_type": "code", "execution_count": null, "id": "c77faab1-3461-4118-866a-6171f2b89aa0", "metadata": {}, "outputs": [], "source": [ "print(\"Last output token:\", outputs[:, -1, :])" ] }, { "cell_type": "markdown", "id": "7edd71fa-628a-4d00-b81d-6d8bcb2c341d", "metadata": {}, "source": [ "- 与第 5 章类似,我们通过 `softmax` 函数将输出 (logits) 转换为概率分数,然后通过`argmax`函数获得最大概率值的索引位置" ] }, { "cell_type": "code", "execution_count": null, "id": "b81efa92-9be1-4b9e-8790-ce1fc7b17f01", "metadata": {}, "outputs": [], "source": [ "probas = torch.softmax(outputs[:, -1, :], dim=-1)\n", "label = torch.argmax(probas)\n", "print(\"Class label:\", label.item())" ] }, { "cell_type": "markdown", "id": "414a6f02-307e-4147-a416-14d115bf8179", "metadata": {}, "source": [ "- 如第 5 章所述,softmax 函数在这里是可选的,因为最大的输出对应于最大的概率分数" ] }, { "cell_type": "code", "execution_count": null, "id": "f9f9ad66-4969-4501-8239-3ccdb37e71a2", "metadata": {}, "outputs": [], "source": [ "logits = outputs[:, -1, :]\n", "label = torch.argmax(logits)\n", "print(\"Class label:\", label.item())" ] }, { "cell_type": "markdown", "id": "dcb20d3a-cbba-4ab1-8584-d94e16589505", "metadata": {}, "source": [ "- 我们可以应用这个概念来计算所谓的\"分类准确性\",计算给定数据集中正确预测的百分比\n", "- 为了计算分类准确率,我们可以将前面基于 `argmax` 的预测代码应用于数据集中的所有示例,并按如下方式计算正确预测的分数:" ] }, { "cell_type": "code", "execution_count": null, "id": "3ecf9572-aed0-4a21-9c3b-7f9f2aec5f23", "metadata": {}, "outputs": [], "source": [ "def calc_accuracy_loader(data_loader, model, device, num_batches=None):\n", " model.eval()\n", " correct_predictions, num_examples = 0, 0\n", " # 先改成评估模式,在初始化两个储存器\n", " if num_batches is None:\n", " num_batches = len(data_loader)\n", " # num_batches设为None则取全部数据\n", " else:\n", " num_batches = min(num_batches, len(data_loader))\n", " # 取num_batches和len(data_loader)中较小的那个\n", " for i, (input_batch, target_batch) in enumerate(data_loader):\n", " if i < num_batches:\n", " input_batch, target_batch = input_batch.to(device), target_batch.to(device)\n", " # 仅处理前 num_batches 批次的数据\n", " with torch.no_grad():\n", " logits = model(input_batch)[:, -1, :] # 最后一个输出词元的概率\n", " # 预测内容取概率最大值\n", " predicted_labels = torch.argmax(logits, dim=-1)\n", " # 当前批次的样本数\n", " num_examples += predicted_labels.shape[0]\n", " # 预测正确的样本数量\n", " correct_predictions += (predicted_labels == target_batch).sum().item()\n", " \n", " else:\n", " break\n", " return correct_predictions / num_examples" ] }, { "cell_type": "markdown", "id": "7165fe46-a284-410b-957f-7524877d1a1a", "metadata": {}, "source": [ "- 用该函数计算不同数据集的分类精度" ] }, { "cell_type": "code", "execution_count": null, "id": "390e5255-8427-488c-adef-e1c10ab4fb26", "metadata": {}, "outputs": [], "source": [ "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", "\n", "#Note:\n", "\n", "#取消注释以下行将允许代码在 Apple Silicon 芯片上运行(如果适用),\n", "#这比在 Apple CPU 上运行快大约 2 倍(根据 M3 MacBook Air 的测量结果)。\n", "#截至目前,在 PyTorch 2.4 版本中,通过 CPU 和 MPS 得到的结果是相同的。\n", "#然而,在 PyTorch 的早期版本中,使用 MPS 时,可能会观察到不同的结果。\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", "print(f\"Running on {device} device.\")\n", "\n", "model.to(device) # 对于 nn.Module 类,无需 model = model.to(device)赋值操作\n", "\n", "torch.manual_seed(123) # 由于训练数据加载器中的随机打乱,因此设置随机种子以确保可重复性\n", "\n", "train_accuracy = calc_accuracy_loader(train_loader, model, device, num_batches=10)\n", "val_accuracy = calc_accuracy_loader(val_loader, model, device, num_batches=10)\n", "test_accuracy = calc_accuracy_loader(test_loader, model, device, num_batches=10)\n", "#各种计算\n", "print(f\"Training accuracy: {train_accuracy*100:.2f}%\")\n", "print(f\"Validation accuracy: {val_accuracy*100:.2f}%\")\n", "print(f\"Test accuracy: {test_accuracy*100:.2f}%\")" ] }, { "cell_type": "markdown", "id": "30345e2a-afed-4d22-9486-f4010f90a871", "metadata": {}, "source": [ "- 可以看到,预测的准确性不是很好,这是因为因为我们还没有对模型进行微调" ] }, { "cell_type": "markdown", "id": "4f4a9d15-8fc7-48a2-8734-d92a2f265328", "metadata": {}, "source": [ "- 在开始微调 (/training) 之前,我们首先必须定义要在训练期间优化的损失函数\n", "- 目标是最大限度地提高模型的垃圾邮件分类准确性;\n", "- 由于分类准确率不是可微分的,我们使用交叉熵损失作为最大化准确率的替代(您可以在我免费提供的[深度学习入门课程](https://sebastianraschka.com/blog/2021/dl-course.html#l08-multinomial-logistic-regression--softmax-regression)的第 8 讲中了解有关此主题的更多信息)\n", "\n", "- `calc_loss_batch` 函数与第 5 章相同,只是我们只对优化最后一个 `tokens model(input_batch)``[:, -1, :]` 感兴趣,而不是所有 `tokens model(input_batch)`" ] }, { "cell_type": "code", "execution_count": null, "id": "2f1e9547-806c-41a9-8aba-3b2822baabe4", "metadata": { "id": "2f1e9547-806c-41a9-8aba-3b2822baabe4" }, "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)[:, -1, :] # Logits of last output token\n", " loss = torch.nn.functional.cross_entropy(logits, target_batch)\n", " return loss\n", " #用交叉熵计算损失函数" ] }, { "cell_type": "markdown", "id": "a013aab9-f854-4866-ad55-5b8350adb50a", "metadata": {}, "source": [ "使用 `calc_closs_loader`,我们在开始训练之前计算初始训练集、验证集和测试集损失" ] }, { "cell_type": "code", "execution_count": null, "id": "b7b83e10-5720-45e7-ac5e-369417ca846b", "metadata": {}, "outputs": [], "source": [ "\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", " # 如果 num_batches 超过数据加载器中的批次数,则减少批次数以匹配数据加载器中的总批次数\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", " # 总损失值\n", " total_loss += loss.item()\n", " \n", " else:\n", " break\n", " return total_loss / num_batches\n", " " ] }, { "cell_type": "code", "execution_count": null, "id": "f6f00e53-5beb-4e64-b147-f26fd481c6ff", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "f6f00e53-5beb-4e64-b147-f26fd481c6ff", "outputId": "49df8648-9e38-4314-854d-9faacd1b2e89" }, "outputs": [], "source": [ "with torch.no_grad(): # 因为我们不进行训练,所以为了提高效率禁用梯度跟踪\n", " train_loss = calc_loss_loader(train_loader, model, device, num_batches=5)\n", " val_loss = calc_loss_loader(val_loader, model, device, num_batches=5)\n", " test_loss = calc_loss_loader(test_loader, model, device, num_batches=5)\n", "\n", "print(f\"Training loss: {train_loss:.3f}\")\n", "print(f\"Validation loss: {val_loss:.3f}\")\n", "print(f\"Test loss: {test_loss:.3f}\")" ] }, { "cell_type": "markdown", "id": "e04b980b-e583-4f62-84a0-4edafaf99d5d", "metadata": {}, "source": [ "- 在下一节中,我们将训练模型以最小化损失值,从而提高分类准确率" ] }, { "cell_type": "markdown", "id": "456ae0fd-6261-42b4-ab6a-d24289953083", "metadata": { "id": "456ae0fd-6261-42b4-ab6a-d24289953083" }, "source": [ "## 6.7 在有监督数据上微调模型" ] }, { "cell_type": "markdown", "id": "6a9b099b-0829-4f72-8a2b-4363e3497026", "metadata": {}, "source": [ "- 在本节中,我们将定义并使用训练函数来提高模型的分类准确率\n", "- 下面的 `train_classifier_simple` 函数实际上与我们在第 5 章中用于预训练模型的 `train_model_simple `函数相同\n", "- 唯二区别\n", " 1. 我们现在跟踪的是已经看到的训练样本数量(examples_seen),而不是token数量\n", " 2. 我们在每个周期后计算准确率,而不是打印一个样例文本" ] }, { "cell_type": "markdown", "id": "979b6222-1dc2-4530-9d01-b6b04fe3de12", "metadata": {}, "source": [ "" ] }, { "cell_type": "code", "execution_count": null, "id": "Csbr60to50FL", "metadata": { "id": "Csbr60to50FL" }, "outputs": [], "source": [ "# 跟第五章的一摸一样那就当重新复习了\n", "def train_classifier_simple(model, train_loader, val_loader, optimizer, device, num_epochs,\n", " eval_freq, eval_iter):\n", " train_losses, val_losses, train_accs, val_accs = [], [], [], []\n", " examples_seen, global_step = 0, -1\n", " # 初始化分类头\n", " for epoch in range(num_epochs):\n", " model.train() \n", " # 每次都进入训练模块\n", " for input_batch, target_batch in train_loader:\n", " optimizer.zero_grad() \n", " # 清零梯度\n", " loss = calc_loss_batch(input_batch, target_batch, model, device)\n", " loss.backward() \n", " # 对损失值执行反向传播记录梯度\n", " optimizer.step() \n", " # 用梯度优化权重\n", " examples_seen += input_batch.shape[0] \n", " global_step += 1\n", "\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", " print(f\"Ep {epoch+1} (Step {global_step:06d}): \"\n", " f\"Train loss {train_loss:.3f}, Val loss {val_loss:.3f}\")\n", "\n", " train_accuracy = calc_accuracy_loader(train_loader, model, device, num_batches=eval_iter)\n", " val_accuracy = calc_accuracy_loader(val_loader, model, device, num_batches=eval_iter)\n", " print(f\"Training accuracy: {train_accuracy*100:.2f}% | \", end=\"\")\n", " print(f\"Validation accuracy: {val_accuracy*100:.2f}%\")\n", " train_accs.append(train_accuracy)\n", " val_accs.append(val_accuracy)\n", "\n", " return train_losses, val_losses, train_accs, val_accs, examples_seen" ] }, { "cell_type": "markdown", "id": "9624cb30-3e3a-45be-b006-c00475b58ae8", "metadata": {}, "source": [ "- `evaluate_model` 在`train_classifier_simple` 是跟第五章相同的" ] }, { "cell_type": "code", "execution_count": null, "id": "bcc7bc04-6aa6-4516-a147-460e2f466eab", "metadata": {}, "outputs": [], "source": [ "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", " # 调用辅助函数 calc_loss_loader 计算模型在 训练集、验证集上的损失" ] }, { "cell_type": "markdown", "id": "e807bfe9-364d-46b2-9e25-3b000c3ef6f9", "metadata": {}, "source": [ "- M3 MacBook Air五分钟训练完\n", "- V100 or A100 GPU大概用半分钟" ] }, { "cell_type": "code", "execution_count": null, "id": "X7kU3aAj7vTJ", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "X7kU3aAj7vTJ", "outputId": "504a033e-2bf8-41b5-a037-468309845513" }, "outputs": [], "source": [ "import time\n", "\n", "start_time = time.time()\n", "\n", "torch.manual_seed(123)\n", "\n", "optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5, weight_decay=0.1)\n", "\n", "num_epochs = 5\n", "train_losses, val_losses, train_accs, val_accs, examples_seen = train_classifier_simple(\n", " model, train_loader, val_loader, optimizer, device,\n", " num_epochs=num_epochs, eval_freq=50, eval_iter=5,\n", ")\n", "\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.\")\n", "# 输出每一次的损失值跟在一定频次下进行准确率输出" ] }, { "cell_type": "markdown", "id": "1261bf90-3ce7-4591-895a-044a05538f30", "metadata": {}, "source": [ "- 跟第五章相似,我们用Matplot作图" ] }, { "cell_type": "code", "execution_count": null, "id": "cURgnDqdCeka", "metadata": { "id": "cURgnDqdCeka" }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "\n", "def plot_values(epochs_seen, examples_seen, train_values, val_values, label=\"loss\"):\n", " fig, ax1 = plt.subplots(figsize=(5, 3))\n", "\n", " ax1.plot(epochs_seen, train_values, label=f\"Training {label}\")\n", " ax1.plot(epochs_seen, val_values, linestyle=\"-.\", label=f\"Validation {label}\")\n", " ax1.set_xlabel(\"Epochs\")\n", " ax1.set_ylabel(label.capitalize())\n", " ax1.legend()\n", "\n", " \n", " ax2 = ax1.twiny() \n", " ax2.plot(examples_seen, train_values, alpha=0) \n", "\n", " fig.tight_layout() \n", " plt.savefig(f\"{label}-plot.pdf\")\n", " plt.show()\n", "# 一个经典的画图操作" ] }, { "cell_type": "code", "execution_count": null, "id": "OIqRt466DiGk", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 307 }, "id": "OIqRt466DiGk", "outputId": "b16987cf-0001-4652-ddaf-02f7cffc34db" }, "outputs": [], "source": [ "epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))\n", "examples_seen_tensor = torch.linspace(0, examples_seen, len(train_losses))\n", "\n", "plot_values(epochs_tensor, examples_seen_tensor, train_losses, val_losses)" ] }, { "cell_type": "markdown", "id": "dbd28174-1836-44ba-b6c0-7e0be774fadc", "metadata": {}, "source": [ "- 图中, 根据训练的斜率, 我们可以发现模型训练的很好\n", "- 此外, 训练和验证损失非常接近可以表明,该模型不会倾向于过拟合训练数据\n", "- 同样的, 我们可以对精度进行绘制" ] }, { "cell_type": "code", "execution_count": null, "id": "yz8BIsaF0TUo", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 307 }, "id": "yz8BIsaF0TUo", "outputId": "3a7ed967-1f2a-4c6d-f4a3-0cc8cc9d6c5f" }, "outputs": [], "source": [ "epochs_tensor = torch.linspace(0, num_epochs, len(train_accs))\n", "examples_seen_tensor = torch.linspace(0, examples_seen, len(train_accs))\n", "\n", "plot_values(epochs_tensor, examples_seen_tensor, train_accs, val_accs, label=\"accuracy\")" ] }, { "cell_type": "markdown", "id": "90aba699-21bc-42de-a69c-99f370bb0363", "metadata": {}, "source": [ "- 根据上面的准确率图,我们可以看到该模型在第 4 个和第 5 循环之后,训练和验证准确率变得相对较高\n", "- 但是,别忘了我们之前在 training 函数中指定了 `eval_iter=5`,这意味着我们只估计了训练和验证集的性能\n", "- 我们可以计算完整数据集的训练、验证和测试集性能,如下所示" ] }, { "cell_type": "code", "execution_count": null, "id": "UHWaJFrjY0zW", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "UHWaJFrjY0zW", "outputId": "e111e6e6-b147-4159-eb9d-19d4e809ed34" }, "outputs": [], "source": [ "train_accuracy = calc_accuracy_loader(train_loader, model, device)\n", "val_accuracy = calc_accuracy_loader(val_loader, model, device)\n", "test_accuracy = calc_accuracy_loader(test_loader, model, device)\n", "\n", "print(f\"Training accuracy: {train_accuracy*100:.2f}%\")\n", "print(f\"Validation accuracy: {val_accuracy*100:.2f}%\")\n", "print(f\"Test accuracy: {test_accuracy*100:.2f}%\")\n", "# 输出总的训练集、验证集和测试集的准确率" ] }, { "cell_type": "markdown", "id": "6882649f-dc7b-401f-84d2-024ff79c74a1", "metadata": {}, "source": [ "- 我们可以看到,训练集和验证集的表现实际上是相同的。\n", "- 然而,由于测试集表现略微较差,我们可以看出,模型在一定程度上对训练数据进行了过拟合,同时也对用于调整超参数(如学习率)的验证数据进行了过拟合。\n", "- 不过,这种情况是正常的,并且通过增加模型的 dropout rate(drop_rate)或优化器设置中的 weight_decay,可能进一步减小这种差距。" ] }, { "cell_type": "markdown", "id": "a74d9ad7-3ec1-450e-8c9f-4fc46d3d5bb0", "metadata": {}, "source": [ "## 6.8 使用LLM作为垃圾消息分类器" ] }, { "cell_type": "markdown", "id": "72ebcfa2-479e-408b-9cf0-7421f6144855", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "fd5408e6-83e4-4e5a-8503-c2fba6073f31", "metadata": {}, "source": [ "- 最后,让我们将微调后的 GPT 模型投入实际应用。\n", "- 以下的 `classify_review` 函数实现了类似于我们之前实现的 `SpamDataset` 的数据预处理步骤。\n", "- 然后,函数返回模型预测的整数类别标签,并返回对应的类别名称。" ] }, { "cell_type": "code", "execution_count": null, "id": "aHdn6xvL-IW5", "metadata": { "id": "aHdn6xvL-IW5" }, "outputs": [], "source": [ "def classify_review(text, model, tokenizer, device, max_length=None, pad_token_id=50256):\n", " model.eval()\n", " # 将模型设置为评估模式,禁用dropout等训练特定操作\n", " input_ids = tokenizer.encode(text)\n", " # 使用tokenizer将输入文本 转换token ID 列表\n", " supported_context_length = model.pos_emb.weight.shape[0]\n", " # 获取模型支持的最大上下文长度(由位置嵌入的权重数量决定)\n", " input_ids = input_ids[:min(max_length, supported_context_length)]\n", " # 如果 token 序列长度超过模型支持的最大长度,则进行截断\n", " input_ids += [pad_token_id] * (max_length - len(input_ids))\n", " # 对序列进行填充\n", " input_tensor = torch.tensor(input_ids, device=device).unsqueeze(0) \n", " # 将 token ID 转换为张量并移动到指定设备(如 GPU),同时增加 batch 维度\n", " # 模型推理\n", " with torch.no_grad():\n", " logits = model(input_tensor)[:, -1, :] \n", " # 禁用梯度计算,仅进行推理\n", " # 将输入张量传入模型,获取 logits,并提取最后一个 token 的 logits\n", " predicted_label = torch.argmax(logits, dim=-1).item()\n", " # 在 logits 上取 softmax 概率最大值的索引,作为预测类别\n", " return \"spam\" if predicted_label == 1 else \"not spam\"\n", " # 如果预测标签是 `1`,返回 \"spam\",否则返回 \"not spam\"" ] }, { "cell_type": "markdown", "id": "f29682d8-a899-4d9b-b973-f8d5ec68172c", "metadata": {}, "source": [ "- 试一下" ] }, { "cell_type": "code", "execution_count": null, "id": "apU_pf51AWSV", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "apU_pf51AWSV", "outputId": "d0fde0a5-e7a3-4dbe-d9c5-0567dbab7e62" }, "outputs": [], "source": [ "text_1 = (\n", " \"You are a winner you have been specially\"\n", " \" selected to receive $1000 cash or a $2000 award.\"\n", ")\n", "\n", "print(classify_review(\n", " text_1, model, tokenizer, device, max_length=train_dataset.max_length\n", "))" ] }, { "cell_type": "code", "execution_count": null, "id": "1g5VTOo_Ajs5", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "1g5VTOo_Ajs5", "outputId": "659b08eb-b6a9-4a8a-9af7-d94c757e93c2" }, "outputs": [], "source": [ "text_2 = (\n", " \"Hey, just wanted to check if we're still on\"\n", " \" for dinner tonight? Let me know!\"\n", ")\n", "\n", "print(classify_review(\n", " text_2, model, tokenizer, device, max_length=train_dataset.max_length\n", "))" ] }, { "cell_type": "markdown", "id": "bf736e39-0d47-40c1-8d18-1f716cf7a81e", "metadata": {}, "source": [ "- 最后,让我们保存模型,以便以后如果需要重用模型时,无需重新训练。" ] }, { "cell_type": "code", "execution_count": null, "id": "mYnX-gI1CfQY", "metadata": { "id": "mYnX-gI1CfQY" }, "outputs": [], "source": [ "torch.save(model.state_dict(), \"review_classifier.pth\")\n", "# 储存!大功告成!" ] }, { "cell_type": "markdown", "id": "ba78cf7c-6b80-4f71-a50e-3ccc73839af6", "metadata": {}, "source": [ "- 下次我们可以这么唤醒这个模型" ] }, { "cell_type": "code", "execution_count": null, "id": "cc4e68a5-d492-493b-87ef-45c475f353f5", "metadata": {}, "outputs": [], "source": [ "model_state_dict = torch.load(\"review_classifier.pth\", map_location=device, weights_only=True)\n", "model.load_state_dict(model_state_dict)" ] }, { "cell_type": "markdown", "id": "5b70ac71-234f-4eeb-b33d-c62726d50cd4", "metadata": { "id": "5b70ac71-234f-4eeb-b33d-c62726d50cd4" }, "source": [ "## 总结" ] }, { "cell_type": "markdown", "id": "dafdc910-d616-47ab-aa85-f90c6e7ed80e", "metadata": {}, "source": [ "- 请参阅 [./gpt_class_finetune.py](./gpt_class_finetune.py) 脚本,这是一个自包含的分类微调脚本。\n", "- 你可以在 [./exercise-solutions.ipynb](./exercise-solutions.ipynb) 中找到练习解决方案。\n", "- 此外,感兴趣的读者可以在 [附录E](../../appendix-E) 中找到关于低秩适应(LoRA)的参数高效微调的介绍。" ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "V100", "provenance": [] }, "kernelspec": { "display_name": "base", "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.11.5" } }, "nbformat": 4, "nbformat_minor": 5 }