本次要分享和总结的论文是
还是那句话,仅仅看看论文,不能说明你看懂了,必须把实现代码过一遍,最好了解到函数级别,才能说明看懂了。
所以接下来,带着代码看论文。
Glove模型优点
论文一上来就说,我们这个模型充分有效的利用了语料库的统计信息,仅仅利用共现矩阵里面的非零元素进行训练,后面又讲道
Glove Model
首先我们先约定几个变量:
-
word−word−occurence :即是共现矩阵,定义为X 。 -
Xi,j 表示wordj 出现在wordi 周边 的次数。 -
Xi=∑kXik -
Pi,j=P(j|i)=Xij/Xi
上面那个“周边”该如何定于?论文在实验部分,提到可以把其假设
接着分析,论文在一个化学领域的数据集上进行了分析,我们假设
- 如果选择
wordk 为solid ,则Pik/Pjk 会非常大。 - 如果选择
wordk 为gas ,则Pik/Pjk 会非常小。 - 如果选择
wordk 为water 或fashion ,则Pik/Pjk 会非常接近1。
由此,我们可以发现,相对于简单的
我们是否可以
这里面
因为向量空间是线性的,因此我们可以将函数
使其仅仅依赖两目标向量的不同之处。可以发现上式右边是一个标量,而左式内参数是一个向量表示,为了保证
这样就避免了过多的维度计算,回归到简单的线性关系。
好了,到了论文中最难理解的部分了,以下是我个人对论文这部分的理解。
我们注意到
于是论文中这样做的:
我也不知道这样理解对不对,在网上查了许多资料和讲解,在这一步讲的明显不合理,根本讲不过去,又仔细看了好一会论文,感觉只能这样理解了。
如果你有更好的理解方式,欢迎留言讨论。
上式中
显然
则:
可推出:
我们可以把
以上模型存在一个问题:他将共现矩阵中每个元素的权重都视作一样,这是不合理的,例如,一个很少出现的词汇携带的信息要比频繁出现的词汇携带的信息要少得多,因此我们需要加上一个
上式中
这样我们就得到了模型的
实现代码分析
下面分析的代码是基于
构建词表
根据提供的语料库,构建词表。
def build_vocab(corpus):
"""
Build a vocabulary with word frequencies for an entire corpus.
Returns a dictionary `w -> (i, f)`, mapping word strings to pairs of
word ID and word corpus frequency.
"""
logger.info("Building vocab from corpus")
vocab = Counter()
for line in corpus:
┆ tokens = line.strip().split()
┆ vocab.update(tokens)
logger.info("Done building vocab from corpus.")
return {word: (i, freq) for i, (word, freq) in enumerate(vocab.iteritems())}
上述函数就是统计了词频,然后返回了
构建共现矩阵
def build_cooccur(vocab, corpus, window_size=10, min_count=None):
"""
Build a word co-occurrence list for the given corpus.
This function is a tuple generator, where each element (representing
a cooccurrence pair) is of the form
┆ (i_main, i_context, cooccurrence)
where `i_main` is the ID of the main word in the cooccurrence and
`i_context` is the ID of the context word, and `cooccurrence` is the
`X_{ij}` cooccurrence value as described in Pennington et al.
(2014).
If `min_count` is not `None`, cooccurrence pairs where either word
occurs in the corpus fewer than `min_count` times are ignored.
"""
vocab_size = len(vocab)
id2word = dict((i, word) for word, (i, _) in vocab.iteritems())
# Collect cooccurrences internally as a sparse matrix for passable
# indexing speed; we'll convert into a list later
cooccurrences = sparse.lil_matrix((vocab_size, vocab_size),
┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ dtype=np.float64)
for i, line in enumerate(corpus):
┆ if i % 1000 == 0:
┆ ┆ logger.info("Building cooccurrence matrix: on line %i", i)
┆ tokens = line.strip().split()
┆ token_ids = [vocab[word][0] for word in tokens]
┆ for center_i, center_id in enumerate(token_ids):
┆ ┆ # Collect all word IDs in left window of center word
┆ ┆ context_ids = token_ids[max(0, center_i - window_size) : center_i]
┆ ┆ contexts_len = len(context_ids)
┆ ┆ for left_i, left_id in enumerate(context_ids):
┆ ┆ ┆ # Distance from center word
┆ ┆ ┆ distance = contexts_len - left_i
┆ ┆ ┆ # Weight by inverse of distance between words
┆ ┆ ┆ increment = 1.0 / float(distance)
┆ ┆ ┆ # Build co-occurrence matrix symmetrically (pretend we
┆ ┆ ┆ # are calculating right contexts as well)
┆ ┆ ┆ cooccurrences[center_id, left_id] += increment
┆ ┆ ┆ cooccurrences[left_id, center_id] += increment
# Now yield our tuple sequence (dig into the LiL-matrix internals to
# quickly iterate through all nonzero cells)
for i, (row, data) in enumerate(itertools.izip(cooccurrences.rows,
┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ cooccurrences.data)):
┆ if min_count is not None and vocab[id2word[i]][1] < min_count:
┆ ┆ continue
┆ for data_idx, j in enumerate(row):
┆ ┆ if min_count is not None and vocab[id2word[j]][1] < min_count:
┆ ┆ ┆ continue
┆ ┆ yield i, j, data[data_idx]
上述函数,构建了共现矩阵
好了,我们得到共现矩阵了。
初始化参数
根据上面的分析,我们知道最终优化的
这样我们需要为上面生成的共现矩阵
W = (np.random.rand(vocab_size * 2, vector_size) - 0.5) / float(vector_size + 1)# 这里面vocab_size * 2,上半voab_size个存储i_main词的向量,下半部分存储其i_context的词向量。这里我们选择的词向量的dim为vocab_size,其实你可以按照具体情况选取不一样的dim
biases = (np.random.rand(vocab_size * 2) - 0.5) / float(vector_size + 1)
## 后面反向求导用到。
gradient_squared = np.ones((vocab_size * 2, vector_size), dtype=np.float64)
gradient_squared_biases = np.ones(vocab_size * 2, dtype=np.float64)
data = [(W[i_main], W[i_context + vocab_size], biases[i_main : i_main + 1], biases[i_context + vocab_size : i_context + vocab_size + 1], gradient_squared[i_main], gradient_squared[i_context + vocab_size], gradient_squared_biases[i_main : i_main + 1], gradient_squared_biases[i_context + vocab_size: i_context + vocab_size + 1], cooccurrence) for i_main, i_context, cooccurrence in cooccurrences]
模型训练
for (v_main, v_context, b_main, b_context, gradsq_W_main, gradsq_W_context,
┆ ┆gradsq_b_main, gradsq_b_context, cooccurrence) in data:
┆ weight = (cooccurrence / x_max) ** alpha if cooccurrence < x_max else 1
┆ # Compute inner component of cost function, which is used in
┆ # both overall cost calculation and in gradient calculation
┆ #
┆ # $$ J' = w_i^Tw_j + b_i + b_j - log(X_{ij}) $$
┆ cost_inner = (v_main.dot(v_context)
┆ ┆ ┆ ┆ ┆ + b_main[0] + b_context[0]
┆ ┆ ┆ ┆ ┆ - log(cooccurrence))
┆ # Compute cost
┆ #
┆ # $$ J = f(X_{ij}) (J')^2 $$
┆ cost = weight * (cost_inner ** 2)
┆ # Add weighted cost to the global cost tracker
┆ global_cost += 0.5 * cost
┆ # Compute gradients for word vector terms.
┆ #
┆ # NB: `main_word` is only a view into `W` (not a copy), so our
┆ # modifications here will affect the global weight matrix;
┆ # likewise for context_word, biases, etc.
┆ grad_main = weight * cost_inner * v_context
┆ grad_context = weight * cost_inner * v_main
┆ # Compute gradients for bias terms
┆ grad_bias_main = weight * cost_inner
┆ grad_bias_context = weight * cost_inner
┆ # Now perform adaptive updates
┆ v_main -= (learning_rate * grad_main / np.sqrt(gradsq_W_main))
┆ v_context -= (learning_rate * grad_context / np.sqrt(gradsq_W_context))
┆ b_main -= (learning_rate * grad_bias_main / np.sqrt(gradsq_b_main))
┆ b_context -= (learning_rate * grad_bias_context / np.sqrt(
┆ ┆ ┆ gradsq_b_context))
┆ # Update squared gradient sums
┆ gradsq_W_main += np.square(grad_main)
┆ gradsq_W_context += np.square(grad_context)
┆ gradsq_b_main += grad_bias_main ** 2
┆ gradsq_b_context += grad_bias_context ** 2
与word2Vec 的区别与联系
这是一个必须仔细思考的问题
-
skip_gram 方法中最后一步的softmax 后,我们希望其周边的词的概率越大越好,这体现在word2Vec 的损失函数上面,我个人感觉这样没有考虑word_pairs 之间的距离因素,而在glove 中考虑到了,上述代码中有体现。 - 感觉
word2Vec、glove 都是在考虑了共现矩阵的基础上建立模型,只是word2Vec 是一种预测型模型,而glove 是基于计数的模型。 -
word2Vec 是一种预测型模型,在计算loss 时,我们希望其window_size 内的单词的概率能够尽可能的高,我们可以用SGD 不断训练这个前向神经网络,使其能够学习到较好的word_repesentation 。 - 而
Glove 呢?是一种基于计数的模型,首先会构造一个很大的共现矩阵,就是上述代码中的cooccurrences 矩阵,其shape 为[vocab_size,vocab_size] ,因此我们需要对其进行降纬,降维后的shape 为[vocab_size,dim] ,该矩阵的每一行的向量可以看做该单词的表示,我们可以不断的最小化reconstruction loss 来寻找这样一个矩阵。