深度学习——基于 PyTorch 的 CBOW 模型实现自然语言处理


基于 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 × VC 个上下文词,每个词是 V×1 向量)。

2. 投影层:上下文词向量的 "平均池化"

投影层的核心作用是将多个上下文词的向量融合为一个 "上下文总向量",步骤如下:

  1. 词向量 lookup(查表)
    模型会维护一个 嵌入矩阵(Embedding Matrix)W ,维度为 V×NN 是我们预设的词向量维度,通常取 50~300)。
    每个上下文词的 one-hot 向量与 W 相乘(本质是 "查表"),可得到该词的低维词向量(维度 N×1)。
    原理:one-hot 向量只有一个位置为 1,与 W 相乘等价于提取 W 中对应行的向量(即该词的词向量)。
  2. 平均池化(Average Pooling)
    C 个上下文词的词向量(每个 N×1)按元素求平均,得到一个 "上下文总向量" (维度仍为 N×1)。
    这一步体现了 "Bag-of-Words" 的思想 ------ 不考虑上下文词的顺序,仅用平均值代表所有上下文的整体信息。
  • 投影层输出维度N×1N 为词向量维度)。

3. 输出层:目标词的 "概率预测"

输出层的任务是根据上下文总向量,预测目标词在词汇表中的概率分布,步骤如下:

  1. 线性变换
    引入第二个权重矩阵 W'(维度 N×V),将投影层输出的上下文总向量(N×1)与 W' 相乘,得到一个 V×1 的向量(可理解为 "每个词汇作为目标词的原始得分")。
  2. 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) 计算损失函数对 WW' 的梯度,然后沿 "梯度负方向" 更新参数,逐步降低损失。

  • 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 的优缺点

优点

  1. 训练效率高:通过 "多上下文→单目标" 的结构,一次更新可利用多个上下文词的信息,适合大规模语料。
  2. 高频词嵌入质量好:高频词的上下文模式更固定,模型能更稳定地学习其语义(例如 "the""is" 等高频功能词,虽语义简单,但 CBOW 能捕捉其 "连接上下文" 的作用)。
  3. 词向量泛化性强:学习到的词向量能捕捉 "语义相似性"(例如 "猫" 和 "狗" 的向量距离近)和 "语法相似性"(例如 "run" 和 "running" 的向量关系类似 "eat" 和 "eating")。

缺点

  1. 忽略上下文顺序:因 "词袋" 特性,无法区分 "我打他" 和 "他打我" 这类 "上下文词相同但顺序不同" 的句子,丢失了语序信息(后续的 CNN、RNN 等模型可弥补这一缺陷)。
  2. 低频词效果差:低频词(如专业术语、生僻词)的上下文样本少,模型难以学习到稳定的语义,嵌入向量质量较低。
  3. 依赖 "窗口大小" 选择:上下文窗口(即选取多少个上下文词)需人工预设(通常取 1~5),窗口过小会丢失长距离语义,过大则引入冗余信息。

六、CBOW 的应用场景

CBOW 学习的词向量是 NLP 任务的 "基础组件",主要用于以下场景:

  1. 下游 NLP 任务的初始化:为分类、情感分析、机器翻译等任务的 "词嵌入层" 提供预训练参数(避免从零开始训练,提升效果和速度)。
  2. 语义相似度计算:直接通过 "词向量的余弦相似度" 衡量两个词的语义接近程度(例如 "国王" 与 "王后" 的相似度高于 "国王" 与 "苹果")。
  3. 词汇聚类 / 分类:对词向量进行聚类(如 K-Means),可将语义相似的词归为一类(例如 "篮球""足球""网球" 会被聚为 "球类运动")。
  4. 简单的文本表示:将文本中所有词的向量求平均,作为文本的 "句向量",用于快速的文本检索、相似度匹配等场景。

总结

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 模型的核心流程

  1. 构造上下文-目标词训练数据;

  2. 使用嵌入层将单词映射为向量;

  3. 通过前向传播预测目标词;

  4. 通过反向传播优化词向量;

  5. 提取训练好的词向量,用于后续 NLP 任务。

虽然示例规模很小,但这正是 Word2Vec CBOW 模型 的核心思想。

相关推荐
Wiktok2 小时前
[Wit]CnOCR模型训练全流程简化记录(包括排除BUG)
python·深度学习·bug
胡乱编胡乱赢3 小时前
关于在pycharm终端连接服务器
人工智能·深度学习·pycharm·终端连接服务器
盼小辉丶4 小时前
DenseNet详解与实现
深度学习·keras·tensorflow2
东方佑5 小时前
当人眼遇见神经网络:用残差结构模拟视觉调焦的奇妙类比
人工智能·深度学习·神经网络
智驱力人工智能5 小时前
深度学习在离岗检测中的应用
人工智能·深度学习·安全·视觉检测·离岗检测
hjs_deeplearning5 小时前
认知篇#12:基于非深度学习方法的图像特征提取
人工智能·深度学习·目标检测
阿杜杜不是阿木木6 小时前
开始 ComfyUI 的 AI 绘图之旅-Flux.1 ControlNet (十)
人工智能·深度学习·ai·ai作画·lora
victory04316 小时前
疾病语音数据集 WAV格式音频
深度学习·音视频
chanalbert7 小时前
信息检索技术综述:从传统稀疏检索到现代深度学习方法
人工智能·深度学习·全文检索