文章目录
- [一. CBOW模型详解](#一. CBOW模型详解)
-
- [1.1 Word2Vec与分布式表示](#1.1 Word2Vec与分布式表示)
- [1.2 CBOW模型原理](#1.2 CBOW模型原理)
- [1.3 网络架构详解](#1.3 网络架构详解)
- [1.4 训练目标与优化](#1.4 训练目标与优化)
- [1.5 CBOW 与 Skip-gram 比较](#1.5 CBOW 与 Skip-gram 比较)
- [1.6 词向量的应用与提取](#1.6 词向量的应用与提取)
- [二. 数据准备与预处理](#二. 数据准备与预处理)
-
- [2.1 语料库与基本参数设置](#2.1 语料库与基本参数设置)
- [2.2 构建词汇表](#2.2 构建词汇表)
- [2.3 构建训练数据集](#2.3 构建训练数据集)
- [2.4 上下文向量化函数](#2.4 上下文向量化函数)
- [三. CBOW模型构建](#三. CBOW模型构建)
-
- [3.1 模型类定义](#3.1 模型类定义)
- [四. 模型训练](#四. 模型训练)
-
- [4.1 设备配置与模型初始化](#4.1 设备配置与模型初始化)
- [4.2 训练循环](#4.2 训练循环)
- [五. 模型测试与词向量提取](#五. 模型测试与词向量提取)
-
- [5.1 模型测试](#5.1 模型测试)
- [5.2 词向量提取与保存](#5.2 词向量提取与保存)
一. CBOW模型详解
1.1 Word2Vec与分布式表示
Word2Vec是Google在2013年提出的一种高效学习词向量的模型,它包含两种主要架构:连续词袋模型(CBOW)和跳字模型(Skip-gram)。这两种模型都基于一个核心思想:"一个词的含义可以通过它周围出现的词来定义",这被称为分布式假设。
在代码实现中,我们通过构建(context, target)训练对来体现这一思想:
python
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)) # 构建训练样本
与传统one-hot编码相比,Word2Vec生成的词向量具有以下优势:
- 低维稠密 :代码中设置
embedding_dim=10,将49维的稀疏one-hot压缩为10维稠密向量 - 语义信息:相似的词在向量空间中距离较近
- 数学运算:支持向量运算,如"国王 - 男人 + 女人 ≈ 女王"
1.2 CBOW模型原理
CBOW模型的核心思想是通过上下文词汇预测中心词。具体来说:
输入 :上下文窗口中所有词的one-hot表示(或索引)
输出 :中心词的概率分布
目标:最大化正确中心词的对数似然
数学表达
给定上下文词序列 C = { w t − 2 , w t − 1 , w t + 1 , w t + 2 } C = \{w_{t-2}, w_{t-1}, w_{t+1}, w_{t+2}\} C={wt−2,wt−1,wt+1,wt+2},CBOW模型试图预测中心词 w t w_t wt:
P ( w t ∣ C ) = exp ( v w t T ⋅ v ˉ C ) ∑ w ∈ V exp ( v w T ⋅ v ˉ C ) P(w_t | C) = \frac{\exp(v_{w_t}^T \cdot \bar{v}C)}{\sum{w \in V} \exp(v_w^T \cdot \bar{v}_C)} P(wt∣C)=∑w∈Vexp(vwT⋅vˉC)exp(vwtT⋅vˉC)
其中:
- v ˉ C \bar{v}_C vˉC 是上下文词向量的平均(或求和)
- v w v_{w} vw是词 w w w 的输出向量
- V V V是词汇表
在代码中,这一原理体现在CBOW模型的forward方法:
python
def forward(self, inputs):
embeds = sum(self.embeddings(inputs)).view(1, -1) # 对上下文词向量求和
out = F.relu(self.proj(embeds))
out = self.output(out)
nl_l_prob = F.log_softmax(out, dim=-1) # 计算对数概率
return nl_l_prob
1.3 网络架构详解
CBOW模型通常包含以下三层:
- 输入层 :上下文词的索引表示,代码中使用
make_context_vector函数转换 - 投影层(隐藏层):共享的嵌入矩阵,将索引映射为稠密词向量
- 输出层:线性层将隐藏层表示映射回词汇表空间
代码中的网络层说明:
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.output = nn.Linear(128, vocab_size) # 输出层
1.4 训练目标与优化
CBOW的训练目标是最大化给定上下文时正确中心词的条件概率:
L = ∑ t = 1 T log P ( w t ∣ w t − m , . . . , w t − 1 , w t + 1 , . . . , w t + m ) \mathcal{L} = \sum_{t=1}^{T} \log P(w_t | w_{t-m}, ..., w_{t-1}, w_{t+1}, ..., w_{t+m}) L=∑t=1TlogP(wt∣wt−m,...,wt−1,wt+1,...,wt+m)
在代码中,我们使用负对数似然损失(NLLLoss):
python
loss_function = nn.NLLLoss() # 负对数似然损失
# 在训练循环中
train_predict = model(context_vector)
loss = loss_function(train_predict, target) # 计算损失
训练过程采用反向传播和Adam优化器:
python
optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam优化器
# 反向传播
optimizer.zero_grad() # 梯度清零
loss.backward() # 反向传播
optimizer.step() # 参数更新
1.5 CBOW 与 Skip-gram 比较
| 特性 | CBOW | Skip-gram | 代码体现 |
|---|---|---|---|
| 训练速度 | 更快 | 较慢 | 在forward中求和操作计算效率高 |
| 输入输出 | 多对一 | 一对多 | 多个上下文词预测一个中心词 |
| 上下文利用 | 求和/平均 | 独立处理 | 使用sum()聚合上下文信息 |
1.6 词向量的应用与提取
训练完成后,可以提取词向量用于各种NLP任务。代码中通过以下方式提取和保存词向量:
python
# 获取Embedding层的权重矩阵
W = model.embeddings.weight.cpu().detach().numpy()
# 构建词向量字典
word_2vec = {}
for word in word_to_idx.keys():
word_2vec[word] = W[word_to_idx[word], :]
# 保存词向量
np.savez(r'word2vec实现.npz', file_1=W)
这些词向量可以应用于:
- 语义相似度计算:通过余弦相似度比较词向量
- 文本分类:作为特征输入分类器
- 命名实体识别:提供上下文语义信息
二. 数据准备与预处理
2.1 语料库与基本参数设置
首先,需要定义上下文窗口的大小并准备训练语料:
python
import torch
import torch.nn as nn # 神经网络
import torch.nn.functional as F
import torch.optim as optim #
from tqdm import tqdm, trange # 显示进度条
import numpy as np
# 设置词左边和右边选择的个数(即上下文词汇个数)
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()
# 中文的语句,你可以选择分词,也可以选择分字
代码分析:
CONTEXT_SIZE = 2:定义上下文窗口大小为2,即每个中心词考虑左右各2个上下文词raw_text:示例英文语料库,使用.split()方法按空格分词- 注释提示对于中文语料,可以选择分词或分字处理
2.2 构建词汇表
python
vocab = set(raw_text) # 集合、词库,里面内容去重
vocab_size = len(vocab)
word_to_idx = {word: i for i, word in enumerate(vocab)} # for循环的复合写法,第1次循环,i得到的索引号,word 第1个单词
idx_to_word = {i: word for i, word in enumerate(vocab)}
代码分析:
vocab = set(raw_text):创建词汇集合,自动去除重复单词vocab_size = len(vocab):获取词汇表大小word_to_idx:创建单词到索引的映射字典idx_to_word:创建索引到单词的映射字典,用于后续的反向查找
2.3 构建训练数据集
python
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)]
+ [raw_text[i + j + 1] for j in range(CONTEXT_SIZE)]
) # 获取上下文词 (['we', 'are', 'to', 'study'])
target = raw_text[i] # 获取目标词'about'
data.append((context, target)) # 将上下文词和目标词保存到data中[((['we', 'are', 'to', 'study']), 'about']
代码分析:
- 循环从第3个词开始到倒数第3个词结束(索引从0开始),确保每个中心词都有完整的上下文
context列表推导式:前部分获取左侧上下文词,后部分获取右侧上下文词target:当前中心词- 最终
data列表包含多个(context, target)元组
2.4 上下文向量化函数
python
def make_context_vector(context, word_to_ix): # 将上下文词转换为one-hot
idxs = [word_to_ix[w] for w in context]
return torch.tensor(idxs, dtype=torch.long) # 强制类型的转换,将列表
print(make_context_vector(data[0][0], word_to_idx)) # 示例
代码分析:
make_context_vector函数:将单词列表转换为对应的索引列表- 返回PyTorch张量,数据类型为
torch.long,适合作为Embedding层的输入 - 打印示例:展示如何将上下文词转换为索引张量
三. CBOW模型构建
3.1 模型类定义
python
class CBOW(nn.Module): # 神经网络
def __init__(self, vocab_size, embedding_dim):
super(CBOW, self).__init__() # 父类的初始化
self.embeddings = nn.Embedding(vocab_size, embedding_dim) # vocab_size:词嵌入的one-hot大小,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) # cnn
out = F.relu(self.proj(embeds)) # nn.relu()激活层
out = self.output(out)
nl_l_prob = F.log_softmax(out, dim=-1) # softmax交叉熵。
return nl_l_prob
代码分析:
初始化方法__init__参数:
vocab_size:词汇表大小,即one-hot向量的维度embedding_dim:词嵌入的维度,将高维one-hot向量压缩到此维度
网络层说明:
self.embeddings:Embedding层,将单词索引映射为稠密向量self.proj:线性投影层,将词向量维度从embedding_dim映射到128维self.output:输出层,将128维特征映射回词汇表大小维度
前向传播forward方法:
inputs:上下文词的索引张量sum(self.embeddings(inputs)):对上下文词的词向量求和,体现CBOW的核心思想.view(1, -1):调整张量形状为[1, embedding_dim]F.relu:ReLU激活函数,引入非线性F.log_softmax:log_softmax函数,计算对数概率,与NLLLoss配合使用
四. 模型训练
4.1 设备配置与模型初始化
python
# 模型在cuda训练
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(device)
model = CBOW(vocab_size, embedding_dim=10).to(device) # 语料库中一共有49个单词,[000000...1]49->[ ... ]300
optimizer = optim.Adam(model.parameters(), lr=0.001) # 优化器
代码分析:
device:自动检测可用设备(CUDA、MPS或CPU)model:创建CBOW模型实例,词嵌入维度设为10optimizer:使用Adam优化器,学习率为0.001
4.2 训练循环
python
losses = [] # 存储损失的集合
loss_function = nn.NLLLoss() # NLLLoss损失函数(当分类列表非常多的情况),将多个类别分别分成0、1两个类别。这里和log_softmax合在一起就是一个
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) # 可以不写forward,torch的内置功能,
loss = loss_function(train_predict, target) # 计算真实值和预测值之间的差距
# 反向传播
optimizer.zero_grad() # 梯度值清零
loss.backward() # 反向传播计算得到每个参数的梯度值
optimizer.step() # 根据梯度更新网络参数
total_loss += loss.item()
losses.append(total_loss)
print(losses)
代码分析:
losses:记录每个epoch的总损失loss_function:负对数似然损失,与log_softmax配合使用model.train():设置模型为训练模式,启用dropout和batch normalizationtqdm(range(200)):使用tqdm包装循环,显示训练进度条- 内部循环:遍历所有训练样本
make_context_vector:将上下文转换为索引张量torch.tensor([word_to_idx[target]]):将目标词转换为索引张量model(context_vector):前向传播获取预测值loss.backward():反向传播计算梯度optimizer.step():更新模型参数
五. 模型测试与词向量提取
5.1 模型测试
python
# 测试
# context = ['People', 'create', 'to', 'direct'] # People create programs to direct
context = ["spirits", "of", "the", "computer"] # spirits of the computer
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表示每一列中的最大值对应的索引号
代码分析:
model.eval():设置模型为评估模式,禁用dropout和batch normalizationpredict.argmax(1):获取预测概率最大值的索引,即预测的中心词
5.2 词向量提取与保存
python
# 获取词向量,这个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_2vec = {}
for word in word_to_idx.keys():
# 词向量矩阵中某个词的索引所对应的那一列即为所该词的词向量
word_2vec[word] = W[word_to_idx[word], :]
print("end")
# 保存训练后的词向量为npz文件'''numpy W 处理矩阵的速度非常快,方便后期其他人项目,要继续使用
np.savez(r'word2vec实现.npz', file_1=W)
data = np.load(r'word2vec实现.npz')
print(data.files)
代码分析:
model.embeddings.weight:获取Embedding层的权重矩阵,即所有词的词向量.cpu().detach().numpy():将张量转移到CPU,脱离计算图,转换为numpy数组word_2vec字典:构建{单词: 词向量}的映射关系np.savez:将词向量矩阵保存为npz格式,便于后续加载和使用


