机器学习基础——详解自然语言处理之tf-idf

本文始发于个人公众号: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

时间: 2024-10-04 23:15:01

机器学习基础——详解自然语言处理之tf-idf的相关文章

Swift学习——Swift基础详解(四)

A:小儿编程很不好! B:多半是不爱学,从看英文版开始,让你爱上编程! Type Aliases    类型重定义(typedef) Swift中重定义类型的关键字是typealias,至于怎么用,应该不必多说了,看例子: typealias AudioSample = UInt16 //定义了一个类型名称AudioSample,代表UInt16类型 var maxAmplitudeFound = AudioSample.min // maxAmplitudeFound is now 0 Boo

Swift学习——Swift基础详解(一)

注:由于基础部分在Swift Tour 中已经大体的说明了,所以在详解中不会达到100%的原文释义 Constants and Variables  常量和变量 常量和变量都需要声明名称和类型(作为程序员,这些基础也就不说了),常量一次赋值不能改变,变量的值可以改变 Declaring Constants and Variables   声明常量和变量 常量和变量在使用之前必须要声明,使用let关键字定义常量,var关键字定义变量 下面的例子可以用来定义用户登录的时候最大的尝试次数: let m

Swift学习——Swift基础详解(二)

上节说了没有营养的变量和常量,这玩意,都差不多,自己稍微看下就好了 Integers    整型 整数就是整数了,没有小数,整数有符号(+,-,0)或者无符号(0,+) Swift提供了8,16,32,64位的有符号和无符号的整数,命名使用C的方式,比如,8位无符号的整型UInt8,32位有符号的整型就是Int32 Integer Bounds    整型范围 可以使用min 和 max获取整数类型的最大值和最小值 let minValue = UInt8.min // minValue is

HAProxy:基础详解

一.简介 HAProxy提供高可用性.负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机,它是免费.快速并且可靠的一种解决方案.HAProxy特别适用于那些负载特大的web站点,这些站点通常又需要会话保持或七层处理.HAProxy运行在时下的硬件上,完全可以支持数以万计的并发连接.并且它的运行模式使得它可以很简单安全的整合进您当前的架构中, 同时可以保护你的web服务器不被暴露到网络上. HAProxy实现了一种事件驱动.单一进程模型,此模型支持非常大的并发连接数.多进程或多线程模型受内存

Swift学习——Swift基础详解(三)

小葵花课堂继续开讲 Numeric Literals    数字文本 数字文本有以下几种写法: A decimal number, with no prefix A binary number, with a 0b prefix An octal number, with a 0o prefix A hexadecimal number, with a 0x prefix 十进制数,无前缀:二进制数,0b前缀:八进制数,0o前缀:十六进制数,0x前缀 论数字17的N种表现形式: let deci

Swift学习——Swift基础详解(八)

Assertions    断言 可选可以让你判断值是否存在,你可以在代码中优雅地处理值缺失的情况.然而,在某些情况下,如果值缺失或者值并不满足特定的条件,你的代码可能并不需要继续执行.这时,你可以在你的代码中触发一个断言(assertion)来结束代码运行并通过调试来找到值缺失的原因. Debugging with Assertions    使用断言进行调试 断言会在运行时判断一个逻辑条件是否为true.从字面意思来说,断言"断言"一个条件是否为真.你可以使用断言来保证在运行其他代

Swift学习——Swift基础详解(六)

Optionals    选配 选配(好像不是这么翻译的)适用于那些值可能为空的情况,一个选配有两种情况:存在值并且等于x,或者值不存在. 选配的概念在OC和C里面并没有,在OC中最接近的概念就是:OC中的返回值为对象的函数,当对象不存在的时候,可以返回nil,但是nil只能代表对象,不可以代表基础类型或者结构体和枚举类型.OC中使用NSNotFound表示值不存在.在Swift中,不需要使用其他的类型表示不存在的值. 举个例子: 在Swift中,String类型有一个方法叫做toInt,但是并

Android BroadcastReceiver基础详解一

-.BroadcastReceivcer概述 1.什么是广播 BroadcastReceiver是Android四大组件之一,本质是一种全局的监听器,用于监听系统全局的广播消息.因此它可以非常方便的实现不同组件之间的通信. 2.BroadcastReceiver的创建启动 BroadcastReceiver是用用于接受程序所放出的Broadcast Intent,与应用程序启动的Activity.Service相同.也只需要两步: ①.创建需要启动的Broadcast的Intent ②.创建一个

haproxy 基础详解 及 动静分离的实现

haproxy 介绍 1 工作在ISO 七层 根据http协议(或者工作在ISO四层 根据tcp协议) 提供web服务的负载均衡调度器 负载均衡调度器分类 工作在四层: # lvs 工作在七层: # nginx (web,http reverse proxy,cache) # haproxy (http reverse proxy,tcp proxy) # tcp: 实现MySQL的读写中读的负载均衡 # ats (apache traffic server) # perlbal # pound