介绍一个在github上很火的项目,其主要的想法是从头构建一个大模型,作者在这个项目中详细说明了每个步骤以及代码的处理逻辑,大家可以参考,https://github.com/FareedKhan-dev/train-llm-from-scratch

代码结构如下:

train-llm-from-scratch/
├── src/
│ ├── models/
│ │ ├── mlp.py # 多层感知机(MLP)模块的定义
│ │ ├── attention.py # 注意力机制的定义(单头、多头)
│ │ ├── transformer_block.py # 单个 Transformer 块的定义
│ │ ├── transformer.py # 主 Transformer 模型的定义
├── config/
│ └── config.py # 包含默认配置(模型参数、文件路径等)
├── data_loader/
│ └── data_loader.py # 包含创建数据加载器/迭代器的函数
├── scripts/
│ ├── train_transformer.py # 训练 Transformer 模型的脚本
│ ├── data_download.py # 下载数据集的脚本
│ ├── data_preprocess.py # 预处理下载数据的脚本
│ ├── generate_text.py # 使用训练好的模型生成文本的脚本
├── data/ # 存储数据集的目录
│ ├── train/ # 包含训练数据
│ └── val/ # 包含验证数据
├── models/ # 存储训练好的模型的目录

scripts/ 目录包含用于数据集下载、数据预处理、模型训练以及使用训练好的模型生成文本的任务脚本。
src/models/ 目录包含 Transformer 模型、多层感知机(MLP)、注意力机制和 Transformer 块的关键组件实现。
config/ 目录包含指定项目默认参数的配置文件。
data_loader/ 目录提供创建数据加载器和迭代器的函数。

下文的目录结构

  • 前提条件和训练时间

  • 安装模块

  • 导入库

  • 准备训练数据

  • Transformer 概述

  • 多层感知机(MLP)

  • 单头注意力

  • 多头注意力

  • Transformer 块

  • 最终模型

  • 批处理

  • 训练参数

  • 训练模型

  • 保存训练好的模型

  • 训练损失

  • 生成文本

  • 下一步计划

前提条件和训练时间 确保你对面向对象编程(OOP)和神经网络(NN)有基本的了解。熟悉 PyTorch 对于编码也会很有帮助。

你需要一个 GPU 来训练你的模型。Colab 或 Kaggle T4 可以用于训练一个 1300 万 + 参数的模型,但对于 10 亿参数的训练则会失败。可以参考以下对比:

安装模块 确保你的环境中安装了 Git。首先需要克隆仓库:

  
`git clone https://github.com/FareedKhan-dev/train-llm-from-scratch.git   cd train-llm-from-scratch   `

然后你可以安装所需的依赖:

  
`pip install -r requirements.txt   `

导入库 让作者们导入将在本博客中使用的所需库:

  
`# PyTorch 用于深度学习函数和张量   import torch   import torch.nn as nn   import torch.nn.functional as F      # 数值运算和数组处理   import numpy as np      # 处理 HDF5 文件   import h5py      # 操作系统和文件管理   import os      # 命令行参数解析   import argparse      # HTTP 请求和交互   import requests      # 循环的进度条   from tqdm import tqdm      # JSON 处理   import json      # Zstandard 压缩库   import zstandard as zstd      # 大语言模型的分词库   import tiktoken      # 数学运算(用于高级数学函数)   import math   `

准备训练数据 作者们的训练数据集需要多样化,包含来自不同领域的信息,而 The Pile 是一个合适的选择。尽管它的大小为 825GB,但作者们将仅使用其中的 5%–10%。让作者们先下载数据集,看看它的效果如何。作者将下载 HuggingFace 上提供的版本。

  
`# 下载验证数据集   !wget https://huggingface.co/datasets/monology/pile-uncopyrighted/resolve/main/val.jsonl.zst      # 下载训练数据集的第一部分   !wget https://huggingface.co/datasets/monology/pile-uncopyrighted/resolve/main/train/00.jsonl.zst      # 下载训练数据集的第二部分   !wget https://huggingface.co/datasets/monology/pile-uncopyrighted/resolve/main/train/01.jsonl.zst      # 下载训练数据集的第三部分   !wget https://huggingface.co/datasets/monology/pile-uncopyrighted/resolve/main/train/02.jsonl.zst   `

下载需要一些时间,但你也可以将训练数据集限制为仅一个文件,即 00.jsonl.zst,而不是三个。它已经分为训练/验证/测试集。完成后,确保将文件正确放置到各自的目录中。

  
`import os   import shutil   import glob      # 定义目录结构   train_dir = "data/train"   val_dir = "data/val"      # 如果目录不存在,则创建它们   os.makedirs(train_dir, exist_ok=True)   os.makedirs(val_dir, exist_ok=True)      # 移动所有训练文件(例如,00.jsonl.zst、01.jsonl.zst 等)   train_files = glob.glob("*.jsonl.zst")   for file in train_files:       if file.startswith("val"):           # 移动验证文件           dest = os.path.join(val_dir, file)       else:           # 移动训练文件           dest = os.path.join(train_dir, file)       shutil.move(file, dest)   `

作者们的数据集是 .jsonl.zst 格式,这是一种常用于存储大型数据集的压缩文件格式。它结合了 JSON Lines(.jsonl,每一行是一个有效的 JSON 对象)和 Zstandard(.zst)压缩。让作者们读取其中一个下载文件的样本,看看它的样子。

  
`in_file = "data/val/val.jsonl.zst"  # 验证文件路径      with zstd.open(in_file, 'r') as in_f:       for i, line in tqdm(enumerate(in_f)):  # 读取前 5 行           data = json.loads(line)           print(f"Line {i}: {data}")  # 打印原始数据以供检查           if i == 2:               break   `

输出结果

Line: 0
{ “text”: “Effect of sleep quality … epilepsy.”, “meta”: { “pile_set_name”: “PubMed Abstracts” } }

Line: 1 { “text”: “LLMops a new GitHub Repository …”, “meta”: { “pile_set_name”: “Github” } }

现在作者们需要对数据集进行编码(分词)。作者们的目标是让 LLM 至少能够输出正确的单词。为此,作者们需要使用一个现成的分词器。作者们将使用 OpenAI 提供的开源分词器 tiktoken,并使用其 r50k_base 分词器(用于 ChatGPT(GPT-3)模型)对作者们的数据集进行分词。

为了避免重复操作,作者们需要创建一个函数,因为作者们将会对训练和验证数据集都进行分词。

  
`def process_files(input_dir, output_file):       """       处理指定输入目录中的所有 .zst 文件,并将编码后的token保存到 HDF5 文件中。          参数:           input_dir (str): 包含输入 .zst 文件的目录。           output_file (str): 输出 HDF5 文件的路径。       """       with h5py.File(output_file, 'w') as out_f:           # 在 HDF5 文件中创建一个可扩展的数据集,名为 'tokens'           dataset = out_f.create_dataset('tokens', (0,), maxshape=(None,), dtype='i')           start_index = 0              # 遍历输入目录中的所有 .zst 文件           for filename in sorted(os.listdir(input_dir)):               if filename.endswith(".jsonl.zst"):                   in_file = os.path.join(input_dir, filename)                   print(f"Processing: {in_file}")                      # 打开 .zst 文件进行读取                   with zstd.open(in_file, 'r') as in_f:                       # 遍历压缩文件中的每一行                       for line in tqdm(in_f, desc=f"Processing {filename}"):                           # 将行加载为 JSON                           data = json.loads(line)                              # 在文本末尾追加结束文本token并进行编码                           text = data['text'] + "<|endoftext|>"                           encoded = enc.encode(text, allowed_special={'<|endoftext|>'})                           encoded_len = len(encoded)                              # 计算新token的结束索引                           end_index = start_index + encoded_len                              # 扩展数据集大小并存储编码后的token                           dataset.resize(dataset.shape[0] + encoded_len, axis=0)                           dataset[start_index:end_index] = encoded                              # 更新下一批token的起始索引                           start_index = end_index   `

关于这个函数,有两个重要点需要注意:

  • 作者们将分词后的数据存储在 HDF5 文件中,这使得在训练模型时能够更快速地访问数据。

  • 添加 <|endoftext|> token用于token每个文本序列的结束,向模型表明它已经到达了一个有意义的上下文的结尾,这有助于生成连贯的输出。

现在,作者们可以简单地使用以下代码对训练和验证数据集进行编码:

  
`# 定义分词数据的输出目录   out_train_file = "data/train/pile_train.h5"   out_val_file = "data/val/pile_dev.h5"      # 加载 GPT-3/GPT-2 模型的分词器   enc = tiktoken.get_encoding('r50k_base')      # 处理训练数据   process_files(train_dir, out_train_file)      # 处理验证数据   process_files(val_dir, out_val_file)   `

让作者们查看分词数据的样本:

  
`with h5py.File(out_val_file, 'r') as file:       # 访问 'tokens' 数据集       tokens_dataset = file['tokens']              # 打印数据集的数据类型       print(f"'tokens' 数据集的数据类型:{tokens_dataset.dtype}")              # 加载并打印数据集的前几个元素       print("数据集的前几个元素:")       print(tokens_dataset[:10])  # 打印前 10 个token   `

输出结果

数据集的数据类型:int32

数据集的前几个元素: [ 2725 6557 83 23105 157 119 229 77 5846 2429]

作者们已经为训练准备好了数据集。接下来,作者们将编写 Transformer 架构,并相应地探讨其理论。

Transformer 概述 让作者们快速了解一下 Transformer 架构是如何处理和理解文本的。它通过将文本分解为称为token的小片段,并预测序列中的下一个token来工作。Transformer 有许多层,称为 Transformer 块,这些层叠加在一起,并在最后用一层来进行预测。

每个 Transformer 块有两个主要组成部分:

  • 自注意力头(Self-Attention Heads):这些部分决定了输入中哪些部分对模型来说最为重要。例如,在处理句子时,注意力头可以突出单词之间的关系,例如代词与其所指代的名词之间的关系。

  • 多层感知机(MLP):这是一个简单的前馈神经网络。它接收自注意力头所强调的信息,并进一步处理。MLP 有一个输入层,用于接收来自注意力头的数据;一个隐藏层,用于增加处理的复杂性;以及一个输出层,用于将结果传递到下一个 Transformer 块。

总的来说,注意力头是“思考什么”的部分,而 MLP 是“如何思考”的部分。将许多 Transformer 块堆叠在一起,可以让模型理解文本中的复杂模式和关系,但这并不总是能够保证。

与其查看原始论文中的图表,不如让作者们可视化一个更简单、更容易理解的架构图表,这就是作者们将要编写的代码。

Transformer 架构

Transformer 架构

让作者们阅读一下作者们将要编写的架构流程:

  1. 输入token被转换为嵌入,并与位置信息相结合。

  2. 模型有 64 个相同的 Transformer 块,这些块按顺序处理数据。

  3. 每个块首先运行多头注意力,以查看token之间的关系。

  4. 每个块然后通过 MLP 处理数据,该 MLP 先扩展数据,然后再压缩数据。

  5. 每一步都使用残差连接(捷径),以帮助信息流动。

  6. 在整个过程中使用层归一化,以稳定训练。

  7. 注意力机制计算哪些token应该相互关注。

  8. MLP 将数据扩展到 4 倍大小,应用 ReLU,然后再压缩回去。

  9. 模型使用 16 个注意力头来捕获不同类型的关系。

  10. 最后一层将处理后的数据转换为与词汇表大小相同的预测结果。

  11. 模型通过反复预测下一个最有可能的token来生成文本。

多层感知机(MLP)

MLP 是 Transformer 前馈网络中的一个基础构建模块。它的作用是引入非线性,并学习嵌入表示中的复杂关系。在定义 MLP 模块时,一个重要的参数是 n_embed,它定义了输入嵌入的维度。

MLP 通常由一个隐藏线性层组成,该层将输入维度扩展一个因子(通常是 4,作者们将使用这个值),后面跟着一个非线性激活函数,通常是 ReLU。这种结构允许作者们的网络学习更复杂的特征。最后,一个投影线性层将扩展后的表示映射回原始嵌入维度。这一系列变换使 MLP 能够细化注意力机制所学习到的表示。

  
`# --- 多层感知机(MLP)类 ---      class MLP(nn.Module):       """       一个简单的多层感知机,包含一个隐藏层。          该模块用于 Transformer 块中的前馈处理。       它扩展输入嵌入的大小,应用 ReLU 激活,然后将其投影回原始嵌入大小。       """       def __init__(self, n_embed):           super().__init__()           self.hidden = nn.Linear(n_embed, 4 * n_embed)  # 线性层,用于扩展嵌入大小           self.relu = nn.ReLU()                        # ReLU 激活函数           self.proj = nn.Linear(4 * n_embed, n_embed)  # 线性层,用于投影回原始大小          def forward(self, x):           """           MLP 的前向传播。              参数:               x (torch.Tensor):输入张量,形状为 (B, T, C),其中 B 是批量大小,                                 T 是序列长度,C 是嵌入大小。              返回:               torch.Tensor:与输入形状相同的输出张量。           """           x = self.forward_embedding(x)           x = self.project_embedding(x)           return x          def forward_embedding(self, x):           """           应用隐藏线性层,然后是 ReLU 激活。              参数:               x (torch.Tensor):输入张量。              返回:               torch.Tensor:经过隐藏层和 ReLU 后的输出。           """           x = self.relu(self.hidden(x))           return x          def project_embedding(self, x):           """           应用投影线性层。              参数:               x (torch.Tensor):输入张量。              返回:               torch.Tensor:经过投影层后的输出。           """           x = self.proj(x)           return x   `

作者们刚刚编写了 MLP 的代码,其中 __init__ 方法初始化了一个隐藏线性层,用于扩展输入嵌入大小(n_embed),以及一个投影层,用于将其缩小回去。ReLU 激活函数在隐藏层之后应用。forward 方法定义了数据通过这些层的流动,通过 forward_embedding 应用隐藏层和 ReLU,通过 project_embedding 应用投影层。

单头注意力

注意力头是模型的核心部分,它的作用是专注于输入序列的相关部分。在定义 Head 模块时,一些重要的参数是 head_sizen_embedcontext_lengthhead_size 参数决定了键(key)、查询(query)和值(value)投影的维度,从而影响注意力机制的表现能力。

输入嵌入的维度 n_embed 定义了这些投影层的输入大小。context_length 用于创建因果掩码(causal mask),确保模型只能关注前面的token。

Head 中,键、查询和值的线性层(nn.Linear)被初始化为无偏置。基于 context_length 的下三角矩阵被注册为缓冲区,用于实现因果掩码,防止注意力机制关注未来的token。

  
`# --- 注意力头类 ---      class Head(nn.Module):       """       单个注意力头。          该模块计算注意力分数,并将其应用于值。       它包括键、查询和值的投影,并使用因果掩码防止关注未来的token。       """       def __init__(self, head_size, n_embed, context_length):           super().__init__()           self.key = nn.Linear(n_embed, head_size, bias=False)   # 键投影           self.query = nn.Linear(n_embed, head_size, bias=False) # 查询投影           self.value = nn.Linear(n_embed, head_size, bias=False) # 值投影           # 下三角矩阵,用于因果掩码           self.register_buffer('tril', torch.tril(torch.ones(context_length, context_length)))          def forward(self, x):           """           注意力头的前向传播。              参数:               x (torch.Tensor):输入张量,形状为 (B, T, C)。              返回:               torch.Tensor:应用注意力后的输出张量。           """           B, T, C = x.shape           k = self.key(x)     # (B, T, head_size)           q = self.query(x)   # (B, T, head_size)           scale_factor = 1 / math.sqrt(C)           # 计算注意力权重:(B, T, head_size) @ (B, head_size, T) -> (B, T, T)           attn_weights = q @ k.transpose(-2, -1) * scale_factor           # 应用因果掩码           attn_weights = attn_weights.masked_fill(self.tril[:T, :T] == 0, float('-inf'))           attn_weights = F.softmax(attn_weights, dim=-1)           v = self.value(x)   # (B, T, head_size)           # 将注意力权重应用于值           out = attn_weights @ v # (B, T, T) @ (B, T, head_size) -> (B, T, head_size)           return out   `

作者们的注意力头类的 __init__ 方法初始化了键、查询和值的线性层,每个线性层都将输入嵌入(n_embed)投影到 head_size。基于 context_length 的下三角矩阵用于因果掩码。forward 方法通过缩放查询和键的点积来计算注意力权重,应用因果掩码,使用 softmax 归一化权重,并计算值的加权和以产生注意力输出。

多头注意力

为了捕获输入序列中的多样化关系,作者们将使用多头注意力的概念。MultiHeadAttention 模块管理多个独立的注意力头,这些头并行运行。

这里的关键参数是 n_head,它决定了并行注意力头的数量。输入嵌入的维度(n_embed)和 context_length 也是实例化各个注意力头所必需的。每个头独立处理输入,将其投影到一个较低维度的子空间中,大小为 n_embed // n_head。通过拥有多个头,模型可以同时关注输入的不同方面。

  
`# --- 多头注意力类 ---      class MultiHeadAttention(nn.Module):       """       多头注意力模块。          该模块将多个注意力头并行组合。每个头的输出被连接起来形成最终输出。       """       def __init__(self, n_head, n_embed, context_length):           super().__init__()           self.heads = nn.ModuleList([Head(n_embed // n_head, n_embed, context_length) for _ in range(n_head)])          def forward(self, x):           """           多头注意力的前向传播。              参数:               x (torch.Tensor):输入张量,形状为 (B, T, C)。              返回:               torch.Tensor:连接所有头的输出后的输出张量。           """           # 沿最后一个维度(C)连接每个头的输出           x = torch.cat([h(x) for h in self.heads], dim=-1)           return x   `

现在作者们已经定义了 MultiHeadAttention 类,__init__ 方法初始化了一个包含 n_headHead 实例的列表,每个头的 head_sizen_embed // n_headforward 方法将每个注意力头应用于输入 x,并将它们的输出沿着最后一个维度连接起来,合并每个头所学到的信息。

Transformer 块

要创建一个拥有数十亿参数的模型,作者们肯定需要一个深度架构。为此,作者们需要编写一个 Transformer 块并将它们堆叠起来。块的关键参数是 n_headn_embedcontext_length。每个块包含一个多头注意力层和一个前馈网络(MLP),每个层之前都应用了层归一化,并且每个层之后都有残差连接。

层归一化由嵌入维度 n_embed 参数化,有助于稳定训练。多头注意力机制如前所述,需要 n_headn_embedcontext_length。MLP 也使用嵌入维度 n_embed。这些组件协同工作,处理输入并学习复杂的模式。

  
`# --- Transformer 块类 ---      class Block(nn.Module):       """       单个 Transformer 块。          该块包含一个多头注意力层,后面跟着一个 MLP,       并且在每个层之前应用层归一化,在每个层之后应用残差连接。       """       def __init__(self, n_head, n_embed, context_length):           super().__init__()           self.ln1 = nn.LayerNorm(n_embed)           self.attn = MultiHeadAttention(n_head, n_embed, context_length)           self.ln2 = nn.LayerNorm(n_embed)           self.mlp = MLP(n_embed)          def forward(self, x):           """           Transformer 块的前向传播。              参数:               x (torch.Tensor):输入张量。              返回:               torch.Tensor:经过该块后的输出张量。           """           # 应用多头注意力,并带有残差连接           x = x + self.attn(self.ln1(x))           # 应用 MLP,并带有残差连接           x = x + self.mlp(self.ln2(x))           return x          def forward_embedding(self, x):           """           前向传播,专注于嵌入和注意力部分。              参数:               x (torch.Tensor):输入张量。              返回:               tuple:一个元组,包含经过 MLP 嵌入后的输出和残差。           """           res = x + self.attn(self.ln1(x))           x = self.mlp.forward_embedding(self.ln2(res))           return x, res   `

作者们的 Block 类代表一个单个的 Transformer 块。__init__ 方法初始化了层归一化层(ln1ln2)、一个多头注意力模块(attn)和一个 MLP 模块,所有这些都由 n_headn_embedcontext_length 参数化。

forward 方法实现了该块的前向传播,应用层归一化和多头注意力,并带有残差连接,然后再次应用层归一化和 MLP,同样带有残差连接。forward_embedding 方法提供了一个替代的前向传播,专注于注意力和初始 MLP 嵌入阶段。

最终模型

到目前为止,作者们已经编写了 Transformer 模型的小型组件。接下来,作者们将整合token嵌入和位置嵌入,并将一系列 Transformer 块串联起来,以执行序列到序列的任务。为此,作者们需要编写几个关键参数:n_headn_embedcontext_lengthvocab_sizeN_BLOCKS

vocab_size 决定了token嵌入层的大小,将每个token映射到一个大小为 n_embed 的密集向量。context_length 参数对于位置嵌入层也很重要,它编码输入序列中每个token的位置,其维度也是 n_embed。注意力头的数量(n_head)和块的数量(N_BLOCKS)决定了网络的深度和复杂性。

这些参数共同定义了 Transformer 模型的架构和容量,因此让作者们来编写代码。

  
`# --- Transformer 模型类 ---      class Transformer(nn.Module):       """       主 Transformer 模型。          该类将token嵌入和位置嵌入与一系列 Transformer 块相结合,       并在最后使用一个线性层进行语言建模。       """       def __init__(self, n_head, n_embed, context_length, vocab_size, N_BLOCKS):           super().__init__()           self.context_length = context_length           self.N_BLOCKS = N_BLOCKS           self.token_embed = nn.Embedding(vocab_size, n_embed)           self.position_embed = nn.Embedding(context_length, n_embed)           self.attn_blocks = nn.ModuleList([Block(n_head, n_embed, context_length) for _ in range(N_BLOCKS)])           self.layer_norm = nn.LayerNorm(n_embed)           self.lm_head = nn.Linear(n_embed, vocab_size)           self.register_buffer('pos_idxs', torch.arange(context_length))          def _pre_attn_pass(self, idx):           """           结合token嵌入和位置嵌入。              参数:               idx (torch.Tensor):输入token索引。              返回:               torch.Tensor:token嵌入和位置嵌入的和。           """           B, T = idx.shape           tok_embedding = self.token_embed(idx)           pos_embedding = self.position_embed(self.pos_idxs[:T])           return tok_embedding + pos_embedding          def forward(self, idx, targets=None):           """           Transformer 的前向传播。              参数:               idx (torch.Tensor):输入token索引。               targets (torch.Tensor, 可选):用于损失计算的目标token索引。默认为 None。              返回:               tuple:如果提供了目标,则返回 logits 和损失。           """           x = self._pre_attn_pass(idx)           for block in self.attn_blocks:               x = block(x)           x = self.layer_norm(x)           logits = self.lm_head(x)           loss = None           if targets isnotNone:               B, T, C = logits.shape               flat_logits = logits.view(B * T, C)               targets = targets.view(B * T).long()               loss = F.cross_entropy(flat_logits, targets)           return logits, loss          def forward_embedding(self, idx):           """           前向传播,专注于嵌入和注意力块。              参数:               idx (torch.Tensor):输入token索引。              返回:               tuple:一个元组,包含经过注意力块后的输出和残差。           """           x = self._pre_attn_pass(idx)           residual = x           for block in self.attn_blocks:               x, residual = block.forward_embedding(x)           return x, residual          def generate(self, idx, max_new_tokens):           """           给定一个起始序列,生成新的token。              参数:               idx (torch.Tensor):初始token序列。               max_new_tokens (int):要生成的token数量。              返回:               torch.Tensor:扩展后的token序列。           """           for _ in range(max_new_tokens):               idx_cond = idx[:, -self.context_length:]               logits, _ = self(idx_cond)               logits = logits[:, -1, :]               probs = F.softmax(logits, dim=-1)               idx_next = torch.multinomial(probs, num_samples=1)               idx = torch.cat((idx, idx_next), dim=1)           return idx   `

作者们的 Transformer 类的 __init__ 方法初始化了token嵌入层(token_embed)和位置嵌入层(position_embed)、一系列块模块(attn_blocks)、一个最终的层归一化层(layer_norm)以及一个用于语言建模的线性层(lm_head)。

_pre_attn_pass 方法结合了token嵌入和位置嵌入。forward 方法通过嵌入层和一系列 Transformer 块处理输入序列,应用最终的层归一化,并生成 logits。如果提供了目标,则计算损失。forward_embedding 方法提供了一个中间的前向传播,直到注意力块的输出,而 generate 方法实现了token生成。

批处理

当作者们使用大数据训练深度学习模型时,由于 GPU 的限制,作者们需要将数据分成批次进行处理。因此,让作者们编写一个 get_batch_iterator 函数,它接受 HDF5 文件的路径 data_path、期望的 batch_size、每个序列的 context_length 以及要将数据加载到的设备。

batch_size 决定了在训练期间并行处理的序列数量,而 context_length 指定了每个输入序列的长度。data_path 指向训练数据的存储位置。

  
`# --- 数据加载工具 ---      def get_batch_iterator(data_path, batch_size, context_length, device="gpu"):       """       创建一个从 HDF5 文件中生成数据批次的迭代器。          参数:           data_path (str):包含token化数据的 HDF5 文件路径。           batch_size (int):每个批次中的序列数量。           context_length (int):每个序列的长度。           device (str, 可选):要将数据加载到的设备('cpu' 或 'cuda')。默认为 "cpu"。          生成:           tuple:一个元组,包含输入序列(xb)和目标序列(yb)。       """       # 以读取模式打开 HDF5 文件       with h5py.File(data_path, 'r') as hdf5_file:           # 提取token化序列的数据集           dataset = hdf5_file['tokens']           # 获取数据集的总大小           dataset_size = dataset.shape[0]           # 计算可以从数据中生成的示例(序列)数量           n_examples = (dataset_size - 1) // context_length           # 创建一个示例索引数组,并随机打乱以增加随机性           example_idxs = np.arange(n_examples)           np.random.shuffle(example_idxs)           # 初始化 epoch 计数器和示例计数器           epochs = 0           counter = 0           whileTrue:               # 检查当前批次是否超过了可用示例的数量               if counter + batch_size > n_examples:                   # 再次打乱索引,并将计数器重置为 0                   np.random.shuffle(example_idxs)                   counter = 0                   print(f"完成 epoch {epochs}")  # 在 epoch 结束时打印 epoch 编号                   epochs += 1# 增加 epoch 计数器               # 选择一批随机索引以生成序列               random_indices = example_idxs[counter:counter + batch_size] * context_length               # 根据随机索引从数据集中提取序列               random_samples = torch.tensor(np.array([dataset[idx:idx + context_length + 1] for idx in random_indices]))               # 将输入序列(xb)和目标序列(yb)分开               xb = random_samples[:, :context_length].to(device)  # 输入序列(随机样本的前半部分)               yb = random_samples[:, 1:context_length + 1].to(device)  # 目标序列(随机样本的后半部分)               # 将计数器增加以移动到下一个批次               counter += batch_size               # 为当前批次生成输入和目标序列的元组               yield xb, yb   `

作者们的 get_batch_iterator 函数负责加载和分批处理训练数据。它接受 data_pathbatch_sizecontext_lengthdevice 作为输入。该函数打开 HDF5 文件,打乱数据,然后进入一个无限循环以生成批次。在每次迭代中,它选择数据的一个随机子集,形成一个输入序列(xb)及其对应的目标序列(yb)的批次。

训练参数

现在作者们已经编写好了模型,作者们需要定义训练参数,例如头的数量、块的数量等,以及数据路径。

  
`# --- 配置 ---      # 定义词汇表大小和 Transformer 配置   VOCAB_SIZE = 50304          # 词汇表中唯一token的数量   CONTEXT_LENGTH = 512        # 模型的最大序列长度   N_EMBED = 2048              # 嵌入空间的维度   N_HEAD = 16                 # 每个 Transformer 块中的注意力头数量   N_BLOCKS = 64               # 模型中的 Transformer 块数量      # 训练和验证数据集的路径   TRAIN_PATH = "data/train/pile_val.h5"# 训练数据集的文件路径   DEV_PATH = "data/val/pile_val.h5"      # 验证数据集的文件路径      # Transformer 训练参数   T_BATCH_SIZE = 32          # 每个训练批次的样本数量   T_CONTEXT_LENGTH = 16      # 训练批次的上下文长度   T_TRAIN_STEPS = 200000     # 总训练步数   T_EVAL_STEPS = 1000        # 执行评估的频率(以步数计)   T_EVAL_ITERS = 250         # 评估模型时的迭代次数   T_LR_DECAY_STEP = 50000    # 学习率衰减的步数   T_LR = 5e-4                # 训练的初始学习率   T_LR_DECAYED = 5e-5        # 学习率衰减后的值   T_OUT_PATH = "models/transformer_B.pt"# 保存训练模型的路径      # 设备配置   DEVICE = 'cuda'      # 将所有配置存储在一个字典中,便于访问和修改   default_config = {       'vocab_size': VOCAB_SIZE,       'context_length': CONTEXT_LENGTH,       'n_embed': N_EMBED,       'n_head': N_HEAD,       'n_blocks': N_BLOCKS,       'train_path': TRAIN_PATH,       'dev_path': DEV_PATH,       't_batch_size': T_BATCH_SIZE,       't_context_length': T_CONTEXT_LENGTH,       't_train_steps': T_TRAIN_STEPS,       't_eval_steps': T_EVAL_STEPS,       't_eval_iters': T_EVAL_ITERS,       't_lr_decay_step': T_LR_DECAY_STEP,       't_lr': T_LR,       't_lr_decayed': T_LR_DECAYED,       't_out_path': T_OUT_PATH,       'device': DEVICE,   }   `

对于大多数参数,作者使用了最常见的值,并将它们存储在一个字典中,便于访问。这里,参数是针对一个拥有十亿参数的模型。如果你想要训练一个拥有数百万参数的模型,你可以减少主要参数,包括 CONTEXT_LENGTHN_EMBEDN_HEADN_BLOCKS。然而,你也可以直接运行作者在 GitHub 仓库中的数百万参数模型脚本。

训练模型

让作者们初始化作者们的 Transformer 模型,并检查其总参数数量。

  
`# --- 初始化模型并打印参数 ---      model = Transformer(       n_head=config['n_head'],       n_embed=config['n_embed'],       context_length=config['context_length'],       vocab_size=config['vocab_size'],       N_BLOCKS=config['n_blocks']   ).to(config['device'])      # 打印模型的总参数数量   total_params = sum(p.numel() for p in model.parameters())   print(f"模型的总参数数量:{total_params:,}")   `

输出结果

模型的总参数数量:2,141,346,251

现在作者们有了一个拥有 20 亿参数的模型,作者们需要定义作者们的 Adam 优化器和损失跟踪函数,这将帮助作者们在整个训练过程中跟踪模型的进展。

  
`# --- 优化器设置和损失跟踪 ---      # 使用指定的学习率设置 AdamW 优化器   optimizer = torch.optim.AdamW(model.parameters(), lr=config['t_lr'])      # 用于在训练过程中跟踪损失值的列表   losses = []      # 定义一个窗口大小,用于在训练循环中平均最近的损失值   AVG_WINDOW = 64      # 辅助函数,用于估计训练和验证数据的平均损失   @torch.no_grad()   def estimate_loss(steps):       """       在训练和验证数据集上评估模型,并计算平均损失。          参数:           steps (int):评估的步数。          返回:           dict:一个字典,包含 'train' 和 'dev' 分割的平均损失。       """       out = {}       model.eval()  # 将模型设置为评估模式          for split in ['train', 'dev']:           # 选择当前分割的适当数据路径           data_path = config['train_path'] if split == 'train'else config['dev_path']           # 创建一个用于评估的批次迭代器           batch_iterator_eval = get_batch_iterator(               data_path, config['t_batch_size'], config['t_context_length'], device=config['device']           )           # 初始化一个张量,用于跟踪每个评估步骤的损失值           losses_eval = torch.zeros(steps)           for k in range(steps):               try:                   # 获取一个批次并计算损失                   xb, yb = next(batch_iterator_eval)                   _, loss = model(xb, yb)                   losses_eval[k] = loss.item()               except StopIteration:                   # 处理数据迭代器提前结束的情况                   print(f"警告:{split} 的迭代器提前结束。")                   break           # 计算当前分割的平均损失           out[split] = losses_eval[:k + 1].mean()       model.train()  # 将模型恢复为训练模式       return out   `

现在,作者们将初始化作者们的批次处理函数和训练循环,这将开始作者们的训练。

  
`# --- 训练循环 ---      # 为训练数据创建一个批次迭代器   batch_iterator = get_batch_iterator(       config['train_path'],       config['t_batch_size'],       config['t_context_length'],       device=config['device']   )      # 创建一个进度条,用于监控训练进度   pbar = tqdm(range(config['t_train_steps']))   for step in pbar:       try:           # 获取一个输入和目标数据的批次           xb, yb = next(batch_iterator)           # 执行前向传播并计算损失           _, loss = model(xb, yb)           # 记录损失以便跟踪           losses.append(loss.item())           pbar.set_description(f"训练损失:{np.mean(losses[-AVG_WINDOW:]):.4f}")           # 反向传播损失并更新模型参数           optimizer.zero_grad(set_to_none=True)           loss.backward()           optimizer.step()           # 定期在训练和验证数据上评估模型           if step % config['t_eval_steps'] == 0:               train_loss, dev_loss = estimate_loss(config['t_eval_iters']).values()               print(f"步数:{step}, 训练损失:{train_loss:.4f}, 验证损失:{dev_loss:.4f}")           # 在指定的步数处衰减学习率           if step == config['t_lr_decay_step']:               print('衰减学习率')               for g in optimizer.param_groups:                   g['lr'] = config['t_lr_decayed']       except StopIteration:           # 处理训练数据迭代器提前结束的情况           print("训练数据迭代器提前结束。")           break   `

保存训练好的模型

由于作者们的训练循环具备处理错误的能力,如果循环抛出任何错误,它将保存作者们部分训练好的模型,以避免损失。一旦训练完成,作者们可以保存作者们训练好的模型,以便后续用于推理。

  
`# --- 保存模型并进行最终评估 ---      # 在训练和验证数据集上对模型进行最终评估   train_loss, dev_loss = estimate_loss(200).values()      # 确保模型保存路径是唯一的,以防文件已存在   modified_model_out_path = config['t_out_path']   save_tries = 0   while os.path.exists(modified_model_out_path):       save_tries += 1       model_out_name = os.path.splitext(config['t_out_path'])[0]       modified_model_out_path = model_out_name + f"_{save_tries}" + ".pt"      # 保存模型的状态字典、优化器状态和训练元数据   torch.save(       {           'model_state_dict': model.state_dict(),           'optimizer_state_dict': optimizer.state_dict(),           'losses': losses,           'train_loss': train_loss,           'dev_loss': dev_loss,           'steps': len(losses),       },       modified_model_out_path   )   print(f"模型已保存至 {modified_model_out_path}")   print(f"训练完成。训练损失:{train_loss:.4f}, 验证损失:{dev_loss:.4f}")   `

拥有十亿参数的模型的最终训练损失为 0.2314,验证损失为 0.643。

训练损失

当作者绘制数百万参数和十亿参数模型的损失曲线时,它们看起来非常不同。

十亿参数模型的损失在开始时要高得多,并且在初期波动很大。它最初迅速下降,但随后会晃动,然后才变得平稳。这表明较大的模型在开始时更难找到正确的学习方式。它可能需要更多的数据和仔细的设置。当学习率降低(红色线条)时,损失更加稳定地下降,这表明这有助于它进行微调。

数百万参数模型的损失从一开始就更容易下降。它没有像较大模型那样大的波动。当学习率降低时,曲线的变化不大。这可能是因为较小的模型更简单,更容易训练,并且更快地找到好的解决方案。这种巨大差异表明,训练非常大的模型要困难得多。它们需要不同的方法,甚至可能需要更多的时间来学习。

现在作者们已经保存了作者们的模型。作者们终于可以使用它进行推理,并看看它如何生成文本。

生成文本

让作者们创建一个函数,从作者们保存的模型中生成文本,它接受保存的模型路径和编码器作为输入,并返回生成的文本。

  
`def generate_text(model_path, input_text, max_length=512, device="gpu"):       """       使用预训练的模型根据给定的输入文本生成文本。          参数:       - model_path (str):模型检查点的路径。       - device (torch.device):要将模型加载到的设备(例如,'cpu' 或 'cuda')。       - input_text (str):用于启动生成的输入文本。       - max_length (int, 可选):生成文本的最大长度。默认为 512。          返回:       - str:生成的文本。       """       # 加载模型检查点       checkpoint = torch.load(model_path)       # 初始化模型(确保在其他地方定义了 Transformer 类)       model = Transformer().to(device)       # 加载模型的状态字典       model.load_state_dict(checkpoint['model_state_dict'])       # 加载 GPT 模型的分词器(作者们使用 GPT 模型的 'r50k_base')       enc = tiktoken.get_encoding('r50k_base')       # 对输入文本进行编码,并添加结束文本token       input_ids = torch.tensor(           enc.encode(input_text, allowed_special={'<|endoftext|>'}),           dtype=torch.long       )[None, :].to(device)  # 添加批量维度,并移动到指定设备       # 使用编码后的输入生成文本       with torch.no_grad():           # 生成最多 'max_length' 个token的文本           generated_output = model.generate(input_ids, max_length)           # 将生成的token解码回文本           generated_text = enc.decode(generated_output[0].tolist())       return generated_text   `

作者们之前定义的 Transformer 需要在这里被调用以加载架构,然后作者们将保存的模型作为该架构中的状态加载。

让作者们先看看数百万参数和十亿参数模型在没有提供任何输入的情况下会随机生成什么内容。

  
`# 定义预训练模型的文件路径   Billion_model_path = 'models/transformer_B.pt'# 十亿模型的路径   Million_model_path = 'models/transformer_M.pt'# 数百万模型的路径      # 使用 '<|endoftext|>' 作为模型的输入(作为提示,允许模型自由生成文本)   input_text = "<|endoftext|>"      # 调用函数,使用十亿模型根据输入文本生成文本   B_output = generate_text(Billion_model_path, input_text)      # 调用函数,使用数百万模型根据输入文本生成文本   M_output = generate_text(Million_model_path, input_text)      # 打印两个模型生成的输出   print(B_output)  # 十亿模型的输出   print(M_output)  # 数百万模型的输出   `

两个 LLM 都能够在上下文较短且简单时生成清晰准确的单词。例如,在数百万参数的输出中,“The villages were directly linked to cities in China” 这句话是有意义的,并且传达了一个清晰的想法。它易于理解,并且逻辑上将村庄与城市联系起来。

然而,当上下文变得更长更复杂时,清晰度开始减弱。在十亿参数的输出中,像 “There are two miles east coast from 1037 and 73 million refugees (hypotetus)” 和 “blacksmith, musician and boutique hospitality and inspire the strain delivered Canadians” 这样的句子变得难以理解。想法似乎脱节,句子结构也不自然。尽管使用的单词可能仍然正确,但整体含义变得令人困惑且不清晰。

积极的一面是,1300 万 + 参数的 LLM 也开始生成某种有意义的内容,并且单词拼写正确。例如,当作者使用主题输入文本时,它开始为作者生成一封电子邮件。尽管显然,更广泛的文本并没有提供有意义的结果,但让作者们看看输出:

  
`# 输入文本   input_text = "Subject: "      # 调用数百万参数模型   m_output = generate_text(Million_model_path, input_text)      print(m_output)  # 数百万模型的输出   `

作者们的数百万参数模型让作者们相信,作者们可以拥有一个非常狭窄、目标明确的 LLM,其参数量不到 10 亿,而作者们的 10 亿参数训练模型则向作者们展示了架构需要深度编码并经过适当考虑。否则,它不会比数百万参数模型更好地进行训练或提高性能。除非你有一个深度架构,否则它只会过拟合数据。

如何学习大模型 AI ?

由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。

但是具体到个人,只能说是:

“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。

这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。

我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

在这里插入图片描述

第一阶段(10天):初阶应用

该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。

  • 大模型 AI 能干什么?
  • 大模型是怎样获得「智能」的?
  • 用好 AI 的核心心法
  • 大模型应用业务架构
  • 大模型应用技术架构
  • 代码示例:向 GPT-3.5 灌入新知识
  • 提示工程的意义和核心思想
  • Prompt 典型构成
  • 指令调优方法论
  • 思维链和思维树
  • Prompt 攻击和防范

第二阶段(30天):高阶应用

该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。

  • 为什么要做 RAG
  • 搭建一个简单的 ChatPDF
  • 检索的基础概念
  • 什么是向量表示(Embeddings)
  • 向量数据库与向量检索
  • 基于向量检索的 RAG
  • 搭建 RAG 系统的扩展知识
  • 混合检索与 RAG-Fusion 简介
  • 向量模型本地部署

第三阶段(30天):模型训练

恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。

到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?

  • 为什么要做 RAG
  • 什么是模型
  • 什么是模型训练
  • 求解器 & 损失函数简介
  • 小实验2:手写一个简单的神经网络并训练它
  • 什么是训练/预训练/微调/轻量化微调
  • Transformer结构简介
  • 轻量化微调
  • 实验数据集的构建

第四阶段(20天):商业闭环

对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。

  • 硬件选型
  • 带你了解全球大模型
  • 使用国产大模型服务
  • 搭建 OpenAI 代理
  • 热身:基于阿里云 PAI 部署 Stable Diffusion
  • 在本地计算机运行大模型
  • 大模型的私有化部署
  • 基于 vLLM 部署大模型
  • 案例:如何优雅地在阿里云私有部署开源大模型
  • 部署一套开源 LLM 项目
  • 内容安全
  • 互联网信息服务算法备案

学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。

如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

在这里插入图片描述

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐