吴恩达老师有言:“机器学习在本质上还是特征工程,数据和特征决定了机器学习的上限,模型和算法只是逼近这个上限而已。”
特征工程主要分为:数据预处理、特征变换、特征提取、特征选择四部分
特征工程
1.数据预处理1.1缺失值处理1.区分缺失值2.处理方法1.2异常值处理1.寻找异常值2.处理异常值1.3优化内存2.特征变换2.1 连续变量无量纲化2.2 连续变量数据变换1.log变换2.连续变量离散化2.3 类别特征转换2.4 不规则特征变换3.特征提取3.1 类别相关的统计特征1.目标编码2.count nunique ratio3.类别特征之间交叉组合3.2 数值相关的统计特征3.3 时间特征3.4 多值特征4.特征选择4.1 特征关联性分析1.皮尔逊相关系数2.卡方检验3.互信息法4.2 特征重要性分析4.3 封装方法1.数据预处理
尽量得到标准、干净、连续的数据,供数据统计、数据挖掘等使用,视情况尝试对缺失值进行处理,比如是否要填充,填充什么。此外,有些竞赛提供的数据集以及对应的存储方式可能使得需要占用超过参赛者本身硬件条件的内存,故有必要进行一定的内存优化,这也有助于在有限的内存空间对更大的数据集进行操作。
os:本人在处理图像时深受内存空间不足之苦…
1.1缺失值处理
除了XGBoost和LightGBM等算法在训练时可以直接处理缺失值以外,其他很多例如LR、DNN、CNN、RNN等都并不能对缺失值进行直接处理。故而在数据准备阶段,要比构建算法阶段花更多时间,因为像填补缺失值这样的操作需要细致处理。
Python中可用“.info()”直接查看Non-Null Count
如果你用kaggle,dataset上传后会自动可视化
1.区分缺失值
首先,需找到缺失值表现形式。除了None、NA和NaN外,还有例如-1或-999来填充的缺失值。还有一种看上去像缺失值,但实际上有实际意义的业务,此时需特殊对待。例如没有填“婚姻状态”的用户可能是对自己隐私比较敏感,应为其单独设为一个分类;没有“驾龄”可能是没有车,为其填充0比较合适。
2.处理方法
数据缺失可以分为类别特征的缺失和数值特征的缺失两种。
对于类别特征,通常会填充一个新类别,如0,-1等。
对于数值特征,可以均值填充(但=对异常值较为敏感),中位数填充(对异常值不敏感)。填充时一定要考虑所选择的填充方法会不会影响数据的准确性。填充方法总结如下:
类别特征:可选择最常见的一类填充方法,即众数;或直接填一个新类别数值特征:可填平均数、中位数、最大最小值等,具体情况具体分析有序数据(如时间序列):可填充相邻值next或previous模型预测填充:普通的填充仅是一个结果的常态,并未考虑其他特征间相互作用的影响,可以对含有缺失值的那一列进行建模并预测其中缺失值的结果。方法虽然复杂但随后得到的结果直觉上比直接填充要好
1.2异常值处理
实际数据中可能会发现某个或某些字段(特征)根据某个变量(如时间序列问题中的时间)排序后,经观察存在一些数值远高于或低于其一定范围内的其他数值。还有些不合理的存在,这些都可以视作异常值,他们可能会给算法性能带来负面影响
1.寻找异常值
首先,找到异常值,总结了两种方法:
通过可视化分析。简单使用散点图(Matplotlib),严重偏离密集区域的点都可当作异常值来处理通过简单的统计分析,即根据基本的统计方法来判断数据是否异常,例如四分位数间距、极差、均差、标准差等,这种方法适合于挖掘单变量的数值型数据。
seaborn库的箱型图好使
2.处理异常值
删除含有异常值的记录。优点:可消除含有异常值的样本带来的不确定性。缺点:减少了样本量视为缺失值。优点:将异常值集中化为一类,增加了数据的可用性。缺点:将异常值和缺失值混为一谈,会影响数据的准确性、平均值(中位数修正)。用对应同类别的数值使用平均值修正该异常值。优缺点同“视为缺失值”不处理。直接在有异常值的数据集上进行数据挖掘。这就听天由命看异常值来源了。扩展
离散型异常值(离散属性定义范围以外的所有值均为异常值)、知识型异常值(如大学牲脱发情况:从无)等,都可以当做类别缺失值来处理。
1.3优化内存
数据集太大而自己的硬件条件有限就有可能会因为内存不够导致代码出现memory error,介绍Python的内存回收机制和数值类型优化这两种有助于优化内存的常见方法。
内存回收机制。在Python的内存回收机制中,gc模块主要运用“引用计数”来跟踪和回收垃圾。在引用计数的基础上,还可以通过“标记清除”来解决容器对象可能产生的循坏引用问题,通过“隔代回收”以空间换取时间来进一步提高垃圾回收的效率。一般来讲,在我们删除一些变量时,使用gc.collect()来释放内存。(亲测,慎用数值类型优化。竞赛中常使用的数据保存格式是csv以及txt,在进行处理时,需要将其读取为表格型数据,即DataFrame格式。需要利用pandas进行操作。pandas可以在底层将数值型数据表示成NumPy数组,并使其在内存中连续存储。这种存储方式不仅消耗的空间较少,还使我们能够快速访问数据。
import pandas as pddf_csv = pd.read_csv('XXXX.csv')df_txt = pd.read_txt('XXXX.txt')df_excel = pd.read_excel('XXXX.xlsx')
pandas重点许多数据类型都具有多个子类型,它们可以使用较少的字节表示不同数据。
我们可以用np.iinfo类来确认每一个int型子类型的最大值和最小值
import numpy as npnp.iinfo(np.int8).minnp.iinfo(np.int8).max
在不允许影响模型泛化性能的情况下:
对于类别型的变量,若其编码ID的数字较大、极不连续且种类较少,则可以从0开始编码(自然数编码),这样可以减少变量的内存占用。对于数值型的变量,常常由于存在浮点数使得内存占用过多,可以考虑先将其最小值和最大值归一化,然后再乘以100、1000等,再取整,节省内存空间。
2.特征变换
2.1 连续变量无量纲化
指将不同规格的数据转换到同一规格。常见无量纲化方法有标准化和区间缩放法。标准化的前提是特征值服从正态分布,标准化后,特征值服从标准正态分布。区间缩放法利用了边界信息,将特征的取值区间缩放到某个特定的范围,例如[0,1]
但特征转换是构建一些模型(如线性回归、KNN、神经网络)的关键,对于决策树相关模型并无影响。还有些纯粹的工程原因,即在进行回归预测时,对目标取对数处理,不仅可以缩小数据范围,而且压缩变量尺度使数据更平稳。
然而,数据要求不仅是通过参数化方法施加的。如果特征没有被规范化,例如当一个特征的分布位于0附近且范围不超过(-1,1),而另一个特征的分布范围在数十万数量级时,会导致分布于0附近的特征变得完全无用。
标准化:最简单的转换是标准化(零-均值规范化)。标准化需要计算特征的均值和标准差。区间缩放:区间缩放思路有很多种,常见的一种使利用最大最小值进行缩放。
2.2 连续变量数据变换
1.log变换
进行log变换可以将倾斜数据变得接近正态分布,因为大多数机器学习模型不能很好地处理非正态分布数据,比如右倾数据。可以应用log(x+1)变换来修正倾斜,其中+1的目的是防止数据等于0,同时保证x都是正的。取对数不会改变数据的性质和相关关系,但是压缩了变量的尺度,不仅数据更加平稳,还削弱了模型的共线性、异方差性等。
扩展:cbox-cox变换,一种自动寻找最佳正态分布变换函数的方法。
2.连续变量离散化
离散化后的特征对异常数据有很强的健壮性,更便于探索数据的相关性。常用的离散化分为无监督和有监督两种。
无监督的离散化分桶操作可以将连续变量离散化,同时使数据平滑,即降低噪声的影响。一般分为等频和等距两种分桶方式。
等频:区间的边界值要经过选择,使得每个区间包含数量大致相等的变量实例。这种分桶方式可以将数据变成均匀分布。等距:将实例从最小到最大值,均分为N等份,每份的间距是相等的。这里只考虑边界,每等份的实例数量可能不等。等距可以保持数据原有的分布,并且区间越多,对数据原貌保持得越好。
有监督的离散化对目标有很好的区分能力,常用的是使用树模型返回叶子节点来进行离散化。如在GBDT+LR经典模型中,就是先使用GBDT来将连续值转化为离散值。具体方法:用训练集中的所有连续值和标签输出来训练LightGBM,共训练两棵决策树,第一棵有4个叶子节点,第二棵树有3个叶子节点。如果某一个样本落在第一棵树的第三个叶子节点上,落在第二棵树的第一个叶子节点上,那么它的编码就是0010100,一共7个离散特征,其中会有两个取值为1的位置,分别对应每棵树中样本落点的位置。最终我们会获得num_trees*num_leaves维特征。
2.3 类别特征转换
在实际数据中,特征并不总是数值,还有可能是类别。对于离散型的类别特征进行编码,一般分为两种情况:自然数编码(特征有意义)和独热(one-hot)编码(特征无意义)。
自然数编码:一列有意义的类别特征(即有顺序关系)可以使用自然数进行编码,利用自然数的大小关系可以保留其顺序关系。以下街上两种自然数编码的常用方式
①调用sklearn中函数:
from sklearn import preprocessingfrom f in columns:le = preprocessing.LableEncoder()le.fit(data[f})
②自定义实现(速度快)
for f in columns:data[f] = data[f].fillna(-999)data[f] = data[f].map(dict(zip(data[f].unique(),range(0,data[f].nunique()))))
独热编码:当类别特征没有意义(即无顺序关系)时,需要使用独热编码。例如,红>蓝>绿不代表任何东西,进行独热编码后,每个特征的取值对应一维特征,最后得到一个样本数×类别数大小的0~1矩阵。可直接调用sklearn中API
2.4 不规则特征变换
不规则特征可能包含样本的很多信息,比如身份证号。各段表示不同的信息。
3.特征提取
机器学习模型很难识别复杂的模式,特别是很难学习到不同特征组合交叉的信息,所以我们可以基于对数据集的直觉分析和业务理解创建一些特征来帮助模型有效学习。下面我们将介绍结构化数据的特征提取方式。(结构化数据由明确定义的数据类型组成,非结构化数据由音频、视频和图片等不容易搜索的数据组成。)
3.1 类别相关的统计特征
类别特征又可以称为离散特征,除了每个类别属性的特定含义外,还可以构造连续型的统计特征,以挖掘更多有价值的信息,比如构造目标编码、count、nunique、和ratio等特征。另外,也可以通过类别特征间的交叉组合构造更加细粒度的特征。
1.目标编码
目标编码可以理解为用目标变量(标签)的统计量对类别特征进行编码,即根据目标变量进行有监督的特征构造。如果是分类问题,可以统计目标均值、中位数和最值。目标编码的方式可以很好地替代类别特征,或者作为新特征。
使用目标变量时,非常重要的一点是不能泄露任何验证集的信息。所有基于目标编码的特征都应该在训练集上计算,测试集则由完整的训练集来构造。更严格一点,我们在统计训练集的特征时,需要采用K折交叉统计法构造目标编码特征,从而最大限度地防止信息泄露。如用五折交叉统计构造特征时,我们将样本划分为五份,对于其中每一份数据,我们都将用另外四份来计算每个类别取值对应目标变量的频次、比例或者均值,简单来说就是未知的数据(一份)在已知的数据(四份)里面取特征。
目标编码方法对于基数较低的类别特征通常很有效,但对于基数较高的类别特征,可能会有过拟合的风险。因为会存在一些类别出现频次非常低,统计出来的结果不具有代表性。一般会加入平滑性来降低过拟合风险。在处置妥当的情况下,无论是线性模型,还是非线性模型,目标编程都是最佳的编码方式和特征构造方式。
五折交叉统计的代码实现:
folds = KFold(n_splits=5,shuffle=True,random_state=)for col in columns:colname = col+'_kfold'for fold_,(trn_idx,val_idx) in enumerate(folds.split(train,train)):tmp = train.iloc[trn_idx]order_label = tmp.groupby([col])['label'].mean()train[colname] = train[col].map(order_label)order_label = train.groupby([col])['label'].mean()test[colname] = test[col].map(order_label)
2.count nunique ratio
count:计数特征,用于统计类别特征的出现频次
nunique和ratio常常会涉及多个类别特征的联合构造。例如在广告点击率预测问题中,对于用户ID和广告ID,使用nunique可以反映用户对广告的兴趣宽度,也就是统计用户ID看过几种广告ID;使用ratio可以反映用户对某类广告的偏好程度,即统计用户ID点击某类广告ID的频次占用户点击所有广告ID频次的比例。
3.类别特征之间交叉组合
交叉组合能够描述更细粒度的内容。对类别特征进行交叉组合在竞赛中是一项非常重要的工作,这样可以进行很好的非线性特征拟合。如用户年龄和用户性别可以组合成“年龄_性别”这样的新特征。一般我们可以对两个类别或三个类别特征进行组合,也称作二阶组合或三阶组合。简单来讲,就是对两个类别特征进行笛卡尔积的操作,产生新的类别特征。
并非所有组合都是需要考虑的,我们会从两个方面进行分析。
业务逻辑方面:比如用户操作系统版本与用户所在城市的组合是没有实际意义的。类别特征的基数:如果基数过大,那么可能导致很多类别只会出现一次,在一轮训练中,每个类别只会被训练一次,显然特征对应权重的置信度是很低的。
3.2 数值相关的统计特征
这里所说的数值特征,我们认为是连续的。数值特征的大小是有意义的,通常不需要处理就可以直接“喂”给模型进行训练。除了之前对数值特征进行各种变换外,还存在一些其他常见的数值特征构造方式。
数值特征之间的交叉组合:一般对数值特征进行加减乘除等算术操作类的交叉组合。这需要我们结合业务理解和数据分析进行构造。**类别特征和数值特征之间的交叉组合 **:除了类别特征之间和数值特征之间的交叉组合外,还可以构造类别特征与数值特征之间的交叉组合。这类特征通常是在类别特征的某个类别中计算数值特征的一些统计量,比如均值、中位数和最值等。按行统计相关特征:行统计在构造时会包含更多的列,直接对多列进行统计。
3.3 时间特征
在实际数据中,通常给出的时间特征是时间戳属性,所以首先需要将其分离成多个维度,比如年月日小时分钟秒钟。如果你的数据源来自于不同的地理数据源,还需要利用时区将数据标准化。除了分离出来的基本时间特征外,还可以构造时间差特征,即计算出各个样本的时间与未来某一个时间的数值差距,这样这个差距是UTC的时间差,从而将时间特征转换为连续值,比如用户首次行为日期与用户注册日期的时间差、用户当前行为与用户上次行为的时间差等。
3.4 多值特征
在竞赛中,可能会遇到某一列特征中每行都包含多个属性的情况,这就是多值特征。例如广告大赛中的兴趣类目,其中包含5个兴趣特征组,每个兴趣特征组都包含若干个兴趣ID。对于多值特征,通常可以进行稀疏化或者向量化的处理,这种操作一般出现在自然语言处理中,比如文本分词后使用TF-IDF(词频-逆文档频率)、LDA(隐含狄利克雷分布)、NMF(非负矩阵分解)等方式进行处理,这里则可以将多值特征看作文本分词后的结果,然后做相同的处理。
对多值特征最基本的处理办法是完全展开,即把这列特征所包含的n个属性展开成n维稀疏矩阵。使用sklearn中的CountVectorizer函数,可以方便地将多值特征展开,只考虑每个属性在这个特征的出现频次。
还有一种情况,比如在广告算法大赛中,需要根据用户的历史点击行为预测用户的属性标签。这时候用户的点击序列尤为重要,当我们构造好用户对应的历史点击序列后,除了使用上述的TF-IDF等方法外,还可以提取点击序列中商品或广告的嵌入表示,比如用Word2Vec、DeepWalk等方法获取embedding向量表示。因为要提取用户单个特征,所以可以对序列中的嵌入向量进行聚合统计,这种方法本质上是假设用户点击过的商品或广告等同重要,是一种比较粗糙的处理方式。我们可以引入时间衰减因素,或者使用序列模型,如RNN、LSTN、GRU,套用NLP的方法进行求解。
4.特征选择
当我们添加新特征时,需要验证它是否确实能够提高模型预测的准确度,以确定不是加入了无用的特征,因为这样只会增加算法运算的复杂度,这时候就要通过特征选择算法自动选择出特征集中的最优子集,帮助模型提供更好的性能。特征选择算法用于从数据中识别并删除不需要、不相关以及冗余特征。这些特征可能会降低模型的准确度和性能,特征选择的方法主要有先验的特征关联性分析以及后验的特征重要性分析。、
4.1 特征关联性分析
特征关联性分析是使用统计量来为特征之间的相关性进行评分。特征按照分数进行排序,要么保留,要么从数据集中删除。
关联性分析方法通常是针对单变量的,并独立考虑特征或者因变量。常见的特征关联性分析方法有皮尔逊相关系数、卡方检验、互信息法和信息增益等。
这些方法速度快、使用方便,但是忽略了特征之间的关系,以及特征和模型之间的关系。
1.皮尔逊相关系数
不仅可以衡量变量之间的线性相关性,解决共线变量问题,还可以衡量特征与标签的相关性。共线变量是指变量之间存在高度相关关系,这会降低模型的学习可用性,可解释性以及测试集的泛化性能。但这三个特性都是我们想增加的,所以删除共线变量是一个有价值的步骤。我们将为删除共线变量建立一个基本的阈值(根据想要保留的特征数量决定)。
下面代码用于解决特征与标签不具有相关性的问题,根据皮尔逊相关系数的计算提取top300的相似特征:
def feature_select_pearson(train,features):featureSelect = features[:]#进行皮尔逊相关性计算corr=[]for feat in featureSelect:corr.append(abs(train[[feat,'target']].fillna(0).corr().values[0][1]))se = pd.Series(corr,index=featureSelect).sort_values(ascending=False)feature_select = se[:300}.index.tolist()#返回特征选择后的训练集return train[feature_select]
2.卡方检验
用于检验特征变量与因变量的相关性。对于分类问题,一般假设与标签独立的特征为无关特征,而卡方检验恰好可以进行独立性检验,所以使用与特征选择。如果检验结果是某个特征与标签独立,则可以去除该特征。
3.互信息法
互信息是对一个联合分布中两个变量之间相互影响关系的度量,也可以用于评价两个变量之间的相关性。互信息法之所以能够用于特征选择,可以从两个角度进行解释:基于KL散度和基于信息增益。互信息越大说明两个变量相关性越高。
但是想把互信息直接用于特征选择其实不太方便,由于:
它不属于度量方式,也没有办法归一化,无法对不同数据集上的结果进行比较对于连续变量的计算不是很方便(X和Y都是集合,xi和y都是离散的取值),通常连续变量需要先离散化,而互信息的结果对离散化的方式很敏感
4.2 特征重要性分析
在实际竞赛中,经常用到的一种特征选择方法是基于树模型评估特征的重要性分数。特征的重要性分数越高,说明特征在模型中被用来构建决策树的次数越多。这里我们以XCBoost为例来介绍树模型评估特征重要性的三种计算方法(weight、gain和cover)。(LightGBM也可以返回特征重要性)
weight计算方式:该方式比较简单,计算特征在所有树中被选为分裂特征的次数,并将以此作为评估特征重要性的依据
params ={'max_depth':10,'subsample':1,'verbose_eval':True,'seed':12,'objective':'binary:logistic'}xgtrain = xgb.DMatrix(x,label=y)bst = xgb.train(params,xgtrain,numm_boost_round=10)importance = bst.get_score(fmap='',importance_type='weight')
gain计算方式:gain表示平均增益。在进行特征重要性评估时,使用gain表示特征在所有树中作为分裂节点的信息增益之和再除以该特征出现的频次。
importance =bst.get_score(fmap='',importance_type='gain')
cover计算方式:cover是特征对每棵树的覆盖率,即特征被分到该节点的样本的二阶导数之和,而特征度量的标准就是平均覆盖率值。
importance = bst.get_score(fmap='',importance_type='cover')
技巧:虽然特征重要性可以帮助我们快速分析特征在模型训练过程中的重要性,但不能将其当做绝对的参考依据。一般而言,只要特征不会导致过拟合,我们就可以选择重要性高的特征进行分析和扩展,对于重要性低的特征,可以考虑将之从特征集中移除,然后观察线下效果,再做进一步判断。
4.3 封装方法
比较耗时。可以将一组特征的选择视作一个搜索问题,在这个问题中,通过准备、评估不同的组合并对这些组合进行比较,从而找出最优的特征子集,搜索过程可以是系统性的,比如最佳优先搜索;也可以是随机的,比如随机爬山算法,或者启发式方法,比如通过向前和向后搜索来添加和删除特征(类似前剪枝和后剪枝算法)
启发式方法:分为前向搜索和后向搜索。前向搜索是每次增量地从剩余未选中的特征中选出一个并将其加入特征集中,待特征集中的特征数量达到初设阈值时,意味着贪心选出了错误率最小的特征子集。既然有增量加,就会有增量减,后者称为后向搜索,即从特征全集开始,每次删除其中的一个特征并评价,知道特征集中的特征数量达到初设阈值,就选出了最佳的特征子集
因为启发式方法会导致局部最优,所以加入模拟退火方式进行改善,这种方式不会因为新加入的特征不能改善效果而舍弃该特征,而是对其添加权重后放入已选特征集。
这种启发式方法是很耗时间耗资源的。
递归消除特征法:用一个基模型来进行多轮训练,每轮训练都会先消除若干权值系数的特征,再基于新特征集进行下一轮训练。可以使用feature_selection库的RFE类来进行特征选择
from sklearn.feature_selection import RFEfrom sklearn.linear_model import LogisticRegression#递归消除特征法,返回特征选择后的数据#参数estimator为基模型#参数n_feature_to_select 为选择的特征个数RFE(estimator=LogisticRegression(),n_features_to_select=2).fit_transform(data,target)
技巧
在使用封装方法进行特征选择时,用全量数据训练并不是最明智的选择。应先对大数据进行采样,再对小数据使用封装方法
以上三种特征选择方法按需使用或组合使用,建议优先考虑特征重要性,其次是特征关联性
此外还有null importance。其思想:将构建好的特征和正确的标签喂给树模型得到一个特征重要性分数,再将特征和打乱后的标签喂给树模型得到一个特征重要性分数,然后对比两个分数,如果前者没有超过后者,那么这个特征就是一个无用的特征。