词向量与分布式语义
文档概述
本文档系统阐述词向量的理论基础、实现原理及认知科学联系。重点介绍Word2Vec、GloVe、FastText等经典模型,以及ELMo、BERT等上下文词向量技术,并探讨Embedding空间的几何性质及其认知意义。本文档还将深入讨论词向量在不同NLP任务中的应用、现代预训练语言模型的发展脉络,以及词向量技术的最新研究前沿。
关键词速览
| 术语 | 英文 | 核心定义 |
|---|---|---|
| 分布式假说 | Distributional Hypothesis | 词语由其上下文定义 |
| 词向量 | Word Vector/Embedding | 词语的稠密向量表示 |
| Word2Vec | Word2Vec | 经典的词嵌入训练模型 |
| GloVe | Global Vectors | 全局共现统计词嵌入 |
| FastText | FastText | 子词嵌入模型 |
| 上下文词向量 | Contextualized Embedding | 随上下文变化的词表示 |
| ELMo | Embeddings from Language Models | 双向LSTM词向量 |
| BERT | Bidirectional Encoder Representations | Transformer编码器 |
| 语义空间 | Semantic Space | 词向量张成的空间 |
| 认知科学 | Cognitive Science | 研究人类认知的学科 |
| Transformer | Transformer | 基于自注意力的序列建模架构 |
| 表征学习 | 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。这种表示存在两个主要问题:
- 维度灾难:词表大小通常在数万到数百万,导致高维稀疏向量难以处理
- 语义鸿沟:任意两个词的表示都是正交的,无法捕捉语义相似性
相比之下,分布式表示将每个词映射到一个低维稠密空间(如100-1000维),每个维度都不是binary的,而是连续值。这种表示的优势在于:
- 维度压缩:从V维稀疏向量压缩到d维稠密向量(d << V)
- 语义编码:语义相似的词在向量空间中距离较近
- 知识迁移:相似的语义特征可以在不同词之间共享
分布式表示的信息论解释
从信息论角度,词向量的学习过程可以理解为一种信息压缩:我们试图用低维向量(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学习到的词向量具有以下有趣的数学性质:
-
线性关系:对于具有某种语法关系的词对,向量差往往编码了这种关系
-
类比完成:词向量空间中的向量运算可以完成词类比任务
- 国家-首都关系: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-gram | CBOW |
|---|---|---|
| 学习方向 | 中心→上下文 | 上下文→中心 |
| 训练数据量 | 较少(每对词一个样本) | 较多(每个中心词一个样本) |
| 稀有词处理 | 更好(从中心词学习) | 较差(被常用词稀释) |
| 训练速度 | 较慢(更多样本) | 较快 |
| 大规模语料 | 表现优秀 | 表现一般 |
| 小规模语料 | 表现较差 | 表现尚可 |
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 results2.2 GloVe模型
GloVe(Global Vectors)由Jeffrey Pennington、Richard Socher和Christopher Manning于2014年在斯坦福大学提出,融合了全局矩阵分解和局部上下文窗口两种方法的优点。
2.2.1 核心思想
GloVe基于全局词共现矩阵进行训练,其损失函数设计结合了:
- 全局统计信息:利用词共现矩阵 ,其中 表示词 在词 的上下文中出现的次数
- 局部上下文:保持Skip-gram的窗口概念
共现矩阵的构建
共现矩阵 是一个 的对称矩阵, 表示词 和词 在同一上下文中共同出现的次数。上下文窗口大小的选择影响共现信息的性质:
- 小窗口:捕捉词汇-语法(lexico-syntactic)关系
- 大窗口:捕捉词汇-语义(lexico-semantic)关系
GloVe的动机
论文标题”Global Vectors”强调了模型利用全局语料统计信息的能力。GloVe的作者认为,词的共现概率比比值能够编码语义信息:
考虑词语 、、 的共现概率:
其中 是词 的总共现次数。
共现概率的比值可以编码词之间的关系:
例如,对于词语”ice”和”steam”:
- 较高
- 较低
- 比值 较高
这表明”solid”是与”ice”相关但与”steam”无关的词语。
2.2.2 损失函数详解
GloVe的损失函数设计解决了两个问题:
- 处理稀有共现:稀有共现的统计不够可靠
- 处理高频共现:高频共现(如”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) / 22.2.3 GloVe与Word2Vec的深入对比
| 特性 | Word2Vec | GloVe |
|---|---|---|
| 训练目标 | 预测概率(条件) | 重构共现概率 |
| 语料利用 | 局部上下文 | 全局共现矩阵 |
| 训练速度 | 快(在线) | 较慢(需要矩阵分解) |
| 语义任务表现 | 相似词效果好 | 类比推理更优 |
| 语法任务表现 | 一般 | 优秀 |
| 参数数量 | ~2×V×d | ~2×V×d + 2×V |
| 内存需求 | 低(流式处理) | 高(需存储共现矩阵) |
| 收敛性 | 取决于采样策略 | 更稳定 |
何时选择哪个模型
-
选择Word2Vec的场景:
- 超大规模语料
- 资源受限
- 需要流式训练
- 增量更新
-
选择GloVe的场景:
- 中等规模语料
- 需要稳定的收敛性
- 语法相关任务
- 类比推理任务
2.3 FastText模型
FastText由Piotr Bojanowski等人于2016年在Facebook AI Research提出,核心创新是引入子词(subword)信息。
2.3.1 子词嵌入原理
FastText将每个词表示为其字符n-gram的集合,这使得模型能够处理:
- 形态学信息:词缀、前缀、后缀的语义
- 未登录词(OOV):可以通过子词组合表示
- 稀有词:共享子词信息的词
子词生成算法
对于词”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的优势
-
处理未登录词(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 -
处理形态丰富语言:如德语、土耳其语、芬兰语等
德语示例: "unwahrscheinlich" (不可能的) 子词: <un, unw, nwä, w...> 可以学习: un- (否定前缀), -lich (形容词后缀) 的语义 -
捕捉词缀信息:自动学习词根、词缀的语义关联
动词形态: - walk, walked, walking - 子词共享: <wal, alk, lk> 等 → 即使"walked"不在词典中,也可以通过子词推断 -
处理拼写变体和错误
用户输入: "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"
- 语义:存储血液的机构
传统词向量会将这些完全不同的含义平均到一个单一的向量中,导致语义模糊。
其他局限性
- 无法处理上下文变化:词的含义随语境变化,但向量不变
- 难以捕捉话语级特征:句子级别的语义无法从词级向量简单聚合
- 缺乏跨句依赖:无法建模句子之间的关系
- 静态语义:无法捕捉动态的语义变化
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) * x3.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_layers3.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 outputs3.3.3 BERT的预训练任务详解
掩码语言模型(MLM)
BERT随机选择15%的token进行mask,但采用了一种特殊策略:
- 80% 替换为
[MASK]token - 10% 替换为随机词(从词表中随机采样)
- 10% 保持不变
这种策略的原因是:
- 如果只使用
[MASK],微调时模型不会看到[MASK],造成预训练-微调不一致 - 加入随机词和保持不变可以让模型更好地学习表示
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的优化版本,主要改进:
- 更多训练数据和更长时间
- 移除NSP任务(实验证明无帮助)
- 更大的batch size
- 动态masking
- 更长的训练序列
ALBERT(A Lite BERT)
参数共享的轻量级BERT:
- 跨层参数共享:所有Transformer层共享参数
- 因子化嵌入:将大词嵌入分解为两个小矩阵
- 句序预测(SOP):比NSP更有效的下一句任务
DistilBERT
知识蒸馏的BERT简化版本:
- 保留85%性能
- 参数减少40%
- 推理速度提升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 | 形容词比较级 |
| 公司-CEO | Apple - 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 跑 跳 飞 游 走 爬
[职业类] [情感类]
医生老师律师 喜怒哀乐悲恐
这种聚类结构具有以下特点:
- 类别内聚性:同类词聚集在局部区域
- 类别间分离性:不同类别的词相对分散
- 层级结构:存在多层级的聚类层次
- 边界模糊性:类别边界不是清晰的,而是渐变的
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 embeddings4.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?]
问题:
- 无法解释语义模糊性和语境依赖
- 特征边界不清晰(如”中年”是+ADULT还是-YOUNG?)
- 无法捕捉特征的重要性差异
分布式表征理论(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_RT5.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_words5.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 主流词向量工具对比
| 工具 | 语言 | 特点 | 预训练模型 | 适用场景 |
|---|---|---|---|---|
| gensim | Python | Word2Vec/FastText实现 | 多语言 | 快速实验 |
| spaCy | Python | 集成词向量 | en_core_web_md | 生产环境 |
| TensorFlow Hub | 多语言 | Universal Sentence Encoder | 17种语言 | 句子嵌入 |
| HuggingFace | Python | BERT系列模型 | 100+模型 | 预训练模型 |
| fastText | C++/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 []参考文献与推荐阅读
-
经典论文
- 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.
-
进阶阅读
- 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.
-
认知科学相关
- 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工具