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 | for (i = 0; i < EXP_TABLE_SIZE; i++) { |
- 网络初始化,
InitNet()
开辟syn0 input vectors, 对于hs or negative 开辟syn1,并创建huffman tree,并写入到vocab[].point and code
。output vectorsyn1
初始化为0,input vectorsyn0
初始化为[-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 | if (sample > 0) { |
根据代码,一个词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 | alpha = starting_alpha * (1 - word_count_actual / (real)(iter * train_words + 1)); |
- 并不是在
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 | 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 |
训练时间:
1 | Starting training using file text8 |
可以看出使用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)
[3]. TensorFlow: Vector Representations of Words