词元嵌入的缺点
由 大模型原理:编码单词位置信息 可知,位置信息是必须要有的,但 大模型原理: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)
