Word2Vec 子采样——实现

Word2Vec Subsampling -- Implementation

我正在 Pytorch 和 Tensorflow2 中实现 Skipgram 模型。我对频繁词的子采样的实现有疑问。从论文中逐字逐句,子采样词 wi 的概率计算为

其中t是一个自定义阈值(通常是一个很小的值,例如0.0001),f是文档中单词的出现频率.尽管作者以不同但几乎相同的方式实现了它,但让我们坚持这个定义。

计算 P(wi) 时,我们可能会得到负值。例如,假设我们有 100 个单词,其中一个单词出现的频率比其他单词高得多(我的数据集就是这种情况)。

import numpy as np
import seaborn as sns

np.random.seed(12345)

# generate counts in [1, 20]
counts = np.random.randint(low=1, high=20, size=99)

# add an extremely bigger count
counts = np.insert(counts, 0, 100000)

# compute frequencies
f = counts/counts.sum()

# define threshold as in paper
t = 0.0001

# compute probabilities as in paper
probs = 1 - np.sqrt(t/f)
sns.distplot(probs);

问:使用此 "probability" 实现子采样的正确方法是什么?

作为附加信息,我在 keras 中看到函数 keras.preprocessing.sequence.make_sampling_table 采用不同的方法:

def make_sampling_table(size, sampling_factor=1e-5):
    """Generates a word rank-based probabilistic sampling table.
    Used for generating the `sampling_table` argument for `skipgrams`.
    `sampling_table[i]` is the probability of sampling
    the i-th most common word in a dataset
    (more common words should be sampled less frequently, for balance).
    The sampling probabilities are generated according
    to the sampling distribution used in word2vec:
    ```
    p(word) = (min(1, sqrt(word_frequency / sampling_factor) /
        (word_frequency / sampling_factor)))
    ```
    We assume that the word frequencies follow Zipf's law (s=1) to derive
    a numerical approximation of frequency(rank):
    `frequency(rank) ~ 1/(rank * (log(rank) + gamma) + 1/2 - 1/(12*rank))`
    where `gamma` is the Euler-Mascheroni constant.
    # Arguments
        size: Int, number of possible words to sample.
        sampling_factor: The sampling factor in the word2vec formula.
    # Returns
        A 1D Numpy array of length `size` where the ith entry
        is the probability that a word of rank i should be sampled.
    """
    gamma = 0.577
    rank = np.arange(size)
    rank[0] = 1
    inv_fq = rank * (np.log(rank) + gamma) + 0.5 - 1. / (12. * rank)
    f = sampling_factor * inv_fq

    return np.minimum(1., f / np.sqrt(f))

比起论文,我更倾向于相信已部署的代码,尤其是在像 word2vec 这样的情况下,论文作者发布的原作者 word2vec.c code 已被广泛使用并用作模板其他实现。如果我们看看它的子采样机制...

        if (sample > 0) {
          real ran = (sqrt(vocab[word].cn / (sample * train_words)) + 1) * (sample * train_words) / vocab[word].cn;
          next_random = next_random * (unsigned long long)25214903917 + 11;
          if (ran < (next_random & 0xFFFF) / (real)65536) continue;
        }

...我们看到那些计数很小的单词 (.cn) 在原始公式中可能给出负值,而不是在这里给出大于 1.0 的值,因此永远不会小于 long-random-masked-and-scaled 永远不会超过 1.0 ((next_random & 0xFFFF) / (real)65536)。因此,作者的意图似乎是让原始公式的所有负值都表示 "never discard"。

根据 keras make_sampling_table() 评论和实施,他们 没有 参考实际的词频.相反,他们假设一个基于词序的类似 Zipf 的分布来合成一个模拟的词频。

如果他们的假设成立——相关词来自具有类似 Zipf 频率分布的自然语言语料库——那么我希望他们的采样概率接近下采样概率是根据真实频率信息计算出来的。对于大多数用途来说,这可能是 "close enough"。

我不确定他们为什么选择这个近似值。也许他们通常过程的其他方面在这一步之前还没有保持真实的频率,并且他们期望始终使用自然语言文本,其中假定的频率通常是真实的。

(幸运的是,因为人们经常想将频率归因于 public 组词向量,这些词向量已经降低了真实计数但仍然从最频繁到最不频繁排序,只是几天前我写了 ——类似于这个 keras 代码所做的。)

但是,如果您使用的数据符合他们的假设(与您的合成或描述的数据集一样),他们的抽样概率将与你会自己计算什么,使用任何形式的使用真实词频的原始公式。

特别是,想象一个分配有一个令牌一百万次,然后一百个令牌每个只出现 10 次。 "rank" 列表中的那百个标记的顺序是任意的——实际上,它们在频率上都是相关的。但是基于模拟的方法,通过在该排序上拟合 Zipfian 分布,实际上会对它们中的每一个进行非常不同的采样。一个 10 次出现的单词幸运地排在第二位,它的采样率会低得多,就好像它出现的频率高得多一样。并且 1st-rank "tall head" 值,通过使其真实频率 *under-*approximated,将比其他方式减少采样。这些效果似乎都没有好处,也不符合频繁词下采样选项的精神——它应该只 "thin out" 非常频繁的词,并且在所有情况下都在原始语料库中留下彼此相同频率的词在下采样语料库中大致等效地呈现给彼此。

因此,对于您的情况,我会使用原始公式(需要特殊处理负值的丢弃概率)或 word2vec.c practical/inverted 实现(保持饱和的概率为 1.0),而不是 keras 风格的近似。

(作为一个完全独立的说明,如果您使用的是负采样,那么仍然可能与您的 dataset/purposes 相关:还有另一个参数控制负示例的相对采样,通常固定在 0.75 在早期的实现中,即 one paper has suggested can usefully vary for non-natural-language token distributions & recommendation-related end-uses. This parameter is named ns_exponent in the Python gensim implementation, but simply a fixed power value internal to a sampling-table pre-calculation in the original word2vec.c code.)