本章涵盖以下内容:
- 为大语言模型的训练准备文本数据集
- 将文本分割成词和子词token
- 字节对编码(Byte Pair Encoding,BPE):一种更为高级的文本分词技术
- 使用滑动窗口方法采样训练示例
- 将tokens转换为向量,输入到大语言模型中
- 2.1 理解词嵌入
- 2.2 文本分词
- 2.3 将 tokens 转换为token IDs
- 2.4 添加特殊上下文token
- 2.5 字节对编码(Byte pair encoding)
- 2.6 使用滑动窗口进行数据采样
- 2.7 构建词嵌入层
- 2.8 位置编码
- 2.9 本章摘要
2.1 理解词嵌入
个人思考: 不同格式的数据源(如文本、图像、音频、视频)在处理和嵌入时,需要不同的模型和技术,原因在于它们的数据结构、特征和处理方式各不相同,因此需要针对性的方法将这些不同的数据类型转换为适合神经网络处理的向量表示。以下总结了不同数据源在嵌入时的一些区别:
数据类型 | 数据特征 | 嵌入模型 | 主要特征 |
---|---|---|---|
文本 | 离散的、序列化的符号数据 | Word2Vec, GloVe, BERT, GPT 等 | 语义关系、上下文理解 |
图像 | 二维像素网格,具有空间特征 | CNN(ResNet、VGG)、ViT | 形状、纹理、颜色等视觉特征 |
音频 | 一维时序信号 | CNN+频谱图、RNN、Transformer | 频率、音调、时序依赖 |
视频 | 时空序列数据 | 3D CNN、RNN+CNN、Video Transformer | 时空特征、动作捕捉 |
个人思考: 这里聊一下检索增强技术(RAG),目前已经广泛应用于特定领域的知识问答场景。尽管GPT在文本生成任务中表现强大,但它们依赖的是预训练的知识,这意味着它们的回答依赖于模型在预训练阶段学习到的信息。这种方式导致了几个问题:
- 知识的有效性: 模型的知识基于它的预训练数据,因此无法获取最新的信息。比如,GPT-3 的知识截止到 2021 年,无法回答最新的事件或发展。
- 模型大小的限制: 即使是大型模型,所能存储和运用的知识也是有限的。如果任务涉及特定领域(如医学、法律、科学研究),模型在预训练阶段可能没有涵盖足够的信息。
- 生成的准确性: 生成模型可能会凭空编造信息(即“幻觉现象”),导致生成内容不准确或虚假。
而检索增强技术正是为了解决上述不足,它大致原理为将外部知识库(如文档、数据库、互联网等)进行向量化后存入到向量数据库中。当用户提交一个查询时,首先将这个查询也编码成一个向量,然后去承载外部知识库的向量数据库中检索(检索技术有很多种)与问题相关的信息。检索到的信息被作为额外的上下文信息输入到LLM中,LLM会将这些外部信息与原始输入结合起来,以更准确和丰富的内容生成回答。想要进一步了解RAG技术及其应用,可以参考:RAG 专区
2.2 文本分词
# Listing 2.1 Reading in a short story as text sample into Python with open("the-verdict.txt", "r", encoding="utf-8") as f: raw_text = f.read() print("Total number of character:", len(raw_text)) print(raw_text[:99])
Total number of character: 20479 I HAD always thought Jack Gisburn rather a cheap genius--though a good fellow enough--so it was no
样本规模请注意,在处理 LLM 时,通常会处理数百万篇文章和数十万本书——也就是几 GB 的文本。然而,为了教学目的,使用像单本书这样的小文本样本就足够了,这样可以阐明文本处理步骤的主要思想,并能够在消费级硬件上合理地运行。
import re text = "Hello, world. This, is a test." result = re.split(r'(\s)', text) print(result)
['Hello,', ' ', 'world.', ' ', 'This,', ' ', 'is', ' ', 'a', ' ', 'test.']
result = re.split(r'([,.]|\s)', text) print(result)
['Hello', ',', '', ' ', 'world', '.', '', ' ', 'This', ',', '', ' ', 'is', ' ', 'a', ' ', 'test', '.', '']
result = [item for item in result if item.strip()] print(result)
['Hello', ',', 'world', '.', 'This', ',', 'is', 'a', 'test', '.']
关于是否删除空白字符的探讨在开发一个简单的分词器时,是否将空白字符编码为单独的字符,或者直接将其删除,取决于我们的应用和需求。删除空白字符可以减少内存和计算资源的消耗。然而,如果我们训练的模型对文本的确切结构敏感(例如,Python 代码对缩进和空格非常敏感),那么保留空白字符就很有用。在这里,为了简化和缩短分词化输出,我们选择删除空白字符。稍后,我们将切换到一个包含空白字符的分词化方案。
text = "Hello, world. Is this-- a test?" result = re.split(r'([,.:;?_!"()\']|--|\s)', text) result = [item.strip() for item in result if item.strip()] print(result)
['Hello', ',', 'world', '.', 'Is', 'this', '--', 'a', 'test', '?']
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', raw_text) preprocessed = [item.strip() for item in preprocessed if item.strip()] print(len(preprocessed))
['I', 'HAD', 'always', 'thought', 'Jack', 'Gisburn', 'rather', 'a', 'cheap', 'genius', '--', 'though', 'a', 'good', 'fellow', 'enough', '--', 'so', 'it', 'was', 'no', 'great', 'surprise', 'to', 'me', 'to', 'hear', 'that', ',', 'in']
2.3 将 tokens 转换为token IDs
all_words = sorted(set(preprocessed)) vocab_size = len(all_words) print(vocab_size)
vocab = {token:integer for integer,token in enumerate(all_words)} for i, item in enumerate(vocab.items()): print(item) if i > 50: break
('!', 0) ('"', 1) ("'", 2) ... ('Her', 49) ('Hermia', 50)
# Listing 2.3 Implementing a simple text tokenizer class SimpleTokenizerV1: def __init__(self, vocab): self.str_to_int = vocab #A self.int_to_str = {i:s for s,i in vocab.items()} #B def encode(self, text): #C preprocessed = re.split(r'([,.?_!"()\']|--|\s)', text) preprocessed = [item.strip() for item in preprocessed if item.strip()] ids = [self.str_to_int[s] for s in preprocessed] return ids def decode(self, ids): #D text = " ".join([self.int_to_str[i] for i in ids]) text = re.sub(r'\s+([,.?!"()\'])', r'\1', text) #E return text #A 将词汇表作为类属性存储,以方便在 encode 和 decode 方法中访问 #B 创建一个反向词汇表,将token ID 映射回原始的文本token #C 将输入文本转换为token ID #D 将token ID 还原为文本 #E 在指定的标点符号前去掉空格
tokenizer = SimpleTokenizerV1(vocab) text = """"It's the last he painted, you know," Mrs. Gisburn said with pardonable pride.""" ids = tokenizer.encode(text) print(ids)
这里原始英文书籍中没有输出打印结果,读者可以自己运行代码查看结果
):print(tokenizer.decode(ids))
'" It\' s the last he painted, you know," Mrs. Gisburn said with pardonable pride.'
text = "Hello, do you like tea?" print(tokenizer.encode(text))
2.4 添加特殊上下文token
all_tokens = sorted(list(set(preprocessed))) all_tokens.extend(["<|endoftext|>", "<|unk|>"]) vocab = {token:integer for integer,token in enumerate(all_tokens)} print(len(vocab.items()))
for i, item in enumerate(list(vocab.items())[-5:]): print(item)
('younger', 1156) ('your', 1157) ('yourself', 1158) ('<|endoftext|>', 1159) ('<|unk|>', 1160)
# Listing 2.4 A simple text tokenizer that handles unknown words class SimpleTokenizerV2: def __init__(self, vocab): self.str_to_int = vocab self.int_to_str = { i:s for s,i in vocab.items()} def encode(self, text): preprocessed = re.split(r'([,.?_!"()\']|--|\s)', text) preprocessed = [item.strip() for item in preprocessed if item.strip()] preprocessed = [item if item in self.str_to_int #A else "<|unk|>" for item in preprocessed] ids = [self.str_to_int[s] for s in preprocessed] return ids def decode(self, ids): text = " ".join([self.int_to_str[i] for i in ids]) text = re.sub(r'\s+([,.?!"()\'])', r'\1', text) #B return text #A 用 <|unk|> tokens替换未知词汇 #B 在指定标点符号前替换空格
text1 = "Hello, do you like tea?" text2 = "In the sunlit terraces of the palace." text = " <|endoftext|> ".join((text1, text2)) print(text)
'Hello, do you like tea? <|endoftext|> In the sunlit terraces of the palace.'
tokenizer = SimpleTokenizerV2(vocab) print(tokenizer.encode(text))
[1160, 5, 362, 1155, 642, 1000, 10, 1159, 57, 1013, 981, 1009, 738, 1013, 1160, 7]
print(tokenizer.decode(tokenizer.encode(text)))
'<|unk|>, do you like tea? <|endoftext|> In the sunlit terraces of the <|unk|>.'
- [BOS](序列开始):这个token表示文本的起始位置,指示 LLM 内容的开始。
- [EOS](序列结束):这个token位于文本的末尾,在连接多个无关文本时特别有用,类似于 <|endoftext|>。例如,在合并两个不同的维基百科文章或书籍时, [EOS] token指示一篇文章结束和下一篇文章开始。
- [PAD](填充):在使用大于 1 的批量大小数据集训练 LLM 时,批量可能包含不同长度的文本。为了确保所有文本长度一致,较短的文本会用 [PAD] token进行扩展或填充,直到达到批量中最长文本的长度。
个人思考: 在训练神经网络时,通常会将不同长度的句子或文本批处理为一个 batch 进行并行训练。然而,不同长度的句子需要补齐到同一长度(基于矩阵运算要求形状一致),这时就需要填充 token 来对齐所有序列的长度,使得模型能够有效处理不同长度的输入。掩码其实就是一个标志位,用来告诉大模型哪些位置需要关注,哪些可以忽略,例如考虑以下句子:
- 句子1:”I love NLP.”
- 句子 2:”Transformers are powerful.”
- 句子 3:”GPT is amazing.”
为了将它们放入一个批次,我们需要将它们填充到相同的长度。假设最长句子的长度为 5(token 数量),因此每个句子需要填充到 5 个 token。填充时,GPT 使用<|endoftext|>
作为填充标记。在输入批次时,我们为每个 token 位置创建一个掩码矩阵,用来标识哪些位置是有效 token(模型应该关注),哪些是填充 token(模型应该忽略)。假设1
表示有效 token,0
表示填充 token,则掩码矩阵如下:
- 句子1(掩码矩阵):
[1, 1, 1, 1, 0]
- 句子2(掩码矩阵):
[1, 1, 1, 1, 0]
- 句子3(掩码矩阵):
[1, 1, 1, 0, 0]
在这个掩码矩阵中,1
表示模型会关注的 token,0
表示模型会忽略的填充 token。通过这种掩码矩阵,模型知道在计算和训练时哪些 token 是有效内容,哪些 token 是填充部分,无需关注。
2.5 字节对编码(Byte pair encoding)
from importlib.metadata import version import tiktoken print("tiktoken version:", version("tiktoken"))
tokenizer = tiktoken.get_encoding("gpt2")
text = "Hello, do you like tea? <|endoftext|> In the sunlit terraces of someunknownPlace." integers = tokenizer.encode(text, allowed_special={"<|endoftext|>"}) print(integers)
[15496, 11, 466, 345, 588, 8887, 30, 220, 50256, 554, 262, 4252, 18250, 8812, 2114, 286, 617, 34680, 27271, 13]
strings = tokenizer.decode(integers) print(strings)
'Hello, do you like tea? <|endoftext|> In the sunlit terraces of someunknownPlace.'
练习 2.1 未知词的字节对编码尝试使用 tiktoken 库中的 BPE 分词器对未知单词 “Akwirw ier” 进行处理,并输出各个token ID。接着,对此列表中的每个结果整数调用 decode 函数,以重现图 2.11 中的映射。最后,调用token ID 的 decode 方法,检查它是否能够重建原始输入 “Akwirw ier”。
个人思考: 字节对编码是一种基于统计的方法,它会先从整个语料库中找出最常见的字节对(byte pair),然后把这些字节对合并成一个新的单元。让我们用一个具体的示例来描述这个过程:
-
初始化:BPE会先将句子中每个字符视为一个单独的token
['T', 'h', 'e', ' ', 'c', 'a', 't', ' ', 'd', 'r', 'a', 'n', 'k', ' ', 't', 'h', 'e', ' ', 'm', 'i', 'l', 'k', ' ', 'b', 'e', 'c', 'a', 'u', 's', 'e', ' ', 'i', 't', ' ', 'w', 'a', 's', ' ', 'h', 'u', 'n', 'g', 'r', 'y']
-
统计最常见的字节对BPE算法会在这些token中找到出现频率最高的“字节对”(即相邻的两个字符),然后将其合并为一个新的token。例如这里最常见的字节对时(’t’, ‘h’),因为它在单词”the”和”that”中出现频率较高。
-
合并字节对根据统计结果,我们将最常见的字节对(’t’, ‘h’)合并为一个新的token,其它类似
['Th', 'e', ' ', 'c', 'a', 't', ' ', 'dr', 'a', 'nk', ' ', 'th', 'e', ' ', 'm', 'i', 'l', 'k', ' ', 'be', 'c', 'a', 'u', 'se', ' ', 'it', ' ', 'wa', 's', ' ', 'hu', 'n', 'gr', 'y']
-
重复步骤2和3,得到最终的token序列
['The', ' ', 'cat', ' ', 'drank', ' ', 'the', ' ', 'milk', ' ', 'because', ' ', 'it', ' ', 'was', ' ', 'hungry']
2.6 使用滑动窗口进行数据采样
with open("the-verdict.txt", "r", encoding="utf-8") as f: raw_text = f.read() enc_text = tokenizer.encode(raw_text) print(len(enc_text))
enc_sample = enc_text[50:]
context_size = 4 #A x = enc_sample[:context_size] y = enc_sample[1:context_size+1] print(f"x: {x}") print(f"y: {y}") #A 上下文大小决定输入中包含多少个token
x: [290, 4920, 2241, 287] y: [4920, 2241, 287, 257]
for i in range(1, context_size+1): context = enc_sample[:i] desired = enc_sample[i] print(context, "---->", desired)
[290] ----> 4920 [290, 4920] ----> 2241 [290, 4920, 2241] ----> 287 [290, 4920, 2241, 287] ----> 257
for i in range(1, context_size+1): context = enc_sample[:i] desired = enc_sample[i] print(tokenizer.decode(context), "---->", tokenizer.decode([desired]))
and ----> established and established ----> himself and established himself ----> in and established himself in ----> a
# Listing 2.5 A dataset for batched inputs and targets import torch from torch.utils.data import Dataset, DataLoader class GPTDatasetV1(Dataset): def __init__(self, txt, tokenizer, max_length, stride): self.input_ids = [] self.target_ids = [] token_ids = tokenizer.encode(txt) #A for i in range(0, len(token_ids) - max_length, stride): #B 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): #C return len(self.input_ids) def __getitem__(self, idx): #D return self.input_ids[idx], self.target_ids[idx] #A 将整个文本进行分词 #B 使用滑动窗口将书籍分块为最大长度的重叠序列。 #C 返回数据集的总行数 #D 从数据集中返回指定行
# Listing 2.6 A data loader to generate batches with input-with pairs def create_dataloader_v1(txt, batch_size=4, max_length=256, stride=128, shuffle=True, drop_last=True, num_workers=0): tokenizer = tiktoken.get_encoding("gpt2") #A dataset = GPTDatasetV1(txt, tokenizer, max_length, stride) #B dataloader = DataLoader( dataset, batch_size=batch_size, shuffle=shuffle, drop_last=drop_last, #C num_workers=0 #D ) return dataloader #A 初始化分词器 #B 创建GPTDatasetV1类 #C drop_last=True会在最后一批次小于指定的batch_size时丢弃该批次,以防止训练期间的损失峰值 #D 用于预处理的CPU进程数量
with open("the-verdict.txt", "r", encoding="utf-8") as f: raw_text = f.read() dataloader = create_dataloader_v1( raw_text, batch_size=1, max_length=4, stride=1, shuffle=False) data_iter = iter(dataloader) #A first_batch = next(data_iter) print(first_batch) #A 将数据加载器转换为 Python 迭代器,以便通过 Python 的内置 next() 函数获取下一个数据条目。
[tensor([[ 40, 367, 2885, 1464]]), tensor([[ 367, 2885, 1464, 1807]])]
first_batch
变量包含两个张量:第一个张量存储输入token ID,第二个张量存储目标token ID。由于 max_length
设置为 4,因此这两个张量各包含 4 个token ID。请注意,输入大小为 4 相对较小,仅用于演示目的。通常,训练 LLM 的输入大小至少为 256。stride=1
的含义,让我们从这个数据集中提取另一个批次:second_batch = next(data_iter) print(second_batch)
[tensor([[ 367, 2885, 1464, 1807]]), tensor([[2885, 1464, 1807, 3619]])]
练习 2.2 针对数据加载器(Data Loaders)设置不同步幅和上下文大小为了更好地理解数据加载器的工作原理,请尝试使用不同的设置进行测试,例如max_length=2
和stride=2
,以及max_length=8
和stride=2
。
dataloader = create_dataloader_v1(raw_text, batch_size=8, max_length=4, stride=4) data_iter = iter(dataloader) inputs, targets = next(data_iter) print("Inputs:\n", inputs) print("\nTargets:\n", targets) This prints the following: Inputs: 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]]) Targets: tensor([[ 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, 287]])
2.7 构建词嵌入层
个人思考: 上面一段描述说的有些笼统,为什么通过反向传播算法训练的大语言模型必须具有Embedding,让我们通过以下几个方面来分析和思考:
深度神经网络和连续向量表示GPT 类模型(以及其他深度神经网络)是基于大量的矩阵运算和数值计算构建的,尤其是神经元之间的连接权重和偏置在训练过程中不断更新。这些运算要求输入的数据是数值形式的向量,因为神经网络只能对数值数据进行有效计算,而无法直接处理原始的离散文字数据(如单词、句子)。
- **向量表示: **通过将每个单词、句子或段落转换为连续向量(Embedding),可以在高维空间中表示文本的语义关系。例如,通过词嵌入(如 Word2Vec、GloVe)或上下文嵌入(如 GPT 中的词嵌入层),每个单词都被转换为一个向量,这个向量可以用于神经网络的计算。
向量嵌入的作用连续向量表示不仅让文本数据可以进入神经网络,还帮助模型捕捉和表示文本之间的语义关系。例如:
- 同义词或相似词:在向量空间中,相似的单词可以有接近的向量表示。这种语义相似性帮助模型理解上下文,并在生成文本时提供参考。
- 上下文关系:GPT 等 LLM 模型不仅依赖单词级别的向量表示,还会考虑句子或段落上下文,形成动态嵌入,从而生成更具连贯性的文本。
反向传播算法的要求深度神经网络通过反向传播算法进行训练,反向传播的本质是利用梯度下降法来更新网络的权重,以最小化损失函数(loss function)。反向传播要求每一层的输入、输出和权重都能够参与梯度计算,而梯度计算只能应用于数值数据。
- 自动微分与梯度计算:在反向传播中,神经网络会根据损失函数的导数来计算梯度,这个过程依赖于自动微分(automatic differentiation)。为了计算每层的梯度,输入的数据必须是数值形式(即向量),否则无法对离散的文本数据求导。
- 梯度更新权重:每次更新网络权重时,神经网络会根据每一层的输入和输出来调整权重,以更好地学习数据的模式。如果输入不是数值形式,就无法实现梯度更新,从而无法通过反向传播训练网络。
input_ids = torch.tensor([2, 3, 5, 1])
vocab_size = 6 output_dim = 3
vocab_size
和 output_dim
在 PyTorch 中实例化一个嵌入层,并将随机种子设置为 123,以便结果可重复:torch.manual_seed(123) embedding_layer = torch.nn.Embedding(vocab_size, output_dim) print(embedding_layer.weight)
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)
print(embedding_layer(torch.tensor([3])))
tensor([[-0.4015, 0.9666, -1.1481]], grad_fn=<EmbeddingBackward0>)
嵌入层与矩阵乘法对于那些熟悉独热编码的人来说,上述嵌入层方法本质上只是实现独热编码后再进行矩阵乘法的一种更高效的方式,相关内容在 GitHub 的补充代码中进行了说明,链接为https://github.com/rasbt/LLM-from-scratch/tree/main/ch02/03_bonus_embedding-vs-matmul。由于嵌入层只是独热编码和矩阵乘法方法的更高效实现,因此可以视为一个可以通过反向传播进行优化的神经网络层。
torch.tensor([2, 3, 5, 1])
):print(embedding_layer(input_ids))
tensor([[ 1.2753, -0.2010, -0.1606], [-0.4015, 0.9666, -1.1481], [-2.8400, -0.7849, -1.4096], [ 0.9178, 1.5810, 1.3010]], grad_fn=<EmbeddingBackward0>)
2.8 位置编码
vocab_size = 50257 output_dim = 256 token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
token_embedding_layer
,如果我们从数据加载器中采样数据,我们将每个批次中的每个token嵌入到一个 256 维的向量中。如果我们的批次大小为 8,每个批次有四个token,那么结果将是一个形状为 8 x 4 x 256 的张量。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 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])
token_embeddings = token_embedding_layer(inputs) print(token_embeddings.shape)
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)
input_embeddings = token_embeddings + pos_embeddings print(input_embeddings.shape)
2.9 本章摘要
- LLM 需要将文本数据转换为数值向量,这称之为嵌入,因为它们无法处理原始文本。嵌入将离散数据(如单词或图像)转化为连续的向量空间,从而使其能够与神经网络操作兼容。
- 作为第一步,原始文本被分解为token,这些token可以是单词或字符。然后,这些token被转换为整数表示,称为token ID。
- 可以添加特殊token,如 <|unk|> 和 <|endoftext|>,以增强模型的理解能力,并处理各种上下文,例如未知单词或无关文本之间的边界分隔。
- 用于像 GPT-2 和 GPT-3 这样的 LLM 的字节对编码(BPE)分词器,可以通过将未知单词分解为子词单元或单个字符,高效地处理这些单词。
- 我们在分词后的文本数据上采用滑动窗口方法,以生成用于 LLM 训练的输入-目标对。
- 在 PyTorch 中,嵌入层作为一种查找操作,用于检索与token ID 对应的向量。生成的嵌入向量提供了token的连续表示,这在训练像 LLM 这样的深度学习模型时至关重要。
- 虽然token嵌入为每个token提供了一致的向量表示,但它们并没有考虑token在序列中的位置。为了解决这个问题,存在两种主要类型的位置嵌入:绝对位置嵌入和相对位置嵌入。OpenAI 的 GPT 模型采用绝对位置嵌入,这些位置嵌入向量会与token嵌入向量相加,并在模型训练过程中进行优化。