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 版权协议,转载请附上原文出处链接和本声明。