目录
一.引言
二.简单的例子
三.朴素贝叶斯原理
1.朴素
2.贝叶斯
四.朴素贝叶斯实战
1.数据准备
2.数据预处理
3.特征 Pipeline 构建
4.Naive Bayes 训练
5.测试与评估
五.总结
贝叶斯方法是统计分析中一个最基本的数据分析方法,其基于假设的先验概率、给定假设下观察到的不同数据的概率以及观察到数据本身而得出结果。其将关于未知参数的先验信息与样本信息节后,再通过贝叶斯公式得到后验概率,然后根据后验信息推断未知参数。朴素贝叶斯最常用于分类处理,下文基于文本实现多分类的朴素贝叶斯模型。
一个学校里有 40 个男生、40 个女生,男生有 10 个穿长裤即 25% 的概率,30 个穿长裤即 75% 的比例,女生有 20 个穿长裤 20 个穿裙子,比例均为 50%。此时有一个近视的同学只能看到一个同学穿着长裤,但看不清是男是女,请问这个同学是男生的概率有多大?
- 先验概率与后验概率
首先规定 B 代表男生,G 代表女生,则 P(B) = P(G) = 40 / (40 + 40) = 0.5 认为是先验概率,其次规定穿长裤事件为 T,看到穿长裤且是男生就转化为条件概率:P(T | B),这个概率是后验概率,也就是我们问题的求解。
- 条件概率
由条件概率可知 P(BT) = P(B) * P(T | B) = P(T) * P(B | T) ,该公式可以理解为当前场景下,穿裤子的男生与男生穿裤子的数量是一样的。
由全概率公式可知 P(T) = P(B) * P(T | B) + P(G) * P(T | G),该公式代表整体穿长裤的概率等于男生穿长裤的概率加女生穿长裤的概率。
- 计算后验概率
基于上面两个概念,我们可以导出后验概率 P(T | B) 的计算公式:
P(B) = 0.5 ,P(T | B) = 0.25,P(T) 根据全概率公式 = 0.25 * 0.5 + 0.5 * 0.5 = 0.375,代入公式得:
所以近视同学看到一个同学穿长裤,其是男生的概率约为 0.33。
朴素贝叶斯,为什么要加个朴素的修饰呢,这里朴素代表简单、直白,表示整个计算过程中遵循以下最简单的假设:
- 独立性
即统计上的独立,有两个随机事件 A、B,若 AB 同时发生的概率:
则说明 A 与 B 独立。在上文中可以理解为穿长裤穿短裤互不影响,而在文本分类中则意味着一个单词出现与其周围相邻的单词没有关系。
- 等重要性
即每个特征同等重要,没有权重也没有区分对待。
这两个假设虽然在实际中往往不切实际,但是朴素贝叶斯的效果往往比较不错。
- 条件概率
解释完朴素的含义,接下来解释下贝叶斯,贝叶斯指贝叶斯公式,用来描述两个条件概率之间的关系,即:
其中 C 表示可能的分类结果,X 为数据集,这里 X 不局限于1维,也可以是多维数据。基于先验信息,我们可以获取 P(X|C)、P(C)、P(X) 就像上面的长裤问题一样,其中 P(C) 也称作先验概率,P(C|X) 称为后验概率。
- 针对文本的多维样本贝叶斯公式
上面公式 Ci 代表当前文本的类别,W 代表当前文本中的不同单词 w,由朴素的独立性可知:
所以上述公式可转换为:
Wi 为 W 文本中的不同字符,P(W|C) 为不同字符在不同类别中出现的概率,P(C) 为先验概率,即当前类别出现的概率,这样在已知样本中全部数据 W 以及对应类别 C 的情况下,就可以求取后验概率 P(C|W),即出现某条文本 text,该 text 最可能属于哪一类可能 C。
ML 中的贝叶斯方法支持多项式朴素贝叶斯 (Multinomial naive Bayes),可以处理有限支持的离散数据。从 Spark 3.0 开始,ML 开始支持 Complement naive Bayes 即多项式朴素贝叶斯的改变形式以及 Gaussian naive Bayes 即高斯朴素贝叶斯,其支持处理连续数据。
下面实现通过 Naive Bayes 算法完成对中文文本的分类,只要包含中文分词、TF-IDF、模型训练以及分类预测等内容。
case class RawDataRecord(category: String, text: String)
case class RawDataRecord 记录一条样本的类别与文本。
val spark = SparkSession
.builder //创建spark会话
.master("local") //设置本地模式
.appName("ChineseClassify") //设置名称
.getOrCreate() //创建会话变量
val sc = spark.sparkContext
sc.setLogLevel("error")
import spark.implicits._
// 实现隐式转换
val tmp = sc.textFile("./NaiveBayes/train").map {
line =>
val info = line.split(",")
RawDataRecord(info(0), info(1))
}.toDF("category", "text")
category 有多种类型,例如 IT、时尚、汽车、财经等。
- 划分数据
在上面数据的基础上 73 开划分训练集与验证集,其中 Train: 18213 Test: 7697:
// 将数据分成训练集和测试集(30%用于测试)
val Array(trainingData, testData) = tmp.randomSplit(Array(0.7, 0.3), seed = 1234L)
println(s"Train: ${trainingData.count()} Test: ${testData.count()}")
- Tokenizer 分词
前面已经讲解过,Tokenizer 负责将空格间隔的 text 文本解析为 words 数组:
// 将分好的词转换为数组 Tokenizer()只能分割以空格间隔的字符串
val tokenizer = new Tokenizer().setInputCol("text").setOutputCol("words")
tokenizer.transform(trainingData).show(5)
- HashingTF
通过 HasingTF 将文本向量化,其中 numFeatures 设定为 500000:
// 将每个词转换成Int型,并计算其在文档中的词频(TF)
val hashingTF = new HashingTF().setInputCol("words").setOutputCol("rawFeatures").setNumFeatures(500000)
hashingTF.transform(tokenizer.transform(trainingData)).show(5)
- IDF 逆向文件频率
"词频-逆向文件频率"(TF-IDF)是一种在文本挖掘中广泛使用的特征向量化方法,它可以体现一个文档中词语在语料库中的重要程度。
词语由 T 表示,文档由 d 表示,语料库由 D 表示。词频 TF(T,d) 是词语 T 在文档 d 中出现的次数。文件频率 DF(T,D) 是包含词语的文档的个数。传统的方法主要依据词频度量词语的重要性,但是容易被过多出现的停用词干扰,例如 '的'、'一个'、'你' 等等,所以除了 TF 词频外,IDF 逆向文件频率,衡量词语能提供多少信息以区分文档。
|D| 代表当前的文档总数,在当前样例场景下代表所有 text 的数量
DF(T,D) 代表包含词语的文档的个数
公式中使用 log 函数,由于 DF(T, D) <= |D| ,所以 IDF 的值一定大于0。可以看到,如果一个词在所有文档中出现,则 DF(T,D) = |D|,此时 log 1 = 0,说明当前词 T 对文档分类没有任何帮助,而当词 T 出现次数很少时,log 函数的取值会非常大,代表当前词的区分度很高。例如汽车领域,车是一个很笼统的概念,而一说劳斯莱斯则拥有很高的区分度。
- TF-IDF
TF-IDF 则是将二者对应的信息相乘,获取当前单词能提供的信息量。一个词语 T 在当前文档 d 以及全部语料 D 前提下的 TF-IDF 值等于词语 T 在当前文档 d 的词频 × 词语 T 在所有文档中的 IDF 值:
通过 tokenizer、hashingTF 与 idf 构成 Pipeline,其中 idf 负责计算文档中的词频。
val idf = new IDF().setInputCol("rawFeatures").setOutputCol("features")
// 管道技术
val pipeline = new Pipeline()
.setStages(Array(tokenizer, hashingTF, idf))
val idfModel = pipeline.fit(trainingData)
val rescaledData = idfModel.transform(trainingData)
rescaledData.take(5).foreach(row => {
println(row)
})
可以将 rescaleData 打印出来查看不同单词的 Pipeline 转换过程:
- 转换数据格式
// 转换成NaiveBayes的输入格式
val trainDataRdd = rescaledData.select($"category", $"features").map {
case Row(label: String, features: Vector) =>
println(s"Label: $label Features: ${features.size}")
LabeledPoint(label.toDouble, Vectors.dense(features.toArray))
}.count()
500000 为我们前面设定的 TF 的 numFeatures:
Label: 0 Features: 500000
Label: 0 Features: 500000
Label: 0 Features: 500000
Label: 0 Features: 500000
- 训练 Bayes 模型
// 训练一个朴素贝叶斯模型。
val model = new NaiveBayes()
.fit(trainDataRdd)
Bayes 模型提供参数 modelType 选择模型类型,Multinomial naive Bayes、Bernoulli naive Bayes、Complement baive Bayes 通常用语文档分类,使用可选参数 "multinomial"、"complement"、"bernoulli" 或 "gaussian" 选择模型训练,默认为 Multinomial。
// 对测试集做同样的处理
val testRescaledData = idfModel.transform(testData)
val testDataRdd = testRescaledData.select($"category", $"features").map {
case Row(label: String, features: Vector) =>
LabeledPoint(label.toDouble, Vectors.dense(features.toArray))
}
// 预测结果
val testPredictionAndLabel = model.transform(testDataRdd)
testPredictionAndLabel.show(1)
//测试结果评估
val evaluator = new MulticlassClassificationEvaluator()
.setLabelCol("label")
.setPredictionCol("prediction")
.setMetricName("accuracy")
//测试结果准确率
val accuracy = evaluator.evaluate(testPredictionAndLabel)
println(s"Test set accuracy = $accuracy"
对 TestData 采用相同的 pipeline 处理得到 Bayes 训练的格式,随后通过 model.transform 获得预测的结果,最终通过 MulticlassClassificationEvaluator 评估模型效果:
ModelType | Accuracy | Cost |
multinomial | 0.9055476159542679 | 266751 |
complement | 0.9120436533714434 | 291882 |
gaussian | 0.8039495907496427 | 162320 |
朴素贝叶斯大概就介绍这么多,其模型源于古典数学理论具有稳定的分类效率,其具备下述优缺点:
✅ 适合小规模数据的多分类问题
✅ 适合增量训练
✅ 对缺失数据不敏感,算法容易理解
✅ 适合文本分类问题
❌ 其独立分布的假设前提,在现实生活中很难完全满足
❌ 其比较依赖可靠的先验概率,如果假设的先验模型不够准确会导致模型效果很差