代码实现:
import torch.optim as optim
from tqdm import tqdm, trange
import numpy as np
import torch
from torch import nn
import torch.nn.functional as F
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)}
data = []
for i in range(CONTEXT_SIZE, len(raw_text) - CONTEXT_SIZE): # (2, 60)
context = (
[raw_text[i - (2-j)] for j in range(CONTEXT_SIZE)] # [we,are]
+ [raw_text[i + j + 1] for j in range(CONTEXT_SIZE)] # [to,study]
)
target = raw_text[i] # 目标词
data.append((context, target))
def make_context_vector(context,word_to_idx):
idxs = [word_to_idx[x] for x 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(device)
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)
null_plob = F.log_softmax(out,dim=-1)
return null_plob
model = CBOW(vocab_size,10).to(device)
optimizer = optim.Adam(model.parameters(),lr=0.001)
losses = []
loss_function = nn.NLLLoss()
model.train()
for epoch in tqdm(range(200)):
total_loss = 0
size = 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()
size += 1
avg_loss = total_loss / size
print(avg_loss)
context = ['People', 'create', 'to', 'direct'] # People create programs to direct
context_vector = make_context_vector(context, word_to_idx).to(device)
# 预测的值
model.eval() # 进入到测试模式
predict = model(context_vector)
max_idx = predict.argmax(1) # dim=1表示每一行中的最大值对应的索引号,dim=0表示每一列中的最大值对应的索引号
# 获取词向量,这个Embedding就是我们需要的词向量,他只是一个模型的一个中间过程
print("CBOW embedding weight=", model.embeddings.weight) # GPU
W = model.embeddings.weight.cpu().detach().numpy() # .detach(): 这个方法会创建一个新的Tensor,它和原来的Tensor共享
# 这意味着这个新的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('jiesu')
# 保存训练后的词向量为npz文件,numpy处理矩阵的速度非常快,方便后期其他操作
np.savez('word2vec实现.npz', file_1=W)
data = np.load('word2vec实现.npz')
print(data.files)
这段代码是一个基于 PyTorch 实现的CBOW(连续词袋模型) 示例,用于训练词向量(类似 Word2Vec 的核心功能)。下面逐步分析代码:
一、核心功能概述
代码通过一段示例文本训练 CBOW 模型,最终得到每个单词的低维向量表示(词嵌入)。CBOW 的核心思想是:用一个单词的上下文(周围单词)预测该单词本身,通过这个过程让模型学习到单词的语义向量。
二、代码分步解析
1. 导入依赖库
import torch.optim as optim # PyTorch优化器
from tqdm import tqdm, trange # 进度条工具
import numpy as np # 数值计算
import torch
from torch import nn # 神经网络模块
import torch.nn.functional as F # 神经网络函数(如激活函数、softmax等)
这些是 PyTorch 深度学习和数据处理的基础库,tqdm
用于显示训练进度,提高可视化体验。
2. 数据预处理
(1)定义超参数和原始文本
CONTEXT_SIZE = 2 # 上下文窗口大小:每个目标词的前后各取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()
CONTEXT_SIZE=2
表示:对于每个目标词,取其前面 2 个词 + 后面 2 个词作为上下文(共 4 个词)。
(2)构建词汇表
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 = []
# 遍历文本,为每个位置的词构建上下文和目标
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)] # 前面2个词(如i=2时,取i-2, i-1)
+ [raw_text[i + j + 1] for j in range(CONTEXT_SIZE)] # 后面2个词(如i=2时,取i+1, i+2)
)
target = raw_text[i] # 目标词(当前位置的词)
data.append((context, target))
例如,对于句子片段We are about to study
:
- 当目标词是
about
(索引 = 2)时,上下文是前面的We, are
和后面的to, study
,即(["We", "are", "to", "study"], "about")
。
data
最终是一个列表,每个元素是(上下文单词列表, 目标词)
的元组,用于模型训练。
(4)上下文转向量函数
def make_context_vector(context, word_to_idx):
# 将上下文单词转为对应的索引,再封装成PyTorch张量
idxs = [word_to_idx[x] for x in context]
return torch.tensor(idxs, dtype=torch.long)
将上下文的单词列表(如["We", "are", "to", "study"]
)转为索引张量(如[10, 5, 3, 8]
),作为模型的输入。
3. 设备配置
device = 'cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu'
print(device)
自动选择训练设备:优先用 NVIDIA GPU(cuda),其次 Apple GPU(mps),最后用 CPU,以加速训练。
4. CBOW 模型定义
class CBOW(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(CBOW, self).__init__()
# 嵌入层:将单词索引转为低维向量(vocab_size→embedding_dim)
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
# 投影层:将嵌入向量映射到128维
self.proj = nn.Linear(embedding_dim, 128)
# 输出层:将128维向量映射到词汇表大小(预测目标词)
self.output = nn.Linear(128, vocab_size)
def forward(self, inputs):
# 1. 上下文嵌入:获取输入的每个单词的嵌入向量,求和(上下文向量的平均/求和代表上下文语义)
embeds = sum(self.embeddings(inputs)).view(1, -1) # 形状:(1, embedding_dim)
# 2. 投影层+激活函数
out = F.relu(self.proj(embeds)) # 形状:(1, 128)
# 3. 输出层+log_softmax(将输出转为概率分布的对数)
out = self.output(out) # 形状:(1, vocab_size)
log_probs = F.log_softmax(out, dim=-1) # 沿最后一维计算softmax并取对数
return log_probs
模型核心逻辑:
嵌入层(embeddings):是训练的核心,其权重就是最终的词向量(每个单词对应一行向量)。
前向传播:将上下文单词的嵌入向量求和(代表上下文的整体语义),通过两层线性网络,最终输出对目标词的概率分布(对数形式)。
5. 模型训练
# 初始化模型(词汇表大小,嵌入维度10),并移动到指定设备
model = CBOW(vocab_size, 10).to(device)
# 优化器:Adam(常用的自适应学习率优化器)
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 损失函数:NLLLoss(负对数似然损失,配合log_softmax使用,等价于交叉熵损失)
loss_function = nn.NLLLoss()
losses = [] # 记录损失
model.train() # 切换到训练模式
# 训练200个epoch
for epoch in tqdm(range(200)):
total_loss = 0 # 总损失
size = 0 # 样本数
for context, target in data:
# 1. 准备数据:上下文向量和目标词索引(均移动到设备)
context_vector = make_context_vector(context, word_to_idx).to(device)
target = torch.tensor([word_to_idx[target]]).to(device) # 目标词的索引
# 2. 模型预测
predict = model(context_vector)
# 3. 计算损失
loss = loss_function(predict, target)
# 4. 反向传播+参数更新
optimizer.zero_grad() # 清空梯度
loss.backward() # 计算梯度
optimizer.step() # 更新参数
total_loss += loss.item() # 累加损失(转为Python数值)
size += 1
# 计算并打印每个epoch的平均损失
avg_loss = total_loss / size
print(f"Epoch {epoch+1}, Average Loss: {avg_loss}")
losses.append(avg_loss)
训练过程流程:
每个 epoch 遍历所有训练样本(上下文 - 目标词对)。
对每个样本,模型根据上下文预测目标词,计算预测值与真实值的损失。
通过反向传播更新模型参数(尤其是嵌入层的权重,即词向量)。
随着训练进行,损失逐渐下降,说明模型越来越能通过上下文准确预测目标词。
6. 模型预测与词向量提取
(1)测试预测效果
# 测试上下文:"People", "create", "to", "direct"(对应原句"People create programs to direct processes")
context = ['People', 'create', 'to', 'direct']
context_vector = make_context_vector(context, word_to_idx).to(device)
model.eval() # 切换到评估模式(关闭 dropout 等训练特有的操作)
predict = model(context_vector)
max_idx = predict.argmax(1) # 取概率最大的索引(预测的目标词)
print("预测的目标词:", idx_to_word[max_idx.item()]) # 理论上应输出"programs"
用训练好的模型测试:给定上下文People create ... to direct
,模型应能预测出中间的目标词programs
。
(2)提取词向量
# 嵌入层的权重就是词向量(vocab_size × embedding_dim 的矩阵)
print("CBOW嵌入层权重(词向量矩阵):", model.embeddings.weight)
# 将词向量矩阵从GPU移到CPU,并转为numpy数组(脱离计算图,不参与梯度计算)
W = model.embeddings.weight.cpu().detach().numpy()
CBOW 模型的核心输出是嵌入层的权重,每个单词对应的行向量就是该单词的语义向量(维度为 10,在初始化时指定)。
(3)构建词 - 向量字典并保存
# 构建{单词: 词向量}字典
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)
# 测试加载
data = np.load('word2vec实现.npz')
print("保存的词向量键名:", data.files) # 输出 ['word_vectors']
将词向量整理为字典,并保存为 numpy 格式,方便后续用于语义相似度计算、文本分类等任务。
三、核心总结
- 数据层面:通过滑动窗口从文本中提取 “上下文 - 目标词” 对,作为训练样本。
- 模型层面:CBOW 模型通过嵌入层将单词转为向量,用上下文向量的总和预测目标词,最终学习到的嵌入层权重就是词向量。
- 训练目标:通过优化预测损失,让语义相近的词(如 “process” 和 “processes”)的向量更接近。
- 输出:得到每个单词的低维向量表示,可用于后续 NLP 任务(如语义相似度计算、文本分类等)。
这段代码是 Word2Vec 中 CBOW 模型的简化实现,核心思想与原始 Word2Vec 一致,适合理解词向量的训练原理。