NLP 入门——PyTorch 从零实现 Word2Vec 之 CBOW 模型

一、引言:自然语言处理的核心难题 ------ 语言到数值的转换

在自然语言处理(NLP)任务中,我们面临的第一个核心问题就是:如何将人类的自然语言,转换为计算机模型可以直接识别、处理的数值?

语言是人类的抽象表达,而模型只能处理结构化的数值数据。因此,语言转换方法是所有 NLP 任务的基础,它的优劣直接决定了后续模型的效果上限。

二、传统方案:统计语言模型的局限

  • 在深度学习兴起之前,主流的语言转换方法是统计语言模型 ,其中最具代表性的就是基于CountVectorizer的词频统计方法。

    2.1 统计语言模型的核心思路

    统计语言模型的核心逻辑非常直观:

  • 对语料进行分词,统计所有不重复的词汇,构建词表;

  • ngram(n 元语法)对词汇进行组合,捕捉局部上下文关系;

  • 统计每个词汇 / 词组在文本中的出现频率,将文本转换为词向量,输入模型。

python 复制代码
from sklearn.feature_extraction.text import CountVectorizer

# 示例语料
texts = ["dog cat fish","dog cat cat","fish bird", 'bird']
# 实例化模型,ngram_range=(1,2)表示1元组和2元组组合
cv = CountVectorizer(ngram_range=(1,2))
# 训练模型并转换
cv_fit=cv.fit_transform(texts)
# 输出词库
print(cv.get_feature_names_out())
# 输出词频向量
print(cv_fit.toarray())

2.2 统计语言模型的致命缺陷

尽管统计方法简单易实现,但它存在两个无法解决的核心问题:

  1. 参数爆炸,无法处理长序列 :当n>3时,词汇组合的数量会呈指数级增长,导致维度灾难,完全无法在实际场景中使用;
  2. 完全丢失语义关联:统计模型只关注词的出现频率,完全不考虑词与词之间的内在语义联系。例如,它无法理解 "cat" 和 "dog" 都是动物,"walking" 和 "running" 都是动作,无法从相似词汇中泛化语义。

因此,统计语言模型逐渐被更先进的神经语言模型所取代。

三、神经语言模型:词嵌入(Word Embedding)的诞生

为了解决统计模型的缺陷,研究者提出了 ** 词嵌入(Word Embedding)** 技术,它的核心目标是:将高维稀疏的 one-hot 编码,映射为低维稠密的语义向量,同时保留词汇的语义信息

3.1 从 one-hot 到词嵌入:解决维度灾难

在词嵌入出现之前,最基础的文本向量化方法是one-hot编码:

  • 假设词表大小为V(例如 4960 个词),每个词对应一个长度为V的向量,其中只有一个位置为 1,其余全为 0;
  • 例如句子 "我爱北京天安门",分词后每个字的 one-hot 编码长度都是 4960,最终输入模型的矩阵维度为4*4960

one-hot 编码的问题非常明显:矩阵极度稀疏,维度灾难,且完全无语义信息

而词嵌入通过神经网络训练,将每个高维的 one-hot 向量,映射为一个低维的稠密向量(通常为 300 维):

  • 同样的句子,转换后矩阵维度仅为4*300,维度大幅降低;
  • 向量中的值不再是 0 和 1,而是可以表达语义的浮点数,语义相似的词,在向量空间中的距离也更近(例如 "西瓜" 和 "苹果" 在向量空间中距离很近,而 "篮球" 和 "乒乓球" 距离更近)。

这种高维稀疏→低维稠密的转换方法,就是词嵌入(Word Embedding)。

3.2 Word2Vec:词嵌入的经典实现

2013 年,Google 团队提出了Word2Vec模型,它是词嵌入技术的里程碑式成果,包含两个经典的训练模型:

  1. CBOW(Continuous Bag-of-Words,连续词袋模型)以上下文词汇预测当前词 ,即通过w_{t-2}, w_{t-1}, w_{t+1}, w_{t+2}预测w_t
  2. Skip-gram以当前词预测其上下文词汇 ,即通过w_t预测w_{t-2}, w_{t-1}, w_{t+1}, w_{t+2}

两者的核心结构对比如下:

|-----------|-------|--------|----------------|
| 模型 | 输入 | 输出 | 适用场景 |
| CBOW | 上下文词汇 | 中心词 | 语料量较小,训练速度快 |
| Skip-gram | 中心词 | 上下文词汇语 | 语料量较大,对稀有词效果更好 |

四、CBOW 模型的原理与训练流程

我们以代码中实现的 CBOW 模型为例,详细拆解其原理与训练过程。

4.1 CBOW 的网络结构

CBOW 是一个简单的三层神经网络:

  1. 输入层 :上下文词汇的 one-hot 编码,维度为C*VC为上下文窗口大小,V为词表大小);
  2. 隐藏层(投影层) :通过共享权重矩阵W_{V×N},将每个 one-hot 向量映射为N维的词向量,然后对所有上下文词向量取平均,得到1×N的向量;
  3. 输出层 :通过权重矩阵W'_{N×V},将投影层向量转换为1×V的向量,再通过softmax归一化,得到每个词作为中心词的概率。

4.2 CBOW 的完整训练流程

  1. 输入:上下文词汇的 one-hot 编码;
  2. 投影 :所有上下文词分别乘以共享矩阵W_{V×N},得到各自的1×N词向量,取平均得到1×N的投影向量;
  3. 输出 :投影向量乘以矩阵W'_{N×V},得到1×V的输出向量;
  4. 归一化 :通过softmax将输出转换为概率分布;
  5. 误差计算:用预测概率与真实标签(中心词的 one-hot 编码)计算损失;
  6. 反向传播 :通过梯度下降,反向更新矩阵WW'
  7. 迭代收敛 :重复上述过程,直到模型收敛,最终矩阵W就是我们需要的词嵌入矩阵。

五、PyTorch 从零实现 CBOW 与 Word2Vec

下面我们结合代码,一步步实现 CBOW 模型,完成词向量的训练与保存。

5.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个词
EMBEDDING_DIM = 10  # 词向量维度

# 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()

# 3. 构建词表与映射
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)}

# 4. 构建训练数据集:(上下文, 目标词)
data = []
for i in range(CONTEXT_SIZE, len(raw_text) - CONTEXT_SIZE):
    # 取当前词左边2个 + 右边2个 作为上下文
    context = [raw_text[i - 2], raw_text[i - 1], raw_text[i + 1], raw_text[i + 2]]
    target = raw_text[i]  # 当前词作为目标
    data.append((context, target))

# 5. 上下文转张量工具函数
def make_context_vector(context, word_to_ix):
    idxs = [word_to_ix[w] for w in context]
    return torch.tensor(idxs, dtype=torch.long)

5.2 定义 CBOW 神经网络模型

接下来,我们定义 CBOW 的网络结构,核心是nn.Embedding层(即权重矩阵W):

python 复制代码
# 设备配置:优先使用GPU/MPS加速
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"使用设备: {device}")

class CBOW(nn.Module):
    def __init__(self, vocab_size, embedding_dim):
        super(CBOW, self).__init__()
        # 词嵌入层:对应权重矩阵W,将one-hot映射为低维词向量
        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):
        # 对所有上下文词的词向量求和取平均
        embds = sum(self.embeddings(inputs)).view(1, -1)
        # 激活层
        out = F.relu(self.proj(embds))
        # 输出层
        out = self.output(out)
        # log_softmax归一化,配合NLLLoss使用
        nll_prob = F.log_softmax(out, dim=-1)
        return nll_prob

# 实例化模型
model = CBOW(vocab_size, EMBEDDING_DIM).to(device)

5.3 模型训练:损失函数与优化器

我们使用NLLLoss(负对数似然损失)作为损失函数,配合log_softmax实现多分类,用 Adam 优化器更新参数:

python 复制代码
# 优化器与损失函数
optimizer = 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()
    losses.append(total_loss)
    if (epoch + 1) % 20 == 0:
        print(f"Epoch {epoch+1}/200, Loss: {total_loss:.4f}")

5.4 模型测试与词向量提取

训练完成后,我们可以测试模型的预测效果,并提取最终的词嵌入矩阵:

python 复制代码
# 测试:用上下文预测中心词
context = ['People', 'create', 'to', 'direct']
context_vector = make_context_vector(context, word_to_idx).to(device)

model.eval()
with torch.no_grad():
    predict = model(context_vector)
max_idx = predict.argmax(1).item()
print(f"上下文: {context}, 预测中心词: {idx_to_word[max_idx]}")

# 提取词嵌入矩阵
print("CBOW词嵌入权重:")
print(model.embeddings.weight)

# 转换为numpy数组
W = model.embeddings.weight.cpu().detach().numpy()
print(W)

# 构建{单词:词向量}字典
word_2_vec = {}
for word in word_to_idx.keys():
    word_2_vec[word] = W[word_to_idx[word], :]

# 保存词向量为npz文件,方便后续使用
np.savez('word2vec实现.npz', word_vectors=W, word_to_idx=word_to_idx)
# 加载验证
data = np.load('word2vec实现.npz', allow_pickle=True)
print("保存的文件内容:", data.files)

六、补充:损失函数与 softmax 的原理

在多分类任务中,我们使用softmax交叉熵损失来计算模型误差,核心公式如下:

  • softmax的作用是将模型的原始输出转换为 0-1 之间的概率分布,保证所有类别概率之和为 1;
  • -log的原因是:当预测正确时,概率p接近 1,-log(p)接近 0,损失值小;当预测错误时,概率p接近 0,-log(p)趋近于无穷大,损失值大,完美符合损失函数的需求。

八、完整代码

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

# 超参数设置
CONTEXT_SIZE = 2
EMBEDDING_DIM = 10

# 加载语料
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()

# 构建词表
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)}

# 构建数据集
data = []
for i in range(CONTEXT_SIZE, len(raw_text) - CONTEXT_SIZE):
    context = [raw_text[i-2], raw_text[i-1], raw_text[i+1], raw_text[i+2]]
    target = raw_text[i]
    data.append((context, target))

# 上下文转张量
def make_context_vector(context, word_to_ix):
    idxs = [word_to_ix[w] for w in context]
    return torch.tensor(idxs, dtype=torch.long)

# 设备配置
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"使用设备: {device}")

# 定义CBOW模型
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):
        embds = sum(self.embeddings(inputs)).view(1, -1)
        out = F.relu(self.proj(embds))
        out = self.output(out)
        return F.log_softmax(out, dim=-1)

# 实例化模型
model = CBOW(vocab_size, EMBEDDING_DIM).to(device)

# 优化器与损失函数
optimizer = 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)

        # 前向传播
        pred = model(context_vector)
        loss = loss_function(pred, target)

        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()
    losses.append(total_loss)
    if (epoch + 1) % 20 == 0:
        print(f"Epoch {epoch+1}/200, Loss: {total_loss:.4f}")

# 测试
context = ['People', 'create', 'to', 'direct']
context_vector = make_context_vector(context, word_to_idx).to(device)
model.eval()
with torch.no_grad():
    pred = model(context_vector)
max_idx = pred.argmax(1).item()
print(f"\n测试结果:上下文{context},预测中心词:{idx_to_word[max_idx]}")

# 提取词向量
W = model.embeddings.weight.cpu().detach().numpy()
word_2_vec = {word: W[word_to_idx[word], :] for word in word_to_idx.keys()}

# 保存词向量
np.savez('word2vec实现.npz', word_vectors=W, word_to_idx=word_to_idx)
data = np.load('word2vec实现.npz', allow_pickle=True)
print("\n词向量保存成功,文件内容:", data.files)
相关推荐
郝学胜-神的一滴3 小时前
PyTorch张量维度操控:transpose与permute深度拆解与实战指南
人工智能·pytorch·python·深度学习·算法·机器学习
星川皆无恙4 小时前
Transformer学习笔记:从 Attention 核心理论到机器翻译代码项目学习实战
大数据·pytorch·笔记·深度学习·神经网络·自然语言处理·transformer
dazzle6 小时前
机器学习算法原理与实践-入门(十一):基于PyTorch的房价预测实战
pytorch·算法·机器学习
古希腊掌管代码的神THU19 小时前
【清华代码熊】RL后训练解析|Cursor Composer 2 技术报告
人工智能·深度学习·自然语言处理·composer
renhongxia121 小时前
大模型Prompt实战:精准生成专业技术文档
人工智能·微服务·语言模型·自然语言处理·机器人·prompt
白小筠1 天前
Pytorch之张量的基本操作
人工智能·pytorch·python
低调小一1 天前
Harness Engineering:把大语言模型从“能说”变成“能稳定交付”的四层系统
人工智能·语言模型·自然语言处理·harness
Legend NO241 天前
非结构化数据分析的核心:自然语言处理技术体系
自然语言处理·数据挖掘·数据分析
热爱生活的猴子1 天前
PyTorch导出ONNX报错(ShapeInferenceError)问题笔记(含dynamo=False作用解析)
人工智能·pytorch·笔记