Tokenization example generated by Llama-3-8B. Each colored subword represents a distinct token.
什么是分词?在计算机科学中,我们将人类使用的语言,如英语和普通话,称为“自然”语言。相比之下,像汇编语言和LISP这样的为与计算机交互而设计的语言被称为“机器”语言,这些语言遵循严格的语法规则,几乎没有解释的空间。虽然计算机在处理自己高度结构化的语言方面表现出色,但在处理人类语言的混乱性方面却显得力不从心。
语言——尤其是文本——构成了我们大部分的交流和知识存储。例如,互联网主要是文本。大型语言模型如ChatGPT、Claude和Llama是在海量文本上进行训练的——基本上是所有在线可用的文本——使用了复杂的计算技术。然而,计算机是基于数字运行的,而不是基于单词或句子。那么,我们如何弥合人类语言和机器理解之间的差距呢?
这就是 自然语言处理 (NLP) 发挥作用的地方。NLP 是一个结合了语言学、计算机科学和人工智能的领域,使计算机能够理解、解释和生成人类语言。无论是将文本从英语翻译成法语,总结文章,还是参与对话,NLP 都能使机器从文本输入中产生有意义的输出。
自然语言处理(NLP)的第一个关键步骤是将原始文本转换为计算机可以有效处理的格式。这个过程被称为 分词。分词涉及将文本分解成更小、更易管理的单元,称为 标记,这些标记可以是单词、子词,甚至是单个字符。以下是该过程的通常工作方式:
- 标准化: 在分词之前,文本会被标准化以确保一致性。这可能包括将所有字母转换为小写,移除标点符号,并应用其他规范化技术。
- 分词: 标准化的文本随后被拆分成词元。例如,句子
“The quick brown fox jumps over the lazy dog”
可以被分词为单词:
["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]
- 数值表示: 由于计算机处理的是数值数据,每个词元都被转换成数值表示。这可以简单到为每个词元分配一个唯一的标识符,也可以复杂到创建多维向量来捕捉词元的意义和上下文。
插图灵感来自弗朗索瓦·乔勒的《用Python进行深度学习》一书中的“图11.1 从文本到向量”
注:保持Markdown语法格式不变,链接中的文本未翻译,因为原文中链接指向的是书名,符合中文表达习惯。
分词不仅仅是将文本分割开,而是为了以一种能够保留意义和上下文的方式准备语言数据,以便计算模型使用。不同的分词方法会显著影响模型理解和处理语言的效果。
在本文中,我们将关注文本标准化和分词,探讨一些技术和实现方法。我们将奠定基础,将文本转换为机器可以处理的数值形式——这是迈向词嵌入和语言建模等高级主题的重要一步,我们将在未来的文章中探讨这些主题。
文本标准化考虑这两个句子:
1.
"夜幕降临,我凝视着圣保罗的天际线。城市生活不是很精彩吗?"
2.
"夜幕降临;我凝视着圣保罗的天际线。城市生活不是很精彩吗?"
乍一看,这些句子传达了相似的含义。然而,在计算机处理时,尤其是在分词或编码等任务中,由于细微的差异,它们可能会显得非常不同:
- 首字母大写:
“dusk”
vs.“Dusk”
- 标点符号: 逗号 vs. 分号;问号的存在
- 缩写:
“Isnt”
vs.“Isn’t”
- 拼写和特殊字符:
“Sao Paulo”
vs.“São Paulo”
这些差异可以显著影响算法如何解释文本。例如,没有使用撇号的 “Isnt”
可能不会被识别为 “is not”
的缩写,而像 “São”
中的 “ã”
这样的特殊字符可能会被误解或导致编码问题。
文本标准化是NLP中一个至关重要的预处理步骤,用于解决这些问题。通过标准化文本,我们可以减少无关的变异性,并确保输入模型的数据具有一致性。这一过程是一种特征工程的形式,我们通过它消除对当前任务无意义的差异。
一种简单的文本标准化方法包括:
- 转换为小写 : 减少了由于大小写差异造成的问题。
- 移除标点 : 通过消除标点符号简化文本。
- 规范化特殊字符 : 将特殊字符如
“ã”
转换为其标准形式 (“a”
)。
将这些步骤应用于我们的句子,我们得到:
1.
“夜幕降临,我在凝视圣保罗的天际线,城市生活真是充满活力”
2.
“夜幕降临,我凝视着圣保罗的天际线,城市生活真是充满活力”
现在,句子更加统一,只突出词汇选择中的有意义的差异(例如,“was gazing at”
与 “gazed at”
)。
虽然有更高级的标准处理技术,如词干提取(将单词还原为其基本形式)和词形还原(将单词还原为其词典形式),这种基本方法也能有效减少表面差异。
Python 实现文本标准化这里是如何用Python实现基本的文本标准化:
import re
import unicodedata
def standardize_text(text: str) -> str:
# 将文本转换为小写
text = text.lower()
# 将Unicode字符规范化为ASCII
text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('utf-8')
# 移除标点符号
text = re.sub(r'[^\w\s]', '', text)
# 移除多余的空格
text = re.sub(r'\s+', ' ', text).strip()
return text
# 示例句子
sentence1 = "dusk fell, i was gazing at the Sao Paulo skyline. Isnt urban life vibrant??"
sentence2 = "Dusk fell; I gazed at the São Paulo skyline. Isn't urban life vibrant?"
# 标准化句子
std_sentence1 = standardize_text(sentence1)
std_sentence2 = standardize_text(sentence2)
print(std_sentence1)
print(std_sentence2)
输出:
夜幕降临,我凝视着圣保罗的天际线,城市生活是多么的充满活力
夜幕降临,我凝视着圣保罗的天际线,城市生活是多么的充满活力
通过标准化文本,我们已经最小化了可能混淆计算模型的差异。模型现在可以专注于句子之间的差异,例如 “was gazing at”
和 “gazed at”
之间的区别,而不是标点或大小写之类的差异。
经过文本标准化后,自然语言处理中的下一个关键步骤是 分词。分词涉及将标准化的文本分解成更小的单元,称为 词元。这些词元是模型用来理解和生成人类语言的基本构建块。分词为文本的向量化做准备,在向量化过程中,每个词元都被转换成机器可以处理的数值表示。
我们的目标是将句子转换为计算机可以高效处理的形式。有三种常见的分词方法:
1. 单词级别分词基于空格和标点符号将文本拆分成单独的单词。这是拆分文本最直观的方法。
text = "dusk fell i gazed at the sao paulo skyline isnt urban life vibrant"
tokens = text.split()
print(tokens)
输出:
['黄昏', '降临', '我', '凝视', '着', '圣', '保罗', '的', '天际线', '都市', '生活', '难道', '不', '充满', '活力', '吗']
2. 字符级别的分词
将文本拆分成单独的字符,包括字母和有时的标点符号。
text = "Dusk fell"
tokens = list(text)
print(tokens)
输出:
['D', 'u', 's', 'k', ' ', 'f', 'e', 'l', 'l']
3. Subword 分词
将单词拆分成更小、有意义的子词单元。这种方法平衡了字符级别分词的粒度和词汇级别分词的语义丰富度。Byte-Pair Encoding (BPE) 和 WordPiece 等算法属于此类方法。例如,BertTokenizer 对 “I have a new GPU!”
进行分词如下:
从 transformers 导入 BertTokenizer
text = "I have a new GPU!"
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
tokens = tokenizer.tokenize(text)
print(tokens)
输出:
['我', '有', '一', '个', '新', 'g', '##p', '!']
在这里,“GPU”
被拆分为 “gp”
和 “##u”
,其中 “##”
表示 “u”
是前一个子词的延续。
子词分词提供了一种在词汇量大小和语义表示之间取得平衡的方法。通过将罕见单词分解为常见的子词,它保持了可管理的词汇量大小,而不牺牲意义。子词携带语义信息,有助于模型更有效地理解上下文。这意味着模型可以通过将新词或罕见词分解为熟悉的子词来处理它们,从而增强其处理更广泛语言输入的能力。
例如,考虑单词 “annoyingly”
,它在训练语料库中可能比较罕见。它可以被分解为子词 “annoying”
和 “ly”
。单独来看,“annoying”
和 “ly”
出现的频率更高,它们的组合意义保留了 “annoyingly”
的核心含义。这种方法在像土耳其语这样的粘着语中特别有益,因为在土耳其语中,通过将子词串接起来可以形成非常长的单词来传达复杂的含义。
注意,标准化步骤通常被集成到分词器本身中。大型语言模型在处理文本时,使用token作为输入和输出。这是由Llama-3–8B在Tiktokenizer生成的token的可视化表示:
Tiktokenizer 示例使用 Llama-3–8B。每个 token 用不同的颜色表示。
此外,Hugging Face 提供了一个出色的分词器指南,我在本文中使用了其中的一些示例。
我们现在来探讨不同的子词分词算法是如何工作的。需要注意的是,所有的这些分词算法都依赖于某种形式的训练,这种训练通常是在相应模型将要训练的数据集上进行的。
字符对编码(BPE)Byte-Pair Encoding(BPE)是一种子词分词方法,由Sennrich等人在2015年的论文《使用子词单元进行稀有词的神经机器翻译》中提出。BPE 从训练数据中所有唯一字符组成的基词汇表开始,迭代地合并最频繁出现的符号对——这些符号可以是字符或字符序列——以形成新的子词。这个过程会一直持续到词汇表达到预定义的大小,这个大小是一个在训练前选择的超参数。
假设我们有以下单词及其频率:
“hug”
(10次出现)“pug”
(5次出现)“pun”
(12次出现)“bun”
(4次出现)“hugs”
(5次出现)
我们的初始基础词汇表包含以下字符:[“h”, “u”, “g”, “p”, “n”, “b”, “s”]
。
我们将单词拆分成单独的字符:
“h” “u” “g”
(拥抱)“p” “u” “g”
(吉娃娃)“p” “u” “n”
(双关语)“b” “u” “n”
(包子)“h” “u” “g” “s”
(拥抱们)
接下来,我们计算每个符号对的频率:
“h u”
: 15 次(来自“hug”
和“hugs”
)“u g”
: 20 次(来自“hug”
,“pug”
,“hugs”
)“p u”
: 17 次(来自“pug”
,“pun”
)“u n”
: 16 次(来自“pun”
,“bun”
)
最频繁的词对是 “u g”
(20次),所以我们合并 “u”
和 “g”
形成 “ug”
并更新我们的词汇:
“h” “ug”
(拥抱)“p” “ug”
(京巴狗)“p” “u” “n”
(双关语)“b” “u” “n”
(面包卷)“h” “ug” “s”
(拥抱们)
我们继续这个过程,合并下一个最频繁出现的词对,例如将 “u n”
合并为 “un”
,直到达到我们期望的词汇表大小。
BPE 通过指定合并操作的数量来控制词汇表的大小。频繁出现的单词保持不变,减少了大量记忆的需求。而罕见或未见过的单词可以通过已知子词的组合来表示。它被用于像 GPT 和 RoBERTa 这样的模型中。
Hugging Face 的 tokenizers 库提供了一种快速且灵活的方式来训练和使用分词器,包括 BPE。
训练一个BPE分词器这里是如何在一个样本数据集上训练一个BPE分词器:
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace
# 初始化一个分词器
tokenizer = Tokenizer(BPE())
# 设置预分词器以在空白处分割
tokenizer.pre_tokenizer = Whitespace()
# 使用所需的词汇表大小初始化训练器
trainer = BpeTrainer(vocab_size=1000, min_frequency=2, special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])
# 要训练的文件
files = ["path/to/your/dataset.txt"]
# 训练分词器
tokenizer.train(files, trainer)
# 保存分词器
tokenizer.save("bpe-tokenizer.json")
使用训练好的BPE分词器:
从tokenizers导入Tokenizer
# 加载分词器
tokenizer = Tokenizer.from_file("bpe-tokenizer.json")
# 对文本输入进行编码
encoded = tokenizer.encode("我有一块新的GPU!")
print("标记:", encoded.tokens)
print("ID:", encoded.ids)
输出:
分词: ['我', '有', '一', '个', '新', 'GP', 'U', '!']
ID: [12, 45, 7, 89, 342, 210, 5]
WordPiece
WordPiece 是另一种子词分词算法,由 Schuster 和 Nakajima 在 2012 年提出,并被像 BERT 这样的模型所普及。与 BPE 类似,WordPiece 从所有唯一的字符开始,但在选择要合并的符号对时有所不同。
WordPiece的工作原理如下:
- 初始化:从所有唯一的字符开始构建词汇表。
- 预分词:将训练文本拆分成单词。
- 构建词汇表:迭代地将新的符号(子词)添加到词汇表中。
- 选择标准:WordPiece不是选择最频繁的符号对,而是选择当添加到词汇表时能最大化训练数据概率的符号对。
使用与之前相同的词频,WordPiece 评估合并哪一对符号会最大程度地增加训练数据的概率。这与基于频率的 BPE 方法相比,采取了更为概率化的方法。
类似于 BPE,我们可以使用 tokenizers
库训练一个 WordPiece 分词器。
from tokenizers import Tokenizer
from tokenizers.models import WordPiece
from tokenizers.trainers import WordPieceTrainer
from tokenizers.pre_tokenizers import Whitespace
# 初始化一个分词器
tokenizer = Tokenizer(WordPiece(unk_token="[UNK]"))
# 设置预分词器
tokenizer.pre_tokenizer = Whitespace()
# 初始化一个训练器
trainer = WordPieceTrainer(vocab_size=1000, min_frequency=2, special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])
# 训练分词器
tokenizer.train(files, trainer)
# 保存分词器
tokenizer.save("wordpiece-tokenizer.json")
使用训练好的WordPiece分词器:
从tokenizers导入Tokenizer
# 加载分词器
tokenizer = Tokenizer.from_file("wordpiece-tokenizer.json")
# 对文本输入进行编码
encoded = tokenizer.encode("我有一块新的GPU!")
print("标记:", encoded.tokens)
print("ID:", encoded.ids)
输出:
分词: ['我', '有', '一', '个', 'G', '##PU', '!']
ID: [10, 34, 5, 78, 301, 502, 8]
结论
分词是自然语言处理中的基础步骤,它为计算模型准备文本数据。通过理解和实现适当的分词策略,我们使模型能够更有效地处理和生成人类语言,为词嵌入和语言模型等高级主题奠定基础。
其他资源本文中的所有代码也可以在我的 GitHub 仓库中找到:github.com/murilogustineli/nlp-medium
- 让我们构建GPT分词器 | Andrej Karpathy在YouTube
- 分词 | Mistral AI大型语言模型
- 分词器概览 | Hugging Face
- 一步一步构建分词器 | Hugging Face
除非另有说明,所有图片均由作者创建。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章