大模型原理:构建GPT-2大语言模型

介绍

大语言模型,比如GPT(生成式预训练Transformer),是旨在一次生成一个词(或词元)的大型深度神经网络架构。
然而,尽管这些模型规模很大,但架构并不那么复杂,因为许多组件是重复的
下图是类GPT大语言模型的自顶向下的示意图,除了嵌入层,还包含一个或多个Transformer块,这些块中包括之前实现的掩码多头注意力模块:

GPT-2与GPT-3

主要关注 GPT-2,因为OpenAI已经公开了该预训练模型的权重。GPT-2是学习实现大语言模型的更好选择,因为能够在单台笔记本电脑上运行

GPT-3在模型架构上与GPT-2基本相同,只是将参数量从GPT-2的15亿扩展到了1750亿,并且训练的数据量更多了。GPT-3需要依赖GPU集群来完成训练和推断。

根据Lambda实验室的说法,在单个V100数据中心GPU上训练GPT-3需要355年,在消费级RTX 8000GPU上则需要665年。

代码实现1-配置参数

通过以下Python字典指定小型GPT-2模型的配置

GPT_CONFIG_124M = {
    "vocab_size": 50257,     # 词汇表大小
    "context_length": 1024,  # 上下文长度
    "emb_dim": 768,          # 嵌入维度
    "n_heads": 12,           # 注意力头的数量
    "n_layers": 12,          # 层数
    "drop_rate": 0.1,        # dropout率
    "qkv_bias": False        # 查询-键-值偏置
}

参数解释:

在GPT_CONFIG_124M字典中,使用简洁的变量名来提高代码的可读性并避免代码过长。

  • vocab_size:表示会被BPE分词器使用的由 50257 个单词组成的词汇表。

  • context_length:指的是模型通过位置嵌入能够处理的最大输入词元数量。

  • emb_dim:表示嵌入维度大小,可以将每个词元转化为768维的向量。

  • n_heads:表示多头注意力机制中注意力头的数量。

  • n_layers:表示模型中的Transformer块数量,接下来的讨论中将介绍。

  • drop_rate:表示dropout机制的强度( 0.1 表示有 10% 的隐藏单元被随机丢弃),以防止过拟合。

  • qkv_bias:指的是是否在多头注意力机制的线性层中添加一个偏置向量,用于查询、键和值的计算。遵循现代大语言模型的做法,在一开始禁用它,在加载OpenAI的预训练GPT-2权重时,会再解释。

解释

通过该配置,实现一个GPT占位架构(DummyGPTModel),如下图所示。从全局上理解各个部分如何协同工作,以及构建完整的GPT模型架构还需要编写哪些其他组件。

编写GPT架构的步骤:

  1. 首先从GPT主干入手,创建一个占位符架构;
  2. 然后实现各个核心组件;
  3. 最后将它们组装成Transformer块,形成完整的GPT架构

代码实现2-封装类

DummyGPTModel

DummyGPTModel 类基于PyTorch的神经网络模块(nn.Module)定义了一个简化版的类GPT模型。DummyGPTModel类中的模型架构包括词元和位置嵌入、dropout、一系列Transformer块(DummyTransformerBlock)、最终层归一化(DummyLayerNorm)和线性输出层(out_head)。配置信息通过一个Python字典(比如之前创建的 GPT_CONFIG_124M )传入

forward 方法描述了数据在模型中的处理流程:

  1. 首先计算输入索引的词元和位置嵌入
  2. 然后应用dropout
  3. 接着通过Transformer块处理数据
  4. 再应用归一化
  5. 最后使用线性输出层生成logits
import torch
import torch.nn as nn


class DummyGPTModel(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
        self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
        self.drop_emb = nn.Dropout(cfg["drop_rate"])
        # 使用占位符替换 TransformerBlock
        self.trf_blocks = nn.Sequential(
            *[DummyTransformerBlock(cfg) for _ in range(cfg["n_layers"])]
        )
        # 使用占位符替换层归一化
        self.final_norm = DummyLayerNorm(cfg["emb_dim"])
        self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)

    def forward(self, in_idx):
        batch_size, seq_len = in_idx.shape
        tok_embeds = self.tok_emb(in_idx)
        pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
        x = tok_embeds + pos_embeds
        x = self.drop_emb(x)
        x = self.trf_blocks(x)
        x = self.final_norm(x)
        logits = self.out_head(x)
        return logits

DummyTransformerBlock

作为Transformer块,将在以后开发

class DummyTransformerBlock(nn.Module):
    """
    一个简单的占位符类,稍后将被真正的TransformerBlock替换
    """
    def __init__(self, cfg):
        super().__init__()

    def forward(self, x):
        """
        这个代码块不执行任何操作,只返回其输入
        :param x:
        :return:
        """
        return x

DummyLayerNorm

作为 归一化层,将在以后开发

class DummyLayerNorm(nn.Module):
    """
    一个简单的占位符类,稍后将被真正的层归一化替换
    """
    def __init__(self, normalized_shape, eps=1e-5):
        """
        这里的参数只是为了模仿层归一化的接口
        :param normalized_shape:
        :param eps:
        """
        super().__init__()

    def forward(self, x):
        return x

代码实现3-准备输入数据

准备输入数据并初始化一个新的GPT模型,以展示其使用方法。基于对分词器的实现

如下图,展示输入数据如何被分词、嵌入,并输入到GPT模型中,在GPT模型中的流入和流出过程:

代码

使用 tiktoken 分词器对包含两个文本输入的批次进行分词处理,以供GPT模型使用:

# 准备输入数据

import tiktoken

tokenizer = tiktoken.get_encoding("gpt2")
batch = []
txt1 = "Every effort moves you"
txt2 = "Every day holds a"

batch.append(torch.tensor(tokenizer.encode(txt1)))
batch.append(torch.tensor(tokenizer.encode(txt2)))
batch = torch.stack(batch, dim=0)
print("batch:\n", batch)

执行结果,以下是这两个文本的词元ID序列:

tensor([[6109, 3626, 6100,  345],
        [6109, 1110, 6622,  257]])

代码实现4-初始化DummyGPTModel

初始化参数量为 1.24亿 的 DummyGPTModel 实例,并将分词后的批次数据传递给它:

# 初始化参数量为 `1.24亿` 的 `DummyGPTModel` 实例
print("\n\n初始化参数量为 `1.24亿` 的 `DummyGPTModel` 实例-----")
# 设置随机种子,复现数据
torch.manual_seed(123)
# 创建 DummyGPTModel 对象
model = DummyGPTModel(GPT_CONFIG_124M)
# 传入分词后的批次数据
logits = model(batch)
print("Output shape:", logits.shape)
print("logits:\n", logits)

执行结果:

初始化参数量为 `1.24亿` 的 `DummyGPTModel` 实例-----
Output shape: torch.Size([2, 4, 50257])
logits:
 tensor([[[-1.2034,  0.3201, -0.7130,  ..., -1.5548, -0.2390, -0.4667],
         [-0.1192,  0.4539, -0.4432,  ...,  0.2392,  1.3469,  1.2430],
         [ 0.5307,  1.6720, -0.4695,  ...,  1.1966,  0.0111,  0.5835],
         [ 0.0139,  1.6755, -0.3388,  ...,  1.1586, -0.0435, -1.0400]],

        [[-1.0908,  0.1798, -0.9484,  ..., -1.6047,  0.2439, -0.4530],
         [-0.7860,  0.5581, -0.0610,  ...,  0.4835, -0.0077,  1.6621],
         [ 0.3567,  1.2698, -0.6398,  ..., -0.0162, -0.1296,  0.3717],
         [-0.2407, -0.7349, -0.5102,  ...,  2.0057, -0.3694,  0.1814]]],
       grad_fn=<UnsafeViewBackward0>)

解释1

输出张量包含两行,对应于两个文本样本。每个文本样本由 4 个词元组成,如下:

txt1 = "Every effort moves you"
txt2 = "Every day holds a"

解释2

每个词元是一个 50 257 维的向量,与分词器的词汇表大小一致,如下:

解释3

嵌入的维度为 50 257 ,因为这些维度中的每一维代表词汇表中的一个唯一词元。

解释4

在实现后处理代码时,将把这些 50 257 维的向量转换回 词元ID,然后将它们 解码为单词。


原文出处:/show_1GW2dwlaqFXF.html