从0开始搭建大语言模型:Dataset构造

接上文:【教程】从0开始搭建大语言模型:文本预处理

通过滑动窗口进行数据采样

我们要构造输入-目标对来对模型进行训练。

在LLM中,它通过预测文本中的下一个单词进行训练,如下所示:
在这里插入图片描述
给定一个文本样本,提取输入块作为LLM的输入子样本,LLM在训练期间的预测任务是预测输入块之后的下一个单词。在训练过程中,我们屏蔽掉所有超过目标的单词。

为下一个单词预测任务创建输入-目标对的最简单和最直观的方法之一是创建两个变量,x和y,其中x包含输入token,y包含目标,它们之间的下标偏移为1,代码实现如下所示:

import tiktoken
with open("./data/the_verdict.txt", "r", encoding="utf-8") as f:
    raw_text = f.read()
tokenizer = tiktoken.get_encoding("gpt2")
enc_text = tokenizer.encode(raw_text)
# 移除前面50个
enc_sample = enc_text[50:]
# 构建input-target对
context_size = 4
x = enc_sample[:context_size]
# 跟x相比往后偏移一个距离
y = enc_sample[1:context_size+1]

输入和目标之间的变化用代码输出为:

# token id形式的表示
for i in range(1, context_size+1):
    context = enc_sample[:i]
    desired = enc_sample[i]
    print(context, "---->", desired)
# 转换为token形式
for i in range(1, context_size+1):
    context = enc_sample[:i]
    desired = enc_sample[i]
    print(tokenizer.decode(context), "---->", tokenizer.decode([desired]))

对应的输出为:

[290] ----> 4920
[290, 4920] ----> 2241
[290, 4920, 2241] ----> 287
[290, 4920, 2241, 287] ----> 257
and ---->  established
and established ---->  himself
and established himself ---->  in
and established himself in ---->  a

在训练的时候,我们希望数据加载器返回两个张量:一个是包含LLM看到的文本的输入张量,一个是包含LLM要预测的目标张量,如下图所示:
在这里插入图片描述
图中为了方便理解展示的是文本token,但实际上都是token id,即一些数字。

为了实现这一功能,使用PytorchDatasetDataLoader,它可以高效地实现。
代码如下:

import torch
from torch.utils.data import Dataset, DataLoader
class GPTDatasetV1(Dataset):
    def __init__(self, txt, tokenizer, max_length, stride):
        # max_length表示选中的文本的最大长度,stride表示间隔多少个词再开始
        self.tokenizer = tokenizer
        self.input_ids = []
        self.target_ids = []
        token_ids = tokenizer.encode(txt)
        # 构造训练集
        for i in range(0, len(token_ids) - max_length, stride):
            # 一个input-target对
            input_chunk = token_ids[i:i + max_length]
            target_chunk = token_ids[i + 1: i + max_length + 1]
            # 加到列表里面
            self.input_ids.append(torch.tensor(input_chunk))
            self.target_ids.append(torch.tensor(target_chunk))
    # 返回数据集大小
    def __len__(self):
        return len(self.input_ids)

    def __getitem__(self, idx):
        return self.input_ids[idx], self.target_ids[idx]

# 创建data loader
def create_dataloader_v1(txt, batch_size=4, max_length=256, stride=128, shuffle=True, drop_last=True):
    tokenizer = tiktoken.get_encoding("gpt2")
    dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=shuffle, drop_last=drop_last)
    return dataloader

调用这两个类,代码为:

# 创建loader,进行数据加载
## stride设置规定了输入在批次中移动的距离,模拟滑动窗口方法
dataloader = create_dataloader_v1(raw_text, batch_size=1, max_length=4, stride=1, shuffle=False)
data_iter = iter(dataloader)
first_batch = next(data_iter)
print(first_batch)

输出为:

[tensor([[  40,  367, 2885, 1464]]), tensor([[ 367, 2885, 1464, 1807]])]

create_dataloader_v1的参数中,stride设置规定了输入在批次中移动的距离,模拟滑动窗口方法,当stride等于窗口大小,即max_length时,可以防止每个batch之间的重叠,过多的重叠可能导致过拟合,如下所示:
在这里插入图片描述

创建token embeddings

准备用于LLM训练的输入文本的最后一步是将token id转换为embedding向量,如下所示:
在这里插入图片描述
一般来说,我们刚开始时用随机值初始化这些embedding权重。这个初始化是LLM学习过程的起点。

在Pytorch中,我们使用nn.Embedding来生成embedding,示例代码为:

input_ids = torch.tensor([2, 3, 5, 1])
vocab_size = 6
output_dim = 3
torch.manual_seed(123)
embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
print(embedding_layer.weight)

这里,vocab_size是字典的大小,output_dim是embedding的尺寸。
输出如下,其中Embedding层的权重尺寸为vocab_size * output_dim

Parameter containing:
tensor([[ 0.3374, -0.1778, -0.1690],
        [ 0.9178,  1.5810,  1.3010],
        [ 1.2753, -0.2010, -0.1606],
        [-0.4015,  0.9666, -1.1481],
        [-1.1589,  0.3255, -0.6315],
        [-2.8400, -0.7849, -1.4096]], requires_grad=True)

我们可以看到权重矩阵有6行3列。词汇表中有6个可能的单词,每个单词对应一行。每个embedding维度都有一列。

对一个token id进行embedding,代码为:

print(embedding_layer(torch.tensor([3])))

此时的输出为:

tensor([[-0.4015,  0.9666, -1.1481]], grad_fn=<EmbeddingBackward0>)

维度是3,表示3这个token id对应的embedding尺寸。

如果我们将ID为3的标记的embedding向量与之前的embedding矩阵进行比较,我们看到它与第4行相同(Python从索引0开始,因此它是与索引3对应的行)。embedding层本质上是一个查找操作,通过token ID从embedding层的权重矩阵中检索行。如下所示:
在这里插入图片描述

编码word位置

LLM的一个小缺点是其中的自注意力机制没有序列中token的位置或顺序的概念。前面介绍的embedding层的工作方式是,相同的token ID总是映射到相同的向量表示,无论令牌ID在输入序列中的位置,如下图所示。
在这里插入图片描述
原则上,token ID的确定性、位置无关的embedding有利于再现性。然而,由于LLM本身的自注意力机制也是位置无关的,因此有助于向LLM中加入额外的位置信息,有两种方式:

  • 相对位置embeddings
  • 绝对位置embeddings

绝对位置embeddings与序列中的特定位置直接关联。对于输入序列中的每个位置,在token的embeddings中添加一个唯一的embeddings,以传递其确切位置。如下图所示:
在这里插入图片描述
相对位置嵌入的重点是标记之间的相对位置或距离。这意味着模型根据“距离多远”而不是“确切位置”来学习关系。这样做的好处是,即使在训练过程中没有看到过长度不同的序列,模型也可以更好地泛化。

这两种类型的位置embedding旨在增强LLM理解token之间的顺序和关系的能力,确保更准确和上下文感知的预测。

OpenAI的GPT模型使用在训练过程中可以优化的绝对位置embedding,而不是像原始Transformer模型中的位置编码那样是固定的或预定义的。

接下来我们来完成对上一节的the_verdict文本内容的embedding,代码如下:

output_dim = 256
vocab_size = 50257
token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
max_length = 4
dataloader = create_dataloader_v1(raw_text, batch_size=8, max_length=max_length, stride=max_length, shuffle=False)
data_iter = iter(dataloader)
inputs, targets = next(data_iter)
print("Token IDs:\n", inputs)
print("\nInputs shape:\n", inputs.shape)
token_embeddings = token_embedding_layer(inputs)
print(token_embeddings.shape)

其中raw_text为文本内容,最后输出的内容为:

Token IDs:
 tensor([[   40,   367,  2885,  1464],
        [ 1807,  3619,   402,   271],
        [10899,  2138,   257,  7026],
        [15632,   438,  2016,   257],
        [  922,  5891,  1576,   438],
        [  568,   340,   373,   645],
        [ 1049,  5975,   284,   502],
        [  284,  3285,   326,    11]])

Inputs shape:
torch.Size([8, 4])
torch.Size([8, 4, 256])

然后创建位置编码,代码为:

# 创建position embedding
context_length = max_length
pos_embedding_layer = torch.nn.Embedding(context_length, output_dim)
pos_embeddings = pos_embedding_layer(torch.arange(context_length))
print(pos_embeddings.shape)

其中pos_embeddings的输入通常是一个占位向量torch.arange(context_length),它包含一个数字序列0,1,…,最大值的长度 - 1。context_length是一个变量,表示LLM支持的输入大小。

最后将位置编码加到embedding上,代码为:

input_embeddings = token_embeddings + pos_embeddings
print(input_embeddings.shape)

整个输入处理的流程为:
在这里插入图片描述

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部