Java-机器学习-全-

Java 机器学习(全)

原文:annas-archive.org/md5/618b6772e26c666b9787d77f8f28d78b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

《Java 机器学习,第二版》 将为您提供从复杂数据中快速获得洞察力的技术和工具。您将从学习如何将机器学习方法应用于各种常见任务开始,包括分类、预测、预测、篮子分析和聚类。

这是一个实用的教程,使用实际示例逐步介绍一些机器学习的实际应用。在不过分回避技术细节的同时,您将使用清晰且实用的示例探索如何使用 Java 库进行机器学习。您将探索如何准备分析数据、选择机器学习方法以及衡量过程的成功。

本书面向的对象

如果您想学习如何使用 Java 的机器学习库从数据中获得洞察力,这本书就是为您准备的。它将帮助您快速上手,并提供您成功创建、定制和轻松部署机器学习应用所需的技能。为了充分利用本书,您应该熟悉 Java 编程和一些基本的数据挖掘概念,但不需要有机器学习的前期经验。

为了充分利用本书

本书假设用户具备 Java 语言的实际知识以及机器学习的基本概念。本书大量使用了 JAR 格式的外部库。假设用户知道如何在终端或命令提示符中使用 JAR 文件,尽管本书也解释了如何进行这一操作。用户可以轻松地在任何通用的 Windows 或 Linux 系统上使用本书。

下载示例代码文件

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

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

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

  2. 选择支持选项卡。

  3. 点击代码下载和勘误表。

  4. 在搜索框中输入本书的名称,并按照屏幕上的说明操作。

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

  • Windows 的 WinRAR/7-Zip

  • Mac 的 Zipeg/iZip/UnRarX

  • Linux 的 7-Zip/PeaZip

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

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

下载彩色图像

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

使用的约定

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

CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“解压存档,并在提取的存档中找到weka.jar。”

代码块设置为以下格式:

data.defineSingleOutputOthersInput(outputColumn); 

EncogModel model = new EncogModel(data); 
model.selectMethod(data, MLMethodFactory.TYPE_FEEDFORWARD);
model.setReport(new ConsoleStatusReportable()); 
data.normalize(); 

任何命令行输入或输出都按照以下方式编写:

$ java -cp moa.jar -javaagent:sizeofag.jar moa.gui.GUI

粗体: 表示新术语、重要单词或你在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示为粗体。以下是一个示例:“我们可以通过点击文件 | 另存为,并在保存对话框中选择 CSV,将其转换为逗号分隔值CSV)格式。”

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

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

联系我们

我们欢迎读者的反馈。

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

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

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

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

评论

请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?潜在的读者可以看到并使用你的无偏见意见来做出购买决定,我们 Packt 可以了解你对我们的产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

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

第一章:应用机器学习快速入门

本章介绍了机器学习的基础知识,阐述了常见的主题和概念,使逻辑易于理解,并使你能够熟悉这个主题。目标是快速学习应用机器学习的逐步过程并掌握主要的机器学习原则。在本章中,我们将涵盖以下主题:

  • 机器学习和数据科学

  • 数据和问题定义

  • 数据收集

  • 数据预处理

  • 无监督学习

  • 监督学习

  • 泛化和评估

如果你已经熟悉机器学习并且渴望开始编码,那么请快速跳转到本节之后的章节。然而,如果你需要刷新记忆或澄清一些概念,那么强烈建议重新回顾本章中介绍的主题。

机器学习和数据科学

现在,每个人都谈论机器学习和数据科学。那么,机器学习究竟是什么呢?它与数据科学有什么关系?这两个术语通常被混淆,因为它们经常使用相同的方法并且有显著的重叠。因此,让我们首先明确它们是什么。Josh Wills 在推特上写道:

“数据科学家是一个比任何软件工程师都擅长统计学,比任何统计学家都擅长软件工程的人。”

– Josh Wills

更具体地说,数据科学包括从统计、计算机科学和其他领域整合方法以从数据中获得洞察力的整个过程。在实践中,数据科学包括数据采集、清洗、分析、可视化和部署的迭代过程。

机器学习,另一方面,主要关注在数据科学流程的分析和建模阶段使用的通用算法和技术。

使用机器学习解决问题

在不同的机器学习方法中,有三种主要的学习方法,如下列所示:

  • 监督学习

  • 无监督学习

  • 强化学习

给定一组示例输入 X 和它们的输出 Y,监督学习的目标是学习一个通用的映射函数 f,它将输入转换为输出,即 f: (X,Y)。

监督学习的例子是信用卡欺诈检测,其中学习算法被提供标记为正常或可疑的信用卡交易(矩阵 X)和向量 Y。学习算法产生一个决策模型,将未见交易标记为正常或可疑(这就是 f 函数)。

相比之下,无监督学习算法不假设有给定的输出标签,因为它们专注于学习数据的结构,例如将相似输入分组到簇中。因此,无监督学习可以揭示数据中的隐藏模式。无监督学习的一个例子是基于物品的推荐系统,其中学习算法发现一起购买的相似物品;例如,购买书籍 A 的人也购买了书籍 B。

强化学习从完全不同的角度处理学习过程。它假设一个智能体,可以是机器人、机器人或计算机程序,与动态环境互动以实现特定目标。环境由一组状态描述,智能体可以采取不同的行动从一个状态移动到另一个状态。一些状态被标记为目标状态,如果智能体达到这个状态,它会收到大量奖励。在其他状态下,奖励较小,不存在,甚至可能是负的。强化学习的目标是找到一个最优策略或映射函数,该函数指定在每个状态下采取的动作,而不需要教师明确指出这会导致目标状态与否。强化学习的一个例子是驾驶车辆的程序,其中状态对应于驾驶条件,例如当前速度、路段信息、周围交通、速度限制和道路上的障碍物;而动作可以是驾驶操作,如左转或右转、停车、加速和继续。学习算法产生一个策略,该策略指定在特定驾驶条件配置下应采取的动作。

在本书中,我们将仅关注监督学习和无监督学习,因为它们共享许多概念。如果您对强化学习感兴趣,可以从理查德·S·萨顿和安德鲁·巴托的《强化学习:入门》(MIT Press,2018 年)这本书开始。

应用机器学习工作流程

本书强调应用机器学习。我们希望为您提供在不同环境中使学习算法工作所需的实用技能。在机器学习中,我们不会花太多时间在数学和理论上,而是会花更多时间在实用、动手技能(和一些技巧)上,以便使这些内容在实际应用中运行良好。我们将重点关注监督学习和无监督学习,并学习数据科学中构建应用机器学习工作流程的基本步骤。

应用机器学习应用中的典型工作流程包括回答一系列可以总结为以下步骤的问题:

图片

  1. 数据和问题定义:第一步是提出有趣的问题,例如:你正在尝试解决的问题是什么为什么它很重要哪种结果格式可以回答你的问题这是一个简单的是/否答案吗你需要从可用的问题中选择一个吗

  2. 数据收集:一旦你有一个问题要解决,你将需要数据。问问自己你需要什么样的数据来回答问题。你能从可用的来源获取数据吗你需要结合多个来源吗你需要生成数据吗是否存在任何抽样偏差需要多少数据

  3. 数据预处理:第一个数据预处理任务是数据清洗。一些例子包括填充缺失值、平滑噪声数据、移除异常值和解决一致性。这通常随后是多个数据源的集成和数据转换到特定的范围(归一化)、到值区间(离散化区间)以及减少维度数量。

  4. 数据分析与建模:数据分析与建模包括无监督和监督机器学习、统计推断和预测。有各种各样的机器学习算法可供选择,包括 k 近邻算法、朴素贝叶斯分类器、决策树、支持向量机SVMs)、逻辑回归、k 均值等。要部署的方法取决于问题定义,正如第一步所讨论的,以及收集到的数据类型。这一步的最终产品是从数据中推断出的模型。

  5. 评估:最后一步是模型评估。使用机器学习构建的模型面临的主要问题是它们如何很好地模拟底层数据;例如,如果一个模型过于具体或者它过度拟合了用于训练的数据,那么它在新的数据上可能表现不佳。模型可能过于通用,这意味着它对训练数据欠拟合。例如,当被问及加利福尼亚的天气时,它总是回答晴天,这确实在大多数时候是正确的。然而,这样的模型实际上并不适用于做出有效的预测。这一步的目标是正确评估模型,并确保它在新的数据上也能正常工作。评估方法包括单独的测试和训练集,交叉验证和留一法交叉验证。

我们将在接下来的章节中更详细地研究每个步骤。我们将尝试理解在应用机器学习工作流程中我们必须回答的问题类型,并查看数据分析和评估的相关概念。

数据和问题定义

当面对一个问题定义时,我们需要提出有助于理解数据目标和目标信息的问题。我们可以问一些非常常见的问题,例如:一旦数据被探索,预期的发现是什么? 在数据探索之后可以提取哪些信息? 或者,需要什么格式的信息才能回答这个问题? 提出正确的问题将有助于更清楚地了解如何进一步进行。数据仅仅是数字、文字、观察、事物描述、图像等形式的一系列测量。

测量尺度

表示数据最常见的方式是使用一组属性-值对。考虑以下示例:

Bob = { 
height: 185cm, 
eye color: blue, 
hobbies: climbing, sky diving 
} 

例如,Bob具有名为heighteye colorhobbies的属性,其值分别为185cmblueclimbingsky diving

一组数据可以简单地以表格的形式呈现,其中列对应属性或特征,行对应特定的数据示例或实例。在监督机器学习中,我们想要从其他属性的值 X 中预测结果 Y 的属性称为类别或目标变量,如下表所示:

Name Height [cm] Eye color Hobbies
Bob 185.0 Blue Climbing, sky diving
Anna 163.0 Brown Reading
... ... ... ...

我们首先注意到属性值的变化有多大。例如,身高是一个数字,眼睛颜色是文本,爱好是一个列表。为了更好地理解值类型,让我们更仔细地看看不同的数据类型或测量尺度。斯坦利·史密斯·史蒂文斯(1946 年)定义了以下四种测量尺度,它们具有越来越丰富的表达属性:

  • 名义数据由互斥但无序的数据组成。例如,眼睛颜色、婚姻状况、拥有的车型等。

  • 有序数据对应于顺序重要但值之间的差异不重要的类别,例如疼痛程度、学生成绩、服务质量评分、IMDb 电影评分等。

  • 区间数据由两个值之间的差异有意义的但没有任何零的概念的数据组成,例如标准化考试分数、华氏温度等。

  • 比率数据具有区间变量的所有属性,并且有一个明确的零的定义;当变量等于零时,这个变量将是缺失的。例如,身高、年龄、股价和每周食品支出等变量都是比率变量。

我们为什么应该关心测量尺度呢?嗯,机器学习在很大程度上依赖于数据的统计属性;因此,我们应该了解每种数据类型所具有的限制。一些机器学习算法只能应用于测量尺度的子集。

以下表格总结了每种测量类型的主要操作和统计属性:

属性 名义 有序 区间 比率
1 分布频率
2 模式和众数
3 值的顺序是已知的
4 可以量化每个值之间的差异
5 可以加减值
6 可以乘除值
7 有真正的零

此外,名义和有序数据对应于离散值,而区间和比率数据也可以对应于连续值。在监督学习中,我们想要预测的属性值的测量尺度决定了可以使用哪种机器算法。例如,从有限列表中预测离散值称为分类,可以使用决策树实现,而预测连续值称为回归,可以使用模型树实现。

数据收集

一旦以正确的方向提出问题,数据探索的目标就变得清晰。因此,下一步是查看数据来源。收集到的数据可能非常无序,并且格式非常多样,这可能涉及从数据库、互联网、文件系统或其他文档中读取。大多数机器学习工具都需要数据以特定格式呈现,以便生成正确的结果。我们有两种选择:观察现有来源的数据或通过调查、模拟和实验生成数据。让我们更详细地看看这两种方法。

寻找或观察数据

数据可以在许多地方找到或观察到。一个明显的数据来源是互联网。随着社交媒体使用的增加,以及随着移动数据计划变得更便宜或甚至提供无限数据,用户消耗的数据呈指数级增长。

现在,在线流媒体平台已经出现——以下图表显示,用于消费视频数据的小时数也在迅速增长:

要从互联网获取数据,有多种选择,如下所示列表所示:

  • 来自维基百科、IMDb 和 百万歌曲数据集(可在以下链接找到:labrosa.ee.columbia.edu/millionsong/)的批量下载。

  • 通过 API(如 Google、Twitter、Facebook 和 YouTube)访问数据。

  • 爬取公开的、非敏感的和匿名化的数据是可以的。务必检查条款和条件,并完全引用信息。

收集到的数据的主要缺点是积累数据需要时间和空间,并且它只覆盖了已经发生的事情;例如,意图和内部及外部动机没有被收集。最后,这样的数据可能存在噪声、不完整、不一致,甚至可能随时间变化。

另一个选择是从移动设备中的惯性传感器和位置传感器、环境传感器以及监控关键性能指标软件代理收集测量数据。

数据生成

另一种方法是自行生成数据,例如,通过调查。在调查设计中,我们必须注意数据采样;也就是说,谁在回答调查。我们只从可以接触并愿意回答的受访者那里获取数据。此外,受访者可以提供符合他们自我形象和研究者期望的答案。

或者,数据可以通过模拟来收集,其中领域专家在微观层面上指定用户的行为模型。例如,人群模拟需要指定不同类型的用户在人群中会如何行为。一些例子可能是跟随人群、寻找逃生路线等。然后,可以在不同的条件下运行模拟,以观察会发生什么(Tsai 等人,2011)。模拟适合研究宏观现象和涌现行为;然而,它们通常很难从经验上进行验证。

此外,你可以设计实验来彻底覆盖所有可能的后果,其中你保持所有变量不变,一次只操纵一个变量。这是最昂贵的方法,但通常提供最好的质量。

采样陷阱

数据收集可能会涉及许多陷阱。为了说明一个,让我分享一个故事。在学生之间免费发送平信应该有一个全球性的、不成文的规则。如果你在邮票应该贴的地方写学生给学生的信,邮件就可以免费递送给收件人。现在,假设雅各布给艾玛寄了一套明信片,并且鉴于艾玛确实收到了一些明信片,她得出结论认为所有明信片都已被递送,而且这个规则确实是真的。艾玛推理,因为她收到了明信片,所以所有明信片都已被递送。然而,她不知道雅各布寄出的但未被递送的明信片;因此,她无法在她的推理中解释这一点。艾玛所经历的是幸存者偏差;也就是说,她是基于幸存的数据得出结论的。顺便说一句,带有学生给学生邮票的明信片上会有一个圆圈黑字母 T 的邮票,这意味着邮资已付,收件人应支付邮资,包括一小笔罚款。然而,邮件服务通常在应用此类费用时成本较高,因此通常不会这样做。(Magalhães,2010)

另一个例子是一项研究发现,平均寿命最低的职业是学生。成为学生并不会导致你过早死亡;相反,成为学生意味着你年轻。这就是为什么平均寿命如此之低的原因。(Gelman 和 Nolan,2002)

此外,一项研究发现,只有 1.5%的交通事故司机报告说他们在使用手机,而 10.9%的人报告说车内有其他乘客分散了他们的注意力。我们能得出使用手机比与车内其他乘客交谈更安全的结论吗?(Uts,2003)要回答这个问题,我们需要知道手机使用的普遍性。很可能是,在收集数据期间,开车时与车内其他乘客交谈的人数比在手机上交谈的人数要多。

数据预处理

数据预处理任务的目标是以最佳方式准备数据,以便用于机器学习算法,因为并非所有算法都能处理缺失数据、额外属性或非规范化值的问题。

数据清理

数据清理,也称为数据净化或数据擦洗,是一个包含以下步骤的过程:

  1. 识别不准确、不完整、不相关或损坏的数据,并将其从进一步处理中移除

  2. 解析数据,提取感兴趣的信息,或验证数据字符串是否处于可接受的格式

  3. 将数据转换为通用的编码格式,例如,UTF-8 或 int32,时间尺度或归一化范围

  4. 将数据转换为通用的数据模式;例如,如果我们从不同类型的传感器收集温度测量值,我们可能希望它们具有相同的结构

填充缺失值

机器学习算法通常不擅长处理缺失值。罕见的例外包括决策树、朴素贝叶斯分类器和一些基于规则的学习者。了解值缺失的原因非常重要。它可能由于许多原因而缺失,例如随机错误、系统错误和传感器噪声。一旦我们确定了原因,就有多种方法可以处理缺失值,如下列所示:

  • 移除实例:如果有足够的数据,并且只有少数几个非相关实例有一些缺失值,那么移除这些实例是安全的。

  • 移除属性:当大多数值缺失、值恒定或属性与另一个属性高度相关时,移除属性是有意义的。

  • 分配特殊值N/A):有时值缺失是由于有效的原因,例如值超出范围、离散属性值未定义,或无法获得或测量该值。例如,如果一个人从未评价过一部电影,那么他对这部电影的评分就不存在。

  • 取平均属性值:如果我们有有限数量的实例,我们可能无法承担移除实例或属性。在这种情况下,我们可以通过分配平均属性值来估计缺失值。

  • 从其他属性预测值:如果属性具有时间依赖性,则从之前的条目预测值。

正如我们所看到的,值可能由于许多原因而缺失,因此了解值缺失、不存在或损坏的原因非常重要。

移除异常值

数据中的异常值是与系列中其他任何值都不同的值,并且会以不同程度影响所有学习方法。这些可能是极端值,可以通过置信区间检测并使用阈值移除。最佳方法是可视化数据并检查可视化以检测不规则性。以下是一个示例图。可视化仅适用于低维数据:

数据转换

数据转换技术将数据集转换为机器学习算法期望的输入格式,甚至可能帮助算法更快地学习并实现更好的性能。这也被称为数据整理或数据清洗。例如,标准化假设数据遵循高斯分布,并按以下方式转换值,使得平均值是 0,标准差是 1:

另一方面,归一化会将属性值缩放到一个小的、指定的范围,通常在 0 到 1 之间:

许多机器学习工具箱会自动为您归一化和标准化数据。

最后一种转换技术是离散化,它将连续属性的取值范围划分为区间。为什么我们应该关心这个问题呢?一些算法,如决策树和朴素贝叶斯,更喜欢离散属性。选择区间的最常见方法如下:

  • 等宽:连续变量的区间被划分为k个等宽的区间

  • 等频率:假设有N个实例,每个k个区间大约包含Nk个实例

  • 最小熵:这种方法递归地分割区间,直到熵(衡量无序)的减少超过由区间分割引入的熵增加(Fayyad 和 Irani,1993)。

前两种方法需要我们指定区间的数量,而最后一种方法会自动设置区间的数量;然而,它需要类变量,这意味着它不适用于无监督机器学习任务。

数据降维

数据降维处理的是大量的属性和实例。属性的数量对应于我们的数据集维度数。预测能力低的维度对整体模型贡献很小,并造成很多危害。例如,具有随机值的属性可能会引入一些随机模式,这些模式会被机器学习算法捕捉到。可能数据中包含大量缺失值,其中我们必须找到大量缺失值的原因,在此基础上,可能用一些替代值填充它或进行插补或完全删除该属性。如果 40%或更多值缺失,那么可能建议删除此类属性,因为这会影响模型性能。

另一个因素是方差,其中常数变量可能具有低方差,这意味着数据彼此非常接近,或者数据中的变化不是很大。

为了解决这个问题,第一组技术删除了这些属性并选择了最有希望的属性。这个过程被称为特征选择或属性选择,包括 ReliefF、信息增益和基尼指数等方法。这些方法主要关注离散属性。

另一组工具专注于连续属性,将数据集从原始维度转换到低维空间。例如,如果我们有三个空间中的一组点,我们可以将其投影到二维空间。一些信息会丢失,但在第三维无关紧要的情况下,我们不会丢失太多,因为数据结构和关系几乎完美地保留了下来。这可以通过以下方法实现:

  • 奇异值分解SVD

  • 主成分分析PCA

  • 后向/前向特征消除

  • 因子分析

  • 线性判别分析LDA

  • 神经网络自编码器

数据降维的第二个问题与实例过多有关;例如,它们可能是重复的,或者来自非常频繁的数据流。主要思想是以一种方式选择实例的子集,使得所选数据的分布仍然类似于原始数据分布,更重要的是,与观察到的过程相似。减少实例数量的技术包括随机数据抽样、分层等。一旦数据准备就绪,我们就可以开始数据分析建模。

无监督学习

无监督学习是关于分析数据并在未标记数据中发现隐藏结构的过程。由于没有给出正确的标签概念,因此也没有误差度量来评估学习模型;然而,无监督学习是一个极其强大的工具。你是否曾经想过亚马逊是如何预测你会喜欢哪些书的?或者 Netflix 是如何在你之前就知道你想看什么?答案可以在无监督学习中找到。我们将在下一节中查看一个类似的无监督学习示例。

寻找相似项目

许多问题可以表述为寻找相似元素集,例如,购买相似产品的客户、内容相似的网页、具有相似对象的图像、访问相似网站的用户等等。

如果两个项目之间的距离很小,则认为它们是相似的。主要问题是每个项目是如何表示的,以及项目之间的距离是如何定义的。距离度量主要有两大类:

  • 欧几里得距离

  • 非欧几里得距离

欧几里得距离

在欧几里得空间中,具有 n 维,两个元素之间的距离基于该空间中元素的位置,这被称为p-norm 距离。两种常用的距离度量是 L2-和 L1-norm 距离。

L2-norm,也称为欧几里得距离,是应用最广泛的距离度量,用于衡量二维空间中两个项目之间的距离。其计算方法如下:

图片

L1-norm,也称为曼哈顿距离、城市街区距离和出租车规范,简单地将每个维度上的绝对差异相加,如下所示:

图片

非欧几里得距离

非欧几里得距离是基于元素的性质,而不是它们在空间中的位置。一些著名的距离包括 Jaccard 距离、余弦距离、编辑距离和汉明距离。

Jaccard 距离用于计算两个集合之间的距离。首先,我们计算两个集合的 Jaccard 相似度,即它们的交集大小除以它们的并集大小,如下所示:

图片

Jaccard 距离随后根据以下公式定义:

图片

余弦距离关注的是两个向量之间的方向而不是大小,因此,具有相同方向的两个向量具有余弦相似度为 1,而垂直的两个向量具有余弦相似度为 0。假设我们有两个多维点,可以将一个点视为从原点(0, 0, ..., 0)到其位置的向量。两个向量形成一个角度,其余弦距离是向量的归一化点积,如下所示:

图片

余弦距离在高维特征空间中常用;例如,在文本挖掘中,其中文本文档代表一个实例,特征对应不同的单词,它们的值对应于单词在文档中出现的次数。通过计算余弦相似度,我们可以衡量两个文档在描述相似内容时匹配的可能性。

编辑距离在比较两个字符串时是有意义的。字符串 a=a1,a2,a3,...an 和 b=b1,b2,b3,...bn 之间的距离是转换字符串 a 到 b 所需的最小单字符插入/删除操作的数量,例如,a = abcd 和 b = abbd。要将 a 转换为 b,我们必须删除第二个 b 并在其位置插入 c。没有最小的操作数量可以将 a 转换为 b,因此距离是 d(a, b) = 2。

汉明距离比较两个相同大小的向量,并计算它们在哪些维度上不同。换句话说,它衡量将一个向量转换为另一个向量所需的替换次数。

有许多距离度量专注于各种属性,例如,相关度衡量两个元素之间的线性关系;马氏距离衡量一个点与其他点的分布之间的距离,SimRank,基于图论,衡量元素出现的结构相似性,等等。正如你所能想象的,为你的问题选择和设计合适的相似性度量超过了一半的战斗。在 A. A. Goshtasby 所著的《图像配准:原理、工具和方法》一书中,收集了关于相似性度量的令人印象深刻的概述和评估,该书由 Springer Science and Business Media(2012 年)出版。

维度诅咒

维度诅咒指的是我们拥有大量特征的情况,通常有数百或数千个,这导致了一个具有稀疏数据且距离异常的极大空间。例如,在高维中,几乎所有的点对彼此的距离都相等;事实上,几乎所有的点对都有接近平均距离的距离。维度诅咒的另一种表现是任何两个向量几乎正交,这意味着所有角度都接近 90 度。这实际上使得任何距离测量都变得无用。

解决维度诅咒的方法可能隐藏在数据降维技术中,我们希望减少特征的数量;例如,我们可以运行一个特征选择算法,如 ReliefF,或者一个特征提取或降维算法,如 PCA。

聚类

聚类是一种根据某些距离度量将相似实例分组到聚类的技术。主要思想是将相似的实例(即彼此接近的实例)放入同一个聚类中,同时将不相似的点(即彼此距离较远的点)保留在不同的聚类中。以下图表显示了聚类可能的外观示例:

图片

聚类算法遵循两种根本不同的方法。第一种是层次或聚合方法,它首先将每个点视为自己的聚类,然后迭代地将最相似的聚类合并在一起。当进一步的合并达到预定义的聚类数量或要合并的聚类分布在很大区域时,它停止。

另一种方法基于点分配。首先,估计初始聚类中心(即质心),例如随机地,然后,每个点被分配到最近的聚类,直到所有点都被分配。这个群体中最著名的算法是 k-means 聚类。

k-means 聚类算法要么选择尽可能远的点作为初始聚类中心,要么(层次化地)聚类数据样本并选择距离每个 k 聚类中心最近的点。

监督学习

监督学习是语音识别、电子邮件垃圾邮件过滤、照片中的人脸识别以及信用卡欺诈检测等令人惊叹的事情背后的关键概念。更正式地说,给定一个由特征 X 描述的学习示例集合 D,监督学习的目标是找到一个预测目标变量 Y 的函数。描述特征 X 和类别 Y 之间关系的函数 f 被称为模型:

图片

监督学习算法的一般结构由以下决策定义(Hand 等人,2001 年):

  1. 定义任务

  2. 决定机器学习算法,它引入特定的归纳偏差;即它对目标概念做出的先验假设

  3. 决定得分或成本函数,例如信息增益、均方根误差等

  4. 决定优化/搜索方法以优化得分函数

  5. 找到一个描述 X 和 Y 之间关系的函数

对于我们具有的任务类型和数据集,许多决策已经为我们做出了。在以下章节中,我们将更详细地探讨分类和回归方法以及相应的得分函数。

分类

当我们处理离散类时,可以应用分类,目标是在目标变量的互斥值中预测一个值。一个例子是信用评分,最终的预测是个人是否有信用责任。最流行的算法包括决策树、朴素贝叶斯分类器、SVMs、神经网络和集成方法。

决策树学习

决策树学习构建了一个分类树,其中每个节点对应一个属性;边对应于从节点起源的属性的可能的值(或区间);每个叶子节点对应一个类标签。决策树可以用来直观和明确地表示预测模型,这使得它成为一个非常透明的(白盒)分类器。著名的算法包括 ID3 和 C4.5,尽管存在许多替代实现和改进(例如,Weka 中的 J48)。

概率分类器

给定一组属性值,概率分类器能够预测一个类集的分布,而不是一个确切的类。这可以用作确定性的程度;也就是说,分类器对其预测有多大的信心。最基本的分类器是朴素贝叶斯,它恰好是当且仅当属性条件独立时,最优的分类器。不幸的是,这在实践中极为罕见。

存在一个巨大的子领域,称为概率图模型,包括数百个算法,例如贝叶斯网络、动态贝叶斯网络、隐马尔可夫模型和条件随机字段,它们不仅可以处理属性之间的特定关系,还可以处理时间依赖性。Kiran R Karkera 撰写了一本关于这个主题的优秀入门书籍,《使用 Python 构建概率图模型》,Packt Publishing(2014 年),而 Koller 和 Friedman 出版了一本全面的理论圣经,《概率图模型》,MIT Press(2009 年)。

核方法

任何线性模型都可以通过将核技巧应用于模型来转化为非线性模型——用核函数替换其特征(预测器)。换句话说,核隐式地将我们的数据集转换到更高维。核技巧利用了这样一个事实,即在更高维中分离实例通常更容易。能够操作核的算法包括核感知器、SVMs、高斯过程、PCA、典型相关分析、岭回归、谱聚类、线性自适应滤波器以及许多其他算法。

人工神经网络

人工神经网络受到生物神经网络结构的启发,能够进行机器学习和模式识别。它们通常用于回归和分类问题,包括各种算法和变体,适用于各种问题类型。一些流行的分类方法包括感知器受限玻尔兹曼机RBM)和深度信念网络

集成学习

集成方法由一组不同的较弱模型组成,以获得更好的预测性能。这些模型分别进行训练,然后以某种方式结合它们的预测来做出整体预测。因此,集成包含多种建模数据的方式,这有望带来更好的结果。这是一类非常强大的技术,因此非常受欢迎。这类技术包括提升、装袋、AdaBoost 和随机森林。它们之间的主要区别在于要组合的弱学习者的类型以及组合它们的方式。

评估分类

我们的分类器表现如何?这比另一个好吗?在分类中,我们计算我们正确和错误分类的次数。假设有两种可能的分类标签:是和否,那么就有四种可能的结果,如下表所示:

预测为正吗?
真的积极吗?
FP-假阳性

四个变量:

  • 真阳性命中):这表示一个被正确预测为是的“是”实例

  • 真阴性正确拒绝):这表示没有实例被正确预测为否

  • 假阳性误报):这表示没有实例被预测为是

  • 假阴性漏报):这表示一个被预测为否的“是”实例

分类器的两个基本性能指标是,首先,分类错误:

图片

其次,分类准确率是另一个性能指标,如下所示:

图片

这两个指标的主要问题是它们无法处理不平衡的类别。例如,将信用卡交易分类为滥用或非滥用是一个具有不平衡类别的例子:有 99.99%的正常交易和极小比例的滥用。声称每笔交易都是正常交易的分类器准确率为 99.99%,但我们主要对那些非常罕见的分类感兴趣。

精确度和召回率

解决方案是使用不涉及真阴性的指标。以下有两种这样的指标:

  • 精确度:这是所有预测为正的例子中,正确预测为正的例子(TP)的比例,占所有预测为正的例子(TP + FP)的比例:

图片

  • 召回率:这是所有正例中正确预测为正例(TP)的比例,占所有正例(TP + FN)的比例:

通常会将这两个指标结合起来,并报告 F 度量,它考虑了精确度和召回率来计算得分,得分作为加权平均,其中得分的最佳值为 1,最差值为 0,如下所示:

ROC 曲线

大多数分类算法返回一个表示分类置信度的值,记为 f(X),该值反过来用于计算预测。以信用卡滥用为例,一条规则可能看起来类似于以下内容:

阈值决定了错误率和真正例率。所有可能阈值值的输出可以绘制为接收者操作特征ROC),如下所示:

随机预测器用红色虚线绘制,完美预测器用绿色虚线绘制。为了比较A分类器是否优于C分类器,我们比较曲线下的面积。

大多数工具箱都提供所有这些先前度量。

回归

回归处理连续的目标变量,与分类不同,分类处理离散的目标变量。例如,为了预测未来几天的外部温度,我们会使用回归,而分类将用于预测是否会下雨。一般来说,回归是一个估计特征之间关系的过程,即一个特征的变化如何影响目标变量。

线性回归

最基本的回归模型假设特征和目标变量之间存在线性依赖关系。模型通常使用最小二乘法进行拟合,即最佳模型最小化误差的平方。在许多情况下,线性回归无法建模复杂的关系;例如,以下图表显示了具有相同线性回归线的四组不同点。左上角的模型捕捉了总体趋势,可以被认为是一个合适的模型,而左下角的模型拟合点更好(除了一个异常值,应仔细检查),而上右和下右的线性模型完全错过了数据的潜在结构,不能被认为是合适的模型:

逻辑回归

线性回归适用于因变量是连续的情况。然而,如果因变量本质上是二元的,即 0 或 1,成功或失败,是或否,真或假,存活或死亡等,那么就使用逻辑回归。一个这样的例子是药物的临床试验,研究对象要么对药物有反应,要么没有反应。它也用于欺诈检测,交易要么是欺诈,要么不是欺诈。通常,使用逻辑函数来衡量因变量和自变量之间的关系。它被视为伯努利分布,当绘制时,看起来类似于字符形状的曲线。

评估回归

在回归中,我们从输入 X 预测数字 Y,预测通常是不准确的或不是精确的。我们必须问的主要问题是:偏差有多大?换句话说,我们想要衡量预测值和真实值之间的距离。

均方误差

均方误差MSE)是预测值与真实值之间平方差的平均值,如下所示:

图片

这个度量对异常值非常敏感,例如,99 次精确预测和 1 次预测偏差 10 分与所有预测偏差 1 分的情况得分相同。此外,这个度量对平均值也很敏感。因此,通常使用一个相对均方误差来比较我们的预测器的均方误差与均值预测器的均方误差(它总是预测平均值)。

平均绝对误差

平均绝对误差MAS)是预测值与真实值之间绝对差的平均值,如下所示:

图片

MAS 对异常值不太敏感,但它对平均值和尺度也很敏感。

相关系数

相关系数CC)比较预测的平均值相对于平均值,乘以训练值相对于平均值。如果这个数字是负数,意味着弱相关;正数表示强相关;零表示无相关。真实值 X 和预测值 Y 之间的相关系数定义为如下:

图片

CC 度量对平均值和尺度完全不敏感,对异常值也不太敏感。它能够捕捉相对顺序,这使得它在排名任务中很有用,例如文档相关性和基因表达。

泛化和评估

一旦构建了模型,我们如何知道它在新的数据上会表现如何?这个模型是否好?为了回答这些问题,我们首先将研究模型泛化,然后看看如何对新数据上的模型性能进行估计。

欠拟合和过拟合

预测器训练可能导致模型过于复杂或过于简单。具有低复杂度的模型(下图中最左侧的模型)可以简单到预测最频繁或平均的类别值,而具有高复杂度的模型(最右侧的模型)可以表示训练实例。过于刚性的模型(左侧显示),无法捕捉复杂模式;而过于灵活的模型(右侧显示),会拟合训练数据中的噪声。主要挑战是选择适当的学习算法及其参数,以便学习到的模型在新数据上表现良好(例如,中间列):

下图显示了随着模型复杂度的增加,训练集中的误差如何降低。简单的刚性模型欠拟合数据,误差较大。随着模型复杂度的增加,它更好地描述了训练数据的潜在结构,因此误差降低。如果模型过于复杂,它就会过拟合训练数据,其预测误差再次增加:

根据任务复杂度和数据可用性,我们希望调整我们的分类器以适应更复杂或更简单的结构。大多数学习算法允许这样的调整,如下所示:

  • 回归: 这是指多项式的阶数

  • 朴素贝叶斯: 这是指属性的个数

  • 决策树: 这是指树中的节点数量——剪枝置信度

  • K 近邻法: 这是指邻居的数量——基于距离的邻居权重

  • SVM: 这是指核类型;成本参数

  • 神经网络: 这是指神经元和隐藏层的数量

通过调整,我们希望最小化泛化误差;即分类器在未来的数据上的表现如何。不幸的是,我们永远无法计算真实的泛化误差;然而,我们可以估计它。尽管如此,如果一个模型在训练数据上表现良好,但在测试数据上的表现却很差,那么这个模型很可能是过拟合的。

训练集和测试集

为了估计泛化误差,我们将数据分为两部分:训练数据和测试数据。一个一般规则是按照训练:测试的比例来分割,即 70:30。我们首先在训练数据上训练预测器,然后预测测试数据的值,最后计算误差,即预测值和真实值之间的差异。这为我们提供了对真实泛化误差的估计。

估计基于以下两个假设:首先,我们假设测试集是我们数据集的无偏样本;其次,我们假设实际的新数据将重新组合成与我们的训练和测试示例相同的分布。第一个假设可以通过交叉验证和分层来缓解。此外,如果数据稀缺,就不能为了单独的测试集而放弃大量数据,因为学习算法在没有足够数据的情况下表现不佳。在这种情况下,可以使用交叉验证代替。

交叉验证

交叉验证将数据集分成大约相同大小的k个集合——例如,在以下图中分成五个集合。首先,我们使用集合 2 到 5 进行学习,集合 1 进行训练。然后我们重复此过程五次,每次留出一个集合进行测试,并平均五次重复的错误:

图片

这样,我们既使用了所有数据用于学习和测试,又避免了使用相同的数据来训练和测试模型。

留一法验证

交叉验证的一个极端例子是留一法验证。在这种情况下,折叠的数量等于实例的数量;我们在所有实例上学习,除了一个实例,然后在被省略的实例上测试模型。我们为所有实例重复此操作,以便每个实例正好用于验证一次。当学习示例有限时,例如少于 50 个,这种方法是推荐的。

分层

分层是一种选择实例子集的过程,使得每个折叠大致包含相同比例的类别值。当一个类别是连续的,折叠的选择使得所有折叠中的平均响应值大致相等。分层可以与交叉验证或单独的训练和测试集一起应用。

摘要

在本章中,我们更新了我们对机器学习基础知识的了解。我们回顾了应用机器学习的流程,并明确了主要任务、方法和算法。我们学习了回归的不同类型以及如何评估它们。我们还探讨了交叉验证及其应用。

在下一章中,我们将学习 Java 库,它们可以执行的任务,以及机器学习的不同平台。

第二章:Java 机器学习库和平台

通过自己实现机器学习算法可能是学习机器学习的最佳方式,但如果你站在巨人的肩膀上,利用现有的开源库,你可以进步得更快。

本章回顾了 Java 中用于机器学习的各种库和平台。目标是了解每个库能为桌面带来什么,以及它能解决什么类型的问题。

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

  • Java 在实现机器学习应用中的必要性

  • Weka,一个通用的机器学习平台

  • Java 机器学习库,一组机器学习算法

  • Apache Mahout,一个可扩展的机器学习平台

  • Apache Spark,一个分布式机器学习库

  • Deeplearning4j,一个深度学习库

  • MALLET,一个文本挖掘库

我们还将讨论如何使用这些库以及其他组件来设计适用于单机和大数据应用的完整机器学习应用堆栈。

Java 在实现机器学习应用中的需求

新的机器学习算法通常首先在大学实验室中编写脚本,将几种语言如 shell 脚本、Python、R、MATLAB、Scala 或 C++粘合在一起,以提供一个新的概念并对其属性进行理论分析。一个算法可能需要经过多次重构才能进入一个具有标准化输入或输出和接口的库。虽然 Python、R 和 MATLAB 相当流行,但它们主要用于脚本编写、研究和实验。另一方面,Java 是事实上的企业语言,这可以归因于静态类型、强大的 IDE 支持、良好的可维护性以及不错的线程模型和高性能并发数据结构库。此外,已经有许多 Java 库可用于机器学习,这使得在现有的 Java 应用程序中应用它们并利用强大的机器学习功能变得非常方便。

机器学习库

MLOSS.org网站上列出了超过 70 个基于 Java 的开源机器学习项目,可能还有更多未列出的项目存在于大学服务器、GitHub 或 Bitbucket 上。在本节中,我们将回顾主要的库和平台,它们能解决的问题类型,它们支持的算法,以及它们可以处理的数据类型。

Weka

Waikato 环境知识分析WEKA)是一个在新西兰怀卡托大学开发的机器学习库,可能是最知名的 Java 库。它是一个通用的库,能够解决各种机器学习任务,如分类、回归和聚类。它具有丰富的图形用户界面、命令行界面和 Java API。您可以在www.cs.waikato.ac.nz/ml/weka/上查看 Weka。

在撰写本书时,Weka 总共有 267 个算法:数据预处理(82 个)、属性选择(33 个)、分类和回归(133 个)、聚类(12 个)和关联规则挖掘(7 个)。图形界面非常适合探索您的数据,而 Java API 允许您开发新的机器学习方案并在您的应用程序中使用这些算法。

Weka 是在GNU 通用公共许可证GNU GPL)下分发的,这意味着您可以复制、分发和修改它,只要您跟踪源文件中的更改并保持其在 GNU GPL 之下。您甚至可以将其商业分发,但您必须披露源代码或获得商业许可证。

除了支持多种文件格式外,Weka 还特色其默认的数据格式 ARFF,通过属性-数据对来描述数据。它由两部分组成。第一部分包含一个标题,它指定了所有属性及其类型,例如,名义、数值、日期和字符串。第二部分包含数据,其中每一行对应一个实例。标题中的最后一个属性隐式地被认为是目标变量,缺失数据用问号标记。例如,回到第一章的例子,“应用机器学习快速入门”,用 ARFF 文件格式编写的Bob实例如下:

@RELATION person_dataset @ATTRIBUTE `Name` STRING @ATTRIBUTE `Height` NUMERIC @ATTRIBUTE `Eye color`{blue, brown, green} @ATTRIBUTE `Hobbies` STRING @DATA 'Bob', 185.0, blue, 'climbing, sky diving' 'Anna', 163.0, brown, 'reading' 'Jane', 168.0, ?, ? 

文件由三个部分组成。第一部分以@RELATION <String>关键字开始,指定数据集名称。下一部分以@ATTRIBUTE关键字开始,后跟属性名称和类型。可用的类型有STRINGNUMERICDATE和一系列分类值。最后一个属性隐式地假设为目标变量,我们想要预测的变量。最后一部分以@DATA关键字开始,每行一个实例。实例值由逗号分隔,必须遵循第二部分中属性的相同顺序。

更多 Weka 的示例将在第三章“基本算法——分类、回归和聚类”和第四章“使用集成进行客户关系预测”中演示。

要了解更多关于 Weka 的信息,可以阅读一本快速入门书籍——由Kaluza, Packt Publishing出版的Weka How-to,开始编码,或者查阅Witten and FrankData Mining: Practical Machine Learning Tools and Techniques with Java Implementations,由Morgan Kaufmann Publishers出版,以获取理论背景和深入解释。

Weka 的 Java API 组织成以下顶级包:

  • weka.associations:这是关联规则学习的数据结构和算法,包括Apriori预测 AprioriFilteredAssociatorFP-Growth广义序列模式GSP)、hotSpotTertius

  • weka.classifiers: 这些是监督学习算法、评估器和数据结构。该包进一步分为以下组件:

    • weka.classifiers.bayes: 这实现了贝叶斯方法,包括朴素贝叶斯、贝叶斯网络、贝叶斯逻辑回归等等。

    • weka.classifiers.evaluation: 这些是针对名义和数值预测的监督评估算法,例如评估统计、混淆矩阵、ROC 曲线等等。

    • weka.classifiers.functions: 这些是回归算法,包括线性回归、同质回归、高斯过程、支持向量机(SVMs)、多层感知器、投票感知器等等。

    • weka.classifiers.lazy: 这些是基于实例的算法,例如 k-最近邻、K*和懒惰贝叶斯规则。

    • weka.classifiers.meta: 这些是监督学习元算法,包括 AdaBoost、Bagging、加性回归、随机委员会等等。

    • weka.classifiers.mi: 这些是多个实例学习算法,例如引用 k-最近邻、多样性密度、AdaBoost 等等。

    • weka.classifiers.rules: 这些是基于分离征服方法的决策表和决策规则,包括 RIPPER、PART、PRISM 等等。

    • weka.classifiers.trees: 这些是各种决策树算法,包括 ID3、C4.5、M5、功能树、逻辑树、随机森林等等。

    • weka.clusterers: 这些是聚类算法,包括 k-means、CLOPE、蜘蛛网、DBSCAN 层次聚类和 FarthestFirst。

    • weka.core: 这些是各种实用类,例如属性类、统计类和实例类。

    • weka.datagenerators: 这些是用于分类、回归和聚类算法的数据生成器。

    • weka.estimators: 这些是针对离散/名义域的各种数据分布估计器,包括条件概率估计等等。

    • weka.experiment: 这些是一组支持必要配置、数据集、模型设置和统计信息的类,用于运行实验。

    • weka.filters: 这些是基于属性和实例的选择算法,用于监督和未监督数据的预处理。

    • weka.gui: 这些是实现探索者、实验者和知识流应用的图形界面。Weka 探索者允许你调查数据集、算法以及它们的参数,并使用散点图和其他可视化方法可视化数据集。Weka 实验者用于设计实验批次,但它只能用于分类和回归问题。Weka 知识流实现了一个可视拖放用户界面来构建数据流,例如加载数据、应用过滤器、构建分类器并对其进行评估。

Java 机器学习

Java 机器学习库Java-ML)是一个具有相同类型算法通用接口的机器学习算法集合。它只提供 Java API,因此主要面向软件工程师和程序员。Java-ML 包含数据预处理、特征选择、分类和聚类的算法。此外,它还提供了一些 Weka 桥接,可以直接通过 Java-ML API 访问 Weka 的算法。可以从java-ml.sourceforge.net下载。

Java-ML 也是一个通用的机器学习库。与 Weka 相比,它提供了更一致的接口和实现,包括一些最新的算法,这些算法在其他包中不存在,例如,广泛的最先进的相似度度量集和特征选择技术,例如,动态时间规整DTW)、随机森林属性评估等。Java-ML 也遵循 GNU GPL 许可证。

Java-ML 支持所有类型的文件,只要它们每行包含一个数据样本,并且特征由逗号、分号或制表符等符号分隔。

该库围绕以下顶级包组织:

  • net.sf.javaml.classification: 这些是分类算法,包括朴素贝叶斯、随机森林、装袋、自组织映射、k 近邻等

  • net.sf.javaml.clustering: 这些是聚类算法,如 k-means、自组织映射、空间聚类、蜘蛛网、ABC 等

  • net.sf.javaml.core: 这些是表示实例和数据集的类

  • net.sf.javaml.distance: 这些是测量实例距离和相似性的算法,例如,切比雪夫距离、余弦距离/相似度、欧几里得距离、杰卡德距离/相似度、马氏距离、曼哈顿距离、闵可夫斯基距离、皮尔逊相关系数、斯皮尔曼脚规距离、DTW 等

  • net.sf.javaml.featureselection: 这些是特征评估、评分、选择和排名的算法,例如,增益比率、ReliefF、Kullback-Leibler 散度、对称不确定性等

  • net.sf.javaml.filter: 这些是通过过滤、删除属性、设置类或属性值等操作实例的方法

  • net.sf.javaml.matrix: 这实现了内存或基于文件的数组

  • net.sf.javaml.sampling: 这实现了采样算法以选择数据集的子集

  • net.sf.javaml.tools: 这些是数据集、实例操作、序列化、Weka API 接口等实用方法

  • net.sf.javaml.utils: 这些是算法的实用方法,例如,统计、数学方法、列联表等

Apache Mahout

Apache Mahout 项目旨在构建一个可扩展的机器学习库。它是基于可扩展的、分布式架构构建的,如 Hadoop,使用 MapReduce 范式,这是一种使用服务器集群的并行、分布式算法处理和生成大型数据集的方法。

Mahout 提供控制台界面和 Java API 作为可扩展的聚类、分类和协同过滤算法。它能够解决三个商业问题:

  • 项目推荐:推荐类似“喜欢这部电影的人也喜欢”的项目

  • 聚类:将文本文档排序到主题相关的文档组中

  • 分类:学习将哪个主题分配给未标记的文档

Mahout 在商业友好的 Apache 许可证下分发,这意味着只要您保留 Apache 许可证并将其包含在程序版权声明中,您就可以使用它。

Mahout 具有以下库:

  • org.apache.mahout.cf.taste: 这些是基于用户和项目协同过滤以及 ALS 矩阵分解的协同过滤算法

  • org.apache.mahout.classifier: 这些是内存和分布式实现,包括逻辑回归、朴素贝叶斯、随机森林、隐马尔可夫模型(HMM)和多层感知器

  • org.apache.mahout.clustering: 这些是聚类算法,如 canopy 聚类、k-means、模糊 k-means、流 k-means 和谱聚类

  • org.apache.mahout.common: 这些是算法的实用方法,包括距离、MapReduce 操作、迭代器等

  • org.apache.mahout.driver: 这实现了用于运行其他类主方法的通用驱动程序

  • org.apache.mahout.ep: 这是一种使用记录步长突变的进化优化

  • org.apache.mahout.math: 这些是在 Hadoop 中的各种数学实用方法和实现

  • org.apache.mahout.vectorizer: 这些是用于数据展示、操作和 MapReduce 作业的类

Apache Spark

Apache Spark,或简称 Spark,是一个构建在 Hadoop 之上的大规模数据处理平台,但与 Mahout 不同,它并不依赖于 MapReduce 范式。相反,它使用内存缓存来提取一组工作数据,处理它,并重复查询。据报道,这比直接与存储在磁盘中的数据工作的 Mahout 实现快十倍。可以从spark.apache.org获取。

在 Spark 之上构建了许多模块,例如 GraphX 用于图处理、Spark Streaming 用于处理实时数据流,以及 MLlib,它是一个机器学习库,具有分类、回归、协同过滤、聚类、降维和优化等功能。

Spark 的 MLlib 可以使用基于 Hadoop 的数据源,例如Hadoop 分布式文件系统HDFS)或 HBase,以及本地文件。支持的数据类型包括以下内容:

  • 本地向量存储在单个机器上。密集向量表示为双精度值数组,例如,(2.0,0.0,1.0,0.0),而稀疏向量通过向量的大小、索引数组和值数组表示,例如,[4, (0, 2), (2.0, 1.0)]。

  • 标记点用于监督学习算法,由带有双精度类型类值的局部向量标记组成。标签可以是类索引、二元结果或多个类索引的列表(多类分类)。例如,标记的密集向量表示为[1.0, (2.0, 0.0, 1.0, 0.0)]。

  • 本地矩阵在单个机器上存储一个密集矩阵。它由矩阵维度和一个按列主序排列的单个双精度数组定义。

  • 分布式矩阵在 Spark 的弹性分布式数据集RDD)上操作,它表示可以并行操作的一组元素。有三种表示形式:行矩阵,其中每一行是可以在单个机器上存储的本地向量,行索引没有意义;索引行矩阵,与行矩阵类似,但行索引是有意义的,即可以识别行并执行连接操作;以及坐标矩阵,当行不能存储在单个机器上且矩阵非常稀疏时使用。

Spark 的 MLlib API 库提供了各种学习算法和实用工具的接口,如下列所示:

  • org.apache.spark.mllib.classification:这些是二元和多类分类算法,包括线性 SVM、逻辑回归、决策树和朴素贝叶斯。

  • org.apache.spark.mllib.clustering:这些是 k-means 聚类算法。

  • org.apache.spark.mllib.linalg:这些是数据表示,包括密集向量、稀疏向量和矩阵。

  • org.apache.spark.mllib.optimization:这些是在 MLlib 中用作低级原语的多种优化算法,包括梯度下降、随机梯度下降SGD)、分布式 SGD 的更新方案以及有限内存的Broyden–Fletcher–Goldfarb–ShannoBFGS)算法。

  • org.apache.spark.mllib.recommendation:这些是基于模型的协同过滤技术,通过交替最小二乘矩阵分解实现。

  • org.apache.spark.mllib.regression:这些是回归学习算法,例如线性最小二乘法、决策树、Lasso 和岭回归。

  • org.apache.spark.mllib.stat:这些是在稀疏或密集向量格式中对样本进行统计的函数,用于计算均值、方差、最小值、最大值、计数和非零计数。

  • org.apache.spark.mllib.tree:这实现了分类和回归决策树学习算法。

  • org.apache.spark.mllib.util:这是一系列用于加载数据、保存数据、预处理、生成和验证数据的实用方法。

Deeplearning4j

Deeplearning4j,或 DL4J,是一个用 Java 编写的深度学习库。它具有分布式和单机深度学习框架,包括并支持各种神经网络结构,如前馈神经网络、RBM、卷积神经网络、深度信念网络、自动编码器等。DL4J 可以解决不同的问题,例如识别人脸、声音、垃圾邮件或电子商务欺诈。

Deeplearning4j 也采用 Apache 2.0 许可证,并可以从deeplearning4j.org下载。该库的组织结构如下:

  • org.deeplearning4j.base:这些是加载类。

  • org.deeplearning4j.berkeley:这些是数学实用方法。

  • org.deeplearning4j.clustering:这是 k-means 聚类的实现。

  • org.deeplearning4j.datasets:这是数据集操作,包括导入、创建、迭代等。

  • org.deeplearning4j.distributions:这些是分布的实用方法。

  • org.deeplearning4j.eval:这些是评估类,包括混淆矩阵。

  • org.deeplearning4j.exceptions:这实现了异常处理程序。

  • org.deeplearning4j.models:这些是监督学习算法,包括深度信念网络、堆叠自动编码器、堆叠降噪自动编码器和 RBM。

  • org.deeplearning4j.nn:这些是基于神经网络的组件和算法的实现,如神经网络、多层网络、卷积多层网络等。

  • org.deeplearning4j.optimize:这些是神经网络优化算法,包括反向传播、多层优化、输出层优化等。

  • org.deeplearning4j.plot:这些是渲染数据的各种方法。

  • org.deeplearning4j.rng:这是一个随机数据生成器。

  • org.deeplearning4j.util:这些是辅助和实用方法。

MALLET

机器学习语言工具包(MALLET)是一个包含大量自然语言处理算法和实用工具的大型库。它可以用于各种任务,如文档分类、文档聚类、信息提取和主题建模。它具有命令行界面,以及用于多种算法(如朴素贝叶斯、HMM、潜在狄利克雷主题模型、逻辑回归和条件随机字段)的 Java API。

MALLET 采用通用公共许可证 1.0,这意味着你甚至可以在商业应用中使用它。可以从mallet.cs.umass.edu下载。MALLET 实例由名称、标签、数据和源表示。然而,有两种方法可以将数据导入 MALLET 格式,如下列所示:

  • 每文件一个实例:每个文件或文档对应一个实例,MALLET 接受输入目录名。

  • 每行一个实例:每一行对应一个实例,假设以下格式——instance_name标签标记。数据将是一个特征向量,由作为标记的独特单词及其出现次数组成。

该库包含以下包:

  • cc.mallet.classify: 这些是用于训练和分类实例的算法,包括 AdaBoost、bagging、C4.5 以及其他决策树模型、多元逻辑回归、朴素贝叶斯和 Winnow2。

  • cc.mallet.cluster: 这些是无监督聚类算法,包括贪婪聚合、爬山、k-best 和 k-means 聚类。

  • cc.mallet.extract: 此实现分词器、文档提取器、文档查看器、清理器等。

  • cc.mallet.fst: 此实现序列模型,包括条件随机字段、HMM、最大熵马尔可夫模型以及相应的算法和评估器。

  • cc.mallet.grmm: 此实现图形模型和因子图,例如推理算法、学习、测试等,例如循环信念传播、吉布斯抽样等。

  • cc.mallet.optimize: 这些是用于寻找函数最大值的优化算法,例如梯度上升、有限内存 BFGS、随机元上升等。

  • cc.mallet.pipe: 这些是作为管道处理数据到 MALLET 实例的方法。

  • cc.mallet.topics: 这些是主题建模算法,例如潜在狄利克雷分配、四级弹珠机分配、层次 PAM、DMRT 等。

  • cc.mallet.types: 此实现基本数据类型,如数据集、特征向量、实例和标签。

  • cc.mallet.util: 这些是各种实用函数,如命令行处理、搜索、数学、测试等。

Encog 机器学习框架

Encog 是一个由数据科学家 Jeff Heaton 开发的 Java/C#机器学习框架。它支持数据归一化和处理,以及 SVM、神经网络、贝叶斯网络、隐马尔可夫模型、遗传编程和遗传算法等多种高级算法。自 2008 年以来,它一直在积极开发。它支持多线程,这提高了多核系统上的性能。

可以在www.heatonresearch.com/encog/找到。MLMethod 是基本接口,它包括所有模型的方法。以下是一些它包含的接口和类:

  • MLRegression: 此接口定义了回归算法

  • MLClassification: 此接口定义了分类算法

  • MLClustering: 此接口定义了聚类算法

  • MLData: 此类表示模型中使用的向量,无论是输入还是输出

  • MLDataPair:此类的功能类似于MLData,但可用于输入和输出

  • MLDataSet:代表训练器的MLDataPair实例列表

  • FreeformNeuron:此类用作神经元

  • FreeformConnection:这显示了神经元之间的加权连接

  • FreeformContextNeuron:这代表一个上下文神经元

  • InputSummation:此值指定了如何将输入求和以形成一个单独的神经元

  • BasicActiveSummation:这是所有输入神经元的简单求和

  • BasicFreeConnection:这是神经元之间的基本加权连接

  • BasicFreeformLayer:此接口提供了一个层

ELKI

ELKI 创建了一个环境,用于开发由索引结构支持的 KDD 应用程序,重点在于无监督学习。它提供了聚类分析和异常检测的各种实现。它提供了 R*-tree 等索引结构以提升性能和可扩展性。到目前为止,它已被学生和教师广泛应用于研究领域,并且最近引起了其他方面的关注。

ELKI 使用 AGPLv3 许可证,可在elki-project.github.io/找到。它由以下包组成:

  • de.lmu.ifi.dbs.elki.algorithm:包含各种算法,如聚类、分类、项集挖掘等

  • de.lmu.ifi.dbs.elki.outlier:定义了一个基于异常的算法

  • de.lmu.ifi.dbs.elki.statistics:定义了一个统计分析算法

  • de.lmu.ifi.dbs.elki.database:这是 ELKI 数据库层

  • de.lmu.ifi.dbs.elki.index:这是索引结构实现

  • de.lmu.ifi.dbs.elki.data:定义了各种数据类型和数据库对象类型

MOA

大规模在线分析MOA)包含大量各种机器学习算法,包括分类、回归、聚类、异常检测、概念漂移检测和推荐系统,以及评估工具。所有算法都针对大规模机器学习设计,具有漂移的概念,并处理实时大数据流。它还与 Weka 很好地协同工作。

它可以作为 GNU 许可证使用,并可以从moa.cms.waikato.ac.nz/下载。以下是其主要包:

  • moa.classifiers:包含分类算法

  • moa.clusters:包含聚类算法

  • moa.streams:包含与流处理相关的类

  • moa.evaluation:用于评估

比较库

以下表格总结了所有展示的库。该表格绝不是详尽的——还有许多更多覆盖特定问题领域的库。本综述应作为 Java 机器学习世界大名的概述:

问题领域 许可证 架构 算法
Weka 通用目的 GNU GPL 单机 决策树、朴素贝叶斯、神经网络、随机森林、AdaBoost、层次聚类等
Java-ML 通用目的 GNU GPL 单机 K-means 聚类、自组织映射、马尔可夫链聚类、蜘蛛网、随机森林、决策树、袋装、距离度量等
Mahout 分类、推荐和聚类 Apache 2.0 许可证 分布式单机 逻辑回归、朴素贝叶斯、随机森林、HMM、多层感知器、k-means 聚类等
Spark 通用目的 Apache 2.0 许可证 分布式 支持向量机(SVM)、逻辑回归、决策树、朴素贝叶斯、k-means 聚类、线性最小二乘、Lasso、岭回归等
DL4J 深度学习 Apache 2.0 许可证 分布式单机 RBM、深度信念网络、深度自动编码器、递归神经网络张量、卷积神经网络和堆叠去噪自动编码器
MALLET 文本挖掘 公共许可 1.0 单机 朴素贝叶斯、决策树、最大熵、HMM 和条件随机字段
Encog 机器学习框架 Apache 2.0 许可证 跨平台 支持向量机(SVM)、神经网络、贝叶斯网络、隐马尔可夫模型(HMMs)、遗传编程和遗传算法
ELKI 数据挖掘 AGPL 分布式单机 聚类检测、异常检测、评估、索引
MOA 机器学习 GNU GPL 分布式单机 分类、回归、聚类、异常检测、推荐系统、频繁模式挖掘

构建机器学习应用

机器学习应用,尤其是那些专注于分类的应用,通常遵循以下图中所示的高级工作流程。该工作流程由两个阶段组成——训练分类器和对新实例进行分类。这两个阶段共享一些共同步骤,如下所示:

首先,我们使用一组训练数据,选择一个代表性子集作为训练集,预处理缺失数据,并提取其特征。选择一个监督学习算法来训练一个模型,该模型在第二阶段部署。第二阶段将新的数据实例通过相同的预处理和特征提取程序,并应用学习到的模型来获取实例标签。如果你能够收集新的标记数据,定期重新运行学习阶段以重新训练模型,并在分类阶段用重新训练的模型替换旧的模型。

传统机器学习架构

结构化数据,如交易、客户、分析和市场数据,通常存储在本地关系型数据库中。给定一个查询语言,例如 SQL,我们可以查询用于处理的数据,如前一个图中的工作流程所示。通常,所有数据都可以存储在内存中,并使用 Weka、Java-ML 或 MALLET 等机器学习库进一步处理。

在架构设计中,一个常见的做法是创建数据管道,将工作流程中的不同步骤分开。例如,为了创建客户数据记录,我们可能需要从不同的数据源抓取数据。然后,该记录可以保存在中间数据库中,以供进一步处理。

要了解大数据架构的高级方面如何不同,我们首先需要明确何时数据被认为是大的。

处理大数据

大数据在短语被发明之前就已经存在了。例如,银行和证券交易所多年来每天都在处理数十亿笔交易,航空公司也有全球实时基础设施用于乘客预订的运营管理等等。那么,大数据究竟是什么呢?道格·兰尼(2001 年)提出,大数据由三个 V 定义:体积、速度和多样性。因此,为了回答你的数据是否大的问题,我们可以将其转化为以下三个子问题:

  • 体积:你能将你的数据存储在内存中吗?

  • 速度:你能用一台机器处理新到达的数据吗?

  • 多样性:你的数据来自单一来源吗?

如果你对所有这些问题都回答了是,那么你的数据可能不是大的,你只是简化了你的应用架构。

如果你对所有这些问题的回答都是否定的,那么你的数据是大的!然而,如果你有混合的回答,那么情况就复杂了。有些人可能认为一个 V 很重要;其他人可能说其他 V 更重要。从机器学习的角度来看,处理内存中或分布式存储中的数据的基本算法实现存在根本差异。因此,一个经验法则是:如果你不能将你的数据存储在内存中,那么你应该考虑使用大数据机器学习库。

确切的答案取决于你试图解决的问题。如果你正在启动一个新项目,我建议你从一个单机库开始,并使用你的数据子集(如果整个数据不适合内存)原型化你的算法。一旦你取得了良好的初步结果,可以考虑转向更强大的工具,如 Mahout 或 Spark。

大数据应用架构

大数据,如文档、网络博客、社交网络、传感器数据等,存储在 NoSQL 数据库中,如 MongoDB,或者分布式文件系统中,如 HDFS。如果我们处理结构化数据,我们可以使用 Cassandra 或 HBase 等系统部署数据库功能,这些系统建立在 Hadoop 之上。数据处理遵循 MapReduce 范式,它将数据处理问题分解成更小的子问题,并将任务分配到处理节点。最后,使用如 Mahout 和 Spark 等机器学习库训练机器学习模型。

MongoDB 是一个 NoSQL 数据库,它以类似 JSON 的格式存储文档。您可以在www.mongodb.org了解更多关于它的信息。Hadoop 是一个用于在计算机集群上分布式处理大数据集的框架。它包括自己的文件系统格式 HDFS、作业调度框架 YARD,并实现了并行数据处理 MapReduce 方法。您可以在hadoop.apache.org/了解更多关于 Hadoop 的信息。Cassandra 是一个旨在提供容错、可扩展和去中心化存储的分布式数据库管理系统。更多信息可在cassandra.apache.org/找到。HBase 是另一个专注于分布式存储随机读写访问的数据库。更多信息可在hbase.apache.org/找到。

摘要

选择机器学习库对你的应用架构有重要影响。关键是考虑你的项目需求。你有什么样的数据?你试图解决什么类型的问题?你的数据量大吗?你需要分布式存储吗?你打算使用什么类型的算法?一旦你弄清楚你需要解决什么问题,选择一个最适合你需求的库。

在下一章中,我们将介绍如何使用所提供的库来完成基本的机器学习任务,例如分类、回归和聚类。

第三章:基本算法 - 分类、回归和聚类

在上一章中,我们回顾了用于机器学习的关键 Java 库以及它们带来的好处。在本章中,我们将最终动手实践。我们将更深入地了解基本的机器学习任务,如分类、回归和聚类。每个主题将介绍分类、回归和聚类的算法。示例数据集将小、简单且易于理解。

本章将涵盖以下主题:

  • 加载数据

  • 过滤属性

  • 构建分类、回归和聚类模型

  • 评估模型

在开始之前

在开始之前,从www.cs.waikato.ac.nz/ml/weka/downloading.html下载 Weka 的最新稳定版本(写作时为 Weka 3.8)。

有多个下载选项可用。您希望将 Weka 作为库添加到源代码中,因此请确保跳过自解压可执行文件,并下载以下截图所示的 ZIP 存档。解压存档,并在解压的存档中找到weka.jar

图片

我们将使用 Eclipse IDE 来展示示例;请按照以下步骤操作:

  1. 开始一个新的 Java 项目。

  2. 右键点击项目属性,选择 Java Build Path,点击库标签,然后选择添加外部 JAR 文件。

  3. 导航到解压 Weka 存档,并选择weka.jar文件。

就这样;我们已经准备好实现基本的机器学习技术了!

分类

我们将从最常用的机器学习技术开始:分类。正如我们在第一章中回顾的那样,主要思想是自动在输入变量和结果之间建立映射。在接下来的几节中,我们将探讨如何加载数据、选择特征、在 Weka 中实现基本分类器以及评估其性能。

数据

对于这个任务,我们将查看ZOO数据库。该数据库包含 101 个动物数据条目,每个动物用 18 个属性描述,如下表所示:

animal aquatic fins
hair predator legs
feathers toothed tail
eggs backbone domestic
milk breathes cat size
airborne venomous type

数据集中的示例条目是一只狮子,具有以下属性:

  • animal: 狮子

  • hair: true

  • feathers: false

  • eggs: false

  • milk: true

  • airborne: false

  • aquatic: false

  • predator: true

  • toothed: true

  • backbone: true

  • breathes: true

  • venomous: false

  • fins: false

  • legs: 4

  • tail: true

  • domestic: false

  • catsize: true

  • type: 哺乳动物

我们的任务将是构建一个模型来预测结果变量animal,给定所有其他属性作为输入。

加载数据

在我们开始分析之前,我们将加载数据到 Weka 的属性-关系文件格式ARFF)并打印加载的实例总数。每个数据样本都包含在一个DataSource对象中,而完整的数据集,包括元信息,由Instances对象处理。

为了加载数据,我们将使用接受多种文件格式并将其转换为InstancesDataSource对象:

DataSource source = new DataSource("data/zoo.arff"); 
Instances data = source.getDataSet(); 
System.out.println(data.numInstances() + " instances loaded."); 
System.out.println(data.toString()); 

这将提供以下输出,显示加载的实例数量:

101 instances loaded.

我们也可以通过调用data.toString()方法来打印完整的数据集。

我们的任务是学习一个模型,能够预测未来示例中的animal属性,对于这些示例我们知道其他属性,但不知道animal标签。因此,我们将从训练集中删除animal属性。我们将通过使用Remove()过滤器过滤掉动物属性来完成此操作。

首先,我们设置一个参数字符串表,指定必须删除第一个属性。剩余的属性将用作我们的数据集以训练分类器:

Remove remove = new Remove(); 
String[] opts = new String[]{ "-R", "1"}; 

最后,我们调用Filter.useFilter(Instances, Filter)静态方法来对所选数据集应用过滤器:

remove.setOptions(opts); 
remove.setInputFormat(data); 
data = Filter.useFilter(data, remove); 
System.out.println(data.toString()); 

特征选择

如在第一章《应用机器学习快速入门》中所述,预处理步骤之一是关注特征选择,也称为属性选择。目标是选择一个相关属性的子集,该子集将用于学习模型。为什么特征选择很重要?属性集越小,模型越简单,用户也越容易理解。这通常需要更短的训练时间并减少过拟合。

属性选择可以考虑到类别值,也可以不考虑。在前一种情况下,属性选择算法评估不同的特征子集,并计算一个表示所选属性质量的分数。我们可以使用不同的搜索算法,如穷举搜索和最佳优先搜索,以及不同的质量分数,如信息增益、基尼指数等。

Weka 通过一个AttributeSelection对象支持此过程,该对象需要两个额外的参数:一个计算属性信息量的评估器,以及一个根据评估器分配的分数对属性进行排序的排序器。

我们将使用以下步骤进行选择:

  1. 在本例中,我们将使用信息增益作为评估者,并根据它们的信息增益分数对特征进行排序:
InfoGainAttributeEval eval = new InfoGainAttributeEval(); 
Ranker search = new Ranker(); 
  1. 我们将初始化一个AttributeSelection对象并设置评估器、排序器和数据:
AttributeSelection attSelect = new AttributeSelection(); 
attSelect.setEvaluator(eval); 
attSelect.setSearch(search); 
attSelect.SelectAttributes(data); 
  1. 我们将打印属性indices的顺序列表,如下所示:
int[] indices = attSelect.selectedAttributes(); 
System.out.println(Utils.arrayToString(indices)); 

此过程将提供以下结果作为输出:

12,3,7,2,0,1,8,9,13,4,11,5,15,10,6,14,16 

最具信息量的属性是12(鳍)、3(蛋)、7(水生)、2(毛发)等等。基于这个列表,我们可以移除额外的、非信息量的特征,以便帮助学习算法实现更准确和更快的模型。

什么会决定保留属性的数量?没有关于确切数字的经验法则;属性的数量取决于数据和问题。属性选择的目的在于选择对模型更有益的属性,因此最好关注属性是否在改进模型。

学习算法

我们已经加载了数据并选择了最佳特征,我们现在准备学习一些分类模型。让我们从基本的决策树开始。

在 Weka 中,决策树是通过J48类实现的,它是 Quinlan 著名的 C4.5 决策树学习器(Quinlan,1993)的重新实现。

我们将通过以下步骤构建决策树:

  1. 我们初始化一个新的J48决策树学习器。我们可以通过字符串表传递额外的参数——例如,控制模型复杂性的树剪枝(参考第一章,应用机器学习快速入门)。在我们的情况下,我们将构建一个未剪枝的树;因此,我们将传递单个-U参数,如下所示:
J48 tree = new J48(); 
String[] options = new String[1]; 
options[0] = "-U"; 

tree.setOptions(options); 
  1. 我们将调用buildClassifier(Instances)方法来初始化学习过程:
tree.buildClassifier(data); 
  1. 构建好的模型现在存储在tree对象中。我们可以通过调用toString()方法提供整个J48未剪枝树:
System.out.println(tree); 

输出将如下所示:

    J48 unpruned tree
    ------------------

    feathers = false
    |   milk = false
    |   |   backbone = false
    |   |   |   airborne = false
    |   |   |   |   predator = false
    |   |   |   |   |   legs <= 2: invertebrate (2.0)
    |   |   |   |   |   legs > 2: insect (2.0)
    |   |   |   |   predator = true: invertebrate (8.0)
    |   |   |   airborne = true: insect (6.0)
    |   |   backbone = true
    |   |   |   fins = false
    |   |   |   |   tail = false: amphibian (3.0)
    |   |   |   |   tail = true: reptile (6.0/1.0)
    |   |   |   fins = true: fish (13.0)
    |   milk = true: mammal (41.0)
    feathers = true: bird (20.0)

    Number of Leaves  : 9

    Size of the tree : 17

输出的树总共有17个节点,其中9个是终端节点(叶子)。

另一种表示树的方法是利用内置的TreeVisualizer树查看器,如下所示:

TreeVisualizer tv = new TreeVisualizer(null, tree.graph(), new PlaceNode2()); 
JFrame frame = new javax.swing.JFrame("Tree Visualizer"); 
frame.setSize(800, 500); 
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
frame.getContentPane().add(tv); 
frame.setVisible(true); 
tv.fitToScreen(); 

上述代码将产生以下输出框架:

图片

决策过程从顶部节点开始,也称为根节点。节点标签指定将要检查的属性值。在我们的例子中,首先,我们检查feathers属性的值。如果羽毛存在,我们跟随右侧分支,这会引导我们到标记为bird的叶子,表示有20个示例支持这个结果。如果羽毛不存在,我们跟随左侧分支,这会引导我们到milk属性。我们再次检查属性的值,并跟随与属性值匹配的分支。我们重复这个过程,直到达到叶子节点。

我们可以通过遵循相同的步骤构建其他分类器:初始化分类器,传递控制模型复杂性的参数,并调用buildClassifier(Instances)方法。

在下一节中,你将学习如何使用训练好的模型为标签未知的新示例分配类标签。

对新数据进行分类

假设我们记录了一个我们不知道标签的动物的属性;我们可以从学习到的分类模型中预测其标签。我们将使用以下动物来完成这个过程:

首先,我们构建一个描述新样本的特征向量,如下所示:

double[] vals = new double[data.numAttributes()]; 
vals[0] = 1.0; //hair {false, true} 
vals[1] = 0.0;  //feathers {false, true} 
vals[2] = 0.0;  //eggs {false, true} 
vals[3] = 1.0;  //milk {false, true} 
vals[4] = 0.0;  //airborne {false, true} 
vals[5] = 0.0;  //aquatic {false, true} 
vals[6] = 0.0;  //predator {false, true} 
vals[7] = 1.0;  //toothed {false, true} 
vals[8] = 1.0;  //backbone {false, true} 
vals[9] = 1.0;  //breathes {false, true} 
vals[10] = 1.0;  //venomous {false, true} 
vals[11] = 0.0;  //fins {false, true} 
vals[12] = 4.0;  //legs INTEGER [0,9] 
vals[13] = 1.0;  //tail {false, true} 
vals[14] = 1.0;  //domestic {false, true} 
vals[15] = 0.0;  //catsize {false, true} 
DenseInstance myUnicorn = new DenseInstance(1.0, vals);
myUnicorn.setDataset(data); 

然后,我们在模型上调用classify(Instance)方法,以获取类值。该方法返回标签索引,如下所示:

double result = tree.classifyInstance(myUnicorn); 
System.out.println(data.classAttribute().value((int) result)); 

这将输出mammal类标签。

评估和预测误差指标

我们构建了一个模型,但我们不知道它是否值得信赖。为了估计其性能,我们可以应用在第一章,《应用机器学习快速入门》中解释的交叉验证技术。

Weka 提供了一个Evaluation类来实现交叉验证。我们传递模型、数据、折数和初始随机种子,如下所示:

Classifier cl = new J48(); 
Evaluation eval_roc = new Evaluation(data); 
eval_roc.crossValidateModel(cl, data, 10, new Random(1), new Object[] {}); 
System.out.println(eval_roc.toSummaryString()); 

评估结果存储在Evaluation对象中。

通过调用toString()方法,可以调用最常用的一组指标。请注意,输出不会区分回归和分类,因此请确保注意有意义的指标,如下所示:

    Correctly Classified Instances          93               92.0792 %
    Incorrectly Classified Instances         8                7.9208 %
    Kappa statistic                          0.8955
    Mean absolute error                      0.0225
    Root mean squared error                  0.14  
    Relative absolute error                 10.2478 %
    Root relative squared error             42.4398 %
    Coverage of cases (0.95 level)          96.0396 %
    Mean rel. region size (0.95 level)      15.4173 %
    Total Number of Instances              101  

在分类中,我们关注的是正确/错误分类的实例数量。

混淆矩阵

此外,我们可以通过检查混淆矩阵来检查特定错误分类发生在哪里。混淆矩阵显示了特定类值是如何被预测的:

double[][] confusionMatrix = eval_roc.confusionMatrix(); 
System.out.println(eval_roc.toMatrixString()); 

最终得到的混淆矩阵如下:

    === Confusion Matrix ===

      a  b  c  d  e  f  g   <-- classified as
     41  0  0  0  0  0  0 |  a = mammal
      0 20  0  0  0  0  0 |  b = bird
      0  0  3  1  0  1  0 |  c = reptile
      0  0  0 13  0  0  0 |  d = fish
      0  0  1  0  3  0  0 |  e = amphibian
      0  0  0  0  0  5  3 |  f = insect
      0  0  0  0  0  2  8 |  g = invertebrate

第一行的列名对应于分类节点分配的标签。然后,每一行额外的对应于一个实际的真实类值。例如,第二行对应于具有mammal真实类标签的实例。在列行中,我们读到所有哺乳动物都被正确分类为哺乳动物。在第四行,reptiles,我们注意到有三个被正确分类为reptiles,而一个被分类为fish,另一个被分类为insect。混淆矩阵让我们了解我们的分类模型可能犯的错误类型。

选择分类算法

朴素贝叶斯是机器学习中最为简单、高效和有效的归纳算法之一。当特征相互独立时,这在现实世界中很少见,但从理论上讲是最佳的,即使有依赖特征,其性能也令人惊讶地具有竞争力(张,2004)。主要缺点是它无法学习特征如何相互作用;例如,尽管你喜欢加柠檬或牛奶的茶,但你讨厌同时加这两种东西的茶。

决策树的主要优点是它是一个易于解释和说明的模型,正如我们在示例中所研究的。它可以处理名义和数值特征,你不必担心数据是否线性可分。

以下是一些其他分类算法的示例:

  • weka.classifiers.rules.ZeroR: 这预测多数类,并被认为是一个基线;也就是说,如果你的分类器的性能比平均值预测器差,那么它不值得考虑。

  • weka.classifiers.trees.RandomTree: 这构建了一个在每个节点考虑 K 个随机选择的属性的树。

  • weka.classifiers.trees.RandomForest: 这构建了一组(森林)随机树,并使用多数投票对新实例进行分类。

  • weka.classifiers.lazy.IBk: 这是一个能够根据交叉验证选择适当邻居值的 k-最近邻分类器。

  • weka.classifiers.functions.MultilayerPerceptron: 这是一个基于神经网络的分类器,它使用反向传播对实例进行分类。网络可以手动构建,或由算法创建,或两者兼而有之。

  • weka.classifiers.bayes.NaiveBayes: 这是一个使用估计类(其中数值估计精度值基于训练数据的分析选择)的朴素贝叶斯分类器。

  • weka.classifiers.meta.AdaBoostM1: 这是一个通过使用 AdaBoost M1 方法来提升名义类分类器的类。只能解决名义类问题。这通常可以显著提高性能,但有时会过拟合。

  • weka.classifiers.meta.Bagging: 这是一个用于通过袋装法减少方差分类器的类。这可以执行分类和回归,具体取决于基学习器。

使用 Encog 进行分类

在上一节中,你看到了如何使用 Weka 库进行分类。在本节中,我们将快速查看如何通过使用 Encog 库实现相同的功能。Encog 要求我们构建一个模型来进行分类。从 github.com/encog/encog-java-core/releases 下载 Encog 库。下载后,将 .jar 文件添加到 Eclipse 项目中,如本章开头所述。

对于这个例子,我们将使用 .csv 格式的 iris 数据集;它可以从 archive.ics.uci.edu/ml/datasets/Iris 下载。从下载路径中,将 iris.data.csv 文件复制到你的数据目录。此文件包含 150 种不同花朵的数据。它包含关于花朵的四个不同测量值,最后一列是标签。

我们现在将执行分类,按照以下步骤进行:

  1. 我们将使用 VersatileMLDataSet 方法来加载文件并定义所有四个列。下一步是调用 analyze 方法,它将读取整个文件并找到统计参数,如均值、标准差等:
File irisFile = new File("data/iris.data.csv");
VersatileDataSource source = new CSVDataSource(irisFile, false, CSVFormat.DECIMAL_POINT);

VersatileMLDataSet data = new VersatileMLDataSet(source); 
data.defineSourceColumn("sepal-length", 0, ColumnType.continuous); 
data.defineSourceColumn("sepal-width", 1, ColumnType.continuous); 
data.defineSourceColumn("petal-length", 2, ColumnType.continuous); 
data.defineSourceColumn("petal-width", 3, ColumnType.continuous); 

ColumnDefinition outputColumn = data.defineSourceColumn("species", 4, ColumnType.nominal);
data.analyze(); 
  1. 下一步是定义输出列。然后,是时候对数据进行归一化;但在那之前,我们需要根据以下模型类型来决定数据将如何归一化:
data.defineSingleOutputOthersInput(outputColumn); 

EncogModel model = new EncogModel(data); 
model.selectMethod(data, MLMethodFactory.TYPE_FEEDFORWARD);

model.setReport(new ConsoleStatusReportable()); 
data.normalize(); 
  1. 下一步是在训练集上拟合模型,留出测试集。我们将保留 30%的数据,如第一个参数0.3所指定;下一个参数指定我们想要随机打乱数据。1001表示有一个 1001 的种子值,因此我们使用holdBackValidation模型:
model.holdBackValidation(0.3, true, 1001);
  1. 现在,是时候根据测量值和标签来训练模型并分类数据了。交叉验证将训练数据集分成五个不同的组合:
model.selectTrainingType(data); 
MLRegression bestMethod = (MLRegression)model.crossvalidate(5, true); 
  1. 下一步是显示每个折叠的结果和错误:
System.out.println( "Training error: " + EncogUtility.calculateRegressionError(bestMethod, model.getTrainingDataset())); 
System.out.println( "Validation error: " + EncogUtility.calculateRegressionError(bestMethod, model.getValidationDataset())); 
  1. 现在,我们将开始使用以下代码块来使用模型预测值:
while(csv.next()) { 
            StringBuilder result = new StringBuilder(); 
            line[0] = csv.get(0); 
            line[1] = csv.get(1); 
            line[2] = csv.get(2); 
            line[3] = csv.get(3); 
            String correct = csv.get(4); 
            helper.normalizeInputVector(line,input.getData(),false); 
            MLData output = bestMethod.compute(input); 
            String irisChosen = helper.denormalizeOutputVectorToString(output)[0]; 

            result.append(Arrays.toString(line)); 
            result.append(" -> predicted: "); 
            result.append(irisChosen); 
            result.append("(correct: "); 
            result.append(correct); 
            result.append(")"); 

            System.out.println(result.toString()); 
        } 

这将产生类似于以下输出的结果:

Encog 在MLMethodFactory中支持许多其他选项,如 SVM、PNN 等。

使用大规模在线分析进行分类

大规模在线分析MOA),如第二章 Java Libraries and Platforms for Machine Learning 中所述,是另一个可以用于分类的库。它主要设计用于与流一起工作。如果它与流一起工作,将会有大量的数据;那么,我们如何评估模型呢?在传统的批量学习模式下,我们通常将数据分为训练集和测试集,如果数据有限,则首选交叉验证。在流处理中,数据似乎无限,交叉验证证明是昂贵的。我们可以使用以下两种方法:

  • 保留:当数据已经分为两个预定义的部分时,这很有用。它给出了当前分类器的估计,如果它与当前数据相似。这种相似性在保留集和当前数据之间很难保证。

  • 交错测试-然后训练,或预 quential:在这个方法中,模型在用于训练之前先被测试。因此,模型总是对其从未见过的数据进行测试。在这种情况下,不需要保留方案。它使用可用的数据。随着时间的推移,这种方法将提高分类的准确性。

MOA 提供了多种生成数据流的方法。首先,从moa.cms.waikato.ac.nz/downloads/下载 MOA 库。将下载的.jar文件添加到 Eclipse 中,就像我们在本章开头为 Weka 所做的那样。我们将使用 MOA 提供的 GUI 工具来了解如何使用 MOA 进行流处理。要启动 GUI,请确保moa.jarsizeofag.jar在当前路径中;然后在命令提示符中运行以下命令:

$ java -cp moa.jar -javaagent:sizeofag.jar moa.gui.GUI

它将显示以下输出:

我们可以看到它有分类、回归、聚类、异常值等选项。点击“配置”按钮将显示用于创建分类器的屏幕。它提供了各种学习者和流来工作,如下面的截图所示:

图片

以下是一个使用RandomTreeGeneratorNaiveBayesHoeffdingTree运行的示例:

图片

评估

评估是模型开发后的下一个重要任务。它让您决定模型在给定数据集上的表现是否良好,并确保它能够处理它从未见过的数据。评估框架主要使用以下功能:

  • 错误估计:这使用保留或交错测试和训练方法来估计错误。K 折交叉验证也被使用。

  • 性能指标:使用 Kappa 统计量,它对流分类器更敏感。

  • 统计验证:在比较评估分类器时,我们必须考虑随机和非随机实验之间的差异。McNemar 测试是流中最受欢迎的测试,用于评估两个分类器之间差异的统计显著性。如果我们只使用一个分类器,参数估计的置信区间表明其可靠性。

  • 过程的成本度量:由于我们处理的是流数据,可能需要访问第三方或基于云的解决方案来获取和处理数据,因此考虑了每小时使用成本和内存成本用于评估目的。

基线分类器

批量学习导致了在不同范式下许多分类器的开发,例如分而治之、懒惰学习器、核方法、图形模型等。现在,如果我们转向相同的流,我们需要了解如何使它们对流中的大数据集进行增量化和快速处理。我们必须从模型复杂性与模型更新速度的角度来思考,这是需要关注的主要权衡。

多数类算法是最简单的分类器之一,它被用作基线。它也被用作决策树叶子的默认分类器。另一个是无变化分类器,它预测新实例的标签。朴素贝叶斯算法以其计算能力和简单性成本低而闻名。它是一个增量算法,非常适合流。

决策树

决策树是一种非常流行的分类技术,使得模型易于解释和可视化。它基于树结构。它根据属性值来划分或分割节点,而树的叶子通常落在多数类分类器上。在流数据中,Hoeffding 树是决策树的一个非常快速的算法;它等待新实例的到来,而不是重用实例。它为大量数据构建树。概念自适应快速决策树CVFDT)处理漂移的概念,它通过滑动窗口保持模型与实例的一致性。其他树包括超快速树森林UFFT)、Hoeffding 自适应树、穷举二叉树等等。

惰性学习

在流处理上下文中,k 最近邻KNN)是最方便的批量方法。使用滑动窗口来确定尚未分类的新实例的 KNN。它通常使用最近 1,000 个实例作为滑动窗口。当滑动窗口滑动时,它也处理概念漂移。

主动学习

我们都知道分类器在标记数据上表现良好,但流数据并不总是如此。例如,流数据可能未标记。标记数据成本高昂,因为它需要人工干预来标记未标记的数据。我们理解流数据生成大量数据。主动学习算法只为选择性数据执行标记。要标记的数据是从适合池设置的历 史数据中决定的。需要定期重新训练以决定是否需要为传入的实例标记标签。标记数据的一个简单策略是使用随机策略。这也被称为基线策略,它要求为每个传入的实例请求一个标签,概率为标记预算。另一种策略是为当前分类器最不自信的实例请求一个标签。这可能效果不错,但很快,分类器就会耗尽其预算或达到其阈值。

回归

我们将通过分析一个能源效率数据集(Tsanas 和 Xifara,2012)来探讨基本的回归算法。我们将根据建筑物的构造特征,如表面、墙体和屋顶面积;高度;玻璃面积;和紧凑性,研究建筑物的供暖和冷却负荷需求。研究人员使用模拟器设计了 12 种不同的房屋配置,同时改变 18 个建筑特征。总共模拟了 768 个不同的建筑物。

我们的首要目标是系统地分析每个建筑特征对目标变量(即供暖或冷却负荷)的影响。第二个目标是比较经典线性回归模型与其他方法的性能,例如 SVM 回归、随机森林和神经网络。为此任务,我们将使用 Weka 库。

加载数据

archive.ics.uci.edu/ml/datasets/Energy+efficiency下载能源效率数据集。

数据集以 Excel 的 XLSX 格式存储,无法被 Weka 读取。我们可以通过点击文件 | 另存为,在保存对话框中选择.csv格式将其转换为逗号分隔值CSV),如下所示截图。确认只保存活动工作表(因为其他所有工作表都是空的),并确认继续,以丢失一些格式化功能。现在,文件已准备好由 Weka 加载:

在文本编辑器中打开文件,检查文件是否正确转换。可能会有一些可能导致问题的细微问题。例如,在我的导出中,每一行都以双分号结尾,如下所示:

X1;X2;X3;X4;X5;X6;X7;X8;Y1;Y2;; 
0,98;514,50;294,00;110,25;7,00;2;0,00;0;15,55;21,33;; 
0,98;514,50;294,00;110,25;7,00;3;0,00;0;15,55;21,33;; 

要删除重复的分号,可以使用查找和替换功能:查找;;并将其替换为;

第二个问题是我的文件在文档末尾有一个长长的空行列表,可以删除,如下所示:

0,62;808,50;367,50;220,50;3,50;5;0,40;5;16,64;16,03;; 
;;;;;;;;;;; 
;;;;;;;;;;; 

现在,我们已经准备好加载数据。让我们打开一个新文件,并使用 Weka 的转换器编写一个简单的数据导入函数,用于读取 CSV 格式的文件,如下所示:

import weka.core.Instances; 
import weka.core.converters.CSVLoader; 
import java.io.File; 
import java.io.IOException; 

public class EnergyLoad { 

  public static void main(String[] args) throws IOException { 

    // load CSV 
    CSVLoader loader = new CSVLoader();
    loader.setFieldSeparator(","); 
    loader.setSource(new File("data/ENB2012_data.csv")); 
    Instances data = loader.getDataSet(); 

    System.out.println(data); 
  } 
} 

数据已加载!让我们继续。

分析属性

在分析属性之前,让我们先尝试理解我们正在处理的内容。总共有八个属性描述建筑特征,还有两个目标变量,即供暖和冷却负荷,如下表所示:

属性 属性名称
X1 相对紧凑度
X2 表面积
X3 墙面积
X4 屋顶面积
X5 总高度
X6 方向
X7 玻璃面积
X8 玻璃面积分布
Y1 供暖负荷
Y2 冷却负荷

建立和评估回归模型

我们将通过设置类属性在特征位置来学习供暖负荷的模型:

data.setClassIndex(data.numAttributes() - 2); 

第二个目标变量,冷却负荷,现在可以删除:

//remove last attribute Y2 
Remove remove = new Remove(); 
remove.setOptions(new String[]{"-R", data.numAttributes()+""}); 
remove.setInputFormat(data);
data = Filter.useFilter(data, remove); 

线性回归

我们将从一个基本的线性回归模型开始,使用LinearRegression类实现。类似于分类示例,我们将初始化一个新的模型实例,传递参数和数据,并调用buildClassifier(Instances)方法,如下所示:

import weka.classifiers.functions.LinearRegression; 
... 
data.setClassIndex(data.numAttributes() - 2);
LinearRegression model = new LinearRegression(); 
model.buildClassifier(data); 
System.out.println(model);

存储在对象中的学习模型可以通过调用toString()方法提供,如下所示:

    Y1 =

        -64.774  * X1 +
         -0.0428 * X2 +
          0.0163 * X3 +
         -0.089  * X4 +
          4.1699 * X5 +
         19.9327 * X7 +
          0.2038 * X8 +
         83.9329

线性回归模型构建了一个函数,该函数线性组合输入变量以估计加热负荷。特征前的数字解释了特征对目标变量的影响:符号对应于正/负影响,而幅度对应于其重要性。例如,特征 X1 的相对紧凑性与加热负荷呈负相关,而玻璃面积呈正相关。这两个特征也对最终的加热负荷估计有显著影响。模型的性能可以通过交叉验证技术进行评估。

十折交叉验证如下:

Evaluation eval = new Evaluation(data); 
eval.crossValidateModel(model, data, 10, new Random(1), new String[]{}); 
System.out.println(eval.toSummaryString()); 

我们可以提供常见的评估指标,包括相关系数、平均绝对误差、相对绝对误差等,作为输出,如下所示:

Correlation coefficient                  0.956  
Mean absolute error                      2.0923 
Root mean squared error                  2.9569 
Relative absolute error                 22.8555 % 
Root relative squared error             29.282  % 
Total Number of Instances              768      

使用 Encog 进行线性回归

现在,我们将快速查看如何使用 Encog 创建回归模型。我们将使用之前章节中使用的数据集,即“加载数据”。以下步骤展示了如何创建模型:

  1. 为了加载数据,我们将使用 VersatileMLDataSet 函数,如下所示:
File datafile = new File("data/ENB2012_data.csv");
VersatileDataSource source = new CSVDataSource(datafile, true, CSVFormat.DECIMAL_POINT);
VersatileMLDataSet data = new VersatileMLDataSet(source); 
data.defineSourceColumn("X1", 0, ColumnType.continuous); 
data.defineSourceColumn("X2", 1, ColumnType.continuous); 
data.defineSourceColumn("X3", 2, ColumnType.continuous); 
data.defineSourceColumn("X4", 3, ColumnType.continuous);
data.defineSourceColumn("X5", 4, ColumnType.continuous);
data.defineSourceColumn("X6", 5, ColumnType.continuous);
data.defineSourceColumn("X7", 6, ColumnType.continuous);
data.defineSourceColumn("X8", 7, ColumnType.continuous);
  1. 由于我们有两个输出,Y1Y2,它们可以通过使用 defineMultipleOutputsOthersInput 函数添加,如下所示:
ColumnDefinition outputColumn1 = data.defineSourceColumn("Y1", 8,    ColumnType.continuous);
ColumnDefinition outputColumn2 = data.defineSourceColumn("Y2", 9,  ColumnType.continuous);
ColumnDefinition outputscol [] = {outputColumn1, outputColumn2};
data.analyze();

data.defineMultipleOutputsOthersInput(outputscol);
  1. 下一步是使用 FEEDFORWARD 实例开发一个简单的回归模型:
EncogModel model = new EncogModel(data); 
model.selectMethod(data, MLMethodFactory.TYPE_FEEDFORWARD);
model.setReport(new ConsoleStatusReportable());

data.normalize();
model.holdBackValidation(0.3, true, 1001);
model.selectTrainingType(data);
MLRegression bestMethod = (MLRegression)model.crossvalidate(5, true);            
NormalizationHelper helper = data.getNormHelper(); 

System.out.println(helper.toString()); 
System.out.println("Final model: " + bestMethod); 

现在,我们的回归模型已经准备好了。输出中的最后几行如下截图所示:

使用 MOA 进行回归

使用 MOA 进行回归需要我们使用图形用户界面。您可以从 www.cs.waikato.ac.nz/~bernhard/halifax17/census.arff.gz 下载数据集。

以下步骤展示了如何执行回归:

  1. 使用以下命令启动 MOA 图形用户界面:
$ java -cp moa.jar -javaagent:sizeofag-1.0.4.jar moa.gui.GUI
  1. 选择“回归”选项卡并点击“配置”,如下截图所示:

  1. 我们将使用下载的 .arff 文件进行回归。当我们点击前一步中的“配置”时,将显示“配置任务”窗口,如下截图所示:

  1. 在“流”选项中点击“编辑”并选择 ArffFileStream;选择我们下载的 .arff 文件,如下截图所示:

  1. 在 classIndex 中指定 -1,这将第一个属性设置为目标。在所有弹出窗口中点击“确定”,然后点击“运行”。由于人口普查文件包含大量数据需要处理,这将花费一些时间,如下截图所示:

回归树

另一种方法是构建一组回归模型,每个模型对应数据的一部分。以下图显示了回归模型和回归树之间的主要区别。回归模型构建一个最适合所有数据的单一模型。另一方面,回归树构建一组回归模型,每个模型模拟数据的一部分,如图右侧所示。与回归模型相比,回归树可以更好地拟合数据,但函数是分段线性图,在建模区域之间有跳跃,如下面的图所示:

图片

在 Weka 中,回归树是在M5类中实现的。模型构建遵循相同的范例:初始化模型,传递参数和数据,然后调用buildClassifier(Instances)方法,如下所示:

import weka.classifiers.trees.M5P; 
... 
M5P md5 = new M5P(); 
md5.setOptions(new String[]{""}); 
md5.buildClassifier(data);  
System.out.println(md5); 

诱导模型是一个叶子节点中有方程的树,如下所示:

    M5 pruned model tree:
    (using smoothed linear models)

    X1 <= 0.75 : 
    |   X7 <= 0.175 : 
    |   |   X1 <= 0.65 : LM1 (48/12.841%)
    |   |   X1 >  0.65 : LM2 (96/3.201%)
    |   X7 >  0.175 : 
    |   |   X1 <= 0.65 : LM3 (80/3.652%)
    |   |   X1 >  0.65 : LM4 (160/3.502%)
    X1 >  0.75 : 
    |   X1 <= 0.805 : LM5 (128/13.302%)
    |   X1 >  0.805 : 
    |   |   X7 <= 0.175 : 
    |   |   |   X8 <= 1.5 : LM6 (32/20.992%)
    |   |   |   X8 >  1.5 : 
    |   |   |   |   X1 <= 0.94 : LM7 (48/5.693%)
    |   |   |   |   X1 >  0.94 : LM8 (16/1.119%)
    |   |   X7 >  0.175 : 
    |   |   |   X1 <= 0.84 : 
    |   |   |   |   X7 <= 0.325 : LM9 (20/5.451%)
    |   |   |   |   X7 >  0.325 : LM10 (20/5.632%)
    |   |   |   X1 >  0.84 : 
    |   |   |   |   X7 <= 0.325 : LM11 (60/4.548%)
    |   |   |   |   X7 >  0.325 : 
    |   |   |   |   |   X3 <= 306.25 : LM12 (40/4.504%)
    |   |   |   |   |   X3 >  306.25 : LM13 (20/6.934%)

    LM num: 1
    Y1 = 
      72.2602 * X1 
      + 0.0053 * X3 
      + 11.1924 * X7 
      + 0.429 * X8 
      - 36.2224

    ...

    LM num: 13
    Y1 = 
      5.8829 * X1 
      + 0.0761 * X3 
      + 9.5464 * X7 
      - 0.0805 * X8 
      + 2.1492

    Number of Rules : 13

该树有13个叶子节点,每个对应一个线性方程。前面的输出在以下图中可视化:

图片

该树可以像分类树一样阅读。最重要的特征位于树的顶部。终端节点,即叶子节点,包含一个线性回归模型,解释了到达该树部分的数据。

评估将提供以下结果作为输出:

    Correlation coefficient                  0.9943
    Mean absolute error                      0.7446
    Root mean squared error                  1.0804
    Relative absolute error                  8.1342 %
    Root relative squared error             10.6995 %
    Total Number of Instances              768     

避免常见回归问题的技巧

首先,我们必须使用先前的研究和领域知识来确定哪些特征应包含在回归中。检查文献、报告和以前的研究,了解哪些类型的特征有效,以及一些合理的变量来建模你的问题。假设你有一组带有随机数据的特征;高度可能存在几个特征与目标变量相关(即使数据是随机的)。

我们必须保持模型简单,以避免过拟合。奥卡姆剃刀原则指出,你应该选择一个最能解释你的数据且假设最少的模型。在实践中,模型可以简单到只有两个到四个预测特征。

聚类

与监督分类器相比,聚类的目标是识别未标记数据集中的内在群体。它可以应用于识别同质群体的代表性示例,找到有用和合适的分组,或找到不寻常的示例,例如异常值。

我们将通过分析一个银行数据集来演示如何实现聚类。该数据集包含 11 个属性,描述了 600 个实例,包括年龄、性别、地区、收入、婚姻状况、子女、汽车拥有状态、储蓄活动、当前活动、抵押状态和 PEP。在我们的分析中,我们将通过应用期望最大化(EM)聚类来尝试识别客户的常见群体。

EM 算法的工作原理如下:给定一组聚类,EM 首先为每个实例分配一个属于特定聚类的概率分布。例如,如果我们从三个聚类——即 A、B 和 C——开始,一个实例可能会得到 0.70、0.10 和 0.20 的概率分布,分别属于 A、B 和 C 聚类。在第二步中,EM 重新估计每个类别的概率分布的参数向量。算法重复这两个步骤,直到参数收敛或达到最大迭代次数。

在 EM 中使用的聚类数量可以手动设置或通过交叉验证自动设置。确定数据集中聚类数量的另一种方法包括肘部方法。此方法查看特定数量聚类解释的方差百分比。该方法建议增加聚类数量,直到额外的聚类不会添加太多信息,即它解释的额外方差很少。

聚类算法

构建聚类模型的过程与构建分类模型的过程非常相似,即加载数据并构建模型。聚类算法在weka.clusterers包中实现,如下所示:

import java.io.BufferedReader; 
import java.io.FileReader; 

import weka.core.Instances; 
import weka.clusterers.EM; 

public class Clustering { 

  public static void main(String args[]) throws Exception{ 

    //load data 
    Instances data = new Instances(new BufferedReader
       (new FileReader("data/bank-data.arff"))); 

    // new instance of clusterer 
    EM model = new EM(); 
    // build the clusterer 
    model.buildClusterer(data); 
    System.out.println(model); 

  } 
} 

模型识别了以下六个聚类:

    EM
    ==

    Number of clusters selected by cross validation: 6

                     Cluster
    Attribute              0        1        2        3        4        5
                       (0.1)   (0.13)   (0.26)   (0.25)   (0.12)   (0.14)
    ======================================================================
    age
      0_34            10.0535  51.8472 122.2815  12.6207   3.1023   1.0948
      35_51           38.6282  24.4056  29.6252  89.4447  34.5208   3.3755
      52_max          13.4293    6.693   6.3459  50.8984   37.861  81.7724
      [total]         62.1111  82.9457 158.2526 152.9638  75.4841  86.2428
    sex
      FEMALE          27.1812  32.2338  77.9304  83.5129  40.3199  44.8218
      MALE            33.9299  49.7119  79.3222  68.4509  34.1642   40.421
      [total]         61.1111  81.9457 157.2526 151.9638  74.4841  85.2428
    region
      INNER_CITY      26.1651  46.7431   73.874  60.1973  33.3759  34.6445
      TOWN            24.6991  13.0716  48.4446  53.1731   21.617  17.9946
    ...

表格可以按以下方式读取:第一行表示六个聚类,而第一列显示了属性及其范围。例如,属性age被分为三个范围:0-3435-5152-max。左侧的列表示每个聚类中特定范围中实例的数量;例如,0-34年龄组的客户主要在聚类 2 中(122 个实例)。

评估

可以通过使用logLikelihood度量来估计聚类算法的质量,该度量衡量识别出的聚类的一致性。数据集被分成多个折叠,并且每个折叠都进行聚类。动机是,如果聚类算法将高概率分配给未用于拟合参数的相似数据,那么它可能已经很好地捕捉到了数据结构。Weka 提供了CluterEvaluation类来估计它,如下所示:

double logLikelihood = ClusterEvaluation.crossValidateModel(model, data, 10, new Random(1));
System.out.println(logLikelihood);  

它提供了以下输出:

-8.773410259774291 

使用 Encog 进行聚类

Encog 支持 k-means 聚类。让我们考虑一个非常简单的例子,数据如下所示:

DATA = { { 28, 15, 22 }, { 16, 15, 32 }, { 32, 20, 44 }, { 1, 2, 3 }, { 3, 2, 1 } };

要从这些数据创建BasicMLDataSet,使用一个简单的for循环,它将数据添加到数据集中:

BasicMLDataSet set = new BasicMLDataSet();

for (final double[] element : DATA) {
    set.add(new BasicMLData(element));
}

使用KMeansClustering函数,将数据集聚为两个聚类,如下所示:

KMeansClustering kmeans = new KMeansClustering(2, set);

kmeans.iteration(100);

// Display the cluster
int i = 1;
for (MLCluster cluster : kmeans.getClusters()) {
    System.out.println("*** Cluster " + (i++) + " ***");
    final MLDataSet ds = cluster.createDataSet();
    final MLDataPair pair = BasicMLDataPair.createPair(ds.getInputSize(), ds.getIdealSize());
    for (int j = 0; j < ds.getRecordCount(); j++) {
        ds.getRecord(j, pair);
        System.out.println(Arrays.toString(pair.getInputArray()));
        }
    }

这将生成以下输出:

*** Cluster 1 ***
[16.0, 15.0, 32.0]
[1.0, 2.0, 3.0]
[3.0, 2.0, 1.0]
*** Cluster 2 ***
[28.0, 15.0, 22.0]
*** Cluster 3 ***
[32.0, 20.0, 44.0]

使用 ELKI 进行聚类

ELKI 支持许多聚类算法。以下列出了一些:

  • 亲和传播聚类算法:这是一种使用亲和传播的聚类分析。

  • DBSCAN: 这是一种基于密度的聚类算法,特别适用于有噪声的应用;它根据密度在数据库中找到集合。

  • EM: 该算法基于期望最大化算法创建聚类。

  • AGNES: 层次聚类(HAC),聚集嵌套(AGNES), 是一种经典的层次聚类算法。

  • SLINK: 这是一种单链接算法。

  • CLINK: 这用于完全链接。

  • HDBSCAN: 这是一种提取聚类层次的方法。

此外,KMeansSort、KMeansCompare、KMedianLloyd、KMediodsEM、KMeansBisecting 等也是 KMean 家族的一些例子。

可以在 elki-project.github.io/algorithms/ 找到详细列表的聚类算法,包括 ELKI 支持的所有算法。

我们需要从 elki-project.github.io/releases/ 获取所需的 .jar 文件。下载可执行存档,并从 elki-project.github.io/datasets/ 下载鼠标数据集。

从终端或命令提示符运行以下命令:

$ java -jar elki-bundle-0.7.1.jar 

上述命令生成以下输出:

图片

我们可以看到两个选项,以橙色显示:dbc.inalgorithm。我们需要指定值。在 dbc.in 中,点击点(...)选择我们下载的 mouse.csv 文件。在 algorithm 中,通过点击加号(+)选择 k-Mean Clustering algorithm,找到 kmean.k 并将其值填为 3。点击现在已启用的“运行任务”按钮,它将生成以下输出:

图片

摘要

在本章中,你学习了如何使用 Weka 实现基本机器学习任务:分类、回归和聚类。我们简要讨论了属性选择过程,并训练了模型以评估其性能。

下一章将重点介绍如何将这些技术应用于解决现实生活中的问题,例如客户保留。

第四章:使用集成进行客户关系预测

任何提供服务、产品或体验的公司都需要对其与客户的关系有一个稳固的理解;因此,客户关系管理(CRM)是现代营销策略的关键要素。企业面临的最大挑战之一是了解确切是什么原因导致客户购买新产品。

在本章中,我们将使用法国电信公司 Orange 提供的真实世界营销数据库。任务将是估计以下客户行为的可能性:

  • 转换供应商(流失)

  • 购买新产品或服务(需求)

  • 向他们推荐升级或附加产品以使销售更有利可图(升级销售)

我们将解决 2009 年 KDD 杯挑战,并展示使用 Weka 处理数据的步骤。首先,我们将解析和加载数据并实现基本基线模型。随后,我们将处理高级建模技术,包括数据预处理、属性选择、模型选择和评估。

KDD 杯是全球领先的数据挖掘竞赛。它由 ACM知识发现与数据挖掘特别兴趣小组每年组织。获胜者通常在 8 月份举行的知识发现与数据挖掘会议上宣布。包括所有对应数据集的年度存档可在www.kdd.org/kdd-cup找到。

客户关系数据库

建立关于客户行为知识的最实用方式是产生解释目标变量(如流失、需求或升级销售)的分数。该分数是通过使用描述客户的输入变量(例如,他们的当前订阅、购买的设备、消耗的分钟数等)的模型计算得出的。然后,这些分数被信息系统用于提供相关个性化营销行动等活动。

客户是大多数基于客户的关系数据库中的主要实体;了解客户的行为很重要。客户的行为会在流失、需求或升级销售方面产生一个分数。基本思想是使用计算模型产生一个分数,该模型可能使用不同的参数,例如客户的当前订阅、购买的设备、消耗的分钟数等。一旦分数形成,它就会被信息系统用来决定针对客户行为的下一个策略,该策略特别设计用于客户。

2009 年,KDD 会议组织了一次关于客户关系预测的机器学习挑战。

挑战

给定一组大量的客户属性,挑战中的任务是估计以下目标变量:

  • 客户流失概率:这是客户更换提供商的可能性。客户流失率也被称为流失率或参与者周转率,是用于计算在给定时间段内进入或离开给定集合的个人、对象、术语或项目数量的度量。这个术语在以客户驱动和基于订阅者模型的行业中广泛使用;例如,手机行业和有线电视运营商。

  • 购买意愿概率:这是购买服务或产品的倾向性。

  • 升级销售概率:这是客户购买附加产品或升级的可能性。升级销售意味着在客户已经使用的产品之外销售其他产品。可以将其视为大多数手机运营商提供的有增值服务。销售人员使用销售技巧试图让客户选择增值服务,这将带来更多收入。很多时候,客户并不了解其他选项,销售人员会说服他们使用或考虑这些选项。

挑战的目标是击败 Orange Labs 开发的内部系统。这是参与者证明他们能够处理大型数据库的机会,包括异构、噪声数据和不平衡的类别分布。

数据集

对于这个挑战,Orange 发布了一个包含大约一百万客户的大型客户数据集,这些数据被描述在十个表格中,每个表格包含数百个字段。在第一步中,他们重新采样数据以选择一个更平衡的子集,包含 10 万名客户。在第二步中,他们使用了一个自动特征构建工具,生成了描述客户的 20,000 个特征,然后缩减到 15,000 个特征。在第三步中,通过随机化特征顺序、丢弃属性名称、用随机生成的字符串替换名义变量以及将连续属性乘以随机因子来匿名化数据集。最后,所有实例都被随机分成训练集和测试集。

KDD 杯提供了两组数据,一组是大数据集,另一组是小数据集,分别对应快速和慢速挑战。训练集和测试集都包含 50,000 个示例,数据分割方式相似,但每个集合的样本顺序不同。

在本章中,我们将使用包含 50,000 个实例的小型数据集,每个实例由 230 个变量描述。这 50,000 行数据对应于一个客户,并且它们与三个二进制结果相关联,每个结果对应于三个挑战(升级销售、客户流失和购买意愿)。

为了使这一点更清晰,以下表格展示了数据集:

图片

表格展示了前 25 个实例,即客户,每个客户都描述了 250 个属性。在这个例子中,只展示了 10 个属性的子集。数据集包含许多缺失值,甚至有空值或常量属性。表格的最后三列对应于三个不同的类别标签,涉及地面真相,即客户是否真的更换了服务提供商(客户流失),购买了服务(需求),或购买了升级(升级销售)。然而,请注意,标签是分别从三个不同的文件中提供的,因此保留实例和相应类别标签的顺序对于确保适当的对应关系至关重要。

评估

提交是根据三个任务(客户流失、需求和升级销售)的 ROC 曲线下面积(AUC)的算术平均值进行评估的。ROC 曲线显示了模型性能,是通过绘制用于确定分类结果的阈值的各种敏感度对特异性进行绘图得到的曲线(参见第一章,应用机器学习快速入门ROC 曲线部分)。现在,ROC 曲线下的面积(AUC)与该曲线下的面积相关——面积越大,分类器越好。大多数工具箱,包括 Weka,都提供了一个 API 来计算 AUC 得分。

基本朴素贝叶斯分类器基线

根据挑战赛的规则,参与者必须超越基本的朴素贝叶斯分类器才能有资格获奖,这假设特征是独立的(参见第一章,应用机器学习快速入门)。

KDD Cup 组织者运行了基本的朴素贝叶斯分类器,没有进行任何特征选择或超参数调整。对于大数据集,朴素贝叶斯在测试集上的总体分数如下:

  • 客户流失问题: AUC = 0.6468

  • 需求问题: AUC = 0.6453

  • 升级销售问题: AUC=0.7211

注意,基线结果仅报告了大数据集。此外,虽然训练集和测试集都提供在 KDD Cup 网站上,但测试集的实际真实标签并未提供。因此,当我们用我们的模型处理数据时,我们无法知道模型在测试集上的表现如何。我们将只使用训练数据,并通过交叉验证评估我们的模型。结果将不可直接比较,但无论如何,我们将对 AUC 得分的合理幅度有一个概念。

获取数据

在 KDD Cup 网页(kdd.org/kdd-cup/view/kdd-cup-2009/Data)上,你应该会看到一个类似于以下截图的页面。首先,在“Small 版本(230 变量)”标题下,下载orange_small_train.data.zip。接下来,下载与该训练数据相关的三组真实标签。以下文件位于“真实二进制目标(小型)”标题下:

  • orange_small_train_appentency.labels

  • orange_small_train_churn.labels

  • orange_small_train_upselling.labels

保存并解压截图中的红色框内标记的所有文件:

图片

在接下来的章节中,首先,我们将加载数据到 Weka 中,并使用朴素贝叶斯分类器进行基本建模,以获得我们自己的基线 AUC 分数。稍后,我们将探讨更高级的建模技术和技巧。

加载数据

我们将直接从.csv格式将数据加载到 Weka 中。为此,我们将编写一个函数,该函数接受数据文件路径和真实标签文件路径。该函数将加载和合并两个数据集,并删除空属性。我们将从以下代码块开始:

public static Instances loadData(String pathData, String 
  pathLabeles) throws Exception { 

首先,我们使用CSVLoader()类加载数据。此外,我们指定\t制表符作为字段分隔符,并强制将最后 40 个属性解析为名义属性:

// Load data 
CSVLoader loader = new CSVLoader(); 
loader.setFieldSeparator("\t"); 
loader.setNominalAttributes("191-last"); 
loader.setSource(new File(pathData)); 
Instances data = loader.getDataSet(); 

CSVLoader类接受许多其他参数,指定列分隔符、字符串定界符、是否存在标题行等。完整的文档可在weka.sourceforge.net/doc.dev/weka/core/converters/CSVLoader.html找到。

一些属性不包含单个值,Weka 会自动将它们识别为String属性。实际上我们不需要它们,因此我们可以安全地使用RemoveType过滤器删除它们。此外,我们指定了-T参数,该参数删除特定类型的属性并指定我们想要删除的属性类型:

// remove empty attributes identified as String attribute  
RemoveType removeString = new RemoveType(); 
removeString.setOptions(new String[]{"-T", "string"}); 
removeString.setInputFormat(data); 
Instances filteredData = Filter.useFilter(data, removeString); 

或者,我们也可以使用Instances类中实现的void deleteStringAttributes()方法,它具有相同的效果;例如,data.removeStringAttributes()

现在,我们将加载数据并分配类标签。我们将再次使用CVSLoader,其中我们指定文件没有标题行,即setNoHeaderRowPresent(true)

// Load labeles 
loader = new CSVLoader(); 
loader.setFieldSeparator("\t"); 
loader.setNoHeaderRowPresent(true); 
loader.setNominalAttributes("first-last"); 
loader.setSource(new File(pathLabeles)); 
Instances labels = loader.getDataSet(); 

加载完两个文件后,我们可以通过调用Instances.mergeInstances (Instances, Instances)静态方法将它们合并在一起。该方法返回一个新的数据集,它包含第一个数据集的所有属性,以及第二个集合的属性。请注意,两个数据集中的实例数量必须相同:

// Append label as class value 
Instances labeledData = Instances.mergeInstances(filteredData, 
   labeles); 

最后,我们将最后一个属性,即我们刚刚添加的标签属性,设置为目标变量,并返回结果数据集:

// set the label attribute as class  
labeledData.setClassIndex(labeledData.numAttributes() - 1); 

System.out.println(labeledData.toSummaryString()); 
return labeledData; 
} 

函数提供输出摘要,如下面的代码块所示,并返回标记的数据集:

    Relation Name:  orange_small_train.data-weka.filters.unsupervised.attribute.RemoveType-Tstring_orange_small_train_churn.labels.txt
    Num Instances:  50000
    Num Attributes: 215

    Name          Type  Nom  Int Real     Missing      Unique  Dist
    1 Var1        Num   0%   1%   0% 49298 / 99%     8 /  0%    18 
    2 Var2        Num   0%   2%   0% 48759 / 98%     1 /  0%     2 
    3 Var3        Num   0%   2%   0% 48760 / 98%   104 /  0%   146 
    4 Var4        Num   0%   3%   0% 48421 / 97%     1 /  0%     4
    ...

基本建模

在本节中,我们将按照 KDD Cup 组织者采取的方法实现我们自己的基线模型。然而,在我们到达模型之前,让我们首先实现评估引擎,该引擎将返回所有三个问题的 AUC。

评估模型

现在,让我们更仔细地看看评估函数。评估函数接受一个初始化的模型,在所有三个问题上对模型进行交叉验证,并报告结果为 ROC 曲线下的面积(AUC),如下所示:

public static double[] evaluate(Classifier model) 
   throws Exception { 

  double results[] = new double[4]; 

  String[] labelFiles = new String[]{ 
    "churn", "appetency", "upselling"}; 

  double overallScore = 0.0; 
  for (int i = 0; i < labelFiles.length; i++) { 

首先,我们调用我们之前实现的Instance loadData(String, String)函数来加载数据并与其选定的标签合并:

    // Load data 
    Instances train_data = loadData( 
     path + "orange_small_train.data", 
      path+"orange_small_train_"+labelFiles[i]+".labels.txt"); 

接下来,我们初始化weka.classifiers.Evaluation类并传递我们的数据集。(数据集仅用于提取数据属性;实际数据不考虑。)我们调用void crossValidateModel(Classifier, Instances, int, Random)方法开始交叉验证,并创建五个折。由于验证是在数据的随机子集中进行的,我们需要传递一个随机种子:

    // cross-validate the data 
    Evaluation eval = new Evaluation(train_data); 
    eval.crossValidateModel(model, train_data, 5,  
    new Random(1)); 

评估完成后,我们通过调用double areUnderROC(int)方法读取结果。由于该指标依赖于我们感兴趣的靶值,该方法期望一个类别值索引,可以通过在类别属性中搜索"1"值的索引来提取,如下所示:

    // Save results 
    results[i] = eval.areaUnderROC( 
      train_data.classAttribute().indexOfValue("1")); 
    overallScore += results[i]; 
  } 

最后,结果被平均并返回:

  // Get average results over all three problems 
  results[3] = overallScore / 3; 
  return results; 
}

实现朴素贝叶斯基线

现在,当我们有了所有这些原料时,我们可以复制我们期望超越的朴素贝叶斯方法。这种方法将不包括任何额外的数据预处理、属性选择或模型选择。由于我们没有测试数据的真实标签,我们将应用五折交叉验证来评估模型在小数据集上的性能。

首先,我们初始化一个朴素贝叶斯分类器,如下所示:

Classifier baselineNB = new NaiveBayes(); 

接下来,我们将分类器传递给我们的评估函数,该函数加载数据并应用交叉验证。该函数返回所有三个问题的 ROC 曲线下的面积分数和总体结果:

double resNB[] = evaluate(baselineNB); 
System.out.println("Naive Bayes\n" +  
"\tchurn:     " + resNB[0] + "\n" +  
"\tappetency: " + resNB[1] + "\n" +  
"\tup-sell:   " + resNB[2] + "\n" +  
"\toverall:   " + resNB[3] + "\n"); 

在我们的情况下,模型返回以下结果:

    Naive Bayes
      churn:     0.5897891153549814
      appetency: 0.630778394752436
      up-sell:   0.6686116692438094
      overall:   0.6297263931170756

这些结果将作为我们处理更高级建模挑战时的基线。如果我们使用显著更复杂、耗时和复杂的技术处理数据,我们期望结果会更好。否则,我们只是在浪费资源。一般来说,在解决机器学习问题时,创建一个简单的基线分类器作为我们的定位点是件好事。

使用集成进行高级建模

在上一节中,我们实现了一个定位基线;现在,让我们专注于重型机械。我们将遵循 KDD Cup 2009 获奖解决方案的方法,该方案由 IBM 研究团队(Niculescu-Mizil 等人)开发。

为了应对这一挑战,他们使用了集成选择算法(Caruana 和 Niculescu-Mizil,2004)。这是一种集成方法,意味着它构建了一系列模型,并以特定的方式组合它们的输出,以提供最终的分类。它具有几个理想的特性,使其非常适合这一挑战,如下所示:

  • 这已被证明是稳健的,性能卓越。

  • 它可以针对特定的性能指标进行优化,包括 AUC。

  • 它允许向库中添加不同的分类器。

  • 它是一种任何时间方法,这意味着如果我们用完时间,我们有一个可用的解决方案。

在本节中,我们将大致遵循他们在报告中描述的步骤。请注意,这并不是他们方法的精确实现,而是一个包括深入探索所需步骤的解决方案概述。

步骤的一般概述如下:

  1. 首先,我们将通过移除明显不会带来任何价值的属性来预处理数据——例如,所有缺失或常量值;修复缺失值,以便帮助机器学习算法,因为它们无法处理这些值;以及将分类属性转换为数值属性。

  2. 接下来,我们将运行属性选择算法,仅选择有助于预测任务的属性子集。

  3. 在第三步,我们将使用各种模型实例化集成选择算法,最后我们将评估性能。

在开始之前

对于这个任务,我们需要一个额外的 Weka 包,ensembleLibrary。Weka 3.7.2 及以上版本支持外部包,主要由学术社区开发。Weka 包的列表可在weka.sourceforge.net/packageMetaData找到,如下截图所示:

图片

prdownloads.sourceforge.net/weka/ensembleLibrary1.0.5.zip?download找到并下载ensembleLibrary包的最新可用版本。

在解压包后,找到ensembleLibrary.jar并将其导入到您的代码中,如下所示:

import weka.classifiers.meta.EnsembleSelection; 

数据预处理

首先,我们将利用 Weka 内置的weka.filters.unsupervised.attribute.RemoveUseless过滤器,它的工作方式正如其名称所暗示的那样。它移除变化不大的属性,例如,所有常量属性都被移除。最大方差(仅适用于名义属性)由-M参数指定。默认参数是 99%,这意味着如果所有实例中有超过 99%具有唯一的属性值,则该属性将被移除,如下所示:

RemoveUseless removeUseless = new RemoveUseless(); 
removeUseless.setOptions(new String[] { "-M", "99" });// threshold 
removeUseless.setInputFormat(data); 
data = Filter.useFilter(data, removeUseless); 

接下来,我们将使用weka.filters.unsupervised.attribute.ReplaceMissingValues过滤器,将数据集中的所有缺失值替换为训练数据中的众数(名义属性)和均值(数值属性)。一般来说,在替换缺失值时应谨慎行事,同时考虑属性的意义和上下文:

ReplaceMissingValues fixMissing = new ReplaceMissingValues(); 
fixMissing.setInputFormat(data); 
data = Filter.useFilter(data, fixMissing); 

最后,我们将使用weka.filters.unsupervised.attribute.Discretize过滤器对数值属性进行离散化,即通过该过滤器将数值属性转换为区间。使用-B选项,我们将数值属性分割成四个区间,而-R选项指定了属性的取值范围(只有数值属性将被离散化):

Discretize discretizeNumeric = new Discretize(); 
discretizeNumeric.setOptions(new String[] { 
    "-B",  "4",  // no of bins 
    "-R",  "first-last"}); //range of attributes 
fixMissing.setInputFormat(data); 
data = Filter.useFilter(data, fixMissing); 

属性选择

在下一步中,我们将仅选择具有信息量的属性,即更有可能帮助预测的属性。解决此问题的标准方法是检查每个属性携带的信息增益。我们将使用weka.attributeSelection.AttributeSelection过滤器,该过滤器需要两个额外的方法:一个评估器(如何计算属性的有用性)和搜索算法(如何选择属性子集)。

在我们的案例中,首先,我们初始化weka.attributeSelection.InfoGainAttributeEval,该类实现了信息增益的计算:

InfoGainAttributeEval eval = new InfoGainAttributeEval(); 
Ranker search = new Ranker(); 

为了只选择高于阈值的顶级属性,我们初始化weka.attributeSelection.Ranker,以便根据信息增益对属性进行排序,信息增益高于特定阈值。我们使用-T参数指定此阈值,同时保持阈值较低,以保留至少包含一些信息的属性:

search.setOptions(new String[] { "-T", "0.001" }); 

设置此阈值的通用规则是按信息增益对属性进行排序,并选择信息增益降至可忽略值时的阈值。

接下来,我们可以初始化AttributeSelection类,设置评估器和排序器,并将属性选择应用于我们的数据集,如下所示:

AttributeSelection attSelect = new AttributeSelection(); 
attSelect.setEvaluator(eval); 
attSelect.setSearch(search); 

// apply attribute selection 
attSelect.SelectAttributes(data); 

最后,通过调用reduceDimensionality(Instances)方法,我们移除了上一次运行中未选择的属性:

// remove the attributes not selected in the last run 
data = attSelect.reduceDimensionality(data); 

最终,我们保留了 230 个属性中的 214 个。

模型选择

多年来,机器学习领域的从业者已经开发了许多学习算法和现有算法的改进。有如此多的独特监督学习方法,以至于难以跟踪所有这些方法。由于数据集的特征各异,没有一种方法在所有情况下都是最好的,但不同的算法能够利用给定数据集的不同特征和关系。

首先,我们需要通过初始化weka.classifiers.EnsembleLibrary类来创建模型库,这将帮助我们定义模型:

EnsembleLibrary ensembleLib = new EnsembleLibrary(); 

接下来,我们将模型及其参数作为字符串值添加到库中;例如,我们可以添加三个具有不同参数的决策树学习器,如下所示:

ensembleLib.addModel("weka.classifiers.trees.J48 -S -C 0.25 -B -M 
   2"); 
ensembleLib.addModel("weka.classifiers.trees.J48 -S -C 0.25 -B -M 
   2 -A"); 

如果你熟悉 Weka 图形界面,你还可以在那里探索算法及其配置,并复制配置,如下面的截图所示。右键单击算法名称,导航到编辑配置 | 复制配置字符串:

为了完成这个示例,我们添加了以下算法及其参数:

  • 作为默认基线使用的朴素贝叶斯:
ensembleLib.addModel("weka.classifiers.bayes.NaiveBayes"); 
  • 基于懒惰模型的 k 近邻算法:
ensembleLib.addModel("weka.classifiers.lazy.IBk"); 
  • 作为简单逻辑回归默认参数的逻辑回归:
ensembleLib.addModel("weka.classifiers.functions.SimpleLogi
   stic"); 
  • 默认参数的支持向量机:
ensembleLib.addModel("weka.classifiers.functions.SMO"); 
  • 本身就是集成方法的AdaBoost
ensembleLib.addModel("weka.classifiers.meta.AdaBoostM1"); 
  • 基于逻辑回归的集成方法LogitBoost
ensembleLib.addModel("weka.classifiers.meta.LogitBoost"); 
  • 基于单层决策树的集成方法DecisionStump
ensembleLib.addModel("classifiers.trees.DecisionStump"); 

由于EnsembleLibrary实现主要针对 GUI 和控制台用户,我们必须通过调用saveLibrary(File, EnsembleLibrary, JComponent)方法将模型保存到文件中,如下所示:

EnsembleLibrary.saveLibrary(new 
   File(path+"ensembleLib.model.xml"), ensembleLib, null); 
System.out.println(ensembleLib.getModels()); 

接下来,我们可以通过实例化weka.classifiers.meta.EnsembleSelection类来初始化集成选择算法。首先,让我们回顾以下方法选项:

  • -L </path/to/modelLibrary>: 这指定了modelLibrary文件,继续列出所有模型。

  • -W </path/to/working/directory>: 这指定了工作目录,所有模型都将保存在这里。

  • -B <numModelBags>: 这设置了袋的数量,即运行集成选择算法的迭代次数。

  • -E <modelRatio>: 这设置了随机选择填充每个模型包的库模型的比例。

  • -V <validationRatio>: 这设置了保留用于验证的训练数据集的比例。

  • -H <hillClimbIterations>: 这设置了在每个模型包上要执行的爬山迭代次数。

  • -I <sortInitialization>: 这设置了排序初始化算法在初始化每个模型包时可以选择的集成库的比例。

  • -X <numFolds>: 这设置了交叉验证的折数。

  • -P <hillclimbMetric>: 这指定了在爬山算法中用于模型选择的度量标准。有效的度量包括准确率、rmse、roc、精确率、召回率、fscore 以及所有这些。

  • -A <algorithm>: 这指定了用于集成选择的算法。有效的算法包括 forward(默认)用于前向选择,backward 用于后向消除,both 用于前向和后向消除,best 用于简单地从集成库中打印出表现最好的模型,以及 library 仅训练集成库中的模型。

  • -R: 这标志是否可以将模型多次选入一个集成。

  • -G: 这表示排序初始化是否在性能下降时贪婪地停止添加模型。

  • -O: 这是一个用于详细输出的标志。这会打印出所有选定模型的性能。

  • -S <num>: 这是一个随机数种子(默认为1)。

  • -D: 如果设置,分类器将以调试模式运行,并且可能向控制台提供额外的输出信息。

我们使用以下初始参数初始化算法,其中我们指定优化 ROC 指标:

EnsembleSelection ensambleSel = new EnsembleSelection(); 
ensambleSel.setOptions(new String[]{ 
  "-L", path+"ensembleLib.model.xml", // </path/to/modelLibrary>
     "-W", path+"esTmp", // </path/to/working/directory> -  
"-B", "10", // <numModelBags>  
  "-E", "1.0", // <modelRatio>. 
  "-V", "0.25", // <validationRatio> 
  "-H", "100", // <hillClimbIterations>  
"-I", "1.0", // <sortInitialization>  
  "-X", "2", // <numFolds> 
  "-P", "roc", // <hillclimbMettric> 
  "-A", "forward", // <algorithm>  
  "-R", "true", // - Flag to be selected more than once 
  "-G", "true", // - stops adding models when performance degrades 
  "-O", "true", // - verbose output. 
  "-S", "1", // <num> - Random number seed. 
  "-D", "true" // - run in debug mode  
}); 

性能评估

评估在计算和内存方面都很重,所以请确保你初始化 JVM 时带有额外的堆空间(例如,java -Xmx16g)。计算可能需要几个小时或几天,具体取决于你包含在模型库中的算法数量。这个例子在一个 12 核心的 Intel Xeon E5-2420 CPU 上,32 GB 的内存,平均使用了 10%的 CPU 和 6 GB 的内存,耗时 4 小时 22 分钟。

我们称我们的评估方法并提供结果作为输出,如下所示:

double resES[] = evaluate(ensambleSel); 
System.out.println("Ensemble Selection\n"  
+ "\tchurn:     " + resES[0] + "\n" 
+ "\tappetency: " + resES[1] + "\n"  
+ "\tup-sell:   " + resES[2] + "\n"  
+ "\toverall:   " + resES[3] + "\n"); 

模型库中的特定分类器集达到了以下结果:

    Ensamble
      churn:     0.7109874158176481
      appetency: 0.786325687118347
      up-sell:   0.8521363243575182
      overall:   0.7831498090978378

总体而言,这种方法使我们相比本章开头设计的初始基线有了超过 15 个百分点的显著改进。虽然很难给出一个明确的答案,但改进主要归因于三个因素:数据预处理和属性选择、探索大量不同的学习方法,以及使用一种能够利用多种基分类器而不过度拟合的集成构建技术。然而,这种改进需要显著增加处理时间以及工作内存。

集成方法 – MOA

如同其词意,集成就是一起查看,或者同时进行。它用于结合多个学习算法,以获得更好的结果和性能。你可以使用各种技术进行集成。一些常用的集成技术或分类器包括袋装、提升、堆叠、模型桶等。

大规模在线分析MOA)支持集成分类器,如准确度加权的集成、准确度更新的集成等。在本节中,我们将向您展示如何使用利用袋装算法:

  1. 打开终端并执行以下命令:
java -cp moa.jar -javaagent:sizeofag-1.0.4.jar moa.gui.GUI
  1. 选择分类标签并点击配置按钮:

图片

这将打开配置任务选项。

  1. 在学习器选项中,选择 bayes.NaiveBayes,然后,在流选项中,点击编辑,如图所示:

图片

  1. 选择 ConceptDriftStream,并在流和漂移流中,选择 AgrawalGenerator;它将使用 Agrawal 数据集作为流生成器:

图片

  1. 关闭所有窗口并点击运行按钮:

图片

这将运行任务并生成以下输出:

图片

  1. 让我们使用 LeveragingBag 选项。为此,打开配置任务窗口,并在 baseLearner 中选择编辑选项,这将显示以下内容;从第一个下拉框中选择 LeveragingBag。你可以在第一个下拉框中找到其他选项,例如提升和平均权重集成:

图片

将流设置为 AgrawalGenerator,如下截图所示:

图片

  1. 关闭配置任务窗口并点击运行按钮;这需要一些时间来完成:

图片

输出显示了每 10,000 个实例后的评估,包括分类正确性所需的 RAM 时间以及 Kappa 统计。正如你所见,随着时间的推移,分类正确性随着实例数量的增加而提高。前一个截图中的图表显示了正确性和实例数量。

摘要

在本章中,我们解决了 2009 年 KDD 杯关于客户关系预测的挑战,实现了数据预处理步骤,并处理了缺失值和冗余属性。我们遵循了获胜的 KDD 杯解决方案,并研究了如何通过使用一系列学习算法来利用集成方法,这可以显著提高分类性能。

在下一章中,我们将解决另一个关于客户行为的问题:购买行为。你将学习如何使用检测频繁发生模式的算法。

第五章:亲和度分析

亲和度分析是市场篮分析MBA)的核心。它可以发现特定用户或群体执行的活动之间的共现关系。在零售领域,亲和度分析可以帮助你了解顾客的购买行为。这些见解可以通过智能交叉销售和升级销售策略来推动收入,并帮助你开发忠诚度计划、销售促销和折扣计划。

在本章中,我们将探讨以下主题:

  • MBA

  • 关联规则学习

  • 在各个领域的其他应用

首先,我们将复习核心的关联规则学习概念和算法,例如支持度和提升度的 Apriori 算法和 FP-Growth 算法。接下来,我们将使用 Weka 对超市数据集进行首次亲和度分析,并研究如何解释生成的规则。我们将通过分析关联规则学习在其他领域中的应用来结束本章,例如 IT 运营分析和医学。

购物篮分析

自电子销售点引入以来,零售商一直在收集大量数据。为了利用这些数据产生商业价值,他们首先开发了一种方法来整合和汇总数据,以了解业务的基本情况。

最近,焦点转向了粒度最低的市场篮交易。在这个细节级别,零售商可以直接看到在其商店购物的每个顾客的市场篮子,不仅了解该特定篮子中购买物品的数量,还了解这些物品是如何一起购买的。这可以用来做出如何区分商店商品组合和商品以及如何有效组合多个产品(在类别内和跨类别)的决策,以推动更高的销售额和利润。这些决策可以在整个零售链、通过渠道、在本地商店层面甚至针对特定顾客(通过所谓的个性化营销)实施,为每个顾客提供独特的产品:

图片

MBA 涵盖了广泛的分析:

  • 物品亲和度:这定义了两个(或更多)物品一起购买的可能性。

  • 驱动项识别:这使能够识别推动人们到商店并始终需要保持库存的物品。

  • 行程分类:这分析篮子内容,并将购物行程分类到某一类别:每周杂货之旅、特殊场合等。

  • 店铺间比较:了解篮子数量可以使任何指标除以总篮子数,从而有效地创建一种方便且易于比较店铺不同特征(每客户的销售单位数、每笔交易的收入、每篮物品数量等)的方法。

  • 收入优化:这有助于确定该商店的神奇价格点,增加购物篮的大小和价值。

  • 营销:这有助于识别更有利可图的广告和促销活动,更精确地定位优惠以提高投资回报率,通过纵向分析生成更好的忠诚度卡促销活动,并吸引更多客流到商店。

  • 运营优化:这有助于通过定制商店和商品组合以适应贸易区域人口统计,并优化商店布局,来匹配库存与需求。

预测模型帮助零售商将正确的优惠提供给正确的客户群体或个人资料,以及了解哪些优惠适用于哪些客户,预测客户对这一优惠的响应概率得分,并理解客户从接受优惠中获得的增值。

关联分析

关联分析用于确定一组商品同时被购买的可能性。在零售业中,存在自然的产品关联;例如,对于购买汉堡饼的人来说,购买汉堡包、番茄酱、芥末、番茄和其他构成汉堡体验的物品是非常典型的。

虽然有些产品关联可能看似微不足道,但也有一些关联并不十分明显。一个经典的例子是牙膏和金枪鱼。似乎吃金枪鱼的人更有可能在用餐后立即刷牙。那么,为什么零售商需要很好地掌握产品关联呢?这些信息对于适当规划促销活动至关重要,因为降低某些商品的价格可能会在不进一步推广这些相关商品的情况下,导致相关高关联商品的需求激增。

在下一节中,我们将探讨关联规则学习的算法:Apriori 和 FP-Growth。

关联规则学习

关联规则学习是发现大型数据库中项目之间有趣关系的一种流行方法。它最常应用于零售业,以揭示产品之间的规律性。

关联规则学习方法通过使用不同的有趣性度量标准,在数据库中寻找有趣且强大的规则模式。例如,以下规则表明,如果一个顾客同时购买洋葱和土豆,他们很可能也会购买汉堡肉:{洋葱,土豆} -> {汉堡}。

另一个可能在每个机器学习课程中都会讲述的经典故事可能是啤酒和尿布的故事。对超市购物者行为的分析显示,购买尿布的客户,可能是年轻男性,也倾向于购买啤酒。这立即成为了一个流行的例子,说明了如何从日常数据中发现意外的关联规则;然而,关于这个故事的真实性存在不同的观点。在《DSS 新闻 2002》中,Daniel Power 说:

"在 1992 年,托马斯·布利斯克,特达数据零售咨询集团的管理员及其团队,对大约 25 家奥斯科药店的 120 万份购物篮进行了分析。开发了数据库查询以识别亲和力。分析确实发现,在下午 5:00 到 7:00 之间,消费者购买啤酒和尿布。奥斯科的管理者没有通过将产品在货架上放得更近来利用啤酒和尿布之间的关系。"

除了前面 MBA 的例子之外,关联规则今天被应用于许多应用领域,包括网络使用挖掘、入侵检测、连续生产和生物信息学。我们将在本章后面更详细地探讨这些领域。

基本概念

在我们深入算法之前,让我们首先回顾基本概念。

交易数据库

在关联规则挖掘中,数据集的结构与第一章中介绍的方法略有不同。首先,没有类值,因为学习关联规则不需要类值。接下来,数据集以事务表的形式呈现,其中每个超市商品对应一个二元属性。因此,特征向量可能非常大。

考虑以下示例。假设我们拥有以下四个收据,如后文所示。每个收据对应一笔购买交易:

为了将这些收据以事务数据库的形式编写,我们首先确定收据中出现的所有可能的项目。这些项目是洋葱土豆汉堡啤酒薯条。每一笔购买,即交易,都在一行中呈现,如果交易中购买了项目,则为1,否则为0,如下表所示:

交易 ID 洋葱 土豆 汉堡 啤酒 薯条
1 0 1 1 0 0
2 1 1 1 1 0
3 0 0 0 1 1
4 1 0 1 1 0

这个例子实际上很小。在实际应用中,数据集通常包含成千上万甚至数百万笔交易,这允许学习算法发现具有统计意义的模式。

项集和规则

项集简单地说是一组项目,例如,{洋葱,土豆,汉堡}。一个规则由两个项集 X 和 Y 组成,以下格式:

X -> Y

这表示一个模式,即当观察到 X 项集时,也会观察到 Y。为了选择有趣的规则,可以使用各种显著性度量。

支持度

对于项集的支持度定义为包含该项集的交易比例。在先前的表中,{土豆,汉堡}项集具有以下支持度,因为它出现在 50%的交易中(四笔交易中的两笔):supp({土豆,汉堡}) = 2/4 = 0.5。

直观上看,它表示支持该模式的交易份额。

提升度

升值是一个衡量目标模型(关联规则)在预测或分类案例为具有增强响应(相对于整体人口)时的性能的指标,与随机选择的目标模型相比。它使用以下公式定义:

图片

置信度

规则的置信度表示其准确性。它使用以下公式定义:

图片

例如,{onions, burger} -> {beer}规则在先前的表中具有置信度0.5/0.5 = 1.0,这意味着当洋葱和汉堡一起购买时,100%的时间也会购买啤酒。

Apriori 算法

Apriori 算法是一种经典算法,用于在事务上进行频繁模式挖掘和关联规则学习。通过识别数据库中的频繁单个项目并将它们扩展到更大的项目集,Apriori 可以确定关联规则,这些规则突出了数据库的一般趋势。

Apriori 算法构建一组项目集,例如,itemset1= {Item A, Item B},并计算支持,即数据库中出现的次数。然后 Apriori 使用自下而上的方法,其中频繁项目集逐个扩展,它通过首先查看较小的集合并认识到一个大的集合不能是频繁的,除非它的所有子集都是频繁的,来消除最大的集合作为候选者。当找不到进一步的扩展时,算法终止。

尽管 Apriori 算法是机器学习中的一个重要里程碑,但它存在许多低效性和权衡。在下一节中,我们将探讨更近期的 FP-Growth 技术。

FP-Growth 算法

FP-Growth(其中 FP 是频繁模式)将事务数据库表示为后缀树。首先,算法统计数据集中项目的出现次数。在第二次遍历中,它构建一个后缀树,这是一种有序的树形数据结构,通常用于存储字符串。以下是根据前一个示例的后缀树示例图:

图片

如果许多事务共享最频繁的项目,后缀树在树根附近提供接近高压缩。直接生长大项目集,而不是生成候选项目并对其与整个数据库进行测试。生长从树的底部开始,通过找到所有匹配最小支持和置信度的项目集。一旦递归过程完成,所有具有最小覆盖范围的大项目集都已找到,并开始创建关联规则。

FP-Growth 算法具有几个优点。首先,它构建了一个 FP 树,以大量紧凑的表示编码原始数据集。其次,它利用 FP 树结构和分而治之策略有效地构建频繁项目集。

超市数据集

位于data/supermarket.arff的超市数据集描述了超市顾客的购物习惯。大多数属性代表特定的商品组,例如,乳制品、牛肉和土豆;或者代表一个部门,例如,部门 79、部门 81 等。以下表格显示了数据库的摘录,其中值为t表示顾客购买了该商品,否则为空。每个顾客有一个实例。该数据集不包含类属性,因为学习关联规则不需要它。以下表格显示了数据的一个样本:

图片

发现模式

为了发现购物模式,我们将使用之前已经研究过的两种算法:Apriori 和 FP-Growth。

Apriori

我们将使用 Weka 中实现的Apriori算法。该算法迭代地减少最小支持度,直到找到给定最小置信度的所需规则数量。我们将使用以下步骤实现该算法:

  1. 我们将使用以下代码行导入所需的库:
import java.io.BufferedReader; 
import java.io.FileReader; 
import weka.core.Instances; 
import weka.associations.Apriori; 
  1. 首先,我们将加载supermarket.arff数据集:
Instances data = new Instances(new BufferedReader(new FileReader("data/supermarket.arff"))); 
  1. 我们将初始化一个Apriori实例,并调用buildAssociations(Instances)函数来开始频繁模式挖掘,如下所示:
Apriori model = new Apriori(); 
model.buildAssociations(data); 
  1. 我们可以输出发现的项集和规则,如下所示:
System.out.println(model); 

输出如下:

   Apriori
    =======

    Minimum support: 0.15 (694 instances)
    Minimum metric <confidence>: 0.9
    Number of cycles performed: 17

    Generated sets of large itemsets:
    Size of set of large itemsets L(1): 44
    Size of set of large itemsets L(2): 380
    Size of set of large itemsets L(3): 910
    Size of set of large itemsets L(4): 633
    Size of set of large itemsets L(5): 105
    Size of set of large itemsets L(6): 1

    Best rules found:

     1\. biscuits=t frozen foods=t fruit=t total=high 788 ==> bread and cake=t 723    <conf:(0.92)> lift:(1.27) lev:(0.03) [155] conv:(3.35)
     2\. baking needs=t biscuits=t fruit=t total=high 760 ==> bread and cake=t 696    <conf:(0.92)> lift:(1.27) lev:(0.03) [149] conv:(3.28)
     3\. baking needs=t frozen foods=t fruit=t total=high 770 ==> bread and cake=t 705    <conf:(0.92)> lift:(1.27) lev:(0.03) [150] conv:(3.27)
    ...

该算法根据置信度输出了十个最佳规则。让我们看看第一条规则并解释输出,如下所示:

biscuits=t frozen foods=t fruit=t total=high 788 ==> bread and cake=t 723    <conf:(0.92)> lift:(1.27) lev:(0.03) [155] conv:(3.35)

它表示,当饼干冷冻食品水果一起购买且总购买价格较高时,也很可能还会购买面包蛋糕{饼干, 冷冻食品, 水果, 总价高}项集出现在788笔交易中,而{面包, 蛋糕}项集出现在723笔交易中。该规则的置信度为0.92,意味着在存在{饼干, 冷冻食品, 水果, 总价高}项集的 92%的交易中,该规则是成立的。

输出还报告了额外的度量,如提升度、杠杆和确信度,这些度量估计了与我们的初始假设的准确性;例如,3.35确信度值表示,如果关联纯粹是随机机会,该规则将错误3.35次。提升度衡量 X 和 Y 在一起出现的次数与它们如果统计独立时预期的次数之比(提升度=1)。X -> Y 规则中的2.16提升度表示 X 的概率是 Y 概率的2.16倍。

FP-Growth

现在,让我们尝试使用更高效的 FP-Growth 算法来获得相同的结果。

FP-Growth 也实现于weka.associations包中:

import weka.associations.FPGrowth; 

FP-Growth 算法的初始化方式与之前我们所做的一样:

FPGrowth fpgModel = new FPGrowth(); 
fpgModel.buildAssociations(data); 
System.out.println(fpgModel); 

输出揭示 FP-Growth 发现了16 条规则

    FPGrowth found 16 rules (displaying top 10)

    1\. [fruit=t, frozen foods=t, biscuits=t, total=high]: 788 ==> [bread and cake=t]: 723   <conf:(0.92)> lift:(1.27) lev:(0.03) conv:(3.35) 
    2\. [fruit=t, baking needs=t, biscuits=t, total=high]: 760 ==> [bread and cake=t]: 696   <conf:(0.92)> lift:(1.27) lev:(0.03) conv:(3.28) 
    ...

我们可以观察到,FP-Growth 找到了与 Apriori 相同的规则集;然而,处理大型数据集所需的时间可以显著缩短。

其他领域的应用

我们研究了亲和力分析,以揭示超市中的购物行为模式。尽管关联规则学习的根源在于分析销售点交易,但它们可以应用于零售业以外的领域,以找到其他类型篮子之间的关系。篮子的概念可以很容易地扩展到服务和产品,例如,分析使用信用卡购买的项目,如租车和酒店房间,以及分析电信客户购买的价值增加服务的信息(呼叫等待、呼叫转接、DSL、快速呼叫等),这可以帮助运营商确定改进服务套餐捆绑的方式。

此外,我们还将探讨以下潜在的跨行业应用示例:

  • 医疗诊断

  • 蛋白质序列

  • 人口普查数据

  • 客户关系管理

  • IT 运营分析

医疗诊断

在医疗诊断中应用关联规则可以帮助医生在治疗患者时。可靠诊断规则归纳的一般问题很困难,因为理论上,没有任何归纳过程可以保证自身归纳出的假设的正确性。实际上,诊断不是一个容易的过程,因为它涉及到不可靠的诊断测试和训练示例中的噪声。

尽管如此,关联规则可以用来识别可能同时出现的症状。在这种情况下,事务对应于一个医疗案例,而症状对应于项目。当患者接受治疗时,记录的症状列表作为一个事务。

蛋白质序列

在理解蛋白质的组成和性质方面已经进行了大量的研究;然而,还有很多事情需要充分理解。现在普遍认为,蛋白质的氨基酸序列不是随机的。

通过关联规则,可以识别蛋白质中存在的不同氨基酸之间的关联。蛋白质是由 20 种氨基酸组成的序列。每种蛋白质都有其独特的三维结构,这取决于氨基酸序列;序列的微小变化可能会改变蛋白质的功能。要应用关联规则,蛋白质对应于一个事务,而氨基酸及其结构对应于项目。

这类关联规则对于增强我们对蛋白质组成的理解是有益的,并且有可能为蛋白质中某些特定氨基酸集合的全球相互作用提供线索。了解这些关联规则或约束对于人工蛋白质的合成是非常有价值的。

人口普查数据

人口普查为研究人员和公众提供了关于社会的各种统计信息。与人口和经济普查相关的信息可以用于规划公共服务(教育、卫生、交通和资金)以及商业(建立新工厂、购物中心或银行,甚至营销特定产品)的预测。

为了发现频繁模式,每个统计区域(例如,市镇、城市和社区)对应一个事务,收集的指标对应于项目。

客户关系管理

客户关系管理(CRM),正如我们在前几章中简要讨论的,是数据丰富的来源,通过这些数据,公司希望识别不同客户群体、产品和服务的偏好,以便增强其产品和服务与客户之间的凝聚力。

关联规则可以加强知识管理过程,并使市场营销人员更好地了解客户,以提供更优质的服务。例如,关联规则可以应用于检测从客户档案和销售数据中不同时间快照的客户行为变化。基本思想是从两个数据集中发现变化,并从每个数据集中生成规则以执行规则匹配。

IT 运营分析

基于大量交易记录,关联规则学习非常适合应用于日常 IT 运营中常规收集的数据,使 IT 运营分析工具能够检测频繁模式并识别关键变化。IT 专家需要看到大局,例如,了解数据库上的问题如何影响应用服务器。

对于特定的一天,IT 运营可能会接收各种警报,并在事务数据库中展示它们。使用关联规则学习算法,IT 运营分析工具可以关联并检测同时出现的警报的频繁模式。这可以更好地理解一个组件如何影响另一个组件。

通过识别出的警报模式,可以应用预测分析。例如,一个特定的数据库服务器托管一个网络应用程序,突然触发了关于数据库的警报。通过分析关联规则学习算法识别出的频繁模式,这意味着 IT 人员需要在网络应用程序受到影响之前采取行动。

关联规则学习还可以发现源自同一 IT 事件的警报事件。例如,每次添加新用户时,都会检测到 Windows 操作系统的六个变化。接下来,在应用组合管理APM)中,IT 可能会面临多个警报,显示数据库中的事务时间过高。如果所有这些问题都源自同一来源(例如,收到数百个关于更改的警报,所有这些更改都归因于 Windows 更新),这种频繁模式挖掘可以帮助快速筛选出多个警报,使 IT 操作员能够专注于真正关键的变化。

摘要

在本章中,你学习了如何利用关联规则学习在事务数据集中获取关于频繁模式的洞察。我们在 Weka 中进行了亲和力分析,并了解到努力的工作在于结果分析——在解释规则时需要仔细注意,因为关联(即相关性)并不等同于因果关系。

在下一章中,我们将探讨如何使用可扩展的机器学习库 Apache Mahout 将商品推荐问题提升到下一个层次,它能够处理大数据。

第六章:使用 Apache Mahout 的推荐引擎

推荐引擎是目前初创公司中最常用的数据科学方法之一。构建推荐系统有两种主要技术:基于内容的过滤和协同过滤。基于内容的算法使用项目的属性来找到具有相似属性的项目。协同过滤算法使用用户评分或其他用户行为,并根据具有相似行为的用户喜欢或购买的项目来做出推荐。

在本章中,我们首先将解释理解推荐引擎原理所需的基本概念,然后我们将展示如何利用 Apache Mahout 实现的各种算法来快速构建可扩展的推荐引擎。

本章将涵盖以下主题:

  • 如何构建推荐引擎

  • 准备 Apache Mahout

  • 基于内容的推荐方法

  • 协同过滤方法

到本章结束时,你将了解适合我们问题的推荐引擎类型以及如何快速实现该引擎。

基本概念

推荐引擎的目标是向用户展示感兴趣的项目。它们与搜索引擎的不同之处在于,相关内容通常在网站上出现,而无需用户请求,用户也不需要构建查询,因为推荐引擎会观察用户的行为并在用户不知情的情况下为用户构建查询。

毫无疑问,最著名的推荐引擎例子是www.amazon.com,它以多种方式提供个性化推荐。以下截图显示了“购买此商品的用户也购买了”的示例。正如你稍后将会看到的,这是一个基于项目的协同过滤推荐的例子,其中推荐了与特定项目相似的项目:

图片

在本节中,我们将介绍与理解构建推荐引擎相关的关键概念。

关键概念

推荐引擎需要以下输入来做出推荐:

  • 项目信息,用属性描述

  • 用户资料,例如年龄范围、性别、位置、朋友等

  • 用户交互,包括评分、浏览、标记、比较、保存和电子邮件

  • 项目将显示的上下文;例如,项目的类别和项目的地理位置

这个输入随后会被推荐引擎结合使用,以帮助获取以下信息:

  • 购买、观看、查看或收藏此项目的用户也购买了、观看、查看或收藏

  • 与此项目相似的项目

  • 你可能认识的其他用户

  • 与你相似的其他用户

现在,让我们更详细地看看这种组合是如何工作的。

基于用户和基于项目的分析

构建推荐引擎取决于引擎在尝试推荐特定物品时是搜索相关物品还是用户。

在基于物品的分析中,引擎专注于识别与特定物品相似的物品,而在基于用户的分析中,首先确定与特定用户相似的用户。例如,确定具有相同配置文件信息(年龄、性别等)或行为历史(购买、观看、查看等)的用户,然后向其他类似用户推荐相同的物品。

这两种方法都需要我们计算一个相似度矩阵,这取决于我们是分析物品属性还是用户行为。让我们深入了解这是如何完成的。

计算相似度

计算相似度有三种基本方法,如下所示:

  • 协同过滤算法通过用户评分或其他用户行为,根据具有相似行为的用户喜欢或购买的内容进行推荐

  • 基于内容的算法使用物品的特性来寻找具有相似特性的物品

  • 混合方法结合了协同过滤和基于内容的过滤

在接下来的几节中,我们将详细探讨每种方法。

协同过滤

协同过滤仅基于用户评分或其他用户行为,根据具有相似行为的用户喜欢或购买的内容进行推荐。

协同过滤的一个关键优势是它不依赖于物品内容,因此能够准确地推荐复杂物品,如电影,而不需要了解物品本身。其基本假设是,过去达成共识的人将来也会达成共识,并且他们将会喜欢与过去相似类型的物品。

这种方法的一个主要缺点是所谓的冷启动问题,这意味着如果我们想构建一个准确的协同过滤系统,算法通常需要大量的用户评分。这通常会将协同过滤排除在产品的第一版之外,并在收集到一定量的数据后引入。

基于内容的过滤

另一方面,基于内容的过滤是基于项目描述和用户偏好配置文件的,它们如下组合。首先,项目用属性进行描述,为了找到相似的项目,我们使用距离度量(如余弦距离或皮尔逊系数)来测量项目之间的距离(关于距离度量,请参阅第一章,《应用机器学习快速入门》)。现在,用户配置文件进入方程。给定用户喜欢的项目类型的反馈,我们可以引入权重,指定特定项目属性的重要性。例如,Pandora 流媒体服务应用基于内容的过滤来创建电台,使用超过 400 个属性。用户最初选择具有特定属性的曲目,并通过提供反馈,强调重要的歌曲属性。

初始时,这种方法对用户反馈信息的需求非常少;因此,它有效地避免了冷启动问题。

混合方法

现在,在协作式和基于内容的推荐之间,你应该选择哪一个?协作式过滤能够从用户针对一个内容源的行为中学习用户偏好,并将这些偏好应用于其他内容类型。基于内容的过滤仅限于推荐用户已经使用过的同一类型的内容。这在某些用例中提供了价值;例如,基于新闻浏览推荐新闻文章是有用的,但如果可以根据新闻浏览推荐不同来源的内容,如书籍和电影,那就更有用了。

协作式过滤和基于内容的过滤不是相互排斥的;在某些情况下,它们可以结合使用,以更有效。例如,Netflix 使用协作式过滤来分析类似用户的搜索和观看模式,以及基于内容的过滤来提供与用户高度评价的电影具有相似特征的电影。

存在着各种各样的混合技术:加权、切换、混合、特征组合、特征增强、级联、元级等。推荐系统是机器学习和数据挖掘社区中的一个活跃领域,在数据科学会议上设有专门的轨道。Adomavicius 和 Tuzhilin 在 2005 年的论文《迈向下一代推荐系统:对现有技术和可能扩展的调查》中对技术进行了很好的概述,其中作者讨论了不同的方法和底层算法,并提供了进一步论文的参考文献。为了更深入地了解特定方法何时合理,并理解所有细微的细节,你应该查看 Ricci 等人编辑的书籍:《推荐系统手册》(第一版,2010 年,Springer-Verlag,纽约)。

探索与利用

在推荐系统中,总是存在一种权衡,即在基于我们已知用户信息推荐符合用户喜好的项目(利用)和推荐不符合用户喜好的项目,旨在让用户接触一些新奇之处(探索)之间。缺乏探索的推荐系统只会推荐与之前用户评分一致的项目,从而防止展示用户当前泡泡之外的项目。在实践中,从用户喜好的泡泡之外获得新项目的惊喜往往是非常受欢迎的,这可能导致令人愉快的惊喜,甚至可能发现新的喜好的区域。

在本节中,我们讨论了开始构建推荐引擎所需的基本概念。现在,让我们看看如何使用 Apache Mahout 实际构建一个推荐引擎。

获取 Apache Mahout

Mahout 在 第二章,“Java 机器学习库和平台”中被介绍为一个可扩展的机器学习库。它提供了一套丰富的组件,您可以使用这些组件从算法选择中构建定制的推荐系统。Mahout 的创造者表示,它被设计为适用于企业;它旨在性能、可扩展性和灵活性。

Mahout 可以配置为以两种方式运行:带有或没有 Hadoop,以及对于单机和分布式处理。我们将专注于配置不带 Hadoop 的 Mahout。对于更高级的配置和 Mahout 的进一步使用,我推荐两本近期出版的书籍:由 Chandramani Tiwary 编著的 Learning Apache Mahout,Packt 出版,以及 Ashish Gupta 编著的 Learning Apache Mahout Classification,Packt 出版。

由于 Apache Mahout 的构建和发布系统基于 Maven,您需要学习如何安装它。我们将查看最方便的方法;使用带有 Maven 插件的 Eclipse。

在 Eclipse 中使用 Maven 插件配置 Mahout

您需要一个较新的 Eclipse 版本,可以从其主页下载(www.eclipse.org/downloads/)。在本书中,我们将使用 Eclipse Luna。打开 Eclipse 并使用默认设置启动一个新的 Maven 项目,如图所示:

图片

将会显示新的 Maven 项目屏幕,如图所示:

图片

现在,我们需要告诉项目添加 Mahout JAR 文件及其依赖项。定位到 pom.xml 文件,并使用文本编辑器打开它(左键单击“打开方式”|“文本编辑器”),如图所示:

图片

定位到以 <dependencies> 开头的行,并在下一行添加以下代码:

<dependency> 
 <groupId>org.apache.mahout</groupId> 
  <artifactId>mahout-mr</artifactId> 
  <version>0.10.0</version> 
</dependency> 

就这样;Mahout 已经添加,我们准备开始。

构建推荐引擎

为了演示基于内容的过滤和协同过滤方法,我们将构建一个书籍推荐引擎。

书籍评分数据集

在本章中,我们将使用一个书籍评分数据集(Ziegler et al., 2005),该数据集是在四周的爬取中收集的。它包含了 Book-Crossing 网站 278,858 名成员的数据和 1,157,112 个评分,包括隐式和显式评分,涉及 271,379 个不同的 ISBN。用户数据被匿名化,但包含人口统计信息。数据集来自Cai-Nicolas Ziegler, Sean M. McNee, Joseph A. Konstan, Georg Lausen: Proceedings of the 14th International World Wide Web Conference (WWW '05)Cai-Nicolas Ziegler,Sean M. McNee,Joseph A. Konstan,Georg Lausen:第十四届国际万维网会议论文集(WWW '05)*,2005 年 5 月 10 日至 14 日,日本千叶(www2.informatik.uni-freiburg.de/~cziegler/BX/)。

Book-Crossing 数据集由三个文件组成,如下:

  • BX-Users:这个文件包含用户信息。请注意,用户 ID(User-ID)已经被匿名化并映射到整数。如果可用,提供人口统计信息(位置和年龄)。否则,这些字段包含空值。

  • BX-Books:书籍通过其相应的 ISBN 进行标识。无效的 ISBN 已经从数据集中删除。此外,还提供了一些基于内容的信息(Book-Title,Book-Author,Year-Of-Publication,和 Publisher),这些信息是从亚马逊网络服务获得的。请注意,在多个作者的情况下,只提供第一个作者。还提供了链接到封面图片的 URL,有三种不同的格式(Image-URL-S,Image-URL-M,和 Image-URL-L),分别指小、中、大 URL。这些 URL 指向亚马逊网站。

  • BX-Book-Ratings:这个文件包含书籍评分信息。评分(Book-Rating)要么是显式的,表示在 1-10 的刻度上(数值越高表示越受欣赏),要么是隐式的,用 0 表示。

加载数据

根据数据存储的位置(文件或数据库),有两种加载数据的方法:首先,我们将详细探讨如何从文件加载数据,包括如何处理自定义格式。最后,我们将快速查看如何从数据库加载数据。

从文件加载数据

可以使用FileDataModel类从文件加载数据。我们期望的是一个逗号分隔的文件,其中每行包含一个userID,一个itemID,一个可选的preference值和一个可选的timestamp,顺序如下:

userID,itemID[,preference[,timestamp]] 

一个可选的偏好设置可以适应具有二元偏好值的程序,也就是说,用户要么表达对一个项目的偏好,要么不表达,没有偏好程度;例如,用喜欢或不喜欢表示。

以井号(#)或空行开始的行将被忽略。这些行也可以包含额外的字段,这些字段将被忽略。

DataModel类假定以下类型:

  • userIDitemID可以被解析为long

  • preference值可以被解析为double

  • timestamp可以被解析为long

如果您能够以先前格式提供数据集,您可以使用以下行加载数据:

DataModel model = new FileDataModel(new File(path)); 

这个类不适用于处理大量数据;例如,数千万行。对于这种情况,一个基于 JDBC 的DataModel和数据库更为合适。

然而,在现实世界中,我们并不能总是确保提供给我们的输入数据只包含userIDitemID的整数值。例如,在我们的案例中,itemID对应于 ISBN 书号,这些书号唯一标识项目,但它们不是整数,FileDataModel的默认设置将不适合处理我们的数据。

现在,让我们考虑如何处理我们的itemID是一个字符串的情况。我们将通过扩展FileDataModel并重写长readItemIDFromString(String)方法来定义我们的自定义数据模型,以便将itemID作为字符串读取并将其转换为long,并返回一个唯一的long值。为了将String转换为唯一的long,我们将扩展另一个 Mahout 的AbstractIDMigrator辅助类,该类正是为此任务设计的。

现在,让我们看看如何扩展FileDataModel

class StringItemIdFileDataModel extends FileDataModel { 

  //initialize migrator to covert String to unique long 
  public ItemMemIDMigrator memIdMigtr; 

  public StringItemIdFileDataModel(File dataFile, String regex) 
     throws IOException { 
    super(dataFile, regex); 
  } 

  @Override 
  protected long readItemIDFromString(String value) { 

    if (memIdMigtr == null) { 
      memIdMigtr = new ItemMemIDMigrator(); 
    } 

    // convert to long 
    long retValue = memIdMigtr.toLongID(value); 
    //store it to cache  
    if (null == memIdMigtr.toStringID(retValue)) { 
      try { 
        memIdMigtr.singleInit(value); 
      } catch (TasteException e) { 
        e.printStackTrace(); 
      } 
    } 
    return retValue; 
  } 

  // convert long back to String 
  String getItemIDAsString(long itemId) { 
    return memIdMigtr.toStringID(itemId); 
  } 
} 

可以覆盖的其他有用方法如下:

  • readUserIDFromString(String value),如果用户 ID 不是数字

  • readTimestampFromString(String value),以改变timestamp的解析方式

现在,让我们看看如何扩展AbstractIDMIgrator

class ItemMemIDMigrator extends AbstractIDMigrator { 

  private FastByIDMap<String> longToString; 

  public ItemMemIDMigrator() { 
    this.longToString = new FastByIDMap<String>(10000); 
  } 

  public void storeMapping(long longID, String stringID) { 
    longToString.put(longID, stringID); 
  } 

  public void singleInit(String stringID) throws TasteException { 
    storeMapping(toLongID(stringID), stringID); 
  } 

  public String toStringID(long longID) { 
    return longToString.get(longID); 
  } 
} 

现在,我们已经准备好了所有东西,我们可以使用以下代码加载数据集:

StringItemIdFileDataModel model = new StringItemIdFileDataModel( 
  new File("datasets/chap6/BX-Book-Ratings.csv"), ";"); 
System.out.println( 
"Total items: " + model.getNumItems() +  
"\nTotal users: " +model.getNumUsers()); 

这提供了用户和项目的总数作为输出:

    Total items: 340556
    Total users: 105283

我们已经准备好继续前进并开始制作推荐。

从数据库加载数据

或者,我们可以使用 JDBC 数据模型之一从数据库加载数据。在本章中,我们不会深入介绍如何设置数据库、连接等详细说明,但我们将给出如何做到这一点的概要。

数据库连接器已被移动到单独的包mahout-integration中;因此,我们必须将此包添加到我们的dependency列表中。打开pom.xml文件并添加以下dependency

<dependency> 
  <groupId>org.apache.mahout</groupId> 
  <artifactId>mahout-integration</artifactId> 
  <version>0.7</version> 
</dependency> 

考虑到我们想要连接到 MySQL 数据库。在这种情况下,我们还需要一个处理数据库连接的包。将以下内容添加到pom.xml文件中:

<dependency> 
  <groupId>mysql</groupId> 
  <artifactId>mysql-connector-java</artifactId> 
  <version>5.1.35</version> 
</dependency> 

现在,我们已经有了所有的包,因此我们可以创建一个连接。首先,让我们使用以下方式初始化一个带有连接详情的DataSource类:

MysqlDataSource dbsource = new MysqlDataSource(); 
  dbsource.setUser("user"); 
  dbsource.setPassword("pass"); 
  dbsource.setServerName("hostname.com"); 
  dbsource.setDatabaseName("db"); 

Mahout 集成实现了JDBCDataModel,以便通过 JDBC 访问各种数据库。默认情况下,此类假定在 JNDI 名称jdbc/taste下有一个可用的DataSource,它提供了访问数据库的权限,

taste_preferences表,具有以下模式:

CREATE TABLE taste_preferences ( 
  user_id BIGINT NOT NULL, 
  item_id BIGINT NOT NULL, 
  preference REAL NOT NULL, 
  PRIMARY KEY (user_id, item_id) 
) 
CREATE INDEX taste_preferences_user_id_index ON taste_preferences 
   (user_id); 
CREATE INDEX taste_preferences_item_id_index ON taste_preferences 
   (item_id); 

基于数据库的数据模型初始化如下。除了 DB 连接对象外,我们还可以指定自定义表名和表列名,如下所示:

DataModel dataModel = new MySQLJDBCDataModel(dbsource, 
   "taste_preferences",  
  "user_id", "item_id", "preference", "timestamp"); 

内存数据库

最后,但同样重要的是,数据模型可以即时创建并保存在内存中。可以从偏好数组创建一个数据库,它将保存一组物品的用户评分。

我们可以按照以下步骤进行。首先,我们创建一个偏好数组PreferenceArrayFastByIdMap哈希表,它存储了一个偏好数组:

FastByIDMap <PreferenceArray> preferences = new FastByIDMap 
   <PreferenceArray> ();  

接下来,我们可以为用户创建一个新的偏好数组,该数组将保存他们的评分。数组必须使用大小参数初始化,以在内存中预留这么多槽位:

PreferenceArray prefsForUser1 =  
  new GenericUserPreferenceArray (10);   

接着,我们将当前偏好的用户 ID 设置在位置0。这实际上会将所有偏好的用户 ID 都设置好:

prefsForUser1.setUserID (0, 1L);  

itemID设置为0位置的当前偏好,如下所示:

prefsForUser1.setItemID (0, 101L);  

将偏好值设置为0位置的偏好,如下所示:

prefsForUser1.setValue (0, 3.0f);   

按照以下方式继续对其他物品评分:

prefsForUser1.setItemID (1, 102L);  
prefsForUser1.setValue (1, 4.5F);  

最后,将用户的preferences添加到哈希表中:

preferences.put (1L, prefsForUser1); // use userID as the key  

现在可以使用偏好哈希表来初始化GenericDataModel

DataModel dataModel = new GenericDataModel(preferences); 

这段代码演示了如何为单个用户添加两个偏好;在实际应用中,你可能需要为多个用户添加多个偏好:

协同过滤

Mahout 中的推荐引擎可以使用org.apache.mahout.cf.taste包构建,该包之前是一个名为Taste的独立项目,并且一直在 Mahout 中继续开发。

基于 Mahout 的协同过滤引擎通过用户的物品偏好(口味)并返回其他物品的估计偏好。例如,一个销售书籍或 CD 的网站可以很容易地使用 Mahout 来找出顾客可能感兴趣的 CD,借助之前的购买数据。

最高级别的包定义了 Mahout 接口到以下关键抽象:

  • DataModel:这代表了一个关于用户及其对物品偏好的信息库

  • UserSimilarity:这定义了两个用户之间相似性的概念

  • ItemSimilarity:这定义了两个物品之间相似性的概念

  • UserNeighborhood:这为给定用户计算邻近用户

    • Recommender:这为用户推荐物品

上述概念的一般结构如下所示:

基于用户的过滤

最基本的基于用户的协同过滤可以通过初始化之前描述的组件来实现,如下所示:

首先,加载数据模型:

StringItemIdFileDataModel model = new StringItemIdFileDataModel( 
    new File("/datasets/chap6/BX-Book-Ratings.csv", ";"); 

接下来,定义如何计算用户之间的相关性;例如,使用皮尔逊相关系数:

UserSimilarity similarity =  
  new PearsonCorrelationSimilarity(model); 

接下来,定义如何根据用户的评分来判断哪些用户是相似的,即彼此靠近的用户:

UserNeighborhood neighborhood =  
  new ThresholdUserNeighborhood(0.1, similarity, model); 

现在,我们可以使用modelneighborhood和类似对象的数据初始化一个GenericUserBasedRecommender默认引擎,如下所示:

UserBasedRecommender recommender =  
new GenericUserBasedRecommender(model, neighborhood, similarity); 

就这样。我们的第一个基本推荐引擎已经准备好了。让我们讨论如何调用推荐。首先,让我们打印出用户已经评价的项目,以及为该用户提供的十个推荐项目:

long userID = 80683; 
int noItems = 10; 

List<RecommendedItem> recommendations = recommender.recommend( 
  userID, noItems); 

System.out.println("Rated items by user:"); 
for(Preference preference : model.getPreferencesFromUser(userID)) { 
  // convert long itemID back to ISBN 
  String itemISBN = model.getItemIDAsString( 
  preference.getItemID()); 
  System.out.println("Item: " + books.get(itemISBN) +  
    " | Item id: " + itemISBN +  
    " | Value: " + preference.getValue()); 
} 

System.out.println("\nRecommended items:"); 
for (RecommendedItem item : recommendations) { 
  String itemISBN = model.getItemIDAsString(item.getItemID()); 
  System.out.println("Item: " + books.get(itemISBN) +  
    " | Item id: " + itemISBN +  
    " | Value: " + item.getValue()); 
} 

这将输出以下推荐,包括它们的分数:

    Rated items:
    Item: The Handmaid's Tale | Item id: 0395404258 | Value: 0.0
    Item: Get Clark Smart : The Ultimate Guide for the Savvy Consumer | Item id: 1563526298 | Value: 9.0
    Item: Plum Island | Item id: 0446605409 | Value: 0.0
    Item: Blessings | Item id: 0440206529 | Value: 0.0
    Item: Edgar Cayce on the Akashic Records: The Book of Life | Item id: 0876044011 | Value: 0.0
    Item: Winter Moon | Item id: 0345386108 | Value: 6.0
    Item: Sarah Bishop | Item id: 059032120X | Value: 0.0
    Item: Case of Lucy Bending | Item id: 0425060772 | Value: 0.0
    Item: A Desert of Pure Feeling (Vintage Contemporaries) | Item id: 0679752714 | Value: 0.0
    Item: White Abacus | Item id: 0380796155 | Value: 5.0
    Item: The Land of Laughs : A Novel | Item id: 0312873115 | Value: 0.0
    Item: Nobody's Son | Item id: 0152022597 | Value: 0.0
    Item: Mirror Image | Item id: 0446353957 | Value: 0.0
    Item: All I Really Need to Know | Item id: 080410526X | Value: 0.0
    Item: Dreamcatcher | Item id: 0743211383 | Value: 7.0
    Item: Perplexing Lateral Thinking Puzzles: Scholastic Edition | Item id: 0806917695 | Value: 5.0
    Item: Obsidian Butterfly | Item id: 0441007813 | Value: 0.0

    Recommended items:
    Item: Keeper of the Heart | Item id: 0380774933 | Value: 10.0
    Item: Bleachers | Item id: 0385511612 | Value: 10.0
    Item: Salem's Lot | Item id: 0451125452 | Value: 10.0
    Item: The Girl Who Loved Tom Gordon | Item id: 0671042858 | Value: 10.0
    Item: Mind Prey | Item id: 0425152898 | Value: 10.0
    Item: It Came From The Far Side | Item id: 0836220730 | Value: 10.0
    Item: Faith of the Fallen (Sword of Truth, Book 6) | Item id: 081257639X | Value: 10.0
    Item: The Talisman | Item id: 0345444884 | Value: 9.86375
    Item: Hamlet | Item id: 067172262X | Value: 9.708363
    Item: Untamed | Item id: 0380769530 | Value: 9.708363

基于项目的过滤

在这里需要讨论的最重要的一点是ItemSimilarity属性。基于项目的推荐器很有用,因为它们可以利用一些非常快速的东西;它们基于项目相似性进行计算,而不是用户相似性,并且项目相似性相对静态。它可以预先计算,而不是实时重新计算。

因此,强烈建议您在使用此类时使用预计算的相似性的GenericItemSimilarity。您也可以使用PearsonCorrelationSimilarity,它实时计算相似性,但您可能会发现对于大量数据来说这非常慢:

StringItemIdFileDataModel model = new StringItemIdFileDataModel( 
  new File("datasets/chap6/BX-Book-Ratings.csv"), ";"); 

ItemSimilarity itemSimilarity = new 
   PearsonCorrelationSimilarity(model); 

ItemBasedRecommender recommender = new 
   GenericItemBasedRecommender(model, itemSimilarity); 

String itemISBN = "0395272238"; 
long itemID = model.readItemIDFromString(itemISBN); 
int noItems = 10; 
List<RecommendedItem> recommendations = 
   recommender.mostSimilarItems(itemID, noItems); 

System.out.println("Recommendations for item: 
   "+books.get(itemISBN)); 

System.out.println("\nMost similar items:"); 
for (RecommendedItem item : recommendations) { 
  itemISBN = model.getItemIDAsString(item.getItemID()); 
  System.out.println("Item: " + books.get(itemISBN) + " | Item id: 
     " + itemISBN + " | Value: " + item.getValue()); 
} 
Recommendations for item: Close to the BoneMost similar items:Item: Private Screening | Item id: 0345311396 | Value: 1.0Item: Heartstone | Item id: 0553569783 | Value: 1.0Item: Clockers / Movie Tie In | Item id: 0380720817 | Value: 1.0Item: Rules of Prey | Item id: 0425121631 | Value: 1.0Item: The Next President | Item id: 0553576666 | Value: 1.0Item: Orchid Beach (Holly Barker Novels (Paperback)) | Item id: 0061013412 | Value: 1.0Item: Winter Prey | Item id: 0425141233 | Value: 1.0Item: Night Prey | Item id: 0425146413 | Value: 1.0Item: Presumed Innocent | Item id: 0446359866 | Value: 1.0Item: Dirty Work (Stone Barrington Novels (Paperback)) | Item id: 
   0451210158 | Value: 1.0

返回的列表将返回一组与我们选定的特定项目相似的项目。

向推荐中添加自定义规则

经常会有一些业务规则要求我们提高所选项目的分数。例如,在书籍数据集中,如果一本书是新的,我们希望给它一个更高的分数。这可以通过使用IDRescorer接口来实现,如下所示:

  • rescore(long, double)接受itemId和原始分数作为参数,并返回一个修改后的分数。

  • isFiltered(long)返回true以排除推荐中的特定项目,或者返回false

我们的示例可以如下实现:

class MyRescorer implements IDRescorer { 

  public double rescore(long itemId, double originalScore) { 
    double newScore = originalScore; 
    if(bookIsNew(itemId)){ 
      originalScore *= 1.3; 
    } 
    return newScore; 
  } 

  public boolean isFiltered(long arg0) { 
    return false; 
  } 

} 

在调用recommender.recommend时提供了一个IDRescorer实例:

IDRescorer rescorer = new MyRescorer(); 
List<RecommendedItem> recommendations =  
recommender.recommend(userID, noItems, rescorer); 

评估

您可能想知道如何确保返回的推荐是有意义的。真正确保推荐有效性的唯一方法是在实际系统中使用 A/B 测试,并使用真实用户。例如,A 组收到一个随机项目作为推荐,而 B 组收到我们引擎推荐的项。

由于这并不总是可能(也不实用),我们可以通过离线统计分析来获得一个估计值。一种进行的方法是使用第一章中介绍的 k 折交叉验证,《应用机器学习快速入门》。我们将数据集划分为多个集合;一些用于训练我们的推荐引擎,其余的用于测试推荐引擎对未知用户推荐项目的效果。

Mahout 实现了RecommenderEvaluator类,它将数据集分成两部分。第一部分(默认为 90%)用于生成推荐,而其余数据则与估计的偏好值进行比较,以测试匹配度。该类不接受recommender对象直接;你需要构建一个实现RecommenderBuilder接口的类,该类为给定的DataModel对象构建一个recommender对象,然后用于测试。让我们看看这是如何实现的。

首先,我们创建一个实现RecommenderBuilder接口的类。我们需要实现buildRecommender方法,它将返回一个recommender,如下所示:

public class BookRecommender implements RecommenderBuilder  { 
  public Recommender buildRecommender(DataModel dataModel) { 
    UserSimilarity similarity =  
      new PearsonCorrelationSimilarity(model); 
    UserNeighborhood neighborhood =  
      new ThresholdUserNeighborhood(0.1, similarity, model); 
    UserBasedRecommender recommender =  
      new GenericUserBasedRecommender( 
        model, neighborhood, similarity); 
    return recommender; 
  } 
} 

现在我们有一个返回推荐对象类的类,我们可以初始化一个RecommenderEvaluator实例。这个类的默认实现是AverageAbsoluteDifferenceRecommenderEvaluator类,它计算预测评分和实际评分之间的平均绝对差异。以下代码展示了如何组合这些部分并运行一个保留测试:

首先,加载一个数据模型,如下所示:

DataModel dataModel = new FileDataModel( 
  new File("/path/to/dataset.csv")); 

接下来,初始化一个evaluator实例,如下所示:

RecommenderEvaluator evaluator =  
  new AverageAbsoluteDifferenceRecommenderEvaluator(); 

初始化实现RecommenderBuilder接口的BookRecommender对象,如下所示:

RecommenderBuilder builder = new MyRecommenderBuilder(); 

最后,调用evaluate()方法,它接受以下参数:

  • RecommenderBuilder:这是实现RecommenderBuilder的对象,可以构建用于测试的recommender

  • DataModelBuilder:这表示要使用的DataModelBuilder;如果为 null,将使用默认的DataModel实现

  • DataModel:这是用于测试的数据集

  • trainingPercentage:这表示用于生成推荐的每个用户的偏好百分比;其余的与估计的偏好值进行比较,以评估recommender的性能

  • evaluationPercentage:这是用于评估的用户百分比

该方法调用如下:

double result = evaluator.evaluate(builder, null, model, 0.9, 
   1.0); 
System.out.println(result); 

该方法返回一个double类型的值,其中0代表最佳可能的评估,意味着推荐器完美匹配用户偏好。一般来说,值越低,匹配度越好。

在线学习引擎

在任何在线平台上,新用户将持续增加。之前讨论的方法对现有用户效果良好。为每个新增用户创建推荐实例的成本很高。我们不能忽视在推荐引擎制作后添加到系统中的用户。为了应对类似这种情况,Apache Mahout 具有将临时用户添加到数据模型的能力。

一般设置如下:

  • 定期使用当前数据重新创建整个推荐(例如,每天或每小时,具体取决于所需时间)

  • 在进行推荐之前,始终检查用户是否存在于系统中

  • 如果用户存在,则完成推荐

  • 如果用户不存在,创建一个临时用户,填写偏好,然后进行推荐

第一步似乎有些棘手,因为它涉及到使用当前数据生成整个推荐的频率。如果系统很大,内存限制将会存在,因为当新的推荐器正在生成时,旧的、正在工作的推荐器应该保留在内存中,以便在新的推荐器准备好之前,请求由旧副本提供服务。

至于临时用户,我们可以用PlusAnonymousConcurrentUserDataModel实例包装我们的数据模型。这个类允许我们获取一个临时用户 ID;该 ID 必须稍后释放以便可以重用(这类 ID 的数量有限)。在获取 ID 后,我们必须填写偏好,然后我们可以像往常一样进行推荐:

class OnlineRecommendation{ 

  Recommender recommender; 
  int concurrentUsers = 100; 
  int noItems = 10; 

  public OnlineRecommendation() throws IOException { 

    DataModel model = new StringItemIdFileDataModel( 
      new File /chap6/BX-Book-Ratings.csv"), ";"); 
    PlusAnonymousConcurrentUserDataModel plusModel = new 
       PlusAnonymousConcurrentUserDataModel
         (model, concurrentUsers); 
    recommender = ...; 

  } 

  public List<RecommendedItem> recommend(long userId, 
     PreferenceArray preferences){ 

    if(userExistsInDataModel(userId)){ 
      return recommender.recommend(userId, noItems); 
    } 

    else{ 

      PlusAnonymousConcurrentUserDataModel plusModel = 
        (PlusAnonymousConcurrentUserDataModel) 
           recommender.getDataModel(); 

      // Take an available anonymous user form the poll 
      Long anonymousUserID = plusModel.takeAvailableUser(); 

      // Set temporary preferences 
      PreferenceArray tempPrefs = preferences; 
      tempPrefs.setUserID(0, anonymousUserID); 
      tempPrefs.setItemID(0, itemID); 
       plusModel.setTempPrefs(tempPrefs, anonymousUserID); 

      List<RecommendedItem> results = 
         recommender.recommend(anonymousUserID, noItems); 

      // Release the user back to the poll 
      plusModel.releaseUser(anonymousUserID); 

      return results; 

    } 

  } 
} 

基于内容的过滤

基于内容的过滤不在 Mahout 框架的范围内,主要是因为如何定义相似项目取决于你。如果我们想进行基于内容的项目相似度,我们需要实现自己的ItemSimilarity。例如,在我们的书籍数据集中,我们可能想要为书籍相似度制定以下规则:

  • 如果流派相同,将0.15加到相似度

  • 如果作者相同,将0.50加到相似度

我们现在可以实施自己的相似度度量,如下所示:

class MyItemSimilarity implements ItemSimilarity { 
 ... 
 public double itemSimilarity(long itemID1, long itemID2) { 
  MyBook book1 = lookupMyBook (itemID1); 
  MyBook book2 = lookupMyBook (itemID2); 
  double similarity = 0.0; 
  if (book1.getGenre().equals(book2.getGenre())  
   similarity += 0.15; 
  } 
  if (book1.getAuthor().equals(book2\. getAuthor ())) { 
   similarity += 0.50; 
  } 
  return similarity; 
 } 
 ... 
} 

我们可以使用这个ItemSimilarity,而不是像LogLikelihoodSimilarity或其他带有GenericItemBasedRecommender的实现。这就是全部。这就是我们在 Mahout 框架中执行基于内容推荐所必须做的。

我们在这里看到的是基于内容推荐的最简单形式之一。另一种方法是根据项目特征的加权向量创建用户的内容配置文件。权重表示每个特征对用户的重要性,并且可以从单独评分的内容向量中计算得出。

摘要

在本章中,你了解了推荐引擎的基本概念,协作过滤和基于内容过滤之间的区别,以及如何使用 Apache Mahout,这是一个创建推荐器的绝佳基础,因为它非常可配置,并提供了许多扩展点。我们探讨了如何选择正确的配置参数值,设置重新评分,并评估推荐结果。

通过本章,我们完成了对用于分析客户行为的数据科学技术的概述,这始于第四章中关于客户关系预测的讨论,即使用集成进行客户关系预测,并继续到第五章的亲和力分析,即亲和力分析。在下一章中,我们将转向其他主题,例如欺诈和异常检测。

第七章:欺诈和异常检测

异常检测用于识别异常、罕见事件和其他异常情况。这些异常可能像针尖上的麦芒,但它们的后果可能非常严重;例如,信用卡欺诈检测、识别网络入侵、制造过程中的故障、临床试验、投票活动以及电子商务中的犯罪活动。因此,当发现异常时,它们具有很高的价值;如果没有发现,则成本高昂。将机器学习应用于异常检测问题可以带来新的见解和更好的异常事件检测。机器学习可以考虑到许多不同的数据来源,并可以发现人类分析难以识别的相关性。

以电子商务欺诈检测为例。在机器学习算法到位的情况下,购买者的在线行为,即网站浏览历史,成为欺诈检测算法的一部分,而不仅仅是持卡人购买历史的记录。这涉及到分析各种数据来源,但这也是一种更稳健的电子商务欺诈检测方法。

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

  • 问题与挑战

  • 可疑模式检测

  • 异常模式检测

  • 与不平衡数据集合作

  • 时间序列中的异常检测

可疑和异常行为检测

从传感器数据中学习模式的问题出现在许多应用中,包括电子商务、智能环境、视频监控、网络分析、人机交互、环境辅助生活等等。我们专注于检测偏离常规行为且可能代表安全风险、健康问题或任何其他异常行为的情况。

换句话说,偏差行为是一种数据模式,它要么不符合预期的行为(异常行为),要么与先前定义的不希望的行为相匹配(可疑行为)。偏差行为模式也被称为异常值、例外、特殊性、惊喜、滥用等等。这种模式相对较少发生;然而,当它们发生时,其后果可能非常严重,并且往往是负面的。典型的例子包括信用卡欺诈、网络入侵和工业损害。在电子商务中,欺诈估计每年使商家损失超过 2000 亿美元;在医疗保健中,欺诈估计每年使纳税人损失 600 亿美元;对于银行来说,成本超过 120 亿美元。

未知之未知

当美国国防部长唐纳德·拉姆斯菲尔德于 2002 年 2 月 12 日举行新闻发布会,关于缺乏证据将伊拉克政府与向恐怖组织供应大规模杀伤性武器联系起来时,这立即成为许多评论的焦点。拉姆斯菲尔德陈述了以下内容(DoD News, 2012):

“关于某些事情尚未发生的报告总是让我感到很有趣,因为我们知道,有已知已知;有我们已知我们知道的事情。我们也知道有已知未知;也就是说,我们知道有一些我们不知道的事情。但还有未知未知——那些我们不知道我们不知道的事情。如果我们回顾我国和其他自由国家的历史,往往是后者更难处理。”

这句话一开始可能听起来有些令人困惑,但未知未知的概念在处理风险、国家安全局和其他情报机构的学者中得到了很好的研究。这个声明基本上意味着以下内容:

  • 已知已知:这些是众所周知的问题或问题;我们知道如何识别它们以及如何处理它们

  • 已知未知:这些是预期或可预见的问题,可以合理预测,但之前尚未发生

  • 未知未知:这些是意外和不可预见的问题,它们具有重大风险,因为基于以往的经验,它们无法被预测

在以下章节中,我们将探讨两种处理前两种已知和未知类型的基本方法:可疑模式检测处理已知已知,以及针对已知未知的异常模式检测。

可疑模式检测

第一种方法涉及一个行为库,该库编码负模式,在以下图中用红色减号表示,并识别观察到的行为是否与库中的匹配。如果一种新模式可以与负模式相匹配,那么它被认为是可疑的:

图片

例如,当你去看医生时,他会检查各种健康症状(体温、疼痛程度、受影响区域等)并将症状与已知疾病相匹配。在机器学习的术语中,医生收集属性并执行分类。

这种方法的一个优点是我们立即知道出了什么问题;例如,如果我们知道疾病,我们可以选择合适的治疗方案。

这种方法的重大缺点是它只能检测事先已知的可疑模式。如果一种模式没有被插入到负模式库中,那么我们就无法识别它。因此,这种方法适用于建模已知已知。

异常模式检测

第二种方法以相反的方式使用模式库,这意味着库只编码正模式,在以下图中用绿色加号标记。当一个观察到的行为(蓝色圆圈)无法与库相匹配时,它被认为是异常的:

图片

这种方法要求我们仅对过去所见到的内容进行建模,即正常模式。如果我们回到医生这个例子,我们最初去看医生的主要原因是因为我们感觉不舒服。我们感知到的感觉状态(例如,头痛和皮肤疼痛)与我们通常的感觉不符,因此我们决定寻求医生的帮助。我们不知道是什么疾病导致了这种状态,也不知道治疗方法,但我们能够观察到它不符合通常的状态。

这种方法的一个主要优点是它不需要我们说任何关于异常模式的内容;因此,它适合于建模已知未知和未知未知。另一方面,它并没有告诉我们具体是什么出了问题。

分析类型

已经提出了几种方法来解决这个问题。我们将异常和可疑行为检测大致分为以下三个类别:模式分析、事务分析和计划识别。在接下来的几节中,我们将快速查看一些实际应用。

模式分析

基于视觉模式(如摄像头)的异常和可疑行为检测是一个活跃的研究领域。Zhang 等人(2007 年)提出了一种从视频序列中分析人类运动的方法,它根据行走轨迹识别异常行为;Lin 等人(2009 年)描述了一个基于颜色特征、距离特征和计数特征的视频监控系统,其中使用了进化技术来测量观察相似性。该系统跟踪每个人,并通过分析他们的轨迹模式来对他们的行为进行分类。该系统从图像的不同部分提取一组视觉低级特征,并使用 SVM 进行分类,以检测攻击性、愉快、醉酒、紧张、中立和疲劳行为。

事务分析

与连续观察不同,事务分析假设离散状态/事务。一个主要的研究领域是入侵检测ID),其目的是检测针对信息系统的一般攻击。有两种类型的 ID 系统,基于签名和基于异常,它们广泛遵循前几节中描述的可疑和异常模式检测。Gyanchandani 等人(2012 年)发表了对 ID 方法的综合评论。

此外,基于可穿戴传感器的环境辅助生活应用也适合于交易分析,因为传感通常是事件驱动的。Lymberopoulos 等人(2008)提出了一种自动提取用户时空模式的方法,这些模式编码为传感器网络中的传感器激活,该传感器网络部署在他们家中。所提出的方法基于位置、时间和持续时间,能够使用 Apriori 算法提取频繁模式,并以马尔可夫链的形式编码最频繁的模式。相关工作的另一个领域包括广泛用于传统活动识别的隐马尔可夫模型(HMMs),但这些问题已经超出了本书的范围。

计划识别

计划识别关注于识别一个代理不可观察状态的机制,给定其与环境交互的观察结果(Avrahami-Zilberbrand,2009)。大多数现有研究假设以活动形式存在的离散观察。为了执行异常和可疑行为检测,计划识别算法可能使用混合方法。一个符号计划识别器用于过滤一致假设,并将它们传递给评估引擎,该引擎专注于排名。

这些是应用于各种现实场景的高级方法,旨在发现异常。在接下来的章节中,我们将深入了解用于可疑和异常模式检测的基本方法。

使用 ELKI 进行异常检测

ELKI代表用于 KDD 应用索引的环境结构,其中KDD代表数据库中的知识发现。它是一个主要用于数据挖掘的开源软件,侧重于无监督学习。它支持各种聚类分析和异常检测算法。以下是一些异常检测算法:

  • 基于距离的异常检测:这用于指定两个参数。如果一个对象与 c 的距离大于 d,且其所有数据对象的分数 p 被标记为异常。有许多算法,如DBOutlierDetectionDBOutlierScoreKNNOutlierKNNWeightOutlierParallelKNNOutlierParallelKNNWeightOutlierReferenceBasedOutlierDetection等。

  • LOF 系列方法:这种方法在特定参数上计算基于密度的局部异常因子。它包括LOFParallelLOFALOCICOFLDFLDOF等算法。

  • 基于角度的异常检测:这使用角度的方差分析,主要使用高维数据集。常见的算法包括ABODFastABODLBABOD

  • 基于聚类的异常检测:这使用 EM 聚类;如果一个对象不属于任何聚类,则被视为异常。这包括EMOutlierKMeansOutlierDetection等算法。

  • 子空间异常检测:这使用的是轴平行子空间的异常检测方法。它包括SODOutRankS1OUTRESAggrawalYuNaiveAggrawalYuEvolutionary等算法。

  • 空间异常检测:它基于从不同来源收集的基于位置的大量数据集,以及相对于邻居的极端数据点。它包括CTLuGLSBackwardSearchAlgorithmCTLuMeanMultipleAttributesCTLuMedianAlgorithmCTLuScatterplotOutlier等算法。

使用 ELKI 的一个示例

在第三章,“基本算法 – 分类、回归和聚类”中,你已经看到了如何为 ELKI 获取所需的.jar文件。我们将遵循类似的过程,如下所示:

打开命令提示符或终端,并执行以下命令:

java -jar elki-bundle-0.7.1.jar

这将提供 GUI 界面,如下面的截图所示:

图片

在 GUI 中,dbc.in和算法参数被突出显示并需要设置。我们将使用pov.csv文件作为dbc.in。此 CSV 文件可以从github.com/elki-project/elki/blob/master/data/synthetic/ABC-publication/pov.csv下载。

对于算法,选择outlier.clustering.EMOutlier,并在em.k中传递3作为值。以下截图显示了所有已填写的选项:

图片

点击“运行任务”按钮,它将处理并生成以下输出:

图片

这显示了聚类和可能的异常。

保险索赔中的欺诈检测

首先,我们将查看可疑行为检测,其目标是了解欺诈模式,这对应于已知已知建模。

数据集

我们将使用一个描述保险交易的数据库集进行工作,该数据库集在 Oracle 数据库在线文档中公开可用,网址为docs.oracle.com/cd/B28359_01/datamine.111/b28129/anomalies.htm

该数据集描述了一个未公开保险公司的车辆事故索赔。它包含 15,430 个索赔;每个索赔由 33 个属性组成,描述以下组件:

  • 客户人口统计详细信息(年龄、性别、婚姻状况等)

  • 购买的政策(政策类型、车辆类别、补充数量、代理商类型等)

  • 索赔情况(索赔的日/月/周、政策报告提交、目击者在场、事故与政策报告之间的过去天数、事故索赔等)

  • 其他客户数据(汽车数量、以前的索赔、驾驶员评分等)

  • 发现欺诈(是或否)

下面的截图显示了已加载到 Weka 中的数据库样本:

图片

现在,任务是创建一个能够识别未来可疑声明的模型。这个任务具有挑战性的地方在于只有 6% 的声明是可疑的。如果我们创建一个虚拟分类器,声称没有任何声明是可疑的,那么它在 94% 的情况下将是准确的。因此,在这个任务中,我们将使用不同的准确度指标:精确度和召回率。

让我们回顾一下第一章《应用机器学习快速入门》中的结果表,其中包含四种可能的结果,分别表示真阳性、假阳性、假阴性和真阴性:

分类为
实际 欺诈
欺诈 TP - 真阳性 FN - 假阴性
无欺诈 FP - 假阳性 TN - 真阴性

精确度和召回率定义如下:

  • 精确度等于正确发出警报的比例,如下所示:

  • 召回率等于正确识别的异常签名比例,如下所示:

  • 使用这些指标——我们的虚拟分类器得分——我们发现 Pr = 0Re = 0,因为它从未将任何实例标记为欺诈 (TP = 0)。在实践中,我们希望通过数字比较分类器;因此,我们使用 F - measure。这是一个事实上的指标,它计算精确度和召回率之间的调和平均值,如下所示:

现在,让我们继续设计一个真正的分类器。

建立可疑模式模型

为了设计一个分类器,我们可以遵循标准的有监督学习步骤,如第一章《应用机器学习快速入门》中所述。在这个菜谱中,我们将包括一些额外的步骤来处理不平衡数据集并根据精确度和召回率评估分类器。计划如下:

  1. .csv 格式加载数据。

  2. 分配类属性。

  3. 将所有属性从数值转换为名义值,以确保没有错误加载的数值。

  4. 实验 1:使用 k 折交叉验证评估模型。

  5. 实验 2:将数据集重新平衡到更平衡的类别分布,并手动执行交叉验证。

  6. 通过召回率、精确度和 f-measure 比较分类器。

首先,让我们使用 CSVLoader 类加载数据,如下所示:

String filePath = "/Users/bostjan/Dropbox/ML Java Book/book/datasets/chap07/claims.csv"; 

CSVLoader loader = new CSVLoader(); 
loader.setFieldSeparator(","); 
loader.setSource(new File(filePath)); 
Instances data = loader.getDataSet(); 

接下来,我们需要确保所有属性都是名义的。在数据导入过程中,Weka 应用一些启发式方法来猜测最可能的属性类型,即数值、名义、字符串或日期。由于启发式方法不能总是猜对类型,我们可以手动设置类型,如下所示:

NumericToNominal toNominal = new NumericToNominal(); 
toNominal.setInputFormat(data); 
data = Filter.useFilter(data, toNominal); 

在我们继续之前,我们需要指定我们将尝试预测的属性。我们可以通过调用 setClassIndex(int) 函数来实现这一点:

int CLASS_INDEX = 15; 
data.setClassIndex(CLASS_INDEX); 

接下来,我们需要移除一个描述政策编号的属性,因为它没有预测价值。我们只需应用Remove过滤器,如下所示:

Remove remove = new Remove(); 
remove.setInputFormat(data); 
remove.setOptions(new String[]{"-R", ""+POLICY_INDEX}); 
data = Filter.useFilter(data, remove); 

现在,我们准备开始建模。

基本方法

基本方法是将课程直接应用,就像在第三章,“基本算法 - 分类、回归、聚类”中所展示的那样,没有任何预处理,也不考虑数据集的具体情况。为了展示基本方法的缺点,我们将简单地使用默认参数构建一个模型,并应用 k 折交叉验证。

首先,让我们定义一些我们想要测试的分类器,如下所示:

ArrayList<Classifier>models = new ArrayList<Classifier>(); 
models.add(new J48()); 
models.add(new RandomForest()); 
models.add(new NaiveBayes()); 
models.add(new AdaBoostM1()); 
models.add(new Logistic()); 

接下来,我们需要创建一个Evaluation对象,并通过调用crossValidate(Classifier, Instances, int, Random, String[])方法执行 k 折交叉验证,提供precisionrecallfMeasure作为输出:

int FOLDS = 3; 
Evaluation eval = new Evaluation(data); 

for(Classifier model : models){ 
  eval.crossValidateModel(model, data, FOLDS,  
  new Random(1), new String[] {}); 
  System.out.println(model.getClass().getName() + "\n"+ 
    "\tRecall:    "+eval.recall(FRAUD) + "\n"+ 
    "\tPrecision: "+eval.precision(FRAUD) + "\n"+ 
    "\tF-measure: "+eval.fMeasure(FRAUD)); 
} 

评估提供了以下分数作为输出:

    weka.classifiers.trees.J48
      Recall:    0.03358613217768147
      Precision: 0.9117647058823529
      F-measure: 0.06478578892371996
    ...
    weka.classifiers.functions.Logistic
      Recall:    0.037486457204767065
      Precision: 0.2521865889212828
      F-measure: 0.06527070364082249

我们可以看到,结果并不十分令人鼓舞。召回率,即发现的欺诈在所有欺诈中的比例,仅为 1-3%,这意味着只有 1-3/100 的欺诈被检测到。另一方面,精确度,即警报的准确性,为 91%,这意味着在 10 个案例中有 9 个,当一个索赔被标记为欺诈时,模型是正确的。

数据集重平衡

由于负面示例(即欺诈实例)的数量与正面示例相比非常小,学习算法在归纳方面遇到了困难。我们可以通过提供一个正负示例比例相当的数据集来帮助他们。这可以通过数据集重平衡来实现。

Weka 有一个内置的过滤器,Resample,它使用有放回或无放回的抽样方法生成数据集的随机子样本。该过滤器还可以使分布偏向均匀的类别分布。

我们将手动实现 k 折交叉验证。首先,我们将数据集分成k个相等的部分。第k部分将用于测试,而其他部分将用于学习。为了将数据集分成部分,我们将使用StratifiedRemoveFolds过滤器,该过滤器在部分内保持类别分布,如下所示:

StratifiedRemoveFolds kFold = new StratifiedRemoveFolds(); 
kFold.setInputFormat(data); 

double measures[][] = new double[models.size()][3]; 

for(int k = 1; k <= FOLDS; k++){ 

  // Split data to test and train folds 
  kFold.setOptions(new String[]{ 
    "-N", ""+FOLDS, "-F", ""+k, "-S", "1"}); 
  Instances test = Filter.useFilter(data, kFold); 

  kFold.setOptions(new String[]{ 
    "-N", ""+FOLDS, "-F", ""+k, "-S", "1", "-V"}); 
    // select inverse "-V" 
  Instances train = Filter.useFilter(data, kFold); 

接下来,我们可以重平衡训练数据集,其中-Z参数指定要重采样的数据集百分比,而-B参数使类别分布偏向均匀分布:

Resample resample = new Resample(); 
resample.setInputFormat(data); 
resample.setOptions(new String[]{"-Z", "100", "-B", "1"}); //with 
   replacement 
Instances balancedTrain = Filter.useFilter(train, resample); 

接下来,我们可以构建分类器并执行评估:

for(ListIterator<Classifier>it = models.listIterator(); 
   it.hasNext();){ 
  Classifier model = it.next(); 
  model.buildClassifier(balancedTrain); 
  eval = new Evaluation(balancedTrain); 
  eval.evaluateModel(model, test); 

// save results for average 
  measures[it.previousIndex()][0] += eval.recall(FRAUD); 
  measures[it.previousIndex()][1] += eval.precision(FRAUD); 
 measures[it.previousIndex()][2] += eval.fMeasure(FRAUD); 
} 

最后,我们使用以下代码行计算平均值并提供最佳模型作为输出:

// calculate average 
for(int i = 0; i < models.size(); i++){ 
  measures[i][0] /= 1.0 * FOLDS; 
  measures[i][1] /= 1.0 * FOLDS; 
  measures[i][2] /= 1.0 * FOLDS; 
} 

// output results and select best model 
Classifier bestModel = null; double bestScore = -1; 
for(ListIterator<Classifier> it = models.listIterator(); 
   it.hasNext();){ 
  Classifier model = it.next(); 
  double fMeasure = measures[it.previousIndex()][2]; 
  System.out.println( 
    model.getClass().getName() + "\n"+ 
    "\tRecall:    "+measures[it.previousIndex()][0] + "\n"+ 
    "\tPrecision: "+measures[it.previousIndex()][1] + "\n"+ 
    "\tF-measure: "+fMeasure); 
  if(fMeasure > bestScore){ 
    bestScore = fMeasure; 
    bestModel = model; 

  } 
} 
System.out.println("Best model:"+bestModel.getClass().getName()); 

现在,模型的性能显著提高,如下所示:

    weka.classifiers.trees.J48
      Recall:    0.44204845100610574
      Precision: 0.14570766048577555
      F-measure: 0.21912423640160392
    ...
    weka.classifiers.functions.Logistic
      Recall:    0.7670657247204478
      Precision: 0.13507459756495374
      F-measure: 0.22969038530557626
    Best model: weka.classifiers.functions.Logistic

我们可以看到,所有模型的表现都显著提高;例如,表现最好的模型——逻辑回归,正确地发现了 76%的欺诈行为,同时产生合理数量的误报——只有 13%被标记为欺诈的索赔实际上是欺诈的。如果未检测到的欺诈比误报的调查成本高得多,那么处理更多的误报是有意义的。

总体性能很可能还有提升空间;我们可以进行属性选择和特征生成,并应用更复杂的模型学习,这些内容我们在第三章,基本算法 – 分类、回归、聚类中讨论过。

网站流量中的异常检测

在第二个示例中,我们将专注于建模前一个示例的相反情况。不是讨论典型的无欺诈案例,而是讨论系统的正常预期行为。如果某些东西无法与我们的预期模型匹配,它将被认为是异常的。

数据集

我们将使用一个由 Yahoo! Labs 发布的公开数据集,这个数据集对于讨论如何检测时间序列数据中的异常非常有用。对于 Yahoo 来说,主要用例是在检测 Yahoo 服务器上的异常流量。

尽管 Yahoo 已经宣布他们的数据是公开的,但你必须申请使用,并且批准通常需要大约 24 小时。数据集可在webscope.sandbox.yahoo.com/catalog.php?datatype=s&did=70获取。

该数据集由 Yahoo 服务的真实流量和一些合成数据组成。总共有 367 个时间序列,每个时间序列包含 741 到 1,680 个观测值,这些观测值以固定间隔记录。每个序列都写在它自己的文件中,每行一个观测值。每个序列还伴随一个第二列的指示符,如果观测值是异常,则使用 1,否则使用 0。真实数据中的异常是通过人工判断确定的,而合成数据中的异常是通过算法生成的。以下表格显示了合成时间序列数据的一个片段:

图片

在下一节中,你将学习如何将时间序列数据转换成属性表示,这样我们就可以应用机器学习算法。

时间序列数据中的异常检测

在原始的、流式的时间序列数据中检测异常需要一些数据转换。最明显的方法是选择一个时间窗口,并采样一个固定长度的时间序列。在下一步,我们希望将新的时间序列与之前收集的集合进行比较,以检测是否有异常情况发生。

比较可以使用各种技术进行,如下所示:

  • 预测最可能的后继值以及置信区间(例如,Holt-Winters 指数平滑)。如果一个新值超出了预测的置信区间,它被认为是异常的。

  • 互相关比较一个新样本与一组正样本库,并寻找精确匹配。如果没有找到匹配,它将被标记为异常。

  • 动态时间卷绕与互相关类似,但允许信号在比较中发生扭曲。

  • 将信号离散化为频带,其中每个频带对应一个字母。例如,A=[min, mean/3]B=[mean/3, mean*2/3],和C=[mean*2/3, max]将信号转换为一系列字母,例如aAABAACAABBA....这种方法减少了存储空间,并允许我们应用第十章中将要讨论的文本挖掘算法,即Mallet 文本挖掘 – 主题建模和垃圾邮件检测

  • 基于分布的方法估计特定时间窗口中值的分布。当我们观察到一个新的样本时,我们可以比较分布是否与之前观察到的分布相匹配。

这个列表绝对不是详尽的。不同的方法专注于检测不同的异常(例如,在值、频率和分布上)。在本章中,我们将关注基于分布的方法的一个版本。

使用 Encog 进行时间序列分析

我们必须从solarscience.msfc.nasa.gov/greenwch/spot_num.txt下载时间序列数据,并将文件保存在data文件夹中。在.java文件中,我们将指定文件路径,然后我们将使用以下代码块指示文件的格式:

File filename = new File("data/spot_num.txt");
CSVFormat format = new CSVFormat('.', ' ');
VersatileDataSource source = new CSVDataSource(filename, true, format);
VersatileMLDataSet data = new VersatileMLDataSet(source);
data.getNormHelper().setFormat(format);
ColumnDefinition columnSSN = data.defineSourceColumn("SSN", ColumnType.continuous);
ColumnDefinition columnDEV = data.defineSourceColumn("DEV", ColumnType.continuous);
data.analyze();
data.defineInput(columnSSN);
data.defineInput(columnDEV);
data.defineOutput(columnSSN);

现在,我们将创建窗口大小为1的前馈网络。在处理时间序列时,你应该记住它永远不应该被打乱。我们将保留一些数据用于验证。我们将使用以下代码行来完成:

EncogModel model = new EncogModel(data);
model.selectMethod(data, MLMethodFactory.TYPE_FEEDFORWARD);

model.setReport(new ConsoleStatusReportable());
data.normalize();

// Set time series.
data.setLeadWindowSize(1);
data.setLagWindowSize(WINDOW_SIZE);
model.holdBackValidation(0.3, false, 1001);
model.selectTrainingType(data);

下一步是使用以下行运行带有五折交叉验证的训练:

MLRegression bestMethod = (MLRegression) model.crossvalidate(5, false);

现在,是时候展示错误和最终模型了。我们将通过以下代码行来完成:

System.out.println("Training error: " + model.calculateError(bestMethod, model.getTrainingDataset()));
System.out.println("Validation error: " + model.calculateError(bestMethod, model.getValidationDataset()));

NormalizationHelper helper = data.getNormHelper();
System.out.println(helper.toString());

// Display the final model.
System.out.println("Final model: " + bestMethod);

输出将类似于以下截图:

图片

现在,我们将使用以下代码块测试模型:

while (csv.next() && stopAfter > 0) {
                StringBuilder result = new StringBuilder();

                line[0] = csv.get(2);// ssn
                line[1] = csv.get(3);// dev
                helper.normalizeInputVector(line, slice, false);

                if (window.isReady()) {
                    window.copyWindow(input.getData(), 0);
                    String correct = csv.get(2); // trying to predict SSN.
                    MLData output = bestMethod.compute(input);
                    String predicted = helper
                            .denormalizeOutputVectorToString(output)[0];

                    result.append(Arrays.toString(line));
                    result.append(" -> predicted: ");
                    result.append(predicted);
                    result.append("(correct: ");
                    result.append(correct);
                    result.append(")");

                    System.out.println(result.toString());
                }

                window.add(slice);

                stopAfter--;
            }

输出将类似于以下截图:

图片

基于直方图的异常检测

在基于直方图的异常检测中,我们根据选定的时间窗口分割信号,如图所示。

对于每个窗口,我们计算直方图;也就是说,对于选定的桶数,我们计算每个桶中有多少值。直方图捕捉了选定时间窗口中值的分布情况,如图中所示。

直方图可以随后直接作为实例展示,其中每个桶对应一个属性。此外,我们可以通过应用降维技术,如主成分分析PCA),来减少属性的数量,这允许我们在图中可视化降维后的直方图,如图表右下角所示,其中每个点对应一个直方图。

在我们的例子中,想法是观察几天内的网站流量,然后创建直方图;例如,四小时的时间窗口,以建立一个积极行为的库。如果一个新时间窗口的直方图无法与积极库匹配,我们可以将其标记为异常:

图片

为了比较一个新直方图与一组现有直方图,我们将使用基于密度的 k 近邻算法,局部异常因子LOF)(Breunig 等,2000)。该算法能够处理具有不同密度的簇,如下面的图所示。例如,右上角的簇较大且分布广泛,与左下角的簇相比,后者较小且密度更高:

图片

让我们开始吧!

加载数据

在第一步,我们需要将数据从文本文件加载到 Java 对象中。这些文件存储在一个文件夹中,每个文件包含一个时间序列,每行一个值。我们将它们加载到一个Double列表中,如下所示:

String filePath = "chap07/ydata/A1Benchmark/real"; 
List<List<Double>> rawData = new ArrayList<List<Double>>(); 

我们需要minmax值来进行直方图归一化;因此,让我们在这个数据传递中收集它们:

double max = Double.MIN_VALUE; 
double min = Double.MAX_VALUE; 

for(int i = 1; i<= 67; i++){ 
  List<Double> sample = new ArrayList<Double>(); 
  BufferedReader reader = new BufferedReader(new 
     FileReader(filePath+i+".csv")); 

  boolean isAnomaly = false; 
  reader.readLine(); 
  while(reader.ready()){ 
    String line[] = reader.readLine().split(","); 
    double value = Double.parseDouble(line[1]); 
    sample.add(value); 

    max = Math.max(max, value); 
    min = Double.min(min, value); 

    if(line[2] == "1") 
      isAnomaly = true; 

  } 
  System.out.println(isAnomaly); 
  reader.close(); 

  rawData.add(sample); 
} 

数据已经加载。接下来,让我们继续到直方图部分。

创建直方图

我们将使用WIN_SIZE宽度创建一个选定时间窗口的直方图。

直方图将包含HIST_BINS值桶。由双列表组成的直方图将存储在数组列表中:

int WIN_SIZE = 500; 
int HIST_BINS = 20; 
int current = 0; 

List<double[]> dataHist = new ArrayList<double[]>(); 
for(List<Double> sample : rawData){ 
  double[] histogram = new double[HIST_BINS]; 
  for(double value : sample){ 
    int bin = toBin(normalize(value, min, max), HIST_BINS); 
    histogram[bin]++; 
    current++; 
    if(current == WIN_SIZE){ 
      current = 0; 
      dataHist.add(histogram); 
      histogram = new double[HIST_BINS]; 
    } 
  } 
  dataHist.add(histogram); 
} 

直方图现在已经完成。最后一步是将它们转换为 Weka 的Instance对象。每个直方图值将对应一个 Weka 属性,如下所示:

ArrayList<Attribute> attributes = new ArrayList<Attribute>(); 
for(int i = 0; i<HIST_BINS; i++){ 
  attributes.add(new Attribute("Hist-"+i)); 
} 
Instances dataset = new Instances("My dataset", attributes, 
   dataHist.size()); 
for(double[] histogram: dataHist){ 
  dataset.add(new Instance(1.0, histogram)); 
} 

数据集现在已经加载,并准备好插入到异常检测算法中。

基于密度的 k 近邻

为了演示 LOF 如何计算分数,我们将首先使用testCV(int, int)函数将数据集分为训练集和测试集。第一个参数指定了折数,而第二个参数指定了要返回的折:

// split data to train and test 
Instances trainData = dataset.testCV(2, 0); 
Instances testData = dataset.testCV(2, 1); 

LOF 算法不是 Weka 默认分布的一部分,但可以通过 Weka 的包管理器在weka.sourceforge.net/packageMetaData/localOutlierFactor/index.html下载。

LOF 算法有两个实现接口:作为一个无监督过滤器,计算 LOF 值(已知未知),以及作为一个有监督的 k 最近邻分类器(已知已知)。在我们的情况下,我们想要计算异常因子,因此我们将使用无监督过滤器接口:

import weka.filters.unsupervised.attribute.LOF; 

过滤器初始化的方式与常规过滤器相同。我们可以使用-min-max参数指定邻居的数量(例如,k=3)。LOF允许我们指定两个不同的k参数,这些参数在内部用作上限和下限,以找到最小或最大的lof值:

LOF lof = new LOF(); 
lof.setInputFormat(trainData); 
lof.setOptions(new String[]{"-min", "3", "-max", "3"}); 

接下来,我们将训练实例加载到作为正例库的过滤器中。加载完成后,我们将调用batchFinished()方法来初始化内部计算:

for(Instance inst : trainData){ 
  lof.input(inst); 
} 
lof.batchFinished(); 

最后,我们可以将过滤器应用于测试数据。Filter() 函数将处理实例并在末尾附加一个额外的属性,包含 LOF 分数。我们可以在控制台中简单地提供分数作为输出:

Instances testDataLofScore = Filter.useFilter(testData, lof); 

for(Instance inst : testDataLofScore){ 
  System.out.println(inst.value(inst.numAttributes()-1)); 
} 

前几个测试实例的 LOF 分数如下:

    1.306740014927325
    1.318239332210458
    1.0294812291949587
    1.1715039094530768

要理解 LOF 值,我们需要了解 LOF 算法的一些背景知识。它比较实例的密度与其最近邻的密度。这两个分数相除,产生 LOF 分数。大约为 1 的 LOF 分数表示密度大致相等,而更高的 LOF 值表示实例的密度显著低于其邻居的密度。在这种情况下,实例可以被标记为异常。

摘要

在本章中,我们探讨了检测异常和可疑模式。我们讨论了两种基本方法,重点关注库编码,即正模式或负模式。接下来,我们处理了两个真实数据集,并讨论了如何处理不平衡的类别分布以及如何在时间序列数据上执行异常检测。

在下一章中,我们将更深入地探讨模式以及更高级的基于模式构建分类器的方法,并讨论如何使用深度学习自动为图像分配标签。

第八章:使用 Deeplearning4j 进行图像识别

图像在 Web 服务、社交网络和在线商店中无处不在。与人类相比,计算机在理解图像及其所代表的内容方面有很大困难。在本章中,我们将首先探讨教会计算机如何理解图像的挑战,然后重点介绍基于深度学习的方法。我们将探讨配置深度学习模型所需的高级理论,并讨论如何使用 Java 库 Deeplearning4j 实现一个能够通过分类图像的模型。

本章将涵盖以下主题:

  • 介绍图像识别

  • 讨论深度学习基础

  • 构建图像识别模型

介绍图像识别

图像识别的一个典型目标是检测和识别数字图像中的对象。图像识别应用于工厂自动化以监控产品质量;监控系统以识别潜在风险活动,如移动的人或车辆;安全应用通过指纹、虹膜或面部特征提供生物识别;自动驾驶汽车以重建道路和环境条件;等等。

数字图像不是以基于属性的描述方式呈现的;相反,它们被编码为不同通道中的颜色量,例如,黑白和红绿蓝通道。学习目标是识别与特定对象相关的模式。图像识别的传统方法包括将图像转换为不同的形式,例如,识别物体角落、边缘、同色块和基本形状。然后使用这些模式来训练一个学习者区分对象。以下是一些传统算法的显著例子:

  • 边缘检测找到图像中对象的边界

  • 角点检测识别两条边缘或其他有趣点(如线端、曲率极大值或极小值等)的交点

  • 块检测识别与周围区域在属性(如亮度或颜色)上不同的区域

  • 岭谷检测使用平滑函数在图像中识别额外的有趣点

  • 尺度不变特征变换SIFT)是一种鲁棒的算法,即使对象的尺度或方向与数据库中的代表性样本不同,也能匹配对象

  • Hough 变换识别图像中的特定模式

一种更近期的方法是基于深度学习。深度学习是一种神经网络形式,它模仿大脑处理信息的方式。深度学习的主要优势在于可以设计出能够自动提取相关模式的神经网络,这些模式反过来又可以用来训练学习器。随着神经网络技术的最新进展,图像识别的准确性得到了显著提升。例如,ImageNet 挑战赛,其中参赛者获得了来自 1,000 个不同物体类别的超过 1,200 万张图片,报告称,最佳算法的错误率从 2010 年的 28%(使用支持向量机SVM))降低到了 2014 年的仅 7%(使用深度神经网络)。

在本章中,我们将快速浏览神经网络,从基本构建块感知器开始,逐渐引入更复杂的结构。

神经网络

最早期的神经网络,在六十年代被引入,其灵感来源于生物神经网络。神经网络的想法是映射生物神经系统,即大脑如何处理信息。它由相互连接的神经元层组成,共同工作。在计算机术语中,它们也被称为人工神经网络ANN)。使用计算机,需要训练来使这个模型学习,就像人类大脑一样。大脑中的神经元在接收到来自附近相互连接的神经元的信号时会被激活,对人工神经网络也是如此。神经网络技术的最新进展已经证明,深度神经网络非常适合模式识别任务,因为它们能够自动提取有趣的特征并学习其背后的表示。在本节中,我们将回顾从单个感知器到深度网络的基本结构和组件。

感知器

感知器是基本神经网络构建块之一,也是最早的监督算法之一。它被定义为特征的总和,这些特征乘以相应的权重和一个偏差。当接收到输入信号时,它将这些信号与分配的权重相乘。这些权重为每个传入的信号或输入定义,并在学习阶段持续调整。权重的调整取决于最后结果的误差。将所有输入与一些称为偏差的偏移值相乘后,所有输入都加在一起。偏差的值也由权重调整。因此,它从随机的权重和偏差开始,并在每次迭代中调整权重和偏差,以便下一个结果向期望的输出移动。最后,最终结果被转换成输出信号。将所有这些加在一起的功能称为求和传递函数,并将其输入到激活函数中。如果二元步激活函数达到阈值,则输出为 1,否则为 0,这为我们提供了一个二元分类器。以下图显示了示意图:

图片

训练感知器涉及一个相当简单的学习算法,该算法计算计算输出值与正确训练输出值之间的误差,并使用此误差来创建对权重的调整,从而实现一种梯度下降的形式。此算法通常称为delta 规则

单层感知器并不非常先进,无法使用它来模拟非线性可分函数,如 XOR。为了解决这个问题,引入了一种具有多个感知器的结构,称为多层感知器,也称为前馈神经网络

前馈神经网络

前馈神经网络是一种由多个感知器组成的 ANN,这些感知器被组织成层,如下面的图所示:输入层、输出层和一层或多层隐藏层。隐藏层与外界没有任何联系,因此得名。每个层感知器,也称为神经元,与下一层的感知器有直接连接,而两个神经元之间的连接则携带一个与感知器权重相似的权重。因此,同一层的所有感知器都与下一层的感知器相连,信息被正向传递到下一层。此图显示了一个具有四个单元的输入层的网络,对应于长度为4的特征向量大小,一个四个单元的隐藏层和一个两个单元的输出层,其中每个单元对应一个类别值:

图片

前馈神经网络通过寻找输入和输出值之间的关系来学习,这些值被多次输入到网络中。训练多层网络最流行的方法是反向传播。在反向传播中,计算出的输出值与相同方式下的正确值进行比较,就像在 delta 规则中一样。然后,通过各种技术将错误反馈到网络中,调整每个连接的权重,以减少错误的值。错误是通过网络输出值与原始输出值之间的平方差来计算的。错误表示我们离原始输出值的距离。这个过程在足够多的训练周期中重复进行,直到错误低于某个阈值。

前馈神经网络可以有多于一个的隐藏层,其中每个额外的隐藏层在先前的层之上构建一个新的抽象。这通常会导致更精确的模型;然而,增加隐藏层的数量会导致两个已知问题:

  • 梯度消失问题:随着隐藏层的增加,使用反向传播的训练越来越不适用于将信息传递到前层,导致这些层训练速度非常慢

  • 过拟合:模型对训练数据拟合得太好,在真实示例上的表现不佳

让我们看看一些其他网络结构,它们解决了这些问题。

自动编码器

自动编码器是一种前馈神经网络,旨在学习如何压缩原始数据集。其目标是复制输入到输出。因此,我们不会将特征映射到输入层,将标签映射到输出层,而是将特征映射到输入和输出层。隐藏层中的单元数量通常不同于输入层中的单元数量,这迫使网络增加或减少原始特征的数量。这样,网络将学习重要的特征,同时有效地应用降维。

下面的图中展示了网络的一个示例。三个单元的输入层首先扩展为四个单元的层,然后压缩为单个单元的层。网络的另一侧将单个层单元恢复到四个单元层,然后回到原始的三个输入层:

图片

一旦网络被训练,我们可以从左侧提取图像特征,就像使用传统的图像处理一样。它由编码器和解码器组成,其中编码器的工作是创建或隐藏一个或多个层,以捕捉输入的本质,而解码器则从这些层中重建输入。

自动编码器也可以组合成堆叠自动编码器,如下面的图所示。首先,我们将讨论基本自动编码器中的隐藏层,如之前所述。然后,我们将重复使用学习到的隐藏层(绿色圆圈)并重复该过程,这实际上学习了一个更抽象的表示。我们可以重复此过程多次,将原始特征转换为越来越低的维度。最后,我们将所有隐藏层堆叠成一个常规的前馈网络,如图表右上角所示:

限制性玻尔兹曼机

限制性玻尔兹曼机RBM)是一种无向神经网络,也称为生成随机网络GSNs),可以学习其输入集上的概率分布。正如其名所示,它们起源于 20 世纪 80 年代引入的玻尔兹曼机,这是一种循环神经网络。在玻尔兹曼机中,每个节点或神经元都与所有其他节点相连,这使得当节点数量增加时难以处理。限制性意味着神经元必须形成两个完全连接的层,一个输入层和一个隐藏层,如下面的图所示:

与前馈网络不同,可见层和隐藏层之间的连接是无向的,因此值可以在可见到隐藏和隐藏到可见的方向上传播。

RBMs 的训练基于对比散度算法,该算法使用梯度下降过程,类似于反向传播,来更新权重,并在马尔可夫链上应用吉布斯采样来估计梯度,即如何改变权重的方向。

RBMs 也可以堆叠起来,形成一个称为深度信念网络DBNs)的类别。在这种情况下,RBM 的隐藏层作为 RBM 层的可见层,如下面的图所示:

在这种情况下,训练是增量式的:层与层地训练。

深度卷积网络

最近在图像识别基准测试中取得非常好的结果的网络结构是卷积神经网络CNN)或 ConvNet。CNN 是一种结构化的前馈神经网络,其结构模仿了视觉皮层的功能,利用输入图像的 2D 结构,即表现出空间局部相关性的模式。它基于大脑如何回忆或记住图像的基本原理。作为人类,我们根据特征来记住图像。给定特征后,我们的大脑将开始形成图像本身。在计算机中,考虑以下图表,它显示了如何检测特征:

同样地,可以从图像中检测到许多特征,如下图所示:

CNN 由多个卷积和子采样层组成,可选地后面跟着全连接层。以下图示展示了这一示例。输入层读取图像中的所有像素,然后我们应用多个滤波器。在以下图中,应用了四个不同的滤波器。每个滤波器都应用于原始图像;例如,一个 6 x 6 滤波器的一个像素是通过计算 6 x 6 输入像素的加权总和以及相应的 6 x 6 权重来计算的。这有效地引入了类似于标准图像处理的滤波器,如平滑、相关性、边缘检测等。生成的图像称为特征图。在以下图的示例中,我们有四个特征图,每个滤波器一个。

下一个层是子采样层,它减小了输入的大小。每个特征图通常通过在连续区域(对于大图像可达 2 x 2 到 5 x 5)上的平均或最大池化进行子采样。例如,如果特征图大小为 16 x 16,子采样区域为 2 x 2,则减小后的特征图大小为 8 x 8,其中 4 个像素(一个 2 x 2 的正方形)通过计算最大值、最小值、平均值或其他函数合并成一个单独的像素:

网络可能包含几个连续的卷积和子采样层,如前图所示。特定的特征图连接到下一个减小/卷积的特征图,而同一层的特征图之间不相互连接。

在最后一个子采样或卷积层之后,通常有一个全连接层,与标准多层神经网络中的层相同,它表示目标数据。

CNN 的训练使用了一种修改后的反向传播算法,该算法考虑了子采样层,并根据该滤波器应用的所有值更新卷积滤波器权重。

一些好的 CNN 设计可以在 ImageNet 竞赛结果页面上找到:www.image-net.org/。一个例子是A. Krizhevsky 等人ImageNet 分类与深度卷积神经网络论文中描述的A. Krizhevsky

这就结束了我们对主要神经网络结构的回顾。在下一节中,我们将继续实际实现。

图像分类

在本节中,我们将讨论如何使用 Deeplearning4j 库实现一些神经网络结构。让我们开始吧。

Deeplearning4j

如我们在第二章“Java 机器学习库和平台”中讨论的那样,Deeplearning4j 是一个开源的、基于 Java 和 Scala 的分布式深度学习项目。Deeplearning4j 依赖于 Spark 和 Hadoop 进行 MapReduce,并行训练模型,并在中心模型中迭代平均它们产生的参数。详细的库总结在第二章“Java 机器学习库和平台”中给出。

获取 DL4J

获取 Deeplearning4j 最方便的方式是通过 Maven 仓库:

  1. 启动一个新的 Eclipse 项目,选择 Maven 项目,如下面的截图所示:

图片

  1. 打开pom.xml文件,并在<dependencies>部分添加以下依赖项:
<dependency> 
    <groupId>org.deeplearning4j</groupId> 
    <artifactId>deeplearning4j-nlp</artifactId> 
   <version>${dl4j.version}</version> 
</dependency> 

<dependency> 
    <groupId>org.deeplearning4j</groupId> 
    <artifactId>deeplearning4j-core</artifactId> 
    <version>${dl4j.version}</version> 
</dependency> 
  1. 最后,右键单击项目,选择 Maven,然后选择更新项目。

MNIST 数据集

最著名的数据库集之一是 MNIST 数据集,它由手写数字组成,如下面的图像所示。该数据集包括 60,000 个训练图像和 10,000 个测试图像:

图片

该数据集通常用于图像识别问题中的算法基准测试。记录的最坏错误率是 12%,没有预处理,在一个层神经网络中使用 SVM。目前,截至 2016 年,最低错误率仅为 0.21%,使用DropConnect神经网络,其次是深度卷积网络,错误率为 0.23%,以及深度前馈网络,错误率为 0.35%。

现在,让我们看看如何加载数据集。

加载数据

Deeplearning4j 自带 MNIST 数据集加载器。加载器初始化为DataSetIterator。首先,让我们导入DataSetIterator类以及impl包中所有支持的数据库集,例如 iris、MNIST 等:

import org.deeplearning4j.datasets.iterator.DataSetIterator; 
import org.deeplearning4j.datasets.iterator.impl.*; 

接下来,我们将定义一些常量,例如图像由 28 x 28 像素组成,有 10 个目标类别和 60,000 个样本。我们将初始化一个新的MnistDataSetIterator类,该类将下载数据集及其标签。参数是迭代批处理大小、示例总数以及数据集是否应该二值化:

int numRows = 28; 
int numColumns = 28; 
int outputNum = 10;
int numSamples = 60000;
int batchSize = 100;
int iterations = 10;
int seed = 123;
DataSetIterator iter = new MnistDataSetIterator(batchSize, 
numSamples,true);  

拥有一个已经实现的数据导入器非常方便,但它不会在你的数据上工作。让我们快速看一下它的实现方式以及需要修改什么以支持你的数据集。如果你急于开始实现神经网络,你可以安全地跳过本节的其余部分,并在需要导入自己的数据时返回。

要加载自定义数据,你需要实现两个类:DataSetIterator,它包含有关数据集的所有信息,以及BaseDataFetcher,它实际上从文件、数据库或网络中提取数据。示例实现可在 GitHub 上找到,地址为github.com/deeplearning4j/deeplearning4j/tree/master/deeplearning4j-core/src/main/java/org/deeplearning4j/datasets/iterator/impl

另一个选项是使用由同一作者开发的Canova库,该库的文档位于deeplearning4j.org/canovadoc/

构建模型

在本节中,我们将讨论如何构建实际的神经网络模型。我们将从一个基本的单层神经网络开始,以建立基准并讨论基本操作。稍后,我们将使用 DBN 和多层卷积网络来改进这个初始结果。

构建单层回归模型

让我们从基于 softmax 激活函数的单层回归模型开始构建,如下面的图所示。由于我们只有一个层,神经网络的输入将是所有图像像素,即 28 x 28 = 748个神经元。输出神经元的数量是10,每个数字一个。网络层是完全连接的,如下面的图所示:

图片

神经网络通过NeuralNetConfiguration.Builder()对象定义,如下所示:

MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder() 

我们将定义梯度搜索的参数,以便使用共轭梯度优化算法进行迭代。momentum参数决定了优化算法收敛到局部最优的速度。momentum值越高,训练速度越快;但过高的速度可能会降低模型的准确性:

.seed(seed) 
.gradientNormalization(GradientNormalization.ClipElementWiseAbsolu
   teValue) 
   .gradientNormalizationThreshold(1.0) 
   .iterations(iterations) 
   .momentum(0.5) 
   .momentumAfter(Collections.singletonMap(3, 0.9)) 
   .optimizationAlgo(OptimizationAlgorithm.CONJUGATE_GRADIENT) 

接下来,我们将指定网络有一层,并定义错误函数NEGATIVELOGLIKELIHOOD,内部感知器激活函数softmax以及与总输入和输出层相对应的数量。

图像像素和目标变量的数量,如下面的代码块所示:

.list(1) 
.layer(0, new  
OutputLayer.Builder(LossFunction.NEGATIVELOGLIKELIHOOD) 
.activation("softmax") 
.nIn(numRows*numColumns).nOut(outputNum).build()) 

最后,我们将网络设置为pretrain,禁用反向传播,并实际构建未训练的网络结构:

   .pretrain(true).backprop(false) 
   .build(); 

一旦定义了网络结构,我们就可以使用它来初始化一个新的MultiLayerNetwork,如下所示:

MultiLayerNetwork model = new MultiLayerNetwork(conf); 
model.init(); 

接下来,我们将通过调用setListeners方法将模型指向训练数据,如下所示:

model.setListeners(Collections.singletonList((IterationListener) 
   new ScoreIterationListener(listenerFreq))); 

我们还将调用fit(int)方法来触发端到端网络训练:

model.fit(iter);  

为了评估模型,我们将初始化一个新的Evaluation对象,该对象将存储批处理结果:

Evaluation eval = new Evaluation(outputNum); 

然后,我们可以按批处理迭代数据集,以保持合理的内存消耗并存储在eval对象中的结果:

DataSetIterator testIter = new MnistDataSetIterator(100,10000); 
while(testIter.hasNext()) { 
    DataSet testMnist = testIter.next(); 
    INDArray predict2 =  
    model.output(testMnist.getFeatureMatrix()); 
    eval.eval(testMnist.getLabels(), predict2); 
} 

最后,我们可以通过调用stats()函数来获取结果:

log.info(eval.stats()); 

一个基本的单层模型达到了以下准确率:

    Accuracy:  0.8945 
    Precision: 0.8985
    Recall:    0.8922
    F1 Score:  0.8953

在 MNIST 数据集上获得 89.22%的准确率,即 10.88%的错误率,相当糟糕。我们将通过从简单的一层网络到使用受限玻尔兹曼机和多层卷积网络的复杂深度信念网络来改进这一点。

构建深度信念网络

在本节中,我们将基于 RBM 构建一个深度信念网络(DBN),如下面的图所示。该网络由四层组成。第一层将748个输入减少到500个神经元,然后到250个,接着到200个,最后到最后的10个目标值:

图片

由于代码与上一个示例相同,让我们看看如何配置这样一个网络:

MultiLayerConfiguration conf = new 
   NeuralNetConfiguration.Builder() 

我们将定义梯度优化算法,如下面的代码所示:

    .seed(seed) 
    .gradientNormalization( 
    GradientNormalization.ClipElementWiseAbsoluteValue) 
    .gradientNormalizationThreshold(1.0) 
    .iterations(iterations) 
    .momentum(0.5) 
    .momentumAfter(Collections.singletonMap(3, 0.9)) 
    .optimizationAlgo(OptimizationAlgorithm.CONJUGATE_GRADIENT) 

我们还将指定我们的网络将有四层:

   .list(4) 

第一层的输入将是748个神经元,输出将是500个神经元。我们将使用均方根误差交叉熵,并使用 Xavier 算法初始化权重,该算法会自动根据输入和输出神经元的数量确定初始化的规模,如下所示:

.layer(0, new RBM.Builder() 
.nIn(numRows*numColumns) 
.nOut(500)          
.weightInit(WeightInit.XAVIER) 
.lossFunction(LossFunction.RMSE_XENT) 
.visibleUnit(RBM.VisibleUnit.BINARY) 
.hiddenUnit(RBM.HiddenUnit.BINARY) 
.build()) 

接下来的两层将具有相同的参数,除了输入和输出神经元的数量:

.layer(1, new RBM.Builder() 
.nIn(500) 
.nOut(250) 
.weightInit(WeightInit.XAVIER) 
.lossFunction(LossFunction.RMSE_XENT) 
.visibleUnit(RBM.VisibleUnit.BINARY) 
.hiddenUnit(RBM.HiddenUnit.BINARY) 
.build()) 
.layer(2, new RBM.Builder() 
.nIn(250) 
.nOut(200) 
.weightInit(WeightInit.XAVIER) 
.lossFunction(LossFunction.RMSE_XENT) 
.visibleUnit(RBM.VisibleUnit.BINARY) 
.hiddenUnit(RBM.HiddenUnit.BINARY) 
.build()) 

现在,最后一层将映射神经元到输出,我们将使用softmax激活函数,如下所示:

.layer(3, new OutputLayer.Builder() 
.nIn(200) 
.nOut(outputNum) 
.lossFunction(LossFunction.NEGATIVELOGLIKELIHOOD) 
.activation("softmax") 
.build()) 
.pretrain(true).backprop(false) 
.build(); 

剩余的训练和评估与单层网络示例相同。请注意,训练深度网络可能比单层网络花费的时间显著更多。准确率应约为 93%。

现在,让我们看看另一个深度网络。

构建多层卷积网络

在这个最后的例子中,我们将讨论如何构建一个卷积网络,如下面的图所示。该网络将包含七层。首先,我们将重复两对卷积和子采样层与最大池化。然后,最后一个子采样层连接到一个由 120 个神经元、84 个神经元和最后三层分别有 10 个神经元的密集连接前馈神经元网络。这样一个网络有效地形成了完整的图像识别流程,其中前四层对应于特征提取,最后三层对应于学习模型:

图片

网络配置初始化与我们之前所做的一样:

MultiLayerConfiguration.Builder conf = new 
   NeuralNetConfiguration.Builder() 

我们将指定梯度下降算法及其参数,如下所示:

.seed(seed) 
.iterations(iterations) 
.activation("sigmoid") 
.weightInit(WeightInit.DISTRIBUTION) 
.dist(new NormalDistribution(0.0, 0.01)) 
.learningRate(1e-3) 
.learningRateScoreBasedDecayRate(1e-1) 
.optimizationAlgo( 
OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT) 

我们还将指定七个网络层,如下所示:

.list(7) 

第一卷积层的输入是完整的图像,输出是六个特征图。卷积层将应用一个 5 x 5 的滤波器,结果将存储在一个 1 x 1 的单元中:

.layer(0, new ConvolutionLayer.Builder( 
    new int[]{5, 5}, new int[]{1, 1}) 
    .name("cnn1") 
    .nIn(numRows*numColumns) 
    .nOut(6) 
    .build()) 

第二层是一个子采样层,它将取一个 2 x 2 的区域,并将最大结果存储在一个 2 x 2 的元素中:

.layer(1, new SubsamplingLayer.Builder( 
SubsamplingLayer.PoolingType.MAX,  
new int[]{2, 2}, new int[]{2, 2}) 
.name("maxpool1") 
.build()) 

接下来的两层将重复前两层:

.layer(2, new ConvolutionLayer.Builder(new int[]{5, 5}, new 
   int[]{1, 1}) 
    .name("cnn2") 
    .nOut(16) 
    .biasInit(1) 
    .build()) 
.layer(3, new SubsamplingLayer.Builder
   (SubsamplingLayer.PoolingType.MAX, new 
   int[]{2, 2}, new int[]{2, 2}) 
    .name("maxpool2") 
    .build()) 

现在,我们将子采样层的输出连接到一个由120个神经元组成的密集前馈网络,然后通过另一个层连接到84个神经元,如下所示:

.layer(4, new DenseLayer.Builder() 
    .name("ffn1") 
    .nOut(120) 
    .build()) 
.layer(5, new DenseLayer.Builder() 
    .name("ffn2") 
    .nOut(84) 
    .build()) 

最后一层将84个神经元与10个输出神经元连接:

.layer(6, new OutputLayer.Builder
   (LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD) 
    .name("output") 
    .nOut(outputNum) 
    .activation("softmax") // radial basis function required 
    .build()) 
.backprop(true) 
.pretrain(false) 
.cnnInputSize(numRows,numColumns,1); 

为了训练这个结构,我们可以重用我们在前两个示例中开发的代码。再次强调,训练可能需要一些时间。网络的准确率应该在大约 98%左右。

由于模型训练在很大程度上依赖于线性代数,因此可以通过使用图形处理单元GPU)来显著加快训练速度。由于撰写本书时,GPU 后端正在进行重写,请查阅最新的文档deeplearning4j.org/documentation

正如我们在不同示例中看到的那样,越来越复杂的神经网络使我们能够自动提取相关特征,从而完全避免传统的图像处理。然而,我们为此付出的代价是处理时间的增加以及大量的学习示例来使这种方法有效。

摘要

在本章中,我们讨论了如何通过覆盖深度学习的基本原理并讨论如何使用 Deeplearning4j 库来实现它们,来识别图像中的模式以区分不同的类别。我们首先刷新了基本神经网络结构,并讨论了如何实现它们来解决手写数字识别问题。

在下一章中,我们将进一步探讨模式;然而,我们将处理具有时间依赖性的模式,这些模式可以在传感器数据中找到。

第九章:使用移动电话传感器进行活动识别

虽然上一章侧重于图像中的模式识别,但本章全部关于在传感器数据中识别模式,这与图像不同,具有时间依赖性。我们将讨论如何使用移动电话惯性传感器识别细粒度的日常活动,如行走、坐着和跑步。本章还提供了相关研究的参考文献,并强调了活动识别社区中的最佳实践。

本章将涵盖以下主题:

  • 介绍活动识别,涵盖移动电话传感器和活动识别流程

  • 从移动设备收集传感器数据

  • 讨论活动分类和模型评估

  • 部署活动识别模型

介绍活动识别

活动识别是行为分析的基础步骤,涉及健康生活方式、健身追踪、远程协助、安全应用、老年护理等。活动识别将来自加速度计、陀螺仪、压力传感器和 GPS 位置等传感器的低级传感器数据转换为对行为原语的高级描述。

在大多数情况下,这些是基本活动,例如以下图中所示的行走、坐着、躺着、跳跃等,或者它们可能是更复杂的行为,如去上班、准备早餐和购物等:

图片

在本章中,我们将讨论如何将活动识别功能添加到移动应用程序中。我们首先将探讨活动识别问题是什么样的,我们需要收集哪些类型的数据,主要挑战是什么,以及如何解决这些问题。

之后,我们将通过一个示例来了解如何在 Android 应用程序中实际实现活动识别,包括数据收集、数据转换和构建分类器。

让我们开始吧!

移动电话传感器

让我们先回顾一下有哪些类型的移动电话传感器以及它们报告的内容。现在大多数智能设备都配备了几个内置传感器,这些传感器可以测量运动、位置、朝向和环境条件。由于传感器提供高精度、频率和准确性的测量,因此可以重建复杂用户运动、手势和动作。传感器常被集成到各种应用中;例如,陀螺仪读数用于在游戏中控制物体,GPS 数据用于定位用户,加速度计数据用于推断用户正在执行的活动,例如骑自行车、跑步或行走。

下图展示了传感器能够检测到的几种交互类型示例:

图片

移动电话传感器可以分为以下三个广泛的类别:

  • 运动传感器:此传感器测量沿三个垂直轴的加速度和旋转力。此类传感器包括加速度计、重力传感器和陀螺仪。

  • 环境传感器:此传感器测量各种环境参数,如光照、空气温度、压力和湿度。此类包括气压计、光度计和温度计。

  • 位置传感器:此传感器测量设备的物理位置。此类包括方向传感器和磁力计。

不同移动平台的更详细描述可在以下链接中找到:

在本章中,我们将仅使用 Android 的传感器框架。

活动识别流程

与我们在前几章中看到的不同,对多维时间序列传感器数据进行分类本质上比分类传统名义数据更复杂。首先,每个观测值在时间上都与前一个和后一个观测值相关联,这使得仅对一组观测值进行直接分类变得非常困难。其次,传感器在不同时间点获得的数据是随机的,即由于传感器噪声、环境干扰等因素的影响,是不可预测的。此外,一项活动可以由以不同方式执行的各种子活动组成,每个人执行活动的方式也略有不同,这导致类内差异很大。最后,所有这些原因使得活动识别模型不够精确,导致新数据经常被错误分类。活动识别分类器的一个高度期望的特性是确保识别的活动序列的连续性和一致性。

为了应对这些挑战,活动识别被应用于以下流程,如图所示:

图片

在第一步,我们尽可能地减少噪声,例如,通过降低传感器采样率、移除异常值、应用高通或低通滤波器等。在下一阶段,我们构建一个特征向量。例如,我们通过应用离散傅里叶变换DFT)将传感器数据从时域转换为频域。DFT 是一种将一系列样本作为输入并返回按频率顺序排列的正弦系数列表的方法。它们代表了原始样本列表中存在的频率组合。

Pete Bevelacqua 在 www.thefouriertransform.com/ 写了一篇关于傅里叶变换的温和介绍。如果你想要获取关于傅里叶变换的更技术性和理论性的背景知识,可以查看罗伯特·加勒格尔和郑丽中在麻省理工学院开放课程中的第八和第九讲:theopenacademy.com/content/principles-digital-communication

接下来,基于特征向量和训练数据集,我们可以构建一个活动识别模型,将原子动作分配给每个观察结果。因此,对于每个新的传感器读数,模型将输出最可能的动作标签。然而,模型会犯错误。因此,最后一个阶段通过移除在现实中不可能发生的转换来平滑活动之间的过渡;例如,活动从躺到站再到躺的转换在不到半秒内发生在物理上是不可能的,因此这种活动之间的转换被平滑为躺-躺-躺。

活动识别模型是通过监督学习方法构建的,包括训练和分类步骤。在训练步骤中,提供一组标记数据来训练模型。第二步是使用训练好的模型为新未见数据分配标签。两个阶段中的数据都必须使用相同的工具进行预处理,例如过滤和特征向量计算。

后处理阶段,即虚假活动的移除,也可以是一个模型本身,因此也需要一个学习步骤。在这种情况下,预处理步骤也包括活动识别,这使得这样的分类器排列成为一个元学习问题。为了避免过拟合,重要的是用于训练后处理阶段的训练数据集不能与用于训练活动识别模型的训练数据集相同。

计划

计划包括一个训练阶段和一个部署阶段。训练阶段可以归结为以下步骤:

  1. 安装 Android Studio 并导入 MyRunsDataCollector.zip

  2. 在你的 Android 手机上加载应用程序。

  3. 收集您的数据,例如站立、行走和跑步,并将数据转换为包含 FFT 的特征向量。不要慌张;我们不会从头开始编写低级信号处理函数,如 FFT,我们将使用现有的代码来完成这项工作。数据将被保存在您的手机上,文件名为 features.arff

  4. 使用导出的数据创建并评估一个活动识别分类器,并实现一个用于去除虚假活动转换的过滤器。

  5. 将分类器重新连接到移动应用程序。

如果您没有 Android 手机,或者想跳过所有与移动应用程序相关的步骤,只需获取位于 data/features.arff 中的收集数据集,然后直接跳转到“构建分类器”部分。

从手机收集数据

本节描述了计划中的前三个步骤。如果您想直接处理数据,可以跳过本节,继续到“构建分类器”部分。该应用程序实现了收集不同活动类(例如站立、行走、跑步等)传感器数据的必要功能。

让我们从准备 Android 开发环境开始。如果您已经安装了它,请跳转到“加载数据收集器”部分。

安装 Android Studio

Android Studio 是 Android 平台的开发环境。我们将快速回顾启动手机上应用程序所需的安装步骤和基本配置。如果您想了解更多关于 Android 开发的信息,我推荐阅读 Kyle Mew 编著的 Packt Publishing 出版的入门书籍《Android 5 编程实例》。

developer.android.com/studio/ 为开发者获取最新的 Android Studio,并按照 developer.android.com/sdk/installing/index.html?pkg=studio 中的安装说明进行操作。安装大约需要 10 分钟,大约占用 0.5 GB 的空间。

按照说明操作,选择您想要的安装选项,最后点击完成以开始安装,如下面的截图所示:

图片

加载数据收集器

首先,从 GitHub 获取 MyRunsDataCollector 的源代码。一旦安装了 Android Studio,选择“打开现有的 Android Studio 项目”选项,如下面的截图所示,并选择 MyRunsDataCollector 文件夹。这将把项目导入到 Android Studio 中:

图片

项目导入完成后,您应该能够看到项目文件结构,如下面的截图所示。收集器包括 CollectorActivity.javaGlobals.javaSensorsService.java。项目还显示了实现低级信号处理的 FFT.java

图片

myrunscollector主包包含以下类:

  • Globals.java:这个类定义了全局常量,例如活动标签和 ID,以及数据文件名。

  • CollectorActivity.java:这个类实现了用户界面动作,即当按下特定按钮时会发生什么。

  • SensorsService.java:这个类实现了一个收集数据、计算特征向量(我们将在以下章节中讨论)并将数据存储到手机上的文件中的服务。

我们接下来要解决的问题是如何设计特征。

特征提取

找到一个人活动适当的表现形式可能是活动识别中最具挑战性的部分。行为需要用简单和通用的特征来表示,这样使用这些特征的模型也将是通用的,并且在不同行为上也能很好地工作。

实际上,设计针对训练集中捕获的观察特定特征并不困难;这些特征在它们身上会工作得很好。然而,由于训练集仅捕获人类行为范围的一部分,过于特定的特征可能在一般行为上失败:

图片

让我们看看这在MyRunsDataCollector中的实现方式。当应用程序启动时,一个名为onSensorChanged()的方法会获取一个包含加速度计传感器读数的三元组(xyz)以及特定的时戳,并从传感器读数中计算振幅。方法会缓冲最多 64 个连续的振幅,在计算 FFT 系数之前标记它们。

现在,让我们继续实际数据收集。

收集训练数据

我们现在可以使用收集器来收集活动识别的训练数据。收集器默认支持三种活动:站立、行走和跑步,如下面的截图所示。

您可以选择一个活动,即目标类值,然后通过点击“开始收集”按钮开始记录数据。确保每个活动至少记录三分钟;例如,如果选择了行走活动,请按“开始收集”并至少行走三分钟。活动结束后,请按“停止收集”。对每个活动重复此操作。

你还可以收集涉及这些活动的不同场景,例如,在厨房里走动、在外面走动、成列行走等。通过这样做,你将为每个活动类别拥有更多数据,并且分类器会更好。有道理,对吧?数据越多,分类器就越不会困惑。如果你只有少量数据,就会发生过拟合,分类器会混淆类别——站立与行走、行走与跑步等。然而,数据越多,它们就越不容易混淆。在调试时,你可能每个类别收集不到三分钟的数据,但对你最终的产品来说,数据越多越好。多个录制实例将简单地累积在同一个文件中。

注意,删除数据按钮会删除存储在手机文件上的数据。如果你想重新开始,请在开始之前点击删除数据;否则,新收集的数据将被附加到文件末尾:

图片

收集器实现了前几节中讨论的图:它收集加速度计样本,计算幅度,使用FFT.java类计算系数,并生成特征向量。然后,数据存储在 Weka 格式的features.arff文件中。特征向量的数量将根据你收集的数据量而变化。你收集数据的时间越长,累积的特征向量就越多。

一旦你停止使用收集工具收集训练数据,我们需要抓取数据以继续工作流程。我们可以使用 Android 设备监控器中的文件浏览器上传手机上的features.arff文件并将其存储在计算机上。你可以通过点击以下截图中的 Android 机器人图标来访问你的 Android 设备监控器:

图片

通过在左侧选择你的设备,你可以在右侧看到你的手机存储内容。导航到mnt/shell/emulated/Android/data/edu.dartmouth.cs.myrunscollector/files/features.arff,如下截图所示:

图片

要将此文件上传到你的计算机,你需要选择文件(它被突出显示)并点击上传。

现在,我们准备构建一个分类器。

构建分类器

一旦将传感器样本表示为特征向量并分配了类别,就可以应用标准的有监督分类技术,包括特征选择、特征离散化、模型学习、k-折交叉验证等。本章不会深入探讨机器学习算法的细节。任何支持数值特征的算法都可以应用,包括 SVMs、随机森林、AdaBoost、决策树、神经网络、多层感知器等。

因此,让我们从一个基本的开始:决策树。在这里,我们将加载数据集,构建类属性集,构建决策树模型,并输出模型:

String databasePath = "/Users/bostjan/Dropbox/ML Java Book/book/datasets/chap9/features.arff"; 

// Load the data in arff format 
Instances data = new Instances(new BufferedReader(new 
   FileReader(databasePath))); 

// Set class the last attribute as class 
data.setClassIndex(data.numAttributes() - 1); 

// Build a basic decision tree model 
String[] options = new String[]{}; 
J48 model = new J48(); 
model.setOptions(options); 
model.buildClassifier(data); 

// Output decision tree 
System.out.println("Decision tree model:\n"+model); 

算法首先输出模型,如下所示:

    Decision tree model:
    J48 pruned tree
    ------------------

    max <= 10.353474
    |   fft_coef_0000 <= 38.193106: standing (46.0)
    |   fft_coef_0000 > 38.193106
    |   |   fft_coef_0012 <= 1.817792: walking (77.0/1.0)
    |   |   fft_coef_0012 > 1.817792
    |   |   |   max <= 4.573082: running (4.0/1.0)
    |   |   |   max > 4.573082: walking (24.0/2.0)
    max > 10.353474: running (93.0)

    Number of Leaves  : 5

    Size of the tree : 9

树的结构相当简单且看似准确,因为终端节点中的多数类分布相当高。让我们运行一个基本的分类器评估来验证结果,如下所示:

// Check accuracy of model using 10-fold cross-validation 
Evaluation eval = new Evaluation(data); 
eval.crossValidateModel(model, data, 10, new Random(1), new 
   String[] {}); 
System.out.println("Model performance:\n"+ 
   eval.toSummaryString()); 

这将输出以下模型性能:

    Correctly Classified Instances         226               92.623  %
    Incorrectly Classified Instances        18                7.377  %
    Kappa statistic                          0.8839
    Mean absolute error                      0.0421
    Root mean squared error                  0.1897
    Relative absolute error                 13.1828 %
    Root relative squared error             47.519  %
    Coverage of cases (0.95 level)          93.0328 %
    Mean rel. region size (0.95 level)      27.8689 %
    Total Number of Instances              244     

分类准确率得分非常高,92.62%,这是一个惊人的结果。结果之所以如此之好,一个重要原因在于我们的评估设计。我的意思是以下内容:序列实例彼此非常相似,因此如果我们在一个 10 折交叉验证过程中随机分割它们,那么我们使用几乎相同的实例进行训练和测试的可能性很高;因此,直接的 k 折交叉验证会产生对模型性能的乐观估计。

一个更好的方法是使用与不同测量集或甚至不同人员相对应的折数。例如,我们可以使用该应用程序从五个人那里收集学习数据。然后,进行 k 个人交叉验证是有意义的,其中模型在四个人身上训练,在第五个人身上测试。对于每个人重复此过程,并将结果平均。这将给我们提供一个更现实的模型性能估计。

不考虑评估注释,让我们看看如何处理分类错误。

减少虚假转换

在活动识别管道的末尾,我们想要确保分类不是太波动,也就是说,我们不希望活动每毫秒都改变。一个基本的方法是设计一个过滤器,它忽略活动序列中的快速变化。

我们构建一个过滤器,它记住最后一个窗口活动并返回最频繁的一个。如果有多个活动具有相同的分数,它返回最近的一个。

首先,我们创建一个新的SpuriousActivityRemoval类,它将包含活动列表和window参数:

class SpuriousActivityRemoval{ 

  List<Object> last; 
  int window; 

  public SpuriousActivityRemoval(int window){ 
    this.last = new ArrayList<Object>(); 
    this.window = window; 
  } 

接下来,我们创建Object filter(Object)方法,该方法将接受一个活动并返回一个过滤后的活动。该方法首先检查我们是否有足够的观察结果。如果没有,它简单地存储观察结果并返回相同的值,如下面的代码所示:

  public Object filter(Object obj){ 
    if(last.size() < window){ 
      last.add(obj); 
      return obj; 
  } 

如果我们已收集了window观察结果,我们只需返回最频繁的观察结果,删除最老的观察结果,并插入新的观察结果:

    Object o = getMostFrequentElement(last); 
    last.add(obj); 
    last.remove(0); 
    return o; 
  } 

这里缺少的是一个从对象列表中返回最频繁元素的函数。我们使用哈希映射来实现这一点,如下所示:

  private Object getMostFrequentElement(List<Object> list){ 

    HashMap<String, Integer> objectCounts = new HashMap<String, 
       Integer>(); 
    Integer frequntCount = 0; 
    Object frequentObject = null; 

现在,我们遍历列表中的所有元素,将每个唯一元素插入到哈希映射中,或者如果它已经在哈希映射中,则更新其计数器。循环结束时,我们存储迄今为止找到的最频繁元素,如下所示:

    for(Object obj : list){ 
      String key = obj.toString(); 
      Integer count = objectCounts.get(key); 
      if(count == null){ 
        count = 0; 
      } 
      objectCounts.put(key, ++count); 

      if(count >= frequntCount){ 
        frequntCount = count; 
        frequentObject = obj; 
      } 
    } 

    return frequentObject; 
  } 

} 

让我们运行一个简单的例子:

String[] activities = new String[]{"Walk", "Walk", "Walk", "Run", 
   "Walk", "Run", "Run", "Sit", "Sit", "Sit"}; 
SpuriousActivityRemoval dlpFilter = new 
   SpuriousActivityRemoval(3); 
for(String str : activities){ 
  System.out.println(str +" -> "+ dlpFilter.filter(str)); 
} 

示例输出了以下活动:

    Walk -> Walk
    Walk -> Walk
    Walk -> Walk
    Run -> Walk
    Walk -> Walk
    Run -> Walk
    Run -> Run
    Sit -> Run
    Sit -> Run
    Sit -> Sit

结果是一个连续的活动序列,也就是说,我们没有快速的变化。这增加了一些延迟,但除非这对应用程序至关重要,否则是可以接受的。

通过将分类器识别的前n个活动附加到特征向量中,可以增强活动识别。将先前活动附加的危险是,机器学习算法可能会学习到当前活动总是与先前活动相同,因为这种情况通常会发生。可以通过拥有两个分类器 A 和 B 来解决这个问题:分类器 B 的属性向量包含由分类器 A 识别的n个先前活动。分类器 A 的属性向量不包含任何先前活动。这样,即使 B 对先前活动给予了很大的权重,由 A 识别的先前活动也会随着 A 不受 B 惯性的影响而改变。

剩下的工作是将分类器和过滤器嵌入到我们的移动应用程序中。

将分类器插入到移动应用中

有两种方法可以将分类器集成到移动应用程序中。第一种方法涉及使用 Weka 库将模型导出为 Weka 格式,将 Weka 库作为我们的移动应用程序的依赖项,加载模型等。该过程与我们在第三章中看到的示例相同,即基本算法-分类、回归和聚类。第二种方法更轻量级:我们将模型导出为源代码,例如,我们创建一个实现决策树分类器的类。然后,我们可以简单地复制并粘贴源代码到我们的移动应用中,甚至不需要导入任何 Weka 依赖项。

幸运的是,一些 Weka 模型可以通过toSource(String)函数轻松导出为源代码:

// Output source code implementing the decision tree 
System.out.println("Source code:\n" +  
  model.toSource("ActivityRecognitionEngine")); 

这将输出一个与我们的模型相对应的ActivityRecognitionEngine类。现在,让我们更仔细地看看输出代码:

class ActivityRecognitionEngine { 

  public static double classify(Object[] i) 
    throws Exception { 

    double p = Double.NaN; 
    p = ActivityRecognitionEngine.N17a7cec20(i); 
    return p; 
  } 
  static double N17a7cec20(Object []i) { 
    double p = Double.NaN; 
    if (i[64] == null) { 
      p = 1; 
    } else if (((Double) i[64]).doubleValue() <= 10.353474) { 
    p = ActivityRecognitionEngine.N65b3120a1(i); 
    } else if (((Double) i[64]).doubleValue() > 10.353474) { 
      p = 2; 
    }  
    return p; 
  } 
... 

输出的ActivityRecognitionEngine类实现了我们之前讨论的决策树。机器生成的函数名,如N17a7cec20(Object []),对应于决策树节点。可以通过classify(Object[])方法调用分类器,其中我们应该传递通过与之前章节中讨论的相同程序获得的特征向量。像往常一样,它返回一个double值,表示类标签索引。

摘要

在本章中,我们讨论了如何为移动应用程序实现活动识别模型。我们探讨了整个流程,包括数据收集、特征提取、模型构建、评估和模型部署。

在下一章中,我们将继续介绍另一个针对文本分析的 Java 库:Mallet。

第十章:使用 Mallet 进行文本挖掘 - 主题建模和垃圾邮件检测

在本章中,我们将首先讨论什么是文本挖掘,它能提供什么样的分析,以及为什么你可能想在你的应用中使用它。然后我们将讨论如何使用Mallet,这是一个用于自然语言处理的 Java 库,涵盖数据导入和文本预处理。之后,我们将探讨两个文本挖掘应用:主题建模,我们将讨论如何使用文本挖掘来识别文本文档中的主题,而无需逐个阅读它们,以及垃圾邮件检测,我们将讨论如何自动将文本文档分类到类别中。

本章将涵盖以下主题:

  • 介绍文本挖掘

  • 安装和使用 Mallet

  • 主题建模

  • 垃圾邮件检测

介绍文本挖掘

文本挖掘,或文本分析,是指从文本文档中自动提取高质量信息的过程,这些文档通常是用自然语言编写的,其中高质量信息被认为是相关的、新颖的和有趣的。

当一个典型的文本分析应用被用来扫描一组文档以生成搜索索引时,文本挖掘可以应用于许多其他应用,包括将文本分类到特定领域;通过文本聚类自动组织一组文档;情感分析以识别和提取文档中的主观信息;概念或实体提取,能够从文档中识别人、地点、组织和其他实体;文档摘要以自动提供原始文档中的最重要观点;以及学习命名实体之间的关系。

基于统计模式挖掘的过程通常涉及以下步骤:

  1. 信息检索和提取

  2. 将非结构化文本数据转换为结构化数据;例如,解析、去除噪声词、词汇分析、计算词频和推导语言特征

  3. 从结构化数据中发现模式并进行标记或注释

  4. 结果的评估和解释

在本章的后面部分,我们将探讨两个应用领域:主题建模和文本分类。让我们来看看它们能带来什么。

主题建模

主题建模是一种无监督技术,如果你需要分析大量的文本文档档案并希望了解档案包含的内容,这可能很有用,而不必亲自阅读每一份文档。文本文档可以是博客文章、电子邮件、推文、文档、书籍章节、日记条目等。主题建模在文本语料库中寻找模式;更确切地说,它通过统计上有意义的单词列表来识别主题。最著名的算法是潜在狄利克雷分配LDA),它假设作者通过从可能的单词篮子中选择单词来创作文本,每个篮子对应一个主题。利用这个假设,可以将文本从最可能的单词来源篮子中数学分解出来。然后算法重复这个过程,直到收敛到最可能的单词分布到篮子中的分布,我们称之为主题

例如,如果我们对一系列新闻文章进行主题建模,算法将返回一个包含这些主题的可能关键词列表。以新闻文章为例,列表可能看起来像以下这样:

  • 胜利者、进球、足球、得分、第一名

  • 公司、股票、银行、信贷、商业

  • 选举、对手、总统、辩论、即将到来

通过查看关键词,我们可以识别出新闻文章涉及体育、商业、即将到来的选举等内容。在本章的后面部分,我们将学习如何使用新闻文章的例子来实现主题建模。

文本分类

在文本分类,或文本分类中,目标是根据其内容将文本文档分配到一类或多类,这些类别通常是一个更广泛的主题领域,如车辆或宠物。这样的通用类别被称为主题,此时的分类任务被称为文本分类文本分类主题分类主题检测。虽然文档可以根据其他属性进行分类,如文档类型、作者和出版年份,但本章的重点将仅限于文档内容。以下是一些文本分类的例子:

  • 电子邮件中的垃圾邮件检测、用户评论、网页等

  • 性内容检测

  • 情感检测,它自动将产品或服务评论分类为正面或负面

  • 根据内容对电子邮件进行排序

  • 主题特定搜索,其中搜索引擎将搜索限制在特定主题或类型,从而提供更准确的结果

这些例子显示了文本分类在信息检索系统中的重要性;因此,大多数现代信息检索系统都使用某种形式的文本分类器。本书中将用作例子的分类任务是检测电子邮件垃圾邮件的文本分类。

我们将继续本章,介绍 Mallet,这是一个基于 Java 的用于统计自然语言处理、文档分类、聚类、主题建模、信息提取和其他机器学习应用文本的软件包。然后我们将介绍两个文本分析应用,即主题建模和作为文本分类的垃圾邮件检测。

安装 Mallet

Mallet 可在马萨诸塞大学阿默斯特分校网站mallet.cs.umass.edu/download.php下载。导航到下载部分,如以下截图所示,并选择最新的稳定版本(2.0.8,本书编写时):

下载 ZIP 文件并提取内容。在提取的目录中,你应该找到一个名为dist的文件夹,其中包含两个 JAR 文件:mallet.jarmallet-deps.jar。第一个包含所有打包的 Mallet 类,而第二个包含所有依赖项。我们将把这两个 JAR 文件作为引用库包含到你的项目中,如以下截图所示:

如果你使用 Eclipse,右键单击项目,选择属性,然后选择 Java 构建路径。选择库选项卡,点击添加外部 JAR 文件。现在,选择两个 JAR 文件并确认,如以下截图所示:

现在我们已经准备好开始使用 Mallet 了。

处理文本数据

文本挖掘的主要挑战之一是将非结构化的自然语言文本转换为基于属性的实例。这个过程涉及许多步骤,如下所示:

首先,我们从互联网、现有文档或数据库中提取一些文本。在第一步结束时,文本可能仍然以 XML 格式或其他专有格式存在。下一步是从实际文本中提取文本并将其分割成文档的部分,例如标题、标题、摘要和正文。第三步涉及对文本编码进行标准化,以确保字符以相同的方式呈现;例如,以 ASCII、ISO 8859-1 和 Windows-1250 等格式编码的文档被转换为 Unicode 编码。接下来,分词将文档分割成特定的单词,而下一步则移除通常具有低预测能力的频繁单词,例如 the、a、I 和 we。

词性标注POS)和词形还原步骤可以包括将每个标记转换为它的基本形式,这被称为词元,通过去除词尾和修饰词来实现。例如,running 变为 run,而 better 变为 good。一种简化的方法是词干提取,它在一个单词上操作,没有任何上下文,因此无法区分具有不同意义的单词,例如,axes 作为 axe 的复数形式以及 axis。

最后一步将标记转换为特征空间。通常,特征空间是词袋模型BoW)的表示。在这个表示中,创建了一个包含数据集中所有单词的集合。然后,每个文档都表示为一个向量,该向量统计特定单词在文档中出现的次数。

考虑以下两个句子的例子:

  • Jacob likes table tennis. Emma likes table tennis too

  • Jacob also likes basketball

在这个例子中,BoW(词袋模型)由以下单词组成:{Jacob, likes, table, tennis, Emma, too, also, basketball},共有八个不同的单词。现在可以将这两个句子表示为向量,使用列表的索引表示特定索引处的单词在文档中出现的次数,如下所示:

  • [1, 2, 2, 2, 1, 0, 0, 0]

  • [1, 1, 0, 0, 0, 0, 1, 1]

这样的向量最终成为进一步学习的实例。

基于 BoW 模型的一个非常强大的展示方式是word2vec。Word2vec 是由 Google 的研究团队,以 Tomas Mikolov 为首,在 2013 年引入的。Word2vec 是一个神经网络,它学习单词的分布式表示。这种展示的一个有趣特性是单词出现在簇中,因此可以使用向量数学重现一些单词关系,例如类比。一个著名的例子表明,king−man+woman 返回 queen。更多细节和实现可以在以下链接中找到:code.google.com/archive/p/word2vec/.

导入数据

在本章中,我们不会探讨如何从网站抓取一组文档或从数据库中提取它们。相反,我们将假设我们已经将它们收集为文档集,并以.txt文件格式存储。现在让我们看看加载它们的两种选项。第一种选项解决的是每个文档都存储在其自己的.txt文件中的情况。第二种选项解决的是所有文档都存储在单个文件中,每行一个文档的情况。

从目录导入

Mallet 支持使用cc.mallet.pipe.iterator.FileIterator类从目录中读取。文件迭代器是通过以下三个参数构建的:

  • 包含文本文件的File[]目录列表

  • 一个文件过滤器,用于指定在目录中要选择哪些文件

  • 应用到文件名上以产生类标签的模式

考虑以下截图所示的数据结构。我们按照五个主题(techentertainmentpoliticssportbusiness)将文档组织在文件夹中。每个文件夹包含特定主题的文档,如下截图所示:

图片

在这种情况下,我们初始化iterator如下:

FileIterator iterator = 
  new FileIterator(new File[]{new File("path-to-my-dataset")}, 
  new TxtFilter(), 
  FileIterator.LAST_DIRECTORY); 

第一个参数指定了我们的根文件夹的路径,第二个参数将迭代器限制为仅.txt文件,而最后一个参数要求方法使用路径中的最后一个目录名作为类标签。

从文件导入

加载文档的另一种选项是通过cc.mallet.pipe.iterator.CsvIterator.CsvIterator(Reader, Pattern, int, int, int),它假设所有文档都在一个文件中,并且通过正则表达式提取每一行返回一个实例。该类通过以下组件初始化:

  • Reader:这是一个对象,指定如何从文件中读取

  • Pattern:这是一个正则表达式,提取三个组:数据、目标标签和文档名

  • int, int, int:这些是正则表达式中数据、目标和名称组的索引

考虑以下格式的文本文档,指定文档名称、类别和内容:

AP881218 local-news A 16-year-old student at a private 
   Baptist...  
AP880224 business The Bechtel Group Inc. offered in 1985 to...  
AP881017 local-news A gunman took a 74-year-old woman hostage...  
AP900117 entertainment Cupid has a new message for lovers 
   this...  
AP880405 politics The Reagan administration is weighing w...  

要将一行解析为三个组,我们可以使用以下正则表达式:

^(\\S*)[\\s,]*(\\S*)[\\s,]*(.*)$

在括号()中出现了三个组,其中第三个组包含数据,第二个组包含目标类,第一个组包含文档 ID。iterator的初始化如下:

CsvIterator iterator = new CsvIterator ( 
fileReader, 
Pattern.compile("^(\\S*)[\\s,]*(\\S*)[\\s,]*(.*)$"), 
  3, 2, 1)); 

在这里,正则表达式提取了由空格分隔的三个组,其顺序为3, 2, 1

现在让我们转向数据预处理管道。

预处理文本数据

一旦初始化了一个将遍历数据的迭代器,我们需要将数据通过本节开头所述的一系列转换传递。Mallet 通过管道和一系列可以包含在管道中的步骤来支持此过程,这些步骤收集在cc.mallet.pipe包中。以下是一些示例:

  • Input2CharSequence:这是一个管道,可以从各种文本源(无论是 URL、文件还是读取器)读取到CharSequence

  • CharSequenceRemoveHTML:这个管道从CharSequence中移除 HTML

  • MakeAmpersandXMLFriendly:这会将标记序列中的&转换为&amp

  • TokenSequenceLowercase:这会将数据字段中每个标记序列中的文本转换为小写

  • TokenSequence2FeatureSequence:这会将每个实例的数据字段中的标记序列转换为特征序列

  • TokenSequenceNGrams:这会将数据字段中的标记序列转换为 ngrams 的标记序列,即两个或更多单词的组合

完整的处理步骤列表可以在以下 Mallet 文档中找到:mallet.cs.umass.edu/api/index.html?cc/mallet/pipe/iterator/package-tree.html

现在我们已经准备好构建一个将导入我们的数据的类。我们将使用以下步骤来完成:

  1. 让我们构建一个管道,其中每个处理步骤在 Mallet 中用一个管道表示。管道可以通过ArrayList<Pipe>对象列表以串行方式连接起来:
ArrayList<Pipe> pipeList = new ArrayList<Pipe>(); 
  1. 让我们从读取文件对象中的数据并转换所有字符为小写开始:
pipeList.add(new Input2CharSequence("UTF-8")); 
pipeList.add( new CharSequenceLowercase() );
  1. 我们将使用正则表达式对原始字符串进行分词。以下模式包括 Unicode 字母、数字和下划线字符:
Pattern tokenPattern = 
Pattern.compile("[\\p{L}\\p{N}_]+"); 

pipeList.add(new CharSequence2TokenSequence(tokenPattern)); 
  1. 我们现在将移除停用词,即没有预测能力的频繁单词,使用标准的英语停用词表。两个额外的参数指示是否应区分大小写以及是否标记删除而不是简单地删除单词。我们将它们都设置为false
pipeList.add(new TokenSequenceRemoveStopwords(new File(stopListFilePath), "utf-8", false, false, false));
  1. 我们可以将实际的单词转换为整数,表示在 BoW(词袋模型)中的单词索引,而不是存储实际的单词:
pipeList.add(new TokenSequence2FeatureSequence()); 
  1. 我们将对类别标签做同样的处理;而不是使用标签字符串,我们将使用一个整数,表示标签在词袋中的位置:
pipeList.add(new Target2Label()); 
  1. 我们也可以通过调用PrintInputAndTarget管道来打印特征和标签:
pipeList.add(new PrintInputAndTarget()); 
  1. 我们将管道列表存储在SerialPipes类中,该类将通过一系列管道转换实例:
SerialPipes pipeline = new SerialPipes(pipeList); 

现在,让我们看看如何在文本挖掘应用中应用这个方法!

BBC 新闻的主题建模

如前所述,主题建模的目标是识别文本语料库中与文档主题相对应的模式。在这个例子中,我们将使用来自 BBC 新闻的数据集。这个数据集是机器学习研究中的标准基准之一,可用于非商业和研究目的。

目标是构建一个能够将一个未分类的文档分配到某个主题的分类器。

BBC 数据集

2006 年,格林和坎宁安收集了 BBC 数据集来研究一个特定的文档——使用支持向量机的聚类挑战。该数据集包含来自 2004 年至 2005 年 BBC 新闻网站上的 2,225 个文档,对应于从五个主题领域收集的故事:商业、娱乐、政治、体育和技术。该数据集可以在以下网站上查看:mlg.ucd.ie/datasets/bbc.html

我们可以在“数据集:BBC”部分下下载原始文本文件。你还会注意到该网站包含一个已经处理过的数据集,但在这个例子中,我们想要自己处理数据集。ZIP 文件包含五个文件夹,每个文件夹对应一个主题。实际的文档放置在相应的主题文件夹中,如下面的截图所示:

图片

现在,让我们构建一个主题分类器。

建模

我们将使用以下步骤开始建模阶段:

  1. 我们将首先导入数据集并使用以下代码行处理文本:
import cc.mallet.types.*; 
import cc.mallet.pipe.*; 
import cc.mallet.pipe.iterator.*; 
import cc.mallet.topics.*; 

import java.util.*; 
import java.util.regex.*; 
import java.io.*; 

public class TopicModeling { 

  public static void main(String[] args) throws Exception { 

String dataFolderPath = "data/bbc"; 
String stopListFilePath = "data/stoplists/en.txt"; 
  1. 我们将创建一个默认的pipeline对象,如之前所述:
ArrayList<Pipe> pipeList = new ArrayList<Pipe>(); 
pipeList.add(new Input2CharSequence("UTF-8")); 
Pattern tokenPattern = Pattern.compile("[\\p{L}\\p{N}_]+"); 
pipeList.add(new CharSequence2TokenSequence(tokenPattern)); 
pipeList.add(new TokenSequenceLowercase()); 
pipeList.add(new TokenSequenceRemoveStopwords(new File(stopListFilePath), "utf-8", false, false, false)); 
pipeList.add(new TokenSequence2FeatureSequence()); 
pipeList.add(new Target2Label()); 
SerialPipes pipeline = new SerialPipes(pipeList); 
  1. 接下来,我们将初始化folderIterator对象:
FileIterator folderIterator = new FileIterator( 
    new File[] {new File(dataFolderPath)}, 
    new TxtFilter(), 
    FileIterator.LAST_DIRECTORY);
  1. 我们现在将使用我们想要用于处理文本的pipeline构建一个新的实例列表:
InstanceList instances = new InstanceList(pipeline);
  1. 我们处理iterator提供的每个实例:
instances.addThruPipe(folderIterator); 
  1. 现在让我们使用cc.mallet.topics.ParallelTopicModel.ParallelTopicModel类创建一个具有五个主题的模型,该类实现了一个简单的线程化 LDA 模型。LDA 是主题建模中常用的方法,它使用狄利克雷分布来估计所选主题生成特定文档的概率。我们不会在本章深入探讨细节;读者可参考 D. Blei 等人(2003 年)的原始论文。

注意:在机器学习中,还有一个具有相同缩写的分类算法,它指的是线性判别分析LDA)。除了常见的缩写外,它与 LDA 模型没有共同之处。

该类使用 alpha 和 beta 参数实例化,可以广泛解释如下:

  • 高 alpha 值意味着每个文档很可能包含大多数主题的混合,而不是任何单个主题。低 alpha 值对文档的这种约束较少,这意味着文档可能只包含少数,甚至只有一个主题的混合。

  • 高 beta 值意味着每个主题很可能包含大多数单词的混合,而不是任何特定的单词;而低值则意味着一个主题可能只包含少数单词的混合。

在我们的案例中,我们最初将两个参数都保持得很低(alpha_t = 0.01, beta_w = 0.01),因为我们假设我们的数据集中的主题混合不多,每个主题都有许多单词:

int numTopics = 5; 
ParallelTopicModel model =  
new ParallelTopicModel(numTopics, 0.01, 0.01); 
  1. 我们将向模型添加instances,由于我们使用并行实现,我们将指定并行运行的线程数,如下所示:
model.addInstances(instances); 
model.setNumThreads(4);
  1. 我们现在将运行模型进行选定次数的迭代。每次迭代都用于更好地估计内部 LDA 参数。对于测试,我们可以使用少量迭代,例如,50 次;而在实际应用中,使用10002000次迭代。最后,我们将调用void estimate()方法,实际上将构建一个 LDA 模型:
model.setNumIterations(1000); 
model.estimate(); 

模型输出以下结果:

    0 0,06654  game england year time win world 6 
    1 0,0863  year 1 company market growth economy firm 
    2 0,05981  people technology mobile mr games users music 
    3 0,05744  film year music show awards award won 
    4 0,11395  mr government people labour election party blair 

    [beta: 0,11328] 
    <1000> LL/token: -8,63377

    Total time: 45 seconds

LL/token表示模型的对数似然,除以总标记数,表示数据在模型下的可能性。值的增加意味着模型正在改进。

输出还显示了描述每个主题的前几个单词。这些单词与初始主题非常吻合:

  • 主题 0gameenglandyeartimewinworld6 ⇒ 体育

  • 主题 1year1companymarketgrowtheconomyfirm ⇒ 金融

  • 主题 2peopletechnologymobilemrgamesusersmusic⇒技术

  • 主题 3filmyearmusicshowawardsawardwon⇒娱乐

  • 主题 4mrgovernmentpeoplelaborelectionpartyblair⇒政治

仍然有一些词没有太多意义,例如mr16。我们可以将它们包含在停用词列表中。此外,一些词出现了两次,例如awardawards。这是因为我们没有应用任何词干提取或词形还原管道。

在下一节中,我们将检查模型是否表现良好。

评估模型

由于统计主题建模具有无监督的性质,这使得模型选择变得困难。对于某些应用,可能有一些外在任务在手,例如信息检索或文档分类,其性能可以评估。然而,一般来说,我们想要估计模型泛化主题的能力,无论任务如何。

2009 年,Wallach 等人提出了一种通过在模型下计算保留文档的对数概率来衡量模型质量的方法。未观察到的文档的似然性可以用来比较模型——更高的似然性意味着更好的模型。

我们将使用以下步骤评估模型:

  1. 让我们将文档分为训练集和测试集(即保留的文档),其中我们使用 90%进行训练,10%进行测试:
// Split dataset 
InstanceList[] instanceSplit= instances.split(new Randoms(), new 
   double[] {0.9, 0.1, 0.0}); 
  1. 现在,让我们仅使用我们文档的90%重建我们的模型:
// Use the first 90% for training 
model.addInstances(instanceSplit[0]); 
model.setNumThreads(4); 
model.setNumIterations(50); 
model.estimate(); 
  1. 我们将初始化一个实现 Wallach 的保留文档对数概率的estimator对象,即MarginalProbEstimator
// Get estimator 
MarginalProbEstimator estimator = model.getProbEstimator(); 

LDA 的直观描述由 Annalyn Ng 在她的博客中总结:annalyzin.wordpress.com/2015/06/21/laymans-explanation-of-topic-modeling-with-lda-2/。要深入了解 LDA 算法、其组件和工作原理,请查看 David Blei 等人于 2003 年发表的原始论文 LDA,网址为jmlr.csail.mit.edu/papers/v3/blei03a.html,或者查看布朗大学 D. Santhanam 的总结演示,网址为www.cs.brown.edu/courses/csci2950-p/spring2010/lectures/2010-03-03_santhanam.pdf

该类实现了许多需要相当深入的理论知识来了解 LDA 方法如何工作的估计器。我们将选择从左到右的评估器,它适用于广泛的用途,包括文本挖掘和语音识别。从左到右的评估器作为double evaluateLeftToRight方法实现,接受以下组件:

  • Instances heldOutDocuments:这测试实例。

  • int numParticles:此算法参数表示从左到右的标记数量,默认值为 10。

  • boolean useResampling:这表示是否在从左到右评估时重新采样主题;重新采样更准确,但会导致文档长度的平方级缩放。

  • PrintStream docProbabilityStream:这是写入每个文档推断的对数概率的文件或stdout

  1. 让我们按照以下方式运行estimator
double loglike = estimator.evaluateLeftToRight( 
  instanceSplit[1], 10, false, null);); 
System.out.println("Total log likelihood: "+loglike); 

在我们的特定情况下,estimator输出了以下log likelihood,当与其他使用不同参数、管道或数据构建的模型进行比较时是有意义的——对数似然越高,模型越好:

    Total time: 3 seconds
    Topic Evaluator: 5 topics, 3 topic bits, 111 topic mask
    Total log likelihood: -360849.4240795393

现在,让我们看看如何利用这个模型。

重复使用模型

由于我们通常不会即时构建模型,因此一次性训练一个模型并重复使用它来分类新数据通常是有意义的。

注意,如果您想对新的文档进行分类,它们需要经过与其他文档相同的管道——管道对于训练和分类都应该是相同的。在训练过程中,管道的数据字母表会随着每个训练实例的更新而更新。如果您创建了一个具有相同步骤的新管道,您不会产生相同的管道,因为其数据字母表是空的。因此,为了在新数据上使用模型,我们必须保存或加载管道以及模型,并使用此管道添加新实例。

保存模型

Mallet 支持基于序列化的保存和恢复对象的标准方法。

我们只需创建一个ObjectOutputStream类的新实例,并将对象写入文件,如下所示:

String modelPath = "myTopicModel"; 

//Save model 
ObjectOutputStream oos = new ObjectOutputStream( 
new FileOutputStream (new File(modelPath+".model"))); 
oos.writeObject(model); 
oos.close();    

//Save pipeline 
oos = new ObjectOutputStream( 
new FileOutputStream (new File(modelPath+".pipeline"))); 
oos.writeObject(pipeline); 
oos.close(); 

恢复模型

通过序列化保存的模型恢复是一个使用ObjectInputStream类的逆操作:

String modelPath = "myTopicModel"; 

//Load model 
ObjectInputStream ois = new ObjectInputStream( 
  new FileInputStream (new File(modelPath+".model"))); 
ParallelTopicModel model = (ParallelTopicModel) ois.readObject(); 
ois.close();    

// Load pipeline 
ois = new ObjectInputStream( 
  new FileInputStream (new File(modelPath+".pipeline"))); 
SerialPipes pipeline = (SerialPipes) ois.readObject(); 
ois.close();    

我们讨论了如何构建一个 LDA 模型来自动将文档分类到主题。在下一个示例中,我们将探讨另一个文本挖掘问题——文本分类。

检测邮件垃圾邮件

垃圾邮件或电子垃圾邮件是指未经请求的消息,通常携带广告内容、感染附件、钓鱼网站或恶意软件链接等。虽然最广为人知的垃圾邮件形式是电子邮件垃圾邮件,但垃圾邮件滥用也出现在其他媒体中:网站评论、即时消息、互联网论坛、博客、在线广告等。

在本章中,我们将讨论如何使用 BoW 表示法构建朴素贝叶斯垃圾邮件过滤,这是一种在第一个商业垃圾邮件过滤器中实现的基本技术;例如,Mozilla Thunderbird 邮件客户端使用此类过滤的原生实现。虽然本章的示例将使用邮件垃圾邮件,但底层方法也可以应用于其他类型的基于文本的垃圾邮件。

邮件垃圾邮件数据集

在 2000 年,安德鲁特斯波洛斯等人收集了第一个电子邮件垃圾邮件数据集,用于评估垃圾邮件过滤算法。他们研究了如何使用朴素贝叶斯分类器来检测垃圾邮件,以及额外的管道,如停用词列表、词干提取和词形还原是否有助于提高性能。该数据集由 Andrew Ng 在 OpenClassroom 的机器学习课程中重新组织,可在openclassroom.stanford.edu/MainFolder/DocumentPage.php?course=MachineLearning&doc=exercises/ex6/ex6.html下载。

选择并下载第二个选项,ex6DataEmails.zip,如图所示:

图片

ZIP 文件包含以下文件夹:

  • nonspam-trainspam-train文件夹包含用于训练的预处理电子邮件,每个文件夹有 350 封电子邮件。

  • nonspam-testspam-test文件夹构成了测试集,包含 130 封垃圾邮件和 130 封非垃圾邮件。这些是你将进行预测的文档。请注意,尽管单独的文件夹告诉你正确的标签,但你应该在没有这种知识的情况下对所有测试文档进行预测。在你做出预测后,你可以使用正确的标签来检查你的分类是否正确。

为了利用 Mallet 的文件夹迭代器,让我们重新组织文件夹结构如下。我们将创建两个文件夹,traintest,并将spam/nospam文件夹放在相应的文件夹下。初始文件夹结构如图所示:

图片

最终文件夹结构将如图所示:

图片

下一步是将电子邮件消息转换为特征向量。

特征生成

我们将使用以下步骤进行特征生成:

  1. 我们将创建一个默认的管道,如前所述:
ArrayList<Pipe> pipeList = new ArrayList<Pipe>(); 
pipeList.add(new Input2CharSequence("UTF-8")); 
Pattern tokenPattern = Pattern.compile("[\\p{L}\\p{N}_]+"); 
pipeList.add(new CharSequence2TokenSequence(tokenPattern)); 
pipeList.add(new TokenSequenceLowercase()); 
pipeList.add(new TokenSequenceRemoveStopwords(new 
   File(stopListFilePath), "utf-8", false, false, false)); 
pipeList.add(new TokenSequence2FeatureSequence()); 
pipeList.add(new FeatureSequence2FeatureVector()); 
pipeList.add(new Target2Label()); 
SerialPipes pipeline = new SerialPipes(pipeList); 

注意,我们添加了一个额外的FeatureSequence2FeatureVector管道,它将特征序列转换为特征向量。当我们有特征向量中的数据时,我们可以使用任何分类算法,就像我们在前面的章节中看到的那样。我们将在 Mallet 中继续我们的例子,以展示如何构建分类模型。

  1. 我们初始化一个文件夹迭代器,以加载train文件夹中的示例,该文件夹包含spamnonspam子文件夹中的电子邮件示例,这些将用作示例标签:
FileIterator folderIterator = new FileIterator( 
    new File[] {new File(dataFolderPath)}, 
    new TxtFilter(), 
    FileIterator.LAST_DIRECTORY); 
  1. 我们将使用我们想要用于处理文本的pipeline对象构建一个新的实例列表:
InstanceList instances = new InstanceList(pipeline); 
  1. 我们将处理迭代器提供的每个实例:
instances.addThruPipe(folderIterator); 

我们现在已经加载数据并将其转换为特征向量。现在让我们在训练集上训练我们的模型,并在测试集上预测spam/nonspam分类。

训练和测试

Mallet 在cc.mallet.classify包中实现了一系列分类器,包括决策树、朴素贝叶斯、AdaBoost、bagging、boosting 等。我们将从一个基本的分类器开始,即朴素贝叶斯分类器。分类器通过ClassifierTrainer类初始化,当我们调用其train(Instances)方法时返回一个分类器:

ClassifierTrainer classifierTrainer = new NaiveBayesTrainer(); 
Classifier classifier = classifierTrainer.train(instances); 

现在我们来看看这个更高级的类是如何工作的,并评估它在独立数据集上的性能。

模型性能

为了在独立数据集上评估分类器,我们将使用以下步骤:

  1. 让我们先导入位于我们的test文件夹中的电子邮件:
InstanceList testInstances = new 
   InstanceList(classifier.getInstancePipe()); 
folderIterator = new FileIterator( 
    new File[] {new File(testFolderPath)}, 
    new TxtFilter(), 
    FileIterator.LAST_DIRECTORY); 
  1. 我们将使用与训练期间初始化相同的管道传递数据:
testInstances.addThruPipe(folderIterator); 
  1. 为了评估分类器的性能,我们将使用cc.mallet.classify.Trial类,该类使用分类器和一组测试实例进行初始化:
Trial trial = new Trial(classifier, testInstances); 
  1. 评估在初始化时立即执行。然后我们可以简单地取出我们关心的度量。在我们的例子中,我们想检查分类垃圾邮件消息的精确率和召回率,或者 F 度量,它返回这两个值的调和平均值,如下所示:
System.out.println( 
  "F1 for class 'spam': " + trial.getF1("spam")); 
System.out.println( 
  "Precision:" + trial.getPrecision(1)); 
System.out.println( 
  "Recall:" + trial.getRecall(1)); 

评估对象输出以下结果:

    F1 for class 'spam': 0.9731800766283524
    Precision: 0.9694656488549618
    Recall: 0.9769230769230769

结果显示,该模型正确地发现了 97.69%的垃圾邮件(召回率),并且当它标记一封电子邮件为垃圾邮件时,它在 96.94%的情况下是正确的。换句话说,它大约每 100 封垃圾邮件中遗漏 2 封,并将每 100 封有效邮件中的 3 封标记为垃圾邮件。所以,它并不完美,但已经是一个很好的开始了!

摘要

在本章中,我们讨论了文本挖掘与传统的基于属性的学习的不同之处,需要大量的预处理步骤将书面自然语言转换为特征向量。此外,我们还讨论了如何利用 Mallet,一个基于 Java 的 NLP 库,通过将其应用于两个现实生活中的问题来应用它。首先,我们使用 LDA 模型在新闻语料库中建模主题,以构建一个能够为新文档分配主题的模型。我们还讨论了如何使用 BoW 表示法构建基于朴素贝叶斯的垃圾邮件过滤分类器。

本章总结了如何应用各种库来解决机器学习任务的演示。由于我们无法在许多地方涵盖更多有趣的应用和提供更多细节,下一章将给出一些进一步的学习指针,如何继续学习并深入了解特定主题。

第十一章:接下来是什么?

本章将带我们结束对 Java 库中机器学习的回顾之旅,并讨论如何利用它们解决实际问题。然而,这绝对不应该结束你的旅程。本章将为你提供一些实际的建议,告诉你如何开始将你的模型部署到现实世界,会遇到哪些挑战,以及如何进一步深化你的知识。它还提供了关于如何找到更多资源、材料、场所和技术以深入了解机器学习的进一步指导。

本章将涵盖以下主题:

  • 生活中机器学习的重要方面

  • 标准和标记语言

  • 云中的机器学习

  • 网络资源和竞赛

生活中的机器学习

论文、会议演示和演讲通常不会讨论模型如何在生产环境中实际部署和维护。在本节中,我们将探讨一些应该考虑的方面。

噪声数据

在实践中,数据通常由于各种原因(如测量错误、人为错误和专家在分类训练示例时的判断错误)而包含错误和不完美。我们将所有这些都称为噪声。噪声也可能来自处理缺失值时,一个具有未知属性值的示例被替换为一组与缺失值概率分布相对应的加权示例。噪声在学习数据中的典型后果是学习模型在新数据中的预测精度低,以及对于用户来说难以解释和理解的用户复杂模型。

类别不平衡

类别不平衡是我们遇到的问题,在第七章“欺诈和异常检测”中,目标是检测欺诈性保险索赔。挑战在于数据集的大部分,通常超过 90%,描述了正常活动,而只有一小部分数据集包含欺诈示例。在这种情况下,如果模型总是预测正常,那么它 90%的时间是正确的。这个问题在实践中非常普遍,可以在各种应用中观察到,包括欺诈检测、异常检测、医疗诊断、油泄漏检测和面部识别。

现在,我们已经了解了什么是类别不平衡问题以及为什么这是一个问题,接下来让我们看看如何处理这个问题。第一种方法是关注除分类准确率之外的措施,例如召回率、精确率和 F 度量。这些措施关注模型在预测少数类(召回率)方面的准确性以及误报的份额(精确率)。另一种方法基于重采样,其主要思想是以一种方式减少过度代表示例的数量,使得新的集合包含两个类别的平衡比例。

特征选择

特征选择可以说是建模过程中最具挑战性的部分,它需要领域知识和对当前问题的深刻洞察。尽管如此,良好特征的性质如下:

  • 可重用性:特征应该可用于在不同模型、应用程序和团队中重用。

  • 可转换性:你应该能够通过操作转换特征,例如,log()max(),或者通过自定义计算将多个特征组合在一起。

  • 可靠性:特征应该易于监控,并且应该存在适当的单元测试以最小化错误或问题。

  • 可解释性:要执行任何前面的操作,你需要能够理解特征的意义并解释它们的值。

你能更好地捕捉特征,你的结果就会越准确。

模型链

一些模型可能会产生输出,这些输出被用作另一个模型的特征。此外,我们可以使用多个模型——集成——将任何模型转换为特征。这是一种获得更好结果的好方法,但这也可能导致问题。必须注意,你的模型输出必须准备好接受依赖。同时,尽量避免反馈循环,因为它们可能会在管道中创建依赖和瓶颈。

评估的重要性

另一个重要方面是模型评估。除非你将你的模型应用于新数据并衡量业务目标,否则你并不是在进行预测分析。评估技术,如交叉验证和分离的培训/测试集,只是分割你的测试数据,这只能给你提供一个关于你的模型将如何表现的估计。生活中往往不会给你一个包含所有案例的培训数据集,因此在现实世界数据集中定义这两个集合需要很多创造力。

最后,我们希望提高业务目标,例如提高广告转化率,并增加推荐项目的点击量。为了衡量改进,执行 A/B 测试,测量在统计上相同的群体中不同算法体验的指标差异。产品的决策始终是数据驱动的。

A/B 测试是一种具有两个变体的随机实验方法:A,对应于原始版本,控制实验;B,对应于一个变体。此方法可用于确定变体是否优于原始版本。它可以用于测试从网站更改到销售电子邮件再到搜索广告的任何内容。Udacity 提供了一门免费课程,涵盖 A/B 测试的设计和分析,课程链接为 www.udacity.com/course/ab-testing--ud257

将模型投入生产

从在实验室中构建准确模型到将其部署到产品中,这一过程涉及数据科学和工程的协作,如下面的三个步骤所示:

  1. 数据研究和假设构建涉及建模问题和执行初始评估。

  2. 解决方案构建和实施是模型通过重写为更高效、稳定和可扩展的代码,找到其进入产品流程的方式。

  3. 在线评估是最后阶段,其中使用 A/B 测试在业务目标上使用实时数据评估模型。

这在以下图中得到了更好的说明:

模型维护

我们需要解决的另一个方面是如何维护模型。这是一个不会随时间变化的模型吗?它是建模一个动态现象,需要模型随时间调整其预测吗?

模型通常在离线批量训练中构建,然后用于实时数据以提供预测,如下所示。如果我们能够收到关于模型预测的反馈,例如,股票是否如模型预测的那样上涨,候选人是否对活动做出了回应,那么应该使用这些反馈来改进初始模型:

反馈对于改进初始模型可能非常有用,但请确保注意你正在采样的数据。例如,如果你有一个预测谁将响应活动的模型,你最初将使用一组具有特定响应/未响应分布和特征属性的随机联系的客户。模型将仅关注最有可能响应的客户子集,而你的反馈将返回给你一组已响应的客户。通过包含这些数据,模型在特定子组中更准确,但可能会完全错过其他一些组。我们称这个问题为探索与利用。一些解决这个问题的方法可以在 Osugi 等人(2005 年)和 Bondu 等人(2010 年)的研究中找到。

标准和标记语言

随着预测模型越来越普及,共享模型和完成建模过程的需求导致开发过程的正式化和可交换格式的出现。在本节中,我们将回顾两个事实上的标准,一个涵盖数据科学流程,另一个指定了在应用程序之间共享模型的可交换格式。

CRISP-DM

跨行业数据挖掘标准过程CRISP-DM)描述了一个在行业中常用的数据挖掘过程。CRISP-DM 将数据挖掘科学过程分为六个主要阶段:

  • 业务理解

  • 数据理解

  • 数据准备

  • 建模

  • 评估

  • 部署

在以下图中,箭头表示流程流程,它可以来回移动通过各个阶段。此外,流程不会随着模型的部署而停止。外部的箭头表示数据科学的循环性质。在过程中学到的经验可以触发新的问题,并重复流程以改进先前结果:

图片

SEMMA 方法论

另一种方法是 样本、探索、修改、建模和评估 (SEMMA)。SEMMA 描述了数据科学中的主要建模任务,同时忽略了数据理解和部署等业务方面。SEMMA 由 SAS Institute 开发,它是最大的统计软件供应商之一,旨在帮助其软件用户执行数据挖掘的核心任务。

预测模型标记语言

预测 模型标记语言 (PMML) 是一种基于 XML 的交换格式,它允许机器学习模型在应用程序和系统之间轻松共享。支持的模型包括逻辑回归、神经网络、决策树、朴素贝叶斯、回归模型等。一个典型的 PMML 文件包括以下部分:

  • 包含一般信息的标题

  • 数据字典,描述数据类型

  • 数据转换,指定标准化、离散化、聚合或自定义函数的步骤

  • 模型定义,包括参数

  • 列出模型使用的属性的架构挖掘

  • 允许对预测结果进行后处理的目标

  • 输出列出要输出的字段和其他后处理步骤

生成的 PMML 文件可以导入到任何 PMML 消费应用程序,例如 Zementis 自适应决策和 预测分析 (ADAPA) 和 通用 PMML 插件 (UPPI) 计分引擎;Weka,它内置了对回归、广义回归、神经网络、TreeModel、RuleSetModel 和 支持向量机 (SVM) 模型的支持;Spark,它可以导出 k-means 聚类、线性回归、岭回归、lasso 模型、二元逻辑模型和 SVM;以及级联,可以将 PMML 文件转换为 Apache Hadoop 上的应用程序。

PMML 的下一代是一个新兴的格式,称为 分析便携格式 (PFA),提供了一种在各个环境中部署完整工作流程的通用接口。

云端机器学习

设置一个能够随着数据量的增加而扩展的完整机器学习堆栈可能具有挑战性。最近一波的 软件即服务 (SaaS) 和 基础设施即服务 (IaaS) 范式已经溢出到机器学习领域。今天的趋势是将实际的数据预处理、建模和预测移动到云端环境,并专注于建模任务。

在本节中,我们将回顾一些提供算法、在特定领域已经训练好的预测模型以及支持数据科学团队协作工作流程的环境的具有前景的服务。

机器学习即服务

第一类是算法即服务,你将获得一个 API 或甚至图形用户界面来连接预先编程的数据科学管道组件:

  • Google Prediction API:它是最早通过其 Web API 引入预测服务的企业之一。该服务与谷歌云存储集成,作为数据存储。用户可以构建一个模型并调用 API 来获取预测。

  • BigML:它实现了一个用户友好的图形界面,支持许多存储提供商(例如,Amazon S3),并提供了一系列数据加工工具、算法和强大的可视化功能。

  • Microsoft Azure Machine Learning:它提供了一大批机器学习算法和数据加工函数的库,以及图形用户界面,以将这些组件连接到应用程序中。此外,它还提供了一项全托管服务,您可以使用它将预测模型部署为可消费的 Web 服务。

  • Amazon Machine Learning:它进入市场较晚。其主要优势是与其他亚马逊服务的无缝集成,而算法数量和用户界面需要进一步改进。

  • IBM Watson Analytics:它专注于提供针对特定领域(如语音识别、机器翻译和异常检测)已经手工定制的模型。它通过解决特定用例,针对广泛的行业。

  • Prediction.IO:它是一个自托管的开源平台,提供从数据存储到建模再到预测服务的一整套解决方案。Prediction.IO 可以与 Apache Spark 通信,以利用其学习算法。此外,它还附带了一系列针对特定领域的模型,例如推荐系统、流失预测等。

预测 API 是一个新兴的领域,因此这些只是其中一些知名的例子;KDnuggetswww.kdnuggets.com/2015/12/machine-learning-data-science-apis.html上整理了一份包含 50 个机器学习 API 的列表。

想要了解更多信息,您可以访问 PAPI,即预测 API 和应用的国际化会议,网址为www.papi.io,或者查看这本书:《Bootstrapping Machine Learning》,作者:L Dorard,出版社:Createspace Independent Pub,2014 年

网络资源和竞赛

在本节中,我们将回顾如何找到学习、讨论、展示或提高我们数据科学技能的额外资源。

数据集

加州大学欧文分校托管了最知名的机器学习数据集存储库之一。UCI 存储库包含超过 300 个数据集,涵盖了各种挑战,包括扑克、电影、葡萄酒质量、活动识别、股票、出租车服务轨迹、广告等。每个数据集通常都配备了一篇研究论文,其中描述了数据集的使用情况,这可以为您如何开始以及预测基线是什么提供线索。

UCI 机器学习仓库可通过archive.ics.uci.edu访问,如下所示:

图片

由 Xiaming Chen 维护的另一个良好维护的集合托管在 GitHub 上:github.com/caesar0301/awesome-public-datasets.

这个令人惊叹的公共数据集仓库维护着来自多个领域的超过 400 个数据源的链接,包括农业、生物学、经济学、心理学、博物馆和交通。针对机器学习的特定数据集收集在图像处理、机器学习和数据挑战部分。

在线课程

由于在线课程的可获得性,学习如何成为一名数据科学家变得更加容易。以下是一些在线学习不同技能的免费资源列表:

以下是一些关于机器学习的在线课程,可以帮助你了解更多:

  • Coursera——Andrew Ng 的机器学习(斯坦福)课程:这门课程教你许多机器学习算法背后的数学知识,解释它们是如何工作的,并探讨为什么它们是有意义的,网址为www.coursera.org/learn/machine-learning.

  • 统计学 110(哈佛)由 Joe Biltzstein 教授:这门课程让你发现你在数据科学旅程中会多次听到的相关术语的概率。讲座可在 YouTube 上观看,网址为projects.iq.harvard.edu/stat110/youtube.

  • 数据科学 CS109(哈佛)由 John A. Paulson 教授:这是一门实践课程,你将学习数据科学领域的 Python 库,以及如何处理机器学习算法,网址为cs109.github.io/2015/.

竞赛

提升知识的最佳方式是解决实际问题,如果您想建立一个经过验证的项目组合,机器学习竞赛是一个可行的起点:

  • Kaggle: 这是排名第一的竞赛平台,提供各种具有高额奖金的挑战,强大的数据科学社区,以及大量的有用资源。您可以在www.kaggle.com/查看。

  • CrowdANALYTIX: 这是一个专注于生命科学和金融服务行业的众包数据分析服务,网址为www.crowdanalytix.com.

  • DrivenData:这是一个举办旨在社会公益的数据科学竞赛的网站www.drivendata.org/

网站和博客

除了在线课程和竞赛之外,还有许多网站和博客发布数据科学社区的最新发展,他们在解决不同问题中的经验,或者只是最佳实践。以下是一些好的起点:

  • KDnuggets:这是数据挖掘、分析、大数据和数据科学的实际门户,涵盖了最新的新闻、故事、事件和其他相关议题www.kdnuggets.com/

  • 机器学习掌握:这是一个入门级博客,提供实用的建议和起点指南。你可以在machinelearningmastery.com/查看。

  • 数据科学中心:这是一个包含各种主题、算法、缓存和商业案例的实用社区文章,更多信息请访问www.datasciencecentral.com/

  • 数据挖掘研究(作者:Sandro Saitta),更多信息请访问www.dataminingblog.com/

  • 《数据挖掘:文本挖掘、可视化和社交媒体》(作者:Matthew Hurst),涵盖了有趣的文本和网页挖掘主题,通常还涉及 Bing 和微软的应用,更多信息请访问datamining.typepad.com/data_mining/

  • Geeking with Greg(作者:Greg Linden,亚马逊推荐引擎的发明者和互联网企业家)。你可以在glinden.blogspot.si/查看。

  • DSGuide:这是一个包含 150 多个数据科学博客的集合dsguide.biz/reader/sources

场地和会议

以下是一些顶级学术会议,它们展示了最新的算法:

  • 数据库中的知识发现KDD

  • 计算机视觉和模式识别CVPR

  • 神经信息处理系统年度会议NIPS

  • 国际机器学习会议ICML

  • 国际数据挖掘会议ICDM

  • 国际普适和泛在计算联合会议UbiComp

  • 国际人工智能联合会议IJCAI

以下是一些商业会议:

  • O'Reilly Strata 会议

  • Strata + Hadoop 世界会议

  • 预测分析世界

  • MLconf

你还可以查看当地的聚会小组。

摘要

在本章中,我们通过讨论模型部署的一些方面来结束本书,我们探讨了数据科学流程的标准和可互换的预测模型格式。我们还回顾了在线课程、竞赛、网络资源和会议,这些都可以帮助你在掌握机器学习艺术的道路上。

我希望这本书能激发你深入探索数据科学的兴趣,并激励你亲自动手,尝试各种库,掌握如何攻击不同的问题。记住,所有的源代码和附加资源都可以在 Packt Publishing 网站上找到:www.packtpub.com/.

posted @ 2025-09-03 10:08  绝不原创的飞龙  阅读(6)  评论(0)    收藏  举报