不可思议的 Word2Vec(中):提取关键词与词语相似度

您所在的位置:网站首页 庆贺的相近的词语是什么 不可思议的 Word2Vec(中):提取关键词与词语相似度

不可思议的 Word2Vec(中):提取关键词与词语相似度

2024-07-03 10:09| 来源: 网络整理| 查看: 265

汇总整理自《不可思议的Word2Vec》系列,作者苏剑林。部分内容有删改。

提取关键词

说到提取关键词,一般会想到 TF-IDF 和 TextRank,大家是否想过,Word2Vec 还可以用来提取关键词?而且,用 Word2Vec 提取关键词,已经初步含有了语义上的理解,而不仅仅是简单的统计了,而且还是无监督的!

什么是关键词?

诚然,TF-IDF 和 TextRank是两种提取关键词的很经典的算法,它们都有一定的合理性,但问题是,这两种算法虽然看上去简单,但并不容易想到。试想一下,没有学过信息相关理论的同学,估计怎么也难以理解为什么 IDF 要取一个对数?为什么不是其他函数?又有多少读者会破天荒地想到,用 PageRank 的思路,去判断一个词的重要性?

说到底,问题就在于:提取关键词和文本摘要,看上去都是一个很自然的任务,有谁真正思考过,关键词在数学上的合理定义应该是什么?或者说,我们获取关键词的目的是什么?

很显然,关键词也好,摘要也好,我们希望能够尽可能快地获取文章的大意,如果一篇文章的关键词是“深度学习”,我们就会知道,这篇文章不可能大谈特谈“钢铁是怎么练成的”,也就是说,我们可以由关键词来猜到文本的大意,用数学来表示,那就是条件概率:

\[p(s \mid w_i)\]

这里的 $s$ 代表着一段文本,$w_i$ 是文本中的某个词,如果 $w_i$ 是文本的关键词,那么应该使得上述概率最大。也就是说,我们只需要对句子中所有的词,算一遍上述概率,然后降序排列,就可以提取关键词了。说白了,关键词就是最能让我们猜到原文的词语。怎么估算这个概率?简单使用朴素贝叶斯假设就好,如果 $s$ 由 $n$ 个词 $w_1,w_2,…,w_n$ 组成,那么:

\[p(s \mid w_i) = p(w_1,w_2,...,w_n \mid w_i) = \prod_{k=1}^n p(w_k \mid w_i)\]

这样,我们只需要估算词与词之间的转移概率 $p(w_k \mid w_i)$,就可以得到条件概率 $p(s \mid w_i)$ 了,从而完成关键词的提取。

这跟 Word2Vec 又有什么联系呢?为了估算 $p(w_k \mid w_i)$,需要有大量的文本进行统计,好在这个过程是无监督的,统计是件很简单的事情。然而,我们有更好的工具,什么工具最擅长于对 $p(w_k \mid w_i)$ 的建模呢?显然就是 Word2Vec 呀!Word2Vec 的 Skip-Gram 模型,不就是用来对这个概率进行建模的么?

当然,并不一定要用贝叶斯假设,更加不一定要用 Word2Vec 来算,但是,关键词的定义本身应该是合理的

Word2Vec 算概率

这时候读者应该会明白,为什么在前篇文章中会那么强调 Skip-Gram + Huffman Softmax 这个组合了,因为这个组合就是对 $p(w_k \mid w_i)$ 进行建模的。当然,由于 Huffman Softmax 的特性,我们要算 $p(w_k \mid w_i)$,需要费一些周折,参考代码如下:

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

这基本上就是直接参考 gensim 中的 Word2Vec 的 score_sg_pair 函数写的,简单的流程是:取出 $w_k$ 的 Huffman 编码(路径),取出 $w_i$ 的词向量,然后根据路径,要把路径上每个节点的概率都算出来,然后乘起来,得到 $p(w_k \mid w_i)$。这是算得是概率对数,应该乘法变为加法。最后的概率是怎么计算的呢?事实上,按照 Word2Vec 的公式,每个节点的概率对数是:

\[\begin{align} & \log\Big(\frac{1}{1+e^{-x^T\theta}}\Big)^{1-d}\Big(1-\frac{1}{1 + e^{-x^T\theta}}\Big)^d\\ =& -(1-d)\log(1 + e^{-x^T\theta})-d\log(1 + e^{-x^T\theta})-dx^T\theta\\ =&-\log(1+e^{-x^T\theta}) - dx^T\theta \end{align}\]

这里的 $\theta$ 是节点向量,$x$ 是输入词向量,$d$ 是该节点的编码(非 0 即 1)。但是,官方的 score_sg_pair 函数并不是这样写的,那是因为:

\[\begin{align} &-\log(1+e^{-x^T\theta})-dx^T\theta\\ =&-\log\Big[e^{dx^T\theta}(1+e^{-x^T\theta})\Big]\\ =&-\log\Big(e^{dx^T\theta}+e^{(d-1)x^T\theta}\Big)\\ =&-\log\Big(1+e^{-(-1)^dx^T\theta}\Big) \end{align}\] 实践

有了上面的铺垫,现在算关键词就简单了:

from collections import Counter def keywords(s): s = [w for w in s if w in model] ws = {w:sum([predict_proba(u, w) for u in s]) for w in s} return Counter(ws).most_common() import pandas as pd #引入它主要是为了更好的显示效果 import jieba s = u'太阳是一颗恒星' pd.Series(keywords(jieba.cut(s)))

输出结果是:

0 (恒星, -27.9013707845) 1 (太阳, -28.1072913493) 2 (一颗, -30.482187911) 3 (是, -36.3372344659)

其它例子:

>>> s=u'昌平区政府网站显示,明十三陵是世界上保存完整、埋葬皇帝最多的墓葬群,1961年被国务院公布为第一批全国重点文物保护单位,并于2003年被列为世界遗产名录。' >>> pd.Series(keywords(jieba.cut(s))) 0 (文物保护, -261.691625676) 1 (名录, -272.297758506) 2 (世界遗产, -273.943120665) 3 (第一批, -280.781786703) 4 (列为, -281.663865896) 5 (明十三陵, -286.298893108) 6 (墓葬群, -287.463013816) ... >>> s=u'雄安新区横空出世,吸引了众多外地炒房客前去购房。然而,在当地政府重拳遏制非法炒房、楼市冻结的背景下,那些怀揣买房钱却在雄安新区无处下手的投资需求,被挤出到周边地区。' >>> pd.Series(keywords(jieba.cut(s))) 0 (炒房客, -326.997266407) 1 (楼市, -336.176584187) 2 (炒房, -337.190896137) 3 (买房, -344.613473556) 4 (购房, -346.396359454) 5 (重拳, -350.207272082) 6 (外地, -355.860419218) >>> s=u'如果给一部古装电影设计服装,必须要考虑故事发生在哪个朝代,汉朝为宽袍大袖,清朝则是马褂旗袍。可在京剧舞台上,几乎任何一个历史人物,根据他的性别年龄、身份地位、基本性格等等,都可以在现有的服饰里找到合适的行头。 ' >>> pd.Series(keywords(jieba.cut(s))) 0 (朝代, -485.150966757) 1 (人物, -493.759615898) 2 (古装, -495.478962392) 3 (汉朝, -503.409908377) 4 (清朝, -503.45656029) 5 (旗袍, -504.76313228) 6 (身份, -507.624260109)

如果要在自己的语料上尝试,那就直接在语料上训练一个 Word2Vec(Skip-Gram + Huffman Softmax)模型即可,然后调用上述代码就可以了。

比较

按照我们一开始的想法,$p(w_k \mid w_i)$ 应该要在整个句子内统计计算,而 Word2Vec 仅仅开了个窗口来计算,这合理吗?事实上,Word2Vec 虽然仅仅开了窗口,但已经成功建立了相似词之间的联系,也就是说,用 Word2Vec 做上述过程,事实上将“相似词语”进行叠加起来进行评估,相比之下,TF-IDF 的方法,仅仅是将“相同词”叠加起来进行评估,因此,我们说 Word2Vec 提取关键词,能够初步结合语义来判断了。而且,Word2Vec 通过考虑 $p(w_k \mid w_i)$ 来考虑了文章内部的关联,这里有点 TextRank 的味道了,是一个二元模型,而 TF-IDF 仅仅考虑词本身的信息量,仅仅是一个一元模型。

而且,Word2Vec 是基于神经网络训练的,自带平滑功能,哪怕两个词语在文本中未曾共现,也能得到一个较为合理的概率。

当然这样做的代价就是:TF-IDF 算法的效率是 $\mathscr{O}(N)$,而用 Word2Vec 提取,效率显然是 $\mathscr{O}(N^2)$,这里的 $N$ 是句子中的词数。

不一样的“相似” 相似度的定义

当用 Word2Vec 得到词向量后,一般我们会用余弦相似度来比较两个词的相似程度,定义为:

\[\cos (\boldsymbol{x}, \boldsymbol{y}) = \frac{\boldsymbol{x}\cdot\boldsymbol{y}}{|\boldsymbol{x}|\times|\boldsymbol{y}|}\]

有了这个相似度概念,我们既可以比较任意两个词之间的相似度,也可以找出跟给定词最相近的词语。这在 gensim 的 Word2Vec 中,由 most_similar 函数实现。

等等!我们很快给出了相似度的计算公式,可是我们居然还没有“定义”相似!连相似都没有定义,怎么就得到了评估相似度的数学公式了呢?

要注意,这不是一个可以随意忽略的问题。很多时候我们都不知道我们干的是什么,就直接去干了。好比上一篇文章说到提取关键词,相信很多人都未曾想过,什么是关键词,难道就仅仅说关键词就是很“关键”的词?而如果想到,关键词就是用来估计文章大概讲什么的,这样我们就得到一种很自然的关键词定义:

\[keywords = \mathop{\arg\max}_{w \in s}p(s \mid 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 \mid 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 \mid 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) >>> 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)

大家可以自己尝试。要说明的是:很遗憾,Huffman Softmax 虽然在训练阶段加速计算,但在预测阶段,当需要遍历一遍词典时,事实上它比原生的 Softmax 还要慢,所以这并不是一个高效率的方案。

究竟做了啥

根据前面两部分,我们可以看到,“相似”一般有两种情景:1、经常跟同一批词语搭配出现;2、经常一起出现。这两种情景,我们都可以认为是词语之间的相似,适用于不同的需求。

比如,在做多义词的词义推断时,比如 star 是“恒星”还是“明星”,就可以利用互信息。我们可以事先找到 star 意思为“恒星”的时候的语料,找出与 star 互信息比较大的的词语,这些词语可能有 sun、planet、earth,类似地,可以找到 star 为“明星”的时候的语料,找出与 star 互信息比较大的词语,这些词语可能有 entertainment、movie 等。到了新的语境,我们就可以根据上下文,来推断究竟是哪个词义。

总而言之,需要明确自己的需求,然后再考虑对应的方法。

汇总整理自《不可思议的Word2Vec》系列,作者苏剑林。部分内容有删改。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3