第11章贝叶斯算法项目实战——新闻分类
本章介绍机器学习中非常经典的算法——贝叶斯算法,相信大家都听说过贝叶斯这个伟大的数学家,接下来看一下贝叶斯算法究竟能解决什么问题。在分类任务中,数值特征可以直接用算法来建立模型,如果数据是文本数据该怎么办呢?本章结合贝叶斯算法通过新闻数据集的分类任务来探索其中每一步细节。
11.1贝叶斯算法
这个问题可以轻松地解决,但是,如果把这个问题反过来还那么容易吗?如果事先并不知道袋子里面黑白球的比例,而是闭着眼睛摸出一个(或好几个)球,观察这些取出来的球的颜色之后,要对袋子里面的黑白球的比例作推测。好像有一点绕,这就是逆向概率问题。接下来就由一个小例子带大家走进贝叶斯算法。
11.1.1贝叶斯公式
直接看贝叶斯公式可能有点难以理解,先通过一个实际的任务来看看贝叶斯公式的来历,假设一个学校中男生占总数的60%,女生占总数的40%。并且男生总是穿长裤,女生则一半穿长裤、一半穿裙子,接下来请听题(见图11-1)。
图11-1贝叶斯公式场景实例
下面通过计算这个小任务推导贝叶斯算法,首先,假设学校里面的总人数为U,这个时候大家可能有疑问,原始条件中,并没有告诉学校的总人数,只告诉了男生和女生的比例,没关系,可以先进行假设,一会能不能用上还不一定呢。
此时穿长裤的男生的个数为:
式中,P(Boy)为男生的概率,根据已知条件,其值为60%;P(Pants|Boy)为条件概率,即在男生这个条件下穿长裤的概率是多大,根据已知条件,所有男生都穿长裤,因此其值是100%。条件都已知,所以穿长裤的男生数量是可求的。
同理,穿长裤的女生个数为:
式中,P(girl)为女生的概率,根据已知条件,其值为40%;P(Pants|Girl)为条件概率,即在女生这个条件下穿长裤的概率是多大,根据已知条件,女生一半穿长裤、一半穿裙子,因此其值是50%,所以穿长裤的女生数量也是可求的。
下面再来分析一下要求解的问题:迎面走来一个穿长裤的学生,你只看得见他(她)穿的是长裤,而无法确定他(她)的性别,你能够推断出他(她)是女生的概率是多大吗?这个问题概括起来就是,首先是一个穿长裤的学生,这是第一个限定条件,接下来这个人还得是女生,也就是第二个条件。总结起来就是:穿长裤的人里面有多少是女生。
为了求解上述问题,首先需计算穿长裤的学生总数,应该是穿长裤的男生和穿长裤的女生总数之和:
此例类别只有两种,所以只需考虑男生和女生即可,二分类这么计算,多分类也是如此,举一反三也是必备的基本功。
要想知道穿长裤的人里面有多少女生,可以用穿长裤的女生人数占穿长裤学生总数的比例确定:
其中穿长裤总数在式(11.3)中已经确定,合并可得:
回到最开始的假设问题中,这个计算结果与总人数有关吗?观察式(11.5),可以发现分子和分母都含有总人数U,因此可以消去,说明计算结果与校园内学生的总数无关。因此,穿长裤的人里面有多少女生的结果可以由下式得到:
分母表示男生中穿长裤的人数和女生中穿长裤的人数的总和,由于原始问题中,只有男生和女生两种类别,既然已经把它们都考虑进来,再去掉总数U对结果的影响,就是穿长裤的概率,可得:
估计贝叶斯公式给大家的印象是,只要把要求解的问题调换了一下位置,就能解决实际问题,但真的有这么神奇吗?还是通过两个实际任务分析一下吧。
11.1.2拼写纠错实例
贝叶斯公式能解决哪类问题呢?下面就以一个日常生活中经常遇到的问题为例,我们打字的时候是不是经常出现拼写错误(见图11-2),但是程序依旧会返回正确拼写的字或者语句,这时候程序就会猜测:“这个用户真正想输入的单词是什么呢?”
图11-2打字时的拼写错误
例如,用户本来想输入“the”,但是由于打字错误,输成“tha”,那么程序能否猜出他到底想输入哪个单词呢?可以用下式表示:
P(猜测他想输入的单词|他实际输入的单词)(11.9)
例如,用户实际输入的单词记为D(D代表一个具体的输入,即观测数据),那么可以有很多种猜测:猜测1,P(h1|D);猜测2,P(h2|D);猜测3,P(h3|D)等。例如h1可能是the,h2可能是than,h3可能是then,到底是哪一个呢?也就是要比较它们各自的概率值大小,哪个可能性最高就是哪个。
先把上面的猜想统一为P(h|D),然后进行分析。直接求解这个公式好像难度有些大,有点无从下手,但是刚刚不是得到贝叶斯公式吗?转换一下能否好解一些呢?先来试试看:
此时该如何理解这个公式呢?实际计算中,需要分别得出分子和分母的具体数值,才能比较最终结果的大小,对于不同的猜测h1、h2、h3……,分母D的概率P(D)相同,因为都是相同的输入数据,由于只是比较最终结果的大小,而不是具体的值,所以这里可以不考虑分母,也就是最终的结果只和分子成正比的关系,化简可得:
很多机器学习算法在求解过程中都是只关心极值点位置,而与最终结果的具体数值无关,这个套路会一直使用下去。
对于给定观测数据,一个猜测出现可能性的高低取决于以下两部分。
图11-3用词云展示了一些词语,其中每个词的大小就是根据其词频大小进行设定。例如,给定的语料库中,单词一共有10000个,其中候选词h1出现500次,候选词h2出现1000次,则其先验概率分别为500/10000、1000/10000。可以看到先验概率对结果具有较大的影响。
图11-3词频统计
在贝叶斯算法中,一直强调先验的重要性,例如,连续抛硬币100次都是正面朝上,按照之前似然函数的思想,参数是由数据决定的,控制正反的参数此时就已经确定,下一次抛硬币时,就会有100%的信心认为也是正面朝上。但是,贝叶斯算法中就不能这么做,由于在先验概率中认为正反的比例1︰1是公平的,所以,在下一次抛硬币的时候,也不会得到100%的信心。
最后把它们组合在一起,就是最终的结果。例如,用户输入“tlp”(观测数据D),那他到底输入的是“top”(猜想h1)还是“tip”(猜想h2)呢?也就是:已知h1=top,h2=tip,D=tlp,求P(top|tlp)和P(tip|tlp)到底哪个概率大。经过贝叶斯公式展开可得:
这个时候,看起来都是写错了一个词,假设这种情况下,它们生成观测数据的可能性相同,即P(tlp|top)=P(tlp|tip),那么最终结果完全由P(tip)和P(top)决定,也就是之前讨论的先验概率。一般情况下,文本数据中top出现的可能性更高,所以其先验概率更大,最终的结果就是h1:top。
讲完这个例子之后,相信大家应该对贝叶斯算法有了一定的了解,其中比较突出的一项就是先验概率,这好像与之前讲过的算法有些不同,以前得到的结果完全是由数据决定其中的参数,在这里先验概率也会对结果产生决定性的影响。
11.1.3垃圾邮件分类
接下来再看一个日常生活中的实例——垃圾邮件分类问题。这里不只要跟大家说明其处理问题的算法流程,还要解释另一个关键词——朴素贝叶斯。贝叶斯究竟是怎么个朴素法呢?从实际问题出发还是很好理解的。
图11-4邮件判断
本例中用D表示收到的这封邮件,注意D并不是一个大邮件,而是由N个单词组成的一个整体。用h+表示垃圾邮件,h表示正常邮件。当收到一封邮件后,只需分别计算它是垃圾邮件和正常邮件可能性是多少即可,也就是P(h+|D)和P(h|D)。
根据贝叶斯公式可得:
P(D)同样是这封邮件,同理,既然分母都是一样的,比较分子就可以。
其中P(h)依旧是先验概率,P(h+)表示一封邮件是垃圾邮件的概率,P(h)表示一封邮件是正常邮件的概率。这两个先验概率都是很容易求出来的,只需要在一个庞大的邮件库里面计算垃圾邮件和正常邮件的比例即可。例如邮件库中包含1000封邮件,其中100封是垃圾邮件,剩下的900封是正常邮件,则P(h+)=100/1000=10%,P(h)=900/1000=90%。
P(D|h+)表示这封邮件是垃圾邮件的前提下恰好由D组成的概率,而P(D|h)表示正常邮件恰好由D组成的概率。感觉似乎与刚刚说过的拼写纠错任务差不多,但是这里需要对D再深入分析一下,因为邮件中的D并不是一个单词,而是由很多单词按顺序组成的一个整体。
D既然是一封邮件,当然是文本语言,也就有先后顺序之分。例如,其中含有N个单词d1,d2…dn,注意其中的顺序不能改变,就像我们不能倒着说话一样,因此:
式中,P(d1,d2,…,dn|h+)为在垃圾邮件当中出现的与目前这封邮件一模一样的概率是多大。这个公式涉及这么多单词,看起来有点棘手,需要对其再展开一下:
式(11.15)表示在垃圾邮件中,第一个词是d1;恰好在第一个词是d1的前提下,第二个词是d2;又恰好在第一个词是d1,第二个词是d2的前提下,第三个词是d3,以此类推。这样的问题看起来比较难以解决,因为需要考虑的实在太多,那么该如何求解呢?
这里有一个关键问题,就是需要考虑前后之间的关系,例如,对于d2,要考虑它前面有d1,正因为如此,才使得问题变得如此烦琐。为了简化起见,如果di与di1是相互独立的,就不用考虑这么多,此时d1这个词出现与否与d2没什么关系。特征之间(词和词之间)相互独立,互不影响,此时P(d2|d1,h+)=P(d2|h+)。
这个时候在原有的问题上加上一层独立的假设,就是朴素贝叶斯,其实理解起来还是很简单的,它强调了特征之间的相互独立,因此式(11.15)可以化简为:
对于式(11.16),只需统计di在垃圾邮件中出现的频率即可。统计词频很容易,但是一定要注意,词频的统计是在垃圾邮件库中,并不在所有的邮件库中。例如P(d1|h+)和P(d1|h)就要分别计算d1在垃圾邮件中的词频和在正常邮件中的词频,其值是不同的。像“销售”“培训”这样的词在垃圾邮件中的词频会很高,贝叶斯算法也是基于此进行分类任务。计算完这些概率之后,代入式(11.16)即可,通过其概率值大小,就可以判断一封邮件是否属于垃圾邮件。
11.2新闻分类任务
下面要做一个新闻分类任务,也就是根据新闻的内容来判断它属于哪一个类别,先来看一下数据:
邀月注:最初用89501条数据,python进程占满16G内存,浏览器进程崩溃,导致所有修改代码丢失,痛定思痛,决定改用1000条新闻,所以上图应为(1001,3)。
由于原始数据都是由爬虫爬下来的,所以看起来有些不整洁,需要清洗一番。这里有几个字段特征:
前5条数据都是与财经有关,我们再来看看后5条数据(见图11-5)。
图11-5财经类别新闻
11.2.1数据清洗
分词的基本原理也是机器学习算法,感兴趣的同学可以了解一下HMM隐马尔可夫模型。
在结果中可以看到将原来的一句话变成了一个list结构,里面每一个元素就是分词后的结果,这份数据规模还是比较小的,只有5000条,分词很快就可以完成(这次耗的不是CPU,是内存,约占去5-8G内存,和数据量有关)。
首先需要选择一个合适的停用词库,网上有很多现成的,但是都没有那么完整,所以,当大家进行数据清洗任务的时候,还需要自己添加一些,停用词如图11-6所示。
图11-6中只截取停用词表中的一部分,都是一些没有实际主题色彩的词,如果想把清洗的任务做得更完善,还是需要往停用词表中加入更多待过滤的词语,数据清洗干净,才能用得舒服。如果添加停用词的任务量实在太大,一个简单的办法就是基于词频进行统计,普遍情况下高频词都是停用词。
对于文本任务来说,数据清洗非常重要,因为其中每一个词都会对结果产生影响,在开始阶段,还是希望尽可能多地去掉这些停用词。
过滤掉停用词的方法很简单,只需要遍历数据集,剔除掉那些出现在停用词表中的词即可,下面看一下对比结果。
1df_content=pd.DataFrame({'content_S':content_S})#专门展示分词后的结果2df_content.head()34stopwords=pd.read_csv("stopwords.txt",index_col=False,sep="\t",quoting=3,names=['stopword'],encoding='utf-8')5stopwords.head(20)67defdrop_stopwords(contents,stopwords):8contents_clean=[]9all_words=[]10forlineincontents:11line_clean=[]12forwordinline:13ifwordinstopwords:14continue15line_clean.append(word)16all_words.append(str(word))17contents_clean.append(line_clean)18returncontents_clean,all_words1920contents=df_content.content_S.values.tolist()21stopwords=stopwords.stopword.values.tolist()22contents_clean,all_words=drop_stopwords(contents,stopwords)2324#df_content.content_S.isin(stopwords.stopword)25#df_content=df_content[~df_content.content_S.isin(stopwords.stopword)]26#df_content.head()2728df_content=pd.DataFrame({'contents_clean':contents_clean})29df_content.head()30#8万9千条新闻,运行了半小时,后改为1000条新闻,邀月注
显然,这份停用词表做得并不十分完善,但是可以基本完成清洗的任务,大家可以酌情完善这份词表,根据实际数据情况,可以选择停用词的指定方法。
中间来一个小插曲,在文本分析中,现在经常会看到各种各样的词云,用起来还是比较有意思的。在Python中可以用wordcloud工具包来做,可以先参考其github文档。
1pipinstallwordcloud
1fromwordcloudimportWordCloud2importmatplotlib.pyplotasplt3%matplotlibinline4importmatplotlib5matplotlib.rcParams['figure.figsize']=(10.0,5.0)67wordcloud=WordCloud(font_path="./data/simhei.ttf",background_color="white",max_font_size=80)8word_frequence={x[0]:x[1]forxinwords_count.head(100).values}9wordcloud=wordcloud.fit_words(word_frequence)10plt.imshow(wordcloud)
11.2.2TF-IDF关键词提取
其中:
关键词结果:伯南克美国失业率经济财政
词袋模型是自然语言处理中最基础的一种特征提取方法,直白地说,它就是看每一个词出现几次,统计词频即可,再把所有出现的词组成特征的名字,依次统计其个数就能够得到文本特征。感觉有点过于简单,只考虑词频,而不考虑词出现的位置以及先后顺序,能不能稍微改进一些呢?还可以通过设置ngram_range来控制特征的复杂度,例如,不仅可以考虑单单一个词,还可以考虑两个词连在一起,甚至更多的词连在一起的组合。
1fromsklearn.feature_extraction.textimportCountVectorizer23vec=CountVectorizer(analyzer='word',max_features=4000,lowercase=False)4feature=vec.fit_transform(words)1feature.shape2#(750,4000)在构建过程中,还额外加入了一个限制条件max_features=4000,表示得到的特征最大长度为4000,这就会自动过滤掉一些词频较小的词语。如果不进行限制,大家也可以去掉这个参数观察,会使得特征长度过大,最终得到的向量长度为85093,而且里面很多都是词频很低的词语,导致特征过于稀疏,这些对建模来说都是不利的,所以,还是非常有必要加上这样一个限制参数,特征确定之后,剩下的任务就交给贝叶斯模型吧:
1fromsklearn.naive_bayesimportMultinomialNB#贝叶斯模型2classifier=MultinomialNB()3classifier.fit(feature,y_train)45test_words=[]6forline_indexinrange(len(x_test)):7try:8#9test_words.append(''.join(x_test[line_index]))10except:11print(line_index,word_index)12test_words[0]1314classifier.score(vec.transform(test_words),y_test)
结果是0.936
贝叶斯模型中导入了MultinomialNB模块,还额外做了一些平滑处理,主要目的是在求解先验概率和条件概率的时候避免其值为0。词袋模型的效果看起来还凑合,能不能改进一些呢?在这份特征中,公平地对待每一个词,也就是看这个词出现的个数,而不管它重要与否,但看起来还是有点问题。因为对于不同主题来说,有些词可能更重要,有些词就没有什么太大价值。还记得老朋友TF-IDF吧,能不能将其应用在特征之中呢?当然是可以的,下面通过一个小例子来看一下吧:
TfidfVectorizer()函数中可以加入很多参数来控制特征(见图11-9),比如过滤停用词,最大特征个数、词频最大、最小比例限制等,这些都会对结果产生不同的影响,建议大家使用的时候,还是先参考其API文档,价值还是蛮大的,并且还有示例代码。
最后还是用同样的模型对比一下两种特征提取方法的结果差异:(结果是0.916)
项目小结:
本章首先讲解了贝叶斯算法,通过两个小例子,拼写纠错和垃圾邮件分类任务概述了贝叶斯算法求解实际问题的流程。以新闻文本数据集为例,从分词、数据清洗以及特征提取开始一步步完成文本分类任务。建议大家在学习过程中先弄清楚每一步的流程和目的,然后再完成核心代码操作,机器学习的难点不只在建模中,数据清洗和预处理依旧是一个难题,尤其是在自然语言处理中。