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

社区文章 发布于 2024年6月27日
Neural tokenization

当前的分词器存在一些众所周知的问题,这些问题正在拖累大型语言模型 (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 的词元。

Tokenization of obfuscated code

直觉

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"]
  • 分词器处理数字时存在问题
    • 碎片化:"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 是词元维度,通常为 64
  • U 是编码维度,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$。

  1. LayerNorm 层执行层归一化。
  2. Reshape 层分割序列轴:(B, S * G_i, E) => (B, S, G_i * E)
  3. Dense 层最终将最后一个维度 G_i * E 压缩到 E

对于 G = [4, 4, 4],第一个块将 UTF-32 字节 4 个一组合并,然后为每个 Unicode 字符生成一个嵌入向量。然后第二个层将嵌入 4 个一组合并,依此类推。

解码器

解码器执行完全相反的操作,使用相同的词元单位。

  1. Dense 层将潜在维度从 E 扩展到 G_i * E
  2. Reshape 层分割特征轴:(B, S, G_i * E) => (B, S * G_i, E)
  3. 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" "r anderem drei 5"

顺序也被编码,并且扮演着重要角色。

样本 共享 立场
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 自带 特殊字符

0000 - 001F 007F - 009F

许多这些特殊字符已经过时,可以重新用作特殊词元。

例如,在 Unicode 中,0x00020x0003 分别代表“文本开始”和“文本结束”,它们类似于 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 上更详细的模型迭代文章

社区

注册登录 发表评论