一.引言
机器学习中最基础的一步就是数据的特征工程,这其中最常见的就是数值型特征的分桶,下面使用两种方法对数值型特征分桶并对比效率。给定数值型特征划分的连续递增(保序) 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)