亚马逊AWS官方博客
Data-centric AI之特征工程(第二讲)
在Data-centric AI之特征工程第一讲中,我们介绍了特征相关的概念以及连续特征和category特征的特点,归纳了“好的特征”应有的特质。在此基础上,介绍了特征预处理的三个子步骤即字符串特征转换为数值特征,异常值处理和缺失值处理。今天我们接着介绍特征预处理的三个子步骤。
特征预处理之样本类别不均衡处理
样本类别不均衡指的是分类任务训练集中的标注/label取值的分布不是均衡的,比如对于二分类任务,一共有1万条样本的训练集中,label为1的样本有1000条,label为0的样本有9000条,正负样本比例是1:9,这个时候我们就说这个训练集是样本类别不均衡的。严格意义上说,样本类别不均衡处理属于样本工程的范畴,但是由于它经常和其他的特征预处理子步骤放在一起讨论,所以我也把它放在了特征工程中来介绍。样本类别不均衡可能会把模型带偏,因为让它看了更多的大类别的样本。在实际的ML项目中,一般见到的分类任务数据集都是样本类别不均衡的,而是否对样本类别不均衡做处理,会发现在模型效果上可能差别比较大。
样本类别不均衡处理的流程:
- 如果本身小类别的样本绝对数量足够大(比如有1万条),只是相对比其他类别的样本少(比如大类别是100万条样本),可以先暂时跳过这个步骤;
- 如果不同类别的训练样本数目稍有差别,通常影响不大,比如二分类负样本与正样本比例不大于4:1,可以先训练看效果,用适合二分类类别不均衡的评价指标AUC-PR来评估;
- 对于正负样本极不平衡的场景,可能考虑是否可以换一个不同的角度来看问题,比如将它看作异常检测问题,并使用异常检测来建模。
对于样本类别不均衡情况下的训练集和验证集的切分,如果是按照时间窗口来切分的话,要尽量保证验证集和训练集有足量的小类别样本;如果是随机切分的话,要分层来随机切分,也就是按照小类别和大类别分别随机切分然后组成训练集和验证集。
处理样本类别不均衡的方法如下:
方法 | 说明 |
收集更多的数据 | 这个方法很重要,尤其是对于小类别来说。如果小类别样本数量不足,常见的方式是把时间窗口拉长来收集更多的样本。 |
数据增强 | 这个方法尤其在NLP,CV领域用的最多(不只是用在分类任务中)。在分类任务中,它的目的是根据已有样本来生成同类别的样本,比如图像数据可以平移、放缩、旋转、翻转;文本数据可以回译,同义词替换等等。在结构化数据建模场景下,可能这个方法用的不多(可能的原因是利用这种方法生成的新的样本可能会带来一些噪音从而可能让模型效果恶化)。 |
损失函数敏感的方法 | 这个方法很重要,在实际项目中也是用的最多的。它的目的是让小类别的权重更大,让模型更看重小类别的样本。按照样本集不同类别的比列来反向调整类别权重class weight(比如小类别与大类别的样本比例是1:8,那么把小类别的class weight值设置为8,大类别的class weight值设置为1)或者样本权重sample weight。 |
集成采样(Ensemble sampling) | 基本思想就是把majority大类别的样本进行切分,然后和minority小类别的样本组合成小的数据集;每个这样的小的数据集用一个学习器, 然后训练多个这样的学习器, 最后再集成这些学习器。 |
使用对类别不均衡以及困难样本效果更好的loss函数 | 困难样本简单理解就是导致loss比较大的样本,在样本类别严重不均衡(比如正负样本比例是1:4000)的情况下,正样本也可以看作是困难样本了。Facol loss函数能更好在这样的情况下工作,但是注意该loss对错误的标注很敏感。 |
使用对类别不均衡不敏感的算法 | 可以使用比如RadiusNeighborsClassifier这样的分类器。 |
采样技术 | Oversampling过采样: 增加minor类达到数量均衡。常用的是SMOTE方法及其变体,它首先为每个稀有类样本随机选出几个邻近样本,并且在该样本与这些邻近样本的连线上随机取点,生成新的稀有类样本(本质上是插值的方法,也是一种数据增强的方法,在实际的结构化数据建模的项目中这个用的可能很少);或者简单复制minor类的样本。 Undersampling下采样: 减少major类的样本以达到数量均衡。 |
基于业务代价方法 | 对于二分类的阈值设定问题,根据验证集的混淆矩阵的四个统计值和每个统计值对应业务的代价/成本来遍历阈值从而找到最优阈值(可以参考Amazon SageMaker的一个例子)。有时对于某些场景不容易预估这个成本/代价,比如游戏APP的用户流失预测任务,此时可以考虑根据大盘历史上对留存和流失用户数的统计然后对离线预测的这批用户的预测概率排序然后基于分位数来判定用户是流失还是留存。 |
基于再缩放(rescalling)策略进行决策 | 对于二分类任务,一个naïve的方法是阈值判断使用正类个数与负类个数的比例(假设正类样本小于负类样本),而不是使用0.5这个阈值。 对于多分类问题,可以给每个类别设置一个阈值(比如按照样本类别占比来设置阈值),最终分类器会选择对应类别的预测概率与该类别的阈值比值最大的类别作为预测结果(Sparkml的逻辑回归模型处理多分类问题就是这样做的)。 |
对于二分类类别不均衡的情况下,在预测得到了概率以后,尽量不要设定某个阈值来判定是否是正例,能排序的话尽量通过排序来解决(比如返回top-K结果或者截断某分位数以上的结果)。
特征预处理之连续特征离散化
经过连续特征离散化处理之后的特征对于异常数据具有较强的鲁棒性,模型会更稳定。离散化相当于引入了非线性,提升模型的表达能力,增强拟合能力。离散化之后方便进行特征交叉(两个连续特征之间不方便做特征交叉),通过使得模型要拟合的值大幅度降低,也降低了模型的复杂度。但是连续特征离散化也有不足的地方,比如离散化可能会丢失特征的一些信息,可能使模型性能恶化,以及离散化可能带入一些噪音。
连续特征离散化的基本假设是根据业务知识和目标任务,认为连续特征不同区间的取值对目标的贡献是不一样的,而同一个区间的取值对目标的贡献差不多。即使是同样的连续性变量在不同的业务场景是否需要离散化都不一样:比如“面积”这样的连续性特征,在预测房子单价的时候可能不需要离散化,但是如果是预测房子是否热卖的时候,可能就需要做离散化。
对于什么时候需要进行离散化,通常如果你之后选用的模型只接受离散特征(比如多项分布朴素贝叶斯模型),或者你想选用那些倾向于使用离散特征的线性模型(比如逻辑回归或者线性回归模型),则进行离散化;否则,可以暂时先不考虑离散化。
离散化也叫分箱,分为有监督分箱和无监督分箱:
实现方法 | |
有监督分箱(分箱数量是动态确定) | 决策树分箱:利用决策树做单变量特征的拟合,从而获得节点的划分值来进行分箱。 卡方分箱(只适用与分类任务和连续性特征):对连续性特征的值排序,把每一个单独的值视为一个分箱。计算相邻两个分箱合并后的卡方值,最后合并卡方值最小的相邻分箱。重复上述过程,直到满足结束条件。 |
无监督分箱(分箱数量是静态指定) | 等频分箱:按照观测个数均分为N等分,每个分箱里面的观测数量基本一致; 等宽分箱:把连续特征的值从最小值到最大值之间均分为N等份,每个区间是一个分箱; 聚类分箱:利用聚类算法比如kmeans做单特征聚类,簇标记作为该特征的离散值。 |
对于无监督分箱,需要先确定分箱的数量和边界。分箱大小不能太小也不能太大,分箱大小必须足够小,使得箱内的属性取值变化对样本标记/目标变量的影响基本在一个不大范围;分箱大小必须足够大,使每个箱内都有足够的样本,如果箱内样本太少,则随机性太大,不具有统计意义上的说服力。然后根据业务领域的经验来指定,准则是结合业务并要求不同的分箱对目标变量的作用不同,或根据模型来选择,也就是说根据具体任务来训练分箱之后的数据集,通过超参数搜索来确定最优的分箱数量和分箱边界。从上面也可以看到,无监督分箱会带入更多的不确定性,因此尽量优先选择有监督分箱。
特征预处理之数值型category特征编码
数值型category特征有两个来源,字符串category特征转来的数值型category特征或者拿到的原始特征就是数值型category特征。之所以需要对数值型category变量编码是为了后续更方便和更好的处理特征以及模型拟合,也就是说从计算复杂度方面(特征维度)考虑或者从隐含语义方面(特征embedding)考虑。
数值型Category特征常用的编码方式如下:
- One-hot向量,适用于category特征的基数不大的情况。对于一个有m个离散值的特征,经过one-hot独热编码处理后,会变为m个二元特征,这m个二元特征互斥,每次只有一个激活。它的优点是,经过one-hot后更容易做特征交叉(深度学习和embedding火之前,很多公司用One-hot + LR模型做CRT预估排序模型)。它的缺点是当category特征基数很大的时候,很容易导致维度灾难(不利于后续的模型进行训练 )。
- Target encoding,适用于把category特征转换为连续特征。它是一种有监督特征编码方法,它用统计方法把每个category特征的枚举值根据目标变量来编码(严格意义上看的话,这种方法有信息泄露的)。对于C分类问题,单个category特征会编码为C−1个连续特征。其出发点是用C-1个条件概率P(Y=yi|X=xi)来代替category特征X的值xi。对于回归问题,单个category特征编码后是单个连续特征,其思路是统计category特征X的值xi对应的Y的均值。target encoding所有的统计计算都是基于训练集来的,所以一旦新数据集的分布发生一点变化,就会产生类似于过拟合所产生的不良的训练效果,所以就有了target encoding的变体比如mean encoding(mean encoding的原理和target encoding基本是一样的,只不过比target encoding多了一个交叉计算的步骤,目的是为了降低过拟合的影响)。
- Hash trick,适用于category特征的基数比较大的情况,把特征值做hash然后对hash桶取模,最终把单维度category特征变成了多维度向量,维度就是hash桶的数量。该向量的维度远远小于原category特征的特征空间,因此从这个角度来看是降维(比如使用tensorflow的feature_column.categorical_column_with_hash_bucket() 把离散特征变成hash column)
- Embedded encoding,适用于category特征基数大,使用深度学习且训练集很大的情况。Embedded encoding可以克服one-hot编码导致的维度灾难问题,但是训练embedding向量比较复杂而且需要大量的训练样本,它主要用在深度学习中,它是不可解释的。 当然其实传统机器学习模型也有可以做embedding的比如FM,MF等(对于FM,每个连续特征就一个embedding向量,而每个category特征的每个唯一值都有一个embedding向量)。Embedding相当于把单维度category特征变成了多维度连续特征;另一个重要的方面是embedding可以发现潜在的语义。使用embedding encoding时,可以方便的把序列特征这样的强特征建模进来。Embedding encoding区别于其他编码方式,它需要模型来学习,而其他编码方式没有学习过程是直接计算出来的。神经网络中的Embedding向量表现形式如下:
表现形式 | 介绍 |
embedding table中的一行向量 | 这是最常见的方式,本质上是静态的embedding向量。 这个也叫input embedding(靠近神经网络的输入层)。 |
神经网络中某一层的输出激活值 | 比如Youtube DNN的在线实时召回模型的user embedding向量就是用神经网络的最后一个全连接层输出的激活值,这个也叫做output embedding(靠近神经网络的输出层)。(因为每次预测时输入的用户的行为序列特征可能不同,因此就可能得到该用户的不同的embedding向量,因此这里得到的就是动态的embedding向量) |
神经网络中某几层的输出的激活值的聚合 | 比如基于Bert模型做词向量的时候,它的word embedding就是用的最后几层的输出激活值聚合而成。这样在每次使用时,输入完整的句子到模型中,获得该句子中每个分词的word embedding,它也是动态的embedding。 对于图片的embedding来说,一般可以用某一层flatten以后的输出激活值或者某几层flatten以后的输出激活值的聚合来表示,这个embedding是静态的embedding。 |
与全连接层的权重相关 | 比如对于Youtube DNN的实时召回模型,它把最后一个全连接层的权重矩阵[hiden_unit, item_count]做转置后得到的矩阵的每行对应一个video item的output embedding,这个是静态的embedding。 |
根据之后想要选择的模型,category特征的基数大小,数据集大小来综合决定选择哪种特征编码方式。每种特征编码方式都有局限性,如果可以的话,可以先选择适合直接处理category特征的模型比如LightGBM和CatBoost:
- 基于树的模型不关心category特征是否有序,并且尽量不要用one-hot向量。SKLearn和XGBoost的实现是不区分数值category特征和连续型特征的,统一都作为连续性特征来处理。因此category数字特征最好不要直接丢给SKLearn或XGBoost的基于tree的模型,要提前对category特征进行编码转换为连续特征或者使用别的模型比如LightGBM或者CatBoost。对于XGBoost,category特征的基数是10到100之间的话,也可以考虑用one-hot编码;即使这样,如果有很多基数小的category特征的话,one-hot编码所有这样的category特征同样不合适。如果在有category特征的情况下,非要使用XGBoost,可以考虑对category特征做target encoding编码为连续特征后在联合其他连续特征送入XGBoost来建模。SparkML的tree-based的模型和LightGBM以及CatBoost,它们能区分category特征和连续特征。因此对于这些模型,category特征可以不做编码。
- 对于深度学习模型的话,不考虑category特征是否有序,常见的处理方法是把category特征先做mapping比如字典映射或者hash映射然后在做embedding。如果category特征的基数不大的话,也可以考虑只是做one-hot编码。不是所有的category特征都一定要做embedding,一般是针对纯id类的category特征并且是有内涵的实体(比如usrid,itemid,channelid等等)做embedding(当然这个也和模型有关,对于DeepFM模型,每个连续特征以及每个category特征的每个特征值都会做embedding。对于wide and deep 模型,usrid/itemid这样的有内涵实体的id类特征做embedding并作为deep部分的一部分输入,而其他的category特征则考虑做one-hot并作为wide部分的输入,这里可以选择哪些category特征做embedding并送入deep部分)。对没有内涵实体的id类特征(比如常驻地或者学历等级也用id来表示了)或者不是id类的category特征(比如性别或者职位级别),可能做embedding的意义不大(这里并不说这样的category特征就一定不做embedding)。如果有些category特征不做embedding,并且要送入神经网络的话,需要选择下面某种方法:转为one-hot(适用于离散特征的基数很小比如小于10并且总的离散特征的数量不多);或者转为连续特征(比如使用前面介绍的target encoding方法)
前面讨论的都是单值category特征的编码方法,现实中还经常会有所谓的多值category特征,如下所示。
Genres |
Animation|Childrens|Comedy |
Animation|Childrens|Fantasy |
Comedy|Romance |
Comedy|Drama |
Comedy |
多值category特征的编码方式如下:
编码方式 | 介绍 |
转换为multi-hot向量 | 最直觉和简单的一种方法,适合特征的基数不大的情况(比如[1, 1, 0, 0]向量就是所谓的multi-hot向量) |
多值category特征转换为单值category特征,然后在编码,最后结果融合 | 本质是拆分单条样本为多条样本。比如对多值category特征的样本拆分为多个单值category特征的样本,然后对单值category特征做embedding(比如预测样本也可以用类似的拆分方式送入模型获得预测值,然后把多个拆分的预测值进行平均) |
多值category特征看作一个有机整体进行编码 | 如果多值category特征表示的是某种序列比如用户点击行为序列: 则可以单独用类似word2vec的思路(或者entity2vec)非监督学习每个item的embedding,对近期点击的item的embedding求和或者平均就是该用户的近期兴趣爱好表征。 也可以利用复杂的神经网络结构直接对包括用户点击行为序列的多值category特征和其他特征拼接进行监督学习;这样多个点击item的embedding配合上pooling做end-to-end训练。 |
多值category特征字符串化 | 如果多值category特征对应的是多个文本标签或者能转成多个文本标签,就可以把这些单个的标签的文本embedding从某个预训练的语言模型获得,然后把所有的这些标签的文本embedding在做一下聚合操作 |
总结
本文介绍了特征预处理的三个子步骤即样本类别不均衡处理,连续特征离散化和数值型category特征编码。数值型category特征编码是重点和难点,样本类别不均衡则是实际项目中绕不开的重要话题。接下来的第三讲我会继续讲特征预处理以及特征工程的其他步骤。再次感谢大家的阅读。