目录
第二步:语料预处理------把原始文本变成模型能"看懂"的数据
第三步:生成训练数据------构建"上下文-中心词"样本对
第六步:核心环节------用PyTorch定义CBOW模型
[1. __init__方法(模型层定义)](#1. __init__方法(模型层定义))
[2. forward方法(前向传播逻辑)](#2. forward方法(前向传播逻辑))
[1. 模型预测](#1. 模型预测)
[2. 词嵌入提取](#2. 词嵌入提取)
[3. 词嵌入保存与加载](#3. 词嵌入保存与加载)
[1. 预期运行结果](#1. 预期运行结果)
6.1和8.2是核心理解
一、先明确核心目标:
在看代码之前,我们先理清核心任务,避免"盲目看代码、不懂其意义":
-
以一段关于"计算过程"的英文语料为训练数据,设定上下文窗口大小为2(即中心词左右各2个词作为上下文);
-
用PyTorch搭建简易CBOW模型,通过"上下文预测中心词"的辅助任务,训练模型学习词语的语义关联;
-
训练完成后,从模型中提取词嵌入向量,将其保存为npz文件,方便后续复用(比如文本分类、语义匹配等任务);
-
用训练好的模型做简单预测,验证模型的训练效果。
简单说,这份代码的核心就是"用PyTorch实战CBOW,最终得到可用的词嵌入",全程贴合工业界基础建模流程,没有多余的复杂操作,新手也能轻松跟上。
二、逐段拆解:代码的每一步,都藏着知识点
接下来,我们跟着代码的执行顺序,逐段拆解,每一段代码都配上详细解读,让每一行代码都有迹可循、有理可依。
第一步:导入依赖库,做好准备工作
代码开头先导入需要的库,都是PyTorch建模的"标配",无需额外安装复杂依赖:
python
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
from tqdm import tqdm
解读:
-
torch相关库:PyTorch的核心库,用于搭建模型、定义损失函数、优化器等;
-
numpy:用于后续词嵌入向量的保存与加载,处理数值计算;
-
tqdm:用于显示训练进度条,直观观察训练过程,提升开发体验(实战中常用小技巧)。
第二步:语料预处理------把原始文本变成模型能"看懂"的数据
任何NLP任务的第一步都是预处理,计算机无法直接处理字符串,必须将其转换为整数索引,这一步就是完成"原始文本→索引数据"的转换:
python
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)
print("长度:",vocab_size)
word_to_idx = {word:i for i,word in enumerate(vocab)}
idx_to_word = {i:word for i,word in enumerate(vocab)}
解读:
-
context_size=2:设定上下文窗口大小为2,意味着每个中心词的上下文由"左边2个词+右边2个词"组成,共4个词;
-
raw_text.split():将原始文本按空格拆分,得到单词列表(英文文本最简单高效的分词方式,中文需用jieba等专门分词工具);
-
vocab=set(raw_text):对单词列表去重,构建词汇表------词汇表是所有不重复单词的集合,vocab_size就是词汇表大小,后续模型的输入输出维度都围绕它展开;
-
word_to_idx与idx_to_word:构建"单词→索引"和"索引→单词"的映射字典,这是NLP建模的基础操作------将字符串单词转换为唯一整数索引,模型才能进行后续计算。
第三步:生成训练数据------构建"上下文-中心词"样本对
CBOW模型的核心任务是"用上下文预测中心词",因此需要将预处理后的单词列表,转换为模型需要的"上下文-中心词"样本对:
python
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))
解读:
-
循环范围:range(context_size, len(raw_text)-context_size)------避免越界!开头前2个词和结尾后2个词,无法满足"左右各2个上下文词"的要求,直接跳过;
-
上下文构建:用列表推导式简洁获取上下文------左边取[i-2, i-1],右边取[i+1, i+2],拼接后得到4个词的上下文,对应中心词raw_text[i];
-
data列表:最终存储的是(上下文单词列表, 中心词)对,比如data[0]就是第一个中心词对应的上下文和目标词,这是模型的核心训练样本。
第四步:辅助函数------将上下文转换为模型可输入的张量
模型输入需要是PyTorch张量(而非列表),因此定义一个辅助函数,将上下文单词列表转换为长整型张量:
python
def make_context_vector(context,word_to_idx):
idxs=[word_to_idx[w] for w in context]
return torch.tensor(idxs,dtype=torch.long)
解读:
-
先将上下文单词列表,通过word_to_idx字典转换为整数索引列表;
-
转换为torch.tensor,且dtype=torch.long------这是nn.Embedding层(词嵌入层)要求的输入类型,必须是长整型,不能用默认的浮点型;
-
测试:print(make_context_vector(data[0][0], word_to_idx))可打印第一个上下文的索引张量,验证转换是否成功。
第五步:设备选择------优先GPU加速,提升训练效率
实战中,GPU能大幅提升训练速度,尤其是语料量较大时,这一步实现"优先使用GPU,无GPU则用CPU":前提是之前配置了cuda,我们在深度学习的初始就将讲诉了配置方法。
python
device="cuda" if torch.cuda.is_available() else "cpu"
print(device)
解读:
-
torch.cuda.is_available():判断当前环境是否有可用的GPU;
-
后续模型和数据都需要通过.to(device)迁移到指定设备上,保证计算在同一设备进行,避免报错。
第六步:核心环节------用PyTorch定义CBOW模型
这是代码的灵魂部分,借助PyTorch的nn.Module,无需手动管理权重矩阵,就能快速搭建CBOW模型:
python
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.out=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.out(out)
nll_prob=F.log_softmax(out,dim=1)
return nll_prob
解读(重点!):
1. __init__方法(模型层定义)
-
super(CBOW, self).init():继承nn.Module(PyTorch所有神经网络模型的基类),获得框架提供的参数管理、模式切换等功能;
-
self.embeddings=nn.Embedding(vocab_size, embedding_dim):词嵌入核心层!对应我们手动实现CBOW时的W1矩阵(词汇表大小×词嵌入维度),作用是将整数索引映射为低维连续的词嵌入向量,权重会在训练中自动优化,无需手动初始化;
-
这里我们输入的索引向量是一个数值,在embedding层内部会拆解成49维度(vocab_size)大小的独热编码,然后通过矩阵运算转化为10维度的目标词向量。这也就是词嵌入的过程,词嵌入就是指将高维的词向量(独热编码)压缩成低纬度的、用较少的维度来表示的向量(这个较少的向量维度,每一维度都可以理解成一种特征,通过不同的特征描述原来的词汇)。这就解决了独热编码,没有语义相关性,维度灾难的问题,在我们得到的低纬度词嵌入向量,是含有语义的,比如篮球和乒乓球,具有相关语义,在词嵌入向量的空间中,坐标应该比较接近。
-
self.proj=nn.Linear(embedding_dim, 128):额外添加的线性投影层(隐藏层),将词嵌入维度(后续设定为10)映射到128维------比基础CBOW多了这一步,能让模型学习更复杂的语义关联,提升词嵌入质量;
-
self.out=nn.Linear(128, vocab_size):输出层,将128维的隐藏层输出,映射回词汇表大小维度,得到每个单词的预测得分。
2. forward方法(前向传播逻辑)
-
embeds=sum(self.embeddings(inputs)).view(1,-1):CBOW的核心操作!
-
self.embeddings(inputs):将输入的4个上下文索引,转换为4个embedding_dim维的词嵌入向量(输出形状:[4, 10]);
-
sum(...):对4个词嵌入向量求和(等价于求平均,不影响概率排序,简化计算),得到1个10维向量;
-
view(1,-1):将10维向量重塑为[1, 10]的二维张量------线性层要求输入是"批次维度×特征维度",这里批次大小为1(单样本训练)。
-
-
out=F.relu(self.proj(embeds)):将聚合后的词嵌入输入投影层,通过ReLU激活函数引入非线性,让模型能学习更复杂的关系;
-
out=self.out(out):将隐藏层输出映射到词汇表维度,得到每个单词的原始预测得分;
-
nll_prob=F.log_softmax(out, dim=1):将原始得分转换为"概率分布的对数值",配合后续NLLLoss损失函数计算损失,避免数值溢出。
第七步:模型初始化与训练------从参数优化到模型收敛
这是PyTorch模型训练的标准流程,完整实现了"初始化→循环训练→梯度更新",还加入了进度条显示,贴合实战:
python
model=CBOW(vocab_size,10).to(device)
optimizer=optim.Adam(model.parameters(),lr=0.001)
print(model)
losses=[]
loss_function=nn.NLLLoss()
model.train()
for epoch in tqdm(range(100)):
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)
解读:
-
模型初始化:model=CBOW(vocab_size,10).to(device)------词嵌入维度设定为10(实战中常用50/100),并将模型迁移到指定设备;
-
优化器:optim.Adam------目前最常用的优化器之一,自适应调整学习率,收敛更快更稳定,lr=0.001是经典初始学习率;
-
损失函数:nn.NLLLoss(负对数似然损失)------专门配合log_softmax使用,等价于交叉熵损失,计算预测结果与真实目标的差距;
-
model.train():将模型切换到训练模式(虽无Dropout等层,但养成习惯,后续复杂模型必备);
-
训练循环(重点!):
-
epoch=100:训练100轮,轮数可根据损失收敛情况调整;
-
单样本训练:遍历每个"上下文-中心词"对,将上下文向量和目标词都迁移到指定设备;
-
前向传播:train_predict=model(context_vector),得到模型预测结果;
-
梯度更新三步曲(缺一不可):
-
optimizer.zero_grad():清空上一轮的梯度,避免梯度累积导致参数更新异常;
-
loss.backward():反向传播,计算所有可训练参数(词嵌入层、线性层权重)的梯度;
-
optimizer.step():根据梯度,更新模型参数,让模型不断逼近最优解。
-
-
losses记录:每轮累计总损失,后续可通过绘制损失曲线,观察模型是否收敛(损失持续下降并平稳,说明模型在有效学习)。
-
第八步:模型验证与词嵌入提取------让训练成果落地
训练完成后,不是结束,而是要验证模型效果、提取词嵌入并保存,让模型产生实际价值:
python
# 模型预测
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()])
# 提取词嵌入权重
print("cbow embedding weight: ",model.embeddings.weight)
W=model.embeddings.weight.cpu().detach().numpy()
# 构建词→词向量字典
word_2_vec={}
for word in word_to_idx.keys():
word_2_vec[word]=W[word_to_idx[word],:]
print('结束')
# 保存与加载词向量
np.savez("cbow.npz",file1=W)
data=np.load("cbow.npz")
print(data['file1'])
解读:
1. 模型预测
-
model.eval():将模型切换到评估模式,禁用训练模式下的特殊行为,保证预测结果稳定;
-
predict.argmax(1):找到预测概率最大的索引(log_softmax输出中,值越大,概率越高);
-
idx_to_word[max_idx.item()]:将索引转换为对应单词------这里上下文是["People","create","to","direct"],真实中心词是"programs",训练到位的话,模型能准确预测。
2. 词嵌入提取
-
model.embeddings.weight:这就是我们要的词嵌入矩阵!对应手动实现中的W1,形状为[vocab_size, 10],每一行是一个单词的10维词嵌入向量;
-
cpu().detach().numpy():张量转Numpy数组的标准操作:
-
detach():脱离计算图,避免后续操作影响模型梯度(评估阶段无需计算梯度);
-
cpu():将GPU上的张量迁移到CPU(Numpy不支持GPU张量);
-
numpy():转换为Numpy数组,方便后续保存和使用。
-
-
word_2_vec字典:构建"单词→词嵌入向量"的映射,后续使用时,可直接通过单词获取对应的词向量。
3. 词嵌入保存与加载
-
np.savez("cbow.npz", file1=W):将词嵌入矩阵保存为npz格式(Numpy压缩格式),占用空间小,方便后续复用(无需重新训练);
-
np.load("cbow.npz"):加载保存的词向量,data['file1']就是之前保存的词嵌入矩阵,可直接用于其他NLP任务。
三、运行结果预期与作业
1. 预期运行结果
运行这份代码后,你会看到这些关键输出,证明代码运行正常:
-
首先打印词汇表大小(约50个左右)和设备类型(cuda或cpu);
-
训练过程中,tqdm进度条推进,每轮打印总损失,损失会持续下降,最终趋于平稳;
-
预测阶段输出一个单词(大概率是"programs"),验证模型预测效果;
-
最后打印词嵌入矩阵和加载后的npz文件内容,形状为[vocab_size, 10],证明词向量保存、加载成功。
2、运行结果部分截图
显然我们得到了正确的预测:program
3、作业
修改代码使得:基于前面四个词预测下一个词。