补充介绍DataWhale的Happy-llm项目中,task1中Word2Vec模型。
Word2Vec 词向量模型,
| 对比维度 | CBOW | Skip-Gram |
|---|---|---|
| 核心原理 | 用上下文词预测中心词 | 用中心词预测上下文词 |
| 输入 | 上下文窗口内的多个词 | 单个中心词 |
| 输出 | 中心词的概率分布 | 多个上下文词的概率分布 |
| 训练速度 | 较快(一次预测一个词) | 较慢(一次预测多个词) |
| 低频词表现 | 较差(上下文平均会平滑低频词信息) | 较好(对低频词有更多训练机会) |
| 词向量质量 | 语义平滑,适合频繁词 | 语义更精细,能捕捉更多细粒度关系 |
| 适用场景 | 小型语料库、常见词汇任务 | 大型语料库、低频词或细粒度语义任务 |
| 计算复杂度 | 较低 | 较高 |
包含了两种主要的架构:CBOW (Continuous Bag-of-Words) 和 Skip-Gram。
CBOW模型的实现
**核心原理:**用"上下文"预测"中心词
首先是CBOW和Skip-Gram二者都必备的pytorch库:
python
import torch
import torch.nn as nn
接着是具体CBOW模型的实现
python
class CBOW(nn.Module): # 定义 CBOW 类,继承自 PyTorch 的神经网络基类 nn.Module,这是所有自定义 PyTorch 模型的标准写法。
def __init__(self, vocab_size, embed_dim): # 定义初始化函数。接收两个参数:vocab_size(词汇表大小,即总共有多少个不同的词)和 embed_dim(词向量维度,即每个词用多少维的向量来表示)。
super(CBOW, self).__init__() # 调用父类 nn.Module 的初始化方法,这是构建 PyTorch 模型必须执行的步骤。
self.input_embedding = nn.Embedding(vocab_size, embed_dim) # 创建输入词嵌入层。它本质上是一个权重矩阵(大小为 vocab_size × embed_dim),用于将输入的上下文单词 ID 映射为密集的词向量。
self.output_embedding = nn.Embedding(vocab_size, embed_dim) # 创建输出词嵌入层。Word2Vec 的标准做法是使用两个独立的嵌入矩阵,一个用于输入词,另一个用于输出词(即被预测的中心词和负样本),这有助于提升模型表达能力。
nn.init.normal_(self.input_embedding.weight.data, mean = 0, std = 0.01) # 对输入嵌入层的权重进行初始化,使其服从均值为 0、标准差为 0.01 的正态(高斯)分布。合理的初始化有助于模型更快、更稳定地收敛。
nn.init.normal_(self.output_embedding.weight.data, mean = 0, std = 0.01) # 对输出嵌入层的权重进行同样的正态分布初始化。
self.loss_fn = nn.BCEWithLogitsLoss() # 定义损失函数。BCEWithLogitsLoss 结合了 Sigmoid 激活函数和二元交叉熵损失 (Binary Cross Entropy)。因为使用了负采样技术,模型将原本的多分类问题(在词表中选词)转化为了多个二分类问题(判断是真实的中心词还是负样本),所以使用二元交叉熵。
def forward(self, inputs): # 定义前向传播过程,即数据如何流经网络并计算损失。
context = inputs["contexts"] # 从输入的字典中获取上下文单词的 ID。形状通常为 [batch_size, context_window_size]。
target = inputs["targets"] # 获取目标单词(包含真实的中心词和随机采样的负样本)的 ID。形状通常为 [batch_size, num_targets]。
label = inputs["labels"] # 获取对应的标签。真实中心词的标签为 1,负样本的标签为 0。
context_embedding = self.input_embedding(context) # 将上下文单词 ID 传入输入嵌入层,查出它们对应的词向量。输出形状变为 [batch_size, context_window_size, embed_dim]。(注:代码中 emdedding 拼写有误,应为 embedding)
context_embedding = context_embedding.mean(1, keepdim = True) # 这是 CBOW 的核心操作。在维度 1(即上下文窗口维度)上求平均值。这意味着将上下文所有词的向量相加求平均,得到一个代表整体上下文的单一向量。keepdim=True 保证求平均后维度不消失,形状变为 [batch_size, 1, embed_dim]。
target_embedding = self.output_embedding(target) # 将目标单词(中心词+负样本)ID 传入输出嵌入层,得到它们的词向量。形状通常为 [batch_size, num_targets, embed_dim]。
embedding = context_embedding* target_embedding # 将上下文向量和目标向量进行逐元素相乘。由于上下文向量形状是 [bsz, 1, dim],目标向量是 [bsz, num_targets, dim],PyTorch 会自动进行广播 (Broadcasting) 机制,使上下文向量与每一个目标向量相乘。
embedding = torch.sum(embedding, dim = 2) # 在维度 2(即 embed_dim 词向量维度)上求和。逐元素相乘后再求和,在数学上等价于向量的点积(内积)。点积的结果越大,说明上下文和该目标词越相似。输出形状变为 [batch_size, num_targets]。(注:embdding 拼写有误)
loss = self.loss_fn(embedding, label.float()) # 将计算出的相似度得分(未经过 Sigmoid 的 logits)和真实标签输入到损失函数中,计算当前 Batch 的损失。
return loss # 返回损失值,供优化器进行反向传播更新权重。
Skip-Gram模型的实现
python
class SkipGram(nn.Module): # 定义 Skip-Gram 类及其初始化参数,参数含义与 CBOW 相同。
def __init__(self, vocab_size, embed_dim):
'''
下面这部分与 CBOW 完全一致。
初始化父类、创建双嵌入矩阵、进行正态分布初始化以及定义二元交叉熵损失函数。
在 Skip-Gram 中,input_embedding 用于中心词,output_embedding 用于上下文词和负样本。
'''
super(SkipGram, self).__init__()
self.input_embedding = nn.Embedding(vocab_size, embed_dim)
self.output_embedding = nn.Embedding(vocab_size, embed_dim)
nn.init.normal_(self.input_embedding.weight.data, mean=0., std=0.01)
nn.init.normal_(self.output_embedding.weight.data, mean=0., std=0.01)
self.loss_fn = nn.BCEWithLogitsLoss()
def forward(self, inputs): # 定义 Skip-Gram 的前向传播。
center_ids = inputs["center_ids"] # 获取上下文词和负样本的 ID。形状为 [batch_size, context_size]。注释解释了 context_size 的组成:真实的上下文词数量 + 负采样词的数量。
label = inputs["labels"] # 获取对应的二分类标签,1 代表真实的上下文词,0 代表负样本。
center_embedding = self.input_embedding(center_ids) # 将中心词 ID 查表得到词向量。此时形状是二维的 [batch_size, embed_dim]。
center_embedding = center_embedding.unsqueeze(1) # 关键步骤。在索引为 1 的位置插入一个新维度。将形状从 [bsz, embed_dim] 变成 [bsz, 1, embed_dim]。这是为了后续能和三维的上下文张量进行广播计算。
context_embedding = self.output_embedding(context_ids) # 将上下文词和负样本 ID 查表得到词向量,形状为三维的 [batch_size, context_size, embed_dim]。
embedding = center_embedding * context_embedding # 将中心词向量和上下文向量逐元素相乘。由于中心词向量形状是 [bsz, 1, dim],它会被广播复制 context_size 次,从而与每一个上下文/负样本向量完成相乘。
embedding = torch.sum(embedding, dim=2) # 在词向量维度 (dim=2) 上求和,计算中心词与每个上下文/负样本的点积相似度得分。形状变为 [batch_size, context_size]。
loss = self.loss_fn(embedding, label.float()) # 将点积得分和标签传入损失函数计算损失。
return loss