【不可思议的Word2Vec】 4.不一样的“相似”
By 苏剑林 | 2017-05-01 | 141201位读者 |相似度的定义 #
当用Word2Vec得到词向量后,一般我们会用余弦相似度来比较两个词的相似程度,定义为
$$\cos (\boldsymbol{x}, \boldsymbol{y}) = \frac{\boldsymbol{x}\cdot\boldsymbol{y}}{|\boldsymbol{x}|\times|\boldsymbol{y}|}$$
有了这个相似度概念,我们既可以比较任意两个词之间的相似度,也可以找出跟给定词最相近的词语。这在gensim的Word2Vec中,由most_similar函数实现。
等等!我们很快给出了相似度的计算公式,可是我们居然还没有“定义”相似!连相似都没有定义,怎么就得到了评估相似度的数学公式了呢?
要注意,这不是一个可以随意忽略的问题。很多时候我们都不知道我们干的是什么,就直接去干了。好比上一篇文章说到提取关键词,相信很多人都未曾想过,什么是关键词,难道就仅仅说关键词就是很“关键”的词?而如果想到,关键词就是用来估计文章大概讲什么的,这样我们就得到一种很自然的关键词定义
$$keywords = \mathop{\text{argmax}}_{w\in s}p(s|w)$$
进而可以用各种方法对它建模。
回到本文的主题来,相似度怎么定义呢?答案是:看场景定义所需要的相似。
那么,通过余弦相似度给出的相似,又是什么情况呢?事实上,Word2Vec本质上来说,还是使用上下文的平均分布描述当前词(因为Word2Vec是不考虑词序的),而余弦值与向量模长没关系,因此它描述的是“相对一致”。那么,余弦相似度大,事实上意味着这两个词经常跟同一批词搭配,或者更粗糙讲,那就是在同一句话中,两个词具有可替换性。比如,“广州”最相近的词语是“东莞”、“深圳”,那是因为很多场景下,直接将矩阵中的“广州”直接换成“东莞”、“深圳”,这个句子还是合理的(是句子本身的合理,但这个句子不一定是事实,比如“广州是广东的省会”,变成“东莞是广东的省会”,这个句子是合理的,但是这个句子并非是事实)。
>>> s = u'广州'
>>> pd.Series(model.most_similar(s))
0 (东莞, 0.840889930725)
1 (深圳, 0.799216389656)
2 (佛山, 0.786817014217)
3 (惠州, 0.779960155487)
4 (珠海, 0.735232532024)
5 (厦门, 0.725090026855)
6 (武汉, 0.724122405052)
7 (汕头, 0.719602525234)
8 (增城, 0.713532149792)
9 (上海, 0.710560560226)
相关:另一种相似 #
前面已经说了,相似度的定义事实上要看场景的,余弦相似度只是其中之一。有时候我们会觉得“东莞”和“广州”压根就没联系,对于“老广州”来说,“白云山”、“白云机场”、“广州塔”这些词才是跟“广州”最相似的,这种场景也是很常见的,比如做旅游的推荐,旅游来到广州后,自然是希望输入“广州”后,自动输出来“白云山”、“白云机场”、“广州塔”这些广州相关的词语,而不是输出“东莞”、“深圳”这些词语。
这种“相似”,准确来说是“相关”,应该怎么描述呢?答案是互信息,定义为
$$\log \frac{p(x,y)}{p(x)p(y)}=\log p(y|x) - \log p(y)$$
互信息越大,说明$x,y$两个词经常一起出现。
这样,在给定词$x$的情况下,我们就可以找出经常跟词$x$一起出现的词,这个也完全可以由Word2Vec中的Skip-Gram+Huffman Softmax模型来完成。代码如下
import numpy as np
import gensim
model = gensim.models.word2vec.Word2Vec.load('word2vec_wx')
def predict_proba(oword, iword):
iword_vec = model[iword]
oword = model.wv.vocab[oword]
oword_l = model.syn1[oword.point].T
dot = np.dot(iword_vec, oword_l)
lprob = -sum(np.logaddexp(0, -dot) + oword.code*dot)
return lprob
from collections import Counter
def relative_words(word):
r = {i:predict_proba(i, word)-np.log(j.count) for i,j in model.wv.vocab.iteritems()}
return Counter(r).most_common()
这时候,“广州”的相关词为
>>> s = u'广州'
>>> w = relative_words(s)
>>> pd.Series(w)
0 (福中路, -17.390365773)
1 (OHG, -17.4582544641)
2 (林寨镇, -17.6119545612)
3 (坪山街道, -17.6462214199)
4 (东圃镇, -17.6648893759)
5 (西翼, -17.6796614955)
6 (北京西, -17.6898282385)
7 (⇋, -17.6950761384)
8 (K1019, -17.7259853233)
9 (景泰街道, -17.7292421556)
10 (PSW3, -17.7296432222)
11 (广州铁路职业技术学院, -17.732288911)
12 (13A06, -17.7382891287)
13 (5872, -17.7404719442)
14 (13816217517, -17.7650583156)
15 (未遂案, -17.7713452536)
16 (增城市, -17.7713832873)
17 (第十甫路, -17.7727940473)
18 (广州白云机场, -17.7897457043)
19 (Faust, -17.7956389314)
20 (国家档案馆, -17.7971039916)
21 (w0766fc, -17.8051687721)
22 (K1020, -17.8106548248)
23 (陈宝琛, -17.8427718407)
24 (jinriGD, -17.8647825023)
25 (3602114109100031646, -17.8729896156)
可以发现,得到的结果基本上都是跟广州紧密相关的。当然,有时候我们稍微强调一下高频词,因此,可以考虑将互信息公式修改为
$$\log \frac{p(x,y)}{p(x)p^{\alpha}(y)}=\log p(y|x) - \alpha\log p(y)$$
其中$\alpha$是一个略小于1的常数。如果取$\alpha=0.9$,那么有
from collections import Counter
def relative_words(word):
r = {i:predict_proba(i, word)-0.9*np.log(j.count) for i,j in model.wv.vocab.iteritems()}
return Counter(r).most_common()
结果重新排列如下:
>>> s = u'广州'
>>> w = relative_words(s)
>>> pd.Series(w)
0 (福中路, -16.8342976099)
1 (北京西, -16.9316053191)
2 (OHG, -16.9532688634)
3 (西翼, -17.0521852934)
4 (增城市, -17.0523156839)
5 (广州白云机场, -17.0557270208)
6 (林寨镇, -17.0867272184)
7 (⇋, -17.1061883426)
8 (坪山街道, -17.1485480457)
9 (5872, -17.1627067119)
10 (东圃镇, -17.192150594)
11 (PSW3, -17.2013228493)
12 (Faust, -17.2178736991)
13 (红粉, -17.2191157626)
14 (国家档案馆, -17.2218467278)
15 (未遂案, -17.2220391092)
16 (景泰街道, -17.2336594498)
17 (光孝寺, -17.2781121397)
18 (国际货运代理, -17.2810157155)
19 (第十甫路, -17.2837591345)
20 (广州铁路职业技术学院, -17.2953441257)
21 (芳村, -17.301106775)
22 (检测院, -17.3041253252)
23 (K1019, -17.3085465963)
24 (陈宝琛, -17.3134413583)
25 (林和西, -17.3150577006)
相对来说,后面这个结果更加可读一点。另外的一些结果,展示如下:
>>> s = u'飞机'
>>> w = relative_words(s)
>>> pd.Series(w)
0 (澳门国际机场, -16.5502216186)
1 (HawkT1, -16.6055740672)
2 (架飞机, -16.6105400944)
3 (地勤人员, -16.6764712234)
4 (美陆军, -16.6781627384)
5 (SU200, -16.6842796275)
6 (起降, -16.6910345896)
7 (上海浦东国际机场, -16.7040362134)
8 (备降, -16.7232609719)
9 (第一架, -16.7304077856)>>> pd.Series(model.most_similar(s))
0 (起飞, 0.771412968636)
1 (客机, 0.758365988731)
2 (直升机, 0.755871891975)
3 (一架, 0.749522089958)
4 (起降, 0.726713418961)
5 (降落, 0.723304390907)
6 (架飞机, 0.722024559975)
7 (飞行, 0.700125515461)
8 (波音, 0.697083711624)
9 (喷气式飞机, 0.696866035461)>>> s = u'自行车'
>>> w = relative_words(s)
>>> pd.Series(w)
0 (骑, -16.4410312554)
1 (放风筝, -16.6607225423)
2 (助力车, -16.8390451582)
3 (自行车, -16.900188791)
4 (三轮车, -17.1053629907)
5 (租赁点, -17.1599389605)
6 (电动车, -17.2038996636)
7 (助动车, -17.2523149342)
8 (多辆, -17.2629832083)
9 (CRV, -17.2856425014)>>> pd.Series(model.most_similar(s))
0 (摩托车, 0.737690329552)
1 (骑, 0.721182465553)
2 (滑板车, 0.7102201581)
3 (电动车, 0.700758457184)
4 (山地车, 0.687280654907)
5 (骑行, 0.666575074196)
6 (单车, 0.651858925819)
7 (骑单车, 0.650207400322)
8 (助力车, 0.635745406151)
9 (三轮车, 0.630989730358)
大家可以自己尝试。要说明的是:很遗憾,Huffman Softmax虽然在训练阶段加速计算,但在预测阶段,当需要遍历一遍词典时,事实上它比原生的Softmax还要慢,所以这并不是一个高效率的方案。
究竟做了啥 #
根据前面两部分,我们可以看到,“相似”一般有两种情景:1、经常跟同一批词语搭配出现;2、经常一起出现。这两种情景,我们都可以认为是词语之间的相似,适用于不同的需求。
比如,在做多义词的词义推断时,比如star是“恒星”还是“明星”,就可以利用互信息。我们可以事先找到star意思为“恒星”的时候的语料,找出与star互信息比较大的的词语,这些词语可能有sun、planet、earth,类似地,可以找到star为“明星”的时候的语料,找出与star互信息比较大的词语,这些词语可能有entertainment、movie等。到了新的语境,我们就可以根据上下文,来推断究竟是哪个词义。
总而言之,需要明确自己的需求,然后再考虑对应的方法。
转载到请包括本文地址:https://www.spaces.ac.cn/archives/4368
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (May. 01, 2017). 《【不可思议的Word2Vec】 4.不一样的“相似” 》[Blog post]. Retrieved from https://www.spaces.ac.cn/archives/4368
@online{kexuefm-4368,
title={【不可思议的Word2Vec】 4.不一样的“相似”},
author={苏剑林},
year={2017},
month={May},
url={\url{https://www.spaces.ac.cn/archives/4368}},
}
May 15th, 2017
博主您好,有两个问题想请教下。
1、 dot = np.dot(iword_vec, oword_l).这里在计算两个词语的点积的时候为什么是用的辅助参数向量而非词向量。因为在计算与词a相似性最高的词的时候用的是直接求两个向量之间的余弦相似度,那么这里的dot我觉得是向量的内积而非a的向量与b的参数向量的内积。
2、假如我知道a的相关性最强的词语是b,但是我训练出来的b词向量只是a词向量第4相似的,我将这个信息作为先验加到词向量中使得a,b变成最相关的,这样的向量一定会比原来好吗?如果结合任务而言在什么任务下可能会好呢?
谢谢回答!
1、我要算概率,不是算相似度,算概率要根据huffman树来算,用到huffman树,自然要用到你说的辅助参数。另外,值得指出的是,虽然用cos算相似度效果还不错,但是从来就没有理论支持这种算法(不妨回忆一下,告诉你用cos的文章,说过为什么用cos吗?)。
2、还是那句话,从来没有理论支撑cos,因此,没必要一定要使得a、b两词的cos最近吧?不过你考虑的这种做法,我感觉应该是glove型词向量的做法。
huffman只是一种近似方法,negative sampling是另外一种,暂时不考虑这种加速训练也不会影响思考问题吧。在做softmax的时候skip_gram用的是当前词去预测上下文中的每一个词,希望目标词可以做到尽可能预测上下文中词,概率计算用的是目标词的词向量和上下文中词的参数向量,这个参数向量的说法不知道是不是准确,我指的是隐层到输出层之间的权重。
我看你在相关词部分计算p(x|y)的时候直接使用w2v去计算,这样好像也并没说一定要词向量与参数向量点积去算概率吧,如果将参数向量替换成词向量之后更好的话,那我完全可以那样算概率吧,虽然这种替换并不一定会更好。欢迎指正
huffman树的概率就是那样计算的,不是感觉出来的...
huffman树是一种近似,但是它本身已经完全归一化的,是一个合理的概率分布。或者干脆说,huffman softmax就是一个概率假设。
skip-gram就是用当前词预测周围词的概率,预测方法就是huffman softmax,那这样的话我肯定是用huffman softmax的定义来计算的,难道还自己另外构造一种么?不用辅助参数而用词向量更准?理由呢?在huffman softmax模型中,从来没有过词向量与词向量做内积这种运算呀。
回答的很好,苏神,我想请问score_sg_pair函数和你的这个有什么区别,你的上一篇文章提到是差不多的,但是score_sg_pair函数在word2vec中有什么用,好像求相似度时,word2vec只用了余弦相似度是吗?
score_sg_pair就是score_sg_pair的作用呀,估算词对的条件概率或共现概率咯。
那个概率是p(y|x),打错了
August 16th, 2017
请问苏剑林老师: 我在运行时出现 r = {i:predict_proba(i, word)-0.9*np.log(j.count) for i,j in model.wv.vocab.iteritems()}
AttributeError: 'dict' object has no attribute 'iteritems' , 你分析一下这是怎么回事?
你用python3?是的话,把所有.iterxxx换成.xxx,也就是去掉iter
可以跑通了,多谢苏老师,您回复真快。
November 22nd, 2017
苏老师您太厉害了。不但要知其然,还要知其所以然!学到了很多。谢谢您!
June 1st, 2018
您好,在做多义词推断有比较好的解决方法嘛。
June 13th, 2018
就我所知:
(1)hierarchical softmax仍需將hidden layer之vector與tree中各節點所代表之vector作inner product
(2)若有n個word 則在計算各word之機率時 以softmax法需計算n個內積 n個exp 若以hierarchical softmax 則需n-1次內積 n-1個exp 兩者效率應是不相上下
(3)word2vec學習結果 是將每個word變成1個vector(亦即:1個點) 但 究竟有意義的 是該點的位置 抑或 該點的方向 應該仔細判別 因為這直接決定了cosine similarity的有效性
September 8th, 2018
苏老师你好,我跑了你上面的代码,但是oword没有point和code属性,我分别改成了index和sample_int属性,最后跑成功了,不知道是不是因为版本的原因,换成这两个属性对吗?还有,我想请问苏老师,这个sample_int是oword的什么属性,我在网上找了好久没找到,打扰苏老师了
感觉不像。你用的是自己训练的word2vec还是我开源的?自己训练的话,有没有设参数hs=1?
September 9th, 2018
哦哦,我用的是https://www.cnblogs.com/Darwin2000/p/5786984.html这里的模型,还有就是我用的syn0替代了你的syn1,不知道这两个是否一样,都是楼上所说的辅助向量
不要生搬硬套,好好看完整个系列的原理。
好的好的
September 9th, 2018
我还得请问一下苏老师你开源得模型在哪里找
找到了,谢谢苏老师
September 11th, 2018
苏神,我想请问为什么在实在中的log(p(y))在程序中时log(j.count),j.count不是单词的频数吗
苏神,我想请问为什么在式子中的log(p(y))在程序中用log(j.count)来表示,j.count不是单词的频数吗
log(p(y))与log(j.count)的差别就是一个常数“log(总词频)”,这个常数对于所有的词都一样的,所以它不改变排序结果。
这样的话,log(P(y|x))-log(总词频)是不是有点说不过去,因为他们两个不是同一个量级的,用减法有点牵强了,还请苏神指正
1、这样的话,log(P(y|x))-log(总词频)是不是有点说不过去,因为他们两个不是同一个量级的,用减法有点牵强了;
2、这里的互信息公式前面为什么不包含联合概率p(x,y),互信息公式不是这样的
还请苏神指正
1、不改变排序
2、哪里不包含$p(x,y)$?$\log \frac{p(x,y)}{p(x)p(y)}=\log p(y|x) - \log p(y)$你没看到?
哦哦,我查了一下确实有这么一个公式,叫“点互信息”,互信息公式的话,的确还差一个p(x,y)...
September 13th, 2018
请问这里的互信息公式为什么在前面不包含p(x,y)