6语音与语言处理(第3版)

向量语义学

词向量

Word2VecGloVe词嵌入

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 编码是典型的离散表示:

python
# One-hot 编码
vocab = {"猫": 0, "狗": 1, "鱼": 2, "汽车": 3}
cat = [1, 0, 0, 0]
dog = [0, 1, 0, 0]
# 猫和狗的向量距离 = 猫和汽车的距离 = √2
# 这显然不合理——猫和狗应该更相似

分布式表示:每个词是一个稠密的低维向量,语义相近的词向量距离近:

python
# 分布式表示(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 构建共现矩阵

最直接的方法:统计词在上下文窗口中共现的频率,构建词-上下文矩阵。

python
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))$$

python
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 ppmi

6.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}$ 可以作为词的低维表示。

python
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
python
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

特性Word2VecGloVe
训练方式局部上下文窗口全局共现矩阵
训练效率高(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)$$

python
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_sim

6.6.2 外在评估

将词向量作为特征输入下游任务(如文本分类、NER),比较任务性能。


三、实战案例

3.1 使用 Gensim 训练 Word2Vec

python
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 使用预训练词向量

python
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 词向量的可视化

python
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 可视化词向量,观察语义相近的词是否在二维空间中聚集。


6/12向量语义
0%