第五章(1):词嵌入进阶之Glove模型讲解与pytorch实现


欢迎大家来到安静到无声的 《基于pytorch的自然语言处理入门与实践》,如果对所写内容感兴趣请看《基于pytorch的自然语言处理入门与实践》系列讲解 - 总目录,同时这也可以作为大家学习的参考。欢迎订阅,请多多支持!

1. Glove模型的概述

在自然语言处理领域,"GloVe"指的是Global Vectors for Word Representation(全局向量词表示)的缩写。该模型是一种用于将单词表示为向量的技术,它旨在将语义上相关的单词捕捉到同一向量空间中。GloVe模型的核心思想是利用大量的语料库数据,通过计算共现矩阵来建立单词之间的联系。共现矩阵记录了在相邻的上下文窗口中,每对单词出现在一起的频率。

通过对这个共现矩阵进行数学处理,GloVe模型可以得到每个单词的词向量表示。与其他词向量模型(如Word2Vec)不同,GloVe模型不仅考虑上下文窗口内的单词,还考虑了全局语料库中所有单词对之间的共现关系。这使得GloVe能够更好地捕捉到相似单词之间的关系和语义信息。通过使用GloVe模型,可以将单词表示为连续的向量空间中的点,且这些向量之间的距离反映了它们之间的语义关系。

2. 共现矩阵

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) )

第五章(1):词嵌入进阶之Glove模型讲解与pytorch实现-LMLPHP
例如:中心词是like,I和deep是上下文。

3 词向量和共现矩阵的近似关系

我们定义一些变量表示

  • X i j X_{ij} Xij表示 j j j在词 i i i上下文出现的次数;
  • X i = ∑ k X i k \mathrm{X_{i}=\sum_{k}X_{ik}} Xi=kXik表示任意单词 k k k出现在 i i i上下文的次数;
  • P i j   =   P ( j ∣ i ) = X i j X i \mathrm{P_{ij}~=~P\left(j|i\right)=\frac{X_{ij}}{X_i}} Pij = P(j∣i)=XiXij表示单词 j j j出现在单词 i i i上下文的概率。

例子:通过一个简单的例子来展示是如何从共现概率抽取出某种语义的,考虑两个单词 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。如下表所示为在大语料库中计算的概率,与我们的期望很符合。
第五章(1):词嵌入进阶之Glove模型讲解与pytorch实现-LMLPHP

上表显示了在大语料库中的这些概率值,以及它们的比值,这些比值肯定了我们的期望。与原始概率相比,比值更能区分相关的单词(比如solid与gas)和不相关的单词(water与fashion),并且也能更好地区分这两个相关的单词。

  1. 观察上式,注意到比值 P i k / P j k P_{ik}/P_{jk} Pik/Pjk​依赖于3个单词 i i i j j j k k k,那么常用的模型如下式:
    F ( w i , w j , w k ∼ ) = P i k P j k \mathrm{F\left(w_i,w_j,\stackrel{\sim}{w_k}\right)=\frac{P_{ik}}{P_{jk}}} F(wi,wj,wk)=PjkPik
    其中 w w w为词向量。
  2. P i k P_{ik} Pik P j k P_{jk} Pjk的差距(距离)在向量空间中表现出来,可使用两者之间的差,即:
    F ( w i − w j , w ~ k ) = P i k P j k F(w_i-w_j,\tilde{w}_k)=\frac{P_{ik}}{P_{jk}} F(wiwj,w~k)=PjkPik
  3. 因为 P i k / P j k P_{ik}/P_{jk} Pik/Pjk是标量,但是前者是向量,我们可以将两个向量做内积,将左侧也转化为标量,如下式所示:
    F ( ( w i − w j ) T w k ∼ ) = P i k P j k \mathrm{F\left((w_i-w_j)^T\stackrel{\sim}{w_k}\right)=\frac{P_{ik}}{P_{jk}}} F((wiwj)Twk)=PjkPik这样也便于计算函数 F ( ⋅ ) F(·) F()
  4. 构造前后关系。考虑到 w i w_i wi w k w_k wk一个作为中心词一个作为上下文词,它们的角色是可以互换的,公式在这种角色互换下应保持不变的形式,任意词作为中心词和背景词的词向量应该相等。词与词之间共现次数矩阵应该对称:因此,公式需要继续变换,即
    F ( ( w i − w j ) T w ~ k ) = F ( w i T w ~ k ) F ( w j T w ~ k ) F ( w i T w ~ k ) = P i k = X i k X i . \begin{aligned}F\left((w_i-w_j)^T\tilde{w}_k\right)&=\frac{F(w_i^T\tilde{w}_k)}{F(w_j^T\tilde{w}_k)}\\\\F(w_i^T\tilde{w}_k)&=P_{ik}=\frac{X_{ik}}{X_i}.\end{aligned} F((wiwj)Tw~k)F(wiTw~k)=F(wjTw~k)F(wiTw~k)=Pik=XiXik.
  5. F ( ⋅ ) F(·) F()取指数函数exp,然后在将两边同时取log函数,可得:
    w i T w k ∼ = log ⁡ ( P i k ) = log ⁡ ( X i k ) − log ⁡ ( X i ) \mathrm{w_i^T\stackrel{\sim}{w_k}=\log(P_{ik})=\log(X_{ik})-\log(X_i)} wiTwk=log(Pik)=log(Xik)log(Xi)
  6. 由于 X i X_i Xi k k k无关,将 X i X_i Xi看做一个 w i w_i wi的偏置 b i b_i bi,再为 w k w_k wk添加另一个偏置 b k b_k bk,就可以解决对称性的问题,这样就可以使用词向量来表示词频:
    w i T w ~ k + b i + b ~ k = log ⁡ ( X i k ) w_{i}^{T}\tilde{w}_{k}+b_{i}+\tilde{b}_{k}=\log(X_{ik}) wiTw~k+bi+b~k=log(Xik)
  7. 当某两个单词没有共现情况时,比值会出现0,为解决 l o g 0 log0 log0为无穷小的问题,作者利用均方误差(最小二乘法)计算损失,并且引入了权重函数 f ( X i j ) \mathrm{f}\left(X_{\mathrm{ij}}\right) f(Xij)
    J = ∑ i , j = 1 V f ( X i j ) ( w i T w j ~ + b i + b j ~ − log ⁡ X i j ) 2 \mathrm{J=\sum_{i,j=1}^Vf(X_{ij})(w_i^T\widetilde{w_j}+b_i+\widetilde{b_j}-\log X_{ij})^2} J=i,j=1Vf(Xij)(wiTwj +bi+bj logXij)2
    其中 V V V是词典大小,其中权重函数满足如下条件:
  • 如果两个词没有出现过,则权重为0。
  • f ( x ) f(x) f(x)为一个非递减的函数,经常出现的单词其权重值越大。
  • f ( x ) f(x) f(x)对于较大的 x x x不能取太大的值,所以一些经常出现的词权重不会太大,比如一些停止词。
  1. 作者采用的分段函数如下:
    f ( x ) = { ( x / x max ⁡ ) α   i f   x < x m a x 1 otherwise  . \mathrm{f(x)=\begin{cases}(x/x_{\max})^\alpha&\mathrm{~if~x<x_{max}}\\1&\text{otherwise }.&\end{cases}} f(x)={(x/xmax)α1 if x<xmaxotherwise .在论文中的 α \alpha α为0.75,图像如下所示:

第五章(1):词嵌入进阶之Glove模型讲解与pytorch实现-LMLPHP

模型的表现依赖于 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 作为最终的词向量。

4. 基于pytorch的Glove模型实现

以下代码主要来自GloVe之Pytorch实现_代码部分_glove pytorch_散人stu174的博客-CSDN博客,我进行了重新的执行,并加以详实注释!

4.1 预处理 ,构建一个词汇表和相应的索引

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))  
#<unk>表示为登陆词(新词)  计算列表中每一个单词的词频数,并相加  
word_count["<unk>"]=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["<unk>"] 中。这里的 <unk> 表示未登录词或者新词。

  • 最后,代码将 word_count 字典中的键(即单词)转换为列表,并存储在 idx2word 变量中。然后,使用字典推导式生成一个名为 word2idx 的字典,该字典将词汇表中的每个单词映射为一个唯一的索引。

通过这段代码,你可以得到一个词汇表及相应的索引,可用于后续对文本进行处理、分析或建模等任务。

4.2 构建共现矩阵(co-occurrence matrix)

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 个中心词,输出一条提示信息,指示已经完成的进度。

通过这段代码,你可以得到一个共现矩阵,其中记录了语料库中每个单词与其周围上下文单词的共现频次。

4.3 构建惩罚系数矩阵

# 检查矩阵是否为对称矩阵  
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,用于对共现频次进行惩罚。

  • 首先,代码定义了两个参数 xMaxalphaxMax 表示共现频次的最大值,根据原始论文建议进行设定。alpha 是一个惩罚系数,用于控制共现频次的惩罚程度。

  • 然后,代码创建了一个与 coMatrix 大小相同、元素类型为浮点数的全零矩阵 wMatrix,用于存储惩罚系数。

  • 接下来,代码通过 sys.getsizeof(wMatrix)/(1024*1024) 计算出 wMatrix 占用内存的大小,并通过 print() 函数输出结果。

  • 然后,代码使用两重循环遍历共现矩阵中的每个元素。如果共现频次大于 0,则根据公式 (coMatrix[i][j]/xMax)**alpha if coMatrix[i][j]<xMax else 1 计算对应位置上的惩罚系数,并将结果赋值给 wMatrix[i][j]。这里的判断条件 coMatrix[i][j]<xMax 用于控制共现频次超过 xMax 的情况,即当共现频次大于等于 xMax 时,惩罚系数设置为 1。

  • 最后,代码通过 (i+1) % 1000 == 0 的判断,每处理 1000 个元素,输出一条提示信息,指示已经完成的进度。

通过这段代码,你可以得到一个惩罚系数矩阵 wMatrix,其中记录了共现频次经过惩罚后的值。这个惩罚系数矩阵将用于后续的词向量计算。

4.4 创建用于训练的数据集

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__ 接受三个参数:coMatrixwMatrixtrain_set。在构造方法内,通过调用父类的构造方法 super(dataset, self).__init__() 初始化基类。然后,将传入的 coMatrixwMatrixtrain_set 转换为 torch.Tensor 并分别赋值给类的属性 self.coMatrixself.wMatrixself.train_set,以便在类的其他方法中使用。

类还实现了两个方法:__len____getitem__

__len__ 方法返回数据集的长度,即共有多少个样本。它简单地返回 self.train_set 的长度。

__getitem__ 方法根据索引 idx 返回数据集中指定位置的样本。首先,根据索引 idxself.train_set 中获取对应的中心词和背景词的索引值 ik。然后,通过索引 ikself.coMatrix 中获取共现频次 x_ik,并从 self.wMatrix 中获取惩罚系数 w。最后,返回 ikx_ikw 这四个值作为样本。

通过这个数据集类,你可以将共现矩阵、惩罚系数矩阵和训练集整合在一起,以便在模型训练过程中方便地获取对应的训练样本。你可以使用 DataLoader 将该数据集传入,并设置相应的批次大小、并行加载等参数,从而实现数据的批次化加载和训练。

4.5 构建一个基于 GloVe 模型的词向量训练模型

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.ModuleGloVe 类是一个模型类,用于实现 GloVe(Global Vectors for Word Representation)模型。

在类的构造方法 __init__ 中,接受两个参数 vocab_sized_model,分别表示词汇表大小和嵌入维度。在构造方法内,首先调用父类的构造方法 super(GloVe, self).__init__() 初始化基类。然后,定义了多个成员变量,包括 vocab_sized_model、中心词矩阵 V、背景词矩阵 U、中心词偏置 B_v 和背景词偏置 B_u

VU 是通过 nn.Embedding 定义的两个嵌入层,分别用于表示中心词和背景词的向量。B_vB_u 是通过 nn.Embedding 定义的两个偏置层,用于表示中心词和背景词的偏置项。

接下来,在构造方法中使用均匀分布的随机数对上述参数进行初始化。其中,initrange 是一个初始范围参数,它的取值是 0.5 / vocab_size。通过调用 self.V.weight.data.uniform_(-initrange, initrange) 等方法,对参数进行均匀分布的随机初始化。

forward 方法定义了前向计算过程。它接受 ikx_ikw 四个输入参数,分别表示中心词、背景词、共现频数和惩罚系数。在方法内部,通过调用 self.V(i)self.U(k) 分别获取中心词和背景词的向量表示,以及调用 self.B_v(i)self.B_u(k) 获取中心词和背景词的偏置项。然后,使用这些参数计算损失函数,并返回损失值。

get_embeding 方法用于获取模型的嵌入层参数,返回中心词矩阵和背景词矩阵的和作为嵌入向量。

通过这个模型类,你可以构建一个基于 GloVe 模型的词向量训练模型。在模型的前向计算过程中,会根据输入的中心词和背景词,在嵌入层中查找对应的向量表示,并根据公式计算损失值。最后,通过反向传播更新模型参数,实现词向量的训练。

4.6 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的自然语言处理入门与实践

第五章(1):词嵌入进阶之Glove模型讲解与pytorch实现-LMLPHP

07-18 16:43