Scala – 数值型特征分桶

一.引言

机器学习中最基础的一步就是数据的特征工程,这其中最常见的就是数值型特征的分桶,下面使用两种方法对数值型特征分桶并对比效率。给定数值型特征划分的连续递增(保序) boundary 如下:

val boundary = Array(0.0, 1, 2, 3, 5, 10, 20, 50, 75, 99)
val numCuts = boundary.zipWithIndex

给定的边界共包含10个分界点,所以可以生成 0-10 共计11个索引。

二.遍历法

遍历法的思想很简单,给定特征值 FeatValue,由于 boundary 是保序的,所以依次遍历分界点与 featValue,当对应值小于分界点时,返回对应位置的 index 即可,这里 index 的获取通过 scala 自带的 zipWithIndex 得到。

  def loopCut(featValue: Double, numCuts: Array[(Double, Int)]): Int = {
    // 获取最大分界点
    val maxCuts = numCuts.map(_._1).max
    // 判断异常值
    if (featValue.equals(NaN)) return 0

    var cutNum = 0
    var state = true
    val it = numCuts.iterator
    while (state && it.hasNext) {
      val value = it.next()
      val edge = value._1
      val index = value._2
      if (edge >= featValue.toDouble) {
        cutNum = index
        state = false
      }
    }

    // 特征值超过所有边界点,取 boundary 长度作为 cut
    if (maxCuts < featValue.toDouble) cutNum = numCuts.size

    cutNum
  }

三.二分法

遍历的方法容易理解,但是需要靠后的特征值,需要遍历的次数大幅增加,所以可以通过二分法缩减寻找目标值的速度。

  def binaryCut(featValue: Double, numCuts: Array[(Double, Int)]): Int = {
    var cutNum = 0
    val cutArr = numCuts.map(_._1)

    // 与起始、终止值相同直接返回
    val numValue = featValue.toDouble
    if (numValue <= cutArr.head) {
      cutNum = 0
      return cutNum
    }

    // 判断边界值
    if (numValue > cutArr.last) {
      cutNum = numCuts.length
      return cutNum
    }

    // 二分查找
    var left = 0
    var right = cutArr.length - 1
    while (left <= right) {
      val middle = left + (right - left) / 2
      if (cutArr.apply(middle) >= numValue && cutArr.apply(middle - 1) < numValue) {
        cutNum = middle
        return cutNum
      } else if (cutArr.apply(middle) < numValue) {
        left = middle + 1
      } else if (cutArr.apply(middle) > numValue) {
        right = middle - 1
      } else {
        return cutNum
      }
    }

    cutNum = numCuts.length
    cutNum
  }

四.效率对比

  def runCut(epoch: Int, cutValue: Double, numCuts: Array[(Double, Int)]): Unit = {
    val st = System.currentTimeMillis()

    val value = cutFunction(cutValue, numCuts)

    (0 until epoch).foreach(epoch => {
      newCut(cutValue, numCuts)
    })

    val end = System.currentTimeMillis() - st
    println(s"[Log] Num: $cutValue CutNum: $value Epoch: $epoch Cost: $end")

  }
[Loop] Num: 88.0 CutNum: 9 Epoch: 1000 Cost: 55
[Binary] Num: 88.0 CutNum: 9 Epoch: 1000 Cost: 6

分别选取 loopCut 和 binaryCut 作为 cutFunction 使用不同 FeatValue 进行 epoch 次测试:

Function FeatValue Epoch Cost(ms)
LoopFunction 遍历查找 1.5 1000 59
BinaryFunction 二分查找 1.5 1000 9
LoopFunction 遍历查找 5.5 1000 63
BinaryFunction 二分查找 5.5 1000 13
LoopFunction 遍历查找 88 1000 55
BinaryFunction 二分查找 88 1000 6

选取前,中,后三个数值进行 cut,二分查找效率都优于遍历查找,在极端情况 [所有 FeatValue 均小于第一个分界点时] 下,遍历查找的速度与二分查找相近。

五.扩展

上述方法主要应用于 scala 工程端的特征处理,在 python tf 端可以通过 tf.feature_column 实现数值型特征分桶的特征处理任务,例如: numeric_column + bucketized_column

numeric_column: 获取离散数值

buketized_column: 离散数值分桶,需给定 boundaries

    cos_index = tf.feature_column.bucketized_column(
        tf.feature_column.numeric_column(key='cos', shape=(1,),
                                         default_value=0,
                                         dtype=tf.dtypes.float32),
        boundaries=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
 
    product_dict = {"cos": np.random.random(size=(10, 1))}
    feature_layer = tf.keras.layers.DenseFeatures(cos_index)
    output = feature_layer(product_dict)
    print(output)
    print("=========================")

同上,boundary 的分界点有 N=9 个,则特征会被划分至 N+1=10 个 Filed 中:

tf.Tensor(
[[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]], shape=(10, 10), dtype=float32)

版权声明:本文为BIT_666原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>