Python-回归分析-全-
Python 回归分析(全)
原文:
annas-archive.org/md5/7493aa2d1357131907468d86f8aaf57d译者:飞龙
前言
| "Frustra fit per plura, quod potest fieri per pauciora.**(用更多的东西去做可以用更少的东西完成的事情是徒劳的)" | ||
|---|---|---|
| --威廉·奥卡姆(1285-1347) |
线性模型早已为学者和实践者所熟知,并被他们长期研究。在它们被纳入数据科学领域,并被众多训练营的课程大纲以及许多实践指南的早期章节所采用之前,它们一直是统计学、经济学以及许多其他受人尊敬的定量研究领域知识体系中的一个突出且相关的元素。
因此,关于线性回归、逻辑回归(其分类变体)以及不同类型的广义线性模型,有大量的专著、书籍章节和论文;这些模型在公式化中适应了原始的线性回归范式,以解决更复杂的问题。
尽管如此,尽管有如此丰富的资源,我们从未遇到过任何一本书真正解释了当作为一个开发者或数据科学家,你必须快速创建一个应用程序或 API,其响应不能通过编程定义,但确实需要从数据中学习时,这些线性模型的快速和易于实现的速度。
当然,我们非常清楚线性模型的局限性(简单不幸地有一些缺点),我们也知道没有任何数据科学问题有固定的解决方案;然而,我们在该领域的经验告诉我们,线性模型以下优势不容忽视:
-
它很容易向自己、管理层或任何人解释它是如何工作的
-
它在处理你的数据问题时非常灵活,因为它可以处理数值和概率估计、排名以及多达多个类别的分类
-
无论你处理的数据量有多少,训练速度都很快
-
它在任何生产环境中快速且易于实现
-
它可以扩展到对用户实时响应
如果你,就像我们每天所做的那样,至关重要的是以快速和有形的方式从数据中创造价值,只需跟随我们,发现线性模型能帮助你达到多远。
本书涵盖的内容
第一章, 回归 – 数据科学的工作马, 介绍了为什么回归对于数据科学确实很有用,如何快速设置 Python 进行数据科学,并通过示例概述了本书中使用的所有包。在本章的结尾,我们将能够运行以下章节中包含的所有示例。你将清楚地了解回归分析不仅仅是从统计学中取出的一个被低估的技术,而是一个强大且有效的数据科学算法。
第二章, 简单线性回归的入门,首先通过描述一个回归问题,即如何拟合回归器,然后给出其算法数学公式的直觉,来介绍简单线性回归。然后,您将学习如何调整模型以获得更高的性能并深入理解其每个参数。最后,将描述梯度下降背后的引擎。
第三章, 实际中的多元回归,将简单线性回归扩展到从多个特征中提取预测信息并创建可以解决现实生活预测任务的模型。在前一章中解释的随机梯度下降技术将被提升以处理特征矩阵,并为了完成概述,您将了解多重共线性、交互作用和多项式回归主题。
第四章, 逻辑回归,继续奠定您对线性模型知识的基石。从必要的数学定义开始,它展示了如何进一步将线性回归扩展到分类问题,包括二元和多类。
第五章, 数据准备,讨论了为模型提供数据的内容,描述了如何以最佳方式准备数据以及如何处理不寻常的情况,特别是当数据缺失和存在异常值时。
第六章, 实现泛化,将向您介绍全面测试您模型的关键数据科学配方,调整其最佳状态,使其简洁,并在将其与真实新鲜数据对比之前,介绍更复杂的技术。
第七章, 在线和批量学习,阐述了在大数据上训练分类器的最佳实践;它首先关注批量学习及其局限性,然后介绍在线学习。最后,您将看到一个大数据的例子,结合了在线学习的优点和哈希技巧的力量。
第八章, 高级回归方法,介绍了回归的一些高级方法。我们不会深入到它们的数学公式,但始终关注实际应用,我们将讨论最小角回归、贝叶斯回归和具有铰链损失的随机梯度下降背后的思想,并简要提及袋装和提升技术。
第九章, 回归模型的实际应用,包含了四个通过线性模型解决的现实世界数据科学问题的实际示例。最终目标是展示如何处理这类问题以及如何围绕其解决方案进行推理,以便它们可以作为你将遇到的类似挑战的蓝图。
你需要为这本书准备的内容
本书提供的代码示例的执行需要安装 Python 3.4.3 或更新的版本,在 Mac OS X、Linux 或 Microsoft Windows 上。
本书展示的代码也将频繁使用 Python 的科学和统计计算基本库,如 SciPy、NumPy、Scikit-learn、Statsmodels,在一定程度上使用 matplotlib 和 pandas。
除了利用科学发行版(如 Continuum Analytics 的 Anaconda)来节省创建工作环境中的耗时操作外,我们还建议你采用 Jupyter 及其 IPython 笔记本,这是一种更高效且对科学友好的方式来在 Python 中编写你的线性模型。
第一章将为你提供逐步设置 Python 环境、这些核心库以及所有必要工具的说明和一些有用的提示。
本书面向的对象
首先,这本书是为那些至少对数据科学、统计学和数学有基本理解的 Python 开发者所写的。尽管这本书不需要你在数据科学或统计学方面有先前的背景知识,但它可以很好地满足任何资历的数据科学家,他们希望学习如何在数据集上最佳地进行回归分析。我们想象你希望将智能融入你的数据产品,但又不想依赖任何黑盒。因此,你更倾向于选择一种简单、易懂且有效的技术,以便将其用于生产级应用。通过这本书,我们将为你提供使用 Python 构建快速且更好的线性模型以及部署这些模型到 Python 或你喜欢的任何计算机语言中的知识。
术语约定
在这本书中,你会找到许多不同的文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 标签以以下方式显示:“检查线性模型时,首先检查coef_属性。”
代码块设置如下:
from sklearn import datasets
iris = datasets.load_iris()
由于我们将使用 IPython 笔记本来展示大部分示例,因此请预期在包含代码块的单元格中始终有一个输入(标记为In:)和经常有一个输出(标记为Out:)。在你的电脑上,你只需在In:之后输入代码,并检查结果是否与Out:内容相符:
In: clf.fit(X, y)
Out: SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0, degree=3, gamma=0.0, kernel='rbf', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False)
当需要在终端命令行中给出命令时,你会看到带有前缀$>的命令,否则,如果它是针对 Python 交互式解释器(REPL),它将前面带有>>>:
$>python
>>> import sys
>>> print sys.version_info
新术语和重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中会以这样的形式出现:“转到数据库部分,并使用 UTF 排序创建一个新的数据库。”
注意
警告或重要提示会以这样的框出现。
小贴士
小贴士和技巧会以这样的形式出现。
读者反馈
我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者的反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大价值的标题。
要向我们发送一般反馈,只需通过电子邮件发送到<feedback@packtpub.com>,并在邮件的主题中提及本书的标题。
如果你在一个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,网址为www.packtpub.com/authors。
客户支持
现在你已经是 Packt 书籍的骄傲所有者了,我们有一些事情可以帮助你从你的购买中获得最大价值。
下载示例代码
你可以从www.packtpub.com的账户下载本书的示例代码文件。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
你可以通过以下步骤下载代码文件:
-
使用你的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持标签上。
-
点击代码下载与勘误表。
-
在搜索框中输入书的名称。
-
选择你想要下载代码文件的书籍。
-
从下拉菜单中选择你购买这本书的地方。
-
点击代码下载。
一旦文件下载完成,请确保使用最新版本的以下软件解压缩或提取文件夹:
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/RegressionAnalysisWithPython_ColorImages.pdf下载此文件。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将在勘误部分显示。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过链接版权@packtpub.com 与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您对本书的任何方面有问题,您可以通过链接问题@packtpub.com 与我们联系,我们将尽力解决问题。
第一章。回归 – 数据科学的工作马
欢迎参加关于数据科学工作马——线性回归及其相关线性模型系列的介绍。
现在,互联互通和数据爆炸是现实,为能够实时读取和解释数据的企业打开了新的机会世界。一切都在促进数据和信息的产生和传播:无处不在的互联网在家中和工作中扩散,大量电子设备装在大量人口的口袋里,以及软件无处不在地产生关于每个过程和事件的数据。每天产生如此多的数据,人类由于数据量、速度和种类而无法处理。因此,机器学习和人工智能正在兴起。
线性回归及其衍生方法,源自统计学和计量经济学领域的漫长而辉煌的历史,可以为你提供一种简单、可靠和有效的工具,从数据中学习并采取行动。如果用正确的数据仔细训练,线性方法可以很好地与最复杂和最新的 AI 技术竞争,为你提供在日益增大的问题中难以匹敌的实现和可扩展性。
在本章中,我们将解释:
-
为什么线性模型可以作为数据科学管道中要评估的模型,或者作为立即开发可扩展的最小可行产品的捷径
-
安装 Python 和将其设置为数据科学任务的快速指南
-
在 Python 中实现线性模型所需的模块
回归分析和数据科学
想象一下,你是一名开发者,正在匆忙地开发一个非常酷的应用程序,这个应用程序将每天为数千名客户服务,使用你公司的网站。根据你在数据仓库中关于客户的信息,你的应用程序预计将迅速提供一个非常聪明且不太明显的答案。不幸的是,这个答案不能很容易地通过编程预定义,因此你需要采用一种典型的数据科学或预测分析的从数据中学习的方法。
在这个时代,这样的应用程序非常频繁地被发现在协助众多成功的网络企业中,例如:
-
在广告业务中,一个提供定向广告的应用程序
-
在电子商务中,一个批量应用程序筛选客户以提供更相关的商业报价,或者一个基于临时数据(如导航记录)的在线应用程序推荐购买的产品
-
在信贷或保险业务中,一个应用程序根据用户的信用评级和与公司的历史关系来决定是否进行在线查询
在机器学习应用于商业问题的用例不断增长的背景下,有无数的其它可能例子。所有这些应用的核心思想是,你不需要编程来定义你的应用程序应该如何行为,你只需通过提供有用的例子来设定一些期望的行为。应用程序会自行学习在任何情况下应该做什么。
在你清楚你应用程序的目的并决定使用基于数据的学习方法之后,你确信你不需要重新发明轮子。因此,你开始阅读关于数据科学和机器学习解决方案的教程和文档,这些解决方案应用于与你类似的问题(它们可能是关于数据科学、机器学习、统计学习和预测分析的论文、在线博客或书籍)。
在阅读了几页之后,你肯定会接触到许多复杂机器学习算法的奇迹,这些算法你之前可能从未听说过。
然而,你开始感到困惑。这并不仅仅是因为其背后的复杂数学;更多的是因为基于非常不同的技术的大量可能的解决方案。你也经常注意到,关于如何在生产环境中部署这些算法以及它们是否能够扩展到实时服务器请求的讨论完全缺失。
在这一点上,你完全不确定应该从哪里开始。这就是这本书将帮助你的时候。
让我们从一开始。
探索数据科学的承诺
在一个更加互联互通的世界和数据的日益可用性增长下,数据科学在近年来已经成为一个非常热门的话题。
在过去,分析解决方案有很强的约束:数据的可用性。有用的数据通常很少,并且总是昂贵且难以获取和存储。鉴于当前的数据爆炸,现在我们手头有大量且便宜的信息,这使得从数据中学习成为现实,从而打开了之前简单不切实际的广泛预测应用的大门。
此外,作为一个互联互通的世界的一部分,你的大多数客户现在都可以通过互联网或通过移动设备接触到(并且容易受到影响)。这仅仅意味着,在基于数据和其预测能力开发智能自动化解决方案方面做得聪明,可以直接并且几乎瞬间影响你的业务运作和表现。能够随时随地即时接触到你的客户,每天 24 小时,每年 365 天,如果你的公司知道该做什么,就能将数据转化为利润。在 21 世纪,正如不久前一篇令人难忘且尚未被反驳的文章在《连线》杂志上所说,“数据是数字经济的新石油”(www.wired.com/insights/2014/07/data-new-oil-digital-economy/)。然而,就像石油一样,数据必须被提取、精炼和分配。
处于实质性专业知识(知道如何经营业务和盈利)、机器学习(从数据中学习)和黑客技能(整合各种系统和数据源)的交汇点,数据科学承诺找到利用你可用数据的工具组合,并将其转化为利润。
然而,硬币的另一面也有其存在。
挑战
不幸的是,将数据科学应用于商业问题中存在一些挑战性的问题:
-
能够处理非结构化数据或为完全不同的目的建模的数据
-
确定如何从异构源中提取此类数据并在及时的方式下整合它
-
从数据中学习一些有效的通用规则,使你能够正确预测你的问题
-
理解所学习的内容,并能够有效地向非技术性的管理层传达你的解决方案
-
在大数据输入下进行实时预测的扩展
前两点主要涉及需要数据操作技能的问题,但从第三点开始,我们真正需要一种数据科学方法来解决问题。
基于机器学习的数据科学方法需要仔细测试不同的算法,评估它们在问题上的预测能力,并最终选择最佳的一个来实施。这正是“数据科学”中的科学意义:提出各种不同的假设并对其进行实验,以找到最适合问题并能推广结果的那个。
不幸的是,在数据科学中并没有什么“白乌托邦”;没有单一假设能够成功适应所有的问题。换句话说,我们说“没有免费的午餐”(这是来自优化领域的一个著名定理的名称),这意味着在数据科学中没有算法或程序能够始终保证你获得最佳结果;每个算法的成功程度可能有所不同,这取决于问题。
数据以各种形状和形式出现,反映了我们所处世界的复杂性。现有的算法应该具有一定的复杂性以处理世界的复杂性,但不要忘记,它们只是模型。模型不过是对我们想要成功表示和复制的规则和法律系统的简化和近似,因为正如开尔文勋爵所说,“你只能控制你能测量的东西”。近似应该根据其有效性来评估,并且应用于实际问题的学习算法的有效性由许多因素(问题类型、数据质量、数据数量等)决定,以至于你真的无法事先知道什么会起作用,什么不会。在这样的前提下,你总是想先测试简单的解决方案,并尽可能遵循奥卡姆剃刀原则,在性能相当的情况下,倾向于更简单的模型而不是更复杂的模型。
有时,即使情况允许引入更复杂、性能更好的模型,其他因素仍可能倾向于采用更简单但性能较差的解决方案。事实上,最佳模型并不总是性能最佳的模型。根据问题和应用的上下文,诸如在生产线中实施简便性、可扩展到增长的数据量以及在实际环境中的性能等问题,可能会深刻地重新定义预测性能在选择最佳解决方案中的重要性。
在这种情况下,如果它们能提供对问题的可接受解决方案,仍然建议使用更简单、调优良好的模型或易于解释的模型。
线性模型
在你最初对使用哪种机器学习算法的问题进行概述时,你可能也偶然发现了线性模型,即线性回归和逻辑回归。它们都被作为基本工具、更复杂知识体系的基础块来介绍,在你希望获得最佳结果之前,你应该掌握这些知识。
线性模型已经被学者和实践者所熟知和研究很长时间。在迅速被纳入数据科学之前,线性模型一直是预测分析和数据挖掘中首先考虑的基本统计模型。它们也是统计学、经济学以及许多其他定量学科知识体系中的突出和相关的工具。
通过简单的检查(通过在线书店的查询、图书馆或直接通过谷歌图书——books.google.com/),你会发现关于线性回归的出版物相当丰富。关于逻辑回归的出版物也相当丰富,以及关于其他不同变种的回归算法,所谓的广义线性模型,它们的公式被调整以面对和解决更复杂的问题。
作为实践者,我们深知线性模型的局限性。然而,我们不能忽视它们的强大正面关键点:简单和高效。我们也不能忽视线性模型确实是应用数据科学中最常用的学习算法之一,使它们成为数据分析(在商业以及许多科学领域)中的真正工作马。
它们远非手头最好的工具,但它们始终是数据科学发现之路上的一个良好起点,因为它们不需要对太多参数进行黑客攻击,而且训练速度非常快。因此,线性模型可以指出你手头数据的预测能力,识别最重要的变量,并允许你在应用更复杂的算法之前快速测试数据的有用转换。
在本书的过程中,你将学习如何基于线性回归模型构建原型,同时保持你的数据处理和操作流程对初始线性模型可能的开发迭代到更强大和复杂的模型(如神经网络或支持向量机)的适应性。
此外,你还将了解到有时你可能甚至不需要更复杂的模型。如果你真的在处理大量数据,在将一定量的输入数据输入到模型之后,使用简单或复杂的算法并不会那么重要了。它们都会发挥出它们最佳的能力。
大数据使简单的模型也能像复杂模型一样有效的能力,已经被 Alon Halevy、Peter Norvig 和 Fernando Pereira 共同撰写的一篇著名论文指出,这篇论文是关于数据的不可思议有效性的(static.googleusercontent.com/media/research.google.com/it//pubs/archive/35179.pdf)。在此之前,这个想法已经因为微软研究人员 Michele Banko 和 Eric Brill 的一篇不太知名的论文而为人所知,这篇论文是关于针对自然语言消歧的非常非常大的语料库的扩展(ucrel.lancs.ac.uk/acl/P/P01/P01-1005.pdf)。
简而言之,拥有更多数据的算法通常比其他算法(无论它们的复杂性如何)更胜一筹;在这种情况下,它可能就是一个线性模型。
然而,线性模型在数据科学流程的下游也可以很有帮助,而不仅仅是上游。因为它们训练速度快,部署也快,你不需要编写复杂的算法就能做到这一点,允许你使用任何你喜欢的脚本或编程语言来编写解决方案,从 SQL 到 JavaScript,从 Python 到 C/C++。
由于它们的实现简单,在构建使用神经网络或集成方法复杂解决方案之后,这些解决方案被逆向工程以找到一种方法,使它们可以作为线性模型在生产环境中可用,并实现更简单和可扩展的实现,这并不罕见。
你将在书中找到的内容
在接下来的几页中,本书将解释算法以及它们在 Python 中的实现,以解决实际的现实世界问题。
线性模型可以归类为监督算法,这些算法如果事先给出一些正确的例子来学习,就可以对数字和类别进行预测。得益于一系列示例,你将立即区分一个问题是否可以使用此算法解决。
由于线性模型家族的统计起源,我们不能忽视从统计角度开始。在说明线性模型的使用背景后,我们将提供理解算法基于何种统计基础以及为何创建算法的目的的所有基本要素。我们将使用 Python 评估线性模型的统计输出,提供有关所使用的不同统计测试的信息。
数据科学方法非常实用(为了解决业务影响的问题),线性模型的统计版本的实际许多限制实际上并不适用。然而,了解 R 平方系数的工作原理或能够评估回归的残差,或突出其预测变量的共线性,可以为你提供更多从回归建模工作中获得良好结果的方法。
从涉及单个预测变量的回归模型开始,我们将继续考虑多个变量,并且从仅预测数字过渡到估计在两个或多个类别中存在某个类别的概率。
我们将特别强调如何准备数据,包括要预测的目标变量(一个数字或一个类别)和预测变量;有助于正确预测的变量。无论你的数据由什么组成,数字、名词、文本、图像或声音,我们都会为你提供正确准备数据并将其转换的方法,以便你的模型表现最佳。
你还将了解到数据科学最基础的科学研究方法,这将帮助你理解为什么数据科学方法不仅仅是理论上的,而且非常实用,因为它允许获得在实际世界问题中真正可以工作的模型。
书的最后几页将涵盖处理大数据和模型复杂性的更高级技术。我们还将提供一些来自相关商业领域的示例,并详细说明如何构建线性模型、验证它,以及随后将其实现到生产环境中。
数据科学中的 Python
考虑到有许多用于创建线性模型的实用软件包,以及它是开发者中相当流行的编程语言这一事实,Python 是我们在这本书中展示所有代码的首选语言。
Python 于 1991 年作为一种通用、解释型、面向对象的语言创建,它逐渐稳步征服了科学界,并成长为一个成熟的数据处理和分析专用软件包生态系统。它允许你进行无数快速实验,轻松的理论发展,以及科学应用的快速部署。
作为一名开发者,你会发现使用 Python 有趣的原因有很多:
-
它提供了一套庞大、成熟的用于数据分析和机器学习的软件包系统。它保证你在数据分析过程中将获得所有你需要的东西,有时甚至更多。
-
它非常灵活。无论你的编程背景或风格如何(面向对象或过程式),你都会享受用 Python 编程。
-
如果你还不了解它,但熟悉其他语言,如 C/C++或 Java,学习和使用它非常简单。掌握基础知识后,没有比立即开始编码更好的学习方法了。
-
它是跨平台的;你的解决方案将在 Windows、Linux 和 Mac OS 系统上完美且平稳地运行。你不必担心可移植性问题。
-
虽然是解释型语言,但与其他主流数据分析语言(如 R 和 MATLAB)相比,它无疑非常快(尽管它不能与 C、Java 以及新兴的 Julia 语言相提并论)。
-
有一些软件包允许你调用其他平台,如 R 和 Julia,将一些计算外包给他们,从而提高你的脚本性能。此外,还有一些静态编译器,如 Cython,或即时编译器,如 PyPy,可以将 Python 代码转换为 C 代码,以实现更高的性能。
-
由于其最小的内存占用和卓越的内存管理,它可以在内存数据上比其他平台表现得更好。当使用各种迭代和重复的数据处理方法来加载、转换、切块、切片、保存或丢弃数据时,内存垃圾收集器通常会拯救你的日子。
安装 Python
作为第一步,我们将创建一个完全工作的数据科学环境,你可以用它来复制和测试书中的示例,并原型化你自己的模型。
无论你打算用哪种语言开发应用程序,Python 都将提供一种简单的方法来访问你的数据,从它构建模型,并提取你在生产环境中进行预测所需的正确参数。
Python 是一种开源的、面向对象的、跨平台的编程语言,与它的直接竞争对手(例如,C/C++ 和 Java)相比,生成的代码非常简洁且易于阅读。它允许你在很短的时间内构建一个可工作的软件原型,轻松维护它,并将其扩展到更大的数据量。由于它是一种通用语言,并且有大量可用的包,可以轻松快速地帮助你解决各种常见和特殊问题,Python 已经成为数据科学家工具箱中最常用的语言。
在 Python 2 和 Python 3 之间进行选择
在开始之前,重要的是要知道 Python 有两个主要分支:版本 2 和 3。由于许多核心功能已经改变,为其中一个版本编写的脚本通常与另一个版本不兼容(它们不会工作,并且会引发错误和警告)。尽管第三个版本是最新的,但旧版本仍然是科学领域使用最广泛的版本,也是许多操作系统的默认版本(主要是为了兼容升级)。当 2008 年发布第三个版本时,大多数科学包还没有准备好,因此科学界陷入了之前的版本。幸运的是,从那时起,几乎所有包都已更新,只剩下少数 Python 3 兼容的遗留问题(有关兼容性概述,请参阅 py3readiness.org/)。
在这本书中,它应该面向广泛的开发者群体,我们一致认为使用 Python 3 而不是旧版本会更好。Python 3 是 Python 的未来;实际上,它是 Python 基金会唯一将继续开发和改进的版本。它将成为未来的默认版本。如果你目前正在使用版本 2 并且希望继续使用它,我们建议你每次启动解释器时都运行以下几行代码。这样做会使 Python 2 能够以最小或没有问题的方式执行大多数版本 3 的代码(代码将修补一些基本的不兼容性,在通过命令 pip install future 安装 future 包之后,并让你安全地运行本书中的所有代码):
from __future__ import unicode_literals
# to make all string literals into unicode strings
from __future__ import print_function # To print multiple strings
from six import reraise as raise_ # Raising exceptions with a traceback
from __future__ import division # True division
from __future__ import absolute_import # Flexible Imports
小贴士
from __future__ import 命令应始终出现在你的脚本开头,否则你可能会遇到 Python 报错。
步骤安装
如果你从未使用过 Python(但这并不意味着你可能没有在机器上安装它),你首先需要从项目的官方网站下载安装程序,www.python.org/downloads/(记住,我们使用的是版本 3),然后在你的本地机器上安装它。
本节为您提供了对可以在您的机器上安装的内容的完全控制。当你打算将 Python 作为你的原型设计和生产语言时,这非常有用。此外,它可以帮助你跟踪你使用的包的版本。无论如何,请务必注意,逐步安装确实需要时间和精力。相反,安装现成的科学发行版可以减轻安装程序的负担,并且可能有助于初始学习,因为它可以为你节省相当多的时间,尽管它会在你的计算机上一次性安装大量(大部分你可能永远不会使用)的包。因此,如果你想立即开始,并且不需要控制你的安装,请跳过这部分,继续下一节关于科学发行版的内容。
由于 Python 是一种多平台编程语言,你将找到适用于运行 Windows 或 Linux/Unix 类操作系统的计算机的安装程序。请记住,一些 Linux 发行版(如 Ubuntu)已经将 Python 打包在仓库中,这使得安装过程变得更加简单:
-
打开 Python 命令行,在终端中输入
python或者在 Python IDLE 图标上点击。然后,为了测试安装,请在 Python 交互式外壳或 REPL 中运行以下代码:>>> import sys >>> print (sys.version)小贴士
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。
www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。![逐步安装]()
如果抛出语法错误,这意味着你正在运行 Python 2 而不是 Python 3。否则,如果你没有遇到错误,并且看到你的 Python 版本是 3.x(在撰写本书时,最新版本是 3.5.0),那么恭喜你,你正在运行我们为本书选择的 Python 版本。
为了澄清,当在终端命令行中给出命令时,我们在命令前加上 $>。否则,如果是针对 Python REPL,则前面加上 >>>。
安装包
根据你的系统和过去的安装,Python 可能不会捆绑所有你需要的东西,除非你已经安装了一个发行版(另一方面,通常包含你需要的远不止这么多)。
要安装你需要的任何包,你可以使用 pip 或 easy_install 命令;然而,easy_install 将会在未来被弃用,而 pip 相对于它有许多重要优势。使用 pip 安装一切是首选的,因为:
-
它是 Python 3 的首选包管理器,并且从 Python 2.7.9 和 Python 3.4 开始,它默认包含在 Python 二进制安装程序中
-
它提供了卸载功能
-
如果由于任何原因包安装失败,它会回滚并使你的系统保持清晰。
命令 pip 在命令行上运行,使得安装、升级和删除 Python 包的过程变得非常简单。
正如我们提到的,如果你运行的是至少 Python 2.7.9 或 Python 3.4,pip 命令应该已经存在。为了验证你的本地机器上安装了哪些工具,如果有错误发生,直接使用以下命令进行测试:
$> pip -V
在某些 Linux 和 Mac 安装中,命令以 pip3 的形式存在(如果你机器上同时安装了 Python 2 和 3,可能性更大),所以,如果你在查找 pip 时收到错误,也尝试运行以下命令:
$> pip3 -V
或者,你也可以测试一下旧命令 easy_install 是否可用:
$> easy_install --version
小贴士
尽管 pip 有优势,但在 Windows 上使用 easy_install 也是有意义的,因为 pip 不会安装二进制包(它会尝试构建它们);因此,如果你在安装包时遇到意外的困难,easy_install 可以帮你解决问题。
如果你的测试以错误结束,你真的需要从头开始安装 pip(在这个过程中,同时也会安装 easy_install)。
要安装 pip,只需遵循 pip.pypa.io/en/stable/installing/ 提供的说明。最安全的方法是从 bootstrap.pypa.io/get-pip.py 下载 get-pip.py 脚本,然后使用以下命令运行它:
$> python get-pip.py
顺便说一句,该脚本还将安装来自 pypi.python.org/pypi/setuptools 的设置工具,其中包含 easy_install。
作为替代,如果你正在运行 Debian/Ubuntu 类 Unix 系统,那么使用 apt-get 安装一切将是一个快速的捷径:
$> sudo apt-get install python3-pip
在检查了这个基本要求之后,你现在就可以安装运行本书提供的示例所需的全部包了。要安装一个通用包 <pk>,你只需要运行以下命令:
$> pip install <pk>
或者,如果你更喜欢使用 easy_install,你也可以运行以下命令:
$> easy_install <pk>
之后,<pk> 包及其所有依赖项将被下载并安装。
如果你不确定一个库是否已安装,只需尝试在其中导入一个模块。如果 Python 解释器抛出 ImportError 错误,可以得出结论,该包尚未安装。
让我们举一个例子。这是当 NumPy 库已经安装时发生的情况:
>>> import numpy
如果没有安装,会发生以下情况:
>>> import numpy
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named numpy
在后一种情况下,在导入之前,你需要通过 pip 或 easy_install 安装它。
请注意,不要将包与模块混淆。使用 pip,你安装一个包;在 Python 中,你导入一个模块。有时,包和模块有相同的名称,但在许多情况下它们并不匹配。例如,sklearn 模块包含在名为 Scikit-learn 的包中。
软件包升级
更常见的情况是,您会发现自己必须升级一个软件包,因为新版本要么是依赖项所必需的,要么具有您希望使用的附加功能。为此,首先通过查看以下示例中的__version__属性来检查您已安装的库版本,例如使用 NumPy 软件包:
>>> import numpy
>>> numpy.__version__ # 2 underscores before and after
'1.9.2'
现在,如果您想将其更新到较新版本,比如 1.10.1 版本,您可以从命令行运行以下命令:
$> pip install -U numpy==1.10.1
或者,尽管我们不推荐这样做,除非证明是必要的,您也可以使用以下命令:
$> easy_install --upgrade numpy==1.10.1
最后,如果您只是想将其升级到最新可用版本,只需运行以下命令:
$> pip install -U numpy
您还可以运行easy_install的替代命令:
$> easy_install --upgrade numpy
科学发行版
如您至今所读,对于数据科学家来说,创建一个工作环境是一项耗时的工作。您首先需要安装 Python,然后,一个接一个地安装您需要的所有库(有时,安装过程可能不会像您之前所期望的那样顺利)。
如果您想节省时间和精力,并确保您有一个可用的 Python 环境,您可以简单地下载、安装并使用科学 Python 发行版。除了 Python 本身之外,发行版还包括各种预安装的软件包,有时它们甚至为您的使用设置了额外的工具和 IDE。其中一些在数据科学家中非常知名,在接下来的章节中,您将找到我们认为最有用和实用的两个软件包的一些关键特性。
为了立即专注于本书的内容,我们建议您首先下载并安装一个科学发行版,例如 Anaconda(据我们看来,这是最完整的一个)。然后,在练习本书中的示例之后,我们建议您决定完全卸载发行版,单独设置 Python,这可以仅伴随您项目所需的软件包。
再次提醒,如果可能的话,下载并安装包含 Python 3 的版本。
我们首先推荐你尝试的第一个包是 Anaconda (www.continuum.io/downloads),这是 Continuum Analytics 提供的一个 Python 发行版,包括近 200 个包,包括 NumPy、SciPy、Pandas、IPython、Matplotlib、Scikit-learn 和 Statsmodels。它是一个跨平台发行版,可以安装在具有其他现有 Python 发行版和版本的机器上,其基础版本是免费的。包含高级功能的附加组件需要单独收费。Anaconda 引入了 conda,一个二进制包管理器,作为命令行工具来管理你的包安装。正如其网站所述,Anaconda 的目标是提供适用于大规模处理、预测分析和科学计算的现成 Python 发行版。
作为第二个建议,如果你在 Windows 上工作,WinPython (winpython.sourceforge.net) 可能是一个相当有趣的替代方案(抱歉,没有 Linux 或 MacOS 版本)。WinPython 也是一个由社区维护的免费、开源 Python 发行版。它是为科学家设计的,包括许多基本包,如 NumPy、SciPy、Matplotlib 和 IPython(与 Anaconda 相同)。它还包括 Spyder 作为 IDE,如果你有使用 MATLAB 语言和界面的经验,这可能很有帮助。一个关键优势是它具有便携性(你可以将其放入任何目录,甚至 U 盘中,无需任何管理权限提升)。使用 WinPython,你可以在计算机上安装不同的版本,将一个版本从 Windows 计算机移动到另一个,只需替换其目录即可轻松替换旧版本。当你运行 WinPython 或其 shell 时,它将自动设置所有必要的环境变量,以便像常规安装和注册在系统上一样运行 Python。
最后,对于在 Windows 上工作的用户,另一个不错的选择可能是 Python(x,y)。Python(x,y) (python-xy.github.io) 是由科学社区维护的一个免费、开源的 Python 发行版。它包含了许多包,例如 NumPy、SciPy、NetworkX、IPython 和 Scikit-learn。它还提供了 Spyder,这是一个受 MATLAB IDE 启发的交互式开发环境。
介绍 Jupyter 或 IPython
IPython 是由 Fernando Perez 在 2001 年启动的一个免费项目。它解决了 Python 在科学调查中的不足。作者认为 Python 缺乏一个用户编程接口,这个接口能够将科学方法(主要指实验和交互式发现)融入软件开发过程中。
科学方法意味着以可重复的方式快速进行不同假设的实验(数据科学中的数据探索和分析任务也是如此),当使用 IPython 时,你将能够在代码编写中更自然地实施探索性、迭代性、试错性研究策略。
最近,IPython 项目的大部分内容已迁移到一个名为 Jupyter 的新项目中(jupyter.org):

这个新项目扩展了原始 IPython 接口在以下广泛编程语言中的潜在可用性:
-
Julia (
github.com/JuliaLang/IJulia.jl) -
Scala (
github.com/mattpap/IScala)
要获取可用内核的完整列表,请访问:github.com/ipython/ipython/wiki/IPython-kernels-for-other-languages。
无论你使用什么语言进行开发,都可以使用相同的类似 IPython 的界面和交互式编程风格,这得益于强大的内核概念,内核是运行用户代码的程序,由前端界面进行通信;然后它们将执行代码的结果反馈给界面本身。
IPython(Python 是零内核,原始起点)可以简单地描述为一个可以通过控制台或基于网络的笔记本操作的交互式任务工具,它提供了特殊的命令,帮助开发者更好地理解和构建当前正在编写的代码。
与围绕编写脚本、运行它并最终评估其结果的脚本界面相反,IPython 允许你分块编写代码,依次运行每个块,并单独评估每个块的结果,检查文本和图形输出。除了图形集成外,它还通过可定制的命令、丰富的历史记录(以 JSON 格式)和计算并行性提供进一步的帮助,以增强处理重数计算时的性能。
在 IPython 中,你可以轻松地结合代码、注释、公式、图表和交互式绘图,以及丰富的媒体,如图片和视频,使其成为所有实验及其结果的完整科学草图板。此外,IPython 允许可重复研究,允许任何数据分析建模在不同的环境下轻松重建:

IPython 在你喜欢的浏览器(例如,可能是 Internet Explorer、Firefox 或 Chrome)上运行,启动时显示一个等待编写代码的单元。每个被单元包围的代码块都可以运行,其结果在单元后的空间中报告。图表可以在笔记本中(内联图表)或单独的窗口中表示。在我们的例子中,我们决定内联绘制我们的图表。
使用 Markdown 语言可以轻松地编写笔记,Markdown 是一种非常简单易用的标记语言 (daringfireball.net/projects/markdown)。
这种方法对于涉及基于数据开发代码的任务尤其有益,因为它自动完成了通常被忽视的记录和说明数据分析如何进行、其前提、假设以及中间/最终结果的职责。如果你的工作部分也包括展示你的工作并吸引内部或外部利益相关者参与项目,IPython 可以通过很少的额外努力为你施展讲述故事的魔法。在网页 github.com/ipython/ipython/wiki/A-gallery-of-interesting-IPython-Notebooks 上,有许多例子,其中一些可能对你工作的启发就像对我们一样。
实际上,我们必须承认,保持一个干净、最新的 IPython Notebook 在经理/利益相关者会议突然出现,需要我们匆忙展示工作状态时,帮了我们无数的忙。
作为额外的资源,IPython 为你提供了一个完整的魔法命令库,允许你执行一些有用的操作,例如测量命令执行所需的时间,或创建包含单元输出的文本文件。我们根据它们是否操作单行代码或整个单元中的代码来区分行魔法和单元魔法。例如,魔法命令 %timeit 测量在行魔法同一行上执行命令所需的时间,而 %%time 是一个单元魔法,它测量整个单元的执行时间。
如果你想了解更多关于魔法命令的信息,只需在 IPython 单元中输入 %quickref 并运行它:将出现一个完整的指南,展示所有可用的命令。
简而言之,IPython 让你可以:
-
查看分析每个步骤的中间(调试)结果
-
只运行代码的部分(或单元)
-
将中间结果以 JSON 格式存储,并具有版本控制它们的能力
-
展示你的工作(这将是一个文本、代码和图像的组合),通过 IPython Notebook Viewer 服务 (
nbviewer.ipython.org/) 分享,并轻松将其导出为 HTML、PDF 或甚至幻灯片
在本书中,IPython 是我们的首选工具,用于清晰有效地展示脚本和数据操作及其后续结果。
注意
对于关于 IPython 全部功能的完整论述,请参阅两本 Packt Publishing 出版的书籍:《IPython 交互式计算与可视化烹饪书》(作者:Cyrille Rossant,Packt Publishing,2014 年 9 月 25 日出版)和《学习 IPython 进行交互式计算与数据可视化》(作者:Cyrille Rossant,Packt Publishing,2013 年 4 月 25 日出版)。
为了说明目的,只需考虑每个 IPython 指令块都有一个编号的输入语句和一个输出语句,因此你将发现本书中的代码至少以两个块的形式呈现,当输出不是非常简单时;否则,只需期待只有输入部分:
In: <the code you have to enter>
Out: <the output you should get>
请注意,我们不对输入或输出进行编号。
虽然我们强烈推荐使用 IPython,但如果您使用 REPL 方法或 IDE 界面,您可以使用相同的指令并期望得到相同的结果(但输出格式和返回结果的扩展可能不同)。
用于线性模型的 Python 包和函数
线性模型在众多科学和商业应用中广泛使用,并且可以在许多不同的 Python 包中找到,以不同的函数形式存在。我们在本书中选择了几个用于展示。其中,Statsmodels 是我们选择用于展示模型统计特性的工具,而 Scikit-learn 则是我们推荐的用于轻松无缝准备数据、构建模型和部署模型的包。我们将专门使用 Statsmodels 构建的模型来展示线性模型的统计特性,并使用 Scikit-learn 来展示如何从数据科学的角度进行建模。
NumPy
NumPy,由 Travis Oliphant 创建,是 Python 语言中每个分析解决方案的核心。它为用户提供多维数组,以及一套用于在这些数组上执行多个数学运算的函数。数组是沿着多个维度排列的数据块,并实现了数学向量矩阵。数组不仅用于存储数据,而且对于快速矩阵运算(向量化)也很有用,这在解决特定数据科学问题时是必不可少的。
在本书中,我们将主要使用 NumPy 的linalg模块;作为一个线性代数函数集合,它将有助于解释算法的细节:
-
导入约定:
import numpy as np -
印刷时的版本:
1.9.2 -
建议安装命令:
pip install numpy
小贴士
作为 Python 社区广泛采用的约定,当导入 NumPy 时,建议将其别名为np:
import numpy as np
在本书中展示的代码中,我们还将使用其他 Python 特性的导入约定。
SciPy
由 Travis Oliphant、Pearu Peterson 和 Eric Jones 发起的一个原始项目,SciPy 补充了 NumPy 的功能,提供了更多科学算法,包括线性代数、稀疏矩阵、信号和图像处理、优化、快速傅里叶变换等。
scipy.optimize 包提供了几个常用的优化算法,用于详细说明如何使用不同的优化方法来估计线性模型:
-
导入约定:
import scipy as sp -
印刷时的版本:
0.16.0 -
建议安装命令:
pip install scipy
Statsmodels
之前是 Scikit 的一部分,Statsmodels 被认为是对 SciPy 统计函数的补充。它包括广义线性模型、离散选择模型、时间序列分析以及一系列描述性统计、参数和非参数检验。
在 Statsmodels 中,我们将使用statsmodels.api和statsmodels.formula.api模块,它们通过提供输入矩阵和公式的规范来提供拟合线性模型的函数:
-
导入约定:
import statsmodels.api as sm和import statsmodels.formula.api as smf -
印刷时的版本:
0.6.1 -
建议安装命令:
pip install statsmodels
Scikit-learn
Scikit-learn 最初是作为SciPy 工具包(SciKits)的一部分开始的,它是 Python 上数据科学操作的核心。它提供了您可能需要的所有数据预处理、监督学习和无监督学习、模型选择、验证和错误度量。期待我们在本书中详细讨论这个包。
Scikit-learn 始于 2007 年,是 David Cournapeau 的 Google Summer of Code 项目。自 2013 年以来,它已被法国国家研究院(INRA)的研究人员接管。
Scikit-learn 提供了数据预处理模块(sklearn.preprocessing、sklearn.feature_extraction)、模型选择和验证模块(sklearn.cross_validation、sklearn.grid_search和sklearn.metrics)以及一套完整的方法(sklearn.linear_model),其中目标值,无论是数字还是概率,都期望是输入变量的线性组合:
-
导入约定:无;模块通常单独导入
-
印刷时的版本:
0.16.1 -
建议安装命令:
pip install scikit-learn
小贴士
注意导入的模块被命名为 sklearn。
摘要
在本章中,我们从数据科学的角度简要探讨了线性模型的有用性,并介绍了一些数据科学方法的基本概念,这些概念将在后续章节中详细解释,并将应用于线性模型。我们还提供了如何设置 Python 环境的详细说明;这些说明将在整本书中用于展示示例,并提供用于快速开发机器学习假设的有用代码片段。
在下一章中,我们将从统计基础开始介绍线性回归。从相关性的概念出发,我们将构建简单的线性回归(仅使用一个预测因子)并提供算法的公式。
第二章:简单线性回归的接近方法
在设置好所有工作工具(直接安装 Python 和 IPython 或使用科学发行版)之后,你现在准备好开始使用线性模型将新能力融入你计划构建的软件中,特别是预测能力。到目前为止,你开发的软件解决方案是基于你定义的某些规范(或其他人交给你的规范)。你的方法始终是通过编写代码,仔细地将每个情况映射到特定的、预先确定的响应,来调整程序的响应以适应特定的输入。回顾一下,通过这样做,你只是结合了从经验中学到的实践。
然而,世界是复杂的,有时你的经验并不足以让你的软件足够智能,在竞争激烈的企业或具有许多不同和可变面的挑战性问题中产生差异。
在本章中,我们将开始探索一种不同于手动编程的方法。我们将提出一种使软件能够自我学习特定输入的正确答案的方法,前提是你能够用数据和目标响应来定义问题,并且你可以在过程中结合一些你的领域专业知识——例如,选择正确的预测特征。因此,当涉及到创建你的软件时,你的经验将继续至关重要,尽管是以从数据中学习的形式。实际上,你的软件将根据你的规格从数据中学习。我们还将说明如何通过求助于从数据中推导知识的最简单方法之一:线性模型来实现这一点。
具体来说,在本章中,我们将讨论以下主题:
-
理解机器学习可以解决的问题
-
回归模型可以解决哪些问题
-
相关性的优缺点
-
如何将相关性扩展到简单的回归模型
-
回归模型的何时、何事和为何
-
梯度下降背后的基本数学
在这个过程中,我们将使用一些统计术语和概念,以便在统计学的更大框架中为您提供线性回归的前景,尽管我们的方法将保持实用,为您提供使用 Python 开始构建线性模型所需的工具和提示,从而丰富您的软件开发。
定义回归问题
多亏了机器学习算法,从数据中推导知识成为可能。机器学习有着坚实的根基,源于多年的研究:它实际上是从五十年代末期以来的一段漫长旅程,当时亚瑟·塞缪尔将机器学习明确界定为“一个研究领域,它赋予计算机在没有明确编程的情况下学习的能力。”
数据爆炸(以前未记录的大量数据的可用性)使得最近和经典的机器学习技术得到广泛应用,并使它们成为高性能技术。如今,如果你可以通过语音与你的手机交谈并期望它能够正确地回应你,充当你的秘书(如 Siri 或 Google Now),这完全是由于机器学习。同样,对于基于机器学习的每个应用也是如此,如人脸识别、搜索引擎、垃圾邮件过滤器、书籍/音乐/电影的推荐系统、手写识别和自动语言翻译。
机器学习算法的一些其他实际用途可能不那么明显,但同样重要且有利可图,如信用评级和欺诈检测、算法交易、网络上的广告定位和健康诊断。
一般而言,机器学习算法可以通过三种方式学习:
-
监督学习:这是我们提供标记示例以从中学习的情况。例如,当我们想要在房地产市场中提前预测房屋的售价时,我们可以获取房屋的历史价格,并让监督学习算法成功地找出如何将价格与房屋特征相关联。
-
无监督学习:这是我们提供没有任何提示的示例,将其留给算法来创建标签的情况。例如,当我们需要根据客户数据库中各组的特征和行为将它们划分为相似段时。
-
强化学习:这是当我们提供没有标签的示例时,就像无监督学习一样,但会从环境中获得反馈,以确定标签猜测是否正确。例如,当我们需要软件在竞争环境中成功行动时,如电子游戏或股市,我们可以使用强化学习。在这种情况下,软件将开始在环境中行动,并直接从其错误中学习,直到找到确保其成功的一组规则。
线性模型和监督学习
无监督学习在机器人视觉和自动特征创建中有着重要的应用,而强化学习对于开发自主人工智能(例如,在机器人技术中,但也在创建智能软件代理)至关重要;然而,监督学习在数据科学中最为重要,因为它使我们能够实现人类种族长期以来一直渴望实现的目标:预测。
预测在商业和一般用途中都有应用,它使我们能够采取最佳行动,因为我们从预测中得知了某种情况的可能结果。预测可以使我们在决策和行动中取得成功,并且自古以来就与魔法或大智慧联系在一起。
监督学习根本不是魔法,尽管它可能对某些人来说看起来像是魔法,正如亚瑟·查尔斯·克拉克爵士所说,“任何足够先进的技术都与魔法无法区分。”基于人类在数学和统计学方面的成就,监督学习有助于利用人类经验和观察,并以一种任何人类心智都无法实现的方式将它们转化为精确的预测。然而,监督学习只能在某些有利条件下进行预测。拥有过去的事例至关重要,我们可以从中提取规则和线索,以支持在给定某些前提的情况下做出高度可能的预测。
无论机器学习算法的确切公式如何,其想法是你可以预测结果,因为观察到的过去中存在某些前提,导致了特定的结论。
在数学形式化中,我们称我们想要预测的结果为响应变量或目标变量,我们通常使用小写字母 y 来标记它。
前提被称为 预测 变量,或者简单地称为属性或特征,如果只有一个,则用小写 x 标记,如果有多个,则用大写 X 标记。使用大写字母 X,我们的意图是使用矩阵符号,因为我们也可以将 y 视为一个响应向量(技术上是一个列向量),将 X 视为一个包含所有特征向量值的矩阵,每个特征向量都排列在矩阵的单独一列中。
同时,始终注意 X 和 y 的维度也很重要;因此,按照惯例,我们可以将 n 称为观测值的数量,将 p 称为变量的数量。因此,我们的 X 将是一个大小为 (n, p) 的矩阵,而我们的 y 将始终是一个大小为 n 的向量。
小贴士
在整本书中,我们还将使用统计符号,这实际上要明确和冗长得多。一个统计公式试图给出公式中所有预测变量的概念(我们将在稍后展示一个例子),而矩阵符号则更为隐晦。
我们可以肯定,当我们以监督方式从数据中学习预测时,我们实际上是在构建一个可以回答关于 X 如何暗示 y 的函数的问题。
使用这些新的矩阵符号表示,我们可以定义一个函数,一个将 X 值转换为 y 的函数映射,可以无误差或在一个可接受的误差范围内进行转换。我们可以肯定,我们所有的努力都是为了确定以下类型的函数:

当函数被指定,并且我们心中有一个带有特定参数的算法以及由某些数据组成的 X 矩阵时,我们通常可以将其称为一个假设。这个术语是合适的,因为我们可以将我们的函数视为一个准备好的假设,设置好所有参数,以便测试它在预测目标 y 时工作得是否好坏。
在讨论函数(执行所有魔法的监督算法)之前,我们首先应该花些时间思考一下是什么在喂养这个算法本身。我们已经介绍了矩阵 X,预测变量,以及向量 y,目标答案变量;现在,我们需要解释如何从我们的数据中提取它们,以及它们在学习算法中的确切作用。
反思预测变量
在监督算法中反思你的预测变量的作用,在整个书籍的说明中,你必须牢记一些注意事项,是的,它们非常重要且具有决定性。
为了存储预测变量,我们使用一个矩阵,通常称为 X 矩阵:

在这个例子中,我们的 X 只包含一个变量,它包含 n 个案例(或观察)。
小贴士
如果你想知道何时使用一个变量或特征,只需考虑在机器学习中,特征 和 属性 是比 变量 更受青睐的术语,变量 具有明确的统计味道,暗示着某种变化。根据上下文和受众,你可以有效地使用其中一个。
在 Python 代码中,你可以通过输入以下代码来构建一个一列的矩阵结构:
In: import numpy as np
vector = np.array([1,2,3,4,5])
row_vector = vector.reshape((5,1))
column_vector = vector.reshape((1,5))
single_feature_matrix = vector.reshape((1,5))
使用 NumPy 的 array,我们可以快速推导出一个向量和矩阵。如果你从一个 Python 列表开始,你会得到一个向量(实际上既不是行向量也不是列向量)。通过使用 reshape 方法,你可以根据你的要求将其转换成一个行向量或列向量。
现实世界的数据通常需要更复杂的矩阵,现实世界的矩阵包含无数不同的数据列(大数据的多样性元素)。很可能会出现,标准的 X 矩阵会有更多的列,因此我们将引用的符号是:

现在,我们的矩阵有更多的变量,所有 p 个变量,因此其大小是 n x p。在 Python 中,有两种方法可以构建这样的数据矩阵:
In: multiple_feature_matrix = np.array([[1,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15]])
你只需要使用 array 函数将一个列表的列表转换,其中每个内部列表都是一个行矩阵;或者你可以创建一个包含你的数据的向量,然后将其重塑成你想要的矩阵形状:
In: multiple_feature_matrix = \
np.array([[1,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15]])
小贴士
在 NumPy 中,也有用于快速创建全一和全零矩阵的特殊函数。作为参数,只需在元组中指定预期的 (x, y) 形状即可:
all_zeros = np.zeros((5,3))
all_ones = np.ones((5,3))
我们正在使用的作为 X 的过去观察集的信息可以深刻地影响我们如何建立 X 和 y 之间的联系。
事实上,通常情况下,我们并不知道 X 和 y 之间可能存在的全部关联范围,因为:
-
我们刚刚观察到某个 X,因此我们对给定 X 的 y 经验是有偏见的,这是一种抽样偏差,因为在彩票中,我们只抽取了游戏中的某些数字,而不是所有可用的数字
-
我们从未观察到某些 (X, y) 关联(请注意括号中的表述,表示 X 和 y 之间的相互关系),因为它们以前从未发生过,但这并不意味着它们将来不会发生(顺便说一句,我们正在努力预测未来)
与第二个问题关系不大,(我们只能通过过去指出的方向来预测未来),但你实际上可以检查你使用的数据有多新。如果你试图在一个非常容易变化且每天都会变化的背景下进行预测,你必须记住你的数据可能会很快过时,你可能无法预测新的趋势。一个需要不断更新模型的可变环境例子是广告行业(那里的竞争环境脆弱且不断变化)。因此,你需要不断地收集更新的数据,这可能会让你构建一个更有效的监督算法。
至于第一个问题,你可以通过使用来自不同来源的越来越多的情况来解决它。你抽取的样本越多,你的 X 集合就越有可能类似于 X 与 y 可能和真实关联的完整集合。这可以通过概率和统计学中的一个重要思想来理解:大数定律。
大数定律表明,随着你实验数量的增加,其结果平均值代表真实值的可能性(即实验本身试图弄清楚的价值)也会增加。
监督算法通过从大型数据存储库(如数据库或数据湖)一次性获取的历史数据的大量样本进行学习,称为批次。或者,它们也可以自行选择对学习最有用的示例,并忽略大量数据(这被称为主动学习,它是一种半监督学习,我们在这里不会讨论)。
如果我们的环境节奏快,它们也可以像数据可用时一样实时流数据,持续适应预测变量和响应之间的任何新关联(这被称为在线学习,我们将在第七章中讨论,在线和批量学习)。
预测 X 矩阵的另一个重要方面是,到目前为止,我们假设我们可以确定性推导出响应 y,使用矩阵 X 中的信息。不幸的是,在现实世界中并不总是如此,而且你实际上试图使用一组完全错误的预测 X 来确定你的响应 y 的情况并不少见。在这种情况下,你必须意识到你实际上在浪费时间试图在 X 和 y 之间拟合某种工作关系,你应该寻找一些不同的 X(在更多变量的意义上,即更多的数据)。
根据所使用的模型,从不同角度来看,拥有更多的变量和案例通常是有益的。更多的案例减少了从有偏见和有限的观察集中学习的可能性。许多算法如果使用大量观察集进行训练,可以更好地估计其内部参数(并产生更准确的预测)。此外,拥有更多的变量在手也可能有益,但这种益处在于它增加了使用解释性特征进行机器学习的机会。实际上,许多算法对特征中存在的冗余信息和噪声很敏感,因此需要一些特征选择来减少模型中涉及的预测因子。这在线性回归中尤为如此,它当然可以利用更多的案例进行训练,但它也应该接收到一个简约且高效的特性集,以发挥其最佳性能。关于 X 矩阵的另一个重要方面是,它应该仅由数字组成。因此,你正在处理的内容真的很重要。你可以处理以下内容:
-
物理测量值,因为它们自然是数字(例如,身高)
-
人类测量值,虽然稍微差一些,但如果有一定的顺序(即,我们根据判断给出的所有数字),仍然是可以接受的,因此它们可以被转换为排名数字(例如,第一、第二和第三值分别为 1、2 和 3,依此类推)
我们称这样的值为定量测量。我们期望定量测量是连续的,这意味着定量变量可以取任何有效的正数或负数作为值。人类测量值通常只有正数,从零或一开始,因此将它们视为定量测量是合理的。
在统计学中,对于物理测量值,我们区分区间变量和比例变量。区别在于比例变量有一个自然的零点,而在区间数据中,零点是任意的。一个很好的例子是温度;事实上,除非你使用开尔文温标,其零点是绝对的,否则华氏和摄氏都有任意的刻度。主要影响在于比率(如果零是任意的,比率也是任意的)。
数值的人类测量被称为有序变量。与区间数据不同,有序数据没有自然零点。此外,区间尺度上每个值之间的间隔是相等且规则的;然而,在有序尺度上,尽管值之间的距离相同,但它们的实际距离可能非常不同。让我们考虑一个由三个文本值组成的尺度:好、平均和坏。接下来,让我们随意决定好是 3,平均是 2,坏是 1。我们称这种随意的值赋为有序编码。现在,从数学的角度来看,尽管 3 和 2 之间的间隔与 2 和 1 之间的间隔相同(即一个点),但我们真的确定好和平均之间的实际距离与平均和坏之间的距离相同吗?例如,从客户满意度的角度来看,从差到平均再到优秀的努力是否花费相同的精力?
定性测量(例如,如好、平均或坏这样的价值判断,或如红色、绿色或蓝色这样的属性)需要进行一些工作,一些巧妙的数据操作,但它们仍然可以通过适当的转换成为我们的 X 矩阵的一部分。甚至更无结构的定性信息(如文本、声音或绘图)也可以转换并减少到数字池中,并可以摄入到 X 矩阵中。
定性变量可以存储为单值向量中的数字,或者每个类别都有一个向量。在这种情况下,我们谈论的是二元变量(在统计语言中也称为虚拟变量)。
我们将在第五章数据准备中更详细地讨论如何将手头的数据(特别是如果其类型是定性数据)转换成适合监督学习的输入矩阵。
在实际处理数据之前,有必要质疑以下问题:
-
数据的质量——也就是说,可用的数据是否真的能代表提取 X-y 规则的正确信息池
-
数据的数量——也就是说,检查有多少数据可用,记住,为了构建稳健的机器学习解决方案,拥有大量不同变量和案例更安全(至少当你处理数千个示例时)
-
数据在时间上的扩展——也就是说,检查数据在过去覆盖了多少时间(因为我们是从过去学习的)
反思响应变量
反思响应变量的作用,我们首先应该关注的是我们将要预测的变量类型,因为这将区分要解决的监督问题的类型。
如果我们的响应变量是一个定量变量,即一个数值,我们的问题将是回归问题。有序变量可以作为回归问题来解决,特别是如果它们包含许多不同的不同值。回归监督算法的输出是一个可以直接使用并与其他预测值以及用于学习的真实响应值进行比较的值。
例如,作为一个回归问题的例子,在房地产行业中,一个回归模型可以仅根据一些关于其位置和特性的信息来预测房屋的价值,这使得我们可以通过使用模型的预测作为公平事实估计的指标来立即发现市场价格是否过于便宜或昂贵(如果我们可以通过模型重建价格,那么它肯定是由我们用作预测者的可测量特性的价值所充分证明的)。
如果我们的响应变量是一个定性变量,我们的问题就是分类问题。如果我们只需要在两个类别之间进行猜测,那么我们的问题被称为二元分类;否则,如果涉及更多类别,则称为多标签分类问题。
例如,如果我们想要猜测两支足球队之间的比赛胜者,我们面临的是一个二元分类问题,因为我们只需要知道第一支队伍是否会获胜(两类是队伍获胜,队伍失败)。相反,我们可以使用多标签分类来预测在特定数量的足球队中哪支队伍会获胜(因此,在我们的预测中,要猜测的类别是球队)。
如果有序变量不包含很多不同的值,它们可以被视为多标签分类问题。例如,如果你必须猜测一支足球队在足球锦标赛中的最终排名,你可以尝试预测它在排行榜上的最终位置作为一个类别。因此,在这个有序问题中,你必须猜测与锦标赛中不同位置相对应的多个类别:类别 1 可以代表第一位置,类别 2 代表第二位置,依此类推。总之,你可以将一支球队的最终排名视为可能性最大的获胜位置类别。
至于输出,分类算法可以提供对精确类别的分类以及属于任何手头类别的概率估计。
继续以房地产行业为例,一个分类模型可以预测一栋房子是否可以成为一笔划算的交易,或者根据其位置和特性是否能够增值,从而允许进行谨慎的投资选择。
响应变量最明显的问题是其精确性。回归问题中的测量误差和分类问题中的误分类可以通过提供不准确的信息来损害模型在真实数据上的表现能力。此外,偏颇的信息(例如,当你只提供某个类别的案例而不是所有可用的案例时)可能会损害模型在现实生活中的预测能力,因为它会导致模型从非现实的角度看待数据。响应变量的不准确比特征问题对模型更困难、更危险。
对于单个预测变量,结果变量 y 也是一个向量。在 NumPy 中,你只需将其设置为通用向量或列向量:
In: y = np.array([1,2,3,4,5]).reshape((5,1))
线性模型家族
线性模型家族之所以得名,是因为指定 X(预测变量)、y(目标变量)之间关系的函数是 X 值的线性组合。线性组合实际上就是一个求和,其中每个加数都经过一个权重的调整。因此,线性模型仅仅是求和的一种更智能的形式。
当然,在这个求和中有一个技巧,使得预测变量在预测答案值时表现得像它们那样。正如我们之前提到的,预测变量应该告诉我们一些信息,它们应该给我们一些关于答案变量的提示;否则,任何机器学习算法都无法正常工作。我们可以预测我们的响应,因为关于答案的信息已经存在于特征中,可能分散、扭曲或转换,但它确实在那里。机器学习只是收集和重建这样的信息。
在线性模型中,这种内部信息通过用于求和的权重变得明显并被提取出来。如果你实际上有一些有意义的预测变量,权重将只做所有繁重的工作来提取和转换它,形成一个适当且精确的答案。
由于 X 矩阵是一个数值矩阵,其元素之和将得到一个数值本身。因此,线性模型是解决任何回归问题的正确工具,但它们不仅限于猜测实数。通过响应变量的转换,它们可以预测计数(正整数)和相对于属于某个特定组或类的概率(或不)。
在统计学中,线性模型家族被称为广义线性模型(GLM)。通过特殊的链接函数、对答案变量的适当转换、对权重适当的约束以及不同的优化过程(学习过程),GLM 可以解决非常广泛的各类问题。在这本书中,我们的论述不会超出统计领域所必需的内容。然而,我们将提出 GLM 大家族中的一些模型,即线性回归和逻辑回归;这两种方法都适合解决数据科学中最基本的两个问题:回归和分类。
由于线性回归不需要对答案变量进行任何特定的转换,并且它在概念上是线性模型的真实基础,因此我们将首先了解它是如何工作的。为了使事情更容易理解,我们将从一个仅使用单个预测变量的线性模型案例开始,即所谓的简单线性回归。与同时使用许多预测变量的多重形式相比,简单线性回归的预测能力非常有限。然而,它更容易理解和弄清楚其工作原理。
准备发现简单的线性回归
我们在整本书中提供了一些 Python 的实际示例,并且不会将各种回归模型仅停留在纯粹的理论层面上的解释。相反,我们将一起探索一些示例数据集,并系统地向您展示实现工作回归模型、解释其结构和部署预测应用的必要命令。
小贴士
数据集是一个包含预测变量有时还包括响应变量的数据结构。在机器学习方面,它可以被结构化或半结构化为矩阵形式,呈现为具有行和列的表格。
对于线性回归简单版本(仅使用一个预测变量来预测响应变量)的初始介绍,我们选择了一些与房地产评估相关的数据集。
房地产是一个非常适合自动预测模型的有趣话题,因为从人口普查中可以获取大量免费数据,而且作为一个开放的市场,还可以从监控市场和其提供的网站中抓取更多数据。此外,由于租房或买房对许多个人来说是一个相当重要的经济决策,因此帮助收集和整理大量可用信息的在线服务确实是一个很好的商业模式想法。
第一个数据集是一个非常历史性的数据集。它来自 Harrison, D. 和 Rubinfeld, D.L. 的论文 Hedonic Housing Prices and the Demand for Clean Air(J. Environ. Economics & Management,vol.5,81-102,1978),该数据集可以在许多分析包中找到,并存在于 UCI 机器学习存储库中(archive.ics.uci.edu/ml/datasets/Housing)。
该数据集由 1970 年人口普查中的波士顿 506 个街区组成,并包含 21 个可能影响房地产价值的变量。目标变量是房屋的中位货币价值,以千美元为单位表示。在可用的特征中,有一些相当明显,例如房间数量、建筑物的年龄和社区的犯罪水平,还有一些不那么明显的特征,例如污染浓度、附近学校的可用性、通往高速公路的通道以及就业中心的距离。
卡内基梅隆大学 Statlib 存储库的第二个数据集(archive.ics.uci.edu/ml/datasets/Housing)包含从 1990 年美国人口普查中得出的 20,640 个观测值。每个观测值是一系列统计数据(9 个预测变量),涉及一个街区组——即大约 1,425 个居住在地理上紧凑区域的人。目标变量是该街区房屋价值的指标(技术上说是普查时中位房屋价值的自然对数)。预测变量基本上是中位收入。
该数据集已被 Pace 和 Barry (1997) 在 Sparse Spatial Autoregressions,Statistics and Probability Letters 中使用,www.spatial-statistics.com/pace_manuscripts/spletters_ms_dir/statistics_prob_lets/pdf/fin_stat_letters.pdf),这是一篇关于包括空间变量的回归分析的论文(分析中关于位置的信息,包括它们在分析中的位置或与其他地点的邻近程度)。该数据集背后的想法是,房屋价值的变异性可以通过代表人口、建筑密度和按区域汇总的人口富裕程度的内生变量(即,房屋本身之外)来解释。
下载数据的代码如下:
In: from sklearn.datasets import fetch_california_housing
from sklearn.datasets import load_boston
boston = load_boston()
california = fetch_california_housing()
小贴士
boston and california variables available for analysis.
从基础知识开始
我们将开始探索第一个数据集,波士顿数据集,但在深入数字之前,我们将上传一系列将在本章剩余部分使用的有用包:
In: import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
如果你正在使用 IPython Notebook,可以在单元格中运行以下命令来指示 Notebook 在其自身中显示任何图形输出(如果你不在 IPython 中工作,只需忽略此命令,因为它在 Python 的 IDLE 或 Spyder 等 IDE 中无法工作):
In: %matplotlib inline
# If you are using IPython, this will make the images available in the Notebook
为了立即选择我们需要的变量,我们只需将所有可用的数据框架成一个 Pandas 数据结构,即DataFrame。
受到 R 统计语言中存在的类似数据结构的启发,DataFrame使得在同一数据集变量下处理不同类型的数据向量变得容易,同时提供了处理缺失值和操作数据的许多便利功能:
In: dataset = pd.DataFrame(boston.data, columns=boston.feature_names)
dataset['target'] = boston.target
到目前为止,我们已经准备好构建我们的第一个回归模型,直接从我们的 Pandas DataFrame 中的数据学习。
正如我们提到的,线性回归只是一个简单的求和,但它确实不是可能的最简单模型。最简单的是统计均值。实际上,你可以简单地通过始终使用相同的常数来猜测,而均值很好地承担了这样的角色,因为它是一个强大的数据总结描述数。
均值与正态分布数据配合得非常好,但通常对于不同的分布也非常适用。一个正态分布的曲线是数据分布,它是对称的,并且具有关于其形状的某些特性(一定的高度和分布)。
正态分布的特性由公式定义,并且有适当的统计测试来确定你的变量是否为正态分布,因为许多其他分布与正态分布的钟形相似,并且许多不同的正态分布是由不同的均值和方差参数生成的。
理解一个分布是否为正态分布的关键是概率密度函数(PDF),这是一个描述分布中值概率的函数。
在正态分布的情况下,概率密度函数(PDF)如下:

在这种表述中,符号µ代表均值(与中位数和众数相同)和符号σ代表方差。基于不同的均值和方差,我们可以计算不同的值分布,如下面的代码所示并进行可视化:
In: import matplotlib.pyplot as plt
import numpy as np
import matplotlib.mlab as mlab
import math
x = np.linspace(-4,4,100)
for mean, variance in [(0,0.7),(0,1),(1,1.5),(-2,0.5)]:
plt.plot(x,mlab.normpdf(x,mean,variance))
plt.show()

由于其性质,正态分布是统计学中的一个基本分布,因为所有统计模型都涉及对正态变量的处理。特别是,当均值为零且方差为 1(单位方差)时,在这种条件下称为标准正态分布的正态分布,对于统计模型具有更加有利的特性。
不管怎样,在现实世界中,正态分布的变量是罕见的。因此,验证我们正在工作的实际分布是否与理想的正态分布相差不远是很重要的,否则它可能会在你的预期结果中引起问题。正态分布变量是统计模型(如平均值和,在某种程度上,线性回归)的重要要求。相反,机器学习模型不依赖于任何关于你的数据应该如何分布的先验假设。但事实上,即使机器学习模型在数据具有某些特征时也能很好地工作,因此,与正态分布变量一起工作比其他分布更可取。在整本书中,我们将提供有关在构建和应用机器学习解决方案时应该寻找和检查的内容的警告。
对于平均值的计算,如果分布不是对称的,或者存在极端情况,可能会出现相关的问题。在这种情况下,极端情况往往会将平均值估计值拉向它们,从而导致与大量数据不匹配。那么,让我们计算一下波士顿 506 个地块的价值的平均值:
In: mean_expected_value = dataset['target'].mean()
在这种情况下,我们使用 Pandas DataFrame 中可用的方法计算了平均值;然而,也可以调用 NumPy 函数mean从数据数组中计算平均值:
In: np.mean(dataset['target'])
从数学公式来说,我们可以将这个简单的解决方案表达如下:

我们现在可以通过测量预测真实y值时产生的误差来评估结果。统计学表明,为了衡量预测值和真实值之间的差异,我们应该将差异平方,然后将它们全部相加。这被称为误差平方和:
In: Squared_errors = pd.Series(mean_expected_value -\
dataset['target'])**2
SSE = np.sum(Squared_errors)
print ('Sum of Squared Errors (SSE): %01.f' % SSE)
现在我们已经计算出来了,我们可以将其可视化为误差分布:
In: density_plot = Squared_errors.plot('hist')

图表显示了某些错误值出现的频率。因此,你将立即注意到,大多数错误都围绕零(该值周围有很高的密度)。这种情况可以被认为是一个好情况,因为在大多数情况下,平均值是一个很好的近似值,但一些错误确实非常远离零,并且可以达到相当大的值(别忘了,错误是平方的,所以效果被强调)。当试图找出这样的值时,你的方法肯定会引导到一个相关的错误,我们应该找到一种更复杂的方法来最小化它。
线性关系的度量
显然,均值不是某些值的良好代表,但它确实是一个良好的起点。当然,均值的一个重要问题是它是固定的,而目标变量是可变的。然而,如果我们假设目标变量是由于我们正在测量的某些其他变量的影响而变化的,那么我们可以根据原因的变化调整均值。
对我们之前的方法的一个改进可能是构建一个基于另一个变量(甚至更多)的某些值(实际上与我们的目标相关)的均值,其变化在某种程度上类似于目标变量的变化。
直观地说,如果我们知道我们想要用我们的模型预测的动态,我们可以尝试寻找我们知道可以影响答案值的变量。
在房地产业务中,我们实际上知道通常房子越大,价格越贵;然而,这个规则只是故事的一部分,价格还受到许多其他因素的影响。目前,我们将保持简单,并假设房屋的扩建是一个正影响价格的因子,因此,更多的空间意味着更高的成本(更多的土地、更多的建筑材料、更多的工作,以及因此更高的价格)。
现在,我们有一个我们知道应该随着我们的目标变量变化的变量,我们只需要测量它,并基于常数值扩展我们的初始公式。
在统计学中,有一个度量可以帮助衡量两个变量如何(在多大程度上以及朝哪个方向)相互关联:相关性。
在相关性分析中,需要考虑几个步骤。首先,你的变量必须标准化(否则你的结果将不是相关性而是协变,这是一种受你正在处理的变量规模影响的关联度量的指标)。
在统计的 Z 分数标准化中,你需要从每个变量中减去其均值,然后将结果除以标准差。得到的转换后的变量将具有均值为 0 和标准差为 1(或单位方差,因为方差是标准差的平方)。
标准化一个变量的公式如下:

这可以通过使用 Python 中的一个简单函数来实现:
In: def standardize(x):
return (x-np.mean(x))/np.std(x)
标准化后,你比较每个变量的平方差与其自身均值的平方差。如果这两个差值在符号上相同,它们的乘积将是正的(表明它们具有相同的方向性);然而,如果它们不同,乘积将变为负的。通过将所有平方差之间的乘积相加,并将它们除以观察次数,你最终将得到一个介于 -1 到 1 之间的相关系数。
相关系的绝对值将向您提供两个比较变量之间关系的强度,1 表示完美匹配,而 0 表示它们之间完全独立(它们之间没有任何关系)。相反,符号将暗示比例;正号是直接的(当一个增长时,另一个也增长),负号是间接的(当一个增长时,另一个缩小)。
协方差可以表示如下:

而皮尔逊相关系数可以表示如下:

让我们直接在 Python 中检查这些公式。如您所注意到的,皮尔逊相关系数实际上是标准化的变量上的协方差,因此我们将correlation函数定义为covariance和standardize函数的包装器(您可以从Scipy导入所有这些函数;我们在这里实际上是在重新创建它们,只是为了帮助您理解它们是如何工作的):
In:
def covariance(variable_1, variable_2, bias=0):
observations = float(len(variable_1))
return np.sum((variable_1 - np.mean(variable_1)) * \
(variable_2 - np.mean(variable_2)))/(observations-min(bias,1))
def standardize(variable):
return (variable - np.mean(variable)) / np.std(variable)
def correlation(var1,var2,bias=0):
return covariance(standardize(var1), standardize(var2),bias)
from scipy.stats.stats import pearsonr
print ('Our correlation estimation: %0.5f' % (correlation(dataset['RM'], dataset['target'])))
print ('Correlation from Scipy pearsonr estimation: %0.5f' % pearsonr(dataset['RM'], dataset['target'])[0])
Out: Our correlation estimation: 0.69536
Correlation from Scipy pearsonr estimation: 0.69536
我们对目标变量值与该区域房屋平均房间数之间关系的相关估计值为 0.695,这是一个正的相关性,并且非常显著,因为相关性的最大正值是 1.0。
小贴士
为了估计一个相关性是否相关,只需将其平方;结果将表示两个变量共享的方差百分比。
让我们绘制当我们对两个变量进行相关性分析时会发生什么。使用散点图,我们可以轻松地可视化两个涉及的变量。散点图是一种图表,其中两个变量的值被视为笛卡尔坐标;因此,对于每个(x, y)值,图上都有一个点表示:
In: x_range = [dataset['RM'].min(),dataset['RM'].max()]
y_range = [dataset['target'].min(),dataset['target'].max()]
scatter_plot = dataset.plot(kind='scatter', x='RM', y='target',\xlim=x_range, ylim=y_range)
meanY = scatter_plot.plot(x_range, [dataset['target'].mean(),\ dataset['target'].mean()], '--' , color='red', linewidth=1)
meanX = scatter_plot.plot([dataset['RM'].mean(),\dataset['RM'].mean()], y_range, '--', color='red', linewidth=1)

散点图还绘制了目标和预测变量的平均值作为虚线。这把图表分成四个象限。如果我们将其与先前的协方差和相关性公式进行比较,我们可以理解为什么相关性值接近 1:在右下角和左上角象限中,只有少数不匹配的点,其中一个变量高于其平均值,而另一个变量低于其平均值。
完美匹配(相关性值为 1 或-1)仅在点位于一条直线上(因此所有点都集中在右上角和左下角象限)时才可能。因此,相关性是线性关联的度量,是您的点接近直线的程度。理想情况下,所有点都在一条直线上有利于将预测变量完美映射到目标变量。
扩展到线性回归
线性回归试图通过一组给定的点拟合一条线,选择最佳拟合。最佳拟合是使由线决定的 x 的值与其对应的 y 值之间的平方差之和最小的线。(这是在检查均值作为预测指标的好坏时遇到的相同平方误差的优化。)
由于线性回归是一条线;在二维空间(x,y)中,它采用笛卡尔平面上经典线的公式形式:y = mx + q,其中 m 是角度系数(表示线与 x 轴之间的角度)和 q 是线与 x 轴之间的截距。
形式上,机器学习将线性回归的正确表达式表示如下:

这里再次,X 是预测变量的矩阵,β 是系数矩阵,而 β[0] 是一个称为 偏差 的常数值(它与笛卡尔公式相同,只是符号不同)。
我们可以通过使用 Python 看到它的实际运行机制来更好地理解其工作原理,首先使用 StatsModels 包,然后使用 Scikit-learn 包。
使用 Statsmodels 进行回归
Statsmodels 是一个考虑到统计分析的包;因此,它的函数提供了相当丰富的统计检查和信息输出。可扩展性对该包来说不是问题;因此,它确实是学习的良好起点,但如果你必须处理相当大的数据集(甚至大数据)时,由于其优化算法,它肯定不是最佳解决方案。
有两种不同的方法(两个模块)使用 Statsmodels 进行线性回归:
-
statsmodels.api:这适用于不同的预测变量和答案变量,并要求你定义预测变量上的任何变量转换,包括添加截距 -
statsmodels.formula.api:这类似于 R,允许你指定一个函数形式(预测变量求和的公式)
我们将使用 statsModels.api 展示我们的示例;然而,我们还将展示使用 statsmodels.formula.api 的另一种方法。
作为第一步,让我们上传 Statsmodels 的两个模块,按照包文档中的惯例命名:
In: import statsmodels.api as sm
import statsmodels.formula.api as smf
作为第二步,有必要定义 y 和 X 变量:
In: y = dataset['target']
X = dataset['RM']
X = sm.add_constant(X)
X 变量需要通过一个常数值 () 进行扩展;偏差将相应地计算。实际上,正如你记得的那样,线性回归的公式如下:

然而,使用 StatsModels.api,公式实际上变成了以下形式:

这可以解释为 X 中变量的组合,乘以其对应的 β 值。
因此,预测变量X现在既包含预测变量又包含一个单位常数。此外,β不再是一个单一的系数,而是一系列系数。
让我们通过使用head方法查看 Pandas DataFrame 的第一个值来验证这一点:
In: X.head()

在这一点上,我们只需要设置线性回归计算的初始化:
In: linear_regression = sm.OLS(y,X)
此外,我们还需要请求回归系数的估计,即β向量:
In: fitted_model = linear_regression.fit()
如果我们想要使用StatsModels.formula.api来管理相同的结果,我们应该输入以下内容:
In: linear_regression = smf.ols(formula='target ~ RM', data=dataset)
fitted_model = linear_regression.fit()
前两条代码行同时包含了两个步骤,无需进行任何特定的变量准备,因为偏差是自动包含的。实际上,关于线性回归应该如何工作的规范被包含在字符串target ~ RM中,其中波浪号(~)左侧的变量名表示答案变量,右侧的变量名(或名称,在多重回归分析的情况下)表示预测变量。
实际上,smf.ols期望的输入与sm.OLS相当不同,因为它可以接受我们的整个原始数据集(它通过提供的公式选择要使用的变量),而sm.OLS期望一个仅包含用于预测的特征的矩阵。因此,在使用这两种截然不同的方法时必须谨慎行事。
一个总结(拟合模型的某种方法)可以快速告诉你关于回归分析所需了解的所有内容。如果你尝试过statsmodesl.formula.api,我们也重新使用StatsModels.api初始化线性回归,因为它们不是在相同的X上工作,而我们的后续代码依赖于sm.OLS规范:
In: linear_regression = sm.OLS(y,X)
fitted_model = linear_regression.fit()
fitted_model.summary()



你将收到一系列相当长的表格,包含许多统计测试和信息。虽然一开始可能相当令人畏惧,但实际上你不需要所有这些输出,除非你的分析目的是统计性的。数据科学主要关注的是实际模型在预测真实数据上的工作,而不是在统计问题的形式化规范上。尽管如此,其中一些输出对于成功构建模型仍然很有用,我们将为你提供对主要数字的洞察。
在解释输出之前,我们首先需要从拟合模型中提取两个元素:系数和基于我们构建模型的数据的预测。它们在接下来的解释中都将非常有用:
In: print (fitted_model.params)
betas = np.array(fitted_model.params)
fitted_values = fitted_model.predict(X)
决定系数
让我们从第一个结果表开始。第一个表分为两列。第一列包含拟合模型的描述:
-
因变量: 这只是提醒你目标变量是什么
-
模型: 这是您已拟合的模型的另一个提醒,OLS 是普通最小二乘法,也是线性回归的另一种说法
-
方法: 参数拟合方法(在这种情况下是最小二乘法,经典计算方法)
-
观测数: 已使用的观测值数量
-
残差 DF: 残差的自由度,即观测值数量减去参数数量
-
模型 DF: 模型中估计的参数数量(不包括常数项)
第二张表提供了一个更有趣的图景,重点关注线性回归模型的拟合程度,并指出模型可能存在的问题:
-
R 平方: 这是决定系数,衡量回归相对于简单平均的效果。
-
调整 R 平方: 这是基于模型中的参数数量和帮助构建它的观测值数量的决定系数的调整值。
-
F 统计量: 这是一个衡量指标,告诉你从统计学的角度来看,除了偏差之外,所有系数加在一起是否与零不同。简单来说,它告诉你你的回归是否真的比简单平均更好。
-
F 统计量的概率: 这是你仅仅因为使用了观测值而得到那个 F 统计量的概率(这种概率实际上被称为 F 统计量的p 值)。如果它足够低,你可以有信心你的回归确实比简单平均更好。通常在统计学和科学中,测试概率必须等于或低于 0.05(统计显著性的传统标准)才能有这种信心。
-
AIC: 这是赤池信息准则。AIC 是根据观测值数量和模型本身的复杂性来评估模型的分数。AIC 分数越低,越好。它对于比较不同模型和进行统计变量选择非常有用。
-
BIC: 这是贝叶斯信息准则。它像 AIC 一样工作,但它对参数更多的模型施加更高的惩罚。
当我们处理多个预测变量时,大多数这些统计数据都是有意义的,因此它们将在下一章中进行讨论。因此,目前,由于我们正在处理简单的线性回归,值得仔细检查的两个度量标准是 F 统计量和 R 平方。实际上,F 统计量是一个测试,如果你有足够的观测值并且可以依赖一个最小相关性的预测变量,它不会告诉你太多。通常,在数据科学项目中,这不应该是一个很大的问题。
R 平方更有趣,因为它告诉你你的回归模型与单个均值相比有多好。它是通过提供一个百分比来做到这一点的,即均值的未解释方差,实际上你的模型能够解释。
如果你想自己计算这个度量,你只需要计算目标变量均值的平方误差和。这就是你的未解释方差的基础线(在我们例子中,我们希望通过模型来解释的房价的变异性)。如果你从这个基础线中减去回归模型的平方误差和,你将得到残差平方误差和,可以通过除以基础线来比较:
In: mean_sum_squared_errors = np.sum((dataset['target']-\dataset['target'].mean())**2)
regr_sum_squared_errors = np.sum((dataset['target']-\fitted_values)**2)
(mean_sum_squared_errors-\regr_sum_squared_errors) / mean_sum_squared_errors
Out: 0.48352545599133412
小贴士
当处理浮点数时,可能会出现舍入误差,所以如果你的计算中某些小数位不匹配,不要害怕;如果它们匹配到第 8 位小数,你可以相当自信地认为结果是相同的。
理想情况下,如果你能将回归的平方误差和减少到零,你将得到最大的解释方差百分比——即得分为 1。
R 平方度量也可以与你的预测变量和目标变量之间相关性的平方所获得的百分比进行比较。
在我们的例子中,它是 0.484,这实际上正好是我们的 R 平方相关系数:
In: (pearsonr(dataset['RM'], dataset['target'])[0])**2
Out: 0.4835254559913339
正如我们所见,R 平方与线性回归试图最小化的平方误差完全一致;因此,更好的 R 平方意味着更好的模型。然而,当同时处理多个预测变量时,这个度量(以及线性回归本身)存在一些问题。再次强调,我们必须等到我们同时建模多个预测变量;因此,对于简单的线性回归来说,更好的 R 平方应该暗示着更好的模型。
系数的意义和重要性
第二个输出表告诉我们关于系数的信息,并提供了一系列测试。这些测试可以让我们确信,我们没有因为分析基础中的几个极端观察值或某些其他问题而被误导:
-
coef:估计的系数
-
std err:系数估计的标准误差;它越大,系数的估计就越不确定
-
t:t 统计量值,一个衡量系数真实值是否与零不同的指标
-
P > |t|:表示系数不同于零的概率的 p 值,仅通过偶然性
-
[95.0% 置信区间]:考虑所有可能的不同观察值和因此不同的估计系数的 95%的所有机会,系数的下限和上限值
从数据科学的角度来看,t 检验和置信区间并不是非常有用,因为我们主要感兴趣的是验证我们的回归在预测答案变量时是否有效。因此,我们将只关注coef值(估计系数)及其标准误差。
系数是我们从回归模型中可以获得的最重要输出,因为它们允许我们重新创建加权求和,从而预测我们的结果。
在我们的例子中,我们的系数对于偏差(也称为截距,回忆一下笛卡尔空间中线的公式)是−34.6706,对于RM变量是9.1021。回忆我们的公式,我们可以将我们获得的数字代入:

现在,如果你用估计的系数替换贝塔和x,用变量的名称替换−34.6706和9.1021,一切就变成了以下内容:

现在,如果你知道波士顿某个地区的平均房间数量,你可以快速估计期望值。例如,x[RM]是4.55:
In: 9.1021*4.55-34.6706
Out: 6.743955
我们必须注意两个点。首先,在这种公式中,每个变量的贝塔成为其单位变化的度量,这对应于变量增加一个单位时结果将经历的变化。在我们的例子中,我们的平均房间空间变为5.55:
In: 9.1021*5.55-34.6706
Out: 15.846055
x[RM]单位变化引起的增加对应于结果的变化,相当于β[RM]。另一个需要注意的点是我们平均房间空间变为 1 或 2 时,我们的估计值将变为负数,这是完全不现实的。这是因为预测变量和目标变量之间的映射发生在预测变量值的限定范围内:
In: (np.min(dataset['RM']),np.max(dataset['RM']))
Out: (3.5609999999999999, 8.7799999999999994)
无论何时我们尝试使用一个x(或一组x)来估计答案值,而这个x(或一组x)超出了我们用于拟合模型的边界,我们都有可能得到一个线性回归计算根本未进行优化的响应。换一种说法,线性回归可以学习它所看到的东西,除非预测变量和目标变量之间存在清晰的线性函数形式(它们可以真正表示为一条线),否则当预测变量具有不寻常的值时,你可能会得到奇怪的估计。换句话说,线性回归总是在它从中学到的值范围内工作(这被称为插值),但只有在某些条件下才能为其学习边界提供正确的值(这被称为外推)。
小贴士
正如我们之前提到的,用于拟合模型的观测数对于获得一个稳健且可靠的线性回归模型至关重要。观测数越多,模型在生产运行中遇到异常值的可能性就越小。
标准误差则非常重要,因为它们表明预测变量与答案之间关系薄弱或不明确。你可以通过将标准误差除以其 beta 值来注意到这一点。如果比率是 0.5 或更大,那么这是一个明显的信号,表明模型对其提供的正确系数估计几乎没有信心。拥有更多案例总是解决方案,因为它可以减少系数的标准误差并提高我们的估计;然而,也有其他方法可以减少误差,例如通过主成分分析去除特征之间的冗余方差,或者通过贪婪选择选择一个简约的预测变量集。所有这些主题将在我们处理多个预测变量时讨论;在本书的这一部分,我们将说明解决此类问题的方法。
评估拟合值
最后一张表处理的是回归残差的分解。残差是目标值与预测拟合值之间的差异:
-
偏度:这是衡量残差围绕平均值对称性的指标。对于对称分布的残差,其值应接近零。正值表示右侧有长尾;负值表示左侧有长尾。
-
峰度:这是衡量残差分布形状的指标。钟形分布的测量值为零。负值指向过于平坦的分布;正值表示峰度过高。
-
Omnibus D'Angostino 测试:这是一个针对偏度和峰度的组合统计测试。
-
Omnibus 概率:这是将 Omnibus 统计量转换为概率。
-
Jarque-Bera 统计量:这是对偏度和峰度的另一种测试。
-
JB 概率:这是 JB 统计量转换为概率。
-
Durbin-Watson 统计量:这是对残差之间相关性的测试(在分析基于时间的数据时相关)。
-
条件数:这是对多重共线性(当处理多个预测变量时,我们将讨论多重共线性的概念)的测试。
在统计实践中,对残差进行密切分析相当重要,因为它可以突出回归分析中存在严重问题的迹象。当处理单个变量时,有趣的是通过视觉检查其残差,以确定是否存在异常案例或残差是否随机分布。特别是,重要的是要密切关注以下三个问题中的任何一个出现:
-
过于偏离平均值的值。大的标准化残差暗示在建模此类观察值时存在严重困难。此外,在学习这些值的过程中,回归系数可能已经被扭曲。
-
预测变量值的方差不同。如果线性回归是基于预测变量的平均条件,则异方差性表明当预测变量具有某些值时,回归工作不正常。
-
在残差点的云层中出现的奇异形状可能表明,你需要为正在分析的数据使用一个更复杂的模型。
在我们的案例中,我们可以通过从答案变量中减去拟合值来轻松计算残差,然后在一个图表中绘制结果的标准残差:
In: residuals = dataset['target']-fitted_values
normalized_residuals = standardize(residuals)
In: residual_scatter_plot = plt.plot(dataset['RM'], normalized_residuals,'bp')
mean_residual = plt.plot([int(x_range[0]),round(x_range[1],0)], [0,0], '-', color='red', linewidth=2)
upper_bound = plt.plot([int(x_range[0]),round(x_range[1],0)], [3,3], '--', color='red', linewidth=1)
lower_bound = plt.plot([int(x_range[0]),round(x_range[1],0)], [-3,-3], '--', color='red', linewidth=1)
plt.grid()

结果的散点图表明,残差显示出我们之前作为警告指出的一些问题,即你的回归分析中可能存在问题。首先,有一些点位于由标准化残差值-3 和+3 之间的两条虚线所界定的带状区域之外(如果残差呈正态分布,理论上应该覆盖 99.7%的值)。这些肯定是具有大误差的显著点,它们实际上可能会使整个线性回归表现不佳。我们将在下一章讨论异常值时讨论此问题的可能解决方案。
然后,点云并非完全随机分布,它在预测变量的不同值(横轴)上显示出不同的方差,你可以发现意外的模式(直线上的点或以某种 U 形排列的核心点)。
我们并不感到惊讶;平均房间数可能是一个很好的预测变量,但它不是唯一的原因,或者它必须被重新考虑为直接原因(房间数表示更大的房子,但如果是平均以下的房间呢?)。这使我们讨论一个强烈的关联是否真的使变量成为线性关系的良好候选者。
相关性不等于因果关系
实际上,看到你的预测变量和目标变量之间存在相关性,并且能够成功地使用线性回归对其进行建模,这并不意味着两者之间真的存在因果关系(尽管你的回归可能非常有效,甚至是最优的)。
虽然使用数据科学方法而不是统计方法可以保证你的模型具有一定的有效性,但在不知道为什么目标变量与预测变量相关时,很容易陷入一些错误。
我们将告诉你六个不同的原因,并提供一个警示词,帮助你轻松处理这样的预测变量:
-
直接因果关系:x 导致 y;例如,在房地产业务中,价值与房屋的平方米数成正比。
-
相互影响:x 导致 y,但它也受到 y 的影响。这在许多宏观经济动态中很典型,政策的效果会增强或减弱其效果。例如,在房地产领域,一个地区的犯罪率可能会降低其价格,但低价意味着该地区可能会迅速变得更加恶化且危险。
-
虚假因果关系:这发生在真实原因实际上是z,它同时导致x和y;因此,它只是一个错误的错觉,即x意味着y,因为背后是z。例如,昂贵艺术品商店和画廊的存在似乎与房价相关;实际上,两者都是由富裕居民的存在决定的。
-
间接因果关系:实际上x并没有导致y,而是导致其他事物,然后这些事物又导致y。一个优秀的市政府在提高税收后投资基础设施可以间接影响房价,因为该地区变得更加宜居,从而吸引更多需求。更高的税收,因此更多的投资,间接影响房价。
-
条件效应:在另一个变量z的值方面,x导致y;例如,当z具有某些值时,x不会影响y,但当z取特定值时,x开始影响y。我们也将这种情况称为交互。例如,当犯罪率低时,一个地区的学校存在可以成为一个吸引点,因此它只会在犯罪性很少时影响房价。
-
随机效应:任何记录的x和y之间的相关性都是由于幸运的抽样选择;实际上,与y之间根本不存在任何关系。
理想的情况是您有一个直接的因果关系;那么,您的模型中将有预测因子,它将始终为您提供最佳值以推导出您的响应。
在其他情况下,目标变量与不完美因果关系可能导致更嘈杂的估计,尤其是在生产中,您将不得不处理模型之前未见过的数据。
互为因果关系在计量经济学模型中更为典型。它们需要特殊类型的回归分析。将它们包含在您的回归分析中可能会改进您的模型;然而,它们的作用可能被低估。
虚假和间接原因会给您的x和y关系添加一些噪声;这可能导致更嘈杂的估计(更大的标准误差)。通常,解决方案是为您的分析获取更多观测值。
如果没有被发现,条件效应可能会限制您模型产生准确估计的能力。如果您对任何这些都不了解,根据您对问题的领域知识,使用一些自动程序检查可能的变量间交互是一个好步骤。
随机效应可能是您模型可能遇到的最糟糕的事情,但如果您遵循我们在第六章中将要描述的数据科学程序,即实现泛化,当我们处理验证您模型结果所需的所有必要行动时,它们是容易避免的。
使用回归模型进行预测
当我们将系数代入回归公式时,预测只是将新数据应用于系数向量进行矩阵乘法的问题。
首先,你可以通过提供一个包含新案例数组的数组来依赖拟合模型。在以下示例中,你可以看到,给定单个新案例的Xp变量,使用拟合模型的predict方法可以轻松预测:
In: RM = 5
Xp = np.array([1,RM])
print ("Our model predicts if RM = %01.f the answer value \is %0.1f" % (RM, fitted_model.predict(Xp)))
Out: Our model predicts if RM = 5 the answer value is 10.8
predict方法的一个很好的用法是将拟合预测投影到我们之前的散点图上,以便我们可以可视化与我们的预测变量,平均房间数量相关的价格动态:
In: x_range = [dataset['RM'].min(),dataset['RM'].max()]
y_range = [dataset['target'].min(),dataset['target'].max()]
scatter_plot = dataset.plot(kind='scatter', x='RM', y='target',\xlim=x_range, ylim=y_range)
meanY = scatter_plot.plot(x_range,\[dataset['target'].mean(),dataset['target'].mean()], '--',\color='red', linewidth=1)
meanX =scatter_plot.plot([dataset['RM'].mean(),\dataset['RM'].mean()], y_range, '--', color='red', linewidth=1)
regression_line = scatter_plot.plot(dataset['RM'], fitted_values,\'-', color='orange', linewidth=1)

x and *y* averages.
除了predict方法外,通过仅使用NumPy中的dot函数,生成预测相当简单。在准备一个包含变量数据和偏差(一列)的X矩阵以及系数向量后,你所要做的就是将矩阵乘以向量。结果本身将是一个长度等于观测数数量的向量:
In: predictions_by_dot_product = np.dot(X,betas)
print ("Using the prediction method: %s" % fitted_values[:10])
print ("Using betas and a dot product: %s" %
predictions_by_dot_product[:10])
Out: Using the prediction method: [ 25.17574577 23.77402099 30.72803225 29.02593787 30.38215211
23.85593997 20.05125842 21.50759586 16.5833549 19.97844155]
Using betas and a dot product: [ 25.17574577 23.77402099 30.72803225 29.02593787 30.38215211
23.85593997 20.05125842 21.50759586 16.5833549 19.97844155]
通过predict方法和这种简单乘法获得的结果比较将揭示完美匹配。因为从线性回归进行预测很简单,如果需要,你甚至可以在不同于 Python 的语言中实现这种乘法。在这种情况下,你只需要找到一个矩阵计算库或自己编写一个函数。据我们所知,你甚至可以在 SQL 脚本语言中轻松编写这样的函数。
使用 Scikit-learn 进行回归
正如我们在使用StatsModels包时所见,可以使用更面向机器学习的包,如 Scikit-learn 来构建线性模型。使用linear_model模块,我们可以设置一个线性回归模型,指定预测变量不应归一化,并且我们的模型应该有偏差:
In: from sklearn import linear_model
linear_regression = \linear_model.LinearRegression(normalize=False,\fit_intercept=True)
数据准备,相反,需要计算观测值并仔细准备预测数组以指定其两个维度(如果保留为向量,拟合过程将引发错误):
In: observations = len(dataset)
X = dataset['RM'].values.reshape((observations,1))
# X should be always a matrix, never a vector
y = dataset['target'].values # y can be a vector
完成所有前面的步骤后,我们可以使用fit方法拟合模型:
In: linear_regression.fit(X,y)
Scikit-learn 包的一个非常方便的特性是,所有模型,无论其复杂度类型如何,都共享相同的方法。fit方法始终用于拟合,并期望一个X和一个y(当模型是监督模型时)。相反,两个常见的用于精确预测(总是用于回归)及其概率(当模型是概率模型时)的方法分别是predict和predict_proba。
拟合模型后,我们可以检查系数向量和偏差常数:
In: print (linear_regression.coef_)
print (linear_regression.intercept_)
Out: [ 9.10210898]
-34.6706207764
使用predict方法和结果列表的前 10 个元素进行切片,我们输出前 10 个预测值:
In: print (linear_regression.predict(X)[:10])
Out: [ 25.17574577 23.77402099 30.72803225 29.02593787 30.38215211
23.85593997 20.05125842 21.50759586 16.5833549 19.97844155]
如前所述,如果我们准备一个新的矩阵并添加一个常数,我们可以通过简单的矩阵-向量乘法自行计算结果:
In: Xp = np.column_stack((X,np.ones(observations)))
v_coef = list(linear_regression.coef_) +\[linear_regression.intercept_]
如预期的那样,乘积的结果为我们提供了与predict方法相同的估计:
In: np.dot(Xp,v_coef)[:10]
Out: array([ 25.17574577, 23.77402099, 30.72803225, 29.02593787,
30.38215211, 23.85593997, 20.05125842, 21.50759586,
16.5833549 , 19.97844155])
在这一点上,质疑这种linear_model模块的使用是很自然的。与 Statsmodels 之前提供的函数相比,Scikit-learn 似乎提供的统计输出很少,而且看起来很多线性回归功能都被移除了。实际上,它提供了数据科学中所需的一切,并且在处理大型数据集时性能非常出色。
如果你正在使用 IPython,只需尝试以下简单的测试来生成一个大型数据集并检查两种线性回归版本的性能:
In: from sklearn.datasets import make_regression
HX, Hy = make_regression(n_samples=10000000, n_features=1,\n_targets=1, random_state=101)
在生成一百万个单一变量的观测值之后,首先使用 IPython 的%%time魔术函数来测量。这个魔术函数会自动计算在 IPython 单元格中完成计算所需的时间:
In: %%time
sk_linear_regression = linear_model.LinearRegression(\normalize=False,fit_intercept=True)
sk_linear_regression.fit(HX,Hy)
Out: Wall time: 647 ms
现在,轮到 Statsmodels 包登场了:
In: %%time
sm_linear_regression = sm.OLS(Hy,sm.add_constant(HX))
sm_linear_regression.fit()
Out: Wall time: 2.13 s
虽然模型中只涉及一个变量,但 Statsmodels 的默认算法证明比 Scikit-learn 慢三倍。我们将在下一章重复这个测试,当一次使用更多的预测变量和其他不同的fit方法时。
最小化成本函数
线性回归的核心是寻找一条直线的方程,该方程能够最小化直线y值与原始值之间差异的平方和。作为提醒,让我们假设我们的回归函数被称为h,其预测为h(X),如下所示:

因此,我们要最小化的成本函数如下:

有很多方法可以最小化它,其中一些在大数据量下表现优于其他方法。在表现较好的方法中,最重要的是伪逆(你可以在统计学的书籍中找到它)、QR 分解和梯度下降。
解释使用平方误差的原因
查看线性回归分析的内部机制,一开始可能会令人困惑,因为我们正在努力最小化我们的估计值与构建模型的数据之间的平方差。平方差不如绝对差(不带符号的差)直观易懂。
例如,如果你需要预测一个货币价值,比如股票价格或广告活动的回报,你更感兴趣的是知道你的绝对误差,而不是你的 R 平方值,这可能会被误解(因为平方会放大更大的损失)。
正如我们之前提到的,线性回归从统计知识领域吸取了步骤,实际上在统计学中有很多原因使得最小化平方误差比最小化绝对误差更可取。
不幸的是,这样的理由相当复杂,过于技术化,因此超出了这本书的实际范围;然而,从高层次的角度来看,一个良好合理的解释是,平方很好地实现了两个非常重要的目标:
-
它消除了负值;因此,当求和时,相反的误差不会相互抵消
-
它强调了更大的差异,因为当它们被平方时,与简单求和绝对值相比,它们将成比例地增加误差总和
使用估计值最小化平方差异使我们使用平均值(正如我们之前所建议的,作为一个基本模型,没有提供任何理由)。
让我们一起用 Python 来检查,而不需要开发所有公式。让我们定义一个包含值的x向量:
In: import numpy as np
x = np.array([9.5, 8.5, 8.0, 7.0, 6.0])
让我们也定义一个返回成本函数(平方差异)的函数:
In: def squared_cost(v,e):
return np.sum((v-e)**2)
使用scipy包提供的fmin最小化过程,我们试图找出一个向量(这将是我们的x向量),其值使得平方和最小:
In: from scipy.optimize import fmin
xopt = fmin(squared_cost, x0=0, xtol=1e-8, args=(x,))
Out: Optimization terminated successfully.
Current function value: 7.300000
Iterations: 44
Function evaluations: 88
我们只输出我们最好的e值,并验证它是否确实是x向量的平均值:
In: print ('The result of optimization is %0.1f' % (xopt[0]))
print ('The mean is %0.1f' % (np.mean(x)))
Out: The result of optimization is 78.0
The mean is 78.0
如果我们尝试找出什么最小化了绝对误差的总和:
In: def absolute_cost(v,e):
return np.sum(np.abs(v-e))
In: xopt = fmin(absolute_cost, x0=0, xtol=1e-8, args=(x,))
Out: Optimization terminated successfully.
Current function value: 5.000000
Iterations: 44
Function evaluations: 88
In: print ('The result of optimization is %0.1f' % (xopt[0]))
print ('The median is %0.1f' % (np.median(x)))
Out: The result of optimization is 8.0
The median is 8.0
我们会发现它是中位数,而不是平均值。不幸的是,中位数并不具备与平均值相同的统计特性。
伪逆和其他优化方法
存在一种解析公式可以用于解决回归分析,并从数据中得到系数向量,最小化成本函数:

展示这个方程超出了这本书的实用范围,但我们可以利用 Python 编程的力量来测试它。
因此,我们可以直接通过使用NumPy的np.linalg.inv来求解矩阵的逆,或者使用其他方法,如求解线性方程中的w,这些方程被称为正规方程:

在这里,函数np.linalg.solve可以为我们完成所有计算:
In: observations = len(dataset)
X = dataset['RM'].values.reshape((observations,1)) # X should be always a matrix, never a vector
Xb = np.column_stack((X,np.ones(observations))) # We add the bias
y = dataset['target'].values # y can be a vector
def matrix_inverse(X,y, pseudo=False):
if pseudo:
return np.dot(np.linalg.pinv(np.dot(X.T,X)),np.dot(X.T,y))
else:
return np.dot(np.linalg.inv(np.dot(X.T, X)),np.dot(X.T,y))
def normal_equations(X,y):
return np.linalg.solve(np.dot(X.T,X), np.dot(X.T,y))
print (matrix_inverse(Xb, y))
print (matrix_inverse(Xb, y, pseudo=True))
print (normal_equations(Xb, y))
Out:
[ 9.10210898 -34.67062078]
[ 9.10210898 -34.67062078]
[ 9.10210898 -34.67062078]
使用这些方法解决线性回归的唯一问题是复杂性,可能是在直接使用np.linalg.inv计算逆时,计算精度可能有所损失,以及,当然,X^TX乘积必须是可逆的(有时当使用相互之间高度相关的多个变量时,它可能不是可逆的)。
即使使用另一个算法(QR 分解,Statsmodels 的核心算法,可以克服一些之前引用的数值问题),最坏的性能可以估计为O(n³);即,立方复杂度。
使用伪逆(在 NumPy 中,np.linalg.pinv)可以帮助实现 O(n^m) 的复杂度,其中 m 估计小于 2.37(大约是二次的)。
这实际上可能是一个很大的限制,限制了快速估计线性回归分析的能力。事实上,如果你正在处理 10³ 个观测值,这在统计分析中是一个可行的观测值数量,最坏情况下需要 10⁹ 次计算;然而,当处理数据科学项目时,这些项目很容易达到 10⁶ 个观测值,找到回归问题解决方案所需的计算次数可能会激增到 10¹⁸。
梯度下降在工作
作为传统经典优化算法的替代方案,梯度下降技术能够通过远少的计算量来最小化线性回归分析的代价函数。梯度下降的复杂度以 O(np)* 的顺序排列,因此即使在出现大量 n(代表观测数)和大量 p(变量数)的情况下,学习回归系数也是可行的。
该方法通过利用一个简单的启发式方法,从随机点开始逐渐收敛到最优解。简单来说,它类似于在山中盲目行走。如果你想下降到最低的山谷,即使你不知道并且看不到路径,你也可以通过先下山一段时间,然后停下来,然后再下山,如此循环,始终在每个阶段都朝着地表下降的方向前进,直到你到达一个不能再下降的点。希望在那个点上,你将到达目的地。
在这种情况下,你唯一的风险是发生在中间的山谷(例如,那里有一片树林或一个湖泊)并错误地将其视为你期望的到达地,因为土地在那里停止下降。
在一个优化过程中,这种情况被定义为局部最小值(而你的目标是全局最小值,即可能的最小值),这是你在最小化所工作的函数过程中可能出现的可能结果。好消息是,在任何情况下,线性模型家族的误差函数都是碗状的(技术上我们的代价函数是凹的),如果你正确下降,你不太可能被困在任何地方。
在给定一组系数(向量 w)的代价函数的情况下,描述基于梯度下降的解决方案的必要步骤是很容易的:

我们首先通过选择一个随机的 w 的初始化值开始,通过选择一些随机数(例如,从标准正态曲线中取出的,具有零均值和单位方差)。
然后,我们开始重复更新 w 的值(恰当地使用梯度下降计算),直到从上一个 J(w) 的边际改进足够小,以至于我们可以确定我们已经最终达到了一个最优的最小值。
我们可以通过从每个系数中减去成本函数的偏导数的一部分 alpha(α,学习率)来适时地更新我们的系数,一个接一个:

在我们的公式中,w[j]应被视为一个单独的系数(我们正在迭代它们)。在解决偏导数之后,最终的解形式如下:

简化一切,我们的x系数的梯度只是我们的预测值的平均值乘以它们各自的x值。
Alpha,被称为学习率,在过程中非常重要,因为,如果它太大,可能会导致优化偏离并失败。你必须把每个梯度看作是一次跳跃或是在一个方向上的奔跑。如果你完全接受它,你可能会错过最优的最小值,最终落在另一个上升的斜坡上。过多的连续长步骤甚至可能迫使你爬上成本斜坡,使你的初始位置(由成本函数给出,其总和的平方是整体适应度分数的损失)变得更糟。
使用较小的 alpha 值,梯度下降不会跳过解,但它可能需要更长的时间才能达到期望的最小值。如何选择合适的 alpha 是一个试错的问题;无论如何,根据我们在许多优化问题中的经验,从一个如 0.01 的 alpha 值开始永远不是一个坏的选择。
自然地,给定相同的 alpha,随着你接近解,梯度在任何情况下都会产生更短的步骤。在图表中可视化这些步骤可以真正给你一个关于梯度下降是否在找到解的提示。
虽然在概念上相当简单(它基于一种直觉,我们肯定已经应用于逐步移动,指导我们如何优化结果),但在处理真实数据时,梯度下降非常有效,并且确实具有可扩展性。这些有趣的特征使其成为机器学习中的核心优化算法;它不仅限于线性模型家族,还可以扩展,例如,用于反向传播过程的神经网络,以最小化训练错误。令人惊讶的是,梯度下降也是另一个复杂机器学习算法的核心,即梯度提升树集成,其中我们有一个迭代过程,使用一个更简单的学习算法(所谓的弱学习器,因为它受到高偏差的限制)来进步优化。
这里是 Python 的一个初步实现。我们将在下一章中对其进行轻微修改,使其能够有效地处理多于一个预测因子:
In: observations = len(dataset)
X = dataset['RM'].values.reshape((observations,1))
# X should be always a matrix, never a vector
X = np.column_stack((X,np.ones(observations))) # We add the bias
y = dataset['target'].values # y can be a vector
现在,在定义了响应变量,选择了我们的预测变量(每栋住宅的平均房间数 RM 特征),并添加了偏差(常数 1)之后,我们就可以在下面的代码中定义我们优化过程中的所有函数了:
In: import random
def random_w( p ):
return np.array([np.random.normal() for j in range(p)])
def hypothesis(X,w):
return np.dot(X,w)
def loss(X,w,y):
return hypothesis(X,w) - y
def squared_loss(X,w,y):
return loss(X,w,y)**2
def gradient(X,w,y):
gradients = list()
n = float(len( y ))
for j in range(len(w)):
gradients.append(np.sum(loss(X,w,y) * X[:,j]) / n)
return gradients
def update(X,w,y, alpha=0.01):
return [t - alpha*g for t, g in zip(w, gradient(X,w,y))]
def optimize(X,y, alpha=0.01, eta = 10**-12, iterations = 1000):
w = random_w(X.shape[1])
path = list()
for k in range(iterations):
SSL = np.sum(squared_loss(X,w,y))
new_w = update(X,w,y, alpha=alpha)
new_SSL = np.sum(squared_loss(X,new_w,y))
w = new_w
if k>=5 and (new_SSL - SSL <= eta and \new_SSL - SSL >= -eta):
path.append(new_SSL)
return w, path
if k % (iterations / 20) == 0:
path.append(new_SSL)
return w, path
在最终定义了所有必要的函数以使梯度下降工作之后,我们可以开始优化它以解决我们的单个回归问题:
IN: alpha = 0.048
w, path = optimize(X,y,alpha, eta = 10**-12, iterations = 25000)
print ("These are our final coefficients: %s" % w)
print ("Obtained walking on this path of squared loss %s" % path)
Out: These are our final coefficients: [9.1021032698295059,\-34.670584445862119]
Obtained walking on this path of squared loss [369171.02494038735, 23714.645148620271, 22452.194702610999, 22154.055704515144, 22083.647505550518, 22067.019977742671, 22063.093237887566, 22062.165903044533, 22061.946904602359, 22061.895186155631, 22061.882972380481, 22061.880087987909, 22061.879406812728, 22061.879245947097, 22061.879207957238, 22061.879198985589, 22061.879196866852, 22061.879196366495, 22061.879196248334, 22061.879196220427, 22061.879196220034]
Scikit-learn 的 linear_regression(以及线性方法模块中存在的其他线性模型)实际上是由梯度下降驱动的,这使得 Scikit-learn 成为我们在数据科学项目中处理大型和大数据时的首选选择。
摘要
在本章中,我们介绍了线性回归作为一种监督机器学习算法。我们解释了它的函数形式,它与均值和相关性统计量之间的关系,并尝试在波士顿房价数据上构建一个简单的线性回归模型。在完成这些之后,我们最终通过提出其关键数学公式及其转换为 Python 代码来简要介绍了回归是如何在底层工作的。
在下一章中,我们将继续讨论线性回归,将我们的预测变量扩展到多个变量,并继续我们在使用单个变量进行初步说明时暂停的解释。我们还将指出你可以应用到的最有用的数据转换,使数据适合由线性回归算法处理。
第三章:实际操作中的多元回归
在上一章中,我们介绍了线性回归作为基于统计学的机器学习监督方法。这种方法通过预测因子的组合来预测数值,这些预测因子可以是连续的数值或二元变量,假设我们手头的数据显示与目标变量之间存在某种关系(一种线性关系,可以通过相关性来衡量)。为了顺利引入许多概念并轻松解释该方法的工作原理,我们将示例模型限制在只有一个预测变量,将建模响应的所有负担都留给了它。
然而,在实际应用中,可能存在一些非常重要的原因决定着你想要建模的事件,但确实很少有一个变量能够单独登台并构建一个有效的预测模型。世界是复杂的(并且确实在原因和效果中相互关联),通常不考虑各种原因、影响因素和线索,很难轻易解释。通常需要更多的变量协同工作,才能从预测中获得更好和可靠的结果。
这种直觉决定性地影响了我们模型的复杂性,从这一点起,我们的模型将不再容易在二维图上表示。给定多个预测因子,每个预测因子都将构成其自身的维度,我们必须考虑我们的预测因子不仅与响应相关,而且彼此之间(有时非常严格)相关,这是数据的一个特征,称为多重共线性。
在开始之前,我们想就我们将要处理的主题选择说几句话。尽管在统计文献中有很多关于回归假设和诊断的出版物和书籍,但你在这里几乎找不到任何东西,因为我们将省略这些主题。我们将限制自己讨论可能影响回归模型结果的问题和方面,基于实际的数据科学方法,而不是纯粹统计的方法。
在这样的前提下,在本章中,我们将要:
-
将单变量回归的程序扩展到多元回归,同时关注可能的问题来源,如多重共线性
-
理解你线性模型方程中每个术语的重要性
-
让你的变量协同工作,通过变量间的交互作用提高你的预测能力
-
利用多项式展开来提高你的线性模型与非线性函数的拟合度
使用多个特征
为了回顾上一章中看到的工具,我们重新加载所有包和波士顿数据集:
In: import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
from sklearn.datasets import load_boston
from sklearn import linear_model
如果你正在 IPython 笔记本中编写代码(我们强烈建议这样做),以下魔法命令将允许你直接在界面上可视化图表:
In: %matplotlib inline
我们仍在使用波士顿数据集,这是一个试图解释 70 年代波士顿不同房价的数据集,给定一系列在人口普查区层面的汇总统计数据:
In: boston = load_boston()
dataset = pd.DataFrame(boston.data, columns=boston.feature_names)
dataset['target'] = boston.target
我们将始终通过保留一系列信息变量来工作,包括观测数量、变量名称、输入数据矩阵和手头的响应向量:
In: observations = len(dataset)
variables = dataset.columns[:-1]
X = dataset.ix[:,:-1]
y = dataset['target'].values
使用 Statsmodels 构建模型
作为将 Statsmodels 中之前所做的分析扩展到更多预测变量的第一步,让我们重新加载包中必要的模块(一个用于矩阵,另一个用于公式):
In: import statsmodels.api as sm
import statsmodels.formula.api as smf
让我们也准备一个合适的输入矩阵,将其命名为Xc,在增加一个包含偏置向量的额外列之后(这是一个具有单位值的常量变量):
In: Xc = sm.add_constant(X)
linear_regression = sm.OLS(y,Xc)
fitted_model = linear_regression.fit()
在拟合了前面的指定模型后,让我们立即请求一个摘要:
In: fitted_model.summary()
Out:



基本上,如前一章所述的各种统计度量陈述仍然有效。我们只是用几句话来强调一些我们之前无法提及的额外功能,因为它们与多个预测变量的存在有关。
首先,现在需要注意调整后的 R 平方。当与多个变量一起工作时,标准 R 平方会因为模型中插入的许多系数而膨胀。如果你使用太多的预测变量,其度量将明显偏离普通 R 平方。调整后的 R 平方考虑了模型的复杂性,并报告了一个更加现实的 R 平方度量。
小贴士
只需在普通 R 平方和调整后的 R 平方度量之间做一个比率。检查它们的差异是否超过 20%。如果超过,这意味着我们在模型规范中引入了一些冗余变量。自然地,比率差异越大,问题越严重。
在我们的例子中并非如此,因为差异相当小,大约在 0.741 和 0.734 之间,转换成比率得出0.741/0.734 = 1.01,即只是标准 R 平方的 1%以上。
然后,一次处理这么多变量时,也应检查系数的重要警告。涉及的风险是系数获取了嘈杂且无价值的信息。通常这样的系数不会远离零,并且会因为它们的大标准误差而明显。统计 t 检验是发现它们的正确工具。
小贴士
注意,具有低 p 值的变量是模型中移除的好候选,因为可能几乎没有证据表明它们的估计系数与零不同。
在我们的例子中,由于大部分不显著(p 值主要大于 0.05),AGE和INDUS变量在模型中由系数表示,其有用性可能受到严重质疑。
最后,条件数测试(Cond. No.)是另一个之前提到的统计量,现在在预测因子系统下获得了新的重要性。当尝试基于矩阵求逆进行优化时,它表示数值不稳定的结果。这种不稳定性的原因是多重共线性,我们将在接下来的段落中对其进行扩展。
提示
当条件数超过 30 的分数时,有一个明确的信号表明不稳定的结果正在使结果变得不那么可靠。预测可能会受到错误的影响,当重新运行相同的回归分析时,系数可能会发生剧烈变化,无论是使用子集还是不同的观察数据集。
在我们的情况下,条件数远远超过 30,这是一个严重的警告信号。
使用公式作为替代
要使用statsmodels.formula.api获得相同的结果,从而解释一个由 Patsy 包(patsy.readthedocs.org/en/latest/)解释的公式,我们使用:
linear_regression = smf.ols(formula = 'target ~ CRIM + ZN +INDUS + CHAS + NOX + RM + AGE + DIS + RAD + TAX + PTRATIO + B + LSTAT', data=dataset)
fitted_model = linear_regression.fit()
在这种情况下,你必须通过在公式的右侧命名它们来明确所有变量以进入模型构建。在拟合模型后,你可以使用之前看到的 Statsmodels 方法来报告系数和结果。
相关矩阵
当尝试使用单个预测因子来建模响应时,我们使用了皮尔逊相关(皮尔逊是其发明者的名字)来估计预测因子和目标之间的线性关联系数。现在分析中变量更多,我们仍然非常感兴趣的是每个预测因子如何与响应相关;然而,我们必须区分预测因子的方差和目标方差之间的关联是由于独特方差还是相互方差。
由于独特方差引起的关联测量被称为偏相关,它表达了由于变量中独特存在的信息所能推测出的响应。它代表了变量在预测响应中的独特贡献,其作为直接原因对目标的影响(如果你可以将其视为原因的话,因为,如所见,相关性并不等同于因果关系)。
相互方差则是同时存在于变量和手头数据集中其他变量中的信息量。相互方差可能有多种原因;可能是一个变量引起或只是干扰了另一个(正如我们在上一章的相关性不等于因果关系部分所描述的)。相互方差,也称为共线性(两个变量之间)或多重共线性(三个或更多变量之间),具有重要作用,对经典统计方法来说是个令人担忧的问题,而对数据科学方法来说则不那么令人畏惧。
对于统计方法,必须说,高度或几乎完美的多重共线性不仅经常使系数估计变得不可能(矩阵求逆不起作用),而且,即使可行,它也会受到系数估计不准确的影响,导致系数的标准误差很大。然而,预测不会受到影响,这使我们转向数据科学观点。
实际上,存在多重共线性变量会使选择分析中正确的变量变得困难(由于方差是共享的,很难确定哪个变量是其因果来源),导致次优解,这些解只能通过增加分析中涉及的观测数量来解决。
要确定影响彼此的预测变量的方式和数量,合适的工具是相关矩阵,尽管当特征数量较多时阅读起来有些困难,但它仍然是确定共享方差存在最直接的方式:
X = dataset.ix[:,:-1]
correlation_matrix = X.corr()
print (correlation_matrix)
这将给出以下输出:

初看之下,一些高相关性似乎出现在TAX、NOX、INDUS和DIS之间的绝对值 0.70 的顺序(在矩阵中用手标出)。这是可以解释的,因为DIS是就业中心的距离,NOX是污染指标,INDUS是该地区非住宅或商业建筑的比例,而TAX是财产税率。这些变量的正确组合可以很好地暗示生产区域。
一种更快但数值表示较少的方法是构建相关性的热图:
In:
def visualize_correlation_matrix(data, hurdle = 0.0):
R = np.corrcoef(data, rowvar=0)
R[np.where(np.abs(R)<hurdle)] = 0.0
heatmap = plt.pcolor(R, cmap=mpl.cm.coolwarm, alpha=0.8)
heatmap.axes.set_frame_on(False)
heatmap.axes.set_yticks(np.arange(R.shape[0]) + 0.5, minor=False)
heatmap.axes.set_xticks(np.arange(R.shape[1]) + 0.5, minor=False)
heatmap.axes.set_xticklabels(variables, minor=False)
plt.xticks(rotation=90)
heatmap.axes.set_yticklabels(variables, minor=False)
plt.tick_params(axis='both', which='both', bottom='off', \top='off', left = 'off', right = 'off')
plt.colorbar()
plt.show()
visualize_correlation_matrix(X, hurdle=0.5)
这将给出以下输出:

当相关系数达到 0.5(这相当于 25%的共享方差)时,热图立即揭示PTRATIO和B与其他预测变量之间关系不大。作为变量意义的提醒,B是一个量化该地区有色人种比例的指标,PTRATIO是该地区学校的师生比例。地图提供的另一个直觉是,一组变量,即TAX、INDUS、NOX和RAD,被证实存在强烈的线性关联。
一种更自动化的方式来检测此类关联(并解决矩阵求逆中的数值问题)是使用特征向量。用通俗易懂的话来说,特征向量是一种非常聪明的重新组合变量之间方差的方法,创建新的特征,累积所有共享的方差。这种重新组合可以通过使用 NumPy 的linalg.eig函数实现,从而得到特征值向量(代表每个新变量的重新组合方差量)和特征向量(一个矩阵,告诉我们新变量与旧变量之间的关系):
In: corr = np.corrcoef(X, rowvar=0)
eigenvalues, eigenvectors = np.linalg.eig(corr)
在提取特征值后,我们按降序打印它们,并寻找任何值接近零或与其他值相比较小的元素。接近零的值可能代表正常方程和其他基于矩阵求逆的优化方法的一个真正问题。较小的值代表一个高但非关键的多重共线性来源。如果你发现任何这些低值,请记住它们在列表中的索引(Python 索引从零开始)。
In: print (eigenvalues)
Out: [ 6.12265476 1.43206335 1.24116299 0.85779892 0.83456618 0.65965056 0.53901749 0.39654415 0.06351553 0.27743495 0.16916744 0.18616388 0.22025981]
通过它们在特征值列表中的索引位置,你可以从特征向量中召回它们的特定向量,该向量包含所有变量载荷——即与原始变量的关联程度。在我们的例子中,我们研究了索引为8的特征向量。在特征向量内部,我们注意到索引位置2、8和9的值,这些值在绝对值方面确实非常突出:
In: print (eigenvectors[:,8])
Out: [-0.04552843 0.08089873 0.25126664 -0.03590431 -0.04389033 -0.04580522 0.03870705 0.01828389 0.63337285 -0.72024335 -0.02350903 0.00485021 -0.02477196]
现在,我们打印变量名称,以了解哪些变量通过它们的值对构建特征向量做出了如此大的贡献:
In: print (variables[2], variables[8], variables[9])
Out: INDUS RAD TAX
找到多重共线性问题后,我们能为这样的变量采取什么补救措施?通常,移除其中一些是最佳解决方案,这将在探索第六章中变量选择如何工作的方式时自动执行,实现泛化。
重新审视梯度下降法
与上一章的内容相连续,我们继续用梯度下降法进行解释和实验。因为我们已经定义了数学公式及其用矩阵表示的 Python 代码翻译,所以我们现在不需要担心是否需要同时处理多个变量。使用矩阵表示法使我们能够轻松地将先前的介绍和例子扩展到多个预测变量,只需对算法进行一些小的修改。
特别要注意的是,通过在优化过程中引入更多需要估计的参数,我们实际上是在我们的拟合线上引入了更多维度(将其变成一个超平面,一个多维表面),这些维度具有一定的共性和差异需要考虑。
特征缩放
在估计系数时,由于特征之间的相似性可能导致估计值的方差增加,正如我们最初讨论的那样,处理不同的特征需要更多的注意。变量之间的多重共线性也有其他缺点,因为它也可能使矩阵求逆(正常方程系数估计的核心矩阵运算)变得非常困难,甚至不可能实现;这样的问题是由于算法的数学限制造成的。相反,梯度下降法根本不受相互相关性影响,即使在完全共线性的情况下,我们也能估计出可靠的系数。
无论如何,尽管它对影响其他方法的常见问题具有相当大的抵抗力,但其简单性也使其容易受到其他常见问题的攻击,例如每个特征中存在的不同尺度。事实上,你的数据中的一些特征可能表示的是以单位为单位的测量值,一些是十进制,还有一些是千位数,这取决于每个特征代表现实世界的哪个方面。在我们的房地产例子中,一个特征可能是房间数量,另一个可能是空气中某些污染物的百分比,最后是邻居中房屋的平均价值。当特征具有不同的尺度时,尽管算法将分别处理每个特征,但优化将由尺度更大的变量主导。在一个不同维度的空间中工作将需要更多的迭代才能收敛到解决方案(有时甚至可能根本无法收敛)。
解决方法非常简单;只需将所有特征置于相同的尺度上即可。这种操作称为特征缩放。特征缩放可以通过标准化或归一化实现。归一化将所有值缩放到零和一之间的区间(通常如此,但也可能使用不同的范围),而标准化则是通过减去均值并除以标准差来获得单位方差。在我们的例子中,标准化更可取,因为它可以轻松地将获得的标准化系数重新调整到原始尺度,并且通过将所有特征中心化到零均值,它使得错误表面对于许多机器学习算法来说更容易处理,比仅仅缩放变量的最大值和最小值要有效得多。
在应用特征缩放时,一个重要的提醒是,改变特征的范围意味着你将不得不使用缩放后的特征来进行预测,除非你能够重新计算系数,就像变量从未被缩放一样。
让我们尝试这个算法,首先使用基于 Scikit-learn preprocessing模块的标准规范化:
In: from sklearn.preprocessing import StandardScaler
observations = len(dataset)
variables = dataset.columns
standardization = StandardScaler()
Xst = standardization.fit_transform(X)
original_means = standardization.mean_
originanal_stds = standardization.std_
Xst = np.column_stack((Xst,np.ones(observations)))
y = dataset['target'].values
在前面的代码中,我们只是使用了 Scikit-learn 的StandardScaler类来标准化变量。这个类可以拟合一个数据矩阵,记录其列均值和标准差,并对自身以及任何其他类似矩阵进行转换,标准化列数据。使用这种方法,在拟合后,我们保留均值和标准差,因为它们将在我们稍后需要使用原始尺度重新计算系数时派上用场:
In: import random
def random_w( p ):
return np.array([np.random.normal() for j in range(p)])
def hypothesis(X,w):
return np.dot(X,w)
def loss(X,w,y):
return hypothesis(X,w) - y
def squared_loss(X,w,y):
return loss(X,w,y)**2
def gradient(X,w,y):
gradients = list()
n = float(len( y ))
for j in range(len(w)):
gradients.append(np.sum(loss(X,w,y) * X[:,j]) / n)
return gradients
def update(X,w,y, alpha=0.01):
return [t - alpha*g for t, g in zip(w, gradient(X,w,y))]
def optimize(X,y, alpha=0.01, eta = 10**-12, iterations = 1000):
w = random_w(X.shape[1])
path = list()
for k in range(iterations):
SSL = np.sum(squared_loss(X,w,y))
new_w = update(X,w,y, alpha=alpha)
new_SSL = np.sum(squared_loss(X,new_w,y))
w = new_w
if k>=5 and (new_SSL - SSL <= eta and \new_SSL - SSL >= -eta):
path.append(new_SSL)
return w, path
if k % (iterations / 20) == 0:
path.append(new_SSL)
return w, path
alpha = 0.02
w, path = optimize(Xst, y, alpha, eta = 10**-12, \iterations = 20000)
print ("These are our final standardized coefficients: " + ', \'.join(map(lambda x: "%0.4f" % x, w)))
Out: These are our final standardized coefficients: -0.9204, 1.0810, 0.1430, 0.6822, -2.0601, 2.6706, 0.0211, -3.1044, 2.6588, -2.0759, -2.0622, 0.8566, -3.7487, 22.5328
我们正在使用的代码与上一章中使用的代码没有太大区别,除了它的输入现在由多个标准化变量组成。在这种情况下,算法在更少的迭代次数中达到收敛,并且使用了比之前更小的 alpha 值,因为在我们之前的例子中,我们的单个变量实际上是未标准化的。观察输出,我们现在需要一种方法来重新缩放系数以适应变量的特征,我们就能以非标准化的形式报告梯度下降的解。
另一点需要提及的是我们选择 alpha 的方法。经过一些测试,我们选择了0.02这个值,因为它在这个非常具体的问题上表现良好。Alpha 是学习率,在优化过程中它可以固定或可变,根据线搜索方法,通过修改其值以尽可能最小化优化过程中的每个单独步骤的成本函数。在我们的例子中,我们选择了固定的学习率,并且我们必须通过尝试几个优化值来确定哪个在更少的迭代次数中最小化了成本。
非标准化系数
给定一个线性回归的标准化系数向量及其偏差,我们可以回忆线性回归的公式,它是:

之前的公式,使用变量的均值和标准差转换预测变量,实际上(经过一些计算)等同于以下表达式:

在我们的公式中,
代表变量的原始均值,而δ代表原始方差。
通过比较两个公式(第一个括号和第二个求和项)的不同部分,我们可以计算出将标准化系数转换为非标准化系数时的偏差和系数等价值。不重复所有数学公式,我们可以快速将它们实现为 Python 代码,并立即提供一个应用示例,展示这些计算如何转换梯度下降系数:
In: unstandardized_betas = w[:-1] / originanal_stds
unstandardized_bias = w[-1]-np.sum((original_means /originanal_stds) * w[:-1])
print ('%8s: %8.4f' % ('bias', unstandardized_bias))
for beta,varname in zip(unstandardized_betas, variables):
print ('%8s: %8.4f' % (varname, beta))
Out:

作为前一个代码片段的输出,你将得到一个与我们的先前估计相同的系数列表,无论是使用 Scikit-learn 还是 Statsmodels。
估计特征重要性
在确认了我们构建的线性模型的系数值,并探索了理解模型是否正确工作所需的基本统计信息之后,我们可以开始审计我们的工作,首先了解预测是如何构成的。我们可以通过考虑每个变量在构成预测值中的作用来获得这一点。对系数进行的第一次检查无疑是检查它们所表达的方向性,这仅仅由它们的符号决定。基于我们对主题的专长(因此,建议我们对正在工作的领域有所了解),我们可以检查所有系数是否都符合我们对方向性的预期。一些特征可能以我们预期的方式降低响应,从而正确地确认它们具有负号系数,而其他特征可能增加响应,因此正系数应该是正确的。当系数不符合我们的预期时,我们就有反转。反转并不罕见,实际上它们可以揭示事物以与我们预期不同的方式运作。然而,如果我们的预测变量之间存在大量多重共线性,反转可能只是由于估计的不确定性较高:一些估计可能如此不确定,以至于优化过程分配给它们错误的符号。因此,当线性回归不符合我们的预期时,最好不要急于得出任何结论;相反,仔细检查所有指向多重共线性的统计指标。
对变量对模型的影响进行第二次检查——也就是说,预测结果中有多少是由特征的变化所主导的。通常情况下,如果影响较小,由变量(例如,可能来自不可靠的来源,或者非常嘈杂——也就是说,测量不准确)引起的变化和反转以及其他困难对预测的破坏性较小,甚至可以忽略不计。
引入影响的概念也提出了使我们的模型在模型系数数量方面经济化的可能性。到目前为止,我们只是专注于这样一个观点,即尽可能好地拟合数据是可取的,我们没有检查我们的预测公式是否很好地推广到新数据。开始对预测变量进行排序可以帮助我们构建新的更简单的模型(只需选择模型中最重要的特征)并且简单的模型在生产阶段更不容易出错。
事实上,如果我们的目标不仅仅是用公式最大限度地拟合我们的现有数据,而且还要很好地拟合未来的数据,那么有必要应用奥卡姆剃刀的原则。这表明,在给出更多正确答案的情况下,简单的模型总是比更复杂的模型更可取。核心思想不是使解释(即线性模型)比应有的更复杂,因为复杂性可能隐藏过拟合。
检查标准化系数
将我们对线性模型的解释从单个变量扩展到大量变量,我们仍然可以将每个单个系数视为每个预测变量对响应变量单位变化的诱导(保持其他预测变量不变)。
直观上看,较大的系数似乎对线性组合的结果影响更大;然而,正如我们在回顾梯度下降时注意到的,不同的变量可能具有不同的尺度,它们的系数可能包含这一点。系数较小或较大可能只是因为变量相对于分析中涉及的其他特征的相对尺度。
通过标准化变量,我们将它们置于一个相似的尺度上,其中单位是变量的标准差。从高值到低值(范围较大)的变量往往具有更大的标准差,你应该预期它们会被缩小。通过这样做,大多数呈正态分布的变量应该会在范围-3 < x < +3内缩小,从而允许比较它们对预测的贡献。高度偏斜的分布不会在范围-3 <x <+3内进行标准化。这种转换无论如何都是有益的,因为它们的范围将被大大缩小,之后甚至可以比较不同的分布,因为那时它们都将代表相同的单位度量——即单位方差。标准化后,较大的系数可以解释为对建立结果(加权求和,因此结果将更接近较大的权重)的主要贡献。因此,我们可以自信地对我们变量进行排序,并找出那些贡献较小的变量。
让我们用一个例子来继续,这次我们将使用波士顿数据集。这次我们将使用 Scikit-learn 的LinearRegression方法,因为我们不需要对其统计属性进行线性模型,而只需要一个使用快速且可扩展算法的工作模型:
In: linear_regression = linear_model.LinearRegression(normalize=False, fit_intercept=True)
在这样的初始化中,除了fit_intercept参数明确指出将偏差插入模型设计之外,normalize选项表示我们是否打算将所有变量重新缩放到零和一之间。这种转换与统计标准化不同,我们暂时将其设置为False状态以省略它:
In: from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
standardization = StandardScaler()
Stand_coef_linear_reg = make_pipeline(standardization,\linear_regression)
除了之前看到的StandardScaler之外,我们还从 Scikit-learn 导入方便的make_pipeline包装器,它允许我们在将数据馈送到线性回归分析之前自动执行一系列操作。现在,Stand_coef_linear_reg管道将在回归之前对数据进行统计标准化,从而输出标准化系数:
In: linear_regression.fit(X,y)
for coef, var in sorted(zip(map(abs,linear_regression.coef_), \
dataset.columns[:-1]), reverse=True):
print ("%6.3f %s" % (coef,var))
Out:

作为第一步,我们输出未标准化数据上的回归系数。如前所述,输出似乎主要由 NOX 变量的巨大系数主导,其绝对值约为 17.8,而忽略了 3.8 和更小的较小系数。然而,我们可能会质疑在标准化变量后是否仍然如此:
In: Stand_coef_linear_reg.fit(X,y)
for coef, var in \
sorted(zip(map(abs,Stand_coef_linear_reg.steps[1][1].coef_), \
dataset.columns[:-1]), reverse=True):
print ("%6.3f %s" % (coef,var))
Out:

现在所有预测变量都在相似尺度上,我们可以轻松地提供每个系数的更现实解释。显然,涉及变量LSTAT、DIS、RM、RAD和TAX的单位变化似乎有更大的影响。LSTAT是低地位人口的比例,这一方面解释了其相关性。
使用标准化尺度无疑指出了最重要的变量,但它仍然不是每个变量预测能力的完整概述,因为:
-
标准化系数代表模型预测效果的好坏,因为大的系数对结果响应有重大影响:尽管拥有大的系数确实是一个变量重要性的提示,但它只告诉我们变量在减少估计误差和使我们的预测更准确中所起的部分作用。
-
标准化系数可以排序,但它们的单位,尽管在尺度上相似,但某种程度上是抽象的(每个变量的标准差)并且相对于手头的数据(因此我们不应该比较不同数据集的标准化系数,因为它们的标准差可能不同)
一种解决方案可能是将基于标准化系数的重要性估计整合,而不是使用与误差度量相关的某些度量。
通过 R-squared 比较模型
从一般的角度来看,我们可以通过比较模型在简单均值方面的表现来评估一个模型,这就是确定系数,R-squared。
R-squared 可以估计模型的好坏;因此,通过比较我们的模型与变量已被移除的替代模型的 R-squared,我们可以了解每个移除变量的预测能力。我们只需计算初始模型与没有该变量的模型的确定系数之间的差异。如果差异很大,该变量在确定更好的 R-squared 和更好的模型中起着非常重要的作用。
在我们的案例中,我们首先需要记录当我们用所有变量构建模型时的 R-squared 值。我们可以将这样的值命名为我们的比较基准:
In: from sklearn.metrics import r2_score
linear_regression = linear_model.LinearRegression(normalize=False,\fit_intercept=True)
def r2_est(X,y):
return r2_score(y,linear_regression.fit(X,y).predict(X))
print ('Baseline R2: %0.3f' % r2_est(X,y))
Out:Baseline R2: 0.741
之后,我们只需从预测变量集中一次移除一个变量,重新估计回归模型并记录其确定系数,然后从完整回归模型得到的基准值中减去:
In: r2_impact = list()
for j in range(X.shape[1]):
selection = [i for i in range(X.shape[1]) if i!=j]
r2_impact.append(((r2_est(X,y) - \r2_est(X.values [:,selection],y)) ,dataset.columns[j]))
for imp, varname in sorted(r2_impact, reverse=True):
print ('%6.3f %s' % (imp, varname))
Out:

在我们得到所有差异之后,每个差异代表每个变量对 R 平方的贡献,我们只需对它们进行排序,然后我们就会对哪些变量在减少线性模型误差方面贡献更多有一个概念;这与知道哪个变量对响应值贡献最大是不同的观点。这种贡献被称为部分 R 平方。
除了建议我们使用这两种度量(标准化系数和部分 R 平方)来区分相关变量和不相关变量之外,通过使用部分 R 平方,你实际上可以直接比较变量的重要性,因为使用比率在这里是有意义的(因此,你可以知道一个变量的重要性是另一个变量的两倍,因为它减少了两倍多的误差)。
另一个值得注意的点是,部分 R 平方并不是初始 R 平方测量的真正分解。实际上,只有当预测变量通过求和所有部分得分相互不相关时,你才能得到完整模型的精确确定系数。这是由于变量之间的共线性造成的。因此,当你从模型中移除一个变量时,你肯定不会移除其所有信息方差,因为包含与移除变量相似信息的相关变量仍然保留在模型中。可能发生的情况是,如果你有两个高度相关的变量,依次移除它们并不会显著改变 R 平方,因为当一个变量被移除时,另一个变量将提供缺失的信息(因此,与标准化系数的双检查并不是多余的)。
有更多复杂的方法可以用来估计变量重要性,但这两种方法应该能为你提供足够的洞察力。了解哪些变量对结果影响更大,可以为你提供以下手段:
-
尽量以合理且易于理解的方式向管理层解释结果。
-
首先根据项目成功的关键性,优先处理数据清洗、准备和转换工作,集中精力关注与项目成功最相关的特征。
-
在构建和使用回归模型时,尽量节约资源,特别是内存,因为使用更少的数据可以减少资源消耗。
如果你希望使用重要性来排除不相关变量,并使用我们提出的一种度量,最安全的方法是在每次决定从集合中排除一个变量时重新计算排名。否则,使用单一的排名可能会隐藏高度相关变量的真正重要性(这对于两种方法都是真的,尽管标准化系数稍微更有揭示性)。
这种方法当然很耗时,但当你注意到你的模型虽然在你当前的数据上呈现良好的拟合,但不能很好地推广到新的观测值时,这是必要的。
我们将在第六章“实现泛化”中更深入地讨论这种情形,届时我们将说明减少预测变量集的最佳方法,保持(简化解决方案)甚至提高预测性能(更有效地泛化)。
交互模型
解释了如何构建多变量回归模型,并触及了其利用和解释的主题后,我们从这一段开始探讨如何改进它。作为第一步,我们将从当前数据对它的拟合工作开始。在接下来的章节中,我们将专注于模型选择和验证,我们将集中讨论如何使其真正具有普遍性——也就是说,能够正确预测新的、之前未见过的数据。
如我们之前推理的那样,线性回归中的 beta 系数代表了预测变量单位变化与响应变量变化之间的联系。此类模型的核心假设是每个预测变量与目标变量之间存在着恒定且单向的关系。这是线性关系假设,具有线的特征,其中方向和波动由角度系数(因此得名线性回归,暗示着回归操作,追溯到从某些数据证据中得出的线性形式)。尽管这是一个很好的近似,但线性关系通常是简化的,在真实数据中往往并不成立。事实上,大多数关系都是非线性的,显示出弯曲、曲线以及增加和减少中的不同波动。
好消息是,我们不必局限于最初提供的特征,但我们可以修改它们,以便拉直它们与目标变量之间的关系。事实上,预测变量与响应变量之间的相似性越大,拟合度越好,训练集中的预测误差就越少。
因此,我们可以这样说:
-
我们可以通过各种方式变换预测变量来改进我们的线性回归。
-
我们可以使用部分 R 平方来衡量这种改进,因为每一次变换都应该影响残差误差的数量,并最终影响确定系数。
发现交互作用
非线性的第一个来源是由于预测变量之间可能存在的交互作用。当其中一个预测变量对响应变量的影响随着另一个预测变量的值而变化时,这两个预测变量就发生了交互。
在数学公式中,交互项(交互变量)必须乘以自身,以便我们的线性模型能够捕捉到它们关系的补充信息,正如以下具有两个交互预测变量的模型示例所示:

在回归模型中,一个易于记忆的交互示例是发动机噪音在评估汽车中的作用。如果你要建模汽车型号的偏好,你会立即注意到发动机噪音可能会增加或减少消费者对汽车的偏好,这取决于汽车的价格(或其类别,这是货币价值的代理)。事实上,如果汽车价格便宜,安静显然是必不可少的,但如果汽车价格昂贵(如法拉利或其他跑车)噪音则是一个突出的好处(显然当你坐在车里时,你希望每个人都注意到你开的酷车)。
处理交互可能听起来有点棘手,但实际上并不难;毕竟,你只是在基于另一个变量转换线性回归中的变量角色。找到交互项可以通过两种不同的方式实现,第一种是领域知识——也就是说,直接了解你正在建模的问题,并在其中融入你的专业知识。当你没有这样的专业知识时,如果使用如 R-squared 这样的揭示性度量进行良好测试,自动搜索所有可能的组合就足够了。
最好的方法是通过一个 Python 示例来展示自动搜索方法,使用 Scikit-learn 中的PolynomialFeatures函数,这个函数允许交互和多项式展开(我们将在下一段中讨论它们):
In: from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import r2_score
linear_regression = linear_model.LinearRegression(normalize=False,\fit_intercept=True)
create_interactions = PolynomialFeatures(degree=2, \interaction_only=True, include_bias=False)
通过degree参数,我们定义了要放入交互中的变量数量,可能有三变量甚至更多变量相互交互。在统计学中,交互被称为双向效应、三向效应等,这取决于涉及的变量数量(而原始变量则被称为主要效应)。
In: def r2_est(X,y):
return r2_score(y,linear_regression.fit(X,y).predict(X))
baseline = r2_est(X,y)
print ('Baseline R2: %0.3f' % baseline)
Out: Baseline R2: 0.741
in: Xi = create_interactions.fit_transform(X)
main_effects = create_interactions.n_input_features_
在回忆起基线 R-squared 值之后,代码使用fit_transform方法创建一个新的输入数据矩阵,通过所有变量的交互效应丰富了原始数据。此时,我们创建了一系列新的线性回归模型,每个模型包含所有主要效应和一个单独的交互效应。我们测量改进,计算与基线的差异,然后仅报告超过一定阈值的交互效应。我们可以决定一个略高于零的阈值,或者基于统计测试确定的阈值。在我们的例子中,我们决定了一个任意阈值,目的是报告所有超过0.01的 R-squared 增量:
In: for k,effect in \ enumerate(create_interactions.powers_[(main_effects):]):
termA, termB = variables[effect==1]
increment = r2_est(Xi[:,list(range(0,main_effects)) \+[main_effects+k]],y) - baseline
if increment > 0.01:
print ('Adding interaction %8s *%8s R2: %5.3f' % \
(termA, termB, increment))
Out:

相关的交互效应显然是由变量'RM'(如之前所见,是最重要的变量之一)及其与另一个关键特征LSTAT的交互产生的。一个重要的启示是,我们将它添加到我们的原始数据矩阵中,作为两个变量之间的简单乘积:
In: Xi = X
Xi['interaction'] = X['RM']*X['LSTAT']
print ('R2 of a model with RM*LSTAT interaction: %0.3f' % \r2_est(Xi,y))
Out: R2 of a model with RM*LSTAT interaction: 0.805
多项式回归
作为交互作用的扩展,多项式展开系统地提供了一种自动创建交互作用和原始变量的非线性幂变换的方法。幂变换是线在拟合响应时可以采取的弯曲。幂的次数越高,可用的弯曲就越多,从而更好地拟合曲线。
例如,如果你有一个简单线性回归的形式:

通过二次变换,称为二次,你将得到一个新的形式:

通过三次变换,称为三次,你的方程将变为:

如果你的回归是多元的,展开将创建额外的项(交互作用),从而增加由展开产生的新特征的数量。例如,由两个预测因子(x[1] 和 x[2])组成的多元回归,使用二次变换展开,将变为:

在继续之前,我们必须注意展开过程的两点:
-
多项式展开会迅速增加预测因子的数量
-
高次多项式转化为预测因子的高次幂,这会对数值稳定性造成问题,因此需要合适的数值格式或标准化过大的数值
测试线性与三次变换
通过在之前提到的 PolynomialFeatures 函数中将 interaction_only 参数关闭,我们可以得到输入矩阵的完整多项式变换,而不仅仅是之前简单的交互作用:
In:
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
linear_regression = linear_model.LinearRegression(normalize=False, \fit_intercept=True)
create_cubic = PolynomialFeatures(degree=3, interaction_only=False, \include_bias=False)
create_quadratic = PolynomialFeatures(degree=2, interaction_only=False, \
include_bias=False)
linear_predictor = make_pipeline(linear_regression)
quadratic_predictor = make_pipeline(create_quadratic, \
linear_regression)
cubic_predictor = make_pipeline(create_cubic, linear_regression)
通过将 PolynomialFeatures 和 LinearRegression 同时放入管道中,我们可以通过单个命令自动创建一个函数,展开数据并对其进行回归。作为一个实验,我们尝试单独对 'LSTAT' 变量进行建模,以获得最佳清晰度,记住我们本来可以一次性展开所有变量:
predictor = 'LSTAT'
x = dataset['LSTAT'].values.reshape((observations,1))
xt = np.arange(0,50,0.1).reshape((50/0.1,1))
x_range = [dataset[predictor].min(),dataset[predictor].max()]
y_range = [dataset['target'].min(),dataset['target'].max()]
scatter = dataset.plot(kind='scatter', x=predictor, y='target', \xlim=x_range, ylim=y_range)
regr_line = scatter.plot(xt, linear_predictor.fit(x,y).predict(xt), \'-', color='red', linewidth=2)

我们第一次拟合是线性的(简单的线性回归),从散点图中我们可以注意到,这条线并没有很好地代表与 'LSTAT' 相关的点云;很可能是我们需要一个曲线。我们立即尝试三次变换:使用两个弯曲,我们应该能得到更好的拟合:
scatter = dataset.plot(kind='scatter', x=predictor, y='target', \xlim=x_range, ylim=y_range)
regr_line = scatter.plot(xt, cubic_predictor.fit(x,y).predict(xt), \'-', color='red', linewidth=2)

我们的图形检查确认,现在我们对 'LSTAT' 和响应之间的关系有了更可信的表示。我们质疑是否不能通过使用更高次的变换做得更好。
寻求更高次解
为了测试更高次的多项式变换,我们准备了一个脚本,该脚本创建展开并报告其 R 平方度量。然后我们尝试绘制系列中最高次的函数,并查看它如何拟合数据点:
In: for d in [1,2,3,5,15]:
create_poly = PolynomialFeatures(degree=d,\interaction_only=False, include_bias=False)
poly = make_pipeline(create_poly, StandardScaler(),\linear_regression)
model = poly.fit(x,y)
print ("R2 degree - %2i polynomial :%0.3f" \%(d,r2_score(y,model.predict(x))))
Out:

显然,线性模型和二次展开(二次多项式)之间的决定系数有很大的差异。这个指标从0.544跳到0.641,差异增加到达到五次方时为0.682。在更高的次数之前,增量并不那么令人惊讶,尽管它还在增长,当次数达到第十五时,达到0.695。作为后者在决定系数方面是最好的结果,查看数据云的图将揭示一个不太平滑的拟合,正如我们可以从较低次数的多项式展开中看到的那样:
In: scatter = dataset.plot(kind='scatter', x=predictor,\y='target', xlim=x_range, ylim=y_range)
regr_line = scatter.plot(xt, model.predict(xt), '-',\color='red', linewidth=2)

仔细观察得到的曲线,你一定会注意到,通过如此高的次数,曲线严格遵循点的分布,当预测值的范围边缘密度降低时,曲线变得不规则。
介绍欠拟合和过拟合
多项式回归为我们提供了一个开始讨论模型复杂性的合适机会。我们没有明确测试它,但你可能已经感觉到,通过增加多项式展开的次数,你将始终获得更好的拟合。我们更进一步说:你模型中的变量越多,越好,直到你将有如此多的 beta 系数,可能等于或几乎等于你的观察数量,以至于你的预测将是完美的。
由于过参数化(模型需要学习的参数过多)导致的性能下降是线性回归以及许多其他机器学习算法的问题。你添加的参数越多,拟合效果越好,因为模型将不再拦截你数据中的规则和规律,而是在这种丰富的尴尬中,开始用数据中所有不规则和错误的信息填充可用的许多系数。在这种情况下,模型不会学习一般规则,而只是以另一种形式记住数据集本身。
这被称为过拟合:对现有数据进行如此完美的拟合,以至于结果远非从数据形式中提取以进行预测;结果只是简单的记忆。另一方面,另一个问题是欠拟合——即当你使用太少参数进行预测时。最直接的例子是使用简单的线性回归拟合非线性关系;显然,它不会匹配曲线的弯曲,并且其中一些预测将是误导性的。
有适当的工具可以验证你是否欠拟合,或者更有可能的是过拟合,它们将在第六章 实现泛化(解释数据科学的章节)中进行讨论;同时,不要过度使用高次多项式进行拟合!
摘要
在本章中,我们继续介绍线性回归,将我们的例子从简单扩展到多元。我们回顾了 Statsmodels 线性函数(经典统计方法)和梯度下降(数据科学引擎)的先前输出。
我们通过移除选定的预测因子并从 R 平方测量的角度评估这种移动的影响,开始对模型进行实验。同时,我们还发现了预测因子之间的相互关系,以及如何通过截断交互作用和通过特征的多项式扩展来使每个预测因子与目标变量之间形成更线性的关系。
在下一章中,我们将再次前进,将回归模型扩展到分类任务,使其成为一个概率预测器。进入概率世界的概念跳跃将使我们能够完成线性模型可以成功应用的潜在问题范围。
第四章. 逻辑回归
在本章中,介绍了另一种监督方法:分类。我们将介绍最简单的分类器,逻辑回归,它与线性回归有相同的基础,但针对分类问题。
在下一章中,你会找到:
-
对分类问题(包括二分类和多分类问题)的正式和数学定义
-
如何评估分类器的性能——即它们的指标
-
逻辑回归背后的数学
-
为逻辑回归专门构建的 SGD 重访公式
-
多分类情况,使用多分类逻辑回归
定义分类问题
虽然逻辑回归这个名字暗示了回归操作,但逻辑回归的目标是分类。在一个非常严谨的世界,比如统计学,为什么这个技术会有歧义的名字?简单地说,这个名字根本没错,它完全合理:它只需要一点介绍和调查。之后,你就会完全理解为什么它被称为逻辑回归,你也不再认为这个名字是错误的。
首先,让我们介绍什么是分类问题,什么是分类器,它是如何运作的,以及它的输出是什么。
在上一章中,我们将回归描述为在目标变量中估计一个连续值的操作;从数学上讲,预测变量是在范围(−∞, +∞)内的一个实数。相反,分类预测一个类别,即一个有限类别集合中的索引。最简单的情况称为二分类,输出通常是布尔值(true/false)。如果类别是true,则该样本通常被称为正样本;否则,它是一个负样本。
为了举一些例子,以下是一些涉及二分类问题的疑问:
-
这封电子邮件是垃圾邮件吗?
-
我的房子价值至少 20 万美元吗?
-
用户是否点击/打开横幅/电子邮件?
-
这份文档是关于金融的吗?
-
图像中有人吗?是男性还是女性?
小贴士
在回归问题的输出上设置一个阈值,以确定该值是否大于或小于一个固定的阈值,实际上是一个二分类问题。
当输出可以有多个值时(即预测标签是分类变量),这种分类被称为多分类。通常,可能的标签被称为级别或类别,它们的列表应该是有限的,并且事先已知(否则它将是一个无监督问题,而不是监督问题)。
多分类分类问题的例子包括:
-
这是什么花?
-
这个网页页面的主要主题是什么?
-
我正在经历哪种类型的网络攻击?
-
图像中画的是哪个数字/字母?
问题的形式化:二分类
让我们从最简单的分类类型开始:二元分类。不要担心;在接下来的几页中,当我们专注于多类分类时,事情将会变得更加复杂。
形式上,一般观测是一个 n- 维特征向量 (x[i]) 与其标签配对:一般 i- 维可以写成:

分类器底下的模型是一个函数,被称为分类函数,它可以是线性的或非线性的。函数的形式如下:

在预测任务期间,分类函数应用于新的特征向量,分类器的输出代表输入样本被分类到的类别,即预测标签。一个完美的分类器对每个可能的输入都预测正确的类别 y。
特征向量 x 应该包含数字。如果你处理的是分类特征(例如性别、成员资格和单词),你应该能够将那个变量转换为一个或多个数字变量(通常是二进制)。我们将在本书后面的第五章中看到更多关于这一点,该章节专门讨论将变量准备成回归最合适形式的数据准备。
为了有一个直观的理解,让我们考虑一个二元分类问题,其中每个特征都有两个维度(一个二维问题)。让我们首先定义输入数据集;在这里,Scikit-learn 库的make_classifier方法非常有用。它通过提供类别数量、问题维度和观测数量作为参数来创建一个用于分类的虚拟数据集。此外,您应指定每个特征都是信息性的(并且没有冗余),并且每个类别由一个点的单一簇组成:
In:
%matplotlib inline
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=100, n_features=2,
n_informative=2, n_redundant=0,
n_clusters_per_class=1,
class_sep = 2.0, random_state=101)
plt.scatter(X[:, 0], X[:, 1], marker='o', c=y,
linewidth=0, edgecolor=None)
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.show()
Out:

评估分类器的性能
为了理解一个分类器是否是一个好的分类器,或者等价地,为了在分类任务中识别具有最佳性能的分类器,我们需要定义一些指标。由于分类目标可以不同——例如,定义标签的正确性或完整性、最小化错误分类的数量、根据拥有某个标签的可能性进行正确的排序,以及许多其他指标——因此没有单一的指标。所有这些指标都可以在应用成本矩阵后从分类矩阵中推导出来:结果突出了哪些错误更昂贵,哪些错误不那么昂贵。
这里公开的度量标准可以用于二进制和多类分类。尽管这不是性能的度量标准,但让我们从混淆矩阵开始,这是最简单的度量标准,它给我们提供了正确分类和每个类别的误分类错误的视觉影响。在行中有真实标签,在列中有预测标签。让我们也为以下实验创建一个虚拟标签集和预测集。在我们的例子中,原始标签是六个 0 和四个 1;分类器误分类的条目是两个 0 和一个 1:
In:
y_orig = [0,0,0,0,0,0,1,1,1,1]
y_pred = [0,0,0,0,1,1,1,1,1,0]
让我们现在为这个实验创建混淆矩阵:
In:
from sklearn.metrics import confusion_matrix
confusion_matrix(y_orig, y_pred)
Out:
array([[4, 2],
[1, 3]])
从这个矩阵中我们可以提取一些证据:
-
样本数量为
10(整个矩阵的总和)。 -
原始数据中标记为
0的样本数量为6;1的数量为4(线的总和)。这些数字被称为支持。 -
预测数据集中标记为
0的样本数量为5;1的数量为5(列的总和)。 -
正确分类的数量为
7(对角线元素的总和)。 -
误分类的数量为
3(所有不在对角线上的数字的总和)。
一个完美的分类示例应该是在对角线上有所有数字,其他地方都是 0。
这个矩阵也可以用热图来图形化表示。这是一种非常有影响力的表示方式,尤其是在处理多类问题时:
In:
plt.matshow(confusion_matrix(y_orig, y_pred))
plt.title('Confusion matrix')
plt.colorbar()
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.show()
Out:

我们将要探索的第一个度量标准来评估分类器的性能是准确度。准确度是正确分类的百分比,占总样本数的比例。你可以直接从混淆矩阵中推导出这个错误度量,通过将对角线上的总和除以矩阵中元素的总和。最佳准确度为 1.0,最差为 0.0。在先前的例子中,准确度为 7/10 = 0.7。
使用 Python,这会变成:
In:
from sklearn.metrics import accuracy_score
accuracy_score(y_orig, y_pred)
Out:
0.69999999999999996
另一个非常流行的度量标准是精确度。它只考虑一个标签,并计算在该标签上正确分类的百分比。当我们考虑标签“1”时,精确度就是混淆矩阵右下角的数字除以第二列元素的总和——即 3/5=0.6。值介于 0 和 1 之间,其中 1 是最佳结果,0 是最差结果。
注意,Scikit-learn 中的这个函数期望二进制输入,其中只有被检查的类别被标记为 true(有时被称为 类别指示器)。为了提取每个标签的精确度分数,你应该将每个类别转换为二进制向量:
In:
from sklearn.metrics import precision_score
precision_score(y_orig, y_pred)
Out:
0.59999999999999998
与精确度经常相伴的另一个错误度量是召回率。精确度关乎你得到的结果的质量(即,标记为1的结果的质量),而召回率关乎你能得到的结果的质量——即,你正确提取的1的实例数量。此外,这里的度量是基于类别的,要计算类别1的召回率分数,你应该将混淆矩阵右下角的数字除以第二行的总和,即3/4=0.75。召回率的范围是0到1;最佳分数是1,意味着原始数据集中所有1的实例都被正确分类为1;分数等于0意味着没有1被正确分类:
In:
from sklearn.metrics import recall_score
recall_score(y_orig, y_pred)
Out:
0.75
精确度和召回率是两个指标,表明分类器在某个类别上的表现如何。将它们的分数合并,使用调和平均数,你将得到全面的 f1 分数,帮助你一眼看出在两个错误度量上的表现。
数学上:

在 Python 中这更容易:
In:
from sklearn.metrics import f1_score
f1_score(y_orig, y_pred)
Out:
0.66666666666666652
总之,如果有这么多错误分数,哪个是最好的?解决方案并不简单,通常最好是拥有并评估所有这些分类器。我们如何做到这一点?这是一个需要写很长的函数吗?不,Scikit-learn 在这里帮助我们,提供了一个方法来计算每个类别的所有这些分数(这真的很有用)。以下是它是如何工作的:
In:
from sklearn.metrics import classification_report
print(classification_report(y_orig, y_pred))
Out:

定义基于概率的方法
让我们逐步介绍逻辑回归是如何工作的。我们说它是一个分类器,但它的名字让人联想到回归器。我们需要将各个部分连接起来的元素是概率解释。
在二元分类问题中,输出可以是0或1。如果我们检查标签属于类别1的概率呢?更具体地说,一个分类问题可以看作:给定特征向量,找到最大化条件概率的类别(要么是 0,要么是 1):

这里是联系:如果我们计算一个概率,分类问题看起来就像回归问题。此外,在二元分类问题中,我们只需要计算属于类别1的成员概率,因此它看起来像是一个定义良好的回归问题。在回归问题中,类别不再是1或0(作为字符串),而是 1.0 和 0.0(作为属于类别1的概率)。
现在我们尝试使用概率解释来拟合一个多重线性回归器在虚拟分类问题上。我们重用本章早期创建的相同数据集,但首先我们将数据集分为训练集和测试集,并将y向量转换为浮点值:
In:
from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, \y_test = train_test_split(X, y.astype(float),\test_size=0.33, random_state=101)
In:
y_test.dtype
Out:
dtype('float64')
In:
y_test
Out:

这里,我们使用这些方法中的几种,将数据集分为两个部分(训练集和测试集),并将y数组中的所有数字转换为浮点数。在最后一个单元格中,我们有效地检查了操作。现在,如果y = 1.0,这意味着相对观察结果是 100%属于类别“1”;y = 0.0表示观察结果是 0%属于类别“1”。由于这是一个二元分类任务,这也意味着它是 100%属于类别“0”(注意,这里的百分比指的是概率)。
现在,让我们继续进行回归:
In:
from sklearn.linear_model import LinearRegression
regr = LinearRegression()
regr.fit(X_train, y_train)
regr.predict(X_test)
Out:

输出——即回归器的预测——应该是属于类别 1 的概率。正如你在最后一个单元格的输出中看到的,这不是一个合适的概率,因为它包含小于 0 和大于 1 的值。这里最简单的方法是将结果裁剪到 0 和 1 之间,并将阈值设置为0.5:如果值大于0.5,则预测的类别是“1”;否则预测的类别是“0”。
这个过程是有效的,但我们可以做得更好。我们已经看到从分类问题过渡到回归问题很容易,然后再用预测值回到预测类别。考虑到这个过程,我们再次开始分析,深入其核心算法的同时引入一些变化。
在我们的假设问题中,我们应用了线性回归模型来估计观察结果属于类别“1”的概率。回归模型如下(如我们在上一章中看到的):

现在,我们已经看到输出不是一个合适的概率。要成为一个概率,我们需要做以下事情:
-
将输出限制在 0.0 和 1.0 之间(裁剪)。
-
如果预测等于阈值(我们之前选择了 0.5),则概率应该是 0.5(对称性)。
为了同时满足这两个条件true,我们最好的办法是将回归器的输出通过一个 sigmoid 曲线,或者 S 形曲线。sigmoid 函数通常将实数域 R 中的值映射到[0,1]范围内的值,并且当映射0时,其值为0.5。
在这样的假设基础上,我们现在可以(第一次)写下逻辑回归算法下面的公式。

还要注意,权重W[0](偏差权重)将负责 sigmoid 的中心点与阈值的错位(它在 0,而阈值在 0.5)。
就这些了。这就是逻辑回归算法。只是还缺了一点:为什么是逻辑?σ函数是什么?
好吧,这两个问题的答案都很简单:标准的选择是 sigma 函数,也称为逆对数函数:

虽然有无限多个函数满足 sigmoid 约束,但选择逻辑函数是因为它是连续的,易于微分,并且计算速度快。如果结果不满意,始终考虑通过引入几个参数,你可以改变函数的陡度和中心。
现在快速绘制 sigmoid 函数:
In:
import numpy as np
def model(x):
return 1 / (1 + np.exp(-x))
X_vals = np.linspace(-10, 10, 1000)
plt.plot(X_vals, model(X_vals), color='blue', linewidth=3)
plt.ylabel('sigma(t)')
plt.xlabel('t')
plt.show()
Out:

你可以立即看到,对于非常低的 t,函数趋向于 0;对于非常高的 t,函数趋向于 1,而在中心,t 为 0 时,函数是 0.5。这正是我们寻找的 sigmoid 函数。
更多关于逻辑和 logit 函数的信息
现在,为什么我们使用 logit 函数的逆函数?难道没有比这更好的方法吗?这个问题的答案来自统计学:我们处理的是概率,而 logit 函数是一个很好的匹配。在统计学中,logit 函数应用于概率,返回对数几率:

这个函数将范围 [0,1] 内的数字转换为 (−∞, +∞) 范围内的数字。
现在,让我们看看你是否可以直观地理解选择逆 logit 函数作为逻辑回归的 sigmoid 函数的逻辑。让我们首先写下两个类别的概率,根据这个逻辑回归方程:

让我们现在计算对数几率:

然而,不出所料,这也是 logit 函数,应用于得到“1”的概率:

我们推理的链条最终闭合了,这就是为什么逻辑回归基于,正如定义所暗示的,逻辑函数。实际上,逻辑回归是广义线性模型(GLM)的大类别中的一个模型:广义线性模型。每个模型都有不同的函数,不同的公式,不同的操作假设,并且不出所料,不同的目标。
让我们看看一些代码
首先,我们从本章开头创建的虚拟数据集开始。创建和拟合逻辑回归分类器非常简单:多亏了 Scikit-learn,这只需要几行 Python 代码。对于回归器,要训练模型,你需要调用 fit 方法,而对于预测类别,你只需要调用 predict 方法:
In:
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression()
clf.fit(X_train, y_train.astype(int))
y_clf = clf.predict(X_test)
print(classification_report(y_test, y_clf))
Out:

注意,这里我们并没有进行回归操作;这就是为什么标签向量必须包含整数(或类别索引)。底部显示的报告显示了一个非常准确的预测:所有类别的分数都接近 1。由于测试集中有 33 个样本,0.97 意味着只有一个案例被错误分类。在这个示例中这几乎是完美的!
现在,让我们更深入地探索一下。首先,我们想检查分类器的决策边界:二维空间中哪些部分被分类为“1”;“0”在哪里?让我们看看你如何在这里直观地看到决策边界:
In:
# Example based on:
# Code source: Gaël Varoquaux, Modified for documentation by Jaques Grobler, License: BSD 3 clause
h = .02 # step size in the mesh
# Plot the decision boundary. For that, we will assign a color to each
# point in the mesh [x_min, m_max]x[y_min, y_max].
x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
# Put the result into a color plot
Z = Z.reshape(xx.shape)
plt.pcolormesh(xx, yy, Z, cmap=plt.cm.autumn)
# Plot also the training points
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k', linewidth=0, cmap=plt.cm.Paired)
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())
plt.xticks(())
plt.yticks(())
plt.show()
Out:

分隔几乎垂直。数字“1”在左侧(黄色)一侧;“0”在右侧(红色)一侧。从早期的截图,你可以立即感知到误分类:它非常接近边界。因此,它属于类别“1”的概率将非常接近 0.5。
现在让我们看看裸概率和权重向量。要计算概率,你需要使用分类器的predict_proba方法。它为每个观测值返回两个值:第一个是该观测值属于类别“0”的概率;第二个是类别“1”的概率。由于我们感兴趣的是类别“1”,因此我们只选择所有观测值的第二个值:
In:
Z = clf.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:,1]
Z = Z.reshape(xx.shape)
plt.pcolormesh(xx, yy, Z, cmap=plt.cm.autumn)
ax = plt.axes()
ax.arrow(0, 0, clf.coef_[0][0], clf.coef_[0][1], head_width=0.5,
head_length=0.5, fc='k', ec='k')
plt.scatter(0, 0, marker='o', c='k')
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())
plt.show()
Out:

在截图上,纯黄色和纯红色是预测概率非常接近 1 和 0 的地方。黑色点是笛卡尔二维空间的起点 (0,0),箭头是分类器权重向量的表示。正如你所见,它与决策边界正交,并且它是指向“1”类别的。权重向量实际上是模型本身:如果你需要将其存储在文件中,请考虑它只是几个浮点数,没有其他东西。
最后,我想关注一下速度。现在让我们看看分类器训练和预测标签需要多少时间:
In:
%timeit clf.fit(X, y)
Out:
1000 loops, best of 3: 291 µs per loop
In:
%timeit clf.predict(X)
Out:
10000 loops, best of 3: 45.5 µs per loop
In:
%timeit clf.predict_proba(X)
Out:
10000 loops, best of 3: 74.4 µs per loop
虽然时间取决于计算机(在这里我们使用完整的 100 点数据集进行训练和预测),但你可以看到逻辑回归在训练和预测类别以及所有类别的概率时都非常快。
逻辑回归的优缺点
逻辑回归之所以非常流行,是因为以下原因:
-
它是线性的:它是分类中线性回归的等价物。
-
它非常容易理解,输出可以是最可能的类别,或者成员的概率。
-
训练很简单:它具有非常少的系数(每个特征一个系数,加上一个偏置)。这使得模型非常小,便于存储(你只需要存储一个权重向量)。
-
它在计算上效率很高:使用一些特殊的技巧(本章后面将介绍),它可以非常快地训练。
-
它有用于多类别分类的扩展。
很遗憾,它不是一个完美的分类器,存在一些缺点:
-
与大多数高级算法相比,它通常表现不佳,因为它往往欠拟合(没有灵活性:边界必须是一条线或超平面)
-
它是线性的:如果问题是非线性的,就没有办法将这个分类器适当地拟合到数据集上
回顾梯度下降
在上一章中,我们介绍了梯度下降技术来加速处理。正如我们在线性回归中看到的,模型的拟合可以通过两种方式完成:闭式或迭代式。闭式在一步内给出最佳可能解(但这是一个非常复杂且耗时的一步);相反,迭代算法通过逐步计算每个更新的少量计算来达到最小值,并且可以在任何时候停止。
梯度下降是拟合逻辑回归模型的一个非常流行的选择;然而,它与牛顿方法共享其流行度。由于逻辑回归是迭代优化的基础,并且我们已经介绍了它,因此我们将重点放在这一节上。不用担心,没有赢家或任何最佳算法:它们最终都可以达到完全相同的模型,只是在系数空间中遵循不同的路径。
首先,我们应该计算损失函数的导数。让我们让它更长一些,并开始推导逻辑函数:

它的一阶导数如下:

这也是逻辑回归使用逻辑函数的另一个原因:它的导数计算量小。现在,假设训练观察值是独立的。关于权重的似然计算如下:

注意,在最后一行,我们使用了一个基于事实的技巧,即 y[i] 可以是 0 或 1。如果 y[i]=1,则只计算乘法的第一因子;否则是第二因子。
现在,让我们计算对数似然:这将使事情变得更容易:

现在,我们有两点需要考虑。首先:SGD 一次只处理一个点;因此,对数似然,逐步,只是单个点的函数。因此,我们可以移除对所有点的求和,并将 (x,y) 命名为观察点。其次,我们需要最大化似然:为了做到这一点,我们需要提取其对 W 的通用 k-th 系数的偏导数。
这里的数学变得有些复杂;因此我们只写最后的结果(这是我们将在模型中使用的思考)。推导和理解中间的方程留给读者作为作业:

由于我们试图最大化似然(及其对数版本),更新权重的正确公式是随机梯度上升:

这是通用的公式。在我们的情况下,组成 W 的每个系数的更新步骤如下:

这里,(x,y) 是为更新步骤和学习步骤选择的(随机)随机观察值。
要查看 SGD 产生的真实示例,请查看本章的最后部分。
多类逻辑回归
将逻辑回归扩展到分类超过两个类别的多类逻辑回归。其基础实际上是一种通用方法:它不仅适用于逻辑回归器,还适用于其他二分类器。基本算法命名为One-vs-rest,或One-vs-all,它简单易懂且易于应用。
让我们用一个例子来描述它:我们必须对三种花卉进行分类,并且给定一些特征,可能的输出是三个类别:f1、f2和f3。这并不是我们之前看到的;事实上,这并不是一个二元分类问题。相反,这个问题似乎很容易分解成三个更简单的问题:
-
问题 #1:正例(即被标记为“1”的例子)是
f1;负例是所有其他例子 -
问题 #2:正例是
f2;负例是f1和f3 -
问题 #3:正例是
f3;负例是f1和f2
对于所有三个问题,我们可以使用二分类器,如逻辑回归器,并且不出所料,第一个分类器将输出P(y = f1|x);第二个和第三个将分别输出P(y = f2|x)和 P(y = f3|x)。
为了做出最终预测,我们只需要选择发出最高概率的分类器。训练了三个分类器后,特征空间不是分为两个子平面,而是根据三个分类器的决策边界来划分。
One-vs-all 方法非常方便,事实上:
-
需要拟合的分类器数量正好等于类别的数量。因此,模型将由N(其中N是类别的数量)权重向量组成。
-
此外,这个操作是令人尴尬的并行,N个分类器的训练可以同时进行,使用多个线程(最多N个线程)。
-
如果类别平衡,每个分类器的训练时间相似,预测时间也相同(即使对于不平衡的类别)。
为了更好地理解,让我们通过一个多类分类的例子来说明,创建一个虚拟的三类数据集,将其分为训练集和测试集,训练一个多类逻辑回归器,将其应用于训练集,并最终可视化边界:
In:
%reset -f
In:
%matplotlib inline
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=200, n_features=2,
n_classes=3, n_informative=2,
n_redundant=0, n_clusters_per_class=1,
class_sep = 2.0, random_state=101)
plt.scatter(X[:, 0], X[:, 1], marker='o', c=y, linewidth=0, edgecolor=None)
plt.show()
Out:

In:
from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y.astype(float),
test_size=0.33, random_state=101)
In:
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression()
clf.fit(X_train, y_train.astype(int))
y_clf = clf.predict(X_test)
In:
from sklearn.metrics import classification_report
print(classification_report(y_test, y_clf))
Out:

In:
import numpy as np
h = .02 # step size in the mesh
x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
# Put the result into a color plot
Z = Z.reshape(xx.shape)
plt.pcolormesh(xx, yy, Z, cmap=plt.cm.autumn)
# Plot also the training points
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k', cmap=plt.cm.Paired)
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())
plt.xticks(())
plt.yticks(())
plt.show()
Out:

在这个虚拟数据集上,分类器实现了完美的分类(精确度、召回率和 f1 分数都是 1.0)。在最后一张图片中,你可以看到决策边界定义了三个区域,并创建了一个非线性划分。
最后,让我们观察第一个特征向量、其原始标签和其预测标签(都报告为类别“0”):
In:
print(X_test[0])
print(y_test[0])
print(y_clf[0])
Out:
[ 0.73255032 1.19639333]
0.0
0
要得到属于三个类别中每一个的概率,你可以简单地应用predict_proba方法(与二分类情况完全相同),分类器将输出三个概率。当然,它们的总和是 1.0,自然,对于类别“0”的最高值是 1。
In:
clf.predict_proba(X_test[0])
Out:
array([[ 0.72797056, 0.06275109, 0.20927835]])
一个例子
我们现在来看一个实际例子,其中包含了本章到目前为止所看到的内容。
我们的数据集是一个人工创建的,由 10,000 个观测值和 10 个特征组成,所有这些特征都是信息性的(也就是说,没有冗余的),标签为“0”和“1”(二元分类)。在机器学习中拥有所有信息性特征不是一个不切实际的假设,因为通常特征选择或特征减少操作会选择非相关特征。
In:
X, y = make_classification(n_samples=10000, n_features=10,
n_informative=10, n_redundant=0,
random_state=101)
现在,我们将向您展示如何使用不同的库和不同的模块来执行分类任务,使用逻辑回归。我们不会关注如何衡量性能,而是关注系数如何构成模型(我们在前几章中命名过)。
作为第一步,我们将使用 Statsmodel。在加载正确的模块后,我们需要向输入集添加一个额外的特征,以便有偏置权重W[0]。之后,训练模型就非常简单了:我们只需要实例化一个logit对象并使用其fit方法。Statsmodel 将训练模型,并显示它是否能够训练一个模型(优化成功终止)或失败:
In:
import statsmodels.api as sm
import statsmodels.formula.api as smf
In:
Xc = sm.add_constant(X)
logistic_regression = sm.Logit(y,Xc)
fitted_model = logistic_regression.fit()
Out:
Optimization terminated successfully.
Current function value: 0.438685
Iterations 7
要深入了解模型,请使用 summary 方法:
In:
fitted_model.summary()
Out:

返回了两个表格:第一个关于数据集和模型性能;第二个关于模型的权重。Statsmodel 提供了关于模型的大量信息;其中一些已经在第二章中展示过,接近简单线性回归,关于一个训练好的回归器。在这里,我们简要描述了对于分类器显示的信息:
-
收敛:这表明分类模型在训练过程中是否已达到收敛。只有当此参数为
true时,才使用该模型。 -
对数似然:这是似然的对数。这是我们之前命名的。
-
LL-Null:这是仅使用截距作为预测因子时的对数似然。
-
LLR p 值:这是得到一个统计上大于 LLR 的对数似然比的概率。基本上,它显示了模型相对于使用常数猜测的优越性。LLR 是对数似然比,即零模型(仅截距)的似然的对数除以备择模型(完整模型)的似然。
-
伪 R 平方:这可以看作是模型未解释的总变异的比例。它计算为1-Log-似然/LL-Null。
至于系数表,每行对应一个系数:const是与截距项(即偏差权重)关联的权重;x1、x2、… x10是与模型中组成的 10 个特征关联的权重。对于每个权重,都有一些值:
-
系数: 这是与该特征关联的模型中的权重。
-
标准误差: 这是系数的标准误差,即其(预测的)标准差(在所有观测值中)除以样本大小的平方根。
-
Z: 这是标准误差与系数之间的比率(即统计 t 值)。
-
P>|z|: 这是从同一总体中采样时获得大于 z 的 t 值的概率。
-
[95.0% 置信区间]: 这是一个区间,在这个区间内,我们有 95%的信心认为系数的真实值就在这里。它是通过计算 系数 +/- 1.96 * 标准误差 得出的。
获取相同结果的另一种方法(通常在模型包含少量特征时使用)是写下回归中涉及的公式。这得益于 Statsmodel 公式 API,它使得拟合操作类似于你在 R 中使用的操作。我们首先需要命名特征,然后写下公式(使用我们设定的名称),最后拟合模型。使用这种方法,截距项会自动添加到模型中。因此,其输出与前面的输出相同:
In:
import pandas as pd
Xd = pd.DataFrame(X)
Xd.columns = ['VAR'+str(i+1) for i in range(10)]
Xd['response'] = y
logistic_regression = smf.logit(formula =
'response ~ VAR1+ VAR2 + VAR3 + VAR4 + \
VAR5 + VAR6 + VAR7 + VAR8 + VAR9 + VAR10', data=Xd)
fitted_model = logistic_regression.fit()
fitted_model.summary()
Out:
[same output as above]
让我们改变我们的方法,现在完全实现随机梯度下降公式。公式中的每一部分都有一个函数,而main函数是优化。与线性回归相比,这里的主要区别是loss函数,即逻辑函数(即 Sigmoid):
In:
from sklearn.preprocessing import StandardScaler
import numpy as np
observations = len(X)
variables = ['VAR'+str(i+1) for i in range(10)]
In:
import random
def random_w( p ):
return np.array([np.random.normal() for j in range(p)])
def sigmoid(X,w):
return 1./(1.+np.exp(-np.dot(X,w)))
def hypothesis(X,w):
return np.dot(X,w)
def loss(X,w,y):
return hypothesis(X,w) - y
def logit_loss(X,w,y):
return sigmoid(X,w) - y
def squared_loss(X,w,y):
return loss(X,w,y)**2
def gradient(X,w,y,loss_type=squared_loss):
gradients = list()
n = float(len( y ))
for j in range(len(w)):
gradients.append(np.sum(loss_type(X,w,y) * X[:,j]) / n)
return gradients
def update(X,w,y, alpha=0.01, loss_type=squared_loss):
return [t - alpha*g for t, g in zip(w, gradient(X,w,y,loss_type))]
def optimize(X,y, alpha=0.01, eta = 10**-12, loss_type=squared_loss, iterations = 1000):
standardization = StandardScaler()
Xst = standardization.fit_transform(X)
original_means, originanal_stds = standardization.mean_, standardization.std_
Xst = np.column_stack((Xst,np.ones(observations)))
w = random_w(Xst.shape[1])
path = list()
for k in range(iterations):
SSL = np.sum(squared_loss(Xst,w,y))
new_w = update(Xst,w,y, alpha=alpha, loss_type=logit_loss)
new_SSL = np.sum(squared_loss(Xst,new_w,y))
w = new_w
if k>=5 and (new_SSL - SSL <= eta and new_SSL - SSL >= -eta):
path.append(new_SSL)
break
if k % (iterations / 20) == 0:
path.append(new_SSL)
unstandardized_betas = w[:-1] / originanal_stds
unstandardized_bias = w[-1]-np.sum((original_means /
originanal_stds) * w[:-1])
return np.insert(unstandardized_betas, 0, unstandardized_bias),
path,k
alpha = 0.5
w, path, iterations = optimize(X, y, alpha, eta = 10**-5, loss_type=logit_loss, iterations = 100000)
print ("These are our final standardized coefficients: %s" % w)
print ("Reached after %i iterations" % (iterations+1))
Out:
These are our final standardized coefficients: [ 0.42991407 0.0670771 -0.78279578 0.12208733 0.28410285 0.14689341
-0.34143436 0.05031078 -0.1393206 0.11267402 -0.47916908]
Reached after 868 iterations
使用随机梯度下降法产生的系数与 Statsmodels 之前推导出的系数相同。如之前所见,代码实现并未进行最佳优化;尽管在求解解决方案方面相当高效,但它仅仅是一种了解随机梯度下降在逻辑回归任务中底层工作原理的指导性方法。尝试调整迭代次数、alpha、eta 和最终结果之间的关系:你会理解这些参数是如何相互关联的,以及如何选择最佳设置。
最后,我们转向 Scikit-learn 库及其逻辑回归的实现。Scikit-learn 有两种实现:一种基于逻辑回归优化的经典解决方案,另一种基于快速随机梯度下降的实现。我们将探索这两种方法。
首先,我们从经典的逻辑回归实现开始。训练过程非常简单,只需要几个参数。我们将将其参数设置到极端,以便解决方案不受正则化(C 值非常高)和容忍度停止标准的限制。我们在本例中这样做是为了获得模型中的相同权重;在真实实验中,这些参数将指导超参数优化。有关正则化的更多信息,请参阅第六章,实现泛化:
In:
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(C=1E4, tol=1E-25, random_state=101)
clf.fit(X,y)
Out:
LogisticRegression(C=10000.0, class_weight=None, dual=False,
fit_intercept=True, intercept_scaling=1, max_iter=100,
multi_class='ovr', penalty='l2', random_state=101,
solver='liblinear', tol=1e-25, verbose=0)
In:
coeffs = [clf.intercept_[0]]
coeffs.extend(clf.coef_[0])
coeffs
Out:

作为最后一个模型,我们尝试了 Scikit-learn 的 SGD 实现。由于模型非常复杂,获得相同的权重非常困难,因为参数应该针对性能进行优化,而不是为了获得与闭式方法相同的结果。因此,使用本例来理解模型中的系数,而不是用于训练真实世界的模型:
In:
from sklearn.linear_model import SGDClassifier
clf = SGDClassifier(loss='log', alpha=1E-4, n_iter=1E2, random_state=101)
clf.fit(X,y)
Out:
SGDClassifier(alpha=0.0001, average=False, class_weight=None,
epsilon=0.1, eta0=0.0, fit_intercept=True, l1_ratio=0.15,
learning_rate='optimal', loss='log', n_iter=100.0,
n_jobs=1, penalty='l2', power_t=0.5, random_state=101,
shuffle=True, verbose=0, warm_start=False)
In:
coeffs = [clf.intercept_[0]]
coeffs.extend(clf.coef_[0])
coeffs
Out:

摘要
我们在本章中看到了如何基于线性回归和对数函数构建二元分类器。它速度快,体积小,非常有效,可以使用基于 SGD 的增量技术进行训练。此外,只需付出很少的努力(使用一对一余方法),二元逻辑回归器就可以变成多类。
在下一章中,我们将重点关注如何准备数据:为了从监督算法中获得最大效果,输入数据集必须经过仔细的清洗和归一化。实际上,现实世界的数据集可能存在缺失数据、错误和异常值,变量可以是分类的,并且具有不同的值范围。幸运的是,一些流行的算法可以处理这些问题,以最佳方式转换数据集,使其适合机器学习算法。
第五章。数据准备
在为理解回归和分类的两个基本线性模型打下坚实基础之后,我们将本章用于讨论为模型提供数据。在接下来的几页中,我们将描述通常可以如何以最佳方式准备数据,以及如何处理更具挑战性的情况,例如数据缺失或存在异常值。
现实世界的实验产生真实数据,与合成或模拟数据相比,真实数据通常变化很大。真实数据也相当混乱,并且经常以明显和某些初始时相当微妙的方式证明是错误的。作为数据从业者,你几乎永远不会找到已经以适合你分析目的的正确形式准备好的数据。
编写关于不良数据和其补救措施的汇编超出了本书的范围,但我们的目的是为你提供基础知识,帮助你管理大多数常见的数据问题,并正确地为你的算法提供数据。毕竟,众所周知的缩写词垃圾输入,垃圾输出(GIGO)是一个我们必须面对和接受的事实。
因此,在本章中,我们将发现各种主题、Python 类和函数,这些将允许你:
-
正确缩放数值特征,不仅更容易比较和解释系数,而且在处理异常或缺失值,或者处理非常稀疏的矩阵(在文本数据处理中非常常见)时也会更加容易
-
将定性特征转换为回归模型可以接受的数值,并正确地转换为预测
-
以最智能的方式转换数值特征,将数据中的非线性关系转换为线性关系
-
确定当重要数据缺失时应该做什么,以估计一个替代方案,甚至让回归自己管理最佳解决方案
-
修复数据中的任何异常或奇怪值,并确保你的回归模型始终正常工作
数值特征缩放
在第三章,实际操作中的多元回归,特征缩放部分,我们讨论了如何将原始变量转换为相似尺度,这有助于更好地解释结果回归系数。此外,当使用基于梯度的算法时,缩放是必不可少的,因为它有助于更快地收敛到解决方案。对于梯度下降,我们将介绍其他只能通过缩放特征才能工作的技术。然而,除了某些算法的技术要求之外,现在我们的目的是引起你注意特征缩放在处理有时可能缺失或错误的数据时的帮助。
缺失或错误的数据不仅可能在训练过程中发生,也可能在生产阶段发生。现在,如果遇到缺失值,您有两个设计选项来创建一个足够鲁棒以应对此类问题的模型:
-
主动处理缺失值(本章中有一段专门介绍这一点)。
-
被动地处理它并:
-
您的系统抛出错误,一切都会崩溃(并且会一直处于崩溃状态,直到问题得到解决)。
-
您的系统忽略缺失数据并计算非缺失值。
-
一定令人担忧的是,您的预测系统可能会陷入困境并停止,但忽略它并汇总现值可能会产生高度偏差的结果。如果您的回归方程被设计为使用所有变量,那么当某些数据缺失时,它将无法正常工作。无论如何,让我们再次回顾线性回归公式:

如您所猜想的,偏差系数实际上始终存在;无论您的预测变量情况如何,它都会出现。因此,即使在极端情况下,例如当所有 X 都缺失时,如果您标准化变量使它们具有零均值。
让我们看看实际操作,并了解如何正确缩放预测变量可以帮助修复缺失值,允许使用高级优化技术,如梯度下降、正则化和随机学习(关于后两种技术将在后续章节中详细介绍),以及轻松检测异常值。
首先,让我们上传用于分析的基本包和函数:
In: import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_boston
from sklearn import linear_model
%matplotlib inline
#To set float output to 5 decimals and to suppress printing of \small floating point values using scientific notation
np.set_printoptions(precision=5, suppress=True)
请注意,波士顿数据集也被重新加载了。我们将 y 作为目标变量,将 X 作为预测变量的数组。
In: boston = load_boston()
dataset = pd.DataFrame(boston.data, \columns=boston.feature_names)
dataset['target'] = boston.target
observations = len(dataset)
variables = dataset.columns[:-1]
X = dataset.ix[:,:-1]
y = dataset['target'].values
由于我们还想对逻辑回归进行测试,我们现在将目标变量转换为二元响应,将所有高于 25 分值的分数设置为“1”级别。
In: yq = np.array(y>25, dtype=int)
在这个操作之后,我们的定性响应变量被命名为 yq。
均值中心化
对于所有缩放操作,我们建议使用 Scikit-learn 包的 preprocessing 模块中的函数。特别是,我们将使用 StandardScaler 和 MinMaxScaler。像 Scikit-learn 中的所有类一样,它们都有 fit 方法,可以记录并存储允许正确缩放的参数。它们还提供了一个 transform 方法,可以立即应用于相同的数据(fit_transform 方法也可以做到这一点)或任何其他数据,例如用于验证、测试的数据,甚至以后的生产数据。
StandardScaler 类将通过移除均值来重新缩放你的变量,这一行为也称为中心化。实际上,在你的训练集中,重新缩放的变量将具有零均值,并且特征将被强制为单位方差。拟合后,该类将包含 mean_ 和 std_ 向量,使你能够访问使缩放成为可能的均值和标准差。因此,在随后的任何用于测试目的或生产中的预测的集合中,你将能够应用完全相同的转换,从而保持算法精确工作所需的数据一致性。
MinMaxScaler 类将根据你指定的范围重新缩放你的变量,设置新的最小值和最大值。拟合后,min_ 和 scale_ 将分别报告从原始变量中减去的最小值和用于将变量除以以获得预期最大值的缩放比例。
提示
如果你将这两个类别之一在训练后用于其他新的数据,新的变量可能会有不同的最大值和最小值,导致生成的转换变量超出范围(超过最大值或低于最小值,或具有异常值)。当这种情况发生时,重要的是要检查新数据是否有异常值,并质疑我们在定义转换和系数时是否使用了正确的训练数据。
现在,让我们上传这两个缩放类,并在对波士顿数据集拟合线性回归时获取系数和截距值的余数:
In: from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import MinMaxScaler
linear_regression = linear_model.LinearRegression(normalize=False,\fit_intercept=True)
linear_regression.fit(X,y)
print ("coefficients: %s\nintercept: %0.3f" % \(linear_regression.coef_,linear_regression.intercept_))
Out:

提示
如果你从你的 Jupyter Notebook 获得的结果是科学记数法,那么首先使用 import numpy as np 然后使用 np.set_printoptions(precision=5, suppress=True) 可能会有所帮助。
特别是,让我们注意到截距。根据线性回归公式,我们可以预期当所有预测变量为零时,这将是对应的回归输出。让我们也看看最小值,以检查它们是负数、零还是正数。
In: dataset.min()
Out:

考虑到我们的变量范围,不可能出现所有预测变量都为零的情况,这意味着尽管截距仍然功能性和对模型正确工作至关重要,但它并不代表任何真正期望的值。
现在,作为一个初步的缩放操作,让我们仅仅将变量中心化,即移除均值,看看这个动作是否会在我们的线性回归中引起变化。
In: centering = StandardScaler(with_mean=True, with_std=False)
linear_regression.fit(centering.fit_transform(X),y)
print ("coefficients: %s\nintercept: %s" % \(linear_regression.coef_,linear_regression.intercept_))
Out:

尽管系数保持不变,但现在截距为 22.533,这个值在我们的波士顿房价问题中具有特殊意义:
In: print ('mean: %0.3f' % np.mean(y))
Out:mean: 22.533
将截距值设为目标值的平均值意味着,当一个或多个值缺失时,我们期望如果中心化了变量,它们将自动获得零值,并且我们的回归将自然地倾向于输出目标变量的平均值。
标准化
在这一点上,我们还可以尝试将所有内容缩放到单位方差并检查结果:
In: standardization = StandardScaler(with_mean=True, with_std=True)
linear_regression.fit(standardization.fit_transform(X),y)
print ("coefficients: %s\nintercept: %0.3f" % \(linear_regression.coef_,linear_regression.intercept_))
Out:

如预期的那样,现在系数不同了,每个系数现在代表在预测变量等效于标准差的变化后,目标变量的单位变化。然而,如果我们的预测变量分布不是正态分布(标准化意味着正态钟形分布),则这些尺度并不完全可比;尽管如此,我们现在可以比较每个预测变量的影响,并允许自动处理缺失值以及高级算法的正确运行。
归一化
归一化与标准化类似,通过作用于预测变量的范围进行缩放,但它具有不同的特性。实际上,在使用归一化时,零值是每个预测变量值域中的最小值。这意味着零不再代表平均值。此外,如果两端存在异常值(大多数值将挤压在 [0,1] 的某个区域,通常在值域的中心),则最大值和最小值之间的缩放可能会产生误导。
In: scaling = MinMaxScaler(feature_range=(0, 1))
linear_regression.fit(scaling.fit_transform(X),y)
print ("coefficients: %s\nintercept: %0.3f" % \(linear_regression.coef_,linear_regression.intercept_))
Out:

在 0 到 1 的范围内应用 MinMaxScaler 会极大地改变系数和截距,但在某些情况下这可能是可以接受的。事实上,当处理来自文本数据或日志的大数据时,我们有时会发现我们正在工作的矩阵并不是特别密集,零经常是遇到的最频繁的值。为了加快计算速度并允许巨大的矩阵保持在内存中,矩阵以稀疏格式存储。
稀疏矩阵并不占用它们大小所需的全部内存,它们只存储坐标和非零值。在这种情况下,标准化变量会将零值变为平均值,并且必须定义大量之前为零的单元格,导致矩阵占用更多的内存。在 0 到 1 之间进行缩放允许以可比较的顺序取值,并保留所有之前为零的条目,从而不修改内存中的矩阵维度。
逻辑回归案例
需要专门讨论逻辑回归。正如我们在上一章中所述,在逻辑回归中,我们建模响应概率的几率比。我们可以使用标准化系数作为处理缺失数据的技巧,就像在线性回归中看到的那样,但事情在尝试猜测线性回归分析中的目标数值时会有所不同。
让我们通过一个例子来探讨这个问题,以澄清情况。我们将使用波士顿数据集来演示逻辑回归案例,并将之前定义的 yq 向量用作响应变量。对于逻辑回归,这次我们不会使用 Scikit-learn 实现,而是使用 Statsmodels 包,这样我们可以轻松地展示模型中系数的一些见解:
In: import statsmodels.api as sm
Xq = sm.add_constant(standardization.fit_transform(X))
logit = sm.Logit(yq, Xq)
result = logit.fit()
print (result.summary())
Out:

使用标准化的预测值,就像线性回归一样,我们可以用相同的尺度来解释系数,并将截距视为当所有预测值都取平均值时的响应。与线性回归相反,在逻辑回归中,预测值的一个单位变化会改变响应的优势比,其量级相当于系数本身的指数化:
In: print ('odd ratios of coefficients: %s' % np.exp(result.params))
Out: odd ratios of coefficients: [ 0.04717 0.90948 1.2896 0.46908 1.2779 0.45277 3.75996 1.10314 0.28966 15.9012 0.16158 0.46602 0.81363 0.07275]
我们回顾一下如何计算优势比:给定一个事件发生的概率 p,优势比是 p 与其补数到 1 的比率,优势比 = p / (1−p)。当优势比等于 1 时,我们的概率正好是 0.5。当概率高于 0.5 时,优势比高于 1;相反,当我们的概率小于 0.5 时,优势比低于 1。通过应用自然对数(正如逻辑回归所做的那样),值将分布在零值(50% 概率)周围。显然,处理概率更直观,因此,一个简单的转换,即 Sigmoid 转换,将系数转换为更可理解的概率:
In: def sigmoid(p):
return 1 / (1 + np.exp(-p))
print ('intercept: %0.3f' % result.params[0])
print ('probability of value above 25 when all predictors are \average: %0.3f' % sigmoid(result.params[0]))
Out: intercept: -3.054
probability of value above 25 when all predictors
are average: 0.045
使用 Sigmoid 函数将截距转换为概率,我们得到 0.045,这是当所有预测值都取平均值时,房屋价值超过 25 的概率。请注意,这样的概率与样本中的平均概率不同:
In: print ('average likelihood of positive response: %0.3f' %
(sum(yq) /float(len(yq))))
Out: average likelihood of positive response: 0.245
实际上,这是在考虑任何可能的预测值时,房屋价值超过 25 的基线概率。我们从逻辑回归中提取的实际上是一个特定的概率,而不是一个普遍的概率。实际上,当你用只有一个截距(所谓的空模型)来建模逻辑回归,允许预测值自由变化时,你可以得到一个可比的似然性:
In: C = np.ones(len(X))
logit = sm.Logit(yq, C)
result = logit.fit()
print (result.summary())
print ('\nprobability of value above 25 using just a constant: %0.3f' % sigmoid(result.params[0]))
Out:

定性特征编码
除了本节迄今为止主要讨论的数值特征之外,你的大部分数据也将包含定性变量。数据库尤其倾向于记录人类可读和可理解的数据;因此,它们充满了定性数据,这些数据可以以文本或仅以单个标签的形式出现在数据字段中,解释信息,例如告诉你观察值的类别或其某些特征。
为了更好地理解定性变量,一个工作示例可以是天气数据集。这样的数据集描述了由于天气信息(如展望、温度、湿度和风速)而想要打网球的条件,这些信息都是可以通过数值测量来表示的。然而,你很容易在网上找到这样的数据,并且它们被记录在数据集中,这些数据集包含了它们的定性翻译,如“晴朗”或“阴天”,而不是数值卫星或气象站的测量。我们将处理这类数据,以展示即使这样,它仍然可以被转换成可以有效地包含到线性模型中的形式:
In: outlook = ['sunny', 'overcast', 'rainy']
temperature = ['hot', 'mild', 'cool']
humidity = ['high', 'normal']
windy = ['TRUE', 'FALSE']
weather_dataset = list()
for o in outlook:
for t in temperature:
for h in humidity:
for w in windy:
weather_dataset.append([o,t,h,w])
play = [0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1]
线性回归器只有在被正确转换为数值后才能分析定性数据。一种常见的定性数据类型是名义变量,它们通过一组有限的文本标签来表示。例如,一个名义变量可以是产品的颜色或天气的展望(如我们的天气示例)。变量可以假设的文本值被称为级别;在我们的例子中,展望有三个级别:“晴朗”、“阴天”和“雨天”,所有这些都以字符串的形式表示。
如果我们认为这些中的任何一个都可能存在或不存在(每个标签排除另一个),我们可以轻松地将每个具有 n 个级别的名义变量转换成 n 个不同的变量,每个变量告诉我们某个特征是否存在。如果我们用 1 表示级别的存在,用 0 表示其不存在(类似于二进制编码,如计算机中的编码),我们将得到将定性信息转换为数值信息的工作转换(技术上它是布尔值,但为了实际目的,我们将其建模为 0-1 的数值整数)。这种转换后的变量在机器学习术语中被称为指标或二元变量,而在统计学中它们被描述为二分法(一个更技术性的术语)或虚拟变量。当级别存在时,它们在回归公式中作为截距的修饰符。
当变量的级别是有序的时候,还有另一种可能的转换。例如,它们可以是定性标签,如好、平均、可接受和差。在这种情况下,标签也可以转换为按照标签意义的顺序递增或递减的数字。因此,在我们的例子中,好可以是 3,平均 2,可接受 1,差 0。这种编码直接将定性变量转换为数值变量,但它只适用于可以排序的标签(即可以定义“大于”和“小于”关系的标签)。这种转换意味着,由于回归模型将为所有级别计算单个系数,因此从好到平均的产出差异与从可接受到差的产出差异相同。在现实中,这通常不成立,因为存在非线性。在这种情况下,二元编码仍然是最佳解决方案。
使用 Pandas 进行虚拟编码
将一组定性变量转换为二元变量的最快方式是使用 Pandas 函数 get_dummies:
In: import pandas as pd
df = pd.DataFrame(weather_dataset, columns=['outlook', \'temperature', 'humidity', 'windy'])
在将所有数据转换成 Pandas DataFrame 之后,调用单个变量和单个案例转换为二元变量的操作相当简单:
In: print (pd.get_dummies(df.humidity).ix[:5,:])
Out: high normal
0 1 0
1 1 0
2 0 1
3 0 1
4 1 0
5 1 0
Pandas 可以轻松地将所有变量转换;你所需要做的只是指出你想要完全转换的 DataFrame 或指定要转换的变量:
In: dummy_encoding = pd.get_dummies(df)
转换后,回归模型可以立即分析得到的新 DataFrame:
In: import statsmodels.api as sm
X = sm.add_constant(dummy_encoding)
logit = sm.Logit(play, X)
result = logit.fit()
print (result.summary())
Out:

一些回归方法并不喜欢所有表示定性变量的二元变量(但我们的情况并非如此)。某些优化方法不喜欢完美的共线性,例如在完全二值化的情况下(实际上,如果你知道所有其他二分法,那么剩余的可以通过求和来完美猜测——当其他变量的和为零时,它具有值 1)。在这种情况下,你只需从每个二元变量集中删除一个你选择的级别。这样做,被省略的系数将包含在截距中,回归模型将像以前一样工作,尽管变量和系数有所不同:
In: X.drop(['outlook_sunny', 'temperature_mild', 'humidity_normal', 'windy_FALSE'], inplace=True, axis=1)
logit = sm.Logit(play, X)
result = logit.fit()
print (result.summary())
Out:

get_dummies 只有一个缺点:它直接构建二元变量,从你要转换的数据集中读取级别。因此,如果你首先从一个样本中构建一组二元变量,然后从另一个样本中构建另一组,由于样本中未出现稀有级别,可能会产生不同的转换数据集。
DictVectorizer 和独热编码
Scikit-learn 包提供了一种方法,虽然不是那么直接,但可以将你的定性变量一致地转换为数值变量。
DictVectorizer类可以读取由字典列表组成的数据集,将字符串标签数据适当地转换为一系列二进制值,并保持数值数据不变。如果你已经在你的数据集中将定性变量编码为数值类型,你只需要在它们被DictVectorizer处理之前将它们转换为字符串值。
你需要做的第一件事是创建你数据集的字典表示,如下例所示:
In: from sklearn.feature_extraction import DictVectorizer
vectorizer = DictVectorizer(sparse = False)
dict_representation = [{varname:var for var, varname in \zip(row,['outlook', 'temperature', 'humidity', 'windy'])}
for row in weather_dataset]
print (dict_representation[0])
print (vectorizer.fit_transform(dict_representation))
Out: {'windy': 'TRUE', 'humidity': 'high', 'temperature': 'hot', 'outlook': 'sunny'}
[[ 1\. 0\. 0\. 0\. 1\. 0\. 1\. 0\. 0\. 1.]
[ 1\. 0\. 0\. 0\. 1\. 0\. 1\. 0\. 1\. 0.]
[ 0\. 1\. 0\. 0\. 1\. 0\. 1\. 0\. 0\. 1.]
...
字典表示形式为字典列表,其键是变量的名称,值是它们的数值或标签值。为了获得这种表示,你需要复制你的数据集,如果你正在处理可用内存较少的情况,这可能会代表一个很大的限制。
另一方面,类别保留了转换的记忆,因此可以使用transform方法在任何其他数据样本上精确复制所有内容,克服了我们之前看到的 Pandas get_dummies方法的限制。
你也可以通过调用features_names_方法轻松可视化转换。
In: print (vectorizer.feature_names_)
Out: ['humidity=high', 'humidity=normal', 'outlook=overcast', \
'outlook=rainy', 'outlook=sunny', 'temperature=cool', \
'temperature=hot', 'temperature=mild', 'windy=FALSE', \
'windy=TRUE']
如果限制的二元化数量不足以证明将整个数据集转换为字典表示的合理性,你可以使用 Scikit-learn 中preprocessing包中的LabelEncoder和LabelBinarizer类,一次编码和转换一个变量。
LabelEncoder将标签转换为数字,LabelBinarizer将数字转换为二分法。所有这些操作在不同样本之间的一致性由 Scikit-learn 中所有类特有的fit和transforms方法保证,其中fit从数据中挑选并记录参数,而transform方法随后应用于新数据。
让我们在outlook变量上测试一个转换。我们首先将文本标签转换为数字:
In: from sklearn.preprocessing import LabelEncoder, LabelBinarizer
label_encoder = LabelEncoder()
print (label_encoder.fit_transform(df.outlook))
Out: [2 2 2 2 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1]
分配的数字由使用inverse_transform方法获得的标签列表中的位置给出:
In: label_encoder.inverse_transform([0,1,2])
Out: array(['overcast', 'rainy', 'sunny'], dtype=object)
或者只需要求记录的课程,浏览classes_内部变量:
In: print (label_encoder.classes_)
Out: ['overcast' 'rainy' 'sunny']
一旦进行数值编码,LabelBinarizer可以将所有内容转换为指示变量,允许你决定应该放置哪些值在二分法中。
事实上,如果你担心丢失的值,你可以将负值编码为−1,将缺失情况保留为0(在这种情况下,缺失值将被动地由截距项负责,就像之前看到的那样)。
In: label_binarizer = LabelBinarizer(neg_label=0, pos_label=1, \sparse_output=False)
print (label_binarizer.fit_transform( \label_encoder.fit_transform(df.outlook)))
Out: [[0 0 1]
[0 0 1]
[0 0 1]
...
这种方法的另一个优点是它允许稀疏表示,因此在处理大型数据集时可以节省内存。
特征哈希器
单热编码是一种强大的转换,它允许仅使用二进制变量来表示任何类型的数据。使用相同的方法,你甚至可以将文本转换为可以被线性回归模型分析的变量。
理念是将文本中任何特定单词的出现转换为特定的二元变量,这样模型就会分配一个与文本中单词出现相关的二元值。例如,如果你想分析拉丁格言 Nomina sunt consequentia rerum(这意味着“名称跟随事物”),你可以将所有文本转换为小写,并通过分词来列举文本中出现的所有不同单词。这样做是为了将它们(在我们的情况下,分词非常简单,我们只是通过空格分割)以通常被称为词袋模型(BoW)的表示方式分开:
In: your_text = 'Nomina sunt consequentia rerum'
mapping_words_in_text = {word:position for position, word in enumerate(set(your_text.lower().split(' ')))}
print (mapping_words_in_text)
Out: {'rerum': 0, 'sunt': 1, 'consequentia': 2, 'nomina': 3}
上述代码只是将你的所有文本数据转换为一个字典,其中包含小写单词及其在二元变量向量中的位置索引。
这个向量的长度是字典的长度,并且当相应的单词出现在分析文本中时,每个二元标志具有单位值。因此,我们所有短语的向量是 [1,1,1,1],而只包含单词 'rerum' 的短语的向量应该是 [1,0,0,0],因为该单词的位置索引是 0。
比喻地说,你可以想象我们的向量就像一排灯;每次,你只打开那些对应于你正在分析文本中出现的单词的灯。
小贴士
将单词转换为指示符只是一个起点。你还可以计算一个单词在文本中出现的次数,并通过考虑你正在转换的文本长度来归一化这个计数。实际上,在较长的文本中,某些单词出现多次的可能性比在较短的文本中要高。通过归一化单词计数,例如,以这种方式,单词计数的总和不能超过某个数值,这样就会使所有文本看起来具有相同的长度。这些只是自然语言处理(NLP)中可能的一些转换,它们适用于线性回归模型。
Scikit-learn 包提供了一个专门用于自动将文本转换为二元变量向量的类;这就是 CountVectorizer 类。它允许将文本数据的列表或数组转换为稀疏矩阵。将 binary 参数设置为 True,在仅使用二元编码转换数据时,将稀疏矩阵表示为对应于单词出现在文本中的单位值集合。作为一个简单的例子,我们可以对以下文本系列进行编码:
In: corpus = ['The quick fox jumped over the lazy dog', 'I sought a dog wondering around with a bird', 'My dog is named Fido']
语料库(对受语言学分析的文档集合的术语,因此常见的是双语语料库或甚至更异质的语料库)中唯一的共同单词是 'dog'。这应该反映在我们的矩阵中;事实上,只有一个列始终具有单位值:
In: from sklearn.feature_extraction.text import CountVectorizer
textual_one_hot_encoder = CountVectorizer(binary=True)
textual_one_hot_encoder.fit(corpus)
vectorized_text = textual_one_hot_encoder.transform(corpus)
print(vectorized_text.todense())
Out: [[0 0 1 0 1 0 1 1 0 0 1 1 0 1 0 0]
[1 1 1 0 0 0 0 0 0 0 0 0 1 0 1 1]
[0 0 1 1 0 1 0 0 1 1 0 0 0 0 0 0]]
为了将生成的矩阵可视化为一个输出,否则它将仅由矩阵中单位值所在的坐标组成,我们需要使用.todense()方法将生成的稀疏矩阵转换为密集矩阵。
小贴士
由于这是一个玩具数据集,这种转换在我们的例子中不会产生太多影响。当你的语料库很大时,请注意不要做同样的事情,因为这可能会在你的系统上引起内存溢出错误。
我们注意到第三列有三个单位,所以我们想象它可能代表单词'dog'。我们可以通过要求一个表示字典和单词位置排列的列表来验证这一点,使用.get_feature_names()方法:
In: print (textual_one_hot_encoder.get_feature_names())
Out: ['around', 'bird', 'dog', 'fido', 'fox', 'is', 'jumped', 'lazy', 'my', 'named', 'over', 'quick', 'sought', 'the', 'with', 'wondering']
利用快速构建单词字典的能力,你可以将其转换并用于文本预测。
使用这种表示可能遇到的唯一问题是当你遇到一个之前从未见过的单词时。让我们看看会发生什么:
In: print (textual_one_hot_encoder.transform(['John went home today']).todense())
Out: [[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]]
这种行为实际上是可以预料的。由于短语中的单词之前都没有遇到过,所以它们在向量化的文本中没有空间容纳。
一个快速有效的解决方案是定义一个非常大的稀疏向量(在填充数据之前不会占用太多空间,无论维度如何),并使用哈希函数的特定特性来确定性地为向量中的每个单词分配一个位置,而无需在分配之前观察单词本身。这也被称为哈希技巧,可以使用 Scikit-learn 的HashingVectorizers应用。
In: from sklearn.feature_extraction.text import HashingVectorizer
hashing_trick = HashingVectorizer(n_features=11, binary=True, \norm=None, non_negative=True)
M = hashing_trick.transform(corpus)
print (M.todense())
Out: [[ 1\. 0\. 0\. 1\. 1\. 0\. 0\. 1\. 0\. 0\. 0.]
[ 0\. 0\. 0\. 1\. 0\. 1\. 0\. 1\. 1\. 0\. 0.]
[ 0\. 0\. 0\. 1\. 0\. 0\. 0\. 1\. 1\. 0\. 0.]]
HashingVectorizer类为你提供了许多选项来探索,特别是对于文本处理,例如允许更复杂的分词(甚至自定义分词),去除常见单词,去除重音符号,以及解析不同的编码。
在我们之前用CountVectorizer所做的事情的一个复制品中,我们固定了一个 11 个元素的输出向量。通过这样做,我们可以注意到并讨论先前输出中的两个相关特征。
首先,很明显单词的位置是不同的(它取决于哈希函数),我们无法获取一个字典,以了解哪个单词在哪个位置(但我们可以确信哈希函数已经正确地完成了其工作)。现在,我们不再担心向量化任何之前未见过的新的文本:
In: print (hashing_trick.transform(['John is the owner of that dog']).todense())
Out: [[1\. 1\. 1\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0.]]
第二件事是,通过仔细观察矩阵中单元值的分布,你会发现某些位置的值集中在特定位置,而其他位置则留空。这是由于在有限数量的位置中,哈希函数的碰撞问题导致的(实际上我们为了便于理解,将n_features参数设置为 11,但在实际分析中,将参数设置为更高的数值是好的实践)。
小贴士
为了避免任何不想要的冲突,一个好的n_features值应该在2**21和2**24之间,这取决于预期的文本多样性(多样性越大,向量应该越大)。
数字特征转换
数值特征可以进行转换,无论目标变量如何。这通常是某些分类器(尤其是基于距离的分类器)性能更好的先决条件。我们通常避免(除了特定情况,例如在建模百分比或具有长尾的分布时)转换目标变量,因为我们将会使目标变量与其他特征之间的任何现有线性关系变得非线性。
我们将继续在波士顿房价数据集上工作:
In: import numpy as np
boston = load_boston()
labels = boston.feature_names
X = boston.data
y = boston.target
print (boston.feature_names)
Out: ['CRIM' 'ZN' 'INDUS' 'CHAS' 'NOX' 'RM' 'AGE' 'DIS' \'RAD' 'TAX' 'PTRATIO' 'B' 'LSTAT']
如前所述,我们使用 Scikit-learn 的LinearRegression来拟合模型,这次我们使用metrics模块中的r2_score函数来测量其 R-squared 值:
In: linear_regression = \linear_model.LinearRegression(fit_intercept=True)
linear_regression.fit(X, y)
from sklearn.metrics import r2_score
print ("R-squared: %0.3f" % r2_score(y, \linear_regression.predict(X)))
Out: R-squared: 0.741
观察残差
残差是在预测值被移除后从原始响应中留下的部分。它是数字信息,告诉我们线性模型无法通过其系数和截距集来理解和预测。
使用 Scikit-learn 处理时,获取残差只需要一个操作:
In: residuals = y - linear_regression.predict(X)
print ("Head of residual %s" % residuals[:5])
print ("Mean of residuals: %0.3f" % np.mean(residuals))
print ("Standard deviation of residuals: %0.3f" \% np.std(residuals))
Out: Head of residual [-6.00821 -3.42986 4.12977 4.79186 8.25712]
Mean of residuals: 0.000
Standard deviation of residuals: 4.680
线性回归的残差总是具有零均值,其标准差取决于产生的误差大小。残差可以提供对异常观察和非线性的洞察,因为,在告诉我们剩余信息之后,它们可以引导我们到具体的问题数据点或数据中的令人困惑的模式。
对于检测非线性的特定问题,我们将使用基于残差的图表,称为部分残差图。在这个图表中,我们将回归残差与从变量的模型系数中得出的值相加,与变量的原始值本身进行比较:
In: var = 7 # the variable in position 7 is DIS
partial_residual = residuals + X[:,var] * \linear_regression.coef_[var]
plt.plot(X[:,var], partial_residual, 'wo')
plt.xlabel(boston.feature_names[var])
plt.ylabel('partial residuals')
plt.show()
Out:

在计算了回归的残差之后,我们决定一次检查一个变量。在选择了我们的变量之后,我们通过将回归的残差与变量值的乘积乘以其系数相加来创建一个部分残差。通过这种方式,我们提取变量从回归线上,并将其放入残差中。现在,作为部分残差,我们既有误差,也有系数加权的变量。如果我们将其与变量本身绘制在一起,我们可以注意到是否存在任何非线性模式。如果有,我们知道我们应该尝试一些修改。
在我们的案例中,有一些迹象表明,当变量的值达到 2 之后,点开始弯曲,这是一个明显的非线性迹象,比如任何弯曲或与拉长的、直线型的点云不同的模式。平方、倒数、对数变换通常可以解决这类问题,而无需添加新的项,例如在使用多项式展开时:
In: X_t = X.copy()
X_t[:,var] = 1./np.sqrt(X_t[:,var])
linear_regression.fit(X_t, y)
partial_residual = residuals + X_t[:,var] * \linear_regression.coef_[var]
plt.plot(X_t[:,var], partial_residual, 'wo')
plt.xlabel(boston.feature_names[var])
plt.ylabel('partial residuals')
plt.show()
print ("R-squared: %0.3f" % r2_score(y, \linear_regression.predict(X_t)))
Out: R-squared: 0.769

注意一下逆平方变换如何使部分残差图变得更直,这一点反映在更高的 R-squared 值上,表明模型捕捉数据分布的能力有所提高。
作为规则,以下转换应该始终尝试(单独或组合)以找到解决非线性问题的方法:
| 函数名称 | 函数 |
|---|---|
| 对数 | np.log(x) |
| 指数 | np.exp(x) |
| 平方 | x**2 |
| 立方 | x**3 |
| 平方根 | np.sqrt(x) |
| 立方根 | x**(1./3.) |
| 反函数 | 1./x |
小贴士
表中建议的一些转换在归一化后或存在零和负值的情况下可能无法正常工作:对数转换需要大于零的正值,平方根不能与负值一起使用,反转换不能与零值一起操作。有时添加一个常数可能有所帮助(例如np.log(x+1))。通常,只需根据你的数据值尝试可能的转换。
通过分箱进行总结
当难以确定确切的转换时,一个快速的解决方案是将连续的数值变量转换为一系列二进制变量,从而允许估计变量数值范围内的每个单独部分的系数。
虽然快速方便,但这种解决方案会增加你的数据集大小(除非你使用矩阵的稀疏表示)并且可能会对你的数据过度拟合。
首先,你将你的值分成等间隔的箱,并使用 Numpy 中的histogram函数来观察箱的边缘。之后,使用digitize函数,根据之前提供的箱边界将它们的值转换为箱号。最后,你可以使用之前存在的 Scikit-learn 中的LabelBinarizer将所有箱号转换为二进制变量。
在这个阶段,你所要做的就是用这组新的二进制指标替换之前的变量,并重新调整模型以检查改进:
In: import numpy as np
from sklearn.preprocessing import LabelBinarizer
LB = LabelBinarizer()
X_t = X.copy()
edges = np.histogram(X_t[:,var], bins=20)[1]
binning = np.digitize(X_t[:,var], edges)
X_t = np.column_stack((np.delete(X_t, var, \axis=1),LB.fit_transform(binning)))
linear_regression.fit(X_t, y)
print ("R-squared: %0.3f" % r2_score(y, \linear_regression.predict(X_t)))
Out: R-squared: 0.768
缺失数据
缺失数据在现实生活中的数据中很常见,有时在随机发生中随机出现,更常见的是由于记录和处理中的某些偏差。所有线性模型都基于完整的数值矩阵工作,并且不能直接处理这类问题;因此,照顾好为算法处理提供合适的数据就取决于你了。
即使你的初始数据集没有缺失数据,在生成阶段仍然可能遇到缺失值。在这种情况下,最好的策略无疑是被动处理它们,就像本章开头所展示的那样,通过标准化所有数值变量。
小贴士
对于指示变量,为了被动地拦截缺失值,一种可能的策略是将标签的存在编码为1,其不存在编码为-1,将零值留给缺失值。
当缺失值从项目开始时就存在时,明确处理它们当然更好,试图找出是否存在任何系统性的缺失值模式。在 Python 数组中,Pandas 和 Scikit-learn 包都是基于它构建的,缺失值由一个特殊值标记,非数字(NaN),可以使用来自 NumPy 常数的值重复使用。
创建一个包含缺失值的玩具数组很容易:
In: import Numpy as np
example = np.array([1,2,np.nan,4,5])
print (example)
Out: [ 1\. 2\. nan 4\. 5.]
同时,也可以发现向量中缺失值的位置(结果是布尔向量):
In: print (np.isnan(example))
Out: [False False True False False]
通过切片或使用函数 nan_to_num,可以轻松地替换所有缺失元素,该函数将每个 nan 转换为零:
In: print (np.nan_to_num(example))
Out: [ 1\. 2\. 0\. 4\. 5.]
使用切片,你可以选择比常数更复杂的东西,比如向量中有效元素的均值:
In: missing = np.isnan(example)
replacing_value = np.mean(example[~missing])
example[missing] = replacing_value
print (example)
Out: [ 1\. 2\. 3\. 4\. 5.]
缺失数据插补
当使用预测模型时,数据样本之间处理的一致性至关重要。如果你用某个常数或特定的均值来替换缺失值,那么在训练和生成阶段都应该保持一致。Scikit-learn 包在 preprocessing 模块中提供了 Imputer 类,它可以通过 fit 方法学习解决方案,然后通过 transform 方法一致地应用它。
让我们在向波士顿数据集中添加一些缺失值后演示这一点:
In: from random import sample, seed
import numpy as np
seed(19)
Xm = X.copy()
missing = sample(range(len(y)), len(y)//4)
Xm[missing,5] = np.nan
print ("Header of Xm[:,5] : %s" % Xm[:10,5])
Out: Header of Xm[:,5] : [ 6.575 nan 7.185 nan 7.147 6.43 6.012 6.172 nan 6.004]
小贴士
由于采样过程的随机性,你得到相同的结果的可能性相当低。请注意,练习集设置了一个种子,这样你可以在你的电脑上得到相同的结果。
现在变量中应有大约四分之一的观测值缺失。让我们使用 Imputer 通过均值来替换它们:
In: from sklearn.preprocessing import Imputer
impute = Imputer(missing_values = 'NaN', strategy='mean', axis=1)
print ("Header of imputed Xm[:,5] : %s" % \impute.fit_transform(Xm[:,5])[0][:10])
Out: Header of imputed Xm[:,5] : [ 6.575 6.25446 7.185 6.25446 7.147 6.43 6.012 6.172 6.25446 6.004 ]
Imputer 允许你定义任何值作为缺失值(有时在重新加工的数据集中,缺失值可以用负值或其他极端值编码)并选择替代策略,而不是均值。其他替代方案是 中位数 和 众数。如果你怀疑异常值正在影响并偏置平均值(在房价中,一些非常昂贵和专属的房屋或地区可能是原因),中位数是有用的。众数,即最频繁的值,如果你正在处理离散值(例如有限范围的整数序列),则是最佳选择。
跟踪缺失值
如果你怀疑缺失值模式中存在一些偏差,通过插补它们,你将失去任何痕迹。在插补之前,一个好的做法是创建一个二进制变量,记录所有缺失值的位置,并将其作为特征添加到数据集中。如前所述,使用 NumPy 创建这样一个新特征非常容易,将 isnan 创建的布尔向量转换为整数向量:
In: missing_indicator = np.isnan(Xm[:,5]).astype(int)
print ("Header of missing indicator : %s" \% missing_indicator[:10])
Out: Header of missing indicator : [0 1 1 0 0 0 0 0 1 1]
线性回归模型将为这个缺失值指标创建一个系数,如果存在任何模式,其信息值将通过系数捕捉。
异常值
在适当转换所有定量和定性变量并修复任何缺失数据后,剩下的只是检测任何可能的异常值,并通过从数据中删除它或将其作为缺失值处理来处理它。
异常值,有时也称为异常,是与其他所有观察到的值非常不同的观察值。它可以被视为一个不寻常的案例,脱颖而出,它可能是由错误(一个完全超出尺度的错误值)或仅仅是偶尔发生(虽然很少,但确实发生了)的值引起的。尽管理解异常值的来源可能有助于以最合适的方式解决问题(错误可以合法地删除;罕见的情况可以保留或限制,甚至可以将其作为缺失值处理),但最关心的是一个或多个异常值对回归分析结果的影响。回归分析中的任何异常数据都意味着回归系数的扭曲以及模型正确预测常规案例的能力受到限制。
小贴士
尽管控制异常值的重要性不言而喻,但遗憾的是,实践者往往忽略了这一活动,因为与其他章节中展示的其他准备工作相比,未能检测到异常值并不会阻止你正在进行的分析,你仍然会得到回归系数和结果(两者可能都相当不准确)。然而,分析过程顺利并不意味着分析本身就没有问题。异常值可能会根据异常值是在目标变量还是预测变量上出现而以两种方式扭曲分析。
为了检测异常值,有几种方法,一些基于对变量单独的观察(单变量方法,或称单变量方法),而另一些则基于将所有变量一起重新组合成一个综合度量(多变量方法)。
最佳的单变量方法基于标准化变量的观察和箱线图的绘制:
-
使用标准化变量,任何得分超过平均值的绝对值三个标准差的值都是可疑的,尽管如果分布不是正态分布,这样的经验法则并不很好地推广。
-
使用箱线图,四分位数范围(简称IQR;它是第 75 百分位数和第 25 百分位数之间的差值)用于检测超出第 75 百分位数和第 25 百分位数的可疑异常值。如果存在值超出 IQR 的例子,它们可以被认为是可疑的,尤其是如果它们的值超过 IQR 边界值的 1.5 倍。如果它们超过 IQR 限制的 3 倍,它们几乎可以肯定是异常值。
小贴士
Scikit-learn 包提供了一些类,用于使用复杂的方法自动检测异常值:EllipticEnvelope和OneClassSVM。尽管这两个复杂算法的详细论述超出了本文的范围,但如果异常值或异常数据是您数据中的主要问题,我们建议您查看这个网页,以获取一些您可以在脚本中采用的快速解决方案:scikit-learn.org/stable/modules/outlier_detection.html。否则,您也可以阅读我们之前出版的书籍 Python 数据科学基础,作者 Alberto Boschetti 和 Luca Massaron,由 Packt Publishing 出版。
响应中的异常值
寻找异常值的第一步是检查响应变量。在观察变量分布和回归残差的图表时,重要的是检查是否存在由于过高或过低而超出主要分布的值。
通常情况下,除非伴随有异常的自变量,否则响应中的异常值对估计系数的影响很小;然而,从统计学的角度来看,由于它们影响了均方根误差的量,它们减少了解释方差(平方 r)并增加了估计的标准误差。这两种影响在统计方法中都是一个问题,而对于数据科学目的来说,这些问题则微不足道。
为了确定哪些响应是异常值,我们首先应该监控目标分布。我们首先回忆一下波士顿数据集:
In: boston = load_boston()
dataset = pd.DataFrame(boston.data, columns=boston.feature_names)
labels = boston.feature_names
X = dataset
y = boston.target
箱线图函数可以提示目标变量中的任何异常值:
In: plt.boxplot(y,labels=('y'))
plt.show()

箱线图及其须须告诉我们,相当多的值超出了四分位数范围,因此它们是可疑的。我们还注意到在值 50 处有一定的集中;实际上,值被限制在 50。
在这一点上,我们可以尝试构建我们的回归模型并检查产生的残差。我们将使用均方根误差对其进行标准化。虽然这不是最精确的方法,但它仍然足够好,可以揭示任何显著的问题:
In: scatter = plt.plot(linear_regression.predict(X), \standardized_residuals, 'wo')
plt.plot([-10,50],[0,0], "r-")
plt.plot([-10,50],[3,3], "r--")
plt.plot([-10,50],[-3,-3], "r--")
plt.xlabel('fitted values')
plt.ylabel('standardized residuals')
plt.show()

通过绘制回归拟合值与标准化残差的散点图,我们发现有几个异常值超过三个标准差,从零均值来看。特别是截顶值,在图中作为点线清晰可见,似乎存在问题。
自变量中的异常值
在检查目标变量后,现在我们也应该看看预测变量。如果异常观测值在目标变量中是异常值,那么在预测变量中的类似情况则被称为有影响力的或高杠杆观测值,因为它们真的可以影响不仅仅是平方和误差(SSE),这次影响系数和截距——简而言之,整个回归解(这就是为什么它们如此重要,需要捕捉)。
标准化后,我们开始使用箱线图来观察分布:
In: standardization = StandardScaler(with_mean=True, with_std=True)
Xs = standardization.fit_transform(X)
boxplot = plt.boxplot(Xs[:,0:7],labels=labels[0:7])

In: boxplot = plt.boxplot(Xs[:,7:13],labels=labels[7:13])

观察完所有箱线图后,我们可以得出结论,存在一些方差受限的变量,如B、ZN和CRIM,它们的特点是值的长尾。还有一些来自DIS和LSTAT的疑似案例。我们可以通过寻找超过表示阈值的值来界定所有这些案例,一个变量一个变量地,但一次性捕捉所有这些案例将是有帮助的。
主成分分析(PCA)是一种可以将复杂数据集简化为较少维度的技术,即数据集原始变量的总和。不深入探讨算法的技术细节,你只需知道算法产生的新维度具有递减的解释力;因此,将这些维度相互对比就像绘制整个数据集的信息一样。通过观察这些合成表示,你可以发现群组和孤立点,如果它们离图表中心非常远,那么它们对回归模型的影响也相当大。
In: from sklearn.decomposition import PCA
pca = PCA()
pca.fit(Xs)
C = pca.transform(Xs)
print (pca.explained_variance_ratio_)
Out: [ 0.47097 0.11016 0.09547 0.06598 0.0642 0.05074 \0.04146 0.0305 0.02134 0.01694 0.01432 0.01301 0.00489]
In: import numpy as np
import matplotlib.pyplot as plt
explained_variance = pca.explained_variance_ratio_
plt.title('Portion of explained variance by component')
range_ = [r+1 for r in range(len(explained_variance))]
plt.bar(range_,explained_variance, color="b", alpha=0.4, \align="center")
plt.plot(range_,explained_variance,'ro-')
for pos, pct in enumerate(explained_variance):
plt.annotate(str(round(pct,2)), (pos+1,pct+0.007))
plt.xticks(range_)
plt.show()
Out:

PCA 创建的第一个维度可以解释数据集信息的 47%,第二个和第三个分别是 11%和 9.5%(explained_variance_ratio_方法可以提供此类信息)。现在我们只需将第一个维度与第二个和第三个维度进行对比,寻找远离中心的孤立点,因为这些就是我们需要调查的高杠杆案例:
In: scatter = plt.scatter(C[:,0],C[:,1], facecolors='none', \edgecolors='black')
plt.xlabel('Dimension 1')
plt.ylabel('Dimension 2')

In: scatter = plt.scatter(C[:,0],C[:,2], facecolors='none', \edgecolors='black')
plt.xlabel('Dimension 1')
plt.ylabel('Dimension 3')
Out:

移除或替换异常值
在能够检测到异常值和有影响力的观测值之后,我们只需讨论我们可以如何处理它们。你可能认为只需删除它们就可以了,但相反,移除或替换一个异常值是需要仔细考虑的事情。
事实上,异常观测值可能由三个原因(相应的补救措施也随之改变)得到解释:
-
它们是异常值,因为它们是罕见事件,所以它们与其他观察结果相比显得不寻常。如果情况如此,删除数据点可能不是正确的解决方案,因为这些点是您想要建模的分布的一部分,它们之所以突出,仅仅是因为偶然。最好的解决方案是增加样本数量。如果增加样本量不可行,那么删除它们或尝试重新采样以避免它们被选中。
-
一些错误发生在数据处理中,异常观察值来自另一个分布(可能有一些数据被混合,可能是来自不同时间或另一个地理环境)。在这种情况下,需要立即删除。
-
该值是由于输入或处理错误而造成的错误。在这种情况下,该值必须被视为缺失值,并且您应该执行缺失值的插补,以获得合理的值。
小贴士
通常情况下,只有当数据点与您用于预测的数据不同,并且通过删除它们,您可以直接确认它们对回归模型的系数或截距有很大影响时,删除数据点才是必要的。在其他所有情况下,为了避免数据挖掘(关于数据挖掘如何负面地影响您的模型的内容将在下一章中详细介绍),应避免任何形式的筛选来改进模型。
摘要
在本章中,我们处理了您在准备数据以便由线性模型分析时可能遇到的各种问题。
我们首先讨论了变量的缩放和了解新变量的尺度不仅允许我们更好地洞察数据,而且帮助我们处理意外缺失的数据。
然后,我们学习了如何通过使用散列技巧来编码定性变量,并处理具有不可预测变量和文本信息的极端多样的可能级别。然后,我们回到定量变量,学习了如何将其转换为线性形状并获得更好的回归模型。
最后,我们处理了一些可能的数据异常,如缺失值和异常值,展示了一些简单而非常有效和高效的快速修复方法。
在此阶段,在继续到更复杂的线性模型之前,我们只需要说明可以帮助您获得真正优秀的预测引擎的数据科学原则,而不仅仅是数学曲线拟合练习。这正是下一章的主题。
第六章:实现泛化
我们必须承认,到目前为止,我们推迟了线性模型必须接受测试和验证以有效预测其目标的关键真实时刻。到目前为止,我们只是通过天真地查看一系列拟合度指标来判断我们是否做得好,所有这些指标都在告诉我们线性模型能否仅基于我们的训练数据中的信息进行适当的预测。
除非你喜欢“沉或浮”的情况,在进入生产前,你将使用与新的软件相同的程序,你需要对你的模型应用正确的测试,并能够预测其现场表现。
此外,无论你对这类模型的技能和经验水平如何,你很容易被误导,认为只要基于你用来定义它的相同数据,你就是在构建一个好的模型。因此,我们将向您介绍样本内和样本外统计之间的基本区别,并展示当使用过多的预测变量、过少的预测变量或仅仅是错误的变量时,它们如何存在风险而偏离。
那么,我们现在终于准备好检查我们是否做得很好,或者是否需要从头开始重新思考一切了。在本书的这个关键章节中,在继续介绍更复杂的技术之前,我们将向您介绍关键的数据科学配方,以彻底测试您的模型,优化其性能,使其经济高效,并在没有任何顾虑的情况下,将其与真实、新鲜的数据进行对比。
在本章中,你将了解如何:
-
使用最合适的成本度量,在验证/测试集或使用交叉验证上测试你的模型
-
根据统计测试和实验选择最佳特征
-
通过调整成本函数使你的模型更加经济
-
使用稳定性选择,一种几乎自动化的变量选择方法
检查样本外数据
到目前为止,我们一直在努力使回归模型拟合数据,甚至通过修改数据本身(输入缺失数据、去除异常值、进行非线性转换或创建新特征)。通过关注如 R 平方等指标,我们尽力减少预测误差,尽管我们不知道这有多成功。
我们现在面临的问题是,我们不应该期望一个拟合良好的模型在生产过程中自动在所有新的数据上表现良好。
在定义和解释问题时,我们回忆起我们关于欠拟合所说的内容。由于我们正在使用线性模型,我们实际上期望将我们的工作应用于与响应变量具有线性关系的数据。具有线性关系意味着,在响应变量的水平上,我们的预测变量总是以相同的速率不断增加(或减少)。在散点图上,这可以通过一条直线和非常细长的点云来表示,这些点云可以被一条直线回归线穿过,预测误差很小或最小。
当关系是非线性的,变化率和方向是可变的(或者说是增加或减少)时,为了使线性模型更好地工作,我们将不得不尝试通过适当的变换使关系变得直线。否则,我们将不得不尝试通过非线性形状到线性形状的不总是成功的近似来猜测响应。
例如,如果关系是二次的(因此函数形状是抛物线),使用直线将导致在预测变量值的一定范围内对预测值的系统性低估或高估的问题。这种系统性错误称为偏差,它是简单模型(如线性回归)的典型特征。具有高偏差的预测模型将系统地倾向于在特定情况下产生错误的预测。由于预测的不准确性是一个不希望的特性,对于应该能够提供有效预测的工具来说,我们必须努力通过添加新变量和通过多项式扩展或其他变换来转换现有变量,以实现更好的响应拟合。这些努力构成了所谓的特征创建阶段。
通过这样做,我们可能会发现自己处于一个不同但同样有问题的情况。事实上,当我们使我们的模型越来越复杂时,它不仅会更好地拟合响应,通过捕捉更多未知函数与预测变量相关的部分,而且通过添加更多和更多的项,我们使模型能够接收那些仅与当前数据相关的信息(我们称之为噪声),这使得模型越来越不能正确地处理不同的数据。
你可以将其视为一种记忆能力,因此,学习算法越复杂,就有更多的空间来拟合我们从学习数据中使用的不是很有用的信息。这种记忆会带来非常不便的后果。尽管我们的模型似乎在我们的数据上拟合得很好,但一旦它应用于不同的数据集,它就会显示出其无法正确预测的能力。在这种情况下,与之前错误是系统性的(系统性低估或高估)相反,错误将显得是随机的,这取决于数据集。这被称为估计的方差,它可能对你来说是一个更大的问题,因为它可以在你测试它之前让你对其存在一无所知。它倾向于在更复杂的算法中发生,在其最简单的形式中,线性回归倾向于在估计上比方差呈现更高的偏差。无论如何,添加过多的项和交互作用或求助于多项式展开确实会使线性模型面临过拟合的风险。
通过样本分割进行测试
由于我们期望模型能够泛化到新的数据,而且我们很少仅仅对拟合或简单地记忆现有数据感兴趣,因此在构建模型时我们需要采取一些预防措施。为了对抗这个问题,多年来从数据中学习的实践已经定义了一系列基于科学验证和测试方法的程序,我们将展示并亲自实践这些方法。
首先,如果我们希望我们的模型在新数据上具有良好的泛化能力,我们必须在这种情况下对其进行测试。这意味着,如果获取新数据不是一项容易的任务或可行的任务,我们应该从一开始就为测试保留一些数据。我们可以通过随机将数据分为两部分,即训练集和测试集,使用 70-80%的数据进行训练,剩余的 20-30%用于测试目的。
Scikit-learn 的cross_validation模块提供了一系列方法,可以帮助我们处理所有这些操作。让我们通过操作我们常用的波士顿住房数据集来尝试一下:
In: import pandas as pd
from sklearn.datasets import load_boston
boston = load_boston()
dataset = pd.DataFrame(boston.data, columns=boston.feature_names)
dataset['target'] = boston.target
observations = len(dataset)
variables = dataset.columns[:-1]
X = dataset.ix[:,:-1]
y = dataset['target'].values
在加载完成后,我们首先将其分为训练和测试两部分:
In: from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.30, random_state=101)
print ("Train dataset sample size: %i" % len(X_train))
print ("Test dataset sample size: %i" % len(X_test))
Out: Train dataset sample size: 354
Test dataset sample size: 152
train_test_split将根据test_size参数中指定的测试比例来分割数据。分割将是随机的,你可以通过在random_state参数中使用特定的数字种子来确定性控制结果(用于复现目的)(我们选择的种子是101)。
有时候,仅仅保留一个样本外数据(包含不在样本内——即用作从训练活动数据中学习的样本)是不够的,因为我们可能需要调整一些参数或做出特定的选择,并且我们希望测试替代方案而不必使用测试数据。解决方案是保留我们数据的一部分用于验证目的,这意味着检查哪些参数可能对我们模型是最优的。我们可以通过两步使用train_test_split来实现这一点:
In: X_train, X_out_sample, y_train, y_out_sample = \
train_test_split(X, y, test_size=0.40, random_state=101)
X_validation, X_test, y_validation, y_test = \
train_test_split(X_out_sample, y_out_sample, test_size=0.50, random_state=101)
print ("Train dataset sample size: %i" % len(X_train))
print ("Validation dataset sample size: %i" % len(X_validation))
print ("Test dataset sample size: %i" % len(X_test))
Out: Train dataset sample size: 303
Validation dataset sample size: 101
Test dataset sample size: 102
交叉验证
虽然在衡量假设的真实误差方面很有帮助,但将数据分为训练集和测试集(有时也分为验证集)会带来一些风险,你必须考虑到:
-
由于它涉及子采样(你随意抽取初始样本的一部分),你可能会承担抽取对训练和测试过于有利或不利的集的风险。
-
通过留出一部分样本,你减少了可以从中学习的示例数量,而线性模型需要尽可能多的示例来减少估计的方差,消除共线性变量,并正确地模拟非线性。
尽管我们总是建议抽取一个小测试样本(比如数据的 10%)作为你工作有效性的最终检查,但避免上述问题的最佳方法,以及轻松管理不同模型和参数的比较,是应用交叉验证,这要求你为训练和测试分割数据,但它会反复进行,直到每个观测值都扮演了训练和测试的角色。
换句话说,你决定将数据分割成多少互斥的部分,然后你反复使用除了不同的一次之外的所有折来训练你的模型;这起到了测试集的作用。
注意
你将数据分割成多少部分通常设置为 3、5、10 或 20,当你有少量训练数据时,你决定一个较大的分割数(每个分割称为折)。
当你完成验证后,使用所有可用的分割作为测试集,你首先计算结果的平均值,这以相当高的准确性告诉你,当面对新数据时(新数据但与手头的数据不太相似)你的模型的整体性能。然后你也注意到交叉验证性能的标准差。这很重要,因为如果存在高偏差(超过平均性能值的一半),这可能表明模型估计的方差很高,并且需要更多的数据才能良好工作。
在以下示例中,你可以看看KFold和StratifiedKFold(来自 Scikit-learn 的cross_validation模块)是如何工作的。
它们都是迭代器:你为每一轮交叉验证抽取训练和测试的索引,唯一的区别在于KFold只是进行随机抽取。相反,StratifiedKFold会考虑到你希望在训练和测试样本中分布的目标变量的分布,就像它在原始集合上一样。
作为两个类的参数,你应该提供:
-
KFold的观测计数和StratifiedKFold的目标向量 -
折叠数(通常选择 10,但如果你有大量观测值,可以减少折叠数,或者如果你的数据集很小,可以增加折叠数)
你还应该决定:
-
是否要打乱数据或按原样取(打乱总是推荐)
-
是否应用随机种子并使结果可重复
In: from sklearn.cross_validation import cross_val_score, \
KFold, StratifiedKFold
from sklearn.metrics import make_scorer
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
import numpy as np
def RMSE(y_true, y_pred):
return np.sum((y_true -y_pred)**2)
lm = LinearRegression()
cv_iterator = KFold(n=len(X), n_folds=10, shuffle=True,\ random_state=101)
edges = np.histogram(y, bins=5)[1]
binning = np.digitize(y, edges)
stratified_cv_iterator = StratifiedKFold(binning, n_folds=10,\ shuffle=True, random_state=101)
second_order=PolynomialFeatures(degree=2, interaction_only=False)
third_order=PolynomialFeatures(degree=3, interaction_only=True)
over_param_X = second_order.fit_transform(X)
extra_over_param_X = third_order.fit_transform(X)
cv_score = cross_val_score(lm, over_param_X, y, cv=cv_iterator,\ scoring='mean_squared_error', n_jobs=1)
注意
n_jobs参数将通过利用并行计算来设置参与结果计算的线程数。当它设置为-1时,将自动使用所有可用的线程,在您的计算机上尽可能快地加速计算。无论如何,根据您正在工作的系统,有时将参数设置为不同于1的值可能会导致问题,减慢结果。在我们的示例中,作为预防措施,它始终设置为1,但如果你需要缩短计算时间,你可以更改其值。
首先,我们尝试获取一个过参数化模型(波士顿数据集原始特征的二次多项式展开)的交叉验证分数。请注意,由于 Scikit-learn 中计算模型交叉验证的自动函数cross_val_score的内部机制,结果为负(尽管它们是平方误差)。这个函数需要模型、特征和目标变量作为输入。它还接受一个用于参数cv的交叉验证迭代器,一个用于scoring的字符串,表示要使用的评分函数的名称(更多内容请参阅:scikit-learn.org/stable/modules/model_evaluation.html);最后,通过指定n_jobs来指定在您的 PC 上并行工作的线程数(1表示只有一个线程在工作,而-1表示使用系统中的所有可用线程):
In: print (cv_score)
Out: [-10.79792467 -19.21944292 -8.39077691 -14.79808458 -10.90565129 -7.08445784 -12.8788423 -16.80309722 -32.40034131 -13.66625192]
小贴士
均方误差为负是因为函数的内部机制只能最大化,而我们的成本指标需要最小化;这就是为什么它变成了负数
在去掉符号后,我们可以取平均值和标准差。在这里,我们还可以注意到标准差很高,因此我们可能需要尝试控制目标变量的分布,因为在房地产业务中,由于非常富有的住宅区,存在异常观测值:
In: print ('Cv score: mean %0.3f std %0.3f' % (np.mean(np.abs(cv_score)), np.std(cv_score)))
Out: Cv score: mean 14.694 std 6.855
要应用这种控制,我们将目标变量分层;也就是说,我们将其划分为区间,并期望在交叉验证过程中保持区间分布:
In:cv_score = cross_val_score(lm, over_param_X, y,\
cv=stratified_cv_iterator, scoring='mean_squared_error', \
n_jobs=1)
print ('Cv score: mean %0.3f std %0.3f' % \
(np.mean(np.abs(cv_score)), np.std(cv_score)))
Out: Cv score: mean 13.584 std 5.226
最后,控制响应分布实际上降低了估计误差的标准差(以及我们的预期平均值)。在交叉验证中成功分层尝试表明,我们应该在正确分布的训练样本上训练,否则我们可能会得到一个由于采样不良而始终不能正常工作的结果模型。
小贴士
关于交叉验证的最后一个评论,我们建议主要用它来评估参数,并且始终依赖于一个小型的抽取测试集进行性能验证。实际上,这有点棘手,但如果交叉验证次数太多(例如改变种子)以寻找最佳性能,你最终会得到最佳结果,这是另一种称为窥探的过拟合形式(如果你对测试集做同样的事情也会发生)。相反,当你使用交叉验证来选择参数时,你只需决定在选项中哪个是最好的,而不是绝对交叉验证值。
Bootstrapping
有时,如果训练数据真的非常小,即使是划分成折叠也可能惩罚模型的训练方式。重抽样技术允许通过尝试复制数据的潜在分布来重复训练和测试验证序列(允许对预期结果的均值和标准差进行精确估计)多次。
Bootstrapping 基于重复抽样的方法,这意味着允许一个观测值被多次抽取。通常,重抽样抽取的观测数与原始数据集的大小相当。此外,总有一部分观测值保持未动,相当于可用观测值的三分之一,可以用来验证:
In: import random
def Bootstrap(n, n_iter=3, random_state=None):
"""
Random sampling with replacement cross-validation generator.
For each iter a sample bootstrap of the indexes [0, n) is
generated and the function returns the obtained sample
and a list of all the excluded indexes.
"""
if random_state:
random.seed(random_state)
for j in range(n_iter):
bs = [random.randint(0, n-1) for i in range(n)]
out_bs = list({i for i in range(n)} - set(bs))
yield bs, out_bs
boot = Bootstrap(n=10, n_iter=5, random_state=101)
for train_idx, validation_idx in boot:
print (train_idx, validation_idx)
输出将显示如下:

如前例所示(遗憾的是,这种方法不是 Scikit-learn 的一部分,最近已被弃用),在 10 个观测值中,平均有四个观测值可用于测试目的。然而,在重抽样过程中,不仅仅是排除的案例提供了洞察。模型实际上拟合到训练数据集,我们还可以检查在重抽样复制中系数是如何确定的,从而让我们了解每个系数的稳定性:
In: import numpy as np
boot = Bootstrap(n=len(X), n_iter=10, random_state=101)
lm = LinearRegression()
bootstrapped_coef = np.zeros((10,13))
for k, (train_idx, validation_idx) in enumerate(boot):
lm.fit(X.ix[train_idx,:],y[train_idx])
bootstrapped_coef[k,:] = lm.coef_
例如,第十个系数索引(PTRATIO)在符号和值上都非常稳定:
In: print(bootstrapped_coef[:,10])
Output: [-1.04150741 -0.93651754 -1.09205904 -1.10422447 -0.9982515
-0.79789273 -0.89421685 -0.92320895 -1.0276369 -0.79189224]
而第六个系数(AGE)具有很大的变异性,经常甚至改变符号:
In: print(bootstrapped_coef[:,6])
Out: [-0.01930727 0.00053026 -0.00026774 0.00607945 0.02225979 -0.00089469 0.01922754 0.02164681 0.01243348 -0.02693115]
总之,bootstrap 是一种可以运行多次的复制形式,这允许你创建多个模型,并以类似交叉验证过程的方式评估它们的结果。
特征贪婪选择
通过跟随本书中的实验,你可能已经注意到,在线性回归模型中添加新变量总是大获成功。这尤其适用于训练误差,而且这种情况不仅发生在我们插入正确的变量时,也发生在我们放置错误的变量时。令人费解的是,当我们添加冗余或无用的变量时,模型拟合度总是或多或少地有所提高。
原因很容易解释;由于回归模型是高偏差模型,它们发现通过增加它们使用的系数数量来增加其复杂性是有益的。因此,一些新的系数可以用来拟合数据中存在的噪声和其他细节。这正是我们之前讨论的记忆/过度拟合效应。当你有与观察值一样多的系数时,你的模型可能会饱和(这是统计学中使用的术语),你可能会得到完美的预测,因为基本上你有一个系数来学习训练集中每个响应。
让我们用一个训练集(样本内观察)和一个测试集(样本外观察)的快速示例来使这个概念更具体。让我们首先找出我们有多少个案例和特征,以及基线性能是什么(对于样本内和样本外):
In: from sklearn.metrics import mean_squared_error
from sklearn.linear_model import LinearRegression
from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=3)
lm = LinearRegression()
lm.fit(X_train,y_train)
print ('Train (cases, features) = %s' % str(X_train.shape))
print ('Test (cases, features) = %s' % str(X_test.shape))
print ('In-sample mean squared error %0.3f' % mean_squared_error(
y_train,lm.predict(X_train)))
print ('Out-sample mean squared error %0.3f' % mean_squared_error(
y_test,lm.predict(X_test)))
Out: Train (cases, features) = (354, 13)
Test (cases, features) = (152, 13)
In-sample mean squared error 22.420
Out-sample mean squared error 22.440
小贴士
最佳方法将是使用交叉验证或自助法进行此类实验,而不仅仅是简单的训练/测试分割,但我们希望使其快速,这就是我们选择这种解决方案的原因。我们向您保证,使用更复杂的估计技术不会改变实验的结果。
因此,我们在样本内和样本外的误差是相似的。我们可以开始通过多项式展开来改进我们的模型:
In: from sklearn.preprocessing import PolynomialFeatures
second_order=PolynomialFeatures(degree=2, interaction_only=False)
third_order=PolynomialFeatures(degree=3, interaction_only=True)
首先,我们应用二次多项式展开:
In: lm.fit(second_order.fit_transform(X_train),y_train)
print ('(cases, features) = %s' % str(second_order.fit_transform(
X_train).shape))
print ('In-sample mean squared error %0.3f' %
mean_squared_error(y_train,lm.predict(second_order.fit_transform(
X_train))))
print ('Out-sample mean squared error %0.3f' %
mean_squared_error(y_test,lm.predict(second_order.fit_transform(
X_test))))
Out: (cases, features) = (354, 105)
In-sample mean squared error 5.522
Out-sample mean squared error 12.034
看起来,好的样本内结果与样本外测试结果几乎没有对应关系。尽管样本外性能有所提高,但结果缺乏可比性是过度拟合的明显迹象;模型中有些系数更有用,但大多数只是用来捕捉数据中的噪声。
现在,我们走向极端,测试三次多项式展开(尽管只使用交互项):
In: lm.fit(third_order.fit_transform(X_train), y_train)
print ('(cases, features) = %s' % str(third_order.fit_transform(
X_train).shape))
print ('In-sample mean squared error %0.3f' %
mean_squared_error(y_train,lm.predict(third_order.fit_transform(
X_train))))
print ('Out-sample mean squared error %0.3f' %
mean_squared_error(y_test,lm.predict(third_order.fit_transform(
X_test))))
Out: (cases, features) = (354, 378)
In-sample mean squared error 0.438
Out-sample mean squared error 85777.890
现在,显然我们的模型出了大问题。由于系数多于观察值(p>n),我们在训练集上实现了完美拟合。然而,在样本外验证中,我们的模型似乎达到了与随机数生成器相同的性能。在接下来的几段中,我们将向您展示如何利用增加的特征数量,而不会产生之前代码片段中展示的任何问题。
Madelon 数据集
对于在众多嘈杂且共线的变量中选择最佳子集的任务,我们决定将我们常用的波士顿房价数据集与一个棘手的 Madelon 数据集(archive.ics.uci.edu/ml/datasets/Madelon)一起使用。这是一个人工数据集(使用算法生成),在 2003 年 NIPS(第七届神经信息处理系统年会)期间的一个特征选择竞赛中展出。
该数据集尤其具有挑战性,因为它是在将 32 个不同的点簇(16 个来自正组,16 个来自负组)放置在五维超立方体的顶点上生成的。从五个度量维度的各种变换中提取了 500 个特征和 2000 个案例。为了使事情更难,一些随机数被添加到特征中作为噪声,并且一些响应被翻转(翻转的占 1%)。所有这些复杂的变换使得处理建模相当困难,尤其是对于线性模型,因为大多数特征与响应的关系肯定是非线性的。这对我们的示例非常有帮助,因为它清楚地表明直接包含所有特征会损害样本外预测的准确性。
要下载并将这样一个有趣且具有挑战性的数据集上传到您的计算机上,请执行以下说明,并允许您的计算机从存储数据的外部网站下载数据所需的时间:
In: try:
import urllib.request as urllib2
except:
import urllib2
import numpy as np
train_data = 'https://archive.ics.uci.edu/ml/machine-learning-databases/madelon/MADELON/madelon_train.data'
validation_data = 'https://archive.ics.uci.edu/ml/machine-learning-databases/madelon/MADELON/madelon_valid.data'
train_response = 'https://archive.ics.uci.edu/ml/machine-learning-databases/madelon/MADELON/madelon_train.labels'
validation_response = 'https://archive.ics.uci.edu/ml/machine-learning-databases/madelon/madelon_valid.labels'
try:
Xt = np.loadtxt(urllib2.urlopen(train_data))
yt = np.loadtxt(urllib2.urlopen(train_response))
Xv = np.loadtxt(urllib2.urlopen(validation_data))
yv = np.loadtxt(urllib2.urlopen(validation_response))
except:
# In case downloading the data doesn't works,
# just manually download the files into the working directory
Xt = np.loadtxt('madelon_train.data')
yt = np.loadtxt('madelon_train.labels')
Xv = np.loadtxt('madelon_valid.data')
yv = np.loadtxt('madelon_valid.labels')
在加载完训练集和验证集后,我们可以开始探索一些可用的信息:
In: print ('Training set: %i observations %i feature' % (Xt.shape))
print ('Validation set: %i observations %i feature' % (Xv.shape))
Out: Training set: 2000 observations 500 feature
Validation set: 600 observations 500 feature
自然地,我们不会触及验证集(我们甚至不会瞥一眼,否则就是窥探),但我们可以尝试通过训练集来了解情况:
In:from scipy.stats import describe
print (describe(Xt))
输出相当长,并以矩阵形式呈现(因此在此处未报告),但它确实告诉我们关于数据集中每个特征的均值、最小值、最大值、方差、偏度和峰度的所有信息。快速浏览它并没有揭示任何特别之处;然而,它明确指出所有变量都近似呈正态分布,并且它们的值范围有限。我们可以使用变量之间的相关性图继续我们的探索:
import matplotlib.pyplot as plt
import matplotlib as mpl
%matplotlib inline
def visualize_correlation_matrix(data, hurdle = 0.0):
R = np.corrcoef(data, rowvar=0)
R[np.where(np.abs(R)<hurdle)] = 0.0
heatmap = plt.pcolor(R, cmap=mpl.cm.coolwarm, alpha=0.8)
heatmap.axes.set_frame_on(False)
plt.xticks(rotation=90)
plt.tick_params(axis='both', which='both', bottom='off',\
top='off', left = 'off',right = 'off')
plt.colorbar()
plt.show()
visualize_correlation_matrix(Xt[:,100:150], hurdle=0.0)
查看以下截图:

简单浏览一下部分特征及其相关系数后,我们可以注意到其中只有几个具有显著的相关性,而其他则只有轻微的相关性。这给人一种它们之间关系嘈杂的印象,因此有效的选择变得相当复杂。
作为最后一步,我们检查一个简单的逻辑回归模型在曲线下面积指标测量的误差方面会得分如何。
小贴士
曲线下面积(AUC)是通过比较在不同分类阈值下正确正例率与错误率之间的比率得出的一个度量。计算起来有点棘手,所以我们建议始终依赖 sklearn.metrics 模块中的 roc_auc_score 函数。
逻辑回归将观察结果分类为正例,如果阈值超过 0.5,因为这种分割总是被证明是最优的,但我们可以自由地改变这个阈值。为了提高顶级结果选择的精度,我们只需将阈值从 0.5 提高到 1.0(提高阈值会增加所选范围内的准确性)。相反,如果我们打算增加猜测的正例总数,我们只需选择一个低于 0.5 的阈值,直到几乎为 0.0(降低阈值会增加所选范围内正例的覆盖率)。
AUC 错误度量帮助我们确定我们的预测是否按顺序排列得当,无论它们在价值方面的有效精度如何。因此,AUC 是评估选择算法的理想错误度量。如果你根据概率正确地排列结果,无论猜测的概率是否正确,你都可以通过改变 0.5 阈值——也就是说,通过选择一定数量的顶级结果——简单地选择用于你项目的正确选择。
在我们的案例中,基线 AUC 度量为 0.602,这是一个相当令人失望的值,因为随机选择应该给我们带来 0.5 的值(1.0 是可能的最大值):
In: from sklearn.cross_validation import cross_val_score
from sklearn.linear_model import LogisticRegression
logit = LogisticRegression()
logit.fit(Xt,yt)
from sklearn.metrics import roc_auc_score
print ('Training area under the curve: %0.3f' % \roc_auc_score(yt,logit.predict_proba(Xt)[:,1]))
print ('Validation area under the curve: %0.3f' % \roc_auc_score(yv,logit.predict_proba(Xv)[:,1]))
Out: Training area under the curve: 0.824
Validation area under the curve: 0.602
特征的单变量选择
In addition, if interpreting your model is a valuable addition, you really should remove non-useful variables, striving for the simplest possible form of your linear model as dictated by Occam's razor, a commonplace practice in science, favoring simpler solutions against more complex ones when their difference in performance is not marked.
特征选择可以通过仅保留模型中最具预测性的变量集来帮助提高模型的样本外性能和其可读性,在某些情况下只是最好的那些,在其他情况下是协同工作效果最好的那些。
特征选择方法有很多。最简单的方法是单变量方法,它通过估计变量在单独考虑时相对于响应的预测值来评估变量的好坏。
这通常涉及使用统计测试,Scikit-learn 提供了三种可能的测试:
-
f_regression类,它执行 F 测试(一种比较不同回归解决方案的统计测试)和 p 值(可以解释为观察到的差异是偶然发生的概率值),并揭示回归的最佳特征 -
f_class,这是一个 Anova F 测试(一种比较类别之间差异的统计测试),另一种对分类问题有用的统计和相关方法 -
Chi2类,这是一个卡方测试(一种针对计数数据的统计测试),当你的问题是分类且你的答案变量是计数或二进制(在所有情况下,都是正数,如销售单位或赚取的金钱)时,是一个很好的选择
所有这些测试都会输出一个分数和一个用 p 值表示的统计测试。高分,由小的 p 值(小于 0.05,表示得分是通过运气获得的概率很低)证实,将为你提供确认,表明某个变量对你的目标预测是有用的。
在我们的例子中,我们将使用f_class(因为我们现在正在处理一个分类问题)并且我们将使用SelectPercentile函数帮助我们选择一定百分比的得分较高的特征:
In: from sklearn.feature_selection import SelectPercentile, f_classif
selector = SelectPercentile(f_classif, percentile=50)
selector.fit(Xt,yt)
variable_filter = selector.get_support()
在选择了上半部分后,希望已经切掉了最不相关的特征并保留了重要的特征,我们在直方图上绘制我们的结果以揭示得分的分布:
In: plt.hist(selector.scores_, bins=50, histtype='bar')
plt.grid()
plt.show()
看看下面的截图:

显然,大多数分数接近零,只有少数得分较高。现在我们将通过直接选择一个为了方便而经验性地选择的阈值来选择我们假设重要的特征:
In: variable_filter = selector.scores_ > 10
print ("Number of filtered variables: %i" % \ np.sum(variable_filter))
from sklearn.preprocessing import PolynomialFeatures
interactions = PolynomialFeatures(degree=2, interaction_only=True)
Xs = interactions.fit_transform(Xt[:,variable_filter])
print ("Number of variables and interactions: %i" % Xs.shape[1])
Out: Number of filtered variables: 13
Number of variables and interactions: 92
现在,我们已经将我们的数据集缩减到仅包含核心特征。在这个时候,测试多项式展开并尝试自动捕捉模型中的任何相关非线性关系是有意义的:
In: logit.fit(Xs,yt)
Xvs = interactions.fit_transform(Xv[:,variable_filter])
print ('Validation area Under the Curve ' + \
'before recursive \ selection: %0.3f' % \
roc_auc_score(yv,logit.predict_proba(Xvs)[:,1]))
Out: Validation area Under the Curve before
recursive selection: 0.808
结果的验证分数(外部样本)约为 0.81,考虑到我们在训练集上的初始过拟合分数为 0.82,这是一个非常有希望的价值。当然,我们可以决定在这里停止,或者尝试进一步过滤多项式展开;特征选择实际上是一项永无止境的工作,尽管在某个点上你必须意识到,进一步的调整只能带来微小的增量结果。
递归特征选择
单变量选择的唯一问题是它将通过单独考虑每个特征,而不是验证它们如何协同工作来决定最佳特征。因此,冗余变量并不罕见地被选中(由于多重共线性)。
多变量方法,如递归消除,可以避免这个问题;然而,它计算成本更高。
递归消除通过从完整模型开始,并尝试依次排除每个变量,通过交叉验证估计来评估移除效果。如果某些变量对模型性能的影响可以忽略不计,那么消除算法就会剪枝它们。当任何进一步的移除被证明会损害模型正确预测的能力时,这个过程就会停止。
这里是RFECV,Scikit-learn 的递归消除实现的一个演示。我们将使用通过二次多项式扩展增强的波士顿数据集,因此这次我们处理的是一个回归问题:
In: from sklearn.feature_selection import RFECV
from sklearn.cross_validation import KFold
from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = \
train_test_split(X, y, test_size=0.30, random_state=1)
lm = LinearRegression()
cv_iterator = KFold(
n=len(X_train), n_folds=10, shuffle=True, random_state=101)
recursive_selector = RFECV(estimator=lm, step=1, cv=cv_iterator, scoring='mean_squared_error')
recursive_selector.fit(second_order.fit_transform(X_train),
y_train)
print ('Initial number of features : %i' %
second_order.fit_transform(X_train).shape[1])
print ('Optimal number of features : %i' %
recursive_selector.n_features_)
Out: Initial number of features : 105
Optimal number of features : 52
给定一个估计器(我们的模型)、一个交叉验证迭代器和误差度量,RFECV会在一段时间后找出,可以放心地从模型中删除一半的特征,而不用担心会降低其性能:
In: essential_X_train = recursive_selector.transform(
second_order.fit_transform(X_train))
essential_X_test = recursive_selector.transform(
second_order.fit_transform(X_test))
lm.fit(essential_X_train, y_train)
print ('cases = %i features = %i' % essential_X_test.shape)
print ('In-sample mean squared error %0.3f' % \
mean_squared_error(y_train,lm.predict(essential_X_train)))
print ('Out-sample mean squared error %0.3f' % \
mean_squared_error(y_test,lm.predict(essential_X_test)))
Out: cases = 152 features = 52
In-sample mean squared error 7.834
Out-sample mean squared error 11.523
基于测试的检查将揭示现在的外部样本性能为 11.5。为了进一步确认,我们还可以运行交叉验证并获得类似的结果:
In: edges = np.histogram(y, bins=5)[1]
binning = np.digitize(y, edges)
stratified_cv_iterator = StratifiedKFold(binning, n_folds=10, shuffle=True, random_state=101)
essential_X = recursive_selector.transform(
second_order.fit_transform(X))
cv_score = cross_val_score(
lm, essential_X, y, cv=stratified_cv_iterator,
scoring='mean_squared_error', n_jobs=1)
print ('Cv score: mean %0.3f std %0.3f' % (np.mean(np.abs(cv_score)), np.std(cv_score)))
Out: Cv score: mean 11.400 std 3.779
通过网格搜索优化的正则化
正则化是修改回归模型中变量角色的另一种方法,以防止过拟合并实现更简单的函数形式。这种替代方法的有趣之处在于,它实际上不需要操作你的原始数据集,这使得它适合于从大量特征和观察中在线学习和预测的系统,而不需要人为干预。正则化通过使用对过于复杂的模型进行惩罚来丰富学习过程,以缩小(或减少到零)与预测项无关的变量或冗余变量的系数(如之前看到的共线性问题)。
Ridge (L2 正则化)
Ridge 回归背后的思想简单直接:如果问题是存在许多变量,由于它们的系数影响回归模型,我们只需减少它们的系数,使它们的贡献最小化,这样它们就不会对结果产生太大影响。
通过计算不同的损失函数,这样的结果很容易实现。在考虑答案的错误时,可以通过施加一个依赖于系数大小的惩罚值来平衡损失函数。
在以下公式中,是对 第二章 中公式的一个重述,接近简单线性回归 段落 梯度下降在起作用,权重更新通过存在一个负项而修改,这个负项是权重减去一个由 lambda 表示的因子的平方。因此,系数越大,在梯度下降优化的更新阶段减少的幅度就越大:

在前面的公式中,每个单独的系数 j,其值由 w[j] 表示,通过梯度下降学习率 α / n 进行更新,其中 n 是观察数的数量。学习率乘以预测的偏差总和(梯度)。新意在于梯度中存在一个惩罚项,该惩罚项是系数的平方乘以一个 λ lambda 系数。
这样,只有当存在优势(预测中的大偏差)时,错误才会传播到系数上,否则系数的值将减少。优势由 λ lambda 值控制,该值必须根据我们正在构建的特定模型进行经验性寻找。
一个例子将阐明这种新方法是如何工作的。首先,我们必须使用 Scikit-learn 中的Ridge类,如果我们的问题是回归,或者我们在LogisticRegression规范中使用惩罚参数(LogisticRegression(C=1.0, penalty='l2', tol=0.01)):
In: from sklearn.linear_model import Ridge
ridge = Ridge(normalize=True)
ridge.fit(second_order.fit_transform(X), y)
lm.fit(second_order.fit_transform(X), y)
正则化对模型的影响由Ridge中的alpha参数和LogisticRegression中的C参数控制。
alpha的值越小,系数值受正则化的控制就越少,其值随着正则化的增加而增加,系数就越会被压缩。其功能可以简单地记住作为一个压缩参数:值越高,模型的复杂性压缩就越高。然而,LogisticRegression中的 C 参数正好相反,较小的值对应于高正则化(alpha = 1 / 2C)。
在完全拟合模型之后,我们可以看看系数值的定义现在是如何的:
In: print ('Average coefficient: Non regularized = %0.3f Ridge = \
%0.3f' % (np.mean(lm.coef_), np.mean(ridge.coef_)))
print ('Min coefficient: Non regularized = %0.3f Ridge = %0.3f' \
% (np.min(lm.coef_), np.min(ridge.coef_)))
print ('Max coefficient: Non regularized = %0.3f Ridge = %0.3f' \
% (np.max(lm.coef_), np.max(ridge.coef_)))
Out: Average coefficient: Non regularized = 1.376 Ridge = -0.027
Min coefficient: Non regularized = -40.040 Ridge = -2.013
Max coefficient: Non regularized = 142.329 Ridge = 1.181
现在,平均系数值几乎接近零,并且值被放置在一个比之前更短的范围内。在正则化形式中,没有任何单个系数具有影响或,更糟糕的是,破坏预测的权重。
优化参数的网格搜索
到目前为止,我们还没有太多关于模型本身的决策要做,无论我们决定使用逻辑回归还是线性回归。重要的是正确地转换我们的变量(实际上,我们已经了解到这也不是一项容易的任务);然而,L2 参数的引入带来了更多的复杂性,因为我们还必须启发式地设置一个值以最大化模型的性能。
继续使用交叉验证,这确保我们以现实的方式评估模型性能,解决这个问题的好方法是系统地检查给定参数可能值范围的模型结果。
Scikit-learn 包中的GridSearchCV类可以通过我们首选的cv迭代器和评分设置,在设置一个字典来解释模型中必须更改的参数(键)和要评估的值范围(与键相关的值列表)之后,将其分配给类的param_grid参数:
In: from sklearn.grid_search import GridSearchCV
edges = np.histogram(y, bins=5)[1]
binning = np.digitize(y, edges)
stratified_cv_iterator = StratifiedKFold(
binning, n_folds=10,shuffle=True, random_state=101)
search = GridSearchCV(
param_grid={'alpha':np.logspace(-4,2,7)},
estimator=ridge, scoring ='mean_squared_error',
n_jobs=1, refit=True, cv=stratified_cv_iterator)
search.fit(second_order.fit_transform(X), y)
print ('Best alpha: %0.5f' % search.best_params_['alpha'])
print ('Best CV mean squared error: %0.3f' % np.abs(
search.best_score_))
Out: Best alpha: 0.00100
Best CV mean squared error: 11.883
搜索的结果,当有大量可能的模型变体要测试时可能需要一些时间,可以通过属性grid_scores_来探索:
In: search.grid_scores_
Out:
[mean: -12.45899, std: 5.32834, params: {'alpha': 0.0001},
mean: -11.88307, std: 4.92960, params: {'alpha': 0.001},
mean: -12.64747, std: 4.66278, params: {'alpha': 0.01},
mean: -16.83243, std: 5.28501, params: {'alpha': 0.1},
mean: -22.91860, std: 5.95064, params: {'alpha': 1.0},
mean: -37.81253, std: 8.63064, params: {'alpha': 10.0},
mean: -66.65745, std: 10.35740, params: {'alpha': 100.0}]
当alpha为0.001时,达到了最大的评分值(实际上,我们应该使用 RMSE 的最小化结果,因此网格搜索使用 RMSE 的负值)。此外,交叉验证评分的标准差相对于我们的可能解决方案是最小的,这证实了它是我们目前可用的最佳解决方案。
小贴士
如果你想要进一步优化结果,只需使用第二次网格搜索探索获胜解决方案周围的价值范围——即在我们的特定情况下从 0.0001 到 0.01,你可能会找到一个稍微更好的值,从预期的结果或解决方案的稳定性(用标准差表示)的角度来看。
自然地,当涉及更多需要优化的参数时,GridSearchCV 可以有效地使用。请注意,参数越多,需要进行的试验就越多,结果是所有可能测试值的组合——即乘积。因此,如果你正在测试一个超参数的四个值和另一个超参数的四个值,最终你需要进行 4 × 4 次试验,并且根据交叉验证的折数,比如说在我们的例子中是 10,你将让你的 CPU 计算 4 × 4 × 10 = 160 个模型。更复杂的搜索甚至可能涉及测试成千上万的模型,尽管 GridSearchCV 可以并行化所有计算,但在某些情况下仍然可能是一个问题。我们将在下一段中讨论一个可能的解决方案。
小贴士
我们已经展示了如何使用更通用的 GridSearchCV 进行网格搜索。无论如何,Scikit-learn 提供了一个专门的功能,用于自动创建交叉验证优化的岭回归:RidgeCV。还有用于我们即将展示的其他正则化变体的自动化类,例如 LassoCV 和 ElasticNetCV。实际上,这些类除了比我们描述的方法更简洁外,在寻找最佳参数方面也更快,因为它们遵循一个优化路径(因此它们实际上并没有在网格上全面搜索)。
随机网格搜索
在网格中寻找良好的超参数组合是一项非常耗时的工作,尤其是如果有许多参数;组合的数量可能会急剧增加,因此你的 CPU 可能需要很长时间来计算结果。
此外,通常并非所有超参数都是重要的;在这种情况下,当进行网格搜索时,你实际上是在浪费时间检查大量彼此之间没有明显区别的解决方案,而忽略了检查关键参数的重要值。
解决方案是一个随机网格搜索,它不仅比网格搜索快得多,而且效率也更高,正如学者詹姆斯·伯格斯特拉和约书亚·本吉奥在论文中指出的(www.jmlr.org/papers/volume13/bergstra12a/bergstra12a.pdf)。
随机搜索通过从您指定的范围或分布中采样可能的参数来工作(NumPy包有很多可以使用的分布,但在这个测试中我们发现logspace函数对于系统地探索 L1/L2 范围是理想的)。给定一定数量的试验,您有很大机会可以得到正确的超参数。
这里,我们尝试使用从100个可能的值中采样的10个值(因此将我们的运行时间减少到网格搜索的1/10):
In: from sklearn.grid_search import RandomizedSearchCV
from scipy.stats import expon
np.random.seed(101)
search_func=RandomizedSearchCV(
estimator=ridge, n_jobs=1, iid=False, refit=True, n_iter=10,
param_distributions={'alpha':np.logspace(-4,2,100)},
scoring='mean_squared_error', cv=stratified_cv_iterator)
search_func.fit(second_order.fit_transform(X), y)
print ('Best alpha: %0.5f' % search_func.best_params_['alpha'])
print ('Best CV mean squared error: %0.3f' % np.abs(
search_func.best_score_))
Out: Best alpha: 0.00046
Best CV mean squared error: 11.790
小贴士
作为一种启发式方法,随机搜索的试验次数取决于在网格搜索下可能尝试的组合数量。从统计概率的角度来看,已经观察到最有效的随机试验次数应该在 30 到 60 之间。超过 60 次随机试验不太可能比之前评估的从调整超参数中获得更多的性能提升。
Lasso (L1 regularization)
岭回归实际上并不是一种选择方法。通过在模型中保留所有系数来惩罚无用的系数,并不能提供很多关于哪些变量在您的线性回归中表现最好的清晰信息,也不会提高其可理解性。
Lasso 正则化,由 Rob Tibshirani 最近提出,在正则化惩罚中使用绝对值而不是二次值,这确实有助于将许多系数值缩小到零,从而使您的结果系数向量变得稀疏:

再次,我们有一个类似于之前 L2 正则化的公式,但现在惩罚项由λ lambda 乘以系数的绝对值组成。
该过程与岭回归相同;您只需使用一个名为Lasso的不同类即可。如果您的问题是分类问题,在您的逻辑回归中只需指定参数penalty为'l1':
In: from sklearn.linear_model import Lasso
lasso = Lasso(alpha=1.0, normalize=True, max_iter=10**5)
#The following comment shows an example of L1 logistic regression
#lr_l1 = LogisticRegression(C=1.0, penalty='l1', tol=0.01)
让我们检查当使用Lasso时,之前在波士顿数据集上看到的线性回归正则化发生了什么变化:
In: from sklearn.grid_search import RandomizedSearchCV
from scipy.stats import expon
np.random.seed(101)
search_func=RandomizedSearchCV(
estimator=lasso, n_jobs=1, iid=False, refit=True, n_iter=15,
param_distributions={'alpha':np.logspace(-5,2,100)},
scoring='mean_squared_error', cv=stratified_cv_iterator)
search_func.fit(second_order.fit_transform(X), y)
print ('Best alpha: %0.5f' % search_func.best_params_['alpha'])
print ('Best CV mean squared error: %0.3f' % np.abs(
search_func.best_score_))
Out: Best alpha: 0.00006
Best CV mean squared error: 12.235
从性能的角度来看,我们得到了一个略差但可比较的均方误差值。
小贴士
您可能已经注意到,使用Lasso正则化比应用岭回归需要更多的时间(通常有更多的迭代)。一个加快事情的好策略是只对数据的一个子集应用 Lasso(这应该会花费更少的时间),找出最佳的 alpha 值,然后直接应用到您的完整样本上以验证性能结果是否一致。
然而,最有趣的是评估哪些系数被减少到零:
In: print ('Zero value coefficients: %i out of %i' % \
(np.sum(~(search_func.best_estimator_.coef_==0.0)),
len(search_func.best_estimator_.coef_)))
Out: Zero value coefficients: 85 out of 105
现在,我们的二次多项式展开已经减少到仅有20个工作变量,就像模型通过递归选择进行了简化,其优势在于你不需要改变数据集结构;你只需将你的数据应用到模型中,只有正确的变量才会为你计算出预测结果。
小贴士
如果你想知道首先使用哪种正则化,ridge还是lasso,一个很好的经验法则是首先运行一个没有任何正则化的线性回归,并检查标准化系数的分布。如果有许多值相似,那么ridge是最好的选择;如果你注意到只有少数重要系数和许多较小的系数,那么使用lasso来移除不重要的系数是可取的。在任何情况下,当你有比观察更多的变量时,你应该始终使用lasso。
弹性网络
Lasso 可以快速且几乎无障碍地减少预测模型中的工作变量数量,使其更加简单和更具可推广性。其策略很简单:它旨在仅保留对解有贡献的变量。因此,如果你在特征中偶然有几个强共线性变量,L1 正则化将仅保留其中一个,基于数据本身的特性(噪声和其他变量的相关性有助于选择)。
这样的特征无论如何可能因为 L1 解(噪声和相关性可能随着数据变化)的不稳定性而变得不理想,因为模型中包含所有相关变量可以保证模型更加可靠(特别是如果它们都依赖于模型中未包含的因素)。因此,通过结合 L1 和 L2 正则化的效果,已经设计出了弹性网络替代方法。
在弹性网络(Scikit-learn 的ElasticNet类)中,你始终有一个alpha参数,它控制正则化对模型系数确定的影响,还有一个l1_ratio参数,它有助于权衡成本函数正则化部分的 L1 和 L2 部分之间的组合。当参数为0.0时,L1 没有作用,因此相当于岭回归。当设置为1.0时,你有一个 lasso 回归。中间值通过混合两种正则化的效果起作用;因此,尽管一些变量将被减少到零值系数,但共线性变量将被减少到相同的系数,允许它们仍然存在于模型公式中。
在下面的例子中,我们尝试使用弹性网络正则化来解决我们的模型:
In: from sklearn.linear_model import ElasticNet
elasticnet = ElasticNet(alpha=1.0, l1_ratio=0.15, normalize=True, max_iter=10**6, random_state=101)
from sklearn.grid_search import RandomizedSearchCV
from scipy.stats import expon
np.random.seed(101)
search_func=RandomizedSearchCV(estimator=elasticnet, param_distributions={'alpha':np.logspace(-5,2,100), 'l1_ratio':np.arange(0.0, 1.01, 0.05)}, n_iter=30, scoring='mean_squared_error', n_jobs=1, iid=False,
refit=True, cv=stratified_cv_iterator)
search_func.fit(second_order.fit_transform(X), y)
print ('Best alpha: %0.5f' % search_func.best_params_['alpha'])
print ('Best l1_ratio: %0.5f' % \ search_func.best_params_['l1_ratio'])
print ('Best CV mean squared error: %0.3f' % \ np.abs(search_func.best_score_))
Out: Best alpha: 0.00002
Best l1_ratio: 0.60000
Best CV mean squared error: 11.900
通过内省解决方案,我们意识到这是通过排除比纯 L1 解更多的变量来实现的;然而,最终的性能与 L2 解相似:
In: print ('Zero value coefficients: %i out of %i' % (np.sum(~(search_func.best_estimator_.coef_==0.0)), len(search_func.best_estimator_.coef_)))
Out: Zero value coefficients: 102 out of 105
稳定性选择
如所示,L1 惩罚的优势在于使你的系数估计稀疏,并且实际上它充当了一个变量选择器,因为它倾向于只留下模型中的必要变量。另一方面,当数据变化时,选择本身往往是不稳定的,需要一定的努力来正确调整 C 参数,以使选择最有效。正如我们在讨论弹性网络时所看到的,特殊性在于 Lasso 在有两个高度相关变量时的行为;根据数据结构(噪声与其他变量的相关性),L1 正则化将只选择这两个变量中的一个。
在与生物信息学(DNA、分子研究)相关的研究领域,通常基于少量观察工作于大量变量。通常,这类问题被称为 p >> n(特征远多于案例)并且它们需要选择用于建模的特征。由于变量众多,并且它们之间也相当相关,因此求助于变量选择,无论是贪婪选择还是 L1 惩罚,可能会导致从相当大的可能解决方案范围内得到多个结果。来自牛津大学和苏黎世联邦理工学院(ETH Zurich)的两位学者,Nicolai Meinshausen 和 Peter Buhlmann,分别提出了尝试利用这种不稳定性并将其转化为更可靠选择的观点。
他们的想法很简单:由于 L1 惩罚受到数据集中存在的案例和变量的影响,在多重共线性情况下选择某个变量而不是其他变量,我们可以对案例和变量进行子采样,并反复用它们拟合一个 L1 惩罚模型。然后,对于每次运行,我们可以记录得到零系数的特征以及没有得到零系数的特征。通过汇总这些多个结果,我们可以计算每个特征得到非零值的频率统计。以这种方式,即使结果不稳定且不确定,最有信息量的特征会比信息量较少的特征更频繁地得到非零系数。最终,一个阈值可以帮助精确地保留重要的变量,并丢弃不重要的变量以及共线性但不太相关的变量。
小贴士
分数也可以解释为对每个变量在模型中角色的排序。
Scikit-learn 提供了两种稳定性选择的实现:RandomizedLogisticRegression用于分类任务,RandomizedLasso作为回归器。它们都在linear_model模块中。
它们还共享相同的几个关键超参数:
-
C:是正则化参数,默认设置为1.0。如果你能够通过交叉验证在所有数据上找到一个好的 C 值,就将这个数字放入参数中。否则,可以自信地使用默认值;这是一个很好的折衷方案。 -
scaling:是每次迭代要保留的特征的百分比;默认值0.5是一个很好的数值;如果数据中有许多冗余变量,则降低该数值。 -
sample_fraction:是要保留的观察值的百分比;如果怀疑数据中有异常值(因此它们不太可能被抽取),则应降低默认值0.75。 -
n_resampling:迭代次数;越多越好,但 200-300 次重采样应该能得到良好的结果。
在 Madelon 上进行实验
从我们过去的实验来看,稳定性选择确实有助于快速解决变量选择中固有的任何问题,即使处理的是稀疏变量,如转换为指示变量的文本数据。
为了证明其有效性,我们将将其应用于 Madelon 数据集,尝试在稳定性选择后获得更好的 AUC 分数:
In: from sklearn.cross_validation import cross_val_score
from sklearn.linear_model import RandomizedLogisticRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
threshold = 0.03 # empirically found value
stability_selection = RandomizedLogisticRegression(n_resampling=300, n_jobs=1,
random_state=101, scaling=0.15,
sample_fraction=0.50, selection_threshold=threshold)
interactions = PolynomialFeatures(degree=4, interaction_only=True)
model = make_pipeline(stability_selection, interactions, logit)
model.fit(Xt,yt)
由于这是一个分类问题,我们将使用RandomizedLogisticRegression类,设置300次重采样,并子采样 15%的变量和 50%的观察值。作为阈值,我们将保留在模型中至少出现 3%时间的所有显著特征。这样的设置相当严格,但这是由于数据集存在高度冗余和 L1 解的极端不稳定性所致。
使用make_pipeline命令拟合解决方案允许我们创建一系列操作,首先在训练数据上拟合和使用,然后使用相同的配置重新应用于验证数据。其想法是首先根据稳定性选择选择重要的相关特征,然后使用多项式展开创建交互(仅乘法项)来捕捉数据中的非线性成分,并使用新导出的特征。如果我们不先选择应该使用哪些变量就创建多项式展开,那么我们的数据集在变量数量上会呈指数增长,甚至可能无法在内存中存储。
RandomizedLogisticRegression更像是一个预处理过滤器而不是预测模型:拟合后,虽然可以让我们查看生成的分数,但它不会基于创建的模型进行任何预测,但它将允许我们将任何类似于我们的数据集(相同数量的列)进行转换,只保留分数高于我们最初在实例化类时定义的阈值的列。
在我们的案例中,在完成重采样后,这可能需要一些时间,我们可以尝试找出模型保留了多少变量:
In: print ('Number of features picked by stability selection: %i' % \ np.sum(model.steps[0][1].all_scores_ >= threshold))
Out: Number of features picked by stability selection: 19
在这里,19 个变量构成一个小集合,可以扩展为四变量交互类型var1 × var2 × var3 × var4,这使我们能够更好地映射 Madelon 数据集起源处的未知转换。
In: from sklearn.metrics import roc_auc_score
print ('Area Under the Curve: %0.3f' % roc_auc_score(
yv,model.predict_proba(Xv)[:,1]))
Out: Area Under the Curve: 0.885
对获得的概率估计进行最终测试揭示给我们,我们已经达到了0.885的 AUC 值,这是一个相当好的从初始0.602基线上的改进。
摘要
在本章中,我们覆盖了相当多的内容,最终探索了建模线性回归或分类模型任务中最实验性和科学的部分。
从泛化的主题开始,我们解释了模型可能出错的地方以及为什么总是很重要通过训练/测试分割、通过自助法和交叉验证来检查您工作的真实性能(尽管我们建议更多地使用后者进行验证工作而不是一般评估本身)。
模型复杂度作为估计中方差的一个来源,给了我们引入变量选择的机会,首先是通过贪婪选择特征,无论是单变量还是多变量,然后使用正则化技术,如岭回归、Lasso 和弹性网络。
最后,我们展示了 Lasso 的一个强大应用,称为稳定性选择,根据我们的经验,我们推荐您在许多特征选择问题中尝试这种方法。
在下一章中,我们将处理增量增长数据集的问题,提出即使您的数据集太大,难以及时装入工作计算机内存的问题,也可能有效的解决方案。
第七章。在线和批量学习
在本章中,您将了解在大数据上训练分类器的最佳实践。在接下来的几页中公开的新方法既可扩展又通用,使其非常适合具有大量观测值的数据集。此外,这种方法还可以让您处理流数据集——即观测值在飞行中传输且不是同时可用的数据集。此外,这种方法通过在训练过程中输入更多数据来提高精度。
与书中迄今为止看到的经典方法(批量学习)相比,这种新的方法不出所料地被称为在线学习。在线学习的核心是 divide et impera(分而治之)原则,即数据的小批量中的每一步都作为训练和改进分类器的输入。
在本章中,我们将首先关注批量学习及其局限性,然后介绍在线学习。最后,我们将提供一个大数据的例子,展示结合在线学习和哈希技巧的好处。
批量学习
当监督任务开始时数据集完全可用,并且不超过您机器上的 RAM 数量时,您可以使用批量学习来训练分类器或回归。如前几章所见,在学习过程中,学习器会扫描整个数据集。这也发生在使用基于 随机梯度下降(SGD)的方法时(见第二章,接近简单线性回归和第三章,实际中的多重回归)。现在让我们比较训练线性回归器所需的时间,并将其性能与数据集中的观测值数量(即特征矩阵 X 的行数)和特征数量(即 X 的列数)联系起来。在这个第一个实验中,我们将使用 Scikit-learn 提供的普通LinearRegression()和SGDRegressor()类,并且我们将存储拟合分类器实际花费的时间,而不进行任何并行化。
让我们首先创建一个函数来创建假数据集:它接受训练点的数量和特征的数目(以及可选的噪声方差)作为参数,并返回归一化的训练和测试特征矩阵以及标签。请注意,X 矩阵中的所有特征都是数值型的:
In:
import matplotlib.pyplot as plt
import matplotlib.pylab as pylab
%matplotlib inline
In:
from sklearn.preprocessing import StandardScaler
from sklearn.datasets.samples_generator import make_regression
import numpy as np
def generate_dataset(n_train, n_test, n_features, noise=0.1):
X, y = make_regression(n_samples=int(n_train + n_test),
n_features=int(n_features), noise=noise,
random_state=101)
X_train = X[:n_train]
X_test = X[n_train:]
y_train = y[:n_train]
y_test = y[n_train:]
X_scaler = StandardScaler()
X_train = X_scaler.fit_transform(X_train)
X_test = X_scaler.transform(X_test)
y_scaler = StandardScaler()
y_train = y_scaler.fit_transform(y_train)
y_test = y_scaler.transform(y_test)
return X_train, X_test, y_train, y_test
现在让我们存储训练和测试学习者在所有这些配置组合中所需的时间:
-
两个分类器:
LinearRegression()和SGDRegressor() -
观测值数量:
1000,10000和100000 -
特征数量:
10,50,100,500和1000
为了平均结果,每个训练操作执行五次。测试数据集始终包含 1000 个观察值:
In:
from sklearn.linear_model import LinearRegression, SGDRegressor
import time
In:
n_test = 1000
n_train_v = (1000, 10000, 100000)
n_features_v = (10, 50, 100, 500, 1000)
regr_v = {'LR': LinearRegression(), 'SGD': SGDRegressor(random_state=101)}
results = {}
for regr_name, regr in regr_v.items():
results[regr_name] = {}
for n_train in n_train_v:
for n_features in n_features_v:
results[regr_name][(n_train, n_features)] = {'train': [], 'pred': []}
for n_repetition in range(5):
X_train, X_test, y_train, y_test = \
generate_dataset(n_train, n_test, n_features)
tick = time.time()
regr.fit(X_train, y_train)
train_time = time.time() - tick
pred = regr.predict(X_test)
predict_time = time.time() - tick - train_time
results[regr_name][(n_train, n_features)]['train'].append(train_time)
results[regr_name][(n_train, n_features)]['pred'].append(predict_time)
最后,让我们绘制结果。在下面的屏幕截图中,左边的图表显示了 LogisticRegressor 算法的训练时间与特征数量的关系,而右边的图表显示了时间与观察数量的关系:
In:
pylab.rcParams['figure.figsize'] = 12, 6
plt.subplot(1, 2, 1)
for n_train in n_train_v:
X = n_features_v
y = [np.mean(results['LR'][(n_train, n_features)]['train'])
for n_features in n_features_v]
plt.plot(X, y, label=str(n_train) + " train points")
plt.title('Training time VS num. features')
plt.xlabel('Num features')
plt.ylabel('Training time [s]')
plt.legend(loc=0)
plt.subplot(1, 2, 2)
for n_features in n_features_v:
X = np.log10(n_train_v)
y = [np.mean(results['LR'][(n_train, n_features)]['train'])
for n_train in n_train_v]
plt.plot(X, y, label=str(n_features) + " features")
plt.title('Training time VS num. training points')
plt.xlabel('Num training points [log10]')
plt.ylabel('Training time [s]')
plt.legend(loc=0)
plt.show()
Out:

在图表中,你可以看到,对于小数据集,分类器表现相当不错,具有少量特征和观察值。而在处理最大的 X 矩阵时,1,000 个特征和 100,000 个观察值(包含 1 亿条记录),训练时间仅为 30 秒以上:这也是回归器不再可扩展的极限。
现在我们来看看测试时间的情况:
In:
plt.subplot(1, 2, 1)
for n_train in n_train_v:
X = n_features_v
y = [np.mean(results['LR'][(n_train, n_features)]['pred'])
for n_features in n_features_v]
plt.plot(X, y, label=str(n_train) + " train points")
plt.title('Prediction time VS num. features')
plt.xlabel('Num features')
plt.ylabel('Prediction time [s]')
plt.legend(loc=0)
plt.subplot(1, 2, 2)
for n_features in n_features_v:
X = np.log10(n_train_v)
y = [np.mean(results['LR'][(n_train, n_features)]['pred'])
for n_train in n_train_v]
plt.plot(X, y, label=str(n_features) + " features")
plt.title('Prediction time VS num. training points')
plt.xlabel('Num training points [log10]')
plt.ylabel('Prediction time [s]')
plt.legend(loc=0)
plt.show()
Out:

测试时间随着特征数量的增加而线性增加,并且与特征数量无关。幸运的是,在大数据中应用线性方法似乎并不是一个大问题。
现在我们来看看线性回归的 SGD 实现会发生什么:
In:
plt.subplot(1, 2, 1)
for n_train in n_train_v:
X = n_features_v
y = [np.mean(results['SGD'][(n_train, n_features)]['train'])
for n_features in n_features_v]
plt.plot(X, y, label=str(n_train) + " train points")
plt.title('Training time VS num. features')
plt.xlabel('Num features')
plt.ylabel('Training time [s]')
plt.legend(loc=0)
plt.subplot(1, 2, 2)
for n_features in n_features_v:
X = np.log10(n_train_v)
y = [np.mean(results['SGD'][(n_train, n_features)]['train'])
for n_train in n_train_v]
plt.plot(X, y, label=str(n_features) + " features")
plt.title('Training time VS num. training points')
plt.xlabel('Num training points [log10]')
plt.ylabel('Training time [s]')
plt.legend(loc=0)
plt.show()
Out:

与之前的回归器相比,结果发生了巨大变化:在最大的矩阵上,这个学习者大约需要 1.5 秒。似乎训练 SGD 回归器所需的时间与特征数量和训练点的数量呈线性关系。现在让我们验证它在测试中的表现:
In:
plt.subplot(1, 2, 1)
for n_train in n_train_v:
X = n_features_v
y = [np.mean(results['SGD'][(n_train, n_features)]['pred'])
for n_features in n_features_v]
plt.plot(X, y, label=str(n_train) + " train points")
plt.title('Prediction time VS num. features')
plt.xlabel('Num features')
plt.ylabel('Prediction time [s]')
plt.legend(loc=0)
plt.subplot(1, 2, 2)
for n_features in n_features_v:
X = np.log10(n_train_v)
y = [np.mean(results['SGD'][(n_train, n_features)]['pred'])
for n_train in n_train_v]
plt.plot(X, y, label=str(n_features) + " features")
plt.title('Prediction time VS num. training points')
plt.xlabel('Num training points [log10]')
plt.ylabel('Prediction time [s]')
plt.legend(loc=0)
plt.show()
Out:

在测试数据集上应用基于 SGD 的学习者所需的时间与其他实现方式大致相同。在这里,同样,当在大数据集上扩展解决方案时,实际上并没有真正的问题。
在线小批量学习
从上一节中,我们学到了一个有趣的教训:对于大数据,始终使用基于 SGD 的学习者,因为它们更快,并且可以扩展。
现在,在本节中,让我们考虑这个回归数据集:
-
观察数量巨大:200 万
-
特征数量众多:100
-
噪声数据集
X_train 矩阵由 2 亿个元素组成,可能无法完全适应内存(在 4GB RAM 的机器上);测试集由 10,000 个观察值组成。
让我们先创建数据集,并打印出最大的数据集的内存占用:
In:
# Let's generate a 1M dataset
X_train, X_test, y_train, y_test = generate_dataset(2000000, 10000, 100, 10.0)
print("Size of X_train is [GB]:", X_train.size * X_train[0,0].itemsize/1E9)
Out:
Size of X_train is [GB]: 1.6
X_train 矩阵本身就有 1.6GB 的数据;我们可以将其视为大数据的起点。现在让我们尝试使用上一节中得到的最佳模型 SGDRegressor() 来对其进行分类。为了评估其性能,我们使用 MAE,即平均绝对误差(对于误差评估,越低越好)。
In:
from sklearn.metrics import mean_absolute_error
regr = SGDRegressor(random_state=101)
tick = time.time()
regr.fit(X_train, y_train)
print("With SGD, after", time.time() - tick ,"seconds")
pred = regr.predict(X_test)
print("the MAE is [log10]:", np.log10(mean_absolute_error(y_test, pred)))
Out:
With SGD, after 5.958770098299116 seconds
the MAE is [log10]: -1.2422451189257
在我们的电脑上(配备 Mac OS 和 4GB 的 RAM),这个操作大约需要 6 秒钟,最终的 MAE 是 10^(-1.24)。
我们能做得更好吗?是的,通过小批量学习和在线学习。在我们看到这些实际应用之前,让我们介绍一下 SGD 如何与小批量一起工作。
-
将
X_train矩阵分成N观测的批次。由于我们使用 SGD,如果可能的话,最好对观测进行洗牌,因为该方法强烈依赖于输入向量的顺序。此时,每个小批量有N行和M列(其中M是特征的数量)。 -
我们使用小批量来训练学习器。SGD 系数的初始化是随机的,如前所述。
-
我们使用另一个小批量来训练学习器。SGD 系数的初始化是前一步的输出(使用
partial_fit方法)。 -
重复步骤 3,直到使用完所有的小批量。在每一步中,根据输入,SGD 模型的系数都会被细化并修改。
这显然是一个明智的方法,并且实现起来不会花费太多时间。你只需要为每个新批量设置每个系数的初始值,并在小批量上训练学习器。
现在,从性能的角度来看,我们使用在线学习能得到什么?
-
我们有一种增量训练模型的方法。由于在每一步我们都可以测试模型,我们可以在我们认为足够好的任何一点停止。
-
我们不需要将整个
X_train矩阵保存在内存中;我们只需要将小批量保存在 RAM 中。这也意味着消耗的 RAM 是恒定的。 -
我们有一种控制学习的方法:我们可以有小的或大的小批量。
现在我们来看看它如何表现,通过改变批量大小(即每个观测的观测数):
In:
def get_minibatch(X, y, batch_size):
# We will shuffle consistently the training observations
from sklearn.utils import resample
X, y = resample(X, y, replace=False, random_state=101)
n_cols = y.shape[0]
for i in range(int(n_cols/batch_size)):
yield (X[i*batch_size:(i+1)*batch_size, :], y[i*batch_size:(i+1)*batch_size])
if n_cols % batch_size > 0:
res_rows = n_cols % batch_size
yield (X[-res_rows:, :], y[-res_rows:])
plot_x = []
plot_y = []
plot_labels = []
for batch_size in (1000, 10000, 100000):
regr = SGDRegressor(random_state=101)
training_time = 0.0
X = []
y = []
for dataset in get_minibatch(X_train, y_train, batch_size):
tick = time.time()
regr.partial_fit(dataset[0], dataset[1])
training_time += (time.time() - tick)
pred = regr.predict(X_test)
X.append(training_time)
y.append(np.log10(mean_absolute_error(y_test, pred)))
print("Report: Mini-batch size", batch_size)
print("First output after [s]:", X[0])
print("First model MAE [log10]:", y[0])
print("Total training time [s]:", X[-1])
print("Final MAE [log10]: ", y[-1])
print()
plot_x.append(X)
plot_y.append(y)
plot_labels.append("Batch size: "+str(batch_size))
Out:

最后,最终的 MAE 总是相同的;也就是说,当两者都在整个训练集上训练时,批量学习和在线学习最终会提供相同的结果。
我们还看到,通过使用小批量(1,000 个观测),我们只需 1 毫秒就能得到一个工作模型。当然,这不是一个完美的解决方案,因为它的 MAE 仅为 10^(-0.94),但我们现在有一个合理的工作模型。
现在,让我们比较一下完全训练模型所需的时间。使用小批量,总时间大约为 1.2 秒;使用批量则超过 5 秒。MAEs 大致相等——为什么时间上有这么大的差异?因为数据集并没有全部适合 RAM,系统一直在与存储内存交换数据。
现在,让我们关注一下小批量大小:小批量是否总是更好?实际上,它将更早地产生输出,但总体上会花费更多时间。
这里是使用不同小批量大小训练学习器时,训练时间和 MAE 的图表:
In:
plt.subplot(1,2,1)
for i in range(len(plot_x)):
plt.plot(plot_x[i], plot_y[i], label=plot_labels[i])
plt.title('Mini-batch learning')
plt.xlabel('Training time [s]')
plt.ylabel('MAE')
plt.legend(loc=0)
plt.subplot(1,2,2)
for i in range(len(plot_x)):
plt.plot(plot_x[i], plot_y[i], label=plot_labels[i])
plt.title('Mini-batch learning: ZOOM 0-0.15s')
plt.xlabel('Training time [s]')
plt.ylabel('MAE')
plt.xlim([0, 0.15])
plt.legend(loc=0)
plt.show()
Out:

一个真实示例
现在,让我们结合特征哈希(见第五章,数据准备)、批量学习和 SGD。根据我们迄今为止所看到的,这应该是处理大数据的最佳方式,因为:
-
特征的数量是恒定的(特征哈希)。
-
每批次的观测数是恒定的(批量学习)。
-
它允许流式数据集。
-
算法是随机的(SGD)。
所有这些点加在一起将确保一些后果:
-
我们可以非常快速地得到一个模型(在第一个小批量之后),该模型会随着时间的推移而改进。
-
RAM 消耗是恒定的(因为每个小批量的大小完全相同)。
-
理想情况下,我们可以处理我们想要的任意多的观察值。
在现实世界的例子中,让我们使用文本输入:二十个新闻组数据集。这个数据集包含从 20 个不同的新闻组中提取的 20,000 条消息(文本内容),每个新闻组都涉及不同的主题。项目的网页是:archive.ics.uci.edu/ml/datasets/Twenty+Newsgroups。
目标是将每个文档分类到可能的标签之一(这是一个分类任务)。让我们首先加载它,并将其分为训练集和测试集。为了使其更真实,我们将从数据集中删除标题、页脚和引用的电子邮件:
In:
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import HashingVectorizer
to_remove = ('headers', 'footers', 'quotes')
data_train = fetch_20newsgroups(subset='train', random_state=101,
remove=to_remove)
data_test = fetch_20newsgroups(subset='test', random_state=101,
remove=to_remove)
labels = data_train.target_names
targets = np.unique(data_train.target)
现在让我们创建一个函数,该函数可以生成数据集的小批量:
In:
def get_minibatch_docs(docs, targets, batch_size):
n_docs = len(docs)
for i in range(int(n_docs/batch_size)):
yield (docs[i*batch_size:(i+1)*batch_size],
targets[i*batch_size:(i+1)*batch_size])
if n_docs % batch_size > 0:
res_rows = n_docs % batch_size
yield (docs[-res_rows:], targets[-res_rows:])
现在,核心任务仅仅是分类文档。我们首先通过HashingVectorizer类应用特征哈希,其输出连接到一个SGDClassifier(另一个具有partial_fit方法的类)。这一事实将确保一个额外的优势:由于HashingVectorizer的输出非常稀疏,因此使用稀疏表示,使得内存中的小批量大小更加紧凑。
要了解最佳哈希大小是多少,我们可以尝试使用1000、5000、10000、50000和100000的大小进行全搜索,然后测量每个学习者的准确度:
In:
from sklearn.linear_model import SGDClassifier
from sklearn.metrics import accuracy_score
import sys
minibatch_size = 1000
values_to_plot = {}
for hash_table_size in (1000, 5000, 10000, 50000, 100000):
values_to_plot[hash_table_size] = {'time': [], 'score': []}
vectorizer = HashingVectorizer(stop_words='english',
non_negative=True, n_features=hash_table_size,
ngram_range=(1, 1))
X_test = vectorizer.transform(data_test.data)
y_test = data_test.target
clf = SGDClassifier(loss='log')
timings = []
for minibatch in get_minibatch_docs(data_train.data, data_train.target, minibatch_size):
y_train = minibatch[1]
tick = time.time()
X_train = vectorizer.transform(minibatch[0])
clf.partial_fit(X_train, y_train, targets)
timings.append(time.time() - tick)
pred = clf.predict(X_test)
values_to_plot[hash_table_size]['score'].append(accuracy_score(y_test, pred))
values_to_plot[hash_table_size]['time'] = np.cumsum(timings)
最后,我们在表示每个哈希大小的时间准确性的图表上绘制我们的结果。图表上的X符号是当分类器输出模型时的实例(和相关准确度):
In:
for k,v in sorted(values_to_plot.items()):
plt.plot(v['time'], v['score'], 'x-', label='Hashsize size '+str(k))
plt.title('Mini-batch learning: 20newsgroups')
plt.xlabel('Training time [s]')
plt.ylabel('Accuracy')
plt.legend(loc=0)
plt.show()
Out:

从获得的结果来看,使用大于 10,000 个元素的哈希表可以让我们达到最佳性能。在这个练习中,小批量大小被固定为 1,000 个观察值;这意味着每个小批量都是一个包含 10 M 个元素的矩阵,以稀疏方式表示。这也意味着,对于每个小批量,使用的内存高达 80 MB 的 RAM。
无测试集的流式场景
在许多实际情况下,测试数据集是不可用的。我们能做什么?最佳实践将是:
-
持续获取数据,直到达到特定的批量大小;比如说 10 个观察值。
-
打乱观察值,并将其中 8 个存储在训练集中,2 个存储在测试集中(用于 80/20 的验证)。
-
在训练集上训练分类器,在测试集上测试。
-
返回步骤 1。随着每个小批量的到来,训练集将增加 10 个观察值,测试集增加 2 个。
我们刚刚描述了经典方法,该方法在数据一致且数据集不是很大时使用。如果特征在整个流中发生变化,并且你需要构建一个必须适应特征统计快速变化的学习者,那么就简单地不要使用测试集,并遵循此算法。此外,这是从大数据中学习的首选方式:
-
获取数据,直到达到小批量大小;比如说 10 个观察结果。不要打乱顺序,并使用所有观察结果来训练学习者。
-
等到你获取到另一个小批量数据。在这些观察结果上测试分类器。
-
使用上一步中接收到的那个小批量更新分类器。
-
返回到步骤 2。
这个算法的好处是,你只需要在内存中保留模型和当前的小批量数据;这些首先用于测试学习者,然后用于更新它。
摘要
在本章中,我们介绍了批量和在线学习概念,这对于能够快速且可扩展地处理大数据集(大数据)是必要的。
在下一章中,我们将探讨一些机器学习的先进技术,这些技术将为一些已知问题类别产生很好的结果。
第八章. 高级回归方法
在本章中,我们将介绍一些高级回归方法。由于其中许多方法非常复杂,我们将跳过大多数数学公式,而是提供技术背后的思想以及一些实用建议,例如解释何时以及何时不使用该技术。我们将举例说明:
-
最小角度回归(LARS)
-
贝叶斯回归
-
使用 hinge loss 的 SGD 分类(注意这不是一个回归器,而是一个分类器)
-
回归树
-
回归器的集成(袋装和提升)
-
使用最小角度偏差的梯度提升回归器
最小角度回归
尽管与 Lasso(在第六章 Chapter 6 中看到)非常相似,但最小角度回归(或简称 LARS)是一种回归算法,它以快速和智能的方式选择模型中使用的最佳特征,即使它们彼此之间非常相关。LARS 是前向选择算法(也称为前向逐步回归)和前向分步回归算法的演变。
这是前向选择算法的工作原理,基于所有变量(包括目标变量)都已被先前归一化的假设:
-
对于一个问题的所有可能的预测因子中,与目标变量 y 具有最大绝对相关性的那个被选中(即具有最大解释能力的那个)。让我们称它为 p[1]。
-
所有其他预测因子现在都投影到 p[1] 最小角度回归上,并移除投影,创建一个与 p[1] 正交的残差向量。
-
第一步在残差向量上重复进行,并再次选择相关性最高的预测因子。让我们称它为 p² 并应用下标。
-
第二步重复进行,使用 p[2],创建一个与 p[2](以及 p[1])正交的残差向量。
-
此过程持续进行,直到预测令人满意,或者当最大绝对相关性低于设定的阈值。在每次迭代后,将一个新的预测因子添加到预测因子列表中,残差与它们都正交。
这种方法并不非常受欢迎,因为它由于其极端贪婪的方法存在严重的限制;然而,它相当快。现在让我们考虑我们有一个具有两个高度相关变量的回归问题。在这个数据集上,前向选择将基于第一个或第二个变量选择预测因子,然后,由于残差将非常低,将在远晚的步骤中重新考虑另一个变量(最终,永远不会)。这一事实将导致模型上的过拟合问题。如果两个高度相关的变量一起被选中,平衡新的预测因子,不是更好吗?这正是前向分步回归算法的核心思想,其中,在每一步中,最佳预测因子部分地添加到模型中。让我们在这里提供详细情况:
-
在该模型中,每个特征都有一个关联权重为零——也就是说,对于每个特征 i,w[i] = 0。
-
对于一个问题的所有可能的预测因子中,与目标变量 y 具有最大(绝对)相关性的那个部分添加到模型中——也就是说,在模型中,w[i] 的权重增加 ε。
-
重复步骤 2,直到探索能力低于预定义的阈值。
与前向选择相比,这种方法有很大的改进,因为在相关特征的情况下,两者都将以相似权重出现在最终模型中。结果是很好的,但创建模型所需的巨大迭代次数是这个算法真正的大问题。再次,由于运行时间,这种方法变得不切实际。
LARS 算法的工作方式如下:
-
在模型中,每个特征都有一个关联权重为零——也就是说,对于每个特征 i,w[i] = 0。
-
对于一个问题的所有可能的预测因子中,与目标变量 y 具有最大(绝对)相关性的那个部分添加到模型中——也就是说,在模型中,w[i] 的权重增加 ε。
-
继续增加 w[i],直到任何其他预测因子(比如说 j)与残差向量的相关性等于当前预测因子的相关性。
-
同时增加 w[i] 和 w[j],直到另一个预测因子与残差向量的相关性等于当前预测因子的相关性。
-
继续添加预测因子和权重,直到所有预测因子都在模型中,或者达到另一个终止标准,例如迭代次数。
这种解决方案能够组合最佳的前向选择和分步回归的部分,创建一个稳定、不太容易过拟合且快速的解决方案。在到达示例之前,你可能想知道为什么它被称为最小角度回归。答案非常简单:如果特征和输出在笛卡尔空间中表示为向量,在每次迭代中,LARS 都会将与残差向量最相关的变量包含在模型中,这是与残差产生最小角度的那个变量。实际上,整个过程可以直观地表达。
LARS 的视觉展示

这里是视觉情况:两个预测因子(x1和x2),不一定正交,以及目标(y)。请注意,一开始,残差对应于目标。我们的模型从u0(所有权重都是0)开始。
然后,由于x2与残差之间的角度比x1小,我们开始向x2的方向行走,同时我们继续计算残差向量。现在,一个问题:我们应该在哪里停止?

我们应该在u1处停止,此时残差与x1之间的角度与残差与x2之间的角度相同。然后我们沿着x1和x2的合成方向前进,达到y。

代码示例
让我们现在在 Python 中使用 Diabetic 数据集来观察 LARS 的实际操作,该数据集包含 10 个数值变量(年龄、性别、体重、血压等),在 442 名患者身上测量,并在一年后显示疾病进展情况。首先,我们想可视化系数的权重路径。为此,lars_path()类对我们有所帮助(尤其是如果其训练是冗长的):
In:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from sklearn import linear_model
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
diabetes = datasets.load_diabetes()
X = StandardScaler().fit_transform(diabetes.data)
y = StandardScaler(with_mean=True, with_std=False) \
.fit_transform(diabetes.target)
alphas, _, coefs = linear_model.lars_path(X, y, verbose=2)
xx = np.sum(np.abs(coefs.T), axis=1)
xx /= xx[-1]
plt.plot(xx, coefs.T)
ymin, ymax = plt.ylim()
plt.vlines(xx, ymin, ymax, linestyle='dashed')
plt.xlabel('|coef| / max|coef|')
plt.ylabel('Coefficients')
plt.axis('tight')
plt.show()
Out:


在输出表中,你可以看到模型中插入的第一个特征是数字 2,接着是数字 8,以此类推。在图像中,相反,你可以同时看到系数的值(彩色线条)和步骤(虚线线条)。记住,在每一步,一个系数变为非零,模型中的所有系数都是线性更新的。在图像的右侧,你可以找到权重的最终值。
这是查看 LARS 系数的图形方式;如果我们只需要回归器(正如我们在前面的章节中看到的),我们只需使用Lars类:
In:
regr = linear_model.Lars()
regr.fit(X, y)
print("Coefficients are:", regr.coef_)
Out:
Coefficients are:
[ -0.47623169 -11.40703082 24.72625713 15.42967916 -37.68035801
22.67648701 4.80620008 8.422084 35.73471316 3.21661161]
如你所预期,回归对象可以使用.fit方法进行拟合,其权重(系数)正是之前截图中所显示的。为了获得模型的质量,与其他回归器类似,你可以使用 score 方法。关于训练数据,以下是评分输出:
In:
print("R2 score is", regr.score(X,y))
Out:
R2 score is 0.517749425413
LARS 总结
优点:
-
系数更新的智能方式产生了低过拟合
-
模型直观且易于解释
-
训练速度与 Forward Selection 相同
-
当特征数量与观测数量相当或更大时,它非常出色
缺点:
-
当特征数量非常大时,即特征数量远大于观测数量时,它可能工作得不是很好,因为在这种情况下,你很可能找到虚假的相关性
-
它不适用于非常嘈杂的特征
贝叶斯回归
贝叶斯回归与线性回归类似,如第三章《实际中的多重回归》中所述,但它不是预测一个值,而是预测其概率分布。让我们从一个例子开始:给定X,训练观察矩阵,和y,目标向量,线性回归创建一个模型(即一系列系数),该模型拟合与训练点具有最小误差的线。然后,当新的观察值到来时,模型应用于该点,并输出一个预测值。这就是线性回归的唯一输出,无法得出结论,该特定点的预测是否准确。让我们用一个非常简单的代码示例来说明:观察现象只有一个特征,观察的数量仅为10:
In:
from sklearn.datasets import make_classification
from sklearn.datasets import make_regression
X, y = make_regression(n_samples=10, n_features=1, n_informative=1, noise=3, random_state=1)
现在,让我们拟合一个经典的线性回归模型,并尝试预测训练支持范围外的回归值(在这个简单示例中,我们预测一个x值是训练值最大值两倍的点的值):
In:
regr = linear_model.LinearRegression()
regr.fit(X, y)
test_x = 2*np.max(X)
pred_test_x = regr.predict(test_x)
pred_test_x
Out:
array([ 10.79983753])
现在我们来绘制训练点、拟合线和预测的测试点(图像最右侧的点):
In:
plt.scatter(X, y)
x_bounds = np.array([1.2*np.min(X), 1.2*np.max(X)]).reshape(-1, 1)
plt.plot(x_bounds, regr.predict(x_bounds) , 'r-')
plt.plot(test_x, pred_test_x, 'g*')
plt.show()
Out:

要有一个预测值的概率密度函数,我们应该从开始就改变假设和线性回归器中的某些步骤。由于这是一个高级算法,涉及的数学非常复杂,我们更愿意传达方法背后的思想,而不是展示一页又一页的数学公式。
首先,我们只能推断预测值的分布,如果每个变量都被建模为分布。实际上,这个模型中的权重被视为具有正态分布的随机变量,以零为中心(即球形的高斯分布)并且具有未知的方差(从数据中学习)。该算法施加的正则化与岭回归设置的正则化非常相似。
预测的输出是一个值(与线性回归完全一样)和一个方差值。使用该值作为均值,方差作为实际方差,我们就可以表示输出的概率分布:
In:
regr = linear_model.BayesianRidge()
regr.fit(X, y)
Out:
BayesianRidge(alpha_1=1e-06, alpha_2=1e-06, compute_score=False,
copy_X=True, fit_intercept=True, lambda_1=1e-06,
lambda_2=1e-06, n_iter=300, normalize=False,
tol=0.001, verbose=False)
In:
from matplotlib.mlab import normpdf
mean = regr.predict(test_x)
stddev = regr.alpha_
plt_x = np.linspace(mean-3*stddev, mean+3*stddev,100)
plt.plot(plt_x, normpdf(plt_x, mean, stddev))
plt.show()
Out:

贝叶斯回归总结
优点:
-
对高斯噪声的鲁棒性
-
如果特征数量与观察数量相当,则非常好
缺点:
-
耗时
-
对变量施加的假设通常与实际情况相差甚远
使用 hinge 损失的 SGD 分类
在第四章 Chapter 4 中,逻辑回归,我们探讨了基于回归器的分类器,逻辑回归。其目标是拟合与一个点被分类为标签的概率相关的最佳概率函数。现在,算法的核心功能考虑了数据集的所有训练点:如果它只基于边界点呢?这正是线性 支持向量机 (SVM) 分类器的情况,其中通过仅考虑接近分离边界的点来绘制线性决策平面。
除了在支撑向量(边界最近的点)上工作之外,SVM 还使用了一种新的决策损失,称为 hinge。以下是它的公式:

其中 t 是点 x 的预期标签,w 是分类器中的权重集。Hinge 损失有时也称为 softmax,因为它实际上是一个截断的最大值。在这个公式中,只使用了边界点(即支撑向量)。
在第一种情况下,这个函数虽然凸,但不可微,因此基于随机梯度下降(SGD)的方法在理论上是无效的。在实践中,由于它是一个连续函数,它有一个分段导数。这导致 SGD 可以在这个技术中被积极用于推导出快速和近似的解。
这里有一个 Python 中的例子:让我们使用 SGDClassifier 类(如第四章 Chapter 4 中所示),逻辑回归,并使用 hinge 损失,应用于从 2 个类别中抽取的 100 个点的数据集。通过这段代码,我们感兴趣的是看到分类器的决策边界和选择的支撑向量:
In:
from sklearn.linear_model import SGDClassifier
# we create 50 separable points
X, y = make_classification(n_samples=100, n_features=2,
n_informative=2, n_redundant=0,
n_clusters_per_class=1, class_sep=2,
random_state=101)
# fit the model
clf = SGDClassifier(loss="hinge", n_iter=500, random_state=101,
alpha=0.001)
clf.fit(X, y)
# plot the line, the points, and the nearest vectors to the plane
xx = np.linspace(np.min(X[:,0]), np.max(X[:,0]), 10)
yy = np.linspace(np.min(X[:,1]), np.max(X[:,1]), 10)
X1, X2 = np.meshgrid(xx, yy)
Z = np.empty(X1.shape)
for (i, j), val in np.ndenumerate(X1):
x1 = val
x2 = X2[i, j]
p = clf.decision_function([[x1, x2]])
Z[i, j] = p[0]
levels = [-1.0, 0.0, 1.0]
linestyles = ['dashed', 'solid', 'dashed']
plt.contour(X1, X2, Z, levels, colors='k', linestyles=linestyles)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.Paired)
plt.show()
Out:

图像展示了属于两个类别的点(左右两侧的点)和决策边界(类别之间的实线)。此外,它还包含两条虚线,它们连接每个类别的支撑向量(即这些线上的点是支撑向量)。决策边界简单地说是它们之间相同距离的线。
与逻辑回归的比较
逻辑回归学习者的目的是利用训练集的所有输入点,并输出一个概率。相反,带有铰链损失的 SGD 直接产生标签,并且只使用边界点来改进模型。它们的性能如何?让我们用一个具有 20 个特征(其中 5 个是有信息的,5 个是冗余的,10 个是随机的)和 10,000 个观察值的合成数据集进行测试。然后,我们将数据分为 70/30 作为训练集和测试集,并训练两个 SGD 分类器:一个带有铰链损失函数,另一个带有逻辑损失函数。最后,我们比较它们在测试集上的预测准确率:
In:
from sklearn.cross_validation import train_test_split
from sklearn.metrics import accuracy_score
X, y = make_classification(n_samples=10000, n_features=20,
n_informative=5, n_redundant=5,
n_clusters_per_class=2, class_sep=1,
random_state=101)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=101)
clf_1 = SGDClassifier(loss="hinge", random_state=101)
clf_1.fit(X_train, y_train)
clf_2 = SGDClassifier(loss="log", random_state=101)
clf_2.fit(X_train, y_train)
print('SVD : ', accuracy_score(y_test, clf_1.predict(X_test)))
print('Log. Regression: ', accuracy_score(y_test, clf_2.predict(X_test)))
Out:
SVD : 0.814333333333
Log. Regression: 0.756666666667
按照惯例,SVM 通常比逻辑回归更准确,但其性能并不出众。然而,SVM 在训练过程中的速度较慢;实际上,在训练时间方面,逻辑回归比 SVM 快 30% 以上。
In:
%timeit clf_1.fit(X_train, y_train)
Out:
100 loops, best of 3: 3.16 ms per loop
In:
%timeit clf_2.fit(X_train, y_train)
Out:
100 loops, best of 3: 4.86 ms per loop
SVR
对于线性回归器/逻辑回归,即使 SVM 有回归对应物,称为 支持向量回归器 (SVR)。它的数学公式非常长,超出了本书的范围。然而,由于它非常有效,我们认为描述它在实践中是如何工作的很重要,特别是应用于波士顿数据集并与线性回归模型进行比较:
In:
from sklearn.svm import SVR
from sklearn.linear_model import SGDRegressor
from sklearn.metrics import mean_absolute_error
from sklearn.datasets import load_boston
boston = load_boston()
X = StandardScaler().fit_transform(boston['data'])
y = boston['target']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=101)
regr_1 = SVR(kernel='linear')
regr_1.fit(X_train, y_train)
regr_2 = SGDRegressor(random_state=101)
regr_2.fit(X_train, y_train)
print('SVR : ', mean_absolute_error(y_test, regr_1.predict(X_test)))
print('Lin. Regression: ', mean_absolute_error(y_test, regr_2.predict(X_test)))
Out:
SVR : 3.67434988716
Lin. Regression: 3.7487663498
SVM 总结
优点如下:
-
可以使用 SGD 加速处理
-
输出通常比逻辑回归更准确(因为只有边界点在公式中)
缺点如下:
-
如果两个类别的点线性可分,则效果非常好,尽管对于非线性可分类别的扩展也是可用的。在这种情况下,尽管复杂性非常高,但结果通常仍然很好。
-
至于逻辑回归,它可以用于二分类问题。
回归树(CART)
一个非常常见的学习者,最近由于其速度而广泛使用,是回归树。它是一个非线性学习者,可以处理分类和数值特征,并且可以交替用于分类或回归;这就是为什么它通常被称为 分类和回归树 (CART)。在本节中,我们将了解回归树是如何工作的。
树由一系列节点组成,这些节点将分支分成两个子节点。然后,每个分支都可以进入另一个节点,或者保持为叶子节点,带有预测值(或类别)。
从根节点(即整个数据集)开始:
-
同时确定用于分割数据集的最佳特征F1以及最佳分割值。如果特征是数值型,分割值是一个阈值T1:在这种情况下,左子节点分支将是F1低于T1的观测值的集合,而右子节点分支是F1大于或等于T1的观测值的集合。如果特征是分类型,分割是在子集S1上进行的:F1特征属于这些级别的观测值组成左子节点分支,其余的观测值组成右子节点分支。
-
然后该操作(独立地)对每个分支再次运行,递归地,直到没有更多分裂的机会。
-
当分裂完成后,会创建一个叶子节点。叶子表示输出值。
你可以立即看到预测是即时的:你只需从根节点遍历到叶子节点,并在每个节点检查一个特征是否低于(或不低于)一个阈值,或者,是否有值在(或不在)一个集合内。
作为总结,我们讨论如何定义最佳的分割特征。关于最佳值或子集呢?对于回归树,我们使用方差减少的标准:在每个节点,对所有特征以及在该特征的所有值或级别进行广泛搜索。选择能够实现左右分支相对于输入集最佳方差组合,并将其标记为最佳。
注意,回归树为每个节点决定最优分割。这种局部优化方法不幸地导致次优结果。此外,建议对回归树进行剪枝;也就是说,你应该移除一些叶子节点以防止过拟合(例如,通过设置方差减少测量的最小阈值)。这就是回归树的缺点。另一方面,它们在某种程度上是准确的,并且相对快速地进行训练和测试。
在代码中,回归树与其他回归器一样简单:
In:
from sklearn.tree import DecisionTreeRegressor
regr = DecisionTreeRegressor(random_state=101)
regr.fit(X_train, y_train)
mean_absolute_error(y_test, regr.predict(X_test))
Out:
3.2842105263157895
回归树总结
优点:
-
它可以模拟非线性行为
-
非常适合分类特征和数值特征,无需归一化
-
分类和回归使用相同的方法
-
快速训练,快速预测时间,内存占用小
缺点:
-
随机算法:它不优化完整解,只优化最佳选择。
-
当特征数量很多时,效果不佳。
-
叶子可以非常具体。在这种情况下,我们需要“剪枝树”,移除一些节点。
Bagging 和 boosting
Bagging 和 boosting 是两种用于结合学习者的技术。这些技术被归类为通用名称集成(或元算法),因为最终目标实际上是将弱学习者集成起来,以创建一个更复杂但更准确的模型。弱学习者的定义并不正式,但理想情况下,它是一个快速、有时是线性的模型,不一定产生优秀的结果(只要它们比随机猜测好就足够了)。最终的集成通常是具有非线性学习者的模型,其性能随着模型中弱学习者数量的增加而提高(注意,这种关系是严格非线性的)。现在让我们看看它们是如何工作的。
Bagging
Bagging 代表Bootstrap Aggregating,其最终目标是通过对弱学习者的结果进行平均来减少方差。现在让我们看看代码;我们将解释它是如何工作的。作为一个数据集,我们将重用之前示例中的波士顿数据集(及其验证分割):
In:
from sklearn.ensemble import BaggingRegressor
bagging = BaggingRegressor(SGDRegressor(), n_jobs=-1,
n_estimators=1000, random_state=101,
max_features=0.8)
bagging.fit(X_train, y_train)
mean_absolute_error(y_test, bagging.predict(X_test))
Out:
3.8345485952100629
Scikit-learn 的BaggingRegressor类是创建 Bagging 回归器的基类。它需要弱学习者(在示例中,它是一个SGDRegressor),回归器的总数(1,000),以及每个回归器中要使用的最大特征数(总数的 80%)。然后,Bagging 学习者就像之前看到的其他学习者一样,通过 fit 方法进行训练。在这个阶段,对于每个弱学习者:
-
X 训练数据集组成特征的 80%是随机选择的
-
弱学习者在训练集的替换样本的 bootstrap 上仅针对选定的特征进行训练
最后,Bagging 模型包含 1,000 个训练好的SGDRegressors。当从集成请求预测时,每个弱学习者都会做出预测,然后对结果进行平均,产生集成预测。
请注意,训练和预测操作都是针对弱学习者的;因此,它们可以在多个 CPU 上并行化(这就是为什么示例中的n_jobs是-1的原因;即,我们使用所有核心)。
最终结果,从 MAE 的角度来看,应该比单个SGDRegressor更好;另一方面,模型大约复杂了 1,000 倍。
通常,集成与决策树或回归树相关联。在这种情况下,回归集成的名称变为随机森林回归器(即,由多个树组成的森林)。由于这种技术通常用作默认的 Bagging 集成,Scikit-learn 中有一个专门的类:
In:
from sklearn.ensemble import RandomForestRegressor
regr = RandomForestRegressor(n_estimators=100,
n_jobs=-1, random_state=101)
regr.fit(X_train, y_train)
mean_absolute_error(y_test, regr.predict(X_test))
Out:
2.6412236842105261
随机森林的一个额外特性是它们能够对模型中的特征重要性进行排序(即,它们检测哪些特征产生了预测变量的最高变异)。以下是代码;请始终记住首先对特征矩阵进行归一化(我们已经在上一节中这样做过了):
In:
sorted(zip(regr.feature_importances_, boston['feature_names']),
key=lambda x: -x[0])
Out:

列表按从最重要的特征到最不重要的特征排序(对于这个集成)。如果你更改弱学习者或任何其他参数,这个列表可能会改变。
提升
提升是一种结合(集成)弱学习者的方法,主要是为了减少预测偏差。与袋装不同,提升产生了一系列的预测者,其中每个输出都是下一个学习者的输入。我们将从一个例子开始,就像我们在前面的子节中做的那样:
In:
from sklearn.ensemble import AdaBoostRegressor
booster = AdaBoostRegressor(SGDRegressor(), random_state=101,
n_estimators=100, learning_rate=0.01)
booster.fit(X_train, y_train)
mean_absolute_error(y_test, booster.predict(X_test))
Out:
3.8621128094354349
来自 Scikit-learn 的 submodule 集成中的 AdaBoostRegressor 类是创建提升回归器的基类。至于袋装,它需要一个弱学习者(一个 SGDRegressor),回归器的总数(100),以及学习率(0.01)。从一个未拟合的集成开始,对于每个弱学习者,训练如下:
-
给定训练集,已经拟合好的学习者的级联产生了一个预测
-
实际值与预测值之间的误差,乘以学习率,被计算
-
在那个错误集上训练一个新的弱学习者,并将其插入到已经训练好的学习者的级联的最后阶段
在训练阶段结束时,集成包含 100 个训练好的 SGDRegressors,它们以级联的形式组织。当从集成请求预测时,最终值是一个递归操作:从最后阶段开始,输出值是前一个阶段的预测值加上学习率乘以当前阶段的预测值。
学习率类似于随机梯度下降中的学习率。较小的学习率需要更多步骤来接近结果,但输出将更精细。较大的学习率需要更少的步骤,但可能接近一个不太准确的结果。
请注意,由于训练和测试不能独立地对每个弱学习者进行,因为要训练一个模型你需要前一个输出链,所以这限制了 CPU 的使用仅限于一个,限制了级联的长度。
在使用决策/回归树进行提升的情况下,Scikit-learn 包提供了一个预构建的类,称为 GradientBoostingRegressor。一个简短的代码片段应该足以演示它是如何工作的:
In:
from sklearn.ensemble import GradientBoostingRegressor
regr = GradientBoostingRegressor(n_estimators=500,
learning_rate=0.01,
random_state=101)
regr.fit(X_train, y_train)
mean_absolute_error(y_test, regr.predict(X_test))
Out:
2.6148878419996806
即使使用提升,也有可能对特征重要性进行排序。事实上,这正是相同的方法:
In:
sorted(zip(regr.feature_importances_, boston['feature_names']),
key=lambda x: -x[0])
Out:

集成总结
优点如下:
-
基于弱学习者的强学习者
-
它们使随机学习成为可能
-
过程的随机性产生了一个稳健的解决方案
缺点如下:
-
训练时间相当长,以及内存占用
-
在提升集成中,学习步骤(alpha)的设置可能非常棘手,类似于随机梯度下降中的更新步骤
带有 LAD 的梯度提升回归器
这不仅仅是一种新技术,它还是一种技术集合,这些技术已经在本书中出现过,并引入了一个新的损失函数,即最小绝对偏差(LAD)。与之前章节中提到的最小二乘函数相比,使用 LAD 计算误差的 L1 范数。
基于 LAD 的回归学习器通常是鲁棒的但不够稳定,因为损失函数存在多个最小值(因此导致多个最佳解)。单独来看,这个损失函数似乎价值不大,但与梯度提升相结合,它创建了一个非常稳定的回归器,因为提升算法克服了 LAD 回归的限制。通过代码,这非常简单实现:
In:
from sklearn.ensemble import GradientBoostingRegressor
regr = GradientBoostingRegressor('lad',
n_estimators=500,
learning_rate=0.1,
random_state=101)
regr.fit(X_train, y_train)
mean_absolute_error(y_test, regr.predict(X_test))
Out:
2.6216986613160258
记得指定使用'lad'损失,否则默认使用最小二乘(L²)损失。此外,另一个损失函数huber结合了最小二乘损失和最小绝对偏差损失,创建了一个更加鲁棒的损失函数。要尝试它,只需在最后一段代码中插入字符串值'huber'代替'lad'。
基于 LAD 的 GBM 总结
优点是它结合了提升集成算法的强度与 LAD 损失,产生了一个非常稳定且鲁棒的学习者;缺点是训练时间非常高(与连续训练 N 个 LAD 学习器的时间完全相同)。
摘要
本章总结了我们在本书中关于回归方法的漫长旅程。我们看到了如何处理不同类型的回归建模,如何预处理数据,以及如何评估结果。在本章中,我们简要介绍了某些前沿技术。在下一章,也是最后一章中,我们将回归应用于现实世界的例子,并邀请您尝试一些具体的例子。
第九章。回归模型的实际应用
我们已经到达了本书的结尾章节。与前面的章节相比,本章在本质上非常实用,因为它主要包含大量代码,而没有数学或其他理论解释。它包括使用线性模型解决的实际数据科学问题的四个实例。最终目标是展示如何处理此类问题以及如何发展其解决方案背后的推理,以便它们可以作为类似挑战的蓝图使用。
对于每个问题,我们将描述要回答的问题,提供数据集的简要描述,并决定我们力求最大化的指标(或我们想要最小化的错误)。然后,在代码中,我们将提供成功完成每个问题的关键思想和直觉。此外,当运行代码时,模型将产生详细的输出,以便为读者提供决定下一步所需的所有信息。由于空间限制,输出将被截断,只包含关键行(截断的行在输出中用[…]表示),但在您的屏幕上,您将看到完整的画面。
小贴士
在本章中,每个部分都提供了一个独立的 IPython Notebook。它们是不同的问题,每个都是独立开发和展示的。
下载数据集
在本书的这一部分,我们将下载本章示例中将要使用的所有数据集。我们选择将它们存储在包含 IPython Notebook 的同一文件夹的单独子目录中。请注意,其中一些相当大(100+ MB)。
小贴士
我们要感谢 UCI 数据集存档的维护者和创建者。多亏了这样的存储库,建模和实现实验可重复性比以前容易得多。UCI 存档来自 Lichman, M. (2013)。UCI 机器学习仓库 [archive.ics.uci.edu/ml]。加州大学欧文分校,信息与计算机科学学院,加州,欧文市。
对于每个数据集,我们首先下载它,然后展示前几行。首先,这有助于证明文件是否已正确下载、解包并放置在正确的位置;其次,它将展示文件本身的结构(标题、字段等):
In:
try:
import urllib.request as urllib2
except:
import urllib2
import requests, io, os
import zipfile, gzip
def download_from_UCI(UCI_url, dest):
r = requests.get(UCI_url)
filename = UCI_url.split('/')[-1]
print ('Extracting in %s' % dest)
try:
os.mkdir(dest)
except:
pass
with open (os.path.join(dest, filename), 'wb') as fh:
print ('\tdecompression %s' % filename)
fh.write(r.content)
def unzip_from_UCI(UCI_url, dest):
r = requests.get(UCI_url)
z = zipfile.ZipFile(io.BytesIO(r.content))
print ('Extracting in %s' % dest)
for name in z.namelist():
print ('\tunzipping %s' % name)
z.extract(name, path=dest)
def gzip_from_UCI(UCI_url, dest):
response = urllib2.urlopen(UCI_url)
compressed_file = io.BytesIO(response.read())
decompressed_file = gzip.GzipFile(fileobj=compressed_file)
filename = UCI_url.split('/')[-1][:-4]
print ('Extracting in %s' % dest)
try:
os.mkdir(dest)
except:
pass
with open( os.path.join(dest, filename), 'wb') as outfile:
print ('\tgunzipping %s' % filename)
cnt = decompressed_file.read()
outfile.write(cnt)
时间序列问题数据集
数据集来源:Brown, M. S., Pelosi, M. & Dirska, H. (2013)。用于道琼斯指数股票财务预测的动态半径物种保守遗传算法。模式识别中的机器学习和数据挖掘,7988,27-41。
In:
UCI_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00312/dow_jones_index.zip'
unzip_from_UCI(UCI_url, dest='./dji')
Out:
Extracting in ./dji
unzipping dow_jones_index.data
unzipping dow_jones_index.names
In:
! head -2 ./dji/dow_jones_index.data
Out:

回归问题数据集
数据集来源:Thierry Bertin-Mahieux, Daniel P.W. Ellis, Brian Whitman, 和 Paul Lamere. 《百万歌曲数据集》。在 2011 年第 12 届国际音乐信息检索学会(ISMIR)会议论文集中。
In:
UCI_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00203/YearPredictionMSD.txt.zip'
unzip_from_UCI(UCI_url, dest='./msd')
Out:
Extracting in ./msd
unzipping YearPredictionMSD.txt
In:
! head -n 2 ./msd/YearPredictionMSD.txt
Out:

多类分类问题数据集
数据集来源:Salvatore J. Stolfo, Wei Fan, Wenke Lee, Andreas Prodromidis, 和 Philip K. Chan. 基于成本的建模与评估用于数据挖掘:应用于欺诈和入侵检测的 JAM 项目结果。
In:
UCI_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/kddcup99-mld/kddcup.data.gz'
gzip_from_UCI(UCI_url, dest='./kdd')
Out:
Extracting in ./kdd
gunzipping kddcup.dat
In:
!head -2 ./kdd/kddcup.dat
Out:

排名问题数据集
创建者/捐赠者:Jeffrey C. Schlimmer
In:
UCI_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/autos/imports-85.data'
download_from_UCI(UCI_url, dest='./autos')
Out:
Extracting in ./autos
decompression imports-85.data
In:
!head -2 ./autos/imports-85.data
Out:

回归问题
给定一些歌曲的描述符,这个问题的目标是预测歌曲的生产年份。这基本上是一个回归问题,因为要预测的目标变量是一个介于 1922 年和 2011 年之间的数字。
对于每首歌曲,除了生产年份外,还提供了 90 个属性。所有这些都与音色相关:其中 12 个与音色平均值相关,78 个属性描述了音色的协方差;所有特征都是数值(整数或浮点数)。
数据集由超过五十万个观测值组成。至于数据集背后的竞赛,作者尝试使用前 463,715 个观测值作为训练集,剩余的 51,630 个用于测试。
用于评估结果的指标是预测年份与测试集中歌曲的实际生产年份之间的平均绝对误差(MAE)。目标是使误差度量最小化。
注意
这个问题的完整描述和附加信息(关于特征提取阶段)可以在以下网站上找到:archive.ics.uci.edu/ml/datasets/YearPredictionMSD
现在,让我们从一些 Python 代码开始。首先,我们加载数据集(记住,如果这个操作失败,意味着你应在运行上一节中的程序之前自行下载数据集)。然后,我们根据数据集提供的指南分割训练和测试部分。最后,我们打印出结果 DataFrame 的大小(以兆字节为单位),以便了解数据集的内存占用情况:
In:
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import pandas as pd
dataset = pd.read_csv('./msd/YearPredictionMSD.txt',
header=None).as_matrix()
In:
X_train = dataset[:463715, 1:]
y_train = np.asarray(dataset[:463715, 0])
X_test = dataset[463715:, 1:]
y_test = np.asarray(dataset[463715:, 0])
In:
print("Dataset is MB:", dataset.nbytes/1E6)
del dataset
Out:
Dataset is MB: 375.17116
由于数据集接近 400 MB,因此它并不算特别小。因此,我们应该非常聪明地使用任何适当的技巧来处理它,以免耗尽内存(并且严重依赖交换文件)或甚至使我们的操作系统崩溃。
现在我们为比较设定一个基线:我们将使用普通的线性回归(vanilla 表示没有额外的风味;就像普通的冰淇淋一样,我们的模型使用标准的超参数)。然后我们打印训练时间(以秒为单位),训练集的 MAE 和测试集的 MAE:
In:
from sklearn.linear_model import LinearRegression, SGDRegressor
from sklearn.cross_validation import KFold
from sklearn.metrics import mean_absolute_error
import time
In:
regr = LinearRegression()
tic = time.clock()
regr.fit(X_train, y_train)
print("Training time [s]:", time.clock()-tic)
print("MAE train set:", mean_absolute_error(y_train,
regr.predict(X_train)))
print("MAE test set:", mean_absolute_error(y_test,
regr.predict(X_test)))
Out:
Training time [s]: 9.989145000000002
MAE train set: 6.79557016727
MAE test set: 6.80049646319
使用线性回归,我们可以在大约 10 秒内实现 6.8 的 MAE。此外,学习器似乎稳定且健壮,因为训练集和测试集的 MAE 之间没有差异(归功于线性回归的泛化能力)。现在让我们尝试做得更好。我们测试了一种随机梯度下降的变化,看看我们是否能够更快地实现更好的 MAE(最终)。我们尝试了少量和大量迭代:
In:
regr = SGDRegressor()
tic = time.clock()
regr.fit(X_train, y_train)
print("Training time [s]:", time.clock()-tic)
print("MAE train set:", mean_absolute_error(y_train,
regr.predict(X_train)))
print("MAE test set:", mean_absolute_error(y_test,
regr.predict(X_test)))
Out:
Training time [s]: 1.5492949999999972
MAE train set: 3.27482912145e+15
MAE test set: 3.30350427822e+15
In:
regr = SGDRegressor(n_iter=100)
tic = time.clock()
regr.fit(X_train, y_train)
print("Training time [s]:", time.clock()-tic)
print("MAE train set:", mean_absolute_error(y_train,
regr.predict(X_train)))
print("MAE test set:", mean_absolute_error(y_test,
regr.predict(X_test)))
Out:
Training time [s]: 24.713879
MAE train set: 2.12094618827e+15
MAE test set: 2.14161266897e+15
结果似乎表明,随机梯度下降回归不适用于数据集的形状:要获得更好的结果可能需要极大的毅力,并且需要非常长的时间。
现在,我们有两种选择:第一种是求助于高级分类器(如集成分类器);否则,我们可以通过特征工程微调模型。对于这个问题,让我们选择第二种选择,因为本书的主要目标是研究线性模型。强烈建议读者尝试第一种方法进行比较。
现在我们尝试使用特征的多项式展开,然后进行特征选择步骤。这确保了我们拥有所有基于问题特征构建的特征,以便选择最佳特征并通过线性回归器运行。
由于我们事先不知道最佳特征数量,让我们将其视为一个参数,并绘制训练集和测试集的 MAE 作为变量。此外,由于我们正在进行回归任务,特征选择应该针对最佳的回归特征;因此,使用回归的 F 分数来对和选择前 K 个特征进行排序。
我们立即遇到了一个问题:多项式特征扩展创建了如此多的额外特征。由于我们在处理一个非常大的数据集,我们可能不得不对训练集进行子采样。让我们首先计算多项式扩展后的特征数量:
In:
from sklearn.preprocessing import PolynomialFeatures
PolynomialFeatures().fit_transform(X_train[:10,:]).shape[1]
Out:
4186
当特征超过 4,000 个时,我们应该选择至少多 10 倍的数据观察,以避免过度拟合的风险。我们打乱数据集,并选择其十二分之一(因此观察数量约为 40,000)。为了管理这一点,我们可以使用 K 折交叉验证,并仅选择第一部分的测试集索引:
In:
from sklearn.pipeline import Pipeline
from sklearn import feature_selection
from sklearn.feature_selection import SelectKBest
import gc
folds = 12
train_idx = list(KFold(X_train.shape[0], folds, random_state=101, shuffle=True))[0][1]
to_plot = []
for k_feat in range(50, 2001, 50):
gc.collect()
print('---------------------------')
print("K = ", k_feat)
poly = PolynomialFeatures()
regr = LinearRegression()
f_sel = SelectKBest(feature_selection.f_regression, k=k_feat)
pipeline = Pipeline([('poly', poly), ('f_sel', f_sel), ('regr', regr)])
tic = time.clock()
pipeline.fit(X_train[train_idx], y_train[train_idx])
print("Training time [s]:", time.clock()-tic)
mae_train = mean_absolute_error(y_train[train_idx], pipeline.predict(X_train[train_idx]))
mae_test = mean_absolute_error(y_test, pipeline.predict(X_test))
print("MAE train set:", mae_train)
print("MAE test set:", mae_test)
to_plot.append((k_feat, mae_train, mae_test))
Out:
...[output]...
In:
plt.plot([x[0] for x in to_plot], [x[1] for x in to_plot], 'b', label='Train')
plt.plot([x[0] for x in to_plot], [x[2] for x in to_plot], 'r--', label='Test')
plt.xlabel('Num. features selected')
plt.ylabel('MAE train/test')
plt.legend(loc=0)
plt.show()
Out:

似乎我们已经找到了 K 的最佳值,即所选特征的数量,在 K=900。在这个时候:
-
训练集的 MAE 和测试集的 MAE 发生了分歧:如果我们使用超过 900 个特征,我们可能开始过度拟合。
-
测试集的 MAE 达到了最低点。测试集 MAE 的读数为 6.70。
-
这是性能和训练时间之间最佳的交易(23 秒)。
如果你有一台更好的机器(具有 16GB 或更多 RAM),你可以重新运行最后两个单元格,增加训练集的大小(例如,将折数从 12 改为 8 或 4)。结果应该会提高几个小数位,尽管训练会变得更长。
测试分类器而不是回归器
作为一个问题(我们不提供解决方案),我们可以将这个问题作为一个分类任务来解决,因为目标变量的数量“仅仅”是 89(虽然这比大多数分类问题都要多)。沿着这条路径,你将遇到不同类型的问题(在分类问题中 MAE 被错误地定义了)。我们鼓励读者尝试一下,并尝试将我们获得的结果与回归学习者的结果相匹配;这里是第一步:
In:
print(np.unique(np.ascontiguousarray(y_train)))
print(len(np.unique(np.ascontiguousarray(y_train))))
Out:
[ 1922\. 1924\. 1925\. 1926\. 1927\. 1928\. 1929\. 1930\. 1931\. 1932.
1933\. 1934\. 1935\. 1936\. 1937\. 1938\. 1939\. 1940\. 1941\. 1942.
1943\. 1944\. 1945\. 1946\. 1947\. 1948\. 1949\. 1950\. 1951\. 1952.
1953\. 1954\. 1955\. 1956\. 1957\. 1958\. 1959\. 1960\. 1961\. 1962.
1963\. 1964\. 1965\. 1966\. 1967\. 1968\. 1969\. 1970\. 1971\. 1972.
1973\. 1974\. 1975\. 1976\. 1977\. 1978\. 1979\. 1980\. 1981\. 1982.
1983\. 1984\. 1985\. 1986\. 1987\. 1988\. 1989\. 1990\. 1991\. 1992.
1993\. 1994\. 1995\. 1996\. 1997\. 1998\. 1999\. 2000\. 2001\. 2002.
2003\. 2004\. 2005\. 2006\. 2007\. 2008\. 2009\. 2010\. 2011.]
89
In:
from sklearn.linear_model import SGDClassifier
regr = SGDClassifier('log', random_state=101)
tic = time.clock()
regr.fit(X_train, y_train)
print("Training time [s]:", time.clock()-tic)
print("MAE train set:", mean_absolute_error(y_train,
regr.predict(X_train)))
print("MAE test set:", mean_absolute_error(y_test,
regr.predict(X_test)))
Out:
Training time [s]: 117.23069399999986
MAE train set: 7.88104546974
MAE test set: 7.7926593066
一个不平衡的多类分类问题
给定一个从连接到互联网的主机流向/来自的包序列的描述,这个问题的目标是检测该序列是否表示恶意攻击。如果是,我们还应该对攻击类型进行分类。这是一个多类分类问题,因为可能的标签有多个。
对于每个观测,揭示了 42 个特征:请注意,其中一些是分类的,而其他的是数值的。数据集由近 500 万个观测组成(但在本练习中我们只使用前 100 万个,以避免内存限制),可能的标签数量为 23。其中之一代表非恶意情况(正常);其余的代表了 22 种不同的网络攻击。应该注意,响应类别的频率是不平衡的:对于某些攻击有多个观测,而对于其他攻击则只有几个。
没有提供关于如何分割训练/测试集或如何评估结果的具体说明。在这个问题中,我们将采用探索性目标:尝试为所有标签揭示准确的信息。我们强烈建议读者采取一些额外步骤,调整学习器以最大化检测恶意活动的精确度。
注意
该问题的完整描述可以在网站上找到:archive.ics.uci.edu/ml/datasets/KDD+Cup+1999+Data.
首先,让我们加载数据。该文件不包含标题;因此,在用 pandas 加载时,我们必须指定列名:
In:
import matplotlib.pyplot as plt
%matplotlib inline
import matplotlib.pylab as pylab
import numpy as np
import pandas as pd
columns = ["duration", "protocol_type", "service",
"flag", "src_bytes", "dst_bytes", "land",
"wrong_fragment", "urgent", "hot", "num_failed_logins",
"logged_in", "num_compromised", "root_shell",
"su_attempted", "num_root", "num_file_creations",
"num_shells", "num_access_files", "num_outbound_cmds",
"is_host_login", "is_guest_login", "count", "srv_count",
"serror_rate", "srv_serror_rate", "rerror_rate",
"srv_rerror_rate", "same_srv_rate", "diff_srv_rate",
"srv_diff_host_rate", "dst_host_count",
"dst_host_srv_count", "dst_host_same_srv_rate",
"dst_host_diff_srv_rate", "dst_host_same_src_port_rate",
"dst_host_srv_diff_host_rate", "dst_host_serror_rate",
"dst_host_srv_serror_rate", "dst_host_rerror_rate",
"dst_host_srv_rerror_rate", "outcome"]
dataset = pd.read_csv('./kdd/kddcup.dat', names=columns, nrows=1000000)
现在我们来检查加载的数据集的前几行(为了了解数据集的整体形状),其大小(以观测和特征计),以及特征类型(以区分分类和数值类型):
In:
print(dataset.head())
Out:
...[head of the dataset] ...
In:
dataset.shape
Out:
(1000000, 42)
In:
dataset.dtypes
Out:
duration int64
protocol_type object
service object
flag object
src_bytes int64
dst_bytes int64
land int64
wrong_fragment int64
urgent int64
hot int64
num_failed_logins int64
logged_in int64
num_compromised int64
root_shell int64
su_attempted int64
num_root int64
num_file_creations int64
num_shells int64
num_access_files int64
num_outbound_cmds int64
is_host_login int64
is_guest_login int64
count int64
srv_count int64
serror_rate float64
srv_serror_rate float64
rerror_rate float64
srv_rerror_rate float64
same_srv_rate float64
diff_srv_rate float64
srv_diff_host_rate float64
dst_host_count int64
dst_host_srv_count int64
dst_host_same_srv_rate float64
dst_host_diff_srv_rate float64
dst_host_same_src_port_rate float64
dst_host_srv_diff_host_rate float64
dst_host_serror_rate float64
dst_host_srv_serror_rate float64
dst_host_rerror_rate float64
dst_host_srv_rerror_rate float64
outcome object
dtype: object
由于它有 1M 行和 42 列,其中一些是分类的,看起来我们正在处理一个非常大的数据集。现在,让我们将目标变量从特征中分离出来,用序数编码包含攻击名称的字符串。为此,我们可以使用LabelEncoder对象:
In:
sorted(dataset['outcome'].unique())
Out:
['back.', 'buffer_overflow.', 'ftp_write.', 'guess_passwd.', 'imap.', 'ipsweep.', 'land.', 'loadmodule.', 'multihop.', 'neptune.', 'nmap.', 'normal.', 'perl.', 'phf.', 'pod.', 'portsweep.', 'satan.', 'smurf.', 'teardrop.', 'warezmaster.']
In:
from sklearn.preprocessing import LabelEncoder
labels_enc = LabelEncoder()
labels = labels_enc.fit_transform(dataset['outcome'])
labels_map = labels_enc.classes_
Out:
array(['back.', 'buffer_overflow.', 'ftp_write.', 'guess_passwd.', 'imap.', 'ipsweep.', 'land.', 'loadmodule.', 'multihop.', 'neptune.', 'nmap.', 'normal.', 'perl.', 'phf.', 'pod.', 'portsweep.', 'satan.', 'smurf.', 'teardrop.', 'warezmaster.'], dtype=object)
目标和变量现在在一个单独的数组中,编码为整数。现在让我们从数据集中删除目标列,并对所有分类特征进行独热编码。为此,我们可以简单地使用 Pandas 的get_dummies函数。因此,数据集的新形状因此更大,因为现在构成分类特征的每个级别都是一个二进制特征:
In:
dataset.drop('outcome', axis=1, inplace=True)
In:
observations = pd.get_dummies(dataset, sparse=True)
del dataset
In:
observations.shape
Out:
(1000000, 118)
由于我们有许多可用的观察结果,并且许多类别只包含少量样本,我们可以对数据集进行洗牌并分成两部分:一部分用于训练,另一部分用于测试:
In:
from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = \
train_test_split(observations.as_matrix(), labels,
train_size=0.5, random_state=101)
del observations
由于我们的任务具有探索性,现在让我们定义一个函数来打印按每个类别的发生次数归一化的混淆矩阵:
In:
def plot_normalised_confusion_matrix(cm, labels_str, title='Normalised confusion matrix', cmap=plt.cm.Blues):
pylab.rcParams['figure.figsize'] = (6.0, 6.0)
cm_normalized = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
plt.imshow(cm_normalized, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(labels_str))
plt.xticks(tick_marks, labels_str, rotation=90)
plt.yticks(tick_marks, labels_str)
plt.tight_layout()
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.show()
现在,让我们为这个任务创建一个基线。在这个第一步中,我们将使用一个简单的SGDClassifier,带有逻辑损失。对于训练集和测试集,我们将打印解决方案的整体准确率、归一化混淆矩阵和分类报告(包含每个类别的精确率、召回率、F1 分数和支持)。
In:
from sklearn.linear_model import SGDClassifier
from sklearn.metrics import classification_report, accuracy_score, confusion_matrix
clf = SGDClassifier('log', random_state=101)
clf.fit(X_train, y_train)
y_train_pred = clf.predict(X_train)
y_test_pred = clf.predict(X_test)
print("TRAIN SET")
print("Accuracy:", accuracy_score(y_train, y_train_pred))
print("Confusion matrix:")
plot_normalised_confusion_matrix(confusion_matrix(y_train, y_train_pred), labels_map)
print("Classification report:")
print(classification_report(y_train, y_train_pred, target_names=labels_map))
print("TEST SET")
print("Accuracy:", accuracy_score(y_test, y_test_pred))
print("Confusion matrix:")
plot_normalised_confusion_matrix(confusion_matrix(y_test, y_test_pred), labels_map)
print("Classification report:")
print(classification_report(y_test, y_test_pred, target_names=labels_map))
Out:
TRAIN SET
Accuracy: 0.781702
Confusion matrix:

Classification report:
precision recall f1-score support
back. 0.00 0.00 0.00 1005
buffer_overflow. 0.00 0.00 0.00 1
ftp_write. 0.00 0.00 0.00 2
guess_passwd. 0.00 0.00 0.00 30
imap. 0.00 0.00 0.00 2
ipsweep. 0.00 0.00 0.00 3730
land. 0.00 0.00 0.00 10
loadmodule. 0.00 0.00 0.00 3
multihop. 1.00 1.00 1.00 102522
neptune. 0.06 0.00 0.00 1149
nmap. 0.96 0.64 0.77 281101
normal. 0.00 0.00 0.00 1
perl. 0.00 0.00 0.00 2
phf. 0.00 0.00 0.00 22
pod. 0.00 0.00 0.00 1437
portsweep. 1.00 0.88 0.93 2698
satan. 0.99 0.99 0.99 106165
smurf. 0.00 0.00 0.00 110
teardrop. 0.00 0.90 0.01 10
avg / total 0.96 0.78 0.85 500000
TEST SET
Accuracy: 0.781338
Confusion matrix:

Classification report:
precision recall f1-score support
back. 0.00 0.00 0.00 997
buffer_overflow. 0.00 0.00 0.00 4
ftp_write. 0.00 0.00 0.00 6
guess_passwd. 0.00 0.00 0.00 23
imap. 0.00 0.00 0.00 10
ipsweep. 0.00 0.00 0.00 3849
land. 0.00 0.00 0.00 7
loadmodule. 0.00 0.00 0.00 2
multihop. 0.00 0.00 0.00 3
neptune. 1.00 1.00 1.00 102293
nmap. 0.05 0.00 0.00 1167
normal. 0.96 0.64 0.77 281286
perl. 0.00 0.00 0.00 1
phf. 0.00 0.00 0.00 1
pod. 0.00 0.00 0.00 18
portsweep. 0.00 0.00 0.00 1345
satan. 1.00 0.88 0.94 2691
smurf. 0.99 1.00 0.99 106198
teardrop. 0.00 0.00 0.00 89
warezmaster. 0.00 0.90 0.01 10
avg / total 0.96 0.78 0.85 500000
尽管输出非常长,但在这个基线中,一些要点立即显现:
-
准确率低(0.80),但分类对过拟合具有弹性。
-
混淆矩阵中只有两条垂直线。这表明在训练阶段,分类器只拟合了两个类别。不出所料,它们是最多的。
-
通过查看分类报告,你可以得出相同的结论(即类别不平衡影响了结果):只有少数几个类别的得分不为零。
这种问题非常常见,当你尝试在一个严重不平衡的数据集上拟合线性学习器时会发生。现在,让我们尝试对小的类别进行过采样,并对最流行的类别进行子采样。在下面的函数中,我们实现了一个带有替换的 bootstrap 算法,其中每个类别在输出数据中至少有min_samples_out个观察结果,最多有max_samples_out个。这应该迫使学习算法以相似权重考虑所有类别。
In:
import random
random.seed(101)
def sample_class_with_replacement(X, y, label, min_samples_out, max_samples_out):
rows = np.where(y==label)[0]
if len(rows) == 0:
raise Exception
n_estraction = min(max(len(rows), min_samples_out), max_samples_out)
extracted = [random.choice(rows) for _ in range(n_estraction)]
return extracted
train_idx = []
for label in np.unique(labels):
try:
idx = sample_class_with_replacement(X_train, y_train, label, 500, 20000)
train_idx.extend(idx)
except:
pass
X_train_sampled_balanced = X_train[train_idx, :]
y_train_sampled_balanced = y_train[train_idx]
现在,我们可以尝试比基线做得更好,通过在这个修改后的(平衡的)训练集上训练学习器,然后将其应用于测试集:
In:
from sklearn.linear_model import SGDClassifier
from sklearn.metrics import classification_report, accuracy_score
from sklearn.metrics import confusion_matrix
clf = SGDClassifier('log', random_state=101)
clf.fit(X_train_sampled_balanced, y_train_sampled_balanced)
y_train_pred = clf.predict(X_train_sampled_balanced)
y_test_pred = clf.predict(X_test)
print("TRAIN SET")
print("Accuracy:", accuracy_score(y_train_sampled_balanced, y_train_pred))
print("Confusion matrix:")
plot_normalised_confusion_matrix(confusion_matrix(
y_train_sampled_balanced, y_train_pred), labels_map)
print("Classification report:")
print(classification_report(y_train_sampled_balanced, y_train_pred, target_names=labels_map))
print("TEST SET")
print("Accuracy:", accuracy_score(y_test, y_test_pred))
print("Confusion matrix:")
plot_normalised_confusion_matrix(confusion_matrix(y_test, y_test_pred), labels_map)
print("Classification report:")
print(classification_report(y_test, y_test_pred, target_names=labels_map))
Out:
TRAIN SET
Accuracy: 0.712668335121
[...]
TEST SET
Accuracy: 0.723616
[...]
结果表明我们正在朝着正确的方向前进。混淆矩阵看起来更接近对角线(这意味着行和列之间的匹配更多地发生在对角线上),测试集中的准确率提高到 0.72。现在让我们尝试一些值来实现超参数优化,并将分数提升到最大。为此,我们运行网格搜索交叉验证,使用三折:
In:
from sklearn.grid_search import GridSearchCV
parameters = {
'loss': ('log', 'hinge'),
'alpha': [0.1, 0.01, 0.001, 0.0001]
}
clfgs = GridSearchCV(SGDClassifier(random_state=101, n_jobs=1),
param_grid=parameters,
cv=3,
n_jobs=1,
scoring='accuracy'
)
clfgs.fit(X_train_sampled_balanced, y_train_sampled_balanced)
clf = clfgs.best_estimator_
print(clfgs.best_estimator_)
y_train_pred = clf.predict(X_train_sampled_balanced)
y_test_pred = clf.predict(X_test)
print("TRAIN SET")
print("Accuracy:", accuracy_score(y_train_sampled_balanced,
y_train_pred))
print("Confusion matrix:")
plot_normalised_confusion_matrix(
confusion_matrix(y_train_sampled_balanced, y_train_pred),
labels_map)
print("Classification report:")
print(classification_report(
y_train_sampled_balanced, y_train_pred,
target_names=labels_map))
print("TEST SET")
print("Accuracy:", accuracy_score(y_test, y_test_pred))
print("Confusion matrix:")
plot_normalised_confusion_matrix(
confusion_matrix(y_test, y_test_pred), labels_map)
print("Classification report:")
print(classification_report(
y_test, y_test_pred, target_names=labels_map))
Out:
TRAIN SET
Accuracy: 0.695202531813
[...]
TEST SET
Accuracy: 0.706034
[...]
虽然我们运行了网格搜索交叉验证,但结果看起来与之前的实验相同。我们现在尝试不同的方法:由于存在许多输出类别,让我们尝试使用OneVsOne策略:而不是为每个类别拟合一个分类器,我们为每一对类别拟合一个分类器。这应该会产生一个更准确、但训练时间更长的模型。即使在这里,每个学习器也是通过网格搜索和三折进行交叉验证的:
In:
from sklearn.multiclass import OneVsOneClassifier
from sklearn.grid_search import GridSearchCV
parameters = {
'estimator__loss': ('log', 'hinge'),
'estimator__alpha': [1.0, 0.1, 0.01, 0.001, 0.0001, 0.00001]
}
clfgs = GridSearchCV(OneVsOneClassifier(SGDClassifier(random_state=101, n_jobs=1)),
param_grid=parameters,
cv=3,
n_jobs=1,
scoring='accuracy'
)
clfgs.fit(X_train_sampled_balanced, y_train_sampled_balanced)
clf = clfgs.best_estimator_
y_train_pred = clf.predict(X_train_sampled_balanced)
y_test_pred = clf.predict(X_test)
print("TRAIN SET")
print("Accuracy:", accuracy_score(y_train_sampled_balanced, y_train_pred))
print("Confusion matrix:")
plot_normalised_confusion_matrix(confusion_matrix(y_train_sampled_balanced, y_train_pred), labels_map)
print("Classification report:")
print(classification_report(y_train_sampled_balanced, y_train_pred, target_names=labels_map))
print("TEST SET")
print("Accuracy:", accuracy_score(y_test, y_test_pred))
print("Confusion matrix:")
plot_normalised_confusion_matrix(confusion_matrix(y_test, y_test_pred), labels_map)
print("Classification report:")
print(classification_report(y_test, y_test_pred, target_names=labels_map))
Out:
TRAIN SET
Accuracy: 0.846250612429
[...]
TEST SET
Accuracy: 0.905708
[...]
结果在训练集和测试集中都更好。现在让我们尝试使用逻辑回归而不是SGDClassifier:
In:
from sklearn.linear_model import LogisticRegression
clf = OneVsOneClassifier(LogisticRegression(random_state=101, n_jobs=1))
clf.fit(X_train_sampled_balanced, y_train_sampled_balanced)
y_train_pred = clf.predict(X_train_sampled_balanced)
y_test_pred = clf.predict(X_test)
print("TRAIN SET")
print("Accuracy:", accuracy_score(y_train_sampled_balanced,
y_train_pred))
print("Confusion matrix:")
plot_normalised_confusion_matrix(
confusion_matrix(y_train_sampled_balanced, y_train_pred),
labels_map)
print("Classification report:")
print(classification_report(
y_train_sampled_balanced, y_train_pred,
target_names=labels_map))
print("TEST SET")
print("Accuracy:", accuracy_score(y_test, y_test_pred))
print("Confusion matrix:")
plot_normalised_confusion_matrix(
confusion_matrix(y_test, y_test_pred), labels_map)
print("Classification report:")
print(classification_report(
y_test, y_test_pred, target_names=labels_map))
Out:
TRAIN SET
Accuracy: 0.985712204876
Confusion matrix:

Classification report:
precision recall f1-score support
back. 1.00 0.98 0.99 1005
buffer_overflow. 1.00 1.00 1.00 500
ftp_write. 1.00 1.00 1.00 500
guess_passwd. 1.00 0.11 0.19 500
imap. 1.00 1.00 1.00 500
ipsweep. 1.00 0.99 1.00 3730
land. 1.00 1.00 1.00 500
loadmodule. 1.00 0.32 0.49 500
multihop. 1.00 1.00 1.00 20000
neptune. 0.91 1.00 0.95 1149
nmap. 0.97 1.00 0.98 20000
normal. 1.00 1.00 1.00 500
perl. 1.00 1.00 1.00 500
phf. 0.99 1.00 1.00 500
pod. 1.00 1.00 1.00 1437
portsweep. 0.98 1.00 0.99 2698
satan. 1.00 1.00 1.00 20000
smurf. 1.00 1.00 1.00 500
teardrop. 0.55 0.83 0.66 500
avg / total 0.99 0.99 0.98 75519
TEST SET
Accuracy: 0.996818
Confusion matrix:

Classification report:
precision recall f1-score support
back. 1.00 0.98 0.99 997
buffer_overflow. 0.00 0.00 0.00 4
ftp_write. 0.00 0.00 0.00 6
guess_passwd. 1.00 0.13 0.23 23
imap. 0.43 0.30 0.35 10
ipsweep. 0.97 0.99 0.98 3849
land. 0.38 0.86 0.52 7
loadmodule. 0.00 0.00 0.00 2
multihop. 0.00 0.00 0.00 3
neptune. 1.00 1.00 1.00 102293
nmap. 0.52 0.99 0.68 1167
normal. 1.00 1.00 1.00 281286
perl. 0.00 0.00 0.00 1
phf. 0.17 1.00 0.29 1
pod. 0.26 1.00 0.42 18
portsweep. 0.96 0.99 0.98 1345
satan. 0.96 1.00 0.98 2691
smurf. 1.00 1.00 1.00 106198
teardrop. 0.99 0.98 0.98 89
warezmaster. 0.45 0.90 0.60 10
avg / total 1.00 1.00 1.00 500000
结果看起来比基线和之前的解决方案都要好。事实上:
-
在平衡的训练集上测量的准确率接近在测试集上记录的准确率。这确保了模型的泛化能力。
-
混淆矩阵几乎是对角线。这意味着所有类别都已包含在拟合(和预测)阶段。
-
对于许多类别,包括支持度较小的类别,精确率/召回率和 F1 分数都不是零。
在这个阶段,我们对解决方案感到满意。如果您想进一步挖掘并测试整个五百万数据集上的更多模型假设,现在是时候转向非线性分类器了。在这样做的时候,请提前注意获取预测所需的复杂性和运行时间(毕竟,您正在使用包含超过一亿个值的数据库)。
一个排名问题
给定一辆车及其价格的一些描述符,这个问题的目标是预测这辆车比其价格所表明的风险程度。保险行业的精算师称这个过程为符号化,结果是排名:+3 表示这辆车有风险;-3 表示它相当安全(尽管数据集中的最低值是-2)。
车辆的描述包括其各种特征(品牌、燃料类型、车身风格、长度等)的规格。此外,您还可以获得与其他车辆相比的价格和标准化损失(这代表每辆车每年平均损失的均值,针对一定范围内的所有车辆进行了标准化)。
数据集中有 205 辆车,特征数量为 25;其中一些是分类的,而另一些是数值的。此外,数据集明确指出存在一些缺失值,使用字符串"?"进行编码。
虽然目标并没有在演示页面上直接说明,但我们的任务是使标签排名损失最小化,这是一个衡量我们排名表现好坏的指标。这个分数是基于概率的,完美的排名会得到零损失。使用回归分数,如 MAE 或 MSE,对这个任务几乎没有相关性,因为预测必须是一个整数;同样,一个分类分数,如accuracy,也没有意义,因为它没有告诉我们我们离完美解决方案有多远。我们将在代码中看到的另一个分数是标签排名平均精度(LRAP)。在这种情况下,完美的排名输出得分为一(与分类中的精确度意义相同)。有关这些指标的更多信息,请参阅 Scikit-learn 网站:scikit-learn.org/stable/modules/model_evaluation.html或 2009 年在神经信息处理系统会议(Advances in Neural Information Processing Systems Conference)上提出的学习排名中的排名度量与损失函数论文。
这个问题的完整描述可以在:archive.ics.uci.edu/ml/datasets/Automobile找到。
首先,让我们加载数据。CSV 文件没有标题;因此我们将手动设置列名。此外,由于数据集的作者已经发布了信息,所有的"?"字符串都将被处理为缺失数据,即 Pandas NaN值:
In:
import matplotlib.pyplot as plt
%matplotlib inline
import matplotlib.pylab as pylab
import numpy as np
import pandas as pd
columns = ["symboling","normalized-losses","make","fuel-type",
"aspiration","num-of-doors","body-style","drive-wheels",
"engine-location","wheel-base","length","width","height",
"curb-weight","engine-type","num-of-cylinders",
"engine-size","fuel-system","bore","stroke",
"compression-ratio","horsepower","peak-rpm","city-mpg",
"highway-mpg","price"]
dataset = pd.read_csv('./autos/imports-85.data',
na_values="?", names=columns)
尽管这不能保证一切完美,但让我们先看看数据集的前几行。在这里,我们可以识别出缺失的数据(包含NaN值)并了解哪些特征是分类的,哪些是数值的:
In:
print(dataset.head())
Out:
symboling normalized-losses make fuel-type aspiration \
0 3 NaN alfa-romero gas std
1 3 NaN alfa-romero gas std
2 1 NaN alfa-romero gas std
3 2 164 audi gas std
4 2 164 audi gas std
num-of-doors body-style drive-wheels engine-location wheel-base ... \
0 two convertible rwd front 88.6 ...
1 two convertible rwd front 88.6 ...
2 two hatchback rwd front 94.5 ...
3 four sedan fwd front 99.8 ...
4 four sedan 4wd front 99.4 ...
engine-size fuel-system bore stroke compression-ratio horsepower \
0 130 mpfi 3.47 2.68 9 111
1 130 mpfi 3.47 2.68 9 111
2 152 mpfi 2.68 3.47 9 154
3 109 mpfi 3.19 3.40 10 102
4 136 mpfi 3.19 3.40 8 115
peak-rpm city-mpg highway-mpg price
0 5000 21 27 13495
1 5000 21 27 16500
2 5000 19 26 16500
3 5500 24 30 13950
4 5500 18 22 17450
[5 rows x 26 columns]
In:
dataset.dtypes
Out:
symboling int64
normalized-losses float64
make object
fuel-type object
aspiration object
num-of-doors object
body-style object
drive-wheels object
engine-location object
wheel-base float64
length float64
width float64
height float64
curb-weight int64
engine-type object
num-of-cylinders object
engine-size int64
fuel-system object
bore float64
stroke float64
compression-ratio float64
horsepower float64
peak-rpm float64
city-mpg int64
highway-mpg int64
price float64
dtype: object
看起来我们有很多分类特征。在这里,我们必须仔细考虑要做什么。数据集只包含 205 个观测值;因此,将所有分类特征转换为虚拟特征并不是一个好主意(我们可能会得到比观测值更多的特征)。让我们尽量保守地创建特征的数量。仔细检查特征,我们可以采用以下方法:
-
一些分类特征实际上是(冗长的)数值:它们包含表示数量的数字。对于这些,我们只需要将单词映射到数字。在这种情况下,不需要创建额外的特征。
-
一些其他分类特征实际上是二元的(两扇门对四扇门,柴油对汽油等)。对于这些,我们可以将两个级别映射到不同的值(0 和 1)。即使在这里,我们也不需要创建额外的特征。
-
所有剩余的都应该进行虚拟编码。
这个过程在下面的单元格中展示。为了创建第一个映射,我们简单地使用 Pandas 提供的map方法。对于第二个映射,我们使用 Scikit-learn 提供的LabelEncoder对象;对于最后一个,我们使用前面示例中看到的get_dummies函数:
In:
from sklearn.preprocessing import LabelEncoder
words_to_nums = {'two':2, 'three':3, 'four':4, 'five':5,
'six':6, 'eight':8, 'twelve':12}
columns_to_map = ['num-of-cylinders', 'num-of-doors']
columns_to_dummy = ['make', 'body-style', 'drive-wheels',
'engine-type', 'fuel-system']
columns_to_label_encode = ['fuel-type', 'aspiration',
'engine-location']
for col in columns_to_map:
dataset[col] = dataset[col].map(pd.Series(words_to_nums))
for col in columns_to_label_encode:
dataset[col] = LabelEncoder().fit_transform(dataset[col])
dataset = pd.get_dummies(dataset, columns=columns_to_dummy)
dataset.shape
Out:
(205,66)
采用这种保守的方法,列的总数最终变为 66(之前是 26)。现在,让我们从 DataFrame 中提取目标值向量,然后将每个NaN值映射到特征的中间值,创建观测矩阵。
为什么我们使用中间值(而不是平均值)?因为数据集非常小,我们不希望引入新的值:
In:
ranks = dataset['symboling'].as_matrix()
observations = dataset.drop('symboling', axis=1).as_matrix()
In:
from sklearn.preprocessing import Imputer
imp = Imputer(strategy="median", axis=0)
observations = imp.fit_transform(observations)
现在,是时候将观测值分成训练集和测试集了。由于数据集非常小,我们决定让测试集由 25%的观测值组成(大约 51 个样本)。此外,我们试图使测试集包含每个类别的样本百分比相同。为此,我们使用了StratifiedKFold类。
In:
from sklearn.cross_validation import StratifiedKFold
kf = StratifiedKFold(ranks, 4, shuffle=True, random_state=101)
idxs = list(kf)[0]
X_train = observations[idxs[0], :]
X_test = observations[idxs[1], :]
y_train = ranks[idxs[0]]
y_test = ranks[idxs[1]]
下一步是创建两个函数:第一个应该将类别映射到每个类别的概率向量(例如,类别-2 变为向量[1.0, 0.0, 0.0, 0.0, 0.0, 0.0];类别+3 变为向量[0.0, 0.0, 0.0, 0.0, 0.0, 1.0],依此类推)。这一步是评分函数所必需的。
我们需要的第二个函数是检查分类器是否在包含所有类别的数据集上训练(由于我们正在操作由仅 153 个样本组成的训练集,我们将使用交叉验证,最好仔细检查每一步)。为此,我们使用简单的assert等式:
In:
def prediction_to_probas(class_pred):
probas = []
for el in class_pred:
prob = [0.]*6
prob[el+2] = 1.0
probas.append(prob)
return np.array(probas)
def check_estimator(estimator):
assert sum(
np.abs(clf.classes_ - np.array([-2, -1, 0, 1, 2, 3]))
) == 0
现在,是时候进行分类了。我们最初将使用简单的LogisticRegression。由于我们有一个多类问题,我们可以在训练过程中使用多个 CPU。训练完分类器后,我们打印Ranking loss和Ranking avg precision score,以便用于比较的基线:
In:
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(random_state=101)
clf.fit(X_train, y_train)
check_estimator(clf)
y_test_proba = prediction_to_probas(y_test)
y_pred_proba = clf.predict_proba(X_test)
In:
from sklearn.metrics import label_ranking_average_precision_score
from sklearn.metrics import label_ranking_loss
print("Ranking loss:", label_ranking_loss(y_test_proba, y_pred_proba))
print("Ranking avg precision:", label_ranking_average_precision_score(y_test_proba, y_pred_proba))
Out:
Ranking loss: 0.0905660377358
Ranking avg precision: 0.822327044025
基线结果已经不错了。Ranking loss接近零(在这种情况下,平均标签精确度接近1)。现在我们尝试通过使用网格搜索交叉验证来改进解决方案。由于训练集中的样本非常少,我们必须使用增强验证,其中每个折叠可能包含在其他折叠中出现的样本。StratifiedShuffleSplit是最好的选择,同时确保验证集包含每个类别的相同百分比样本。我们将创建五个折叠,每个折叠包含 70%的训练集作为训练,剩余的 30%作为测试。
我们最后应该创建的是用于交叉验证的评分函数:Scikit-learn 在GridSearchCV对象中不包含任何现成的学习到排名评分函数,因此我们必须自己构建它。我们决定将其构建为标签排名损失的负值:由于网格搜索的目标是最大化分数,我们必须将其反转以找到最小值:
In:
from sklearn.grid_search import GridSearchCV
from sklearn.metrics import make_scorer
from sklearn.cross_validation import StratifiedShuffleSplit
def scorer(estimator, X, y):
check_estimator(estimator)
y_proba = prediction_to_probas(y)
return -1*label_ranking_loss(y_proba, estimator.predict_proba(X))
params = {'C': np.logspace(-1, 1, 10)}
cv = StratifiedShuffleSplit(y_train, random_state=101,
n_iter=5, train_size=0.70)
gs_cv = GridSearchCV(LogisticRegression(random_state=101),
param_grid=params,
n_jobs=1,
cv=cv,
scoring=scorer)
gs_cv.fit(X_train, y_train)
clf = gs_cv.best_estimator_
y_pred_proba = clf.predict_proba(X_test)
print("Ranking loss:",
label_ranking_loss(y_test_proba, y_pred_proba))
print("Ranking avg precision:",
label_ranking_average_precision_score(y_test_proba,
y_pred_proba))
Out:
Ranking loss: 0.0716981132075
Ranking avg precision: 0.839622641509
通过结合超参数优化过程和交叉验证,我们已经能够提高性能。现在,让我们检查这个解决方案的混淆矩阵看起来如何:
In:
from sklearn.metrics import confusion_matrix
def plot_normalised_confusion_matrix(cm):
labels_str = [str(n) for n in range(-2, 4)]
pylab.rcParams['figure.figsize'] = (6.0, 6.0)
cm_normalized = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
plt.imshow(cm_normalized, interpolation='nearest', cmap=plt.cm.Blues)
plt.colorbar()
tick_marks = np.arange(len(labels_str))
plt.xticks(tick_marks, labels_str, rotation=90)
plt.yticks(tick_marks, labels_str)
plt.tight_layout()
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.show()
plot_normalised_confusion_matrix(confusion_matrix(y_test, clf.predict(X_test)), )
Out:

除了-2 类(我们确实非常少有样本)外,看起来相当对角。总体而言,我们认为排名损失低于 0.1 是一个非常好的结果。
一个时间序列问题
本章将要讨论的最后一个问题是关于时间预测。这些问题的标准名称是时间序列分析,因为预测是基于过去提取的描述符;因此,当前时间的输出将成为预测下一个时间点的特征。在这个练习中,我们使用 2011 年道琼斯指数中包含的几只股票的收盘价。
数据集由几个特征组成,但在这个问题中(为了使练习简短而完整),我们只使用 30 只测量股票每周的收盘价,按时间顺序排列。数据集跨越了六个月:我们使用数据集的前半部分(对应于观察年份的第一季度,有 12 周)来训练我们的算法,后半部分(包含第二季度,有 13 周)来测试预测。
此外,由于我们不期望读者有经济学背景,我们尽量使事情尽可能简单。在现实生活中,这样的预测可能过于简单,无法从市场中获利,但在这个简短的例子中,我们试图将重点放在时间序列分析上,摒弃所有其他输入和来源。
注意
该问题的完整描述可以在以下链接找到:archive.ics.uci.edu/ml/datasets/Dow+Jones+Index.
根据与数据集一起分发的 readme 文件,没有缺失值;因此,加载操作相当直接:
In:
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import pandas as pd
dataset = pd.read_csv('./dji/dow_jones_index.data')
现在我们来尝试解码我们感兴趣的行(股票和收盘价):看起来收盘价都是字符串,以$开头,后面跟着相对于收盘价的浮点值。然后我们应该选择正确的列,并将收盘价转换为正确的数据类型:
In:
print(dataset.head())
Out:
quarter stock date open high low close volume \
0 1 AA 1/7/2011 $15.82 $16.72 $15.78 $16.42 239655616
1 1 AA 1/14/2011 $16.71 $16.71 $15.64 $15.97 242963398
2 1 AA 1/21/2011 $16.19 $16.38 $15.60 $15.79 138428495
3 1 AA 1/28/2011 $15.87 $16.63 $15.82 $16.13 151379173
4 1 AA 2/4/2011 $16.18 $17.39 $16.18 $17.14 154387761
percent_change_price percent_change_volume_over_last_wk \
0 3.79267 NaN
1 -4.42849 1.380223
2 -2.47066 -43.024959
3 1.63831 9.355500
4 5.93325 1.987452
previous_weeks_volume next_weeks_open next_weeks_close \
0 NaN $16.71 $15.97
1 239655616 $16.19 $15.79
2 242963398 $15.87 $16.13
3 138428495 $16.18 $17.14
4 151379173 $17.33 $17.37
percent_change_next_weeks_price days_to_next_dividend \
0 -4.428490 26
1 -2.470660 19
2 1.638310 12
3 5.933250 5
4 0.230814 97
percent_return_next_dividend
0 0.182704
1 0.187852
2 0.189994
3 0.185989
4 0.175029
In:
observations = {}
for el in dataset[['stock', 'close']].iterrows():
stock = el[1].stock
close = float(el[1].close.replace("$", ""))
try:
observations[stock].append(close)
except KeyError:
observations[stock] = [close]
现在我们为每只股票创建一个特征向量。在最简单的例子中,它只包含过去 25 周排序后的收盘价:
In:
X = []
stock_names = sorted(observations.keys())
for stock in stock_names:
X.append(observations[stock])
X = np.array(X)
现在我们来构建一个基线:我们可以尝试在最初的 12 周使用回归器,然后通过递归偏移数据来测试它——也就是说,为了预测第 13 周,我们使用前 12 周的数据;为了预测第 14 周的价值,我们使用以第 13 周结束的 12 周数据。依此类推。
注意,在这个非常简单的方法中,我们为所有股票独立地构建了一个分类器,不考虑它们的价格,并且我们使用 R²和 MAE 为每个分析周(第 13 周的评分,第 14 周的评分,依此类推)的学习者打分。最后,我们计算这些评分的均值和方差:
In:
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score, mean_absolute_error
X_train = X[:, :12]
y_train = X[:, 12]
regr_1 = LinearRegression()
regr_1.fit(X_train, y_train)
In:
plot_vals = []
for offset in range(0, X.shape[1]-X_train.shape[1]):
X_test = X[:, offset:12+offset]
y_test = X[:, 12+offset]
r2 = r2_score(y_test, regr_1.predict(X_test))
mae = mean_absolute_error(y_test, regr_1.predict(X_test))
print("offset=", offset, "r2_score=", r2)
print("offset=", offset, "MAE =", mae)
plot_vals.append( (offset, r2, mae) )
print()
print("r2_score: mean=", np.mean([x[1] for x in plot_vals]), "variance=", np.var([x[1] for x in plot_vals]))
print("mae_score: mean=", np.mean([x[2] for x in plot_vals]), "variance=", np.var([x[2] for x in plot_vals]))
Out:
offset= 0 r2_score= 0.999813479679
offset= 0 MAE = 0.384145971072
offset= 1 r2_score= 0.99504246854
offset= 1 MAE = 1.602203752
offset= 2 r2_score= 0.995188278161
offset= 2 MAE = 1.76248455475
offset= 3 r2_score= 0.998287091734
offset= 3 MAE = 1.15856848271
offset= 4 r2_score= 0.997938802118
offset= 4 MAE = 1.11955148717
offset= 5 r2_score= 0.985036566148
offset= 5 MAE = 2.94239117688
offset= 6 r2_score= 0.991598279578
offset= 6 MAE = 2.35632383083
offset= 7 r2_score= 0.995485519307
offset= 7 MAE = 1.73191962456
offset= 8 r2_score= 0.992872581249
offset= 8 MAE = 1.9828644662
offset= 9 r2_score= 0.990012202362
offset= 9 MAE = 2.66825249081
offset= 10 r2_score= 0.996984329367
offset= 10 MAE = 1.38682132207
offset= 11 r2_score= 0.999029861989
offset= 11 MAE = 0.761720947323
offset= 12 r2_score= 0.996280599178
offset= 12 MAE = 1.53124828142
r2_score: mean= 0.99489000457 variance= 1.5753065199e-05
mae_score: mean= 1.64526895291 variance= 0.487371842069
在 13 个测试周中,平均 R²为 0.99(方差为 0.0000157),平均 MAE 为 1.64(方差为 0.48)。这就是基线;让我们绘制它:
In:
fig, ax1 = plt.subplots()
ax1.plot([x[0] for x in plot_vals], [x[1] for x in plot_vals], 'b-')
ax1.plot(plot_vals[0][0], plot_vals[0][1], 'bo')
ax1.set_xlabel('test week')
# Make the y-axis label and tick labels match the line color.
ax1.set_ylabel('r2_score', color='b')
for tl in ax1.get_yticklabels():
tl.set_color('b')
ax1.set_ylim([0.9, 1.1])
ax2 = ax1.twinx()
ax2.plot([x[0] for x in plot_vals], [x[2] for x in plot_vals], 'r-')
ax2.plot(plot_vals[0][0], plot_vals[0][2], 'ro')
ax2.set_ylabel('mae score', color='r')
for tl in ax2.get_yticklabels():
tl.set_color('r')
ax2.set_ylim([0, 3.3])
plt.xlim([-.1, 12.1])
plt.show()
Out:

我们能确定 12 周前的价值仍然是当前周的优良预测因子吗?现在让我们尝试通过减少训练周数来提高我们的分数。作为一个额外的优势,我们也将拥有更多的训练数据。让我们尝试使用5(略多于一个月):
In:
training_len = 5
X_train_short = X[:, :training_len]
y_train_short = X[:, training_len]
for offset in range(1, 12-training_len):
X_train_short = np.vstack( (X_train_short, X[:, offset:training_len+offset]) )
y_train_short = np.concatenate( (y_train_short, X[:, training_len+offset]) )
In:
regr_2 = LinearRegression()
regr_2.fit(X_train_short, y_train_short)
In:
plot_vals = []
for offset in range(0, X.shape[1]-X_train.shape[1]):
X_test = X[:, 12-training_len+offset:12+offset]
y_test = X[:, 12+offset]
r2 = r2_score(y_test, regr_2.predict(X_test))
mae = mean_absolute_error(y_test, regr_2.predict(X_test))
print("offset=", offset, "r2_score=", r2)
print("offset=", offset, "MAE =", mae)
plot_vals.append( (offset, r2, mae) )
print()
print("r2_score: mean=", np.mean([x[1] for x in plot_vals]), "variance=", np.var([x[1] for x in plot_vals]))
print("mae_score: mean=", np.mean([x[2] for x in plot_vals]), "variance=", np.var([x[2] for x in plot_vals]))
Out:
offset= 0 r2_score= 0.998579501272
offset= 0 MAE = 0.85687189133
offset= 1 r2_score= 0.999412004606
offset= 1 MAE = 0.552138850961
offset= 2 r2_score= 0.998668959234
offset= 2 MAE = 0.941052814674
offset= 3 r2_score= 0.998291291965
offset= 3 MAE = 1.03476245234
offset= 4 r2_score= 0.997006831124
offset= 4 MAE = 1.45857426198
offset= 5 r2_score= 0.996849578723
offset= 5 MAE = 1.04394939395
offset= 6 r2_score= 0.998134003499
offset= 6 MAE = 1.05938998285
offset= 7 r2_score= 0.998391605331
offset= 7 MAE = 0.865007491822
offset= 8 r2_score= 0.999317752361
offset= 8 MAE = 0.607975744054
offset= 9 r2_score= 0.996058731277
offset= 9 MAE = 1.62548930127
offset= 10 r2_score= 0.997319345983
offset= 10 MAE = 1.2305378204
offset= 11 r2_score= 0.999264102166
offset= 11 MAE = 0.649407612032
offset= 12 r2_score= 0.998227164258
offset= 12 MAE = 1.020568135
r2_score: mean= 0.998116990138 variance= 9.8330905525e-07
mae_score: mean= 0.995825057897 variance= 0.0908384278533
采用这种方法,R²和 MAE 的平均值都有所提高,并且它们的方差明显降低:
In:
fig, ax1 = plt.subplots()
ax1.plot([x[0] for x in plot_vals], [x[1] for x in plot_vals], 'b-')
ax1.plot(plot_vals[0][0], plot_vals[0][1], 'bo')
ax1.set_xlabel('test week')
# Make the y-axis label and tick labels match the line color.
ax1.set_ylabel('r2_score', color='b')
for tl in ax1.get_yticklabels():
tl.set_color('b')
ax1.set_ylim([0.95, 1.05])
ax2 = ax1.twinx()
ax2.plot([x[0] for x in plot_vals], [x[2] for x in plot_vals], 'r-')
ax2.plot(plot_vals[0][0], plot_vals[0][2], 'ro')
ax2.set_ylabel('mae score', color='r')
for tl in ax2.get_yticklabels():
tl.set_color('r')
ax2.set_ylim([0, 2.2])
plt.xlim([-.1, 12.1])
plt.show()
Out:

由于这种方法似乎效果更好,我们现在尝试网格搜索最佳训练长度,范围从 1 到 12:
In:
training_lens = range(1,13)
models = {}
for training_len in training_lens:
X_train_short = X[:, :training_len]
y_train_short = X[:, training_len]
for offset in range(1, 12-training_len):
X_train_short = np.vstack( (X_train_short, X[:, offset:training_len+offset]) )
y_train_short = np.concatenate( (y_train_short, X[:, training_len+offset]) )
regr_x = LinearRegression()
regr_x.fit(X_train_short, y_train_short)
models[training_len] = regr_x
plot_vals = []
for offset in range(0, X.shape[1]-X_train.shape[1]):
X_test = X[:, 12-training_len+offset:12+offset]
y_test = X[:, 12+offset]
r2 = r2_score(y_test, regr_x.predict(X_test))
mae = mean_absolute_error(y_test, regr_x.predict(X_test))
plot_vals.append( (offset, r2, mae) )
fig, ax1 = plt.subplots()
ax1.plot([x[0] for x in plot_vals], [x[1] for x in plot_vals], 'b-')
ax1.plot(plot_vals[0][0], plot_vals[0][1], 'bo')
ax1.set_xlabel('test week')
# Make the y-axis label and tick labels match the line color.
ax1.set_ylabel('r2_score', color='b')
for tl in ax1.get_yticklabels():
tl.set_color('b')
ax1.set_ylim([0.95, 1.05])
ax2 = ax1.twinx()
ax2.plot([x[0] for x in plot_vals], [x[2] for x in plot_vals], 'r-')
ax2.plot(plot_vals[0][0], plot_vals[0][2], 'ro')
ax2.set_ylabel('mae score', color='r')
for tl in ax2.get_yticklabels():
tl.set_color('r')
ax2.set_ylim([0, max([2.2, 1.1*np.max([x[2] for x in plot_vals])])])
plt.xlim([-.1, 12.1])
plt.title("results with training_len={}".format(training_len))
plt.show()
print("r2_score: mean=", np.mean([x[1] for x in plot_vals]), "variance=", np.var([x[1] for x in plot_vals]))
print("mae_score: mean=", np.mean([x[2] for x in plot_vals]), "variance=", np.var([x[2] for x in plot_vals]))
Out:
... [images are omitted] ...
results with training_len=1
r2_score: mean= 0.998224065712 variance= 1.00685934679e-06
mae_score: mean= 0.95962574798 variance= 0.0663013566722
results with training_len=2
r2_score: mean= 0.998198628321 variance= 9.17757825917e-07
mae_score: mean= 0.969741651259 variance= 0.0661101843822
results with training_len=3
r2_score: mean= 0.998223327997 variance= 8.57207677825e-07
mae_score: mean= 0.969261583196 variance= 0.0715715354908
results with training_len=4
r2_score: mean= 0.998223602314 variance= 7.91949263056e-07
mae_score: mean= 0.972853132744 variance= 0.0737436496017
results with training_len=5
r2_score: mean= 0.998116990138 variance= 9.8330905525e-07
mae_score: mean= 0.995825057897 variance= 0.0908384278533
results with training_len=6
r2_score: mean= 0.997953763986 variance= 1.14333232014e-06
mae_score: mean= 1.04107069762 variance= 0.100961792252
results with training_len=7
r2_score: mean= 0.997481850128 variance= 1.85277659214e-06
mae_score: mean= 1.19114613181 variance= 0.121982635728
results with training_len=8
r2_score: mean= 0.99715522262 variance= 3.27488548806e-06
mae_score: mean= 1.23998671525 variance= 0.173529737205
results with training_len=9
r2_score: mean= 0.995975415477 variance= 5.76973840581e-06
mae_score: mean= 1.48200981286 variance= 0.22134177338
results with training_len=10
r2_score: mean= 0.995828230003 variance= 4.92217626753e-06
mae_score: mean= 1.51007677609 variance= 0.209938740518
results with training_len=11
r2_score: mean= 0.994520917305 variance= 7.24129427869e-06
mae_score: mean= 1.78424593989 variance= 0.213259808552
results with training_len=12
r2_score: mean= 0.99489000457 variance= 1.5753065199e-05
mae_score: mean= 1.64526895291 variance= 0.487371842069
最佳折衷方案是training_len=3。
开放性问题
正如你所见,在这个例子中,我们没有对数据进行归一化处理,使用了价格高低不同的股票。这个事实可能会让学习者感到困惑,因为观察值没有相同的中心。通过一些预处理,我们可能会获得更好的结果,应用每只股票的归一化。你能想到我们还能做些什么,以及我们如何测试这个算法吗?
摘要
在本章中,我们探讨了涉及分类器和回归器的四个实际数据科学示例。我们强烈鼓励读者阅读、理解,并尝试添加更多步骤,以提高性能。



浙公网安备 33010602011771号