Spark引用h2o框架,实施线上异常点检测——孤立森林模型(isolation forest)

Spark异常点检测算法——孤立森林模型

异常检测的特性

在生产中通常要进行异常数据检测,异常检测又被称为“离群点检测” (outlier detection),一般具有两个特性

  • 异常数据跟大部分样本数据不太一样
  • 异常数据在整体数据中的占比比重较小

以用户行为的埋点为例,这类数据通常对于异常数据的界限没有一个明确的划分。因此SVM、逻辑回归等这类需要大量正向、负向样本的算法并不适用于上述情况。

对于这类没有确定结果的数据来说,我们期望拥有一个无监督模型,根据样本间的相似性对样本集进行分类,从而检测出对应的异常数据。

网上搜索的关于异常点检测的无监督学习模型中,有一个准度和效率双佳的异常点检测算法 —— isolation forest(孤立森林)

孤立森林算法适用于连续数据的异常检测,将异常定义为“容易被孤立的离群点”,可以理解为分布稀疏且离密度高的群体较远的点。用统计学来解释,在数据空间里面,分布稀疏的区域表示数据发生在此区域的概率很低,因而可以认为落在这些区域里的数据是异常的。

孤立森林算法原理

简单解释一下什么是孤立森林: 「假设我们用一个随机超平面来切割(split)数据空间(data space), 切一次可以生成两个子空间

之后我们再继续用一个随机超平面来切割每个子空间,循环下去,直到每子空间里面只有一个数据点为止。

直观上来讲,我们可以发现那些密度很高的是可以被切很多次才会停止切割,但是那些密度很低的点很容易很早的就停到一个子空间里了」。

v231512a0116d69174ac1b80141c191416_720w.png

以上图为例,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

还有很多地方不太明确,这里占坑,后续继续补充。。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值