在自然语言处理(NLP)领域,词向量(Word Embedding)是连接自然语言与机器学习模型的桥梁,而Word2Vec无疑是词向量技术中最经典的代表。Word2Vec包含两种核心模型:CBOW(Continuous Bag-of-Words,连续词袋模型)和Skip-gram。本文将从原理入手,基于PyTorch框架从零实现CBOW模型,帮助大家理解词向量的生成过程。
一、CBOW模型核心原理
CBOW模型的核心思想是通过上下文预测中心词:给定一个目标词的上下文(左右相邻的若干个词),模型学习输出这个目标词的概率分布,最终通过模型训练得到的嵌入层权重,即为我们所需的词向量。
举个简单例子:句子"We are about to study"中,若以"about"为中心词,上下文窗口设为2(即左右各2个词),则上下文是["We","are","to","study"],CBOW模型要做的就是用这4个词预测中心词"about"。
CBOW模型的核心流程:
-
将上下文词汇转换为索引,再通过嵌入层(Embedding)映射为低维稠密向量;
-
对所有上下文词向量做平均/求和操作;
-
通过全连接层将词向量映射到词汇表维度;
-
用Softmax激活函数输出词汇表中每个词作为中心词的概率;
-
以负对数似然损失(NLLLoss)优化模型,最终得到词向量。
二、代码实现:从零构建CBOW模型
1. 数据预处理:构建语料库与词汇映射
首先我们需要准备语料,并完成基础的数据预处理:构建词汇表、词汇-索引映射、生成训练数据(上下文-中心词对)。
python
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from tqdm import tqdm
import numpy as np
# 1. 配置参数与原始语料
CONTEXT_SIZE = 2 # 上下文窗口大小:左右各2个词
raw_text = """We are about to study the idea of a computational process.
Computational processes are abstract beings that inhabit computers.
As they evolve, processes manipulate other abstract things called data.
The evolution of a process is directed by a pattern of rules called a program.
People create programs to direct processes.
In effect,we conjure the spirits of the computer with our spells.""".split()
# 2. 构建词汇表与映射关系
vocab = set(raw_text) # 去重得到词汇表
vocab_size = len(vocab)
word_to_idx = {word: i for i, word in enumerate(vocab)} # 词→索引
idx_to_word = {i: word for i, word in enumerate(vocab)} # 索引→词
# 3. 生成训练数据:(上下文, 中心词) 对
data = []
for i in range(CONTEXT_SIZE, len(raw_text) - CONTEXT_SIZE):
# 拼接左上下文(前2个词)和右上下文(后2个词)
context = (
[raw_text[i-(2-j)] for j in range(CONTEXT_SIZE)] +
[raw_text[i+j+1] for j in range(CONTEXT_SIZE)]
)
target = raw_text[i] # 中心词
data.append((context, target))
# 4. 工具函数:将上下文转换为索引张量
def make_context_vector(context, word_to_ix):
idxs = [word_to_ix[w] for w in context]
return torch.tensor(idxs, dtype=torch.long)
# 测试数据预处理结果
print("示例上下文索引:", make_context_vector(data[0][0], word_to_idx))
2. 定义CBOW模型结构
基于PyTorch的nn.Module自定义CBOW模型,核心包含三层:
• 嵌入层(Embedding):将词索引映射为低维词向量;
• 投影层(Linear):对词向量做线性变换;• 输出层(Linear):映射回词汇表维度,输出概率分布。
python
# 设备配置:优先使用GPU/MPS,否则用CPU
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")
class CBOW(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(CBOW, self).__init__()
# 嵌入层:vocab_size个词,每个词映射为embedding_dim维向量
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
# 投影层:将词向量映射到128维
self.proj = nn.Linear(embedding_dim, 128)
# 输出层:映射回词汇表维度,用于预测中心词
self.output = nn.Linear(128, vocab_size)
def forward(self, input):
# 1. 嵌入层:获取上下文词向量并求和
embeds = sum(self.embeddings(input)).view(1, -1)
# 2. 投影层 + ReLU激活
out = F.relu(self.proj(embeds))
# 3. 输出层 + 对数Softmax(适配NLLLoss)
out = self.output(out)
nll_prob = F.log_softmax(out, dim=-1)
return nll_prob
3. 模型训练
配置优化器、损失函数,迭代训练模型,并记录损失变化:
python
# 初始化模型、优化器、损失函数
model = CBOW(vocab_size, 10).to(device) # 词向量维度设为10
optimizer = optim.Adam(model.parameters(), lr=0.001)
loss_function = nn.NLLLoss() # 负对数似然损失(适配log_softmax输出)
losses = []
# 训练模式
model.train()
# 迭代训练200轮
for epoch in tqdm(range(200), desc="Training"):
total_loss = 0
for context, target in data:
# 1. 准备数据:上下文张量 + 目标词张量
context_vector = make_context_vector(context, word_to_idx).to(device)
target = torch.tensor([word_to_idx[target]]).to(device)
# 2. 前向传播
predict = model(context_vector)
loss = loss_function(predict, target)
# 3. 反向传播 + 优化
optimizer.zero_grad() # 清空梯度
loss.backward() # 反向传播
optimizer.step() # 更新参数
# 累计损失
total_loss += loss.item()
losses.append(total_loss)
# 打印每轮损失(可选)
# print(f"Epoch {epoch+1}, Loss: {total_loss:.4f}")
print("训练完成,损失变化:", losses[:5], "...") # 打印前5轮损失
4. 模型测试与词向量提取
训练完成后,我们可以用自定义上下文测试模型预测效果,并提取最终的词向量:
python
# 测试模型:自定义上下文预测中心词
context = ['People','create','to','direct']
context_vector = make_context_vector(context, word_to_idx).to(device)
# 评估模式(关闭Dropout等)
model.eval()
with torch.no_grad(): # 禁用梯度计算
predict = model(context_vector)
max_idx = predict.argmax(1) # 取概率最大的索引
print("预测中心词:", idx_to_word[max_idx.item()])
# 提取词向量:嵌入层权重即为词向量
print("\n嵌入层权重(词向量矩阵):", model.embeddings.weight)
# 转换为CPU上的numpy数组(方便后续使用)
W = model.embeddings.weight.cpu().detach().numpy()
# 构建词→词向量的映射字典
word_2_vec = {}
for word in word_to_idx.keys():
word_2_vec[word] = W[word_to_idx[word], :]
# 保存词向量到文件
np.savez('cbow_word2vec.npz', word_vectors=W)
# 加载验证
data = np.load('cbow_word2vec.npz')
print("\n保存的词向量文件:", data.files)
print("示例词向量(process):", word_2_vec["process"])
三、结果分析与拓展
1. 损失变化解读
训练过程中,损失值应逐步下降并趋于稳定,这说明模型正在学习到上下文与中心词的关联规律。如果损失下降缓慢,可尝试调整学习率、词向量维度、上下文窗口大小或训练轮数。
2. 词向量的应用
提取到的词向量可用于:
• 词汇相似度计算:通过余弦相似度衡量两个词的语义相似性;
• 文本分类/情感分析:将词向量作为模型输入特征;
• 词汇类比推理:如"国王-男人+女人=女王"的经典推理任务。
3. 模型优化方向
本文实现的是基础版CBOW,可从以下方向优化:
• 替换求和为平均:上下文词向量求和改为平均,减少长上下文的数值偏差;
• 增大语料库:本文语料较小,实际应用需用大规模语料训练;
• 加入负采样(Negative Sampling):替代全量Softmax,提升训练效率;
• 调整超参数:词向量维度(如100/200/300)、上下文窗口、学习率等。
四、总结
本文从原理到代码,完整实现了基于CBOW的Word2Vec模型,核心要点:
-
CBOW通过"上下文预测中心词"学习词向量,是Word2Vec的核心模型之一;
-
嵌入层的权重即为词向量,这是NLP中词向量的核心来源;
-
PyTorch的nn.Embedding层是实现词向量的关键,结合全连接层可快速搭建CBOW模型。
通过本文的实现,相信大家对词向量的生成逻辑有了更直观的理解。在此基础上,可进一步探索Skip-gram模型、负采样优化等内容,深入掌握Word2Vec的精髓。