本文始发于个人公众号: 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 的原理进行解释, 而不只是简单地了解原理, 那我觉得这个知识点才是真正掌握了, 那么当你在面试当中遇到自然也就能游刃有余了.
今天的文章就是这些, 如果觉得有所收获, 请顺手扫码点个关注吧, 你们的举手之劳对我来说很重要.
来源: https://www.cnblogs.com/techflow/p/12407502.html