文章目录
- 一. CBOW模型详解
- 1.1 Word2Vec与分布式表示
- 1.2 CBOW模型原理
- 数学表达
- 1.3 网络架构详解
- 代码中的网络层说明:
- 1.4 训练目标与优化
- 1.5 CBOW 与 Skip-gram 比较
- 1.6 词向量的应用与提取
- 二. 数据准备与预处理
- 2.1 语料库与基本参数设置
- 2.2 构建词汇表
- 2.3 构建训练数据集
- 2.4 上下文向量化函数
- 三. CBOW模型构建
- 3.1 模型类定义
- 四. 模型训练
- 4.1 设备配置与模型初始化
- 4.2 训练循环
- 五. 模型测试与词向量提取
- 5.1 模型测试
- 5.2 词向量提取与保存
一. CBOW模型详解
1.1 Word2Vec与分布式表示
Word2Vec是Google在2013年提出的一种高效学习词向量的模型,它包含两种主要架构:连续词袋模型(CBOW)和跳字模型(Skip-gram)。这两种模型都基于一个核心思想:“一个词的含义可以通过它周围出现的词来定义”,这被称为分布式假设。
在代码实现中,我们通过构建(context, target)训练对来体现这一思想:
foriinrange(CONTEXT_SIZE,len(raw_text)-CONTEXT_SIZE):context=([raw_text[i-(2-j)]forjinrange(CONTEXT_SIZE)]+[raw_text[i+j+1]forjinrange(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_twt:
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}_CvˉC是上下文词向量的平均(或求和)
- v w v_{w}vw是词w ww的输出向量
- V VV是词汇表
在代码中,这一原理体现在CBOW模型的forward方法:
defforward(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)# 计算对数概率returnnl_l_prob1.3 网络架构详解
CBOW模型通常包含以下三层:
- 输入层:上下文词的索引表示,代码中使用
make_context_vector函数转换 - 投影层(隐藏层):共享的嵌入矩阵,将索引映射为稠密词向量
- 输出层:线性层将隐藏层表示映射回词汇表空间
代码中的网络层说明:
classCBOW(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):
loss_function=nn.NLLLoss()# 负对数似然损失# 在训练循环中train_predict=model(context_vector)loss=loss_function(train_predict,target)# 计算损失训练过程采用反向传播和Adam优化器:
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任务。代码中通过以下方式提取和保存词向量:
# 获取Embedding层的权重矩阵W=model.embeddings.weight.cpu().detach().numpy()# 构建词向量字典word_2vec={}forwordinword_to_idx.keys():word_2vec[word]=W[word_to_idx[word],:]# 保存词向量np.savez(r'word2vec实现.npz',file_1=W)这些词向量可以应用于:
- 语义相似度计算:通过余弦相似度比较词向量
- 文本分类:作为特征输入分类器
- 命名实体识别:提供上下文语义信息
二. 数据准备与预处理
2.1 语料库与基本参数设置
首先,需要定义上下文窗口的大小并准备训练语料:
importtorchimporttorch.nnasnn# 神经网络importtorch.nn.functionalasFimporttorch.optimasoptim#fromtqdmimporttqdm,trange# 显示进度条importnumpyasnp# 设置词左边和右边选择的个数(即上下文词汇个数)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 构建词汇表
vocab=set(raw_text)# 集合、词库,里面内容去重vocab_size=len(vocab)word_to_idx={word:ifori,wordinenumerate(vocab)}# for循环的复合写法,第1次循环,i得到的索引号,word 第1个单词idx_to_word={i:wordfori,wordinenumerate(vocab)}代码分析:
vocab = set(raw_text):创建词汇集合,自动去除重复单词vocab_size = len(vocab):获取词汇表大小word_to_idx:创建单词到索引的映射字典idx_to_word:创建索引到单词的映射字典,用于后续的反向查找
2.3 构建训练数据集
data=[]# 获取上下文词,将上下文词作为输入,目标词作为输出。构建训练数据集。foriinrange(CONTEXT_SIZE,len(raw_text)-CONTEXT_SIZE):# (2, 60)context=([raw_text[i-(2-j)]forjinrange(CONTEXT_SIZE)]+[raw_text[i+j+1]forjinrange(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 上下文向量化函数
defmake_context_vector(context,word_to_ix):# 将上下文词转换为one-hotidxs=[word_to_ix[w]forwincontext]returntorch.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 模型类定义
classCBOW(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)defforward(self,inputs):embeds=sum(self.embeddings(inputs)).view(1,-1)# cnnout=F.relu(self.proj(embeds))# nn.relu()激活层out=self.output(out)nl_l_prob=F.log_softmax(out,dim=-1)# softmax交叉熵。returnnl_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 设备配置与模型初始化
# 模型在cuda训练device="cuda"iftorch.cuda.is_available()else"mps"iftorch.backends.mps.is_available()else"cpu"print(device)model=CBOW(vocab_size,embedding_dim=10).to(device)# 语料库中一共有49个单词,[000000...1]49->[ ... ]300optimizer=optim.Adam(model.parameters(),lr=0.001)# 优化器代码分析:
device:自动检测可用设备(CUDA、MPS或CPU)model:创建CBOW模型实例,词嵌入维度设为10optimizer:使用Adam优化器,学习率为0.001
4.2 训练循环
losses=[]# 存储损失的集合loss_function=nn.NLLLoss()# NLLLoss损失函数(当分类列表非常多的情况),将多个类别分别分成0、1两个类别。这里和log_softmax合在一起就是一个model.train()# 不代表开始训练,模型具备训练的能力,设置一个可写的权限??forepochintqdm(range(200)):# 开始训练total_loss=0forcontext,targetindata: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 模型测试
# 测试# context = ['People', 'create', 'to', 'direct'] # People create programs to directcontext=["spirits","of","the","computer"]# spirits of the computercontext_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 词向量提取与保存
# 获取词向量,这个Embedding就是我们需要的问题。他只是一个模型的一个中间过程print("CBOW embedding weight=",model.embeddings.weight)# GPUW=model.embeddings.weight.cpu().detach().numpy()# .detach(); 这个方法会创建一个新的Tensor,它和原来的Tensor共享数据,但是不会追踪梯度。# 这意味着这个新的Tensor不会参与梯度的反向传播,这对于防止在计算梯度时意外修改某些参数很有用。print(W)# 生成词嵌入字典,即{单词1:词向量1,单词2:词向量2...}的格式word_2vec={}forwordinword_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格式,便于后续加载和使用