Spark朴素贝叶斯分类器

  Janix520

一、Who is 贝叶斯

二、什么是贝叶斯分类器

贝叶斯分类器主要有四种,分别是:Naive Bayes、TAN、BAN和GBN

由于涉及算法,比较抽象,想了解详情的请参考博文:分类算法之朴素贝叶斯分类(Naive Bayesian classification)

如果上面你看完还是一时接受不了这个算法,那么,我就发挥一下,简明扼要形象的说一下我的理解

假如辨别男人的特征是:    短头发,运动鞋,大鼻梁

假如辨别女人的特征是:    长头发,高跟鞋,皮肤白

这些特征当然不能百分百辨别一个人的性别,比如,女人也可以留短发,女人也可以穿运动鞋,但是,如果我给你两个特征,让你断定下这个人的性别是什么,比如我给你的两个特征是

短头发,运动鞋

你第一反应这个人肯定是男人,如果我说你说错了,这个人是个女性,那么你就尴尬了,别慌,如果给你1000个人,都是这样的特征,再让你猜每个人的性别,答案会怎么样呢?

必然是这1000个人里面性别为男性的概率要大于女性的概率,不要问为什么,因为我知道女生天性爱美,就短头发一点,很多女性都做不到吧,至少我身边的异性是这样的,哈哈

如果只是给你一个人的话,你不敢说这话,因为男女占比各一半,但是我给你多加个特征提示,比如

短发,运动鞋,喉结

你第一反应肯定是男人,没错,喉结是男性的象征,但是,我可以告诉你女性雄性激素过高也可以有喉结,变形人说不定也有喉结,,这时候你又慌了,别急,还是同上,给你1000个测试数据,让你做判断,结果肯定是出现男性的概率要大于女性

因此,概率最大的那个才是我们最终要的结果,而且特征越多,越能根据测试数据进行最大化的概率匹配,得到的答案越是准确

分类哪去了?

假如我说,上面已经涉及到了分类了,你们有没有发现呢?

男性是一个分类标签,女性也是一个分类标签,而得到一个测试数据属于哪个分类标签的过程就是贝叶斯分类器算法该干的事

三、Java代码实现贝叶斯分类器

注意了,注意了,注意了,下面是本篇的精华所在,千万不要打盹!!!

(1)我们把男性和女性的特征做一个合并

总过6列

短发(1) 长发(2) 运动鞋(3) 高跟鞋(4) 喉结(5) 皮肤白(6)

上述我们可以将这些关键特征做成词汇表,比如下面的这种【当然本篇只是举个例子,后续章节会继续提到】

(2)假设男性的特征有:短发、运动鞋、喉结这三个,则其分类标签的向量表示我们可以用

(1,0,1,0,1,0) ==> 1表示对应的特征向量值等于true【有】,0表示false【无】

(3)假设女性的特征有:长发、短发、运动鞋、高跟鞋、皮肤白这五个,则其分类标签的向量表示我们可以用

(1,1,1,1,0,1) == 1表示对应的特征向量值等于true【有】,0表示false【无】

(4)假设我们提供了一个人的测试数据,其具有短发(1),运动鞋(3)这两个特征,则用向量表示我们可以用

(1,0,1,0,0,0)== 1表示对应的特征向量值等于true【有】,0表示false【无】

(5)实例化SparkContext对象
/**
 * 本地模式,*表示启用多个线程并行计算
 */
SparkConf conf = new SparkConf().setAppName("NaiveBayesTest").setMaster("local[*]");
JavaSparkContext sc = new JavaSparkContext(conf);

(6)定义男性和女性两种分类的向量
/**
 * MLlib的本地向量主要分为两种,DenseVector和SparseVector
 * 前者是用来保存稠密向量,后者是用来保存稀疏向量		 
 */
 
/**
 * 两种方式分别创建向量  == 其实创建稀疏向量的方式有两种,本文只讲一种
 * (1.0, 0.0, 1.0, 0.0, 1.0, 0.0)
 * (1.0, 1.0, 1.0, 1.0, 0.0, 1.0)
 */
 
//稠密向量 == 连续的
Vector vMale = Vectors.dense(1,0,1,0,1,0);
 
 
//稀疏向量 == 间隔的、指定的,未指定位置的向量值默认 = 0.0
int len = 6;
int[] index = new int[]{0,1,2,3,5};
double[] values = new double[]{1,1,1,1,1};
//索引0、1、2、3、5位置上的向量值=1,索引4没给出,默认0
Vector vFemale = Vectors.sparse(len, index, values);

女性的向量我就不解释了吧,对照特征词汇表自己翻译

这里,你可以打印两种向量对象,看看输出值是什么,比如,这里我打印稀疏向量vFemale的值,效果如下

(7)生成训练集,类型 == LabelPoint
/**
 * labeled point 是一个局部向量,要么是密集型的要么是稀疏型的
 * 用一个label/response进行关联
 * 在MLlib里,labeled points 被用来监督学习算法
 * 我们使用一个double数来存储一个label,因此我们能够使用labeled points进行回归和分类
 * 在二进制分类里,一个label可以是 0(负数)或者 1(正数)
 * 在多级分类中,labels可以是class的索引,从0开始:0,1,2,......
 */
 
//训练集生成 ,规定数据结构为LabeledPoint == 构建方式:稠密向量模式  ,1.0:类别编号 == 男性
LabeledPoint train_one = new LabeledPoint(1.0,vMale);  //(1.0, 0.0, 1.0, 0.0, 1.0, 0.0)
//训练集生成 ,规定数据结构为LabeledPoint == 构建方式:稀疏向量模式  ,2.0:类别编号 == 女性
LabeledPoint train_two = new LabeledPoint(2.0,vFemale); //(1.0, 1.0, 1.0, 1.0, 0.0, 1.0)
//我们也可以给同一个类别增加多个训练集
LabeledPoint train_three = new LabeledPoint(2.0,Vectors.dense(0,1,1,1,0,1));

训练样本集越多,贝叶斯分类器算法得到的分类结果越精确

List集合存放训练集样本
//List存放训练集【三个训练样本数据】
List<LabeledPoint> trains = new ArrayList<>();
trains.add(train_one);
trains.add(train_two);
trains.add(train_three)

(9)获得JavaRDD
/**
 * SPARK的核心是RDD(弹性分布式数据集)
 * Spark是Scala写的,JavaRDD就是Spark为Java写的一套API
 * JavaSparkContext sc = new JavaSparkContext(sparkConf);    //对应JavaRDD
 * SparkContext	    sc = new SparkContext(sparkConf)    ;    //对应RDD
 * 数据类型为LabeledPoint
 */
JavaRDD<LabeledPoint> trainingRDD = sc.parallelize(trains);

(10)JavaRDD转RDD【Scala】,并利用贝叶斯分类器对RDD数据集进行训练
/**
 * 利用Spark进行数据分析时,数据一般要转化为RDD
 * JavaRDD转Spark的RDD
 */
NaiveBayesModel nb_model = NaiveBayes.train(trainingRDD.rdd());

(11)模拟测试集数据 == 稠密向量【一个人拥有特征:短发,运动鞋】
//测试集生成  == 以下的向量表示,这个人具有特征:短发(1),运动鞋(3)
double []  dTest = {1,0,1,0,0,0};
Vector vTest =  Vectors.dense(dTest);//测试对象为单个vector,或者是RDD化后的vector

(12)贝叶斯分类器分类测试
//朴素贝叶斯用法
int modelIndex =(int) nb_model.predict(vTest);
System.out.println("标签分类编号:"+modelIndex);// 分类结果 == 返回分类的标签值
/**
 * 计算测试目标向量与训练样本数据集里面对应的各个分类标签匹配的概率结果
 */
System.out.println(nb_model.predictProbabilities(vTest)); 
if(modelIndex == 1){
	System.out.println("答案:贝叶斯分类器推断这个人的性别是男性");
}else if(modelIndex == 2){
	System.out.println("答案:贝叶斯分类器推断这个人的性别是男性");
}

这一步才算是结果计算:分类器拿到测试数据样本,并和已经训练好的训练集样本进行概率匹配,而训练集样本又是有分类标签号标注的,因此,贝叶斯分类器最后计算返回的结果就是概率最大的那个分类标签号,也就是下面我会提到的问题模板的索引

(13)最后一步,别忘了关闭sc资源
//最后不要忘了释放资源
sc.close();

四、完整demo

关键pom依赖

<!-- JUnit单元测试 -->
<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.spark/spark-core -->
<dependency>
	<groupId>org.apache.spark</groupId>
	<artifactId>spark-core_2.11</artifactId>
	<version>2.3.0</version>
	<exclusions>
		<exclusion>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.spark/spark-mllib -->
<dependency>
	<groupId>org.apache.spark</groupId>
	<artifactId>spark-mllib_2.11</artifactId>
	<version>2.3.0</version>
</dependency>
<dependency>
	<groupId>org.codehaus.janino</groupId>
	<artifactId>janino</artifactId>
</dependency>

BayesTest.java


import java.util.ArrayList;
import java.util.List;
 
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.mllib.classification.NaiveBayes;
import org.apache.spark.mllib.classification.NaiveBayesModel;
import org.apache.spark.mllib.linalg.Vector;
import org.apache.spark.mllib.linalg.Vectors;
import org.apache.spark.mllib.regression.LabeledPoint;
import org.junit.Test;
 
public class BayesTest {
 
	@Test
	public void TestA(){
		
		/**
		 * 本地模式,*表示启用多个线程并行计算
		 */
		SparkConf conf = new SparkConf().setAppName("NaiveBayesTest").setMaster("local[*]");
		JavaSparkContext sc = new JavaSparkContext(conf);
		
		
		/**
		 * MLlib的本地向量主要分为两种,DenseVector和SparseVector
		 * 前者是用来保存稠密向量,后者是用来保存稀疏向量		 
		 */
		
		/**
		 * 两种方式分别创建向量  == 其实创建稀疏向量的方式有两种,本文只讲一种
		 * (1.0, 0.0, 1.0, 0.0, 1.0, 0.0)
		 * (1.0, 1.0, 1.0, 1.0, 0.0, 1.0)
		 */
		
		//稠密向量 == 连续的
		Vector vMale = Vectors.dense(1,0,1,0,1,0);
		
		
		//稀疏向量 == 间隔的、指定的,未指定位置的向量值默认 = 0.0
		int len = 6;
		int[] index = new int[]{0,1,2,3,5};
		double[] values = new double[]{1,1,1,1,1};
		//索引0、1、2、3、5位置上的向量值=1,索引4没给出,默认0
		Vector vFemale = Vectors.sparse(len, index, values);
		//System.err.println("vFemale == "+vFemale);
		/**
		 * labeled point 是一个局部向量,要么是密集型的要么是稀疏型的
		 * 用一个label/response进行关联
		 * 在MLlib里,labeled points 被用来监督学习算法
		 * 我们使用一个double数来存储一个label,因此我们能够使用labeled points进行回归和分类
		 * 在二进制分类里,一个label可以是 0(负数)或者 1(正数)
		 * 在多级分类中,labels可以是class的索引,从0开始:0,1,2,......
		 */
		
		//训练集生成 ,规定数据结构为LabeledPoint == 构建方式:稠密向量模式  ,1.0:类别编号 == 男性
		LabeledPoint train_one = new LabeledPoint(1.0,vMale);  //(1.0, 0.0, 1.0, 0.0, 1.0, 0.0)
		//训练集生成 ,规定数据结构为LabeledPoint == 构建方式:稀疏向量模式  ,2.0:类别编号 == 女性
		LabeledPoint train_two = new LabeledPoint(2.0,vFemale); //(1.0, 1.0, 1.0, 1.0, 0.0, 1.0)
		//我们也可以给同一个类别增加多个训练集
		LabeledPoint train_three = new LabeledPoint(2.0,Vectors.dense(0,1,1,1,0,1)); 
		
		//List存放训练集【三个训练样本数据】
		List<LabeledPoint> trains = new ArrayList<>();
		trains.add(train_one);
		trains.add(train_two);
		trains.add(train_three);
		
		/**
		 * SPARK的核心是RDD(弹性分布式数据集)
		 * Spark是Scala写的,JavaRDD就是Spark为Java写的一套API
		 * JavaSparkContext sc = new JavaSparkContext(sparkConf);    //对应JavaRDD
		 * SparkContext	    sc = new SparkContext(sparkConf)    ;    //对应RDD
		 * 数据类型为LabeledPoint
		 */
		JavaRDD<LabeledPoint> trainingRDD = sc.parallelize(trains); 
	
		/**
		 * 利用Spark进行数据分析时,数据一般要转化为RDD
		 * JavaRDD转Spark的RDD
		 */
		NaiveBayesModel nb_model = NaiveBayes.train(trainingRDD.rdd());
		
		//测试集生成  == 以下的向量表示,这个人具有特征:短发(1),运动鞋(3)
        double []  dTest = {1,0,1,0,0,0};
        Vector vTest =  Vectors.dense(dTest);//测试对象为单个vector,或者是RDD化后的vector
 
        //朴素贝叶斯用法
        int modelIndex =(int) nb_model.predict(vTest);
        System.out.println("标签分类编号:"+modelIndex);// 分类结果 == 返回分类的标签值
        /**
         * 计算测试目标向量与训练样本数据集里面对应的各个分类标签匹配的概率结果
         */
        System.out.println(nb_model.predictProbabilities(vTest)); 
        if(modelIndex == 1){
        	System.out.println("答案:贝叶斯分类器推断这个人的性别是男性");
        }else if(modelIndex == 2){
        	System.out.println("答案:贝叶斯分类器推断这个人的性别是女性");
        }
        //最后不要忘了释放资源
        sc.close();
		
	}
}

五、运行效果

标签分类编号:1
[0.5524861878453037,0.4475138121546963]
答案:贝叶斯分类器推断这个人的性别是男性

明显,根据提供的两个特征,短发和运动鞋,贝叶斯分类器计算的最终答案是男性,因为,具有该特征的男性的概率要大于具有该特征的女性的概率

如果你对这个结果抱有怀疑的态度,你可以再加个特征,高跟鞋(4),试一试

测试数据集向量数组: double [] dTest = {1,0,1,1,0,0};

效果截图

为什么有高跟鞋特征是女性的概率不是百分百或是百分之九十呢?

首先,短发和运动鞋,这两个特征男性和女性的可能性都有

其次,高跟鞋虽然是女性的特征,但却不是唯一能决定性别走向的因素,因为短发的男生也是有可能穿高跟鞋的,因此,我们不难发现,贝叶斯概率公式真的很NB,他不是乱来的,你以为穿高跟鞋的绝对是女性,但是贝叶斯分类器告诉你,这种概率只有59%,而不是100%!!!

如果还不过瘾,我们再来测试一组,比如一个人具有特征:短发(1),高跟鞋(4),喉结(5)

测试数据集向量: double [] dTest = {1,0,0,1,1,0};

按理说,有男性特征喉结,就能说明这个人八九不离十就是男性,我们看一下贝叶斯分类器的计算结果是否和我们的猜想吻合

没毛病,概率和现实很贴切

我们再来个特征多一点的,比如一个人具有特征:长发(2),运动鞋(3),高跟鞋(4),皮肤白(6)

备注说明:这个人除了头发固定是长发外,鞋子自备了两双,换着换,其实最关键的是Ta没有喉结!!!

测试数据集向量: double [] dTest = {0,1,1,1,0,1};

最后运行结果

女性的概率居然高达:88.7%

------你的笑你的泪,是我筑梦路上,最美的太阳。