GloVe模型的原理与实现

GloVe

论文:https://www.aclweb.org/anthology/D14-1162

相较于skip-gram+softmax模型,GloVe利用了语料数据集的全局统计信息,并使用最小二乘法重新设计目标函数。

论文解读

相关的数学符号定义如下:

词-词共现矩阵:X

词j出现在词i上下文的次数:

表示出现在词i上下文的所有词的次数总和

表示词j出现在词i上下文窗口中的概率

假设i=ice,j=steam,这两个单词的关系可以通过学习它们分别与第三个词的共现概率的比率来表示。

假设k=solid,那么k与i更加相关,因此我们希望的值比较大。同样的,k=gas,那么k与j更加相关,因此我们希望的值很小。那么对于k=water或者fashion时,k同时与i和j相关或者不相关,因此应该接近1。上述规律如下表所示:

glove-1

综上所述,比率涉及到i,jk三个词,这个比率的一般形式表示如下:

此时,为词向量。

后面的公式演变都是为了找到函数F使得,式(1)左右两边接近或相等。

为了表示词向量在向量空间的线性关系,式(1)使用差分的形式进行表示:

式(2)的左边参数为向量,右边为标量,我们可以使用比较复杂的神经网络来实现这个F函数,但是神经网络会把我们试图获取到的线性关系抵消掉。因此,使用向量点乘的方式来表示左边的参数:

首先,令函数F是同质的,则可得到:

式(3)和式(4)一一对应,可得到:

,则式(4)表示为

式(6)中,独立于k,所以该项可以都放在的偏置项中;最后再为加上一个偏置项,使得得到的式(7)满足词向量中的线性结构,即i,k同质:

式(7)可以发现,我们可以通过词向量和偏执项一起来表示词共现次数的log值。

我们的目标是使得等式两边的差距最小化,因此自然而言的就想到使用平方差损失函数来构建目标函数。

还有一个问题,那就是词的权重都是一样的,而我们希望对于很少出现(可能是噪声),或经常出现的词的权重不要太大。因此,在式(7)的基础上引入权重:

此处,V为字典大小,通过实验,作者得到权重函数如下所示:

glove-2

此处

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import numpy as np

class GloVe(object):
"""
parameters:
n:the embedding dimension.
xmax : int (default: 100)
Word pairs with frequency greater than this are given weight 1.0.
Word pairs with frequency under this are given weight
(c / xmax) ** alpha, where c is the co-occurence count
alpha:float (default: 0.75)
Exponent in the weighting function (see [1]_, eq. (9)).
learning_rate: float (default: 0.01)
Learning rate used for the Adagrad optimizer.
max_iter : int (default: 100)
Number of training epochs. Default: 100
"""
def __init__(self, n=100,xmax=100,alpha=0.75,
max_iter=100,learning_rate=0.05,**kwargs):
self.n = n
self.xmax = xmax
self.alpha = alpha
self.max_iter = max_iter
self.learning_rate = learning_rate
self.n_words = None
self.errors = list()

def fit(self, X, vocab=None):
"""
parameter:
X:array-like of shape = [n_words,n_words] co-occurrence matrix
vocab: iterable or None,Rownames for 'X'
returns:
embedding matrix:[n_words,embedding_dim]
"""
weights, log_coincidence=self._initialize(X)
return self._fit(X, weights, log_coincidence,vocab=vocab)

def _initialize(self, coincidence):
self.n_words = coincidence.shape[0]
bounded = np.minimum(coincidence, self.xmax)#式(9)中的x_max
weights = (bounded/float(self.xmax))**self.alpha
log_coincidence = log_of_array_ignoring_zeros(coincidence)#目标函数中的log项
return weights, log_coincidence

def _fit(self, coincidence, weights, log_coincidence, vocab=None):
self._initialize_w_c_b(self.n_words,vocab)

for iteration in range(self.max_iter):
pred = self._make_prediction()
gradients, error = self._get_gradients_and_error(pred,log_coincidence,weights)
self.errors.append(error)
self._apply_updates(gradients)
return self.W+self.C

def _initialize_w_c_b(self, n_words, vocab):
self.W = randmatrix(n_words,self.n)
self.C = randmatrix(n_words, self.n)

self.bw = randmatrix(n_words,1)
self.bc = randmatrix(n_words, 1)
self.ones= np.ones((n_words,1))

def _make_prediction(self):
'''式(7)的左侧'''
pred = np.dot(self.W, self.C.T)+self.bw+self.bc.T
return pred

def _get_gradients_and_error(self,predictions,log_coincidence,weights):
diffs = np.square(predictions-log_coincidence)
weighted_diffs = np.multiply(weights, diffs)
wgrad = weighted_diffs.dot(self.C)
cgrad = weighted_diffs.T.dot(self.W)
bwgrad = weighted_diffs.sum(axis=1).reshape(-1,1)
bcgrad = weighted_diffs.sum(axis=0).reshape(-1,1)
error = (0.5*np.multiply(weights, diffs**2)).sum()

return {'W':wgrad,'C':cgrad,'bw':bwgrad,'bc':bcgrad},error

def _apply_updates(self, gradients):
'''Apply AdaGrad update to parameters'''
if not hasattr(self, 'optimizers'):
self.optimizer={obj:AdaGradOptimizer(self.learning_rate) for obj in ['W','C','bw','bc']}

self.W -= self.optimizer['W'].get_step(gradients['W'])
self.C -= self.optimizer['C'].get_step(gradients['C'])
self.bw -= self.optimizer['bw'].get_step(gradients['bw'])
self.bc -= self.optimizer['bc'].get_step(gradients['bc'])

class AdaGradOptimizer(object):
def __init__(self, learning_rate, initial_accumulator_value=0.1):
self.learning_rate = learning_rate
self.initial_accumulator_value = initial_accumulator_value
self._momentum = None

def get_step(self, grad):
"""Compuet the 'step' to take for the next gradient descent update."""
if self._momentum is None:
self._momentum = self.initial_accumulator_value*np.ones_like(grad)
self._momentum += grad**2
return self.learning_rate*grad/np.sqrt(self._momentum)

def log_of_array_ignoring_zeros(M):
log_M = M.copy()
mask = log_M>0
log_M[mask]=np.log(log_M[mask])
return log_M

def randmatrix(m, n, random_seed=None):
"""参数初始化函数"""
val = np.sqrt(6.0 / (m + n))
np.random.seed(random_seed)
return np.random.uniform(-val, val, size=(m, n))

参考资料