TF-IDF算法
一、TF-IDF算法简介
TF-IDF (词频-逆文档频次) 算法包含两部分:TF算法和IDF算法。
频次:一个单词在某篇文档中出现的次数。
- TF算法
TF(Term Frequency)算法是统计一个词在一篇文档中出现的频次。
基本思想:一个词在文档中出现的次数越多,其对文档的表达能力就越强。
特点:TF仅衡量词的出现频次,而没有考虑到词对文档的区分能力。
计算:tf(word)=(word在文档中出现的次数)/(文档总词数)
tf计算时,仅用频次来表示的话,长文本中的词出现频次高的概率会更大,这一点会影响到不同文档之间关键词权值的比较。计算过程中,一般会对词频做归一化处理,即分母一般为文档总词数。分母也可以为该篇文档中词出现最多的次数,代码中会判断是sum或max。
2. IDF算法
IDF(Inverse Document Frequency)算法是统计一个词在文档集的多少个文档中出现。
基本思想:一个词在越少的文档中出现,则其对文档的区分能力也就越强。
特点:IDF强调词的区分能力,但一个词既然能够在一篇文档中频繁出现,表明这个词能够很好地表现该篇文档的特征,忽略这一点显然是不合理的。
计算:idf(word)=log{(文档集中文档总数量)/(word出现过的文档数量 + 1)}
idf计算时,分母加1是采用拉普拉斯平滑,避免有部分新的词没有在语料库中出现过而导致分母为0的情况,增强算法的健壮性。
3. TF-IDF算法
TF-IDF算法从词频、逆文档频次两个角度对词的重要性进行度量。
基本思想:TF-IDF值越大,越适合为文档的关键词。
特点:TF-IDF即考虑词的出现频次,也考虑词对文档的区分能力。
计算:tf-idf(word)= tf(word)* idf(word)
说明:1) tf和idf是相加还是相乘,idf的计算是否取对数,经过大量的理论推导和试验研究后,上述方式是较为有效的计算方式之一。
2) TF-IDF算法可以用来进行关键词提取。关键词可以根据tf-idf值由大到小排序取TopN。
二、python实现TF-IDF算法
1. 硬件系统: win10+anaconda37+pycharm
2. 数据准备
链接:https://pan.baidu.com/s/1X5FtrhhhCzlYC1-Y1jIPfQ
提取码:a9oh
随便下载2-5个txt文件即可,为了测试看数据方便,自己可以将文件中的内容删除些,方便跑代码过程研究逻辑。数据已经用空格分割处理好的数据。
3. 代码逻辑
3.1) load_data函数:读取数据,并对文档集中所有词进行编码,同时进行词频统计(默认sum);
3.2) idf函数:计算每篇文章中每个单词的idf值;
3.3) doc_tf_idf函数:计算每篇文章中每个单词的tf-idf值;
3.4) 对应某篇文档根据tf-idf值倒序取topN。
4. python实现
4.1) 变量简介
doc_list:列表,[文档名列表],文档不重复
word_encoding:字典,单层,{word词:word编码},编码不重复
word_idf:字典,单层,{ word编码:idf值}},word编码不重复
doc_word_freq_dict:字典,双层,{文档名:{word编码:word计数}},每篇文档中的每个word编码不重复
doc_word_tfidf:字典,双层,{文档名:{word编码:tfidf值}}
4.2) 代码实现
# 测试数据路径
file_path = './test_newsdata'
# word编码字典 {单词:单词编码}
word_encoding = dict()
def load_data(tf_denominator_type="sum"):
"""
加载新闻数据,并做词频统计(word count)
:return: doc_list [文档名列表]
doc_dict 每篇文档对应每个单词编码的词频
{文档名:word_freq} => {文档名:{单词编码:单词计数}}
"""
doc_list = []
doc_word_freq_dict = dict()
i = 0 # 循环初始变量
for doc_names in os.listdir(file_path):
doc_name = doc_names.split('.')[0]
doc_list.append(doc_name)
# 用i来展示当前文件读取到哪里了,类似于每读取第10整数倍的文章数时,进行打印输出(进度条的感觉)
if i % 10 == 0:
print("The {0} file had been loaded!".format(i), 'file loaded ...')
# 以可读模式逐个打开目录下的文件,并逐行读取文件中的word转化成列表,以每篇文章为单位
with open(file_path + '/' + doc_names, 'r', encoding='utf-8') as f:
# word_freq 记录每篇文章中的单词编码及出现次数 {word编码: word计数}
word_freq = dict()
words_count_sum = 0 # 文档中单词总数统计
words_count_max = 0 # 文档中单词出现次数最大值
# 每行读取并去除空格
for line in f.readlines():
words = line.strip().split(' ')
print('The line corresponds to words: {0}'.format(words))
for word in words:
# 空字符串,此处也可以跳过停用词(stop_list为停用词集合,可百度得到,亦可自行增加)
# if len(word.strip()) < 1 or word in stop_list:
if len(word.strip()) < 1:
continue
# word编码的逻辑:
# 1. 若word未曾出现在word_encoding编码表中,则刚好以word_encoding长度向上增加,从0开始;
# 2. 否则直接进行下一步词频统计
if word_encoding.get(word, -1) == -1:
word_encoding[word] = len(word_encoding)
# word_encoding_id 记录word对应的编码id
word_encoding_id = word_encoding[word]
print('doc_name: {2}, word: {0}, word_encoding_id: {1} '.format(word, word_encoding_id, doc_name))
# 统计每篇文章中的word编码所对应的词频
# 第一次出现的word编码计1次,否则计数就+1
if word_freq.get(word_encoding_id, -1) == -1:
word_freq[word_encoding_id] = 1
else:
word_freq[word_encoding_id] += 1
print('doc_name: {0}, word_freq: {1}'.format(doc_name, word_freq))
# 文档中单词出现次数的最大值
if word_freq[word_encoding_id] > words_count_max:
words_count_max = word_freq[word_encoding_id]
# 文档中单词出现次数加和
words_count_sum += 1
# 计算tf值(计算占比: 1. 分母为总单词数;2、分母为单词最大词频)
for word in word_freq.keys():
if tf_denominator_type == "sum":
word_freq[word] /= words_count_sum
if tf_denominator_type == "max":
word_freq[word] /= words_count_max
doc_word_freq_dict[doc_name] = word_freq
i += 1
return doc_word_freq_dict, doc_list
def idf(doc_word_freq_dict):
"""
idf: 统计每个单词的逆文档词频,
计算idf值(idf = log(文档集的总数量/(文档出现次数+1)))
(当新单词未出现在word编码时,拉普拉斯平滑消除异常报错)
:param: doc_word_freq_dict {文档名:{word编码:word计数}}
:return: word_idf {word编码: idf值}
"""
word_idf = {}
docs_num = len(doc_word_freq_dict) # 文档集总数量
# step1: 统计对应每个单词在文档集中出现的文档数量
for doc in doc_word_freq_dict.keys():
# print("doc: {0}, doc_dict: {1}".format(doc, doc_dict[doc]))
for word_id in doc_word_freq_dict[doc].keys():
# 统计当前文档是否出现word编码,第一次出现计1,否则+1
if word_idf.get(word_id, -1) == -1:
word_idf[word_id] = 1
else:
word_idf[word_id] += 1
# step2: 计算idf
for word_id in word_idf.keys():
word_idf[word_id] = math.log(docs_num/(word_idf[word_id] + 1))
return word_idf
def doc_tf_idf():
"""
实现tf*idf,计算每篇文章中对应每个单词的tf-idf值
:return: doc_word_freq_dict {文档名:{单词: tf-idf值}}
doc_list 文档名列表
"""
doc_word_freq_dict, doc_list = load_data()
word_idf = idf(doc_word_freq_dict)
doc_word_tfidf = dict()
for doc in doc_list:
word_tfidf = dict()
for word_id in doc_word_freq_dict[doc].keys():
word_tfidf[word_id] = doc_word_freq_dict[doc][word_id] * word_idf[word_id]
print('doc: {0}, word: {1}, tf: {2}, idf: {3}, tfidf: {4} '.format(doc,
word_id,
doc_word_freq_dict[doc][word_id],
word_idf[word_id],
word_tfidf[word_id]))
# print(word_tfidf)
doc_word_tfidf[doc] = word_tfidf
# print(len(doc_word_tfidf))
print('-----------------------------n我是优雅的分割线!n----------------------------')
print('doc_list: {0}'.format(doc_list))
print('word_encoding: {0}'.format(word_encoding))
print('word_idf: {0}'.format(word_idf))
print('doc_word_freq_dict: {0}'.format(doc_word_freq_dict))
print('doc_word_tfidf: {0}'.format(doc_word_tfidf))
return doc_word_tfidf, doc_list
doc_word_tfidf, doc_list = doc_tf_idf()
# 提取文档的关键词Top10
# sorted(doc_word_tfidf['1000business'].items(), key=operator.itemgetter(1), reverse=True)[0:10]
sorted(doc_word_tfidf['1000business'].items(), key=lambda x: x[1], reverse=True)[0:10]
三、TF-IDF优缺点及改进方向
优点:仅考虑了词的两个统计信息(频次和在多少个文档中出现),衡量词对文档的重要性。
缺点:对文本的信息利用程度较低,比如词的词性、出现的位置等信息。
改进方向:在关键词提取过程中,结合场景,可以考虑以下方面:
1) 文档中的名词,作为一种定义现实实体的词,带有更多的关键信息,可对名词赋予更高的权重;
2) 文中的起始段落和末尾段落比起其他部分的文本更重要,对出现在这些位置的词赋予更高的权重。
备注:
无监督关键词提取算法,主要包括TF-IDF算法、TextRank算法和主题模型算法(包括LSA、LSI、LDA等),以后有机会再继续介绍。