结合源码分析Spark中的Accuracy(准确率), Precision(精确率), 和F1-Measure
Posted 小帆的帆
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了结合源码分析Spark中的Accuracy(准确率), Precision(精确率), 和F1-Measure相关的知识,希望对你有一定的参考价值。
转载请标明出处:小帆的帆的专栏
例子
某大学一个系,总共100人,其中男90人,女10人,现在根据每个人的特征,预测性别
Accuracy(准确率)
Accuracy=预测正确的数量需要预测的总数
计算
由于我知道男生远多于女生,所以我完全无视特征,直接预测所有人都是男生
我预测所的人都是男生,而实际有90个男生,所以
预测正确的数量 = 90
需要预测的总数 = 100
Accuracy = 90 / 100 = 90%
问题
在男女比例严重不均匀的情况下,我只要预测全是男生,就能获得极高的Accuracy。
所以在正负样本严重不均匀的情况下,Accuracy指标失效
Precision(精确率), Recall(召回率)
. | 实际为真 | 实际为假 |
---|---|---|
预测为真 | TP | FP |
预测为假 | FN | TN |
# 前面的T和F,代表预测是否正确
# 后面的P和N,代表预测是真还是假
TP:预测为真,正确了
FP:预测为真,结果错了
TN:预测为假,正确了
FN:预测为假,结果错了
Precision=TPTP+FP=预测为真,实际也为真预测为真的总数
Recall=TPTP+FN=预测为真,实际也为真实际为真的总数
计算
注意
:在正负样本严重不均匀的情况下,正样本必须是数量少的那一类。这里女生是正样本。是不是女生,是则预测为真,不是则预测为假。
- 如果没有预测为真的情况,计算时分母会为0,所以做了调整,也容易比较Accuracy和Precision, Recall的区别
. | 实际为真 | 实际为假 |
---|---|---|
预测为真 | 1 | 0 |
预测为假 | 10 | 89 |
Accuracy = (1 + 89)/ (1 + 0 + 10 + 89) = 90 / 100 = 0.9
Precision = 1 / 1 + 0 = 1
Recall = 1 / 1 + 10 = 0.09090909
注意
:为方便与后面Spark的计算结果对比,无限循环小数,我们不做四合五入
问题
虽然我们稍微调整了预测结果,但是Accuracy依然无法反应预测结果。
而Precision在这里达到了1,但是Recall却极低。因此Precision,Recall的组合能够反应我们的预测效果不佳。
但是Precision,Recall在对比的时候会出现问题,比如一个模型的Precision是0.9,Recall是0.19,那么与上面的1和0.0909对比,哪个模型更好呢?
所以我们需要一个指标,能够综合的反应Precision和Recall
F1-Measure
F1值就是Precision和Recall的调和均值
1F1=1Precision+1Recall
整理后:
F1=2×Precision×RecallPrecision+Recall
计算
计算上面提到的对比情况
F1 = (2 * 1 * 0.09090909) / 1 + 0.09090909 = 0.1666666
F1 = (2 * 0.9 * 0.19) / 0.9 + 0.19 = 0.3137
很显然后一种更好
调整Precision, Recall的权重
Fa=(a2+1)×Precision×Recalla2×(Precision+Recall)
当a等于1时,Precision,Recall各占50%,就是F1-Measure了
Spark源码分析
Spark中API计算Precision,Recall,F1
用Spark API计算出上面我们手工计算出的值
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
import org.apache.spark.SparkConf, SparkContext
object Test
def main(args: Array[String])
val conf = new SparkConf().setAppName("test").setMaster("local") // 调试的时候一定不要用local[*]
val sc = new SparkContext(conf)
sc.setLogLevel("ERROR")
// 我们先构造一个与上文一样的数据
/**
* 实际为真 实际为假
* 预测为真 1 0
* 预测为假 10 89
*/
// 左边是预测为真的概率,右边是真实值
val TP = Array((1.0, 1.0)) // 预测为真,实际为真
val TN = new Array[(Double, Double)](89) // 预测为假, 实际为假
for (i <- TN.indices)
TN(i) = (0.0, 0.0)
val FP = new Array[(Double, Double)](10) // 预测为假, 实际为真
for (i <- FP.indices)
FP(i) = (0.0, 1)
val all = TP ++ TN ++ FP
val scoreAndLabels = sc.parallelize(all)
// 打印观察数据
// scoreAndLabels.collect().foreach(println)
// println(scoreAndLabels.count())
// 到这里,我们构造了一个与上文例子一样的数据
val metrics = new BinaryClassificationMetrics(scoreAndLabels)
// 下面计算的值,我们先只看右边的数,它表示计算的precision,recall,F1等
// 左边是Threshold,后面会细说
/**
* (1.0,1.0) // precision跟我们自己之前计算的一样
* (0.0,0.11) // 这是什么?先不管
*/
metrics.precisionByThreshold().collect().foreach(println)
println("---")
/**
* (1.0,0.09090909090909091) // recall跟我们自己之前计算的一样
* (0.0,1.0) // 先忽略
*/
metrics.recallByThreshold().collect().foreach(println)
println("---")
/**
* (1.0,0.16666666666666669) // f1跟我们自己之前计算的一样
* (0.0,0.19819819819819817) // 先忽略
*/
metrics.fMeasureByThreshold().collect().foreach(println)
至此,我们用Spark API计算出了各个值。但是有几个疑问
- 无论是precision,recall,还是fMeasure,后面都跟一个ByThreshold,为什么?
- 这三个指标,不应该是一个数嘛,为什么返回一个RDD,里面包含一堆数?
要弄清楚,就出要知道它们是怎么计算出来的
计算分析(以Precision为例)
- 从代码的角度,一步步跟踪到Precision的计算公式,公式找到了值也就算出来了
- 从数据的角度,你的输入数据是怎么一步步到结果的
代码角度
# 类声明
# scoreAndLabels是一个RDD,存放预测为真的概率和真实值
# numBins,先忽略
class BinaryClassificationMetrics (val scoreAndLabels: RDD[(Double, Double)], val numBins: Int)
调用BinaryClassificationMetrics的precisionByThreshold方法计算,precision
new BinaryClassificationMetrics(scoreAndLabels).precisionByThreshold()
跟踪进入precisionByThreshold方法
def precisionByThreshold(): RDD[(Double, Double)] = createCurve(Precision)
# 调用了createCurve(Precision)
# precisionByThreshold返回的RDD,就是这个createCurve方法的返回值
# 两个问题
# createCurve是什么?
# 参数Precision又是什么?
跟踪进入createCurve方法
/** Creates a curve of (threshold, metric). */
private def createCurve(y: BinaryClassificationMetricComputer): RDD[(Double, Double)] =
// confusions肯定是一个RDD,因为它调用了map,然后就作为返回值返回了
// 所以confusions是关键,对它做变换,就能得到结果
confusions.map case (s, c) =>
// precisionByThreshold返回的RDD,左边是threshold,右边是precision
// 所以这里的s,就是threshold
// y(c),就是precision
// y是传入的参数,也就是createCurve(Precision)中的,Precision
// 下面就先看看Precision是什么
(s, y(c))
跟踪进入Precision
// 上文中的y(c),也就是Precision(c),这语法,自然是调用apply方法
/** Precision. Defined as 1.0 when there are no positive examples. */
private[evaluation] object Precision extends BinaryClassificationMetricComputer
override def apply(c: BinaryConfusionMatrix): Double =
// 看名字numTruePositives,就是TP的数量嘛
// totalPositives = TP + FP
val totalPositives = c.numTruePositives + c.numFalsePositives
// totalPositives为0,也就一个真都没预测
if (totalPositives == 0)
// 0 / 0,会出错,这里是直接返回1
1.0
else
// 公式出现
// Precision = TP / (TP + FP)
c.numTruePositives.toDouble / totalPositives
到这里找到了Precision的计算公式,但是上面提到的两个疑问,还没有解决,Threshold怎么回事,返回RDD干嘛?
但是通过上面的分析,我们找到了线索,confusions这个通过变换就能出结果的变量,也许就是答案。
数据角度
跟踪到confusions的声明
private lazy val (
cumulativeCounts: RDD[(Double, BinaryLabelCounter)],
confusions: RDD[(Double, BinaryConfusionMatrix)]) =
// ... 省略了60行左右
(cumulativeCounts, confusions)
这60行里做了什么,我们拷贝出来,一步步分析
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
import org.apache.spark.rdd.RDD
import org.apache.spark.SparkConf, SparkContext
object Test
def main(args: Array[String])
val conf = new SparkConf().setAppName("test").setMaster("local") // 调试的时候一定不要用local[*]
val sc = new SparkContext(conf)
sc.setLogLevel("ERROR")
val TP = Array((1.0, 1.0))
val TN = new Array[(Double, Double)](89)
for (i <- TN.indices)
TN(i) = (0.0, 0.0)
/**
* *******这里改了********这里改了********这里改了*****
*/
// 从10改成了5,有5个样本有60%的概率是真的;另外5个设置成了40%,在下面
val FP1 = new Array[(Double, Double)](5)
for (i <- FP1.indices)
FP1(i) = (0.6, 1)
val FP2 = new Array[(Double, Double)](5) // 有5个样本有40%的概率是真的
for (i <- FP2.indices)
FP2(i) = (0.4, 1)
val all = TP ++ TN ++ FP1 ++ FP2
val scoreAndLabels = sc.parallelize(all, 2) // 调整并行度为2,后面会说,为什么要调整
// 打印观察数据
scoreAndLabels.collect().foreach(println)
val metrics = new BinaryClassificationMetrics(scoreAndLabels)
// 先看下调整后的结果
// 左边一列多了0.6,和0.4,猜的话,应该是因为上面的概率我们添加了0.6和0.4<以上是关于结合源码分析Spark中的Accuracy(准确率), Precision(精确率), 和F1-Measure的主要内容,如果未能解决你的问题,请参考以下文章
结合源码分析Spark中的Accuracy(准确率), Precision(精确率), 和F1-Measure
Accuracy(准确率), Precision(精确率), 和F1-Measure, 结合Spark源码分析
准确率(Accuracy) 精确率(Precision) 召回率(Recall)和F1-Measure(精确率和召回率的调和平均值)
结合Spark源码分析, combineByKey, aggregateByKey, foldByKey, reduceByKey