1.3 预测建模的过程
通过观察模型的某些不同特征,我们已经对预测建模过程的各种步骤有所了解。在本节,我们要顺序讲解这些步骤,并理解每个步骤是如何对该任务的整体成功起作用的。
1.3.1 定义模型的目标
简而言之,每个项目的第一步是准确找出期望的结果是什么,因为这样有助于引导我们在项目的进展过程中做出正确的决定。在一个预测分析学项目里,这个问题包括深入研究我们要进行的预测的类型,以及从细节上去理解任务。例如,假定我们要尝试创建一个模型来预测某公司的雇员流失。我们首先需要准确定义这个任务,同时尽量避免把问题发散得太广或收敛得过细。我们可以把雇员流失的情况用全职的新雇员在入职后的最初六个月之内跳槽的比例来衡量。注意,一旦合理地定义了问题,对于采用什么数据开展工作的思考而言,我们就已经取得了一些进展。例如,我们不需要对兼职的合同工或实习生收集数据。这项任务也意味着只需要从所在公司收集数据,但同时我们也认识到,该模型不一定适用于为其他公司的职员进行预测。如果只对流失情况感兴趣,也就意味着不需要对员工的工作效率或病假情况作出预测(不过,问一下负责我们要创建的模型的主管领导也没有坏处,免得将来出问题)。
一旦我们对要创建的模型有了足够明确的思路,下一个要问的逻辑性问题就是我们想要达到的性能是怎样的,以及该如何衡量它。那就是说,需要为模型定义一个性能指标,以及满足要求的性能的最低阈值。在本书中,我们会针对评估模型性能的方法进行相当详细的讲解。就目前而言,我们要强调的是,虽然在用某些数据训练模型之后再讨论其性能评估问题的情况并不少见,但是在实践中要记住的很重要的一点是,定义模型的期望值和性能目标是预测建模者在一开始就必须和项目利益相关方讨论的问题。模型从来都不是完美无瑕的,建模者很容易陷入尝试改善性能的无限循环中。清楚的性能目标不仅对于我们决定采用哪些方法有指导意义,也有助于使我们了解模型是否满足要求。
最后,还需要考虑到收集数据的时候能够获得的数据,以及使用该模型时所处的场景。例如,假定我们知道,雇员流失模型要被用作决定是否聘用某个应聘者的因素之一。在这个背景下,就只能从现有雇员的数据中收集他们被聘用之前的那部分数据。我们不能使用他们的第一次绩效评价的结果,因为对于应聘者来说,这些数据还不存在。
1.3.2 收集数据
训练一个模型用于预测往往是一种数据密集型的探险,如果这里面有一种东西是多多益善的话,那就是数据。数据收集经常是整个过程中最耗费时间和资源的部分,这也就是要在第一步把定义任务和正确识别要收集的数据两项工作做好的重要性所在。当我们学习诸如逻辑回归之类模型的工作原理时,通常会通过一个示例数据集来进行,这也是我们在本书中遵循的主要方法。不幸的是,没有模拟收集数据这个过程的方法,这样看起来我们的大部分精力是花在训练和改进模型上的。在利用已有数据集来学习模型的时候,必须牢记,通常有很多精力是花在收集、甄选和预处理数据上的。我们会在后续章节更仔细地观察数据预处理。
在收集数据的时候,要一直关注我们收集的数据种类是否正确。很多在预处理阶段进行的合理性检查在收集过程中也适用,这是为了让我们能发现是否在该过程的早期犯了错误。例如,总是要检查对特征的测量是否正确,是否采用了正确的度量单位。还必须确保数据是从足够近期、可靠且与手头任务相关的数据源收集的。在上一节讲解的雇员流失模型里,当收集以往雇员的信息时,必须确保对特征的度量是一致的。例如,在度量某个人在公司里工作了多少天的时候,必须一致地使用日历天或工作日。在收集日期数据的时候还必须进行格式检查,例如当某人加入或离开公司的时候,要统一使用美国日期格式(月份后面是日期)或欧洲格式(日期后面是月份),这两种格式不能混用,否则,一个类似03/05/2014的日期就会带有歧义。我们还要尽可能从较大范围的样本获取信息,以免在数据收集过程中引入隐藏的偏误。例如,如果我们需要雇员流失的通用模型,就不能只从女性雇员或者单个部门的雇员收集数据。
我们如何知道已经收集了足够多的数据了?一开始,在收集数据的过程中,没有创建和测试任何模型的时候,还无从得知最后需要多少数据,而且这时候也没有任何简单的经验规则可以照搬。不过,我们可以预见问题的某些特征会需要更多的数据。例如,在创建一个可以从3个类别中学习预测的分类器的时候,可能就需要检查是否有足够的观测数据来代表每个类别。
输出类别的数量越大,需要收集的数据就越多。类似地,对于回归模型,对照预测结果的范围来检查训练数据里的输出变量范围也是有用的。如果要创建一个覆盖很大输出范围的回归模型,与同等精确度需求下覆盖较小输出范围的回归模型相比,也需要收集更多的数据。
另一个有助于估算需要多少数据的重要因素就是模型的性能要求。直观地说,模型所需要的精确度越高,必须收集的数据就越多。还必须注意,改善模型的性能并非一个线性的过程。把精确度从90%提高到95%往往会比从70%到90%的飞跃还需要更多的努力以及多得多的数据。有更少参数或设计更简单的模型,例如线性回归模型,需要的数据往往比类似神经网络等更复杂的模型更少。最后,要放到模型里的特征数目越多,必须收集的数据量就越大。
此外,必须注意到一个事实:对额外数据的需求也不会是线性的。也就是说,创建一个带有2倍数量的特征的模型往往需要的数据量远远多于原有数据量的2倍。考虑到模型所需要处理的输入的不同组合的数量,这个道理应该是显而易见的。增加2倍的维度数量会导致远大于2倍的可能的输入组合数量。为了理解这个道理,假定有个模型带有3个输入特征,每个特征可以取10个可能的值,我们就有了103=1000种可能的输入组合。如果加入1个额外的特征,它也可以取10个可能的值,就把可能的组合增加到了10 000个,这就比原先输入组合数量的2倍大多了。
有一些研究工作在尝试从更定量的视角讨论某个数据集是否有足够数据的问题,但我们在本书中没有时间讲解它们。更深入学习预测建模的这个领域的一个好的起步点是研究学习曲线(learning curve)。简而言之,借助这种方法,通过先从小部分数据起步并逐渐增加更多数据,可以对同一组数据创建连续的模型。这里的思路是,通过这个过程,如果预测的精确度对于测试数据总是在改善而不是递减,我们就很可能可以通过获取更多数据受益。作为数据收集阶段的最后一个提示是,即使我们认为已经有了足够的数据,在选择终止收集并开始建模之前,也总是应该考虑为了获得更多数据会给我们带来多少成本(从时间和资源方面而言)。
1.3.3 选取模型
一旦明确了预测任务,并得到了合适的数据,下一步就是选取我们的第一个模型。首先,根本就没有什么最好的模型,甚至运用某些经验法则的最优模型也是不存在的。在大部分情况下,明智的做法是在起步阶段先用一个简单模型,例如在分类任务的情况下采用朴素贝叶斯模型(Na飗e Bayes model)或逻辑回归(logistic regression),或在回归的情况下采用线性模型(linear model)。简单的模型会让我们得到一个初始的基线性能,然后我们就可以努力地改进它。起步的简单模型也有利于回答一些有用的问题,例如有多少特征对结果有贡献,也就是说,每个特征的重要性有多大,它和输出是正相关还是负相关。有时候,这种分析本身就是产生第一个简单模型并延续到用于最终预测的更复杂模型的依据。
有时候,简单模型可能就对我们手头的任务给出了足够的精确度,因此我们就无需投入更多精力来获得那么一丁点额外的改进。另一方面,简单模型通常对于任务来说是不够的,这就要求我们挑出更复杂的模型。选择更复杂模型而非简单模型并不总是简单直接的决定,即使我们可以看到复杂模型的精确度会高很多。具体的一些限制条件(例如现有的特征数量或能够获得的数据)都可能阻碍我们转而使用更复杂的模型。要了解如何选择一个模型,就涉及理解工具箱里的那些模型的各种优势和限制。对于在本书遇到的每种模型,我们都要特别注意学习这些要点。在实际项目中,为了帮助指导我们的决定,需要经常回顾任务需求,并提出一些问题,例如:
承担的任务是什么类型的?某些模型只适合特定任务,例如回归、分类或聚类。
模型是否需要解释它的预测结果?某些模型,例如决策树,更善于给出易于解读的要点,用于解释它们作出具体预测的原因。
是否有任何关于预测时间的限制?
是否需要频繁更新模型?从而导致的训练时间是否很关键?
如果特征高度相关,该模型是否能正常工作?
该模型是否能根据特征的数量和得到的数据量有效地进行伸缩?例如,如果我们有海量的数据,我们可能需要一个训练过程能够并行化的模型,以便利用并行计算机架构。
在实践中,即使初步分析指向了特定模型,在作出最终决定之前,也很有可能需要尝试很多其他选项。
1.3.4 数据的预处理
在使用数据训练模型之前,通常需要对它进行预处理。在本节,我们会讨论一批一些常用到的预处理步骤。其中一些是为了检测并解决数据中的问题所必需的,而其他的则是用于变换数据让它们适用于选择的模型。
探索性的数据分析
一旦有了一些数据,并决定针对某个具体模型开始工作,首先需要做的事情就是查看数据本身。这不一定是流程中非常结构化的一部分,它主要包括理解每个特征测量的是什么,以及对我们已经收集的数据进行初步了解。理解每个特征代表了什么,以及它的度量单位是什么,这些步骤确实是非常重要的。对其度量单位的一致性进行检查也是一个很好的思路。我们有时把这项对数据进行探索并可视化的分析过程称为探索性数据分析(exploratory data analysis)。
一个很好的实践是对我们的数据框调用R语言的summary()函数,获得每个特征的一些基础衡量指标,例如均值和方差,还有极大极小值等。有时候很容易通过数据中的不一致发现在数据收集过程中有一些错误。例如,对于一个回归问题,多条观测数据具有相同的特征值,输出却大相径庭,这就有可能(取决于应用的情况)是一个信号,表明存在错误的测量值。类似地,了解特征的测量过程中是否存在显著的噪声也是一个好的思路。这样有时候可能会产生不同的模型选择,或意味着该特征必须被忽略。
另一个对数据框里的特征进行概括的有用函数是psych包里的describe()函数。它返回有关每个特征的偏斜程度,以及针对位置(例如均值和中位数)和分布情况(例如标准差)的常规衡量指标。
探索性的数据分析的一个重要部分是利用绘图把数据可视化。根据情景的不同,有多样化的绘图可以供我们使用。例如,为数值特征创建箱线图(box plot)来对范围和四分位数(quartiles)进行可视化。条形图(bar plot)和马赛克图(mosaic plot)可以用于数据的分类输入特征在不同组合下的比例情况进行可视化。本书不会在信息可视化方面探讨更多的细节,因为它是一个自成体系的领域。
R是一个创建可视化的完美平台。基础R软件包提供了很多不同的函数来对数据进行绘图。有两个能创建更多高级绘图的杰出扩展包,它们是lattice和ggplot2。有两本关于这两个扩展包的参考书,其中还讲解了有效进行可视化的原理,它们是《Lattice: Multivariate Data Visualization with R》和《ggplot2: Elegant Graphics for Data Analysis》,它们都是Springer出版社出版的,属于Use R!系列教材之一。
特征变换
通常,我们会发现数值特征是用完全互不相同的进位制来衡量的。例如,用摄氏度来衡量某人的体温,因此它的数值通常会在36~38的范围之内。同时,我们也会测量某人每微升血液中的白细胞计数。这个特征一般是以千来取值的。如果要把这些特征作为某个算法(例如kNN)的输入,会发现白细胞计数的较大数值会在欧几里得距离的计算中处于支配地位。在输入里可能还有一些对于分类重要且有用的其他特征,但如果衡量它们的进位制产生的是比1000小很多的值,我们实际上就主要是根据单个特征(也就是白细胞计数)来选取我们的最邻近的样本。这种问题通常会出现于并影响到很多模型,而不仅仅是kNN。我们处理这种问题的方法是,在将这些输入特征用于我们的模型之前,对这些特征进行变换(也称为比例缩放)。
我们会讨论特征比例缩放的三种流行方法。当我们知道我们的输入特征接近正态分布的时候,一种可能用到的变换是Z评分归一化(Z-score normalization),它的原理是对特征减去其均值并除以其标准差:
E (x)是x的期望或均值,标准差是x的方差的平方根,该方差表示为Var (x)。注意,这种变换的结果是,新的特征将以均值0为中心,方差为1。另一种可能的变换更适合输入符合均匀分布的情况,就是按比例缩放所有的特征和输出,让它们处于单个区间内,典型的是单位区间[0,1]:
第三种方法被称为博克斯-考克斯变换(Box-Cox transformation)。这种变换通常运用在输入特征呈现高度偏斜(不对称)而我们的模型要求输入特征符合正态分布或至少是对称分布的情况:
因为是在分母里,所以它必须取非0的值。该变换实际上对值为0的是有定义的:在本例中,它就是输入特征的自然对数,即ln(x)。
注意,这是一种参数化的变换,因而需要给指定一个具体的值。有多种方式可以根据数据本身给估算一个适当的值。我们会为此讲解一种进行这种估算的技术,称为交叉验证(cross-validation),我们会在本书第5章里遇到它。
博克斯-考克斯变换的原始参考资料是在1964年发表在皇家统计学会杂志(the Journal of the Royal Statistical Society)上的一篇论文,名为《An analysis of Transfor-mations》,作者是G. E. P. Box和D. R. Cox两人。
为了直观感受这些变换在实践中的应用情况,我们要在iris数据集的Sepal.Length特征上进行尝试。不过,在开始之前,要介绍需要运用的第一个R扩展包,即caret。
caret包是一个具备多种用途的非常有用的组件。它提供了预测建模过程中常用的一组辅助函数,覆盖了从数据预处理和可视化到特征选择和再抽样技术。它的特性还包括:具有针对很多预测模型函数的统一接口,并提供了并行处理的功能。
利用caret组件进行预测建模的权威参考资料是一本名为《Applied Predictive Modeling》的书,该书由Max Kuhn和Kjell Johnson编写,Springer出版社出版。Max Kuhn是caret包本身的主要作者。该书还有一个配套的网站:http:// appliedpredic-tivemodeling.com。
当对用来训练模型的数据进行输入特征的变换时,必须记住,对于在预测时要用到的后续输入,也需要对其中的特征运用同样的变换。因此,利用caret包对数据的变换是分两步完成的。在第一步,调用preProcess()函数保存要运用到数据上的变换参数,然后在第二步,调用predict()函数对变换进行实际的计算。我们往往会只调用一次preProcess(),然后在每次需要对某些数据进行同样的变换时就调用predict()函数。preProcess()函数会把一个带有某些数量值的数据框作为它的第一个输入,我们也会给method参数指定一个包含要运用的变换名的向量。然后predict() 函数会一并读入前面函数的输出和我们要变换的数据—对于训练数据本身,它很可能是同一个数据框。让我们看看这整个过程的实际运用:
下载样例代码
你可以从你在http://www.packtpub.com的账号下载你已经购买的所有Packt出版书籍的样例代码文件。如果你是在其他地方购买本书,你可以访问http://www.packtpub.com/support并注册,文件会直接通过电子邮件发送给你。
我们已经创建了鸢尾花数据的数值特征的3个版本,它们的区别在于每个版本用到了不同的变换。为了把变换的效果可视化,我们可以通过调用density()函数,对每个比例缩放后的数据框绘制萼片长度(Sepal.Length)特征的密度图,并绘制结果,如下所示:
注意,Z评分(Z-score)和单位区间(unit interval)变换在对数值进行了平移和缩放的同时保留了密度图的总体形态,而博克斯-考克斯变换还改变了整体形态,产生了比原始图偏斜更少的密度图。
类别特征的编码
从线性回归到神经网络,很多模型都要求所有输入是数值型的,因此我们经常需要一种把分类字段在某个数值标尺中进行编码的方法。例如,如果我们有一个尺寸特征,它从一个集合{small, medium, large}中取值,我们就会需要用数值1、2、3来分别代表它。在有序分类的情况下(例如刚才描述的尺寸特征),这种映射应该是合理的。
数字3是这个数值区间里最大的,它对应的是large分类,相比用取值2代表的medium分类,它离用数字1代表的small分类更远。这个数值区间的用法只是一种可能的映射,尤其是它强制让medium分类与large和small分类的间距相等,这样做是否合适,取决于我们对于具体特征的认知。在无序分类的情况下,例如品牌或颜色,总体上我们会避免把它们映射到单个数值区间里。例如,如果把集合{blue, green, white, red, orange}相应地映射为数字1到5,那么这个区间就是随意的,没有什么理由让red离white更近而离blue比较远。为了克服这个问题,我们创建了一系列指示特征Ii,它的形式如下:
我们需要和分类一样多的指示特征,因此对于颜色示例,可以创建5个指示特征。在这个情况下,I1就可以是:
通过这种方式,原始的颜色特征会被映射为5个指示特征,对于每条观测数据,这些指示特征中只有一个会取值为1,其他的都为0,这是因为在原始特征中,每条观测数据只包含一种颜色值。指示特征是二进制的特征,因为它们只会取两个值:0和1。
我们经常还会遇到另一种方法,即利用n-1个二进制特征对一个因素的n个级别进行编码。这是通过选择一个级别作为参考级别,它用所有n-1个二进制特征都取值为0的情况来表示。这样对于特征数量而言更为经济,并且可以避免在它们之间引入一种线性依赖关系,但是它违反了所有特征相互之间等距的性质。
缺失数据
有时候,数据会包含缺失的值,这是因为对于某些观测数据,某些特征是无法获取或无法合适地测量的。例如,假定在iris数据集里,缺少了某条观测数据里的花瓣长度(petal length)的测量值。这样,在这个鸢尾花样本的Petal.Length特征里就会有一个缺失值。大部分模型并不先天具备处理缺失数据的能力。通常情况下,数据里的缺失值会用空条目或NA符号表示。我们必须检查是否在数据中实际存在着缺失值,但被错误地分配了一个像0这样的(通常是非常合法的)特征值。
在决定如何处理缺失数据之前,尤其是当我们的方法是直接丢弃带有缺失值的观测数据时,必须认识到具体缺失的值可能会符合某个模式。具体而言,我们经常要区分不同的所谓缺失值机制。在理想的随机完全缺失(Missing Completely At Random,MCAR)情况下,缺失值的出现是独立于它们所属的特征以及其他特征的真实值的。在这种情况下,如果某个鸢尾花花瓣长度缺失了一个值,那么这个缺失的情况是独立于该花瓣实际有多长以及其他任何特征(例如该观测数据是否来自versicolor或setosa品种)的。随机缺失(Missing At Random,MAR)的情况就更不那么理想。在这种情况下,某个缺失值是独立于其所属特征的真实值的,但可能会和其他特征相关。这种情况的一个示例是,缺失的花瓣长度值主要在iris数据集的setosa品种样本里出现,但它们的出现仍然独立于实际花瓣长度的值。在问题最多的非随机缺失(Missing Not At Random,MNAR)情况里,存在某种模式能够解释缺失值会根据该特征本身的真实值出现。例如,如果在测量非常小的花瓣长度时遇到困难,并且最终产生了一些缺失值,那么,在这种情况下简单地丢弃不完整样本,就可能只得到花瓣长度较大的观测数据样本,因而使数据产生偏误。
处理缺失值有很多种方法,但在本书中我们不会深入钻研这个问题。在有缺失值的少数情况下,我们会从数据集里排除它们,但是要当心,在一个实际项目里,要调研缺失值的来源,以便确保能安全地进行排除操作。另一种方法是尝试猜测或估算缺失值。kNN算法本身就是一种这样的方法,它会给某个特征有缺失值的样本找到最邻近的样本。具体做法是先排除缺失值所在的维度(即特征),再进行距离的计算。随后,这个缺失值就可以被计算为最邻近的样本在该维度取值的均值。
对如何处理缺失值感兴趣的读者可以在Wiley出版社出版的《Statistical Analysis with Missing Data》(第二版,由Roderick J. A. Little和Donald B. Rubin编写)一书中找到详细的论述。
离群值
离群值(Outlier)也是经常需要处理的一种问题。离群值是其中一个或多个特征严重偏离了其他数据的某条观测数据。在某些情况下,这也可能是某个真实的罕见状况,代表了在我们要建模的系统里的一种合乎情理的表现。在其他情况下,这可能说明在测量数据中存在一个错误。例如,在记录人口年龄的时候,一个110的值可能会是离群值,有可能是因为对实际值11的记录错误而产生的。但它也有可能是一个正确而极为罕见的测量值。通常,要处理的问题领域会对于离群值是否属于测量错误给出很好的指示,如果是这样的话,作为数据预处理的一部分,常常会需要从数据中完全排除离群值。在第2章中我们会看到排除离群值的更多细节。
丢弃有问题的特征
预处理数据集也包括了丢弃某些特征的决定,如果知道它们会给我们的模型带来问题的话。一个常见的示例是两个或多个特征相互之间高度相关的情况。在R里,可以利用cor()函数轻松地对某个数据框计算其中每一对特征的相关系数:
在这里,可以看到Petal.Length特征和Petal.Width特征是高度相关的,它们的相关系数大于0.96。caret组件提供了findCorrelation()函数,它把一个相关系数矩阵作为输入,还可以选用为特征配对的相关系数的绝对值指定一个阈值的cutoff参数。它会返回一个(有可能长度为0的)向量,该向量会显示由于相关性而需要从该数据框里删除的列。cutoff的默认设置是0.9:
消除相关性的另一种方法是对整个特征空间进行完全的变换,例如在很多降维(dimen-sionality reduction)方法里采用的技术,比如主成分分析(Principal Component Analysis,PCA)和奇异值分解(Singular Value Decomposition,SVD)。我们稍后会介绍前者,后者则会在第11章里介绍。
类似的,还会需要删除相互为线性组合(linear combinations)的特征。对于特征的线性组合,指的是把每个特征乘以一个标量常数所得到的特征之和。为了查看caret包是如何处理这些特征,我们要创建一个新的鸢尾花数据框,在它里面增加两个列,分别名为Cmb和Cmb.N,如下所示:
正如我们所见,Cmb是Sepal.Length 和Petal. Width特征的完全线性组合。Cmb.N则是和Cmb相同但加上了某些均值为0且标准差很小(0.1)的高斯噪声的特征,因而它的值很接近Cmb的值。利用findLinearCombos()函数,caret包能够检测出严格的特征线性组合,不过当这些特征有噪声时就检测不到了:
正如我们所见,该函数只建议我们应该从该数据框中删除第5个特征(Cmb),因为它是第1和第4个特征的一个严格的线性组合。严格的线性组合很少见,不过当有很大数量的特征并且它们之间存在冗余时会出现。具有相关性的特征以及线性组合都是线性回归模型里的问题,我们很快会在第2章看到。在本章,我们也会看到一种检测相互之间非常接近线性组合的特征的方法。
对于有问题的特征要审视的最后一个问题,就是在数据集里存在根本不变化或者说方差近似为0的特征。对于某些模型,具有这种类型的特征不会给我们带来问题。对于其他模型,它可能就会产生问题,我们会演示为何会是这样的。正如前一个示例,我们会创建一个新的鸢尾花数据框,如下所示:
这里,ZV列对于所有观测对象都是一个常数6.5。Yellow列则是一个虚构的列,用来记录观测对象在花瓣上是否带有黄色。除了第一个以外,其他所有的观测对象都设定这个特征为FALSE,因而让它成为方差近似为0的列。caret包采用的是方差近似为0的定义,这个定义会检查某个特征取的唯一值数量和总观测对象数量相比是否非常小,或者最常见值和第二常见值出现数量的比例(被称为频数比,frequency ratio)是否非常高。把nearZeroVar()函数运用到一个数据框,会返回包含了方差为0或近似为0的特征构成的一个向量。通过设定saveMetrics参数为TRUE,我们可以看到更多关于我们数据框里的特征的信息:
在这里,我们可以看到,ZV列被识别为方差为0(zeroVar=TRUE)的列(由定义可知,它也是方差近似为0的列)。Yellow列确实有非0方差,但它较高的频数比和较低的独特值比例使它被判定为方差近似为0的列(nzv=TRUE)。在实践中,往往会删除方差为0的列,这是因为他们不会为我们的模型提供任何信息。不过,要删除方差近似为0的列就比较微妙,必须小心从事。要理解这一点,可以考虑这样一个事实:利用上面那个newer_iris数据集进行品种预测的某个模型会学习到,如果某个样本在花瓣里有黄色,那么不管所有其他预测因素如何,我们都会预测出它是setosa品种,因为它对应的是整个数据集里唯一的一个在花瓣里有黄颜色的观测数据。这在现实中也许确实成立,如果是这样的话,这个黄色的特征就是有信息量的,我们也应该保留它。另一方面,在鸢尾花瓣中出现黄颜色也可能是完全随机的,不仅不能标识品种,而且是极罕见的情况。这就解释了为什么在数据集中只有一条观测数据在花瓣里带有黄色。在这种情况下,由于前面所说的结论,保留该特征就是危险的。保留该特征的另一个潜在问题会在我们把数据划分为训练和测试数据集或以其他方式划分数据(比如第5章里讲解的交叉验证)的时候浮现出来。这里的问题是,对我们数据的划分会导致方差近似为0的列里出现唯一值,例如某个数据集的Yellow列里只有FALSE值。
1.3.5 特征工程和降维
在模型里采用的特征数量和类型是在预测建模过程中要作出的最重要决定。为模型配备正确的特征,就是确保我们为预测准备了充分的依据。在另一面,要处理的特征数量就正好是该模型具有的维度数量。大量的维度会成为复杂度加剧的根源。高维的问题通常来自于数据的稀疏性(data sparsity),它意味着由于存在的维度数量的原因,会导致覆盖所有特征取值的可能组合范围的极大增长,以至于难以收集足够的数据为训练过程提供足够的有代表性的样本。我们还通常谈到维度诅咒(curse of dimensionality)。它描述了一个事实:由于可能的输入产生的空间之大是压倒性的,导致我们所收集的数据点在特征空间里很可能会相互远离。其结果是局部方法(例如利用训练数据中靠近待预测点的观测数据来进行预测的kNN算法)在高维空间里的效果不佳。太大的特征集合还会产生显著增加训练模型(以及在某些情况下作出预测)所需时间等方面的问题。
因此,特征工程包括两类流程。第一类会增大特征空间,它会根据我们数据里的特征来设计新的特征。有时候,由两个原始特征的乘积或比率构成一个新特征会有更好的效果。有多种方法可以把已有特征组合为新特征,而这通常需要相关问题具体应用领域的专业知识的指导。从总体思路而言,这个过程需要经验和大量的反复试验。注意,无法保证加入新特征一定不会降低性能。有时候,增加一个噪声很大或者与已有特征高度相关的特征确实会导致我们损失精确度。
特征工程的第二类过程是特征缩减或收缩,它会减小特征空间的尺寸。在前面关于数据预处理的小节可以看到如何检测会给模型带来某些问题的个别特征。特征选择(feature selection)指从原始的一组特征中选取对于目标输出最具信息量的特征子集的过程。某些方法(例如树形模型)具备内建的特征选择方法,这是我们将会在第6章里看到的。在第2章我们也会探讨对线性模型进行特征选择的一些方法。另一种减少总特征数的方法,即降维(dimensionality reduction)的概念,是把整个特征集变换为全新的一个数量更少的特征集。这方面的一个经典范例是主成分分析(Principal Component Analysis,PCA)。
简而言之,PCA 会创建一个输入特征的新集合,它被称为主成分(principal components),这个集合中全部都是原始输入特征的线性组合。对于第一个主成分,要选取线性组合的权值,以便能获取数据中最大量的变异(variation)。如果我们能够把第一个主成分可视化为原始特征空间里的一条直线,它应该是上面的数据变异最大的一条直线。它还恰好应该是最接近原始特征空间上所有数据点的一条线。后续的每个主成分要尽可能获取最大变异的一条直线,但是要满足的条件是,新的主成分和之前计算得到的主成分不相关。因此,第二个主成分要选取在数据中具有最高程度变异的原始输入特征的线性组合,同时和第一个主成分不相关。
主成分是根据它们获取的变异量以降序进行自然排序的。这让我们能够通过保留前N个组成部分来以简单的方式进行降维,我们在这个过程中选取N的依据是,这样选取的主成分元素要包含来自原始数据集最小量的方差。我们不会深入介绍计算主成分所需的相关线性代数方面的细节。
相反,我们会把关注点转移到一个事实:这个过程是对原始特征的方差和度量单位敏感的。为此,我们经常会在进行这个过程之前对特征进行比例缩放。为了可视化地讲解PCA多么有用,我们会再一次转到我们忠实的鸢尾花(iris)数据集。可以利用caret包执行PCA算法。为此,指定在preProcess()函数的method参数为pca。也可以利用thresh参数,它指定了必须保持的最小方差。我们会显式使用0.95这个值,这样就保持了原始数据95%的方差,但是注意,这也是该参数的默认值:
这个变换的结果是,我们现在只剩下了2个特征,所以我们可以得出结论:鸢尾花的数值特征的前2个主成分就包含了该数据集里超过95%的变异。
如果我们对了解用来计算主成分的权值感兴趣,可以查看pp_pca对象的rotation属性:
这意味着第一个主成分PC1的计算方法如下:
0.52·Sepal.Length-0.27·Sepal.Width+0.58·Petal.Length+0.57·Petal.Width
有时候,相比直接指定一个主成分获取的总方差的阈值,我们可能会想要查看每个主成分及其方差的绘图。这种图称为陡坡图(scree plot),要绘制这样的图,可以先进行PCA并声明我们要保留所有的主成分元素。为此,不要指定一个方差的阈值,而是设定要保留的主成分数量的pcaComp参数。主成分的总数和开始时的原始特征或维度数是相等的,所以就设置它为4,也就是包括了所有主成分。然后计算这些组成部分的方差和累积方差,并把它保存在一个数据框里。最后,我们要在后续的图表中绘制它,并注意图中括号里的数字是获取方差的累积百分比:
正如我们所见,第一个主成分占到了iris数据集总方差的73.45%,再加上第二个主成分,获得的总方差是96.27%。PCA 是一种无监督的降维方法,它不需要利用输出变量,即使输出结果能够获得。相反,它是从几何的角度去看待特征空间里的数据的。这意味着我们不能确保PCA给出的新特征空间一定能在预测问题里有良好表现,它只能带来更少特征的计算优势。即使PCA减少了模型的精确度,但是只要这种精确度的减少比较小而且对于特定任务是可以接受的,计算上的这些优势就会让它成为可行的选择。作为最后一条提示,必须指出,通常被称为荷载(loading)的主成分权值在归一化的情况下是唯一的,只有正负号的变化。在特征之间具有完全相关或完全线性组合的情况下,我们会得到一些恰好为0的主成分。
1.3.6 训练和评估模型
在之前对参数化模型的讨论中,我们看到这些模型会有一个利用训练数据集对模型进行训练的过程。而对于非参数化模型来说,通常它们要么进行惰性学习—也就是除了记住训练数据之外并没有真正的训练过程;要么像样条的情况那样,会对训练数据进行局部的计算。
无论哪种方式,如果要评估模型的性能,就需要把数据分拆为一个训练集(training set)和一个测试集(test set)。这里的关键思路是,要根据模型处理后续未知数据的预期表现来评估我们的模型。我们的做法是利用测试集,也就是把收集到的数据的一部分(通常是15%~30%)留出来,在训练过程中不要用到它们。例如,一种可能的分拆方式是把原始数据里80%的观测数据作为训练集,把剩下的20%作为测试集。需要一个测试集的原因是,我们无法用训练集公平地评估模型的性能,因为训练数据是用来拟合模型的,而它并不能代表我们之前没有看到过的数据。从预测的角度来说,如果我们的目标只是针对训练数据产生最佳性能,那么最好的手段就是直接记住输入数据和期望的输出值,这样我们的模型就成了一个简单的查找表了!
有一个好问题是,如何决定把多少数据分别用于训练和测试。这里就涉及一些权衡,让这个问题的答案意义重大。一方面,我们会想把尽可能多的数据放在训练集里,这样模型就可以根据更多的样本进行学习。另一方面,我们也想有一个大的测试集,这样就能使用很多样本对训练好的模型进行测试,以便让我们对该模型预测性能的估算具有最小的方差。如果我们的测试集里只有少量观测数据,那么我们就无法真正总结出模型用于未知数据的性能的一般规律。
另一个起作用的因素是,初始收集的数据量有多大。如果只有很少的数据,我们可能就必须用其中的大部分去训练模型,比如划分为85%/15%。如果我们有充足的数据,那么我们也许会考虑70%/30%的划分,这样就能对于测试集产生更精确的预测。
要利用caret包划分数据集,可以用createDataPartition()函数创建一个抽样向量,里面包含了要在测试集里用到的那些行的索引。对这些行的选取过程是通过利用p参数对行进行随机抽样,直到抽样的行数达到预先指定的比例:
在报告涉及生成随机数的统计学分析结果时,有一种可取的做法是,用随机选取但固定的一个数值调用set.seed()函数。该函数会确保从后续关于随机数生成的函数调用所生成的随机数在代码每次运行的时候都是相同的。这样做能让其他阅读这个分析过程的人能准确地重现分析结果。注意,如果代码里有几个函数进行了随机数的生成,或同一个随机数生成函数被调用了多次,原则上我们必须在每一次调用之前先调用set.seed()。
利用为iris数据集创建的抽样向量,我们就能构建自己的训练集和测试集了。我们会对之前在试验不同的特征变换方法时构建的一些iris数据集版本进行这个操作。
现在我们就可以为iris数据集创建并测试3种不同的模型。它们分别是未归一化(unnormalized)模型、输入特征已通过Z评分(Z-score)变换进行中心化和比例缩放的模型,以及带有两个主成分的PCA模型。在创建好这些模型之后,可以使用测试集来衡量它们中每一个的预测性能。不过,这样就意味着在我们对其未知精确度的最终估算中,已经在模型选取过程中用到了测试集,因而产生了有偏误的估算。因为这个原因,我们经常会保持一套单独划分的数据,通常和测试集一样大,被称为验证集(validation set)。这个数据集是用来在运用测试集预测未知性能之前对模型参数(例如kNN里的k值)以及输入特征的不同编码和变换进行调优的。在第5章中,我们会讨论这种方法的一种替代方法,称为交叉验证(cross-validation)。
一旦划分了数据,按照所要求的相关训练过程训练了模型,并对模型参数进行了调优,我们就必须在测试集上评估它的性能。通常我们不会在测试集里得到和训练集里相同的性能。有时候我们甚至可能会发现,当部署模型的时候,看到的性能和根据训练集或测试集的性能而得到的预期并不相符。这种性能上的不一致有很多可能的原因。其中第一个原因就是,之前收集的数据要么对建模的过程不具有代表性,要么就是某些特征输入的组合没有纳入训练数据中。这样就可能产生和预期不一致的结果。这种情况既可能发生在真实世界中,也可能发生在测试集里,比如它包含了离群值(outlier)的情况。另一种普遍存在的情况是模型过拟合(over fitting)问题。
过拟合问题的表现是这样的:某些模型,尤其是更灵活的模型,在它们的训练数据集里表现良好,但对于未知的测试集就明显表现更差。在这种问题里出现的情况是,某个模型对训练数据中的观测数据拟合得过于严密,却无法对未知数据广义化。换言之,该模型在训练数据集中提取了不真实的细节和变异,而这些细节和变异对于整体的基础观测数据并不具有代表性。过拟合是我们不能根据训练数据的性能来选取模型的关键原因之一。其他训练和测试数据性能不一致的来源还包括模型偏误和方差。这些因素合在一起就构成了统计性建模领域中的一种著名的权衡,即偏误-方差权衡(bias-variance tradeoff)。
统计性模型的方差是指,如果使用另一个选取的训练集(但还是从要预测的同一个原始过程或系统生成的)来训练模型,模型预测出的函数会有多大的变化。我们想要的是一个较低的方差,因为我们当然不希望从同一个过程生成的不同训练集预测出很不一样的函数。模型偏误则是指在预测函数中内生出来的错误,它是关于具体的一个模型能够学习哪些函数形式的局限性的结果。例如,线性模型会在试图拟合非线性函数的时候引入偏误,这是因为它们只能学习线性函数。一个良好的预测模型的理想状况是它兼具了低方差和低偏误。对于一个预测建模者,很重要的一点是注意这样的事实:偏误-方差权衡(bias-variance trade-off)来源于对模型的选择。对目标函数作出更少假设而产生的复杂模型,相比于更简单却更严格的模型(例如线性模型),往往会有更少的偏误和更大的方差。究其原因,是因为更复杂模型具有的灵活性让它们能够更近似地估算训练数据,但这样的结果是它们对训练数据里的变化也会更敏感。当然,这种情况和复杂模型经常会表现出来的过拟合问题也是相关的。
我们可以通过先在iris数据集上训练某些kNN模型来实际观察过拟合的效果。有很多扩展包都提供了kNN算法的实现,但是我们会使用我们熟悉的caret包提供的knn3()函数。要用这个函数训练模型,我们所要做的就是给它提供一个包含数值输入特征的数据框、一个输出标签的向量,以及我们要用于该预测的最近邻数量k:
要观察k的不同取值的效果,我们可以利用现成的2个维度的鸢尾花PCA模型,用来进行可视化和反复训练:
在前面的图中,我们使用了不同的符号来注明对应不同品种的数据点。图中显示的线对应了不同鸢尾花品种之间的判定边界(decision boundary),这些品种也就是我们输出变量的类别标签。注意,使用一个较低的k值(例如1),会非常严密地获得在数据中的局部变异,结果是判定边界非常不规整。更大的k值要利用很多邻近点来创建一个预测,产生的是平滑的效果和更平滑的决策边界。在kNN里对k值进行调优就是一个对模型参数进行调优以平衡过拟合效果的示例。
在本节,我们还没有提到过任何具体的性能衡量指标。回归和分类对于模型的质量有不同的衡量指标,我们会在对关于预测建模过程的讨论进行总结之后再讲解它们。
1.3.7 重复尝试不同模型及模型的最终选择
在这个过程(而且肯定是一个迭代过程)的第一次迭代中,我们通常会在训练并评估完一个简单模型后到达这个阶段。简单模型通常让我们能以最少的精力获得一个快速而粗略的解决方案,从而让我们大致感受到离一个会产生合理精确度预测的模型还有多远。简单模型还可以给我们产生一个性能的基线水平,我们可以用它作为未来模型性能的标杆。作为建模者,我们在不同方法之间往往会厚此薄彼,但重要的是记住,要对解决问题的不同方法进行尝试,并利用数据帮助我们决定最终采用哪一个,这是值得花费精力的事情。
在选取最终的模型之前,有必要考虑的事,采用多个模型来解决我们的问题是否是个好的思路。在第7章里,我们会用一整章来学习涉及多个模型共同作用来提高整体系统的预测精确度的技术。
1.3.8 部署模型
一旦选取了采用的最终模型,我们就需要落实它的实现,这样最终用户就能可靠地使用它。程序员们称该过程为部署到生产系统(deploying to production)。这是扎实的软件工程原理会起到极其重要作用的领域。下面的指导原则给出了一些有用的建议:
模型必须进行优化,以提高它计算预测结果的速度。例如,这意味着确保任何要在运行时计算的特征能高效地完成处理。
模型必须有完备的文档。最终的输入特征必须清晰定义,用于训练的方法和数据必须保存,以便在需要更改时易于重新进行训练。训练集和测试集的原始性能也必须保存下来,作为后续改进的参考。
模型性能必须持续进行监控。它的重要性不仅在于作为验证模型是否达到预期效果的手段,而且在于捕捉任何潜在的数据变化。如果要建模的过程随时间改变了,我们模型的性能就有可能下降,这就表明了训练一个新模型的必要性。
用来实现该模型的软件必须利用标准的单元和集成测试方法进行适当的测试。通常,我们会利用大量已有的R语言包,它们的函数已经通过了测试,但模型最终的部署会需要我们自己编写某些额外的代码,比如用来进行特征计算的代码。
部署的模型必须能处理输入中的错误。例如,如果某些输入特征是缺失的,它应该通过适当的错误信息让用户明确了解模型无法作出预测的原因。错误和警告也必须写进日志,尤其是对于模型被部署用于实时环境下的持续性预测的情况。