Spark异常点检测算法——孤立森林模型
异常检测的特性
在生产中通常要进行异常数据检测,异常检测又被称为“离群点检测” (outlier detection),一般具有两个特性
- 异常数据跟大部分样本数据不太一样
- 异常数据在整体数据中的占比比重较小
以用户行为的埋点为例,这类数据通常对于异常数据的界限没有一个明确的划分。因此SVM、逻辑回归等这类需要大量正向、负向样本的算法并不适用于上述情况。
对于这类没有确定结果的数据来说,我们期望拥有一个无监督模型,根据样本间的相似性对样本集进行分类,从而检测出对应的异常数据。
网上搜索的关于异常点检测的无监督学习模型中,有一个准度和效率双佳的异常点检测算法 —— isolation forest(孤立森林)
孤立森林算法适用于连续数据的异常检测,将异常定义为“容易被孤立的离群点”,可以理解为分布稀疏且离密度高的群体较远的点。用统计学来解释,在数据空间里面,分布稀疏的区域表示数据发生在此区域的概率很低,因而可以认为落在这些区域里的数据是异常的。
孤立森林算法原理
简单解释一下什么是孤立森林: 「假设我们用一个随机超平面来切割(split)数据空间(data space), 切一次可以生成两个子空间。
之后我们再继续用一个随机超平面来切割每个子空间,循环下去,直到每子空间里面只有一个数据点为止。
直观上来讲,我们可以发现那些密度很高的簇是可以被切很多次才会停止切割,但是那些密度很低的点很容易很早的就停到一个子空间里了」。
以上图为例,d是最早被随机平面切割出来的点,那么d最有可能是异常点,因为最早它就被孤立了。
Spark内的孤立森林模型
由于SparkML内并没有孤立森林的相关模型(SparkML本身集成的模型就很少= =),因此需要印入外部框架sparkling water。
sparkling water将h2o.ai和Spark相结合,在spark平台上运行h2o服务。
maven(基于spark2.3.*):
<dependency>
<groupId>ai.h2o</groupId>
<artifactId>sparkling-water-package_2.11</artifactId>
<version>3.32.1.3-1-2.3</version>
</dependency>
example:
import ai.h2o.sparkling._
val conf = new H2OConf().setLogLevel("ERROR")
val h2oContext = H2OContext.getOrCreate(conf)
//数据处理,这里将数据按两个维度聚合
//source:位置
//ts: ${start_hour}-${start_minutes},${end_hour}-${end_minutes}
val exposure = spark.readspark.read.parquet("/tmp/test_5m_exposure").select(
$"*",
concat_ws(
"-", hour($"time.start"), minute($"time.start")
).as("start"),
concat_ws(
"-", hour($"time.end"), minute($"time.end")
).as("end"),
$"num".cast("double") as "count"
)
.select(
$"*",
concat_ws(",",$"start",$"end") as "ts"
)
//孤立森林模型参数设置
val estimator = new H2OIsolationForest()
//设置参与模型训练、评估的列名
.setFeaturesCols("source", "ts", "count")
//设置孤立树的个数,通常来说树越多评估越准确
.setNTrees(512)
//设置分类列名
.setColumnsToCategorical("source", "ts")
//模型训练
val model = estimator.fit(exposure)
//模型存入hdfs
model.save("/tmp/test_model")
//test
val test = spark.sparkContext.makeRDD(
Seq(
("xxx", "17-30,17-35", 0.0),
("xxx", "17-30,17-35", 100.0),
("xxx", "17-30,17-35", 29.0),
("xxx", "17-30,17-35", 1000.0),
("xxx", "17-30,17-35", 1000.0)
)
).toDF("source", "ts", "count")
model.transform(exposure).where($"ts".equalTo("17-30,17-35")).show(false)
model.transform(test).show(false)
show:
schema:
root
|-- source: string (nullable = true)
|-- ts: string (nullable = true)
|-- count: double (nullable = false)
|-- detailed_prediction: struct (nullable = true)
| |-- score: double (nullable = false)
| |-- normalizedScore: double (nullable = false)
|-- prediction: double (nullable = true)
正常样本数据:
+--------+-----------+-------+---------------------------+----------+
|source |ts |count |detailed_prediction |prediction|
+--------+-----------+-------+---------------------------+----------+
|xxx|17-30,17-35|11317.0|[6.46, 0.08928571428571429]|6.46 |
|xxx|17-30,17-35|19030.0|[6.0, 0.2261904761904762] |6.0 |
|xxx|17-30,17-35|19251.0|[5.98, 0.23214285714285715]|5.98 |
|xxx|17-30,17-35|8974.0 |[6.48, 0.08333333333333333]|6.48 |
|xxx|17-30,17-35|21131.0|[5.22, 0.4583333333333333] |5.22 |
|xxx|17-30,17-35|5943.0 |[6.46, 0.08928571428571429]|6.46 |
|xxx|17-30,17-35|7773.0 |[6.5, 0.07738095238095238] |6.5 |
|xxx|17-30,17-35|13362.0|[6.46, 0.08928571428571429]|6.46 |
|xxx|17-30,17-35|17062.0|[6.24, 0.15476190476190477]|6.24 |
+--------+-----------+-------+---------------------------+----------+
测试异常数据:
+--------+-----------+-------------+--------------------------+----------+
|source |ts |count |detailed_prediction |prediction|
+--------+-----------+-------------+--------------------------+----------+
|xxx|17-30,17-35|0.0 |[4.62, 0.6369047619047619]|4.62 |
|xxx|17-30,17-35|100.0 |[4.62, 0.6369047619047619]|4.62 |
|xxx|17-30,17-35|29.0 |[4.62, 0.6369047619047619]|4.62 |
|xxx|17-30,17-35|1000.0 |[4.96, 0.5357142857142857]|4.96 |
|xxx|17-30,17-35|9.999999999E9|[3.68, 0.9166666666666666]|3.68 |
+--------+-----------+-------------+--------------------------+----------+
可以调用model.getModelDetails
获取当前模型信息
//截取的部分信息
"training_metrics": {
"model": {
"name": "IsolationForest_model_Java_1622706331444_1",
"type": "Key\u003cModel\u003e",
"URL": "/3/Models/IsolationForest_model_Java_1622706331444_1"
},
"model_checksum": 6222419329611649696,
"frame": {
"name": "frame_rdd_34579938405",
"type": "Key\u003cFrame\u003e",
"URL": "/3/Frames/frame_rdd_34579938405"
},
"frame_checksum": 3954439017226277896,
"description": "Metrics reported on Out-Of-Bag training samples",
"model_category": "AnomalyDetection",
"scoring_time": 1622706342432,
"MSE": "NaN",
"RMSE": "NaN",
"nobs": 2590,
"custom_metric_value": 0.0,
"mean_score": 6.086300520578171,
"mean_normalized_score": 0.24640110809612353
}
mojo模型的使用
sparkling water支持将训练后的模型保存为mojo格式。
在非spark应用上,可以使用sparkling water的相应api读取模型进行预测
example:
//模型加载至内存
val is = new FileInputStream("/path")
val reader = MojoReaderBackendFactory.createReaderBackend(is, MojoReaderBackendFactory.CachingStrategy.MEMORY)
val mojoModel = ModelMojoReader.readFrom(reader)
val config = new EasyPredictModelWrapper.Config()
config.setModel(mojoModel)
config.setConvertUnknownCategoricalLevelsToNa(true)
val easyPredictModelWrapper = new EasyPredictModelWrapper(config)
//数据预测 test1
val row:RowData=new RowData()
row.put("source","xxx")
row.put("ts","17-30,17-5")
row.put("count","60.0")
val normalizedScore=easyPredictModelWrapper.predictAnomalyDetection(row).normalizedScore
println(normalizedScore) //result:0.92
//数据预测 test2
row.clear()
row.put("source","xxx")
row.put("ts","17-30,17-5")
row.put("count","2777.0")
val normalizedScore2=easyPredictModelWrapper.predictAnomalyDetection(row).normalizedScore
println(normalizedScore2) //result:0.12666666666666668
还有很多地方不太明确,这里占坑,后续继续补充。。