TextRank源码的学习与详细解析

目录

 

1.textrank源码解析

2.textrank源码中UndirectWeightedGraph类方法分解解析

(1)初始化函数

(2)添加边的函数def addEdge(self, start, end, weight)

(3)def rank(self)函数(个人觉得在这个无向有权图类中最重要的一部分)

3.textrank源码中TextRank(KeywordExtractor)类的代码分片解释

(1)类的初始化片段

(2) def pairfilter(self, wp)函数

(3)def textrank()非常重要


1.textrank源码解析

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import, unicode_literals
import sys
from operator import itemgetter
from collections import defaultdict
import jieba.posseg
from .tfidf import KeywordExtractor
from .._compat import *


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)#权值list表
        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)#e[2]是什么?

        # 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]:
                    # 将这些入度结点贡献后的权值相加
                    # 贡献率 = 入度结点与结点n的共现次数 / 入度结点的所有出度的次数
                    s += e[2] / outSum[e[1]] * ws[e[1]]
                # 更新结点n的权值
                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):
        #初始化时,默认加载分词函数tokenizer = jieba.dt以及词性标注工具jieba.posseg.dt,停用词stop_words = self.STOP_WORDS.copy(),
        #词性过滤集合pos_filt = frozenset(('ns', 'n', 'vn', 'v')),窗口span = 5,(("ns", "n", "vn", "v"))表示词性为地名、名词、动名词、动词。
        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):
            #词i满足过滤条件
            if self.pairfilter(wp):
                # 依次遍历词i 之后窗口范围内的词
                for j in xrange(i + 1, i + self.span):
                    # 词j 不能超出整个句子
                    if j >= len(words):
                        break
                        # 词j不满足过滤条件,则跳过
                    if not self.pairfilter(words[j]):
                        continue
                        # 将词i和词j作为key,出现的次数作为value,添加到共现词典中
                    if allowPOS and withFlag:
                        cm[(wp, words[j])] += 1
                    else:
                        cm[(wp.word, words[j].word)] += 1
        # 依次遍历共现词典的每个元素,将词i,词j作为一条边起始点和终止点,共现的次数作为边的权重
        for terms, w in cm.items():
            g.addEdge(terms[0], terms[1], w)
            # 运行textrank算法
        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)
        # 输出topK个词作为关键词
        if topK:
            return tags[:topK]
        else:
            return tags

    extract_tags = textrank

本人主要研究的是关键词提取,之前看了很多博客,但是都没有研究过textrank源码,仅是是用别人的代码来跑程序,今天是对源码的一个学习,其主调函数是TextRank.textrank函数,主要是在jieba/analyse/textrank.py中实现。

TextRank源码的学习与详细解析_第1张图片

这是源代码主要的目录,包含两个类,就是UndirectWeightedGraph和TextRank(KeywordExtractor)。下面进行分别阐述

2.textrank源码中UndirectWeightedGraph类方法分解解析

(1)初始化函数

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

其初始化函数__init__中的self.graph = defaultdict(list)实质上就是一个字典,这个字典是嵌套,是将列表存储在字典中,词典的Key是后续要添加的词,词典的value则是一个由(起始点,终止点,边的权重)构成的三元组所组成的列表,表示已这个词作为起始点的所有的边。

(2)添加边的函数def addEdge(self, start, end, weight)

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))

无向有权图添加边的操作是在addEdge函数中完成的,因为是无向图,所以我们需要依次将start作为起始点,end作为终止点,然后再将start作为终止点,end作为起始点,这两条边的权重是相同的。

(3)def rank(self)函数(个人觉得在这个无向有权图类中最重要的一部分)

这个代码中的解释是博客搜索中常规的解释,但是有些部分解释的没有那么详细,本人凭自己的理解做一个详细的分析:

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]:
                # 将这些入度结点贡献后的权值相加
                # 贡献率 = 入度结点与结点n的共现次数 / 入度结点的所有出度的次数
                s += e[2] / outSum[e[1]] * ws[e[1]]
            # 更新结点n的权值
            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

昨天晚上写的一部分:在UndirectWeightedGraph类中的函数rank是textrank算法的迭代,首先对每个节点赋予相同的权重,并统计该节点的所有出度的次数之和,接着就是进行迭代计算,在每次的迭代中,依次遍历每个节点;对于节点n,首先要根据无向有权图得到节点n的所有入度节点,对于无向有权图,入度节点和出度节点是相同的,都是与节点n相连的节点, (实际上无向有权图可以看做是双向图,也就是双箭头,节点之间有入度那就有出度),在这里比较疑惑地是outSum[n] = sum((e[2] for e in out), 0.0)中的e[2]是什么,在源码中也没有找到e[2]来源于哪里?在前面我们已经计算出这个入度节点的所有出度的次数,而它对于节点的权值的共现等于它本身的权值  乘以 它与节点n的共现次数/和各节点的所有出度的次数,将各个入度节点得到的权值相加,再乘以一定的阻尼系数,即可得到节点n的权值,迭代完成后,对权值进行归一化,并返回各个节点及其对应的权重。(一知半解状态)

今天:一句一句进行分析

        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)

这是个循环结构,对n各结点的权值赋值为初始值=》然后统计各个结点的出度次数之和,e应该是结点指向其他节点的边,而e[2]应该是代表结点出度的边的值,因为无向图可以视作一个双向图,既有指出的也有链入的,如图。

TextRank源码的学习与详细解析_第2张图片

 

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]:
                # 将这些入度结点贡献后的权值相加
                # 贡献率 = 入度结点与结点n的共现次数 / 入度结点的所有出度的次数
                s += e[2] / outSum[e[1]] * ws[e[1]]
            # 更新结点n的权值
            ws[n] = (1 - self.d) + self.d * s

这一部分博客上只做了简单的介绍,个人觉得应该与textrank公式对应起来理解,首先代码的第一行是对图keys进行一个排序,重要的是遍历中的内容,首先限定了迭代的次数,然后在每次迭代中依次遍历排序keys中的每个节点,接下来的s的计算应该与textrank的公式对照起来,如图所示:

代码中的e[2]应该对应公式中Wji,即结点vj到结点vi的边的权重,而outSum[e[1]] * ws[e[1]]则对应公式中的分母部分,就是将节点vj指定其他节点的边的权重相加作为分母,结点vj到结点vi的边的权重在vj出度中的占比,而s+= e[2] / outSum[e[1]] * ws[e[1]]就是textrank中所对应的d的后半部分,应该这样就比较清晰了。

 (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

剩下的就是权值大小的获取,但是看不懂的是(min_rank, max_rank) = (sys.float_info[0], sys.float_info[3])这句代码,为什么要选取这两个值作为比较的基准值呢sys.float_info[0], sys.float_info[3]?(需要思考)

3.textrank源码中TextRank(KeywordExtractor)类的代码分片解释

(1)类的初始化片段

    def __init__(self):
        #初始化时,默认加载分词函数tokenizer = jieba.dt以及词性标注工具jieba.posseg.dt,停用词stop_words = self.STOP_WORDS.copy(),
        #词性过滤集合pos_filt = frozenset(('ns', 'n', 'vn', 'v')),窗口span = 5,(("ns", "n", "vn", "v"))表示词性为地名、名词、动名词、动词。
        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

类在初始化时,默认加载了分词函数和词性标注函数tokenizer = postokenizer = jieba.posseg.dt、停用词表stop_words = self.STOP_WORDS.copy()、词性过滤集合pos_filt = frozenset((‘ns’, ‘n’, ‘vn’, ‘v’)),窗口span = 5,((“ns”, “n”, “vn”, “v”))表示词性为地名、名词、动名词、动词。

(2) def pairfilter(self, wp)函数

    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)#strip()该方法只能删除开头或是结尾的字符,不能删除中间部分的字符。

这个函数在网上没有找到类似的答案,本人根据下文的程序代码,将其理解为是定义了一个分词过滤条件,wp.flag应该是词所对应的词性要满足词性过滤条件,wp.word的词长要大于2,且词是非停用词。疑问;wp是个具体的什么形式?暂时没想明白。

(3)def textrank()非常重要

def textrank(self, sentence, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'), withFlag=False):

在def textrank(self, sentence, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'), withFlag=False):中传入的参数withFlag如何理解,它与withWeight为false的情况下返回的结果有何差异?需要好好考虑。

在做遍历前的代码解释;

        self.pos_filt = frozenset(allowPOS)
        
        g = UndirectWeightedGraph()
        
        cm = defaultdict(int)
        
        words = tuple(self.tokenizer.cut(sentence))


#tuple是一个元组,tuple是一种有序列表,它和list非常相似,但tuple一旦初始化就不能修改,而且没有append() insert()这些方法,可以获取元素但不能赋值变成另外的元素

首先是引入词性限制集合,即:self.pos_filt = frozenset(allowPOS),其中 frozenset()函数的含义是:返回一个冻结的集合,冻结后集合不能再添加或删除任何元素。然后定义一个无向有权图,以及共现词典,然后使用结巴分词器tokenizer分词器进行分词,tuple()是一个元组,tuple也是一种有序列表,它和list非常相似, 但tuple一旦初始化就不能修改,而且没有append() insert()这些方法,可以获取元素但不能赋值变成另外的元素,分词结果应该是带有词性的分词结果;

下面是两个遍历;

第一个遍历:

        for i, wp in enumerate(words):
            #词i满足过滤条件
            if self.pairfilter(wp):
                # 依次遍历词i 之后窗口范围内的词
                for j in xrange(i + 1, i + self.span):
                    # 词j 不能超出整个句子
                    if j >= len(words):
                        break
                        # 词j不满足过滤条件,则跳过
                    if not self.pairfilter(words[j]):
                        continue
                        # 将词i和词j作为key,出现的次数作为value,添加到共现词典中
                    if allowPOS and withFlag:
                        cm[(wp, words[j])] += 1
                    else:
                        cm[(wp.word, words[j].word)] += 1

依次遍历分词结果,如果某个词满足过滤条件(词性在词性过滤集合中 allowPos,并且长度大于等于2,且不是停用词),将这个词之后窗口范围内的词j(也要满足前面所述条件),将它们两两(词i和词j)作为key,出现的次数作为value添加到共现词典中,即cm词典格式[(词i,词j):次数];

第二个遍历;

        for terms, w in cm.items():
            g.addEdge(terms[0], terms[1], w)
            # 运行textrank算法
        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)
        # 输出topK个词作为关键词
        if topK:
            return tags[:topK]
        else:
            return tags

    extract_tags = textrank

然后,依次遍历共现词典,将词典中的每个元素,key = (词i,词j),value = 词i和词j共现的次数,其中词i,词j作为一条边的起始点和终止点,共现次数作为边的权重,添加到之前定义的无向有权图中。

然后对这个无向有权图进行迭代计算textrank,最终经过若干次迭代后,收敛,每个词都对应一个指标值;

如果设置了权重标志位,则根据指标值对无向有权图中的词进行降序排序,最后输出top K个词作为关键词。

这里简单讲解一下key=itemgetter(1)中的itemgetter函数,因为自己不懂,就进行了粗略的的查询,它是operator模块中的函数,用于获取对象的哪些维度的数据,参数为一些序号(即需要获取的数据在对象中的序号);

还查了一下__getitem__但是没有看懂,后续查了再补充。

如果有对源码有更好理解的,希望能够彼此互相学习!

下面是学习的博客链接:

https://www.cnblogs.com/tsdblogs/p/9982886.html

https://blog.csdn.net/Atishoo_13/article/details/86616607

https://www.cnblogs.com/zhbzz2007/p/6177832.html

https://blog.csdn.net/qq_41664845/article/details/82869596

 

 

 

 

你可能感兴趣的:(自然语言处理,python学习,TextRank源码解析)