大模型原理:embedding词元嵌入2-加入位置信息

词元嵌入的缺点

由 大模型原理:编码单词位置信息 可知,位置信息是必须要有的,但 大模型原理:embedding词元嵌入 中的实现方式,没有位置信息

可以采用两种位置信息嵌入策略:

  • 绝对位置嵌入
  • 相对位置嵌入

绝对位置嵌入

绝对位置嵌入(absolute positional embedding),直接与序列中的 特定位置 相关联。对于输入序列的每个位置,该方法都会向对应词元的嵌入向量中添加一个独特的位置嵌入,以明确指示其在序列中的确切位置。

例子

序列中的第一个词元会有 一个特定的位置嵌入,第二个词元则会有 另一个不同的位置嵌入,以此类推,如图:

上图中,位置嵌入被添加到 词元嵌入向量中,从而生成大语言模型的 输入嵌入。位置向量与原始词元嵌入的维度相同。

提示:为简单起见,词元嵌入的值均设置为 1

相对位置嵌入(更好)

相对位置嵌入(relative positional embedding),关注的是 词元之间的相对位置或距离,而非它们的绝对位置。模型学习的是 词元之间的“距离”关系,而不是它们在序列中的“具体位置”。这种方法使得模型能够 更好地适应不同长度(包括在训练过程中从未见过的长度)的序列。

选择位置嵌入

以上两种位置嵌入都旨在提升大语言模型对 词元顺序 及其 相互关系的理解能力,从而实现更准确、更具上下文感知力的预测。选择使用哪种嵌入策略,通常取决于具体的应用场景和数据特性。

OpenAI的 GPT模型 使用的是 绝对位置嵌入,这些嵌入会在训练过程中被优化,有别于原始Transformer模型中的固定或预定义位置编码。这种优化是模型训练的一部分

例子

本例中,创建 256 维的嵌入向量层,表示输入的词元编码。使用BPE分词器(词汇量为 50257)

提示:这个维度比原始GPT-3模型的维度(GPT-3模型的嵌入维度为 12 288)要小

准备

从 大模型原理:通用代码合集 复制下列部分:

  • 导入模块
  • GPTDatasetV1 类
  • create_dataloader_v1() 函数

读取文本并编码

一次读取8批,每批读取4个词元(滑动窗口大小是4)

# 读取文本
with open("the-verdict.txt", "r", encoding="utf-8") as f:
    raw_text = f.read()

# 批次是8
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("\n输入词元ID:\n", inputs)  # 打印如输入词元ID,一次读取8批,每批读取4个词
print("\n输入词元ID shape:\n", inputs.shape)  # 8批4个词,所以是8行4列

执行结果:

输入词元ID:
 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]])

输入词元ID shape:
 torch.Size([8, 4])

通过执行结果可知:词元ID张量的维度为 8×4(8行4列),一次读取8批,包含8个文本样本,每个样本由4个词元组成

创建嵌入层

  • 创建 256 维的嵌入向量层,表示输入的词元编码
  • 词汇量为 50257,使用BPE分词器
# 词汇表数量
vocab_size = 50257
# 维度
output_dim = 256

# 设置随机种子
# torch.manual_seed(123)
# 创建嵌入层
token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)  # [50257,256]
print("\n初始嵌入层权重:\n", token_embedding_layer.weight)

执行结果:

初始嵌入层权重:
 Parameter containing:
tensor([[ 0.1181, -0.6761,  0.4792,  ...,  0.0184, -0.1651,  0.5454],
        [-0.2018,  0.1588,  0.3712,  ...,  0.8369,  0.3870,  1.5421],
        [-0.1476, -2.0082,  0.1505,  ...,  0.3379, -0.0102,  0.0197],
        ...,
        [-1.0388, -1.1792,  0.2926,  ..., -0.5563,  0.9102,  0.3377],
        [-0.6926, -0.5396,  0.2561,  ..., -1.5883, -0.7262,  1.6189],
        [ 0.7106, -1.4136,  0.3272,  ...,  0.3205, -1.5987, -0.8900]],
       requires_grad=True)

将词元ID嵌入256维的向量中

# 使用嵌入层将这些词元ID嵌入256维的向量中
token_embeddings = token_embedding_layer(inputs)
print("\n词元ID嵌入向量 shape:\n", token_embeddings.shape)  # 8批,4个词,每个词256维,所以是[8, 4, 256]

执行结果:

词元ID嵌入向量 shape:
 torch.Size([8, 4, 256])

可知,该张量的维度为 8×4×256,即:每个词元ID都已被嵌入一个256维的向量中

创建绝对位置嵌入层

为了获取GPT模型所采用的绝对位置嵌入,只需创建一个维度与 token_embedding_layer 相同的嵌入层即可

# 创建绝对位置嵌入层,也就是创建一个维度与token_embedding_layer相同的嵌入层即可
context_length = max_length
pos_embedding_layer = torch.nn.Embedding(context_length, output_dim)  # [4,256]
print("\n位置嵌入层权重:\n", pos_embedding_layer.weight)
print("\ntorch.arange(context_length)执行结果:\n", torch.arange(context_length))
pos_embeddings = pos_embedding_layer(torch.arange(context_length))
print("\n位置向量 shape\n", pos_embeddings.shape)  # 4行,256列

执行结果:

位置嵌入层权重:
 Parameter containing:
tensor([[-0.3511,  0.5899,  1.1541,  ...,  0.0353,  0.0582,  0.2973],
        [-0.6929,  1.2882,  0.9555,  ...,  0.7818, -0.3587,  1.3432],
        [-1.0396,  0.7830,  0.1704,  ..., -0.6021,  0.3498, -0.8344],
        [ 1.1285, -1.8034,  0.1672,  ...,  0.9331, -0.8382, -0.0715]],
       requires_grad=True)

torch.arange(context_length)执行结果:
 tensor([0, 1, 2, 3])

位置向量 shape
 torch.Size([4, 256])
tensor([0, 1, 2, 3])

pos_embeddings 的输入通常是一个占位符向量 torch.arange(context_length),它包含一个从 0 开始递增,直至最大输入长度减1的数值序列。
context_length 是一个变量,表示模型支持的输入块的 最大长度。我们将其设置为与 输入文本的最大长度一致。在实际情况中,输入文本的长度可能会超出模型支持的块大小,这时需要截断文本

词元嵌入张量 加上 位置嵌入张量

将这些向量直接添加到词元嵌入中:PyTorch会在每个批次中的每个 4×256 维的词元嵌入张量上都添加一个 4×256 维的 pos_embeddings 张量

'''
将这些向量直接添加到词元嵌入中
PyTorch会在每个批次中的每个4×256维的词元嵌入张量上都添加一个4×256维的pos_embeddings张量
'''
input_embeddings = token_embeddings + pos_embeddings
print("\n词元嵌入向量+位置嵌入向量:\n", input_embeddings.shape)

执行结果:

torch.Size([8, 4, 256])

完整代码

from importlib.metadata import version
import tiktoken
import torch
from torch.utils.data import Dataset, DataLoader

print("查看 version 版本号:", version("tiktoken"))
print("查看所有支持的编码:", tiktoken.list_encoding_names())


class GPTDatasetV1(Dataset):
    """
    GPTDatasetV1类继承自PyTorch的Dataset类,并定义了如何从数据集中提取单行数据。
    每行数据包含多个词元ID(数量由max_length参数决定),这些词元ID被分配给input_chunk张量,
    而target_chunk张量包含相应的目标词元ID。
    """

    def __init__(self, txt, tokenizer, max_length, stride):
        """
        初始化
        :param txt: 文本
        :param tokenizer: 分词器
        :param max_length: 窗口大小
        :param stride: 步长
        """
        self.input_ids = []
        self.target_ids = []
        # 对全部文本进行分词
        token_ids = tokenizer.encode(txt)
        # 使用滑动窗口将文本划分为长度为max_length的重叠序列
        for i in range(0, len(token_ids) - max_length, stride):
            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:
        """
        return len(self.input_ids)

    def __getitem__(self, idx):
        """
        返回指定索引的数据,必须实现,否则传入数据加载器报错
        :param idx:
        :return:
        """
        return self.input_ids[idx], self.target_ids[idx]


def create_dataloader_v1(txt, batch_size=4, max_length=256, stride=128, shuffle=True, drop_last=True, num_workers=0):
    """
    数据加载器,用于批量生成输入-目标对
    :param txt:
    :param batch_size:
    :param max_length:
    :param stride:
    :param shuffle:
    :param drop_last:
    :param num_workers:
    :return:
    """
    # 初始化分词器,选择对应模型的编码,不同模型编码规则不同
    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,  # 如果drop_last为True且批次大小小于指定的batch_size,则会删除最后一批,以防止在训练期间出现损失剧增
        num_workers=num_workers  # 用于预处理的CPU进程数
    )
    return dataloader


# 读取文本
with open("the-verdict.txt", "r", encoding="utf-8") as f:
    raw_text = f.read()

# 批次是8
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("\n输入词元ID:\n", inputs)  # 打印如输入词元ID,一次读取8批,每批读取4个词
print("\n输入词元ID shape:\n", inputs.shape)  # 8批4个词,所以是8行4列

# 词汇表数量
vocab_size = 50257
# 维度
output_dim = 256

# 设置随机种子
# torch.manual_seed(123)
# 创建嵌入层
token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)  # [50257,256]
print("\n初始嵌入层权重:\n", token_embedding_layer.weight)

# 使用嵌入层将这些词元ID嵌入256维的向量中
token_embeddings = token_embedding_layer(inputs)
print("\n词元ID嵌入向量 shape:\n", token_embeddings.shape)  # 8批,4个词,每个词256维,所以是[8, 4, 256]

# 创建绝对位置嵌入层,也就是创建一个维度与token_embedding_layer相同的嵌入层即可
context_length = max_length
pos_embedding_layer = torch.nn.Embedding(context_length, output_dim)  # [4,256]
print("\n位置嵌入层权重:\n", pos_embedding_layer.weight)
print("\ntorch.arange(context_length)执行结果:\n", torch.arange(context_length))
pos_embeddings = pos_embedding_layer(torch.arange(context_length))
print("\n位置向量 shape\n", pos_embeddings.shape)  # 4行,256列

'''
将这些向量直接添加到词元嵌入中
PyTorch会在每个批次中的每个4×256维的词元嵌入张量上都添加一个4×256维的pos_embeddings张量
'''
input_embeddings = token_embeddings + pos_embeddings
print("\n词元嵌入向量+位置嵌入向量:\n", input_embeddings.shape)

原文出处:/show_1GW2YuupnGNY.html