最近学习使用了传统的自然语言处理技术进行关键词的提取,接下来我介绍一下两种常用的算法:TFIDF和TextRank。目前BiLSTM也可以用于提取文本关键词,有空再学。
TF-IDF(term frequency-inverse document frequency)是一种用于信息检索与数据挖掘的常用加权技术。TF-IDF是一种统计方法,用来评估一个字词对于一个文件集或语料库中的一份文件的重要程度。
首先解释一下TF-IDF的意思:
TF(term frequency):词语在一篇文章中出现的频率
IDF(inverse document frequency):反文档频率,与词语在其他文档中出现的频率负相关
TF-IDF的主要思想是:如果某个词或短语在一篇文章中出现的频率高,即TF值高;并且在其他文章中很少出现,即IDF值高,那么认为这个词或短语具有很好的类别区分能力,适合作为该文章的关键词。
TF-IDF的具体计算公式为:
文档中词的tfidf值越高,便认为该词越可以代表该文档的主题。TF-IDF算法的python实现如下,同时jieba库中也实现了TF-IDF,有兴趣的话也可以去了解一下。
# TF-IDf算法python实现
import re
import math
# 获取一个文档中每个词的TF值,doc参数保存文档中的句子列表,返回单词与其tf值的字典
# 首先对文档中的单词进行切分,然后统计每个词的词频
def GetWordTF(doc):
words_count = 0 # 单词总数
words_map = {} # 单词与单词数的映射
tf_map = {} # tf值映射词典,格式: tf_map[word] = tf_word
for sentence in doc: # 遍历文档中的每个句子
# 单词的切分方式可以根据所给的数据格式进行修改
# 我将提取英文句子中的每个单词,使用正则表达式提取并去除空字符串
words_arr = [word for word in re.split(r'\W+',sentence) if word]
words_count += len(words_arr) # 统计有效词的总长度
for word in words_arr: # 遍历每一个词并进行统计单词数
words_map[word] = words_map.get(word,0) + 1
for key,val in words_map.items(): # 计算每个单词的tf值
tf_map[key] = val / words_count
return tf_map
# 获取文档每个单词在文档集docSet中的IDF值映射
def GetWordIDF(tfMap,docSet):
docs_num = len(docSet) # 文档集中文档的总数
word_doc_num = {} # 包含word的文档数,格式为word_doc_num[word] = num of doc that contains word
idf_map = {} # idf值映射字典,格式idf_map[word] = idf_word
for key,val in tfMap.items(): # 遍历文档中出现的单词
for doc in docSet: # 遍历每个文档,检查该文档中是否出现了单词key
for sentence in doc: # 遍历文档中的每个句子
words_arr = [word for word in re.split(r'\W+', sentence) if word] # 提取句子中的每个单词
if key in words_arr: # 如果该文档中有该词,则统计
word_doc_num[key] = word_doc_num.get(key,0) + 1
break
for key,val in word_doc_num.items(): # 计算每个单词的idf值
idf_map[key] = math.log(docs_num / val)
return idf_map
# 使用TFIDF算法获取文档的前topNum个关键词,其中每个文档是以列表表示的,列表项为文档的一个句子
def GetKeywordsByTFIDF(entityDescriptionList,docSet,topNum):
tf_map = GetWordTF(entityDescriptionList) # 获取每个单词的tf值
idf_map = GetWordIDF(tf_map,docSet) # 获取每个单词的idf值
tfidf_map = {}
for key,val in tf_map.items(): # 计算每个词的tfidf值
tfidf_map[key] = tf_map[key] * idf_map[key]
tfidf_sorted_list = sorted(tfidf_map.items(),key = lambda x:x[1],reverse=True) # 将字典按值从大到小排序
if topNum > len(tfidf_sorted_list): # 保证topNum不大于文档中词的总数
topNum = len(tfidf_sorted_list)
keywords = [] # 保存文档的前topNum个关键字
for i in range(topNum):
keywords.append(tfidf_sorted_list[i][0]) # 关键字保存在元组的第0个元素中
return keywords
TF-IDF算法对于有多段文本的关键词提取非常有效,但是对于单篇或文档集较少的文本则表现得不很好。对于单篇文档,可以使用TextRank算法实现关键词提取。
TextRank是一种基于图排序的算法,思想源于谷歌的PageRank算法,通过把文本分割为若干组成单元(单词、句子)并建立图模型,利用投票机制对文本中的重要成分进行排序,仅利用单篇文档本身的信息即可实现关键词提取。
TextRank利用投票的原理,让每一个单词给它的邻居投赞成票,票的权重取决于自己的票数。假设每一个词是一个顶点(Vertex),那么所有的词就构成了一个网络,这个网络里面每个顶点会有指向其他顶点的边,也会有其他顶点指向自己的边。通过计算每个顶点所连接的指向自己的顶点的权重和,最终得到该顶点的权重值。
TextRank存在的主要问题是初始值的确定,为了后续计算的简便性,这里会给初值赋为一个非0值。同时,引入了一个阻尼系数的概念,该参数表示从某一个指定的顶点,到任意一个其他顶点的概率。TextRank的具体公式如下:
于是,使用TextRank算法提取关键词时,首先需要把图构建出来。图的节点就是单词,至于边可以利用n-gram的思路,认为某个单词只与它附近的n个单词有关,即与它附近的n个词对应的节点连一条无向边。也可以做一些其他操作,比如把某类词性的词删掉,一些自定义词删掉,只保留一部分单词等。我的代码实现中,假设每个长为k的滑动窗口中的任意两个单词对应的节点之间存在一条无向无权边。当构图成功后,就可以使用上述公式进行迭代求解了。Python实现的代码如下:
# 使用TextRank算法实现关键词提取,返回关键词列表,参数含义如下:
# sentence 保存待提取关键字的句子
# windowLength 保存滑动窗口的大小
# topNum 表示需要返回排名前topNum的关键词
# d 表示textrank算法的阻尼系数,默认为0.85
# maxIter 表示算法最大迭代次数
# minDiff 迭代后变化值小于minDiff时也停止迭代
def GetKeywordsByTextRank(sentence,windowLength,topNum=3,d=0.85,maxIter=10000,minDiff=0.0001):
# 单词的切分方式可以根据所给的数据格式进行修改
# 我将提取英文句子中的每个单词,使用正则表达式提取并去除空字符串
words_arr = [word for word in re.split(r'\W+', sentence) if word]
words_num = len(words_arr) # 句子的长度
word_graph = {} # 保存每个单词的连接状态,格式为word_graph[word] = [与该词存在边的单词的集合]
textrank_map = {} # 保存每个textrank值的字典,格式为textrank_map[word] = textrank value of the word
textrank_map_t = {} # 用于保存前一次迭代的tankrank结果
for words_index in range(words_num): # 遍历句子中的每个单词,开始根据给定的窗口值构图
textrank_map[words_arr[words_index]] = 1 - d # 为每个词初始化一个textrank值
window_lower = max(0, words_index - windowLength) # 滑动窗口的下边界
window_upper = min(words_num, words_index + windowLength) # 滑动窗口的上边界
for window_index in range(window_lower,window_upper): # 遍历窗口中的单词,构建单词的连接关系
if window_index == words_index: # 自己与自己认为没有边
continue
if not words_arr[window_index] in word_graph.get(words_arr[words_index],[]): # 检查两词节点之间是否有边
if word_graph.get(words_arr[words_index],0) == 0: # 检查该词的边集是否为空
word_graph[words_arr[words_index]] = [words_arr[window_index]] # 为空则生成包含该点的边集
else:
word_graph[words_arr[words_index]].append(words_arr[window_index]) # 将该边添加到边集中
for iter_i in range(maxIter): # 利用textrank计算公式迭代计算
max_diff = 0 # 表示迭代前后两次的变化
for word,neibor_list in word_graph.items(): # 遍历每个单词
for con_word in neibor_list: # 遍历与每个单词存在相邻关系的单词
con_word_out_len = len(word_graph[con_word]) # 计算当前节点连接的节点个数
if word == con_word or con_word_out_len == 0:
continue # 如果是该节点本身或无连出节点则不更新
# 使用公式对textrank值进行更新
textrank_map[word] = 1 - d + d * textrank_map_t.get(con_word, 0) /con_word_out_len
max_diff = max(max_diff,abs(textrank_map[word]-textrank_map_t.get(word,0)))
for word,val in textrank_map.items():
textrank_map_t[word] = val
if(max_diff < minDiff): # 各个单词节点的textrank值如果均无明显变化,则可结束迭代
break
textrank_sorted_list = sorted(textrank_map.items(),key=lambda x:x[1],reverse=True) # 按照textrank值从大到小排序
if topNum > len(textrank_sorted_list): # 保证topNum不大于文档中词的总数
topNum = len(textrank_sorted_list)
if topNum < 1: # 保证topNum大于0
topNum = 1
keywords = [] # 保存将要返回的关键词
for i in range(topNum):
keywords.append(textrank_sorted_list[i][0])
return keywords
可以看出TextRank算法对于一段文本中多次出现的词,会赋予更大的权重,因为它连出的节点更多,所以当各个节点初始权重一致时,则最终出现次数最多的词权重就会更大。这也会使该算法对类似于“的”、“你、我、他”等常用词,会出现比较大的误差。对于这种情况,可以在最开始构建边时进行处理,去掉一些停用词或者选择自己需要的词性的词,从而得出实际有用的词语。
后记:前端暂时不支持Latex,公式我只能贴图了。深度学习最近比较流行,还有很多需要学的呀!