本次要分享和总结的论文是 GloVe:Global Vectors for Word Representation ,这是一篇介绍新的 word Representation 方法,该方法现在越来越常被提起,其主要对标的是 word2Vec 方法,论文链接GloVe,其参考的实现代码链接代码实现,其参考的教程Glove: Tutorial
还是那句话,仅仅看看论文,不能说明你看懂了,必须把实现代码过一遍,最好了解到函数级别,才能说明看懂了。
所以接下来,带着代码看论文。
论文一上来就说,我们这个模型充分有效的利用了语料库的统计信息,仅仅利用共现矩阵里面的非零元素进行训练,后面又讲道 skip_gram 没有很有效的利用语料库中的一些统计信息。下面我们来看看 Glove 是怎么利用语料库的统计信息的。
首先我们先约定几个变量:
上面那个“周边”该如何定于?论文在实验部分,提到可以把其假设 wordi 前后 context_windows 个词,具体 windows 多大,可以自定义。
接着分析,论文在一个化学领域的数据集上进行了分析,我们假设 wordi 为 ice , wordj 为 steam ,这时:
由此,我们可以发现,相对于简单的 Pij ,上面的比率能更好的区别联系性较大的词汇( solid、gas )和联系性不大的词汇( water、fashion )。
我们是否可以 model 出一个模型,能很好的反映上述关系?假设我们找到了这样的一个模型 F :
因为向量空间是线性的,因此我们可以将函数 F 改变为:
使其仅仅依赖两目标向量的不同之处。可以发现上式右边是一个标量,而左式内参数是一个向量表示,为了保证 F 是个线性结构,我们将上式演变如下:
好了,到了论文中最难理解的部分了,以下是我个人对论文这部分的理解。
我们注意到 wi 到 wj 的距离与 wj 到 wi 的距离是相等的,并且共现矩阵是一个对称的矩阵,即 XT==X ,我们把 wi 称为主单词, w̃ 称为 wi 的上下文的某个单词,从某种角度看, wi 与 w̃ 的角色是可以互换的,它们的地位是相等的,那么我们就希望模型 F 能隐含这种特性。再看看上式:
于是论文中这样做的:
我也不知道这样理解对不对,在网上查了许多资料和讲解,在这一步讲的明显不合理,根本讲不过去,又仔细看了好一会论文,感觉只能这样理解了。
如果你有更好的理解方式,欢迎留言讨论。
上式中 F((wi−wj)T∗w̃ k)=F(wTi∗w̃ k−wTj∗w̃ k)=F(wTiw̃ k)F(wTjw̃ k)
显然 exp 函数具有这个特性,再有:
则: wTiw̃ k=log(Pik)=log(Xik)−log(Xi)
可推出:
以上模型存在一个问题:他将共现矩阵中每个元素的权重都视作一样,这是不合理的,例如,一个很少出现的词汇携带的信息要比频繁出现的词汇携带的信息要少得多,因此我们需要加上一个 weigthting function f(Xij) 来处理这个问题,使其 loss 能更加关注共现矩阵中出现较频繁的元素。
上式中 V 为 vocab size ,论文中 f(Xij) 定义如下:
这样我们就得到了模型的 loss function ,由此可以 minimize 这个损失函数,来得出 word representation:wi、w̃ j 。
下面分析的代码是基于 sicpy、numpy 实现的。
根据提供的语料库,构建词表。
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())}
上述函数就是统计了词频,然后返回了 word:(i,freq) 的字典, i 是词对应的序号。
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]
上述函数,构建了共现矩阵 cooccurrences ,注意参数 window_size ,对于一个 word ,我们只选择与他距离在 window_size 内的词汇,组成一对 word_pairs 并且 Xij 的值与 word_pairs 之间的距离成反比,这样距离更近的词就具有更高的权重。这一做法,在论文实验部分有详细的介绍。上述函数返回 (i_main,i_context,cooccurrence) 的生成器。
好了,我们得到共现矩阵了。
根据上面的分析,我们知道最终优化的 loss 函数为:
这样我们需要为上面生成的共现矩阵 cooccurrence 中的每个词初始化出其对应的词向量,也即是上式中的 wTi 、 w̃ j ,并且初始化其偏置项 bi 、 b̃ j 。
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
这是一个必须仔细思考的问题