分布式语义学详解

文档概述

本文档深入探讨分布式语义学的理论基础、数学原理、算法实现和应用场景。从Firth的原始假说出发,系统阐述共现矩阵构建、奇异值分解降维、点互信息算法等核心技术,详细分析Skip-gram/CBOW模型的数学本质,深入探讨语义空间的几何性质和代数结构,并展望分布式语义学与深度学习的融合发展方向。

关键词速览

术语英文核心定义
FirthJohn Rupert Firth分布式假说的提出者,伦敦学派核心人物
共现矩阵Co-occurrence Matrix词与上下文词的共现统计矩阵
SVDSingular Value Decomposition奇异值分解降维技术
PMIPointwise Mutual Information点互信息算法,衡量词与上下文关联强度
PPMIPositive PMI正点互信息,去除负值
Skip-gramSkip-gram Model用中心词预测上下文词的模型
CBOWContinuous Bag-of-Words用上下文词预测中心词的模型
语义空间Semantic Space词向量张成的几何空间
词汇表征Lexical Representation词语的数学向量表示
降维Dimensionality Reduction高维到低维的映射过程
上下文向量Context Vector词的上下文统计表示
词嵌入Word Embedding将词映射到连续向量空间的技术
GloVeGlobal Vectors结合全局统计与局部上下文的嵌入方法
语义相似度Semantic Similarity词语在语义空间中的距离度量
语义类比Semantic Analogy词向量空间中的线性关系

一、分布式假说的理论渊源与哲学基础

1.1 John Rupert Firth与分布式语义学的诞生

约翰·鲁珀特·弗思(John Rupert Firth,1890-1960)是英国著名语言学家,伦敦学派(London School of Linguistics)的核心人物和奠基人之一。他不仅在语言学领域做出了开创性贡献,更是分布式语义学(Distributional Semantics)这一重要范式的创始人。弗思的学术思想深受人类学家Bronisław Malinowski的影响,强调语言的意义必须从语言的实际使用中理解,这一观点深刻塑造了现代计算语言学和自然语言处理的发展方向。

弗思在1957年发表的经典论文中首次明确提出了分布式假说(Distributional Hypothesis),这一假说成为分布式语义学的理论基础:

“It is a principle of linguistic meaning that words occurring in similar contexts tend to have similar meanings.” (语言意义的原则:出现在相似上下文中的词往往具有相似的意义。)

这一假说的提出标志着语言学研究范式的重大转变——从关注语言形式的内省分析,转向基于语言使用分布的经验研究。弗思的这一观点与 Ludwig Wittgenstein 的”意义即使用”(meaning as use)哲学观点不谋而合,共同构成了20世纪语言学转向的重要理论基础。

1.1.1 弗思的学术传承

弗思的学术思想可以追溯到多位重要的语言学先驱:

结构主义传统

  • Ferdinand de Saussure(索绪尔):提出语言(langue)与言语(parole)的区分,强调语言系统内部的符号关系
  • Leonard Bloomfield(布龙菲尔德):行为主义语言学代表,主张对语言现象进行客观描写

功能主义视角

  • Bronisław Malinowski(马林诺夫斯基):人类学功能主义创始人,强调语言的实际使用语境
  • J.R. Firth(弗思):将功能主义应用于语言分析,提出语境(context)理论

弗思在1930年代发展出”语境”(context)理论,认为词的意义可以通过其在语言环境中的分布来理解。他区分了三种语境:

  1. 词境(collocational context):词语在文本中的搭配模式
  2. 句境(context of situation):语句使用的具体情境
  3. 文化语境(context of culture):语言所承载的文化背景

1.2 假说的认知与认知科学基础

分布式假说的合理性可以从多个学科角度得到论证,这些论证共同构成了分布式语义学的科学基础。

1.2.1 语言习得视角

从认知发展角度分析,儿童在习得语言时面临一个根本性的挑战:他们无法直接”看到”或”触摸”词语的内部语义结构。与此形成对比的是,词语的使用模式——即分布式假说所关注的核心——对语言学习者来说是完全可及的。儿童通过反复观察词语在不同情境中的使用,逐渐建立起对词语意义的理解。

这一学习过程可以用数学语言形式化描述。设 表示词 的上下文分布,则:

其中 是从上下文分布到语义表示的映射函数。当两个词 具有相似的上下文分布 时,它们应该具有相似的语义表示:

这一推论直接导出了分布式语义学的核心假设。

1.2.2 信息论视角

从 Claude Shannon 的信息论角度重新审视分布式假说,可以获得更加形式化的理论基础。词语的上下文携带着关于该词语意义的信息,这种信息携带量可以通过互信息(Mutual Information)来量化。

互信息的定义

与上下文 之间的互信息定义为:

互信息的物理意义在于衡量两个随机变量之间的统计依赖性。当 时,上下文 的出现提供了关于词 的信息;当 时,上下文的存在反而减少了关于词的信息。

上下文信息的累积效应

对于一个词 ,考虑其在语料中出现的所有上下文窗口,其总的上下文信息量可以通过积分形式表达:

这一公式表明,一个词的语义内容可以通过其与上下文的互信息加权求和来估计。

1.2.3 认知心理学视角

分布式语义学的认知心理学基础可以追溯到 Latent Semantic Analysis (LSA) 的开创性研究。Landauer 和 Dumais(1997)在《Psychological Review》发表的研究表明,仅通过统计分析大量文本的共现模式,就能学习到与人类语义知识高度相关的语义表示。

他们的实验设计如下:

  1. 收集包含数百万文档的大型语料库
  2. 构建词-文档共现矩阵
  3. 使用奇异值分解(SVD)进行降维
  4. 比较学习到的语义空间与人类语义判断的一致性

实验结果发现,LSA学习到的语义知识能够在多项认知任务上与人类表现相当,这一发现被称为”Plato’s Problem”(柏拉图问题)的计算解决方案——即如何从有限的接触经验中学习到丰富的知识。

1.3 分布式语义学的形式化框架

分布式语义学的核心数学框架可以形式化为三元组结构:

其中各元素的详细定义如下:

词汇集合

  • 是目标词汇的有限集合
  • 通常包含语料中出现频率超过某个阈值的词
  • 集合大小 决定了矩阵的维度

上下文元素集合

  • 是上下文元素的集合
  • 上下文元素可以是词汇、文档、依存关系等多种形式
  • 集合大小 决定了矩阵的列维度

词汇-上下文矩阵

  • 是词汇-上下文的共现统计矩阵
  • 表示词汇 与上下文 的共现强度
  • 矩阵的具体计算方式决定了分布式表示的性质

示例:小型语料的矩阵构建

考虑以下简化语料:

"The cat sat on the mat. The dog sat on the log."

对于此语料,词汇集合和上下文集合定义为:

使用窗口大小为1的上下文定义(左右各1个词),共现矩阵 如下:

矩阵的行对应词汇,列对应上下文,行列交叉处的数值表示共现次数。


二、共现矩阵与统计分析方法论

2.1 共现矩阵的构建方法

共现矩阵是分布式语义学的基石,其构建过程直接影响最终的语义表示质量。一个精心设计的共现矩阵能够捕捉词汇之间的语义关系,而一个粗劣的设计则可能导致语义扭曲或信息丢失。

2.1.1 上下文定义策略

上下文定义是共现矩阵构建的核心决策,它决定了”什么样的词应该被放在一起”这一根本问题。

窗口上下文方法

最基本也是最常用的上下文定义方式是固定大小的词窗口。在这种方法中,每个词的上下文定义为出现在其周围特定距离内的词。

窗口上下文的数学定义:

对于语料中的词 (位置为 ),其窗口大小为 的上下文定义为:

窗口大小是一个关键超参数,它影响捕捉的语义关系类型:

窗口大小捕捉的语义关系典型应用
小窗口(1-2)语法搭配、词法关系形态变化、同义词
中等窗口(3-5)语义相似、主题关联一般语义关系
大窗口(8+)主题一致性、领域关联话题模型、文档主题
def build_cooccurrence_matrix(corpus, vocabulary, window_size=5, 
                              weighting_scheme='raw'):
    """
    构建词汇-词汇共现矩阵
    
    Args:
        corpus: 分词后的语料列表,每个元素是一个词
        vocabulary: 词汇表字典 (word -> idx)
        window_size: 窗口半径(单侧)
        weighting_scheme: 权重方案 ('raw', 'log', 'distance')
    
    Returns:
        cooc_matrix: scipy sparse matrix, shape (vocab_size, vocab_size)
    """
    import scipy.sparse as sp
    from collections import defaultdict
    
    vocab_size = len(vocabulary)
    cooc_matrix = sp.lil_matrix((vocab_size, vocab_size), dtype=np.float64)
    
    # 词到索引的快速查找
    word_to_idx = vocabulary
    
    # 遍历语料中的每个位置
    for i, word in enumerate(corpus):
        if word not in word_to_idx:
            continue
        word_idx = word_to_idx[word]
        
        # 确定窗口范围
        start = max(0, i - window_size)
        end = min(len(corpus), i + window_size + 1)
        
        # 遍历窗口内的上下文词
        for j in range(start, end):
            if i == j:  # 跳过自身
                continue
            
            context_word = corpus[j]
            if context_word not in word_to_idx:
                continue
            
            context_idx = word_to_idx[context_word]
            
            # 计算权重
            distance = abs(i - j)
            if weighting_scheme == 'raw':
                weight = 1.0
            elif weighting_scheme == 'log':
                weight = np.log(1 + 1.0 / distance)
            elif weighting_scheme == 'distance':
                weight = 1.0 / distance
            else:
                weight = 1.0
            
            cooc_matrix[word_idx, context_idx] += weight
    
    return cooc_matrix.tocsr()

基于依存关系的上下文

更语义化的上下文定义利用句法依存关系。这种方法认为,句法依存关系比简单的词汇共现更能捕捉词语之间的语义联系。

依存关系上下文的理论基础来自语言学的依存语法(Dependency Grammar)。在依存语法中,每个词与其他词之间存在依存关系,形成一个树结构。

句子: "The small cat quietly sat on the comfortable mat"

依存分析:
nsubj(sat, cat)          # cat是sat的主语
det(cat, The)            # The是cat的限定词
amod(cat, small)         # small是cat的修饰语
advmod(sat, quietly)     # quietly是sat的状语
root(root, sat)          # sat是句子核心
prep(sat, on)            # on是sat的介词
pobj(on, mat)            # mat是on的介词宾语
det(mat, the)             # the是mat的限定词
amod(mat, comfortable)    # comfortable是mat的修饰语

基于依存关系的上下文可以表示为关系类型-词对的集合:

cat的依存上下文: {(det, The), (amod, small), (nsubj, sat)}
sat的依存上下文: {(root, root), (nsubj, cat), (advmod, quietly), (prep, on)}

这种上下文定义的优点在于:

  1. 捕捉句法功能关系而非仅仅是共现
  2. 保留了关系类型信息(如主语vs宾语)
  3. 更符合人类对语义关系的直觉
def build_dependency_cooccurrence_matrix(sentences, vocabulary):
    """
    基于依存关系的共现矩阵构建
    
    Args:
        sentences: 依存分析结果列表,每个元素是(deps, tokens)元组
                  deps是依存关系列表 [(head_idx, rel, dep_idx)]
        vocabulary: 词汇表
    """
    vocab_size = len(vocabulary)
    # 使用(关系类型, 词)作为上下文标识
    cooc_matrix = defaultdict(lambda: defaultdict(float))
    rel_word_vocab = {}
    
    for deps, tokens in sentences:
        for head_idx, rel, dep_idx in deps:
            head_word = tokens[head_idx]
            dep_word = tokens[dep_idx]
            
            # 创建关系-词对的唯一标识
            rel_word = (rel, dep_word)
            
            if rel_word not in rel_word_vocab:
                rel_word_vocab[rel_word] = len(rel_word_vocab)
            
            if head_word in vocabulary and rel_word in rel_word_vocab:
                cooc_matrix[head_word][rel_word] += 1
                # 双向关系
                rel_word_rev = (rel, head_word)
                if dep_word in vocabulary:
                    cooc_matrix[dep_word][rel_word_rev] += 1
    
    return cooc_matrix, rel_word_vocab

文档级上下文

另一种重要的上下文定义是基于文档级共现。在这种定义下,一个词的上下文是其所在文档的标识,矩阵变为词-文档共现矩阵。

词-文档矩阵的定义:

其中 是词 在文档 中的词频, 是逆文档频率权重。

词-文档矩阵的主要特点是捕捉词语的主题语义。同一主题下的词语(如同属”计算机科学”主题的”算法”、“数据结构”、“编程”)会在相似的文档中频繁出现,从而获得相似的向量表示。

2.1.2 权重方案设计

原始共现计数是最简单的权重方案,但它存在一些问题,特别是对高频词(如”the”、“is”、“of”)的过度加权。多种权重方案被提出来解决这些问题。

对数平滑

最常见的预处理是对共现计数进行对数变换:

对数变换的作用机制:

  • 压缩大数值范围:将 变换为
  • 减少高频词的过度影响
  • 使分布更加平滑

Hellinger距离加权

Hellinger距离加权用于处理概率分布的归一化:

其中 是行边缘和。这个变换确保每行的L2范数为1:

逆文档频率(IDF)加权

对于词-文档矩阵,可以使用TF-IDF风格的加权:

其中 是文档总数, 是包含词 的文档数。

def apply_weighting(cooc_matrix, scheme='ppmi'):
    """
    对共现矩阵应用权重变换
    
    Args:
        cooc_matrix: 原始共现矩阵 (scipy sparse)
        scheme: 权重方案 ('log', 'hellinger', 'ppmi', 'ppmi_svd')
    
    Returns:
        weighted_matrix: 加权后的矩阵
    """
    if scheme == 'log':
        # 对数变换:w' = log(1 + w)
        cooc_coo = cooc_matrix.tocoo()
        data = np.log1p(cooc_coo.data)
        return sparse.csr_matrix((data, (cooc_coo.row, cooc_coo.col)), 
                                 shape=cooc_matrix.shape)
    
    elif scheme == 'hellinger':
        # Hellinger加权:w' = sqrt(w / row_sum)
        row_sums = np.array(cooc_matrix.sum(axis=1)).flatten()
        row_sums[row_sums == 0] = 1  # 避免除零
        # 逐行归一化
        return sparse.csr_matrix(cooc_matrix / row_sums[:, None] ** 0.5)
    
    elif scheme == 'ppmi':
        # PPMI变换
        return compute_ppmi(cooc_matrix)
    
    elif scheme == 'ppmi_svd':
        # PPMI + SVD
        ppmi = compute_ppmi(cooc_matrix)
        return sparse.csr_matrix(ppmi)
    
    return cooc_matrix

2.1.3 矩阵类型与选择

根据应用场景的不同,可以选择不同类型的共现矩阵:

矩阵类型定义对称性特点与应用
词-词矩阵 = 词与词的共现对称捕捉词汇搭配关系
词-上下文矩阵 = 词与上下文的共现非对称更灵活,可自定义上下文
词-文档矩阵 = 词在文档中的频次非对称捕捉主题信息,用于话题模型
词-位置矩阵 = 词在位置的频次非对称捕捉位置模式
词-依存矩阵 = 词与依存关系的共现非对称捕捉句法功能关系

2.2 共现矩阵的数学性质分析

理解共现矩阵的数学性质对于设计高效的分布式语义系统至关重要。

2.2.1 稀疏性特征

自然语言的共现矩阵通常是高度稀疏的。以一个典型的语料库为例:

  • 词汇表大小:
  • 共现矩阵大小: 个元素
  • 非零元素比例:通常小于 1%

稀疏度的定义:

稀疏性的成因

  1. Zipf定律:词频分布极不均匀,大多数词只出现少量次数
  2. 语义特异性:特定词只与有限的语义相关词共现
  3. 语言结构性:语法规则限制了可能的共现模式

稀疏性的影响与处理

稀疏性带来的挑战:

  • 存储效率低:大量内存用于存储零值
  • 计算效率低:涉及稀疏矩阵的运算开销大
  • 统计不可靠:低频共现的噪声比高

解决方案:

  • 降维:通过矩阵分解减少维度
  • 阈值化:过滤低频共现
  • 稀疏编码:使用专门的稀疏数据结构
def analyze_sparsity(cooc_matrix):
    """
    分析共现矩阵的稀疏性特征
    
    Returns:
        sparsity_stats: 包含各种稀疏性指标的字典
    """
    total_elements = cooc_matrix.shape[0] * cooc_matrix.shape[1]
    non_zero = cooc_matrix.nnz
    zero = total_elements - non_zero
    
    # 计算稀疏度
    sparsity = zero / total_elements
    
    # 行稀疏度分布
    row_nnz = np.array((cooc_matrix > 0).sum(axis=1)).flatten()
    
    # 列稀疏度分布
    col_nnz = np.array((cooc_matrix > 0).sum(axis=0)).flatten()
    
    stats = {
        'total_elements': total_elements,
        'non_zero_count': non_zero,
        'zero_count': zero,
        'sparsity_percent': sparsity * 100,
        'density_percent': (1 - sparsity) * 100,
        'avg_nnz_per_row': np.mean(row_nnz),
        'avg_nnz_per_col': np.mean(col_nnz),
        'median_nnz_per_row': np.median(row_nnz),
        'max_nnz_per_row': np.max(row_nnz),
        'min_nnz_per_row': np.min(row_nnz[row_nnz > 0]) if any(row_nnz > 0) else 0,
    }
    
    return stats

2.2.2 矩阵的对称性与分解

对称性分析

词-词共现矩阵具有对称性:。这意味着:

  • 可以使用对称的矩阵分解方法(如SVD的截断形式)
  • 词之间的关系是双向的(如果共现,则也与共现)
  • 特征值分解和奇异值分解等价

非对称性的价值

词-上下文矩阵通常是非对称的,这种非对称性携带了重要信息:

非对称性的来源:

  • 词汇与其上下文的关系本身可能是不对称的
  • 例如:“猫”经常出现在”宠物”的上下文中,但”宠物”不一定是”猫”的典型上下文

这种非对称性可以编码方向性语义信息。


三、奇异值分解与语义空间构建

3.1 奇异值分解(SVD)理论

奇异值分解(Singular Value Decomposition,SVD)是处理共现矩阵的核心降维技术。它将高维稀疏矩阵分解为低维稠密表示,同时保留尽可能多的原始信息。

3.1.1 SVD数学原理详解

对于任意 的实数矩阵 ,SVD分解将其表示为三个特殊矩阵的乘积:

其中:

左奇异向量矩阵

  • 的正交矩阵:
  • 的列向量 的特征向量
  • 的列提供了词向量的旋转基

奇异值对角矩阵

  • 是对角矩阵,对角线元素为奇异值
  • 奇异值的平方 的特征值
  • 奇异值的大小表示对应成分的信息量

右奇异向量矩阵

  • 的正交矩阵:
  • 的列向量 的特征向量
  • 提供了上下文向量的旋转基

3.1.2 截断SVD与降维

在实际应用中,我们通常只保留前 个最大的奇异值及其对应的奇异向量,这称为截断SVD(Truncated SVD):

其中

降维后词向量的提取

维向量表示可以从 提取:

方法一:直接使用左奇异向量

方法二:加权左奇异向量

这种加权放大了重要奇异值对应的方向。

方法三:双侧加权

这种加权更加重视高奇异值成分。

不同加权方法的选择会影响词向量的性质,通常实验验证哪种方法更适合具体任务。

降维的信息保持度

截断SVD保留的信息量可以通过解释方差比例来衡量:

其中 是奇异值的总数。典型的 值(如300)通常能保留80-95%的原始信息。

3.1.3 SVD的数值实现

import numpy as np
from scipy.sparse import csr_matrix
from scipy.sparse.linalg import svds
from sklearn.decomposition import TruncatedSVD
 
class SVDWordEmbeddings:
    """
    基于SVD的词嵌入模型
    
    核心技术:
    1. 构建词汇-上下文共现矩阵
    2. 应用PPMI变换
    3. 使用截断SVD降维
    4. 提取词向量
    """
    
    def __init__(self, embedding_dim=300, min_sv=1e-6):
        """
        Args:
            embedding_dim: 目标嵌入维度
            min_sv: 最小奇异值阈值(用于过滤)
        """
        self.embedding_dim = embedding_dim
        self.min_sv = min_sv
        self.word_to_idx = None
        self.embeddings = None
        self.U = None
        self.S = None
        self.Vt = None
    
    def fit(self, cooc_matrix, word_to_idx):
        """
        训练SVD嵌入
        
        Args:
            cooc_matrix: scipy sparse共现矩阵 (vocab_size, context_size)
            word_to_idx: 词汇到索引的映射
        """
        self.word_to_idx = word_to_idx
        
        # 转换为CSR格式(高效切片)
        if not isinstance(cooc_matrix, csr_matrix):
            cooc_matrix = csr_matrix(cooc_matrix)
        
        # 存储原始维度信息
        m, n = cooc_matrix.shape
        k = min(self.embedding_dim, min(m, n) - 1)
        
        # 截断SVD分解
        # svds返回按奇异值升序排列的因子,需要反转
        U, s, Vt = svds(cooc_matrix.astype(np.float64), k=k)
        
        # 反转顺序,使奇异值降序排列
        idx = np.argsort(s)[::-1]
        self.U = U[:, idx]
        self.S = s[idx]
        self.Vt = Vt[idx, :]
        
        # 过滤过小的奇异值
        valid_mask = self.S > self.min_sv
        self.S = self.S[valid_mask]
        self.U = self.U[:, valid_mask]
        
        # 提取词向量:U * sqrt(S)
        self.embeddings = self.U * np.sqrt(self.S)
        
        return self
    
    def get_embedding(self, word):
        """获取单个词的嵌入向量"""
        if self.word_to_idx is None or word not in self.word_to_idx:
            return None
        idx = self.word_to_idx[word]
        return self.embeddings[idx]
    
    def get_similar_words(self, word, top_k=10):
        """找出与给定词最相似的词"""
        from sklearn.metrics.pairwise import cosine_similarity
        
        if self.word_to_idx is None or word not in self.word_to_idx:
            return []
        
        target_idx = self.word_to_idx[word]
        target_vec = self.embeddings[target_idx:target_idx+1]
        
        # 计算余弦相似度
        similarities = cosine_similarity(target_vec, self.embeddings)[0]
        
        # 获取top_k相似词(排除自身)
        idx_to_word = {v: k for k, v in self.word_to_idx.items()}
        top_indices = np.argsort(similarities)[::-1][:top_k+1]
        
        results = []
        for idx in top_indices:
            if idx != target_idx:
                results.append((idx_to_word[idx], similarities[idx]))
        
        return results[:top_k]
    
    def save(self, filepath):
        """保存模型参数"""
        np.savez(filepath,
                 embeddings=self.embeddings,
                 U=self.U,
                 S=self.S,
                 Vt=self.Vt)
    
    def load(self, filepath, word_to_idx):
        """加载模型参数"""
        data = np.load(filepath)
        self.embeddings = data['embeddings']
        self.U = data['U']
        self.S = data['S']
        self.Vt = data['Vt']
        self.word_to_idx = word_to_idx
 
 
def compute_ppmi(cooc_matrix, eps=1e-10):
    """
    将共现矩阵转换为PPMI (Positive Pointwise Mutual Information)
    
    PPMI是分布式语义学中最重要的预处理步骤之一
    它去除了负PMI值(表示意外低频共现的关系)
    
    数学定义:
    PPMI(w, c) = max(0, log₂ [P(w,c) / (P(w)·P(c))])
               = max(0, log₂ [#(w,c)·|D| / (#w·#c)])
    
    Args:
        cooc_matrix: 原始共现计数矩阵
        eps: 防止除零的小常数
    
    Returns:
        ppmi_matrix: PPMI值矩阵
    """
    # 转换为密集矩阵(演示用)
    M = cooc_matrix.toarray().astype(np.float64)
    
    # 计算总计数
    total = M.sum()
    
    # 计算词概率 P(w)
    word_probs = M.sum(axis=1, keepdims=True) / total
    
    # 计算上下文概率 P(c)
    context_probs = M.sum(axis=0, keepdims=True) / total
    
    # 计算联合概率 P(w,c)
    joint_probs = M / total
    
    # 计算PMI
    # PMI = log2(P(w,c) / (P(w) * P(c)))
    # 在计数形式下:log2(#(w,c)·|D| / (#w·#c))
    pmi = np.log2(joint_probs / (word_probs * context_probs + eps) + eps)
    
    # PPMI = max(PMI, 0)
    ppmi = np.maximum(pmi, 0)
    
    return ppmi

3.2 语义空间的性质与结构

3.2.1 语义空间的维度意义

SVD降维后的每个维度对应一个潜在的语义因子。虽然这些因子不一定对应人类可解释的语义概念,但通过分析可以发现它们编码了语言中的各种语义和语法模式。

典型语义维度示例

考虑一个经过适当训练的语义空间,某些维度可能对应以下语义轴:

语义空间维度分析(简化2D可视化):

维度1 (D1): 性别语义轴
    │
    │   man   boy   king   he
    │    
────┼─────────────────────── 男性区域
    │   
    │   woman  girl  queen she
    │   
    └──────────────────────────────→ 维度2 (D2)

语义轴分析:
D1 (主轴): 男性-女性 对立
  - 正方向: king, man, boy, he, his, male
  - 负方向: queen, woman, girl, she, her, female

D2 (次轴): 成人-儿童 或 王权-平民
  - 正方向: king, queen, adult, mature
  - 负方向: boy, girl, child, young

PCA分析语义维度

可以使用主成分分析(PCA)来可视化语义空间的主要变异方向:

def analyze_semantic_dimensions(embeddings, word_to_idx, category_dict, n_components=5):
    """
    分析语义空间的主要维度及其语义解释
    
    Args:
        embeddings: 词嵌入矩阵
        word_to_idx: 词汇表
        category_dict: 类别字典 {category_name: [word1, word2, ...]}
        n_components: 分析的主成分数量
    """
    from sklearn.decomposition import PCA
    import matplotlib.pyplot as plt
    
    # 收集各类别的词向量
    vectors = []
    labels = []
    categories = []
    
    for cat_name, words in category_dict.items():
        for word in words:
            if word in word_to_idx:
                vectors.append(embeddings[word_to_idx[word]])
                labels.append(word)
                categories.append(cat_name)
    
    vectors = np.array(vectors)
    
    # PCA分析
    pca = PCA(n_components=min(n_components, len(vectors)))
    coords = pca.fit_transform(vectors)
    
    print("=" * 60)
    print("语义空间主成分分析")
    print("=" * 60)
    
    for i, (var_ratio, component) in enumerate(zip(
        pca.explained_variance_ratio_[:n_components],
        pca.components_[:n_components]
    )):
        print(f"\n主成分 {i+1}: 解释方差 = {var_ratio:.2%}")
        print(f"权重向量 (前10个): {component[:10]}")
        
        # 找出该成分上权重最高和最低的词
        weighted_scores = vectors @ component
        top_indices = np.argsort(weighted_scores)[::-1][:5]
        bottom_indices = np.argsort(weighted_scores)[:5]
        
        print(f"  正向高分词: {[(labels[j], weighted_scores[j]) for j in top_indices]}")
        print(f"  负向高分词: {[(labels[j], weighted_scores[j]) for j in bottom_indices]}")
    
    return coords, labels, categories, pca

3.2.2 距离度量与语义相似度

语义空间中的距离度量是计算词语相似度的基础。不同的度量方式捕捉语义相似性的不同方面。

常用距离度量公式

度量名称公式语义解释特点
欧氏距离$d_2(\mathbf{v}_i, \mathbf{v}_j) = \\mathbf{v}_i - \mathbf{v}_j\2 = \sqrt{\sum_k (v{i,k} - v_{j,k})^2}$
余弦相似度$\cos(\mathbf{v}_i, \mathbf{v}_j) = \frac{\mathbf{v}_i \cdot \mathbf{v}_j}{\\mathbf{v}_i\\
曼哈顿距离特征维度差异总和线性复杂度
切比雪夫距离最大维度差异对异常值敏感
马氏距离考虑相关性的距离考虑维度间相关性

余弦相似度详解

余弦相似度是词向量领域最广泛使用的度量方式。它的优势在于:

  1. 方向性:只关注向量指向的方向,不受向量长度影响
  2. 归一化:取值范围为
  3. 语义直觉:方向相似 = 语义相似

余弦相似度与语义相似度的关系可以通过以下实验验证:

def evaluate_similarity_metrics(embeddings, word_to_idx, test_pairs, human_ratings=None):
    """
    评估不同相似度度量与人类判断的一致性
    
    Args:
        embeddings: 词嵌入矩阵
        word_to_idx: 词汇表
        test_pairs: 测试词对列表 [(word1, word2, category), ...]
        human_ratings: 人类相似度评分(可选)
    
    Returns:
        results: 各度量方式的结果字典
    """
    from sklearn.metrics.pairwise import cosine_similarity, euclidean_distances
    from scipy.stats import spearmanr, pearsonr
    
    results = {}
    
    # 计算各种相似度
    for word1, word2, category in test_pairs:
        if word1 not in word_to_idx or word2 not in word_to_idx:
            continue
        
        idx1 = word_to_idx[word1]
        idx2 = word_to_idx[word2]
        
        v1 = embeddings[idx1]
        v2 = embeddings[idx2]
        
        # 余弦相似度
        cos_sim = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
        
        # 欧氏距离转相似度
        euc_dist = np.linalg.norm(v1 - v2)
        euc_sim = 1 / (1 + euc_dist)
        
        # 存储结果
        key = (word1, word2)
        if key not in results:
            results[key] = {'cosine': [], 'euclidean': [], 'category': category}
        results[key]['cosine'].append(cos_sim)
        results[key]['euclidean'].append(euc_sim)
    
    # 如果有人类评分,计算相关性
    if human_ratings:
        cos_scores = [np.mean(r['cosine']) for r in results.values()]
        euc_scores = [np.mean(r['euclidean']) for r in results.values()]
        
        cos_corr, cos_p = spearmanr(cos_scores, human_ratings)
        euc_corr, euc_p = spearmanr(euc_scores, human_ratings)
        
        print("=" * 60)
        print("相似度度量评估结果")
        print("=" * 60)
        print(f"余弦相似度 vs 人类评分:")
        print(f"  Spearman相关系数: {cos_corr:.4f} (p={cos_p:.4e})")
        print(f"欧氏距离 vs 人类评分:")
        print(f"  Spearman相关系数: {euc_corr:.4f} (p={euc_p:.4e})")
    
    return results

四、PMI算法家族与信息论方法

4.1 点互信息(PMI)基础理论

点互信息(Pointwise Mutual Information,PMI)是衡量两个事件关联强度的经典信息论指标,在分布式语义学中扮演着核心角色。

4.1.1 PMI的定义与推导

基本定义

对于离散随机变量 (词)和 (上下文),它们的互信息定义为:

互信息衡量的是两个变量之间的统计依赖程度,其取值始终非负。

点互信息(PMI) 是互信息的”点级”版本,衡量特定词-上下文对的关联强度:

等价计数形式

在实际计算中,我们通常用计数来估计概率:

其中:

  • :词 与上下文 的共现次数
  • :词 的总出现次数
  • :上下文 的总出现次数
  • :语料中的总词数

直观解释

PMI的取值具有明确的语义:

PMI值含义解释
正关联共现频率高于随机期望
独立共现频率符合随机期望
负关联共现频率低于随机期望

例如:

  • “计算机”与”编程”的PMI可能很高(正关联)
  • “猫”与”股票”的PMI可能为负(负关联)

4.1.2 PMI的数学性质

非负性(互信息整体)

这意味着 在某种加权意义上成立。

对称性

这一性质来自对数运算。

信息论解释

PMI可以解释为观察到 时关于 的信息增益:

其中 是熵函数。这意味着当给定上下文 时,词 的不确定性减少量就是PMI。

4.1.3 PMI的问题与解决方案

零值问题

(从未共现)时:

这在数值计算中是个严重问题,因为:

  • 大量的零共现会产生
  • 这些值会主导统计结果
  • 后续的矩阵分解会受到影响

解决方案一:加一平滑

解决方案二:PPMI(正点互信息)

PPMI去除了所有负值,将它们替换为0。这一变换具有直观的合理性:负关联可能是噪声,不应该被编码到语义表示中。

PPMI的性质

PPMI保留了正关联信息,同时过滤掉了负关联。实验表明,PPMI变换后的矩阵更适合进行SVD分解,能产生更好的语义表示。

class PMIMatrix:
    """
    PMI矩阵计算器
    
    提供多种PMI变体的计算
    """
    
    def __init__(self, cooc_matrix, total_count=None):
        """
        Args:
            cooc_matrix: 原始共现计数矩阵
            total_count: 语料总词数(如果为None,则用矩阵元素和)
        """
        self.M = cooc_matrix.astype(np.float64)
        if total_count is None:
            self.total = self.M.sum()
        else:
            self.total = total_count
        
        # 预计算边际概率
        self.word_probs = self.M.sum(axis=1, keepdims=True) / self.total
        self.context_probs = self.M.sum(axis=0, keepdims=True) / self.total
    
    def compute_pmi(self, log_base=2):
        """
        计算原始PMI矩阵
        
        PMI(w,c) = log [P(w,c) / (P(w) * P(c))]
        
        Returns:
            pmi_matrix: PMI值矩阵(可能包含负值和-inf)
        """
        with np.errstate(divide='ignore', invalid='ignore'):
            # P(w,c) / (P(w) * P(c))
            ratio = self.M / (self.word_probs * self.context_probs * self.total)
            pmi = np.log(ratio) / np.log(log_base)
            # 处理零除问题
            pmi[~np.isfinite(pmi)] = 0
        
        return pmi
    
    def compute_ppmi(self, log_base=2, eps=1e-10):
        """
        计算PPMI矩阵
        
        PPMI(w,c) = max(0, PMI(w,c))
        
        Returns:
            ppmi_matrix: PPMI值矩阵(非负)
        """
        pmi = self.compute_pmi(log_base)
        # 截断负值
        ppmi = np.maximum(pmi, 0)
        return ppmi
    
    def compute_npmi(self, log_base=2, eps=1e-10):
        """
        计算NPMI(正规化PMI)
        
        NPMI(w,c) = PMI(w,c) / -log P(w,c)
        
        取值范围为[-1, 1],便于跨词比较
        """
        pmi = self.compute_pmi(log_base)
        
        # P(w,c)
        joint_prob = self.M / self.total
        
        # 避免log(0)
        with np.errstate(divide='ignore', invalid='ignore'):
            npmi = pmi / (-np.log(joint_prob + eps))
            npmi[~np.isfinite(npmi)] = 0
        
        return npmi
    
    def compute_sppmi(self, k=5, log_base=2):
        """
        计算S-PSI(平滑正PMI的一种形式)
        
        考虑负采样的PMI变体
        """
        pmi = self.compute_pmi(log_base)
        
        # 计算负采样的期望PMI
        # 这是一种简化的实现
        neg_bias = np.log(k)
        
        sppmi = pmi - np.log(k)
        sppmi = np.maximum(sppmi, 0)
        
        return sppmi

4.2 PMI变体家族

4.2.1 正规化PMI(NPMI)

正规化点互信息(NPMI)通过除以负对数似然来规范化PMI:

NPMI的性质

NPMI的取值范围为

NPMI值含义
1完美正关联:
0统计独立:
-1完全不相容:

这一规范化特性使得NPMI更适合进行跨词比较,因为它的取值不依赖于共现频率的绝对大小。

4.2.2 差分PMI(DPMI)

差分PMI(DPMI)考虑位置信息的PMI变体,用于捕捉词的方向性语义:

其中 分别表示左右两侧的上下文。

应用场景

DPMI可以捕捉语义的方向性:

  • “在…之前” vs “在…之后”:left和right的PMI差异
  • 主语vs宾语关系:通过依存方向区分
def compute_dpmi(corpus, vocabulary, window_size=2):
    """
    计算差分PMI矩阵
    
    DPMI捕捉词与左右上下文的非对称关系
    """
    vocab_size = len(vocabulary)
    
    # 分别统计左右共现
    left_counts = np.zeros((vocab_size, vocab_size))
    right_counts = np.zeros((vocab_size, vocab_size))
    word_counts = np.zeros(vocab_size)
    
    word_to_idx = vocabulary
    
    for i, word in enumerate(corpus):
        if word not in word_to_idx:
            continue
        
        word_idx = word_to_idx[word]
        word_counts[word_idx] += 1
        
        # 左侧上下文
        for j in range(max(0, i - window_size), i):
            ctx_word = corpus[j]
            if ctx_word in word_to_idx:
                left_counts[word_idx, word_to_idx[ctx_word]] += 1
        
        # 右侧上下文
        for j in range(i + 1, min(len(corpus), i + window_size + 1)):
            ctx_word = corpus[j]
            if ctx_word in word_to_idx:
                right_counts[word_idx, word_to_idx[ctx_word]] += 1
    
    # 计算PMI
    total = len(corpus)
    
    # 左PMI
    left_pmi = compute_pmi_from_counts(left_counts, word_counts, total)
    # 右PMI
    right_pmi = compute_pmi_from_counts(right_counts, word_counts, total)
    
    # DPMI = 左PMI - 右PMI
    dpmi = left_pmi - right_pmi
    
    return dpmi, left_counts, right_counts
 
 
def compute_pmi_from_counts(counts, word_counts, total):
    """从计数矩阵计算PMI"""
    vocab_size = len(word_counts)
    
    # 计算词概率
    word_probs = word_counts / total
    
    # 计算上下文概率
    context_probs = counts.sum(axis=1, keepdims=True)
    context_probs = np.divide(
        context_probs, 
        context_probs.sum() + 1e-10
    )
    
    # 计算联合概率
    joint_probs = counts / total
    
    # 计算PMI
    with np.errstate(divide='ignore', invalid='ignore'):
        ratio = joint_probs / (word_probs[:, None] * context_probs)
        pmi = np.log2(ratio + 1e-10)
    
    return pmi

4.2.3 PMI-IR(基于信息检索的PMI)

PMI-IR是Church和Hanks(1989)提出的基于信息检索的PMI变体。它利用搜索引擎结果来计算词语相似度:

这种方法在互联网语料成为主要信息源的今天仍然有意义。


五、Skip-gram与CBOW模型深度解析

5.1 Skip-gram模型详解

Skip-gram是Word2Vec的核心模型之一,由Tomas Mikolov等人于2013年提出。它通过预测上下文来学习词向量,是分布式语义学思想与深度学习的完美结合。

5.1.1 模型架构与数学推导

Skip-gram的核心思想

Skip-gram模型的核心假设是:中心词可以有效地预测其上下文词。这一假设直接继承了分布式假说的核心思想,但通过神经网络实现了端到端的学习。

模型结构

输入层: 中心词 $w_t$ 的one-hot向量 $\mathbf{x} \in \mathbb{R}^V$

嵌入层: $\mathbf{v}_t = W_{in}^\top \mathbf{x} \in \mathbb{R}^d$
        其中 $W_{in} \in \mathbb{R}^{V \times d}$ 是输入嵌入矩阵

输出层: 对每个上下文位置 $j$,计算:
        $p(w_{t+j} \mid w_t) = \text{softmax}(\mathbf{u}_{w_{t+j}}^\top \mathbf{v}_t)$
        
        其中 $\mathbf{u}_{w} = W_{out}[:, w]$ 是词 $w$ 的输出嵌入
        $W_{out} \in \mathbb{R}^{d \times V}$ 是输出嵌入矩阵

Softmax函数

输出层的Softmax定义为:

其中 是输入词(中心词), 是输出词(上下文词)。

目标函数

Skip-gram的似然函数:

其中 是上下文窗口半径, 是训练语料中的词数。

对应的负对数似然(损失函数):

5.1.2 Skip-gram与分布式假说的联系

Skip-gram的目标函数直接实现了分布式假说的核心思想:

这等价于:相似的词应该产生相似的上下文预测分布,从而获得相似的向量表示。

Skip-gram的隐含优化

Skip-gram可以被证明等价于(近似地)优化一个矩阵分解目标。Levy和Goldberg(2014)证明,在负采样(Negative Sampling)的假设下,Skip-gram隐式地分解了词-上下文PMI矩阵。

具体而言,Skip-gram学习的词向量 和上下文向量 满足:

其中 是负采样参数。

5.1.3 Skip-gram的完整PyTorch实现

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np
from collections import Counter, defaultdict
 
class SkipGramModel(nn.Module):
    """
    Skip-gram词向量模型
    
    核心思想:用中心词预测上下文词
    训练目标:最大化 p(context | center)
    
    数学上等价于学习一种特殊的PMI嵌入
    """
    
    def __init__(self, vocab_size, embedding_dim, init_range=0.5):
        """
        Args:
            vocab_size: 词汇表大小
            embedding_dim: 嵌入维度
            init_range: 初始化范围 [-init_range/d, init_range/d]
        """
        super().__init__()
        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        
        # 输入嵌入矩阵(用于中心词)
        self.in_embeddings = nn.Embedding(vocab_size, embedding_dim)
        # 输出嵌入矩阵(用于上下文词)
        self.out_embeddings = nn.Embedding(vocab_size, embedding_dim)
        
        # 初始化
        nn.init.uniform_(self.in_embeddings.weight, 
                        -init_range/embedding_dim, 
                        init_range/embedding_dim)
        nn.init.zeros_(self.out_embeddings.weight)
    
    def forward(self, center, context):
        """
        前向传播(用于训练)
        
        Args:
            center: 中心词ID [batch_size]
            context: 上下文词ID [batch_size]
        
        Returns:
            loss: 负对数似然损失
        """
        # 中心词嵌入 [batch, dim]
        v_center = self.in_embeddings(center)
        
        # 上下文词嵌入 [batch, dim]
        u_context = self.out_embeddings(context)
        
        # 计算点积(未归一化的相似度)
        score = torch.sum(v_center * u_context, dim=1)
        
        # 负对数似然损失(简化版,仅用于演示)
        # 实际使用中应使用负采样
        loss = torch.nn.functional.binary_cross_entropy_with_logits(
            score, torch.ones_like(score)
        )
        
        return loss
    
    def get_embeddings(self, layer='in'):
        """
        获取训练好的嵌入向量
        
        Args:
            layer: 'in' 返回输入嵌入, 'out' 返回输出嵌入
                   'both' 返回输入+输出(平均)
        
        Returns:
            embeddings: numpy数组
        """
        if layer == 'in':
            return self.in_embeddings.weight.detach().numpy()
        elif layer == 'out':
            return self.out_embeddings.weight.detach().numpy()
        elif layer == 'both':
            in_emb = self.in_embeddings.weight.detach().numpy()
            out_emb = self.out_embeddings.weight.detach().numpy()
            return (in_emb + out_emb) / 2
        else:
            raise ValueError(f"Unknown layer: {layer}")
 
 
class NegativeSamplingLoss(nn.Module):
    """
    负采样损失函数
    
    负采样通过只更新k个负样本而不是整个词汇表,
    大大提高了训练效率
    
    损失函数:
    L = -log σ(v_c · u_o) - Σ_{i=1}^k log σ(-v_c · u_{n_i})
    """
    
    def __init__(self, num_negatives=5, embedding_dim=100):
        super().__init__()
        self.num_negatives = num_negatives
        self.embedding_dim = embedding_dim
    
    def forward(self, center_emb, context_emb, negative_emb):
        """
        Args:
            center_emb: 中心词嵌入 [batch, dim]
            context_emb: 上下文词嵌入 [batch, dim]
            negative_emb: 负样本嵌入 [batch, num_neg, dim]
        
        Returns:
            loss: 损失标量
        """
        # 正样本损失: -log σ(v_c · u_o)
        pos_score = torch.sum(center_emb * context_emb, dim=1)
        pos_loss = torch.nn.functional.binary_cross_entropy_with_logits(
            pos_score, torch.ones_like(pos_score)
        )
        
        # 负样本损失: -Σ log σ(-v_c · u_n)
        neg_score = torch.bmm(negative_emb, center_emb.unsqueeze(2)).squeeze()
        neg_loss = torch.nn.functional.binary_cross_entropy_with_logits(
            neg_score, torch.zeros_like(neg_score)
        )
        
        return pos_loss + neg_loss
 
 
class Word2VecDataset(Dataset):
    """
    Word2Vec训练数据集
    
    生成 (center_word, context_word, negative_samples) 三元组
    """
    
    def __init__(self, corpus, word_to_idx, window_size=5, 
                 num_negatives=5, sampling_factor=1e-5):
        """
        Args:
            corpus: 语料列表
            word_to_idx: 词汇表
            window_size: 上下文窗口大小
            num_negatives: 负采样数量
            sampling_factor: 高频词下采样概率
        """
        self.corpus = corpus
        self.word_to_idx = word_to_idx
        self.window_size = window_size
        self.num_negatives = num_negatives
        
        # 计算词频分布(用于负采样)
        word_counts = Counter(corpus)
        total = sum(word_counts.values())
        
        # 计算采样概率(基于词频的3/4次方)
        self.neg_sampling_probs = {}
        for word, count in word_counts.items():
            if word in word_to_idx:
                self.neg_sampling_probs[word] = (count / total) ** 0.75
        
        # 构建采样表
        self._build_sampling_table()
        
        # 生成训练样本
        self.training_pairs = self._generate_pairs()
        
        # 过滤有效样本
        self.training_pairs = [
            (c, ctx, self._sample_negatives())
            for c, ctx in self.training_pairs
            if c in word_to_idx and ctx in word_to_idx
        ]
    
    def _build_sampling_table(self):
        """构建负采样表"""
        vocab = list(self.neg_sampling_probs.keys())
        probs = np.array([self.neg_sampling_probs[w] for w in vocab])
        probs /= probs.sum()
        
        # 采样表(每个词重复的次数与其采样概率成正比)
        self.sampling_table = np.random.choice(
            vocab, 
            size=10000000, 
            p=probs,
            replace=True
        )
    
    def _generate_pairs(self):
        """生成中心词-上下文词对"""
        pairs = []
        n = len(self.corpus)
        
        for i, word in enumerate(self.corpus):
            start = max(0, i - self.window_size)
            end = min(n, i + self.window_size + 1)
            
            for j in range(start, end):
                if i != j:
                    pairs.append((self.corpus[i], self.corpus[j]))
        
        return pairs
    
    def _sample_negatives(self):
        """采样负样本"""
        indices = np.random.randint(0, len(self.sampling_table), 
                                   size=self.num_negatives)
        return [self.sampling_table[i] for i in indices]
    
    def __len__(self):
        return len(self.training_pairs)
    
    def __getitem__(self, idx):
        center, context, negs = self.training_pairs[idx]
        
        return (
            self.word_to_idx[center],
            self.word_to_idx[context],
            [self.word_to_idx.get(n, 0) for n in negs]
        )
 
 
def train_word2vec(corpus, vocab_size=50000, embedding_dim=100,
                   window_size=5, num_negatives=5, learning_rate=0.01,
                   epochs=5, batch_size=512, min_count=5):
    """
    完整的Word2Vec训练流程
    
    Args:
        corpus: 语料列表(分词后)
        vocab_size: 词汇表大小上限
        embedding_dim: 嵌入维度
        window_size: 上下文窗口大小
        num_negatives: 负采样数量
        learning_rate: 学习率
        epochs: 训练轮数
        batch_size: 批大小
        min_count: 最小词频阈值
    
    Returns:
        model: 训练好的模型
        word_to_idx: 词汇表
    """
    # 1. 构建词汇表
    word_counts = Counter(corpus)
    filtered_words = [
        word for word, count in word_counts.items()
        if count >= min_count
    ]
    filtered_words.sort(key=lambda w: -word_counts[w])
    
    if len(filtered_words) > vocab_size:
        filtered_words = filtered_words[:vocab_size]
    
    word_to_idx = {w: i for i, w in enumerate(filtered_words)}
    idx_to_word = {i: w for w, i in word_to_idx.items()}
    
    print(f"词汇表大小: {len(word_to_idx)}")
    
    # 2. 创建数据集和数据加载器
    dataset = Word2VecDataset(
        corpus, word_to_idx, 
        window_size=window_size,
        num_negatives=num_negatives
    )
    dataloader = DataLoader(
        dataset, 
        batch_size=batch_size, 
        shuffle=True,
        num_workers=0
    )
    
    print(f"训练样本数: {len(dataset)}")
    
    # 3. 初始化模型
    vocab_size = len(word_to_idx)
    model = SkipGramModel(vocab_size, embedding_dim)
    neg_loss_fn = NegativeSamplingLoss(num_negatives, embedding_dim)
    optimizer = optim.Adam(model.parameters(), lr=learning_rate)
    
    # 4. 训练循环
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = model.to(device)
    
    for epoch in range(epochs):
        total_loss = 0
        n_batches = 0
        
        for batch in dataloader:
            center = batch[0].to(device)
            context = batch[1].to(device)
            negatives = torch.tensor(
                [s[:num_negatives] for s in batch[2]]
            ).to(device)
            
            # 前向传播
            center_emb = model.in_embeddings(center)
            context_emb = model.out_embeddings(context)
            negative_emb = model.out_embeddings(negatives)
            
            # 计算损失
            loss = neg_loss_fn(center_emb, context_emb, negative_emb)
            
            # 反向传播
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            
            total_loss += loss.item()
            n_batches += 1
        
        avg_loss = total_loss / n_batches
        print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}")
    
    return model, word_to_idx

5.2 CBOW模型详解

CBOW(Continuous Bag-of-Words)与Skip-gram互补,用上下文词预测中心词。

5.2.1 模型架构

输入: 上下文词 (c个词,每个是V维one-hot向量)
     ↓ 分别通过嵌入矩阵
     ↓ 求和/平均
隐藏层: 上下文平均嵌入 $\bar{v} = \frac{1}{2c}\sum_{-c \leq j \leq c, j \neq 0} v_{w_{t+j}}$
     ↓ 输出嵌入矩阵
输出: Softmax → 中心词概率分布

目标函数

5.2.2 CBOW与Skip-gram的对比分析

特性Skip-gramCBOW
预测方向中心→上下文上下文→中心
训练样本数较少(中心词少)较多(上下文词多)
稀有词处理更好(每个样本都独立)较差(被平均稀释)
训练速度较慢(更多输出词)较快
大规模语料表现好可能欠拟合
语义精度较好略差

5.2.3 CBOW实现

class CBOWModel(nn.Module):
    """
    CBOW词向量模型
    
    用上下文词的嵌入之和来预测中心词
    
    核心思想:中心词的意义由其上下文"投票"决定
    """
    
    def __init__(self, vocab_size, embedding_dim):
        super().__init__()
        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        
        # 共享嵌入矩阵
        self.embeddings = nn.Embedding(vocab_size, embedding_dim)
        
        # 输出层
        self.output_layer = nn.Linear(embedding_dim, vocab_size, bias=False)
        
        # 初始化
        nn.init.uniform_(self.embeddings.weight, -0.5/embedding_dim, 0.5/embedding_dim)
    
    def forward(self, context_words, context_mask=None):
        """
        CBOW前向传播
        
        Args:
            context_words: 上下文词ID张量 [batch, 2*window_size]
            context_mask: 有效上下文掩码 [batch, 2*window_size](可选)
        
        Returns:
            logits: 未归一化的词表得分 [batch, vocab_size]
        """
        # 嵌入上下文词
        embedded = self.embeddings(context_words)  # [batch, 2*w, dim]
        
        # 求和(如果有掩码则加权求和)
        if context_mask is not None:
            embedded = embedded * context_mask.unsqueeze(-1)
            context_sum = embedded.sum(dim=1)
            context_count = context_mask.sum(dim=1, keepdim=True).clamp(min=1)
            context_mean = context_sum / context_count
        else:
            context_mean = embedded.mean(dim=1)
        
        # 输出层
        logits = self.output_layer(context_mean)
        
        return logits
    
    def get_embeddings(self):
        """获取词嵌入"""
        return self.embeddings.weight.detach().numpy()
 
 
class CBOWWithNegativeSampling(nn.Module):
    """
    带负采样的CBOW模型
    
    使用负采样来近似softmax,提高训练效率
    """
    
    def __init__(self, vocab_size, embedding_dim, num_negatives=5):
        super().__init__()
        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        self.num_negatives = num_negatives
        
        self.embeddings = nn.Embedding(vocab_size, embedding_dim)
        self.output_bias = nn.Parameter(torch.zeros(vocab_size))
        
        nn.init.uniform_(self.embeddings.weight, -0.5/embedding_dim, 0.5/embedding_dim)
    
    def forward(self, context_words, target, negatives):
        """
        Args:
            context_words: 上下文词ID [batch, 2*window_size]
            target: 目标词ID [batch]
            negatives: 负样本ID [batch, num_neg]
        
        Returns:
            loss: 损失标量
        """
        batch_size = context_words.size(0)
        
        # 上下文嵌入
        ctx_emb = self.embeddings(context_words).mean(dim=1)  # [batch, dim]
        
        # 目标词嵌入
        target_emb = self.embeddings(target)  # [batch, dim]
        
        # 负样本嵌入
        neg_emb = self.embeddings(negatives)  # [batch, num_neg, dim]
        
        # 正样本分数
        pos_score = torch.sum(ctx_emb * target_emb, dim=1)
        pos_loss = torch.nn.functional.binary_cross_entropy_with_logits(
            pos_score, torch.ones_like(pos_score)
        )
        
        # 负样本分数
        neg_score = torch.bmm(neg_emb, ctx_emb.unsqueeze(2)).squeeze()
        neg_loss = torch.nn.functional.binary_cross_entropy_with_logits(
            neg_score, torch.zeros_like(neg_score)
        )
        
        return pos_loss + neg_loss

六、语义空间的几何性质与代数结构

6.1 线性语义关系

分布式语义空间的一个惊人发现是:词向量空间中存在系统性的线性关系(Mikolov et al., 2013)。

6.1.1 语义类比现象

最著名的例子是”king - man + woman ≈ queen”:

数学解释

设性别语义可以用一个方向向量 表示:

则:

这意味着:

  • 从”king”沿着性别方向移动
  • 到达”queen”(女性对应词)

语义类比的应用

语义类比可以用于多种NLP任务:

类比类型公式示例
语义类比A:B ≈ C:D北京:中国 :: 东京:日本
句法类比动词变形run:running ≈ swim:swimming
单复数名词变形apple:apples ≈ car:cars
def evaluate_word_analogies(embeddings, word_to_idx, idx_to_word, analogy_file=None):
    """
    评估词向量的语义类比能力
    
    典型类比格式:
    :capital-common-countries
    Paris France Tokyo Japan
    Beijing China Tokyo Japan
    
    Args:
        embeddings: 词嵌入矩阵
        word_to_idx: 词汇表
        idx_to_word: 索引到词的映射
        analogy_file: 类比测试文件路径(可选)
    
    Returns:
        accuracy: 类比准确率
        results: 详细结果
    """
    from sklearn.metrics.pairwise import cosine_similarity
    
    # 如果没有提供类比文件,使用内置测试集
    if analogy_file is None:
        analogies = [
            # 首都关系
            ('france', 'paris', 'japan', 'tokyo'),
            ('china', 'beijing', 'germany', 'berlin'),
            # 单复数
            ('apple', 'apples', 'car', 'cars'),
            ('man', 'men', 'woman', 'women'),
            # 动词变形
            ('run', 'running', 'swim', 'swimming'),
            ('walk', 'walking', 'talk', 'talking'),
            # 性别对应
            ('king', 'queen', 'man', 'woman'),
            ('actor', 'actress', 'waiter', 'waitress'),
            # 比较级
            ('good', 'better', 'bad', 'worse'),
        ]
    else:
        analogies = load_analogies_from_file(analogy_file)
    
    correct = 0
    total = 0
    results = []
    
    for a, b, c, expected in analogies:
        # 跳过词汇表中不存在的词
        if not all(w in word_to_idx for w in [a, b, c, expected]):
            continue
        
        # 计算目标向量
        v_a = embeddings[word_to_idx[a]]
        v_b = embeddings[word_to_idx[b]]
        v_c = embeddings[word_to_idx[c]]
        
        # v_target = v_b - v_a + v_c
        v_target = v_b - v_a + v_c
        
        # 计算与所有词的余弦相似度
        similarities = cosine_similarity([v_target], embeddings)[0]
        
        # 找出最相似的词(排除a, b, c)
        similarities[word_to_idx[a]] = -np.inf
        similarities[word_to_idx[b]] = -np.inf
        similarities[word_to_idx[c]] = -np.inf
        
        predicted_idx = np.argmax(similarities)
        predicted_word = idx_to_word[predicted_idx]
        
        is_correct = predicted_word == expected
        if is_correct:
            correct += 1
        total += 1
        
        results.append({
            'a': a, 'b': b, 'c': c, 'expected': expected,
            'predicted': predicted_word,
            'correct': is_correct,
            'similarity': similarities[predicted_idx]
        })
    
    accuracy = correct / total if total > 0 else 0
    
    print("=" * 60)
    print("语义类比评估结果")
    print("=" * 60)
    print(f"准确率: {accuracy:.2%} ({correct}/{total})")
    
    for r in results:
        status = "✓" if r['correct'] else "✗"
        print(f"{status} {r['a']}:{r['b']} :: {r['c']}:? | "
              f"expected={r['expected']}, predicted={r['predicted']}")
    
    return accuracy, results
 
 
def load_analogies_from_file(filepath):
    """从文件加载类比测试集"""
    analogies = []
    with open(filepath, 'r') as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith(':'):
                continue
            parts = line.lower().split()
            if len(parts) == 4:
                analogies.append(tuple(parts))
    return analogies

6.1.2 语义超平面与类别分割

不同语义类别在向量空间中形成聚类,可以通过超平面进行分割。

二元分类超平面

对于二分类问题,可以使用支持向量机(SVM)找到最优分割超平面:

其中 是法向量, 是偏置。

多类分割

对于多类问题,可以构建多类分类器:

def train_semantic_classifier(embeddings, word_to_idx, category_dict):
    """
    训练语义分类器,验证语义空间的类别可分性
    
    Args:
        embeddings: 词嵌入矩阵
        word_to_idx: 词汇表
        category_dict: 类别字典 {category_name: [word1, word2, ...]}
    """
    from sklearn.linear_model import LogisticRegression
    from sklearn.model_selection import cross_val_score
    
    # 收集训练数据
    X = []
    y = []
    categories = list(category_dict.keys())
    cat_to_label = {cat: i for i, cat in enumerate(categories)}
    
    for cat_name, words in category_dict.items():
        for word in words:
            if word in word_to_idx:
                X.append(embeddings[word_to_idx[word]])
                y.append(cat_to_label[cat_name])
    
    X = np.array(X)
    y = np.array(y)
    
    # 训练分类器
    clf = LogisticRegression(max_iter=1000, multi_class='multinomial')
    scores = cross_val_score(clf, X, y, cv=5)
    
    print("=" * 60)
    print("语义分类器评估")
    print("=" * 60)
    print(f"5折交叉验证准确率: {scores.mean():.2%} ± {scores.std():.2%}")
    
    # 如果分类准确率高,说明语义空间具有良好的类别分离性
    if scores.mean() > 0.8:
        print("结论: 语义空间类别分离性良好")
    elif scores.mean() > 0.6:
        print("结论: 语义空间类别有一定分离性")
    else:
        print("结论: 语义空间类别分离性较差")
    
    return clf, scores

6.2 语义空间的度量性质

6.2.1 距离分布分析

词向量空间中的距离分布呈现特定的统计规律,这些规律反映了语言的语义结构。

距离分布类型

距离分布直方图(典型形态):

频率
  │
  │                     ████
  │                   ████████████
  │                ████████████████
  │             ███████████████████████
  │          ███████████████████████████████
  │       ███████████████████████████████████████
  │    ████████████████████████████████████████████████████
  └─────────────────────────────────────────────────────────────→ 距离
    0    0.5    1.0    1.5    2.0    2.5    3.0    3.5    4.0

特征分析:
- 短尾区域: 相似词对集中(cat-dog, happy-glad)
- 长尾区域: 不相关词对(cat-philosophy, happy-electron)
- 中间区域: 一般相似词对

距离分布代码分析

def analyze_distance_distribution(embeddings, word_to_idx, sample_size=10000):
    """
    分析词向量空间的距离分布
    
    统计不同类型词对之间的距离分布
    """
    from sklearn.metrics.pairwise import cosine_distances
    
    vocab_size = len(word_to_idx)
    n = min(sample_size, vocab_size)
    
    # 随机采样词对
    indices1 = np.random.randint(0, vocab_size, size=n)
    indices2 = np.random.randint(0, vocab_size, size=n)
    
    # 计算余弦距离
    distances = []
    for i1, i2 in zip(indices1, indices2):
        if i1 != i2:
            v1, v2 = embeddings[i1], embeddings[i2]
            dist = 1 - np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
            distances.append(dist)
    
    distances = np.array(distances)
    
    print("=" * 60)
    print("距离分布统计")
    print("=" * 60)
    print(f"均值: {distances.mean():.4f}")
    print(f"标准差: {distances.std():.4f}")
    print(f"中位数: {np.median(distances):.4f}")
    print(f"最小值: {distances.min():.4f}")
    print(f"最大值: {distances.max():.4f}")
    print(f"25%分位数: {np.percentile(distances, 25):.4f}")
    print(f"75%分位数: {np.percentile(distances, 75):.4f}")
    
    return distances

6.2.2 相似度与语义关联性

分布式语义学测量的相似度与人类的语义关联性高度相关。

def evaluate_similarity_correlation(embeddings, word_pairs, human_ratings):
    """
    评估词向量相似度与人类评分的一致性
    
    Args:
        embeddings: 词嵌入矩阵
        word_pairs: 词对列表 [(word1, word2), ...]
        human_ratings: 人类相似度评分 [0-10]
    """
    from scipy.stats import spearmanr, pearsonr
    
    computed_similarities = []
    
    for w1, w2 in word_pairs:
        if w1 in word_to_idx and w2 in word_to_idx:
            v1 = embeddings[word_to_idx[w1]]
            v2 = embeddings[word_to_idx[w2]]
            sim = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
            computed_similarities.append(sim)
        else:
            computed_similarities.append(np.nan)
    
    # 过滤缺失值
    valid_mask = ~np.isnan(computed_similarities)
    computed = np.array(computed_similarities)[valid_mask]
    human = np.array(human_ratings)[valid_mask]
    
    # 计算相关系数
    spearman_corr, spearman_p = spearmanr(computed, human)
    pearson_corr, pearson_p = pearsonr(computed, human)
    
    print("=" * 60)
    print("相似度相关性评估")
    print("=" * 60)
    print(f"Spearman相关系数: {spearman_corr:.4f} (p={spearman_p:.4e})")
    print(f"Pearson相关系数: {pearson_corr:.4f} (p={pearson_p:.4e})")
    
    return spearman_corr, pearson_corr

6.3 语义空间的异常现象

6.3.1 维度灾难与稀疏性

高维空间中存在”维度灾难”问题,这对语义空间的构建有重要影响。

维度灾难的数学表述

维单位超立方体中,距离原点在 以内的超立方体体积比例:

这意味着:

  • 随机点几乎都分布在”表面”
  • 点间距离趋于相等
  • 聚类效果下降

解决方案

  1. 适度降维:使用50-300维而非原始的高维空间
  2. 正则化:在训练过程中添加L2正则化
  3. 稀疏编码:鼓励稀疏表示

6.3.2 语义空间的非线性结构

简单的线性模型可能无法捕捉复杂的语义关系。某些语义关系需要非线性方法才能准确建模。

def analyze_linearity(embeddings, word_to_idx, analogy_pairs):
    """
    分析语义空间中线性关系的强度
    
    检查哪些语义关系可以用线性运算捕捉
    """
    from sklearn.metrics.pairwise import cosine_similarity
    
    linear_correct = 0
    total = 0
    linearity_scores = []
    
    for a, b, c, d_expected in analogy_pairs:
        if not all(w in word_to_idx for w in [a, b, c, d_expected]):
            continue
        
        # 计算类比向量的接近程度
        v_a = embeddings[word_to_idx[a]]
        v_b = embeddings[word_to_idx[b]]
        v_c = embeddings[word_to_idx[c]]
        v_d_expected = embeddings[word_to_idx[d_expected]]
        
        # 期望的类比向量
        v_analogy = v_b - v_a + v_c
        
        # 实际向量与期望向量的相似度
        similarity = cosine_similarity([v_analogy], [v_d_expected])[0, 0]
        linearity_scores.append(similarity)
        
        total += 1
    
    avg_linearity = np.mean(linearity_scores)
    
    print("=" * 60)
    print("线性关系分析")
    print("=" * 60)
    print(f"平均线性关系强度: {avg_linearity:.4f}")
    
    if avg_linearity > 0.7:
        print("结论: 语义关系高度线性化")
    elif avg_linearity > 0.4:
        print("结论: 语义关系部分线性化")
    else:
        print("结论: 语义关系非线性化明显")
    
    return linearity_scores

七、GloVe模型与全局统计方法

7.1 GloVe模型原理

GloVe(Global Vectors for Word Representation)是Pennington等人于2014年提出的方法,结合了全局矩阵分解和局部上下文方法的优点。

7.1.1 模型定义

GloVe的核心目标是学习词向量,使得词的共现概率比值能够被词向量的点积准确预测。

共现概率比

考虑一对词 关于上下文词 的共现概率比:

这个比值能够捕捉词语之间的语义关系:

  • 时, 的关联更强
  • 时, 的关联更强
  • 时, 与两者关联相同

GloVe的目标函数

其中:

  • 是词 和上下文词 的共现计数
  • 是加权函数
  • 是偏置项

加权函数设计

典型的参数设置为:

class GloVeModel(nn.Module):
    """
    GloVe词嵌入模型
    
    结合全局共现统计与局部上下文窗口
    目标: 使词向量的点积近似对数共现计数
    """
    
    def __init__(self, vocab_size, embedding_dim, x_max=100, alpha=0.75):
        super().__init__()
        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        self.x_max = x_max
        self.alpha = alpha
        
        # 词嵌入
        self.w = nn.Embedding(vocab_size, embedding_dim)
        # 上下文嵌入
        self.w_tilde = nn.Embedding(vocab_size, embedding_dim)
        # 偏置
        self.b = nn.Parameter(torch.zeros(vocab_size))
        self.b_tilde = nn.Parameter(torch.zeros(vocab_size))
        
        # 初始化
        nn.init.uniform_(self.w.weight, -0.5/embedding_dim, 0.5/embedding_dim)
        nn.init.uniform_(self.w_tilde.weight, -0.5/embedding_dim, 0.5/embedding_dim)
    
    def weighting_function(self, X):
        """
        GloVe的加权函数 f(x)
        
        f(x) = (x/x_max)^alpha  if x < x_max
               1                 otherwise
        """
        return torch.where(
            X < self.x_max,
            (X.float() / self.x_max) ** self.alpha,
            torch.ones_like(X.float())
        )
    
    def forward(self, i_indices, j_indices, X_ij):
        """
        GloVe前向传播
        
        Args:
            i_indices: 词索引 [batch_size]
            j_indices: 上下文词索引 [batch_size]
            X_ij: 共现计数 [batch_size]
        
        Returns:
            loss: 加权均方误差损失
        """
        # 获取嵌入
        w_i = self.w(i_indices)  # [batch, dim]
        w_j = self.w_tilde(j_indices)  # [batch, dim]
        b_i = self.b[i_indices]
        b_j = self.b_tilde[j_indices]
        
        # 计算预测值
        X_pred = torch.sum(w_i * w_j, dim=1) + b_i + b_j
        
        # 真实值的对数
        X_true = torch.log(X_ij.float() + 1)
        
        # 加权损失
        weight = self.weighting_function(X_ij)
        loss = weight * (X_pred - X_true) ** 2
        
        return loss.mean()
 
 
def train_glove(cooc_matrix, vocab_size=50000, embedding_dim=100,
                x_max=100, alpha=0.75, learning_rate=0.05,
                epochs=50, batch_size=512):
    """
    训练GloVe模型
    
    Args:
        cooc_matrix: 共现矩阵 (scipy sparse)
        vocab_size: 词汇表大小
        embedding_dim: 嵌入维度
        x_max: 加权函数阈值
        alpha: 加权函数指数
        learning_rate: 学习率
        epochs: 训练轮数
        batch_size: 批大小
    
    Returns:
        embeddings: 最终词嵌入
    """
    # 转换为COO格式以便高效采样非零元素
    cooc = cooc_matrix.tocoo()
    
    # 创建模型
    model = GloVeModel(vocab_size, embedding_dim, x_max, alpha)
    optimizer = optim.Adam(model.parameters(), lr=learning_rate)
    
    # 创建训练样本(非零元素)
    i_indices = torch.LongTensor(cooc.row)
    j_indices = torch.LongTensor(cooc.col)
    X_ij = torch.FloatTensor(cooc.data)
    
    dataset = TensorDataset(i_indices, j_indices, X_ij)
    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
    
    print(f"训练样本数: {len(dataset)}")
    
    # 训练循环
    for epoch in range(epochs):
        total_loss = 0
        n_batches = 0
        
        for i, j, x in dataloader:
            optimizer.zero_grad()
            loss = model(i, j, x)
            loss.backward()
            optimizer.step()
            
            total_loss += loss.item()
            n_batches += 1
        
        if (epoch + 1) % 5 == 0:
            print(f"Epoch {epoch+1}/{epochs}, Loss: {total_loss/n_batches:.4f}")
    
    # 获取最终嵌入
    embeddings = (model.w.weight + model.w_tilde.weight).detach().numpy()
    
    return embeddings

八、深度学习时代的词向量方法

8.1 从静态嵌入到上下文嵌入

分布式语义学的发展经历了从静态词向量到上下文相关词向量的演进。

静态词向量的局限

Word2Vec、GloVe等方法产生的词向量是静态的:一个词无论在什么上下文中,都对应同一个向量表示。这导致:

  1. 多义词问题:bank既可以是”银行”也可以是”河岸”,但只能有一个向量
  2. 上下文无关:无法区分”The cat sat on the mat”和”The bank sat on the money”

上下文嵌入的解决方案

ELMo(Embeddings from Language Models)首次引入了上下文嵌入的概念:

class ELMoEmbedder:
    """
    ELMo上下文词嵌入
    
    使用双向LSTM生成上下文相关的词向量
    每个词的表示是其所有层表示的加权组合
    """
    
    def __init__(self, model_name='elmo'):
        from allennlp.modules.elmo import Elmo
        
        # 加载预训练ELMo
        options_file = "https://allennlp.s3.amazonaws.com/models/elmo/2x4096_512_2048cnn_2xhighway/elmo_2x4096_512_2048cnn_2xhighway_options.json"
        weight_file = "https://allennlp.s3.amazonaws.com/models/elmo/2x4096_512_2048cnn_2xhighway/elmo_2x4096_512_2048cnn_2xhighway_weights.hdf5"
        
        self.elmo = Elmo(options_file, weight_file, num_output_representations=1)
    
    def embed(self, sentences):
        """
        生成上下文嵌入
        
        Args:
            sentences: 句子列表 [['The', 'cat', 'sat'], ...]
        
        Returns:
            embeddings: 上下文嵌入张量
        """
        embeddings = self.elmo(sentences)
        return embeddings['elmo_representations'][0]

8.2 Transformer时代的语义表示

8.2.1 BERT的双向上下文建模

BERT(Bidirectional Encoder Representations from Transformers)使用Transformer编码器架构,实现了真正的双向上下文建模:

核心创新

  1. 遮蔽语言模型(Masked LM):随机遮蔽输入中的词,预测被遮蔽的词
  2. 下一句预测(NSP):判断句子对是否是连续的上下文
  3. 双向上下文:同时考虑左侧和右侧的上下文
class BERTEmbedder:
    """
    BERT上下文词嵌入
    
    使用Transformer编码器生成深度双向上下文嵌入
    """
    
    def __init__(self, model_name='bert-base-uncased'):
        from transformers import BertModel, BertTokenizer
        
        self.tokenizer = BertTokenizer.from_pretrained(model_name)
        self.model = BertModel.from_pretrained(model_name)
        self.model.eval()
    
    def embed_sentence(self, sentence):
        """
        获取句子的BERT嵌入
        
        Args:
            sentence: 输入句子字符串
        
        Returns:
            token_embeddings: 每个token的嵌入 [seq_len, hidden_dim]
            sentence_embedding: 句子级别的嵌入 [hidden_dim]
        """
        # 分词
        inputs = self.tokenizer(
            sentence, 
            return_tensors='pt',
            padding=True,
            truncation=True
        )
        
        # 前向传播
        with torch.no_grad():
            outputs = self.model(**inputs)
        
        # 获取最后一层隐藏状态
        hidden_states = outputs.last_hidden_state
        
        # 第一个token([CLS])的嵌入作为句子嵌入
        sentence_embedding = hidden_states[:, 0, :]
        
        return hidden_states, sentence_embedding
    
    def embed_words(self, sentence):
        """
        获取句子中每个词的上下文嵌入
        
        使用WordPiece分词,可能一个词对应多个token
        """
        inputs = self.tokenizer(
            sentence,
            return_tensors='pt',
            return_offsets_mapping=True
        )
        
        offsets = inputs.pop('offset_mapping')[0]
        inputs.pop('token_type_ids', None)
        
        with torch.no_grad():
            outputs = self.model(**inputs)
        
        hidden_states = outputs.last_hidden_state[0]  # [seq_len, hidden]
        
        # 将WordPiece嵌入聚合回原始词
        word_embeddings = self._aggregate_subwords(hidden_states, offsets)
        
        return word_embeddings
    
    def _aggregate_subwords(self, hidden_states, offsets):
        """将子词嵌入聚合为词嵌入"""
        word_embeddings = []
        
        for i, offset in enumerate(offsets):
            if offset[0] == offset[1]:  # 特殊token
                continue
            
            # 找到所有属于当前词的子词
            subword_indices = []
            for j, o in enumerate(offsets):
                if o[0] >= offset[0] and o[1] <= offset[1]:
                    subword_indices.append(j)
            
            if subword_indices:
                # 平均聚合
                avg_embedding = hidden_states[subword_indices].mean(dim=0)
                word_embeddings.append(avg_embedding.numpy())
        
        return np.array(word_embeddings)

8.2.2 语义空间的新特性

Transformer模型产生的语义空间具有一些新特性:

层次化语义表示

BERT隐藏层的语义特性(从浅到深):

Layer 1-4:   浅层表示
             ├── 形态学信息(词形变化)
             ├── 位置编码信息
             └── 基础语法关系

Layer 5-8:   中层表示
             ├── 句法依赖关系
             ├── 实体类型信息
             └── 局部语义关系

Layer 9-12:  深层表示
             ├── 深层语义关系
             ├── 跨句推理
             └── 任务相关知识
def analyze_bert_layers(bert_model, sentence_pairs, relation_type='synonymy'):
    """
    分析BERT不同层级的语义特性
    
    Args:
        bert_model: BERT模型
        sentence_pairs: 句子对列表
        relation_type: 关系类型
    """
    import matplotlib.pyplot as plt
    
    from transformers import BertModel, BertTokenizer
    
    tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
    model = BertModel.from_pretrained('bert-base-uncased', output_hidden_states=True)
    model.eval()
    
    layer_similarities = {i: [] for i in range(13)}  # 12层 + 嵌入层
    
    for sent1, sent2 in sentence_pairs:
        # 分词
        inputs1 = tokenizer(sent1, return_tensors='pt', truncation=True, max_length=128)
        inputs2 = tokenizer(sent2, return_tensors='pt', truncation=True, max_length=128)
        
        with torch.no_grad():
            outputs1 = model(**inputs1)
            outputs2 = model(**inputs2)
        
        hidden_states1 = outputs1.hidden_states
        hidden_states2 = outputs2.hidden_states
        
        # 计算每层的相似度
        for layer_idx in range(13):
            # 使用[CLS]token的嵌入
            h1 = hidden_states1[layer_idx][0, 0]
            h2 = hidden_states2[layer_idx][0, 0]
            
            sim = torch.cosine_similarity(h1.unsqueeze(0), h2.unsqueeze(0))
            layer_similarities[layer_idx].append(sim.item())
    
    # 绘制结果
    layer_means = [np.mean(layer_similarities[i]) for i in range(13)]
    
    plt.figure(figsize=(10, 6))
    plt.plot(range(13), layer_means, 'bo-')
    plt.xlabel('BERT Layer')
    plt.ylabel('Average Cosine Similarity')
    plt.title(f'Semantic Similarity by Layer ({relation_type})')
    plt.grid(True)
    plt.show()

九、实践应用与前沿方向

9.1 分布式语义学的应用场景

应用领域具体任务分布式语义的作用
信息检索查询-文档匹配计算语义相似度,支持同义词扩展
文本分类情感分析、主题分类文本表示与特征提取
机器翻译跨语言对齐语义空间映射与转换
问答系统问答应答语义匹配与推理
推荐系统物品相似度协同过滤的语义扩展
语义搜索意图匹配深层语义理解

9.2 前沿研究方向

神经符号推理 将分布式表示与符号推理结合,构建更强大的AI系统。

多模态语义 整合视觉、语言、音频等多种模态的语义信息。

动态语义 研究词语意义的实时变化和新兴用法。


参考文献与推荐阅读

  1. Firth, J. R. (1957). Studies in linguistic analysis. Blackwell.
  2. Landauer, T. K., & Dumais, S. T. (1997). A solution to Plato’s problem: The latent semantic analysis theory of acquisition. Psychological Review, 104(2), 211-240.
  3. Deerwester, S., et al. (1990). Indexing by latent semantic analysis. JASIS, 41(6), 391-407.
  4. Turney, P. D., & Pantel, P. (2010). From frequency to meaning: Vector space models of semantics. JAIR, 37, 141-188.
  5. Mikolov, T., et al. (2013). Efficient estimation of word representations in vector space. ICLR Workshop.
  6. Mikolov, T., et al. (2013). Distributed representations of words and phrases and their compositionality. NIPS.
  7. Pennington, J., Socher, R., & Manning, C. (2014). GloVe: Global vectors for word representation. EMNLP.
  8. Levy, O., & Goldberg, Y. (2014). Neural word embedding as implicit matrix factorization. NIPS.
  9. Peters, M. E., et al. (2018). Deep contextualized word representations. NAACL.
  10. Devlin, J., et al. (2019). BERT: Pre-training of deep bidirectional transformers for language understanding. NAACL.

关联文档