实验简介
Hadoop 是什么
Hadoop
是一个由 Apache
基金会所开发的分布式系统基础架构。主要解决海量数据的存储和海量数据的分析计算问题。广义上来说 HADOOP
通常是指一个更广泛的概念—— HADOOP 生态圈
。
Hadoop
的优势:
高可靠性
:因为 Hadoop 假设计算元素和存储会出现故障,因为它维护多个工作数据副本,在出现故障时可以对失败的节点重新分布处理。高扩展性
:在集群间分配任务数据,可方便的扩展数以千计的节点。高效性
:在 MapReduce 的思想下, Hadoop 是并行工作的,以加快任务处理速度。高容错性
:自动保存多份副本数据,并且能够自动将失败的任务重新分配。
Hadoop
组成:
Hadoop HDFS
:一个高可靠、高吞吐量的分布式文件系统。Hadoop MapReduce
: 一个分布式的离线并行计算框架。Hadoop YARN
: 作业调度与集群资源管理的框架。Hadoop Common
:支持其他模块的工具模块(Configuration、RPC、序列化机制、日志操作)。
实验任务说明
本文意在通过 hadoop
集群,完成贝叶斯文本分类的任务。
有关利用 朴素贝叶斯算法
进行文本分类的原理参见上节内容,这里不做过多说明。
本实验是在 Windown 8
操作系统下,通过搭建 VMware Workstation
虚拟机的方式进行 hadoop
集群的部署。本次实验部署了 3
台 hadoop
虚拟机的节点,集群部署规划如下表:
hadoop1 | hadoop2 | hadoop3 | |
---|---|---|---|
HDFS | NameNode DataNode |
DataNode | SecondaryNameNode DataNode |
YARN | NodeManager | ResourceManager NodeManager |
NodeManager |
有关于虚拟机安装与 hadoop
集群环境搭建这里略。
贝叶斯分类器任务框架
数据集简介
数据集文件夹为 NBCorpus
,里面一共两个子文件夹 Country
和 Industry
,实验要求从中选取一个完成即可。
Country
和 Industry
下每个子目录就是一个文档类别,但有的子目录下文件非常少,因此要选择文件比较多的目录(至少二个)进行训练和测试。
每个文件已经分好词,一行一个单词。
训练集和测试集的选取
我在数据集中选择了 Country
文件夹下的 CHINA
和 CANA
作为本次实验的样本,其中 CHINA
类中包含 255
个文本,CANA
类中包含 263
个文本。按照 70%
与 30%
的比例选取训练集和测试集。表格如下:
CHINA | CANA | |
---|---|---|
文档总数 | 255 | 263 |
训练集数 | 178 | 184 |
测试集数 | 77 | 79 |
手动随机抽取相应数量的文档放入相应的数据输入路径,训练集与测试集相应的路径如下所示:
e:/INPUT/TRAIN/CHINA/
,训练集类CHINA
文档路径e:/INPUT/TRAIN/CANA/
,训练集类CANA
文档路径e:/INPUT/TEST/CHINA/
,测试集类CHINA
文档路径e:/INPUT/TEST/CANA/
,测试集类CANA
文档路径
实验任务分解
Bayes
分类器的 MapReduce
实现分为 训练
和 测试
两个阶段。
其中 训练
阶段需要编写两个 MapReduce
任务,MapReduce 任务一
完成计算文档 d
出现在类 c
中的先验概率的所需数据,我将该阶段称为 训练先验概率
阶段;MapReduce 任务二
完成计算词项 t
出现在类 c
中的条件概率的所需数据,我将该阶段称为 训练条件概率
阶段。
其中 测试
阶段需要编写一个 MapReduce 任务三
,用于完成 预测
测试集中文档所属的类别,我将其称为 预测
阶段。另外还要编写一个java程序,用于对 任务三
中预测的结果进行 评估
,我将其称为 评估
阶段。
实验任务所有代码均在 eclipse
编辑器中完成。我在 eclipse
创建了一个名为 Naive Bayes
的 project
,src
文件夹下创建三个 package
,分别取名为 DocCount
,WordCount
以及 Predition
,分别用于完成 MapReduce 任务一
、MapReduce 任务二
以及 MapReduce 任务三
。最后的 评估 阶段代码写在了 Predition
这个包中。项目中的代码结构如下图所示:
MapReduce 任务一 训练先验概率
任务说明
- 需要编写一个单独的
MapReduce Job
,计算结果写入文件; - 实现一个自定义的
InputFormat
和RecordReader
,每读取一个文件(实际上不需要读取文件内容),输出<ClassName,1>
,其中ClassName
为读取的文件所在的类别目录名,<ClassName,1>
为Map
的输入,Map
不做任何处理,直接输出<ClassName,1>
; Map
的输出交给Combine
处理,Combine
的输入为<ClassName,{1,1,...,1}>
,在Combine
中计算1
的个数,所以Combine
的输出为<ClassName,Count>
,Count
为属于ClassName
类别的文档个数,但是局部的;Combine
的输出交给Reducer
,Reducer
的输入为<ClassName,{count1,count2, ..., countn}>
,在Reduce
里对count1
,count2
,…,countn
求和,就得到了ClassName
的总数TotalCount
,Reducer
的输出为<ClassName,TotalCount>
并写到文件;- 该作业主要统计了每种类别文档的总数目,具体概率的计算放在了后面。作业的输出会产生多个文件,取决于
Reducer
的个数,每个文件里一行的格式为:类名 文档总数
代码目录
DocCount
DocCountDriver.java
: 主程序入口DocCountMapper.java
: 实现Map
阶段DocCountReducer.java
: 实现Reduce
阶段WholeFileInputFormat.java
: 重写的InputFormat
类WholeRecordReader.java
: 重写的RecordReader
类
重写 InputFormat 与 RecordReader
之所以要重写这个类,主要是因为 hadoop
中默认的 MapReduce
程序,每一个 Map
任务的调用输入的 key
为文档中每一行行号,数据类型为 LongWritable
;value
为文档中的一行内容,数据类型为 Text
。但是第一任务,我们要求每一个 Map
任务处理一个文档,而不是文档中的每一行记录,所以需要对 InputFormat
与 RecordReader
这两个类进行重写,以符合任务的需求。
重写的 InputFormat
的类文件 WholeFileInputFormat.java
内容如下:
1 | package DocCount; |
重写的 RecordReader
的类文件 WholeRecordReader.java
内容如下:
1 | package DocCount; |
MapReduce 任务
设计的 Map
任务程序 DocCountMapper.java
代码如下:
1 | package DocCount; |
设计的 Reduce
任务程序 DocCountReducer.java
代码如下:
1 | package DocCount; |
main 函数入口
对于程序的入口,我们专门写了一个类进行 main()
封装,命名为 Driver
,程序 DocCountDriver.java
代码如下:
1 | package DocCount; |
程序运行结果
程序运行过程如图所示:
程序运行结束如图所示:
程序输出文件如图所示:
程序输出数据内容如下:
MapReduce 任务二 训练条件概率
任务说明
- 需要编写一个单独的MapReduce Job ,计算结果写入文件;
- 实现一个自定义的
Writable
类型,要求Map
每读取一个文件中的一行(一个单词),输出<<ClassName,Term>,1>
,其中key
为<ClassName,Term>
,ClassName
为读取的文件所在的类别目录名,Term
为单词,1
表示Term
在ClassName
的类里出现一次; Map
的输出交给Combine
处理,Combine
的输入<<ClassName,Term>,{1,1,...,1}>
,在Combine
中计算1
的个数,所以Combine
的输出为<<ClassName,Term>,Count>
,Count
为Term
在ClassName
的类里出现的次数,但是局部的;Combine
的输出交给Reducer
,Reducer
的输入为<<ClassName,Term>,{count1,count2,...,countn}>
,在Reduce
里把count1
,count2
,…,countn
求和,就得到了Term
在ClassName
的类里出现的总次数TotalCount
;Reduce
输出<<ClassName,Term>,TotalCount>
;- 该作业只统计了每个
<ClassName,Term>
对出现的总次数,具体条件概率计算放在了后面。作业的输出会产生多个文件,取决于Reducer
的个数,每个文件里一行的格式为:类名 单词 出现次数
代码目录
WordCount
WordCountDriver.java
: 主程序入口WordCountMapper.java
: 实现Map
阶段WordCountReducer.java
: 实现Reduce
阶段TextPair.java
:重写Wtrtable
类,自定义一个<Text,Text>
的数据类型
定义新的数据类型
Map
任务需要输出的键值为 <ClassName,Term>
,然而我查了下 hadoop
的数据类型,如下表:
Java | Hadoop Writable |
---|---|
boolean | BooleanWritalbe |
byte | ByteWritable |
int | IntWritable |
float | FloatWritable |
long | LongWritable |
double | DoubleWritable |
string | Text |
map | MapWritable |
array | ArrayWritable |
上表中,左侧为 java
的数据类型,右侧为 hadoop
默认的数据类型,每行存储的数据是一样的,只不过一个是 java
的类,一个是 hadoop
的类。
看了下数据类型,只有 MapWritable
满足要求,因为要求输出为键值对形式。无奈这个类不太会用,程序各种编译不通过,所幸重新写了一个自定义的数据类型,取名为 TextPair
,文件 TextPair.java
代码如下:
1 | package WordCount; |
MapReduce 任务
设计的 Map
任务程序 WordCountMapper.java
代码如下:
1 | package WordCount; |
设计的 Reduce
任务程序 WordCountReducer.java
代码如下:
1 | package WordCount; |
main 函数入口
main()
写在 WordCountDriver.java
中,代码如下:
1 | package WordCount; |
程序运行结果
程序运行过程如图所示:
程序运行结束如图所示:
程序输出文件如图所示:
程序输出数据内容如下:
MapReduce 任务三 预测
任务说明
- 预测前将训练得到文件加载到内存里,计算先验概率和每个类别里单词出现的条件概率,可以交给自定义
Mapper
类和自定义Reducer
类的包装类Prediction
来处理,在Prediction
类里定义成类变量来保存这些学习到的概率,这样Mapper
类和Reducer
类都可以访问到这些概率。保存这些概率的数据结构应该用HashTable
,这样可以高效地读取所需的概率值; - 在
Prediction
类实现一个静态方法,计算一个文档属于某类的条件概率P(class|doc)
,该方法无需用MapReduce实现,需要计算其中每个单词出现的频率,该方法命名为conditionalProbabilityForClass
; - 每读取一个文件,这里需要把文件内容作为一个整体读取成为一个
String
,产生<docId,content>
作为Map
的输入; - 在
Map
里写一个for
循环,对于每一个类别c
在循环中调用conditionalProbabilityForClass
函数,得到<docId,<ClassName,Prob>>
,作为Map
的输出。因此Map
的输入为<docId,content>
,Map
的输出为list<docId,<ClassName,Prob>>
; Reduce
任务输入为<docId,list<ClassName,Prob>>
,找到最大的Prob
,输出<docId,最大Prob对应的ClassName>
。
代码目录
Predition
PredictDriver.java
: 主程序入口PredictMapper.java
: 实现Map
阶段PredictReducer.java
: 实现Reduce
阶段PredictTestInputFormat.java
: 重写的InputFormat
类PredictTestRecordReader.java
: 重写的RecordReader
类Prediction.java
: 封装类,用于保存学习到的先验概率和条件概率,并定义一个方法实现P(c|d)
的计算,并给Map
调用
封装类说明
Prediction.java
用于在 MapReduce
运行之前,对之前的任务的运行结果进行预处理,之前的运行结果主要存放在 "e:/z_output_doc/"
和 "e:/z_output_word/"
下。将其数据取出,分别用于计算该文档 d
属于类 c
的先验概率 P(c)
,以及在类 c
中单词 t
出现的条件概率 p(t|c)
,并将计算结果存入相应的哈希表 class_prob
和 class_term_prob
中。并定义一个方法实现 P(c|d)
的计算,传给 Map
调用。封装类 Prediction.java
代码如下:
1 | package Predition; |
重写 InputFormat 与 RecordReader
之所以要重写这个类,原因前面做 MapReduce 任务一
说过,默认的类不满足任务需求,我们要求给 Map
的出入为整个文档的内容,并以字符串的形式传递,所以要进行改写。
重写的 InputFormat
的类文件 PredictTestInputFormat.java
内容如下:
1 | package Predition; |
重写的 RecordReader
的类文件 PredictTestRecordReader.java
内容如下:
1 | package Predition; |
MapReduce 任务
设计的 Map
任务程序 PredictMapper.java
代码如下:
1 | package Predition; |
设计的 Reduce
任务程序 PredictReducer.java
代码如下:
1 | import java.io.IOException; |
main 函数入口
main()
写在 PredictDriver.java
中,代码如下:
1 | package Predition; |
程序运行结果
程序运行过程如图所示:
程序运行结束如图所示:
程序输出文件如图所示:
程序输出数据内容如下:
评估
任务说明
- 根据
MapReduce 任务三
的预测结果,计算每一个类别的精确率与召回率; - 类别超过一个,评估算法对于所有类别的精确率与召回率的宏平均与微平均指标。
代码目录
Predition
Evaluation.java
: 该类主要计算预测的评估指标
预测代码实现
Evaluation.java
代码内容如下:
1 | package Predition; |
运行结果
运行结果如下图所示:
对上表进行整理,结果如下:
CHINA | Yes(Ground Truth) | No(Ground Truth) |
---|---|---|
Yes(Classified) | 77 | 14 |
No(Classified) | 0 | 65 |
CHINA
类的精确率为 0.8461
,CHINA
类的召回率为 1.00
,F1
值为 0.9167
。
CANA | Yes(Ground Truth) | No(Ground Truth) |
---|---|---|
Yes(Classified) | 65 | 0 |
No(Classified) | 14 | 77 |
CANA
类的精确率为 1.00
,CANA
类的召回率为 0.8228
,F1
值为 0.9028
。
微平均的计算结果为
P: 0.923077
R: 0.911392
F1: 0.909722
宏平均的计算结果为
P: 0.910256
R: 0.910256
F1: 0.910256