概述

词嵌入(Word Embedding)是自然语言处理的基础任务之一:把离散的词符号映射到低维连续实数向量,使得”意义相近的词在向量空间中也相近”。1 2013 年前后,Word2Vec、GloVe、FastText 等经典算法相继出现,彻底改变了 NLP 研究的范式——从”特征工程”转向”表示学习”。

本文系统梳理这三种词嵌入方法的数学原理、训练目标与相互关系,并给出 PyTorch 完整实现。


一、问题的起点:从 One-Hot 到分布式表示

One-Hot 表示的局限

传统 NLP 把每个词表示为 维 one-hot 向量( 为词表大小)。这种表示有三个根本问题:

  1. 维度灾难:词表通常 5 万-100 万
  2. 语义鸿沟:向量两两正交,无法表达”相似”或”相关”
  3. 泛化失败:从未在训练中见过的词无法获得表示

分布式假设(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 对比

维度Word2VecGloVe
训练方式局部窗口 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 + 负采样框架,仅替换评分函数:

这种”求和”的形式带来两个好处:

  1. OOV 处理:未登录词可以直接由其字符 n-gram 的嵌入求和得到
  2. 形态学共享:形近词(teach/teacher)共享 n-gram 向量

4.4 实际意义

  • 词表规模从 增长到约 (n-gram 总量)
  • 实际计算量只与词长度成正比(O(|w|))
  • 对低资源语言和罕见词特别有效

五、三者对比与实践指南

特性Word2VecGloVeFastText
训练目标局部窗口预测全局共现分解Skip-gram + n-gram
OOV 处理不支持不支持支持
形态学不利用不利用利用
训练速度慢(n-gram 量大)
嵌入质量优(对低资源更佳)
主流实现gensim, fastaigensim, 官方 CfastText 官方 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 model

6.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 embeddings

6.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 : ? → 期望 woman
  • Paris : France :: Tokyo : ? → 期望 Japan

数学上:

7.2 词相似度

使用 WordSim-353、SimLex-999 等数据集,计算人评相似度与向量余弦相似度的 Spearman/Pearson 相关系数。

7.3 下游任务

词嵌入的实际价值最终要在下游任务中体现:

  • 命名实体识别
  • 情感分析
  • 文本分类
  • 机器翻译

八、局限与后续发展

8.1 经典方法的局限

  1. 静态嵌入:每个词一个固定向量,无法处理一词多义(polysemy)
  2. 上下文无关:bank(银行)和 bank(河岸)的嵌入相同
  3. 无法利用全局上下文:仅看窗口内的局部信息
  4. 次词信息丢失(除 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

  1. Mikolov, T., Chen, K., Corrado, G., & Dean, J. (2013). Efficient Estimation of Word Representations in Vector Space. arXiv:1301.3781.

  2. Goldberg, Y., & Levy, O. (2014). word2vec Explained: Deriving Mikolov et al.’s Negative-Sampling Word-Embedding Method. arXiv:1402.3722.

  3. Pennington, J., Socher, R., & Manning, C. D. (2014). GloVe: Global Vectors for Word Representation. EMNLP 2014.

  4. Bojanowski, P., Grave, E., Joulin, A., & Mikolov, T. (2017). Enriching Word Vectors with Subword Information. Transactions of ACL.