词嵌入深度实战:从分布假设、Word2Vec 负采样到 GloVe 与上下文化表示的工程全解
词嵌入(Word Embedding)是把离散符号映射为连续稠密向量的技术,它是一切现代 NLP 与 LLM 的底层地基:Transformer 的第一层就是一个巨大的嵌入查找表,检索式增强(RAG)的"语义向量"本质也是嵌入,甚至连多模态对齐都依赖共享嵌入空间。本文从分布假设的第一性原理出发,推导 Word2Vec 负采样与 GloVe 的数学,给出可复现的 PyTorch 实现,并落到 Transformer 嵌入层与工程化检索嵌入的真实生产细节。
一、为什么需要嵌入:从符号表到连续向量空间
语言模型最早用 one-hot 表示词:词表大小为 V,每个词是一个 V 维单位向量。它在工程上立刻暴露三个致命问题:
| 维度 | one-hot 的问题 | 嵌入的解法 |
|---|---|---|
| 维度灾难 | 词表 10 万 → 每词 10 万维稀疏向量,矩阵无法存 | 压缩到 100~4096 维稠密向量 |
| 正交性 | 任意两 one-hot 内积为 0,"猫"与"狗"毫不相似 | 语义相近的词向量内积/余弦相近 |
| 泛化 | 未见组合无法推理 | 连续空间可插值、可类比 |
核心思想来自 分布假设(Distributional Hypothesis):"出现在相似上下文中的词,语义相似"(Harris, 1954;Firth, 1957)。嵌入的学习目标,就是让共现模式编码进向量几何。
二、经典静态嵌入的第一性原理
2.1 Word2Vec:CBOW 与 Skip-gram
Word2Vec(Mikolov et al., 2013)给出两个互为对偶的架构:
- CBOW:用上下文窗口词预测中心词(适合高频词、训练快)。
- Skip-gram:用中心词预测上下文词(对低频词、罕见组合更鲁棒,工业界默认)。
Skip-gram 的朴素目标是最大化平均对数似然:
$$
\frac{1}{T}\sum_{t=1}^{T}\sum_{-c\le j\le c, j\ne 0} \log p(w_{t+j}\mid w_t)
$$
其中 $c$ 为窗口半宽。朴素 softmax 概率需对全词表归一化,复杂度 $O(V)$,无法接受。
2.2 负采样:NCE 的简化
Word2Vec 用 Negative Sampling 把多分类变成二分类。对每个正样本 $(w_o, w_c)$,采样 $k$ 个负词 $w_{neg}\sim P_n(w)$(通常用 $P_n(w)^{3/4}$ 平滑 unigram 分布)。损失为:
$$
\mathcal{L} = -\log\sigma(\mathbf{v}'_{w_o}\!^\top \mathbf{v}_{w_c})
- \sum_{i=1}^{k}\mathbb{E}_{w_i\sim P_n}\big[\log\sigma(-\mathbf{v}'_{w_i}\!^\top \mathbf{v}_{w_c})\big]
$$
其中 $\mathbf{v}_w$ 是输入(center)向量,$\mathbf{v}'_w$ 是输出(context)向量,$\sigma$ 为 sigmoid。直觉:拉高真实上下文词的得分,压低噪声词的得分。$k$ 通常取 5~15(小语料取更大)。
2.3 层级 Softmax(备选)
用 Huffman 树把词表组织成二叉树,每个词是叶子,概率分解为路径上二分类乘积,复杂度降到 $O(\log V)$,对低频词更友好。但它依赖词频构建树、实现更复杂,负采样因其简洁成为事实标准。
2.4 GloVe:共现矩阵上的加权最小二乘
GloVe(Pennington et al., 2014)不走局部滑窗似然,而是直接对全局 共现计数矩阵 $X_{ij}$(词 $i$ 在词 $j$ 上下文出现的次数)建模。核心洞察:词向量点积应等于共现的对数:
$$
\mathbf{w}_i^\top \tilde{\mathbf{w}}_j + b_i + \tilde{b}_j \approx \log X_{ij}
$$
损失为加权最小二乘:
$$
\mathcal{J} = \sum_{i,j=1}^{V} f(X_{ij})\,
\big(\mathbf{w}_i^\top \tilde{\mathbf{w}}_j + b_i + \tilde{b}_j - \log X_{ij}\big)^2
$$
权重 $f(x)=\min(1,(x/x_{\max})^\alpha)$($\alpha\approx0.75$)抑制高频噪声、保护稀有共现。GloVe 兼具全局统计信息与矩阵分解的可解释性。
2.5 fastText:子词 n-gram 与 OOV
fastText(Bojanowski et al., 2017)把词表示为其 字符 n-gram(3~6 元)向量之和,因此能为零样本 OOV 词(拼写变体、新词、形态衍生)合成向量,对小语种与形态丰富语言尤其有效。代价是词表需存 n-gram 而非整词,内存更高。
三、从零实现:PyTorch 中的 Skip-gram + 负采样
下面是可训练的最小可复现实现(教学规模,工业用 gensim/torch.nn.Embedding 的噪声对比损失)。
import torch
import torch.nn as nn
import torch.nn.functional as F
from collections import Counter
import random
class SkipGramNeg(nn.Module):
def __init__(self, vocab_size, embed_dim, unigram_counts, neg_samples=5):
super().__init__()
self.in_emb = nn.Embedding(vocab_size, embed_dim) # 中心词
self.out_emb = nn.Embedding(vocab_size, embed_dim) # 上下文词
self.neg_samples = neg_samples
# 负采样分布 P_n(w)^(3/4)
self.noise_dist = torch.tensor(
[c**0.75 for c in unigram_counts], dtype=torch.float
)
def forward(self, center, context, neg_mask=None):
# center/context: [B]
v_c = self.in_emb(center) # [B, d]
v_o = self.out_emb(context) # [B, d]
pos = torch.bmm(v_c.unsqueeze(1), v_o.unsqueeze(2)).squeeze() # [B]
pos_loss = F.logsigmoid(pos).mean()
# 采样负词: [B, neg]
neg = torch.multinomial(self.noise_dist, center.size(0)*self.neg_samples, replacement=True)
neg = neg.view(center.size(0), self.neg_samples)
v_neg = self.out_emb(neg) # [B, neg, d]
neg_score = torch.bmm(v_c.unsqueeze(1), v_neg.transpose(1,2)).squeeze(1) # [B, neg]
neg_loss = F.logsigmoid(-neg_score).mean()
return -(pos_loss + neg_loss)
# 训练循环(伪代码骨架)
# model = SkipGramNeg(V, d, counts)
# opt = torch.optim.SGD(model.parameters(), lr=0.025)
# for center, ctx in batches:
# loss = model(center, ctx)
# loss.backward(); opt.step()
要点:logsigmoid 直接对应负采样损失的形式;学习率随训练进度线性退火(lr * (1 - progress))能显著稳定收敛;负采样数 $k$ 越大越平滑但越慢。
四、GloVe 训练循环实现
GloVe 需要先把语料扫一遍建共现矩阵,再最小化加权平方误差。
import torch, torch.nn as nn
class GloVe(nn.Module):
def __init__(self, vocab_size, embed_dim, xmax=100.0, alpha=0.75):
super().__init__()
self.w = nn.Parameter(torch.randn(vocab_size, embed_dim) * 0.01)
self.w_tilde = nn.Parameter(torch.randn(vocab_size, embed_dim) * 0.01)
self.b = nn.Parameter(torch.zeros(vocab_size))
self.b_tilde = nn.Parameter(torch.zeros(vocab_size))
self.xmax, self.alpha = xmax, alpha
def weight(self, x):
return torch.clamp((x / self.xmax) ** self.alpha, max=1.0)
def forward(self, i, j, xij):
# i,j: [B] 词索引; xij: [B] 共现计数
logx = torch.log(xij + 1e-10)
inner = (self.w[i] * self.w_tilde[j]).sum(1) + self.b[i] + self.b_tilde[j] - logx
return (self.weight(xij) * inner.pow(2)).mean()
# 仅对非零共现条目 (i,j,xij) 构造批次,稀疏但高效
建表阶段注意:窗口需对称(左右各 c),共现计数可加距离衰减权重 $1/d$(近邻权重更高)。
五、Transformer 时代:嵌入层如何工作
在 LLM 中,嵌入表就是 nn.Embedding(vocab, d_model),它把 token id 查表成 d_model 维向量。三个工程关键点直接决定训练稳定性与效果:
1. 与位置编码相加。 词嵌入本身不含顺序信息,必须叠加上位置信号(RoPE 则旋转进 q/k,见《相对位置编码深度实战》)。
class TokenEmbedding(nn.Module):
def __init__(self, vocab, d_model):
super().__init__()
self.emb = nn.Embedding(vocab, d_model)
def forward(self, x, pos_emb=None):
h = self.emb(x) * (self.d_model ** 0.5) # 尺度缩放
if pos_emb is not None:
h = h + pos_emb
return h
2. 尺度 $\sqrt{d_{\text{model}}$。 原始 Transformer 把嵌入乘 $\sqrt{d_{\text{model}}$ 再与位置编码相加,抵消后续 LayerNorm 前的大方差(否则高维随机向量范数过大,淹没位置信号)。这是 Pre-LN 时代仍常保留的约定。
3. 初始化与权重共享。 嵌入表建议用截断正态($\sigma=0.02$)或 Xavier;权重共享(tied embedding) 让输入嵌入表与 LM 输出投影头共用同一矩阵(lm_head.weight = emb.weight),可省去 vocab × d 的额外参数、提升小模型表现,前提是嵌入维度等于隐藏维度。
六、现代视角:从静态到上下文化与检索式嵌入
上下文化嵌入。 静态嵌入(Word2Vec/GloVe)一个词只有一个向量,无法区分"苹果手机"与"苹果水果"。ELMo、BERT 一脉用双向语言模型产出 上下文相关 向量——同一词在不同句子中表示不同。这正是 LLM 隐藏状态可被直接当作句/词级嵌入使用的由来。
检索式(稠密)嵌入。 RAG 的语义向量通常由 bi-encoder 产出:查询与文档分别编码为向量,用余弦相似度在向量库检索。工程上:
- 归一化:推理前对向量做 L2 归一化,使内积等价余弦,避免长度主导相似度。
- 温度:对比训练的损失 $\log\frac{e^{\cos(q,d^+)/\tau}}{\sum e^{\cos(q,d)/\tau}}$,温度 $\tau$ 控制分布锐度。
- Matryoshka 表示学习:让同一向量前缀子向量即可用,便于按检索精度/存储成本动态截断维度。
- 量化:生产向量库普遍用
int8(标量量化)或二进制(乘积量化 PQ)压缩,召回率损失 <1% 时存储降 4~32 倍。
七、生产陷阱清单
| 陷阱 | 现象 | 正确做法 |
|---|---|---|
| 低频词欠拟合 | 长尾词向量随机游走 | 提高负采样 $k$,或用 fastText 子词兜底 |
| 未归一化检索向量 | 高范数文档总是"更相似" | 推理前 L2 归一化,再算余弦 |
| 嵌入维度≠隐藏维度却强行 tied | 形状报错 | 仅在 d_emb==d_hidden 时共享权重 |
| 训练集与推理 tokenizer 不一致 | OOV 全变 |
固定 tokenizer 与词表版本,持久化 |
| 共现矩阵未加距离衰减 | 远近上下文同等权重 | 加 $1/d$ 衰减或仅用近窗 |
| 温度 $\tau$ 过大 | 难负例被淹没、区分度差 | 对比训练 $\tau$ 取 0.02~0.1 区间调参 |
| 忘记 $\sqrt{d}$ 缩放 | 位置信号被淹没、收敛慢 | 嵌入输出乘 $\sqrt{d_{\text{model}}$ |
| 向量库未量化 | 内存爆、延迟高 | int8/PQ 压缩,先测召回率损失 |
结语
词嵌入从分布假设出发,经 Word2Vec 负采样、GloVe 全局统计、fastText 子词,演化成今天 Transformer 的嵌入表与检索式稠密向量。理解它的第一性原理——共现如何编码进几何、损失如何把"预测邻居"变成"向量邻近"——能让你在调试 LLM 训练不稳、RAG 召回偏差、向量库成本失控时,直击根因而非盲目调参。它与本系列已发的归一化、位置编码、交叉注意力、残差、学习率与梯度累积共同构成"现代 LLM 内部机制与训练工程"的完整拼图。

发表评论 取消回复