我们对文档分析的时候,通常需要提取关键词,中文分词可以使用jieba分词,英文通过空格和特殊字符分割即可。那么分割之后是不是出现频率越高这些词就能越好代表这篇文章描述的内容呢?答案是否定的,比如英文中常见的词a、an等,中文中常见的“的”、“你”等等。有一些词可以通过过滤stop Word词表去掉,但是对于领域文档分析就会遇到更复杂的情况,比如需要把100份文档分到不同的领域,提取每个领域的关键词;或者需要提取每个文档具有代表性的描述关键词之后,进行文档归类。这个时候仅仅统计单词出现的频率就无法区别文档描述的内容了,这个时候我们还需要考虑这些词是否在其他文档里出现的频率,这样才能保证这些词具有本文档有代表性的同时能够区分其他文档,即TF-IDF的思想。
- TF-IDF基本介绍
TF-IDF (全称:Term Frequency - Inverse Document Frequency)在文本挖掘及信息检索中非常常用的加权统计方法。通常用来评估单词或者单字对于一份文档在整个文档集或者语料库中的重要程度。字词的重要程度与单词在文档中出现的频率成正比,与单词在其他文档中出现的频率成反比。
当一个单词在当前这篇文章里出现的频率比较高,但是在其他文章中出现的频率比较低,则我们认为这个单词具有很强的区分能力,比较适合做分类。
公式为:TF-IDF = TF * IDF
(1)TF(Term Frequency)计算方法
假设在文档d中,总得单词数为size(d),单词w出现的次数为count(w, d),则单词w在d中的频率为:
tf(w, d) = count(w, d) / size(d)
这里为什么要对出现的次数除去文档总的单词数呢,这里其实是在做归一化,是为了保证数据结果相对于其他文档的公平性,比如长文档里统计的某个单词的出现次数很大概率会比短文档高,但是其并不一定能代表文档。
(2)IDF (Inverse Document Frequency)计算方法
假设在文档集D总文档数为size(D),而w在文档集的count(w, D)个文档出现次数,则单词逆向文件频率为:
idf(w, D) = log(size(D) / count(w, D))
这里大家可能会考虑count(w, D)作为除数会不会为0的情况,写程序实现的时候要注意的。出现在文档里的词才会去算这个单词的TF-IDF,那么这个单词至少出现在文档集的其中一个文档中,所以至少为1。
(3)一个Query与文档的匹配度计算方法
TF-IDF最常用的场景就是在搜索引擎中进行匹配候选文档列表。那么用户一般输入的是一个查询(Query),其中包含多个单词,经过查询解析系统(Query Parser),会产生多个关键词(Search Teram),中文用结巴,英文用空格即可。之后我们要算哪些文档与这些关键词更匹配,之后根据匹配度进行排序(Ranking system),再到用户体验系统(User Experience)展现页面。简单的搜索引擎流程就是上面的逻辑,当然很多细节我们避开不谈,我们看看计算匹配度用TF-IDF怎么得到。
假设一个查询q中n个单词分别为w[1]、w[2]、...、w[n],其中w[i]在文档d中相对于文档集D的TF-IDF为tf-idf(w[i], d),则这个查询在文档d中的TF-IDF为:
tf-idf(q, d) = sum { tf-idf(w[i], d) | i=1..n } = sum { tf(w[i], d) * idf(w[i], D) | i=1..n }
通常在计算Query的权重之前,我们倾向于提前把文档集中所有关键词的TF-IDF都计算出来并保存。之后根据Query获取文档计算时,可以直接计算Query的TF-IDF,而不用每次都计算每个词的TF-IDF。
- Python 简单例子
让我看下用python如何计算文档的TF-IDF的例子。下面实现了对于raw text文档计算TF-IDF之后,然后可以实现根据关键字搜索top n相关文档的例子,简单模拟搜索引擎的小功能。(当然搜索引擎的架构还是比较复杂的,找个时间可以聊一聊。现在很多聊天工具基本也都是:语音识别+语音分析+搜索引擎,可见搜索是未来的一个趋势,也是简化我们生活的方式。5年前做搜索引擎的时候,大家就在讨论未来的搜索引擎是什么,大家更多考虑基于语音的搜索,Google、Apple和微软领先推出了语音助手搜索工具:Google Assistant、Siri、Cortana,紧接着国内就疯狂的刷语音助手。现在搜索引擎已经从传统的网页搜索引擎转换为了语音助手。)
例子比较简单,大家看代码注释就行了,不过多的讲解了。用高级语言写程序的原则:能用但中函数名和变量名表达清楚的,就少加注释。
#!/usr/bin/python
# encoding: UTF-8
import re
import os
import sys
from sklearn import feature_extraction
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
class DocumentMg:
def __init__(self, orig_file_dir, seg_file_dir, tfidf_file_dir):
print('Document manager is initializing...')
self.orig_file_dir = orig_file_dir
self.seg_file_dir = seg_file_dir
self.tfidf_file_dir = tfidf_file_dir
if not os.path.exists(self.seg_file_dir):
os.mkdir(self.seg_file_dir)
if not os.path.exists(tfidf_file_dir):
os.mkdir(tfidf_file_dir)
# get all file in the directory
def get_file_list(self, dir):
file_list = []
files = os.listdir(dir)
for f in files:
if f[0] == '.' or f[0] == '..':
pass
else:
file_list.append(f)
return file_list
# keep English, digital and ' '
def clean_eng_data(self, data):
comp = re.compile("[^ ^a-z^A-Z^0-9]")
return comp.sub('', data)
# keep Chinese, English and digital and '.'
def clean_chinese_data(self, data):
comp = re.compile("[^\u4e00-\u9fa5^.^a-z^A-Z^0-9]")
return comp.sub('', data)
# split file content as word list
def split_to_word(self, file_name):
fi = open(self.orig_file_dir + '/' + file_name, 'r+', encoding='UTF-8')
data = fi.read()
fi.close()
clean_data = self.clean_eng_data(data)
seg_list = clean_data.split(' ')
result = []
for seg in seg_list:
if (seg != ' ' and seg != '\n' and seg != '\r\n'):
result.append(seg)
fo = open(self.seg_file_dir + '/' + file_name, 'w+')
fo.write(' '.join(result))
fo.close()
# compute tfidf and save to file
def compute_tfidf(self):
all_file_list = self.get_file_list(self.orig_file_dir)
for f in all_file_list:
self.split_to_word(f)
corpus = []
for fname in all_file_list:
file_path = self.seg_file_dir + '/' + fname
f = open(file_path, 'r+')
content = f.read()
f.close()
corpus.append(content)
vectorizer = CountVectorizer()
transformer = TfidfTransformer()
tfidf = transformer.fit_transform(vectorizer.fit_transform(corpus))
word = vectorizer.get_feature_names()
weight = tfidf.toarray()
for i in range(len(weight)):
tfidf_file = self.tfidf_file_dir + '/' + all_file_list[i]
print (u'-------------Writing tf-idf result in the file ', tfidf_file, '----------------')
f = open(tfidf_file, 'w+')
for j in range(len(word)):
f.write(word[j] + " " + str(weight[i][j]) + '\n')
f.close()
# init search engine
def init_search_engine(self):
self.global_weight = {}
all_file_list = self.get_file_list(self.tfidf_file_dir)
# if file not exist, we need regenerate. It will work on first time
if len(all_file_list) == 0:
doc_mg.compute_tfidf()
all_file_list = self.get_file_list(self.tfidf_file_dir)
for fname in all_file_list:
file_path = self.tfidf_file_dir + '/' + fname
f = open(file_path, 'r+')
line = f.readline()
file_weight = {}
while line:
parts = line.split(' ')
if len(parts) != 2:
continue
file_weight[parts[0]] = float(parts[1])
line = f.readline()
self.global_weight[fname] = file_weight
f.close()
# preprocess query
def preprocess_query(self, query):
clean_data = self.clean_eng_data(query)
seg_list = clean_data.split(' ')
result = []
for seg in seg_list:
if (seg != ' ' and seg != '\n' and seg != '\r\n'):
result.append(seg)
return result
# search releated top n file, you can try to use min-heap to implement it.
# but here we will use limited insertion
def search_related_files(self, query, top_num):
keywords = self.preprocess_query(query)
top_docs = []
for fname in self.global_weight:
# calculate document weight
fweight = 0
for word in keywords:
if word in self.global_weight[fname].keys():
fweight += self.global_weight[fname][word]
# instert document weight
idx = 0
for idx in range(len(top_docs)):
if fweight < top_docs[idx]['fweight']:
break
top_docs.insert(idx, {'fname': fname, 'fweight': fweight})
# remove exceed document weight
if len(top_docs) > top_num:
top_docs.remove(top_docs[0])
return top_docs
if __name__ == '__main__':
if len(sys.argv) < 2:
print('Usage: python tf-idf.py ')
sys.exit()
orig_file_dir = sys.argv[1] # the folder to store your plain text files
seg_file_dir = './tfidf_data/seg_file' # the folder to store split text files
tfidf_file_dir = './tfidf_data/tfidf_file' # the folder to store tfidf result
# initialize document manager
doc_mg = DocumentMg(orig_file_dir, seg_file_dir, tfidf_file_dir)
# initialzie search engine
doc_mg.init_search_engine()
query = 'understand cable bar'
# search query and get top documents with weight
doc_list = doc_mg.search_related_files(query, 2)
print('query is: ', query)
print('result is: ')
print(doc_list)