第6章:向量语义学(Vector Semantics)
一、导读
1.1 本章学习目标
向量语义学是 NLP 从离散表示走向分布式表示的关键转折点。它的核心思想是:用稠密的低维向量表示词义,语义相近的词在向量空间中距离相近。这一思想彻底改变了 NLP 的技术路线,为后来的深度学习奠定了基础。
本章的核心学习目标包括:
- 理解分布式假设:为什么"一个词的含义由它的上下文决定"?
- 掌握词共现矩阵:如何从语料中构建词-上下文矩阵
- 理解降维方法:SVD/PCA 如何提取潜在语义
- 掌握 Word2Vec:CBOW 和 Skip-gram 模型的原理和实现
- 理解 GloVe:全局矩阵分解与局部上下文窗口的结合
- 了解词向量的评估方法:内在评估和外在评估
1.2 为什么本章如此重要
向量语义学是 NLP 历史上最重要的突破之一。在词向量出现之前,NLP 系统使用离散的符号表示(如 one-hot 编码),无法捕捉词与词之间的语义关系。词向量将词映射为稠密的连续向量,使得"国王 - 男人 + 女人 ≈ 女王"这样的语义运算成为可能。
词向量的影响是深远的:
- 为深度学习铺路:词向量是 NLP 深度学习的标准输入表示
- 迁移学习的先驱:预训练的词向量可以跨任务迁移
- 语义理解的突破:首次让机器能够捕捉词的语义关系
- 启发了后续发展:从静态词向量到上下文相关的 BERT/GPT
理解词向量,是理解现代 NLP 的必经之路。
1.3 核心问题引导
什么是分布式假设?为什么"上下文决定语义"?
词共现矩阵和 SVD 如何产生词向量?它们的局限性是什么?
Word2Vec 的 CBOW 和 Skip-gram 有什么区别?为什么它们如此高效?
GloVe 如何结合全局统计和局部上下文?
词向量能捕捉哪些语义关系?不能捕捉什么?
静态词向量的局限性是什么?为什么需要上下文相关的表示?
二、核心概念详解
6.1 分布式假设
6.1.1 分布相似性
J.R. Firth (1957) 提出:"You shall know a word by the company it keeps."(通过一个词的上下文可以了解它的含义)。
这就是分布式假设(Distributional Hypothesis):如果两个词出现在相似的上下文中,那么它们有相似的语义。
例如,"猫"和"狗"经常出现在相似的上下文中("养___"、"___很可爱"、"喂___"),因此它们的语义相似。
6.1.2 从离散到分布式
离散表示:每个词是一个独立的符号,词与词之间没有关联。One-hot 编码是典型的离散表示:
# One-hot 编码
vocab = {"猫": 0, "狗": 1, "鱼": 2, "汽车": 3}
cat = [1, 0, 0, 0]
dog = [0, 1, 0, 0]
# 猫和狗的向量距离 = 猫和汽车的距离 = √2
# 这显然不合理——猫和狗应该更相似分布式表示:每个词是一个稠密的低维向量,语义相近的词向量距离近:
# 分布式表示(2维示意)
cat = [0.8, 0.3] # 动物维度高,宠物维度高
dog = [0.7, 0.5] # 与猫相近
fish = [0.6, 0.1] # 动物维度高,但宠物维度低
car = [0.1, 0.9] # 动物维度低,机械维度高
# 猫和狗的距离 < 猫和汽车的距离 ✓6.2 词共现矩阵
6.2.1 构建共现矩阵
最直接的方法:统计词在上下文窗口中共现的频率,构建词-上下文矩阵。
import numpy as np
from collections import defaultdict
def build_cooccurrence_matrix(corpus, window_size=2):
vocab = sorted(set(word for sentence in corpus for word in sentence))
word_to_idx = {word: i for i, word in enumerate(vocab)}
vocab_size = len(vocab)
matrix = np.zeros((vocab_size, vocab_size))
for sentence in corpus:
for i, word in enumerate(sentence):
for j in range(max(0, i - window_size), min(len(sentence), i + window_size + 1)):
if i != j:
context_word = sentence[j]
matrix[word_to_idx[word]][word_to_idx[context_word]] += 1
return matrix, vocab
# 示例
corpus = [
["I", "love", "natural", "language", "processing"],
["I", "love", "deep", "learning"],
["natural", "language", "is", "fascinating"],
["deep", "learning", "is", "powerful"],
]
matrix, vocab = build_cooccurrence_matrix(corpus, window_size=2)
print(f"Vocabulary: {vocab}")
print(f"Matrix shape: {matrix.shape}")6.2.2 PPMI(正点互信息)
原始共现频率受词频影响较大。PMI(点互信息)和 PPMI(正 PMI)是更好的度量:
$$\text{PMI}(w, c) = \log \frac{P(w, c)}{P(w) P(c)}$$
$$\text{PPMI}(w, c) = \max(0, \text{PMI}(w, c))$$
def compute_ppmi(cooccurrence_matrix):
total = cooccurrence_matrix.sum()
row_sums = cooccurrence_matrix.sum(axis=1, keepdims=True)
col_sums = cooccurrence_matrix.sum(axis=0, keepdims=True)
p_w = row_sums / total
p_c = col_sums / total
p_wc = cooccurrence_matrix / total
pmi = np.log2(p_wc / (p_w * p_c + 1e-10) + 1e-10)
ppmi = np.maximum(0, pmi)
return ppmi6.3 降维方法
6.3.1 SVD(奇异值分解)
SVD 将共现矩阵分解为三个矩阵的乘积,保留最重要的 k 个奇异值:
$$M = U \Sigma V^T \approx U_k \Sigma_k V_k^T$$
$U_k \Sigma_k^{1/2}$ 可以作为词的低维表示。
from sklearn.decomposition import TruncatedSVD
# 对 PPMI 矩阵进行 SVD 降维
svd = TruncatedSVD(n_components=50, random_state=42)
word_vectors = svd.fit_transform(ppmi_matrix)
print(f"降维后词向量维度: {word_vectors.shape}")
# 计算词相似度
from sklearn.metrics.pairwise import cosine_similarity
def most_similar(word, vocab, vectors, topk=5):
idx = vocab.index(word)
sims = cosine_similarity([vectors[idx]], vectors)[0]
top_indices = sims.argsort()[-topk-1:][::-1]
return [(vocab[i], sims[i]) for i in top_indices if vocab[i] != word]
print(most_similar("love", vocab, word_vectors))6.3.2 LSA(潜在语义分析)
LSA 就是对词-文档矩阵(或词-词共现矩阵)应用 SVD。它是词向量的早期形式。
6.4 Word2Vec
6.4.1 核心思想
Word2Vec (Mikolov et al., 2013) 使用神经网络从大规模语料中学习词向量。它有两种架构:
CBOW(Continuous Bag of Words):用上下文词预测中心词。
Skip-gram:用中心词预测上下文词。
6.4.2 Skip-gram 模型
Skip-gram 的目标是最大化上下文词的概率:
$$\max \frac{1}{T} \sum_{t=1}^{T} \sum_{-c \leq j \leq c, j \neq 0} \log P(w_{t+j} | w_t)$$
条件概率使用 softmax:
$$P(w_O | w_I) = \frac{\exp(\mathbf{v}_{w_O}^T \mathbf{u}_{w_I})}{\sum_{w=1}^{W} \exp(\mathbf{v}_w^T \mathbf{u}_{w_I})}$$
其中 $\mathbf{u}$ 是输入词向量,$\mathbf{v}$ 是输出词向量。
6.4.3 负采样
直接计算 softmax 的分母需要遍历整个词汇表,计算量太大。负采样用二分类任务近似:
- 正样本:(中心词, 上下文词) → 标签 1
- 负样本:(中心词, 随机词) → 标签 0
import numpy as np
class SkipGram:
def __init__(self, vocab_size, embedding_dim, lr=0.025):
self.vocab_size = vocab_size
self.embedding_dim = embedding_dim
self.lr = lr
# 初始化词向量
self.W_in = np.random.randn(vocab_size, embedding_dim) * 0.01
self.W_out = np.random.randn(vocab_size, embedding_dim) * 0.01
def sigmoid(self, x):
return 1 / (1 + np.exp(-np.clip(x, -6, 6)))
def train_step(self, center_word, context_word, negative_samples):
# 正样本梯度
h = self.W_in[center_word]
target = self.W_out[context_word]
pred = self.sigmoid(np.dot(target, h))
grad = (1 - pred) * h
# 更新
self.W_out[context_word] += self.lr * (1 - pred) * h
self.W_in[center_word] += self.lr * (1 - pred) * target
# 负样本
for neg in negative_samples:
target_neg = self.W_out[neg]
pred_neg = self.sigmoid(np.dot(target_neg, h))
self.W_out[neg] -= self.lr * pred_neg * h
self.W_in[center_word] -= self.lr * pred_neg * target_neg
def get_word_vector(self, word_idx):
return self.W_in[word_idx]6.4.4 Word2Vec 的优化技巧
- 高频词降采样:降低高频词(如"的"、"是")的训练频率
- 负采样数量:小数据集用 5-20 个负样本
- 上下文窗口:通常 5-10
- 向量维度:通常 100-300
6.5 GloVe
6.5.1 核心思想
GloVe (Pennington et al., 2014) 结合了全局矩阵分解和局部上下文窗口的优点。它直接优化共现矩阵的对数:
$$J = \sum_{i,j=1}^{V} f(X_{ij}) (\mathbf{w}_i^T \mathbf{w}_j + b_i + b_j - \log X_{ij})^2$$
其中 $f(x)$ 是权重函数,避免高频词对主导损失。
6.5.2 GloVe vs Word2Vec
| 特性 | Word2Vec | GloVe |
|---|---|---|
| 训练方式 | 局部上下文窗口 | 全局共现矩阵 |
| 训练效率 | 高(SGD) | 中(需要构建共现矩阵) |
| 低频词表现 | 较差 | 较好 |
| 实现复杂度 | 简单 | 中等 |
6.6 词向量的评估
6.6.1 内在评估
词相似度任务:与人工标注的词相似度数据比较(如 SimLex-999、WordSim-353)。
类比任务:$a : b :: c : ?$,预测 d。例如,"man : king :: woman : ?" 应该得到 "queen"。
$$\hat{d} = \arg\max_x \cos(\mathbf{v}_x, \mathbf{v}_b - \mathbf{v}_a + \mathbf{v}_c)$$
def word_analogy(a, b, c, vocab, vectors):
"""a : b :: c : ?"""
idx_a = vocab.index(a)
idx_b = vocab.index(b)
idx_c = vocab.index(c)
target = vectors[idx_b] - vectors[idx_a] + vectors[idx_c]
best_word = None
best_sim = -1
for i, word in enumerate(vocab):
if word in [a, b, c]:
continue
sim = np.dot(target, vectors[i]) / (np.linalg.norm(target) * np.linalg.norm(vectors[i]))
if sim > best_sim:
best_sim = sim
best_word = word
return best_word, best_sim6.6.2 外在评估
将词向量作为特征输入下游任务(如文本分类、NER),比较任务性能。
三、实战案例
3.1 使用 Gensim 训练 Word2Vec
from gensim.models import Word2Vec
from gensim.test.utils import common_texts
# 训练 Word2Vec 模型
model = Word2Vec(sentences=common_texts, vector_size=100, window=5,
min_count=1, workers=4, sg=1) # sg=1 使用 Skip-gram
# 查看词向量
print(model.wv["computer"])
# 最相似的词
print(model.wv.most_similar("computer", topn=5))
# 词类比
print(model.wv.most_similar(positive=["woman", "king"], negative=["man"], topn=3))
# 词相似度
print(model.wv.similarity("computer", "human"))3.2 使用预训练词向量
from gensim.models import KeyedVectors
import numpy as np
# 加载预训练词向量(示例使用小模型)
# 实际使用时可下载大规模预训练向量
# wget https://dl.fbaipublicfiles.com/fasttext/vectors-crawl/cc.zh.300.vec.gz
def load_word_vectors(filepath, max_words=50000):
vectors = {}
with open(filepath, "r", encoding="utf-8") as f:
for i, line in enumerate(f):
if i >= max_words:
break
values = line.strip().split()
word = values[0]
vector = np.array(values[1:], dtype=float)
vectors[word] = vector
return vectors
# 计算文本相似度
def text_similarity(text1, text2, word_vectors):
words1 = [w for w in text1 if w in word_vectors]
words2 = [w for w in text2 if w in word_vectors]
if not words1 or not words2:
return 0.0
vec1 = np.mean([word_vectors[w] for w in words1], axis=0)
vec2 = np.mean([word_vectors[w] for w in words2], axis=0)
return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))3.3 词向量的可视化
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
def visualize_words(words, word_vectors, title="Word Vectors (PCA)"):
indices = [list(word_vectors.keys()).index(w) for w in words if w in word_vectors]
vectors = np.array([list(word_vectors.values())[i] for i in indices])
pca = PCA(n_components=2)
reduced = pca.fit_transform(vectors)
plt.figure(figsize=(10, 8))
for i, word in enumerate(words):
if word in word_vectors:
plt.scatter(reduced[i, 0], reduced[i, 1])
plt.annotate(word, (reduced[i, 0], reduced[i, 1]), fontsize=12)
plt.title(title)
plt.xlabel("PC1")
plt.ylabel("PC2")
plt.grid(True, alpha=0.3)
plt.show()
# 可视化语义关系
words = ["猫", "狗", "鱼", "鸟", "汽车", "火车", "飞机", "轮船"]
# visualize_words(words, word_vectors)四、本章小结
本章系统讲解了向量语义学:
分布式假设:词的含义由其上下文决定,这是词向量的理论基础。
词共现矩阵:统计词在上下文中的共现频率,构建词-词矩阵。PPMI 是比原始频率更好的度量。
降维方法:SVD 将高维稀疏矩阵降为低维稠密向量。LSA 是早期的词向量方法。
Word2Vec:CBOW 和 Skip-gram 使用神经网络高效学习词向量。负采样解决了计算效率问题。
GloVe:结合全局共现统计和局部上下文学习,在低频词上表现更好。
评估方法:内在评估(词相似度、类比任务)和外在评估(下游任务性能)。
词向量是 NLP 从离散走向连续的转折点。然而,静态词向量有一个根本局限:同一个词在不同上下文中的含义是固定的。下一章我们将学习神经网络和 Transformer,它们能够产生上下文相关的动态词表示。
五、思考与练习
思考题
分布式假设有什么局限性?有没有语义相近但上下文不同的词?
比较 SVD 和 Word2Vec 两种词向量学习方法的优缺点。
Word2Vec 的负采样为什么有效?负样本的数量对结果有什么影响?
词向量能捕捉反义词关系吗?(如"好"和"坏")请分析原因。
编程练习
在中文维基百科语料上训练 Word2Vec 模型。评估词类比和词相似度任务的性能。
实现 GloVe 的损失函数,分析不同权重函数 $f(x)$ 对结果的影响。
使用 t-SNE 可视化词向量,观察语义相近的词是否在二维空间中聚集。