分词是沉重的负担 (Tokun 第一部分)

当前的分词器存在一些众所周知的问题,这些问题正在拖累大型语言模型 (LLM)。
这些算法遵循人类语言直觉将文本转换为数字。但神经网络有其独特的方式来存储和处理数据,与任何可解释的算法都不同。
实际上,模型可以通过训练产生更高效的文本编码。
这个过程不同于训练模型来理解语言。
- 提议的模型与 Transformers 具有不同的架构。
- 最佳训练数据**不是**人类文本,而是随机字节。
无论内容如何,模型都将输入文本分割成 16 个 Unicode 字符的块。
混淆代码、原始十六进制或 Brainf*ck 编程语言都将被压缩 16 倍。
嵌入 1 | 嵌入 2 | 嵌入 3 |
---|---|---|
$P='++)u){$o.=u) |
$t{u)$i}^$k{$j}; |
}}u)retuu)rn $o; |
0x60806040523480 |
1561001057600080 |
fd5b506000805460 |
++++++++[>++++[> |
++>+++>+++>+<<<< |
-]>+>+>->>+[<]<- |
传统分词器绝不会将这些字符组合成词元,尤其是长度为 16 的词元。

直觉
OpenAI 表示 GPT-4 词元的平均长度为 4 个字符,在英语中。
对于 UTF-8,这 4 个字符在拉丁语系中不到 8 字节,在绝大多数情况下不到 12 字节。
在嵌入层中,这些 8 字节被像 cl100
这样的常见分词器转换为 100k 维的向量。如果向量的元素存储为 float32
,那么每个 4 字符将占用 400k 字节的空间。
尽管有各种技巧和优化,编码中仍存在大量浪费。让我们详细看看它是如何工作的。
最先进的分词技术
假设您在提示 GPT-4o
中包含了以下摘录:
Une unité lexicale ou token lexical ou plus simplement token est un couple composé d'un nom et d'une valeur optionnelle (e.g. 135677).
由于大型语言模型 (LLM) 实际上不处理文本,因此该句子首先需要翻译成数字。这个过程有几个阶段,主要包括:编码、分词和嵌入。
目前,考虑来自分词器 o200k
(在 GPT-4o
中使用)的 最终结果。

该句子被分成称为“词元”的块,这些块与 ID 具有一对一的匹配。每个分词器都有自己的词汇表,o200k
包含 200k 个已识别的词元。
这些 ID 尚不能直接用于神经网络:AI 模型处理张量,张量是“高级数组”。诀窍是将每个 ID 解释为 200k 元素数组中的索引。
"." => 13 => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, ..., 0]
这个操作被称为“独热编码”。
然后,此索引用于从 LLM 的第一层检索相应的嵌入。
这些(一系列)嵌入是神经网络的实际输入。这就是本文中我建议改进的地方。
将文本转换为嵌入的过程将被称为“编码”。分词是一种编码文本的技术。
我们将看到,一个专门的神经网络可以独立地训练以编码文本。
与性能的关系
编码输入有两个轴,其维度直接影响性能。
正向传播
首先,词元数量与序列维度相关。它定义了大型语言模型 (LLM) 可以一次处理的信息量。
通过在每个词元中容纳更多字符,您可以选择:
- 降低序列维度:保持相同的注意力范围,但降低计算需求。
- 保持相同的序列维度:增加注意力,同时保持成本不变。
模型大小
第二个轴的维度是恒定的,为数十万,即词汇量大小。
它与模型的大小直接相关,因为它需要为每个元素分配一个神经元。例如,llama3-8B
的第一层(嵌入层)有一个 128000 x 4096
的核。
模型的大小对成本有全面影响。参数数量大致是效率和质量之间的平衡。
模型训练
由于词元彼此之间没有关联,大型语言模型 (LLM) 必须看到每个变体才能构建相关的嵌入。训练过 "hot dog"
并不能迁移到 "hotdog"
。
如果词元/嵌入能够原生包含每个字符的信息,那么两者只会在空格上有所不同。大型语言模型 (LLM) 不需要针对每个变体进行训练,它们能够原生理解其中的细微差别。
虽然我无法量化其影响程度,但这会降低构建有意义嵌入(预训练阶段)所需的数据量。
当前分词器的局限性
前面的例子已经揭示了一些奇怪之处。
正如 Andrej Karpathy 指出的,还有更多问题:
- 分词器是在神经网络模型外部构建和操作的。
- 它们在跨语言方面泛化能力差。
- 它们导致输入向量的维度达到数十万。
- 它们需要定义额外的“特殊词元”。
- 词汇表外的单词会被碎片化:
["option", "nelle"]
- 词元彼此之间预先没有关联
- 字符:
"hello"
与"h"
或 ASCII 码104
没有关系。 - 大写:
"New-York"
和"new-york"
- 拼写错误:
"helllo"
和"hello"
- 重复:
" "
和"\t"
- 变形
- 动词变位:
"is"
和"be"
- 复数:
"languages"
和"language"
- 性别:
"franc"
和"franche"
- 格:属格、主格等
- 动词变位:
- 字符:
- 单词根据其周围元素以不同的方式分词
- GPT-4 将
"\thello world"
拆分为["\th", "ello", " world"]
- 而
"hello world"
则结果为["hello", " world"]
- GPT-4 将
- 分词器处理数字时存在问题
- 碎片化:
"8222.3"
被拆分为["822", "2", ".", "3"]
- 基数:
"0x10"
和"16"
- 格式:
"1.6e-1"
和"0.16"
- 碎片化:
显然,我问了 ChatGPT
他/它/它们是否需要补充一些内容:
- 对训练数据的依赖可能导致偏差并限制泛化能力
- 效率:某些分词器在编码和解码大文本时可能很慢
- 复合词处理:
"hotdog"
与"hot dog"
无关
tokun
模型解决了大部分这些缺点。
由于个人知识所限,本文主要侧重于西方语言。不过,这些概念也在亚洲和中东语言上进行了测试。
提议
与其在大型语言模型 (LLM) 外部构建词汇表,不如训练神经网络将任何字符序列转换为向量嵌入。
该模型将同时学习压缩和解压缩文本,从原始 Unicode 字节开始。

然后,大型语言模型 (LLM) 可以使用这种编码并预测下一个嵌入,然后将其转换为每个单独的字节。

与当前技术相比,输入轴将减少几个数量级。
例如,134 个字符的提示将被表示为:
- 由分词器
o200k
表示为形状为(32, 199_998)
的向量 - 由
tokun
表示为形状为(9, 256)
的向量
UTF-32 <=> “更好”的 UTF-8
就像传统分词一样,目标是从独立字节中组成有意义的词元。
它始于文本字符和符号的编码,遵循 Unicode 标准。
通常使用 UTF 进行翻译,但这些方案并不能完美地满足神经网络的要求。
编码 | 优点 | 缺点 |
---|---|---|
UTF-8 | 无间隙 | 大小可变 |
UTF-32 | 固定大小 | 空数据 |
固定大小将允许将输入文本分割成固定形状的张量。
避免空数据将有助于最大化输入空间中的信息密度。
实际上,我们可以通过神经网络压缩 UTF-32,从而同时实现这两个目标。这将是 tokun
模型的第一层模块。
该模型
总的来说,该模型是一个简单的 变分自编码器。
原始的 实现使用了 Tensorflow。
输入
输入张量的形状为 (B, S * T, U)
,其中:
B
是批次维度S
是序列维度T
是词元维度,通常为 64U
是编码维度,256
原始文本样本按如下方式预处理:
- 每个文本样本右侧填充
0x00
至固定长度S
- 然后编码为 UTF-32,这意味着每个字符占 4 字节。
嵌入
模型的前半部分,即编码器,将输入转换为压缩的嵌入向量。
给定输入张量 (B, S * T, U)
,嵌入的形状为 (B, S, L)
。L
是潜在维度,通常选择 U = L = 256
。
因此,编码器将序列长度除以因子 T = 64
。由于序列由 UTF-32 字节组成,每个字符 4 个字节,因此文本序列被 压缩了 16 倍。
输出
模型的后半部分将这些嵌入解码回其组成字节。
因此,整个模型(编码器 + 解码器)生成的概率向量与输入具有相同的形状。它们可以通过 argmax
轻松进行后处理,以预测实际的字节值。
架构
超参数
N = 3
,分词块的数量G = [4, 4, 4]
,每个块的词元单位维度U = 256
,UTF-32-BE 的编码维度E = 256
,嵌入维度L = 256
,潜在维度
这些最后一轴的维度理论上可能不同。正如我们将在结果中看到的那样,这些选择似乎最合适。
编码器
编码器是一个 CNN,具有类似于 WaveNet 模型 的堆叠空洞卷积。在堆叠 2 个分词块的情况下,编码过程如下:

每个块层将序列轴按块合并,因子为 $G_i$。
LayerNorm
层执行层归一化。Reshape
层分割序列轴:(B, S * G_i, E)
=>(B, S, G_i * E)
。Dense
层最终将最后一个维度G_i * E
压缩到E
。
对于 G = [4, 4, 4]
,第一个块将 UTF-32 字节 4 个一组合并,然后为每个 Unicode 字符生成一个嵌入向量。然后第二个层将嵌入 4 个一组合并,依此类推。
解码器
解码器执行完全相反的操作,使用相同的词元单位。
Dense
层将潜在维度从E
扩展到G_i * E
。Reshape
层分割特征轴:(B, S, G_i * E)
=>(B, S * G_i, E)
。LayerNorm
层执行层归一化。
它是一个解词元化层堆栈,用于解压缩连续的嵌入。
头
头部在最后一个轴上应用投影,然后是 softmax,以计算每个字节的概率。
变体
对该模型进行了许多变体训练和比较,包括:
- 归一化,包括 RMS 和层归一化
- 注意力机制
- 位置嵌入
- 前馈层
令人惊讶的是,最简单的模型表现显著更好。
模型唯一相关的变化在于词元单位:
[4, 16]
在容量和灵活性之间实现了最佳平衡[4, 4, 4]
经常卡在 75% 的准确率,但运气好的话可以达到 100%:它很脆弱[4, 4]
可用于增强嵌入的韧性- 其他变体表现不佳/臃肿
训练
单词、数字和代码只占 Unicode 字符所有可能组合的非常有限的范围。使用标准数据集可能会促使模型强调常见模式,而忽略独特的序列。
tokun
的作用实际上是压缩编码,而不是语言。
因此,最重要的一部分是用 UTF-32-BE 字节的随机序列训练模型。由于数据集是随机的,它可以原生扩展,无需数据增强。
还在 MLQA 上进行了验证,以确保模型在常规文本上保持准确性。
特征
该模型旨在用作分词器:它应该能够将嵌入解码回原始字节序列而不会出错。
这就是为什么我们将在本节中比较输入和输出的原因。
例如,它们是根据引言中的提示计算的,其中:
sample = """Une unité lexicale ou token lexical ou plus simplement token est un couple composé d'un nom et d'une valeur optionnelle (e.g. 135677)."""
inputs = tokun.pipeline.preprocess(text=sample, groups=[4, 16], expand=[1], flatten=True) # input = UTF-32-BE bytes
embeddings = MODEL._encoder(inputs) # embedding = tokens
outputs = MODEL._decoder(embeddings) # output = probabilities for each byte
predictions = tokun.pipeline.postprocess(outputs) # text = interpreted probabilities
输入压缩
任何输入张量 的序列长度都会除以 16。
print(len(sample)) # count in Unicode characters
# 134
print(inputs.shape) # count in UTF-32 bytes
# (1, 576)
print(embeddings.shape) # number of embeddings
# (1, 9, 256)
print(9 * 16) # padded input
# 144
而且嵌入的维度只有 256,而 llama3-8B
等模型则为 4096。
可靠性
当前的分词器偏向于其训练集中最频繁的出现。它们的性能取决于输入文本的内容。
tokun
将始终以 16 个字符的块来分块输入,无论内容如何。
语言无关性
该模型可以编码任何 Unicode 序列,包括任何人类语言。
它从未在韩语上进行过训练,但嵌入仍能以 100% 的准确率解码。
print(sample)
# 프롬프트 엔지니어링(영어: prompt engineering)은 인공지능의 한 개념으로 특히 자연어 처리 부분에 해당된다.
print(predictions)
# 프롬프트 엔지니어링(영어: prompt engineering)은 인공지능의 한 개념으로 특히 자연어 처리 부분에 해당된다.
print(tokun.evaluation.compart(sample, prediction))
# 1.0
任何编程语言
print(sample)
# ++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.
print(predictions)
# ++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.
print(tokun.evaluation.compart(sample, prediction))
# 1.0
或随机二进制(打印前十六进制编码)
print(sample.encode('utf-8').hex())
# 000084ea000055510001952c00018225000007a00003163c0003e4ff0003726b
print(predictions.encode('utf-8').hex())
# 000084ea000055510001952c00018225000007a00003163c0003e4ff0003726b
print(tokun.evaluation.compart(sample, prediction))
# 1.0
有意义的词元
嵌入向量包含所有信息,直至字节级别。
这确保了内容相似的文本输入,如“hotdog”和“hot-dog”,在潜在空间中彼此接近。
这可以在 TensorBoard 中使用 t-SNE 算法 或 UMAP 进行可视化。

来自不同 Unicode 平面的语言被清晰地分隔开来:Unicode 空间结构非常清晰,码点的邻近性是有意义的。
即使在同一字母表中,德语、英语和越南语样本也相互分离。嵌入共享的字符越多,它们就越接近。
顺序也被编码,并且扮演着重要角色。
样本 | 共享 | 立场 |
---|---|---|
ated in the cons |
100% | 100% |
ated by the conc |
82% | 81% |
ased on the Cons |
90% | 81% |
video game cons |
82% | 44% |
ower in the arch |
82% | 56% |
ations, the Brit |
82% | 44% |
rtension is more |
78% | 31% |
jury in the 5th |
55% | 0% |
以上样本根据其嵌入与“ated in the cons”向量的距离进行排序。右侧第一个百分比是共享字符的比例,第二个百分比是位置完全相同的字符的比例。
请记住,此模型未在人类语言上训练,而是在随机 Unicode 数据上训练。
内置特殊词元
Unicode 自带 特殊字符:
许多这些特殊字符已经过时,可以重新用作特殊词元。
例如,在 Unicode 中,0x0002
和 0x0003
分别代表“文本开始”和“文本结束”,它们类似于 GPT-4
中使用的 <|im_start|>
和 <|im_end|>
。
鲁棒性
即使在不遵守底层结构的情况下,嵌入对于噪声也相当鲁棒。
std = tf.math.reduce_std(EMBEDDINGS[64]['en'], axis=0)
noise = tf.random.normal(shape=(256,), mean=0., stddev=tf.math.reduce_mean(__std).numpy())
inputs = tokun.pipeline.preprocess(sample, groups=[4, 16], expand=[1], flatten=True)
embeddings = MODEL._encoder(inputs)
print(tokun.pipeline.postprocess(MODEL._decoder(embeddings)))
# Une unité lexicale ou token lexical ou plus simplement token est un couple composé d'un nom et d'une valeur optionnelle (e.g. 135677).
嵌入可以承受振幅为 $\sigma$ 的结构化噪声,并在此范围之外开始出现错误。
print(tokun.pipeline.postprocess(MODEL._decoder(embeddings + 1.2 * std)))
# Une unité lexicale ou token lexical ou plus simplement token est un couple composé d'un nom et d'une valeur optionnelle (e.g. 135677).
print(tokun.pipeline.postprocess(MODEL._decoder(embeddings + 1.3 * std)))
# Une unité lexicale ou token lexical ou plus simpleねent token est un couple composé d'un nom et d'une valeur optionnelle (e.g. 135677).
它们更容易受到随机噪声的影响。
print(tokun.pipeline.postprocess(MODEL._decoder(embeddings + 0.1 * noise)))
# Une unité lexicale ou token lexical ou plus simplement token est un couple composé d'un nom et d'une valeur optionnelle (e.g. 135677).
print(tokun.pipeline.postprocess(MODEL._decoder(embeddings + 0.2 * noise)))
# Une unité lꝥxicɡle ou token꜠lexɩcal ou plus꜠simᝰlement tokeꝮ es un couple ꝣompɯsé d'un nom꜠づt ᝤ'une ɶaleur꜠optɩonnelle (e.ꝧ. 1ȳ5677).�����꜀팀��
解码在每个嵌入邻域的稳定性在 UMAP 图上更为明显。

一组完整的向量编码相同的文本实际上是一个很好的特性。如果模型是一对一匹配的,它将非常脆弱,并且宿主 LLM 将无法预测精确的向量。
回到基于存储空间的估计,每 64 个输入字节对应 1024 个嵌入字节似乎是一个合理的比率。它为大型语言模型的预测留有余地,同时具有显著的压缩因子。
这也可以解释为什么鲁棒性高度依赖于模型变体:tokun-4x16
只能承受 $0.1 * \sigma$,而 tokun-4x4
可以承受高达 $\sigma$ 的噪声。
结论
到目前为止,大型语言模型 (LLM) 在预训练阶段学习理解语言,然后在微调阶段理解人类交互或其他任务,最后学习如何根据策略行事。
在这里,我们认为神经网络可以学习编码和解码文本,以更好地满足其需求。
这些过程中的每一个都需要特定的架构和数据。就像在常规编程语言中一样,正在构建神经网络模块。
机器学习实际上让人联想到 HTML 和声明式编程语言:不是指定神经网络必须遵循的过程,而是通过数据展示结果的属性。
与其提及“通用人工智能”等模糊概念,不如将该领域标准化和合理化,使其成为一种新的编程语言,这样会更有益。
资源
所有模型的变体都已在以下平台提供:
您还可以在以下平台找到笔记本:
以及 GitHub 上更详细的模型迭代文章。