Word2Vec模型详解:CBOW与Skip-gram
Word2Vec模型详解:CBOW与Skip-gram
目录
- 模型概述
- 理论基础
- CBOW模型详解
- Skip-gram模型详解
- 模型对比
- 代码实现详解
- 训练过程分析
- 应用场景
- 实验结果
- 总结
模型概述
Word2Vec是一种用于生成词向量的神经网络模型,由Google在2013年提出。它包含两种主要架构:
- CBOW (Continuous Bag of Words): 根据上下文词汇预测目标词
- Skip-gram: 根据目标词预测上下文词汇
这两种模型都能将词汇转换为高维密集向量,使得语义相近的词在向量空间中距离较近。
理论基础
基本假设
Word2Vec基于分布式假设:在相似上下文中出现的词汇具有相似的语义。
核心思想
通过神经网络学习词汇的分布式表示,使得:
- 语义相似的词汇在向量空间中距离较近
- 词汇间的语义关系通过向量运算体现
- 支持词汇类比推理(如:国王-男人+女人=皇后)
CBOW模型详解
模型架构
上下文词汇 → 嵌入层 → 平均池化 → 线性层 → 目标词概率
CBOW模型通过上下文词汇的平均嵌入来预测中心词汇。
代码实现
1. 模型定义
# 定义CBOW模型类,继承自PyTorch的神经网络模块
class CBOWModel(nn.Module):def __init__(self, vocab_size, embedding_dim):# 调用父类构造函数super(CBOWModel, self).__init__()# 创建词嵌入层,将词汇索引映射到密集向量self.embedding = nn.Embedding(vocab_size, embedding_dim)# 创建线性层,将嵌入向量映射到词汇表大小的输出self.linear = nn.Linear(embedding_dim, vocab_size)def forward(self, context_words):# 前向传播函数,context_words形状: [batch_size, context_size]# 获取上下文词汇的嵌入向量embeds = self.embedding(context_words) # 形状: [batch_size, context_size, embedding_dim]# 计算上下文词汇嵌入的平均值mean_embed = torch.mean(embeds, dim=1) # 形状: [batch_size, embedding_dim]# 通过线性层得到最终输出out = self.linear(mean_embed) # 形状: [batch_size, vocab_size]return out
关键特点:
- 单一嵌入层:所有词汇共享同一个嵌入空间
- 平均池化:对上下文词汇嵌入求平均
- 分类任务:输出目标词汇的概率分布
2. 数据集创建
def create_cbow_dataset(corpus, window_size=2):"""创建CBOW训练数据集"""# 初始化数据列表data = []# 遍历语料库,跳过边界位置for i in range(window_size, len(corpus) - window_size):# 初始化上下文词汇列表context = []# 当前位置的词汇作为目标词target = corpus[i]# 收集目标词周围的上下文词汇for j in range(i - window_size, i + window_size + 1):# 跳过目标词本身if j != i:# 添加上下文词汇到列表context.append(corpus[j])# 将上下文和目标词作为训练样本data.append((context, target))# 返回训练数据集return data
数据格式:每个样本为 ([上下文词汇列表], 目标词)
3. 完形填空功能
def cloze_test(model, sentence, mask_position, word_to_id, id_to_word, window_size=2, top_k=5):"""完形填空函数"""# 将句子分割成词汇列表words = sentence.lower().replace('.', ' .').split()# 检查mask位置是否有效if mask_position >= len(words) or mask_position < 0:print(f"无效的mask位置: {mask_position}")return None# 保存被遮掩的真实词汇true_word = words[mask_position]print(f"被遮掩的词汇: '{true_word}'")# 提取上下文词汇(排除mask位置)context_words = []for i in range(max(0, mask_position - window_size), min(len(words), mask_position + window_size + 1)):if i != mask_position and words[i] in word_to_id:context_words.append(word_to_id[words[i]])# 进行预测with torch.no_grad():context_tensor = torch.tensor(context_words, dtype=torch.long).unsqueeze(0)output = model(context_tensor)probabilities = F.softmax(output, dim=1)# 获取top-k预测结果top_probs, top_indices = torch.topk(probabilities, top_k, dim=1)# 显示预测结果predictions = []for i in range(top_k):word_idx = top_indices[0][i].item()prob = top_probs[0][i].item()predicted_word = id_to_word[word_idx]predictions.append((predicted_word, prob))print(f"{predicted_word}\t\t{prob:.4f}")return predictions
完形填空原理:CBOW天然适合完形填空,因为它就是根据上下文预测目标词。
Skip-gram模型详解
模型架构
目标词 → 目标嵌入层 ↘→ 相似度计算 → 上下文词概率
上下文词 → 上下文嵌入层 ↗
Skip-gram使用两个独立的嵌入层,通过负采样进行训练。
代码实现
1. 模型定义
# 定义Skip-gram模型类,继承自PyTorch的神经网络模块
class SkipGramModel(nn.Module):def __init__(self, vocab_size, embedding_dim):# 调用父类构造函数super(SkipGramModel, self).__init__()# 创建输入词嵌入层,将目标词索引映射到密集向量self.target_embedding = nn.Embedding(vocab_size, embedding_dim)# 创建输出词嵌入层,用于预测上下文词self.context_embedding = nn.Embedding(vocab_size, embedding_dim)def forward(self, target_word, context_words):# 前向传播函数# target_word: [batch_size] - 目标词索引# context_words: [batch_size] - 上下文词索引# 获取目标词的嵌入向量target_embed = self.target_embedding(target_word) # [batch_size, embedding_dim]# 获取上下文词的嵌入向量context_embed = self.context_embedding(context_words) # [batch_size, embedding_dim]# 计算目标词和上下文词的相似度得分score = torch.sum(target_embed * context_embed, dim=1) # [batch_size]return score
关键特点:
- 双嵌入层:目标词和上下文词有不同的嵌入空间
- 点积相似度:计算目标词和上下文词嵌入的内积
- 回归任务:输出相似度得分
2. 负采样训练
def negative_sampling(vocab_size, num_negatives=5):"""负采样:随机选择负样本词汇"""# 随机生成负样本词汇索引negative_samples = torch.randint(0, vocab_size, (num_negatives,))return negative_samplesdef train_skipgram(model, train_data, vocab_size, epochs=100, learning_rate=0.01, num_negatives=5):"""训练Skip-gram模型(使用负采样)"""# 定义二元交叉熵损失函数criterion = nn.BCEWithLogitsLoss()# 定义Adam优化器optimizer = optim.Adam(model.parameters(), lr=learning_rate)for epoch in range(epochs):total_loss = 0for target, context in train_data:# 正样本训练target_tensor = torch.tensor([target], dtype=torch.long)context_tensor = torch.tensor([context], dtype=torch.long)positive_label = torch.tensor([1.0], dtype=torch.float)positive_score = model(target_tensor, context_tensor)positive_loss = criterion(positive_score, positive_label)# 负样本训练negative_samples = negative_sampling(vocab_size, num_negatives)negative_labels = torch.zeros(num_negatives, dtype=torch.float)target_repeated = target_tensor.repeat(num_negatives)negative_scores = model(target_repeated, negative_samples)negative_loss = criterion(negative_scores, negative_labels)# 总损失total_sample_loss = positive_loss + negative_loss# 反向传播和参数更新optimizer.zero_grad()total_sample_loss.backward()optimizer.step()total_loss += total_sample_loss.item()return losses
负采样原理:
- 正样本:真实的(目标词, 上下文词)对,标签为1
- 负样本:随机的(目标词, 非上下文词)对,标签为0
- 避免计算整个词汇表的softmax,提高训练效率
3. 上下文预测功能
def predict_context_words(model, target_word, word_to_id, id_to_word, top_k=5):"""预测给定目标词的上下文词汇"""target_idx = word_to_id[target_word]target_tensor = torch.tensor([target_idx], dtype=torch.long)model.eval()with torch.no_grad():# 计算目标词与所有词汇的相似度得分scores = []vocab_size = len(word_to_id)for word_idx in range(vocab_size):context_tensor = torch.tensor([word_idx], dtype=torch.long)score = model(target_tensor, context_tensor)scores.append((word_idx, score.item()))# 按得分降序排序并返回top-k结果scores.sort(key=lambda x: x[1], reverse=True)predictions = []for i in range(min(top_k, len(scores))):word_idx, score = scores[i]predicted_word = id_to_word[word_idx]if predicted_word != target_word:predictions.append((predicted_word, score))return predictions
上下文预测原理:Skip-gram擅长预测给定词汇的可能上下文,适用于词汇扩展和同义词发现。
模型对比
特性 | CBOW | Skip-gram |
---|---|---|
输入 | 上下文词汇 | 目标词 |
输出 | 目标词概率 | 上下文词得分 |
嵌入层 | 单一共享嵌入 | 双独立嵌入 |
训练数据量 | 较少 | 较多 |
计算复杂度 | 较低 | 较高 |
适用场景 | 完形填空、语言模型 | 词汇扩展、相似词发现 |
对频率的敏感性 | 对高频词效果好 | 对低频词效果好 |
数学表示
CBOW目标函数:
maximize Σ log P(w_t | context(w_t))
Skip-gram目标函数:
maximize Σ Σ log P(w_c | w_t)
其中:
w_t
: 目标词w_c
: 上下文词context(w_t)
: 目标词的上下文窗口
代码实现详解
共享组件
1. 数据预处理
def preprocess(text):"""文本预处理函数"""# 转换为小写并分割单词word = text.lower().replace('.', ' .').split(' ')# 创建词汇到ID的映射word_to_id = {}id_to_word = {}for w in word:if w not in word_to_id:tmp = len(word_to_id)word_to_id[w] = tmpid_to_word[tmp] = w# 获得ID列表corpus = [word_to_id[w] for w in word]return corpus, word_to_id, id_to_word
2. 相似度计算
def cos_similarity(x, y, eps=1e-8):"""计算余弦相似度"""nx = x / (np.sqrt(np.sum(x ** 2)) + eps)ny = y / (np.sqrt(np.sum(y ** 2)) + eps)return np.dot(nx, ny)
3. 词向量可视化
def visualize_embeddings(word_vectors, word_to_id, max_words=50):"""可视化词向量(使用PCA降维到2D)"""from sklearn.decomposition import PCAwords = list(word_vectors.keys())[:max_words]vectors = [word_vectors[word] for word in words]pca = PCA(n_components=2)vectors_2d = pca.fit_transform(vectors)plt.figure(figsize=(12, 8))plt.scatter(vectors_2d[:, 0], vectors_2d[:, 1], alpha=0.7)for i, word in enumerate(words):plt.annotate(word, (vectors_2d[i, 0], vectors_2d[i, 1]))plt.title('Word Embeddings Visualization (PCA)')plt.show()
训练流程对比
CBOW训练流程
- 提取上下文词汇窗口
- 计算上下文词汇嵌入的平均值
- 通过线性层预测目标词
- 使用交叉熵损失进行优化
Skip-gram训练流程
- 选择目标词和一个上下文词
- 计算正样本得分和损失
- 进行负采样,计算负样本损失
- 使用二元交叉熵损失进行优化
训练过程分析
超参数设置
# 共同超参数
window_size = 2 # 上下文窗口大小
embedding_dim = 100 # 词向量维度
epochs = 100 # 训练轮数
learning_rate = 0.01 # 学习率# Skip-gram特有参数
num_negatives = 5 # 负采样数量
训练数据量对比
以示例文本为例:
- CBOW: 15个训练样本(每个位置一个样本)
- Skip-gram: 60个训练样本(每个(目标词,上下文词)对一个样本)
损失函数分析
CBOW损失曲线特点:
- 初始损失较高(约3.0)
- 快速收敛到接近0
- 曲线较为平滑
Skip-gram损失曲线特点:
- 初始损失很高(约7.6)
- 收敛较慢,存在波动
- 最终损失相对较高(约1.3)
应用场景
CBOW适用场景
-
完形填空任务
# 例:根据"you say ___ and I"预测"goodbye" cloze_test(model, "you say goodbye and I say hello", 2, word_to_id, id_to_word)
-
语言模型构建
- 文本生成
- 语法检查
- 自动补全
-
高频词处理
- 对常见词汇效果更好
- 适合处理语法功能词
Skip-gram适用场景
-
词汇扩展和发现
# 例:根据"love"找到相关词汇 predict_context_words(model, "love", word_to_id, id_to_word)
-
同义词挖掘
- 发现语义相似词汇
- 构建同义词词典
-
低频词处理
- 对罕见词汇效果更好
- 适合处理专业术语
-
词汇类比推理
# 概念上的:king - man + woman ≈ queen
实验结果
训练效果对比
测试文本: “you say goodbye and I say hello. what are you talking about? I love natural language processing.”
CBOW结果
相似词查找:
- "you" 的相似词: are, language, goodbye, i, about?
- "say" 的相似词: goodbye, what, natural, love, talking
- "hello" 的相似词: and, love, are, goodbye, about?完形填空准确性: 中等
收敛速度: 快
Skip-gram结果
相似词查找:
- "you" 的相似词: say, language, natural, love, hello
- "say" 的相似词: talking, hello, you, language, i
- "hello" 的相似词: goodbye, ., say, language, about?上下文预测能力: 强
词汇发现能力: 强
性能分析
指标 | CBOW | Skip-gram |
---|---|---|
训练速度 | 快 | 慢 |
内存消耗 | 低 | 高 |
收敛稳定性 | 高 | 中等 |
词汇发现能力 | 中等 | 强 |
语法理解能力 | 强 | 中等 |
代码使用指南
运行CBOW模型
cd nlp
python cbow_pytorch.py
功能包括:
- 模型训练和损失可视化
- 相似词查找
- 批量完形填空测试
- 交互式完形填空
- 词向量可视化
运行Skip-gram模型
cd nlp
python skipgram_pytorch.py
功能包括:
- 负采样训练
- 相似词查找
- 上下文词预测
- 词向量可视化
自定义使用
# 导入模型
from cbow_pytorch import CBOWModel, train_cbow
from skipgram_pytorch import SkipGramModel, train_skipgram# 准备数据
text = "your custom text here"
corpus, word_to_id, id_to_word = preprocess(text)# 训练CBOW
cbow_model = CBOWModel(len(word_to_id), 100)
cbow_data = create_cbow_dataset(corpus)
train_cbow(cbow_model, cbow_data)# 训练Skip-gram
skipgram_model = SkipGramModel(len(word_to_id), 100)
skipgram_data = create_skipgram_dataset(corpus)
train_skipgram(skipgram_model, skipgram_data, len(word_to_id))
优化建议
模型改进方向
-
层次化Softmax
- 替代负采样,提高大词汇表训练效率
- 基于词频构建二叉树结构
-
子词信息
- 引入字符级或子词级信息
- 处理未登录词和形态变化
-
动态窗口
- 根据距离调整上下文权重
- 更好捕捉长距离依赖
工程优化
-
批处理训练
# 当前实现是单样本训练,可以改为批处理 batch_size = 32
-
数据并行
# 使用PyTorch的DataParallel进行多GPU训练 model = nn.DataParallel(model)
-
内存优化
- 使用gradient checkpointing
- 动态词汇表
总结
核心要点
-
CBOW与Skip-gram是互补的:
- CBOW:上下文→目标词,适合完形填空
- Skip-gram:目标词→上下文,适合词汇发现
-
训练策略不同:
- CBOW:使用交叉熵损失,直接优化
- Skip-gram:使用负采样,避免昂贵的softmax
-
应用场景各异:
- CBOW:语言建模、语法任务
- Skip-gram:语义分析、词汇扩展
选择建议
- 小数据集:选择CBOW,训练更稳定
- 大数据集:选择Skip-gram,效果更好
- 完形填空:使用CBOW
- 词汇发现:使用Skip-gram
- 平衡考虑:可以同时训练两个模型并集成
扩展方向
- FastText: 加入子词信息
- GloVe: 结合全局统计信息
- ELMo: 上下文相关的词向量
- BERT: 双向Transformer编码器
这两个模型为现代NLP奠定了重要基础,理解它们的原理和实现对深入学习词嵌入技术具有重要意义。