一、引言:让计算机理解词语
在之前的博客中,我们学习了CNN处理图像、迁移学习复用预训练模型。但有一类数据比图像更贴近人类的日常——文本。
然而,计算机不认识苹果、香蕉这些词,它只认识数字。如何把词语变成计算机能处理的数字?如何让计算机理解苹果和香蕉都是水果,而苹果和汽车关系较远?
这就是自然语言处理(NLP) 的核心问题之一——词嵌入(Word Embedding)。
词嵌入是将词语映射到低维稠密向量空间的技术。在这个空间里,语义相近的词距离更近。比如,猫和狗的向量会很接近,而"猫"和"汽车"的向量则相距较远。
本篇博客将基于PyTorch,从零实现一个CBOW(Continuous Bag of Words) 模型,学习词嵌入,并完成一个单词预测系统。同时,我们还会学习如何使用NumPy保存和加载词向量文件。
二、什么是CBOW
2.1 CBOW的核心思想
CBOW是Word2Vec的两种训练方式之一(另一种是Skip-gram)。它的核心思想非常直观:
用上下文预测中心词
给定一句话:"We are about to study the idea of a computational process"
如果中心词是"study",上下文窗口大小为2,那么:
-
上下文词:["about", "to", "the", "idea"]
-
目标词:"study"
CBOW模型接收上下文词作为输入,输出对中心词的预测。训练完成后,模型学到的词向量(embedding权重)就包含了词语的语义信息。
2.2 CBOW vs Skip-gram
| 对比项 | CBOW | Skip-gram |
|---|---|---|
| 输入 | 上下文词 | 中心词 |
| 输出 | 中心词 | 上下文词 |
| 训练速度 | 快 | 慢 |
| 小数据集表现 | 好 | 一般 |
| 罕见词表现 | 一般 | 好 |
三、数据准备
3.1 加载文本与构建词汇表
CONTEXT_SIZE = 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()
# 构建词汇表
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)}
关键点:
-
raw_text.split():按空格分词,得到单词列表 -
set(raw_text):去重,得到词汇表 -
word_to_idx和idx_to_word:实现词与索引的相互转换
3.2 构建训练数据
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))
逻辑解析:
-
遍历每个可以作为中心词的位置(从第2个到倒数第2个)
-
context:取中心词前后各2个词,共4个上下文词 -
target:中心词 -
每个样本为
(context, target)的形式
示例:对于 "We are about to study the idea..."
i=4("study"):context = ["about", "to", "the", "idea"],target = "study"
3.3 上下文向量转换
def make_context_vector(text, word_to_ix):
idxs = [word_to_ix[w] for w in text]
return torch.tensor(idxs, dtype=torch.long)
将上下文单词列表转为索引张量,作为模型输入。
四、CBOW模型定义
import torch
import torch.nn as nn
import torch.nn.functional as F
class CBOW(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(CBOW, self).__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim) # 词嵌入层
self.proj = nn.Linear(embedding_dim, 120) # 投影层
self.output = nn.Linear(120, vocab_size) # 输出层
def forward(self, inputs):
# 1. 获取上下文词的嵌入向量并求平均
embeds = sum(self.embedding(inputs)).view(1, -1) / len(inputs)
# 2. 投影 + ReLU激活
out = F.relu(self.proj(embeds))
# 3. 输出层
out = self.output(out)
# 4. log_softmax(配合NLLLoss使用)
nll_prob = F.log_softmax(out, dim=-1)
return nll_prob
逐层解析:
| 层 | 作用 | 输入维度 | 输出维度 |
|---|---|---|---|
embedding | 将词索引映射为稠密向量 | vocab_size | embedding_dim |
proj | 投影到隐藏层 | embedding_dim | 120 |
output | 输出每个词的概率 | 120 | vocab_size |
关键操作:
-
self.embedding(inputs):输入4个上下文词的索引,得到4个嵌入向量 -
sum(...) / len(inputs):对4个向量求平均,得到上下文表示 -
.view(1, -1):调整形状为 (1, embedding_dim) -
F.log_softmax(out, dim=-1):将输出转为对数概率分布
五、模型训练
device = "cuda" if torch.cuda.is_available() else "cpu"
model = CBOW(vocab_size, 10).to(device)
optimizer = optim.Adam(model.parameters(), lr=0.001)
losses = []
loss_fn = nn.NLLLoss()
model.train()
for epoch in tqdm(range(200)):
total_loss = 0
for context, target in data:
context_vec = make_context_vector(context, word_to_idx).to(device)
target = torch.tensor([word_to_idx[target]]).to(device)
train_pred = model(context_vec)
loss = loss_fn(train_pred, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
losses.append(total_loss)
训练流程:
-
将上下文转为索引张量
-
前向传播,得到预测分布
-
计算NLL损失
-
梯度清零、反向传播、更新参数
-
累加损失,记录每个epoch的总损失
tqdm的作用:显示训练进度条,方便观察训练状态。
六、词嵌入提取与保存
6.1 提取词向量
训练完成后,model.embedding.weight 就是所有词的嵌入矩阵:
model.eval()
W = model.embedding.weight.cpu().detach().numpy()
word_2_vec = {}
for word in word_to_idx.keys():
word_2_vec[word] = W[word_to_idx[word], :]
W的形状:(vocab_size, embedding_dim),每行对应一个词的向量。
6.2 保存为NPZ文件
np.savez("word_2_vec.npz", file1=W)
data = np.load("word_2_vec.npz")
print(data)
七、NumPy文件保存与加载
代码中还包含了两个演示NumPy文件操作的示例。
7.1 保存单个数组(.npy)
import numpy as np
a = np.random.randint(5, size=(2, 4))
np.save("test.npy", a)
b = np.load("test.npy")
print(b)
特点:
-
.npy格式保存单个数组 -
加载后直接得到数组
7.2 保存多个数组(.npz)
a = np.random.randint(0, 10, (3,), dtype="int")
b = np.random.randint(0, 10, (3,), dtype="int")
c = np.random.randint(0, 10, (3,), dtype="int")
np.savez("test.npz", a=a, b=b, c=c)
data = np.load("test.npz")
print(data.files) # ['a', 'b', 'c']
aa = data[data.files[0]]
bb = data[data.files[1]]
cc = data[data.files[2]]
特点:
-
.npz格式可保存多个命名数组 -
加载后类似字典,通过
data.files查看所有键 -
通过
data['key']获取对应数组
7.3 加载预训练词向量
d = np.load("../embedding_Tencent.npz")
e = d["embeddings"]
print("Tencent的词向量为:", e)
八、实战:单词预测系统
nlp_work.py 将CBOW模型应用到一个实际任务——给定连续4个单词,预测第5个单词。
8.1 任务描述
读取 AI报道.txt,训练模型后:
-
输入:"the rise of AI"
-
输出:"also"
8.2 代码要点
CONTEXT_SIZE = 5
with open('AI报道.txt', 'r') as f:
raw_text = f.readline().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-(5-j)] for j in range(CONTEXT_SIZE)]
target = raw_text[i]
data.append((context, target))
8.3 预测
context = ["the", "rise", "of", "AI"]
context_vector = make_context_vector(context, word_to_idx).to(device)
model.eval()
pred = model(context_vector)
max_idx = pred.argmax(1)
print(idx_to_word[int(max_idx)])
argmax(1):取输出概率最大的索引,即模型认为最可能的下一个词。
九、总结
本篇博客围绕CBOW词嵌入,系统讲解了:
| 知识点 | 核心内容 |
|---|---|
| 词嵌入 | 将词语映射为稠密向量 |
| CBOW模型 | 用上下文预测中心词 |
| Embedding层 | nn.Embedding 实现词向量查表 |
| NLLLoss | 配合 log_softmax 计算分类损失 |
| 词向量提取 | model.embedding.weight |
| NumPy保存 | .npy 单数组,.npz 多数组 |
| 单词预测 | 输入4个词,输出第5个词 |

2113

被折叠的 条评论
为什么被折叠?



