【前言】在很多业务场景建模中,都会遇到大规模特征,也就是ID特征的一种形式。比如,电商平台有1亿用户,围绕着用户本身会有很多静态和动态特征,组合起来就看到所有用户对应所有维度的结果(如0和1),这个也是一个大稀疏矩阵。同样,对于商品也可以这样处理,而这一切的特征信息(离散和连续)在进入模型前,都可以预先进行数据编码,比如One-Hot Encoding、Dummy Encoding与Effect Encoding等,通过这样的编码处理后,总体的特征维度会变得特别大(高维建模的一种形式),间接性也会提升模型效果。因此,本文将会针对数据预处理中的数据编码进行扩展与实践,而针对稀疏矩阵处理、正则化处理、业务场景应用与优化,将不属于本文的介绍范围。
说明:此文首发于 数据重构未来 知识星球。为了便于大家理解后去实践,这里直接贴出原文(附原码),同时核心代码见于星球里!
一、 关于数据编码介绍
这里会依次对上述提到的数据编码(One-Hot Encoding、Dummy Encoding与Effect Encoding)形式进行介绍,具体如下所示:
① One-Hot编码
即独热编码,又称一位有效编码,其方法是使用N位状态寄存器来对N个状态进行编码,每个状态都有它独立的寄存器位,并且在任意时候,其中只有一位有效。
② Dummy编码
它与One-Hot编码类似,但是相比之下会缺少一个状态位,更简洁一些,因为如果一个研究样本存在4个状态位,那只需要知道其中3个就可以推测出最后一个状态位的值了。
③ Effect编码
从直观上的结果来看,它与Dummy编码有些类似,但是在变量赋值上存在差异性,相对来说,它在回归系数的解释方面会更有优势。
案例说明:在互联网金融行业,借款用户的基本信息中都会涉及学历情况,如小学、初中、高中、本科及以上,共计4个状态。
在以往的模型中,对于这类离散特征的数据,很多时候就直接标识(0、1、2、3),这种形式存在的不足先不过多讨论,假如我们采用上述3种数据编码形式进行处理,会分别得到以下结果。
[One-Hot]
小学 -> [1,0,0,0]
初中 -> [0,1,0,0]
高中 -> [0,0,1,0]
本科及以上 -> [0,0,0,1]
[Dummy]
小学 -> [1,0,0]
初中 -> [0,1,0]
高中 -> [0,0,1]
本科及以上 -> [0,0,0]
[Effect]
小学 -> [1,0,0]
初中 -> [0,1,0]
高中 -> [0,0,1]
本科及以上 -> [1,1,1]
通过上面的例子,基本上可以看出它们之间编码的差异性,不过在模型实践(R、Python、Spark)使用和推广中,倾向性会选择One-Hot编码,虽然会存在信息冗余,但毕竟特征属性的每个值都需要在建模中考虑,而且间接性也能扩展总维度。
二、 关于使用One-Hot的合理性
对于它的优势,想必通过上面的案例也能看出来,比如不需要数据归一化处理,能够处理离散型数据特征(对于连续性数据也可以按业务需求划分区间,看作离散特征),扩充模型特征总数。
而对于这种编码形式存在的合理性,可以结合欧式空间的距离计算进行理解。在大家所接触的各类场景(回归、聚类、分类等)的模型中,都会涉及相似度的计算,相应的计算公式也有很多,但不管采取哪种方法,最关键的都是样本与样本之间,点与点的距离分析。
因此,就像One-Hot编码就可以将特征值映射到欧式空间,每种状态位对应一个点,从非数值特征量化的角度来看,并不强调属性值之间会存在相似度的差异性,所以可以结合上面的案例,实际来看看。
常规方式,对于小学、初中、高中、本科及以上,量化处理以后为0、1、2、3,结果发现不同学历之间的距离不一致(3与1、0与1),这肯定不是模型输入想看到的。
但如果采取One-Hot编码,通过欧式距离的计算,显然可以看得出来每个学历之间的距离都是sqrt(2),更为合理性。
三、 如何实践One-Hot编码
其实上面的内容都是在给大家作个简单归纳,可能有朋友以前都了解过,相对来说还是很好去理解的。唯独的困难点,我认为有三个方面,具体如下:
其一,结合大规模数据的特征样本去快速实现One-Hot编码;
其二,One-Hot编码的业务应用场景实践;
其三,One-Hot编码的特征优化策略;
本文主要会针对第一点进行介绍和实践,其余的两点,会放在后续来介绍,毕竟我也需要亲自结合业务实践才好下决定!
案例说明:在互联网金融行业,涉及资金端(理财)会经常做大量营销活动,也会与很多渠道进行合作从而获取新客户,这其中就不可避免会涉及“羊毛党(关于它的介绍,可以参考我以往的文章)”的监控防范,现在需要线下训练一个二分类模型,从而初步去识别新用户是否为“羊毛党”。
现阶段有10个训练样本(便于本文实践,数量就限制了),其中“羊毛用户”与“非羊毛用户”各占50%的比例,结合业务调研,初步筛选5个特征(手机号风险辨别、设备风险辨别、是否为代理IP、是否为高危地区、会员等级),最终的样本数据如下所示:
ID phoneRisk deviceRisk is_agentIP is_riskArea grade category
1 低 低 否 否 1 0
2 低 中 否 否 0 0
3 中 中 是 是 2 0
4 低 低 否 否 3 0
5 低 高 否 是 4 0
6 中 低 是 否 0 1
7 高 低 否 否 1 1
8 中 低 是 是 0 1
9 低 中 否 否 2 1
10 中 中 是 否 1 1
注:以上数据均为模拟,共计10个训练样本,其中phoneRisk与deviceRisk取值为低、中、高;is_agentIP与is_riskArea取值均为是、否;grade的取值为0、1、2、3、4;
【数据量化】
在实践的模型输入中,都需要将上述样本数据集进行量化,保证均为数值型,便于模型的计算,因此,量化后的数据集如下所示:
ID phoneRisk deviceRisk is_agentIP is_riskArea grade category
1 0 0 0 0 1 0
2 0 1 0 0 0 0
3 1 1 1 1 2 0
4 0 0 0 0 3 0
5 0 2 0 1 4 0
6 1 0 1 0 0 1
7 2 0 0 0 1 1
8 1 0 1 1 0 1
9 0 1 0 0 2 1
10 1 1 1 0 1 1
接下来,我们会分别采取Python、Spark与自定义方法的形式,去结合One-Hot编码规则,将上述样本数据进行预处理,同时也将会对比不同方法之间的差异性和优缺点!
【利用Python来实现One-Hot编码】
这里的核心,主要是利用sklearn库中的OneHotEncoder方法去实现,具体代码如下:
from sklearn.preprocessing import OneHotEncoder
Sample = [[0,0,0,0,1],
[0,1,0,0,0],
[1,1,1,1,2],
[0,0,0,0,3],
[0,2,0,1,4],
[1,0,1,0,0],
[2,0,0,0,1],
[1,0,1,1,0],
[0,1,0,0,2],
[1,1,1,0,1]]
Train_enc = OneHotEncoder()
Train_enc.fit(Sample)
#每个维度的取数和偏移量
print(Train_enc.n_values_)
print(Train_enc.feature_indices_)
# 样本的数据编码结果
print(Train_enc.transform(Sample).toarray())
最终的结果输出如下所示:
[3 3 2 2 5]
[ 0 3 6 8 10 15]
[[ 1. 0. 0. 1. 0. 0. 1. 0. 1. 0. 0. 1. 0. 0. 0.]
[ 1. 0. 0. 0. 1. 0. 1. 0. 1. 0. 1. 0. 0. 0. 0.]
[ 0. 1. 0. 0. 1. 0. 0. 1. 0. 1. 0. 0. 1. 0. 0.]
[ 1. 0. 0. 1. 0. 0. 1. 0. 1. 0. 0. 0. 0. 1. 0.]
[ 1. 0. 0. 0. 0. 1. 1. 0. 0. 1. 0. 0. 0. 0. 1.]
[ 0. 1. 0. 1. 0. 0. 0. 1. 1. 0. 1. 0. 0. 0. 0.]
[ 0. 0. 1. 1. 0. 0. 1. 0. 1. 0. 0. 1. 0. 0. 0.]
[ 0. 1. 0. 1. 0. 0. 0. 1. 0. 1. 1. 0. 0. 0. 0.]
[ 1. 0. 0. 0. 1. 0. 1. 0. 1. 0. 0. 0. 1. 0. 0.]
[ 0. 1. 0. 0. 1. 0. 0. 1. 1. 0. 0. 1. 0. 0. 0.]]
以上就是利用Python去实现数据编码的过程,看起来还是挺容易,不过在我看来,有几点值得担忧,具体如下:
① 人工成本参与,这里所涉及的Sample其实只是样本数据中的特征集,不涉及ID和category两个字段,需要单独维护。当然,这并不算缺点,毕竟你可以利用DataFrame读取文本数据,抽取特征集数据转换成Array,再进行数据编码;
② 使用场景限制,如果训练样本足够大,再或者未来投入于真实业务场景中,如果利用DataFrame一次性读取样本数据存放于内存中,想必是不现实的;
③ 计算效率限制,通过代码可以看出来,数据编码前期有一个训练过程fit,后期会将训练结果使用于其他数据(包括样本本身)中,讲真的,我觉得过于繁琐,而且增加了工作量;
所以,如果只是日常业务需求的分析建模,应用于事后数据挖掘和事前分析预测,不涉及线上用户的自动识别和业务决策,倒是可以利用Python去满足日常需求。
【利用Spark来实现One-Hot编码】
以前的文章就提到过,Spark目前支持4种语言,分别为R、Python、Java与Scala(原生底层语言),为了吸引更多数据科学热爱者投身于Spark的使用中,自然而然也会在Spark的ml和mllib包中开发相应的数据算法,包括本文提到的One-Hot编码。
但是毕竟Spark不完全只服务于数据挖掘类的应用,所以有些方法使用起来相对很麻烦,个性化不够高,就比如One-Hot编码而言,官方只提供了单一特征的数据编码样式,对于多特征的还需要另外开发,具体如下所示:
object OneHotEncoder {
//请查看评论中
}
最终针对单特征的数据编码(因为category有3种取值,所以对应3个状态位)结果如下所示:
1.0,0.0,0.0
0.0,1.0,0.0
0.0,0.0,0.0
1.0,0.0,0.0
以上就是利用Spark官方提供的案例去实现单一特征的数据编码,但也只是了解而已,如果想要应用于实际模型中,还是需要二次开发,接下来会针对性介绍如何去给多特征进行数据编码。
(phoneRisk deviceRisk is_agentIP is_riskArea grade category)
0,0,0,0,1,0
...
1,1,1,0,1,1
以上就是之前的样本数据(字段分割形式可多样,这里按逗号进行分割),存放于本地目录中,以onehot.txt命名。
在实践开发过程前,我们需要先介绍一个知识点,也就是RDD -> DataFrame,一方面是Spark2.X系列提倡使用后者,接口更灵活,另一方面,本文也会使用到。
SparkSQL提供了两种方式把RDD转换为DataFrame。
① 第一种通过反射(需知道schema,本文所采用);
② 第二种通过提供的接口创建schema。
注:对于第一种方法,Case classes 在 Scala 2.10 只支持最多22个特征,需自定义接口突破这个限制。
object OneHotEncoderUpdate {
//请查看评论中
}
最终的数据查询结果如下所示:
+---------+----------+----------+-----------+-----+-----------------------------+
|phoneRisk|deviceRisk|is_agentIP|is_riskArea|grade|category|
+---------+----------+----------+-----------+-----+-----------------------------+
| 0| 1| 0| 0| 0| 0|
| 1| 0| 1| 0| 0| 1|
| 1| 0| 1| 1| 0| 1|
+---------+----------+----------+-----------+-----+-----------------------------+
以上就完成了数据从本地目录(也可以HDFS)中读取,生成RDD,并最终转化为DataFrame来进行One-Hot数据编码了(这里贴出核心代码)。
val colTips = Array("phoneRisk","deviceRisk","is_agentIP","is_riskArea","grade")
val index_transformers:Array[org.apache.spark.ml.PipelineStage] = colTips.map(record =>{
new StringIndexer().setInputCol(record).setOutputCol(s"${record}_index")
}
)
val index_pipeline = new Pipeline().setStages(index_transformers)
val index_model = index_pipeline.fit(value)
val df_indexed = index_model.transform(value)
val indexColumns = df_indexed.columns.filter(x => x contains "index")
val one_hot_encoders:Array[org.apache.spark.ml.PipelineStage] = indexColumns.map(record => {
new OneHotEncoder().setInputCol(record).setOutputCol(s"${record}_vec")
}
)
val pipeline = new Pipeline().setStages(index_transformers ++ one_hot_encoders)
val model = pipeline.fit(value)
val ouputResult = model.transform(value).select("category","phoneRisk_index_vec","deviceRisk_index_vec","is_agentIP_index_vec","is_riskArea_index_vec","grade_index_vec").map(record =>{
val value = new StringBuilder()
val tips = Array("phoneRisk_index_vec","deviceRisk_index_vec","is_agentIP_index_vec","is_riskArea_index_vec","grade_index_vec")
value.append(record.apply(0)).append(",")
for(tip <- tips){
record.getAs[SparseVector](tip).toArray.map(data =>{
value.append(data).append(",")
}
)
value.append(0.0).append(",")
}
value.toString().substring(0,value.length-1)
}
)
最终的数据编码结果如下所示(可以对照Python的编码结果,by:乐平汪二):
0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,1.0,0.0,1.0,0.0,0.0,0.0,0.0
0,1.0,0.0,0.0,0.0,1.0,0.0,1.0,0.0,1.0,0.0,0.0,1.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,0.0,0.0,0.0,1.0,0.0,0.0
0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,1.0,0.0,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,0.0,0.0,0.0,0.0,1.0,0.0
1,0.0,1.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,0.0
1,0.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,1.0,0.0,1.0,0.0,0.0,0.0,0.0
1,0.0,1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0
1,1.0,0.0,0.0,0.0,1.0,0.0,1.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0
1,0.0,1.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,1.0,0.0,0.0,0.0,0.0
同样也能对样本数据的特征进行数据编码,但是不管是Python,还是Spark,即使写了很多代码,但是简单来说也仅仅是为了模型数据的预处理,并没有过多作用。
因此,为了得了这个结果,当数据量很大时,而且特征数也在一定数量下,除了Spark的这种形式较为合适外,我们是否还有更轻松、更高效的方式去针对数据集进行预处理呢?
【利用Scala自定义实现One-Hot编码】
相比繁琐的代码和处理逻辑,我更愿意选择个性化的方法去满足样本数据预处理的需求,而且只需要针对特征属性与属性值进行单独维护即可,具体如下所示(这里举一个简单的案例):
def OneHot(inputValue:String,featureSet:Array[String]):String={
//请查看评论中
}
就比如上面的OneHot方法,在确保数据质量可靠(数据清洗和过滤需要把关)的情况下,每一行样本数据,通过字符分割获取到相应的字段值后,我只需要向OneHot方法传入两个值(字段值,该特征的属性值数组),即可返回该字段值的独热编码,还是挺简单的!
def main(args:Array[String]):Unit={
val value = "硕士"
val sets = Array("小学","中学","高中","大学","硕士")
println(OneHot(value,sets))
}
具体的输出结果为:0,0,0,0,1
因此,如果正式使用于业务场景建模中,假设LR模型有15个特征,这里只需要单独创建一个配置文件,负责维护15个特征的属性名+属性值集,这样的话就可以在数据预处理中,直接对样本数据、线上数据进行One-Hot编码。
四、 本文总结
这篇文章开头用了少量的篇幅介绍了数据编码的知识点,毕竟结合数据就能够很快理解这个编码的结果形式,所以这不是难点。而接下来,在所谓的三个难点之中(参考第3小节),本文的核心是以工程的形式,针对特征样本去快速实现One-Hot编码。
这里分别介绍了Python、Spark以及自定义的形式,各有优缺点,以及应用场景,因此大家在实际业务场景的建模中,需要针对性去评估。
的确,本文太多代码了,很多偏业务的朋友可能消化有些吃力,但是我认为这些代码都还算好理解,毕竟我这周也是从调研、理解、实践、开发、总结,这过程来的。而且这个知识点对于模型优化还是很重要,另外在后期内容中,我还会结合业务实践,去与大家共同分享数据编码接下来的两个困难点!
<完>
更多精彩章节,可以加入知识星球阅读...
推荐一下我的新书,《轻松学数据挖掘—算法、场景与数据产品》!
我的自述:
我是乐平汪二(@lp_wanger),一个有大数据情怀的小学生,也是《轻松学大数据挖掘》本书的作者!这本书记录了我个人的数据成长历程,能够让你了解到大数据中的数据挖掘模式。而本书对于我的意义更多是一份回忆,一种年轻时的状态,希望本书对大家有所帮助。