在自然语言处理(NLP)领域,词向量是非常基础且重要的概念,它能将文本中的词语转化为计算机可理解的数值向量。而 CBOW(Continuous Bag-of-Words)模型是获取词向量的经典方法之一,今天我们就来详细介绍如何使用 PyTorch 框架实现 CBOW 模型,并训练得到词向量。
一、CBOW 模型原理简介
CBOW 模型的核心思想是根据上下文词语来预测中间的目标词语。比如在句子 “People create programs to direct processes” 中,如果我们把 “programs” 作为目标词,那么它的上下文可以是 “People”“create”“to”“direct”(这里我们设置上下文窗口大小为 2,即目标词左右各 2 个词)。模型通过学习这种上下文与目标词之间的关系,最终得到每个词语对应的词向量。
二、代码实现步骤详解
1. 导入所需库
首先,我们需要导入实现模型所需的各种库,包括 PyTorch 相关库用于构建和训练模型、numpy 用于数据处理、tqdm 用于显示训练进度等。
import torch.optim as optim
import numpy as np
import torch
import torch.nn.functional as F
from torch import nn
from tqdm import tqdm
2. 数据预处理
数据预处理是模型训练的基础,这一步我们需要准备训练数据,包括确定上下文窗口大小、处理原始文本、构建词汇表以及创建词语与索引之间的映射关系。
(1)设置上下文窗口大小和原始文本
我们设置上下文窗口大小CONTEXT_SIZE = 2,意味着每个目标词的上下文是其左右各 2 个词。然后定义一段原始文本,用于后续的模型训练。
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()
这里使用split()方法将原始文本按空格分割成单词列表。
(2)构建词汇表和映射关系
词汇表是所有不重复单词的集合,我们通过set(raw_text)获取。然后分别创建单词到索引(word_to_idx)和索引到单词(idx_to_word)的映射字典,方便后续将单词转换为模型可处理的数值形式。
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)}
(3)生成训练数据
根据上下文窗口大小,我们遍历原始文本单词列表,为每个可能的目标词构建对应的上下文和目标词对,组成训练数据data。
data = []
for i in range(CONTEXT_SIZE, len(raw_text) - CONTEXT_SIZE):
# 构建上下文:左边2个词 + 右边2个词
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)) # 添加到训练数据
例如,当i取合适的值时,context就是目标词raw_text[i]左右各 2 个词组成的列表,target就是中间的目标词。
(4)定义上下文向量转换函数
该函数用于将上下文单词列表转换为对应的索引张量,以便输入到模型中。
def make_context_vector(context, word_to_idx):
idxs = [word_to_idx[w] for w in context]
return torch.tensor(idxs, dtype=torch.long)
我们可以通过以下代码测试该函数是否正常工作:
print(make_context_vector(data[0][0], word_to_idx))
3. 配置训练设备
为了提高模型训练速度,我们优先使用 GPU 进行训练,如果没有 GPU 则使用 CPU(这里也考虑了苹果设备的 MPS 加速)。
device = 'cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu'
print(device)
执行该代码后,会输出当前使用的训练设备。
4. 构建 CBOW 模型
使用 PyTorch 的nn.Module类来构建 CBOW 模型,模型主要包括嵌入层(embeddings)、投影层(proj)和输出层(output)。
(1)模型初始化
在__init__方法中定义模型的各个层:
- 嵌入层:将单词的索引转换为指定维度的词向量,nn.Embedding(vocab_size, embedding_dim)中vocab_size是词汇表大小,embedding_dim是词向量的维度。
- 投影层:对嵌入层的输出进行线性变换,这里设置输出维度为 128。
- 输出层:将投影层的输出转换为词汇表大小维度的向量,用于后续计算每个单词作为目标词的概率。
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) # 输出层
(2)前向传播
在forward方法中定义模型的前向传播过程:
- 首先,通过嵌入层获取上下文单词的词向量,并对这些词向量求和。
- 然后,将求和后的结果通过投影层,并使用 ReLU 激活函数进行非线性变换。
- 最后,将投影层的输出通过输出层,再经过log_softmax函数计算每个单词的对数概率,方便后续计算损失。
def forward(self, inputs):
embeds = sum(self.embeddings(inputs)).view(1, -1) # 对上下文词嵌入求和
out = F.relu(self.proj(embeds)) # 通过投影层并激活
out = self.output(out) # 输出层
nll_probs = F.log_softmax(out, dim=-1) # 对数softmax
return nll_probs
(3)初始化模型
指定词向量维度为 10,并将模型移动到之前配置好的训练设备上。
model = CBOW(vocab_size, 10).to(device)
5. 配置优化器和损失函数
- 优化器:这里使用 Adam 优化器,学习率设置为 0.001,用于更新模型参数以最小化损失。
- 损失函数:由于模型输出的是对数概率,我们使用负对数似然损失函数(nn.NLLLoss())来计算模型的损失。
optimizer = optim.Adam(model.parameters(), lr=0.001)
loss_function = nn.NLLLoss()
6. 模型训练
设置训练轮数为 200,使用tqdm显示训练进度。在每一轮训练中,遍历所有训练数据,进行前向传播计算损失,然后通过反向传播更新模型参数,并累加每一轮的总损失,最后将总损失添加到losses列表中,以便后续观察损失变化情况。
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)
# 打印每一轮的总损失
print(losses)
在训练过程中,我们可以观察到losses列表中的损失值逐渐减小,这表明模型在不断学习并改善预测效果。
7. 模型预测
训练完成后,我们可以使用模型进行预测。这里选择上下文['People', 'create', 'to', 'direct'],将其转换为上下文向量并输入到模型中,模型会输出每个单词作为目标词的对数概率,我们通过argmax(1)获取概率最大的单词对应的索引,从而得到模型预测的目标词。
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)
# 可以通过以下代码查看预测的目标词
# print(idx_to_word[max_idx.item()])
8. 获取和保存词向量
(1)获取词向量
模型训练完成后,嵌入层的权重就是我们需要的词向量。我们可以通过model.embeddings.weight获取嵌入层权重,并将其转换为 numpy 数组,方便后续处理和使用。
print("CBOW embedding weight=", model.embeddings.weight)
W = model.embeddings.weight.cpu().detach().numpy() # 将权重从GPU(如果使用)转移到CPU并转换为numpy数组
print(W)
为了更方便地查看每个单词对应的词向量,我们创建word_2_vec字典,将单词与对应的词向量关联起来。
word_2_vec = {}
for word in word_to_idx.keys():
word_2_vec[word] = W[word_to_idx[word], :]
print('词向量获取完成') # 修正原代码中“jiesu”为有意义的提示信息
(2)保存词向量
使用 numpy 的savez方法将词向量保存为 npz 文件,以便后续在其他任务中使用。保存后,我们可以通过load方法加载该文件,并查看文件中的数据名称。
# 保存训练后的词向量为npz文件
np.savez('word2vec实现.npz', file_1=W)
# 加载保存的npz文件
data = np.load('word2vec实现.npz')
# 查看文件中的数据名称
print(data.files)
# 可以通过以下代码获取保存的词向量
# saved_W = data['file_1']
三、总结
通过以上步骤,我们成功使用 PyTorch 实现了 CBOW 模型,并训练得到了词向量。整个过程包括数据预处理、模型构建、模型训练、模型预测以及词向量的获取和保存。CBOW 模型作为获取词向量的经典方法,虽然结构相对简单,但在很多 NLP 任务中都有着广泛的应用。通过本次实践,相信大家对 CBOW 模型的原理和实现有了更深入的理解。
在实际应用中,我们可以根据具体任务需求调整模型的参数,如词向量维度、上下文窗口大小、学习率、训练轮数等,以获得更好的词向量效果。同时,训练得到的词向量可以用于文本分类、情感分析、机器翻译等多种 NLP 任务,为后续的模型训练提供有力的支持。