准备了很久,终于开始决定开始写关于机器学习相关的文章。深刻体会刚刚涉足一个新领域时的那种茫然和不知所措,而后在各个大神的文章帮助下渐渐走出了自己的一条路。现在想以分享设计方案的方式回馈技术社区和技术分享平台。虽然,这些不一定是最优的设计方案,希望这些技术方案对正在开发中正在迷惑的你有所帮助。
【背景】
在spark的架构中MLlib的工具库非常的全面,几乎包含大部分的机器学习的算法和场景。但是有些组件包却没有实现或者实现了没有暴露出来,在这些场景下用户就不得不自己实现相关的工程包的封装。最近一段时间,将会逐步分享一系列的机器算法的spark工程实现,其中包含IV值计算和分箱计算。
【概念准备】
今天打算分享的主题是信息熵(information gain),我们先来看一下百度词条对于信息熵的定义和设计背景。信息熵,是一个数学上颇为抽象的概念,在这里不妨把信息熵理解成某种特定信息的出现概率。信息量度量的是一个具体事件发生了所带来的信息,而熵则是在结果出来之前对可能产生的信息量的期望——考虑该随机变量的所有可能取值,即所有可能发生事件所带来的信息量的期望。
【计算公式】
【spark实现单列计算】
df.cache()
val count = df.count()
val percentEtlPlan = col("group_count")/count
val entropyEtlPlan = sum(-percentEtlPlan*log(percentEtlPlan)).alias("entropy")
df.groupBy("a").count()
.select(col("count").alias("group_count"))
.select(entropyEtlPlan)
.head().getAs[Double]("entropy")
【优化】
信息熵在我们项目中,主要目的是为了计算后期的信息增益和增益率做准备。是用来做特征筛选工具策略,在工程调用上会有大量的列需要计算信息熵。假如需要计算一万列的信息熵,即使一秒钟计算一列也需要三个多小时,更何况一秒钟也无法完成这些计算,这个时间消耗是无法接受的。所以后面对这个计算又做了多列批量计算的优化。根据上面的代码大家可以看出,计算一列需要两次的shuffle,一万列就需要两万次的shuffle。主要的优化点是考虑,多列计算时shuffle是否可以合并,在新的设计中规约了shuffle次数,以有限次shuffle来完成无数个列信息熵的计算。
/**
* @desc 计算信息熵
* @param df
* @param calCols 需要计算的列
* @param dfRowNum 当前数据集总行数
* @return
*/
override def calculator(
df: DataFrame,
calCols: Array[String],
ycol: String,
dfRowNum: Double
): Map[String, Double] = {
if (df.storageLevel == StorageLevel.NONE) {
df.persist(StorageLevel.DISK_ONLY)
}
val result = if (dfRowNum == 0) {
calCols.map(col => (col, 0.0D)).toMap
} else {
/** 将列分成多组,一次计算多列的信息熵 */
val calculaAggColNum = PropertiesUtils.calculaAggColNum
val colsGroup: Array[Array[String]] = calCols.sliding(calculaAggColNum, calculaAggColNum).toArray
val result = colsGroup.map(cols => {
/** 拼接数据行转列逻辑计划 */
val expodeEtlPlan = explode(array(cols.map(colT => array(lit(colT), col(colT))): _*)).alias("arr")
/** 拼接每组百分比的计算的逻辑计划 */
val percent = col("count") / dfRowNum.toDouble
/** 信息计算逻辑 */
val percentMultLogPercent = -percent * log(percent)
df.select(expodeEtlPlan)
.select(col("arr").getItem(0).alias("key"), col("arr").getItem(1).alias("value"))
.groupBy(col("key"), col("value"))
.agg(count("key").alias("count"))
.select(percentMultLogPercent.alias("percent"), col("key"))
.groupBy("key")
.agg(sum(col("percent")).alias("percent_sum"))
.collect()
.map(row => (row.getAs[String]("key"), row.getAs[Double]("percent_sum")))
.toMap
})
if (result.isEmpty) Map[String, Double]() else result.reduce(_ ++ _)
}
val remainCols = (calCols.toSet -- result.keySet).map((_, 0.0D)).toMap
result ++ remainCols
}
【设计思路】
在计算多列的场景,需要将多列拼接成两列(列名,列值),如下
后面按照group by(key,value)计算count,就可以计算出每列的每个值出现的频率。再做百分比转换以及取对数,最后加和。
【后记】
可能会有人提出异议,在第一步将多列转换成两列的过程中,会不会导致行数暴增导致内存内存膨胀,并且在explode拼接过程中消耗大量时间。根据实验结果和spark的特性分析,以上问题不存在,因为不会存在只存放key和value的dataFrame的这个中间结果,只是逻辑上会存在这个表达。实际计算过程中,原始多列在shuffle过程中就开始计算count,只留下不重复的key,value,count三列。所以不会存在内存膨胀现象。我们一个计算批次(2亿条,7000列)的数据集上,同时计算信息熵的情况下才用到20分钟左右,并且计算过程中GC时间稳定,内存使用剧增不明显。