【不可思议的Word2Vec】 3.提取关键词
By 苏剑林 | 2017-04-07 | 200186位读者 |本文主要是给出了关键词的一种新的定义,并且基于Word2Vec给出了一个实现方案。这种关键词的定义是自然的、合理的,Word2Vec只是一个简化版的实现方案,可以基于同样的定义,换用其他的模型来实现。
说到提取关键词,一般会想到TF-IDF和TextRank,大家是否想过,Word2Vec还可以用来提取关键词?而且,用Word2Vec提取关键词,已经初步含有了语义上的理解,而不仅仅是简单的统计了,而且还是无监督的!
什么是关键词? #
诚然,TF-IDF和TextRank是两种提取关键词的很经典的算法,它们都有一定的合理性,但问题是,如果从来没看过这两个算法的读者,会感觉简直是异想天开的结果,估计很难能够从零把它们构造出来。也就是说,这两种算法虽然看上去简单,但并不容易想到。试想一下,没有学过信息相关理论的同学,估计怎么也难以理解为什么IDF要取一个对数?为什么不是其他函数?又有多少读者会破天荒地想到,用PageRank的思路,去判断一个词的重要性?
说到底,问题就在于:提取关键词和文本摘要,看上去都是一个很自然的任务,有谁真正思考过,关键词的定义是什么?这里不是要你去查汉语词典,获得一大堆文字的定义,而是问你数学上的定义。关键词在数学上的合理定义应该是什么?或者说,我们获取关键词的目的是什么?
很显然,关键词也好,摘要也好,我们希望能够尽可能快地获取文章的大意,如果一篇文章的关键词是“深度学习”,我们就会知道,这篇文章不可能大谈特谈“钢铁是怎么练成的”,也就是说,我们可以由关键词来猜到文本的大意,用数学来表示,那就是条件概率
$$p(s|w_i)$$
这里的$s$代表着一段文本,$w_i$是文本中的某个词,如果$w_i$是文本的关键词,那么应该使得上述概率最大。也就是说,我们只需要对句子中所有的词,算一遍上述概率,然后降序排列,就可以提取关键词了。说白了,关键词就是最能让我们猜到原文的词语。怎么估算这个概率?简单使用朴素贝叶斯假设就好,如果$s$由$n$个词$w_1,w_2,\dots,w_n$组成,那么
$$p(s|w_i)=p(w_1,w_2,\dots,w_n|w_i)=\prod_{k=1}^n p(w_k|w_i)$$
这样,我们只需要估算词与词之间的转移概率$p(w_k|w_i)$,就可以得到条件概率$p(s|w_i)$了,从而完成关键词的提取。
这跟Word2Vec又有什么联系呢?为了估算$p(w_k|w_i)$,需要有大量的文本进行统计,好在这个过程是无监督的,统计是件很简单的事情。然而,我们有更好的工具,什么工具最擅长于对$p(w_k|w_i)$的建模呢?读者可能就已经猜到了,显然就是Word2Vec呀!Word2Vec的Skip-Gram模型,不就是用来对这个概率进行建模的么?鉴于Word2Vec的“快准狠”的特性,我们没理由弃之不用呀!(当然,按照文章开头的说法,并不一定要用贝叶斯假设,更加不一定要用Word2Vec来算,但是,关键词的定义本身应该是合理的)
Word2Vec算概率 #
这时候读者应该会明白,为什么我在前两篇文章中会那么强调Skip-Gram + Huffman Softmax这个组合了,因为这个组合就是对$p(w_k|w_i)$进行建模的。当然,由于Huffman Softmax的特性,我们要算$p(w_k|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|w_i)$。这是算得是概率对数,应该乘法变为加法。最后的概率是怎么计算的呢?事实上,按照Word2Vec的公式,每个节点的概率对数是:
$$\begin{aligned}&\log \left(\frac{1}{1+e^{-\boldsymbol{x}^{\top} \boldsymbol{\theta}}}\right)^{1-d}\left(1-\frac{1}{1+e^{-\boldsymbol{x}^{\top} \boldsymbol{\theta}}}\right)^{d}\\
=&-(1-d)\log (1+e^{-\boldsymbol{x}^{\top} \boldsymbol{\theta}}) - d \log (1+e^{-\boldsymbol{x}^{\top} \boldsymbol{\theta}}) - d \boldsymbol{x}^{\top} \boldsymbol{\theta}\\
=&-\log (1+e^{-\boldsymbol{x}^{\top} \boldsymbol{\theta}}) - d \boldsymbol{x}^{\top} \boldsymbol{\theta}\end{aligned}$$
这里的$\boldsymbol{\theta}$是节点向量,$\boldsymbol{x}$是输入词向量,$d$是该节点的编码(非0即1)。但是,官方的score_sg_pair函数并不是这样写的,那是因为
$$\begin{aligned}&-\log (1+e^{-\boldsymbol{x}^{\top} \boldsymbol{\theta}}) - d \boldsymbol{x}^{\top} \boldsymbol{\theta}\\
=&-\log \bigg[e^{d \boldsymbol{x}^{\top}\theta}(1+e^{-\boldsymbol{x}^{\top} \boldsymbol{\theta}})\bigg]\\
=&-\log \bigg(e^{d \boldsymbol{x}^{\top}\theta}+e^{(d-1)\boldsymbol{x}^{\top} \boldsymbol{\theta}}\bigg)\\
=&-\log \bigg(1+e^{-(-1)^d \boldsymbol{x}^{\top}\theta}\bigg)\end{aligned}$$
实践为上 #
有了上面的铺垫,现在算关键词就简单了:
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|w_i)$应该要在整个句子内统计计算,而Word2Vec仅仅开了个窗口来计算,这合理吗?事实上,Word2Vec虽然仅仅开了窗口,但已经成功建立了相似词之间的联系,也就是说,用Word2Vec做上述过程,事实上将“相似词语”进行叠加起来进行评估,相比之下,TF-IDF的方法,仅仅是将“相同词”叠加起来进行评估,因此,我们说Word2Vec提取关键词,能够初步结合语义来判断了。而且,Word2Vec通过考虑$p(w_k|w_i)$来考虑了文章内部的关联,这里有点TextRank的味道了,是一个二元模型,而TF-IDF仅仅考虑词本身的信息量,仅仅是一个一元模型。
而且,Word2Vec是基于神经网络训练的,自带平滑功能,哪怕两个词语在文本中未曾共现,也能得到一个较为合理的概率。
当然这样做的代价就是:TF-IDF算法的效率是$\mathcal{O}(N)$,而用Word2Vec提取,效率显然是$\mathcal{O}(N^2)$,这里的$N$是句子中的词数。
转载到请包括本文地址:https://kexue.fm/archives/4316
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Apr. 07, 2017). 《【不可思议的Word2Vec】 3.提取关键词 》[Blog post]. Retrieved from https://kexue.fm/archives/4316
@online{kexuefm-4316,
title={【不可思议的Word2Vec】 3.提取关键词},
author={苏剑林},
year={2017},
month={Apr},
url={\url{https://kexue.fm/archives/4316}},
}
April 10th, 2017
您好,这个我模型我用windows下的gensim导入,不报错,但是也不出结果,请问可能是什么原因
你好,请问能详细描述下你的问题吗?不出结果是什么意思?
我也是用gensim训练的词向量,提示没有syn1...
不同的训练方案吧?我这里用的是sg=1, hs=1这两个选项,请看第二篇。
April 14th, 2017
请问老师,我机器上跑您的程序时出现错误说:
oword_l = model.syn1[oword.point].T
AttributeError: 'Word2Vec' object has no attribute 'syn1'
1、检查你的gensim是不是最新版;
2、检查你的训练选项是不是有sg=1, hs=1,参考代码:
word2vec = gensim.models.word2vec.Word2Vec(sentences(), size=256, window=10, min_count=64, sg=1, hs=1, iter=10, workers=25)
参考链接:http://kexue.fm/archives/4304/
建议把三篇文章都看完,不要盲目直奔目标。
感谢苏老师!跑通了,训练参数 sg=1,hs=1 必须
April 24th, 2017
当然这样做的代价就是:TF-IDF算法的效率是O(N),而用Word2Vec提取,效率显然是O(N2),这里的N是句子中的词数。所以说效率是下降了吗???
是的,对于长句子,效率下降得很严重。
June 27th, 2017
你好,苏老师。
word2vec模型也训练好了,接下来就是提取关键词的过程
data["key_word_5"] = data["content_sum"].map(lambda x : (keywords(jieba.cut(str(x)))))
0.5G的数据跑了一天还没跑完,想问一下是什么情况
那你就逐条跑,看看每一条的速度咯~
August 12th, 2017
你好,苏老师!请问你加载的模型是自己使用自定义语料训练出来的还是下载的?可以发这个word2vec_wx模型给我吗。
看文章要看全套呀~^_^
http://kexue.fm/archives/4304/
October 10th, 2017
老师,您好!我想用word2vecl来提关键词进而来做文本相似,就是后续需要用到权重,不知道, (朝代, -485.150966757),这种后面可以当做权重吗?能不能变成正数?
事实上所谓“权值”是没有定义的,“权值”要怎么定义?是要句子中每个词的权重之和加起来等于1?还是要每个词都有一个接近1的数字?假如是后者,那么我说“太阳是一颗恒星”中“太阳”的权重是0.8,那么又能从中得到什么信息?(这个词很重要?为什么很重要?如果其他词是0.99的权重呢?)
这里词语后面的负数,是条件概率的对数值,其绝对数值并没有意义,只有相对的意义,如果要强行归一化,只能自己发散思维了
是要句子中每个词的权重之和加起来等于1,我之前是用的jieba分词里的tf-idf来提取关键词,后面自带的有权重,不知道老师您对于word2vec来提取关键词,再进行赋予权重有什么好的意见?
将所有概率对数减去均值,除以标准差进行规范化,然后做一次softmax(全部取指数,然后归一化)
谢谢老师,您知道用word2vec对关键词进行聚类之后再提取特征吗?我看了一篇论文上面是说通过Word2vec进行聚类之后,选择每个聚类的中心的词作为关键词,不知道老师有什么好的建议?
October 24th, 2017
苏老师,有个地方不清楚,在提取关键词这个步骤中,计算p(oword|iword),需要计算p(iword|iword)吗?也就是在该词出现的情况下,这个词本身出现的概率
按公式来看,是必要的。
November 26th, 2017
老师您好!概率对数就是输出的lprob,那么均值的是每个节点的均值还是输入词的均值,不知道该如何计算?
输出的,比如本文例子中的 -261.691625676、-272.297758506那些,对所有输出减去均值,然后除以标准差,然后做一下softmax,就可以得到归一化的概率了。这是个经验做法,没啥理论依据。
May 12th, 2018
为什么我运行出来的结果都是一些没有意义的词啊
May 12th, 2018
我要提取关键字是可以直接写您这段程序是吗?需不需要写您上一篇文章所写的训练的代码呢?
你需要打好word2vec的基础,然后看完并弄懂本文的非代码部分,然后再来看本文的代码。
你是用我前文提供的训练好的模型,还是你自己训练的?如果是我提供的模型,直接跑上述代码并没有问题。
我直接用的您上文提供的训练好的代码,我是要提取文档的关键字,可以用您这段程序吗?