自学内容网 自学内容网

手撸 chatgpt 大模型: 大模型训练前的数据预处理算法详解

在将文本用于训练 LLM 模型之前,我们需要将文本转换为模型可以处理的某种数学对象。预处理的第一步是将单词转换为数字,并能够将数字转换回单词。例如,给定文本:
“I love you”,我们需要将其转换为数字数组,例如 [1, 2, 3];然后,再将这些数字数组转换回原始文本,比如 [1, 2, 3] 变回 “I love you”。

让我们看看如何实现这一点。首先,我们需要一些基础文本作为素材,可以通过以下代码从链接下载文本:https://en.wikisource.org/wiki/Fire-Tongue/Chapter_1

import urllib.request 
url = "https://en.wikisource.org/wiki/Fire-Tongue/Chapter_1"
file_path = "fire-tongue.txt"
urllib.request.urlretrieve(url, file_path)

with open("fire-tongue.txt", "r", encoding="utf-8") as f:
  raw_text = f.read()

print(f"Total number of characters: {len(raw_text)}")
print(raw_text)

运行上述代码后,我们会将名为 fire-tongue.txt 的文件保存到磁盘中,此文件实际上是目标页面的 HTML 内容。输出结果如下:

``py
Total number of characters: 57211

可以看到,许多单词与特殊字符(例如 <、>、!、#)混合在一起。我们需要通过以下代码将单词与这些特殊符号分开,并将每个单词和符号计为独立的单元:

```py
# 根据给定字符分割文本
import re
preprocessed = re.split(r'([,.:;?_!=\-\"<>#\{\}\'$\&/()\[\]+]|--|\s)', raw_text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
print(len(preprocessed))
print(preprocessed[:30])

运行代码后,我们得到以下结果:

['<', '!', 'DOCTYPE', 'html', '>', '<', 'html', 'class', '=', '"', 'client', '-', 'nojs', '"', 'lang', '=', '"', 'en', '"', 'dir', '=', '"', 'ltr', '"', '>', '<', 'head', '>', '<', 'meta']

可以看到,像 < 和 ! 这样的符号被单独分离出来了。这些单词或符号被称为标记(tokens)。接下来,我们将计算文本中的唯一标记数量,并为每个标记分配一个唯一的数字,首先移除重复标记并计算不同标记的总数:

# 按字母顺序对标记分配编号
all_words = sorted(set(preprocessed))
vocab_size = len(all_words)
# 输出唯一标记总数
print(vocab_size)

运行代码后,我们得到以下结果:

1999

可以看到,尽管文本中的标记总数为 57211,但若将重复标记视为一个,总共有 1999 个唯一标记。我们接着查看前 50 个唯一标记:

vocab = {token: integer for integer, token in enumerate(all_words)}
# 打印前 50 个标记
for i, item in enumerate(vocab.items()):
  print(item)
  if i >= 50:
    break

运行代码后,我们得到以下结果:

('!', 0)
('"', 1)
('#', 2)
('$', 3)
('%', 4)
('%2C', 5)
('&', 6)
("'", 7)
('(', 8)
(')', 9)
...

如您所见,每个标记都与一个数字配对。例如,可以通过以下类来实现标记与数字间的转换:

# 实现标记与数字的转换
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])
    # 移除标点符号前的多余空格
    text = re.sub(r'\s+([,.?"()\'])', r'\1', text)
    return text

然后,我们可以通过以下代码对句子进行标记化和反向标记化:

tokenizer = SimpleTokenizerV1(vocab)
text = """
One summer's evening when the little clock upon his table was rapidly approaching the much-desired hour,
"""
ids = tokenizer.encode(text)
print(ids)
print(tokenizer.decode(ids))

运行上述代码后,您将看到句子如何被转换为数字列表并成功恢复。如您所见,不同的标记会转换为不同的数字,例如 One -> 332,summer’ -> 1706。给定一组数字,我们可以将它们转换为多个单词组成的句子。但是,我们的方案存在一个问题。例如,运行以下代码时会出错:

# 问题在于,tokenizer 无法处理文本中不存在的单词
text = "Hello, do you like tea?"
print(tokenizer.encode(text))

这是因为单词 Hello 从未出现在文本中,因此 tokenizer 无法将 Hello 转换为对应的数字,因为没有 Hello 的映射。为了解决这个问题,我们可以使用特殊的标记来表示文本中未出现的所有外来单词。例如,对于任何未见过的单词,我们将它们映射到标记 |unk|,并使用另一个特殊标记 |endoftext| 来表示两个不同文本来源的连接。因此,我们添加以下代码:

# 处理未见过的单词
# 添加特殊标记 *|unk|* 和 *|endoftext|*,所有未见过的单词映射到 *|unk|*
# 用 *|endoftext|* 作为区分不同文本来源的符号
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())[-10:]):
  print(item)

运行上述代码我们得到以下结果:

2001
('z', 1991)
('{', 1992)
('|', 1993)
('}', 1994)
('~ext', 1995)
('—', 1996)
('←', 1997)
('→', 1998)
('*|endoftext|*', 1999)
('*|unk|*', 2000)

现在让我们更新 tokenizer 来处理未见过的单词,如下所示:

# 将标记转换为 id,将 id 转换为标记
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):
    # 将句子转换为 id 列表
    preprocessed = re.split(r'([,.:;?_!=\-\"<>#\{\}\'$\&/()\[\]+]|--|\s)', text)
    preprocessed = [item.strip() for item in preprocessed if item.strip()]
    print(f"encode preprocessed: {preprocessed}")
    # 为未见过的单词添加 unk 标记
    preprocessed = [item if item in self.str_to_int else "*|unk|*" for item in preprocessed]
    # 将标记映射到 id
    ids = [self.str_to_int[s] for s in preprocessed]
    return ids 

  def decode(self, ids):
    # 将 id 列表转换为单词
    text = " ".join([self.int_to_str[i] for i in ids])
    # 删除特定符号前的空格
    text = re.sub(r'\s+([,.?"()\'])', r'\1', text)
    return text

让我们通过以下代码测试新的 tokenizer:

tokenizer = SimpleTokenizerV2(vocab)
text1 = "Hello, do you like tea?"
text2 = "In the sunlit rerraces of palace."
# 注意前后的空格
text  = " *|endoftext|* ".join((text1, text2))
        
print(text)
ids = tokenizer.encode(text)
print(ids)
print(tokenizer.decode(ids))

运行上述代码我们得到以下结果:

Hello, do you like tea? *|endoftext|* In the sunlit rerraces of palace.
encode preprocessed: ['Hello', ',', 'do', 'you', 'like', 'tea', '?', '*|endoftext|*', 'In', 'the', 'sunlit', 'rerraces', 'of', 'palace', '.']
[2000, 12, 805, 1988, 1169, 2000, 159, 1999, 276, 1743, 2000, 2000, 1325, 2000, 14]
*|unk|*, do you like *|unk|*? *|endoftext|* In the *|unk|* *|unk|* of *|unk|*.

如我们所见,这次未见过的单词 Hello 映射到了标记 |unk|


原文地址:https://blog.csdn.net/tyler_download/article/details/143815554

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!