R-机器学习精要-全-

R 机器学习精要(全)

原文:annas-archive.org/md5/ab0f9114b761532dd330b0aa890f88d6

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

面对商业问题时,机器学习使你能够开发强大而有效的数据驱动解决方案。数据量和来源的最近爆炸性增长提高了基于数据的解决方案的有效性,因此这个领域变得越来越有价值。开发机器学习解决方案有特定的要求,并且有一些软件和工具支持它。一个非常好的选择是使用 R,这是一个由广泛的国际社区支持的统计开源编程语言。R 的结构是为统计分析而设计的,国际社区开发了最前沿的解决方案。因此,R 允许你仅用几行代码就开发出强大的机器学习解决方案。

有机器学习教程,它们通常需要一些对统计学和计算机科学基础知识的了解。这本书不仅仅是一个教程。它甚至不需要你在统计学或计算机科学方面有强大的背景。目标不是为你提供一个所有技术的完整概述,也不是教你如何构建复杂解决方案。这本书是一条充满实践例子的道路,为你提供构建新问题解决方案的专业知识。目的是以这种方式展示方法背后的最重要的概念,以便你对机器学习有深入的理解,能够识别和使用新的算法。

本书涵盖内容

第一章,将数据转化为行动,展示了新技术如何允许你以数据驱动的方式解决商业问题。

第二章,R – 开发机器学习算法的强大工具,解释了为什么 R 是机器学习的绝佳选择,并涵盖了软件的基础知识。

第三章,简单的机器学习分析,展示了机器学习解决方案的一个简单示例。

第四章,步骤 1 – 数据探索与特征工程,展示了在使用机器学习算法之前如何清洗和转换数据。

第五章,步骤 2 – 应用机器学习技术,展示了如何将机器学习算法应用于解决问题。

第六章,步骤 3 – 验证结果,展示了如何衡量算法的准确性以便调整其参数。

第七章,机器学习技术概述,介绍了机器学习算法的主要分支。

第八章,适用于商业的机器学习示例,展示了如何使用机器学习解决商业问题。

您需要为本书准备的东西

您运行代码所需的唯一软件是 R,最好是 3.0.0+。强烈推荐安装 RStudio Desktop IDE,尽管这不是必需的。

本书面向对象

本书旨在帮助那些想学习如何使用 R 进行一些机器学习的人,以便从他们的数据中获得洞察力并找到解决一些现实生活问题的方法。也许您已经对机器学习有所了解,但从未使用过 R,或者也许您对 R 有点了解,但对机器学习是新手。在两种情况下,这本书都会让您快速上手。对基本编程概念有一定的了解会有所帮助,但不需要有先前的经验。

惯例

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

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式如下所示:“加载包含 random forest 算法的 randomForest 包。”

代码块设置如下:

[default]
arrayFeatures <- names(dtBank)
arrayFeatures <- arrayFeatures[arrayFeatures != 'output']
formulaAll <- paste('output', '~')
formulaAll <- paste(formulaAll, arrayFeatures[1])
for(nameFeature in arrayFeatures[-1]){
  formulaAll <- paste(formulaAll, '+', nameFeature)
}
formulaAll <- formula(formulaAll)

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

n1 + n2
[1] 5
n1 * n2
[1] 6

新术语重要词汇以粗体显示。

注意

警告或重要注意事项以如下框中的形式出现。

小贴士

小贴士和技巧看起来是这样的。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大价值的标题。

要向我们发送一般反馈,请简单地发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果您在某个领域有专业知识,并且对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南 www.packtpub.com/authors

客户支持

现在,您已经成为 Packt 书籍的骄傲所有者,我们有一些东西可以帮助您从购买中获得最大收益。

下载示例代码

您可以从 www.packtpub.com 下载示例代码文件,适用于您购买的所有 Packt 出版物。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出中的变化。您可以从www.packtpub.com/sites/default/files/downloads/7740OS_coloredimages.PDF下载此文件。

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将在勘误部分显示。

引用和参考文献

  • 第四章,步骤 1 – 数据探索和特征工程,第五章,步骤 2 – 应用机器学习技术,第六章,步骤 3 – 验证结果,以及标志数据集:

    Bache, K. & Lichman, M. (2013). UCI 机器学习仓库 archive.ics.uci.edu/ml。Irvine, CA:加州大学信息与计算机科学学院。

  • 第八章,商业适用的机器学习示例,以及银行数据集:

    [Moro et al., 2014] S. Moro, P. Cortez 和 P. Rita. 预测银行电话营销成功的数据驱动方法。决策支持系统,Elsevier,62:22-31,2014 年 6 月

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过链接mailto:copyright@packtpub.com copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者和我们提供有价值内容的能力方面所提供的帮助。

问题

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

第一章. 将数据转化为行动

面对商业问题,我们需要知识和专业知识来找到解决方案。此外,我们还需要相关的数据,这些数据将有助于确定解决方案。本章展示了新技术如何使我们能够构建强大的机器,这些机器可以从数据中学习,为商业决策提供支持。

本章将涵盖以下主题:

  • 处理商业问题的一般思路

  • 与数字技术相关的新挑战

  • 新工具如何帮助使用信息

  • 工具如何识别不明显的信息

  • 工具如何估计未来事件的结果

  • 为什么选择 R?

商业决策中的数据驱动方法

专业知识和信息在商业决策中扮演着重要角色。本节展示了数据驱动技术如何改变面对挑战的方法并改进解决方案。

商业决策来源于知识和专业知识

随着时间的推移,处理商业问题的总体思路并没有改变,它结合了知识和信息。在采用数字技术之前,知识来源于以往经验和他人提供的专业知识。至于信息,它涉及分析当前情况并将其与过去事件进行比较。

一个简单的例子是一位水果商希望为其商品定价。产品的价格应该最大化利润,这取决于销售量和价格本身。这位经销商开始工作时与他的父亲一起工作,他的父亲为他们提供了所有的知识。因此,他们已经知道不同水果的价格。此外,在每天结束时,他们可以观察到每种水果的销售量。基于这一点,他们可以提高销售很好的水果的价格,降低未售出水果的价格。这个简单的例子展示了水果商如何结合领域知识和信息来解决他们的问题,如下面的图所示:

商业决策来源于知识和专业知识

这个简单的例子展示了简单挑战需要知识和数据的结合。

数字时代提供了更多的数据和专业知识

尽管处理商业问题的总体思路没有改变,但数字技术为我们提供了新的强大工具。

互联网允许人们相互连接并分享他们的专业知识,这样每个人都能访问到大量信息。在互联网之前,知识来源于可信赖的人和书籍。现在,信息的传播使得人们能够找到来自世界各地不同人的书籍和文章。此外,网站和论坛允许用户相互连接,以便分享专业知识并找到快速答案。

数字技术跟踪不同的活动并产生大量相关数据。我们谈论数据时指的是信息集——可以是定量的或定性的,可以被机器处理。因此,面对商业问题时,我们可以使用来自不同来源的大量数据。一些信息可能不太相关,但即使移除它们,我们通常仍然拥有大量数据。因此,我们在结果上有很多改进的潜力。

来自数字技术的变化涉及获取专业知识的过程和数据性质。因此,解决问题的方法提出了新的挑战。

面对商业问题的公司的一个简单例子是汽车经销商,他们销售不同品牌的二手车,并希望设定最相关的价格。汽车经销商应根据车型、年龄和其他特征来确定价格。这个例子旨在说明一种可能的情况,并不一定与真实问题相关。

汽车经销商需要确定每辆车的最佳价格,以便最大化收入。与水果商类似,如果汽车价格过高,汽车经销商在短时间内不会出售它,因此会产生额外的存储成本,汽车的价值也会下降。这会导致额外成本和利润下降,从而损害业务。另一方面,如果价格过低,公司会立即出售汽车。尽管存储成本较低,但公司并没有获得最佳利润。为了出售汽车并最大化利润,公司希望找出最佳价格。

让我们看看帮助找到解决方案的专业知识和信息。公司可以使用:

  • 已售出不同汽车的代理商的知识

  • 来自互联网的信息

  • 历史销售数据

代理商可以利用他们的过往经验,因此他们的知识有助于确定最佳价格。然而,当市场变化迅速时,仅凭经验设定价格是不够的。

互联网为我们提供了大量信息,因为有许多在线购物网站显示二手车的价格。在线购物与实体市场不同,但专家代理商可以查看网站并比较价格。这样,代理商可以将他们的专业知识与在线信息相结合,以良好的方式确定正确的价格。

这种方法可以取得良好的结果,但仍然不是最优的。查看不同的网站会耗费时间,尤其是当有众多汽车类别时,每天检查价格可能很困难,甚至不可能。另一个问题是可能存在许多网站,使得一个人无法处理所有信息。通过自动化我们的网络研究和更系统地使用数据,我们可以更快地获取信息。

要获取信息,数据来源是公司销售和在线市场,一个好的汽车定价解决方案应该考虑所有这些来源。公司销售数据显示了客户过去对其价格的反应。例如,我们知道过去每辆汽车的销售所需时间。如果时间过长,价格可能过高。这个标准是客观的,专家代理可以使用这些信息来识别当前错误的价格。

来自在线购物网站的数据显示汽车价格,我们可以使用能够存储价格和销售历史记录的工具。尽管这些信息与问题的相关性较低,但可以像处理公司销售数据一样进行处理,从而提高结果准确性,如下图所示:

数字时代提供更多数据和专业知识

这个例子展示了拥有更多信息和专业知识的好处。这里的挑战是如何以最合适的方式使用信息来改进解决方案。一般来说,我们使用的信息越多,潜在的结果越准确。在最坏的情况下,我们有很多不相关信息,但我们可以识别并使用其中一小部分相关部分。

技术连接数据和商业

只要数据能够被人类理解,一个人就可以通过结合数据和专业知识来解决商业问题。由于数字技术的发展,数据量的增长改变了处理问题的方法,因为更多的数据需要新的工具才能使用。此外,新设备使我们能够执行 10 年前在个人电脑上不可能进行的数据分析。

这个事实不仅改变了处理数据的方式,也改变了制定商业决策的整体流程。

有几种方法可以用来使用数据中的信息。例如,互联网电影流媒体提供商 Netflix 使用一个根据你的兴趣生成个性化电影推荐的工具。机器学习指的是从数据中学习以提供洞察和行动的工具,它是人工智能的一个子领域。机器学习技术不仅处理数据,而且连接数据和商业。这种信息和知识之间的交互至关重要,并影响构建解决方案的几乎每个步骤。

知识在构建识别解决方案的工具中仍然发挥着重要作用。由于有许多处理相同问题的机器学习工具,你可以利用你的专业知识来选择最相关的工具。此外,大多数工具都有一些参数,因此有必要了解问题来设置它们,如下图所示:

技术连接数据和商业

机器学习技术确定结果后,我们可以使用信息和专业知识来验证其性能。例如,在汽车经销商的例子中,我们可以构建一个工具,该工具可以自动识别最佳价格并预测每辆车的销售所需时间。从以前的数据开始,我们可以使用这个工具来估计销售汽车所需的时间,并将估计时间与实际时间进行比较。此外,我们可以识别当前价格,并使用知识和专业知识来判断它们是否合理。这样,我们可以比较机器学习方法与现实之间的相似性。

验证有助于比较不同的技术并选择表现最佳的技术。此外,技术通常需要设置不同的选项,而验证有助于选择最合适的选项,如下面的图示所述:

技术连接数据和商业

总结来说,机器学习与商业之间的互动极其重要,并且贯穿于构建解决方案的每个环节。

识别隐藏模式

数据显示了一些明显的信息,它还包含大量更隐含的信息。有时,解决商业问题需要一些不那么明显的信息,这些信息可能部分是主观的。本节展示了某些机器学习技术如何从数据中发现隐藏的结构和模式。

数据包含隐藏信息

跟踪活动的数据包含与技术设备相关的信息。例如,在超市中,收银机跟踪购买行为。因此,可以获取有关每个商品过去销售的一些信息。可用的信息是销售点POS)数据,它通过以下属性显示交易:

  • 项目 ID

  • 已售出单位数量

  • 商品价格

  • 购买日期和时间

  • 收银机的 ID

  • 客户 ID(适用于使用 Nectar 卡的客户)

通过分析数据,一些信息会显现出来,并且很容易获取,而另一些信息则隐藏起来。从交易开始,很容易确定过去销售的总金额。例如,我们可以计算一天内销售了多少个产品单位。这样做非常简单:

  1. 根据产品 ID 和日期选择交易。

  2. 添加单位数量。

仍然可以获取一些稍微详细一些的信息。我们可以将商品分为部门,并且根据每个部门上一年销售的总单位数,我们可以:

  1. 为每个部门生成产品 ID 列表。

  2. 对于每个部门,选择上一年的交易和该部门的商品 ID。

  3. 添加单位数量。

有可能提取关于过去整体销售的任何其他类型的信息。如果分析的目标是顾客而不是销售呢?

我们可以使用客户 ID 来跟踪每位顾客的购买情况。例如,给定一个单独的客户 ID,我们可以确定他们购买的总单位数。这些数据仍然容易获得,所以我们不能谈论隐藏模式。然而,关于顾客的信息仍然有很多不能直接显示。

一些顾客有相似的客户习惯。客户类别的例子包括:

  • 学生

  • 家庭主妇

  • 老年人

每个人群都表现出一些特定的购买习惯,如下所示:

  • 可用的资金

  • 顾客感兴趣的 产品

  • 购买日期和时间

例如,学生平均可支配的资金比其他人少。妈妈们更愿意购买杂货和家居产品。学生更可能在放学后去超市;老年人几乎在任何时间都会去。

数据没有显示哪些顾客 ID 与每个顾客类别相关联,即使它包含一些关于他们行为的信息。然而,要识别哪些顾客相似以执行简单的分析操作是很困难的。此外,为了识别群体,我们需要对顾客类别有一个初始猜测。

商业问题需要隐藏信息

一个商业问题可能需要一些隐藏信息。在超市的例子中,我们希望针对某些顾客群体进行临时的营销和折扣活动。

营销活动的选项决定了以下内容:

  • 哪些商品做广告

  • 哪些商品打折

  • 折扣

  • 哪些工作日受到促销的影响

如果超市非常小,就有可能提取每位顾客的数据,并因此针对他们进行特定的营销活动。然而,超市很大,顾客众多,所以如果不使用数据处理,将无法单独考虑每一位顾客。

一种可能性是定义一种自动读取每位顾客数据并相应选择营销活动的方法。这种方法需要以下条件:

  • 组织数据和选定的信息

  • 数据建模

  • 定义行为

这种方法有效,尽管它有一些缺点。关于营销活动的决策需要关于客户基础的总体情况。在理解了顾客行为模式之后,可以定义一种方法,从顾客行为出发选择营销活动。因此,这种方法需要一些前期分析。

另一种解决方案是识别具有相似习惯的顾客群体。一旦定义了这些群体,就可以单独分析每个群体,以了解其共同的购买行为。

下面的图表显示了由小圆圈表示的一些顾客,其中大圆圈代表顾客的同质群体:

商业问题需要隐藏信息

这样,超市对每个群体都有一些信息,有助于他们通过结合以下信息来识别正确的营销活动:

  • 关于该群体顾客的一些汇总信息

  • 一些业务知识,使他们能够定义适当的营销活动

假设每个顾客在未来的习惯将相同,至少在短期内,可以识别每个顾客群体的购买行为和兴趣,并相应地对它们进行相同的营销活动。

重新塑造数据

从 POS 数据开始,我们想要模拟超市顾客的购买习惯,以便识别同质群体。尽管 POS 数据不直接显示顾客行为,但它包含顾客 ID。每个顾客的行为可以通过测量他们的习惯来模拟。例如,我们可以测量他们在过去几年中购买的单位总数。同样,我们可以定义一些其他关键绩效指标(KPIs),这些指标是描述行为不同方面的值。在提取与顾客相关的所有交易后,我们可以定义以下 KPIs:

  • 他们上一年购买的单位总数

  • 他们上一年度的总消费金额

  • 在晚上 6 点至 7 点之间购买的单位百分比

  • 在特定商品部门的总消费金额

  • 在夏季花费的百分比

选择 KPIs 有不同的选项,并且它们应该与问题相关。在我们的例子中,我们想要确定顾客可能对哪些产品感兴趣。

与问题相关的某些关键绩效指标(KPIs)如下:

  • 上一年度的总消费金额,以确定顾客可能花费的最大金额

  • 在不同商品部门的消费百分比,以确定顾客的兴趣所在

  • 早上和上午早些时候购买的百分比,以确定家庭主妇和退休人员

对于一小群顾客,通过观察数据很容易识别同质群体。然而,如果我们有众多顾客和/或 KPIs,我们需要计算工具来揭示数据中的隐藏模式。

使用无监督学习识别模式

有些机器学习算法可以识别隐藏的结构,这一技术分支被称为“无监督学习”。从数据开始,无监督学习算法识别出没有直接显示的模式和标签。

在我们的例子中,我们使用一组适当的 KPIs 来模拟客户,这些 KPIs 描述了他们的购买行为。我们的目标是识别具有相似 KPIs 值的群体。

为了关联客户,第一步是衡量他们之间的相似度。观察两位客户的资料,我们可以看到,如果他们的关键绩效指标(KPIs)值相似,那么他们就是相似的。由于客户众多,我们无法手动观察数据,因此我们需要定义一个标准。这个标准是一个函数,它接受两个客户的 KPIs 作为输入,并计算一个距离,这是一个表示值之间差异的数字。这样,就有了一个客观的方式来陈述两个客户是如何相似的。

我们通过对象来模拟客户,其相似性可以衡量。有几种机器学习算法可以将相似的对象分组,它们被称为聚类技术。这些技术将相似的客户分组在一起,从而识别出同质群体。

根据以下因素,有不同选项来分组客户:

  • 希望的聚类数量

  • 每个 KPI 的相关性

  • 识别聚类的途径

聚类有多种选择,大多数算法都包含一些参数。为了选择合适的技术和设置,我们需要探索数据以理解商业问题。

本章仅是一个介绍性章节,聚类只是无监督学习的一个例子。

使用无监督学习进行商业决策

聚类技术使我们能够识别同质客户群体。对于每个聚类,超市必须定义一个针对其客户的营销活动,使用促销和折扣。

对于每个聚类,可以定义一个总结表,显示平均客户的行为。结合这些信息以及一些商业专业知识,超市可以最大化活动的积极影响。

总之,聚类使我们能够将大量数据转换为少量相关信息。然后,商业专家可以阅读和理解聚类结果,以做出最佳决策。

这个例子展示了数据和专业知识是如何紧密相连的。机器学习算法需要使用商业专业知识定义的 KPIs。在算法处理完数据后,商业专业知识是必要的,以确定正确的行动。

估计行动的影响

当商业决策包括在多个选项之间进行选择时,解决方案需要估计每个选项的影响。本章展示了机器学习技术如何根据选项预测未来事件,以及我们如何衡量准确性。

商业问题需要预测未来事件

如果我们必须在多个选项之间做出选择,我们将评估每个替代方案的影响,并选择最佳方案。为了说明这一点,例子是一个计划开始销售新商品的超市,其商业决策包括选择其价格。

为了选择最佳价格,公司需要知道:

  • 价格选项

  • 每个价格选项对商品销售的影响

  • 每个价格选项对其他商品销售的影响

理想解决方案是最大化短期和长期的整体收入影响。就商品本身而言,如果其价格过高,公司不会销售它,从而错失潜在利润。另一方面,如果价格过低,公司将以较差的收入销售不同数量的商品。

此外,新商品的价格将对类似商品的销售产生影响。例如,如果超市正在销售一种新谷物,所有其他谷物产品的销售都将受到影响。如果新价格过低,一些购买其他商品的顾客可能会想省钱,从而购买新商品。这样,一些顾客将花费更少的钱,整体收入将下降。相反,如果新商品定价过高,顾客可能会认为其他商品太便宜,从而认为其质量较低。

有不同的影响,一个选项是在第一步中定义商品的最低价和最高价,以避免对相关商品销售的负面影响。然后,我们可以选择新价格,最大化商品本身的收入。

收集数据以从中学习

假设我们已经为新商品定义了最低价和最高价。目标是利用数据来发现允许我们最大化商品收入的信息。收入取决于:

  • 商品的定价

  • 下一个月将要售出的单位数量

为了最大化收入,我们希望根据价格进行估计,并选择最大化收入的价格。如果我们可以根据价格估计销售量,我们就可以相应地估计收入。

数据显示了过去交易的信息,包括:

  • 商品编号

  • 日期

  • 在一天内售出的单位数量

  • 商品的定价

还有其他映射商品的数据,包括一些特征。为了简化问题,所有特征都是分类的,因此它们显示的是类别而不是数字。以下是一些特征的例子:

  • 部门名称

  • 产品名称

  • 品牌名称

  • 定义商品的其他分类特征

我们没有关于新物品销售的数据,因此我们需要使用一些相似物品的数据来估计客户行为。我们假设:

  • 未来客户的消费行为与过去相似

  • 相似物品间的客户行为相似

  • 新物品的销售不受其是新上市产品这一事实的影响

由于我们想要估计新物品的销售量,起点是相似物品的销售量。对于每个物品,我们提取其上个月的交易数据,并计算:

  • 上个月的总单位数量

  • 上个月最常见的价格

此外,对于每个物品,我们都有定义其特征的数据。每个物品的数据是估计收入的基础。

使用监督学习预测未来结果

在我们的问题中,机器学习算法的目标是根据物品的价格预测其销售量。从数据中学习以预测未来事件的技术的分支被称为监督学习。算法的起点是一个包含已知事件的对象的训练数据集。算法识别描述对象和事件的数据之间的关系。然后,它们构建一个定义这种关系的模型,并使用该模型对其他对象上的事件进行预测。监督学习和无监督学习之间的区别在于,监督学习技术使用已知事件的训练,而无监督学习技术识别隐藏的模式。

例如,我们有一个新物品,其价格可以是 2 美元、3 美元或 4 美元。为了确定最佳价格,我们需要估计未来的收入。

数据显示了任何物品的销售量,这取决于其价格和特征。估计新物品销售量的方法,根据其价格,是使用定义数量(k)的最多相似物品的销售量。对于每个价格,步骤如下:

  1. 定义哪些是具有最多相似度的k个物品,给定新物品的特征和价格。

  2. 定义如何使用相似物品的数据来估计新物品的销售量。

为了识别最相似的物品,我们必须决定什么是“相似”以及如何衡量“相似”。为了做到这一点,我们可以定义一种方法来衡量任何两个物品之间的相似度,这取决于特征和价格。相似度可以通过距离函数来衡量,考虑以下特征:

  • 价格差异

  • 同一产品

  • 同一品牌

  • 同一部门

  • 其他相似特征

一种简单的方法是将距离测量为特征之间的不相似性之和。例如,一个非常简单的不相似性可以是价格差异加上显示不同值的分类特征的数目。一种稍微更有优势的方法是根据其相关性为每个特征赋予不同的权重。例如,不属于同一部门的两项非常不相似,而属于同一产品但品牌不同的两项则非常相似。

在定义了距离函数之后,我们希望根据价格识别最相似的 k 个对象。对于每个价格点,我们定义一个具有新物品特征和所选价格的项目。然后,对于超市中的每个项目,我们计算项目与新物品之间的距离。这样,我们可以选择距离最低的 k 个项目。

在确定了最相似的 k 项之后,我们需要确定如何利用这些信息来估算新的体积。一种简单的方法是计算 k 项的销售量的平均值。一种更高级的方法是给予更相似的项目更高的权重。

依赖于过去数据的未来事件估计技术被称为监督学习技术。所展示的算法是k 近邻KNN)算法,它是最基本的监督学习技术之一。

摘要

本章展示了如何通过结合专业知识和信息来面对商业问题。您看到了数字技术如何导致信息量的增加,并为我们提供了面对挑战的新技术。您对机器学习技术的两个最重要的分支有了概述:无监督学习和监督学习。无监督学习技术识别数据中隐藏的一些结构,而监督学习技术则使用数据来估计未知情况。

下一章将展示与机器学习问题相关的挑战,并定义识别其解决方案的软件的要求。然后,该章节介绍了本书中将使用的软件,并提供了一个简要教程。

第二章. R – 开发机器学习算法的强大工具

在使用机器学习技术之前,我们需要选择合适的软件。有许多编程语言和软件类型为用户提供机器学习工具。实际上,最重要的部分是知道使用哪些技术以及如何构建新的技术,而软件只是工具。然而,选择正确的软件可以使你构建更快、更准确的解决方案。

在本章中,你将学习:

  • 构建机器学习解决方案的软件要求

  • 如何 R,结合 RStudio,有助于开发机器学习解决方案

  • R 的结构

  • R 的工具

  • 一些重要的 R 包

为什么选择 R

理解开发机器学习解决方案的挑战有助于选择能够以最简单和最有效的方式面对这些挑战的软件。本章说明了软件要求并解释了为什么我们将使用 R。

机器学习的交互式方法

开发机器学习解决方案包括具有不同要求的步骤。此外,一个步骤的结果有助于改进之前的步骤,因此通常需要返回修改它。

在面对问题并构建其机器学习解决方案之前,我们希望尽可能多地了解挑战和可用资源。实际上,为了定义通往解决方案的正确路径,拥有所有可能的信息非常重要。为此,从数据开始,我们使用统计和机器学习工具从数据中提取业务洞察和模式。

一个简单的例子是一个大型超市推出针对一些特定顾客的新营销活动。可用的数据是过去销售的交易记录。在构建任何机器学习技术之前,我们需要一些基本信息,例如前一年的总客户数量和总销售额。在知道总客户数量后,我们希望确定平均客户的年度支出。然后,下一步可以是根据同质购买习惯将客户分组,并计算每个组的平均客户年度支出。

在提取一些基本信息之后,我们对问题的概述将更加详细,并且经常会提出新的问题。因此,我们需要通过应用其他统计和机器学习模型来识别新的模式和提取新的洞察。这个过程将持续进行,直到信息使我们能够确定最终的机器学习解决方案。

对于问题的解决方案,通常有多种选择。为了选择最合适的一个,我们可以构建其中的一些并比较它们的结果。此外,大多数算法都可以调整以提高其性能,而调整取决于结果。

总之,构建机器学习解决方案包括与彼此紧密相关的不同步骤。新步骤的目标基于对前一步的分析,有时,一个步骤会根据后续结果进行修改。没有一条从起点到终点的明确路径,软件应允许这一点。

机器学习的交互式方法

机器学习软件的期望

对于机器学习软件,有多种选择,本节展示了我们对选择的期望。软件应同时为用户提供机器学习工具,并允许构建特定的解决方案。

最重要的机器学习技术由不同类型的软件和包提供。然而,使用尖端技术可以改进解决方案。大多数机器学习算法都是由学术界开发的,用于研究,因此它们进入商业领域需要时间。此外,除了少数例外,公司没有足够的资源来开发先进的技术。因此,软件应允许用户访问学术界开发的工具。在免费和开源软件的情况下,通常有一个国际社区为用户提供包含尖端工具的许多包。

另一个软件需求是允许用户开发快速有效的解决方案。解决机器学习问题需要大量的交互,即用户经常根据结果修改解决方案。一个良好的用户友好的图形包对于探索每一步的结果并确定下一步要做什么非常重要。因此,该工具应允许用户快速构建可重用的组件,用于数据探索、处理和可视化。

总之,软件需求包括:

  • 机器学习工具

  • 图形包

  • 组件的可重用性

R 和 RStudio

我们将使用的软件是 R,本小节解释了原因。

R是一种专为数据分析和学习设计的编程语言。它是一种解释型语言,因为它直接执行命令,所以比其他编程语言更易于使用。尽管与一些商业软件相比,它的学习曲线更陡峭,但与其他编程语言相比,R 更容易学习。

R 是最受欢迎的统计编程语言,有一个庞大的国际社区支持它。它的存储库(CRAN)包含超过 5000 个包含统计和机器学习工具的包。这样,我们可以使用其国际社区提供的最尖端工具。

其他有用的 R 工具是其图形包,它允许仅使用几行代码就生成漂亮且专业的图表。这样,在解决方案开发过程中探索数据和结果变得非常容易。

R 的另一个优点是RStudio,这是一个为 R 项目设计的 IDE。RStudio 包括交互式控制台和用于访问 R 帮助、可视化/保存图表以及调试的工具。R 与 RStudio 结合使用,使用户能够相对快速地开发强大的机器学习解决方案。

R 教程

我假设你已经熟悉一种编程语言,尽管不一定是 R。本节包含一个简短的 R 教程,展示了构建机器学习解决方案时有用的工具。由于对 R 的适当介绍需要整本书,这个教程只关注一些相关主题。

如果你已经熟悉 R,你可以快速阅读这一部分。如果你是 R 的新手,我建议你结合这个部分和一个交互式在线教程,以获得更全面的了解。此外,玩转这些工具以获得更多熟悉感将非常有用。

在开始教程之前,我们需要安装 R 和 RStudio。这两种软件都是开源的,并且支持大多数操作系统。阅读 RStudio 教程以了解如何使用这个强大的 IDE 也是很有用的。

我的建议是在 RStudio 环境中生成一个新的 R 脚本,并将代码复制粘贴到脚本中。你可以通过转到特定的命令行并按Ctrl + Enter来运行命令。

R 的基本工具

R 的基本结构非常简单。任何类型的变量都存储在可以通过输入其名称来可视化的对象中。让我们开始定义一些数字:

n1 <- 2
n2 <- 3

我们可以通过输入其名称来可视化一个对象,如下所示:

n1
[1] 2

我们可以在对象上执行一些基本操作:

n1 + n2
[1] 5
n1 * n2
[1] 6

任何操作的输出都可以存储在另一个对象中:

nSum <- n1 + n2
nProd <- n1 * n2
nSum
[1] 5

添加注释到代码的标准语法是在行首加上一个井号,如下所示:

# we performed some basic operations on the numbers

我们可以将 R 函数应用于对象,并且语法很简单,因为参数始终在括号内:result <- functionName(argument1, argument2, …)

例如,我们可以使用sum来计算数值变量的总和:

sum(2, 3)
[1] 5
sum(2, 3, 4)
[1] 9

同样,对于运算符,我们可以将函数的输出存储到另一个对象中,如下所示:

s1 <- sum(2, 3)
s2 <- sum(2, 3, 4)

还有打印控制台消息的函数。例如,对于任何对象,print都会以相同的方式显示其内容,即只需输入对象名称:

print(s1)
[1] 5

定义新函数所使用的语法很简单。例如,我们可以定义一个函数funProd,它计算其两个参数的乘积:

funProd <- function(n1, n2)
{
  n <- n1 * n2
  return(n)
}

n1n2输入在括号内定义,操作包含在大括号内。return方法终止函数,并将结果作为输出。我们只需输入其名称即可在任意函数中可视化代码。

为了跟踪函数的执行情况,我们可以在函数执行时打印变量,如下所示:

funProdPrint <- function(n1, n2){
  n <- n1 * n2
  print(n1)
  print(n2)
  print(n)
  return(n)
}
prod <- funProdPrint(n1 = 2, n2 = 3)
[1] 2
[1] 3
[1] 6

提示

下载示例代码

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

与文档相关联的不同 R 函数。我们可以使用help来显示它们的描述,如下所示:

help(sum)

另一个选项是使用sum,但就我个人而言,我更喜欢使用help,以便使用与其他 R 函数相同的括号语法。

为了执行基本的 R 操作,我们必须将数据存储在向量中,这些向量是包含值排序集合的对象。我们可以使用c定义一个新的向量,这是一个将输入连接起来的函数,如下所示:

a1 <- c(1, 2, 3)
a1
[1] 1 2 3

我们可以使用方括号提取向量的一个元素。将1放在方括号内可以提取第一个元素。请注意,R 的索引与其他编程语言(如 Python)不同,Python 中第一个元素的索引为0,而不是1,如下所示:

a1[1]
[1] 1

我们可以通过在方括号内放置一个向量来同时提取多个元素,如下所示:

a1[c(1, 2)]
[1] 1 2

我们也可以对向量执行一些基本操作:

a sPaste <- paste(s1, s2, sep = '_')
1 + 1
[1] 2 3 4
a2 <- c(1, 2, 3)
a1 + a2
[1] 2 4 6

如果我们要定义一个包含整数序列的向量,我们可以使用此表示法:

a3 <- 1:10
a3
[1]  1  2  3  4  5  6  7  8  9 10

向量可以包含未定义的值,在 R 中为NA

a4 <- c(1, NA, 2)

如果我们对NA值执行操作,输出将是NA

另一个重要的数据元素是布尔值。布尔变量使用TRUEFALSE定义,基本运算符是&&&(AND)、|||(OR),以及!(NOT)。布尔元素可以是单个元素或向量。在向量的情况下,短形式(&|)比较每个元素,而长形式(&&||)仅评估每个向量的第一个元素,如下所示:

bool1 <- TRUE
bool2 <- FALSE
bool3 <- bool1 & bool2
bool4 <- bool1 | bool2
bool5 <- !bool1
bool3
[1] FALSE
bool4
[1] TRUE
bool5
[1] FALSE

我们可以使用关系运算符定义布尔变量,如==(等于)、!=(不等于)、<=(小于或等于)、>=<>

x1 <- 1
x2 <- 2
bool5 <- x1 == x2
bool6 <- x1 != x2
bool7 <- x1 <= x2
bool5
[1] FALSE
bool6
[1] TRUE
bool7
[1] FALSE

布尔变量可以包含在由if定义的if语句中,其语法与函数类似。我们将条件放在括号内,操作放在花括号内,如下所示:

if(bool5){
  x <- 1
}else{
  x <- 2
}
x
[1] 2

我们可以使用for和它们的语法定义for循环,与if的语法相同。括号包含变量名和值向量,花括号包含操作,如下所示:

vectorI <- c(1, 2, 5)
x <- 0
for(i in vectorI)
{
  if(i > 1)
  {
    x <- x + i
  }
}
x
[1] 7

如果我们要对一个操作进行固定次数的重复,我们可以定义一个包含前n个整数的向量:

nIter <- 10
vectorIter <- 1:nIter
total <- 0
for(i in vectorIter){
  total <- total + 1
}
total
[1] 10

本小节展示了 R 的一些基本组件。下一小节将介绍用于分析数据的 R 对象。

理解基本的 R 对象

有不同种类的对象,我们已经看到了其中的一些:numericfunctionbooleanvector。我们可以轻松地识别前例中使用的对象的类别。

考虑以下示例:

class(n1)
[1] "numeric"
class(funProd)
[1] "function"
class(bool5)
[1] "logical"
class(a1)
[1] "numeric"

a1向量属于numeric类别,因为它的所有元素都是数值。同样,一个包含逻辑元素的向量属于logical

字符串使用单引号或双引号定义,如下所示:

s1 <- 'string1'
s2 <- "string2"

有不同的字符串函数,如paste,它连接两个字符串,以及substring,它从字符串中提取子集,如下所示:

sPaste <- paste(s1, s2, sep = '_')
sPaste
[1] "string1_string2"
sSub <- substring(sPaste, 2, 5)
sSub
[1] "trin"

可以像定义numericlogical一样定义一个string向量:

vectorStrings <- c(s1, s2, sPaste, sSub)
vectorStrings
[1] "string1"         "string2"         "string1_string2" "trin"
class(vectorStrings)
[1] "character"

一个向量可以包含任何类型的对象(甚至可能是函数)。如果我们定义一个包含字符串和数字的向量会发生什么?

vectorStringsNum <- c(s1, s2, 10, 1.3)
vectorStringsNum
[1] "string1" "string2" "10"      "1.3" 
class(vectorStringsNum)
[1] "character"

如前述代码所示,R 将数字转换为字符,以便有一个同质的向量。然而,还有其他数据结构允许我们存储异质对象。

如果我们有分类变量,我们可以使用字符串来存储它们,但还有一个选项:factors。这个 R 对象包含一个变量,其值属于一个已定义的值集合,称为levels。每个层级都与一个整数相关联,数据可以被视为整数或字符,以获得相同的结果。因子还可以帮助创建有序变量。

从一个字符串开始,我们可以使用factor生成因子:

vectorString <- c('a', 'a', 'b', 'c')
vectorFactor <- factor(vectorString)
class(vectorFactor)
[1] "factor"

使用层级,我们可以识别可能的值:

levels(vectorFactor)

另一个有用的函数,尽管不一定与因子相关,是table,它计算每个层级的出现次数:

table(vectorFactor)
vectorFactor
a b c 
2 1 1 

另一个有用的数据元素是Date,它是 R 中用于存储日期的选项之一。我们开始构建一个如'2013-01-01'的字符串,并在另一个字符串中定义年、月和日的位置,如下所示:

stringDate <- '2013-01-01'
formatDate <- '%Y-%m-%d'

现在,使用as.Date,我们可以生成日期对象:

date1 <- as.Date(stringDate, format = formatDate)
class(date1)
[1] "Date"
date1
[1] "2013-01-01"

我们可以在日期上应用简单的操作,例如添加一定数量的天数:

date2 <- date1 + 10
date2
[1] "2013-01-11"

我们还可以使用布尔运算符来匹配两个日期:

date1 > date2
[1] FALSE

另一种数据类型是list,它定义了一个有序的异质数据元素集合:

l1 <- list(1, a1, sPaste)
l1
[[1]]
[1] 1

[[2]]
[1] 1 2 3

[[3]]
[1] "string1_string2"

每个对象都可以与一个键相关联,这允许我们访问它:

l2 <- list(elNumber = 1, elvector = a1, elString = sPaste)
l2
$elNumber
[1] 1

$elVector
[1] 1 2 3

$elString
[1] "string1_string2"

在这两种情况下,我们可以使用双方括号和元素的索引从列表中提取一个元素,如下所示:

l1[[1]]
[1] 1
l2[[1]]
[1] 1

l2的情况下,我们已定义其键,因此我们可以使用$运算符来访问其元素:

l2$elNumber
[1] 1

我们可以使用names来可视化所有键名:

names(l2)
[1] "elNumber" "elVector"  "elString"

还可以定义或更改键名:

names(l1) <- c('el1', 'el2', 'el3')
names(l1)
[1] "el1" "el2" "el3"

为了从一个列表中提取子列表,我们可以使用单方括号,类似于向量:

l3 <- l2[1]
l3
$elNumber
[1] 1
l4 <- l2[c(1, 2)]
l4
$elNumber
[1] 1

$elVector
[1] 1 2 3

一个允许你存储表格数据的 R 对象是matrix。要生成一个新的矩阵,将所有值放入一个向量中,并使用matrix,如下所示:

vectorMatrix <- c(1, 2, 3, 11, 12, 13)
matrix1 <- matrix(vectorMatrix, ncol = 2)
matrix1
 [,1] [,2]
[1,]    1   11
[2,]    2   12
[3,]    3   13

使用t,我们可以转置矩阵,这意味着我们可以交换行和列:

matrix2 <- t(matrix1)
matrix2
 [,1] [,2] [,3]
[1,]    1    2    3
[2,]   11   12   13

如前述代码所示,matrix1matrix2 只包含数值数据。使用 cbind,我们可以添加另一列。如果我们添加一个字符列会发生什么?

vector3 <- c('a', 'b', 'c')
matrix3 <- cbind(matrix1, vector3)
matrix3

R 将数字转换为字符。原因是矩阵,就像向量一样,只能包含同质数据。

矩阵可以具有行和列名,我们可以使用 rownamescolnames 来显示它们:

rownames(matrix3)
NULL
colnames(matrix3)
[1] ""       ""       "vector3"

我们通过向 matrix3 添加一列来定义 matrix3。它的列名和 R 自动将最后一列名设置为向量名,即 vector3。使用相同的函数,我们可以手动设置行和列名,如下所示:

rownames(matrix3) <- c('row1', 'row2', 'row3')
colnames(matrix3) <- c('col1', 'col2', 'col3')
matrix3
 col1 col2 col3
row1 "1"  "11" "a" 
row2 "2"  "12" "b" 
row3 "3"  "13" "c" 

我们可以使用 View 来可视化数据框:

View(df2)

小贴士

对于数据框,请参阅本章的脚本。

有一些函数允许对向量、矩阵或列表的每个元素执行相同的操作。这些函数如下:

  • apply:对矩阵的每一行、列或元素应用函数

  • sapply:对向量的每个元素应用函数

  • lapply:对列表的每个元素应用函数

sapply 函数是最简单的,因此我们可以从它开始。我们可以定义一个向量 x1,包含介于 110 之间的整数,以及一个函数 func1,该函数返回输入的平方:

x1 <- 1:10
func1 <- function(el){
  result <- el ^ 2
  return(result)
}

现在,我们可以通过指定参数来使用 sapplyX—数组,FUN—函数:

sapply(X = x1, FUN = func1)
[1]   1   4   9  16  25  36  49  64  81 100

类似地,我们可以使用 lapply

l1 <- list(a = 1, b = 2, c = 3)
lapply(X = l1, FUN = func1)
$a
[1] 1

$b
[1] 4

$c
[1] 9

执行矩阵操作的函数是 apply。它可以用来对每一行应用相同的函数。让我们首先定义一个矩阵:

matrix4 <- matrix(1:9, nrow = 3)
matrix4
 [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6    9

为了将 sum 函数应用于每一行,我们使用 apply,定义 MARGIN 输入等于 1,指定我们在每一行上执行操作:

apply(X = matrix4, MARGIN = 1, FUN = sum)
[1] 12 15 18

定义 MARGIN = 2,我们对每一列执行操作:

apply(X = matrix4, MARGIN = 2, FUN = sum)
[1]  6 15 24

我们可以使用 MARGIN = c(1, 2) 将函数应用于矩阵的每个元素:

apply(X = matrix4, MARGIN = c(1, 2), FUN = func1)
 [,1] [,2] [,3]
[1,]    1   16   49
[2,]    4   25   64
[3,]    9   36   81

本节展示了与机器学习分析相关的某些 R 对象和工具。然而,它们仍然只是基础知识。

R 的标准是什么?

有一些风格规则可以使代码干净、标准化,本小节展示了其中的一些。

与其他编程语言不同,R 不需要任何缩进。然而,缩进代码可以使代码更易读、更整洁。R 的标准是使用两个空格,RStudio 自动定义这种缩进。

分配变量的标准是使用 <- 操作符,即使可以使用 = 来使 R 更接近其他编程语言。然而,如果在使用函数输入时使用,这两个操作符有不同的含义。

标识符有不同的选项,并且我个人喜欢小驼峰命名法:

lowerCamelCase

然而,R 社区非常大,有不同的惯例。

每个操作符都应该由空格包围,并且在函数输入中,应该在逗号后始终有一个空格:

x <- 1
sum(1, 2)

有一些其他的样式规则,你可以在google-styleguide.googlecode.com/svn/trunk/Rguide.xml找到它们。

一些有用的 R 包

有不同的 R 包为用户提供通用函数和特定技术。本章介绍了两个强大的通用包:data.tableplyr

一些包已经安装在了 R 的基本版本中。然而,为了使用data.tableplyr,我们需要从官方 CRAN 仓库使用install.packages下载它们。让我们从data.table开始,这是一个提供用于处理数据框的额外工具的包:

install.packages('data.table')

如果命令不起作用,你可以指定仓库:

install.packages(
  pkgs = 'data.table',
  repos = 'http://cran.us.r-project.org'
)

在安装包之后,我们需要加载它才能使用其函数。不幸的是,R 会在不使用命名空间的情况下导入包中的所有函数,有时可能会在不同包之间出现名称冲突:

library(data.table)

包含了一个名为data.table的新类,它继承自data.frame。继承意味着如果未被覆盖,数据表可以使用所有数据框工具,以及其他工具。

为了使用这个包,起点是我们将要分析的 dataset。R 为用户提供了一些 datasets,我们可以使用data查看它们的列表和描述:

data()

我们将要使用的数据集是iris。尽管它是一个非常标准的教程数据集,但我决定使用它,因为它很好地展示了数据表工具。我保证在接下来的章节中我会选择更有趣的主题。首先,让我们读取数据描述:

help(iris)

数据集包含了关于三种鸢尾花物种的数据:setosaversicolorvirginica。数据显示了每个花朵的萼片和花瓣的长度和宽度。

iris数据集是一个数据框。首先,让我们使用data.table将其转换为数据表:

class(iris)
[1] "data.frame"
dtIris <- data.table(iris)
class(dtIris)
[1] "data.table" "data.frame"

dtIris对象属于data.tabledata.frame类,因为继承。在分析数据之前,我们可以使用str快速探索其结构:

str(dtIris)
Classes 'data.table' and 'data.frame':  150 obs. of  5 variables:
 $ Sepal.Length: num  5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
 $ Sepal.Width : num  3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
 $ Petal.Length: num  1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
 $ Petal.Width : num  0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
 $ Species     : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
 - attr(*, ".internal.selfref")=<externalptr> 

如我们所见,有四个显示花朵属性的数值列和一个显示物种的因子列。现在,使用print,我们可以显示dtIris中包含的数据:

print(dtIris)
 Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
 1:          5.1         3.5          1.4         0.2    setosa
 2:          4.9         3.0          1.4         0.2    setosa
 3:          4.7         3.2          1.3         0.2    setosa
 4:          4.6         3.1          1.5         0.2    setosa
 5:          5.0         3.6          1.4         0.2    setosa
 --- 
146:          6.7         3.0          5.2         2.3 virginica
147:          6.3         2.5          5.0         1.9 virginica
148:          6.5         3.0          5.2         2.0 virginica
149:          6.2         3.4          5.4         2.3 virginica
150:          5.9         3.0          5.1         1.8 virginica

现在,我们可以看到前五行和最后五行。为了看到整个表,我们可以使用View

View(dtIris)

在查看数据之后,让我们看看基本操作。方括号允许我们执行一系列操作。例如,通过在方括号中放入一个数字,我们可以提取相关的行:

dtIris[1]
 Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1:          5.1         3.5          1.4         0.2  setosa

通过放入一个向量,我们可以提取更多的行:

dtIris[1:3]
 Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1:          5.1         3.5          1.4         0.2  setosa
2:          4.9         3.0          1.4         0.2  setosa
3:          4.7         3.2          1.3         0.2  setosa

如果我们要提取一个列,我们将列名作为第二个参数插入:

dtIris[, Species]
[1] setosa     setosa     setosa     setosa     setosa     setosa 
…
[145] virginica  virginica  virginica  virginica  virginica  virginica 
Levels: setosa versicolor virginica

而不是使用列名,我们也可以使用列的位置编号,在这个例子中是 5。我们也可以同时提取行和列:

dtIris[1:3, Species]
[1] setosa setosa setosa
Levels: setosa versicolor virginica

如果我们想要定义一个只包含前三个列的数据表,我们可以使用类似的表示法,包括将Species作为字符串,并在第三个参数中添加with = F

dtIris[1:3, 'Species', with = F]
 Species
1:  setosa
2:  setosa
3:  setosa

我们也可以从dtIris中提取包含两个或更多列的数据表:

dtIris[1:3, c(5, 1, 2), with = F]
 Species Sepal.Length Sepal.Width
1:  setosa          5.1         3.5
2:  setosa          4.9         3.0
3:  setosa          4.7         3.2

我们在第一个参数中放入一个向量以选择行。像数据框和矩阵一样,我们可以选择定义逻辑向量的行,例如dtIris$Sepal.Length > 7。在数据表的情况下,我们可以直接访问列而不使用$运算符。然后,我们只需要将Sepal.Length > 7作为第一个参数包含进去:

dtIris[Sepal.Length > 7]
 Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
 1:          7.1         3.0          5.9         2.1 virginica
 2:          7.6         3.0          6.6         2.1 virginica
 3:          7.3         2.9          6.3         1.8 virginica
 4:          7.2         3.6          6.1         2.5 virginica
 5:          7.7         3.8          6.7         2.2 virginica
 6:          7.7         2.6          6.9         2.3 virginica
 7:          7.7         2.8          6.7         2.0 virginica
 8:          7.2         3.2          6.0         1.8 virginica
 9:          7.2         3.0          5.8         1.6 virginica
10:          7.4         2.8          6.1         1.9 virginica
11:          7.9         3.8          6.4         2.0 virginica
12:          7.7         3.0          6.1         2.3 virginica

要定义一个新列,我们可以在第二个方括号参数中使用:=运算符。我们可以通过直接输入它们的名称来访问其他列。例如,我们可以将Sepal.Area定义为Sepal.Length * Sepal.Width的乘积:

dtIris[, Sepal.Area := Sepal.Length * Sepal.Width]
dtIris[1:6]
 Sepal.Length Sepal.Width Petal.Length Petal.Width Species Sepal.Area
1:          5.1         3.5          1.4         0.2  setosa      17.85
2:          4.9         3.0          1.4         0.2  setosa      14.70
3:          4.7         3.2          1.3         0.2  setosa      15.04
4:          4.6         3.1          1.5         0.2  setosa      14.26
5:          5.0         3.6          1.4         0.2  setosa      18.00
6:          5.4         3.9          1.7         0.4  setosa      21.06

如果我们想要计算平均Sepal.Area,我们可以在方括号中的第二个参数内执行操作:

dtIris[, mean(Sepal.Area)]
[1] 17.82287

如果我们想知道每个物种的平均花瓣面积,语法相同;我们在第三个参数中包含by = 'Species'

dtIris[, mean(Sepal.Area), by = 'Species']
 Species      V1
1:     setosa 17.2578
2: versicolor 16.5262
3:  virginica 19.6846

我们同时计算更多的统计数据。例如,我们可以确定每个物种的最大和最小花瓣面积。在这种情况下,语法类似,只是在第二个参数中添加了list

dtIris[
     , list(areaMin = min(Sepal.Area), areaMax = max(Sepal.Area)),
     by = 'Species'
     ]
 Species areaMin areaMax
1:     setosa   10.35   25.08
2: versicolor   10.00   22.40
3:  virginic
a   12.25   30.02

另一个有用的包是plyr,它包含一些类似于apply的函数,并且适用于不同的上下文。让我们首先安装并加载这个包:

install.packages('plyr')
library('plyr')

一个有用的函数是dlply,它将数据框分割成块,对每个块应用一个函数,并定义一个包含函数输出的列表。输入类型如下所示:

  • .data: 这是一个数据框。

  • .variables: 这是定义分割的变量。每个块对应于变量的一个可能值。

  • .fun: 这是应用于每个块的函数。

例如,从iris数据框开始,我们可以计算每个物种的平均花瓣长度。首先,我们可以通过计算平均花瓣长度来定义funcDl

funcDl <- function(dtChunk){
  result <- mean(dtIris$Sepal.Length)
  return(result)
}

现在,我们可以使用dlplyfuncDl应用于每个物种:

dlply(
  .data = iris,
  .variables = 'Species',
  .fun = funcDl
)
$setosa
[1] 5.843333

$versicolor
[1] 5.843333

$virginica
[1] 5.843333

让我们探索列表中包含的数据:

names(listIris)

列表中的每个元素都有对应物种的名称。让我们看看其中一个元素:

listIris$setosa

dlply函数从一个数据框生成一个列表,在名称中,d代表数据框,l代表列表。还有其他ply函数,选项如下:

  • a: 数组

  • d: 数据框

  • l: 列表

例如,adply从一个数组定义数据框,而laply从一个列表定义数组。

本节介绍了两个有用的包。CRAN 存储库中有超过 5,000 个包,我们将在接下来的章节中看到其中的一些。

摘要

在本章中,您了解了开发机器学习解决方案所需的软件。您看到了为什么 R 结合 RStudio 是帮助您克服机器学习挑战的好工具。

您学习了 R 的基础知识以及一些最重要的数据类型和函数。您还看到了data.tableplyr等包。

下一章将向您展示一个使用探索性数据分析与机器学习可以面临的挑战的简单示例。您将看到使用 R 工具构建图表和使用机器学习算法。

第三章。简单的机器学习分析

本章展示了探索性数据分析与机器学习技术的示例。R 为我们提供了不同的数据集,可以用来实验这些工具。在本章中,我们将使用关于泰坦尼克号乘客的有趣数据集。

在泰坦尼克号事件中发生了一些事实,例如优先救助妇女和儿童的政策以及第一社会阶层的特权。为了调查发生了什么,我们可以使用与事件相关的数据。R 数据集是关于一些乘客的,它显示了他们的个人数据和谁幸存。首先,我们可以探索一些数据以了解发生了什么。然后,从其他乘客的个人数据出发,机器学习模型的目标是预测哪些新乘客会幸存。

在本章中,我们将涵盖以下主题:

  • 探索数据

  • 使用简单图表可视化数据

  • 使用机器学习技术探索数据

  • 使用机器学习技术预测结果

交互式探索数据

本节展示了如何使用简单技术可视化数据。我们使用data.table包处理数据,并使用基本的 R 图表可视化信息。一个优秀的绘图包是ggplot2,它允许你创建漂亮的图表。不幸的是,它的语法比基本的 R 图表复杂,所以在这本书中没有足够的空间介绍它。

R 为我们提供了一个包含一些乘客生存统计数据的Titanic数据集。在开始分析数据之前,让我们使用以下代码查看它们的文档:

help(Titanic)

文档显示,乘客根据他们的社会阶层、性别和年龄被分成不同的组。对于每个组,数据集显示了幸存人数和未幸存人数。我们可以使用class查看数据的格式:

class(Titanic)
[1] "table"

对象Titanic属于table类,因此它显示了分类变量组合的计数,如下所示:

Titanic
, , Age = Child, Survived = No

 Sex
Class  Male Female
 1st     0      0
 2nd     0      0
 3rd    35     17
 Crew    0      0
...

表格显示了频率,即每个变量组合(包括个人数据和幸存者数据)的乘客数量。

定义包含数据的表格

在本小节中,我们将把数据转换成更方便的格式。第一步是定义一个数据框:

dfTitanic <- data.frame(Titanic)

我们可以使用str查看dfTitanic的结构:

str(dfTitanic)
'data.frame':   32 obs. of  5 variables:
 $ Class   : Factor w/ 4 levels "1st","2nd","3rd",..: 1 2 3 4 1 2 3 4 1 2 ...
 $ Sex     : Factor w/ 2 levels "Male","Female": 1 1 1 1 2 2 2 2 1 1 ...
 $ Age     : Factor w/ 2 levels "Child","Adult": 1 1 1 1 1 1 1 1 2 2 ...
 $ Survived: Factor w/ 2 levels "No","Yes": 1 1 1 1 1 1 1 1 1 1 ...
 $ Freq    : num  0 0 35 0 0 0 17 0 118 154 ...

有四个因素代表乘客的属性,Freq显示每个属性组合的乘客数量。为了使用强大的数据处理工具,我们将dfTitanic转换成数据表:

library(data.table)
dtTitanic <- data.table(dfTitanic)

我们可以使用head查看表的顶部行:

head(dtTitanic)
 Class    Sex   Age Survived Freq
1:   1st   Male Child       No    0
2:   2nd   Male Child       No    0
3:   3rd   Male Child       No   35
4:  Crew   Male Child       No    0
5:   1st Female Child       No    0
6:   2nd Female Child       No    0

在这里,Class(等级)、Sex(性别)、Age(年龄)和Survived(是否存活)代表属性,而Freq显示每个组合的乘客数量。例如,有 35 名男性三等舱儿童幸存。其他五个特征组合没有乘客。

要开始分析,我们可以定义包含乘客总数的nTot

nTot <- dtTitanic[, sum(Freq)]
nTot
[1] 2201

2201名乘客。其中有多少幸存?我们可以使用简单的数据表聚合来计算幸存和未幸存的乘客数量。我们需要指定以下内容:

  • 操作:为了计算乘客数量,我们求和Freq列,所以操作是n=sum(Freq)

  • 聚合:我们按Survived列的每个可能值计算乘客数量,因此我们需要指定我们按Survived聚合

这是数据表语法。我们使用方括号,并且三个参数是:

  • 选择行:我们使用所有表格,所以参数为空

  • 操作:这包含一个包含操作的列表,即n=sum(Freq)

  • 聚合:我们指定我们按Survived聚合

考虑以下代码:

dtSurvived <- dtTitanic[, list(n=sum(Freq)), by='Survived']
dtSurvived
 Survived    n
1:       No 1490
2:      Yes  711

通过直方图可视化数据

我们可以通过构建直方图来可视化dtSurvived,R 函数是barplot

help(barplot)

在我们这个例子中,我们需要的是heightnames.arg参数,指定条形的高度和标签。在这两种情况下,参数都需要一个向量。让我们看看我们如何构建图表。按照以下步骤:

  1. 定义包含乘客数量的高度向量:

    vectorHeight <- dtSurvived[, n]
    
  2. 定义包含幸存乘客数量的名称向量:

    vectorNames <- dtSurvived[, Survived]
    
  3. 构建图表:

    barplot(height=vectorHeight, names.arg=vectorNames)
    

直方图如下:

通过直方图可视化数据

直方图显示了幸存或未幸存的乘客数量。每个条形的高度等于乘客数量,标签显示了条形代表的内容。我们可以只用一行代码构建相同的图表:

barplot(height=dtSurvived[, n], names.arg=dtSurvived[, Survived])

此图表显示了乘客总数。如果我们想可视化百分比呢?让我们看看以下步骤:

  1. 定义包含乘客数量除以乘客总数的percentage列。我们可以使用数据表操作:=定义新列。此列将是height参数:

    dtSurvived[, percentage := n / sum(n)]
    
  2. 定义包含蓝色和红色以供可视化的colorPlot列。我们使用ifelse函数指定,如果Survived == 'Yes',颜色为blue,否则为红色。此列将是col参数:

    dtSurvived[, colorPlot := ifelse(Survived == 'Yes', 'blue', 'red')]
    
  3. 构建图表,并预期包括col参数,定义color向量。此外,百分比范围在 0 到 1 之间,因此我们可以指定图表区域将在 0 到 1 之间,添加ylim参数等于c(0, 1)

    barplot(
      height=dtSurvived[, percentage],
      names.arg=dtSurvived[, Survived],
      col=dtSurvived[, colorPlot],
      ylim=c(0, 1)
    )
    

直方图如下:

通过直方图可视化数据

我们可以向图表添加标题和图例;按照以下步骤:

  1. 定义包含百分比的textPercentage列,例如,对于 0.323035 的百分比,我们在图例中显示 32%:

    dtSurvived[, textPercentage := paste(round(percentage * 100), '%', sep='')]
    
  2. 定义图表标题:

    plotTitle <- 'Proportion of passengers surviving or not'
    
  3. 定义y轴标签:

    ylabel <- 'percentage'
    
  4. 构建图表:

    barplot(
      height=dtSurvived[, percentage],
      names.arg=dtSurvived[, Survived],
      col=dtSurvived[, colorPlot],
      ylim=c(0, 1),
      legend.text=dtSurvived[, textPercentage],
      ylab=ylabel,
      main=plotTitle
    )
    

直方图如下:

通过直方图可视化数据

总体生存率为32%,尽管它在不同的属性组合中有所变化。下一小节将向您展示如何可视化属性的影响。

可视化特征的影响

在本小节中,我们确定性别对生存率的影响。首先,我们可以定义dtGender,显示每个性别中幸存或未幸存的乘客数量。操作是n=sum(Freq),并且对SurvivedSex的每个组合进行操作。类似于上一节,我们执行简单的数据表聚合,指定以下内容:

  • 选择行:我们使用整个表,因此参数为空

  • 操作:这是一个包含操作的列表,即n=sum(Freq)

  • 聚合:我们通过两列进行聚合,因此我们定义by=c('Survived', 'Sex')

考虑以下代码:

dtGender <- dtTitanic[, list(n=sum(Freq)), by=c('Survived', 'Sex')]
dtGender
 Survived    Sex    n
1:       No   Male 1364
2:       No Female  126
3:      Yes   Male  367
4:      Yes Female  344

现在,我们可以通过直方图可视化新的数据表,就像我们之前看到的那样。步骤如下:

  1. 添加percentage列,通过将n除以该性别的乘客总数来计算。操作是n / sum(n),并且按性别进行。然后,我们使用:=操作指定我们计算by='Sex'的总和:

    dtGender[, percentage := n / sum(n), by='Sex']
    
  2. 定义绘图颜色:

    dtGender[, colorPlot := ifelse(Survived == 'Yes', 'blue', 'red')]
    
  3. 定义Y轴标签:

    dtGender[, textPercentage := paste(round(percentage * 100), '%', sep='')]
    
  4. 提取包含男性生存统计数据的表:

    dtGenderMale <- dtGender[Sex == 'Male']
    
  5. 为男性构建直方图:

    barplot(
      height=dtGenderMale[, percentage],
      names.arg=dtGenderMale[, Survived],
      col=dtGenderMale[, colorPlot],
      ylim=c(0, 1),
      legend.text=dtGenderMale[, textPercentage],
      ylab='percentage',
      main='Survival rate for the males'
    )
    
  6. 我们可以直接在提取向量时添加Sex == 'Male'来构建图表,而不是提取dtGenderMale。我们可以以类似的方式为女性构建相同的直方图:

    barplot(
      height=dtGender[Sex == 'Female', percentage],
      names.arg=dtGender[Sex == 'Female', Survived],
      col=dtGender[Sex == 'Female', colorPlot],
      ylim=c(0, 1),
      legend.text=dtGender[Sex == 'Female', textPercentage],
      ylab='percentage',
      main='Survival rate for the females'
    )
    

让我们显示我们构建的图表:

可视化特征的影响

与 32%的幸存乘客相比,男性的生存率仅为21%

可视化特征的影响

如预期的那样,女性的生存率显著高于平均水平。

我们可以在同一张图表中比较两个性别,只显示生存率,即Yes列。我们可以使用相同的命令构建绘图,并包括Survived == 'Yes'条件。唯一的区别是col参数,在这种情况下是Sex列,它是一个有两个级别的因子。在这种情况下,barplot自动定义两种颜色,即黑色和红色:

barplot(
  height=dtGender[Survived == 'Yes', percentage],
  names.arg=dtGender[Survived == 'Yes', Sex],
  col=dtGender[Survived == 'Yes', Sex],
  ylim=c(0, 1),
  legend.text=dtGender[Survived == 'Yes', textPercentage],
  ylab='percentage',
  main='Survival rate by gender'
)

直方图如下所示:

可视化特征的影响

该图表使我们能够可视化差异,图例显示生存率。正如预期的那样,差异很大。

可视化两个特征结合的影响

在本章中,我们研究另一个特征的影响:等级。乘客的不同等级的生存率是如何变化的?首先,我们可以通过以下步骤构建与性别相同的生存率图表:

  1. 定义包含每个等级的乘客是否幸存或未幸存的dtClass

    dtClass <- dtTitanic[, list(n=sum(Freq)), by=c('Survived', 'Class')]
    
  2. 定义每个等级中幸存或未幸存的乘客的百分比:

    dtClass[, percentage := n / sum(n), by='Class']
    
  3. 定义百分比文本:

    dtClass[, textPercentage := paste(round(percentage * 100), '%', sep='')]
    
  4. 构建直方图:

    barplot(
      height=dtClass[Survived == 'Yes', percentage],
      names.arg=dtClass[Survived == 'Yes', Class],
      col=dtClass[Survived == 'Yes', Class],
      ylim=c(0, 1),
      legend.text=dtClass[Survived == 'Yes', textPercentage],
      ylab='survival rate',
      main='Survival rate by class'
    )
    

直方图如下所示:

可视化两个特征结合的影响

生存率在各个等级之间差异很大。我们可以注意到,属于较高等级的乘客更有可能幸存,而船员的生存率与三等舱相似。我们能得出结论说等级对生存率有重大影响吗?

该图表显示了每个等级的整体生存率。然而,考虑到女性更有可能幸存,女性与男性的比例较高的等级可能具有更高的生存率。如果较高的生存率仅由性别解释,那么属于不同等级的事实根本没有任何影响。

为了了解生存率之间的差异是否取决于每个等级中女性的百分比,我们可以通过等级可视化性别比例。图表是一个直方图,显示了每个社会等级中女性的百分比,命令与之前类似:

dtGenderFreq <- dtTitanic[, list(n=sum(Freq)), by=c('Sex', 'Class')]
dtGenderFreq[, percentage := n / sum(n), by='Class']
dtGenderFreq <- dtGenderFreq[Sex == 'Female']
dtGenderFreq[, textPercentage := paste(round(percentage * 100), '%', sep='')]
barplot(
  height=dtGenderFreq[, percentage],
  names.arg=dtGenderFreq[, Class],
  col=dtGenderFreq[, Class],
  ylim=c(0, 1),
  legend.text=dtGenderFreq[, textPercentage],
  ylab='survival rate',
  main='Percentage of females'
)

直方图如下所示:

可视化两个特征结合的影响

性别比例在不同等级之间差异很大,因为顶级等级中女性的百分比较高,而船员中几乎没有女性。因此,女性的百分比可能已经通过等级影响了生存率。为了更好地理解两个属性对生存率的影响,我们需要同时考虑性别和等级。为此,我们可以计算这两个特征的每个组合的生存率。使用以下步骤构建图表:

  1. 计算每个SurvivedSexClass组合的乘客总数。现在by参数包括三个列名:

    dtGenderClass <- dtTitanic[, list(n=sum(Freq)), by=c('Survived', 'Sex', 'Class')]
    
  2. 添加指定每个特征组合的乘客总数的nTot列(不包括Survived)。by参数包括两个特征:

    dtGenderClass[, nTot := sum(n), by=c('Sex', 'Class')]
    
  3. 添加percentage列。by参数包括两个特征:

    dtGenderClass[, percentage := n / sum(n), by=c('Sex', 'Class')]
    
  4. 使用Survived == 'Yes'条件提取包含生存率的列:

    dtGenderClass <- dtGenderClass[Survived == 'Yes']
    
  5. 添加textPercentage列:

    dtGenderClass[, textPercentage := paste(round(percentage * 100), '%', sep='')]
    
  6. 添加colorPlot列。rainbow函数构建一个包含定义数量的彩虹颜色的向量。在这种情况下,我们为每一行定义一个列,因此我们使用rainbow(nrow(dtGenderClass))

    dtGenderClass[, colorPlot := rainbow(nrow(dtGenderClass))]
    
  7. 定义要包含在标签中的组名。由于直方图将显示每个特征组合的生存率,我们使用paste将每个组的名称设置为性别和等级的组合。为了将名称适应到图表中,我们定义了包含性别缩写的SexAbbr

    dtGenderClass[, SexAbbr := ifelse(Sex == 'Male', 'M', 'F')]
    dtGenderClass[, barName := paste(Class, SexAbbr, sep='')]
    
  8. 定义包含绘图名称和组内乘客数量的标签。由于我们希望在不同行中显示名称和数量,我们使用\n(在字符串中定义新行的符号)将它们分开:

    dtGenderClass[, barLabel := paste(barName, nTot, sep='\n')]
    
  9. 生成直方图。与ylim类似,xlim参数定义了要可视化的x区域。在这种情况下,我们使用xlim来避免图例和图表重叠:

    barplot(
      height=dtGenderClass[, percentage],
      names.arg=dtGenderClass[, barLabel],
      col=dtGenderClass[, colorPlot],
      xlim=c(0, 11),
      ylim=c(0, 1),
      ylab='survival rate',
      legend.text=dtGenderClass[, textPercentage]
    )
    

生成的直方图如下:

可视化两个特征结合的影响

我们可以在其列下找到该组的乘客数量。除了女性船员外,每个条形图至少包含 100 名乘客,因此我们可以假设结果是具有意义的。为了衡量意义,我们本可以使用诸如置信区间或假设检验之类的统计技术,但这不是本书的主题。

班级以不同的方式影响着男性和女性。在男性方面,尽管头等舱的生存率显著较高,但生存率非常低。在女性方面,除了第三班外,每个班级的生存率都接近 100%。

我们也可以从相反的角度来看图表,以了解性别对同一班级乘客的影响。在所有情况下,生存率都显著较高,尽管对于某些特定班级的差异要大得多。性别和班级的影响是相关的,因此如果我们想了解它们的影响,我们需要同时考虑这两个特征。

我们还没有探索年龄。我们可以可视化所有特征的每个组合的生存率。准备和绘制表的代码与之前类似。在这种情况下,我们只需直接对dtTitanic应用操作。步骤如下:

  1. 计算每个三个特征的组合中生存或未生存的人的百分比:

    dtTitanic[, nTot := sum(Freq), by=c('Sex', 'Class', 'Age')]
    
  2. 添加每个属性组合中生存的乘客的百分比:

    dtTitanic[, percentage := Freq / nTot]
    
  3. 使用Survived == 'Yes'条件提取生存率:

    dtAll <- dtTitanic[Survived == 'Yes', ]
    
  4. 添加包括所有三个特征缩写的图例文本。对于班级,我们使用子字符串函数,这是一个提取字符串一部分的函数。在我们的情况下,我们提取第一个字符,因此我们指定使用substring(Class, 1, 1)提取11之间的元素:

    dtAll[, ClassAbbr := substring(Class, 1, 1)]
    dtAll[, SexAbbr := ifelse(Sex == 'Male', 'M', 'F')]
    dtAll[, AgeAbbr := ifelse(Age == 'Child', 'C', 'A')]
    dtAll[, textLegend := paste(ClassAbbr, SexAbbr, AgeAbbr, sep='')];
    
  5. 添加绘图颜色:

    dtAll[, colorPlot := rainbow(nrow(dtAll))]
    
  6. 添加要显示在标签中的百分比:

    dtAll[, labelPerc := paste(round(percentage * 100), '%', sep='')]
    
  7. 添加包含百分比和总数的标签:

    dtAll[, label := paste(labelPerc, nTot, sep='\n')]
    
  8. 生成绘图。由于我们比之前有更多的组,因此布局不同,以便可视化所有相关信息。xlim参数为图例留出一些空间,而cex.names参数减小了标签文本的大小:

    barplot(
      height=dtAll[, percentage],
      names.arg=dtAll[, label],
      col=dtAll[, colorPlot],
      xlim=c(0, 23),
      legend.text=dtAll[, textLegend],
      cex.names=0.5
    )
    

直方图如下:

可视化两个特征结合的影响

图例显示了特征的缩写组合。例如,1MC代表一等舱、男性、儿童。在没有任何乘客的组合情况下,我们没有关于百分比的任何信息,因此条形标签显示NaN%

由于我们结合了三个特征,一些组非常小。例如,我们只有五位一等舱男性儿童。还有其他完全没有乘客的组(例如,船员中的儿童)。因此,这种方法有一些局限性。

使用机器学习模型探索数据

可视化每个乘客组的生存率为我们提供了数据的概览。我们了解不同的特征如何与生存率以及彼此相互作用。例如,我们知道社会阶层对生存率的影响取决于性别。但哪两个特征的影响最大?每个特征的影响有多大?我们还没有定义特征的排名或量化它们的影响。一些机器学习技术允许我们进一步调查,回答我们的问题。

使用决策树探索数据

我们有三个特征(类别、性别和年龄),并希望根据这些特征将乘客分成不同的组。由于某些组(例如一等舱的女童)的数据量不足,我们无法为每个特征组合定义一个组。一种解决方案是将乘客分成组,使得每个组都有足够的数据。一个组由一些关于特征的约束条件定义,例如男性不属于一等舱。这些组应该覆盖所有可能的情况,且不重叠。一种识别足够大组的机器学习技术是决策树学习。

有一位新乘客,我们知道他是一位二等舱的男童。我们不知道这位乘客是否会幸存,我们想要预测这一点。我们如何使用这些数据?我们可以检查这位乘客是男性还是女性。根据我们之前的数据探索,他作为男性,幸存的可能性为 21%。考虑到社会阶层,我们可以说他幸存的可能性为 14%。有 179 位二等舱男性乘客,所以这个结果是有意义的。然后,知道他是一位儿童,我们可以查看二等舱男性儿童的生存率,这是 100%。说他会以 100%的概率幸存合理吗?只有 11 位乘客是二等舱男性儿童,所以我们没有足够的数据来做出准确的预测。我们应该使用二等舱男性的生存率吗?如果我们使用所有男性儿童的生存率呢?或者二等舱儿童的生存率呢?不同的选择会导致不同的结果。

一种解决方案是识别关键特征,并且只考虑它们。例如,如果性别和舱位是两个最重要的特征,我们可以使用它们来进行预测。然而,在第三等舱男性儿童的情况下,我们拥有的数据比第一等舱男性儿童多得多。如果我们只在第三等舱男性中考虑年龄会怎样?我们想要包含的特征数量取决于我们考虑的组。

我们不仅可以选择两个最重要的特征,还可以定义一个标准,只有当分组足够大时才进行分割,我们可以通过决策树可视化这个原则。假设一开始所有乘客都属于同一个组。我们可以根据性别将他们分成两组。然后,我们可以将男性分成两组:一边是一等舱,另一边是所有其他舱位。对于女性,最有意义的分割可能是一个不同的:一边是儿童,另一边是成人。

决策树学习技术从数据中学习以识别最有意义的分割,并且可以用来探索数据。树继续分割数据,直到由树叶定义的组变得太小。然后,对于每个组,我们使用相关的数据来定义一个属性,该属性可以是:

  • Categoric:这是一个其值属于类别的属性。在这种情况下,类别是SurvivedNot survived。树执行分类。

  • Numeric:这是一个可以测量的属性,在这种情况下,它是生存率。树执行回归。

我们可以使用rpart包在 R 中构建决策树。此外,我们可以使用另一个名为rpart.plot的包来可视化树。为了使用这些包,我们需要安装并加载它们。如果遇到安装问题,可以在install.packages函数的参数中指定仓库:

install.packages('rpart')
install.packages('rpart.plot')

安装完成后,我们可以加载以下两个包:

library('rpart')
library('rpart.plot')

起始点是dtTitanic,它包含每个特征组合的一行。在构建决策树之前,我们需要将数据转换成另一种格式。我们需要为每位乘客创建一行,除了Freq之外,其他列保持不变。为了生成新格式的新的表格,我们使用data.table操作与listby

对于dtTitanic的每一行,我们希望生成一个具有与Freq相同的行数的表格。每一行对应于SurvivedSexAgeClass之间的组合,因此by参数包含一个包含四个特征的向量。

在新的表中,每一行包含一个乘客,所以 Freq 等于 1。然后,对于 dtTitanic 的每一行,我们需要定义一个包含 Freq1 的向量。为了做到这一点,我们使用 rep 函数,它是一个复制元素一定次数的函数。在我们的例子中,我们使用 rep(1, Freq))。其他列复制 by 中定义的元素,即 SurvivedSexAgeClass,所以我们不需要重新定义它们:

dtLong <- dtTitanic[
  , list(Freq = rep(1, Freq)),
  by=c('Survived', 'Sex', 'Age', 'Class')
  ]

Freq 对于每一行都是 1,所以我们不再需要它,可以删除它:

dtLong[, Freq := NULL]

为了构建显示生存率的决策树,我们需要更改 Survived 的格式。我们希望将 NoYes 分别改为 01。要修改列,我们可以使用 ifelse

dtLong[, Survived := ifelse(Survived == 'Yes', 1, 0)]

让我们使用 head 查看一下 DtLong 的前六行:

head(dtLong)
 Survived  Sex   Age Class
1:        0 Male Child   3rd
2:        0 Male Child   3rd
3:        0 Male Child   3rd
4:        0 Male Child   3rd
5:        0 Male Child   3rd
6:        0 Male Child   3rd

前六行显示了六名未幸存的男性儿童。

dtLong 对象包含决策树算法的标准输入,我们可以使用 rpart 来构建模型。我们的目标是定义一组乘客,关于他们,我们能够估计生存率:

help(rpart)

必要的参数是:

  • formula:这是一个公式对象,定义了要预测的属性和用于预测的特征。公式由一个字符串定义,例如 outcome ~ feature1 + feature2 + feature3

  • data:这是数据框或数据表,在我们的例子中是 dtLong

我们需要从 Survived ~ Sex + Age + Class 字符串开始定义公式:

formulaRpart <- formula('Survived ~ Sex + Age + Class')

现在我们可以构建包含决策树的 treeRegr。由于 Survived 是数值型的,函数会自动构建一个回归树:

treeRegr <- rpart(
  formula=formulaRpart,
  data=dtLong
)

treeRegr 对象包含决策树,我们可以使用 prp(treeRegr) 来可视化它:

使用决策树探索数据

让我们看看这棵树。每个内部节点都标记了一个条件,该条件将数据分成两部分。例如,顶部的节点将乘客分为男性和女性。左边的分支对应满足条件的乘客(在这种情况下,男性乘客),右边的分支对应其他人(女性)。每个叶子定义了该组的生存率。例如,右边的叶子表明不属于第三类的女性的生存率为 93%。

由于数据不足,这棵树不包含所有可能的特征组合。例如,在女性的情况下,只有 45 个孩子,她们属于不同的社会阶层,因此这棵树不会根据她们的年龄来划分女性。

假设我们有一个新的乘客,她是女性、儿童、二等舱。我们如何预测她是否会生存?她是一个不属于第三类的女性,所以她的预期生存率为 93%。因此,我们可以说她很可能会生存。

树定义了一个表示为数字的生存率。如果我们想预测乘客是否存活?我们可以通过向rpart添加method='class'输入来构建一个分类树:

treeClass = rpart(
    formula='Survived ~ Sex + Age + Class',
    data=dtLong,
    method='class'
)
prp(treeClass)

树的表示如下:

使用决策树探索数据

这棵树预测只有女性和三等舱外的儿童乘客会存活。这个结果对于探索数据是有用的。下一步是使用机器学习模型来预测结果。我们可以用这棵树来达到这个目的,尽管它只定义了 16 种可能特征组合中的五个乘客组,所以它可能不是最合适的技术。还有更高级的算法,在下一章我们将看到其中之一。

预测新的结果

给定一个新乘客以及他们的个人信息,我们想要预测他们是否会存活。我们至今探索的选项都是基于将乘客分为组并确定每个组的生存率。对于某些特征组合,例如头等舱女性儿童,我们没有足够的数据,因此我们必须使用更大组(如不属于三等舱的女性)的生存率。我们忽略了某些细节,例如他们是儿童的事实,这样我们就丢失了信息。有没有一种方法可以估计任何特征组合的生存率,而不管我们有多少乘客?

有许多机器学习算法会同时考虑所有特征。在本章中,我们将看到一个非常流行的算法,即随机森林算法。在这个上下文中,它并不是最佳选择,因为当有更多特征时,它的表现会更好,但它对于说明一般方法是有益的。

构建机器学习模型

如其名所示,随机森林算法基于许多随机决策树。算法通过重复以下步骤来构建ntree个树:

  1. 通过从数据中选择一个随机的行(在我们的案例中,即dtLongsampsize次来生成构建树的数据。每一行可以选择多次,最终我们得到一个包含sampsize个随机行的表格。

  2. 随机选择一个mtry数量的特征(不幸的是,在我们的案例中我们没有很多特征,但仍然可以选择它们的子集)。

  3. 基于采样数据,仅考虑所选特征来构建决策树。

随机森林模型由ntree个决策树组成。在我们的上下文中,给定一个新乘客,模型使用每个树来预测他们的生存率。最终的预测值是生存率的平均值。在算法的另一种变体中,我们使用众数而不是平均值。

随机森林是一个流行的算法,它由randomForest包提供。让我们安装并加载它:

install.packages('randomForest')
library('randomForest')

与简单的决策树学习一样,随机森林的特征可以是分类的或数值的。

在我们的案例中,所有特征都是分类的,每个特征有两个到四个可能的值。我们可以将特征转换为数值格式。例如,在Sex的情况下,可能的值是MaleFemale。我们可以定义一个数值特征,当性别为Male时为1,否则为0。新的特征以不同的方式展示了相同的信息。以这种方式从分类特征派生出的数值特征被称为虚拟变量。在具有两个以上类别的分类特征的情况下,我们可以为每个类别(除了一个)定义一个虚拟变量。这样,通过查看虚拟变量,如果其中一个等于1,我们就知道哪个组。如果它们都等于0,我们就知道还有一个组剩下。

我们可以通过以下步骤定义一个包含虚拟变量的新表:

  1. 构建分类特征表的副本:

    dtDummy <- copy(dtLong)
    
  2. Sex转换为虚拟变量:

    dtDummy[, Male := Sex == 'Male']
    dtDummy[, Sex := NULL]
    
  3. Age转换为虚拟变量:

    dtDummy[, Child := Age == 'Child']
    dtDummy[, Age := NULL]
    
  4. Class转换为三个虚拟变量:

    dtDummy[, Class1 := Class == '1st']
    dtDummy[, Class2 := Class == '2nd']
    dtDummy[, Class3 := Class == '3rd']
    dtDummy[, Class := NULL]
    
  5. 定义formulaRf公式:

    formulaRf <- formula('Survived ~ Male + Child + Class1 + Class2 + Class3')
    
  6. 构建forest,包含随机森林模型。所有参数都保留为它们的默认值:

    forest <- randomForest(
      formula=formulaRf,
      data=dtDummy
    )
    

我们将随机森林模型存储在名为forest的列表中,该列表包含机器学习模型、所有相关参数和信息。我们可以通过观察列表的元素来探索模型。例如,模型构建的树的数量包含在ntree元素中:

forest$ntree
[1] 500

另一个参数是mtry,它定义了每次迭代中使用的变量数量:

forest$mtry
[1] 1

树的数量已默认设置为 500。

算法一次只选择一个特征。原因是随机森林旨在与许多特征一起工作,因此在这种环境中表现不佳。

另一个参数是type,它定义了算法的输出。随机森林可以用于不同的目的,在我们的案例中,我们想要估计生存率,因此我们想要将其用于回归:

forest$type
[1] "regression"

如预期的那样,forest正在执行回归。

如果我们想更改一些参数,我们可以在参数中定义它们。在本章中,我们没有定义一个标准来设置参数,所以我们只是分配另一个值。例如,我们可以使用三个随机特征和每个1500行随机数据构建1000棵树。我们可以通过更改参数来重建forest

forest <- randomForest(
  formula=formulaRf,
  data=dtDummy,
  ntree=1000,
  mtry=3,
  sampsize=1500
)

我们构建了一个随机forest模型,下一个小节将展示如何使用它。

使用模型预测新的结果

现在我们已经构建了模型,我们可以使用它来进行一些预测。如果我们有一个新的乘客,他们的生存率是多少?首先,让我们提取一个随机乘客:

rowRandom <- dtDummy[100]
rowRandom
 Survived Freq Male Child Class1 Class2 Class3
1:       No    1 TRUE FALSE   TRUE  FALSE  FALSE

随机乘客是一位一等舱的成年男性。我们可以使用forest模型来估计他的生存率。predict函数允许我们将模型应用于新数据,从而获得预测:

predict(forest, rowRandom)
 1 
0.3830159

估计的生存率约为 38%,因此乘客不太可能幸存。我们可以使用相同的方法来预测所有乘客的生存率。然而,这意味着要使用构建模型所用的相同数据来应用模型。这种方法不适合测试模型,因为预测值将与初始数据相关。考虑到这个结果不能使用,我们可以用它来比较预测与实际数据:

prediction = predict(forest, dtDummy)

我们可以使用sample查看六行随机数据的预测:

sample(prediction, 6)
 1895       448       967      1553      1683         4 
0.6934046 0.2260507 0.2499303 0.3830159 0.2260507 0.2974706

我们为每个乘客定义了一个生存率。让我们将估计的生存率添加到dtDummy表中:

dtDummy[, SurvivalRatePred := predict(forest, dtDummy)]

现在,我们可以预测如果乘客的生存率高于一个阈值,例如 50%,乘客将幸存。我们可以定义一个新的列名SurvivedPred,包含我们的预测:

dtDummy[, SurvivedPred := ifelse(SurvivalRatePred > 0.5, 1, 0)]

现在,我们可以比较预测的生存率与初始数据。为了评估两个值匹配的次数,我们可以定义一个error列,如果值不匹配则为TRUE

dtDummy[, error := SurvivedPred != Survived]

从错误列开始,我们可以计算总体误差,即我们做出错误预测的乘客百分比。我们需要将错误数量除以乘客数量。我们可以通过将错误求和来得到错误数量,因为布尔变量的向量之和等于TRUE值的数量。总乘客数由.N定义,在data.table表示法中等于行数:

percError <- dtDummy[, sum(error) / .N]
percError
[1] 0.2094502

模型在 21%的情况下预测了错误的结果,因此我们的准确率为 79%。无论如何,这个结果没有任何意义,因为我们正在对构建模型所用的相同数据进行预测。此外,如果我们知道有多少乘客幸存,我们只需猜测每个乘客最常见的结果。如果其中超过一半的乘客幸存,我们可以将SurvivedPred设置为TRUE,并猜测超过一半。让我们计算幸存的整体概率。总体生存率低于 50%,因此每个乘客不太可能幸存。然后,在没有其他信息的情况下,我们可以预测没有人会幸存:

dtTitanic[Survived == 'No', sum(Freq)] / dtTitanic[, sum(Freq)]
[1] 0.676965

即使不考虑任何特征,我们也能达到超过 65%的准确率,所以 79%只是高出 15%。此外,正如之前所说,这个准确率不能使用,因为我们正在将模型应用于构建模型所用的相同数据。

验证模型

为了评估模型的实际准确率,我们可以使用数据的一部分来构建模型,例如 80%的乘客。然后,我们可以将模型应用于剩余的 20%数据。我们用来构建模型的数据称为训练集,而其他数据称为测试集

我们可以将每行以 80%的概率分配到训练集中。这样,训练集将包括大约 80%的数据。为了定义哪些行应该包含在训练集中,我们可以定义一个逻辑向量,称为indexTrain,对于属于训练集的每一行,它都是TRUE。我们可以使用sample函数生成这个向量,其参数为:

  • x:这代表可能的值;在这种情况下,TRUEFALSE

  • size:这代表向量长度;在这种情况下,它等于dtDummy中的行数

  • replace:如果值为TRUE,每个值(TRUEFALSE)可以被采样多次

  • prob:这是一个包含采样x值概率的向量;在这种情况下,它是c(0.8, 0.2)

考虑以下代码:

indexTrain <- sample(
  x=c(TRUE, FALSE),
  size=nrow(dtDummy),
  replace=TRUE,
  prob=c(0.8, 0.2)
)

现在,我们可以提取indexTrain等于TRUE的行:

dtTrain <- dtDummy[indexTrain]

同样地,我们提取测试集的行。!运算符表示NOT,它允许提取indexTrain等于FALSE的行:

dtTest <- dtDummy[!indexTrain]

现在,我们可以使用之前相同的参数来构建模型。知道我们数据较少,我们可以只减少定义每个树使用的数据的sampsize参数:

forest <- randomForest(
  formula=formulaRf,
  data=dtTrain,
  ntree=1000,
  mtry=3,
  sampsize=1200
)

我们构建了一个模型,没有考虑到dtTest,因此我们可以用它来预测dtTest。和之前一样,我们预测如果乘客的生存率高于 50%,他们将会幸存。预测之后,我们可以使用和之前相同的 R 命令来估计误差:

dtTest[, SurvivalRatePred := predict(forest, dtTest)]
dtTest[, SurvivedPred := ifelse(SurvivalRatePred > 0.5, 1, 0)]
dtTest[, error := SurvivedPred != Survived]
percError <- dtTest[, sum(error) / .N]
percError
[1] 0.2416107

估计误差percError取决于我们如何分割数据,所以每次定义新的随机训练/测试分割时它都不同。然而,我们可以重复这些步骤多次并计算平均误差。这种方法称为交叉验证,它是一种非常有用的工具,用于估计准确性。

本章展示了一种通用的构建和验证机器学习模型的方法。使用这种方法,我们可以预测一个属性并估计预测的准确性。

摘要

在本章中,我们学习了如何使用数据表操作处理数据,并构建了一些简单的 R 图表进行探索性数据分析。我们学习了如何使用决策树找到有用的见解并构建机器学习模型(随机森林)进行预测。我们看到了如何更改模型的参数以及如何验证它。

接下来的三章将详细介绍本章介绍的方法。第四章,步骤 1 - 数据探索和特征工程,深入介绍了机器学习的第一步,即数据探索和特征工程。

第四章。步骤 1 – 数据探索和特征工程

有不同类型的问题需要机器学习解决方案。例如,我们的目标可以是预测未来的结果或从数据中识别模式。起点是一组对象(例如,商品)或人(例如,超市的顾客)。在大多数情况下,机器学习技术从描述对象/人的某些特征开始识别解决方案。特征是数值和/或分类属性,它们是机器学习模型的基础。拥有正确的特征将提高模型的表现力和准确性,因此定义与问题相关的某些特征非常重要。

在本章中,你将:

  • 构建机器学习解决方案

  • 构建特征数据

  • 清洗数据

  • 探索定义的特征

  • 修改特征

  • 使用过滤器对特征进行排序

构建机器学习解决方案

在机器学习解决方案的哪个阶段我们正在定义特征?让我们看一下构建解决方案整个过程的概述。我们可以将方法分为三个步骤:

  1. 定义我们将要使用的特征。

  2. 应用一个或多个技术来解决问题。

  3. 评估结果并优化性能。

在第一步,我们可以通过使用过滤器来评估每个特征的相关性,并选择最相关的特征。我们还可以定义一些特征组合,这些特征组合有助于描述数据。

在第二步,当我们构建模型时,我们可以使用一些技术(嵌入方法)来对特征进行排序并自动识别最相关的特征。

最后一步非常重要,因为我们有更多信息,这使我们能够识别更合适的特征集。例如,我们可以使用具有不同特征集的相同模型,并评估哪种特征组合表现更好。一个选项是使用一个包装器,该包装器包括使用所选特征集构建模型,迭代地添加(或删除)一个特征,如果它提高了模型准确性,则保留这种变化。

总之,特征选择是一个循环而不是一个步骤,它发生在过程的每个部分。本章展示了特征工程过程,该过程包括定义特征、转换它们以及确定它们的排名。步骤如下:

  • 探索数据

  • 定义/转换新特征

  • 识别最相关的特征

尽管探索数据始终在开始时,但所有步骤都可以重复,直到我们找到令人满意的解决方案,因此它们不一定总是遵循相同的顺序。例如,在确定最相关的特征后,我们可以探索数据,识别新的模式,从而定义一些新的特征。

特征选择的过程与模型相关,在本章中,我们确定了一些适合许多模型的特征。

本章展示了国旗的示例。基于一个国家的国旗,我们的目标是确定该国的语言。假设我们知道所有国家的国旗和其中一些国家的语言,模型将估计其他国家的语言。

构建特征数据

本节展示了我们如何构建原始数据以构建特征。对于每个国家,数据包括:

  • 国旗的图片

  • 一些地理数据,例如大陆、地理象限、面积和人口

  • 该国的语言和宗教

目标是构建一个从国旗预测国家语言的模型。大多数模型可以处理数值和/或分类数据,因此我们不能将国旗图片作为模型的特征。解决方案是定义一些特征,例如颜色的数量,来描述每个国旗。这样,我们就从一张表格开始,该表格的行对应国家,列对应国旗特征。

基于图片构建具有国旗属性的矩阵将花费很多时间。幸运的是,我们可以使用包含一些特征的数据库。我们拥有的数据仍然有些杂乱,因此我们需要对其进行清理和转换,以构建一个格式正确的特征表。

数据集中的特征显示了一些关于的信息:

  • 国旗上的颜色

  • 国旗上的图案

  • 国旗中的一些附加元素,如文本或一些星星

  • 一些地理数据,例如大陆、地理象限、面积和人口

  • 该国的语言和宗教

将表格以正确格式引导的步骤如下:

  1. archive.ics.uci.edu/ml/machine-learning-databases/flags/下载数据集和相关信息,并下载flag.data

  2. 打开 RStudio 并将工作目录设置为包含数据的文件夹:

    setwd('path/containing/the/data')
    
  3. 将数据加载到 R 环境中:

    dfFlag <- read.csv('flag.data', header=FALSE)
    

现在,我们可以使用str查看dfFlag的结构:

str(dfFlag)
'data.frame':    194 obs. of  30 variables:
 $ V1 : Factor w/ 194 levels "Afghanistan",..: 1 2 3 4 5 6 7 8 9 10 ...
 $ V2 : int  5 3 4 6 3 4 1 1 2 2 ...
 $ V3 : int  1 1 1 3 1 2 4 4 3 3 ...
 $ V4 : int  648 29 2388 0 0 1247 0 0 2777 2777 ...
 $ V5 : int  16 3 20 0 0 7 0 0 28 28 ...
 $ V6 : int  10 6 8 1 6 10 1 1 2 2 ...
 $ V7 : int  2 6 2 1 0 5 1 1 0 0 ...
 $ V8 : int  0 0 2 0 3 0 0 0 0 0 ...
 $ V9 : int  3 0 0 0 0 2 1 1 3 3 ...
 $ V10: int  5 3 3 5 3 3 3 5 2 3 ...
 $ V11: int  1 1 1 1 1 1 0 1 0 0 ...
 $ V12: int  1 0 1 0 0 0 0 0 0 0 ...
 $ V13: int  0 0 0 1 1 0 1 1 1 1 ...
 $ V14: int  1 1 0 1 1 1 0 1 0 1 ...
 $ V15: int  1 0 1 1 0 0 1 1 1 1 ...
 $ V16: int  1 1 0 0 0 1 0 1 0 0 ...
 $ V17: int  0 0 0 1 0 0 1 0 0 0 ...
 $ V18: Factor w/ 8 levels "black","blue",..:5 7 5 2 4 7 8 7 2 2 ...
 $ V19: int  0 0 0 0 0 0 0 0 0 0 ...
 $ V20: int  0 0 0 0 0 0 0 0 0 0 ...
 $ V21: int  0 0 0 0 0 0 0 0 0 0 ...
 $ V22: int  0 0 0 0 0 0 0 0 0 0 ...
 $ V23: int  1 1 1 0 0 1 0 1 0 1 ...
 $ V24: int  0 0 1 0 0 0 0 0 0 0 ...
 $ V25: int  0 0 0 1 0 0 0 1 0 0 ...
 $ V26: int  1 0 0 1 0 1 0 0 0 0 ...
 $ V27: int  0 1 0 1 0 0 1 0 0 0 ...
 $ V28: int  0 0 0 0 0 0 0 0 0 0 ...
 $ V29: Factor w/ 7 levels "black","blue",..: 1 6 4 2 2 6 7 1 2 2 ...
 $ V30: Factor w/ 8 levels "black","blue",..: 5 7 8 7 7 1 2 7 2 2 ...

dfFlag对象包含 30 列,其名称未定义。我们有描述flag.description.txt中数据的文档,这允许我们定义列名。前七列包含一些与国旗无关的属性。让我们开始定义一些包含特征名称的向量。第一列是国家的名称。以下是定义名称的步骤:

  1. 定义国家名称:

    nameCountry <- 'name'
    
  2. 定义三个地理特征名称:continentzonearea

    namesGeography <- c('continent', 'zone', 'area')
    
  3. 定义包括语言在内的该国公民的三个特征名称:

    namesDemography <- c('population', 'language', 'religion')
    
  4. 定义一个包含七个属性且顺序正确的唯一向量:

    namesAttributes <- c(nameCountry, namesGeography, namesDemography)
    
  5. 定义描述条形、条纹和颜色的特征名称:

    namesNumbers <- c('bars', 'stripes', 'colors')
    
  6. 对于某些颜色,有一个变量,如果国旗包含该颜色则显示1,否则显示0。定义它们的名称:

    namesColors <- c('red', 'green', 'blue', 'gold', 'white', 'black', 'orange')
    
  7. 定义主要颜色的名称:

    nameMainColor <- 'mainhue'
    
  8. 定义显示包含在国旗中的图案/绘画(例如,一个形状、一张图片或一段文字)数量的属性名称:

    namesDrawings <- c(
      'circles', 'crosses', 'saltires', 'quarters',
      'sunstars', 'crescent', 'triangle', 'icon', 'animate', 'text'
    )
    
  9. 悬挂:四个角中的两个角的颜色:

    namesAngles <- c('topleft', 'botright')
    
  10. 定义包含所有名称且顺序正确的 namesFlag

    namesFlag <- c(namesNumbers, namesColors, nameMainColor, namesDrawings, namesAngles)
    
  11. 设置绑定 namesAttributesnamesFlagdfFlag 列名:

    names(dfFlag) <- c(namesAttributes, namesFlag)
    

现在,数据框具有正确的列名。然而,一些列,如 language,包含数字而不是属性名称,文档显示了这些数字的含义。例如,对于语言,1 对应英语,2 对应西班牙语。我们可以使用以下步骤构建一个具有正确格式的数据表:

  1. dfFlag 转换为 dtFlag 数据表:

    library(data.table)
    dtFlag <- data.table(dfFlag)
    
  2. 显示 continent 列:

    dtFlag[1:20, continent]
    [1] 5 3 4 6 3 4 1 1 2 2 6 3 1 5 5 1 3 1 4 1
    
    
  3. continent 列包含介于 16 之间的数字,文档显示 1=N.America2=S.America3=Europe4=Africa5=Asia6=Oceania。然后,我们定义一个包含大陆的向量:

    vectorContinents <- c('N.America', 'S.America', 'Europe', 'Africa', 'Asia', 'Oceania')
    
  4. continent 转换为 factor 类型,其级别为 vectorContinents

    dtFlag[, continent := factor(continent, labels=vectorContinents)]
    
  5. continent 类似,将 zone 转换为 factor

    vectorZones <- c('NE', 'SE', 'SW', 'NW')
    dtFlag[, zone := factor(zone, labels=vectorZones)]
    
  6. language 转换为 factor

    vectorLanguages <- c(
      'English', 'Spanish', 'French', 'German', 'Slavic',
      'Other Indo-European', 'Chinese', 'Arabic',
      'Japanese/Turkish/Finnish/Magyar', 'Others')
      dtFlag[, language := factor(language, labels=vectorLanguages)]
    
  7. religion 转换为 factor

    vectorReligions <- c(
      'Catholic', 'Other Christian', 'Muslim', 'Buddhist',
      'Hindu', 'Ethnic', 'Marxist', 'Others'
    )
    dtFlag[, religion := factor(religion, labels=vectorReligions)]
    

让我们来看看 dtFlag

str(dtFlag)
Classes 'data.table' and 'data.frame':	194 obs. of  30 variables:
 $ name      : Factor w/ 194 levels "Afghanistan",..: 1 2 3 4 5 6 7 8 9 10 ...
 $ continent : int  5 3 4 6 3 4 1 1 2 2 ...
 $ zone      : Factor w/ 4 levels "NE","SE","SW",..: 1 1 1 3 1 2 4 4 3 3 ...
 $ area      : int  648 29 2388 0 0 1247 0 0 2777 2777 ...
 $ population: int  16 3 20 0 0 7 0 0 28 28 ...
 $ language  : int  10 6 8 1 6 10 1 1 2 2 ...
 $ religion  : int  2 6 2 1 0 5 1 1 0 0 ...
 $ bars      : int  0 0 2 0 3 0 0 0 0 0 ...
 $ stripes   : int  3 0 0 0 0 2 1 1 3 3 ...
 $ colors    : int  5 3 3 5 3 3 3 5 2 3 ...
 $ red       : int  1 1 1 1 1 1 0 1 0 0 ...
 $ green     : int  1 0 1 0 0 0 0 0 0 0 ...
 $ blue      : int  0 0 0 1 1 0 1 1 1 1 ...
 $ gold      : int  1 1 0 1 1 1 0 1 0 1 ...
 $ white     : int  1 0 1 1 0 0 1 1 1 1 ...
 $ black     : int  1 1 0 0 0 1 0 1 0 0 ...
 $ orange    : int  0 0 0 1 0 0 1 0 0 0 ...
 $ mainhue   : Factor w/ 8 levels "black","blue",..: 5 7 5 2 4 7 8 7 2 2 ...
 $ circles   : int  0 0 0 0 0 0 0 0 0 0 ...
 $ crosses   : int  0 0 0 0 0 0 0 0 0 0 ...
 $ saltires  : int  0 0 0 0 0 0 0 0 0 0 ...
 $ quarters  : int  0 0 0 0 0 0 0 0 0 0 ...
 $ sunstars  : int  1 1 1 0 0 1 0 1 0 1 ...
 $ crescent  : int  0 0 1 0 0 0 0 0 0 0 ...
 $ triangle  : int  0 0 0 1 0 0 0 1 0 0 ...
 $ icon      : int  1 0 0 1 0 1 0 0 0 0 ...
 $ animate   : int  0 1 0 1 0 0 1 0 0 0 ...
 $ text      : int  0 0 0 0 0 0 0 0 0 0 ...
 $ topleft   : Factor w/ 7 levels "black","blue",..: 1 6 4 2 2 6 7 1 2 2 ...
 $ botright  : Factor w/ 8 levels "black","blue",..: 5 7 8 7 7 1 2 7 2 2 ...
 - attr(*, ".internal.selfref")=<externalptr> 

数据格式正确。尽管我们不得不正确地转换数据,但它仍然比手动定义特征花费的时间少得多。

探索和可视化特征

在定义了特征之后,我们可以探索它们并确定它们与问题的关系。在本节中,您将了解如何探索数据并定义一些简单的图表。

让我们从特征开始,例如,mainhue,它显示国旗的主要颜色。我们想要识别最常见的颜色,为此,我们可以使用 table 来计算每个可能值的出现次数。我们可以从 dtFlag 中提取 mainhue 列并对其应用 table

table(dtFlag[, mainhue])
black   blue  brown   gold  green orange    red  white 
 5     40      2     19     31      4     71     22

最常见的三种主要颜色是红色、蓝色和绿色。请注意,我们可以在方括号内放置 table,以获得相同的结果,但代码更简洁:dtFlag[, table(mainhue)]

我们如何对任何其他列执行相同的操作?首先,让我们定义一个名为 nameCol 的字符串,它包含我们想要分析的列的名称。为了访问该列,我们可以在 dtFlag 的方括号中使用 get(nameCol)

nameCol <- 'mainhue'
dtFlag[, table(get(nameCol))]

这种表示法非常有用,因为我们可以很容易地使用名称字符串将其包含在函数中,从而可视化其他所有列的相同结果:

listTableCol = lapply(
  namesAngles, function(nameCol){
    dtFlag[, table(get(nameCol))]
  })
listTableCol[[1]]
 black   blue   gold  green orange    red  white 
 12     43      6     32      4     56     41 

如果我们想制作一个图表呢?我们可以使用 barplot 制作直方图。让我们首先提取具有每个频率值的表格:

nameCol <- 'language'
freqValues <- dtFlag[, table(get(nameCol))]

freqValues 方法包含说列表中任何语言的国家的数量。我们可以使用 names 提取一个语言向量:

names(freqValues)

现在,我们已经拥有了构建直方图所需的所有必要数据(如果你还没有阅读第三章,请参阅barplot的文档)。此外,我们可以使用rainbow定义颜色:

barplot(
  height = freqValues,
  names.arg = names(freqValues),
  main = nameCol,
  col = rainbow(length(freqValues)),
  ylab = 'number of flags'
)

获得的直方图如下:

探索和可视化特征

如果我们想要探索一个属性,这个图表非常有用。为了仅用一行代码完成此操作,我们可以定义一个函数,为通用列nameCol构建此图表。此外,我们可以添加legend来显示百分比。为了显示legend,我们计算percValues,它包含显示该值的行百分比,并将其用作legend.text参数,如下所示:

barplotAttribute <- function(dtData, nameCol)
{
  # define the frequency
  freqValues <- dtData[, table(get(nameCol))]
  # define the percentage
  percValues <- freqValues / sum(freqValues)
  percValues <- round(percValues * 100)
  percValues <- paste(percValues, '%')
  # generate the histogram
  barplot(
    height = freqValues,
    names.arg = names(freqValues),
    main = nameCol,
    col = rainbow(length(freqValues)),
    legend.text = percValues,
    ylab = 'number of flags'
  )
}

让我们将函数应用于另一列,例如stripes

barplotAttribute(dtFlag, 'stripes')

探索和可视化特征

使用for循环,我们可以为每个标志属性生成相同的图表。我们需要时间在每张图表和下一张图表之间查看结果,所以我们使用readline停止程序。脚本会暂停,直到我们在控制台中按下Enter。这样,我们可以非常快速地探索所有特征,如下所示:

for(nameCol in namesFlag)
{
  barplotAttribute(dtFlag, nameCol)
  readline()
}

通过这几行代码,我们已经观察到了每个特征的值出现的频率。

另一种快速探索是,给定一种颜色,计算包含该颜色的标志数量。例如,让我们计算包含红色部分的标志。有一个名为red的属性,其值为1表示标志包含红色部分,否则为0。如果我们对所有列值求和,我们将获得包含红色部分的标志总数,如下所示:

dtFlag[, sum(red)]
[1] 153
dtFlag[, sum(get('red'))]
[1] 153

如我们之前所见,我们可以在方括号内使用get。我们如何为所有常见的颜色做同样的事情?namesColors向量包含了所有颜色属性的名称,如下所示:

namesColors
[1] "red"    "green"  "blue"   "gold"   "white"  "black"  "orange"

namesColors的第一个元素是red,因此我们可以用它来计算包含红色的标志:

dtFlag[, sum(get(namesColors[1]))]
[1] 153

我们可以使用sapply(请参阅文档)在namesColors的每个元素上应用一个函数。在这种情况下,该函数计算包含特定颜色的标志的数量:

sapply(namesColors, function(nameColor){
  dtFlag[, sum(get(nameColor))]
})
red  green   blue   gold  white  black orange 
153     91     99     91    146     52     26 

最常见的颜色是红色和绿色。

到目前为止,我们已经探索了标志的特征;下一步是看看它们如何与国家的语言相关。一种快速的方法是使用决策树(请参阅第三章,简单机器学习分析)。

首先,让我们导入生成和可视化决策树的包:

library('rpart')
library('rpart.plot')

决策树模型需要一个公式对象,用于定义变量之间的关系。在这种情况下,公式是language ~ feature1 + feature2 + …。我们可以通过添加namesFlag中包含的所有名称来构建公式,如下所示:

formulaRpart <- 'language ~ '
for(name in namesFlag){
  formulaRpart <- paste(formulaRpart, '+', name)
}
formulaRpart <- formula(formulaRpart)

我们可以使用rpart构建模型,并使用prp可视化树:

tree <- rpart(formula=formulaRpart, data=dtFlag)
prp(tree)

探索和可视化特征

树的一些节点不易阅读。例如,saltires 如果旗帜上有十字,则显示 1,否则显示 0。第一个树节点表示 saltires >= 0.5 条件,因此左侧的旗帜上有十字。这反映了特征格式不正确的事实,因此下一步将是转换特征。

首先,让我们定义一个新的数据表 dtFeatures,其中包含特征和结果。从现在起,我们将修改 dtFeatures,直到所有特征都处于正确的格式,如下所示:

dtFeatures <- dtFlag[, c('language', namesFlag), with=FALSE]

让我们定义一个函数来可视化表格。我们将重用此函数来跟踪特征转换过程中的进度,如下所示:

plotTree <- function(dtFeatures){
  formulaRpart <- paste(names(dtFeatures)[1], '~')
  for(name in names(dtFeatures)[-1]){
    formulaRpart <- paste(formulaRpart, '+', name)
  }
  formulaRpart <- formula(formulaRpart)
  tree <- rpart(formula=formulaRpart, data=dtFeatures)
  prp(tree)
}
plotTree(dtFeatures)

图表与之前完全相同。

到目前为止,我们已经看到了一些探索特征的技术。数据探索使我们能够调查数据性质,这是清理当前特征和定义其他特征的开端。此外,我们构建了一些函数,允许我们仅用一行代码生成一些图表。我们可以使用这些函数来跟踪特征转换。

修改特征

我们的特性是描述旗帜的属性,其中一些可能不在正确的格式中。在本节中,我们将查看每个特征,并在必要时对其进行转换。

为了跟踪我们已经处理过的特征,让我们开始定义一个空向量 namesProcessed,其中包含我们已经处理过的特征。当我们转换一个特征时,我们将特征名称添加到 namesProcessed 中:

namesProcessed <- c()

让我们从数值列开始,例如 red,它有两种可能的结果:0,如果旗帜包含红色,否则为 1red 变量定义了一个属性,因此它应该是分类的而不是数值的。然后,我们可以将 red 转换为特征,如果颜色是红色,则为 yes,否则为 no

如果我们查看每个特征的图表,我们会注意到其中一些只显示两个值,总是 01。为了将每个值转换为 yesno 格式,我们可以使用一个 for 循环。对于 namesFlag 中的每个特征,我们检查是否存在两种可能的值。如果是这样,我们将特征转换为因子。让我们从 red 开始:

nameFeat <- 'red'

我们可以检查 nameFeat 是否显示两个值:

length(unique(dtFeatures[, get(nameFeat)])) == 2

在这种情况下,答案是 TRUE,因此我们可以生成一个包含相同列的向量,对于 01 分别用 noyes 表示。为此,我们使用 factor,指定标签为 noyes,如下所示:

vectorFactor <- dtFeatures[
  , factor(get(nameFeat), labels=c('no', 'yes'))
]
head(vectorFactor)
[1] yes yes yes yes yes yes
Levels: no yes

现在,我们可以使用for循环将每个显示两种可能结果的特性进行转换。对于每个特性,我们使用if检查它是否只有两个值。在生成vectorFactor后,我们使用方括号内的eval覆盖旧列。执行dtFeatures[, eval('red') := vectorFactor]dtFeatures[, red := vectorFactor]相同,如下所示:

for(nameFeat in namesFlag){
  if(length(unique(dtFeatures[, get(nameFeat)])) == 2){
    vectorFactor <- dtFeatures[
      , factor(get(nameFeat), labels=c('no', 'yes'))]
    dtFeatures[, eval(nameFeat) := vectorFactor]
    namesProcessed <- c(namesProcessed, nameFeat)
  }
}

让我们看看我们还没有转换的特征。namesFlag特征包含所有初始特征,而namesProcessed包含我们已经转换的特征。为了可视化不在namesProcessed中的特征,我们可以使用setdiff函数,该函数给出第一个向量中存在而第二个向量中不存在的元素,如下所示:

setdiff(namesFlag, namesProcessed)

还有许多特征我们还没有分析。例如,bars是一个显示旗帜中垂直横杠数量的数值属性。如果我们使用bars作为数值特征,模型将识别语言和模型之间的关系。所有讲西班牙语国家的旗帜都包含零个或三个横杠,因此模型可以学习到“如果横杠少于四个,那么语言只能是西班牙语”这样的东西。然而,没有讲西班牙语的国家其旗帜有 1 个或 2 个横杠。一种解决方案是根据横杠的数量将国家分组,如下所示:

barplotAttribute(dtFeatures, 'bars')

修改特征

图表显示,具有显著数量旗帜的组是03横杠。因此,组可以如下所示:

  • 没有横杠的旗帜

  • 带有三个横杠的旗帜

  • 所有其他旗帜

我们可以定义一个名为nBars0的新列,如果旗帜没有垂直横杠,则等于TRUE

dtFeatures[, nBars0 := bars == 0]

同样,我们定义nBars3,对于有三个横杠的旗帜,它为TRUE。我们不需要为剩余的旗帜定义一个列,因为它们可以通过检查nBars0nBars3是否为FALSE来识别:

dtFeatures[, nBars1_2 := bars %in% c(1, 2)]
dtFeatures[, nBars3 := bars == 3]

让我们删除初始的bars列并将bars添加到namesProcessed中:

dtFeatures[, bars := NULL]
namesProcessed <- c(namesProcessed, 'bars')

我们执行的操作被称为离散化,因为我们从数值特征生成了一些离散特征。

同样,我们可以转换stripescolors

barplotAttribute(dtFeatures, 'stripes')
dtFeatures[, nStrp0 := stripes == 0]
dtFeatures[, nStrp2 := stripes == 2]
dtFeatures[, nStrp3 := stripes == 3]
dtFeatures[, nStrp5 := stripes == 5]
dtFeatures[, stripes := NULL]
namesProcessed <- c(namesProcessed, 'stripes')

barplotAttribute(dtFeatures, 'colors')
dtFeatures[, nCol12 := colors %in% c(1, 2)]
dtFeatures[, nCol3 := colors == 3]
dtFeatures[, nCol4_5 := colors %in% c(4, 5)]
dtFeatures[, colors := NULL]
namesProcessed <- c(namesProcessed, 'colors')

让我们看看namesDrawings中我们还没有处理过的特征:

for(nameCol in setdiff(namesDrawings, namesProcessed)){
  barplotAttribute(dtFeatures, nameCol)
  readline()
}

修改特征

在所有这些特征中,大多数旗帜显示0。因此,我们可以将旗帜分为两类:0和其余部分。我们正在定义一个新的分类变量,如果值大于0则为yes,否则为no。这个过程被称为二值化,因为我们把一些数值特征转换成了只显示两个值的分类特征,如下所示:

for(nameCol in setdiff(namesDrawings, namesProcessed)){
  dtFeatures[, eval(nameCol) := ifelse(get(nameCol) > 0, 'yes', 'no')]
  namesProcessed <- c(namesProcessed, nameCol)
}

让我们探索以下代码中显示的剩余特征:

for(nameCol in setdiff(namesFlag, namesProcessed)){
  barplotAttribute(dtFeatures, nameCol)
  readline()
}

得到的图表如下:

修改特征

剩下的三个特征是topleftbotrightmainhue。它们都是分类的,并且显示超过两个可能的值。例如,mainhue有八个选项。然而,只有少数标志以blackbrownorange为主要颜色。我们没有足够的信息来考虑不太常见的颜色。在这种情况下,我们可以为每个它们定义一个新的分类变量,称为虚拟变量。我们可以决定为至少有 15 个标志的每个可能颜色定义一个虚拟变量。对于topleftbotright的情况也是类似的,因此我们可以以相同的方式转换所有这些特征,如下所示:

namesToDummy <- c("topleft", "botright", "mainhue")
for(nameCol in namesToDummy){
  frequencyColors <- dtFeatures[, list(.N), by=nameCol]
  for(color in frequencyColors[N > 20, get(nameCol)]){
    nameFeatNew <- paste(nameCol, color, sep='')
    dtFeatures[, eval(nameFeatNew) := get(nameCol) == color]
  }
  dtFeatures[, eval(nameCol) := NULL]
  namesProcessed <- c(namesProcessed, nameCol)
}

现在,我们已经转换了所有特征。然而,我们定义的一些新列属于逻辑类别。最好将它们可视化为主要显示yesno的分类属性,因此最好将它们转换,如下所示:

for(nameCol in names(dtFeatures)){
  if(dtFeatures[, class(get(nameCol))] == 'logical'){
    print(nameCol)
    dtFeatures[, eval(nameCol) := ifelse(get(nameCol), 'yes', 'no')]
  }
}

让我们看看以下代码如何改变决策树:

plotTree(dtFeatures)

得到的图表如下:

修改特征

决策树与之前的树相似。然而,决策树的每个节点都在检查一个结果为yesno的条件。

在本章中,我们看到了三种转换特征的方法:

  • 离散化:从一个数值变量开始,我们将所有可能的值分组到集合中。然后,对于每个集合,我们定义一个新的变量,如果数值变量属于该集合则显示yes,否则显示no

  • 二值化:从一个数值变量开始,我们通过定义两个集合来离散化一个数值变量。我们定义一个阈值,并检查变量是否高于或低于该阈值。

  • 虚拟变量:从一个分类变量开始,我们识别最常见的输出。然后,对于每个常见输出,我们定义一个新的变量,如果变量等于该值则显示yes,否则显示no

使用过滤器或降维对特征进行排序

在上一节中,我们定义了不同的特征。但它们是否真的与问题相关?有一些称为嵌入式模型的技术可以自动选择最相关的特征。我们也可以使用不同的特征集构建相同的机器学习模型,并选择性能更好的集合。这两种选择都很好,尽管它们需要大量的计算能力。

另一种方法是使用过滤器,这是一种识别最相关特征的技术。我们在应用任何机器学习模型之前使用过滤器,这样我们就能大幅减少算法的计算成本。一些过滤器会单独考虑每个特征,并且计算效率非常高。

一个简单的过滤器是皮尔逊相关系数,它是衡量变量之间线性关系的度量。相关系数是一个介于-1 和 1 之间的数字,这两个极端值表示两个变量之间存在清晰的线性关系。当绘制图表时,所有点都位于同一条线上。相关系数为 0 表示两个变量之间没有线性依赖关系。相关系数的绝对值越高,线性关系越强。在我们的案例中,我们可以测量每个标志属性与语言之间的相关性,并选择相关性系数较高的属性。

另一种考虑每个特征的技术是信息增益率。假设我们想要构建一个模型,但我们对标志一无所知。在这种情况下,我们能做的最好的事情就是识别最常见的语言,并假设每个国家都说那种语言。如果我们只知道哪些旗帜包含红色呢?模型肯定会比没有任何信息要好。好多少呢?一个特征的信息增益率是一个指数,它量化了添加该特征所带来的改进。

相关性和信息增益率分别考虑每个特征,因此它们完全忽略了它们之间的相互作用。例如,我们可以有两个对语言有重大影响的特征,并且它们之间关系非常紧密,以至于它们包含相同的信息。假设我们已经在模型中包含了一个这两个特征中的一个。添加另一个不会提供任何更多信息,尽管它本身可能非常相关。如果两个特征之间的关系是线性的,我们谈论的是多重共线性

在其他情况下,我们有两个特征,如果单独考虑,它们的相关性很小,但如果一起考虑,则具有很大的影响。如果我们使用这种类型的过滤器对特征进行排序,我们将排除这两个特征,从而丢失一些有用的信息。

对特征进行排序的另一种方法是识别相关的特征组合。一种技术是主成分分析(PCA),它基于特征之间的相关性。从特征开始,PCA 定义了一组称为主成分的变量,它们彼此线性独立。主成分的数量等于或小于特征的数量,并且这些成分按方差排序。然后,可以选择具有高方差的成分子集。然而,PCA 有其局限性,因为它仅基于线性关系,并且它没有考虑到预测事物的属性(在我们的例子中是语言)。

有不同的技术,我们在这章中使用的是信息增益比,因为它简单且有意义。R 为我们提供了FSelector包,其中包含用于特征选择的工具。该包要求你在计算机上安装 JRE,如下所示:

install.packages('FSelector')
library('FSelector')

让我们构建一个包含所有特征名称的namesFeatures向量。然后,我们可以使用information.gain函数计算它们的信息增益比,如下所示:

namesFeatures <- names(dtFeatures)[-1]
dfGains <- information.gain(language~., dtFeatures)

dfGains方法是一个具有名为attr_importance字段的 DataFrame。特征名称是行名称,所以让我们添加另一个包含名称的列:

dfGains$feature <- row.names(dfGains)

让我们将数据框转换为数据表:

dtGains <- data.table(dfGains)

为了看到最相关的特征,我们可以按相关性对它们进行排序:

dtGains <- dtGains[order(attr_importance, decreasing = T)]
 head(dtGains)
 attr_importance      feature
1:       0.1583055         blue
2:       0.1537296     saltires
3:       0.1313155 botrightblue
4:       0.1262545  mainhueblue
5:       0.1205012       nStrp3
6:       0.1149405     quarters

bluesaltires特征定义了非常相关的属性。为了可视化最相关的特征,我们可以构建一个包含前 12 个属性的图表,如下所示:

dtGainsTop <- dtGains[1:12]
barplot(
  height = dtGainsTop[, attr_importance],
  names.arg = dtGainsTop[, feature],
  main = 'information gain',
  col = rainbow(nrow(dtGainsTop)),
  legend.text = dtGainsTop[, feature],
  xlim=c(0, 20)
)

获得直方图如下:

使用过滤器或降维来排序特征

现在我们已经定义了特征排序,我们能够从最相关的特征构建模型。我们可以包括所有相关性高于所选阈值的特征,或者从顶部开始选择一定数量的特征。然而,我们还没有考虑到特征之间的交互。例如,在顶级特征中,我们有旗帜包含蓝色蓝色是主要颜色右下角是蓝色。尽管它们都非常相关,但它们都是关于蓝色的,所以它们是冗余的,我们可以排除其中一个。

总之,过滤器是快速且有用的方法来排序特征,但当我们构建模型时,我们必须非常小心地使用它们。

摘要

在本章中,你学习了如何进行特征选择。在加载和探索特征之后,你看到了如何使用离散化和二值化来转换它们。你还把分类特征转换为虚拟变量。你理解了特征选择的重要性,并使用信息增益比对特征进行了排序。在下一章中,我们将使用机器学习技术来预测语言。

第五章。步骤 2 – 应用机器学习技术

本章重点在于应用机器学习算法,这是开发解决方案的核心。有不同类型的从数据中学习的技巧。根据我们的目标,我们可以使用数据来识别对象之间的相似性或对新对象估计属性。

为了展示机器学习技术,我们从上一章中处理过的旗帜数据开始。然而,阅读本章不需要你了解前面的内容,尽管了解数据来源是推荐的。

在本章中,你将学习到:

  • 识别项目的一致性组

  • 探索和可视化项目组

  • 估计一个新国家的语言

  • 设置机器学习技术的配置

识别项目的一致性组

我们的数据描述了每个国家旗帜。有没有办法识别具有类似旗帜属性的国家组?我们可以使用一些聚类技术,这些是机器学习算法,它们使用数据定义同质集群。

从上一章的旗帜属性开始,我们构建了一个特征表并将其存储到dtFeatures.txt文件中。为了将文件加载到 R 中,第一步是使用setwd定义包含文件的目录。然后,我们可以使用read.table将文件加载到dfFeatures数据框中,并将其转换为dtFeatures数据表,如下所示:

# load the flag features
setwd('<INSER YOUR DIRECTORY/PATH>")
dfFeatures <- read.table(file = 'dtFeatures.txt')
library("data.table")
dtFeatures <- data.table(dfFeatures)

让我们来看看数据,使用str,类似于前面的章节:

# explore the features
str(dtFeatures)
Classes 'data.table' and 'data.frame':	194 obs. of  38 variables:
 $ language     : Factor w/ 10 levels "Arabic","Chinese",..: 8 7 1 3 7 8 3 3 10 10 ...
 $ red          : Factor w/ 2 levels "no","yes": 2 2 2 2 2 2 1 2 1 1 ...
 $ green        : Factor w/ 2 levels "no","yes": 2 1 2 1 1 1 1 1 1 1 ...
 $ blue         : Factor w/ 2 levels "no","yes": 1 1 1 2 2 1 2 2 2 2 ...

语言列是一个因素,有 10 种语言,称为该因素的级别。所有其他列都包含描述旗帜的特征,它们是具有两个级别的因素:。特征如下:

  • 如果旗帜包含颜色,则colors特征(例如,red)具有级别

  • 如果旗帜包含图案,则patterns特征(例如,circle)具有级别

  • 后跟数字的nBars/nStrp/nCol特征(例如,nBars3)如果旗帜有 3 条横线,则具有级别

  • 后跟颜色的topleft/botright/mainhue特征(例如,topleftblue)如果左上部分是蓝色,则具有级别

使用 k-means 识别组

我们的目标是识别类似旗帜的组。为此,我们可以开始使用基本的聚类算法,即k-means

k-means 的目标是识别k(例如,八个)同质标志聚类。想象一下将所有标志分成八个聚类。其中一个包含 10 个标志,其中 7 个包含红色。假设我们有一个red属性,如果标志包含红色则为1,否则为0。我们可以说这个聚类的average flag包含红色的概率为 70%,因此其red属性为 0.7。对每个其他属性做同样的处理,我们可以定义average flag,其属性是组内的平均值。每个聚类都有一个平均标志,我们可以使用相同的方法来确定。

k-means 算法基于一个称为聚类中心的平均对象。一开始,算法将标志分为 8 个随机组并确定它们的 8 个中心。然后,k-means 将每个标志重新分配到中心最相似的组。这样,聚类更加同质化,算法可以重新计算它们的中心。经过几次迭代后,我们就有 8 个包含同质标志的组。

k-means 算法是一个非常流行的技术,R 为我们提供了kmeans函数。为了使用它,我们可以查看其帮助信息:

# K-MEANS
# see the function documentation
help(kmeans)

我们需要两个输入:

  • x:数值数据矩阵

  • centers:聚类数量(或开始时的聚类中心)

dtFeatures开始,我们需要构建一个数值特征矩阵dtFeaturesKm。首先,我们可以将特征名称放入arrayFeatures中,并生成包含所有特征的dtFeaturesKm数据表。执行以下步骤:

  1. 定义包含特征名称的arrayFeatures向量。dtFeatures方法包含第一列的属性和其余列的特征,因此我们提取除第一列之外的所有列名:

    arrayFeatures <- names(dtFeatures)[-1]
    
  2. 定义包含特征的dtFeaturesKm

    dtFeaturesKm <- dtFeatures[, arrayFeatures, with=F]
    
  3. 将通用列(例如,red)转换为数值格式。我们可以使用as.numeric将列格式从因子转换为数值:

    dtFeaturesKm[, as.numeric(red)]
    
  4. 新向量包含1如果值是no,如果是yes则包含2。为了与我们的 k-means 描述使用相同的标准,我们更愿意将no属性设置为0,将yes属性设置为1。这样,当我们计算组内的平均属性时,它将是一个介于 0 和 1 之间的数字,可以看作是属性为yes的标志部分的百分比。然后,为了得到 0 和 1,我们可以使用as.numeric(red) – 1

    dtFeaturesKm[, as.numeric(red) - 1]
    

    或者,我们也可以使用 ifelse 函数完成同样的操作。

  5. 我们需要将每个列格式转换为 0-1。arrayFeatures数据表包含所有特征的名称,我们可以使用for循环处理每个特征。如果我们想转换包含在nameCol中的列名,我们需要使用eval-get表示法。使用eval(nameCol) :=我们重新定义列,使用get(nameCol)我们使用列的当前值,如下所示:

    for(nameCol in arrayFeatures)
      dtFeaturesKm[
        , eval(nameCol) := as.numeric(get(nameCol)) - 1
        ]
    
  6. 现在将所有特征转换为 0-1 格式。让我们可视化它:

    View(dtFeaturesKm)
    
  7. kmeans函数需要数据以矩阵形式。为了将dtFeaturesKm转换为矩阵,我们可以使用as.matrix

    matrixFeatures <- as.matrix(dtFeaturesKm)
    

matrixFeatures数据表包含构建 k-means 算法的数据,其他kmeans输入是参数。k-means 算法不会自动检测集群数量,因此我们需要通过centers输入来指定它。给定对象集,我们可以从中识别出任意数量的集群。哪个数字最能反映数据?有一些技术允许我们定义它,但它们超出了本章的范围。我们可以定义一个合理的中心数量,例如,8:

# cluster the data using the k-means
nCenters <- 8
modelKm <- kmeans(
  x = matrixFeatures,
  centers = nCenters
  )

modelKm函数是一个包含不同模型组件的列表。kmeans的帮助提供了关于输出的详细描述,我们可以使用names来获取元素名称。让我们看看组件:

names(modelKm)
[1] "cluster"      "centers"      "totss"        "withinss" 
[5] "tot.withinss" "betweenss"    "size"         "iter" 
[9] "ifault" 

我们可以可视化包含在centers中的集群中心,如下所示:

View(modelKm$centers)

每行定义一个中心,每列显示一个属性。所有属性都在 0 到 1 之间,它们代表具有属性等于1的集群中旗帜的百分比。例如,如果red0.5,这意味着一半的旗帜包含红色。

我们将使用的是cluster元素,它包含一个标签,指定每个旗帜的集群。例如,如果一个集群的第一个元素是3,这意味着matrixFeatures(以及dtFeatures)中的第一个旗帜属于第三个集群。

探索集群

我们可以查看每个集群,以探索其旗帜。为了做到这一点,我们可以在定义clusterKm列时将集群添加到初始表中,如下所示:

# add the cluster to the data table
dtFeatures[, clusterKm := modelKm$cluster]

为了探索一个集群,我们可以确定其国家中有多少个国家使用每种语言。从dtFeatures开始,我们可以使用数据表聚合来总结每个集群的数据。首先,让我们定义包含集群的列:

# aggregate the data by cluster
nameCluster <- 'clusterKm'

我们想确定每个集群中有多少行。允许我们确定行数的表格命令是.N,如下所示:

dtFeatures[, list(.N), by=nameCluster]

如果我们想为集群大小指定不同的列名,我们可以在列表中指定它,如下所示:

dtFeatures[, list(nCountries=.N), by=nameCluster]

为了确定每种语言有多少个国家,我们可以使用table

dtFeatures[, table(language)]

为了在聚合中使用table,输出应该是列表。为此,我们可以使用as.list将表转换为列表,如下所示:

dtFeatures[, as.list(table(language))]

现在,我们可以使用by对每个组应用此操作,如下所示:

dtFeatures[, as.list(table(language)), by=nameCluster]

如果我们想可视化说每种语言的国家百分比?我们可以将表中的每个值除以集群中的国家数量,如下所示:

dtFeatures[, as.list(table(language) / .N), by=nameCluster]

我们希望生成包含每个组国家数量和每种语言百分比的dtClusters。为了做到这一点,我们可以使用我们刚刚看到的命令生成两个列表。为了合并这两个列表,我们只需使用c(list1, list2),如下所示:

dtClusters <- dtFeatures[
  , c(list(nCountries=.N), as.list(table(language) / .N)),
  by=nameCluster
  ]

dtClusters的每一行代表一个聚类。nCountries列显示聚类中的国家数量,所有其他列显示每种语言的百分比。为了可视化这些数据,我们可以为每个聚类构建一个条形图。每个条形被分割成代表说每种语言的国家数量的段。barplot函数允许我们构建所需的图表,如果我们提供矩阵作为输入。每个矩阵列对应一个条形,每行定义条形分割的块。

我们需要定义一个包含语言百分比的矩阵。这可以通过执行以下步骤来完成:

  1. 定义包含dtClusters语言列名称的arrayLanguages

    arrayLanguages <- dtFeatures[, unique(language)]
    
  2. 构建dtBarplot包含语言列:

    dtBarplot <- dtClusters[, arrayLanguages, with=F]
    
  3. 使用as.matrixdtBarplot转换为矩阵。为了构建图表,我们需要使用 R 函数t转置矩阵(反转行和列):

    matrixBarplot <- t(as.matrix(dtBarplot))
    
  4. 定义一个包含聚类大小的向量,即国家数量。我们将在列下显示这些数字:

    nBarplot <- dtClusters[, nCountries]
    
  5. 将图例名称定义为国家名称:

    namesLegend <- names(dtBarplot)
    
  6. 减少图例名称的长度,以避免图例与图表重叠。使用substring,我们将名称限制为 12 个字符,如下所示:

    help(substring)
    namesLegend <- substring(namesLegend, 1, 12)
    
  7. 使用rainbow定义颜色。我们需要为namesLegend的每个元素定义一个颜色,因此颜色的数量是length(namesLegend),如下所示:

    arrayColors <- rainbow(length(namesLegend))
    
  8. 使用paste定义图表标题:

    plotTitle <- paste('languages in each cluster of', nameCluster)
    

现在我们有了所有barplot输入,因此我们可以构建图表。为了确保图例不与条形重叠,我们包括xlim参数,该参数指定绘图边界,如下所示:

# build the histogram
barplot(
  height = matrixBarplot,
  names.arg = nBarplot,
  col = arrayColors,
  legend.text = namesLegend,
  xlim = c(0, ncol(matrixBarplot) * 2),
  main = plotTitle,
  xlab = 'cluster'
)

得到的图表如下:

探索聚类

K-means 算法从通过随机分割数据定义的初始聚类开始执行一系列步骤。最终输出取决于每次运行算法时不同的初始随机分割。因此,如果我们多次运行 k-means,可能会得到不同的结果。然而,这个图表帮助我们识别语言组内的某些模式。例如,在第八个聚类中,几乎所有国家都说英语,因此我们可以推断出有一些使用类似国旗的英语国家。在第五个聚类中,超过一半的国家说法语,因此我们可以得出同样的结论。一些不太相关的结果是,阿拉伯语在第一个聚类中占有很高的比例,西班牙语在第七个聚类中相当相关。

我们正在使用其他聚类算法,并将以类似的方式可视化结果。为了使代码干净且紧凑,我们可以定义plotCluster函数。输入是dtFeatures特征数据表和聚类列名nameCluster。代码几乎与前面的相同,如下所示:

# define a function to build the histogram
plotCluster <- function(
  dtFeatures, # data table with the features
  nameCluster # name of the column defining the cluster
){
  # aggregate the data by cluster
  dtClusters <- dtFeatures[
    , c(list(nCountries=.N), as.list(table(language) / .N)),
    by=nameCluster]

  # prepare the histogram inputs
  arrayLanguages <- dtFeatures[, unique(language)]
  dtBarplot <- dtClusters[, arrayLanguages, with=F]
  matrixBarplot <- t(as.matrix(dtBarplot))
  nBarplot <- dtClusters[, nCountries]
  namesLegend <- names(dtBarplot)
  namesLegend <- substring(namesLegend, 1, 12)
  arrayColors <- rainbow(length(namesLegend))

  # build the histogram
  barplot(
    height = matrixBarplot,
    names.arg = nBarplot,
    col = arrayColors,
    legend.text = namesLegend,
    xlim=c(0, ncol(matrixBarplot) * 2),
    main = paste('languages in each cluster of', nameCluster),
    xlab = 'cluster'
  )

}

此函数应构建与上一个相同的直方图。让我们使用以下代码来检查它:

# visualize the histogram using the functions
plotCluster(dtFeatures, nameCluster)

另一种可视化聚类的方法是使用不同颜色为每个聚类构建世界地图。此外,我们还可以可视化语言的世界地图。

为了构建地图,我们需要安装和加载rworldmap包,如下所示:

# define a function for visualizing the world map
install.packages('rworldmap')
library(rworldmap)

此包从国家名称开始构建世界地图,即在我们的案例中是dfFeatures行的名称。我们可以将country列添加到dtFeatures中,如下所示:

dtFeatures[, country := rownames(dfFeatures)]

我们的数据相当旧,所以德国仍然分为两部分。为了在地图上可视化它,我们可以将Germany-FRG转换为Germany。同样,我们可以将USSR转换为Russia,如下所示:

dtFeatures[country == 'Germany-FRG', country := 'Germany']
dtFeatures[country == 'USSR', country := 'Russia']

现在,我们可以定义一个函数来构建显示聚类的世界地图。输入是dtFeatures数据表和要可视化的特征colPlot列名(例如,clusterKm)。另一个参数是colourPalette,它决定了地图中使用的颜色。有关更多信息,请参阅help(mapCountryData),如下所示:

plotMap <- function(
  dtFeatures, # data table with the countries
  colPlot # feature to visualize
  colourPalette = 'negpos8' # colors
){
  # function for visualizing a feature on the world map

我们定义了包含要可视化的聚类的colPlot列。在字符串的情况下,我们只使用前 12 个字符,如下所示:

  # define the column to plot
  dtFeatures[, colPlot := NULL]
  dtFeatures[, colPlot := substring(get(colPlot), 1, 12)]

我们构建了包含我们构建图表所需数据的mapFeatures。有关更多信息,请参阅help(joinCountryData2Map)joinCode = 'NAME'输入指定国家由其名称定义,而不是缩写。nameJoinColumn指定我们拥有国家名称的列,如下所示:

  # prepare the data to plot
  mapFeatures <- joinCountryData2Map(
    dtFeatures[, c('country', 'colPlot'), with=F],
    joinCode = 'NAME',
    nameJoinColumn = 'country'
  )

我们可以使用mapCountryData构建图表。我们指定使用彩虹的颜色,并且缺失数据的该国将以灰色显示,如下面的代码所示:

  # build the chart
  mapCountryData(
    mapFeatures,
    nameColumnToPlot='colPlot',
    catMethod = 'categorical',
    colourPalette = colourPalette,
    missingCountryCol = 'gray',
    mapTitle = colPlot
  )

}

现在,我们可以使用plotMap在地图上可视化 k-means 聚类,如下所示:

plotMap(dtFeatures, colPlot = 'clusterKm')

探索聚类

我们可以看到许多亚洲国家属于第五个聚类。此外,我们可以观察到意大利、法国和爱尔兰属于同一个聚类,因为它们的旗帜相似。除此之外,很难识别出其他任何模式。

识别聚类的层次结构

识别同质群体的其他技术是层次聚类算法。这些技术通过迭代合并对象来构建聚类。一开始,我们为每个国家都有一个聚类。我们定义了两个聚类如何相似的一个度量,并在每一步中,我们识别出旗帜最相似的两组聚类并将它们合并成一个唯一的聚类。最后,我们有一个包含所有国家的聚类。

执行层次聚类的 R 函数是 hclust。让我们看看它的 help 函数:

# HIERARCHIC CLUSTERING
# function for hierarchic clustering
help(hclust)

第一个输入是 d,文档解释说它是一个差异结构,即包含所有对象之间距离的矩阵。如文档建议,我们可以使用 dist 函数来构建输入,如下所示:

# build the distance matrix
help(dist)

dist 的输入是一个描述旗帜的数值矩阵。我们已为 k-means 算法构建了 matrixDistances,因此我们可以重用它。另一个相关输入是 method,它指定了 dist 如何测量两个旗帜之间的距离。我们应该使用哪种方法?所有特征都是二进制的,因为它们有两种可能的输出,即 01。因此,距离可以是具有不同值的属性的数量。以这种方式确定距离的 method 对象是 manhattan,如下所示:

matrixDistances <- dist(matrixFeatures, method = 'manhattan')

matrixDistances 函数包含任何两个旗帜之间的差异。另一个输入是 method,它指定了聚合方法。在我们的情况下,我们将方法设置为 completemethod 有其他选项,它们定义了连接,即计算簇之间距离的方式,如下所示:

# build the hierarchic clustering model
modelHc <- hclust(d = matrixDistances, method = 'complete')

modelHc 方法包含聚类模型,我们可以使用 plot 来可视化簇。你可以查阅 hclust 的帮助来了解 plot 参数,如下所示:

# visualize the hierarchic clustering model
plot(modelHc, labels = FALSE, hang = -1)

识别簇的层次结构

此图表显示了算法过程。在底部,我们有所有国家,每个旗帜属于不同的簇。每条线代表一个簇,当算法合并簇时,线会汇聚。在图表的左侧,你可以看到一个表示旗帜之间距离的刻度,在每一级,算法合并彼此距离特定的簇。在顶部,所有旗帜都属于同一个簇。这个图表被称为树状图。考虑以下代码:

# define the clusters
heightCut <- 17.5
abline(h=heightCut, col='red')

我们想要识别的簇是红色线以上的簇。从 modelHc 开始识别簇的函数是 cutree,我们可以在 h 参数中指定水平线的高度,如下所示:

cutree(modelHc, h = heightCut)

现在,我们可以将簇添加到 dtFeatures 中,如下所示:

dcFeatures[, clusterHc := cutree(modelHc, h = heightCut)]

如前所述,我们可以看到每个簇中使用的语言。我们可以重用 plotClusterplotMap

# visualize the clusters
plotCluster(dtFeatures, nameCluster = 'clusterHc')

识别簇的层次结构

在第八个簇中,英语是主要语言。除此之外,阿拉伯语只在第一个簇中相关,法语和德语如果一起考虑,在第二个和第三个簇中相关,西班牙语在第三个簇中相关。

我们还可以用簇可视化世界地图,如下所示:

plotMap(dtFeatures, colPlot = 'clusterHc')

得到的图表如下:

识别簇的层次结构

与 k-means 类似,唯一有一个主要簇的大陆是亚洲。

本节描述了两种识别同质旗帜集群的流行聚类技术。它们都允许我们理解不同旗帜之间的相似性,我们可以利用这些信息作为支持来解决一些问题。

应用 k 最近邻算法

本节展示了如何使用一种简单的监督学习技术——k 最近邻KNN),从其旗帜开始估计一个新国家的语言。在这种情况下,我们估计的是语言,这是一个categoric属性,所以我们使用分类技术。如果属性是数值的,我们会使用回归技术。我选择 KNN 的原因是它易于解释,并且有一些选项可以修改其参数以提高结果的准确性。

让我们看看 KNN 是如何工作的。我们知道 150 个国家的旗帜和语言,我们想要根据其旗帜确定一个新国家的语言。首先,我们确定与新的旗帜最相似的 10 个国家。其中,有六个西班牙语国家,两个英语国家,一个法语国家和一个阿拉伯语国家。

在这 10 个国家中,最常见的语言是西班牙语,因此我们可以预期新的旗帜属于一个讲西班牙语的国家。

KNN 基于这种方法。为了估计一个新国家的语言,我们确定旗帜最相似的K个国家。然后,我们估计新国家说的是他们中最常见的语言。

我们有一个表格,通过 37 个二进制属性描述了 194 个旗帜,这些属性可以是YesNo。例如,mainhuegreen属性是yes,如果旗帜的主要颜色是绿色,否则是no。所有属性都描述了旗帜的颜色和图案。

与上一节类似,在修改dtFeatures之前,我们定义了包含特征名称的arrayFeatures。由于我们向dtFeatures添加了一些列,所以我们从dfFeatures中提取特征名称。然后,我们添加了包含来自dfFeatures的国家名称的country列,如下所示:

# define the feature names
arrayFeatures <- names(dfFeatures)[-1]
# add the country to dtFeatures
dtFeatures[, country := rownames(dfFeatures)]
dtFeatures[country == 'Germany-FRG', country := 'Germany']
dtFeatures[country == 'USSR', country := 'Russia']

dtFeatures开始,我们可以应用 KNN。给定一个新的旗帜,我们如何确定最相似的 10 个旗帜?对于任何两个旗帜,我们可以测量它们之间的相似度。最简单的方法是计算两个旗帜中有多少特征值相同。它们共有的属性越多,它们就越相似。

在上一章中,我们已经探索并转换了特征,因此我们不需要处理它们。然而,我们还没有探索语言列。对于每种语言,我们可以使用table来确定说这种语言的国家数量,如下所示:

dtFeatures[, table(language)]

不同语言的国家数量差异很大。最受欢迎的语言是英语,有 43 个国家,还有一些语言只有四个国家。为了对所有语言有一个概览,我们可以通过构建图表来可视化表格。在前一节中,我们定义了plotMap,它显示了世界地图上的群体。我们可以用它来显示说每种语言的国家,如下所示:

plotMap(dtFeatures, colPlot = 'language', colourPalette = 'rainbow')

得到的图表如下:

应用 k 最近邻算法

看到一张显示说每种语言的国家地图是件好事,但它仍然有点难以理解群体的大小。更好的选择是生成一个饼图,其切片与每个群体中的国家数量成比例。R 函数是pie,如下所示:

# visualize the languages
help(pie)

pie函数需要一个输入,即包含每种语言说国家数量的向量。如果输入向量的字段有名称,它将在图表中显示。我们可以使用table构建所需的向量,如下所示:

arrayTable <- dtFeatures[, table(language)]

幸运的是,pie不需要任何其他参数:

pie(arrayTable)

得到的图表如下:

应用 k 最近邻算法

有些语言只在少数几个国家说。例如,只有 4 个斯拉夫国家。给定一个新国家,我们想要从其国旗开始确定其语言。让我们假设我们不知道这 4 个斯拉夫国家中有一个国家说的是哪种语言。如果我们考虑其 10 个最近的邻居,其中不可能有超过 3 个其他斯拉夫国家。如果在其 10 个邻居中有 4 个说英语的国家呢?尽管在其附近还有其他斯拉夫国家,但由于英语群体更大,所以算法会估计这个国家说的是英语。同样,我们也会遇到任何其他小群体的问题。像几乎所有的机器学习算法一样,KNN 无法对属于任何其他更小群体的国家进行分类。

在处理任何分类问题时,如果某些群体很小,我们就没有足够的相关信息。在这种情况下,即使是一个很好的技术也无法对属于小群体的新对象进行分类。此外,给定一个属于中等大小群体的新国家,它很可能有很多属于大群体的邻居。因此,说这些语言之一的新国家可能会被分配到大型群体中。

通过了解模型限制,我们可以定义一个可行的机器学习问题。为了避免存在小群体,我们可以合并一些群体。聚类技术使我们能够识别哪些语言群体定义得更好,相应地,我们可以将这些群体中的语言分开:英语西班牙语法语和德语斯拉夫语和其他印欧语系阿拉伯语其他

我们可以定义语言组来构建listGroups,其元素包含组说的语言。例如,我们可以定义包含SlavicOther Indo-European语言的indoEu组,如下所示:

# reduce the number of groups
listGroups <- list(
  english = 'English',
  spanish = 'Spanish',
  frger = c('French', 'German'),
  indoEu = c('Slavic', 'Other Indo-European'),
  arabic = 'Arabic',
  other = c(
    'Japanese/Turkish/Finnish/Magyar', 'Chinese', 'Others'
    )
  )

现在,我们可以重新定义包含语言组的language列。对于listGroups的每个元素,我们将所有语言转换为元素名称。例如,我们将SlavicOther Indo-European转换为indoEu

我们可以在for循环内执行此操作。所有的组名都包含在names(listGroups)列表中,因此我们可以遍历names(listGroups)的元素,如下所示:

for(nameGroup in names(listGroups)){

在这里,nameGroup定义了一个组名,listGroups[[nameGroup]]包含其语言。我们可以使用language %in% listGroups[[nameGroup]]提取说任何组语言的dtFeatures的行。然后,我们可以使用:=数据表符号将语言列重新分配给nameGroup组名,如下所示:

  dtFeatures[
    language %in% listGroups[[nameGroup]],
    language := nameGroup
    ]
}

我们重新定义了language列,按语言进行分组。让我们看看它:

dtFeatures[, language]

在这里,language是一个因子,并且只有六个可能的级别,即我们的语言组。然而,你可以看到 R 在控制台打印了16 Levels: Arabic Chinese English French ... Other。原因是language列的格式是factor,它跟踪前 10 个初始值。为了只显示六个语言组,我们可以使用factor重新定义language列,如下所示:

dtFeatures[, language := factor(language)]
dtFeatures[, language]

现在我们只有六个级别。就像我们之前做的那样,我们可以使用plotMap可视化组大小数据,如下所示:

# visualize the language groups
plotMap(dtFeatures, colPlot = 'language')

得到的地图如下:

应用 k 最近邻算法

我们可以看到,每个类别的国家在地理上彼此相邻。

为了可视化新的组大小,我们可以使用pie,如下所示:

pie(dtFeatures[, table(language)])

得到的图表如下:

应用 k 最近邻算法

所有的六个组都有足够的国家。英语其他组比其他组稍大,但大小是可比的。

现在我们可以构建 KNN 模型。R 为我们提供了包含 KNN 算法的kknn包。让我们按照以下步骤安装和加载包:

# install and load the package
install.packages("kknn")
library(kknn)

构建 KNN 的函数称为kknn,例如在包中。让我们看看它的帮助函数:

help(kknn)

第一个输入是公式,它定义了特征和输出。然后,我们必须定义一个训练集,包含用于构建模型的数据,以及一个测试集,包含应用模型的数据。我们使用训练集的所有信息,假装不知道测试集国家的语言。还有其他一些可选输入定义了一些模型参数。

所有的特征名称都包含在arrayFeatures中。为了定义输出如何依赖于特征,我们需要构建一个格式为output ~ feature1 + feature2 + …的字符串。执行以下步骤:

  1. 定义字符串的第一部分:output ~

    formulaKnn <- 'language ~'
    
  2. 对于每个特征,使用paste添加+ feature

    for(nameFeature in arrayFeatures){
      formulaKnn <- paste(formulaKnn, '+', nameFeature)
    }
    
  3. 将字符串转换为formula格式:

    formulaKnn <- formula(formulaKnn)
    

我们构建了包含要放入kknn中的关系的formulaKnn

现在,我们需要从dtFeatures开始定义训练集和测试集。一个公平的分割是将 80%的数据放入训练集。为此,我们可以以 80%的概率将每个国家添加到训练集中,否则添加到测试集中。我们可以定义长度等于dtFeatures中行数的indexTrain向量。R 函数是sample,如下所示:

help(sample)

参数包括:

  • x:要放入向量的值,在这种情况下为TRUEFALSE

  • size:向量长度,即在我们的情况下dtFeatures中的行数。

  • replace:为了多次采样值,设置为TRUE

  • prob:选择x中元素的概率。在我们的情况下,我们以 80%的概率选择TRUE,以 20%的概率选择FALSE

使用我们的论点,我们可以构建indexTrain,如下所示:

# split the dataset into training and test set
indexTrain <- sample(
  x=c(TRUE, FALSE),
  size=nrow(dtFeatures),
  replace=TRUE,
  prob=c(0.8, 0.2)
)

现在,我们需要将indexTrainTRUE的行添加到训练集中,将剩余的行添加到测试集中。我们使用简单的数据表操作提取所有indexTrainTRUE的行,如下所示:

dtTrain <- dtFeatures[indexTrain]

为了提取测试行,我们必须使用 R 中的NOT运算符切换TRUEFALSE,如下所示:

dtTest <- dtFeatures[!indexTrain]

现在我们有了使用kknn的所有基本参数。我们设置的其它参数是:

  • k:邻居的数量是10

  • kernel:KNN 有选项为特征分配不同的相关性,但我们目前不使用此功能。将kernel参数设置为rectangular,我们使用基本的 KNN。

  • distance:我们想要计算两个标志之间的距离,即它们没有的共同属性的数量(类似于上一章)。为了做到这一点,我们将距离参数设置为1。有关更多信息,您可以了解闵可夫斯基距离

让我们构建 KNN 模型:

# build the model
modelKnn <- kknn(
  formula = formulaKnn,
  train = dtTrain,
  test = dtTest,
  k = 10,
  kernel = 'rectangular',
  distance = 1
)

模型已从dtTrain中学习并估计了dtTest中国家的语言。正如我们在kknn的帮助中看到的那样,modelKnn是一个包含模型描述的列表。显示预测语言的组件是fitted.valued,如下所示:

# extract the fitted values
modelKnn$fitted.values

我们可以将预测的语言添加到dtTest中,以便与实际语言进行比较:

# add the estimated language to dtTest
dtTest[, languagePred := modelKnn$fitted.values]

对于dtTest中的国家,我们知道实际和预测的语言。我们可以使用sum(language == languagePred)来计算它们相同的次数。我们可以通过将正确预测的数量除以总数来衡量模型精度,即.N(行数),如下所示:

# evaluate the model
percCorrect <- dtTest[, sum(language == languagePred) / .N]
percCorrect

在这里,percCorrect根据训练/测试数据集分割有很大的变化。由于我们有不同的语言组,percCorrect并不特别高。

优化 k 最近邻算法

我们使用 37 个具有不同相关性的语言特征构建了我们的 KNN 模型。给定一个新的标志,其邻居是具有许多属性共享的标志,无论它们的相关性如何。如果一个标志具有与语言无关的不同共同属性,我们将错误地将其包括在邻域中。另一方面,如果一个标志共享一些高度相关的属性,它将不会被包括。

KNN 在存在无关属性的情况下表现较差。这个事实被称为维度诅咒,这在机器学习算法中相当常见。解决维度诅咒的一种方法是根据特征的相关性对特征进行排序,并选择最相关的。另一种在本章中不会看到的选择是使用降维技术。

在上一章的 使用过滤器或降维对特征进行排序 部分,我们使用信息增益比来衡量特征的相关性。现在,我们可以从 dtTrain 开始计算 dtGains 表,类似于上一章,从 dtTrain 开始。我们不能使用整个 dtFeatures,因为我们假装不知道测试集国家的语言。如果你想看看 information.gain 是如何工作的,你可以看看第四章,步骤 1 – 数据探索和特征工程。考虑以下示例:

# compute the information gain ratio
library('FSelector')
formulaFeat <- paste(arrayFeatures, collapse = ' + ')
formulaGain <- formula(paste('language', formulaFeat, sep = ' ~ '))
dfGains <- information.gain(language~., dtTrain)
dfGains$feature <- row.names(dfGains)
dtGains <- data.table(dfGains)
dtGains <- dtGains[order(attr_importance, decreasing = T)]
View(dtGains)

feature 列包含特征名称,attr_importance 列显示特征增益,它表示其相关性。为了选择最相关的特征,我们可以首先使用排序后的特征重建 arrayFeatures。然后,我们将能够选择顶部,如下所示:

# re-define the feature vector
arrayFeatures <- dtGains[, feature]

arrayFeatures 开始,给定一个 nFeatures 数量,我们想要使用前 nFeatures 个特征构建公式。为了能够为任何 nFeatures 执行此操作,我们可以定义一个构建公式的函数,如下所示:

# define a function for building the formula
buildFormula <- function(
  arrayFeatures, # feature vector
  nFeatures # number of features to include
){

步骤如下:

  1. 提取前 nFeatures 个特征并将它们放入 arrayFeaturesTop

    arrayFeaturesTop <- arrayFeatures[1:nFeatures]
    
  2. 构建公式字符串的第一部分:

    formulaKnn <- paste('language', '~')
    
  3. 将特征添加到公式中:

    for(nameFeature in arrayFeaturesTop){
      formulaKnn <- paste(formulaKnn, '+', nameFeature)
    }
    
  4. formulaKnn 转换为 formula 格式:

    formulaKnn <- formula(formulaKnn)
    
  5. 返回输出:

      return(formulaKnn)
    }
    
    formulaKnnTop <- buildFormula(arrayFeatures, nFeatures = 10)
    formulaKnnTop
    

使用我们的函数,我们可以使用前 10 个特征构建 formulaKnnTop,如下所示:

现在,我们可以使用与之前相同的输入构建模型,除了 formula input 现在包含 formulaKnnTop,如下所示:

# build the model
modelKnn <- kknn(
  formula = formulaKnnTop,
  train = dtTrain,
  test = dtTest,
  k = 10,
  kernel = 'rectangular',
  distance = 1
)

如前所述,我们可以在名为 languagePred10 的新列中向 dtTest 添加预测的语言:

# add the output to dtTest
dtTest[, languagePredTop := modelKnn$fitted.values]

我们可以计算我们正确识别的语言的百分比:

# evaluate the model
percCorrectTop <- dtTest[, sum(language == languagePredTop) / .N]
percCorrectTop

通过选择顶部特征,我们是否取得了任何改进?为了确定哪个模型最准确,我们可以比较 percCorrect10percCorrect,并确定哪个是最高的。我们随机定义了 dtTraindtTest 之间的分割,所以每次运行算法时结果都会变化。

避免维度灾难的另一个选项。旗帜由 37 个不同相关性的特征描述,我们选择了其中最相关的 10 个。这样,相似性取决于在排名前 10 的特征中共同的特征数量。如果我们有两个旗帜,只有两个排名前 10 的特征和 20 个剩余特征是共同的,它们是否比两个共同拥有三个排名前 10 的特征的旗帜相似度低?我们不是忽略其他 27 个特征,而是可以给它们一个较低的相关性,并使用它们。

有一种 KNN 的变体,称为加权 KNN,它识别每个特征的相关性并根据此构建 KNN。有不同版本的 KNN,kknn函数允许我们使用其中的一些,指定kernel参数。在我们的情况下,我们可以设置kernel = 'optimal',如下所示:

# build the weighted knn model
modelKnn <- kknn(
  formula = formulaKnn,
  train = dtTrain,
  test = dtTest,
  k = 10,
  kernel = 'optimal',
  distance = 1
)

如前所述,我们可以测量准确性:

# add the estimated language to dtTest
dtTest[, languagePredWeighted := modelKnn$fitted.values]
percCorrectWeighted <- dtTest[
  , sum(language == languagePredWeighted) / .N
  ]

根据训练/测试分割,percCorrectWeighted可以高于或低于percCorrect

我们看到了构建监督机器学习模型的不同选项。为了确定哪个表现最好,我们需要评估每个选项并优化参数。

摘要

在本章中,你学习了如何识别同质聚类并可视化聚类过程和结果。你定义了一个可行的监督机器学习问题,并使用 KNN 解决了它。你评估了模型、准确性和修改了其参数。你还对特征进行了排序并选择了最相关的。

在下一章中,你将看到一种更好的方法来评估监督学习模型的准确性。你将看到一种结构化的方法来优化模型参数和选择最相关的特征。

第六章:第 3 步 – 验证结果

在上一章中,我们从新国家的国旗开始估计其语言。为此,我们使用了 KNN 算法,这是一种监督学习算法。我们构建了 KNN 并通过对估计的语言进行交叉验证来测量其准确性。在本章中,我们将了解如何以更可靠的方式测量准确性,并将调整 KNN 参数以提高其性能。为了能够完成本章的任务,你不需要阅读上一章,尽管这样做是推荐的,这样你可以理解 KNN 算法是如何工作的。

在本章中,你将学习如何:

  • 验证算法的准确性

  • 调整算法参数

  • 选择最相关的数据特征

  • 优化参数和特征

验证机器学习模型

从描述国家、国旗及其语言的表格开始,KNN 根据国旗属性估计新国家的语言。在本章中,我们将评估 KNN 的性能。

测量算法的准确性

我们已经通过交叉验证估计的语言来评估了算法的准确性。首先,我们将数据分为两部分,即训练集和测试集。然后,我们使用训练集构建 KNN 算法来估计测试集国家的语言。计算估计语言正确的次数,我们定义了一个准确度指数,即正确猜测的百分比。准确度取决于我们放入测试集的数据。由于我们随机定义了训练集国家,每次重复交叉验证时准确度都会改变。因此,这种方法的结果不可靠。

本章的目标是使用一种可靠的技术来评估 KNN,即准确性在验证同一模型两次时不会改变。重复进行训练/测试集分割和验证多次,几乎每个国家至少会在训练集和测试集中出现一次。我们可以计算平均准确性,并将考虑训练集和测试集中的所有国家。经过几次迭代后,平均准确性将变得可靠,因为增加迭代次数不会显著改变它。

在评估 KNN 之前,我们需要加载kknndata.table包:

# load the packages
library('kknn')
library('data.table')

我们可以定义一个函数,构建和交叉验证 KNN,使用一组定义好的参数和数据,这样我们可以快速评估任何配置的算法。由于 R 命令与上一章类似,我们将快速浏览它们。函数的输入是:

  • 包含数据的表格

  • 包含我们使用的特征名称的向量

  • KNN 参数

步骤如下:

  1. 定义哪些行属于训练集和测试集。我们构建indexTrain,这是一个向量,指定哪些行将包含在训练集中。我们将测试集的标志设置为 10%的概率。在第五章中,步骤 2 – 应用机器学习技术,我们将概率设置为 20%,但在这章中我们将多次重复验证,所以 10%就足够了。

  2. indexTrain开始,提取进入dtTraindtTest的行。

  3. 定义定义特征和预测属性的公式。

  4. 使用输入参数构建 KNN。

  5. 定义包含测试集估计语言的languageFitted向量。

  6. 计算多少次languageFitted与真实语言相同。

  7. 计算准确率指数,即预测语言和实际语言匹配的次数除以测试集中国家的数量。

这是构建函数的 R 代码。注释反映了编号的要点,如下所示:

validateKnn <- function(
  dtFeatures, # data table with the features
  arrayFeatures, # feature names array
  k = 10, # knn parameter
  kernel = 'rectangular', # knn parameter
  distance = 1 # knn parameter
){

  # 1 define the training/test set rows
  indexTrain <- sample(
    x=c(TRUE, FALSE),
    size=nrow(dtFeatures),
    replace=TRUE,
    prob=c(0.9, 0.1)
  )

  # 2 define the training/test set
  dtTrain <- dtFeatures[indexTrain]
  dtTest <- dtFeatures[!indexTrain]

  # 3 define the formula
  formulaOutput <- 'language ~'
  formulaFeatures <- paste(arrayFeatures, collapse = ' + ')
  formulaKnn <- paste(formulaOutput, formulaFeatures)
  formulaKnn <- formula(formulaKnn)

  # 4 build the KNN model
  modelKnn <- kknn(
    formula = formulaKnn,
    train = dtTrain,
    test = dtTest,
    k = k,
    kernel = kernel,
    distance = distance
  )

  # 5 defining the predicted language
  languageFitted <- modelKnn$fitted.values

  # 6 count the corrected predictions and the total
  languageReal <- dtTest[, language]
  nRows <- length(languageReal)

  # 7 define the accuracy index
  percCorrect <- sum(languageFitted == languageReal) / nRows

  return(percCorrect)
}

在这里,validateKnn是验证 KNN 算法的起点。

定义平均准确率

为了使用validateKnn,我们需要定义输入,如下所示:

  • 特征的数据表,如下所示:

    setwd('<INSER/YOUR/DIRECTORY/PATH>")
    dfFeatures <- read.table(file = 'dtFeatures.txt')
    
  • 包含所有可能包含在 KNN 中的特征的向量:

    arrayFeatures <- names(dfFeatures)
    arrayFeatures <- arrayFeatures[arrayFeatures != 'language']
    
  • KNN 参数可以是设置的,也可以保留为默认值。

现在,我们有了使用validateKnn所需的所有元素。我们可以使用它们的随机子集,例如,前 10 个特征。至于参数,我们可以将它们全部保留为默认值,除了k等于8,如下所示:

# evaluate a model accuracy
validateKnn(
  dtFeatures = dtFeatures,
  arrayFeatures = arrayFeatures[1:10],
  k = 8
)
[1] 0.3571429

多次运行validateKnn,我们可以注意到每次的结果都不同,这是预期的。然而,现在我们可以定义另一个函数,该函数运行validateKnn多次。然后,我们计算准确率平均值,并将其用作可靠的性能指标。我们的新函数称为cvKnn,因为它交叉验证 KNN 定义的次数。

cvKnn参数是数据表、迭代次数、特征名称和 KNN 参数。让我们开始定义数据表和迭代次数。所有其他输入与validateKnn相同。为了使代码清晰紧凑,我们可以使用省略号(...)指定我们可以添加其他参数。然后,我们可以再次使用省略号将这些参数传递给任何函数。这意味着当我们调用validateKnn时,我们可以使用validateKnn(...)来指定cvKnn的任何额外参数都将作为validateKnn的输入。

函数步骤如下:

  1. 定义一个空的向量arrayPercCorrect,它将包含准确率。

  2. 运行validateKnn并定义arrayPercCorrect,它包含准确率。

  3. 将准确率arrayPercCorrect添加到arrayPercCorrect中。

这是构建函数的代码:

cvKnn <- function(
  dtFeatures, # data table with the features
  nIterations=10, # number of iterations
  ... # feature names array and knn parameters
){

  # 1 initialize the accuracy array
  arrayPercCorrect <- c()

  for(iIteration in 1:nIterations){

    # 2 build and validate the knn
    percCorrect <- validateKnn(dtFeatures, ...)

    # 3 add the accuracy to the array
    arrayPercCorrect <- c(arrayPercCorrect, percCorrect)
  }

  return(arrayPercCorrect)
}

现在,我们可以使用cvKnn构建和验证 KNN 500 次。然后,我们计算平均准确率作为 KNN 性能指标:

# determine the accuracies
arrayPercCorrect = cvKnn(
  dtFeatures, nIterations=500,
  arrayFeatures=arrayFeatures
)
# compute the average accuracy
percCorrectMean <- mean(arrayPercCorrect)
percCorrectMean
[1] 0.2941644

我们定义percCorrectMean,它可以作为准确率指标。

可视化平均准确率计算

为了看到结果在任意迭代时的变化程度,我们可以将每个步骤的准确率与平均值进行比较。首先,我们使用plot构建一个包含准确率的图表,参数如下:

  • x:这是我们想要绘制的向量(arrayPercCorrect)。

  • ylim:这是介于 0 和 1 之间的准确率。通过ylim = c(0, 1),我们指定可视化的区域在 0 和 1 之间。

  • xlabylab:这是坐标轴标签。

  • main:这是标题。

代码如下:

# plot the accuracy at each iteration
plot(
  x = arrayPercCorrect,
  ylim = c(0, 1),
  xlab = 'Iteration', ylab = 'Accuracy',
  main = 'Accuracy at each iteration'
)

为了将准确率与平均值进行比较,我们可以通过绘制一条红色的虚线水平线来显示平均值,如下所示:

help(abline)
abline(h=percCorrectMean, col='red', lty='dashed')

我们可以通过为最小值和最大值范围绘制水平线来可视化值的范围,如下所示:

abline(h=min(arrayPercCorrect), col='blue', lty='dashed')
abline(h=max(arrayPercCorrect), col='blue', lty='dashed')

得到的图表如下:

可视化平均准确率计算

准确率从一个迭代到另一个迭代变化很大,范围在 0%到 70%之间。正如预期的那样,单个准确率是完全不可靠的。500 次迭代中的平均值怎么样?我们需要多少次迭代才能得到一个稳定的结果?

我们可以可视化第一次迭代的准确率指标,然后是前两次迭代的平均值,然后是前三次迭代的平均值,依此类推。如果在任何点上平均值不再变化,我们就不需要再继续了。通过构建图表,我们可以观察到达到稳定平均值所需的迭代次数。

首先,让我们定义包含累积平均值的arrayCumulate,这是直到每个迭代的局部平均值,如下所示:

# plot the average accuracy until each iteration
arrayCumulate <- c()
for(nIter in 1:length(arrayPercCorrect)){
  cumulateAccuracy <- mean(arrayPercCorrect[1:nIter])
  arrayCumulate <- c(arrayCumulate, cumulateAccuracy)
}

使用与之前相同的命令,我们构建一个新的图表。唯一的新的参数是type='l',它指定我们显示的是线而不是点。为了放大平均值所在的区域,我们移除了ylim参数,如下所示:

plot(
  x = arrayCumulate,
  type = 'l',
  xlab = 'Iteration', ylab = 'Cumulate accuracy',
  main = 'Average accuracy until each iteration'
)
abline(h = percCorrectMean, col = 'red', lty = 'dashed')

得到的图表如下:

可视化平均准确率计算

我们可以注意到,准确率在 100 次迭代后几乎保持稳定。假设它不会因不同的参数配置而变化太多,我们可以使用 100 次迭代来验证 KNN 算法。

在本节中,我们看到了如何使用一组特定的特征和一些定义的参数自动评估模型性能。在接下来的章节中,我们将使用这个函数来优化模型性能。

调整参数

本节向您展示如何通过调整参数来提高 KNN 的性能。我们处理的是定义邻居数量的k参数。使用以下步骤来识别表现最佳的k参数:

  1. 定义我们将测试的 k 值。KNN 在本地工作,也就是说,给定一个新的国家国旗,它只识别几个相似的国旗。我们最多应该使用多少个?由于总共有不到 200 个国旗,我们不希望使用超过 50 个国旗。然后,我们应该测试 1 到 50 之间的每个 k,并可以定义包含选项的 arrayK

    # define the k to test
    arrayK <- 1:50
    
  2. 定义迭代次数。对于 arrayK 中的每个 k,我们需要构建和验证 KNN,次数足够高,由 nIterations 定义。在前一章中,我们了解到我们需要至少 100 次迭代才能得到有意义的 KNN 准确性:

    nIterations <- 100
    
  3. 评估每个 k 的准确性。

  4. 选择最大化准确性的 k

最后两个步骤更详细,我们将深入探讨。

为了测量每个 k 的准确性,我们定义 dtAccuracyK 为一个空的数据表,它将包含准确性。然后,我们使用 for 循环运行每个 arrayK 中的 k 的 KNN 并添加新结果。步骤如下:

  1. 使用 cvKnn 运行和验证 KNN。

  2. 定义要添加到 dtAccuracyK 的行,包含准确性和 k

  3. 使用 rbind 将新行添加到 dtAccuracyK

    # validate the knn with different k
    dtAccuracyK <- data.table()
    for(k in arrayK)
    {
    
      # run the KNN and compute the accuracies
      arrayAccuracy <- cvKnn(
        dtFeatures,
        nIterations=nIterations,
        arrayFeatures = arrayFeatures,
        k = k
      )
      # define the new data table rows
      rowsAccuracyK <- data.table(
        accuracy = arrayAccuracy,
        k = k
      )
      # add the new rows to the accuracy table
      dtAccuracyK <- rbind(
        dtAccuracyK,
        rowsAccuracyK
        )
    }
    

现在,让我们看看 result.head(dtAccuracyK)

 accuracy k
1: 0.3636364 1
2: 0.4545455 1
3: 0.4000000 1
4: 0.2727273 1
5: 0.3000000 1
6: 0.2500000 1

dtAccuracyK 的每一行都包含 KNN 的一个迭代。第一列显示准确性,第二列显示迭代中使用的 k

为了可视化结果,我们可以使用 plot。我们想要可视化的两个维度是 k 和准确性。输入如下:

  • xy:这些是图表维度,分别是 kaccuracy

  • xlabylab:这些是轴标签,分别是 kaccuracy

  • main:这是图表标题

  • ylim:这些是 y 区域限制,分别是 01

  • col:这是点的颜色,为灰色,以便强调我们稍后要添加的黑点

代码如下:

# plot all the accuracies
plot(
  x = dtAccuracyK[, k],
  y = dtAccuracyK[, accuracy],
  xlab = 'K', ylab = 'Accuracy',
  main = 'KNN accuracy using different k',
  ylim = c(0, 1),
  col = 'grey'
)

得到的图表如下:

调整参数

小贴士

您也可以使用 type = 'str(dtCvK)' 而不是 type = 'o'

我们无法注意到任何与 k 相关的差异。原因是准确性从一个迭代到另一个迭代变化很大。为了识别表现更好的 k,我们可以计算每个 k 的平均性能。我们称新的数据表为 dtCvK,因为我们正在交叉验证模型,如下所示:

# compute the average accuracy
dtCvK <- dtAccuracyK[
  , list(accuracy = mean(accuracy)),
  by='k'
  ]
View(dtCvK)

在这里,dtCvK 包含每个 k 的平均准确性。我们可以使用添加新点到当前图表的函数将它们添加到图表中。为了使点更明显,我们使用 pch = 16 显示完整点,如下所示:

# add the average accuracy to the chart
help(points)
points(
  x = dtCvK[, k],
  y = dtCvK[, accuracy],
  pch = 16
)

图表如下:

调整参数

平均准确性随 k 变化,但很难注意到差异,因为它始终在 0.3 到 0.4 之间。为了更清楚地看到差异,我们可以只绘制平均值而不可视化 y 限制,如下所示:

# plot the average accuracy
plot(
  x = dtCvK[, k],
  y = dtCvK[, accuracy],
  xlab = 'k', ylab = 'accuracy',
  main = 'average knn accuracy using different k',
  type = 'o'
)

小贴士

您也可以使用 type = 'str(dtCvK)' 而不是 type = 'o'

我们可以识别表现最佳的 k 并使用 abline 将其添加到图表中:

# identify the k performing best
kOpt <- dtCvK[accuracy == max(accuracy), k]
abline(v = kOpt, col = 'red')

小贴士

您也可以使用 kOpt <- 27 而不是 kOpt <- dtCvK[accuracy == max(accuracy), k]

得到的图如下:

调整参数

最佳 k 值为 27,如果 k 在 22 到 30 的范围内,KNN 的表现非常好。

在本章中,我们确定了表现最佳的 k。然而,还有一些其他参数我们没有优化,例如距离方法。此外,我们可以通过选择要包含的特征来改进算法,我们将在下一节中探讨。

选择要包含在模型中的数据特征

在上一节中,我们设置了一个最大化性能的 KNN 参数。另一个调整选项是定义我们用于构建模型的数据。我们的表格描述了使用 37 个特征的标志,并将它们全部包含在模型中。然而,KNN 可能仅包括其中的一小部分时表现更好。

选择特征的最简单方法是使用过滤器(如在第四章的 使用过滤器或降维对特征进行排序 部分中预期的那样,第四章,步骤 1 – 数据探索和特征工程),该过滤器估计每个特征的影响,并仅包含最相关的特征。在根据相关性对所有特征进行排序后,我们可以定义 n 参数,指定我们在模型中包含多少个这样的特征。然后,我们可以根据 n 最大化准确性,使用与上一节类似的方法。

第一步是定义如何对特征进行排序。我们可以使用信息增益率过滤器来估计每个特征的影响,同时忽略其他特征。我们已讨论过信息增益率及其局限性(请参阅第四章的 使用过滤器或降维对特征进行排序 部分,第四章,步骤 1 – 数据探索和特征工程),我们将使用相同的 R 命令,如下所示:

# rank the features
library('FSelector')
dfGains <- information.gain(
  language~., dtFeatures
  )
dfGains$feature <- row.names(dfGains)
dtGains <- data.table(dfGains)
dtGains <- dtGains[order(attr_importance, decreasing = T)]
arrayFeatures <- dtGains[, feature]

在这里,arrayFeatures 包含按相关性排序的特征。现在,我们可以通过选择前 n 个特征来构建模型。n 的选项是介于 1 和特征总数之间的数字,我们定义 arrayN 来包含它们,如下所示:

# define the number of features to test
arrayN <- 1:length(arrayFeatures)

为了存储每次迭代的准确性,我们定义 dtAccuracyN 为一个空数据表,并使用 for 循环迭代地添加行。步骤如下:

  1. 使用 cvKnn 验证 KNN 并将准确性存储在 arrayAccuracy 中。我们将 k 参数设置为 kOpt (27),即上一节中定义的最佳 k

  2. 定义包含要添加行的 rowsAccuracyN 数据表。

  3. 使用 rbind 将新行添加到 dtAccuracyN

这是生成 for 循环的代码:

for(n in arrayN)
{
  # 1 run the KNN and compute the accuracies
  arrayAccuracy <- cvKnn(
    dtFeatures,
    nIterations = nIterations,
    arrayFeatures = arrayFeatures[1:n],
    k = kOpt
  )

  # 2 define the new data table rows
  rowsAccuracyN <- data.table(
    accuracy = arrayAccuracy,
    n = n
  )

  # 3 add the new rows to the accuracy table
  dtAccuracyN <- rbind(
    dtAccuracyN,
    rowsAccuracyN
  )
}

在这里,dtAccuracyN包含每个迭代的准确率,取决于n。我们可以通过以下步骤构建一个包含所有准确率和它们在不同n值上的平均值的图表:

  1. 建立一个显示每次迭代的准确率的图表:

    plot(
      x = dtAccuracyN[, n],
      y = dtAccuracyN[, accuracy],
      xlab = 'N', ylab = 'Accuracy',
      main = 'KNN accuracy using different features',
      ylim = c(0, 1),
      col = 'grey'
    )
    
  2. dtAccuracyN开始,计算每个迭代的平均准确率:

    dtCvN <- dtAccuracyN[
      , list(accuracy = mean(accuracy)),
      by='n'
      ]
    
  3. 将平均准确率的点添加到图表中:

    Points(
      x = dtCvN[, n],
      y = dtCvN[, accuracy],
      xlab = 'n', ylab = 'accuracy',
      pch = 16
    )
    

得到的图如下:

选择模型中包含的数据特征

图表显示,我们使用高值的n实现了最佳准确率。为了确定最佳的n,我们可以仅绘制它们的平均值。然后,我们定义nOpt,即表现最佳的n,并添加一个对应的红色垂直线,如图所示:

# plot the average accuracy
plot(
  x = dtCvN[, n],
  y = dtCvN[, accuracy],
  xlab = 'N', ylab = 'Accuracy',
  main = 'Average knn accuracy using different features',
  type = 'o'
)

# identify the n performing best
nOpt <- dtCvN[accuracy == max(accuracy), n]
abline(v = nOpt, col = 'red')

得到的图如下:

选择模型中包含的数据特征

表现最好的特征数量是15,在此之后性能缓慢下降。

在图表中,我们可以注意到有些点在添加新特征时准确率会大幅下降(例如,31113)。在这些点上,我们添加的特征降低了性能。如果我们决定不包含它会怎样呢?我们可以仅使用最相关的特征来构建模型,然后添加第二个最相关的特征。如果性能有所提高,我们保留第二个特征;否则,我们丢弃它。之后,我们用同样的方法处理第三个特征,并重复此过程,直到我们添加或丢弃了每个特征。这种方法被称为包装器,它允许我们定义比过滤器更好的特征集。

在本节中,我们确定了最佳的n和最佳的k,因此我们使用它们来构建具有良好性能的 KNN。

一起调整特征和参数

在前两个部分中,我们使用所有特征(n=37)确定了最佳k。然后,使用最佳的k,我们确定了最佳的n。如果算法在k=30n=25时表现更好,会怎样呢?我们还没有充分探索这个组合以及许多其他选项,所以可能存在比k=27n=15表现更好的组合。

为了确定最佳选项,最简单的方法是测试所有备选方案。然而,如果变量之间存在太多的可能组合,我们可能没有足够的计算能力来测试所有这些组合。在这种情况下,我们可以使用梯度下降等优化算法来确定最佳参数。

幸运的是,在我们的案例中,我们只需要调整两个参数,并且可以测试它们可能值的一部分。例如,如果我们选择 20 个n的值和 20 个k的值,我们就有 400 种组合。为了做到这一点,我们执行以下步骤:

  1. 定义 k 的选项。包括所有特征,KNN 在 k=26 时表现最佳,之后 40 就表现不佳。然而,设置较低的 n,情况可能会改变,因此我们需要测试所有可能的 k。为了限制选项数量,我们可以将测试限制在奇数。让我们使用 seq 生成 1 到 49 之间的所有奇数。fromto 参数定义序列的开始和结束。by 参数定义增量,为 2 以生成奇数。使用 seq,我们构建包含所有 k 选项的 arrayK,如下所示:

    arrayK <- seq(from = 1, to =  49, by = 2)
    
  2. 定义 n 的选项。我们已经看到,算法仅使用少量特征集时表现非常糟糕,因此我们可以测试 n 的值在 10 到特征总数之间,即 37。与 k 类似,我们只包括奇数:

    arrayN <- seq(from = 11, to = 37, by = 2)
    
  3. 生成 kn 之间所有可能的组合。为此,我们可以使用 expand.grid。给定两个或多个向量,expand.grid 生成一个包含它们所有可能组合的数据框。在我们的情况下,我们生成一个从 arrayK 开始的 k 列和一个从 arrayN 开始的 n 列,如下所示:

    dfParameters <- expand.grid(k=arrayK, n=arrayN)
    
  4. dfParameters 转换为数据表:

    dtParameters <- data.table(dfParameters)
    

现在,我们可以使用 head 查看 dtParameters

head(dtParameters)
 k  n
1:  1 11
2:  3 11
3:  5 11
4:  7 11
5:  9 11
6: 11 11

在这里,dtParameters 包含每个 350 种组合的行。我们需要确定准确度并将它们存储在一个名为 accuracy 的新列中。为了做到这一点,我们使用一个遍历行的 for 循环。iConfig 变量是行索引,定义为介于 1 和 nrow(dtParameters) 行数之间的数字。存在不同的组合,因此这部分代码可能需要一段时间才能运行。在每次迭代后,我们使用行中包含的参数构建模型:

  • k:这具有 dtParameters[iConfig, k] 参数

  • n:这具有 dtParameters[iConfig, n] 参数

考虑以下代码:

# validate the knn with different k and nFeatures
for(iConfig in 1:nrow(dtParameters)){  

  arrayAccuracy <- cvKnn(
    dtFeatures, nIterations = nIterations,
    arrayFeatures = arrayFeatures[1:dtParameters[iConfig, n]],
    k = dtParameters[iConfig, k]
  )

现在,我们可以计算 arrayAccuracy 平均值并将其添加到 dtParameters

  # add the average accuracy to dtParameters
  dtParameters[iConfig, accuracy := mean(arrayAccuracy)]
}

dtParameters 的每一行包含一个参数集及其相关的准确度。为了更方便地查看准确度,我们可以构建一个矩阵,其行对应于 n,列对应于 k。矩阵的每个元素显示准确度。为了构建矩阵,我们可以使用 reshape,如下所示:

# reshape dtParameters into a matrix
help(reshape)

reshape 语法相当复杂。在我们的情况下,我们想要构建的矩阵是 wide 格式,因此我们需要指定 direction = "wide"。其他参数定义我们使用的列,它们是:

  • v.names:此列定义矩阵值(准确度)

  • idvar:此列定义矩阵行(n 的值)

  • timevar:此列定义矩阵列(k 的值)

使用 reshape,我们可以构建如所示的 dfAccuracy 数据框:

dfAccuracy <- reshape(
  data = dtParameters,
  direction = "wide",
  v.names = "accuracy",
  idvar = "n",
  timevar = "k"
)
View(dfAccuracy)

n 列包含 n 参数,我们将其删除以获得仅包含准确度的数据框。然后,我们将数据框转换为矩阵,如下所示:

dfAccuracy$n <- NULL
matrixAccuracy <- as.matrix(dfAccuracy)

现在,我们可以将 nk 分别指定为行名和列名,如下所示:

rownames(matrixAccuracy) <- arrayN
colnames(matrixAccuracy) <- arrayK
View(matrixAccuracy)

为了可视化参数的准确率,我们可以构建一个热图,这是一个表示矩阵的图表。两个图表维度是 kn,颜色代表值。我们可以使用 image 构建这个图表:

# plot the performance depending on k and n
help(image)

我们使用的参数是:

  • z:这是一个矩阵

  • xy:这些是维度名称,包含在 arrayNarrayK

  • xLabyLab:这些是坐标轴标签

  • col:这是我们显示的颜色向量(我们可以使用 heat.colors 函数)

考虑以下代码:

image(
  x = arrayN, y = arrayK, z = matrixAccuracy,
  xlab = 'n', ylab = 'k',
  col = heat.colors(100)
)

得到的图如下:

一起调整特征和参数

高准确率用浅黄色表示,低准确率用红色表示。我们可以注意到,我们在 k 在 9 到 19 范围内和 n 在 29 到 33 范围内达到了最佳准确率。最差性能发生在 n 低而 k 高的情况下。

让我们看看最佳性能组合是什么。考虑以下代码:

# identify the best k-n combination
kOpt <- dtParameters[accuracy == max(accuracy), k]
nOpt <- dtParameters[accuracy == max(accuracy), n]

最佳组合是 k=11n=33,我们无法通过单独最大化参数来识别它。原因是,只有当我们不包括所有特征时,KNN 才会在 k=11 时表现良好。

在本节中,我们看到了一种优化两个参数的简单方法。在其他情况下,我们需要更高级的技术。

这种方法的局限性在于我们只调整了两个参数。我们可以通过调整其他 KNN 参数(如距离方法)来达到更好的性能。

摘要

在本章中,我们学习了如何将模型的性能评估为预测的平均准确率。我们了解了如何确定表示准确率的准确交叉验证指数。从交叉验证指数开始,我们调整了参数。此外,我们还学习了如何使用过滤器或 frapper 选择特征,以及如何同时调整特征和参数。本章描述了构建机器学习解决方案的最后部分,下一章将概述一些最重要的机器学习技术。

第七章。机器学习技术概述

有不同的机器学习技术,本章将概述最相关的技术。其中一些已经在前面的章节中介绍过,一些是新的。

在本章中,你将学习以下主题:

  • 最相关的技术分支:监督学习和无监督学习

  • 使用监督学习进行预测

  • 使用无监督学习识别隐藏的模式和结构

  • 这些技术的优缺点

概述

机器学习技术有不同的类别,在本章中我们将看到两个最相关的分支——监督学习和无监督学习,如下所示:

概述

监督学习和无监督学习技术处理由特征描述的对象。监督学习技术的例子是决策树学习,无监督技术的例子是 k-means。在这两种情况下,算法都是从一组对象中学习的,区别在于它们的目标:监督技术预测已知性质的属性,而无监督技术识别新的模式。

监督学习技术预测对象的属性。算法从已知属性的训练集对象中学习,并预测其他对象的属性。监督学习技术分为两类:分类和回归。如果预测的属性是分类的,我们谈论分类;如果属性是数值的,我们谈论回归。

无监督学习技术识别一组对象的模式和结构。无监督学习的两个主要分支是聚类和降维。聚类技术根据对象的属性识别同质群体,例如 k-means。降维技术识别一组描述对象的显著特征,例如主成分分析。聚类和降维之间的区别在于所识别的属性是分类的或数值的,如下所示:

概述

本章将展示每个分支的一些流行技术。为了说明技术,我们将重复使用第四章、步骤 1 – 数据探索和特征工程;第五章、步骤 2 – 应用机器学习技术;以及第六章、步骤 3 – 验证结果中的标志数据集,这些数据集可以在本书的支持代码包中找到。

监督学习

本章将向您展示一些流行的监督学习算法的示例。这些技术在面对商业问题时非常有用,因为它们可以预测未来的属性和结果。此外,可以测量每种技术及其/或参数的准确性,以便选择最合适的技术并以最佳方式设置它。

如预期,有两种技术类别:分类和回归。然而,大多数技术都可以在这两种情况下使用。以下每个小节介绍一个不同的算法。

K 最近邻算法

K 最近邻算法是一种监督学习算法,用于分类或回归。给定一个新对象,算法从其最相似的k个邻居对象预测其属性。K 最近邻算法是一种懒惰学习算法,因为它直接查询训练数据来做出预测。

在分类属性的情况下,算法将其估计为相似对象中最常见的。在数值属性的情况下,它计算它们之间的中位数或平均值。为了说明哪些是k个最相似的对象,KNN 使用一个相似性函数来评估两个对象有多相似。为了测量相似性,起点通常是一个表示差异的距离矩阵。然后,算法计算新对象与每个其他对象的相似性,并选择k个最相似的对象。

在我们的例子中,我们将使用国旗数据集,特征是国旗上的条纹数量和颜色数量。我们想要从其国旗属性预测的属性是新国家的语言。

训练集由一些国家组成,这些国家没有两个国家的国旗特征相同。首先,让我们可视化这些数据。我们可以显示国家在图表中,其维度是两个特征,颜色是语言,如下所示:

K 最近邻算法

我们有两个新国家:

  • 7 条条纹和 4 种颜色

  • 3 条条纹和 7 种颜色

我们想使用 4-最近邻算法确定两个新国家的语言。我们可以将这两个国家添加到图表中,并确定每个国家的 4 个最近点,如下所示:

K 最近邻算法

关于图表右侧的国家,其最近的 4 个邻居都属于其他类别,因此我们估计该国的语言为其他。另一个国家的邻域是混合的:1 个英语国家,1 个其他印欧语系国家,以及 2 个西班牙国家。最常见的语言是西班牙语,所以我们估计它是一个讲西班牙语的国家。

KNN 是一种简单且可扩展的算法,在许多情况下都能取得良好的结果。然而,在存在许多特征的情况下,相似度函数考虑了所有这些特征,包括不那么相关的特征,这使得使用距离变得困难。在这种情况下,KNN 无法识别有意义的最近邻,这个问题被称为维度诅咒。一种解决方案是通过选择最相关的特征或使用降维技术来降低维度(这是下一节的主题)。

决策树学习

决策树学习是一种监督学习算法,它构建一个分类或回归树。树的每个叶子节点代表属性估计,每个节点根据特征的某个条件对数据进行分割。

决策树学习是一种贪婪方法,因为它使用训练集来构建一个不需要你查询数据的模型。所有其他监督学习技术也都是贪婪的。

算法的目标是定义最相关的特征,并根据它将集合分成两组。然后,对于每个组,算法识别其最相关的特征,并将组中的对象分成两部分。这个过程一直进行,直到我们识别出叶子节点作为对象的小组。对于每个叶子节点,如果它是分类的,算法估计特征为众数;如果是数值的,则估计为平均值。在构建树之后,如果我们有太多的叶子节点,我们可以定义一个停止分割树的级别。这样,每个叶子节点将包含一个合理大的组。这种停止分割的过程称为剪枝。通过这种方式,我们找到了一个更简单且更准确的预测。

在我们的例子中,我们想要根据不同的旗帜属性(如颜色和图案)确定一个新国家的语言。算法从训练集构建树学习。让我们可视化它:

决策树学习

在任何节点,如果答案是true,我们向左走,如果答案是false,我们向右走。首先,模型识别出最相关的属性是十字形。如果一个旗帜包含十字形,我们向左走,并确定相关国家是英国。否则,我们向右走,检查旗帜是否包含蓝色。然后,我们继续检查条件,直到达到叶子节点。

假设我们没有考虑西班牙国旗来构建树。我们如何估计西班牙的语言?从顶部开始,我们检查遇到的每个节点的条件。

这些是步骤:

  1. 旗帜上不包含十字形,所以我们向左走。

  2. 旗帜包含蓝色,所以我们向右走。

  3. 旗帜上不包含十字架,所以crosses = notrue,我们向左走。

  4. 旗帜上不包含动画图像,所以我们向右走。

  5. 国旗有两种主要颜色,所以number of colors not equal to 4 or 5true,我们向左移动。

  6. 国旗没有任何条形,所以我们向左移动。

国旗没有垂直条纹,所以nStrp0 = notrue,我们向左移动,如图所示:

决策树学习

最后,估计的语言是西班牙语

决策树学习可以处理数值和/或分类特征和属性,因此它可以在只需要少量数据准备的不同环境中应用。此外,它适用于有大量特征的情况,这与其他算法不同。一个缺点是算法可能会过拟合,即模型过于接近数据并且比现实更复杂,尽管剪枝可以帮助解决这个问题。

线性回归

线性回归是一种统计模型,用于识别数值变量之间的关系。给定一组由y属性和x1, …,xn特征描述的对象,该模型定义了特征与属性之间的关系。这种关系由线性函数y = a0 + a1 * x1 + … + an * xn描述,而a0, …,an是由方法定义的参数,使得关系尽可能接近数据。

在机器学习的情况下,线性回归可以用来预测数值属性。算法从训练数据集中学习以确定参数。然后,给定一个新的对象,模型将它的特征插入到线性函数中以估计属性。

在我们的例子中,我们想要从国家的面积估计其人口。首先,让我们可视化面积(以千平方公里为单位)和人口(以百万为单位)的数据,如图下所示:

线性回归

大多数国家的面积在 3000 千平方公里以下,人口在 2 亿以下,只有少数国家的面积和/或人口要高得多。因此,大多数点都集中在图表的左下角。为了分散点,我们可以使用对数面积和人口来转换特征,如图下所示:

线性回归

线性回归的目标是识别一个尽可能接近数据的线性关系。在我们的例子中,我们有两个维度,因此我们可以用一条线来可视化这种关系。给定区域,线性回归估计人口位于这条线上。让我们在以下图表中查看具有对数特征的示例:

线性回归

给定一个关于我们已知其面积的新国家,我们可以使用回归线来估计其人口。在图表中,有一个我们已知其面积的新国家。线性回归估计该点位于红色线上。

线性回归是一种非常简单和基本的技术。缺点是它需要数值特征和属性,因此在许多情况下不适用。然而,可以使用虚拟变量或其他技术将分类特征转换为数值格式。

另一个缺点是模型对特征和属性之间关系的假设很强。估计输出的函数是线性的,所以在某些情况下,它可能与真实关系相差甚远。此外,如果现实中特征之间相互影响,模型无法跟踪这种影响。可以使用使关系线性的转换来解决此问题。也可以定义新的特征来表示非线性交互。

线性回归非常基础,它是某些其他技术的起点。例如,逻辑回归预测一个值在 0 到 1 范围内的属性。

感知器

人工神经网络ANN)是逻辑类似于生物神经系统的监督学习技术。简单的人工神经网络技术是单层感知器,它是一种分类技术,估计一个二进制属性,其值可以是 0 或 1。感知器的工作方式类似于神经元,即它将所有输入的影响相加,如果总和高于定义的阈值,则输出为 1。该模型基于以下参数:

  • 每个特征的权重,定义其影响

  • 估计输出为 1 的阈值

从特征开始,模型通过以下步骤估计属性

  • 通过线性回归计算输出:将每个特征乘以其权重,并将它们相加

  • 如果输出高于阈值,则估计属性为 1,否则为 0

模型如图所示:

感知器

在开始时,算法使用定义好的系数集和阈值构建感知器。然后,算法使用训练集迭代地改进系数。在每一步中,算法估计每个对象的属性。然后,算法计算真实属性和估计属性之间的差异,并使用该差异来修改系数。在许多情况下,算法无法达到一个稳定的系数集,这些系数不再被修改,因此我们需要定义何时停止。最后,我们有一个由系数集定义的感知器,我们可以用它来估计新对象的属性。

感知器是神经网络的一个简单例子,它使我们能够轻松理解变量的影响。然而,感知器依赖于线性回归,因此它在同一程度上有限:特征影响是线性的,特征不能相互影响。

集成

每个算法都有一些弱点,导致结果不正确。如果我们能够使用不同的算法解决相同的问题并选择最佳结果会怎样?如果只有少数算法犯了同样的错误,我们可以忽略它们。我们无法确定哪个结果是正确的,哪个是错误的,但还有一个选择。通过在新对象上执行监督学习,我们可以应用不同的算法,并从中选择最常见或平均的结果。这样,如果大多数算法识别出正确的估计,我们将考虑它。集成方法基于这个原则:它们结合不同的分类或回归算法以提高准确性。

集成方法需要不同算法和/或训练数据集产生的结果之间的可变性。一些选项包括:

  • 改变算法配置:算法是相同的,其参数在一个范围内变化。

  • 改变算法:我们使用不同的技术来预测属性。此外,对于每种技术,我们可以使用不同的配置。

  • 使用不同的数据子集:算法是相同的,每次它都从训练数据的不同随机子集中学习。

  • 使用不同的数据样本(袋装):算法是相同的,它从自助样本中学习,即从训练数据集中随机选择的一组对象。同一个对象可以被选择多次。

最终结果结合了所有算法的输出。在分类的情况下,我们使用众数,在回归的情况下,我们使用平均值或中位数。

我们可以使用任何监督学习技术的组合来构建集成算法,因此有几种选择。一个例子是随机森林,它通过袋装(在上一个列表中的最后一个要点中解释的技术)结合了决策树学习算法。

集成方法通常比单个算法表现更好。在分类的情况下,集成方法消除了仅影响算法一小部分的偏差。然而,不同算法的逻辑通常是相关的,相同的偏差可能很常见。在这种情况下,集成方法保留了偏差。

集成方法并不总是适用于回归问题,因为偏差会影响最终结果。例如,如果只有一个算法计算出一个非常偏差的结果,平均结果会受到很大影响。在这种情况下,中位数表现更好,因为它更加稳定,并且不受异常值的影响。

无监督学习

本章展示了某些无监督学习技术。当面对商业问题时,这些技术使我们能够识别隐藏的结构和模式,并执行探索性数据分析。此外,无监督学习可以简化问题,使我们能够构建更准确且更简化的解决方案。这些技术也可以用于解决本身的问题。

技术的两个分支是聚类和降维,其中大多数技术不适用于两种上下文。本章展示了某些流行技术。

k-means

k-means 是一种基于质心的聚类技术。给定一组对象,算法识别k个同质簇。k-means 是基于质心的,因为每个簇由其质心表示,代表其平均对象。

算法的目的是识别k个质心。然后,k-means 将每个对象关联到最近的质心,定义k个簇。算法从一个随机的质心集合开始,并迭代地改变它们,以改进聚类。

在我们的例子中,数据是关于国家旗帜的,两个特征是条纹数量和颜色数量。我们选择国家子集的方式是确保没有任何两面旗帜具有相同的属性值。我们的目标是识别两个同质的国家群体。k-means 算法的第一步是确定两个随机质心。让我们在图表中可视化数据和质心:

k-means

o代表国家旗帜,x代表质心。在运行 k-means 之前,我们需要定义一个距离,这是一种确定对象之间差异性的方法。例如,在上面的图表中,我们可以使用欧几里得距离,它表示连接两个点的线段的长度。该算法是迭代的,每一步包括以下步骤:

  1. 对于每个点,确定距离最小的质心。然后,将该点分配到与最近质心相关的簇。

  2. 以一种方式重新计算每个簇的质心,使其成为其对象的平均值。

最后,我们有两个簇,相关的质心代表平均对象。让我们可视化它们,如图所示:

k-means

颜色代表簇,黑色x代表最终的质心。

k-means 是最受欢迎的聚类技术之一,因为它易于理解,并且不需要太多的计算能力。然而,该算法有一些局限性。它包含一个随机成分,因此如果我们对同一组数据运行两次,它可能会识别出不同的聚类。另一个缺点是它无法在特定环境中识别聚类,例如,当聚类具有不同的大小或复杂形状时。k-means 是一个非常简单和基本的算法,它是某些更复杂技术的起点。

层次聚类

层次聚类是聚类技术的一个分支。从一个对象集合开始,目标构建一个聚类层次。在聚合层次聚类中,每个对象最初属于不同的聚类。然后,算法将聚类合并,直到有一个包含所有对象的聚类。在确定了层次之后,我们可以在任何点上定义聚类并停止它们的合并。

在每次聚合步骤中,算法将两个最相似的聚类合并,并且有一些参数定义了相似性。首先,我们需要定义一种方法来衡量两个对象之间的相似程度。根据情况,有多种选择。然后,我们需要定义聚类之间的相似性;这些方法被称为链接。为了衡量相似性,我们首先定义一个距离函数,它是相反的。为了确定聚类 1 和聚类 2 之间的距离,我们测量聚类 1 中每个可能对象与聚类 2 中每个对象之间的距离。测量两个聚类之间距离的选项包括:

  • 单链接:这是最小距离

  • 完全 链接:这是最大距离

  • 平均 链接:这是平均距离

根据链接方式的不同,算法的结果也会不同。

该示例使用与 k-means 相同的数据。国家旗帜由条纹和颜色数量表示,我们希望识别同质群体。我们使用的距离是欧几里得距离(仅仅是两点之间的距离)和链接方式为完全链接。首先,让我们从它们的层次结构中识别聚类,如图所示:

层次聚类

该图表称为树状图,图表底部每个对象属于不同的聚类。然后,向上合并聚类,直到所有对象属于同一个聚类。高度是算法合并聚类时的距离。例如,在高度 3 处,所有距离低于 3 的聚类已经合并。

红线位于高度 6 处,它定义了何时停止合并,其下方的对象被分为 4 个聚类。现在我们可以按照以下方式在图表中可视化聚类:

层次聚类

点的颜色代表簇。算法正确地识别了右侧的组,并且以良好的方式将左侧的组分为三部分。

层次聚类有多种选项,其中一些在某些情境下会产生非常好的结果。与 k-means 不同,该算法是确定性的,因此它总是导致相同的结果。

层次聚类的缺点之一是计算时间(O(n³)),这使得它无法应用于大型数据集。另一个缺点是需要手动选择算法配置和树状图切割。为了确定一个好的解决方案,我们通常需要用不同的配置运行算法,并可视化树状图以定义其切割。

PCA

主成分分析PCA)是一种将特征进行转换的统计过程。PCA 的原理基于线性相关性和方差的概念。在机器学习环境中,PCA 是一种降维技术。

从描述一组对象的特征开始,目标定义了其他彼此线性不相关的变量。输出是一个新的变量集,这些变量定义为初始特征的线性组合。此外,新变量根据其相关性进行排序。新变量的数量小于或等于初始特征的数量,并且可以选择最相关的特征。然后,我们能够定义一组更小的特征,从而降低问题维度。

算法从具有最高方差的特征组合开始定义,然后在每一步迭代地定义另一个特征组合,以最大化方差,条件是新组合与其他组合不线性相关。

在第四章的例子中,步骤 1 – 数据探索和特征工程,第五章,步骤 2 – 应用机器学习技术,以及第六章,步骤 3 – 验证结果中,我们定义了 37 个属性来描述每个国家国旗。应用 PCA 后,我们可以定义 37 个新的属性,这些属性是变量的线性组合。属性按相关性排序,因此我们可以选择前六个,从而得到一个描述国旗的小表格。这样,我们能够构建一个基于六个相关特征的监督学习模型来估计语言。

在存在大量特征的情况下,PCA 允许我们定义一组更小的相关变量。然而,这项技术并不适用于所有情境。一个缺点是结果取决于特征的缩放方式,因此有必要首先标准化变量。

处理监督学习问题时,我们可以使用 PCA 来降低其维度。然而,PCA 只考虑特征,而忽略了它们与预测属性之间的关系,因此它可能会选择与问题不太相关的特征组合。

摘要

在本章中,我们学习了机器学习技术的主要分支:监督学习和无监督学习。我们了解了如何使用监督学习技术,如 KNN、决策树、线性回归和神经网络来估计数值或分类属性。我们还看到,通过结合不同的监督学习算法的技术,即集成,可以提高性能。我们学习了如何使用 k-means 和层次聚类等聚类技术来识别同质群体。我们还理解了降维技术,如 PCA,对于将定义较小变量集的特征进行转换的重要性。

下一章将展示一个可以使用机器学习技术解决的商业问题的例子。我们还将看到监督学习和无监督学习技术的示例。

第八章:适用于商业的机器学习示例

本章的目的是向您展示机器学习如何帮助解决商业问题。大多数技术已在上一章中探讨过,因此本章的节奏很快。技术涉及无监督学习和监督学习。无监督算法从数据中提取隐藏结构,监督技术预测属性。本章使用两个分支的技术解决商业挑战。

在本章中,你将学习如何:

  • 将机器学习方法应用于商业问题

  • 对银行的客户群进行细分

  • 识别营销活动的目标

  • 选择表现更好的技术

问题概述

一家葡萄牙银行机构发起了一项电话营销活动。该机构资源有限,因此需要选择目标客户。从过去活动的数据开始,我们可以使用机器学习技术为公司提供一些支持。数据显示了客户的个人细节以及以前营销活动的信息。机器学习算法的目标是识别更有可能订阅的客户。从数据开始,算法需要理解如何使用新客户的数据来预测每个客户订阅的可能性。

数据概述

数据包括大约超过 2,500 名受营销活动影响的客户,该活动包括一个或多个电话呼叫。我们有一些关于客户的信息,并且我们知道谁已经订阅。

表格的每一行对应一个客户,其中有一列显示输出,如果客户已订阅则显示yes,否则显示no。其他列是描述客户的特征,它们是:

  • 个人详情:这包括诸如年龄、工作、婚姻状况、教育、信用违约、平均年度余额、住房和个人贷款等详细信息。

  • 与公司的沟通:这包括诸如联系方式、最后联系月份和星期几、最后通话时长和联系次数等详细信息。

  • 以前的营销活动:这包括诸如上次营销活动前的天数、过去联系次数和过去结果等详细信息。

这是表格的一个示例。y列显示预测属性,如果客户已订阅则显示yes,否则显示no

年龄 工作 婚姻状况 ... 联系方式 y
30 services married cellular no
33 management single telephone yes
41 blue-collar single unknown no
35 self-employed married telephone no

数据存储在bank.csv文件中,我们可以通过在 R 中构建数据表来加载它们。sep=';'字段指定文件中的字段由分号分隔,如下所示:

library(data.table)
dtBank <- data.table(read.csv('bank.csv', sep=';'))

duration特征显示最终通话的秒数。我们分析的目标是定义哪些客户需要联系,我们在联系客户之前无法知道通话时长。此外,在知道通话时长后,我们已经知道客户是否订阅了,因此使用此属性来预测结果是没有意义的。因此,我们移除了duration特征,如下所示:

# remove the duration
dtBank[, duration := NULL]

下一步是探索数据以了解上下文。

探索输出

在本小节中,我们快速探索并转换数据。

y输出是分类的,可能的输出结果为yesno,我们的目标是可视化比例。为此,我们可以使用以下步骤构建饼图:

  1. 使用table统计订阅和未订阅的客户数量:

    dtBank[, table(y)]
    y
     no  yes
    4000  521
    
    
  2. 确定订阅和未订阅客户的百分比:

    dtBank[, table(y) / .N]
    y
     no     yes 
    0.88476 0.11524
    
    
  3. 从比例开始构建一个确定百分比的函数:

    DefPercentage <- function(frequency)
    {
      percentage = frequency / sum(frequency)
      percentage = round(percentage * 100)
      percentage = paste(percentage, '%')
      return(percentage)
    }
    
  4. 确定百分比:

    defPercentage(dtBank[, table(y) / .N])
    [1] "88 %" "12 %"
    
    
  5. 查看 R 函数barplot的帮助,该函数用于构建条形图:

    help(barplot)
    
  6. 定义条形图输入:

    tableOutput <- dtBank[, table(y)]
    colPlot <- rainbow(length(tableOutput))
    percOutput <- defPercentage(tableOutput)
    
  7. 构建条形图:

    barplot(
      height = tableOutput,
      names.arg = percOutput,
      col = colPlot,
      legend.text = names(tableOutput),
      xlab = 'Subscribing'
      ylab = 'Number of clients',
      main = 'Proportion of clients subscribing'
    )
    

获得的图表如下:

探索输出

只有 12%的客户订阅了,因此输出值分布不均。下一步是探索所有数据。

探索和转换特征

与输出类似,我们可以构建一些图表来探索特征。让我们首先使用str查看它们:

str(dtBank)
Classes 'data.table' and 'data.frame':    4521 obs. of  16 variables:
 $ age      : int  30 33 35 30 59 35 36 39 41 43 ...
 $ job      : Factor w/ 12 levels "admin.","blue-collar",..: 11 8 5 5 2 5 7 10 3 8 ...
 $ marital  : Factor w/ 3 levels "divorced","married",..: 2 2 3 2 2 3 2 2 2 2 ...
...

特征属于两种数据类型:

  • 分类:这种数据类型以因子格式存储特征

  • 数值:这种数据类型以整数格式存储特征

对于分类特征和数值特征的图表是不同的,因此我们需要将特征分为两组。我们可以通过以下步骤定义一个包含分类特征的向量以及一个包含数值特征的向量:

  1. 使用lapply定义每一列的类:

    classFeatures <- lapply(dtBank, class)
    
  2. 移除包含输出的y列:

    classFeatures <- classFeatures[names(classFeatures) != 'y']
    
  3. 确定分类特征:

    featCategoric <- names(classFeatures)[
      classFeatures == 'factor'
      ]
    
  4. 确定数值特征:

    featNumeric <- names(classFeatures)[
      classFeatures == 'integer'
      ]
    

与输出类似,我们可以为九个分类特征中的每一个构建饼图。为了避免图表过多,我们可以将三个饼图放在同一个图表中。R 函数是par,它允许定义图表网格:

help(par)

我们需要的输入是:

  • mfcol:这是一个包含列数和行数的向量。对于每个特征,我们构建一个饼图和一个包含其图例的图表。我们将饼图放在底部行,图例放在顶部。然后,我们有两行三列。

  • mar:这是一个定义图表边距的向量:

    par(mfcol = c(2, 3), mar = c(3, 4, 1, 2))
    

现在,我们可以使用for循环构建直方图:

for(feature in featCategoric){

for循环内执行以下步骤:

  1. 定义饼图输入:

      TableFeature <- dtBank[, table(get(feature))]
      rainbCol <- rainbow(length(tableFeature))
      percFeature <- defPercentage(tableFeature)
    
  2. 定义一个新的图表,其图例由特征名称与其颜色匹配组成。我们将特征名称作为图例标题:

       plot.new()
       legend(
         'top', names(tableFeature),
         col = rainbCol, pch = 16,
         title = feature
       )
    
  3. 构建将在底部行显示的直方图:

      barplot(
        height = tableFeature,
        names.arg = percFeature,
        col = colPlot,
        xlab = feature,
        ylab = 'Number of clients'
      )
    }
    

我们构建了包含三个分类特征的三个图表。让我们看看第一个:

探索和转换特征

job 属性有不同的级别,其中一些级别拥有大量的客户。然后,我们可以为每个相关的职位定义一个虚拟变量,并忽略其他职位。为了确定最相关的职位,我们计算属于每个级别的百分比。然后,我们设置一个阈值,并忽略所有低于阈值的级别。在这种情况下,阈值是 0.08,即 8%。在定义新的虚拟列之后,我们移除 job

percJob <- dtBank[, table(job) / .N]
colRelevant <- names(percJob)[percJob > 0.08]
for(nameCol in colRelevant){
  newCol <- paste('job', nameCol, sep='_')
  dtBank[, eval(newCol) := ifelse(job == nameCol, 1, 0)]
}
dtBank[, job := NULL]

在这里,marital,定义婚姻状况,有三个级别,其中 divorcedsingle 占有较小的,尽管是显著的,部分。我们可以定义两个虚拟变量来定义三个级别:

dtBank[, single := ifelse(marital == 'single', 1, 0)]
dtBank[, divorced := ifelse(marital == 'divorced', 1, 0)]
dtBank[, marital := NULL]

关于 education,超过一半的客户接受了中等教育,因此我们可以假设 unknown 的 4% 是 secondary。然后,我们有三个属性,我们可以定义两个虚拟变量:

dtBank[, edu_primary := ifelse(education == 'primary', 1, 0)]
dtBank[, edu_tertiary := ifelse(education == 'tertiary', 1, 0)]
dtBank[, education := NULL]

得到的图如下:

探索和转换特征

默认住房贷款属性有两个不同的级别,因此可以使用 as.numeric 将它们转换为数值形式。为了在属性为 no 时得到 0,在属性为 yes 时得到 1,我们减去 1,如下所示:

dtBank[, housing := as.numeric(housing) - 1]
dtBank[, default := as.numeric(default) - 1]
dtBank[, loan := as.numeric(loan) - 1]

得到的直方图如下:

探索和转换特征

在这里,contact 有三个选项,其中一个是 unknown。所有选项都有一个显著的份额,因此我们可以定义两个虚拟变量,如下所示:

dtBank[, cellular := ifelse(contact == 'cellular', 1, 0)]
dtBank[, telephone := ifelse(contact == 'telephone', 1, 0)]
dtBank[, contact := NULL]

我们可以将 month 转换为一个数值变量,其中一月对应于 1,十二月对应于 12。特征值是月份名称的缩写,不带大写字母,例如,jan 对应于 January。为了定义数值特征,我们定义一个向量,其第一个元素是 jan,第二个元素是 feb,依此类推。然后,使用 which,我们可以识别向量中的对应元素。例如,apr 是向量的第四个元素,因此使用 which 我们得到 4。为了构建有序月份名称的向量,我们使用包含缩写月份名称的 month.abbtolower 来取消首字母大写,如下所示:

Months <- tolower(month.abb)
months <- c(
    'jan', 'feb', 'mar', 'apr', 'may', 'jun',
    'jul', 'aug', 'sep', 'oct', 'nov', 'dec'
)
dtBank[
  , month := which(month == months),
  by=1:nrow(dtBank)
  ]

poutcome 中,successfailure 占有少量客户。然而,它们非常相关,因此我们定义了两个虚拟变量:

dtBank[, past_success := ifelse(poutcome == 'success', 1, 0)]
dtBank[, past_failure := ifelse(poutcome == 'failure', 1, 0)]
dtBank[, poutcome := NULL]

我们将所有分类特征转换为数值格式。下一步是探索数值特征并在必要时进行转换。

有六个数值特征,我们可以为每个特征构建一个图表。图表是一个直方图,显示特征值的分布。为了在同一图表中可视化所有图形,我们可以使用 par 将它们放在一个 3 x 2 的网格中。参数如下:

  • mfrow:与mfcol类似,它定义了一个图形网格。区别只是我们将图形添加到网格的顺序。

  • mar:我们将边距设置为默认值,即c(5, 4, 4, 2) + 0.1,如下所示:

    par(mfrow=c(3, 2), mar=c(5, 4, 4, 2) + 0.1)
    

我们可以使用hist构建直方图。输入如下:

  • x:这是包含数据的向量

  • main:这是图表标题

  • xlab:这是 x 轴下的标签

我们可以直接在数据表方括号内使用hist。为了一步构建所有图表,我们使用一个for循环:

for(feature in featNumeric){
  dtBank[, hist(x = get(feature), main=feature, xlab = feature)]
}

获得的直方图如下:

探索和转换特征

在这里,年龄天数在其可能值上是均匀分布的,因此它们不需要任何处理。其余特征集中在较小的值上,因此我们需要对它们进行转换。我们用来定义转换特征的函数是对数,它允许我们拥有更分散的值。对数适用于具有大于 0 值的特征,因此我们需要从特征中移除负值。

为了避免零值,在计算对数之前将特征加1

根据数据描述,如果机构之前没有联系过客户,则pdays等于-1。为了识别首次联系的客户,我们可以定义一个新的虚拟变量,如果pdays等于-1,则该变量为1。然后,我们将所有负值替换为0,如下所示:

dtBank[, not_contacted := ifelse(pdays == -1, 1, 0)]
dtBank[pdays == -1, pdays := 0]

balance特征表示过去的余额,我们可以定义一个虚拟变量,如果余额为负,则该变量为1。然后,我们将负余额替换为0

dtBank[, balance_negative := ifelse(balance < 0, 1, 0)]
dtBank[balance < 0, balance := 0]

现在,我们可以计算所有特征的对数。由于对数的输入必须是正数,而一些特征等于0,我们在计算对数之前将每个特征加1

dtBank[, pdays := log(pdays + 1)]
dtBank[, balance := log(balance + 1)]
dtBank[, campaign := log(campaign + 1)]
dtBank[, previous := log(previous + 1)]

我们已经将所有特征转换为数值格式。现在,我们可以看一下新的特征表:

str(dtBank)
View(dtBank)

唯一不是数值或整数的列是输出y。我们可以将其转换为数值格式,并将其名称更改为 output:

dtBank[, output := as.numeric(y) – 1]
dtBank[, y := NULL]

我们已加载数据并进行了清理。现在我们准备构建机器学习模型。

聚类客户

为了应对下一场营销活动,我们需要识别更有可能订阅的客户。由于难以逐个评估客户,我们可以确定同质客户群体,并识别最有希望的群体。

从历史数据开始,我们根据客户的个人详细信息对客户进行聚类。然后,给定一个新客户,我们识别最相似的群体并将新客户关联到该群体。我们没有新客户客户行为的信息,因此聚类仅基于个人属性。

有不同的技术执行聚类,在本节中我们使用一个相关的算法,即层次聚类。层次聚类的参数之一是链接,它是计算两组之间距离的方式。主要选项包括:

  • 单链接:这是第一组中的一个对象与第二组中的一个对象之间的最小距离

  • 完全链接:这是第一组中的一个对象与第二组中的一个对象之间的最大距离

  • 平均链接:这是第一组中的一个对象与第二组中的一个对象之间的平均距离

在我们的案例中,我们选择了平均链接,这个选择来自于测试三个选项。

我们定义dtPers只包含个人特征,如下所示:

featPers <- c(
  'age', 'default', 'balance', 'balance_negative',
  'housing', 'loan',
  'job_admin.', 'job_blue-collar',	'job_management',
  'job_services', 'job_technician',
  'single', 'divorced', 'edu_primary', 'edu_tertiary'
)
dtPers <- dtBank[, featPers, with=F]

现在,我们可以应用层次聚类,步骤如下:

  1. 定义距离矩阵:

    d <- dist(dtPers, method = 'euclidean')
    
  2. 构建层次聚类模型:

    hcOut <- hclust(d, method = 'average')
    
  3. 可视化树状图。par方法定义了绘图布局,在这种情况下,它只包含一个图表,而plot包含一个改进外观的参数。labelshang功能避免了底部图表的杂乱,其他参数指定了图表标题和坐标轴标签,如下所示:

    par(mfrow = c(1, 1))
    plot(
      hcOut,
      labels = FALSE,
      hang = -1,
      main = 'Dendrogram',
      xlab = 'Client clusters',
      ylab = 'Agglomeration distance'
    )
    

得到的直方图如下:

聚类客户

我们可以在树状图的高度40处切割树状图来识别三个集群。还有另一种选择,即在较低的水平(约 18)切割树状图,识别七个集群。我们可以探索这两种选择,并使用rect.hclust在树状图上可视化这两个分割,如下所示:

k1 <- 3
k2 <- 7
par(mfrow=c(1, 1))
rect.hclust(hcOut, k = k1)
rect.hclust(hcOut, k = k2)

得到的直方图如下:

聚类客户

为了确定最成功的集群,我们可以使用饼图显示订阅客户的比例,并在饼图的标题中放置集群中的客户数量。让我们看看第一次分割的三个集群的图表。构建饼图的步骤与我们之前执行的步骤类似:

  1. 定义包含输出属性的数据表:

    dtClust <- dtBank[, 'output', with = F]
    
  2. 在数据表中添加定义集群的两列。每一列对应不同的集群数量:

    dtClust[, clusterHc1 := cutree(hclOut, k = k1)]
    dtClust[, clusterHc2 := cutree(hclOut, k = k2)]
    
  3. 定义一个包含一行三列的绘图布局。oma参数定义了外部边距:

    par(mfrow = c(1, 3), oma = c(0, 0, 10, 0))
    
  4. 使用与数据探索类似的命令,构建三个直方图,显示每个集群订阅或不订阅客户的百分比:

    for(iCluster in 1:k1){
      tableClust <- dtClust[
        clusterHc1 == iCluster,
        table(output)
        ]
      sizeCluster <- dtClust[, sum(clusterHc1 == iCluster)]
      titlePie <- paste(sizeCluster, 'clients')
      barplot(
        height = tableClust,
        names.arg = defPercentage(tableClust),
        legend.text = c('no', 'yes'),
        col = c('blue', 'red'),
        main = titlePie
      )
    }
    
  5. 添加图表的标题:

    mtext(
      text = 'Hierarchic clustering, n = 3',
      outer = TRUE, line = 1, cex = 2
    )
    

得到的直方图如下:

聚类客户

第一和第二集群包含大多数客户,并且在这两个集群上的活动并没有特别成功。第三集群较小,其客户订阅的比例显著更高。然后,我们可以开始针对与第三集群类似的新客户的营销活动。

使用相同的 R 命令,我们可以可视化由第二次分割确定的七个簇的相同图表,如下所示:

  1. 定义具有两行四列的绘图布局:

    par(mfrow = c(2, 4), oma = c(0, 0, 10, 0))
    
  2. 构建直方图:

    for(iCluster in 1:k2){
      tableClust <- dtClust[
        clusterHc2 == iCluster,
        table(output)
        ]
      sizeCluster <- dtClust[, sum(clusterHc2 == iCluster)]
      titlePie <- paste(sizeCluster, 'clients')
      barplot(
        height = tableClust,
        names.arg = defPercentage(tableClust),
        col = c('blue', 'red'),
        main = titlePie
      )
    }
    
  3. 添加图表标题:

    mtext(
      text = 'Hierarchic clustering, n = 7',
      outer = TRUE, line = 1, cex = 2
    )
    

获得的直方图如下:

聚类客户

前三个簇包含大多数客户,营销活动对它们的成效并不特别显著。第四和第五簇的订阅客户百分比显著更高。最后两个簇虽然规模很小,但非常成功。营销活动将开始针对与最后两个簇相似的所有新客户,并将针对第四和第五簇的一部分客户。

总之,使用聚类,我们识别出一些小客户群体,在这些群体上营销活动非常成功。然而,大多数客户属于一个大簇,我们对其了解不足。原因是营销活动在具有特定特征的少数客户上取得了成功。

预测输出

过去的营销活动针对了一部分客户群。在 1000 名客户中,我们如何识别出那些更愿意订阅的 100 名客户?我们可以构建一个从数据中学习并估计哪些客户与之前营销活动中订阅的客户更相似的模型。对于每个客户,模型估计一个分数,如果客户更有可能订阅,则分数更高。有不同机器学习模型确定分数,我们使用两种表现良好的技术,如下所示:

  • 逻辑回归:这是线性回归的一种变体,用于预测二元输出

  • 随机森林:这是一种基于决策树的集成方法,在存在许多特征的情况下表现良好

最后,我们需要从两种技术中选择一种。有一些交叉验证方法允许我们估计模型精度(见第六章,步骤 3 – 验证结果)。从那时起,我们可以测量两种选项的精度,并选择表现更好的一个。

在选择最合适的机器学习算法后,我们可以使用交叉验证来优化它。然而,为了避免过度复杂化模型构建,我们不执行任何特征选择或参数优化。

这些是构建和评估模型的步骤:

  1. 加载包含随机森林算法的 randomForest 包:

    library('randomForest')
    
  2. 定义输出和变量名的公式。公式格式为 output ~ feature1 + feature2 + ...

    arrayFeatures <- names(dtBank)
    arrayFeatures <- arrayFeatures[arrayFeatures != 'output']
    formulaAll <- paste('output', '~')
    formulaAll <- paste(formulaAll, arrayFeatures[1])
    for(nameFeature in arrayFeatures[-1]){
      formulaAll <- paste(formulaAll, '+', nameFeature)
    }
    formulaAll <- formula(formulaAll)
    
  3. 初始化包含所有测试集的表格:

    dtTestBinded <- data.table()
    
  4. 定义迭代次数:

    nIter <- 10
    
  5. 开始一个 for 循环:

    for(iIter in 1:nIter)
    {
    
  6. 定义训练集和测试集:

    indexTrain <- sample(
      x = c(TRUE, FALSE),
      size = nrow(dtBank),
      replace = T,
      prob = c(0.8, 0.2)
    )
    dtTrain <- dtBank[indexTrain]
    dtTest <- dtBank[!indexTrain]
    
  7. 从测试集中选择一个子集,使得我们有相同数量的output == 0output == 1。首先,根据输出将dtTest分成两部分(dtTest0dtTest1),并计算每部分的行数(n0n1)。然后,由于dtTest0有更多的行,我们随机选择n1行。最后,我们重新定义dtTest,将dtTest0dtTest1绑定,如下所示:

      dtTest1 <- dtTest[output == 1]
      dtTest0 <- dtTest[output == 0]
      n0 <- nrow(dtTest0)
      n1 <- nrow(dtTest1)
      dtTest0 <- dtTest0[sample(x = 1:n0, size = n1)]
      dtTest <- rbind(dtTest0, dtTest1)
    
  8. 使用randomForest构建随机森林模型。公式参数定义了变量与数据之间的关系,数据参数定义了训练数据集。为了避免模型过于复杂,所有其他参数都保留为默认值:

    modelRf <- randomForest(
      formula = formulaAll,
      data = dtTrain
    )
    
  9. 使用glm构建逻辑回归模型,这是一个用于构建广义线性模型GLM)的函数。GLMs 是线性回归的推广,允许定义一个将线性预测器与输出连接的链接函数。输入与随机森林相同,增加family = binomial(logit)定义回归为逻辑回归:

    modelLr <- glm(
      formula = formulaAll,
      data = dtTest,
      family = binomial(logit)
    )
    
  10. 使用predict函数预测随机森林的输出。该函数的主要参数是object定义模型和newdata定义测试集,如下所示:

    dtTest[, outputRf := predict(
      object = modelRf, newdata = dtTest, type='response'
      )]
    
  11. 使用predict函数预测逻辑回归的输出,类似于随机森林。另一个参数是type='response',在逻辑回归的情况下是必要的:

    dtTest[, outputLr := predict(
      object = modelLr, newdata = dtTest, type='response'
      )]
    
  12. 将新的测试集添加到dtTestBinded

    dtTestBinded <- rbind(dtTestBinded, dtTest)
    
  13. 结束for循环:

    }
    

我们构建了包含output列的dtTestBinded,该列定义了哪些客户订阅以及模型估计的得分。通过比较得分与实际输出,我们可以验证模型性能:

为了探索dtTestBinded,我们可以构建一个图表,显示非订阅客户得分的分布情况。然后,我们将订阅客户的分布添加到图表中,并进行比较。这样,我们可以看到两组得分之间的差异。由于我们使用相同的图表进行随机森林和逻辑回归,我们定义了一个按照给定步骤构建图表的函数:

  1. 定义函数及其输入,包括数据表和得分列的名称:

    plotDistributions <- function(dtTestBinded, colPred)
    {
    
  2. 计算未订阅客户的分布密度。当output == 0时,我们提取未订阅的客户,并使用density定义一个density对象。调整参数定义了从数据开始构建曲线的平滑带宽,带宽可以理解为细节级别:

      densityLr0 <- dtTestBinded[
        output == 0,
        density(get(colPred), adjust = 0.5)
        ]
    
  3. 计算已订阅客户的分布密度:

      densityLr1 <- dtTestBinded[
        output == 1,
        density(get(colPred), adjust = 0.5)
        ]
    
  4. 使用rgb定义图表中的颜色。颜色是透明的红色和透明的蓝色:

      col0 <- rgb(1, 0, 0, 0.3)
      col1 <- rgb(0, 0, 1, 0.3)
    
  5. 使用polygon函数构建显示未订阅客户得分分布的密度图。在这里,polygon函数用于向图表添加面积:

      plot(densityLr0, xlim = c(0, 1), main = 'density')
      polygon(densityLr0, col = col0, border = 'black')
    
  6. 将已订阅的客户添加到图表中:

      polygon(densityLr1, col = col1, border = 'black')
    
  7. 添加图例:

      legend(
        'top',
        c('0', '1'),
        pch = 16,
        col = c(col0, col1)
      )
    
  8. 结束函数:

      return()
    }
    

现在,我们可以使用plotDistributions在随机森林输出上:

par(mfrow = c(1, 1))
plotDistributions(dtTestBinded, 'outputRf')

获得的直方图如下:

预测输出

x 轴代表得分,y 轴代表与订阅了相似得分的客户数量成比例的密度。由于我们并没有每个可能得分的客户,假设细节级别为 0.01,密度曲线在意义上是平滑的,即每个得分的密度是相似得分数据的平均值。

红色和蓝色区域分别代表未订阅和订阅客户。很容易注意到,紫色区域来自两条曲线的叠加。对于每个得分,我们可以识别哪个密度更高。如果最高曲线是红色,客户更有可能订阅,反之亦然。

对于随机森林,大多数未订阅客户的得分在00.2之间,密度峰值在0.05左右。订阅客户的得分分布更广,尽管更高,但峰值在0.1左右。两个分布重叠很多,因此很难从得分开始识别哪些客户会订阅。然而,如果营销活动针对得分高于 0.3 的所有客户,他们很可能属于蓝色簇。总之,使用随机森林,我们能够识别出一小部分很可能订阅的客户。

为了进行比较,我们可以构建关于逻辑回归输出的相同图表,如下所示:

plotDistributions(dtTestBinded, 'outputLr')

获得的直方图如下:

预测输出

对于逻辑回归,两个分布略有重叠,但它们明显覆盖了两个不同的区域,并且它们的峰值相距很远。得分高于 0.8 的客户很可能订阅,因此我们可以选择一小部分客户。如果我们选择得分高于 0.5 或 0.6 的客户,我们也能够识别出一大批可能订阅的客户。

总结来说,逻辑回归似乎表现更好。然而,分布图仅用于探索性能,并不能提供明确的评估。下一步是定义如何使用指标来评估模型。

我们将使用的验证指标是 AUC,它依赖于另一个图表,即接收者操作特征ROC)。在构建分类模型后,我们定义一个阈值,并假设得分高于阈值的客户将订阅。ROC 显示了模型精度随阈值的变化。曲线维度为:

  • 真正率:此指标显示在订阅客户中,有多少百分比的客户得分高于阈值。此指标应尽可能高。

  • 假正率:此指标显示在非订阅客户中,有多少百分比的客户得分高于阈值。此指标应尽可能低。

曲线下面积AUC)是 ROC 曲线下的面积。给定一个订阅了服务的随机客户和另一个未订阅的随机客户,AUC 表示订阅客户的得分高于其他客户的概率。

我们可以定义一个函数来构建图表并计算 AUC 指数:

  1. 加载包含用于交叉验证模型的函数的ROCR包:

    library('ROCR')
    
  2. 定义函数及其输入,包括数据表和得分列的名称:

    plotPerformance <- function(dtTestBinded, colPred)
    {
    
  3. 定义一个预测对象,它是构建 ROC 图表的起点。该函数是prediction,由ROCR包提供:

      pred <- dtTestBinded[, prediction(get(colPred), output)]
    
  4. 构建 ROC 图表。由ROCR包提供的函数是performance,它允许以不同的方式评估预测。在这种情况下,我们想要构建一个包含truefalse正率的图表,因此输入是真正率tpr)和假正率fpr):

      perfRates <- performance(pred, 'tpr', 'fpr')
      plot(perfRates)
    
  5. 使用performance计算 AUC 指数。输入是auc,它定义了我们在计算 AUC 指数:

      perfAuc <- performance(pred, 'auc')
      auc <- perfAuc@y.values[[1]]
    
  6. 将 AUC 指数作为函数输出:

      return(auc)
    }
    

使用plotPerformance,我们可以构建关于随机森林的图表,并计算存储在aucRf中的auc指数:

aucRf <- plotPerformance(dtTestBinded, 'outputRf')

获得的直方图如下:

预测输出

如预期,图表显示了 tpr 和 fpr。当阈值是1时,没有客户的比率高于它,因此没有正例(预测为订阅的客户)。在这种情况下,我们处于右上角,两个指数都等于 100%。随着阈值的降低,我们有更多的正客户,因此 tpr 和 fpr 降低。最后,当阈值是0时,tpr 和 fpr 都等于0,我们处于左下角。在一个理想的情况下,tpr 等于1,fpr 等于0(左上角)。然后,曲线越接近左上角,越好。

与随机森林类似,我们构建图表并计算逻辑回归的 AUC 指数:

aucLr <- plotPerformance(dtTestBinded, 'outputLr')

获得的直方图如下:

预测输出

逻辑回归的图表与随机森林的图表相似。观察细节,我们可以注意到左下角的曲线更陡峭,右上角的曲线则不那么陡峭,因此定义 AUC 的曲线下面积更大。

交叉验证包含一个随机成分,因此 AUC 指数可能会有所变化。设置nIter = 100,我上次执行脚本时,随机森林的 AUC 大约为 73%,逻辑回归的 AUC 大约为 79%。我们可以得出结论,逻辑回归表现更好,因此我们应该使用它来构建模型。

在本节中,我们学习了如何构建一个为顾客提供评分的模型。此算法允许公司识别出更有可能订阅的客户,并且还可以估计其准确性。本章的延续将是选择特征子集和优化参数,以实现更好的性能。

摘要

在本章中,你学习了如何探索和转换与商业问题相关的数据。你使用聚类技术来细分银行的客户群,并使用监督学习技术来识别对客户进行评分的排名。在构建机器学习模型后,你能够通过可视化 ROC 曲线和计算 AUC 指数来交叉验证它。这样,你就有能力选择最合适的技巧。

本书展示了机器学习模型如何解决商业问题。这本书不仅仅是一个教程,它是一条道路,展示了机器学习的重要性,如何开发解决方案,以及如何使用这些技术来解决商业问题。我希望这本书不仅传达了机器学习概念,还传达了对一个既有价值又迷人的领域的热情。我想感谢你跟随这条道路。我希望这只是美好旅程的开始。

如果你有任何疑问,请随时联系我。

posted @ 2025-09-03 10:23  绝不原创的飞龙  阅读(11)  评论(0)    收藏  举报