Working with Text Data
- 本章内容包罗
- 准备文本以举行大语言模子练习
- 将文本分割为单词和子词标记
- 字节对编码(BPE)作为一种更高级的文本分词方法
- 利用滑动窗口方法采样练习样本
- 将标记转换为向量,以输入到大语言模子中
Understanding word embeddings
- 深度神经网络模子,包括LLMs,无法直接处理原始文本,我们需要一种将单词表示为连续值向量的方法,将数据转换为矢量格式的概念通常称为嵌入
- 通过不同的嵌入模子处理各种不同的数据格式
- embedding核心:从离散对象(例如单词、图像甚至整个文档)到连续向量空间中的点的映射。
- embedding目标:非数字数据转换为神经网络可以识别的格式。
- 早期且最流行的示例之一是 Word2Vec 方法,主要思想是,出现在相似上下文中的单词往往具有相似的含义。
- 固然我们可以利用预练习模子如Word2Vec来为机器学习模子天生嵌入,但LLMs通常天生本身的嵌入,这些嵌入是输入层的一部分,并在练习过程中更新。将嵌入优化作为LLM练习的一部分,而不是利用Word2Vec的上风在于,嵌入是为当前特定的任务和数据量身定制的
- GPT-2(117M 和 125M 参数)embedding维度:768,GPT-3(175B 参数)embedding维度 1228
Tokenizing text
- 我们对文本举行分词,即将文本分解成更小的单位,如单个单词和标点符号。
- 我们将把文本转换为token ID 并创建token embedding。
删除空格可节省内存与计算资源,但在需准确处理文本结构的模子(如Python代码)中,保留空格则显得尤为重要。为简化标记化输出,本文选择去除空格。
- from importlib.metadata import version
- import os
- import urllib.request
- import re
- print("torch version:", version("torch"))
- print("tiktoken version:", version("tiktoken"))
- 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])
- # tokenizer text
- preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', raw_text)
- preprocessed = [item.strip() for item in preprocessed if item.strip()]
- print(preprocessed[:30])
- print(len(preprocessed))
复制代码
Converting tokens into token IDS
- 为了将之前天生的 token 映射到 token ID,我们必须首先构建一个所谓的词汇表。该词汇表界说了我们怎样将每个唯一单词和特殊字符映射到唯一整数。
构建词汇表的步骤:
- 将练习数据会合的文本标记为单独的标记。
- 按字母次序排序并删除重复标记。
- 将唯一标记映射到唯一整数值,形成词汇表。
注:示例词汇表较小,未包罗标点符号或特殊字符。
- """Creating a vocabulary"""
- # 所有的词汇表
- all_words = sorted(set(preprocessed))
- vocab_size = len(all_words)
- print(vocab_size) # 1130,则词汇表大小为 1130
- vocab = {token:integer for integer,token in enumerate(all_words)}
- # 查看前 50个
- for i, item in enumerate(vocab.items()):
- print(item)
- if i >= 50:
- break
复制代码 vocab字典包罗与唯一整数标签关联的各个token,下一个目标就是应用这个词汇表将新文本转换为token ID。创建词汇表的逆版本,将标记 ID 映射回相应的文本标记。
- 实现一个简单的tokenizer类
- encode 函数将文本转换为token ID。
- decode 函数将token ID 转换回文本。
可以通过现有词汇表实例化新的分词器对象,对文本举行编码和解码
- class SimpleTokenizerV1:
- 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()
- ]
- 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])
- # Replace spaces before the specified punctuations
- text = re.sub(r'\s+([,.?!"()\'])', r'\1', text)
- return text
- #实例化
- tokenizer = SimpleTokenizerV1(vocab)
- text = """"It's the last he painted, you know,"
- Mrs. Gisburn said with pardonable pride."""
- # encode
- ids = tokenizer.encode(text)
- print(ids)
- # decode
- print(tokenizer.decode(ids))
复制代码
- 美中不足:对未包罗在词汇表中的词汇举行encode会报错
Adding special context tokens
- 为未知词和文本结尾添加一些“特殊”标记很有用。
如果遇到不属于词汇表的单词,我们可以修改分词器以利用 <|unk|> 标记。在处理多个独立文本源时,我们在这些文本之间添加 <|endoftext|> 标记。这些 <|endoftext|> 标记作为标记,指示特定段落的开始或竣事,使 LLM 可以或许更有用地处理和明白。
- 通过查看decoded_text和原始输入文本举行表叫,我们知道当前的练习集数据不包罗“Hello”和“palace”一词。
- 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
- 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])
- # Replace spaces before the specified punctuations
- text = re.sub(r'\s+([,.:;?!"()\'])', r'\1', text)
- return text
- all_tokens = sorted(list(set(preprocessed)))
- all_tokens.extend(["<|endoftext|>", "<|unk|>"]) #
- vocab = {token:integer for integer,token in enumerate(all_tokens)}
- tokenizer = SimpleTokenizerV2(vocab)
- text1 = "Hello, do you like tea haha?"
- text2 = "In the sunlit terraces of the palace."
- text = " <|endoftext|> ".join((text1, text2))
- print(f"merged text: {text}")
- """输出"""
- merged text: Hello, do you like tea haha? <|endoftext|> In the sunlit terraces of the palace.
- encoded text:[1131, 5, 355, 1126, 628, 975, 1131, 10, 1130, 55, 988, 956, 984, 722, 988, 1131, 7]
- decodec text:<|unk|>, do you like tea <|unk|>? <|endoftext|> In the sunlit terraces of the <|unk|>.
复制代码 - 到目前为止,我们已经讨论了分词作为将文本处理为 LLM 输入的关键步骤。根据 LLM 的不同,一些研究人员还思量了以下额外的特殊标记:
- [BOS](序列开始):标记文本出发点,指示 LLM 内容的开始。
- [EOS](序列竣事):位于文本末尾,用于连接多个不相关文本,指示段落竣事。
- [PAD](填充):在批量练习中,用于将较短文本填充至与最长文本类似的长度。
- 留意:
GPT模子的分词器不利用上述提到的标记,仅利用**<|endoftext|>标记,类似于[EOS],也用于填充。在批量输入练习时,通常利用掩码忽略填充标记,因此详细填充标记的选择无关紧急。此外,GPT模子不利用<|unk|>**标记表示词汇表外单词,而是采用字节对编码(BPE)分词器,将单词分解为子词单位,详见下一节。
Byte pair encoding(BPE)
- 前面实现了一个简单的 tokenization, 本节将介绍一种更复杂的分词方案,基于一种称为字节对编码(Byte Pair Encoding, BPE)的概念。本节中讨论的BPE分词器被用于练习诸如GPT-2、GPT-3以及ChatGPT原始模子等大型语言模子。由实现较为复杂,所以利用了tiktoken 库。
- import importlib
- import 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("intergers",integers)
- string = tokenizer.decode(integers)
- print("string,",string)
- """输出"""
- intergers [15496, 11, 466, 345, 588, 8887, 30, 220, 50256, 554, 262, 4252, 18250, 8812, 2114, 1659, 617, 34680, 27271, 13]
- string, Hello, do you like tea? <|endoftext|> In the sunlit terracesof someunknownPlace.
复制代码 根据上面的 tokenid 和解码文本,可做出两个观察:
- <|endoftext|>标记的ID较大:<|endoftext|>标记被分配了一个较大的标记ID,即50256。实际上,用于练习GPT-2、GPT-3以及ChatGPT原始模子的BPE分词器,其词汇表总巨细为50,257,其中<|endoftext|>被分配了最大的标记ID。
- BPE分词器处理未知词的能力:BPE分词器可以或许精确编码和解码未知词,例如"someunknownPlace"。它可以或许处理任何未知词,而不需要利用<|unk|>标记。
- BPE算法的核心在于将不在预界说词汇表中的单词分解为更小的子词单位,甚至单个字符,从而使其可以或许处理词汇表外的单词。因此,得益于BPE算法,如果分词器在分词过程中遇到不熟悉的单词,它可以将其表示为一系列子词标记或字符。
- import tiktoken
- tokenizer = tiktoken.get_encoding("gpt2")
- text = "Akwirw ier"
- ids = tokenizer.encode(text)
- print(ids)
- for i in ids: # 对该列表中的每个结果整数调用解码函数
- print(i,'\t',tokenizer.decode([i]))
-
- """输出"""
- [33901, 86, 343, 86, 220, 959]
- 33901 Ak
- 86 w
- 343 ir
- 86 w
- 220
- 959 ier
复制代码 - BPE算法的详细讨论和实现超出了本书的范围,但简而言之,它通过迭代地将频仍出现的字符合并为子词,并将频仍的子词合并为单词来构建词汇表。例如,BPE首先将全部单个字符(如"a"、“b"等)添加到其词汇表中。在下一阶段,它将经常一起出现的字符组合合并为子词。例如,“d"和"e"可能会被合并为子词"de”,这在很多英语单词中很常见,如"define”、“depend”、“made"和"hidden”。合并操纵由频率阈值决定。
Data sampling with a sliding window
- 我们练习大型语言模子(LLMs)以逐词天生文本,因此我们需要相应地准备练习数据,使得序列中的下一个词成为预测的目标:(留意该图中表现的文本将在 LLM 处理之前举行tokenize;然而,为了清楚起见,该图省略了 tokenize步骤)
- import tiktoken
- tokenizer = tiktoken.get_encoding("gpt2")
- 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))
- #选择前 50个演示
- enc_sample = enc_text[50:]
- context_size = 4 #A
- # 创建下一个单词预测任务
- for i in range(1, context_size+1):
- context = enc_sample[:i]
- desired = enc_sample[i]
- print(context, "---->", desired)
-
- # 对应的文本
- 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吸取的输入,而箭头右侧的标记ID则代表LLM应预测的目标token ID。
- 在我们将标记转换为嵌入之前,只剩下一个任务:实现一个高效的数据加载器,它迭代输入数据集并将输入和目标作为 PyTorch 张量返回,这可以被认为是作为多维数组。
为了实现高效的数据加载器,我们收集张量 x 中的输入,其中每一行代表一个输入上下文。第二个张量 y 包罗相应的预测目标(下一个单词),这些目标是通过将输入移动一个位置来创建的。(文本只做演示,实际 BPE 将会转化为 tokenID)

- from torch.utils.data import Dataset, DataLoader
- import tiktoken
- import torch
- class GPTDatasetV1(Dataset):
- def __init__(self, txt, tokenizer, max_length, stride):
- self.input_ids = []
- self.target_ids = []
- # Tokenize the entire text
- token_ids = tokenizer.encode(txt, allowed_special={"<|endoftext|>"})
- # Use a sliding window to chunk the book into overlapping sequences of 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 len(self.input_ids)
- def __getitem__(self, idx):
- 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):
- # Initialize the tokenizer
- tokenizer = tiktoken.get_encoding("gpt2")
- # Create dataset
- dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
- # Create dataloader
- dataloader = DataLoader(
- dataset,
- batch_size=batch_size,
- shuffle=shuffle,
- drop_last=drop_last,
- num_workers=num_workers
- )
- return dataloader
- 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)
- first_batch = next(data_iter)
- print("first_batch", first_batch)
- second_batch = next(data_iter)
- print("second_batch", second_batch)
- """输出"""
- first_batch [tensor([[ 40, 367, 2885, 1464]]), tensor([[ 367, 2885, 1464, 1807]])]
- second_batch [tensor([[ 367, 2885, 1464, 1807]]), tensor([[2885, 1464, 1807, 3619]])]
复制代码
- first_batch 变量包罗两个张量:第一个张量存储输入 token ID,第二个张量存储目标 tokenID.(max_length 设置为 4仅用于演示,通常利用至少设为 256)
- stride=1,则第二个批次的 token ID 与第一个批次相比移动了一个位置,模仿滑动窗口位置。
下图是 stride = 1 和 stride=4 的不同上下文巨细的数据加载器
- 较小的批量巨细在练习期间需要的内存较少,但会导致模子更新更加噪声化。与通例深度学习一样,批量巨细是一个需要衡量的超参数,在练习大型语言模子时需要举行实验。下图时 batch 大于 1 举行采样,并将 strides 增大到 4,这是为了充实利用数据集(我们不跳过单个单词),但也避免批次之间的任何重叠,因为更多的重叠可能会导致过分拟合增加。
- dataloader = create_dataloader_v1(raw_text, batch_size=8, max_length=4, stride=4, shuffle=False)
- data_iter = iter(dataloader)
- inputs, targets = next(data_iter)
- print("Inputs:\n", inputs)
- print("\nTargets:\n", targets)
复制代码
Creating token embeddings
- 数据已经险些准备好供大型语言模子(LLM)利用,但最后让我们利用嵌入层将这些标记嵌入到连续的向量表示中,通常这些嵌入层是LLM本身的一部分,并在模子练习期间举行更新(练习)
- 为了简单起见,假设我们有一个只有6个单词的小词汇表,而且我们盼望创建巨细为3的嵌入(GPT-3的 BPEtokenizer 中有 257 单词,embedding 维度 12288 维度),这会有 6*3 的embedding权重矩阵,embedding层的权重矩阵包罗小的随机值。这些值在 LLM 练习期间作为 LLM 优化本身的一部分举行优化。
- import torch
- import tiktoken
- input_ids = torch.tensor([2, 3, 5, 1])
- #为了简单起见,假设我们有一个只有6个单词的小词汇表,并且我们希望创建大小为3的嵌入
- vocab_size = 6
- output_dim = 3
- 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)
复制代码 我们可以看到权重矩阵有六行三列。每行对应词汇表中的六个可能的标记之一,每列对应三个嵌入维度之一。
实例化嵌入层后,现在将其应用于tokend ID 以得到嵌入向量
- print(embedding_layer(torch.tensor([3])))
- """输出"""
- tensor([[-0.4015, 0.9666, -1.1481]], grad_fn=<EmbeddingBackward0>)
复制代码 如果我们比力tokenID为3的embedding向量与之前的 embedding矩阵,我们会发现它与第4行完全类似(Python从零开始索引,因此它对应于索引3的行)。换句话说,embedding层本质上是一种查找操纵,它通过 tokenID从 embedding层的权重矩阵中检索行。
现在应用到全部的四个 tokenID,你会发现 tokenid 为 [2, 3, 5, 1]的分别对应 embedding 层权重矩阵的第[3,4,6,2]行。
- input_ids = 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>)
复制代码
Encoding word positions
- tokenID embedding原则上是得当大型语言模子(LLM)的输入。然而,LLM的一个小缺点是,其自留意力机制对序列中 tokening的位置或次序没有概念,embedding层将 token ID转换为类似的向量表示,而不管它们在输入序列中的位置怎样:
原则上,令牌ID的确定性和位置无关嵌入有利于可再现性。然而,由于大型语言模子的自留意力机制本身是位置无关的,因此注入位置信息是有益的。位置感知嵌入分为两类:相对位置嵌入和绝对位置嵌入。绝对位置嵌入为序列中每个位置分配唯一嵌入,表示其确切位置。例如,第一个标记有特定位置嵌入,第二个标记有不同嵌入,依此类推。将位置嵌入与令牌嵌入结合,形成模子的输入嵌入。
而相对位置嵌入关注标记间的相对距离而非绝对位置,有助于模子泛化到不同长度的序列。绝对位置嵌入则为每个位置分配唯一嵌入,强调标记的绝对位置。两者均旨在增强模子对标记次序和关系的明白,确保更准确和上下文感知的预测,选择取决于详细应用和数据特性。OpenAI 的 GPT 模子采用优化后的绝对位置嵌入,而不是像原始 Transformer 模子中的位置编码那样固定或预界说,该优化过程是模子练习的一部分。
- 之前处于阐明的目标,重点讨论非常小的 embedding 巨细。BytePair 编码器的词汇表巨细为 50,257,现在假设我们需要将输入令牌编码为 256 维的向量表示。
- vocab_size = 50257
- output_dim = 256
- 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("Inputs shape:\n", inputs.shape)
- """输出"""
- 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 ID 张量是 8x4 维的,这意味着数据每个批次由 8 个文本样本组成,每个样本有 4 个 token。现在让我们利用嵌入层将这些令牌 ID 嵌入到 256 维向量中。
- token_embeddings = token_embedding_layer(inputs)
- print(token_embeddings.shape)
- """输出"""
- torch.Size([8, 4, 256])
复制代码 每个 token ID 现在都嵌入为 256 维向量。
- GPT-2 利用绝对位置嵌入,因此我们只需创建另一个嵌入层
- pos_embeddings = pos_embedding_layer(torch.arange(max_length))
- print(pos_embeddings.shape)
- """输出"""
- torch.Size([4, 256])
复制代码 pos_embeddings 的输入通常是占位符向量 torch.arange(context_length),其中包罗数字 0、1、… 的序列,直到最大输入长度 − 1。 context_length是一个变量,表示 LLM 支持的输入巨细。这里,我们选择它类似于输入文本的最大长度。在实践中,输入文本可能比支持的上下文长度长,在这种情况下我们必须截断文本。
要创建大型语言模子(LLM)中利用的输入嵌入,我们只需将令牌嵌入和位置嵌入相加:
- input_embeddings = token_embeddings + pos_embeddings
- print(input_embeddings.shape)
- """输出"""
- torch.Size([8, 4, 256])
复制代码 我们可以看到,位置嵌入张量由四个 256 维向量组成。我们现在可以将它们直接添加到令牌嵌入中,其中 PyTorch 会将 4x256 维 pos_embeddings 张量添加到 8 个批次中每个批次中的每个 4x256 维令牌嵌入张量。
- 如下图所示,在输入处理流程的初始阶段,输入文本被分割为独立的令牌。分割后,这些令牌根据预界说的词汇表转换为令牌 ID。
Summary
- 总结
- 文本数据转换为数值向量:LLMs 需要将文本数据转换为数值向量,称为 embeddings,因为它们无法处理原始文本。Embeddings 将离散数据(如单词或图像)转换为连续向量空间,使其与神经网络操纵兼容。
- 原始文本分解为 tokens:作为第一步,原始文本被分解为 tokens,这些 tokens 可以是单词或字符。然后,这些 tokens 被转换为整数表示,称为 token IDs。
- 特殊 tokens 的添加:可以添加特殊 tokens,如 <|unk|> 和 <|endoftext|>,以增强模子的明白并处理各种上下文,例如未知单词或标记不相关文本之间的边界。
- BPE tokenizer 的处理能力:用于 GPT-2 和 GPT-3 等 LLMs 的 byte pair encoding (BPE) tokenizer 可以通过将未知单词分解为子词单位或单个字符来高效处理未知单词。
- 滑动窗口方法天生练习数据:我们在 tokenized 数据上利用滑动窗口方法来天生 LLM 练习的输入-目标对。
- PyTorch 中的 Embedding 层:PyTorch 中的 Embedding 层作为一个查找操纵,检索与 token IDs 对应的向量。天生的 embedding 向量提供了 tokens 的连续表示,这对于练习像 LLMs 这样的深度学习模子至关重要。
- positional embeddings 的作用:固然 token embeddings 为每个 token 提供了同等的向量表示,但它们缺乏对 token 在序列中位置的感知。为了纠正这一点,存在两种主要类型的 positional embeddings:绝对和相对。OpenAI 的 GPT 模子利用绝对 positional embeddings,这些 embeddings 被添加到 token embedding 向量中,并在模子练习期间举行优化。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |