论文分享-->GloVe: Global Vectors for Word Representation

本次要分享和总结的论文是 GloVe:Global Vectors for Word Representation ,这是一篇介绍新的 word Representation 方法,该方法现在越来越常被提起,其主要对标的是 word2Vec 方法,论文链接GloVe,其参考的实现代码链接代码实现,其参考的教程Glove: Tutorial

还是那句话,仅仅看看论文,不能说明你看懂了,必须把实现代码过一遍,最好了解到函数级别,才能说明看懂了。

所以接下来,带着代码看论文。

Glove模型优点

论文一上来就说,我们这个模型充分有效的利用了语料库的统计信息,仅仅利用共现矩阵里面的非零元素进行训练,后面又讲道 skip_gram 没有很有效的利用语料库中的一些统计信息。下面我们来看看 Glove 是怎么利用语料库的统计信息的。

Glove Model

首先我们先约定几个变量:

  • wordwordoccurence :即是共现矩阵,定义为 X
  • Xi,j 表示 wordj 出现在 wordi 周边 的次数。
  • Xi=kXik
  • Pi,j=P(j|i)=Xij/Xi

上面那个“周边”该如何定于?论文在实验部分,提到可以把其假设 wordi 前后 context_windows 个词,具体 windows 多大,可以自定义。

接着分析,论文在一个化学领域的数据集上进行了分析,我们假设 wordi ice wordj steam ,这时:

  • 如果选择 wordk solid ,则 Pik/Pjk 会非常大。
  • 如果选择 wordk gas ,则 Pik/Pjk 会非常小。
  • 如果选择 wordk water fashion ,则 Pik/Pjk 会非常接近1。

由此,我们可以发现,相对于简单的 Pij ,上面的比率能更好的区别联系性较大的词汇( solidgas )和联系性不大的词汇( waterfashion )。

我们是否可以 model 出一个模型,能很好的反映上述关系?假设我们找到了这样的一个模型 F

F(wi,wj,w̃ k)=PikPjk
 
这里面 w 表示的是一个词向量, w̃  表示其在 context_windows 内的某个词。显然我们希望这个 F 能表示上述比率的关系。

因为向量空间是线性的,因此我们可以将函数 F 改变为:

F(wiwj,w̃ k)=PikPjk
 

使其仅仅依赖两目标向量的不同之处。可以发现上式右边是一个标量,而左式内参数是一个向量表示,为了保证 F 是个线性结构,我们将上式演变如下:

F((wiwj)Tw̃ k)=PikPjk
 
这样就避免了过多的维度计算,回归到简单的线性关系。

好了,到了论文中最难理解的部分了,以下是我个人对论文这部分的理解。

我们注意到 wi wj 的距离与 wj wi 的距离是相等的,并且共现矩阵是一个对称的矩阵,即 XT==X ,我们把 wi 称为主单词, w̃  称为 wi 的上下文的某个单词,从某种角度看, wi w̃  的角色是可以互换的,它们的地位是相等的,那么我们就希望模型 F 能隐含这种特性。再看看上式:

F((wiwj)Tw̃ k)=PikPjkPkiPkj
 

于是论文中这样做的:

F((wiwj)Tw̃ k)F(wTiw̃ k)F(wTjw̃ k)==F(w̃ Tkwi)F(w̃ Tkwj)

我也不知道这样理解对不对,在网上查了许多资料和讲解,在这一步讲的明显不合理,根本讲不过去,又仔细看了好一会论文,感觉只能这样理解了。

如果你有更好的理解方式,欢迎留言讨论。

上式中 F((wiwj)Tw̃ k)F(wTiw̃ kwTjw̃ k)=F(wTiw̃ k)F(wTjw̃ k)

显然 exp 函数具有这个特性,再有:

F(wTiw̃ k)=Pik=XikXi

则: wTiw̃ k=log(Pik)=log(Xik)log(Xi)

可推出:

log(Xik)=wTiw̃ k+log(Xi)

我们可以把 log(Xi) 纳入到 wi 的偏执项中,为了对称性,也加入 w̃ k 的偏置项,则得如下公式:
log(Xik)=wTiw̃ k+bi+b̃ k

以上模型存在一个问题:他将共现矩阵中每个元素的权重都视作一样,这是不合理的,例如,一个很少出现的词汇携带的信息要比频繁出现的词汇携带的信息要少得多,因此我们需要加上一个 weigthting function f(Xij) 来处理这个问题,使其 loss 能更加关注共现矩阵中出现较频繁的元素。

J=i,j=1Vf(Xij)(wTiw̃ j+bi+b̃ jlogXij)

上式中 V vocab size ,论文中 f(Xij) 定义如下:

f(x)={(x/xmax)α1if(x<xmax)otherwise

这样我们就得到了模型的 loss function ,由此可以 minimize 这个损失函数,来得出 word representationwiw̃ j

实现代码分析

下面分析的代码是基于 sicpynumpy 实现的。

构建词表

根据提供的语料库,构建词表。

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:
    ┆   ┆   continuefor 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 函数为:

J=i,j=1Vf(Xij)(wTiw̃ j+bi+b̃ jlogXij)

这样我们需要为上面生成的共现矩阵 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

与word2Vec 的区别与联系

这是一个必须仔细思考的问题

  • skip_gram 方法中最后一步的 softmax 后,我们希望其周边的词的概率越大越好,这体现在 word2Vec 的损失函数上面,我个人感觉这样没有考虑 word_pairs 之间的距离因素,而在 glove 中考虑到了,上述代码中有体现。
  • 感觉 word2Vecglove 都是在考虑了共现矩阵的基础上建立模型,只是 word2Vec 是一种预测型模型,而 glove 是基于计数的模型。
  • word2Vec 是一种预测型模型,在计算 loss 时,我们希望其 window_size 内的单词的概率能够尽可能的高,我们可以用 SGD 不断训练这个前向神经网络,使其能够学习到较好的 word_repesentation
  • Glove 呢?是一种基于计数的模型,首先会构造一个很大的共现矩阵,就是上述代码中的 cooccurrences 矩阵,其 shape [vocab_size,vocab_size] ,因此我们需要对其进行降纬,降维后的 shape [vocab_size,dim] ,该矩阵的每一行的向量可以看做该单词的表示,我们可以不断的最小化 reconstruction loss 来寻找这样一个矩阵。

你可能感兴趣的:(自然语言处理)