作者:安静到无声 个人主页
作者简介:人工智能和硬件设计博士生、CSDN与阿里云开发者博客专家,多项比赛获奖者,发表SCI论文多篇。
Thanks♪(・ω・)ノ 如果觉得文章不错或能帮助到你学习,可以点赞收藏评论+关注哦! o( ̄▽ ̄)d
欢迎大家来到安静到无声的 《基于pytorch的自然语言处理入门与实践》,如果对所写内容感兴趣请看《基于pytorch的自然语言处理入门与实践》系列讲解 - 总目录,同时这也可以作为大家学习的参考。欢迎订阅,请多多支持!
在自然语言处理领域,"GloVe"指的是Global Vectors for Word Representation(全局向量词表示)的缩写。该模型是一种用于将单词表示为向量的技术,它旨在将语义上相关的单词捕捉到同一向量空间中。GloVe模型的核心思想是利用大量的语料库数据,通过计算共现矩阵来建立单词之间的联系。共现矩阵记录了在相邻的上下文窗口中,每对单词出现在一起的频率。
通过对这个共现矩阵进行数学处理,GloVe模型可以得到每个单词的词向量表示。与其他词向量模型(如Word2Vec)不同,GloVe模型不仅考虑上下文窗口内的单词,还考虑了全局语料库中所有单词对之间的共现关系。这使得GloVe能够更好地捕捉到相似单词之间的关系和语义信息。通过使用GloVe模型,可以将单词表示为连续的向量空间中的点,且这些向量之间的距离反映了它们之间的语义关系。
GloVe模型的核心是共现矩阵,它用于记录语料库中每对单词的共现频率。共现矩阵是一个方阵,其中的每个元素表示两个单词在上下文窗口内同时出现的次数。具体来说,假设我们有一个包含 N N N个单词的语料库,并且假设我们的上下文窗口(window length)大小为 k k k(即考虑每个单词的前后 k k k个单词作为上下文)。那么共现矩阵 X X X的大小为 N × N N×N N×N。
X X X的每个元素 X i j X_{ij} Xij表示j在词i上下文出现的次数,此外 X i j X_{ij} Xij可能与 X j i X_{ji} Xji不相等,因为单词 i i i和 j j j的上下文窗口可能不重叠。共现矩阵的构建是通过遍历整个语料库来实现的。在遍历语料库的过程中,每当我们发现两个单词在某个上下文窗口内同时出现时,我们就在共现矩阵中相应的位置增加计数。
一旦得到了共现矩阵 X X X,GloVe模型会进一步对 X X X进行处理,以获得最终的单词向量表示。这个处理过程包括使用矩阵分解技术(如奇异值分解或因子分解机)来学习单词的向量表示,使得这些向量能够捕捉到单词之间的语义关系。
例如:语料库如下:
• I like deep learning.
• I like NLP.
• I enjoy flying.
则共现矩阵表示如下:(使用对称的窗函数(左右window length都为1) )
我们定义一些变量表示
例子:通过一个简单的例子来展示是如何从共现概率抽取出某种语义的,考虑两个单词 i i i和 j j j之间的某种特殊兴趣。 假设我们对热力学感兴趣,然后我们令 i = i c e i=ice i=ice(冰)和 j = s t e a m j=steam j=steam(蒸汽),两个单词之间的关系能通过研究它们基于不同单词 k k k得到的共现矩阵概率之比来确定。令 k = s o l i d k=solid k=solid(固体),我们期望 P i k / P j k P_{ik}/P_{jk} Pik/Pjk的值很大;同理假设 k = g a s k=gas k=gas(气体),我们期望 P i k / P j k P_{ik}/P_{jk} Pik/Pjk的值很小;而对于那些与两者都相关或都不相关的单词比如 k = w a t e r k=water k=water或 k = f a s h i o n k=fashion k=fashion, P i k / P j k P_{ik}/P_{jk} Pik/Pjk的值应该接近1。如下表所示为在大语料库中计算的概率,与我们的期望很符合。
上表显示了在大语料库中的这些概率值,以及它们的比值,这些比值肯定了我们的期望。与原始概率相比,比值更能区分相关的单词(比如solid与gas)和不相关的单词(water与fashion),并且也能更好地区分这两个相关的单词。
模型的表现依赖于 x m a x x_{max} xmax,实验中 x m a x = 100 x_{max}=100 xmax=100,当 α \alpha α为0.75时效果较好。模型最终生成两个词向量,分别为 W W W和 W ~ \widetilde{W} W ,论文中作者采用了 W + W ~ W+\widetilde{W} W+W 作为最终的词向量。
以下代码主要来自GloVe之Pytorch实现_代码部分_glove pytorch_散人stu174的博客-CSDN博客,我进行了重新的执行,并加以详实注释!
from collections import Counter
with open("train.txt","r",encoding="utf-8") as f:
corpus=f.read()
corpus=corpus.lower().split(" ") #一共有多少个单词
#设置词典大小
vocab_size=10000
#统计词的数量,原数据约237148个单词,用于记录语料库中出现频次最高的前vocab_size个词及其对应的频次。
word_count=dict(Counter(corpus).most_common(vocab_size))
#表示为登陆词(新词) 计算列表中每一个单词的词频数,并相加
word_count["" ]=len(corpus)-sum(list(word_count.values()))
#建立词表索引
idx2word=list(word_count.keys())
word2idx={w:i for i,w in enumerate(idx2word)}
这段代码的作用是对给定的文本语料库进行处理,以构建一个词汇表(vocabulary)和相应的索引。
首先,代码导入了 collections
模块中的 Counter
类。
接下来,代码使用 open()
函数打开名为 “train.txt” 的文本文件,并通过指定编码方式为 “utf-8” 读取文件内容,并将内容存储在变量 corpus
中。
然后,代码将 corpus
字符串转换为小写,并使用 .split(" ")
方法按照空格将其分割为单词列表。这一步的目的是为了方便对单词进行统计和处理。
接下来,代码设置了一个变量 vocab_size
,用于指定词汇表的大小,即最多包含多少个不同的单词。
然后,代码使用 Counter(corpus)
统计单词在 corpus
中出现的次数,并通过调用 .most_common(vocab_size)
方法,获取出现频次最高的前 vocab_size
个单词及其对应的频次,并将结果以字典的形式存储在 word_count
变量中。
接下来,代码计算未被记录在 word_count
中的单词的总频次,并将其存储在 word_count["
中。这里的
表示未登录词或者新词。
最后,代码将 word_count
字典中的键(即单词)转换为列表,并存储在 idx2word
变量中。然后,使用字典推导式生成一个名为 word2idx
的字典,该字典将词汇表中的每个单词映射为一个唯一的索引。
通过这段代码,你可以得到一个词汇表及相应的索引,可用于后续对文本进行处理、分析或建模等任务。
import numpy as np
import sys
window_size=5
coMatrix=np.zeros((vocab_size,vocab_size),np.int32)
print("共现矩阵占内存大小为:{}MB".format(sys.getsizeof(coMatrix)/(1024*1024)))
#语料库进行编码
corpus_encode=[word2idx.get(w,1999) for w in corpus] #是一个列表
#遍历语料库
for i,center_word in enumerate(corpus_encode):
#这一步和word2vec一样,提取背景词,但是后面有要防止边界越界
pos_indices=list(range(i-window_size,i))+list(range(i+1,i+1+window_size))
pos_indices=[idx%len(corpus_encode) for idx in pos_indices] #对pos_indices中的每个索引进行取模运算,将其限制在区间范围内。这是因为corpus的长度是20,所以索引不能超过这个范围。
context_word=[corpus_encode[idx] for idx in pos_indices] #这段代码的意思是根据上面提到的上下文位置索引列表pos_indices,从corpus中选择对应位置的词,将它们存储在列表context_word中。
#context_word代表当前center_word周围的所有关联词
for word_idx in context_word:
coMatrix[center_word][word_idx]+=1 #可以理解为横坐标的中心词,纵坐标为上下文词出现的频数
if((i+1)%1000000==0):
print("已完成{}/{}".format(i+1,len(corpus_encode)))
共现矩阵占内存大小为:381.46983337402344MB
已完成1000000/15313011
已完成2000000/15313011
已完成3000000/15313011
已完成4000000/15313011
已完成5000000/15313011
已完成6000000/15313011
已完成7000000/15313011
已完成8000000/15313011
已完成9000000/15313011
已完成10000000/15313011
已完成11000000/15313011
已完成12000000/15313011
已完成13000000/15313011
已完成14000000/15313011
已完成15000000/15313011
这段代码的作用是构建一个共现矩阵(co-occurrence matrix),用于记录语料库中每个单词与其周围上下文单词的共现频次。
首先,代码创建了一个大小为 (vocab_size, vocab_size)
的全零矩阵 coMatrix
,用于存储共现矩阵。其中,vocab_size
表示词汇表的大小,即单词的数量。
然后,代码使用 sys.getsizeof(coMatrix)/(1024*1024)
计算出 coMatrix
占用内存的大小,并通过 print()
函数输出结果。
接下来,代码对语料库进行编码。通过遍历 corpus
中的每个单词,使用 word2idx.get(w,1999)
将其转换为对应的索引,并将结果存储在列表 corpus_encode
中。如果某个单词不存在于词汇表中,则将其编码为 1999,这里的 1999 是一个特殊的索引值,用于表示未登录词或者新词。
然后,代码开始遍历 corpus_encode
列表,对每个中心词进行处理。为了提取中心词周围的背景词,代码创建了一个名为 pos_indices
的索引列表。该列表包含了当前中心词位置前后 window_size
范围内的索引值。需要注意的是,为了避免索引越界,代码使用了取模运算将索引限制在语料库范围内(长度为 len(corpus_encode)
)。
接下来,代码根据 pos_indices
列表从 corpus_encode
中选择对应位置的词,并将它们存储在列表 context_word
中。这样,context_word
就代表了当前中心词周围的所有背景词。
然后,代码通过遍历 context_word
列表,将每个背景词与当前中心词在 coMatrix
中的对应位置加一。这一操作可以理解为,将共现矩阵中横坐标为中心词、纵坐标为上下文词的位置的值加一。
最后,代码通过 (i+1) % 1000000 == 0
的判断,每处理 1000000 个中心词,输出一条提示信息,指示已经完成的进度。
通过这段代码,你可以得到一个共现矩阵,其中记录了语料库中每个单词与其周围上下文单词的共现频次。
# 检查矩阵是否为对称矩阵
is_symmetric = np.allclose(coMatrix, coMatrix.T)
print("是否为对称阵? :", is_symmetric)
del corpus,corpus_encode #释放内存
是否为对称阵? : True
xMax=100 #出现的最大值,依照原始论文所建立
alpha=0.75 #惩罚系数
wMatrix=np.zeros_like(coMatrix,np.float32) #惩罚系数矩阵
print("惩罚系数矩阵占内存大小为:{}MB".format(sys.getsizeof(wMatrix)/(1024*1024)))
for i in range(vocab_size):
for j in range(vocab_size):
if(coMatrix[i][j]>0): #判断频数是否是大于0
wMatrix[i][j]=(coMatrix[i][j]/xMax)**alpha if coMatrix[i][j]<xMax else 1
if((i+1)%1000==0):
print("已完成{}/{}".format(i+1,vocab_size))
惩罚系数矩阵占内存大小为:381.46983337402344MB
已完成1000/10000
已完成2000/10000
已完成3000/10000
已完成4000/10000
已完成5000/10000
已完成6000/10000
已完成7000/10000
已完成8000/10000
已完成9000/10000
已完成10000/10000
这段代码的作用是根据共现矩阵 coMatrix
构建一个惩罚系数矩阵 wMatrix
,用于对共现频次进行惩罚。
首先,代码定义了两个参数 xMax
和 alpha
。xMax
表示共现频次的最大值,根据原始论文建议进行设定。alpha
是一个惩罚系数,用于控制共现频次的惩罚程度。
然后,代码创建了一个与 coMatrix
大小相同、元素类型为浮点数的全零矩阵 wMatrix
,用于存储惩罚系数。
接下来,代码通过 sys.getsizeof(wMatrix)/(1024*1024)
计算出 wMatrix
占用内存的大小,并通过 print()
函数输出结果。
然后,代码使用两重循环遍历共现矩阵中的每个元素。如果共现频次大于 0,则根据公式 (coMatrix[i][j]/xMax)**alpha if coMatrix[i][j]
wMatrix[i][j]
。这里的判断条件 coMatrix[i][j]
xMax
的情况,即当共现频次大于等于 xMax
时,惩罚系数设置为 1。
最后,代码通过 (i+1) % 1000 == 0
的判断,每处理 1000 个元素,输出一条提示信息,指示已经完成的进度。
通过这段代码,你可以得到一个惩罚系数矩阵 wMatrix
,其中记录了共现频次经过惩罚后的值。这个惩罚系数矩阵将用于后续的词向量计算。
import torch
from torch.utils.data.dataset import Dataset
from torch.utils.data.dataloader import DataLoader
train_set=[]
for i in range(vocab_size):
for j in range(vocab_size):
if(coMatrix[i][j]!=0): #仅仅收集频次不是0的数据
train_set.append([i,j])
if((i+1)%1000==0):
print("已完成{}/{}".format(i+1,vocab_size))
已完成1000/10000
已完成2000/10000
已完成3000/10000
已完成4000/10000
已完成5000/10000
已完成6000/10000
已完成7000/10000
已完成8000/10000
已完成9000/10000
已完成10000/10000
这段代码的作用是创建一个用于训练的数据集 train_set
。其中,数据集中的每个样本由两个索引值组成,表示共现矩阵中频次不为零的元素的位置。
首先,代码定义了一个 batch_size
,表示每个批次的样本数量。
然后,代码创建了一个空列表 train_set
,用于存储数据集中的样本。
接下来,代码使用两重循环遍历共现矩阵中的每个元素。如果某个元素的频次不等于零,则将其所在位置的索引值 [i,j]
添加到 train_set
中。这样,train_set
中就记录了那些频次不为零的元素在共现矩阵中的位置。
最后,代码通过 (i+1) % 1000 == 0
的判断,每处理 1000 个元素,输出一条提示信息,指示已经完成的进度。
通过这段代码,你可以得到一个数据集 train_set
,其中包含了共现矩阵中频次不为零的元素的位置。接下来,你可以使用这个数据集进行训练,并根据需要使用 DataLoader
创建数据加载器,以便进行批次化的训练。
class dataset(Dataset):
def __init__(self,coMatrix,wMatrix,train_set):
super(dataset,self).__init__()
self.coMatrix=torch.Tensor(coMatrix)
self.wMatrix=torch.Tensor(wMatrix)
self.train_set=torch.Tensor(train_set)
def __len__(self):
return len(self.train_set)
def __getitem__(self,idx):
#中心词和背景词
i,k=train_set[idx]
#共现频次
x_ik=self.coMatrix[i][k]
#惩罚系数
w=self.wMatrix[i][k]
return i,k,x_ik,w
这段代码定义了一个名为 dataset
的类,继承自 torch.utils.data.Dataset
。这个类用于创建自定义的数据集类,以便在训练过程中使用。
类的构造方法 __init__
接受三个参数:coMatrix
、wMatrix
和 train_set
。在构造方法内,通过调用父类的构造方法 super(dataset, self).__init__()
初始化基类。然后,将传入的 coMatrix
、wMatrix
和 train_set
转换为 torch.Tensor
并分别赋值给类的属性 self.coMatrix
、self.wMatrix
和 self.train_set
,以便在类的其他方法中使用。
类还实现了两个方法:__len__
和 __getitem__
。
__len__
方法返回数据集的长度,即共有多少个样本。它简单地返回 self.train_set
的长度。
__getitem__
方法根据索引 idx
返回数据集中指定位置的样本。首先,根据索引 idx
从 self.train_set
中获取对应的中心词和背景词的索引值 i
和 k
。然后,通过索引 i
和 k
从 self.coMatrix
中获取共现频次 x_ik
,并从 self.wMatrix
中获取惩罚系数 w
。最后,返回 i
、k
、x_ik
、w
这四个值作为样本。
通过这个数据集类,你可以将共现矩阵、惩罚系数矩阵和训练集整合在一起,以便在模型训练过程中方便地获取对应的训练样本。你可以使用 DataLoader
将该数据集传入,并设置相应的批次大小、并行加载等参数,从而实现数据的批次化加载和训练。
batch_size=10
data=dataset(coMatrix,wMatrix,train_set)
dataloader=DataLoader(data,batch_size,shuffle=True,drop_last=True,num_workers=0)
del coMatrix,wMatrix
import torch.nn as nn
from torch.nn.modules import Module
class GloVe(Module):
def __init__(self,vocab_size,d_model=50):
super(GloVe,self).__init__()
self.vocab_size=vocab_size
self.d_model=d_model
#中心词矩阵和背景词矩阵
self.V=nn.Embedding(vocab_size,d_model)
self.U=nn.Embedding(vocab_size,d_model)
#中心词偏置和背景词偏置
self.B_v=nn.Embedding(vocab_size,1)
self.B_u=nn.Embedding(vocab_size,1)
#随机初始化参数(这种初始化方式收敛更快),embedding原来是默认(0,1)正态分布
initrange = 0.5 / self.vocab_size
self.V.weight.data.uniform_(-initrange, initrange)
self.U.weight.data.uniform_(-initrange, initrange)
self.B_v.weight.data.uniform_(-initrange, initrange)
self.B_u.weight.data.uniform_(-initrange, initrange)
def forward(self,i,k,x_ik,w):#输入的是中心词,背景词,共现频数,惩罚系数
#i[batch],k[batch],x_ik[batch],f_x_ik[batch]
v_i=self.V(i)
u_k=self.U(k)
b_i=self.B_v(i)
b_k=self.B_u(k)
#v_i[batch,d_model] u_k[batch,d_model] b_i[batch,1] b_k[batch,1]
#这里要清楚torch.mul()是矩阵按位相乘,两个矩阵的shape要相同
#torch.mm()则是矩阵的乘法
loss=(torch.mul(v_i,u_k).sum(dim=1)+b_i+b_k-torch.log(x_ik))**2
#loss[batch]
loss=loss*w*0.5
return loss.sum()
def get_embeding(self):
#两者相加输出,而非取其中一个
return self.V.weight.data.cpu().numpy()+self.U.weight.data.cpu().numpy()
这段代码定义了一个名为 GloVe
的类,继承自 torch.nn.Module
。GloVe
类是一个模型类,用于实现 GloVe(Global Vectors for Word Representation)模型。
在类的构造方法 __init__
中,接受两个参数 vocab_size
和 d_model
,分别表示词汇表大小和嵌入维度。在构造方法内,首先调用父类的构造方法 super(GloVe, self).__init__()
初始化基类。然后,定义了多个成员变量,包括 vocab_size
、d_model
、中心词矩阵 V
、背景词矩阵 U
、中心词偏置 B_v
和背景词偏置 B_u
。
V
和 U
是通过 nn.Embedding
定义的两个嵌入层,分别用于表示中心词和背景词的向量。B_v
和 B_u
是通过 nn.Embedding
定义的两个偏置层,用于表示中心词和背景词的偏置项。
接下来,在构造方法中使用均匀分布的随机数对上述参数进行初始化。其中,initrange
是一个初始范围参数,它的取值是 0.5 / vocab_size
。通过调用 self.V.weight.data.uniform_(-initrange, initrange)
等方法,对参数进行均匀分布的随机初始化。
forward
方法定义了前向计算过程。它接受 i
、k
、x_ik
和 w
四个输入参数,分别表示中心词、背景词、共现频数和惩罚系数。在方法内部,通过调用 self.V(i)
和 self.U(k)
分别获取中心词和背景词的向量表示,以及调用 self.B_v(i)
和 self.B_u(k)
获取中心词和背景词的偏置项。然后,使用这些参数计算损失函数,并返回损失值。
get_embeding
方法用于获取模型的嵌入层参数,返回中心词矩阵和背景词矩阵的和作为嵌入向量。
通过这个模型类,你可以构建一个基于 GloVe 模型的词向量训练模型。在模型的前向计算过程中,会根据输入的中心词和背景词,在嵌入层中查找对应的向量表示,并根据公式计算损失值。最后,通过反向传播更新模型参数,实现词向量的训练。
import scipy
from torch.optim.adagrad import Adagrad
from sklearn.metrics.pairwise import cosine_similarity
d_model=100
model = GloVe(vocab_size, d_model).cuda() #创建模型
#寻找附近的词,和Word2vec一样
def nearest_word(word, embedding_weights):
index = word2idx[word]
embedding = embedding_weights[index]
cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights],np.float)
return [idx2word[i] for i in cos_dis.argsort()[:10]]#找到前10个最相近词语
epochs = 10
optim = Adagrad(model.parameters(), lr=0.05) #选择Adagrad优化器
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
for epoch in range(epochs):
allloss=0
for step,(i,k,x_ik,w) in enumerate(dataloader):
#反向传播梯度更新
optim.zero_grad()
i=i.to(device)
k=k.to(device)
x_ik=x_ik.to(device)
w=w.to(device)
loss=model(i,k,x_ik,w)
loss.backward()
optim.step()
#记录总损失
allloss = allloss + loss
if((step+1)%2000==0):
print("epoch:{}, iter:{}, loss:{}".format(epoch+1,step+1,allloss/(step+1)))
if((step+1)%5000==0):
print("nearest to one is{}".format(nearest_word("one",model.get_embeding())))
nearest to one is['one', 'two', 'nine', 'eight', 'three', 'four', 'seven', 'five', 'six', 'zero']
epoch:10, iter:1282000, loss:1.2875440120697021
epoch:10, iter:1284000, loss:1.2875365018844604
nearest to one is['one', 'two', 'nine', 'eight', 'three', 'four', 'seven', 'five', 'six', 'zero']
epoch:10, iter:1286000, loss:1.2875158786773682
这段代码主要是使用了之前定义的 GloVe
模型进行词向量训练,并且在每个 epoch 结束后,输出损失值和找到的与单词 “one” 最相近的词语。
首先,通过 GloVe(vocab_size, d_model).cuda()
创建了一个模型对象 model
,并将其移动到 CUDA 设备上进行计算。
然后,定义了一个 nearest_word
函数,用于寻找与给定单词最相近的词语。该函数接受两个参数,即要查找的单词和词向量的权重矩阵 embedding_weights
。在函数内部,首先通过 word2idx[word]
获取给定单词的索引,然后从权重矩阵 embedding_weights
中获取对应的词向量表示 embedding
。接下来,计算给定单词与所有其他单词之间的余弦相似度,并根据相似度排序找到前 10 个最相近的词语,最后返回这些词语。
接下来,使用 Adagrad 优化器 Adagrad(model.parameters(), lr=0.05)
对模型参数进行优化。
然后,根据设备是否支持 CUDA 选择将数据放在 GPU 上还是 CPU 上。
接下来,开始训练过程,共进行 epochs
轮。在每个 epoch 中,对数据加载器 dataloader
进行迭代,迭代的结果包括中心词索引 i
、背景词索引 k
、共现频数 x_ik
和惩罚系数 w
。在每个步骤(step)中,首先将这些输入数据移动到对应的设备上。然后,通过调用模型的前向计算 model(i, k, x_ik, w)
获取损失值 loss
。接下来,通过调用 loss.backward()
进行反向传播,计算参数的梯度。最后,通过调用优化器的 optim.step()
方法更新模型参数。
在每个 epoch 的过程中,记录总损失 allloss
并输出当前的损失值以及迭代次数。当迭代次数达到 2000 的倍数时,输出损失值。当迭代次数达到 5000 的倍数时,输出与单词 “one” 最相近的词语。
通过这段代码,可以进行 GloVe 模型的训练,并且可以查找与指定单词最相近的词语。
共现矩阵
Glove模型介绍 (cbww.cn)
GloVe模型理解_愤怒的可乐的博客-CSDN博客
GloVe模型原理简介 - 知乎 (zhihu.com)
《GloVe: Global Vectors for Word Representation》论文
一个基于PyTorch实现的Glove词向量的实例_pytorch glove_程序员的自我反思的博客-CSDN博客
GloVe之Pytorch实现_代码部分_glove pytorch_散人stu174的博客-CSDN博客
--------推荐专栏--------
手把手实现Image captioning
CNN模型压缩
模式识别与人工智能(程序与算法)
FPGA—Verilog与Hls学习与实践
基于Pytorch的自然语言处理入门与实践