关键词抽取与自动文摘

关键词抽取与自动文摘

在自然语言处理中对于关键词抽取与自动文摘这两个主题,有着多种多样的方式去解决它们,这里将介绍一种叫做TextRank的方法,就可以解决这两个问题。我将结合具体的代码,试图将算法解释地更加清楚些。

论文『TextRank: Bringing Order into Texts』中首次提出了TextRank方法,如果想全面了解下这个方法,还是仔细看下这篇论文。

当你一看到TextRank这个名字的时候,你是否会觉得很熟悉,它是否有让你联想到Google的PageRank。其实,该方法就是基于PageRank而来的,只是将page替换成word、sentence以完成关键词抽取与自动文摘任务。当然其中的一些计算准则是以文本为标准的。

如果你还不熟悉PageRank的话,还是去网上找一篇博客来看看,其实是不难的。

关键词抽取中的TextRank

之前我们也提到了,就是将PageRank模型中的page替换成word。因为是用于文本中,所以与用于网页中是不一样的。改动的地方主要有:

PageRank中因为有链接的存在,所以是将存在链接的两个网页相连。而在文本处理中,一个词的出现,与其前后词之间是有联系的。在TextRank中存在一个窗口的概念,将一个词与窗口内的所有词进行连接。窗口值的设置也是比较讲究的。实验表明,如果窗口值设置的过大,不仅带来计算时间上的提高(因为图中的边数增加),而且会造成模型性能的下降。这里模型的性能是使用具体的任务去衡量的,关键词抽取是作为具体任务的前驱。

因为链接是具有指向性的,所以PageRank模型构造的是一个有向图。而在文本中,词与词之间的联系是很难确定方向的,所以既可以构造一个有向图也可以构造一个无向图。实验表明,有向图和无向图在收敛性上是没有差别的,就是它们都会收敛到一个固定值上。至于初始值的设置,在PageRank中就已经证明了,最终的结果与初值的设定无关,只与迭代的次数有关。只要迭代的次数足够,就能收敛到一个固定值。

在PageRank中,会将爬虫所爬到的所有网页都加入到模型中去,至于死链的去除,这都是后话。但在TextRank中,不会将文本中出现的所有词都加入到图模型中。因为不是所有的词都是有其实际意义的,例如说介词。所以在构建图模型时,会利用一定的语法规则过滤掉一些词,以减小图的规模。这里的过滤规则一般来说是基于词性的,所以在之前进行分词是还需要将词的词性给标记出来。实验表明,最佳的选择是保留名词和形容词。

PageRank模型中,连接的关系一般只会发生一次,很少会有一个网页中存在两个指向同一个网页的链接。但在文本中却不同,有一些词是固定的搭配,它们会在一起出现多词。所以TextRank所构造的图应该是一个赋权图。这里的权值应该是两者共同出现的次数。相应的每个结点权值的更新公式应该更改为:

WS(Vi)=(1d)+d×VjIn(Vi)wjiVkOut(Vj)wjkWS(Vj)

其中d表示衰减系数,这个在PageRank模型中就已经有了的, WS(Vi) 表示结点 Vi 的权值。在无向图中, In(Vi)Out(Vi) 均表示与结点 Vi 相连的结点;在有向图中,则分别表示结点的入边和出边。

在构造完图之后,就与PageRank一样,使用迭代的算法计算每一个结点的权值。根据你想获得的关键词的数目,输出权值最高的那些结点所对应的词。

jieba中的关键词抽取

下面我们以开源中文分词库jieba为例,看看其中关键词抽取的实现。项目的主页是https://github.com/fxsjy/jieba,这里列出的是python的版本,还有其他很多语言的版本可供使用。

# 用于表示文本的无向带权图
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,textrank值的计算只进行10次循环
            # 进行textrank值的更新
            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 # 表示词与词之间有联系的窗口的大小为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): # i和wp分别代表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

自动文摘中的TextRank

在关键词抽取任务中,图模型中的结点是词,在自动文摘中结点表示的就是句子。模型的构建方式自然也要发生一些改变,主要有以下两点:

首先,窗口模型在这个任务中并不适用,因为前后句子间的关系不像是词间的关系那么紧密。

其次,图中的权值使用的是句子间的相似度,具体是使用哪一种衡量标准,还是要看实现吧。

模型构建好之后,还是利用迭代的方式求出每一个句子的权值。根据你所希望文摘的长度,输出权值最高的句子。我觉得这个方法所存在的最为重要的一个问题是:因为只是从文章中抽取出几个句子作为代表,这些句子之间逻辑上可能并没有联系,所以得到的文摘看上去会有一些别扭。当然也有方法是根据文摘的内容自动地生成摘要,那样的话,逻辑上会更为通顺一些。当然方法的难度上肯定会更大,这个就不在讨论范围之内了。

snownlp中的自动文摘

接下来,我们以开源库snownlp为例,看一下其中的自动文摘算法实现。算法的主页是https://github.com/isnowfy/snownlp,其主要的任务与jieba还是非常类似的。

class SnowNLP(object):
    @property
    def sentences(self): # 将一篇文章按句划分,实际上是细分到了逗号的
        return normal.get_sentences(self.doc)

    def summary(self, limit=5):
        doc = []
        sents = self.sentences
        for sent in sents: # 这里对整个句子进行了分词,是为了方便计算句子间的相似度。一个句子的分词结果是以一个list方式存储的,并没有破坏句子的完整性
            words = seg.seg(sent)
            words = normal.filter_stop(words)
            doc.append(words)
        rank = textrank.TextRank(doc)
        rank.solve()
        ret = []
        for index in rank.top_index(limit):
            ret.append(sents[index]) # 这里应该返回整个句子,而不是句子的分词结果
        return ret
class TextRank(object):

    def __init__(self, docs):
        self.docs = docs
        self.bm25 = BM25(docs)
        self.D = len(docs)
        self.d = 0.85
        self.weight = [] # 存储一篇文档中每个句子间的相似度,作为连接边的权值
        self.weight_sum = [] # 某一个句子与其他句子之间的相似度之和, 作为该点的入度(出度)
        self.vertex = [] # 每一个句子的textrank值
        self.max_iter = 200
        self.min_diff = 0.001
        self.top = []

    def solve(self):
        for cnt, doc in enumerate(self.docs):
            scores = self.bm25.simall(doc) # 使用BM25作为衡量句子间相似度的标准
            self.weight.append(scores)
            self.weight_sum.append(sum(scores)-scores[cnt])
            self.vertex.append(1.0)
        for _ in range(self.max_iter):
            m = []
            max_diff = 0
            for i in range(self.D):
                m.append(1-self.d)
                for j in range(self.D):
                    if j == i or self.weight_sum[j] == 0: # 判断是否等于0是为了避免除0错误
                        continue
                    # 进行textrank值的更新
                    m[-1] += (self.d*self.weight[j][i]
                              / self.weight_sum[j]*self.vertex[j])
                if abs(m[-1] - self.vertex[i]) > max_diff: # 记录最大的改变量
                    max_diff = abs(m[-1] - self.vertex[i])
            self.vertex = m
            if max_diff <= self.min_diff: # 如果textrank值没有明显的变化,则跳出循环
                break
        self.top = list(enumerate(self.vertex))
        self.top = sorted(self.top, key=lambda x: x[1], reverse=True) # 按照textrank的值进行排序

    def top_index(self, limit):
        return list(map(lambda x: x[0], self.top))[:limit]

    def top(self, limit):
        return list(map(lambda x: self.docs[x[0]], self.top))

这里使用了使用了BM25作为衡量两个句子间相似度的标准,关于BM25的含义,可参考wiki:https://en.wikipedia.org/wiki/Okapi_BM25

class BM25(object):

    def __init__(self, docs):
        self.D = len(docs)
        self.avgdl = sum([len(doc)+0.0 for doc in docs]) / self.D # 计算文档的平均长度
        self.docs = docs
        self.f = [] # 统计每一个词出现的次数
        self.df = {} # 统计一个词在多少篇文档中出现过
        self.idf = {}
        self.k1 = 1.5
        self.b = 0.75
        self.init()

    def init(self):
        for doc in self.docs: # 在传入文档时,每一个文档都已经是分好词了的,每一篇文档就是一个list
            tmp = {}
            for word in doc:
                if not word in tmp:
                    tmp[word] = 0
                tmp[word] += 1
            self.f.append(tmp)
            for k, v in tmp.items():
                if k not in self.df:
                    self.df[k] = 0
                self.df[k] += 1
        for k, v in self.df.items():
            self.idf[k] = math.log(self.D-v+0.5)-math.log(v+0.5) # 计算逆文档频率,这个公式在wiki中有介绍,并不是一般我们计算逆文档频率的公式

    def sim(self, doc, index):
        score = 0
        for word in doc:
            if word not in self.f[index]:
                continue
            d = len(self.docs[index])
            # 计算输入与该篇文档的相似度
            score += (self.idf[word]*self.f[index][word]*(self.k1+1)
                      / (self.f[index][word]+self.k1*(1-self.b+self.b*d
                                                      / self.avgdl)))
        return score

    def simall(self, doc): # 将输入看做是查询语句,计算相似度
        scores = [] # 计算文档之间两两相似度
        for index in range(self.D):
            score = self.sim(doc, index)
            scores.append(score)
        return scores
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
关键词抽取是一种用于从文本中提取出最具代表性的关键词的方法。在Python中,有多种方法可以实现关键词抽取,其中包括TF-IDF、TextRank和Word2Vec词向量聚类等方法。 TF-IDF是一种常用的关键词抽取方法,它通过计算词频-逆文档频率(TF-IDF)值来评估一个词在文本中的重要程度。TF-IDF的计算公式是根据词频和文档频率之间的关系来得出的。 TextRank是一种基于图的排序算法,它通过将文本中的词作为节点,根据词之间的共现关系构建图,并通过迭代计算节点的重要性得到关键词。TextRank算法可以将文本中的重要信息进行抽取和排序,从而得到关键词。 而Word2Vec词向量聚类是一种将单词表示为向量的方法。通过训练一个word2vec模型,我们可以将每个词映射为一个向量表示,然后可以使用向量之间的相似度来确定关键词。 在Python中,有多个库可以实现关键词抽取,其中比较常用的是jieba库。jieba库提供了一个简单易用的接口,可以方便地实现关键词抽取。你可以使用jieba库的tfidf函数来进行关键词抽取,通过调整函数的参数,可以实现不同的筛选和返回方式。 总结起来,关键词抽取是一种从文本中提取出最具代表性的关键词的方法,Python中可以使用TF-IDF、TextRank和Word2Vec词向量聚类等方法实现关键词抽取。其中,jieba库是一个常用的工具库,可以方便地实现关键词抽取。你可以通过调用jieba库的tfidf函数来进行关键词抽取,并通过调整参数来实现不同的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值