嵌入:词语如何变成输入

14 阅读14分钟

如果说 softmax 是把分数变概率的桥,embedding 就是把词变向量的桥。所有现代 NLP 模型的第一步都是 embedding,但它的设计直觉、历史演化和工程取舍很少被系统讲清楚。本文试着把这条线一次讲透。

写这篇时我特意翻了一下 1957 年 Firth 的那篇老论文,又翻了 2013 年 Mikolov 的 word2vec、2014 年 Pennington 的 GloVe、2018 年 Peters 的 ELMo 和 Devlin 的 BERT。半个多世纪的时间跨度,不同时代的人用截然不同的方法在解决同一个问题:「怎么用一组数字来表示一个词」。这个问题的答案变了几次,但核心思路始终在「分布假设」上。

读完本文,你应该能理解:embedding 不是一个孤立的工程技巧,而是一条从语言学假设、统计学方法、神经网络训练一路演化的概念线。理解了这条线,再看 Transformer 的 embedding 层、tied weights、subword 这些细节,就不会觉得它们是「凭空出现」的设计。

Embedding 演化路径转存失败,建议直接上传图片文件

原文链接

零、先认几个词

这篇会反复出现一些机器学习词汇,先不用急着背定义,把它们理解成下面这些朴素对象就够。

向量就是一串数字,例如 [0.2, -0.7, 1.1]。在 NLP 里,一个词、一个句子、一个段落都可以被表示成向量。向量的好处是可以计算:两个向量能算距离、算夹角、做加减法,也能送进模型继续处理。

矩阵就是一张数字表。embedding 矩阵可以想成一本「词向量字典」:第 1234 行存着 cat 这个 token 的向量,第 5678 行存着 dog 的向量。所谓 lookup,就是按 ID 去这张表里取一行。

参数就是模型内部会被训练改变的数字。普通程序的规则由工程师手写,神经网络的许多规则藏在参数里。训练前这些数字通常是随机的;训练后,它们会变成一套对任务有用的数字表。

训练就是不断让模型做题、看答案、改参数。比如让模型看到 the cat sat on the,猜下一个词;猜错了,就根据错误方向微调参数。这个「错误有多大」叫 loss,把 loss 往小调的过程叫优化。

反向传播可以先理解成一种自动算账方法:最后答案错了多少,会沿着计算链路倒着分摊给每个参数,告诉它「你该往哪个方向改一点」。本文不展开推导,只需要知道 embedding 矩阵里的每一行也会通过这种方式被更新。

logit / score / 分数是一回事:模型在变成概率之前吐出的原始分数。分数可以是任意实数,不要求加起来等于 1。softmax 负责把一堆分数变成概率分布,上一篇已经专门讲过。sigmoid 则是把一个分数压到 0 到 1 之间,常用来做「是 / 不是」这种二分类判断。

梯度下降 / SGD / Adam都是改参数的方法。可以把它想成在山坡上找最低点:loss 是海拔,参数是你站的位置,梯度告诉你往哪里下坡。SGD 每次只看一小批样本,Adam 是更会调步子的版本。

有了这些直觉,后面再看到「embedding lookup」「矩阵分解」「negative sampling」「weight tying」就不会那么陌生。它们本质上都围绕同一件事展开:怎样把词变成一串有用的数字。

一、为什么需要 embedding:从 one-hot 的痛苦说起

1.1 计算机眼里的词是什么

打开任何 NLP 任务的代码,第一件事都是「分词 + 建词表」。设词表大小是 VV(典型值:3×1043 \times 10^41×1051 \times 10^5)。每个词获得一个唯一整数 ID,比如 cat = 1234

但神经网络不能直接处理整数 ID。整数有大小关系(1234 < 1235),但词语之间没有这种「序」(cat 不该比 cap 大)。把 ID 当数字喂进去会让网络学到错误的归纳偏置。

这里的「不能直接处理」不是说计算机会报错,而是说这个输入形式会误导模型。模型只会看到数字 1234、1235、9000,却不知道这些数字只是编号,不是大小、温度、价格那样有连续意义的量。ID 越接近,词义不一定越接近;ID 差得远,词义也不一定差得远。

最朴素的解决办法是 one-hot 编码:把 ID 1234 变成长度 VV 的向量,第 1234 位是 1,其他都是 0。这样每个词都是一个独立的「方向」,没有大小关系,互相正交。

把它想成一个有 VV 个抽屉的柜子:cat 只打开第 1234 个抽屉,其他抽屉全关;dog 只打开另一个抽屉。这样确实避免了「ID 大小」的问题,但也意味着每个词都孤零零地站在自己的抽屉里,互相之间没有任何语义联系。

如果把 one-hot 写成真正的计算,它是这样的:先准备词表 token -> id,再把 id 映射成标准基向量 ei\boldsymbol{e}_i。词表大小为 V=5V=5 时,cat=2 就是 [0, 0, 1, 0, 0]。如果下一层有一个矩阵 WRV×dW \in \mathbb{R}^{V \times d},one-hot 乘矩阵就是:

eiW=W[i,:].\boldsymbol{e}_i^\top W = W[i, :].

也就是说,one-hot 本身不是在“学习语义”,它只是在做一件非常机械的事:把离散 ID 变成“取第几行”的选择器。后来的 embedding lookup 也是这个动作,只是工程上不真的构造那条几万维 one-hot 向量,而是直接按 ID 查表。

1.2 one-hot 的三个致命问题

第一个问题是维度灾难V=5×104V = 5 \times 10^4 时,每个词是 5 万维的向量。一句话 100 个词就是 100 个这样的向量。存储和计算都很恐怖。

第二个问题是稀疏性。one-hot 向量 99.998% 都是 0。把这种稀疏向量送进神经网络的全连接层,等价于「查矩阵的某一列」——这个操作不需要乘以一整个矩阵,只需要查表。这意味着 one-hot 把神经网络的功能浪费了。

第三个问题,也是最致命的:所有词之间距离相同。任意两个 one-hot 向量的欧氏距离都是 2\sqrt{2},余弦相似度都是 0。模型完全不知道 catdog 应该比 catapple 更相似。这意味着 one-hot 抹掉了所有语义信息。

one-hot 与稠密 embedding 对比转存失败,建议直接上传图片文件

上图里,one-hot 的每一行几乎全是空白,只有一个红格子表示「这个词就是它」。稠密 embedding 则像一条短短的温度条,每个维度都有实数值。catdog 的颜色模式相近,表示它们在向量空间里靠得近;apple 的模式不同,表示它离动物语义更远。

1.3 我们到底想要什么样的表示

把上面的问题翻一翻,「想要的表示」就清楚了:

第一,低维。用几百维(典型 256~1024)就够,不用几万维。

第二,稠密。每一维都有非零值,每一维都参与计算,矩阵乘法发挥作用。

第三,语义距离有意义。相似的词在向量空间里靠近,不同的词远离。这是核心。

满足这三条的表示叫「分布式表示」(distributed representation)或「dense embedding」。这个目标是清楚的,但怎么训出这种表示?这就是 70 年来的故事。

「分布式」这个词容易吓人,它不是分布式系统里的多机器部署,而是说一个概念不是由某一个维度单独表示,而是分散在很多维度里共同表示。比如「猫」可能在某些维度上体现动物性、宠物性、毛茸茸、会叫、常和人类生活在一起;没有哪一维单独等于「猫」,但这些维度合起来就能把它和别的词区分开。

1.4 一个历史小注

「embedding」这个词本身在数学里有「嵌入」的含义:把一个对象嵌入到另一个空间。比如群嵌入到群、流形嵌入到欧氏空间。NLP 借用了这个词:把离散的词「嵌入」到连续的向量空间。

最早系统化用「embedding」这个词的论文之一是 Bengio 2003 的「A Neural Probabilistic Language Model」。在他的框架里,词被嵌入到一个低维向量空间,再用神经网络做语言建模。这是后来所有 word embedding 工作的祖宗。

但「分布式表示」这个想法可以追溯到更远——Hinton 1986 的「Learning representations by back-propagating errors」就在讨论分布式表示的概念。那时候还没有大规模训练 NLP 模型的能力,但思想已经在了。

1.5 一个直觉性的目标

设想我们最终成功训出了 embedding。那它应该满足这种性质:

embedding("king") - embedding("man") + embedding("woman") ≈ embedding("queen")

这是 Mikolov 2013 那篇 word2vec 论文里最经典的结果。它告诉我们:embedding 不仅捕捉了「相似性」,还捕捉了「类比关系」。

举个几何坐标的简单例子: 假设我们的词向量被浓缩到了只有两个维度:维度 1 代表**“性别倾向”(0代表男性,1代表女性),维度 2 代表“权威地位”**(0代表平民,1代表君主)。

  • man(男人)的坐标大概是 [0.0, 0.0]
  • woman(女人)的坐标大概是 [1.0, 0.0]
  • king(国王)的坐标大概是 [0.0, 1.0]

那么 king - man + woman 的坐标计算就是: [0.0, 1.0] - [0.0, 0.0] + [1.0, 0.0] = [1.0, 1.0] 而在空间中 [1.0, 1.0] 这个位置(女性 + 权威),自然就对应着 queen(女王)。这说明 king - man + woman = queen 并不是数据的巧合,而是 embedding 空间内禀的几何抽象结构在起作用。

二维语义空间示意转存失败,建议直接上传图片文件

真实 embedding 往往有几百到几千维,肉眼看不见。上图故意压成二维,只为了说明「方向」的直觉:从 man 指向 woman 的箭头大致表示性别方向;从 man 指向 king 的箭头大致表示王室身份方向。把同样的王室方向加到 woman 上,就会走到 queen 附近。

第一次看到这个结果的时候人们是震惊的——一个用「预测周围词」训练出来的向量空间,竟然天然包含了「性别」「皇室」等抽象概念,而且这些概念表现为线性子空间。这是 NLP 研究范式的一次转折。

1.6 从 LSA 到 dense embedding 的过渡

在神经网络之前,NLP 用过另一条路径:Latent Semantic Analysis(LSA / LSI,Deerwester 1990)。LSA 的做法是:构建词-文档共现矩阵,做奇异值分解(SVD),保留前 kk 个奇异值方向,得到每个词的 kk 维向量。

LSA 的向量也是低维稠密的,也能表达语义相似(比如「car」和「automobile」会靠近)。本质上 LSA 是一种线性的 embedding 方法。

word2vec 出来之后大家发现:word2vec 的非线性优势其实不大,很多场景下 LSA 也能做。Levy & Goldberg 2014 的工作甚至证明 SGNS(Skip-gram + negative sampling)在数学上等价于 PMI 矩阵的隐式分解——也是一种矩阵分解。

这给我们一个 humbling 的启示:很多看起来「神经网络才能做」的事情,其实 30 年前的线性方法已经在做了。神经网络的优势在 scale 和灵活性,不在「线性 vs 非线性」这个对立。

1.7 embedding 与 representation learning

更广义地,embedding 是 representation learning(表征学习)的一个特例:把原始数据(离散符号、图像像素、音频波形)映射到一个适合下游任务的向量空间。

图像有 image embedding(CNN/ViT 的中间层输出),音频有 audio embedding(wav2vec),代码有 code embedding,分子有 molecular embedding,蛋白质有 protein embedding。所有这些都是同一个思路的具体化:把「原始的、离散的、不规则的」对象变成「连续的、规则的、可计算的」向量。

这个统一的视角让 embedding 不再仅仅是 NLP 概念,而是机器学习里普遍的「数字化」工具。

这里的「适合下游任务」很重要。embedding 不是为了让数字本身好看,而是为了让后面的任务更容易做:分类时相似样本更容易被分到一起,检索时相关文本更容易被找出来,生成时模型更容易知道下一个词该往哪个语义方向走。

二、Distributional Hypothesis:一切的起点

2.1 Firth 那句著名的话

1957 年,英国语言学家 J. R. Firth 写下:

"You shall know a word by the company it keeps."

直译:你能从一个词的伙伴中认识它。意思是:一个词的意义,由它经常和哪些词一起出现来决定。

这听起来朴素,但是一个深刻的转折。在 Firth 之前,主流的语义学走的是「哲学定义」路线——给「猫」下定义需要列出「四足、有毛、会捉鼠、家养」等属性。Firth 说:你不需要定义,只要观察这个词周围出现什么词就行。

「猫」周围常出现「miaow」「fur」「mouse」「pet」;「狗」周围常出现「bark」「fur」「bone」「pet」;两者周围词重叠很多,所以「猫」「狗」语义相近。「苹果」周围常出现「fruit」「red」「eat」,跟「猫」「狗」重叠少,所以语义远。

Firth 这句话之所以被引用了无数遍,是因为它把一个抽象的语义学问题变成了一个可计算的统计学问题。在那个没有大数据、没有大算力的年代,他的话更像是一种远见,要再过 50 年才被工程化兑现。

2.2 这个假设有多大胆

Firth 的假设把「语义」从「内涵」转成了「外延的统计」。这是一个非常大胆的简化——它实际上把语义全部还原为词的共现统计。

这种还原在哲学上有问题:「上帝」这个词的语义能从它的共现词学出来吗?「自由」呢?「正义」呢?显然,对这些抽象概念,单纯的共现统计可能学到的只是这些词在某种文本里的「使用模式」,而不是它们的「真实语义」。

但实践上,Firth 的假设非常好用。计算机能处理大量文本,能精确统计共现,能把这些统计变成向量空间。这个假设把「语义建模」变成了「统计学习」,从此 NLP 不再依赖人工词典和规则。

2.3 Harris 的版本

跟 Firth 几乎同时代,美国语言学家 Zellig Harris 在 1954 年也提出了类似的假设。他用更数学化的语言:

「在相同的语言环境里出现的词,倾向于有相似的意义。」(Distributional structure)

Harris 还指出可以用矩阵代数来分析这种共现关系——在 60 年前他就预见了用线性代数处理语义的可能。这条线后来催生了「distributional semantics」整个研究领域,最终在 word2vec、GloVe 这些工作里开花结果。

2.4 共现矩阵的雏形

把分布假设具体化成算法的第一步是「共现矩阵」。建一个 V×VV \times V 的矩阵 MMMijM_{ij} 是词 ii 和词 jj 在某个上下文窗口里共现的次数。

「上下文窗口」就是以某个词为中心,向左、向右各看几个词。窗口大小为 2 时,句子 the cat sat on mat 里,sat 的上下文就是 the, cat, on, mat。窗口越小,统计越偏局部搭配;窗口越大,统计越偏主题相关。

举个局部窗口共现情况的例子: 假设手头只有 3 句话:“I like Apple”、“I like Deep Learning”、“Apple is good”。如果把上下文窗口长设为 1(只往左看一格,右看一格),关于 likeApple 这个小矩阵片区域长这样(简化版):

IlikeAppleDeep...
I0200...
like2011...
Apple0100...

你可以把上面的每一行直接当成那个对应词的粗糙向量表示(比如 like 的向量就是 [2, 0, 1, 1...])。但这带来一个大问题:矩阵太大(真实场景有几万到几十万列)、太稀疏(绝大多数交叉格子都是 0,白占内存)、且容易被高频无意义虚词主导(比如「the」和所有词都频繁共现,导致它的数值霸占焦点,抹杀了区分度)。

后续工作的核心都是想办法让这个共现矩阵「变好」:用 PMI(pointwise mutual information)替代原始计数解决高频词主导问题;用 SVD 降维解决稀疏性和维度问题;用预测式的损失(word2vec)替代统计式的拟合(共现矩阵)。

共现矩阵本身的算法并不神秘,真正跑起来大概是这几步:

  1. 先分词,统计词频,过滤太低频的词,得到词表。
  2. 准备一个稀疏字典 counts[(center, context)],初始为空。
  3. 扫语料中的每个位置 tt,把当前位置词当作中心词 wtw_t
  4. 向左、向右各看窗口大小 kk,例如 k=2k=2 时看 t2,t1,t+1,t+2t-2,t-1,t+1,t+2
  5. 每看到一个上下文词 wcw_c,就执行 counts[(w_t, w_c)] += weight。最简单的 weight=1;GloVe 常用距离衰减,离中心越远权重越小,比如 weight=1/distance
  6. 扫完后,把这个稀疏字典视作矩阵 MM 的非零项。

所以“共现矩阵”不是一个抽象名词,它就是一次滑窗计数。它没有反向传播,也没有优化器,只有统计。它的训练数据是语料,输出是一个稀疏表:哪些词经常在窗口里互相撞见。

2.5 PMI 与 PPMI

直接用共现次数有偏。比如 (the, cat) 共现 1000 次,(cat, miaow) 共现 100 次。表面上前者更紧密,但「the」太常见了,跟所有词共现都多。真正的「关联强度」要看「比独立期望多多少」。

PMI(pointwise mutual information):

PMI(i,j)=logP(i,j)P(i)P(j).\mathrm{PMI}(i, j) = \log \frac{P(i, j)}{P(i) P(j)}.

正值表示「共现比独立期望多」,越大关联越强;负值表示「共现比独立期望少」,几乎不重要。负值常常被截断为 0,得到 PPMI(positive PMI)。

这个公式里的 P(i,j)P(i, j) 是两个词一起出现在窗口里的概率,P(i)P(j)P(i)P(j) 是「如果两个词完全没关系,只是各自随机出现」时的期望概率。PMI 比的就是这两件事:真实共现概率有没有明显超过随机碰上的概率。

举个极小的例子:thecat 共现次数很多,但 the 本来就到处出现,所以它和 cat 的关系未必强;miaowcat 共现次数少得多,但 miaow 平时很少出现,一旦出现又常常靠近 cat,所以 PMI 反而高。

PMI 的实际计算也可以按表格做:先从共现矩阵里得到总共现次数 N=i,jMijN=\sum_{i,j}M_{ij},再估计

P(i,j)=MijN,P(i)=jMijN,P(j)=iMijN.P(i,j)=\frac{M_{ij}}{N}, \quad P(i)=\frac{\sum_j M_{ij}}{N}, \quad P(j)=\frac{\sum_i M_{ij}}{N}.

然后把这三个数塞进 PMI 公式。PPMI 再多做一步:

PPMI(i,j)=max(PMI(i,j),0).\mathrm{PPMI}(i,j)=\max(\mathrm{PMI}(i,j),0).

这里仍然没有“训练”意义上的迭代优化,只有统计估计。它的关键风险是低频噪声:如果两个罕见词碰巧共现一次,PMI 可能被抬得很高。因此实际系统常会过滤低频词、做平滑,或者只保留足够可靠的非零项。

原始共现次数与 PMI 的区别转存失败,建议直接上传图片文件

PPMI 矩阵是早期分布式表示的基石。Levy & Goldberg 2014 证明了 word2vec 在某种意义下等价于隐式分解 PMI 矩阵。这条线索把现代神经词向量和经典统计语义连接到了一起。

2.6 SVD 降维

有了 PPMI 矩阵 MRV×VM \in \mathbb{R}^{V \times V},再做 SVD:M=UΣVM = U \Sigma V^\top,取 UU 的前 dd 列就是词向量。这就是 LSA(Latent Semantic Analysis,1990 年提出)的思路。

SVD 本身做的事是把一个大矩阵拆成三块:UU 存“行方向”的主轴,Σ\Sigma 存每个主轴的重要程度,VV^\top 存“列方向”的主轴。保留前 dd 个最大奇异值,就是只保留最重要的 dd 个语义方向,丢掉噪声和细碎共现。

LSA 的完整流程可以写成这样:

  1. 建词表和文档表,得到词-文档矩阵或词-词共现矩阵。
  2. 把原始计数改成更合理的权重,常见做法是 TF-IDF 或 PPMI。
  3. 对这个矩阵做截断 SVD,只保留前 dd 个奇异值。
  4. ii 的向量取 Ud[i,:]ΣdαU_d[i,:]\Sigma_d^\alpha,常见 α=0\alpha=01/21/2。乘不乘 Σ\Sigma 是实现选择:乘上会让更重要的方向权重大一些。
  5. 下游比较时通常先 normalize,再算余弦相似度。

这里也没有神经网络训练,SVD 是一次矩阵分解。它的“学习”来自矩阵本身:共现统计已经把语义关系写进了表里,SVD 只是把这张巨大稀疏表压缩成低维稠密坐标。

LSA 在 word2vec 之前是分布语义的主流方法。它的优点是数学清晰、有理论保证;缺点是 SVD 计算昂贵、在线更新困难、捕捉不到非线性关系。

2.7 共现矩阵的稀疏挑战

理论上 V×VV \times V 共现矩阵有 V2V^2 项,但绝大多数是 0。V=100kV = 100k 时有 101010^{10} 个项,存不下;幸好实际非零项可能只有 10710^7 量级,存稀疏矩阵就够了。

但稀疏矩阵的 SVD 比稠密 SVD 麻烦得多:你需要 Lanczos 迭代或类似的稀疏特征值方法,工程实现复杂。这是 1990 年代信息检索领域的一个研究方向,催生了 ARPACK 这样的稀疏特征值库。

word2vec 的优势之一就是绕过这个问题:不显式构建共现矩阵,直接用滑窗在语料上跑 SGD,每步只看一对词。这在工程上简单得多。

2.8 PMI 的对称性与 directionality

PMI(x, y) = PMI(y, x),对称。这意味着「以 x 为中心、y 在窗口内」和「以 y 为中心、x 在窗口内」的统计是同一回事。

但有些语言任务关心方向:「主语 → 谓语」和「谓语 → 主语」语义不同。简单 PMI 抹掉这个信息。后来一些工作(比如 dependency-based embedding)显式引入方向,但主流仍然是无向的——大数据下,方向信息可以靠 word order 在更大模型里学回来。

word2vec 出现后 LSA 一度淡出,但近年又回归——人们意识到 word2vec 训出来的向量其实跟 SVD 分解 PPMI 的结果惊人相似,只是 word2vec 用了更高效的训练方法。

三、Word2Vec:神经网络方法的开端

3.1 Mikolov 的工作

2013 年,Tomas Mikolov 等人在 Google 发布了 word2vec。这是 NLP 的一次范式革命:第一次有了一个能在大规模语料(几十亿词)上高效训练高质量词向量的方法。

word2vec 不是一个模型,是两个模型 + 两个加速技巧的组合:

  • 两个模型:CBOW(Continuous Bag-of-Words)和 Skip-gram。
  • 两个加速:Hierarchical Softmax 和 Negative Sampling。

我们逐个讲。

先把「神经网络方法」这几个字放轻一点。word2vec 里的网络非常浅,远没有今天的大模型复杂:它基本就是一张 embedding 表、一次简单的平均或点积、再接一个预测目标。它的厉害之处不在结构复杂,而在训练目标抓住了分布假设,并且能在大语料上跑得很快。

值得提的是,Mikolov 在做 word2vec 之前已经在 RNN 语言模型上做了好几年研究(他的博士论文就是 RNNLM)。word2vec 的诞生不是凭空冒出来的,而是他从 RNN 语言模型的「副产物」(中间层的词向量)反思而来——「能不能不要这么复杂的模型,直接用一个最简单的目标训词向量?」

这种「极简化」的思路在工程上特别有价值。后来人们意识到,word2vec 的成功很大程度上来自「简单到足以在大数据上跑起来」,而不是模型本身有多巧妙。这是工程实践里反复出现的教训:能 scale 的简单方法,往往比 scale 不上去的精巧方法赢。

3.2 CBOW:上下文预测中心词

CBOW 的训练目标:给定上下文窗口 {wtk,,wt1,wt+1,,wt+k}\{w_{t-k}, \ldots, w_{t-1}, w_{t+1}, \ldots, w_{t+k}\},预测中心词 wtw_t

具体做法:把上下文每个词的 embedding 求平均得到 h\mathbf{h},再用 softmax 预测中心词:

P(wtcontext)=exp(vwth)wVexp(vwh).P(w_t | \text{context}) = \frac{\exp(\mathbf{v}_{w_t}^\top \mathbf{h})}{\sum_{w \in V} \exp(\mathbf{v}_w^\top \mathbf{h})}.

训练目标是让 cross-entropy 尽可能小。

这里的 h\mathbf{h} 可以理解成「上下文的合影」:thecatonmat 各自有一条向量,把它们平均后得到一条综合向量。模型再问:看到这张合影,最可能被遮住的中心词是什么?如果正确答案是 sat,而模型把概率给了 run,loss 就会变大,训练会推动这些词向量往更合理的方向移动。

训练时用的是 cross-entropy loss。白话说,它惩罚「模型没把概率放到正确答案上」这件事;正确词概率越低,惩罚越大。

CBOW 的特点:训练速度快(每个样本只有一个目标),高频词学得好(出现次数多,被预测多次),但低频词信号弱。

CBOW 真正训练一条样本时,动作很具体:

  1. 从句子里选一个中心位置,比如 the cat sat on the mat 里的 sat
  2. 取窗口里的上下文词,例如 the, cat, on, the
  3. 查这些上下文词的输入 embedding,求平均得到 h\mathbf{h}
  4. h\mathbf{h} 和输出矩阵里每个候选词向量做点积,得到整个词表的 logits。
  5. softmax 后拿正确中心词 sat 算 cross-entropy。
  6. 反向传播只会更新这些上下文词的输入向量,以及输出端参与打分的向量。若使用 full softmax,输出端全词表都被更新;若使用 negative sampling,只更新正样本和少数负样本。

所以 CBOW 的“模型本身”就是两张表:输入 embedding 表和输出 embedding 表。中间没有深层网络,只有查表、平均、点积、softmax 或负采样。

3.3 Skip-gram:中心词预测上下文

Skip-gram 反过来:给定中心词 wtw_t,预测上下文里的每个词。

P(wt+jwt)=exp(vwt+juwt)wVexp(vwuwt).P(w_{t+j} | w_t) = \frac{\exp(\mathbf{v}_{w_{t+j}}^\top \mathbf{u}_{w_t})}{\sum_{w \in V} \exp(\mathbf{v}_w^\top \mathbf{u}_{w_t})}.

注意这里有两套 embedding:u\mathbf{u} 是中心词的,v\mathbf{v} 是上下文词的。最终用 u\mathbf{u}(或两者平均)作为词向量。

Skip-gram 训练慢(每个中心词产生多个上下文目标),但低频词学得好(每出现一次,所有上下文都来给它一次梯度信号)。

Skip-gram 的训练样本拆得更碎。同一句 the cat sat on the mat,中心词 sat、窗口大小 2,会生成四个二元样本:(sat, the)(sat, cat)(sat, on)(sat, the)。每个样本都在问同一个问题:给定中心词 sat,某个上下文词是不是应该被预测出来。

具体训练步骤是:查中心词的输入向量 usat\mathbf{u}_{sat};查目标上下文词的输出向量 vcat\mathbf{v}_{cat};计算点积 vcatusat\mathbf{v}_{cat}^\top\mathbf{u}_{sat};把它送进 softmax 或 sigmoid;根据目标标签更新两条向量。Skip-gram 之所以对低频词友好,是因为一个低频中心词只要出现一次,就能从多个上下文样本里收到多次训练信号。

CBOW 与 Skip-gram 的训练目标转存失败,建议直接上传图片文件

把图里的箭头翻译成一句话:CBOW 是「看周围,猜中间」,Skip-gram 是「看中间,猜周围」。两者都没有人工告诉模型 cat 是动物、sat 是动作;模型只是在大量句子里反复做这种填空题,最后词向量自然形成了语义结构。

3.4 选择哪个

经验上,Skip-gram 在大多数任务上比 CBOW 略好(特别是稀有词的语义),但 CBOW 训练更快。Mikolov 论文里两者都用,主要给出 Skip-gram 的结果。

实际上后来主流做法是用 Skip-gram 加 negative sampling。这是 2013 年第二篇 word2vec 论文(NIPS 版本)介绍的「SGNS」(Skip-gram with Negative Sampling),是 word2vec 的「标准实现」。

3.5 全词表 softmax 太慢

注意 3.2 和 3.3 的公式分母都是「在整个词表上求和」。V=105V = 10^5 时每次都要算 10510^5 次 exp 和归一化,慢得不能忍受。

这就需要加速技巧。第一种是 Hierarchical Softmax:把词表组织成二叉树(霍夫曼树),每个词对应从根到叶的路径,预测词等价于在路径上做 O(logV)O(\log V) 次二分类。把 O(V)O(V) 降到 O(logV)O(\log V)

Hierarchical Softmax 的训练细节是:先给词表建一棵二叉树,常用霍夫曼树,让高频词路径更短。每个内部节点都有一个可学习向量。要预测目标词 sat 时,不再对 10 万个词打分,而是沿着根到 sat 叶子的路径走,比如路径编码是 left, right, left。模型在每个内部节点做一次二分类:下一步该走左还是右。三步都走对,才等价于预测出 sat。loss 是这条路径上几个二分类 cross-entropy 的和。

这套方法的好处是精确归一化,仍然给出了一个合法概率;坏处是实现复杂,树结构会影响训练,而且每次更新的是路径上的节点向量,不像负采样那么直接。

第二种是 Negative Sampling(NEG):不算完整 softmax,而是对每个正样本(真正的上下文词),随机采几个「负样本」(不是上下文的词),用 binary cross-entropy 训练区分正负。这把 O(V)O(V) 降到 O(k+1)O(k+1)kk 是负样本数(典型 5~20)。

负采样(Negative Sampling)的核心动作 假设词表有 10 万个词。原始 softmax 每次训练 (cat, sat) 这个样本时,要给 sat 和其余 99999 个词都算分,再做归一化。 负采样把这个多分类问题改成若干个二分类问题:真实窗口里出现过的 (cat, sat) 标成正样本 1;再按噪声分布抽 kk 个词,例如 apple, plane, blue,组成 (cat, apple)(cat, plane) 这类负样本 0。训练时只对这 1+k1+k 个词对算 sigmoid 和 binary cross-entropy,只更新中心词向量、正样本向量和这几个负样本向量。成本从 O(V)O(V) 变成 O(k+1)O(k+1),这就是它能在大语料上跑起来的关键。

全词表 softmax 与负采样对比转存失败,建议直接上传图片文件

NEG 比 Hierarchical Softmax 简单且经验上效果略好,是 word2vec 的标准选择。

负采样的训练对象不是“从全词表里选正确词”,而是“判断一对词像不像真实共现”。对于正样本 (cat, sat),标签是 1;随机抽到的 (cat, apple)(cat, plane) 标签是 0。每一对都算一次 sigmoid:

p(y=1c,w)=σ(vwuc).p(y=1|c,w)=\sigma(\mathbf{v}_w^\top\mathbf{u}_c).

如果正样本分数低,训练会把 ucat\mathbf{u}_{cat}vsat\mathbf{v}_{sat} 拉近;如果负样本分数高,训练会把 ucat\mathbf{u}_{cat}vapple\mathbf{v}_{apple} 推远。这里的“拉近/推远”不是比喻,而是梯度更新真的在改变这几条向量的点积。

3.6 SGNS 的目标函数

具体写一下 SGNS。中心词 cc,上下文词 ww,用 sigmoid 替代 softmax:

L=logσ(vwuc)i=1kEwiPn[logσ(vwiuc)].\mathcal{L} = -\log \sigma(\mathbf{v}_w^\top \mathbf{u}_c) - \sum_{i=1}^{k} \mathbb{E}_{w_i \sim P_n}[\log \sigma(-\mathbf{v}_{w_i}^\top \mathbf{u}_c)].

第一项让正样本的点积大;第二项让 kk 个负样本(从噪声分布 PnP_n 采样)的点积小。PnP_n 通常是 unigram 分布的 0.75 次幂——这个 0.75 是经验调出来的,能让低频词被采样到的概率比 unigram 更高,从而学得更好。

如果这个公式看起来重,可以先抓住三个动作:第一,把中心词向量和真实上下文词向量做点积;第二,用 sigmoid 把点积变成「这一对像不像真搭配」的概率;第三,对随机负样本做相反训练,让它们的点积变小。点积大,说明两个向量方向相近;点积小甚至为负,说明它们不该靠太近。

这里的噪声分布 PnP_n 就是「负样本从哪里抽」。如果完全按词频抽,theofand 会太常被抽到;取 0.75 次幂会把高频词压平一点,让中低频词也有机会参与训练。

SGNS 的一轮训练循环可以压成伪代码:

for sentence in corpus:
	for center position t:
		c = sentence[t]
		for context word w in window(t):
			update_pair(c, w, label=1)
			for n in sample_noise(k):
				update_pair(c, n, label=0)

update_pair 做的就是二分类 logistic regression:算点积、过 sigmoid、算 binary cross-entropy、用 SGD 或 Adam 更新中心词向量和目标词向量。一次更新只碰到 1+k1+k 个输出词向量,因此 SGNS 能在大语料上跑得非常快。

3.7 经典结果:king - man + woman = queen

word2vec 论文里最让人印象深刻的实验是「类比测试」。给定 (a, b, c),要找 d 使得 a:b = c:d。比如 (man, king, woman) → queen。

具体做法:在 embedding 空间里算 vbva+vc\mathbf{v}_b - \mathbf{v}_a + \mathbf{v}_c,然后找词表里向量与之最接近的词。在 word2vec 训出来的 embedding 上,这种类比有 60-70% 的正确率。

这意味着 embedding 空间里「king - man」这个差向量大致代表「皇室身份」这个抽象概念,跨词类型保持稳定。这是「线性结构」的强证据,也是 word2vec 让世界为之惊艳的根源。

3.8 word2vec 的其他八卦

word2vec 一开始是 Mikolov 在 Google 实验室里做的内部项目,2013 年放出来时整个 NLP 社区都疯了——因为它训得又快又好,远超之前所有方法。

有一段时间,「跑 word2vec 训词向量」成了几乎每个 NLP 项目的开头第一步。直到 2018 年 ELMo/BERT 出现,预训练词向量的范式才被「预训练上下文表示」取代。但即使到今天,很多简单任务(比如基于规则的关键词匹配)仍然用 word2vec,因为它便宜、快、够用。

Mikolov 后来跳槽到 Facebook,做了 fastText(subword embedding),是 word2vec 的延伸。这是另一段故事,跟 BPE 那一篇有关。

3.9 word2vec 等价于矩阵分解

Levy & Goldberg 2014 证明了一个让人惊讶的结论:SGNS 在某种意义下等价于隐式分解 shifted PMI 矩阵。

具体说:当 SGNS 收敛时,uwvcPMI(w,c)logk\mathbf{u}_w^\top \mathbf{v}_c \approx \mathrm{PMI}(w, c) - \log k,其中 kk 是负样本数。也就是说 word2vec 在对 PMI 矩阵做低秩近似。

这个结论把 word2vec 和经典分布语义(PPMI + SVD)联系起来。两条看似完全不同的路径,本质上做的是同一件事。这是 NLP 领域里少见的「不同方法的惊人统一」。

这个等价也解释了为什么 word2vec 不是魔法。它没有读懂世界,它只是通过大量正负样本,把“真实共现比随机共现多多少”压进了点积里。区别在于:PPMI+SVD 是先显式建完整矩阵再分解;SGNS 是不建矩阵,边扫语料边用 SGD 做一个隐式分解。

3.10 word2vec 的工程参数

实际跑 word2vec 时常见的超参:词向量维度 100-300;窗口大小 5(CBOW)或 10(Skip-gram);负样本数 5-15;学习率 0.025(线性衰减);下采样阈值 10310^{-3}10510^{-5}(高频词被随机丢弃);最少出现次数 5(频率太低的词不学)。

这些参数有「最佳实践」但没有最优解。Mikolov 的开源代码 word2vec.c 是一个 C 实现,跑 6B 词只要几小时(多核 CPU)。这种「能在 CPU 上几小时跑完」的便利性是它能流行的工程基础。

3.11 word2vec 后的衍生

word2vec 之后衍生出无数变体:

  • Doc2Vec(Le & Mikolov 2014):扩展到段落和文档级 embedding。
  • node2vec(Grover 2016):把 Skip-gram 思路用到图节点上,做随机游走作为「上下文」。
  • item2vec:用户行为序列里的物品,类比词序列里的词。推荐系统大量使用。
  • prod2vec、event2vec、anything2vec:「2vec 现象」,几乎每个领域都套用了一遍。

这些工作把 word2vec 的核心方法(局部窗口预测 + negative sampling)迁移到各种离散对象上,是 representation learning 跨领域扩散的一个早期范例。

四、GloVe:全局共现的回归

4.1 word2vec 的局限

word2vec 是「局部窗口」方法:每次只看一个词及其几个邻居。这意味着它没有显式利用「整个语料里的全局共现统计」。

「局部」不是说它看不到全局规律,而是说它每次训练只拿一小段窗口当样本。全局规律要靠无数次局部样本慢慢累积出来。GloVe 的想法更直接:既然最终要学的是共现规律,不如先把整份语料的共现次数表做出来,再让向量去拟合这张表。

在 SVD/LSA 那条路径里,全局共现矩阵是出发点;在 word2vec 那条路径里,全局信息被「分散在数百万次局部样本」中间接学到。两条路各有优劣。

Pennington、Socher、Manning 在 2014 年提出 GloVe(Global Vectors),尝试把两条路结合起来:直接用全局共现矩阵作为目标,但用神经网络的方法去拟合它。

4.2 GloVe 的目标函数

XijX_{ij} 是词 ii 和词 jj 的共现次数。GloVe 的目标:

L=i,jf(Xij)(wiw~j+bi+b~jlogXij)2.\mathcal{L} = \sum_{i,j} f(X_{ij}) (\mathbf{w}_i^\top \tilde{\mathbf{w}}_j + b_i + \tilde{b}_j - \log X_{ij})^2.

直观解释:用两个向量的点积(加偏置)拟合共现次数的对数。f(Xij)f(X_{ij}) 是一个权重函数,对很高的共现做截断,避免被「the the」这种统计主导。

把公式拆开看:XijX_{ij} 是词 ii 和词 jj 一起出现了多少次;logXij\log X_{ij} 是把次数取对数,因为次数分布太悬殊,直接拟合原始次数会被高频词压垮;wiw~j\mathbf{w}_i^\top \tilde{\mathbf{w}}_j 是两个向量的点积,表示模型认为这两个词有多相关;bib_ib~j\tilde b_j 是偏置,用来吸收某些词天然更常见这件事。

最后那个平方表示「模型预测值」和「真实 log 共现次数」差多少。差得越大,loss 越大,训练就会调整向量,让下一次预测更接近。

对一个具体非零项 (i,j)(i,j),GloVe 的训练就是在做这件事:

y^ij=wiw~j+bi+b~j,yij=logXij.\hat y_{ij}=\mathbf{w}_i^\top \tilde{\mathbf{w}}_j+b_i+\tilde b_j, \quad y_{ij}=\log X_{ij}.

如果 catmiaow 共现 20 次,y=log20y=\log 20;模型当前预测 y^\hat y 太小,就把两者向量的点积调大;如果预测太大,就调小。权重 f(Xij)f(X_{ij}) 决定这个样本的梯度有多重。GloVe 本质上不是分类,而是在稀疏共现表上跑加权最小二乘回归。

4.3 它和 word2vec 的关系

实践上,GloVe 和 word2vec 在很多下游任务上效果接近,没有压倒性优势。但 GloVe 的训练直接用全局矩阵,可以并行化得更彻底,工程上很有吸引力。

斯坦福放出来的 GloVe 预训练向量(300 维、840B 词)一度是几乎所有 NLP 项目的默认起点。直到 ELMo / BERT 替代它们。

4.4 GloVe 的几何意义

GloVe 论文里有一个非常漂亮的推导:从「词向量应该满足某些函数关系」的需求出发,反推出「点积加偏置等于 log 共现」这个目标函数。这种「从性质推目标」的推导很值得学习——相比 word2vec 的「先有模型再说」,GloVe 的推导更有数学风味。

但这种数学优雅没有在效果上带来质的差异。事实证明,分布式语义这个领域,方法的细节没有大家想象的那么关键,关键是数据规模和训练 trick。

4.5 加权函数 f(Xij)f(X_{ij})

GloVe 的加权函数典型形式是:

f(x)={(x/xmax)α,x<xmax1,xxmaxf(x) = \begin{cases} (x/x_{\max})^{\alpha}, & x < x_{\max} \\ 1, & x \ge x_{\max} \end{cases}

xmax=100x_{\max}=100α=3/4\alpha=3/4 是论文里的取值。它做了两件事:第一,过滤掉 Xij=0X_{ij}=0 的项(log 0 没定义);第二,截断高频对的权重,避免「the of」、「of the」这种泛滥的共现支配整个目标函数。

GloVe 加权函数示意转存失败,建议直接上传图片文件

这张图的意思是:低频共现不是没用,但要谨慎地给权重;中频共现逐渐变重要;超过阈值以后,再高的次数也不继续放大。这样做是在保护训练目标,不让少数超高频词把所有梯度都抢走。

这个 α=3/4\alpha=3/4 不是巧合——和 word2vec negative sampling 里采样分布的 3/4 次方是同一个数。这个数在分布式语义里反复出现,背后大概有共通的频率分布解释,但没有理论上的精确推导。

4.6 GloVe 的训练实现

GloVe 第一步要扫一遍语料,构建共现矩阵 XijX_{ij}。这一步需要决定窗口大小(典型 5 或 10)和加权方式(距离衰减常用 1/d1/d)。结果是一个稀疏矩阵,可能有几百万到几千万个非零项。

第二步是优化。GloVe 用 AdaGrad 在非零项上跑 SGD。每个非零项独立计算梯度,所以训练高度并行。300 维 GloVe 在 6B 词上训练大概几小时(多核 CPU),比 word2vec 还快。

GloVe 还有一个细节:每个词学两套向量 w\mathbf{w}w~\tilde{\mathbf{w}},对应「中心词」和「上下文词」。最后用的时候把两套向量相加。这个 trick 比单套效果略好,估计是某种集成效应。

GloVe 的训练循环也可以写得很具体:

build sparse cooccurrence table X
initialize W, W_context, b, b_context
for epoch in range(num_epochs):
	for (i, j, x_ij) in nonzero(X):
		y = log(x_ij)
		pred = dot(W[i], W_context[j]) + b[i] + b_context[j]
		loss = f(x_ij) * (pred - y)^2
		update W[i], W_context[j], b[i], b_context[j] with AdaGrad

它和 SGNS 最大的手感差异是:SGNS 的样本来自“滑窗产生的正负词对”;GloVe 的样本来自“已经统计好的非零共现项”。SGNS 一边扫语料一边学,GloVe 先统计再回归。

4.7 GloVe 训练好的预训练词向量

斯坦福放出的 GloVe 预训练向量有几个版本:6B(Wikipedia + Gigaword,6B token,词表 400k,50/100/200/300 维),42B(Common Crawl,42B token,词表 1.9M,300 维),840B(Common Crawl,840B token,词表 2.2M,300 维)。

工业界默认用 840B 的版本,因为它见过的语料最大、覆盖最广。但有时小一点的(6B)反而在某些 benchmark 上表现更好——更小的词表 + 更干净的数据可能让向量更聚焦。

这些预训练向量从 2014 年到 2018 年(ELMo / BERT 出来之前)一直是 NLP 的事实标准基础设施。

4.8 GloVe 与 word2vec 的对比

实证 benchmark 上两者难分高下。GloVe 在某些类比任务(语义类比)上略好,word2vec 在某些 syntactic 任务上略好。具体差异往往在百分点以下,被超参选择和数据预处理的差异淹没。

实践上选择常常是工程驱动的:训练资源少用 GloVe(一次扫语料即可),训练资源多用 Skip-gram(更精细的局部建模)。多数项目用预训练向量,所以选哪个其实就是「下载哪个文件」的问题。

4.9 GloVe 与近年的复兴

近年来一些工作回到 GloVe 类的「显式共现矩阵」思路:LLM 时代的 context 越来越长,embedding 训练数据越来越多,全局共现矩阵反而再次有可行的 scale。但这些尝试多数还在研究阶段,没有变成工业主流。

历史经常重复:一个老方法在新条件下复活。GloVe 思路是否会在 LLM embedding 时代再次复兴,值得观察。

4.10 GloVe 与 Transformer 训练目标的对比

GloVe 用 log-bilinear 损失拟合共现 log 计数,是「在共现统计上做线性回归」。Transformer 的 next-token prediction 是「在条件分布上做最大似然」。表面上完全不同,但都从语料里学语义。

这里的 log-bilinear 可以先不当作新概念硬记。log 指它拟合的是共现次数的对数;bilinear 指它的核心计算是两个向量的点积;「线性回归」指它把点积结果尽量调到接近目标数值。相比之下,Transformer 的 next-token prediction 不直接拟合共现次数,而是给定前文,预测下一个 token 的概率。

近年一些理论工作尝试统一这两条线:用 statistical interpretation 来分析 Transformer,发现它隐式建模了某种共现统计的高阶版本。但这是开放领域,没有结论。

五、静态 embedding 的局限

5.1 一词多义的问题

word2vec、GloVe 都是「静态 embedding」:一个词对应一个向量,永远不变。这有一个明显问题:一词多义

「bank」可以是「银行」也可以是「河岸」;「apple」可以是「水果」也可以是「公司」;「Java」可以是「编程语言」也可以是「咖啡」也可以是「印尼岛」。静态 embedding 把这些含义平均成一个向量,结果是它对每种含义都不准。

可以把静态 embedding 想成一张纸质词典:每个词条只有一个固定解释。对于意思稳定的词,这还可以;对于多义词,这个固定解释就像把几种含义搅成一杯混合饮料,哪一种味道都不纯。

除了多义词外,这套按独立单词死记硬背的映射体系还面临着另一个硬伤:OOV(Out of Vocabulary,未登录词)与形态变化问题。 在它的词库视角中,英语里的 run / running / ran,或者中文里的 / 奔跑 / 快跑 ,都被当成毫不相干的独立 ID 处理。它们不仅各自占用着昂贵的词表空间位,并且互相之间完全没法共享属于“跑”这个概念的核心特征。更糟的是,一旦线上突然遇到一个训练语料前所未见的词(比如现造的网络新梗词、含有错别字的词),传统的静态分词因为查表直接报空,只能统一推给一个废弃垃圾桶标签叫 <UNK> 向量。对于模型而言,一旦遇上 OOV,对应的这块语义区间就彻底变成了瞎子。

可以说,“如何学会举一反三的认新词和变体词”,成了后来所有 NLP 需要解决的又一命门。为了解决 OOV 和臃肿形态问题,后来的研究者彻底抛弃了按“整个独立单词”强映射的思维,进而转向了现在的大模型里如日中天的解法:BPE 与 Subword(子词切分算法)(也就是大模型常说的 Token 的真面目,我们将在后续章节专门介绍)。而针对本节所说的一词多义,模型必须学会的是让向量自己长上耳朵,动态感知上下文。

5.2 上下文应该改变 embedding

人类读「I deposited money at the bank」和「I sat by the bank」时,「bank」激发的是不同的语义。embedding 应该跟随上下文变化。

上下文改变同一个词的 embedding转存失败,建议直接上传图片文件

上图里,同一个 bank 在「deposit money」语境中应该靠近 moneyloanaccount;在「sat by the river」语境中应该靠近 riverwatershore。上下文化 embedding 的目标,就是让词向量根据句子环境移动到正确的语义区域。

ELMo(Peters 2018)是第一个系统做「上下文化 embedding」的工作。它的思路:用一个双向 LSTM 在大语料上做语言模型预训练,然后把 LSTM 的隐藏状态作为词的「上下文 embedding」。同一个词在不同句子里有不同的向量。

如果你还不知道 LSTM,可以先把它理解成一种会按顺序读句子的神经网络。它从左到右读一遍句子,同时维护一份「到目前为止我读懂了什么」的记忆;双向 LSTM 则再从右到左读一遍。某个位置的隐藏状态,就是这个词连同周围上下文的综合表示。

ELMo 的训练流程更具体一点:先把词表示成字符级输入,用 char-CNN 得到词的初始表示;再训练两套语言模型,一套从左到右预测下一个词,另一套从右到左预测上一个词。每个位置都会产生多层 LSTM hidden state。预训练结束后,ELMo 通常不只拿最后一层,而是让下游任务学一组权重,把不同层的 hidden state 加权求和。也就是说,ELMo 的 embedding 不是查表得到的固定向量,而是“这个词经过左右语言模型读完整句话后,在这个位置上的中间状态”。

5.3 ELMo 到 BERT

ELMo 是 LSTM 的,BERT(Devlin 2018)把这一套搬到 Transformer 上,用 masked language model 训练,得到了更强的上下文 embedding。BERT 之后所有现代 NLP 模型都是「上下文 embedding」的范式。

GPT 系列也是。GPT 用单向 Transformer 做自回归 LM,得到的 hidden state 同样是上下文 embedding。本质上 GPT 是个语言模型,它的中间层每个位置的向量就是那个 token 在那个位置的「上下文 embedding」。

BERT 的训练目标叫 masked language model。它先随机选一部分 token 做预测目标,经典 BERT 选 15%;这些位置里,80% 替换成 [MASK],10% 替换成随机 token,10% 保持原样。模型读完整句,然后只在被选中的位置上预测原始 token。loss 是这些位置的 cross-entropy 之和。这样训练出来的 hidden state 同时看过左边和右边,所以更适合需要双向理解的任务。

GPT 的训练目标更直接:给定前文 x1,,xtx_1,\ldots,x_t,预测下一个 token xt+1x_{t+1}。因为用了 causal mask,第 tt 个位置只能看见自己和左边,看不见右边。训练 loss 是所有位置的 next-token cross-entropy:

L=tlogP(xt+1xt).\mathcal{L}=-\sum_t \log P(x_{t+1}\mid x_{\le t}).

所以 BERT 和 GPT 都会产生上下文化 embedding,但训练出来的“习性”不同:BERT 的向量擅长填空式理解,GPT 的向量擅长前缀续写。把它们都叫 contextual embedding 没错,但不能把训练目标混为一谈。

我们后面第 25 篇 BERT、第 26 篇 GPT 会展开。

5.4 static 还有用吗

虽然现在主流是上下文 embedding,但 static embedding 没死。一些场景仍然适用:

资源受限场景(移动端、嵌入式):BERT 太大,跑 word2vec 加余弦相似度就够。

简单关键词任务(搜索、推荐):很多工业场景不需要那么强的语义建模。

热启动场景:训练 BERT 时偶尔会用 word2vec 初始化某些层,加速收敛。

5.5 contextual embedding 的层间差异

BERT 的不同层学到的 embedding 性质不同。Tenney et al. 2019 的 "BERT Rediscovers the Classical NLP Pipeline" 系统分析过这个:底层学到的是 surface 特征(词性、词形),中层是句法(依存、constituency),高层是语义(共指、逻辑)。

这里的「层」可以想成流水线上的多次加工:第一层看到的还很接近原始 token,越往上,信息越经过上下文混合。底层更像在回答「这个词长什么样、像什么词性」;中层更像在回答「它和句子里其他词是什么语法关系」;高层更像在回答「整句话到底在说什么」。

这意味着「BERT 的 embedding」不是一个东西,而是一个层次结构。下游任务用 BERT 时究竟该用哪一层的输出?取决于任务性质。POS tagging 用底层就够;问答更适合高层。

ELMo 的标准做法是把所有层的输出做加权平均(权重学得到),让下游任务自己选。这是一种灵活但参数多的方案。

5.6 BERT [CLS] token 的故事

BERT 在每个输入序列前面加一个特殊 token [CLS],预训练时让它的 embedding 学习「整个序列的全局表示」。这样下游分类任务可以直接用 [CLS] 的输出向量。

[CLS] 可以理解成一个专门放在句首的「汇总代表」。句子里所有 token 经过 Transformer 互相交换信息后,[CLS] 这个位置也会吸收全句信息。做情感分类、主题分类时,工程上常把它的向量交给一个小分类器。

具体 fine-tune 时通常是这样:输入 [CLS] sentence [SEP],取最后一层 [CLS] 的 hidden state hcls\mathbf{h}_{cls},接一个线性分类头 Whcls+bW\mathbf{h}_{cls}+b,再用任务标签算 cross-entropy。梯度会同时更新分类头和 BERT 参数。也就是说 [CLS] 不是天生就懂整句,它是在预训练和下游分类训练中被迫承担“汇总位”的职责。

但实践发现 [CLS] 不一定是「最佳的句子表示」——SBERT 等工作显示 mean pooling 经常更好。这又一次说明 embedding 的「最佳用法」需要任务驱动调研,没有 one-size-fits-all。

mean pooling 是另一个朴素做法:把句子里所有 token 的向量取平均,当作整句向量。它不依赖某一个特殊位置,很多检索和句子相似度任务里反而更稳。

5.7 ELMo 的双向 LSTM 细节

ELMo 用的是「双向语言模型」:从左到右一个 LSTM 跑 P(wtw<t)P(w_t | w_{<t}),从右到左一个 LSTM 跑 P(wtw>t)P(w_t | w_{>t}),两个独立训练。最终的 embedding 是两个方向各层的拼接。

注意 ELMo 不是真正的「双向」——左右两个方向是独立的,没有交互。BERT 才是真正双向的(masked language model 让任意位置可以同时看左右)。这是 ELMo 到 BERT 的关键升级。

5.8 上下文 embedding 的「身份问题」

哲学性问题:同一个词在两个不同句子里的 embedding 是否还是「同一个词」?严格说,contextual embedding 把「词」打散成「词在某语境下的实例」,每个实例有自己的向量。

这意味着传统词典里那种「词的定义」其实在 contextual 模型里不存在——只有大量上下文实例的统计分布。这是语义观的一次哲学转向,从「词的本质」到「词在语境里的使用」。

六、Transformer 的 embedding 矩阵

6.1 输入端的 embedding

Transformer 的输入流程:token IDs → embedding lookup → embedding 向量 → 加位置编码 → 送进 transformer block。

embedding lookup 是一个矩阵 ERV×dE \in \mathbb{R}^{V \times d},给定 token ID i,输出 E[i,:]E[i, :],即矩阵的第 i 行。这本质上是「one-hot 向量乘以矩阵」,但工程实现就是查表,比真的做矩阵乘快得多。

Transformer 输入端的 embedding 流程转存失败,建议直接上传图片文件

这里的 V×dV \times d 读作「VV 行、dd 列」:VV 是词表大小,有多少个 token 就有多少行;dd 是每个 token 向量的长度。比如 V=50000,d=768V=50000, d=768,就表示这张表有 5 万行,每行 768 个数字。

EE 是可学习的参数。训练时它从随机初始化开始,通过反向传播逐步学到「让任务表现最好的词向量」。这跟 word2vec 不同——word2vec 是先单独训练词向量再用,Transformer 是端到端学。

「端到端」的意思是:embedding 层、注意力层、前馈层、输出层一起训练。最终 loss 来自「预测下一个 token 是否正确」或类似任务,梯度会一路传回 embedding 表,把被用到的那些 token 行也一起改掉。

把一次前向和反向拆开看:输入 token IDs [101, 5321, 2049];embedding 层取出 E[101]E[101]E[5321]E[5321]E[2049]E[2049];加上位置向量后送进 Transformer;输出层给每个位置算 logits;用目标 token 算 loss;反向传播时,梯度会回到这三行 embedding。没有出现在 batch 里的 token 行,这一步没有梯度。下一批数据出现了别的 token,别的行再被更新。

6.2 维度选择

dd(embedding 维度)的典型值:BERT-base 是 768,BERT-large 是 1024,GPT-3 是 12288,GPT-4 推测在万级别。

dd 越大表达能力越强但参数越多。对于词表 5 万、d=1024d=1024 的模型,embedding 矩阵有 5×1075 \times 10^7 个参数。这通常占整个模型参数的 5-10%,不算大头但也不小。

为什么维度大一点会更有表达力?可以类比地图:二维地图只能放经纬度,三维地图还能放高度;更高维的向量空间能同时容纳更多语义方向。但维度不是越大越好,因为每多一列,词表里的每个 token 都要多存一个数字,训练和部署成本都会上升。

6.3 tied embedding:输入和输出共享

观察:模型最后一层把隐藏状态投影回词表得到 logits,需要一个 WRV×dW \in \mathbb{R}^{V \times d} 矩阵。这个矩阵的形状跟输入 embedding 矩阵 EE 一样。

是否可以让它们共享参数?W=EW = E?答案是肯定的,叫「tied weights」或「weight tying」(Press & Wolf 2017)。

输出端的 WW 可以想成另一张「候选词表」:模型在某个位置得到隐藏状态后,会拿它和每个候选 token 的向量做点积,点积越大,说明越像下一个 token。既然输入端已经有一张 token 向量表,输出端再单独养一张形状一样的表就显得有点浪费。weight tying 就是让这两张表共用同一份参数。

不共享时,输出 logits 通常是:

logits=hWout+b.\mathrm{logits}=\mathbf{h}W_{out}^\top+b.

共享时直接令 Wout=EW_{out}=E,于是每个候选 token 的 logit 就是 h\mathbf{h} 和这个 token embedding 的点积。训练时同一行参数会从两个方向收到梯度:输入端告诉它“这个 token 作为上下文该怎么表示”,输出端告诉它“这个 token 作为候选答案该怎么被打分”。

为什么有用:第一,省一半 embedding 参数;第二,实证上略提升泛化性。直觉解释:「这个词在输入时的语义」和「这个词作为输出时的概率分布」应该是一致的,强制共享可以利用这种对称性。

GPT-2、GPT-3、LLaMA 都用 tied embedding。BERT 没有完全 tie。这是工程上的一个非默认但常见选择。

6.4 token + position

embedding 不仅是 token 的,还有位置的。Transformer 没有 RNN 的天然顺序信息,需要显式注入位置。

最简单的:每个位置 pp 也对应一个向量 EpposE_p^{\text{pos}},把它加到 token embedding 上:

xp=E[tokenID(p),:]+Eppos.\mathbf{x}_p = E[\text{tokenID}(p), :] + E_p^{\text{pos}}.

后面第 21 篇专门讲位置编码(绝对、相对、RoPE 等)。这里只先把「embedding 包含 token + position」的图建起来。

为什么是「加」而不是拼接?因为 token 向量和位置向量长度相同,加完以后仍然是 dd 维,不会改变后面 Transformer block 期待的输入形状。你可以把它想成在 token 的语义坐标上叠加一个位置提示:同样的 cat,出现在第 3 位和第 30 位,送进模型时会带着不同的位置标记。

6.5 normalization 的位置

很多实现里 embedding 之后会立刻接一个 LayerNorm(或 RMSNorm)。这是为了让初始的输入分布稳定,避免某些 token 的 embedding 范数特别大主导后面所有计算。

LayerNorm / RMSNorm 可以先理解成「把一条向量的数值范围拉回正常区间」。如果某个 token 的向量特别大,它和后面所有向量点积都会偏大,训练会不稳定。归一化层做的事就是给输入做一次尺度整理,让后面的层别被极端数值带偏。

LLaMA 用 RMSNorm 替代 LayerNorm,位置仍然是 embedding 之后。这些细节看似无关紧要,但在大规模训练时影响稳定性。第 23 篇 LayerNorm 会展开。

6.6 embedding 与 logit 共享导致的尺度问题

tied embedding 还有一个微妙的副作用。embedding 表达「token 的语义」,logit 表达「下一个 token 是它的对数几率」。这两者的「自然尺度」未必一样。

具体说,embedding 范数适合大约 1(让点积稳定),logit 适合范围 ±\pm 几(softmax 之前)。tied weights 让它们必须共用同一矩阵,可能在某一面是次优的。

「范数」就是向量长度。两个词向量的方向表示语义相似性,长度则会影响点积的大小。tied weights 把输入语义空间和输出打分空间绑在一起,节省参数,也带来这种尺度上的小矛盾。

实践中常见的修正:在 logit 端额外乘一个可学习的缩放因子,或者在 logit 前再过一个 LayerNorm。这些细节在 GPT-Neo、LLaMA 的源代码里都能看到。

6.7 vocabulary 的扩展

预训练完成后,有时需要添加新词到词表(新领域、新语言)。怎么处理?

这在真实系统里很常见。比如一个通用模型没见过某个药品名、公司内部术语或新产品代号,但下游任务必须处理它。添加新 token 的本质是给 embedding 表新增一行:问题在于这行数字一开始应该怎么填。

最简单:新词用「相近词的 embedding」初始化,然后微调。实际上有时简单的随机初始化也能工作,因为 fine-tune 会快速学到合适的值。

更复杂的扩展(添加几千个新词)需要先在新词表上跑一段时间的预训练,让 embedding 收敛。这是 LLM 多语言扩展时的工程挑战。

6.8 embedding 的 freeze 与 unfreeze

迁移学习时一个常见问题:是冻结 embedding 还是让它继续训练?

冻结的好处:保留预训练知识、防止小数据下灾难性遗忘。坏处:限制了模型适应新领域的能力。

实践上常见的策略是「分层学习率」:embedding 用很小的学习率(让它微调但不剧烈变动),上层 transformer 用正常学习率。HuggingFace Transformers 库的 discriminative_learning_rate 就是这种思路。

6.9 embedding 在量化部署里

LLM 部署时常做量化(INT8、INT4)。embedding 矩阵作为最大的单一参数块,是量化的重点。

量化就是把原本高精度的小数用更少的位数存起来。比如 FP16 是 16 位浮点数,INT8 是 8 位整数,INT4 是 4 位整数。位数越少,模型越省显存、跑得越快,但数字误差也越大。

INT8 量化通常对 embedding 影响很小(几百维向量的小数值精度差异不致命)。但 INT4 / INT2 这种激进量化下,embedding 量化误差会传播到所有下游层,要谨慎。GPTQ、AWQ 等量化方法都有针对 embedding 的特殊处理。

七、subword:让 embedding 处理无限词表

7.1 OOV 问题

预训练词向量永远会遇到「词表外」(out-of-vocabulary, OOV)问题:训练时没见过的词,inference 时怎么办?

word2vec 一般给个 <UNK> token 兜底。这是粗糙的处理——所有没见过的词都映射到同一个向量,丧失区分度。

<UNK> 就像词表里的「其他」分类。它能保证程序不断,但会把完全不同的未知词挤进同一个向量:新药名、用户名、错别字、外语词都变成同一个东西。模型知道「这里有个未知词」,却不知道未知词之间有什么区别。

更优雅的做法是「subword」:把词拆成更小的单位(字符、字符 n-gram、BPE pieces)。即使整个词没见过,组成它的 subword 大概率见过,于是可以从 subword 组合出词的表示。

7.2 BPE 的引入

BPE(Byte-Pair Encoding)是 GPT、LLaMA 等模型用的子词切分算法。它从字符级别开始,逐步合并最常出现的 bigram,直到达到目标词表大小。

bigram 就是相邻的两个单位。BPE 一开始把文本看成字符序列,然后反复问:「哪两个相邻单位最常一起出现?」如果 lo 经常挨着,就合成 lo;如果 low 经常挨着,再合成 low。合并次数越多,词表里就会出现越长、越常见的片段。

BPE 让词表里同时包含完整词(the, cat)、词根(un-, -ing)、单字符(a, b, c),覆盖几乎所有可能输入。即使是没见过的新词(比如人名),也能被切成已知的 subword。

BPE 的训练算法可以按下面的方式手工跑出来:

  1. 把训练文本先拆成最小单位。经典 BPE 从字符开始,byte-level BPE 从字节开始。
  2. 统计所有相邻 pair 的频率,例如 l oo we r
  3. 找到频率最高的 pair,把它合并成一个新 token,例如把 lo 合成 lo
  4. 用这个新 token 重写语料,再重新统计 pair。
  5. 重复合并,直到词表达到目标大小,比如 32K 或 100K。

训练结束后会得到一个“合并规则列表”。推理时不是再统计频率,而是按这份规则把新文本切开:能合并的就合并,不能合并的保留更小单位。比如规则里有 r+u -> ruru+n -> runn+ing -> ningrunning 就可能被切成 running

Subword 切分示意转存失败,建议直接上传图片文件

这张图的核心直觉是:模型不必死记每个完整单词。running 可以拆成和 run 相关的片段,unhappiness 可以拆出否定前缀、词根和后缀。这样一来,没见过的长词也可以由见过的小片段拼出来。

第 29 篇专门讲 tokenization(BPE、WordPiece、SentencePiece 等),这里只先说:subword 是现代 LLM 处理无限词表的标准方案。

7.3 fastText:在 embedding 层做 subword

fastText(Bojanowski 2016)把 subword 思想直接放到 embedding 层:每个词的 embedding 是它所有字符 n-gram embedding 的平均。

好处:未见词也能算 embedding(只要字符 n-gram 见过);morphologically 相关的词(cat, cats, catlike)共享部分 embedding,参数效率高。

fastText 的训练目标基本沿用 Skip-gram + negative sampling,只是“中心词向量”不再是单独一行查表,而是若干字符 n-gram 向量的和。比如 where 会被包上边界符号 <where>,拆出 <whwhehererere> 等 n-gram。训练样本 (where, is) 更新时,不只更新 where 这个词向量,还会更新这些 n-gram 的向量。这样 whereverwhereby 即使很少出现,也能共享 wheher 等子结构的统计信号。

fastText 主要在静态 embedding 时代用。Transformer 时代主流改用 BPE 类的子词切分,但思路是延续的。

7.4 字符级 vs 子词级 vs 词级

历史上有过三种切分粒度:字符级、子词级、词级。

字符级(character-level):词表小(几百个字符)、长序列、零 OOV。早期工作 Kim 2016 的 char-CNN 用这个。缺点:序列长,O(N2)O(N^2) attention 开销大。

这里的 NN 是序列长度。Transformer 的 attention 需要让每个位置看每个位置,所以长度翻倍,注意力计算大约变成四倍。字符级虽然词表小,但一句话会被切得很长,因此 attention 成本会涨得很快。

词级(word-level):词表大(几万到几十万)、序列短、有 OOV。早期 word2vec、GloVe 用这个。缺点:OOV 问题、词表参数多。

子词级(subword-level):折中。词表中等(一万到十万)、序列中等长、几乎无 OOV。BPE 类算法。现代 LLM 的标准。

每种切分都有适用场景,但近几年 BPE 几乎一统天下。

7.5 byte-level fallback

GPT-2 之后流行 byte-level BPE:词表的最底层不是字符而是字节。这能保证「任何 UTF-8 字符串都能被 tokenize」,零失败率。

字节可以理解成计算机存文本的最小积木之一。无论是英文、中文、罕见符号还是乱码,只要它能被编码成 UTF-8 字节序列,就可以被 byte-level BPE 拆开。因此它特别鲁棒,线上不容易遇到「这个字符完全无法处理」的问题。

代价是对 ASCII 之外的字符(比如中文或罕见符号)需要多个 token,显得「贵」一些。但鲁棒性是值得的。第 29 篇 BPE 那一篇会展开。

7.6 WordPiece 与 SentencePiece

BPE 不是唯一的子词算法。Google 在 BERT 里用了 WordPiece:和 BPE 类似,但合并准则不是「频率」而是「合并后能最大化 likelihood」。原理上它是一种贪心的 EM。

WordPiece 可以理解成“带概率目标的 BPE”。它也从小片段开始合并,但优先选择能最大提升语料似然的合并。常见近似打分会偏向“两个片段一起出现得比各自独立出现更意外”的 pair。BERT 的词表里常见 ##ing##ly 这种写法,## 表示这个片段不能作为词首,只能接在前一个片段后面。推理时,WordPiece 通常用最长匹配:从当前位置开始找词表里最长可用片段,找不到就退回更短片段,直到完整切完。

SentencePiece 是 Google 开源的工具,把 tokenization 和 detokenization 都做得「端到端」:直接处理原始 Unicode 字符串,不需要预先分词(对中文、日文友好),还能跑 unigram language model 做切分。

SentencePiece 的 unigram 模型更像在训练一个“片段概率表”。它先准备一个很大的候选子词集合,然后用 EM 估计每个子词的概率:E 步计算一句话可能有哪些切法、每种切法概率多大;M 步更新子词概率;然后剪掉贡献小的子词,继续迭代。推理时用 Viterbi 找概率最高的切分,也可以采样多个切分做数据增强。

detokenization 是 tokenization 的反过程:把 token 序列还原成文本。对英文来说空格比较明显,对中文、日文、混合符号文本来说,空格和边界没那么统一,所以 SentencePiece 这类工具会把「怎么切」和「怎么还原」一起设计好。

T5、ALBERT、mT5、LLaMA-1 都用 SentencePiece。它的优势是支持多语言、对空白符号统一处理,缺点是依赖额外的库。

7.7 Unicode 与字节对齐

中文一个字符在 UTF-8 里通常是 3 字节。如果用 byte-level BPE,「中」会被切成「e4 b8 ad」三个字节 token,效率低。如果用 character-level,「中」就是一个 token,效率高但词表大。

LLaMA-2/3 的解法是「在 byte-level BPE 上预处理常见 Unicode 块」,让常见字符占一个 token。这种 trade-off 是多语言模型设计里的反复出现的工程问题。

八、训练 embedding 的工程细节

8.1 初始化

embedding 的标准初始化:从均值 0、方差 1/d1/d(或 1/d1/\sqrt{d})的正态分布或均匀分布采样。这是 Xavier/He 初始化的应用。

初始化就是训练开始前怎么给参数填第一版数字。如果全填 0,所有词一开始完全一样,训练很难打破对称;如果随机数太大,后面的点积和 softmax 会变得极端;如果太小,信号又容易弱得动不了。所以初始化看起来只是开局,实际会影响训练是否稳定。

为什么这样?让 embedding 后续的点积、求和等操作的输出方差大约为 1,避免初始训练时数值爆炸或消失。

具体到 LLaMA:embedding 用 N(0,1)\mathcal{N}(0, 1) 初始化(不是 1/d1/d),然后通过 RMSNorm 归一化。这是设计选择上的细节差异。

8.2 学习率

embedding 层在训练初期会收到非常多的梯度(每个词出现就更新一次),但是绝大多数词更新次数远低于其他层。这可能让 embedding 层学得比其他层慢。

一个常见做法是给 embedding 层一个稍大的学习率(比如全局学习率的 10x)。但现代 LLM 通常不这么搞,依赖 Adam 的自适应学习率自动平衡。

学习率就是每次改参数的步子有多大。步子太小,训练像原地慢走;步子太大,可能越过最低点甚至发散。embedding 的特殊之处在于:高频 token 经常被更新,低频 token 很少被更新,所以它们对学习率的敏感度不一样。

最朴素的 SGD 更新是:

wwηwL.w \leftarrow w - \eta \nabla_w \mathcal{L}.

这里 ww 代表某一组参数,η\eta 是学习率,wL\nabla_w \mathcal{L} 是当前参数让 loss 增大的方向。Adam 在这个基础上给每个参数维护两份移动平均:一份是一阶动量,表示近期梯度大概往哪走;一份是二阶矩,表示这个参数的梯度波动有多大。embedding 里低频 token 更新少,Adam 的自适应缩放能缓和“有的行经常被改、有的行很久不动”的不均衡。

8.3 梯度 sparse 更新

每个 batch 里只用到了词表里的一小部分 token,所以每次反向传播只有这部分 embedding 收到非零梯度。框架会做 sparse update 以提升效率。

举个例子:词表有 5 万个 token,但一个 batch 里可能只出现了 2000 种 token。那这一步训练只需要更新这 2000 行 embedding,剩下 48000 行完全没参与计算。所谓 sparse gradient,就是只记录这些被用到的行的梯度,不为整张大表都存一份更新。

PyTorch 里 nn.Embedding(sparse=True) 启用 sparse 梯度,配合 optim.SparseAdam 使用。在大词表场景能显著加速训练。

注意 sparse update 不是说 embedding 矩阵本身稀疏,而是说梯度稀疏。矩阵每一行仍然是稠密向量,只是这一步只给出现过的 token 行记梯度。实现里通常会把 batch 里的 token ID 去重,比如 [5, 5, 9, 20] 只更新第 5、9、20 三行,并把重复出现的第 5 行梯度累加。

8.4 正则化

embedding 层常常不加 weight decay。理由:embedding 是查表,不是「线性变换」语义,weight decay 会把 embedding 拉向零,损害语义。

weight decay 是一种正则化:训练时轻轻惩罚参数太大,防止模型把某些权重放得过于极端。对普通线性层它常常有用;对 embedding 层则更微妙,因为一个罕见词本来就需要靠较明显的向量方向表达自己,粗暴拉小可能会把语义也拉淡。

但也有人会对 embedding 加 dropout(随机置零某些 embedding 维度)来正则化。这个做法是经验性的,不一定都有效。

dropout 可以理解成训练时故意遮住一部分信号,逼模型不要太依赖某几个维度。比如某个词向量有 768 维,dropout 会随机把其中一部分维度暂时置零。推理时不再随机遮挡,而是使用完整向量。

8.5 词表越大,embedding 越大

GPT-4 的词表大约 10 万词,d12288d \approx 12288,embedding 矩阵就有 10910^9 量级参数。这是模型总参数里相当可观的一块。

LLaMA 通过 BPE 把词表压到 32K 来节省 embedding 参数。这是「词表大小 vs 序列长度」的权衡:词表小则单 token 信息密度低、序列变长;词表大则反之。

这个权衡非常具体:词表越大,embedding 表越宽,参数越多;词表越小,同一句话会被拆成更多 token,后面的 attention 成本又会上升。tokenizer 设计不是纯文本预处理,而是直接影响模型参数量和推理成本的工程选择。

8.6 ALBERT 的因子分解

ALBERT(Lan 2020)注意到 BERT 的 embedding 矩阵 V×dV \times d 是巨大的浪费:VV 很大但 embedding 学到的真实秩远小于 dd

ALBERT 把 embedding 分成两步:先 V×eV \times eee 较小,比如 128),再 e×de \times d。前者是真正的「词到语义」的映射,后者是「语义到模型隐层维度」的投影。

前向时的计算是:token ID 先查小表 EsmallRV×eE_{small}\in\mathbb{R}^{V\times e},得到 ee 维向量;再乘投影矩阵 PRe×dP\in\mathbb{R}^{e\times d},得到 Transformer 需要的 dd 维输入。训练时两块都参与反向传播:某个 token 行会被更新,投影矩阵也会被更新。它不是简单的压缩文件,而是把“词表规模”和“模型隐藏维度”这两个本来绑死的量拆开训练。

这可以类比图像压缩:不直接存一张巨大的原图,而是先存一个小的中间表示,再用另一张变换表把它扩展到模型需要的宽度。如果 embedding 矩阵本来就有很多冗余,这种分解就能省大量参数。

参数量:原本 Vd=30000×768=23MVd = 30000 \times 768 = 23M,分解后 Ve+ed=30000×128+128×768=3.94MVe + ed = 30000 \times 128 + 128 \times 768 = 3.94M。节省 80%。

这是「embedding 矩阵其实是低秩的」这个观察的工程兑现。后来很多大模型都借鉴了类似的思路,尤其是多语言模型(词表特别大)。

「低秩」的直觉是:虽然表面上有很多列,但真正独立变化的方向没那么多。就像一份用户画像可能有上百个字段,但其中很多字段高度相关,最后只需要少数几个潜在因子就能解释大部分差异。

8.7 LLM 训练里的 embedding 分片

数千亿参数的 LLM 训练时,embedding 矩阵不能放在单卡上。常见做法:把 vocab 维度切分(vocab parallel),让每张卡持有词表的一部分,前向时用 all-gather 汇集 logit。

vocab parallel 的意思是按词表行切开:第一张卡负责 token 0 到 9999,第二张卡负责 10000 到 19999,以此类推。all-gather 则是多卡之间把各自算出来的片段互相收集起来,拼成完整结果。你不需要手写这些通信细节,但要知道 embedding 表不是小配角,它常常是大模型分布式训练里必须认真切分的对象。

输出层也常按 vocab 切。每张卡只算自己那一片词表的 logits,然后大家一起做分布式 softmax:先求全局最大值避免数值溢出,再求全局分母,最后只在本卡持有的 logits 上算本地 loss 贡献。反向时,每张卡只更新自己那片输出 embedding。这个过程听起来琐碎,但它决定了十万词表、万维 hidden state 的模型能不能放进多卡训练。

这种切分细节在 Megatron-LM、DeepSpeed 等框架里有专门的实现。你不需要自己写,但需要知道为什么 embedding 是分布式训练里的关键瓶颈。

8.8 embedding 的稀疏激活

token embedding lookup 本质上是稀疏的:每个 batch 只激活若干行。如果 embedding 矩阵是 sparse 的(大部分词从不出现),lookup 是高效的;但梯度更新需要小心——只更新激活过的行,否则浪费计算。

PyTorch 的 nn.Embedding(sparse=True) 选项对应这种语义。但 Adam 优化器和 sparse gradient 配合得不好(Adam 需要全量统计),所以现代实现常常用 dense 更新 + 稀疏 lookup 的组合。

Adam 需要为每个参数维护动量和二阶矩估计,相当于每个参数旁边还有两份小账本。对巨大 embedding 表来说,如果这些账本也要按稀疏方式维护,框架和优化器实现会更复杂,所以工程实现常常在理论节省和系统简单之间折中。

8.9 embedding 的 weight decay

embedding 矩阵是否要加 weight decay?

实践上的争论:加 decay 让所有 embedding 趋零,会模糊罕见词;不加 decay 让高频词的 embedding 自由膨胀,可能 dominate。

折中方案:embedding 不加 decay,但加 LayerNorm 让范数受控。或者只对部分维度加。这些细节是 LLM 训练的工程经验,没有定论。GPT-2 / LLaMA 的具体实现各有侧重。

8.10 embedding 的 dropout

经典 NLP 模型里有时会对 embedding 加 dropout:随机把某一维度置零,作为正则化。BERT 在 embedding 之后用了 0.1 的 dropout rate。

但现代大模型对 embedding dropout 的态度复杂。GPT-3 之后很多模型不再用 dropout(数据量足够大,过拟合不是主要问题)。这又是一个「随时代变化」的工程默认值。

九、嵌入空间的几何

9.1 各向异性问题

研究发现现代 contextual embedding(BERT、GPT)有一个「各向异性」(anisotropy)问题:embedding 集中在一个细长的「锥」里,几乎所有词的余弦相似度都偏高。

各向同性(isotropy)可以理解成「点在空间里比较均匀地散开」;各向异性(anisotropy)则是「点都挤到某几个方向上」。如果所有向量都挤在同一个方向附近,那么任意两个词看起来都挺相似,余弦相似度就失去了区分力。

各向同性与各向异性示意转存失败,建议直接上传图片文件

这意味着「相似性」的区分度被压缩了。Ethayarajh 2019 的论文 "How Contextual are Contextualized Word Representations?" 系统研究了这个现象。

9.2 isotropy 修正

针对各向异性,有几个修正方法:减去均值(PCA centering)、whitening、对比学习目标。SimCSE(Gao 2021)就是一个用对比学习显著改善 sentence embedding 各向异性的工作。

PCA centering 的直觉是:先找出所有 embedding 共同偏向的中心方向,再把这个公共偏移减掉。whitening 更进一步,会把不同方向的方差拉得更均衡。对比学习则是训练目标层面的修正:让相似句子更近,不相似句子更远,直接把空间整理成更适合检索的形状。

这些修正都有很具体的算法形态。PCA centering 通常这样做:收集一批 embedding,算均值 μ\mu,把每个向量先变成 xμ\mathbf{x}-\mu;再对所有向量做 PCA,找到方差最大的前几个主方向;最后从每个向量里减去它在这些公共方向上的投影。直觉上,这些公共方向往往是“所有句子都有的背景成分”,减掉后语义差异会更突出。

whitening 再进一步:先算协方差矩阵 Σ\Sigma,做特征分解 Σ=UΛU\Sigma=U\Lambda U^\top,然后把向量变换为

z=(xμ)UΛ1/2.\mathbf{z}=(\mathbf{x}-\mu)U\Lambda^{-1/2}.

这样处理后,各个方向的方差被拉到接近 1,空间更像一个均匀球体。它不是训练新模型,而是对已经拿到的 embedding 做后处理。

SimCSE 这类对比学习则是真训练。无监督 SimCSE 会把同一句话送进模型两次,因为 dropout 随机性不同,得到两条略有差异的向量,把它们当正样本;同一个 batch 里的其他句子当负样本;用 InfoNCE loss 训练,让正样本余弦相似度高、负样本低。监督 SimCSE 则用 NLI 数据:蕴含句对当正样本,矛盾句对当 hard negative。这样训出来的句向量更适合检索和相似度计算。

理解:embedding 的几何不是天然完美的,不同的训练目标会塑造不同的几何形态。如果你做 retrieval,要特别关注 embedding 几何的均匀性。

9.3 embedding 的可解释性

word2vec 时代有不少工作分析 embedding 各维度的语义:「这一维好像对应性别」、「那一维好像对应数量」等等。

但这里要小心:embedding 的单个维度通常不是人类可读的标签。更常见的情况是,一个语义概念分散在很多维度里,而某个维度也参与表达多个概念。把某一维强行解释成「性别维」或「复数维」,往往只是粗略近似。

但 contextual embedding 时代这种分析变难了——每一维不再有清晰的语义对应。embedding 变成黑盒。这是表达力增强的代价。

第 47 篇可解释性会再回到这个话题。

9.4 embedding 的探针实验

「probing」是一种系统研究 embedding 学到了什么的方法:在固定 embedding 上训练一个简单分类器(线性或浅层 MLP),看它能不能预测某种语言学属性。

probing 的步骤很朴素:先冻结大模型,不再改它;然后取出某一层的向量;最后训练一个很小的模型去猜标签。这个小模型越简单,越能说明信息已经明显写在 embedding 里。如果要用很深很复杂的模型才能猜出来,那就很难说是 embedding 自己学到了,还是 probe 模型额外学出来的。

如果一个简单分类器能从 BERT 第 X 层 embedding 准确预测 POS 标签,说明那一层的 embedding 编码了 POS 信息。这种实验设计让我们能说「BERT 在第 5 层学到了语法」。

POS 是 part-of-speech,也就是词性,例如名词、动词、形容词。MLP 是多层感知机,可以先理解成几层普通的全连接神经网络。

probing 的局限:它只说明信息「存在」,不说明信息「被使用」。可能存在但下游任务从未利用。

一个标准 probe 实验可以这样做:先准备带标签的数据,比如每个 token 的 POS 标签;用冻结的 BERT 跑这些句子,保存第 ll 层每个 token 的 hidden state;训练一个小线性分类器 Wh+bW\mathbf{h}+b 去预测 POS;只更新这个小分类器,不更新 BERT。如果线性 probe 准确率很高,说明 POS 信息在这一层已经接近线性可分。为了防止 probe 自己太强,研究里常限制分类器容量,或者报告 control task,确认不是 probe 记住了数据。

9.5 embedding 的对齐

不同模型、不同训练数据训出来的 embedding 在不同的坐标系下。要把它们对齐(比如把 word2vec 和 GloVe 对齐到同一空间),需要 Procrustes 变换或类似的正交变换。

「坐标系不同」可以这样理解:一张地图可以把北画在上面,也可以旋转 30 度后再画。地图上城市之间的相对距离没变,但坐标数值全变了。不同 embedding 模型也类似,绝对坐标不重要,重要的是点和点之间的相对关系。Procrustes 变换就是找一个旋转/翻转,让两套点云尽量对齐。

跨语言 embedding 对齐是这个问题的应用:把英文 embedding 和中文 embedding 对齐到同一空间,让 cat 的向量靠近。这是早期机器翻译里的一个研究方向。

Procrustes 对齐也不是玄学。假设你有一小本双语词典,里面有 nn 对已知翻译词。把英文向量堆成矩阵 XRn×dX\in\mathbb{R}^{n\times d},中文向量堆成 YRn×dY\in\mathbb{R}^{n\times d},目标是找一个正交矩阵 RR,让 XRXR 尽量接近 YY

minRXRYF,RR=I.\min_R \|XR-Y\|_F, \quad R^\top R=I.

解法是对 XYX^\top Y 做 SVD:XY=UΣVX^\top Y=U\Sigma V^\top,然后取 R=UVR=UV^\top。这个 RR 只旋转/翻转空间,不拉伸距离,所以能尽量保留原 embedding 的内部几何结构。

9.6 embedding 的 norm 分布

观察一个有趣现象:高频词的 embedding 范数往往比低频词小。直觉解释:高频词在很多上下文出现,需要在 embedding 空间里「居中」(任意上下文都能容易和它做点积)。低频词只在少数上下文出现,可以在远离中心的「角落」里。

范数就是向量长度。方向更多表达「像什么」,长度有时会混入「这个词有多特殊、多有信息量」这样的信号。高频虚词因为哪里都能出现,往往不适合站到某个特别尖锐的语义方向上。

这种范数差异有时被作为「informativeness」的代理:低频词常常更 informative,它们的 embedding 范数更大。

9.7 t-SNE 与 UMAP 可视化

embedding 是高维(数百维)对象,肉眼看不见。可视化常用工具是 t-SNE(van der Maaten & Hinton 2008)和 UMAP(McInnes 2018),它们把高维点映射到 2D 或 3D,保持局部邻近关系。

t-SNE 的优点是聚类清晰,缺点是「全局结构」不可信(远距离两个聚类的相对位置不反映真实距离);UMAP 在保持全局结构上更好,速度也快。

t-SNE / UMAP 都是在做一件有损压缩:把几百维甚至几千维的点压到二维纸面上。压缩时会尽量保留「谁和谁是邻居」,但不保证远处的距离、方向、面积仍然可信。所以图上两个簇离得远,不一定表示原始空间里真的远那么多。

t-SNE 的算法直觉是:先在高维空间里把“谁离谁近”转成一组邻居概率,再在二维空间里摆放点,让二维里的邻居概率尽量接近高维里的邻居概率。它优化的是 KL divergence,所以特别在意局部邻居是否保住,常把簇画得很分明。

UMAP 的做法更像先建一张近邻图:每个点连向它的若干近邻,边权表示局部距离;再在二维里优化一张相似的图。UMAP 通常比 t-SNE 快,也更努力保留一点全局结构,但它仍然是可视化算法,不是语义距离计算器。

可视化时一个常见误区:把 t-SNE/UMAP 上的距离当作「真实语义距离」。这是错的,可视化只是定性的,定量比较仍要回到原始 embedding 空间用余弦相似度。

9.8 球面投影:normalize 后的余弦

在很多场景下,会先把 embedding 归一化(除以自己的范数),让所有 embedding 落在单位球面上。这样 dot product 就等价于余弦相似度。

余弦相似度只看两个向量夹角,不看长度。归一化以后,每个向量长度都变成 1,点积自然只剩下方向相似性。这对语义检索很有用,因为我们通常更关心「意思方向像不像」,而不想让向量长度过度影响排名。

球面归一化的好处:去掉范数差异(避免高频词 dominate),比较纯粹的「方向相似性」;几何上让 embedding 集中在固定区域,方便检索结构(HNSW 等数据结构对单位向量更友好)。

在很多 retrieval 场景,embedding 直接发布的就是单位向量。OpenAI text-embedding-3 等都是这样。

单位化的计算很直接:

x^=xx2.\hat{\mathbf{x}}=\frac{\mathbf{x}}{\|\mathbf{x}\|_2}.

归一化后,两个向量的点积就是余弦相似度:x^y^=cos(θ)\hat{\mathbf{x}}^\top\hat{\mathbf{y}}=\cos(\theta)。检索系统里常把所有文档向量先离线归一化并建 ANN 索引,线上查询向量也归一化,再用内积检索。这个小步骤会直接影响召回排序,不是可有可无的数学装饰。

9.9 hubness 现象

高维 embedding 空间里有一个反直觉现象:「中心化」的某些点会成为「枢纽」(hub),即很多查询的最近邻都是同一批点。这种现象在维度 d>100d > 100 后开始显现,并且越高维越严重。

hubness 影响 retrieval 的多样性:top-k 检索经常被几个 hub 主导,导致结果重复。Suzuki 2013 等工作提出过修正方法(mutual k-NN、reciprocal rank 等)。

hub 可以想成高维空间里的「万能邻居」:它不一定真的和每个查询最相关,但由于站在空间中心附近,很多查询都会把它排进最近邻。mutual k-NN 的想法是加一道互相确认:不只要求 A 觉得 B 近,也要求 B 觉得 A 近,从而过滤掉一部分单向的假近邻。

这也是「球面归一化 + 余弦相似度」流行的原因之一:在归一化的空间里,hubness 比纯欧氏距离弱一些。

处理 hubness 的方法也很工程化。mutual k-NN 要求两个点互相出现在对方的 top-k 近邻里,单向“我觉得你近”不算数。CSLS(cross-domain similarity local scaling)则会扣掉某个点在邻域里的平均相似度:如果一个点对谁都挺近,它的分数就被下调。跨语言检索和 embedding 对齐里经常用 CSLS,就是为了防止少数万能邻居霸榜。

9.10 embedding 的稳定性与可重复性

同一份训练数据,相同超参数,不同随机种子下训练的 embedding 在数值上完全不同。但「邻居关系」是稳定的——「dog」最近邻总是「cat、puppy、pet」之类。

这是一个深刻的现象:embedding 的「绝对坐标」无意义,只有「相对关系」有意义。这跟物理学里的「坐标系不变性」类似——重要的是观察者无关的几何性质,不是具体数值。

十、关键概念回顾

embedding 是把离散的词翻译成连续向量的桥。从 70 年前 Firth 的「You shall know a word by the company it keeps」开始,分布假设奠定了一切的基础。

word2vec 把分布假设变成了高效的神经网络训练算法,CBOW 和 Skip-gram 两个模型,加上 negative sampling 加速,定义了 2013-2018 年的 NLP 主流范式。GloVe 在同一时期提出全局共现的视角,与 word2vec 殊途同归。

ELMo、BERT 把 embedding 推进到「上下文化」的新阶段:同一个词在不同句子里有不同向量,解决了一词多义问题。Transformer 时代,embedding 不再是预训练的产物,而是端到端学习的一部分。tied weights、subword tokenization、位置编码等机制让现代 LLM 的 embedding 既高效又灵活。

理解了这条 70 年的演化线,再看 Transformer 的输入端,就不只看到「一个矩阵 + 一个查表」,而能看到「分布假设 → 共现统计 → 神经网络 → 端到端学习」这一路的演变。每一步都是对前一步的回应和改进。

十一、常见误解

11.1 「上下文 embedding 一定比静态好」

不一定。在某些低资源、对延迟敏感的任务里,静态 embedding 仍然是更好的选择。BERT 跑一次比 word2vec 慢百倍,准确率提升不一定值。

11.2 「embedding 一旦训好就不该改」

不对。下游任务往往要 fine-tune embedding。一刀切「冻结 embedding」会丧失任务自适应能力。LLM 时代的标准做法是端到端微调,包括 embedding 层。

11.3 「embedding 是模型的一部分独立模块」

不准确。在端到端训练的现代模型里,embedding 和其他层是耦合的——embedding 的最佳取值依赖于后面的 transformer 层。把 embedding 单独抽出来用,效果不如端到端训练。

很多人会想「我能不能拿别人 BERT 训好的 embedding 直接用」。可以用作初始化,但通常需要 fine-tune 才能发挥作用。

11.4 「embedding 维度越高越好」

不一定。维度太高会导致:参数量爆炸、点积统计性质漂移、下游小数据微调容易过拟合。维度选择和模型其他超参(heads、layers)必须协调。

经验上:word2vec 50-300,BERT 768,GPT-3 12288。维度选择往往跟随 embedding 之外的整体宽度,而不是独立旋钮。

11.5 「越大模型 embedding 越好」

不一定。LLM 内部的 hidden state 维度往往很大(几千),但用作 embedding 时不一定就更优秀。专门做 retrieval 的 embedding 模型(如 BGE、E5)虽然小(几百兆参数),但在 retrieval benchmark 上经常打败 GPT-4 hidden state。

原因:LLM 训练目标是「next-token prediction」,不是「相似性检索」。两者用到的「中间表征」性质不同。embedding 任务需要专门的对比学习目标。

十二、embedding 的偏见与公平性

12.1 embedding 学到了人类的偏见

word2vec 和 GloVe 训练完之后人们发现一些尴尬的结果:

embedding("man") - embedding("woman") ≈ embedding("computer programmer") - embedding("homemaker")

也就是说 embedding 把「程序员」和「男性」、「家务」和「女性」连到了一起。这不是偶然——训练语料(Wikipedia、新闻)里就这样写,分布假设忠实地把它学了下来。

Bolukbasi 2016 的 "Man is to Computer Programmer as Woman is to Homemaker?" 系统揭示了这种偏见。这是 ML 公平性研究的标志性论文之一。

这里的偏见不是模型「有主观态度」,而是统计关联被写进了向量空间。训练语料里某些职业、性别、地域、族群经常以不平衡方式出现,embedding 会把这种不平衡当成语言规律学下来。后续系统如果直接使用这些向量,就可能把历史数据里的偏差当成现实判断标准。

12.2 偏见来源

embedding 偏见的根源是训练数据本身的统计。如果你在 1900 年的英语语料上训 embedding,「医生」会强烈关联男性;2020 年的语料就会更平衡。embedding 是社会语言习惯的镜子。

这给我们一个 warning:embedding(以及更下游的 LLM)会继承训练数据里的所有偏见——种族、性别、地域、宗教。「中性技术」不存在,数据有 bias,模型就有 bias。

bias 在这里指系统性偏差,不是普通随机误差。随机误差像噪声,正负方向可能互相抵消;系统性偏差会稳定地把某些群体、职业或概念推向同一个方向,因此更危险。

12.3 debiasing 的尝试

研究界提出过多种「去偏见」方法:在 embedding 空间里找出「性别方向」并投影掉、用对抗训练让 embedding 不可被分类器分出性别等。

「投影掉」可以想成在向量空间里找出一条敏感方向,例如从 woman 指向 man 的方向,然后把某些职业词在这条方向上的分量减掉。对抗训练则像安排一个监督者:如果一个分类器还能轻易从 embedding 里猜出性别,说明敏感信息仍然明显,训练就继续惩罚这种可识别性。

Bolukbasi 那类 hard debias 方法大概分三步:第一,准备一些定义性词对,比如 (man, woman)(he, she),用它们的差向量估计“性别子空间”;第二,对应该中性的词,比如 doctorprogrammerteacher,减去它们在性别子空间上的投影;第三,对某些成对词做 equalize,让 grandfather/grandmother 这类词到中性词的距离保持对称。它是一个明确的线性代数后处理算法。

对抗 debias 则是训练时加一个 adversary。主模型负责做原任务,例如预测上下文或分类;对抗分类器负责从 embedding 里预测敏感属性,例如性别。训练时主模型一边要做好原任务,一边要让对抗分类器猜不出敏感属性。实现上常用 gradient reversal:对抗分类器正常最小化自己的 loss,主模型收到反向符号的梯度,等于主动抹掉那些容易暴露敏感属性的信息。

但这些方法多被后续工作证明只是「藏起来」了偏见,本质并没有消除——简单的 probe 仍然能从「去偏见」后的 embedding 里恢复性别信息。这告诉我们:bias 不是 embedding 的某个独立维度,而是渗透在整个表征里的统计模式。

真正的解决途径是从数据源头改。但语言数据反映社会,社会本身有 bias,这就成为一个棘手的循环。

12.4 偏见的下游放大

embedding 的偏见会在下游任务里被放大。例如简历筛选系统用 embedding 算「候选人 ↔ 职位」相似度,如果 embedding 里「程序员 → 男性」的关联强,女性候选人就会被系统性低估。

Caliskan 2017 的 "Semantics Derived Automatically from Language Corpora Contain Human-Like Biases"(发表在 Science)系统地展示了这种放大。这是 ML 公平性研究里最有影响力的论文之一。

所谓下游放大,是指 embedding 里的轻微相关性到了决策系统里可能变成硬结果。向量空间里只是「稍微更近」,排序系统里就可能变成「排在前十」或「排不进前十」;分类系统里就可能变成「通过」或「拒绝」。

这给所有用 embedding 的工程师一个责任:你的系统不是中立的,它继承了训练数据的所有偏见,并且在决策里放大它们。设计 ML 系统时把这条放在心上。

12.5 持续的开放问题

偏见处理仍是 NLP 公平性研究的 open question。包括:怎么定量度量 embedding 偏见?多种偏见(性别、种族、地域)能否同时去除?去偏见和保持下游任务效果之间的 trade-off 在哪?

这些问题没有定论。embedding 既是技术工件,也是社会工件。它的研究越深入,对社会的影响也越深入——这是 NLP 研究者必须正视的一个面向。

十三、下一步

embedding 把词变成向量后,下一个问题是:怎么处理由这些向量构成的序列? 答案历史上是 RNN,未来是 Transformer。我们先讲 RNN。

09 RNN 与序列建模 会从 Vanilla RNN 开始,讲 BPTT、梯度消失爆炸,引出 LSTM 和 GRU,最后到 Seq2Seq 框架。这是 Transformer 出现之前所有序列建模的故事。

10 RNN 的根本局限 会讲 RNN 的「三难」(长程依赖、梯度稳定、训练并行),解释为什么需要一个新范式来取代它,以及 Vaswani 2017 是怎么解决这个问题的。

十四、参考文献

  1. Firth, J. R. (1957). A Synopsis of Linguistic Theory, 1930-1955. Studies in Linguistic Analysis. 「分布假设」的语言学起源。
  2. Harris, Z. S. (1954). Distributional Structure. Word. 与 Firth 同期的另一版本。
  3. Bengio, Y., Ducharme, R., Vincent, P., Janvin, C. (2003). A Neural Probabilistic Language Model. JMLR. 神经网络词向量的鼻祖。
  4. Mikolov, T., Chen, K., Corrado, G., Dean, J. (2013). Efficient Estimation of Word Representations in Vector Space. ICLR Workshop. word2vec 第一篇。
  5. Mikolov, T., Sutskever, I., Chen, K., Corrado, G., Dean, J. (2013). Distributed Representations of Words and Phrases and their Compositionality. NeurIPS. word2vec 第二篇 (NEG)。
  6. Pennington, J., Socher, R., Manning, C. D. (2014). GloVe: Global Vectors for Word Representation. EMNLP. GloVe 论文。
  7. Bojanowski, P., Grave, E., Joulin, A., Mikolov, T. (2017). Enriching Word Vectors with Subword Information. TACL. fastText。
  8. Levy, O., Goldberg, Y. (2014). Neural Word Embedding as Implicit Matrix Factorization. NeurIPS. word2vec 与矩阵分解的等价。
  9. Peters, M. E. et al. (2018). Deep Contextualized Word Representations. NAACL. ELMo。
  10. Devlin, J., Chang, M.-W., Lee, K., Toutanova, K. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. NAACL. BERT。
  11. Press, O., Wolf, L. (2017). Using the Output Embedding to Improve Language Models. EACL. Tied embedding。
  12. Ethayarajh, K. (2019). How Contextual are Contextualized Word Representations? Comparing the Geometry of BERT, ELMo, and GPT-2 Embeddings. EMNLP. 各向异性。
  13. Gao, T., Yao, X., Chen, D. (2021). SimCSE: Simple Contrastive Learning of Sentence Embeddings. EMNLP.

上一篇:07 Softmax 与概率分布

下一篇:09 RNN 与序列建模

回到:Transformer 与注意力机制 总览