一、词向量与 CBOW 模型
在自然语言处理(NLP)领域,词向量是将离散单词转化为连续数值向量的核心技术,它能让计算机理解单词的语义信息(如语义相似的单词向量距离更近)。而CBOW(Continuous Bag-of-Words,连续词袋模型) 是 Word2Vec 的经典实现之一,核心思想是通过上下文单词预测中心目标单词,训练过程中学习到的嵌入层参数就是我们需要的词向量,兼具实现简单、训练高效的特点。
二、语料预处理
原始文本通常包含标点、空值、多余空格等噪声,无法直接用于模型训练,语料预处理的目标是将原始文本转化为干净、标准化的单词序列
2.1 核心处理步骤
-
读取文本语料:使用 Pandas 读取本地文本文件,处理空值
-
文本拼接与降噪:将多行文本拼接为单行,去除标点符号(逗号、句号等)
-
单词分割:按空格分割为单词列表,兼容 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
import pandas as pd
# 超参数:上下文窗口大小(左右各4个单词)
CONTEXT_SIZE = 4
# 1. 读取语料并处理空值
raw_text = pd.read_table(r'.\AI报道.txt', header=None)
total_text = raw_text.iloc[:, 0].dropna().str.cat(sep=' ')
# 2. 去除标点,标准化文本
total_text = total_text.replace(',', '').replace('.', '')
# 3. 分割为单词列表(兼容多空格,过滤空值)
total_text = total_text.split()
# 4. 构建词汇表与双向映射
vocab = set(word for word in total_text if word.strip()) # 唯一单词集合
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)} # 索引→单词
-
dropna()必须保留:避免原始文本中的空行导致后续处理报错 -
无参数
split():相比split(' '),能自动处理多个连续空格,且不会生成空字符串元素
三、构造训练数据集
python
# 构造(上下文, 目标词)训练样本
data = []
for i in range(CONTEXT_SIZE, len(total_text) - CONTEXT_SIZE):
# 提取上下文:目标词左侧4个单词
context = [total_text[j + i - 4] for j in range(CONTEXT_SIZE)]
# 提取目标词
target = total_text[i]
data.append((context, target))
# 将上下文单词列表转为索引张量(数值化)
def make_context_vector(context, word_to_idx):
idxs = [word_to_idx[w] for w in context]
return torch.tensor(idxs, dtype=torch.long) # 强制转为长整型张量(Embedding层要求)
# 测试:输出第一个样本的上下文张量
print(make_context_vector(data[0][0], word_to_idx))
-
遍历边界:
range(CONTEXT_SIZE, len(total_text)-CONTEXT_SIZE)确保上下文不会超出单词列表范围,避免索引越界; -
张量类型:
dtype=torch.long是必须的,PyTorch 的nn.Embedding层仅接受长整型(LongTensor)索引输入; -
样本格式:
data列表中每个元素为(['单词1','单词2',...,'单词8'], '目标词'),是 CBOW 模型的标准训练样本。
四、定义 CBOW 模型 ------ 基于 PyTorch 的神经网络实现
CBOW 模型的网络结构简单且固定,核心由嵌入层、线性层、激活层、输出层组成,训练过程中嵌入层的权重会被不断优化,最终成为我们需要的词向量。
5.1 CBOW 模型核心结构
-
嵌入层(Embedding):核心层,将单词索引(one-hot 向量等价)压缩为低维连续向量(词向量),输入为
vocab_size(词汇表大小),输出为embedding_dim(词向量维度,超参数) -
投影层(Linear1):将词向量映射到隐藏层(128 维),增加模型拟合能力;
-
激活层(ReLU):引入非线性,解决线性模型表达能力不足的问题;
-
输出层(Linear2):将隐藏层特征映射回词汇表大小,输出每个单词为目标词的概率;
-
LogSoftmax:对输出层结果做归一化,配合 NLLLoss 计算损失。
python
class CBOW(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(CBOW, self).__init__() # 继承父类nn.Module的初始化
# 嵌入层:vocab_size→embedding_dim,核心词向量层
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
# 投影层:词向量→128维隐藏层
self.proj = nn.Linear(embedding_dim, 128)
# 输出层:隐藏层→词汇表大小,预测每个单词的概率
self.output = nn.Linear(128, vocab_size)
def forward(self, inputs):
# 1. 嵌入层:索引→词向量,求和后重塑形状(适配线性层)
embeds = sum(self.embeddings(inputs)).view(1, -1)
# 2. 投影层+ReLU激活
out = F.relu(self.proj(embeds))
# 3. 输出层:隐藏层→词汇表维度
out = self.output(out)
# 4. LogSoftmax归一化,dim=-1表示最后一维归一化
nll_prob = F.log_softmax(out, dim=-1)
return nll_prob
view(1, -1):将求和后的一维张量重塑为(1, embedding_dim)的二维张量,因为 PyTorch 线性层要求输入为二维(batch_size, feature_dim)
五、模型训练
模型训练是优化嵌入层权重(词向量)的核心过程,需要完成设备配置、模型实例化、优化器与损失函数选择、训练循环,同时通过损失值变化监控训练效果
5.1设备、模型、优化器、损失函数
python
# 1. 自动选择训练设备(GPU→MPS→CPU),提升训练速度
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"训练设备:{device}")
# 2. 实例化CBOW模型并移至指定设备,embedding_dim=10为词向量维度(超参数)
model = CBOW(vocab_size, 10).to(device)
# 3. 选择优化器:Adam优化器,学习率lr=0.001(超参数)
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 4. 选择损失函数:NLLLoss(配合LogSoftmax使用,适合多分类任务)
loss_function = nn.NLLLoss()
# 5. 模型设为训练模式:开启梯度追踪、批量归一化/ dropout生效
model.train()
# 存储每轮损失值,监控训练效果
losses = []
5.2训练循环
python
# 训练200轮,tqdm显示进度条
for epoch in tqdm(range(200)):
total_loss = 0 # 累计每轮总损失
# 遍历所有训练样本
for context, target in data:
# 1. 上下文数值化并移至指定设备
context_vector = make_context_vector(context, word_to_idx).to(device)
# 2. 目标词数值化并移至指定设备(转为张量,适配损失函数)
target = torch.tensor([word_to_idx[target]]).to(device)
# 3. 前向传播:预测目标词概率
train_predict = model(context_vector)
# 4. 计算损失:预测值与真实值的差距
loss = loss_function(train_predict, target)
# 5. 反向传播与参数更新(核心三步)
optimizer.zero_grad() # 梯度清零:避免上一轮梯度累积
loss.backward() # 反向传播:计算所有参数的梯度
optimizer.step() # 参数更新:根据梯度调整权重
# 累计本轮损失
total_loss += loss.item()
# 存储本轮平均损失,监控训练趋势
losses.append(total_loss / len(data))
print(f"第{epoch+1}轮损失:{losses[-1]:.4f}")
-
设备迁移:所有张量和模型必须移至同一设备,否则会报设备不匹配错误
-
梯度清零:
optimizer.zero_grad()是训练循环的核心,PyTorch 会自动累积梯度,若不清零会导致梯度计算错误 -
损失累加:
loss.item()将张量类型的损失值转为 Python 数值,避免张量累积导致的内存泄漏 -
模型训练模式:
model.train()开启梯度追踪,若使用 dropout/batchnorm 层,该方法会让其按训练模式工作 -
损失监控:若损失值持续下降并趋于稳定,说明模型训练有效;若损失值不下降,需调整学习率、词向量维度等超参数。
六、模型测试与词向量生成
6.1 模型测试:输入前四个单词预测第五个词
python
# 测试上下文(需来自训练语料的词汇表)
test_context = ['AI','Revolutionizing','Industries','and']
# 上下文数值化并移至设备
context_vector = make_context_vector(test_context, word_to_idx).to(device)
# 模型设为测试模式:关闭梯度追踪、dropout/batchnorm固定
model.eval()
# 前向传播预测
with torch.no_grad(): # 关闭梯度计算,提升速度并节省内存
predict = model(context_vector)
# 提取预测结果:取概率最大的索引,转为单词
max_idx = predict.argmax(1).cpu().item() # 设备迁移+转为Python整数
predict_word = idx_to_word[max_idx]
print(f"模型预测的目标单词:{predict_word}")
-
model.eval():将模型转为测试模式,关闭梯度追踪,若有 dropout/batchnorm 层,会按测试模式工作(如 dropout 不随机丢弃神经元) -
torch.no_grad():上下文管理器,临时关闭梯度计算,避免测试过程中产生无用梯度,提升计算速度并节省内存 -
argmax(1):按行取概率最大的索引(dim=1),对应词汇表中概率最高的目标词 -
cpu().item():将设备上的张量索引转为 Python 整数,才能通过idx_to_word映射为单词
6.2词向量提取与字典生成
python
# 提取嵌入层权重(词向量矩阵),并做设备/梯度/格式转换
# cpu():移至CPU;detach():脱离计算图,停止梯度追踪;numpy():转为Numpy数组
word_embedding_weight = model.embeddings.weight.cpu().detach().numpy()
# 生成词向量字典:{单词:对应的词向量}
word_2_vec = {}
for word in word_to_idx.keys():
# 按单词索引取权重矩阵的对应行,即为该单词的词向量
word_2_vec[word] = word_embedding_weight[word_to_idx[word], :]
# 打印示例:查看某个单词的词向量
print(f"单词'AI'的词向量:{word_2_vec['AI']}")
print(f"词向量维度:{word_2_vec['AI'].shape}")
-
detach()必须使用:嵌入层权重是模型的可训练参数,处于计算图中,detach()可脱离计算图并停止梯度追踪,避免后续操作报错 -
权重矩阵形状:
(vocab_size, embedding_dim),即词汇表中每个单词对应一个embedding_dim维的词向量 -
词向量字典的意义:将数值化的词向量与单词一一对应,可直接用于后续语义分析、文本分类等 NLP 任务