关键词提取算法一般可分为有监督学习和无监督学习两类。
有监督的关键词提取方法可以通过分类的方式进行,通过构建一个较为完善的词表,然后判断每个文档与词表中的每个词的匹配程度,以类似打标签的方式,达到关键词提取的效果。优点是可以获得较高的精度,缺点是需要大批量的标注数据,并且要对词表进行人工维护。

无监督学习既不需要词表也不需要标注语料,也因此无监督的学习得到了大量的应用。

TF-IDF(term frequency–inverse document frequency,词频—逆文档频次算法)
是一种用于资讯检索与资讯探勘的常用加权技术,是一种统计方法。常用于评估在一个文档集中一个词对某份文档的重要性,一个词对文档越重要越有可能成为关键词。

TF-IDF算法由两部分组成:TF算法以及IDF算法。TF算法是统计一个词在一篇文档中出现的频次。也即是一个词在文档中出现的次数越多,其对文档的表达能力也越强。而IDF算法则是统计一个词在文档集的多少个文档中出现,即是如果一个词在越少的文档中出现,则其对文档的区分能力也越强。

但TF仅衡量词的出现频次,但没有考虑到词对文档的区分能力,所以在实际应用中一般都是将TF和IDF算法一起使用,从词频、逆文档频次两个角度对词的重要性进行衡量。

(1)TF的计算方式:
hanlp 抽取关键词 关键词提取常用的算法_tf-idf
在计算公式中,其中hanlp 抽取关键词 关键词提取常用的算法_hanlp 抽取关键词_02表示词hanlp 抽取关键词 关键词提取常用的算法_tf-idf_03在文档中hanlp 抽取关键词 关键词提取常用的算法_gensim_04中的出现频次,但仅是用频次来表示,长文本中的词出现频次高的概率会更大,因此会影响到不同文档之间关键词权值的比较,所以这里对词频进行了归一化,分母就是统计文档中每个词出现次数的总和,也即是一篇文档中总词数。

(2)IDF的计算方式:
hanlp 抽取关键词 关键词提取常用的算法_jieba_05

其中hanlp 抽取关键词 关键词提取常用的算法_关键词提取算法_06为文档中文档总数,hanlp 抽取关键词 关键词提取常用的算法_关键词提取算法_07为文档集中出现词hanlp 抽取关键词 关键词提取常用的算法_tf-idf_03的文档数量。分母之所以加1是采用了拉普拉斯平滑,比避免有部分新的词没有在语料库中出现过而使分母为0的情况出现,增强算法的健壮性。

(3)TF—IDF的计算方式:
hanlp 抽取关键词 关键词提取常用的算法_tf-idf_09
TF—IDF算法是TF和IDF算法的综合使用,这里的hanlp 抽取关键词 关键词提取常用的算法_jieba_10hanlp 抽取关键词 关键词提取常用的算法_hanlp 抽取关键词_11相乘是经验所得。

下面是一个具体的案例:
这里假设已经安装好了jieba和gensim包,下面是代码实现:

(1)idf的计算

  • 加载已有的文档数据集
  • 加载停用表
  • 对数据集中的文档进行分词
  • 根据停用词表过滤掉干扰词
  • 根据过滤好词统计计算idf值

(2)新文档的关键词提取

  • 对新文档进行分词
  • 根据停用词表过滤掉干扰词
  • 根据计算好的{词:tf-idf值}得出新文档的关键词
#!/usr/bin/env python3
# -*- coding: utf-8 -*-


import math

import jieba
import jieba.posseg as psg
from gensim import corpora, models
from jieba import analyse
import functools


# TF-IDF类
class TfIdf(object):
    # 四个参数分别是:训练好的idf字典,默认idf值,处理后的待提取文本,关键词数量
    def __init__(self, idf_dic, default_idf, word_list, keyword_num):
        self.word_list = word_list
        self.idf_dic, self.default_idf = idf_dic, default_idf
        self.tf_dic = self.get_tf_dic()
        self.keyword_num = keyword_num

    # 统计tf值
    def get_tf_dic(self):
        tf_dic = {}
        for word in self.word_list:
            tf_dic[word] = tf_dic.get(word, 0.0) + 1.0

        tt_count = len(self.word_list)
        for k, v in tf_dic.items():
            tf_dic[k] = float(v) / tt_count

        return tf_dic

    # 按公式计算tf-idf
    def get_tfidf(self):
        tfidf_dic = {}
        for word in self.word_list:
            idf = self.idf_dic.get(word, self.default_idf) # 这里是计算待处理文本的词的idf(针对文档集)
            tf = self.tf_dic.get(word, 0)   # 待处理文本的词频(针对自身的文档)

            tfidf = tf * idf
            tfidf_dic[word] = tfidf

        #print(tfidf_dic.items())
        # 根据tf-idf排序,去排名前keyword_num的词作为关键词
        for k, v in sorted(tfidf_dic.items(), key=functools.cmp_to_key(cmp), reverse=True)[:self.keyword_num]:
            print(k + "/ ", end='')
    
        
#  排序函数,用于topK关键词的按值排序
def cmp(e1, e2):
    import numpy as np
    res = np.sign(e1[1] - e2[1])
    if res != 0:
        return res
    else:
        a = e1[0] + e2[0]
        b = e2[0] + e1[0]
        if a > b:
            return 1
        elif a == b:
            return 0
        else:
            return -1        

# 停用词表加载方法
def get_stopword_list():
    # 停用词表存储路径,每一行为一个词,按行读取进行加载
    # 进行编码转换确保匹配准确率
    stop_word_path = './stopword.txt'
    stopword_list = [sw.replace('\n', '') for sw in open(stop_word_path).readlines()]
    return stopword_list


# 去除干扰词
def word_filter(seg_list, pos=False):
    stopword_list = get_stopword_list()
    #print('stopword_list:',stopword_list)
    filter_list = []
    # 根据POS参数选择是否词性过滤    
    for seg in seg_list:
        if not pos:   # 不进行词性过滤,则将词性都标记为n,表示全部保留
            word = seg
            flag = 'n'
        else:
            #print(seg.word,seg.flag)
            word = seg.word  # 词性标注分词后的词
            flag = seg.flag  # 词性标注分词后的词性
        if not flag.startswith('n'):
            continue
        # 过滤停用词表中的词,以及长度为<2的词
        if not word in stopword_list and len(word) > 1:
            filter_list.append(word)

    return filter_list


# 分词方法,调用结巴接口
def seg_to_list(sentence, pos=False):
    if not pos:
        # 不进行词性标注的分词方法
        seg_list = jieba.cut(sentence)
    else:
        # 进行词性标注的分词方法
        seg_list = psg.cut(sentence)
    return seg_list


# 数据加载,pos为是否词性标注的参数,corpus_path为数据集路径
def load_data(pos=False, corpus_path='./corpus.txt'):
    # 调用上面方式对数据集进行处理,处理后的每条数据仅保留非干扰词
    doc_list = []
    for line in open(corpus_path, 'r'):
        content = line.strip()
        seg_list = seg_to_list(content, pos) # 这里对语料进行不标注词性的分词
        filter_list = word_filter(seg_list, pos)
        doc_list.append(filter_list)
    return doc_list

# idf值统计方法
def train_idf(doc_list):
    idf_dic = {}
    # 总文档数
    tt_count = len(doc_list)
    # 每个词出现的文档数
    for doc in doc_list:
        # 每篇文档的不重复词
        for word in set(doc):  
            idf_dic[word] = idf_dic.get(word, 0.0) + 1.0

    # 按公式转换为idf值,分母加1进行平滑处理
    for k, v in idf_dic.items():
        idf_dic[k] = math.log(tt_count / (1.0 + v))

    # 对于没有在字典中的词,默认其仅在一个文档出现,得到默认idf值
    default_idf = math.log(tt_count / (1.0))
    return idf_dic, default_idf


def tfidf_extract(word_list, pos=False, keyword_num=10):
    # 加载已有的文档数据集,分词时不标注词性
    doc_list = load_data(pos)  
    # IDF值的计算
    idf_dic, default_idf = train_idf(doc_list)
    tfidf_model = TfIdf(idf_dic, default_idf, word_list, keyword_num)
    tfidf_model.get_tfidf()
    

if __name__ == '__main__':
    text = '6月19日,《2012年度“中国爱心城市”公益活动新闻发布会》在京举行。' + \
           '中华社会救助基金会理事长许嘉璐到会讲话。基金会高级顾问朱发忠,全国老龄' + \
           '办副主任朱勇,民政部社会救助司助理巡视员周萍,中华社会救助基金会副理事长耿志远,' + \
           '重庆市民政局巡视员谭明政。晋江市人大常委会主任陈健倩,以及10余个省、市、自治区民政局' + \
           '领导及四十多家媒体参加了发布会。中华社会救助基金会秘书长时正新介绍本年度“中国爱心城' + \
           '市”公益活动将以“爱心城市宣传、孤老关爱救助项目及第二届中国爱心城市大会”为主要内容,重庆市' + \
           '、呼和浩特市、长沙市、太原市、蚌埠市、南昌市、汕头市、沧州市、晋江市及遵化市将会积极参加' + \
           '这一公益活动。中国雅虎副总编张银生和凤凰网城市频道总监赵耀分别以各自媒体优势介绍了活动' + \
           '的宣传方案。会上,中华社会救助基金会与“第二届中国爱心城市大会”承办方晋江市签约,许嘉璐理' + \
           '事长接受晋江市参与“百万孤老关爱行动”向国家重点扶贫地区捐赠的价值400万元的款物。晋江市人大' + \
           '常委会主任陈健倩介绍了大会的筹备情况。'

    pos = True
    seg_list = seg_to_list(text, pos)
    #for i in seg_list:  # 打印输出进行了词性标注分词的结果
        #print(i)    
    filter_list = word_filter(seg_list, pos)
    print(filter_list)
    print('TF-IDF模型结果:')
    tfidf_extract(filter_list)

运行结果:

['年度', '中国', '爱心', '城市', '公益活动', '新闻', '发布会', '在京举行', '中华', '社会', '基金会', '理事长', '许嘉璐', '讲话', '基金会', '朱发忠', '全国', '老龄', '朱勇', '民政部', '社会', '巡视员', '周萍', '中华', '社会', '基金会', '副理事长', '志远', '重庆市', '民政局', '巡视员', '明政', '晋江市', '人大常委会', '陈健倩', '自治区', '民政局', '领导', '媒体', '发布会', '中华', '社会', '基金会', '秘书长', '本年度', '中国', '爱心', '城市', '公益活动', '爱心', '城市', '项目', '中国', '爱心', '城市', '大会', '内容', '重庆市', '呼和浩特市', '长沙市', '太原市', '蚌埠市', '南昌市', '汕头市', '沧州市', '晋江市', '遵化市', '公益活动', '中国', '雅虎', '副总编', '张银生', '凤凰网', '城市', '频道', '总监', '赵耀', '媒体', '优势', '方案', '中华', '社会', '基金会', '中国', '爱心', '城市', '大会', '承办方', '晋江市', '许嘉璐', '理事长', '晋江市', '国家', '重点', '地区', '价值', '款物', '晋江市', '人大常委会', '陈健倩', '大会', '情况']
TF-IDF模型结果:
晋江市/ 城市/ 大会/ 爱心/ 中华/ 基金会/ 陈健倩/ 重庆市/ 许嘉璐/ 巡视员/

下面简单解释下代码:
tf-idf算法是需要语料库的,主要是用来统计计算idf值,通过计算每篇训练文档集中每篇文档中的过滤好的词在多少篇训练文档中出现,然后根据上述的idf计算公式计算其值,也即是训练过程,这里的所谓的训练好的模型也就是根据训练语料库计算好的过滤后词的idf值。然后针对待处理文本,遍历每个词的tf值,再结合计算好的idf值计算得到tf-idf值,排序得出值最大的前10个词作为待处理文档的关键词。

从代码里还要注意几点:

  • 待处理文本的分词采用jieba的带词性标注的分词,而训练集文档分词时采用的是不带词性标注的分词。
  • 要构建一个停用词表,因为除了能表达文档信息的实词外,还有很多无用的词会干扰词的tf/idf统计值,因此需要构建一个停用词表过滤掉所有文档里的干扰词。现在中文自然语言处理常用的一个停用词表是哈工大的停用词表,里面包含大部分中文文本常用的干扰词。当然在实际应用中,也可以根据具体的项目构建一个更适用的停用词表。除了停用词表外,也可以适用词性对文本的词进一步的筛选,在这里对于待处理的文档筛选的是只保留名词性的词语(也即是以“n”开头的词),从代码里可以看出。
  • 训练文档集中的文档在分词后过滤干扰词时,保留了所有的词性的词,而待处理文档则是带词性标注的分词后,除了过滤干扰词外,还过滤掉了除名词性的词以外的词。
  • tf-idf的训练主要是根据数据集生成对应的idf值字典,后续计算每个词的tf-idf时,直接从字典中读取。而LSI和LDA的训练是根据现有的数据集生成文档-主题分布矩阵和主题-词分布矩阵,在gensim中有实现好的训练方法。
  • 训练得到idf值,tf值是针对待处理文本的,总和计算tf-idf值进行排序得到关键词。