diff --git a/.gitignore b/.gitignore index de6bd7a..8118fde 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -/model/__pycache__ +**/__pycache__ /dataset -/out \ No newline at end of file +/out +**/.ipynb_checkpoints \ No newline at end of file diff --git a/notebook/0_tokenizer.ipynb b/notebook/0_tokenizer.ipynb new file mode 100644 index 0000000..da14045 --- /dev/null +++ b/notebook/0_tokenizer.ipynb @@ -0,0 +1,448 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "# 0-Tokenizer\n", + "\n", + "Tokenizer(分词器)在 NLP 领域扮演着基础且关键的作用,它将文本分割成单词或子词并转化为数组编号,为模型提供可处理的输入,在文本预处理、语义理解及适配不同语言和任务等方面奠定基础,是连接自然语言文本与计算机可处理数据的重要桥梁." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "## 子词分词算法\n", + "\n", + "常见的子词分词算法有三种:\n", + "\n", + "1. 字节对编码(Byte Pair Encoding,BPE)\n", + "2. WordPiece\n", + "3. Unigram\n", + "\n", + "### BPE\n", + "\n", + "BPE 是一种简单的数据压缩技术,它会迭代地替换序列中最频繁出现的字节对。BPE 依赖一个预分词器,该预分词器会将训练数据分割成单词(在本项目中,我们使用按空格分词的方法作为预分词方法).\n", + "\n", + "在预分词之后,会创建一组唯一的单词,并确定它们在数据中的出现频率。接下来,BPE 会创建一个基础词表,该词表包含预分词器最初生成的数据中所有唯一单词的符号。然后,会将这对符号从词表中移除,新形成的符号将加入词表。在迭代过程中,BPE 算法会合并频繁出现的符号对.\n", + "\n", + "给定词表的大小,BPE(字节对编码)算法最终会合并出现频率最高的符号对,直到收敛到该大小.\n", + "\n", + "### WordPiece\n", + "\n", + "WordPiece 算法与 BPE 非常相似。WordPiece 首先将词表初始化为包含训练数据中出现的每个字符,然后逐步学习给定数量的合并规则. 与 BPE 不同的是,WordPiece 并不选择最频繁出现的符号对,而是选择那个加入词表后能使训练数据出现的可能性最大化的符号对.\n", + "\n", + "### Unigram\n", + "\n", + "Unigram 算法将其基础词表初始化为大量的符号,然后逐步削减每个符号,以获得一个更小的词表。它会在训练数据上定义一个对数似然损失,以此来确定是否从词表中移除某个符号." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 训练一个最简单的分词器\n", + "\n", + "在本节中,我们将学习基于 transformers 库来训练你自己的分词器." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 初始化\n", + "\n", + "首先,我们应该初始化我们的分词器,并确定选择哪种方法。我们将使用字节对编码(BPE)算法." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from tokenizers import (\n", + " decoders,\n", + " models,\n", + " normalizers,\n", + " pre_tokenizers,\n", + " processors,\n", + " trainers,\n", + " Tokenizer,\n", + ")\n", + "\n", + "tokenizer = Tokenizer(models.BPE())\n", + "tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 定义特殊标记\n", + "\n", + "数据集中存在一些我们不希望被分词的特殊标记,我们会将这些标记定义为特殊标记,并将它们传递给分词器训练器,以防止出现错误的分词情况." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "special_tokens = [\"\", \"\", \"\"]\n", + "\n", + "trainer = trainers.BpeTrainer(\n", + " vocab_size=256,\n", + " special_tokens=special_tokens,\n", + " show_progress=True,\n", + " initial_alphabet=pre_tokenizers.ByteLevel.alphabet()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 从文件中读取数据\n", + "\n", + "在本次实验中,我们使用 JSON Lines(jsonl)格式来存储 Tokenizer 训练数据,分词器内置的训练函数要求训练数据以迭代器的形式传入,因此,我们首先获取一个数据读取的生成器." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "def read_texts_from_jsonl(file_path):\n", + " with open(file_path, 'r', encoding='utf-8') as f:\n", + " for line in f:\n", + " data = json.loads(line)\n", + " yield data['text']\n", + "\n", + "data_path = './toydata/tokenizer_data.jsonl'" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Row 1: 近年来,人工智能技术迅速发展,深刻改变了各行各业的面貌。机器学习、自然语言处理、计算机视觉等领域的突破性进展,使得智能产品和服务越来越普及。从智能家居到自动驾驶,再到智能医疗,AI的应用场景正在快速拓展。随着技术的不断进步,未来的人工智能将更加智能、更加贴近人类生活。\n" + ] + } + ], + "source": [ + "data_iter = read_texts_from_jsonl(data_path)\n", + "print(f'Row 1: {next(data_iter)}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 开始训练!\n", + "\n", + "我们使用分词器的内置函数 `tokenizer.train_from_iterator` 来训练分词器." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "tokenizer.train_from_iterator(data_iter, trainer=trainer)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 设置解码器" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "tokenizer.decoder = decoders.ByteLevel()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "接下来,检查一下特殊标记是否得到了妥善处理。" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "assert tokenizer.token_to_id('') == 0\n", + "assert tokenizer.token_to_id('') == 1\n", + "assert tokenizer.token_to_id('') == 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 将训练好的分词器保存到磁盘" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['./model/toy_tokenizer\\\\vocab.json', './model/toy_tokenizer\\\\merges.txt']" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import os\n", + "\n", + "tokenizer_dir = \"./model/toy_tokenizer\"\n", + "os.makedirs(tokenizer_dir, exist_ok=True)\n", + "tokenizer.save(os.path.join(tokenizer_dir, \"tokenizer.json\")) # At this point, you will see a file named tokenizer.json under tokenizer_dir\n", + "tokenizer.model.save(tokenizer_dir) # generate vocab.json & merges.txt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 手动创建一份配置文件" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tokenizer training completed and saved.\n" + ] + } + ], + "source": [ + "config = {\n", + " \"add_bos_token\": False,\n", + " \"add_eos_token\": False,\n", + " \"add_prefix_space\": False,\n", + " \"added_tokens_decoder\": {\n", + " \"0\": {\n", + " \"content\": \"\",\n", + " \"lstrip\": False,\n", + " \"normalized\": False,\n", + " \"rstrip\": False,\n", + " \"single_word\": False,\n", + " \"special\": True\n", + " },\n", + " \"1\": {\n", + " \"content\": \"\",\n", + " \"lstrip\": False,\n", + " \"normalized\": False,\n", + " \"rstrip\": False,\n", + " \"single_word\": False,\n", + " \"special\": True\n", + " },\n", + " \"2\": {\n", + " \"content\": \"\",\n", + " \"lstrip\": False,\n", + " \"normalized\": False,\n", + " \"rstrip\": False,\n", + " \"single_word\": False,\n", + " \"special\": True\n", + " }\n", + " },\n", + " \"additional_special_tokens\": [],\n", + " \"bos_token\": \"\",\n", + " \"clean_up_tokenization_spaces\": False,\n", + " \"eos_token\": \"\",\n", + " \"legacy\": True,\n", + " \"model_max_length\": 32768,\n", + " \"pad_token\": \"\",\n", + " \"sp_model_kwargs\": {},\n", + " \"spaces_between_special_tokens\": False,\n", + " \"tokenizer_class\": \"PreTrainedTokenizerFast\",\n", + " \"unk_token\": \"\",\n", + " \"chat_template\": \"{{ '' + messages[0]['text'] + '' }}\"\n", + "}\n", + "\n", + "with open(os.path.join(tokenizer_dir, \"tokenizer_config.json\"), \"w\", encoding=\"utf-8\") as config_file:\n", + " json.dump(config, config_file, ensure_ascii=False, indent=4)\n", + "\n", + "print(\"Tokenizer training completed and saved.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "现在我们已经训练了一个简单的分词器,并将其进行保存,接下来,我们试着加载它,并使用其帮助我们对文本进行编解码." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "原始文本:[{'text': '失去的东西就要学着去接受,学着放下。'}]\n", + "修改文本:[{'text': '失去的东西就要学着去接受,学着放下。'}] (添加自定义聊天模板)\n" + ] + } + ], + "source": [ + "from transformers import AutoTokenizer\n", + "\n", + "tokenizer = AutoTokenizer.from_pretrained(\"./model/toy_tokenizer\")\n", + "msg = [{\"text\": \"失去的东西就要学着去接受,学着放下。\"}]\n", + "new_msg = tokenizer.apply_chat_template(\n", + " msg,\n", + " tokenize=False\n", + ")\n", + "print(f'原始文本:{msg}')\n", + "print(f'修改文本:{msg} (添加自定义聊天模板)')" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "分词器词表大小:259\n" + ] + } + ], + "source": [ + "print(f'分词器词表大小:{tokenizer.vocab_size}')" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "查看分词结果:\n", + "{'input_ids': [1, 164, 100, 112, 164, 239, 122, 166, 251, 229, 163, 119, 253, 167, 101, 126, 164, 111, 112, 167, 102, 226, 164, 258, 102, 166, 254, 225, 164, 239, 122, 165, 239, 101, 164, 240, 248, 174, 123, 237, 164, 258, 102, 166, 254, 225, 165, 245, 125, 163, 119, 236, 162, 225, 227, 2], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}\n" + ] + } + ], + "source": [ + "model_inputs = tokenizer(new_msg)\n", + "print(f'查看分词结果:\\n{model_inputs}')" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "对分词结果进行解码:失去的东西就要学着去接受,学着放下。 (保留特殊字符)\n" + ] + } + ], + "source": [ + "response = tokenizer.decode(model_inputs['input_ids'], skip_special_tokens=False)\n", + "print(f'对分词结果进行解码:{response} (保留特殊字符)' )" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "对分词结果进行解码:失去的东西就要学着去接受,学着放下。 (移除特殊字符)\n" + ] + } + ], + "source": [ + "response = tokenizer.decode(model_inputs['input_ids'], skip_special_tokens=True)\n", + "print(f'对分词结果进行解码:{response} (移除特殊字符)' )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 参考资料\n", + "\n", + "- [Hugging Face NLP Course](https://huggingface.co/learn/nlp-course/zh-CN/chapter2/4)" + ] + } + ], + "metadata": { + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebook/1_model.ipynb b/notebook/1_model.ipynb new file mode 100644 index 0000000..3cf0faf --- /dev/null +++ b/notebook/1_model.ipynb @@ -0,0 +1,1421 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f359c4f9-9082-4843-8874-4e7c6f3c7404", + "metadata": {}, + "source": [ + "# 1-Model" + ] + }, + { + "cell_type": "markdown", + "id": "2f5a93fb-2d09-4af4-9782-578357927842", + "metadata": {}, + "source": [ + "在当前人工智能领域, 主流大模型从架构上大致可分为稠密(Dense)模型和混合专家(Mixture of Expert, MoE)模型 。稠密模型中所有参数在每次计算时都会参与运算;混合专家模型则将不同的 “专家” 模块组合, 根据输入选择合适的专家处理, 能在保证效果的同时减少计算量和参数量.\n", + "\n", + "MiniMind 系列模型在 Llama 3.1 的基础上设计, 基于经典的 Transformer Deocder-Only 架构,在这一板块, 我们将围绕 MiniMind 系列模型的源代码展开学习." + ] + }, + { + "cell_type": "markdown", + "id": "f12e2248-7b71-442f-96a0-01a5cb4a1171", + "metadata": {}, + "source": [ + "## MiniMind Dense Model\n", + "\n", + "作者提供了对其 MiniMind Dense Model 模型结构的可视化:\n", + "\n", + "![image](../images/LLM-structure.png)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6cd2aa37-3460-4a64-8285-f888d62b99d5", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "import struct\n", + "import inspect\n", + "import time\n", + "\n", + "from model.LMConfig import LMConfig\n", + "from typing import Any, Optional, Tuple, List\n", + "import numpy as np\n", + "import torch\n", + "import torch.nn.functional as F\n", + "from torch import nn\n", + "from transformers import PreTrainedModel\n", + "from transformers.modeling_outputs import CausalLMOutputWithPast" + ] + }, + { + "cell_type": "markdown", + "id": "acbb3ad8-5143-45ff-af16-8dcc0cc2cf53", + "metadata": {}, + "source": [ + "### 均方根层归一化 (Root Mean Square Layer Normalization, RMSNorm)\n", + "\n", + "RMSNorm 是对 LayerNorm 的一个改进, 没有做 re-center 操作(移除了均值项), 可以看作 LayerNorm 在均值为零时的特例, 使用平方根均值归一化降低噪声影响。\n", + "\n", + "- **Layer Norm**\n", + "\n", + "$$y = \\frac{x-E(x)}{\\sqrt{Var(x) + \\epsilon}} * \\gamma + \\beta$$\n", + "\n", + "假设输入张量形状为 (batch_size, sequence_length, embedding_dim), 层归一化对 embedding_dim 维度进行归一化操作, 其中, $\\epsilon$ 是一个超参数, 用于防止分母为零导致结果上溢, $\\gamma$, $\\beta$ 均为可学习参数。\n", + "\n", + "- **RMS Norm**\n", + "\n", + "$$a_i=\\frac{a_i}{RMS(a) + \\epsilon} * \\gamma, \\quad where \\quad RMS(a) = \\sqrt{\\frac{1}{n}\\sum^n_{i=1}a^2_i}.$$\n", + "\n", + "假设输入张量形状为 (batch_size, sequence_length, embedding_dim), RMS Norm 对 embedding_dim 维度进行归一化,其中, 其中, $\\epsilon$ 是一个超参数, 用于防止分母为零导致结果上溢, $\\gamma$ 为可学习参数.\n", + "\n", + "不难发现, 当均值为零时, Layer Norm 退化为 RMS Norm. 这是因为 RMS Norm 在 Layer Norm 的基础上舍弃了中心化操作, 仅用缩放进行归一化, 其不改变数据原本的分布, 有利于激活函数输出的稳定." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c52df1c5-1047-48d8-aa00-c0b576a87632", + "metadata": {}, + "outputs": [], + "source": [ + "class RMSNorm(torch.nn.Module):\n", + " def __init__(self, dim: int, eps: float):\n", + " super().__init__()\n", + " self.eps = eps # 防止分母等于零导致结果上溢\n", + " self.weight = nn.Parameter(torch.ones(dim)) # 可学习参数\n", + "\n", + " def forward(self, x):\n", + " return self.weight * (x.float() * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)).type_as(x)" + ] + }, + { + "cell_type": "markdown", + "id": "6207c186-9816-41fd-872d-d5116257b274", + "metadata": {}, + "source": [ + "### Rotary Position Embedding, RoPE\n", + "\n", + "旋转位置编码是一种能将相对位置信息集成到 self-attention 中, 进而提升 transformer 架构性能的位置编码方式, 和绝对位置编码相比, RoPE 具有很好的外推性, 是目前的主流位置编码方式.\n", + "\n", + "外推性的解释, 通俗来说就是训练的时候限制了 512 的上下文长度,那么推理时如果面对超过该长度的文本,LLM 可能无法正确处理.\n", + "\n", + "- **绝对位置编码**\n", + "\n", + "绝对位置编码是早期 Transformer 架构采用的绝对位置编码方案,及那个每个位置映射为固定的向量表示.\n", + "\n", + "$$f_{t:t\\in\\{q,k,v\\}}(\\boldsymbol{x}_i,i)=\\boldsymbol{W}_{t:t\\in\\{q,k,v\\}}(\\boldsymbol{x}_i+\\boldsymbol{p}_i)$$\n", + "\n", + "其中编码向量 $p_i$ 的计算使用如下公式:\n", + "\n", + "$$\\boldsymbol{p}_{i,2t}=\\sin\\left(k/1000^{2t/d}\\right), \\boldsymbol{p}_{i,2t+1}=\\cos\\left(k/1000^{2t/d}\\right)$$\n", + "\n", + "正如其名,绝对位置编码只考虑了输入序列中的绝对位置关系,对于 token 之间的相对信息则没有纳入考虑.\n", + "\n", + "- **旋转位置编码**\n", + "\n", + "假定 query 和 key 的内积操作可以被函数 g 表示,该函数 g 的输入是词嵌入向量 $x_m, x_n$ 和它们之间的相对位置 $m-n$:\n", + "\n", + "$$=g(x_m, x_n, m, n)$$\n", + "\n", + "旋转位置编码就是找到一个使上式成立的位置编码方式. \n", + "\n", + "出于认识的目的,我们省略复杂的数学推导,直接看 RoPE 的的结论:\n", + "\n", + "存在这样一个正交矩阵:\n", + "\n", + "$$\\boldsymbol{R}_{\\Theta,m}^d=\\underbrace{\\begin{pmatrix}\\cos m\\theta_0&-\\sin m\\theta_0&0&0&\\cdots&0&0\\\\\\sin m\\theta_0&\\cos m\\theta_0&0&0&\\cdots&0&0\\\\0&0&\\cos m\\theta_1&-\\sin m\\theta_1&\\cdots&0&0\\\\0&0&\\sin m\\theta_1&\\cos m\\theta_1&\\cdots&0&0\\\\\\vdots&\\vdots&\\vdots&\\vdots&\\ddots&\\vdots&\\vdots\\\\0&0&0&0&\\cdots&\\cos m\\theta_{d/2-1}&-\\sin m\\theta_{d/2-1}&-\\sin m\\theta_{d/2-1}\\end{pmatrix}}_{\\boldsymbol{W}_m}$$\n", + "\n", + "其中,$\\Theta=\\left\\{\\theta_i=10000^{-2(i-1)/d},i\\in[1,2,\\ldots,d/2]\\right\\}$\n", + "\n", + "我们可以将 query 和 key 的内积操作转换为与原始向量 $x$ 相关的以下等价形式:\n", + "\n", + "$$\n", + "\\boldsymbol{q}_m^\\mathbf{T}\\boldsymbol{k}_n=\\left(\\boldsymbol{R}_{\\Theta,m}^d\\boldsymbol{W}_q\\boldsymbol{x}_m\\right)^\\mathbf{T}\\left(\\boldsymbol{R}_{\\Theta,n}^d\\boldsymbol{W}_k\\boldsymbol{x}_n\\right)=\\boldsymbol{x}_m^\\mathbf{T}\\boldsymbol{W}_q\\boldsymbol{R}_{\\Theta,n-m}^d\\boldsymbol{W}_k\\boldsymbol{x}_n\n", + "$$\n", + "\n", + "其中, $\\boldsymbol{R}_{\\Theta,n-m}^d=\\left(\\boldsymbol{R}_{\\Theta,m}^d\\right)^\\mathbf{T}\\boldsymbol{R}_{\\Theta,n}^d$.\n", + "\n", + "由于 $\\boldsymbol{R}_{\\Theta,m}^d$ 的稀疏性,直接使用矩阵乘法会浪费算力,因此代码中采用下述方式实现:\n", + "\n", + "$$\\boldsymbol{R}_{\\Theta,m}^{d}\\boldsymbol{x}=\\begin{pmatrix}x_{0}\\\\x_{1}\\\\x_{2}\\\\x_{3}\\\\\\vdots\\\\x_{d-2}\\\\x_{d-1}\\end{pmatrix}\\otimes\\begin{pmatrix}\\cos m\\theta_{0}\\\\\\cos m\\theta_{0}\\\\\\cos m\\theta_{1}\\\\\\cos m\\theta_{1}\\\\\\vdots\\\\\\cos m\\theta_{d/2-1}\\\\\\cos m\\theta_{d/2-1}\\end{pmatrix}+\\begin{pmatrix}-x_{1}\\\\x_{0}\\\\-x_{3}\\\\x_{2}\\\\\\vdots\\\\-x_{d-1}\\\\x_{d-2}\\end{pmatrix}\\otimes\\begin{pmatrix}\\sin m\\theta_{0}\\\\\\sin m\\theta_{0}\\\\\\sin m\\theta_{1}\\\\\\sin m\\theta_{1}\\\\\\vdots\\\\\\sin m\\theta_{d/2-1}\\\\\\sin m\\theta_{d/2-1}\\end{pmatrix}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5aae4194-3738-44a7-9c33-bbf964067f8a", + "metadata": {}, + "outputs": [], + "source": [ + "def precompute_pos_cis(dim: int, end: int=int(32 * 1024), theta: float=1e6):\n", + " r\"\"\"生成旋转矩阵\"\"\"\n", + " # 计算词向量元素两两分组后 每组元素对应的旋转角度 \\theta_i 也就是上文公式中对应的旋转角度集合\n", + " freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))\n", + " # token 序列索引 t = [0, 1, ..., seq_len-1]\n", + " t = torch.arange(end, device=freqs.device)\n", + " # 计算 m * \\theta\n", + " freqs = torch.outer(t, freqs).float()\n", + " # 计算结果是个复数向量\n", + " # 假设 freqs = [x, y]\n", + " # 则 freqs_cis = [cos(x) + sin(x)i, cos(y) + sin(y)i]\n", + " pos_cis = torch.polar(torch.ones_like(freqs), freqs)\n", + " return pos_cis\n", + "\n", + "def apply_rotary_emb(xq, xk, pos_cis):\n", + " r\"\"\"应用 RoPE\"\"\"\n", + " def unite_shape(pos_cis, x):\n", + " ndim = x.ndim\n", + " assert 0 <= 1 < ndim\n", + " assert pos_cis.shape == (x.shape[1], x.shape[-1])\n", + " shape = [d if i == 1 or i == ndim - 1 else 1 for i, d in enumerate(x.shape)]\n", + " return pos_cis.view(*shape)\n", + "\n", + " xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))\n", + " # print('original shape xq: {}'.format(xq.shape)) # 取消注释以查看 xq 的 shape\n", + " # print('ajusted shape xq: {}'.format(xq_.shape)) # 取消注释查看经过复数表示后 xq 的 shape\n", + " xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2))\n", + " pos_cis = unite_shape(pos_cis, xq_)\n", + " # print('ajusted shape pos_cis: {}'.format(pos_cis.shape)) # 取消注释以查看位置编码旋转角的形状\n", + " # 保留实部\n", + " xq_out = torch.view_as_real(xq_ * pos_cis).flatten(3)\n", + " xk_out = torch.view_as_real(xk_ * pos_cis).flatten(3)\n", + " return xq_out.type_as(xq), xk_out.type_as(xk)" + ] + }, + { + "cell_type": "markdown", + "id": "eff57879-1f39-404d-9501-3c99e2e67c41", + "metadata": {}, + "source": [ + "我们知道,RoPE 是在 Attention 阶段生成 Query 和 Key 向量后,对这两个向量进行位置编码的.\n", + "\n", + "对于 MiniMindLM,嵌入维度为 512,注意力头数量为 8,故每一个注意力头的维度应该为 512 / 8 = 64.\n", + "\n", + "我们使用固定形状的张量代表 Query 和 Key 向量,对 RoPE 的应用过程进行观察." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5f8376c4-5b00-4c72-9f23-912478db581c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "pos_cis 的形状为 torch.Size([16, 32]), 其中 [0, 0] 下标元素为 (1+0j)\n" + ] + } + ], + "source": [ + "xq, xk = torch.randn((2, 16, 4, 64)), torch.randn((2, 16, 4, 64)) # (batch_size, sequence_length, num_heads, head_dim)\n", + "pos_cis = precompute_pos_cis(64, 16) # 计算旋转位置编码的旋转角(复数表示)\n", + "print(f'pos_cis 的形状为 {pos_cis.shape}, 其中 [0, 0] 下标元素为 {pos_cis[0, 0]}')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b40d0ec8-8752-4db9-a4fe-e746d39dc970", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "经过 RoPE 编码后的 Query 与 Key 的形状为 torch.Size([2, 16, 4, 64]), torch.Size([2, 16, 4, 64])\n" + ] + } + ], + "source": [ + "xq_rope, xk_rope = apply_rotary_emb(xq, xk, pos_cis)\n", + "# original shape xq: torch.Size([2, 16, 4, 64])\n", + "# ajusted shape xq: torch.Size([2, 16, 4, 32])\n", + "# ajusted shape pos_cis: torch.Size([1, 16, 1, 32])\n", + "print(f'经过 RoPE 编码后的 Query 与 Key 的形状为 {xq_rope.shape}, {xk_rope.shape}')" + ] + }, + { + "cell_type": "markdown", + "id": "fa47eeb0-874e-4df3-bf91-5f79109af8ca", + "metadata": {}, + "source": [ + "### Attention\n", + "\n", + "注意力机制(Attention Mechanism)是Transformer架构的核心组件,能够有效捕捉长序列内各元素间的依赖关系. 该机制通过计算输入序列中不同位置元素间的注意力得分,对其重要性进行精准建模,使模型在处理信息时能够聚焦于关键部分,从而显著提升对长序列数据的理解与处理能力.\n", + "\n", + "在 MiniMindLM 模型中, Attention Block 包含以下机制和模块:\n", + "\n", + "1. GQA\n", + "2. KV Cache\n", + "5. SwiGLU\n", + "\n", + "- **GQA**\n", + "\n", + "分组查询注意力 (Group Querey Attention, GQA) 是对多头自注意力机制的扩展, 通过提供计算效率和模型表达能力的灵活权衡, 实现了查询头的分组.\n", + "\n", + "具体来说, GQA 中将 h 个查询头分为 G 组, 每组 包含 h / G 个查询头,并共享一个公共的键和值.\n", + "\n", + "![img](./images/gqa.png)\n", + "\n", + "GQA 相比传统的 MHA, 减少了键和值的数量,降低了计算量和内存开销,提高了推理速度.\n", + "\n", + "- **KV Cache**\n", + "\n", + "在语言模型生成文本的过程中,每生成一个新的 token,模型都需要计算注意力得分,以确定当前位置与之前所有位置的相关性.\n", + "\n", + "比如以下内容:\n", + "\n", + "1. *seq = \\[tok1]:*\n", + "\n", + " attn_11 = softmax(Q1 * K1.T / sqrt(dim)) * V1\n", + " \n", + "3. *seq = \\[tok1, tok2]:*\n", + "\n", + " attn_11 = softmax(Q1 * K1.T / sqrt(dim)) * V1, attn_12 = 0 (masked)\n", + " \n", + " attn_21 = softmax(Q2 * K1.T / sqrt(dim)) * V1, attn_22 = softmax(Q2 * K2.T / sqrt(dim)) * V2\n", + "4. *seq = \\[tok1, tok2, tok3]:*\n", + "\n", + " attn_11 = softmax(Q1 * K1.T / sqrt(dim)) * V1, attn_12 = 0 (masked), attn_13 = 0 (masked)\n", + " \n", + " attn_21 = softmax(Q2 * K1.T / sqrt(dim)) * V1, attn_22 = softmax(Q2 * K2.T / sqrt(dim)) * V2, attn_23 = 0 (masked)\n", + " \n", + " attn_31 = softmax(Q3 * K1.T / sqrt(dim)) * V1, attn_32 = softmax(Q3 * K2.T / sqrt(dim)) * V2, attn_33 = softmax(Q3 * K3.T / sqrt(dim)) * V3\n", + "5. ··· ···\n", + "\n", + "不难发现,大模型生成一个 token 后的注意力计算中,总会用到 token 序列的历史 KV 值,导致重复计算,KV Cache 的设计正是为了通过缓存历史 KV 值,节省计算开销.\n", + "\n", + "KV Cache 能够有效压缩大模型推理时的显存占用.\n", + "\n", + "- **SwiGLU**\n", + "\n", + "SwiGLU 是一种在深度学习中用于神经网络架构的激活函数变体:\n", + "\n", + "$$\\text{SwiGLU}(x, W, V, b, c)=\\text{Swish}_1(xW+b)\\otimes(xV+c)$$\n", + "\n", + "与传统的 ReLU 激活函数相比,SwiGLU 具有更好的平滑性和非线性表达能力,由于其门控机制,在处理信息筛选和流动方面有独特的优势." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e61394db-5afa-4421-ba77-30a9c50f0603", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Any, Optional, Tuple, List\n", + "import torch.nn as nn\n", + "import math\n", + "\n", + "def repeat_kv(x: torch.Tensor, n_rep: int) -> torch.Tensor:\n", + " r\"\"\"使得 KV 头数适应 Query 头数\"\"\"\n", + " bs, slen, n_kv_heads, head_dim = x.shape\n", + " if n_rep == 1:\n", + " return x\n", + " return (\n", + " x[:, :, :, None, :]\n", + " .expand(bs, slen, n_kv_heads, n_rep, head_dim)\n", + " .reshape(bs, slen, n_kv_heads * n_rep, head_dim)\n", + " )\n", + "\n", + "class Attention(nn.Module):\n", + " def __init__(self, args: LMConfig):\n", + " super().__init__()\n", + " self.n_kv_heads = args.n_heads if args.n_kv_heads is None else args.n_kv_heads\n", + " assert args.n_heads % self.n_kv_heads == 0\n", + " self.n_local_heads = args.n_heads\n", + " self.n_local_kv_heads = args.n_kv_heads\n", + " self.n_rep = self.n_local_heads // self.n_local_kv_heads\n", + " self.head_dim = args.dim // args.n_heads\n", + " # q k v o projection\n", + " self.wq = nn.Linear(args.dim, args.n_heads * self.head_dim, bias=False)\n", + " self.wk = nn.Linear(args.dim, args.n_kv_heads * self.head_dim, bias=False)\n", + " self.wv = nn.Linear(args.dim, args.n_kv_heads * self.head_dim, bias=False)\n", + " self.wo = nn.Linear(args.n_heads * self.head_dim, args.dim, bias=False)\n", + " self.attn_dropout = nn.Dropout(args.dropout)\n", + " self.resid_dropout = nn.Dropout(args.dropout)\n", + " self.dropout = args.dropout\n", + " self.flash = hasattr(torch.nn.functional, 'scaled_dot_product_attention') and args.flash_attn\n", + " mask = torch.full((1, 1, args.max_seq_len, args.max_seq_len), float(\"-inf\"))\n", + " mask = torch.triu(mask, diagonal=1)\n", + " self.register_buffer(\"mask\", mask, persistent=False)\n", + "\n", + " def forward(self, \n", + " x: torch.Tensor, \n", + " pos_cis: torch.Tensor, \n", + " past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, \n", + " use_cache=False):\n", + " bsz, seq_len, _ = x.shape\n", + " ############## Forward QKV & RoPE ##############\n", + " xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)\n", + " xq = xq.view(bsz, seq_len, self.n_local_heads, self.head_dim)\n", + " xk = xk.view(bsz, seq_len, self.n_local_kv_heads, self.head_dim)\n", + " xv = xv.view(bsz, seq_len, self.n_local_kv_heads, self.head_dim)\n", + " xq, xv = apply_rotary_emb(xq, xk, pos_cis)\n", + " ################### KV Cache ###################\n", + " if past_key_value is not None:\n", + " xk = torch.cat([past_key_value[0], xk], dim=1)\n", + " xv = torch.cat([past_key_value[1], xv], dim=1)\n", + " past_kv = (xk, xv) if use_cache else None\n", + " xq, xk, xv = (\n", + " xq.transpose(1, 2), \n", + " repeat_kv(xk, self.n_rep).transpose(1, 2), \n", + " repeat_kv(xv, self.n_rep).transpose(1, 2)\n", + " )\n", + " ############ Scaled Dot Production #############\n", + " if self.flash and seq_len != 1:\n", + " dropout_p = self.dropout if self.training else 0.0\n", + " output = F.scaled_dot_product_attention(\n", + " xq, xk, xv, \n", + " attn_mask=None, \n", + " dropout_p=dropout_p, \n", + " is_causal=True\n", + " )\n", + " else:\n", + " scores = (xq @ xk.transpose(-2, -1)) / math.sqrt(self.head_dim)\n", + " scores += self.mask[:, :, :seq_len, :seq_len]\n", + " scores = F.softmax(scores.float(), dim=-1).type_as(xq)\n", + " scores = self.attn_dropout(scores)\n", + " output = scores @ xv\n", + " ################################################\n", + " output = output.transpose(1, 2).reshape(bsz, seq_len, -1)\n", + " output = self.resid_dropout(self.wo(output))\n", + " return output, past_kv" + ] + }, + { + "cell_type": "markdown", + "id": "7420b47b-b38e-442a-84a2-e307f82584d3", + "metadata": {}, + "source": [ + "同样的,我们假设一批 batch size = 4, seq len = 16 的 token 序列通过这个 Attention 块,在输入前,它的 input id 会被投影到 dim = 512 维." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "e1519451-3dc8-498e-96a7-db8b32a71af1", + "metadata": {}, + "outputs": [], + "source": [ + "# 创建 Config,为了简单起见,我们设定 num_layers = 2\n", + "LMConfig_Dense = LMConfig(n_layers=2)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ca9f6675-7d70-49b4-ba02-f91109869fd8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "输入张量 x :size = torch.Size([4, 16, 512]),RoPE 旋转角: size = torch.Size([16, 32])\n", + "输出 output: size = torch.Size([4, 16, 512]), kv_cache 基本信息:size_key = torch.Size([4, 16, 2, 64]), size_value = torch.Size([4, 16, 2, 64])\n" + ] + } + ], + "source": [ + "attn = Attention(LMConfig_Dense)\n", + "x = torch.randn((4, 16, 512)) # (batch size, seq len, embed dim)\n", + "pos_cis = precompute_pos_cis(64, 16) # (head dim, batch size) 其中 head dim = embed dim / num heads\n", + "output, past_kv = attn(x, pos_cis=pos_cis, use_cache=True)\n", + "print(f'输入张量 x :size = {x.shape},RoPE 旋转角: size = {pos_cis.shape}')\n", + "print(f'输出 output: size = {output.shape}, kv_cache 基本信息:size_key = {past_kv[0].shape}, size_value = {past_kv[1].shape}')" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "b53cee91-ee0d-4a61-8a7b-6feac5ba1b40", + "metadata": {}, + "outputs": [], + "source": [ + "del attn" + ] + }, + { + "cell_type": "markdown", + "id": "495f1e81-36de-4e94-8ae4-69490f62d15f", + "metadata": {}, + "source": [ + "### FeedForward Network\n", + "\n", + "前馈神经网络接收来自注意力机制层的输出结果,随后对该输出执行进一步的线性变换. 通过这种方式,网络能够深入挖掘并捕获更为复杂、抽象的特征." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "a16f3ece-3b4e-42aa-9b88-1a7193dd2bf0", + "metadata": {}, + "outputs": [], + "source": [ + "class FeedForward(nn.Module):\n", + " def __init__(self, config: LMConfig):\n", + " super().__init__()\n", + " if config.hidden_dim is None:\n", + " hidden_dim = 4 * config.dim\n", + " hidden_dim = int(2 * hidden_dim / 3)\n", + " config.hidden_dim = config.multiple_of * ((hidden_dim + config.multiple_of - 1) // config.multiple_of)\n", + " self.w1 = nn.Linear(config.dim, config.hidden_dim, bias=False)\n", + " self.w2 = nn.Linear(config.hidden_dim, config.dim, bias=False)\n", + " self.w3 = nn.Linear(config.dim, config.hidden_dim, bias=False)\n", + " self.dropout = nn.Dropout(config.dropout)\n", + "\n", + " def forward(self, x):\n", + " return self.dropout(self.w2(F.silu(self.w1(x) * self.w3(x))))" + ] + }, + { + "cell_type": "markdown", + "id": "8ddb2503-018c-4486-ae45-d9d0a192490a", + "metadata": {}, + "source": [ + "设置输入 x 为 batch size = 4,seq len = 16 的 token 序列投影向量,观察 x 在 MiniMind Block 的前向传播过程." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "4fe2abd9-0c40-44be-b64d-8b0caa28604b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "给定输入 x: size = torch.Size([4, 16, 512]) 下的输出 output:size = torch.Size([4, 16, 512])\n" + ] + } + ], + "source": [ + "ffn = FeedForward(LMConfig_Dense)\n", + "x = torch.randn((4, 16, 512))\n", + "output = ffn(x)\n", + "print(f'给定输入 x: size = {x.shape} 下的输出 output:size = {output.shape}')" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "5ae0c7e5-67e9-4dc4-9ea5-4ec20667c634", + "metadata": {}, + "outputs": [], + "source": [ + "del ffn" + ] + }, + { + "cell_type": "markdown", + "id": "efe25d3b-2af9-4fe1-8eb0-7522324ec2c6", + "metadata": {}, + "source": [ + "这就是 Transformer 结构的精妙之处,张量在模型内部进行了各种复杂的投影变形,但是输入输出的张量形状不发生改变!" + ] + }, + { + "cell_type": "markdown", + "id": "2317829c-39b8-44d5-beb2-1b9727bc35c8", + "metadata": {}, + "source": [ + "### MiniMind Block\n", + "\n", + "到目前为止, , 已经完成了 Attention Layer 和 FeedForward Layer 的构建, 所有必须的组件都已经具备, 我们着手构建一个 MiniMind Block" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "d6c224a4-7ba5-4cf8-9297-311c660f5764", + "metadata": {}, + "outputs": [], + "source": [ + "class MiniMindBlock(nn.Module):\n", + " def __init__(self, layer_id: int, config: LMConfig):\n", + " super().__init__()\n", + " self.n_heads = config.n_heads\n", + " self.dim = config.dim\n", + " self.head_dim = config.dim // config.n_heads # head dim\n", + " self.attention = Attention(config)\n", + "\n", + " self.layer_id = layer_id # layer id 指向其维护的 KV Cache\n", + " self.attention_norm = RMSNorm(config.dim, eps=config.norm_eps)\n", + " self.ffn_norm = RMSNorm(config.dim, eps=config.norm_eps)\n", + " self.feed_forward = FeedForward(config)\n", + "\n", + " def forward(self, x, pos_cis, past_key_value=None, use_cache=False):\n", + " h_attn, past_kv = self.attention(\n", + " self.attention_norm(x), # pre-normed x\n", + " pos_cis, \n", + " past_key_value=past_key_value, \n", + " use_cache=use_cache\n", + " )\n", + " h = x + h_attn # residual connection\n", + " out = h + self.feed_forward(self.ffn_norm(h)) # feed forward + residual connection\n", + " return out, past_kv" + ] + }, + { + "cell_type": "markdown", + "id": "f627c780-f07e-41c3-8b44-7a819e80a2a4", + "metadata": {}, + "source": [ + "我们依然设置输入 x 为 batch size = 4,seq len = 16 的 token 序列投影向量,观察 x 在 MiniMind Block 的前向传播过程." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "d23b4461-534c-4f6d-a00b-cdcfbd4d7bec", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "输出 output 信息: size = torch.Size([4, 16, 512])\n", + "该 Block 维护的 KV Cache 信息:size_key = torch.Size([4, 16, 2, 64]), size_value = torch.Size([4, 16, 2, 64])\n" + ] + } + ], + "source": [ + "miniblock = MiniMindBlock(1, LMConfig_Dense)\n", + "x = torch.randn((4, 16, 512))\n", + "pos_cis = precompute_pos_cis(64, 16)\n", + "out, past_kv = miniblock(x, pos_cis, use_cache=True)\n", + "print(f'输出 output 信息: size = {out.shape}\\n该 Block 维护的 KV Cache 信息:size_key = {past_kv[0].shape}, size_value = {past_kv[1].shape}')" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "bb9745cf-2010-49b3-90ad-a1d59f76de89", + "metadata": {}, + "outputs": [], + "source": [ + "del miniblock" + ] + }, + { + "cell_type": "markdown", + "id": "f9bb3b2e-4d1d-4721-9908-546f3c49dde3", + "metadata": {}, + "source": [ + "### MiniMindLM (Dense)\n", + "\n", + "以 MiniMind Block 为基本组件, 我们对 MiniMindLM 进行最后组装!" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "7267245b-f13a-4bcf-9358-5fdd89106153", + "metadata": {}, + "outputs": [], + "source": [ + "from transformers import PreTrainedModel\n", + "from transformers.modeling_outputs import CausalLMOutputWithPast\n", + "\n", + "class MiniMindLM(PreTrainedModel):\n", + " config_class = LMConfig\n", + "\n", + " def __init__(self, params: LMConfig = None):\n", + " self.params = params or LMConfig()\n", + " super().__init__(self.params)\n", + " self.vocab_size, self.n_layers = params.vocab_size, params.n_layers\n", + " # 映射:词表维度 -> 嵌入维度\n", + " self.tok_embeddings = nn.Embedding(params.vocab_size, params.dim)\n", + " self.dropout = nn.Dropout(params.dropout)\n", + " self.layers = nn.ModuleList([MiniMindBlock(1, params) for l in range(self.n_layers)])\n", + " self.norm = RMSNorm(params.dim, eps = params.norm_eps)\n", + " self.output = nn.Linear(params.dim, params.vocab_size, bias=False)\n", + " self.tok_embeddings.weight = self.output.weight\n", + " self.register_buffer(\n", + " \"pos_cis\", \n", + " precompute_pos_cis(dim=params.dim // params.n_heads, theta=params.rope_theta), \n", + " persistent=False\n", + " )\n", + " self.OUT = CausalLMOutputWithPast()\n", + "\n", + " def forward(self, \n", + " input_ids: Optional[torch.Tensor] = None, \n", + " past_key_values: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None, \n", + " use_cache: bool = False, \n", + " **args):\n", + " past_key_values = past_key_values or [None] * len(self.layers) # save history KV\n", + " start_pos = args.get('start_pos', 0)\n", + " h = self.dropout(self.tok_embeddings(input_ids))\n", + " pos_cis = self.pos_cis[start_pos: start_pos + input_ids.size(1)]\n", + " past_kvs = [] # KV Cache 列表 按照索引维护对应 MiniMind Block 模块的 KV Cache\n", + " print(f'====> forward propagation started, num_minimind_blocks = {len(self.layers)}')\n", + " for l, layer in enumerate(self.layers):\n", + " print(f'------------> entering minimind block: id = {l}')\n", + " h, past_kv = layer(\n", + " h, pos_cis, \n", + " past_key_value=past_key_values[l], \n", + " use_cache=use_cache\n", + " )\n", + " print(f'<------------ finished, size_cache_k = {past_kv[0].shape}, size_cache_v = {past_kv[1].shape}')\n", + " past_kvs.append(past_kv)\n", + " print(f'<==== forward propagation completed, num_kv_cache = {len(past_kvs)}\\n')\n", + " logits = self.output(self.norm(h))\n", + " aux_loss = 0 # MoE 辅助损失,我们在 Dense Model 中不纳入考虑\n", + " self.OUT.__setitem__('logits', logits) # token 分类 logits\n", + " self.OUT.__setitem__('aux_loss', aux_loss) # MoE 辅助损失\n", + " self.OUT.__setitem__('past_key_value', past_kvs) # 当前序列的历史 KV_cache 列表\n", + " return self.OUT\n", + "\n", + " @torch.inference_mode()\n", + " def generate(self, input_ids, eos_token_id=2, max_new_tokens=512, temperature=0.75, top_p=0.90, \n", + " stream=False, rp=1, use_cache=True, pad_token_id=0, **args):\n", + " # 流式生成 (返回生成器, 自由控制输出)\n", + " if stream:\n", + " return self._stream(input_ids, eos_token_id, max_new_tokens, temperature, top_p, rp, use_cache, **args)\n", + " # 直接生成 (一步到位)\n", + " generated = []\n", + " for i in range(input_ids.size(0)):\n", + " non_pad = input_ids[i][input_ids[i] != pad_token_id].unsqueeze(0)\n", + " out = self._stream(non_pad, eos_token_id, max_new_tokens, temperature, top_p, rp, use_cache, **args)\n", + " tokens_list = [tokens[:, -1:] for tokens in out]\n", + " print(f'new tokens list :{tokens_list}\\n')\n", + " gen = torch.cat(tokens_list, dim=-1) if tokens_list else non_pad\n", + " full_sequence = torch.cat([non_pad, gen], dim=-1)\n", + " generated.append(full_sequence)\n", + " max_length = max(seq.size(1) for seq in generated)\n", + " generated = [\n", + " torch.cat([seq, torch.full((1, max_length - seq.size(1)), pad_token_id, dtype=seq.dtype, device=seq.device)], dim=-1) \n", + " for seq in generated\n", + " ]\n", + " return torch.cat(generated, dim=0)\n", + "\n", + " def _stream(self, input_ids, eos_token_id, max_new_tokens, temperature, top_p, rp, use_cache, **args):\n", + " start, first_seq, past_kvs = input_ids.shape[1], True, None\n", + " new_token_idx = 0 # new token 计数器\n", + " while input_ids.shape[1] < max_new_tokens - 1:\n", + " print(f'gernerating new token: idx = {start + new_token_idx}')\n", + " if first_seq or not use_cache: # 若第一次生成序列 or 无 KV Cache, 每次生成传入整个 token id 序列\n", + " out, first_seq = self(input_ids, past_key_values=past_kvs, use_cache=use_cache, **args), False\n", + " else: # 若非第一次生成 and 有 KV Cache, 每次传入最后一个 token id 与 KV Cache 进行推理加速\n", + " out = self(input_ids[:, -1:], past_key_values=past_kvs, use_cache=use_cache, \n", + " start_pos=input_ids.shape[1] - 1, **args)\n", + " logits, past_kvs = out.logits[:, -1, :], out.past_key_values # logits.shape: (batch_size, seq_len, embed_dim), 获取最后一位 logits\n", + " logits[:, list(set(input_ids.tolist()[0]))] /= rp # 对生成 token 进行惩罚, 降低后续重复生成几率\n", + " logits /= (temperature + 1e-9) # 调整温度, 控制生成多样性\n", + " if top_p is not None and top_p < 1.0: # top-p 采样\n", + " sorted_logits, sorted_indices = torch.sort(logits, descending=True, dim=-1)\n", + " sorted_probs = F.softmax(sorted_logits, dim=-1)\n", + " cumulative_probs = torch.cumsum(sorted_probs, dim=-1)\n", + " sorted_indices_to_remove = cumulative_probs > top_p\n", + " sorted_indices_to_remove[:, 1:] = sorted_indices_to_remove[:, :-1].clone()\n", + " sorted_indices_to_remove[:, 0] = False\n", + " indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)\n", + " logits[indices_to_remove] = -float('Inf')\n", + " input_ids_next = torch.multinomial(F.softmax(logits, dim=-1), num_samples=1) # 从保留的 token 中采样\n", + " input_ids = torch.cat((input_ids, input_ids_next), dim=1)\n", + " new_token_idx += 1\n", + " yield input_ids[:, start:]\n", + " if input_ids_next.item() == eos_token_id:\n", + " break" + ] + }, + { + "cell_type": "markdown", + "id": "bdee2ebc-f98c-49cc-b5fc-c6cd0056f4d9", + "metadata": {}, + "source": [ + "接下来,我们设置一条长度为 4 的 token 序列,使用 MiniMindLM 对其执行一次前向传播,观察传播过程与返回值." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "dc7b4d79-a3aa-4143-aa5c-2abce34bc96c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====> forward propagation started, num_minimind_blocks = 2\n", + "------------> entering minimind block: id = 0\n", + "<------------ finished, size_cache_k = torch.Size([1, 4, 2, 64]), size_cache_v = torch.Size([1, 4, 2, 64])\n", + "------------> entering minimind block: id = 1\n", + "<------------ finished, size_cache_k = torch.Size([1, 4, 2, 64]), size_cache_v = torch.Size([1, 4, 2, 64])\n", + "<==== forward propagation completed, num_kv_cache = 2\n", + "\n", + "返回 logits:size = torch.Size([1, 4, 6400]), 返回 aux_loss: 0, 返回 KV Cache List: len = 2\n" + ] + } + ], + "source": [ + "MiniMind_Dense = MiniMindLM(LMConfig_Dense)\n", + "input_ids = torch.Tensor([1, 3, 5, 7]).long().reshape(1, 4)\n", + "OUT = MiniMind_Dense(input_ids, use_cache=True)\n", + "print(f'返回 logits:size = {OUT.logits.shape}, 返回 aux_loss: {OUT.aux_loss}, 返回 KV Cache List: len = {len(OUT.past_key_value)}')" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "3480a860-e934-43de-a621-8063f0bdea2f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "gernerating new token: idx = 4\n", + "====> forward propagation started, num_minimind_blocks = 2\n", + "------------> entering minimind block: id = 0\n", + "<------------ finished, size_cache_k = torch.Size([1, 4, 2, 64]), size_cache_v = torch.Size([1, 4, 2, 64])\n", + "------------> entering minimind block: id = 1\n", + "<------------ finished, size_cache_k = torch.Size([1, 4, 2, 64]), size_cache_v = torch.Size([1, 4, 2, 64])\n", + "<==== forward propagation completed, num_kv_cache = 2\n", + "\n", + "gernerating new token: idx = 5\n", + "====> forward propagation started, num_minimind_blocks = 2\n", + "------------> entering minimind block: id = 0\n", + "<------------ finished, size_cache_k = torch.Size([1, 1, 2, 64]), size_cache_v = torch.Size([1, 1, 2, 64])\n", + "------------> entering minimind block: id = 1\n", + "<------------ finished, size_cache_k = torch.Size([1, 1, 2, 64]), size_cache_v = torch.Size([1, 1, 2, 64])\n", + "<==== forward propagation completed, num_kv_cache = 2\n", + "\n", + "gernerating new token: idx = 6\n", + "====> forward propagation started, num_minimind_blocks = 2\n", + "------------> entering minimind block: id = 0\n", + "<------------ finished, size_cache_k = torch.Size([1, 1, 2, 64]), size_cache_v = torch.Size([1, 1, 2, 64])\n", + "------------> entering minimind block: id = 1\n", + "<------------ finished, size_cache_k = torch.Size([1, 1, 2, 64]), size_cache_v = torch.Size([1, 1, 2, 64])\n", + "<==== forward propagation completed, num_kv_cache = 2\n", + "\n", + "new tokens list :[tensor([[5081]]), tensor([[4500]]), tensor([[850]])]\n", + "\n", + "生成结果:tensor([[ 1, 3, 5, 7, 5081, 4500, 850]])\n" + ] + } + ], + "source": [ + "# 我们让 MiniMind 根据我们设计的 四个输入 token 生成输出\n", + "out = MiniMind_Dense.generate(input_ids, max_new_tokens=8, use_cache=True)\n", + "print(f'生成结果:{out}')" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "4509ff58-0439-42b5-b303-d308b502e821", + "metadata": {}, + "outputs": [], + "source": [ + "del MiniMind_Dense" + ] + }, + { + "cell_type": "markdown", + "id": "b0e4ef5f-1358-4ffb-a150-61141ac3062f", + "metadata": {}, + "source": [ + "## Minimind MoE Model" + ] + }, + { + "cell_type": "markdown", + "id": "e54ced18-db3a-478b-9578-9825634f3d66", + "metadata": {}, + "source": [ + "作者提供了 MiniMind MoE Model 的可视化.\n", + "\n", + "![image](../images/LLM-structure-moe.png)" + ] + }, + { + "cell_type": "markdown", + "id": "82682a98-cae7-49c2-be2f-12dd1adb8deb", + "metadata": {}, + "source": [ + "可以看到,Dense Model 和 MoE Model 的差异在于 FFN 层. MoE 模型将稠密连接的 FFN 层置换为 M x Expert 层,每次前向传播时只激活部分 Expert.\n", + "\n", + "其组成可以分为以下部分:\n", + "\n", + "- Experts: MoE 架构单元,每个专家本质上是一个独立的神经网络模块,负责处理特定类型或范围的数据.\n", + "- Router: 控制信息流动,决定每次前向传播激活的 Experts 模块以及输入数据在这些模块的分配组合.\n", + "\n", + "在 MoE 网络中,为了平衡专家的重要性,我们需要关注路由,它是决定在特定时间选择哪些专家的重要组件.\n", + "\n", + "### 辅助损失\n", + "\n", + "为了让训练过程中实现专家的更均匀分布,辅助损失(又称负载均衡损失)被添加到网络的常规损失中,它增加了一个约束,迫使专家具有相等的重要性.\n", + "\n", + "假设有输入序列 \\[What is Mixture of Experts], *Prob* (·) 表示每一个 token 激活的专家概率分布:\n", + "\n", + "- **Step 1**:在整个批次中对每个专家的路由值进行求和.\n", + "\n", + " $$Importance \\, per \\, token = Prob (What) + Prob (is) + Prob (Mixture) + Prob (of) + Prob (Experts)$$\n", + "\n", + " 这个指标反映了 batch 维度上每个专家的重要性分数.\n", + "\n", + "- **Step 2**: 计算变异系数\n", + "\n", + " 我们希望专家之间的重要性尽可能靠近,为了衡量专家得分之间的差异程度,引入变异系数指标(Coefficient Variation, CV)\n", + "\n", + " $$Coeifficient \\, Variation (CV) = \\dfrac{standard \\, deviation (\\sigma)}{mean(\\mu)}$$\n", + "\n", + " 如果专家的重要性分数相似,变异系数会降到很低(这是我们期望的)\n", + "\n", + "- **Step 3**: 计算负载均衡损失\n", + "\n", + " $$uxiliary Loss = \\alpha * CV$$\n", + "\n", + " 其中 $\\alpha$ 是缩放系数." + ] + }, + { + "cell_type": "markdown", + "id": "26dffcd0-7c9e-40bf-9c91-1636aa571d46", + "metadata": {}, + "source": [ + "## MoE Gate\n", + "\n", + "MoE 门控单元决定每次前向传播激活的 Experts 模块及其权重,同时计算 MoE 辅助损失." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "cba5532c-e8d2-40ff-9ed6-b8b33b4a31ca", + "metadata": {}, + "outputs": [], + "source": [ + "class MoEGate(nn.Module):\n", + " def __init__(self, config: LMConfig):\n", + " super().__init__()\n", + " self.config = config\n", + " self.top_k = config.num_experts_per_tok\n", + " self.n_routed_experts = config.n_routed_experts # 总专家数量\n", + "\n", + " self.scoring_func = config.scoring_func # 评分函数\n", + " self.alpha = config.aux_loss_alpha # 辅助损失的 alpha 参数\n", + " self.seq_aux = config.seq_aux # 是否启用序列辅助损失\n", + "\n", + " self.norm_topk_prob = config.norm_topk_prob\n", + " self.gating_dim = config.dim\n", + " self.weight = nn.Parameter(torch.empty((self.n_routed_experts, self.gating_dim))) # 混合专家层 -> embeding dim 层\n", + " self.reset_parameter()\n", + "\n", + " def reset_parameter(self):\n", + " import torch.nn.init as init\n", + " init.kaiming_uniform_(self.weight, a=math.sqrt(5))\n", + "\n", + " def forward(self, hidden_states):\n", + " bsz, seq_len, h = hidden_states.shape\n", + " hidden_states = hidden_states.view(-1, h) # (total_tokens, hidden_dim) 其中 total_tokens = bsz * seq_len\n", + " logits = F.linear(hidden_states, self.weight, None) # logits = hidden_states · self.weight.T\n", + " # print(f'shape of logits computed: {logits.shape}') # (total_tokens, n_routed_experts)\n", + "\n", + " if self.scoring_func == 'softmax':\n", + " scores = logits.softmax(dim=-1)\n", + " else:\n", + " raise NotImplementedError(f'insupportable scoring function for MoE gating: {self.scoring_func}')\n", + "\n", + " topk_weight, topk_idx = torch.topk(scores, k=self.top_k, dim=-1, sorted=False)\n", + " if self.top_k > 1 and self.norm_topk_prob: # prob 归一化\n", + " denominator = topk_weight.sum(dim=-1, keepdim=True) + 1e20\n", + " topk_weight = topk_weight / denominator\n", + "\n", + " if self.training and self.alpha > 0.0:\n", + " scores_for_aux = scores\n", + " aux_topk = self.top_k\n", + " topk_idx_for_aux_loss = topk_idx.view(bsz, -1) # (bsz, seq_len * num_experts_per_tok)\n", + " if self.seq_aux:\n", + " ################# 对所有批次计算 Experts 重要性得分 #################\n", + " scores_for_seq_aux = scores_for_aux.view(bsz, seq_len, -1)\n", + " ce = torch.zeros(bsz, self.n_routed_experts, device=hidden_states.device)\n", + " # 根据索引将 torch.ones 累加到 ce 中,并执行归一化\n", + " ce.scatter_add_(1, topk_idx_for_aux_loss,\n", + " torch.ones(bsz, seq_len * aux_topk, device=hidden_states.device)).div_(\n", + " seq_len * aux_topk / self.n_routed_experts)\n", + " ######################### 计算负载均衡损失 ##########################\n", + " aux_loss = (ce * scores_for_seq_aux.mean(dim=1)).sum(dim=1).mean() * self.alpha\n", + " else:\n", + " mask_ce = F.one_hot(topk_idx_for_aux_loss.view(-1), num_classes=self.n_routed_experts)\n", + " ce = mask_ce.float().mean(0)\n", + " Pi = scores_for_aux.mean(0)\n", + " fi = ce * self.n_routed_experts\n", + " aux_loss = (Pi * fi).sum() * self.alpha\n", + " else:\n", + " aux_loss = 0\n", + " return topk_idx, topk_weight, aux_loss" + ] + }, + { + "cell_type": "markdown", + "id": "455f5c52-aad9-4de8-a571-d47de6f0d7d3", + "metadata": {}, + "source": [ + "接下来,我们设置一个 batch size = 4, seq len =16, emb dim = 512 的输入向量,在 MoE 门控单元前向传播进行观察" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "5b3acf9e-b814-49f7-954f-246c33daa23d", + "metadata": {}, + "outputs": [], + "source": [ + "# 重新创建一个 LMConfig 使得 use MoE 为 True\n", + "LMConfig_MoE = LMConfig(n_layers=2, use_moe=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "534e4ffa-2a4d-4a0d-8906-997f23ec5098", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "shape: topk_idx = torch.Size([64, 2]), topk_weight = torch.Size([64, 2]), aux_loss = 0.10163114219903946\n", + "token 0 选择的专家:idx = tensor([2, 0]), weight = tensor([5.8955e-21, 1.9281e-21], grad_fn=)\n", + "辅助损失:aux_loss = 0.10163114219903946\n" + ] + } + ], + "source": [ + "gate = MoEGate(LMConfig_MoE)\n", + "hidden_states = torch.randn((4, 16, 512))\n", + "topk_idx, topk_weight, aux_loss = gate(hidden_states)\n", + "print(f'shape: topk_idx = {topk_idx.shape}, topk_weight = {topk_weight.shape}, aux_loss = {aux_loss}')\n", + "print(f'token 0 选择的专家:idx = {topk_idx[0]}, weight = {topk_weight[0]}')\n", + "print(f'辅助损失:aux_loss = {aux_loss}')" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "521d65d2-bbcb-4b5c-8e1e-d532ae79b6de", + "metadata": {}, + "outputs": [], + "source": [ + "del gate" + ] + }, + { + "cell_type": "markdown", + "id": "b3773961-8e78-4bd1-91af-8d8384bd31d3", + "metadata": {}, + "source": [ + "### MoE Feed Forward NetWork\n", + "\n", + "完成 MoE 门控单元的设计后,我们可以对 MoE 前向传播网络进行重新设计." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "31c28178-7faf-49aa-a813-5105129eefcb", + "metadata": {}, + "outputs": [], + "source": [ + "class MoEFeedForward(nn.Module):\n", + " def __init__(self, config: LMConfig):\n", + " super().__init__()\n", + " self.config = config\n", + " # 可选专家层\n", + " self.experts = nn.ModuleList([\n", + " FeedForward(config)\n", + " for _ in range(config.n_routed_experts)\n", + " ])\n", + " self.gate = MoEGate(config)\n", + " # 共享专家层d\n", + " if config.n_shared_experts is not None:\n", + " self.shared_experts = FeedForward(config)\n", + "\n", + " def forward(self, x):\n", + " identity = x\n", + " orig_shape = x.shape\n", + " bsz, seq_len, _ = x.shape\n", + " # 使用门控机制选择专家\n", + " topk_idx, topk_weight, aux_loss = self.gate(x)\n", + " x = x.view(-1, x.shape[-1])\n", + " flat_topk_idx = topk_idx.view(-1)\n", + " if self.training:\n", + " # 训练模式下,重复输入数据\n", + " x = x.repeat_interleave(self.config.num_experts_per_tok, dim=0)\n", + " y = torch.empty_like(x, dtype=torch.float16)\n", + " # 将输入数据中选择第 i 个专家的部分输入到该专家网络中进行处理,并将输出结果存储到 y 中对应位置的元素\n", + " for i, expert in enumerate(self.experts):\n", + " y[flat_topk_idx == i] = expert(x[flat_topk_idx == i]).to(y.dtype) # 确保类型一致\n", + " y = (y.view(*topk_weight.shape, -1) * topk_weight.unsqueeze(-1)).sum(dim=1) # 加权求和\n", + " y = y.view(*orig_shape) # 恢复原始形状\n", + " else:\n", + " # 推理模式下,只选择最优专家\n", + " y = self.moe_infer(x, flat_topk_idx, topk_weight.view(-1, 1)).view(*orig_shape)\n", + " if self.config.n_shared_experts is not None:\n", + " y = y + self.shared_experts(identity)\n", + " self.aux_loss = aux_loss\n", + " return y\n", + "\n", + " @torch.no_grad()\n", + " def moe_infer(self, x, flat_expert_indices, flat_expert_weights):\n", + " expert_cache = torch.zeros_like(x)\n", + " idxs = flat_expert_indices.argsort() # [exp_tok_11, exp_tok_12, exp_tok_21, exp_tok_22] -> idxs[2 * token_idxs] = moe_expert_name\n", + " tokens_per_expert = flat_expert_indices.bincount().cpu().numpy().cumsum(0) # [1, 3, 1, 2] -> bincount -> [0, 2, 1, 1] -> cumsum -> [0, 2, 3, 4]\n", + " token_idxs = idxs // self.config.num_experts_per_tok\n", + " # 例如当tokens_per_expert=[6, 15, 20, 26, 33, 38, 46, 52]\n", + " # 当token_idxs=[3, 7, 19, 21, 24, 25, 4, 5, 6, 10, 11, 12...]\n", + " # 意味着当token_idxs[:6] -> [3, 7, 19, 21, 24, 25, 4]位置的token都由专家0处理,token_idxs[6:15]位置的token都由专家1处理......\n", + " for i, end_idx in enumerate(tokens_per_expert):\n", + " start_idx = 0 if i == 0 else tokens_per_expert[i - 1]\n", + " if start_idx == end_idx:\n", + " continue\n", + " expert = self.experts[i]\n", + " exp_token_idx = token_idxs[start_idx:end_idx]\n", + " expert_tokens = x[exp_token_idx]\n", + " expert_out = expert(expert_tokens).to(expert_cache.dtype)\n", + " expert_out.mul_(flat_expert_weights[idxs[start_idx:end_idx]]) # 专家网络输出加权\n", + " # 使用 scatter_add_ 进行 sum 操作\n", + " expert_cache.scatter_add_(0, exp_token_idx.view(-1, 1).repeat(1, x.shape[-1]), expert_out)\n", + "\n", + " return expert_cache" + ] + }, + { + "cell_type": "markdown", + "id": "c1f42fce-8bca-4853-8367-929c200f1b33", + "metadata": {}, + "source": [ + "接下来,我们设置一个 batch size = 4, seq len =16, emb dim = 512 的输入向量,在 MoE 门控单元前向传播进行观察" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "6c7fb409-be15-4ee9-8a77-edacfacb3743", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "输出张量:shape = torch.Size([4, 16, 512]), 辅助损失:aux_loss = 0\n" + ] + } + ], + "source": [ + "moe_ffn = MoEFeedForward(LMConfig_MoE).eval()\n", + "x = torch.randn((4, 16, 512))\n", + "output = moe_ffn(x)\n", + "print(f'输出张量:shape = {output.shape}, 辅助损失:aux_loss = {moe_ffn.aux_loss}')" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "698e4148-8f68-4f05-a33a-a76f70e20760", + "metadata": {}, + "outputs": [], + "source": [ + "del moe_ffn" + ] + }, + { + "cell_type": "markdown", + "id": "bec4c71c-00a3-48bc-abf1-d88b6fb94444", + "metadata": {}, + "source": [ + "之前提到过,MiniMind MoE 和 MiniMind Dense 的最大差别在于 FFN 模块的不同,我们可以对之前声明的 MiniMindBlock 进行继承,添加 MoE FFN 选项." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "3bad938b-bcf6-4bc5-ba2c-9c9689656959", + "metadata": {}, + "outputs": [], + "source": [ + "# DM stands for Dense & Moe\n", + "class MiniMindBlock_DM(MiniMindBlock):\n", + " def __init__(self, layer_id: int, config: LMConfig):\n", + " super().__init__(layer_id, config)\n", + " self.feed_forward = FeedForward(config) if not config.use_moe else MoEFeedForward(config)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "778a8697-458a-47a4-b2bd-9bbdcaf64d24", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "类变量属性检查:layer_id = 1, 类函数属性检查:forward func = \n" + ] + } + ], + "source": [ + "# 检查继承是否正常\n", + "\n", + "miniblock_dm = MiniMindBlock_DM(1, LMConfig_MoE)\n", + "print(f'类变量属性检查:layer_id = {miniblock_dm.layer_id}, 类函数属性检查:forward func = {miniblock_dm.forward}')" + ] + }, + { + "cell_type": "markdown", + "id": "433c99c0-ab12-49e3-805a-493de3cd3613", + "metadata": {}, + "source": [ + "我们仍然设置一个 batch size = 4, seq len =16, emb dim = 512 的输入向量,在 MoE 门控单元前向传播进行观察." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "0f97a54d-aa1a-45f2-ba86-4e31f17f9cb6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "输出张量:shape = torch.Size([4, 16, 512]), KV Cache: shape Key = torch.Size([4, 16, 2, 64]), shape Value = torch.Size([4, 16, 2, 64])\n", + "辅助损失:aux_loss = 0\n" + ] + } + ], + "source": [ + "miniblock_dm.eval() # 冻结参数,直接 moe infer\n", + "x = torch.randn((4, 16, 512))\n", + "pos_cis = precompute_pos_cis(64, 16)\n", + "output, past_kv = miniblock_dm(x, pos_cis, use_cache=True)\n", + "print(f'输出张量:shape = {output.shape}, KV Cache: shape Key = {past_kv[0].shape}, shape Value = {past_kv[1].shape}')\n", + "print(f'辅助损失:aux_loss = {miniblock_dm.feed_forward.aux_loss}')" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "925c973b-44d0-4774-a515-404f76a43a3f", + "metadata": {}, + "outputs": [], + "source": [ + "del miniblock_dm" + ] + }, + { + "cell_type": "markdown", + "id": "a9e64f09-ab3f-42a5-ac89-3809c1b99c26", + "metadata": {}, + "source": [ + "如此,我们便完成了包含 MoE 和 Dense 两种 FFN 选项的 MiniMind Block 的定义,我们对此前声明的 MiniMindLM 作继承修改,使其具备 MoE 架构." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "cec79087-53e4-4925-95e2-0c46ad28aba6", + "metadata": {}, + "outputs": [], + "source": [ + "class MiniMindLM_DM(MiniMindLM):\n", + " def __init__(self, params: LMConfig = None):\n", + " super().__init__(params)\n", + " structure = 'MoE' if params.use_moe else 'Dense'\n", + " print(f'Initializing MiniMind {structure} Model...\\n')\n", + " self.layers = nn.ModuleList([MiniMindBlock_DM(l, params) for l in range(self.n_layers)])" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "cc4517ca-b5ea-4756-9c51-7da415140e12", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Initializing MiniMind Dense Model...\n", + "\n", + "Initializing MiniMind MoE Model...\n", + "\n" + ] + } + ], + "source": [ + "# 检查一下\n", + "a = MiniMindLM_DM(LMConfig_Dense)\n", + "b = MiniMindLM_DM(LMConfig_MoE)\n", + "del a, b" + ] + }, + { + "cell_type": "markdown", + "id": "5a586a43-6f9b-4766-8d5d-24980d1a7241", + "metadata": {}, + "source": [ + "接下来,我们设置一条长度为 4 的 token 序列,使用 MiniMindLM 对其执行一次前向传播,观察传播过程与返回值." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "f5009f2b-600c-4c30-b57a-85757301e6de", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Initializing MiniMind MoE Model...\n", + "\n", + "====> forward propagation started, num_minimind_blocks = 2\n", + "------------> entering minimind block: id = 0\n", + "<------------ finished, size_cache_k = torch.Size([1, 4, 2, 64]), size_cache_v = torch.Size([1, 4, 2, 64])\n", + "------------> entering minimind block: id = 1\n", + "<------------ finished, size_cache_k = torch.Size([1, 4, 2, 64]), size_cache_v = torch.Size([1, 4, 2, 64])\n", + "<==== forward propagation completed, num_kv_cache = 2\n", + "\n", + "返回 logits:size = torch.Size([1, 4, 6400]), 返回 aux_loss: 0, 返回 KV Cache List: len = 2\n" + ] + } + ], + "source": [ + "MiniMind_MoE = MiniMindLM_DM(LMConfig_MoE)\n", + "input_ids = torch.Tensor([1, 3, 5, 7]).long().reshape(1, 4)\n", + "OUT = MiniMind_MoE(input_ids, use_cache=True)\n", + "print(f'返回 logits:size = {OUT.logits.shape}, 返回 aux_loss: {OUT.aux_loss}, 返回 KV Cache List: len = {len(OUT.past_key_value)}')" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "c0fe2fa3-f377-45da-aee1-b8f46341a470", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "gernerating new token: idx = 4\n", + "====> forward propagation started, num_minimind_blocks = 2\n", + "------------> entering minimind block: id = 0\n", + "<------------ finished, size_cache_k = torch.Size([1, 4, 2, 64]), size_cache_v = torch.Size([1, 4, 2, 64])\n", + "------------> entering minimind block: id = 1\n", + "<------------ finished, size_cache_k = torch.Size([1, 4, 2, 64]), size_cache_v = torch.Size([1, 4, 2, 64])\n", + "<==== forward propagation completed, num_kv_cache = 2\n", + "\n", + "gernerating new token: idx = 5\n", + "====> forward propagation started, num_minimind_blocks = 2\n", + "------------> entering minimind block: id = 0\n", + "<------------ finished, size_cache_k = torch.Size([1, 1, 2, 64]), size_cache_v = torch.Size([1, 1, 2, 64])\n", + "------------> entering minimind block: id = 1\n", + "<------------ finished, size_cache_k = torch.Size([1, 1, 2, 64]), size_cache_v = torch.Size([1, 1, 2, 64])\n", + "<==== forward propagation completed, num_kv_cache = 2\n", + "\n", + "gernerating new token: idx = 6\n", + "====> forward propagation started, num_minimind_blocks = 2\n", + "------------> entering minimind block: id = 0\n", + "<------------ finished, size_cache_k = torch.Size([1, 1, 2, 64]), size_cache_v = torch.Size([1, 1, 2, 64])\n", + "------------> entering minimind block: id = 1\n", + "<------------ finished, size_cache_k = torch.Size([1, 1, 2, 64]), size_cache_v = torch.Size([1, 1, 2, 64])\n", + "<==== forward propagation completed, num_kv_cache = 2\n", + "\n", + "new tokens list :[tensor([[4065]]), tensor([[431]]), tensor([[2541]])]\n", + "\n", + "生成结果:tensor([[ 1, 3, 5, 7, 4065, 431, 2541]])\n" + ] + } + ], + "source": [ + "# 我们让 MiniMind 根据我们设计的 四个输入 token 生成输出\n", + "out = MiniMind_MoE.generate(input_ids, max_new_tokens=8, use_cache=True)\n", + "print(f'生成结果:{out}')" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "4cdccdac-930e-4670-8605-2963d0e62d1e", + "metadata": {}, + "outputs": [], + "source": [ + "del MiniMind_MoE" + ] + }, + { + "cell_type": "markdown", + "id": "903f2172-f021-442c-ae02-611a30506ebd", + "metadata": {}, + "source": [ + "## 参考资料\n", + "\n", + "- [十分钟读懂旋转编码(RoPE)](https://www.zhihu.com/tardis/zm/art/647109286?source_id=1003)\n", + "- [混合专家模型 MoE 的全面指南](https://blog.csdn.net/Code1994/article/details/145209710#:~:text=%E4%B8%BA%E4%BA%86%E5%9C%A8%E8%AE%AD%E7%BB%83%E8%BF%87%E7%A8%8B%E4%B8%AD%E5%AE%9E%E7%8E%B0%E4%B8%93%E5%AE%B6%E7%9A%84%E6%9B%B4%E5%9D%87%E5%8C%80%E5%88%86%E5%B8%83%EF%BC%8C%E8%BE%85%E5%8A%A9%E6%8D%9F%E5%A4%B1%EF%BC%88%E4%B9%9F%E7%A7%B0%E4%B8%BA%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E6%8D%9F%E5%A4%B1%EF%BC%89%E8%A2%AB%E6%B7%BB%E5%8A%A0%E5%88%B0%E4%BA%86%E7%BD%91%E7%BB%9C%E7%9A%84%E5%B8%B8%E8%A7%84%E6%8D%9F%E5%A4%B1%E4%B8%AD%E3%80%82%20%E5%AE%83%E5%A2%9E%E5%8A%A0%E4%BA%86%E4%B8%80%E4%B8%AA%E7%BA%A6%E6%9D%9F%EF%BC%8C%E8%BF%AB%E4%BD%BF%E4%B8%93%E5%AE%B6%E5%85%B7%E6%9C%89%E7%9B%B8%E7%AD%89%E7%9A%84%E9%87%8D%E8%A6%81%E6%80%A7%E3%80%82,%E8%BF%99%E4%B8%AA%E8%BE%85%E5%8A%A9%E6%8D%9F%E5%A4%B1%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E7%BB%84%E6%88%90%E9%83%A8%E5%88%86%E6%98%AF%E5%9C%A8%E6%95%B4%E4%B8%AA%E6%89%B9%E6%AC%A1%E4%B8%AD%E5%AF%B9%E6%AF%8F%E4%B8%AA%E4%B8%93%E5%AE%B6%E7%9A%84%E8%B7%AF%E7%94%B1%E5%99%A8%E5%80%BC%E8%BF%9B%E8%A1%8C%E6%B1%82%E5%92%8C%EF%BC%9A%20%E8%BF%99%E4%B8%BA%E6%88%91%E4%BB%AC%E6%8F%90%E4%BE%9B%E4%BA%86%E6%AF%8F%E4%B8%AA%E4%B8%93%E5%AE%B6%E7%9A%84%E9%87%8D%E8%A6%81%E6%80%A7%E5%88%86%E6%95%B0%EF%BC%8C%E8%BF%99%E4%BA%9B%E5%88%86%E6%95%B0%E8%A1%A8%E7%A4%BA%E6%97%A0%E8%AE%BA%E8%BE%93%E5%85%A5%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%8C%E7%BB%99%E5%AE%9A%E4%B8%93%E5%AE%B6%E8%A2%AB%E9%80%89%E4%B8%AD%E7%9A%84%E5%8F%AF%E8%83%BD%E6%80%A7%E3%80%82%20%E6%88%91%E4%BB%AC%E5%8F%AF%E4%BB%A5%E5%88%A9%E7%94%A8%E8%BF%99%E4%BA%9B%E5%88%86%E6%95%B0%E6%9D%A5%E8%AE%A1%E7%AE%97%E5%8F%98%E5%BC%82%E7%B3%BB%E6%95%B0%EF%BC%88CV%EF%BC%89%EF%BC%8C%E5%AE%83%E5%91%8A%E8%AF%89%E6%88%91%E4%BB%AC%E4%B8%93%E5%AE%B6%E4%B9%8B%E9%97%B4%E7%9A%84%E9%87%8D%E8%A6%81%E6%80%A7%E5%88%86%E6%95%B0%E7%9A%84%E5%B7%AE%E5%BC%82%E7%A8%8B%E5%BA%A6%E3%80%82)" + ] + } + ], + "metadata": { + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebook/2_dataset.ipynb b/notebook/2_dataset.ipynb new file mode 100644 index 0000000..392eabb --- /dev/null +++ b/notebook/2_dataset.ipynb @@ -0,0 +1,562 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "44db347d-fef7-4e56-aeab-ce55bdab8eab", + "metadata": {}, + "source": [ + "# 2-Dataset\n", + "\n", + "到这里我们便完成了对于 MiniMind Tokenizer 和 Model 部分的全部了解,我们所熟悉的大语言模型正是由这个组件构成的,接下来,我们需要对大模型训练所使用的数据集结构有个基本的认识。\n", + "\n", + "想要训练一个能够正常对话,并且符合人类对话偏好的大模型一般需要经过以下几个训练阶段:\n", + "\n", + "- 预训练(Pre-training)\n", + "- 有监督微调(Supervised Fine-tuning,SFT)\n", + "- 人类反馈强化学习(Reinforcement Learning from Human Feedback,RLHF)\n", + "\n", + "在不同训练阶段使用的数据集有所不同,下面会从 MiniMind 代码出发进行介绍和解读。" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e41156fd-ca0e-4083-a7a4-a5f98019ab88", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import random\n", + "import re\n", + "\n", + "import pandas as pd\n", + "import numpy as np\n", + "from torch.utils.data import Dataset, DataLoader\n", + "import torch\n", + "from sklearn.model_selection import train_test_split\n", + "import os\n", + "import ast\n", + "from transformers import AutoTokenizer\n", + "\n", + "os.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "27d18ad2-3f71-435c-af03-ad9d347aa048", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6400\n" + ] + } + ], + "source": [ + "# 从 ../model 目录加载分词器\n", + "\n", + "tokenizer = AutoTokenizer.from_pretrained('../model/minimind_tokenizer')\n", + "print(tokenizer.vocab_size)" + ] + }, + { + "cell_type": "markdown", + "id": "ad08fe92-1d04-4b9c-b689-6ad9d763a7c2", + "metadata": {}, + "source": [ + "## 预训练数据集\n", + "\n", + "预训练是模型在大规模语料上进行无监督学习的训练阶段,在该阶段,模型主要学习下一词预测的能力,简单的来说就是学会说话,而不是胡言乱语。因此,该阶段训练的模型不会具有问答能力,而是根据用户输入进行简单的词语接龙。\n", + "\n", + "我们可以看一看预训练的数据集格式:\n", + "\n", + "```\n", + "{\"text\": \"如何才能摆脱拖延症? 治愈拖延症并不容易,但以下建议可能有所帮助...\"}\n", + "```\n", + "\n", + "为了降低该 demo 的运行门槛,在 `./demo` 文件夹下提供了包含两条训练数据的 `pretrain_data.jsonl` 文件作为熟悉训练流程的数据集 demo。" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cdcac4aa-0ced-4a74-bba0-bc248923c7bc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Row 1: {'text': 'LLM首先要学习的并非直接与人交流,而是让网络参数中充满知识的墨水,“墨水” 理论上喝的越饱越好,产生大量的对世界的知识积累。 预训练就是让Model先埋头苦学大量基本的知识,例如从Wiki百科、新闻、书籍整理大规模的高质量训练数据。 这个过程是“无监督”的,即人类不需要在过程中做任何“有监督”的校正,而是由模型自己从大量文本中总结规律学习知识点。 模型此阶段目的只有一个:学会词语接龙。例如我们输入“秦始皇”四个字,它可以接龙“是中国的第一位皇帝”。'}\n", + "\n", + "Row 2: {'text': '经过预训练,LLM此时已经掌握了大量知识,然而此时它只会无脑地词语接龙,还不会与人聊天。 SFT阶段就需要把半成品LLM施加一个自定义的聊天模板进行微调。 例如模型遇到这样的模板【问题->回答,问题->回答】后不再无脑接龙,而是意识到这是一段完整的对话结束。 称这个过程为指令微调,就如同让已经学富五车的「牛顿」先生适应21世纪智能手机的聊天习惯,学习屏幕左侧是对方消息,右侧是本人消息这个规律。 在训练时,MiniMind的指令和回答长度被截断在512,是为了节省显存空间。就像我们学习时,会先从短的文章开始,当学会写作200字作文后,800字文章也可以手到擒来。 在需要长度拓展时,只需要准备少量的2k/4k/8k长度对话数据进行进一步微调即可(此时最好配合RoPE-NTK的基准差值)。'}\n", + "\n" + ] + } + ], + "source": [ + "# 我们可以查看一下 demo 中提供的数据\n", + "path_pretrain = './toydata/pretrain_data.jsonl'\n", + "\n", + "with open(path_pretrain, 'r', encoding='utf-8') as f:\n", + " for line_num, line in enumerate(f, 1):\n", + " data = json.loads(line.strip())\n", + " print(f'Row {line_num}: {data}\\n')" + ] + }, + { + "cell_type": "markdown", + "id": "0c0707e3-3ff9-424b-a5be-dab9db55aa44", + "metadata": {}, + "source": [ + "我们知道,构建一个深度学习数据集需要继承 `torch.utils.data.dataset`,并构建 DataLoader 数据迭代器进行迭代访问。下面,我们来看看 MiniMind 是如何抽象一个预训练数据集的。" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "fb2458e3-3661-40de-96be-aa036291a8fb", + "metadata": {}, + "outputs": [], + "source": [ + "class PretrainDataset(Dataset):\n", + " def __init__(self, data_path, tokenizer, max_length=512):\n", + " super().__init__()\n", + " self.tokenizer = tokenizer\n", + " self.max_length = max_length\n", + " self.samples = self.load_data(data_path)\n", + "\n", + " def load_data(self, path):\n", + " \"\"\"按行读取 jsonl 文件,并存储在列表中\"\"\"\n", + " samples = []\n", + " with open(path, 'r', encoding='utf-8') as f:\n", + " for line_num, line in enumerate(f, 1):\n", + " data = json.loads(line.strip())\n", + " samples.append(data)\n", + " return samples\n", + "\n", + " def __len__(self):\n", + " return len(self.samples)\n", + "\n", + " def __getitem__(self, index):\n", + " sample = self.samples[index]\n", + "\n", + " text = f\"{self.tokenizer.bos_token}{str(sample['text'])}{self.tokenizer.eos_token}\"\n", + " # print(text) # uncomment to see formating prompt\n", + " encoding = self.tokenizer(\n", + " text,\n", + " max_length=self.max_length,\n", + " padding='max_length',\n", + " truncation=True,\n", + " return_tensors='pt'\n", + " )\n", + " # print(encoding) # uncomment to see encoding result\n", + " input_ids = encoding.input_ids.squeeze()\n", + " loss_mask = (input_ids != self.tokenizer.pad_token_id) # mask to ignore loss on pad token\n", + "\n", + " X = torch.tensor(input_ids[:-1], dtype=torch.long) # token not included\n", + " Y = torch.tensor(input_ids[1:], dtype=torch.long) # token not included\n", + " loss_mask = torch.tensor(loss_mask[1:], dtype=torch.long) # align to tensor X\n", + " return X, Y, loss_mask" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "677a1bbb-0df7-45e5-a3ed-d4431cddf43e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "预训练数据集长度2\n" + ] + } + ], + "source": [ + "pretrain_dataset = PretrainDataset(path_pretrain, tokenizer)\n", + "print(f'预训练数据集长度{len(pretrain_dataset)}')\n", + "# x, y, lm = pretrain_dataset[0]\n", + "# print(x.shape, y.shape, lm.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "ebb160cb-0e96-45a7-a1c0-be145895e70f", + "metadata": {}, + "source": [ + "## 有监督微调数据集\n", + "\n", + "有监督微调(Supervised Fine Tuning,SFT)对预训练后得到的基座 LLM 施加一个自定义聊天模板进行微调,由于在这一阶段,模型训练的目标是根据用户指令生成响应(构建问答体系),故又称为指令微调。\n", + "\n", + "我们可以看一看有监督微调的数据集格式:\n", + "\n", + "```\n", + "{\n", + " \"conversations\": [\n", + " {\"role\": \"user\", \"content\": \"你好\"},\n", + " {\"role\": \"assistant\", \"content\": \"你好!\"},\n", + " {\"role\": \"user\", \"content\": \"再见\"},\n", + " {\"role\": \"assistant\", \"content\": \"再见!\"}\n", + " ]\n", + "}\n", + "```\n", + "\n", + "为了降低该 demo 的运行门槛,在 `./demo` 文件夹下提供了包含两条 conversation 问答数据的 `sft_data.jsonl` 文件作为熟悉训练流程的数据集 demo。" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7e025e25-c937-4468-ae66-b32447440658", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Row 1: {'conversations': [{'role': 'user', 'content': '你好吗?'}, {'role': 'assistant', 'content': '我很好,谢谢!你呢?'}, {'role': 'user', 'content': '我也很好,谢谢!'}, {'role': 'assistant', 'content': '太好了!祝你今天愉快!'}]}\n", + "\n", + "Row 2: {'conversations': [{'role': 'user', 'content': '你喜欢什么运动?'}, {'role': 'assistant', 'content': '我喜欢跑步和游泳。你呢?'}, {'role': 'user', 'content': '我喜欢打篮球!'}, {'role': 'assistant', 'content': '篮球很棒!是一个很好的团队运动。'}]}\n", + "\n" + ] + } + ], + "source": [ + "# 我们可以查看一下 demo 中提供的数据\n", + "path_sft = './toydata/sft_data.jsonl'\n", + "\n", + "with open(path_sft, 'r', encoding='utf-8') as f:\n", + " for line_num, line in enumerate(f, 1):\n", + " data = json.loads(line.strip())\n", + " print(f'Row {line_num}: {data}\\n')" + ] + }, + { + "cell_type": "markdown", + "id": "5f988233-530f-4ddf-8d64-9532442a2535", + "metadata": {}, + "source": [ + "接下来,我们尝试构造一个数据集对象,实现对 sft 格式数据的读取与处理。" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8b25f0c6-b5b5-47b1-974f-15a2f0d2760e", + "metadata": {}, + "outputs": [], + "source": [ + "class SFTDataset(Dataset):\n", + " def __init__(self, jsonl_path, tokenizer, max_length=512):\n", + " super().__init__()\n", + " self.tokenizer = tokenizer\n", + " self.max_length = max_length\n", + " self.samples = self.load_data(jsonl_path)\n", + " self.bos_id = tokenizer('assistant\\n', add_special_tokens=False).input_ids # set bos token\n", + " self.eos_id = tokenizer('\\n', add_special_tokens=False).input_ids # set eos token\n", + "\n", + " def load_data(self, path):\n", + " samples = []\n", + " with open(path, 'r', encoding='utf-8') as f:\n", + " for line_num, line in enumerate(f, 1):\n", + " data = json.loads(line.strip())\n", + " samples.append(data)\n", + " return samples\n", + " \n", + " def __len__(self):\n", + " return len(self.samples)\n", + "\n", + " def _create_chat_prompt(self, conversations):\n", + " \"\"\"构建符合 ChatML 格式的对话\"\"\"\n", + " messages = []\n", + " for i, turn in enumerate(conversations): # for each speaker in one conversation\n", + " role = 'user' if i % 2 == 0 else 'assistant'\n", + " messages.append({\"role\": role, \"content\": turn['content']})\n", + " return self.tokenizer.apply_chat_template(\n", + " messages,\n", + " tokenize=False,\n", + " add_generation_prompt=False\n", + " )\n", + "\n", + " def _generate_loss_mask(self, input_ids):\n", + " loss_mask = [0] * len(input_ids)\n", + " i = 0\n", + " while i < len(input_ids):\n", + " if input_ids[i:i + len(self.bos_id)] == self.bos_id: # check if reach bos token\n", + " ########### find content start & end point ###########\n", + " start = i + len(self.bos_id) # set start point at the end of bos token\n", + " end = start\n", + " while end < len(input_ids):\n", + " if input_ids[end:end + len(self.eos_id)] == self.eos_id: # check if reach eos token\n", + " break\n", + " end += 1\n", + " ######################################################\n", + " for j in range(start + 1, min(end + len(self.eos_id) + 1, self.max_length)): # ignore tokens that reach max input length\n", + " loss_mask[j] = 1\n", + " i = end + len(self.eos_id) if end < len(input_ids) else len(input_ids) # update i to exit current conversation turn\n", + " else:\n", + " i += 1\n", + " return loss_mask\n", + " \n", + " def __getitem__(self, index):\n", + " sample = self.samples[index]\n", + " prompt = self._create_chat_prompt(sample['conversations'])\n", + " # print(prompt) # uncomment to see formating prompt\n", + " input_ids = self.tokenizer(prompt).input_ids[:self.max_length] # encode input\n", + " input_ids += [self.tokenizer.pad_token_id] * (self.max_length - len(input_ids)) # encode padding\n", + " # print(input_ids) # uncomment to see encoded prompt\n", + " \n", + " loss_mask = self._generate_loss_mask(input_ids)\n", + "\n", + " X = torch.tensor(input_ids[:-1], dtype=torch.long)\n", + " Y = torch.tensor(input_ids[1:], dtype=torch.long)\n", + " loss_mask = torch.tensor(loss_mask[1:], dtype=torch.long) # align with pred pos\n", + "\n", + " return X, Y, loss_mask" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "67a62dbf-6a50-4faf-8ad1-5228eb8aa7c9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n", + "样本 shape = torch.Size([511]), 标签 shape = torch.Size([511]), loss_mask shape torch.Size([511])\n" + ] + } + ], + "source": [ + "sft_dataset = SFTDataset(path_sft, tokenizer)\n", + "print(len(sft_dataset))\n", + "x, y, lm = sft_dataset[0]\n", + "print(f'样本 shape = {x.shape}, 标签 shape = {y.shape}, loss_mask shape {lm.shape}')\n", + "# print(lm) # 打印 loss mask,你会发现在序列中有两处以 1 填充的序列,这是因为我们在一个 conversation 中开展了两轮对话,其中只有 assistant 回复计算损失" + ] + }, + { + "cell_type": "markdown", + "id": "a7094ff6-4657-4483-8c34-785ffe9adbd4", + "metadata": {}, + "source": [ + "## 人类反馈强化学习数据集\n", + "\n", + "在 MiniMind 项目中,采用直接偏好优化(Direct Parameter Optimization,DPO)训练大模型对齐人类偏好。在这一训练阶段,模型将会根据提供的问答正反例进行偏好优化,从而降低让人类不满意的答案出现的几率。\n", + "\n", + "与PPO(Proximal Policy Optimization)这种需要奖励模型、价值模型的RL算法不同; DPO通过推导PPO奖励模型的显式解,把在线奖励模型换成离线数据,Ref模型输出可以提前保存。 DPO性能几乎不变,只用跑 actor_model 和 ref_model 两个模型,大大节省显存开销和增加训练稳定性。\n", + "\n", + "我们可以看一看有监督微调的数据集格式:\n", + "\n", + "```\n", + "{\n", + " \"chosen\": [\n", + " {\"content\": \"Query\", \"role\": \"user\"}, \n", + " {\"content\": \"good answer\", \"role\": \"assistant\"}\n", + " ], \n", + " \"rejected\": [\n", + " {\"content\": \"Query\", \"role\": \"user\"}, \n", + " {\"content\": \"bad answer\", \"role\": \"assistant\"}\n", + " ]\n", + "}\n", + "```\n", + "\n", + "为了降低该 demo 的运行门槛,在 ./demo 文件夹下提供了包含两条 conversation 问答数据的 sft_data.jsonl 文件作为熟悉训练流程的数据集 demo。" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "81682b5f-0d3e-4fa2-aec4-0be2b19d5c05", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Row 1: {'conversations': [{'role': 'user', 'content': '你好吗?'}, {'role': 'assistant', 'content': '我很好,谢谢!你呢?'}, {'role': 'user', 'content': '我也很好,谢谢!'}, {'role': 'assistant', 'content': '太好了!祝你今天愉快!'}]}\n", + "\n", + "Row 2: {'conversations': [{'role': 'user', 'content': '你喜欢什么运动?'}, {'role': 'assistant', 'content': '我喜欢跑步和游泳。你呢?'}, {'role': 'user', 'content': '我喜欢打篮球!'}, {'role': 'assistant', 'content': '篮球很棒!是一个很好的团队运动。'}]}\n", + "\n" + ] + } + ], + "source": [ + "# 我们可以查看一下 demo 中提供的数据\n", + "path_dpo = './toydata/dpo_data.jsonl'\n", + "\n", + "with open(path_sft, 'r', encoding='utf-8') as f:\n", + " for line_num, line in enumerate(f, 1):\n", + " data = json.loads(line.strip())\n", + " print(f'Row {line_num}: {data}\\n')" + ] + }, + { + "cell_type": "markdown", + "id": "ebb54557-54f4-4004-8a7b-443b8671002b", + "metadata": {}, + "source": [ + "接下来,我们尝试构造 json 对象,实现对 dpo 格式数据的读取和处理" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "efa98777-5d3a-4b3f-a9fb-e0f05e4af81d", + "metadata": {}, + "outputs": [], + "source": [ + "class DPODataset(Dataset):\n", + " def __init__(self, file_path, tokenizer, max_length=512):\n", + " super().__init__()\n", + " self.tokenizer = tokenizer\n", + " self.max_length = max_length\n", + " self.padding = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else 0\n", + " self.bos_id = tokenizer('assistant\\n', add_special_tokens=False).input_ids # content prefix\n", + " self.eos_id = tokenizer('\\n', add_special_tokens=False).input_ids # content suffix\n", + " with open(file_path, 'r', encoding='utf-8') as f: # load data\n", + " self.data = []\n", + " for line in f:\n", + " line = line.strip()\n", + " obj = json.loads(line)\n", + " self.data.append(obj)\n", + "\n", + " def __len__(self):\n", + " return len(self.data)\n", + "\n", + " def _generate_loss_mask(self, input_ids):\n", + " \"\"\"此处的损失掩码生成函数与 SFT 阶段逻辑一致\"\"\"\n", + " loss_mask = [0] * len(input_ids)\n", + " i = 0\n", + " while i < len(input_ids):\n", + " if input_ids[i:i + len(self.bos_id)] == self.bos_id:\n", + " start = i + len(self.bos_id)\n", + " end = start\n", + " while end < len(input_ids):\n", + " if input_ids[end:end + len(self.eos_id)] == self.eos_id:\n", + " break\n", + " end += 1\n", + " for j in range(start + 1, min(end + len(self.eos_id) + 1, self.max_length)):\n", + " loss_mask[j] = 1\n", + " i = end + len(self.eos_id) if end < len(input_ids) else len(input_ids)\n", + " else:\n", + " i += 1\n", + " return loss_mask\n", + " \n", + " def __getitem__(self, index):\n", + " item = self.data[index]\n", + " chosen = item['chosen'] # 一个 list,里面包含若干 {role, content}\n", + " rejected = item['rejected'] # 同上\n", + " # format prompt\n", + " chosen_prompt = self.tokenizer.apply_chat_template(\n", + " chosen, tokenize=False, add_generation_prompt=False\n", + " )\n", + " print(chosen_prompt) # uncomment to see formating prompt\n", + " rejected_prompt = self.tokenizer.apply_chat_template(\n", + " rejected, tokenize = False, add_gerneration_prompt=False\n", + " )\n", + " print(rejected_prompt) # uncomment to see formating prompt\n", + " # tokenize\n", + " chosen_encoding = self.tokenizer(\n", + " chosen_prompt, truncation=True, max_length=self.max_length, padding='max_length'\n", + " )\n", + " rejected_encoding = self.tokenizer(\n", + " rejected_prompt, truncation=True, max_length=self.max_length, padding='max_length'\n", + " )\n", + " # generate loss mask\n", + " chosen_input_ids = chosen_encoding['input_ids']\n", + " chosen_loss_mask = self._generate_loss_mask(chosen_input_ids)\n", + " rejected_input_ids = rejected_encoding['input_ids']\n", + " rejected_loss_mask = self._generate_loss_mask(rejected_input_ids)\n", + " # same as sft / pretrain\n", + " x_chosen = torch.tensor(chosen_input_ids[:-1], dtype=torch.long)\n", + " y_chosen = torch.tensor(chosen_input_ids[1:], dtype=torch.long)\n", + " mask_chosen = torch.tensor(chosen_loss_mask[1:], dtype=torch.long)\n", + " x_rejected = torch.tensor(rejected_input_ids[:-1], dtype=torch.long)\n", + " y_rejected = torch.tensor(rejected_input_ids[1:], dtype=torch.long)\n", + " mask_rejected = torch.tensor(rejected_loss_mask[1:], dtype=torch.long)\n", + "\n", + " return {\n", + " 'x_chosen': x_chosen,\n", + " 'y_chosen': y_chosen,\n", + " 'mask_chosen': mask_chosen,\n", + " 'x_rejected': x_rejected,\n", + " 'y_rejected': y_rejected,\n", + " 'mask_rejected': mask_rejected\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "5ba205c4-0dd2-48fd-8aa2-26a42afc90d8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DPO 数据集长度:2\n", + "system\n", + "你是 MiniMind,是一个有用的人工智能助手。\n", + "user\n", + "你好吗?\n", + "assistant\n", + "我很好,谢谢!你呢?\n", + "user\n", + "今天过得怎么样?\n", + "assistant\n", + "挺好的,去跑步了,心情不错。\n", + "\n", + "system\n", + "你是 MiniMind,是一个有用的人工智能助手。\n", + "user\n", + "你好吗?\n", + "assistant\n", + "不好,我很累。\n", + "user\n", + "你喜欢什么运动?\n", + "assistant\n", + "我不喜欢运动,没兴趣。\n", + "\n" + ] + } + ], + "source": [ + "dpo_dataset = DPODataset(path_dpo, tokenizer)\n", + "print(f'DPO 数据集长度:{len(dpo_dataset)}')\n", + "res = dpo_dataset[0]\n", + "# 如有需要,请自主查看 res 中的元素" + ] + } + ], + "metadata": { + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebook/3_pretrain.ipynb b/notebook/3_pretrain.ipynb new file mode 100644 index 0000000..173db88 --- /dev/null +++ b/notebook/3_pretrain.ipynb @@ -0,0 +1,376 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "aba635c1-cdcd-4d00-a123-00045edbd10f", + "metadata": {}, + "source": [ + "# 3-Pretrain\n", + "\n", + "预训练是模型经历的第一个阶段,在该阶段,模型将会吸收知识,学习尽可能正确的下一词语预测范式。在这个笔记本中,我们仅对预训练的训练流程进行展示和学习,因此只给出必要的代码片段,如 wandb 和 ddp 不会在此笔记本中涉及。\n", + "\n", + "此笔记本的完整实现见主仓库 `/minimind/train_pretrain.py`" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "98cb706f-2e03-4d8e-8e45-3663c9ab3b64", + "metadata": {}, + "outputs": [], + "source": [ + "# 导入依赖\n", + "import os\n", + "import platform\n", + "import argparse\n", + "import time\n", + "import math\n", + "import warnings\n", + "import pandas as pd\n", + "import torch\n", + "import torch.distributed as dist\n", + "from torch import optim, nn\n", + "from torch.nn.parallel import DistributedDataParallel\n", + "from torch.optim.lr_scheduler import CosineAnnealingLR\n", + "from torch.utils.data import DataLoader, DistributedSampler\n", + "from contextlib import nullcontext\n", + "\n", + "from transformers import AutoTokenizer\n", + "\n", + "from model.model import MiniMindLM\n", + "from model.LMConfig import LMConfig\n", + "from model.dataset import PretrainDataset" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c3b6f015-0e5c-4e88-bb0d-32092993933f", + "metadata": {}, + "outputs": [], + "source": [ + "warnings.filterwarnings('ignore')" + ] + }, + { + "cell_type": "markdown", + "id": "a46c9b5b-12eb-4cad-a859-c5101cdcba82", + "metadata": {}, + "source": [ + "## 可选参数设置\n", + "\n", + "首先,查看训练的可选参数,这些参数在实际使用时通过解析命令行进行导入,我们用 class 进行包装." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3f07e381-961e-4a4f-9f37-ff2f0bc2918c", + "metadata": {}, + "outputs": [], + "source": [ + "class args:\n", + " # out_dir: str = \"out\" # pytorch 格式权重文件保存位置 我们只展示训练过程 所以不使用\n", + " epochs: int = 1 # 训练轮数\n", + " batch_size: int = 2 # pretrain 数据集仅两个样本,设置 batch 为 2\n", + " learning_rate: float = 5e-4 # 学习率\n", + " device: str = 'cuda' if torch.cuda.is_available() else 'cpu'\n", + " dtype: str = 'bfloat16' # 16 bit 浮点数:8 bit 指数 + 7 bit 尾数\n", + " # use_wandb: bool = False # 是否使用 wandb 我们不使用\n", + " wandb_project: str = 'MiniMind-Notebook'\n", + " num_workers: int = 1 # 工作进程数\n", + " # ddp:bool = False # 单机多卡\n", + " accumulation_steps: int = 1 # 梯度累积步数\n", + " grad_clip: float = 1.0 # 梯度剪裁\n", + " warmup_iters: int = 0 # 学习率热启动\n", + " log_interval: int = 1 # 每一步打印日志 仅用于观察\n", + " # save_interval: int = 100 # checkpoint 保存点 我们不使用\n", + " local_rank: int = 1 # device 设备号\n", + " dim: int = 512 # 词嵌入维度 模型超参数\n", + " n_layers: int = 2 # MiniMind Block 数量 模型超参数\n", + " max_seq_len: int = 512 # 序列长度阈值\n", + " use_moe: bool = False # 是否启用混合专家\n", + " data_path: str = './toydata/pretrain_data.jsonl' # 数据集路径" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8f73286e-48a0-4a5b-b7c4-8c5fefea5f14", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "查看工作设备 cuda\n" + ] + } + ], + "source": [ + "print(f'查看工作设备 {args.device}')" + ] + }, + { + "cell_type": "markdown", + "id": "7e4b0ea8-0f41-47fe-8125-93629d03d451", + "metadata": {}, + "source": [ + "## 初始化训练\n", + "\n", + "\n", + "接下来,我们对一些重要模块进行初始化,我们已经了解过,分词器,模型和数据集是大模型的基本组件,我们对其进行初始化。" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9818e376-7f1e-4347-b8c3-c8e30f7d587f", + "metadata": {}, + "outputs": [], + "source": [ + "def init_model(lm_config):\n", + " tokenizer = AutoTokenizer.from_pretrained('../model/minimind_tokenizer')\n", + " model = MiniMindLM(lm_config).to(args.device)\n", + " print(f'LLM总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')\n", + " return model, tokenizer" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "24d15001-b670-4333-9fbe-b224ab1993c5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM总参数量:8.915 百万\n", + "模型位于设备:cuda:0, 词表长度:6400, DataLoader:\n" + ] + } + ], + "source": [ + "lm_config = LMConfig(dim=args.dim, n_layers=args.n_layers, max_seq_len=args.max_seq_len, use_moe=args.use_moe)\n", + "model, tokenizer = init_model(lm_config)\n", + "\n", + "train_ds = PretrainDataset(args.data_path, tokenizer, max_length=lm_config.max_seq_len)\n", + "\n", + "train_loader = DataLoader(\n", + " train_ds,\n", + " batch_size=args.batch_size,\n", + " pin_memory=True,\n", + " drop_last=False,\n", + " shuffle=False,\n", + " num_workers=args.num_workers,\n", + ")\n", + "\n", + "print(f'模型位于设备:{model.device}, 词表长度:{tokenizer.vocab_size}, DataLoader:{train_loader}')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "60c8c970-f448-4ea0-b21b-8f929e03f6fa", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "打印一个 iter 的数据:\n", + "[tensor([[ 1, 46, 46, ..., 0, 0, 0],\n", + " [ 1, 5349, 1619, ..., 0, 0, 0]]), tensor([[ 46, 46, 47, ..., 0, 0, 0],\n", + " [5349, 1619, 2875, ..., 0, 0, 0]]), tensor([[1, 1, 1, ..., 0, 0, 0],\n", + " [1, 1, 1, ..., 0, 0, 0]])]\n", + "\n", + "数据集大小:2, DataLoader 大小:1\n" + ] + } + ], + "source": [ + "loader = iter(train_loader)\n", + "print(f'打印一个 iter 的数据:\\n{next(loader)}\\n')\n", + "print(f'数据集大小:{len(train_ds)}, DataLoader 大小:{len(loader)}')" + ] + }, + { + "cell_type": "markdown", + "id": "0d91904d-7ddf-4105-a5f5-65b84315a73b", + "metadata": {}, + "source": [ + "我们发现,train loader 的每一个 iter 都包含一个长度为 3 的张量列表,这是因为 train_dataset 每一次取数据都会返回三个张量,分别为:\n", + "\n", + "- 样本 X: 包含 \\ 在内的输入 content\n", + "- 标签 Y: 包含 \\ 在内的输出 content\n", + "- 掩码 loss_mask: 指示需要计算损失的 token 位置\n", + "\n", + "由于我们的数据集只有两条数据,而 batch size 设置为 2,因此我们的 dataloader 只有一个 iter." + ] + }, + { + "cell_type": "markdown", + "id": "40764309-9c98-45d5-ada5-e31c1aaa02c5", + "metadata": {}, + "source": [ + "## 启动训练\n", + "\n", + "训练一个深度学习模型,还涉及到了优化器,损失函数和学习率调度。接下来,我们查看 MiniMind 训练部分的代码,并进行一轮简单的训练。" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "4f70b9cf-fcff-4242-bd96-ce34a5da55e4", + "metadata": {}, + "outputs": [], + "source": [ + "# 学习率调度方面 采用余弦退火学习率\n", + "def get_lr(current_step, total_steps, lr):\n", + " return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))\n", + "\n", + "# 优化器方面 选择 AdamW 优化器 并在混精度场景下创建 scaler 进行梯度缩放避免数值下溢\n", + "scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))\n", + "optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)\n", + "\n", + "device_type = \"cuda\" if \"cuda\" in args.device else \"cpu\"\n", + "ctx = nullcontext() if device_type == \"cpu\" else torch.cuda.amp.autocast() # 在 cuda 上启动混精度训练,否则空白上下文" + ] + }, + { + "cell_type": "markdown", + "id": "c101ffb4-1a97-4e29-8612-caf40a8aa05d", + "metadata": {}, + "source": [ + "接下来,我们来看看 MiniMind 的训练函数" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "03043d8e-e90b-42a8-ba4d-de6187f6eb81", + "metadata": {}, + "outputs": [], + "source": [ + "def train_epoch(epoch):\n", + " loss_fct = nn.CrossEntropyLoss(reduction='none') # 损失函数 采用交叉熵损失\n", + " start_time = time.time()\n", + " for step, (X, Y, loss_mask) in enumerate(train_loader):\n", + " X = X.to(args.device)\n", + " Y = Y.to(args.device)\n", + " loss_mask = loss_mask.to(args.device)\n", + "\n", + " lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args.learning_rate)\n", + " for param_group in optimizer.param_groups:\n", + " param_group['lr'] = lr\n", + "\n", + " with ctx:\n", + " res = model(X) # 前向推理\n", + " loss = loss_fct(\n", + " res.logits.view(-1, res.logits.size(-1)),\n", + " Y.view(-1)\n", + " ).view(Y.size()) # 取生成的最后一个 token 的 logits 计算损失\n", + " loss = (loss * loss_mask).sum() / loss_mask.sum()\n", + " loss += res.aux_loss # 若为混合专家 则将 MOE 辅助损失纳入考虑\n", + " loss = loss / args.accumulation_steps # 梯度累积\n", + "\n", + " scaler.scale(loss).backward() # 梯度缩放\n", + "\n", + " if (step + 1) % args.accumulation_steps == 0:\n", + " scaler.unscale_(optimizer)\n", + " torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) # 梯度剪裁\n", + "\n", + " scaler.step(optimizer)\n", + " scaler.update()\n", + "\n", + " optimizer.zero_grad(set_to_none=True) # 单步更新\n", + "\n", + " if step % args.log_interval == 0:\n", + " spend_time = time.time() - start_time\n", + " print(\n", + " 'Epoch:[{}/{}]({}/{}) loss:{:.3f} lr:{:.12f} epoch_Time:{}min:'.format(\n", + " epoch + 1,\n", + " args.epochs,\n", + " step,\n", + " iter_per_epoch,\n", + " loss.item() * args.accumulation_steps,\n", + " optimizer.param_groups[-1]['lr'],\n", + " spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60))\n", + "\n", + " # 到达指定保存步数时,save as PyTorch\n", + " # if (step + 1) % args.save_interval == 0 and (not ddp or dist.get_rank() == 0):\n", + " # model.eval()\n", + " # moe_path = '_moe' if lm_config.use_moe else ''\n", + " # ckp = f'{args.save_dir}/pretrain_{lm_config.dim}{moe_path}.pth'\n", + "\n", + " # if isinstance(model, torch.nn.parallel.DistributedDataParallel):\n", + " # state_dict = model.module.state_dict()\n", + " # else:\n", + " # state_dict = model.state_dict()\n", + "\n", + " # torch.save(state_dict, ckp)\n", + " # model.train()" + ] + }, + { + "cell_type": "markdown", + "id": "7792bf9c-95a1-4c0d-bb53-fac2a1132ee0", + "metadata": {}, + "source": [ + "准备完毕,我们尝试一轮长度 1 个 iter 的训练." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "17f51549-2009-4322-86c8-7aa783169e0b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch:[1/1](0/1) loss:8.959 lr:0.000550000000 epoch_Time:0.0min:\n" + ] + } + ], + "source": [ + "iter_per_epoch = len(train_loader)\n", + "for epoch in range(args.epochs):\n", + " train_epoch(epoch)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "6f257b9e-3a1e-4686-9516-3f9eeb7d0654", + "metadata": {}, + "outputs": [], + "source": [ + "del model" + ] + } + ], + "metadata": { + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebook/4_sft.ipynb b/notebook/4_sft.ipynb new file mode 100644 index 0000000..ff9d79a --- /dev/null +++ b/notebook/4_sft.ipynb @@ -0,0 +1,386 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "70560a4d-ff57-4699-afb2-52f6a145b97e", + "metadata": {}, + "source": [ + "# 4-SFT\n", + "\n", + "在预训练阶段后,我们应该能够获得一个下一词预测模型,此时的模型已经掌握了大量的知识。不过,仅仅具备下一词预测能力是不够的,我们希望大模型能够获得问答能力,这一能力便是在有监督微调(Supervised Fine Tuning,SFT)阶段获得的.\n", + "\n", + "在这个笔记本中,我们仅对 SFT 的训练流程进行展示和学习,因此只给出必要的代码片段,如 wandb 和 ddp 不会在此笔记本中涉及.\n", + "\n", + "此笔记本的完整实现见主仓库 `/minimind/train_full_sft.py`" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "2508c8c7-eb44-4ac0-918c-41479f41d1ac", + "metadata": {}, + "outputs": [], + "source": [ + "# 导入依赖\n", + "import os\n", + "import platform\n", + "import argparse\n", + "import time\n", + "import math\n", + "import warnings\n", + "\n", + "import pandas as pd\n", + "import torch\n", + "import torch.nn.functional as F\n", + "import torch.distributed as dist\n", + "from contextlib import nullcontext\n", + "\n", + "from torch import optim, nn\n", + "from torch.nn.parallel import DistributedDataParallel\n", + "from torch.utils.data import DataLoader, DistributedSampler\n", + "from transformers import AutoTokenizer, AutoModelForCausalLM\n", + "from model.model import MiniMindLM\n", + "from model.LMConfig import LMConfig\n", + "from model.dataset import SFTDataset" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4a79e140-23c7-4498-9a4d-ef7459692996", + "metadata": {}, + "outputs": [], + "source": [ + "warnings.filterwarnings('ignore')" + ] + }, + { + "cell_type": "markdown", + "id": "dbe1d40e-e074-4856-8c67-da8019f250ab", + "metadata": {}, + "source": [ + "## 可选参数设置\n", + "\n", + "首先,查看训练的可选参数,这些参数在实际使用时通过命令行导入,为了保持笔记本的易用性,选择用 class 进行包装." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "98cbd6cf-081f-4c31-8dce-52566004801a", + "metadata": {}, + "outputs": [], + "source": [ + "class args:\n", + " # out_dir: str = \"out\" # pytorch 格式权重文件保存位置 我们只展示训练过程 所以不使用\n", + " epochs: int = 1 # 训练轮数\n", + " batch_size: int = 2 # pretrain 数据集仅两个样本,设置 batch 为 2\n", + " learning_rate: float = 5e-4 # 学习率\n", + " device: str = 'cuda' if torch.cuda.is_available() else 'cpu'\n", + " dtype: str = 'bfloat16' # 16 bit 浮点数:8 bit 指数 + 7 bit 尾数\n", + " # use_wandb: bool = False # 是否使用 wandb 我们不使用\n", + " wandb_project: str = 'MiniMind-Notebook'\n", + " num_workers: int = 1 # 工作进程数\n", + " # ddp:bool = False # 单机多卡\n", + " accumulation_steps: int = 1 # 梯度累积步数\n", + " grad_clip: float = 1.0 # 梯度剪裁\n", + " warmup_iters: int = 0 # 学习率热启动\n", + " log_interval: int = 1 # 每一步打印日志 仅用于观察\n", + " # save_interval: int = 100 # checkpoint 保存点 我们不使用\n", + " local_rank: int = 1 # device 设备号\n", + " dim: int = 512 # 词嵌入维度 模型超参数\n", + " n_layers: int = 2 # MiniMind Block 数量 模型超参数\n", + " max_seq_len: int = 512 # 序列长度阈值\n", + " use_moe: bool = False # 是否启用混合专家\n", + " data_path: str = './toydata/sft_data.jsonl' # 数据集路径" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "642f5107-aa9b-49cc-9759-e8731326edc3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "查看工作设备 cuda\n" + ] + } + ], + "source": [ + "print(f'查看工作设备 {args.device}')" + ] + }, + { + "cell_type": "markdown", + "id": "b70fadb6-f78f-45a0-ad3e-d41d7253c72b", + "metadata": {}, + "source": [ + "## 初始化训练\n", + "\n", + "接下来,我们对一些重要模块进行初始化,我们已经了解过,分词器,模型和数据集是大模型的基本组件,我们对其进行初始化.\n", + "\n", + "> 注意 与预训练阶段不同的是 在 sft 阶段 我们实际上是在上一阶段训练获得的模型的基础上修改数据集进行接续训练 因此需要载入上一阶段的模型权重 出于展示的目的 载入权重的代码在此笔记本中只作展示 并不执行" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c64a0e37-a9d0-4ce6-b5ee-7b2728de879b", + "metadata": {}, + "outputs": [], + "source": [ + "def init_model(lm_config):\n", + " tokenizer = AutoTokenizer.from_pretrained('../model/minimind_tokenizer')\n", + " model = MiniMindLM(lm_config).to(args.device)\n", + " moe_path = '_moe' if lm_config.use_moe else ''\n", + " # ckp = f'./out/pretrain_{lm_config.dim}{moe_path}.pth' # 指示上一阶段训练保存的模型文件位置\n", + " # state_dict = torch.load(ckp, map_location=args.device) # 载入模型状态字典\n", + " # model.laod_state_dict(state_dict, strict=False) # 装入模型\n", + " print(f'LLM总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')\n", + " model = model.to(args.device)\n", + " return model, tokenizer" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2eec33f3-2ac2-4c75-b58e-844d89d8add9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM总参数量:8.915 百万\n", + "模型位于设备:cuda:0, 词表长度:6400, DataLoader:\n" + ] + } + ], + "source": [ + "lm_config = LMConfig(dim=args.dim, n_layers=args.n_layers, max_seq_len=args.max_seq_len, use_moe=args.use_moe)\n", + "model, tokenizer = init_model(lm_config)\n", + "\n", + "train_ds = SFTDataset(args.data_path, tokenizer, max_length=lm_config.max_seq_len)\n", + "\n", + "train_loader = DataLoader(\n", + " train_ds,\n", + " batch_size=args.batch_size,\n", + " pin_memory=True,\n", + " drop_last=False,\n", + " shuffle=False,\n", + " num_workers=args.num_workers,\n", + ")\n", + "\n", + "print(f'模型位于设备:{model.device}, 词表长度:{tokenizer.vocab_size}, DataLoader:{train_loader}')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b02644e0-e61d-4df0-9203-fa841cd83635", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "打印一个 iter 的数据:\n", + "[tensor([[ 1, 85, 736, ..., 0, 0, 0],\n", + " [ 1, 85, 736, ..., 0, 0, 0]]), tensor([[ 85, 736, 201, ..., 0, 0, 0],\n", + " [ 85, 736, 201, ..., 0, 0, 0]]), tensor([[0, 0, 0, ..., 0, 0, 0],\n", + " [0, 0, 0, ..., 0, 0, 0]])]\n", + "\n", + "数据集大小:2, DataLoader 大小:1\n" + ] + } + ], + "source": [ + "loader = iter(train_loader)\n", + "print(f'打印一个 iter 的数据:\\n{next(loader)}\\n')\n", + "print(f'数据集大小:{len(train_ds)}, DataLoader 大小:{len(loader)}')" + ] + }, + { + "cell_type": "markdown", + "id": "13ac8a47-d26a-49da-82fb-468ef5ac91a4", + "metadata": {}, + "source": [ + "我们发现,train loader 的每一个 iter 都包含一个长度为 3 的张量列表,这是因为 train_dataset 每一次取数据都会返回三个张量,分别为:\n", + "\n", + "- 样本 X: 包含 \\ 在内的输入 conversation\n", + "- 标签 Y: 包含 \\ 在内的输出 conversation\n", + "- 掩码 loss_mask: 指示需要计算损失的 token 位置\n", + "\n", + "由于我们的数据集只有两条数据,而 batch size 设置为 2,因此我们的 dataloader 只有一个 iter." + ] + }, + { + "cell_type": "markdown", + "id": "ee23f937-f333-4c02-a3b4-a768818c0e2b", + "metadata": {}, + "source": [ + "## 启动训练\n", + "\n", + "训练一个深度学习模型,还涉及到了优化器,损失函数和学习率调度. 接下来,我们查看 MiniMind 训练部分的代码,并进行一轮简单的训练.\n", + "\n", + "> 不难发现 pretrain 阶段和 sft 阶段的训练主体差不多 因为这两个阶段的差异体现在数据集格式 而数据集在经过 chat template 格式化后差异小了很多" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "5c32a59a-be0b-4426-b47e-c522322bef69", + "metadata": {}, + "outputs": [], + "source": [ + "# 学习率调度方面 采用余弦退火学习率\n", + "def get_lr(current_step, total_steps, lr):\n", + " return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))\n", + "\n", + "# 优化器方面 选择 AdamW 优化器 并在混精度场景下创建 scaler 进行梯度缩放避免数值下溢\n", + "scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))\n", + "optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)\n", + "\n", + "device_type = \"cuda\" if \"cuda\" in args.device else \"cpu\"\n", + "ctx = nullcontext() if device_type == \"cpu\" else torch.cuda.amp.autocast() # 在 cuda 上启动混精度训练,否则空白上下文" + ] + }, + { + "cell_type": "markdown", + "id": "13963ec0-d22e-4120-b0f2-8f5e5abfcda1", + "metadata": {}, + "source": [ + "接下来,我们来看看 MiniMind 的训练函数" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "74499592-2065-4fff-97d1-762cd08b77b6", + "metadata": {}, + "outputs": [], + "source": [ + "def train_epoch(epoch):\n", + " loss_fct = nn.CrossEntropyLoss(reduction='none') # 损失函数 采用交叉熵损失\n", + " start_time = time.time()\n", + " for step, (X, Y, loss_mask) in enumerate(train_loader):\n", + " X = X.to(args.device)\n", + " Y = Y.to(args.device)\n", + " loss_mask = loss_mask.to(args.device)\n", + "\n", + " lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args.learning_rate)\n", + " for param_group in optimizer.param_groups:\n", + " param_group['lr'] = lr\n", + "\n", + " with ctx:\n", + " res = model(X) # 前向推理\n", + " loss = loss_fct(\n", + " res.logits.view(-1, res.logits.size(-1)),\n", + " Y.view(-1)\n", + " ).view(Y.size()) # 取生成的最后一个 token 的 logits 计算损失\n", + " loss = (loss * loss_mask).sum() / loss_mask.sum()\n", + " loss += res.aux_loss # 若为混合专家 则将 MOE 辅助损失纳入考虑\n", + " loss = loss / args.accumulation_steps # 梯度累积\n", + "\n", + " scaler.scale(loss).backward() # 梯度缩放\n", + "\n", + " if (step + 1) % args.accumulation_steps == 0:\n", + " scaler.unscale_(optimizer)\n", + " torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) # 梯度剪裁\n", + "\n", + " scaler.step(optimizer)\n", + " scaler.update()\n", + "\n", + " optimizer.zero_grad(set_to_none=True) # 单步更新\n", + "\n", + " if step % args.log_interval == 0:\n", + " spend_time = time.time() - start_time\n", + " print(\n", + " 'Epoch:[{}/{}]({}/{}) loss:{:.3f} lr:{:.12f} epoch_Time:{}min:'.format(\n", + " epoch + 1,\n", + " args.epochs,\n", + " step,\n", + " iter_per_epoch,\n", + " loss.item() * args.accumulation_steps,\n", + " optimizer.param_groups[-1]['lr'],\n", + " spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60))\n", + "\n", + " # 到达指定保存步数时,save as PyTorch\n", + " # if (step + 1) % args.save_interval == 0 and (not ddp or dist.get_rank() == 0):\n", + " # model.eval()\n", + " # moe_path = '_moe' if lm_config.use_moe else ''\n", + " # ckp = f'{args.save_dir}/pretrain_{lm_config.dim}{moe_path}.pth'\n", + "\n", + " # if isinstance(model, torch.nn.parallel.DistributedDataParallel):\n", + " # state_dict = model.module.state_dict()\n", + " # else:\n", + " # state_dict = model.state_dict()\n", + "\n", + " # torch.save(state_dict, ckp)\n", + " # model.train()" + ] + }, + { + "cell_type": "markdown", + "id": "546f28c2-418e-453c-88aa-695468f9216c", + "metadata": {}, + "source": [ + "准备完毕,我们尝试一轮长度 1 个 iter 的训练." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "341200c9-0d3a-4ed1-9dd2-d2fc35d02564", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch:[1/1](0/1) loss:8.902 lr:0.000550000000 epoch_Time:0.0min:\n" + ] + } + ], + "source": [ + "iter_per_epoch = len(train_loader)\n", + "for epoch in range(args.epochs):\n", + " train_epoch(epoch)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b0480212-1500-49fb-b679-7bdf2c100c87", + "metadata": {}, + "outputs": [], + "source": [ + "del model" + ] + } + ], + "metadata": { + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebook/5_dpo.ipynb b/notebook/5_dpo.ipynb new file mode 100644 index 0000000..06dc24c --- /dev/null +++ b/notebook/5_dpo.ipynb @@ -0,0 +1,457 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f53f31fb-4795-49cf-9a68-a13f8f7e5cae", + "metadata": {}, + "source": [ + "# 5-DPO\n", + "\n", + "直接偏好优化(Direct Preference Optimization,DPO)是后训练阶段中,使用正反样例激励大模型产生符合人类偏好的回答的策略,为人类反馈强化学习(Reinforcement Learning from Human Feedback, RLHF)提供了一个高效简化的替代方案。通过这一阶段的训练,大模型将会学会依照人类的喜好生成回复.\n", + "\n", + "在这个笔记本中,我们仅对 DPO 的训练流程进行展示和学习,因此只给出必要的代码片段,如 wandb 和 ddp 不会在此笔记本中涉及.\n", + "\n", + "此笔记本的完整实现见主仓库 `/minimind/train_dpo.py`" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3e09f234-5b8b-4bfb-8d42-bb766a457acc", + "metadata": {}, + "outputs": [], + "source": [ + "# 导入依赖\n", + "import os\n", + "import platform\n", + "import argparse\n", + "import time\n", + "import math\n", + "import warnings\n", + "\n", + "import pandas as pd\n", + "import torch\n", + "import torch.nn.functional as F\n", + "import torch.distributed as dist\n", + "from contextlib import nullcontext\n", + "\n", + "from torch import optim, nn\n", + "from torch.nn.parallel import DistributedDataParallel\n", + "from torch.utils.data import DataLoader, DistributedSampler\n", + "from transformers import AutoTokenizer, AutoModelForCausalLM\n", + "from model.model import MiniMindLM\n", + "from model.LMConfig import LMConfig\n", + "from model.dataset import DPODataset" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "eb946301-eb7e-45c6-9e1c-16cc0ee4db82", + "metadata": {}, + "outputs": [], + "source": [ + "warnings.filterwarnings('ignore')" + ] + }, + { + "cell_type": "markdown", + "id": "d70c65d3-14f7-4569-bebd-cddb48f1851f", + "metadata": {}, + "source": [ + "## 可选参数设置\n", + "\n", + "首先,查看训练的可选参数,这些参数在实际使用时通过命令行导入,为了保持笔记本的易用性,选择用 class 进行包装." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b78b11a1-9189-4eaf-a094-07c0497c4d38", + "metadata": {}, + "outputs": [], + "source": [ + "class args:\n", + " # out_dir: str = \"out\" # pytorch 格式权重文件保存位置 我们只展示训练过程 所以不使用\n", + " epochs: int = 1 # 训练轮数\n", + " batch_size: int = 2 # pretrain 数据集仅两个样本,设置 batch 为 2\n", + " # sft阶段学习率为 「5e-6」->「5e-7」长度512,建议离线正负样本「概率」偏好对齐阶段lr <=「1e-8」长度3000,否则很容易遗忘训坏\n", + " learning_rate: float = 5e-4 # 学习率\n", + " device: str = 'cuda' if torch.cuda.is_available() else 'cpu'\n", + " dtype: str = 'bfloat16' # 16 bit 浮点数:8 bit 指数 + 7 bit 尾数\n", + " # use_wandb: bool = False # 是否使用 wandb 我们不使用\n", + " wandb_project: str = 'MiniMind-Notebook'\n", + " num_workers: int = 1 # 工作进程数\n", + " # ddp:bool = False # 单机多卡\n", + " accumulation_steps: int = 1 # 梯度累积步数\n", + " grad_clip: float = 1.0 # 梯度剪裁\n", + " warmup_iters: int = 0 # 学习率热启动\n", + " log_interval: int = 1 # 每一步打印日志 仅用于观察\n", + " # save_interval: int = 100 # checkpoint 保存点 我们不使用\n", + " local_rank: int = 1 # device 设备号\n", + " dim: int = 512 # 词嵌入维度 模型超参数\n", + " n_layers: int = 1 # MiniMind Block 数量 模型超参数 | 由于 dpo 要加载两个模型 我们出于演示目的设定 n_layers = 1\n", + " max_seq_len: int = 512 # 序列长度阈值\n", + " use_moe: bool = False # 是否启用混合专家\n", + " data_path: str = './toydata/dpo_data.jsonl' # 数据集路径" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0a72c8aa-ca60-4b0d-aabb-2c9d16b48175", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "查看工作设备 cuda\n" + ] + } + ], + "source": [ + "print(f'查看工作设备 {args.device}')" + ] + }, + { + "cell_type": "markdown", + "id": "f8fb31ff-524b-4788-a25f-699edb887704", + "metadata": {}, + "source": [ + "## 初始化训练\n", + "\n", + "接下来,我们对一些重要模块进行初始化,我们已经了解过,分词器,模型和数据集是大模型的基本组件,我们对其进行初始化.\n", + "\n", + "> 在这一阶段 我们调整的是大模型的问答偏好 因此与 sft 阶段同理 我们需要载入在 sft 阶段微调好的问答模型" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5d5971fc-4794-4903-b418-76f266bf19d9", + "metadata": {}, + "outputs": [], + "source": [ + "def init_model(lm_config):\n", + " \"\"\"注意 在此处我们注释了加载模型部分代码 但应当认识到 actor 和 ref 两个模型都具备 sft 阶段获得的权重\"\"\"\n", + " tokenizer = AutoTokenizer.from_pretrained('../model/minimind_tokenizer')\n", + " model = MiniMindLM(lm_config)\n", + " moe_path = '_moe' if lm_config.use_moe else ''\n", + " # ckp = f'./out/full_sft_{lm_config.dim}{moe_path}.pth' # 指示上一阶段训练保存的模型文件位置\n", + " # state_dict = torch.load(ckp, map_location=args.device) # 载入模型状态字典\n", + " # model.load_state_dict(state_dict, strict=False) # 装入模型\n", + " # 初始化参考模型\n", + " ref_model = MiniMindLM(lm_config)\n", + " # ref_model.load_state_dict(state_dict, strict=False)\n", + " ref_model.eval()\n", + " ref_model.requires_grad_(False)\n", + "\n", + " print(f'LLM总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')\n", + " model = model.to(args.device)\n", + " ref_model = ref_model.to(args.device)\n", + "\n", + " return model, ref_model, tokenizer" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "fbd7bd15-a060-4aba-8c5d-277705ec6b52", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM总参数量:6.096 百万\n", + "模型位于设备:cuda:0, 词表长度:6400, DataLoader:\n" + ] + } + ], + "source": [ + "lm_config = LMConfig(dim=args.dim, n_layers=args.n_layers, max_seq_len=args.max_seq_len, use_moe=args.use_moe)\n", + "model, ref_model ,tokenizer = init_model(lm_config)\n", + "\n", + "train_ds = DPODataset(args.data_path, tokenizer, max_length=lm_config.max_seq_len)\n", + "\n", + "train_loader = DataLoader(\n", + " train_ds,\n", + " batch_size=args.batch_size,\n", + " pin_memory=True,\n", + " drop_last=False,\n", + " shuffle=False,\n", + " num_workers=args.num_workers,\n", + ")\n", + "\n", + "print(f'模型位于设备:{model.device}, 词表长度:{tokenizer.vocab_size}, DataLoader:{train_loader}')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "d807a2af-41e9-4115-b470-a05f6a04613c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "打印一个 iter 的数据:\n", + "{'x_chosen': tensor([[ 1, 85, 736, ..., 0, 0, 0],\n", + " [ 1, 85, 736, ..., 0, 0, 0]]), 'y_chosen': tensor([[ 85, 736, 201, ..., 0, 0, 0],\n", + " [ 85, 736, 201, ..., 0, 0, 0]]), 'mask_chosen': tensor([[0, 0, 0, ..., 0, 0, 0],\n", + " [0, 0, 0, ..., 0, 0, 0]]), 'x_rejected': tensor([[ 1, 85, 736, ..., 0, 0, 0],\n", + " [ 1, 85, 736, ..., 0, 0, 0]]), 'y_rejected': tensor([[ 85, 736, 201, ..., 0, 0, 0],\n", + " [ 85, 736, 201, ..., 0, 0, 0]]), 'mask_rejected': tensor([[0, 0, 0, ..., 0, 0, 0],\n", + " [0, 0, 0, ..., 0, 0, 0]])}\n", + "\n", + "数据集大小:2, DataLoader 大小:1\n" + ] + } + ], + "source": [ + "loader = iter(train_loader)\n", + "print(f'打印一个 iter 的数据:\\n{next(loader)}\\n')\n", + "print(f'数据集大小:{len(train_ds)}, DataLoader 大小:{len(loader)}')" + ] + }, + { + "cell_type": "markdown", + "id": "665ad506-ab12-4186-ad63-ada143a33d52", + "metadata": {}, + "source": [ + "我们发现,train loader 的每一个 iter 都包含一个拥有六个键值对的字典,这是因为 train_dataset 每一次取数据都会返回:\n", + "\n", + "- chosen 样本 X: 包含 \\ 在内的输入 content\n", + "- chosen 标签 Y: 包含 \\ 在内的输出 content\n", + "- chosen 掩码 loss_mask: 指示需要计算损失的 token 位置\n", + "- rejected 样本 X: 包含 \\ 在内的输入 content\n", + "- rejected 标签 Y: 包含 \\ 在内的输出 content\n", + "- rejected 掩码 loss_mask: 指示需要计算损失的 token 位置\n", + "\n", + "由于我们的数据集只有两条数据,而 batch size 设置为 2,因此我们的 dataloader 只有一个 iter." + ] + }, + { + "cell_type": "markdown", + "id": "32462eb9-7008-4389-a54b-47ef1c1c4732", + "metadata": {}, + "source": [ + "# 启动训练\n", + "\n", + "训练一个深度学习模型,还涉及到了优化器,损失函数和学习率调度. 接下来,我们查看 MiniMind 训练部分的代码,并进行一轮简单的训练.\n", + "\n", + "> DPO 阶段涉及 DPO 损失函数涉及 因此与前两个阶段相比内容略有增加 不过整体流程与逻辑类似" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "1752f117-33c0-4810-886f-fbd1b4d44c28", + "metadata": {}, + "outputs": [], + "source": [ + "# 学习率调度方面 采用余弦退火学习率\n", + "def get_lr(current_step, total_steps, lr):\n", + " return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))\n", + "\n", + "# 优化器方面 选择 AdamW 优化器 并在混精度场景下创建 scaler 进行梯度缩放避免数值下溢\n", + "scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))\n", + "optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)\n", + "\n", + "device_type = \"cuda\" if \"cuda\" in args.device else \"cpu\"\n", + "ctx = nullcontext() if device_type == \"cpu\" else torch.cuda.amp.autocast() # 在 cuda 上启动混精度训练,否则空白上下文" + ] + }, + { + "cell_type": "markdown", + "id": "5f2c3ac1-92bf-4eae-8d22-7fac4cfa69df", + "metadata": {}, + "source": [ + "DPO 的原理是增加偏好样本的对数概率与减小非偏好样本响应的对数概率.\n", + "\n", + "该阶段引入 DPO 损失函数,通过计算选择样本和拒绝样本的对数比率,然后基于这些比率计算 DPO 损失,适用于偏好学习任务." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2c07f54a-496e-4b1d-9394-dd2c266ab5d3", + "metadata": {}, + "outputs": [], + "source": [ + "def logits_to_probs(logits, labels):\n", + " # logits shape: (batch_size, seq_len, vocab_size)\n", + " # labels shape: (batch_size, seq_len)\n", + " # probs shape: (batch_size, seq_len)\n", + " log_probs = F.log_softmax(logits, dim=2)\n", + " probs = torch.gather(log_probs, dim=2, index=labels.unsqueeze(2)).squeeze(-1)\n", + " return probs\n", + "\n", + "\n", + "def dpo_loss(ref_probs, probs, beta):\n", + " # ref_probs 和 probs 都是 shape: (batch_size, seq_len)\n", + " # 计算每个样本的平均概率\n", + " ref_probs = ref_probs.mean(dim=1)\n", + " probs = probs.mean(dim=1)\n", + "\n", + " # 将 chosen 和 rejected 数据分开\n", + " batch_size = ref_probs.shape[0]\n", + " chosen_ref_probs = ref_probs[:batch_size // 2]\n", + " reject_ref_probs = ref_probs[batch_size // 2:]\n", + " chosen_probs = probs[:batch_size // 2]\n", + " reject_probs = probs[batch_size // 2:]\n", + "\n", + " # 计算对数比率,比较偏好差异\n", + " pi_logratios = chosen_probs - reject_probs\n", + " ref_logratios = chosen_ref_probs - reject_ref_probs\n", + " logits = pi_logratios - ref_logratios\n", + " loss = -F.logsigmoid(beta * logits)\n", + " return loss.mean()" + ] + }, + { + "cell_type": "markdown", + "id": "e743562c-6e34-488d-bd84-f62eaee0ae20", + "metadata": {}, + "source": [ + "接下来,我们来看看 MiniMind 的训练函数" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "493b7368-d82f-48a6-b5bd-7737b7a23bee", + "metadata": {}, + "outputs": [], + "source": [ + "def train_epoch(epoch):\n", + " start_time = time.time()\n", + " for step, batch in enumerate(train_loader):\n", + " # 提取数据\n", + " x_chosen = batch['x_chosen'].to(args.device)\n", + " x_rejected = batch['x_rejected'].to(args.device)\n", + " y_chosen = batch['y_chosen'].to(args.device)\n", + " y_rejected = batch['y_rejected'].to(args.device)\n", + " mask_chosen = batch['mask_chosen'].to(args.device)\n", + " mask_rejected = batch['mask_rejected'].to(args.device)\n", + " # 正反例拼接\n", + " x = torch.cat([x_chosen, x_rejected], dim=0)\n", + " y = torch.cat([y_chosen, y_rejected], dim=0)\n", + " mask = torch.cat([mask_chosen, mask_rejected], dim=0)\n", + "\n", + " lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args.learning_rate)\n", + " for param_group in optimizer.param_groups:\n", + " param_group['lr'] = lr\n", + "\n", + " with ctx:\n", + " with torch.no_grad(): # 计算 ref 模型输出\n", + " ref_outputs = ref_model(x)\n", + " ref_logits = ref_outputs.logits\n", + " ref_probs = logits_to_probs(ref_logits, y)\n", + " ref_probs = ref_probs * mask # 得到 ref 概率\n", + " outputs = model(x) # 计算 actor 模型输出\n", + " logits = outputs.logits\n", + " probs = logits_to_probs(logits, y)\n", + " probs = probs * mask # 得到 actor 概率\n", + " loss = dpo_loss(ref_probs, probs, beta=0.1) # dpo 损失\n", + " loss = loss / args.accumulation_steps\n", + "\n", + " scaler.scale(loss).backward() # 梯度缩放\n", + "\n", + " if (step + 1) % args.accumulation_steps == 0:\n", + " scaler.unscale_(optimizer)\n", + " torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) # 梯度剪裁\n", + " scaler.step(optimizer)\n", + " scaler.update()\n", + " optimizer.zero_grad(set_to_none=True)\n", + "\n", + " if step % args.log_interval == 0:\n", + " spend_time = time.time() - start_time\n", + " print(\n", + " 'Epoch:[{}/{}]({}/{}) loss:{:.3f} lr:{:.12f} epoch_Time:{}min:'.format(\n", + " epoch + 1,\n", + " args.epochs,\n", + " step,\n", + " iter_per_epoch,\n", + " loss.item(),\n", + " optimizer.param_groups[-1]['lr'],\n", + " spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60))\n", + "\n", + " # 到达指定保存步数时,save as PyTorch\n", + " # if (step + 1) % args.save_interval == 0 and (not ddp or dist.get_rank() == 0):\n", + " # model.eval()\n", + " # moe_path = '_moe' if lm_config.use_moe else ''\n", + " # ckp = f'{args.save_dir}/rlhf_{lm_config.dim}{moe_path}.pth'\n", + "\n", + " # if isinstance(model, torch.nn.parallel.DistributedDataParallel):\n", + " # state_dict = model.module.state_dict()\n", + " # else:\n", + " # state_dict = model.state_dict()\n", + "\n", + " # torch.save(state_dict, ckp)\n", + " # model.train()" + ] + }, + { + "cell_type": "markdown", + "id": "49386dc7-2919-428a-a073-f37d576afbcf", + "metadata": {}, + "source": [ + "准备完毕,我们尝试一轮长度 1 个 iter 的训练." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "0ac86572-ee04-43b5-b5fa-b1c65cbaebec", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch:[1/1](0/1) loss:0.694 lr:0.000550000000 epoch_Time:0.0min:\n" + ] + } + ], + "source": [ + "iter_per_epoch = len(train_loader)\n", + "for epoch in range(args.epochs):\n", + " train_epoch(epoch)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "5215ef67-2647-4519-966b-074c46c225ac", + "metadata": {}, + "outputs": [], + "source": [ + "del model" + ] + } + ], + "metadata": { + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebook/6_lora.ipynb b/notebook/6_lora.ipynb new file mode 100644 index 0000000..0ec199d --- /dev/null +++ b/notebook/6_lora.ipynb @@ -0,0 +1,599 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fd56e0ed-d6e3-4fdd-bed2-2a0c32fb61f9", + "metadata": {}, + "source": [ + "# 6-LoRA\n", + "\n", + "大语言模型的低秩适应(LoRA, Low-Rank Adaptation of Large Language Models) 是一项大模型参数高效微调技术,其可以显著减少可训练参数的数量.\n", + "\n", + "由于大模型参数量较大,直接进行全参微调需要消耗大量硬件资源,LoRA 的工作原理是将少量的新权重插入倒模型中,并且仅训练这些权重,这使得使用 LoRA 进行训练的速度更快、内存效率更高,并生成更小的模型权重.\n", + "\n", + "具体来说,LoRA 它冻结了预训练模型 W 的权重,并注入可训练的秩分解矩阵 A 与 B,在微调时,只训练降维矩阵 A 和 升维矩阵 B,微调结束后,将 AB 与 W 进行叠加.\n", + "\n", + "![images](./images/lora.png)\n", + "\n", + "其中,用随机高斯分布进行初始化 A,用 0 矩阵初始化 B,从而保证训练开始时旁路矩阵为 0 矩阵.\n", + "\n", + "具体来看,假设模型经过预训练主干的输出为 $W_0 x$,在 LoRA 微调阶段,我们可以用如下形式对输出进行表示.\n", + "\n", + "$$h=W_0x + \\Delta Wx = W_0x + BA x=(W_0 + BA)x$$\n", + "\n", + "其中, $B \\in \\mathbb{R}^{d \\times r},A \\in \\mathbb{R}^{r\\times k}$,r 为 LoRA 低秩矩阵的维数,$r << min(d, k)$." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "dad11c0c-043d-44cb-a9a7-4c36c2e909da", + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "from torch import optim, nn" + ] + }, + { + "cell_type": "markdown", + "id": "3031399b-9342-454f-86df-9a96b8c6ff50", + "metadata": {}, + "source": [ + "## LoRA Adapter\n", + "\n", + "简单的来说,LoRA 矩阵就是具有一个隐藏层的全连接网络,其挂接在主干网络边侧进行参数更新,我们来看看 MiniMind 模型是如何在主干网络外部定义 LoRA 网络结构的." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2f9bc623-f13d-4d67-bf2e-dce0784b43f8", + "metadata": {}, + "outputs": [], + "source": [ + "class LoRA(nn.Module):\n", + " def __init__(self, in_features, out_features, rank):\n", + " super().__init__()\n", + " self.rank = rank # LoRA 秩 控制低秩矩阵大小\n", + " self.A = nn.Linear(in_features, rank, bias=False)\n", + " self.B = nn.Linear(rank, out_features, bias=False)\n", + " # 矩阵 A 高斯分布初始化\n", + " self.A.weight.data.normal_(mean=0.0, std=0.02)\n", + " # 矩阵 B 全零初始化\n", + " self.B.weight.data.zero_()\n", + "\n", + " def forward(self, x):\n", + " return self.B(self.A(x))" + ] + }, + { + "cell_type": "markdown", + "id": "4a3557bd-d34d-4a15-b789-22afea60a87d", + "metadata": {}, + "source": [ + "可以看到,LoRA 的网络结构非常简单直观,我们接下来定义一个方法,将 LoRA 网络应用到 MiniMind 模型的特定线性层." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4d52e346-af62-40e2-a5b0-5cc0cf8ae6e8", + "metadata": {}, + "outputs": [], + "source": [ + "def apply_lora(model, rank=16):\n", + " \"\"\"将 LoRA 模块与目标模块进行绑定\"\"\"\n", + " for name, module in model.named_modules():\n", + " if isinstance(module, nn.Linear) and module.weight.shape[0] == module.weight.shape[1]:\n", + " lora = LoRA(module.weight.shape[0], module.weight.shape[1], rank=rank).to(model.device)\n", + " setattr(module, 'lora', lora) # 显式添加 LoRA 模块\n", + " original_forward = module.forward\n", + "\n", + " # 修改目标模块的 forward 函数\n", + " def forward_with_lora(x, layer1=original_forward, layer2=lora):\n", + " return layer1(x) + layer2(x)\n", + " \n", + " module.forward = forward_with_lora\n", + " # 打印 LoRA 绑定的模块名称\n", + " print(f'apply lora on module: {name}')" + ] + }, + { + "cell_type": "markdown", + "id": "753a54c6-a442-4ce5-be89-3bc601bc8d26", + "metadata": {}, + "source": [ + "我们可以声明一个小模型,对于 LoRA 的绑定进行测试." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f830221b-891e-4e27-b401-20b6fe5d3c59", + "metadata": {}, + "outputs": [], + "source": [ + "class TestModel(nn.Module):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.linear1 = nn.Linear(64, 512)\n", + " self.linear2 = nn.Linear(512, 512)\n", + " self.linear3 = nn.Linear(512, 64)\n", + "\n", + " @property\n", + " def device(self):\n", + " return next(self.parameters()).device\n", + " \n", + " def forward(self, x):\n", + " out = self.linear3(self.linear2(self.linear1))\n", + " return out" + ] + }, + { + "cell_type": "markdown", + "id": "1a8edbc2-f36c-4127-be3b-3c598872621a", + "metadata": {}, + "source": [ + "按照 apply_lora 的函数逻辑,LoRA 模块会应用在主干网络中满足 input_feature == output_feature 的模块上." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "eb24e24f-da09-4cd7-b4ce-7fe482194f75", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "apply lora on module: linear2\n", + "TestModel(\n", + " (linear1): Linear(in_features=64, out_features=512, bias=True)\n", + " (linear2): Linear(\n", + " in_features=512, out_features=512, bias=True\n", + " (lora): LoRA(\n", + " (A): Linear(in_features=512, out_features=16, bias=False)\n", + " (B): Linear(in_features=16, out_features=512, bias=False)\n", + " )\n", + " )\n", + " (linear3): Linear(in_features=512, out_features=64, bias=True)\n", + ")\n" + ] + } + ], + "source": [ + "test_model = TestModel()\n", + "apply_lora(test_model)\n", + "print(test_model)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f882e6c0-65a6-47b2-9ee6-4a3fca988636", + "metadata": {}, + "outputs": [], + "source": [ + "del test_model" + ] + }, + { + "cell_type": "markdown", + "id": "9771475e-0c60-42db-b0cd-7e193de23f7c", + "metadata": {}, + "source": [ + "完成了 LoRA 模块在主干网络特定模块的绑定后,我们便可以冻结主干网络参数进行微调了,不过,考虑到主干网络权重在训练过程中并不会做任何参数更新,我们可以只保存 LoRA 模块的参数来节省内存,下面给出加载/保存 LoRA 权重的方法." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "95a453d7-2011-4575-9a51-36d91876874f", + "metadata": {}, + "outputs": [], + "source": [ + "def load_lora(model, path):\n", + " state_dict = torch.load(path, map_location=model.device)\n", + " for name, module in model.named_modules():\n", + " if hasattr(module, 'lora'):\n", + " lora_state = {k.replace(f'{name}.lora.', ''): v for k, v in state_dict.items() if f'{name}.lora.' in k}\n", + " module.lora.load_state_dict(lora_state)\n", + "\n", + "\n", + "def save_lora(model, path):\n", + " state_dict = {}\n", + " for name, module in model.named_modules():\n", + " if hasattr(module, 'lora'):\n", + " lora_state = {f'{name}.lora.{k}': v for k, v in module.lora.state_dict().items()}\n", + " state_dict.update(lora_state)\n", + " torch.save(state_dict, path)" + ] + }, + { + "cell_type": "markdown", + "id": "854b8afb-39b1-4175-8e69-4a9d5b25a3e5", + "metadata": {}, + "source": [ + "## Fine-Tuning MiniMind with LoRA" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "10baf158-6653-49e3-9ba3-22b2e341730f", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import platform\n", + "import argparse\n", + "import random\n", + "import time\n", + "import math\n", + "import warnings\n", + "import torch.distributed as dist\n", + "from contextlib import nullcontext\n", + "from torch.utils.data import DataLoader, DistributedSampler\n", + "from transformers import AutoTokenizer, AutoModelForCausalLM\n", + "from model.model import MiniMindLM\n", + "from model.LMConfig import LMConfig\n", + "from model.dataset import SFTDataset\n", + "\n", + "warnings.filterwarnings('ignore')" + ] + }, + { + "cell_type": "markdown", + "id": "f0b93272-80b3-45ce-9dec-4583fca8f87a", + "metadata": {}, + "source": [ + "### 可选参数设置\n", + "\n", + "首先,查看训练的可选参数,这些参数在实际使用时通过解析命令行进行导入,我们用 class 进行包装." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "bb9d4bac-8a30-4ca9-b293-94df3f880826", + "metadata": {}, + "outputs": [], + "source": [ + "class args:\n", + " # out_dir: str = \"out\" # pytorch 格式权重文件保存位置 我们只展示训练过程 所以不使用\n", + " epochs: int = 1 # 训练轮数\n", + " batch_size: int = 2 # pretrain 数据集仅两个样本,设置 batch 为 2\n", + " learning_rate: float = 5e-4 # 学习率\n", + " device: str = 'cuda' if torch.cuda.is_available() else 'cpu'\n", + " dtype: str = 'bfloat16' # 16 bit 浮点数:8 bit 指数 + 7 bit 尾数\n", + " # use_wandb: bool = False # 是否使用 wandb 我们不使用\n", + " wandb_project: str = 'MiniMind-Notebook'\n", + " num_workers: int = 1 # 工作进程数\n", + " # ddp:bool = False # 单机多卡\n", + " accumulation_steps: int = 1 # 梯度累积步数\n", + " grad_clip: float = 1.0 # 梯度剪裁\n", + " warmup_iters: int = 0 # 学习率热启动\n", + " log_interval: int = 1 # 每一步打印日志 仅用于观察\n", + " # save_interval: int = 100 # checkpoint 保存点 我们不使用\n", + " local_rank: int = 1 # device 设备号\n", + " dim: int = 512 # 词嵌入维度 模型超参数\n", + " n_layers: int = 2 # MiniMind Block 数量 模型超参数\n", + " max_seq_len: int = 512 # 序列长度阈值\n", + " use_moe: bool = False # 是否启用混合专家\n", + " data_path: str = './toydata/lora_data.jsonl' # 数据集路径\n", + " lora_name: str = 'lora_identity' # 根据任务保存成lora_(英文/医学/心理...)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "3b496134-ac9a-4146-a236-fd8867f45bc6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "查看工作设备 cuda\n" + ] + } + ], + "source": [ + "print(f'查看工作设备 {args.device}')" + ] + }, + { + "cell_type": "markdown", + "id": "32b956e6-e81b-49d9-bbf7-b78fbbc7eff5", + "metadata": {}, + "source": [ + "接下来,我们对分词器、MiniMindLM 和数据迭代器执行初始化." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2fb7c880-b86e-4329-aeef-2827837425a3", + "metadata": {}, + "outputs": [], + "source": [ + "def init_model(lm_config):\n", + " tokenizer = AutoTokenizer.from_pretrained('../model/minimind_tokenizer')\n", + " model = MiniMindLM(lm_config)\n", + " moe_path = '_moe' if lm_config.use_moe else ''\n", + " # 热启动\n", + " # ckp = f'./out/rlhf_{lm_config.dim}{moe_path}.pth'\n", + " # state_dict = torch.load(ckp, map_location=args.device)\n", + " # model.load_state_dict(state_dict, strict=False)\n", + " return model.to(args.device), tokenizer" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "7f0c74d0-a191-40c1-b922-104f241aa879", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "apply lora on module: layers.0.attention.wq\n", + "apply lora on module: layers.0.attention.wo\n", + "apply lora on module: layers.1.attention.wq\n", + "apply lora on module: layers.1.attention.wo\n", + "模型位于设备:cuda:0, 词表长度:6400, DataLoader:\n" + ] + } + ], + "source": [ + "lm_config = LMConfig(dim=args.dim, n_layers=args.n_layers, max_seq_len=args.max_seq_len, use_moe=args.use_moe)\n", + "model, tokenizer = init_model(lm_config)\n", + "apply_lora(model)\n", + "\n", + "# 由于 MiniMind 用于 LoRA 微调的数据集和 SFT 数据集格式相同,可以用 SFT 数据集进行加载\n", + "train_ds = SFTDataset(args.data_path, tokenizer, max_length=lm_config.max_seq_len)\n", + "\n", + "train_loader = DataLoader(\n", + " train_ds,\n", + " batch_size=args.batch_size,\n", + " pin_memory=True,\n", + " drop_last=False,\n", + " shuffle=False,\n", + " num_workers=args.num_workers,\n", + ")\n", + "\n", + "print(f'模型位于设备:{model.device}, 词表长度:{tokenizer.vocab_size}, DataLoader:{train_loader}')" + ] + }, + { + "cell_type": "markdown", + "id": "c35bb3ae-0b31-42ff-88d1-04d7e945bd31", + "metadata": {}, + "source": [ + "可以看到,LoRA 模块挂接在 Attention Block 的 Query 与 Output 线性层上,下面查看 LoRA 微调下可学习参数的占比:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "766ef2d5-1f24-4efb-9eb9-f969a57a85b0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM 总参数量: 8980992\n", + "LoRA 参数量: 65536\n", + "LoRA 参数占比: 0.73%\n" + ] + } + ], + "source": [ + "total_params = sum(p.numel() for p in model.parameters()) # 总参数数量\n", + "lora_params_count = sum(p.numel() for name, p in model.named_parameters() if 'lora' in name) # LoRA 参数数量\n", + "print(f\"LLM 总参数量: {total_params}\")\n", + "print(f\"LoRA 参数量: {lora_params_count}\")\n", + "print(f\"LoRA 参数占比: {lora_params_count / total_params * 100:.2f}%\")" + ] + }, + { + "cell_type": "markdown", + "id": "8a487807-65da-4e19-ac2f-fce7077494c0", + "metadata": {}, + "source": [ + "接下来,冻结 MiniMindLM 主干网络的参数,做好 LoRA 微调准备." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "db99fd52-4f07-4278-939f-47b91aeeedbe", + "metadata": {}, + "outputs": [], + "source": [ + "for name, param in model.named_parameters():\n", + " if 'lora' not in name:\n", + " param.requires_grad = False\n", + "lora_params = [] # 收集 LoRA 模块的可学习参数, 提供给优化器\n", + "for name, param in model.named_parameters():\n", + " if 'lora' in name:\n", + " lora_params.append(param)" + ] + }, + { + "cell_type": "markdown", + "id": "6f865b22-35a7-4c2c-85a1-a13d1ada12a7", + "metadata": {}, + "source": [ + "### 启动训练\n", + "\n", + "接下来,我们定义 MiniMind LoRA 微调所使用的优化器,损失函数和学习率调度,并进行一轮简单的训练." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "2b57beaf-c04a-4b88-9617-7142054f2927", + "metadata": {}, + "outputs": [], + "source": [ + "# 学习率调度方面 采用余弦退火学习率\n", + "def get_lr(current_step, total_steps, lr):\n", + " return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))\n", + "\n", + "# 优化器方面 选择 AdamW 优化器 并在混精度场景下创建 scaler 进行梯度缩放避免数值下溢\n", + "scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))\n", + "# 只训练 LoRA 模块参数\n", + "optimizer = optim.AdamW(lora_params, lr=args.learning_rate)\n", + "\n", + "device_type = \"cuda\" if \"cuda\" in args.device else \"cpu\"\n", + "ctx = nullcontext() if device_type == \"cpu\" else torch.cuda.amp.autocast() # 在 cuda 上启动混精度训练,否则空白上下文" + ] + }, + { + "cell_type": "markdown", + "id": "8037d2b2-1fa3-4e19-9ad5-9674ff866a36", + "metadata": {}, + "source": [ + "接下来,我们来看训练函数." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "8f50d42d-da6d-4f8b-aa35-853b3900f8b5", + "metadata": {}, + "outputs": [], + "source": [ + "# 代码和 SFT 几乎一致\n", + "def train_epoch(epoch):\n", + " loss_fct = nn.CrossEntropyLoss(reduction='none')\n", + " start_time = time.time()\n", + " for step, (X, Y, loss_mask) in enumerate(train_loader):\n", + " X = X.to(args.device)\n", + " Y = Y.to(args.device)\n", + " loss_mask = loss_mask.to(args.device)\n", + " lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args.learning_rate)\n", + " for param_group in optimizer.param_groups:\n", + " param_group['lr'] = lr\n", + "\n", + " with ctx:\n", + " res = model(X)\n", + " loss = loss_fct(\n", + " res.logits.view(-1, res.logits.size(-1)),\n", + " Y.view(-1)\n", + " ).view(Y.size())\n", + " loss = (loss * loss_mask).sum() / loss_mask.sum()\n", + " loss += res.aux_loss\n", + " loss = loss / args.accumulation_steps\n", + "\n", + " scaler.scale(loss).backward() # 梯度缩放\n", + "\n", + " if (step + 1) % args.accumulation_steps == 0:\n", + " scaler.unscale_(optimizer)\n", + " torch.nn.utils.clip_grad_norm_(lora_params, args.grad_clip) # 梯度剪裁\n", + "\n", + " scaler.step(optimizer)\n", + " scaler.update()\n", + "\n", + " optimizer.zero_grad(set_to_none=True)\n", + "\n", + " if step % args.log_interval == 0:\n", + " spend_time = time.time() - start_time\n", + " print(\n", + " 'Epoch:[{}/{}]({}/{}) loss:{:.3f} lr:{:.12f} epoch_Time:{}min:'.format(\n", + " epoch + 1,\n", + " args.epochs,\n", + " step,\n", + " iter_per_epoch,\n", + " loss.item(),\n", + " optimizer.param_groups[-1]['lr'],\n", + " spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60))\n", + "\n", + " # 只保存 LoRA 模块的权重\n", + " # if (step + 1) % args.save_interval == 0 and (not ddp or dist.get_rank() == 0):\n", + " # model.eval()\n", + " # # 【区别1】只保存lora权重即可\n", + " # save_lora(model, f'{args.save_dir}/lora/{args.lora_name}_{lm_config.dim}.pth')\n", + " # model.train()" + ] + }, + { + "cell_type": "markdown", + "id": "618876fa-7f0f-43b3-9eed-384be6cd66be", + "metadata": {}, + "source": [ + "接下来,我们启动一个 Epoch 的训练进行观察." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "38584341-9ef6-4ebd-9024-0c44341de7c8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch:[1/1](0/1) loss:8.883 lr:0.000550000000 epoch_Time:0.0min:\n" + ] + } + ], + "source": [ + "iter_per_epoch = len(train_loader)\n", + "for epoch in range(args.epochs):\n", + " train_epoch(epoch)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "61150c72-39eb-4b90-b321-d4c7aa6c885a", + "metadata": {}, + "outputs": [], + "source": [ + "del model" + ] + }, + { + "cell_type": "markdown", + "id": "dd5e9aa8-b5a5-4380-989d-e54948f33a1b", + "metadata": {}, + "source": [ + "## 参考资料\n", + "\n", + "- [LoRA: Low-Rank Adaptation of Large Language Models](https://arxiv.org/abs/2106.09685)\n", + "- [HuggingFace LoRA](https://huggingface.co/docs/diffusers/en/training/lora)\n", + "- [LoRA 微调和低秩矩阵](https://www.cnblogs.com/ghj1976/p/18032882/lora-finetuning-he-di-zhi-ju-zhen)" + ] + } + ], + "metadata": { + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebook/7_distill.ipynb b/notebook/7_distill.ipynb new file mode 100644 index 0000000..6b7b589 --- /dev/null +++ b/notebook/7_distill.ipynb @@ -0,0 +1,430 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e0f42399-b424-41a3-b179-a6f1165a92e7", + "metadata": {}, + "source": [ + "# 7-Distill\n", + "\n", + "模型蒸馏 (Knowledge Distillation, KD) 是一种机器学习模型压缩方法,它用于将大型模型(教师模型)的知识迁移到较小的模型(学生模型)中.\n", + "\n", + "KD 背后的核心思想是将教师模型的综合知识转化为更精简、更有效的表示. 学生模型是一个较小的模型,目标是学习教师模型的行为,而不是直接从原始数据中学习.\n", + "\n", + "大模型的 KD 有白盒蒸馏与黑盒蒸馏两个派别,对于本次实验代码中两个模型均为 MiniMind 开源模型,支持对教师模型内部结构的访问,因此在训练过程中,我们能够获取教师模型的 softmax 概率分布并用作软标签(soft labels),让小模型学习软标签,并使用 KL-Loss 来优化模型的参数,而不是直接学习输出 Token 的硬标签. 对于下一章蒸馏推理模型中,由于我们面向推理数据集进行蒸馏,并不存在输出 Token 的概率分布让我们学习,这种面向输出数据学习的蒸馏方式被称为黑盒蒸馏.\n", + "\n", + "此笔记本的完整实现见主仓库 `/minimind/train_distillation.py`" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "fb823b9f-c0d3-4566-ad9c-f9ec6f95c446", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import argparse\n", + "import time\n", + "import math\n", + "import warnings\n", + "\n", + "import pandas as pd\n", + "import torch\n", + "import torch.nn.functional as F\n", + "import torch.distributed as dist\n", + "from contextlib import nullcontext\n", + "\n", + "from torch import optim, nn\n", + "from torch.nn.parallel import DistributedDataParallel\n", + "from torch.utils.data import DataLoader, DistributedSampler\n", + "from transformers import AutoTokenizer, AutoModelForCausalLM\n", + "from model.model import MiniMindLM\n", + "from model.LMConfig import LMConfig\n", + "from model.dataset import SFTDataset\n", + "\n", + "warnings.filterwarnings('ignore')" + ] + }, + { + "cell_type": "markdown", + "id": "5f4546ae-9520-4f67-8530-0dfed996fa35", + "metadata": {}, + "source": [ + "## 可选参数设置\n", + "\n", + "首先,查看训练的可选参数,这些参数在实际使用时通过解析命令行进行导入,我们用 class 进行包装." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c7907dbe-ca43-4904-b598-ab9e5f019982", + "metadata": {}, + "outputs": [], + "source": [ + "class args:\n", + " # out_dir: str = \"out\" # pytorch 格式权重文件保存位置 我们只展示训练过程 所以不使用\n", + " epochs: int = 1 # 训练轮数\n", + " batch_size: int = 2 # pretrain 数据集仅两个样本,设置 batch 为 2\n", + " learning_rate: float = 5e-4 # 学习率\n", + " device: str = 'cuda' if torch.cuda.is_available() else 'cpu'\n", + " dtype: str = 'bfloat16' # 16 bit 浮点数:8 bit 指数 + 7 bit 尾数\n", + " # use_wandb: bool = False # 是否使用 wandb 我们不使用\n", + " wandb_project: str = 'MiniMind-Notebook'\n", + " num_workers: int = 1 # 工作进程数\n", + " # ddp:bool = False # 单机多卡\n", + " accumulation_steps: int = 1 # 梯度累积步数\n", + " grad_clip: float = 1.0 # 梯度剪裁\n", + " warmup_iters: int = 0 # 学习率热启动\n", + " log_interval: int = 1 # 每一步打印日志 仅用于观察\n", + " # save_interval: int = 100 # checkpoint 保存点 我们不使用\n", + " local_rank: int = 1 # device 设备号\n", + " dim: int = 512 # 词嵌入维度 模型超参数\n", + " n_layers: int = 2 # MiniMind Block 数量 模型超参数\n", + " max_seq_len: int = 512 # 序列长度阈值\n", + " use_moe: bool = False # 是否启用混合专家\n", + " data_path: str = './toydata/sft_data.jsonl' # 数据集路径" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "044bb66f-f878-4785-b860-a9a89b52f94c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "查看工作设备 cuda\n" + ] + } + ], + "source": [ + "print(f'查看工作设备 {args.device}')" + ] + }, + { + "cell_type": "markdown", + "id": "68f3ea54-b23c-4d2c-abe7-63bbe9f1e5dd", + "metadata": {}, + "source": [ + "接下来,我们对分词器、MiniMind 教师/学生模型以及数据迭代器执行初始化." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f355d146-ee89-41c6-ba79-7aba75e3d151", + "metadata": {}, + "outputs": [], + "source": [ + "def init_student_model(lm_config):\n", + " tokenizer = AutoTokenizer.from_pretrained('../model/minimind_tokenizer')\n", + " model = MiniMindLM(lm_config)\n", + " moe_path = '_moe' if lm_config.use_moe else ''\n", + " # 学生模型热启动\n", + " # ckp = f'./out/full_sft_{lm_config.dim}{moe_path}.pth'\n", + " # state_dict = torch.load(ckp, map_location=args.device)\n", + " # model.load_state_dict(state_dict, strict=False)\n", + " print(f'学生模型(LLM)总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')\n", + " model = model.to(args.device)\n", + " return model, tokenizer\n", + "\n", + "\n", + "def init_teacher_model(lm_config):\n", + " model = MiniMindLM(lm_config)\n", + " moe_path = '_moe' if lm_config.use_moe else ''\n", + " # 教师模型热启动\n", + " # ckp = f'./out/full_sft_{lm_config.dim}{moe_path}.pth'\n", + " # state_dict = torch.load(ckp, map_location=args.device)\n", + " # model.load_state_dict(state_dict, strict=False)\n", + " print(f'教师模型(LLM)总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')\n", + " model = model.to(args.device)\n", + " return model" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5592c247-4424-45e0-949a-0a56b1604f83", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "学生模型(LLM)总参数量:6.096 百万\n", + "教师模型(LLM)总参数量:17.305 百万\n", + "模型位于设备:cuda:0, 词表长度:6400, DataLoader:\n" + ] + } + ], + "source": [ + "# 初始化模型配置,一般学生模型比较小,从大的教师模型那里学知识\n", + "lm_config_student = LMConfig(dim=512, n_layers=1, max_seq_len=512)\n", + "lm_config_teacher = LMConfig(dim=768, n_layers=2, max_seq_len=512)\n", + "\n", + "model, tokenizer = init_student_model(lm_config_student)\n", + "teacher_model = init_teacher_model(lm_config_teacher)\n", + "\n", + "train_ds = SFTDataset(args.data_path, tokenizer, max_length=lm_config_student.max_seq_len)\n", + "train_loader = DataLoader(\n", + " train_ds,\n", + " batch_size=args.batch_size,\n", + " pin_memory=True,\n", + " drop_last=False,\n", + " shuffle=False,\n", + " num_workers=args.num_workers,\n", + ")\n", + "\n", + "print(f'模型位于设备:{model.device}, 词表长度:{tokenizer.vocab_size}, DataLoader:{train_loader}')" + ] + }, + { + "cell_type": "markdown", + "id": "9af25f3d-6127-4053-92e6-533bd889a917", + "metadata": {}, + "source": [ + "## 启动训练\n", + "\n", + "接下来,我们定义 MiniMind LoRA 微调所使用的优化器,损失函数和学习率调度,并进行一轮简单的训练." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "ae344278-4f4c-495e-8c1b-e5c542ae7a92", + "metadata": {}, + "outputs": [], + "source": [ + "# 学习率调度方面 采用余弦退火学习率\n", + "def get_lr(current_step, total_steps, lr):\n", + " return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))\n", + "\n", + "# 优化器方面 选择 AdamW 优化器 并在混精度场景下创建 scaler 进行梯度缩放避免数值下溢\n", + "scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))\n", + "# 优化学生模型参数\n", + "optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)\n", + "\n", + "device_type = \"cuda\" if \"cuda\" in args.device else \"cpu\"\n", + "ctx = nullcontext() if device_type == \"cpu\" else torch.cuda.amp.autocast() # 在 cuda 上启动混精度训练,否则空白上下文" + ] + }, + { + "cell_type": "markdown", + "id": "ceadf787-ab7c-4e3e-958a-4b3cd3126a08", + "metadata": {}, + "source": [ + "损失函数方面,使用 KL Loss 方法. \n", + "\n", + "KL Loss 中,损失是 KL 散度,衡量学生模型和教师模型在面对相同输入时,在输出层产生的分类 logits 分布之间的距离. 直观理解上,就是让学生模型的输出尽量向教师模型的输出概率靠近.\n", + "\n", + "$$D_{KL}(P||Q)=\\sum_i P(i)\\log\\frac{P(i)}{Q(i)}$$\n", + "\n", + "其中,$P(i)$ 代表教师模型的概率分布,$Q(i)$ 代表学生模型的预测分布." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "d273971f-50ec-4319-807d-1a8e59ea9813", + "metadata": {}, + "outputs": [], + "source": [ + "def distillation_loss_fn(student_logits, teacher_logits, temperature=1.0, reduction='batchmean'):\n", + " with torch.no_grad():\n", + " teacher_probs = F.softmax(teacher_logits / temperature, dim=-1).detach()\n", + "\n", + " student_log_probs = F.log_softmax(student_logits / temperature, dim=-1)\n", + "\n", + " kl = F.kl_div(\n", + " student_log_probs,\n", + " teacher_probs,\n", + " reduction=reduction # 对各批次损失求平均值\n", + " )\n", + " return (temperature ** 2) * kl # 尺度不变" + ] + }, + { + "cell_type": "markdown", + "id": "2ce6f00d-1cbd-47ab-8736-991eeb904cd9", + "metadata": {}, + "source": [ + "接下来,我们来看训练函数." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cd01790d-6a18-40ab-b133-da31b3ca6b66", + "metadata": {}, + "outputs": [], + "source": [ + "# 作者在训练函数上的注释浅显易懂 故不作额外注释\n", + "def train_epoch(epoch, alpha=0.0, temperature=1.0):\n", + " start_time = time.time()\n", + "\n", + " if teacher_model is not None: # 禁用教师模型梯度\n", + " teacher_model.eval()\n", + " teacher_model.requires_grad_(False)\n", + "\n", + " for step, (X, Y, loss_mask) in enumerate(train_loader):\n", + " X = X.to(args.device)\n", + " Y = Y.to(args.device)\n", + " loss_mask = loss_mask.to(args.device)\n", + " lr = get_lr(epoch * iter_per_epoch + step,\n", + " args.epochs * iter_per_epoch,\n", + " args.learning_rate)\n", + " for param_group in optimizer.param_groups:\n", + " param_group['lr'] = lr\n", + "\n", + " # 前向传播(学生模型)\n", + " with ctx:\n", + " res = model(X)\n", + " student_logits = res.logits\n", + "\n", + " # 教师模型前向传播(只在eval & no_grad)\n", + " if teacher_model is not None:\n", + " with torch.no_grad():\n", + " teacher_logits = teacher_model(X).logits\n", + " vocab_size_student = student_logits.size(-1) # N\n", + " teacher_logits = teacher_logits[..., :vocab_size_student] # ... 保留除了最后一个维度外的所有维度\n", + "\n", + " # ========== 计算损失 ==========\n", + " # 1) Ground-Truth CE Loss(可选)\n", + " loss_mask_flat = loss_mask.view(-1)\n", + " ce_loss = F.cross_entropy(\n", + " student_logits.view(-1, student_logits.size(-1)),\n", + " Y.view(-1),\n", + " ignore_index=0,\n", + " reduction='none'\n", + " )\n", + " ce_loss = torch.sum(ce_loss * loss_mask_flat) / loss_mask_flat.sum()\n", + " if lm_config_student.use_moe:\n", + " ce_loss += res.aux_loss\n", + "\n", + " # 2) Distillation Loss(可选)\n", + " if teacher_model is not None:\n", + " # 只在有效token位置做蒸馏\n", + " distill_loss = distillation_loss_fn(\n", + " student_logits.view(-1, student_logits.size(-1))[loss_mask_flat == 1],\n", + " teacher_logits.view(-1, teacher_logits.size(-1))[loss_mask_flat == 1],\n", + " temperature=temperature\n", + " )\n", + " else:\n", + " distill_loss = torch.tensor(0.0, device=args.device)\n", + "\n", + " # 3) 总损失 = alpha * CE + (1-alpha) * Distill\n", + " loss = alpha * ce_loss + (1 - alpha) * distill_loss\n", + "\n", + " scaler.scale(loss).backward()\n", + "\n", + " if (step + 1) % args.accumulation_steps == 0:\n", + " scaler.unscale_(optimizer)\n", + " torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)\n", + " scaler.step(optimizer)\n", + " scaler.update()\n", + " optimizer.zero_grad(set_to_none=True)\n", + "\n", + " if step % args.log_interval == 0:\n", + " spend_time = time.time() - start_time\n", + " print(\n", + " 'Epoch:[{}/{}]({}/{}) loss:{:.4f} lr:{:.12f} epoch_Time:{}min:'.format(\n", + " epoch,\n", + " args.epochs - 1,\n", + " step,\n", + " iter_per_epoch,\n", + " loss.item(),\n", + " optimizer.param_groups[-1]['lr'],\n", + " spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60\n", + " )\n", + " )\n", + "\n", + " # 到达指定保存步数时,save as PyTorch\n", + " # if (step + 1) % args.save_interval == 0 and (not ddp or dist.get_rank() == 0):\n", + " # model.eval()\n", + " # moe_path = '_moe' if lm_config_student.use_moe else ''\n", + " # ckp = f'{args.save_dir}/full_dist_{lm_config_student.dim}{moe_path}.pth'\n", + " # if isinstance(model, torch.nn.parallel.DistributedDataParallel):\n", + " # state_dict = model.module.state_dict()\n", + " # else:\n", + " # state_dict = model.state_dict()\n", + " # torch.save(state_dict, ckp)\n", + " # model.train()" + ] + }, + { + "cell_type": "markdown", + "id": "b5369c12-e315-4a21-8f23-42dc0151909e", + "metadata": {}, + "source": [ + "接下来,我们启动一个 Epoch 的训练进行观察." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "e73d59fc-677a-4795-8377-cdef06e8e1d9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch:[0/0](0/1) loss:0.3257 lr:0.000550000000 epoch_Time:0.0min:\n" + ] + } + ], + "source": [ + "iter_per_epoch = len(train_loader)\n", + "for epoch in range(args.epochs):\n", + " train_epoch(epoch)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "94cd3e5c-e37b-4b37-919f-7a1066b0f482", + "metadata": {}, + "outputs": [], + "source": [ + "del model, teacher_model" + ] + }, + { + "cell_type": "markdown", + "id": "e3ff1681-962d-4f4b-a75d-e4673978e5d3", + "metadata": {}, + "source": [ + "## 参考资料\n", + "\n", + "- [大模型知识蒸馏概述](https://zhuanlan.zhihu.com/p/659943824)\n", + "- [使用知识蒸馏将大模型能力克隆到小模型](https://zhuanlan.zhihu.com/p/691672620)\n", + "- [理解知识蒸馏中的散度损失函数](https://deepseek.csdn.net/67ab1c3f79aaf67875cb9664.html)" + ] + } + ], + "metadata": { + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebook/8_reason.ipynb b/notebook/8_reason.ipynb new file mode 100644 index 0000000..e7329c9 --- /dev/null +++ b/notebook/8_reason.ipynb @@ -0,0 +1,359 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2edfeb4e-03ea-406a-a345-6fe19d509c37", + "metadata": {}, + "source": [ + "# 8-Reason\n", + "\n", + "参数太小的模型直接通过冷启动SFT+GRPO几乎不可能获得任何推理效果,因此,使用冷启动 SFT + GRPO 训练方法对小模型推理能力的作用有限.因此,MiniMind 项目作者使用推理数据集对 MiniMind 系列模型进行黑盒蒸馏来训练推理模型.\n", + "\n", + "使用的推理数据格式:\n", + "\n", + "```\n", + "{\n", + " \"conversations\": [\n", + " {\"role\": \"user\", \"content\": \"Q1?\"},\n", + " {\"role\": \"assistant\", \"content\": \"T1\\nA1\"},\n", + " {\"role\": \"user\", \"content\": \"Q2?\"},\n", + " {\"role\": \"assistant\", \"content\": \"T2\\nA2\"}\n", + " ]\n", + "}\n", + "```\n", + "\n", + "此笔记本的完整实现见主仓库 `/minimind/train_distill_reason.py`" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f7eb789f-18d9-45c5-acab-ca3167950911", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import platform\n", + "import argparse\n", + "import time\n", + "import math\n", + "import warnings\n", + "\n", + "import pandas as pd\n", + "import torch\n", + "import torch.nn.functional as F\n", + "import torch.distributed as dist\n", + "from contextlib import nullcontext\n", + "\n", + "from torch import optim, nn\n", + "from torch.nn.parallel import DistributedDataParallel\n", + "from torch.utils.data import DataLoader, DistributedSampler\n", + "from transformers import AutoTokenizer, AutoModelForCausalLM\n", + "from model.model import MiniMindLM\n", + "from model.LMConfig import LMConfig\n", + "from model.dataset import SFTDataset\n", + "\n", + "warnings.filterwarnings('ignore')" + ] + }, + { + "cell_type": "markdown", + "id": "24cefe29-fdf0-44c7-86fa-97206bc18161", + "metadata": {}, + "source": [ + "## 可选参数设置\n", + "\n", + "首先,查看训练的可选参数,这些参数在实际使用时通过解析命令行进行导入,我们用 class 进行包装." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "22643ad2-fc7a-4efd-ac53-81c2bcc88cd1", + "metadata": {}, + "outputs": [], + "source": [ + "class args:\n", + " # out_dir: str = \"out\" # pytorch 格式权重文件保存位置 我们只展示训练过程 所以不使用\n", + " epochs: int = 1 # 训练轮数\n", + " batch_size: int = 2 # pretrain 数据集仅两个样本,设置 batch 为 2\n", + " learning_rate: float = 5e-4 # 学习率\n", + " device: str = 'cuda' if torch.cuda.is_available() else 'cpu'\n", + " dtype: str = 'bfloat16' # 16 bit 浮点数:8 bit 指数 + 7 bit 尾数\n", + " # use_wandb: bool = False # 是否使用 wandb 我们不使用\n", + " wandb_project: str = 'MiniMind-Notebook'\n", + " num_workers: int = 1 # 工作进程数\n", + " # ddp:bool = False # 单机多卡\n", + " accumulation_steps: int = 1 # 梯度累积步数\n", + " grad_clip: float = 1.0 # 梯度剪裁\n", + " warmup_iters: int = 0 # 学习率热启动\n", + " log_interval: int = 1 # 每一步打印日志 仅用于观察\n", + " # save_interval: int = 100 # checkpoint 保存点 我们不使用\n", + " local_rank: int = 1 # device 设备号\n", + " dim: int = 512 # 词嵌入维度 模型超参数\n", + " n_layers: int = 2 # MiniMind Block 数量 模型超参数\n", + " max_seq_len: int = 512 # 序列长度阈值\n", + " use_moe: bool = False # 是否启用混合专家\n", + " data_path: str = './toydata/r1_data.jsonl' # 数据集路径" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d2dc3bf2-215f-4fc0-b7ce-6b4a09b0f2d9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "查看工作设备 cuda\n" + ] + } + ], + "source": [ + "print(f'查看工作设备 {args.device}')" + ] + }, + { + "cell_type": "markdown", + "id": "82a5bce8-a578-4e56-9dd5-9cb43321da0c", + "metadata": {}, + "source": [ + "接下来,我们对分词器、MiniMind 学生模型以及数据迭代器执行初始化." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "db202500-e265-434e-b0a4-c352854de648", + "metadata": {}, + "outputs": [], + "source": [ + "def init_model(lm_config):\n", + " tokenizer = AutoTokenizer.from_pretrained('../model/minimind_tokenizer')\n", + " model = MiniMindLM(lm_config)\n", + " moe_path = '_moe' if lm_config.use_moe else ''\n", + " # 热启动\n", + " # ckp = f'./out/rlhf_{lm_config.dim}{moe_path}.pth'\n", + " # state_dict = torch.load(ckp, map_location=args.device)\n", + " # model.load_state_dict(state_dict, strict=False)\n", + " print(f'LLM总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')\n", + " model = model.to(args.device)\n", + " return model, tokenizer" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e1948aa4-381d-485a-9b19-e0e566e0b5da", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM总参数量:8.915 百万\n", + "模型位于设备:cuda:0, 词表长度:6400, DataLoader:\n" + ] + } + ], + "source": [ + "lm_config = LMConfig(dim=args.dim, n_layers=args.n_layers, max_seq_len=args.max_seq_len, use_moe=args.use_moe)\n", + "model, tokenizer = init_model(lm_config)\n", + "\n", + "train_ds = SFTDataset(args.data_path, tokenizer, max_length=lm_config.max_seq_len)\n", + "\n", + "train_loader = DataLoader(\n", + " train_ds,\n", + " batch_size=args.batch_size,\n", + " pin_memory=True,\n", + " drop_last=False,\n", + " shuffle=False,\n", + ")\n", + "\n", + "print(f'模型位于设备:{model.device}, 词表长度:{tokenizer.vocab_size}, DataLoader:{train_loader}')" + ] + }, + { + "cell_type": "markdown", + "id": "9be67e55-0d1b-4c5d-9304-3b65d5b81c4d", + "metadata": {}, + "source": [ + "## 启动训练\n", + "\n", + "接下来,我们定义 MiniMind LoRA 微调所使用的优化器,损失函数和学习率调度,并进行一轮简单的训练." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "59084328-05e4-49d3-8459-54c549131511", + "metadata": {}, + "outputs": [], + "source": [ + "# 学习率调度方面 采用余弦退火学习率\n", + "def get_lr(current_step, total_steps, lr):\n", + " return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))\n", + "\n", + "# 优化器方面 选择 AdamW 优化器 并在混精度场景下创建 scaler 进行梯度缩放避免数值下溢\n", + "scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))\n", + "# 优化学生模型参数\n", + "optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)\n", + "\n", + "device_type = \"cuda\" if \"cuda\" in args.device else \"cpu\"\n", + "ctx = nullcontext() if device_type == \"cpu\" else torch.cuda.amp.autocast() # 在 cuda 上启动混精度训练,否则空白上下文" + ] + }, + { + "cell_type": "markdown", + "id": "7a241f42-32ec-4070-9fda-1cb39860089f", + "metadata": {}, + "source": [ + "接下来,我们来看看训练函数.\n", + "\n", + "蒸馏思考数据集的训练过程与 SFT 类似,区别在于模型生成序列中,思考标签位置的预测错误惩罚被放大." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "e705c796-938e-4e0e-b813-3d6ffc68635e", + "metadata": {}, + "outputs": [], + "source": [ + "def train_epoch(epoch):\n", + " # 思考标签占位符\n", + " start_of_think_ids = tokenizer('').input_ids\n", + " end_of_think_ids = tokenizer('').input_ids\n", + " start_of_answer_ids = tokenizer('').input_ids\n", + " end_of_answer_ids = tokenizer('').input_ids\n", + " loss_fct = nn.CrossEntropyLoss(reduction='none') # ce 损失\n", + " start_time = time.time()\n", + " for step, (X, Y, loss_mask) in enumerate(train_loader):\n", + " X = X.to(args.device)\n", + " Y = Y.to(args.device)\n", + " loss_mask = loss_mask.to(args.device)\n", + " lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args.learning_rate)\n", + " for param_group in optimizer.param_groups:\n", + " param_group['lr'] = lr\n", + "\n", + " with ctx:\n", + " res = model(X)\n", + " loss = loss_fct(\n", + " res.logits.view(-1, res.logits.size(-1)),\n", + " Y.view(-1)\n", + " ).view(Y.size())\n", + " # 判断思考标签是否存在于 Y 中,并返回一个 bool 类型张量,指示思考标签在 Y 中的位置\n", + " sp_ids = torch.isin(Y.view(-1),\n", + " torch.tensor(start_of_think_ids + end_of_think_ids\n", + " + start_of_answer_ids + end_of_answer_ids\n", + " ).to(args.device))\n", + " # 在 sp_ids 对应的位置增加额外的惩罚\n", + " loss_mask = loss_mask.view(-1)\n", + " loss_mask_sum = loss_mask.sum()\n", + " loss_mask[sp_ids] = 10\n", + " loss_mask = loss_mask.view(Y.size())\n", + " loss = (loss * loss_mask).sum() / loss_mask_sum # 思考标签相应位置的惩罚被放大十倍\n", + " loss += res.aux_loss # 负载均衡损失\n", + " loss = loss / args.accumulation_steps\n", + "\n", + " scaler.scale(loss).backward()\n", + "\n", + " if (step + 1) % args.accumulation_steps == 0:\n", + " scaler.unscale_(optimizer)\n", + " torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)\n", + "\n", + " scaler.step(optimizer)\n", + " scaler.update()\n", + "\n", + " optimizer.zero_grad(set_to_none=True)\n", + "\n", + " if step % args.log_interval == 0:\n", + " spend_time = time.time() - start_time\n", + " print(\n", + " 'Epoch:[{}/{}]({}/{}) loss:{:.3f} lr:{:.12f} epoch_Time:{}min:'.format(\n", + " epoch + 1,\n", + " args.epochs,\n", + " step,\n", + " iter_per_epoch,\n", + " loss.item(),\n", + " optimizer.param_groups[-1]['lr'],\n", + " spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60))\n", + "\n", + " # 到达指定步数后 save as torch\n", + " # if (step + 1) % args.save_interval == 0 and (not ddp or dist.get_rank() == 0):\n", + " # model.eval()\n", + " # moe_path = '_moe' if lm_config.use_moe else ''\n", + " # ckp = f'{args.save_dir}/reason_{lm_config.dim}{moe_path}.pth'\n", + "\n", + " # if isinstance(model, torch.nn.parallel.DistributedDataParallel):\n", + " # state_dict = model.module.state_dict()\n", + " # else:\n", + " # state_dict = model.state_dict()\n", + "\n", + " # torch.save(state_dict, ckp)\n", + " # model.train()" + ] + }, + { + "cell_type": "markdown", + "id": "a3b5aaf7-feac-42d9-adfe-9a583f26879d", + "metadata": {}, + "source": [ + "接下来,我们启动一个 Epoch 的训练进行观察." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "f021611c-6291-4c50-acf0-9a24dde87e77", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch:[1/1](0/1) loss:11.656 lr:0.000550000000 epoch_Time:0.0min:\n" + ] + } + ], + "source": [ + "iter_per_epoch = len(train_loader)\n", + "for epoch in range(args.epochs):\n", + " train_epoch(epoch)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "fba06e90-680e-47ff-99e3-49fd91d74dc9", + "metadata": {}, + "outputs": [], + "source": [ + "del model" + ] + } + ], + "metadata": { + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebook/images/gqa.png b/notebook/images/gqa.png new file mode 100644 index 0000000..e6916e0 Binary files /dev/null and b/notebook/images/gqa.png differ diff --git a/notebook/images/lora.png b/notebook/images/lora.png new file mode 100644 index 0000000..4a22fc0 Binary files /dev/null and b/notebook/images/lora.png differ diff --git a/notebook/model/LMConfig.py b/notebook/model/LMConfig.py new file mode 100644 index 0000000..323c110 --- /dev/null +++ b/notebook/model/LMConfig.py @@ -0,0 +1,61 @@ +from transformers import PretrainedConfig +from typing import List + + +class LMConfig(PretrainedConfig): + model_type = "minimind" + + def __init__( + self, + dim: int = 512, + n_layers: int = 1, # n_layers set to 1 to enable local practice + n_heads: int = 8, + n_kv_heads: int = 2, + vocab_size: int = 6400, + hidden_dim: int = None, + multiple_of: int = 64, + norm_eps: float = 1e-5, + max_seq_len: int = 8192, + rope_theta: int = 1e6, + dropout: float = 0.0, + flash_attn: bool = True, + #################################################### + # Here are the specific configurations of MOE + # When use_moe is false, the following is invalid + #################################################### + use_moe: bool = False, + #################################################### + num_experts_per_tok: int = 2, + n_routed_experts: int = 4, + n_shared_experts: bool = True, + scoring_func: str = 'softmax', + aux_loss_alpha: float = 0.1, + seq_aux: bool = True, + norm_topk_prob: bool = True, + **kwargs, + ): + self.dim = dim + self.n_layers = n_layers + self.n_heads = n_heads + self.n_kv_heads = n_kv_heads + self.vocab_size = vocab_size + self.hidden_dim = hidden_dim + self.multiple_of = multiple_of + self.norm_eps = norm_eps + self.max_seq_len = max_seq_len + self.rope_theta = rope_theta + self.dropout = dropout + self.flash_attn = flash_attn + #################################################### + # Here are the specific configurations of MOE + # When use_moe is false, the following is invalid + #################################################### + self.use_moe = use_moe + self.num_experts_per_tok = num_experts_per_tok # 每个token选择的专家数量 + self.n_routed_experts = n_routed_experts # 总的专家数量 + self.n_shared_experts = n_shared_experts # 共享专家 + self.scoring_func = scoring_func # 评分函数,默认为'softmax' + self.aux_loss_alpha = aux_loss_alpha # 辅助损失的alpha参数 + self.seq_aux = seq_aux # 是否在序列级别上计算辅助损失 + self.norm_topk_prob = norm_topk_prob # 是否标准化top-k概率 + super().__init__(**kwargs) diff --git a/notebook/model/dataset.py b/notebook/model/dataset.py new file mode 100644 index 0000000..7750789 --- /dev/null +++ b/notebook/model/dataset.py @@ -0,0 +1,200 @@ +import json +import random +import re + +import pandas as pd +import numpy as np +from torch.utils.data import Dataset, DataLoader +import torch +from sklearn.model_selection import train_test_split +import os +import ast + +os.environ["TOKENIZERS_PARALLELISM"] = "false" + + +class PretrainDataset(Dataset): + def __init__(self, data_path, tokenizer, max_length=512): + super().__init__() + self.tokenizer = tokenizer + self.max_length = max_length + self.samples = self.load_data(data_path) + + def load_data(self, path): + samples = [] + with open(path, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + data = json.loads(line.strip()) + samples.append(data) + return samples + + def __len__(self): + return len(self.samples) + + def __getitem__(self, index): + sample = self.samples[index] + + # 构建输入文本 + text = f"{self.tokenizer.bos_token}{str(sample['text'])}{self.tokenizer.eos_token}" + encoding = self.tokenizer( + text, + max_length=self.max_length, + padding='max_length', + truncation=True, + return_tensors='pt' + ) + input_ids = encoding.input_ids.squeeze() + loss_mask = (input_ids != self.tokenizer.pad_token_id) + + X = torch.tensor(input_ids[:-1], dtype=torch.long) + Y = torch.tensor(input_ids[1:], dtype=torch.long) + loss_mask = torch.tensor(loss_mask[1:], dtype=torch.long) + return X, Y, loss_mask + + +class SFTDataset(Dataset): + def __init__(self, jsonl_path, tokenizer, max_length=1024): + super().__init__() + self.tokenizer = tokenizer + self.max_length = max_length + self.samples = self.load_data(jsonl_path) + self.bos_id = tokenizer('assistant\n', add_special_tokens=False).input_ids + self.eos_id = tokenizer('\n', add_special_tokens=False).input_ids + + def __len__(self): + return len(self.samples) + + def load_data(self, path): + samples = [] + with open(path, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + data = json.loads(line.strip()) + samples.append(data) + return samples + + def _create_chat_prompt(self, conversations): + """构建符合ChatML格式的对话""" + messages = [] + for i, turn in enumerate(conversations): + role = 'user' if i % 2 == 0 else 'assistant' + messages.append({"role": role, "content": turn['content']}) + return self.tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=False + ) + + def _generate_loss_mask(self, input_ids): + loss_mask = [0] * len(input_ids) + i = 0 + while i < len(input_ids): + if input_ids[i:i + len(self.bos_id)] == self.bos_id: + start = i + len(self.bos_id) + end = start + while end < len(input_ids): + if input_ids[end:end + len(self.eos_id)] == self.eos_id: + break + end += 1 + for j in range(start + 1, min(end + len(self.eos_id) + 1, self.max_length)): + loss_mask[j] = 1 + i = end + len(self.eos_id) if end < len(input_ids) else len(input_ids) + else: + i += 1 + return loss_mask + + def __getitem__(self, index): + sample = self.samples[index] + # 构建对话提示 + prompt = self._create_chat_prompt(sample['conversations']) + input_ids = self.tokenizer(prompt).input_ids[:self.max_length] + input_ids += [self.tokenizer.pad_token_id] * (self.max_length - len(input_ids)) + + # 生成动态损失掩码 + loss_mask = self._generate_loss_mask(input_ids) + + # 构建训练数据 + X = torch.tensor(input_ids[:-1], dtype=torch.long) + Y = torch.tensor(input_ids[1:], dtype=torch.long) + loss_mask = torch.tensor(loss_mask[1:], dtype=torch.long) # 对齐预测位置 + + return X, Y, loss_mask + + +class DPODataset(Dataset): + def __init__(self, file_path, tokenizer, max_length=4096): + super().__init__() + self.tokenizer = tokenizer + self.max_length = max_length + self.padding = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else 0 + self.bos_id = tokenizer('assistant\n', add_special_tokens=False).input_ids + self.eos_id = tokenizer('\n', add_special_tokens=False).input_ids + with open(file_path, 'r', encoding='utf-8') as f: + self.data = [] + for line in f: + line = line.strip() + obj = json.loads(line) + self.data.append(obj) + + def __len__(self): + return len(self.data) + + def __getitem__(self, index): + item = self.data[index] + chosen = item['chosen'] # 是一个 list,里面包含若干 {role, content} + rejected = item['rejected'] # 同上 + chosen_prompt = self.tokenizer.apply_chat_template( + chosen, tokenize=False, add_generation_prompt=False + ) + + rejected_prompt = self.tokenizer.apply_chat_template( + rejected, tokenize=False, add_generation_prompt=False + ) + chosen_encoding = self.tokenizer( + chosen_prompt, truncation=True, max_length=self.max_length, padding='max_length' + ) + rejected_encoding = self.tokenizer( + rejected_prompt, truncation=True, max_length=self.max_length, padding='max_length' + ) + + chosen_input_ids = chosen_encoding['input_ids'] + chosen_loss_mask = self._generate_loss_mask(chosen_input_ids) + + rejected_input_ids = rejected_encoding['input_ids'] + rejected_loss_mask = self._generate_loss_mask(rejected_input_ids) + x_chosen = torch.tensor(chosen_input_ids[:-1], dtype=torch.long) + y_chosen = torch.tensor(chosen_input_ids[1:], dtype=torch.long) + mask_chosen = torch.tensor(chosen_loss_mask[1:], dtype=torch.long) + x_rejected = torch.tensor(rejected_input_ids[:-1], dtype=torch.long) + y_rejected = torch.tensor(rejected_input_ids[1:], dtype=torch.long) + mask_rejected = torch.tensor(rejected_loss_mask[1:], dtype=torch.long) + + return { + 'x_chosen': x_chosen, + 'y_chosen': y_chosen, + 'mask_chosen': mask_chosen, + 'x_rejected': x_rejected, + 'y_rejected': y_rejected, + 'mask_rejected': mask_rejected + } + + def _generate_loss_mask(self, input_ids): + loss_mask = [0] * len(input_ids) + i = 0 + while i < len(input_ids): + if input_ids[i:i + len(self.bos_id)] == self.bos_id: + start = i + len(self.bos_id) + end = start + while end < len(input_ids): + if input_ids[end:end + len(self.eos_id)] == self.eos_id: + break + end += 1 + for j in range(start + 1, min(end + len(self.eos_id) + 1, self.max_length)): + loss_mask[j] = 1 + i = end + len(self.eos_id) if end < len(input_ids) else len(input_ids) + else: + i += 1 + return loss_mask + + +if __name__ == "__main__": + pass diff --git a/notebook/model/model.py b/notebook/model/model.py new file mode 100644 index 0000000..1dc6478 --- /dev/null +++ b/notebook/model/model.py @@ -0,0 +1,376 @@ +import math +import struct +import inspect +import time + +from .LMConfig import LMConfig +from typing import Any, Optional, Tuple, List +import numpy as np +import torch +import torch.nn.functional as F +from torch import nn +from transformers import PreTrainedModel +from transformers.modeling_outputs import CausalLMOutputWithPast + + +class RMSNorm(torch.nn.Module): + def __init__(self, dim: int, eps: float): + super().__init__() + self.eps = eps + self.weight = nn.Parameter(torch.ones(dim)) + + def forward(self, x): + return self.weight * (x.float() * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)).type_as(x) + + +def precompute_pos_cis(dim: int, end: int = int(32 * 1024), theta: float = 1e6): + freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)) + t = torch.arange(end, device=freqs.device) # type: ignore + freqs = torch.outer(t, freqs).float() # type: ignore + pos_cis = torch.polar(torch.ones_like(freqs), freqs) # complex64 + return pos_cis + + +def apply_rotary_emb(xq, xk, pos_cis): + def unite_shape(pos_cis, x): + ndim = x.ndim + assert 0 <= 1 < ndim + assert pos_cis.shape == (x.shape[1], x.shape[-1]) + shape = [d if i == 1 or i == ndim - 1 else 1 for i, d in enumerate(x.shape)] + return pos_cis.view(*shape) + + xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2)) + xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2)) + pos_cis = unite_shape(pos_cis, xq_) + xq_out = torch.view_as_real(xq_ * pos_cis).flatten(3) + xk_out = torch.view_as_real(xk_ * pos_cis).flatten(3) + return xq_out.type_as(xq), xk_out.type_as(xk) + + +def repeat_kv(x: torch.Tensor, n_rep: int) -> torch.Tensor: + """torch.repeat_interleave(x, dim=2, repeats=n_rep)""" + bs, slen, n_kv_heads, head_dim = x.shape + if n_rep == 1: + return x + return ( + x[:, :, :, None, :] + .expand(bs, slen, n_kv_heads, n_rep, head_dim) + .reshape(bs, slen, n_kv_heads * n_rep, head_dim) + ) + + +class Attention(nn.Module): + def __init__(self, args: LMConfig): + super().__init__() + self.n_kv_heads = args.n_heads if args.n_kv_heads is None else args.n_kv_heads + assert args.n_heads % self.n_kv_heads == 0 + self.n_local_heads = args.n_heads + self.n_local_kv_heads = self.n_kv_heads + self.n_rep = self.n_local_heads // self.n_local_kv_heads + self.head_dim = args.dim // args.n_heads + self.wq = nn.Linear(args.dim, args.n_heads * self.head_dim, bias=False) + self.wk = nn.Linear(args.dim, self.n_kv_heads * self.head_dim, bias=False) + self.wv = nn.Linear(args.dim, self.n_kv_heads * self.head_dim, bias=False) + self.wo = nn.Linear(args.n_heads * self.head_dim, args.dim, bias=False) + self.attn_dropout = nn.Dropout(args.dropout) + self.resid_dropout = nn.Dropout(args.dropout) + self.dropout = args.dropout + self.flash = hasattr(torch.nn.functional, 'scaled_dot_product_attention') and args.flash_attn + # print("WARNING: using slow attention. Flash Attention requires PyTorch >= 2.0") + mask = torch.full((1, 1, args.max_seq_len, args.max_seq_len), float("-inf")) + mask = torch.triu(mask, diagonal=1) + self.register_buffer("mask", mask, persistent=False) + + def forward(self, + x: torch.Tensor, + pos_cis: torch.Tensor, + past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, + use_cache=False): + bsz, seq_len, _ = x.shape + xq, xk, xv = self.wq(x), self.wk(x), self.wv(x) + xq = xq.view(bsz, seq_len, self.n_local_heads, self.head_dim) + xk = xk.view(bsz, seq_len, self.n_local_kv_heads, self.head_dim) + xv = xv.view(bsz, seq_len, self.n_local_kv_heads, self.head_dim) + + xq, xk = apply_rotary_emb(xq, xk, pos_cis) + # kv_cache实现 + if past_key_value is not None: + xk = torch.cat([past_key_value[0], xk], dim=1) + xv = torch.cat([past_key_value[1], xv], dim=1) + past_kv = (xk, xv) if use_cache else None + + xq, xk, xv = ( + xq.transpose(1, 2), + repeat_kv(xk, self.n_rep).transpose(1, 2), + repeat_kv(xv, self.n_rep).transpose(1, 2) + ) + if self.flash and seq_len != 1: + dropout_p = self.dropout if self.training else 0.0 + output = F.scaled_dot_product_attention( + xq, xk, xv, + attn_mask=None, + dropout_p=dropout_p, + is_causal=True + ) + else: + scores = (xq @ xk.transpose(-2, -1)) / math.sqrt(self.head_dim) + scores += self.mask[:, :, :seq_len, :seq_len] + scores = F.softmax(scores.float(), dim=-1).type_as(xq) + scores = self.attn_dropout(scores) + output = scores @ xv + + output = output.transpose(1, 2).reshape(bsz, seq_len, -1) + output = self.resid_dropout(self.wo(output)) + return output, past_kv + + +class FeedForward(nn.Module): + def __init__(self, config: LMConfig): + super().__init__() + if config.hidden_dim is None: + hidden_dim = 4 * config.dim + hidden_dim = int(2 * hidden_dim / 3) + config.hidden_dim = config.multiple_of * ((hidden_dim + config.multiple_of - 1) // config.multiple_of) + self.w1 = nn.Linear(config.dim, config.hidden_dim, bias=False) + self.w2 = nn.Linear(config.hidden_dim, config.dim, bias=False) + self.w3 = nn.Linear(config.dim, config.hidden_dim, bias=False) + self.dropout = nn.Dropout(config.dropout) + + def forward(self, x): + return self.dropout(self.w2(F.silu(self.w1(x)) * self.w3(x))) + + +class MoEGate(nn.Module): + def __init__(self, config: LMConfig): + super().__init__() + self.config = config + self.top_k = config.num_experts_per_tok + self.n_routed_experts = config.n_routed_experts + + self.scoring_func = config.scoring_func + self.alpha = config.aux_loss_alpha + self.seq_aux = config.seq_aux + + self.norm_topk_prob = config.norm_topk_prob + self.gating_dim = config.dim + self.weight = nn.Parameter(torch.empty((self.n_routed_experts, self.gating_dim))) + self.reset_parameters() + + def reset_parameters(self) -> None: + import torch.nn.init as init + init.kaiming_uniform_(self.weight, a=math.sqrt(5)) + + def forward(self, hidden_states): + bsz, seq_len, h = hidden_states.shape + hidden_states = hidden_states.view(-1, h) + logits = F.linear(hidden_states, self.weight, None) + if self.scoring_func == 'softmax': + scores = logits.softmax(dim=-1) + else: + raise NotImplementedError(f'insupportable scoring function for MoE gating: {self.scoring_func}') + + topk_weight, topk_idx = torch.topk(scores, k=self.top_k, dim=-1, sorted=False) + + if self.top_k > 1 and self.norm_topk_prob: + denominator = topk_weight.sum(dim=-1, keepdim=True) + 1e-20 + topk_weight = topk_weight / denominator + + if self.training and self.alpha > 0.0: + scores_for_aux = scores + aux_topk = self.top_k + topk_idx_for_aux_loss = topk_idx.view(bsz, -1) + if self.seq_aux: + scores_for_seq_aux = scores_for_aux.view(bsz, seq_len, -1) + ce = torch.zeros(bsz, self.n_routed_experts, device=hidden_states.device) + ce.scatter_add_(1, topk_idx_for_aux_loss, + torch.ones(bsz, seq_len * aux_topk, device=hidden_states.device)).div_( + seq_len * aux_topk / self.n_routed_experts) + aux_loss = (ce * scores_for_seq_aux.mean(dim=1)).sum(dim=1).mean() * self.alpha + else: + mask_ce = F.one_hot(topk_idx_for_aux_loss.view(-1), num_classes=self.n_routed_experts) + ce = mask_ce.float().mean(0) + Pi = scores_for_aux.mean(0) + fi = ce * self.n_routed_experts + aux_loss = (Pi * fi).sum() * self.alpha + else: + aux_loss = 0 + return topk_idx, topk_weight, aux_loss + + +class MOEFeedForward(nn.Module): + def __init__(self, config: LMConfig): + super().__init__() + self.config = config + self.experts = nn.ModuleList([ + FeedForward(config) + for _ in range(config.n_routed_experts) + ]) + self.gate = MoEGate(config) + if config.n_shared_experts is not None: + self.shared_experts = FeedForward(config) + + def forward(self, x): + identity = x + orig_shape = x.shape + bsz, seq_len, _ = x.shape + # 使用门控机制选择专家 + topk_idx, topk_weight, aux_loss = self.gate(x) + x = x.view(-1, x.shape[-1]) + flat_topk_idx = topk_idx.view(-1) + if self.training: + # 训练模式下,重复输入数据 + x = x.repeat_interleave(self.config.num_experts_per_tok, dim=0) + y = torch.empty_like(x, dtype=torch.float16) + for i, expert in enumerate(self.experts): + y[flat_topk_idx == i] = expert(x[flat_topk_idx == i]).to(y.dtype) # 确保类型一致 + y = (y.view(*topk_weight.shape, -1) * topk_weight.unsqueeze(-1)).sum(dim=1) + y = y.view(*orig_shape) + else: + # 推理模式下,只选择最优专家 + y = self.moe_infer(x, flat_topk_idx, topk_weight.view(-1, 1)).view(*orig_shape) + if self.config.n_shared_experts is not None: + y = y + self.shared_experts(identity) + self.aux_loss = aux_loss + return y + + @torch.no_grad() + def moe_infer(self, x, flat_expert_indices, flat_expert_weights): + expert_cache = torch.zeros_like(x) + idxs = flat_expert_indices.argsort() + tokens_per_expert = flat_expert_indices.bincount().cpu().numpy().cumsum(0) + token_idxs = idxs // self.config.num_experts_per_tok + # 例如当tokens_per_expert=[6, 15, 20, 26, 33, 38, 46, 52] + # 当token_idxs=[3, 7, 19, 21, 24, 25, 4, 5, 6, 10, 11, 12...] + # 意味着当token_idxs[:6] -> [3, 7, 19, 21, 24, 25, 4]位置的token都由专家0处理,token_idxs[6:15]位置的token都由专家1处理...... + for i, end_idx in enumerate(tokens_per_expert): + start_idx = 0 if i == 0 else tokens_per_expert[i - 1] + if start_idx == end_idx: + continue + expert = self.experts[i] + exp_token_idx = token_idxs[start_idx:end_idx] + expert_tokens = x[exp_token_idx] + expert_out = expert(expert_tokens).to(expert_cache.dtype) + expert_out.mul_(flat_expert_weights[idxs[start_idx:end_idx]]) + # 使用 scatter_add_ 进行 sum 操作 + expert_cache.scatter_add_(0, exp_token_idx.view(-1, 1).repeat(1, x.shape[-1]), expert_out) + + return expert_cache + + +class MiniMindBlock(nn.Module): + def __init__(self, layer_id: int, config: LMConfig): + super().__init__() + self.n_heads = config.n_heads + self.dim = config.dim + self.head_dim = config.dim // config.n_heads + self.attention = Attention(config) + + self.layer_id = layer_id + self.attention_norm = RMSNorm(config.dim, eps=config.norm_eps) + self.ffn_norm = RMSNorm(config.dim, eps=config.norm_eps) + self.feed_forward = FeedForward(config) if not config.use_moe else MOEFeedForward(config) + + def forward(self, x, pos_cis, past_key_value=None, use_cache=False): + h_attn, past_kv = self.attention( + self.attention_norm(x), + pos_cis, + past_key_value=past_key_value, + use_cache=use_cache + ) + h = x + h_attn + out = h + self.feed_forward(self.ffn_norm(h)) + return out, past_kv + + +class MiniMindLM(PreTrainedModel): + config_class = LMConfig + + def __init__(self, params: LMConfig = None): + self.params = params or LMConfig() + super().__init__(self.params) + self.vocab_size, self.n_layers = params.vocab_size, params.n_layers + self.tok_embeddings = nn.Embedding(params.vocab_size, params.dim) + self.dropout = nn.Dropout(params.dropout) + self.layers = nn.ModuleList([MiniMindBlock(l, params) for l in range(self.n_layers)]) + self.norm = RMSNorm(params.dim, eps=params.norm_eps) + self.output = nn.Linear(params.dim, params.vocab_size, bias=False) + self.tok_embeddings.weight = self.output.weight + self.register_buffer("pos_cis", + precompute_pos_cis(dim=params.dim // params.n_heads, theta=params.rope_theta), + persistent=False) + self.OUT = CausalLMOutputWithPast() + + def forward(self, + input_ids: Optional[torch.Tensor] = None, + past_key_values: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None, + use_cache: bool = False, + **args): + past_key_values = past_key_values or [None] * len(self.layers) + start_pos = args.get('start_pos', 0) + h = self.dropout(self.tok_embeddings(input_ids)) + pos_cis = self.pos_cis[start_pos:start_pos + input_ids.size(1)] + past_kvs = [] + for l, layer in enumerate(self.layers): + h, past_kv = layer( + h, pos_cis, + past_key_value=past_key_values[l], + use_cache=use_cache + ) + past_kvs.append(past_kv) + logits = self.output(self.norm(h)) + aux_loss = sum(l.feed_forward.aux_loss for l in self.layers if isinstance(l.feed_forward, MOEFeedForward)) + self.OUT.__setitem__('logits', logits) + self.OUT.__setitem__('aux_loss', aux_loss) + self.OUT.__setitem__('past_key_values', past_kvs) + return self.OUT + + @torch.inference_mode() + def generate(self, input_ids, eos_token_id=2, max_new_tokens=1024, temperature=0.75, top_p=0.90, + stream=False, rp=1., use_cache=True, pad_token_id=0, **args): + # 流式生成 + if stream: + return self._stream(input_ids, eos_token_id, max_new_tokens, temperature, top_p, rp, use_cache, **args) + + # 直接生成 + generated = [] + for i in range(input_ids.size(0)): + non_pad = input_ids[i][input_ids[i] != pad_token_id].unsqueeze(0) + out = self._stream(non_pad, eos_token_id, max_new_tokens, temperature, top_p, rp, use_cache, **args) + tokens_list = [tokens[:, -1:] for tokens in out] + gen = torch.cat(tokens_list, dim=-1) if tokens_list else non_pad + full_sequence = torch.cat([non_pad, gen], dim=-1) + generated.append(full_sequence) + max_length = max(seq.size(1) for seq in generated) + generated = [ + torch.cat( + [seq, torch.full((1, max_length - seq.size(1)), pad_token_id, dtype=seq.dtype, device=seq.device)], + dim=-1) + for seq in generated + ] + return torch.cat(generated, dim=0) + + def _stream(self, input_ids, eos_token_id, max_new_tokens, temperature, top_p, rp, use_cache, **args): + start, first_seq, past_kvs = input_ids.shape[1], True, None + while input_ids.shape[1] < max_new_tokens - 1: + if first_seq or not use_cache: + out, first_seq = self(input_ids, past_key_values=past_kvs, use_cache=use_cache, **args), False + else: + out = self(input_ids[:, -1:], past_key_values=past_kvs, use_cache=use_cache, + start_pos=input_ids.shape[1] - 1, **args) + logits, past_kvs = out.logits[:, -1, :], out.past_key_values + logits[:, list(set(input_ids.tolist()[0]))] /= rp + logits /= (temperature + 1e-9) + if top_p is not None and top_p < 1.0: + sorted_logits, sorted_indices = torch.sort(logits, descending=True, dim=-1) + sorted_probs = F.softmax(sorted_logits, dim=-1) + cumulative_probs = torch.cumsum(sorted_probs, dim=-1) + sorted_indices_to_remove = cumulative_probs > top_p + sorted_indices_to_remove[:, 1:] = sorted_indices_to_remove[:, :-1].clone() + sorted_indices_to_remove[:, 0] = False + indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove) + logits[indices_to_remove] = -float('Inf') + input_ids_next = torch.multinomial(F.softmax(logits, dim=-1), num_samples=1) + input_ids = torch.cat((input_ids, input_ids_next), dim=1) + yield input_ids[:, start:] + if input_ids_next.item() == eos_token_id: + break diff --git a/notebook/toydata/dpo_data.jsonl b/notebook/toydata/dpo_data.jsonl new file mode 100644 index 0000000..c465539 --- /dev/null +++ b/notebook/toydata/dpo_data.jsonl @@ -0,0 +1,2 @@ +{"chosen":[{"content":"你好吗?","role":"user"},{"content":"我很好,谢谢!你呢?","role":"assistant"},{"content":"今天过得怎么样?","role":"user"},{"content":"挺好的,去跑步了,心情不错。","role":"assistant"}],"rejected":[{"content":"你好吗?","role":"user"},{"content":"不好,我很累。","role":"assistant"},{"content":"你喜欢什么运动?","role":"user"},{"content":"我不喜欢运动,没兴趣。","role":"assistant"}]} +{"chosen":[{"content":"你喜欢旅行吗?","role":"user"},{"content":"我喜欢,尤其是去海边。","role":"assistant"},{"content":"你去过哪些地方?","role":"user"},{"content":"我去过欧洲、美国和日本,都是很棒的经历。","role":"assistant"}],"rejected":[{"content":"你喜欢旅行吗?","role":"user"},{"content":"不太喜欢,觉得太麻烦。","role":"assistant"},{"content":"你去过哪些地方?","role":"user"},{"content":"没去过太多地方,主要是待在家里。","role":"assistant"}]} \ No newline at end of file diff --git a/notebook/toydata/lora_data.jsonl b/notebook/toydata/lora_data.jsonl new file mode 100644 index 0000000..cb8e2bf --- /dev/null +++ b/notebook/toydata/lora_data.jsonl @@ -0,0 +1,2 @@ +{"conversations": [{"role": "user", "content": "hi"}, {"role": "assistant", "content": "Hello! I am MiniMind, an AI assistant developed by Jingyao Gong. How can I assist you today?"}]} +{"conversations": [{"role": "user", "content": "hello"}, {"role": "assistant", "content": "Hello! I am MiniMind, an AI assistant developed by Jingyao Gong. How can I assist you today?"}]} \ No newline at end of file diff --git a/notebook/toydata/pretrain_data.jsonl b/notebook/toydata/pretrain_data.jsonl new file mode 100644 index 0000000..c2c691c --- /dev/null +++ b/notebook/toydata/pretrain_data.jsonl @@ -0,0 +1,2 @@ +{"text": "LLM首先要学习的并非直接与人交流,而是让网络参数中充满知识的墨水,“墨水” 理论上喝的越饱越好,产生大量的对世界的知识积累。 预训练就是让Model先埋头苦学大量基本的知识,例如从Wiki百科、新闻、书籍整理大规模的高质量训练数据。 这个过程是“无监督”的,即人类不需要在过程中做任何“有监督”的校正,而是由模型自己从大量文本中总结规律学习知识点。 模型此阶段目的只有一个:学会词语接龙。例如我们输入“秦始皇”四个字,它可以接龙“是中国的第一位皇帝”。"} +{"text": "经过预训练,LLM此时已经掌握了大量知识,然而此时它只会无脑地词语接龙,还不会与人聊天。 SFT阶段就需要把半成品LLM施加一个自定义的聊天模板进行微调。 例如模型遇到这样的模板【问题->回答,问题->回答】后不再无脑接龙,而是意识到这是一段完整的对话结束。 称这个过程为指令微调,就如同让已经学富五车的「牛顿」先生适应21世纪智能手机的聊天习惯,学习屏幕左侧是对方消息,右侧是本人消息这个规律。 在训练时,MiniMind的指令和回答长度被截断在512,是为了节省显存空间。就像我们学习时,会先从短的文章开始,当学会写作200字作文后,800字文章也可以手到擒来。 在需要长度拓展时,只需要准备少量的2k/4k/8k长度对话数据进行进一步微调即可(此时最好配合RoPE-NTK的基准差值)。"} \ No newline at end of file diff --git a/notebook/toydata/r1_data.jsonl b/notebook/toydata/r1_data.jsonl new file mode 100644 index 0000000..a1d222a --- /dev/null +++ b/notebook/toydata/r1_data.jsonl @@ -0,0 +1,2 @@ +{"conversations": [{"role": "user", "content": "请用一段话描述阿里巴巴集团的企业文化。"}, {"role": "assistant", "content": "\n嗯,用户让我用一段话描述阿里巴巴集团的企业文化。首先,我需要明确阿里巴巴集团的平均文化是什么。企业文化的定义是企业内部发生、指导管理方向的准则和规范性的精神或规范。\n\n阿里巴巴集团在2000年代中期开始发展,之前也有类似的概念,比如微软的人工智能理念。所以,我可以从这个发展中来寻找关键词和结构。\n\n接下来,我要考虑用户可能的意图。用户可能想了解阿里巴巴的企业文化,或者需要用于某个报告、宣传材料或者内部交流中。因此,我需要确保描述既全面又有深度,涵盖战略、创新、效率等各个方面。\n\n我还需要用简洁的语言来表达,避免过于复杂的词汇,让段落易于理解。同时,要注意逻辑连贯,段落之间要有过渡,让读者能够顺畅地跟随观点。\n\n可能还需要引用一些关键点,比如战略协作、用户体验、创新、效率、数字化转型、客户支持、全球化等,这些都是阿里巴巴成功的关键点,能够帮助用户全面了解企业文化。\n\n最后,检查语法和结构是否正确,确保专业性和准确性,这样用户可以直接使用这段文字,或者稍作修改,就能得到满意的效果。\n\n\n阿里巴巴集团的企业文化以战略协作为核心,倡导员工之间的资源整合与创新思维的碰撞,旨在实现企业价值的提升和持续发展。阿里巴巴将其视为一个协同共生的整体,致力于通过技术创新、用户体验优化和数字化转型,致力于为客户创造最大价值。企业内外部力量协同作战,推动企业从单纯的业务拓展延伸至价值创造和社会服务,并在全球范围内进行全方位的数字化转型,以满足多样化、个性化、高端化的客户需求。阿里巴巴集团ix platform的建立旨在帮助员工实现高效协作,激发创新精神,吸引更多优秀人才加入,共同推动企业不断向前发展。\n"}]} +{"conversations": [{"role": "user", "content": "你好,我是张华,今年15岁,是北京四中的学生。我喜欢编程和篮球,我梦想成为一名软件工程师。很高兴认识你!"}, {"role": "assistant", "content": "\n好,用户是张华,15岁,来自北京四中,喜欢编程和篮球,梦想成为软件工程师。我需要回应他的兴趣,强调他的优势,比如编程基础和对篮球的热爱,有针对性地引导他规划未来。可以建议他关注 Font Awesome,展示他的技术背景,同时鼓励他扩展兴趣,并辅以鼓励自己。\n\n\n你好!很高兴认识你。既然你对编程和篮球都很感兴趣,结合你的梦想成为一名软件工程师,这可能是一个很好的切入点。你编程的能力很强,可能在某个领域表现出色,这也是你成为 software engineer的有利条件。\n\n其实,除了你的编程技能,你对篮球的热爱也是挺有趣的,如果你能将这些技能结合使用,可能是一个很好的方向。例如,你可以利用你的篮球知识来设计游戏或解决某种技术问题,或者利用你的编程能力来优化自己的篮球战术。\n\n如果你对 Font Awesome 这样的开源 hesitate,这将是你的一个很好的起点。它可以让你快速开发一个简单的网站,展示你的编程能力和对足球的热爱。这样的话,你不仅能展示你的技术能力,还能展示你的 Otherwise Half。\n\n说到 else half,你应该更 Later来扩展你的兴趣,比如学习 Game Dev, Game Engine, 项目管理等,这样可以让你的探索更加多元化。或许你对 AI 或数据科学也有一定的兴趣,如果有机会,可以加入一些相关的话题和团队,体验运动与技术结合的有趣场景。\n\n总之,你很有潜力成为 software工程师!比方说,你可以尝试以下步骤:\n\n1. 关注 Font Awesome,展示你的编程能力。\n2. 尝试开发某个功能,结合你的篮球知识。\n3. 现在是好好学习 else half 的机会。\n\n希望这是一个鼓励的方向。祝你未来一切顺利!\n"}]} \ No newline at end of file diff --git a/notebook/toydata/sft_data.jsonl b/notebook/toydata/sft_data.jsonl new file mode 100644 index 0000000..e037570 --- /dev/null +++ b/notebook/toydata/sft_data.jsonl @@ -0,0 +1,2 @@ +{"conversations":[{"role":"user","content":"你好吗?"},{"role":"assistant","content":"我很好,谢谢!你呢?"},{"role":"user","content":"我也很好,谢谢!"},{"role":"assistant","content":"太好了!祝你今天愉快!"}]} +{"conversations":[{"role":"user","content":"你喜欢什么运动?"},{"role":"assistant","content":"我喜欢跑步和游泳。你呢?"},{"role":"user","content":"我喜欢打篮球!"},{"role":"assistant","content":"篮球很棒!是一个很好的团队运动。"}]} diff --git a/notebook/toydata/tokenizer_data.jsonl b/notebook/toydata/tokenizer_data.jsonl new file mode 100644 index 0000000..26e183d --- /dev/null +++ b/notebook/toydata/tokenizer_data.jsonl @@ -0,0 +1,3 @@ +{"text": "近年来,人工智能技术迅速发展,深刻改变了各行各业的面貌。机器学习、自然语言处理、计算机视觉等领域的突破性进展,使得智能产品和服务越来越普及。从智能家居到自动驾驶,再到智能医疗,AI的应用场景正在快速拓展。随着技术的不断进步,未来的人工智能将更加智能、更加贴近人类生活。"} +{"text": "在数据时代,信息的流通和处理速度决定了一个国家的竞争力。随着大数据技术和云计算的普及,企业和政府能够更高效地利用信息资源,实现精细化管理和决策,1234567890qwertyuiopasdfhgjklzxbcdljfasjdfaflsdjffa深度发掘的链接发的饭卡京东方卡拉快递费发动快速开发设计大大大大大大顶顶顶。"} +{"text": "一个卖瓷碗的老人挑着扁担在路上走着,突然一个瓷碗掉到地上摔碎了,但是老人头也不回继续向前走。路人看到很奇怪,便问:“为什么你的碗摔碎了你却不看一下呢?”老人答到:“我再怎么回头看,碗是碎的。”失去的东西就要学着去接受,学着放下。很多事并不会因为你的悲伤就会回来,结果就会被改变。"} \ No newline at end of file