2.3 特征工程与模型调优
前面已经学习了机器学习的常用工具 Pandas 和 scikit-learn、模型的评估指标与复杂度度量,下面接着介绍机器学习项目中的特征工程、模型选择与模型调优问题。在此之前,我们先来了解一个数据挖掘项目的完整流程。
2.3.1 数据挖掘项目流程
一个完整的数据挖掘项目流程主要包含六大部分,分别是业务理解、数据分析、特征工程、模型选择、模型评估、项目落地,如图2-8所示。
图2-8 数据挖掘项目流程
1.业务理解
拿到一个数据挖掘任务后,先不要急着一头钻进去,而是先要准确地理解该业务问题,即你需要弄明白你要干一件什么事情,比如是做分类问题还是聚类问题;要达到什么效果,比如预测准确率达到什么级别;项目规模有多大,比如是否需要大数据分布式平台处理;是否要求实时性等。建议大家在做数据挖掘项目时,可以至顶向下去考虑,这样不只是对于该问题的解决比较有帮助,同时还可以锻炼你的架构思维,这对于一个优秀的数据工程师是很有必要的。
2.数据分析
准确理解业务问题后,接下来需要做的就是数据分析。数据挖掘项目终归还是面向数据的,因此在正式使用模型之前,我们需要进一步了解数据情况。具体来讲,一般包括:
● 数据规模有多大?即考虑:样本数目有多少,特征维度有多大,需要什么级别的数据处理平台?
● 特征数据的类型有哪些?比如最常见的数值型和类别型。
● 各样本特征的取值是否存在缺失,如果有缺失,那么缺失规模各自有多大?
● 各特征取值的分布情况,比如类别型特征一共包含多少个类别,每个类别占比多少?数值型特征的分布情况怎样,是否满足高斯分布等?
3.特征工程
做好基本的数据分析后,就可以开始结合实际业务逻辑做一些特征工程了。这里记住一句话:特征决定你最后结果的上限,模型所做的其实是尽可能地逼近这一上限。由此可见特征工程的重要性。
什么是特征工程呢?特征工程其实是一个比较宽广的概念,最基本的如特征数据清洗、归一化/标准化处理、特征交互、特征映射等都属于特征工程的范畴。
怎样做特征工程呢?这一点会在后面详细介绍。
4.模型选择
做好特征工程后,就需要为我们的数据选择合适的模型了。这里模型的选择一般还是需要借助经验来挑选,没有万能的模型,不同的模型适用的范围是不同的;也没有最好的模型,只有最适合的模型。大家不要迷信某一类模型,比如有的使用者可能觉得深度学习模型一定比普通机器学习模型效果更佳。其实未必,比如在数据规模较小时,深度学习模型很容易发生过拟合,而有的普通模型,比如最简单的KNN,反而在这时候表现得还可以,这是完全有可能的。具体选择什么样的模型,一方面要结合具体的业务场景,另一方面要求我们熟悉各个模型的原理与特点,这在后续专门介绍各个模型的章节中会详细介绍。
这里补充说明一下,模型选择和特征工程这两步可能需要反复进行。因为不同的模型,其对特征的提取能力不同,比如Logistic回归模型是一个线性模型,只能进行线性分类;而决策树模型是一个非线性模型,其本身可以进行线性划分。所以,如果你要使用Logistic回归模型,但又希望其具有非线性划分能力,那么你可以采用的办法就是去做一些人工交互特征,再把它用在新的数据上。
5.模型评估
选择好我们认为合适的模型并将其应用于处理好的数据上之后,我们需要使用一些评估方法来检测我们的选择是否合理。最基本的评估方法就是前面介绍的回归问题、分类问题、聚类问题等各自对应的评估指标。但一个有经验的工程师应该想到,模型评估的本质其实还是根植于我们的业务场景当中的。通俗地讲就是:一切有利于提高实际业务场景目标的评估指标都可以作为我们模型的评估指标,所以有时候你可能会看到,在有些项目中,我们会直接使用损失函数的值作为评估指标,而并非一定要使用前面我们介绍的种种固定的评估指标。
6.项目落地
模型训练好后,工作其实并没有结束,因为还有一个很重要的环节需要考虑,那就是项目落地。如果一个数据挖掘项目在理论阶段验证得很好,但是却没法实践,比如实际场景需要进行实时推荐,而你的模型每次训练和预测就要几个小时甚至更长时间,这时候是不是很尴尬?所以,在项目的设计之初,在中途模型选型之时,我们就需要考虑到后续的落地实践是否可行。
2.3.2 特征工程
1.数据清洗
数据清洗主要是对原始给定的数据进行规整化,目的是得到一份适合机器学习模型处理的基本数据集。
从某网页中提取的原始数据如图2-9所示。
图2-9 原始网页数据示例
可以看到,网页中除含有文本数据外,还含有一些我们不需要的标签数据,这时我们就应该进行数据清洗工作,清洗后的数据示例如图2-10所示。
图2-10 清洗后的数据示例
当然,这只是一个基本的例子,实际的数据清洗过程往往复杂得多。比如,不同类型的数据(如文本、图像)、不同格式的数据(如.txt、.csv、.json、. html、.jpg)等,一般来说,首先我们需要理解业务目标,然后借助各种工具(如各种Python库、数据库等)去进行解析和处理,最终得到一份或多份比较规整的结构化数据表。
一般在实际业务场景下,我们获得的数据或多或少存在缺失的情况,缺失数据如图2-11所示。
图2-11 缺失数据
大部分机器学习模型并不能自动处理数据含有缺失的情况,所以在正式开始模型训练前,必不可少的就是确定各个特征所含缺失值的情况,以及制定相应的处理方式。假设原始数据标定名称是data,已经转化成Pandas熟悉的DataFrame格式;填充后得到的新数据为 data_new,也是 DataFrame 格式,下面演示几种常用的缺失值处理方式。
(1)直接删除缺失数据
当数据量比较大而缺失情况又不是很严重时,可以考虑直接删除训练集中含有缺失值样本的数据(即含有缺失值的行)。
直接删除缺失数据的好处是可以降低数据中的噪声,毕竟对一个缺失值进行填充不可能做到百分之百准确,而不准确的填充会给数据带来额外的噪声。坏处也很明显,首先,这会减少我们的训练数据,在数据量本来就不多的情况下可能会造成比较坏的影响;其次,测试集中的样本你是不能删除的,这样可能造成测试集和训练集数据分布不一致,而很多时候,数据分布对模型训练和预测是有比较大的影响的。
(2)固定值填充
固定值填充是一种很简单的方式,比如直接用“0”填充缺失值。
使用固定值填充也不是一种科学的方法,特别是当缺失比例较大时,如果强行用固定值去填充,给数据带来的噪声是非常大的,很容易造成模型的过拟合。简单来说,模型是从数据中去寻找规律的,如果强行改变数据中的某些值,其实就是在诱导模型将注意力转移到那个错误的方向。
(3)均值/中位数填充
使用均值和中位数填充算是固定值填充的一个优化方案。
对于各个特征来说,其本质上还是使用固定值填充,只不过这个固定值(各个特征的均值/中位数)可能离实际情况更接近一些,毕竟大部分数据取值的分布还是服从高斯分布的,而高斯分布的中间部分是占据了整体取值情况的大部分的。
(4)相邻值填充
使用相邻值填充也算是固定值填充的一个改进方案,比如使用缺失位置前面或后面的值进行缺失值的填充。
这种方式的优点是填充的值来源于特征取值的某个真实情况,并且不同缺失位置填充的是不同的值,数据的扰动比直接使用固定值或均值填充要好,因而模型可能不那么容易过拟合。缺点也比较明显,那就是采用相邻值代替时,偶然性比较大,即有可能某个缺失值正好跟它相邻位置的取值是接近的,但也有可能相差很大;当相差很大时,这种方式的填充肯定是不如均值填充的。
(5)模型预测填充
还有一种方式是使用模型来预测进行填充。还是以图2-11为例,例子中的特征f1、f2、f3、f4、f6和f7不存在缺失,假设现在要用模型预测填充f5的缺失部分,则处理方式为:先将不含缺失的特征(f1,f2,f3,f4,f6,f7)和待填充的特征(f5)取出来,然后按f5特征是否缺失将数据集分为测试集和训练集两部分,如上例中就是将样本(0,1,2,4)作为训练集,样本(1,3)作为测试集;在训练集上训练某个回归模型后对测试集进行预测,再用预测值代替原来的缺失值即可。
这样做的好处是在尽量保证填充准确度的同时增加数据扰动,理论上来说会比前面的方法要好一些。但根据个人的实践来看,有时候直接使用均值/中位数填充得到的效果更好一些。可能的原因是,一般用来做训练集特征的数目(即不含缺失值的特征数目)还是比较少的,在它们的基础上训练得到的模型很明显会存在欠拟合。
综合来讲,具体问题还是需要具体分析和对待,没有哪种方式是万能的,也没有一个绝对的指导方针。特征工程本来就是一个考验耐心的工作,需要我们去多尝试、多理解和多积累经验。
2.特征处理
特征处理的主要目的是结合我们后续所使用模型的特点(不同模型对输入数据类型的要求可能不一样),将清洗后的数据进行相应的转化。比如,数值型特征一般需要进行归一化、标准化、离散化等处理,类别型特征一般可以进行one-hot编码处理。
(1)归一化
一般我们需要将不同特征取值的量纲统一,这在有些情况下极其重要。图2-12是从某个数据项目中取出的5个样本对应特征的实际数据,可以看到,f5和f288这两个特征的取值完全不在一个量级上。
图2-12 不同量级的特征取值
这时候假设我们直接用它来训练一个线性回归模型,最终会得到一个表达式
h(x)=w1x(1)+⋯+wNx(N)+b
式中,x(j),j=1,2,…,N,表示样本数据中的N个特征,即图2-12中的date, f1, f2, …, f291等;wj是特征x(j)对应的权重系数。
由于f5和f288这两个特征的量纲不一样,因此会导致f5和f288这两个特征对应的权重系数的量纲也不一样,而在很多情况下,特征对应的权重系数值大小是可以直接反映对应特征在模型中的重要性的。所以对于上述例子中的情况,如果我们不先将f5和f288特征的量纲进行统一化,而直接比较这两个特征的权重系数是没有意义的,或者说是错误的。
特征归一化的原理为
式中,x表示某个特征的原始取值,x'表示该特征被归一化处理后对应的取值,xmin和xmax分别表示该特征原始取值中的最小值和最大值。
可以看到,将特征数据进行归一化处理后,该特征的所有取值都将被压缩至[0,1]这个区间。所以,如果我们分别对训练集和测试集数据中的所有特征进行归一化处理,就可以得到一个统一量纲的特征数据集了。
(2)标准化
标准化是另外一种统一特征量纲的方法,表达式为
式中,μ是某个特征的原始取值的均值,σ是对应标准差。
注意:进行特征标准化处理后,特征数据的取值范围并不在[0,1]区间,这点和归一化不同。实际上,特征标准化就是将原始的特征数据转化成一个标准的正太分布,所以它的前提其实假设了原始特征数据的取值分布服从正太分布。
至于什么时候使用归一化,什么时候使用标准化,则要看实际情况,一个基本的指导原则是:特征标准化不会改变特征取值的分布,而特征归一化会改变特征取值的分布。
(3)离散化
数值型特征除直接使用外,还有一种更精细的处理方法,那就是离散化。离散化的方式比较灵活,最简单的,你可以直接将某个数值型特征根据其取值大小做均分,比如1~100被均分成10等份,即1~10对应类别“1”,11~20对应类别“2”……91~100对应类别“10”。
但实际情况下,我们一般会根据实际的业务特征或者数据的分布情况来决定离散化的划分区间。比如数据集中年龄的取值为1~80岁,那么一种可能的划分为:0~18为类别“1”,表示未成年人;19~36为类别“2”,表示青壮年;37~48为类别“3”,表示中年……以此类推。具体划分为多少个等级可以根据实际的业务场景来决定,比如你需要建立用户的血糖预测模型,那么将年龄按照上述阶段划分就比较有意义(不同年龄段的血糖特性是不一样的,如图2-13所示)。
图2-13 年龄特征离散化
(4)one-hot编码
对于类别型特征,有些模型是无法处理的。比如在线性回归模型或者Logistic回归模型中,模型的基本原理是赋予各个特征一个权重系数,即用该特征的取值乘以它对应权重系数得到的结果作为该特征在模型中的贡献值。很明显,对于类别型特征(比如性别的“男”和“女”),你无法直接将其视为一个具体的数值,且要求该数值能代表该特征的重要性大小。这种情况下一般就需要使用one-hot编码。
对类别型特征进行one-hot编码的过程如图2-14所示。例子中有性别特征(取值为“男”和“女”两种)和年龄特征(取值为1~7共七个级别);所以,对于第一个样本,它的原始类别是“年龄-3”“性别-男”,因此它进行one-hot编码后对应的向量就是[0,1,0,0,1,0,0,0,0],即只有对应类别型的数值为“1”,其他均为“0”。
图2-14 对类别型特征进行one-hot编码
3.特征交互
特征交互就是人为的或者通过构造模型自动将两个或两个以上的特征进行交互,常用的交互方式有求和、最差、相乘、取对数等。假设原始特征为f1和f2,则这两个特征之间的交互可以是f1+f2、 f1−f2或f1 × f2等。scikit-learn中有实现做特征交互的功能,在preprocessing模块下。下面的例子是对原始特征数据train_matrix中的所有特征做二阶的多项式交互。
scikit-learn实现如下:
4.特征映射
特征映射是一个比特征交互更高级的问题,一般使用某些机器学习模型来实现,比如后面讲树模型及其集成学习模型后,我们会专门介绍基于梯度提升树(GBDT)的高阶特征映射,所以这里先不进行详细展开。
2.3.3 模型选择与模型调优
1.模型选择
模型选择的典型方法是正则化(Regularization)和交叉验证(Cross Validation),其中正则化方式上面应介绍过,所以这里介绍基于交叉验证的模型选择方法。顺便说一下,二者进行模型选择的思路是不同的:正则化进行模型选择是从模型的角度来考虑的,而交叉验证其实是从数据层面来考虑的。
在有监督学习问题中,一般会给定两部分数据集,即训练集(Training Set)和测试集(Test Set)。训练集是已知结果标签的数据集,主要用来训练模型;测试集是结果标签未知的数据集,一般就是我们需要预测结果标签的数据集。在进行模型选择时,我们一般将原始的训练集按比例(如8∶2)分为两部分,一部分作为训练集,另一部分作为验证集。我们利用训练集数据训练模型,并在验证集上进行验证,最后把在验证集上表现较好的模型当作我们最终的模型,然后使用该模型在原始的全量训练集上再重新训练后对测试集进行预测输出,如图2-15所示。
图2-15 训练集和测试集
另一种更为可靠的方法是S折交叉验证(S-fold Cross Validation),其基本操作是:首先随机地将原始训练集划分为 S 个相互无交集的数据子集,然后每次利用其中的S-1个子集数据作为训练集,剩下的那1个子集作为验证集,将模型的训练和验证过程在这可能的S种数据组合中重复进行,最后选择S次评测中平均测试误差最小的模型,如图2-16所示。
图2-16 S折交叉验证
数据集切分和交叉验证在scikit-learn中均有完整实现,具体如下。
(1)数据集切分
使用cross_validation类中的train_test_split函数可以很容易地将原始数据按照指定比例切成训练集(train)和验证集(test),如下:
上面的程序就表示将原始数据按照7∶3划分成训练集和验证集,其中参数x和参数y分别表示原始数据中的样本特征和样本标签,x_train和x_test分别表示训练集和验证集的样本特征,y_train和y_test分别表示训练集和验证集的样本标签。
(2)交叉验证
使用cross_validation 类中的cross_val_score函数可以很容易地实现S折交叉验证,如下:
其中,clf是指定的模型类别;x和y分别表示原始数据中的样本特征和样本标签;cv参数指定S折中交叉验证中的S值,默认为5;scoring指定模型采用的评估标准,默认为None。
注意:从 scikit-learn 0.18版本开始,上面的 cross_validation 类将被model_selection 类替代,所以读者在使用过程中请注意查看自己的scikit-learn版本号。
2.模型调优
前面说过,在模型选定后,一般还需进行模型的参数调优工作。各个模型对应的参数在后续章节中将陆续进行讲解,这里先简单介绍使用 scikit-learn 进行模型调优的两种基本方式:网格搜索寻优(GridSearchCV)和随机搜索寻优(RandomizedSearchCV)。
(1)网格搜索寻优
网格搜索寻优其实是一种暴力寻优方法,它的做法就是将模型的某些参数放在一个网格中,然后通过遍历的方式,用交叉验证对参数空间进行求解,寻找最佳的参数。scikit-learn中的GridSearchCV类实现了该功能,下面介绍该类的参数、属性和方法。
scikit-learn实现如下:
参数
● estimator:指定需要调优的基学习器模型。
● param_grid:给出学习器要优化的参数(以字典形式),字典的每个键放基学习器的一个参数,字典的值用列表形式给出该参数对应的候选值。
● scoring:指定模型采用的评估标准,如scoring='roc_auc’就表示寻优过程使用 AUC 值来评估,具体使用哪种评估标准可以根据前面讲解的模型评估标准确定。
● cv:确定S折交叉验证的S值,默认为3。
● iid:可选True或False。如果为True,则表示数据是独立同分布的,默认是True。
● n_jobs:指定计算机运行使用的CPU核数,默认为−1,表示使用所有可用的CPU核。
属性
● grid_scores_:列表形式,列表中的每个元素对应一个参数组合的测试得分。
● best_params_:最佳参数组合(字典形式)。
● best_score_:最佳学习器的评估分数。
方法
● fit(X_train,y_train):在训练集(X_train,y_train)上训练模型。
● score(X_test,y_test):返回模型在测试集(X_test,y_test)上的预测准确率。
● predict(X):用训练好的模型来预测待预测数据集X,返回数据为预测集对应的结果标签y。
● predict_proba(X):返回一个数组,数组的元素依次是预测集X属于各个类别的概率。
● predict_log_proba(X):返回一个数组,数组的元素依次是预测集X属于各个类别的对数概率。
例10 网格搜索寻优
输出:
(2)随机搜索寻优
在参数较少时,采用暴力寻优是可以的;但是当参数过多,或者当参数为连续取值时,暴力寻优明显不大可取,所以提出随机搜索寻优的方式,其做法是:对这些连续值做一个采样,从中挑选出一些值作为代表。scikit-learn 中的RandomizedSearchCV类实现了该功能,下面介绍该类的参数、属性和方法。
scikit-learn实现如下:
参数中的大部分与网格搜索寻优的相同,下面仅列出不同之处。
● param_distributions:给出学习器要优化的参数(以字典形式),字典的每个键放基学习器的一个参数,字典的值用列表形式给出该参数对应的候选值。
● n_iter:一个整数,指定参数采样的数量,默认是10。
● refit:可选True或False,默认为True,表示在参数优化之后使用整个数据集来重新训练该最优的estimator。
属性和方法与网格搜索寻优的相同,这里不再赘述。