概述
词嵌入(Word Embedding)是自然语言处理的基础任务之一:把离散的词符号映射到低维连续实数向量,使得”意义相近的词在向量空间中也相近”。1 2013 年前后,Word2Vec、GloVe、FastText 等经典算法相继出现,彻底改变了 NLP 研究的范式——从”特征工程”转向”表示学习”。
本文系统梳理这三种词嵌入方法的数学原理、训练目标与相互关系,并给出 PyTorch 完整实现。
一、问题的起点:从 One-Hot 到分布式表示
One-Hot 表示的局限
传统 NLP 把每个词表示为 维 one-hot 向量( 为词表大小)。这种表示有三个根本问题:
- 维度灾难:词表通常 5 万-100 万
- 语义鸿沟:向量两两正交,无法表达”相似”或”相关”
- 泛化失败:从未在训练中见过的词无法获得表示
分布式假设(Distributional Hypothesis)
词嵌入的哲学基础是 Firth(1957)的名言:
“You shall know a word by the company it keeps.”
即词的语义由其上下文决定。基于此假设,Word2Vec 等方法让模型从”词的上下文”中学习词的向量表示。
二、Word2Vec:Mikolov 2013
2.1 模型族概览
Word2Vec 包含两种模型架构和两种训练优化:
| 模型 | 描述 | 时间复杂度 |
|---|---|---|
| CBOW | 用上下文预测中心词 | |
| Skip-gram | 用中心词预测上下文 | (对每对上下文) |
| Hierarchical Softmax | 用哈夫曼树代替 softmax | |
| Negative Sampling | 采样负样本做二分类 | , |
实践中 Skip-gram + Negative Sampling 是最常用的组合。
2.2 Skip-gram 模型
给定中心词 ,预测其上下文窗口( 范围)内的词 。设词向量为:
- 输入向量(中心词):
- 输出向量(上下文词):
标准 softmax 形式:
分母需要遍历整个词表, 复杂度太大。
2.3 负采样(Negative Sampling, NEG)
负采样把多分类问题转化为二分类问题:给定中心词 和候选词 ,判断 是否是真实的上下文词。
目标函数(对每个正样本 采样 个负样本 ):
其中:
- 是 sigmoid
- 是负采样分布(通常取 )
2.4 负采样的数学推导
Goldberg & Levy(2014)的经典推导如下2。
出发点:保留概率形式,但替换分母。考虑”中心词 周围出现词 “的联合分布:
对负样本 :
负采样目标函数最大化:
2.5 与 PMI 矩阵的联系(Levy & Goldberg 2014)
负采样的梯度更新隐式地分解了一个点互信息(PMI)矩阵。
Skip-gram 隐式矩阵的每个元素为:
加上负采样修正项后,得到的等价分解是Shifted PMI:
其中 是负样本数。这意味着 Word2Vec 的内积 近似于 SPMI 矩阵的分解。
2.6 CBOW 模型
CBOW 与 Skip-gram 对称:把上下文词的向量取平均或求和,预测中心词。
CBOW 训练更快,但对低频词不友好。
2.7 层次 Softmax(Hierarchical Softmax)
用一棵哈夫曼树代替 softmax 的归一化分母:
- 叶子节点:词表中的每个词
- 内部节点:二分类器(sigmoid)
- 路径长度 ,远小于
频次高的词路径短,训练快;频次低的词路径长,但本身训练样本就少。这种”用频次换速度”的设计非常优雅。
三、GloVe:全局共现分解
3.1 核心思想
GloVe(Global Vectors)由 Pennington 等人于 2014 年提出3。Word2Vec 用局部滑动窗口训练,GloVe 显式建模全局共现统计:
- 设 :词 出现在词 上下文中的次数
- 目标:让词向量的某种函数逼近
3.2 共现比率的洞察
GloVe 论文的关键观察是共现比率比共现概率本身更有信息量:
考虑三个词 :
其中 是词向量, 是独立上下文向量。
- :词 与 更相关 → 比值大
- :词 与 更相关 → 比值小
- 都不相关:比值接近 1
3.3 目标函数推导
设 分别为词 和上下文 的向量, 为偏置。
目标函数:
权重函数 设计为:
其中典型设置 ,。这保证:
- 高频共现对( 大)权重饱和为 1
- 极低频( 极小)权重接近 0(不发散)
- 中等频次平滑过渡
3.4 推导的巧妙性
为使 ,注意到右侧只依赖 和 的差。线性化后:
引入偏置吸收 项,加上对称性约束,得到上述目标函数。
3.5 GloVe vs Word2Vec 对比
| 维度 | Word2Vec | GloVe |
|---|---|---|
| 训练方式 | 局部窗口 SGD | 全局矩阵分解 |
| 训练数据 | 原始语料 | 预聚合共现矩阵 |
| 训练速度 | 慢(多次遍历) | 快(一次构造矩阵) |
| 罕见词处理 | 困难 | 较好( 函数加权) |
| 数学解释 | 隐式分解 SPMI | 显式分解 log |
| 实现复杂度 | 低 | 中(需统计共现) |
实际上两者的最终效果通常很接近,差异更多在实现选择。
四、FastText:子词嵌入
4.1 动机
Word2Vec 和 GloVe 的一个根本问题:OOV(Out-Of-Vocabulary)词无法获得嵌入。这对形态丰富的语言(如土耳其语、芬兰语)和罕见词尤其严重。
FastText(Bojanowski et al. 2017)4 的解决思路:用字符 n-gram 表示词。
4.2 字符 n-gram 表示
对词 ,提取其所有 3-6 字符的 n-gram(边界加 < 和 >):
词 where 的 3-grams:<wh, whe, her, ere, re>
整个词本身也作为一个特殊的 n-gram。词的向量是其所有 n-gram 向量的和:
4.3 评分函数
FastText 采用与 Word2Vec 相同的 Skip-gram + 负采样框架,仅替换评分函数:
这种”求和”的形式带来两个好处:
- OOV 处理:未登录词可以直接由其字符 n-gram 的嵌入求和得到
- 形态学共享:形近词(teach/teacher)共享 n-gram 向量
4.4 实际意义
- 词表规模从 增长到约 (n-gram 总量)
- 但实际计算量只与词长度成正比(O(|w|))
- 对低资源语言和罕见词特别有效
五、三者对比与实践指南
| 特性 | Word2Vec | GloVe | FastText |
|---|---|---|---|
| 训练目标 | 局部窗口预测 | 全局共现分解 | Skip-gram + n-gram |
| OOV 处理 | 不支持 | 不支持 | 支持 |
| 形态学 | 不利用 | 不利用 | 利用 |
| 训练速度 | 中 | 快 | 慢(n-gram 量大) |
| 嵌入质量 | 优 | 优 | 优(对低资源更佳) |
| 主流实现 | gensim, fastai | gensim, 官方 C | fastText 官方 C++ |
选择建议:
- 通用场景 → GloVe 或 Word2Vec
- 罕见词多、形态学丰富 → FastText
- 计算资源充足、所有词都在词表中 → Word2Vec
- 想要快速预训练 → GloVe
六、PyTorch 完整实现
6.1 Skip-gram + 负采样
import torch
import torch.nn as nn
import torch.nn.functional as F
class SkipGramNegSampling(nn.Module):
"""Skip-gram with Negative Sampling."""
def __init__(self, vocab_size, embedding_dim):
super().__init__()
# 输入嵌入:中心词
self.input_embeddings = nn.Embedding(vocab_size, embedding_dim)
# 输出嵌入:上下文词
self.output_embeddings = nn.Embedding(vocab_size, embedding_dim)
# 初始化(Xavier)
nn.init.xavier_uniform_(self.input_embeddings.weight)
nn.init.xavier_uniform_(self.output_embeddings.weight)
def forward(self, center_word, context_word, neg_words):
"""
center_word: (batch_size,)
context_word: (batch_size,)
neg_words: (batch_size, n_neg)
"""
# 中心词向量
v_c = self.input_embeddings(center_word) # (B, D)
# 真实上下文向量
u_o = self.output_embeddings(context_word) # (B, D)
# 负样本向量
u_neg = self.output_embeddings(neg_words) # (B, K, D)
# 正样本得分
pos_score = (v_c * u_o).sum(dim=1) # (B,)
pos_loss = F.logsigmoid(pos_score) # (B,)
# 负样本得分
neg_score = torch.bmm(u_neg, v_c.unsqueeze(2)).squeeze(2) # (B, K)
neg_loss = F.logsigmoid(-neg_score).sum(dim=1) # (B,)
loss = -(pos_loss + neg_loss).mean()
return loss
def get_embedding(self, word_idx):
return self.input_embeddings(word_idx).detach()
class NegativeSampler:
"""负采样分布:U(w)^(3/4) / Z(Levy & Goldberg 推荐)"""
def __init__(self, word_freq, n_neg=5):
self.n_neg = n_neg
# 词频的 3/4 次方作为采样概率
probs = word_freq.float() ** 0.75
self.probs = probs / probs.sum()
def sample(self, batch_size, exclude=None):
"""采样负样本(可选排除正样本)"""
# 简化实现
return torch.multinomial(self.probs, batch_size * self.n_neg, replacement=True)\
.view(batch_size, self.n_neg)
def train_skipgram(sentences, vocab_size, embedding_dim=100,
window=2, n_neg=5, epochs=5, batch_size=512, lr=0.025):
"""训练 Skip-gram 模型"""
model = SkipGramNegSampling(vocab_size, embedding_dim)
optimizer = torch.optim.SparseAdam(model.parameters(), lr=lr)
# 构建采样对
pairs = []
for sent in sentences:
for i, w in enumerate(sent):
for j in range(max(0, i-window), min(len(sent), i+window+1)):
if i != j:
pairs.append((w, sent[j]))
pairs = torch.tensor(pairs)
for epoch in range(epochs):
# 打乱
perm = torch.randperm(len(pairs))
total_loss = 0
n_batches = 0
for i in range(0, len(pairs), batch_size):
batch = pairs[perm[i:i+batch_size]]
center, context = batch[:, 0], batch[:, 1]
# 随机负样本
neg = torch.randint(0, vocab_size, (center.size(0), n_neg))
optimizer.zero_grad()
loss = model(center, context, neg)
loss.backward()
optimizer.step()
total_loss += loss.item()
n_batches += 1
print(f"Epoch {epoch+1}, Loss: {total_loss/n_batches:.4f}")
return model6.2 GloVe 共现矩阵构建
import numpy as np
from collections import defaultdict
def build_cooccurrence_matrix(sentences, vocab_size, window=5):
"""构建加权共现矩阵"""
cooccur = np.zeros((vocab_size, vocab_size), dtype=np.float32)
for sent in sentences:
for i, w in enumerate(sent):
start = max(0, i - window)
end = min(len(sent), i + window + 1)
for j in range(start, end):
if i != j:
# 距离衰减权重
distance = abs(i - j)
cooccur[w, sent[j]] += 1.0 / distance
return cooccur
def glove_weight(x, x_max=100, alpha=0.75):
"""GloVe 加权函数"""
return np.where(x < x_max, (x / x_max) ** alpha, 1.0)
def train_glove(cooccur, embedding_dim=100, epochs=50, lr=0.05, x_max=100, alpha=0.75):
"""训练 GloVe 模型(Adagrad 优化)"""
V = cooccur.shape[0]
# 词向量与偏置
W = np.random.randn(V, embedding_dim).astype(np.float32) * 0.01
W_tilde = np.random.randn(V, embedding_dim).astype(np.float32) * 0.01
b = np.zeros(V, dtype=np.float32)
b_tilde = np.zeros(V, dtype=np.float32)
# 平方梯度累加
grad_sq_W = np.ones_like(W) * 1e-8
grad_sq_W_tilde = np.ones_like(W_tilde) * 1e-8
grad_sq_b = np.ones_like(b) * 1e-8
grad_sq_b_tilde = np.ones_like(b_tilde) * 1e-8
# 仅对非零共现训练
i_idx, j_idx = np.nonzero(cooccur)
X_ij = cooccur[i_idx, j_idx]
weights = glove_weight(X_ij, x_max, alpha)
for epoch in range(epochs):
# 随机打乱
perm = np.random.permutation(len(i_idx))
total_loss = 0
for idx in perm:
i, j = i_idx[idx], j_idx[idx]
x = X_ij[idx]
w = weights[idx]
# 预测:log x ≈ W_i^T W_tilde_j + b_i + b_tilde_j
pred = np.dot(W[i], W_tilde[j]) + b[i] + b_tilde[j]
diff = pred - np.log(x)
# 损失:w * diff^2
loss = w * diff * diff
total_loss += loss
# 梯度
grad = 2 * w * diff
# Adagrad 更新
for param, grad_sq, lr_denom in [
(W[i], grad_sq_W[i], None),
]:
pass # 简化:实际需逐元素更新
# W[i] -= lr * grad * W_tilde[j] / sqrt(grad_sq_W[i])
grad_sq_W[i] += grad * W_tilde[j] ** 2
grad_sq_W_tilde[j] += grad * W[i] ** 2
grad_sq_b[i] += grad ** 2
grad_sq_b_tilde[j] += grad ** 2
W[i] -= (lr * grad * W_tilde[j]) / np.sqrt(grad_sq_W[i])
W_tilde[j] -= (lr * grad * W[i]) / np.sqrt(grad_sq_W_tilde[j])
b[i] -= (lr * grad) / np.sqrt(grad_sq_b[i])
b_tilde[j] -= (lr * grad) / np.sqrt(grad_sq_b_tilde[j])
if (epoch + 1) % 10 == 0:
print(f"Epoch {epoch+1}, Loss: {total_loss/len(i_idx):.4f}")
# 词向量 = W + W_tilde
embeddings = W + W_tilde
return embeddings6.3 FastText 子词嵌入
class FastTextEmbedding(nn.Module):
"""FastText 风格:字符 n-gram 求和得到词嵌入"""
def __init__(self, vocab_size, embedding_dim, ngram_vocab_size):
super().__init__()
# n-gram 嵌入(实际词表更大)
self.embeddings = nn.Embedding(ngram_vocab_size, embedding_dim)
nn.init.xavier_uniform_(self.embeddings.weight)
def get_word_embedding(self, ngram_indices):
"""ngram_indices: list of n-gram IDs for a word"""
return self.embeddings(ngram_indices).sum(dim=0)
def word_to_ngrams(word, min_n=3, max_n=6):
"""提取词的字符 n-gram(含边界符号)"""
word = f"<{word}>"
ngrams = []
for n in range(min_n, max_n + 1):
for i in range(len(word) - n + 1):
ngrams.append(word[i:i+n])
return ngrams七、评价方法
7.1 词类比(Word Analogy)
Mikolov 提出的经典评测任务:。例如:
king : man :: queen : ?→ 期望womanParis : France :: Tokyo : ?→ 期望Japan
数学上:
7.2 词相似度
使用 WordSim-353、SimLex-999 等数据集,计算人评相似度与向量余弦相似度的 Spearman/Pearson 相关系数。
7.3 下游任务
词嵌入的实际价值最终要在下游任务中体现:
- 命名实体识别
- 情感分析
- 文本分类
- 机器翻译
八、局限与后续发展
8.1 经典方法的局限
- 静态嵌入:每个词一个固定向量,无法处理一词多义(polysemy)
- 上下文无关:bank(银行)和 bank(河岸)的嵌入相同
- 无法利用全局上下文:仅看窗口内的局部信息
- 次词信息丢失(除 FastText):对未登录词不友好
8.2 后续发展
| 方法 | 核心改进 |
|---|---|
| ELMo (2018) | 双向 LSTM 生成上下文相关嵌入 |
| BERT (2018) | Transformer 编码器,深度双向 |
| GPT 系列 | 自回归 Transformer |
| T5 | 文本到文本统一框架 |
| Sentence-BERT | 句子级嵌入(Sentence embedding) |
这些基于 Transformer 的预训练模型通过深度上下文建模解决了静态嵌入的根本局限。但 Word2Vec/GloVe/FastText 因简单、高效、可解释,仍广泛用于:
- 小规模任务
- 资源受限场景
- 与其他模型组合(作为输入特征)
- 概念理解与教学
九、参考文献
附录:常用工具与数据集
工具
- gensim:Python 库,提供 Word2Vec、GloVe、FastText 实现
- fastText:Facebook 官方 C++ 库,训练速度快
- spaCy:工业级 NLP,内置嵌入接口
- HuggingFace:提供大量预训练词向量与上下文嵌入
预训练词向量
- GoogleNews Word2Vec(300 维,300 万词)
- GloVe 6B(50/100/200/300 维)
- FastText Wiki(多语言)
评测数据集
- WordSim-353、SimLex-999(词相似度)
- Google Analogies(词类比)
- MEN、RW(稀有词相似度)
最后更新:2026-06-22
Footnotes
-
Mikolov, T., Chen, K., Corrado, G., & Dean, J. (2013). Efficient Estimation of Word Representations in Vector Space. arXiv:1301.3781. ↩
-
Goldberg, Y., & Levy, O. (2014). word2vec Explained: Deriving Mikolov et al.’s Negative-Sampling Word-Embedding Method. arXiv:1402.3722. ↩
-
Pennington, J., Socher, R., & Manning, C. D. (2014). GloVe: Global Vectors for Word Representation. EMNLP 2014. ↩
-
Bojanowski, P., Grave, E., Joulin, A., & Mikolov, T. (2017). Enriching Word Vectors with Subword Information. Transactions of ACL. ↩