Github源码地址:https://github.com/courseralxy/MapReduce-Big-Data-Processing/tree/master/final%20project
文字版实验报告:
MapReduce大数据课程设计3:邮件自动分类
牛哥1 161220082 月哥2 161220085 圆哥3 161220083
1(南京大学 计算机科学与技术系,南京 210023)
2(南京大学 计算机科学与技术系,南京 210023)
3(南京大学 计算机科学与技术系,南京 210023)
MapReduce Big Data Class Design: Email auto-classification
Brother Niu1 Brother Yue2 Brother Yuan3
1(Department of Computer Science and Technology, Nanjing University, Nanjing 210023, China)
+ Corresponding author: Llf: + 86-1xx-xxxxxxxx: [email protected]
2(Department of Computer Science and Technology, Nanjing University, Nanjing 210023, China)
+ Corresponding author: Lxy: + 86-1xx-xxxxxxxx, E-mail: [email protected]
3(Department of Computer Science and Technology, Nanjing University, Nanjing 210023, China)
+ Corresponding author: Lxy: +86-1xx-xxxxxxxx, E-mail: [email protected]
电子邮件产生于上世纪60年代,早期分时计算机的出现使基于计算机的邮件和消息传递成为可能,早期大型机和小型计算机的大多数开发人员们独立开发了许多功能类似但通常互相不兼容的邮件应用程序。许多美国大学都是ARPANET的一部分,ARPANET旨在实现系统之间的软件可移植性。在此情况下,出现了简单邮件传输协议SMTP。在20世纪80年代末和90年代初,政府开放系统互连概况(GOSIP)的一部分专有商业系统或X.400电子邮件系统占主导地位。二十世纪末互联网的诞生,使得SMTP,POP3和IMAP电子邮件协议成为标准。
早期的电子邮件系统要求作者和收件人同时在线,与即时消息一样。如今的电子邮件系统基于存储转发模型。服务器接受、转发、传递和存储消息。发件人与收件人不需要同时在线; 他们只需要短暂连接,发送或接收邮件即可。最早的电子邮件仅限于使用ASCII文本进行通信,通过多用途Internet邮件扩展(MIME),才得以发送例如图片和文档之类的多媒体介质。
我们常说,技术是一把双刃剑,电子邮件的出现,极大地丰富了人们交流和联系的渠道,但随之出现的还有大量的垃圾邮件。当今时代,网民们在互联网上裸奔,黑产通过各类方式收集网民们的电子邮件账户,之后将其转卖给各类公司,这种交易的价格十分廉价,因此一笔信息交易的量级能够达到百万级别。公司在获取到大量邮箱账户后,便会向这些邮箱账户中频繁发送推广邮件,用户不堪其扰。这些电子牛皮癣极大影响网民心情,甚至还有可能掩埋住重要邮件。除了广告邮件,还有人盯上了网民的电脑数据,通过发送木马邮件和钓鱼邮件,在粗心的收件人的电脑中植入病毒,窃取个人数据,监控其网络流量,甚至盗取银行账户密码使收件人蒙受经济损失。除了以上两种最为常见的垃圾邮件形式以外,还有很多其他种类的新型垃圾邮件,因此垃圾邮件的分类十分重要。
伴随着电子邮件兴起的,还有一个叫做“电子邮件破产(email bankruptcy)”的名词,他是指用户收到了太多邮件无法一次读完,如此积压下去最终达到了人力不可逾越的数量,再也不可能全部读完。尽管这些邮件都是有用的邮件,但有用也是有级别的,相同的邮件对于不同的用户可能有不同的优先级,不同的邮件对相同的用户也有不同的优先级,如何能将邮件按照一定的标准,例如内容或者主题,自动进行归类,就成为了一个有价值的研究主题。
给定训练集是新闻邮件训练集,共有20种类别,每个类别都有自己的名称(文件夹名称),每个文件夹内有大量的邮件,邮件以文本形式呈现。因此本任务是一项文本分类任务,给定一段文本,判断其类别,类别共20种。
同一封邮件,其内容或许不仅仅是只属于某个类别,也可能属于多个类别。在实际应用场景中考虑到用户可能由于对于某种邮件有错误的认知,而去错误的分类文件夹中寻找,如果能将同一封邮件分到多个可能的邮箱,那么可以大大加快用户寻找到该邮件的速度。基于这个想法,我们提出了另外一种性能度量方式:前K类命中率。前K类命中率是指,将邮件属于所有类别的可能性大小进行排序,取可能性最大的K个,将该邮件同时分到这K个类别中,计算测试样例中前K个类别含有正确类别的比例。KNN作为一个经典的机器学习分类模型,恰好契合我们的前K类命中率的想法,并且KNN易于并行化实现,因此作为我们首选的分类算法。
由于数据集中每一封邮件都是一个长字符串,而且其长度不一,无法直接作为KNN的输入。即使采用一些方法对短文本进行补齐和截断,将字符串的Unicode码当作是数值输入也是毫无意义的,因此应当对短文本进行一定的处理,将处理后的数据作为机器学习算法的输入,也就是要对数据进行向量化。向量化比较好的一种方式是TF-IDF,产生one-hot编码,并且TF-IDF简单、高效、易并行。
为了更加客观地评价KNN最终的分类效果,我们使用朴素贝叶斯进行类比实验,并使用准确率(Accuracy)作为评估指标,看KNN的表现是否能够超过朴素贝叶斯。需要注意的是朴素贝叶斯用到的向量化并非TF-IDF,而是基于统计词频的CountVectorize模型。
这一节详细讲述了词频、TF-IDF、KNN、朴素贝叶斯的原理。
对文本进行向量化的一个直观的方法是使用字典统计词频。例如,“我爱你,中国”这句话中,包含“我”、“爱”、“你”和“中国”这四个词,这句话的词频就是“我”:1,“爱”:1,“你”:1,“中国”:1。而“世界是我们的,也是你们的,但终究是你们的”词频是“世界”:1,“是”:2,“我们”:1,“的”:3,“也”:1,“你们”:2,“但”:1,“终究”:1。
仅仅通过词频的方式将文本转换成向量会出现一个问题,在长度长的短文本中,词频会比长度短的短文本高得多,但是这两条短文本表达的都是同样的意思。如果采用one-hot编码,这个词袋模型并不能考虑词与词之间的顺序,并且这个模型的假设是词与词相互独立,但一个句子是一个序列,前后词是相互影响的,除此以外,通过one-hot编码得到的样本特征矩阵是稀疏的,因为中文词库非常大,然后数据集又是短文本,因此每条数据的词量很少。
TF-IDF是information retrieval领域中常用的文本表示方法,由TF和IDF两部分构成。
TF为词的词频,计算方式如下
IDF逆文档频率。在一条短文本中,几个词出现的次数可能是相同的,但是它们各自对于这个句子的“重要性”是不一样的,即如果某个词比较少见,但是在一条文本中多次出现,那么它很可能就反映了这条文本的特征IDF就是要给予最常见的词(停用词等)最小的权重,给予较常见的词(如“中国”)较小的权重,给予少见的词较大的权重,这个权重就叫做Inverse Document Frequency(逆文档频率,缩写为IDF),计算方式如下
两者相乘就得到了这个词向量化中的TF-IDF值,如下图所示
TF-IDF的原理可以说类似于信息学中的信息熵,TF-IDF值与该词在文章中出现的频率成正比,与该词在整个语料库中出现的次数成反比,因此可以很好地提取文本关键词,用于表示文本内容
KNN是机器学习中一种常用的监督学习方法,其原理是给定测试样本,基于某种距离度量找出训练集中与测试样本最靠近的k个训练样本,然后基于这k个邻居的信息来进行预测。通常,在分类任务重可以使用投票法,即选择这k个样本中出现次数最多的类别标记作为预测结果;在回归任务中则可以使用平均法,即将这k个样本的实值输出标记的平均值作为预测结果。除以此外,还可以基于距离远近进行加权平均或加权投票,距离越近样本权重越大。
以下有多种距离度量的方式,本次实验中采用的最适合文档分类问题的余弦距离。
欧氏距离是最为常见的距离度量方式,全称是欧几里得距离或欧几里得度量,指的是在欧几里得空间中,两点间的直线距离,也成为向量的自然长度,计算公式如下:
可见,欧式距离即两个向量差值的二范数
曼哈顿距离源于纽约繁华的曼哈顿街道,指在几何度量空间对距离进行度量,也成为棋盘距离,计算公式如下:
可见,曼哈顿距离即两个向量差值的一范数
余弦距离也成为余弦相似度,是用向量空间中两个向量夹角的余弦值作为衡量两个个体间差异的大小。相比前面两种距离度量方式,余弦距离更加注重两个向量在方向上的差异,而非在距离或长度上,其计算公式如下:
贝叶斯算法的核心基于著名的贝叶斯公式,它把计算“具有某特征的条件下属于某类的概率”转化为计算“属于某类的条件下具有某特征的概率”,即后验概率等于先验概率乘以调整因子。在朴素贝叶斯算法中,首先预估一个先验概率,然后加入实验结果,看这个实验到底是增强还是削弱了先验概率,由此得到更接近事实的后验概率。
朴素贝叶斯方法给予随机变量独立性假设,这种假设非常适合处理文本分类问题。以朴素贝叶斯的观点,句子中两词之间的关系是相互独立的,即一个词的特征向量中每个维度都是相互独立的。
朴素贝叶斯方法的主要流程如下:
在训练集中,很多样本的取值可能并不在其中,但是这不并代表这种情况发生的概率为0,因为未被观测到,并不代表出现的概率为0 。在概率估计时,通常解决这个问题的方法是要进行平滑处理,常用拉普拉斯修正。
朴素贝叶斯中先验概率的计算公式为:
类的条件概率计算公式为:
经由拉普拉斯修正后,
先验概率计算公式变为:
类的条件概率计算公式变为:
朴素贝叶斯常见有两种模型,多项式模型(multinomial model)即词频型和伯努利模型(Bernoulli model)即文档型。二者的计算粒度不一样,多项式模型以单词为粒度,伯努利模型以文件为粒度,因此二者的先验概率和类条件概率的计算方法都不同。邮件分类任务中通常多项式模型能取得更好的效果,本次实验中采用的即是多项式模型。
在多项式模型中, 设某文档d=(t1,t2,…,tk),tk是该文档中出现过的单词,允许重复。我们默认采用哪个拉普拉斯修正,则先验概率和类条件概率计算如下:
先验概率P(c) = 类c下单词总数 / 整个训练样本的单词总数
类条件概率P(tk|c)=(类c下单词tk在各个文档中出现过的次数之和 + 1) / (类c下单词总数 + |V|)
其中V是训练样本的单词表(即抽取单词,单词出现多次,只算一个),|V|则表示训练样本包含多少种单词。 P(tk|c)可以看作是单词tk在证明d属于类c上提供了多大的证据,而P(c)则可以认为是类别c在整体上占多大比例(有多大可能性)。
朴素贝叶斯模型进行预测的方法是计算不同类的后验概率Ppost,取其中最大者的类作为预测的类别。后验概率的计算方式如下:
由于P(X)的大小是固定不变的,因此在比较后验概率时,只比较上式的分子部分即可。
数据预处理主要包括标点符号去除、停用词及数字去除和类别转化三部分。
在使用StringTokenizer对输入value进行切分前,会先将句子中的标点符号去掉。标点符号只考虑了标准键盘上出现的中英文标点符号。考虑到总量并不算很多,就没有采用正则表达式进行匹配。程序在setup中首先将储存有这些标点符号的文件读入为一个ArrayList
停用词表精选了英文中常见的800多个词汇,还包含0~9这是个数字。使用StringTokenizer对输入value进行切分以后,对于切分出的每个单词,判断其是否在停用词表中,如果在停用词表中,将这个词抛弃;只有不在停用词表中的词才会被map操作发射出去
所有邮件按其归属的文件夹名来划分类,map发射出去的时候回以文件夹名来表示它属于的类。
TF值计算、IDF值计算和TF-IDF值计算分别由三个不同的Job组成。
Mapper主要有setup、map和cleanup组成。在setup中,读入标点符号和常见词这两个停用词表,将value中的标点符号、数字和常见词进行去除。之后的map操作中,使用StringTokenizer将value切分成词汇,然后发射<文件名+词汇,1>,并对总词汇进行计数。cleanup中发射<文件名+”!”,总词数>。
Combiner继承Reducer类,由reduce方法组成。因为”!”在 ASCII 码表中排在所有英文字母之前,因此Combine操作首先收到的就是总词数,然后对相同词汇的数量进行累加,之后除以总词数,就得到了词汇的TF值,发射<文件名+词汇,TF值>
Partitioner继承HashPartitioner类,重载了getPartition方法。由于key是文件名+词汇组成,因此同一个文件的不同词汇并不会被放到同一个Reducer上进行reduce,因此需要把文件名作为key进行Partition。直接取文件名,然后调用父类的getPartition方法,将key改为文件名,其他参数原封不动地传进去即可。
Reducer主要有setup、map和cleanup组成。我们希望将同一个文件的词汇的TF值还写到同一个文件中,并且文件名还与原先文件名保持一致,因此输出的format采用了MultipleOutputs类,setup和cleanup就是用于MultipleOutput实例的初始化和清理。在Reduce中,调用MultipleOutputs类的write方法,将词汇及其TF值写入与原先文件同名但不同文件夹的中间结果文件中。
除了以上操作外,还需注明邮件所属类别。我们将邮件所述类别通过job的setProfileParams方法提前写入,然后将其加到文件名的前面,用#符号分隔开来。
IDF计算会读取所有文件,文件数量经由文件系统操作统计,之后通过job的setProfileParams方法提前写入,在map和reduce操作中统计词汇数量和计算IDF值。
Mapper只有一个map操作,发射<词汇,1>。
Reducer中对词汇数量进行累加,之后从Context取得文件总数计算词汇IDF值,然后将词汇及其IDF值写入IDF结果文件。除此以外,还可以加一个Combiner,把Mapper的结果先进行一次累加,减小数据传输量。
Mapper由setup和map组成。setup中读取存放IDF计算结果的文件,将词汇与其IDF值分别存放于一个字符串数组和一个浮点数数组的相应位置。map操作中,读入TF文件,之后找到其IDF值进行计算,然后发射<词汇,词汇的TF-IDF值>
Reduce操作与TF的job类似,也是按照文件名将TF-IDF向量写入。除了将文件中出现的TF-IDF值写入,没出现的词认为其TF-IDF值为0,也要写入文件。TF-IDF值的写入顺序与IDF中间结果文件的顺序相同,因此在最后的TF-IDF文件中,只有向量值,没有词汇,减小了数据量。也方便了后续的计算
KNN原理如2.4所述,此处不再赘述。KNN的计算仅需重写Mapper,核心就是计算距离,然后根据K值筛选。Map操作首先在setup方法中,将已经计算好的训练集文档的TF-IDF值读入内存。考虑到训练集的规模和文档向量的稀疏特征,采用hashmap储存。在map方法中,计算每一个训练集样本与测试样本之间的距离,根据预先设定的K值,始终保留K最近邻,保存在长度为K的数组中。在计算完所有训练集样本与该测试样本的距离后,采用投票法,根据K近邻中最多的类别,判断测试样本的类别,然后发射<测试样本文件名, 类别>
CountVectorize是进行朴素贝叶斯之前的预处理步骤,分为Count、ListWords、Vectorize三个阶段。
Mapper主要有setup、map组成。在setup中,读入标点符号和常见词这两个停用词表,将value中的标点符号、数字和常见词进行去除。之后的map操作中,使用StringTokenizer将value切分成词汇,然后发射<文件名+词汇,1>, 用以统计单个文件的词汇出现频数。
Combiner将相同词汇的频数加起来,Partitioner阶段将key拆分,只保留文件名,从而将相同键值的键值对发送到同一个reduce服务器上。
使用MUltipleOutputs类,将不同邮件的词汇统计存放到不同的文件中,输出的文件名由邮件的类和邮件本身的名字组成,即“类别#原文件名”,以便后续的步骤。输出的内容为<词汇, 频数>。
读取Count步骤的生成的训练集的文件,遇到一个词就发射<词,词频>。
将接收到的键值对中的值加起来,形成一个词出现的总次数,然后输出键值对<单词,次数>。
首先在setup函数中把上一个步骤生成的wordlist加载,便于获得词汇的索引值写入向量。并且发送一个总词数和总词汇数键值对<”!”, wordcount: wordTypes>。
在map阶段,读取Count阶段生成的每一个文件,取得一个词在某一个邮件中的频数,在词汇表中找到这个词的索引值,然后发射<词所在文件,词#词的频数>。其中词所在的文件会标注出该邮件属于训练集还测试集。
对于训练集中未出现在词典中的词,将其直接抛弃不作为特征向量。
这里为了区分训练集向量和测试集向量,这里也使用了MultipleOutputs类来进行输出,同时也使用本身的context变量来输出
由于采用了多项式模型,在发射键值队时,除了要发送
需要统计的信息有两种:某个类所有文件包含的单词总数、属于某个类的某个词在该类中出现的次数,因此依然采用MultipleOutputs进行输出。这里需要通过键值对中的键来区分是两种信息中的哪一种。无论是哪一种,在输出前都需要将他们收到的键值中的值累加起来再输出。这样就把模型的基本信息给输出了。
和Knn一样,预测只需要重写Mapper。首先在setup步骤先读入需要的三个文件:vectorize步骤中生成的记录总词数和总词汇数的文件、训练模型步骤中生成的两个文件。
将输入的样本的特征提取出,然后依次计算它在每个类下的概率,选择概率最高的类,输出键值对<样本名字#概率值,预测类>。
本次实验中,我们采取准确率(Accuracy)这样一个经典的分类效果衡量指标对分类器的表现进行评估,准确率简单、高效,易于实现,其计算公式如下:
准确率实现在Mapper阶段时,对预测的结果进行判断。如果预测的类别和真正的类别相同,则发射
首先在集群上进过了4小时左右的计算,得到TF-IDF表,然后进行KNN的训练和预测,K值默认为10。最终的预测结果截图如下:
进一步我们计算了预测结果的准确率:Accuracy=0.787。截图如下:
首先算出CountVectorize表,然后采用朴素贝叶斯进行训练和预测。预测结果截图如下:
朴素贝叶斯预测结果的准确率:Accuracy=0.614。截图如下:
KNN中K值的选取非常重要。如果当K的取值过小时,一旦有噪声得成分存在们将会对预测产生比较大影响,例如取K值为1时,一旦最近的一个点是噪声,那么就会出现偏差,K值的减小就意味着整体模型变得复杂,容易发生过拟合;如果K的值取的过大时,就相当于用较大邻域中的训练实例进行预测,学习的近似误差会增大。这时与输入目标点较远实例也会对预测起作用,使预测发生错误。K值的增大就意味着整体的模型变得简单;如果K==N的时候,那么就是取全部的实例,即为取实例中某分类下最多的点,就对预测没有什么实际的意义了;K的取值尽量要取奇数,以保证在计算结果最后会产生一个较多的类别,如果取偶数可能会产生相等的情况,不利于预测。
我们在实验中KNN的预测准确率偏低,可能是因为没有取到一个合适的K值。因此,我们做了一组实验,观察K值对准确率的影响。
K值 |
准确率 |
3 |
0.866 |
5 |
0.840 |
10 |
0.787 |
15 |
0.778 |
可见,在K值较小时,实验结果就有很大改进。这反映了在现实中存在很多不确定性,分类问题也不是非黑即白的。同一封邮件,其内容或许不仅仅是只属于某个类别,也可能属于多个类别,即一封邮件中包含了多个类别的信息。如果K值取得比较大,可能会有更多训练数据和邮件中次要的信息进行匹配,导致最终预测的准确率不升反降。从实验结果开看,K=3时,KNN的准确率最高,达到0.866,算得上是不错的预测表现。
实验中朴素贝叶斯的准确率只有0.614,显著低于朴素贝叶斯的平均表现。预测完成后,我们检查了朴素贝叶斯的预测文件,发现大部分测试样本的后验概率都是0,导致算法无法进行合理分类。究其原因,是因为文档中大部分词的词频都不高,最多的也不超过1000,然而词典的总词数是10万左右,使得词的类条件概率值非常接近0,最后的计算得到的后验概率有甚者达到10的-100次方级别。在数据如此小的情况下,计算机无法保持其精度,也就无法比较不同的后验概率来进行分类判别。
鉴于此种情况,我们在每一个类条件概率外加一层log,取其对数值。对数可以将非常接近0的数据映射到差异性比较大的大数上,从而回避了类条件概率趋于0而无法判别的问题。
以下是改进后朴素贝叶斯的预测结果截图:
改进后朴素贝叶斯结果的准确率:Accuracy=0.897。截图如下:
可以发现朴素贝叶斯的效果取得了非常大的提升,准确率接近0.9,甚至高于KNN。说明朴素贝叶斯确实是一个经典、高效的文档分类算法,在此类问题中表现非常优秀。
尽管TF-IDF模型非常适合表示文本特征,但是它还是没有利用到词的顺序信息。对于这个问题,更为合适的一个做法是提取短文本每个词的词向量,之后将其补齐和截断到统一的长度,然后训练一个长短期记忆网络LSTM,这样既利用了词本身的信息,还利用了词之间的语序信息,毕竟LSTM生来就是为处理NLP任务的。鉴于时间关系,我们并没有实现这一部分代码。我们预测词向量模型的改进一定能进一步提高KNN的预测表现。
KNN和朴素贝叶斯都是非常优秀的文档分类算法,经过我们的改进后,这两者都有了不错的准确率。从结果来看,KNN的表现略微低于朴素贝叶斯,可能是因为词向量模型采用的是简单的TF-IDF,并没有充分利用文本中的语言信息。
KNN和朴素贝叶斯都非常适合用mapreduce的方式进行并行化,能在较短时间内完成对大量数据的处理,最终取得了不错的成果。
一同完成
圆哥:主体
牛哥:改进TF-IDF值的最终计算
月哥:提出TF-IDF向量归一化
牛哥:主体
月哥:步骤优化(在vectorize中进行词汇、词数统计)
圆哥:KNN主体
月哥:KNN算法修正
牛哥:KNN内存优化,朴素贝叶斯。
牛哥:将文件IO从本地操作改为HDFS操作
圆哥:引言,第1~2章、第3.1~3.3、5.3小节
牛哥:3.4、3.5小节
月哥:其他部分、校对