JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

把文字变成数字

在任何语言模型开始"思考"一句话之前,这句话必须先变成数字。本指南带你走过那个没人炫耀、却至关重要的第一步——把文本切成词元、建立一份词表,再把一串整齐的整数交到模型手里。

模型只懂数字

在前面的阶梯里你已经知道,神经网络本质上是一叠矩阵乘法——它吃进数字向量,再吐出数字向量。它没有任何位置可以放下字母 *q* 或单词 *cat*。所以任何文本系统要做的第一件事,就是把人类的文字转换成数学能咀嚼的数字。这个转换是所有自然语言处理朴实无华的大门,而它做得好不好,决定了后面的一切。

关键在于,文本是*一串离散的符号*,而不是连续的量。21.5 度的气温本身就是一个数字;但单词 *however* 不是。于是我们玩一个两步走的游戏:先把文本切成若干单位(这就是分词 / tokenization),再通过查一张固定的列表,把每个单位映射成一个整数 ID。这些整数本身还不带任何含义——它们只是地址。含义是后面才挂上去的:每个 ID 会去索引一张嵌入表,而这张表是网络在训练中学出来的。

该在哪里切?词、字符,以及两者各自的麻烦

最直觉的做法,是按空格切开,把每个*单词*当作一个词元。早期的 NLP 正是这么做的,它也天然契合你之前见过的那些基于计数的方法——词袋TF-IDF 都假设有一份干净的单词清单。但词级分词有两个顽固的毛病。第一,词表会爆炸:单是英语,把复数、时态、拼写错误和人名都算上,就有数百万种词形。第二,在测试时你*总会*遇到清单里从没出现过的词——令人头疼的"未登录词"问题——而纯词级模型别无选择,只能把它当成一个统一的"未知"符号扔掉。

另一个极端,是把*字符*当作词元。这样词表就很小(几百个符号),而且永远不会有未登录词——你能拼写出任何东西。代价是序列变得非常长,而且模型得从零重新学会:字母 *c-a-t* 往往是结伴出现的。对许多书写系统来说还更糟:中文、日文、泰文里根本没有空格可切,所以"直接用词"从一开始就不是个选项。这个领域需要一条折中的路。

留意一下,这其实是一个乔装打扮过的权衡。词元信息密度高,但既脆弱又庞大;字符元稳健又紧凑,却逼出又长又底层的序列。像去掉停用词(那些基于计数的方法最爱丢掉的 *the*、*of*、*and*)这类预处理,对统计词频是有意义的,但它会实实在在地伤害需要每个词来建模流畅语言的现代模型。最终胜出的答案是:把高频词整个保留,把罕见词拆成碎片。

子词:字节对编码取一个折中

占主导地位的折中方案是子词分词,其中最有名的配方是字节对编码BPE)。这个想法妙在简单,借自上世纪九十年代的一个数据压缩小技巧。先把所有文本拆成单个字符。然后反复地找出出现频率最高的相邻符号对,把它合并成一个新的单一符号。这样做上几千次,*th*、*ing*、*tion* 这类常见片段,以及像 *the* 这样的高频整词,就会自然而然地结晶成各自独立的词元;而一个罕见词则被留作若干更小的碎片。

start: l o w e r _   n e w e s t _
# most frequent pair is (e, s) -> merge
step1: l o w e r _   n e w es t _
# next most frequent pair is (es, t) -> merge
step2: l o w e r _   n e w est _
# ... after many merges:
final: low er    new est
# 'lower' -> [low, er]   'newest' -> [new, est]
微缩版 BPE:合并出现频率最高的相邻对,如此反复。罕见词最终化为可复用的碎片,而不是一个单一的"未知"。

这一个想法一举解决了前面的两个问题。基本上不再有未登录词了:最坏情况下,一个怪词会退回到它的字符甚至原始字节,所以*任何*字符串都能被编码。而词表保持着固定、可控的规模——通常是 3 万到 10 万个词元——这个大小你在一开始就通过"决定做多少次合并"来确定。BPE 的近亲(BERT 家族用的 WordPiece,以及 Unigram/SentencePiece)在*如何*挑选碎片上有所不同,但它们共享同样的精神:在字母和单词之间,学出一块中间地带。

词表,以及通往模型输入的完整流水线

训练 BPE 的产物是一份词表:一张有序的列表,给每个词元分配一个固定的整数 ID,外加那一串合并规则。这份词表一旦定下来就被冻结,随模型一起发布——编码和解码都是确定性的查表,而非学习。还会留几个特殊词元占据保留席位,比如用来把短序列补齐的填充词元,以及标记一段文字开头或结尾的标记。解码不过是反向操作:拿到模型输出的 ID,查出它们对应的碎片,再粘回成文本。

  1. 规范化:若模型需要就转小写,修正 unicode 的怪癖,有时去掉重音——轻量的、因模型而异的清理。
  2. 分词:套用学好的合并规则,把文本切成子词词元。
  3. 映射成 ID:在词表里查每个词元,得到它的整数 ID。
  4. 填充或截断:把序列弄成固定长度,好让许多样本堆叠进一个批次。
  5. 嵌入:每个 ID 去索引一个学出来的向量;至此网络终于有了可以运算的真实数字。

那个"从 ID 到向量"的步骤值得停下来想想,因为它直接连向本阶梯接下来的几篇指南。从数学上看,取出嵌入表的第 *i* 行,和用一个独热向量去乘那张表是完全等价的——整数 ID 不过是为某一行起的一个紧凑名字。出来的那些向量,就是大名鼎鼎的词向量;word2vecGloVe 是早期独立学习它们的方法,你接下来就会遇到。现代模型则干脆把嵌入表和其余一切一起联合学出来。

为什么这个枯燥的步骤暗中要紧

我们很容易把分词当成管道工程,然后就略过它,但它的种种选择会渗进每一个角落。正因为词元不是单词,模型在需要"字符级视力"的任务上会犯难:数一个词里有几个字母、把字符串倒过来、或者做那种数字被尴尬切开的算术。这些往往不是深层推理的失败——而是分词留下的痕迹。同理,分词器没在其上训练过的语言会被切成多得多的词元,于是它们更费钱、在上下文窗口里塞得更少,表现也可能更差。这是一个真实、可量化的公平性问题,而不是传言。

这里有一份值得保持的清醒。子词分词是一个巧妙的工程把戏,而不是一套语言理论——它根本不知道"词素"是什么,它找出的那些碎片(*est*、*tion*)也只是有时才对得上真正的语言单位。研究者们不断在问:能不能干脆把它整个去掉,直接喂给模型原始字节?确实有少数系统这么做,代价是序列更长。但就目前而言,BPE 式的分词仍是那个安静的、近乎通用的第一步——它把你的句子,变成大语言模型真正吃进去的那一串整数。