本文始发于个人公众号:TechFlow,原创不易,求个关注

今天的文章和大家聊聊文本分析当中的一个简单但又大名鼎鼎的算法——TF-idf。说起来这个算法是自然语言处理领域的重要算法,但是因为它太有名了,以至于虽然我不是从事NLP领域的,但在面试的时候仍然被问过好几次,可见这个算法的重要性。

好在算法本身并不困难,虽然从名字上看疑惑重重,但是一旦理解了其中的原理,一切都水到渠成,再也不怕面试的时候想不起来了。废话不多说,我们进入正题。

算法原理

TF-idf名字的中间用分隔号进行了分割,并且TF和idf都不像是人名,所以它其实是表明了这个算法是由TF和idf两个部分构成的。我们先来看TF的部分。

TF的解释

TF的英文全称是Term Frequency,Frequency很好理解就是频次、频率。而这个Term硬翻译是项的意思,联系上下文,它其实是指的文本当中的单词或者短语。所以结合起来,Term Frequency就是短语的频率。其实如果你明白了它的意思,剩下的光凭猜测都可以猜测出一个大概。

它的意思很朴素,就是字面意思,即一个单词在本文当中的重要性和它出现的频率有关。

这个观点很直观,比如我们在网页搜索”TechFlow“,出来的网站当中通篇连一个”TechFlow“都没有,显然这次搜索的质量很差。如果一个网站当中包含的”TechFlow“很多,那说明很有可能搜索正确,这个网站就是我们想要的。

除此之外,它还可以反映单词的重要程度。如果在同一个文本当中,一个Term的出现频率比另一个大,那么一般情况下,显然它的重要程度也更大。

据说早期的搜索引擎就是用的这个策略,它衡量用户搜索的关键词在各个网页文本当中出现的频率。倾向于将出现频率高的网页排在前面,由于排名靠前的网页能够获得大量的流量。所以由于利益的驱动,后来越来越多的网页倾向于在内容当中嵌入更多的搜索热词,以此来获得更高的排名和更多的流量。相信大家也都有过类似的体会,当我们使用搜索引擎输入某个关键词,搜索出来的网页号称有相关的匹配,但是当我们真正点击进去却什么也没有发现,或者是满屏的广告。

在早期的互联网当中存在大量这样的网页,它们以囊括更多的搜索热词为生。以此还衍生出了一个技术工种——SEO,即search engine optimization搜索引擎优化,专门用各种手段来替各大网页优化搜索引擎当中的排名。

很快搜索引擎的工程师也都发现了这个问题,也正是为了解决这个问题,才引入了IDF的概念。

IDF的概念

IDF的英文是Inverse Document Frequency,即逆文档频率。这个概念很难翻译,也很难直白地解释,所以往往我们还是使用它的英文缩写。它表达的意思也很简单,就是越广泛存在的Term越不重要,也就是Term的重要性和出现的广泛性成反比。

举个例子,最常用的”的“,”了“,”是的“这些单词肯定广泛出现在各个文章当中,而像是“搜索”,“机器学习”这些短语会出现的文章可能就要少得多。显然对于搜索引擎或者是一些其他模型而言,这些出现更少的单词的参考意义更大,因为往往意味着更加精准的导向。所以IDF可以简单理解成出现广泛程度的倒数,它的定义也很简单:

\[\displaystyle idf_i=\log\frac{|D|}{1 + |\{j:t_i \in d_j \}|}
\]

其中\(|D|\)是所有文档的数量,\(t_i\)是第i个短语,\(|\{j:t_i \in d_j \}|\)表示包含第i个短语的文档的数量。为了防止它为0,我们为它加上一个常数1。同样,我们也可以写出TF的公式:

\[TF(t) = \frac{TF_i}{TN_t}
\]

分母的\(TN_t\)表示文章t当中包含的所有Term的数量,分子\(TF_i\)表示\(Term_i\)在文档中的数量。

我们回顾一下这两个概念可以发现,TF衡量的是短语和文档的关系,而idf衡量的是短语和所有文档的关系。也就是说前者衡量的是短语对于某一个具体文档的重要性,而idf衡量的是短语对于所有文档的重要性。这两者有点像是局部和整体的关系,我们将两者相乘就可以得到一个Term兼容两者最终得到的重要性,也就是说TF-idf是用来计算短语在某个文档中重要性的算法

TF-idf的算法也很简单,我们直接将TF和idf计算得到的取值相乘即可。

算法的原理理解了之后,我们可以自己动手写一个计算TF-idf的算法,并不复杂,整个过程不超过40行:

class TFIdfCalculator:

    # 初始化方法
def __init__(self, text=[]):
# 自定义的文本预处理,包括停用词过滤和分词,归一化等
self.preprocessor = SimpleTextPreprocessing()
# 防止用户只传了单条文本,做兼容处理
if isinstance(text, list):
rows = self.preprocessor.preprocess(text)
else:
rows = self.preprocessor.preprocess([text]) self.count_list = []
# 使用Counter来计算词频
for row in rows:
self.count_list.append(Counter(row)) # fit接口,初始化工作
def fit(self, text):
self.__init__(text) # 计算词频,即单词出现次数除以总词数
# 用在初始化之后
def tf(self, word, count):
return count[word] / sum(count.values()) # 计算包含单词的文本数量
def num_containing(self, word):
return sum(1 for count in self.count_list if word in count) # 计算idf,即log(文档数除以出现次数+1)
def idf(self, word):
return math.log(len(self.count_list) / (1 + self.num_containing(word))) # 计算tfidf,即tf*idf
def tf_idf(self, word, count_id):
if isinstance(count_id, int) and count_id < len(self.count_list):
return self.tf(word, self.count_list[count_id]) * self.idf(word)
else:
return 0.0

其中SimpleTextPreprocessing是我自己开发的一个进行文本预处理的类,包括分词、去除停用词以及词性归一化等基本操作。这些内容在之前朴素贝叶斯分类的文章当中曾经提到过,感兴趣的同学可以点击下方的链接进行查看。

机器学习基础——朴素贝叶斯做文本分类代码实战

我们来实验一下代码:

tfidf = TFIdfCalculator()
tfidf.fit(['go until jurong', 'point craze go', 'cine there got amore', 'cine point until'])
print(tfidf.tf_idf('jurong', 0))
print(tfidf.tf_idf('go', 0))

我们自己创建了一些无意义的文本进行调用,我们计算第一条文本当中go和jurong单词的重要程度。根据TFidf的定义,go出现在了第一条和第二条文本当中,它出现的次数更多,所以它的idf更小,并且两者在第一条文本当中出现的词频一致,所以应该jurong的TFidf更大。

最后的结果也符合我们预期,jurong的TFidf是0.345,而go的TFidf是0.143。

深度思考

TFidf的原理我们都理解了,代码也写出来了,看似圆满了,但其实有一个关键的点被我们忽略了。有一点很奇怪,为什么我们计算idf的时候需要对拟文本频率这个值求log呢?虽然从结果上来看求了log之后的结果看起来更加正常,并且分布也更加合理。但这是结果不是原因,而从原理上来说,这个log出现的原因是什么呢?

其实在TFidf这个理论出现的早期,并没有人想过这个问题,可以说是误打误撞。后来有大神从香农信息论的角度给与了解释,这一切才完美的自圆其说。

在之前关于交叉熵的推导文章当中,我们曾经讨论过,如果存在一个事件A,它包含的信息量是\(-\log(P(A))\),即它发生概率的对数。也就是说发生概率越小的事件,它的信息量越大。这个log的出现是有玄机的,信息论的本质是将信息量化。信息量化的结果是bit,也就是二进制位。我们都知道一个二进制位能够表示0和1两个数字,代表了2分量的信息。随着bit的增多,我们能表示的信息量也在增大,但是信息量不是线性增长的,而是指数增长的。

举个简单又经典的例子,32支球队挺近了世界杯,这其中只有一支球队能够获胜。假设最终获胜的是法国队、西班牙队,我们知道消息的时候并不会惊讶。而如果获胜的是日本队,估计所有人会大吃一惊。这背后的原因就和信息量有关,我们都知道虽然从表面上来看32支球队是平等的,哪一支都有获胜的可能,但是实际上各个球队获胜的概率是不同的。

假设法国队、西班牙这种劲旅获胜的概率是1/4,\(-\log(\frac{1}{4})=2\)那么我们只需要2个bit就可以表示。假设日本队获胜的概率是1/128,那么我们需要7个bit才能表示,显然后者的信息量大得多。

到这里,大家也就明白了,我们取对数的本质是计算信息量对应的bit的数量。bit的数量是线性的,信息量是指数级的,也就是说我们将一个指数级的信息量转化成了线性的bit。对于大多数模型而言,线性的特征更加容易拟合,这也是TFidf效果出色的本质原因。

最后,我们从信息论的角度解释一下idf,假设互联网世界当中所有的文档有\(2^{30}\)。现在用户搜索中美贸易战,其中包含中国和美国的文档数量都是\(2^{14}\),那么中国和美国这两个词包含的信息量就是\(\log(\frac{2^{30}}{2^{14}})=16\),而如果包含贸易战这个词的文档数量只有\(2^6\),那么贸易战这个词包含的信息量就是\(\log(\frac{2^{30}}{2^6})=24\),那么显然,贸易战这个词的信息量要比中国和美国大得多,那么它在文档排序当中起到的作用也就应该更大。

如果你能从信息论的角度对TFidf的原理进行解释,而不只是简单地了解原理,那我觉得这个知识点才是真正掌握了,那么当你在面试当中遇到自然也就能游刃有余了。

今天的文章就是这些,如果觉得有所收获,请顺手扫码点个关注吧,你们的举手之劳对我来说很重要。

最新文章

  1. 随堂笔记javascript篇之chrome调试:
  2. day27_反射
  3. 【学】jQuery的源码思路6——增加each,animaion,ajax以及插件机制
  4. InfluxDB学习之InfluxDB的HTTP API写入操作
  5. iOS事件传递&amp;响应者链条
  6. JDBC的批量处理数据
  7. powershell命令大全
  8. oracle rac IP详解
  9. JavaScript实现拖拽预览,AJAX小文件上传
  10. 【Windows 8】pid为4的system进程占用80端口的解决办法
  11. SQL Server 统计信息的创建与更新
  12. WPF 自定义标题栏
  13. loadrunner 参数存储在data.ws、paralist、globals.h 中区别(参数与变量额区别于使用)
  14. MySQL拓展操作
  15. TestNG(一)
  16. HTTP Authentication
  17. 信息摘要算法之一:MD5算法解析及实现
  18. pandas获取groupby分组里最大值所在的行,获取第一个等操作
  19. Vue加载json文件
  20. Spring ConversionService 类型转换(二) ConversionService

热门文章

  1. Thinkphp中js报错,Uncaught SyntaxError: Unexpected token }
  2. Python程序在docker中运行,未找到自定义模块
  3. 通过java语言实现MD5加密
  4. day40-进程-生产者消费者模型进阶
  5. CentOS-TFTP服务搭建
  6. swagger-ui不显示问题定位
  7. 复习break、continue、while、do-while的运用
  8. java 变量分类
  9. seek for|contrary to|lag behind|take up|take advantage of|be confident of|allow for |
  10. pytorch中的view函数和max函数