《Python数据分析与机器学习实战-唐宇迪》读书笔记第11章--贝叶斯算法项目实战 ——新闻分类
第11章贝叶斯算法项目实战——新闻分类
本章介绍机器学习中非常经典的算法——贝叶斯算法,相信大家都听说过贝叶斯这个伟大的数学家,接下来看一下贝叶斯算法究竟能解决什么问题。在分类任务中,数值特征可以直接用算法来建立模型,如果数据是文本数据该怎么办呢?本章结合贝叶斯算法通过新闻数据集的分类任务来探索其中每一步细节。
11.1贝叶斯算法
贝叶斯(Thomas Bayes,1701—1761年),英国数学家。所谓的贝叶斯定理源于他生前为解决一个“逆概”问题而写的一篇文章。先通过一个小例子来理解一下什么是正向和逆向概率。假设你的口袋里面有N个白球、M个黑球,你伸手进去随便拿一个球,问拿出黑球的概率是多大?
这个问题可以轻松地解决,但是,如果把这个问题反过来还那么容易吗?如果事先并不知道袋子里面黑白球的比例,而是闭着眼睛摸出一个(或好几个)球,观察这些取出来的球的颜色之后,要对袋子里面的黑白球的比例作推测。好像有一点绕,这就是逆向概率问题。接下来就由一个小例子带大家走进贝叶斯算法。
11.1.1贝叶斯公式
直接看贝叶斯公式可能有点难以理解,先通过一个实际的任务来看看贝叶斯公式的来历,假设一个学校中男生占总数的60%,女生占总数的40%。并且男生总是穿长裤,女生则一半穿长裤、一半穿裙子,接下来请听题(见图11-1)。
图11-1 贝叶斯公式场景实例
- 1.正向概率。随机选取一个学生,他(她)穿长裤和穿裙子的概率是多大?这就简单了,题目中已经告诉大家男生和女生对于穿着的概率。
- 2.逆向概率。迎面走来一个穿长裤的学生,你只看得见他(她)穿的是否是长裤,而无法确定他(她)的性别,你能够推断出他(她)是女生的概率有多大吗?这个问题似乎有点难度,好像没办法直接计算出来,但是是否可以间接求解呢?来试一试吧。
下面通过计算这个小任务推导贝叶斯算法,首先,假设学校里面的总人数为U,这个时候大家可能有疑问,原始条件中,并没有告诉学校的总人数,只告诉了男生和女生的比例,没关系,可以先进行假设,一会能不能用上还不一定呢。
此时穿长裤的男生的个数为:
式中,P(Boy)为男生的概率,根据已知条件,其值为60%;P(Pants|Boy)为条件概率,即在男生这个条件下穿长裤的概率是多大,根据已知条件,所有男生都穿长裤,因此其值是100%。条件都已知,所以穿长裤的男生数量是可求的。
同理,穿长裤的女生个数为:
式中,P(girl)为女生的概率,根据已知条件,其值为40%;P(Pants|Girl)为条件概率,即在女生这个条件下穿长裤的概率是多大,根据已知条件,女生一半穿长裤、一半穿裙子,因此其值是50%,所以穿长裤的女生数量也是可求的。
下面再来分析一下要求解的问题:迎面走来一个穿长裤的学生,你只看得见他(她)穿的是长裤,而无法确定他(她)的性别,你能够推断出他(她)是女生的概率是多大吗?这个问题概括起来就是,首先是一个穿长裤的学生,这是第一个限定条件,接下来这个人还得是女生,也就是第二个条件。总结起来就是:穿长裤的人里面有多少是女生。
为了求解上述问题,首先需计算穿长裤的学生总数,应该是穿长裤的男生和穿长裤的女生总数之和:
此例类别只有两种,所以只需考虑男生和女生即可,二分类这么计算,多分类也是如此,举一反三也是必备的基本功。
要想知道穿长裤的人里面有多少女生,可以用穿长裤的女生人数占穿长裤学生总数的比例确定:
其中穿长裤总数在式(11.3)中已经确定,合并可得:
回到最开始的假设问题中,这个计算结果与总人数有关吗?观察式(11.5),可以发现分子和分母都含有总人数U,因此可以消去,说明计算结果与校园内学生的总数无关。因此,穿长裤的人里面有多少女生的结果可以由下式得到:
分母表示男生中穿长裤的人数和女生中穿长裤的人数的总和,由于原始问题中,只有男生和女生两种类别,既然已经把它们都考虑进来,再去掉总数U对结果的影响,就是穿长裤的概率,可得:
现在这个问题似乎解决了,不需要计算具体的结果,只需观察公式的表达即可,上面的例子中可以把穿长裤用A表示,女生用B表示。这就得到贝叶斯公式的推导过程,最终公式可以概括为:
估计贝叶斯公式给大家的印象是,只要把要求解的问题调换了一下位置,就能解决实际问题,但真的有这么神奇吗?还是通过两个实际任务分析一下吧。
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)相同,因为都是相同的输入数据,由于只是比较最终结果的大小,而不是具体的值,所以这里可以不考虑分母,也就是最终的结果只和分子成正比的关系,化简可得:
很多机器学习算法在求解过程中都是只关心极值点位置,而与最终结果的具体数值无关,这个套路会一直使用下去。
对于给定观测数据,一个猜测出现可能性的高低取决于以下两部分。
- P(h):表示先验概率,它的大小可以认为是事先已经计算好了的,比如有一个非常大的语料库,里面都是各种文章、新闻等,可以基于海量的文本进行词频统计。
图11-3用词云展示了一些词语,其中每个词的大小就是根据其词频大小进行设定。例如,给定的语料库中,单词一共有10000个,其中候选词h1出现500次,候选词h2出现1000次,则其先验概率分别为500/10000、1000/10000。可以看到先验概率对结果具有较大的影响。
图11-3 词频统计
在贝叶斯算法中,一直强调先验的重要性,例如,连续抛硬币100次都是正面朝上,按照之前似然函数的思想,参数是由数据决定的,控制正反的参数此时就已经确定,下一次抛硬币时,就会有100%的信心认为也是正面朝上。但是,贝叶斯算法中就不能这么做,由于在先验概率中认为正反的比例1︰1是公平的,所以,在下一次抛硬币的时候,也不会得到100%的信心。
- P(D|h):表示这个猜测生成观测数据的可能性大小,听起来有点抽象,还是举一个例子。例如猜想的这个词h需要通过几次增删改查能得到观测结果D,这里可以认为通过一次操作的概率值要高于两次,毕竟你写错一个字母的可能性高一些,一次写错两个就是不可能的。
最后把它们组合在一起,就是最终的结果。例如,用户输入“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)。
图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与di −1是相互独立的,就不用考虑这么多,此时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新闻分类任务
下面要做一个新闻分类任务,也就是根据新闻的内容来判断它属于哪一个类别,先来看一下数据:
1 import pandas as pd 2 import jieba 3 #pip install jieba 4 5 df_news = pd.read_table('./data/SohusiteData.txt',names=['category','theme','content'],encoding='utf-8',delimiter='^') 6 df_news = df_news.dropna() 7 df_news.tail() 8 9 # df_news_small=df_news.head(1000) 10 11 # w_filenameCSV = './data/data.txt' 12 # # write to files 13 # with open(w_filenameCSV,'w',newline='',encoding='utf-8') as write_csv: 14 # write_csv.write(df_news_small.to_csv(sep='^', index=False,encoding='utf-8')) 15 16 17 # df_news_small = pd.read_csv('./data/data.txt',names=['category','theme','content'],encoding='utf-8',delimiter='^') 18 # df_news_small.head(5)
邀月注:最初用89501条数据,python进程占满16G内存,浏览器进程崩溃,导致所有修改代码丢失,痛定思痛,决定改用1000条新闻,所以上图应为(1001,3)。
由于原始数据都是由爬虫爬下来的,所以看起来有些不整洁,需要清洗一番。这里有几个字段特征:
- Category:当前新闻所属的类别,一会要进行分类任务,这就是标签。
- Theme:新闻的主题,这个暂时不用,大家在练习的时候,也可以把它当作特征。
- Content:新闻的内容,也就是一篇文章,内容很丰富。
前5条数据都是与财经有关,我们再来看看后5条数据(见图11-5)。
图11-5 财经类别新闻
这些都与另一个主题——土地相关,任务已经很明确,根据文章的内容进行类别的划分。那么如何做呢?之前看到的数据都是数值型,直接传入算法中求解参数即可。这份数据显得有些特别,都是文本,计算机可不认识这些文字,所以,首先需要把这些文字转换成特征,例如将一篇文章转换成一个向量,这样计算机就能识别了。
11.2.1数据清洗
对于一篇文章来说,里面的内容很丰富,对于中文数据来说,通常的做法是先把文章进行分词,然后在词的层面上去构建文章向量。下面先选一篇文章,然后进行分词:
1 content = df_news.content.values.tolist() #将每一篇文章转换成一个list 2 print (content[1000]) #随便选择其中一个看看
这里选择使用结巴分词工具包完成这个分词任务(Python中经常用的分词工具),首先直接在命令行中输入“pip install jieba”完成安装。结巴工具包还是很实用的,主要用来分词,其实它还可以做一些自然语言处理相关的任务,想具体了解的同学可以参考其GitHub文档。
分词的基本原理也是机器学习算法,感兴趣的同学可以了解一下HMM隐马尔可夫模型。
在结果中可以看到将原来的一句话变成了一个list结构,里面每一个元素就是分词后的结果,这份数据规模还是比较小的,只有5000条,分词很快就可以完成(这次耗的不是CPU,是内存,约占去5-8G内存,和数据量有关)。
1 content_S = [] 2 for line in content: 3 current_segment = jieba.lcut(line) #对每一篇文章进行分词 4 if len(current_segment) > 1 and current_segment != '\r\n': #换行符 5 content_S.append(current_segment) #保存分词的结果
Building prefix dict from the default dictionary ... Dumping model to file cache d:\Temp\jieba.cache Loading model cost 0.962 seconds. Prefix dict has been built successfully.
1 content_S[900]
完成分词任务之后,要处理的对象就是其中每一个词,我们知道一篇文章的主题应该由其内容中的一些关键词决定,例如“订车”“一汽”“车展”等,一看就知道与汽车相关。但是另一类词,例如“今年”“在”“3月”等,似乎既可以在汽车相关的文章中使用,也可以在其他文章中使用,它们称作停用词,也就是要过滤的目标。
首先需要选择一个合适的停用词库,网上有很多现成的,但是都没有那么完整,所以,当大家进行数据清洗任务的时候,还需要自己添加一些,停用词如图11-6所示。
图11-6中只截取停用词表中的一部分,都是一些没有实际主题色彩的词,如果想把清洗的任务做得更完善,还是需要往停用词表中加入更多待过滤的词语,数据清洗干净,才能用得舒服。如果添加停用词的任务量实在太大,一个简单的办法就是基于词频进行统计,普遍情况下高频词都是停用词。
对于文本任务来说,数据清洗非常重要,因为其中每一个词都会对结果产生影响,在开始阶段,还是希望尽可能多地去掉这些停用词。
过滤掉停用词的方法很简单,只需要遍历数据集,剔除掉那些出现在停用词表中的词即可,下面看一下对比结果。
1 df_content=pd.DataFrame({'content_S':content_S}) #专门展示分词后的结果 2 df_content.head() 3 4 stopwords=pd.read_csv("stopwords.txt",index_col=False,sep="\t",quoting=3,names=['stopword'], encoding='utf-8') 5 stopwords.head(20) 6 7 def drop_stopwords(contents,stopwords): 8 contents_clean = [] 9 all_words = [] 10 for line in contents: 11 line_clean = [] 12 for word in line: 13 if word in stopwords: 14 continue 15 line_clean.append(word) 16 all_words.append(str(word)) 17 contents_clean.append(line_clean) 18 return contents_clean,all_words 19 20 contents = df_content.content_S.values.tolist() 21 stopwords = stopwords.stopword.values.tolist() 22 contents_clean,all_words = drop_stopwords(contents,stopwords) 23 24 #df_content.content_S.isin(stopwords.stopword) 25 #df_content=df_content[~df_content.content_S.isin(stopwords.stopword)] 26 #df_content.head() 27 28 df_content=pd.DataFrame({'contents_clean':contents_clean}) 29 df_content.head() 30 #8万9千条新闻,运行了半小时,后改为1000条新闻,邀月注
显然,这份停用词表做得并不十分完善,但是可以基本完成清洗的任务,大家可以酌情完善这份词表,根据实际数据情况,可以选择停用词的指定方法。
中间来一个小插曲,在文本分析中,现在经常会看到各种各样的词云,用起来还是比较有意思的。在Python中可以用wordcloud工具包来做,可以先参考其github文档。
1 pip install wordcloud
1 df_all_words=pd.DataFrame({'all_words':all_words}) 2 df_all_words.head() 3 4 import numpy as np 5 # words_count=df_all_words.groupby(by=['all_words'])['all_words'].agg({"count":np.size}) 6 # SpecificationError: nested renamer is not supported 7 # print(df_all_words.head(10) 8 # df_words=df_all_words(['all_words']) 9 10 # print(df_all_words.columns) 11 df_all_words.loc[:, 'count'] = 1 #设置一个整列值 12 13 # print(df_all_words.columns) 14 15 data_group3 = df_all_words.groupby('all_words').agg({'count':'count'}) 16 data_group3.head(10) 17 18 # help(words_count.reset_index) 19 words_count=data_group3.reset_index().sort_values(by=['count'],ascending=False) 20 # words_count.drop(31733,axis=0) #'\ue40c'对应的是31733 21 # words_count.drop(index=31733,axis=0) #'\ue40c'对应的是31733 22 23 words_count.head(100)
1 from wordcloud import WordCloud 2 import matplotlib.pyplot as plt 3 %matplotlib inline 4 import matplotlib 5 matplotlib.rcParams['figure.figsize'] = (10.0, 5.0) 6 7 wordcloud=WordCloud(font_path="./data/simhei.ttf",background_color="white",max_font_size=80) 8 word_frequence = {x[0]:x[1] for x in words_count.head(100).values} 9 wordcloud=wordcloud.fit_words(word_frequence) 10 plt.imshow(wordcloud)
{'比赛': 246, '中': 227, '北京': 181, '中国': 166, '时间': 165, '市场': 92, '冠军': 82, '美国': 77, '经济': 68, '体育': 64, '选手': 64,
'国际': 61, '昨日': 58, '球员': 57, '公司': 55, '号': 55, '女人': 54, '世界': 52, '新': 51, '一场': 50, '决赛': 49, '球队': 48, '争夺':
47, '主场': 46, '说': 45, '公开赛': 45, '前': 44, '报道': 43, '搜狐': 43, '网球': 42, '最终': 42, '上海': 42, '全球': 42, '轮': 40, '赛季': 40,
'期': 39, '联赛': 39, '点': 39, '岁': 39, '种子': 38, '男人': 38, '球迷': 38, '发布': 38, '对手': 36, '结束': 35, '投资': 35, '正式': 35,
'广州': 34, '队': 34, '全国': 34, '凌晨': 33, '伦敦': 32, '晋级': 32, '数据': 32, '英国': 31, '亿元': 31, '显示': 30, '温网': 30,
'展开': 30, '战胜': 30, '公布': 30, '当日': 30, '里': 30, '表现': 29, '击败': 29, '温布尔登': 29, '国内': 29, '发展': 29, '元': 29,
'近日': 29, '分钟': 28, '品牌': 28, '称': 28, '迎战': 28, '昨天': 27, '赛段': 26, '参加': 26, '迎来': 26, '做': 26, '企业': 26, '关注': 25,
'情况': 25, '推出': 25, '开奖': 25, '俱乐部': 25, '讯': 25, '图为': 24, '媒体': 24, '赛': 24, '进一步': 24, '女单': 24, '意大利': 24,
'深圳': 24, '接受': 23, '组': 23, '摄': 23, '日前': 23, '男单': 23, '恒大': 23, '集团': 23}
邀月提示:你可以修改为自己的数据试试,效果:
11.2.2TF-IDF关键词提取
在文本分析中,经常会涉及打标签和特征提取,TF-IDF是经常用到的套路。在一篇文章中,经过清洗之后,剩下的都是稍微有价值的词,但是这些词的重要程度相同吗?如何从一篇文章中找出最有价值的几个词呢?如果只按照词频进行统计,得到的结果并不会太好,因为词频高的可能都是一些套话,并不是主题,这时候TF-IDF就派上用场了。
这里借用一个经典的例子——一篇文章《中国的蜜蜂养殖》。
当进行词频统计的时候,发现在这篇文章中,“中国”“蜜蜂”“养殖”这3个词出现的次数是一样的,假设都是10次,这个时候如何判断其各自的重要性呢?这篇文章讲述的应该是与蜜蜂和养殖相关的技术,所以“蜜蜂”和“养殖”这两个词应当是重点。而“中国”这个词,既可以说中国的蜜蜂,还可以说中国的篮球、中国的大熊猫,能派上用场的地方简直太多了,并不专门针对某一个主题,所以,在这篇文章的类别划分中,它应当不是那么重要。
这样就可以给出一个合理的定义,如果一个词在整个语料库中(可以当作是在所有文章中)出现的次数都很高(这篇文章有它,另一篇还有这个词),那么这个词的重要程度就不高,因为它更像一个通用词。如果另一个词在整体的语料库中的词频很低,但是在这一篇文章中却大量出现,就有理由认为它在这篇文章中很重要。例如,“蜜蜂”这个词,在篮球、大熊猫相关的文章中基本不可能出现,在这篇文章中却大量出现。TF-IDF计算公式如下:
其中:
词频这个概念很好理解,逆文档频率就看这个词是不是哪儿都出现,出现得越多,其值就越低。掌握TF-IDF之后,下面以一篇文章试试效果:
1 import jieba.analyse #工具包 2 index = 345 #随便找一篇文章就行 3 content_S_str = "".join(content_S[index]) #把分词的结果组合在一起,形成一个句子 4 print (content_S_str) #打印这个句子 5 print (" ".join(jieba.analyse.extract_tags(content_S_str, topK=5, withWeight=False)))#选出来5个核心词
关键词结果:伯南克 美国 失业率 经济 财政
简单过一遍文章可以发现,讲的大概就是美国失业率,得到的关键词也与文章的主题差不多。关键词提取方法还是很实用的,想一想大家每天使用各种APP都能看到很多广告,不同的用户收到的广告应该不同。接下来还需将重点放回分类任务中,先来看一下标签都有哪些类别:
1 # df_news = pd.read_table('./data/data.txt',names=['category','theme','content'],encoding='utf-8',delimiter='^') 2 # df_news = df_news.dropna() 3 # df_news.tail() 4 5 # print(df_news['category'][1:1000]) 6 # print(df_news['category'][1:1000]) 7 print(len(contents_clean)) 8 9 10 # df_train=pd.DataFrame({'contents_clean':contents_clean,'label':df_news['category']}) 11 # ValueError: array length 1000 does not match index length 1001 12 # ValueError: arrays must all be same length 13 ###***************************************过滤第一行标题******************************* 14 # d = {'contents_clean':contents_clean, 'label': df_news['category'][0:1000]} 15 d = {'contents_clean':contents_clean, 'label': df_news['category']} 16 df_train = pd.DataFrame(data=d) 17 # df_train=pd.DataFrame(data={'contents_clean':contents_clean,'label':df_news['category']}) 18 19 #数据异常检查,邀月注: 20 #************************************************************************************* 21 # 读取数据 22 # train = pd.read_csv('./data/train.csv/train.csv') 23 24 # 检查数据中是否有缺失值 25 # np.isnan(df_train).any() 26 27 # Flase:表示对应特征的特征值中无缺失值 28 # True:表示有缺失值 29 30 # 2、删除有缺失值的行 31 # df_train.dropna(inplace=True) 32 33 # 然后在看数据中是否有缺失值 34 35 # 也可以根据需要对缺失值进行填充处理: 36 # df_train.fillna(0) 37 print(type(df_train)) 38 # print(np.isnan(df_train['category']).any()) 39 # print(np.isnan(df_train).any()) 40 41 # from sklearn.impute import SimpleImputer 42 # imp_mean = SimpleImputer(missing_values=np.nan, strategy='mean') 43 # imp_mean.fit(df_train) 44 45 # np.nan_to_num(df_train, nan=np.nanmean(df_train)) 46 47 #******************************************************************** 48 # df_train.tail() 49 # print(df_train.head(1000))
一直报这个错:删除原data.txt文件中的标题行即可。
# df_train=pd.DataFrame(data={'contents_clean':contents_clean,'label':df_news['category']}) # ValueError: array length 1000 does not match index length 1001
1 df_train.label.unique() 2 #array(['category', '财经', '女性', '体育', '奥运'], dtype=object)
1 # label_mapping = {"汽车": 1, "财经": 2, "科技": 3, "健康": 4, "体育":5, "教育": 6,"文化": 7,"军事": 8,"娱乐": 9,"时尚": 0} 2 label_mapping = {"女性": 1, "财经": 2, "体育":5, "奥运": 6} 3 df_train['label'] = df_train['label'].map(label_mapping) #构建一个映射方法 4 df_train.head()
1 from sklearn.model_selection import train_test_split 2 3 x_train, x_test, y_train, y_test = train_test_split(df_train['contents_clean'].values, df_train['label'].values, random_state=1) 4 5 #x_train = x_train.flatten() 6 x_train[0][1] 7 8 words = [] 9 for line_index in range(len(x_train)): 10 try: 11 #x_train[line_index][word_index] = str(x_train[line_index][word_index]) 12 words.append(' '.join(x_train[line_index])) 13 except: 14 print (line_index,word_index) 15 words[0]
1 print (len(words))
到目前为止,已经处理了标签,切分了数据集,接下来就要提取文本特征了,这里通过一个小例子给大家介绍最简单的词袋模型。
1 from sklearn.feature_extraction.text import CountVectorizer 2 texts=["dog cat fish","dog cat cat","fish bird", 'bird'] #为了简单期间,这里4句话我们就当做4篇文章了 3 cv = CountVectorizer() #词频统计 4 cv_fit=cv.fit_transform(texts) #转换数据 5 6 print(cv.get_feature_names()) 7 print(cv_fit.toarray()) 8 9 10 print(cv_fit.toarray().sum(axis=0))
['bird', 'cat', 'dog', 'fish'] [[0 1 1 1] [0 2 1 0] [1 0 0 1] [1 0 0 0]] [2 3 2 2]
向sklearn中的feature_extraction.text模块导入CountVectorizer,也就是词袋模型要用的模块,这里还有很丰富的文本处理方法,感兴趣的读者也可以尝试一下其他方法。为了简单起见,构造了4个句子,暂且当作4篇文章就好。观察发现,这4篇文章中总共包含4个不同的词:“bird”“cat”“dog”“fish”。所以词袋模型的向量长度就是4,在结果中打印get_feature_names()可以得到特征中各个位置的含义,例如,从第一个句子“dog cat fish”得到的向量为[0 1 1 1],它的意思就是首先看第一个位置’bird’在这句话中有没有出现,出现了几次,结果为0;接下来同样看“cat”,发现出现了1次,那么向量的第二个位置就为1;同理“dog”“fish”在这句话中也各出现了1次,最终的结果也就得到了。
词袋模型是自然语言处理中最基础的一种特征提取方法,直白地说,它就是看每一个词出现几次,统计词频即可,再把所有出现的词组成特征的名字,依次统计其个数就能够得到文本特征。感觉有点过于简单,只考虑词频,而不考虑词出现的位置以及先后顺序,能不能稍微改进一些呢?还可以通过设置ngram_range来控制特征的复杂度,例如,不仅可以考虑单单一个词,还可以考虑两个词连在一起,甚至更多的词连在一起的组合。
1 from sklearn.feature_extraction.text import CountVectorizer 2 texts=["dog cat fish","dog cat cat","fish bird", 'bird'] 3 cv = CountVectorizer(ngram_range=(1,4)) #设置ngram参数,让结果不光包含一个词,还有2个,3个的组合 4 cv_fit=cv.fit_transform(texts) 5 6 print(cv.get_feature_names()) 7 print(cv_fit.toarray()) 8 9 10 print(cv_fit.toarray().sum(axis=0))
['bird', 'cat', 'cat cat', 'cat fish', 'dog', 'dog cat', 'dog cat cat', 'dog cat fish', 'fish', 'fish bird'] [[0 1 0 1 1 1 0 1 1 0] [0 2 1 0 1 1 1 0 0 0] [1 0 0 0 0 0 0 0 1 1] [1 0 0 0 0 0 0 0 0 0]] [2 3 1 1 2 2 1 1 2 1]
这里只加入ngram_range=(1,4)参数,其他保持不变,观察结果中的特征名字可以发现,此时不仅是一个词,还有两两组合或三个组合在一起的情况。例如,“cat cat”表示文本中出现“cat”词后面又跟了一个“cat”词出现的个数。与之前的单个词来对比,这次得到的特征更复杂,特征的长度明显变多。可以考虑上下文的前后关系,在这个简单的小例子中看起来没什么问题。如果实际文本中出现不同词的个数成千上万了呢?那使用ngram_range=(1,4)参数,得到的向量长度就太大了,用起来就很麻烦。所以,通常情况下,ngram参数一般设置为2,如果大于2,计算起来就成累赘了。接下来对所有文本数据构建词袋模型:
1 from sklearn.feature_extraction.text import CountVectorizer 2 3 vec = CountVectorizer(analyzer='word', max_features=4000, lowercase = False) 4 feature = vec.fit_transform(words)
1 feature.shape 2 #(750, 4000)
在构建过程中,还额外加入了一个限制条件max_features=4000,表示得到的特征最大长度为4000,这就会自动过滤掉一些词频较小的词语。如果不进行限制,大家也可以去掉这个参数观察,会使得特征长度过大,最终得到的向量长度为85093,而且里面很多都是词频很低的词语,导致特征过于稀疏,这些对建模来说都是不利的,所以,还是非常有必要加上这样一个限制参数,特征确定之后,剩下的任务就交给贝叶斯模型吧:
1 from sklearn.naive_bayes import MultinomialNB #贝叶斯模型 2 classifier = MultinomialNB() 3 classifier.fit(feature, y_train) 4 5 test_words = [] 6 for line_index in range(len(x_test)): 7 try: 8 # 9 test_words.append(' '.join(x_test[line_index])) 10 except: 11 print (line_index,word_index) 12 test_words[0] 13 14 classifier.score(vec.transform(test_words), y_test)
结果是0.936
贝叶斯模型中导入了MultinomialNB模块,还额外做了一些平滑处理,主要目的是在求解先验概率和条件概率的时候避免其值为0。词袋模型的效果看起来还凑合,能不能改进一些呢?在这份特征中,公平地对待每一个词,也就是看这个词出现的个数,而不管它重要与否,但看起来还是有点问题。因为对于不同主题来说,有些词可能更重要,有些词就没有什么太大价值。还记得老朋友TF-IDF吧,能不能将其应用在特征之中呢?当然是可以的,下面通过一个小例子来看一下吧:
1 from sklearn.feature_extraction.text import TfidfVectorizer 2 3 X_test = ['卡尔 敌法师 蓝胖子 小小','卡尔 敌法师 蓝胖子 痛苦女王'] 4 5 tfidf=TfidfVectorizer() 6 weight=tfidf.fit_transform(X_test).toarray() 7 word=tfidf.get_feature_names() 8 print (weight) 9 for i in range(len(weight)): 10 print (u"第", i, u"篇文章的tf-idf权重特征") 11 for j in range(len(word)): 12 print (word[j], weight[i][j])
[[0.44832087 0.63009934 0.44832087 0. 0.44832087] [0.44832087 0. 0.44832087 0.63009934 0.44832087]] 第 0 篇文章的tf-idf权重特征 卡尔 0.44832087319911734 小小 0.6300993445179441 敌法师 0.44832087319911734 痛苦女王 0.0 蓝胖子 0.44832087319911734 第 1 篇文章的tf-idf权重特征 卡尔 0.44832087319911734 小小 0.0 敌法师 0.44832087319911734 痛苦女王 0.6300993445179441 蓝胖子 0.44832087319911734
简单写了两句话,就是要分别构建它们的特征。一共出现5个词,所以特征的长度依旧为5,这和词袋模型是一样的,接下来得到的特征就是每一个词的TF-IDF权重值,把它们组合在一起,就形成了特征矩阵。观察发现,在两篇文章当中,唯一不同的就是“小小”和“痛苦女王”,其他词都是一致的,所以要论重要程度,还是它们更有价值,其权重值自然更大。在结果中分别进行了打印,方便大家观察。
TfidfVectorizer()函数中可以加入很多参数来控制特征(见图11-9),比如过滤停用词,最大特征个数、词频最大、最小比例限制等,这些都会对结果产生不同的影响,建议大家使用的时候,还是先参考其API文档,价值还是蛮大的,并且还有示例代码。
最后还是用同样的模型对比一下两种特征提取方法的结果差异:(结果是0.916)
1 from sklearn.feature_extraction.text import TfidfVectorizer 2 3 vectorizer = TfidfVectorizer(analyzer='word', max_features=4000, lowercase = False) 4 vectorizer.fit(words) 5 6 from sklearn.naive_bayes import MultinomialNB 7 classifier = MultinomialNB() 8 classifier.fit(vectorizer.transform(words), y_train) 9 10 classifier.score(vectorizer.transform(test_words), y_test)
效果比之前的词袋模型有所提高,这也在预料之中,那么,还有没有其他更好的特征提取方法呢?上一章中曾提到word2vec词向量模型,这里当然也可以使用,只不过难点在于如何将词向量转换成文章向量,传统机器学习算法在处理时间序列相关特征时,效果还是有所欠缺,等弄懂神经网络之后,再向大家展示如何应用词向量特征,感兴趣的同学可以先预习gensim工具包,自然语言处理任务肯定会用上它。
gensim工具包不只有word2vec模块,主题模型,文章向量等都有具体的实现和示例代码,学习价值还是很大的。
项目小结:
本章首先讲解了贝叶斯算法,通过两个小例子,拼写纠错和垃圾邮件分类任务概述了贝叶斯算法求解实际问题的流程。以新闻文本数据集为例,从分词、数据清洗以及特征提取开始一步步完成文本分类任务。建议大家在学习过程中先弄清楚每一步的流程和目的,然后再完成核心代码操作,机器学习的难点不只在建模中,数据清洗和预处理依旧是一个难题,尤其是在自然语言处理中。
第11章完。
该书资源下载,请至异步社区:https://www.epubit.com