词嵌入 入门
神经网络只吃数字,但文本是符号。把这两端接起来,这个领域花了好多年。 4 个短主题讲完这段路:one-hot 编码, 第一反应的做法、不可扩展;词嵌入(word embeddings)(Word2Vec、GloVe), 让所有现代 NLP 系统成为可能的「稠密向量」思想; 让大家爱上这个想法的著名直觉 —— king − man + woman ≈ queen; 以及静态词嵌入的局限 —— 每个词只有一个向量、不管上下文 —— 而下一篇 primer(Transformer)恰好就是来解这个的。
One-Hot 编码
最显然的第一招 —— 也是现在没人用的那一招。
神经网络只吃数字。文本是符号。把两端最便宜地接起来,就是one-hot 编码:选一个大小为 V 的词表, 每个词表示成长度 V 的向量,在它自己的位置上是 1、别的地方都是 0。 词袋(Bag-of-words)和 TF-IDF(文本 primer §1、§2)基本就是这些向量的聚合。
选一个小词表 —— ["the", "cat", "sat", "on", "mat"] —— 编码长这样:
the = [1, 0, 0, 0, 0] cat = [0, 1, 0, 0, 0] sat = [0, 0, 1, 0, 0] on = [0, 0, 0, 1, 0] mat = [0, 0, 0, 0, 1]
毛病在哪?三件事,按严重程度排:
- 稀疏又巨大。GPT-4 级别的词表大约
100,000个 token。 每个词都是 10 万维向量、只有一个非零位。每个向量的几乎全部都是浪费的存储。 框架支持稀疏表示,但 embedding 层本质上还是个巨大的V × d_embed表; one-hot 乘上这张表就只是查一行而已,所以现代代码里其实从不真的构造 one-hot 向量。 但概念上,每个 token 就是这么个东西。 - 没有相似度结构。任意两个不同的 one-hot 向量,余弦相似度都精确等于 0。 \"cat\" 和 \"kitten\" 之间的数学距离,跟 \"cat\" 和 \"calculator\" 之间一样。 除非模型见过两个词都出现过很多次相似上下文,否则它没办法知道它们是相关的。
- 不泛化。如果你用一百万句包含 \"dog\" 的话训模型、一句包含 \"puppy\" 的都没有,那模型知道关于 \"dog\" 的一切,却对 \"puppy\" 一无所知 —— 哪怕两个词几乎是一个意思。每个词都得从零学一遍。
Bag-of-words 和 TF-IDF 继承了这三个问题,还多加一个:词序完全丢光(文本 primer §2)。 让这个领域突然打开局面的修法,不是「更好地把 one-hot 们聚合起来」 —— 而是把 one-hot 本身换成一个小的稠密向量。这就是 §2。
在 Transformer 里:一个 token 进入 Transformer 的第一件事, 就是去 embedding 查表 —— 一张 (vocab_size, d_model) 的矩阵, 每一行是某个 token 那个(学到的、稠密的)embedding。 「one-hot 向量乘 embedding 矩阵」在数学上仍然成立,但实际上框架只去取对应那一行而已。 无论如何:one-hot 被降级成了一层薄薄的索引,而流入模型剩下部分的是下一节里的那个稠密向量。
词嵌入 —— Word2Vec 与 GloVe
学到的稠密向量,让相似的词彼此挨着。
如果每个词都是一个小的稠密向量 —— 比如 300 维实数 —— 而不是巨大的稀疏 one-hot, 会怎么样?如果这些向量是从「词在真实文本中怎么出现」学出来的、 让相似的词最终待在相似的位置 —— 又会怎样?这就是词嵌入(word embedding)的想法,它是一个转折点。
Word2Vec(Mikolov 等,2013)是那个让它出名的具体配方。 训练一个小神经网络,从中心词预测上下文(skip-gram)或反过来(CBOW)。 网络有一个 d_embed 大小的隐藏层。训练完丢掉预测头, 留下的隐藏层权重就是 embeddings —— 每个词一个向量。
Skip-gram,位置 t 的词:
input = embedding[w_t]
对窗口内每一个上下文位置 w_{t±j}:
预测「在 w_t 条件下 w_{t±j} 的对数概率」
隐藏层是共享的。预测信号把「出现在相似上下文里的词」
推向相似的 embedding。GloVe(Pennington 等,2014)用另一条路到了同一个地方: 把全局的「词-词共现矩阵」做低秩分解、找到能近似它的低维向量。 数学不同,实战中得到的 embedding 差不多。
两种方法都在利用一条语言学事实,常归给 John Firth:「看一个词的为人,看它跟谁在一起。」出现在相似上下文里的词倾向于意思相近; 如果你的损失函数奖励「能从一个预测另一个」,得到的向量就会自动捕捉到这种相似性。
这些性质让词嵌入大约 5 年里一直是默认表示:
- 稠密。典型尺寸:100 到 300 维。每一维都有意义、都非零。 跟 10 万维 one-hot 一比,内存少了好几个数量级。
- 相似词 → 相似向量。
cosine(v(\"cat\"), v(\"kitten\"))很大;cosine(v(\"cat\"), v(\"calculator\"))很小。 下游分类器只见过 \"cat\",也能免费学到点关于 \"kitten\" 的东西。 - 泛化。训练时见过 \"the cat sat\" 的模型, 有时也能合理处理 \"the kitten sat\",哪怕训练里根本没见过 \"kitten\" —— 因为 kitten 的向量就在 cat 旁边。
- 可迁移。在一个巨大的文本语料(维基百科、网页)上把 embedding 训一次, 下载下来、插到你的任务里。它们是 NLP 里最早一批「预训练件」, 也是现在所有「先预训练再微调」工作流的哲学祖先。
实战上要知道的:Word2Vec / GloVe 的 embedding 会直接把训练数据的偏见编码进去—— 职业的性别关联、种族刻板印象等等 —— 因为训练语料本身就有这些。 2010 年代早期的 NLP 文献有一大堆做「去偏」的论文。 到上下文化 embedding 时代这个问题也没消失,反而更微妙了。
在 Transformer 里:现代 Transformer 开头的那张(vocab_size, d_model) embedding 表,在功能上就是一个学过的、 类似 Word2Vec 的东西 —— 只是它跟模型其它部分一起、在语言建模损失上联合训练, 而不是独立的预处理步骤。从这张表里取出来的向量,就是 attention 的输入。 Word2Vec 的想法还活着,只是训练配方换了。
king − man + woman ≈ queen
让所有人爱上词嵌入的那个直觉。
Word2Vec / GloVe 的 embedding 编码的东西,比「相似词彼此相邻」还诡异 —— 它们编码方向。最著名的例子(几乎成了一个 meme):
v(king) − v(man) + v(woman) ≈ v(queen)
拿 \"king\" 的向量、减去 \"man\" 的向量、加上 \"woman\" 的向量 —— 你几乎正好落在 \"queen\" 的向量上。从 \"man\" 到 \"woman\" 的向量, 在 embedding 空间里是一个一致的方向,同一个方向应用在 \"king\" 上, 就落到 \"queen\"。性别是一个方向,不是一个坐标。
其他出乎意料经常成立的类比:
- 首都。
v(Paris) − v(France) + v(Italy) ≈ v(Rome)。 「是…的首都」这个方向在多个国家上都一致。 - 动词时态。
v(walking) − v(walk) + v(swim) ≈ v(swimming)。 \"-ing\" 这个变形成了一个可学的方向。 - 比较级。
v(big) → v(bigger)跟v(fast) → v(faster)是平行的。 - 单数 / 复数。
v(dogs) − v(dog) ≈ v(cats) − v(cat)。
为什么这能成立?Word2Vec 和 GloVe 训练时就在「把出现在相似上下文里的词,推向相似向量」。 \"King\" 和 \"queen\"除了「指代谁的性别」之外,出现的上下文很相似。 \"Man\" 和 \"woman\" 同样,大多数语境属性(主语、有意识、人物名词性)都一致、 在性别这一项上不同。所以在向量空间里,「性别轴」就是 man / woman 和 king / queen 以相同方式变化的那一维。向量减法把那一维分离出来,向量加法把它搬到别处。
诚实地说几个 caveat。这些类比不是总成立 —— 在精心挑过的对子、最干净、最高频的词上效果最好。 最近一些批判性分析指出,有些著名 demo 部分是「工程」出来的(原 demo 用的近邻规则把输入词 排除掉了,会偏向答案)。而且上下文化 embedding(§4 和下一篇 primer 的话题) 部分破坏了这个干净结构 —— 因为每次出现都有自己的向量,没有一个统一的 \"v(king)\" 可以指。 但核心观察站得住:embedding 空间里的方向是有意义的。
在 2013 年的某一刻,这件事很震撼。看起来这个领域无心插柳教会了计算机做类比推理。 后来我们了解到,真相更无聊也更有趣:计算机学到的是「近似共现统计」, 类比能力是这种统计规律的一个副作用。无论如何,这个 demo 是合适那种「爆款」 —— 它撑起了之后十年的进展。
在 Transformer 里:输入端的 embedding 矩阵仍然可训、仍然编码词的相似度、 在某种程度上仍然编码「你能用向量算术探测出来」的类比结构。但模型大部分知识 已经从静态 embedding 移到了 attention 和 FFN 层 —— 这就是 §4 要讲的事, 也是通往 Transformer 的桥。
为啥单纯的词嵌入不够
一个词无论有多少个意思、无论上下文是什么,都只对应一个向量。
Word2Vec 和 GloVe 是巨大的飞跃,在 NLP 顶端坐了好多年。但到大约 2018 年时, 它们的一个根本局限已经很清楚:一个词的每次出现拿到的都是同一个向量, 无论它在什么句子里。LLM 中的文本 primer §3 已经指过这件事;这里给最干净的演示。
考虑词 \"bank\":
- \"She sat by the river bank.\" —— 河岸,水边的一道斜坡。
- \"She walked into the bank.\" —— 一家金融机构。
- \"He works at the blood bank.\" —— 医学储血设施。
三个意思。Word2Vec 给它们一个向量。训练过程必须在这三种含义(还有更多)上做平均, 结果就是 \"bank\" 的 embedding 落在「河岸聚类」和「金融聚类」之间的某个位置 —— 对哪一类都没用。
静态 embedding 搞不定的事的完整清单:
- 多义词。\"bank\"、\"bat\"、\"Apple\"、\"rose\"、\"lead\" —— 字典里那些「一词多义」的整页内容,对 Word2Vec 是看不见的。
- 词表外的词(OOV)。词表是训练时固定的。新词 (\"ChatGPT\"、一个品牌、一个人名)根本没有 embedding。 子词切分(BPE、SentencePiece)能帮上忙(把未知词拆成已知片段),但仍是个绕路。
- 短语组合。\"New York\" 是一个城市,但
v(\"New\") + v(\"York\")是另一回事。 Transformer 之前的 NLP 得另起一条「短语检测」流水线才能把组合意义找回来。 - 语法角色。名词的 \"run\" 和动词的 \"run\" 拿到同一个向量。 模型只能靠下游从上下文里去消歧,embedding 这一层帮不上忙。
- 长距离上下文。就算根据前一段你提到了沿着密西西比河徒步, \"bank\" 应该是河岸的意思,静态 embedding 也没办法知道这件事。
修法简短地说:不要给一个词「embed 一次」然后就完事。要每个句子 embed 一次、 让它依赖于句子。每一次 \"bank\" 的出现都生成自己的向量,从周围 token 算出来。上下文化 embedding(contextual embeddings)。ELMo(2018)用一个双向 LSTM 架在词 embedding 上面做了这件事。BERT 和 GPT(2018)用 Transformer 做了。 下游任务上的提升如此巨大,以至于两年之内,所有 NLP benchmark 榜首都是上下文化模型 —— 静态 embedding 被降级成了「上下文模型的输入层」。
这个降级,就是现代 Transformer 的结构本身。第一层仍然是一张可学的 embedding 表 (本质上就是 Word2Vec,跟整模型联合训练)。架在它上面的几十层取这个静态向量, 通过关注序列里的其它 token,逐步把它上下文化。到最后一层, 你输入里位置 47 处的 \"bank\" 向量,依赖于序列里每一个其它 token —— 它没有固定的意思,只有「这一句里这一个的意思」。
这就是桥。静态词嵌入是 2013–2018 时代的正确答案 —— 稠密、有语义、可类比、漂亮。 它们就在一件事上栽了 —— 上下文 —— 而修那件事,就是下一篇 primer 的全部内容。
在 Transformer 里:在 embedding 表上面堆 attention 层的全部目的, 就是把每个 token 的「无上下文输入 embedding」转成「有上下文的输出向量」。 数据走到最后一层时,输入里的每一个 \"bank\" 都有一个向量,反映出它是河岸、银行、还是血库 —— 这些区别足够鲜明,下游层能对每一个采取不同的处理方式。