Python-真实世界的数据科学-十-

64 阅读1小时+

Python 真实世界的数据科学(十)

原文:Python: Real-World Data Science

协议:CC BY-NC-SA 4.0

三十四、下一步...

在本书的学习过程中,没有采取很多途径,没有提出选择,并且没有充分探索主题。 在本附录中,我为那些希望进行更多学习并使用 Python 进行数据挖掘的人员创建了一系列后续步骤。 考虑本书的第二个问题是英雄模式。

本附录按章节进行了细分,包括文章,书籍和其他资源,以了解有关数据挖掘的更多信息。 还包括扩展本章中完成的工作的一些挑战。 其中一些将是小的改进; 有些工作要做得更多—我已经记下了那些明显比其他任务更广泛的任务。

第 1 章–数据挖掘入门

Scikit 学习教程

scikit-learn.org/stable/tuto…

scikit-learn 文档包含在中,是一系列有关数据挖掘的教程。 教程范围从基本介绍到玩具数据集,一直到最近研究中使用的技术的综合教程。

这里的教程将花费相当长的时间-它们非常全面-但是值得努力学习。

扩展 IPython Notebook

ipython.org/ipython-doc…

IPython Notebook 是一个功能强大的工具。 它可以通过多种方式进行扩展,其中之一是与主计算机分开创建一个服务器来运行笔记本电脑。 如果您使用低功率的主计算机(例如小型笔记本电脑),但可以使用功能更强大的计算机,这将非常有用。 此外,您可以设置节点以执行并行计算。更多数据集可从这个页面获得。

第 2 章–使用 scikit-learn 估计器进行分类

更复杂的管道

scikit-learn.org/stable/modu…

我们在模块中使用的 Pipelines 遵循单个流-一个步骤的输出是另一步骤的输入。

管道也遵循转换器和估计器接口-这使我们可以将管道嵌入管道中。 对于非常复杂的模型,这是一个有用的构造,但如上链接所示,当与 Feature Unions 结合使用时,其功能将非常强大。

这使我们可以一次提取多种类型的特征,然后将它们组合以形成单个数据集。 有关更多详细信息,请参见这个页面上的示例。

比较分类器

scikit-learn 中有可供使用的许多分类器。 您为特定任务选择的任务将基于多种因素。 您可以比较 f1 分数以查看哪种方法更好,并且可以调查那些分数的偏差以查看该结果是否具有统计意义。

一个重要的因素是它们是在相同的数据上进行训练和测试的,也就是说,一个分类器的测试集就是所有分类器的测试集。 我们对随机状态的使用使我们能够确保确实如此-这是复制实验的重要因素。

第 3 章:用决策树预测体育获胜者

有关 Pandas 的更多信息

pandas 库是一个很棒的软件包-您通常编写的用于加载数据的任何内容都可能已在 pandas 中实现。 您可以在教程中了解有关此内容的更多信息

克里斯·莫菲特(Chris Moffitt)也撰写了一篇很棒的博客文章,概述了人们在 Excel 中执行的常见任务以及如何在 Pandas 中执行这些任务

您还可以使用 Pandas 处理大型数据集。 请参阅从用户 Jeff(撰写本文时为时的最高答案)到此 StackOverflow 问题的答案,以获取有关该过程的广泛概述

Brian Connelly 撰写了另一本关于 Pandas 的出色教程

第 7 章–使用图挖掘发现要遵循的帐户

更复杂的算法

在预测图形中的链接(包括社交网络)方面进行了广泛的研究。 例如,David Liben-Nowell 和 Jon Kleinberg 发表了有关该主题的论文,该论文将成为上面链接的更复杂算法的理想之地。 可在这个页面获得。

第 4 章-使用亲和力分析推荐电影

Eclat 算法

www.borgelt.net/eclat.html

本章中实现的先验算法很容易成为关联规则挖掘图中最著名的,但不一定是最好的。 Eclat 是一种更现代的算法,可以相对容易地实现。

第 5 章–使用提升器提取特征

Vowpal Wabbit

hunch.net/~vw/

Vowpal Wabbit 是一个不错的项目,它为基于文本的问题提供了非常快速的特征提取。 它带有 Python 包装器,可让您从 Python 代码中调用它。 在大型数据集上进行测试,例如我们在第 12 章,“处理大数据”中使用的数据集。

第 6 章–使用朴素贝叶斯的社交媒体洞察

自然语言处理和词性标记

与其他领域中使用的某些语言模型相比,本章中使用的技术非常轻巧。 例如,词性标记可以帮助消除单词形式的歧义,从而提高准确性。 NLTK 随附的书中有一章对此有所说明。 整本书也很值得一读。

第 8 章–用神经网络击败验证码

更深层的网络

这些技术可能会愚弄我们当前的实现,因此需要进行改进以使该方法更好。 尝试使用第 11 章,“使用深度学习”对图像中的对象进行分类的更深层网络。

但是,较大的网络需要更多数据,因此,为了获得良好的性能,您可能需要生成本章中完成的几千个以上的样本。 生成这些数据集是并行化的一个很好的选择,并行化是许多可以独立执行的小任务。

强化学习

pybrain.org/docs/tutori…

强化学习作为数据挖掘的下一件大事越来越受到关注-尽管已经有很长时间了! PyBrain 有一些强化学习算法,值得与此数据集(以及其他数据集)进行检查。

第 9 章-作者归属

本地 n-gram

分类器的另一种形式是局部 n-gram,它涉及为每个作者选择最佳功能,而不是为整个数据集全局选择最佳特征。 我写了一个关于使用本地 n-gram 进行作者身份归属的教程,该教程可在这个页面获得。

第 10 章-集群新闻文章

实时集群

k 均值算法可以随着时间进行迭代训练和更新,而不是在给定的时间范围内进行离散分析。 可以通过多种方式跟踪集群的移动,例如,您可以跟踪每个集群中流行的单词以及质心每天移动多少。 记住 API 限制-您可能只需要每隔几小时进行一次检查就可以使算法保持最新​​状态。

第 11 章–使用深度学习对图像中的对象进行分类

Keras 和 Pylearn2

如果要通过 Python 进行深度学习进一步学习,其他值得研究的深度学习库是 Keras 和 Pylearn2。 它们都基于 Theano 并具有不同的用法和功能。

可在此处找到 Keras

Pylearn2 可以在此处找到

在撰写本文时,两者都不是稳定的平台,尽管 Pylearn2 在两者中更稳定。 也就是说,他们俩都做得很好,值得为以后的项目进行研究。

另一个名为 Torch 的库非常受欢迎,但是在编写本文时,它没有 python 绑定(请参阅这个页面)。

Mahotas

另一个用于图像处理的软件包是 Mahotas,它包括更好,更复杂的图像处理技术,尽管它们可能会带来很高的计算成本,但它们可以帮助实现更高的准确性。 但是,许多图像处理任务是并行化的良好候选者。 可以在研究文献中找到更多的图像分类技术,以此调查文件作为一个良好的开端

第 12 章-处理大数据

Hadoop 课程

Yahoo 和 Google 都提供了有关 Hadoop 的出色教程,从入门到相当高级。 他们没有专门使用 Python 解决问题,但是学习 Hadoop 概念,然后将其应用到 Pydoop 或类似的库中可以产生很好的结果。

雅虎的教程

Google 的教程

Pydoop

Pydoop 是一个运行 Hadoop 作业的 python 库-它也有一个很棒的教程,可以在这里找到

尽管您也可以在 mrjob 中获得该功能,但 Pydoop 还可以与 HDFS(Hadoop 文件系统)一起使用。 Pydoop 将使您对运行某些作业有更多控制。

推荐引擎

构建大型推荐引擎是对大数据技能的良好测试。 Mark Litwintschik 的一篇很棒的博客文章介绍了使用 Apache Spark 这一大数据技术的引擎

更多资源

Kaggle 比赛:

www.kaggle.com/

Kaggle 定期举办数据挖掘比赛,并经常获得奖金。 在 Kaggle 比赛中测试您的技能是学习与现实世界中的数据挖掘问题一起工作的一种快速而出色的方法。 这些论坛非常好,可以共享环境-在竞赛期间,您通常会看到发布前 10 名参赛者的代码!

三十五、赋予计算机学习数据的能力

在我看来,机器学习是使数据有意义的算法的应用和科学,是所有计算机科学中最激动人心的领域! 我们生活在一个数据丰富的时代。 使用机器学习领域的自学习算法,我们可以将这些数据转化为知识。 得益于近年来开发的许多强大的开源库,可能再没有比现在更好的时机进入机器学习领域并学习如何利用强大的算法来发现数据模式并对未来事件做出预测。

在本章中,我们将学习机器学习的主要概念和不同类型。 连同对相关术语的基本介绍,我们将为成功使用机器学习技术解决实际问题奠定基础。

在本章中,我们将介绍以下主题:

  • 机器学习的一般概念
  • 三种学习和基本术语
  • 成功设计机器学习系统的基础

如何将数据转化为知识

在现代技术的这个时代,我们拥有大量的资源:大量的结构化和非结构化数据。 在 20 世纪下半叶,机器学习作为人工智能的一个子领域发展起来,其中涉及开发自学习算法以从该数据中获取知识以进行预测。 机器学习不再需要人工从大量数据的分析中得出规则并建立模型,而是提供了一种更有效的替代方法来捕获数据中的知识,从而逐步提高了预测模型的性能并做出了数据驱动的决策。 机器学习不仅在计算机科学研究中变得越来越重要,而且在我们的日常生活中也起着越来越重要的作用。 借助机器学习,我们可以使用强大的电子邮件垃圾邮件过滤器,便捷的文本和语音识别软件,可靠的 Web 搜索引擎,具有挑战性的国际象棋棋手,以及希望不久之后可以使用的安全高效的自动驾驶汽车。

三种不同类型的机器学习

在本节中,我们将研究三种类型的机器学习:监督学习非监督学习强化学习。 我们将了解这三种不同学习类型之间的基本区别,并使用概念性示例,为可以应用这些问题的实际问题领域发展直觉:

The three different types of machine learning

通过监督学习对未来进行预测

监督学习中的主要目标是从标记的训练数据中学习一个模型,该模型可让我们对看不见或将来的数据进行预测。 在此,术语监督是指一组样本,其中所需的输出信号(标签)是已知的。

考虑到电子邮件垃圾邮件过滤的示例,我们可以在标签电子邮件,正确标记为垃圾邮件的电子邮件的语料库上使用监督机器学习算法训练模型,以预测是否有新的电子邮件 -mail 属于两个类别之一。 具有离散类标签的监督学习任务,例如在先前的电子邮件垃圾邮件过滤示例中,也称为分类任务。 监督学习的另一个子类别是回归,其中结果信号是一个连续值:

Making predictions about the future with supervised learning

用于预测类别标签的分类

分类是监督学习的子类别,目的是根据过去的观察结果预测新实例的类别标签。 这些类标签是离散的无序值,可以理解为实例的组成员资格。 前面提到的电子邮件垃圾邮件检测示例代表了二进制分类任务的典型示例,其中机器学习算法学习了一组规则以区分两种可能的类别:垃圾邮件和非垃圾邮件。 垃圾邮件。

但是,类标签集不必具有二进制性质。 通过监督学习算法学习的预测模型可以将训练数据集中显示的任何类别标签分配给新的未标签实例。 多类别分类任务的典型示例是手写字符识别。 在这里,我们可以收集一个训练数据集,该数据集由字母表中每个字母的多个手写示例组成。 现在,如果用户通过输入设备提供了新的手写字符,我们的预测模型将能够以一定的准确性预测字母表中的正确字母。 但是,我们的机器学习系统将无法正确识别零到九的任何数字,例如,如果它们不是我们训练数据集的一部分。

下图说明了给定 30 个训练样本的二进制分类任务的概念:15 个训练样本被标记为负面类别(圆圈),而 15 个训练样本被标记为正面类别 ](加号)。 在这种情况下,我们的数据集是二维的,这意味着每个样本都有两个与之关联的值:x[1]x[2]。 现在,我们可以使用监督式机器学习算法来学习一条规则(决策边界以黑色虚线表示),该规则可以将这两个类别分开,并根据x[1]x[2]将新数据分为这两个类别中的每一个 值:

Classification for predicting class labels

预测连续结果的回归

我们在的上一节中了解到,分类的任务是为实例分配分类无序的标签。 监督学习的第二种类型是对连续结果的预测,这也称为回归分析。 在回归分析中,我们得到了许多预测变量(解释性)变量和连续响应变量(结果),我们试图找到这些变量之间的关系,使我们能够 预测结果。

例如,让我们假设我们对预测学生的数学 SAT 成绩感兴趣。 如果为考试而花费的时间与最终分数之间存在关联,我们可以将其用作训练数据,以学习一个模型,该模型利用学习时间来预测计划参加此考试的未来学生的考试分数。

注意

术语回归由弗朗西斯·加尔顿(Francis Galton)于 1886 年在他的文章向遗传性中枢的平庸回归中提出。 人口不会随着时间增加。 他观察到父母的身高并未传递给孩子,但孩子的身高正在向人口均值回归。

下图说明了线性回归的概念。 给定一个预测变量x和一个响应变量y,我们对该数据拟合一条直线,以使采样点和拟合点之间的距离(通常是平均平方距离)最小化 线。 现在,我们可以使用从该数据中学到的截距和斜率来预测新数据的结果变量:

Regression for predicting continuous outcomes

通过强化学习解决互动问题

机器学习的另一种类型是强化学习。 在强化学习中,目标是开发一个基于与环境的交互作用来提高其性能的系统( agent )。 由于有关环境当前状态的信息通常还包含所谓的奖励信号,因此我们可以将强化学习视为与监督的学习相关的领域。 但是,在强化学习中,此反馈不是正确的地面真理标签或值,而是衡量奖励函数对动作的评估程度的方法。 通过与环境的交互,代理可以使用强化学习来学习一系列行动,这些行动可以通过探索性的试错法或深思熟虑的计划来最大化这种奖励。

强化学习的一个流行示例是国际象棋引擎。 在这里,座席根据棋盘的状态(环境)决定一系列动作,奖励可以定义为游戏结束时

Solving interactive problems with reinforcement learning

通过无监督学习发现隐藏结构

在监督学习中,当我们训练模型时,我们事先知道正确答案,在强化学习中,我们定义了奖励针对特定动作的度量 代理人。 但是,在无监督学习中,我们正在处理未标记的数据或未知结构的数据。 使用无监督学习技术,我们可以在没有已知结果变量或奖励函数指导的情况下,探索数据结构以提取有意义的信息。

通过聚类查找子组

聚类是一种探索性数据分析技术,它使我们可以将一堆信息组织成有意义的子组(集群),而无需事先知道它们的组成员身份。 分析期间可能出现的每个聚类定义一组对象,这些对象具有一定程度的相似性,但与其他聚类中的对象更为不同,这就是为什么聚类有时也称为“无监督分类”的原因。 聚类是一种用于构造信息并在数据之间派生有意义的关系的出色技术,例如,它允许营销人员根据他们的兴趣来发现客户组,以开发独特的营销程序。

下图说明了如何基于它们的特征x[1]x[2]的相似性,将聚类应用于将未标记的数据分为三个不同的组:

Finding subgroups with clustering

降低数据压缩量

无监督学习的另一个子字段是降维。 通常,我们使用的是高维数据(每次观察都带有大量测量值),这可能对有限的存储空间和机器学习算法的计算性能提出了挑战。 无监督降维是特征预处理中从数据中去除噪声的一种常用方法,这还会降低某些算法的预测性能,并在保留大多数相关信息的同时将数据压缩到较小的维子空间中。

有时,降维对于可视化数据也很有用,例如,可以将高维特征集投影到一维,二维或三维特征空间上,以便通过 3D 或 2D 对其进行可视化 -散点图或直方图。 下图显示了一个示例,其中应用了非线性降维将 3D Swiss Roll 压缩到新的 2D 特征子空间上:

Dimensionality reduction for data compression

Dimensionality reduction for data compression

基本术语和符号介绍

现在,我们已经讨论了机器学习的三大类(监督学习,无监督学习和强化学习),下面让我们看一下将在下一章中使用的基本术语。 下表描述了 Iris 数据集的摘录,其中是机器学习领域中的经典示例。 鸢尾花数据集包含来自三个不同物种的 150 种鸢尾花的测量值:SetosaVersicolorVirginica。 在这里,每个花朵样本代表我们数据集中的一行,并且以厘米为单位的花朵测量值存储为列,我们也将其称为数据集的特征:

An introduction to the basic terminology and notations

为了使标记和实现简单而有效,我们将使用线性代数的一些基础知识。 在以下各章中,我们将使用矩阵向量表示法来引用我们的数据。 我们将遵循通用约定将每个样本表示为特征矩阵X中的单独行,其中每个特征都存储为单独的列。

然后,可以将由 150 个样本和 4 个要素组成的 Iris 数据集写为150 x 4矩阵x ∈ R^(150x4)

An introduction to the basic terminology and notations

注意

在本模块的其余部分,我们将使用上标*(i)*指代i训练样本,下标j指代j训练数据集的维度。

我们分别使用小写的粗体字母表示向量x ∈ R^(nx1)和使用大写的粗体字母表示矩阵X ∈ R^(nxm)。 要引用向量或矩阵中的单个元素,我们用斜体写字母(分别为x^(n)x^(m, n))。

例如,x^(150, 1)是指花朵样品 150 的第一维度,即萼片长度。 因此,该特征矩阵中的每一行都代表一个花朵实例,并且可以写为四维行向量x^(i) ∈ R(1x4)x^(i) = [x^(i, 1), x^(i, 2), x^(i, 3), x^(i, 4)]

每个要素维都是一个 150 维的列向量x[j] ∈ R(150x1),例如:

An introduction to the basic terminology and notations

同样,我们将目标变量(此处为类标签)存储为 150 维列向量y = [y^(1), ..., y^(150)]^T, y = {setosa, versicolor, verginica}

建立机器学习系统的路线图

在前面的中,我们讨论了机器学习的基本概念以及三种不同的学习类型。 在本节中,我们将讨论伴随学习算法的机器学习系统的其他重要部分。 下图显示了在预测模型中使用机器学习的典型工作流程图,我们将在以下小节中进行讨论:

A roadmap for building machine learning systems

预处理–使数据成形

原始数据很少以学习算法的最佳性能所必需的形式出现。 因此,数据的预处理是任何机器学习应用中最关键的步骤之一。 如果以上一节中的鸢尾花数据集为例,我们可以将原始数据视为一系列花图像,以从中提取有意义的特征。 有用的功能可能是颜色,色调,花朵的强度,高度以及花朵的长度和宽度。 许多机器学习算法还要求所选特征必须具有相同的比例才能获得最佳性能,这通常是通过将特征转换为[0,1]范围或均值和单位方差为零的标准正态分布来实现的,因为我们将 请参阅后面的章节。

某些选定的特征可能高度相关,因此在一定程度上是多余的。 在那些情况下,降维技术可用于将特征压缩到较低维子空间上。 减少特征空间的维数具有以下优点:需要较少的存储空间,并且学习算法可以运行得更快。

为了确定我们的机器学习算法是否不仅在训练集上表现良好,而且还可以很好地推广到新数据,我们还希望将数据集随机分为单独的训练和测试集。 我们使用训练集来训练和优化我们的机器学习模型,同时保留测试集直到最后评估最终模型。

训练和选择预测模型

正如我们将在后面的章节中看到的那样,已经开发了许多不同的机器学习算法来解决不同的问题任务。 可以从 David Wolpert 著名的没有免费午餐定理中总结出一个重要的观点,那就是我们不能“免费”学习(学习算法之间缺乏先验区分, DH Wolpert 1996;没有免费的午餐定理用于优化,DH Wolpert 和 WG Macready,1997)。 直观地讲,我们可以将这一概念与流行语联系起来:“如果您仅有的工具是锤子,我想这很诱人,就好像把它当作钉子一样对待”(Abraham Maslow,1966 年 )。 例如,每种分类算法都有其固有的偏差,如果不对任务进行任何假设,则没有哪个分类模型会具有优势。 因此,在实践中,有必要比较至少几种不同的算法,以训练和选择性能最佳的模型。 但是,在我们可以比较不同的模型之前,我们首先必须确定一个衡量绩效的指标。 一种常用的度量标准是分类准确性,它定义为正确分类的实例的比例。

要问的一个合法问题是:如果不将测试集用于模型选择,而将其保留用于最终模型评估,那么我们如何知道哪个模型在最终测试数据集和真实数据上表现良好? 为了解决此问题中嵌入的问题,可以使用不同的交叉验证技术,其中将训练数据集进一步分为训练和验证子集以估计[HTG5 模型的泛化性能。 最后,我们也不能期望软件库提供的不同学习算法的默认参数对于我们的特定问题任务是最佳的。 因此,我们将在以后的章节中频繁使用超参数优化技术,这些技术可帮助我们微调模型的性能。 直观地,我们可以将这些超参数视为不是从数据中学习到的参数,而是代表我们可以用来提高其性能的模型的旋钮,当我们看到实际示例时,这些参数将在后面的章节中变得更加清楚。

评估模型并预测看不见的数据实例

在我们选择了适合训练数据集的模型之后,我们可以使用测试数据集来估计它在看不见的数据上的表现如何,以估计泛化误差。 如果我们对其性能感到满意,我们现在可以使用此模型来预测新的未来数据。 很重要,请注意,先前提到的过程的参数(例如特征缩放和降维)仅从训练数据集中获取,并且稍后再次应用相同的参数来转换测试数据集 与任何新数据样本一样,否则测试数据上的性能可能会过分乐观。

使用 Python 进行机器学习

Python 是上最流行的数据科学编程语言之一,因此拥有由其强大社区开发的大量有用的附加库。

尽管对于诸如计算密集型任务,解释性语言(如 Python)的性能不如低级编程语言,但已开发了诸如 NumPySciPy 等扩展库。 它们在较低层的 Fortran 和 C 实现的基础上进行,以便在多维数组上进行快速和向量化操作。

对于机器学习编程任务,我们将主要参考 scikit-learn 库,它是迄今为止最受欢迎和可访问的开源机器学习库之一。

Using Python for machine learning

Using Python for machine learning

Using Python for machine learning

Using Python for machine learning

三十六、训练机器学习分类算法

在本章中,我们将使用第一种以算法描述的机器学习算法进行分类,即感知器自适应线性神经元。 我们将首先在 Python 中逐步实现感知器,然后对其进行训练,以对 Iris 数据集中的不同花卉种类进行分类。 这将帮助我们理解用于分类的机器学习算法的概念,以及如何在 Python 中有效地实现它们。 然后,在第 3 章,“使用 scikit-learn 机器学习库”中,讨论使用自适应线性神经元进行优化的基础将为使用更强大的分类器奠定基础。

我们将在本章中介绍的主题如下:

  • 建立机器学习算法的直觉
  • 使用 pandas,NumPy 和 matplotlib 读取,处理和可视化数据
  • 在 Python 中实现线性分类算法

人工神经元–简要了解机器学习的早期历史

在更详细地讨论感知器和相关算法之前,让我们简要介绍一下机器学习的早期知识。 为了理解生物大脑如何设计 HT,Warren McCullock 和 Walter Pitts 发表了第一个简化的脑细胞概念,即所谓的 McCullock-Pitts(MCP)神经元。 1943 年(WS McCulloch 和 W. Pitts。神经活动中固有的思想的逻辑演算。数学生物物理学通报,5(4):115–133,1943 年)。 神经元是大脑中相互连接的神经细胞,参与处理和化学和电信号的传输,如下图所示:

Artificial neurons – a brief glimpse into the early history of machine learning

McCullock 和 Pitts 将这种神经细胞描述为具有二进制输出的简单逻辑门。 多个信号到达树突,然后整合到细胞体中,如果累积的信号超过某个阈值,则会生成输出信号,该信号将被轴突传递。

仅仅几年后,弗兰克·罗森布拉特(Frank Rosenblatt)就发布了基于 MCP 神经元模型的感知器学习规则的第一个概念(F. Rosenblatt,感知器,感知和识别自动机,康奈尔航空实验室,1957 年)。 Rosenblatt 运用他的感知器规则,提出了一种算法,该算法将自动学习最佳权重系数,然后将其与输入特征相乘,从而确定神经元是否触发。 在监督学习和分类的情况下,可以使用这种算法来预测样本是否属于一个类别或另一个类别。

更正式地讲,我们可以将此问题作为二进制分类任务提出,为简单起见,我们将两个类称为1(正类)和-1(负类)。 然后,我们可以定义激活函数φ(z),该函数采用某些输入值x和相应的权重向量w的线性组合,其中z是所谓的净输入(z = w[1]x[1] + ... + w[m]x[m]):

Artificial neurons – a brief glimpse into the early history of machine learning

现在,如果特定样本x^(i)的激活(即φ(z)的输出)大于定义的阈值θ,则我们将预测 1 类和-1 类,否则。 在感知器算法中,激活函数φ(·)是简单的单位阶跃函数,有时也称为 Heaviside 阶跃函数

Artificial neurons – a brief glimpse into the early history of machine learning

为简单起见,我们可以将阈值θ带到方程式的左侧,并将权重零定义为w[0] = -θx[0] = 1,以便我们以更紧凑的形式写zφ(z)

Artificial neurons – a brief glimpse into the early history of machine learning

注意

在以下各节中,我们将经常使用线性代数的基本符号。 例如,我们将使用向量点积来缩写xw中值的乘积之和,而上标T代表转置,该操作可将列向量转换为行向量,反之亦然:

Artificial neurons – a brief glimpse into the early history of machine learning

例如:

Artificial neurons – a brief glimpse into the early history of machine learning

此外,转置操作还可以应用于矩阵以在其对角线上反射它,例如:

Artificial neurons – a brief glimpse into the early history of machine learning

在本书中,我们将仅使用线性代数的基本概念。 但是,如果需要快速复习,请查看 Zico Kolter 出色的《线性代数复习和参考》,该书可从这个页面免费获得。

下图说明了如何通过感知器的激活函数将净输入z = w^T x压缩为二进制输出(-1 或 1)(左子图),以及如何将其用于区分两个线性可分离的类 (右图):

Artificial neurons – a brief glimpse into the early history of machine learning

MCP 神经元和 Rosenblatt 的阈值感知器模型背后的整个想法是使用还原论方法来模拟大脑中单个神经元的工作方式:触发或不触发。 因此,Rosenblatt 的初始感知器规则非常简单,可以通过以下步骤进行总结:

  1. 将权重初始化为 0 或较小的随机数。

  2. 对于每个训练样本x^(i)执行以下步骤:

    1. 计算输出值y_hat
    2. 更新权重。

这里,输出值是由我们前面定义的单位步长函数预测的类别标签,权重向量w中每个权重w[j]的同时更新可以更正式地写为:

Artificial neurons – a brief glimpse into the early history of machine learning

用于更新权重w[j]Δw[j]值由感知器学习规则计算得出:

Artificial neurons – a brief glimpse into the early history of machine learning

其中η是学习率(介于 0.0 和 1.0 之间的常数),y^(i)是第i个训练样本的真实类别标签,而y_hat^(i)是预测的类别标签。 重要的是要注意,权重向量中的所有权重都同时更新,这意味着在更新所有权重Δw[j]之前,我们不会重新计算y_hat^(i)。 具体来说,对于 2D 数据集,我们将编写以下更新:

Artificial neurons – a brief glimpse into the early history of machine learning

Artificial neurons – a brief glimpse into the early history of machine learning

Artificial neurons – a brief glimpse into the early history of machine learning

在用 Python 实现感知器规则之前,让我们进行一个简单的思想实验,以说明该学习规则实际上有多么简单。 在感知器正确预测类标签的两种情况下,权重保持不变:

Artificial neurons – a brief glimpse into the early history of machine learning

Artificial neurons – a brief glimpse into the early history of machine learning

但是,在预测错误的情况下,将权重分别推向正目标类或负目标类的方向:

Artificial neurons – a brief glimpse into the early history of machine learning

Artificial neurons – a brief glimpse into the early history of machine learning

为了更好地了解乘法因子x[j]^(i),让我们来看另一个简单的示例,其中:

Artificial neurons – a brief glimpse into the early history of machine learning

假设x[j]^(i) = 0.5,我们将该样本错误分类为-1。 在这种情况下,我们将相应的权重增加 1,这样,下次遇到该样本时,激活x[j]^(i) × w[j]^(i)会更强,因此更有可能超过单位阶跃函数对样本进行分类的阈值 +1:

Artificial neurons – a brief glimpse into the early history of machine learning

权重更新与x[j]^(i)的值成比例。 例如,如果我们有另一个样本x[j]^(i) = 2被错误地分类为-1,我们将更大程度地推动决策边界,以在下一次正确分类该样本:

Artificial neurons – a brief glimpse into the early history of machine learning

重要的是,请注意,只有当两类线性可分离并且学习速率足够小时,才能保证感知器的收敛。 如果两个类别不能由线性决策边界分开,我们可以设置训练数据集的最大通过次数(历元)和/或可容忍的错误分类数量的阈值-感知器 否则永远不会停止更新权重:

Artificial neurons – a brief glimpse into the early history of machine learning

提示

下载示例代码

该课程所有四个部分的代码文件都可以在这个页面上找到。

现在,在进入下一部分的实现之前,让我们在一个简单的图中总结一下我们刚刚学到的东西,该图说明了感知器的一般概念:

Artificial neurons – a brief glimpse into the early history of machine learning

前面的图说明了感知器如何接收样本x的输入并将其与权重w组合以计算净输入。 然后,将净输入传递到激活函数(此处为单位阶跃函数),该函数生成二进制输出-1 或+1(样本的预测类别标签)。 在学习阶段,此输出用于计算预测误差并更新权重。

在 Python 中实现感知器学习算法

在的上一节中,我们了解了 Rosenblatt 的感知器规则如何工作; 现在让我们继续在 Python 中实现它,并将其应用于在第 1 章,“赋予计算机从数据中学习能力的虹膜数据集”中。 我们将采用面向对象的方法将感知器接口定义为 Python Class,这使我们可以初始化可以通过fit方法从数据中学习的新感知器对象,并通过单独的predict方法进行预测 。 按照惯例,我们在对象初始化时未创建的属性上添加下划线,而是通过调用对象的其他方法(例如self.w_)来创建。

注意

如果您还不熟悉,但还不熟悉 Python 的科学库或需要复习,请参阅以下资源:

NumPy

Pandas

Matplotlib

另外,为了更好地和遵循代码示例,我建议您从 Packt 网站下载 IPython 笔记本。 有关 IPython 笔记本的一般介绍,请访问这个页面

import numpy as np    
class Perceptron(object):
    """Perceptron classifier.

    Parameters
    ------------
    eta : float
        Learning rate (between 0.0 and 1.0)
    n_iter : int
        Passes over the training dataset.

    Attributes
    -----------
    w_ : 1d-array
        Weights after fitting.
    errors_ : list
        Number of misclassifications in every epoch.

    """
    def __init__(self, eta=0.01, n_iter=10):
        self.eta = eta
        self.n_iter = n_iter

    def fit(self, X, y):
        """Fit training data.

        Parameters
        ----------
        X : {array-like}, shape = [n_samples, n_features]
            Training vectors, where n_samples 
            is the number of samples and
            n_features is the number of features.
        y : array-like, shape = [n_samples]
            Target values.

        Returns
        -------
        self : object

        """
        self.w_ = np.zeros(1 + X.shape[1])
        self.errors_ = []

        for _ in range(self.n_iter):
            errors = 0
            for xi, target in zip(X, y):
                update = self.eta * (target - self.predict(xi))
                self.w_[1:] += update * xi
                self.w_[0] += update
                errors += int(update != 0.0)
            self.errors_.append(errors)
        return self

    def net_input(self, X):
        """Calculate net input"""
        return np.dot(X, self.w_[1:]) + self.w_[0]

    def predict(self, X):
        """Return class label after unit step"""
        return np.where(self.net_input(X) >= 0.0, 1, -1)

使用此感知器实现,我们现在可以以给定的学习率etan_iter来初始化新的Perceptron对象,该学习率是历元数(经过训练集)。 通过fit方法,我们将self.w_中的权重初始化为零向量R^(m + 1),其中m代表数据集中的维数(特征)数,其中我们为零权重添加 1(即 , 门槛)。

注意

一维数组的 NumPy 索引与使用方括号([])表示法的 Python 列表相似。 对于二维数组,第一个索引器引用行号,第二个索引器引用列号。 例如,我们将使用X[2, 3]选择 2D 数组X的第三行和第四列。

权重初始化后,fit方法循环遍历训练集中的所有单个样本,并根据我们在上一节中讨论的感知器学习规则更新权重。 类别标签是通过predict方法预测的,在fit方法中也称为预测重量更新的类别标签,但在我们使用predict预测新数据的类别标签后, 拟合了我们的模型。 此外,我们还在列表self.errors_中的每个时期收集了错误分类的数量,以便稍后我们可以分析感知器在训练过程中的表现。 net_input方法中使用的np.dot函数仅计算向量点积w^T x

注意

代替使用 NumPy 通过a.dot(b)np.dot(a, b)计算两个数组 a 和 b 之间的向量点积,我们还可以通过sum([i*j for i,j in zip(a, b)]在纯 Python 中执行计算。 但是,与传统的 Python for 循环结构相比,使用 NumPy 的优势在于其算术运算是向量化的。 向量化 表示元素算术运算自动应用于数组中的所有元素。 通过将算术运算表示为数组上的指令序列,而不是一次对每个元素执行一组运算,我们可以更好地利用具有单指令,多数据的现代 CPU 体系结构 (SIMD)支持。 此外,NumPy 使用高度优化的线性代数库,例如基本线性代数子程序BLAS)和线性代数程序包LAPACK) 用 C 或 Fortran 编写的代码。 最后,NumPy 还允许我们使用线性代数的基础,例如向量和矩阵点积,以更紧凑和直观的方式编写代码。

在虹膜数据集上训练感知器模型

为了测试我们的感知器实现,我们将从 Iris 数据集中加载两个花类 SetosaVersicolor。 尽管感知器规则不限于二维,但出于可视化目的,我们仅考虑两个特征萼片长度花瓣长度。 同样,出于实际原因,我们只选择了两种花类 SetosaVersicolor。 但是,感知器算法可以扩展到多类分类,例如,通过一对全技术。

注意

一对全OvA),或有时也称为一对一静态OvR) 是用于将的二进制分类器扩展到多类问题的技术。 使用 OvA,我们可以为每个类别训练一个分类器,其中将特定类别视为阳性类别,而将所有其他类别的样本视为阴性类别。 如果要对新数据样本进行分类,则将使用n分类器,其中n是类别标签的数量,然后将具有最高置信度的类别标签分配给特定样本。 对于感知器,我们将使用 OvA 选择与最大绝对净输入值关联的类别标签。

首先,我们将使用Pandas库将[IRIS 数据集]直接从 UCI 机器学习存储库加载到DataFrame对象中,并通过[ tail方法来检查数据是否正确加载:

>>> import pandas as pd
>>> df = pd.read_csv('https://archive.ics.uci.edu/ml/'
...   'machine-learning-databases/iris/iris.data', header=None)
>>> df.tail()

Training a perceptron model on the Iris dataset

接下来,我们分别提取对应于 50 朵鸢尾花和 50 朵鸢尾花花的前 100 个类别标签,并将类别标签转换为两个整数类别标签[ 我们分配给向量y的 HTG0]( Versicolor )和-1Setosa ),其中 PandasDataFrame的值方法产生相应的 NumPy 表示形式。 同样,我们提取这 100 个训练样本的第一特征列(萼片长度)和第三特征列(花瓣长度),并将它们分配给特征矩阵X, 我们可以通过二维散点图进行可视化:

>>> import matplotlib.pyplot as plt
>>> import numpy as np

>>> y = df.iloc[0:100, 4].values
>>> y = np.where(y == 'Iris-setosa', -1, 1)
>>> X = df.iloc[0:100, [0, 2]].values
>>> plt.scatter(X[:50, 0], X[:50, 1],
...             color='red', marker='o', label='setosa')
>>> plt.scatter(X[50:100, 0], X[50:100, 1],
...             color='blue', marker='x', label='versicolor')
>>> plt.xlabel('sepal length')
>>> plt.ylabel('petal length')
>>> plt.legend(loc='upper left')
>>> plt.show()

执行前面的代码示例之后,我们现在应该看到以下散点图:

Training a perceptron model on the Iris dataset

现在是时候在刚刚提取的 Iris 数据子集上训练感知器算法了。 此外,我们将为每个时期绘制misclassification error,以检查算法是否收敛,并找到将两个鸢尾花类分开的决策边界:

>>> ppn = Perceptron(eta=0.1, n_iter=10)
>>> ppn.fit(X, y)
>>> plt.plot(range(1, len(ppn.errors_) + 1), ppn.errors_, 
...         marker='o')
>>> plt.xlabel('Epochs')
>>> plt.ylabel('Number of misclassifications')
>>> plt.show()

执行完前面的代码后,我们应该看到错误分类错误与时期数的关系图,如下所示:

Training a perceptron model on the Iris dataset

如上图所示,我们的感知器在第六个时期之后已经收敛,现在应该能够对训练样本进行完美分类。 让我们实现一个小的便利函数,以可视化 2D 数据集的决策边界:

from matplotlib.colors import ListedColormap

def plot_decision_regions(X, y, classifier, resolution=0.02):

 # setup marker generator and color map
 markers = ('s', 'x', 'o', '^', 'v')
 colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
 cmap = ListedColormap(colors[:len(np.unique(y))])

 # plot the decision surface
 x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
 x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
 xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
 np.arange(x2_min, x2_max, resolution))
 Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
 Z = Z.reshape(xx1.shape)
 plt.contourf(xx1, xx2, Z, alpha=0.4, cmap=cmap)
 plt.xlim(xx1.min(), xx1.max())
 plt.ylim(xx2.min(), xx2.max())

 # plot class samples
 for idx, cl in enumerate(np.unique(y)):
 plt.scatter(x=X[y == cl, 0], y=X[y == cl, 1],
 alpha=0.8, c=cmap(idx),
 marker=markers[idx], label=cl)

首先,我们定义多个colorsmarkers,然后通过ListedColormap从颜色列表中创建一个颜色图。 然后,我们确定两个特征的最小值和最大值,并使用这些特征向量通过 NumPy meshgrid函数创建一对网格阵列xx1xx2。 由于我们在两个特征维度上训练了感知器分类器,因此我们需要展平栅格阵列并创建一个矩阵,该矩阵具有与 Iris 训练子集相同的列数,以便我们可以使用predict方法来预测类标签[ Z的相应网格点。 在将预测的类别标签Z重塑为具有与xx1xx2相同尺寸的网格后,我们现在可以通过 matplotlib 的contourf函数绘制等高线图,该函数将不同的决策区域映射到每种预测的不同颜色 网格数组中的类:

>>> plot_decision_regions(X, y, classifier=ppn)
>>> plt.xlabel('sepal length [cm]')
>>> plt.ylabel('petal length [cm]')
>>> plt.legend(loc='upper left')
>>> plt.show()

执行完前面的代码示例后,我们现在应该看到决策区域的图,如下图所示:

Training a perceptron model on the Iris dataset

正如我们在中看到的那样,感知器学习了一个决策边界,该边界能够完美地对虹膜训练子集中的所有花朵样本进行分类。

注意

尽管感知器对两个鸢尾花类别进行了完美分类,但是收敛是感知器最大的问题之一。 弗兰克·罗森布拉特(Frank Rosenblatt)在数学上证明,如果可以通过线性超平面将这两类分开,则感知器学习规则会收敛。 但是,如果无法通过这样的线性决策边界将类完美地分开,则权重将永远不会停止更新,除非我们设置最大时期数。

Training a perceptron model on the Iris dataset

自适应线性神经元与学习的收敛

在本节中,我们将看一下另一种类型的单层神经网络:自适应线性神经网络Adaline)。 伯纳德·威德罗(Bernard Widrow)和他的博士生特德·霍夫(Tedd Hoff)在弗兰克·罗森布拉特(Frank Rosenblatt)的感知器算法发布仅几年后,就发布了 Adaline ,可以认为是对后者的改进(B. Widrow 等人,自适应*“ Adaline 使用化学“介导剂”的神经元*。编号技术报告 1553-2。StanfordElectron。Labs。Stanford,CA,1960 年 10 月)。 Adaline 算法特别有趣,因为它说明了定义和最小化成本函数的关键概念,这将为理解更高级的机器学习分类算法(例如逻辑回归和支持向量机以及我们所使用的回归模型)奠定基础 将在以后的章节中讨论。

Adaline 规则(也称为 Widrow-Hoff 规则)和 Rosenblatt 感知器之间的主要区别是权重是根据线性激活函数而不是像 感知器。 在 Adaline 中,此线性激活函数φ(z)只是网络输入的身份函数,因此φ(w^T x) = w^T x

使用线性激活函数来学习权重时,类似于我们之前看到的单位步长函数的量化器可以用于预测类别标签,如下所示 数字:

Adaptive linear neurons and the convergence of learning

如果将上图与之前看到的感知器算法的图示进行比较,则不同之处在于我们知道使用线性激活函数的连续值输出来计算模型误差并更新权重,而不是二进制类 标签。

通过梯度下降最小化成本函数

监督机器学习算法的关键要素之一是定义一个目标函数,该目标函数将在学习过程中进行优化。 这个目标函数通常是我们要最小化的成本函数。 对于 Adaline,我们可以定义成本函数J,以将权重定义为计算结果和真实类别标签之间的平方误差总和SSE)。

Minimizing cost functions with gradient descent

为方便起见,仅添加了1/2一词; 如下面的段落所示,它将使推导梯度变得更加容易。 与单位步长函数相比,此连续线性激活函数的主要优点是成本函数变得可微。 此成本函数的另一个不错的特性是它是凸的。 因此,我们可以使用称为梯度下降的简单但功能强大的优化算法来找到权重,以最小化我们的成本函数来对 Iris 数据集中的样本进行分类。

如下图所示,我们可以将梯度下降的原理描述为爬下山直到达到局部或全局最低成本。 在每次迭代中,我们都从梯度上走了一步,其中步长由学习率的值以及梯度的斜率确定:

Minimizing cost functions with gradient descent

现在,通过使用梯度下降,我们可以通过远离成本函数J(w)的梯度∇J(w)来更新权重:

Minimizing cost functions with gradient descent

这里,权重变化Δw定义为负梯度乘以学习率η

Minimizing cost functions with gradient descent

要计算成本函数的梯度,我们需要针对每个权重w[j]和:

Minimizing cost functions with gradient descent

计算成本函数的偏导数,以便将权重w[j]的更新写为:

Minimizing cost functions with gradient descent

由于我们同时更新所有权重,因此我们的 Adaline 学习规则变为w := w + Δw

注意

对于那些熟悉微积分的人,可以通过以下方法获得相对于jth 权重的 SSE 成本函数的偏导数:

Minimizing cost functions with gradient descent

Minimizing cost functions with gradient descent

Minimizing cost functions with gradient descent

Minimizing cost functions with gradient descent

Minimizing cost functions with gradient descent

Minimizing cost functions with gradient descent

尽管 Adaline 学习规则看起来与感知器规则相同,但是z^(i) = w^T x^(i)φ(z^(i))是实数,而不是整数类标签。 此外,权重更新是基于训练集中的所有样本计算的(而不是在每个样本之后递增地更新权重),这就是为什么此方法也称为“批量”梯度下降的原因。

在 Python 中实现自适应线性神经元

由于的感知器规则和 Adaline 非常相似,因此我们将采用我们先前定义的感知器实现,并更改fit方法,以便通过梯度下降最小化成本函数来更新权重:

class AdalineGD(object):
    """ADAptive LInear NEuron classifier.

    Parameters
    ------------
    eta : float
        Learning rate (between 0.0 and 1.0)
    n_iter : int
        Passes over the training dataset.

    Attributes
    -----------
    w_ : 1d-array
        Weights after fitting.
    errors_ : list
        Number of misclassifications in every epoch.

    """
    def __init__(self, eta=0.01, n_iter=50):
        self.eta = eta
        self.n_iter = n_iter

    def fit(self, X, y):
        """ Fit training data.

        Parameters
        ----------
        X : {array-like}, shape = [n_samples, n_features]
            Training vectors, 
            where n_samples is the number of samples and
            n_features is the number of features.
        y : array-like, shape = [n_samples]
            Target values.

        Returns
        -------
        self : object

        """
        self.w_ = np.zeros(1 + X.shape[1])
        self.cost_ = []

        for i in range(self.n_iter):
            output = self.net_input(X)
            errors = (y - output)
            self.w_[1:] += self.eta * X.T.dot(errors)
            self.w_[0] += self.eta * errors.sum()
            cost = (errors**2).sum() / 2.0
            self.cost_.append(cost)
        return self

    def net_input(self, X):
        """Calculate net input"""
        return np.dot(X, self.w_[1:]) + self.w_[0]

    def activation(self, X):
        """Compute linear activation"""
        return self.net_input(X)

    def predict(self, X):
        """Return class label after unit step"""
        return np.where(self.activation(X) >= 0.0, 1, -1)

而不是像在感知器中那样在评估每个单独的训练样本之后更新权重,我们基于整个训练数据集通过零重量的self.eta * errors.sum()和权重 1 至mself.eta * X.T.dot(errors)计算梯度。 X.T.dot(errors)是我们的特征矩阵与误差向量之间的矩阵向量乘法。 与先前的感知器实现类似,我们在列表self.cost_中收集成本值,以检查算法在训练后是否收敛。

注意

执行矩阵向量乘法类似于计算向量点积,其中矩阵中的每一行都被视为单行向量。 此向量化方法表示更紧凑的符号,并使用 NumPy 进行更有效的计算。 例如:

Implementing an Adaptive Linear Neuron in Python

在实践中,通常需要进行一些实验才能找到良好的学习率η,以实现最佳收敛。 因此,让我们选择两种不同的学习率η = 0.1η = 0.0001作为起点,并绘制成本函数与时期数的关系图,以了解 Adaline 实施从训练数据中学习得如何。

注意

学习率η以及时期数n_iter是感知器和 Adaline 学习算法的所谓超参数。 在第 4 章,“建立良好的训练集-数据预处理”中,我们将介绍不同的技术来自动查找产生分类模型最佳性能的不同超参数的值。

现在,让我们针对两种不同学习率的时间数绘制成本:

>>> fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(8, 4))
>>> ada1 = AdalineGD(n_iter=10, eta=0.01).fit(X, y)
>>> ax[0].plot(range(1, len(ada1.cost_) + 1),
...            np.log10(ada1.cost_), marker='o')
>>> ax[0].set_xlabel('Epochs')
>>> ax[0].set_ylabel('log(Sum-squared-error)')
>>> ax[0].set_title('Adaline - Learning rate 0.01')
>>> ada2 = AdalineGD(n_iter=10, eta=0.0001).fit(X, y)
>>> ax[1].plot(range(1, len(ada2.cost_) + 1),
...            ada2.cost_, marker='o')
>>> ax[1].set_xlabel('Epochs')
>>> ax[1].set_ylabel('Sum-squared-error')
>>> ax[1].set_title('Adaline - Learning rate 0.0001')
>>> plt.show()

正如我们在中看到的,接下来生成的成本函数图所示,我们遇到了两种不同类型的问题。 左图显示了如果我们选择一个太大的学习率会发生什么—而不是最小化成本函数,在每个时期误差都会变得更大,因为我们超过了全局最小值:

Implementing an Adaptive Linear Neuron in Python

尽管我们看到正确的绘图可以看到成本降低了,但是所选的学习速率η = 0.0001很小,以至于算法需要大量的时间才能收敛。 下图说明了如何更改特定权重参数的值以最小化成本函数J(左子图)。 右侧的子图说明了如果我们选择的学习率太大而超出全局最小值,则会发生什么情况:

Implementing an Adaptive Linear Neuron in Python

我们将在本书中遇到的许多机器学习算法都需要某种特征缩放以实现最佳性能,我们将在第 3 章,“使用 Scikit 学习的机器学习分类器导论”中对此进行详细讨论。 。 梯度下降是受益于特征缩放的众多算法之一。 在这里,我们将使用称为标准化的特征缩放方法,该方法使我们的数据具有标准正态分布的属性。 每个特征的均值以 0 为中心,特征列的标准偏差为 1。例如,要标准化第j个特征,我们只需要从每个训练样本中减去样本均值μ[j]并除以 通过其标准偏差σ[j]

Implementing an Adaptive Linear Neuron in Python

此处,x[j]是由所有训练样本n的第j个特征值组成的向量。

使用 NumPy 方法meanstd可以轻松实现标准化:

>>> X_std = np.copy(X)
>>> X_std[:,0] = (X[:,0] - X[:,0].mean()) / X[:,0].std()
>>> X_std[:,1] = (X[:,1] - X[:,1].mean()) / X[:,1].std()

标准化之后,我们将再次训练 Adaline,并看到它现在以学习率η = 0.01收敛:

>>> ada = AdalineGD(n_iter=15, eta=0.01)
>>> ada.fit(X_std, y)
>>> plot_decision_regions(X_std, y, classifier=ada)
>>> plt.title('Adaline - Gradient Descent')
>>> plt.xlabel('sepal length [standardized]')
>>> plt.ylabel('petal length [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()
>>> plt.plot(range(1, len(ada.cost_) + 1), ada.cost_, marker='o')
>>> plt.xlabel('Epochs')
>>> plt.ylabel('Sum-squared-error')
>>> plt.show()

执行完前面的代码后,我们应该看到决策区域图以及下降成本图,如下图所示:

Implementing an Adaptive Linear Neuron in Python

正如我们在前面的图中所看到的,Adaline 现在在使用学习率η = 0.01训练了标准化特征之后收敛。 但是,请注意,即使所有样本均已正确分类,SSE 仍为非零。

大规模机器学习和随机梯度下降

在上一节中,我们学习了如何通过从整个训练集中计算出的梯度的相反方向上走一步来最小化成本函数。 这就是为什么这种方法有时也称为批次梯度下降的原因。 现在想象一下,我们有一个非常庞大的数据集,其中包含数百万个数据点,这在许多机器学习应用中并不罕见。 在这种情况下,运行批次梯度下降在计算上可能会非常昂贵,因为每次我们向全局最小值迈出一步时,我们都需要重新评估整个训练数据集。

批量梯度下降算法的一种流行替代方法是随机梯度下降,有时也称为迭代在线梯度下降。 代替基于所有样本的累积误差之和来更新权重x^(i)

Large scale machine learning and stochastic gradient descent

我们为每个训练样本逐步更新权重:

Large scale machine learning and stochastic gradient descent

尽管可以将随机梯度下降视为梯度下降的近似值,但由于更频繁的权重更新,它通常更快地达到收敛。 由于每个梯度是基于单个训练示例计算的,因此误差表面的噪声比梯度下降的噪声大,这还具有以下优点:随机梯度下降可以更容易地逃脱浅层局部最小值。 为了通过随机梯度下降获得准确的结果,将数据随机显示是很重要的,这就是为什么我们要为每个时期改组训练集以防止周期。

注意

在随机梯度下降实现中,固定学习率η通常由随时间降低的自适应学习率代替,例如c1 / (n_iters + c2),其中c1c2是常数。 请注意,随机梯度下降未达到全局最小值,而是一个非常接近全局最小值的区域。 通过使用自适应学习率,我们可以进一步退火到更好的全局最小值

随机梯度下降的另一个优点是我们可以将其用于在线学习。 在在线学习中,随着新训练数据的到来,我们的模型将得到即时训练。 如果我们要积累大量数据(例如,典型 Web 应用中的客户数据),这将特别有用。 如果存在存储空间问题,则通过在线学习,系统可以立即适应变化,并且在更新模型后可以丢弃训练数据。

注意

批量梯度下降与随机梯度下降之间的折衷是所谓的微型批量学习。 迷你批量学习可以理解为将批量梯度下降应用于训练数据的较小子集,例如一次 50 个样本。 相对于批次梯度下降的优势在于,由于更频繁的重量更新,通过小批量可以更快地达到收敛。 此外,小批量学习允许我们通过向量化运算来替换随机梯度下降SGD)中训练样本的 for 循环,这可以进一步提高我们的计算效率 学习算法。

由于我们已经使用梯度下降实现了 Adaline 学习规则,因此我们仅需进行一些调整即可修改学习算法,以通过随机梯度下降来更新权重。 在fit方法中,我们现在将在每个训练样本之后更新权重。 此外,我们将实现在线学习的附加partial_fit方法,该方法不会重新初始化权重。 为了检查训练后算法是否收敛,我们将成本计算为每个时期训练样本的平均成本。 此外,我们将在每个时期之前向shuffle训练数据添加一个选项,以避免在优化成本函数时出现周期; 通过random_state参数,我们可以指定随机种子的一致性:

from numpy.random import seed

class AdalineSGD(object):
    """ADAptive LInear NEuron classifier.

    Parameters
    ------------
    eta : float
        Learning rate (between 0.0 and 1.0)
    n_iter : int
        Passes over the training dataset.

    Attributes
    -----------
    w_ : 1d-array
        Weights after fitting.
    errors_ : list
        Number of misclassifications in every epoch.
    shuffle : bool (default: True)
        Shuffles training data every epoch 
        if True to prevent cycles.
    random_state : int (default: None)
        Set random state for shuffling 
        and initializing the weights.

    """
    def __init__(self, eta=0.01, n_iter=10, 
               shuffle=True, random_state=None):
        self.eta = eta
        self.n_iter = n_iter
        self.w_initialized = False
        self.shuffle = shuffle
        if random_state:
            seed(random_state)

    def fit(self, X, y):
        """ Fit training data.

        Parameters
        ----------
        X : {array-like}, shape = [n_samples, n_features]
            Training vectors, where n_samples 
            is the number of samples and
            n_features is the number of features.
        y : array-like, shape = [n_samples]
            Target values.

        Returns
        -------
        self : object

         """
        self._initialize_weights(X.shape[1])
        self.cost_ = []
        for i in range(self.n_iter):
            if self.shuffle:
                X, y = self._shuffle(X, y)
            cost = []
            for xi, target in zip(X, y):
                cost.append(self._update_weights(xi, target))
            avg_cost = sum(cost)/len(y)
            self.cost_.append(avg_cost)
        return self

    def partial_fit(self, X, y):
        """Fit training data without reinitializing the weights"""
        if not self.w_initialized:
            self._initialize_weights(X.shape[1])
        if y.ravel().shape[0] > 1:
            for xi, target in zip(X, y):
                self._update_weights(xi, target)
        else:
            self._update_weights(X, y)
        return self

    def _shuffle(self, X, y):
        """Shuffle training data"""
        r = np.random.permutation(len(y))
        return X[r], y[r]

    def _initialize_weights(self, m):
        """Initialize weights to zeros"""
        self.w_ = np.zeros(1 + m)
        self.w_initialized = True

    def _update_weights(self, xi, target):
        """Apply Adaline learning rule to update the weights"""
        output = self.net_input(xi)
        error = (target - output)
        self.w_[1:] += self.eta * xi.dot(error)
        self.w_[0] += self.eta * error
        cost = 0.5 * error**2
        return cost

    def net_input(self, X):
        """Calculate net input"""
        return np.dot(X, self.w_[1:]) + self.w_[0]

    def activation(self, X):
        """Compute linear activation"""
        return self.net_input(X)

    def predict(self, X):
        """Return class label after unit step"""
        return np.where(self.activation(X) >= 0.0, 1, -1)

我们现在在AdalineSGD分类器中使用的_shuffle方法的工作方式如下:通过numpy.random中的permutation函数,我们生成一个随机数的唯一数字,范围为 0 至 100.这些数字然后可以用作索引,以改组我们的特征矩阵和类标签向量。

然后,我们可以使用 fit 方法来训练AdalineSGD分类器,并使用我们的plot_decision_regions绘制训练结果:

>>> ada = AdalineSGD(n_iter=15, eta=0.01, random_state=1)
>>> ada.fit(X_std, y)
>>> plot_decision_regions(X_std, y, classifier=ada)
>>> plt.title('Adaline - Stochastic Gradient Descent')
>>> plt.xlabel('sepal length [standardized]')
>>> plt.ylabel('petal length [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()
>>> plt.plot(range(1, len(ada.cost_) + 1), ada.cost_, marker='o')
>>> plt.xlabel('Epochs')
>>> plt.ylabel('Average Cost')
>>> plt.show()

下图显示了我们通过执行前面的代码示例获得的的两个图:

Large scale machine learning and stochastic gradient descent

如我们所见,的平均成本下降得很快,而且 15 个纪元后的最终决策边界看起来与 Adaline 的批次梯度下降相似。 如果要更新模型(例如,在具有流数据的在线学习场景中),我们可以简单地对单个样本(例如ada.partial_fit(X_std[0, :], y[0]))调用partial_fit方法。

Large scale machine learning and stochastic gradient descent

Large scale machine learning and stochastic gradient descent

Large scale machine learning and stochastic gradient descent

三十七、scikit-learn 机器学习分类器之旅

在本章中,我们将浏览学术界和行业中常用的流行且功能强大的机器学习算法。 在了解几种分类监督学习算法之间的差异的同时,我们还将对它们各自的优缺点进行直观的了解。 此外,我们将通过 scikit-learn 库迈出第一步,该库提供了一个用户友好的界面,可有效,高效地使用这些算法。

我们将在本章中学习的主题如下:

  • 流行分类算法概念介绍
  • 使用 scikit-learn 机器学习库
  • 选择机器学习算法时要问的问题

选择分类算法

为特定问题任务选择合适的分类算法需要实践:每种算法都有自己的怪癖,并基于某些假设。 重申“无免费午餐”定理:在所有可能的情况下,没有一个分类器最有效。 在实践中,始终建议您比较至少几种不同学习算法的性能,以针对特定问题选择最佳模型。 这些可能在特征或样本的数量,数据集中的噪声量以及类别是否可线性分离方面有所不同。

最终,分类器的性能,计算能力以及预测能力在很大程度上取决于可用于学习的基础数据。 训练机器学习算法所涉及的五个主要步骤可以总结如下:

  1. 功能选择。
  2. 选择性能指标。
  3. 选择分类器和优化算法。
  4. 评估模型的性能。
  5. 调整算法。

由于本书的方法是逐步建立机器学习知识,因此在本章中,我们将主要关注不同算法的主要概念,并重新讨论诸如功能选择和预处理,性能指标以及超参数调整之类的主题。 有关本书后面的更详细的讨论。

scikit-learn 的第一步

在第 2 章和“训练机器学习分类算法”中,您了解了两种相关的学习分类算法:感知器 规则和[ Adaline,我们自己在 Python 中实现。 现在我们来看看 scikit-learn API,它结合了用户友好的界面和高度优化的几种分类算法的实现。 但是,scikit-learn 库不仅提供了多种学习算法,还提供了许多方便的功能来预处理数据以及微调和评估我们的模型。 我们将在第 4 章,“建立良好的训练集–数据预处理”和第 5 章,“通过降维压缩数据”中更详细地讨论这些基本概念。

通过 scikit-learn 训练感知器

为了开始使用 scikit-learn 库,我们将训练一个感知器模型,该模型类似于在第 2 章,“训练分类机器学习算法”中实现的模型。 为简单起见,在以下各节中,我们将使用已经熟悉的 Iris 数据集。 方便地,Iris 数据集已经可以通过 scikit-learn 获得,因为它是一个简单而流行的数据集,通常用于测试和试验算法。 另外,我们仅将鸢尾花数据集中的两个功能用于可视化目的。

我们将 150 个花朵样本的花瓣长度花瓣宽度 分配给特征矩阵X和花朵种类的相应类别标签 向量y

>>> from sklearn import datasets
>>> import numpy as np
>>> iris = datasets.load_iris()
>>> X = iris.data[:, [2, 3]]
>>> y = iris.target

如果执行np.unique(y)返回存储在iris.target中的不同类别标签,则会看到鸢尾花的类别名称,鸢尾花-Setosa鸢尾花Iris-Virginica 已作为存储为整数(012),为许多机器学习库的最佳性能而推荐使用。

为了评估训练好的模型在看不见的数据上的效果,我们将进一步将数据集拆分为单独的训练和测试数据集。 在第 5 章和“通过降维压缩”压缩数据中,我们将更详细地讨论围绕模型评估的最佳实践:

>>> from sklearn.cross_validation import train_test_split
>>> X_train, X_test, y_train, y_test = train_test_split(
...           X, y, test_size=0.3, random_state=0)

使用 scikit-learn 的cross_validation模块中的train_test_split函数,我们将Xy阵列随机分为 30%的测试数据(45 个样本)和 70%的训练数据(105 个样本)。

正如我们从第 2 章,“训练分类的机器学习算法”中的梯度下降示例中所记得的那样,许多机器学习和优化算法也需要特征缩放以实现最佳性能。在这里,我们将使用 scikit-learn 的preprocessing模块中的StandardScaler类对功能进行标准化:

>>> from sklearn.preprocessing import StandardScaler
>>> sc = StandardScaler()
>>> sc.fit(X_train)
>>> X_train_std = sc.transform(X_train)
>>> X_test_std = sc.transform(X_test)

使用前面的代码,我们从预处理模块中加载了StandardScaler类,并初始化了一个新的StandardScaler对象,该对象已分配给变量scStandardScaler使用fit方法,从训练数据中估算出每个特征维度的参数μ(样本平均值)和σ(标准偏差)。 通过调用transform方法,我们然后使用那些估计的参数μσ标准化训练数据。 请注意,我们使用相同的缩放参数来标准化测试集,以使训练数据集和测试数据集中的值彼此可比。

标准化训练数据后,我们现在可以训练感知器模型。 默认情况下,scikit-learn 中的大多数算法已通过单项相对于其余OvR)方法支持多类分类,这使我们可以将三个花类馈入 感知器一次全部。 代码如下:

>>> from sklearn.linear_model import Perceptron
>>> ppn = Perceptron(n_iter=40, eta0=0.1, random_state=0)
>>> ppn.fit(X_train_std, y_train)

scikit-learn 接口使我们想起第 2 章,“训练机器学习算法”的感知器实现:从linear_model模块加载Perceptron类后 ,我们初始化了一个新的Perceptron对象,并通过fit方法训练了模型。 在这里,模型参数eta0等于我们在自己的感知器实现中使用的学习率eta,参数n_iter定义了历元数(经过训练集)。 我们从第 2 章和“分类训练机器学习算法”记得,找到合适的学习率需要做一些实验。 如果学习率太大,该算法将超出全局最小成本。 如果学习速率太小,则该算法需要更多的时间才能收敛,这可能会使学习变慢,尤其是对于大型数据集。 同样,我们使用random_state参数来实现每个时期后训练数据集的初始改组的可重复性。

在 scikit-learn 中训练了模型之后,我们可以通过predict方法进行预测,就像在第 2 章,“训练分类机器学习算法”中我们自己的感知器实现中一样。 代码如下:

>>> y_pred = ppn.predict(X_test_std)
>>> print('Misclassified samples: %d' % (y_test != y_pred).sum())
Misclassified samples: 4

在执行前面的代码时,我们看到感知器对 45 个花样本中的 4 个进行了错误分类。 因此,测试数据集上的错误分类错误为 0.089 或 8.9%(4/45 ≈ 0.089)。

注意

代替错误分类错误,许多机器学习从业者报告了模型的分类准确性,其计算方法如下:

1-错误分类错误 = 0.911 或 91.1%。

Scikit-learn 还实现了可通过metrics模块获得的各种不同的性能指标。 例如,我们可以如下计算感知器在测试集上的分类精度:

>>> from sklearn.metrics import accuracy_score
>>> print('Accuracy: %.2f' % accuracy_score(y_test, y_pred))
0.91

这里,y_test是真实类别标签,y_pred是我们之前预测的类别标签。

注意

请注意,我们根据本章中的测试集评估模型的性能。 在第 5 章,“通过降维压缩数据”中,您将学习有用的技术,包括图形分析(例如学习曲线),以检测和防止过拟合。 过度拟合意味着该模型很好地捕获了训练数据中的模式,但是无法很好地概括为看不见的数据。

最后,我们可以使用第 2 章和“分类训练机器学习算法”的plot_decision_regions函数,绘制新的决策区域的图 训练好的感知器模型,并可视化其如何分离不同的花朵样本。 但是,我们添加一个小的修改以通过小圆圈突出显示来自测试数据集的样本:

from matplotlib.colors import ListedColormap
import matplotlib.pyplot as plt

def plot_decision_regions(X, y, classifier, 
                    test_idx=None, resolution=0.02):

    # setup marker generator and color map
    markers = ('s', 'x', 'o', '^', 'v')
    colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
    cmap = ListedColormap(colors[:len(np.unique(y))])

    # plot the decision surface
    x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
                         np.arange(x2_min, x2_max, resolution))
    Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
    Z = Z.reshape(xx1.shape)
    plt.contourf(xx1, xx2, Z, alpha=0.4, cmap=cmap)
    plt.xlim(xx1.min(), xx1.max())
    plt.ylim(xx2.min(), xx2.max())

    # plot all samples
    for idx, cl in enumerate(np.unique(y)):
        plt.scatter(x=X[y == cl, 0], y=X[y == cl, 1],
                    alpha=0.8, c=cmap(idx),
                    marker=markers[idx], label=cl)

 # highlight test samples
 if test_idx:
 X_test, y_test = X[test_idx, :], y[test_idx] 
 plt.scatter(X_test[:, 0], X_test[:, 1], c='', 
 alpha=1.0, linewidths=1, marker='o', 
                s=55, label='test set')

通过对plot_decision_regions函数进行的轻微修改(在前面的代码中已突出显示),我们现在可以指定要在结果图上标记的样本的索引。 代码如下:

>>> X_combined_std = np.vstack((X_train_std, X_test_std))
>>> y_combined = np.hstack((y_train, y_test))
>>> plot_decision_regions(X=X_combined_std, 
...                       y=y_combined, 
...                       classifier=ppn,
...                       test_idx=range(105,150))
>>> plt.xlabel('petal length [standardized]') 
>>> plt.ylabel('petal width [standardized]') 
>>> plt.legend(loc='upper left')
>>> plt.show()

正如我们在结果图中看到的那样,三个花类不能通过线性决策边界完美地分开:

Training a perceptron via scikit-learn

我们从第 2 章和“训练机器学习分类算法”的讨论中还记得,感知器算法从未收敛于不能完全线性分离的数据集,这就是为什么使用 实践中通常不建议使用感知器算法。 在以下各节中,我们将介绍功能更强大的线性分类器,即使这些类不是完全线性可分离的,它们也会收敛到最低成本。

注意

Perceptron以及其他 scikit-learn 函数和类具有其他参数,为清楚起见,我们将其省略。 您可以使用 Python 中的help函数(例如help(Perceptron))或阅读出色的 scikit-learn 在线文档,以了解有关这些参数的更多信息。

Training a perceptron via scikit-learn

通过逻辑回归建模类概率

尽管感知器规则为机器学习算法分类提供了很好而又轻松的介绍,但其最大的缺点是,如果类不是完全线性可分离的,则它永远不会收敛。 上一节中的分类任务将是这种情况的一个示例。 凭直觉,我们可以认为原因是权重不断更新,因为每个时期始终至少存在一个错误分类的样本。 当然,您可以更改学习率并增加时期数,但要注意,感知器将永远不会收敛于该数据集。 为了更好地利用我们的时间,我们现在来看看另一种简单的功能更强大的线性和二进制分类问题算法:Logistic 回归。 请注意,尽管逻辑回归的名称如此,但它是分类的模型,而不是回归的模型。

Logistic 回归直觉和条件概率

Logistic 回归是一种分类模型,非常易于实现,但在线性可分离类上的表现很好。 它是工业上最广泛使用的分类算法之一。 与感知器和 Adaline 相似,本章中的逻辑回归模型也是用于二分类的线性模型,可以通过 OvR 技术将其扩展到多分类。

为了解释逻辑回归作为概率模型的思想,让我们首先介绍几率,这是支持特定事件的几率。 优势比可以写为p / (1 - p),其中p代表阳性事件的概率。 术语阳性事件不一定表示,而是指我们要预测的事件,例如,患者患有某种疾病的概率; 我们可以将积极的事件视为类标签y = 1。 然后,我们可以进一步定义 logit 函数,它只是比值比(log-odds)的对数:

Logistic regression intuition and conditional probabilities

logit 函数采用 0 到 1 范围内的输入值,并将它们转换为整个实数范围内的值,我们可以用来表达特征值和对数奇数之间的线性关系:

Logistic regression intuition and conditional probabilities

在此,p(y = 1 | x)是特定样本由于其特征x而属于类别 1 的条件概率。

现在,我们真正感兴趣的是预测某个样本属于特定类别的概率,这是 logit 函数的逆形式。 它也被称为逻辑函数,由于其特征 S 形,有时有时缩写为 Sigmoid 函数。

Logistic regression intuition and conditional probabilities

此处,z是净输入,即权重和样本特征的线性组合,可以计算为z = w^T x = w[0] + w[1]x[1] + ... + w[m]x[m]

现在,让我们简单地绘制 sigmoid 函数以得到-7 到 7 范围内的某些值,以查看其外观:

>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> def sigmoid(z):
...     return 1.0 / (1.0 + np.exp(-z))
>>> z = np.arange(-7, 7, 0.1)
>>> phi_z = sigmoid(z)
>>> plt.plot(z, phi_z)
>>> plt.axvline(0.0, color='k')
>>> plt.axhspan(0.0, 1.0, facecolor='1.0', alpha=1.0, ls='dotted')
>>> plt.axhline(y=0.5, ls='dotted', color='k')
>>> plt.yticks([0.0, 0.5, 1.0])
>>> plt.ylim(-0.1, 1.1)
>>> plt.xlabel('z')
>>> plt.ylabel('$\phi (z)$')
>>> plt.show() 

由于执行了前面的代码示例,现在我们应该看到 S 形(S 型)曲线:

Logistic regression intuition and conditional probabilities

我们可以看到,如果z趋于无穷大(z -> +∞),则φ(z)接近 1,因为对于z的较大值,exp(-z)变得非常小。 类似地,由于分母越来越大,z -> -∞φ(z)趋于 0。 因此,我们得出的结论是,此 S 型函数将实数值作为输入并将其转换为[0, 1]范围内的值,并在φ(z) = 0.5处进行截距。

要为逻辑回归模型建立一些直觉,我们可以将其与我们先前在第 2 章,“训练机器学习分类算法”中的 Adaline 实现相关。 在 Adaline 中,我们使用身份函数φ(z) = z作为激活函数。 在逻辑回归中,此激活函数简单地变成了我们前面定义的 S 型函数,如下图所示:

Logistic regression intuition and conditional probabilities

然后,将 S 型函数的输出解释为属于类别 1 φ(z) = p(y = 1 | x; w)的特定样本的概率,因为其特征x由权重w参数化。 例如,如果我们为特定的花朵样本计算φ(z) = 0.8,则意味着该样本是鸢尾花-杂色花朵的机会是 80%。 同样,此花是鸢尾-Setosa 花的概率可以计算为p(y = 0 | x; w) = 1 - p(y = 1 | x; w) = 0.2或 20%。 然后,可以通过量化器(单位步长函数)将预测的概率简单地转换为二进制结果:

Logistic regression intuition and conditional probabilities

如果我们查看前面的 S 型曲线,则等效于以下内容:

Logistic regression intuition and conditional probabilities

实际上,在许多应用中,我们不仅对预测的类别标签感兴趣,而且在估计类别成员资格的概率方面特别有用。 Logistic 回归用于天气预报,例如,不仅可以预测特定日子是否下雨,还可以报告下雨的机会。 类似地,逻辑回归可以用于预测患者具有某些症状的特定疾病的机会,这就是为什么逻辑回归在医学领域享有广泛欢迎的原因。

学习物流成本函数的权重

您学习了,如何使用逻辑回归模型预测概率和类别标签。 现在,让我们简要讨论一下模型的参数,例如权重w。 在上一章中,我们定义了平方和误差成本函数:

Learning the weights of the logistic cost function

为了学习 Adaline 分类模型的权重w,我们将其最小化。 为了解释如何得出逻辑回归的成本函数,我们首先定义在构建逻辑回归模型时要最大化的L的可能性,假设数据集中的各个样本为 彼此独立。 计算公式如下:

Learning the weights of the logistic cost function

在实践中,更容易最大化此方程的(自然)对数,这称为对数似然函数:

Learning the weights of the logistic cost function

首先,应用对数函数可减少出现数字下溢的可能性,如果可能性很小,则可能发生这种情况。 其次,我们可以将因子的乘积转换为因子的总和,这可以使您更容易通过加法获得此函数的导数,正如您可能从微积分中还记得的那样。

现在,我们可以使用诸如梯度上升之类的优化算法来最大化此对数似然函数。 或者,让我们将对数似然重写为成本函数J,可以使用梯度下降来将其最小化,如第 2 章和“训练机器学习分类算法”一样:

Learning the weights of the logistic cost function

为了更好地了解这个成本函数,让我们看一下我们为一个单样本实例计算的成本:

Learning the weights of the logistic cost function

查看前面的公式,我们可以看到,如果y = 0,则第一项变为零;如果y = 1,则第二项变为零:

Learning the weights of the logistic cost function

下图显示了φ(z)不同值的单样本实例分类的成本:

Learning the weights of the logistic cost function

如果我们正确地预测样本属于类别 1,我们可以看到成本接近 0(纯蓝色线)。类似地,我们可以在y轴上看到,如果我们正确预测y = 0(虚线)。 但是,如果预测错误,则成本将达到无穷大。 道德是我们会以越来越大的代价惩罚错误的预测。

使用 scikit-learn 训练逻辑回归模型

如果我们自己实现逻辑回归,则可以简单地将成本函数J替换为第 2 章,“训练机器学习分类算法”的 Adaline 实现, 新的成本函数:

Training a logistic regression model with scikit-learn

这将计算出每个时期对所有训练样本进行分类的成本,最终我们将得到一个有效的逻辑回归模型。 但是,由于 scikit-learn 实现了高度逻辑优化的 Logistic 回归版本,该版本还支持现成的多类设置,因此我们将跳过该实现并使用sklearn.linear_model.LogisticRegression类以及熟悉的fit方法来训练模型 在标准化的花卉训练数据集上:

>>> from sklearn.linear_model import LogisticRegression
>>> lr = LogisticRegression(C=1000.0, random_state=0)
>>> lr.fit(X_train_std, y_train)
   >>> plot_decision_regions(X_combined_std, 
...                       y_combined, classifier=lr,
...                       test_idx=range(105,150))
>>> plt.xlabel('petal length [standardized]')
>>> plt.ylabel('petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()

将模型拟合到训练数据上之后,我们绘制了决策区域,训练样本和测试样本,如下所示:

Training a logistic regression model with scikit-learn

看一下我们用来训练LogisticRegression模型的先前代码,您现在可能会想:“这个神秘的参数C是什么?” 我们将在一秒钟内解决这个问题,但让我们先在下一部分中简要讨论过度拟合和正则化的概念。

此外,我们可以通过predict_proba方法预测样本的类成员概率。 例如,我们可以预测第一个 Iris-Setosa 样本的概率:

>>> lr.predict_proba(X_test_std[0,:])

这将返回以下数组:

array([[  0.000,   0.063,   0.937]])

前面的数组告诉我们,该模型预测样本属于 Iris-Virginica 类的机会为 93.7%,样本为 Iris-Versicolor 花的机会为 6.3%。

我们可以证明,通过梯度下降进行逻辑回归的权重更新确实等于在第 2 章和“训练机器学习分类算法”中的 Adaline 中使用的方程式。 让我们开始计算对数j权重的对数似然函数的偏导数:

Training a logistic regression model with scikit-learn

在继续之前,让我们首先计算 S 型函数的偏导数:

Training a logistic regression model with scikit-learn

现在,我们可以用我们的第一个等式替换∂φ(z)/∂z = φ(z)(1 - φ(z)),以获得以下信息:

Training a logistic regression model with scikit-learn

请记住,目标是找到使对数可能性最大化的权重,以便我们对每个权重执行如下更新:

Training a logistic regression model with scikit-learn

由于我们同时更新所有权重,因此我们可以编写以下通用更新规则:

Training a logistic regression model with scikit-learn

我们定义Δw如下:

Training a logistic regression model with scikit-learn

由于最大化对数可能性等于最小化我们先前定义的成本函数J,因此我们可以编写如下的梯度下降更新规则:

Training a logistic regression model with scikit-learn

Training a logistic regression model with scikit-learn

这等于,与第 2 章,“分类训练机器学习算法”中的 Adaline 中的梯度下降规则相同。

通过正则化处理过拟合

过度拟合是在机器学习中的一个常见问题,在该模型中,模型在训练数据上表现良好,但对于未见数据(测试数据)却不能很好地泛化。 如果模型遭受过度拟合,我们也可以说该模型具有很高的方差,这可能是由于参数过多导致给定基础数据的模型过于复杂所致。 同样,我们的模型也可能会遇到拟合不足(高偏差)的问题,这意味着我们的模型不够复杂,无法很好地捕获训练数据中的模式,因此在看不见的情况下也会表现出较低的性能 数据。

尽管到目前为止,我们仅遇到用于分类的线性模型,但是可以通过使用更复杂的非线性决策边界来最好地说明过度拟合和欠拟合的问题,如下图所示:

Tackling overfitting via regularization

注意

如果我们要在训练数据集的不同子集上多次重新训练模型,则方差度量特定样本实例的模型预测的一致性(或可变性)。 我们可以说该模型对训练数据中的随机性敏感。 相反,如果我们在不同的训练数据集上多次重建模型,则偏差通常会衡量预测与正确值的差距。 偏差是系统误差的度量,该误差不是由于随机性引起的。

寻求良好偏差偏差权衡的一种方法是通过正则化调整模型的复杂性。 正则化是处理共线性(要素之间的高度相关性),从数据中滤除噪声并最终防止过度拟合的非常有用的方法。 正则化背后的概念是引入附加信息(偏差)以惩罚极端参数权重。 最常见的正则化形式是所谓的 L2 正则化(有时也称为 L2 收缩或权重衰减),其写法如下:

Tackling overfitting via regularization

在此,λ是所谓的正则化参数。

注意

正则化是为什么诸如标准之类的特征缩放很重要的另一个原因。 为了使正则化正常工作,我们需要确保我们所有的功能都具有可比的规模。

为了应用正则化,我们只需要将正则化项添加到为逻辑回归定义的成本函数中即可缩小权重:

Tackling overfitting via regularization

通过正则化参数λ,我们可以控制拟合数据的拟合程度,同时保持较小的权重。 通过增加λ的值,我们增加了正则化强度。

在 scikit-learn 中为LogisticRegression类实现的参数C来自支持向量机中的约定,这将是下一部分的主题。 C与正则化参数λ直接相关,它是反函数:

Tackling overfitting via regularization

因此,我们可以如下重写逻辑回归的正则化成本函数:

Tackling overfitting via regularization

因此,减小逆正则化参数C的值意味着我们正在增加正则化强度,这可以通过绘制两个权重系数的 L2 正则化路径来可视化:

>>> weights, params = [], []
>>> for c in np.arange(-5, 5):
...     lr = LogisticRegression(C=10**c, random_state=0)
...     lr.fit(X_train_std, y_train)
...     weights.append(lr.coef_[1])
...     params.append(10**c)
>>> weights = np.array(weights)
>>> plt.plot(params, weights[:, 0], 
...          label='petal length')
>>> plt.plot(params, weights[:, 1], linestyle='--', 
...          label='petal width')
>>> plt.ylabel('weight coefficient')
>>> plt.xlabel('C')
>>> plt.legend(loc='upper left')
>>> plt.xscale('log')
>>> plt.show()

通过执行上述代码,我们为十个逻辑逆模型拟合了反正则化参数C的不同值。 为了说明的目的,我们仅收集了第 2 类与所有分类器的权重系数。 请记住,我们使用 OvR 技术进行多类分类。

正如我们在结果图中看到的所示,如果我们减小参数C,即增加正则化强度,则权重系数会减小:

Tackling overfitting via regularization

注意

由于对单个分类算法的深入介绍超出了本书的范围,因此我热烈推荐 Scott Menard 博士的 Logistic 回归:从入门到高级概念和应用Sage 出版物,希望了解更多关于逻辑回归的读者。

Tackling overfitting via regularization

支持向量机的最大边距分类

另一种强大且广泛使用的学习算法是支持向量机SVM),可以将其视为感知器的扩展。 使用感知器算法,我们将错误分类错误最小化。 但是,在 SVM 中,我们的优化目标是使余量最大化。 余量定义为分离的超平面(决策边界)与最接近该超平面的训练样本之间的距离,即所谓的支持向量。 下图对此进行了说明:

Maximum margin classification with support vector machines

最大直觉

具有较大边界的决策边界背后的理由是,它们倾向于具有较低的泛化误差,而具有较小边界的模型更倾向于过度拟合。 为了直观了解裕量最大化,让我们仔细看一下平行于决策边界的那些超平面,它们可以表示为:

Maximum margin intuition

Maximum margin intuition

如果我们将这两个线性方程式(1)和(2)彼此相减,则会得到:

Maximum margin intuition

我们可以通过向量w的长度对其进行归一化,其定义如下:

Maximum margin intuition

因此,我们得出以下等式:

Maximum margin intuition

然后,前面等式的左侧可以解释为正超平面和负超平面之间的距离,这是我们想要最大化的所谓余量。

现在,在样本被正确分类的约束下,通过最大化2 / ||w||,SVM 的目标函数变为了该裕度的最大化,可以写成如下:

Maximum margin intuition

Maximum margin intuition

这两个等式基本上说所有负样本都应该落在负超平面的一侧,而所有正样本都应该落在正超平面的后面。 也可以更紧凑地编写如下:

Maximum margin intuition

但是,实际上,更容易将倒数项1/2 ||w||²最小化,这可以通过二次编程来解决。 但是,关于二次编程的详细讨论超出了本书的范围,但是,如果您有兴趣,可以在 Vladimir Vapnik 的中了解有关支持向量机SVM)的更多信息。 ]统计学习理论Springer Science &商业媒体或 Chris JC Burges 在模式识别支持向量机教程中的出色解释(Data 挖掘和知识发现,2(2):121-167,1998 年)。

使用松弛变量处理非线性可分情况

尽管我们不想深入探讨裕度分类背后更复杂的数学概念,但让我们简要地提及松弛变量ξ。 它是由弗拉基米尔·瓦普尼克(Vladimir Vapnik)在 1995 年提出的,并导致了所谓的软边际分类。 引入松弛变量ξ的动机是,对于非线性可分离的数据,需要放宽线性约束,以在存在错误分类的情况下,在适当的成本惩罚下允许优化的收敛。

正值松弛变量仅添加到线性约束中:

Dealing with the nonlinearly separable case using slack variables

Dealing with the nonlinearly separable case using slack variables

因此,要最小化的新目标(受前面的约束)变为:

Dealing with the nonlinearly separable case using slack variables

使用变量C,我们可以控制错误分类的代价。 较大的C值对应较大的错误惩罚,而如果我们为C选择较小的值,则对误分类错误的要求就不那么严格。 然后,我们可以使用参数C来控制边距的宽度,从而调整偏差方差的权衡,如下图所示:

Dealing with the nonlinearly separable case using slack variables

这个概念与正则化相关,我们先前在正则回归的背景下讨论过,其中增加C的值会增加偏差并降低模型的方差。

现在我们学习了线性 SVM 的基本概念,让我们训练一个 SVM 模型来对 Iris 数据集中的不同花朵进行分类:

>>> from sklearn.svm import SVC
   >>> svm = SVC(kernel='linear', C=1.0, random_state=0)
>>> svm.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std, 
...                       y_combined, classifier=svm,
...                       test_idx=range(105,150))
>>> plt.xlabel('petal length [standardized]')
>>> plt.ylabel('petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()

下图显示了执行前面的代码示例后可视化的 SVM 的决策区域:

Dealing with the nonlinearly separable case using slack variables

注意

Logistic 回归与 SVM

在实际的分类任务中,线性逻辑回归和线性支持向量机通常会产生非常相似的结果。 Logistic 回归试图使训练数据的条件可能性最大化,这使其比 SVM 更容易出现异常值。 SVM 最关心的是最靠近决策边界的点(支持向量)。 另一方面,逻辑回归的优势在于它是一个更简单的模型,可以更轻松地实现。 此外,逻辑回归模型可以轻松更新,这在处理流数据时很有吸引力。

scikit-learn 中的替代实现

我们在上一节中通过 scikit-learn 使用的,PerceptronLogisticRegression类利用了 LIBLINEAR 库,该库是由台湾大学开发的高度优化的 C/C++ 库。 同样,我们用来训练 SVM 的SVC类也使用了 LIBSVM,这是专门用于 SVM 的等效 C/C++ 库

与本地 Python 实现相比,使用 LIBLINEAR 和 LIBSVM 的优势在于,它们可以非常快速地训练大量的线性分类器。 但是,有时我们的数据集太大而无法容纳计算机内存。 因此,scikit-learn 还可以通过SGDClassifier类提供替代实现,该类还支持通过partial_fit方法进行在线学习。 SGDClassifier类的概念类似于我们在第 2 章,“训练机器学习分类算法”中为 Adaline 实现的随机梯度算法。 我们可以使用以下默认参数初始化感知器的随机梯度下降版本,逻辑回归和支持向量机:

>>> from sklearn.linear_model import SGDClassifier
>>> ppn = SGDClassifier(loss='perceptron')
>>> lr = SGDClassifier(loss='log')
>>> svm = SGDClassifier(loss='hinge')

使用内核 SVM 解决非线性问题

SVM 在机器学习从业者中如此受欢迎的另一个原因是,它们可以轻松地内核化来解决非线性分类问题。 在讨论内核 SVM 背后的主要概念之前,让我们首先定义并创建一个样本数据集,以查看这种非线性分类问题的外观。

使用下面的代码,我们将使用 NumPy 中的logical_xor函数创建一个简单的数据集,该数据集具有 XOR 门的形式,其中将为 100 个样本分配类别标签,为 100 个样本分配类别标签 -1,分别为:

>>> np.random.seed(0)
>>> X_xor = np.random.randn(200, 2)
>>> y_xor = np.logical_xor(X_xor[:, 0] > 0, X_xor[:, 1] > 0)
>>> y_xor = np.where(y_xor, 1, -1)

>>> plt.scatter(X_xor[y_xor==1, 0], X_xor[y_xor==1, 1],
...             c='b', marker='x', label='1')
>>> plt.scatter(X_xor[y_xor==-1, 0], X_xor[y_xor==-1, 1],
...             c='r', marker='s', label='-1')
>>> plt.ylim(-3.0)
>>> plt.legend()
>>> plt.show()

执行完代码后,我们将获得一个具有随机噪声的 XOR 数据集,如下图所示:

Solving nonlinear problems using a kernel SVM

显然,我们无法使用线性超平面作为决策边界,通过线性逻辑回归或线性 SVM 模型,将正负类样本很好地分离开来。

处理此类线性不可分离的数据的内核方法背后的基本思想是创建原始特征的非线性组合,以通过映射函数φ(·)将它们投影到更高维的空间,在该映射函数中线性可分离。 如下图所示,我们可以将二维数据集转换到新的三维特征空间上,在该空间中,类可以通过以下投影进行分离:

Solving nonlinear problems using a kernel SVM

这使我们能够通过线性超平面来分离图中所示的两个类别,如果我们将其投影回原始特征空间,它将成为非线性决策边界:

Solving nonlinear problems using a kernel SVM

使用内核技巧在高维空间中找到分离的超平面

为了使用 SVM 解决非线性问题,我们通过映射函数φ(·)将训练数据转换到更高维的特征空间,并训练线性 SVM 模型以对该新特征空间中的数据进行分类。 然后,我们可以使用相同的映射函数φ(·)来转换看不见的新数据,以使用线性 SVM 模型对其进行分类。

但是,这种映射方法的一个问题是,新功能的构建在计算上非常昂贵,尤其是当我们处理高维数据时。 这就是所谓的内核技巧起作用的地方。 尽管我们没有详细介绍如何解决二次编程任务以训练 SVM,但实际上,我们所需要做的只是用φ(x^(i)) · φ(x^(j))代替点积x^(i) · x^(j)。 在中,为了节省显式计算两点之间的点积的昂贵步骤,我们定义了一个所谓的核函数:k(x^(i), x^(j)) = φ(x^(i)) · φ(x^(j))

使用最广泛的内核之一是径向基函数内核RBF 内核)或高斯内核:

Using the kernel trick to find separating hyperplanes in higher dimensional space

通常将其简化为:

Using the kernel trick to find separating hyperplanes in higher dimensional space

此处,γ = 1 / (2σ²)是要优化的自由参数。

粗略地说,术语内核可以解释为一对样本之间的相似度函数。 负号将距离量度反转为相似度得分,并且由于指数项,所得相似度得分将落在 1(对于完全相似的样本)和 0(对于非常不同的样本)之间的范围内。

现在,我们定义了内核技巧背后的全局,让我们看看是否可以训练内核 SVM,该 SVM 能够绘制出将 XOR 数据很好地分开的非线性决策边界。 在这里,我们仅使用先前导入的 scikit-learn 中的SVC类,并将参数kernel='linear'替换为kernel='rbf'

>>> svm = SVC(kernel='rbf', random_state=0, gamma=0.10, C=10.0)
>>> svm.fit(X_xor, y_xor)
>>> plot_decision_regions(X_xor, y_xor, classifier=svm)
>>> plt.legend(loc='upper left')
>>> plt.show()

正如我们在中看到的结果图所示,内核 SVM 相对较好地分离了 XOR 数据:

Using the kernel trick to find separating hyperplanes in higher dimensional space

我们设置为gamma=0.1γ参数可以理解为高斯球的截止参数。 如果我们增加γ的值,则会增加训练样本的影响或影响范围,从而导致较软的决策边界。 为了更好地了解γ,让我们将 RBF 内核 SVM 应用于我们的鸢尾花数据集:

>>> svm = SVC(kernel='rbf', random_state=0, gamma=0.2, C=1.0)
>>> svm.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std, 
...                       y_combined, classifier=svm,
...                       test_idx=range(105,150))
>>> plt.xlabel('petal length [standardized]')
>>> plt.ylabel('petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()

由于我们为γ选择了较小的值,因此 RBF 内核 SVM 模型的最终决策边界将相对较软,如下图所示:

Using the kernel trick to find separating hyperplanes in higher dimensional space

现在让我们增加γ的值,并观察对决策边界的影响:

>>> svm = SVC(kernel='rbf', random_state=0, gamma=100.0, C=1.0)
>>> svm.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std,
...                       y_combined, classifier=svm,
...                       test_idx=range(105,150))
>>> plt.xlabel('petal length [standardized]')
>>> plt.ylabel('petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()

在结果图中,我们现在可以看到,使用相对较大的γ值,围绕类 0 和 1 的决策边界要紧密得多:

Using the kernel trick to find separating hyperplanes in higher dimensional space

尽管模型非常适合训练数据集,但是这样的分类器可能会在看不见的数据上具有很高的泛化误差,这说明γ的优化在控制过度拟合方面也起着重要作用。

决策树学习

尽管我们在前面的模块中已经了解了决策树,但是让我们更深入地研究。 如果我们关注可解释性,则决策树分类器是有吸引力的模型。 就像名称决策树所暗示的那样,我们可以认为此模型是通过基于提出一系列问题来做出决策来分解数据。

让我们考虑以下示例,其中我们使用决策树来决定特定日期的活动:

Decision tree learning

基于我们训练集中的功能,决策树模型学习一系列问题以推断样本的类别标签。 尽管上图说明了基于分类变量的决策树的概念,但是如果我们的特征是像 Iris 数据集中那样的实数,则可以应用相同的概念。 例如,我们可以简单地沿萼片宽度特征轴定义一个截止值,然后问一个二元问题“分隔宽度>= 2.8厘米?”。

使用决策算法,我们从树的根部开始,对特征上的数据进行分割,从而获得最大的信息增益IG),这将在更多内容中进行解释。 在下一节中详细介绍。 在一个迭代过程中,我们可以在每个子节点上重复此拆分过程,直到叶子纯净为止。 这意味着每个节点上的样本都属于同一类。 实际上,这可能会导致具有许多节点的非常深的树,这很容易导致过度拟合。 因此,我们通常希望通过设置树的最大深度的限制来修剪树。

最大限度地提高信息获取能力–物有所值

为了使在信息最多的特征上分割节点,我们需要定义一个目标函数,该目标函数要通过树学习算法进行优化。 在这里,我们的目标功能是使每次拆分的信息增益最大化,定义如下:

Maximizing information gain – getting the most bang for the buck

f是执行分割的功能,D[p]D[j]是父级的数据集,第 j 个子节点,I [ 是我们的杂质度量,N[p]是父节点上的样本总数,N[j]是第j个子节点中的样本数。 可以看到,信息增益只是父节点的杂质与子节点杂质之和之间的差,子节点的杂质越低,信息增益就越大。 但是,为了简化并减少组合搜索空间,大多数库(包括 scikit-learn)都采用二进制决策树。 这意味着每个父节点都分为两个子节点D[left]D[right]

Maximizing information gain – getting the most bang for the buck

现在,二元决策树中常用的三种杂质测度或分裂标准是基尼杂质I[G]),I[H]) ,以及分类错误I[E])。 让我们从所有非空p(i | t) ≠ 0的熵的定义开始:

Maximizing information gain – getting the most bang for the buck

在此,p(i | t)是属于特定节点t的类别i的样本的比例。 因此,如果节点上的所有样本都属于同一类别,则熵为 0,如果我们具有统一的类别分布,则熵为最大。 例如,在二进制类别设置中,如果p(i = 1 | t) = 1p(i = 0 | t) = 0,则熵为 0。 如果类别通过p(i = 1 | t) = 0.5p(i = 0 | t) = 0.5均匀分布,则熵为 1。因此,可以说熵准则试图使树中的互信息最大化。

直观地,基尼杂质可以被理解为使错误分类的可能性最小化的标准:

Maximizing information gain – getting the most bang for the buck

类似于熵,如果类别完美混合,例如在二进制类别设置(c = 2)中,则基尼杂质最大。

Maximizing information gain – getting the most bang for the buck

但是,在实践中,基尼杂质和熵通常会产生非常相似的结果,通常不值得花大量时间使用不同的杂质标准评估树木,而不是尝试使用不同的修剪截止值。

另一个杂质度量是分类错误:

Maximizing information gain – getting the most bang for the buck

这是用于修剪的有用标准,但不建议用于增长决策树,因为它对节点的类概率的更改不太敏感。 我们可以通过查看下图中所示的两种可能的拆分方案来说明这一点:

Maximizing information gain – getting the most bang for the buck

我们从父节点D[p]的数据集D[p]开始,该数据集由 1 类的 40 个样本和 2 类的 40 个样本组成,我们分别分成两个数据集D[left]D[right]。 在场景 A 和场景 B 中,使用分类误差作为划分标准的信息增益将是相同的(IG[E] = 0.25):

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

但是,基尼杂质比方案A(IG[G] = 0.125)更倾向于方案B(IG[G] = 0.166...)中的拆分,而方案A(IG[G] = 0.125)的确更纯净:

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

类似地,熵标准比方案A(IG[H] = 0.19)更倾向于方案B(IG[H] = 0.31)

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

Maximizing information gain – getting the most bang for the buck

为了更直观地比较前面讨论的三个不同杂质标准,让我们绘制类别 1 的概率范围[0,1]的杂质指数。请注意,我们还将添加熵的缩放版本 (熵/ 2 )观察到,基尼杂质是熵和分类误差之间的中间量度。 代码如下:

>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> def gini(p):
...     return (p)*(1 - (p)) + (1 - p)*(1 - (1-p))
>>> def entropy(p):
...     return - p*np.log2(p) - (1 - p)*np.log2((1 - p))
>>> def error(p):
...     return 1 - np.max([p, 1 - p])
>>> x = np.arange(0.0, 1.0, 0.01)
>>> ent = [entropy(p) if p != 0 else None for p in x]
>>> sc_ent = [e*0.5 if e else None for e in ent]
>>> err = [error(i) for i in x]
>>> fig = plt.figure()
>>> ax = plt.subplot(111)
>>> for i, lab, ls, c, in zip([ent, sc_ent, gini(x), err], 
...                   ['Entropy', 'Entropy (scaled)', 
...                   'Gini Impurity', 
...                   'Misclassification Error'],
...                   ['-', '-', '--', '-.'],
...                   ['black', 'lightgray',
...                      'red', 'green', 'cyan']):
...     line = ax.plot(x, i, label=lab, 
...                    linestyle=ls, lw=2, color=c)
>>> ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.15),
...           ncol=3, fancybox=True, shadow=False)
>>> ax.axhline(y=0.5, linewidth=1, color='k', linestyle='--')
>>> ax.axhline(y=1.0, linewidth=1, color='k', linestyle='--')
>>> plt.ylim([0, 1.1])
>>> plt.xlabel('p(i=1)')
>>> plt.ylabel('Impurity Index')
>>> plt.show()

前面的代码示例生成的图如下:

Maximizing information gain – getting the most bang for the buck

建立决策树

决策树可以通过将特征空间划分为矩形来建立复杂的决策边界。 但是,我们必须谨慎,因为决策树越深,决策边界变得越复杂,这很容易导致过度拟合。 使用 scikit-learn,我们现在将使用熵作为杂质标准来训练最大深度为 3 的决策树。 尽管出于可视化目的可能需要特征缩放,但是请注意,特征缩放不是决策树算法的要求。 代码如下:

>>> from sklearn.tree import DecisionTreeClassifier
>>> tree = DecisionTreeClassifier(criterion='entropy', 
...                               max_depth=3, random_state=0)
>>> tree.fit(X_train, y_train)
>>> X_combined = np.vstack((X_train, X_test))
>>> y_combined = np.hstack((y_train, y_test))
>>> plot_decision_regions(X_combined, y_combined, 
...                    classifier=tree, test_idx=range(105,150))
>>>plt.xlabel('petal length [cm]')
>>>plt.ylabel('petal width [cm]') 
>>> plt.legend(loc='upper left')
>>> plt.show()

在执行上述代码示例的之后,我们获得了决策树的典型轴平行决策边界:

Building a decision tree

scikit-learn 的一个不错的功能是它允许我们在训练后将决策树导出为.dot文件,我们可以使用 GraphViz 程序将其可视化。 该程序可从这个页面免费获得,并受 Linux,Windows 和 Mac OS X 支持。

首先,我们使用tree子模块中的export_graphviz函数通过 scikit-learn 创建.dot文件,如下所示:

>>> from sklearn.tree import export_graphviz
>>> export_graphviz(tree, 
...                 out_file='tree.dot',
...                 feature_names=['petal length', 'petal width'])

在计算机上安装 GraphViz 之后,可以通过在保存tree.dot文件的位置从命令行执行以下命令,将tree.dot文件转换为 PNG 文件:

> dot -Tpng tree.dot -o tree.png

Building a decision tree

查看我们通过 GraphViz 创建的决策树图,我们现在可以很好地追溯决策树从训练数据集中确定的拆分。 我们从根部的 105 个样本开始,使用花瓣宽度截止值≤0.75 厘米,将其分为两个子节点,分别具有 34 和 71 个样本。 第一次拆分后,我们可以看到左侧的子节点已经是纯节点,并且仅包含 Iris-Setosa 类的样本(熵= 0)。 然后使用右侧的其他拆分从 Iris-Versicolor 和 Iris-Virginica 类中分离样本。

通过随机森林将弱者和强者合并

随机森林 由于其良好的分类性能,可伸缩性和易用性,在过去的十年中在机器学习应用中获得了巨大的普及。 直观上,可以将随机森林视为决策树的集合。 集成学习的思想是结合弱学习者建立一个更健壮的模型强学习者,该模型具有更好的泛化误差并且不易受到影响 过度拟合。 随机森林算法可以归纳为四个简单步骤:

  1. 绘制大小为n的随机引导程序样本(从训练集中随机选择n样本,并进行替换)。

  2. 从引导程序样本中增长决策树。 在每个节点上:

    1. 随机选择d功能,无需替换。
    2. 使用根据目标函数提供最佳拆分的功能(例如,通过最大化信息增益)拆分节点。
  3. 重复步骤 1 至 2k次。

  4. 汇总每棵树的预测,以多数票分配类别标签。 多数投票将在第 7 章,“结合不同模型进行整合学习”中进行详细讨论。

当我们训练各个决策树时,第 2 步有一个轻微修改:我们不考虑评估所有功能以确定每个节点上的最佳分割,而只考虑其中的一个随机子集。

尽管随机森林不能提供与决策树相同的可解释性,但是随机森林的一大优势是我们不必为选择良好的超参数值而担心。 通常,我们不需要修剪随机森林,因为集成模型对于来自各个决策树的噪声非常鲁棒。 实际上,我们真正需要关心的唯一参数是我们为随机森林选择的树数k(第 3 步)。 通常,树的数量越多,随机森林分类器的性能越好,但以增加的计算成本为代价。

尽管在实践中不太常见,但可以优化随机森林分类器的其他超参数(使用我们将在第 5 章,“通过降维压缩”压缩数据中讨论的技术)的大小 自举样本的n(步骤 1)和为每个分割随机选择的特征数d(步骤 2.1)。 通过引导程序样本的样本大小n,我们控制了随机森林的偏差-方差折衷。 通过为n选择较大的值,我们可以减少随机性,因此森林更可能过度适应。 另一方面,我们可以通过为模型性能选择n较小的值来减少过拟合的程度。 在大多数实施方式中,包括 scikit-learn 中的RandomForestClassifier实施方式,引导程序样本的样本大小均应选择为等于原始训练集中的样本数,这通常会提供良好的偏差方差折衷。 对于每个拆分中的特征数d,我们希望选择一个比训练集中的特征总数小的值。 scikit-learn 和其他实现中使用的合理默认值是d = √m,其中m是训练集中功能的数量。

方便地,我们不必自己从各个决策树构造随机森林分类器。 scikit-learn 中已经有一个我们可以使用的实现:

>>> from sklearn.ensemble import RandomForestClassifier
>>> forest = RandomForestClassifier(criterion='entropy',
...                                 n_estimators=10, 
...                                 random_state=1,
...                                 n_jobs=2)
>>> forest.fit(X_train, y_train)
>>> plot_decision_regions(X_combined, y_combined, 
...                classifier=forest, test_idx=range(105,150))
>>> plt.xlabel('petal length')
>>> plt.ylabel('petal width')
>>> plt.legend(loc='upper left')
>>> plt.show()

执行完前面的代码后,我们应该看到由随机森林中的树木集合形成的决策区域,如下图所示:

Combining weak to strong learners via random forests

使用前面的代码,我们通过n_estimators参数从 10 个决策树中训练了一个随机森林,并使用熵准则作为杂质度量来分割节点。 尽管我们从很小的训练数据集中生长出非常小的随机森林,但出于演示目的,我们使用了n_jobs参数,这使我们可以使用计算机的多个核(这里为两个)并行化模型训练。

K 近邻–惰性学习算法

我们将在本章中讨论的最后一个监督学习算法是 k 最近邻分类器KNN),这特别有趣,因为它从根本上讲 与我们到目前为止讨论的学习算法不同。

KNN 是懒惰学习者的典型示例。 之所以称其为懒惰,并不是因为它看上去简单,而是因为它没有从训练数据中学习判别函数,而是存储了训练数据集。

注意

参数模型与非参数模型

机器学习算法可以分为参数非参数模型。 使用参数模型,我们从训练数据集中估计参数,以学习可以对新数据点进行分类的功能,而不再需要原始训练数据集。 参数模型的典型示例是感知器,逻辑回归和线性 SVM。 相比之下,非参数模型无法通过一组固定的参数来表征,并且参数的数量随训练数据的增长而增加。 到目前为止,我们已经看到了两个非参数模型的例子:决策树分类器/随机森林和内核 SVM。

KNN 属于非参数模型的子类别,该子类别被描述为基于实例的学习。 基于实例学习的模型的特征是记忆训练数据集,而惰性学习是基于实例学习的特例,在学习过程中无需花费零成本。

KNN 算法本身非常简单,可以通过以下步骤进行总结:

  1. 选择k的数量和距离度量。
  2. 找到我们要分类的样本的k最近邻居。
  3. 通过多数表决分配班级标签。

下图说明了如何根据新数据点()在五个最邻近邻居之间的多数表决方式为其分配三角形类别标签。

K-nearest neighbors – a lazy learning algorithm

基于选择的距离度量,KNN 算法在训练数据集中找到最接近(最相似)我们要分类的点的k个样本。 然后,由新数据点的k最近邻居中的多数票决定其类别标签。

这种基于内存的方法的主要优势在于,分类器会在我们收集新的训练数据时立即进行调整。 但是,不利的一面是,在最坏的情况下,用于对新样本进行分类的计算复杂度会随着训练数据集中的样本数量线性增加,除非该数据集的维数(特征)很少,并且该算法已使用有效数据来实现 结构,例如 KD 树。 (JH Friedman,JL Bentley 和 RA Finkel。一种用于寻找对数期望时间的最佳匹配的算法。ACMTransactions on Mathematical Software(TOMS),3(3):209-226,1977。)此外,我们不能放弃 训练样本,因为不涉及训练步骤。 因此,如果我们使用大型数据集,则存储空间可能会成为一个挑战。

通过执行以下代码,我们现在将使用欧几里德距离度量在 scikit-learn 中实现 KNN 模型:

>>> from sklearn.neighbors import KNeighborsClassifier
>>> knn = KNeighborsClassifier(n_neighbors=5, p=2,
...                            metric='minkowski')
>>> knn.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std, y_combined, 
...                       classifier=knn, test_idx=range(105,150))
>>> plt.xlabel('petal length [standardized]')
>>> plt.ylabel('petal width [standardized]')
>>> plt.show()

通过在此数据集的 KNN 模型中指定五个邻居,我们获得了相对平滑的决策边界,如下图所示:

K-nearest neighbors – a lazy learning algorithm

注意

在平局的情况下,KNN 算法的 scikit-learn 实现将更喜欢距离样本更近的邻居。 如果邻居的距离相似,则算法将选择训练数据集中最先出现的类别标签。

k正确选择对于在过度拟合和欠拟合之间找到良好的平衡至关重要。 我们还必须确保选择适合于数据集中要素的距离度量。 通常,简单的欧几里德距离度量用于实值样本,例如,虹膜数据集中的花朵具有以厘米为单位的特征。 但是,如果我们使用欧几里德距离度量,则标准化数据也很重要,以使每个要素对距离的贡献均等。 我们在前面的代码中使用的'minkowski'距离只是欧几里得距离和曼哈顿距离的概括,可以写成如下形式:

K-nearest neighbors – a lazy learning algorithm

如果我们将参数p=2或曼哈顿距离分别设置为p=1,则它成为欧几里德距离。 scikit-learn 中提供了许多其他距离度量,并且可以将提供给metric参数。 它们在这个页面中列出。

注意

维度的诅咒

值得一提的是,由于维度的诅咒,KNN 非常容易过拟合。 维度的诅咒描述了一种现象,其中随着固定大小的训练数据集的维度数量的增加,特征空间变得越来越稀疏。 凭直觉,我们可以想到即使是最接近的邻居在高维空间中也相距太远,无法给出良好的估计。

我们在关于逻辑回归的部分中讨论了正则化的概念,这是避免过度拟合的一种方法。 但是,在不适用正则化的模型(例如决策树和 KNN)中,我们可以使用特征选择和降维技术来帮助我们避免降维。 下一章将对此进行更详细的讨论。

K-nearest neighbors – a lazy learning algorithm

K-nearest neighbors – a lazy learning algorithm

三十八、建立良好的训练集——数据预处理

数据的质量和其中包含的有用信息的数量是决定机器学习算法学习程度的关键因素。 因此,在将数据集输入学习算法之前,确保对数据集进行检查和预处理绝对至关重要。 在本章中,我们将讨论基本的数据预处理技术,这些技术将帮助我们建立良好的机器学习模型。

我们将在本章中介绍的主题如下:

  • 从数据集中删除和估算缺失值
  • 将分类数据转化为机器学习算法的形状
  • 选择模型构建的相关特征

处理丢失的数据

在实际应用中,并非经常因各种原因而缺少一个或多个值。 例如,数据收集过程中可能存在错误,某些度量不适用,某些字段可能仅在调查中留为空白。 我们通常会看到缺失值作为数据表中的空格或占位符字符串,例如NaN(非数字)。

不幸的是,如果我们简单地忽略它们,大多数计算工具将无法处理此类缺失值,或者会产生无法预测的结果。 因此,至关重要的是,在继续进行进一步分析之前,应对那些遗漏的值进行处理。 但是在讨论几种处理缺失值的技术之前,让我们从 CSV逗号分隔值)文件创建一个简单的示例数据框,以便更好地了解 问题:

>>> import pandas as pd
>>> from io import StringIO
>>> csv_data = '''A,B,C,D
... 1.0,2.0,3.0,4.0
... 5.0,6.0,,8.0
... 10.0,11.0,12.0,'''
>>> # If you are using Python 2.7, you need
>>> # to convert the string to unicode:
>>> # csv_data = unicode(csv_data)
>>> df = pd.read_csv(StringIO(csv_data))
>>> df
   A   B   C   D
0  1   2   3   4
1  5   6 NaN   8
2  10  11  12 NaN

使用前面的代码,我们通过read_csv函数将 CSV 格式的数据读取到了 PandasDataFrame中,并注意到两个缺失的单元格被NaN替换了。 前面的代码示例中的StringIO功能仅用于说明目的。 它使我们可以将分配给csv_data的字符串读入 PandasDataFrame中,就好像它是我们硬盘上的常规 CSV 文件一样。

对于较大的DataFrame,手动查找缺失值可能很繁琐; 在这种情况下,我们可以使用isnull方法返回带有布尔值的DataFrame,该布尔值指示单元格是否包含数字值(False)或是否缺少数据(True)。 使用sum方法,我们可以按如下所示返回每列缺失值的数量:

>>> df.isnull().sum()
A    0
B    0
C    1
D    1
dtype: int64

这样,我们可以计算每列缺失值的数量。 在以下小节中,我们将研究如何处理这些丢失的数据的不同策略。

注意

尽管 scikit-learn 是为处理 NumPy 数组而开发的,但有时使用 pandas 的DataFrame预处理数据会更方便。 我们始终可以通过values属性访问DataFrame的基础 NumPy 数组,然后再将其提供给 scikit-learn 估计器:

>>> df.values
array([[  1.,   2.,   3.,   4.],
       [  5.,   6.,  nan,   8.],
       [ 10.,  11.,  12.,  nan]])

消除具有缺失值的样品或特征

处理丢失数据的最简单方法之一就是简单地从数据集中完全删除相应的特征(列)或样本(行)。 缺少值的行可以通过dropna方法轻松删除:

>>> df.dropna()
   A  B  C  D
0  1  2  3  4

类似地,我们可以通过将axis参数设置为1来删除任何行中至少具有NaN的列:

>>> df.dropna(axis=1)
   A   B
0  1   2
1  5   6
2  10  11

dropna方法支持一些方便使用的其他参数:

# only drop rows where all columns are NaN
>>> df.dropna(how='all')  

# drop rows that have not at least 4 non-NaN values
>>> df.dropna(thresh=4)  

# only drop rows where NaN appear in specific columns (here: 'C')
>>> df.dropna(subset=['C'])

尽管删除丢失的数据似乎是一种方便的方法,但它也具有某些缺点。 例如,我们最终可能会删除过多的样本,这将使可靠的分析变得不可能。 或者,如果我们删除太多的功能列,则将冒丢失分类器需要在类之间进行区分的有价值信息的风险。 因此,在下一节中,我们将介绍处理缺失值的最常用替代方法之一:插值技术。

插补缺失值

通常,样本的删除或整个特征列的删除根本不可行,因为我们可能会丢失太多有价值的数据。 在这种情况下,我们可以使用不同的插值技术来估计数据集中其他训练样本的缺失值。 最常见的插值技术之一是均值插补,其中我们仅将缺失值替换为整个特征列的平均值。 一种方便的方法是使用 scikit-learn 的Imputer类,如以下代码所示:

>>> from sklearn.preprocessing import Imputer
>>> imr = Imputer(missing_values='NaN', strategy='mean', axis=0)
>>> imr = imr.fit(df)
>>> imputed_data = imr.transform(df.values)
>>> imputed_data
array([[ 1., 2., 3., 4.],
       [ 5., 6., 7.5, 8.],
       [ 10., 11., 12., 6.]])

在这里,我们用相应的均值替换了每个NaN值,该均值是针对每个功能列分别计算的。 如果将axis=0设置更改为axis=1,我们将计算行均值。 strategy参数的其他选项是medianmost_frequent,其中后者将丢失的值替换为最频繁的值。 这对于估算分类要素值很有用。

了解 scikit-learn 估算器 API

在的上一部分中,我们使用了 scikit-learn 的Imputer类来估算数据集中的缺失值。 Imputer 类属于 scikit-learn 中所谓的变换器类,用于数据转换。 这些估计量的两个基本方法是fittransformfit方法用于从训练数据中学习参数,transform方法使用这些参数来转换数据。 任何要转换的数据数组都必须具有与用于模型拟合的数据数组相同数量的特征。 下图说明了如何使用安装在训练数据上的提升器来转换训练数据集和新的测试数据集:

Understanding the scikit-learn estimator API

在第 3 章,“使用 Scikit-learn” 的机器学习分类器中使用的分类器属于 scikit-learn 中所谓的估计器,其 API 在概念上与提升器类非常相似。 估计器具有predict方法,但也可以具有transform方法,我们将在后面看到。 您可能还记得,当我们训练那些估计器进行分类时,我们还使用了fit方法来学习模型的参数。 但是,在监督学习任务中,我们另外提供了适合模型的类标签,然后可以通过predict方法将其用于对新数据样本进行预测,如下图所示:

Understanding the scikit-learn estimator API

Understanding the scikit-learn estimator API

处理分类数据

到目前为止,我们仅使用数值。 但是,现实世界的数据集包含一个或多个分类要素列并不少见。 在讨论分类数据时,我们必须进一步区分标称标称特征。 序数特征可以理解为可以排序或排序的分类值。 例如,T 恤尺寸将是一个常规特征,因为我们可以定义一个订单 XL >L>M。 相比之下,标称特征并不意味着任何顺序,并且继续前面的示例,我们可以将 T 恤颜色视为标称特征,因为通常来说,这是没有意义的, 例如,红色大于蓝色

在探索用于处理此类分类数据的不同技术之前,让我们创建一个新的数据框来说明问题:

>>> import pandas as pd
>>> df = pd.DataFrame([
...            ['green', 'M', 10.1, 'class1'], 
...            ['red', 'L', 13.5, 'class2'], 
...            ['blue', 'XL', 15.3, 'class1']])
>>> df.columns = ['color', 'size', 'price', 'classlabel']
>>> df
   color size  price classlabel
0  green    M   10.1     class1
1    red    L   13.5     class2
2   blue   XL   15.3     class1

从前面的输出中可以看到,新创建的DataFrame包含一个名义特征(color),一个序数特征(size)和一个数值特征(price)列。 类标签(假设我们为监督学习任务创建了数据集)存储在最后一列中。 我们在本书中讨论的分类学习算法不在类标签中使用序数信息。

映射序数特征

为了确保的学习算法能够正确解释序数特征,我们需要将分类字符串值转换为整数。 不幸的是,没有方便的功能可以自动导出size功能标签的正确顺序。 因此,我们必须手动定义映射。 在下面的简单示例中,假设我们知道功能之间的区别,例如XL = L + 1 = M + 2

>>> size_mapping = {
...                 'XL': 3,
...                 'L': 2,
...                 'M': 1}
>>> df['size'] = df['size'].map(size_mapping)
>>> df
   color  size  price classlabel
0  green     1   10.1     class1
1    red     2   13.5     class2
2   blue     3   15.3     class1

如果我们想要在稍后的阶段将整数值转换回原始字符串表示形式,我们可以简单地定义一个反向映射字典inv_size_mapping = {v: k for k, v in size_mapping.items()},然后可以通过 Pandas 的map方法在转换后的字典中使用它 功能列类似于我们之前使用的size_mapping词典。

编码类标签

许多机器学习库都要求将类标签编码为整数值。 尽管 scikit-learn 中用于分类的大多数估计器都会在内部将类标签转换为整数,但是将类标签作为整数数组提供以避免技术故障是一种很好的做法。 要对类标签进行编码,我们可以使用类似于前面讨论的序数特征映射的方法。 我们需要记住,类标签不是而是序数,并且我们将哪个整数分配给特定的字符串标签都没有关系。 因此,我们可以简单地枚举从 0 开始的类标签:

>>> import numpy as np
>>> class_mapping = {label:idx for idx,label in
...                  enumerate(np.unique(df['classlabel']))}
>>> class_mapping
{'class1': 0, 'class2': 1}

接下来,我们可以使用映射字典将类标签转换为整数:

>>> df['classlabel'] = df['classlabel'].map(class_mapping)
>>> df
   color  size  price  classlabel
0  green     1   10.1           0
1    red     2   13.5           1
2   blue     3   15.3           0

我们可以按如下所示反转映射字典中的键/值对,以将转换后的类标签映射回原始字符串表示形式:

>>> inv_class_mapping = {v: k for k, v in class_mapping.items()}
>>> df['classlabel'] = df['classlabel'].map(inv_class_mapping)
>>> df
   color  size  price classlabel
0  green     1   10.1     class1
1    red     2   13.5     class2
2   blue     3   15.3     class1

另外,是一个方便的LabelEncoder类,直接在 scikit-learn 中实现以实现相同的目的:

>>> from sklearn.preprocessing import LabelEncoder
>>> class_le = LabelEncoder()
>>> y = class_le.fit_transform(df['classlabel'].values)
>>> y
array([0, 1, 0])

请注意,fit_transform方法只是分别调用fittransform的快捷方式,我们可以使用inverse_transform方法将整数类标签转换回原始字符串表示形式:

>>> class_le.inverse_transform(y)
array(['class1', 'class2', 'class1'], dtype=object)

对名义特征执行一次热编码

在的上一部分中,我们使用了一种简单的字典映射方法将序数大小特征转换为整数。 由于 scikit-learn 的估计器对待类标​​签没有任何顺序,因此我们使用方便的LabelEncoder类将字符串标签编码为整数。 似乎我们可以使用类似的方法来转换数据集的标称color列,如下所示:

>>> X = df[['color', 'size', 'price']].values
>>> color_le = LabelEncoder()
>>> X[:, 0] = color_le.fit_transform(X[:, 0])
>>> X
array([[1, 1, 10.1],
       [2, 2, 13.5],
       [0, 3, 15.3]], dtype=object)

执行完前面的代码后,NumPy 数组X的第一列现在包含新的color值,其值编码如下:

  • 蓝色→0
  • 绿色→1
  • 红色→2

如果我们此时停止并将数组提供给分类器,那么在处理分类数据时,我们将犯下最常见的错误之一。 你能发现问题吗? 尽管颜色值没有特定的顺序,但是学习算法现在将假定绿色大于蓝色,并且红色大于 绿色。 尽管此假设不正确,但该算法仍可以产生有用的结果。 但是,这些结果不是最佳的。

针对此问题的常见解决方法是使用一种称为单热编码的技术。 这种方法背后的想法是为名义特征列中的每个唯一值创建一个新的虚拟特征。 在这里,我们将color功能转换为三个新功能:bluegreenred。 然后可以使用二进制值来指示样品的特定颜色; 例如,蓝色样本可以被编码为blue=1green=0red=0。 要执行此转换,我们可以使用在scikit-learn.preprocessing模块中实现的OneHotEncoder

>>> from sklearn.preprocessing import OneHotEncoder
>>> ohe = OneHotEncoder(categorical_features=[0])
>>> ohe.fit_transform(X).toarray()
array([[  0\. ,   1\. ,   0\. ,   1\. ,  10.1],
       [  0\. ,   0\. ,   1\. ,   2\. ,  13.5],
       [  1\. ,   0\. ,   0\. ,   3\. ,  15.3]])

初始化OneHotEncoder时,我们通过categorical_features参数定义了要转换的变量的列位置(请注意color是特征矩阵X中的第一列)。 默认情况下,当我们使用transform方法时,OneHotEncoder返回一个稀疏矩阵,并且为了通过toarray 方法。 稀疏矩阵只是存储大型数据集的一种更有效的方法,并且是许多 scikit-learn 函数支持的一种方法,如果它包含很多零,则特别有用。 要省略toarray步骤,我们可以将编码器初始化为OneHotEncoder(…,sparse=False)以返回常规 NumPy 数组。

通过单热编码创建那些虚拟特征的一种更方便的方法是使用在 Pandas 中实现的get_dummies方法。 应用于DataFrame上的get_dummies方法将仅转换字符串列,并使所有其他列保持不变:

>>> pd.get_dummies(df[['price', 'color', 'size']])
   price  size  color_blue  color_green  color_red
0   10.1     1           0            1          0
1   13.5     2           0            0          1
2   15.3     3           1            0          0

在训练和测试集中划分数据集

第 1 章,“使计算机具有从数据中学习的能力”和第 3 章中,我们简要介绍了将数据集划分为单独的数据集以进行训练和测试的概念。 和“使用 Scikit-learn”的机器学习分类器的浏览。 请记住,在我们将其释放到现实世界之前,可以将测试集理解为模型的最终测试。 在本节中,我们将准备一个新的数据集 Wine 数据集。 在对数据集进行预处理之后,我们将探索用于特征选择的不同技术,以减少数据集的维数。

Wine 数据集是可从 UCI 机器学习存储库获得的另一个开源数据集; 它由 178 个葡萄酒样品组成,具有 13 个描述其不同化学特性的特征。

使用 pandas 库,我们将直接从 UCI 机器学习存储库中读取开源 Wine 数据集:

>>> df_wine = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data', header=None)
>>> df_wine.columns = ['Class label', 'Alcohol', 
...                    'Malic acid', 'Ash', 
...                    'Alcalinity of ash', 'Magnesium', 
...                    'Total phenols', 'Flavanoids',
...                    'Nonflavanoid phenols', 
...                    'Proanthocyanins', 
...                    'Color intensity', 'Hue', 
...                    'OD280/OD315 of diluted wines', 
...                    'Proline']
>>>  print('Class labels', np.unique(df_wine['Class label']))
Class labels [1 2 3]
>>> df_wine.head()

下表列出了葡萄酒数据集中的 13 种不同特征,它们描述了 178 种葡萄酒样品的化学特性:

Partitioning a dataset in training and test sets

样品属于 1、2 和 3 三个不同类别中的一个,它们分别是在意大利不同地区种植的三种不同类型的葡萄。

将此数据集随机划分为单独的测试训练数据集的便捷方法是使用 scikit-learn 的cross_validation子模块中的train_test_split函数:

>>> from sklearn.cross_validation import train_test_split
>>> X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values
>>> X_train, X_test, y_train, y_test = \
...        train_test_split(X, y, test_size=0.3, random_state=0)

首先,我们将要素列 1-13 的 NumPy 数组表示形式分配给变量X,然后将第一列中的类标签分配给变量y。 然后,我们使用train_test_split函数将Xy随机分为单独的训练和测试数据集。 通过设置test_size=0.3,我们将 30%的葡萄酒样品分配给X_testy_test,其余 70%的样品分别分配给X_trainy_train

注意

如果将数据集分为训练和测试数据集,则必须记住,我们扣留了学习算法可以从中受益的有价值的信息。 因此,我们不想为测试集分配过多的信息。 但是,测试集越小,泛化误差的估计就越不准确。 将数据集划分为训练集和测试集都是为了平衡这种权衡。 实际上,最常用的分割是 60:40、70:30 或 80:20,具体取决于初始数据集的大小。 但是,对于大型数据集,将 90:10 或 99:1 分成训练和测试子集也是常见且适当的。 最好不要在模型训练和评估后丢弃分配的测试数据,而是在整个数据集上重新训练分类器以获得最佳性能。

使功能达到相同的规模

特征缩放是我们预处理流程中很容易忘记的关键步骤。 决策树和随机森林是我们无需担心特征缩放的极少数机器学习算法之一。 但是,如果功能相同,则大多数机器学习和优化算法的性能都会好得多,正如我们在实现时在第 2 章和“训练机器学习分类算法”中所看到的那样梯度下降优化算法。

一个简单的例子可以说明特征缩放的重要性。 假设我们有两个要素,其中一个要素的缩放比例为 1 到 10,第二个要素的缩放比例为 1 到 100,000。 当我们想到第 2 章和“训练机器学习分类算法”中的 Adaline 中的平方误差函数时,可以很直观地说该算法 将主要根据第二个功能中的较大误差忙于优化权重。 另一个例子是具有欧几里德距离测度的 k 最近邻KNN)算法; 样本之间的计算距离将由第二个特征轴控制。

现在,有两种常见的方法可以将不同的功能放到相同的规模上:标准化标准化。 这些术语通常在不同领域中非常宽松地使用,其含义必须从上下文中得出。 通常,归一化是指将特征重新缩放到[0,1]的范围,这是最小-最大缩放的一种特殊情况。 为了规范化我们的数据,我们可以简单地将最小-最大缩放应用于每个特征列,其中样本x^(i)的新值x_norm^(i)可以如下计算:

Bringing features onto the same scale

在此,x^(i)是特定样本,x_min是特征列中的最小值,x_max是最大值。

最小-最大缩放过程在 scikit-learn 中实现,可以按以下方式使用:

>>> from sklearn.preprocessing import MinMaxScaler
>>> mms = MinMaxScaler()
>>> X_train_norm = mms.fit_transform(X_train)
>>> X_test_norm = mms.transform(X_test)

尽管通过最小-最大缩放进行归一化是一种常用的技术,当我们需要在有界区间中的值时很有用,但对于许多机器学习算法而言,标准化可能更为实用。 原因是很多线性模型,例如我们从第 3 章和“使用 Scikit-learn” 进行的机器学习分类器游记中所记得的逻辑回归和 SVM,将权重初始化为 0 或接近 0 的较小随机值。使用标准化,我们将特征列居中于标准偏差为 1 的均值 0,以便特征列采用正态分布的形式,这使学习权重变得更加容易。 此外,与最小-最大缩放相反,标准化可维护有关异常值的有用信息,并使算法对异常值的敏感性降低,后者可将数据缩放到有限的值范围。

标准化过程可以用以下公式表示:

Bringing features onto the same scale

在此,μ[x]分别是特定特征列的样本均值,σ[x]是相应的标准差。

下表说明了两种常用的特征缩放技术之间的区别,即对由数字 0 到 5 组成的简单样本数据集的标准化和规范化:

|

输入

|

标准化的

|

归一化

| | --- | --- | --- | | 0.0 | -1.336306 | 0.0 | | 1.0 | -0.801784 | 0.2 | | 2.0 | -0.267261 | 0.4 | | 3.0 | 0.267261 | 0.6 | | 4.0 | 0.801784 | 0.8 | | 5.0 | 1.336306 | 1.0 |

类似于MinMaxScaler,scikit-learn 还实现了一个用于标准化的类:

>>> from sklearn.preprocessing import StandardScaler
>>> stdsc = StandardScaler()
>>> X_train_std = stdsc.fit_transform(X_train)
>>> X_test_std = stdsc.transform(X_test)

同样,重要的是要强调我们只对训练数据拟合一次StandardScaler,并使用这些参数来变换测试集或任何新的数据点。

选择有意义的功能

如果我们注意到,模型在训练数据集上的表现要好于在测试数据集上的表现,则该观察结果是过拟合的有力指标。 过度拟合意味着模型太适合参数拟合训练数据集中的特定观测值,但不能很好地推广到真实数据—我们说该模型具有高方差。 过度拟合的原因是,对于给定的训练数据,我们的模型过于复杂,为了减少泛化误差,常见的解决方案如下:

  • 收集更多训练数据
  • 通过正则化引入对复杂性的惩罚
  • 选择参数更少的更简单模型
  • 降低数据的维度

收集更多的训练数据通常不适用。 在下一章中,我们将学习一种有用的技术,以检查更多的训练数据是否完全有用。 在以下各节中,我们将探讨通过正则化和通过特征选择减少维数来减少过度拟合的常见方法。

具有 L1 正则化的稀疏解决方案

我们从第 3 章,“使用 Scikit 学习的机器学习分类器”回顾,L2 正则化是一种方法 为了通过惩罚较大的单个权重来降低模型的复杂性,在这里我们定义了权重向量w的 L2 范数:

Sparse solutions with L1 regularization

降低模型复杂度的另一种方法是相关的 L1 正则化

Sparse solutions with L1 regularization

在这里,我们简单地用将权重的平方替换为权重的绝对值之和。 与 L2 正则化相反,L1 正则化产生稀疏特征向量。 大多数功能权重将为零。 如果我们拥有一个具有许多不相关特征的高维数据集,则稀疏性在实践中可能会有用,尤其是在我们的不相关维度比样本多的情况下。 从这个意义上讲,L1 正则化可以理解为一种特征选择技术。

为了更好地理解 L1 正则化如何鼓励稀疏性,让我们退后一步,看看正则化的几何解释。 让我们为两个权重系数w[1]w[2]绘制凸成本函数的轮廓。 在这里,我们将考虑在第 2 章,“训练机器学习分类算法”中用于 Adaline 的平方误差(SSE)成本函数的和。 因为它比 Logistic 回归的成本函数对称且易于绘制; 但是,相同的概念也适用于后者。 请记住,我们的目标是找到权重系数的组合,以最小化训练数据的成本函数,如下图所示(椭圆的中间点):

Sparse solutions with L1 regularization

现在,我们可以认为正则化是在成本函数中添加惩罚项以鼓励较小的权重; 或者,换句话说,我们会惩罚较大的权重。

因此,通过通过正则化参数λ增加正则强度,我们将权重缩小为零,并减少了模型对训练数据的依赖性。 让我们在下图中针对 L2 惩罚项说明这个概念。

Sparse solutions with L1 regularization

二次 L2 正则化项由阴影球表示。 在这里,我们的权重系数不能超过正则化预算-权重系数的组合不能落在阴影区域之外。 另一方面,我们仍然希望最小化成本函数。 在惩罚约束下,我们的最大努力是选择 L2 球与未惩罚成本函数的轮廓相交的点。 正则化参数λ的值越大,惩罚成本函数的增长越快,这导致 L2 球变窄。 例如,如果我们将正则化参数增加到无穷大,则权重系数将实际上变为零,由 L2 球的中心表示。 总结示例的主要信息:我们的目标是最小化未惩罚成本函数和惩罚项的总和,这可以理解为在没有足够训练数据的情况下增加偏差并倾向于使用更简单的模型来减少方差。 拟合模型。

现在让我们讨论 L1 正则化和稀疏性。 L1 正则化的主要概念与我们在此处讨论的相似。 但是,由于 L1 损失是绝对权重系数的总和(请记住 L2 项是二次项),因此我们可以将其表示为菱形预算,如下图所示:

Sparse solutions with L1 regularization

在上图中,我们可以看到成本函数的轮廓触及w[1] = 0处的 L1 菱形。 由于 L1 正则化系统的轮廓很锐利,因此最有可能(即,成本函数的椭圆与 L1 钻石的边界之间的交点)位于轴上,这会鼓励稀疏性。 为什么 L1 正则化会导致稀疏解的数学细节超出了本书的范围。 如果您有兴趣,可以在统计学习的要素Trevor Hastie,Robert Tibshirani 和 Jerome Friedman的第 3.4 节中找到有关 L2 与 L1 正则化的出色部分。 斯普林格

对于 scikit-learn 中支持 L1 正则化的正则化模型,我们可以简单地将penalty参数设置为'l1'以产生稀疏解:

>>> from sklearn.linear_model import LogisticRegression
>>> LogisticRegression(penalty='l1')

将应用于标准 Wine 数据,L1 正则逻辑回归将产生以下稀疏解:

>>> lr = LogisticRegression(penalty='l1', C=0.1)
>>> lr.fit(X_train_std, y_train)
>>> print('Training accuracy:', lr.score(X_train_std, y_train))
Training accuracy: 0.983870967742
>>> print('Test accuracy:', lr.score(X_test_std, y_test))
Test accuracy: 0.981481481481

训练和测试准确性(均为 98%)均未表明我们的模型有任何过拟合。 当我们通过lr.intercept_属性访问拦截项时,我们可以看到该数组返回三个值:

>>> lr.intercept_
array([-0.38379237, -0.1580855 , -0.70047966])

由于我们将LogisticRegression对象拟合到多类数据集上,因此默认情况下,它使用单对剩余OvR)方法,其中第一个截距属于 适合 1 类与 2 类和 3 类的模型; 第二个值是适合类别 2 与类别 1 和类别 3 的模型的截距; 第三个值是分别适合 3 类与 1 类和 2 类的模型的截距:

>>> lr.coef_
array([[ 0.280, 0.000, 0.000, -0.0282, 0.000,
         0.000, 0.710, 0.000, 0.000, 0.000,
         0.000, 0.000, 1.236],
       [-0.644, -0.0688 , -0.0572, 0.000, 0.000,
         0.000, 0.000, 0.000, 0.000, -0.927,
         0.060, 0.000, -0.371],
       [ 0.000, 0.061, 0.000, 0.000, 0.000,
         0.000, -0.637, 0.000, 0.000, 0.499,
        -0.358, -0.570, 0.000
       ]])

我们通过lr.coef_属性访问的权重数组包含三行权重系数,每个类别一个权重向量。 每行包含 13 个权重,其中每个权重乘以 13 维 Wine 数据集中的相应特征以计算净输入:

Sparse solutions with L1 regularization

我们注意到权重向量是稀疏的,这意味着它们只有几个非零的条目。 作为 L1 正则化的结果(用作特征选择方法),我们刚刚训练了一个模型,该模型对于此数据集中的潜在不相关特征具有鲁棒性。

最后,让我们绘制正则化路径,它是针对不同正则化强度的不同特征的权重系数:

>>> import matplotlib.pyplot as plt
>>> fig = plt.figure()
>>> ax = plt.subplot(111)   
>>> colors = ['blue', 'green', 'red', 'cyan', 
...          'magenta', 'yellow', 'black', 
...          'pink', 'lightgreen', 'lightblue', 
...          'gray', 'indigo', 'orange']
>>> weights, params = [], []
>>> for c in np.arange(-4, 6):
...     lr = LogisticRegression(penalty='l1', 
...                             C=10**c, 
...                             random_state=0)
...     lr.fit(X_train_std, y_train)
...     weights.append(lr.coef_[1])
...     params.append(10**c)
>>> weights = np.array(weights)
>>> for column, color in zip(range(weights.shape[1]), colors):
...     plt.plot(params, weights[:, column],
...              label=df_wine.columns[column+1],
...              color=color)
>>> plt.axhline(0, color='black', linestyle='--', linewidth=3)
>>> plt.xlim([10**(-5), 10**5])
>>> plt.ylabel('weight coefficient')
>>> plt.xlabel('C')
>>> plt.xscale('log')
>>> plt.legend(loc='upper left')
>>> ax.legend(loc='upper center', 
...           bbox_to_anchor=(1.38, 1.03),
...           ncol=1, fancybox=True)
>>> plt.show()

结果图为我们提供了有关 L1 正则化行为的更多见解。 如我们所见,如果使用强正则化参数(C < 0.1)惩罚模型,则所有特征权重将为零; C是正则化参数λ的倒数。

Sparse solutions with L1 regularization

顺序特征选择算法

降低模型复杂性并避免过度拟合的另一种方法是通过特征选择来降低降维,这对于非正规模型尤其有用。 降维技术主要分为两类:特征选择特征提取。 使用特征选择,我们选择原始特征的子集。 在特征提取中,我们从特征集中获取信息以构造新的特征子空间。 在本节中,我们将介绍经典的特征选择算法系列。 在下一章,第 5 章中,通过降维压缩数据,我们将学习有关将数据集压缩到低维特征子空间的不同特征提取技术。

顺序特征选择算法是一系列贪婪搜索算法,用于将初始 d 维特征空间缩小为 k 维特征子空间,其中k<d。 特征选择算法背后的动机是自动选择与问题最相关的特征子集,以通过消除不相关的特征或噪声来提高计算效率或减少模型的泛化误差,这对于不使用特征的算法很有用 支持正则化。 一种经典的顺序特征选择算法是顺序向后选择SBS),该算法旨在以最小的分类器性能衰减来减小初始特征子空间的维数。 提高计算效率。 在某些情况下,如果模型过度拟合,SBS 甚至可以提高模型的预测能力。

注意

贪婪算法在组合搜索问题的每个阶段都进行局部最优选择,与穷举搜索算法相反,穷举搜索算法通常会得出次优的解决方案,穷举搜索算法评估所有可能的组合并保证找到最优解。 但是,在实践中,穷举搜索通常在计算上不可行,而贪婪算法则允许使用一种不太复杂,在计算上更有效的解决方案。

SBS 算法背后的思想非常简单:SBS 顺序从完整特征子集中删除特征,直到新特征子空间包含所需数量的特征为止。 为了确定在每个阶段要删除的功能,我们需要定义要最小化的标准函数J。 由标准函数计算的标准可以简单地是去除特定特征之后和之前分类器的性能差异。 然后,可以简单地将在每个阶段要删除的功能定义为使该标准最大化的功能。 或者,更直观地说,在每个阶段,我们都消除了删除后性能损失最小的功能。 根据前面的 SBS 定义,我们可以通过 4 个简单步骤概述该算法:

  1. k = d初始化算法,其中d是整个特征空间X[d]的维数。

  2. 确定最大化标准x⁻ = argmaxJ(X[k] - x)的特征x⁻,其中x ∈ X[k]

  3. 从功能集X[k-1] := X[k] - x⁻; k := k - 1中删除功能x⁻

  4. 如果k等于所需功能的数量,则终止;否则,请转到步骤 2。

    注意

    您可以在 大规模特征选择技术的比较研究F. Ferri,P。Pudil,M。Hatef 和 J. Kittler 中找到几种连续特征算法的详细评估。 大规模特征选择技术的比较研究。 练习 IV 中的模式识别,第 403-413 页,1994

不幸的是, SBS 算法尚未在 scikit-learn 中实现。 但是,因为它是如此简单,所以让我们从头开始在 Python 中实现它:

from sklearn.base import clone
from itertools import combinations
import numpy as np
from sklearn.cross_validation import train_test_split
from sklearn.metrics import accuracy_score

class SBS():
    def __init__(self, estimator, k_features, 
        scoring=accuracy_score,
        test_size=0.25, random_state=1):
        self.scoring = scoring
        self.estimator = clone(estimator)
        self.k_features = k_features
        self.test_size = test_size
        self.random_state = random_state

    def fit(self, X, y):
        X_train, X_test, y_train, y_test = \
                train_test_split(X, y, test_size=self.test_size, 
                                 random_state=self.random_state)

        dim = X_train.shape[1]
        self.indices_ = tuple(range(dim))
        self.subsets_ = [self.indices_]
        score = self._calc_score(X_train, y_train, 
                                 X_test, y_test, self.indices_)
        self.scores_ = [score]

        while dim > self.k_features:
            scores = []
            subsets = []

            for p in combinations(self.indices_, r=dim-1):
                score = self._calc_score(X_train, y_train, 
                                         X_test, y_test, p)
                scores.append(score)
                subsets.append(p)

            best = np.argmax(scores)
            self.indices_ = subsets[best]
            self.subsets_.append(self.indices_)
            dim -= 1

            self.scores_.append(scores[best])
        self.k_score_ = self.scores_[-1]

        return self

    def transform(self, X):
        return X[:, self.indices_]

    def _calc_score(self, X_train, y_train, 
                          X_test, y_test, indices):
        self.estimator.fit(X_train[:, indices], y_train)
        y_pred = self.estimator.predict(X_test[:, indices])
        score = self.scoring(y_test, y_pred)
        return score

在先前的实现中,我们定义了k_features参数以指定要返回的所需特征数量。 默认情况下,我们使用 scikit-learn 中的accuracy_score来评估模型和估计器的性能,以对特征子集进行分类。 在fit方法的while循环内部,评估并缩减itertools.combination函数创建的特征子集,直到特征子集具有所需的维数为止。 在每次迭代中,基于内部创建的测试数据集X_test将最佳子集的准确性得分收集在列表self.scores_中。 我们稍后将使用这些分数来评估结果。 最终特征子集的列索引已分配给self.indices_,我们可以通过transform方法使用它来返回具有所选特征列的新数据数组。 请注意,我们没有删除fit方法中的显式标准,而是仅删除了性能最好的子集中不包含的特征。

现在,让我们来看一下使用来自 scikit-learn 的 KNN 分类器的 SBS 实现:

>>> from sklearn.neighbors import KNeighborsClassifier
>>> import matplotlib.pyplot as plt
>>> knn = KNeighborsClassifier(n_neighbors=2)
>>> sbs = SBS(knn, k_features=1)
>>> sbs.fit(X_train_std, y_train)

尽管我们的 SBS 实现已将数据集拆分为fit函数中的测试和训练数据集,但我们仍将训练数据集X_train馈给了算法。 然后,SBS fit方法将创建用于测试(验证)和训练的新训练子集,这就是为什么此测试集也称为验证数据集的原因。 这种方法对于防止我们的原始测试集成为训练数据的一部分是必要的。

请记住,我们的 SBS 算法在每个阶段都会收集最佳特征子集的分数,因此,让我们继续进行实施中更令人兴奋的部分,并绘制在验证数据集上计算出的 KNN 分类器的分类精度。 代码如下:

>>> k_feat = [len(k) for k in sbs.subsets_]
>>> plt.plot(k_feat, sbs.scores_, marker='o')
>>> plt.ylim([0.7, 1.1])
>>> plt.ylabel('Accuracy')
>>> plt.xlabel('Number of features')
>>> plt.grid()
>>> plt.show()

如下图所示,由于减少了特征数量,验证数据集上的 KNN 分类器的精度有所提高,这很可能是由于我们在[...]中讨论的减少了第 3 章,“使用 Scikit-learn 的机器学习分类器”中的 KNN 算法上下文。 此外,我们可以在下图中看到,对于 k = {5,6,7,8,9,10},分类器达到了 100%的精度:

Sequential feature selection algorithms

为了满足我们的好奇心,让我们看看在验证数据集上产生如此出色性能的这五个功能是什么:

>>> k5 = list(sbs.subsets_[8])
>>> print(df_wine.columns[1:][k5])
Index(['Alcohol', 'Malic acid', 'Alcalinity of ash', 'Hue', 'Proline'], dtype='object')

使用前面的代码,我们从sbs.subsets_属性中第 9 个位置的位置获得了 5 个特征子集的列索引,并从 Pandas Wine DataFrame的列索引中返回了相应的特征名称。 ]。

接下来,让我们评估原始测试集上的 KNN 分类器的性能:

>>> knn.fit(X_train_std, y_train)
>>> print('Training accuracy:', knn.score(X_train_std, y_train))
Training accuracy: 0.983870967742
>>> print('Test accuracy:', knn.score(X_test_std, y_test))
Test accuracy: 0.944444444444

在前面的代码中,我们使用了完整的功能集,并在训练数据集上获得了约 98.4%的准确性。 但是,测试数据集的准确性略低(〜94.4%),这表明略有过度拟合的迹象。 现在,让我们使用选定的 5 个功能子集,看看 KNN 的表现如何:

>>> knn.fit(X_train_std[:, k5], y_train)
>>> print('Training accuracy:', 
...        knn.score(X_train_std[:, k5], y_train))
Training accuracy: 0.959677419355
>>> print('Test accuracy:',
...        knn.score(X_test_std[:, k5], y_test))
Test accuracy: 0.962962962963

使用的少于 Wine 数据集中原始特征的一半,测试集的预测准确性提高了近 2%。 此外,我们减少了过拟合,这可以从测试(〜96.3%)与训练(〜96.0%)准确性之间的微小差距中看出。

注意

scikit-learn 中的特征选择算法

可以通过 scikit-learn 获得更多功能选择算法。 其中包括基于特征权重的递归后向消除,基于树的重要性选择方法以及单变量统计检验。 关于不同功能选择方法的全面讨论超出了本书的范围,但是可以在这个页面中找到带有说明性示例的出色摘要。 ]。

评估随机森林的特征重要性

在前面的中,您学习了如何使用 L1 正则化通过 Logistic 回归将不相关的特征归零,以及如何使用 SBS 算法进行特征选择。 从数据集中选择相关特征的另一种有用方法是使用随机森林,这是我们在第 3 章,“使用 Scikit-learn”的机器学习分类器中引入的一种集成技术。 使用随机森林,我们可以根据森林中所有决策树计算出的平均杂质减少量来衡量特征重要性,而无需假设我们的数据是否可线性分离。 方便地,scikit-learn 中的随机森林实现已经为我们收集了功能重要性,因此我们可以在拟合RandomForestClassifier之后通过feature_importances_属性访问它们。 通过执行以下代码,我们现在将在 Wine 数据集上训练一万棵树木的森林,并通过它们各自的重要性度量对 13 个要素进行排名。 请记住(根据我们在第 3 章,“使用 Scikit-learn 进行的机器学习分类器”的讨论),我们不需要使用标准化或标准化的基于树的模型。 代码如下:

>>> from sklearn.ensemble import RandomForestClassifier
>>> feat_labels = df_wine.columns[1:]
>>> forest = RandomForestClassifier(n_estimators=10000,
...                                random_state=0,
...                                n_jobs=-1)
>>> forest.fit(X_train, y_train)
>>> importances = forest.feature_importances_
>>> indices = np.argsort(importances)[::-1]
>>> for f in range(X_train.shape[1]):
...     print("%2d) %-*s %f" % (f + 1, 30, 
...                             feat_labels[indices[f]], 
...                             importances[indices[f]]))
1) Color intensity                0.182483
2) Proline                        0.158610
3) Flavanoids                     0.150948
4) OD280/OD315 of diluted wines   0.131987
5) Alcohol                        0.106589
6) Hue                            0.078243
7) Total phenols                  0.060718
8) Alcalinity of ash              0.032033
9) Malic acid                     0.025400
10) Proanthocyanins               0.022351
11) Magnesium                     0.022078

12) Nonflavanoid phenols           0.014645
13) Ash                            0.013916
>>> plt.title('Feature Importances')
>>> plt.bar(range(X_train.shape[1]), 
...         importances[indices],
...         color='lightblue', 
...         align='center')
>>> plt.xticks(range(X_train.shape[1]), 
...            feat_labels[indices], rotation=90)
>>> plt.xlim([-1, X_train.shape[1]])
>>> plt.tight_layout()
>>> plt.show()

在执行前面的代码之后,我们创建了一个图,根据其相对重要性对 Wine 数据集中的不同特征进行排名; 请注意,功能重要性已归一化,因此它们的总和为 1.0。

Assessing feature importance with random forests

我们可以得出结论,根据 10,000 个决策树中的平均杂质减少量,酒的颜色强度是数据集中最具区分性的特征。 有趣的是,上图中的前三个特征也在我们上一节中实现的 SBS 算法选择的前五个特征中。 但是,就可解释性而言,随机森林技术带有重要的陷阱,值得一提。 例如,如果两个或多个特征高度相关,则一个特征的排名可能很高,而其他特征的信息可能无法完全捕获。 另一方面,如果我们仅对模型的预测性能感兴趣,而对特征重要性的解释不感兴趣,则无需担心此问题。 总结本节有关特征重要性和随机森林的情况,值得一提的是,scikit-learn 还实现了transform方法,该方法在模型拟合后根据用户指定的阈值选择特征,如果要使用[ RandomForestClassifier作为功能选择器和 scikit-learn 管道中的中间步骤,这使我们可以将不同的预处理步骤与估算器连接起来,如我们在第 6 章,“评估和超参数调整,学习模型的最佳实践”中所见。 例如,我们可以使用以下代码将阈值设置为 0.15,以将数据集缩小为 3 个最重要的特征,即颜色强度脯氨酸类黄酮

>>> X_selected = forest.transform(X_train, threshold=0.15)
>>> X_selected.shape
(124, 3)

Assessing feature importance with random forests

Assessing feature importance with random forests