R-机器学习项目-全-

R 机器学习项目(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

当涉及到轻松执行计算统计(统计计算)并探索机器学习的数学方面时,R 是最受欢迎的语言之一。通过这本书,你将利用 R 生态系统构建高效的机器学习应用程序,以在你的组织中执行智能任务。

这本书将帮助你测试你的知识和技能,指导你如何从简单的到复杂的机器学习项目进行构建。你将首先学习如何使用集成方法构建强大的机器学习模型来预测员工流失。接下来,你将实现一个笑话推荐引擎,对亚马逊评论进行情感分析。你还将探索不同的聚类技术,使用批发数据来细分客户。除此之外,这本书还将让你熟悉使用自动编码器和强化学习进行信用卡欺诈检测,以及如何在赌场老丨虎丨机上做出预测并赢得胜利。

在本书结束时,你将能够自信地执行复杂任务,为自动化操作构建研究和商业项目。

这本书面向的对象

这本书是为那些希望通过构建实际项目来掌握机器学习概念的数据分析师、数据科学家和机器学习开发者而编写的。每个项目都将帮助你测试你的专业知识,以实现机器学习算法和技术的工作机制。对机器学习的基本理解和 R 编程的实际知识是必不可少的。

本书涵盖的内容

第一章,探索机器学习领域,将简要回顾从业者必须了解的各种机器学习概念。在本章中,我们将涵盖监督学习、强化学习、无监督学习和实际机器学习应用案例等主题。

第二章,使用集成模型预测员工流失,介绍了通过集成学习方法创建强大的机器学习模型。本章涉及的项目来自人力资源领域。保留有才华的员工是公司面临的关键挑战。如果我们能够提前很好地预测员工的流失,那么人力资源或管理团队可能采取措施防止潜在的流失变成现实。恰好可以通过应用机器学习来预测员工流失。本章利用了 IBM 整理的公共数据集,该数据集提供了一个伪员工流失人口和特征。我们首先介绍当前的问题,然后尝试使用探索性数据分析EDA)来探索数据集。下一步是预处理阶段,这包括利用先前的领域经验创建新特征。一旦数据集准备充分,将使用多种集成技术创建模型,例如袋装、提升、堆叠和随机化。最后,我们将部署最终选定的模型用于生产。我们还将了解用于创建模型的多种集成技术背后的概念。

第三章,实现一个笑话推荐引擎,介绍了推荐引擎,这些引擎旨在预测用户会对电影、音乐等内容给出哪些评分。基于用户之前喜欢或看过的内容以及其他配置文件属性,推荐引擎会建议用户可能喜欢的新内容。这类引擎在近年来获得了很大的重要性。我们通过一个笑话推荐引擎项目来探索令人兴奋的推荐系统领域。在本章中,我们首先理解协作过滤算法的概念和类型。然后我们将构建一个推荐引擎,使用基于用户的协作过滤和基于项目的协作过滤等协作过滤方法提供个性化的笑话推荐。本项目使用的数据集是一个名为 Jester 笑话数据集的公开数据集。除此之外,我们还将探索 R 中可用于构建推荐系统的各种库,并将比较这些方法获得的效果。此外,我们利用市场篮子分析技术,这是一种在营销领域相当流行的技术,来辨别各种笑话之间的关系。

第四章,《使用自然语言处理进行亚马逊评论的情感分析》,涵盖了情感分析,这包括找出句子的情感并将其标记为正面、负面或中性。本章介绍了情感分析,并涵盖了可以用来分析文本的各种技术。我们将了解文本挖掘概念以及根据语气对文本进行标记的各种方式。

我们将应用情感分析于亚马逊产品评论数据。这个数据集包含数百万条亚马逊客户评论和星级评分。这是一个分类任务,我们将根据语气将每条评论分类为正面、负面或中性。除了使用各种流行的 R 文本挖掘库对要分类的评论进行预处理外,我们还将利用广泛的文本表示,如词袋模型、word2vec、fastText 和 Glove。每种文本表示随后被用作机器学习算法的输入以执行分类。在实施这些技术的过程中,我们还将了解这些技术背后的概念,并探索其他我们可以成功应用这些技术的实例。

第五章,《利用批发数据进行的客户细分》,涵盖了客户的细分、分组或聚类,这可以通过无监督学习实现。在本章中,我们探讨了客户分组的各个方面。客户细分是产品销售者用来了解客户和收集信息的重要工具。客户可以根据不同的标准进行细分,例如年龄和消费模式。在本章中,我们学习了客户分组的各种技术。对于项目,我们使用包含批发交易的数据库。这个数据库可在 UCI 机器学习数据库中找到。我们将应用高级聚类技术,如 k-means、DIANA 和 AGNES。有时,我们可能不知道手头数据集中存在的组数。我们将探索处理这种模糊性的机器学习技术,让机器根据输入数据的潜在特征找出可能的组数。评估聚类算法的输出是实践者常常面临的挑战领域。我们也探讨了这一领域,以便全面理解将聚类算法应用于现实世界问题的方法。

第六章,使用深度神经网络进行图像识别,介绍了卷积神经网络CNNs),这是一种深度神经网络,在计算机视觉应用中非常流行。在本章中,我们学习了 CNNs 背后的基本概念。我们探讨了为什么 CNNs 在处理计算机视觉问题(如目标检测)方面表现得如此出色。我们讨论了迁移学习方面,以及它是如何与 CNNs 协同工作来解决计算机视觉问题的。正如本书其他地方一样,我们将遵循“做中学”的哲学。我们将通过在 MNIST 这个流行的公开数据集上构建多类分类模型来学习所有这些概念。项目的目标是分类给定的手写数字图像。项目探讨了从原始图像创建特征的方法。我们将学习可以应用于图像数据的各种预处理技术,以便使用深度学习模型。

第七章,使用自编码器进行信用卡欺诈检测,介绍了自编码器,这是一种另一种无监督的深度学习网络。我们首先通过理解自编码器以及它们与其他深度学习网络(如循环神经网络RNNs)和卷积神经网络(CNNs))的不同之处来开始本章。我们将通过实施一个识别信用卡欺诈的项目来学习自编码器。信用卡公司一直在寻找检测信用卡欺诈的方法。欺诈检测是银行保护其收入的关键方面。通过在金融领域应用机器学习来解决特定的欺诈检测问题可以实现这一点。欺诈通常是一个需要立即采取行动的异常事件。在本章中,我们将使用自编码器来检测欺诈。自编码器是包含瓶颈层的神经网络,其维度小于输入数据。在本章中,我们将熟悉降维以及如何使用它来识别信用卡欺诈检测。对于项目,我们将使用 H2O 深度学习框架与 R 结合使用。至于数据集,我们使用一个包含 2013 年 9 月欧洲持卡人信用卡交易的公开数据集。总共有 284,807 笔交易,其中 492 笔是欺诈交易。

第八章,使用循环神经网络进行自动文本生成,介绍了最近受到很多关注的某些深度神经网络DNNs)。这是由于它们在机器学习的各个领域取得了显著成果,从人脸识别和物体检测到音乐生成和神经艺术。本章介绍了理解深度学习所需的概念。我们讨论了神经网络的基本组成部分,如神经元、隐藏层、各种激活函数、处理神经网络中遇到问题的技术,以及使用优化算法获取神经网络中的权重。我们还将从头实现一个神经网络来展示这些概念。本章的内容将帮助我们获得神经网络的基础知识。然后,我们将通过一个项目学习如何应用 RNN。人们一直认为,创作任务,如撰写故事、写诗和绘画,只能由人类完成。但多亏了深度学习,这种情况已经不再成立了!现在,技术可以完成创作任务。我们将创建一个基于长短期记忆LSTM)网络的应用程序,这是一种 RNN 的变体,可以自动生成文本。为了完成这个任务,我们使用了 MXNet 框架,它扩展了对 R 语言的支持以执行深度学习。在实现这个项目的过程中,我们还将了解更多关于 RNN 和 LSTM 周围的概念。

第九章,使用强化学习赢得赌场老丨虎丨机,首先解释了强化学习。我们讨论了强化学习的各种概念,包括解决所谓的多臂老丨虎丨机问题的策略。我们实现了一个项目,使用 UCB 和 Thompson 抽样技术来解决多臂老丨虎丨机问题。

附录,未来的道路,简要讨论了机器学习领域的进步以及跟上这些进步的必要性。

为了充分利用本书

本书涵盖的项目旨在让您了解将各种机器学习技术应用于现实世界问题的实际知识。预期您对 R 有良好的实际操作知识,并对机器学习有一些基本理解。在开始此项目之前,对机器学习和 R 的基本知识是必须的。

还应注意的是,项目代码使用的是 R 版本 3.5.2(2018-12-20),昵称为 Eggshell Igloo。项目代码已在 Linux Mint 18.3 Sylvia 上成功测试。没有理由相信代码在其他平台,如 Windows 上无法工作;然而,这并不是作者测试过的事情。

下载示例代码文件

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

您可以通过以下步骤下载代码文件:

  1. www.packt.com上登录或注册。

  2. 选择“支持”标签。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

下载文件后,请确保您使用最新版本解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/R-Machine-Learning-Projects。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包可供在 github.com/PacktPublishing/ 找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789807943_ColorImages.pdf

使用的约定

在本书中使用了多种文本约定。

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个例子:“rsample库包含了这个数据集,我们可以直接从库中使用这个数据集。”

代码块设置如下:

setwd("~/Desktop/chapter 2") 
library(rsample)
data(attrition) 
str(attrition) 
mydata<-attrition 

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

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

粗体:表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的文字会以这种方式显示。以下是一个例子:“您可能记得亚马逊(或任何电子商务网站)上显示推荐的 Customers Who Bought This Item Also Bought This 标题。”

警告或重要提示看起来像这样。

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

联系我们

欢迎读者反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并给我们发送电子邮件至customercare@packtpub.com

勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告这一点,我们将不胜感激。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并附上材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评价

请留下您的评价。一旦您阅读并使用过这本书,为何不在购买它的网站上留下评价呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 公司可以了解您对我们产品的看法,并且我们的作者可以查看他们对书籍的反馈。谢谢!

想了解更多关于 Packt 的信息,请访问packt.com

第一章:探索机器学习领域

机器学习ML)是人工智能AI)的一个令人惊叹的子领域,它试图模仿人类的认知学习行为。类似于婴儿通过观察遇到的例子来学习的方式,机器学习算法通过观察作为输入提供的数据点来学习未来事件的结果或响应。

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

  • 机器学习与软件工程

  • 机器学习方法的类型

  • 机器学习术语——快速回顾

  • 机器学习项目流程

  • 学习范式

  • 数据集

机器学习与软件工程

随着大多数人从传统的软件工程实践转向机器学习,理解这两个领域之间的潜在差异非常重要。表面上,这两个领域似乎都生成某种代码以执行特定任务。一个有趣的事实是,与软件工程中程序员根据几个条件明确编写程序不同,机器学习算法通过观察输入示例来推断游戏的规则。学到的规则随后被用于当向系统提供新的输入数据时进行更好的决策。

正如你可以在以下图表中观察到的,在没有人工干预的情况下自动从数据中推断动作是机器学习与传统编程之间的关键区别:

机器学习与传统编程的另一个关键区别是,通过机器学习获得的知识能够通过成功解释算法以前从未见过的数据来推广到训练样本之外,而用传统编程编写的程序只能执行代码中包含的响应。

另一个区别是,在软件工程中,有解决手头问题的特定方法。给定基于某些输入假设和包含条件的算法,你将能够保证在给定输入的情况下获得输出。在机器学习世界中,无法对算法获得的输出提供此类保证。在机器学习世界中,要确认某种技术是否优于另一种技术,而不实际在处理问题的数据集上尝试这两种技术,也是非常困难的。

机器学习与软件工程不同!机器学习项目可能涉及一些软件工程,但机器学习不能被视为与软件工程相同。

虽然存在多个关于机器学习的正式定义,以下是一些经常遇到的关键定义:

"机器学习是让计算机在没有明确编程的情况下行动的科学。"

—斯坦福

"机器学习基于可以无规则编程依赖从数据中学习的算法。"

—麦肯锡公司

随着数据成为未来动力的兴起,行业从业者将 AI、ML、数据挖掘、数据科学和数据分析这些术语互换使用。理解这些术语之间的关键区别以避免混淆是很重要的。

虽然 AI、ML、数据挖掘、数据科学和数据分析这些术语被互换使用,但它们并不相同!

让我们来看看以下术语:

  • AI:AI 是一种范式,其中机器能够以智能的方式执行任务。在 AI 的定义中,并没有指定机器的智能是手动实现还是自动实现。因此,可以安全地假设,即使是一个用几个if...elseswitch...case语句编写的程序,如果它被注入了机器来执行任务,也可以被认为是 AI。

  • ML:另一方面,ML 是通过从提供作为输入的数据中学习来实现机器智能的一种方式,因此我们有一个智能机器在执行任务。可以观察到,ML 实现了与 AI 相同的目标,只是智能是通过自动实现的。因此,可以得出结论,ML 仅仅是实现 AI 的一种方式。

  • 数据挖掘:数据挖掘是一个专注于发现数据集未知属性的特定领域。数据挖掘的主要目标是提取从大量输入数据中得出的规则,而在机器学习中,算法不仅从数据输入中推断规则,还使用这些规则对任何新的、传入的数据进行预测。

  • 数据分析:数据分析是一个涵盖执行基本描述性统计、数据可视化和数据点沟通以得出结论的领域。数据分析可以被认为是数据科学的一个基本层次。从业者对数据挖掘或机器学习练习提供的输入数据进行数据分析是正常的。这种数据分析通常被称为探索性数据分析(EDA)

  • 数据科学:数据科学是一个包括数据分析和数据挖掘、机器学习以及与工作领域相关的任何特定领域专长的总称。数据科学是一个涉及处理数据的多个方面的概念,例如从一个或多个来源获取数据、数据清洗、数据准备以及根据现有数据创建新的数据点。它包括执行数据分析。它还包括在数据上使用一个或多个数据挖掘或机器学习技术来推断知识,以创建一个在未见数据上执行任务的算法。这个概念还包括以使其在未来执行指定任务有用的方式部署算法。

以下是一个维恩图,展示了在数据科学领域工作的专业人士所需具备的技能。它有三个圆圈,每个圆圈定义了数据科学专业人士应具备的特定技能:

图片

让我们探索前面图表中提到的以下技能:

  • 数学与统计知识:这项技能是分析数据的统计属性所必需的。

  • 黑客技能:编程技能在快速处理数据中起着关键作用。机器学习算法被应用于创建输出,该输出将对未见数据进行预测。

  • 实质性专业知识:这项技能指的是对当前问题领域领域专业知识。它帮助专业人士能够向系统提供适当的输入,以便从中学习,并评估输入和获得的结果的适当性。

要成为一名成功的数据科学专业人士,你需要具备数学、编程技能以及对商业领域的了解。

如我们所见,人工智能、数据科学、数据分析、数据挖掘和机器学习都是相互关联的。所有这些领域目前都是行业中最热门的领域。结合正确的技能集和实际经验,将引领你在这些当前趋势领域拥有强大的职业生涯。由于机器学习构成了领先领域的基础,下一节将探讨可能应用于多个实际问题的各种机器学习方法。

机器学习无处不在!大多数时候,我们可能在使用基于机器学习的东西,但并没有意识到它的存在或它对我们生活的影响!让我们一起探索一些我们每天都能体验到的非常流行的设备或应用程序,它们由机器学习驱动:

  • 虚拟个人助理VPAs)如Google AlloAlexaGoogle NowGoogle HomeSiri等等

  • 显示你起点和终点交通预测的智能地图

  • 在 Uber 或类似交通服务中基于需求的价格上涨

  • 机场、火车站和其他公共场所的自动视频监控

  • 在社交媒体网站如 Facebook 上发布的图片中的人脸识别

  • 在 Facebook 上为你提供的个性化新闻推送

  • 在 YouTube 上为你提供的广告

  • Facebook 和其他类似网站上你可能认识的人的建议

  • 根据你的个人资料在 LinkedIn 上的工作推荐

  • 在 Google Mail 上的自动回复

  • 在在线客户支持论坛中与你交谈的聊天机器人

  • 搜索引擎结果过滤

  • 邮件垃圾邮件过滤

当然,这个列表并没有结束。前面提到的应用只是基本的一些,它们展示了机器学习对我们今天生活的影响。引用没有哪个学科领域机器学习没有触及,这并不令人惊讶!

本节中的主题绝不是机器学习的详尽描述,而只是为了让我们开始探索之旅的一个快速接触点。现在我们已经对机器学习是什么以及它可以在哪里应用有了基本的了解,让我们在下一节中深入探讨其他机器学习相关主题。

机器学习方法的类型

机器学习可以解决多种旨在解决现实世界问题的任务。一般来说,机器学习方法意味着一组特定的算法,这些算法适合解决特定类型的问题,并且该方法解决了问题带来的任何约束。例如,特定问题的约束可能是可用于作为学习算法输入的有标签数据的可用性。

从本质上讲,流行的机器学习方法是监督学习、无监督学习、半监督学习、强化学习和迁移学习。本节其余部分将详细说明这些方法。

监督学习

当一个人对需要从问题中实现的结果非常清楚,但对影响输出的数据之间的关系不确定时,就会应用监督学习算法。我们希望我们应用在数据上的机器学习算法能够感知不同数据元素之间的关系,以便实现预期的输出。

这个概念可以通过一个例子来更好地解释——在银行,在发放贷款之前,他们希望预测贷款申请人是否会偿还贷款。在这种情况下,问题非常明确。如果向潜在客户 X 发放贷款,有两种可能性:X 会成功偿还贷款或 X 不会偿还贷款。银行希望使用机器学习来识别客户 X 所属的类别;即,成功的贷款偿还者或违约的贷款偿还者。

虽然要解决的问题定义是清晰的,但请注意,对客户将有助于成功偿还或未偿还贷款的特征并不明确,这是我们希望机器学习算法通过观察数据中的模式来学习的东西。

这里的主要挑战是我们需要提供代表成功偿还贷款的客户和未能偿还贷款的客户的数据。银行可以简单地查看历史数据以获取两类客户的记录,然后根据适当的情况将每条记录标记为已支付或未支付类别。

因此标记的记录现在成为监督学习算法的输入,以便它可以学习两类客户的模式。从标记数据中学习的过程称为训练,学习过程中获得(算法)输出称为模型。理想情况下,银行会从训练数据中保留一部分标记数据,以便能够测试创建的模型,这种数据被称为测试数据。应该不会令人惊讶的是,用于训练模型的标记数据被称为训练数据

一旦模型建立,通过用测试数据测试模型来获取测量结果,以确保模型达到令人满意的性能水平,否则进行模型构建迭代,直到获得所需的模型性能。在测试数据上达到所需性能的模型可以由银行用来推断任何新的贷款申请人是否会成为未来的违约者,如果是的话,就可以在向该申请人提供贷款方面做出更好的决策。

简而言之,当目标非常明确且作为算法学习模式的输入有标签数据时,会使用监督机器学习算法。以下图表总结了监督学习过程:

图片

监督学习可以进一步分为两类,即分类回归。本节中解释的银行贷款违约者预测是一个分类的例子,它旨在预测一个名义类型的标签,如是或否。另一方面,也可以预测数值(连续值),这种预测称为回归。回归的一个例子是根据该地区的房屋需求、卧室数量、房屋尺寸和公共交通的可达性来预测城市黄金地段房屋的月租金。

存在多种监督学习算法,该领域一些广为人知的算法包括分类和回归树CART)、逻辑回归、线性回归、朴素贝叶斯、神经网络、k-最近邻KNN)和支持向量机SVM)。

无监督学习

标签数据的可用性并不常见,手动标注数据也不便宜。这就是无监督学习发挥作用的情况。

例如,一家小型精品公司想要向在其 Facebook 页面上注册的客户推出促销活动。虽然业务目标很明确——需要向客户推出促销活动——但不清楚哪些客户属于哪个群体。与监督学习方法中存在关于不良债务人和良好债务人的先验知识不同,在这种情况下没有这样的线索。

当将客户信息作为输入提供给无监督学习算法时,它试图识别数据中的模式,从而将具有相似属性的客户数据进行分组。

同类鸟儿聚在一起,这是在无监督学习中客户分组的原理。

这些有机群体形成的推理可能不是非常直观。可能需要一些研究来识别导致一组客户聚集在群体中的因素。大多数时候,这项研究是手动的,每个群体中的数据点需要验证。这项研究可能成为确定特定促销活动需要推出的群体的基础。这种无监督学习的应用称为聚类。以下图表显示了无监督机器学习在聚类数据点中的应用:

图片

有许多聚类算法。然而,最受欢迎的包括 k-means 聚类、k-modes 聚类、层次聚类、模糊聚类等。

其他形式的无监督学习确实存在。例如,在零售行业,一种称为关联规则挖掘的无监督学习方法被应用于客户购买行为,以识别一起购买的物品。在这种情况下,与监督学习不同,根本不需要标签。所涉及的任务只需要机器学习算法识别客户一起计费的产品之间的潜在关联。从关联规则挖掘中获得的信息有助于零售商将一起购买的产品放置在附近。其理念是,可以直观地鼓励顾客购买额外的产品。

在现有的执行关联规则挖掘的算法中,等价类转换Eclat)和频率模式增长FPG)是流行的算法。

另一种形式的无监督学习是异常检测或离群点检测。该练习的目标是识别不属于无监督学习算法输入的其他元素的点。与关联规则挖掘类似,由于问题的性质,算法不需要使用标签来实现目标。

欺诈检测是信用卡行业中异常检测的重要应用。信用卡交易实时监控,任何可疑的交易模式都会立即标记,以避免信用卡用户和信用卡提供商遭受损失。监控的不寻常模式可能是在外币中进行的巨额交易,而不是特定客户通常交易的正常货币。这可能是同一天在两个不同大陆的实体店中的交易。一般想法是能够标记出偏离常规的模式。

K-means 聚类和单类 SVM 是两种著名的无监督机器学习算法,用于观察人群中的异常情况。

总体来说,可以理解,无监督学习无疑是非常重要的一种方法,考虑到用于训练的标记数据是一种稀缺资源。

半监督学习

半监督学习是监督和无监督方法的混合体。机器学习需要大量的数据用于训练。大多数时候,观察到模型训练所使用的数据量与模型性能之间存在直接的正比关系。

在如医学成像等利基领域,有大量的图像数据(MRI、X 光、CT 扫描)可用。然而,合格的放射科医生的时间和可用性来标记这些图像是稀缺的。在这种情况下,我们可能只能获得少数由放射科医生标记的图像。

半监督学习通过构建一个初始模型来利用少量标记图像,该模型用于标记领域中存在的大量未标记数据。一旦有大量标记数据可用,可以使用监督机器学习算法来训练和创建一个用于在未见数据上的预测任务的最终模型。以下图表说明了半监督学习涉及到的步骤:

图片

语音分析、蛋白质合成和网页内容分类是某些领域,在这些领域中,存在大量未标记的数据和较少的标记数据。在这些领域应用半监督学习取得了成功的结果。

生成对抗网络GANs)、半监督支持向量机S3VMs)、基于图的方法和马尔可夫链方法是半监督机器学习领域中众所周知的方法。

强化学习

强化学习RL)是一种既不是监督学习也不是无监督学习的机器学习方法。在这种方法中,一开始就向这种学习算法提供奖励定义作为输入。由于算法没有提供用于训练的标记数据,这种学习算法不能归类为监督学习。另一方面,它也不归类为无监督学习,因为算法被提供了关于奖励定义的信息,这些信息指导算法通过采取解决问题的步骤。

强化学习旨在通过依赖收到的反馈来不断改进解决任何问题的策略。目标是最大化奖励,同时采取解决问题的步骤。获得的奖励由算法本身根据奖励和惩罚定义来计算。其理念是实现最优步骤,以最大化奖励来解决当前问题。

以下图表展示了通过强化学习方法在火灾特定情境下自动确定理想行为的机器人:

图片

一台机器在 Atari 视频游戏中超越人类被称为强化学习最显著的成功故事之一。为了实现这一壮举,将大量人类玩过的示例游戏作为输入提供给算法,该算法学习了采取最大化奖励的步骤。在这种情况下,奖励是最终得分。算法在从示例输入中学习后,仅模拟了游戏中每个步骤的模式,最终最大化了获得的分数。

虽然强化学习可能看起来只能应用于游戏场景,但在工业界也有许多这种方法的应用案例。以下提到的例子是其中三个这样的案例:

  • 基于自发的供需关系动态定价商品和服务,以实现利润最大化,是通过强化学习的一种变体Q 学习来实现的。

  • 在仓库中有效利用空间是库存管理专业人士面临的关键挑战。市场需求波动、大量库存的可用性和库存补充的延迟是影响空间利用的关键约束。强化学习算法用于优化采购库存的时间以及从仓库中检索商品的时间,从而直接影响到被称为库存管理领域的空间管理问题。

  • 在医学科学中,为了治疗如癌症等疾病,需要长期治疗和差异化的药物管理。治疗高度个性化,基于患者的特征。治疗通常涉及在各个阶段的治疗策略的变化。这种治疗计划通常被称为动态治疗制度DTR)。强化学习有助于处理临床试验数据,根据输入到强化学习算法的患者特征,提出适当的个性化 DTR。

有四种非常流行的强化学习算法,分别是 Q 学习、状态-动作-奖励-状态-动作SARSA)、深度 Q 网络DQN)和深度确定性策略梯度DDPG)。

迁移学习

代码的可重用性是面向对象编程OOP)的基本概念之一,在软件工程领域非常流行。同样,迁移学习涉及重用为完成特定任务而构建的模型来解决另一个相关任务。

要达到更好的性能测量,机器学习模型需要在大量标记数据上进行训练是理所当然的。数据量较少意味着训练较少,结果是性能次优的模型。

迁移学习试图通过重用不同相关模型获得的知识来解决数据量较少时出现的问题。拥有较少的数据点来训练模型不应该阻碍构建更好的模型,这是迁移学习的核心概念。以下图表展示了迁移学习在图像识别任务中的目的,该任务用于分类狗和猫的图像:

图片

在这个任务中,一个神经网络模型涉及到在第一层中检测边缘、颜色块检测等。只有在渐进层(可能在最后几层)中,模型才会尝试识别狗或猫的面部特征,以便将它们分类为目标之一(狗或猫)。

可能会观察到,识别边缘和颜色块的任务并不特定于猫和狗的图像。即使模型是在非狗或非猫的图像上训练的,也可能一般地推断出边缘或颜色块的知识。最终,如果将这种知识结合从推断猫脸与狗脸中得出的知识,即使数量较少,我们也将拥有比在较少图像上训练得到的次优模型更好的模型。

在狗-猫分类器的情况下,首先,在一个大型图像集上训练一个模型,这些图像不仅限于猫和狗的图像。然后,将模型取来,在狗和猫的脸上重新训练最后几层。因此获得的模型经过测试并使用后,证明了性能测量结果是令人满意的。

迁移学习的概念不仅用于图像相关任务。另一个例子是它在自然语言处理NLP)中的应用,它可以对文本数据进行情感分析。

假设有一家公司推出了一款前所未有的新产品(比如说,现在是一架飞行汽车)。任务是分析与新产品质量相关的推文,并将每一条推文识别为正面、负面或中性情感。可能会观察到,在飞行汽车领域,之前标记的推文是不可用的。在这种情况下,我们可以使用基于多个产品和领域通用产品评论标记数据的模型。我们可以通过补充飞行汽车领域特定的术语来重用该模型,从而获得一个新的模型。这个新模型最终将用于测试和部署,以分析关于新推出的飞行汽车的推文中的情感。

可以通过以下两种方式实现迁移学习:

  • 通过重用自己训练的模型

  • 通过重用预训练模型

预训练模型是由各种组织或个人作为其研究工作或作为比赛的一部分构建的模型。这些模型通常非常复杂,并且在大量的数据上进行了训练。它们也被优化以高精度执行其任务。这些模型在现代硬件上训练可能需要几天或几周的时间。组织或个人通常会以许可许可证的形式发布这些模型以供重用。通过迁移学习范式可以下载并重用这些预训练模型。这将有效地利用预训练模型所拥有的大量现有知识,这对于有限的硬件资源和数据量的个人来说可能难以获得。

有多个预训练模型由不同的机构提供。以下描述的是一些流行的预训练模型:

  • Inception-V3 模型:这个模型作为大型视觉识别挑战的一部分在 ImageNet 上进行了训练。比赛要求参与者将给定的图像分类为 1000 个类别之一。其中一些类别包括动物名称和物体名称。

  • MobileNet:这个预训练模型是由谷歌构建的,旨在使用 ImageNet 数据库进行目标检测。其架构是为移动设备设计的。

  • VCG Face:这是一个为面部识别构建的预训练模型。

  • VCG 16:这是一个在MS COCO数据集上训练的预训练模型。这个模型实现了图像描述;也就是说,给定一个输入图像,它生成一个描述图像内容的字幕。

  • 谷歌的 Word2Vec 模型和斯坦福的 GloVe 模型:这些预训练模型以文本作为输入,并生成单词向量作为输出。分布式单词向量是表示文档的一种形式,用于自然语言处理(NLP)或机器学习(ML)应用。

现在我们对各种可能的机器学习方法有了基本的了解,在下一节中,我们将快速回顾机器学习中使用的核心术语。

机器学习术语 – 快速回顾

在本节中,我们回顾了流行的机器学习术语。这个非详尽的回顾将帮助我们快速复习,并使我们能够顺利地跟随本书涵盖的项目。

深度学习

这是一个革命性的趋势,在最近的机器学习(ML)世界中已经成为一个超级热门的话题。它是一类使用具有多个隐藏层神经元的人工神经网络ANNs)来解决问题的机器学习算法。

通过将深度学习应用于几个现实世界问题,可以获得优越的结果。卷积神经网络CNNs)、循环神经网络RNNs)、自编码器AEs)、生成对抗网络GANs)和深度信念网络DBNs)是一些流行的深度学习方法。

大数据

这个术语指的是大量数据,这些数据结合了结构化数据类型(类似于表格的行和列)和非结构化数据类型(文本文档、语音录音、图像数据等)。由于数据量很大,它不适合机器学习算法需要执行的主存储器。需要单独的策略来处理这些大量数据。数据分布式处理和结果合并(通常称为MapReduce)是一种策略。也有可能每次只处理足够的数据,使其能够适应主存储器,并将结果存储在硬盘上的某个地方;我们需要重复这个过程,直到完全处理完所有数据。数据处理后,需要将结果合并,以获得所有已处理数据的最终结果。

要在大数据上执行机器学习,需要特殊技术,如 Hadoop 和 Spark。不用说,你需要磨练专门技能,以便成功地将这些技术应用于大数据上的机器学习算法。

自然语言处理

这是机器学习的一个应用领域,旨在使计算机理解人类语言,如英语、法语和普通话。自然语言处理应用允许用户使用口语与计算机交互。

聊天机器人、语音合成、机器翻译、文本分类和聚类、文本生成和文本摘要是一些流行的自然语言处理应用。

计算机视觉

这个机器学习领域试图模仿人类的视觉。目标是使计算机能够看到、处理和确定图像或视频中的对象。深度学习和强大硬件的可用性导致了该领域机器学习非常强大应用的出现。

自动驾驶汽车等自动驾驶汽车、物体识别、物体跟踪、运动分析和图像恢复是计算机视觉的一些应用。

成本函数

成本函数、损失函数或误差函数在实践中可以互换使用。每个都用于定义和衡量模型的误差。机器学习算法的目标是最小化数据集中的损失。

成本函数的一些例子包括用于线性回归的平方损失、用于支持向量机的 hinge 损失以及用于分类算法中准确度测量的 0/1 损失。

模型准确度

准确度是用于衡量机器学习模型性能的流行指标之一。这个测量方法易于理解,并有助于从业者非常容易地向其商业用户传达模型的良好性能。

通常,这个指标用于分类问题。准确度是通过正确预测的数量除以总预测数量来衡量的。

混淆矩阵

这是一个描述分类模型性能的表格。它是一个 nn 列的矩阵,其中 n 代表分类模型预测的类别数量。它通过记录模型在比较实际标签时的正确和错误预测数量而形成。

混淆矩阵可以通过一个例子更好地解释——假设数据集中有 100 张图片,其中包含 50 张狗图片和 50 张猫图片。一个旨在将图像分类为猫图像或狗图像的模型被给出这个数据集。模型的输出显示,40 张狗图片被正确分类,20 张猫图片被正确预测。以下表格是模型预测输出的混淆矩阵构建:

模型预测标签 实际标签
20
10

预测变量

这些变量也被称为独立变量x 值。这些是帮助预测因变量或目标或响应变量的输入变量。

在房屋租赁预测用例中,房屋的面积(平方英尺)、卧室数量、该地区空置房屋的数量、距离公共交通的远近、以及医院和学校等设施的可达性都是一些预测变量,它们决定了房屋的租金。

响应变量

实践者将因变量或目标或 y 值互换地用作响应变量的替代词。这是模型根据作为模型输入的独立变量预测的输出变量。

在房屋租赁预测用例中,预测的租金是响应变量。

降维

特征减少(或特征选择)或降维是将独立变量的输入集减少到所需变量数量更少的过程,这些变量是模型预测目标所需的。

在某些情况下,可以通过组合多个因变量来表示它们,而不会丢失太多信息。例如,与其有两个独立变量,如矩形的长度和矩形的宽度,不如用一个称为面积的单个变量来表示这些维度,该变量代表矩形的长度和宽度。

以下是我们需要对给定输入数据集执行降维的多个原因:

  • 为了帮助数据压缩,因此适应更小的磁盘空间。

  • 使用更少的维度来表示数据时,数据处理的时间会减少。

  • 它从数据集中移除了冗余特征。冗余特征通常在数据中被称为多重共线性

  • 将数据减少到更少的维度有助于通过图表和图表可视化数据。

  • 降维从数据集中移除噪声特征,从而提高模型性能。

在数据集中实现降维有许多方法。使用过滤器,例如信息增益过滤器和对称属性评估过滤器,是一种方法。基于遗传算法的选择和主成分分析PCA)是其他流行的技术,用于实现降维。存在混合方法来实现特征选择。

类不平衡问题

假设需要构建一个分类器,用于识别猫和狗的图像。这个问题有两个类别,即猫和狗。如果训练一个分类模型,则需要训练数据。在这种情况下,训练数据是基于作为输入提供的狗和猫的图像,以便监督学习模型可以学习狗与猫的特征。

可能会出现这样的情况:如果数据集中有 100 张用于训练的图像,其中 95 张是狗的图片,5 张是猫的图片。这种不同类别在训练数据集中的不平等表示被称为类不平衡问题。

大多数机器学习技术在工作时,每个类别的示例数量大致相等时效果最好。可以采用某些技术来应对数据中的类不平衡问题。一种技术是减少多数类(狗的图像)样本,使它们与少数类(猫的图像)相等。在这种情况下,会有信息损失,因为许多狗的图像未被使用。另一种选择是生成与少数类(猫的图像)数据相似的人工数据,以便使数据样本的数量与多数类相等。合成少数过采样技术SMOTE)是生成人工数据的一种非常流行的技术。

应注意,准确率不是评估训练数据集存在类不平衡问题时模型性能的好指标。假设基于类不平衡数据集构建的模型预测任何测试样本为多数类。在这种情况下,得到 95%的准确率,因为测试数据集中大约 95%的图像是狗的图像。但这个性能只能被称为骗局,因为模型没有任何区分能力——它只是预测任何需要预测的图像为狗的类别。在这种情况下,所有图像都被预测为狗,但模型仍然以非常高的准确率逃脱,这表明它是一个很好的模型,无论它是否真的是这样!

在存在类不平衡问题时,有几种其他性能指标可供使用,F1 分数和受试者工作特征曲线下面积AUCROC)是一些流行的指标。

模型偏差和方差

虽然有几种机器学习算法可以构建模型,但可以根据模型产生的偏差和方差误差来选择模型。

偏差误差发生在模型从提供给它的数据集中学习真实信号的能力有限时。一个高度偏差的模型本质上意味着模型在平均情况下是一致的但不够准确。

方差误差发生在模型对其训练数据集过于敏感时。一个模型具有高方差本质上意味着训练好的模型在平均情况下对任何测试数据集都会产生高精度,但它们的预测结果不一致。

欠拟合与过拟合

欠拟合与过拟合是与偏差和方差密切相关的概念。这两个是模型表现不佳的最大原因,因此,在构建机器学习模型时,从业者必须非常关注这些问题。

当模型在训练数据和测试数据上表现都不好时,这种情况被称为欠拟合。这种情况可以通过观察高训练误差和高测试误差来检测。存在欠拟合问题意味着用于拟合模型的机器学习算法不适合对训练数据的特征进行建模。因此,唯一的补救措施是尝试其他类型的机器学习算法来建模数据。

过拟合是一种情况,其中模型对训练数据的特征学习得如此之好,以至于它无法泛化到其他未见数据。在过拟合模型中,模型将训练数据中的噪声或随机波动视为真实信号,并在未见数据中也寻找这些模式,因此影响了模型的表现。

过拟合在非参数和非线性模型中更为普遍,如决策树和神经网络。剪枝是克服这一问题的补救措施之一。另一种补救措施是称为dropout的技术,其中从模型中随机丢弃一些学习到的特征,从而使模型对未见数据更具泛化能力。正则化是解决过拟合问题的另一种技术。这是通过对模型系数进行惩罚来实现的,从而使模型更好地泛化。L1 惩罚和 L2 惩罚是正则化在回归场景中可以执行的类型。

对于从业者来说,目标是确保模型既不过拟合也不欠拟合。为了实现这一点,学习何时停止训练机器学习数据至关重要。可以在图表上绘制训练误差和验证误差(在一个小部分训练数据集上测量的误差,这部分数据被保留),并确定训练数据持续下降,而验证误差开始上升的点。

有时,在训练数据上获得性能度量并期望在未见过的数据上获得相似度量可能不起作用。通过采用称为 k 折交叉验证的数据重采样技术,可以从模型中获得更现实的训练和测试性能估计。k 折交叉验证中的k代表一个数字;例如,包括 3 折交叉验证、5 折交叉验证和 10 折交叉验证。k 折交叉验证技术涉及将训练数据分成k部分,并运行训练过程k+1 次。在每次迭代中,训练在数据k - 1 个分区上进行,而k(th)分区则专门用于测试。需要注意的是,在每次迭代中,用于测试的*k*(th)分区和用于训练的k - 1 个分区都会进行洗牌,因此训练数据和测试数据在每个迭代中都不会保持不变。这种方法使得能够获得对未来未见数据中模型可预期的性能的悲观度量。

在实践中,使用 10 折交叉验证和 10 次运行来获得模型性能被认为是对模型性能的黄金标准估计。在工业设置和关键机器学习应用中,始终推荐以这种方式估计模型性能。

数据预处理

这实际上是在机器学习项目管道的早期阶段采用的一个步骤。数据预处理涉及将原始数据转换为机器学习算法可接受的输入格式。

特征哈希、缺失值插补、将变量从数值型转换为名义型以及相反操作,是数据预处理阶段众多可执行操作中的几个步骤。

将原始文本文档转换为词向量是数据预处理的一个例子。因此获得的词向量可以输入到机器学习算法中,以实现文档分类或文档聚类。

保留样本

在处理训练数据集时,会保留一小部分数据用于测试模型的性能。这部分小数据是未见数据(未用于训练),因此可以依赖为此数据获得的度量。获得的度量可以用来调整模型的参数,或者只是报告模型的性能,以便设定期望,即从模型中可以期望达到何种性能水平。

可能需要注意的是,基于保留样本的性能测量并不像 k 折交叉验证估计那样稳健。这是因为,在从原始数据集中随机分割保留集的过程中,可能存在一些未知的偏差。此外,也没有保证保留数据集代表了训练数据集中涉及的所有类别。如果我们需要在保留数据集中代表所有类别,那么需要应用一种称为分层保留样本的特殊技术。这确保了在保留数据集中有所有类别的代表。显然,从分层保留样本中获得的表现测量比从非分层保留样本中获得的表现测量更准确。

70%-30%,80%-20%,和 90%-10%通常是机器学习项目中观察到的训练数据-保留数据分割的集合。

超参数调整

机器学习或深度学习算法在训练模型之前将超参数作为输入。每个算法都附带其自己的超参数集,并且某些算法可能没有超参数。

超参数调整是模型构建的重要步骤。每个机器学习算法都附带一些默认的超参数值,通常用于构建初始模型,除非从业者手动覆盖超参数。为模型设置正确的超参数组合和正确的超参数值在大多数情况下可以极大地提高模型的表现。因此,强烈建议将超参数调整作为机器学习模型构建的一部分。搜索可能的超参数值宇宙是一个非常耗时的工作。

k-means 聚类和 k-最近邻分类中的k,随机森林中的树的数量和树的深度,以及 XGBoost 中的eta都是超参数的例子。

网格搜索和基于贝叶斯优化的超参数调整是实践中两种流行的超参数调整方法。

性能指标

模型需要在未见过的数据上进行评估以评估其好坏。好坏可以用几种方式表达,这些方式被称为模型性能指标。

存在多个指标来报告模型的性能。准确率、精确度、召回率、F 分数、灵敏度、特异性、AUROC 曲线、均方根误差RMSE)、汉明损失和均方误差MSE)是其他指标中一些流行的模型性能指标。

特征工程

特征工程是从数据集中的现有数据或通过从外部数据源获取额外数据来创建新特征的艺术。这是出于添加额外特征以提高模型性能的目的。特征工程通常需要领域专业知识和对业务问题的深入理解。

让我们来看一个特征工程的例子——对于一个正在从事贷款违约预测项目的银行,从过去几个月的区域失业趋势信息中获取和补充训练数据集可能会提高模型的性能。

模型可解释性

通常,在商业环境中,当构建机器学习模型时,仅仅报告获得的性能度量以确认模型的良好性可能是不够的。利益相关者通常渴望了解模型的“为什么”,即是什么因素导致了模型的性能?换句话说,利益相关者希望了解影响的原因。本质上,利益相关者的期望是了解模型中各种特征的重要性以及每个变量对模型影响的方向。

例如,数据集中每天锻炼时间这一特征对癌症预测模型的预测是否有任何影响?如果有,那么每天锻炼时间会推动预测向负面方向还是正面方向?

虽然这个例子听起来很简单,可以生成答案,但在现实世界的机器学习项目中,由于变量之间的复杂关系,模型的可解释性并不那么简单。很少有一个特征在孤立的情况下对预测产生任何方向的影响。实际上,是特征的组合影响了预测结果。因此,解释特征对预测影响程度就更加困难。

线性模型通常更容易解释,即使是对于商业用户来说也是如此。这是因为通过线性算法进行模型训练后,我们获得了各种特征的权重。这些权重是特征对模型预测贡献的直接指标。毕竟,在线性模型中,预测是模型权重和通过函数传递的特征的线性组合。需要注意的是,现实世界中的变量之间的交互并不一定是线性的。因此,试图模拟具有非线性关系的潜在数据的线性模型可能没有很好的预测能力。因此,虽然线性模型的解释性很好,但它是以模型性能为代价的。

相反,非线性和非参数模型往往很难解释。在大多数情况下,即使是构建模型的人也可能不清楚是什么因素在驱动预测以及预测的方向。这仅仅是因为预测结果是变量复杂非线性组合的结果。众所周知,与线性模型相比,一般而言非线性模型是性能更好的模型。因此,需要在模型可解释性和模型性能之间进行权衡。

虽然模型可解释性的目标难以实现,但完成这一目标还是有其价值的。它有助于回顾被认为表现良好的模型,并确认用于模型构建和测试的数据中不存在意外噪声。显然,以噪声作为特征模型的泛化能力会失败。模型可解释性有助于确保没有噪声作为特征渗入模型。此外,它还有助于建立与最终将成为模型输出消费者的业务用户的信任。毕竟,构建一个输出不会被消费的模型是没有意义的!

非参数、非线性模型难以解释,甚至可能无法解释。现在有专门的机器学习方法可以帮助解释黑盒模型的可解释性。部分依赖图PDP)、局部可解释模型无关解释LIME)和Shapley 加性解释SHAP),也称为 Sharpley 的,是一些从业者用来解析黑盒模型内部结构的流行方法。

现在我们已经很好地理解了机器学习各种基本术语,我们的下一步是探索机器学习项目流程的细节。下一节中讨论的这次旅行有助于我们了解构建机器学习项目、部署它以及获取用于商业用途的预测的过程。

机器学习项目流程

大多数关于机器学习项目的现有内容,无论是通过书籍、博客还是教程,都是以这种方式解释机器学习的机制,即将可用的数据集分为训练集、验证集和测试集。模型使用训练集构建,并通过验证数据进行迭代超参数调整来改进模型。一旦构建并改进到可接受的程度,就用未见过的测试数据进行测试,并报告测试结果。大多数公开内容在这一点上就结束了。

在实际情况下,业务环境中的 ML 项目不仅限于这一步骤。我们可能会观察到,如果只停留在测试和报告构建模型的性能,那么在预测未来数据方面,模型实际上并没有真正的用途。我们还需要意识到,构建模型的想法是能够在生产环境中部署模型,并基于新数据进行预测,以便企业可以采取适当的行动。

简而言之,模型需要被保存并重复使用。这也意味着,需要对需要做出预测的新数据进行预处理,其方式与训练数据相同。这确保了新数据具有与训练数据相同数量的列,以及相同的列类型。在实验室中构建的模型的生产化部分在教学中完全被忽视。本节涵盖了从数据预处理到在实验室中构建模型再到模型生产化的端到端流水线。

ML 流水线描述了从原始数据获取到对未见数据进行预测结果后处理的整个流程,以便使其可用于业务采取某种行动。可能的情况是,流水线可以在一般化层面上进行描述,或者以非常细粒度的方式进行描述。本节重点描述一个通用的流水线,该流水线可以应用于任何 ML 项目。图 1.8 显示了 ML 项目流水线的各个组成部分,也称为跨行业数据挖掘标准流程CRISP-DM)。

业务理解

一旦使用 ML 解决的问题描述得非常明确,ML 流水线的第一步就是确定该问题是否与业务相关,以及项目目标是否明确无误。还应该明智地检查当前的问题是否可以作为一个 ML 问题来解决。这些是在业务理解步骤中通常涵盖的各个方面。

理解和获取数据

下一步是确定所有与当前业务问题相关的数据来源。组织将有一个或多个系统,例如人力资源管理系统、会计系统和库存管理系统。根据问题的性质,我们可能需要从多个来源获取数据。此外,通过数据获取步骤获得的数据不一定总是以表格数据的形式存在;它可能是非结构化数据,例如电子邮件、录音文件和图像。

在具有一定规模的 corporate organizations 中,ML 专业人士可能无法独自完成从各种系统中获取数据的任务。为了成功完成流水线这一步骤,可能需要与其他组织内的专业人士进行紧密合作:

图片

准备数据

数据准备使 ML 算法能够创建输入数据。我们从数据源获得的数据通常不是很干净。有时,数据不能直接融合到 ML 算法中以创建模型。我们需要确保原始数据得到清理,并且以 ML 算法可以接受的格式准备。

EDA(探索性数据分析)是创建输入数据过程的一个子步骤。它是一个使用视觉和定量辅助工具来理解数据而不带有数据内容偏见的过程。EDA 使我们能够更深入地了解手头的数据。它帮助我们理解所需的数据准备步骤。在 EDA 过程中,我们可以获得的一些见解包括数据中存在异常值、数据中存在缺失值以及数据的重复。所有这些问题都在数据清洗过程中得到解决,这是数据准备过程中的另一个子步骤。在数据清洗过程中可以采用几种技术,以下是一些流行的技术:

  • 删除异常值记录。

  • 删除数据中的冗余列和不相关列。

  • 缺失值插补——用特殊值 NA 或空白、中位数、平均值或众数或回归值填充缺失值。

  • 对数据进行缩放。

  • 从非结构化文本数据中移除停用词,如aandhow

  • 使用诸如词干提取和词形还原等技术对非结构化文本文档中的单词进行归一化。

  • 消除文本数据中的非词典词。

  • 在文本文档中对拼写错误的单词进行拼写纠正。

  • 将文本中的不可识别的领域特定缩写词替换为实际单词描述。

  • 图像数据的旋转、缩放和平移。

将非结构化数据表示为向量,如果手头的问题需要通过监督学习来处理,则为记录提供标签,处理数据中的类别不平衡问题,进行特征工程,通过诸如对数变换、最小-最大变换、平方根变换和立方变换等变换函数转换数据,这些都是数据准备过程的一部分。

数据准备步骤的输出是表格数据,可以轻松地适应 ML 算法作为输入以创建模型。

在数据准备过程中通常执行的一个附加子步骤是将数据集划分为训练数据、验证数据和测试数据。这些不同的数据集在模型构建步骤中用于特定的目的。

模型构建和评估。

一旦数据准备就绪,在创建模型之前,我们需要从可用的特征列表中选择和选择特征。这可以通过几种现成的特征选择技术来完成。一些 ML 算法(例如 XGBoost)在算法中内置了特征选择,因此在进行建模活动之前,我们不需要显式执行特征选择。

有一系列机器学习算法可供尝试,并在数据上创建模型。此外,还可以通过集成技术创建模型。需要选择算法(们)并使用训练数据集创建模型,然后使用验证数据集调整模型的超参数。最后,可以使用测试数据集对创建的模型进行测试。在模型构建步骤中,需要处理的问题包括选择合适的指标来评估模型性能、过拟合、欠拟合以及可接受的性能阈值。所有这些问题都需要在模型构建步骤中加以注意。需要注意的是,如果我们没有在模型上获得可接受的性能,就需要回到之前的步骤,获取更多数据或创建额外的特征,然后再次重复模型构建步骤,以检查模型性能是否有所改善。这可能需要多次重复,直到模型达到所需的性能水平。

在模型构建步骤结束时,我们可能会得到一系列模型,每个模型都有其在未见测试数据上的性能度量。表现最好的模型可以被选中用于生产。

模型部署

下一步是将最终模型保存下来,以便将来使用。有几种方法可以将模型保存为对象。一旦保存,模型可以在任何时候重新加载,并用于对新数据的评分。将模型保存为对象是一个简单的任务,Python 和 R 中都有许多库可以实现这一点。由于保存了模型,模型对象会持久化到磁盘上,形成.sav文件、.pkl文件或.pmml对象,具体取决于所使用的库。然后可以将对象加载到内存中,以对未见数据进行评分。

最终选定的用于生产的模型可以部署到以下两种模式中进行未见数据的评分:

  • 批量模式:批量模式评分是指将需要评分的未见数据累积到一个文件中,然后在预定时间运行一个批量作业(这只是另一个可执行脚本)来进行评分。作业将模型对象从磁盘加载到内存中,并对需要评分的文件中的每条记录运行。输出将按照批量作业脚本中的指示写入到指定位置的其他文件中。需要注意的是,要评分的记录应该具有与训练数据相同的列数,列的类型也应该符合训练数据。应确保因素列(名义类型数据)的级别数与训练数据相匹配。

  • 实时模式:有时业务需要模型评分即时发生。在这种情况下,与批量模式不同,数据不会累积,我们也不会等待批量作业运行进行评分。预期是,当数据记录可用于评分时,应该由模型进行评分。评分的结果应该几乎瞬间对业务用户可用。在这种情况下,需要将模型部署为一个可以处理任何请求的 Web 服务。要评分的记录可以通过简单的 API 调用传递给 Web 服务,该调用反过来返回可以由下游应用程序消费的评分结果。再次强调,通过 API 调用传递的未评分数据记录应遵守训练数据记录的格式。

另一种实现近似实时结果的方法是每天多次以非常频繁的间隔运行模型作业在微批次数据上。数据在间隔期间累积,直到模型作业启动。模型作业对累积的数据进行评分并输出结果,类似于批量模式。业务用户可以在微批次作业执行完成后立即看到评分结果。与微批次处理与批量处理相比的唯一区别是,与批量模式不同,业务用户不需要等到下一个工作日才能获得评分结果。

尽管模型构建管道以成功部署 ML 模型并使其可用于评分而结束,但在现实世界的业务场景中,工作并没有在这里结束。当然,成功者会涌入,但需要在某个时间点(可能在部署后的几个月)再次审视模型。如果一个模型没有定期维护,则不太可能被业务使用得很好。

为了避免模型过时且不被业务用户使用,重要的是在一段时间内收集关于模型性能的反馈,并捕捉是否需要将任何改进纳入模型。未见数据没有标签,因此将模型输出与业务期望的输出进行比较是一项手动操作。在这种情况下,与业务用户合作是获取反馈的强烈要求。

如果对模型有持续的业务需求,并且如果现有模型在评分的未见数据上的性能未达到标准,则需要调查以确定根本原因。可能发生的情况是,与模型最初训练的数据相比,在一段时间内评分的数据中发生了许多变化。在这种情况下,强烈需要重新校准模型,并且从头开始再次开始是一个非常好的主意!

现在本书已经涵盖了机器学习的所有基本要素和项目流程,接下来要讨论的主题是学习范式,这将帮助我们学习几个机器学习算法。

学习范式

在其他关于机器学习(ML)的书籍或内容中,大多数遵循的学习范式采用自下而上的方法。这种方法从底层开始,逐步向上推进。首先覆盖理论元素,例如算法的数学介绍、算法的演变、变体以及算法所采用的参数,然后深入到针对特定数据集的机器学习算法的应用。这可能是一个好的方法;然而,真正看到算法产生的结果需要更长的时间。学习者需要极大的耐心和毅力,等待算法的实际应用被涵盖。在大多数情况下,从事机器学习的工作者和某些行业专业人士对实际方面非常感兴趣,他们希望体验算法的力量。对于这些人来说,重点不是算法的理论基础,而是实际应用。在这种情况下,自下而上的方法适得其反。

本书在教授几种机器学习算法时遵循的学习范式与自下而上的方法相反。它更倾向于遵循一个非常实用的自上而下的方法。这种方法的重点是通过编码学习

书的每一章都将专注于学习特定类别的机器学习算法。首先,章节将关注如何在各种情况下使用算法,以及如何在实践中从算法中获得结果。一旦使用代码和数据集演示了算法的实际应用,章节的其余部分将逐渐揭示到目前为止章节中体验到的算法的理论细节/概念。所有理论细节都将确保只涵盖理解代码和在任何新的数据集上应用算法所必需的详细程度。这确保了我们能够学习算法的专注应用领域,而不是在机器学习世界中不那么重要的、不希望出现的理论方面。

数据集

书中的每一章都描述了一个机器学习项目,该项目使用机器学习算法或一组算法来解决商业问题,这些算法是我们试图在特定章节中学习的。考虑的项目来自不同的领域,从医疗保健到银行和金融,再到机器人。在接下来的章节中解决的商业问题被精心挑选,以展示解决接近现实世界的商业用例。用于问题的数据集是流行的公开数据集。这将帮助我们不仅探索本书中涵盖的解决方案,还可以检查为该问题开发的其它解决方案。每一章中解决的问题通过在各个领域应用机器学习算法来丰富我们的经验,并帮助我们理解如何成功解决各个领域的商业问题。

摘要

哇!到目前为止,我们已经一起学到了很多,现在我们已经到达了这一章的结尾。在这一章中,我们涵盖了所有与机器学习(ML)相关的内容,包括术语和项目流程。我们还讨论了学习范式、数据集以及每一章将要涉及的所有主题和项目。

在下一章中,我们将开始着手处理机器学习集成,以预测员工流失。

第二章:使用集成模型预测员工流失

如果你回顾了最近的机器学习竞赛,我相信你一定会注意到的一个关键观察结果是,大多数竞赛中所有三个获胜者的方案都包括非常好的特征工程,以及调优良好的集成模型。从这个观察结果中,我得出的一个结论是,好的特征工程和构建表现良好的模型是两个应该给予同等重视的领域,以便提供成功的机器学习解决方案。

虽然特征工程大多数时候是依赖于构建模型的人的创造力和领域专业知识,但构建一个表现良好的模型可以通过一种称为集成学习的哲学来实现。机器学习从业者经常使用集成技术来超越甚至最佳性能的个体机器学习算法产生的性能基准。在本章中,我们将学习这个激动人心的机器学习领域的以下主题:

  • 集成学习的哲学

  • 理解员工流失问题和数据集

  • 使用 K 最近邻模型进行性能基准测试

  • Bagging

  • 随机森林中的随机化

  • Boosting

  • Stacking

集成学习的哲学

集成学习,在机器学习从业者中非常著名,可以通过一个简单的现实世界、非机器学习示例来很好地理解。

假设你已经申请了一家非常有声望的企业的职位,并且你被邀请参加面试。仅凭一次与面试官的面试,你不太可能被选中工作。在大多数情况下,你将经历多轮面试,与几个面试官或面试官小组进行面试。组织对面试官的期望是,每位面试官都是特定领域的专家,并且面试官已经根据你在面试官领域专业知识中的经验评估了你的工作适应性。当然,你被选中工作取决于所有与你交谈的面试官的综合反馈。组织认为,由于你的选择是基于多个专家做出的综合决策,而不是仅基于一个专家的决策,这可能会存在某些偏见,因此你将更有可能在工作中取得成功。

现在,当我们谈论所有面试官反馈的整合时,整合可以通过几种方法发生:

  • 平均:假设你的工作候选人资格是基于你在面试中通过一个截止分数。假设你已经见过十个面试官,每个面试官都对你进行了最高 10 分的评分,这代表面试官在他领域专业知识中对你经验的感知。现在,你的综合评分是通过简单地平均所有面试官给你的分数来计算的。

  • 多数投票:在这种情况下,每位面试官并没有给出 10 分中的实际分数。然而,在 10 位面试官中,有 8 位确认您适合该职位。两位面试官表示不认可您的候选人资格。由于大多数面试官对您的面试表现感到满意,您被选中担任该职位。

  • 加权平均:假设有四位面试官在您申请的职位中具备一些有益的次要技能,这些技能对于该职位并非必需。您接受了所有 10 位面试官的面试,每位面试官都给您打出了 10 分中的分数。与平均方法类似,在加权平均方法中,您的最终面试分数是通过平均所有面试官给出的分数来获得的。

然而,并非所有分数在计算最终分数时都同等重要。每个面试分数都会乘以一个权重,得到一个乘积。所有这些乘积相加,从而得到最终分数。每个面试的权重是测试候选人技能的重要性以及该技能对完成工作的重要性函数。显然,对于工作来说,“有益的”技能与“必需的”技能相比,权重较低。最终分数现在本质上代表了候选人拥有的必需技能的比例,这对您的选择有更大的影响。

与面试的类比类似,机器学习中的集成也基于综合学习产生模型。术语“综合学习”本质上代表通过应用多个机器学习算法或从属于大型数据集的多个数据子集中获得的学习。类似于面试,通过应用集成技术,从多个模型中学习多个模型。然而,通过应用平均、多数投票或加权平均技术对每个单独模型做出的预测进行综合,从而得出关于预测的最终结论。应用集成技术和预测综合技术的模型通常被称为集成

每个机器学习算法都是特殊的,并且有自己独特的方式来建模底层的训练数据。例如,k-最近邻算法通过计算数据集中元素之间的距离来学习;朴素贝叶斯通过计算数据中每个属性属于特定类的概率来学习。可以使用不同的机器学习算法创建多个模型,并通过结合几个机器学习算法的预测来进行预测。同样,当数据集被划分为子集,并且使用专注于每个数据集的算法训练多个模型时,每个模型都非常专注,并且专门学习它所训练的数据子集的特性。在这两种情况下,通过整合多个算法和多个数据子集的模型,当我们通过结合多个模型的优势来综合预测时,我们会得到更好的预测。否则,使用单个模型进行预测是无法获得这种效果的。

集成学习的核心在于,当我们结合多个模型的预测而不是仅仅依赖单个模型进行预测时,我们可以获得更好的预测。这与“团结就是力量”的管理哲学没有不同,这通常被称为协同效应

现在我们已经理解了集成学习的核心哲学,我们现在可以探索不同的集成技术类型了。然而,我们将通过在一个项目中实现它们来学习集成技术,该项目旨在预测员工的流失。正如我们已知的,在构建任何机器学习项目之前,对问题和数据有深入的理解非常重要。因此,在下一节中,我们首先关注理解当前面临的员工流失问题,然后研究与该问题相关联的数据集,最后通过探索性数据分析(EDA)理解数据集的特性。本节中我们获得的关键见解来自于一次性的练习,并将适用于我们在后续章节中应用的所有集成技术。

开始学习

要开始本节的学习,您需要从本章代码的 GitHub 链接下载WA_Fn-UseC_-HR-Employee-Attrition.csv数据集。

理解员工流失问题和数据集

人力资源分析有助于解释组织数据。它发现数据中与人员相关的发展趋势,并帮助人力资源部门采取适当的步骤,使组织运行顺畅并盈利。在企业环境中,员工流失是管理人员和人力资源人员必须应对的复杂挑战之一。有趣的是,可以部署机器学习模型来预测潜在的员工流失案例,从而帮助适当的人力资源人员或管理人员采取必要的步骤来留住员工。

在本章中,我们将构建机器学习集成,以预测潜在的离职案例。用于项目的职位离职数据集是由 IBM 的数据科学家创建的虚构数据集。rsample库包含了这个数据集,我们可以直接从库中使用这个数据集。

这是一个包含 31 个属性、1,470 条记录的小数据集。可以通过以下代码获取数据集的描述:

setwd("~/Desktop/chapter 2") 
library(rsample) 
data(attrition) 
str(attrition) 
mydata<-attrition 

这将产生以下输出:

'data.frame':1470 obs. of  31 variables: 
 $ Age                     : int  41 49 37 33 27 32 59 30 38 36 ... 
 $ Attrition               : Factor w/ 2 levels "No","Yes": 2 1 2 1 1 1 1 1 1 1 .... 
 $ BusinessTravel          : Factor w/ 3 levels "Non-Travel","Travel_Frequently",..: 3 2 3 2 3 2 3 3 2 3 ... 
 $ DailyRate               : int  1102 279 1373 1392 591 1005 1324 1358 216 1299 ... 
 $ Department              : Factor w/ 3 levels "Human_Resources",..: 3 2 2 2 2 2 2 2 2 2 ... 
 $ DistanceFromHome        : int  1 8 2 3 2 2 3 24 23 27 ... 
 $ Education               : Ord.factor w/ 5 levels "Below_College"<..: 2 1 2 4 1 2 3 1 3 3 ... 
 $ EducationField          : Factor w/ 6 levels "Human_Resources",..: 2 2 5 2 4 2 4 2 2 4 ... 
 $ EnvironmentSatisfaction : Ord.factor w/ 4 levels "Low"<"Medium"<..: 2 3 4 4 1 4 3 4 4 3 ... 
 $ Gender                  : Factor w/ 2 levels "Female","Male": 1 2 2 1 2 2 1 2 2 2 ... 
 $ HourlyRate              : int  94 61 92 56 40 79 81 67 44 94 ... 
 $ JobInvolvement          : Ord.factor w/ 4 levels "Low"<"Medium"<..: 3 2 2 3 3 3 4 3 2 3 ... 
 $ JobLevel                : int  2 2 1 1 1 1 1 1 3 2 ... 
 $ JobRole                 : Factor w/ 9 levels "Healthcare_Representative",..: 8 7 3 7 3 3 3 3 5 1 ... 
 $ JobSatisfaction         : Ord.factor w/ 4 levels "Low"<"Medium"<..: 4 2 3 3 2 4 1 3 3 3 ... 
 $ MaritalStatus           : Factor w/ 3 levels "Divorced","Married",..: 3 2 3 2 2 3 2 1 3 2 ... 
 $ MonthlyIncome           : int  5993 5130 2090 2909 3468 3068 2670 2693 9526 5237 ... 
 $ MonthlyRate             : int  19479 24907 2396 23159 16632 11864 9964 13335 8787 16577 ... 
 $ NumCompaniesWorked      : int  8 1 6 1 9 0 4 1 0 6 ... 
 $ OverTime                : Factor w/ 2 levels "No","Yes": 2 1 2 2 1 1 2 1 1 1 ... 
 $ PercentSalaryHike       : int  11 23 15 11 12 13 20 22 21 13 ... 
 $ PerformanceRating       : Ord.factor w/ 4 levels "Low"<"Good"<"Excellent"<..: 3 4 3 3 3 3 4 4 4 3 ... 
 $ RelationshipSatisfaction: Ord.factor w/ 4 levels "Low"<"Medium"<..: 1 4 2 3 4 3 1 2 2 2 ... 
 $ StockOptionLevel        : int  0 1 0 0 1 0 3 1 0 2 ... 
 $ TotalWorkingYears       : int  8 10 7 8 6 8 12 1 10 17 ... 
 $ TrainingTimesLastYear   : int  0 3 3 3 3 2 3 2 2 3 ... 
 $ WorkLifeBalance         : Ord.factor w/ 4 levels "Bad"<"Good"<"Better"<..: 1 3 3 3 3 2 2 3 3 2 ... 
 $ YearsAtCompany          : int  6 10 0 8 2 7 1 1 9 7 ... 
 $ YearsInCurrentRole      : int  4 7 0 7 2 7 0 0 7 7 ... 
 $ YearsSinceLastPromotion : int  0 1 0 3 2 3 0 0 1 7 ... 
 $ YearsWithCurrManager    : int  5 7 0 0 2 6 0 0 8 7 ... 

要查看数据集中的Attrition目标变量,请运行以下代码:

table(mydata$Attrition) 

这将产生以下输出:

 No   Yes  
1233  237  

在数据集中的 1,470 个观测值中,我们有 1,233 个样本(83.87%)是非离职案例,237 个离职案例(16.12%)。显然,我们正在处理一个类别不平衡的数据集。

我们现在将通过corrplot库使用以下代码可视化数据中的高度相关变量:

# considering only the numeric variables in the dataset 
numeric_mydata <- mydata[,c(1,4,6,7,10,11,13,14,15,17,19,20,21,24,25,26,28:35)] 
# converting the target variable "yes" or "no" values into numeric 
# it defaults to 1 and 2 however converting it into 0 and 1 to be consistent 
numeric_Attrition = as.numeric(mydata$Attrition)- 1 
# create a new data frame with numeric columns and numeric target  
numeric_mydata = cbind(numeric_mydata, numeric_Attrition) 
# loading the required library 
library(corrplot) 
# creating correlation plot 
M <- cor(numeric_mydata) 
corrplot(M, method="circle") 

这将产生以下输出:

在前面的屏幕截图中,可能观察到单元格中的较深和较大的蓝色点表示单元格中对应行和列的变量之间存在强相关性。独立变量之间的高度相关性表明数据中存在冗余特征。数据中高度相关特征的存在问题被称为多重共线性。如果我们想要拟合一个回归模型,那么我们需要通过一些技术来处理数据中的高度相关变量,例如删除冗余特征或应用主成分分析或偏最小二乘回归,这些技术直观地减少了冗余特征。

从输出中我们可以推断出以下变量高度相关,如果我们要构建一个基于回归的模型,构建模型的人需要通过一些技术来处理这些变量,例如删除冗余特征或应用主成分分析或偏最小二乘回归,这些技术直观地减少了冗余特征。

JobLevel-MonthlyIncomeJobLevel-TotalWorkingYearsMonthlyIncome-TotalWorkingYearsPercentSalaryHike-PerformanceRatingYearsAtCompany-YearsInCurrentRoleYearsAtCompany-YearsWithCurrManagerYearsWithCurrManager-YearsInCurrentRole

现在,让我们绘制各种独立变量与依赖的Attrition变量之间的关系图,以了解独立变量对目标的影响:

### Overtime vs Attiriton 
l <- ggplot(mydata, aes(OverTime,fill = Attrition)) 
l <- l + geom_histogram(stat="count") 

tapply(as.numeric(mydata$Attrition) - 1 ,mydata$OverTime,mean) 

No Yes
0.104364326375712 0.305288461538462

让我们运行以下命令以获取图形视图:

print(l) 

前面的命令生成了以下输出:

在前面的输出中,可以观察到加班的员工与未加班的员工相比,更容易出现离职现象:

让我们通过执行以下命令来计算员工的离职率:

### MaritalStatus vs Attiriton 
l <- ggplot(mydata, aes(MaritalStatus,fill = Attrition)) 
l <- l + geom_histogram(stat="count") 

tapply(as.numeric(mydata$Attrition) - 1 ,mydata$MaritalStatus,mean) 
Divorced 0.100917431192661 
Married 0.12481426448737 
Single 0.25531914893617 

让我们运行以下命令以获取图形视图:

print(l) 

前面的命令生成了以下输出:

在前面的输出中,可以观察到单身员工有更高的流失率:

###JobRole vs Attrition 
l <- ggplot(mydata, aes(JobRole,fill = Attrition)) 
l <- l + geom_histogram(stat="count") 

tapply(as.numeric(mydata$Attrition) - 1 ,mydata$JobRole,mean) 

Healthcare Representative    Human Resources 
               0.06870229    0.23076923 
    Laboratory Technician    Manager 
               0.23938224    0.04901961 
   Manufacturing Director    Research Director 
               0.06896552    0.02500000 
       Research Scientist    Sales Executive 
               0.16095890    0.17484663 
     Sales Representative 
               0.39759036 
mean(as.numeric(mydata$Attrition) - 1) 
[1] 0.161224489795918 

执行以下命令以获取相同情况的图形表示:

print(l)

看看运行前面命令生成的以下输出:

图片

在前面的输出中,可以观察到实验室技术人员、销售代表和从事人力资源工作的员工比其他组织角色的员工流失率更高。

让我们执行以下命令来检查员工性别对流失率的影响:

###Gender vs Attrition 
l <- ggplot(mydata, aes(Gender,fill = Attrition)) 
l <- l + geom_histogram(stat="count") 

tapply(as.numeric(mydata$Attrition) - 1 ,mydata$Gender,mean) 

Female 0.147959183673469 
Male 0.170068027210884 

运行以下命令以获取相同情况的图形表示:

print(l)

这将产生以下输出:

图片

在前面的输出中,你可以看到员工的性别对流失率没有影响,换句话说,流失率在所有性别中观察到是相同的。

让我们通过执行以下命令来计算来自不同领域的员工的属性:

###EducationField vs Attrition el <- ggplot(mydata, aes(EducationField,fill = Attrition)) 
l <- l + geom_histogram(stat="count") 

tapply(as.numeric(mydata$Attrition) - 1 ,mydata$EducationField,mean) 

Human Resources    Life Sciences    Marketing 
       0.2592593    0.1468647        0.2201258 
         Medical   Other Technical  Degree 
       0.1357759    0.1341463        0.2424242

让我们执行以下命令以获取图形表示:

print(l)

这将产生以下输出:

图片

观察前面的图表,我们可以得出结论,拥有技术学位或人力资源学位的员工观察到有更高的流失率。看看下面的代码:

###Department vs Attrition 
l <- ggplot(mydata, aes(Department,fill = Attrition)) 
l <- l + geom_histogram(stat="count") 

tapply(as.numeric(mydata$Attrition) - 1 ,mydata$Department,mean) 
Human Resources  Research & Development  Sales 
   0.1904762       0.1383975              0.2062780 

让我们执行以下命令来检查不同部门的流失率:

print(l) 

这将产生以下输出:

图片

观察前面的图表,我们可以得出结论,研发部门与销售和人力资源部门的流失率相比较低。看看下面的代码:

###BusinessTravel vs Attrition 
l <- ggplot(mydata, aes(BusinessTravel,fill = Attrition)) 
l <- l + geom_histogram(stat="count") 

tapply(as.numeric(mydata$Attrition) - 1 ,mydata$BusinessTravel,mean) 
 Non-Travel   Travel_Frequently   Travel_Rarely 
  0.0800000    0.2490975           0.1495686

执行以下命令以获取相同情况的图形表示:

print(l) 

这将产生以下输出:

图片

观察前面的图表,我们可以得出结论,经常出差的员工与没有出差状态或很少出差的员工相比,更容易出现流失。

让我们通过执行以下命令来计算员工的加班时间:

### x=Overtime, y= Age, z = MaritalStatus , t = Attrition 
ggplot(mydata, aes(OverTime, Age)) +   
  facet_grid(.~MaritalStatus) + 
  geom_jitter(aes(color = Attrition),alpha = 0.4) +   
  ggtitle("x=Overtime, y= Age, z = MaritalStatus , t = Attrition") +   
  theme_light() 

这将产生以下输出:

图片

观察前面的图表,我们可以得出结论,年轻(年龄 < 35)且单身但加班的员工更容易出现流失:

### MonthlyIncome vs. Age, by  color = Attrition 
ggplot(mydata, aes(MonthlyIncome, Age, color = Attrition)) +  
  geom_jitter() + 
  ggtitle("MonthlyIncome vs. Age, by  color = Attrition ") + 
  theme_light() 

这将产生以下输出:

图片

观察前面的图表,我们可以得出结论,年轻(年龄 < 30)的员工流失率较高,并且观察到大多数流失率发生在收入低于 7500 美元的员工中。

虽然我们已经了解了关于当前数据的许多重要细节,但实际上还有更多值得探索和学习的内容。然而,为了进入下一步,我们在这一 EDA 步骤处停止。需要注意的是,在现实世界中,数据可能不会像我们在这一磨损数据集中看到的那样非常干净。例如,数据中可能会有缺失值;在这种情况下,我们会进行缺失值插补。幸运的是,我们有一个完美的数据集,可以用来创建模型,而无需进行任何数据清洗或额外的预处理。

用于性能基准的 K-最近邻模型

在本节中,我们将实现 k-最近邻KNN)算法,并在我们的 IBM 离职数据集上构建模型。当然,我们已经从 EDA 中了解到,我们手头的数据集中存在类别不平衡问题。然而,我们现在不会对数据集进行类别不平衡处理,因为这是一个独立的整个领域,并且该领域有几种技术可用,因此超出了本章中涵盖的机器学习集成主题的范围。我们将暂时将数据集视为现状,并构建机器学习模型。此外,对于类别不平衡数据集,Kappa 或精确率和召回率或接收者操作特征(ROC)曲线下的面积(AUROC)是合适的指标。然而,为了简化,我们将使用 准确率 作为性能指标。我们将采用 10 折交叉验证重复 10 次来评估模型性能。现在,让我们使用 KNN 算法构建我们的离职预测模型,如下所示:

# Load the necessary libraries 
# doMC is a library that enables R to use multiple cores available on the sysem thereby supporting multiprocessing.  
library(doMC) 
# registerDoMC command instructs R to use the specified number of cores to execute the code. In this case, we ask R to use 4 cores available on the system 
registerDoMC(cores=4) 
# caret library has the ml algorithms and other routines such as cross validation etc.  
library(caret) 
# Setting the working directory where the dataset is located 
setwd("~/Desktop/chapter 2") 
# Reading the csv file into R variable called mydata 
mydata <- read.csv("WA_Fn-UseC_-HR-Employee-Attrition.csv") 
#Removing the non-discriminatory features (as identified during EDA) from the dataset  
mydata$EmployeeNumber=mydata$Over18=mydata$EmployeeCount=mydata$StandardHours = NULL 
# setting the seed prior to model building ensures reproducibility of the results obtained 
set.seed(10000) 
# setting the train control parameters specifying gold standard 10 fold cross validation  repeated 10 times 
fitControl = trainControl(method="repeatedcv", number=10,repeats=10) 
###creating a model on the data. Observe that we specified Attrition as the target and that model should learn from rest of the variables. We specified mydata as the dataset to learn. We pass the train control parameters and specify that knn algorithm need to be used to build the model. K can be of any length - we specified 20 as parameter which means the train command will search through 20 different random k values and finally retains the model that produces the best performance measurements. The final model is stored as caretmodel 
caretmodel = train(Attrition~., data=mydata, trControl=fitControl, method = "knn", tuneLength = 20) 
# We output the model object to the console  
caretmodel 

这将产生以下输出:

k-Nearest Neighbors  
1470 samples 
  30 predictors 
   2 classes: 'No', 'Yes'  
No pre-processing 
Resampling: Cross-Validated (10 fold, repeated 10 times)  
Summary of sample sizes: 1323, 1323, 1324, 1323, 1324, 1322, ...  
Resampling results across tuning parameters: 
  k   Accuracy   Kappa        
   5  0.8216447  0.0902934591 
   7  0.8349033  0.0929511324 
   9  0.8374198  0.0752842114 
  11  0.8410920  0.0687849122 
  13  0.8406861  0.0459679081 
  15  0.8406875  0.0337742424 
  17  0.8400748  0.0315670261 
  19  0.8402770  0.0245499585 
  21  0.8398721  0.0143638854 
  23  0.8393945  0.0084393721 
  25  0.8391891  0.0063246624 
  27  0.8389174  0.0013913143 
  29  0.8388503  0.0007113939 
  31  0.8387818  0.0000000000 
  33  0.8387818  0.0000000000 
  35  0.8387818  0.0000000000 
  37  0.8387818  0.0000000000 
  39  0.8387818  0.0000000000 
  41  0.8387818  0.0000000000 
  43  0.8387818  0.0000000000 
Accuracy was used to select the optimal model using the largest value. 
The final value used for the model was k = 11\. 

从模型输出中我们可以看到,当 k = 11 时,表现最好的模型,我们使用这个 k 值获得了 84% 的准确率。在本章的其余部分,当我们实验几种集成技术时,我们将检查这个 KNN 获得的 84% 准确率是否会被击败。

在一个实际的项目构建情况下,仅仅确定最佳超参数是不够的。模型需要在包含最佳超参数的完整数据集上训练,并且模型需要保存以供将来使用。我们将在本节的其余部分回顾这些步骤。

在这种情况下,caretmodel 对象已经包含了训练好的模型,其中 k = 11,因此我们不会尝试使用最佳超参数重新训练模型。要检查最终模型,可以使用以下代码查询模型对象:

caretmodel$finalModel 

这将产生以下输出:

11-nearest neighbor model 
Training set outcome distribution: 
  No  Yes  
1233  237  

下一步是将您最好的模型保存到文件中,以便我们稍后可以加载它们并预测未见数据。可以使用 saveRDS R 命令将模型保存到本地目录:

 # save the model to disk 
saveRDS(caretmodel, "production_model.rds") 

在这种情况下,caretmodel 模型被保存在工作目录中的 production_model.rds 文件中。现在该模型以文件形式序列化,可以随时加载,并可用于评估未见数据。加载和评估可以通过以下 R 代码实现:

# Set the working directory to the directory where the saved .rds file is located  
setwd("~/Desktop/chapter 2") 
#Load the model  
loaded_model <- readRDS("production_model.rds") 
#Using the loaded model to make predictions on unseen data 
final_predictions <- predict(loaded_model, unseen_data) 

请注意,unseen_data需要在通过predict命令评分之前读取。

代码中的一部分,其中最终模型在全部数据集上训练,保存模型,在需要时从文件中重新加载它,并集体评分未看到的资料,被称为构建机器学习生产化管道。这个管道对所有机器学习模型都是相同的,无论模型是使用单个算法还是使用集成技术构建。因此,在后面的章节中,当我们实现各种集成技术时,我们不会涵盖生产化管道,而只是停止在通过 10 折交叉验证重复 10 次获得性能度量。

Bagging

Bootstrap aggregation 或Bagging是机器学习实践社区最早广泛采用的集成技术。Bagging 涉及从单个数据集中创建多个不同的模型。为了理解 Bagging,了解一个重要的统计技术——bootstrap,是很有必要的。

Bootstrapping 涉及创建数据集的多个随机子集。有可能同一个数据样本被多个子集选中,这被称为带有替换的 bootstrap。这种方法的优点是,它减少了由于使用整个数据集而导致的估计量的标准误差。这个技术可以通过一个例子来更好地解释。

假设你有一个包含 1,000 个样本的小数据集。根据样本,你需要计算代表该样本的总体平均。现在,直接做这件事的方法是以下公式:

图片

由于这是一个小样本,我们在估计总体平均时可能会出现误差。通过采用带有替换的 bootstrap 采样可以减少这种误差。在这种技术中,我们创建了 10 个子集,每个数据集包含 100 个项目。一个数据项可以在子集中随机表示多次,并且对数据子集中以及跨子集的数据项表示的次数没有限制。现在,我们取每个数据子集中样本的平均值,因此,我们最终得到 10 个不同的平均值。使用所有这些收集到的平均值,我们使用以下公式估计总体的平均:

图片

现在,我们有了更好的平均估计,因为我们已经将小样本外推以随机生成多个样本,这些样本代表了原始总体。

在袋装中,实际训练数据集通过带有替换的 bootstrap 抽样分成多个袋。假设我们最终得到 n 个袋,当机器学习算法应用于这些袋中的每一个时,我们获得 n 个不同的模型。每个模型都专注于一个袋。当需要对新未见数据做出预测时,这些 n 个模型中的每一个都会独立地对数据进行预测。通过结合所有 n 个模型的预测,得出一个观察值的最终预测。在分类的情况下,采用投票,并将多数视为最终预测。对于回归,考虑所有模型的预测平均值作为最终预测。

基于决策树的算法,如分类和回归树CART),是不稳定的学习者。原因是训练数据集的微小变化会严重影响创建的模型。模型变化本质上意味着预测也会变化。袋装是一种非常有效的技术,可以处理对数据变化的极高敏感性。因为我们可以在数据集的子集上构建多个决策树模型,然后根据每个模型的预测得出最终预测,因此数据变化的影响被消除或不太明显。

在数据子集上构建多个模型时,会遇到一个直观的问题,那就是过拟合。然而,通过在不应用任何剪枝的情况下生长深度树,可以克服这个问题。

使用袋装法的一个缺点是,与使用独立机器学习算法构建模型相比,构建模型所需的时间更长。这是显而易见的,因为在袋装法中会构建多个模型,而不是一个单独的模型,构建这些多个模型需要时间。

现在,让我们编写 R 代码来实现袋装集成,并比较获得的性能与 KNN 算法获得的性能。然后,我们将探讨袋装方法的运作机制。

caret 库提供了一个框架,可以与任何独立的机器学习算法实现袋装。ldaBagplsBagnbBagtreeBagctreeBagsvmBagnnetBagcaret 中提供的一些示例方法。在本节中,我们将使用三种不同的 caret 方法实现袋装,例如 treebagsvmbagnbbag

袋装分类和回归树(treeBag)实现

首先,加载必要的库并注册用于并行处理的核心数:

library(doMC) 
registerDoMC(cores = 4)  
library(caret) 
#setting the random seed for replication 
set.seed(1234) 
# setting the working directory where the data is located 
setwd("~/Desktop/chapter 2") 
# reading the data 
mydata <- read.csv("WA_Fn-UseC_-HR-Employee-Attrition.csv") 
#removing the non-discriminatory features identified during EDA 
mydata$EmployeeNumber=mydata$Over18=mydata$EmployeeCount=mydata$StandardHours = NULL 
#setting up cross-validation 
cvcontrol <- trainControl(method="repeatedcv", repeats=10, number = 10, allowParallel=TRUE) 
# model creation with treebag , observe that the number of bags is set as 10 
train.bagg <- train(Attrition ~ ., data=mydata, method="treebag",B=10, trControl=cvcontrol, importance=TRUE) 
train.bagg 

这将产生以下输出:

Bagged CART  
1470 samples 
  30 predictors 
   2 classes: 'No', 'Yes'  
No pre-processing 
Resampling: Cross-Validated (10 fold, repeated 10 times)  
Summary of sample sizes: 1324, 1323, 1323, 1322, 1323, 1322, ...  
Resampling results: 
  Accuracy  Kappa     
  0.854478  0.2971994 

我们可以看到,我们实现了 85.4%的更好准确率,而使用 KNN 算法获得的准确率是 84%。

支持向量机袋装(SVMBag)实现

在 SVMBag 和 NBBag 实现中,加载库、注册多进程、设置工作目录、从工作目录读取数据、从数据中移除非判别性特征以及设置交叉验证参数的步骤保持不变。因此,我们不在 SVMBag 或 NBBag 代码中重复这些步骤。相反,我们将专注于讨论 SVMBag 或 NBBag 特定的代码:

# Setting up SVM predict function as the default svmBag$pred function has some code issue 
svm.predict <- function (object, x) 
{ 
 if (is.character(lev(object))) { 
    out <- predict(object, as.matrix(x), type = "probabilities") 
    colnames(out) <- lev(object) 
    rownames(out) <- NULL 
  } 
  else out <- predict(object, as.matrix(x))[, 1] 
  out 
} 
# setting up parameters to build svm bagging model 
bagctrl <- bagControl(fit = svmBag$fit, 
                      predict = svm.predict , 
                      aggregate = svmBag$aggregate) 
# fit the bagged svm model 
set.seed(300) 
svmbag <- train(Attrition ~ ., data = mydata, method="bag",trControl = cvcontrol, bagControl = bagctrl,allowParallel = TRUE) 
# printing the model results 
svmbag 

这将导致以下输出:

Bagged Model  

1470 samples 
  30 predictors 
   2 classes: 'No', 'Yes'  

No pre-processing 
Resampling: Cross-Validated (10 fold, repeated 10 times)  
Summary of sample sizes: 1324, 1324, 1323, 1323, 1323, 1323, ...  
Resampling results: 
  Accuracy   Kappa     
  0.8777721  0.4749657 

Tuning parameter 'vars' was held constant at a value of 44 

你将看到我们达到了 87.7%的准确率,这比 KNN 模型的 84%准确率要高得多。

简单贝叶斯(nbBag)袋装化实现

我们现在将通过执行以下代码来实现nbBag实现:

# setting up parameters to build svm bagging model 
bagctrl <- bagControl(fit = nbBag$fit, 
                      predict = nbBag$pred , 
                      aggregate = nbBag$aggregate) 
# fit the bagged nb model 
set.seed(300) 
nbbag <- train(Attrition ~ ., data = mydata, method="bag", trControl = cvcontrol, bagControl = bagctrl) 
# printing the model results 
nbbag 

这将导致以下输出:

Bagged Model  

1470 samples 
  30 predictors 
   2 classes: 'No', 'Yes'  

No pre-processing 
Resampling: Cross-Validated (10 fold, repeated 10 times)  
Summary of sample sizes: 1324, 1324, 1323, 1323, 1323, 1323, ...  
Resampling results: 

  Accuracy   Kappa      
  0.8389878  0.00206872 

Tuning parameter 'vars' was held constant at a value of 44 

我们可以看到,在这种情况下,我们只达到了 83.89%的准确率,略低于 KNN 模型的 84%性能。

尽管我们只展示了caret方法中用于袋装化的三个示例,但代码在实现其他方法时保持不变。在代码中需要做的唯一更改是在bagControl中替换fitpredictaggregate参数。例如,要实现使用神经网络算法的袋装化,我们需要将bagControl定义为以下内容:

bagControl(fit = nnetBag$fit, predict = nnetBag$pred , aggregate = nnetBag$aggregate) 

可能需要注意,R 中需要有一个适当的库来运行caret方法,否则将导致错误。例如,nbBag需要在执行代码之前在系统上安装klaR库。同样,ctreebag函数需要安装party包。用户在使用caret袋装化之前需要检查系统上是否存在适当的库。

我们现在已经了解了通过袋装化技术实施项目的实现方法。下一小节将介绍袋装化的底层工作机制。这将有助于我们了解袋装化在内部如何处理我们的数据集,以便产生比独立模型性能更好的性能测量结果。

随机森林中的随机化

正如我们在袋装化中看到的,我们创建了多个袋装,每个模型都在这些袋装上进行训练。每个袋装都由实际数据集的子集组成,然而每个袋装中的特征或变量数量保持不变。换句话说,我们在袋装化中所做的是对数据集行进行子集化。

在随机森林中,当我们通过子集化行从数据集中创建袋装时,我们还子集化了需要包含在每个袋装中的特征(列)。

假设你的数据集中有 1,000 个观测值和 20 个特征。我们可以创建 20 个袋装,其中每个袋装有 100 个观测值(这是由于有放回的重新抽样而成为可能),并且每个袋装有五个特征。现在,训练了 20 个模型,每个模型只能看到分配给它的袋装。最终的预测是通过投票或基于问题是否为回归问题或分类问题进行平均得出的。

与袋装相比,随机森林的另一个关键区别是用于构建模型的机器学习算法。在袋装中,可以使用任何机器学习算法来创建模型,但是随机森林模型是专门使用 CART 构建的。

随机森林建模是另一种非常流行的机器学习算法。它是那些多次证明自己是最优算法之一的算法之一,尽管它在噪声数据集上应用。对于一个已经理解了自助法的个人来说,理解随机森林就像小菜一碟。

使用随机森林实现流失预测模型

让我们通过执行以下代码,通过随机森林建模来获取我们的流失模型:

# loading required libraries and registering multiple cores to enable parallel processing 
library(doMC) 
library(caret) 
registerDoMC(cores=4) 
# setting the working directory and reading the dataset 
setwd("~/Desktop/chapter 2") 
mydata <- read.csv("WA_Fn-UseC_-HR-Employee-Attrition.csv") 
# removing the non-discriminatory features from the dataset as identified during EDA step 
mydata$EmployeeNumber=mydata$Over18=mydata$EmployeeCount=mydata$StandardHours = NULL 
# setting the seed for reproducibility 
set.seed(10000) 
# setting the cross validation parameters 
fitControl = trainControl(method="repeatedcv", number=10,repeats=10) 
# creating the caret model with random forest algorithm 
caretmodel = train(Attrition~., data=mydata, method="rf", trControl=fitControl, verbose=F) 
# printing the model summary 
caretmodel 

这将导致以下输出:

Random Forest  

1470 samples 
  30 predictors 
   2 classes: 'No', 'Yes'  

No pre-processing 
Resampling: Cross-Validated (10 fold, repeated 10 times)  
Summary of sample sizes: 1323, 1323, 1324, 1323, 1324, 1322, ...  
Resampling results across tuning parameters: 

  mtry  Accuracy   Kappa     
   2    0.8485765  0.1014859 
  23    0.8608271  0.2876406 
  44    0.8572929  0.2923997 

Accuracy was used to select the optimal model using the largest value. 
The final value used for the model was mtry = 23\. 

我们看到,最佳的随机森林模型实现了 86%的更好准确率,而 KNN 的准确率是 84%。

提升

弱学习器是一种表现相对较差的算法——通常,弱学习器获得的准确率仅略高于随机水平。通常,如果不是总是观察到,弱学习器在计算上很简单。决策树桩或 1R 算法是弱学习器的例子。提升将弱学习器转换为强学习器。这本质上意味着提升不是一个进行预测的算法,而是与一个底层的弱机器学习算法一起工作以获得更好的性能。

提升模型是一系列在数据子集上学习的模型,这些子集与袋装集成技术类似。不同之处在于数据子集的创建。与袋装不同,用于模型训练的所有数据子集并不是在训练开始之前就创建好的。相反,提升通过一个机器学习算法构建第一个模型,该算法在整个数据集上进行预测。现在,有一些被错误分类的实例,这些实例是子集,并被第二个模型使用。第二个模型只从第一个模型输出的错误分类数据集中学习。

第二个模型的错误分类实例成为第三个模型的输入。构建模型的过程会重复进行,直到满足停止标准。对未见数据集中的观察值的最终预测是通过平均或投票所有模型对该特定、未见观察值的预测来得到的。

在提升算法家族中,各种算法之间存在着细微的差别,然而我们不会详细讨论它们,因为本章的目的是获得对机器学习集成的一般理解,而不是深入了解各种提升算法。

在获得更好的性能的同时,度量是提升集成最大的优势;模型可解释性困难、更高的计算时间、模型过拟合是使用提升时遇到的一些问题。当然,这些问题可以通过使用专门的技术来解决。

提升算法无疑是超级流行的,并且观察到在许多 Kaggle 和类似比赛中获胜者都在使用。有几种提升算法可供选择,例如梯度提升机GBMs)、自适应提升AdaBoost)、梯度树提升、极端梯度提升XGBoost)和轻梯度提升机LightGBM)。在本节中,我们将学习两种最受欢迎的提升算法的理论和实现,即 GBMs 和 XGBoost。在学习提升的理论概念及其优缺点之前,让我们首先开始关注使用 GBMs 和 XGBoost 实现员工流失预测模型。

GBM 实现

让我们实现使用 GBM 的员工流失预测模型:

# loading the essential libraries and registering the cores for multiprocessing 
library(doMC) 
library(mlbench) 
library(gbm) 
library(caret) 
registerDoMC(cores=4) 
# setting the working directory and reading the dataset 
setwd("~/Desktop/chapter 2") 
mydata <- read.csv("WA_Fn-UseC_-HR-Employee-Attrition.csv") 
# removing the non-discriminatory features as identified by EDA step 
mydata$EmployeeNumber=mydata$Over18=mydata$EmployeeCount=mydata$StandardHours = NULL 
# converting the target attrition feild to numeric as gbm model expects all numeric feilds in the dataset 
mydata$Attrition = as.numeric(mydata$Attrition) 
# forcing the attrition column values to be 0 and 1 instead of 1 and 2 
mydata = transform(mydata, Attrition=Attrition-1) 
# running the gbm model with 10 fold cross validation to identify the number of trees to build - hyper parameter tuning 
gbm.model = gbm(Attrition~., data=mydata, shrinkage=0.01, distribution = 'bernoulli', cv.folds=10, n.trees=3000, verbose=F) 
# identifying and printing the value of hyper parameter identified through the tuning above 
best.iter = gbm.perf(gbm.model, method="cv") 
print(best.iter) 
# setting the seed for reproducibility 
set.seed(123) 
# creating a copy of the dataset 
mydata1=mydata 
# converting target to a factor 
mydata1$Attrition=as.factor(mydata1$Attrition) 
# setting up cross validation controls 
fitControl = trainControl(method="repeatedcv", number=10,repeats=10) 
# runing the gbm model in tandem with caret  
caretmodel = train(Attrition~., data=mydata1, method="gbm", distribution="bernoulli",  trControl=fitControl, verbose=F, tuneGrid=data.frame(.n.trees=best.iter, .shrinkage=0.01, .interaction.depth=1, .n.minobsinnode=1)) 
# printing the model summary 
print(caretmodel) 

这将产生以下输出:

2623 
Stochastic Gradient Boosting  

1470 samples 
  30 predictors 
   2 classes: '0', '1'  

No pre-processing 
Resampling: Cross-Validated (10 fold, repeated 10 times)  
Summary of sample sizes: 1323, 1323, 1323, 1322, 1323, 1323, ...  
Resampling results: 
  Accuracy   Kappa     
  0.8771472  0.4094991 
Tuning parameter 'n.trees' was held constant at a value of 2623 
Tuning parameter 'shrinkage' was held constant at a value of 0.01 
Tuning parameter 'n.minobsinnode' was held constant at a value of 1 

你会看到,使用 GBM 模型,我们实现了超过 87%的准确率,这比使用 KNN 实现的 84%的准确率要好。

使用 XGBoost 构建员工流失预测模型

现在,让我们使用 XGBoost 实现员工流失预测模型:

# loading the required libraries and registering the cores for multiprocessing 
library(doMC) 
library(xgboost) 
library(caret) 
registerDoMC(cores=4) 
# setting the working directory and loading the dataset 
setwd("~/Desktop/chapter 2") 
mydata <- read.csv("WA_Fn-UseC_-HR-Employee-Attrition.csv") 
# removing the non-discriminatory features from the dataset as identified in EDA step 
mydata$EmployeeNumber=mydata$Over18=mydata$EmployeeCount=mydata$StandardHours = NULL 
# setting up cross validation parameters 
ControlParamteres <- trainControl(method = "repeatedcv",number = 10, repeats=10, savePredictions = TRUE, classProbs = TRUE) 
# setting up hyper parameters grid to tune   
parametersGrid <-  expand.grid(eta = 0.1, colsample_bytree=c(0.5,0.7), max_depth=c(3,6),nrounds=100, gamma=1, min_child_weight=2,subsample=0.5) 
# printing the parameters grid to get an intuition 
print(parametersGrid) 
# xgboost model building 
modelxgboost <- train(Attrition~., data = mydata, method = "xgbTree", trControl = ControlParamteres, tuneGrid=parametersGrid) 
# printing the model summary 
print(modelxgboost) 

这将产生以下输出:

图片

eXtreme Gradient Boosting  
1470 samples 
  30 predictors 
   2 classes: 'No', 'Yes'  

No pre-processing 
Resampling: Cross-Validated (10 fold, repeated 10 times)  
Summary of sample sizes: 1323, 1323, 1322, 1323, 1323, 1322, ...  
Resampling results across tuning parameters: 

  max_depth  colsample_bytree  Accuracy   Kappa     
  3          0.5               0.8737458  0.3802840 
  3          0.7               0.8734728  0.3845053 
  6          0.5               0.8730674  0.3840938 
  6          0.7               0.8732589  0.3920721 

Tuning parameter 'nrounds' was held constant at a value of 100 
Tuning parameter 'min_child_weight' was held constant at a value of 2 
Tuning parameter 'subsample' was held constant at a value of 0.5 
Accuracy was used to select the optimal model using the largest value. 
The final values used for the model were nrounds = 100, max_depth = 3, eta = 0.1, gamma = 1, colsample_bytree = 0.5, min_child_weight = 2 and subsample = 0.5\. 

再次,我们观察到,使用 XGBoost 模型,我们实现了超过 87%的准确率,这比使用 KNN 实现的 84%的准确率要好。

堆叠

在我们迄今为止所学习的所有集成中,我们都以某种方式操纵了数据集,并暴露了数据集的子集以进行模型构建。然而,在堆叠中,我们不会对数据集做任何事情;相反,我们将应用一种不同的技术,该技术涉及使用多个机器学习算法。在堆叠中,我们使用各种机器学习算法构建多个模型。每个算法都有一种独特的学习数据特征的方式,最终的堆叠模型间接地结合了所有这些独特的学习方式。堆叠通过通过投票或平均(正如我们在其他类型的集成中所做的那样)获得最终预测,从而获得了几个机器学习算法的联合力量。

使用堆叠构建员工流失预测模型

让我们构建一个使用堆叠的员工流失预测模型:

# loading the required libraries and registering the cpu cores for multiprocessing 
library(doMC) 
library(caret) 
library(caretEnsemble) 
registerDoMC(cores=4) 
# setting the working directory and loading the dataset 
setwd("~/Desktop/chapter 2") 
mydata <- read.csv("WA_Fn-UseC_-HR-Employee-Attrition.csv") 
# removing the non-discriminatory features from the dataset as identified in EDA step 
mydata$EmployeeNumber=mydata$Over18=mydata$EmployeeCount=mydata$StandardHours = NULL 
# setting up control paramaters for cross validation 
control <- trainControl(method="repeatedcv", number=10, repeats=10, savePredictions=TRUE, classProbs=TRUE) 
# declaring the ML algorithms to use in stacking 
algorithmList <- c('C5.0', 'nb', 'glm', 'knn', 'svmRadial') 
# setting the seed to ensure reproducibility of the results 
set.seed(10000) 
# creating the stacking model 
models <- caretList(Attrition~., data=mydata, trControl=control, methodList=algorithmList) 
# obtaining the stacking model results and printing them 
results <- resamples(models) 
summary(results) 

这将产生以下输出:

summary.resamples(object = results) 

Models: C5.0, nb, glm, knn, svmRadial  
Number of resamples: 100  

Accuracy  
               Min.   1st Qu.    Median      Mean   3rd Qu.      Max. NA's 
C5.0      0.8082192 0.8493151 0.8639456 0.8625833 0.8775510 0.9054054    0 
nb        0.8367347 0.8367347 0.8378378 0.8387821 0.8424658 0.8435374    0 
glm       0.8299320 0.8639456 0.8775510 0.8790444 0.8911565 0.9387755    0 
knn       0.8027211 0.8299320 0.8367347 0.8370763 0.8438017 0.8630137    0 
svmRadial 0.8287671 0.8648649 0.8775510 0.8790467 0.8911565 0.9319728    0 

Kappa  Min.          1st Qu.     Median     Mean   3rd Qu.      Max.  NA's 
C5.0   0.03992485 0.29828006 0.37227344 0.3678459 0.4495049 0.6112590    0 
nb     0.00000000 0.00000000 0.00000000 0.0000000 0.0000000 0.0000000    0 
glm    0.26690604 0.39925723 0.47859218 0.4673756 0.5218094 0.7455280    0 
knn   -0.05965697 0.02599388 0.06782465 0.0756081 0.1320451 0.2431312    0 
svmRadial 0.24565 0.38667527 0.44195662 0.4497538 0.5192393 0.7423764    0 

# Identifying the correlation between results 
modelCor(results) 

这将产生以下输出:

图片

从相关性表的结果中我们可以看出,没有任何一个单独的机器学习算法的预测结果高度相关。高度相关的结果意味着算法产生了非常相似的预测。与接受单个预测相比,结合这些非常相似的预测可能并不会真正带来显著的好处。在这个特定的情况下,我们可以观察到没有任何算法的预测是高度相关的,因此我们可以直接进入下一步,即堆叠预测:

# Setting up the cross validation control parameters for stacking the predictions from individual ML algorithms 
stackControl <- trainControl(method="repeatedcv", number=10, repeats=10, savePredictions=TRUE, classProbs=TRUE) 
# stacking the predictions of individual ML algorithms using generalized linear model 
stack.glm <- caretStack(models, method="glm", trControl=stackControl) 
# printing the stacked final results 
print(stack.glm) 

这将产生以下输出:

A glm ensemble of 2 base models: C5.0, nb, glm, knn, svmRadial 
Ensemble results: 
Generalized Linear Model  
14700 samples 
    5 predictors 
    2 classes: 'No', 'Yes'  
No pre-processing 
Resampling: Cross-Validated (10 fold, repeated 10 times)  
Summary of sample sizes: 13230, 13230, 13230, 13230, 13230, 13230, ...  
Resampling results: 
  Accuracy   Kappa     
  0.8844966  0.4869556 

使用基于 GLM 的 stacking,我们达到了 88%的准确率。现在,让我们来检查使用随机森林建模而不是 GLM 来堆叠来自五个机器学习算法的每个算法的预测对观察结果的影响:

# stacking the predictions of individual ML algorithms using random forest 
stack.rf <- caretStack(models, method="rf", trControl=stackControl) 
# printing the summary of rf based stacking 
print(stack.rf) 

这将产生以下输出:

A rf ensemble of 2 base models: C5.0, nb, glm, knn, svmRadial 
Ensemble results: 
Random Forest  
14700 samples 
    5 predictors 
    2 classes: 'No', 'Yes'  
No pre-processing 
Resampling: Cross-Validated (10 fold, repeated 10 times)  
Summary of sample sizes: 13230, 13230, 13230, 13230, 13230, 13230, ...  
Resampling results across tuning parameters: 
  mtry  Accuracy   Kappa     
  2     0.9122041  0.6268108 
  3     0.9133605  0.6334885 
  5     0.9132925  0.6342740 
Accuracy was used to select the optimal model using the largest value.
The final value used for the model was mtry = 3\. 

我们看到,只需付出很少的努力,我们就能够通过堆叠预测实现 91%的准确率。现在,让我们来探讨堆叠的工作原理。

最后,我们发现了各种集成技术,这些技术可以为我们提供性能更好的模型。然而,在结束本章之前,还有一些事情我们需要注意。

在 R 中实现机器学习模型的方式不止一种。例如,可以使用ipred库中的函数来实现 bagging,而不是像本章中所做的那样使用caret。我们应该意识到,超参数调整是模型构建的重要组成部分,以便获得最佳性能的模型。超参数的数量以及这些超参数的可接受值取决于我们打算使用的库。这就是为什么我们在本章构建的模型中对超参数调整的关注较少。尽管如此,阅读库文档以了解可以使用库函数调整的超参数非常重要。在大多数情况下,将超参数调整纳入模型可以显著提高模型性能。

摘要

回顾一下,我们使用的是不平衡的数据集来构建流失模型。在模型构建之前使用技术来解决类别不平衡是获得更好的模型性能测量值的关键方面之一。我们使用了 bagging、随机化、boosting 和 stacking 来实现和预测流失模型。我们仅通过使用模型中现成的特征就实现了 91%的准确率。特征工程是一个关键方面,其作用在机器学习模型中不容忽视。这可能又是探索进一步提高模型性能的另一条途径。

在下一章中,我们将探讨通过构建个性化推荐引擎来推荐产品或内容的秘密配方。我已经准备好实施一个推荐笑话的项目。翻到下一章继续学习之旅。

第三章:实现笑话推荐引擎

我相信这也是你经历过的事情:在亚马逊上购买手机时,你也会看到一些手机配件的推荐,如屏幕保护膜和手机壳。不出所料,我们大多数人最终都会购买这些推荐中的一个或多个!电子商务网站上的推荐引擎的主要目的是吸引买家从供应商那里购买更多商品。当然,这与销售人员试图在实体店向客户推销或交叉销售并无不同。

你可能记得在亚马逊(或任何电子商务网站)上看到的“购买此商品的用户还购买了此商品”的标题,其中显示了推荐。这些推荐的目的是让你不仅仅购买一个产品,而是购买一个产品组合,从而推动销售额向上增长。亚马逊的推荐非常成功,麦肯锡估计,亚马逊总销售额中有高达 35%是由于他们的推荐!

在本章中,我们将学习关于推荐引擎的理论和实现,以向用户推荐笑话。为此,我们使用 R 的recommenderlab库中可用的 Jester 笑话数据集。我们将涵盖以下主要主题:

  • 推荐引擎的基本方面

  • 理解笑话推荐问题和数据集

  • 使用基于物品的协同过滤技术的推荐系统

  • 使用基于用户的协同过滤技术的推荐系统

  • 使用关联规则挖掘技术的推荐系统

  • 基于内容的推荐引擎

  • 混合笑话推荐系统

推荐引擎的基本方面

虽然显示推荐的基本意图是推动销售,但它们实际上服务的概念远不止于此。高度个性化的内容是推荐引擎能够提供的东西。这本质上意味着零售平台(如亚马逊)上的推荐引擎能够通过正确的渠道在正确的时间向正确的客户提供正确的内容。提供个性化内容是有意义的;毕竟,向客户展示无关的产品是没有意义的。此外,随着客户注意力集中时间的缩短,企业希望通过展示正确的产品并鼓励他们购买正确的产品来最大化他们的销售机会。在人工智能中,个性化内容推荐可以通过以下几种方式实现:

  • 映射一起购买的产品:让我们以一个在购物网站上搜索书包的在线购物者为例。购物者很可能在购买书包时还会购买其他与学校相关的物品。因此,将书包与笔记本、铅笔、钢笔和笔盒一起展示,可以确保更高的额外销售概率。

  • 基于客户人口统计的推荐:向保守的中产阶级客户推荐高端手机和时尚的手机配件作为推荐产品,他们通常寻找划算的交易,可能不会大幅提升推荐产品的销售额。相反,这样的客户可能会觉得这些不相关的推荐令人烦恼,从而影响他们的忠诚度。

  • 基于客户之间相似性的推荐:向客户推荐的产品是基于其他类似客户购买或喜欢的产品。例如,向居住在城市的年轻女性推荐新到的化妆品产品。在这种情况下,推荐不仅仅是因为客户的属性,还因为类似类型的其他客户购买了这款产品。随着该产品在类似个体中的流行,该产品被选为推荐的产品。

  • 基于产品相似性的推荐:如果您搜索特定品牌的笔记本电脑背包,除了搜索结果外,还会显示其他品牌笔记本电脑背包的推荐。这种推荐完全基于产品之间的相似性。

  • 基于客户历史购买档案的推荐:如果客户一直购买某个品牌的牛仔裤,他们会看到该品牌他们倾向于购买的牛仔裤的新品种的推荐。这些推荐完全基于客户的历史购买。

  • 混合推荐:可能可以将一个或多个推荐方法结合起来,为顾客提供最佳的推荐。例如,可以通过使用从历史数据中推断出的客户偏好以及客户的人口统计信息来生成推荐列表。

在线零售行业中,推荐系统的一些应用包括回购活动、新闻通讯推荐、重新绑定被遗弃购物车的销售额、定制折扣和优惠,以及电子商务网站的流畅浏览体验。

由于存在几个常见的用例,可能会给人一种印象,即推荐系统仅在电子商务行业中使用。然而,事实并非如此。以下是一些推荐系统在非电子商务领域的用例:

  • 在制药行业中,推荐系统被应用于识别具有特定特征的药物,这些药物对患者的治疗效果会更好

  • 股票推荐是基于一组成功人士的股票选择进行的

  • YouTube 和在线媒体使用推荐引擎为用户当前观看的内容提供相似的内容

  • 旅游推荐是基于用户或类似用户访问过的旅游景点

  • 识别未来员工在不同角色中的技能和个性特征

  • 在烹饪科学中,可以通过应用推荐系统来探索可以搭配的菜肴。

由于推荐系统的用例几乎存在于每个领域,因此这个列表可以增长到巨大的规模。

现在我们对推荐系统的概念及其对商业的价值有了基本的了解,我们可以进入下一部分,尝试理解 Jester's Jokes 推荐数据集以及构建推荐引擎可能解决的问题。

推荐引擎类别

在实施我们的第一个推荐系统之前,让我们详细探讨推荐系统的类型。以下图表显示了推荐系统的广泛类别:

图片

推荐系统类别

图表中展示的每种技术都可以用来构建一个能够向用户推荐笑话的推荐系统模型。让我们简要地探讨各种推荐引擎类别。

基于内容的过滤

认知过滤,或基于内容的过滤,通过比较产品属性和客户档案属性来推荐物品。每个产品的属性表示为一组标签或术语——通常是出现在产品描述文档中的单词。客户档案用相同的术语表示,并通过分析客户查看或评分过的产品内容来构建。

协同过滤

社会过滤,或称为协同过滤,通过使用其他人的推荐来过滤信息。协同过滤背后的原理是,过去欣赏过相同物品的客户在未来也有很高的可能性表现出相似的兴趣。

我们通常在观看电影之前向朋友寻求评论和推荐。朋友的推荐比其他人的推荐更容易被接受,因为我们与朋友有共同兴趣。这就是协同过滤工作的相同原理。

协同过滤可以进一步分为基于记忆和基于模型两种,如下所示:

  • 基于记忆:在此方法中,使用用户评分信息来计算用户或物品之间的相似度。然后使用这种计算出的相似度来提出推荐。

  • 基于模型:数据挖掘方法被应用于识别数据中的模式,然后使用学习到的模式来生成推荐。

混合过滤

在这类推荐系统中,我们结合了多种类型的推荐系统来得出最终的推荐。

开始

要开始,您需要从 GitHub 链接下载支持文件。

理解笑话推荐问题和数据集

肯·戈德堡博士及其同事,特蕾莎·罗德、德鲁夫·古普塔和克里斯·珀金斯,通过他们的论文《Eigentaste:一种常数时间协同过滤算法》向世界介绍了一个数据集,这篇论文在推荐系统领域相当受欢迎。这个数据集被称为 Jester 笑话数据集。为了创建它,向许多用户展示了几个笑话,并要求他们进行评分。用户提供的各种笑话的评分构成了数据集。该数据集的数据收集于 1999 年 4 月至 2003 年 5 月之间。以下是该数据集的属性:

  • 来自 79,681 个用户的 150 个笑话的超过 11,000,000 个评分

  • 每一行代表一个用户(行 1 = 用户 #1)

  • 每一列代表一个笑话(列 1 = 笑话 #1)

  • 评分以从 -10.00 到 +10.00 的实数值给出;-10 是最低可能的评分,10 是最高评分

  • 99 代表空评分

R 中的 recommenderlab 包提供了由肯·戈德堡博士小组提供的原始数据集的一个子集。我们将利用这个子集来完成本章中涉及的项目。

recommenderlab 库中提供的 Jester5k 数据集包含一个 5,000 x 100 的评分矩阵(5,000 个用户和 100 个笑话),评分介于 -10.00 到 +10.00 之间。所有选定的用户都评了 36 个或更多的笑话。该数据集以 realRatingMatrix 格式存在。这是 recommenderlab 预期数据应采用的特殊矩阵格式,以便应用库中打包的各种函数。

如我们所知,探索性数据分析EDA)是任何数据科学项目的第一步。根据这一原则,让我们首先读取数据,然后对数据集进行 EDA 步骤:

# including the required libraries
library(data.table)
library(recommenderlab)
# setting the seed so as to reproduce the results
set.seed(54)
# reading the data to a variable
library(recommenderlab)
data(Jester5k)
str(Jester5k)

这将产生以下输出:

Formal class 'realRatingMatrix' [package "recommenderlab"] with 2 slots
  ..@ data     :Formal class 'dgCMatrix' [package "Matrix"] with 6 slots
  .. .. ..@ i       : int [1:362106] 0 1 2 3 4 5 6 7 8 9 ...
  .. .. ..@ p       : int [1:101] 0 3314 6962 10300 13442 18440 22513 27512 32512 35685 ...
  .. .. ..@ Dim     : int [1:2] 5000 100
  .. .. ..@ Dimnames:List of 2
  .. .. .. ..$ : chr [1:5000] "u2841" "u15547" "u15221" "u15573" ...
  .. .. .. ..$ : chr [1:100] "j1" "j2" "j3" "j4" ...
  .. .. ..@ x       : num [1:362106] 7.91 -3.2 -1.7 -7.38 0.1 0.83 2.91 -2.77 -3.35 -1.99 ...
  .. .. ..@ factors : list()
  ..@ normalize: NULL

数据结构输出相当直观,我们看到它为我们已经讨论过的细节提供了经验证据。让我们继续我们的 EDA:

# Viewing the first 5 records in the dataset
head(getRatingMatrix(Jester5k),5)

这将产生以下输出:

2.5 x 100 sparse Matrix of class "dgCMatrix"
   [[ suppressing 100 column names ‘j1’, ‘j2’, ‘j3’ ... ]]                                                                                                           
u2841   7.91  9.17  5.34  8.16 -8.74  7.14  8.88 -8.25  5.87  6.21  7.72  6.12 -0.73  7.77 -5.83 -8.88  8.98
u15547 -3.20 -3.50 -9.56 -8.74 -6.36 -3.30  0.78  2.18 -8.40 -8.79 -7.04 -6.02  3.35 -4.61  3.64 -6.41 -4.13
u15221 -1.70  1.21  1.55  2.77  5.58  3.06  2.72 -4.66  4.51 -3.06  2.33  3.93  0.05  2.38 -3.64 -7.72  0.97
u15573 -7.38 -8.93 -3.88 -7.23 -4.90  4.13  2.57  3.83  4.37  3.16 -4.90 -5.78 -5.83  2.52 -5.24  4.51  4.37
u21505  0.10  4.17  4.90  1.55  5.53  1.50 -3.79  1.94  3.59  4.81 -0.68 -0.97 -6.46 -0.34 -2.14 -2.04 -2.57                                
u2841  -9.32 -9.08 -9.13 7.77  8.59  5.29  8.25  6.02  5.24  7.82  7.96 -8.88  8.25  3.64 -0.73  8.25  5.34 -7.77
u15547 -0.15 -1.84 -1.84 1.84 -1.21 -8.59 -5.19 -2.18  0.19  2.57 -5.78  1.07 -8.79  3.01  2.67 -9.22 -9.32  3.69
u15221  2.04  1.94  4.42 1.17  0.10 -5.10 -3.25  3.35  3.30 -1.70  3.16 -0.29  1.36  3.54  6.17 -2.72  3.11  4.81
u15573  4.95  5.49 -0.49 3.40 -2.14  5.29 -3.11 -4.56 -5.44 -6.89 -0.24 -5.15 -3.59 -8.20  2.18  0.39 -1.21 -2.62
u21505 -0.15  2.43  3.16 1.50  4.37 -0.10 -2.14  3.98  2.38  6.84 -0.68  0.87  3.30  6.21  5.78 -6.21 -0.78 -1.36
## number of ratings
print(nratings(Jester5k))

这将产生以下输出:

362106## number of ratings per user

我们将使用以下命令打印数据集的摘要:

print(summary(rowCounts(Jester5k)))

这将产生以下输出:

   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
  36.00   53.00   72.00   72.42  100.00  100.00

现在,我们将绘制直方图:

## rating distribution
hist(getRatings(Jester5k), main="Distribution of ratings")

这将产生以下输出:

图片

从输出中,我们可以看到一种相对正常的分布。我们还可以看到,正面评分的数量超过了负面评分的数量。

The Jester5K dataset also provides a character vector called JesterJokes. The vector is of length 100. These are the actual 100 jokes among others that were shown to the users to get the ratings. We could examine the jokes with the following command:

head(JesterJokes,5)

这将产生以下输出:

j1 "A man visits the doctor. The doctor says \"I have bad news for you.You have cancer and Alzheimer's disease\". The man replies \"Well,thank God I don't have cancer!\""
j2 "This couple had an excellent relationship going until one day he came home from work to find his girlfriend packing. He asked her why she was leaving him and she told him that she had heard awful things about him. \"What could they possibly have said to make you move out?\" \"They told me that you were a pedophile.\" He replied, \"That's an awfully big word for a ten year old.\""
j3  "Q. What's 200 feet long and has 4 teeth? A. The front row at a Willie Nelson Concert."
j4 "Q. What's the difference between a man and a toilet? A. A toilet doesn't follow you around after you use it."
j5 "Q. What's O. J. Simpson's Internet address? A. Slash, slash, backslash, slash, slash, escape."

基于我们拥有的 5,000 个用户评分,我们可以进行额外的 EDA 来识别用户评分最高的笑话。这可以通过以下代码完成:

## 'best' joke with highest average rating
best <- which.max(colMeans(Jester5k))
cat(JesterJokes[best])

这将产生以下输出:

A guy goes into confession and says to the priest, "Father, I'm 80 years old, widower, with 11 grandchildren. Last night I met two beautiful flight attendants. They took me home and I made love to both of them. Twice." The priest said: "Well, my son, when was the last time you were in confession?" "Never Father, I'm Jewish." "So then, why are you telling me?" "I'm telling everybody."

我们可以执行额外的 EDA(探索性数据分析)来可视化单变量和多变量分析。这次探索将帮助我们详细了解每个变量以及它们之间的关系。虽然我们不会深入探讨这些方面的每一个,但以下是一些可以探索的想法:

  • 探索总是对大多数笑话给出高评分的用户

  • 对笑话提供的评分之间的相关性

  • 识别非常挑剔的用户

  • 探索最受欢迎或最不受欢迎的笑话

  • 识别评分最少的笑话以及它们之间的关联

转换 DataFrame

我们将在本章中使用名为 recommenderlab 的 R 库中的函数来构建推荐引擎项目。无论我们实现哪种推荐系统类别,数据集都需要满足一些先决条件,以便能够应用 recommenderlab 函数。用于协同过滤的预构建 recommenderlab 函数期望输入 realRatingMatrix。在我们的案例中,Jester5k 数据集已经处于这种格式,因此,我们可以直接使用这个矩阵来应用 recommenderlab 函数。

如果我们的数据以 R DataFrame 的形式存在,并且我们打算将其转换为 realRatingMatrix,则可以执行以下步骤:

  1. 将 DataFrame 转换为 R 矩阵,如下所示:
# convert the df dataframe to a matrix
r_mat <- as.matrix(df)
  1. 使用 as() 函数将结果矩阵转换为 realRatingMatrix,如下所示:
# convert r_mat matrix to a recommenderlab realRatingMatrix
r_real_mat <- as(r_mat,"realRatingMatrix")

在这里,我们假设 DataFrame 的名称为 df,代码将将其转换为 realRatingMatrix,这可以作为 recommenderlab 函数的输入。

划分 DataFrame

另一个先决条件是将数据集划分为训练集和测试集。这些子集将在后续部分用于实现我们的推荐系统并衡量性能。recommenderlab 库中的 evaluationScheme() 函数可以用来将数据集划分为训练集和测试集。可以向此函数传递多个用户指定的参数。在下面的代码中,realRatingMatrix 根据一个 80/20 的训练/测试分割进行分割,每个用户最多推荐 20 项。此外,我们指定任何大于 0 的评分应被视为正面评分,符合预定义的 [-10, 10] 评分范围。《Jester5k》数据集可以通过以下代码划分为训练集和测试集:

# split the data into the training and the test set
Jester5k_es <- evaluationScheme(Jester5k, method="split", train=0.8, given=20, goodRating=0)
# verifying if the train - test was done successfully
print(Jester5k_es)

这将产生以下输出:

Evaluation scheme with 20 items given
Method: ‘split’ with 1 run(s).
Training set proportion: 0.800
Good ratings: >=0.000000
Data set: 5000 x 100 rating matrix of class ‘realRatingMatrix’ with 362106 ratings.

evaluationScheme() 函数的输出中,我们可以观察到该函数生成了一个包含训练集和测试集的单一 R 对象。这个对象将被用来定义和评估各种推荐模型。

使用基于项目的协同过滤技术构建推荐系统

R 的recommenderlab包提供了基于物品的协同过滤ITCF)选项来构建推荐系统。这是一个非常直接的方法,只需要我们调用函数并提供必要的参数。通常,这些参数将对模型的表现产生很大影响;因此,测试每个参数组合是获得最佳推荐模型的关键。以下是可以传递给Recommender函数的参数:

  • 数据归一化:归一化评分矩阵是准备数据以供推荐引擎使用的关键步骤。归一化过程通过消除评分偏差来处理矩阵中的评分。此参数的可能值是NULLCenterZ-Score

  • 距离:这表示模型中要使用的相似度度量类型。此参数的可能值是余弦相似度、欧几里得距离和皮尔逊相关系数。

使用这些参数组合,我们可以构建和测试 3x3 的 ITCF 模型。ITCF 背后的基本直觉是,如果一个人喜欢物品 A,那么他们也很可能喜欢物品 B,只要 A 和 B 是相似的。这里的“相似”一词并不表示基于物品属性的相似性,而是指用户偏好的相似性,例如,喜欢物品 A 的一组人也喜欢物品 B。以下图显示了 ITCF 的工作原理:

示例显示基于物品的协同过滤的工作原理

示例显示基于物品的协同过滤的工作原理

让我们更详细地探索一下这个图。在 ITCF 中,西瓜和葡萄将形成相似物品的邻域,这意味着无论用户是谁,等效的不同物品都将形成一个邻域。所以当用户 X 喜欢西瓜时,来自同一邻域的其他物品,即葡萄,将由基于物品的协同过滤推荐系统推荐。

ITCF 涉及以下三个步骤:

  1. 通过距离度量计算基于物品的相似度:这涉及到计算物品之间的距离。距离可以通过多种距离度量之一来计算,例如余弦相似度、欧几里得距离、曼哈顿距离或 Jaccard 指数。这一步骤的输出是获得一个相似度矩阵,其中每个单元格对应于单元格行上指定的物品与单元格列上指定的物品之间的相似度。

  2. 预测特定用户的目标物品评分:评分是通过计算对与目标物品非常相似的物品所做的评分的加权总和得出的。

  3. 推荐前 N 个最佳物品:一旦所有物品都被预测出来,我们就推荐前N个物品。

现在,让我们构建每个 ITCF 模型,并使用测试数据集来衡量其性能。以下代码使用几个参数组合训练 ITCF 模型:

type = "IBCF"
##train ITCF cosine similarity models
# non-normalized
ITCF_N_C <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = NULL, method="Cosine"))
# centered
ITCF_C_C <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = "center",method="Cosine"))
# Z-score normalization
ITCF_Z_C <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = "Z-score",method="Cosine"))
##train ITCF Euclidean Distance models
# non-normalized
ITCF_N_E <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = NULL, method="Euclidean"))
# centered
ITCF_C_E <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = "center",method="Euclidean"))
# Z-score normalization
ITCF_Z_E <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = "Z-score",method="Euclidean"))
#train ITCF pearson correlation models
# non-normalized
ITCF_N_P <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = NULL, method="pearson"))
# centered
ITCF_C_P <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = "center",method="pearson"))
# Z-score normalization
ITCF_Z_P <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = "Z-score",method="pearson"))

我们现在有了 ITCF 模型,所以让我们计算我们创建的每个模型在测试数据上的性能。目标是确定适用于此数据集的最佳性能 ITCF 模型。以下代码在测试数据集上获取所有九个模型的性能测量结果:

# compute predicted ratings from each of the 9 models on the test dataset
pred1 <- predict(ITCF_N_C, getData(Jester5k_es, "known"), type="ratings")
pred2 <- predict(ITCF_C_C, getData(Jester5k_es, "known"), type="ratings")
pred3 <- predict(ITCF_Z_C, getData(Jester5k_es, "known"), type="ratings")
pred4 <- predict(ITCF_N_E, getData(Jester5k_es, "known"), type="ratings")
pred5 <- predict(ITCF_C_E, getData(Jester5k_es, "known"), type="ratings")
pred6 <- predict(ITCF_Z_E, getData(Jester5k_es, "known"), type="ratings")
pred7 <- predict(ITCF_N_P, getData(Jester5k_es, "known"), type="ratings")
pred8 <- predict(ITCF_C_P, getData(Jester5k_es, "known"), type="ratings")
pred9 <- predict(ITCF_Z_P, getData(Jester5k_es, "known"), type="ratings")
# set all predictions that fall outside the valid range to the boundary values
pred1@data@x[pred1@data@x[] < -10] <- -10
pred1@data@x[pred1@data@x[] > 10] <- 10
pred2@data@x[pred2@data@x[] < -10] <- -10
pred2@data@x[pred2@data@x[] > 10] <- 10
pred3@data@x[pred3@data@x[] < -10] <- -10
pred3@data@x[pred3@data@x[] > 10] <- 10
pred4@data@x[pred4@data@x[] < -10] <- -10
pred4@data@x[pred4@data@x[] > 10] <- 10
pred5@data@x[pred5@data@x[] < -10] <- -10
pred5@data@x[pred5@data@x[] > 10] <- 10
pred6@data@x[pred6@data@x[] < -10] <- -10
pred6@data@x[pred6@data@x[] > 10] <- 10
pred7@data@x[pred7@data@x[] < -10] <- -10
pred7@data@x[pred7@data@x[] > 10] <- 10
pred8@data@x[pred8@data@x[] < -10] <- -10
pred8@data@x[pred8@data@x[] > 10] <- 10
pred9@data@x[pred9@data@x[] < -10] <- -10
pred9@data@x[pred9@data@x[] > 10] <- 10
# aggregate the performance measurements obtained from all the models
error_ITCF <- rbind(
  ITCF_N_C = calcPredictionAccuracy(pred1, getData(Jester5k_es, "unknown")),
  ITCF_C_C = calcPredictionAccuracy(pred2, getData(Jester5k_es, "unknown")),
  ITCF_Z_C = calcPredictionAccuracy(pred3, getData(Jester5k_es, "unknown")),
  ITCF_N_E = calcPredictionAccuracy(pred4, getData(Jester5k_es, "unknown")),
  ITCF_C_E = calcPredictionAccuracy(pred5, getData(Jester5k_es, "unknown")),
  ITCF_Z_E = calcPredictionAccuracy(pred6, getData(Jester5k_es, "unknown")),
  ITCF_N_P = calcPredictionAccuracy(pred7, getData(Jester5k_es, "unknown")),
  ITCF_C_P = calcPredictionAccuracy(pred8, getData(Jester5k_es, "unknown")),
  ITCF_Z_P = calcPredictionAccuracy(pred9, getData(Jester5k_es, "unknown"))
)
library(knitr)
kable(error_ITCF)

这将产生以下输出:

|         |     RMSE|      MSE|      MAE|
|:--------|--------:|--------:|--------:|
|ITCF_N_C | 4.533455| 20.55221| 3.460860|
|ITCF_C_C | 5.082643| 25.83326| 4.012391|
|ITCF_Z_C | 5.089552| 25.90354| 4.021435|
|ITCF_N_E | 4.520893| 20.43848| 3.462490|
|ITCF_C_E | 4.519783| 20.42844| 3.462271|
|ITCF_Z_E | 4.527953| 20.50236| 3.472080|
|ITCF_N_P | 4.582121| 20.99583| 3.522113|
|ITCF_C_P | 4.545966| 20.66581| 3.510830|
|ITCF_Z_P | 4.569294| 20.87845| 3.536400|

我们看到,在具有欧几里得距离的数据上运行的 ITCF 推荐应用产生了最佳的性能测量结果。

基于用户协同过滤技术构建推荐系统

我们之前构建的基于项目的过滤器的笑话推荐系统,使用了 R 中可用的强大recommenderlab库。在这个基于用户的协同过滤UBCF)方法的实现中,我们使用了相同的库。

以下图表显示了 UBCF 的工作原理:

图片

展示用户基于协同过滤器工作原理的示例

为了更好地理解这个概念,让我们详细讨论前面的图表。假设有三个用户:X、Y 和 Z。在 UBCF 中,用户 X 和 Z 非常相似,因为他们都喜欢草莓和西瓜。用户 X 还喜欢葡萄和橙子。因此,基于用户的协同过滤器向用户 Z 推荐葡萄和橙子。这个想法是,相似的人倾向于喜欢相似的事物。

用户协同过滤器和项目协同过滤器之间的主要区别可以通过以下在线零售网站上常见的推荐标题来展示:

  • ITCF:购买此商品的用户还购买了

  • UBCF:与您相似的用户购买了

基于以下三个关键步骤构建用户协同过滤器:

  1. 使用相似性函数w测量用户x与每个用户对之间的距离,找到k 个最近邻KNN):

图片

  1. 预测用户x将对 KNN 已评分但x未评分的所有项目提供的评分。

  2. 向用户x推荐的N个项目是具有最佳预测评分的前N个项目。

简而言之,在 UBCF 过程中构建了用户-项目矩阵,并根据相似用户预测用户的未查看项目的评分。在预测中获得最高评分的项目构成了最终的推荐列表。

该项目的实现与 ITCF 非常相似,因为我们使用了相同的库。代码中唯一需要更改的是将 IBCF 方法更改为使用 UBCF。以下代码块是使用 UBCF 的项目实现的全代码:

library(recommenderlab)
data(Jester5k)
# split the data into the training and the test set
Jester5k_es <- evaluationScheme(Jester5k, method="split", train=0.8, given=20, goodRating=0)
print(Jester5k_es)
type = "UBCF"
#train UBCF cosine similarity models
# non-normalized
UBCF_N_C <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = NULL, method="Cosine"))
# centered
UBCF_C_C <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = "center",method="Cosine"))
# Z-score normalization
UBCF_Z_C <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = "Z-score",method="Cosine"))
#train UBCF Euclidean Distance models
# non-normalized
UBCF_N_E <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = NULL, method="Euclidean"))
# centered
UBCF_C_E <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = "center",method="Euclidean"))
# Z-score normalization
UBCF_Z_E <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = "Z-score",method="Euclidean"))
#train UBCF pearson correlation models
# non-normalized
UBCF_N_P <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = NULL, method="pearson"))
# centered
UBCF_C_P <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = "center",method="pearson"))
# Z-score normalization
UBCF_Z_P <- Recommender(getData(Jester5k_es, "train"), type,
                        param=list(normalize = "Z-score",method="pearson"))
# compute predicted ratings from each of the 9 models on the test dataset
pred1 <- predict(UBCF_N_C, getData(Jester5k_es, "known"), type="ratings")
pred2 <- predict(UBCF_C_C, getData(Jester5k_es, "known"), type="ratings")
pred3 <- predict(UBCF_Z_C, getData(Jester5k_es, "known"), type="ratings")
pred4 <- predict(UBCF_N_E, getData(Jester5k_es, "known"), type="ratings")
pred5 <- predict(UBCF_C_E, getData(Jester5k_es, "known"), type="ratings")
pred6 <- predict(UBCF_Z_E, getData(Jester5k_es, "known"), type="ratings")
pred7 <- predict(UBCF_N_P, getData(Jester5k_es, "known"), type="ratings")
pred8 <- predict(UBCF_C_P, getData(Jester5k_es, "known"), type="ratings")
pred9 <- predict(UBCF_Z_P, getData(Jester5k_es, "known"), type="ratings")
# set all predictions that fall outside the valid range to the boundary values
pred1@data@x[pred1@data@x[] < -10] <- -10
pred1@data@x[pred1@data@x[] > 10] <- 10
pred2@data@x[pred2@data@x[] < -10] <- -10
pred2@data@x[pred2@data@x[] > 10] <- 10
pred3@data@x[pred3@data@x[] < -10] <- -10
pred3@data@x[pred3@data@x[] > 10] <- 10
pred4@data@x[pred4@data@x[] < -10] <- -10
pred4@data@x[pred4@data@x[] > 10] <- 10
pred5@data@x[pred5@data@x[] < -10] <- -10
pred5@data@x[pred5@data@x[] > 10] <- 10
pred6@data@x[pred6@data@x[] < -10] <- -10
pred6@data@x[pred6@data@x[] > 10] <- 10
pred7@data@x[pred7@data@x[] < -10] <- -10
pred7@data@x[pred7@data@x[] > 10] <- 10
pred8@data@x[pred8@data@x[] < -10] <- -10
pred8@data@x[pred8@data@x[] > 10] <- 10
pred9@data@x[pred9@data@x[] < -10] <- -10
pred9@data@x[pred9@data@x[] > 10] <- 10
# aggregate the performance statistics
error_UBCF <- rbind(
  UBCF_N_C = calcPredictionAccuracy(pred1, getData(Jester5k_es, "unknown")),
  UBCF_C_C = calcPredictionAccuracy(pred2, getData(Jester5k_es, "unknown")),
  UBCF_Z_C = calcPredictionAccuracy(pred3, getData(Jester5k_es, "unknown")),
  UBCF_N_E = calcPredictionAccuracy(pred4, getData(Jester5k_es, "unknown")),
  UBCF_C_E = calcPredictionAccuracy(pred5, getData(Jester5k_es, "unknown")),
  UBCF_Z_E = calcPredictionAccuracy(pred6, getData(Jester5k_es, "unknown")),
  UBCF_N_P = calcPredictionAccuracy(pred7, getData(Jester5k_es, "unknown")),
  UBCF_C_P = calcPredictionAccuracy(pred8, getData(Jester5k_es, "unknown")),
  UBCF_Z_P = calcPredictionAccuracy(pred9, getData(Jester5k_es, "unknown"))
)
library(knitr)
print(kable(error_UBCF))

这将产生以下输出:

|         |     RMSE|      MSE|      MAE|
|:--------|--------:|--------:|--------:|
|UBCF_N_C | 4.877935| 23.79425| 3.986170|
|UBCF_C_C | 4.518210| 20.41422| 3.578551|
|UBCF_Z_C | 4.517669| 20.40933| 3.552120|
|UBCF_N_E | 4.644877| 21.57488| 3.778046|
|UBCF_C_E | 4.489157| 20.15253| 3.552543|
|UBCF_Z_E | 4.496185| 20.21568| 3.528534|
|UBCF_N_P | 4.927442| 24.27968| 4.074879|
|UBCF_C_P | 4.487073| 20.13382| 3.553429|
|UBCF_Z_P | 4.484986| 20.11510| 3.525356|

根据 UBCF 输出,我们观察到使用皮尔逊相关系数作为距离的 Z 分数归一化数据产生了最佳性能度量。此外,如果我们愿意,可以比较 UBCF 和 ITCF 的结果(需要在同一测试数据集上进行测试),以得出接受为最终推荐引擎部署构建的 18 个模型中最佳模型的结论。

在代码中需要注意的关键点是传递给method参数的UBCF值。在先前的项目中,我们构建了一个基于项目的协同过滤器;我们只需要将传递给method参数的值替换为 IBCF。

基于关联规则挖掘技术构建推荐系统

关联规则挖掘,或市场篮子分析,是一种在零售业中用于识别需要放在一起以鼓励交叉销售的产品的高度流行数据挖掘技术。这个算法的一个有趣方面是,通过挖掘历史发票来识别一起购买的产品。

有几种现成的算法可用于执行市场篮子分析。其中一些是 Apriori,等价类转换ECLAT),和频繁模式增长FP-growth)。我们将通过在 Jester 笑话数据集上应用 Apriori 算法来学习如何通过推荐笑话给用户来解决问题。我们现在将学习支撑 Apriori 算法的理论方面。

Apriori 算法

算法的构建块是任何给定事务中找到的项目。每个事务可能包含一个或多个项目。形成事务的项目称为项目集。一个事务的例子是发票。

给定事务数据集,目标是找到数据中相互关联的项目。关联性通过项目在相同上下文中的出现频率来衡量。例如,在购买另一个产品时购买一个产品代表一个关联规则。关联规则检测项目的共同使用情况。

更正式地说,我们可以将关联规则挖掘定义为,给定一个项目集 I = {I1, I2, .., Im}和事务数据库 D = {t1, t2, .., tn},其中 ti = {Ii1, Ii2, .., Iim},其中 Iik 是 I 的元素,一个关联是 X->Y 的蕴涵,其中 X 和 Y 是 I 的子集,且 X 和 Y 的交集是φ。简而言之,关联表达了从 X->Y 的蕴涵,其中 X 和 Y 是项目集。

通过一个例子可以更好地理解算法。所以,让我们考虑以下表格,它显示了超市中代表性样本事务的列表:

事务 项目
1 牛奶,酸奶,巧克力
2 面包,黄油
3 可乐,果酱
4 面包,牛奶,黄油,可乐
5 面包,牛奶,黄油,果酱

超市中的样本事务

让我们尝试探索一些基本概念,这将帮助我们理解 Apriori 算法是如何工作的:

  • :任何单个产品,它是每个交易的一部分。例如,牛奶、可乐和黄油都被称为项。

  • 项集:一个或多个项目的集合。例如,{butter, milk, coke}, {butter, milk}

  • 支持计数:项集出现的频率。例如,支持计数或 σ {butter, bread, milk} = 2

  • 支持:包含项集的交易的比例。例如,s = {butter, bread, milk} = 2/5

  • 频繁项集:支持度大于最小阈值的项集。

  • 在上下文中项集的支持度:同时包含 XY 的上下文的比例:

图片

因此,s 对于 {milk, butter} -> {bread} 将是 s = σ {milk, butter, bread}/N = 2/5 = 0.4

  • 置信度:衡量规则强度,而支持度衡量它在数据库中应该出现的频率。它通过以下公式计算 Y 中项目在包含 X 中的出现频率:

图片

例如:对于 {bread} -> {butter}

c 或 α = σ {butter, bread} / σ {bread} = 3/3 = 1

让我们考虑另一个例子,{curd} -> {bread} 的置信度:

c 或 α = σ {curd,bread} / σ {bread} = 0/3 = 0

Apriori 算法旨在从项目的列表中生成所有可能的项集组合,然后剪枝那些已经达到预定义支持度和置信度参数值的项集。因此,可以理解 Apriori 算法是一个两步算法:

  1. 从项目生成项集

  2. 基于预定义的支持度和置信度评估和剪枝项集

让我们详细讨论第一步。假设集合中有 n 个项目。可以创建的项集数量是 2^n,所有这些都需要在第二步中进行评估,以便得出最终结果。即使只考虑 100 个不同的项目,生成的项集数量也是 1.27e+30!巨大的项集数量提出了严重的计算挑战。

Apriori 算法通过预先排除那些通常很少见或不重要的项集来克服这一挑战。Apriori 原则指出,如果一个项集是频繁的,那么它的所有子集也必须是频繁的。这意味着如果一个项没有达到预定义的支持度阈值,那么这样的项就不会参与项集的创建。因此,Apriori 算法提出了有限数量的项集,这些项集可以在不遇到计算挑战的情况下进行评估。

算法的第一步本质上是迭代的。在第一次迭代中,它考虑所有长度为 1 的项集,也就是说,每个项集只包含一个项目。然后每个项目都会被评估以排除那些被发现没有达到预设支持阈值的项集。第一次迭代的结果是所有满足所需支持的长度的 1 项集。这成为第二次迭代的输入,现在使用第一次迭代中输出的最终项集形成长度为 2 的项集。在第二步中形成的每个项集都会再次检查支持阈值;如果没有达到,这样的项集就会被排除。迭代会一直持续到无法再创建新的项集。项集的过程在以下图中说明:

图片

展示 Apriori 算法中项集创建的插图

一旦我们完成了算法的第一步的所有迭代后的所有项集,第二步就开始了。生成的每个项集都会被测试,以检查它是否满足预定义的置信度值。如果它没有达到阈值,这样的项集就会被从最终输出中排除。

在所有迭代都完成并且最终规则是 Apriori 的输出时,我们使用一个称为提升度的指标来从最终输出中消耗相关的规则。提升度定义了在已知另一个项目或项集已被购买的情况下,一个项目或项集相对于其典型购买率购买的可能性有多大。对于每个项集,我们使用以下公式来获取提升度测量值:

图片

让我们更深入地了解提升度指标。假设在一个超市里,牛奶和面包偶然一起被购买。在这种情况下,预计会有大量的交易涵盖购买的牛奶和面包。提升度(牛奶 -> 面包)超过 1 意味着这些商品比这些商品偶然一起购买的情况更频繁地一起被发现。在评估规则以评估其在商业中的有用性时,我们通常会寻找大于 1 的提升度值。高于 1 的提升度值表明生成的项集非常强大,因此值得考虑实施。

现在,让我们使用 Apriori 算法来实现推荐系统:

# load the required libraries
library(data.table)
library(arules)
library(recommenderlab)
# set the seed so that the results are replicable
set.seed(42)
# reading the Jester5k data
data(Jester5k)
class(Jester5k)

这将产生以下输出:

[1] "realRatingMatrix"
attr(,"package")
[1] "recommenderlab"

从输出中我们可以看到,recommenderlab库中的Jester5k数据是realRatingsMatrix格式。我们也知道这个矩阵中的单元格包含用户为各种笑话提供的评分,并且我们知道评分范围在-10 到+10 之间。

Jester5k 数据集上应用 Apriori 算法为我们理解笑话之间的关联提供了机会。然而,在应用 Apriori 算法之前,我们需要将数据集转换为二进制值,其中 1 代表正面评分,0 代表负面评分或无评分。recommenderlab 库提供了 binarize() 函数,它可以为我们执行所需的操作。以下代码将评分矩阵二值化:

# binarizing the Jester ratings
Jester5k_bin <- binarize(Jester5k, minRating=1)
# let us verify the binarized object
class(Jester5k_bin)

这将导致以下输出:

[1] "binaryRatingMatrix"
attr(,"package")
[1] "recommenderlab"

从输出中我们可以观察到,realRatingsMatrix 已成功转换为 binaryRatingMatrix。挖掘关联的 Apriori 算法期望输入一个矩阵而不是 binaryRatingMatrix。我们可以非常容易地将 Jester5k_bin 对象转换为矩阵格式,以下代码可以完成此操作:

# converting the binaryratingsmatrix to matrix format
Jester5k_bin_mat <- as(Jester5k_bin,"matrix")
# visualizing the matrix object
View(Jester5k_bin_mat)

这将导致以下输出:

从输出中我们可以看到,矩阵的所有单元格都表示为 TRUEFALSE,但 Apriori 预期单元格应该是数字的。现在让我们使用以下代码将单元格转换为 10,分别代表 TRUEFALSE

# converting the cell values to 1 and 0
Jester5k_bin_mat_num <- 1*Jester5k_bin_mat
# viewing the matrix
View(Jester5k_bin_mat_num)

这将导致以下输出:

现在我们已经准备好在数据集上应用 Apriori 算法。我们需要向算法传递两个参数,即 supportconfidence。算法根据这两个参数值挖掘数据集。我们将 0.5 作为支持值的输入,将 0.8 作为置信值的输入。以下代码行提取了存在于我们 Jester 笑话数据集中的笑话关联:

rules <- apriori(data = Jester5k_bin_mat_num, parameter = list(supp = 0.005, conf = 0.8))

这将导致以下输出:

Apriori
Parameter specification:
 confidence minval smax arem  aval originalSupport maxtime support minlen maxlen target   ext
        0.8    0.1    1 none FALSE            TRUE       5     0.5      1     10  rules FALSE
Algorithmic control:
 filter tree heap memopt load sort verbose
    0.1 TRUE TRUE  FALSE TRUE    2    TRUE
Absolute minimum support count: 2500
set item appearances ...[0 item(s)] done [0.00s].
set transactions ...[100 item(s), 5000 transaction(s)] done [0.02s].
sorting and recoding items ... [29 item(s)] done [0.00s].
creating transaction tree ... done [0.00s].
checking subsets of size 1 2 3 done [0.01s].
writing ... [78 rule(s)] done [0.00s].
creating S4 object  ... done [0.00s].

从执行 Apriori 算法创建的 rules 对象现在包含了从数据集中提取和挖掘的所有笑话关联。从输出中我们可以看到,总共提取了 78 个笑话关联。我们可以使用以下代码行来检查这些规则:

inspect(rules)

这将导致以下输出:

     lhs          rhs   support confidence lift     count
[1]  {j48}     => {j50} 0.5068  0.8376860  1.084523 2534
[2]  {j56}     => {j36} 0.5036  0.8310231  1.105672 2518
[3]  {j56}     => {j50} 0.5246  0.8656766  1.120762 2623
[4]  {j42}     => {j50} 0.5150  0.8475971  1.097355 2575
[5]  {j31}     => {j27} 0.5196  0.8255481  1.146276 2598

展示的输出只是列表中 78 条规则中的五条。读取每条规则的方式是,左侧列(lhs)中显示的笑话导致右侧列(rhs)中的笑话;也就是说,喜欢规则左侧 lhs 中笑话的用户通常也倾向于喜欢右侧 rhs 中显示的笑话。例如,在第一条规则中,如果一个用户喜欢了笑话 j48,那么他们很可能也会喜欢 j50,因此值得向只阅读了笑话 j48 的用户推荐笑话 j50

虽然 Apriori 算法生成了多条规则,但每条规则的强度由一个称为提升度的指标来指定。这是一个描述规则在商业环境中价值的指标。请注意,要使规则被认为是通用的,其提升度必须小于或等于1。大于 1 的提升度值表示在商业中实施更好的规则。以下代码行旨在将这样的强规则置于列表顶部:

# converting the rules object into a dataframe
rulesdf <- as(rules, "data.frame")
# employing quick sort on the rules dataframe. lift and confidence are
# used as keys to sort the dataframe. - in the command indicates that we
# want lift and confidence to be sorted in descending order
rulesdf[order(-rulesdf$lift, -rulesdf$confidence), ]

这将产生以下输出:

图片

可以观察到,显示的输出只是规则输出的子集。第一条规则表明j35是一个可以推荐给已经阅读过笑话j29j50的用户笑话。

同样,我们也可以编写一个脚本来搜索用户已经阅读的所有笑话,并将其与规则的左侧进行匹配;如果找到匹配项,则可以将规则的对应右侧推荐给用户作为笑话。

基于内容的推荐引擎

仅基于从客户那里收到的显式或隐式反馈的推荐引擎被称为基于内容的推荐系统。显式反馈是客户通过填写关于偏好的调查问卷、对感兴趣的笑话进行评分或选择与笑话相关的通讯录、将笑话添加到观察列表等方式表达的兴趣。隐式反馈则是一种更为温和的方法,例如客户访问一个页面、点击笑话链接,或者在电子商务页面上阅读笑话评论所花费的时间。根据收到的反馈,向客户推荐类似的笑话。需要注意的是,基于内容的推荐不考虑系统中其他客户的偏好和反馈;相反,它完全基于特定客户的个性化反馈。

在推荐过程中,系统会识别出客户已经对某些产品给予好评的产品,以及客户尚未评价的产品,并寻找等效产品。与好评产品相似的产品会被推荐给客户。在这个模型中,客户的偏好和行为在逐步微调推荐中起着重要作用——也就是说,每次推荐后,根据客户是否对推荐做出响应,系统会逐步学习以提供不同的推荐。以下图表展示了基于内容的推荐系统是如何工作的:

图片

基于内容的推荐系统的工作原理

在我们的愚人笑话数据集中,我们包含了用户对各种笑话的评分以及笑话本身的内容。请记住,JesterJokes字符向量包含了笑话本身存在的文本。笑话中存在的文本相似性可以用作向用户推荐笑话的一种方法。假设是,如果一个人喜欢笑话中的内容,并且如果还有另一个笑话的内容非常相似,那么推荐这个后者的笑话可能会被用户喜欢。

Jester 笑话数据集中没有提供与笑话相关的额外元数据,然而,可以从笑话的内容中创建这样的元数据。例如,笑话的长度、笑话中名词的数量、笑话中出现的幽默词汇数量以及笑话的中心主题。文本处理不仅仅是推荐领域,它还涉及到使用 NLP 技术。由于我们将在不同的章节中介绍 NLP,因此我们在这里不会涉及它。

区分基于 ITCF 和基于内容的推荐

可能看起来基于项目的协同推荐和基于内容的推荐是相同的。实际上,它们并不相同。让我们谈谈它们之间的区别。

ITCF 完全基于用户-项目排名。当我们计算项目之间的相似度时,我们不包含项目属性,只是基于所有顾客的评分来计算项目的相似度。因此,项目的相似度是基于评分而不是项目本身的元数据来计算的。

在基于内容的推荐中,我们使用用户和项目的相关内容。通常,我们使用共享属性空间的内容来构建用户配置文件和项目配置文件。例如,对于一部电影,我们可以用其中的演员和类型(例如使用二进制编码)来表示它。对于用户配置文件,我们可以根据用户进行同样的操作,例如一些演员/类型。然后可以使用余弦相似度等方法计算用户和项目的相似度。这种余弦度量导致推荐的产生。

基于内容的过滤通过识别每个产品上分配的标签来识别相似的产品。每个产品根据每个标签的词频和逆文档频率分配权重。之后,计算用户喜欢产品的概率,以便得出最终的推荐列表。

尽管基于内容的推荐系统非常高效且个性化,但这种模型存在一个固有的问题。让我们通过一个例子来了解基于内容的推荐中过度专业化的难题。

假设有以下五种电影类型:

  • 喜剧

  • 惊悚片

  • 科幻

  • 动作

  • 爱情

有这样一个客户,杰克,他通常观看惊悚片和科幻电影。基于这个偏好,基于内容的推荐引擎只会推荐与这些类型相关的电影,并且永远不会推荐其他类别的电影。这个问题是由于基于内容的推荐引擎仅依赖于用户的过去行为和偏好来确定推荐。

与基于内容的推荐系统不同,在 ITCF 推荐中,基于顾客的积极偏好构建相似产品的邻域。因此,系统会生成可能被顾客偏好的邻域中的产品推荐。ITCF 通过利用不同用户给出的评分之间的相关性来实现这一点,而协同过滤则依赖于用户过去的偏好或评分相关性,它能够从顾客的兴趣领域生成相似产品的推荐。如果产品不受欢迎且很少用户对其给出反馈,这种技术可能导致不良预测。

为笑话推荐构建混合推荐系统

我们可以看到,基于内容的过滤和协同过滤都有其优点和缺点。为了克服这些问题,组织构建了结合两种或更多技术的推荐系统,这些系统被称为混合推荐模型。一个例子是结合基于内容、IBCF、UBCF 和基于模型的推荐引擎。这考虑了所有可能影响为用户做出最相关推荐的方面。以下图表展示了混合推荐引擎中遵循的示例方法:

图片

混合推荐引擎的示例方法

我们需要注意到,实现混合推荐引擎没有标准的方法。为了结合推荐,以下是一些建议的策略:

  • 投票:对从各个推荐系统中获得的推荐输出进行投票。

  • 基于规则的选取:我们可以制定规则,建议对从各个推荐系统中获得的推荐输出进行加权。在这种情况下,获得更高权重的推荐系统的输出将占主导地位,并对最终推荐结果产生更大的影响。

  • 组合:将所有推荐引擎的推荐结果一起展示。最终的推荐列表只是从各个推荐系统中获得的所有推荐输出的并集。

  • 属性集成:从所有推荐系统中提取元数据,将其作为输入提供给另一个推荐系统。

再次强调,对某个问题有效的方法可能对另一个问题不一定适用,因此这些策略在提出最终推荐策略之前需要单独进行测试。

recommenderlab库提供了HybridRecommender函数,允许用户一次性在相同的数据集上训练多个推荐器引擎并组合预测。该函数有一个权重参数,提供了一种指定将用于组合单个预测以到达最终推荐预测(针对未见数据)的每个模型的权重的途径。实现基于混合推荐引擎的项目非常简单,与我们在基于项目的协同过滤或基于用户的协同过滤项目中学习到的代码没有太大区别。无论如何,让我们编写代码并构建一个针对Jester5k数据集的混合推荐引擎:

# including the required libraries
library(recommenderlab)
# accessing the Jester5k dataset that is a part of recommenderlab library
data(Jester5k)
# split the data into the training and the test set
Jester5k_es <- evaluationScheme(Jester5k, method="split", train=0.8, given=20, goodRating=0)

之前的代码是用来训练混合推荐器的。这就是它与我们所构建的ITCFUBCF推荐器不同的地方。从代码中我们可以观察到,我们使用了四种不同的推荐器方法,这些方法将构成混合推荐器。让我们逐一讨论这些方法:

  • 流行推荐方法简单地推荐流行的笑话(由收到的评分数量决定)给用户。

  • 我们使用的第二种推荐器方法是基于项目的协同过滤方法,使用非归一化数据,但通过余弦相似度计算项目之间的距离。

  • Z-score归一化的数据上基于用户进行基于Z-score的协同过滤,用户之间的距离通过欧几里得距离来计算。

  • 一种随机推荐方法,为用户提供随机推荐。

我们绝不认为这四种推荐器方法的组合是此问题的最佳混合。这个项目的目的是展示混合推荐器的实现。涉及的方法的选择完全是任意的。在现实中,我们可能需要尝试多种组合来识别最佳混合。混合分类器使用以下代码构建:

#train a hybrid recommender model
hybrid_recom <- HybridRecommender(
  Recommender(getData(Jester5k_es, "train"), method = "POPULAR"),
  Recommender(getData(Jester5k_es, "train"), method="IBCF",
              param=list(normalize = NULL, method="Cosine")),
  Recommender(getData(Jester5k_es, "train"), method="UBCF",
                          param=list(normalize = "Z-score",method="Euclidean")),
  Recommender(getData(Jester5k_es, "train"), method = "RANDOM"),
  weights = c(.2, .3, .3,.2)
)
# Observe the model that is built
print (getModel(hybrid_recom)

这将产生以下输出:

$recommender
$recommender[[1]]
Recommender of type ‘POPULAR’ for ‘realRatingMatrix’
learned using 4000 users.
$recommender[[2]]
Recommender of type ‘IBCF’ for ‘realRatingMatrix’
learned using 4000 users.
$recommender[[3]]
Recommender of type ‘UBCF’ for ‘realRatingMatrix’
learned using 4000 users.
$recommender[[4]]
Recommender of type ‘RANDOM’ for ‘realRatingMatrix’
learned using 4000 users.
$weights
[1] 0.2 0.3 0.3 0.2

观察混合模型中的权重分配。我们看到流行和随机推荐器各分配了 20%的权重,而前一个混合中涉及的ITCFUBCF方法各分配了 30%的权重。在构建混合推荐器时设置权重不是强制性的,在这种情况下,混合推荐器中涉及的方法将分配相等的权重。现在我们的模型已经准备好了,让我们使用以下代码进行预测并评估性能:

# making predictions
pred <- predict(hybrid_recom, getData(Jester5k_es, "known"), type="ratings")
# # set the predictions that fall outside the valid range to the boundary values
pred@data@x[pred@data@x[] < -10] <- -10
pred@data@x[pred@data@x[] > 10] <- 10
# calculating performance measurements
hybrid_recom_pred = calcPredictionAccuracy(pred, getData(Jester5k_es, "unknown"))
# printing the performance measurements
library(knitr)
print(kable(hybrid_recom_pred))

这将产生以下输出:

|     |         x|
|:----|---------:|
|RMSE |  4.468849|
|MSE  | 19.970611|
|MAE  |  3.493577|

摘要

在本章中,我们广泛使用了recommenderlab库来构建基于 Jester 笑话数据集的各种类型的笑话推荐引擎。我们还了解了这些方法背后的理论概念。

推荐系统是一个独立的机器学习领域。这个主题非常广泛,无法仅在一章中涵盖。存在多种类型的推荐系统,并且它们可以应用于特定场景的数据集。矩阵分解、奇异值分解近似、最流行项和 SlopeOne 是一些可能用于构建推荐系统的技术。这些技术超出了本章的范围,因为这些技术很少在商业环境中用于构建推荐系统,而本章的目的是介绍更多流行的技术。关于推荐引擎的进一步学习可以是在探索和研究这些很少使用的技术,并将它们应用于现实世界问题的方向。

下一章将专注于自然语言处理技术。我们将使用几种流行的技术来实现亚马逊产品评论的情感分析引擎。我们将探索语义和句法方法来分析文本,然后将它们应用于亚马逊评论语料库。我已经准备好翻过这一页,进入下一章。你呢?

参考文献

虽然recommenderlab库在 R 社区中非常受欢迎,但这并不是构建推荐系统的唯一选择。以下是一些你可能依赖的其他流行库来实现推荐引擎:

  • rrecsys:有几个流行的推荐系统,如全局/项目/用户平均基线、基于项目的 KNN、FunkSVD、BPR 和加权 ALS 用于快速原型设计。有关更多信息,请参阅cran.r-project.org/web/packages/rrecsys/index.htmlImplementations

  • recosystem:这是libmf库的 R 语言包装器,用于矩阵分解的推荐系统(www.csie.ntu.edu.tw/~cjlin/libmf/)。它通常用于通过潜在空间中两个矩阵的乘积来近似一个不完整的矩阵。这项任务的常见名称还包括协同过滤、矩阵补全和矩阵恢复。此包支持高性能的多核并行计算。

  • rectools:这是一个高级推荐系统包,用于结合用户和项目协变量信息,包括具有并行计算的项类别偏好,统计潜在因子模型的创新变化,焦点小组发现者,NMF,方差分析和余弦模型。

第四章:使用 NLP 进行亚马逊评论的情感分析

每天我们都会从电子邮件、博客、社交媒体评论等在线帖子中生成数据。说非结构化文本数据比任何组织数据库中存在的表格数据大得多并不令人惊讶。对于组织来说,从与组织相关的文本数据中获得有用的见解非常重要。由于与数据库中的数据相比,文本数据具有不同的性质,因此需要采用不同的方法来理解文本数据。在本章中,我们将学习许多自然语言处理(NLP)的关键技术,这些技术帮助我们处理文本数据。

NLP 的常见定义如下:计算机科学和人工智能的一个领域,处理计算机与人类(自然)语言之间的交互;特别是,如何编程计算机以有效地处理大量自然语言数据。

从一般意义上讲,NLP 处理的是理解人类语言的自然表达。它帮助机器阅读和理解“文本”。

人类语言非常复杂,需要解决多个歧义才能正确理解口语或书面文本。在自然语言处理领域,应用了多种技术来处理这些歧义,包括词性标注器、术语消歧、实体提取、关系提取、关键词识别等。

为了自然语言系统能够成功工作,一个一致的知识库,如详细的同义词词典、词汇表、语言和语法规则的数据集、本体和最新的实体,是先决条件。

可以指出,自然语言处理(NLP)不仅关注从句法角度理解文本,还关注从语义角度理解文本。类似于人类,目标是让机器能够感知说话背后的潜在信息,而不仅仅是句子中词语的结构。NLP 有众多应用领域,以下只是其中的一小部分:

  • 语音识别系统

  • 问答系统

  • 机器翻译

  • 文本摘要

  • 虚拟代理或聊天机器人

  • 文本分类

  • 主题分段

由于自然语言处理本身是一个非常大的领域,不可能在一个章节中涵盖所有领域。因此,我们将专注于本章的“文本分类”。我们通过实施一个项目来实现,该项目对亚马逊.com 客户表达的评论进行情感分析。情感分析是一种文本分类任务,我们将每个文档(评论)分类到可能的类别之一。可能的类别可以是正面、负面或中性,或者可以是正面、负面或 1 到 10 的评分。

需要分类的文本文档不能直接输入到机器学习算法中。每个文档都需要以机器学习算法可以接受的格式表示。在本章中,我们将探讨、实现和理解词袋模型(BoW)词嵌入方法。这些方法中,文本可以表示。

随着本章的进展,我们将涵盖以下主题:

  • 情感分析问题

  • 理解亚马逊评论数据集

  • 使用 BoW 方法构建文本情感分类器

  • 理解词嵌入方法

  • 基于路透社新闻语料库的预训练 Word2Vec 词嵌入构建文本情感分类器

  • 使用 GloVe 词嵌入构建文本情感分类器

  • 使用 fastText 构建文本情感分类器

情感分析问题

情感分析是最通用的文本分类应用之一。其目的是分析用户评论、员工反馈等消息,以确定潜在的情感是积极的、消极的还是中性的。

分析和报告文本中的情感允许企业快速获得综合的高层次洞察,而无需阅读收到的每一条评论。

虽然可以根据收到的总体评论生成整体情感,但还有一个扩展领域,称为基于方面的情感分析。它侧重于根据服务的每个方面推导情感。例如,一位在撰写评论时访问过餐厅的客户通常会涵盖环境、食品质量、服务质量、价格等方面。尽管关于每个方面的反馈可能不会在特定的标题下引用,但评论中的句子自然会涵盖客户对其中一个或多个这些方面的看法。基于方面的情感分析试图识别每个方面的评论中的句子,然后确定情感是积极的、消极的还是中性的。按每个方面提供情感有助于企业快速识别其薄弱环节。

在本章中,我们将讨论和实现旨在从评论文本中识别整体情感的方法。这个任务可以通过多种方式实现,从简单的词典方法到复杂的词嵌入方法。

词库方法实际上并不是一种机器学习方法。它更是一种基于预定义的正负词字典的规则方法。该方法涉及查找每个评论中的正词和负词的数量。如果评论中正词的数量多于负词的数量,则该评论被标记为正面,否则被标记为负面。如果正词和负词的数量相等,则评论被标记为中性。由于实现此方法很简单,并且它需要一个预定义的字典,因此我们不会在本章中介绍词库方法的实现。

虽然将情感分析问题视为无监督聚类问题是可能的,但在本章中,我们将它视为监督分类问题。这是因为我们有亚马逊评论标记数据集可用。我们可以利用这些标签来构建分类模型,因此,使用监督算法。

入门

数据集可在以下 URL 下载和使用:

drive.google.com/drive/u/0/folders/0Bz8a_Dbh9Qhbfll6bVpmNUtUcFdjYmF2SEpmZUZUcVNiMUw1TWN6RDV3a0JHT3kxLVhVR2M .

理解亚马逊评论数据集

我们在本章的各个项目中使用亚马逊产品评论极性数据集。这是一个由张翔构建并公开的数据集。它被用作论文《Character-level Convolutional Networks for Text Classification》和《Advances in Neural Information Processing Systems》28 中的文本分类基准,作者为张翔、赵军波、杨立昆(NIPS 2015)

亚马逊评论极性数据集是通过将评分 1 和 2 视为负面,4 和 5 视为正面来构建的。评分 3 的样本被忽略。在数据集中,类别 1 是负面,类别 2 是正面。该数据集有 1,800,000 个训练样本和 200,000 个测试样本。

train.csvtest.csv文件包含所有样本,以逗号分隔值的形式存在。它们包含三列,分别对应类别索引(1 或 2)、评论标题和评论文本。评论标题和文本使用双引号(")进行转义,任何内部的引号通过两个双引号("")进行转义。换行符通过反斜杠后跟一个“n”字符进行转义,即"\n"。

为了确保我们能够运行我们的项目,即使是在最基本的基础设施下,让我们将我们的数据集中要考虑的记录数限制为仅 1,000 条。当然,我们在项目中使用的代码可以扩展到任意数量的记录,只要硬件基础设施支持即可。让我们首先读取数据,并使用以下代码可视化记录:

# reading first 1000 reviews
reviews_text<-readLines('/home/sunil/Desktop/sentiment_analysis/amazon _reviews_polarity.csv', n = 1000)
# converting the reviews_text character vector to a dataframe
reviews_text<-data.frame(reviews_text)
# visualizing the dataframe
View(reviews_text)

这将产生以下输出:

阅读完文件后,我们可以看到数据集中只有一列,而这列包含了评论文本和情感成分。为了在本章中使用涉及 BoW、Word2vec 和 GloVe 方法的情感分析项目,我们将稍微修改数据集的格式。让我们用以下代码修改数据集的格式:

# separating the sentiment and the review text
# post separation the first column will have the first 4 characters
# second column will have the rest of the characters
# first column should be named "Sentiment"
# second column to be named "SentimentText"
library(tidyr)
reviews_text<-separate(data = reviews_text, col = reviews_text, into = c("Sentiment", "SentimentText"), sep = 4)
# viewing the dataset post the column split
View(reviews_text)

这将产生以下输出:

现在我们数据集中有两列。然而,这两列中都存在可能引起进一步处理数据集问题的多余标点符号。让我们尝试用以下代码删除标点符号:

# Retaining only alphanumeric values in the sentiment column
reviews_text$Sentiment<-gsub("[^[:alnum:] ]","",reviews_text$Sentiment)
# Retaining only alphanumeric values in the sentiment text
reviews_text$SentimentText<-gsub("[^[:alnum:] ]"," ",reviews_text$SentimentText)
# Replacing multiple spaces in the text with single space
reviews_text$SentimentText<-gsub("(?<=[\\s])\\s*|^\\s+|\\s+$", "", reviews_text$SentimentText, perl=TRUE)
# Viewing the dataset
View(reviews_text)
# Writing the output to a file that can be consumed in other projects
write.table(reviews_text,file = "/home/sunil/Desktop/sentiment_analysis/Sentiment Analysis Dataset.csv",row.names = F,col.names = T,sep=',')

这将产生以下输出:

从前面的输出中,我们看到我们有一个干净的数据集,可以立即使用。此外,我们还已将输出写入文件。当我们构建情感分析器时,我们可以直接从Sentiment Analysis Dataset.csv文件中读取数据集。

fastText 算法期望数据集以不同的格式。fastText 的数据输入应遵守以下格式:

__label__<X>  <Text>

在这个例子中,X是类名。Text 是导致该类下指定评分的实际评论文本。评分和文本应放在同一行上,无需引号。类是__label__1__label__2,每行应只有一个类。让我们用以下代码块完成fastText库所需的格式:

# reading the first 1000 reviews from the dataset
reviews_text<-readLines('/home/sunil/Desktop/sentiment_analysis/amazon _reviews_polarity.csv', n = 1000)
# basic EDA to confirm that the data is read correctly
print(class(reviews_text))
print(length(reviews_text))
print(head(reviews_text,2))
# replacing the positive sentiment value 2 with __label__2
reviews_text<-gsub("\\\"2\\\",","__label__2 ",reviews_text)
# replacing the negative sentiment value 1 with __label__1
reviews_text<-gsub("\\\"1\\\",","__label__1 ",reviews_text)
# removing the unnecessary \" characters
reviews_text<-gsub("\\\""," ",reviews_text)
# replacing multiple spaces in the text with single space
reviews_text<-gsub("(?<=[\\s])\\s*|^\\s+|\\s+$", "", reviews_text, perl=TRUE)
# Basic EDA post the required processing to confirm input is as desired
print("EDA POST PROCESSING")
print(class(reviews_text))
print(length(reviews_text))
print(head(reviews_text,2))
# writing the revamped file to the directory so we could use it with
# fastText sentiment analyzer project
fileConn<-file("/home/sunil/Desktop/sentiment_analysis/Sentiment Analysis Dataset_ft.txt")
writeLines(reviews_text, fileConn)
close(fileConn)

这将产生以下输出:

[1] "EDA PRIOR TO PROCESSING"
[1] "character"
[1] 1000
[1] "\"2\",\"Stuning even for the non-gamer\",\"This sound track was beautiful! It paints the senery in your mind so well I would recomend it even to people who hate vid. game music! I have played the game Chrono Cross but out of all of the games I have ever played it has the best music! It backs away from crude keyboarding and takes a fresher step with grate guitars and soulful orchestras. It would impress anyone who cares to listen! ^_^\""                                                                                  
[2] "\"2\",\"The best soundtrack ever to anything.\",\"I'm reading a lot of reviews saying that this is the best 'game soundtrack' and I figured that I'd write a review to disagree a bit. This in my opinino is Yasunori Mitsuda's ultimate masterpiece. The music is timeless and I'm been listening to it for years now and its beauty simply refuses to fade.The price tag on this is pretty staggering I must say, but if you are going to buy any cd for this much money, this is the only one that I feel would be worth every penny.\""
[1] "EDA POST PROCESSING"
[1] "character"
[1] 1000\
[1] "__label__2 Stuning even for the non-gamer , This sound track was beautiful! It paints the senery in your mind so well I would recommend it even to people who hate vid. game music! I have played the game Chrono Cross but out of all of the games I have ever played it has the best music! It backs away from crude keyboarding and takes a fresher step with grate guitars and soulful orchestras. It would impress anyone who cares to listen! ^_^"                                                                                   
[2] "__label__2 The best soundtrack ever to anything. , I'm reading a lot of reviews saying that this is the best 'game soundtrack' and I figured that I'd write a review to disagree a bit. This in my opinino is Yasunori Mitsuda's ultimate masterpiece. The music is timeless and I'm been listening to it for years now and its beauty simply refuses to fade. The price tag on this is pretty staggering I must say, but if you are going to buy any cd for this much money, this is the only one that I feel would be worth every penny."

从基本的 EDA 代码输出中,我们可以看到数据集已经处于所需格式,因此我们可以继续到下一个部分,使用 BoW 方法实现情感分析引擎。在实现的同时,我们将深入研究该方法背后的概念,并探索该方法中可以用来获得更好结果的一些子技术。

使用 BoW 方法构建文本情感分类器

BoW 方法的目的是将提供的评论文本转换为矩阵形式。它通过忽略单词的顺序和意义,将文档表示为一组不同的单词。矩阵的每一行代表每个评论(在 NLP 中通常称为文档),列代表所有评论中存在的通用单词集。对于每个文档和每个单词,记录该单词在该特定文档中的存在或单词出现的频率。最后,从单词频率向量创建的矩阵表示文档集。这种方法用于创建训练模型所需的输入数据集,以及准备需要由训练模型使用的测试数据集以执行文本分类。现在我们了解了 BoW 的动机,让我们跳入实现基于此方法的情感分析分类器步骤,如下面的代码块所示:

# including the required libraries
library(SnowballC)
library(tm)
# setting the working directory where the text reviews dataset is located
# recollect that we pre-processed and transformed the raw dataset format
setwd('/home/sunil/Desktop/sentiment_analysis/')
# reading the transformed file as a dataframe
text <- read.table(file='Sentiment Analysis Dataset.csv', sep=',',header = TRUE)
# checking the dataframe to confirm everything is in tact
print(dim(text))
View(text)

这将导致以下输出:

> print(dim(text))
[1] 1000 2
> View(text)

处理文本数据的第一个步骤是创建一个语料库,这是一个文本文档的集合。tm包中的VCorpus函数可以将数据框中的评论评论列转换为不稳定的语料库。这可以通过以下代码实现:

# transforming the text into volatile corpus
train_corp = VCorpus(VectorSource(text$SentimentText))
print(train_corp)

这将导致以下输出:

> print(train_corp)
<<VCorpus>>
Metadata:  corpus specific: 0, document level (indexed): 0
Content:  documents: 1000

从不稳定的语料库中,我们创建一个文档-词矩阵DTM)。DTM 是使用tm库的DocumentTermMatrix函数创建的稀疏矩阵。矩阵的行表示文档,列表示特征,即单词。该矩阵是稀疏的,因为数据集中的所有唯一单语元集都成为 DTM 的列,并且由于每个评论评论没有单语元集的所有元素,大多数单元格将有一个 0,表示不存在单语元。

虽然在 BoW(词袋)方法中可以提取 n-gram(单语元、双语元、三元语元等)作为一部分,但可以将 tokenize 参数设置为控制列表的一部分,并在DocumentTermMatrix函数中传递以在 DTM(文档-词矩阵)中实现 n-gram。必须注意的是,将 n-gram 作为 DTM 的一部分会创建 DTM 中非常高的列数。这是 BoW 方法的一个缺点,在某些情况下,由于内存限制,它可能会阻碍项目的执行。鉴于我们的特定案例也受硬件基础设施的限制,我们在这个项目中仅包括 DTM 中的单语元。除了仅生成单语元之外,我们还通过对tm库的DocumentTermMatrix函数中的控制列表传递参数,对评论文本文档进行一些额外的处理。在创建 DTM 期间对评论文本文档进行的处理如下:

  1. 将文本的字母大小写改为小写。

  2. 移除任何数字。

  3. 使用 Snowball 词干提取项目的英语语言停用词表来移除停用词。停用词是一些常见的单词,如 a、an、in 和 the,它们在根据评论内容判断情感时并不增加价值。

  4. 移除标点符号。

  5. 进行词干提取,其目的是将单词还原为单词的基本形式,即从名词中去除复数s,从动词中去除ing,或去除其他前缀。词干是一组具有相等或非常相似意义的自然词组。在词干提取过程之后,每个单词都由其词干表示。《SnowballC》库提供了获取评论评论中每个单词根的能力。

现在让我们使用以下代码块从易变语料库创建 DTM 并进行文本预处理:

# creating document term matrix
dtm_train <- DocumentTermMatrix(train_corp, control = list(
  tolower = TRUE,removeNumbers = TRUE,
  stopwords = TRUE,
  removePunctuation = TRUE,
  stemming = TRUE
))
# Basic EDA on dtm
inspect(dtm_train)

这将产生以下输出:

> inspect(dtm_train)
<<DocumentTermMatrix (documents: 1000, terms: 5794)>>
Non-/sparse entries: 34494/5759506
Sparsity           : 99%
Maximal term length: 21
Weighting          : term frequency (tf)
Sample             :
     Terms
Docs  book can get great just like love one read time
  111    0   3   2     0    0    0    2   1    0    2
  162    4   1   0     0    0    1    0   0    1    0
  190    0   0   0     0    0    0    0   0    0    0
  230    0   1   1     0    0    0    1   0    0    0
  304    0   0   0     0    0    3    0   2    0    0
  399    0   0   0     0    0    0    0   0    0    0
  431    9   1   0     0    0    1    2   0    0    1
  456    1   0   0     0    0    0    0   1    2    0
  618    0   2   3     1    4    1    3   1    0    1
  72     0   0   1     0    2    0    0   1    0    1

从输出中我们可以看到,有 1,000 个文档被处理并形成矩阵的行。有 5,794 列代表经过额外文本处理后的独特单语素。我们还看到 DTM 有 99%的稀疏性,并且仅在 34,494 个单元格中有非零条目。非零单元格代表单词在 DTM 行对应的文档中的出现频率。权重是通过默认的“词频”权重进行的,因为我们没有在提供给DocumentTermMatrix函数的控制列表中指定任何权重参数。通过在控制列表中传递适当的权重参数到DocumentTermMatrix函数,也可以实现其他形式的权重,例如词频-逆文档频率TFIDF)。现在,我们将坚持基于词频的权重,这是默认的。我们还从inspect函数中看到,一些样本文档以及这些文档中的词频被输出。

DTM(文档-词矩阵)往往变得非常大,即使是正常大小的数据集也是如此。移除稀疏项,即仅出现在极少数文档中的项,是一种可以尝试的技术,可以在不丢失矩阵固有的显著关系的情况下减小矩阵的大小。让我们从矩阵中移除稀疏列。我们将尝试使用以下代码行移除至少有 99%稀疏元素的术语:

# Removing sparse terms
dtm_train= removeSparseTerms(dtm_train, 0.99)
inspect(dtm_train)

这将产生以下输出:

> inspect(dtm_train)
<<DocumentTermMatrix (documents: 1000, terms: 686)>>
Non-/sparse entries: 23204/662796
Sparsity           : 97%
Maximal term length: 10
Weighting          : term frequency (tf)
Sample             :
     Terms
Docs  book can get great just like love one read time
  174    0   0   1     1    1    2    0   2    0    1
  304    0   0   0     0    0    3    0   2    0    0
  355    3   0   0     0    1    1    2   3    1    0
  380    4   1   0     0    1    0    0   1    0    2
  465    5   0   1     1    0    0    0   2    6    0
  618    0   2   3     1    4    1    3   1    0    1
  72     0   0   1     0    2    0    0   1    0    1
  836    1   0   0     0    0    3    0   0    5    1
  866    8   0   1     0    0    1    0   0    4    0
  959    0   0   2     1    1    0    0   2    0    1

现在我们从inspect函数的输出中看到,矩阵的稀疏性降低到 97%,单语素(矩阵的列)的数量减少到686。我们现在已经准备好可以使用任何机器学习分类算法进行训练的 DTM。在接下来的几行代码中,让我们尝试将我们的 DTM 划分为训练集和测试集:

# splitting the train and test DTM
dtm_train_train <- dtm_train[1:800, ]
dtm_train_test <- dtm_train[801:1000, ]
dtm_train_train_labels <- as.factor(as.character(text[1:800, ]$Sentiment))
dtm_train_test_labels <- as.factor(as.character(text[801:1000, ]$Sentiment))

我们将使用一种名为朴素贝叶斯的机器学习算法来创建模型。朴素贝叶斯通常在具有名义特征的 数据上训练。我们可以观察到我们的 DTM(词袋模型)中的单元格是数值型的,因此需要将其转换为名义型,以便将数据集作为输入提供给朴素贝叶斯模型进行创建。由于每个单元格表示评论中的单词频率,并且单词在评论中使用的次数不会影响情感,让我们编写一个函数将具有非零值的单元格值转换为Y,在值为零的情况下,将其转换为N,以下是一段代码:

cellconvert<- function(x) {
x <- ifelse(x > 0, "Y", "N")
}

现在,让我们应用函数到训练数据集和测试数据集的所有行上,这些数据集是我们在这个项目中之前创建的,以下是一段代码:

# applying the function to rows in training and test datasets
dtm_train_train <- apply(dtm_train_train, MARGIN = 2,cellconvert)
dtm_train_test <- apply(dtm_train_test, MARGIN = 2,cellconvert)
# inspecting the train dtm to confirm all is in tact
View(dtm_train_train)

这将产生以下输出:

从输出中我们可以看到,训练和测试 DTM 中的所有单元格现在都已转换为名义值。因此,让我们继续使用e1071库中的朴素贝叶斯算法构建文本情感分析分类器,如下所示:

# training the naive bayes classifier on the training dtm
library(e1071)
nb_senti_classifier=naiveBayes(dtm_train_train,dtm_train_train_labels)
# printing the summary of the model created
summary(nb_senti_classifier)

这将产生以下输出:

> summary(nb_senti_classifier)
        Length Class  Mode    
apriori   2    table  numeric 
tables  686    -none- list    
levels    2    -none- character
call      3    -none- call  

前面的摘要输出显示,nb_senti_classifier对象已成功从训练 DTM 创建。现在让我们使用模型对象在测试数据 DTM 上预测情感。在以下代码块中,我们指示预测应该是类别而不是预测概率:

# making predictions on the test data dtm
nb_predicts<-predict(nb_senti_classifier, dtm_train_test,type="class")
# printing the predictions from the model
print(nb_predicts)

这将产生以下输出:

[1] 1 1 2 1 1 1 1 1 1 2 2 1 2 2 2 2 1 2 1 1 2 1 2 1 1 1 2 2 1 2 2 2 2 1 2 1 1 1 1 2 2 2 2 1 2 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 2 1 1 1 1 1 1 1 2 1 1 2 2 1 2 2 2 2 1 2 2 1 1 1 1 1 2 1 1 2 1 1 1 1 1 2 2 2 2 2 2 1 2 2 1 2 1 1 1 1 2 2 2 2 2 1 1 1 2 2 2 1 1 1 1 1 2 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 2 1 1 1 1 1 1 2 1 1 1 1 1 1 2 2 2 2 2 1 2 2 1 2 2 1 1 2 2 1 1 2 2 2 2 2 2 2 2 2 2 2 1 1 2 1 2 1 2 2 1 1 1 1 2
Levels: 1 2

使用以下代码,我们现在使用rminer库中的mmetric函数计算模型的准确率:

# computing accuracy of the model
library(rminer)
print(mmetric(nb_predicts, dtm_train_test_labels, c("ACC")))

这将产生以下输出:

[1] 79

我们仅使用一个非常快速和基本的 BoW 模型就实现了 79%的准确率。可以通过参数调整、词干提取、创建新特征等技术进一步提高模型。

BoW(词袋)方法的优缺点

现在我们已经了解了 BoW(词袋)方法的原理和实现,让我们来分析一下这种方法的优势和劣势。在优势方面,BoW 方法非常简单易懂且易于实现,因此为任何文本数据集的定制化提供了很大的灵活性。可以观察到,当只考虑单词时,BoW 方法并不保留单词的顺序。这个问题通常通过在 DTM(词袋矩阵)中保留 n-gram 来解决。然而,这需要更大的基础设施来处理文本和构建分类器。该方法的另一个严重缺点是它不尊重单词的语义。例如,“汽车”和“汽车”这两个词在相同的上下文中经常被使用。基于 BoW 构建的模型将句子“购买二手汽车”和“购买旧汽车”视为非常不同的句子。尽管这些句子是相同的,但 BoW 模型不会将这些句子分类为相同,因为这些句子中的单词并不匹配。使用称为词嵌入的方法可以考虑到句子中单词的语义。这是我们将在下一节中探讨的内容。

理解词嵌入

我们在前面章节中讨论的 BoW 模型存在一个问题,即它们没有捕捉到关于单词意义或上下文的信息。这意味着潜在的关联,如上下文邻近性,在单词集合中没有被捕捉到。例如,这种方法无法捕捉简单的关联,例如确定“汽车”和“公交车”这两个词都指的是经常在交通上下文中讨论的车辆。通过词嵌入,我们可以克服 BoW 方法中遇到的问题,词嵌入是一种改进的映射语义相似单词的方法。

词向量将单词表示为多维连续的浮点数,其中语义相似的单词在几何空间中被映射到邻近的点。例如,“水果”和“叶子”这两个词会有相似的词向量,“树”。这是由于它们意义的相似性,而“电视”这个词在几何空间中则相对较远。换句话说,在相似上下文中使用的单词将被映射到邻近的向量空间。

词向量可以是n维的,n可以是用户创建时输入的任何数字(例如 10、70、500)。这些维度是隐含的,因为对人类来说,这些维度在现实中代表什么可能并不明显。有如连续词袋CBOW)和跳字模型Skip-Gram)等方法,可以从提供的文本作为训练输入到词嵌入算法中构思词向量。此外,词向量中的单个数字代表单词在各个维度上的分布权重。在一般意义上,每个维度代表一个潜在的意义,而单词在该维度上的数值权重捕捉了它与该意义的关联程度。因此,单词的语义嵌入在向量的维度中。

尽管词向量是多维的,无法直接可视化,但可以通过使用如 t-SNE 降维技术等将它们投影到二维空间,从而可视化学习到的向量。以下图表显示了国家首都、动词时态和性别关系在二维空间中的学习词向量:

图片

在二维空间中可视化词嵌入

当我们观察词嵌入的可视化时,我们可以感知到向量捕捉了关于单词及其相互之间的一些通用、实际上非常有用的语义信息。有了这个,文本中的每个单词现在都可以表示为矩阵中的一行,类似于 BoW 方法,但与 BoW 方法不同,它捕捉了单词之间的关系。

将单词表示为向量的优点是它们适用于数学运算。例如,我们可以对向量进行加法和减法。这里的典型例子是展示通过使用词向量,我们可以确定以下内容:

国王 - 男人 + 女人 = 女王

在给定的例子中,我们从国王的词向量中减去了性别(男人),并添加了另一个性别(女人),从而通过操作(国王 - 男人 + 女人)获得了一个新的词向量,该向量与女王的词向量映射最为接近。

下面展示了可以在词向量上实现的更多数学运算的惊人例子:

  • 给定两个单词,我们可以确定它们之间的相似度:
model.similarity('woman','man')

输出如下:

0.73723527
  • 从给定的单词集中找出不同类的一个:
model.doesnt_match('breakfast cereal dinner lunch';.split())

不同类的单词如下所示:

'cereal'
  • 推导类比,例如:
model.most_similar(positive=['woman','king'],negative=['man'],topn=1)

输出如下:

queen: 0.508

现在,这一切对我们意味着,机器能够识别句子中给出的语义相似的单词。以下图表是关于词嵌入的一个让我笑的笑话,但这个笑话确实传达了词嵌入应用的力量,否则使用 BoW 类型的文本表示是不可能实现的:

一个展示词嵌入应用能力的笑话

有几种技术可以从文本数据中学习词嵌入。Word2vec、GloVe 和 fastText 是一些流行的技术。这些技术中的每一种都允许我们从我们拥有的文本数据中训练自己的词嵌入,或者使用现成的预训练向量。

这种学习我们自己的词嵌入的方法需要大量的训练数据,可能会很慢,但这个选项将学习一个既针对特定文本数据又针对当前 NLP 任务的嵌入。

预训练词嵌入向量是在大量文本数据(通常为数十亿单词)上训练的向量,这些数据通常来自维基百科等来源。这些通常是谷歌或 Facebook 等公司提供的高质量词嵌入向量。我们可以下载这些预训练向量文件,并使用它们来获取我们想要分类或聚类的文本中单词的词向量。

基于路透社新闻语料库,使用预训练的 word2vec 词嵌入构建文本情感分类器

Word2vec 是由 Tomas Mikolov 等人于 2013 年在谷歌开发的,作为使基于神经网络的嵌入训练更高效的回应,从那时起,它已经成为开发预训练词嵌入的事实标准。

Word2vec 引入了以下两种不同的学习模型来学习词嵌入:

  • CBOW: 通过预测当前词的上下文来学习嵌入。

  • 连续 Skip-Gram: 连续 Skip-Gram 模型通过预测给定当前词的周围词来学习。

CBOW 和 Skip-Gram 学习方法都专注于根据局部使用上下文学习单词,其中单词的上下文由一个邻近单词的窗口定义。这个窗口是模型的可配置参数。

R 中的softmaxreg库提供了预训练的word2vec词嵌入,可用于构建针对亚马逊评论数据的情感分析引擎。预训练的向量是使用word2vec模型构建的,并且基于Reuter_50_50数据集,UCI 机器学习仓库(archive.ics.uci.edu/ml/datasets/Reuter_50_50)。

不加任何延迟,让我们进入代码并回顾一下这个代码中采用的方法:

# including the required library
library(softmaxreg)
# importing the word2vec pretrained vector into memory
data(word2vec)

让我们检查word2vec预训练嵌入。它只是一个数据框,因此可以通过常规的dimView命令进行审查,如下所示:

View(word2vec)

这将导致以下输出:

这里,让我们使用以下 dim 命令:

dim(word2vec)

这将导致以下输出:

[1] 12853 21

从前面的输出中,我们可以观察到有12853个单词在预训练向量中获得了单词向量。每个单词都是使用 20 个维度定义的,这些维度定义了单词的上下文。在下一步中,我们可以查找评论中每个单词的单词向量。由于预训练的单词嵌入中只有 12,853 个单词,我们可能会遇到一个不在预训练嵌入中的单词。在这种情况下,未识别的单词用一个填充有零的 20 维向量表示。

我们还需要理解,单词向量仅在单词级别上可用,因此为了解码整个评论,我们取构成评论的所有单词的单词向量的平均值。让我们通过一个例子来回顾从单个单词向量获取句子单词向量的概念。

假设我们想要获取单词向量的句子是,“今天早上非常明亮且阳光明媚”。构成句子的单个单词是“it”、“is”、“very”、“bright”、“and”、“sunny”、“this”和“morning”。

现在,我们可以在预训练向量中查找这些单词,并获取以下表格中所示的相应单词向量:

dim1 dim2 dim3 ..... .... dim19 dim20
it -2.25 0.75 1.75 -1.25 -0.25 -3.25 -2.25
is 0.75 1.75 1.75 -2.25 -2.25 0.75 -0.25
very -2.25 2.75 1.75 -0.25 0.75 0.75 -2.25
bright -3.25 -3.25 -2.25 -1.25 0.75 1.75 -0.25
and -0.25 -1.25 -2.25 2.75 -3.25 -0.25 1.75
sunny 0 0 0 0 0 0 0
this -2.25 -3.25 2.75 0.75 -0.25 -0.25 -0.25
morning -0.25 -3.25 -2.25 1.75 0.75 2.75 2.75

现在,我们有了构成句子的单词向量。请注意,这些并不是实际的单词向量值,只是为了演示方法而编造的。此外,观察单词sunny在所有维度上用零表示,以表示该单词未在预训练的单词嵌入中找到。为了获取句子的单词向量,我们只需计算每个维度的平均值。得到的向量是一个 1 x 20 的向量,代表句子,如下所示:

句子 -1.21875 -0.71875 0.15625 0.03125 -0.46875 0.28125 -0.09375

softmaxreg库提供了wordEmbed函数,我们可以传递一个句子并要求它计算句子的mean词向量。以下是一个自定义函数,用于将wordEmbed函数应用于我们手头的每个亚马逊评论。在将此函数应用于评论数据集之后,我们期望得到一个n x 20 的矩阵,这是我们评论的词向量表示。nn x 20 中代表行数,20 是通过每个评论表示的维度数,如下所示:

# function to get word vector for each review
docVectors = function(x)
{
  wordEmbed(x, word2vec, meanVec = TRUE)
}
# setting the working directory and reading the reviews dataset
setwd('/home/sunil/Desktop/sentiment_analysis/')
text = read.csv(file='Sentiment Analysis Dataset.csv', header = TRUE)
# applying the docVector function on each of the reviews
# storing the matrix of word vectors as temp
temp=t(sapply(text$SentimentText, docVectors))
# visualizing the word vectors output
View(temp)

这将导致以下输出:

图片

然后我们使用dim命令来审查temp,如下所示:

dim(temp)

这将导致以下输出:

1000 20

从输出中我们可以看到,我们为语料库中的每个评论创建了词向量。现在可以使用这个数据框通过机器学习算法构建分类模型。以下用于分类的代码与用于 BoW 方法的代码没有不同:

# splitting the dataset into train and test
temp_train=temp[1:800,]
temp_test=temp[801:1000,]
labels_train=as.factor(as.character(text[1:800,]$Sentiment))
labels_test=as.factor(as.character(text[801:1000,]$Sentiment))
# including the random forest library
library(randomForest)
# training a model using random forest classifier with training dataset
# observe that we are using 20 trees to create the model
rf_senti_classifier=randomForest(temp_train, labels_train,ntree=20)
print(rf_senti_classifier)

这将导致以下输出:

randomForest(x = temp_train, y = labels_train, ntree = 20)
               Type of random forest: classification
                     Number of trees: 20
No. of variables tried at each split: 4
        OOB estimate of  error rate: 44.25%
Confusion matrix:
    1   2 class.error
1 238 172   0.4195122
2 182 208   0.4666667

上述输出显示,随机森林模型对象已成功创建。当然,模型可以进一步改进;然而,我们在这里不会这么做,因为重点是展示如何利用词嵌入,而不是获得最佳性能的分类器。

接下来,我们使用以下代码利用随机森林模型对测试数据进行预测,并报告性能:

# making predictions on the dataset
rf_predicts<-predict(rf_senti_classifier, temp_test)
library(rminer)
print(mmetric(rf_predicts, labels_test, c("ACC")))

这将导致以下输出:

[1] 62.5

我们看到,使用从路透社新闻组数据集制作的预训练word2vec嵌入,我们得到了 62%的准确率。

使用 GloVe 词嵌入构建文本情感分类器

斯坦福大学的 Pennington 等人开发了一种word2vec方法的扩展,称为全局词表示GloVe),用于高效地学习词向量。

GloVe 结合了矩阵分解技术的全局统计,如 LSA,以及word2vec中的基于局部上下文的学习。与word2vec不同,GloVe 不是使用窗口来定义局部上下文,而是通过整个文本语料库的统计来构建一个显式的词上下文或词共现矩阵。因此,学习模型产生了通常更好的词嵌入。

R 中的text2vec库有一个 GloVe 实现,我们可以用它来训练,从自己的训练语料库中获得词嵌入。或者,可以下载预训练的 GloVe 词嵌入并重复使用,就像我们在上一节中提到的早期word2vec预训练嵌入项目中所做的那样。

以下代码块展示了如何创建和使用 GloVe 词嵌入进行情感分析,或者,实际上,任何文本分类任务。我们不会明确讨论涉及的步骤,因为代码已经对每个步骤进行了详细的注释:

# including the required library
library(text2vec)
# setting the working directory
setwd('/home/sunil/Desktop/sentiment_analysis/')
# reading the dataset
text = read.csv(file='Sentiment Analysis Dataset.csv', header = TRUE)
# subsetting only the review text so as to create Glove word embedding
wiki = as.character(text$SentimentText)
# Create iterator over tokens
tokens = space_tokenizer(wiki)
# Create vocabulary. Terms will be unigrams (simple words).
it = itoken(tokens, progressbar = FALSE)
vocab = create_vocabulary(it)
# consider a term in the vocabulary if and only if the term has appeared aleast three times in the dataset
vocab = prune_vocabulary(vocab, term_count_min = 3L)
# Use the filtered vocabulary
vectorizer = vocab_vectorizer(vocab)
# use window of 5 for context words and create a term co-occurance matrix
tcm = create_tcm(it, vectorizer, skip_grams_window = 5L)
# create the glove embedding for each each in the vocab and
# the dimension of the word embedding should set to 50
# x_max is the maximum number of co-occurrences to use in the weighting
# function
# note that training the word embedding is time consuming - be patient
glove = GlobalVectors$new(word_vectors_size = 50, vocabulary = vocab, x_max = 100)
wv_main = glove$fit_transform(tcm, n_iter = 10, convergence_tol = 0.01)

这将导致以下输出:

INFO [2018-10-30 06:58:14] 2018-10-30 06:58:14 - epoch 1, expected cost 0.0231
INFO [2018-10-30 06:58:15] 2018-10-30 06:58:15 - epoch 2, expected cost 0.0139
INFO [2018-10-30 06:58:15] 2018-10-30 06:58:15 - epoch 3, expected cost 0.0114
INFO [2018-10-30 06:58:15] 2018-10-30 06:58:15 - epoch 4, expected cost 0.0100
INFO [2018-10-30 06:58:15] 2018-10-30 06:58:15 - epoch 5, expected cost 0.0091
INFO [2018-10-30 06:58:15] 2018-10-30 06:58:15 - epoch 6, expected cost 0.0084
INFO [2018-10-30 06:58:16] 2018-10-30 06:58:16 - epoch 7, expected cost 0.0079
INFO [2018-10-30 06:58:16] 2018-10-30 06:58:16 - epoch 8, expected cost 0.0074
INFO [2018-10-30 06:58:16] 2018-10-30 06:58:16 - epoch 9, expected cost 0.0071
INFO [2018-10-30 06:58:16] 2018-10-30 06:58:16 - epoch 10, expected cost 0.0068

以下使用glove模型来获取组合词向量:

# Glove model learns two sets of word vectors - main and context.
# both matrices may be added to get the combined word vector
wv_context = glove$components
word_vectors = wv_main + t(wv_context)
# converting the word_vector to a dataframe for visualization
word_vectors=data.frame(word_vectors)
# the word for each embedding is set as row name by default
# using the tibble library rownames_to_column function, the rownames is copied as first column of the dataframe
# we also name the first column of the dataframe as words
library(tibble)
word_vectors=rownames_to_column(word_vectors, var = "words")
View(word_vectors)

这将导致以下输出:

图片

我们使用softmaxreg库来获取每个评论的平均词向量。这与我们在上一节中使用的word2vec预训练嵌入类似。注意,我们将我们自己的训练词嵌入word_vectors传递给wordEmbed()函数,如下所示:

library(softmaxreg)
docVectors = function(x)
{
  wordEmbed(x, word_vectors, meanVec = TRUE)
}
# applying the function docVectors function on the entire reviews dataset
# this will result in word embedding representation of the entire reviews # dataset
temp=t(sapply(text$SentimentText, docVectors))
View(temp)

这将导致以下输出:

图片

现在,我们将数据集分成训练和测试部分,并使用randomforest库构建一个模型进行训练,如下面的代码行所示:

# splitting the dataset into train and test portions
temp_train=temp[1:800,]
temp_test=temp[801:1000,]
labels_train=as.factor(as.character(text[1:800,]$Sentiment))
labels_test=as.factor(as.character(text[801:1000,]$Sentiment))
# using randomforest to build a model on train data
library(randomForest)
rf_senti_classifier=randomForest(temp_train, labels_train,ntree=20)
print(rf_senti_classifier)

这将导致以下输出:

Call:
 randomForest(x = temp_train, y = labels_train, ntree = 20)
               Type of random forest: classification
                     Number of trees: 20
No. of variables tried at each split: 7

        OOB estimate of  error rate: 42.12%
Confusion matrix:
    1   2 class.error
1 250 160   0.3902439
2 177 213   0.4538462

然后,我们使用创建的随机森林模型来预测标签,如下所示:

# predicting labels using the randomforest model created
rf_predicts<-predict(rf_senti_classifier, temp_test)
# estimating the accuracy from the predictions
library(rminer)
print(mmetric(rf_predicts, labels_test, c("ACC")))

这将导致以下输出:

[1] 66.5

使用这种方法,我们获得了 66%的准确率。尽管词嵌入是从仅 1,000 个文本样本中的词获得的,但模型可能通过使用预训练嵌入进一步改进。使用预训练嵌入的整体框架与我们在上一节中word2vec项目中使用的相同。

使用 fastText 构建文本情感分类器

fastText是一个库,是word2vec的词表示扩展。它由 Facebook 研究团队在 2016 年创建。虽然 Word2vec 和 GloVe 方法将词作为训练的最小单元,但 fastText 将词分解成多个 n-gram,即子词。例如,单词 apple 的三元组是 app、ppl 和 ple。单词 apple 的词嵌入是所有词 n-gram 的总和。由于算法嵌入生成的性质,fastText 更占用资源,并且需要额外的时间来训练。fastText的一些优点如下:

  • 它为罕见词(包括拼写错误的词)生成更好的词嵌入。

  • 对于不在词汇表中的词,fastText 可以从其字符 n-gram 中构建一个词的向量,即使这个词没有出现在训练语料库中。Word2vec 和 GloVe 都不具备这种可能性。

fastTextR库为 fastText 提供了一个接口。让我们利用fastTextR库在我们的项目中构建一个基于亚马逊评论的情感分析引擎。虽然我们可以下载预训练的 fastText 词嵌入并用于我们的项目,但让我们尝试根据我们手头的评论数据集训练一个词嵌入。需要注意的是,使用 fastText 预训练词嵌入的方法与我们之前处理过的基于word2vec的项目中采用的方法相似。

与前一小节中介绍的项目类似,代码中包含内联注释。这些注释解释了每一行,指出了在本项目中构建亚马逊评论情感分析器所采取的方法。现在让我们看看以下代码:

# loading the required libary
library(fastTextR)
# setting the working directory
setwd('/home/sunil/Desktop/sentiment_analysis/')
# reading the input reviews file
# recollect that fastText needs the file in a specific format and we created one compatiable file in
# "Understanding the Amazon Reviews Dataset" section of this chaptertext = readLines("Sentiment Analysis Dataset_ft.txt")
# Viewing the text vector for conformation
View(text)

这将产生以下输出:

现在,让我们将评论分为训练和测试数据集,并使用以下代码行查看它们:

# dividing the reviews into training and test
temp_train=text[1:800]temp_test=text[801:1000]
# Viewing the train datasets for confirmation
View(temp_train)

这将给出以下输出:

使用以下代码查看测试数据集:

View(temp_test)

这将给出以下输出:

我们现在将使用以下代码为训练和测试数据集创建一个.txt文件:

# creating txt file for train and test dataset
# the fasttext function expects files to be passed for training and testing
fileConn<-file("/home/sunil/Desktop/sentiment_analysis/train.ft.txt")
writeLines(temp_train, fileConn)
close(fileConn)
fileConn<-file("/home/sunil/Desktop/sentiment_analysis/test.ft.txt")
writeLines(temp_test, fileConn)
close(fileConn)
# creating a test file with no labels
# recollect the original test dataset has labels in it
# as the dataset is just a subset obtained from full dataset
temp_test_nolabel<- gsub("__label__1", "", temp_test, perl=TRUE)
temp_test_nolabel<- gsub("__label__2", "", temp_test_nolabel, perl=TRUE)

现在,我们将使用以下命令确认没有标签的测试数据集:

View(temp_test_nolabel)

这将产生以下输出:

现在,我们将没有标签的测试数据集写入文件,以便我们可以用它进行测试,如下所示:

fileConn<-file("/home/sunil/Desktop/sentiment_analysis/test_nolabel.ft.txt")
writeLines(temp_test_nolabel, fileConn)
close(fileConn)
# training a supervised classification model with training dataset file
model<-fasttext("/home/sunil/Desktop/sentiment_analysis/train.ft.txt",
method = "supervised", control = ft.control(nthreads = 3L))
# Obtain all the words from a previously trained model=
words<-get_words(model)
# viewing the words for confirmation. These are the set of words present  # in our training data
View(words)

这将产生以下输出:

现在,我们将从先前训练的模型中获取词向量,并查看训练数据集中每个词的词向量,如下所示:

# Obtain word vectors from a previously trained model.
word_vec<-get_word_vectors(model, words)
# Viewing the word vectors for each word in our training dataset
# observe that the word embedding dimension is 5
View(word_vec)

这将产生以下输出:

我们将在没有标签的测试数据集上预测评论的标签,并将其写入文件以供将来参考。然后,我们将预测结果放入数据框中,以计算性能并使用以下代码行查看准确率的估计:

# predicting the labels for the reviews in the no labels test dataset
# and writing it to a file for future reference
predict(model, newdata_file= "/home/sunil/Desktop/sentiment_analysis/test_nolabel.ft.txt",result_file="/home/sunil/Desktop/sentiment_analysis/fasttext_result.txt")
# getting the predictions into a dataframe so as to compute performance   # measurementft_preds<-predict(model, newdata_file= "/home/sunil/Desktop/sentiment_analysis/test_nolabel.ft.txt")
# reading the test file to extract the actual labels
reviewstestfile<
readLines("/home/sunil/Desktop/sentiment_analysis/test.ft.txt")
# extracting just the labels frm each line
library(stringi)
actlabels<-stri_extract_first(reviewstestfile, regex="\\w+")
# converting the actual labels and predicted labels into factors
actlabels<-as.factor(as.character(actlabels))
ft_preds<-as.factor(as.character(ft_preds))
# getting the estimate of the accuracy
library(rminer)
print(mmetric(actlabels, ft_preds, c("ACC")))

这将产生以下输出:

[1] 58

我们在评论数据上使用fastText方法达到了 58%的准确率。作为下一步,我们可以检查是否可以通过使用预训练的 fastText 词嵌入来进一步提高准确率。正如我们已经知道的,通过使用预训练嵌入来实现项目与我们在本章早期部分描述的word2vec项目中的实现并没有太大区别。区别仅仅在于,获得词嵌入的训练步骤需要被丢弃,本项目代码中的模型变量应该用预训练的词嵌入来初始化。

摘要

在本章中,我们学习了各种自然语言处理技术,包括 BoW、Word2vec、GloVe 和 fastText。我们构建了涉及这些技术的项目,以对亚马逊评论数据集进行情感分析。所构建的项目涉及两种方法,一种是利用预训练的词嵌入,另一种是从我们自己的数据集中构建词嵌入。我们尝试了这两种方法来表示文本,以便可以被机器学习算法消费,从而产生了能够执行情感分析的能力的模型。

在下一章中,我们将通过利用批发数据集来学习客户细分。我们将把客户细分视为一个无监督问题,并使用各种技术构建项目,以识别电子商务公司客户群内的固有群体。来吧,让我们一起探索使用机器学习构建电子商务客户细分引擎的世界!

第五章:使用批发数据进行的客户细分

在当今竞争激烈的世界里,一个组织的成功在很大程度上取决于它对其客户行为的理解程度。为了更好地调整组织努力以适应个人需求,理解每个客户都是一项非常昂贵的任务。根据组织的大小,这项任务可能也非常具有挑战性。作为替代方案,组织依赖于一种称为细分的方法,该方法试图根据已识别的相似性将客户分类到不同的群体中。客户细分的关键方面允许组织将其努力扩展到各种客户子集的个人需求(如果不仅仅是满足个人需求),从而获得更大的收益。

在本章中,我们将学习客户细分的概念和重要性。然后,我们将深入了解学习基于客户特征的客户子群体识别的各种机器学习ML)方法。我们将使用批发数据集实施几个项目,以了解细分技术的机器学习技术。在下一节中,我们将从学习客户细分的基础和实现细分所需的机器学习技术的基础开始。随着我们的进展,我们将涵盖以下主题:

  • 理解客户细分

  • 理解批发客户数据集和细分问题

  • 使用 DIANA 在批发客户数据中识别客户细分

  • 使用 AGNES 在批发客户数据中识别客户细分

理解客户细分

在基本层面上,客户细分或市场细分是将特定市场中的广泛潜在客户划分为特定的客户子群体,其中每个子群体都包含具有某些相似性的客户。以下图表展示了客户细分的正式定义,其中客户被划分为三个群体:

图片

描述客户细分定义的插图

客户细分需要组织收集有关客户的数据并分析它,以识别可用于确定子群体的模式。客户的细分可以通过与客户相关的多个数据点来实现。以下是一些数据点:

  • 人口统计学:这个数据点包括种族、民族、年龄、性别、宗教、教育水平、收入、生活阶段、婚姻状况、职业

  • 心理统计学:这个数据点包括生活方式、价值观、社会经济地位、个性

  • 行为学:这个数据点包括产品使用、忠诚度、意识、场合、知识、喜好和购买模式

在世界上有数十亿人口的情况下,有效地利用客户细分可以帮助组织缩小范围,仅针对对其业务有意义的客户群体,最终推动转化和收入。以下是一些组织通过识别客户细分所试图实现的具体目标:

  • 识别销售团队能够追求的更高比例的机会

  • 识别对产品有更高兴趣的客户群体,并根据高兴趣客户的需要定制产品

  • 开发针对特定客户群体的非常专注的营销信息,以推动对产品的更高质量的外部兴趣

  • 选择最适合各个细分市场的沟通渠道,这可能包括电子邮件、社交媒体、广播或其他方法,具体取决于细分市场

  • 专注于最有利可图的客户

  • 推销和交叉销售其他产品和服务

  • 测试定价选项

  • 识别新产品或服务的机会

当一个组织需要执行细分时,它通常可以寻找共同的特征,例如共享需求、共同兴趣、相似的生活方式,甚至相似的人口统计特征,并在客户数据中制定细分。不幸的是,创建细分并不那么简单。随着大数据的出现,组织现在可以查看数百个客户特征,以便制定细分。一个人或组织中的少数人去处理数百种类型的数据,找出它们之间的关系,然后根据每个数据点的不同值建立细分是不切实际的。这就是无监督机器学习,称为聚类,发挥作用的地方。

聚类是使用机器学习算法识别不同类型数据之间关系的机制,从而基于这些关系产生新的细分。简单来说,聚类找到数据点之间的关系,以便它们可以被细分。

术语聚类分析客户细分在机器学习从业者中密切相关,并且可以互换使用。然而,这两个术语之间存在一个重要的区别。

聚类是一种帮助组织根据相似性和统计关系整理数据的工具。聚类在指导开发合适的客户细分方面非常有帮助。它还提供了潜在目标客户的 useful 统计指标。虽然组织的目的是从数据中识别有效的客户细分,但仅仅在数据上应用聚类技术并将数据进行分组本身可能或可能不会提供有效的客户细分。这本质上意味着聚类得到的输出,即聚类,需要进一步分析以了解每个聚类的含义,然后确定哪些聚类可以用于下游活动,例如商业促销。以下是一个流程图,有助于我们理解聚类在客户细分过程中的作用:

图片

聚类在客户细分中的作用

现在我们已经了解到聚类是进行客户细分的基础,在本章的剩余部分,我们将讨论各种聚类技术,并围绕这些技术实施项目以创建客户细分。对于我们的项目,我们使用批发客户数据集。在深入项目之前,让我们了解数据集并执行探索性数据分析EDA)以更好地理解数据。

理解批发客户数据集和细分问题

UCI 机器学习仓库在archive.ics.uci.edu/ml/datasets/wholesale+customers提供了批发客户数据集。该数据集指的是批发分销商的客户。它包括各种产品类别的年度支出,以货币单位m.u.)表示。这些项目的目标是应用聚类技术来识别与某些商业活动相关的细分市场,例如推出营销活动。在我们实际使用聚类算法获取聚类之前,让我们首先读取数据并执行一些 EDA,以下代码块将帮助我们理解数据:

# setting the working directory to a folder where dataset is located
setwd('/home/sunil/Desktop/chapter5/')
# reading the dataset to cust_data dataframe
cust_data = read.csv(file='Wholesale_customers_ data.csv', header = TRUE)
# knowing the dimensions of the dataframe
print(dim(cust_data))
Output : 
440 8
# printing the data structure
print(str(cust_data))
'data.frame': 440 obs. of 8 variables:
 $ Channel : int 2 2 2 1 2 2 2 2 1 2 ...
 $ Region : int 3 3 3 3 3 3 3 3 3 3 ...
 $ Fresh : int 12669 7057 6353 13265 22615 9413 12126 7579...
 $ Milk : int 9656 9810 8808 1196 5410 8259 3199 4956...
 $ Grocery : int 7561 9568 7684 4221 7198 5126 6975 9426...
 $ Frozen : int 214 1762 2405 6404 3915 666 480 1669...
 $ Detergents_Paper: int 2674 3293 3516 507 1777 1795 3140 3321...
 $ Delicassen : int 1338 1776 7844 1788 5185 1451 545 2566...
# Viewing the data to get an intuition of the data 
View(cust_data)

这将给出以下输出:

图片

现在我们来检查数据集中是否有任何缺失字段的条目:

# checking if there are any NAs in data
print(apply(cust_data, 2, function (x) sum(is.na(x))))
Output :
Channel Region Fresh Milk 
0 0 0 0 
Grocery Frozen Detergents_Paper Delicassen

0 0 0 0 
# printing the summary of the dataset 
print(summary(cust_data))

这将给出以下输出:

Channel Region Fresh Milk 
 Min. :1.000 Min. :1.000 Min. : 3 Min. : 55 
 1st Qu.:1.000 1st Qu.:2.000 1st Qu.: 3128 1st Qu.: 1533 
 Median :1.000 Median :3.000 Median : 8504 Median : 3627 
 Mean :1.323 Mean :2.543 Mean : 12000 Mean : 5796 
 3rd Qu.:2.000 3rd Qu.:3.000 3rd Qu.: 16934 3rd Qu.: 7190 
 Max. :2.000 Max. :3.000 Max. :112151 Max. :73498 
 Grocery Frozen Detergents_Paper Delicassen 
 Min. : 3.0 Min. : 3.0 Min. : 3 Min. : 25.0 
 1st Qu.: 256.8 1st Qu.: 408.2 1st Qu.: 2153 1st Qu.: 742.2
 Median : 816.5 Median : 965.5 Median : 4756 Median : 1526.0
 Mean : 2881.5 Mean : 1524.9 Mean : 7951 Mean : 3071.9
 3rd Qu.: 3922.0 3rd Qu.: 1820.2 3rd Qu.:10656 3rd Qu.: 3554.2
 Max. :40827.0 Max. :47943.0 Max. :92780 Max. :60869.0

从 EDA 中,我们看到这个数据集中有 440 个观测值,并且有八个变量。数据集没有任何缺失值。尽管最后六个变量是批发商从批发商那里带来的商品,但前两个变量是因素(分类变量),代表购买的位置和渠道。在我们的项目中,我们打算根据不同产品的销售来识别细分市场,因此,数据中的位置和渠道变量并不很有用。让我们使用以下代码从数据集中删除它们:

# excluding the non-useful columns from the dataset
cust_data<-cust_data[,c(-1,-2)]
# verifying the dataset post columns deletion
dim(cust_data)

这给出了以下输出:

440 6

我们可以看到只保留了六列,这证实了非必需列的删除是成功的。从 EDA 代码的总结输出中,我们还可以观察到所有保留列的尺度是相同的,因此我们不需要显式地归一化数据。

可能需要注意的是,大多数聚类算法都涉及某种形式的距离计算(例如欧几里得、曼哈顿、Grower)。确保数据集列之间的尺度是一致的是非常重要的,以防止某个变量因为尺度较高而在距离计算中成为主导变量。在数据列中观察到不同尺度的情况下,我们将依赖诸如 Z 变换或 min-max 变换等技术。对数据进行这些技术之一的应用确保了数据集列的适当缩放,因此在使用聚类算法时,数据集中没有主导变量。

聚类算法强制在输入数据集中识别子组,即使没有集群存在。为了确保我们从聚类算法中获得有意义的集群输出,检查数据中是否存在集群是非常重要的。聚类趋势,或聚类分析的可行性,是识别数据集中是否存在集群的过程。给定一个输入数据集,这个过程确定它是否具有非随机或非均匀的数据结构分布,这将导致有意义的集群。Hopkins 统计量用于确定聚类趋势。它取值在 0 到 1 之间,如果 Hopkins 统计量的值接近 0(远低于 0.5),则表明数据集中存在有效的集群。接近 1 的 Hopkins 值表明数据集中存在随机结构。

factoextra 库有一个内置的 get_clust_tendency() 函数,该函数在输入数据集上计算 Hopkins 统计量。让我们将此函数应用于我们的批发数据集,以确定数据集是否适用于聚类。以下代码完成了 Hopkins 统计量的计算:

# setting the working directory to a folder where dataset is located
setwd('/home/sunil/Desktop/chapter5/')
# reading the dataset to cust_data dataframe
cust_data = read.csv(file='Wholesale_customers_ data.csv', header = TRUE)
# removing the non-required columns
cust_data<-cust_data[,c(-1,-2)]
# inlcuding the facto extra library 
library(factoextra)
# computing and printing the hopikins statistic
print(get_clust_tendency(cust_data, graph=FALSE,n=50,seed = 123))

这将给出以下输出:

$hopkins_stat
[1] 0.06354846

我们数据集的 Hopkins 统计输出非常接近 0,因此我们可以得出结论,我们有一个适合聚类练习的数据集。

聚类算法的分类

R 中现成的聚类算法有很多。然而,所有这些算法都可以分为以下两类:

  • 平面或划分算法:这些算法依赖于一个输入参数,该参数定义了在数据集中要识别的聚类数量。输入参数有时直接来自业务,或者可以通过某些统计方法建立。例如,肘部法

  • 层次算法:在这些算法中,聚类不是在单一步骤中确定的。它们涉及多个步骤,从包含所有数据点的单个聚类开始,到包含单个数据点的n个聚类。层次算法可以进一步分为以下两种类型:

    • 划分类型:一种自上而下的聚类方法,其中所有点最初被分配到一个单个聚类中。在下一步中,聚类被分割成两个最不相似的聚类。分割聚类的过程递归进行,直到每个点都有自己的聚类,例如,DIvisive ANAlysis(DIANA)聚类算法。

    • 聚合类型:一种自下而上的方法,在初始运行中,数据集中的每个点被分配n个独特的聚类,其中n等于数据集中的观测数。在下一个迭代中,最相似的聚类被合并(基于聚类之间的距离)。合并聚类的递归过程继续进行,直到我们只剩下一个聚类,例如,聚合嵌套(AGNES)算法。

如前所述,有众多聚类算法可供选择,我们将专注于使用每种聚类类型的一个算法来实现项目。我们将使用 k-means 算法来实现项目,它是一种平面或划分类型的聚类算法。然后我们将使用 DIANA 和 AGNES 进行客户细分,DIANA 和 AGNES 分别是划分和聚合类型的算法。

使用 k-means 聚类在批发客户数据中识别客户细分

k-means 算法可能是从划分聚类类型中最受欢迎和最常用的聚类方法。尽管我们通常称之为 k-means 聚类算法,但这个算法有多个实现,包括MacQueenLloyd 和 Forgy以及Hartigan-Wong算法。研究表明,在大多数情况下,Hartigan-Wong 算法的性能优于其他两种算法。R 中的 k-means 默认使用 Hartigan-Wong 实现。

k 均值算法需要将 k 值作为参数传递。该参数表示要使用输入数据创建的簇数。对于从业者来说,确定最佳 k 值通常是一个挑战。有时,我们可以去一家企业询问他们预计数据中有多少簇。企业的回答将直接转换为要输入算法的k参数值。然而,在大多数情况下,企业对簇数一无所知。在这种情况下,责任将落在机器学习从业者身上,他们需要确定 k 值。幸运的是,有几种方法可以确定这个值。这些方法可以分为以下两类:

  • 直接方法:这些方法依赖于优化一个标准,例如簇内平方和平均轮廓。这种方法包括V 肘方法V 轮廓方法

  • 测试方法:这些方法包括将证据与零假设进行比较。差距统计是这种方法的一个流行例子。

除了肘方法、轮廓方法和差距统计方法之外,还有 30 多种其他指数和方法被发表出来用于确定最佳簇数。我们不会深入探讨这些方法的理论细节,因为在一个章节中涵盖 30 种方法是不切实际的。然而,R 提供了一个名为NbClust的出色库函数,使我们能够一次性实现所有这些方法。NbClust函数功能强大,它通过改变簇数、距离度量以及聚类方法的全部组合来确定最佳簇数!一旦库函数计算了所有 30 个指数,就会在输出上应用多数规则来确定最佳簇数,即作为算法输入的 k 值。让我们使用以下代码块为我们的批发数据集实现NbClust以确定最佳 k 值:

# setting the working directory to a folder where dataset is located
setwd('/home/sunil/Desktop/chapter5/')
# reading the dataset to cust_data dataframe
cust_data = read.csv(file='Wholesale_customers_ data.csv', header = TRUE)
# removing the non-required columns
cust_data<-cust_data[,c(-1,-2)]
# including the NbClust library
library(NbClust)
# Computing the optimal number of clusters through the NbClust function with distance as euclidean and using kmeans 
NbClust(cust_data,distance="euclidean", method="kmeans")

这将给出以下输出:

******************************************************************* 
* Among all indices: 
* 1 proposed 2 as the best number of clusters 
* 11 proposed 3 as the best number of clusters 
* 2 proposed 4 as the best number of clusters 
* 1 proposed 5 as the best number of clusters 
* 4 proposed 8 as the best number of clusters 
* 1 proposed 10 as the best number of clusters 
* 1 proposed 12 as the best number of clusters 
* 1 proposed 14 as the best number of clusters 
* 1 proposed 15 as the best number of clusters 
                   ***** Conclusion ***** 
* According to the majority rule, the best number of clusters is 3 
******************************************************************* 

根据结论,我们看到可能用于我们问题的 k 值是3。此外,通过将 k 均值解中的簇数与组内总平方和绘制肘曲线可以帮助确定最佳簇数。k 均值由目标函数定义,该函数试图最小化所有簇内所有平方距离的总和(簇内距离)。在肘曲线绘制方法中,我们使用不同的 k 值计算簇内距离,并将不同 k 值的簇内距离绘制成图表。肘曲线的弯曲处表明了对于数据集而言的 k 值是最佳的。在 R 中,可以使用以下代码块获得肘曲线:

# computing the the intra-cluster distance with Ks ranging from 2 to 10
library(purrr)
tot_withinss <- map_dbl(2:10, function(k){
  model <- kmeans(cust_data, centers = k, nstart = 50)
  model$tot.withinss
})
# converting the Ks and computed intra-cluster distances to a dataframe
screeplot_df <- data.frame(k = 2:10,

                           tot_withinss = tot_withinss)
# plotting the elbow curve
library(ggplot2)
print( ggplot(screeplot_df, aes(x = k, y = tot_withinss)) + 
         geom_line() + 
         scale_x_continuous(breaks = 1:10) + 
         labs(x = "k", y = "Within Cluster Sum of Squares") + 
         ggtitle("Total Within Cluster Sum of Squares by # of Clusters (k)") +
         geom_point(data = screeplot_df[2,], aes(x = k, y = tot_withinss),
                    col = "red2", pch = 4, size = 7))

这将给出以下输出:

即使是肘部曲线法输出的结果,我们也可以看到,我们数据集的最佳聚类数量是3

NbClust函数中我们可以看到,我们使用了欧几里得距离作为距离。在NbClust函数中,我们可以使用多种距离类型(euclideanmaximummanhattancanberrabinaryminkowski)作为距离参数的值。让我们理解这个距离实际上意味着什么。我们已经知道,我们数据集中的每个观察值是由表示特征的值组成的。这本质上意味着我们数据集中的每个观察值都可以表示为多维空间中的点。如果我们说两个观察值相似,我们期望这两个点在多维空间中的距离较低,即这两个点在多维空间中彼此靠近。两点之间的高距离值表明它们非常不相似。

欧几里得、曼哈顿以及其他类型的距离度量是多维空间中两点之间距离度量的各种方式。每种距离度量都涉及一种特定的技术来计算两点之间的距离。曼哈顿和欧几里得中涉及的技术以及它们度量的区别在下图中展示:

图片

曼哈顿距离与欧几里得距离度量的区别

欧几里得距离度量平面上的最短距离,而曼哈顿度量是在允许水平或垂直移动的情况下最短路径。

例如,如果ab是两个点,其中a= (0,0)b = (3,4),那么看看以下内容:

  • dist_euclid (a,b) = sqrt(3²+4²) = 5

  • dist_manhattan(a,b) = 3+4 = 7

  • a=(a1,...,an), b=(b1,...,bn)(在n维度和点)

  • dist_euclid (a,b) = sqrt((a1-b1)² + ... + (an-bn)²)

  • dist_manhattan(a,b) = sum(abs(a1-b1) + ... + abs(an-bn))

这两种度量都测量最短路径,但欧几里得度量没有任何限制,而曼哈顿度量只允许除了一个维度外所有维度都保持恒定的路径。

同样,其他距离度量也涉及某种独特的方法来度量给定点之间的相似性。在本章中,我们不会详细讲解每种技术,但需要理解的是,距离度量基本上定义了给定观察之间的相似程度。需要注意的是,距离度量不仅用于NbClust,还用于多个机器学习算法,包括 k-means。

现在我们已经学习了识别 k 值的各种方法,并将它们应用于识别批发数据集的最佳聚类数量,接下来让我们用以下代码实现 k-means 算法:

library(cluster)
# runing kmeans in cust_data dataset to obtain 3 clusters
kmeansout <- kmeans(cust_data, centers = 3, nstart = 50) 
print (kmeansout)

这将产生以下输出:

> kmeansout
K-means clustering with 3 clusters of sizes 330, 50, 60
Cluster means:
     Fresh Milk Grocery Frozen Detergents_Paper Delicassen
1 8253.47 3824.603 5280.455 2572.661 1773.058 1137.497
2 8000.04 18511.420 27573.900 1996.680 12407.360 2252.020
3 35941.40 6044.450 6288.617 6713.967 1039.667 3049.467
Clustering vector:
  [1] 1 1 1 1 3 1 1 1 1 2 1 1 3 1 3 1 1 1 1 1 1 1 3 2 3 1 1 1 2 3 1 1 1 3 1 1 3 1 2 3 3 1 1 2 1 2 2 2 1 2 1 1 3 1
 [55] 3 1 2 1 1 1 1 2 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 2 2 3 1 3 1 1 2 1 1 1 1 1 1 1 1 1 1 3 1 1 1 1
[109] 1 2 1 2 1 1 1 1 1 1 1 1 1 1 1 1 3 3 1 1 1 3 1 1 1 1 1 1 1 1 1 1 1 3 3 1 1 2 1 1 1 3 1 1 1 1 1 2 1 1 1 1 1 1
[163] 1 2 1 2 1 1 1 1 1 2 1 2 1 1 3 1 1 1 1 3 1 3 1 1 1 1 1 1 1 1 1 1 1 1 3 1 1 1 2 2 3 1 1 2 1 1 1 2 1 2 1 1 1 1
[217] 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 3 1 1 1 1 1 1 3 3 3 1 1 1 1 1 1 1 1 1 2 1 3 1 3 1 1 3 3 1 1 3 1 1 2 2 1 2 1
[271] 1 1 1 3 1 1 3 1 1 1 1 1 3 3 3 3 1 1 1 3 1 1 1 1 1 1 1 1 1 1 1 2 1 1 2 1 2 1 1 2 1 3 2 1 1 1 1 1 1 2 1 1 1 1
[325] 3 3 1 1 1 1 1 2 1 2 1 3 1 1 1 1 1 1 1 2 1 1 1 3 1 2 1 2 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 3 1 1 1 1 1 1 3
[379] 1 1 3 1 3 1 2 1 1 1 1 1 1 1 1 3 1 1 1 1 1 1 1 3 3 3 1 1 3 2 1 1 1 1 1 1 1 1 1 1 2 1 1 1 3 1 1 1 1 3 1 1 1 1
[433] 1 1 1 3 3 2 1 1

从 k-means 的输出中,我们可以观察和推断关于输出聚类的几个方面。显然,形成了三个聚类,这与我们传递给算法的 k 参数一致。我们看到第一个聚类中有 330 个观测值,而第二个和第三个聚类较小,只有 50 和 60 个观测值。k-means 输出还为我们提供了聚类中心。中心点是特定聚类中所有点的代表。由于不可能研究分配给每个聚类的每个单独的观测值并确定聚类的业务特征,因此聚类中心可以用来代表聚类中的点。聚类中心帮助我们快速得出关于聚类内容定义的结论。k-means 输出还产生了每个观测值的聚类分配。我们批发数据集中的每个观测值都被分配到三个聚类中的一个(1,2,3)。

可以通过使用 factoextra 库中的 fviz_cluster() 函数来查看聚类结果。该函数提供了聚类的一个良好说明。如果有超过两个维度(变量),fviz_cluster 将执行 主成分分析 (PCA) 并根据解释大部分方差的前两个主成分来绘制观测值。可以通过以下代码创建聚类可视化:

library(factoextra)
fviz_cluster(kmout,data=cust_data)

这将生成以下图形作为输出:

图片

k-means 算法的工作原理

k-means 算法的执行涉及以下步骤:

  1. 从数据集中随机选择 k 个观测值作为初始聚类中心。

  2. 对于数据集中的每个观测值,执行以下操作:

    1. 计算观测值与每个聚类中心之间的距离。

    2. 识别与观测值距离最小的聚类中心。

    3. 将观测值分配到最近的中心。

  3. 将所有点分配给其中一个聚类中心后,计算新的聚类中心。这可以通过计算分配给聚类的所有点的平均值来完成。

  4. 重复执行 步骤 2步骤 3,直到聚类中心(均值)不再改变或达到用户定义的迭代次数。

在 k-means 中需要注意的一个关键问题是,初始步骤中簇质心的选择是随机的,簇分配是基于实际观测值与随机选择的簇质心之间的距离进行的。这本质上意味着,如果我们最初选择除所选观测值之外的观测值作为簇质心,我们将获得与我们所获得的簇不同的簇。在技术术语中,这被称为非全局最优解局部最优解cluster库的 k-means 函数具有nstart选项,它可以解决 k-means 算法中遇到的非全局最优解问题。

nstart选项使算法能够通过从数据集中抽取多个中心观测值来尝试多个随机起始点(而不是只有一个),然后检查簇的平方和,并继续使用最佳起始点,从而得到更稳定的输出。在我们的案例中,我们将nstart值设置为50,因此通过 50 个随机初始簇质心集来检查后,选择最佳起始点。以下图表描述了 k-means 聚类算法中涉及的高级步骤:

图片

k-means 聚类步骤

在监督机器学习(ML)方法中,例如分类,我们有真实标签,因此我们可以将我们的预测与真实标签进行比较,并报告我们分类的性能。与监督机器学习方法不同,在聚类中,我们没有真实标签。因此,根据聚类计算性能度量是一个挑战。

作为性能度量的替代,我们使用一个称为簇质量的伪度量。簇质量通常通过称为簇内距离和簇间距离的度量来计算,这些度量在以下图表中说明:

图片

已定义簇内距离和簇间距离

聚类任务的目的是获得高质量的簇。如果观测值之间的距离最小且簇之间的距离最大,则簇被称为高质量簇

存在多种测量簇间和簇内距离的方法:

  • 簇内距离:这个距离可以测量为簇中所有点对之间的(绝对/平方)距离的(总和、最小值、最大值或平均值),或者为(直径)——两个最远的点,或者为质心与簇中所有点之间的距离。

  • 簇间距离:这个距离是通过所有簇对之间的(平方)距离的总和来测量的,其中两个簇之间的距离是通过以下方式之一计算的:

    • 质心之间的距离

    • 最远点对之间的距离

    • 属于簇的最接近一对点的距离

不幸的是,我们无法精确地确定簇间距离和簇内距离的偏好值。轮廓指数是一个基于簇间距离和簇内距离的指标,可以轻松计算并易于解释。

轮廓指数是通过计算每个参与聚类练习的观测值的平均簇内距离 a 和平均最近簇距离 b 来计算的。一个观测值的轮廓指数由以下公式给出:

图片

在这里,b 是一个观测值与其不属于的最近簇之间的距离。

轮廓指数的值介于 [-1, 1] 之间。对于一个观测值,+1 的值表示该观测值远离其邻近簇,并且非常接近其分配到的簇。同样,-1 的值告诉我们该观测值比其分配到的簇更接近其邻近簇。0 的值表示它位于两个簇之间的距离边界上。+1 是理想的值,而 -1 是最不受欢迎的值。因此,值越高,簇的质量越好。

cluster 库提供了轮廓函数,可以轻松地应用于我们的 k-means 聚类输出,以了解形成簇的质量。以下代码计算了我们的三个簇的轮廓指数:

# computing the silhouette index for the clusters
si <- silhouette(kmout$cluster, dist(cust_data, "euclidean"))
# printing the summary of the computed silhouette index 
print(summary(si))

这将给出以下输出:

Silhouette of 440 units in 3 clusters from silhouette.default(x = kmout$cluster, dist = dist(cust_data, from "euclidean")) :
 Cluster sizes and average silhouette widths:
       60 50 330 
0.2524346 0.1800059 0.5646307 
Individual silhouette widths:
   Min. 1st Qu. Median Mean 3rd Qu. Max. 
-0.1544 0.3338 0.5320 0.4784 0.6743 0.7329 

正如我们所见,轮廓指数可以从 -1 到 +1 变化,后者更受欢迎。从输出结果来看,这些簇都是高质量的簇,因为平均宽度是一个接近 1 的正数,比 -1 更接近 1。

事实上,轮廓指数不仅可以用来衡量形成簇的质量,还可以用来计算 k 值。类似于肘部方法,我们可以遍历多个 k 值,然后识别出在整个簇中产生最大轮廓指数值的 k。然后可以使用识别出的 k 进行聚类。

文献中描述了大量的簇质量度量。轮廓指数只是我们在本章中介绍的一个度量,因为它在机器学习社区中非常受欢迎。clusterCrit 库提供了广泛的指标来衡量簇的质量。我们不会在这里探索其他簇质量指标,但感兴趣的读者应参考此库以获取有关如何计算簇质量的更多信息。

到目前为止,我们已经介绍了 k-means 聚类算法来识别聚类,但最初开始的原始细分任务并没有结束。细分进一步扩展到理解从聚类练习中形成的每个聚类对业务意味着什么。例如,我们使用从 k-means 获得的聚类质心,并尝试识别这些是什么:

Fresh Milk Grocery Frozen Detergents_Paper Delicatessen
1 8253.47 3824.603 5280.455 2572.661 1773.058 1137.497
2 8000.04 18511.420 27573.900 1996.680 12407.360 2252.020
3 35941.40 6044.450 6288.617 6713.967 1039.667 3049.467

这里是每个聚类的几个示例洞察:

  • 聚类 1 是低消费群体(平均消费:22,841.744),大部分消费分配到新鲜类别

  • 聚类 2 是高消费群体(平均消费:70,741.42),大部分消费在杂货类别

  • 聚类 3 是中等消费群体(平均消费:59,077.568),大部分消费在新鲜类别

现在,根据业务目标,可以选择一个或多个聚类进行目标定位。例如,如果目标是让高消费群体消费更多,可以推出针对在冷冻熟食店产品上的消费低于质心值(即冷冻1,996.680熟食店2,252.020)的聚类 2 个人的促销活动。

使用 DIANA 在批发客户数据中识别客户细分

当数据中不一定有圆形(或超球体)聚类时,层次聚类算法是一个很好的选择,并且我们事先也不知道聚类的数量。与平面或划分算法不同,使用层次聚类算法时,不需要在将算法应用于数据集之前决定并传递要形成的聚类数量。

层次聚类会产生一个树状图(树形图),可以直观地验证以轻松确定聚类数量。直观验证使我们能够在树状图上合适的位置进行切割。

这种聚类算法产生的结果具有可重复性,因为算法对距离度量的选择不敏感。换句话说,无论选择哪种距离度量,我们都会得到相同的结果。这种聚类也适用于更复杂的(二次)数据集,特别是用于探索聚类之间存在的层次关系。

分裂层次聚类,也称为DIvisive ANAlysisDIANA),是一种层次聚类算法,它采用自上而下的方法来识别给定数据集中的聚类。以下是 DIANA 识别聚类的步骤:

  1. 所有数据集的观测值都被分配到根节点,因此在初始步骤中只形成一个单一聚类。

  2. 在每次迭代中,最异质的聚类被分割成两个。

  3. 步骤 2重复进行,直到所有观测值都在它们自己的聚类中:

图片

分裂层次聚类算法的工作原理

一个明显的问题就是关于算法用来将簇分割成两个的技术。答案是它根据某些(不)相似性度量来执行。欧几里得距离用于测量两个给定点之间的距离。该算法通过基于所有数据点成对距离的最远距离度量来分割数据。链接定义了数据点距离的具体细节。下一图展示了 DIANA 在分割簇时考虑的各种链接方式。以下是一些考虑用于分割组的一些距离:

  • 单链接法: 最近距离或单链接

  • 完全链接法: 最远距离或完全链接

  • 平均链接法: 平均距离或平均链接

  • 重心链接法: 重心距离

  • Ward 方法: 最小化平方欧几里得距离和

看看以下图表以更好地理解前面的距离:

图片

展示 DIANA 使用的各种链接类型的示意图

通常,要使用的链接类型作为参数传递给聚类算法。cluster库提供了diana函数来执行聚类。让我们用以下代码在我们的批发数据集上应用它:

# setting the working directory to a folder where dataset is located
setwd('/home/sunil/Desktop/chapter5/')
# reading the dataset to cust_data dataframe
cust_data = read.csv(file='Wholesale_customers_ data.csv', header = TRUE)
# removing the non-required columns
cust_data<-cust_data[,c(-1,-2)]
# including the cluster library so as to make use of diana function
library(cluster)
# Compute diana()
cust_data_diana<-diana(cust_data, metric = "euclidean",stand = FALSE)
# plotting the dendogram from diana output
pltree(cust_data_diana, cex = 0.6, hang = -1,
       main = "Dendrogram of diana")
# Divise coefficient; amount of clustering structure found
print(cust_data_diana$dc)

这将给出以下输出:

> print(cust_data_diana$dc)
[1] 0.9633628

看看以下输出:

图片

plot.hclust()plot.dendrogram()函数也可以用于 DIANA 聚类输出。plot.dendrogram()生成的树状图遵循 DIANA 算法执行的分割的自然结构。使用以下代码生成树状图:

plot(as.dendrogram(cust_data_diana), cex = 0.6,horiz = TRUE)

这将给出以下输出:

图片

在树状图输出中,每个出现在右侧的叶子都与数据集中的每个观测相关。当我们从右向左遍历时,相似的观测被分组到一个分支中,这些分支本身在更高的级别上融合。

水平轴上提供的融合级别表示两个观测之间的相似性。融合级别越高,观测之间的相似性就越大。需要注意的是,关于两个观测之间接近性的结论只能基于包含这两个观测的分支首次融合的级别得出。为了识别簇,我们可以在树状图的一定级别处进行切割。切割所进行的级别定义了获得的簇的数量。

我们可以使用cutree()函数来获取数据集中每个观测的簇分配。执行以下代码以获取簇并审查聚类输出:

# obtain the clusters through cuttree
# Cut tree into 3 groups
grp <- cutree(cust_data_diana, k = 3)
# Number of members in each cluster
table(grp)
# Get the observations of cluster 1
rownames(cust_data)[grp == 1]

这将给出以下输出:

> table(grp)
grp
  1 2 3 
364 44 32 
> rownames(cust_data)[grp == 1]
  [1] "1" "2" "3" "4" "5" "6" "7" "8" "9" "11" "12" "13" "14" "15" "16"
"17" "18" "19" 
 [19] "20" "21" "22" "25" "26" "27" "28" "31" "32" "33" "34" "35" "36" "37" "38" "41" "42" "43" 
 [37] "45" "49" "51" "52" "54" "55" "56" "58" "59" "60" "61" "63" "64" "65" "67" "68" "69" "70" 
 [55] "71" "72" "73" "74" "75" "76" "77" "79" "80" "81" "82" "83" "84" "85" "89" "90" "91" "92" 
 [73] "94" "95" "96" "97" "98" "99" "100" "101" "102" "103" "105" "106" "107" "108" "109" "111" "112" "113"
 [91] "114" "115" "116" "117" "118" "119" "120" "121" "122" "123" "124" "127" "128" "129" "131" "132" "133" "134"
[109] "135" "136" "137" "138" "139" "140" "141" "142" "144" "145" "147" "148" "149" "151" "152" "153" "154" "155"
[127] "157" "158" "159" "160" "161" "162" "163" "165" "167" "168" "169" "170" "171" "173" "175" "176" "178" "179"
[145] "180" "181" "183" "185" "186" "187" "188" "189" "190" "191" "192" "193" "194" "195" "196" "198" "199" "200"
[163] "203" "204" "205" "207" "208" "209" "211" "213" "214" "215" "216" "218" "219" "220" "221" "222" "223" "224"
[181] "225" "226" "227" "228" "229" "230" "231" "232" "233" "234" "235" "236" "237" "238" "239" "241" "242" "243"
[199] "244" "245" "246" "247" "248" "249" "250" "251" "253" "254" "255" "257" "258" "261" "262" "263" "264" "265"
[217] "266" "268" "269" "270" "271" "272" "273" "275" "276" "277" "278" "279" "280" "281" "282" "284" "287" "288"
[235] "289" "291" "292" "293" "294" "295" "296" "297" "298" "299" "300" "301" "303" "304" "306" "308" "309" "311"
[253] "312" "314" "315" "316" "317" "318" "319" "321" "322" "323" "324" "325" "327" "328" "329" "330" "331" "333"
[271] "335" "336" "337" "338" "339" "340" "341" "342" "343" "345" "346" "347" "348" "349" "351" "353" "355" "356"
[289] "357" "358" "359" "360" "361" "362" "363" "364" "365" "366" "367" "368" "369" "370" "372" "373" "374" "375"
[307] "376" "377" "379" "380" "381" "382" "384" "385" "386" "387" "388" "389" "390" "391" "392" "393" "394" "395"
[325] "396" "397" "398" "399" "400" "401" "402" "403" "404" "405" "406" "407" "409" "410" "411" "412" "413" "414"
[343] "415" "416" "417" "418" "420" "421" "422" "423" "424" "425" "426" "427" "429" "430" "431" "432" "433" "434"
[361] "435" "436" "439" "440"

我们还可以通过factoextra库中的fviz_cluster函数可视化聚类输出。使用以下代码获取所需的可视化:

library(factoextra)
fviz_cluster(list(data = cust_data, cluster = grp))

这将给出以下输出:

图片

还可以在树状图中本身对簇进行着色编码。这可以通过以下代码实现:

plot(as.hclust(cust_data_diana))
rect.hclust(cust_data_diana, k = 4, border = 2:5)

这将给出以下输出:

图片

现在簇已经识别,我们讨论的评估簇质量(通过轮廓指数)的步骤也适用于此处。因为我们已经在 k-means 聚类算法下讨论了这一主题,所以我们不会在这里重复步骤。代码和输出解释与 k-means 下讨论的内容相同。

如前所述,簇的输出并不是我们手头上的客户细分练习的最终点。类似于我们在 k-means 算法下进行的讨论,我们可以分析 DIANA 簇的输出以识别有意义的细分,以便将这些业务目标推广到特定识别的细分中。

使用 AGNES 在批发客户数据中识别客户细分

AGNES 在聚类数据集时遵循自下而上的方法,与 DIANA 相反。以下图解说明了 AGNES 算法的聚类工作原理:

图片

聚类层次聚类算法的工作原理

除了 AGNES 遵循的从下而上的方法之外,算法背后的实现细节与 DIANA 相同;因此,我们在这里不会重复讨论概念。以下代码块使用 AGNES 将我们的批发数据集聚类成三个簇;它还创建了所形成的簇的可视化:

# setting the working directory to a folder where dataset is located
setwd('/home/sunil/Desktop/chapter5/')
# reading the dataset to cust_data dataframe
cust_data = read.csv(file='Wholesale_customers_ data.csv', header = TRUE)
# removing the non-required columns
cust_data<-cust_data[,c(-1,-2)]
# including the cluster library so as to make use of agnes function
library(cluster)
# Compute agnes()
cust_data_agnes<-agnes(cust_data, metric = "euclidean",stand = FALSE)
# plotting the dendogram from agnes output
pltree(cust_data_agnes, cex = 0.6, hang = -1,
       main = "Dendrogram of agnes")
# agglomerative coefficient; amount of clustering structure found
print(cust_data_agnes$ac)
plot(as.dendrogram(cust_data_agnes), cex = 0.6,horiz = TRUE)
# obtain the clusters through cuttree
# Cut tree into 3 groups
grp <- cutree(cust_data_agnes, k = 3)
# Number of members in each cluster
table(grp)
# Get the observations of cluster 1
rownames(cust_data)[grp == 1]
# visualization of clusters
library(factoextra)
fviz_cluster(list(data = cust_data, cluster = grp))
library(factoextra)
fviz_cluster(list(data = cust_data, cluster = grp))
plot(as.hclust(cust_data_agnes))
rect.hclust(cust_data_agnes, k = 3, border = 2:5)

这是你将获得的输出:

[1] 0.9602911
> plot(as.dendrogram(cust_data_agnes), cex = 0.6,horiz = FALSE)

查看以下屏幕截图:

图片

查看以下代码块:

> grp <- cutree(cust_data_agnes, k = 3)
> # Number of members in each cluster
> table(grp)
grp
  1 2 3
434 5 1 
> rownames(cust_data)[grp == 1]
  [1] "1" "2" "3" "4" "5" "6" "7" "8" "9" "10" "11" "12" "13" "14" "15" "16" "17" "18" 
 [19] "19" "20" "21" "22" "23" "24" "25" "26" "27" "28" "29" "30" "31" "32" "33" "34" "35" "36" 
 [37] "37" "38" "39" "40" "41" "42" "43" "44" "45" "46" "47" "49" "50" "51" "52" "53" "54" "55" 
 [55] "56" "57" "58" "59" "60" "61" "63" "64" "65" "66" "67" "68" "69" "70" "71" "72" "73" "74" 
 [73] "75" "76" "77" "78" "79" "80" "81" "82" "83" "84" "85" "88" "89" "90" "91" "92" "93" "94" 
 [91] "95" "96" "97" "98" "99" "100" "101" "102" "103" "104" "105" "106" "107" "108" "109" "110" "111" "112"
[109] "113" "114" "115" "116" "117" "118" "119" "120" "121" "122" "123" "124" "125" "126" "127" "128" "129" "130"
[127] "131" "132" "133" "134" "135" "136" "137" "138" "139" "140" "141" "142" "143" "144" "145" "146" "147" "148"
[145] "149" "150" "151" "152" "153" "154" "155" "156" "157" "158" "159" "160" "161" "162" "163" "164" "165" "166"
[163] "167" "168" "169" "170" "171" "172" "173" "174" "175" "176" "177" "178" "179" "180" "181" "183" "184" "185"
[181] "186" "187" "188" "189" "190" "191" "192" "193" "194" "195" "196" "197" "198" "199" "200" "201" "202" "203"
[199] "204" "205" "206" "207" "208" "209" "210" "211" "212" "213" "214" "215" "216" "217" "218" "219" "220" "221"
[217] "222" "223" "224" "225" "226" "227" "228" "229" "230" "231" "232" "233" "234" "235" "236" "237" "238" "239"
[235] "240" "241" "242" "243" "244" "245" "246" "247" "248" "249" "250" "251" "252" "253" "254" "255" "256" "257"
[253] "258" "259" "260" "261" "262" "263" "264" "265" "266" "267" "268" "269" "270" "271" "272" "273" "274" "275"
[271] "276" "277" "278" "279" "280" "281" "282" "283" "284" "285" "286" "287" "288" "289" "290" "291" "292" "293"
[289] "294" "295" "296" "297" "298" "299" "300" "301" "302" "303" "304" "305" "306" "307" "308" "309" "310" "311"
[307] "312" "313" "314" "315" "316" "317" "318" "319" "320" "321" "322" "323" "324" "325" "326" "327" "328" "329"
[325] "330" "331" "332" "333" "335" "336" "337" "338" "339" "340" "341" "342" "343" "344" "345" "346" "347" "348"
[343] "349" "350" "351" "352" "353" "354" "355" "356" "357" "358" "359" "360" "361" "362" "363" "364" "365" "366"
[361] "367" "368" "369" "370" "371" "372" "373" "374" "375" "376" "377" "378" "379" "380" "381" "382" "383" "384"
[379] "385" "386" "387" "388" "389" "390" "391" "392" "393" "394" "395" "396" "397" "398" "399" "400" "401" "402"
[397] "403" "404" "405" "406" "407" "408" "409" "410" "411" "412" "413" "414" "415" "416" "417" "418" "419" "420"
[415] "421" "422" "423" "424" "425" "426" "427" "428" "429" "430" "431" "432" "433" "434" "435" "436" "437" "438"
[433] "439" "440"

执行以下命令:

> fviz_cluster(list(data = cust_data, cluster = grp))

上述命令生成了以下输出:

图片

查看以下命令:

> plot(as.hclust(cust_data_agnes))
> rect.hclust(cust_data_agnes, k = 3, border = 2:5)

上述命令生成了以下输出:

图片

从 AGNES 聚类输出中我们可以看到,数据集的大量观测值被分配到一个簇中,而分配到其他簇的观测值非常少。这对于我们的细分下游练习来说并不是一个好的输出。为了获得更好的簇分配,你可以尝试使用除了 AGNES 算法当前使用的默认平均链接方法之外的其他簇链接方法。

摘要

在本章中,我们学习了分割的概念及其与聚类的关系,聚类是一种机器学习无监督学习技术。我们使用了从 UCI 仓库可用的批发数据集,并实现了使用 k-means、DIANA 和 AGNES 算法进行聚类。在本章的过程中,我们还研究了与聚类相关的各个方面,例如聚类的趋势、距离、链接度量以及确定正确聚类数量的方法,以及测量聚类输出的方法。我们还探讨了如何利用聚类输出进行客户细分。

计算机能否像人类一样看到并识别物体和生物?让我们在下一章中探索这个问题的答案。

第六章:使用深度神经网络进行图像识别

在 1966 年,麻省理工学院的 Seymour Papert 教授构想了一个名为夏季视觉项目的雄心勃勃的夏季项目。对于研究生来说,任务是将摄像头连接到计算机上,并使其能够理解它所看到的内容!我相信研究生完成这个项目会非常困难,因为即使今天,这个任务仍然只完成了一半。

当人类向外看时,能够识别他们所看到的物体。他们无需思考,就能将一只猫归类为猫,一只狗归类为狗,一株植物归类为植物,一个动物归类为动物——这是因为人类大脑从其广泛的先验学习数据库中提取知识。毕竟,作为人类,我们有数百万年的进化背景,使我们能够从我们所看到的事物中得出推论。计算机视觉处理的是复制人类视觉过程,以便将其传递给机器并自动化它们。

本章全部关于通过机器学习(ML)学习计算机视觉的理论和实现。我们将构建一个前馈深度学习网络和LeNet以实现手写数字识别。我们还将构建一个使用预训练的 Inception-BatchNorm 网络来识别图像中对象的工程项目。随着我们在本章的进展,我们将涵盖以下主题:

  • 理解计算机视觉

  • 通过深度学习实现计算机视觉

  • MNIST 数据集简介

  • 实现用于手写数字识别的深度学习网络

  • 使用预训练模型实现计算机视觉

技术要求

对于本章涵盖的项目,我们将使用一个非常流行的开源数据集 MNIST。我们将使用Apache MXNet,这是一个现代开源深度学习软件框架,用于训练和部署所需的深度神经网络。

理解计算机视觉

在当今世界,我们拥有先进的摄像头,它们在模仿人眼捕捉光和色彩方面非常成功;但在整个图像理解方面,正确地捕捉图像仅仅是第一步。在图像捕捉之后,我们需要启用能够解释所捕捉内容并围绕其构建上下文的技术。这就是人类大脑在眼睛看到某物时所做的事情。接下来是巨大的挑战:我们都知道,计算机将图像视为代表一系列颜色范围内强度的整数值的巨大堆叠,当然,计算机与图像本身没有关联的上下文。这就是机器学习(ML)发挥作用的地方。机器学习使我们能够为数据集训练一个上下文,这样它就能使计算机理解某些数字序列实际上代表什么对象。

计算机视觉是机器学习应用中新兴的领域之一。它可以在多个领域用于多种目的,包括医疗保健、农业、保险和汽车行业。以下是一些其最流行的应用:

  • 从医学图像中检测疾病,如 CT 扫描/MRI 扫描图像

  • 识别作物疾病和土壤质量以支持更好的作物产量

  • 从卫星图像中识别石油储备

  • 自动驾驶汽车

  • 监控和管理银屑病患者的皮肤状况

  • 区分杂草和作物

  • 面部识别

  • 从个人文件中提取信息,例如护照和身份证

  • 为无人机和飞机检测地形

  • 生物识别

  • 公共监控

  • 组织个人照片

  • 回答视觉问题

这只是冰山一角。说没有哪个领域我们不能找到计算机视觉的应用,并不夸张。因此,计算机视觉是机器学习从业者需要关注的重点领域。

利用深度学习实现计算机视觉

首先,让我们了解术语深度学习。它简单地说就是多层神经网络。多层使得深度学习成为神经网络的一种增强和强大的形式。人工神经网络ANNs)自 20 世纪 50 年代以来一直存在。它们一直被设计为两层;然而,深度学习模型是构建在多个隐藏层之上的。以下图表展示了一个假设的深度学习模型:

图片

深度学习模型—高级架构

神经网络在计算上很重,因此能够启用最多 22 个核心的中央处理器CPU)通常被认为是一个基础设施瓶颈,直到最近。这种基础设施限制也限制了神经网络解决现实世界问题的使用。然而,最近,具有数千个核心的图形处理单元GPU)的可用性,与 CPU 相比,提供了指数级强大的计算可能性。这极大地推动了深度学习模型的使用。

数据以多种形式存在,如表格、声音、HTML 文件、TXT 文件和图像。线性模型通常无法从非线性数据中学习。非线性算法,如决策树和梯度提升机,也无法很好地从这类数据中学习。另一方面,创建特征之间非线性交互的深度学习模型,在处理非线性数据时能提供更好的解决方案,因此它们已成为机器学习社区中首选的模型。

深度学习模型由一系列相互连接的神经元组成,这些神经元创建了神经网络架构。任何深度学习模型都将有一个输入层、两个或更多隐藏层(中间层)和一个输出层。输入层由与数据中输入变量数量相等的神经元组成。用户可以决定深度学习网络应该有多少个神经元和隐藏层。通常,这是通过用户通过交叉验证策略构建网络来优化的。神经元数量和隐藏层数量的选择代表了研究者的挑战。输出层中的神经元数量基于问题的结果来决定。例如,如果是回归问题,则只有一个输出神经元;对于分类问题,输出神经元等于涉及问题的类别数量。

卷积神经网络

深度学习算法有多种类型,我们在计算机视觉中一般使用的一种称为卷积神经网络CNN)。CNNs 将图像分解成像素的小组,然后通过应用过滤器对这些像素进行计算。然后将结果与它们已经了解的像素矩阵进行比较。这有助于 CNNs 为图像属于已知类别之一提供概率。

在前几层中,CNN 识别形状,如曲线和粗糙边缘,但经过几次卷积后,它们能够识别动物、汽车和人类等物体。

当 CNN 首次为可用数据构建时,网络的滤波器值是随机初始化的,因此它产生的预测大多是错误的。但是,然后它不断将其在标记数据集上的预测与实际值进行比较,更新滤波器值,并在每次迭代中提高 CNN 的性能。

CNNs 的层

一个 CNN 由一个输入层和一个输出层组成;它还有各种隐藏层。以下是一个 CNN 中的各种隐藏层:

  • 卷积:假设我们有一个以像素表示的图像,在深度学习中,卷积通常是一个 3 x 3 的小矩阵,我们将矩阵的每个元素与图像的 3 x 3 部分的每个元素相乘,然后将它们全部加起来,得到该点卷积的结果。以下图表说明了卷积在像素上的过程:

图片

在图像上的卷积应用

  • ReLU(修正线性单元):一个非线性激活,它丢弃输入矩阵中的负数。例如,假设我们有一个 3 x 3 的矩阵,其值在矩阵的单元格中为负数、零和正数。将此矩阵作为 ReLU 的输入,它将矩阵中的所有负数转换为零并返回 3 x 3 矩阵。ReLU 是一个可以作为 CNN 架构一部分定义的激活函数。以下图表展示了 ReLU 在 CNN 中的功能:

图片

CNN 中的 ReLU

  • 最大池化:最大池化可以在 CNN 架构中设置为一个层。它允许识别特定特征是否存在于前一层。它将输入矩阵中的最高值替换为最大值并给出输出。让我们考虑一个例子,给定一个 2 x 2 的最大池化层,输入一个 4 x 4 的矩阵,最大池化层将输入矩阵中的每个 2 x 2 替换为四个单元格中的最高值。因此获得的输出矩阵是非重叠的,它是一个具有降低分辨率的图像表示。以下图表说明了 CNN 中最大池化的功能:

图片

CNN 中最大池化层的功能

应用最大池化的原因有很多,例如减少参数数量和计算负载,消除过拟合,最重要的是,迫使神经网络看到更大的图景,因为在之前的层中,它专注于看到图像的片段。

  • 全连接层:也称为密集层,它涉及对层的输入向量进行线性操作。该层确保每个输入都通过权重与每个输出相连接。

  • Softmax:通常应用于深度神经网络最后一层的激活函数。在多类分类问题中,我们需要深度学习网络的全连接输出被解释为概率。数据中特定观察的总概率(对于所有类别)应加起来为 1,并且观察属于每个类别的概率应在 0 到 1 之间。因此,我们将全连接层的每个输出转换为总和中的一部分。然而,我们不是简单地做标准比例,而是出于一个非常具体的原因应用这个非线性指数函数:我们希望将最高输出尽可能接近 1,将较低输出尽可能接近 0。Softmax 通过将真实线性比例推向 1 或 0 来实现这一点。

以下图表说明了 softmax 激活函数:

图片

Softmax 激活函数

  • Sigmoid:这与 softmax 类似,但应用于二分类,例如猫与狗。使用这种激活函数,观察所属的类别被分配比其他类别更高的概率。与 softmax 不同,概率不需要加起来等于 1。

MXNet 框架简介

MXNet 是一个功能强大的开源深度学习框架,旨在简化深度学习算法的开发。它用于定义、训练和部署深度神经网络。MXNet 轻量级、灵活且超可扩展,即它允许快速模型训练并支持多种语言的灵活编程模型。现有深度学习框架(如 Torch7、Theano 和 Caffe)的问题在于,用户需要学习另一个系统或不同的编程风格。

然而,MXNet 通过支持多种语言(如 C++、Python、R、Julia 和 Perl)解决了这个问题,从而消除了用户学习新语言的需求;因此,他们可以使用该框架并简化网络定义。MXNet 模型能够适应小量的内存,并且可以轻松地在 CPU、GPU 以及多台机器上训练。mxnet包对 R 语言来说已经可用,安装详情可以在Apache 孵化器中查阅。

理解 MNIST 数据集

修改后的国家标准与技术研究院MNIST)是一个包含手写数字图像的数据集。这个数据集在机器学习社区中非常流行,用于实现和测试计算机视觉算法。MNIST 数据集是一个由 Yann LeCun 教授提供的开源数据集yann.lecun.com/exdb/mnist/,其中提供了表示训练集和测试集的单独文件。测试集和训练集的标签也作为单独的文件提供。训练集有 60,000 个样本,测试集有 10,000 个样本。

下图显示了 MNIST 数据集的一些样本图像。每个图像还附带一个标签,指示以下截图中显示的数字:

图片

MNIST 数据集的样本图像

前一图中显示的图像标签是5041。数据集中的每个图像都是灰度图像,并以 28 x 28 像素表示。以下截图显示了以像素表示的样本图像:

图片

MNIST 数据集的样本图像,以 28 * 28 像素表示

可以将 28 x 28 像素矩阵展平,表示为 784 个像素值的向量。本质上,训练数据集是一个 60,000 x 784 的矩阵,可以与 ML 算法一起使用。测试数据集是一个 10,000 x 784 的矩阵。可以使用以下代码从源下载训练和测试数据集:

# setting the working directory where the files need to be downloaded
setwd('/home/sunil/Desktop/book/chapter 6/MNIST')
# download the training and testing dataset from source
download.file("http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz","train-images-idx3-ubyte.gz")
download.file("http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz","train-labels-idx1-ubyte.gz")
download.file("http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz","t10k-images-idx3-ubyte.gz")
download.file("http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz","t10k-labels-idx1-ubyte.gz")
# unzip the training and test zip files that are downloaded
R.utils::gunzip("train-images-idx3-ubyte.gz")
R.utils::gunzip("train-labels-idx1-ubyte.gz")
R.utils::gunzip("t10k-images-idx3-ubyte.gz")
R.utils::gunzip("t10k-labels-idx1-ubyte.gz")

一旦数据下载并解压,我们将在工作目录中看到文件。然而,这些文件是二进制格式,不能通过常规的 read.csv 命令直接加载。以下自定义函数代码有助于从二进制文件中读取训练和测试数据:

# function to load the image files
load_image_file = function(filename) {
  ret = list()
  # opening the binary file in read mode 
  f = file(filename, 'rb')
  # reading the binary file into a matrix called x
 readBin(f, 'integer', n = 1, size = 4, endian = 'big')
 n = readBin(f, 'integer', n = 1, size = 4, endian = 'big')
 nrow = readBin(f, 'integer', n = 1, size = 4, endian = 'big')
 ncol = readBin(f, 'integer', n = 1, size = 4, endian = 'big')
 x = readBin(f, 'integer', n = n * nrow * ncol, size = 1, signed = FALSE)
  # closing the file
  close(f)
  # converting the matrix and returning the dataframe
  data.frame(matrix(x, ncol = nrow * ncol, byrow = TRUE))
}
# function to load label files
load_label_file = function(filename) {
  # reading the binary file in read mode
  f = file(filename, 'rb')
  # reading the labels binary file into y vector 
  readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  n = readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  y = readBin(f, 'integer', n = n, size = 1, signed = FALSE)
  # closing the file
  close(f)
  # returning the y vector
  y
}

可以使用以下代码调用这些函数:

# load training images data through the load_image_file custom function
train = load_image_file("train-images-idx3-ubyte")
# load  test data through the load_image_file custom function
test  = load_image_file("t10k-images-idx3-ubyte")
# load the train dataset labels
train.y = load_label_file("train-labels-idx1-ubyte")
# load the test dataset labels
test.y  = load_label_file("t10k-labels-idx1-ubyte")

在 RStudio 中,当我们执行代码时,我们可以在环境标签下看到 traintesttrain.ytest.y。这证实了数据集已成功加载,并且相应的数据框已创建,如下面的截图所示:

图片

一旦图像数据被加载到数据框中,它就变成了代表像素值的数字序列。以下是一个辅助函数,它将像素数据可视化为 RStudio 中的图像:

# helper function to visualize image given a record of pixels
show_digit = function(arr784, col = gray(12:1 / 12), ...) {
  image(matrix(as.matrix(arr784), nrow = 28)[, 28:1], col = col, ...)
}

show_digit() 函数可以像任何其他 R 函数一样调用,以数据框记录号作为参数。例如,以下代码块中的函数有助于将训练数据集中的 3 记录可视化为 RStudio 中的图像:

# viewing image corresponding to record 3 in the train dataset
show_digit(train[3, ])

这将给出以下输出:

图片

大卫·罗宾逊博士在他的博客 探索手写数字分类:MNIST 数据集的整洁分析 (varianceexplained.org/r/digit-eda/) 中,对 MNIST 数据集进行了美丽的探索性数据分析,这将帮助您更好地理解数据集。

实现手写数字识别的深度学习网络

mxnet 库提供了几个函数,使我们能够定义构成深度学习网络的层和激活。层的定义、激活函数的使用以及每个隐藏层中要使用的神经元数量通常被称为网络架构。决定网络架构更多的是一种艺术而非科学。通常,可能需要多次实验迭代来决定适合该问题的正确架构。我们称之为艺术,因为没有找到理想架构的精确规则。层的数量、这些层中的神经元类型以及层的类型很大程度上是通过试错来决定的。

在本节中,我们将构建一个具有三个隐藏层的简单深度学习网络。以下是我们的网络的一般架构:

  1. 输入层被定义为网络中的初始层。mx.symbol.Variable MXNet 函数定义了输入层。

  2. 在网络中定义了一个全连接层,也称为密集层,作为第一个隐藏层,具有 128 个神经元。这可以通过mx.symbol.FullyConnected MXNet 函数实现。

  3. ReLU 激活函数作为网络的一部分被定义。mx.symbol.Activation函数帮助我们定义网络中的 ReLU 激活函数。

  4. 定义第二个隐藏层;它是一个具有 64 个神经元的另一个密集层。这可以通过mx.symbol.FullyConnected函数实现,类似于第一个隐藏层。

  5. 在第二个隐藏层的输出上应用 ReLU 激活函数。这可以通过mx.symbol.Activation函数实现。

  6. 我们网络中的最后一个隐藏层也是一个全连接层,但只有十个输出(等于类别的数量)。这也可以通过mx.symbol.FullyConnected函数实现。

  7. 输出层需要被定义,并且这应该是每个类别的预测概率;因此,我们在输出层应用 softmax。mx.symbol.SoftmaxOutput函数使我们能够配置输出中的 softmax。

我们并不是说这是针对该问题的最佳网络架构,但这是我们打算构建以展示使用 MXNet 实现深度学习网络架构的架构。

现在我们已经有了蓝图,让我们深入编写以下代码块来构建网络:

# setting the working directory
setwd('/home/sunil/Desktop/book/chapter 6/MNIST')
# function to load image files
load_image_file = function(filename) {
  ret = list()
  f = file(filename, 'rb')
  readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  n    = readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  nrow = readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  ncol = readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  x = readBin(f, 'integer', n = n * nrow * ncol, size = 1, signed
= FALSE)
  close(f)
  data.frame(matrix(x, ncol = nrow * ncol, byrow = TRUE))
}
# function to load the label files
load_label_file = function(filename) {
  f = file(filename, 'rb')
  readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  n = readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  y = readBin(f, 'integer', n = n, size = 1, signed = FALSE)
  close(f)
  y }
# loading the image files
train = load_image_file("train-images-idx3-ubyte")
test  = load_image_file("t10k-images-idx3-ubyte")
# loading the labels
train.y = load_label_file("train-labels-idx1-ubyte")
test.y  = load_label_file("t10k-labels-idx1-ubyte")
# lineaerly transforming the grey scale image i.e. between 0 and 255 to # 0 and 1
train.x <- data.matrix(train/255)
test <- data.matrix(test/255)
# verifying the distribution of the digit labels in train dataset
print(table(train.y))
# verifying the distribution of the digit labels in test dataset
print(table(test.y))

这将给出以下输出:

train.y
   0    1    2   3    4    5    6    7    8    9 
5923 6742 5958 6131 5842 5421 5918 6265 5851 5949 

test.y
   0    1    2    3    4    5    6    7    8    9 
 980 1135 1032 1010  982  892  958 1028  974 1009 

现在,定义三个层并开始训练网络以获得类别概率,并确保使用以下代码块可重现结果:

# including the required mxnet library 
library(mxnet)
# defining the input layer in the network architecture
data <- mx.symbol.Variable("data")
# defining the first hidden layer with 128 neurons and also naming the # layer as fc1
# passing the input data layer as input to the fc1 layer
fc1 <- mx.symbol.FullyConnected(data, name="fc1", num_hidden=128)
# defining the ReLU activation function on the fc1 output and also # naming the layer as ReLU1
act1 <- mx.symbol.Activation(fc1, name="ReLU1", act_type="relu")
# defining the second hidden layer with 64 neurons and also naming the # layer as fc2
# passing the previous activation layer output as input to the
fc2 layer
fc2 <- mx.symbol.FullyConnected(act1, name="fc2", num_hidden=64)
# defining the ReLU activation function on the fc2 output and also 
# naming the layer as ReLU2
act2 <- mx.symbol.Activation(fc2, name="ReLU2", act_type="relu")
# defining the third and final hidden layer in our network with 10 
# neurons and also naming the layer as fc3
# passing the previous activation layer output as input to the
fc3 layer
fc3 <- mx.symbol.FullyConnected(act2, name="fc3", num_hidden=10)
# defining the output layer with softmax activation function to obtain # class probabilities 
softmax <- mx.symbol.SoftmaxOutput(fc3, name="sm")
# defining that the experiment should run on cpu
devices <- mx.cpu()
# setting the seed for the experiment so as to ensure that the results # are reproducible
mx.set.seed(0)
# building the model with the network architecture defined above
model <- mx.model.FeedForward.create(softmax, X=train.x, y=train.y,
ctx=devices, num.round=10, array.batch.size=100,array.layout ="rowmajor",
learning.rate=0.07, momentum=0.9,  eval.metric=mx.metric.accuracy,
initializer=mx.init.uniform(0.07), 
epoch.end.callback=mx.callback.log.train.metric(100))

这将给出以下输出:

Start training with 1 devices
[1] Train-accuracy=0.885783334343384
[2] Train-accuracy=0.963616671562195
[3] Train-accuracy=0.97510000983874
[4] Train-accuracy=0.980016676982244
[5] Train-accuracy=0.984233343303204
[6] Train-accuracy=0.986883342464765
[7] Train-accuracy=0.98848334223032
[8] Train-accuracy=0.990800007780393
[9] Train-accuracy=0.991300007204215
[10] Train-accuracy=0.991516673564911

要在测试数据集上进行预测并获取测试数据集中每个观察的标签,请使用以下代码块:

# making predictions on the test dataset
preds <- predict(model, test)
# verifying the predicted output
print(dim(preds))
# getting the label for each observation in test dataset; the
# predicted class is the one with highest probability
pred.label <- max.col(t(preds)) - 1
# observing the distribution of predicted labels in the test dataset
print(table(pred.label))

这将给出以下输出:

[1]    10 10000
pred.label
   0    1    2    3    4    5    6    7    8    9 
 980 1149 1030 1021 1001  869  960 1001  964 1025 

让我们使用以下代码检查模型的性能:

# obtaining the performance of the model
print(accuracy(pred.label,test.y))

这将给出以下输出:

Accuracy (PCC): 97.73% 
Cohen's Kappa: 0.9748 
Users accuracy: 
   0    1    2    3    4    5    6    7    8    9 
98.8 99.6 98.0 97.7 98.3 96.1 97.9 96.3 96.6 97.7 
Producers accuracy: 
   0    1    2    3    4    5    6    7    8    9 
98.8 98.3 98.2 96.7 96.4 98.6 97.7 98.9 97.6 96.2 
Confusion matrix 
   y
x      0    1    2    3    4    5    6    7    8    9
  0  968    0    1    1    1    2    3    1    2    1
  1    1 1130    3    0    0    1    3    8    1    2
  2    0    1 1011    2    2    0    0   11    3    0
  3    1    2    6  987    0   14    2    2    4    3
  4    1    0    2    1  965    2   10    3    6   11
  5    1    0    0    4    0  857    2    0    3    2
  6    5    2    3    0    4    5  938    0    3    0
  7    0    0    2    2    1    1    0  990    3    2
  8    1    0    4    8    0    5    0    3  941    2
  9    2    0    0    5    9    5    0   10    8  986

要可视化网络架构,请使用以下代码:

# Visualizing the network architecture
graph.viz(model$symbol)

这将给出以下输出:

图片

在基于 CPU 的笔记本电脑上运行简单的架构几分钟,并且付出最小的努力,我们能够在测试数据集上实现97.7%的准确率。深度学习网络能够通过观察它提供的输入图像来学习解释数字。通过改变架构或增加迭代次数,可以进一步提高系统的准确率。值得注意的是,在早期实验中,我们运行了 10 次迭代。

在模型构建过程中,可以通过num.round参数简单地修改迭代次数。关于最佳迭代次数,也没有硬性规定,所以这也是需要通过试错来确定的事情。让我们构建一个包含 50 次迭代的模型,并观察其对性能的影响。代码将与早期项目相同,只是在模型构建代码中进行了以下修改:

model <- mx.model.FeedForward.create(softmax, X=train.x, y=train.y,
ctx=devices, num.round=50, array.batch.size=100,array.layout ="rowmajor",
learning.rate=0.07, momentum=0.9,  eval.metric=mx.metric.accuracy,
initializer=mx.init.uniform(0.07), 
epoch.end.callback=mx.callback.log.train.metric(100))

注意到num.round参数现在设置为50,而不是之前的10

这将给出以下输出:

[35] Train-accuracy=0.999933333396912
[36] Train-accuracy=1
[37] Train-accuracy=1
[38] Train-accuracy=1
[39] Train-accuracy=1
[40] Train-accuracy=1
[41] Train-accuracy=1
[42] Train-accuracy=1
[43] Train-accuracy=1
[44] Train-accuracy=1
[45] Train-accuracy=1
[46] Train-accuracy=1
[47] Train-accuracy=1
[48] Train-accuracy=1
[49] Train-accuracy=1
[50] Train-accuracy=1
[1]    10 10000
pred.label
   0    1    2    3    4    5    6    7    8    9 
 992 1139 1029 1017  983  877  953 1021  972 1017 
Accuracy (PCC): 98.21% 
Cohen's Kappa: 0.9801 
Users accuracy: 
   0    1    2    3    4    5    6    7    8    9 
99.3 99.5 98.2 98.2 98.1 97.1 98.0 97.7 98.0 97.8 
Producers accuracy: 
   0    1    2    3    4    5    6    7    8    9 
98.1 99.1 98.4 97.5 98.0 98.7 98.5 98.3 98.3 97.1 
Confusion matrix 
   y
x      0    1    2    3    4    5    6    7    8    9
  0  973    0    2    2    1    3    5    1    3    2
  1    1 1129    0    0    1    1    3    2    0    2
  2    1    0 1013    1    3    0    0    9    2    0
  3    0    1    5  992    0   10    1    1    3    4
  4    0    0    2    0  963    2    7    1    1    7
  5    0    0    0    4    1  866    2    0    2    2
  6    2    2    1    0    3    5  939    0    1    0
  7    0    1    6    3    1    1    0 1004    2    3
  8    1    1    3    4    0    2    1    3  955    2
  9    2    1    0    4    9    2    0    7    5  987

我们可以从输出中观察到,训练数据集达到了 100%的准确率。然而,在测试数据集上,我们观察到的准确率为 98%。本质上,我们的模型在训练和测试数据集上都应该表现出相同的性能,才能被称为一个好的模型。不幸的是,在这种情况下,我们遇到了一种称为过拟合的情况,这意味着我们创建的模型泛化能力不好。换句话说,模型用太多的参数进行了训练,或者训练时间过长,仅针对训练数据集的数据变得过于专业化;结果,它在新数据上表现不佳。模型泛化是我们应该特别追求的目标。有一种称为dropout的技术可以帮助我们克服过拟合问题。

实现 dropout 以避免过拟合

Dropout 是在激活层之后的网络架构中定义的,它会随机将激活值设置为 0。换句话说,dropout 随机删除了神经网络的一部分,这使我们能够防止过拟合。当我们持续丢弃在过程中学习到的信息时,我们无法完全过拟合我们的训练数据。这允许我们的神经网络更好地学习泛化。

在 MXNet 中,可以使用mx.symbol.Dropout函数轻松地将 dropout 定义为网络架构的一部分。例如,以下代码定义了在第一个 ReLU 激活(act1)和第二个 ReLU 激活(act2)之后的 dropout:

dropout1 <- mx.symbol.Dropout(data = act1, p = 0.5)
dropout2 <- mx.symbol.Dropout(data = act2, p = 0.3)

data参数指定了 dropout 所接受的输入和p的值指定了要执行的 dropout 量。在dropout1的情况下,我们指定要丢弃 50%的权重。再次强调,关于应该包含多少 dropout 以及在哪一层,并没有硬性规定。这是需要通过试错来确定的事情。带有 dropout 的代码几乎与早期项目相同,只是现在包括了激活层之后的 dropout:

# code to read the dataset and transform it to train.x and train.y remains # same as earlier project, therefore that code is not shown here
# including the required mxnet library 
library(mxnet)
# defining the input layer in the network architecture
data <- mx.symbol.Variable("data")
# defining the first hidden layer with 128 neurons and also naming the # layer as fc1
# passing the input data layer as input to the fc1 layer
fc1 <- mx.symbol.FullyConnected(data, name="fc1", num_hidden=128)
# defining the ReLU activation function on the fc1 output and also naming the layer as ReLU1
act1 <- mx.symbol.Activation(fc1, name="ReLU1", act_type="relu")
# defining a 50% dropout of weights learnt
dropout1 <- mx.symbol.Dropout(data = act1, p = 0.5)
# defining the second hidden layer with 64 neurons and also naming the layer as fc2
# passing the previous dropout output as input to the fc2 layer
fc2 <- mx.symbol.FullyConnected(dropout1, name="fc2", num_hidden=64)
# defining the ReLU activation function on the fc2 output and also naming the layer as ReLU2
act2 <- mx.symbol.Activation(fc2, name="ReLU2", act_type="relu")
# defining a dropout with 30% weight drop
dropout2 <- mx.symbol.Dropout(data = act2, p = 0.3)
# defining the third and final hidden layer in our network with 10 neurons and also naming the layer as fc3
# passing the previous dropout output as input to the fc3 layer
fc3 <- mx.symbol.FullyConnected(dropout2, name="fc3", num_hidden=10)
# defining the output layer with softmax activation function to
obtain class probabilities 
softmax <- mx.symbol.SoftmaxOutput(fc3, name="sm")
# defining that the experiment should run on cpu
devices <- mx.cpu()
# setting the seed for the experiment so as to ensure that the results are reproducible
mx.set.seed(0)
# building the model with the network architecture defined above
model <- mx.model.FeedForward.create(softmax, X=train.x, y=train.y, ctx=devices, num.round=50, array.batch.size=100,array.layout = "rowmajor", learning.rate=0.07, momentum=0.9,  eval.metric=mx.metric.accuracy, initializer=mx.init.uniform(0.07), epoch.end.callback=mx.callback.log.train.metric(100))
# making predictions on the test dataset
preds <- predict(model, test)
# verifying the predicted output
print(dim(preds))
# getting the label for each observation in test dataset; the predicted class is the one with highest probability
pred.label <- max.col(t(preds)) - 1
# observing the distribution of predicted labels in the test
dataset
print(table(pred.label))
# including the rfUtilities library so as to use accuracy function
library(rfUtilities)
# obtaining the performance of the model
print(accuracy(pred.label,test.y))
# printing the network architecture
graph.viz(model$symbol) 

这将给出以下输出和可视化的网络架构:

[35] Train-accuracy=0.958950003186862
[36] Train-accuracy=0.958983335793018
[37] Train-accuracy=0.958083337446054
[38] Train-accuracy=0.959683336317539
[39] Train-accuracy=0.95990000406901
[40] Train-accuracy=0.959433337251345
[41] Train-accuracy=0.959066670437654
[42] Train-accuracy=0.960250004529953
[43] Train-accuracy=0.959983337720235
[44] Train-accuracy=0.960450003842513
[45] Train-accuracy=0.960150004227956
[46] Train-accuracy=0.960533337096373
[47] Train-accuracy=0.962033336758614
[48] Train-accuracy=0.96005000303189
[49] Train-accuracy=0.961366670827071
[50] Train-accuracy=0.961350003282229
[1]    10 10000
pred.label
   0    1    2    3    4    5    6    7    8    9 
 984 1143 1042 1022  996  902  954 1042  936  979 
Accuracy (PCC): 97.3% 
Cohen's Kappa: 0.97 
Users accuracy: 
   0    1    2    3    4    5    6    7    8    9 
98.7 98.9 98.1 97.6 98.2 97.3 97.6 97.4 94.3 94.7 
Producers accuracy: 
   0    1    2    3    4    5    6    7    8    9 
98.3 98.3 97.1 96.5 96.8 96.2 98.0 96.1 98.1 97.7 
Confusion matrix 
   y
x      0    1    2    3    4    5    6    7    8    9
  0  967    0    0    0    0    2    5    1    6    3
  1    0 1123    3    0    1    1    3    5    2    5
  2    1    2 1012    4    3    0    0   14    4    2
  3    2    1    4  986    0    6    1    3   12    7
  4    0    0    3    0  964    2    5    0    5   17
  5    2    3    0    9    0  868    7    0    9    4
  6    3    2    0    0    5    3  935    0    6    0
  7    4    1    9    4    3    3    0 1001    6   11
  8    1    3    1    2    1    3    2    1  918    4
  9    0    0    0    5    5    4    0    3    6  956

看一下下面的图:

从输出中我们可以看到,dropout 现在被包含在网络架构中。我们还观察到,与我们的初始项目相比,这个网络架构在测试数据集上的准确率更低。一个可能的原因是我们包含的 dropout 百分比(50%和 30%)太高。我们可以调整这些百分比,并重新构建模型以确定准确率是否会提高。然而,这个想法是演示 dropout 作为正则化技术的作用,以避免深度神经网络过拟合。

除了 dropout 之外,还有其他技术可以用来避免过拟合的情况:

  • 增加数据:添加更多训练数据。

  • 数据增强:通过应用翻转、扭曲、添加随机噪声和旋转等技术,合成地创建额外的数据。以下截图显示了应用数据增强后生成的样本图像:

图片

应用数据增强的样本图像

  • 降低网络架构的复杂性:更少的层、更少的 epoch 等。

  • 批量归一化:确保网络中生成的权重不会推得太高或太低的过程。这通常是通过从每一层的每个权重中减去该层所有权重的平均值并除以标准差来实现的。它能够防止过拟合,执行正则化,并显著提高训练速度。mx.sym.batchnorm()函数使我们能够在激活后定义批量归一化。

我们不会专注于开发另一个使用批量归一化的项目,因为在项目中使用此函数与我们之前项目中使用的其他函数非常相似。到目前为止,我们一直专注于通过增加 epoch 来提高模型的性能,另一个选择是尝试不同的架构,并评估它是否提高了测试数据集上的准确率。关于这一点,让我们探索 LeNet,它专门设计用于文档的光学字符识别。

使用 MXNet 库实现 LeNet 架构

在他们 1998 年的论文《基于梯度的学习应用于文档识别》中,LeCun 等人介绍了 LeNet 架构。

LeNet 架构由两套卷积、激活和池化层组成,随后是一个全连接层、激活、另一个全连接层,最后是一个 softmax 分类器。以下图表说明了 LeNet 架构:

图片

LeNet 架构

现在,让我们使用以下代码块在我们的项目中使用mxnet库实现 LeNet 架构:

## setting the working directory
setwd('/home/sunil/Desktop/book/chapter 6/MNIST')
# function to load image files
load_image_file = function(filename) {
  ret = list()
  f = file(filename, 'rb')
  readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  n    = readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  nrow = readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  ncol = readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  x = readBin(f, 'integer', n = n * nrow * ncol, size = 1, signed
= FALSE)
  close(f)
  data.frame(matrix(x, ncol = nrow * ncol, byrow = TRUE))
}
# function to load label files
load_label_file = function(filename) {
  f = file(filename, 'rb')
  readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  n = readBin(f, 'integer', n = 1, size = 4, endian = 'big')
  y = readBin(f, 'integer', n = n, size = 1, signed = FALSE)
  close(f)
  y
}
# load images
train = load_image_file("train-images-idx3-ubyte")
test  = load_image_file("t10k-images-idx3-ubyte")
# converting the train and test data into a format as required by LeNet
train.x <- t(data.matrix(train))
test <- t(data.matrix(test))
# loading the labels
train.y = load_label_file("train-labels-idx1-ubyte")
test.y  = load_label_file("t10k-labels-idx1-ubyte")
# linearly transforming the grey scale image i.e. between 0 and 255 to # 0 and 1
train.x <- train.x/255
test <- test/255
# including the required mxnet library 
library(mxnet)
# input
data <- mx.symbol.Variable('data')
# first convolution layer
conv1 <- mx.symbol.Convolution(data=data, kernel=c(5,5), num_filter=20)
# applying the tanh activation function
tanh1 <- mx.symbol.Activation(data=conv1, act_type="tanh")
# applying max pooling 
pool1 <- mx.symbol.Pooling(data=tanh1, pool_type="max", kernel=c(2,2), stride=c(2,2))
# second conv
conv2 <- mx.symbol.Convolution(data=pool1, kernel=c(5,5), num_filter=50)
# applying the tanh activation function again
tanh2 <- mx.symbol.Activation(data=conv2, act_type="tanh")
#performing max pooling again
pool2 <- mx.symbol.Pooling(data=tanh2, pool_type="max",
                           kernel=c(2,2), stride=c(2,2))
# flattening the data
flatten <- mx.symbol.Flatten(data=pool2)
# first fullconnected later
fc1 <- mx.symbol.FullyConnected(data=flatten, num_hidden=500)
# applying the tanh activation function
tanh3 <- mx.symbol.Activation(data=fc1, act_type="tanh")
# second fullconnected layer
fc2 <- mx.symbol.FullyConnected(data=tanh3, num_hidden=10)
# defining the output layer with softmax activation function to obtain # class probabilities 
lenet <- mx.symbol.SoftmaxOutput(data=fc2)
# transforming the train and test dataset into a format required by 
# MxNet functions
train.array <- train.x
dim(train.array) <- c(28, 28, 1, ncol(train.x))
test.array <- test
dim(test.array) <- c(28, 28, 1, ncol(test))
# setting the seed for the experiment so as to ensure that the
# results are reproducible
mx.set.seed(0)
# defining that the experiment should run on cpu
devices <- mx.cpu()
# building the model with the network architecture defined above
model <- mx.model.FeedForward.create(lenet, X=train.array, y=train.y,
ctx=devices, num.round=3, array.batch.size=100, learning.rate=0.05, 
momentum=0.9, wd=0.00001, eval.metric=mx.metric.accuracy, 
           epoch.end.callback=mx.callback.log.train.metric(100))
# making predictions on the test dataset
preds <- predict(model, test.array)
# getting the label for each observation in test dataset; the
# predicted class is the one with highest probability
pred.label <- max.col(t(preds)) - 1
# including the rfUtilities library so as to use accuracy
function
library(rfUtilities)
# obtaining the performance of the model
print(accuracy(pred.label,test.y))
# printing the network architecture
graph.viz(model$symbol,direction="LR")

这将给出以下输出和可视化的网络架构:

Start training with 1 devices
[1] Train-accuracy=0.678916669438283
[2] Train-accuracy=0.978666676680247
[3] Train-accuracy=0.98676667680343
Accuracy (PCC): 98.54% 
Cohen's Kappa: 0.9838 
Users accuracy: 
    0     1     2     3     4     5     6     7     8     9 
 99.8 100.0  97.0  98.4  98.9  98.2  98.2  98.7  98.2  97.8 
Producers accuracy: 
   0    1    2    3    4    5    6    7    8    9 
98.0 96.9 99.1 99.3 99.0 99.3 99.6 97.7 98.7 98.3  
Confusion matrix 
   y
x      0    1    2    3    4    5    6    7    8    9
  0  978    0    2    2    1    3    7    0    4    1
  1    0 1135   15    2    1    0    5    7    1    5
  2    0    0 1001    2    1    1    0    3    2    0
  3    0    0    0  994    0    5    0    1    1    0
  4    0    0    1    0  971    0    1    0    0    8
  5    0    0    0    3    0  876    2    0    1    0
  6    0    0    0    0    2    1  941    0    1    0
  7    1    0    7    1    3    1    0 1015    3    8
  8    1    0    6    1    1    1    2    1  956    0
  9    0    0    0    5    2    4    0    1    5  987

看一下以下图表:

图片

在我的 4 核 CPU 盒子上,代码运行时间不到 5 分钟,但仍然在测试数据集上仅用三个训练轮次就达到了 98%的准确率。我们还可以看到,在训练和测试数据集上我们都获得了 98%的准确率,这证实了没有过拟合。

我们看到tanh被用作激活函数;让我们实验一下,看看如果我们将其更改为 ReLU,它是否有任何影响。项目的代码将与之前的项目相同,只是我们需要找到并替换tanh为 ReLU。我们不会重复代码,因为只有以下几行与早期项目不同:

ReLU1 <- mx.symbol.Activation(data=conv1, act_type="relu")
pool1 <- mx.symbol.Pooling(data=ReLU1, pool_type="max",
                           kernel=c(2,2), stride=c(2,2))
ReLU2 <- mx.symbol.Activation(data=conv1, act_type="relu")
pool2 <- mx.symbol.Pooling(data=ReLU2, pool_type="max",
                           kernel=c(2,2), stride=c(2,2))
ReLU3 <- mx.symbol.Activation(data=conv1, act_type="relu")
fc2 <- mx.symbol.FullyConnected(data=ReLU3, num_hidden=10)

当你使用 ReLU 作为激活函数运行代码时,将得到以下输出:

Start training with 1 devices
[1] Train-accuracy=0.627283334874858
[2] Train-accuracy=0.979916676084201
[3] Train-accuracy=0.987366676231225
Accuracy (PCC): 98.36% 
Cohen's Kappa: 0.9818 
Users accuracy: 
   0    1    2    3    4    5    6    7    8    9 
99.8 99.7 97.9 99.4 98.6 96.5 97.7 98.2 97.4 97.9 
Producers accuracy: 
   0    1    2    3    4    5    6    7    8    9 
97.5 97.2 99.6 95.6 99.7 99.2 99.7 98.0 99.6 98.2 
Confusion matrix 
   y
x      0    1    2    3    4    5    6    7    8    9
  0  978    0    3    1    1    2   12    0    5    1
  1    1 1132    6    0    2    1    5   11    1    6
  2    0    0 1010    1    0    0    0    1    2    0
  3    0    2    4 1004    0   23    1    3    9    4
  4    0    0    1    0  968    0    1    0    0    1
  5    0    1    0    1    0  861    2    0    3    0
  6    0    0    0    0    0    3  936    0    0    0
  7    1    0    6    3    0    1    0 1010    1    9
  8    0    0    2    0    1    0    1    0  949    0
  9    0    0    0    0   10    1    0    3    4  988

使用 ReLU 作为激活函数时,我们没有看到准确率有显著提高。它保持在 98%,这与使用tanh激活函数获得的结果相同。

作为下一步,我们可以尝试通过增加额外的训练轮次来重建模型,看看是否可以提高准确率。或者,我们可以尝试调整卷积层中过滤器的数量和大小,看看会发生什么!进一步的实验还可以包括添加更多种类的层。除非我们进行实验,否则我们不知道结果会是什么!

使用预训练模型实现计算机视觉

在第一章《探索机器学习领域》中,我们提到了一个名为迁移学习的概念。其想法是将模型中学到的知识应用到另一个相关任务中。现在几乎所有的计算机视觉任务都使用了迁移学习。除非有大量的标记数据集可用于训练,否则很少从头开始训练模型。

通常情况下,在计算机视觉中,卷积神经网络(CNNs)试图在早期层检测边缘,在中层层检测形状,并在后期层检测一些特定任务的特征。无论 CNNs 要检测的图像是什么,早期和中层层的功能保持不变,这使得我们可以利用预训练模型获得的知识。通过迁移学习,我们可以重用早期和中层层,只需重新训练后期层。这有助于我们利用最初训练任务上的标记数据。

迁移学习提供了两个主要优势:它节省了我们的训练时间,并确保即使我们拥有的标记训练数据很少,我们也能有一个好的模型。

XceptionVGG16VGG19ResNet50InceptionV3InceptionResNetV2MobileNetDenseNetNASNetMobileNetV2QuocNetAlexNetInception(GoogLeNet)和BN-Inception-v2是一些广泛使用的预训练模型。虽然我们不会深入探讨这些预训练模型的细节,但本节的想法是通过 MXNet 利用预训练模型来实施一个检测图像(输入)内容的项目。

在本节中展示的代码中,我们使用了预训练的 Inception-BatchNorm 网络来预测图像的类别。在运行代码之前,需要将预训练模型下载到工作目录。模型可以从data.mxnet.io/mxnet/data/Inception.zip下载。让我们探索以下代码,使用inception_bn预训练模型标记一些测试图像:

# loading the required libraries
library(mxnet)
library(imager)
# loading the inception_bn model to memory
model = mx.model.load("/home/sunil/Desktop/book/chapter 6/Inception/Inception_BN", iteration=39)
# loading the mean image
mean.img = as.array(mx.nd.load("/home/sunil/Desktop/book/chapter 6/Inception/mean_224.nd")[["mean_img"]])
# loading the image that need to be classified
im <- load.image("/home/sunil/Desktop/book/chapter 6/image1.jpeg")
# displaying the image
plot(im)

这将产生以下输出:

图片

为了使用预训练模型处理图像并预测具有最高概率的图像 ID,我们使用以下代码:

# function to pre-process the image so as to be consumed by predict function that is using inception_bn model
preproc.image <- function(im, mean.image) {
  # crop the image
  shape <- dim(im)
  short.edge <- min(shape[1:2])
  xx <- floor((shape[1] - short.edge) / 2)
  yy <- floor((shape[2] - short.edge) / 2)
  cropped <- crop.borders(im, xx, yy)
  # resize to 224 x 224, needed by input of the model.
  resized <- resize(cropped, 224, 224)
  # convert to array (x, y, channel)
  arr <- as.array(resized) * 255
  dim(arr) <- c(224, 224, 3)
  # subtract the mean
  normed <- arr - mean.img
  # Reshape to format needed by mxnet (width, height, channel,
num)
  dim(normed) <- c(224, 224, 3, 1)
  return(normed)
}
# calling the image pre-processing function on the image to be classified
normed <- preproc.image(im, mean.img)
# predicting the probabilties of labels for the image using the pre-trained model
prob <- predict(model, X=normed)
# sorting and filtering the top three labels with highest
probabilities
max.idx <- order(prob[,1], decreasing = TRUE)[1:3]
# printing the ids with highest probabilities
print(max.idx)

这将产生以下输出,其中包含最高概率的 ID:

[1] 471 627 863

让我们使用以下代码打印出与最高概率预测 ID 对应的标签:

# loading the pre-trained labels from inception_bn model 
synsets <- readLines("/home/sunil/Desktop/book/chapter
6/Inception/synset.txt")
# printing the english labels corresponding to the top 3 ids with highest probabilities
print(paste0("Predicted Top-classes: ", synsets[max.idx]))

这将给出以下输出:

[1] "Predicted Top-classes: n02948072 candle, taper, wax light"        
[2] "Predicted Top-classes: n03666591 lighter, light, igniter, ignitor"
[3] "Predicted Top-classes: n04456115 torch"      

从输出结果中,我们可以看到它已经正确地标记了作为输入传递的图像。我们可以使用以下代码测试更多图像,以确认分类是否正确:

im2 <- load.image("/home/sunil/Desktop/book/chapter 6/image2.jpeg")
plot(im2)
normed <- preproc.image(im2, mean.img)
prob <- predict(model, X=normed)
max.idx <- order(prob[,1], decreasing = TRUE)[1:3]
print(paste0("Predicted Top-classes: ", synsets[max.idx]))

这将给出以下输出:

图片

看看以下代码:

[1] "Predicted Top-classes: n03529860 home theater, home theatre"   
[2] "Predicted Top-classes: n03290653 entertainment center"         [3] "Predicted Top-classes: n04404412 television, television system"

同样,我们可以使用以下代码尝试第三张图像:

# getting the labels for third image
im3 <- load.image("/home/sunil/Desktop/book/chapter
6/image3.jpeg")
plot(im3)
normed <- preproc.image(im3, mean.img)
prob <- predict(model, X=normed)
max.idx <- order(prob[,1], decreasing = TRUE)[1:3]
print(paste0("Predicted Top-classes: ", synsets[max.idx]))

这将给出以下输出:

图片

看看以下输出:

[1] "Predicted Top-classes: n04326547 stone wall" 
[2] "Predicted Top-classes: n03891251 park bench" 
[3] "Predicted Top-classes: n04604644 worm fence, snake fence, snake-rail fence, Virginia fence"

摘要

在本章中,我们学习了计算机视觉及其与深度学习的关联。我们探索了一种特定的深度学习算法,即计算机视觉中广泛使用的卷积神经网络(CNNs)。我们研究了名为 MXNet 的开源深度学习框架。在详细讨论 MNIST 数据集之后,我们使用各种网络架构构建了模型,并成功地对 MNIST 数据集中的手写数字进行了分类。在本章的结尾,我们深入探讨了迁移学习概念及其与计算机视觉的关联。本章最后一个项目使用 Inception-BatchNorm 预训练模型对图像进行了分类。

在下一章中,我们将探索一种称为自动编码器神经网络的非监督学习算法。我非常期待实现一个使用自动编码器捕获信用卡欺诈的项目。你准备好了吗?让我们出发吧!

第七章:使用自编码器进行信用卡欺诈检测

欺诈管理一直被银行和金融公司视为一个非常痛苦的问题。与卡片相关的欺诈已被证明对公司的对抗特别困难。芯片和 PIN 等技术已经可用,并被大多数信用卡系统供应商,如 Visa 和 MasterCard 所使用。然而,现有技术无法遏制 100%的信用卡欺诈。不幸的是,骗子想出了新的钓鱼方式来从信用卡用户那里获取密码。此外,像读卡器这样的设备使窃取信用卡数据变得轻而易举!

尽管有一些技术能力可以用来对抗信用卡欺诈,但《尼尔森报告》,这是一份覆盖全球支付系统的领先出版物,估计信用卡欺诈将在 2020 年激增至 320 亿美元(nilsonreport.com/upload/content_promo/The_Nilson_Report_10-17-2017.pdf)。为了了解估计的损失,这超过了可口可乐(20 亿美元)、沃伦·巴菲特的大都会公司(240 亿美元)和摩根大通(235 亿美元)最近公布的利润!

虽然提供信用卡芯片技术的公司一直在大量投资以推进技术以应对信用卡欺诈,但在本章中,我们将探讨机器学习是否以及如何帮助解决信用卡欺诈问题。随着我们进入本章,我们将涵盖以下主题:

  • 信用卡欺诈检测中的机器学习

  • 自编码器和各种类型

  • 信用卡欺诈数据集

  • 在 R 中使用 H2O 库构建 AE

  • 使用自编码器进行信用卡欺诈检测的实施

信用卡欺诈检测中的机器学习

欺诈检测的任务通常归结为异常检测,其中数据集被验证以寻找数据中的潜在异常。传统上,这项任务被认为是一项手动任务,风险专家会手动检查所有交易。尽管存在技术层,但它纯粹基于基于规则的系统,该系统扫描每一笔交易,然后将列为可疑的交易提交人工审查以对交易做出最终决定。然而,这个系统存在一些主要的缺点:

  • 组织需要为人工审查人员设立大量的欺诈管理预算。

  • 对作为人工审查人员工作的员工进行广泛的培训是必要的。

  • 培训人员手动审查交易既耗时又昂贵。

  • 即使是最受培训的人工审查人员也携带某些偏见,因此使整个审查系统不准确。

  • 人工审查增加了完成交易所需的时间。顾客可能会因为通过信用卡交易所需的长等待时间而感到沮丧。这可能会影响顾客的忠诚度。

  • 人工审查可能会导致假阳性。假阳性不仅会影响过程中的销售,还会影响客户产生的终身价值。

幸运的是,随着机器学习ML)、人工智能AI)和深度学习的兴起,在很大程度上自动化了人工信用卡交易审查过程成为可能。这不仅节省了大量劳动力,而且还能更好地检测信用卡欺诈,否则由于人工审查者携带的偏见,欺诈检测可能会受到影响。

基于机器学习的欺诈检测策略通常可以使用监督机器学习和无监督机器学习技术来完成。

当有大量标记为真实欺诈的交易数据可用时,通常使用监督机器学习模型。模型在标记的数据集上训练,然后使用得到的模型将任何新的信用卡交易分类到两个可能的类别之一。

在大多数组织中,问题在于标记的数据不可用,或者可用的标记数据非常少。这使得监督学习模型不太可行。这就是无监督模型发挥作用的地方。它们被设计用来在交易中识别异常行为,并且它们不需要显式的预标记数据来识别异常行为。在无监督欺诈检测中的基本思想是通过识别不符合大多数交易的交易来检测行为异常。

另一点需要记住的是,欺诈事件是罕见的,并不像真实交易那样普遍。由于欺诈的罕见性,数据集中可能会出现严重的类别不平衡问题。换句话说,人们会观察到数据集中 95%或更多的数据是真实交易,而少于 5%的数据属于欺诈交易。此外,即使你今天了解到了一个欺诈交易,模型明天可能也会因为不同的特征而面临异常。因此,真实交易的问题空间是众所周知的,并且几乎是停滞不前的;然而,欺诈交易的问题空间并不为人所知,并且不是恒定的。由于这些原因,用无监督学习而不是监督学习来处理欺诈检测问题是有意义的。

异常检测是一种无监督学习算法,也被称为单类分类算法。它区分正常异常观察。算法建立的关键原则是异常观察不符合数据集中其他常见观察的预期模式。因为它学习真实交易的模式,任何不符合这一模式的都被称为异常,因此也被视为欺诈交易。以下图示展示了在二维空间中的异常检测:

图片

在 2D 空间中展示的异常检测

异常的一个简单例子是识别时间序列中离均值(标准差)太远的点。以下图展示了在时间序列中被识别为异常的数据点:

图片

通过标准差识别的时间序列中的异常

在本章中,我们将集中精力研究一种称为AEs的无监督深度学习应用类型。

自动编码器解释

自动编码器AEs)是前馈和非循环类型的神经网络。它们的目标是将给定的输入复制到输出。自动编码器通过将输入压缩到低维摘要来工作。这个摘要通常被称为潜在空间表示。自动编码器试图从潜在空间表示中重建输出。编码器潜在空间表示解码器是构成自动编码器的三个部分。以下图展示了在从 MNIST 数据集样本中选取的样本上应用自动编码器的示例:

图片

在 MNIST 数据集样本上的自动编码器应用

自动编码器(AE)的编码器和解码器组件是完全连接的前馈网络。潜在空间表示中的神经元数量是一个需要作为构建 AE 的一部分传递的超参数。在潜在语义空间中决定的神经元或节点数量决定了在将实际输入图像压缩到潜在空间表示时获得的压缩量。自动编码器的一般架构如下所示:

图片

自动编码器的一般架构

给定的输入首先通过一个编码器,这是一个全连接的人工神经网络ANN)。编码器作用于输入并减少其维度,如超参数所指定。解码器是另一个全连接的 ANN,它拾取这个减少的输入(潜在空间表示)然后重建输出。目标是使输出输入相同。一般来说,编码器解码器的架构是镜像的。尽管没有这样的要求强制编码器解码器的架构必须相同,但通常是这样实践的。实际上,自动编码器(AE)的唯一要求是从给定的输入中获得相同的输出。任何介于两者之间的都可以根据构建 AE 的个人喜好和想法进行定制。

从数学上讲,编码器可以表示为:

图片

其中 x 是输入,h 是作用于输入以将其表示为简洁摘要格式的函数。另一方面,解码器可以表示为:

图片

虽然期望得到图片,但这并不总是如此,因为重建是从紧凑的摘要表示中完成的;因此,会出现某些错误。错误 e 是从原始输入 x 和重建输出 r 计算得出的,图片

然后,自动编码器网络通过减少均方误差MSE)来学习,错误被传播回隐藏层进行调整。解码器和编码器的权重是彼此的转置,这使得学习训练参数更快。编码器和解码器的镜像架构使得学习训练参数更快成为可能。在不同的架构中,权重不能简单地转置;因此,计算时间会增加。这就是为什么保持编码器和解码器的镜像架构的原因。

基于隐藏层的自动编码器类型

根据隐藏层的大小,自动编码器可以分为两种类型,欠完备的自动编码器过度完备的自动编码器

  • 欠完备的自动编码器:如果自动编码器只是学习将输入复制到输出,那么它就没有用了。想法是产生一个简洁的表示作为编码器的输出,这个简洁的表示应该包含输入的最有用特征。输入层达到的简洁程度由我们在潜在空间表示中使用的神经元或节点数量控制。这可以在构建自动编码器时作为一个参数设置。如果神经元的数量设置为比输入特征更少的维度,那么自动编码器被迫学习输入数据的大部分关键特征。潜在空间中神经元数量少于输入维度的架构称为欠完备的自动编码器。

  • 过度完备的自动编码器:在潜在空间中,神经元的数量可以等于或超过输入维度。这种架构被称为过度完备的自动编码器。在这种情况下,自动编码器不学习任何东西,只是将输入复制到潜在空间,然后通过解码器传播。

除了潜在空间中神经元的数量外,以下是一些可以在自动编码器(AE)架构中使用的其他参数:

  • 编码器和解码器中的层数:编码器和解码器的深度可以设置为任何数字。通常,在编码器和解码器的镜像架构中,层数设置为相同的数字。最后一张图展示了编码器和解码器中,除了输入和输出之外,都有两层自动编码器的示意图。

  • 编码器和解码器每层的神经元数量:在编码器中,每层的神经元数量随着层数的减少而减少,在解码器中,每层的神经元数量随着层数的增加而增加。编码器和解码器层的神经元是对称的。

  • 损失函数:自动编码器在反向传播过程中使用如均方误差(MSE)或交叉熵等损失函数来学习权重。如果输入范围在(0,1)之间,则使用交叉熵作为度量标准,否则使用均方误差。

基于约束的自动编码器类型

根据对损失的约束,自动编码器可以分为以下类型:

  • 简单自动编码器:这是可能的最简单的自动编码器架构,其中编码器和解码器都是全连接神经网络层。

  • 稀疏自动编码器:稀疏自动编码器是引入信息瓶颈的替代方法,无需减少我们隐藏层中的节点数量。而不是偏好欠完备的自动编码器,损失函数被构建成惩罚层内激活的方式。对于任何给定的观察值,网络被鼓励学习编码和解码,这仅依赖于激活少量神经元。

  • 去噪自动编码器:这是一种过完备的自动编码器,存在学习恒等函数零函数的风险。本质上,自动编码器学习到的输出等于输入,因此使自动编码器变得无用。去噪自动编码器通过随机初始化一些输入为 0 来避免学习恒等函数的问题。在损失函数的计算过程中,不考虑噪声引起的输入;因此,网络仍然学习正确的权重,而不存在学习恒等函数的风险。同时,自动编码器被训练学习从损坏的输入中重建输出。

以下图示是 MNIST 数据集样本图像上的去噪自动编码器的示例:

图片

在 MNIST 样本上应用去噪自动编码器

  • 卷积自动编码器:当处理图像作为输入时,可以使用卷积层作为编码器和解码器网络的一部分。这类使用卷积层的自动编码器被称为卷积自动编码器。以下图示展示了在自动编码器中使用卷积的示例:

图片

卷积自动编码器

  • 堆叠自动编码器:堆叠自动编码器是在编码器和解码器中都有多层的一种自动编码器。你可以参考自动编码器的一般架构作为堆叠自动编码器架构的示例说明,其中编码器和解码器都有两层(不包括输入和输出层)。

    • 变分自动编码器变分自动编码器VAE),而不是构建一个输出单个
  • e 值来描述每个潜在状态属性,为每个潜在属性描述一个概率分布。这使得设计复杂的数据生成模型和生成虚构名人图像以及数字艺术品成为可能。以下图示展示了 VAE 中数据的表示:

图片

在变分自动编码器(VAE)中,编码器模型有时被称为识别模型,而解码器模型有时被称为生成模型。编码器输出一系列关于潜在特征的统计分布。这些特征是随机采样的,并由解码器用于重建输入。对于任何潜在分布的采样,解码器都应能够准确地重建输入。因此,在潜在空间中彼此靠近的值应该对应于非常相似的重建。

自动编码器的应用

以下是一些自动编码器可能被使用的实际应用:

  • 图像着色:给定一个灰度图像作为输入,自动编码器可以自动着色图像,并以彩色图像作为输出。

  • 噪声去除:去噪自动编码器能够从图像中去除噪声,并在无噪声的情况下重建图像。例如,可以从视频和图像中去除水印的任务也可以完成。

  • 降维:自动编码器以压缩形式表示输入数据,但只关注关键特征。因此,像图像这样的东西可以用减少的像素表示,在图像重建过程中信息损失不大。

  • 图像搜索:这是根据给定的输入识别相似图像。

  • 信息检索:在从语料库检索信息时,自动编码器可以用来将属于给定输入的所有文档分组在一起。

  • 主题建模:变分自动编码器用于近似后验分布,并且已经成为推断文本文档潜在主题分布的有希望的替代方案。

我们已经涵盖了理解自动编码器及其应用所需的基本知识。让我们从高层次上了解我们将要使用自动编码器在信用卡欺诈检测问题上的解决方案。

信用卡欺诈数据集

通常在一个欺诈数据集中,我们对于负类(非欺诈/真实交易)有足够的数据,而对于正类(欺诈交易)则非常少或没有数据。这在机器学习领域被称为类别不平衡问题。我们在非欺诈数据上训练一个自动编码器(AE),并使用编码器学习特征。然后使用解码器在训练集上计算重建误差以找到阈值。这个阈值将用于未见过的数据(测试数据集或其他)。我们使用这个阈值来识别那些测试实例中值大于阈值的欺诈实例。

在本章的项目中,我们将使用以下 URL 源的数据集:essentials.togaware.com/data/。这是一个公开的信用卡交易数据集。该数据集最初通过研究论文 Calibrating Probability with Undersampling for Unbalanced Classification,A. Dal Pozzolo, O. Caelen, R. A Johnson 和 G. Bontempi,IEEE Symposium Series on Computational Intelligence (SSCI),南非开普敦,2015 年提供。数据集也可在此 URL 上找到:www.ulb.ac.be/di/map/adalpozz/data/creditcard.Rdata。该数据集是在 Worldline 和 ULB(Université Libre de Bruxelles)机器学习组(mlg.ulb.ac.be)在大数据挖掘和欺诈检测研究合作期间收集和分析的。

以下是数据集的特征:

  • 论文将数据集以 Rdata 文件的形式提供。该数据集的 CSV 转换版本也可在 Kaggle 以及其他网站上找到。

  • 它包含 2013 年 9 月欧洲持卡人用信用卡进行的交易。

  • 记录并呈现为数据集的是两天内发生的交易。

  • 数据集中总共有 284,807 笔交易。

  • 该数据集存在严重的类别不平衡问题。所有交易中只有 0.172% 是欺诈交易(492 笔欺诈交易)。

  • 数据集中总共有三十个特征,即 V1, V2, ..., V28, Time, 和 Amount

  • 变量 V1, V2, ..., V28 是从原始变量集中通过主成分分析(PCA)获得的。

  • 由于保密性,产生主成分的原始变量集没有公开。

  • Time 特征包含每个交易与数据集中第一个交易之间的秒数。

  • Amount 特征是交易金额。

  • 因变量名为 Class。欺诈交易在类别中表示为 1,真实交易表示为 0。

我们现在将进入使用自动编码器(AE)进行信用卡欺诈检测。

在 R 中使用 H2O 库构建自动编码器(AE)。

我们将使用 H2O 中可用的自动编码器实现来执行我们的项目。H2O 是一个完全开源、分布式、内存中的机器学习平台,具有线性可扩展性。它提供了一些最广泛使用的机器学习算法的并行化实现。它支持易于使用的、无监督的、非线性的自动编码器,作为其深度学习模型的一部分。H2O 的深度学习自动编码器基于多层神经网络架构,整个网络一起训练,而不是逐层堆叠。

可以使用以下命令在 R 中安装 h2o 包:

install.packages("h2o")

关于 H2O 在 R 中的安装和依赖的详细信息,可在以下 URL 找到:cran.r-project.org/web/packages/h2o/index.html

一旦成功安装了包,h2o包提供的函数,包括 AE,可以通过在 R 代码中包含以下行简单使用:

library(h2o)

在使用自动编码器(AE)编码我们的信用卡欺诈检测系统之前,我们需要做的一切。不再等待,让我们开始构建我们的代码来探索和准备我们的数据集,以及实现捕获欺诈性信用卡交易的 AE。

信用卡欺诈检测的自动编码器代码实现

如同所有其他项目一样,让我们首先将数据加载到 R 数据框中,然后执行 EDA 以更好地了解数据集。请注意,在代码中包含h2o以及doParallel库。这些包含使我们能够使用h2o库中的 AE,以及利用笔记本电脑/台式机上的多个 CPU 核心,如下所示:

# including the required libraries
library(tidyverse)
library(h2o)
library(rio)
library(doParallel)
library(viridis)
library(RColorBrewer)
library(ggthemes)
library(knitr)
library(caret)
library(caretEnsemble)
library(plotly)
library(lime)
library(plotROC)
library(pROC)

在本地主机上的端口54321初始化 H2O 集群。nthreads定义要使用的线程池数量,这接近要使用的 CPU 数量。在我们的情况下,我们说的是使用所有 CPU,我们还指定 H2O 集群使用的最大内存为8G

localH2O = h2o.init(ip = 'localhost', port = 54321, nthreads = -1,max_mem_size = "8G")
# Detecting the available number of cores
no_cores <- detectCores() - 1
# utilizing all available cores
cl<-makeCluster(no_cores)
registerDoParallel(cl)

你将得到与以下代码块中所示类似的输出:

H2O is not running yet, starting it now...
Note:  In case of errors look at the following log files:
    /tmp/RtmpKZvQ3m/h2o_sunil_started_from_r.out
    /tmp/RtmpKZvQ3m/h2o_sunil_started_from_r.err
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
Starting H2O JVM and connecting: ..... Connection successful!
R is connected to the H2O cluster:
    H2O cluster uptime:         4 seconds 583 milliseconds
    H2O cluster timezone:       Asia/Kolkata
    H2O data parsing timezone:  UTC
    H2O cluster version:        3.20.0.8
    H2O cluster version age:    2 months and 27 days 
    H2O cluster name:           H2O_started_from_R_sunil_jgw200
    H2O cluster total nodes:    1
    H2O cluster total memory:   7.11 GB
    H2O cluster total cores:    4
    H2O cluster allowed cores:  4
    H2O cluster healthy:        TRUE
    H2O Connection ip:          localhost
    H2O Connection port:        54321
    H2O Connection proxy:       NA
    H2O Internal Security:      FALSE
    H2O API Extensions:         XGBoost, Algos, AutoML, Core V3, Core V4
    R Version:                  R version 3.5.1 (2018-07-02)

现在,为了设置数据文件位置的当前工作目录,加载 Rdata 并将其读入数据框,并使用以下代码查看数据框:

# setting the working directory where the data file is location
setwd("/home/sunil/Desktop/book/chapter 7")
# loading the Rdata file and reading it into the dataframe called cc_fraud
cc_fraud<-get(load("creditcard.Rdata"))
# performing basic EDA on the dataset
# Viewing the dataframe to confirm successful load of the dataset
View(cc_fraud)

这将给出以下输出:

图片

现在,让我们使用以下代码打印数据框结构:

print(str(cc_fraud))

这将给出以下输出:

'data.frame':     284807 obs. of  31 variables:
 $ Time  : num  0 0 1 1 2 2 4 7 7 9 ...
 $ V1    : num  -1.36 1.192 -1.358 -0.966 -1.158 ...
 $ V2    : num  -0.0728 0.2662 -1.3402 -0.1852 0.8777 ...
 $ V3    : num  2.536 0.166 1.773 1.793 1.549 ...
 $ V4    : num  1.378 0.448 0.38 -0.863 0.403 ...
 $ V5    : num  -0.3383 0.06 -0.5032 -0.0103 -0.4072 ...
 $ V6    : num  0.4624 -0.0824 1.8005 1.2472 0.0959 ...
 $ V7    : num  0.2396 -0.0788 0.7915 0.2376 0.5929 ...
 $ V8    : num  0.0987 0.0851 0.2477 0.3774 -0.2705 ...
 $ V9    : num  0.364 -0.255 -1.515 -1.387 0.818 ...
 $ V10   : num  0.0908 -0.167 0.2076 -0.055 0.7531 ...
 $ V11   : num  -0.552 1.613 0.625 -0.226 -0.823 ...
 $ V12   : num  -0.6178 1.0652 0.0661 0.1782 0.5382 ...
 $ V13   : num  -0.991 0.489 0.717 0.508 1.346 ...
 $ V14   : num  -0.311 -0.144 -0.166 -0.288 -1.12 ...
 $ V15   : num  1.468 0.636 2.346 -0.631 0.175 ...
 $ V16   : num  -0.47 0.464 -2.89 -1.06 -0.451 ...
 $ V17   : num  0.208 -0.115 1.11 -0.684 -0.237 ...
 $ V18   : num  0.0258 -0.1834 -0.1214 1.9658 -0.0382 ...
 $ V19   : num  0.404 -0.146 -2.262 -1.233 0.803 ...
 $ V20   : num  0.2514 -0.0691 0.525 -0.208 0.4085 ...
 $ V21   : num  -0.01831 -0.22578 0.248 -0.1083 -0.00943 ...
 $ V22   : num  0.27784 -0.63867 0.77168 0.00527 0.79828 ...
 $ V23   : num  -0.11 0.101 0.909 -0.19 -0.137 ...
 $ V24   : num  0.0669 -0.3398 -0.6893 -1.1756 0.1413 ...
 $ V25   : num  0.129 0.167 -0.328 0.647 -0.206 ...
 $ V26   : num  -0.189 0.126 -0.139 -0.222 0.502 ...
 $ V27   : num  0.13356 -0.00898 -0.05535 0.06272 0.21942 ...
 $ V28   : num  -0.0211 0.0147 -0.0598 0.0615 0.2152 ...
 $ Amount: num  149.62 2.69 378.66 123.5 69.99 ...
 $ Class : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...

现在,要查看类别分布,请使用以下代码:

print(table(cc_fraud$Class))

你将得到以下输出:

     0      1
284315    492

要查看V1Class变量之间的关系,请使用以下代码:

# Printing the Histograms for Multivariate analysis
theme_set(theme_economist_white())
# visualization showing the relationship between variable V1 and the class
ggplot(cc_fraud,aes(x="",y=V1,fill=Class))+geom_boxplot()+labs(x="V1",y="")

这将给出以下输出:

图片

要可视化交易金额相对于类别的分布,请使用以下代码:

# visualization showing the distribution of transaction amount with
# respect to the class, it may be observed that the amount are discretized
# into 50 bins for plotting purposes
ggplot(cc_fraud,aes(x = Amount)) + geom_histogram(color = "#D53E4F", fill = "#D53E4F", bins = 50) + facet_wrap( ~ Class, scales = "free", ncol = 2)

这将给出以下输出:

图片

要可视化交易时间相对于类别的分布,请使用以下代码:

ggplot(cc_fraud, aes(x =Time,fill = Class))+ geom_histogram(bins = 30)+
  facet_wrap( ~ Class, scales = "free", ncol = 2)

这将给出以下输出:

图片

使用以下代码可视化V2变量相对于Class

ggplot(cc_fraud, aes(x =V2, fill=Class))+ geom_histogram(bins = 30)+
  facet_wrap( ~ Class, scales = "free", ncol = 2)

你将得到以下输出:

图片

使用以下代码来可视化V3相对于Class

ggplot(cc_fraud, aes(x =V3, fill=Class))+ geom_histogram(bins = 30)+
  facet_wrap( ~ Class, scales = "free", ncol = 2)

以下图表是结果输出:

图片

要可视化V3变量相对于Class,请使用以下代码:

ggplot(cc_fraud, aes(x =V4,fill=Class))+ geom_histogram(bins = 30)+
  facet_wrap( ~ Class, scales = "free", ncol = 2)

以下图表是结果输出:

图片

使用以下代码来可视化V6变量相对于类别

ggplot(cc_fraud, aes(x=V6, fill=Class)) + geom_density(alpha=1/3) + scale_fill_hue()

以下图表是结果输出:

图片

使用以下代码来可视化V7变量相对于类别

ggplot(cc_fraud, aes(x=V7, fill=Class)) + geom_density(alpha=1/3) + scale_fill_hue()

以下图表是结果输出:

图片

使用以下代码来可视化V8变量相对于类别

ggplot(cc_fraud, aes(x=V8, fill=Class)) + geom_density(alpha=1/3) + scale_fill_hue()

以下图表是结果输出:

图片

要可视化V9变量相对于类别,请使用以下代码:

# visualizationshowing the V7 variable with respect to the class
ggplot(cc_fraud, aes(x=V9, fill=Class)) + geom_density(alpha=1/3) + scale_fill_hue()

以下图表是结果输出:

图片

要可视化V10变量相对于类别,请使用以下代码:

# observe we are plotting the data quantiles
ggplot(cc_fraud, aes(x ="",y=V10, fill=Class))+ geom_violin(adjust = .5,draw_quantiles = c(0.25, 0.5, 0.75))+labs(x="V10",y="")

以下图表是结果输出:

图片

从与类别相关的变量的所有可视化中,我们可以推断出大多数主成分都集中在0上。现在,为了绘制数据中类别的分布,请使用以下代码:

cc_fraud %>%
  ggplot(aes(x = Class)) +
  geom_bar(color = "chocolate", fill = "chocolate", width = 0.2) +
  theme_bw()

以下条形图是结果输出:

图片

我们观察到类别的分布非常不平衡。与少数类(欺诈交易:1)相比,数据集中代表主要类(非欺诈交易,表示为0)的表示过于密集。在传统的监督机器学习处理这类问题时,我们会用如合成少数类过采样技术SMORT)等技术来处理类别不平衡问题。然而,在使用自编码器(AE)时,我们不在数据预处理阶段处理类别不平衡;相反,我们将数据原样输入到 AE 中进行学习。实际上,AE 正在从多数类学习数据的阈值和特征;这就是我们称之为单类分类问题的原因。

在训练我们的自编码器(AE)之前,我们需要做一些特征工程。让我们首先关注数据中的时间变量。目前,它是以秒为单位的格式,但我们可以更好地将其表示为天数。运行以下代码以查看数据集中时间的当前形式:

print(summary(cc_fraud$Time))

你将得到以下输出:

   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
      0   54202   84692   94814  139320  172792

我们知道,给定的一天中有 86,400 秒(每分钟 60 秒,每小时 60 分钟,每天 24 小时)。我们将时间变量转换为天数,考虑到时间中的值,如果秒数小于或等于 86,400,则表示为day1,超过 86,400 的任何值变为day2。由于从摘要中我们可以看到,时间变量的最大值表示为172792秒,因此只有两种可能的天数:

# creating a new variable called day based on the seconds 
# represented in Time variable
 cc_fraud=cc_fraud %>% mutate(Day = case_when(.$Time > 3600 * 24 ~ "day2",.$Time < 3600 * 24 ~ "day1"))
#visualizing the dataset post creating the new variable
View(cc_fraud%>%head())

以下是转换后的前六行的结果输出:

图片

现在,使用以下代码来查看最后六行:

View(cc_fraud%>%tail())

转换后的最后六行的结果是以下内容:

图片

现在,让我们使用以下代码打印交易按交易发生的日期的分布:

print(table(cc_fraud[,"Day"]))

你将得到以下输出:

  day1   day2
144786 140020

让我们根据Time变量中的秒数创建一个新的变量Time_day,并使用以下代码根据DayTime_day变量进行汇总:

cc_fraud$Time_day <- if_else(cc_fraud$Day == "day2", cc_fraud$Time - 86400, cc_fraud$Time)
print(tapply(cc_fraud$Time_day,cc_fraud$Day,summary,simplify = FALSE))

我们得到以下结果输出:

$day1
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
      0   38432   54689   52948   70976   86398

$day2
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
      1   37843   53425   51705   68182   86392

使用以下代码将数据集中的所有字符变量转换为因子:

cc_fraud<-cc_fraud%>%mutate_if(is.character,as.factor)

我们可以通过将变量转换为因子来进一步微调Time_day变量。因子代表交易发生的时间,例如,“上午”、“下午”、“傍晚”和“夜间”。我们可以使用以下代码创建一个名为Time_Group的新变量,基于一天中的不同时间段:

cc_fraud=cc_fraud %>% 
  mutate(Time_Group = case_when(.$Time_day <= 38138~ "morning" ,
                                .$Time_day <= 52327~  "afternoon",
                                .$Time_day <= 69580~"evening",
                                .$Time_day > 69580~"night"))
#Visualizing the data post creating the new variable
View(head(cc_fraud))

转换后的前六行的结果是以下内容:

图片

使用以下代码查看并确认最后六行:

View(tail(cc_fraud))

这将给出以下输出,我们看到我们已经成功地将表示一天中各种时间的交易数据转换过来:

图片

看看以下代码:

#visualizing the transaction count by day
cc_fraud %>%drop_na()%>%
  ggplot(aes(x = Day)) +
  geom_bar(fill = "chocolate",width = 0.3,color="chocolate") +
  theme_economist_white()

上述代码将生成以下输出:

图片

从可视化中我们可以推断,第一天和第二天发生的交易数量没有差异。两者都接近 15 万笔交易。

现在我们将Class变量转换为因子,然后使用以下代码通过Time_Group变量可视化数据:

cc_fraud$Class <- factor(cc_fraud$Class)
cc_fraud %>%drop_na()%>%
  ggplot(aes(x = Time_Group)) +
  geom_bar(color = "#238B45", fill = "#238B45") +
  theme_bw() +
  facet_wrap( ~ Class, scales = "free", ncol = 2)

这将生成以下输出:

图片

从这个可视化中得到的推断是,非欺诈交易的数量在一天中的所有时间段几乎保持不变,而我们在上午Time组中看到欺诈交易数量的巨大增长。

让我们对交易金额与类别进行最后的探索:

# getting the summary of amount with respect to the class
print(tapply(cc_fraud$Amount  ,cc_fraud$Class,summary))

上述代码将生成以下输出:

$`0`
    Min.  1st Qu.   Median     Mean  3rd Qu.     Max.
    0.00     5.65    22.00    88.29    77.05 25691.16
$`1`
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
   0.00    1.00    9.25  122.21 105.89 2125.87

从摘要中得出的一个有趣见解是,欺诈交易的平均金额比真实交易要高。然而,我们在欺诈交易中看到的最大交易金额远低于真实交易。还可以看出,真实交易的中位数金额更高。

现在,让我们将我们的 R 数据框转换为 H2O 数据框,以便对其应用 AE。这是使用h2o库中的函数的一个要求:

# converting R dataframe to H2O dataframe
cc_fraud_h2o <- as.h2o(cc_fraud)
#splitting the data into 60%, 20%, 20% chunks to use them as training,
#vaidation and test datasets
splits <- h2o.splitFrame(cc_fraud_h2o,ratios = c(0.6, 0.2), seed = 148)  
# creating new train, validation and test h2o dataframes
train <- splits[[1]]
validation <- splits[[2]]
test <- splits[[3]]
# getting the target and features name in vectors
target <- "Class"
features <- setdiff(colnames(train), target)

tanh激活函数是一个缩放和移动的对数函数。h2o库还提供了其他函数,如 ReLu 和 Maxout,也可以使用。在第一个 AE 模型中,让我们使用tanh激活函数。这个选择是任意的,也可以根据需要尝试其他激活函数。

h2o.deeplearning函数有一个参数 AE,应该将其设置为TRUE以训练一个 AE 模型。现在让我们构建我们的 AE 模型:

model_one = h2o.deeplearning(x = features, training_frame = train,
                             AE = TRUE,
                             reproducible = TRUE,
                             seed = 148,
                             hidden = c(10,10,10), epochs = 100,
 activation = "Tanh",
                             validation_frame = test)

上述代码生成以下输出:

 |===========================================================================================================================| 100%

我们将保存模型,这样我们就不必一次又一次地重新训练。然后加载磁盘上持久化的模型,并使用以下代码打印模型以验证 AE 学习:

h2o.saveModel(model_one, path="model_one", force = TRUE)
model_one<-h2o.loadModel("/home/sunil/model_one/DeepLearning_model_R_1544970545051_1")
print(model_one)

这将生成以下输出:

Model Details:
==============
H2OAutoEncoderModel: deeplearning
Model ID:  DeepLearning_model_R_1544970545051_1
Status of Neuron Layers: auto-encoder, gaussian distribution, Quadratic loss, 944 weights/biases, 20.1 KB, 2,739,472 training samples, mini-batch size 1
  layer units  type dropout       l1       l2 mean_rate rate_rms momentum mean_weight weight_rms mean_bias bias_rms
1     1    34 Input  0.00 %       NA       NA        NA       NA       NA          NA         NA        NA       NA
2     2    10  Tanh  0.00 % 0.000000 0.000000  0.610547 0.305915 0.000000   -0.000347   0.309377 -0.028166 0.148318
3     3    10  Tanh  0.00 % 0.000000 0.000000  0.181705 0.103598 0.000000    0.022774   0.262611 -0.056455 0.099918
4     4    10  Tanh  0.00 % 0.000000 0.000000  0.133090 0.079663 0.000000    0.000808   0.337259  0.032588 0.101952
5     5    34  Tanh      NA 0.000000 0.000000  0.116252 0.129859 0.000000    0.006941   0.357547  0.167973 0.688510
H2OAutoEncoderMetrics: deeplearning
 Reported on training data. Training Set Metrics:
=====================
MSE: (Extract with `h2o.mse`) 0.0003654009
RMSE: (Extract with `h2o.rmse`) 0.01911546
H2OAutoEncoderMetrics: deeplearning
 Reported on validation data. Validation Set Metrics:
=====================
MSE: (Extract with `h2o.mse`) 0.0003508435
RMSE: (Extract with `h2o.rmse`) 0.01873082

我们现在将使用构建的 AE 模型在测试数据集上进行预测,使用以下代码:

test_autoencoder <- h2o.predict(model_one, test)

这将生成以下输出:

|===========================================================================================================================| 100%

通过h2o.deepfeatures函数,我们可以在内部层以有意识的方式可视化表示数据的编码器。让我们尝试可视化第二层中的降维数据:

train_features <- h2o.deepfeatures(model_one, train, layer = 2) %>%
  as.data.frame() %>%
  mutate(Class = as.vector(train[, 31]))
# printing the reduced data represented in layer2
print(train_features%>%head(3))

上述代码将生成以下输出:

DF.L2.C1  DF.L2.C2     DF.L2.C3    DF.L2.C4   DF.L2.C5 
-0.12899115 0.1312075  0.115971952 -0.12997648 0.23081912
-0.10437942 0.1832959  0.006427409 -0.08018725 0.05575977
-0.07135827 0.1705700 -0.023808057 -0.11383244 0.10800857
DF.L2.C6   DF.L2.C7    DF.L2.C8  DF.L2.C9  DF.L2.C10  Class0.1791547 0.10325721  0.05589069 0.5607497 -0.9038150     0
0.1588236 0.11009450 -0.04071038 0.5895413 -0.8949729     0
0.1676358 0.10703990 -0.03263755 0.5762191 -0.8989759     0

现在我们使用以下代码绘制DF.L2.C1相对于DF.L2.C2的数据,以验证编码器是否检测到了欺诈交易:

ggplot(train_features, aes(x = DF.L2.C1, y = DF.L2.C2, color = Class)) +
  geom_point(alpha = 0.1,size=1.5)+theme_bw()+
  scale_fill_brewer(palette = "Accent")

这将生成以下输出:

再次,我们绘制DF.L2.C3相对于DF.L2.C4的数据,以验证编码器是否检测到了任何欺诈交易,使用以下代码:

ggplot(train_features, aes(x = DF.L2.C3, y = DF.L2.C4, color = Class)) +
  geom_point(alpha = 0.1,size=1.5)+theme_bw()+
  scale_fill_brewer(palette = "Accent")

上述代码将生成以下输出:

从两个可视化图中我们可以看出,通过我们的 AE 模型进行降维处理确实检测到了欺诈交易。那些散布的点(用1表示)描绘了被检测到的欺诈交易。我们也可以使用我们的第一个模型来训练一个新的模型,使用其他隐藏层。这会产生 10 列,因为第三层有 10 个节点。我们只是在尝试切出一层,在这一层中已经进行了一定程度的降维,并使用它来构建一个新的模型:

# let's consider the third hidden layer. This is again a random choice
# in fact we could have taken any layer among the 10 inner layers
train_features <- h2o.deepfeatures(model_one, validation, layer = 3) %>%
  as.data.frame() %>%
  mutate(Class = as.factor(as.vector(validation[, 31]))) %>%
  as.h2o()

上述代码将生成以下输出:

|===========================================================================================================================| 100% |===========================================================================================================================| 100%

如我们所见,训练模型和数据已成功创建。我们现在将继续训练新的模型,保存它并打印它。首先,我们将从切片编码器层获取特征名称:

features_two <- setdiff(colnames(train_features), target)

然后,我们将训练一个新的模型:

model_two <- h2o.deeplearning(y = target,
                              x = features_two,
                              training_frame = train_features,
                              reproducible = TRUE,
                              balance_classes = TRUE,
                              ignore_const_cols = FALSE,
                              seed = 148,
                              hidden = c(10, 5, 10),
                              epochs = 100,
                              activation = "Tanh")

然后,我们将保存模型以避免再次重新训练,然后使用以下代码检索模型并打印它:

h2o.saveModel(model_two, path="model_two", force = TRUE)
model_two <- h2o.loadModel("/home/sunil/model_two/DeepLearning_model_R_1544970545051_2")
print(model_two)

这将生成以下输出:

Model Details:
==============
H2OBinomialModel: deeplearning
Model ID:  DeepLearning_model_R_1544970545051_2
Status of Neuron Layers: predicting Class, 2-class classification, bernoulli distribution, CrossEntropy loss, 247 weights/biases, 8.0 KB, 2,383,962 training samples, mini-batch size 1
  layer units    type dropout       l1       l2 mean_rate rate_rms momentum mean_weight weight_rms mean_bias bias_rms
1     1    10   Input  0.00 %       NA       NA        NA       NA       NA          NA         NA        NA       NA
2     2    10    Tanh  0.00 % 0.000000 0.000000  0.001515 0.001883 0.000000   -0.149216   0.768610 -0.038682 0.891455
3     3     5    Tanh  0.00 % 0.000000 0.000000  0.003293 0.004916 0.000000   -0.251950   0.885017 -0.307971 0.531144
4     4    10    Tanh  0.00 % 0.000000 0.000000  0.002252 0.001780 0.000000    0.073398   1.217405 -0.354956 0.887678
5     5     2 Softmax      NA 0.000000 0.000000  0.007459 0.007915 0.000000   -0.095975   3.579932  0.223286 1.172508
H2OBinomialMetrics: deeplearning
 Reported on training data.
  Metrics reported on temporary training frame with 9892 samples MSE:  0.1129424
RMSE:  0.336069
LogLoss:  0.336795
Mean Per-Class Error:  0.006234916
AUC:  0.9983688
Gini:  0.9967377
Confusion Matrix (vertical: actual; across: predicted) for F1-optimal threshold:
          0    1    Error      Rate
0      4910   62 0.012470  =62/4972
1         0 4920 0.000000   =0/4920
Totals 4910 4982 0.006268  =62/9892
Maximum Metrics: Maximum metrics at their respective thresholds
                        metric threshold    value idx
1                       max f1  0.009908 0.993739 153
2                       max f2  0.009908 0.997486 153
3                 max f0point5  0.019214 0.990107 142
4                 max accuracy  0.009908 0.993732 153
5                max precision  1.000000 1.000000   0
6                   max recall  0.009908 1.000000 153
7              max specificity  1.000000 1.000000   0
8             max absolute_mcc  0.009908 0.987543 153
9   max min_per_class_accuracy  0.019214 0.989541 142
10 max mean_per_class_accuracy  0.009908 0.993765 153
Gains/Lift Table: Extract with `h2o.gainsLift(<model>, <data>)` or `h2o.gainsLift(<model>, valid=<T/F>, xval=<T/F>)

为了测量模型在测试数据上的性能,我们需要将测试数据转换为与训练数据相同的降维维度:

test_3 <- h2o.deepfeatures(model_one, test, layer = 3)
print(test_3%>%head())

上述代码将生成以下输出:

|===========================================================================================================================| 100%

我们可以看到,数据已经成功转换。现在,为了使用model_two在测试数据集上进行预测,我们将使用以下代码:

test_pred=h2o.predict(model_two, test_3,type="response")%>%
  as.data.frame() %>%
  mutate(actual = as.vector(test[, 31]))

这将生成以下输出:

|===========================================================================================================================| 100%

如我们所见,从输出中,预测已成功完成,现在让我们使用以下代码可视化预测结果:

test_pred%>%head()
  predict        p0           p1 actual
1       0 1.0000000 1.468655e-23      0
2       0 1.0000000 2.354664e-23      0
3       0 1.0000000 5.987218e-09      0
4       0 1.0000000 2.888583e-23      0
5       0 0.9999988 1.226122e-06      0
6       0 1.0000000 2.927614e-23      0
# summarizing the predictions
print(h2o.predict(model_two, test_3) %>%
  as.data.frame() %>%
  dplyr::mutate(actual = as.vector(test[, 31])) %>%
  group_by(actual, predict) %>%
  dplyr::summarise(n = n()) %>%
  mutate(freq = n / sum(n)))

这将生成以下输出:

|===========================================================================================================================| 100%
# A tibble: 4 x 4
# Groups:   actual [2]
  actual predict     n   freq
  <chr>  <fct>   <int>  <dbl>
1 0      0       55811 0.986
2 0      1         817 0.0144
3 1      0          41 0.414
4 1      1          58 0.586

我们可以看到,我们的 AE 能够以 98%的准确率正确预测非欺诈交易,这是好的。然而,在预测欺诈交易时,它只产生了 58%的准确率。这确实是一个需要关注的问题。我们的模型需要一些改进,这可以通过以下选项实现:

  • 使用其他层的潜在空间表示作为输入来构建model_two(记住我们目前使用的是第 3 层的表示)

  • 使用 ReLu 或 Maxout 激活函数而不是Tanh

  • 通过h2o.anomaly函数检查被错误分类的实例,并增加或减少截止阈值 MSE 值,这些值将欺诈交易与非欺诈交易区分开来

  • 在编码器和解码器中尝试更复杂的架构

我们在本章中不会尝试这些选项,因为它们具有实验性质。然而,感兴趣的读者可以尝试这些选项来提高模型的准确性。

最后,一个最佳实践是明确关闭h2o集群。这可以通过以下命令完成:

h2o.shutdown()

摘要

在本章中,我们了解了一种无监督的深度学习技术,称为 AEs。我们涵盖了 AEs 的定义、工作原理、类型和应用。我们探讨了 H2O,这是一个开源库,使我们能够创建深度学习模型,包括 AEs。然后我们讨论了一个信用卡欺诈公开数据集,并使用 AE 实现了一个项目来检测欺诈信用卡交易。

深度神经网络能否帮助完成创意任务,如散文生成、故事写作、图像标题生成和诗歌创作?不确定吗?!让我们在下一章探索 RNNs,这是一种特殊的深度神经网络,使我们能够完成创意任务。翻到下一页,探索用于散文生成的 RNNs 世界。

第八章:使用循环神经网络进行自动散文生成

我们几乎通过这本书互动了将近 200 页,但我意识到我还没有向你正确地介绍自己!我想现在是时候了。通过这本书的作者简介,你已经了解了我的一些信息;然而,我想告诉你一些关于我居住的城市的情况。我住在南印度的一个城市,名叫班加罗尔,也被称为班加罗尔。这座城市以其 IT 人才和人口多样性而闻名。我喜欢这座城市,因为它充满了大量的正能量。每天,我都会遇到来自各行各业的人——来自多个民族、多个背景、说多种语言的人等等。卡纳达语是位于班加罗尔的卡纳塔克邦的官方语言。尽管我能说一些卡纳达语,但我的口语水平并不如本土的卡纳达语使用者。当然,这是我要改进的一个领域,我正在努力提高。像我一样,许多从其他地方迁移到这个城市的人在与卡纳达语交流时也遇到了问题。有趣的是,不懂语言并没有阻止我们用当地人的语言与他们互动。猜猜看,是什么帮助我们解决了这个问题:像谷歌翻译、谷歌文本转语音等移动应用程序。这些应用程序是基于称为机器翻译和语音识别的自然语言处理技术构建的。这些技术反过来又作用于被称为语言模型的东西。语言模型是我们将在本章深入探讨的主题。

本章的目标包括探讨以下主题:

  • 需要语言建模来解决自然语言处理任务

  • 语言模型的工作原理

  • 语言模型的应用

  • 语言建模与神经网络之间的关系

  • 循环神经网络

  • 正常前馈网络与循环神经网络之间的区别

  • 长短期记忆网络

  • 一个使用循环神经网络自动生成文本的项目

理解语言模型

在英语中,字符a在单词和句子中出现的频率远高于字符x。同样,我们也可以观察到单词is出现的频率高于单词specimen。通过检查大量文本,我们可以学习字符和单词的概率分布。以下截图是一个显示给定语料库(文本数据集)中字母概率分布的图表:

图片

语料库中字母的概率分布

我们可以观察到字符的概率分布是非均匀的。这本质上意味着,即使由于噪声而丢失,我们也可以恢复单词中的字符。如果一个特定的字符在单词中缺失,它可以根据缺失字符周围的字符重建。缺失字符的重建不是随机进行的,而是通过选择给定缺失字符周围字符具有最高概率分布的字符来完成的。从技术上讲,句子中单词或词中字符的统计结构遵循最大熵的距离。

语言模型利用语言的统计结构来表达以下内容:

  • 给定句子中的w_1, w_2, w_3,...w_N个词,语言模型会给这个句子分配一个概率P(w_1, w_2, w_3,.... w_N)

  • 然后它将下一个单词(在这种情况下为w_4)的概率分配为P(w_4 | w_1, w_2, w_3)

语言模型使得在 NLP 中开发许多应用成为可能,以下列出其中一些:

  • 机器翻译:P(enormous cyclone tonight) > P(gain typhoon this evening)

  • 拼写纠正:P(satellite constellation) > P(satelitte constellation)

  • 语音识别:P(I saw a van) > P(eyes awe of an)

  • 打字预测:在谷歌搜索中的自动完成,打字辅助应用

现在我们来看一下如何计算单词的概率。考虑一个简单的句子,Decembers are cold。这个句子的概率如下表示:

P("Decembers are cold") = P("December") * P ("are" | "Decembers") * P("cold" | "Decembers are")

从数学上讲,句子中单词(或词中的字母)的概率计算可以表示如下:

图片

俄罗斯数学家安德烈·马尔可夫描述了一个具有马尔可夫属性马尔可夫假设的随机过程。这基本上意味着,人们可以仅基于过程的当前状态对未来进行预测,就像知道过程的全貌一样,因此独立于这样的历史。

基于马尔可夫的假设,我们可以将cold的条件概率重新写为如下:

*P("cold" | "Decembers are") 等同于 P("cold" | "are")

从数学上讲,马尔可夫的假设可以表示如下:

图片

虽然这个数学公式代表了二元模型(一次考虑两个词),但它可以很容易地扩展到 n 元模型。在 n 元模型中,条件概率仅依赖于更多一些的先前词。

从数学上讲,n 元模型可以表示如下:

图片

以埃兹拉·庞德(Ezra Pound)的著名诗歌 A Girl 作为我们构建 bigram 模型的语料库。以下就是文本语料库:

The tree has entered my hands,
The sap has ascended my arms,
The tree has grown in my breast-Downward,
The branches grow out of me, like arms.
Tree you are,
Moss you are,
You are violets with wind above them.
A child - so high - you are,
And all this is folly to the world.

我们已经知道,在大词模型中,条件概率仅基于前一个单词来计算。因此,一个单词的概率可以按以下方式计算:

如果我们要计算在诗歌中给定单词 my 的条件下单词 arms 的概率,它是通过在诗歌中单词 armsmy 同时出现的次数除以单词 my 在诗歌中出现的次数来计算的。

我们看到,单词 my arms 在诗歌中只出现了一次(在句子 The sap has ascended my arms 中)。然而,单词 my 在诗歌中出现了三次(在句子 The tree has entered my handsThe sap has ascended my armsThe tree has grown in my breast-Downward 中)。

因此,给定 my 的条件下单词 arms 的条件概率是 1/3,形式上表示如下:

P("arms" | "my") = P("arms", "my") / P("my") = 1 / 3

为了计算第一个和最后一个单词的概率,分别在句子的开始和结尾添加了特殊标记 。同样,可以通过乘以所有 bigram 概率来使用相同的方法计算句子或单词序列的概率。

由于语言建模涉及根据已存在的单词序列预测序列中的下一个单词,我们可以训练一个语言模型,从给定的起始序列创建序列中的后续单词。

探索循环神经网络

循环神经网络RNNs)是一组用于处理序列数据的神经网络。RNN 通常用于实现语言模型。作为人类,我们的大部分语言理解都基于上下文。例如,让我们考虑句子 Christmas falls in the month of --------。用单词 December 来填补空白很容易。这里的本质思想是,关于最后一个单词的信息被编码在句子的前几个元素中。

RNN 架构背后的中心主题是利用数据的序列结构。正如其名所示,RNN 以循环的方式运行。本质上,这意味着对序列或句子中的每个元素执行相同的操作,其输出取决于当前输入和之前的操作。

RNN 通过将网络在时间 t 的输出与网络在时间 t+1 的输入循环连接来工作。这些循环允许信息从一个时间步到下一个时间步的持续。以下是一个表示 RNN 的电路图:

表示 RNN 的电路图

该图表示一个 RNN,它通过一个简单的循环来记住之前输入的信息。这个循环将来自上一个时间戳的信息添加到当前时间戳的输入中。在特定的时间步长tX[t]是网络的输入,O[t]是网络的输出,h[t]是它从网络中先前节点记住的细节。在中间,有一个 RNN 单元,它包含类似于前馈网络的神经网络。

在 RNN 定义方面,一个需要深思的关键点是时间戳。定义中提到的时间戳与过去、现在和未来无关。它们只是代表序列或句子中的一个词或项目。

让我们考虑一个例子句子:圣诞节假期棒极了。在这个句子中,看一下以下时间戳:

  • 圣诞节是 x[0]

  • 假期是 x[1]

  • 是 x[2];

  • 棒极了是 x[3]

如果 t=1,那么看一下以下:

  • x[t] = 假期 → 当前时间戳的事件

  • x[t-1] = 圣诞节 → 上一个时间戳的事件

从前面的电路图中可以观察到,在 RNN 中,相同的操作在不同的节点上反复执行。图中还有一个代表单个时间步长延迟的黑方块。理解带有循环的 RNN 可能会有些困惑,所以让我们展开计算图。展开的 RNN 计算图如下所示:

图片

RNN—展开的计算图视图

在前面的图中,每个节点都与一个特定的时间相关联。在 RNN 架构中,每个节点在每个时间步长x[t]接收不同的输入。它还具有在每个时间步长o[t]产生输出的能力。网络还维护一个记忆状态h[t],它包含关于时间t之前网络中发生的事情的信息。由于这是在网络的各个节点上运行的同一步骤,因此可以将整个网络以简化的形式表示,如图中 RNN 电路图所示。

现在,我们明白为什么在 RNN 中看到循环这个词,因为它对序列中的每个元素执行相同的任务,输出取决于之前的计算。理论上,RNN 可以利用任意长序列中的信息,但在实践中,它们被实现为只回顾几个步骤。

形式上,一个 RNN 可以用以下方程定义:

图片

在方程中,h[t]是时间戳t处的隐藏状态。可以使用 Tanh、Sigmoid 或 ReLU 等激活函数来计算隐藏状态,并在方程中表示为![]。W是时间戳t处输入到隐藏层的权重矩阵。X[t]是时间戳t处的输入。U是时间戳t-1处的隐藏层到时间戳t处的隐藏层的权重矩阵,h[t-1]是时间戳t处的隐藏状态。

在反向传播过程中,RNN 通过学习UW权重。在每个节点上,隐藏状态和当前输入的贡献由UW决定。UW的比例进而导致当前节点的输出生成。激活函数在 RNN 中添加了非线性,从而在反向传播过程中简化了梯度计算。以下图示展示了反向传播的概念:

神经网络中的反向传播

以下图示展示了 RNN 的整体工作机制以及通过反向传播学习权重UW的方式。它还展示了网络中使用UW权重矩阵生成输出的情况,如图所示:

RNN 中权重的作用

前馈神经网络与 RNN 的比较

与其他神经网络相比,RNN 的一个基本区别在于,在所有其他网络中,输入之间是相互独立的。然而,在 RNN 中,所有输入都是相互关联的。在应用中,为了预测给定句子中的下一个单词,所有先前单词之间的关系有助于预测当前输出。换句话说,RNN 在训练过程中会记住所有这些关系。这与其他类型的神经网络不同。以下图示展示了前馈网络的表示:

前馈神经网络架构

从前面的图中,我们可以看到前馈网络架构中不涉及任何循环。这与 RNN 电路图和 RNN 展开计算图中展示的 RNN 架构形成对比。前馈网络中的数学操作是在节点上执行的,信息直接通过,没有任何循环。

使用监督学习,输入被馈送到前馈网络后转换为输出。在这种情况下,输出可以是分类时的标签,或者回归时的数字。如果我们考虑图像分类,输入图像的标签可以是

前馈神经网络在标记图像上训练,直到预测标签的错误最小化。一旦训练完成,该模型能够对之前未见过的图像进行分类。一个训练好的前馈网络可以暴露于任何随机的照片集合;第一个照片的分类对模型需要分类的第二张或后续照片没有任何影响或影响。让我们通过一个例子来讨论这个问题,以便更好地理解这个概念:如果第一个图像被前馈网络识别为,这并不意味着第二个图像将被分类为。换句话说,模型得出的预测没有时间顺序的概念,标签的决定仅基于当前提供的输入。总结来说,在前馈网络中,不会使用历史预测信息来为当前预测提供信息。这与 RNN 非常不同,在 RNN 中,前一个预测被考虑是为了帮助当前预测。

另一个重要的区别是,前馈网络按设计将一个输入映射到一个输出,而 RNN 可以有多种形式:将一个输入映射到多个输出,多个输入到多个输出,或多个输入到一个输出。以下图展示了 RNN 可能实现的多种输入输出映射:

RNN 的输入输出映射可能性

让我们回顾一下使用 RNN 可能实现的输入输出映射的一些实际应用。前面图中的每个矩形都是一个向量,箭头代表函数,例如矩阵乘法。输入向量是下方的矩形(用红色着色),输出向量是上方的矩形(用蓝色着色)。中间的矩形(用绿色着色)是包含 RNN 状态的向量。

下面是图中展示的各种映射形式:

  • 一对一输入输出:最左边的是没有使用 RNN 的普通处理模式,从固定大小的输入到固定大小的输出;例如,图像分类。

  • 一对一输入到多输出:序列输出,例如,图像字幕将图像作为输入,然后输出一个单词句子。

  • 多输入到一对一输出:序列输入,例如,情感分析,其中给定的句子作为输入提供给 RNN,输出是一个表示句子积极或消极情感的分类。

  • 多输入到多输出:序列输入和序列输出;例如,对于机器翻译任务,RNN 读取英语句子作为输入,然后输出印地语或其他语言的句子。

  • 多输入到多输出:同步序列输入和输出,例如,视频分类,我们希望为视频的每一帧进行标记。

现在我们来回顾一下前馈网络和 RNN 之间的最终区别。为了在前馈神经网络中设置权重而执行的反向传播的方式与在 RNN 中执行所谓的时间反向传播BPTT)的方式不同。我们已经知道,神经网络中反向传播算法的目的是调整神经网络的权重,以最小化网络输出与对应输入的预期输出之间的误差。反向传播本身是一种监督学习算法,它允许神经网络根据特定错误进行纠正。反向传播算法包括以下步骤:

  1. 向神经网络提供训练输入,并通过网络传播以获得输出

  2. 将预测输出与实际输出进行比较,并计算误差

  3. 计算误差相对于学习到的网络权重的导数

  4. 修改权重以最小化误差

  5. 重复

在前馈网络中,在输出仅在结束时才可用的情况下,在结束时运行反向传播是有意义的。在 RNN 中,输出在每个时间步产生,并且这种输出会影响后续时间步的输出。换句话说,在 RNN 中,一个时间步的误差取决于前一个时间步。因此,正常的反向传播算法不适用于 RNN。因此,使用称为 BPTT(时间反向传播)的不同算法来修改 RNN 中的权重。

时间反向传播

我们已经知道,循环神经网络(RNNs)是循环图,与无环方向图的前馈网络不同。在前馈网络中,误差导数是从上层计算的。然而,在 RNN 中,我们没有这样的层来进行误差导数计算。解决这个问题的简单方法是将 RNN 展开,使其类似于前馈网络。为了实现这一点,RNN 中的隐藏单元在每个时间步都会被复制。每个时间步的复制形成一层,类似于前馈网络中的层。每个时间步t层连接到时间步t+1中所有可能的层。因此,我们随机初始化权重,展开网络,然后使用反向传播来优化隐藏层中的权重。最低层通过传递参数进行初始化。这些参数也是作为反向传播的一部分进行优化的。时间反向传播算法包括以下步骤:

  1. 向网络提供一系列输入和输出对的时序步骤

  2. 展开网络,然后计算并累积每个时间步的误差

  3. 收缩网络并更新权重

  4. 重复

总结来说,使用 BPTT 时,错误是从最后一个时间步向后传播到第一个时间步,同时展开所有时间步。计算每个时间步的错误,这允许更新权重。以下图表是时间反向传播的可视化:

图片

RNN 中的时间反向传播

应当注意,随着时间步数的增加,BPTT 算法的计算成本可能会变得非常高。

RNN 中梯度的问题和解决方案

RNN 并不完美,它们有两个主要问题,即梯度爆炸梯度消失。为了理解这些问题,我们首先了解什么是梯度。梯度是相对于其输入的偏导数。用简单的话来说,梯度衡量的是函数的输出在输入略有变化时会发生多少变化。

梯度爆炸

梯度爆炸与 BPTT 算法赋予权重极高重要性但缺乏合理依据的情况相关。这个问题导致网络不稳定。在极端情况下,权重的值可能会变得非常大,导致溢出并产生 NaN 值。

在训练网络时,可以通过观察以下细微迹象来检测梯度爆炸问题:

  • 在训练过程中,模型权重会迅速变得非常大。

  • 在训练过程中,模型权重变为 NaN 值。

  • 在训练过程中,每个节点和层的错误梯度值始终大于 1.0。

有几种方法可以处理梯度爆炸问题。以下是一些流行的技术:

  • 如果我们可以截断或压缩梯度,这个问题就可以轻易解决。这被称为梯度裁剪

  • 在训练过程中,通过减少先前时间步的权重更新也可能减少梯度爆炸问题。这种具有较少步更新技术的称为时间截断反向传播TBPTT)。它是 BPTT 训练算法的一个修改版本,其中序列一次处理一个时间步,并且定期(k1时间步)对固定数量的时间步(k2时间步)执行 BPTT 更新。k1是更新之间的前向传递时间步数。k2是应用 BPTT 的时间步数。

  • 通过检查网络权重的大小并对大权重值应用惩罚来执行权重正则化。

  • 通过使用长短期记忆单元LSTMs)或门控循环单元GRUs)而不是普通的 RNN。

  • 仔细初始化权重,如Xavier初始化或He初始化。

梯度消失

我们已经知道,长期依赖对于 RNN 正确运行非常重要。由于长期依赖,RNN 可能会变得太深。当激活函数的梯度非常小的时候,就会出现梯度消失问题。在反向传播过程中,当权重与低梯度相乘时,它们往往会变得非常小,并在网络中进一步消失。这使得神经网络忘记长期依赖。以下图表展示了导致梯度消失的原因:

图片

梯度消失的原因

总结来说,由于梯度消失问题,RNN 在记忆序列中非常远的先前单词时遇到困难,并且只能根据最近的单词进行预测。这可能会影响 RNN 预测的准确性。有时,模型可能无法预测或分类它应该执行的操作。

有几种方法可以处理梯度消失问题。以下是一些最流行的技术:

  • 初始化网络权重为恒等矩阵,以最大限度地减少梯度消失的可能性。

  • 将激活函数设置为 ReLU 而不是 sigmoidtanh。这使得网络计算接近恒等函数。这效果很好,因为当错误导数在时间上向后传播时,它们保持为 0 或 1 的常数,因此不太可能遭受梯度消失问题。

  • 使用 LSTM,它是常规循环网络的变体,旨在使捕获序列数据中的长期依赖变得容易。标准的 RNN 以一种方式运行,即隐藏状态激活受其附近的其他局部激活的影响,这对应于短期记忆,而网络权重受整个长序列中发生的计算的影响,这对应于长期记忆。RNN 被重新设计,使其具有可以像权重一样起作用并能够在长距离上保留信息的激活状态,因此得名 长短期记忆

在 LSTM 中,而不是每个隐藏节点只是一个具有单个激活函数的节点,每个节点本身就是一个可以存储其他信息的记忆单元。具体来说,它维护自己的细胞状态。正常的 RNN 接收先前的隐藏状态和当前输入,并输出一个新的隐藏状态。LSTM 做的是同样的,但它还接收旧的细胞状态,并将输出新的细胞状态。

使用 RNN 构建自动散文生成器

在这个项目中,我们将尝试使用 RNN(递归神经网络)构建一个字符级语言模型,根据一些初始种子字符生成散文。字符级语言模型的主要任务是预测数据序列中所有先前字符之后的下一个字符。换句话说,RNN 的功能是逐字符生成文本。

首先,我们向 RNN 提供一大块文本作为输入,并要求它根据先前字符序列的概率分布来建模序列中下一个字符的概率分布。这些由 RNN 模型构思的概率分布将允许我们逐字符生成新文本。

构建语言模型的第一要求是确保模型可以使用它来计算各种字符的概率分布的文本语料库。输入文本语料库越大,RNN 对概率的建模就越好。

我们不需要费很大力气来获取训练 RNN 所需的庞大文本语料库。有一些经典文本(书籍),如《圣经》,可以用作语料库。最好的部分是许多经典文本已不再受版权保护。因此,这些文本可以自由下载并用于我们的模型中。

Project Gutenberg 是获取不再受版权保护的免费书籍的最佳场所。可以通过以下网址访问 Project Gutenberg:www.gutenberg.org。例如,《圣经》、《爱丽丝梦游仙境》等书籍都可以从 Project Gutenberg 获得。截至 2018 年 12 月,有 58,486 本书可供下载。这些书籍以多种格式提供,以便我们下载和使用,不仅限于本项目,还适用于任何需要大量文本语料库输入的项目。以下截图是 Project Gutenberg 的一个样本书籍及其可供下载的多种格式:

图片

Sample book available from Project Gutenberg in various formats

无论下载的文件格式如何,Project Gutenberg 都会在实际书籍文本中添加标准的页眉和页脚文本。以下是在一本书中可以看到的页眉和页脚的示例:

*** START OF THIS PROJECT GUTENBERG EBOOK ALICE'S ADVENTURES IN WONDERLAND ***

THE END

从 Project Gutenberg 网站下载的书籍文本中删除此页眉和页脚文本是至关重要的。对于下载的文本文件,可以在文本编辑器中打开文件并删除页眉和页脚。

对于本章的项目,让我们使用童年的最爱书籍作为文本语料库:刘易斯·卡罗尔的《爱丽丝梦游仙境》。虽然我们有从 Project Gutenberg 下载这本书的文本格式并将其用作文本语料库的选择,但 R 语言的languageR库使这项任务对我们来说更加容易。languageR库已经包含了《爱丽丝梦游仙境》的文本。在安装了languageR库之后,使用以下代码将文本数据加载到内存中并打印出加载的文本:

# including the languageR library
library("languageR")
# loading the "Alice’s Adventures in Wonderland" to memory
data(alice)
# printing the loaded text
print(alice)

你将得到以下输出:

[1] "ALICE"           "S"                "ADVENTURES"       "IN"               "WONDERLAND"      
[6] "Lewis"            "Carroll"          "THE"              "MILLENNIUM"       "FULCRUM"        
  [11] "EDITION"          "3"                "0"                "CHAPTER"          "I"              
  [16] "Down"             "the"              "Rabbit-Hole"      "Alice"            "was"            
  [21] "beginning"        "to"               "get"              "very"             "tired"          
  [26] "of"               "sitting"          "by"               "her"              "sister"         
  [31] "on"               "the"              "bank"             "and"              "of"             
  [36] "having"           "nothing"          "to"               "do"               "once"           
  [41] "or"               "twice"            "she"              "had"              "peeped"         
  [46] "into"             "the"              "book"             "her"              "sister"         
  [51] "was"              "reading"          "but"              "it"               "had"            
  [56] "no"         "pictures"         "or"               "conversations"    "in"              

从输出中我们可以看到,书籍文本存储为一个字符向量,其中向量的每个项目都是从书籍文本中通过标点符号分割出来的单词。也可能注意到,书籍文本中并没有保留所有的标点符号。

以下代码从字符向量中的单词重构句子。当然,在重构过程中,我们不会得到诸如句子边界之类的东西,因为字符向量中的标点符号没有字符向量项目那么多。现在,让我们从单个单词重构书籍文本:

alice_in_wonderland<-paste(alice,collapse=" ")
print(alice_in_wonderland)

你将得到以下输出:

[1] "ALICE S ADVENTURES IN WONDERLAND Lewis Carroll THE MILLENNIUM FULCRUM EDITION 3 0 CHAPTER I Down the Rabbit-Hole Alice was beginning to get very tired of sitting by her sister on the bank and of having nothing to do once or twice she had peeped into the book her sister was reading but it had no pictures or conversations in it and what is the use of a book thought Alice without pictures or conversation So she was considering in her own mind as well as she could for the hot day made her feel very sleepy and stupid whether the pleasure of making a daisy-chain would be worth the trouble of getting up and picking the daisies when suddenly a White Rabbit with pink eyes ran close by her There was nothing so VERY remarkable in that nor did Alice think it so VERY much out of the way to hear the Rabbit say to itself Oh dear Oh dear I shall be late when she thought it over afterwards it occurred to her that she ought to have wondered at this but at the time it all seemed quite natural but when the Rabbit actually TOOK A WATCH OUT OF ITS WAISTCOAT- POCKET and looked at it and then hurried on Alice started to her feet for it flashed across her mind that she had never before seen a rabbit with either a waistcoat-pocket or a watch to take out of it and burning with curiosity she ran across the field after it and fortunately was just in time to see it pop down a large rabbit-hole under the hedge In another moment down went Alice after it never once considering how in the world she was to get out again The rabbit-hole we .......

从输出中,我们可以看到一段长文本是由单词构成的。现在,我们可以继续对这段文本进行一些预处理,以便将其输入到 RNN 中,使模型学习字符之间的依赖关系以及序列中字符的条件概率。

需要注意的一件事是,与生成序列中下一个字符的字符级语言模型一样,你还可以构建一个词级语言模型。然而,字符级语言模型的优势在于它可以创建其自身的独特单词,而这些单词不在我们训练它的词汇表中。

现在,让我们学习 RNN 是如何工作的,以便理解序列中字符之间的依赖关系。假设我们只有四种可能的字母词汇,[aple],并且我们的目的是在训练序列apple上训练一个 RNN。这个训练序列实际上是四个独立的训练示例的来源:

  • 在给定上下文a的情况下,字母p的概率应该是可能的,换句话说,在单词apple中,给定字母a的条件概率p

  • 与第一点类似,在上下文ap中,p应该是可能的。

  • 在上下文app中,字母 l也应该是有可能的。

  • 在上下文appl中,字母 e应该是有可能的。

我们开始使用 1-of-k 编码将单词 apple 中的每个字符编码成一个向量。1-of-k 编码表示单词中的每个字符都是零,除了在词汇表中字符索引处的单个 1。这样用 1-of-k 编码表示的每个字符随后通过步函数逐个输入到 RNN 中。RNN 接收这个输入并生成一个四维输出向量(每个字符一个维度,并且我们词汇表中有四个字符)。这个输出向量可以解释为 RNN 当前分配给序列中下一个字符的置信度。下面的图是 RNN 学习字符的可视化:

图片

RNN 学习字符语言模型

在前面的图中,我们看到一个具有四维输入和输出层的 RNN。还有一个包含三个神经元的隐藏层。该图显示了当 RNN 接收字符 appl 的输入时,正向传递中的激活情况。输出层包含 RNN 分配给每个后续字符的置信度。RNN 的期望是输出层中的绿色数字高于红色数字。绿色数字的高值使得可以根据输入预测正确的字符。

我们看到在第一次步长中,当 RNN 接收输入字符 a 时,它将 1.0 的置信度分配给下一个字母是 a,将 2.2 的置信度分配给字母 p,将 -3.0 分配给 l,将 4.1 分配给 e。根据我们的训练数据,我们考虑的序列是 apple;因此,在第一次步长中,以 a 作为输入时,下一个正确的字符是 p。我们希望我们的 RNN 在第一步(用绿色表示)中最大化置信度,并最小化所有其他字母(用红色表示)的置信度。同样,在每个四个时间步长中,我们都希望我们的 RNN 分配更高的置信度给期望的输出字符。

由于 RNN 完全由可微操作组成,我们可以运行反向传播算法来确定我们应该调整每个权重的方向,以增加正确目标(粗体绿色数字)的分数。

基于梯度方向,参数被更新,算法实际上通过一个很小的量在梯度相同方向上改变权重。理想情况下,如果梯度下降法成功运行并更新了权重,我们会看到正确选择的权重略有增加,而对于错误的字符,权重会降低。例如,我们会发现第一次步长中正确字符 p 的分数略有提高,比如说从 2.2 提高到 2.3。同时,字符 ale 的分数会被观察到低于梯度下降之前分配的分数。

在 RNN 中,通过梯度下降更新参数的过程会重复多次,直到网络收敛,换句话说,直到预测与训练数据一致。

从技术角度来说,我们同时对每个输出向量运行标准的 softmax 分类器,也称为交叉熵损失。RNN 通过小批量随机梯度下降或自适应学习率方法(如 RMSProp 或 Adam)进行训练,以稳定更新。

你可能会注意到,当第一次输入字符p时,输出是p;然而,当相同的输入第二次被输入时,输出是l。因此,RNN 不能仅依赖于给定的输入。这就是 RNN 使用其循环连接来跟踪上下文以执行任务并做出正确预测的地方。没有上下文,网络要预测特定的正确输出将会非常困难。

当我们必须使用训练好的 RNN 模型生成文本时,我们将种子输入字符提供给网络,并得到关于下一个可能出现的字符的分布。然后从这个分布中进行采样,并将其反馈回网络,以获取下一个字母。这个过程会重复,直到达到最大字符数(直到达到特定用户定义的字符长度),或者直到模型遇到行尾字符,如

实施项目

现在我们知道了 RNN 如何构建字符级模型,让我们通过 RNN 实现项目,生成我们自己的单词和句子。通常,RNN 训练计算量很大,建议我们在图形处理单元GPU)上运行代码。然而,由于基础设施限制,我们不会在项目代码中使用 GPU。mxnet库允许字符级语言模型(带有 RNN)在 CPU 上执行,因此让我们开始编写我们的项目代码:

# including the required libraries
library("readr")
library("stringr")
library("stringi")
library("mxnet")
library("languageR")

要使用languageR库的《爱丽丝梦游仙境》书籍文本并将其加载到内存中,请使用以下代码:

data(alice)

接下来,我们将测试数据转换为特征向量,并将其输入到 RNN 模型中。make_data函数读取数据集,清除任何非字母数字字符,将其拆分为单个字符,并将其分组为长度为seq.len的序列。在这种情况下,seq.len设置为100

make_data <- function(txt, seq.len = 32, dic=NULL) {
  text_vec <- as.character(txt)
  text_vec <- stri_enc_toascii(str = text_vec)
  text_vec <- str_replace_all(string = text_vec, pattern = "[^[:print:]]", replacement = "")
  text_vec <- strsplit(text_vec, '') %>% unlist
  if (is.null(dic)) {
    char_keep <- sort(unique(text_vec))
  } else char_keep <- names(dic)[!dic == 0]

要删除字典中不存在的术语,请使用以下代码:

text_vec <- text_vec[text_vec %in% char_keep]

要构建字典并通过-1调整它以具有1-lag标签,请使用以下代码:

dic <- 1:length(char_keep)
 names(dic) <- char_keep
 # reversing the dictionary
 rev_dic <- names(dic)
 names(rev_dic) <- dic
 # Adjust by -1 to have a 1-lag for labels
 num.seq <- (length(text_vec) - 1) %/% seq.len
 features <- dic[text_vec[1:(seq.len * num.seq)]]
 labels <- dic[text_vec[1:(seq.len*num.seq) + 1]]
 features_array <- array(features, dim = c(seq.len, num.seq))
 labels_array <- array(labels, dim = c(seq.len, num.seq))
 return (list(features_array = features_array, labels_array = labels_array, dic = dic, rev_dic
 = rev_dic))
 }

将序列长度设置为100,然后从alice数据字符向量中构建长文本序列。接着在alice_in_wonderland文本文件上调用make_data()函数。观察seq.ln和空字典作为输入。seq.ln决定了上下文,即 RNN 需要回溯多少个字符来生成下一个字符。在训练过程中,seq.ln被用来获取正确的权重:

seq.len <- 100
 alice_in_wonderland<-paste(alice,collapse=" ")
 data_prep <- make_data(alice_in_wonderland, seq.len = seq.len, dic=NULL)

要查看准备好的数据,使用以下代码:

print(str(data_prep))

这将给出以下输出:

> print(str(data_prep))
List of 4
 $ features_array: int [1:100, 1:1351] 9 31 25 13 17 1 45 1 9 15 ...
 $ labels_array  : int [1:100, 1:1351] 31 25 13 17 1 45 1 9 15 51 ...
 $ dic           : Named int [1:59] 1 2 3 4 5 6 7 8 9 10 ...
  ..- attr(*, "names")= chr [1:59] " " "-" "[" "]" ...
 $ rev_dic       : Named chr [1:59] " " "-" "[" "]" ...
  ..- attr(*, "names")= chr [1:59] "1" "2" "3" "4" ...

要查看features数组,使用以下代码:

# Viewing the feature array
View(data_prep$features_array)

这将给出以下输出:

图片

要查看labels数组,使用以下代码:

# Viewing the labels array
View(data_prep$labels_array)

你将得到以下输出:

图片

现在,让我们打印字典,它包括唯一的字符,使用以下代码:

# printing the dictionary - the unique characters
print(data_prep$dic)

你将得到以下输出:

> print(data_prep$dic)
    -  [  ]  *  0  3  a  A  b  B  c  C  d  D  e  E  f  F  g  G  h  H  i  I  j  J  k  K  l  L  m  M  n  N  o  O  p
 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
 P  q  Q  r  R  s  S  t  T  u  U  v  V  w  W  x  X  y  Y  z  Z
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

使用以下代码打印字符的索引:

# printing the indexes of the characters
print(data_prep$rev_dic)

这将给出以下输出:

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28
" " "-" "[" "]" "*" "0" "3" "a" "A" "b" "B" "c" "C" "d" "D" "e" "E" "f" "F" "g" "G" "h" "H" "i" "I" "j" "J" "k"
 29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56
"K" "l" "L" "m" "M" "n" "N" "o" "O" "p" "P" "q" "Q" "r" "R" "s" "S" "t" "T" "u" "U" "v" "V" "w" "W" "x" "X" "y"
 57  58  59
"Y" "z" "Z"

使用以下代码块获取特征和标签以训练模型,将数据分成 90:10 的训练和评估比例:

X <- data_prep$features_array
Y <- data_prep$labels_array
dic <- data_prep$dic
rev_dic <- data_prep$rev_dic
vocab <- length(dic)
samples <- tail(dim(X), 1)
train.val.fraction <- 0.9
X.train.data <- X[, 1:as.integer(samples * train.val.fraction)]
X.val.data <- X[, -(1:as.integer(samples * train.val.fraction))]
X.train.label <- Y[, 1:as.integer(samples * train.val.fraction)]
X.val.label <- Y[, -(1:as.integer(samples * train.val.fraction))]
train_buckets <- list("100" = list(data = X.train.data, label = X.train.label))
eval_buckets <- list("100" = list(data = X.val.data, label = X.val.label))
train_buckets <- list(buckets = train_buckets, dic = dic, rev_dic = rev_dic)
eval_buckets <- list(buckets = eval_buckets, dic = dic, rev_dic = rev_dic)

使用以下代码创建训练和评估数据集的迭代器:

vocab <- length(eval_buckets$dic)
batch.size <- 32
train.data <- mx.io.bucket.iter(buckets = train_buckets$buckets, batch.size = batch.size, data.mask.element = 0, shuffle = TRUE)
eval.data <- mx.io.bucket.iter(buckets = eval_buckets$buckets, batch.size = batch.size,data.mask.element = 0, shuffle = FALSE)

创建一个多层 RNN 模型以从字符级语言模型中采样。它有一个一对一的模型配置,因为对于每个字符,我们想要预测下一个字符。对于长度为100的序列,也有100个标签,对应相同的字符序列,但偏移量为+1。参数的output_last_state设置为TRUE,这是为了在推理时访问 RNN 单元的状态,我们可以看到使用了lstm单元。

rnn_graph_one_one <- rnn.graph(num_rnn_layer = 3,
                               num_hidden = 96,
                               input_size = vocab,
                               num_embed = 64,
                               num_decode = vocab,
                               dropout = 0.2,
                               ignore_label = 0,
                               cell_type = "lstm",
                               masking = F,
                               output_last_state = T,
                               loss_output = "softmax",
                               config = "one-to-one")

使用以下代码可视化 RNN 模型:

graph.viz(rnn_graph_one_one, type = "graph",
          graph.height.px = 650, shape=c(500, 500))

以下图表显示了结果输出:

图片

现在,使用以下代码行将 CPU 设置为执行代码的设备:

devices <- mx.cpu()

然后,通过 Xavier 初始化器初始化网络的权重:

initializer <- mx.init.Xavier(rnd_type = "gaussian", factor_type = "avg", magnitude = 3)

使用adadelta优化器通过学习过程更新网络中的权重:

optimizer <- mx.opt.create("adadelta", rho = 0.9, eps = 1e-5, wd = 1e-8,
                           clip_gradient = 5, rescale.grad = 1/batch.size)

使用以下代码行设置指标记录并定义一个自定义测量函数:

logger <- mx.metric.logger()
epoch.end.callback <- mx.callback.log.train.metric(period = 1, logger = logger)
batch.end.callback <- mx.callback.log.train.metric(period = 50)
mx.metric.custom_nd <- function(name, feval) {
  init <- function() {
    c(0, 0)
  }
  update <- function(label, pred, state) {
    m <- feval(label, pred)
    state <- c(state[[1]] + 1, state[[2]] + m)
    return(state)
  }
  get <- function(state) {
    list(name=name, value = (state[[2]] / state[[1]]))
  }
  ret <- (list(init = init, update = update, get = get))
  class(ret) <- "mx.metric"
  return(ret)
}

困惑度是衡量预测模型变化性的一个指标。如果困惑度是预测错误的度量,定义一个函数来计算错误,使用以下代码行:

mx.metric.Perplexity <- mx.metric.custom_nd("Perplexity", function(label, pred) {
  label <- mx.nd.reshape(label, shape = -1)
  label_probs <- as.array(mx.nd.choose.element.0index(pred, label))
  batch <- length(label_probs)
  NLL <- -sum(log(pmax(1e-15, as.array(label_probs)))) / batch
  Perplexity <- exp(NLL)
  return(Perplexity)
}

使用以下代码执行模型创建,你将看到在这个项目中我们运行了 20 次迭代:

model <- mx.model.buckets(symbol = rnn_graph_one_one,
                          train.data = train.data, eval.data = eval.data,
                          num.round = 20, ctx = devices, verbose = TRUE,
                          metric = mx.metric.Perplexity,
                          initializer = initializer,
   optimizer = optimizer,
                          batch.end.callback = NULL,
                          epoch.end.callback = epoch.end.callback)

这将给出以下输出:

Start training with 1 devices
[1] Train-Perplexity=23.490355102639
[1] Validation-Perplexity=17.6250266989171
[2] Train-Perplexity=14.4508382001841
[2] Validation-Perplexity=12.8179427398927
[3] Train-Perplexity=10.8156810097278
[3] Validation-Perplexity=9.95208184606089
[4] Train-Perplexity=8.6432934902383
[4] Validation-Perplexity=8.21806492033906
[5] Train-Perplexity=7.33073759154393
[5] Validation-Perplexity=7.03574648385079
[6] Train-Perplexity=6.32024660528852
[6] Validation-Perplexity=6.1394327776089
[7] Train-Perplexity=5.61888374338248
[7] Validation-Perplexity=5.59925324885983
[8] Train-Perplexity=5.14009899947491]
[8] Validation-Perplexity=5.29671693342219
[9] Train-Perplexity=4.77963053659987
[9] Validation-Perplexity=4.98471501141549
[10] Train-Perplexity=4.5523402301526
[10] Validation-Perplexity=4.84636357676712
[11] Train-Perplexity=4.36693337145912
[11] Validation-Perplexity=4.68806078057635
[12] Train-Perplexity=4.21294955131918
[12] Validation-Perplexity=4.53026345109037
[13] Train-Perplexity=4.08935886339982
[13] Validation-Perplexity=4.50495393289961
[14] Train-Perplexity=3.99260373800419
[14] Validation-Perplexity=4.42576079641165
[15] Train-Perplexity=3.91330125104996
[15] Validation-Perplexity=4.3941619024578
[16] Train-Perplexity=3.84730588206837
[16] Validation-Perplexity=4.33288830915229
[17] Train-Perplexity=3.78711049085869
[17] Validation-Perplexity=4.28723362252784
[18] Train-Perplexity=3.73198720637659
[18] Validation-Perplexity=4.22839393379393
[19] Train-Perplexity=3.68292148768833
[19] Validation-Perplexity=4.22187018296206
[20] Train-Perplexity=3.63728269095417
[20] Validation-Perplexity=4.17983276293299

接下来,保存模型以供以后使用,然后从磁盘加载模型以进行推理和逐字符采样文本,最后使用以下代码将预测的字符合并成一个句子:

mx.model.save(model, prefix = "one_to_one_seq_model", iteration = 20)
# the generated text is expected to be similar to the training data
set.seed(0)
model <- mx.model.load(prefix = "one_to_one_seq_model", iteration = 20)
internals <- model$symbol$get.internals()
sym_state <- internals$get.output(which(internals$outputs %in% "RNN_state"))
sym_state_cell <- internals$get.output(which(internals$outputs %in% "RNN_state_cell"))
sym_output <- internals$get.output(which(internals$outputs %in% "loss_output"))
symbol <- mx.symbol.Group(sym_output, sym_state, sym_state_cell)

使用以下代码提供种子字符以开始文本:

infer_raw <- c("e")
infer_split <- dic[strsplit(infer_raw, '') %>% unlist]
infer_length <- length(infer_split)
infer.data <- mx.io.arrayiter(data = matrix(infer_split), label = matrix(infer_split), batch.size = 1, shuffle = FALSE)
infer <- mx.infer.rnn.one(infer.data = infer.data,
                          symbol = symbol,
                          arg.params = model$arg.params,
                          aux.params = model$aux.params,
                          input.params = NULL,
                          ctx = devices)
pred_prob <- as.numeric(as.array(mx.nd.slice.axis(infer$loss_output, axis = 0, begin = infer_length-1, end = infer_length)))
pred <- sample(length(pred_prob), prob = pred_prob, size = 1) - 1
predict <- c(predict, pred)
for (i in 1:200) {
  infer.data <- mx.io.arrayiter(data = as.matrix(pred), label = as.matrix(pred), batch.size = 1,
shuffle = FALSE) 
  infer <- mx.infer.rnn.one(infer.data = infer.data,
                            symbol = symbol,
                            arg.params = model$arg.params,
                            aux.params = model$aux.params,
                            input.params = list(rnn.state = infer[[2]],
                            rnn.state.cell = infer[[3]]),
                            ctx = devices)
  pred_prob <- as.numeric(as.array(infer$loss_output))
  pred <- sample(length(pred_prob), prob = pred_prob, size = 1, replace = T) - 1
  predict <- c(predict, pred)
}

使用以下代码行来打印预测的文本,在处理预测字符并将它们合并成一个句子之前:

predict_txt <- paste0(rev_dic[as.character(predict)], collapse = "")
predict_txt_tot <- paste0(infer_raw, predict_txt, collapse = "")
# printing the predicted text
print(predict_txt_tot)

这将给出以下输出:

[1] "eNAHare I eat and in Heather where and fingo I ve next feeling or fancy to livery dust a large pived as a pockethion What isual child for of cigstening to get in a strutching voice into saying she got reaAlice glared in a Grottle got to sea-paticular and when she heard it would heard of having they began whrink bark of Hearnd again said feeting and there was going to herself up it Then does so small be THESE said Alice going my dear her before she walked at all can t make with the players and said the Dormouse sir your mak if she said to guesss I hadn t some of the crowd and one arches how come one mer really of a gomoice and the loots at encand something of one eyes purried asked to leave at she had Turtle might I d interesting tone hurry of the game the Mouse of puppled it They much put eagerly"

从输出中我们可以看到,我们的 RNN 能够自动生成文本。当然,生成的文本并不非常连贯,需要一些改进。我们可以依赖几种技术来提高连贯性,并从 RNN 生成更有意义的文本。以下是一些这些技术:

  • 实现一个词级语言模型而不是字符级语言模型。

  • 使用更大的 RNN 网络。

  • 在我们的项目中,我们使用了 LTSM 单元来构建我们的 RNN。我们本可以使用更先进的 GRU 单元而不是 LSTM 单元。

  • 我们运行了 20 次 RNN 训练迭代;这可能太少,无法得到正确的权重。我们可以尝试增加迭代次数,并验证 RNN 是否能产生更好的预测。

  • 当前模型使用了 20%的 dropout。这可以改变以检查对整体预测的影响。

  • 我们的语料库保留了很少的标点符号;因此,我们的模型在生成文本时没有预测标点符号作为字符的能力。在 RNN 训练的语料库中包含标点符号可能会产生更好的句子和单词结尾。

  • seq.ln参数决定了在预测下一个字符之前需要查找历史中的字符数量。在我们的模型中,我们将此设置为 100。这可能需要改变以检查模型是否产生更好的单词和句子。

由于空间和时间限制,我们不会在本章尝试这些选项。感兴趣的读者可以通过实验这些选项来使用字符 RNN 生成更好的单词和句子。

摘要

本章的主要主题是使用 RNN 自动生成文本。我们以关于语言模型及其在现实世界中的应用的讨论开始本章。然后,我们对循环神经网络及其在语言模型任务中的适用性进行了深入概述。讨论了传统前馈网络和 RNN 之间的差异,以更清楚地理解 RNN。然后,我们讨论了 RNN 经历的梯度爆炸和梯度消失问题及其解决方案。在获得 RNN 的详细理论基础后,我们继续实现了一个使用 RNN 的字符级语言模型。我们使用《爱丽丝梦游仙境》作为文本语料库输入来训练 RNN 模型,然后生成一个字符串作为输出。最后,我们讨论了一些改进我们的字符 RNN 模型的想法。

想必在玩老丨虎丨机时实施一个能让你更常赢的项目怎么样?这正是本书倒数第二章将要探讨的内容。第九章的标题是《用强化学习赢得老丨虎丨机》。来吧,让我们一起学习如何赚取免费的钱。

第九章:使用强化学习赢得赌场老丨虎丨机

如果你一直在关注机器学习ML)新闻,我敢肯定你一定遇到过这样的标题:计算机在多种游戏中表现优于世界冠军。如果你还没有,以下是我快速谷歌搜索的一些新闻片段,值得花时间阅读以了解情况:

强化学习RL)是人工智能AI)的一个子领域,它使计算机系统在诸如 Atari Breakout 和围棋等游戏中表现出比人类玩家更好的性能。

在本章中,我们将探讨以下主题:

  • 强化学习的概念

  • 多臂老丨虎丨机问题

  • 解决多臂老丨虎丨机问题的方法

  • 强化学习的实际应用

  • 使用强化学习技术实施一个项目,以最大化我们在多臂老丨虎丨机机器上获胜的机会

理解强化学习

强化学习是一个非常重要的领域,但有时从业者会忽视它来解决复杂、现实世界的问题。遗憾的是,甚至大多数机器学习教科书只关注监督学习和无监督学习,而完全忽略了强化学习。

作为一个领域,强化学习近年来势头强劲;然而,其起源可以追溯到 1980 年。它是由 Rich Sutton 和 Andrew Barto 发明的,Rich 的博士论文导师。即使在 1980 年代,它也被认为过时。然而,Rich 相信强化学习和它的承诺,坚持认为它最终会被认可。

使用 RL 术语进行快速谷歌搜索显示,RL 方法通常用于游戏,如跳棋和国际象棋。游戏问题是需要采取行动以找到动态问题的长期最优解的问题。它们是动态的,因为条件不断变化,有时是对其他代理的响应,这可能是对抗性的。

尽管强化学习在游戏领域的成功得到了证明,但它也是一个新兴领域,正越来越多地应用于其他领域,如金融、经济学和其他跨学科领域。在强化学习领域有许多方法,它们在人工智能和运筹学社区中独立发展。因此,这是一个机器学习从业者需要了解的关键领域。

简而言之,强化学习是一个主要关注创建从错误中学习的模型的领域。想象一下,一个人被置于一个新环境中。起初,他们会犯错误,但他们会从中学习,这样当相同的情况在未来再次出现时,他们就不会再犯同样的错误。强化学习使用以下技术来训练模型:

环境 ----------> 尝试并失败 -----------> 从失败中学习 ----------> 达成目标

从历史上看,你不能使用机器学习来让一个算法学会如何在某项任务上比人类做得更好。所能做的只是模仿人类的行为,也许计算机可以更快地运行它们。然而,强化学习(RL)使得创建比人类在执行某些任务上更好的模型成为可能。

SYBBIO 的 CEO 和联合创始人 Isaac Abhadu 在 Quora 上给出了一个精彩的解释,详细说明了强化学习与监督学习的区别。他指出,简而言之,强化学习框架与监督学习非常相似。

假设我们试图让一个算法在乒乓球游戏中表现出色。我们将输入帧通过模型运行,使其产生一些随机输出动作,就像在监督学习设置中做的那样。然而,在强化学习的情况下,我们自己并不知道目标标签是什么,因此我们不会告诉机器在每种特定情况下应该做什么。相反,我们应用一种称为策略梯度的方法。

因此,我们从一个随机网络开始,向它输入一个输入帧,使其产生一个随机输出动作来响应该帧。这个动作随后被发送回游戏引擎,使其产生另一个帧。这个循环不断重复。它唯一会提供的反馈是游戏的计分板。每当我们的代理做对了——也就是说,它产生了一些成功的序列——它将获得一分,通常被称为奖励。每当它产生一个失败的序列,它将失去一分——这被称为惩罚

代理追求的最终目标是不断更新其策略,以获得尽可能多的奖励。因此,随着时间的推移,它将找出如何在这款游戏中战胜人类。

强化学习不是一件快速的事情。代理最初会输很多。但我们会继续给它输入帧,使其不断产生随机输出动作,并最终发现成功的动作。它将不断积累关于哪些动作是成功的知识,经过一段时间后,将变得不可战胜。

强化学习与其他机器学习算法的比较

强化学习涉及一个环境,即要解决的问题集,以及一个智能体,它简单地说就是人工智能算法。智能体将执行某个动作,该动作的结果将导致智能体状态的改变。这种改变导致智能体获得奖励,这是一种积极的奖励,或者是一种惩罚,这是一种由于执行了错误动作而产生的负面奖励。通过重复动作和奖励过程,智能体学习环境。它理解各种状态和期望的以及不期望的各种动作。执行动作并从奖励中学习的过程就是强化学习。以下图示展示了强化学习中智能体和环境之间的关系:

图片

强化学习中智能体和环境之间的关系

强化学习(RL)、深度学习(DL)和机器学习(ML)都以某种方式支持自动化。它们都涉及从给定数据中学习。然而,RL 与其他技术的区别在于,RL 通过试错来学习正确的动作,而其他技术则专注于通过在现有数据中寻找模式来学习。另一个关键区别是,为了使深度学习和机器学习算法更好地学习,我们需要向它们提供大量的标记数据集,而强化学习则不需要这样做。

通过将训练家中的宠物作为类比,让我们更好地理解强化学习。想象一下,我们正在教我们的宠物狗,桑迪,一些新的技巧。不幸的是,桑迪不懂英语;因此,我们需要找到一种替代方法来训练他。我们模拟一个情境,桑迪尝试以许多不同的方式做出回应。对于任何期望的回应,我们都会用骨头奖励桑迪。这使宠物狗明白,下次他遇到类似的情况时,他会执行期望的行为,因为他知道有奖励。所以,这是从积极回应中学习;如果他受到负面回应,比如皱眉,他将会被劝阻进行不期望的行为。

强化学习术语

让我们通过宠物狗训练的类比来理解强化学习的关键术语——智能体、环境、状态、策略、奖励和惩罚:

  • 我们的宠物狗桑迪是暴露在环境中的智能体。

  • 环境是一个房子或游乐区,这取决于我们想要教给桑迪什么。

  • 每个遇到的情况都称为状态。例如,桑迪爬到床下或奔跑可以被解释为状态。

  • 智能体桑迪通过执行动作来改变从一个状态到另一个状态。

  • 在状态改变后,我们根据执行的动作给予智能体奖励或惩罚。

  • 策略指的是选择动作以找到更好结果的战略。

既然我们已经理解了每个强化学习(RL)术语,让我们更正式地定义这些术语,并在下面的图中可视化智能体的行为:

  • 状态:世界的完整描述被称为状态。我们不会抽象掉世界上存在的任何信息。状态可以是位置、常数或动态的。状态通常记录在数组、矩阵或更高阶的张量中。

  • 动作:环境通常定义可能的动作;也就是说,不同的环境根据智能体导致不同的动作。智能体的有效动作记录在一个称为动作空间的空间中。环境中可能的有效动作数量是有限的。

  • 环境:这是智能体生活和与之交互的空间。对于不同类型的环境,我们使用不同的奖励和政策。

  • 奖励和回报:奖励函数是强化学习中必须始终跟踪的函数。它在调整、优化算法和停止算法训练中起着至关重要的作用。奖励是根据当前世界状态、刚刚采取的动作和下一个世界状态计算的。

  • 策略:在强化学习中,策略是智能体用于选择下一个动作的规则;策略也被称为智能体的大脑。

看看下面的流程图,以更好地理解这个过程:

图片

强化学习中的智能体行为

在每个步骤,t,智能体执行以下任务:

  1. 执行动作 a[t]

  2. 接收观察结果 s[t]

  3. 接收标量奖励 r[t]

环境执行以下任务:

  1. 行动 a[t] 的变化

  2. 发出观察结果 s[t+1]

  3. 发出标量奖励 r[t+1]

时间步长 t 在每次迭代后增加。

多臂老丨虎丨机问题

让我用一个类比来更好地理解这个话题。你喜欢披萨吗?我非常喜欢!我在班加罗尔的最喜欢的餐厅提供美味的披萨。每次我想吃披萨时,我都会去这个地方,而且我几乎可以肯定我会得到最好的披萨。然而,每次都去同一家餐厅让我担心我错过了镇上其他地方更好吃的披萨!

一个可用的替代方案是逐一尝试餐厅并品尝那里的披萨,但这意味着我最终可能会吃到不太好的披萨的概率非常高。然而,这是我发现比我所知道的餐厅提供更好披萨的唯一方法。我知道你一定在想,为什么我要谈论披萨,而我应该谈论强化学习。让我切入正题。

这个任务中的困境源于信息不完整。换句话说,要解决这个问题,必须收集足够的信息来制定最佳的整体策略,然后探索新的行动。这最终将导致总体不良体验的最小化。这种情况也可以被称为探索利用的困境:

图片

探索与利用的困境

上述图表恰当地总结了最好的披萨问题。

多臂老丨虎丨机问题MABP)是披萨类比的一个简化形式。它用来表示类似的问题,并且找到解决这些问题的良好策略已经在很大程度上帮助了许多行业。

老丨虎丨机被定义为偷你钱的人!单臂老丨虎丨机是一种简单的老丨虎丨机。我们在赌场中找到这种机器:你将硬币投入老丨虎丨机,拉动杠杆,并向幸运之神祈祷以获得即时奖励。但百万美元的问题是为什么老丨虎丨机被称为老丨虎丨机?结果证明,所有赌场都这样配置老丨虎丨机,使得所有赌徒最终都会输钱!

多臂老丨虎丨机是一个假设但复杂的老丨虎丨机,其中我们有一排排列的多台老丨虎丨机。赌徒可以拉动几个杠杆,每个杠杆给出不同的回报。以下图表描述了对应奖励的概率分布,这些奖励对于每一层都是不同的,并且对赌徒来说是未知的:

多臂老丨虎丨机

给定这些老丨虎丨机和一系列初始试验后,任务是确定拉动哪个杠杆以获得最大奖励。换句话说,拉动任何一个臂都会给我们一个随机奖励,要么 R=+1 表示成功,要么 R=0 表示失败;这被称为即时奖励。发出 1 或 0 奖励的多臂老丨虎丨机被称为伯努利。目标是按顺序拉动臂,同时收集信息以最大化长期总回报。形式上,伯努利 MABP 可以描述为 (A,R) 的元组,其中以下适用:

  • 我们有 K 台机器,具有奖励概率 {θ1,…,θK}。

  • 在每个时间步 t,我们选择一台老丨虎丨机的动作 a 并获得奖励 r

  • A 是一组动作,每个动作都指代与一台老丨虎丨机的交互。动作 a 的值是期望奖励,![]。如果时间步 t 中的动作 a 在第 i 台机器上,那么 ![]。Q(a) 通常被称为动作值函数。

  • R 是一个奖励函数。在伯努利老丨虎丨机的情况下,我们以随机的方式观察到奖励 r。在时间步 t,![] 可能以概率 ![,] 返回奖励 1,否则为 0。

我们可以用多种策略解决 MABP。我们将在本节中简要回顾一些策略。为了确定最佳策略并比较不同的策略,我们需要一个定量方法。一种方法是在一定预定义的试验次数后直接计算累积奖励。比较每种策略的累积奖励为我们提供了识别问题最佳策略的机会。

有时,我们可能已经知道给定老丨虎丨机问题的最佳动作。在这些情况下,研究后悔的概念可能很有趣。

让我们想象一下,我们已知给定老丨虎丨机问题的最佳拉动臂的详细信息。假设通过反复拉动这个最佳臂,我们可以获得最大的期望奖励,这在下图的水平线上表示:

图片

通过拉动最佳臂在 MABP 中获得的最大奖励

根据问题描述,我们需要通过拉动多臂老丨虎丨机的不同臂进行重复试验,直到我们大致确定在时间 t 时拉动哪个臂可以获得最大的平均回报。在探索和决定最佳臂的过程中涉及到许多轮次。这些轮次,通常称为 试验,也会产生一些损失,这被称为 后悔。换句话说,我们希望在学习的阶段也最大化奖励。后悔可以概括为衡量我们没有选择最优臂的后悔程度。

下图展示了由于尝试找到最佳臂而产生的后悔:

图片

MAB 中的后悔概念

解决 MABP 的策略

根据探索的方式,解决 MABP 的策略可以分为以下几种类型:

  • 无探索

  • 随机探索

  • 智能探索,优先考虑不确定性

让我们深入了解属于每种策略类型的某些算法的细节。

让我们考虑一个非常简单的方法,即长时间只玩一台老丨虎丨机。在这里,我们不做任何探索,只是随机选择一个臂并反复拉动以最大化长期奖励。你一定想知道这是怎么工作的!让我们来探究一下。

在概率论中,大数定律是一个描述进行相同实验大量次的结果的定理。根据这个定律,从大量试验中获得的结果的平均值应该接近期望值,并且随着试验次数的增加而越来越接近。

我们可以只玩一台机器进行大量轮次,以便最终根据大数定律估计真实的奖励概率。

然而,这种策略存在一些问题。首先,我们不知道大量轮次的价值。其次,重复玩相同的插槽需要超级资源密集。最重要的是,我们无法保证使用这种策略将获得最佳长期奖励。

ε-贪婪算法

强化学习中的贪婪算法是一个完全的利用算法,它不考虑探索。贪婪算法总是选择具有最高估计动作值的动作。动作值是根据过去经验通过平均迄今为止观察到的与目标动作相关的奖励来估计的。

然而,如果我们能够成功地将动作值估计为期望的动作值;如果我们知道真实的分布,我们就可以直接选择最佳的动作。ε-贪婪算法是贪婪和随机方法的简单组合。

ε有助于进行这种估计。它将探索作为贪婪算法的一部分。为了对抗总是根据估计的动作值选择最佳动作的逻辑,偶尔,ε概率会为了探索而选择一个随机动作;其余时间,它表现得像原始的贪婪算法,选择已知的最佳动作。

在此算法中,ε是一个可调整的参数,它决定了采取随机行动而不是基于原则行动的概率。在训练过程中,也可以调整ε的值。通常,在训练过程的开始,ε的值通常初始化为一个较大的概率。由于环境未知,较大的ε值鼓励探索。然后,该值逐渐减少到一个小的常数(通常设置为 0.1)。这将增加利用选择的速率。

由于算法的简单性,这种方法已成为最近大多数强化学习算法的事实上的技术。

尽管该算法被广泛使用,但这种方法远非最优,因为它只考虑了动作是否最有利。

鲍尔兹曼或 softmax 探索

鲍尔兹曼探索也称为softmax 探索。与始终采取最佳动作或始终采取随机动作相反,这种探索通过加权概率同时偏好这两种方法。这是通过对网络对每个动作的值估计进行 softmax 操作来实现的。在这种情况下,尽管不能保证,但代理估计为最佳的动作最有可能被选择。

拉普拉斯探索相较于ε贪婪算法具有最大的优势。这种方法了解其他动作可能值的概率。换句话说,让我们想象一个智能体有五个可用的动作。通常,在ε贪婪方法中,四个动作被估计为非最优,并且它们都被同等考虑。然而,在拉普拉斯探索中,这四个次优选择根据它们的相对价值进行权衡。这使得智能体能够忽略估计为很大程度上次优的动作,并更多地关注可能具有潜力的、但不一定是理想的动作。

温度参数(τ)控制 softmax 分布的扩散,使得在训练开始时所有动作都被同等考虑,而在训练结束时动作分布变得稀疏。该参数随时间衰减。

衰减的ε贪婪

ε的值对于确定ε贪婪算法在给定问题上的表现至关重要。我们不必在开始时设置此值然后逐渐减小,而是可以使ε依赖于时间。例如,ε可以保持为 1 / log(t + 0.00001)。随着时间的推移,ε的值将不断减少。这种方法有效,因为随着时间的推移ε的减少,我们对最优动作的信心增强,探索的需求减少。

随机选择动作的问题在于,经过足够的时间步数后,即使我们知道某些臂是坏的,此算法仍会以概率epsilon/n继续选择它。本质上,我们正在探索一个坏动作,这听起来并不很高效。绕过这一问题的方法可能是优先探索具有强大潜力的臂,以获得最优值。

上置信界算法

上置信界(UCB)算法是 MABP 最流行和最广泛使用的解决方案。该算法基于面对不确定性的乐观原则。这本质上意味着,我们对臂的不确定性越小,探索该臂的重要性就越大。

假设我们有两个可以尝试的臂。如果我们已经尝试了第一个臂 100 次,但第二个臂只尝试了一次,那么我们对第一个臂的回报可能相当有信心。然而,我们对第二个臂的回报非常不确定。这导致了 UCB 算法系列的产生。这可以通过以下图表进一步解释:

图片

解释上置信界算法的插图

在前面的图表中,每个条形代表不同的臂或动作。红色点代表真实的期望奖励,条形的中心代表观察到的平均奖励。条形的宽度代表置信区间。我们已经知道,根据大数定律,我们拥有的样本越多,观察到的平均数就越接近真实平均数,条形就越缩小。

UCB 算法背后的思想是始终选择具有最高上界的臂或动作,即观察到的平均数和置信区间单侧宽度的总和。这平衡了对尚未尝试很多次的臂的探索和对已经尝试过的臂的利用。

Thompson 采样

Thompson 采样是 MABP(多臂老丨虎丨机)中最古老的启发式算法之一。它是一种基于贝叶斯思想的随机算法,在几项研究表明它比其他方法具有更好的经验性能后,最近引起了极大的兴趣。

我在stats.stackexchange.com/questions/187059/could-anyone-explain-thompson-sampling-in-simplest-terms上找到了一个美丽的解释。我认为我无法比这个更好地解释 Thompson 采样。您可以参考这个链接以获取更多信息。

多臂老丨虎丨机——现实世界用例

在现实世界中,我们遇到了许多与本章中我们回顾的 MABP 类似的情况。我们可以将这些 RL 策略应用于所有这些情况。以下是一些与 MABP 类似的现实世界用例:

  • 在众多替代方案中找到最佳药物/药品

  • 在可能的产品中确定推出最佳产品

  • 决定为每个网站分配的流量(用户)数量

  • 确定推出产品的最佳营销策略

  • 确定最佳股票组合以最大化利润

  • 找出最佳投资股票

  • 在给定的地图上找出最短路径

  • 广告和文章的点击率预测

  • 根据文章内容预测在路由器上缓存的最有益内容

  • 为组织不同部门分配资金

  • 在有限的时间和任意选择阈值下,从一群学生中挑选出表现最佳的运动员

到目前为止,我们已经涵盖了几乎所有我们需要知道的基本细节,以便将 RL(强化学习)应用到 MABP(多臂老丨虎丨机)的实际实现中。让我们在下一节中开始编写解决 MABP 的代码解决方案。

使用 UCB 和 Thompson 采样算法解决 MABP 问题

在这个项目中,我们将使用上置信限和 Thompson 采样算法来解决 MABP 问题。我们将比较它们在三种不同情况下的性能和策略——标准奖励、标准但更波动的奖励以及有些混乱的奖励。让我们准备模拟数据,一旦数据准备就绪,我们将使用以下代码查看模拟数据:

# loading the required packages
library(ggplot2)
library(reshape2)
# distribution of arms or actions having normally distributed
# rewards with small variance
# The data represents a standard, ideal situation i.e.
# normally distributed rewards, well seperated from each other.
mean_reward = c(5, 7.5, 10, 12.5, 15, 17.5, 20, 22.5, 25, 26)
reward_dist = c(function(n) rnorm(n = n, mean = mean_reward[1], sd = 2.5),
                function(n) rnorm(n = n, mean = mean_reward[2], sd = 2.5),
                function(n) rnorm(n = n, mean = mean_reward[3], sd = 2.5),
                function(n) rnorm(n = n, mean = mean_reward[4], sd = 2.5),
                function(n) rnorm(n = n, mean = mean_reward[5], sd = 2.5),
                function(n) rnorm(n = n, mean = mean_reward[6], sd = 2.5),
                function(n) rnorm(n = n, mean = mean_reward[7], sd = 2.5),
                function(n) rnorm(n = n, mean = mean_reward[8], sd = 2.5),
                function(n) rnorm(n = n, mean = mean_reward[9], sd = 2.5),
                function(n) rnorm(n= n, mean = mean_reward[10], sd = 2.5))
#preparing simulation data
dataset = matrix(nrow = 10000, ncol = 10)
for(i in 1:10){
  dataset[, i] = reward_dist[[i]](n = 10000)
}
# assigning column names
colnames(dataset) <- 1:10
# viewing the dataset that is just created with simulated data
View(dataset)

这将给出以下输出:

图片

现在,创建一个包含臂和奖励组合的熔化数据集,然后使用以下代码将臂列转换为名义类型:

# creating a melted dataset with arm and reward combination
dataset_p = melt(dataset)[, 2:3]
colnames(dataset_p) <- c("Bandit", "Reward")
# converting the arms column in the dataset to nominal type
dataset_p$Bandit = as.factor(dataset_p$Bandit)
# viewing the dataset that is just melted
View(dataset_p)

这将给出以下输出:

图片

现在,使用以下代码绘制带奖机的奖励分布:

#ploting the distributions of rewards from bandits
ggplot(dataset_p, aes(x = Reward, col = Bandit, fill = Bandit)) +
  geom_density(alpha = 0.3) +
  labs(title = "Reward from different bandits")

这将给出以下输出:

图片

现在,让我们使用以下代码在假设的手臂上实现 UCB 算法,该手臂具有正态分布:

# implementing upper confidence bound algorithm
UCB <- function(N = 1000, reward_data){
  d = ncol(reward_data)
  bandit_selected = integer(0)
  numbers_of_selections = integer(d)
  sums_of_rewards = integer(d)
  total_reward = 0
  for (n in 1:N) {
    max_upper_bound = 0
    for (i in 1:d) {
      if (numbers_of_selections[i] > 0){
        average_reward = sums_of_rewards[i] / numbers_of_selections[i]
        delta_i = sqrt(2 * log(1 + n * log(n)²) /
numbers_of_selections[i])
        upper_bound = average_reward + delta_i
      } else {
        upper_bound = 1e400
      }
      if (upper_bound > max_upper_bound){
        max_upper_bound = upper_bound
        bandit = i
      }
    }
    bandit_selected = append(bandit_selected, bandit)
    numbers_of_selections[bandit] = numbers_of_selections[bandit] + 1
    reward = reward_data[n, bandit]
    sums_of_rewards[bandit] = sums_of_rewards[bandit] + reward
    total_reward = total_reward + reward
  }
  return(list(total_reward = total_reward, bandit_selected bandit_selected, numbers_of_selections = numbers_of_selections, sums_of_rewards = sums_of_rewards))
}
# running the UCB algorithm on our
# hypothesized arms with normal distributions
UCB(N = 1000, reward_data = dataset)

您将得到以下结果:

$total_reward
       1
25836.91
$numbers_of_selections
 [1]   1   1   1   1   1   1   2   1  23 968
$sums_of_rewards
 [1]     4.149238    10.874230     5.998070    11.951624    18.151797    21.004781    44.266832    19.370479   563.001692
[10] 25138.139942

接下来,我们将使用正态-伽马先验和正态似然函数实现 Thompson 抽样算法,以以下代码估计后验分布:

# Thompson sampling algorithm
rnormgamma <- function(n, mu, lambda, alpha, beta){
  if(length(n) > 1)
    n <- length(n)
  tau <- rgamma(n, alpha, beta)
  x <- rnorm(n, mu, 1 / (lambda * tau))
  data.frame(tau = tau, x = x)
}
T.samp <- function(N = 500, reward_data, mu0 = 0, v = 1, alpha = 2,
beta = 6){
  d = ncol(reward_data)
  bandit_selected = integer(0)
  numbers_of_selections = integer(d)
  sums_of_rewards = integer(d)
  total_reward = 0
  reward_history = vector("list", d)
  for (n in 1:N){
    max_random = -1e400
    for (i in 1:d){
      if(numbers_of_selections[i] >= 1){
        rand = rnormgamma(1,
                          (v * mu0 + numbers_of_selections[i] * mean(reward_history[[i]])) / (v + numbers_of_selections[i]),
                          v + numbers_of_selections[i],
                          alpha + numbers_of_selections[i] / 2,
                          beta + (sum(reward_history[[i]] - mean(reward_history[[i]])) ^ 2) / 2 + ((numbers_of_selections[i] * v) / (v + numbers_of_selections[i])) * (mean(reward_history[[i]]) - mu0) ^ 2 / 2)$x
      }else {
        rand = rnormgamma(1, mu0, v, alpha, beta)$x
      }
      if(rand > max_random){
        max_random = rand
        bandit = i
      }
    }
    bandit_selected = append(bandit_selected, bandit)
    numbers_of_selections[bandit] = numbers_of_selections[bandit] + 1
    reward = reward_data[n, bandit]
    sums_of_rewards[bandit] = sums_of_rewards[bandit] + reward
    total_reward = total_reward + reward
    reward_history[[bandit]] = append(reward_history[[bandit]], reward)
  }
  return(list(total_reward = total_reward, bandit_selected = bandit_selected, numbers_of_selections = numbers_of_selections, sums_of_rewards = sums_of_rewards))
}
# Applying Thompson sampling using normal-gamma prior and Normal likelihood to estimate posterior distributions
T.samp(N = 1000, reward_data = dataset, mu0 = 40)

您将得到以下结果:

$total_reward
      10
24434.24
$numbers_of_selections
 [1]  16  15  15  14  14  17  16  19  29 845
$sums_of_rewards
 [1]    80.22713   110.09657   141.14346   171.41301   212.86899   293.30138   311.12230   423.93256   713.54105 21976.59855

从结果中,我们可以推断出 UCB 算法迅速识别出第 10 个手臂产生最多的奖励。我们还观察到,Thompson 抽样在找到最佳选择之前尝试了更多次最差的带奖机。

现在,让我们使用以下代码模拟具有大方差正态分布奖励的带奖机数据,并绘制奖励分布:

# Distribution of bandits / actions having normally distributed rewards with large variance
# This data represents an ideal but more unstable situation: normally distributed rewards with much larger variance,
# thus not well separated from each other.
mean_reward = c(5, 7.5, 10, 12.5, 15, 17.5, 20, 22.5, 25, 26)
reward_dist = c(function(n) rnorm(n = n, mean = mean_reward[1], sd = 20),
                function(n) rnorm(n = n, mean = mean_reward[2], sd = 20),
                function(n) rnorm(n = n, mean = mean_reward[3], sd = 20),
                function(n) rnorm(n = n, mean = mean_reward[4], sd = 20),
                function(n) rnorm(n = n, mean = mean_reward[5], sd = 20),
                function(n) rnorm(n = n, mean = mean_reward[6], sd = 20),
                function(n) rnorm(n = n, mean = mean_reward[7], sd = 20),]
                function(n) rnorm(n = n, mean = mean_reward[8], sd = 20),
                function(n) rnorm(n = n, mean = mean_reward[9], sd = 20),
                function(n) rnorm(n = n, mean = mean_reward[10], sd = 20))
#preparing simulation data
dataset = matrix(nrow = 10000, ncol = 10)
for(i in 1:10){
  dataset[, i] = reward_dist[[i]](n = 10000)
}
colnames(dataset) <- 1:10
dataset_p = melt(dataset)[, 2:3]
colnames(dataset_p) <- c("Bandit", "Reward")
dataset_p$Bandit = as.factor(dataset_p$Bandit)
#plotting the distributions of rewards from bandits
ggplot(dataset_p, aes(x = Reward, col = Bandit, fill = Bandit)) +
  geom_density(alpha = 0.3) +
  labs(title = "Reward from different bandits")

您将得到以下结果图:

图片

使用以下代码对具有更高方差的奖励应用 UCB:

# Applying UCB on rewards with higher variance
UCB(N = 1000, reward_data = dataset)

您将得到以下输出:

$total_reward
       1
25321.39
$numbers_of_selections
 [1]   1   1   1   3   1   1   2   6 903  81
$sums_of_rewards
 [1]     2.309649    -6.982907   -24.654597    49.186498     8.367174   -16.211632    31.243270   104.190075 23559.216706  1614.725305

接下来,使用以下代码对具有更高方差的奖励应用 Thompson 抽样:

# Applying Thompson sampling on rewards with higher variance
T.samp(N = 1000, reward_data = dataset, mu0 = 40)

您将得到以下输出:

$total_reward
       2
24120.94
$numbers_of_selections
 [1]  16  15  14  15  15  17  20  21 849  18
$sums_of_rewards
 [1]    94.27878    81.42390   212.00717   181.46489   140.43908   249.82014   368.52864   397.07629 22090.20740 305.69191

从结果中,我们可以推断出,当奖励的波动性更大时,UCB 算法更容易陷入次优选择,并且永远找不到最优的带奖机。Thompson 抽样通常更稳健,能够在各种情况下找到最优的带奖机。

现在,让我们通过以下代码模拟更混乱的分布型带奖机数据,并绘制带奖机的奖励分布:

# Distribution of bandits / actions with rewards of different distributions
# This data represents an more chaotic (possibly more realistic) situation:
# rewards with different distribution and different variance.
mean_reward = c(5, 7.5, 10, 12.5, 15, 17.5, 20, 22.5, 25, 26)
reward_dist = c(function(n) rnorm(n = n, mean = mean_reward[1], sd = 20),
                function(n) rgamma(n = n, shape = mean_reward[2] / 2, rate
                 = 0.5),
                function(n) rpois(n = n, lambda = mean_reward[3]),
                function(n) runif(n = n, min = mean_reward[4] - 20, max = mean_reward[4] + 20),
                function(n) rlnorm(n = n, meanlog = log(mean_reward[5]) - 0.25, sdlog = 0.5),
                function(n) rnorm(n = n, mean = mean_reward[6], sd = 20),
                function(n) rexp(n = n, rate = 1 / mean_reward[7]),
                function(n) rbinom(n = n, size = mean_reward[8] / 0.5, prob = 0.5),
                function(n) rnorm(n = n, mean = mean_reward[9], sd = 20),
                function(n) rnorm(n = n, mean = mean_reward[10], sd = 20))
#preparing simulation data
dataset = matrix(nrow = 10000, ncol = 10)
for(i in 1:10){
  dataset[, i] = reward_dist[[i]](n = 10000)
}
colnames(dataset) <- 1:10
dataset_p = melt(dataset)[, 2:3]
colnames(dataset_p) <- c("Bandit", "Reward")
dataset_p$Bandit = as.factor(dataset_p$Bandit)
#plotting the distributions of rewards from bandits
ggplot(dataset_p, aes(x = Reward, col = Bandit, fill = Bandit)) +
  geom_density(alpha = 0.3) +
  labs(title = "Reward from different bandits")

您将得到以下结果图:

图片

使用以下代码通过 UCB 对具有不同分布的奖励应用:

# Applying UCB on rewards with different distributions
UCB(N = 1000, reward_data = dataset)

您将得到以下输出:

$total_reward
       1
22254.18
$numbers_of_selections
 [1]   1   1   1   1   1   1   1 926  61   6
$sums_of_rewards
 [1]     6.810026     3.373098     8.000000    12.783859    12.858791    11.835287     1.616978 20755.000000 1324.564987   117.335467

接下来,使用以下代码对具有不同分布的奖励应用 Thompson 抽样:

# Applying Thompson sampling on rewards with different distributions
T.samp(N = 1000, reward_data = dataset, mu0 = 40)

您将得到以下结果:

$total_reward
       2
24014.36
$numbers_of_selections
 [1]  16  14  14  14  14  15  14  51 214 634
$sums_of_rewards
 [1]    44.37095   127.57153   128.00000   142.66207   191.44695   169.10430   150.19486  1168.00000  5201.69130 16691.32118

从前面的结果中,我们看到两种算法的性能相似。Thompson 抽样算法在选择它认为最好的带奖机之前尝试所有带奖机多次的主要原因是我们在这个项目中选择了一个具有相对较高均值的先验分布。由于先验具有更大的均值,算法在开始时更倾向于探索而非利用。只有当算法非常确信它已经找到了最佳选择时,它才会将利用的价值置于探索之上。如果我们降低先验的均值,利用的价值就会更高,算法就会更快地停止探索。通过改变使用的先验分布,您可以调整探索与利用的相对重要性,以适应具体问题。这是更多证据,突出了 Thompson 抽样算法的灵活性。

摘要

在本章中,我们学习了强化学习(RL)。我们首先定义了 RL 及其与其他机器学习(ML)技术的区别。然后,我们回顾了多臂老丨虎丨机问题(MABP)的细节,并探讨了可以用来解决此问题的各种策略。讨论了与 MABP 类似的用例。最后,通过使用 UCB 和 Thompson 抽样算法,在三个不同的模拟数据集上实现了项目来解决 MABP。

我们几乎到达了这本书的结尾。本书的附录未来之路,正如其名所示,是一个指导章节,建议从现在开始如何成为更好的 R 数据科学家。我非常兴奋,我已经到达了 R 项目之旅的最后阶段。你们也和我一起吗?

第十章:前方的路

恭喜你到达这本书的这一阶段!对我来说,写作各个章节和实施项目是一次激动人心的旅程。由于这次旅程,我学到了很多,希望你的经历也是一样的。

最后,我们来到了本书的结尾章节!

2018 年可以被认为是数据科学、机器学习ML)和人工智能AI)的繁荣年份。只需看看有多少初创公司在他们的标语中包含 ML 和 AI,大公司收购的焦点,以及最大技术会议的主题。我们很快就会意识到数据、ML 和 AI 无处不在,我相信这种趋势在未来几年还将持续。越来越多的行业将大规模利用 ML 和 AI。这本质上将在实施 ML 和 AI 的业务人才方面创造差距。因此,这是在这个领域投入更多学习的最佳时机。AI 和 ML 技能与商业技能的结合将在行业内受到高度重视。

我们还需要意识到,人工智能和机器学习领域正在快速发展——新的机器学习算法、新的平台、新的基础设施和新的数据类型只是其中几个例子。为了保持相关性,唯一的选择是让自己跟上最新的趋势和技术。其他最被追求的特征是忘记旧知识,学习新知识,以及足够灵活和谦逊地说,不知道没关系,但我愿意学习

最后,最好的学习方法和保持领先的方法是什么?当然,资源丰富——大规模开放在线课程MOOCs)、书籍、博客、会议、研讨会、课程等等。所需的一切就是时间和学习的意愿。为了快乐地超越,快乐地学习吧!

posted @ 2025-09-03 10:23  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报