diff options
| author | eltrac <hi@eltr.ac> | 2026-09-07 23:41:43 +0800 |
|---|---|---|
| committer | eltrac <hi@eltr.ac> | 2026-09-07 23:41:43 +0800 |
| commit | 75f2833d128f76af9cda59fd67b59a2272c4618e (patch) | |
| tree | 25fd6b0c11bdc21fc6d48976c4cc928b80c067ac | |
| parent | 9612caade52fb028e1ab5079c5a555f0571bbc9e (diff) | |
[publish] /article/token-translation
| -rw-r--r-- | content/article/token-translation.md | 219 |
1 files changed, 203 insertions, 16 deletions
diff --git a/content/article/token-translation.md b/content/article/token-translation.md index 30a14937..b8429d99 100644 --- a/content/article/token-translation.md +++ b/content/article/token-translation.md @@ -1,35 +1,39 @@ +++ title = 'Token 应译作「符」' -subtitle = '' -slug = '' -tags = [] -categories = '' -date = 2026-09-07T08:25:45+08:00 -draft = true +slug = 'token-translation' +tags = ["语言","Lisp","编程"] +categories = '软件杂谈' +date = 2026-09-07 23:37:10 +draft = false toc = true +fedi = 'https://akk.eltr.ac/objects/871e9998-56c8-4222-af8b-33c83d76b174' +++ -整场「要不要把 Token 翻译成词元?」的争论令我感到诡异,诡异的点在于所有参与者都只看到了 Token 作为大语言模型计价单位的意思,而 Token 一词存在于英语语言中已经有很久了。若把 Token 先前的释义也译作「词元」,场面会非常可笑: +整场「要不要把 Token 翻译成词元?」的争论令我感到诡异,诡异的点在于所有参与者都只看到了 Token 作为大语言模型计价单位的意思,而 Token 一词存在于英语语言中已经有很久了(对中文的影响也很大)。若把 Token 先前的释义也译作「词元」,场面会非常可笑: - 这套桌游里的某个词元不见了! - 请求体里必须携带词元才能通过验证。 - 在构造抽象语法树之前,要先把字符串词元化。(这倒还算接近的) -- 把你安全词元插入电脑的 USB-C 接口。 +- 把安全词元插入电脑的 USB-C 接口。 - 为保证数据安全,可以把敏感信息词元化。 - 用你的词元兑换优惠或者奖品。 <!--more--> -正如人们滥用「人工智能」一词指代「大语言模型」,因为他们只知道热度最高的技术,便让 “AI” 这个上世纪就出现的概念被人工智能的一个小分支占据一样,人们滥用 Token 一词,以至于他们不关心其他的意思。 +正如人们滥用「人工智能」一词指代「大语言模型」,因为他们只知道热度最高的技术,便让 AI 这个上世纪就出现的概念被人工智能的一个小分支占据一样,人们滥用 Token 一词,以至于他们不关心其他的意思。更何况「词元」这个意思用在 LLM 里其实相当不准确。 不过,也需要批评前人,Token 一词的翻译本身就很混乱。 -桌游里的 Token 是「指示物」的意思,还有人根据情况译作「代币」「标记」。除了 Token,用来表示人物、角色的指示物还被称作 *Meeple*(My People),音译作「米宝」。能有米宝来指代所有的指示物吗?当然不行!米宝的本意是人物,是 Token 里相当小的一类。同理,能用「词元」对应 Token 吗?不能!词元只指代相当小的范畴,你当然不能说桌游里的指示物是词元。 +## 桌游中的 Token + +桌游里的 Token 是「指示物」的意思,还有人根据情况译作「代币」「标记」。大富翁里用来标记地产的房子,飞行棋里的飞机,龙与地下城里的角色小人,都叫作 Token。 + +用来表示人物、角色的指示物还被称作 *Meeple*(My People),音译作「米宝」。能用米宝来指代所有的指示物吗?当然不行!米宝的本意是人物,是 Token 里相当小的一类。同理,能用「词元」对应 Token 吗?不能!词元只指代相当小的范畴,你当然不能说桌游里的指示物是词元。 不仅桌游里的代币是 Token,游戏厅里的游戏机和赌场里的老虎机使用的代币,也叫作 Token coin,不过如今更多地被 *ticket*(票券)取代了。 -在计算机中,Token 有三类。一类是用来表示「执行某种操作的权限」的对象,另一类是编译器和解释器中划分的语法单元,还有一类是自然语言处理(NLP)中划分的词法记号(Lexical token)或别的语言单元——在讲第三类的时候,我会讲到「词元」这个翻译不仅犯了范畴问题,本身的表意也不准确。 +## 密码学中的 Token -第一类 Token 通常叫作「令牌」,往往与密码学相关。常见的场景是客户端输入账号密码之后,服务端生成一个(多半有期限)的凭证,一般是字符串,之后客户端只需要在访问时提供这个凭证就好,期限时间内不需要再次输入密码。这个字符串一般保存在 Cookies 里,服务端自动读取,或者客户端携带在 HTTP 请求头里: +密码学中的 Token 通常叫作「令牌」。常见的场景是客户端输入账号密码之后,服务端生成一个(多半有期限)的凭证,一般是字符串,之后客户端只需要在访问时提供这个凭证就好,期限时间内不需要再次输入密码。这个字符串一般保存在 Cookies 里,服务端自动读取,或者客户端携带在 HTTP 请求头里: ``` Authorization: Bearer eyJhbGci...<snip>...yu5CSpyHI @@ -37,7 +41,13 @@ Authorization: Bearer eyJhbGci...<snip>...yu5CSpyHI `Bearer` 之后的部分就是 Token。最常见的令牌之一是 [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token),其他的应用场景也不少,不限于 HTTP 通信。任何能够在通信过程中,从密码学意义上验证用户身份的凭证字符串,都叫令牌。 -第二类 Token 在解析代码的过程中产生,解析(parse)还没有到编译(compile)或者解释(interpret)的地步,无论编译器还是解释器都有这个步骤。仔细想想,源代码只是储存在文件里的文本而已,可以当作字符串处理,解释器和编译器是怎么把字符串变成有意义、可执行的语句的?我们可以现场写个 Lisp 解释器来理解其过程。 +顺带一提,令牌可以当作是时效性很强的密钥,如果泄露,攻击者就可以在令牌过期之前使用你的身份访问相关资源。令牌往往存放在 Cookies 里,这也是为什么[第三方 Cookies](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Third-party_cookies#what_is_the_problem_with_third-party_cookies)(或者说跨域 Cookies)常常受人诟病,现代浏览器也采取了很多限制。 + +还有一种密码学方法叫作令牌化([Tokenization](https://en.wikipedia.org/wiki/Tokenization_(data_security))),指代把敏感信息替换为不敏感的等价物(即 Token)的过程。我对此了解不多,就不闹笑话了。 + +## 编译器和解释器中的 Token + +这类 Token 在解析代码的过程中产生,解析(parse)或者预处理(preprocess)还没有到编译(compile)或者解释(interpret)的地步,无论编译器还是解释器都有这个步骤。仔细想想,源代码只是储存在文件里的文本而已,可以当作字符串处理,解释器和编译器是怎么把字符串变成有意义、可执行的语句的?我们可以现场写个 Lisp 解析器来理解其过程。 程序员极有可能接触过简单的解释器,比方说,如何给下面这个算式字符串求值? @@ -47,7 +57,8 @@ Authorization: Bearer eyJhbGci...<snip>...yu5CSpyHI 答案是构造抽象语法树(AST)。不难发现,上述算式可以表示为树形结构,`(1 + 3)` 是一个整体,`4 * 7` 因为乘法的优先级更高,也作为一个整体,那么就可以把 `-` 当作根,前面两个整体作为 `-` 的左右子树。继续递归,不难发现 `(1 + 3)` 是 `+` 为根,`1` 和 `3` 为叶子结点的子树,而 `4 * 7` 是 `*` 为根,`4` 和 `7` 为叶子结点的子树。 - + +{.dark:invert} 接下来只要深度优先遍历 AST 就好了,不难发现结点有两类,非叶子结点是操作符,叶子结点是数字。遍历时若是遇到数字,就直接返回值;若是遇到操作符,就根据操作符计算左右子树的值。在一系列奇妙的递归之后,程序就能求出这棵树的值为 `-24`。 @@ -59,7 +70,7 @@ Authorization: Bearer eyJhbGci...<snip>...yu5CSpyHI 也就是说,把中置操作符换成**前置**操作符,把所有的括号**显式**地写出来。如果把 `()` 看成列表,前置操作符的写法其实是嵌套的列表,而嵌套的列表就是树。这样一来,把字符串解析成 AST 的算法就好写多了。 -假设我们现在不仅仅是要表示算式,要表示程式…… +假设我们现在不仅仅是要表示算式,而是要表示程式…… ```lisp (format t "it's ~a~%" @@ -68,5 +79,181 @@ Authorization: Bearer eyJhbGci...<snip>...yu5CSpyHI 上面这段代码应该打印 `it's -24`。 - +同样,手动解析为 AST,变成下图的这个样子: + + +{.dark:invert} + +现在,非叶子结点是函数,叶子结点是函数的参数。读者可能会觉得把不同类型的数据都放在一起有点奇怪,和刚才的算式差得有点远。不过,只要想到深度优先遍历和递归求值的思路,就不会觉得奇怪了。只不过函数不仅仅是计算得出了值,还可能带有副作用。比方说 `format` 的副作用就是在指定的输出流(此处是 `stdout`)中打印文本。 + +手动遍历这棵树: + +1. 求值根节点 `format`,发现是函数,于是求它的子树。 +2. 求值 `format` 的左子树,发现是值,返回 `t`。 +3. 求值 `format` 的第二棵子树,发现是值,返回 `"it's ~a~%"` +4. 求值 `format` 的右子树 `-`,发现是函数,求它的子树。 +5. 求值 `-` 的左子树 `+`,发现是函数,求它的子树 +6. 求值 `+` 的左子树,发现是值,返回 `1` +7. 求值 `+` 的右子树,发现是值,返回 `3` +8. 结点 `+` 的子树都求值完毕,对它本身求值得到 `4` +9. 求值 `-` 的右子树 `*`,发现是函数,求它的子树 +10. 求值 `*` 的左子树,发现是值,返回 `4` +11. 求值 `*` 的右子树,发现是值,返回 `7` +12. 结点 `*` 的子树都求值完毕,对它本身求值得到 `28` +13. 结点 `-` 的子树都求值完毕,对它本身求值得到 `-24` +14. 结点 `format` 的子树都求值完毕,对它本身求值得到 `nil`,同时发生副作用:`"it's -24"` 被写入 `stdout` + +或许用一门非 Lisp 语言表述上面这个程序会更直观: + +```go +// 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\")")` 应该得到: + +```go +[]string{ + "(", + "format", + "t", + "\"Hello World\"", + ")" +} +``` + +之所以把括号留下,是因为它们需要作为标记传给 `parse()` 函数。一遇到 `(`,就认为下一个是根,之后的一直到 `)` 都是子树。这个函数作为解析器,输入划分好的 Tokens,输出可以递归求值的 AST。`parse()` 具体怎么实现就不赘述了。 + +除了 Lisp,你还可以参照 C 语言编译器 `gcc` 的文档的 [1.3 Tokenization](https://gcc.gnu.org/onlinedocs/cpp/Tokenization.html) 这一节。Tokenization 发生在预处理(preprocess)阶段。 + +至此,你应该明白我说的第二类 Token 指什么了。这类 Token 准确来说叫作 [Lexical Token](https://en.wikipedia.org/wiki/Lexical_analysis#Token)(词法记号,或者说…… 词元)。词法的(lexical)和句法的(syntactic,或名词 syntax)指代不同的抽象层次,词法分析就是把字符串划分为词法记号(lexical token)的过程,而句法分析(更常见的说法是语法分析)则是判断词法记号的组合有没有犯句法错误(syntax error)。Token 是词法层次的实体,分析句法时候我们就默认 Token 已经变成 AST 中不可分割的结点了。至于语法(grammar),则是词法和句法的组合,不过编程语言一般不说 *grammar*,都默认使用 *syntax*(句法或语法)一词。 + +*真是为了一盘醋包了一场酣畅淋漓的饺子啊。* + +## 自然语言处理中的 Token + +接下来是第三类 Token,即应用在 NLP 领域的 Token。前面我们在处理编程语言,而 NLP 是对自然语言的处理。那么 NLP 中的 Token 是指 [jieba](https://github.com/fxsjy/jieba) 这类分词工具吗? + +的确,中文分词算法就是 NLP 分词的一种,叫作 *Word tokenization*。把英文句子打散成一个一个的单词,也属于这类分词。[^1] + +其他的分词方式还有: + +1. 字符分词(Character tokenization) +2. 子词分词(Sub-word tokenization) +3. 句分词(Sentence tokenization) +4. N 元分词(N-gram tokenization) + +第三个是不是看起来很不对劲?如果是以句子为单位,那为什么会叫作分词呢?这里的 Token 不是词法意义上的 Token,把 *tokenization* 译作「分词」显然不对,那么把 *token* 译作「词元」就更不对了。若是译作「词元」,难道不会和 *word* 混淆吗? + +上述老派的 NLP 方法,一般用在非 LLM 聊天机器人、搜索引擎的 Query 分析和词频统计中,根据用例不同选用不同的 Tokenization 方法。那么在更新的 NLP 技术,大语言模型中,Token 又指代什么呢? + +大语言模型使用的 Tokenization 方法与简单 NLP 类似,至少输入都是自然语言词句,但算法过程和输出完全不同。LLM 用到了字节对编码(Byte-pair encoding,BFE),最早在 1994 年提出的 BFE 算法如下。[^2] + +假设有形如 `aaabdaaabac` 的输入,其中 `aa` 这个字节对的出现频率很高,于是替换为整个字符串中没有出现过的其他字符,比如 `Z`,于是就变成了: + +``` +ZabdZabac +Z=aa +``` + +可见 `ab` 也出现了多次,替换为 `Y` + +``` +ZYdZYac +Y=ab +Z=aa +``` + +现在又有了多次出现的 `ZY`,替换为 `X` + +``` +XdXac +X=ZY +Y=ab +Z=aa +``` + +通过递归的字节对编码,我们把 `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,编译器和解释器划分的词法单元是 Token,NLP 中的字符、词语、句子和 N 元是 Token,LLM 中的字节对编码也是 Token。 + +把上面这些句子里的 Token 换成「词元」这个中文词,说得通吗?哪怕仅仅是在 LLM 的语境下,「词元」也词不达意。字节对编码显然不是词。 + +Token 不该译作「词元」,我想这没什么争议了。唯一可以译作「词元」的,只有计算机在做词法分析时得到的词法单元,但那也只是小部分而已。 + +英文里的 Token 本意是什么?它的[词源](https://www.etymonline.com/word/token)是原始印欧语词根 _*deik-_,意思是展示(*to show*),同源的古诺斯语 *teikn* 意味着星座和征兆(omen),古英语对应的单词 *tacen* 意思是指示、符号、证据和征兆。 + +星座和征兆都是符号或者说对某种符号的解读,符号就是指示含义或另一个物体的东西。*Token* 就是「指示另一个实体的东西」,至于这「另一个实体」是什么,要根据语境来看。 + +桌游里的 Token 指示游戏人物、生命值、财产等等,通行凭证或者说令牌指示访问某个资源的权限,编译器和解释器中的词法记号指示不能继续分割的词法要素,NLP 中的 Token 根据使用场景指示一层含义(词语表示的含义、句子表示的含义、N 个词语组成的短语或者说 N 元表示的含义),LLM 中的 Token 指示字节对,同时也对字节对的出现频率做统计学表示。 + +说到这里我想到一个有些偏门的例子,在《星露谷物语》的某些 Mod 里,由于缺乏标识「玩家是否完成某项任务」的机制,所以有的 Mod 会在玩家与某个 NPC 达成关键进度时,给予名为某某 Token 的物品,并提醒玩家要带着这个 Token 进入特定场景才会触发下一部分的剧情。这里的 Token 就指示「玩家已经达成了某项进度」。 + +说起通行凭证和令牌,不知道读者有没有看过早年间的古装剧。如果有的话,读者可能熟悉「兵符」这种东西: + +> 「符」一般分為兩半,右半在帝王,左半在將軍,調兵之時,使者帶符上朝驗合,才可行動。「符」形狀多種,以虎符為主;質料有銅、木、玉等,以青銅為多。 + +{{< right "——[漢語多功能字庫](https://humanum.arts.cuhk.edu.hk/Lexis/lexi-mf/search.php?word=%E7%AC%A6)" >}} + +通行凭证和令牌不就是「符」吗?词法记号和 NLP 中的 token 就更不用说了,语言本身就是符号。编程语言中的关键字、括号、字面量也是词法符号。==就算未来出现更多基于 *token* 本意衍生出的用法,它多半也是符号,是用来指示某个东西的,而汉语中同样表示「指示某个东西的东西」的词,就是「符」。== + +--- + +## 番外:语言学中的…… Lemma + +只有 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*…… + +[^1]: 参考:[Tokenization in NLP - geeksforgeeks](https://www.geeksforgeeks.org/nlp/nlp-how-tokenizing-text-sentence-words-works/) +[^2]: 参考:[Byte-pair encoding](https://en.wikipedia.org/wiki/Byte-pair_encoding) |
