0%

Word2Vec学习及基于tensorflow的实现

word2vec是自然语言处理中非常重要的基础。最近,读了一下相关论文,学习了相关的模型细节,并使用tensorflow实现了网络结构,对中文维基百科语料库进行了训练。相关论文及实现参考见文末。

关于词向量

在自然语言处理中,第一步就是要将自然语言数字化。所以,词向量的表示方式就很重要。最常见的有One-hot Representation和 Distributed Representation。

One-hot Representation

这是一种很直接的表示方式,一个词向量中只有一个维度的值是1,其它的绝大多数是0。例如“dog”这个词的one-hot表示为[0, 0, 1, 0, ......]

这种表示方法得到的向量是非常稀疏的,维度非常大,会造成维度灾难。这种表示方法有一个很大的问题,如果两个词之间存在着关联,如果以one-hot方式表示的话,这部分关联信息就会丧失。模型几乎不能利用从“dog”学来的东西来处理“cat”,而distributed的表示方法能很好的解决这个问题。

Distributed Representation

这种表示方法下,词向量的维度比较低,一般不会超过几百维。它的主要思想是,(句法和语义上)相近的词有相近的向量表示。这个相近的向量表示,可以用两个词向量之间的cosine夹角来度量。

这种表示方法也被成为word embedding。关于”distributed”的一种解释是,不像one-hot那样只在一个维度上有非零值。目前主要有两种训练模型:CBOW (Continuous Bag of Words) 和 Skip-gram model,两种模型比较相近。

训练模型与细节

CBOW 模型

模型的输入为某个词$w_t$前后的某些词,输出为要预测的$w_t$的输出y。假如窗口大小为2,则输入为$w _{t-1}, w _{t+1}$ ,这些输入是one-hot向量。每个词先通过矩阵W,映射到各自对应的向量;然后把这些各自得到的向量相加;最后通过一个softmax层,获得相应的概率。其实,这个矩阵W,就是我们最终要得到的各个词的词向量的表示。

Skip-gram 模型

和上面的模型非常像,输入为某一个词$w_t$ ,输出为这个词前后词的概率。

如果一份文档由T个词组成,$w_1, w_2, …, w_T$ ,那么skip-gram的目标函数就是最大化平均对数概率:
$$
\frac{1}{T} \sum_{t=1}^{T}\sum_{-c\leq j \leq c} \log p(w_{t+j } | w_t)
$$
其中,c表示中心词$w_t$前后各c个词。如果直接以softmax方式计算$p(w_O | w_I)$
$$
p(w_O|w_I) = \frac{\exp({v’{w_O}}^\top v_{w_I})}{\sum_{w=1}^{W} \exp({v’{w}}^\top v_{w_I})}
$$
其中,W是总词汇量,$v_{w}^{‘}$和$v_w$ 分别是词w的输出和输入的向量表示,即分别是上图中两个矩阵$W_{V \times N}, W_{N \times V}^{‘}$ 中的一行和一列。

然而,这样计算是不可行的,因为W实在太大了。

评价指标

论文中是采用analogical reasoning task来进行评价学到的词向量的好坏的。所谓的analogical reasoning task就是,比如给定“Germany” : “Berlin” :: “France” : ?。找到这样的词x,vec(x)和vec(“Berlin”) - vec(“Germany”) + vec(“France”)在cosine距离上最相近。这种推理任务分两类,(1)syntactic analogy和(2)semantic analogy。

然而,感觉这种评价指标有点片面,而且对于具体的问题可能这项指标的作用不大。可以根据具体的任务相关来评价它的好坏。

能想到的其他的评价方式,比如聚类看分布,通过cosine距离找近义词等。

优化

Hierarchical Softmax

上面已经指出,计算softmax的复杂度太高,而通过Hierarchical Softmax,可以将每次的复杂度O(W)降到O(log(W))。

具体是,最后的输出层是一个二叉树,Huffman树。Huffman树是根据Huffman编码来进行得到的,这儿就不具体讲Huffman编码的算法了。最后的到的Huffman树,每个词代表的节点是叶子节点,而且词频越高的节点,深度越低。这样正向传播的时候,每次选择左子树,或者右子树,直到到达目标叶子节点,得到目标概率值。

Negative Sampling

这也是对softmax层的优化,negative sampling的效率和效果都比Hierarchical Softmax要好些。Hierarchical Softmax是准确的计算概率,而这种方法是一种近似计算。对出目标词外的其他词进行负采样,可以降低其他单词的权重。

重新定义了上面的$p(w_O | w_I)$ :
$$
p(w_O | w_I) = \log \sigma({v’{w_O}}^\top v_{w_I} )+ \sum_{i=1}^{k}\mathbb{E}{w_i \sim P_n(w)}[\log \sigma(-{v’_{w_i}}^\top v_{w_I})]
$$
其中,$ P_n(w)$ 是词w周围的噪声分布,具体的选取是根据词频越高,被选取的几率越大。k为除词w之外,其他要取的数量。论文中建议对于大的训练集,取2-5,小的训练集,取5-20。

Subsampling

这是为了解决一些高频词,如”in”,”the”等。它们提供的信息非常少,即使在上百万的样本上进行训练,这些词在对应的词向量在训练时也不会发生显著变化;而且,这些高频词的出现对于正常词的训练帮助也很小。为了解决高频词和低频次之间的不平衡,每个词有一定概率被丢弃:
$$
P(w_i) = 1 - \sqrt{\frac{t}{f(w_i)}}
$$
其中,f(wi)是词wi的出现频率,t是一个选定的阈值,通常为1e-5。

在实际中,表现的很好,加速了学习过程(对比试验中减少了几乎一半的时间),甚至低频词的向量的更精确。

源码阅读

阅读了word2vec源码, 进一步了解了其中的细节。

  • 因为对sigmoid的精度要求不是很高,而且要重复用到。所以sigmoid 进行打表。
1
2
3
4
for (i = 0; i < EXP_TABLE_SIZE; i++) {
expTable[i] = exp((i / (real)EXP_TABLE_SIZE * 2 - 1) * MAX_EXP); // Precompute the exp() table
expTable[i] = expTable[i] / (expTable[i] + 1); // Precompute f(x) = x / (x + 1)
}
  • 网络初始化,InitNet() 开辟syn0 input vectors, 对于hs or negative 开辟syn1,并创建huffman tree,并写入到vocab[].point and code。output vector syn1 初始化为0,input vector syn0初始化为[-0.5/layer_size, 0.5/layer_size]
  • 对于Negative Sampling, InitUnigramTable() 是初始化table数组。按照采样概率,在这个1e8大小的table上,每个单词的出现的次数=被采样的概率*1e8。使用的是unigram distribution, 3/4是经验值。

$$
P(w_i)=\frac{f(w_i)^{3/4}}{\sum_{j=0}^{n}f(w_j)^{3/4}}
$$

  • 对于下采样,代码和论文是不一样的,代码是这么写的。
1
2
3
4
5
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;
}

根据代码,一个词wi被保留的概率就是:
$$
P(w_i) = \sqrt {\frac{t}{f(w_i)}} + \frac{t}{f(w_i)}
$$

  • alpha 学习速率的变化。初始0.025 starting_alpha = alpha = 0.025 alpha更新 :
1
2
alpha = starting_alpha * (1 - word_count_actual / (real)(iter * train_words + 1));
if (alpha < starting_alpha * 0.0001) alpha = starting_alpha * 0.0001
  • 并不是在windows里面的所有词来做训练, 而是random b = [0,windows)。然后for (a = b; a < window * 2 + 1 - b; a++)
  • word2vec.c 还提供了了通过k-mean聚成预先设定好的classes个类,对每个词聚类。

参数选择及对比

训练时主要的参数有选择训练模型cbow或者sg,词向量大小size,上下文窗口window,下采样的阈值sample,选择hierarchical softmax 或者负采样计算softmax,负采样的数量negative,迭代数据次数iter,词表限制的词频阈值min-count等。

首先对于两个训练模型训练速度上进行了对比。

1
2
3
4
time ./word2vec -train text8 -output vectors.bin -cbow 1 -size 200 -window 8 -negative 25 -hs 0 -sample 1e-4 -threads 20 -binary 1 -iter 15
time ./word2vec -train text8 -output vectors.bin -cbow 1 -size 200 -window 8 -negative 0 -hs 1 -sample 1e-4 -threads 20 -binary 1 -iter 15
time ./word2vec -train text8 -output vectors.bin -cbow 0 -size 200 -window 8 -negative 25 -hs 0 -sample 1e-4 -threads 20 -binary 1 -iter 15
time ./word2vec -train text8 -output vectors.bin -cbow 0 -size 200 -window 8 -negative 0 -hs 1 -sample 1e-4 -threads 20 -binary 1 -iter 15

训练时间:

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
Starting training using file text8
Vocab size: 71291
Words in train file: 16718843
Alpha: 0.000005 Progress: 100.10% Words/thread/sec: 89.08k
real 3m4.607s
user 47m0.884s
sys 0m2.292s
Starting training using file text8
Vocab size: 71291
Words in train file: 16718843
Alpha: 0.000005 Progress: 100.11% Words/thread/sec: 172.98k
real 1m37.869s
user 24m14.380s
sys 0m1.440s
Starting training using file text8
Vocab size: 71291
Words in train file: 16718843
Alpha: 0.000002 Progress: 100.11% Words/thread/sec: 11.15k
real 23m43.240s
user 375m19.832s
sys 0m3.400s
Starting training using file text8
Vocab size: 71291
Words in train file: 16718843
Alpha: 0.000002 Progress: 100.11% Words/thread/sec: 22.72k
real 11m51.509s
user 184m14.460s
sys 0m1.452s

可以看出使用sg模型远比使用cbow要慢。negative=25的时候,比使用hs要慢,对比论文中的实验,当neg=5的时候和hs效率相当。

sg模型获得的低频词的词向量质量比cbow好一些。hs对于低频词表现更好,负采样对于高频词表现更好。关于其他参数的比较与较好的经验设置,可以参考《How to Generate a Good Word Embedding?》

首先根据具体任务,选一个领域相似的语料,在这个条件下,语料越大越好。然后下载一个 word2vec 的新版(14年9月更新),语料小(小于一亿词,约 500MB 的文本文件)的时候用 Skip-gram 模型,语料大的时候用 CBOW 模型。最后记得设置迭代次数为三五十次,维度至少选 50,就可以了。

其中也说,语料对词向量的影响比模型的影响要重要。做具体 NLP 任务(用作特征、用作神经网络初始化)的时候,50 维之后效果提升就比较少了。

基于维基中文语料库的训练

采用的数据是中文维基百科的数据,进行了数据抽取,繁体转简体,分词。

基于tensorflow来进行训练,在计算loss时,用了tf.nn.nce_loss来进行计算,它的输入是隐藏层到输出层的权重矩阵和偏差,隐藏层矩阵,真实值label,词典大小和negative sampling中采样大小k值。

实际的训练效果,示例如下:

利用t-SNE对出现频度最高的500个词的词向量进行降维,下面是降维到2维的结果。

从图中可以看出,一些国家名称聚在了一起;”国王”,”皇帝”,”主席”,”总统”聚在了一起;”作品”,”故事”,”小说”等比较相近。

参考

[1].Mikolov, T., Sutskever, I., Chen, K., Corrado, G. & Dean, J. Distributed representations of words and phrases and their compositionality. (NIPS 2013)

[2].word2vec 中的数学原理详解

[3]. TensorFlow: Vector Representations of Words

[4].Wikimedia Downloads

[5].《How to Generate a Good Word Embedding?》

[6]. word2vec Parameter Learning Explained