词向量与分布式语义

文档概述

本文档系统阐述词向量的理论基础、实现原理及认知科学联系。重点介绍Word2Vec、GloVe、FastText等经典模型,以及ELMo、BERT等上下文词向量技术,并探讨Embedding空间的几何性质及其认知意义。本文档还将深入讨论词向量在不同NLP任务中的应用、现代预训练语言模型的发展脉络,以及词向量技术的最新研究前沿。

关键词速览

术语英文核心定义
分布式假说Distributional Hypothesis词语由其上下文定义
词向量Word Vector/Embedding词语的稠密向量表示
Word2VecWord2Vec经典的词嵌入训练模型
GloVeGlobal Vectors全局共现统计词嵌入
FastTextFastText子词嵌入模型
上下文词向量Contextualized Embedding随上下文变化的词表示
ELMoEmbeddings from Language Models双向LSTM词向量
BERTBidirectional Encoder RepresentationsTransformer编码器
语义空间Semantic Space词向量张成的空间
认知科学Cognitive Science研究人类认知的学科
TransformerTransformer基于自注意力的序列建模架构
表征学习Representation Learning自动学习数据有效表示的技术
预训练Pre-training在大规模数据上预先训练模型
微调Fine-tuning在特定任务上调整预训练模型
注意力机制Attention Mechanism根据相关性加权聚合信息的技术

一、分布式假说(Distributional Hypothesis)

1.1 理论起源与核心思想

分布式假说是现代计算语言学和词向量研究的理论基石,由英国语言学家约翰·鲁伯特·费斯(John Rupert Firth)于1957年在丹麦哥本哈根举行的第九届国际语言学大会上首次系统性地提出。费斯被公认为现代语言学的奠基人之一,他在伦敦大学亚非学院(SOAS)任教期间,发展了一套独特的语言学理论体系,强调语言的实际使用而非抽象的语法规则。

费斯的经典论述至今仍是NLP领域最重要的指导思想之一,其原文如下:

“You shall know a word by the company it keeps” (观其伴,知其义)

这一假说的核心洞见在于:语义相似的词语倾向于出现在相似的上下文中。因此,我们可以通过分析一个词的上下文分布来推断其语义。这一思想与认知科学中的分布表征假说(Distributed Representation Hypothesis)高度一致,后者认为知识不是存储在单一神经元或节点中,而是分布式地存储在多个神经元的激活模式中。

1.2 历史发展脉络

分布式假说的思想渊源可以追溯到更早的语言学研究:

早期探索阶段(1950-1970年代)

在这一阶段,语言学家开始意识到词语的意义与其使用环境密切相关。费斯的学生和追随者,如韩礼德(M.A.K. Halliday),进一步发展了系统功能语言学,强调语言的社会功能和使用语境。然而,由于计算能力的限制,这一时期的分布式语义研究主要停留在理论层面。

统计语言学阶段(1980-1990年代)

随着计算机技术的发展,研究者开始尝试使用统计方法实现分布式语义表示。这一时期的代表性工作包括:

  • 潜在语义分析(LSA):由Scott Deerwester等人于1990年提出,使用奇异值分解(SVD)对词-文档共现矩阵进行降维,生成词的潜在语义表示。
  • 随机索引(Random Indexing):由Peter Gärdenfors提出,通过随机投影高效地生成分布式语义表示。
  • 贝叶斯潜在语义分析(PLSA):由Thomas Hofmann于1999年提出,使用概率模型对潜在语义进行建模。

这些方法虽然计算效率较低(需要显式构建和分解大型矩阵),但为后来的词向量研究奠定了重要的理论基础。

神经词向量阶段(2003-2013年)

这一阶段的标志性事件是Yoshua Bengio等人在2003年提出的神经语言模型(Neural Language Model),首次使用神经网络学习词的低维密集表示。该模型使用一个三层的神经网络(输入层、隐藏层、输出层)来预测下一个词,同时学习得到词的分布式表示。然而,由于计算复杂度较高(O(V×N)的输出层计算,其中V是词表大小,N是隐藏层维度),该模型在当时并未得到广泛应用。

Word2Vec时代(2013年至今)

2013年,Tomas Mikolov等人提出了Word2Vec模型,包括Skip-gram和CBOW两种架构,通过一系列技术优化(如负采样、分层Softmax)大幅提高了训练效率,使得在大规模语料上训练词向量成为可能。这一突破引发了词向量研究的热潮,催生了GloVe、FastText、ELMo、BERT等一系列重要模型。

1.3 形式化定义与数学基础

从数学角度,分布式假说可以形式化为:

其中上下文相似度可以通过以下方式度量:

这里 表示词语 的上下文向量,可以使用多种方式定义:

词袋模型(Bag-of-Words Context)

最简单的上下文表示方法是将目标词周围的词视为一个集合,不考虑顺序:

其中 是上下文词的词向量, 是以 为中心的上下文窗口。

加权词袋模型(Weighted BOW)

考虑到距离越近的词对目标词语义影响越大,可以使用距离加权:

其中 是衰减因子, 是词 到目标词 的距离。

依存关系上下文(Dependency-based Context)

基于依存句法分析,只考虑与目标词有句法关系的词:

其中 是依存关系类型, 表示Hadamard积(或逐元素乘法)。

1.4 上下文的定义与选择

上下文的定义方式直接影响词向量的质量,不同的上下文定义编码不同类型的语言信息。

窗口上下文(Window-based)

最常用的上下文定义是基于词语周围的固定窗口:

原始句子: "The quick brown fox jumps over the lazy dog"
窗口大小=2时的上下文:
- "quick" 的上下文: [The, brown, fox]
- "fox" 的上下文: [quick, brown, jumps, over]

窗口大小=1时的上下文:
- "quick" 的上下文: [The, brown]
- "fox" 的上下文: [brown, jumps]

窗口大小的选择是一个重要的超参数:

  • 较小的窗口(1-2):倾向于捕捉词汇的句法功能,生成的向量在语法关系上相似(如动词的过去式、名词的复数形式)
  • 较大的窗口(5-10):倾向于捕捉更广泛的语义主题,生成的向量在主题相关性上相似

更广义的上下文定义

上下文类型定义应用场景优势劣势
词汇窗口固定窗口内的词句法/语义分析简单高效忽略长距离依赖
文档级同一文档中的词主题建模捕捉主题相似性忽略词序
依赖结构依存语法关系中的词句法语义编码语法关系需要依存分析器
语义框架同一框架中的词框架语义学深层语义需要框架知识库
句法上下文句法树中的相关节点句法功能精确语法关系计算复杂度高
话语级整个语篇中的词篇章分析宏观语义需要长文本处理

1.5 分布式表示的理论基础

分布式表示的核心思想源于信息论和认知科学的交叉领域。

稀疏表示 vs 分布式表示

传统的one-hot表示是一种稀疏表示,每个词由一个维度为V(词表大小)的向量表示,其中只有一个维度为1,其余全为0。这种表示存在两个主要问题:

  1. 维度灾难:词表大小通常在数万到数百万,导致高维稀疏向量难以处理
  2. 语义鸿沟:任意两个词的表示都是正交的,无法捕捉语义相似性

相比之下,分布式表示将每个词映射到一个低维稠密空间(如100-1000维),每个维度都不是binary的,而是连续值。这种表示的优势在于:

  1. 维度压缩:从V维稀疏向量压缩到d维稠密向量(d << V)
  2. 语义编码:语义相似的词在向量空间中距离较近
  3. 知识迁移:相似的语义特征可以在不同词之间共享

分布式表示的信息论解释

从信息论角度,词向量的学习过程可以理解为一种信息压缩:我们试图用低维向量(d维)来编码关于词语意义的高维信息(理论上需要的信息量):

其中 是词的语义信息量, 是编码所需的信息, 是向量分量的离散级别。

1.6 分布式假说的认知科学对应

分布式假说与认知科学中的多个重要理论存在深刻联系:

分布式记忆理论(Distributed Memory)

心理学的分布式记忆理论认为,记忆不是存储在单一神经元中,而是分布式地存储在多个神经元的连接权重中。这与词向量的分布式表示高度一致:词的语义不是存储在单个维度中,而是编码在整个向量空间中。

原型理论(Prototype Theory)

Eleanor Rosch提出的原型理论认为,概念以原型(典型成员)为中心向外辐射,边界模糊。词向量空间中的语义聚类也呈现类似结构:典型成员(如”水果”类别中的”苹果”)距离类别中心更近,而非典型成员(如”牛油果”)距离较远。

特征捆绑问题(Binding Problem)

神经科学研究表明,不同的感知特征(如颜色、形状、大小)由不同的神经元群处理,但在识别物体时需要将这些特征捆绑在一起。分布式语义表示通过向量空间的连续性提供了一种特征捆绑的机制:词向量的每个维度可能对应多个语义特征的加权组合。


二、经典词向量模型

2.1 Word2Vec系列模型

Word2Vec由Tomas Mikolov等人于2013年在谷歌提出,是词向量研究的里程碑式工作。该模型通过浅层神经网络学习词的高维稠密表示,具有训练速度快、效果好的优点,迅速成为NLP领域的基础工具。

2.1.1 Skip-gram模型详解

Skip-gram模型的核心思想是:用中心词预测上下文词。与传统的基于上下文的语言模型不同,Skip-gram完全忽略了词的顺序,将上下文视为一个词袋。

模型架构

输入层: 中心词 one-hot 向量 [V × 1]
    ↓ W [V × d] (输入嵌入矩阵)
隐藏层: 词嵌入 [d × 1]
    ↓ W' [d × V] (输出嵌入矩阵)
输出层: Softmax → 上下文词概率分布 [V × 1]

目标函数

给定训练语料中的词序列 ,Skip-gram的目标是最大化所有位置的上下文条件概率之和:

其中 是上下文窗口大小,条件概率定义为:

这里 是输入词嵌入(input embedding), 是输出词嵌入(output embedding)。注意Word2Vec维护两套嵌入:输入嵌入和输出嵌入,这在后续的相似度计算中通常会综合使用。

高效训练技术:负采样(Negative Sampling)

由于完整softmax需要遍历整个词表(通常数万到数百万),计算代价极高。负采样通过将多分类问题转化为多个二分类问题来解决这一挑战:

对于每个正样本 ,我们采样 个负样本 ,目标是最大化正样本的对数概率,同时最小化负样本的对数概率:

其中 是sigmoid函数, 是负样本数量(通常取5-20), 是负采样分布,通常采用unigram分布的3/4次方:

这种分布设计使得低频词被采样的概率略高,有助于处理长尾分布。

分层Softmax(Hierarchical Softmax)

另一种加速方法是使用霍夫曼编码构建二叉树,将输出层替换为树结构,每个叶子节点对应一个词。树的深度为 ,因此预测一个词只需要计算 个sigmoid函数,而非整个词表的Softmax。

Skip-gram的数学性质

Skip-gram学习到的词向量具有以下有趣的数学性质:

  1. 线性关系:对于具有某种语法关系的词对,向量差往往编码了这种关系

  2. 类比完成:词向量空间中的向量运算可以完成词类比任务

    • 国家-首都关系:Paris - France + Italy ≈ Rome
    • 动词时态关系:walk - walked + think ≈ thought
    • 公司-CEO关系:Apple - Jobs + Google ≈ Pichai

这些性质表明,Skip-gram不仅捕捉了词的语义相似性,还捕捉了词之间的语法和语义关系。

2.1.2 CBOW模型详解

CBOW(Continuous Bag-of-Words)与Skip-gram互补,用上下文预测中心词。模型假设上下文中所有词的贡献是等价的(词袋假设)。

CBOW模型架构

上下文词: [w_{t-c}, ..., w_{t-1}, w_{t+1}, ..., w_{t+c}]
    ↓ 各自的嵌入
嵌入层: [d × 2c] 个 d维向量
    ↓ 平均池化
隐藏层: [d × 1] 平均向量
    ↓ W' [d × V]
输出层: Softmax → 中心词概率分布 [V × 1]

CBOW与Skip-gram的对比

特性Skip-gramCBOW
学习方向中心→上下文上下文→中心
训练数据量较少(每对词一个样本)较多(每个中心词一个样本)
稀有词处理更好(从中心词学习)较差(被常用词稀释)
训练速度较慢(更多样本)较快
大规模语料表现优秀表现一般
小规模语料表现较差表现尚可

CBOW代码实现

class CBOW(nn.Module):
    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.linear = nn.Linear(embedding_dim, vocab_size, bias=False)
        self.bias = nn.Parameter(torch.zeros(vocab_size))
    
    def forward(self, context_words):
        """
        Args:
            context_words: 上下文词ID [batch_size, window_size * 2]
            形状: [batch_size, 2c]
        
        Returns:
            中心词的对数概率 [batch_size, vocab_size]
        """
        # 获取上下文词的嵌入
        embedded = self.embeddings(context_words)  # [batch, 2c, embed_dim]
        
        # 对上下文词嵌入求平均
        averaged = torch.mean(embedded, dim=1)    # [batch, embed_dim]
        
        # 预测中心词
        output = self.linear(averaged) + self.bias  # [batch, vocab_size]
        
        return output
    
    def get_embeddings(self):
        """获取词嵌入矩阵"""
        return self.embeddings.weight.detach().cpu().numpy()

2.1.3 Skip-gram完整实现

import torch
import torch.nn as nn
import torch.optim as optim
from collections import Counter
import numpy as np
import re
from typing import List, Tuple, Dict
import random
 
class SkipGramModel(nn.Module):
    """
    Skip-gram词向量模型
    
    核心思想:用中心词预测上下文词
    训练目标:最大化真实上下文词的概率,最小化负采样词的概率
    
    关键技术:
    1. 负采样:将多分类转化为二分类
    2. 降采样:对高频词进行随机丢弃
    3. 两套嵌入:输入嵌入和输出嵌入分离
    """
    
    def __init__(self, vocab_size, embedding_dim, learning_rate=0.025, 
                 window_size=5, min_count=5, negative_samples=5, 
                 subsampling_threshold=1e-5):
        super().__init__()
        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        self.learning_rate = learning_rate
        self.window_size = window_size
        self.min_count = min_count
        self.negative_samples = negative_samples
        self.subsampling_threshold = subsampling_threshold
        
        # 词嵌入层
        self.target_embeddings = nn.Embedding(vocab_size, embedding_dim)
        self.context_embeddings = nn.Embedding(vocab_size, embedding_dim)
        
        # 初始化:使用均匀分布
        nn.init.uniform_(self.target_embeddings.weight, -0.5/embedding_dim, 0.5/embedding_dim)
        nn.init.uniform_(self.context_embeddings.weight, -0.5/embedding_dim, 0.5/embedding_dim)
    
    def forward(self, target, context, negative):
        """
        Args:
            target: 中心词ID [batch_size]
            context: 上下文词ID [batch_size]
            negative: 负采样词ID [batch_size, num_negative]
        Returns:
            正样本损失 + 负样本损失
        """
        # 正样本得分
        target_emb = self.target_embeddings(target)      # [batch, dim]
        context_emb = self.context_embeddings(context)   # [batch, dim]
        pos_score = torch.sum(target_emb * context_emb, dim=1)  # [batch]
        pos_loss = torch.nn.functional.binary_cross_entropy_with_logits(
            pos_score, torch.ones_like(pos_score)
        )
        
        # 负样本损失
        neg_emb = self.context_embeddings(negative)     # [batch, k, dim]
        neg_score = torch.bmm(neg_emb, target_emb.unsqueeze(2)).squeeze()  # [batch, k]
        neg_loss = torch.nn.functional.binary_cross_entropy_with_logits(
            neg_score, torch.zeros_like(neg_score)
        )
        
        return pos_loss + neg_loss
    
    def most_similar(self, word, word_to_idx, idx_to_word, top_k=10):
        """
        查找最相似的词
        
        Args:
            word: 查询词
            word_to_idx: 词到索引的映射
            idx_to_word: 索引到词的映射
            top_k: 返回前k个最相似的词
        
        Returns:
            [(词, 相似度), ...]
        """
        word_idx = word_to_idx.get(word)
        if word_idx is None:
            return []
        
        # 使用输入嵌入
        word_vec = self.target_embeddings.weight[word_idx]
        
        # 计算与所有词的余弦相似度
        all_embeddings = self.target_embeddings.weight
        similarities = torch.nn.functional.cosine_similarity(
            word_vec.unsqueeze(0), all_embeddings
        )
        
        # 获取最相似的词
        _, top_indices = torch.topk(similarities, top_k + 1)
        
        results = []
        for idx in top_indices:
            if idx.item() != word_idx:
                results.append((idx_to_word[idx.item()], similarities[idx].item()))
        
        return results[:top_k]
 
 
class Word2VecTrainer:
    """
    Word2Vec训练器
    
    负责数据预处理、词表构建、训练循环
    """
    
    def __init__(self, corpus: List[str], embedding_dim=100, 
                 window_size=5, min_count=5, negative_samples=5,
                 subsampling_threshold=1e-4, epochs=5, learning_rate=0.025):
        self.corpus = corpus
        self.embedding_dim = embedding_dim
        self.window_size = window_size
        self.min_count = min_count
        self.negative_samples = negative_samples
        self.subsampling_threshold = subsampling_threshold
        self.epochs = epochs
        self.learning_rate = learning_rate
        
        self.word_counts = None
        self.vocab = None
        self.word_to_idx = None
        self.idx_to_word = None
        self.total_words = 0
        self.model = None
    
    def preprocess(self, text: str) -> List[str]:
        """
        文本预处理:分词、转小写、过滤特殊字符
        """
        # 简单分词
        text = text.lower()
        tokens = re.findall(r'\b\w+\b', text)
        return tokens
    
    def build_vocab(self, tokenized_corpus: List[List[str]]):
        """
        构建词表
        
        步骤:
        1. 统计词频
        2. 过滤低频词
        3. 构建映射
        4. 计算采样概率
        """
        # 统计词频
        self.word_counts = Counter()
        self.total_words = 0
        for sentence in tokenized_corpus:
            for word in sentence:
                self.word_counts[word] += 1
                self.total_words += 1
        
        # 过滤低频词
        self.vocab = [
            word for word, count in self.word_counts.items() 
            if count >= self.min_count
        ]
        
        # 构建映射
        self.word_to_idx = {word: idx for idx, word in enumerate(self.vocab)}
        self.idx_to_word = {idx: word for word, idx in self.word_to_idx.items()}
        
        print(f"词表大小: {len(self.vocab)}")
        print(f"原始词数: {self.total_words}")
        print(f"高频词: {self.word_counts.most_common(10)}")
    
    def subsample(self, sentence: List[str]) -> List[str]:
        """
        高频词降采样
        
        高频词(如"the", "is")携带的信息量较低,
        根据论文中的公式进行随机丢弃:
        P(discard) = 1 - sqrt(t / f(w))
        
        其中 t 是阈值(通常1e-4),f(w)是词频
        """
        if self.subsampling_threshold <= 0:
            return sentence
        
        result = []
        for word in sentence:
            freq = self.word_counts.get(word, 0) / self.total_words
            keep_prob = min(1, (np.sqrt(freq / self.subsampling_threshold) + 1) 
                           * (self.subsampling_threshold / freq))
            
            if random.random() < keep_prob:
                result.append(word)
        
        return result
    
    def generate_training_data(self, tokenized_corpus: List[List[str]]) -> List[Tuple[int, int]]:
        """
        生成Skip-gram训练数据
        
        Returns:
            [(center_idx, context_idx), ...]
        """
        pairs = []
        
        for sentence in tokenized_corpus:
            # 降采样
            sentence = self.subsample(sentence)
            
            for i, word in enumerate(sentence):
                if word not in self.word_to_idx:
                    continue
                
                center_idx = self.word_to_idx[word]
                
                # 生成正样本:窗口内的上下文词
                start = max(0, i - self.window_size)
                end = min(len(sentence), i + self.window_size + 1)
                
                for j in range(start, end):
                    if i != j and sentence[j] in self.word_to_idx:
                        pairs.append((center_idx, self.word_to_idx[sentence[j]]))
        
        return pairs
    
    def train(self, verbose=True):
        """
        完整训练流程
        """
        # 预处理语料
        if isinstance(self.corpus[0], str):
            tokenized_corpus = [self.preprocess(text) for text in self.corpus]
        else:
            tokenized_corpus = self.corpus
        
        # 构建词表
        self.build_vocab(tokenized_corpus)
        
        # 生成训练数据
        training_pairs = self.generate_training_data(tokenized_corpus)
        
        # 初始化模型
        self.model = SkipGramModel(
            len(self.vocab), self.embedding_dim,
            window_size=self.window_size,
            negative_samples=self.negative_samples
        )
        optimizer = optim.Adam(self.model.parameters(), lr=self.learning_rate)
        
        # 训练循环
        for epoch in range(self.epochs):
            random.shuffle(training_pairs)
            total_loss = 0
            
            for i in range(0, len(training_pairs), 256):  # mini-batch
                batch = training_pairs[i:i+256]
                
                # 准备批次数据
                targets = torch.tensor([p[0] for p in batch], dtype=torch.long)
                contexts = torch.tensor([p[1] for p in batch], dtype=torch.long)
                
                # 生成负样本
                neg_samples = np.random.choice(
                    len(self.vocab), size=(len(batch), self.negative_samples), 
                    replace=False
                )
                negatives = torch.tensor(neg_samples, dtype=torch.long)
                
                # 前向传播与反向传播
                optimizer.zero_grad()
                loss = self.model(targets, contexts, negatives)
                loss.backward()
                optimizer.step()
                
                total_loss += loss.item()
            
            if verbose:
                avg_loss = total_loss / len(training_pairs)
                print(f"Epoch {epoch+1}/{self.epochs}, Loss: {avg_loss:.6f}")
        
        return self.model
    
    def get_word_vector(self, word: str) -> np.ndarray:
        """获取词向量"""
        idx = self.word_to_idx.get(word)
        if idx is None:
            return None
        return self.model.target_embeddings.weight[idx].detach().numpy()
    
    def get_similarity(self, word1: str, word2: str) -> float:
        """计算两个词的相似度"""
        vec1 = self.get_word_vector(word1)
        vec2 = self.get_word_vector(word2)
        
        if vec1 is None or vec2 is None:
            return None
        
        # 余弦相似度
        norm1 = np.linalg.norm(vec1)
        norm2 = np.linalg.norm(vec2)
        return np.dot(vec1, vec2) / (norm1 * norm2)
    
    def analogy(self, a: str, b: str, c: str, top_k=5) -> List[Tuple[str, float]]:
        """
        词类比:a - b ≈ c - d, 求 d
        
        例如: king - man + woman ≈ queen
        """
        vec_a = self.get_word_vector(a)
        vec_b = self.get_word_vector(b)
        vec_c = self.get_word_vector(c)
        
        if any(v is None for v in [vec_a, vec_b, vec_c]):
            return []
        
        # 计算目标向量
        target_vec = vec_a - vec_b + vec_c
        
        # 归一化
        target_norm = target_vec / np.linalg.norm(target_vec)
        all_embeddings = self.model.target_embeddings.weight.detach().numpy()
        all_norms = np.linalg.norm(all_embeddings, axis=1, keepdims=True)
        normalized_embeddings = all_embeddings / (all_norms + 1e-8)
        
        # 计算余弦相似度
        similarities = np.dot(normalized_embeddings, target_norm)
        
        # 获取最相似的词(排除a, b, c)
        exclude = {self.word_to_idx.get(w) for w in [a, b, c] if w in self.word_to_idx}
        
        results = []
        for idx in np.argsort(similarities)[::-1]:
            if idx not in exclude:
                results.append((self.idx_to_word[idx], similarities[idx]))
                if len(results) >= top_k:
                    break
        
        return results

2.2 GloVe模型

GloVe(Global Vectors)由Jeffrey Pennington、Richard Socher和Christopher Manning于2014年在斯坦福大学提出,融合了全局矩阵分解局部上下文窗口两种方法的优点。

2.2.1 核心思想

GloVe基于全局词共现矩阵进行训练,其损失函数设计结合了:

  1. 全局统计信息:利用词共现矩阵 ,其中 表示词 在词 的上下文中出现的次数
  2. 局部上下文:保持Skip-gram的窗口概念

共现矩阵的构建

共现矩阵 是一个 的对称矩阵, 表示词 和词 在同一上下文中共同出现的次数。上下文窗口大小的选择影响共现信息的性质:

  • 小窗口:捕捉词汇-语法(lexico-syntactic)关系
  • 大窗口:捕捉词汇-语义(lexico-semantic)关系

GloVe的动机

论文标题”Global Vectors”强调了模型利用全局语料统计信息的能力。GloVe的作者认为,词的共现概率比比值能够编码语义信息:

考虑词语 的共现概率:

其中 是词 的总共现次数。

共现概率的比值可以编码词之间的关系:

例如,对于词语”ice”和”steam”:

  • 较高
  • 较低
  • 比值 较高

这表明”solid”是与”ice”相关但与”steam”无关的词语。

2.2.2 损失函数详解

GloVe的损失函数设计解决了两个问题:

  1. 处理稀有共现:稀有共现的统计不够可靠
  2. 处理高频共现:高频共现(如”the”, “is”)的信息量较低

加权损失函数

其中权重函数 定义为:

通常取

这个权重函数的设计:

  • 时,权重随共现次数增加而增加(但增速放缓)
  • 时,权重固定为1,不再增加

这种设计使得:

  • 稀有共现获得适中的权重(避免完全忽略)
  • 极高频共现的权重受限(避免主导损失函数)

GloVe的完整训练流程

import numpy as np
from scipy.sparse import csr_matrix
from collections import Counter
 
class GloVeModel:
    """
    GloVe词向量模型
    
    基于全局共现矩阵的词嵌入学习
    """
    
    def __init__(self, vocab_size, embedding_dim, x_max=100, alpha=0.75,
                 learning_rate=0.05, max_iter=100):
        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        self.x_max = x_max
        self.alpha = alpha
        self.learning_rate = learning_rate
        self.max_iter = max_iter
        
        # 词嵌入
        self.W = np.random.randn(vocab_size, embedding_dim) * 0.1
        self.W_tilde = np.random.randn(vocab_size, embedding_dim) * 0.1
        
        # 偏置
        self.b = np.zeros(vocab_size)
        self.b_tilde = np.zeros(vocab_size)
    
    def weight_function(self, x):
        """
        加权函数 f(x)
        
        公式: f(x) = (x/x_max)^alpha if x < x_max else 1
        """
        if x < self.x_max:
            return (x / self.x_max) ** self.alpha
        return 1.0
    
    def build_cooccurrence_matrix(self, corpus, window_size=5):
        """
        构建共现矩阵
        
        Args:
            corpus: 语料库 [[word1, word2, ...], ...]
            window_size: 上下文窗口大小
        """
        vocab_size = len(self)
        cooccurrence = np.zeros((vocab_size, vocab_size), dtype=np.float32)
        
        for sentence in corpus:
            for i, word_idx in enumerate(sentence):
                # 确定窗口范围
                start = max(0, i - window_size)
                end = min(len(sentence), i + window_size + 1)
                
                for j in range(start, end):
                    if i != j:
                        # 距离加权
                        distance = abs(i - j)
                        weight = 1.0 / distance
                        
                        cooccurrence[word_idx, sentence[j]] += weight
        
        return cooccurrence
    
    def fit(self, X):
        """
        训练GloVe模型
        
        Args:
            X: 共现矩阵 [vocab_size, vocab_size]
        """
        # 预先计算权重
        fX = np.vectorize(self.weight_function)(X)
        
        # 获取非零元素的索引
        non_zero_indices = np.where(X > 0)
        
        for iteration in range(self.max_iter):
            total_loss = 0
            
            for i, j in zip(non_zero_indices[0], non_zero_indices[1]):
                # 计算预测误差
                diff = (self.W[i] + self.W_tilde[j]).dot(self.W[j] + self.W_tilde[i])
                diff += self.b[i] + self.b_tilde[j] - np.log(max(X[i, j], 1e-8))
                
                # 加权误差
                weight = fX[i, j]
                weighted_diff = weight * diff
                
                # 更新权重
                lr = self.learning_rate / np.sqrt(iteration + 1)  # 学习率衰减
                
                # 更新嵌入和偏置
                self.W[i] -= lr * weighted_diff * self.W[j]
                self.W_tilde[j] -= lr * weighted_diff * self.W[i]
                self.b[i] -= lr * weighted_diff
                self.b_tilde[j] -= lr * weighted_diff
                
                total_loss += weight * diff ** 2
            
            if iteration % 10 == 0:
                print(f"Iteration {iteration}, Loss: {total_loss:.4f}")
    
    def get_embedding(self, word_idx):
        """获取词嵌入(输入和输出嵌入的平均)"""
        return (self.W[word_idx] + self.W_tilde[word_idx]) / 2
    
    def get_all_embeddings(self):
        """获取所有词嵌入"""
        return (self.W + self.W_tilde) / 2

2.2.3 GloVe与Word2Vec的深入对比

特性Word2VecGloVe
训练目标预测概率(条件)重构共现概率
语料利用局部上下文全局共现矩阵
训练速度快(在线)较慢(需要矩阵分解)
语义任务表现相似词效果好类比推理更优
语法任务表现一般优秀
参数数量~2×V×d~2×V×d + 2×V
内存需求低(流式处理)高(需存储共现矩阵)
收敛性取决于采样策略更稳定

何时选择哪个模型

  1. 选择Word2Vec的场景

    • 超大规模语料
    • 资源受限
    • 需要流式训练
    • 增量更新
  2. 选择GloVe的场景

    • 中等规模语料
    • 需要稳定的收敛性
    • 语法相关任务
    • 类比推理任务

2.3 FastText模型

FastText由Piotr Bojanowski等人于2016年在Facebook AI Research提出,核心创新是引入子词(subword)信息

2.3.1 子词嵌入原理

FastText将每个词表示为其字符n-gram的集合,这使得模型能够处理:

  1. 形态学信息:词缀、前缀、后缀的语义
  2. 未登录词(OOV):可以通过子词组合表示
  3. 稀有词:共享子词信息的词

子词生成算法

对于词”where”(n=3),生成以下子词:

特殊边界符号:
<where>  (完整词本身)

字符n-gram (n=3):
<wh, whe, her, ere, re>

注意:<和>是边界符号,确保子词不会跨越词的边界

更准确地说,每个词被表示为:

  • 词本身加上边界符号:
  • 长度为3到6的字符n-gram:<wh, whe, her, ere, re>

词嵌入计算

对于词 ,其嵌入是所有子词嵌入的平均:

其中 是词 的所有子词集合, 是子词 的嵌入。

2.3.2 FastText的优势

  1. 处理未登录词(OOV):对于词典中不存在的词,可以通过对字符n-gram求和来估计其嵌入

    def get_oov_embedding(self, word, char_ngram_min=3, char_ngram_max=6):
        """
        获取未登录词的嵌入
        
        对于OOV词,通过其字符n-gram的平均嵌入来估计
        """
        subwords = self._generate_ngrams(word, char_ngram_min, char_ngram_max)
        embeddings = []
        
        for subword in subwords:
            if subword in self.subword_to_idx:
                embeddings.append(self.subword_embeddings[subword_to_idx[subword]])
        
        if embeddings:
            return np.mean(embeddings, axis=0)
        return None
  2. 处理形态丰富语言:如德语、土耳其语、芬兰语等

    德语示例: "unwahrscheinlich" (不可能的)
    子词: <un, unw, nwä, w...>
    可以学习: un- (否定前缀), -lich (形容词后缀) 的语义
    
  3. 捕捉词缀信息:自动学习词根、词缀的语义关联

    动词形态:
    - walk, walked, walking
    - 子词共享: <wal, alk, lk> 等
    → 即使"walked"不在词典中,也可以通过子词推断
    
  4. 处理拼写变体和错误

    用户输入: "goood" (typo)
    子词: <goo, ooo, ood>
    可能与 "good" 的子词重叠
    → 可以找到正确的意图
    

2.3.3 FastText的完整实现

class FastTextModel(nn.Module):
    """
    FastText模型 - 使用子词嵌入
    
    关键改进:
    1. 子词嵌入:每个词由字符n-gram组成
    2. OOV处理:通过子词组合表示未知词
    3. 形态学习:自动捕捉词缀信息
    """
    
    def __init__(self, vocab_size, embedding_dim, ngram_min=3, ngram_max=6):
        super().__init__()
        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        self.ngram_min = ngram_min
        self.ngram_max = ngram_max
        
        # 子词嵌入(所有字符n-gram的嵌入)
        self.subword_embeddings = nn.Embedding(vocab_size, embedding_dim)
        
        # 词级别嵌入(用于完整词本身)
        self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)
        
        # 初始化
        nn.init.uniform_(self.subword_embeddings.weight, -0.1, 0.1)
        nn.init.uniform_(self.word_embeddings.weight, -0.1, 0.1)
    
    def _get_ngrams(self, word: str) -> List[str]:
        """
        获取词的字符n-gram
        
        对于 "where",n=3时:
        返回 ['<wh', 'whe', 'her', 'ere', 're>']
        """
        # 添加边界符号
        word = f"<{word}>"
        ngrams = []
        
        for n in range(self.ngram_min, self.ngram_max + 1):
            for i in range(len(word) - n + 1):
                ngrams.append(word[i:i+n])
        
        return ngrams
    
    def get_word_embedding(self, word_ids: torch.Tensor, 
                          subword_ids: torch.Tensor) -> torch.Tensor:
        """
        获取词嵌入(结合词级和子词级嵌入)
        
        Args:
            word_ids: 词ID [batch_size]
            subword_ids: 子词ID列表 [batch_size, num_subwords]
        
        Returns:
            组合后的嵌入 [batch_size, embed_dim]
        """
        # 完整词嵌入
        word_emb = self.word_embeddings(word_ids)  # [batch, dim]
        
        # 子词嵌入平均
        subword_emb = self.subword_embeddings(subword_ids)  # [batch, num_sub, dim]
        subword_avg = torch.mean(subword_emb, dim=1)        # [batch, dim]
        
        # 组合:两种嵌入的平均
        combined = (word_emb + subword_avg) / 2
        
        return combined
 
 
class FastTextClassifier(nn.Module):
    """
    基于FastText的文本分类器
    
    使用FastText的子词嵌入进行文本分类
    """
    
    def __init__(self, vocab_size, embedding_dim, num_classes,
                 ngram_min=3, ngram_max=6):
        super().__init__()
        
        self.fasttext = FastTextModel(
            vocab_size, embedding_dim, ngram_min, ngram_max
        )
        
        # 分类器
        self.classifier = nn.Sequential(
            nn.Linear(embedding_dim, 256),
            nn.ReLU(),
            nn.Dropout(0.2),
            nn.Linear(256, num_classes)
        )
    
    def forward(self, word_ids, subword_ids, mask=None):
        """
        前向传播
        
        Args:
            word_ids: [batch_size, seq_len]
            subword_ids: [batch_size, seq_len, max_subwords]
            mask: [batch_size, seq_len]
        
        Returns:
            logits: [batch_size, num_classes]
        """
        batch_size, seq_len = word_ids.shape
        
        # 获取每个词的嵌入
        word_emb = self.fasttext.get_word_embedding(
            word_ids.view(-1), subword_ids.view(batch_size * seq_len, -1)
        )
        word_emb = word_emb.view(batch_size, seq_len, -1)  # [batch, seq, dim]
        
        # 平均池化(考虑mask)
        if mask is not None:
            mask_expanded = mask.unsqueeze(-1).float()  # [batch, seq, 1]
            sum_emb = (word_emb * mask_expanded).sum(dim=1)
            count = mask_expanded.sum(dim=1).clamp(min=1)
            sentence_emb = sum_emb / count
        else:
            sentence_emb = word_emb.mean(dim=1)
        
        # 分类
        logits = self.classifier(sentence_emb)
        
        return logits
 
 
class FastTextTrainer:
    """
    FastText训练器
    
    支持子词嵌入的Skip-gram训练
    """
    
    def __init__(self, corpus, embedding_dim=100, window_size=5,
                 min_count=5, ngram_min=3, ngram_max=6):
        self.corpus = corpus
        self.embedding_dim = embedding_dim
        self.window_size = window_size
        self.min_count = min_count
        self.ngram_min = ngram_min
        self.ngram_max = ngram_max
        
        self.word_to_idx = {}
        self.idx_to_word = {}
        self.subword_to_idx = {}
        self.idx_to_subword = {}
        self.model = None
    
    def build_vocab_with_subwords(self):
        """
        构建词表和子词表
        """
        # 统计词频
        word_counts = Counter()
        for sentence in self.corpus:
            for word in sentence:
                word_counts[word] += 1
        
        # 构建词表
        vocab_words = [word for word, count in word_counts.items() 
                      if count >= self.min_count]
        
        self.word_to_idx = {w: i for i, w in enumerate(vocab_words)}
        self.idx_to_word = {i: w for w, i in self.word_to_idx.items()}
        
        # 构建子词表
        subword_counts = Counter()
        for word in vocab_words:
            word = f"<{word}>"
            for n in range(self.ngram_min, self.ngram_max + 1):
                for i in range(len(word) - n + 1):
                    subword = word[i:i+n]
                    subword_counts[subword] += 1
        
        # 过滤低频子词
        min_subword_count = 5
        subwords = [sg for sg, cnt in subword_counts.items() 
                   if cnt >= min_subword_count]
        
        self.subword_to_idx = {sg: i for i, sg in enumerate(subwords)}
        self.idx_to_subword = {i: sg for sg, i in self.subword_to_idx.items()}
        
        print(f"词表大小: {len(self.word_to_idx)}")
        print(f"子词表大小: {len(self.subword_to_idx)}")
    
    def get_word_subword_ids(self, word: str):
        """
        获取词对应的子词ID列表
        """
        word = f"<{word}>"
        subword_ids = []
        
        for n in range(self.ngram_min, self.ngram_max + 1):
            for i in range(len(word) - n + 1):
                subword = word[i:i+n]
                if subword in self.subword_to_idx:
                    subword_ids.append(self.subword_to_idx[subword])
        
        return subword_ids
    
    def train(self, epochs=5, lr=0.025, negative_samples=5):
        """
        训练FastText模型
        """
        vocab_size = len(self.word_to_idx)
        subword_vocab_size = len(self.subword_to_idx)
        
        # 初始化模型
        self.model = FastTextSkipGram(vocab_size, subword_vocab_size, 
                                       self.embedding_dim, self.ngram_min, self.ngram_max)
        optimizer = optim.Adam(self.model.parameters(), lr=lr)
        
        # 生成训练数据
        training_pairs = self._generate_pairs()
        
        for epoch in range(epochs):
            random.shuffle(training_pairs)
            total_loss = 0
            
            for center_word, context_word in training_pairs:
                # 获取中心词的子词信息
                center_idx = self.word_to_idx[center_word]
                center_subwords = self.get_word_subword_ids(center_word)
                
                # 获取上下文词
                context_idx = self.word_to_idx[context_word]
                context_subwords = self.get_word_subword_ids(context_word)
                
                # 生成负样本
                neg_indices = np.random.choice(
                    vocab_size, size=negative_samples, replace=False
                )
                
                # 训练步骤
                optimizer.zero_grad()
                loss = self.model(
                    torch.tensor([center_idx]),
                    torch.tensor([center_subwords]),
                    torch.tensor([context_idx]),
                    torch.tensor([context_subwords]),
                    torch.tensor(neg_indices)
                )
                loss.backward()
                optimizer.step()
                
                total_loss += loss.item()
            
            print(f"Epoch {epoch+1}, Loss: {total_loss/len(training_pairs):.4f}")
        
        return self.model

三、上下文词向量(Contextualized Embeddings)

3.1 传统词向量的局限性

传统词向量(如Word2Vec、GloVe、FastText)存在一个根本性的限制:每个词只有一个固定的向量表示。这导致无法处理一词多义(polysemy)问题。

一词多义的挑战

考虑词语”bank”:

"bank" 只有一个向量表示,无法区分:

场景1: 金融机构
- "I went to the bank to deposit money."
- 语义:银行、存款、金融机构

场景2: 河岸
- "We sat on the bank of the river."
- 语义:河岸、自然地理

场景3: 库存
- "a blood bank"
- 语义:存储血液的机构

传统词向量会将这些完全不同的含义平均到一个单一的向量中,导致语义模糊。

其他局限性

  1. 无法处理上下文变化:词的含义随语境变化,但向量不变
  2. 难以捕捉话语级特征:句子级别的语义无法从词级向量简单聚合
  3. 缺乏跨句依赖:无法建模句子之间的关系
  4. 静态语义:无法捕捉动态的语义变化

3.2 ELMo:双向LSTM词向量

ELMo(Embeddings from Language Models)由Matthew Peters等人于2018年提出,首次实现了上下文相关的词表示,是NLP领域的重要突破。

3.2.1 模型架构

ELMo使用双向LSTM(BiLSTM)作为编码器,分别从前向后和从后向前处理文本:

输入: The mouse ate the cheese
         ↓
字符卷积层 + Highway网络
(将字符级信息压缩为词级表示)
         ↓
前向LSTM: [The] → [mouse] → [ate] → [the] → [cheese]
后向LSTM: [cheese] ← [the] ← [ate] ← [mouse] ← [The]
         ↓
双向LSTM各层输出
(前向hidden + 后向hidden = 每层的完整表示)
         ↓
加权组合(学习得到)→ 最终表示

字符级卷积网络

ELMo使用字符卷积网络(CharCNN)来处理未登录词和形态变化:

class CharCNN(nn.Module):
    """
    字符级卷积网络
    
    输入: 字符序列 → 输出: 词嵌入
    优势: 处理未登录词和形态变化
    """
    
    def __init__(self, char_vocab_size, char_embed_dim=16, 
                 num_filters=2048, kernel_width=8):
        super().__init__()
        
        self.char_embeddings = nn.Embedding(char_vocab_size, char_embed_dim)
        
        # 多个卷积核捕捉不同长度的字符模式
        self.convs = nn.ModuleList([
            nn.Conv1d(char_embed_dim, num_filters, kernel_size=k)
            for k in range(1, kernel_width + 1)
        ])
        
        self.highway = HighwayNetwork(num_filters * kernel_width)
    
    def forward(self, char_ids):
        """
        Args:
            char_ids: [batch, seq_len, word_len]
        Returns:
            embeddings: [batch, seq_len, embed_dim]
        """
        batch_size, seq_len, word_len = char_ids.shape
        
        # [batch * seq_len, word_len, char_dim]
        char_emb = self.char_embeddings(char_ids.view(-1, word_len))
        char_emb = char_emb.transpose(1, 2)  # [N, char_dim, word_len]
        
        # 多尺度卷积
        conv_outputs = []
        for conv in self.convs:
            conv_out = torch.relu(conv(char_emb))  # [N, num_filters, *]
            pooled = torch.max(conv_out, dim=2)[0]  # 最大池化
            conv_outputs.append(pooled)
        
        # 拼接所有卷积结果
        output = torch.cat(conv_outputs, dim=1)  # [N, num_filters * kernel_width]
        
        # Highway网络
        output = self.highway(output)  # [N, embed_dim]
        
        # 恢复维度
        output = output.view(batch_size, seq_len, -1)
        
        return output
 
 
class HighwayNetwork(nn.Module):
    """
    Highway网络
    
    引入门控机制,允许信息跳过变换层
    有助于梯度流动和特征选择
    """
    
    def __init__(self, input_dim):
        super().__init__()
        self.gate = nn.Linear(input_dim, input_dim)
        self.transform = nn.Linear(input_dim, input_dim)
    
    def forward(self, x):
        gate = torch.sigmoid(self.gate(x))
        transform = torch.relu(self.transform(x))
        return gate * transform + (1 - gate) * x

3.2.2 预训练目标

ELMo通过预测下一个词来预训练双向语言模型:

前向语言模型

后向语言模型

联合目标

注意两个方向共享部分权重(字符级编码器),但LSTM层是独立的。

3.2.3 上下文表示的获取

对于每个词 ,其ELMo表示为各层表示的加权组合:

其中:

  • 是BiLSTM的层数(通常为2)
  • 表示输入层(字符CNN输出)
  • 表示第一、二层BiLSTM输出
  • 是可学习的层权重(通过Softmax归一化)
  • 是任务特定的缩放因子(可学习)
class ELMo(nn.Module):
    """
    ELMo模型
    
    关键组件:
    1. 字符CNN编码器
    2. 两层双向LSTM
    3. 可学习的层权重组合
    """
    
    def __init__(self, vocab_size, embedding_dim=512, hidden_dim=4096, 
                 num_layers=2, dropout=0.1):
        super().__init__()
        
        self.num_layers = num_layers
        
        # 字符CNN编码器
        self.char_cnn = CharCNN(char_vocab_size=262, char_embed_dim=16,
                               num_filters=embedding_dim, kernel_width=8)
        
        # 双向LSTM层
        self.lstm_layers = nn.ModuleList([
            nn.LSTM(
                input_size=embedding_dim,
                hidden_size=hidden_dim,
                num_layers=1,
                bidirectional=True,
                batch_first=True
            ) for _ in range(num_layers)
        ])
        
        # 层归一化
        self.layer_norms = nn.ModuleList([
            nn.LayerNorm(2 * hidden_dim) for _ in range(num_layers + 1)
        ])
        
        # 层权重(通过Softmax归一化)
        self.layer_weights = nn.Parameter(torch.ones(num_layers + 1))
        
        # 任务特定缩放因子
        self.gamma = nn.Parameter(torch.ones(1))
        
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, token_ids, mask=None):
        """
        Args:
            token_ids: [batch, seq_len, word_len] 字符ID
            mask: [batch, seq_len] 有效位置掩码
        
        Returns:
            elmo_embeddings: [batch, seq_len, 2 * hidden_dim]
            all_layers: 每层的表示列表
        """
        # 字符CNN编码
        char_emb = self.char_cnn(token_ids)  # [batch, seq, embed_dim]
        char_emb = self.layer_norms[0](char_emb)
        char_emb = self.dropout(char_emb)
        
        # 存储各层表示
        all_layers = [char_emb]
        hidden_states = char_emb
        
        # BiLSTM层
        for i, lstm in enumerate(self.lstm_layers):
            lstm_out, _ = lstm(hidden_states)  # [batch, seq, 2*hidden_dim]
            lstm_out = self.layer_norms[i + 1](lstm_out)
            lstm_out = self.dropout(lstm_out)
            
            all_layers.append(lstm_out)
            hidden_states = lstm_out
        
        # 层权重归一化(Softmax)
        layer_weights = torch.softmax(self.layer_weights, dim=0)
        
        # 加权组合
        elmo_emb = torch.zeros_like(hidden_states)
        for i, layer in enumerate(all_layers):
            elmo_emb += layer_weights[i] * layer
        
        # 缩放
        elmo_emb = self.gamma * elmo_emb
        
        return elmo_emb, all_layers

3.2.4 ELMo的应用方式

ELMo的上下文嵌入可以以多种方式集成到下游任务中:

方式一:替换

直接替换原来的词嵌入:

方式二:拼接

将ELMo嵌入与原有表示拼接:

方式三:加权拼接

给ELMo和原表示分配可学习的权重:

3.3 BERT:Transformer编码器

BERT(Bidirectional Encoder Representations from Transformers)由Jacob Devlin等人于2018年在谷歌提出,是NLP领域的里程碑式模型,彻底改变了预训练语言模型的发展方向。

3.3.1 核心创新

BERT的核心创新包括三个方面:

1. 双向Transformer编码器

与ELMo的浅层双向不同,BERT使用深度双向Transformer编码器,能够捕捉更复杂的长距离依赖:

ELMo:                    BERT:
前向LSTM → 后向LSTM       Transformer Layer
    ↓         ↓              ↓
  拼接       拼接         深层双向注意力
    ↓                      ↓
  各层加权组合            多层堆叠

2. 掩码语言模型(Masked Language Model, MLM)

随机mask输入中的词,让模型预测被mask的词:

输入: The [MASK] ate the cheese
目标: mouse

这与传统的语言模型不同,后者只能利用单向信息预测下一个词。

3. 下一句预测(Next Sentence Prediction, NSP)

学习句子间关系,判断句子B是否是句子A的下一句:

输入: [CLS] The man went to [MASK] store [SEP] He bought a gallon [MASK] milk [SEP]
目标: IsNext (是下一句)

这对问答、自然语言推理等任务非常重要。

3.3.2 BERT架构详解

class BERTConfig:
    """BERT配置"""
    
    def __init__(self,
                 vocab_size=30522,
                 hidden_size=768,
                 num_hidden_layers=12,
                 num_attention_heads=12,
                 intermediate_size=3072,
                 hidden_dropout=0.1,
                 attention_dropout=0.1,
                 max_position_embeddings=512,
                 type_vocab_size=2):
        self.vocab_size = vocab_size
        self.hidden_size = hidden_size
        self.num_hidden_layers = num_hidden_layers
        self.num_attention_heads = num_attention_heads
        self.intermediate_size = intermediate_size
        self.hidden_dropout = hidden_dropout
        self.attention_dropout = attention_dropout
        self.max_position_embeddings = max_position_embeddings
        self.type_vocab_size = type_vocab_size
 
 
class BERTModel(nn.Module):
    """
    BERT模型核心结构
    
    关键组件:
    1. 嵌入层:Token嵌入 + 位置嵌入 + 段落嵌入
    2. Transformer编码器层(堆叠)
    3. 池化层
    """
    
    def __init__(self, config: BERTConfig):
        super().__init__()
        self.config = config
        
        # 嵌入层
        self.embeddings = nn.ModuleDict({
            'token': nn.Embedding(config.vocab_size, config.hidden_size),
            'position': nn.Embedding(config.max_position_embeddings, config.hidden_size),
            'segment': nn.Embedding(config.type_vocab_size, config.hidden_size)
        })
        self.embed_layer_norm = nn.LayerNorm(config.hidden_size)
        self.embed_dropout = nn.Dropout(config.hidden_dropout)
        
        # Transformer编码器层
        self.encoder_layers = nn.ModuleList([
            TransformerEncoderLayer(config) 
            for _ in range(config.num_hidden_layers)
        ])
        
        # 池化层
        self.pooler = nn.Sequential(
            nn.Linear(config.hidden_size, config.hidden_size),
            nn.Tanh(),
            nn.Dropout(config.hidden_dropout)
        )
    
    def forward(self, input_ids, attention_mask=None, token_type_ids=None):
        """
        Args:
            input_ids: [batch_size, seq_len] token ID
            attention_mask: [batch_size, seq_len] 注意力掩码
            token_type_ids: [batch_size, seq_len] 段落ID
        
        Returns:
            last_hidden_state: [batch_size, seq_len, hidden_size]
            pooled_output: [batch_size, hidden_size]
        """
        batch_size, seq_len = input_ids.shape
        
        # 1. 嵌入计算
        position_ids = torch.arange(seq_len, device=input_ids.device)
        position_ids = position_ids.unsqueeze(0).expand(batch_size, -1)
        
        token_emb = self.embeddings['token'](input_ids)
        position_emb = self.embeddings['position'](position_ids)
        
        # 段落嵌入(用于区分句子A和句子B)
        if token_type_ids is None:
            token_type_ids = torch.zeros_like(input_ids)
        segment_emb = self.embeddings['segment'](token_type_ids)
        
        # 组合并归一化
        embeddings = token_emb + position_emb + segment_emb
        embeddings = self.embed_layer_norm(embeddings)
        embeddings = self.embed_dropout(embeddings)
        
        # 2. Transformer编码
        hidden_states = embeddings
        
        # 创建注意力掩码(扩展以适配多头注意力)
        if attention_mask is not None:
            # [batch, seq_len] → [batch, 1, 1, seq_len]
            extended_attention_mask = attention_mask.unsqueeze(1).unsqueeze(2)
            extended_attention_mask = extended_attention_mask.to(dtype=hidden_states.dtype)
            extended_attention_mask = (1.0 - extended_attention_mask) * -10000.0
        else:
            extended_attention_mask = None
        
        for layer in self.encoder_layers:
            hidden_states = layer(hidden_states, extended_attention_mask)
        
        # 3. 池化(取[CLS] token)
        pooled = self.pooler(hidden_states[:, 0])
        
        return {
            'last_hidden_state': hidden_states,
            'pooled_output': pooled
        }
 
 
class TransformerEncoderLayer(nn.Module):
    """
    Transformer编码器层
    
    包含:
    1. 多头自注意力
    2. 残差连接 + 层归一化
    3. 前馈网络
    4. 残差连接 + 层归一化
    """
    
    def __init__(self, config: BERTConfig):
        super().__init__()
        
        # 多头注意力
        self.attention = nn.MultiheadAttention(
            config.hidden_size, 
            config.num_attention_heads,
            dropout=config.attention_dropout,
            batch_first=True
        )
        self.attention_norm = nn.LayerNorm(config.hidden_size)
        
        # 前馈网络
        self.ffn = nn.Sequential(
            nn.Linear(config.hidden_size, config.intermediate_size),
            nn.GELU(),  # BERT使用GELU而非ReLU
            nn.Dropout(config.hidden_dropout),
            nn.Linear(config.intermediate_size, config.hidden_size),
            nn.Dropout(config.hidden_dropout)
        )
        self.ffn_norm = nn.LayerNorm(config.hidden_size)
    
    def forward(self, x, attention_mask=None):
        """
        Args:
            x: [batch, seq_len, hidden_size]
            attention_mask: [batch, 1, 1, seq_len] 扩展掩码
        
        Returns:
            hidden_states: [batch, seq_len, hidden_size]
        """
        # 自注意力 + 残差
        attn_out, _ = self.attention(
            x, x, x,
            attn_mask=attention_mask,
            key_padding_mask=(attention_mask.squeeze(1).squeeze(1) == -10000) if attention_mask is not None else None
        )
        x = self.attention_norm(x + attn_out)
        
        # 前馈网络 + 残差
        ffn_out = self.ffn(x)
        x = self.ffn_norm(x + ffn_out)
        
        return x
 
 
class BERTForMaskedLM(nn.Module):
    """
    BERT掩码语言模型
    
    用于预训练的完整模型
    """
    
    def __init__(self, config: BERTConfig):
        super().__init__()
        self.bert = BERTModel(config)
        
        # 语言模型头部
        self.lm_head = nn.Linear(config.hidden_size, config.vocab_size)
        
        # 绑定输入输出嵌入
        self.lm_head.weight = self.bert.embeddings['token'].weight
    
    def forward(self, input_ids, attention_mask=None, token_type_ids=None,
               masked_lm_labels=None):
        """
        Args:
            masked_lm_labels: [batch, seq_len] 被mask位置的标签
        
        Returns:
            loss (可选): 交叉熵损失
            logits: [batch, seq_len, vocab_size] 每个位置对每个词的打分
        """
        outputs = self.bert(input_ids, attention_mask, token_type_ids)
        sequence_output = outputs['last_hidden_state']
        
        # 计算语言模型 logits
        logits = self.lm_head(sequence_output)
        
        outputs = {'logits': logits}
        
        if masked_lm_labels is not None:
            loss_fct = nn.CrossEntropyLoss()
            # 只计算被mask位置的损失
            masked_lm_loss = loss_fct(
                logits.view(-1, self.bert.config.vocab_size),
                masked_lm_labels.view(-1)
            )
            outputs['loss'] = masked_lm_loss
        
        return outputs
 
 
class BERTForNextSentencePrediction(nn.Module):
    """
    BERT下一句预测模型
    """
    
    def __init__(self, config: BERTConfig):
        super().__init__()
        self.bert = BERTModel(config)
        
        # NSP分类器
        self.nsp_classifier = nn.Linear(config.hidden_size, 2)
        
        # 初始化
        self.apply(self._init_weights)
    
    def _init_weights(self, module):
        """权重初始化"""
        if isinstance(module, nn.Linear):
            nn.init.normal_(module.weight, std=0.02)
            if module.bias is not None:
                nn.init.zeros_(module.bias)
    
    def forward(self, input_ids, attention_mask=None, token_type_ids=None,
                next_sentence_labels=None):
        """
        Returns:
            loss (可选): NSP损失
            next_sentence_logits: [batch, 2] IsNext/NotNext分类
        """
        outputs = self.bert(input_ids, attention_mask, token_type_ids)
        
        # 使用[CLS]位置的输出
        pooled_output = outputs['pooled_output']
        
        # NSP分类
        next_sentence_logits = self.nsp_classifier(pooled_output)
        
        outputs = {'logits': next_sentence_logits}
        
        if next_sentence_labels is not None:
            loss_fct = nn.CrossEntropyLoss()
            nsp_loss = loss_fct(
                next_sentence_logits,
                next_sentence_labels
            )
            outputs['loss'] = nsp_loss
        
        return outputs

3.3.3 BERT的预训练任务详解

掩码语言模型(MLM)

BERT随机选择15%的token进行mask,但采用了一种特殊策略:

  • 80% 替换为 [MASK] token
  • 10% 替换为随机词(从词表中随机采样)
  • 10% 保持不变

这种策略的原因是:

  1. 如果只使用 [MASK],微调时模型不会看到 [MASK],造成预训练-微调不一致
  2. 加入随机词和保持不变可以让模型更好地学习表示
def mask_tokens(inputs, tokenizer, mlm_probability=0.15):
    """
    BERT式掩码处理
    
    采用80/10/10策略:
    - 80%: 替换为[MASK]
    - 10%: 替换为随机词
    - 10%: 保持不变
    
    这样可以减少预训练与微调的不一致性问题
    """
    labels = inputs.clone()
    
    # 获取特殊token掩码
    probability_matrix = torch.full(labels.shape, mlm_probability)
    special_tokens_mask = torch.tensor(
        [tokenizer.get_special_tokens_mask(val) for val in labels.tolist()]
    )
    probability_matrix.masked_fill_(special_tokens_mask.bool(), value=0.0)
    
    # 不对padding位置进行mask
    padding_mask = labels.eq(tokenizer.pad_token_id)
    probability_matrix.masked_fill_(padding_mask.bool(), value=0.0)
    
    # 随机选择要mask的位置
    masked_indices = torch.bernoulli(probability_matrix).bool()
    labels[~masked_indices] = -100  # 不计算损失的标记
    
    # 实际替换操作
    # 1. 80%替换为[MASK]
    indices_replaced = torch.bernoulli(
        torch.full(labels.shape, 0.8)
    ).bool() & masked_indices
    inputs[indices_replaced] = tokenizer.mask_token_id
    
    # 2. 10%替换为随机词
    indices_random = torch.bernoulli(
        torch.full(labels.shape, 0.5)
    ).bool() & masked_indices & ~indices_replaced
    random_words = torch.randint(
        len(tokenizer), labels.shape, dtype=torch.long
    )
    inputs[indices_random] = random_words[indices_random]
    
    # 3. 10%保持不变(上面已经处理了)
    
    return inputs, labels

下一句预测(NSP)

NSP任务帮助模型理解句子间关系:

def create_pretraining_data(sentences, tokenizer, seq_length=512, mask_prob=0.15):
    """
    创建BERT预训练数据
    
    Args:
        sentences: 句子列表
        tokenizer: BERT分词器
        seq_length: 最大序列长度
        mask_prob: mask概率
    
    Returns:
        input_ids, token_type_ids, attention_mask, masked_lm_labels, next_sentence_labels
    """
    for i in range(len(sentences) - 1):
        # 正样本:连续句子对
        tokens_a = tokenizer.encode(sentences[i])
        tokens_b = tokenizer.tokenize(sentences[i + 1])
        is_next = 1
        
        # 50%概率使用负样本
        if random.random() < 0.5:
            # 负样本:随机句子对
            tokens_b = tokenizer.tokenize(random.choice(sentences))
            is_next = 0
        
        # 组合句子(添加[CLS], [SEP], [SEP])
        # 格式: [CLS] sentence A [SEP] sentence B [SEP]
        input_ids = [tokenizer.cls_token_id] + tokens_a + [tokenizer.sep_token_id] + tokens_b + [tokenizer.sep_token_id]
        
        # 段落ID(用于区分句子A和B)
        token_type_ids = [0] * (len(tokens_a) + 2) + [1] * (len(tokens_b) + 1)
        
        # 截断或填充到固定长度
        if len(input_ids) > seq_length:
            input_ids = input_ids[:seq_length]
            token_type_ids = token_type_ids[:seq_length]
        else:
            padding = [tokenizer.pad_token_id] * (seq_length - len(input_ids))
            input_ids.extend(padding)
            token_type_ids.extend([0] * len(padding))
        
        # 应用MLM
        input_ids, masked_lm_labels = mask_tokens(
            torch.tensor(input_ids), tokenizer, mask_prob
        )
        
        # 注意力掩码
        attention_mask = (input_ids != tokenizer.pad_token_id).long()
        
        yield {
            'input_ids': input_ids,
            'token_type_ids': torch.tensor(token_type_ids),
            'attention_mask': attention_mask,
            'masked_lm_labels': masked_lm_labels,
            'next_sentence_label': torch.tensor(is_next)
        }

3.3.4 BERT的变体与发展

RoBERTa(Robustly Optimized BERT)

Facebook AI对BERT的优化版本,主要改进:

  1. 更多训练数据和更长时间
  2. 移除NSP任务(实验证明无帮助)
  3. 更大的batch size
  4. 动态masking
  5. 更长的训练序列

ALBERT(A Lite BERT)

参数共享的轻量级BERT:

  1. 跨层参数共享:所有Transformer层共享参数
  2. 因子化嵌入:将大词嵌入分解为两个小矩阵
  3. 句序预测(SOP):比NSP更有效的下一句任务

DistilBERT

知识蒸馏的BERT简化版本:

  1. 保留85%性能
  2. 参数减少40%
  3. 推理速度提升60%

ERNIE、XLNet、ELECTRA等

各有特色的预训练模型,在不同任务上展现出优势。


四、Embedding空间的几何性质

4.1 语义空间的数学结构

词向量空间具有丰富的几何结构,这些结构编码了语言学规律。

4.1.1 线性关系

词向量空间中存在系统性的线性关系,这种现象被称为”语义合成的加性”:

Mikolov等人发现这种关系广泛存在:

关系类型示例向量运算
国家-首都Paris - France + Italy ≈ Rome地名+国名运算
动词时态walk - walked + think ≈ thought动词形态变化
复数形式apple - apples + car ≈ cars单复数转换
形容词关系big - bigger + small ≈ smaller形容词比较级
公司-CEOApple - Jobs + Google ≈ Pichai组织-领导关系
货币-国家Dollar - USA + Japan ≈ Yen货币-国家关系

线性可加性的数学解释

这种线性可加性的根源在于,词的语义可以分解为多个语义特征的线性组合:

其中 表示语义特征向量, 是权重。

例如,“king”的语义可以分解为:

而”queen”的语义可以分解为:

两者的差异主要在于性别特征,因此:

这解释了为什么向量差能够编码语义关系。

4.1.2 语义距离与相似度度量

词向量空间中的距离度量有多种选择:

度量方法公式特性适用场景
余弦相似度方向相似性,对长度不敏感语义相似度
欧氏距离$\\mathbf{a} - \mathbf{b}\_2$
曼哈顿距离稀疏向量友好高维稀疏数据
内积非归一化相似度排序、推荐
JS散度概率分布距离概率建模
Wasserstein距离最优传输距离分布比较
def compute_similarity(vecs1, vecs2, method='cosine'):
    """
    词向量相似度计算
    
    Args:
        vecs1: [n, d] 或 [d]
        vecs2: [m, d] 或 [d]
        method: 'cosine', 'euclidean', 'manhattan', 'dot'
    
    Returns:
        相似度/距离矩阵
    """
    if len(vecs1.shape) == 1:
        vecs1 = vecs1.unsqueeze(0)
    if len(vecs2.shape) == 1:
        vecs2 = vecs2.unsqueeze(0)
    
    if method == 'cosine':
        # 余弦相似度
        norm1 = vecs1 / vecs1.norm(dim=1, keepdim=True)
        norm2 = vecs2 / vecs2.norm(dim=1, keepdim=True)
        return torch.mm(norm1, norm2.T)
    
    elif method == 'euclidean':
        # 欧氏距离(转换为相似度)
        distances = torch.cdist(vecs1, vecs2, p=2)
        return 1 / (1 + distances)
    
    elif method == 'manhattan':
        # 曼哈顿距离
        distances = torch.cdist(vecs1, vecs2, p=1)
        return 1 / (1 + distances)
    
    elif method == 'dot':
        # 内积(未归一化)
        return torch.mm(vecs1, vecs2.T)
 
 
class WordVectorAnalyzer:
    """
    词向量分析工具
    
    提供多种几何分析功能
    """
    
    def __init__(self, embeddings, word_to_idx, idx_to_word):
        self.embeddings = embeddings  # [vocab_size, embed_dim]
        self.word_to_idx = word_to_idx
        self.idx_to_word = idx_to_word
        self.vocab_size, self.embed_dim = embeddings.shape
    
    def find_similar_words(self, word, top_k=10):
        """找到最相似的词"""
        if word not in self.word_to_idx:
            return []
        
        idx = self.word_to_idx[word]
        word_vec = self.embeddings[idx]
        
        # 计算余弦相似度
        similarities = self._cosine_similarity(word_vec, self.embeddings)
        
        # 排序并返回top k
        top_indices = similarities.argsort(descending=True)[1:top_k+1]
        
        return [(self.idx_to_word[idx], similarities[idx].item()) 
                for idx in top_indices]
    
    def analogy(self, a, b, c, top_k=5):
        """
        词类比: a - b ≈ c - d
        
        例: king - man + woman ≈ queen
        """
        if any(w not in self.word_to_idx for w in [a, b, c]):
            return []
        
        vec_a = self.embeddings[self.word_to_idx[a]]
        vec_b = self.embeddings[self.word_to_idx[b]]
        vec_c = self.embeddings[self.word_to_idx[c]]
        
        # 计算目标向量
        target = vec_a - vec_b + vec_c
        
        # 计算相似度
        similarities = self._cosine_similarity(target, self.embeddings)
        
        # 排除输入词
        exclude = {self.word_to_idx[w] for w in [a, b, c]}
        
        results = []
        for idx in similarities.argsort(descending=True):
            if idx not in exclude:
                results.append((self.idx_to_word[idx], similarities[idx].item()))
                if len(results) >= top_k:
                    break
        
        return results
    
    def clustering(self, words=None, n_clusters=10):
        """对词进行聚类分析"""
        from sklearn.cluster import KMeans
        
        if words is None:
            vecs = self.embeddings
            labels = list(self.idx_to_word.values())
        else:
            indices = [self.word_to_idx[w] for w in words if w in self.word_to_idx]
            vecs = self.embeddings[indices]
            labels = [self.idx_to_word[i] for i in indices]
        
        kmeans = KMeans(n_clusters=min(n_clusters, len(labels)))
        clusters = kmeans.fit_predict(vecs)
        
        result = {}
        for label, cluster_id in zip(labels, clusters):
            if cluster_id not in result:
                result[cluster_id] = []
            result[cluster_id].append(label)
        
        return result
    
    def project_to_2d(self, words=None, method='pca'):
        """降维可视化"""
        from sklearn.decomposition import PCA
        from sklearn.manifold import TSNE
        
        if words is None:
            vecs = self.embeddings
            labels = list(self.idx_to_word.values())
        else:
            indices = [self.word_to_idx[w] for w in words if w in self.word_to_idx]
            vecs = self.embeddings[indices]
            labels = [self.idx_to_word[i] for i in indices]
        
        if method == 'pca':
            projector = PCA(n_components=2)
        else:
            projector = TSNE(n_components=2)
        
        coords = projector.fit_transform(vecs)
        
        return coords, labels
    
    def _cosine_similarity(self, vec, vecs):
        """计算单个向量与一组向量的余弦相似度"""
        norm_vec = vec / (vec.norm() + 1e-8)
        norm_vecs = vecs / (vecs.norm(dim=1, keepdim=True) + 1e-8)
        return torch.mm(norm_vec.unsqueeze(0), norm_vecs.T).squeeze(0)

4.2 语义类别的空间分布

4.2.1 聚类结构

词向量空间中的语义类别呈现聚类分布,这是分布式语义假设的直接体现:

Embedding空间示意图(2D降维可视化)

                    [水果类]
                     🍎 🍊 🍋 🍇 🍓
                   [动物类]
                 🐕 🐈 🐇 🐁 🐘 🦁 🐯
              [颜色类]
             🔴 🔵 🟢 🟡 🟣 🟠
    [数字类]                    [动作类]
      1 2 3 4 5 10              跑 跳 飞 游 走 爬
         [职业类]           [情感类]
           医生老师律师         喜怒哀乐悲恐

这种聚类结构具有以下特点:

  1. 类别内聚性:同类词聚集在局部区域
  2. 类别间分离性:不同类别的词相对分散
  3. 层级结构:存在多层级的聚类层次
  4. 边界模糊性:类别边界不是清晰的,而是渐变的

4.2.2 类别边界的模糊性与原型效应

语义类别之间的边界往往是模糊的,这反映了语义原型理论(Prototype Theory):

# 使用GMM建模语义类别的概率分布
from sklearn.mixture import GaussianMixture
 
def model_semantic_categories(word_embeddings, category_labels, n_components=3):
    """
    使用高斯混合模型建模语义类别
    
    假设每个语义类别不是严格的聚类,
    而是服从高斯分布的概率分布
    这与原型理论一致:类别成员有典型程度之分
    """
    categories = set(category_labels)
    category_models = {}
    category_centers = {}
    
    for cat in categories:
        mask = np.array(category_labels) == cat
        cat_embeddings = word_embeddings[mask]
        
        # 每个类别用多个高斯建模(捕捉子类变体)
        n_comp = min(n_components, len(cat_embeddings))
        gmm = GaussianMixture(n_components=n_comp, covariance_type='full')
        gmm.fit(cat_embeddings)
        category_models[cat] = gmm
        
        # 计算类别中心
        category_centers[cat] = cat_embeddings.mean(axis=0)
    
    return category_models, category_centers
 
 
def compute_prototypicality(word_embeddings, category_members):
    """
    计算词的原型性
    
    原型词 = 距离类别中心最近的词
    原型性 = 1 / (1 + 距离)
    """
    category_center = np.mean(word_embeddings[category_members], axis=0)
    
    distances = []
    for word_idx in category_members:
        dist = cosine_distance(word_embeddings[word_idx], category_center)
        distances.append((word_idx, dist))
    
    # 按距离排序
    distances.sort(key=lambda x: x[1])
    
    # 计算原型性分数
    prototypicality = {}
    for i, (word_idx, dist) in enumerate(distances):
        # 越近的词原型性越高
        prototypicality[word_idx] = 1 / (1 + dist)
    
    return prototypicality

原型效应的词向量证据

实验表明,词向量空间确实表现出原型效应:

def demonstrate_prototype_effect():
    """
    演示词向量中的原型效应
    
    假设:
    - "苹果"在水果类中比"牛油果"更接近中心
    - "知更鸟"在鸟类中比"企鹅"更接近中心
    """
    # 水果类别示例
    fruit_words = ['苹果', '香蕉', '橙子', '葡萄', '西瓜', '草莓', '蓝莓', '牛油果', '猕猴桃']
    fruit_center = average_embeddings(fruit_words)
    
    # 计算每个水果到中心的距离
    fruit_distances = {}
    for fruit in fruit_words:
        emb = get_embedding(fruit)
        dist = cosine_distance(emb, fruit_center)
        fruit_distances[fruit] = dist
    
    # 按距离排序
    sorted_fruits = sorted(fruit_distances.items(), key=lambda x: x[1])
    
    print("水果类别原型性排序(越近越典型):")
    for fruit, dist in sorted_fruits:
        print(f"  {fruit}: {dist:.4f}")
    
    # 预期结果:
    # 苹果、香蕉、橙子等典型水果距离较近
    # 牛油果、猕猴桃等非常见水果距离较远

4.3 语义空间的异常现象与挑战

4.3.1 性别偏差

词向量中系统性存在的性别偏见是一个重要问题:

职业词的性别向量投影:

                    男性方向
                        ↑
            医生  ──────┼──────  护士
                        │
           工程师 ──────┼──────  保育员
                        │
           CEO ────────┼──────  行政助理
                        │
                    女性方向

偏见的来源

词向量中的性别偏见主要来源于训练语料中的统计偏差:

  • 语料中”医生”更多与”他”共现
  • “护士”更多与”她”共现
  • 这种统计偏差被词向量编码

偏见校正方法

def debias_embeddings(embeddings, definitional_pairs, equalize_pairs):
    """
    Hard Debias算法(Bolukbasi et al., 2016)
    
    步骤:
    1. 计算性别方向向量
    2. 对中性词去偏
    3. 均衡化性别词
    
    Args:
        embeddings: 词嵌入矩阵
        definitional_pairs: 定义性词对 [(男人词, 女人词), ...]
        equalize_pairs: 需要均衡化的词对 [(职业男, 职业女), ...]
    """
    # 1. 计算性别方向
    gender_direction = compute_gender_direction(embeddings, definitional_pairs)
    
    # 归一化性别方向
    gender_direction = gender_direction / np.linalg.norm(gender_direction)
    
    # 2. 中性词去偏
    neutral_words = identify_neutral_words(embeddings, gender_direction)
    
    for word, idx in neutral_words.items():
        v = embeddings[idx]
        # 移除性别分量
        v_bias = (v @ gender_direction) * gender_direction
        embeddings[idx] = v - v_bias
    
    # 3. 均衡化性别词对
    for (word1, word2) in equalize_pairs:
        v1, v2 = embeddings[word1], embeddings[word2]
        
        # 计算两词的中点
        v_mean = (v1 + v2) / 2
        
        # 将两词投影到与性别方向正交的子空间
        v1_bias = (v1 @ gender_direction) * gender_direction
        v2_bias = (v2 @ gender_direction) * gender_direction
        
        # 均衡化后的向量
        embeddings[word1] = v_mean + (v1 - v_mean - v1_bias)
        embeddings[word2] = v_mean + (v2 - v_mean - v2_bias)
 
 
def compute_gender_direction(embeddings, definitional_pairs):
    """
    从定义性词对计算性别方向向量
    
    定义性词对:如 he-she, king-queen, man-woman
    这些词的差异向量编码了性别方向
    """
    directions = []
    
    for word1, word2 in definitional_pairs:
        if word1 in embeddings and word2 in embeddings:
            # 差向量编码性别差异
            direction = embeddings[word1] - embeddings[word2]
            directions.append(direction)
    
    # 使用主成分分析获取主要性别方向
    directions_matrix = np.array(directions)
    
    # 第一主成分作为性别方向
    from sklearn.decomposition import PCA
    pca = PCA(n_components=1)
    gender_direction = pca.fit_transform(directions_matrix).flatten()
    
    return gender_direction
 
 
def soft_debias(embeddings, gender_direction, gender_words):
    """
    软去偏方法
    
    不是完全移除性别分量,而是削弱其影响
    """
    for word, idx in gender_words.items():
        v = embeddings[idx]
        
        # 计算当前词在性别方向上的投影
        projection = (v @ gender_direction) * gender_direction
        
        # 软化:乘以一个小于1的系数
        embeddings[idx] = v - 0.5 * projection
    
    return embeddings

4.3.2 其他偏差问题

除了性别偏差,词向量还可能编码其他类型的偏见:

偏差类型示例来源影响
种族偏差名字与职业的关联语料统计歧视性应用
年龄偏差年龄与能力的关联语料统计年龄歧视
文化偏差特定文化的概念权重训练语料文化中心主义
语言偏差多语言中的语义差异翻译语料语义漂移

五、与认知科学的联系

5.1 分布式表征与人类语义记忆

词向量的分布式表征假说与认知科学中的分布式记忆理论高度一致。

5.1.1 特征理论 vs. 分布式表征

经典特征理论(Feature Theory)

20世纪70-80年代的语义学研究提出了特征理论:

  • 词义由一组二元或数值特征定义
  • 特征如:[+ANIMATE, +HUMAN, +MALE, +ADULT]
  • 概念是这些特征值的组合
"男人" 的特征表示:
[+ANIMATE, +HUMAN, +MALE, +ADULT, -MARRIED?]

"女人" 的特征表示:
[+ANIMATE, +HUMAN, -MALE, +ADULT, -MARRIED?]

问题:

  1. 无法解释语义模糊性和语境依赖
  2. 特征边界不清晰(如”中年”是+ADULT还是-YOUNG?)
  3. 无法捕捉特征的重要性差异

分布式表征理论(Distributed Representation)

与特征理论不同,分布式表征:

  • 词义由整个向量空间的模式定义
  • 每个维度可能对应多个相关特征的加权组合
  • 更好地解释语义泛化和原型效应
"男人" 的分布式表示:
[0.92, 0.87, 0.95, 0.78, 0.23, ..., 0.45]
 (语义向量的前几个维度)

5.1.2 语义网络的神经基础

人类语义记忆的神经表征

                 前颞叶 (ATL)
              ┌──────────────┐
              │              │
              │    语义知识   │
              │   (分布式存储) │
              │              │
              └──────────────┘
         梭状回        角回/缘上回
           ↓            ↓
    ┌──────┴────────────┴──────┐
    │                           │
    │      语义处理网络         │
    │   (词向量空间类比)        │
    │                           │
    │  前下额叶皮层 ←→ 后颞叶   │
    │      ↑           ↑        │
    │   词汇检索    视觉识别     │
    └───────────────────────────┘

研究表明,语义知识在前颞叶(ATL)区域以分布式方式存储:

  • ATL是语义知识的”枢纽”
  • 受损会导致语义痴呆(semantic dementia)
  • 词向量空间的分布式特性与这一神经发现高度一致

5.2 语义启动效应

语义启动效应(Semantic Priming)现象与词向量的语义相似度计算密切相关:

实验范式

启动词呈现: "医生" (prime)
      ↓ (短暂间隔,100-300ms)
目标词呈现: "医院" vs "面包"
      ↓
测量反应时和准确率

结果: 
- "医生-医院" 配对 → 反应更快、更准确
- "医生-面包" 配对 → 反应较慢、较低准确率

向量空间解释

def simulate_priming(prime_embedding, target_embedding, distractor_embedding):
    """
    模拟语义启动效应
    
    假设:语义相关词对的处理更快
    因为它们在语义空间中距离更近
    """
    prime_target_distance = cosine_distance(prime_embedding, target_embedding)
    prime_distractor_distance = cosine_distance(prime_embedding, distractor_embedding)
    
    # 相关词对距离更近 → 处理更快
    faster_for_target = prime_target_distance < prime_distractor_distance
    
    # 计算启动效应大小
    priming_effect = prime_distractor_distance - prime_target_distance
    
    return {
        'faster_for_target': faster_for_target,
        'priming_effect': priming_effect,
        'prime_target_distance': prime_target_distance,
        'prime_distractor_distance': prime_distractor_distance
    }
 
 
def predict_reaction_time(embedding_distance):
    """
    预测基于语义距离的反应时
    
    使用简单的线性模型:
    RT = base_RT + slope * distance
    """
    base_RT = 300  # ms
    slope = 20     # ms/单位距离
    
    predicted_RT = base_RT + slope * embedding_distance
    return predicted_RT

5.3 原型效应与最佳示例

人类在分类时表现出原型效应(Prototype Effect):某些成员比其他人更能代表类别。

词向量空间中也存在类似现象:

def compute_prototypicality(word_embeddings, category_members):
    """
    计算词的原型性
    
    原型词 = 距离类别中心最近的词
    
    认知科学中的原型效应:
    - "水果"类别中,苹果比牛油果更典型
    - "鸟"类别中,知更鸟比企鹅更典型
    - "家具"类别中,椅子比脚凳更典型
    """
    # 计算类别中心
    category_embeddings = word_embeddings[category_members]
    category_center = np.mean(category_embeddings, axis=0)
    
    # 计算每个词到中心的距离
    distances = cosine_similarity(category_embeddings, category_center.reshape(1, -1))
    distances = distances.flatten()
    
    # 原型性 = 1 / (1 + 距离)
    prototypicality_scores = 1 / (1 + distances)
    
    # 排序
    sorted_indices = np.argsort(prototypicality_scores)[::-1]
    
    results = []
    for idx in sorted_indices:
        word_idx = category_members[idx]
        results.append({
            'word_idx': word_idx,
            'prototypicality': prototypicality_scores[idx],
            'distance_to_center': distances[idx]
        })
    
    return results
 
 
def find_category_boundaries(word_embeddings, category_labels):
    """
    寻找类别边界
    
    类别边界处的词通常更难分类
    这与认知科学中的"边界效应"一致
    """
    unique_categories = set(category_labels)
    boundary_words = []
    
    for i, cat1 in enumerate(unique_categories):
        for cat2 in list(unique_categories)[i+1:]:
            # 找到两类之间距离最近的词对
            cat1_indices = [j for j, c in enumerate(category_labels) if c == cat1]
            cat2_indices = [j for j, c in enumerate(category_labels) if c == cat2]
            
            # 计算所有跨类别词对的距离
            min_distance = float('inf')
            boundary_pair = None
            
            for idx1 in cat1_indices:
                for idx2 in cat2_indices:
                    dist = cosine_distance(word_embeddings[idx1], word_embeddings[idx2])
                    if dist < min_distance:
                        min_distance = dist
                        boundary_pair = (idx1, idx2)
            
            if boundary_pair:
                boundary_words.append({
                    'category_pair': (cat1, cat2),
                    'word_pair': boundary_pair,
                    'distance': min_distance
                })
    
    return boundary_words

5.4 概念组合与语义合成

人类能够灵活组合概念生成新意义。词向量的组合性研究探索这一能力:

组合操作的数学表达

简单组合(向量加法):

加法组合捕捉了概念的叠加关系,但对于修饰关系(如”红苹果”)效果有限。

属性组合(Hadamard积/逐元素乘法):

乘法操作可以更好地捕捉属性修饰关系:

  • 两个向量的Hadamard积保留了共同激活的维度
  • 消去了不相关维度

复杂组合(神经网络组合器):

class NeuralComposition(nn.Module):
    """
    神经网络语义组合器
    
    比简单线性组合更能处理复杂语义组合
    
    例如:
    - "红苹果":苹果 + 红色属性
    - "法国国王":国王 + 法国属性 + 隐含的统治者
    """
    
    def __init__(self, embed_dim, hidden_dim):
        super().__init__()
        
        # 组合网络
        self.f = nn.Sequential(
            nn.Linear(embed_dim * 2, hidden_dim),
            nn.ReLU(),
            nn.Dropout(0.1),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, embed_dim)
        )
        
        # 门控机制(学习何时信任组合)
        self.gate = nn.Sequential(
            nn.Linear(embed_dim * 2, embed_dim),
            nn.Sigmoid()
        )
    
    def forward(self, head, modifier):
        """
        组合两个词的语义表示
        
        Args:
            head: 被修饰词 [batch, embed_dim]
            modifier: 修饰词 [batch, embed_dim]
        
        Returns:
            组合表示 [batch, embed_dim]
        """
        # 拼接输入
        combined = torch.cat([head, modifier], dim=-1)
        
        # 学习组合变换
        transformation = self.f(combined)
        
        # 门控:决定组合的程度
        gate_values = self.gate(combined)
        
        # 加权组合
        result = gate_values * transformation + (1 - gate_values) * head
        
        return result
 
 
class DeepSemanticCompositor(nn.Module):
    """
    深度语义组合器
    
    使用Transformer架构进行复杂语义组合
    适用于多词组合、短语和句子表示
    """
    
    def __init__(self, embed_dim, num_heads=4, num_layers=2):
        super().__init__()
        
        # 使用自注意力进行组合
        self.attention = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
        
        # 层归一化
        self.norm1 = nn.LayerNorm(embed_dim)
        self.norm2 = nn.LayerNorm(embed_dim)
        
        # 前馈网络
        self.ffn = nn.Sequential(
            nn.Linear(embed_dim, embed_dim * 4),
            nn.ReLU(),
            nn.Dropout(0.1),
            nn.Linear(embed_dim * 4, embed_dim)
        )
    
    def forward(self, word_embeddings):
        """
        组合多个词的嵌入
        
        Args:
            word_embeddings: [batch, seq_len, embed_dim]
        
        Returns:
            组合表示 [batch, embed_dim]
        """
        # 自注意力组合
        attn_out, _ = self.attention(word_embeddings, word_embeddings, word_embeddings)
        x = self.norm1(word_embeddings + attn_out)
        
        # 前馈组合
        ffn_out = self.ffn(x)
        x = self.norm2(x + ffn_out)
        
        # 平均池化得到整体表示
        composed = x.mean(dim=1)
        
        return composed

六、现代预训练语言模型

6.1 GPT系列:生成式预训练

GPT(Generative Pre-trained Transformer)系列是OpenAI开发的重要语言模型。

GPT-1 (2018)

首次提出”预训练+微调”范式:

  • 无监督预训练:预测下一个词
  • 有监督微调:适应具体任务

GPT-2 (2019)

  • 扩大模型规模(15亿参数)
  • 提出”语言模型是无监督多任务学习器”
  • 在多种任务上无需微调即可取得不错效果

GPT-3 (2020)

  • 1750亿参数
  • 上下文学习(In-context Learning):通过提示完成任务
  • 展示了大规模语言模型的涌现能力

GPT-4及以后

  • 多模态能力
  • 更强的推理能力
  • 更好的指令遵循

6.2 T5与序列到序列范式

T5(Text-to-Text Transfer Transformer)将所有NLP任务统一为文本到文本的格式:

所有任务都是: 输入文本 → 输出文本

翻译: "Hello" → "你好"
摘要: "长文本..." → "短文本"
问答: "问题? 上下文..." → "答案"
分类: "文本" → "标签"

6.3 大型多语言模型

mBERT / XLM-R

  • 多语言预训练
  • 跨语言迁移能力
  • 支持100+语言

BLOOM

  • 1760亿参数
  • 多语言开源模型
  • 46种语言

七、实践应用与工具

7.1 主流词向量工具对比

工具语言特点预训练模型适用场景
gensimPythonWord2Vec/FastText实现多语言快速实验
spaCyPython集成词向量en_core_web_md生产环境
TensorFlow Hub多语言Universal Sentence Encoder17种语言句子嵌入
HuggingFacePythonBERT系列模型100+模型预训练模型
fastTextC++/Python快速子词嵌入157种语言多语言
** flair**Python上下文嵌入多语言NER等序列标注

7.2 词向量可视化工具

# 使用t-SNE/UMAP可视化词向量
from sklearn.manifold import TSNE
import plotly.express as px
import pandas as pd
 
def visualize_embeddings(word_embeddings, words, labels=None, method='tsne'):
    """
    词向量可视化
    
    Args:
        word_embeddings: [n_words, embed_dim]
        words: 词列表
        labels: 类别标签
        method: 'tsne' 或 'umap'
    """
    # 降维
    if method == 'tsne':
        reducer = TSNE(n_components=2, random_state=42, perplexity=min(30, len(words)-1))
    else:
        import umap
        reducer = umap.UMAP(n_components=2, random_state=42)
    
    coords = reducer.fit_transform(word_embeddings)
    
    # 创建DataFrame
    df = pd.DataFrame({
        'x': coords[:, 0],
        'y': coords[:, 1],
        'word': words,
        'label': labels or ['unknown'] * len(words)
    })
    
    # 绘制交互式散点图
    fig = px.scatter(df, x='x', y='y', text='word', color='label',
                     title=f'Word Embeddings Visualization ({method.upper()})')
    fig.update_traces(textposition='top center', textfont_size=10)
    fig.update_layout(showlegend=True)
    
    return fig
 
 
def visualize_word_analogies(embeddings, word_pairs, word_to_idx):
    """
    可视化词类比关系
    
    展示 a - b ≈ c - d 的线性关系
    """
    fig = go.Figure()
    
    for (a, b, c, d) in word_pairs:
        if all(w in word_to_idx for w in [a, b, c, d]):
            vec_a = embeddings[word_to_idx[a]]
            vec_b = embeddings[word_to_idx[b]]
            vec_c = embeddings[word_to_idx[c]]
            vec_d = embeddings[word_to_idx[d]]
            
            # 绘制向量箭头
            # a → b (实际关系)
            # c → d (类比关系)
            
            # 使用PCA降维到2D
            all_vecs = np.array([vec_a, vec_b, vec_c, vec_d])
            coords = PCA(n_components=2).fit_transform(all_vecs)
            
            fig.add_trace(go.Scatter(
                x=[coords[0][0], coords[1][0]],
                y=[coords[0][1], coords[1][1]],
                mode='lines+markers+text',
                name=f'{a} - {b}',
                text=[a, b],
                textposition='top center'
            ))
            
            fig.add_trace(go.Scatter(
                x=[coords[2][0], coords[3][0]],
                y=[coords[2][1], coords[3][1]],
                mode='lines+markers+text',
                name=f'{c} - {d}',
                text=[c, d],
                textposition='top center'
            ))
    
    fig.show()
 
 
def create_embedding_atlas(embeddings, words, clusters, output_path):
    """
    创建词嵌入图谱
    
    将词聚类结果可视化
    """
    from sklearn.decomposition import PCA
    
    # 降维
    coords = PCA(n_components=2).fit_transform(embeddings)
    
    # 分配颜色
    unique_clusters = list(set(clusters))
    colors = px.colors.qualitative.Set3[:len(unique_clusters)]
    cluster_to_color = {c: colors[i % len(colors)] for i, c in enumerate(unique_clusters)}
    
    # 创建图像
    fig = go.Figure()
    
    for cluster_id in unique_clusters:
        mask = np.array(clusters) == cluster_id
        cluster_words = [w for w, m in zip(words, mask) if m]
        cluster_coords = coords[mask]
        
        fig.add_trace(go.Scatter(
            x=cluster_coords[:, 0],
            y=cluster_coords[:, 1],
            mode='markers+text',
            marker=dict(size=10, color=cluster_to_color[cluster_id]),
            text=cluster_words,
            textposition='top center',
            name=f'Cluster {cluster_id}'
        ))
    
    fig.update_layout(
        title='Word Embedding Atlas',
        showlegend=True,
        width=1200,
        height=800
    )
    
    fig.write_html(output_path)

八、最佳实践与调优指南

8.1 词向量选择指南

任务类型与模型选择

任务类型推荐模型理由
词语相似度Word2Vec, GloVe, FastText词级语义
词语类比Word2Vec, GloVe线性关系保持好
句子相似度Sentence-BERT, Universal Sentence Encoder句子级表示
情感分析BERT, RoBERTa上下文敏感
命名实体识别BERT, BiLSTM+ELMo序列标注
机器翻译Transformer序列生成
问答系统T5, BART生成式

8.2 超参数调优

class Word2VecTuner:
    """
    Word2Vec超参数调优器
    
    关键超参数:
    1. embedding_dim: 嵌入维度
    2. window_size: 上下文窗口大小
    3. min_count: 最小词频阈值
    4. negative_samples: 负采样数量
    5. subsampling: 高频词降采样率
    """
    
    def __init__(self, corpus, eval_tasks):
        self.corpus = corpus
        self.eval_tasks = eval_tasks  # 评估任务
    
    def tune(self, param_grid):
        """
        网格搜索调优
        """
        results = []
        
        for params in self._generate_param_combinations(param_grid):
            # 训练模型
            model = Word2VecTrainer(
                self.corpus,
                embedding_dim=params['embedding_dim'],
                window_size=params['window_size'],
                min_count=params['min_count'],
                negative_samples=params['negative_samples']
            )
            model.train(verbose=False)
            
            # 评估
            scores = self._evaluate(model)
            
            results.append({
                'params': params,
                'scores': scores
            })
            
            print(f"Params: {params}")
            print(f"Scores: {scores}")
        
        # 返回最佳配置
        best = max(results, key=lambda x: x['scores']['average'])
        return best
    
    def _generate_param_combinations(self, param_grid):
        """生成参数组合"""
        keys = param_grid.keys()
        values = param_grid.values()
        
        for combo in itertools.product(*values):
            yield dict(zip(keys, combo))

8.3 生产环境部署

class WordVectorServer:
    """
    词向量服务
    
    用于生产环境的词向量API
    """
    
    def __init__(self, model_path):
        self.model = self._load_model(model_path)
        self.embeddings = self.model.wv
        self.word_to_idx = {w: i for i, w in enumerate(self.embeddings.index_to_key)}
    
    def get_embedding(self, word):
        """获取单个词的嵌入"""
        if word in self.embeddings:
            return self.embeddings[word].tolist()
        return None
    
    def get_batch_embeddings(self, words):
        """批量获取词嵌入"""
        embeddings = []
        for word in words:
            emb = self.get_embedding(word)
            embeddings.append(emb if emb else [0.0] * self.embeddings.vector_size)
        return embeddings
    
    def find_similar(self, word, top_k=10):
        """找到最相似的词"""
        if word not in self.embeddings:
            return []
        return self.embeddings.most_similar(word, topn=top_k)
    
    def analogy(self, a, b, c, top_k=5):
        """词类比"""
        try:
            return self.embeddings.most_similar(positive=[a, c], negative=[b], topn=top_k)
        except KeyError:
            return []

参考文献与推荐阅读

  1. 经典论文

    • Mikolov, T., et al. (2013). Distributed representations of words and phrases and their compositionality. NeurIPS.
    • Mikolov, T., et al. (2013). Efficient estimation of word representations in vector space. ICLR Workshop.
    • Pennington, J., Socher, R., & Manning, C. D. (2014). GloVe: Global vectors for word representation. EMNLP.
    • Bojanowski, P., et al. (2017). Enriching word vectors with subword information. TACL, 5, 135-146.
    • Peters, M. E., et al. (2018). Deep contextualized word representations. NAACL-HLT.
    • Devlin, J., et al. (2019). BERT: Pre-training of deep bidirectional transformers for language understanding. NAACL-HLT.
    • Rogers, A., et al. (2020). A primer in BERTology: What we know about how BERT works. TACL, 8, 842-866.
  2. 进阶阅读

    • Levy, O., & Goldberg, Y. (2014). Neural Word Embedding as Implicit Matrix Factorization. NeurIPS.
    • Schnabel, T., et al. (2015). Evaluation methods for unsupervised word embeddings. EMNLP.
    • Bakarov, A. (2018). A Survey of Word Embeddings Evaluation Methods. arXiv.
    • Ethayarajh, K. (2019). How Contextual are Contextualized Word Representations? EMNLP.
  3. 认知科学相关

    • Firth, J. R. (1957). Papers in Linguistics 1934-1951. Oxford University Press.
    • Rosch, E. (1975). Cognitive representations of semantic categories. Journal of Experimental Psychology.
    • Rogers, T. T., & McClelland, J. L. (2004). Semantic Cognition: A Parallel Distributed Processing Approach. MIT Press.

关联文档

实践示例

  • 使用gensim训练Word2Vec:参考gensim官方文档
  • 使用HuggingFace加载BERT:transformers库文档
  • 词向量可视化:使用TensorBoard或Plotly
  • 偏见检测与修正:参考Tensorflow的what-if工具