跳转至

TextRank

TextRank算法提取关键词的结构化流程如下

image-20200918173649653

数据预处理

进行关键词提取之前,需要对源文件进行一系列预处理:

  • 分句
  • 分词(词干提取、词形还原)
  • 过滤数字、特殊字符等,大小写转换

分句

使用python中的nltk库进行分句

from nltk.tokenize import sent_tokenize
sents = sent_tokenize(text)

分句情况大致如下,可以看出分句情况较为准确

image-20200918173351972

分词(词干提取、词形还原)

nltk提供了分词工具,API如下

1
2
3
4
5
from nltk.stem import WordNetLemmatizer

wnl = WordNetLemmatizer()
print(wnl.lemmatize('ate', 'v'))
print(wnl.lemmatize('fancier', 'n'))

但是,这种分词方法需要确定单词在的词性,好在nltk也为我们提供了方法来判断句子的词性,将其封装为方法如下

def _get_wordnet_pos(self, tag):
    """ 返回词性
    """
    if tag.startswith('J'):
        return wordnet.ADJ
    elif tag.startswith('V'):
        return wordnet.VERB
    elif tag.startswith('N'):
        return wordnet.NOUN
    elif tag.startswith('R'):
        return wordnet.ADV
    else:
        return None

结合后进行调用,如下:

from nltk import word_tokenize, pos_tag
from nltk.corpus import wordnet
from nltk.stem import WordNetLemmatizer

def cutwords_sent(text):
    """ 将文本切分为句子,然后再逐句分词(原形)
    """
    sents = sent_tokenize(text)  # 句子划分
    lemmas_sents = []
    for idx, sent in enumerate(sents):
        tokens = word_tokenize(sent)  # 分词
        tagged_sent = pos_tag(tokens)  # 获取单词词性
        lemmas_sent = []
        wnl = WordNetLemmatizer()
        for tag in tagged_sent:
            wordnet_pos = get_wordnet_pos(tag[1]) or wordnet.NOUN
            lemmas_sent.append(wnl.lemmatize(tag[0], pos=wordnet_pos))  # 词形还原
        lemmas_sents.insert(idx,lemmas_sent)

结果如图

image-20200918173456799

可以看出分词后的效果还不错,但仍存在问题为

  1. 没有剔除掉;:.,等特殊符号
  2. 没有剔除数字等
  3. 没有剔除一些如a、the、of等介词

过滤

问题1、2容易使用正则表达式进行剔除;

问题3我们通过nltk提供的英文停用词列表、以及“不妨假设长度为4以下的字符串无效”来进行剔除。

import re
from nltk.corpus import stopwords

invalid_word = stopwords.words('english')

# 预处理,如果是False就丢掉
def is_valid(self, word):
    if re.match(r"[()\-:;,.0-9]+", word):
        return False
    elif len(word) < 4 or word in invalid_word:
        return False
    else:
        return True

建立关系矩阵

建立关系矩阵Mn*n,其中n为单词数量(相同单词仅记一次),Mij表示j到i存在权重为Mij的关系。

关系的定义如下:

取窗口大小为win,则在每个分句中,去除停用词、标点、无效词后,每个单词与距离为win以内的单词存在联系

为了方便表示关系矩阵,这里以一个(String word, Array relative_words)的Map来进行表示存在word→relative_words的关系,例子如下(来源网络 参考)

先看看测试数据:

程序员(英文Programmer)是从事程序开发、维护的专业人员。一般将程序员分为程序设计人员和程序编码人员,但两者的界限并不非常清楚,特别是在中国。软件从业人员分为初级程序员、高级程序员、系统分析员和项目经理四大类。

分词后,得到:

句分词 = [程序员, 英文, 程序, 开发, 维护, 专业, 人员, 程序员, 分为, 程序, 设计, 人员, 程序, 编码, 人员, 界限, 特别, 中国, 软件, 人员, 分为, 程序员, 高级, 程序员, 系统, 分析员, 项目, 经理]

之后建立两个大小为5的窗口,每个单词将票投给它身前身后距离5以内的单词:

{开发=[专业, 程序员, 维护, 英文, 程序, 人员],

软件=[程序员, 分为, 界限, 高级, 中国, 特别, 人员],

程序员=[开发, 软件, 分析员, 维护, 系统, 项目, 经理, 分为, 英文, 程序, 专业, 设计, 高级, 人员, 中国],

分析员=[程序员, 系统, 项目, 经理, 高级],

维护=[专业, 开发, 程序员, 分为, 英文, 程序, 人员],

系统=[程序员, 分析员, 项目, 经理, 分为, 高级],

项目=[程序员, 分析员, 系统, 经理, 高级],

经理=[程序员, 分析员, 系统, 项目],

分为=[专业, 软件, 设计, 程序员, 维护, 系统, 高级, 程序, 中国, 特别, 人员],

英文=[专业, 开发, 程序员, 维护, 程序],

程序=[专业, 开发, 设计, 程序员, 编码, 维护, 界限, 分为, 英文, 特别, 人员],

特别=[软件, 编码, 分为, 界限, 程序, 中国, 人员],

专业=[开发, 程序员, 维护, 分为, 英文, 程序, 人员],

设计=[程序员, 编码, 分为, 程序, 人员],

编码=[设计, 界限, 程序, 中国, 特别, 人员],

界限=[软件, 编码, 程序, 中国, 特别, 人员],

高级=[程序员, 软件, 分析员, 系统, 项目, 分为, 人员],

中国=[程序员, 软件, 编码, 分为, 界限, 特别, 人员],

人员=[开发, 程序员, 软件, 维护, 分为, 程序, 特别, 专业, 设计, 编码, 界限, 高级, 中国]}

实现部分代码如下

def add_to_dict(word_list, windows=5):
    valid_word_list = []  # 先进行过滤
    for word in word_list:
        word = str(word).lower()
        if is_valid(word):
            valid_word_list.append(word)
    # 根据窗口进行关系建立
    if len(valid_word_list) < windows:
        win = valid_word_list
        build_words_from_windows(win)
    else:
        index = 0
        while index + windows <= len(valid_word_list):
            win = valid_word_list[index:index + windows]
            index += 1
            build_words_from_windows(win)

# 根据小窗口,将关系建立到words中
def build_words_from_windows(win):
    for word in win:
        if word not in words.keys():
            words[word] = []
        for other in win:
            if other == word or other in words[word]:
                continue
            else:
                words[word].append(other)

def build_words(text='', n_gram=5):
    """创建words 矩阵
    """
    if isinstance(text,str) and len(text)>0:
        cutwords_sent(text)
    for lemmas_sent in lemmas_sents:
        add_to_dict(lemmas_sent,n_gram)

2.3 迭代

TextRank的计算公式类似PageRank

image-20200918174031410

迭代的终止条件有以下两种

  1. max_diff < 指定阈值,说明已收敛
  2. max_iter > 指定迭代次数,说明迭代次数达到上限

代码实现如下

def text_rank(top_N=0, d=0.85, max_iter=100):
    min_diff = 0.05
    words_weight = {}  # {str,float)
    for word in words:
        words_weight[word] = 1 / len(words.keys())

    for i in range(max_iter):
        n_words_weight = {}  # {str,float)
        max_diff = 0
        for word in words:
            n_words_weight[word] = 1 - d
            for other in words[word]:
                if other == word or len(words[other]) == 0:
                    continue
                n_words_weight[word] += d * words_weight[other] / len(words[other])
            max_diff = max(n_words_weight[word] - words_weight[word], max_diff)
        words_weight = n_words_weight
        # print('iter', i, 'max diff is', max_diff)
        if max_diff < min_diff:
            # print('break with iter', i)
            break
    keywords = sorted(words_weight.items(),
                    key=lambda x: (x[1], x[0]),
                    reverse=True)
    if top_N > 0:
        keywords = keywords[:top_N]
    keywords = dict((x, y) for x, y in keywords)

    return dict(keywords)

凡本网注明"来源:XXX "的文/图/视频等稿件,本网转载出于传递更多信息之目的,并不意味着赞同其观点或证实其内容的真实性。如涉及作品内容、版权和其它问题,请与本网联系,我们将在第一时间删除内容!
作者: cpaulyz
来源: https://www.cnblogs.com/cpaulyz/p/13717637.html