














整场「要不要把 Token 翻译成词元?」的争论令我感到诡异,诡异的点在于所有参与者都只看到了 Token 作为大语言模型计价单位的意思,而 Token 一词存在于英语语言中已经有很久了(对中文的影响也很大)。若把 Token 先前的释义也译作「词元」,场面会非常可笑:
正如人们滥用「人工智能」一词指代「大语言模型」,因为他们只知道热度最高的技术,便让 AI 这个上世纪就出现的概念被人工智能的一个小分支占据一样,人们滥用 Token 一词,以至于他们不关心其他的意思。更何况「词元」这个意思用在 LLM 里其实相当不准确。
不过,也需要批评前人,Token 一词的翻译本身就很混乱。
桌游里的 Token 是「指示物」的意思,还有人根据情况译作「代币」「标记」。大富翁里用来标记地产的房子,飞行棋里的飞机,龙与地下城里的角色小人,都叫作 Token。
用来表示人物、角色的指示物还被称作 Meeple(My People),音译作「米宝」。能用米宝来指代所有的指示物吗?当然不行!米宝的本意是人物,是 Token 里相当小的一类。同理,能用「词元」对应 Token 吗?不能!词元只指代相当小的范畴,你当然不能说桌游里的指示物是词元。
不仅桌游里的代币是 Token,游戏厅里的游戏机和赌场里的老虎机使用的代币,也叫作 Token coin,不过如今更多地被 ticket(票券)取代了。
密码学中的 Token 通常叫作「令牌」。常见的场景是客户端输入账号密码之后,服务端生成一个(多半有期限)的凭证,一般是字符串,之后客户端只需要在访问时提供这个凭证就好,期限时间内不需要再次输入密码。这个字符串一般保存在 Cookies 里,服务端自动读取,或者客户端携带在 HTTP 请求头里:
Authorization: Bearer eyJhbGci...<snip>...yu5CSpyHI
Bearer 之后的部分就是 Token。最常见的令牌之一是
JWT
,其他的应用场景也不少,不限于 HTTP 通信。任何能够在通信过程中,从密码学意义上验证用户身份的凭证字符串,都叫令牌。
顺带一提,令牌可以当作是时效性很强的密钥,如果泄露,攻击者就可以在令牌过期之前使用你的身份访问相关资源。令牌往往存放在 Cookies 里,这也是为什么 第三方 Cookies (或者说跨域 Cookies)常常受人诟病,现代浏览器也采取了很多限制。
还有一种密码学方法叫作令牌化( Tokenization ),指代把敏感信息替换为不敏感的等价物(即 Token)的过程。我对此了解不多,就不闹笑话了。
这类 Token 在解析代码的过程中产生,解析(parse)或者预处理(preprocess)还没有到编译(compile)或者解释(interpret)的地步,无论编译器还是解释器都有这个步骤。仔细想想,源代码只是储存在文件里的文本而已,可以当作字符串处理,解释器和编译器是怎么把字符串变成有意义、可执行的语句的?我们可以现场写个 Lisp 解析器来理解其过程。
程序员极有可能接触过简单的解释器,比方说,如何给下面这个算式字符串求值?
答案是构造抽象语法树(AST)。不难发现,上述算式可以表示为树形结构,(1 + 3) 是一个整体,4 * 7 因为乘法的优先级更高,也作为一个整体,那么就可以把 - 当作根,前面两个整体作为 - 的左右子树。继续递归,不难发现 (1 + 3) 是 + 为根,1 和 3 为叶子结点的子树,而 4 * 7 是 * 为根,4 和 7 为叶子结点的子树。
接下来只要深度优先遍历 AST 就好了,不难发现结点有两类,非叶子结点是操作符,叶子结点是数字。遍历时若是遇到数字,就直接返回值;若是遇到操作符,就根据操作符计算左右子树的值。在一系列奇妙的递归之后,程序就能求出这棵树的值为 -24。
回顾一开始的算式,能不能把它写成这个样子?
也就是说,把中置操作符换成前置操作符,把所有的括号显式地写出来。如果把 () 看成列表,前置操作符的写法其实是嵌套的列表,而嵌套的列表就是树。这样一来,把字符串解析成 AST 的算法就好写多了。
假设我们现在不仅仅是要表示算式,而是要表示程式……
(format t "it's ~a~%"
(- (+ 1 3) (* 4 7)))
上面这段代码应该打印 it's -24。
同样,手动解析为 AST,变成下图的这个样子:
现在,非叶子结点是函数,叶子结点是函数的参数。读者可能会觉得把不同类型的数据都放在一起有点奇怪,和刚才的算式差得有点远。不过,只要想到深度优先遍历和递归求值的思路,就不会觉得奇怪了。只不过函数不仅仅是计算得出了值,还可能带有副作用。比方说 format 的副作用就是在指定的输出流(此处是 stdout)中打印文本。
手动遍历这棵树:
format,发现是函数,于是求它的子树。format 的左子树,发现是值,返回 t。format 的第二棵子树,发现是值,返回 "it's ~a~%"format 的右子树 -,发现是函数,求它的子树。- 的左子树 +,发现是函数,求它的子树+ 的左子树,发现是值,返回 1+ 的右子树,发现是值,返回 3+ 的子树都求值完毕,对它本身求值得到 4- 的右子树 *,发现是函数,求它的子树* 的左子树,发现是值,返回 4* 的右子树,发现是值,返回 7* 的子树都求值完毕,对它本身求值得到 28- 的子树都求值完毕,对它本身求值得到 -24format 的子树都求值完毕,对它本身求值得到 nil,同时发生副作用:"it's -24" 被写入 stdout或许用一门非 Lisp 语言表述上面这个程序会更直观:
// eval 对 AST 或子树求值
func eval(node Node) any {
if node.IsLeaf() {
// 如果是叶子结点,直接返回值
return node.Value
}
fn := node.Value
var args []any
for _, child := node.GetChildren() {
args = append(args, eval(child))
}
// applyFn 根据函数名 fn,把某个操作作用在参数 args 上,最后返回值
return applyFn(fn, args)
}
你已经学会怎么写 Lisp 解释器了,快去造一门 Lisp 方言吧!
不过 eval() 函数的前提是 AST 已经被构造好了,而构造 AST 的前提是什么?回顾上面两棵树,你会发现每个结点中包含的都是有意义的语言单元。这个说法可能有点没头没脑的,难道还能包含没意义的语言单位吗?事实是,如果要创造完整的解释器,构造出 AST,就需要先划分出有意义的语言单元。
不能只根据空格划分,不然 "Hello World" 就会被分成 "Hello 和 World" 两个单元。甚者,(format t "Hello World") 会变成:(format、t、"Hello 和 World")。
假设有一个 tokenize() 函数,它的输入是未经处理的字符串,输出是字符串数组,数组里的每个元素都是独立的语言单元。执行 tokenize("(format t \"Hello World\")") 应该得到:
[]string{
"(",
"format",
"t",
"\"Hello World\"",
")"
}
之所以把括号留下,是因为它们需要作为标记传给 parse() 函数。一遇到 (,就认为下一个是根,之后的一直到 ) 都是子树。这个函数作为解析器,输入划分好的 Tokens,输出可以递归求值的 AST。parse() 具体怎么实现就不赘述了。
除了 Lisp,你还可以参照 C 语言编译器 gcc 的文档的
1.3 Tokenization
这一节。Tokenization 发生在预处理(preprocess)阶段。
至此,你应该明白我说的第二类 Token 指什么了。这类 Token 准确来说叫作 Lexical Token (词法记号,或者说…… 词元)。词法的(lexical)和句法的(syntactic,或名词 syntax)指代不同的抽象层次,词法分析就是把字符串划分为词法记号(lexical token)的过程,而句法分析(更常见的说法是语法分析)则是判断词法记号的组合有没有犯句法错误(syntax error)。Token 是词法层次的实体,分析句法时候我们就默认 Token 已经变成 AST 中不可分割的结点了。至于语法(grammar),则是词法和句法的组合,不过编程语言一般不说 grammar,都默认使用 syntax(句法或语法)一词。
真是为了一盘醋包了一场酣畅淋漓的饺子啊。
接下来是第三类 Token,即应用在 NLP 领域的 Token。前面我们在处理编程语言,而 NLP 是对自然语言的处理。那么 NLP 中的 Token 是指 jieba 这类分词工具吗?
的确,中文分词算法就是 NLP 分词的一种,叫作 Word tokenization。把英文句子打散成一个一个的单词,也属于这类分词。1
其他的分词方式还有:
第三个是不是看起来很不对劲?如果是以句子为单位,那为什么会叫作分词呢?这里的 Token 不是词法意义上的 Token,把 tokenization 译作「分词」显然不对,那么把 token 译作「词元」就更不对了。若是译作「词元」,难道不会和 word 混淆吗?
上述老派的 NLP 方法,一般用在非 LLM 聊天机器人、搜索引擎的 Query 分析和词频统计中,根据用例不同选用不同的 Tokenization 方法。那么在更新的 NLP 技术,大语言模型中,Token 又指代什么呢?
大语言模型使用的 Tokenization 方法与简单 NLP 类似,至少输入都是自然语言词句,但算法过程和输出完全不同。LLM 用到了字节对编码(Byte-pair encoding,BFE),最早在 1994 年提出的 BFE 算法如下。2
假设有形如 aaabdaaabac 的输入,其中 aa 这个字节对的出现频率很高,于是替换为整个字符串中没有出现过的其他字符,比如 Z,于是就变成了:
可见 ab 也出现了多次,替换为 Y
现在又有了多次出现的 ZY,替换为 X
通过递归的字节对编码,我们把 aaabdaaabac 压缩成了 XdXac。
大模型修改了 BFE 算法,不再用来压缩字符串,而是用来把字符转换为自然数,因为只有数字才能在神经网络算法中处理。字符转换成自然数之后,就成了 Token。
比方说,aaabdaaabac 表示为:
0, 0, 0, 1, 2, 0, 0, 0, 1, 0, 3
a=0, b=1, d=2, c=3
再递归得到:
4, 5, 2, 4, 5, 0, 3
a=0, b=1, d=2, c=3, aa=4, ab=5
大语言模型的 Token 可以很大,GPT-3.5 和 GPT-4 的 Token 长度为 100258,其中有 258 个特殊 Token。这些自然数被 Unicode 编码之后可能会变成奇怪的字符,比如字节 32 会变成 Ġ(U+0120,对应的十进制数是 288,即 32+256),在某些大模型里表示空格。
通过字节对编码得到的 Token 显然不是词语,我们是在字节(byte)的层面对其进行编码的,没有任何意义的字符组合也可以被编码为 Token,我们根本没有在「分词」。
老派 NLP 中的 Token 可以是句子,如今的 LLM 中的 Token 则是基于字节的编码,无论怎么看,LLM 的 Token 都不应该译作「词元」。
桌游小人是 Token,游戏代币是 Token,通行凭证是 Token,编译器和解释器划分的词法单元是 Token,NLP 中的字符、词语、句子和 N 元是 Token,LLM 中的字节对编码也是 Token。
把上面这些句子里的 Token 换成「词元」这个中文词,说得通吗?哪怕仅仅是在 LLM 的语境下,「词元」也词不达意。字节对编码显然不是词。
Token 不该译作「词元」,我想这没什么争议了。唯一可以译作「词元」的,只有计算机在做词法分析时得到的词法单元,但那也只是小部分而已。
英文里的 Token 本意是什么?它的 词源 是原始印欧语词根 *deik-,意思是展示(to show),同源的古诺斯语 teikn 意味着星座和征兆(omen),古英语对应的单词 tacen 意思是指示、符号、证据和征兆。
星座和征兆都是符号或者说对某种符号的解读,符号就是指示含义或另一个物体的东西。Token 就是「指示另一个实体的东西」,至于这「另一个实体」是什么,要根据语境来看。
桌游里的 Token 指示游戏人物、生命值、财产等等,通行凭证或者说令牌指示访问某个资源的权限,编译器和解释器中的词法记号指示不能继续分割的词法要素,NLP 中的 Token 根据使用场景指示一层含义(词语表示的含义、句子表示的含义、N 个词语组成的短语或者说 N 元表示的含义),LLM 中的 Token 指示字节对,同时也对字节对的出现频率做统计学表示。
说到这里我想到一个有些偏门的例子,在《星露谷物语》的某些 Mod 里,由于缺乏标识「玩家是否完成某项任务」的机制,所以有的 Mod 会在玩家与某个 NPC 达成关键进度时,给予名为某某 Token 的物品,并提醒玩家要带着这个 Token 进入特定场景才会触发下一部分的剧情。这里的 Token 就指示「玩家已经达成了某项进度」。
说起通行凭证和令牌,不知道读者有没有看过早年间的古装剧。如果有的话,读者可能熟悉「兵符」这种东西:
「符」一般分為兩半,右半在帝王,左半在將軍,調兵之時,使者帶符上朝驗合,才可行動。「符」形狀多種,以虎符為主;質料有銅、木、玉等,以青銅為多。
—— 漢語多功能字庫
通行凭证和令牌不就是「符」吗?词法记号和 NLP 中的 token 就更不用说了,语言本身就是符号。编程语言中的关键字、括号、字面量也是词法符号。就算未来出现更多基于 token 本意衍生出的用法,它多半也是符号,是用来指示某个东西的,而汉语中同样表示「指示某个东西的东西」的词,就是「符」。
只有 Token 是「词元」吗?不,Lemma 也叫「词元」。
语言学的分支词法学(令人意外,英文名和 Lexical 以及 Lemma 没有关系,叫作 Morphology,morph- 是变化、变形的意思)中有个概念叫作 Lemma,一般译作词目或者词元。
Lemma 也可以叫作 canonical form(规范形式)、dictionary form(词典形式)和 citation form(引用形式)。许多欧洲语言的词语都有多种词位变化,而在词典中往往写作一种形式,把变位作为衍生形式写在同一个条目中。词典中规范的形式就叫作词目或者词元。
比方说 go 也有 went、going、goes、to go 和 gone 等变位,但是在编写词典条目和查找词目的时候,我们只找 go 这个条目,而不找 went 或者 gone。有趣的是,gone 作为形容词,也可以是独立的词元。
可能这种区分看起来很没必要,但我给读者举个法语的例子就能看出词元这个概念的必要性了。法语词目 avoir 表示「有」,第一人称单数现在时直陈式为 ai,第二人称单数现在时直陈式为 as,第三人称单数现在时直陈式为 a,第一人称复数现在时直陈式为 avons,第二人称复数现在时直陈式为 avez,第三人称复数现在时直陈式为 ont,第一人称单数简单将来时直陈式为 aurai……
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。