〔探索AI的无限可能,加入“AIGCmagic”社区,让AIGC科技点亮生活〕
本文作者:AIGCmagic社区 刘一手
零. 初步认识tokenization
在计算机科学领域,自然语言(如英语和普通话)与机器语言(如汇编语言和 LISP)之间存在着明显的差异。自然语言是人类交流的自然形式,而机器语言则是计算机理解和执行的严格结构化语言。尽管计算机在处理自身语言方面表现出色,但它们在处理人类语言的复杂性和模糊性方面却面临挑战。
语言,尤其是文本,是我们交流和知识存储的主要方式。互联网上的内容大部分是文本形式,而大型语言模型如 ChatGPT、Claude 和 Llama 等,通过复杂的计算技术对这些文本进行训练。这些模型能够处理和理解大量的在线文本数据。
然而,计算机本质上是数字处理设备,它们并不直接操作单词或句子。那么,如何弥合人类语言和机器理解之间的差距呢?
这就是自然语言处理(NLP)的用武之地。NLP 是一个跨学科领域,它结合了语言学、计算机科学和人工智能,旨在使计算机能够理解、解释和生成人类语言。NLP 通过算法和模型,使机器能够从文本输入中提取意义,并产生有意义的输出,无论是进行语言翻译、文章摘要还是进行对话。
Tokenization 是自然语言处理(NLP)中的一个关键步骤,它涉及将原始文本转换为计算机可以有效处理的格式。这个过程不仅仅是简单的文本拆分;它还确保以一种保留计算模型含义和上下文的方式来准备语言数据。
以下是 tokenization 过程的典型工作方式:
-
标准化:在标记化之前,文本首先需要进行标准化以确保一致性。这可能包括将所有字母转换为小写、删除标点符号以及应用其他规范化技术。这一步骤的目的是减少文本中的变异性,以便模型可以更准确地处理和理解数据。
-
Tokenization:标准化后的文本接下来被拆分为更小、可管理的单元,这些单元称为 token。Token 可以是单词、子单词,甚至是单个字符,具体取决于应用的需求和上下文。
-
数字表示:也叫索引。由于计算机以数字数据为依据进行操作,因此每个 token 都会转换为数字表示。这个过程可以简单到为每个 token 分配一个唯一标识符,也可以复杂到创建多维向量来捕获 token 的含义和上下文。数字表示使得计算机能够对文本进行数学运算和处理。
不同的 tokenization 方法 可以显著影响模型理解和处理语言的能力。例如,一些模型可能需要考虑单词的形态变化,而其他模型可能更关注整个短语或句子的结构。
下面这张图展示了文本处理的基本流程,从原始文本到标准化、分词、索引,再到向量编码的过程。每个步骤都将文本转化为更适合机器处理的形式,最终用于自然语言处理任务中的模型训练和预测。
本文重点介绍文本处理的前两步:文本标准化和tokenization。
一. 文本标准化
在自然语言处理(NLP)中,文本的细微变化可能会对算法的解释和处理产生显著影响。考虑以下两个句子:
- “dusk fell, i was gazing at the Sao Paulo skyline. Isnt urban life vibrant??”
- “Dusk fell; I gazed at the São Paulo skyline. Isn’t urban life vibrant?”
翻译成中文都是:“黄昏降临,我凝视着圣保罗的天际线。城市生活是不是很充满活力?”
乍一看,这些句子传达了相似的含义。然而,当计算机处理它们时,特别是在 tokenization 或编码等任务中,它们可能会因细微的变化而显得截然不同:
- 大写:“dusk”与“Dusk”
- 标点符号:逗号与分号;问号的存在
- 缩写:“Isnt”与“Isnt”
- 拼写和特殊字符:“Sao Paulo”与“São Paulo”
这些差异会显著影响算法对文本的解释方式。例如,没有撇号的“Isnt”可能不会被识别为“is not”的缩写,而“São”中的“ã”等特殊字符可能会被误解或导致编码问题。
文本标准化是 NLP 中解决这些问题的关键预处理步骤。通过标准化文本,我们可以减少不相关的变化并确保输入模型的数据是一致的。此过程是一种特征工程,我们消除了对手头任务没有意义的差异。
文本标准化的一种简单方法包括:
- 转换为小写:减少因大写而导致的差异。
- 删除标点符号:通过消除标点符号简化文本。
- 规范化特殊字符:将“ã”等字符转换为其标准形式(“a”)。
将这些步骤应用于我们的句子,我们得到:
- “dusk fell i was gazing at the sao paulo skyline isnt urban life vibrant”
- “dusk fell i gazed at the sao paulo skyline isnt urban life vibrant”
翻译成中文:黄昏时分,我凝视着圣保罗的天际线,城市生活真是充满活力啊。
现在,句子更加统一,只突出了词汇选择上的有意义的差异(例如,“was gazing at”与“gazed at”)。
虽然有更高级的标准化技术,如词干提取(将单词简化为词根形式)和词形还原(将单词简化为字典形式),但这种基本方法有效地最大限度地减少了表面差异。
以下是基于 Python 实现基本文本标准化的方法:
import re
import unicodedata
def standardize_text(text):
# 将文本转换为小写
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)
# 输出
dusk fell i was gazing at the sao paulo skyline isnt urban life vibrant
dusk fell i gazed at the sao paulo skyline isnt urban life vibrant
确实,通过标准化文本,我们可以显著减少那些可能会使计算模型混淆的表面差异。这种预处理步骤允许模型更加专注于文本中真正重要的变化,从而提高其理解和处理语言的能力。
二.tokenization
在自然语言处理(NLP)的流程中,文本标准化是首要步骤,它为后续的处理打下了坚实的基础。文本标准化之后,紧接着的关键步骤是tokenization。
Tokenization是将标准化后的文本分解为更小的单元,这些单元被称为tokens。Tokens 是构建语言模型的基础,它们是机器学习和深度学习模型用来理解和生成人类语言的基本元素。通过将文本分解成tokens,我们为模型提供了一种方式来识别和处理语言中的各个组成部分。
在tokenization过程中,每个token都会被转换成机器可以处理的数字表示。这种转换是文本矢量化(vectorization)的一部分,矢量化是将文本数据转换为可以被机器学习算法处理的格式的过程。通过这种方式,模型能够识别语言中的模式和结构,从而进行有效的语言理解和生成。
tokenization目前有三种主流方式:单词级、字符级、子词级。
2.1 单词级的tokenization
做法:根据空格和标点符号将文本拆分为单个单词。这是分解文本最直观的方法。
比如:
text = "dusk fell i gazed at the sao paulo skyline isnt urban life vibrant"
tokens = text.split()
print(tokens)
#输出
['dusk', 'fell', 'i', 'gazed', 'at', 'the', 'sao', 'paulo', 'skyline', 'isnt', 'urban', 'life', 'vibrant']
- 优点:
单词级 Tokenization是最简单的方法,它将文本分割成单词。每个单词被视为一个单独的 token。这种方法在处理英语等空格分隔的语言时非常有效,因为单词之间的界限清晰。
2.2 字符级的tokenization
做法:将文本分解为单个字符,包括字母、标点符号等。
比如:
text = "life vibrant"
tokens = list(text)
print(tokens)
# 输出
['l', 'i', 'f', 'e', ' ', 'v', 'i', 'b', 'r', 'a', 'n', 't']
- 优点:
与单词级不同,字符级 tokenization 将文本分割成单个字符。这种方法不考虑单词边界,适用于没有明显单词分隔的语言,如中文和日文。
2.3 子词级的tokenization
做法:这种方法介于单词级和字符级之间,它将单词分割成更小的、有意义的片段,称为子词(subword)。
BERT(Bidirectional Encoder Representations from Transformers)模型广泛使用了子词级 tokenization。以下是使用 transformers 库中的 BertTokenizer 方法进行子词级 tokenization 的示例:
from transformers import BertTokenizer
text = "I have a new GPU!"
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
tokens = tokenizer.tokenize(text)
print(tokens)
# 输出
['i', 'have', 'a', 'new', 'gp', '##u', '!']
在这个例子中,单词 “GPU” 被拆分为 “gp” 和 “##u”。这里的 “##” 符号表示 “u” 是前一个子词 “gp” 的延续。这种拆分方式使得模型能够更好地理解和处理单词的内部结构。
- 优点:
(1) 减少词汇表大小:通过使用子词而不是整个单词,可以显著减少模型需要学习的词汇量。
(2) 处理罕见词和新词:子词级 tokenization 能够通过识别和使用常见的词根和词缀来处理未知的单词。
(3) 保留语义信息:通过将单词分割成有意义的片段,子词级 tokenization 能够保留更多的语义信息。
另外一个例子:单词“annoyingly”,这是一个在训练语料库中可能很少见的单词。通过子词级 tokenization,它可以被分解为**“annoying”和“ly”**。这两个部分单独出现的频率都更高,它们的组合含义保留了“annoyingly”的本质。
from transformers import BertTokenizer
text = "It is annoyingly long."
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
tokens = tokenizer.tokenize(text)
print(tokens)
# 输出
['it', 'is', 'annoying', '##ly', 'long', '.']
这种方法在土耳其语等黏着性语言中尤其有用。在这些语言中,单词可能会变得非常长,因为子单词会串在一起来传达复杂的含义。例如,一个单词可能包含多个词缀,每个词缀都有特定的语法或语义功能。
请注意,标准化步骤通常集成到 tokenizer 本身中。这包括转换为小写、去除标点符号、替换特殊字符等操作。大型语言模型在处理文本时使用 token 作为输入和输出。
以下是 Llama-3–70B 在 “Tiktokenizer” 工具上生成的 token 的视觉表示:
小结
tokenization的三种方式都有其优势和适用场景。单词级 tokenization 适合处理空格分隔的语言,而字符级 tokenization 适合处理没有明显单词边界的语言。子词级 tokenization 则提供了一种平衡,它通过减少词汇表的大小来提高模型的泛化能力,同时仍然能够处理未知词汇。
在实际应用中,选择哪种 tokenization 方法取决于具体的任务需求和语言特性。例如,在处理英语文本时,单词级 tokenization 可能是首选;而在处理中文文本时,字符级或子词级 tokenization 可能更为合适。随着深度学习技术的发展,子词级 tokenization 因其在处理多种语言和任务中的灵活性和有效性,越来越受到重视。
下面重点介绍子词级 tokenization常用两种算法:字节对编码 (BPE)和WordPiece。
三.子词级tokenization常用算法
3.1 字节对编码 (BPE)
字节对编码(Byte-Pair Encoding,简称 BPE)是一种高效的子词 tokenization 方法,它在自然语言处理领域被广泛使用,尤其是在机器翻译和文本生成任务中。BPE 方法的核心思想是通过迭代合并数据中最常见的字符对来构建词汇表,从而有效地处理罕见词和新词。
BPE 的步骤
- 初始化:从一组基本字符开始,这些字符是训练数据中出现的所有唯一字符。
- 计算频率:计算所有可能的字符对的频率。
- 合并最常见的对:合并出现频率最高的字符对,形成新的子词。
- 重复:重复合并过程,直到达到预定义的词汇表大小。
示例
假设我们有以下单词及其频率:
- “hug”(出现 10 次)
- “pug”(出现 5 次)
- “pun”(出现 12 次)
- “bun”(出现 4 次)
- “hugs”(出现 5 次)
初始基础词汇表由以下字符组成:[“h”、“u”、“g”、“p”、“n”、“b”、“s”]。
拆分单词
将单词拆分成单个字符:
- “h” “u” “g”(hug)
- “p” “u” “g”(pug)
- “p” “u” “n”(pun)
- “b” “u” “n”(bun)
- “h” “u” “g” “s”(hugs)
计算符号对频率
计算每个符号对的频率:
- “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”(hug)
- “p” “ug”(pug)
- “p” “u” “n”(pun)
- “b” “u” “n”(bun)
- “h” “ug” “s”(hugs)
继续合并
我们继续这个过程,将下一个最常见的对(例如“u n”合并到“un”),直到达到我们想要的词汇量。
应用到新单词
对于不在基础词汇表中的新单词,如 "bug"
和 "mug"
:
"bug"
分词为["b", "ug"]
"mug"
因包含未在基础词汇表中的"m"
,所以分词为["<unk>", "ug"]
注意事项
- 单个字母通常不会被替换为
<unk>
符号,因为训练数据中通常会包含每个字母至少一次的出现。 - 特殊字符(如表情符号)可能没有足够的出现次数而被视为
<unk>
。
超参数选择
词汇表的大小(基础词汇表大小+合并次数)是一个需要选择的超参数。例如,GPT的词汇表大小为40,478,因为它有478个基础字符,并在40,000次合并后停止训练。
BPE的训练
Hugging Face tokenizers 库提供了一种快速灵活的方式来训练和使用 tokenizers,包括 BPE。
假设在同目录下定义了一个train_bpe_files.txt文件,里面包含了任意一段文本,以“I love natural language processing.”句子为例,代码如下:
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace
# 初始化BPE分词器
tokenizer = Tokenizer(BPE())
# 设置tokenizer的预分词器为空格切分(后续迭代融合)
tokenizer.pre_tokenizer = Whitespace()
# 设置Bpe训练器,特殊字符
trainer = BpeTrainer(vocab_size=100, min_frequency=2,special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])
# 指定训练文件训练分词器
tokenizer.train(files=['train_bpe_files.txt'], trainer=trainer)
# 获取训练后的词表
a = tokenizer.get_vocab()
# 保存分词器
tokenizer.save("bpe-tokenizer.json")
# 测试分词结果
# sentence = 'I have a new GPU!'
# out = tokenizer.encode(sentence)
# print("Tokens:", out.tokens)
# print("IDs:", out.ids)
from tokenizers import Tokenizer
# 加载分词器
tokenizer = Tokenizer.from_file("bpe-tokenizer.json")
# 编码文本输入
encoded = tokenizer.encode("I have a new GPU!")
print("Tokens:", encoded.tokens)
print("IDs:", encoded.ids)
# 输出
Tokens: ['I', 'have', 'a', 'new', 'G', 'P', 'U', '!']
IDs: [14, 93, 19, 94, 12, 16, 18, 5]
-
要注意的是:
分词结果会根据训练数据的不同而有所差异。这是因为分词器在训练过程中学习到的词汇表和合并规则是基于训练数据的统计特性来确定的。 -
关于special_tokens的说明:
在使用预训练模型或训练自己的分词器时,特殊字符(特殊标记)扮演着重要的角色。它们帮助模型理解文本的结构和语义,以下是这些特殊字符的作用:
[UNK] (未知):代表未知词或在词汇表中未出现的词。当分词器遇到一个不在训练词汇表中的词时,它会使用 [UNK] 来代替。
[CLS] (分类):在BERT等模型中,[CLS] 通常被添加到输入序列的开始处。它用于聚合整个序列的表示,常用于分类任务。
[SEP] (分隔):用于分隔句子或句子对。在处理两个句子的序列时,如问答或句子对任务,[SEP] 用来区分它们。
[PAD] (填充):用于将所有序列填充到相同的长度,以便能够批量处理。在模型的输入中,[PAD] 被添加到较短的序列末尾,直到达到所需的最大长度。
[MASK]:在BERT等模型中,[MASK] 是掩码token,用于掩码一些词,然后让模型预测被掩码的词。
3.2 WordPiece
WordPiece是用于BERT、DistilBERT和Electra的子词分割算法。该算法在《日语和韩语语音搜索》(Schuster等人,2012)中概述,与BPE非常相似。WordPiece首先初始化词汇表以包括训练数据中存在的每个字符,并逐步学习给定数量的合并规则。与BPE不同,WordPiece不是选择最频繁的符号对,而是选择一旦添加到词汇表中就能最大化训练数据可能性的符号对。
WordPiece 的工作原理如下:
- 初始化: 从包含所有唯一字符的词汇表开始。
- Pre-tokenization: 将训练文本拆分为单词。
- 构建词汇表: 迭代地将新符号(子词)添加到词汇表中。
- 选择标准: WordPiece 不会选择最常见的符号对,而是选择在添加到词汇表中时最大可能增加训练数据可能性的符号对。
WordPiece 的训练
假设在同目录下定义了一个train_wordpiece_files.txt文件,里面包含了任意一段文本,以“I love natural language processing.”句子为例,代码如下:
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]"])
# 训练分词器
files = ["train_wordpiece_files.txt"]
tokenizer.train(files, trainer)
# 保存分词器
tokenizer.save("wordpiece-tokenizer.json")
# 测试分词结果
from tokenizers import Tokenizer
# 加载分词器
tokenizer = Tokenizer.from_file("wordpiece-tokenizer.json")
# 编码文本输入
encoded = tokenizer.encode("I have a new GPU!")
print("Tokens:", encoded.tokens)
print("IDs:", encoded.ids)
# 输出
Tokens: ['I', 'have', 'a', 'new', 'G', '##P', '##U', '!']
IDs: [14, 116, 19, 117, 12, 66, 67, 5]
四. 其他
除了字节对编码 (BPE)和WordPiece,还有另外两种字词级tokenization算法也比较常用,分别做个简单介绍:
Unigram
- Unigram是一种基于统计的分词方法,它通过考虑每个可能的词序列的概率来进行分词。这种方法试图找到最可能的单词序列,给定一个句子。
- 计算简单,对新词有较好的识别能力。但忽略了上下文信息,可能导致分词结果不够准确。
SentencePiece
- SentencePiece是一种基于子词(subword)的分词技术,它旨在处理包括未登录词在内的各种语言现象。SentencePiece通过训练一个模型来学习如何将文本分割成词汇单元,这些单元可以是完整的单词、词根或字符序列。
- 能够有效地处理未登录词,适用于多语言环境。但可能需要较大的计算资源来训练模型,并且对于某些特定领域可能不够精确。
最后,来看看主流模型使用的分词器:
Model | Type of Tokenizer |
---|---|
fast MPNet | WordPiece |
PhoBERT | Byte-Pair-Encoding |
T5 | SentencePiece |
fast T5 | Unigram |
fast MBART | BPE |
fast PEGASUS | Unigram |
PEGASUS | SentencePiece |
XLM | Byte-Pair-Encoding |
TAPAS | WordPiece |
BertGeneration | SentencePiece |
BERT | WordPiece |
fast BERT | WordPiece |
XLNet | SentencePiece |
GPT-2 | byte-level Byte-Pair-Encoding |
fast XLNet | Unigram |
fast GPT-2 | byte-level Byte-Pair-Encoding |
fast ALBERT | Unigram |
ALBERT | SentencePiece |
CTRL | Byte-Pair-Encoding |
fast GPT | Byte-Pair-Encoding |
Flaubert | Byte-Pair Encoding |
FAIRSEQ | Byte-Pair Encoding |
Reformer | SentencePiece |
fast Reformer | Unigram |
Marian | SentencePiece |