今天我们进行自然语言处理(NLP)的学习。基于上下文预测中间词的CBOW模型------它是Word2Vec的核心模型之一,也是理解"词嵌入"思想的关键。博文分为三个部分:名词补充介绍,CBOW原理讲解,代码实现CBOW。
一、补充知识:筑牢CBOW学习基础
在学习CBOW模型前,我们先理清几个核心名词的含义,这些都是今日学习中高频接触的关键概念,理解它们才能更好地掌握模型逻辑。
1. Word2Vec
Word2Vec是2013年由Google提出的一套"词向量生成工具",核心目标是将自然语言中的单词,转换为计算机能理解的低维稠密向量(即词嵌入向量)。它包含两个核心模型:
-
CBOW(Continuous Bag-of-Words):连续词袋模型,核心逻辑是"通过上下文单词预测中心词"(也是我们今日重点学习的模型);
-
Skip-gram:跳字模型,逻辑与CBOW相反,是"通过中心词预测上下文单词"。
Word2Vec的核心价值是解决了传统One-Hot向量的"维度灾难"问题,同时让生成的词向量蕴含语义信息(比如"苹果"和"香蕉"的向量相似度高于"苹果"和"汽车")。
2. 词嵌入(Embedding)与Embedding_dim
Embedding层是深度学习中将离散型数据(如单词、类别)转换为连续型向量表示的核心组件。它可以将高维的稀疏表示(如one-hot编码)转换为低维的稠密向量。Embedding_dim表示词嵌入的维度。
3. One-Hot向量(独热编码)
One-Hot向量是早期表示单词的方式,核心是"用词汇表大小作为向量长度,某个单词对应位置记为1,其余为0"。比如词汇表为[We, are, about, to, study],则"are"的One-Hot向量是[0,1,0,0,0]。
缺点很明显:词汇表越大,向量维度越高(比如10万词对应10万维),存在"维度灾难";且向量间是正交关系,无法体现语义关联(比如"苹果"和"香蕉"的相似度为0)。
4. 索引张量(Index Tensor)
CBOW模型无法直接处理单词,需要先将单词转换为数字索引(比如通过word_to_idx字典将"We"映射为0),再将索引组成的列表转换为PyTorch的张量------这种存储单词索引的张量就是"索引张量"。
关键要求:PyTorch的nn.Embedding层(词嵌入层)要求输入的索引张量必须是torch.long(长整型)类型,否则会报错。
5. Log-Softmax与NLLLoss
这是CBOW模型计算损失的核心组合,等价于交叉熵损失(nn.CrossEntropyLoss):
-
Log-Softmax:先通过Softmax将模型输出的原始得分(无范围限制)归一化为0~1的概率分布,再对概率取自然对数,避免单独计算Softmax时的数值溢出;
-
NLLLoss(负对数似然损失):找到目标词索引对应的对数概率,取其负值作为损失------损失越小,说明模型预测目标词的概率越高。
二. CBOW 词嵌入模型原理(核心是 "用上下文预测目标词")

1、模型流程:
训练流程可简化为:① 输入:上下文词的 One-Hot 编码;② 映射:通过V×N矩阵将 One-Hot 转为低维词嵌入向量;③ 聚合:平均多个上下文的词嵌入,得到上下文整体特征;④ 映射回:通过N×V矩阵将低维特征转回词汇表维度;⑤ 预测:Softmax 归一化得概率,选最大概率对应词为预测结果;⑥ 优化:对比真实标签算损失,反向传播调整矩阵参数,提升预测精度。
2、 核心设计逻辑(为什么要映射低维)
- 降成本:把
V×V级别的参数压缩为V×N+N×V(N 远小于 V),降低计算 / 存储压力; - 捕语义:让语义相近的词在低维空间中向量更接近;
- 提泛化:让模型学到词的共性特征,能理解未见过的上下文组合。
三、项目案例:基于CBOW实现语料库预测中间词
1、数据准备:构建训练数据与词汇表
CBOW模型的训练数据是"上下文-中心词"对(),我们先从原始文本中提取这些数据,并构建词汇表和单词-索引映射。
python
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
# 原始文本(可根据需求替换)
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."""
# 文本分词(按空格分割,简单处理)
raw_text = raw_text.replace(".", "").split()
# 上下文窗口大小:中心词前后各2个词
CONTEXT_SIZE = 2
用集合去重得到词汇表,再构建"单词→索引"和"索引→单词"的双向映射字典(方便后续转换):
python
# 构建词汇表(不重复单词集合)
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)}
遍历原始文本,提取每个中心词对应的上下文,组成训练样本:
python
data = []
for i in range(CONTEXT_SIZE, len(raw_text) - CONTEXT_SIZE):
# 上下文:中心词前后各2个词
context = [
raw_text[i - CONTEXT_SIZE], raw_text[i - CONTEXT_SIZE + 1],
raw_text[i + 1], raw_text[i + 2]
]
# 中心词(目标词)
target = raw_text[i]
data.append((context, target))
# 查看前3个训练样本
print("训练样本示例:", data[:3])
定义函数,将单词列表形式的上下文,转为模型所需的torch.long类型索引张量:
python
def make_context_vector(context, word_to_idx):
# 将上下文单词转为索引列表
idxs = [word_to_idx[word] for word in context]
# 转为long类型张量(nn.Embedding要求输入为long型)
return torch.tensor(idxs, dtype=torch.long)
2、定义CBOW模型
CBOW模型结构分为三层:词嵌入层(Embedding)、中间特征层(Linear+ReLU)、输出层(Linear),核心逻辑是"上下文嵌入聚合→特征变换→预测中心词"。
python
class CBOW(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(CBOW, self).__init__()
# 1. 词嵌入层:将索引转为低维嵌入向量
# 输入维度:vocab_size(词汇表大小),输出维度:embedding_dim(词向量维度)
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
# 2. 中间特征层(proj层):增强特征表达能力
# 输入维度:embedding_dim,输出维度:128(超参数,可调整)
self.proj = nn.Linear(embedding_dim, out_features=128)
# 3. 输出层:将特征映射到词汇表维度,预测每个单词是中心词的得分
# 输入维度:128,输出维度:vocab_size
self.output = nn.Linear(128, vocab_size)
def forward(self, inputs):
# 步骤1:获取上下文单词的词嵌入(inputs是索引张量,形状:[4])
embeds = self.embeddings(inputs) # 输出形状:[4, embedding_dim]
# 步骤2:聚合上下文嵌入(求和,合并为一个代表上下文语义的向量)
embeds_sum = sum(embeds).view(1, -1) # 输出形状:[1, embedding_dim](适配后续层输入)
# 步骤3:特征变换+非线性激活(ReLU引入非线性,拟合复杂语义)
proj_out = F.relu(self.proj(embeds_sum)) # 输出形状:[1, 128]
# 步骤4:输出预测得分,转为对数概率(供损失计算)
output = self.output(proj_out) # 输出形状:[1, vocab_size]
return F.log_softmax(output, dim=-1) # dim=-1:在词汇表维度做归一化
词嵌入层操作解释:

CBOW 的逻辑是 "用上下文预测中心词",因此需要把多个上下文单词的嵌入向量合并成一个 "整体特征"------ 求和是最简单的聚合方式(也可以用mean()平均,效果几乎一致,只是缩放系数不同,模型会通过权重补偿)。
sum vs mean :求和(sum)和求平均(mean)都是常见的聚合方式,比如把代码改成torch.mean(self.embeddings(inputs), dim=0).view(1, -1),逻辑完全等价 ------ 求平均只是把求和结果除以单词数,模型训练时会自动调整权重,最终效果无差异。
3、模型训练
配置训练参数,通过"前向传播→计算损失→反向更新"的循环,让模型学习上下文与中心词的关联规律。
初始化模型、损失函数与优化器:
python
# 超参数设置
embedding_dim = 10 # 词向量维度(小语料选10即可)
learning_rate = 0.001 # 学习率
epochs = 500 # 训练轮数
# 初始化模型(传入词汇表大小和词向量维度)
model = CBOW(vocab_size, embedding_dim)
# 损失函数:NLLLoss(配合log_softmax使用)
loss_fn = nn.NLLLoss()
# 优化器:Adam(自适应学习率,训练更稳定)
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
循环训练:
python
for epoch in range(epochs):
total_loss = 0.0
# 遍历所有训练样本
for context, target in data:
# 1. 准备输入和标签:上下文转索引张量,目标词转索引
context_tensor = make_context_vector(context, word_to_idx)
target_idx = torch.tensor([word_to_idx[target]], dtype=torch.long)
# 2. 前向传播:模型预测
log_prob = model(context_tensor)
# 3. 计算损失
loss = loss_fn(log_prob, target_idx)
# 4. 反向传播+参数更新
optimizer.zero_grad() # 清空上一轮梯度
loss.backward() # 反向计算梯度
optimizer.step() # 更新模型参数
# 累加总损失
total_loss += loss.item()
# 每100轮打印一次平均损失(查看训练进度)
if (epoch + 1) % 100 == 0:
avg_loss = total_loss / len(data)
print(f"Epoch [{epoch+1}/{epochs}], Average Loss: {avg_loss:.4f}")
4、输入上下文进行预测接口
python
def predict_center_word():
# 接收用户输入的4个上下文单词(空格分隔)
user_input = input("请输入4个上下文单词(空格分隔):")
context = user_input.split()
# 转换为索引张量
context_tensor = make_context_vector(context, word_to_idx)
# 模型切换到预测模式(关闭梯度追踪,节省资源)
model.eval()
with torch.no_grad():
# 预测对数概率
predict_log_prob = model(context_tensor)
# 取概率最大的索引(即预测的中心词索引)
# 注意:预测索引是张量,需用.item()转为Python整数
pred_idx = predict_log_prob.argmax(1).item()
# 索引转单词,输出结果
print(f"输入的上下文:{context}")
print(f"预测的中心词:{idx_to_word[pred_idx]}")
# 调用预测函数
predict_center_word()
