【NLP】关键词提取:TFIDF、TextRank

前两天看到论文《Chinese Poetry Generation with Planning based Neural Network》中使用TextRank进行关键词提取。在阅读文章时也想到了除了TextRank之外,经常还使用TFIDF进行关键词提取。

一些算法的使用取决于业务场景和算法的特性。关键词提取是干什么的呢?「关键词抽取的任务就是从一段给定的文本中自动抽取出若干有意义的词语或词组。」 那么这个 「有意义的」 就会和算法的特性结合在一起了。

补充一句:这两种方案是无监督的,当然也可以使用分类的方式进行有监督的处理,本文不讨论关于有监督的关键词提取方法。

技术交流

目前开通了技术交流,群友已超过3000人,添加时最好的备注方式为:来源+兴趣方向,方便找到志同道合的朋友,资料、代码获取也可以加入

方式1、添加微信号:dkl88191,备注:来自CSDN
方式2、微信搜索公众号:Python学习与数据挖掘,后台回复:加群

TFIDF

1.基本理论

TF-IDF(term frequency–inverse document frequency)是一种用于信息检索与数据挖掘的常用加权技术。TF是词频(Term Frequency),IDF是逆文本频率指数(Inverse Document Frequency)。也就是说:一个词语在一篇文章中出现次数越多, 同时在所有文档中出现次数越少, 越能够代表该文章。

词频 (term frequency, TF) 指的是某一个给定的词语在该文件中出现的次数,在实际的任务中也可以是一个句子,需要结合具体的任务灵活变通。这个数字通常会被归一化,一般是词频除以文章总词数(同一个词语在长文件里可能会比短文件有更高的词频,而不管该词语重要与否), 以防止它偏向长的文件。找到一篇文献,其中词频表示该词项在文档中出现的频率:

指:特征项也可以认为是一个词语在文档或语句中出现的频数除以文本或语句中的总词数. 文本中也存在很多无用的词语,例如:“的”、“是”、“在”----这一类最常用的词。它们叫做"停用词"(stop words),数量多并且对文本的表征贡献度不大。这个时候就需要使用一个叫做逆向文件频率的东东,它反应该词项在文档数据中的重要程度,计算公式如下:

由总文件数除以包含特征项的文件书(加0.1是为了避免分母为0),再将结果取对数。可想而知他表达的物理意义:当每篇文档都包含这个特征,那么,最后的结果接近0,那么也就是说这个特征并不是很重要。

那么一个文档中的一个特征最后的tf-idf权重则为:

2.代码解读

现在我们来看看jieba分词中tfidf是如何实现的,当然类scikit-learn库中也有tfidf库,但是scikit-learn为了使得各个模型训练方式保持一致,对其进行了比较的封装,暂时不考虑算法之外的编程设计和算法实现技巧。

扫描二维码关注公众号,回复: 14523334 查看本文章

TextRank

1.PageRank原理简介

了解一下PageRank原理更容易理解TextRank的基本原理。PageRank最开始是用来网页重要性的。整个www可以看作一张有向图,节点是网页。如果网页A存在到网页B的链接,那么有一条从网页A指向网页B的有向边。其基本思想有两条:

「链接数量」:一个网页被越多的其他网页链接,说明这个网页越重要。「链接质量」:一个网页被一个越高权值的网页链接,也能表明这个网页越重要。

一个节点的打分计算公式如下:

其中:

  • 表示网页的重要性得分,也就是PageRank(PR)值

  • 表示指向网页的所有其他网页的一个集合

  • 表示网页中包含的其他网页的一个集合,则表示网页数量

  • 是一个阻尼系数,为了解决没有入链网页的得分,一般取0.85

通过不断的迭代PR 值就能收敛到一个稳定的值,而当阻尼系数接近 1 时,需要的迭代次数会陡然增加很多,且排序不稳定。

「链接网页的初始分数如何确定:」 算法开始时会将所有网页的得分初始化为 1,然后通过多次迭代来对每个网页的分数进行收敛。收敛时的得分就是网页最终得分。若不能收敛,也可以通过设定最大迭代次数来对计算进行控制,计算停止时的分数就是网页的得分。

2.TextRank原理简介

进行关键词提取时,TextRank算法思想和PageRank算法类似,不同的是,TextRank中是以词为节点,以**「共现关系」**建立起节点之间的链接,PageRank中是有向边,而TextRank中是无向边,或者说是双向边。

什么是共现关系呢?将文本进行分词,去除停用词或词性筛选等之后,设定窗口长度为,即最多只能出现K个词,进行窗口滑动,在窗口中共同出现的词之间即可建立起无向边。

原论文中的例子对如下文本进行关键词提取:

Compatibility of systems of linear constraints over the set of natural numbers.
Criteria of compatibility of a system of linear Diophantine equations, strict
inequations, and nonstrict inequations are considered. Upper bounds for
components of a minimal set of solutions and algorithms of construction of
minimal generating sets of solutions for all types of systems are given.
These criteria and the corresponding algorithms for constructing a minimal
supporting set of solutions can be used in solving all the considered types
systems and systems of mixed types.

进行图的构建得到如下图:图片

关键词结果如下:图片

总体来说效果还可可以的。TextRank计算公式如下:

可以看出,该公式仅仅比 PageRank 多了一个权重项 ,用来表示两个节点之间的边连接有不同的重要程度。

TextRank用于关键词提取的算法如下 :

  1. 把给定的文本 按照完整句子进行分割,得到

  2. 对于每个句子,进行分词和词性标注,并过滤掉停用词,只保留指定词性的单词,如名词、动词、形容词,得到,其中是保留后的候选关键词

  3. 构建候选关键词图,其中为节点集,由上一步生成的候选关键词组成,然后采用共现关系构造任两点之间的边,两个节点之间存在边仅当他们对应的词汇在长度的窗口中共现,表示窗口大小,即最多共现个单词

  4. 根据上面的公式,迭代传播各节点的权重,直至收敛

  5. 对节点权重进行倒排序,从而得到最重要的个词语,作为候选关键词

  6. 由上一步得到的最重要的个单词,在原始文本中进行标记,若形成相邻词组,则组合成多词关键词

上面最后一步相当于是对 TextRank 结果进行处理,或者可以考虑候选关键词在文本中出现的位置来进一步处理,一般出现在文档靠前以及靠后的词更重要.

提取关键词短语的方法基于关键词提取,可以简单认为:如果提取出的若干关键词在文本中相邻,那么构成一个被提取的关键短语。

如果是生成摘要,则将文本中的每个句子分别看做一个节点,如果两个句子有相似性,那么认为这两个句子对应的节点之间存在一条无向有权边。考察两个句子的相似度就有很多方法了。

算法使用分析

对于TFIDF算法来说,如果对当前现有的文本数据进行关键词提取,就可以使用当前的语料计算各个词语的权重,获取对应文档的关键词,而对于已经有了一部分语料,提取新文本的关键词的话,新文本中的关键词提取效果比较依赖于已有的语料。

对于TextRank来说,如果待提取关键词的文本较长,那么可以直接使用该文本进行关键词提取,不需要相关的语料。当面对提取待关键词的文本较短,例如就是一个句子,那么就需要通过语料数据计算各个词语的重要程度可能更好。

除此之外,还需要考虑文本长度等来灵活运用这两种算法。

代码解读

1.代码实现

jieba分词是中文信息处理中必不可少的利器,其中就实现了TFIDF和TextRank算法。这里我们顺便看一下它是如何实现的以及如何使用的。这两个算法的实现在python jieba中路径如下:图片看了一下源码的实现,

1.1 TFIDF

首先实现了一个关键词提取的类:

class KeywordExtractor(object):

    STOP_WORDS = set((
        "the", "of", "is", "and", "to", "in", "that", "we", "for", "an", "are",
        "by", "be", "as", "on", "with", "can", "if", "from", "which", "you", "it",
        "this", "then", "at", "have", "all", "not", "one", "has", "or", "that"
    ))

    def set_stop_words(self, stop_words_path):
        abs_path = _get_abs_path(stop_words_path)
        if not os.path.isfile(abs_path):
            raise Exception("jieba: file does not exist: " + abs_path)
        content = open(abs_path, 'rb').read().decode('utf-8')
        for line in content.splitlines():
            self.stop_words.add(line)

    def extract_tags(self, *args, **kwargs):
        raise NotImplementedError

核心的代码如下:

class TFIDF(KeywordExtractor):

    def __init__(self, idf_path=None):
        self.tokenizer = jieba.dt
        self.postokenizer = jieba.posseg.dt
        self.stop_words = self.STOP_WORDS.copy()
        self.idf_loader = IDFLoader(idf_path or DEFAULT_IDF)
        self.idf_freq, self.median_idf = self.idf_loader.get_idf()

    def set_idf_path(self, idf_path):
        new_abs_path = _get_abs_path(idf_path)
        if not os.path.isfile(new_abs_path):
            raise Exception("jieba: file does not exist: " + new_abs_path)
        self.idf_loader.set_new_path(new_abs_path)
        self.idf_freq, self.median_idf = self.idf_loader.get_idf()

    def extract_tags(self, sentence, topK=20, withWeight=False, allowPOS=(), withFlag=False):
        """
        Extract keywords from sentence using TF-IDF algorithm.
        Parameter:
            - topK: return how many top keywords. `None` for all possible words.
            - withWeight: if True, return a list of (word, weight);
                          if False, return a list of words.
            - allowPOS: the allowed POS list eg. ['ns', 'n', 'vn', 'v','nr'].
                        if the POS of w is not in this list,it will be filtered.
            - withFlag: only work with allowPOS is not empty.
                        if True, return a list of pair(word, weight) like posseg.cut
                        if False, return a list of words
        """
        if allowPOS:
            allowPOS = frozenset(allowPOS)
            words = self.postokenizer.cut(sentence)
        else:
            words = self.tokenizer.cut(sentence)
        freq = {
    
    }
        for w in words:
            if allowPOS:
                if w.flag not in allowPOS:
                    continue
                elif not withFlag:
                    w = w.word
            wc = w.word if allowPOS and withFlag else w
            if len(wc.strip()) < 2 or wc.lower() in self.stop_words:
                continue
            freq[w] = freq.get(w, 0.0) + 1.0
        total = sum(freq.values())
        for k in freq:
            kw = k.word if allowPOS and withFlag else k
            freq[k] *= self.idf_freq.get(kw, self.median_idf) / total

        if withWeight:
            tags = sorted(freq.items(), key=itemgetter(1), reverse=True)
        else:
            tags = sorted(freq, key=freq.__getitem__, reverse=True)
        if topK:
            return tags[:topK]
        else:
            return tags

jieba分词中已经计算了27万个词语的idf值,可直接计算当前语句或文档的各个词语的TFIDF值,进而获取对应的关键词。如果需要在自己的语料中计算idf值的话,建议还是使用相对专业的库如:scikit-learn,本文就不展开介绍了。

1.2 TextRank

TextRank除了继承KeywordExtractor这个类之外,还书写一个计算无向图的类如下:

class UndirectWeightedGraph:
    d = 0.85

    def __init__(self):
        self.graph = defaultdict(list)

    def addEdge(self, start, end, weight):
        # use a tuple (start, end, weight) instead of a Edge object
        self.graph[start].append((start, end, weight))
        self.graph[end].append((end, start, weight))

    def rank(self):
        ws = defaultdict(float)
        outSum = defaultdict(float)

        wsdef = 1.0 / (len(self.graph) or 1.0)
        for n, out in self.graph.items():
            ws[n] = wsdef
            outSum[n] = sum((e[2] for e in out), 0.0)

        # this line for build stable iteration
        sorted_keys = sorted(self.graph.keys())
        for x in xrange(10):  # 10 iters
            for n in sorted_keys:
                s = 0
                for e in self.graph[n]:
                    s += e[2] / outSum[e[1]] * ws[e[1]]
                ws[n] = (1 - self.d) + self.d * s

        (min_rank, max_rank) = (sys.float_info[0], sys.float_info[3])

        for w in itervalues(ws):
            if w < min_rank:
                min_rank = w
            if w > max_rank:
                max_rank = w

        for n, w in ws.items():
            # to unify the weights, don't *100.
            ws[n] = (w - min_rank / 10.0) / (max_rank - min_rank / 10.0)

        return ws

核心代码如下:

class TextRank(KeywordExtractor):

    def __init__(self):
        self.tokenizer = self.postokenizer = jieba.posseg.dt
        self.stop_words = self.STOP_WORDS.copy()
        self.pos_filt = frozenset(('ns', 'n', 'vn', 'v'))
        self.span = 5

    def pairfilter(self, wp):
        return (wp.flag in self.pos_filt and len(wp.word.strip()) >= 2
                and wp.word.lower() not in self.stop_words)

    def textrank(self, sentence, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'), withFlag=False):
        """
        Extract keywords from sentence using TextRank algorithm.
        Parameter:
            - topK: return how many top keywords. `None` for all possible words.
            - withWeight: if True, return a list of (word, weight);
                          if False, return a list of words.
            - allowPOS: the allowed POS list eg. ['ns', 'n', 'vn', 'v'].
                        if the POS of w is not in this list, it will be filtered.
            - withFlag: if True, return a list of pair(word, weight) like posseg.cut
                        if False, return a list of words
        """
        self.pos_filt = frozenset(allowPOS)
        g = UndirectWeightedGraph()
        cm = defaultdict(int)
        words = tuple(self.tokenizer.cut(sentence))
        for i, wp in enumerate(words):
            if self.pairfilter(wp):
                for j in xrange(i + 1, i + self.span):
                    if j >= len(words):
                        break
                    if not self.pairfilter(words[j]):
                        continue
                    if allowPOS and withFlag:
                        cm[(wp, words[j])] += 1
                    else:
                        cm[(wp.word, words[j].word)] += 1

        for terms, w in cm.items():
            g.addEdge(terms[0], terms[1], w)
        nodes_rank = g.rank()
        if withWeight:
            tags = sorted(nodes_rank.items(), key=itemgetter(1), reverse=True)
        else:
            tags = sorted(nodes_rank, key=nodes_rank.__getitem__, reverse=True)

        if topK:
            return tags[:topK]
        else:
            return tags

    extract_tags = textrank

这个实现也是针对当前文档或语句计算的topK个关键词,也没有用到一些语料。所以不管是tfdif和textrank如果是业务比较复杂的话要么使用scikit-learn要么还是自己去实现。不过上面的代码也还是可以去借鉴的,实现的方式也很不错。

2.使用样例

测试样本,也是上一篇文章的一部分内容。

前段时间回顾和学习了基于RNN+Attention与基于CNN+Attention的seq2seq模型:【NLP】seq2seq 由浅入深——基于Rnn和Cnn的处理方式,于是现在想找一些案例练练手。
seq2seq最常见的实践就是翻译,看看网上大多是什么英语到法语,到德语的一些案例。说实话,能不能整点能看懂的呢?或许大家都没有公开的语料吧,坦白讲我也没有,哈哈。那就去github上找找。
除了机器翻译,seq2seq还是有一些比较有意思的落地场景。比如说,我们打电话到海底捞预定,一般情况下接电话的女声,其实是一个机器人来帮你预定,听起来还是比较智能的。这里就用到了seq2seq,但是涉及到语音处理。另一个是华为团队,通过seq2seq为基础设计的模型实现了计算机对微博的自动回复,并通过模型间的对比得到了一系列有意思的结果。如下图,post为微博主发的文,其余四列为不同模型对该条微博做出的回复。

2.1 TFIDF

jieba中默认使用TFIDF提取关键词,如下:

text = '''前段时间回顾和学习了基于RNN+Attention与基于CNN+Attention的seq2seq模型:【NLP】seq2seq 由浅入深——基于Rnn和Cnn的处理方式,于是现在想找一些案例练练手。
seq2seq最常见的实践就是翻译,看看网上大多是什么英语到法语,到德语的一些案例。说实话,能不能整点能看懂的呢?或许大家都没有公开的语料吧,坦白讲我也没有,哈哈。那就去github上找找。
除了机器翻译,seq2seq还是有一些比较有意思的落地场景。比如说,我们打电话到海底捞预定,一般情况下接电话的女声,其实是一个机器人来帮你预定,听起来还是比较智能的。这里就用到了seq2seq,但是涉及到语音处理。另一个是华为团队,通过seq2seq为基础设计的模型实现了计算机对微博的自动回复,并通过模型间的对比得到了一系列有意思的结果。如下图,post为微博主发的文,其余四列为不同模型对该条微博做出的回复。
'''
res = jieba.analyse.extract_tags(text, topK=5)
print(res)

结果如下:图片总体结果还是差强人意的。

2.2 TextRank

jieba中调用textrank进行关键词提取的函数接口与使用tfidf类似,具体操作如下:

res = jieba.analyse.textrank(text, topK=5)
print(res)

图片这里的结果看起来就没有使用TFIDF提取的效果好了,不过“模型”这个关键词提取出来了,毕竟一方面TFIDF中使用了事先已经计算好了的idf值。而测试文本描述的内容主题也不是很突出。

总结

总得来说,TFIDF和TextRank两个算法从理解和实现上的难度来说并不难(不要问我TextRank算法为什么迭代后能够收敛)。但是具体的实际场景和业务下是需要结合相关语料进行处理,使用提供接口比较好的包如scikit-learn中的TFIDF,业务比较复杂的话TextRank也可以自己去实现。不过使用jieba的tfidf进行关键词提取也还是可行的,毕竟已经有计算好了的idf值,覆盖的词语数也达到了27万多,总体效果也是可以接受的。

猜你喜欢

转载自blog.csdn.net/qq_34160248/article/details/126919062