基于 PyTorch 的 CBOW 模型实现与讲解
在自然语言处理(NLP)任务中,词向量(Word Embedding)是非常核心的概念。通过词向量,模型能够将离散的词语映射到连续的低维空间中,使得语义相近的词也能在向量空间中距离更近。本文将通过一个 基于 CBOW(Continuous Bag of Words)模型 的小示例,带领大家一步步理解词向量的构建与训练过程。
CBOW(Continuous Bag-of-Words)模型详解
CBOW 是 Word2Vec (2013 年由 Google 的 Tomas Mikolov 团队提出)的两大核心模型之一(另一为 Skip-gram),其核心思想是通过 "上下文词汇" 来预测 "目标词汇",最终学习出能捕捉语义信息的低维词向量(Word Embedding)。它因训练速度快、对高频词友好的特点,在自然语言处理(NLP)基础任务中被广泛应用。
一、CBOW 的核心思想:"用上下文猜目标词"
CBOW 的名称中,"Continuous"(连续)对应 "低维连续的词向量","Bag-of-Words"(词袋)则表示不考虑上下文词汇的顺序(仅关注 "哪些词出现",不关注 "词出现的先后")。
其核心逻辑可通过一个简单例子理解:
假设句子为 "I love eating delicious food" ,若以 "eating" 为目标词(Target Word) ,选择前后各 2 个词作为上下文词(Context Words) (即 "I, love, delicious, food"),CBOW 模型的任务就是:
输入上下文词的向量 → 模型计算 → 输出对目标词 "eating" 的预测概率。通过大量类似句子的训练,模型会逐渐学会 "哪些上下文通常对应哪些目标词",并将这种语义关联编码到词向量中(例如 "delicious" 和 "food" 的词向量会更接近,因为它们常共同出现在 "eating" 的上下文里)。
二、CBOW 的模型结构(从输入到输出)
CBOW 的结构非常简洁,主要分为 输入层、投影层、输出层 三层,本质是一个 "多输入→单输出" 的浅层神经网络。
1. 输入层:上下文词的 "one-hot 编码"
- 输入内容 :选取的
C
个上下文词(例如前 2 后 2,共 4 个词)。- 编码方式 :采用 one-hot 向量 (独热编码)。假设整个词汇表大小为
V
,则每个词的 one-hot 向量维度为V×1
,向量中仅对应该词的位置为 1,其余均为 0。
例:若词汇表为 {I, love, eating, delicious, food, ...}(共 10000 个词),则 "love" 的 one-hot 向量为 [0, 1, 0, 0, 0, ..., 0](仅第 2 位为 1)。- 输入维度 :
C × V
(C
个上下文词,每个词是V×1
向量)。2. 投影层:上下文词向量的 "平均池化"
投影层的核心作用是将多个上下文词的向量融合为一个 "上下文总向量",步骤如下:
- 词向量 lookup(查表) :
模型会维护一个 嵌入矩阵(Embedding Matrix)W
,维度为V×N
(N
是我们预设的词向量维度,通常取 50~300)。
每个上下文词的 one-hot 向量与W
相乘(本质是 "查表"),可得到该词的低维词向量(维度N×1
)。
原理:one-hot 向量只有一个位置为 1,与W
相乘等价于提取W
中对应行的向量(即该词的词向量)。- 平均池化(Average Pooling) :
将C
个上下文词的词向量(每个N×1
)按元素求平均,得到一个 "上下文总向量" (维度仍为N×1
)。
这一步体现了 "Bag-of-Words" 的思想 ------ 不考虑上下文词的顺序,仅用平均值代表所有上下文的整体信息。
- 投影层输出维度 :
N×1
(N
为词向量维度)。3. 输出层:目标词的 "概率预测"
输出层的任务是根据上下文总向量,预测目标词在词汇表中的概率分布,步骤如下:
- 线性变换 :
引入第二个权重矩阵W'
(维度N×V
),将投影层输出的上下文总向量(N×1
)与W'
相乘,得到一个V×1
的向量(可理解为 "每个词汇作为目标词的原始得分")。- Softmax 激活 :
对线性变换后的V×1
向量应用 Softmax 函数,将原始得分转换为概率值(所有概率之和为 1)。最终概率最大的词汇,就是模型预测的 "目标词"。
- 输出层输出维度 :
V×1
(每个元素对应一个词汇作为目标词的概率)。三、CBOW 的训练过程:最小化 "预测误差"
训练的核心是通过调整嵌入矩阵
W
和权重矩阵W'
的参数,让模型对 "真实目标词" 的预测概率尽可能大。具体步骤如下:1. 定义 "损失函数":交叉熵损失
由于输出是 "词汇表上的概率分布",且任务是 "单类别预测"(只有一个真实目标词),通常使用 交叉熵损失(Cross-Entropy Loss) 来衡量 "预测分布" 与 "真实分布" 的差距。
假设真实目标词的 one-hot 向量为
y
(仅真实词位置为 1,其余为 0),模型预测的概率分布为ŷ
,则损失函数为:
L = -Σ(y_i × log(ŷ_i))
由于
y
是 one-hot 向量,只有真实目标词对应的y_i=1
,因此损失可简化为:
L = -log(ŷ_t)
(ŷ_t
是模型对 "真实目标词t
" 的预测概率)。损失越小,说明模型对真实目标词的预测越准确。
2. 反向传播:更新参数
通过 梯度下降法(Gradient Descent) 计算损失函数对
W
和W'
的梯度,然后沿 "梯度负方向" 更新参数,逐步降低损失。
- 对
W'
的梯度:直接关联输出层的误差,计算相对简单。- 对
W
的梯度:由于投影层做了 "平均池化",每个上下文词的词向量参数会被 "平均分配" 误差,因此所有上下文词的向量参数会同步更新(这也是 CBOW 训练速度快的原因之一)。3. 优化:解决 "词汇表过大" 的问题
若词汇表
V
很大(例如百万级),W'
的维度(N×V
)会非常大,导致 Softmax 计算和梯度更新效率极低。为解决这一问题,Word2Vec 提出了两种优化方案:
- Hierarchical Softmax(分层 Softmax) :
用 "二叉树" 替代传统 Softmax,将 "V 分类" 转化为 "log2 (V) 次二分类"(例如 V=100 万时,仅需 20 次二分类)。树的叶子节点对应词汇表中的词,内部节点对应 "中间分类器",大幅减少计算量。- Negative Sampling(负采样) :
不计算 "所有词汇的概率",而是将任务转化为 "二分类"------ 判断 "(上下文,真实目标词)" 是 "正样本"(符合语义的对),同时随机采样K
个 "(上下文,非目标词)" 作为 "负样本"(不符合语义的对),仅对这K+1
个样本计算损失并更新参数(K
通常取 5~20)。
负采样是更常用的优化方式,尤其在高频词较多的场景下效率更高。四、CBOW 与 Skip-gram 的核心区别
CBOW 和 Skip-gram 同属 Word2Vec,但核心逻辑和适用场景差异显著,具体对比如下:
对比维度 CBOW(Continuous Bag-of-Words) Skip-gram 核心任务 用 "上下文词" 预测 "目标词" 用 "目标词" 预测 "上下文词" 输入输出 多输入(C 个上下文词)→ 单输出(目标词) 单输入(目标词)→ 多输出(C 个上下文词) 训练速度 快(每次更新可处理 C 个上下文词的信息) 慢(每次更新需单独处理每个上下文词) 对高频词的友好度 好(高频词的上下文更稳定,预测更准确) 一般(高频词需重复训练,效率低) 对低频 / 罕见词的效果 差(低频词的上下文样本少,嵌入质量低) 好(可通过 "目标词→多个上下文" 生成更多样本) 适用场景 高频词多、词汇表大、追求训练效率的场景 低频词多、需要更精准语义嵌入的场景 五、CBOW 的优缺点
优点
- 训练效率高:通过 "多上下文→单目标" 的结构,一次更新可利用多个上下文词的信息,适合大规模语料。
- 高频词嵌入质量好:高频词的上下文模式更固定,模型能更稳定地学习其语义(例如 "the""is" 等高频功能词,虽语义简单,但 CBOW 能捕捉其 "连接上下文" 的作用)。
- 词向量泛化性强:学习到的词向量能捕捉 "语义相似性"(例如 "猫" 和 "狗" 的向量距离近)和 "语法相似性"(例如 "run" 和 "running" 的向量关系类似 "eat" 和 "eating")。
缺点
- 忽略上下文顺序:因 "词袋" 特性,无法区分 "我打他" 和 "他打我" 这类 "上下文词相同但顺序不同" 的句子,丢失了语序信息(后续的 CNN、RNN 等模型可弥补这一缺陷)。
- 低频词效果差:低频词(如专业术语、生僻词)的上下文样本少,模型难以学习到稳定的语义,嵌入向量质量较低。
- 依赖 "窗口大小" 选择:上下文窗口(即选取多少个上下文词)需人工预设(通常取 1~5),窗口过小会丢失长距离语义,过大则引入冗余信息。
六、CBOW 的应用场景
CBOW 学习的词向量是 NLP 任务的 "基础组件",主要用于以下场景:
- 下游 NLP 任务的初始化:为分类、情感分析、机器翻译等任务的 "词嵌入层" 提供预训练参数(避免从零开始训练,提升效果和速度)。
- 语义相似度计算:直接通过 "词向量的余弦相似度" 衡量两个词的语义接近程度(例如 "国王" 与 "王后" 的相似度高于 "国王" 与 "苹果")。
- 词汇聚类 / 分类:对词向量进行聚类(如 K-Means),可将语义相似的词归为一类(例如 "篮球""足球""网球" 会被聚为 "球类运动")。
- 简单的文本表示:将文本中所有词的向量求平均,作为文本的 "句向量",用于快速的文本检索、相似度匹配等场景。
总结
CBOW 是一种 "简单高效" 的词向量学习模型,其核心是 "用上下文预测目标词",通过浅层神经网络和优化算法(分层 Softmax / 负采样)实现高效训练。尽管它忽略了语序信息,对低频词效果有限,但凭借 "训练快、高频词嵌入质量高" 的优势,至今仍是 NLP 领域学习基础词向量的重要方法之一,也是理解 "词嵌入思想" 的核心入门模型
1. 数据准备
首先,我们准备一段原始文本作为语料。文本内容来自 计算机程序的本质 的开篇:
import torch
from torch import nn
from tqdm import tqdm
import torch.nn.functional as F
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()
在自然语言处理中,我们通常需要将句子拆分为单词序列,然后再进行建模。这里的 split()
就是完成这个任务的。
接着,定义上下文窗口大小 CONTEXT_SIZE=2
,表示预测一个词时,会使用左右各两个词作为上下文。
CONTEXT_SIZE=2
vocab=set(raw_text)
vocab_size = len(vocab)
word_to_idx= {word: idx for idx, word in enumerate(vocab)}
idx_to_word= {idx:word for idx, word in enumerate(vocab)}
这里我们先生成词汇表(vocabulary),并建立 单词到索引(word_to_idx) 与 索引到单词(idx_to_word) 的映射关系。
2. 构造训练数据
CBOW 的训练数据由 上下文(context) 和 目标词(target) 组成。例如,在句子 "People create programs to direct processes" 中,如果预测 "programs",那么它的上下文就是 ["People", "create", "to", "direct"]
。
data=[]
for i in range(CONTEXT_SIZE,len(raw_text)-CONTEXT_SIZE):
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))
print(data)
这样,每一个样本都会以 (上下文, 目标词)
的形式存储。
3. 上下文向量化
深度学习模型无法直接处理单词,因此需要将上下文转化为对应的索引张量。
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))
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")
例如输入 ["People", "create", "to", "direct"]
,就会输出对应的索引向量。
4. 定义 CBOW 模型
CBOW 的核心思想是:
给定上下文词,预测目标词。
在实现上,模型结构大致如下:
-
Embedding 层:将词索引映射为低维向量。
-
Linear + ReLU + Linear:通过全连接层对词向量进行变换。
-
Softmax:输出每个词作为目标词的概率分布。
class CBOW(nn.Module):
def init(self, vocab_size, embedding_dim):
super(CBOW, self).init()
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
self.proj = nn.Linear(embedding_dim, 128)
self.output = nn.Linear(128, vocab_size)def forward(self, inputs): embeds = sum(self.embeddings(inputs)).view(1, -1) out = F.relu(self.proj(embeds)) out = self.output(out) nll_prob = F.log_softmax(out, dim=-1) return nll_prob
注意这里对上下文词向量取了 求和操作,这正是 CBOW 的特点:将上下文词的嵌入相加后作为输入。
5. 模型训练
训练时,我们采用 负对数似然损失(NLLLoss),优化器为 Adam。
model = CBOW(vocab_size, 10).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
loss_function = nn.NLLLoss()
通过循环迭代,模型不断更新参数,使得目标词的预测概率越来越高。
losses = [] # 存储损失的集合
model.train()
for epoch in tqdm(range(200)):
total_loss = 0
for context, target in data:
context_vector = make_context_vector(context, word_to_idx).to(device)
target = torch.tensor([word_to_idx[target]]).to(device)
train_predict = model(context_vector) # 前向传播
loss = loss_function(train_predict, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
# print(total_loss)
losses.append(total_loss)
print("=====losses=====",losses)
6. 模型测试与词向量提取
训练完成后,可以输入新的上下文进行预测:
context = ['People', 'create', 'to', 'direct']
context_vector = make_context_vector(context, word_to_idx).to(device)
model.eval()
predict = model(context_vector)
max_idx = predict.argmax(1)
此外,最重要的是 Embedding 层权重 就是我们得到的词向量。
# 获取词向量,这个Embedding就是我们需要的词向量,他只是一个模型的一个中间过程
print("CBOW embedding'weight=", model.embeddings.weight) # GPU
W = model.embeddings.weight.cpu().detach().numpy()
# 这意味着这个新的Tensor不会参与梯度的反向传播,这对于防止在
print(W)
# 生成词嵌入字典,即{单词1:词向量1, 单词2:词向量2...}的格式
word_2_vec = {}
for word in word_to_idx.keys():
# 词向量矩阵中某个词的索引所对应的那一列即为该词的词向量
word_2_vec[word] = W[word_to_idx[word], :]
print('Done')
此时,每个单词都被表示为一个固定维度的向量(这里是 10 维)。
7. 保存数据示例
最后,代码还演示了如何使用 NumPy 保存矩阵数据:
import numpy as np
a = np.random.randint(5, size=(2, 4))
np.save('test.npy', a)
这段代码会生成一个 test.npy
文件,用于存储一个随机整数数组。
8. 总结
本文通过一个简单的例子,展示了 CBOW 模型的核心流程:
-
构造上下文-目标词训练数据;
-
使用嵌入层将单词映射为向量;
-
通过前向传播预测目标词;
-
通过反向传播优化词向量;
-
提取训练好的词向量,用于后续 NLP 任务。
虽然示例规模很小,但这正是 Word2Vec CBOW 模型 的核心思想。