精通-Sklearn-机器学习-全-

精通 Sklearn 机器学习(全)

原文:annas-archive.org/md5/134cf6a20c6d15c910e0f04905bbd7ae

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

近年来,机器学习逐渐崭露头角,它是研究从经验中学习的软件的学科。虽然机器学习是一个新兴领域,但它已经找到了许多应用。我们每天都依赖其中的一些应用;在某些情况下,它们的成功已使它们变得司空见惯。许多其他应用则是最近才被提出的,并暗示着机器学习的巨大潜力。

本书将探讨几种机器学习模型和学习算法。我们将讨论机器学习常应用于的任务,并学习如何衡量机器学习系统的性能。我们将使用一个流行的 Python 编程语言库,名为 scikit-learn,该库在一个简单而多功能的 API 下,集成了许多优秀的机器学习模型和算法实现。

本书的动机有两个目标:

  • 它的内容应当易于理解。本书仅假设读者熟悉基础的编程和数学知识。

  • 它的内容应当具有实用性。本书提供了读者可以应用于现实世界问题的实际案例。

本书的内容涵盖

第一章,机器学习基础,将机器学习定义为研究和设计通过从经验中学习来提高任务执行性能的程序。这一定义贯穿本书的其他章节;在每一章中,我们都会研究一个机器学习模型,将其应用于一个任务,并衡量其性能。

第二章,线性回归,讨论了线性回归模型,它将解释变量和模型参数与连续响应变量关联起来。你将学习成本函数,并使用正规方程来寻找产生最佳模型的参数值。

第三章,特征提取与预处理,描述了如何将文本、图像和分类变量表示为可用于机器学习模型的特征。

第四章,从线性回归到逻辑回归,讨论了如何将线性回归推广到支持分类任务。我们结合了逻辑回归模型与上一章的一些特征工程技术,创建了一个垃圾邮件过滤器。

第五章,使用决策树进行非线性分类和回归,从线性模型出发,讨论使用决策树模型进行分类和回归。我们使用决策树的集成方法构建了一个横幅广告拦截器。

第六章,使用 K-Means 进行聚类,介绍了无监督学习。我们研究了 K-means 算法,并将其与逻辑回归结合,创建了一个半监督的照片分类器。

第七章,使用 PCA 进行降维,讨论了另一种无监督学习任务——降维。我们使用主成分分析来可视化高维数据,并构建了一个人脸识别器。

第八章,感知机,描述了一种名为感知机的在线二元分类器。感知机的局限性促使了本书最后几章中所描述的模型。

第九章,从感知机到支持向量机,讨论了使用支持向量机进行高效的非线性分类和回归。我们使用支持向量机识别街头标志中的字符。

第十章,从感知机到人工神经网络,介绍了用于分类和回归的强大非线性模型——人工神经网络。我们构建了一个可以识别手写数字的网络。

本书所需的内容

本书中的示例假设您已安装 Python 2.7。第一章将描述如何在 Linux、OS X 和 Windows 上安装 scikit-learn 0.15.2 及其依赖项和其他库。

本书适合人群

本书面向具有一定机器学习经验的软件开发人员。scikit-learn 的 API 文档齐全,但假设读者理解机器学习算法的原理,并知道何时适合使用这些算法。本书并不尝试复制 API 文档的内容,而是描述了机器学习模型的工作原理、其参数是如何学习的以及如何评估它们。在实际操作中,我们将通过详细的玩具示例来演示算法,以帮助构建有效应用这些算法所需的理解。

约定

本书中,您将看到多种文本样式,这些样式用于区分不同类型的信息。以下是一些样式的示例及其含义的解释。

内联代码的格式如下:“TfidfVectorizer结合了CountVectorizerTfidfTransformer。”

代码块的表示方式如下:

>>> import pandas as pd
>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> from sklearn.linear_model.logistic import LogisticRegression
>>> from sklearn.cross_validation import train_test_split
>>> df = pd.read_csv('sms/sms.csv')
>>> X_train_raw, X_test_raw, y_train, y_test = train_test_split(df['message'], df['label'])
>>> vectorizer = TfidfVectorizer()
>>> X_train = vectorizer.fit_transform(X_train_raw)
>>> X_test = vectorizer.transform(X_test_raw)
>>> classifier = LogisticRegression()
>>> classifier.fit(X_train, y_train)

读者反馈

我们始终欢迎读者的反馈。告诉我们您对本书的看法——您喜欢什么,或者可能不喜欢什么。读者反馈对我们开发真正能让您受益的书籍非常重要。

要向我们发送一般反馈,只需通过电子邮件发送到<feedback@packtpub.com>,并在邮件主题中提及书名。

如果你在某个领域拥有专业知识,并且有兴趣撰写或参与编写书籍,请查看我们在www.packtpub.com/authors上的作者指南。

客户支持

既然你已经成为 Packt 书籍的骄傲拥有者,我们为你提供了一些帮助,以便你能从购买中获得最大的价值。

下载示例代码

你可以从你的帐户在www.packtpub.com下载你购买的所有 Packt 书籍的示例代码文件。如果你在其他地方购买了本书,你可以访问www.packtpub.com/support并注册,直接通过电子邮件获取文件。

勘误

尽管我们已经尽力确保内容的准确性,但难免会出现错误。如果你在我们的书籍中发现错误——无论是文本还是代码中的错误——我们将非常感激你向我们报告。通过这样做,你可以帮助其他读者避免困惑,并帮助我们改进后续版本。如果你发现任何勘误,请访问www.packtpub.com/submit-errata报告,选择你的书籍,点击勘误提交表单链接,输入勘误详细信息。一旦你的勘误被确认,提交将被接受,并且勘误将上传到我们的网站,或添加到该书的勘误列表中。任何现有的勘误可以通过访问www.packtpub.com/support并选择你的书名来查看。

盗版

互联网版权材料的盗版问题在所有媒体中普遍存在。在 Packt,我们非常重视保护我们的版权和许可。如果你在互联网上发现我们的作品的任何非法副本,请立即提供该位置地址或网站名称,以便我们采取相应的措施。

请通过<copyright@packtpub.com>与我们联系,并提供涉嫌盗版材料的链接。

我们感谢你在保护我们的作者以及我们向你提供有价值内容方面的帮助。

问题

如果你在本书的任何方面遇到问题,可以通过<questions@packtpub.com>与我们联系,我们将尽最大努力解决问题。

第一章:机器学习的基础

本章将回顾机器学习中的基本概念。我们将讨论机器学习算法的应用、监督学习与无监督学习的广谱、训练数据和测试数据的使用以及模型评估。最后,我们将介绍 scikit-learn,并安装接下来章节所需的工具。

长久以来,我们的想象力被能够学习并模仿人类智慧的机器所吸引。虽然像阿瑟·克拉克的 HAL 和艾萨克·阿西莫夫的 Sonny 这样的通用人工智能的愿景尚未实现,但可以通过经验获得新知识和技能的软件程序变得越来越常见。我们使用这些机器学习程序来发现我们喜欢的新音乐,快速找到我们想在网上购买的鞋子。机器学习程序使我们能够对智能手机发出命令,并让我们的恒温器自动调整温度。机器学习程序能够比人类更好地解读潦草的邮寄地址,并更警惕地防止信用卡欺诈。从研究新药物到估算新闻标题版本的页面浏览量,机器学习软件正在成为许多行业的核心。机器学习甚至渗透到长期以来被认为是人类独有的活动中,比如撰写回顾杜克大学篮球队输给北卡罗来纳大学的体育专栏。

机器学习是设计和研究利用过去经验做出未来决策的软件工具;它是研究从数据中学习的程序。机器学习的根本目标是归纳,即从规则应用的示例中推导出一个未知的规则。机器学习的经典例子是垃圾邮件过滤。通过观察成千上万封已标记为垃圾邮件或正常邮件的电子邮件,垃圾邮件过滤器学会了对新邮件进行分类。

人工智能研究的先驱计算机科学家阿瑟·塞缪尔曾说过,机器学习是“赋予计算机在没有明确编程的情况下学习能力的研究。”在 1950 年代和 1960 年代,塞缪尔开发了可以下跳棋的程序。虽然跳棋的规则很简单,但要击败经验丰富的对手,需要复杂的策略。塞缪尔并没有明确编程这些策略,而是通过与成千上万场游戏的经验,程序学会了复杂的行为,使它能够击败许多人类对手。

计算机科学家 Tom Mitchell 的名言更正式地定义了机器学习:“一个程序可以说是通过经验E在某些任务类T和性能度量P上学习,如果它在T中的任务表现,按照P度量,随着经验E的增加而得到改进。”例如,假设你有一组图片,每张图片描绘的是狗或猫。一个任务可能是将图片分类为狗和猫的不同集合。程序可以通过观察已经分类的图片来学习执行此任务,并通过计算正确分类的图片百分比来评估其性能。

我们将使用 Mitchell 的机器学习定义来组织本章内容。首先,我们将讨论经验的类型,包括监督学习和无监督学习。接下来,我们将讨论机器学习系统可以执行的常见任务。最后,我们将讨论可以用来评估机器学习系统的性能度量。

从经验中学习

机器学习系统通常被描述为通过经验进行学习,可能有或没有人类的监督。在监督学习问题中,程序通过学习一对对标注的输入和输出,预测输入的输出;也就是说,程序通过正确答案的示例来学习。在无监督学习中,程序不通过标注数据来学习,而是尝试发现数据中的模式。例如,假设你收集了描述人们身高和体重的数据。一个无监督学习问题的例子是将数据点分组。程序可能会将数据分为男性和女性,或者儿童和成人。

现在假设数据也标注了个人的性别。一个监督学习问题的例子是根据一个人的身高和体重来预测他或她是男性还是女性。我们将在接下来的章节中讨论监督学习和无监督学习的算法和示例。

监督学习和无监督学习可以被认为处于一个光谱的两端。一些问题类型,被称为半监督学习问题,既利用监督数据也利用无监督数据;这些问题位于监督学习和无监督学习之间的光谱上。半监督机器学习的一个例子是强化学习,其中程序根据其决策获得反馈,但反馈可能与单个决策无关。例如,一个学习玩横版滚动视频游戏(如超级马里奥兄弟)的强化学习程序,当它完成一个关卡或超过某个分数时,可能会获得奖励,而当它失去生命时,则会受到惩罚。然而,这种监督反馈与特定决策(例如是否奔跑、避开食人蘑菇或捡起火焰花)无关。尽管本书会讨论半监督学习,我们将主要关注监督学习和无监督学习,因为这两个类别涵盖了大多数常见的机器学习问题。在接下来的章节中,我们将更详细地回顾监督学习和无监督学习。

监督学习程序通过从标记的示例中学习,了解输入应产生的输出。机器学习程序的输出有许多不同的名称。机器学习涉及多个学科,许多学科都有自己的术语。本书中,我们将输出称为响应变量。响应变量的其他名称包括因变量、回归变量、标准变量、测量变量、响应变量、解释变量、结果变量、实验变量、标签和输出变量。类似地,输入变量也有多个名称。本书中,我们将输入变量称为特征,它们所测量的现象称为解释变量。解释变量的其他名称包括预测变量、回归变量、控制变量、操控变量和暴露变量。响应变量和解释变量可以取实数值或离散值。

包含监督经验的示例集合被称为训练集。用于评估程序性能的示例集合被称为测试集。响应变量可以被看作是由解释变量提出的问题的答案。监督学习问题通过学习不同问题的答案来学习;也就是说,监督学习程序提供了正确的答案,并且必须学会对未见过的但相似的问题做出正确回应。

机器学习任务

两个最常见的监督学习任务是分类回归。在分类任务中,程序必须学习从一个或多个解释变量预测响应变量的离散值。也就是说,程序必须预测新观察值的最可能类别、类或标签。分类的应用包括预测某只股票价格是上涨还是下跌,或者决定一篇新闻文章是属于政治还是休闲版块。在回归问题中,程序必须预测一个连续响应变量的值。回归问题的例子包括预测新产品的销量,或者根据工作描述预测工资。与分类类似,回归问题也需要监督学习。

一个常见的无监督学习任务是发现训练数据中相关观察值的组,称为。这个任务被称为聚类或聚类分析,它将观察值分配到组中,使得同一组内的观察值在某种相似性度量下,比与其他组的观察值更为相似。聚类通常用于探索数据集。例如,给定一组电影评论,聚类算法可能会发现正面和负面评论的集合。系统无法将这些簇标记为“正面”或“负面”;在没有监督的情况下,它只能知道这些分组的观察值在某种度量下是相似的。聚类的一个常见应用是发现市场中某个产品的顾客细分。通过了解哪些特征是特定顾客群体的共同点,市场营销人员可以决定他们的营销活动需要强调哪些方面。聚类也被互联网广播服务使用;例如,给定一组歌曲,聚类算法可能会根据歌曲的流派将它们分组。使用不同的相似性度量,同一个聚类算法可能会根据歌曲的调性,或者它们包含的乐器来分组歌曲。

降维是另一种常见的无监督学习任务。有些问题可能包含成千上万甚至百万个解释变量,而处理这些变量可能需要大量的计算资源。此外,如果某些解释变量捕捉到噪声或与潜在关系无关,程序的泛化能力可能会降低。降维是发现那些对响应变量变化贡献最大的解释变量的过程。降维还可以用于数据可视化。回归问题如预测房屋价格与房屋大小之间的关系就很容易进行可视化;房屋的大小可以绘制在图表的x轴上,房屋的价格可以绘制在y轴上。同样,当添加第二个解释变量时,住房价格回归问题也很容易进行可视化。例如,房屋的浴室数量可以绘制在 z 轴上。然而,当问题涉及成千上万的解释变量时,就变得无法可视化了。

训练数据和测试数据

训练集中的观察值构成了算法用来学习的经验。在监督学习问题中,每个观察值由一个观察到的响应变量和一个或多个观察到的解释变量组成。

测试集是一个类似的观察集合,用于通过某些性能指标评估模型的表现。重要的是,测试集不能包含来自训练集的任何观察。如果测试集中包含了训练集中的示例,那么就很难评估算法是否学会了从训练集中泛化,还是仅仅记住了它。一个能够很好地泛化的程序将能够有效地使用新数据执行任务。相反,一个通过学习过于复杂的模型记住训练数据的程序,可能能够准确预测训练集中的响应变量的值,但对于新示例的响应变量预测会失败。

记忆训练集被称为过拟合。一个记住其观察结果的程序可能无法很好地完成任务,因为它可能会记住噪声或偶然的关系和结构。平衡记忆和泛化,或者过拟合和欠拟合,是许多机器学习算法共同面临的问题。在后续章节中,我们将讨论正则化,它可以应用于许多模型以减少过拟合。

除了训练数据和测试数据之外,有时还需要一个第三组数据,称为验证集保留集。验证集用于调整称为超参数的变量,这些变量控制模型的学习方式。程序仍然会在测试集上进行评估,以提供其在现实世界中的表现估计;其在验证集上的表现不应作为模型在现实世界中的表现估计,因为程序已经专门针对验证数据进行了调整。通常,将一个监督学习数据集划分为训练集、验证集和测试集。对分区大小没有严格要求,大小可以根据可用数据量的不同而有所变化。通常会将 50%或更多的数据分配给训练集,25%分配给测试集,其余部分分配给验证集。

一些训练集可能只包含几百个观察值,而其他的可能包含数百万个。廉价存储、网络连接的增加、配备传感器的智能手机的普及以及对隐私态度的转变,共同促成了当今大数据的状态,或是包含数百万或数十亿个样本的训练集。虽然本书不会使用需要在几十或上百台机器上进行并行处理的数据集,但许多机器学习算法的预测能力会随着训练数据量的增加而提升。然而,机器学习算法也遵循“垃圾进,垃圾出”的原则。一个通过阅读一本庞大且充满错误的教材来备考的学生,可能不会比一个阅读一本简洁但写得很好的教材的学生考得更好。同样,一个在大量嘈杂、无关或标签错误的数据上训练的算法,也不会比在一个较小且更能代表现实问题的数据集上训练的算法表现得更好。

许多监督学习数据集是手动准备的,或者通过半自动化过程生成的。在某些领域,创建一个大型监督数据集可能是非常昂贵的。幸运的是,scikit-learn 提供了几个数据集,允许开发者专注于模型的实验开发。在开发过程中,尤其是当训练数据稀缺时,一种称为交叉验证的做法可以用来在相同的数据上训练和验证算法。在交叉验证中,训练数据被划分。算法使用除一个分区之外的所有分区进行训练,并在剩余的分区上进行测试。然后,这些分区会旋转多次,以便算法能在所有数据上进行训练和评估。下图展示了包含五个分区或折叠的交叉验证:

训练数据和测试数据

原始数据集被划分为五个大小相等的子集,标记为AE。最初,模型在BE的分区上进行训练,并在A分区上进行测试。在下一轮中,模型在ACDE的分区上进行训练,并在B分区上进行测试。分区将轮换,直到模型在所有分区上进行过训练和测试。交叉验证提供了比单一数据分区测试更准确的模型性能估计。

性能指标、偏差和方差

有许多指标可以用来衡量一个程序是否在更有效地执行任务。对于监督学习问题,许多性能指标衡量的是预测错误的数量。预测错误有两个根本原因:模型的偏差方差。假设你有许多训练集,它们都是唯一的,但在代表性上都相等。如果一个模型的偏差很大,那么无论用哪个训练集进行训练,它都会对某个输入产生类似的错误;模型会偏向自己对真实关系的假设,而不是训练数据中展示的关系。相反,如果一个模型的方差很大,它会根据训练集的不同而对某个输入产生不同的错误。高偏差的模型缺乏灵活性,而高方差的模型可能过于灵活,以至于将训练集中的噪声也拟合进去。也就是说,一个高方差的模型会过拟合训练数据,而高偏差的模型则会欠拟合训练数据。可以通过将偏差和方差比作投掷飞镖来帮助理解。每一支飞镖类似于来自不同数据集的一个预测。一个高偏差但低方差的模型会将飞镖投向远离靶心的地方,但飞镖会聚集在一起。一个高偏差和高方差的模型会将飞镖投向四处;飞镖既远离靶心,又互相分散。

一个低偏差和高方差的模型会将飞镖投向更接近靶心的地方,但飞镖聚集得不好。最后,一个低偏差和低方差的模型会将飞镖投向紧紧聚集在靶心周围的地方,如下图所示:

性能指标、偏差和方差

理想情况下,模型既具有低偏差又具有低方差,但减少其中一个往往会增加另一个。这被称为偏差-方差权衡。我们将在本书中讨论许多模型的偏差和方差。

无监督学习问题没有错误信号可供测量;相反,无监督学习问题的性能指标衡量的是数据中发现的结构的一些属性。

大多数性能衡量标准只能针对特定类型的任务进行计算。机器学习系统应该使用能够代表现实世界中错误成本的性能衡量标准进行评估。虽然这看起来显而易见,但以下例子描述了使用一种适合一般任务但不适用于具体应用的性能衡量标准。

考虑一个分类任务,其中机器学习系统观察肿瘤,并必须预测这些肿瘤是恶性还是良性。准确率,即被正确分类的实例所占的比例,是程序性能的直观衡量标准。虽然准确率能够衡量程序的表现,但它并不能区分被误判为良性的恶性肿瘤和被误判为恶性的良性肿瘤。在某些应用中,所有类型错误的成本可能是一样的。然而,在这个问题中,未能识别恶性肿瘤可能比将良性肿瘤错误分类为恶性肿瘤更为严重。

我们可以测量每一种可能的预测结果,以创建对分类器性能的不同视角。当系统正确地将肿瘤分类为恶性时,这种预测被称为真正例。当系统错误地将良性肿瘤分类为恶性时,这种预测是假正例。类似地,假负例是将肿瘤错误预测为良性,而真负例是正确预测肿瘤为良性。这四种结果可以用来计算几个常见的分类性能衡量标准,包括准确率精确度召回率

准确率通过以下公式计算,其中TP是正确预测为恶性肿瘤的数量,TN是正确预测为良性肿瘤的数量,FP是错误预测为恶性肿瘤的数量,FN是错误预测为良性肿瘤的数量:

性能衡量标准、偏差和方差

精确度是预测为恶性肿瘤中,实际为恶性的比例。精确度通过以下公式计算:

性能衡量标准、偏差和方差

召回率是系统识别出的恶性肿瘤的比例。召回率通过以下公式计算:

性能衡量标准、偏差和方差

在这个例子中,精确度衡量的是被预测为恶性的肿瘤中,实际为恶性的比例。召回率衡量的是实际恶性肿瘤中被检测出的比例。

精度和召回率度量可能揭示一个具有令人印象深刻的准确率的分类器实际上未能检测出大多数恶性肿瘤。如果大多数肿瘤是良性的,即使是一个从不预测恶性的分类器,也可能具有很高的准确率。一个不同的分类器,尽管准确率较低,但召回率较高,可能更适合此任务,因为它能检测到更多的恶性肿瘤。

还有许多其他用于分类的性能度量方法可以使用;我们将在后续章节中讨论一些,包括多标签分类问题的度量标准。在下一章中,我们将讨论回归任务的一些常见性能度量标准。

scikit-learn 简介

自 2007 年发布以来,scikit-learn 已成为最流行的 Python 开源机器学习库之一。scikit-learn 提供了用于机器学习任务的算法,包括分类、回归、降维和聚类。它还提供了提取特征、处理数据和评估模型的模块。

scikit-learn 作为 SciPy 库的扩展而构思,基于流行的 Python 库 NumPy 和 matplotlib。NumPy 扩展了 Python,使其能够高效地操作大型数组和多维矩阵。matplotlib 提供了可视化工具,SciPy 提供了科学计算模块。

scikit-learn 因为拥有文档齐全、易于使用和多功能的 API,在学术研究中非常流行。开发人员可以通过只改变几行代码,利用 scikit-learn 实验不同的算法。scikit-learn 封装了某些流行的机器学习算法实现,例如 LIBSVM 和 LIBLINEAR。其他 Python 库,包括 NLTK,也为 scikit-learn 提供了封装。scikit-learn 还包含各种数据集,使开发人员可以专注于算法,而无需担心数据的获取和清理。

作为许可宽松的 BSD 许可证下的开源软件,scikit-learn 可以在商业应用中不受限制地使用。scikit-learn 的许多算法速度快且可扩展,适用于几乎所有的数据集,除了极大的数据集。最后,scikit-learn 以其可靠性著称;库中的大部分内容都由自动化测试覆盖。

安装 scikit-learn

本书是为 scikit-learn 版本 0.15.1 编写的;请使用该版本以确保示例能够正确运行。如果您之前已安装过 scikit-learn,可以通过以下代码检索版本号:

>>> import sklearn
>>> sklearn.__version__
'0.15.1'

如果你之前没有安装过 scikit-learn,可以通过包管理器安装它,或者从源代码构建它。我们将在接下来的章节中回顾 Linux、OS X 和 Windows 的安装过程,但可以参考scikit-learn.org/stable/install.html获取最新的安装说明。以下说明仅假设你已经安装了 Python 2.6、Python 2.7 或 Python 3.2 或更高版本。前往www.python.org/download/获取安装 Python 的说明。

在 Windows 上安装 scikit-learn

scikit-learn 需要 Setuptools,这是一个支持 Python 软件打包和安装的第三方包。可以通过运行bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py中的引导脚本,在 Windows 上安装 Setuptools。

也可以获取适用于 32 位和 64 位版本的 Windows 二进制文件。如果无法确定需要哪一版本,请安装 32 位版本。两个版本都依赖于 NumPy 1.3 或更高版本。32 位版本的 NumPy 可以从sourceforge.net/projects/numpy/files/NumPy/下载。64 位版本可以从www.lfd.uci.edu/~gohlke/pythonlibs/#scikit-learn下载。

可以从sourceforge.net/projects/scikit-learn/files/下载 32 位版本的 Windows 安装程序。可以从www.lfd.uci.edu/~gohlke/pythonlibs/#scikit-learn下载 64 位版本的安装程序。

也可以从源代码在 Windows 上构建 scikit-learn。构建需要一个 C/C++编译器,如 MinGW(www.mingw.org/)、NumPy、SciPy 和 Setuptools。

要构建,请从github.com/scikit-learn/scikit-learn克隆 Git 仓库,并执行以下命令:

python setup.py install

在 Linux 上安装 scikit-learn

在 Linux 上安装 scikit-learn 有多种选择,具体取决于你的发行版。推荐的安装 scikit-learn 的方式是使用pip。你也可以使用包管理器进行安装,或者从源代码构建 scikit-learn。

要使用pip安装 scikit-learn,请执行以下命令:

sudo pip install scikit-learn

要构建 scikit-learn,请从github.com/scikit-learn/scikit-learn克隆 Git 仓库。然后安装以下依赖项:

sudo apt-get install python-dev python-numpy python-numpy-dev python-setuptools python-numpy-dev python-scipy libatlas-dev g++

导航到仓库目录并执行以下命令:

python setup.py install

在 OS X 上安装 scikit-learn

可以使用 Macports 在 OS X 上安装 scikit-learn:

sudo port install py26-sklearn

如果安装了 Python 2.7,请运行以下命令:

sudo port install py27-sklearn

scikit-learn 也可以使用pip通过以下命令进行安装:

pip install scikit-learn

验证安装

为了验证 scikit-learn 是否已正确安装,请打开 Python 控制台并执行以下命令:

>>> import sklearn
>>> sklearn.__version__
'0.15.1'

要运行 scikit-learn 的单元测试,首先安装nose库。然后执行以下命令:

nosetest sklearn –exe

恭喜!您已成功安装 scikit-learn。

安装 pandas 和 matplotlib

pandas 是一个开源库,为 Python 提供数据结构和分析工具。pandas 是一个强大的库,许多书籍描述了如何使用 pandas 进行数据分析。我们将使用 pandas 的一些便利工具来导入数据和计算汇总统计信息。

pandas 可以在 Windows、OS X 和 Linux 上使用pip通过以下命令进行安装:

pip install pandas

pandas 也可以在基于 Debian 和 Ubuntu 的 Linux 发行版上通过以下命令进行安装:

apt-get install python-pandas

matplotlib 是一个用于轻松创建图表、直方图和其他图形的 Python 库。我们将使用它来可视化训练数据和模型。matplotlib 有几个依赖项。像 pandas 一样,matplotlib 依赖于 NumPy,NumPy 应该已经安装好。在基于 Debian 和 Ubuntu 的 Linux 发行版上,可以通过以下命令安装 matplotlib 及其依赖项:

apt-get install python-matplotlib

OS X 和 Windows 的二进制文件可以从matplotlib.org/downloads.html下载。

总结

在本章中,我们将机器学习定义为设计和研究能够通过从经验中学习来提高任务表现的程序。我们讨论了经验中监督的范围。在这个范围的一个端点是监督学习,在这种学习方式下,程序通过带有相应输出标签的输入来学习。范围的另一端是无监督学习,在这种学习方式下,程序必须在没有标签的数据中发现隐藏的结构。半监督方法则结合了带标签和不带标签的训练数据。

我们讨论了常见的机器学习任务类型,并回顾了示例应用。在分类任务中,程序必须根据解释变量预测离散响应变量的值。在回归任务中,程序必须根据解释变量预测连续响应变量的值。在回归任务中,程序必须根据解释变量预测连续响应变量的值。无监督学习任务包括聚类,其中根据某种相似度度量将观测值组织成组;以及降维,它将一组解释变量减少为一组较小的合成特征,同时尽可能保留更多信息。我们还回顾了偏差-方差权衡,并讨论了不同机器学习任务的常见性能衡量标准。

我们还讨论了 scikit-learn 的历史、目标和优势。最后,我们通过安装 scikit-learn 和其他常用的配套库来准备我们的开发环境。在下一章中,我们将更详细地讨论回归任务,并使用 scikit-learn 构建我们的第一个机器学习模型。

第二章:线性回归

在本章中,你将学习如何在回归问题中使用线性模型。首先,我们将研究简单线性回归,它建立了一个响应变量与单个解释变量之间的关系。接下来,我们将讨论多重线性回归,它是简单线性回归的推广,支持多个解释变量。然后,我们将讨论多项式回归,这是一种多重线性回归的特例,能够有效地建模非线性关系。最后,我们将讨论如何通过找到使成本函数最小化的参数值来训练我们的模型。在讨论更大数据集的应用之前,我们将通过一个玩具问题来学习这些模型和学习算法的工作原理。

简单线性回归

在上一章中,你了解到训练数据用于估计监督学习问题中模型的参数。过去对解释变量及其相应响应变量的观察构成了训练数据。该模型可以用于预测在先前未观察到的解释变量值下响应变量的值。回顾一下,回归问题的目标是预测连续响应变量的值。在本章中,我们将研究几个线性回归模型的示例。我们将讨论每种方法的训练数据、模型、学习算法和评估指标。首先,让我们考虑简单线性回归。简单线性回归可用于建立一个响应变量与一个解释变量之间的线性关系。线性回归已被应用于许多重要的科学和社会问题;我们将要考虑的这个例子可能并非其中之一。

假设你想知道比萨饼的价格。你可能会直接查看菜单。然而,这本书是一本机器学习书籍,所以我们将使用简单线性回归来预测比萨饼的价格,基于我们可以观察到的比萨饼的某个属性。让我们建模比萨饼大小与价格之间的关系。首先,我们将编写一个使用 scikit-learn 的程序,可以根据比萨饼的大小预测它的价格。然后,我们将讨论简单线性回归是如何工作的,以及它如何被推广到解决其他类型的问题。假设你已经在比萨饼日志中记录了你之前吃过的比萨饼的直径和价格。这些观察数据构成了我们的训练数据:

训练实例 直径(英寸) 价格(美元)
1 6 7
2 8 9
3 10 13
4 14 17.5
5 18 18

我们可以通过使用matplotlib将训练数据绘制在图表上来可视化它:

>>> import matplotlib.pyplot as plt
>>> X = [[6], [8], [10], [14],   [18]]
>>> y = [[7], [9], [13], [17.5], [18]]
>>> plt.figure()
>>> plt.title('Pizza price plotted against diameter')
>>> plt.xlabel('Diameter in inches')
>>> plt.ylabel('Price in dollars')
>>> plt.plot(X, y, 'k.')
>>> plt.axis([0, 25, 0, 25])
>>> plt.grid(True)
>>> plt.show()

前面的脚本产生了以下图形。比萨饼的直径绘制在x轴上,价格绘制在y轴上。

简单线性回归

从训练数据的图形中,我们可以看到比萨的直径与其价格之间存在正相关关系,这也应该得到我们自己吃比萨经验的验证。随着比萨直径的增大,其价格通常也会增加。以下的比萨价格预测程序使用线性回归来建模这种关系。让我们回顾以下程序,并讨论线性回归是如何工作的:

>>> from sklearn.linear_model import LinearRegression
>>> # Training data
>>> X = [[6], [8], [10], [14],   [18]]
>>> y = [[7], [9], [13], [17.5], [18]]
>>> # Create and fit the model
>>> model = LinearRegression()
>>> model.fit(X, y)
>>> print 'A 12" pizza should cost: $%.2f' % model.predict([12])[0]
A 12" pizza should cost: $13.68

简单线性回归假设响应变量与解释变量之间存在线性关系;它用一个称为超平面的线性面来建模这个关系。超平面是一个子空间,其维度比包含它的空间少一个维度。在简单线性回归中,响应变量有一个维度,解释变量有另一个维度,总共有两个维度。因此,回归超平面只有一个维度;一个一维的超平面就是一条直线。

sklearn.linear_model.LinearRegression 类是一个估计器。估计器根据观察到的数据预测一个值。在 scikit-learn 中,所有估计器都实现了 fit()predict() 方法。前者用于学习模型的参数,后者用于利用学习到的参数预测响应变量的值。使用 scikit-learn 很容易进行不同模型的实验,因为所有估计器都实现了 fitpredict 方法。

LinearRegressionfit 方法学习以下简单线性回归模型的参数:

简单线性回归

简单线性回归 是响应变量的预测值;在这个例子中,它是比萨的预测价格。 简单线性回归 是解释变量。截距项 简单线性回归 和系数 简单线性回归 是模型的参数,这些参数由学习算法学习得出。下图中的直线描绘了比萨的大小与价格之间的关系。使用这个模型,我们预计一个 8 英寸比萨的价格约为 7.33 美元,而一个 20 英寸比萨的价格约为 18.75 美元。

简单线性回归

使用训练数据学习简单线性回归的参数值,以生成最佳拟合模型的过程称为普通最小二乘法线性最小二乘法。在本章中,我们将讨论近似模型参数值和解析解法的方法。首先,我们必须定义什么是模型拟合训练数据。

评估模型适应度的代价函数

由若干参数值集生成的回归线在下图中绘制。我们如何评估哪些参数生成了最佳拟合的回归线?

评估模型拟合度的代价函数

代价 函数,也称为损失函数,用于定义和衡量模型的误差。模型预测的价格与训练集中比萨实际价格之间的差异被称为残差训练误差。稍后,我们将在一个单独的测试数据集上评估模型;预测值与测试数据中观察到的值之间的差异被称为预测 误差测试误差

我们模型的残差由以下图中训练实例的点和回归超平面之间的垂直线表示:

评估模型拟合度的代价函数

我们可以通过最小化残差的总和来生成最佳的比萨价格预测器。也就是说,当模型对所有训练样本的响应变量预测值接近观察到的实际值时,我们的模型就得到了拟合。这种衡量模型拟合度的方式被称为残差 平方和代价函数。严格来说,这个函数通过对所有训练样本的残差平方求和来评估模型的拟合度。残差平方和通过以下公式计算,其中 评估模型拟合度的代价函数 为观察值,评估模型拟合度的代价函数 为预测值:

评估模型拟合度的代价函数

让我们通过在前一个脚本中添加以下两行来计算我们模型的残差平方和:

>>> import numpy as np
>>> print 'Residual sum of squares: %.2f' % np.mean((model.predict(X) - y) ** 2)
Residual sum of squares: 1.75

现在我们有了代价函数,可以找到最小化该函数的模型参数值。

解普通最小二乘法以进行简单线性回归

在这一节中,我们将解决简单线性回归的普通最小二乘法问题。回顾一下,简单线性回归由以下方程给出:

解普通最小二乘法以进行简单线性回归

同时,回想一下,我们的目标是求解使成本函数最小化的解决普通最小二乘法的简单线性回归解决普通最小二乘法的简单线性回归 的值。我们将首先求解解决普通最小二乘法的简单线性回归。为此,我们将计算解决普通最小二乘法的简单线性回归方差解决普通最小二乘法的简单线性回归解决普通最小二乘法的简单线性回归协方差

方差是衡量一组值分布范围的指标。如果该组中的所有数值相等,则方差为零。小方差表示这些数值接近该组的均值,而一组包含远离均值且彼此差异较大的数值则具有较大的方差。方差可以通过以下方程计算:

解决普通最小二乘法的简单线性回归

在上述方程中,解决普通最小二乘法的简单线性回归解决普通最小二乘法的简单线性回归 的均值,解决普通最小二乘法的简单线性回归解决普通最小二乘法的简单线性回归解决普通最小二乘法的简单线性回归 训练实例中的值,解决普通最小二乘法的简单线性回归 是训练实例的数量。让我们计算训练集中的比萨直径的方差:

>>> from __future__ import division
>>> xbar = (6 + 8 + 10 + 14 + 18) / 5
>>> variance = ((6 - xbar)**2 + (8 - xbar)**2 + (10 - xbar)**2 + (14 - xbar)**2 + (18 - xbar)**2) / 4
>>> print variance
23.2

NumPy 还提供了var方法来计算方差。可以使用ddof关键字参数设置贝塞尔修正来计算样本方差:

>>> import numpy as np
>>> print np.var([6, 8, 10, 14, 18], ddof=1)
23.2

协方差是衡量两个变量共同变化程度的指标。如果变量的值一起增加,则它们的协方差为正;如果一个变量倾向于增加而另一个减少,则它们的协方差为负。如果两个变量之间没有线性关系,则它们的协方差为零;这些变量是线性不相关的,但不一定是独立的。协方差可以通过以下公式计算:

解决普通最小二乘法的简单线性回归

与方差类似,求解普通最小二乘法用于简单线性回归求解普通最小二乘法用于简单线性回归训练实例的直径,求解普通最小二乘法用于简单线性回归是直径的平均值,求解普通最小二乘法用于简单线性回归是价格的平均值,求解普通最小二乘法用于简单线性回归求解普通最小二乘法用于简单线性回归训练实例的价格,而求解普通最小二乘法用于简单线性回归是训练实例的数量。我们来计算训练集中比萨饼直径和价格的协方差:

>>> xbar = (6 + 8 + 10 + 14 + 18) / 5
>>> ybar = (7 + 9 + 13 + 17.5 + 18) / 5
>>> cov = ((6 - xbar) * (7 - ybar) + (8 - xbar) * (9 - ybar) + (10 - xbar) * (13 - ybar) +
>>>        (14 - xbar) * (17.5 - ybar) + (18 - xbar) * (18 - ybar)) / 4
>>> print cov
>>> import numpy as np
>>> print np.cov([6, 8, 10, 14, 18], [7, 9, 13, 17.5, 18])[0][1]
22.65
22.65

现在我们已经计算了自变量的方差和响应与自变量的协方差,我们可以使用以下公式求解求解普通最小二乘法用于简单线性回归

求解普通最小二乘法用于简单线性回归求解普通最小二乘法用于简单线性回归

在求解了求解普通最小二乘法用于简单线性回归后,我们可以使用以下公式求解求解普通最小二乘法用于简单线性回归

求解普通最小二乘法用于简单线性回归

在前面的公式中,求解普通最小二乘法用于简单线性回归求解普通最小二乘法用于简单线性回归的平均值,而求解普通最小二乘法用于简单线性回归求解普通最小二乘法用于简单线性回归的平均值。求解普通最小二乘法用于简单线性回归是重心的坐标,这是模型必须经过的点。我们可以使用重心和求解普通最小二乘法用于简单线性回归的值来求解求解普通最小二乘法用于简单线性回归,如下所示:

求解普通最小二乘法用于简单线性回归

现在我们已经解决了能够最小化成本函数的模型参数的值,我们可以代入比萨饼的直径并预测其价格。例如,一款 11 英寸的比萨饼预计价格为 12.70 美元,而一款 18 英寸的比萨饼预计价格为 19.54 美元。恭喜你!你使用了简单线性回归来预测比萨饼的价格。

评估模型

我们使用了学习算法从训练数据中估算模型的参数。我们如何评估我们的模型是否能良好地表示实际关系呢?假设你在披萨日志中找到了另一页记录。我们将使用这页中的条目作为测试集来衡量模型的表现:

测试实例 直径(英寸) 观察到的价格(美元) 预测的价格(美元)
1 8 11 9.7759
2 9 8.5 10.7522
3 11 15 12.7048
4 16 18 17.5863
5 12 11 13.6811

可以使用多种方法来评估我们模型的预测能力。我们将使用r 平方来评估我们的披萨价格预测器。R 平方衡量模型预测响应变量观察值的准确度。更具体地说,r 平方是模型解释的响应变量方差的比例。r 平方值为 1 表示模型可以毫无误差地预测响应变量。r 平方值为 0.5 表示模型能够预测响应变量方差的一半。计算 r 平方有几种方法。在简单线性回归的情况下,r 平方等于皮尔逊积矩相关系数的平方,或皮尔逊的r

使用这种方法,r 平方应该是一个介于零和一之间的正数。这种方法是直观的;如果 r 平方描述了模型解释的响应变量方差比例,它不能大于 1 或小于 0。其他方法,包括 scikit-learn 使用的方法,并不会将 r 平方计算为皮尔逊r的平方,如果模型表现极差,它可能返回负的 r 平方。我们将遵循 scikit-learn 使用的方法来计算我们披萨价格预测器的 r 平方值。

首先,我们必须计算总平方和。评估模型是第评估模型个测试实例的响应变量观察值,且评估模型是响应变量观察值的均值。

评估模型评估模型

接下来,我们必须计算残差平方和。请记住,这也是我们的成本函数。

评估模型评估模型

最后,我们可以使用以下公式来计算 r 平方:

评估模型评估模型

0.6620的 r 平方值表示模型能够解释测试实例价格变化的大部分方差。现在,让我们使用 scikit-learn 来验证我们的计算。LinearRegressionscore方法返回模型的 r 平方值,如下例所示:

>>> from sklearn.linear_model import LinearRegression
>>> X = [[6], [8], [10], [14],   [18]]
>>> y = [[7], [9], [13], [17.5], [18]]
>>> X_test = [[8],  [9],   [11], [16], [12]]
>>> y_test = [[11], [8.5], [15], [18], [11]]
>>> model = LinearRegression()
>>> model.fit(X, y)
>>> print 'R-squared: %.4f' % model.score(X_test, y_test)
R-squared: 0.6620

多元线性回归

我们已经训练并评估了一个模型,用来预测比萨的价格。当你迫不及待想要向朋友和同事展示这个比萨价格预测器时,你却因模型不完美的 r 平方得分以及预测可能带来的尴尬而感到担忧。我们如何改进这个模型?

回想一下你个人的吃比萨经验,你可能对比萨的其他特征有一些直觉,这些特征与比萨的价格相关。例如,价格通常取决于比萨上的配料数量。幸运的是,你的比萨日志详细描述了配料;让我们把配料数量作为第二个解释变量,加入我们的训练数据。我们不能继续进行简单线性回归,但可以使用简单线性回归的推广方法——多元线性回归,它可以使用多个解释变量。严格来说,多元线性回归是以下模型:

多元线性回归

这个编辑没有意义。更改为:“简单线性回归使用一个解释变量和一个系数,而多元线性回归为每个任意数量的解释变量使用一个系数。”

多元线性回归

对于简单线性回归,这相当于以下内容:

多元线性回归

多元线性回归 是训练实例中响应变量的值的列向量。多元线性回归 是模型参数值的列向量。多元线性回归,称为设计矩阵,是训练实例中解释变量值的多元线性回归 维矩阵。多元线性回归 是训练实例的数量,多元线性回归 是解释变量的数量。让我们更新我们的比萨训练数据,将配料数量加入,值如下:

训练实例 直径(英寸) 配料数量 价格(美元)
1 6 2 7
2 8 1 9
3 10 0 13
4 14 2 17.5
5 18 0 18

我们还必须更新我们的测试数据,加入第二个解释变量,如下所示:

测试实例 直径(英寸) 配料数量 价格(美元)
1 8 2 11
2 9 0 8.5
3 11 2 15
4 16 2 18
5 12 0 11

我们的学习算法必须估计三个参数的值:两个特征的系数和截距项。虽然有人可能会试图通过将方程两边除以 多元线性回归 来求解 多元线性回归,但矩阵除法是不可能的。就像除以一个整数等同于乘以该整数的倒数一样,我们可以通过将 多元线性回归 乘以 多元线性回归 的逆矩阵来避免矩阵除法。矩阵求逆用上标 -1 来表示。只有方阵才能求逆。多元线性回归 很可能不是方阵;训练实例的数量必须等于特征的数量,才有可能是方阵。我们将把 多元线性回归 乘以其转置矩阵,得到一个可以求逆的方阵。矩阵的转置用上标 多元线性回归 来表示,矩阵的转置是将矩阵的行变为列,列变为行,形式如下:

多元线性回归

总结一下,我们的模型由以下公式给出:

多元线性回归

我们从训练数据中知道 多元线性回归多元线性回归 的值。我们必须找到 多元线性回归 的值,以最小化代价函数。我们可以如下所示求解 多元线性回归

多元线性回归

我们可以使用 NumPy 来求解 多元线性回归,如下所示:

>>> from numpy.linalg import inv
>>> from numpy import dot, transpose
>>> X = [[1, 6, 2], [1, 8, 1], [1, 10, 0], [1, 14, 2], [1, 18, 0]]
>>> y = [[7], [9], [13], [17.5], [18]]
>>> print dot(inv(dot(transpose(X), X)), dot(transpose(X), y))
[[ 1.1875    ]
 [ 1.01041667]
 [ 0.39583333]]

NumPy 还提供了一个最小二乘函数,可以更简洁地求解参数的值:

>>> from numpy.linalg import lstsq
>>> X = [[1, 6, 2], [1, 8, 1], [1, 10, 0], [1, 14, 2], [1, 18, 0]]
>>> y = [[7],    [9],    [13],    [17.5],  [18]]
>>> print lstsq(X, y)[0]
[[ 1.1875    ]
 [ 1.01041667]
 [ 0.39583333]]

让我们更新我们的比萨价格预测程序,使用第二个解释变量,并将其在测试集上的表现与简单线性回归模型进行比较:

>>> from sklearn.linear_model import LinearRegression
>>> X = [[6, 2], [8, 1], [10, 0], [14, 2], [18, 0]]
>>> y = [[7],    [9],    [13],    [17.5],  [18]]
>>> model = LinearRegression()
>>> model.fit(X, y)
>>> X_test = [[8, 2], [9, 0], [11, 2], [16, 2], [12, 0]]
>>> y_test = [[11],   [8.5],  [15],    [18],    [11]]
>>> predictions = model.predict(X_test)
>>> for i, prediction in enumerate(predictions):
>>>     print 'Predicted: %s, Target: %s' % (prediction, y_test[i])
>>> print 'R-squared: %.2f' % model.score(X_test, y_test)
Predicted: [ 10.0625], Target: [11]
Predicted: [ 10.28125], Target: [8.5]
Predicted: [ 13.09375], Target: [15]
Predicted: [ 18.14583333], Target: [18]
Predicted: [ 13.3125], Target: [11]
R-squared: 0.77

看起来将配料数量作为一个解释变量已提高了我们模型的性能。在后续章节中,我们将讨论为什么仅通过单一测试集来评估模型可能会提供不准确的性能估计,以及如何通过在多个数据分区上进行训练和测试来更准确地估计其性能。不过,目前我们可以接受,多元线性回归模型的表现明显优于简单线性回归模型。或许比萨的其他属性也可以用来解释其价格。如果这些解释变量与响应变量之间的关系在现实世界中不是线性的怎么办?在下一节中,我们将探讨多元线性回归的一个特例,该特例可以用来建模非线性关系。

多项式回归

在前面的例子中,我们假设自变量与响应变量之间的关系是线性的。这个假设并不总是成立。在本节中,我们将使用多项式回归,它是多元线性回归的一种特例,通过在模型中添加大于一的多项式项。通过添加多项式项转换训练数据,能够捕捉到实际的曲线关系,之后使用与多元线性回归相同的方式进行拟合。为了便于可视化,我们将再次仅使用一个解释变量——披萨的直径。让我们通过以下数据集将线性回归与多项式回归进行比较:

训练实例 直径(英寸) 价格(美元)
1 6 7
2 8 9
3 10 13
4 14 17.5
5 18 18
测试实例 直径(英寸) 价格(美元)
--- --- ---
1 6 7
2 8 9
3 10 13
4 14 17.5

二次回归,即使用二次多项式进行回归,公式如下:

多项式回归

我们仅使用一个解释变量,但现在模型有三个项,而不是两个。解释变量经过转换,并作为第三项添加到模型中,以捕捉曲线关系。此外,注意多项式回归的公式与多元线性回归的向量表示形式相同。PolynomialFeatures转换器可以用来轻松地将多项式特征添加到特征表示中。让我们将模型拟合到这些特征上,并与简单线性回归模型进行比较:

>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from sklearn.linear_model import LinearRegression
>>> from sklearn.preprocessing import PolynomialFeatures

>>> X_train = [[6], [8], [10], [14],   [18]]
>>> y_train = [[7], [9], [13], [17.5], [18]]
>>> X_test = [[6],  [8],   [11], [16]]
>>> y_test = [[8], [12], [15], [18]]

>>> regressor = LinearRegression()
>>> regressor.fit(X_train, y_train)
>>> xx = np.linspace(0, 26, 100)
>>> yy = regressor.predict(xx.reshape(xx.shape[0], 1))
>>> plt.plot(xx, yy)

>>> quadratic_featurizer = PolynomialFeatures(degree=2)
>>> X_train_quadratic = quadratic_featurizer.fit_transform(X_train)
>>> X_test_quadratic = quadratic_featurizer.transform(X_test)

>>> regressor_quadratic = LinearRegression()
>>> regressor_quadratic.fit(X_train_quadratic, y_train)
>>> xx_quadratic = quadratic_featurizer.transform(xx.reshape(xx.shape[0], 1))

>>> plt.plot(xx, regressor_quadratic.predict(xx_quadratic), c='r', linestyle='--')
>>> plt.title('Pizza price regressed on diameter')
>>> plt.xlabel('Diameter in inches')
>>> plt.ylabel('Price in dollars')
>>> plt.axis([0, 25, 0, 25])
>>> plt.grid(True)
>>> plt.scatter(X_train, y_train)
>>> plt.show()

>>> print X_train
>>> print X_train_quadratic
>>> print X_test
>>> print X_test_quadratic
>>> print 'Simple linear regression r-squared', regressor.score(X_test, y_test)
>>> print 'Quadratic regression r-squared', regressor_quadratic.score(X_test_quadratic, y_test)

以下是前述脚本的输出:

[[6], [8], [10], [14], [18]]
[[  1   6  36]
 [  1   8  64]
 [  1  10 100]
 [  1  14 196]
 [  1  18 324]]
[[6], [8], [11], [16]]
[[  1   6  36]
 [  1   8  64]
 [  1  11 121]
 [  1  16 256]]
Simple linear regression r-squared 0.809726797708
Quadratic regression r-squared 0.867544365635

简单线性回归模型用实线表示,绘制在下图中。用虚线表示的二次回归模型明显更好地拟合了训练数据。

多项式回归

简单线性回归模型的 R 平方值为 0.81;二次回归模型的 R 平方值提高到 0.87。虽然二次和三次回归模型是最常见的,我们也可以添加任意阶数的多项式。下图展示了二次回归和三次回归模型:

多项式回归

现在,让我们尝试一个更高阶的多项式。下图中的回归曲线是由一个九次方多项式创建的:

多项式回归

九次多项式回归模型几乎完美地拟合了训练数据!然而,模型的 R 方值为 -0.09。我们创建了一个非常复杂的模型,完美地拟合了训练数据,但未能逼近真实关系。这个问题称为 过拟合。该模型应该生成一个通用规则,将输入映射到输出;然而,它只是记住了训练数据中的输入和输出。因此,模型在测试数据上的表现较差。它预测一个 16 英寸的比萨饼应该少于 10 美元,而一个 18 英寸的比萨饼应该超过 30 美元。该模型完美拟合了训练数据,但未能学习到大小与价格之间的真实关系。

正则化

正则化是一组可以防止过拟合的技术。正则化通过向问题中添加信息,通常是对复杂性进行惩罚,从而对问题进行调整。奥卡姆剃刀原理指出,假设越少的假设越好。因此,正则化旨在找到最简单的模型来解释数据。

scikit-learn 提供了几种正则化的线性回归模型。岭回归,也称为 提霍诺夫正则化,惩罚变得过大的模型参数。岭回归通过将系数的 L2 范数添加到残差平方和代价函数中来进行调整,具体如下:

正则化

正则化 是一个超参数,用于控制惩罚的强度。超参数是模型的参数,不会自动学习,必须手动设置。当 正则化 增加时,惩罚增强,代价函数的值也增加。当 正则化 等于零时,岭回归等同于线性回归。

scikit-learn 还提供了 最小绝对收缩和选择算子LASSO)的实现。LASSO 通过将其 L1 范数添加到代价函数中来惩罚系数,具体如下:

正则化

LASSO 会产生稀疏的参数;大多数系数将变为零,模型将依赖于特征的一个小子集。与此不同的是,岭回归会产生模型,其中大多数参数虽然小,但不为零。当解释变量之间存在相关性时,LASSO 会将一个变量的系数收缩到零。岭回归则会更均匀地收缩它们。最后,scikit-learn 提供了 弹性网 正则化的实现,该方法线性地结合了 LASSO 和岭回归使用的 L1 和 L2 惩罚。也就是说,LASSO 和岭回归是弹性网方法的特例,其中 L1 或 L2 惩罚的超参数之一为零。

应用线性回归

我们通过一个简单的问题学习了如何使用线性回归模型建立解释变量与响应变量之间的关系。现在,我们将使用一个真实的数据集,并应用线性回归来解决一个重要任务。假设你正在参加一个聚会,想要喝到最好喝的葡萄酒。你可以向朋友们请求推荐,但你怀疑他们会喝任何葡萄酒,无论它的来源如何。幸运的是,你带了 pH 测试条和其他工具来测量葡萄酒的各种物理化学属性——毕竟,这是一个聚会。我们将使用机器学习根据葡萄酒的物理化学属性来预测其质量。

UCI 机器学习库的葡萄酒数据集测量了 1,599 种不同红酒的十一种物理化学属性,包括 pH 值和酒精含量。每种酒的质量已由人工评审员打分,分数范围从零到十;零表示最差质量,十表示最佳质量。数据集可以从archive.ics.uci.edu/ml/datasets/Wine下载。我们将把这个问题视为一个回归任务,并将葡萄酒的质量回归到一个或多个物理化学属性上。这个问题中的响应变量仅取 0 到 10 之间的整数值;我们可以将其视为离散值,并将问题作为多类分类任务来处理。然而,在本章中,我们将把响应变量视为一个连续值。

探索数据

固定酸度 挥发酸度 柠檬酸度 残留糖分 氯化物 游离二氧化硫 总二氧化硫 密度 pH 硫酸盐 酒精 质量
7.4 0.7 0 1.9 0.076 11 34 0.9978 3.51 0.56 9.4 5
7.8 0.88 0 2.6 0.098 25 67 0.9968 3.2 0.68 9.8 5
7.8 0.76 0.04 2.3 0.092 15 54 0.997 3.26 0.65 9.8 5
11.2 0.28 0.56 1.9 0.075 17 60 0.998 3.16 0.58 9.8 6

scikit-learn 是一个用于构建机器学习系统的工具;与 SPSS Statistics 或 R 语言等软件包相比,它在数据探索方面的能力较为贫弱。我们将使用 pandas,这是一个开源的 Python 数据分析库,用于从数据中生成描述性统计信息;我们将使用这些统计信息来指导模型的一些设计决策。pandas 将一些来自 R 的概念引入了 Python,例如数据框(dataframe),它是一个二维、表格化且异质的数据结构。使用 pandas 进行数据分析是多本书籍的主题;在以下示例中,我们将只使用一些基本方法。

首先,我们将加载数据集并查看一些基本的摘要统计信息。数据以 .csv 文件提供。请注意,字段是用分号而不是逗号分隔的。

>>> import pandas as pd
>>> df = pd.read_csv('winequality-red.csv', sep=';')
>>> df.describe()

                pH    sulphates      alcohol      quality
count  1599.000000  1599.000000  1599.000000  1599.000000
mean      3.311113     0.658149    10.422983     5.636023
std       0.154386     0.169507     1.065668     0.807569
min       2.740000     0.330000     8.400000     3.000000
25%       3.210000     0.550000     9.500000     5.000000
50%       3.310000     0.620000    10.200000     6.000000
75%       3.400000     0.730000    11.100000     6.000000
max       4.010000     2.000000    14.900000     8.000000

pd.read_csv()函数是一个方便的工具,可以将.csv文件加载到数据框中。Dataframe.describe()方法计算数据框中每一列的汇总统计信息。前面的代码示例只显示了数据框最后四列的汇总统计信息。注意质量变量的汇总情况;大多数酒的评分是五分或六分。可视化数据有助于表明响应变量与解释变量之间是否存在关系。我们可以使用matplotlib来创建一些散点图。请参考以下代码片段:

>>> import matplotlib.pylab as plt
>>> plt.scatter(df['alcohol'], df['quality'])
>>> plt.xlabel('Alcohol')
>>> plt.ylabel('Quality')
>>> plt.title('Alcohol Against Quality')
>>> plt.show()

前面代码片段的输出结果如下图所示:

探索数据

在前图的散点图中,酒精含量与质量之间呈现出微弱的正相关关系;高酒精含量的酒往往质量较高。下图则揭示了挥发性酸度与质量之间的负相关关系:

探索数据

这些图表表明响应变量依赖于多个解释变量;我们来用多元线性回归建模这种关系。我们如何决定在模型中包含哪些解释变量呢?Dataframe.corr()计算一对一的相关矩阵。相关矩阵确认了酒精含量和质量之间存在最强的正相关关系,而质量与挥发性酸度呈负相关,挥发性酸度是导致酒品有醋味的属性。总结来说,我们假设优质的酒具有较高的酒精含量,并且不带有醋味。这个假设似乎有道理,尽管它暗示了酒类爱好者的味觉可能比他们声称的要不那么精细。

拟合和评估模型

现在我们将数据分为训练集和测试集,训练回归器,并评估其预测结果:

>>> from sklearn.linear_model import LinearRegression
>>> import pandas as pd
>>> import matplotlib.pylab as plt
>>> from sklearn.cross_validation import train_test_split

>>> df = pd.read_csv('wine/winequality-red.csv', sep=';')
>>> X = df[list(df.columns)[:-1]]
>>> y = df['quality']
>>> X_train, X_test, y_train, y_test = train_test_split(X, y)

>>> regressor = LinearRegression()
>>> regressor.fit(X_train, y_train)
>>> y_predictions = regressor.predict(X_test)
>>> print 'R-squared:', regressor.score(X_test, y_test)
0.345622479617

首先,我们使用 pandas 加载了数据,并将响应变量与解释变量分开。接着,我们使用train_test_split函数将数据随机分割成训练集和测试集。可以使用关键字参数指定两个分区的数据比例。默认情况下,25%的数据被分配给测试集。最后,我们训练了模型并在测试集上进行了评估。

r 平方值为 0.35,表示测试集中的 35%的方差由模型解释。如果将不同的 75%的数据分配到训练集,模型的表现可能会有所变化。我们可以使用交叉验证来更好地估计估计器的性能。回顾第一章,每一轮交叉验证都训练并测试数据的不同分割,以减少变异性:

>>> import pandas as pd
>>> from sklearn. cross_validation import cross_val_score
>>> from sklearn.linear_model import LinearRegression
>>> df = pd.read_csv('data/winequality-red.csv', sep=';')
>>> X = df[list(df.columns)[:-1]]
>>> y = df['quality']
>>> regressor = LinearRegression()
>>> scores = cross_val_score(regressor, X, y, cv=5)
>>> print scores.mean(), scores
0.290041628842 [ 0.13200871  0.31858135  0.34955348  0.369145    0.2809196 ]

cross_val_score 辅助函数允许我们使用提供的数据和估计器轻松执行交叉验证。我们使用 cv 关键字参数指定了五折交叉验证,即每个实例将随机分配到五个分区之一。每个分区将用于训练和测试模型。 cross_val_score 返回每轮估计器 score 方法的值。 R 平方分数的范围为 0.13 到 0.36!分数的均值 0.29 比从单一训练/测试拆分产生的 R 平方分数更好地估计了估计器的预测能力。

让我们检查一些模型的预测结果,并将真实质量分数与预测分数绘制在一起:

Predicted: 4.89907499467 True: 4
Predicted: 5.60701048317 True: 6
Predicted: 5.92154439575 True: 6
Predicted: 5.54405696963 True: 5
Predicted: 6.07869910663 True: 7
Predicted: 6.036656327 True: 6
Predicted: 6.43923020473 True: 7
Predicted: 5.80270760407 True: 6
Predicted: 5.92425033278 True: 5
Predicted: 5.31809822449 True: 6
Predicted: 6.34837585295 True: 6

以下图展示了前述代码的输出:

拟合和评估模型

预期地,很少有预测能完全匹配响应变量的真实值。由于大部分训练数据是关于普通葡萄酒的,该模型在预测普通葡萄酒的质量时表现更好。

使用梯度下降拟合模型

在本章的示例中,我们通过以下方程解析地求解了模型参数的值,该方程最小化了成本函数的值:

使用梯度下降拟合模型

请回忆,使用梯度下降拟合模型 是每个训练示例的解释变量值的矩阵。 使用梯度下降拟合模型 的点积结果是一个维度为 使用梯度下降拟合模型 的方阵,其中 使用梯度下降拟合模型 等于解释变量的数量。反转这个方阵的计算复杂度几乎是解释变量数量的立方。虽然本章的示例中解释变量数量较少,但在后续章节中遇到数万个解释变量的问题时,这种反转可能成本高昂。此外,如果 使用梯度下降拟合模型 的行列式为零,它就无法反转。在本节中,我们将讨论另一种有效估算模型参数最优值的方法,称为梯度下降。请注意,我们对良好拟合的定义没有改变;我们仍将使用梯度下降估算模型参数的值,以最小化成本函数的值。

梯度下降有时通过一个类比来描述:假设一个蒙着眼睛的人正试图从山坡上的某个地方走到山谷的最低点。他无法看到地形,因此他每次都朝着坡度最陡的方向迈出一步。接着他再迈出一步,依然朝着坡度最陡的方向。每一步的大小与当前位置的坡度成正比。当地形陡峭时,他迈出较大的一步,因为他相信自己仍然接近山顶,并且不会超过山谷的最低点。当地形变得不那么陡峭时,他会迈出较小的步伐。如果他继续迈出大步,可能会不小心越过山谷的最低点。此时他需要改变方向,重新朝着山谷的最低点迈进。通过逐步减小步伐,他可以避免反复跨越山谷的最低点。蒙眼的人继续走,直到他再也无法迈出一步使自己的高度降低;此时,他就找到了山谷的底部。

从形式上讲,梯度下降是一种优化算法,可以用来估计函数的局部最小值。回想一下,我们使用的是残差平方和(RSS)代价函数,其公式如下:

使用梯度下降拟合模型

我们可以使用梯度下降来找到模型参数的值,从而最小化代价函数的值。梯度下降通过计算代价函数在每一步的偏导数,迭代更新模型参数的值。计算代价函数的偏导数所需的微积分超出了本书的范围,而且在使用 scikit-learn 时也不需要了解这些内容。然而,理解梯度下降如何工作可以帮助你更有效地使用它。

需要注意的是,梯度下降估计的是函数的局部最小值。对于一个凸的代价函数,其值在所有可能参数的取值下的三维图形看起来像一个碗。碗的底部是唯一的局部最小值。非凸代价函数可能有多个局部最小值,也就是说,它们的代价函数值的图形可能会有多个峰值和谷值。梯度下降只保证找到局部最小值;它会找到一个谷底,但不一定是最低的那个谷底。幸运的是,残差平方和代价函数是凸的。

梯度下降的一个重要超参数是学习率,它控制盲人行走的步伐大小。如果学习率足够小,代价函数将随着每次迭代而减少,直到梯度下降收敛到最优参数。然而,随着学习率的减小,梯度下降收敛所需的时间也会增加;如果盲人迈小步,他将比迈大步花更多时间到达山谷。如果学习率过大,盲人可能会反复越过山谷的底部,也就是说,梯度下降可能在参数的最优值附近震荡。

梯度下降有两种类型,它们通过每次训练迭代中用于更新模型参数的训练实例数量来区分。批量梯度下降(有时简称为梯度下降)使用所有训练实例来更新模型参数。随机梯度下降SGD)则通过每次迭代仅使用一个训练实例来更新参数。训练实例通常是随机选择的。由于随机梯度下降比批量梯度下降收敛得更快,因此当训练实例数达到数十万或更多时,通常会选择使用它来优化代价函数。批量梯度下降是一个确定性算法,给定相同的训练集,它会产生相同的参数值。作为一种随机算法,SGD 每次运行时可能会产生不同的参数估计值。由于 SGD 每次仅使用单个训练实例来更新权重,它可能无法像梯度下降那样有效地最小化代价函数。尽管如此,它的逼近通常足够接近,特别是对于像残差平方和这样的凸代价函数。

让我们使用随机梯度下降来估计使用 scikit-learn 构建的模型的参数。SGDRegressor 是一种 SGD 的实现,甚至可以用于具有数十万或更多特征的回归问题。它可以用来优化不同的代价函数,以拟合不同的线性模型;默认情况下,它将优化残差平方和。在这个例子中,我们将预测波士顿住房数据集中基于 13 个解释变量的房价:

>>> import numpy as np
>>> from sklearn.datasets import load_boston
>>> from sklearn.linear_model import SGDRegressor
>>> from sklearn.cross_validation import cross_val_score
>>> from sklearn.preprocessing import StandardScaler
>>> from sklearn.cross_validation import train_test_split
>>> data = load_boston()
>>> X_train, X_test, y_train, y_test = train_test_split(data.data, data.target)

scikit-learn 提供了一个便捷的函数来加载数据集。首先,我们使用 train_test_split 将数据分为训练集和测试集:

>>> X_scaler = StandardScaler()
>>> y_scaler = StandardScaler()
>>> X_train = X_scaler.fit_transform(X_train)
>>> y_train = y_scaler.fit_transform(y_train)
>>> X_test = X_scaler.transform(X_test)
>>> y_test = y_scaler.transform(y_test)

接下来,我们使用 StandardScaler 对特征进行标准化,这将在下一章中详细介绍:

>>> regressor = SGDRegressor(loss='squared_loss')
>>> scores = cross_val_score(regressor, X_train, y_train, cv=5)
>>> print 'Cross validation r-squared scores:', scores
>>> print 'Average cross validation r-squared score:', np.mean(scores)
>>> regressor.fit_transform(X_train, y_train)
>>> print 'Test set r-squared score', regressor.score(X_test, y_test)

最后,我们训练了估计器,并通过交叉验证和测试集进行了评估。以下是脚本的输出:

Cross validation r-squared scores: [ 0.73428974  0.80517755  0.58608421  0.83274059  0.69279604]
Average cross validation r-squared score: 0.730217627242
Test set r-squared score 0.653188093125

摘要

本章讨论了三种线性回归的案例。我们通过一个简单线性回归的例子,展示了如何使用一条直线来建模单一解释变量与响应变量之间的关系。接着,我们讨论了多元线性回归,它将简单线性回归推广到多个解释变量与响应变量之间关系的建模。最后,我们描述了多项式回归,这是多元线性回归的一个特例,用于建模解释变量与响应变量之间的非线性关系。这三种模型可以视为广义线性模型的特例,广义线性模型是一个用于建模线性关系的框架,我们将在第四章《从线性回归到逻辑回归》中详细讨论。

我们使用残差平方和成本函数来评估模型的拟合程度,并讨论了两种方法来学习能够最小化成本函数的模型参数值。首先,我们通过解析方法求解了模型参数的值。接着,我们讨论了梯度下降法,这是一种能够高效估计模型参数最优值的方法,即使模型有大量特征时也能应用。在本章的例子中,特征是解释变量的简单测量,因此很容易将它们应用到我们的模型中。在下一章,你将学习如何为不同类型的解释变量创建特征,包括分类变量、文本和图像。

第三章. 特征提取与预处理

前一章讨论的示例使用了简单的数字解释变量,例如比萨饼的直径。许多机器学习问题需要从类别变量、文本或图像的观察中学习。在本章中,你将学习处理数据的基本技术,并为这些观察创建特征表示。这些技术可以与在第二章中讨论的线性回归模型一起使用,也可以与我们将在后续章节讨论的模型一起使用。

从类别变量中提取特征

许多机器学习问题具有类别特征或名义特征,而不是连续特征。例如,一个根据职位描述预测薪资的应用程序,可能使用诸如职位所在地点之类的类别特征。类别变量通常使用一对 K独热编码进行编码,其中解释变量通过每个可能值的一个二进制特征进行编码。

例如,假设我们的模型有一个 city 解释变量,能够取三个值之一:New YorkSan FranciscoChapel Hill。独热编码使用每个可能的城市一个二进制特征来表示该解释变量。

在 scikit-learn 中,DictVectorizer 类可用于对类别特征进行独热编码:

>>> from sklearn.feature_extraction import DictVectorizer
>>> onehot_encoder = DictVectorizer()
>>> instances = [
>>>     {'city': 'New York'},
>>>     {'city': 'San Francisco'},
>>>     {'city': 'Chapel Hill'}>>> ]
>>> print onehot_encoder.fit_transform(instances).toarray()
[[ 0\.  1\.  0.] [ 0\.  0\.  1.][ 1\.  0\.  0.]]

注意,生成的特征在特征向量中的顺序不一定与它们出现的顺序相同。在第一个训练示例中,city 特征的值为 New York。特征向量中的第二个元素对应于 New York 值,并且在第一次出现时被设置为 1。直观上看,可能会认为用单个整数特征来表示类别解释变量的值,但这将编码虚假的信息。例如,前一个示例的特征向量将只有一个维度。New York 可以表示为 0San Francisco1Chapel Hill2。这种表示法会为变量的值编码一个在现实世界中不存在的顺序;城市之间没有自然顺序。

从文本中提取特征

许多机器学习问题使用文本作为解释变量。文本必须转换为一种不同的表示形式,将其尽可能多的含义编码到特征向量中。在接下来的章节中,我们将回顾机器学习中最常见的文本表示方式的变体:词袋模型。

词袋表示法

文本的最常见表示方法是袋模型。这种表示方法使用一个多重集(或袋),该袋编码了文本中出现的单词;袋模型不编码文本的任何语法,忽略单词的顺序,并不考虑任何语法规则。袋模型可以被视为对独热编码(one-hot encoding)的扩展。它为文本中每个感兴趣的单词创建一个特征。袋模型的直觉是,包含相似单词的文档通常具有相似的意义。尽管袋模型编码的信息有限,但它仍然可以有效地用于文档分类和检索。

一组文档称为语料库。让我们使用一个包含以下两个文档的语料库来检查袋模型:

corpus = [
    'UNC played Duke in basketball',
    'Duke lost the basketball game'
]

这个语料库包含八个独特的单词:UNCplayedDukeinbasketballlostthegame。语料库的独特单词构成了它的词汇。袋模型使用一个特征向量,每个元素对应语料库词汇中的一个单词,来表示每个文档。我们的语料库有八个独特的单词,因此每个文档将由一个包含八个元素的向量来表示。构成特征向量的元素数量称为向量的维度字典将词汇映射到特征向量中的索引位置。

在最基本的袋模型表示中,特征向量中的每个元素都是一个二进制值,表示对应的单词是否出现在文档中。例如,第一个文档中的第一个单词是 UNC。字典中的第一个单词是 UNC,因此向量中的第一个元素为 1。字典中的最后一个单词是 game,第一个文档中没有出现 game,因此向量中的第八个元素被设为 0CountVectorizer 类可以从字符串或文件中生成袋模型表示。默认情况下,CountVectorizer 将文档中的字符转换为小写,并分词。分词是将字符串拆分为词元的过程,或者是将字符分割成有意义的序列。词元通常是单词,但也可以是包括标点符号和词缀在内的较短序列。CountVectorizer 类使用一个正则表达式来分词,按空白字符拆分字符串,并提取长度为两个或更多字符的字符序列。

我们语料库中的文档由以下特征向量表示:

>>> from sklearn.feature_extraction.text import CountVectorizer
>>> corpus = [
>>>     'UNC played Duke in basketball',
>>>     'Duke lost the basketball game'
>>> ]
>>> vectorizer = CountVectorizer()
>>> print vectorizer.fit_transform(corpus).todense()
>>> print vectorizer.vocabulary_
[[1 1 0 1 0 1 0 1]
 [1 1 1 0 1 0 1 0]]
{u'duke': 1, u'basketball': 0, u'lost': 4, u'played': 5, u'game': 2, u'unc': 7, u'in': 3, u'the': 6}

现在让我们向语料库中添加第三个文档:

corpus = [
    'UNC played Duke in basketball',
    'Duke lost the basketball game',
    'I ate a sandwich'
]

我们语料库的字典现在包含以下十个独特的单词。注意,Ia 没有被提取出来,因为它们不符合 CountVectorizer 用来分词的默认正则表达式:

{u'duke': 2, u'basketball': 1, u'lost': 5, u'played': 6, u'in': 4, u'game': 3, u'sandwich': 7, u'unc': 9, u'ate': 0, u'the': 8} 

现在,我们的特征向量如下:

UNC played Duke in basketball = [[0 1 1 0 1 0 1 0 0 1]]
Duke lost the basketball game = [[0 1 1 1 0 1 0 0 1 0]]
I ate a sandwich = [[1 0 0 0 0 0 0 1 0 0]]

前两篇文章的含义比它们与第三篇文章的含义更为相似,而且在使用如欧几里得距离这样的度量标准时,它们的特征向量也比与第三篇文章的特征向量更为相似。两个向量之间的欧几里得距离等于两个向量之间差异的欧几里得范数,或称 L2 范数:

词袋模型表示

记住,向量的欧几里得范数等于该向量的大小,大小由以下方程给出:

词袋模型表示

scikit-learn 的euclidean_distances函数可以用来计算两个或多个向量之间的距离,并且它确认了语义上最相似的文档在空间中也是彼此最接近的。以下示例中,我们将使用euclidean_distances函数来比较我们的文档特征向量:

>>> from sklearn.metrics.pairwise import euclidean_distances
>>> counts = [
>>>     [0, 1, 1, 0, 0, 1, 0, 1],
>>>     [0, 1, 1, 1, 1, 0, 0, 0],
>>>     [1, 0, 0, 0, 0, 0, 1, 0]
>>> ]
>>> print 'Distance between 1st and 2nd documents:', euclidean_distances(counts[0], counts[1])
>>> print 'Distance between 1st and 3rd documents:', euclidean_distances(counts[0], counts[2])
>>> print 'Distance between 2nd and 3rd documents:', euclidean_distances(counts[1], counts[2])
Distance between 1st and 2nd documents: [[ 2.]]
Distance between 1st and 3rd documents: [[ 2.44948974]]
Distance between 2nd and 3rd documents: [[ 2.44948974]]

现在假设我们使用的是新闻文章语料库,而不是我们之前的简单语料库。我们的词典可能包含数十万个独特的单词,而不仅仅是十二个。表示文章的特征向量将包含数十万个元素,并且许多元素将是零。大多数体育文章不会包含任何特定于财经文章的词汇,大多数文化文章也不会包含任何特定于财经文章的词汇。具有大量零值元素的高维特征向量称为稀疏向量

使用高维数据会为所有机器学习任务带来一些问题,包括那些不涉及文本的任务。第一个问题是,高维向量需要比小型向量更多的内存。NumPy 提供了一些数据类型,通过高效地表示稀疏向量的非零元素,缓解了这个问题。

第二个问题被称为维度灾难,或Hughes 效应。随着特征空间的维度增加,需要更多的训练数据以确保每个特征值组合都有足够的训练实例。如果某个特征的训练实例不足,算法可能会对训练数据中的噪声进行过拟合,导致无法泛化。在接下来的部分,我们将回顾几种减少文本特征维度的策略。在第七章中,PCA 降维一节中,我们将回顾数值维度减少的技术。

停用词过滤

降低特征空间维度的一个基本策略是将所有文本转换为小写。这一策略源于这样一个观点:字母的大小写不会影响大多数单词的含义;sandwichSandwich在大多数语境下具有相同的意义。大写字母可能表示某个单词位于句子的开头,但袋装词模型(bag-of-words model)已经丢弃了所有关于单词顺序和语法的信息。

第二种策略是删除语料库中大多数文档都常见的单词。这些单词被称为停用词,包括限定词如theaan;助动词如dobewill;以及介词如onaroundbeneath。停用词通常是功能性词汇,它们通过语法而非词汇本身的定义为文档的含义做出贡献。CountVectorizer类可以通过stop_words关键字参数过滤停用词,并且还包含一个基本的英语停用词表。我们来重新创建我们的文档特征向量,并应用停用词过滤:

>>> from sklearn.feature_extraction.text import CountVectorizer
>>> corpus = [
>>>     'UNC played Duke in basketball',
>>>     'Duke lost the basketball game',
>>>     'I ate a sandwich'
>>> ]
>>> vectorizer = CountVectorizer(stop_words='english')
>>> print vectorizer.fit_transform(corpus).todense()
>>> print vectorizer.vocabulary_
[[0 1 1 0 0 1 0 1]
 [0 1 1 1 1 0 0 0]
 [1 0 0 0 0 0 1 0]]
{u'duke': 2, u'basketball': 1, u'lost': 4, u'played': 5, u'game': 3, u'sandwich': 6, u'unc': 7, u'ate': 0}

特征向量现在的维度较少,前两个文档向量彼此之间仍然比与第三个文档更相似。

词干提取和词形还原

虽然停用词过滤是一种简单的降维策略,但大多数停用词表只包含几百个单词。即便经过过滤,大型语料库仍可能包含数十万个独特的词汇。两种进一步降维的相似策略被称为词干提取词形还原

高维文档向量可能单独编码同一单词的多种派生或屈折形式。例如,jumpingjumps都是jump这个词的形式;在一个关于跳远的语料库中,文档向量可能会用特征向量中的单独元素编码每个屈折形式。词干提取和词形还原是将单词的屈折形式和派生形式压缩成单一特征的两种策略。

让我们考虑另一个包含两个文档的玩具语料库:

>>> from sklearn.feature_extraction.text import CountVectorizer
>>> corpus = [
>>>     'He ate the sandwiches',
>>>     'Every sandwich was eaten by him'
>>> ]
>>> vectorizer = CountVectorizer(binary=True, stop_words='english')
>>> print vectorizer.fit_transform(corpus).todense()
>>> print vectorizer.vocabulary_
[[1 0 0 1]
 [0 1 1 0]]
{u'sandwich': 2, u'ate': 0, u'sandwiches': 3, u'eaten': 1}

这些文档具有相似的含义,但它们的特征向量没有共同的元素。两个文档都包含了ate的一个词形和sandwich的一个屈折形式。理想情况下,这些相似性应该体现在特征向量中。词形还原是根据词语的上下文确定屈折词的词形,即形态学根词的过程。词形是单词的基础形式,用来在字典中查找单词。词干提取与词形还原的目标相似,但它并不试图生成单词的形态学根词。相反,词干提取移除所有看起来像是词缀的字符模式,结果是一个不一定是有效单词的词形。词形还原通常需要一个词汇资源,如 WordNet,以及单词的词性。词干提取算法通常使用规则而非词汇资源来生成词干,并且可以在任何令牌上运行,即使没有其上下文。

让我们考虑在两个文档中对单词gathering进行词形还原:

corpus = [
    'I am gathering ingredients for the sandwich.',
    'There were many wizards at the gathering.'
]

在第一句中,gathering是动词,其词形还原为gather。在第二句中,gathering是名词,其词形还原为gathering。我们将使用自然语言工具包NLTK)来进行词干提取和词形还原。可以通过www.nltk.org/install.html上的说明安装 NLTK。安装完成后,执行以下代码:

>>> import nltk
>>> nltk.download()

然后按照说明下载 NLTK 的语料库。

使用gathering的词性,NLTK 的WordNetLemmatizer正确地对两个文档中的单词进行了词形还原,结果如以下示例所示:

>>> from nltk.stem.wordnet import WordNetLemmatizer
>>> lemmatizer = WordNetLemmatizer()
>>> print lemmatizer.lemmatize('gathering', 'v')
>>> print lemmatizer.lemmatize('gathering', 'n')
gather
gathering

让我们将词形还原与词干提取进行比较。Porter 词干提取器无法考虑屈折形式的词性,并且对两个文档都返回gather

>>> from nltk.stem import PorterStemmer
>>> stemmer = PorterStemmer()
>>> print stemmer.stem('gathering')
gather

现在让我们对我们的示例语料库进行词形还原:

>>> from nltk import word_tokenize
>>> from nltk.stem import PorterStemmer
>>> from nltk.stem.wordnet import WordNetLemmatizer
>>> from nltk import pos_tag
>>> wordnet_tags = ['n', 'v']
>>> corpus = [
>>>     'He ate the sandwiches',
>>>     'Every sandwich was eaten by him'
>>> ]
>>> stemmer = PorterStemmer()
>>> print 'Stemmed:', [[stemmer.stem(token) for token in word_tokenize(document)] for document in corpus]
>>> def lemmatize(token, tag):
>>>     if tag[0].lower() in ['n', 'v']:
>>>         return lemmatizer.lemmatize(token, tag[0].lower())
>>>     return token
>>> lemmatizer = WordNetLemmatizer()
>>> tagged_corpus = [pos_tag(word_tokenize(document)) for document in corpus]
>>> print 'Lemmatized:', [[lemmatize(token, tag) for token, tag in document] for document in tagged_corpus]
Stemmed: [['He', 'ate', 'the', 'sandwich'], ['Everi', 'sandwich', 'wa', 'eaten', 'by', 'him']]
Lemmatized: [['He', 'eat', 'the', 'sandwich'], ['Every', 'sandwich', 'be', 'eat', 'by', 'him']]

通过词干提取和词形还原,我们减少了特征空间的维度。尽管语料库字典中的单词在句子中屈折形式不同,我们仍然生成了更有效地编码文档含义的特征表示。

扩展词袋模型并加入 TF-IDF 权重

在上一节中,我们使用了词袋模型(bag-of-words)表示法来创建特征向量,这些向量编码了语料库字典中的单词是否出现在文档中。这些特征向量并没有编码语法、单词顺序或单词的频率。直观地说,单词在文档中出现的频率可以表明该文档与该单词的相关程度。包含一个单词出现的长文档可能讨论的主题完全不同于包含该单词多次出现的文档。在本节中,我们将创建编码单词频率的特征向量,并讨论解决由编码术语频率所引发的两个问题的策略。

我们现在不再使用二进制值表示特征向量中的每个元素,而是使用一个整数,表示单词在文档中出现的次数。

我们将使用以下语料库。经过停用词过滤后,语料库通过以下特征向量表示:

>>> from sklearn.feature_extraction.text import CountVectorizer
>>> corpus = ['The dog ate a sandwich, the wizard transfigured a sandwich, and I ate a sandwich']
>>> vectorizer = CountVectorizer(stop_words='english')
>>> print vectorizer.fit_transform(corpus).todense()
[[2 1 3 1 1]]
{u'sandwich': 2, u'wizard': 4, u'dog': 1, u'transfigured': 3, u'ate': 0}

dog 的元素现在设置为 1,而 sandwich 的元素设置为 2,表示相应的单词分别出现了一次和两次。请注意,CountVectorizerbinary 关键字参数被省略了;它的默认值是 False,这导致它返回的是原始的词频而非二值频率。在特征向量中对词汇的原始频率进行编码提供了更多关于文档含义的信息,但假设所有文档的长度大致相同。

在两个文档中,可能会有许多单词的出现频率相同,但如果一个文档的长度比另一个大得多,两个文档仍然可能是不同的。scikit-learn 的 TfdfTransformer 对象可以通过将词频向量矩阵转换为标准化的词频权重矩阵来缓解这个问题。默认情况下,TfdfTransformer 会对原始计数进行平滑处理,并应用 L2 标准化。平滑后的标准化词频由以下公式给出:

扩展词袋模型与 TF-IDF 权重

扩展词袋模型与 TF-IDF 权重 是文档 扩展词袋模型与 TF-IDF 权重 中词 扩展词袋模型与 TF-IDF 权重 的频率,且 扩展词袋模型与 TF-IDF 权重 是计数向量的 L2 范数。除了对原始词频进行归一化之外,我们还可以通过计算对数缩放的词频来改进特征向量,这将词频缩放到更有限的范围,或者通过计算增强的词频来进一步减轻较长文档的偏差。对数缩放的词频由以下公式给出:

扩展词袋模型与 TF-IDF 权重

TfdfTransformer 对象的 sublinear_tf 关键字参数设置为 True 时,它会计算对数缩放的词频。增强的频率由以下公式给出:

扩展词袋模型与 TF-IDF 权重

扩展词袋模型与 TF-IDF 权重 是文档 扩展词袋模型与 TF-IDF 权重 中所有单词的最大频率。scikit-learn 0.15.2 版本没有实现增强词频,但可以轻松地将 CountVectorizer 的输出转换。

归一化、对数尺度的词频和增强的词频可以表示文档中词项的频率,同时减轻文档大小不同所带来的影响。然而,这些表示方法仍然存在一个问题。特征向量中对于在文档中频繁出现的词项赋予了很大的权重,即使这些词项在语料库的大多数文档中也频繁出现。这些词项并不能有效地帮助表示特定文档相对于整个语料库的含义。例如,大部分关于杜克大学篮球队的文章可能都会出现basketballCoach Kflop这些词汇。这些词汇可以被视为语料库特有的停用词,可能对计算文档相似度没有帮助。逆文档频率IDF)是衡量词项在语料库中稀有或常见程度的指标。逆文档频率的计算公式如下:

扩展词袋模型与 TF-IDF 权重

在这里,扩展词袋模型与 TF-IDF 权重表示语料库中文档的总数,扩展词袋模型与 TF-IDF 权重表示包含术语扩展词袋模型与 TF-IDF 权重的文档数量。一个术语的TF-IDF值是其词频和逆文档频率的乘积。TfidfTransformer在其use_idf关键字参数设置为默认值True时返回 TF-IDF 的权重。由于 TF-IDF 加权特征向量通常用于表示文本,scikit-learn 提供了一个TfidfVectorizer类,它封装了CountVectorizerTfidfTransformer。我们可以使用TfidfVectorizer为我们的语料库创建 TF-IDF 加权特征向量:

>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> corpus = [
>>>     'The dog ate a sandwich and I ate a sandwich',
>>>     'The wizard transfigured a sandwich'
>>> ]
>>> vectorizer = TfidfVectorizer(stop_words='english')
>>> print vectorizer.fit_transform(corpus).todense()
[[ 0.75458397  0.37729199  0.53689271  0\.          0\.        ]
 [ 0\.          0\.          0.44943642  0.6316672   0.6316672 ]]

通过将 TF-IDF 权重与原始词频进行比较,我们可以看到,像沙拉这种在语料库中许多文档中都出现的词汇已经被削弱权重。

空间高效的特征向量化方法,使用哈希技巧

在本章之前的示例中,使用一个包含语料库所有唯一词项的字典,将文档中的词项映射到特征向量的元素。创建这个字典有两个缺点。首先,语料库需要进行两次遍历:第一次遍历用来创建字典,第二次遍历用来为文档创建特征向量。其次,字典必须存储在内存中,对于大型语料库来说,这可能是不可接受的。通过对词项应用哈希函数来直接确定其在特征向量中的索引,可以避免创建字典。这个捷径称为哈希技巧。以下示例使用HashingVectorizer来演示哈希技巧:

>>> from sklearn.feature_extraction.text import HashingVectorizer
>>> corpus = ['the', 'ate', 'bacon', 'cat']
>>> vectorizer = HashingVectorizer(n_features=6)
>>> print vectorizer.transform(corpus).todense()
[[-1\.  0\.  0\.  0\.  0\.  0.]
 [ 0\.  0\.  0\.  1\.  0\.  0.]
 [ 0\.  0\.  0\.  0\. -1\.  0.]
 [ 0\.  1\.  0\.  0\.  0\.  0.]]

哈希技巧是无状态的。它可以用于在并行和在线或流式应用中创建特征向量,因为它不需要对语料库进行初始遍历。请注意,n_features是一个可选的关键字参数。它的默认值,空间高效的特征向量化与哈希技巧,对于大多数问题来说是足够的;这里设置为6,以使矩阵足够小,既可以打印出来,又能显示所有非零特征。此外,请注意,某些词频是负数。由于哈希冲突是可能的,HashingVectorizer使用了一个带符号的哈希函数。特征的值与其标记的哈希值具有相同的符号;如果词语cats在文档中出现两次,并且其哈希值为-3,则文档特征向量的第四个元素将减少二。若词语dogs也出现两次,并且哈希值为3,特征向量的第四个元素将增加二。使用带符号的哈希函数可以使哈希冲突的错误互相抵消,而不是累积;信息丢失优于信息丢失和引入虚假信息。哈希技巧的另一个缺点是,生成的模型更难以检查,因为哈希函数无法回忆出每个特征向量元素映射的输入标记是什么。

从图像中提取特征

计算机视觉是研究和设计处理和理解图像的计算工件。这些工件有时会使用机器学习。计算机视觉的概述远远超出了本书的范围,但在本节中,我们将回顾一些在计算机视觉中用于表示机器学习问题中图像的基本技术。

从像素强度中提取特征

数字图像通常是一个栅格(或像素图),它将颜色映射到网格上的坐标。图像可以被视为一个矩阵,其中每个元素代表一个颜色。图像的基本特征表示可以通过将矩阵的行连接在一起并重新塑形为一个向量来构造。光学字符识别OCR)是一个典型的机器学习问题。我们将使用此技术创建可以用于 OCR 应用程序的基本特征表示,用于识别字符分隔的表单中的手写数字。

scikit-learn 附带的digits数据集包含超过 1,700 个手写数字(从零到九)的灰度图像。每个图像的边长为八个像素。每个像素由一个强度值表示,范围在零到 16 之间;白色是最强的,表示为零,黑色是最弱的,表示为 16。下图是从数据集中提取的手写数字图像:

从像素强度中提取特征

让我们通过将 8 x 8 的矩阵重新塑形为 64 维向量,为图像创建一个特征向量:

>>> from sklearn import datasets
>>> digits = datasets.load_digits()
>>> print 'Digit:', digits.target[0]
>>> print digits.images[0]
>>> print 'Feature vector:\n', digits.images[0].reshape(-1, 64)
Digit: 0
[[  0\.   0\.   5\.  13\.   9\.   1\.   0\.   0.]
 [  0\.   0\.  13\.  15\.  10\.  15\.   5\.   0.]
 [  0\.   3\.  15\.   2\.   0\.  11\.   8\.   0.]
 [  0\.   4\.  12\.   0\.   0\.   8\.   8\.   0.]
 [  0\.   5\.   8\.   0\.   0\.   9\.   8\.   0.]
 [  0\.   4\.  11\.   0\.   1\.  12\.   7\.   0.]
 [  0\.   2\.  14\.   5\.  10\.  12\.   0\.   0.]
 [  0\.   0\.   6\.  13\.  10\.   0\.   0\.   0.]]
Feature vector:
[[  0\.   0\.   5\.  13\.   9\.   1\.   0\.   0\.   0\.   0\.  13\.  15\.  10\.  15.
    5\.   0\.   0\.   3\.  15\.   2\.   0\.  11\.   8\.   0\.   0\.   4\.  12\.   0.
    0\.   8\.   8\.   0\.   0\.   5\.   8\.   0\.   0\.   9\.   8\.   0\.   0\.   4.
   11\.   0\.   1\.  12\.   7\.   0\.   0\.   2\.  14\.   5\.  10\.  12\.   0\.   0.
    0\.   0\.   6\.  13\.  10\.   0\.   0\.   0.]]

这种表示方法对于一些基本任务(如识别打印字符)是有效的。然而,记录图像中每个像素的强度会生成过于庞大的特征向量。一张 100 x 100 的灰度图像将需要一个 10,000 维的向量,而一张 1920 x 1080 的灰度图像将需要一个 2,073,600 维的向量。与我们之前创建的 TF-IDF 特征向量不同,在大多数问题中,这些向量并不是稀疏的。空间复杂度并不是这种表示方法的唯一缺点;从特定位置像素的强度中学习会导致模型对图像的尺度、旋转和位移变化敏感。训练于我们基本特征表示的模型可能无法识别即使是轻微位移、放大或旋转几度后的相同数字。此外,从像素强度中学习本身也有问题,因为模型可能会对光照变化产生敏感性。因此,这种表示方法对于涉及照片或其他自然图像的任务是无效的。现代计算机视觉应用通常使用手工设计的特征提取方法,这些方法适用于许多不同的问题,或者使用深度学习等技术自动学习无监督问题的特征。在下一节中,我们将重点介绍前者。

将兴趣点提取为特征

我们之前创建的特征向量表示了图像中的每个像素;图像的所有信息属性和噪声属性都被表示出来。检查训练数据后,我们可以发现所有图像的边缘都有一圈白色像素;这些像素并不是有用的特征。人类可以快速识别许多物体,而不需要观察物体的每个属性。我们可以通过观察车盖的轮廓来识别一辆车,而无需观察后视镜;我们可以通过鼻子或嘴巴来识别一张人脸的图像。这种直觉促使我们创建仅包含图像最有信息量属性的表示。这些信息属性或兴趣点是那些被丰富纹理包围,并且即使在图像扰动的情况下也能被重现的点。边缘角点是两种常见的兴趣点类型。边缘是像素强度快速变化的边界,角点是两条边缘的交点。我们使用 scikit-image 提取下图中的兴趣点:

将兴趣点提取为特征

提取兴趣点的代码如下:

>>> import numpy as nps
>>> from skimage.feature import corner_harris, corner_peaks
>>> from skimage.color import rgb2gray
>>> import matplotlib.pyplot as plt
>>> import skimage.io as io
>>> from skimage.exposure import equalize_hist

>>> def show_corners(corners, image):
>>>     fig = plt.figure()
>>>     plt.gray()
>>>     plt.imshow(image)
>>>     y_corner, x_corner = zip(*corners)
>>>     plt.plot(x_corner, y_corner, 'or')
>>>     plt.xlim(0, image.shape[1])
>>>     plt.ylim(image.shape[0], 0)
>>>     fig.set_size_inches(np.array(fig.get_size_inches()) * 1.5)
>>>     plt.show()

>>> mandrill = io.imread('/home/gavin/PycharmProjects/mastering-machine-learning/ch4/img/mandrill.png')
>>> mandrill = equalize_hist(rgb2gray(mandrill))
>>> corners = corner_peaks(corner_harris(mandrill), min_distance=2)
>>> show_corners(corners, mandrill)

下图展示了提取的兴趣点。在该图像的 230400 个像素中,有 466 个被提取为兴趣点。这种表示方式更为紧凑;理想情况下,兴趣点附近有足够的变化,能够重建它们,即使图像的光照发生了变化。

提取兴趣点作为特征

SIFT 和 SURF

尺度不变特征变换SIFT)是一种从图像中提取特征的方法,相较于我们之前讨论的提取方法,它对图像的尺度、旋转和光照的敏感度较低。每个 SIFT 特征或描述符是一个向量,用于描述图像某一区域的边缘和角点。与我们之前示例中的兴趣点不同,SIFT 还捕捉了每个兴趣点及其周围环境的组成信息。加速稳健特征SURF)是另一种从图像中提取兴趣点并创建与图像的尺度、方向和光照无关的描述的方法。SURF 的计算速度比 SIFT 更快,并且在识别经过某些变换的图像特征时更为有效。

解释 SIFT 和 SURF 提取的实现超出了本书的范围。然而,通过对它们工作原理的直觉理解,我们仍然可以有效地使用实现这些算法的库。

在这个示例中,我们将使用mahotas库从以下图像中提取 SURF。

SIFT 和 SURF

就像提取的兴趣点一样,提取的 SURF 只是创建特征表示的第一步,这些表示可以用于机器学习任务。在训练集中,每个实例都会提取不同的 SURF。在第六章,K 均值聚类中,我们将聚类提取的 SURF,以学习可以被图像分类器使用的特征。在以下示例中,我们将使用mahotas库来提取 SURF 描述符:

>>> import mahotas as mh
>>> from mahotas.features import surf

>>> image = mh.imread('zipper.jpg', as_grey=True)
>>> print 'The first SURF descriptor:\n', surf.surf(image)[0]
>>> print 'Extracted %s SURF descriptors' % len(surf.surf(image))
The first SURF descriptor:
[  6.73839947e+02   2.24033945e+03   3.18074483e+00   2.76324459e+03
  -1.00000000e+00   1.61191475e+00   4.44035121e-05   3.28041690e-04
   2.44845817e-04   3.86297608e-04  -1.16723672e-03  -8.81290243e-04
   1.65414959e-03   1.28393061e-03  -7.45077384e-04   7.77655540e-04
   1.16078772e-03   1.81434398e-03   1.81736394e-04  -3.13096961e-04
    3.06559785e-04   3.43443699e-04   2.66200498e-04  -5.79522387e-04
   1.17893036e-03   1.99547411e-03  -2.25938217e-01  -1.85563853e-01
   2.27973631e-01   1.91510135e-01  -2.49315698e-01   1.95451021e-01
   2.59719480e-01   1.98613061e-01  -7.82458546e-04   1.40287015e-03
   2.86712113e-03   3.15971628e-03   4.98444730e-04  -6.93986983e-04
   1.87531652e-03   2.19041521e-03   1.80681053e-01  -2.70528820e-01
   2.32414943e-01   2.72932870e-01   2.65725332e-01   3.28050743e-01
   2.98609869e-01   3.41623138e-01   1.58078002e-03  -4.67968721e-04
   2.35704122e-03   2.26279888e-03   6.43115065e-06   1.22501486e-04
   1.20064616e-04   1.76564805e-04   2.14148537e-03   8.36243899e-05
   2.93382280e-03   3.10877776e-03   4.53469215e-03  -3.15254535e-04
   6.92437341e-03   3.56880279e-03  -1.95228401e-04   3.73674995e-05
   7.02700555e-04   5.45156362e-04]
Extracted 994 SURF descriptors

数据标准化

许多估算器在训练标准化数据集时表现更好。标准化数据具有零均值单位方差。一个具有零均值的解释变量是围绕原点居中的;它的平均值为零。特征向量具有单位方差,当其特征的方差都在同一数量级时,才满足单位方差。例如,假设一个特征向量编码了两个解释变量,第一个变量的值范围从零到一,第二个解释变量的值范围从零到 100,000。第二个特征必须缩放到接近{0,1}的范围,以确保数据具有单位方差。如果一个特征的方差比其他特征大几个数量级,那么这个特征可能会主导学习算法,导致其无法从其他变量中学习。一些学习算法在数据没有标准化时,也会更慢地收敛到最佳参数值。可以通过减去变量的均值并将差值除以变量的标准差来标准化一个解释变量。使用 scikit-learn 的scale函数可以轻松地标准化数据:

>>> from sklearn import preprocessing
>>> import numpy as np
>>> X = np.array([
>>>     [0., 0., 5., 13., 9., 1.],
>>>     [0., 0., 13., 15., 10., 15.],
>>>     [0., 3., 15., 2., 0., 11.]
>>> ])
>>> print preprocessing.scale(X)
[[ 0\.         -0.70710678 -1.38873015  0.52489066  0.59299945 -1.35873244]
 [ 0\.         -0.70710678  0.46291005  0.87481777  0.81537425  1.01904933]
 [ 0\.          1.41421356  0.9258201  -1.39970842 -1.4083737   0.33968311]]

摘要

在本章中,我们讨论了特征提取,并对将任意数据转化为机器学习算法可以使用的特征表示的基本技术有了理解。首先,我们使用独热编码和 scikit-learn 的DictVectorizer从类别型解释变量中创建了特征。然后,我们讨论了为机器学习问题中最常见的数据类型之一——文本数据——创建特征向量。我们研究了词袋模型的几种变体,该模型舍弃了所有语法信息,仅对文档中令牌的频率进行编码。我们从创建基本的二进制词频开始,使用了CountVectorizer。你学会了通过过滤停用词和词干提取令牌来预处理文本,并且将特征向量中的词频替换为 TF-IDF 权重,这些权重对常见词语进行了惩罚,并对不同长度的文档进行了标准化。接下来,我们为图像创建了特征向量。我们从一个光学字符识别问题开始,其中我们将手写数字的图像表示为像素强度的平铺矩阵。这是一种计算上比较昂贵的方法。我们通过仅提取图像中最有趣的点作为 SURF 描述符,改进了图像的表示。

最后,你学会了如何标准化数据,以确保我们的估算器能够从所有解释变量中学习,并且能够尽可能快速地收敛。我们将在后续章节的示例中使用这些特征提取技术。在下一章,我们将结合词袋模型表示法和多重线性回归的泛化方法来分类文档。

第四章:从线性回归到逻辑回归

在第二章,线性回归中,我们讨论了简单线性回归、多元线性回归和多项式回归。这些模型是广义线性模型的特例,广义线性模型是一种灵活的框架,相比普通线性回归,它要求的假设更少。在本章中,我们将讨论这些假设,尤其是它们与广义线性模型的另一个特例——逻辑回归的关系。

与我们之前讨论的模型不同,逻辑回归用于分类任务。回想一下,在分类任务中,目标是找到一个函数,将一个观测值映射到其关联的类别或标签。学习算法必须使用特征向量对及其对应的标签来推导映射函数参数的值,从而生成最佳分类器,这个分类器的性能通过特定的性能度量来衡量。在二分类问题中,分类器必须将实例分配到两个类别中的一个。二分类的例子包括预测病人是否患有某种疾病,音频样本是否包含人类语音,或者杜克大学男篮队是否会在 NCAA 锦标赛第一轮输掉比赛。在多分类任务中,分类器必须为每个实例分配多个标签中的一个。在多标签分类中,分类器必须为每个实例分配多个标签中的一个子集。在本章中,我们将通过几个使用逻辑回归的分类问题,讨论分类任务的性能度量,并应用你在上一章中学到的一些特征提取技术。

使用逻辑回归进行二分类

普通线性回归假设响应变量服从正态分布。正态 分布,也称为高斯分布钟形曲线,是一个描述观测值在任意两个实数之间的概率的函数。正态分布的数据是对称的。也就是说,一半的值大于均值,另一半的值小于均值。正态分布数据的均值、中位数和众数也相等。许多自然现象大致遵循正态分布。例如,人的身高是正态分布的;大多数人身高适中,少数人高,少数人矮。

在某些问题中,响应变量并非服从正态分布。例如,抛硬币可以有两个结果:正面或反面。伯努利分布描述了一个随机变量的概率分布,该变量以概率P取正类,以概率1-P取负类。如果响应变量代表一个概率,它必须限制在{0,1}的范围内。线性回归假设解释变量值的恒定变化会导致响应变量值的恒定变化,但如果响应变量的值代表一个概率,这一假设就不成立。广义线性模型通过使用链接函数将解释变量的线性组合与响应变量联系起来,从而消除了这一假设。事实上,我们已经在第二章中使用了一个链接函数,线性回归;普通线性回归是广义线性模型的一个特例,它使用恒等链接函数将解释变量的线性组合与正态分布的响应变量联系起来。我们可以使用不同的链接函数将解释变量的线性组合与非正态分布的响应变量联系起来。

在逻辑回归中,响应变量描述了结果为正类的概率。如果响应变量等于或超过判别阈值,则预测为正类;否则,预测为负类。响应变量被建模为解释变量线性组合的逻辑 函数的函数。逻辑函数由以下方程给出,它始终返回一个介于零和一之间的值:

使用逻辑回归的二分类

以下是逻辑函数在区间{-6,6}范围内的值的图示:

使用逻辑回归的二分类

对于逻辑回归,使用逻辑回归的二分类等同于解释变量的线性组合,如下所示:

使用逻辑回归的二分类

Logit 函数是逻辑函数的逆函数。它将使用逻辑回归的二分类与解释变量的线性组合联系起来:

使用逻辑回归的二分类

现在我们已经定义了逻辑回归模型,接下来让我们将其应用于一个二分类任务。

垃圾邮件过滤

我们的第一个问题是经典的二分类问题的现代版本:垃圾邮件分类。然而,在我们的版本中,我们将对垃圾短信和正常短信进行分类,而不是电子邮件。我们将使用在第三章,特征提取与预处理中学到的技术从短信中提取 TF-IDF 特征,并使用逻辑回归对短信进行分类。

我们将使用来自 UCI 机器学习库的 SMS 垃圾邮件分类数据集。该数据集可以从archive.ics.uci.edu/ml/datasets/SMS+Spam+Collection下载。首先,让我们探索数据集,并使用 pandas 计算一些基本的统计信息:

>>> import pandas as pd
>>> df = pd.read_csv('data/SMSSpamCollection', delimiter='\t', header=None)
>>> print df.head()

      0                                                  1
0   ham  Go until jurong point, crazy.. Available only ...
1   ham                      Ok lar... Joking wif u oni...
2  spam  Free entry in 2 a wkly comp to win FA Cup fina...
3   ham  U dun say so early hor... U c already then say...
4   ham  Nah I don't think he goes to usf, he lives aro...
[5 rows x 2 columns]

>>> print 'Number of spam messages:', df[df[0] == 'spam'][0].count()
>>> print 'Number of ham messages:', df[df[0] == 'ham'][0].count()

Number of spam messages: 747
Number of ham messages: 4825

每行数据包含一个二进制标签和一条短信。数据集包含 5,574 个实例;4,827 条消息是正常短信,剩余 747 条是垃圾短信。正常短信标记为零,垃圾短信标记为一。虽然值得注意的或案例结果通常被分配标签一,而非案例结果通常分配标签零,但这些分配是任意的。检查数据可能会揭示应当在模型中捕获的其他属性。以下是一些典型消息,代表了两类短信:

Spam: Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005\. Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's
Spam: WINNER!! As a valued network customer you have been selected to receivea £900 prize reward! To claim call 09061701461\. Claim code KL341\. Valid 12 hours only.
Ham: Sorry my roommates took forever, it ok if I come by now?
Ham: Finished class where are you.

让我们使用 scikit-learn 的LogisticRegression类进行一些预测:

>>> import numpy as np
>>> import pandas as pd
>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> from sklearn.linear_model.logistic import LogisticRegression
>>> from sklearn.cross_validation import train_test_split, cross_val_score

首先,我们使用 pandas 加载.csv文件,并将数据集划分为训练集和测试集。默认情况下,train_test_split()将 75%的样本分配到训练集中,并将剩余的 25%的样本分配到测试集中:

>>> df = pd.read_csv('data/SMSSpamCollection', delimiter='\t', header=None)
>>> X_train_raw, X_test_raw, y_train, y_test = train_test_split(df[1], df[0])

接下来,我们创建一个TfidfVectorizer。回顾第三章,特征提取与预处理TfidfVectorizer结合了CountVectorizerTfidfTransformer。我们用训练短信对其进行拟合,并对训练和测试短信进行转换:

>>> vectorizer = TfidfVectorizer()
>>> X_train = vectorizer.fit_transform(X_train_raw)
>>> X_test = vectorizer.transform(X_test_raw)

最后,我们创建一个LogisticRegression实例并训练我们的模型。像LinearRegression一样,LogisticRegression实现了fit()predict()方法。作为一个合理性检查,我们打印了一些预测结果供人工检查:

>>> classifier = LogisticRegression()
>>> classifier.fit(X_train, y_train)
>>> predictions = classifier.predict(X_test)
>>> for i, prediction in enumerate(predictions[:5]):
>>>     print 'Prediction: %s. Message: %s' % (prediction, X_test_raw[i])

以下是脚本的输出:

Prediction: ham. Message: If you don't respond imma assume you're still asleep and imma start calling n shit
Prediction: spam. Message: HOT LIVE FANTASIES call now 08707500020 Just 20p per min NTT Ltd, PO Box 1327 Croydon CR9 5WB 0870 is a national rate call
Prediction: ham. Message: Yup... I havent been there before... You want to go for the yoga? I can call up to book 
Prediction: ham. Message: Hi, can i please get a  &lt;#&gt;  dollar loan from you. I.ll pay you back by mid february. Pls.
Prediction: ham. Message: Where do you need to go to get it?

我们的分类器表现如何?我们用于线性回归的性能指标对于这个任务不合适。我们关心的只是预测类别是否正确,而不是距离决策边界有多远。在接下来的部分,我们将讨论一些可以用来评估二分类器的性能指标。

二分类性能指标

存在多种指标来评估二元分类器对可信标签的性能。最常见的指标包括准确性精确率召回率F1 度量ROC AUC 分数。所有这些度量都依赖于真正预测真负预测假正预测假负预测的概念。是指类别。表示预测类别是否与真实类别相同。

对于我们的短信垃圾分类器,真正预测是指分类器正确预测消息是垃圾。真负预测是指分类器正确预测消息是非垃圾。将非垃圾消息预测为垃圾是假正预测,将垃圾消息错误分类为非垃圾是假负预测。混淆矩阵列联表可以用来可视化真正和假正、假负预测。矩阵的行是实例的真实类别,列是实例的预测类别:

>>> from sklearn.metrics import confusion_matrix
>>> import matplotlib.pyplot as plt

>>> y_test = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]
>>> y_pred = [0, 1, 0, 0, 0, 0, 0, 1, 1, 1]
>>> confusion_matrix = confusion_matrix(y_test, y_pred)
>>> print(confusion_matrix)
>>> plt.matshow(confusion_matrix)
>>> plt.title('Confusion matrix')
>>> plt.colorbar()
>>> plt.ylabel('True label')
>>> plt.xlabel('Predicted label')
>>> plt.show()

 [[4 1]
 [2 3]]

混淆矩阵显示有四个真负预测,三个真正预测,两个假负预测和一个假正预测。在多类问题中,混淆矩阵变得更加有用,因为在这些问题中确定最常见的错误类型可能很困难。

二分类性能指标

准确性

准确性衡量分类器预测正确的分数比例。scikit-learn 提供一个函数来计算给定正确标签的一组预测的准确性:

>>> from sklearn.metrics import accuracy_score
>>> y_pred, y_true = [0, 1, 1, 0], [1, 1, 1, 1]
>>> print 'Accuracy:', accuracy_score(y_true, y_pred)

Accuracy: 0.5

LogisticRegression.score() 使用准确率预测和评分测试集标签。让我们评估我们分类器的准确性:

>>> import numpy as np
>>> import pandas as pd
>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> from sklearn.linear_model.logistic import LogisticRegression
>>> from sklearn.cross_validation import train_test_split, cross_val_score
>>> df = pd.read_csv('data/sms.csv')
>>> X_train_raw, X_test_raw, y_train, y_test = train_test_split(df['message'], df['label'])
>>> vectorizer = TfidfVectorizer()
>>> X_train = vectorizer.fit_transform(X_train_raw)
>>> X_test = vectorizer.transform(X_test_raw)
>>> classifier = LogisticRegression()
>>> classifier.fit(X_train, y_train)
>>> scores = cross_val_score(classifier, X_train, y_train, cv=5)
>>> print np.mean(scores), scores

Accuracy 0.956217208018 [ 0.96057348  0.95334928  0.96411483  0.95454545  0.94850299]

请注意,由于训练集和测试集是随机分配的,您的准确性可能会有所不同。虽然准确性衡量了分类器的整体正确性,但它不区分假正误差和假负误差。有些应用程序对假负误差比假正误差更敏感,反之亦然。此外,如果人群中类别的比例不均衡,准确性不是一个信息丰富的度量。例如,一个预测信用卡交易是否欺诈的分类器可能更容易对假负误差敏感,而不是对假正误差敏感。为了促进客户满意度,信用卡公司可能更愿意冒险验证合法交易,而不是冒险忽略欺诈交易。由于大多数交易是合法的,准确性不是这个问题的合适度量。一个总是预测交易合法的分类器可能有很高的准确性分数,但是并不实用。因此,分类器通常使用称为精确率和召回率的两个额外指标进行评估。

精确率和召回率

回顾第一章,机器学习基础,精确度是正确预测为正类的比例。例如,在我们的 SMS 垃圾邮件分类器中,精确度是被分类为垃圾邮件的消息中实际是垃圾邮件的比例。精确度通过以下比率给出:

精确度和召回率

在医学领域,有时称为敏感度,召回率是分类器识别的真正正实例的比例。召回率为 1 表示分类器没有做出任何假阴性预测。对于我们的 SMS 垃圾邮件分类器,召回率是正确分类为垃圾邮件的垃圾邮件消息的比例。召回率通过以下比率计算:

精确度和召回率

单独来看,精确度和召回率通常并不能提供足够的信息;它们都是分类器性能的片面视图。精确度和召回率都可能无法区分表现良好的分类器和某些表现不佳的分类器。一种简单的分类器通过对每个实例都预测为正类,可以轻松获得完美的召回率。例如,假设测试集包含十个正类实例和十个负类实例。一个对每个实例都预测为正类的分类器将获得 1 的召回率,如下所示:

精确度和召回率

一个对每个示例都预测为负类的分类器,或者只做出假阳性和真阴性预测的分类器,将会获得零的召回率。同样,一个预测只有单个实例为正类且恰好正确的分类器,将获得完美的精确度。

scikit-learn 提供了一个函数,可以根据一组预测和相应的可信标签集来计算分类器的精确度和召回率。让我们计算一下我们 SMS 分类器的精确度和召回率:

>>> import numpy as np
>>> import pandas as pd
>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> from sklearn.linear_model.logistic import LogisticRegression
>>> from sklearn.cross_validation import train_test_split, cross_val_score
>>> df = pd.read_csv('data/sms.csv')
>>> X_train_raw, X_test_raw, y_train, y_test = train_test_split(df['message'], df['label'])
>>> vectorizer = TfidfVectorizer()
>>> X_train = vectorizer.fit_transform(X_train_raw)
>>> X_test = vectorizer.transform(X_test_raw)
>>> classifier = LogisticRegression()
>>> classifier.fit(X_train, y_train)
>>> precisions = cross_val_score(classifier, X_train, y_train, cv=5, scoring='precision')
>>> print 'Precision', np.mean(precisions), precisions
>>> recalls = cross_val_score(classifier, X_train, y_train, cv=5, scoring='recall')
>>> print 'Recalls', np.mean(recalls), recalls

Precision 0.992137651822 [ 0.98717949  0.98666667  1\.          0.98684211  1\.        ]
Recall 0.677114261885 [ 0.7         0.67272727  0.6         0.68807339  0.72477064]

我们分类器的精确度是 0.992;几乎所有它预测为垃圾邮件的消息实际上都是垃圾邮件。它的召回率较低,这表明它错误地将大约 22% 的垃圾邮件消息误分类为非垃圾邮件。由于训练和测试数据是随机划分的,您的精确度和召回率可能有所不同。

计算 F1 得分

F1 得分是精确度和召回率的调和平均数或加权平均数。也称为 f 值或 f 得分,F1 得分通过以下公式计算:

计算 F1 得分

F1 度量值惩罚那些精度和召回率不平衡的分类器,例如总是预测正类的简单分类器。一个具有完美精度和召回率的模型将获得 F1 分数为 1。一个具有完美精度但召回率为零的模型将获得 F1 分数为零。至于精度和召回率,scikit-learn 提供了一个函数来计算一组预测的 F1 分数。让我们计算我们分类器的 F1 分数。以下代码片段继续前面的示例:

>>> f1s = cross_val_score(classifier, X_train, y_train, cv=5, scoring='f1')
>>> print 'F1', np.mean(f1s), f1s

F1 0.80261302628 [ 0.82539683  0.8         0.77348066  0.83157895  0.7826087 ]

我们分类器的精度和召回率的算术平均值为 0.803。由于分类器的精度和召回率差异较小,因此 F1 度量的惩罚较小。有时模型会使用 F0.5 和 F2 分数进行评估,分别偏向精度和召回率。

ROC AUC

接收者操作特征(Receiver Operating Characteristic),或称ROC 曲线,可视化分类器的性能。与准确率不同,ROC 曲线对类别不平衡的数据集不敏感;与精度和召回率不同,ROC 曲线展示了分类器在所有判别阈值下的表现。ROC 曲线绘制了分类器的召回率与其误报率之间的关系。误报率,或称假阳性率,是假阳性数量除以总负样本数。它通过以下公式计算:

ROC AUC

AUC是 ROC 曲线下的面积,它将 ROC 曲线简化为一个值,该值代表分类器的预期性能。下图中的虚线表示一个随机预测类别的分类器,它的 AUC 为 0.5。实线则表示一个优于随机猜测的分类器:

ROC AUC

让我们绘制我们 SMS 垃圾短信分类器的 ROC 曲线:

>>> import numpy as np
>>> import pandas as pd
>>> import matplotlib.pyplot as plt
>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> from sklearn.linear_model.logistic import LogisticRegression
>>> from sklearn.cross_validation import train_test_split, cross_val_score
>>> from sklearn.metrics import roc_curve, auc
>>> df = pd.read_csv('data/sms.csv')
>>> X_train_raw, X_test_raw, y_train, y_test = train_test_split(df['message'], df['label'])
>>> vectorizer = TfidfVectorizer()
>>> X_train = vectorizer.fit_transform(X_train_raw)
>>> X_test = vectorizer.transform(X_test_raw)
>>> classifier = LogisticRegression()
>>> classifier.fit(X_train, y_train)
>>> predictions = classifier.predict_proba(X_test)
>>> false_positive_rate, recall, thresholds = roc_curve(y_test, predictions[:, 1])
>>> roc_auc = auc(false_positive_rate, recall)
>>> plt.title('Receiver Operating Characteristic')
>>> plt.plot(false_positive_rate, recall, 'b', label='AUC = %0.2f' % roc_auc)
>>> plt.legend(loc='lower right')
>>> plt.plot([0, 1], [0, 1], 'r--')
>>> plt.xlim([0.0, 1.0])
>>> plt.ylim([0.0, 1.0])
>>> plt.ylabel('Recall')
>>> plt.xlabel('Fall-out')
>>> plt.show()

从 ROC AUC 图中可以明显看出,我们的分类器优于随机猜测;大部分图形区域位于其曲线下方:

ROC AUC

使用网格搜索调优模型

超参数是模型的参数,它们不是通过学习获得的。例如,我们的逻辑回归 SMS 分类器的超参数包括正则化项的值和用于去除频繁或不频繁出现的词汇的阈值。在 scikit-learn 中,超参数通过模型的构造函数进行设置。在之前的示例中,我们没有为 LogisticRegression() 设置任何参数;我们使用了所有超参数的默认值。这些默认值通常是一个好的起点,但它们可能无法产生最优模型。网格搜索是一种常见的方法,用于选择产生最佳模型的超参数值。网格搜索为每个需要调整的超参数提供一组可能的值,并评估在这些值的笛卡尔积元素上训练的模型。也就是说,网格搜索是一种穷举搜索,它为开发者提供的每种超参数值的所有可能组合训练和评估模型。网格搜索的一个缺点是,即使对于小型的超参数值集合,它的计算成本也很高。幸运的是,这是一个尴尬的并行问题;因为进程之间不需要同步,许多模型可以轻松并行训练和评估。让我们使用 scikit-learn 的 GridSearchCV() 函数来寻找更好的超参数值:

import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model.logistic import LogisticRegression
from sklearn.grid_search import GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.cross_validation import train_test_split
from sklearn.metrics import precision_score, recall_score, accuracy_score

pipeline = Pipeline([
    ('vect', TfidfVectorizer(stop_words='english')),
    ('clf', LogisticRegression())
])
parameters = {
    'vect__max_df': (0.25, 0.5, 0.75),
    'vect__stop_words': ('english', None),
    'vect__max_features': (2500, 5000, 10000, None),
    'vect__ngram_range': ((1, 1), (1, 2)),
    'vect__use_idf': (True, False),
    'vect__norm': ('l1', 'l2'),
    'clf__penalty': ('l1', 'l2'),
    'clf__C': (0.01, 0.1, 1, 10),
}

GridSearchCV() 接受一个估计器、一个参数空间和一个性能衡量标准。参数 n_jobs 指定并行作业的最大数量;将 n_jobs 设置为 -1 以使用所有 CPU 核心。请注意,fit() 必须在 Python 的 main 块中调用,以便分叉出额外的进程;此示例必须作为脚本执行,而不是在交互式解释器中运行:

if __name__ == "__main__":
    grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1, scoring='accuracy', cv=3)
    df = pd.read_csv('data/sms.csv')
    X, y, = df['message'], df['label']
    X_train, X_test, y_train, y_test = train_test_split(X, y)
    grid_search.fit(X_train, y_train)
    print 'Best score: %0.3f' % grid_search.best_score_
    print 'Best parameters set:'
    best_parameters = grid_search.best_estimator_.get_params()
    for param_name in sorted(parameters.keys()):
        print '\t%s: %r' % (param_name, best_parameters[param_name])
    predictions = grid_search.predict(X_test)
    print 'Accuracy:', accuracy_score(y_test, predictions)
    print 'Precision:', precision_score(y_test, predictions)
    print 'Recall:', recall_score(y_test, predictions)

以下是脚本的输出:

Fitting 3 folds for each of 1536 candidates, totalling 4608 fits
[Parallel(n_jobs=-1)]: Done   1 jobs       | elapsed:    0.2s
[Parallel(n_jobs=-1)]: Done  50 jobs       | elapsed:    4.0s
[Parallel(n_jobs=-1)]: Done 200 jobs       | elapsed:   16.9s
[Parallel(n_jobs=-1)]: Done 450 jobs       | elapsed:   36.7s
[Parallel(n_jobs=-1)]: Done 800 jobs       | elapsed:  1.1min
[Parallel(n_jobs=-1)]: Done 1250 jobs       | elapsed:  1.7min
[Parallel(n_jobs=-1)]: Done 1800 jobs       | elapsed:  2.5min
[Parallel(n_jobs=-1)]: Done 2450 jobs       | elapsed:  3.4min
[Parallel(n_jobs=-1)]: Done 3200 jobs       | elapsed:  4.4min
[Parallel(n_jobs=-1)]: Done 4050 jobs       | elapsed:  7.7min
[Parallel(n_jobs=-1)]: Done 4608 out of 4608 | elapsed:  8.5min finished
Best score: 0.983
Best parameters set:
  clf__C: 10
  clf__penalty: 'l2'
  vect__max_df: 0.5
  vect__max_features: None
  vect__ngram_range: (1, 2)
  vect__norm: 'l2'
  vect__stop_words: None
  vect__use_idf: True
Accuracy: 0.989956958393
Precision: 0.988095238095
Recall: 0.932584269663

优化超参数的值提高了我们模型在test集上的召回率。

多类分类

在前面的章节中,你学会了使用逻辑回归进行二分类。然而,在许多分类问题中,通常会涉及多个类别。我们可能希望根据音频样本预测歌曲的类型,或根据星系的类型对图像进行分类。多类分类的目标是将实例分配给一组类别中的一个。scikit-learn 使用一种称为一对多(one-vs.-all)或一对其余(one-vs.-the-rest)的方法来支持多类分类。一对多分类为每个可能的类别使用一个二分类器。预测时,具有最大置信度的类别会被分配给该实例。LogisticRegression 天生支持使用一对多策略进行多类分类。让我们使用 LogisticRegression 来解决一个多类分类问题。

假设你想看一部电影,但你对观看糟糕的电影有强烈的排斥感。为了帮助你做出决策,你可以阅读一些关于你考虑的电影的评论,但不幸的是,你也对阅读电影评论有强烈的排斥感。让我们使用 scikit-learn 来找出评价较好的电影。

在这个例子中,我们将对来自 Rotten Tomatoes 数据集中的电影评论短语进行情感分类。每个短语可以被分类为以下情感之一:负面、稍微负面、中立、稍微正面或正面。虽然这些类别看似有序,但我们将使用的解释变量并不总是能支持这种顺序,因为讽刺、否定和其他语言现象的存在。相反,我们将把这个问题作为一个多类分类任务来处理。

数据可以从www.kaggle.com/c/sentiment-analysis-on-movie-reviews/data下载。首先,让我们使用 pandas 来探索数据集。请注意,以下代码片段中的导入和数据加载语句是后续代码片段所必需的:

>>> import pandas as pd
>>> df = pd.read_csv('movie-reviews/train.tsv', header=0, delimiter='\t')
>>> print df.count()

PhraseId      156060
SentenceId    156060
Phrase        156060
Sentiment     156060
dtype: int64

数据集的列是以制表符分隔的。数据集包含 156,060 个实例。

>>> print df.head()

   PhraseId  SentenceId                                             Phrase  \
0         1           1  A series of escapades demonstrating the adage ...
1         2           1  A series of escapades demonstrating the adage ...
2         3           1                                           A series
3         4           1                                                  A
4         5           1                                             series

   Sentiment
0          1
1          2
2          2
3          2
4          2

[5 rows x 4 columns]

情感列包含响应变量。0 标签对应情感负面1 对应稍微负面,依此类推。短语列包含原始文本。每个电影评论的句子已被解析成更小的短语。在这个例子中,我们不需要短语 ID句子 ID列。让我们打印一些短语并查看它们:

>>> print df['Phrase'].head(10)

0    A series of escapades demonstrating the adage ...
1    A series of escapades demonstrating the adage ...
2                                             A series
3                                                    A
4                                               series
5    of escapades demonstrating the adage that what...
6                                                   of
7    escapades demonstrating the adage that what is...
8                                            escapades
9    demonstrating the adage that what is good for ...
Name: Phrase, dtype: object

现在让我们来查看目标类别:

>>> print df['Sentiment'].describe()

count    156060.000000
mean          2.063578
std           0.893832
min           0.000000
25%           2.000000
50%           2.000000
75%           3.000000
max           4.000000
Name: Sentiment, dtype: float64

>>> print df['Sentiment'].value_counts()

2    79582
3    32927
1    27273
4     9206
0     7072
dtype: int64

>>> print df['Sentiment'].value_counts()/df['Sentiment'].count()

2    0.509945
3    0.210989
1    0.174760
4    0.058990
0    0.045316
dtype: float64

最常见的类别是中立,它包含超过 50% 的实例。准确度对于这个问题来说并不是一个有意义的性能衡量标准,因为一个退化的分类器只预测中立就能获得接近 0.5 的准确度。大约四分之一的评论是正面或稍微正面的,约五分之一的评论是负面或稍微负面的。让我们用 scikit-learn 来训练一个分类器:

import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model.logistic import LogisticRegression
from sklearn.cross_validation import train_test_split
from sklearn.metrics.metrics import classification_report, accuracy_score, confusion_matrix
from sklearn.pipeline import Pipeline
from sklearn.grid_search import GridSearchCV

def main():
    pipeline = Pipeline([
        ('vect', TfidfVectorizer(stop_words='english')),
        ('clf', LogisticRegression())
    ])
    parameters = {
        'vect__max_df': (0.25, 0.5),
        'vect__ngram_range': ((1, 1), (1, 2)),
        'vect__use_idf': (True, False),
        'clf__C': (0.1, 1, 10),
    }
    df = pd.read_csv('data/train.tsv', header=0, delimiter='\t')
    X, y = df['Phrase'], df['Sentiment'].as_matrix()
    X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5)
    grid_search = GridSearchCV(pipeline, parameters, n_jobs=3, verbose=1, scoring='accuracy')
    grid_search.fit(X_train, y_train)
    print 'Best score: %0.3f' % grid_search.best_score_
    print 'Best parameters set:'
    best_parameters = grid_search.best_estimator_.get_params()
    for param_name in sorted(parameters.keys()):
        print '\t%s: %r' % (param_name, best_parameters[param_name])

if __name__ == '__main__':
    main()

以下是脚本的输出:

Fitting 3 folds for each of 24 candidates, totalling 72 fits
[Parallel(n_jobs=3)]: Done   1 jobs       | elapsed:    3.3s
[Parallel(n_jobs=3)]: Done  50 jobs       | elapsed:  1.1min
[Parallel(n_jobs=3)]: Done  68 out of  72 | elapsed:  1.9min remaining:    6.8s
[Parallel(n_jobs=3)]: Done  72 out of  72 | elapsed:  2.1min finished
Best score: 0.620
Best parameters set:
  clf__C: 10
  vect__max_df: 0.25
  vect__ngram_range: (1, 2)
  vect__use_idf: False

多类分类性能指标

与二分类一样,混淆矩阵有助于可视化分类器所犯的错误类型。可以为每个类别计算精确度、召回率和 F1 分数,同时也可以计算所有预测的准确性。让我们评估分类器的预测结果。以下代码片段继续上一个示例:

    predictions = grid_search.predict(X_test)
    print 'Accuracy:', accuracy_score(y_test, predictions)
    print 'Confusion Matrix:', confusion_matrix(y_test, predictions)
    print 'Classification Report:', classification_report(y_test, predictions)

以下内容将被追加到输出中:

Accuracy: 0.636370626682
Confusion Matrix: [[ 1129  1679   634    64     9]
 [  917  6121  6084   505    35]
 [  229  3091 32688  3614   166]
 [   34   408  6734  8068  1299]
 [    5    35   494  2338  1650]]
Classification Report:              precision    recall  f1-score   support

          0       0.49      0.32      0.39      3515
          1       0.54      0.45      0.49     13662
          2       0.70      0.82      0.76     39788
          3       0.55      0.49      0.52     16543
          4       0.52      0.36      0.43      4522

avg / total       0.62      0.64      0.62     78030

首先,我们使用通过网格搜索找到的最佳参数集进行预测。尽管我们的分类器比基线分类器有所改进,但它仍然常常将稍微正面稍微负面误分类为中立

多标签分类和问题转化

在前面的部分,我们讨论了二分类问题,其中每个实例必须被分配到两个类别中的一个;还有多分类问题,其中每个实例必须被分配到类别集合中的一个。接下来我们要讨论的分类问题类型是多标签分类问题,其中每个实例可以被分配到类别集合的一个子集。多标签分类的例子包括为论坛上的帖子分配标签,以及对图像中的物体进行分类。多标签分类的方法有两种类型。

问题转化方法是将原始的多标签问题转化为一组单标签分类问题的技术。我们将要回顾的第一个问题转化方法是将训练数据中遇到的每个标签集转化为单一标签。例如,考虑一个多标签分类问题,在该问题中,新闻文章必须从一组类别中分配到一个或多个类别。以下训练数据包含七篇文章,这些文章可以涉及五个类别中的一个或多个。

类别
实例 本地
1
2
3
4
5
6
7

将问题转化为一个单标签分类任务,使用在训练数据中看到的标签的幂集,最终得到以下训练数据。之前,第一个实例被分类为本地美国。现在它只有一个标签,本地 多标签分类和问题转化 美国

类别
实例 本地
1
2
3
4
5
6
7

具有五个类别的多标签分类问题现在变成了一个具有七个类别的多类分类问题。虽然幂集问题转换直观易懂,但增加类别的数量通常是不可行的;此转换可能会生成许多新标签,而这些标签仅对应少数训练实例。此外,分类器只能预测在训练数据中出现过的标签组合。

类别 类别
实例 本地 非本地 实例
1 1
2 2
3 3
4 4
5 5
6 6
7 7
类别 类别
实例 美国 非美国 实例
1 1
2 2
3 3
4 4
5 5
6 6
7 7
类别 类别
实例 科学与技术 非科学与技术 实例
1 1
2 2
3 3
4 4
5 5
6 6

第二种问题转换是为训练集中的每个标签训练一个二分类器。每个分类器预测实例是否属于某个标签。我们的示例需要五个二分类器;第一个分类器预测实例是否应该被分类为Local,第二个分类器预测实例是否应该被分类为US,依此类推。最终预测是所有二分类器预测的联合结果。转换后的训练数据如上图所示。此问题转换确保单标签问题将拥有与多标签问题相同数量的训练样本,但忽略了标签之间的关系。

多标签分类性能评估指标

多标签分类问题必须使用与单标签分类问题不同的性能评估指标进行评估。最常见的两种性能指标是汉明损失贾卡尔相似度。汉明损失是错误标签的平均比例。需要注意的是,汉明损失是一个损失函数,完美的得分是零。贾卡尔相似度,或称贾卡尔指数,是预测标签与真实标签交集的大小除以预测标签与真实标签并集的大小。它的取值范围从零到一,得分为一时表示完美。贾卡尔相似度的计算公式如下:

多标签分类性能评估指标

>>> import numpy as np
>>> from sklearn.metrics import hamming_loss
>>> print hamming_loss(np.array([[0.0, 1.0], [1.0, 1.0]]), np.array([[0.0, 1.0], [1.0, 1.0]]))
0.0
>>> print hamming_loss(np.array([[0.0, 1.0], [1.0, 1.0]]), np.array([[1.0, 1.0], [1.0, 1.0]]))
0.25
>>> print hamming_loss(np.array([[0.0, 1.0], [1.0, 1.0]]), np.array([[1.0, 1.0], [0.0, 1.0]]))
0.5
>>> print jaccard_similarity_score(np.array([[0.0, 1.0], [1.0, 1.0]]), np.array([[0.0, 1.0], [1.0, 1.0]]))
1.0
>>> print jaccard_similarity_score(np.array([[0.0, 1.0], [1.0, 1.0]]), np.array([[1.0, 1.0], [1.0, 1.0]]))
0.75
>>> print jaccard_similarity_score(np.array([[0.0, 1.0], [1.0, 1.0]]), np.array([[1.0, 1.0], [0.0, 1.0]]))
0.5

总结

本章我们讨论了广义线性模型,它将普通线性回归扩展,以支持具有非正态分布的响应变量。广义线性模型使用连接函数将解释变量的线性组合与响应变量关联起来;与普通线性回归不同,关系不必是线性的。特别地,我们考察了逻辑连接函数,这是一种 sigmoid 函数,它对任何实数值返回一个介于零和一之间的值。

我们讨论了逻辑回归,它是一种广义线性模型,使用逻辑连接函数将解释变量与伯努利分布的响应变量关联起来。逻辑回归可用于二分类任务,在该任务中,实例必须被分配到两个类别中的一个;我们使用逻辑回归来分类垃圾短信和正常短信。接着我们讨论了多类分类任务,在此任务中,每个实例必须从标签集合中分配一个标签。我们使用一对多策略来分类电影评论的情感。最后,我们讨论了多标签分类任务,其中实例必须被分配到标签集合的子集。完成了广义线性模型的回归与分类讨论后,我们将在下一章介绍一种非线性模型——决策树,用于回归和分类。

第五章:使用决策树进行非线性分类和回归

在前几章中,我们讨论了广义线性模型,它通过链接函数将解释变量的线性组合与一个或多个响应变量联系起来。你学习了如何使用多元线性回归来解决回归问题,并且我们使用了逻辑回归来处理分类任务。在这一章中,我们将讨论一个用于分类和回归任务的简单非线性模型:决策树。我们将使用决策树构建一个广告拦截器,它能够学习将网页上的图像分类为横幅广告或页面内容。最后,我们将介绍集成学习方法,这些方法结合多个模型来生成一个估算器,其预测性能优于任何单一模型。

决策树

决策树是类似树状的图形,用于建模决策过程。它们类似于游戏《二十个问题》。在《二十个问题》中,一名玩家,称为回答者,选择一个物体,但不向其他玩家透露物体是什么,这些其他玩家被称为提问者。物体应该是常见名词,比如“吉他”或“三明治”,而不是“1969 年吉布森 Les Paul Custom 吉他”或“北卡罗来纳”。提问者必须通过提问最多二十个可以回答“是”、“否”或“也许”的问题来猜测物体。提问者的一个直观策略是逐步提高问题的具体性;作为第一个问题问“它是乐器吗?”不会有效减少可能的答案数量。决策树的分支指定了可以检查的最短解释变量序列,以估算响应变量的值。延续这个类比,在《二十个问题》游戏中,提问者和回答者都知道训练数据,但只有回答者知道测试实例的特征值。

决策树通常通过递归地将训练实例集根据实例的解释变量值划分为子集来学习。以下图示展示了一个决策树,我们将在本章稍后更详细地讨论它。

决策树

决策树的内部节点通过框表示,用来测试解释变量。这些节点通过边连接,边上指定了测试结果的可能性。训练实例根据测试结果被分为多个子集。例如,一个节点可能会测试某个解释变量的值是否超过某个阈值。通过测试的实例将沿着边连接到节点的右子节点,而未通过测试的实例将沿着边连接到节点的左子节点。子节点同样测试它们的训练实例子集,直到满足停止准则。在分类任务中,决策树的叶节点代表类别。在回归任务中,叶节点中包含的实例的响应变量值可能会被平均,以生成响应变量的估计值。构建完决策树后,为一个测试实例做预测只需要沿着边走,直到到达叶节点。

训练决策树

我们将使用一种名为迭代二分法 3ID3)的算法来创建决策树。ID3 由罗斯·昆兰发明,是最早用于训练决策树的算法之一。假设你需要将动物分类为猫或狗。不幸的是,你不能直接观察这些动物,只能通过它们的几个属性来做决定。对于每只动物,你会被告知它是否喜欢玩接飞盘,它是否经常生气,以及它最喜欢哪三种食物之一。

为了分类新的动物,决策树将在每个节点检查一个解释变量。它将根据测试结果选择沿着哪条边到达下一个节点。例如,第一个节点可能会问动物是否喜欢玩接飞盘。如果喜欢,我们将沿着边走到左子节点;如果不喜欢,我们将沿着边走到右子节点。最终,一条边将连接到叶节点,指示该动物是猫还是狗。

以下十四个实例构成我们的训练数据:

训练实例 玩接飞盘 是否生气 最喜欢的食物 物种
1 培根
2 狗粮
3 猫粮
4 培根
5 猫粮
6 培根
7 猫粮
8 狗粮
9 猫粮
10 狗粮
11 培根
12 猫粮
13 猫粮
14 培根

从这些数据我们可以看出,猫通常比狗更易怒。大多数狗喜欢玩接飞盘,而大多数猫拒绝。狗喜欢狗粮和培根,而猫只喜欢猫粮和培根。is grumpyplays fetch 这两个解释变量可以轻松转换为二进制特征。favorite food 这个解释变量是一个类别变量,具有三个可能的值;我们将对其进行独热编码。回顾第三章,特征提取与预处理,独热编码通过与变量值相同数量的二进制特征来表示类别变量。如果用单个整数值特征表示类别变量,会对其值编码一种人为的顺序。由于 favorite food 有三个可能的状态,我们将用三个二进制特征来表示它。从这个表中,我们可以手动构建分类规则。例如,一个既易怒又喜欢猫粮的动物一定是猫,而一个喜欢玩接飞盘并喜欢培根的动物一定是狗。即使是对于一个小数据集,手动构建这些分类规则也非常繁琐。相反,我们将通过创建决策树来学习这些规则。

选择问题

像“二十个问题”一样,决策树通过测试一系列解释变量的值来估算响应变量的值。应该首先测试哪个解释变量?直观地说,产生包含所有猫或所有狗的子集的测试要优于产生仍包含猫和狗的子集的测试。如果子集中的成员属于不同的类别,我们仍然无法确定如何对实例进行分类。我们还应该避免创建只将一只猫或一只狗从其他动物中分离出来的测试;这种测试类似于在“二十个问题”的前几轮问特定问题。更正式地说,这些测试可能很少对实例进行分类,并且可能不会减少我们的不确定性。减少分类不确定性的测试是最好的。我们可以使用一种叫做的度量来量化不确定性的大小。

用比特(bits)来衡量,熵量化了变量中的不确定性。熵由以下方程给出,其中 选择问题 是结果的数量,![选择问题

例如,一次掷公平硬币只有两种结果:正面和反面。硬币正面朝上的概率是 0.5,反面朝上的概率也是 0.5。那么该掷投的熵等于以下公式:

选择问题

也就是说,只需要一个比特就能表示两个同样可能的结果:正面和反面。两次掷公平硬币可以产生四种可能的结果:正正、正反、反正和反反。每个结果的概率是0.5 x 0.5 = 0.25。两次掷投的熵等于以下公式:

选择问题

如果硬币的两面是相同的,那么表示其结果的变量的熵为 0 比特;也就是说,我们对结果始终确定,变量永远不会代表新信息。熵也可以表示为比特的分数。例如,不公平硬币有两面不同,但其加权使得两面并非等可能出现。假设不公平硬币正面朝上的概率为 0.8,反面朝上的概率为 0.2。那么该硬币一次掷投的熵等于以下公式:

选择问题

一次不公平硬币的掷投结果可以有一个比特的分数熵。掷投有两个可能的结果,但我们并不完全不确定,因为其中一个结果更为常见。

让我们计算分类一个未知动物的熵。如果我们的动物分类训练数据中狗和猫的数量相等,而我们对该动物一无所知,那么决策的熵为 1。我们所知道的只是该动物可能是猫或狗;就像公平的掷硬币一样,两个结果的可能性是相等的。然而,我们的训练数据包含了六只狗和八只猫。如果我们对未知动物一无所知,那么决策的熵可以通过以下公式来表示:

选择问题

由于猫更常见,我们对结果的确定性更高。现在让我们找出最有助于分类该动物的解释变量;也就是说,我们要找出能最大程度降低熵的解释变量。我们可以测试玩接飞盘这一解释变量,并将训练实例划分为玩接飞盘的动物和不玩接飞盘的动物。这会产生以下两个子集:

选择问题

决策树通常以类似流程图的图示方式呈现。上一个图示的顶端框是根节点;它包含我们所有的训练实例,并指定将要测试的解释变量。在根节点,我们还没有从训练集中排除任何实例,熵值大约等于 0.985。根节点测试plays fetch这一解释变量。回想一下,我们将这个布尔型解释变量转换为二值特征。对于plays fetch等于零的训练实例,沿着左子树的边缘走;而对于喜欢玩接球的动物,则沿着右子树的边缘走。左子节点包含一个子集,包含七只不喜欢玩接球的猫和两只狗。该节点的熵值如下所示:

选择问题

右子树包含一个子集,其中有一只猫和四只喜欢玩接球的狗。该节点的熵值如下所示:

选择问题

我们可以测试is grumpy这一解释变量,而不是测试plays fetch。此测试产生了以下决策树。与之前的树一样,未通过测试的实例沿左边缘走,已通过测试的实例沿右边缘走。

选择问题

我们也可以将实例划分为偏好猫粮的动物和不偏好猫粮的动物,以产生以下决策树:

选择问题

信息增益

对偏好猫粮的动物进行测试,结果得到了两个子集,一个子集包含六只猫、零只狗和 0 比特的熵,另一个子集包含两只猫、六只狗和 0.811 比特的熵。我们如何衡量这些测试中,哪个减少了我们对分类的不确定性最多呢?对这些子集熵值的平均值似乎是衡量熵减少的一种合适方法。在这个例子中,猫粮测试产生的子集具有最低的平均熵。直观上,这个测试似乎是有效的,因为我们可以用它来分类几乎一半的训练实例。然而,选择产生最低平均熵的子集的测试可能会产生一个次优的决策树。例如,假设有一个测试,产生了一个子集,里面有两只狗和没有猫,另一个子集包含四只狗和八只猫。第一个子集的熵值等于以下(注意第二项被省略,因为信息增益未定义):

信息增益

第二个子集的熵值等于以下值:

信息增益

这些子集的平均熵值仅为 0.459,但包含大部分实例的子集几乎有一位的熵。这类似于在“二十个问题”中早早问一些具体问题;我们可能会幸运地在前几次就猜中,但更有可能的是我们会浪费问题而没有排除很多可能性。相反,我们将使用一种叫做信息增益的度量来衡量熵的减少。通过以下公式计算,信息增益是父节点的熵值与子节点熵值加权平均值之间的差异。信息增益是实例的集合,信息增益是正在测试的解释变量,信息增益是实例信息增益的属性值,信息增益是属性信息增益值等于信息增益的实例数量,信息增益是解释变量信息增益值为信息增益的实例子集的熵值。

信息增益

以下表格包含了所有测试的信息增益。在这种情况下,猫粮测试仍然是最好的,因为它能最大程度地增加信息增益。

测试 父节点熵 子节点熵 子节点熵 加权平均 信息增益
会捡球吗? 0.9852 0.7642 0.7219 0.7490 * 9/14 + 0.7219 * 5/14 = 0.7491 0.2361
是不是易怒? 0.9852 0.9183 0.8113 0.9183 * 6/14 + 0.8113 * 8/14 = 0.85710.8572 0.1280
最喜欢的食物 = 猫粮 0.9852 0.8113 0 0.8113 * 8 /14 + 0.0 * 6/14 = 0.4636 0.5216
最喜欢的食物 = 狗粮 0.9852 0.8454 0 0.8454 * 11/14 + 0.0 * 3/14 = 0.6642 0.3210
最喜欢的食物 = 培根 0.9852 0.9183 0.971 0.9183 * 9/14 + 0.9710 * 5/14 = 0.9371 0.0481

现在让我们在树上添加另一个节点。由该测试生成的子节点之一是一个叶子节点,里面只有猫。另一个节点仍然包含两只猫和六只狗。我们将在此节点上添加一个测试。剩下的哪些解释变量能最大程度地减少我们的不确定性?以下表格包含了所有可能测试的信息增益:

测试 父节点熵 子节点熵 子节点熵 加权平均 信息增益
会捡球吗? 0.8113 1 0 1.0 * 4/8 + 0 * 4/8 = 0.5 0.3113
是不是易怒? 0.8113 0 1 0.0 * 4/8 + 1 * 4/8 = 0.5 0.3113
最喜欢的食物=狗粮 0.8113 0.9710 0 0.9710 * 5/8 + 0.0 * 3/8 = 0.6069 0.2044
最喜欢的食物=培根 0.8113 0 0.9710 0.0 * 3/8 + 0.9710 * 5/8 = 0.6069 0.2044

所有的测试都产生熵为 0 的子集,但 性格烦躁玩接球 测试产生了最大的 信息增益。ID3 通过任意选择最佳测试来打破平局。我们将选择 性格烦躁 测试,它将父节点的八个实例分割成一个包含四只狗的叶节点和一个包含两只猫和两只狗的节点。以下是当前树的图示:

信息增益

我们现在将选择另一个解释变量来测试子节点的四个实例。剩下的测试项 最喜欢的食物=培根最喜欢的食物=狗粮玩接球,都会产生一个包含一只狗或一只猫的叶节点,以及一个包含剩余动物的节点。剩下的测试项产生相等的信息增益,如下表所示:

测试 父节点的熵 子节点的熵 子节点的熵 加权平均 信息增益
玩接球? 1 0.9183 0 0.688725 0.311275
最喜欢的食物=狗粮 1 0.9183 0 0.688725 0.311275
最喜欢的食物=培根 1 0 0.9183 0.688725 0.311275

我们将任意选择 玩接球 测试,产生一个包含一只狗的叶节点和一个包含两只猫和一只狗的节点。剩下两个解释变量,我们可以测试喜欢培根的动物,或者可以测试喜欢狗粮的动物。这两项测试会产生相同的子集,并创建一个包含一只狗的叶节点和一个包含两只猫的叶节点。我们将任意选择测试喜欢狗粮的动物。以下是完成的决策树图示:

信息增益

让我们对以下测试数据中的一些动物进行分类:

测试实例 玩接球 性格烦躁 最喜欢的食物 物种
1 培根
2 狗粮
3 狗粮
4 培根
5 猫粮

让我们对第一只动物进行分类,它喜欢玩接球,性格不常烦躁,并且喜欢培根。我们将沿着边缘走到根节点的左子节点,因为这只动物最喜欢的食物不是猫粮。它不烦躁,因此我们将沿着边缘走到第二级节点的左子节点。这里是一个叶节点,只有狗;我们已正确分类该实例。为了将第三个测试实例分类为猫,我们沿着边缘走到根节点的左子节点,再走到第二级节点的右子节点,再走到第三级节点的左子节点,最后走到四级节点的右子节点。

恭喜!你已经使用 ID3 算法构建了一个决策树。其他算法也可以用来训练决策树。C4.5 是 ID3 的改进版本,能够处理连续的解释变量,并且可以处理缺失的特征值。C4.5 还可以对树进行剪枝。剪枝通过将分类较少实例的分支替换为叶节点来减少树的大小。由 scikit-learn 实现的决策树使用了 CART,这是一种支持剪枝的学习算法。

基尼不纯度

在上一节中,我们通过创建产生最大信息增益的节点构建了一个决策树。学习决策树的另一个常见启发式方法是基尼不纯度,它衡量一个集合中各类别的比例。基尼不纯度由以下公式给出,其中 基尼不纯度 是类别的数量,基尼不纯度 是该节点的实例子集,基尼不纯度 是从节点子集中选择类别 基尼不纯度 的元素的概率:

基尼不纯度

直观地讲,当集合中的所有元素都属于同一类别时,基尼不纯度为零,因为选择该类别元素的概率等于一。像熵一样,当每个类别被选择的概率相等时,基尼不纯度最大。基尼不纯度的最大值取决于可能的类别数量,它由以下公式给出:

基尼不纯度

我们的问题有两个类别,因此基尼不纯度的最大值将等于一半。scikit-learn 支持使用信息增益和基尼不纯度来学习决策树。没有明确的规则来帮助你决定何时使用其中一个标准;在实际应用中,它们通常会产生相似的结果。与机器学习中的许多决策一样,最好比较使用两种选项训练的模型的表现。

使用 scikit-learn 训练决策树

让我们使用决策树创建一个可以屏蔽网页上横幅广告的软件。该程序将预测网页上每张图像是否是广告或文章内容。被分类为广告的图像可以使用层叠样式表(CSS)隐藏。我们将使用来自archive.ics.uci.edu/ml/datasets/Internet+Advertisements互联网广告数据集来训练一个决策树分类器,该数据集包含 3,279 张图像的数据。类别的比例是倾斜的;459 张图像是广告,2,820 张是内容。决策树学习算法可能会根据不平衡类别比例的数据生成偏向的树;我们将在不修改数据集的情况下评估模型,然后决定是否值得通过过采样或欠采样实例来平衡训练数据。解释变量是图像的维度、包含页面的 URL 中的词、图像 URL 中的词、图像的 alt 文本、图像的锚文本以及围绕图像标签的词窗口。响应变量是图像的类别。解释变量已经转换为特征表示。前三个特征是实数,表示图像的宽度、高度和宽高比。其余特征表示文本变量的二元词频。在接下来的示例中,我们将使用网格搜索寻找产生最高准确度的决策树超参数值,然后在测试集上评估该树的表现:

import pandas as pd
from sklearn.tree import DecisionTreeClassifier
from sklearn.cross_validation import train_test_split
from sklearn.metrics import classification_report
from sklearn.pipeline import Pipeline
from sklearn.grid_search import GridSearchCV

首先,我们使用 pandas 读取 .csv 文件。该 .csv 文件没有标题行,因此我们使用其索引将包含响应变量值的最后一列与特征分开:

if __name__ == '__main__':
    df = pd.read_csv('data/ad.data', header=None)
    explanatory_variable_columns = set(df.columns.values)
    response_variable_column = df[len(df.columns.values)-1]
    # The last column describes the targets
    explanatory_variable_columns.remove(len(df.columns.values)-1)

    y = [1 if e == 'ad.' else 0 for e in response_variable_column]
    X = df[list(explanatory_variable_columns)]

我们将广告编码为正类,将内容编码为负类。超过四分之一的实例至少缺少一个图像维度的值。这些缺失值用空格和问号标记。我们用负一替换了缺失值,但我们本可以填补缺失值;例如,我们可以用平均高度值替换缺失的高度值:

    X.replace(to_replace=' *\?', value=-1, regex=True, inplace=True)

然后,我们将数据拆分为训练集和测试集:

    X_train, X_test, y_train, y_test = train_test_split(X, y)

我们创建了一个管道和 DecisionTreeClassifier 的实例。然后,我们将 criterion 关键字参数设置为 entropy,以便使用信息增益启发式方法构建树:

    pipeline = Pipeline([
        ('clf', DecisionTreeClassifier(criterion='entropy'))
    ])

接下来,我们指定了网格搜索的超参数空间:

    parameters = {
        'clf__max_depth': (150, 155, 160),
        'clf__min_samples_split': (1, 2, 3),
        'clf__min_samples_leaf': (1, 2, 3)
    }

我们将 GridSearchCV() 设置为最大化模型的 F1 分数:

    grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1, scoring='f1')
    grid_search.fit(X_train, y_train)
    print 'Best score: %0.3f' % grid_search.best_score_
    print 'Best parameters set:'
    best_parameters = grid_search.best_estimator_.get_params()
    for param_name in sorted(parameters.keys()):
        print '\t%s: %r' % (param_name, best_parameters[param_name])

    predictions = grid_search.predict(X_test)
    print classification_report(y_test, predictions)

Fitting 3 folds for each of 27 candidates, totalling 81 fits
[Parallel(n_jobs=-1)]: Done   1 jobs       | elapsed:    1.7s
[Parallel(n_jobs=-1)]: Done  50 jobs       | elapsed:   15.0s
[Parallel(n_jobs=-1)]: Done  71 out of  81 | elapsed:   20.7s remaining:    2.9s
[Parallel(n_jobs=-1)]: Done  81 out of  81 | elapsed:   23.3s finished
Best score: 0.878
Best parameters set:
	clf__max_depth: 155
	clf__min_samples_leaf: 2
	clf__min_samples_split: 1
             precision    recall  f1-score   support

          0       0.97      0.99      0.98       710
          1       0.92      0.81      0.86       110

avg / total       0.96      0.96      0.96       820

该分类器在测试集中检测到超过 80% 的广告,并且大约 92% 的它预测为广告的图像确实是广告。总体而言,性能非常有前景;在接下来的章节中,我们将尝试修改模型以提高其性能。

树集成

集成学习方法将一组模型组合起来,生成一个比其单个组件具有更好预测性能的估计器。随机森林是一个由在训练实例和解释变量的随机选择子集上训练的决策树集合组成的模型。随机森林通常通过返回其构成树预测的众数或均值来进行预测;scikit-learn 的实现返回树预测的均值。与决策树相比,随机森林更不容易过拟合,因为没有单棵树可以从所有实例和解释变量中学习;没有单棵树可以记住表示中的所有噪声。

让我们更新广告拦截器的分类器,改用随机森林。使用 scikit-learn 的 API,替换DecisionTreeClassifier非常简单;我们只需将对象替换为RandomForestClassifier的实例。像之前的例子一样,我们将使用网格搜索来找到生成具有最佳预测性能的随机森林的超参数值。

首先,从ensemble模块导入RandomForestClassifier类:

from sklearn.ensemble import RandomForestClassifier

接下来,将pipeline中的DecisionTreeClassifier替换为RandomForestClassifier实例,并更新超参数空间:

    pipeline = Pipeline([
        ('clf', RandomForestClassifier(criterion='entropy'))
    ])
    parameters = {
        'clf__n_estimators': (5, 10, 20, 50),
        'clf__max_depth': (50, 150, 250),
        'clf__min_samples_split': (1, 2, 3),
        'clf__min_samples_leaf': (1, 2, 3)
    }

输出如下:

Fitting 3 folds for each of 108 candidates, totalling 324 fits
[Parallel(n_jobs=-1)]: Done   1 jobs       | elapsed:    1.1s
[Parallel(n_jobs=-1)]: Done  50 jobs       | elapsed:   17.4s
[Parallel(n_jobs=-1)]: Done 200 jobs       | elapsed:  1.0min
[Parallel(n_jobs=-1)]: Done 324 out of 324 | elapsed:  1.6min finished
Best score: 0.936
Best parameters set:
   clf__max_depth: 250
   clf__min_samples_leaf: 1
   clf__min_samples_split: 3
   clf__n_estimators: 20
             precision    recall  f1-score   support

          0       0.97      1.00      0.98       705
          1       0.97      0.83      0.90       115

avg / total       0.97      0.97      0.97       820

用随机森林替代单一决策树显著降低了错误率;随机森林将广告的精确度和召回率分别提高到 0.97 和 0.83。

决策树的优缺点

使用决策树所涉及的妥协与我们讨论的其他模型不同。决策树易于使用。与许多学习算法不同,决策树不要求数据具有零均值和单位方差。虽然决策树可以容忍解释变量的缺失值,但当前 scikit-learn 的实现无法做到这一点。决策树甚至可以学会忽略与任务无关的解释变量。

小型决策树可以通过 scikit-learn 的tree模块中的export_graphviz函数轻松地进行解释和可视化。决策树的分支是逻辑谓词的结合,它们可以很容易地被可视化为流程图。决策树支持多输出任务,单一决策树可以用于多类分类,而无需采用像一对多这样的策略。

像我们讨论的其他模型一样,决策树是急切学习者。急切学习者必须在可以用来估计测试实例的值之前,从训练数据中构建一个与输入无关的模型,但一旦模型构建完成,它们可以相对快速地做出预测。相比之下,懒惰 学习者(如 k 近邻算法)则将所有的泛化推迟到必须做出预测时才进行。懒惰学习者不需要花时间训练,但与急切学习者相比,它们通常预测较慢。

决策树比我们讨论的许多模型更容易过拟合,因为它们的学习算法可能会产生庞大且复杂的决策树,完美地拟合每个训练实例,但却无法概括真实的关系。有几种技术可以缓解决策树中的过拟合问题。修剪是一种常见策略,通过去除决策树中一些最高的节点和叶子来减少过拟合,但在 scikit-learn 中尚未实现这一功能。然而,通过为树设置最大深度,或者仅在它们将包含的训练实例数量超过某个阈值时才创建子节点,也能达到类似效果。DecisionTreeClassifierDecisionTreeRegressor类提供了设置这些约束的关键字参数。创建随机森林也可以减少过拟合。

高效的决策树学习算法,如 ID3,是贪心的。它们通过做出局部最优决策来高效学习,但并不能保证产生全局最优的树。ID3 通过选择一系列解释变量来构建树。每个解释变量之所以被选择,是因为它比其他变量更能减少节点的不确定性。然而,为了找到全局最优树,可能需要进行局部次优的测试。

在我们的示例中,树的大小并不重要,因为我们保留了所有节点。然而,在实际应用中,树的增长可能会受到修剪或类似机制的限制。修剪具有不同形状的树可以产生具有不同性能的树。在实践中,通常由信息增益或基尼不纯度启发式方法指导的局部最优决策往往能产生一个可接受的决策树。

总结

本章中,我们学习了用于分类和回归的简单非线性模型——决策树。就像益智游戏“二十个问题”一样,决策树由一系列用于检查测试实例的问题组成。决策树的分支终止于叶子,叶子指定了响应变量的预测值。我们讨论了如何使用 ID3 算法训练决策树,该算法通过递归地将训练实例划分为子集,从而减少我们对响应变量值的不确定性。我们还讨论了集成学习方法,它通过结合一组模型的预测,生成具有更好预测性能的估算器。最后,我们使用随机森林预测网页上的图像是否为横幅广告。在下一章中,我们将介绍我们的第一个无监督学习任务:聚类。

第六章:K-Means 聚类

在前几章中,我们讨论了监督学习任务;我们研究了从标注训练数据中学习的回归和分类算法。在本章中,我们将讨论一种无监督学习任务——聚类。聚类用于在未标注的数据集中寻找相似观测值的群组。我们将讨论 K-Means 聚类算法,并将其应用于图像压缩问题,学习如何评估其性能。最后,我们将探讨一个结合聚类和分类的半监督学习问题。

请回想一下第一章,机器学习基础中提到的,无监督学习的目标是发现未标注训练数据中隐藏的结构或模式。聚类,或称为聚类分析,是将观测结果分组的任务,使得同一组或簇内的成员在给定的度量标准下比其他簇的成员更相似。与监督学习一样,我们将观测值表示为n维向量。例如,假设你的训练数据由下图中的样本组成:

K-Means 聚类

聚类可能会揭示以下两个群体,用方框和圆圈表示:

K-Means 聚类

聚类也可能揭示以下四个群体:

K-Means 聚类

聚类常用于探索数据集。社交网络可以进行聚类,以识别社区并建议人们之间缺失的连接。在生物学中,聚类用于寻找具有相似表达模式的基因群组。推荐系统有时会使用聚类来识别可能吸引用户的产品或媒体。在营销中,聚类用于寻找相似消费者的细分群体。在接下来的章节中,我们将通过一个使用 K-Means 算法进行数据集聚类的示例。

使用 K-Means 算法进行聚类

K-Means 算法是一种流行的聚类方法,因其速度和可扩展性而受到青睐。K-Means 是一个迭代过程,通过将聚类的中心,即质心,移动到其组成点的均值位置,并重新将实例分配到它们最近的聚类中。标题中的K-Means 算法的聚类是一个超参数,用于指定应该创建的聚类数;K-Means 会自动将观察值分配到聚类中,但无法确定适当的聚类数量。K-Means 算法的聚类必须是一个小于训练集实例数的正整数。有时,聚类问题的上下文会指定聚类数。例如,一个生产鞋子的公司可能知道它能够支持生产三种新款式。为了了解每种款式应该面向哪些客户群体,它对客户进行了调查,并从结果中创建了三个聚类。也就是说,K-Means 算法的聚类的值是由问题的上下文指定的。其他问题可能不需要特定数量的聚类,且最优聚类数可能模糊不清。我们将在本章后面讨论一种估算最优聚类数的启发式方法,称为肘部法则。

K-Means 的参数包括聚类质心的位置和分配给每个聚类的观察值。像广义线性模型和决策树一样,K-Means 参数的最优值是通过最小化一个成本函数来找到的。K-Means 的成本函数由以下公式给出:

K-Means 算法的聚类

在上述公式中,K-Means 算法的聚类是聚类K-Means 算法的聚类的质心。成本函数对聚类的扭曲进行求和。每个聚类的扭曲等于其质心与其组成实例之间的平方距离之和。紧凑的聚类扭曲较小,而包含分散实例的聚类扭曲较大。通过一个迭代过程来学习最小化成本函数的参数,过程包括将观察值分配到聚类中,然后移动聚类。首先,聚类的质心被初始化为随机位置。实际上,将质心的位置设置为随机选择的观察值的位置通常能得到最佳结果。在每次迭代中,K-Means 将观察值分配到它们最近的聚类中,然后将质心移动到它们分配的观察值的均值位置。我们通过手动操作一个例子来演示,使用如下表中的训练数据:

实例 X0 X1
1 7 5
2 5 7
3 7 7
4 3 3
5 4 6
6 1 4
7 0 0
8 2 2
9 8 7
10 6 8
11 5 5
12 3 7

有两个解释变量,每个实例有两个特征。实例的散点图如下所示:

使用 K-Means 算法的聚类

假设 K-Means 将第一个聚类的重心初始化为第五个实例,将第二个聚类的重心初始化为第十一个实例。对于每个实例,我们将计算其到两个重心的距离,并将其分配给距离最近的聚类。初始分配情况显示在下表的Cluster列中:

实例 X0 X1 C1 距离 C2 距离 上一个聚类 新聚类 是否改变?
1 7 5 3.16228 2 C2
2 5 7 1.41421 2 C1
3 7 7 3.16228 2.82843 C2
4 3 3 3.16228 2.82843 C2
5 4 6 0 1.41421 C1
6 1 4 3.60555 4.12311 C1
7 0 0 7.21110 7.07107 C2
8 2 2 4.47214 4.24264 C2
9 8 7 4.12311 3.60555 C2
10 6 8 2.82843 3.16228 C1
11 5 5 1.41421 0 C2
12 3 7 1.41421 2.82843 C1
C1 重心 4 6
C2 重心 5 5

绘制的重心和初始聚类分配如下图所示。分配给第一个聚类的实例用X标记,分配给第二个聚类的实例用点标记。重心的标记比实例的标记大。

使用 K-Means 算法的聚类

现在我们将两个重心移动到其组成实例的均值位置,重新计算训练实例到重心的距离,并将实例重新分配到距离最近的重心:

实例 X0 X1 C1 距离 C2 距离 上一个聚类 新聚类 是否改变?
1 7 5 3.492850 2.575394 C2 C2
2 5 7 1.341641 2.889107 C1 C1
3 7 7 3.255764 3.749830 C2 C1
4 3 3 3.492850 1.943067 C2 C2
5 4 6 0.447214 1.943067 C1 C1
6 1 4 3.687818 3.574285 C1 C2
7 0 0 7.443118 6.169378 C2 C2
8 2 2 4.753946 3.347250 C2 C2
9 8 7 4.242641 4.463000 C2 C1
10 6 8 2.720294 4.113194 C1 C1
11 5 5 1.843909 0.958315 C2 C2
12 3 7 1 3.260775 C1 C1
C1 重心 3.8 6.4
C2 重心 4.571429 4.142857

新的聚类在下图中进行了绘制。请注意,质心在分散,并且几个实例已改变了其分配:

K-Means 算法聚类

现在,我们将再次将质心移至其构成实例的位置的均值,并重新将实例分配给最近的质心。质心继续分散,如下图所示:

K-Means 算法聚类

在下一次迭代中,实例的质心分配不会发生变化;K-Means 将继续迭代,直到满足某个停止准则。通常,这个准则是后续迭代中成本函数值之间的差异阈值,或质心位置变化的阈值。如果这些停止准则足够小,K-Means 会收敛到一个最优解。这个最优解不一定是全局最优解。

局部最优解

回想一下,K-Means 最初将聚类的质心位置设置为随机选定的观察值位置。有时,随机初始化不太幸运,质心被设置到导致 K-Means 收敛到局部最优解的位置。例如,假设 K-Means 随机初始化了两个聚类质心,位置如下:

局部最优解

K-Means 最终会收敛到一个局部最优解,如下图所示。这些聚类可能有信息价值,但更可能的是,顶部和底部的观察组会形成更具信息性的聚类。为了避免局部最优解,K-Means 通常会重复执行数十次甚至数百次。在每次迭代中,它会随机初始化到不同的起始聚类位置。选择使成本函数最小化的初始化方案。

局部最优解

肘部法则

如果问题的背景没有指定肘部法则,则可以使用一种叫做肘部法则的技术来估计最佳聚类数。肘部法则通过不同的肘部法则值绘制由成本函数产生的值。当肘部法则增加时,平均失真度会减小;每个聚类的构成实例会更少,且实例会更接近各自的质心。然而,随着肘部法则的增加,平均失真度的改善会逐渐减少。在失真度改善下降最明显的肘部法则值处,称为“肘部”。我们可以使用肘部法则来选择数据集的聚类数。下图的散点图可视化了一个有两个明显聚类的数据集:

肘部法则

我们将使用以下代码计算并绘制每个肘部法则值从 1 到 10 的聚类平均失真:

>>> import numpy as np 
>>> from sklearn.cluster import KMeans 
>>> from scipy.spatial.distance import cdist 
>>> import matplotlib.pyplot as plt 

>>> cluster1 = np.random.uniform(0.5, 1.5, (2, 10))
>>> cluster2 = np.random.uniform(3.5, 4.5, (2, 10))
>>> X = np.hstack((cluster1, cluster2)).T
>>> X = np.vstack((x, y)).T 

>>> K = range(1, 10) 
>>> meandistortions = [] 
>>> for k in K: 
>>>     kmeans = KMeans(n_clusters=k) 
>>>     kmeans.fit(X) 
>>>     meandistortions.append(sum(np.min(cdist(X, kmeans.cluster_centers_, 'euclidean'), axis=1)) / X.shape[0]) 

>>> plt.plot(K, meandistortions, 'bx-') 
>>> plt.xlabel('k') 
>>> plt.ylabel('Average distortion') 
>>> plt.title('Selecting k with the Elbow Method') 
>>> plt.show()

肘部法则

随着我们将肘部法则1增加到2,平均失真迅速改善。对于大于 2 的肘部法则值,改进非常小。现在,我们将肘部法则应用于具有三个聚类的以下数据集:

肘部法则

以下图形展示了该数据集的肘部图。由此可以看出,当添加第四个聚类时,平均失真的改善速度急剧下降,也就是说,肘部法则确认对于该数据集,肘部法则应设置为三。

肘部法则

评估聚类

我们将机器学习定义为设计和研究从经验中学习以提高任务执行性能的系统,任务的性能通过给定的度量进行衡量。K-Means 是一种无监督学习算法;没有标签或基准真值来与聚类进行比较。然而,我们仍然可以使用内在度量来评估算法的性能。我们已经讨论了测量聚类失真度。在本节中,我们将讨论另一种聚类性能度量,称为轮廓系数。轮廓系数是衡量聚类紧凑性和分离度的指标。随着聚类质量的提高,它会增加;对于远离彼此的紧凑聚类,轮廓系数较大;对于大的重叠聚类,轮廓系数较小。轮廓系数是针对每个实例计算的;对于一组实例,它是个体样本分数的平均值。实例的轮廓系数通过以下公式计算:

评估聚类

a是聚类中实例之间的平均距离。b是实例与下一个最接近的聚类中实例之间的平均距离。以下示例运行四次 K-Means 算法,分别创建两个、三个、四个和八个聚类,并计算每次运行的轮廓系数:

>>> import numpy as np
>>> from sklearn.cluster import KMeans
>>> from sklearn import metrics
>>> import matplotlib.pyplot as plt
>>> plt.subplot(3, 2, 1)
>>> x1 = np.array([1, 2, 3, 1, 5, 6, 5, 5, 6, 7, 8, 9, 7, 9])
>>> x2 = np.array([1, 3, 2, 2, 8, 6, 7, 6, 7, 1, 2, 1, 1, 3])
>>> X = np.array(zip(x1, x2)).reshape(len(x1), 2)
>>> plt.xlim([0, 10])
>>> plt.ylim([0, 10])
>>> plt.title('Instances')
>>> plt.scatter(x1, x2)
>>> colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'b']
>>> markers = ['o', 's', 'D', 'v', '^', 'p', '*', '+']
>>> tests = [2, 3, 4, 5, 8]
>>> subplot_counter = 1
>>> for t in tests:
>>>     subplot_counter += 1
>>>     plt.subplot(3, 2, subplot_counter)
>>>     kmeans_model = KMeans(n_clusters=t).fit(X)
>>>     for i, l in enumerate(kmeans_model.labels_):
>>>         plt.plot(x1[i], x2[i], color=colors[l], marker=markers[l], ls='None')
>>>     plt.xlim([0, 10])
>>>     plt.ylim([0, 10])
>>>     plt.title('K = %s, silhouette coefficient = %.03f' % (
>>>         t, metrics.silhouette_score(X, kmeans_model.labels_, metric='euclidean')))
>>> plt.show()

该脚本生成了以下图形:

评估聚类

数据集包含三个明显的聚类。因此,当评估聚类等于三时,轮廓系数最大。将评估聚类设置为八时,产生的实例聚类相互之间的距离与它们与其他聚类的实例之间的距离相当,且这些聚类的轮廓系数最小。

图像量化

在前面的章节中,我们使用聚类来探索数据集的结构。现在让我们将其应用于一个不同的问题。图像量化是一种有损压缩方法,它用单一颜色替代图像中一系列相似的颜色。量化减少了图像文件的大小,因为表示颜色所需的位数较少。在以下示例中,我们将使用聚类来发现图像的压缩调色板,其中包含其最重要的颜色。然后,我们将使用压缩调色板重建图像。此示例需要mahotas图像处理库,可以通过pip install mahotas安装:

>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from sklearn.cluster import KMeans
>>> from sklearn.utils import shuffle
>>> import mahotas as mh

首先我们读取并展平图像:

>>> original_img = np.array(mh.imread('img/tree.jpg'), dtype=np.float64) / 255
>>> original_dimensions = tuple(original_img.shape)
>>> width, height, depth = tuple(original_img.shape)
>>> image_flattened = np.reshape(original_img, (width * height, depth))

然后我们使用 K-Means 从 1,000 个随机选择的颜色样本中创建 64 个聚类。每个聚类将是压缩调色板中的一种颜色。代码如下:

>>> image_array_sample = shuffle(image_flattened, random_state=0)[:1000]
>>> estimator = KMeans(n_clusters=64, random_state=0)
>>> estimator.fit(image_array_sample)

接下来,我们预测原始图像中每个像素的聚类分配:

>>> cluster_assignments = estimator.predict(image_flattened)

最后,我们从压缩调色板和聚类分配中创建压缩图像:

>>> compressed_palette = estimator.cluster_centers_
>>> compressed_img = np.zeros((width, height, compressed_palette.shape[1]))
>>> label_idx = 0
>>> for i in range(width):
>>>     for j in range(height):
>>>         compressed_img[i][j] = compressed_palette[cluster_assignments[label_idx]]
>>>         label_idx += 1
>>> plt.subplot(122)
>>> plt.title('Original Image')
>>> plt.imshow(original_img)
>>> plt.axis('off')
>>> plt.subplot(121)
>>> plt.title('Compressed Image')
>>> plt.imshow(compressed_img)
>>> plt.axis('off')
>>> plt.show()

图像的原始版本和压缩版本如下图所示:

图像量化

聚类学习特征

在这个例子中,我们将在半监督学习问题中将聚类与分类结合起来。你将通过对无标签数据进行聚类来学习特征,并使用学习到的特征构建监督分类器。

假设你拥有一只猫和一只狗。假设你购买了一部智能手机,表面上是为了与人类交流,但实际上只是为了拍摄你的猫和狗。你的照片非常棒,你确信你的朋友和同事们会喜欢详细查看这些照片。你希望能够体贴一些,尊重有些人只想看到猫的照片,而有些人只想看到狗的照片,但将这些照片分开是费力的。让我们构建一个半监督学习系统,可以分类猫和狗的图像。

回想一下从第三章,特征提取和预处理,我们可以看到,对图像进行分类的一个天真的方法是使用所有像素的强度或亮度作为解释变量。即使是小图像,这种方法也会产生高维特征向量。与我们用来表示文档的高维特征向量不同,这些向量并不是稀疏的。此外,显而易见的是,这种方法对图像的光照、尺度和方向非常敏感。在第三章中,特征提取和预处理,我们还讨论了 SIFT 和 SURF 描述符,它们以一种对尺度、旋转和光照不变的方式描述图像的有趣区域。在这个例子中,我们将聚类从所有图像中提取的描述符,以学习特征。然后,我们将用一个向量表示图像,向量的每个元素代表一个聚类。每个元素将编码从分配给聚类的图像中提取的描述符的数量。这种方法有时被称为特征包表示法,因为聚类的集合类似于词袋表示法的词汇表。我们将使用 Kaggle 的 Dogs vs. Cats 竞赛训练集中的 1,000 张猫和 1,000 张狗的图像。数据集可以从 www.kaggle.com/c/dogs-vs-cats/data 下载。我们将猫标记为正类,狗标记为负类。请注意,这些图像有不同的尺寸;由于我们的特征向量不表示像素,因此我们不需要调整图像的尺寸使其具有相同的尺寸。我们将使用图像的前 60% 进行训练,然后在剩余的 40% 上进行测试:

>>> import numpy as np
>>> import mahotas as mh
>>> from mahotas.features import surf
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.metrics import *
>>> from sklearn.cluster import MiniBatchKMeans
>>> import glob

首先,我们加载图像,将它们转换为灰度图像,并提取 SURF 描述符。与许多类似特征相比,SURF 描述符可以更快地提取,但从 2,000 张图像中提取描述符仍然是计算密集型的。与之前的例子不同,这个脚本在大多数计算机上执行需要几分钟时间。

>>> all_instance_filenames = []
>>> all_instance_targets = []
>>> for f in glob.glob('cats-and-dogs-img/*.jpg'):
>>>     target = 1 if 'cat' in f else 0
>>>     all_instance_filenames.append(f)
>>>     all_instance_targets.append(target)
>>> surf_features = []
>>> counter = 0
>>> for f in all_instance_filenames:
>>>     print 'Reading image:', f
>>>     image = mh.imread(f, as_grey=True)
>>>     surf_features.append(surf.surf(image)[:, 5:])

>>> train_len = int(len(all_instance_filenames) * .60)
>>> X_train_surf_features = np.concatenate(surf_features[:train_len])
>>> X_test_surf_feautres = np.concatenate(surf_features[train_len:])
>>> y_train = all_instance_targets[:train_len]
>>> y_test = all_instance_targets[train_len:]

然后,我们在下面的代码示例中将提取的描述符分组到 300 个聚类中。我们使用 MiniBatchKMeans,这是 K-Means 的一种变体,每次迭代使用实例的随机样本。由于它仅计算每次迭代中一些实例到质心的距离,MiniBatchKMeans 收敛更快,但其聚类的失真可能更大。实际上,结果是相似的,这种折衷是可以接受的。

>>> n_clusters = 300
>>> print 'Clustering', len(X_train_surf_features), 'features'
>>> estimator = MiniBatchKMeans(n_clusters=n_clusters)
>>> estimator.fit_transform(X_train_surf_features)

接下来,我们为训练和测试数据构建特征向量。我们找到与每个提取的 SURF 描述符相关联的聚类,并使用 NumPy 的 binCount() 函数对它们进行计数。以下代码为每个实例生成一个 300 维的特征向量:

>>> X_train = []
>>> for instance in surf_features[:train_len]:
>>>     clusters = estimator.predict(instance)
>>>     features = np.bincount(clusters)
>>>     if len(features) < n_clusters:
>>>         features = np.append(features, np.zeros((1, n_clusters-len(features))))
>>>     X_train.append(features)

>>> X_test = []
>>> for instance in surf_features[train_len:]:
>>>     clusters = estimator.predict(instance)
>>>     features = np.bincount(clusters)
>>>     if len(features) < n_clusters:
>>>         features = np.append(features, np.zeros((1, n_clusters-len(features))))
>>>     X_test.append(features)

最后,我们在特征向量和目标上训练了一个逻辑回归分类器,并评估了其精度、召回率和准确率:

>>> clf = LogisticRegression(C=0.001, penalty='l2')
>>> clf.fit_transform(X_train, y_train)
>>> predictions = clf.predict(X_test)
>>> print classification_report(y_test, predictions)
>>> print 'Precision: ', precision_score(y_test, predictions)
>>> print 'Recall: ', recall_score(y_test, predictions)
>>> print 'Accuracy: ', accuracy_score(y_test, predictions)

Reading image: dog.9344.jpg
...
Reading image: dog.8892.jpg
Clustering 756914 features
             precision    recall  f1-score   support

          0       0.71      0.76      0.73       392
          1       0.75      0.70      0.72       408

avg / total       0.73      0.73      0.73       800

Precision:  0.751322751323
Recall:  0.696078431373
Accuracy:  0.7275

这个半监督系统比仅使用像素强度作为特征的逻辑回归分类器具有更好的精度和召回率。此外,我们的特征表示仅有 300 维;即使是小的 100 x 100 像素图像也会有 10,000 维。

总结

本章我们讨论了第一个无监督学习任务:聚类。聚类用于发现无标签数据中的结构。你学习了 K 均值聚类算法,该算法通过迭代地将实例分配到聚类中,并细化聚类中心的位置。虽然 K 均值是通过经验进行学习的,而非监督学习,但其性能依然可衡量;你学会了使用失真度和轮廓系数来评估聚类。我们将 K 均值应用于两个不同的问题。首先,我们使用 K 均值进行图像量化,这是一种通过单一颜色表示一系列颜色的压缩技术。我们还将 K 均值用于半监督图像分类问题中的特征学习。

在下一章,我们将讨论另一种无监督学习任务——降维。就像我们为猫狗图像分类所创建的半监督特征表示一样,降维可以用来减少一组解释变量的维度,同时尽可能保留更多信息。

第七章:PCA 降维

本章我们将讨论一种叫做主成分分析PCA)的降维技术。降维的动机来源于几个问题。首先,它可以用来缓解由维度灾难引发的问题。其次,降维可以在最小化信息丢失的情况下压缩数据。第三,理解具有数百维度的数据结构是困难的;而仅有两三个维度的数据则容易进行可视化。我们将使用 PCA 在二维空间中可视化一个高维数据集,并构建一个人脸识别系统。

PCA 概述

回想一下第三章,特征提取与预处理,我们知道涉及高维数据的问题可能会受到维度灾难的影响。随着数据集维度的增加,估计器需要的样本数会指数增长。获取如此庞大的数据在某些应用中可能是不可行的,而且从大数据集中学习需要更多的内存和处理能力。此外,随着数据维度的增加,数据的稀疏性通常会增加。在高维空间中,检测相似实例变得更加困难,因为所有实例都呈现稀疏特征。

主成分分析,也叫做卡尔霍嫩-洛夫变换(Karhunen-Loeve Transform),是一种用于在高维数据中寻找模式的技术。PCA 常用于探索和可视化高维数据集。它还可以用来压缩数据,并在数据被另一个估计器使用之前对其进行处理。PCA 将一组可能相关的高维变量减少到一组低维的、线性不相关的合成变量,称为主成分。低维数据将尽可能保留原始数据的方差。

PCA 通过将数据投影到一个低维子空间来减少数据集的维度。例如,一个二维数据集可以通过将数据点投影到一条线上来降低维度;数据集中的每个实例将由一个单一的值表示,而不是一对值。一个三维数据集可以通过将变量投影到一个平面上来降低到二维。一般来说,一个n维的数据集可以通过将数据集投影到一个k维子空间来降低维度,其中k小于n。更正式地说,PCA 可以用来找到一组向量,这些向量构成一个子空间,最小化投影数据的平方误差和。这个投影将保留原始数据集方差的最大比例。

想象一下,你是一本园艺用品目录的摄影师,负责拍摄一个洒水壶。洒水壶是三维的,但照片是二维的;你必须创造一个二维的表示,尽可能多地描述洒水壶。以下是你可以使用的四张可能的照片:

PCA 概述

在第一张照片中,可以看到洒水壶的背面,但看不到前面。第二张照片是从洒水壶的喷口正下方拍摄的;这张照片提供了第一张照片中看不到的壶前面的信息,但现在手柄不可见。从第三张图片的鸟瞰视角,无法辨别洒水壶的高度。第四张照片是目录中最明显的选择;这张图片清楚地展示了洒水壶的高度、顶部、喷口和手柄。

PCA 的动机是类似的;它可以将高维空间中的数据投影到一个低维空间,并尽可能保留方差。PCA 旋转数据集,使其与主要成分对齐,以最大化前几个主成分中的方差。假设我们有如下图所示的数据集:

PCA 概述

这些实例大致形成了一个从原点到图表右上角的长而细的椭圆。为了减少这个数据集的维度,我们必须将这些点投影到一条线上。以下是两个可以进行投影的线条。沿哪条线,实例变化最多?

PCA 概述

这些实例沿着虚线比沿着点线变化得更多。实际上,虚线是第一个主成分。第二个主成分必须与第一个主成分正交;也就是说,第二个主成分必须是统计独立的,当绘制时,第二主成分将垂直于第一个主成分,如下图所示:

PCA 概述

每个后续的主成分保留剩余方差的最大部分;唯一的限制是,每个主成分必须与其他主成分正交。

现在假设数据集是三维的。这些点的散点图看起来像一个稍微绕着一个轴旋转的平面圆盘。

PCA 概述

这些点可以旋转和平移,使得倾斜的平面几乎完全位于二维空间中。现在这些点形成了一个椭圆;第三维几乎没有方差,可以丢弃。

PCA(主成分分析)在数据集的方差在各维度之间分布不均时最为有效。考虑一个三维数据集,其具有球形凸包。由于每个维度的方差相等,PCA 无法有效地应用于该数据集;任何维度都无法丢弃,否则会丢失大量信息。

通过可视化方式,很容易识别只有二维或三维的数据集的主成分。在下一节中,我们将讨论如何计算高维数据的主成分。

执行主成分分析

在讨论主成分分析如何工作之前,我们必须先定义几个术语。

方差、协方差和协方差矩阵

回想一下,方差是衡量一组值分布的程度。方差是值与均值的平方差的平均值,按以下公式计算:

方差、协方差和协方差矩阵

协方差是衡量两个变量一起变化的程度;它是衡量两个变量集之间相关性强度的度量。如果两个变量的协方差为零,则这两个变量不相关。请注意,不相关的变量不一定是独立的,因为相关性仅仅是线性依赖性的度量。两个变量的协方差使用以下公式计算:

方差、协方差和协方差矩阵

如果协方差非零,符号表示变量之间是正相关还是负相关。当两个变量正相关时,一个变量随着另一个变量的增加而增加。当变量负相关时,一个变量相对于其均值减少,而另一个变量相对于其均值增加。协方差 矩阵描述数据集中每对维度之间的协方差值。元素方差、协方差和协方差矩阵表示数据的方差、协方差和协方差矩阵方差、协方差和协方差矩阵维度的协方差。例如,一个三维数据的协方差矩阵如下所示:

方差、协方差和协方差矩阵

让我们计算以下数据集的协方差矩阵:

2 0 −1.4
2.2 0.2 −1.5
2.4 0.1 −1
1.9 0 −1.2

变量的均值为 2.125、0.075 和-1.275。然后,我们可以计算每一对变量的协方差,得到以下协方差矩阵:

方差、协方差和协方差矩阵

我们可以使用 NumPy 验证我们的计算:

>>> import numpy as np
>>> X = [[2, 0, -1.4],
>>>     [2.2, 0.2, -1.5],
>>>     [2.4, 0.1, -1],
>>>     [1.9, 0, -1.2]]
>>> print np.cov(np.array(X).T)
[[ 0.04916667  0.01416667  0.01916667]
 [ 0.01416667  0.00916667 -0.00583333]
 [ 0.01916667 -0.00583333  0.04916667]]

特征向量和特征值

一个向量由方向大小(或长度)来描述。矩阵的特征向量是一个非零向量,满足以下方程:

特征向量和特征值

在前面的方程中,特征向量和特征值 是一个特征向量,A 是一个方阵,特征向量和特征值 是一个称为特征值的标量。特征向量的方向在通过 A 变换后保持不变;只有其大小发生了变化,这一变化由特征值表示;即,用矩阵与其特征向量相乘相当于对特征向量进行缩放。前缀 eigen 是德语单词,意思是 属于特有的;矩阵的特征向量是 属于 数据结构并表征数据结构的向量。

特征向量和特征值只能从方阵中得出,并非所有方阵都有特征向量或特征值。如果一个矩阵有特征向量和特征值,那么每个维度都会有一对对应的特征向量和特征值。矩阵的主成分是其协方差矩阵的特征向量,按其对应的特征值排序。特征值最大的特征向量是第一个主成分;第二个主成分是特征值第二大的特征向量,依此类推。

让我们计算以下矩阵的特征向量和特征值:

特征向量和特征值

请记住,A 和任何 A 的特征向量相乘的结果必须等于特征向量与其特征值的乘积。我们将从找到特征值开始,特征值可以通过以下特征方程求得:

特征向量和特征值特征向量和特征值

特征方程指出,矩阵的行列式,也就是数据矩阵与单位矩阵与特征值的乘积之间的差为零:

特征向量和特征值

这个矩阵的两个特征值都是-1。我们现在可以使用特征值来求解特征向量:

特征向量和特征值

首先,我们将方程设置为零:

特征向量和特征值

A 的值代入后得到如下结果:

特征向量和特征值

我们可以将第一个特征值代入我们的第一个特征值中来求解特征向量。

特征向量和特征值

前面的方程可以改写为一组方程:

特征向量和特征值

任何满足前面方程的非零向量,如下所示,都可以作为特征向量:

特征向量和特征值

PCA 需要单位特征向量,或者说长度为1的特征向量。我们可以通过将特征向量除以其范数来归一化它,范数由以下公式给出:

特征向量和特征值

我们向量的范数等于以下值:

特征向量和特征值

这将产生以下单位特征向量:

特征向量和特征值

我们可以使用 NumPy 验证我们对特征向量的解是否正确。eig函数返回一个包含特征值和特征向量的元组:

>>> import numpy as np
>>> w, v = np.linalg.eig(np.array([[1, -2], [2, -3]]))
>>> w; v
array([-0.99999998, -1.00000002])
array([[ 0.70710678,  0.70710678],

使用主成分分析进行降维

让我们使用主成分分析(PCA)将以下二维数据集降维为一维:

x1 x2
0.9 1
2.4 2.6
1.2 1.7
0.5 0.7
0.3 0.7
1.8 1.4
0.5 0.6
0.3 0.6
2.5 2.6
1.3 1.1

PCA 的第一步是从每个观测值中减去每个解释变量的均值:

x1 x2
0.9 - 1.17 = -0.27 1 - 1.3 = -0.3
2.4 - 1.17 = 1.23 2.6 - 1.3 = 1.3
1.2 - 1.17 = 0.03 1.7 - 1.3 = 0.4
0.5 - 1.17 = -0.67 -0.7 - 1.3 = 0.6
0.3 - 1.17 = -0.87 -0.7 - 1.3 = 0.6
1.8 - 1.17 = 0.63 1.4 - 1.3 = 0.1
0.5 - 1.17 = -0.67 0.6 - 1.3 = -0.7
0.3 - 1.17 = -0.87 0.6 - 1.3 = -0.7
2.5 - 1.17 = 1.33 2.6 - 1.3 = 1.3
1.3 - 1.17 = 0.13 1.1 - 1.3 = -0.2

接下来,我们必须计算数据的主成分。回顾一下,主成分是按其特征值排序的数据协方差矩阵的特征向量。主成分可以通过两种不同的技术来找到。第一种技术需要计算数据的协方差矩阵。由于协方差矩阵是方阵,我们可以使用上一节中描述的方法计算特征向量和特征值。第二种技术则利用数据矩阵的奇异值分解(SVD)来找到协方差矩阵的特征向量和特征值的平方根。我们将首先通过第一种方法完成一个示例,然后描述 scikit-learn 中 PCA 实现所使用的第二种方法。

以下矩阵是数据的协方差矩阵:

使用主成分分析进行降维

使用上一节中描述的技术,特征值为 1.250 和 0.034。以下是单位特征向量:

使用主成分分析进行降维

接下来,我们将把数据投影到主成分上。第一个特征向量具有最大的特征值,是第一个主成分。我们将构建一个变换矩阵,其中矩阵的每一列都是一个主成分的特征向量。如果我们将一个五维数据集降维到三维,我们将构建一个包含三列的矩阵。在这个例子中,我们将把二维数据集投影到一维,因此我们只会使用第一个主成分的特征向量。最后,我们将计算数据矩阵和变换矩阵的点积。以下是将数据投影到第一个主成分后的结果:

主成分分析降维

许多 PCA 的实现,包括 scikit-learn 中的实现,使用奇异值分解(SVD)来计算特征向量和特征值。SVD 由以下方程给出:

主成分分析降维

主成分分析降维 的列称为数据矩阵的左奇异向量,主成分分析降维 的列是其右奇异向量,主成分分析降维 的对角元素是其奇异值。虽然矩阵的奇异向量和奇异值在信号处理和统计学的某些应用中很有用,但我们关注它们仅仅是因为它们与数据矩阵的特征向量和特征值有关。具体来说,左奇异向量是协方差矩阵的特征向量,主成分分析降维 的对角元素是协方差矩阵特征值的平方根。计算 SVD 超出了本章的范围;然而,使用 SVD 得到的特征向量应该与从协方差矩阵推导出的特征向量相似。

使用主成分分析(PCA)可视化高维数据

通过将数据可视化为二维或三维图形,可以轻松发现数据中的模式。高维数据集无法直接图形化表示,但我们仍然可以通过将数据降维至两到三个主成分来获得其结构的一些洞察。

费舍尔的鸢尾花数据集于 1936 年收集,是来自三种鸢尾花物种的每种各 50 个样本的集合:鸢尾花 Setosa、鸢尾花 Virginica 和鸢尾花 Versicolor。解释变量是花瓣和萼片的长度和宽度的测量值。鸢尾花数据集通常用于测试分类模型,并且包含在 scikit-learn 中。让我们将 iris 数据集的四个维度降到二维,以便可视化:

>>> import matplotlib.pyplot as plt
>>> from sklearn.decomposition import PCA
>>> from sklearn.datasets import load_iris

首先,我们加载内置的鸢尾花数据集,并实例化一个PCA估计器。PCA类接受一个要保留的主成分数量作为超参数。像其他估计器一样,PCA暴露了一个fit_transform()方法,返回降维后的数据矩阵:

>>> data = load_iris()
>>> y = data.target
>>> X = data.data
>>> pca = PCA(n_components=2)
>>> reduced_X = pca.fit_transform(X)

最后,我们组装并绘制了降维后的数据:

>>> red_x, red_y = [], []
>>> blue_x, blue_y = [], []
>>> green_x, green_y = [], []
>>> for i in range(len(reduced_X)):
>>>     if y[i] == 0:
>>>         red_x.append(reduced_X[i][0])
>>>         red_y.append(reduced_X[i][1])
>>>     elif y[i] == 1:
>>>         blue_x.append(reduced_X[i][0])
>>>         blue_y.append(reduced_X[i][1])
>>>     else:
>>>         green_x.append(reduced_X[i][0])
>>>         green_y.append(reduced_X[i][1])
>>> plt.scatter(red_x, red_y, c='r', marker='x')
>>> plt.scatter(blue_x, blue_y, c='b', marker='D')
>>> plt.scatter(green_x, green_y, c='g', marker='.')
>>> plt.show()

缩减后的实例在下图中绘制。数据集的三个类别分别用其自己的标记样式表示。从这个数据的二维视图中,可以清楚地看出一个类别可以很容易地与其他两个重叠的类别分开。没有图形表示,很难注意到这种结构。这一洞察可以影响我们选择分类模型。

使用 PCA 可视化高维数据

使用 PCA 进行面部识别

现在让我们将 PCA 应用于面部识别问题。面部识别是一种监督分类任务,其目标是从面部图像中识别出一个人。在本例中,我们将使用来自 AT&T 实验室剑桥分部的我们的面部数据库数据集。该数据集包含四十个人的每个人的十张图像。这些图像在不同的光照条件下创建,并且主体改变了他们的面部表情。图像是灰度的,尺寸为 92 x 112 像素。以下是一个示例图像:

使用 PCA 进行面部识别

尽管这些图像很小,但编码每个像素强度的特征向量将具有 10,304 个维度。从这样高维数据的训练可能需要许多样本以避免过拟合。因此,我们将使用 PCA 来以少数主成分紧凑地表示图像。

我们可以将图像的像素强度矩阵重塑为一个向量,并为所有训练图像创建这些向量的矩阵。每个图像都是这个数据集主成分的线性组合。在面部识别的上下文中,这些主成分被称为特征脸。特征脸可以被看作是标准化的面部组件。数据集中的每张脸都可以表示为一些特征脸的组合,并且可以近似为最重要的特征脸的组合:

>>> from os import walk, path
>>> import numpy as np
>>> import mahotas as mh
>>> from sklearn.cross_validation import train_test_split
>>> from sklearn.cross_validation import cross_val_score
>>> from sklearn.preprocessing import scale
>>> from sklearn.decomposition import PCA
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.metrics import classification_report
>>> X = []
>>> y = []

首先,我们将图像加载到NumPy数组中,并将它们的矩阵重塑为向量:

>>> for dir_path, dir_names, file_names in walk('data/att-faces/orl_faces'):
>>>     for fn in file_names:
>>>         if fn[-3:] == 'pgm':
>>>             image_filename = path.join(dir_path, fn)
>>>             X.append(scale(mh.imread(image_filename, as_grey=True).reshape(10304).astype('float32')))
>>>             y.append(dir_path)
>>> X = np.array(X)

然后,我们将图像随机分割为训练集和测试集,并在训练集上拟合PCA对象:

>>> X_train, X_test, y_train, y_test = train_test_split(X, y)
>>> pca = PCA(n_components=150)

我们将所有实例缩减为 150 个维度并训练一个逻辑回归分类器。数据集包含四十个类别;scikit-learn 在幕后自动使用一对所有策略创建二进制分类器:

>>> X_train_reduced = pca.fit_transform(X_train)
>>> X_test_reduced = pca.transform(X_test)
>>> print 'The original dimensions of the training data were', X_train.shape
>>> print 'The reduced dimensions of the training data are', X_train_reduced.shape
>>> classifier = LogisticRegression()
>>> accuracies = cross_val_score(classifier, X_train_reduced, y_train)

最后,我们使用交叉验证和测试集评估分类器的性能。在完整数据集上训练的分类器的每类平均 F1 分数为 0.94,但训练所需的时间明显更长,在具有更多训练实例的应用中可能会变得非常缓慢:

>>> print 'Cross validation accuracy:', np.mean(accuracies), accuracies
>>> classifier.fit(X_train_reduced, y_train)
>>> predictions = classifier.predict(X_test_reduced)
>>> print classification_report(y_test, predictions)

以下是脚本的输出:

The original dimensions of the training data were (300, 10304)
The reduced dimensions of the training data are (300, 150)
Cross validation accuracy: 0.833841819347 [ 0.82882883  0.83        0.84269663]
             precision    recall  f1-score   support

data/att-faces/orl_faces/s1       1.00      1.00      1.00         2
data/att-faces/orl_faces/s10       1.00      1.00      1.00         2
data/att-faces/orl_faces/s11       1.00      0.60      0.75         5
...
data/att-faces/orl_faces/s9       1.00      1.00      1.00         2

avg / total       0.92      0.89      0.89       100

摘要

在本章中,我们研究了降维问题。高维数据难以进行可视化。高维数据集还可能遭遇维度灾难;估计器需要大量样本才能从高维数据中学习并进行泛化。我们通过使用一种叫做主成分分析的技术来缓解这些问题,该技术通过将数据投影到低维子空间,将一个高维、可能相关的数据集降维为一组不相关的主成分。我们使用主成分分析将四维 Iris 数据集可视化为二维,并构建了一个人脸识别系统。在下一章中,我们将回到监督学习。我们将讨论一种早期的分类算法——感知机,这将为我们在最后几章中讨论更高级的模型做准备。

第八章:感知器

在前面的章节中,我们讨论了广义线性模型,该模型通过连接函数将解释变量和模型参数的线性组合与响应变量关联。在本章中,我们将讨论另一种线性模型,称为感知器。感知器是一种二分类器,可以从单个训练实例中学习,这对于从大数据集中进行训练非常有用。更重要的是,感知器及其局限性激发了我们将在最后几章讨论的模型。

感知器由弗兰克·罗森布拉特(Frank Rosenblatt)于 1950 年代末在康奈尔航空实验室发明,感知器的开发最初是受模拟人脑的努力驱动的。大脑由称为神经元的细胞组成,这些神经元处理信息,并通过称为突触的神经元之间的连接传递信息。据估计,人脑由多达 1000 亿个神经元和 100 万亿个突触组成。如下面的图所示,神经元的主要组成部分包括树突、细胞体和轴突。树突从其他神经元接收电信号。信号在神经元的细胞体中被处理,然后通过轴突传递到另一个神经元。

感知器

单个神经元可以被看作是一个计算单元,处理一个或多个输入并生成输出。感知器的功能与神经元类似;它接受一个或多个输入,处理它们并返回一个输出。看起来仅仅模拟人脑中数百亿个神经元中的一个,似乎用途有限。一定程度上这是正确的;感知器无法逼近一些基本的函数。然而,我们仍然会讨论感知器,原因有二。首先,感知器能够进行在线的错误驱动学习;学习算法可以通过单个训练实例而不是整个训练实例批次来更新模型的参数。在线学习对于从太大以至于无法全部存储在内存中的训练集进行学习非常有用。第二,理解感知器的工作原理对于理解我们将在后续章节中讨论的一些更强大的模型是必要的,包括支持向量机和人工神经网络。感知器通常使用如下图所示的图表来表示:

感知器

标记为 感知机感知机感知机 的圆圈是输入单元。每个输入单元代表一个特征。感知机通常会使用一个额外的输入单元来表示常数偏置项,但该输入单元通常在图示中省略。中间的圆圈是计算单元或神经元的主体。连接输入单元和计算单元的边类似于树突。每条边是加权的,或与一个参数相关联。参数可以容易地解释;与正类相关的解释变量将具有正权重,而与负类相关的解释变量将具有负权重。指向计算单元外部的边返回输出,可以将其视为轴突。

激活函数

感知机通过使用激活函数处理解释变量和模型参数的线性组合来对实例进行分类,如下方公式所示。参数和输入的线性组合有时被称为感知机的预激活

激活函数

这里,激活函数 是模型的参数,激活函数 是常数偏置项,激活函数 是激活函数。常用几种不同的激活函数。Rosenblatt 原始的感知机使用了Heaviside 阶跃函数。Heaviside 阶跃函数也叫做单位阶跃函数,表示如下公式,其中 激活函数 是特征的加权组合:

激活函数

如果解释变量和偏置项的加权和大于零,激活函数返回一,感知机预测该实例属于正类。否则,函数返回零,感知机预测该实例属于负类。Heaviside 阶跃激活函数在下图中绘制:

激活函数

另一个常见的激活函数是逻辑 sigmoid 激活函数。这个激活函数的梯度可以高效地计算,这在后续章节构建人工神经网络时非常重要。逻辑 sigmoid 激活函数由以下公式给出,其中 激活函数 是加权输入的总和:

激活函数

这个模型应该很熟悉;它是解释变量值和模型参数的线性组合,通过逻辑函数进行处理。也就是说,它与逻辑回归的模型相同。虽然使用逻辑 sigmoid 激活函数的感知机与逻辑回归有相同的模型,但它学习参数的方式不同。

感知机学习算法

感知机学习算法首先将权重设置为零或小的随机值。然后它预测一个训练实例的类别。感知机是一种基于错误的学习算法;如果预测正确,算法继续处理下一个实例。如果预测错误,算法会更新权重。更正式地,更新规则如下所示:

感知机学习算法

对于每个训练实例,每个解释变量的参数值增加!感知机学习算法,其中!感知机学习算法是实例!感知机学习算法的真实类别,!感知机学习算法是实例!感知机学习算法的预测类别,!感知机学习算法是实例!感知机学习算法的解释变量!感知机学习算法的值,而!感知机学习算法是控制学习率的超参数。如果预测正确,!感知机学习算法等于零,且!感知机学习算法项等于零。因此,如果预测正确,权重不会更新。如果预测错误,权重将增加学习率、!感知机学习算法和特征值的乘积。

这个更新规则类似于梯度下降的更新规则,权重的调整是为了正确分类实例,并且更新的大小由学习率控制。每次遍历训练实例称为一个周期(epoch)。当学习算法在完成一个周期时没有错误分类任何实例,就认为学习算法已收敛。学习算法并不保证一定会收敛;在本章后面,我们将讨论无法线性分离的数据集,对于这些数据集,收敛是无法实现的。因此,学习算法还需要一个超参数,指定在算法终止之前可以完成的最大周期数。

感知机的二分类

让我们解决一个玩具分类问题。假设您希望将成年猫与小猫分开。您的数据集中只有两个解释变量:动物白天睡觉的时间比例和动物白天易怒的时间比例。我们的训练数据包括以下四个实例:

实例 白天睡觉的时间比例 易怒的时间比例 小猫还是成年猫?
1 0.2 0.1 小猫
2 0.4 0.6 小猫
3 0.5 0.2 小猫
4 0.7 0.9 成年猫

下面的散点图显示这些实例是线性可分的:

感知器进行二元分类

我们的目标是训练一个能够使用两个实数解释变量分类动物的感知器。我们将小猫表示为正类,成年猫表示为负类。前面的网络图描述了我们将要训练的感知器。

我们的感知器有三个输入单元。感知器进行二元分类 是偏置项的输入单元。感知器进行二元分类感知器进行二元分类 是两个特征的输入单元。我们感知器的计算单元使用了海维赛德激活函数。在这个例子中,我们将最大训练轮数设为十;如果算法在 10 轮内没有收敛,它将停止并返回当前权重值。为了简单起见,我们将学习率设为一。最初,我们将所有权重设置为零。让我们看一下第一个训练轮次,如下表所示:

Epoch 1
实例 初始权重x激活 预测,目标 正确 更新后的权重
0 0, 0, 0;1.0, 0.2, 0.1;1.00 + 0.20 + 0.1*0 = 0.0; 0, 1 False 1.0, 0.2, 0.1
1 1.0, 0.2, 0.1;1.0, 0.4, 0.6;1.01.0 + 0.40.2 + 0.6*0.1 = 1.14; 1, 1 True 1.0, 0.2, 0.1
2 1.0, 0.2, 0.1;1.0, 0.5, 0.2;1.01.0 + 0.50.2 + 0.2*0.1 = 1.12; 1, 1 True 1.0, 0.2, 0.1
3 1.0, 0.2, 0.1;1.0, 0.7, 0.9;1.01.0 + 0.70.2 + 0.9*0.1 = 1.23; 1, 0 False 0, -0.5, -0.8

最初,所有权重都等于零。第一个实例的解释变量的加权和为零,激活函数输出为零,感知器错误地预测小猫为成年猫。由于预测错误,我们根据更新规则更新权重。我们将每个权重增加学习率、真实标签与预测标签之间的差异以及相应特征的值的乘积。

接下来,我们继续进行第二次训练实例,并使用更新后的权重计算特征的加权和。这个加权和等于 1.14,因此激活函数输出 1。这个预测是正确的,所以我们继续进行第三个训练实例,并且不更新权重。第三个实例的预测也是正确的,因此我们继续进行第四个训练实例。第四个实例的特征加权和为 1.23。激活函数输出 1,错误地预测这个成年猫是小猫。由于这个预测是错误的,我们将每个权重增加学习率、真实标签与预测标签之间的差异以及相应特征的乘积。我们通过对训练集中的所有实例进行分类完成了第一次训练周期。感知机并没有收敛;它错误地分类了训练集中的一半实例。下图展示了第一次训练周期后的决策边界:

使用感知机的二元分类

请注意,决策边界在整个周期中都有移动;周期结束时由权重形成的决策边界可能并不会产生周期初期看到的相同预测。由于我们没有超过最大训练周期数,我们将再次遍历这些实例。第二次训练周期如下表所示:

第 2 周期
实例 初始权重 x 激活 预测, 目标 正确 更新后的权重
0 0, -0.5, -0.81.0, 0.2, 0.11.00 + 0.2-0.5 + 0.1*-0.8 = -0.18 0, 1 错误 1, -0.3, -0.7
1 1, -0.3, -0.71.0, 0.4, 0.61.01.0 + 0.4-0.3 + 0.6*-0.7 = 0.46 1, 1 正确 1, -0.3, -0.7
2 1, -0.3, -0.71.0, 0.5, 0.21.01.0 + 0.5-0.3 + 0.2*-0.7 = 0.71 1, 1 正确 1, -0.3, -0.7
3 1, -0.3, -0.71.0, 0.7, 0.91.01.0 + 0.7-0.3 + 0.9*-0.7 = 0.16 1, 0 错误 0, -1, -1.6

第二个训练周期开始时,使用了第一个训练周期的权重值。在这个周期中,有两个训练实例被错误分类。权重更新了两次,但第二个周期结束时的决策边界与第一个周期结束时的决策边界相似。

使用感知机的二元分类

算法在这个周期未能收敛,因此我们将继续训练。下表描述了第三个训练周期:

第 3 周期
实例 初始权重 x 激活 预测, 目标 正确 更新后的权重
0 0, -1, -1.61.0, 0.2, 0.11.00 + 0.2-1.0 + 0.1*-1.6 = -0.36 0, 1 错误 1,-0.8, -1.5
1 1,-0.8, -1.51.0, 0.4, 0.61.01.0 + 0.4-0.8 + 0.6*-1.5 = -0.22 0, 1 错误 2, -0.4, -0.9
2 2, -0.4, -0.91.0, 0.5, 0.21.02.0 + 0.5-0.4 + 0.2*-0.9 = 1.62 1, 1 正确 2, -0.4, -0.9
3 2, -0.4, -0.91.0, 0.7, 0.91.02.0 + 0.7-0.4 + 0.9*-0.9 = 0.91 1, 0 错误 1, -1.1, -1.8

感知机在这一周期中比之前的周期分类更多的实例错误。下图描绘了第三个周期结束时的决策边界:

感知机的二分类

感知机在第四和第五训练周期中继续更新其权重,并且继续对训练实例进行错误分类。在第六个周期中,感知机正确分类了所有实例;它收敛到了一个权重集,可以将两个类别分开。下表描述了第六个训练周期:

第 6 周期
实例 初始权重x激活值 预测值,目标值 正确 更新后的权重
0 2, -1, -1.51.0, 0.2, 0.11.02 + 0.2-1 + 0.1*-1.5 = 1.65 1, 1 正确 2, -1, -1.5
1 2, -1, -1.51.0, 0.4, 0.61.02 + 0.4-1 + 0.6*-1.5 = 0.70 1, 1 正确 2, -1, -1.5
2 2, -1, -1.51.0, 0.5, 0.21.02 + 0.5-1 + 0.2*-1.5 = 1.2 1, 1 正确 2, -1, -1.5
3 2, -1, -1.51.0, 0.7, 0.91.02 + 0.7-1 + 0.9*-1.5 = -0.05 0, 0 正确 2, -1, -1.5

第六次训练周期结束时的决策边界如下图所示:

感知机的二分类

下图显示了所有训练周期中的决策边界。

感知机的二分类

感知机的文档分类

scikit-learn 提供了感知机的实现。与我们使用的其他实现一样,Perceptron 类的构造函数接受设置算法超参数的关键字参数。Perceptron 同样暴露了 fit_transform()predict() 方法。Perceptron 还提供了 partial_fit() 方法,允许分类器训练并对流数据进行预测。

在这个例子中,我们训练一个感知器来分类 20 个新闻组数据集中的文档。该数据集由约 20,000 个文档组成,采样自 20 个 Usenet 新闻组。该数据集通常用于文档分类和聚类实验;scikit-learn 提供了一个便利的函数来下载和读取数据集。我们将训练一个感知器来分类来自三个新闻组的文档:rec.sports.hockeyrec.sports.baseballrec.auto。scikit-learn 的Perceptron原生支持多类分类;它将使用“一对多”策略为训练数据中的每个类别训练一个分类器。我们将文档表示为 TF-IDF 加权的词袋。partial_fit()方法可以与HashingVectorizer结合使用,在内存受限的环境中对大量或流数据进行训练:

>>> from sklearn.datasets import fetch_20newsgroups
>>> from sklearn.metrics.metrics import f1_score, classification_report
>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> from sklearn.linear_model import Perceptron

>>> categories = ['rec.sport.hockey', 'rec.sport.baseball', 'rec.autos']
>>> newsgroups_train = fetch_20newsgroups(subset='train', categories=categories, remove=('headers', 'footers', 'quotes'))
>>> newsgroups_test = fetch_20newsgroups(subset='test', categories=categories, remove=('headers', 'footers', 'quotes'))

>>> vectorizer = TfidfVectorizer()
>>> X_train = vectorizer.fit_transform(newsgroups_train.data)
>>> X_test = vectorizer.transform(newsgroups_test.data)

>>> classifier = Perceptron(n_iter=100, eta0=0.1)
>>> classifier.fit_transform(X_train, newsgroups_train.target )
>>> predictions = classifier.predict(X_test)
>>> print classification_report(newsgroups_test.target, predictions)

以下是脚本的输出:

             precision    recall  f1-score   support

          0       0.89      0.87      0.88       396
          1       0.87      0.78      0.82       397
          2       0.79      0.88      0.83       399

avg / total       0.85      0.85      0.85      1192

首先,我们使用fetch_20newsgroups()函数下载并读取数据集。与其他内置数据集一致,函数返回一个包含datatargettarget_names字段的对象。我们还指定删除文档的标题、页脚和引用。每个新闻组在标题和页脚中使用不同的约定;保留这些解释性变量会使得文档分类变得过于简单。我们使用TfifdVectorizer生成 TF-IDF 向量,训练感知器,并在测试集上进行评估。未经超参数优化,感知器的平均精度、召回率和 F1 分数为 0.85。

感知器的局限性

尽管感知器在我们的例子中很好地分类了实例,但该模型存在局限性。像感知器这种使用 Heaviside 激活函数的线性模型并不是通用函数逼近器;它们无法表示某些函数。具体来说,线性模型只能学习逼近线性可分数据集的函数。我们所检查的线性分类器找到一个超平面,将正类与负类分开;如果没有一个超平面能够分开这些类别,那么问题就不是线性可分的。

一个简单的线性不可分的函数示例是逻辑运算XOR,即排他或运算。XOR 的输出为 1,当其输入之一为 1 而另一个为 0 时。XOR 的输入和输出在以下图中以二维形式绘制。当 XOR 输出1时,实例用圆圈标记;当 XOR 输出0时,实例用菱形标记,如下图所示:

感知器的局限性

使用单一的直线无法将圆形与菱形分开。假设这些实例是放置在板上的钉子。如果你用一根橡皮筋围绕正实例,并且用第二根橡皮筋围绕负实例,那么橡皮筋会在板中间交叉。橡皮筋代表 外壳,即包含集合内所有点以及连接集合中一对点的任何直线上的所有点的外包络。特征表示在更高维空间中比在低维空间中更有可能是线性可分的。例如,当使用高维表示如词袋模型时,文本分类问题往往是线性可分的。

在接下来的两章中,我们将讨论可以用于建模线性不可分数据的技术。第一种技术叫做核化,它将线性不可分的数据投影到一个更高维的空间,在这个空间中数据是线性可分的。核化可以应用于许多模型,包括感知机,但它与支持向量机特别相关,支持向量机将在下一章中讨论。支持向量机还支持可以找到将线性不可分的类别以最少错误分开的超平面的技术。第二种技术创建了一个感知机的有向图。由此生成的模型,称为人工神经网络,是一个通用的函数逼近器;我们将在第十章中讨论人工神经网络,从感知机到人工神经网络

摘要

本章中,我们讨论了感知机。感知机受到神经元的启发,是一种用于二分类的线性模型。感知机通过处理解释变量和权重的线性组合以及激活函数来对实例进行分类。虽然带有逻辑 sigmoid 激活函数的感知机与逻辑回归是相同的模型,但感知机通过在线的、基于误差的算法来学习其权重。感知机在某些问题中可以有效使用。像我们讨论的其他线性分类器一样,感知机并不是一个通用的函数逼近器;它只能通过超平面将一种类别的实例与另一种类别的实例分开。一些数据集是线性不可分的;也就是说,没有可能的超平面能够正确地分类所有实例。在接下来的章节中,我们将讨论两种可以处理线性不可分数据的模型:人工神经网络,它通过感知机图构建一个通用的函数逼近器;以及支持向量机,它将数据投影到一个更高维的空间,在这个空间中数据是线性可分的。

第九章. 从感知机到支持向量机

在上一章中,我们讨论了感知机。作为一种二分类器,感知机无法有效地对线性不可分的特征表示进行分类。在我们讨论第二章中线性回归时,也遇到了类似的问题;我们检查了一个响应变量与解释变量之间不是线性关系的数据集。为了提高模型的准确性,我们引入了一种多重线性回归的特例,称为多项式回归。我们创建了特征的合成组合,并能够在更高维的特征空间中建模响应变量与特征之间的线性关系。

尽管这种增加特征空间维度的方法看起来是用线性模型逼近非线性函数的有希望的技术,但它有两个相关的问题。第一个是计算问题;计算映射后的特征并处理更大的向量需要更多的计算能力。第二个问题涉及到泛化;增加特征表示的维度引入了维度灾难。学习高维特征表示需要指数级更多的训练数据,以避免过拟合。

本章中,我们将讨论一种强大的分类和回归模型,称为支持向量****机SVM)。首先,我们将重新探讨将特征映射到更高维空间。然后,我们将讨论支持向量机如何解决在学习映射到更高维空间的数据时遇到的计算和泛化问题。整个书籍都致力于描述支持向量机,描述训练 SVM 所用的优化算法需要比我们在之前的章节中使用的更为高级的数学。我们不会像之前那样通过玩具示例详细推导,而是尽量通过建立直觉来理解支持向量机的工作原理,以便有效地在 scikit-learn 中应用它们。

核与核技巧

回想一下,感知机通过使用超平面作为决策边界,将正类的实例与负类的实例分开。决策边界由以下方程给出:

核与核技巧

预测是通过以下函数进行的:

核与核技巧

请注意,之前我们将内积表示为核与核技巧,即核与核技巧。为了与支持向量机中使用的符号约定保持一致,我们将在本章采用前一种表示法。

虽然证明超出了本章的范围,但我们可以用不同的方式来写这个模型。下面的模型表达式被称为对偶形式。我们之前使用的表达式是原始形式:

核函数与核技巧

原始形式与对偶形式之间最重要的区别在于,原始形式计算的是模型参数和测试实例特征向量的内积,而对偶形式计算的是训练实例和测试实例特征向量的内积。稍后,我们将利用对偶形式的这一性质来处理线性不可分的类别。首先,我们需要正式化将特征映射到更高维空间的定义。

在第二章的多项式回归部分中,线性回归,我们将特征映射到一个更高维的空间,在这个空间中它们与响应变量是线性相关的。这个映射通过从原始特征的组合中创建二次项来增加特征的数量。这些合成特征使我们能够用线性模型表示一个非线性函数。一般来说,映射由以下表达式给出:

核函数与核技巧

下图左侧的图示展示了一个线性不可分数据集的原始特征空间。右侧的图示显示了数据在映射到更高维空间后变得线性可分:

核函数与核技巧

让我们回到决策边界的对偶形式,并观察到特征向量仅出现在点积中。我们可以通过如下方式将数据映射到更高维空间,通过对特征向量应用映射:

核函数与核技巧核函数与核技巧

如前所述,这个映射使我们能够表示更复杂的模型,但它也带来了计算和泛化问题。映射特征向量并计算它们的点积可能需要大量的计算资源。

请注意第二个方程式,尽管我们已将特征向量映射到更高维空间,但特征向量仍然只以点积的形式出现。点积是标量;一旦计算出这个标量,我们就不再需要映射后的特征向量。如果我们能够使用其他方法来产生与映射向量点积相同的标量,就可以避免显式计算点积和映射特征向量的高昂工作。

幸运的是,有一种方法叫做 核技巧核函数是一种函数,给定原始特征向量后,它返回与其对应的映射特征向量的点积相同的值。核函数并不显式地将特征向量映射到更高维的空间,或者计算映射向量的点积。核函数通过一系列不同的操作产生相同的值,这些操作通常可以更高效地计算。核函数在以下方程中定义得更为正式:

核函数与核技巧

让我们演示一下核函数是如何工作的。假设我们有两个特征向量,xz

核函数与核技巧核函数与核技巧

在我们的模型中,我们希望通过以下转换将特征向量映射到更高维的空间:

核函数与核技巧

映射后的归一化特征向量的点积相当于:

核函数与核技巧

由以下方程给出的核函数产生的值与映射特征向量的点积相同:

核函数与核技巧核函数与核技巧

让我们为特征向量代入值,使这个例子更加具体:

核函数与核技巧核函数与核技巧核函数与核技巧核函数与核技巧

由以下方程给出的核函数 核函数与核技巧 产生了与映射特征向量的点积 核函数与核技巧 相同的值,但从未显式地将特征向量映射到更高维空间,并且需要更少的算术运算。这个例子使用的是二维特征向量。即使是具有适度特征数量的数据集,也可能导致映射特征空间的维度巨大。scikit-learn 提供了几种常用的核函数,包括多项式核函数、Sigmoid 核函数、高斯核函数和线性核函数。多项式核函数由以下方程给出:

核函数与核技巧

二次核函数,或多项式核函数,其中 k 等于 2,通常用于自然语言处理。

Sigmoid 核函数由以下方程给出。核函数与核技巧核函数与核技巧 是可以通过交叉验证调整的超参数:

核函数与核技巧

高斯核是处理需要非线性模型问题的一个不错的选择。高斯核是一个径向基函数。在映射后的特征空间中,作为超平面的决策边界类似于原始空间中的超球面决策边界。高斯核产生的特征空间可以有无限多个维度,这是其他方法无法做到的。高斯核的表达式如下:

核函数与核技巧

核函数与核技巧是一个超参数。在使用支持向量机时,特征缩放始终很重要,但在使用高斯核时,特征缩放尤其重要。

选择一个核函数可能具有挑战性。理想情况下,核函数将以一种对任务有用的方式来衡量实例之间的相似性。虽然核函数通常与支持向量机一起使用,但它们也可以与任何可以通过两个特征向量的点积表示的模型一起使用,包括逻辑回归、感知机和主成分分析。在下一节中,我们将讨论映射到高维特征空间所导致的第二个问题:泛化。

最大间隔分类与支持向量

下图展示了来自两个线性可分类的实例以及三个可能的决策边界。所有这些决策边界都将正类的训练实例与负类的训练实例分开,且感知机可以学习它们中的任何一个。哪个决策边界最有可能在测试数据上表现最佳?

最大间隔分类与支持向量

从这个可视化中可以直观地看出,虚线决策边界是最优的。实线决策边界靠近许多正实例。测试集可能包含一个正实例,其第一个解释变量的值稍小,最大间隔分类与支持向量;该实例将被错误分类。虚线决策边界远离大多数训练实例,但它靠近一个正实例和一个负实例。下图提供了评估决策边界的不同视角:

最大间隔分类与支持向量

假设所绘制的线是逻辑回归分类器的决策边界。标记为A的实例远离决策边界;它会被预测为属于正类,并且概率较高。标记为B的实例仍会被预测为属于正类,但由于该实例接近决策边界,概率会较低。最后,标记为C的实例会被预测为属于正类,但概率较低;即使训练数据有细微变化,也可能改变预测的类别。最有信心的预测是针对那些远离决策边界的实例。我们可以通过其函数间隔来估计预测的信心。训练集的函数间隔由以下方程给出:

最大间隔分类与支持向量最大间隔分类与支持向量

在前述公式中,最大间隔分类与支持向量是实例的真实类别。对于实例A,函数间隔较大,而对于实例C,函数间隔较小。如果C被错误分类,则函数间隔为负数。函数间隔等于 1 的实例被称为支持向量。这些实例足以定义决策边界;其他实例则不需要用于预测测试实例的类别。与函数间隔相关的是几何间隔,即分隔支持向量的带状区域的最大宽度。几何间隔等于标准化后的函数间隔。必须对函数间隔进行标准化,因为它们可以通过使用最大间隔分类与支持向量来缩放,这对训练来说是有问题的。当最大间隔分类与支持向量是单位向量时,几何间隔等于函数向量。我们现在可以将最佳决策边界的定义形式化为具有最大几何间隔。通过以下约束优化问题,可以求解最大化几何间隔的模型参数:

最大间隔分类与支持向量最大间隔分类与支持向量

支持向量机的一个有用特性是,该优化问题是凸的;它有一个单一的局部最小值,这也是全局最小值。虽然证明超出了本章的范围,但之前的优化问题可以通过模型的对偶形式来表示,以适应核方法,如下所示:

最大间隔分类和支持向量最大间隔分类和支持向量最大间隔分类和支持向量

找到最大化几何间隔的参数,前提是所有正实例的函数间隔至少为 1,所有负实例的函数间隔最多为 -1,这是一个二次规划问题。这个问题通常使用一种叫做 序列最小优化SMO)的算法来解决。SMO 算法将优化问题分解为一系列最小的子问题,然后通过解析方法解决这些子问题。

在 scikit-learn 中分类字符

让我们应用支持向量机解决一个分类问题。近年来,支持向量机在字符识别任务中得到了成功应用。给定一张图像,分类器必须预测出图像中的字符。字符识别是许多光学字符识别系统中的一个组件。即便是较小的图像,在使用原始像素强度作为特征时,也需要高维表示。如果类别是线性不可分的,并且必须映射到更高维度的特征空间,特征空间的维度可能会变得更大。幸运的是,SVM 适合高效地处理这种数据。首先,我们将使用 scikit-learn 来训练一个支持向量机以识别手写数字。接着,我们将处理一个更具挑战性的问题:在照片中识别字母数字字符。

分类手写数字

混合国家标准与技术研究所(MNIST)数据库包含 70,000 张手写数字图像。这些数字是从美国人口普查局的员工和美国高中生的手写文档中采样得到的。图像为灰度图,尺寸为 28 x 28 像素。我们可以使用以下脚本查看其中的一些图像:

>>> import matplotlib.pyplot as plt
>>> from sklearn.datasets import fetch_mldata
>>> import matplotlib.cm as cm

>>> digits = fetch_mldata('MNIST original', data_home='data/mnist').data
>>> counter = 1
>>> for i in range(1, 4):
>>>     for j in range(1, 6):
>>>         plt.subplot(3, 5, counter)
>>>         plt.imshow(digits[(i - 1) * 8000 + j].reshape((28, 28)), cmap=cm.Greys_r)
>>>         plt.axis('off')
>>>         counter += 1
>>> plt.show()

首先,我们加载数据。如果数据集未在磁盘上找到,scikit-learn 提供了 fetch_mldata 方便的函数来下载数据集并将其读入一个对象。然后,我们为数字零、数字一和数字二创建一个子图,显示五个实例。脚本将生成以下图形:

分类手写数字

MNIST 数据集被划分为 60,000 张图像的训练集和 10,000 张图像的测试集。该数据集通常用于评估各种机器学习模型,之所以流行,是因为几乎不需要预处理。我们将使用 scikit-learn 构建一个分类器,能够预测图像中展示的数字。

首先,我们导入必要的类:

from sklearn.datasets import fetch_mldata
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import scale
from sklearn.cross_validation import train_test_split
from sklearn.svm import SVC
from sklearn.grid_search import GridSearchCV
from sklearn.metrics import classification_report

在网格搜索过程中,脚本会派生额外的进程,这要求从 __main__ 块执行。

if __name__ == '__main__':
    data = fetch_mldata('MNIST original', data_home='data/mnist')
    X, y = data.data, data.target
    X = X/255.0*2 – 1

接下来,我们使用fetch_mldata便捷函数加载数据。我们对特征进行缩放,并将每个特征围绕原点居中。然后,我们使用以下代码行将预处理后的数据分割为训练集和测试集:

    X_train, X_test, y_train, y_test = train_test_split(X, y)

接下来,我们实例化一个SVC,即支持向量分类器对象。该对象提供了类似于 scikit-learn 其他估算器的 API;分类器使用fit方法进行训练,并通过predict方法进行预测。如果查看SVC的文档,你会发现该估算器需要的超参数比我们讨论的大多数其他估算器要多。通常,更强大的估算器需要更多的超参数。对于SVC来说,最有趣的超参数是通过kernelgammaC关键字参数设置的。kernel关键字参数指定要使用的核。scikit-learn 提供了线性、多项式、sigmoid 和径向基函数核的实现。当使用多项式核时,还应该设置degree关键字参数。C控制正则化,它类似于我们在逻辑回归中使用的 lambda 超参数。gamma关键字参数是 sigmoid、多项式和 RBF 核的核系数。设置这些超参数可能会很具挑战性,因此我们通过网格搜索来调整它们,代码如下所示。

    pipeline = Pipeline([
        ('clf', SVC(kernel='rbf', gamma=0.01, C=100))
    ])
    print X_train.shape
    parameters = {
        'clf__gamma': (0.01, 0.03, 0.1, 0.3, 1),
        'clf__C': (0.1, 0.3, 1, 3, 10, 30),
    }
    grid_search = GridSearchCV(pipeline, parameters, n_jobs=2, verbose=1, scoring='accuracy')
    grid_search.fit(X_train[:10000], y_train[:10000])
    print 'Best score: %0.3f' % grid_search.best_score_
    print 'Best parameters set:'
    best_parameters = grid_search.best_estimator_.get_params()
    for param_name in sorted(parameters.keys()):
        print '\t%s: %r' % (param_name, best_parameters[param_name])
    predictions = grid_search.predict(X_test)
    print classification_report(y_test, predictions)

以下是前述脚本的输出:

Fitting 3 folds for each of 30 candidates, totalling 90 fits
[Parallel(n_jobs=2)]: Done   1 jobs       | elapsed:  7.7min
[Parallel(n_jobs=2)]: Done  50 jobs       | elapsed: 201.2min
[Parallel(n_jobs=2)]: Done  88 out of  90 | elapsed: 304.8min remaining:  6.9min
[Parallel(n_jobs=2)]: Done  90 out of  90 | elapsed: 309.2min finished
Best score: 0.966
Best parameters set:
	clf__C: 3
	clf__gamma: 0.01
             precision    recall  f1-score   support

        0.0       0.98      0.99      0.99      1758
        1.0       0.98      0.99      0.98      1968
        2.0       0.95      0.97      0.96      1727
        3.0       0.97      0.95      0.96      1803
        4.0       0.97      0.98      0.97      1714
        5.0       0.96      0.96      0.96      1535
        6.0       0.98      0.98      0.98      1758
        7.0       0.97      0.96      0.97      1840
        8.0       0.95      0.96      0.96      1668
        9.0       0.96      0.95      0.96      1729

avg / total       0.97      0.97      0.97     17500

最好的模型具有 0.97 的平均 F1 分数;通过在超过一万实例上进行训练,可以进一步提高此分数。

自然图像中的字符分类

现在让我们尝试一个更具挑战性的问题。我们将在自然图像中分类字母数字字符。Chars74K 数据集由 T. E. de Campos、B. R. Babu 和 M. Varma 为自然图像中的字符识别收集,包含了超过 74,000 张数字 0 到 9 以及大写和小写字母的图像。以下是三张小写字母z的图像示例。Chars74K 可以从www.ee.surrey.ac.uk/CVSSP/demos/chars74k/下载。

自然图像中的字符分类

收集的图像类型各异。我们将使用 7,705 张从印度班加罗尔街景照片中提取的字符图像。与 MNIST 相比,这部分 Chars74K 中的图像展示了各种字体、颜色和扰动。解压档案后,我们将使用English/Img/GoodImg/Bmp/目录中的文件。首先,我们将导入所需的类。

import os
import numpy as np
from sklearn.svm import SVC
from sklearn.cross_validation import train_test_split
from sklearn.metrics import classification_report
import Image

接下来,我们将定义一个使用 Python 图像库调整图像大小的函数:

def resize_and_crop(image, size):
    img_ratio = image.size[0] / float(image.size[1])
    ratio = size[0] / float(size[1])
    if ratio > img_ratio:
        image = image.resize((size[0], size[0] * image.size[1] / image.size[0]), Image.ANTIALIAS)
        image = image.crop((0, 0, 30, 30))
    elif ratio < img_ratio:
        image = image.resize((size[1] * image.size[0] / image.size[1], size[1]), Image.ANTIALIAS)
        image = image.crop((0, 0, 30, 30))
    else:
        image = image.resize((size[0], size[1]), Image.ANTIALIAS)
    return image

然后,我们将加载每个 62 个类别的图像,并将它们转换为灰度图像。与 MNIST 不同,Chars74K 的图像尺寸不一致,因此我们将使用我们定义的 resize_and_crop 函数将其调整为边长为 30 像素的大小。最后,我们将处理后的图像转换为 NumPy 数组:

X = []
y = []

for path, subdirs, files in os.walk('data/English/Img/GoodImg/Bmp/'):
    for filename in files:
        f = os.path.join(path, filename)
        img = Image.open(f).convert('L') # convert to grayscale
        img_resized = resize_and_crop(img, (30, 30))
        img_resized = np.asarray(img_resized.getdata(), dtype=np.float64) \
            .reshape((img_resized.size[1] * img_resized.size[0], 1))
        target = filename[3:filename.index('-')]
        X.append(img_resized)
        y.append(target)

X = np.array(X)
X = X.reshape(X.shape[:2])

We will then train a support vector classifier with a polynomial kernel.classifier = SVC(verbose=0, kernel='poly', degree=3)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1)
classifier.fit(X_train, y_train)
predictions = classifier.predict(X_test)
print classification_report(y_test, predictions)

上述脚本生成以下输出:

             precision    recall  f1-score   support

        001       0.24      0.22      0.23        23
        002       0.24      0.45      0.32        20
       ...
        061       0.33      0.15      0.21        13
        062       0.08      0.25      0.12         8

avg / total       0.41      0.34      0.36      1927

显然,这比在 MNIST 中分类数字要更加具有挑战性。字符的外观变化更为广泛,由于这些图像是从照片中采样的,而非扫描文档,字符也更容易受到扰动。此外,Chars74K 中每个类别的训练实例远少于 MNIST 中的数量。通过增加训练数据、采用不同的图像预处理方法,或使用更复杂的特征表示,分类器的性能可以得到提升。

摘要

在本章中,我们讨论了支持向量机——一种强大的模型,可以缓解感知机的一些局限性。感知机可以有效地用于线性可分的分类问题,但它无法表示更复杂的决策边界,除非将特征空间扩展到更高的维度。不幸的是,这种扩展容易引发计算和泛化问题。支持向量机通过使用核函数来解决第一个问题,核函数避免了显式计算特征映射。它们通过最大化决策边界与最近实例之间的边距来解决第二个问题。在下一章中,我们将讨论人工神经网络模型,类似于支持向量机,这些模型通过扩展感知机来克服其局限性。

第十章. 从感知机到人工神经网络

在第八章,感知机中,我们介绍了感知机,它是一个用于二分类的线性模型。你了解到,感知机并不是一个通用的函数逼近器;它的决策边界必须是一个超平面。在前一章中,我们介绍了支持向量机,通过使用核函数将特征表示有效地映射到一个更高维的空间,从而使实例能够线性可分,解决了感知机的一些局限性。在本章中,我们将讨论人工神经网络,它是用于分类和回归的强大非线性模型,采用了一种不同的策略来克服感知机的局限性。

如果感知机类似于神经元,那么人工神经网络,或称神经网络,则类似于大脑。正如数十亿个神经元与数万亿个突触组成了人类大脑,人工神经网络是由感知机或其他人工神经元组成的有向图。该图的边有权重;这些权重是模型的参数,必须通过学习来获得。

整本书讲述了人工神经网络的各个方面;本章将概述其结构和训练方法。在写作时,已经为 scikit-learn 开发了一些人工神经网络,但它们在版本 0.15.2 中不可用。读者可以通过查看包含神经网络模块的 scikit-learn 0.15.1 的分支来跟随本章的示例。该分支中的实现可能会在未来的 scikit-learn 版本中合并,且不会对本章中描述的 API 做出任何更改。

非线性决策边界

回想一下第八章,感知机,我们提到过,虽然一些布尔函数如 AND、OR 和 NAND 可以通过感知机来近似实现,但如以下图所示,线性不可分的函数 XOR 是无法通过感知机来实现的:

非线性决策边界

让我们更详细地回顾 XOR(异或),以便直观理解人工神经网络的强大功能。与当两个输入都为 1 时,AND 输出 1;当至少有一个输入为 1 时,OR 输出 1 不同,XOR 的输出是 1,仅当其中一个输入为 1 时。我们可以将 XOR 视为当两个条件成立时输出 1。第一个条件是至少有一个输入必须为 1;这与 OR 测试的条件相同。第二个条件是不能两个输入都为 1;NAND 测试这个条件。我们可以通过将输入先用 OR 和 NAND 处理,再用 AND 验证两个函数的输出都为 1 来产生与 XOR 相同的输出。也就是说,OR、NAND 和 AND 这三个函数可以组合成与 XOR 相同的输出。

以下表格提供了输入 AB 的异或、或、与、非与的真值表。通过这些表格,我们可以验证将或(OR)和非与(NAND)的输出输入到与(AND)中,得到的结果与将 AB 输入到异或(XOR)中得到的结果相同:

A B A 和 B A 非与 B A 或 B A 异或 B
0 0 0 1 0 0
0 1 0 1 1 1
1 0 0 1 1 1
1 1 1 0 1 0
A B A 或 B A 非与 B (A 或 B) 和 (A 非与 B)
--- --- --- --- ---
0 0 0 1 0
0 1 1 1 1
1 0 1 1 1
1 1 1 0 0

我们不会尝试用单个感知器表示异或(XOR),而是从多个人工神经元构建人工神经网络,每个神经元逼近一个线性函数。每个实例的特征表示将作为输入传递给两个神经元;一个神经元表示非与(NAND),另一个神经元表示或(OR)。这些神经元的输出将被第三个神经元接收,该神经元表示与(AND),用于测试异或的两个条件是否同时为真。

前馈和反馈人工神经网络

人工神经网络由三个部分组成。第一部分是模型的架构,或称拓扑结构,描述了神经元的层次以及它们之间连接的结构。第二部分是人工神经元使用的激活函数。第三部分是学习算法,用于寻找最优的权重值。

人工神经网络主要有两种类型。前馈神经网络是最常见的一种神经网络,其特点是定向无环图。前馈神经网络中的信号只会朝一个方向传播——向输出层传播。相反,反馈神经网络,或称递归神经网络,则包含循环。反馈循环可以表示网络的内部状态,根据输入的不同,反馈循环可以导致网络行为随时间发生变化。前馈神经网络通常用于学习一个函数,将输入映射到输出。由于反馈神经网络的时间行为特性,它们更适合处理输入序列。由于反馈神经网络在 scikit-learn 中没有实现,我们将在讨论中仅限于前馈神经网络。

多层感知器

多层感知器MLP)是最常用的人工神经网络之一。这个名字有些误导;多层感知器并不是一个具有多层的单一感知器,而是由多个层次的人工神经元组成,这些神经元可以是感知器。MLP 的各层形成一个定向无环图。通常,每一层都与后续的层完全连接;每一层中的每个人工神经元的输出都是下一层中每个人工神经元的输入,直到输出层。MLP 通常有三层或更多层的人工神经元。

输入层由简单的输入神经元组成。 输入神经元连接到至少一个隐藏层的人工神经元。 隐藏层表示潜在变量; 该层的输入和输出在训练数据中无法观察到。 最后,最后一个隐藏层连接到输出层。 以下图表描述了具有三层的多层感知器的架构。 标有+1的神经元是偏置神经元,大多数架构图中没有描绘。

多层感知器

隐藏层中的人工神经元或单元通常使用非线性激活函数,如双曲正切函数和逻辑函数,其方程如下:

多层感知器多层感知器

与其他监督模型一样,我们的目标是找到最小化成本函数值的权重值。 平方误差成本函数通常与多层感知器一起使用。 其由以下方程给出,其中m是训练实例的数量:

多层感知器

最小化成本函数

反向传播算法通常与梯度下降等优化算法结合使用,以最小化成本函数的值。 该算法以反向传播的混成词命名,并指向网络层中错误流动的方向。 反向传播理论上可用于训练任意数量的隐藏单元排列在任意数量的层的前馈网络,尽管计算能力限制了这种能力。

反向传播类似于梯度下降,它使用成本函数的梯度来更新模型参数的值。 与我们之前看到的线性模型不同,神经网络包含表示潜在变量的隐藏单元; 我们无法从训练数据中知道隐藏单元应该做什么。 如果我们不知道隐藏单元应该做什么,我们就无法计算它们的错误,也无法计算成本函数相对于它们的权重的梯度。 克服这一问题的一个天真解决方案是随机扰动隐藏单元的权重。 如果对一个权重的随机改变减少了成本函数的值,我们保存该改变并随机改变另一个权重的值。 这种解决方案的明显问题是其昂贵的计算成本。 反向传播提供了一个更有效的解决方案。

我们将通过反向传播训练一个前馈神经网络。该网络有两个输入单元,两个隐藏层,每个隐藏层有三个隐藏单元,和两个输出单元。输入单元完全连接到第一个隐藏层的单元,分别称为Hidden1Hidden2Hidden3。连接单元的边缘被初始化为小的随机权重。

前向传播

在前向传播阶段,特征被输入到网络,并通过后续层传递以产生输出激活值。首先,我们计算Hidden1单元的激活值。我们找到输入到Hidden1的加权和,然后通过激活函数处理这个和。注意,Hidden1除了接收来自输入单元的输入外,还接收来自偏置单元的恒定输入,偏置单元在图中未显示。在下图中,前向传播是激活函数:

前向传播

接下来,我们计算第二个隐藏单元的激活值。与第一个隐藏单元一样,它接收来自两个输入单元的加权输入,以及来自偏置单元的恒定输入。然后,我们将加权输入的和或预激活通过激活函数处理,如下图所示:

前向传播

然后,我们以相同的方式计算Hidden3的激活值:

前向传播

计算完第一层所有隐藏单元的激活值后,我们继续处理第二层隐藏单元。在这个网络中,第一层隐藏单元与第二层隐藏单元完全连接。与第一层隐藏单元类似,第二层隐藏单元接收来自偏置单元的恒定输入,这些偏置单元在图中未显示。接下来,我们计算Hidden4的激活值:

前向传播

接下来,我们计算Hidden5Hidden6的激活值。计算完第二层所有隐藏单元的激活值后,我们进入输出层,如下图所示。Output1的激活值是第二层隐藏单元的激活值的加权和,通过激活函数处理后得到。与隐藏单元类似,输出单元也接收来自偏置单元的恒定输入:

前向传播

我们以相同的方式计算Output2的激活值:

前向传播

我们已经计算出网络中所有单元的激活值,现在前向传播已完成。由于网络使用初始随机权重值,可能无法很好地逼近真实函数。我们现在必须更新权重值,使网络能够更好地逼近我们的函数。

反向传播

我们只能在输出单元处计算网络的误差。隐藏单元代表潜在变量;我们无法在训练数据中观察到它们的真实值,因此无法计算它们的误差。为了更新它们的权重,我们必须将网络的误差通过其各层反向传播。我们将从Output1开始。它的误差等于真实输出与预测输出之间的差,乘以该单元激活函数的偏导数:

反向传播

然后我们计算第二个输出单元的误差:

反向传播

我们计算了输出层的误差。现在我们可以将这些误差反向传播到第二个隐藏层。首先,我们将计算隐藏单元Hidden4的误差。我们将Output1的误差与连接Hidden4Output1的权重值相乘。我们同样计算Output2的误差。然后我们将这些误差相加,并计算它们的和与Hidden4的偏导数的乘积:

反向传播

我们同样计算了Hidden5的误差:

反向传播

然后我们计算下图中的Hidden6误差:

反向传播

我们计算了第二隐藏层相对于输出层的误差。接下来,我们将继续将误差反向传播到输入层。隐藏单元Hidden1的误差是它的偏导数与第二隐藏层中误差的加权和的乘积:

反向传播

我们同样计算隐藏单元Hidden2的误差:

反向传播

我们同样计算了Hidden3的误差:

反向传播

我们计算了第一隐藏层的误差。现在我们可以使用这些误差来更新权重值。我们将首先更新连接输入单元到Hidden1的边的权重,以及连接偏置单元到Hidden1的边的权重。我们将通过学习率、Hidden1的误差和Input1的值的乘积来递增连接Input1Hidden1的权重值。

我们将同样通过学习率、Hidden1的误差和Input2的值的乘积来递增Weight2的值。最后,我们将通过学习率、Hidden1的误差和 1 的乘积来递增连接偏置单元到Hidden1的权重值。

反向传播

然后我们将使用相同的方法更新连接隐藏单元Hidden2到输入单元和偏置单元的权重值:

反向传播

接下来,我们将更新连接输入层到Hidden3的权重值:

反向传播

自输入层到第一个隐藏层的权重值更新后,我们可以继续处理连接第一个隐藏层到第二个隐藏层的权重。我们将Weight7的值增加学习率、Hidden4的误差和Hidden1的输出的乘积。接着,类似地更新Weight8Weight15的权重值:

反向传播

Hidden5Hidden6的权重更新方式相同。我们更新了连接两个隐藏层的权重值。现在,我们可以更新连接第二个隐藏层和输出层的权重值。使用与前几层权重相同的方法,我们递增了W16W21的权重值:

反向传播反向传播

Weight21的值增加学习率、Output2的误差和Hidden6的激活的乘积后,我们完成了对网络权重的值的更新。现在,我们可以使用新的权重值执行另一个前向传播;使用更新后的权重计算得到的成本函数值应该更小。我们将重复此过程,直到模型收敛或满足其他停止标准。与我们讨论过的线性模型不同,反向传播不会优化凸函数。反向传播可能会收敛于指定局部而非全局最小值的参数值。在实践中,对于许多应用而言,局部最优通常是足够的。

用多层感知器逼近异或

让我们训练一个多层感知器来逼近异或函数。在撰写本文时,多层感知器已作为 2014 年 Google Summer of Code 项目的一部分实现,但尚未合并或发布。未来版本的 scikit-learn 很可能会包含这个多层感知器的实现,而 API 描述的部分将不会有任何改变。在此期间,可以从github.com/IssamLaradji/scikit-learn.git克隆包含多层感知器实现的 scikit-learn 0.15.1 分支。

首先,我们将创建一个玩具二元分类数据集,代表异或,并将其分为训练集和测试集:

>>> from sklearn.cross_validation import train_test_split
>>> from sklearn.neural_network import MultilayerPerceptronClassifier
>>> y = [0, 1, 1, 0] * 1000
>>> X = [[0, 0], [0, 1], [1, 0], [1, 1]] * 1000
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=3)

接下来,我们实例化MultilayerPerceptronClassifier。我们通过n_hidden关键字参数指定网络的架构,该参数接受一个隐藏层中隐藏单元数的列表。我们创建了一个使用逻辑激活函数的具有两个单元的隐藏层。MultilayerPerceptronClassifier类会自动创建两个输入单元和一个输出单元。在多类问题中,分类器将为每个可能的类别创建一个输出单元。

选择一个架构是具有挑战性的。虽然有一些经验法则可以选择隐藏单元和层数的数量,但这些规则通常仅仅是通过轶事证据来支持的。最佳的隐藏单元数量取决于训练实例的数量、训练数据中的噪声、要逼近的函数的复杂性、隐藏单元的激活函数、学习算法以及采用的正则化方法。在实践中,架构只能通过交叉验证比较它们的性能来进行评估。

我们通过调用 fit()方法训练网络:

>>> clf = MultilayerPerceptronClassifier(n_hidden=[2],
>>>                                      activation='logistic',
>>>                                      algorithm='sgd',
>>>                                      random_state=3)
>>> clf.fit(X_train, y_train)

最后,我们打印一些预测结果以供手动检查,并评估模型在测试集上的准确性。该网络在测试集上完美地逼近了 XOR 函数:

>>> print 'Number of layers: %s. Number of outputs: %s' % (clf.n_layers_, clf.n_outputs_)
>>> predictions = clf.predict(X_test)
>>> print 'Accuracy:', clf.score(X_test, y_test)
>>> for i, p in enumerate(predictions[:10]):
>>>     print 'True: %s, Predicted: %s' % (y_test[i], p)
Number of layers: 3\. Number of outputs: 1
Accuracy: 1.0
True: 1, Predicted: 1
True: 1, Predicted: 1
True: 1, Predicted: 1
True: 0, Predicted: 0
True: 1, Predicted: 1
True: 0, Predicted: 0
True: 0, Predicted: 0
True: 1, Predicted: 1
True: 0, Predicted: 0
True: 1, Predicted: 1

手写数字分类

在上一章中,我们使用支持向量机对 MNIST 数据集中的手写数字进行了分类。在本节中,我们将使用人工神经网络对图像进行分类:

from sklearn.datasets import load_digits
from sklearn.cross_validation import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neural_network.multilayer_perceptron import MultilayerPerceptronClassifier

首先,我们使用load_digits便捷函数加载 MNIST 数据集。我们将在交叉验证期间分叉额外的进程,这需要从main-保护块中执行:

>>> if __name__ == '__main__':
>>>     digits = load_digits()
>>>     X = digits.data
>>>     y = digits.target

特征缩放对于人工神经网络尤为重要,它将帮助某些学习算法更快地收敛。接下来,我们创建一个Pipeline类,在拟合MultilayerPerceptronClassifier之前先对数据进行缩放。该网络包含一个输入层、一个具有 150 个单元的隐藏层、一个具有 100 个单元的隐藏层和一个输出层。我们还增加了正则化超参数alpha的值。最后,我们打印三个交叉验证折叠的准确率。代码如下:

>>>     pipeline = Pipeline([
>>>         ('ss', StandardScaler()),
>>>         ('mlp', MultilayerPerceptronClassifier(n_hidden=[150, 100], alpha=0.1))
>>>     ])
>>>     print cross_val_score(pipeline, X, y, n_jobs=-1)
Accuracies [ 0.95681063  0.96494157  0.93791946]

平均准确率与支持向量分类器的准确率相当。增加更多的隐藏单元或隐藏层,并进行网格搜索来调整超参数,可能会进一步提高准确率。

总结

本章我们介绍了人工神经网络,它们是用于分类和回归的强大模型,可以通过组合多个人工神经元来表示复杂的函数。特别地,我们讨论了人工神经元的有向无环图,称为前馈神经网络。多层感知机是前馈网络的一种类型,其中每一层都与后续层完全连接。一个具有一个隐藏层和有限数量隐藏单元的 MLP 是一个通用的函数逼近器。它可以表示任何连续函数,尽管它不一定能够自动学习适当的权重。我们描述了网络的隐藏层如何表示潜在变量,以及如何使用反向传播算法学习它们的权重。最后,我们使用 scikit-learn 的多层感知机实现来逼近 XOR 函数并分类手写数字。

本章总结了本书的内容。我们讨论了各种模型、学习算法和性能评估标准,以及它们在 scikit-learn 中的实现。在第一章中,我们将机器学习程序定义为那些通过经验学习来改善其任务表现的程序。随后,我们通过实例演示了机器学习中一些最常见的经验、任务和性能评估标准。我们对比萨的价格与其直径进行了回归分析,并对垃圾邮件和普通文本消息进行了分类。我们将颜色聚类用于图像压缩,并对 SURF 描述符进行了聚类以识别猫狗的照片。我们使用主成分分析进行面部识别,构建了随机森林以屏蔽横幅广告,并使用支持向量机和人工神经网络进行光学字符识别。感谢您的阅读;希望您能够利用 scikit-learn 以及本书中的示例,将机器学习应用到您自己的实践中。

posted @ 2025-07-14 17:27  绝不原创的飞龙  阅读(14)  评论(0)    收藏  举报