大模型原理:embedding词元嵌入2-加入位置信息 作者:马育民 • 2026-01-09 18:04 • 阅读:10003 # 词元嵌入的缺点 由 [大模型原理:编码单词位置信息](https://www.malaoshi.top/show_1GW2YsuvK4X0.html "大模型原理:编码单词位置信息") 可知,位置信息是必须要有的,但 [大模型原理:embedding词元嵌入](https://www.malaoshi.top/show_1GW2YriiPo97.html "大模型原理:embedding词元嵌入") 中的实现方式,没有位置信息 可以采用两种位置信息嵌入策略: - 绝对位置嵌入 - 相对位置嵌入 # 绝对位置嵌入 绝对位置嵌入(absolute positional embedding),直接与序列中的 **特定位置 相关联**。对于输入序列的每个位置,该方法都会向对应词元的嵌入向量中添加一个独特的位置嵌入,以明确指示其在序列中的确切位置。 ### 例子 序列中的第一个词元会有 **一个特定的位置嵌入**,第二个词元则会有 **另一个不同的位置嵌入**,以此类推,如图: [](https://www.malaoshi.top/upload/0/0/1GW2YtQB90KE.png) 上图中,**位置嵌入**被添加到 **词元嵌入向量中**,从而生成大语言模型的 **输入嵌入**。位置向量与原始词元嵌入的维度相同。 **提示:**为简单起见,词元嵌入的值均设置为 `1` # 相对位置嵌入(更好) 相对位置嵌入(relative positional embedding),关注的是 **词元之间的相对位置或距离**,而非它们的绝对位置。模型学习的是 **词元之间的“距离”关系**,而不是它们在序列中的“具体位置”。这种方法使得模型能够 **更好地适应不同长度**(包括在训练过程中从未见过的长度)的序列。 # 选择位置嵌入 以上两种位置嵌入都旨在提升大语言模型对 **词元顺序** 及其 **相互关系的理解能力**,从而实现更准确、更具上下文感知力的预测。选择使用哪种嵌入策略,通常取决于具体的应用场景和数据特性。 OpenAI的 **GPT模型** 使用的是 **绝对位置嵌入**,这些嵌入会在训练过程中被优化,有别于原始Transformer模型中的固定或预定义位置编码。这种优化是模型训练的一部分 # 例子 本例中,创建 `256` 维的嵌入向量层,表示输入的词元编码。使用BPE分词器(词汇量为 `50257`) **提示:**这个维度比原始GPT-3模型的维度(GPT-3模型的嵌入维度为 `12 288`)要小 ### 基础代码 - 导入模块 - 之前定义的 `GPTDatasetV1` 类 - 之前定义的 `create_dataloader_v1()` 函数,使用BPE分词器 ``` 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 ``` ### 读取文本并编码 一次读取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) ``` 原文出处:http://malaoshi.top/show_1GW2YuupnGNY.html