NLP--Glove

原理

word2vec在词表示上有着很好的效果,但是word2vec忽略了词在统计上的一些规律,因为如此Word2vec需要学习大量的数据才能得到好的效果,而Glove则考虑了词之间统计上的数值关系。

co-occurrence probability

Glove认为co-occurrence probability可以对词的意思进行编码,首先先看统计上很容易估算出的一个后验概率
P ( w j ∣ w i ) = X i j X i = P i j P(w_j|w_i)=\frac{X_{ij}}{X_i}=P_{ij} P(wjwi)=XiXij=Pij
其中 w i w_i wi是中心词, w j w_j wj是背景词 X i j X_{ij} Xij是语料库中 w i , w j w_i,w_j wi,wj同时出现的次数,即 ∣ p o s ( w i ) − p o s ( w j ) ∣ ≤ w i n d o w |pos(w_i)-pos(w_j)|\leq window pos(wi)pos(wj)window pos(x)表示x在文中的位置,window为窗口大小。同理 X i X_i Xi为对应的这个词在语料库中出现的次数。
那么我们就可以估计很多次的这个后验概率,比如下图
在这里插入图片描述
然后所谓的co-occurrence probability就是再引入一个词K。
两个词 w i w_i wi w j w_j wj 的co-occurrence probability就是 P ( w k ∣ w i ) P ( w k ∣ w j ) \frac{P(w_k|w_i)}{P(w_k|w_j)} P(wkwj)P(wkwi)
为什么要这么做呢,可以看上面的图,如果词i和词j关于词k的关系都很近那么他们的商就会接近于1,如果一个远一个近那么他们的商就会很大或者很小,这样就可以借用第三个词来表现出两个词之间的差异了,商接近于1肯能说明词k是一个停用词,或者是一个很长用的词,不能体现出词i和j的区别。
在这里插入图片描述
那么co-occurrence probability怎么和向量联系起来呢?
我们可以这么做,令
F ( ( w i − w j ) ⋅ w k T ) = P i k P j k F((w_i - w_j)\cdot w_k^T)=\frac{P_{ik}}{P_{jk}} F((wiwj)wkT)=PjkPik
这个 ( w i − w j ) (w_i - w_j) (wiwj)可以表示两个概率之间的相除关系,而乘 w k w_k wk可以控制他们之间商的大小,然后得出的结果通过函数F最后得到结果。
假设这个F函数是一个自然数底数的指数函数,那么两边同时取log就得到
( w i − w j ) ⋅ w k T = l o g ( P i k ) − l o g ( P j k ) (w_i - w_j)\cdot w_k^T=log(P_{ik}) - log(P_{jk}) (wiwj)wkT=log(Pik)log(Pjk)
那我们自然就可以想到一个对应关系那就是
w i ⋅ w k T = l o g ( P i k ) = l o g ( X i k X i ) w_i\cdot w_k^T=log(P_{ik})=log(\frac{X_{ik}}{X_i}) wiwkT=log(Pik)=log(XiXik)
我们进行移项就可以得到
w i ⋅ w k T − l o g ( P i k ) = 0 w_i\cdot w_k^T-log(P_{ik})=0 wiwkTlog(Pik)=0
即我们希望这个式子尽可能的接近于0,所以我们需要进行平方得到
( w i ⋅ w k T − l o g ( P i k ) ) 2 (w_i\cdot w_k^T-log(P_{ik}))^2 (wiwkTlog(Pik))2
从而我们也可以退出代价函数了
C ( θ ) = ∑ i , j = 1 V ( w i ⋅ w j T − l o g ( P i j ) ) 2 C(\theta) = \sum\limits_{i,j=1}^V(w_i\cdot w_j^T-log(P_{ij}))^2 C(θ)=i,j=1V(wiwjTlog(Pij))2
我们把 l o g ( P i j ) log(P_{ij}) log(Pij)展开得到
C ( θ ) = ∑ i , j = 1 V ( w i ⋅ w j T − l o g ( X i j ) + l o g ( X i ) ) 2 C(\theta) = \sum\limits_{i,j=1}^V(w_i\cdot w_j^T-log(X_{ij}) + log(X_{i}))^2 C(θ)=i,j=1V(wiwjTlog(Xij)+log(Xi))2
由于 X i X_i Xi和相关性无关所以我们可以把它看成一个bias,同时 w i ⋅ w j T w_i\cdot w_j^T wiwjT也有一个bias我们既做 b j b_j bj我们把 X i X_i Xi记做 b i b_i bi最终可以得到
C ( θ ) = ∑ i , j = 1 V ( w i ⋅ w j T + b i + b j − l o g ( X i j ) ) 2 C(\theta) = \sum\limits_{i,j=1}^V(w_i\cdot w_j^T + b_i + b_j -log(X_{ij}))^2 C(θ)=i,j=1V(wiwjT+bi+bjlog(Xij))2
到此为止还有一个问题没有解决,那就是对于所有的 X i j X_{ij} Xij并不一定都在语料库中,有的直接就是0,所以我们需要给这个每个损失函数一个权重使他们 X i j X_{ij} Xij出现越多的权重越大,但是也不能太大,我们也同时设置一个上界,超出这个上界之后即使再大我们也让他权重为1。
通过大量的实现Glove的作者得出了这么一个函数。
w e i g h t ( x ) = { ( x x m a x ) 0.75 x < x m a x 1 x ≥ x m a x weight(x)=\left\{ \begin{matrix} (\frac{x}{x_{max}})^{0.75} && x < x_{max}\\ 1 && x \geq x_{max} \end{matrix} \right. weight(x)={(xmaxx)0.751x<xmaxxxmax
在这里插入图片描述
我们把它当成每个损失的权重,这样就可以解决上述的问题了
所以我们最终的代价函数就是:
C ( θ ) = ∑ i , j = 1 V w e i g h t ( X i j ) ( w i ⋅ w j T − l o g ( X i j ) + l o g ( X i ) ) 2 C(\theta) = \sum\limits_{i,j=1}^Vweight(X_{ij})(w_i\cdot w_j^T-log(X_{ij}) + log(X_{i}))^2 C(θ)=i,j=1Vweight(Xij)(wiwjTlog(Xij)+log(Xi))2
至此Glove的理论部分就讲完了

实现

实现的代码参考了这位老哥的
https://blog.csdn.net/a553181867/article/details/104837957
代码已经传到gayhub了,包括语料库:https://github.com/zipper112/Glove
我分成了几个文件来写:
首先是config文件,里面包含了路径和设置

CORPUS = './source/corpus.txt'
LOCAL_CORPUS = './source/localcorpus.txt' # 这个路径是用来存从CORPUS中切出的一小片语料,可以用来测试模型的正确性,用于调试
MODEL = './source/embedding.pth' # 存embedding
VOC = './source/voc.txt' # 存放词典

BATCH_SIZE = 80
EMB_SIZE = 300
VOC_SIZE = 10000
W = 3
EPOCH = 4 # 迭代次数

其次是processing文件,里面放处理数据的函数

import config
import numpy as np
from collections import Counter

这是一个切片函数,这个函数要不要都行,我当时调试时写了这个,目的是切出一小片数据集

def toLocal(num): # 切出前num行,如果是是None则是切出全部
    tmp = None
    with open(config.CORPUS, encoding='utf-8', mode='r') as rs:
        tmp = rs.read().split('\n')
    if num == None:
        num = len(tmp)
    with open(config.LOCAL_CORPUS, encoding='utf-8', mode='w') as ws:
        ws.write('\n'.join(tmp[:num]))

然后是处理语料库的函数

def loadCorpus(useLocal=None):
    content, words, corpus = None, None, config.CORPUS
    if useLocal:
        corpus = config.LOCAL_CORPUS
    with open(corpus, encoding='utf-8', mode='r') as rs:
        content = rs.read()
    words = content.split()
    content = [sentence.split(' ') for sentence in content.split('\n')]# content里放所有句子,每个句子用一个列表表示,列表里是一个个的词
    voc = dict(Counter(words).most_common(config.VOC_SIZE - 1)) # 取出前VOC_SIZE - 1个词作为词典
    voc['<UNK>'] = len(words) - sum(voc.values()) # 剩下的标记为UNK

    voc_chr = sorted(voc.keys())
    word2idx = {word: i for i, word in enumerate(voc_chr)}
    idx2word = {i: word for i, word in enumerate(voc_chr)}

    with open(config.VOC, encoding='utf-8', mode='w') as ws:# 保存词典
        ws.write('\n'.join(voc_chr))
    
    return voc, word2idx, idx2word, content

接下来是计算一个矩阵,这个矩阵中的第i行第j个位置表示 X i j X_{ij} Xij,这里计算我们只需要计算每个词的右边w个就可以了,因为只要 C o o M a t r i x [ i ] [ j ] CooMatrix[i][j] CooMatrix[i][j]存在那么 C o o M a t r i x [ j ] [ i ] CooMatrix[j][i] CooMatrix[j][i]也一定存在,同时我们把所有同时出现过的词对都给存起来当做数据集的一份。

def ComputeCooccurance(content: list, word2idx: dict):
    CooMatrix = np.zeros(shape=(config.VOC_SIZE, config.VOC_SIZE), dtype=np.float32)
    data = []
    for i in range(len(content)):
        for j in range(len(content[i])):
            for k in range(j + 1, min(j + config.W + 1, len(content[i]))): # 只计算词的右边的w个
                xx, yy = word2idx.get(content[i][j], word2idx['<UNK>']), word2idx.get(content[i][k], word2idx['<UNK>'])
                data.append((xx, yy))
                data.append((yy, xx)) # 存词对
                CooMatrix[xx][yy] += 1
                CooMatrix[yy][xx] += 1
    return CooMatrix, data

然后我们计算权重矩阵,我们直接使用numpy的索引功能来快速的计算出整个权重矩阵

def WeightScale(CooMatrix: np.array):
    weightMatrix = np.zeros_like(CooMatrix)
    idx, didx = CooMatrix < 100, CooMatrix >= 100
    weightMatrix = CooMatrix.copy()
    weightMatrix[idx] /= 100
    weightMatrix[idx] **= 0.75
    weightMatrix[didx] = 1
    return weightMatrix

最后我又做了一个LoadVoc的功能用来导入保存好的词典

def loadVoc():
    idx2word, word2idx = None, None
    with open(config.VOC, encoding='utf-8', mode='r') as rs:
        idx2word = rs.read().split('\n')
    
    word2idx = {word: i for i, word in enumerate(idx2word)}
    idx2word = {i: word for i, word in enumerate(idx2word)}
    return word2idx, idx2word

接下来就是model文件,这里放了对应的Dataset类和对应的Glove模型

import torch
from torch import nn
from torch.utils import data

首先是构建Dataset

class GloveDataset(data.Dataset):
    def __init__(self, weightMatrix, cooMatrix, data): # 分别传入权重矩阵,Coo矩阵,和之前顺便计算出的data
        super(GloveDataset, self).__init__()
        self.train_set = data
        self.weightMatrix = weightMatrix
        self.cooMatrix = cooMatrix
        
    def __len__(self):
        return len(self.train_set)
    
    def __getitem__(self, idx):
        i, j = self.train_set[idx]
        return torch.tensor(i, dtype=torch.long), torch.tensor(j, dtype=torch.long), torch.tensor(self.cooMatrix[i][j]), torch.tensor(self.weightMatrix[i][j])

接下来就是Glove模型

class Glove(nn.Module):
    def __init__(self, voc_size, embedding_size):
        super(Glove, self).__init__()
        self.emb1 = nn.Embedding(voc_size, embedding_size) # 表示中心词embedding
        self.emb2 = nn.Embedding(voc_size, embedding_size)# 表示背景词embedding
        self.bias1 = nn.Embedding(voc_size, 1) # 表示中心词和背景词乘积的bias的矩阵
        self.bias2 = nn.Embedding(voc_size, 1) # 表示Xi的bias矩阵
    
    def forward(self, X, y, coo, weight):
        L = self.emb1(X)
        R = self.emb2(y)
        bL = self.bias1(X)
        bR = self.bias2(y)

        loss = torch.sum(L * R, dim=1) # 先做乘积
        loss = (loss + bL + bR + torch.log(coo)) ** 2 # 按公式计算
        loss = loss * weight # 乘以weight
        return loss.mean() # 取均值
    
    def getEmbedding(self): # 拿到对应的词向量
        return self.emb1.weight.to(torch.device('cpu'))

接下来就是训练了

import torch
import Processing
import Model
import config

device = torch.device('cuda:0')
voc, word2idx, idx2word, content = Processing.loadCorpus(useLocal=True)
print('Done1')
CooMatrix, data = Processing.ComputeCooccurance(content, word2idx)
print('Done2')
weightMatrix = Processing.WeightScale(CooMatrix)
print('Done3')
dataset = Model.GloveDataset(weightMatrix, CooMatrix, data)
print('Done4')
dataloader = torch.utils.data.DataLoader(dataset, batch_size=config.BATCH_SIZE)
glove = Model.Glove(config.VOC_SIZE, config.EMB_SIZE).to(device)
optm = torch.optim.Adam(glove.parameters(), lr=0.001)
print('Done5')
print(len(dataset))


for i in range(config.EPOCH):
    for j, X in enumerate(dataloader):
        X, y, Coo, wei = X
        X, y, Coo, wei = X.to(device), y.to(device), Coo.to(device), wei.to(device)
        loss = glove(X, y, Coo, wei)
        optm.zero_grad()
        loss.backward()
        optm.step()
        if j % 2000 == 0:
            print(loss.item())

torch.save(glove.getEmbedding(), config.MODEL)

我电脑显卡不是很好,练了快两个小时。
然后写出一个函数来计算一下相似度然后找一下最近的词,测试一下这个模型如何。

import Processing
import torch
import config
import scipy.spatial
import numpy as np

word2idx, idx2word = Processing.loadVoc()
e = torch.load(config.MODEL).detach().numpy()

def getMostNearest(s, embedding):
    vec = embedding[word2idx[s]]
    tmp = [scipy.spatial.distance.cosine(v, vec) for v in embedding]
    res = [idx2word[i] for i in np.argsort(np.array(tmp))[:100]]
    res = list(filter(lambda x: len(x)>1, res)) # 很多停用词都是单个字的,所以使用一个filter来进行简单的过滤
    return s, res[:10]

print(getMostNearest('人民', e))
print(getMostNearest('科技', e))
print(getMostNearest('美国', e))
print(getMostNearest('经济', e))
"""
('人民', ['人民', '建设', '生产', '他们', '人员', '企业', '市场', '我们', '自己', '社会'])
('科技', ['科技', '技术', '社会', '活动', '研究', '工作', '教育', '要求', '文化', '以及'])
('美国', ['美国', '中国', '政府', '日本', '<UNK>', '一些', '我国', '他们', '要求', '决定'])
('经济', ['经济', '发展', '国家', '我国', '社会', '中国', '文化', '<UNK>', '改革', '关系'])
"""

可以看到,我们可以找到与我们想要词之间比较接近的词。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值