原理
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(wj∣wi)=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(wk∣wj)P(wk∣wi)。
为什么要这么做呢,可以看上面的图,如果词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((wi−wj)⋅wkT)=PjkPik
这个
(
w
i
−
w
j
)
(w_i - w_j)
(wi−wj)可以表示两个概率之间的相除关系,而乘
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})
(wi−wj)⋅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})
wi⋅wkT=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
wi⋅wkT−log(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
(wi⋅wkT−log(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=1∑V(wi⋅wjT−log(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=1∑V(wi⋅wjT−log(Xij)+log(Xi))2
由于
X
i
X_i
Xi和相关性无关所以我们可以把它看成一个bias,同时
w
i
⋅
w
j
T
w_i\cdot w_j^T
wi⋅wjT也有一个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=1∑V(wi⋅wjT+bi+bj−log(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<xmaxx≥xmax
我们把它当成每个损失的权重,这样就可以解决上述的问题了
所以我们最终的代价函数就是:
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=1∑Vweight(Xij)(wi⋅wjT−log(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>', '改革', '关系'])
"""
可以看到,我们可以找到与我们想要词之间比较接近的词。