Python-机器学习系统设计指南-全-

Python 机器学习系统设计指南(全)

原文:Designing Machine Learning Systems with Python

协议:CC BY-NC-SA 4.0

零、前言

机器学习是计算世界已经看到的最大趋势之一。机器学习系统具有深刻而令人兴奋的能力,能够为各种惊人的应用提供重要见解,从开创性和拯救生命的医学研究到发现我们宇宙的基本物理方面;从为我们提供更好、更清洁的食物到网络分析和经济建模。事实上,我们生活中几乎没有任何一个领域不被这项技术以某种方式触及。每个人都想进入机器学习领域,为了在这个领域获得足够的认可,必须能够理解和设计一个服务于项目需求的机器学习系统。

这本书涵盖了什么

第 1 章机器学习中的思考,让你从机器学习的基础知识开始,正如标题所说,它将帮助你在机器学习范式中思考。您将学习机器学习中涉及的设计原则和各种模型。

第二章工具与技术解释了 Python 自带了一个大型的机器学习任务包库。这一章将给你一些大型图书馆的味道。它将涵盖诸如 NumPy、SciPy、Matplotlib 和 Scilit-learn 之类的包。

第 3 章将数据转化为信息解释了原始数据可以有多种不同的格式,可以有不同的数量和质量。有时,我们被数据淹没,有时我们努力从数据中获取每一滴信息。数据要成为信息,需要一些有意义的结构。在本章中,我们将介绍一些广泛的主题,如大数据、数据属性、数据源以及数据处理和分析。

第 4 章模型——从信息中学习,带你浏览逻辑模型——在这里我们探索一种逻辑语言并创建一个假设空间映射,树模型——在这里我们会发现它们可以应用于广泛的任务,并且既有描述性又易于解释;和规则模型——我们讨论基于有序规则列表和无序规则集的模型。

第 5 章线性模型介绍了最广泛使用的模型之一,它构成了许多先进非线性技术的基础,例如支持向量机和神经网络。在本章中,我们将研究机器学习中一些最常用的技术。我们将为线性和逻辑回归创建假设表示。

第六章神经网络,介绍人工神经网络强大的机器学习算法。我们将看到这些网络是如何成为大脑中神经元的简化模型的。

第 7 章特征–算法如何看待世界,介绍了不同类型的特征–数量特征、顺序特征和分类特征。我们还将详细学习结构化和转换特征。

第 8 章用集成学习解释了创建机器学习集成的动机背后的原因,这种动机来自于清晰的直觉,并建立在丰富的理论历史基础上。可以创建的机器学习集成的类型和模型本身一样多种多样,主要考虑围绕三件事:我们如何划分数据,我们如何选择模型,以及我们使用什么方法来组合它们的结果。

第 9 章设计策略和案例研究介绍了一些设计策略,以确保机器学习应用的最佳性能。我们将学习模型选择和参数调整技术,并将它们应用到几个案例研究中。

这本书你需要什么

你所需要的只是一种学习机器学习和 Python V3 软件的倾向,你可以从https://www.python.org/downloads/下载。

这本书是给谁的

这本书是为数据科学家,科学家,或只是好奇。你需要知道一些线性代数和一些 Python。你需要有一些机器学习概念的基础知识。

惯例

在这本书里,你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。

文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄如下所示:“NumPy 使用一个dtype对象来描述数据的各个方面。”

任何命令行输入或输出都编写如下:

import numpy as np
import matplotlib.pyplot as plt

x = np.arange(0., 5., 0.2)
plt.plot(x, x**4, 'r', x, x*90, 'bs', x, x**3, 'g^')
plt.show()

新名词重要词语以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,出现在文本中,如下所示:“单击下一步按钮,您将进入下一个屏幕。”

警告或重要提示会出现在这样的框中。

型式

提示和技巧是这样出现的。

读者反馈

我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它有助于我们开发出你真正能从中获益的标题。

要给我们发送一般反馈,只需发送电子邮件<[feedback@packtpub.com](mailto:feedback@packtpub.com)>,并在您的邮件主题中提及书名。

如果你对某个主题有专业知识,并且对写作或投稿感兴趣,请参见我们位于www.packtpub.com/authors的作者指南。

客户支持

现在,您已经自豪地拥有了一本书,我们有许多东西可以帮助您从购买中获得最大收益。

下载示例代码

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

您可以按照以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。
  2. 将鼠标指针悬停在顶部的 SUPPORT 选项卡上。
  3. 点击代码下载&勘误表
  4. 搜索框中输入图书名称。
  5. 选择要下载代码文件的书籍。
  6. 从您购买这本书的下拉菜单中选择。
  7. 点击代码下载

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

  • 视窗系统的 WinRAR / 7-Zip
  • zipeg/izp/un ARX for MAC
  • 适用于 Linux 的 7-Zip / PeaZip

勘误表

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

要查看之前提交的勘误表,请前往https://www.packtpub.com/books/content/support并在搜索栏中输入图书名称。所需信息将出现在勘误表部分。

盗版

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

请通过<[copyright@packtpub.com](mailto:copyright@packtpub.com)>联系我们,获取疑似盗版资料的链接。

我们感谢您在保护我们的作者方面的帮助,以及我们为您带来有价值内容的能力。

问题

如果您对本书的任何方面有问题,可以在<[questions@packtpub.com](mailto:questions@packtpub.com)>联系我们,我们将尽最大努力解决问题。

一、机器学习中的思维

机器学习系统具有深刻而令人兴奋的能力,能够为各种各样的应用提供重要的见解;从开创性和拯救生命的医学研究,到发现我们宇宙的基本物理方面。从为我们提供更好、更清洁的食物,到网络分析和经济建模。事实上,我们生活中几乎没有任何领域没有被这项技术以某种方式触及。随着物联网的扩展,产生了惊人数量的数据,很明显,智能系统正在以相当戏剧性的方式改变社会。有了开源工具,例如 Python 及其库提供的工具,以及以 Web 为代表的不断增长的开源知识库,以新的令人兴奋的方式学习和应用这项技术相对容易且便宜。在本章中,我们将涵盖以下主题:

  • 人类接口
  • 设计原则
  • 模型
  • 统一建模语言

人机界面

对于那些年龄足够大,或者不幸使用了早期版本的微软办公套件的人来说,你们可能会记得 Clippy 先生的办公助手。这项功能最早是在 Office 97 中引入的,每当您在文档开头键入“亲爱的”一词时,它都会不请自来地从计算机屏幕的右下角弹出,并提示“看起来您正在写信,您需要帮助吗?”。

在早期版本的 Office 中默认打开的 Clippy 先生几乎被该软件的用户普遍嘲笑,并可能作为机器学习的第一个大失败而载入史册。

那么,为什么快乐的克利珀先生这么讨厌呢?显然,处于消费软件开发前沿的微软员工并不愚蠢,自动化助理可以帮助完成日常办公任务的想法并不一定是个坏主意。事实上,自动化助理的后期化身,至少是最好的自动化助理,在背景下无缝运行,并显著提高了工作效率。考虑预测文本。有很多例子,有些非常有趣,说明预测文本出现了惊人的错误,但在大多数情况下,它并没有失败,它没有被注意到。它只是成为我们正常工作流程的一部分。

此时,我们需要区分错误和失败。Clippy 先生之所以失败,是因为它过于张扬且设计拙劣,不一定是因为它出错了;也就是说,它可以提出正确的建议,但很可能你已经知道你正在写信。预测文本有很高的错误率,也就是说,它经常把预测弄错,但它不会失败,主要是因为它被设计成失败的方式:不引人注目。

用系统工程的话来说,任何具有紧密耦合的人机界面的系统的设计都是困难的。人类的行为,就像一般的自然世界一样,不是我们总能预测到的。表情识别系统、自然语言处理和手势识别技术等等,都开辟了人机交互的新途径,这对机器学习专家来说有着重要的应用。

每当我们设计一个需要人工输入的系统时,我们需要预测可能的方式,而不仅仅是预期的方式,人类将与系统交互。本质上,我们试图用这些系统来做的是向它们灌输对人类经验的广阔全景的一些理解。

在网络的最初几年,搜索引擎使用一个简单的系统,该系统基于搜索词在文章中出现的次数。Web 开发人员很快开始通过增加关键搜索词的数量来玩这个系统。显然,这将导致关键词军备竞赛,并导致一个非常无聊的网络。衡量优质入站链接数量的页面排名系统旨在提供更准确的搜索结果。当然,现在现代搜索引擎使用更复杂和秘密的算法。

对于 ML 设计者来说,同样重要的是生成的数据量不断增加。这带来了几个挑战,最明显的是它的巨大。然而,算法在提取知识和见解方面的能力是巨大的,而这在较小的数据集上是不可能的。因此,许多人类互动现在已经数字化,我们才刚刚开始了解和探索这些数据可以被使用的许多方式。

作为一个好奇的例子,考虑研究20世纪书籍中的情绪表达(Acerbi 等人,2013)。虽然严格来说更多的是数据分析研究,而不是机器学习,但它是说明性的,原因有几个。它的目的是用情绪评分的方式,从 20 世纪的书籍中提取文本,绘制情感内容的图表。通过古腾堡数字图书馆项目、WordNet(http://wordnet.princeton.edu/wordnet/)和谷歌的 Ngram 数据库(books.google.com/ngrams)获得大量数字化文本后,这项研究的作者能够绘制出 20 世纪文化变迁的地图,反映在当时的文献中。他们通过描绘语气词语的使用趋势来做到这一点。

在这项研究中,作者将每个单词(标记为 1 克,并将其与情绪评分和发表年份联系起来。我们可以看到,情绪词,如快乐、悲伤、恐惧等,可以根据它们唤起的积极或消极情绪来评分。心情评分来自 WordNet(wordnet.princeton.edu)。WordNet 为每个情绪词分配一个情感分数。最后,作者简单统计了每个情绪词的出现次数:

The human interface

这里, ci 是特定语气词的计数, n 是语气词的总计数(不是所有的词,只是带有语气分数的词)CtheT7】是文字中 the 的计数。这将总和正常化,以考虑到一些年份更多的书被写入(或数字化)。此外,由于许多后来的书倾向于包含更多的技术语言,单词被用来规范化,而不是得到总字数。这在散文文本中给出了一个更准确的长时间的情感表达。最后,根据正态分布 M z 通过减去平均值并除以标准差来归一化得分。**

*The human interface

这个数字取自 20 世纪 T2 图书公司(阿尔伯托·阿西尔比、瓦西利奥斯·兰波斯、菲利普·加内特、r·亚历山大·本特利)PLOS 的《情感的表达》。

在这里,我们可以看到这项研究生成的图表之一。它显示了这一时期所写书籍的喜悲评分,并清楚地显示了与二战时期相关的负面趋势。

这项研究之所以有趣,有几个原因。首先,它是数据驱动科学的一个例子,以前被认为是软的科学,如社会学和人类学,被赋予了坚实的经验基础。尽管有一些令人印象深刻的结果,这项研究相对容易实施。这主要是因为大部分艰苦的工作已经由 WordNet 和谷歌完成了。这突出了如何使用互联网上免费提供的数据资源,以及软件工具,如 Python 的数据和机器学习包,任何有数据技能和动机的人都可以在这项工作的基础上再接再厉。

设计原则

系统设计和设计其他东西,比如房子,经常会有相似之处。这个类比在一定程度上成立。我们试图将设计组件放入符合规范的结构中。当我们考虑他们各自的操作环境时,这个类比就失效了。在房屋的设计中,一般认为景观一旦形成,就不会改变。

软件环境略有不同。系统是互动和动态的。我们设计的任何系统都将嵌套在其他系统中,无论是电子系统、物理系统还是人类系统。同样,计算机网络中的不同层(应用层、传输层、物理层等)嵌套了不同的含义和功能集,因此在项目的不同级别执行活动。

作为这些系统的设计者,我们还必须对环境有强烈的意识,也就是我们工作的领域。这些知识为我们提供了数据模式的线索,并帮助我们为工作提供背景。

机器学习项目可以分为五个截然不同的活动,如下所示:

  • 定义对象和规范
  • 准备和探索数据
  • 模型结构
  • 履行
  • 测试
  • 部署

设计师主要关心前三个。然而,他们经常在其他活动中发挥重要作用,而且在许多项目中必须发挥重要作用。还应该说,项目的时间表不一定是这些活动的线性序列。重要的是,它们是不同的活动。它们可能相互平行发生,也可能以其他方式相互作用,但它们通常涉及不同类型的任务,这些任务可以在人力和其他资源、项目阶段和外部性方面分开。此外,我们需要考虑不同的活动涉及不同的运作模式。当你勾画出一个想法时,考虑一下你的大脑工作的不同方式,与你从事一个特定的分析任务,比如一段代码相比。

通常,最难的问题是从哪里开始。我们可以开始钻研一个问题的不同元素,有一个特征集的想法,也许还有一个我们可能使用的模型的想法。这可能会导致一个定义的对象和规范,或者我们可能不得不做一些初步的研究,比如检查可能的数据集和来源、可用的技术,或者与系统的其他工程师、技术人员和用户交谈。我们需要探索经营环境和各种制约因素;它是 web 应用的一部分,还是科学家的实验室研究工具?

在设计的早期阶段,我们的工作流程会在处理不同的元素之间切换。例如,我们从一个一般的问题开始——也许有一个任务的想法,或者解决它所必需的任务——然后我们把它分成我们认为是关键的特征,用一个玩具数据集在几个模型上尝试它,回去细化特征集,调整我们的模型,精确地定义任务,并细化模型。当我们觉得我们的系统足够健壮时,我们可以用一些真实的数据来测试它。当然,然后我们可能需要回去更改我们的功能集。

选择和优化特征通常是机器学习设计者的一项主要活动(实际上,本身就是一项任务)。在充分描述任务之前,我们无法真正决定我们需要什么特征,当然,任务和特征都受到我们可以构建的可行模型类型的限制。

问题类型

作为设计师,我们被要求解决一个问题。我们得到了一些数据和一个预期的输出。第一步是以一种机器可以理解的方式,以一种对人类有意义的方式来构建问题。我们可以采用以下六种方法来精确定义我们的机器学习问题:

  • 探索性:这里我们分析数据,寻找一个趋势或者变量之间的关系等模式。探索通常会导致一种假设,比如将饮食与疾病联系起来,或者将犯罪率与城市住宅联系起来。
  • 描述性:这里我们尝试总结一下我们数据的具体特征。例如,平均寿命,平均温度,或人口中左撇子的数量。
  • 推理:推理问题是一个试图支持假设的问题,例如,通过使用不同的数据集来证明(或否定)预期寿命和收入之间的一般联系。
  • 预测性:在这里,我们正在尝试预测未来的行为。例如,通过分析收入来预测预期寿命。
  • 偶然的:这是一次的尝试,试图找出是什么导致了一些事情。低收入会导致预期寿命降低吗?
  • 机械论:这试图回答诸如“是什么机制将收入与预期寿命联系起来?”

大多数机器学习问题在开发过程中都会涉及到这几种类型的问题。例如,我们可以首先探索数据,寻找模式或趋势,然后描述数据的某些关键特征。这可能使我们能够做出预测,并找到特定问题背后的原因或机制。

你问的问题对吗?

这个问题在它的主题领域里必须是合理的和有意义的。这些领域知识使你能够理解数据中重要的东西,并看到某个模式或相关性在哪里有意义。

问题应该尽可能具体,同时仍然给出有意义的答案。它通常以一个概括的说法开始,比如“我想知道富有是否意味着健康”。因此,你做一些进一步的研究,发现你可以得到按地理区域划分的财富统计数据,比如说从税务局。我们可以通过健康的反义词来衡量健康,也就是疾病,比如住院人数,我们可以通过将疾病与地理区域联系起来来检验我们最初的主张,“富裕意味着健康”。我们可以看到,一个更具体的问题依赖于几个可能有问题的假设。

我们还应该考虑到,我们的结果可能会被这样一个事实所混淆,即穷人可能没有医疗保险,因此尽管生病,他们去医院的可能性也更小。我们想要发现的东西和我们试图测量的东西之间是相互作用的。这种互动或许隐藏了真实的患病率。然而,一切都没有失去。因为我们知道这些事情,那么也许我们可以在我们的模型中解释它们。

通过尽可能多地了解我们正在从事的领域,我们可以让事情变得更容易。

你可以通过检查你正在问的问题,或者其中的一部分,是否已经被回答,或者是否有数据集可以帮助你解决这个问题,从而节省很多时间。通常,你必须同时从几个不同的角度来处理一个问题。尽可能多做准备性研究。很有可能其他设计师已经完成了可以给你自己带来启发的工作。

任务

任务是在一段时间内进行的特定活动。我们必须区分人工任务(规划、设计和实现)和机器任务(分类、聚类、回归等等)。还要考虑当人和机器重叠时,例如,在为模型选择特征时。我们在机器学习中的真正目标是尽可能多地将这些任务从人工任务转换为机器任务。

将现实世界的问题与特定的任务相匹配并不总是容易的。许多现实世界的问题看似在概念上有联系,但需要截然不同的解决方案。或者,看起来完全不同的问题可能需要类似的方法。不幸的是,没有简单的查找表来匹配特定的任务和问题。很大程度上取决于设置和领域。一个领域的类似问题在另一个领域可能无法解决,也许是因为缺乏数据。然而,有少量的任务应用于大量的方法来解决许多最常见的问题类型。换句话说,在所有可能的编程任务的空间中,有一个对我们的特定问题有用的任务子集。在这个子集中,有一个较小的任务子集,这些任务很容易,实际上可以有效地应用于我们的问题。

机器学习任务发生在三大设置中:

  • 监督学习:这里的目标是从标记的训练数据中学习一个模型,该模型允许对看不见的未来数据进行预测。
  • 无监督学习:这里我们处理未标记的数据,我们的目标是在这个数据中找到隐藏的模式,提取有意义的信息。
  • 强化学习:这里的目标是开发一个系统,基于它与环境的交互来提高它的性能。这通常包括一个奖励信号。这类似于监督学习,只是强化学习使用奖励函数来不断提高其性能,而不是有一个标记的训练集。

现在,让我们来看看一些主要的机器学习任务。下图应该给你一个起点,让你尝试并决定什么类型的任务适合不同的机器学习问题:

Tasks

分类

分类可能是最常见的任务类型;这在一定程度上是因为它相对容易,容易理解,并且解决了许多常见的问题。分类是指根据一组实例的特征为其分配类。这是一种监督学习方法,因为它依赖于一个标记的训练集来学习一组模型参数。然后,该模型可以应用于未标记的数据,以预测每个实例属于哪个类。大致有两种分类任务:二进制分类和 T5】多类分类。典型的二进制分类任务是电子邮件垃圾邮件检测。在这里,我们使用电子邮件的内容来确定它是否属于两个类别之一:垃圾邮件还是非垃圾邮件。多类分类的一个例子是手写识别,我们试图预测一个类,例如,字母名称。在这种情况下,我们对每个字母数字字符都有一个类。多类分类有时可以通过将二进制分类任务链接在一起来实现,然而,我们以这种方式丢失信息,并且我们无法定义单个决策边界。因此,多类分类通常与二类分类分开处理。

回归

有些情况下,我们感兴趣的不是离散类,而是连续变量,例如概率。这些类型的问题是回归问题。回归分析的目的是了解输入、自变量的变化如何影响因变量的变化。最简单的回归问题是线性的,包括将一条直线拟合到一组数据上,以便进行预测。这通常是通过最小化训练集中每个实例的误差平方和来实现的。典型的回归问题包括根据症状的范围和严重程度估计疾病的可能性,或者根据过去的表现预测考试成绩。

聚类

聚类是最著名的无监督方法。在这里,我们关注的是对未标记数据集中的实例之间的相似性进行度量。我们经常使用几何模型根据实例的特征值来确定实例之间的距离。我们可以使用任意的接近度来确定每个实例属于哪个集群。聚类常用于数据挖掘和探索性数据分析。有各种各样的方法和算法来执行这项任务,其中一些方法包括基于距离的方法,以及为每个聚类找到一个中心点,或者使用基于分布的统计技术。

与聚类相关的是关联;这是一项无人监督的任务,要在数据中找到某种类型的模式。这项任务背后是产品推荐系统,如亚马逊和其他在线商店提供的产品推荐系统。

降维

许多数据集包含大量与每个实例相关的特征或测量值。这可能会在计算能力和内存分配方面带来挑战。同样,许多特征可能包含冗余信息或与其他特征相关的信息。在这些情况下,我们的学习模型的性能可能会显著下降。降维最常用于特征前置;它将数据压缩到较低维度的子空间中,同时保留有用的信息。当我们想要可视化数据时,通常通过将更高维度投影到一维、二维或三维上,也可以使用降维。

从这些基本的机器任务中,有许多衍生的任务。在许多应用中,这可能只是将学习模型应用于预测,以建立偶然的关系。我们必须记住解释和预测是不一样的。一个模型可以做出预测,但除非我们明确知道它是如何做出预测的,否则我们无法开始形成一个可理解的解释。解释需要人类对这个领域的知识。

我们还可以使用预测模型从一般模式中寻找异常。在这里,我们对偏离预测的个别情况感兴趣。这通常被称为异常检测在检测银行欺诈、噪声过滤,甚至寻找外星生命等方面有着广泛的应用。

一个重要且潜在有用的任务是子群发现。我们在这里的目标不是像在集群中那样,划分整个域,而是找到一个具有实质上不同分布的子组。本质上,子群发现是试图找到一个因变量和许多独立解释变量之间的关系。我们并不是试图找到一个完整的关系,而是一组不同的实例,这些实例在某些方面对领域很重要。例如,建立一个亚组,吸烟者=真家族史=真作为心脏病的目标变量=真

最后,我们考虑控制型任务。给定不同的条件,这些行为优化控制设置,以使收益最大化。这可以通过几种方式实现。我们可以克隆专家的行为:机器直接从人类那里学习,并在给定不同条件下对行动做出预测。任务是学习专家行为的预测模型。这类似于强化学习,任务是学习条件和最佳行动之间的关系。

错误

在机器学习系统中,软件缺陷会带来非常严重的现实后果;如果你嵌入流水线机器人的算法将人归类为生产组件,会发生什么?显然,在关键系统中,你需要为失败做好准备。在你的设计过程和系统中应该有一个健壮的故障和错误检测程序。

有时为了调试和检查逻辑缺陷,有必要设计非常复杂的系统。可能需要生成具有特定统计结构的数据集,或者创建人工人类来模拟界面。例如,开发一种方法来验证您的设计在数据、模型和任务级别的逻辑是否合理。错误可能很难追踪,作为一名科学家,你必须假设有错误,并试图证明不是这样。

识别并优雅地捕捉错误的想法对于软件设计者来说很重要,但是作为机器学习系统的设计者,我们必须更进一步。我们需要能够在我们的模型中捕捉从错误中学习的能力。

必须考虑我们如何选择测试集,特别是它在数据集的其余部分中的代表性。例如,如果与训练集相比它是有噪声的,那么它将在测试集上给出较差的结果,这表明我们的模型是过度拟合的,而事实上并非如此。为了避免这种情况,使用了交叉验证的过程。例如,这是通过将数据随机分成十个大小相等的块来实现的。我们使用九个组块来训练模型,一个用于测试。我们这样做 10 次,用每个组块测试一次。最后,我们取测试集性能的平均值。除了分类之外,交叉验证还用于其他有监督的学习问题,但是正如您所料,无监督的学习问题需要进行不同的评估。

对于无监督的任务,我们没有带标签的训练集。因此,评估可能有点棘手,因为我们不知道正确答案是什么样子的。例如,在一个聚类问题中,我们可以通过度量来比较不同模型的质量,例如聚类直径与聚类之间的距离之比。然而,在任何复杂的问题中,我们永远无法判断是否还有另一个模型尚未构建,哪个更好。

优化

优化问题在许多不同的领域无处不在,如金融、商业、管理、科学、数学和工程。优化问题包括以下内容:

  • 我们希望最大化或最小化的目标函数。
  • 决策变量,即一组可控输入。为了满足目标函数,这些输入在指定的约束条件内变化。
  • 不可控或固定输入的参数。
  • 约束是决策变量和参数之间的关系。它们定义了决策变量可以有哪些值。

大多数优化问题只有一个目标函数。在我们可能有多个目标函数的情况下,我们经常发现它们相互冲突,例如,降低成本和增加产量。在实践中,我们试图将多个目标重新表述为一个单一的函数,也许是通过创建目标函数的加权组合。在我们的成本和产出例子中,一个单位成本的变量可能会起作用。

决策变量是我们为了实现目标而控制的变量。它们可能包括资源或劳动力之类的东西。对于模型的每次运行,模块的参数是固定的。我们可以使用几个情况,在这里我们选择不同的参数来测试多个条件下的变化。

对于许多不同类型的优化问题,有成千上万种求解算法。其中大部分涉及到首先找到一个可行的解决方案,然后通过调整决策变量来迭代地改进它,希望找到一个最优的解决方案。许多优化问题可以用线性规划技术很好地解决。他们假设目标函数和所有约束对于决策变量是线性的。当这些关系不是线性时,我们通常使用合适的二次函数。如果系统是非线性的,那么目标函数可能不是凸的。也就是说,它可能有多个局部极小值,并且不能保证局部极小值是全局极小值。

线性规划

为什么线性模型如此普遍?首先,它们相对容易理解和实现。它们基于一个建立在 17 世纪中期的数学理论,这个理论后来在数字计算机的发展中发挥了关键作用。计算机的独特任务是实现线性程序,因为计算机在很大程度上是基于线性编程理论来概念化的。线性函数总是凸的,这意味着它们只有一个最小值。线性规划 ( 线性规划)问题通常用单纯形法求解。假设我们想要解决优化问题,我们将使用以下语法:

最大 x1T3】+x2T7】带约束:2x1T11】+x2≤4x1T19】+*2x2≤3*****

我们假设x1T3x2T7】大于等于 0。我们需要做的第一件事是将其转换为标准形式。这是通过保证问题是一个最大化问题来完成的,也就是我们把 min z 转换为 max -z 。我们还需要通过添加非负松弛变量将不等式转化为等式。这里的例子已经是一个最大化问题,所以我们可以保持目标函数不变。我们确实需要将约束中的不平等改变为平等:**

2x1T3】+x2T7】+x3= 4x1T15】+2x2T19】+*x4= 3*****

如果我们让 z cf 目标函数,那么我们可以写出以下内容:

z-x1-x2= 0

我们现在有以下线性方程组:

  • 目标:z-x1-x2+0+0 = 0
  • 约束 1:2x1+x2+x3+0 = 4
  • 约束 2:x1+2x2T7】+0+x4= 3**

我们的目标是最大化 z ,记住所有变量都是非负的。我们可以看到x1T5x2T9】出现在所有方程中,被称为非基本方程。 x 3x 4 值各只出现在一个等式中。它们被称为基本变量。我们可以通过将所有非基本变量赋给 0 来找到一个基本解。这里,这给了我们以下内容:**

x1= x2= 0x3= 4x4= 3z = 0

记住我们的目标是最大化 z ,这是最佳解决方案吗?我们可以看到由于 z 在我们的线性系统的第一个方程中减去 x 1x 2 ,我们能够增加这些变量。如果这个方程中的系数都是非负的,那么就没有办法增加 z 。我们将知道,当目标方程中的所有系数都为正时,我们已经找到了最优解。

这里不是这样。所以,我们取目标方程中一个负系数的非基本变量(比如说 x 1 ,叫做 进入变量),用一个叫做旋转的技巧,把它从非基本变成基本变量。同时,我们将把一个基本变量,叫做 离开变量,变成一个非基本变量。我们可以看到 x 1 出现在两个约束方程中,那么我们选择哪一个做枢轴呢?记住我们需要保持系数为正。我们发现,通过使用枢轴元素,我们可以找到另一个基本解,该元素产生方程右侧与其各自输入系数的最低比率。对于 x 1 ,在本例中,第一个约束为 4/2 ,第二个约束为 3/1 。因此,我们将在约束 1 中使用 x 1 进行枢轴旋转。

我们将约束 1 除以 2 ,得到如下结果:

x1+x【2】+x= 2

我们现在可以用 x 1 来写这个,然后代入其他方程,从那些方程中去掉 x1 。一旦我们完成了一点代数运算,我们就得到下面的线性系统:

z-1/2x2+1/3 x3= 2

x1T3+1/2 x2T7+1/2x3= 2**

3/2x2–1/2x3+x4= 1

我们有另一个基本解决方案。但是,这是最优解吗?由于我们在第一个方程中还有一个负系数,所以答案是否定的,我们现在可以和 x2 进行同样的枢转过程,利用比值法则,我们发现可以在第三个方程中的 3/2x2 上进行枢转。这给了我们以下信息:

z+1/3x3T3】+1/3x4= 7/3**

x1+2/3x3-1/3 x4= 5/3

x2-1/3x3T3+2/3 x4= 2/3**

这就给了我们 x 3 = x 4 = 0x 1 = 5/3x2 = 2/3z = 7/3 的解决方案。这是最优解,因为在第一个方程中没有更多的否定。

我们可以用下面的图表来形象化这一点。阴影区域是我们将找到可行解决方案的区域:

Linear programming

双变量优化问题

车型

线性编程为我们提供了一种将现实世界的问题编码成计算机语言的策略。然而,我们必须记住,我们的目标不仅仅是解决一个问题的实例,而是创建一个模型,从新的数据中解决独特的问题。这就是学习的本质。一个学习模型必须有一个机制来评估它的输出,反过来,改变它的行为到一个更接近解决方案的状态。

这个模型本质上是一个假设,也就是对一个现象提出的解释。目标是让它对问题进行概括。在监督学习问题的情况下,从训练集中获得的知识被应用于未标记的测试。在无监督学习问题的情况下,例如聚类,系统不从训练集中学习。它必须从数据集本身的特征中学习,比如相似度。在这两种情况下,过程都是迭代的。它重复一组定义明确的任务,这使模型更接近正确的假设。

模型是机器学习系统的核心。他们正在学习什么。有许多模型,这些模型有许多变化,也有独特的解决方案。我们可以看到机器学习系统解决的问题(回归、分类、关联等)出现在许多不同的设置中。它们已经成功地应用于科学、工程、数学、商业的几乎所有分支,以及社会科学;他们和他们经营的领域一样多样化。

这种模型的多样性给了机器学习系统很大的解决问题的能力。然而,对于设计师来说,决定哪一个或哪几个模型是解决特定问题的最佳模型也可能有点令人生畏。让事情变得复杂的是,通常有几个模型可以解决你的任务,或者你的任务可能需要几个模型。当你着手这样一个项目时,你根本不知道哪一个是解决原始问题的最准确和最有效的途径。

为了我们在这里的目的,让我们把这个广阔的画布分成三个重叠的、非相互的和排他的类别:几何的、概率的和逻辑的。在这三个模型中,必须区分模型如何划分实例空间。实例空间可以被视为数据的所有可能实例,无论每个实例是否出现在数据中。实际数据是实例空间的空间子集。

划分这个空间有两种方法:分组和分级。两者的主要区别在于分组模型将实例空间划分为称为的固定离散单元。它们的分辨率是有限的,无法区分超过这个分辨率的类。另一方面,放坡会在整个实例空间上形成一个全局模型,而不是将空间分割成段。理论上,它们的分辨率是无限的,无论实例有多相似,它们都可以区分它们。分组和分级之间的区别不是绝对的,许多模型都包含两者的元素。例如,线性分类器通常被认为是分级模型,因为它基于连续函数。然而,存在线性模型不能区分的情况,例如,平行于决策边界的线或表面。

几何模型

几何模型使用实例空间的概念。几何模型最明显的例子是当所有的特征都是数字的,并且可以成为笛卡尔坐标系统中的坐标。当我们只有两三个特征时,它们很容易被可视化。然而,由于许多机器学习问题有数百或数千个特征,因此维度,可视化这些空间是不可能的。然而,许多几何概念,如线性变换,仍然适用于这个超空间。这可以帮助我们更好地理解我们的模型。例如,我们期望许多学习算法是平移不变的,也就是说,我们将原点放在坐标系的什么位置并不重要。同样,我们可以使用欧几里得距离的几何概念来衡量实例之间的任何相似性;这为我们提供了一种方法来聚类相似的实例,并在它们之间形成决策边界。

假设我们使用线性分类器将段落分为快乐或悲伤,我们设计了一组测试。每个测试都与一个权重 w 相关联,以确定每个测试对整体结果的贡献程度。

我们可以简单地将每个测试相加并乘以它的权重,得到一个总分数,并创建一个决策规则,该规则将创建一个边界,例如,如果快乐分数大于一个阈值, t

Geometric models

每个特征对整体结果都有独立的贡献,因此规则是线性的。这种贡献取决于每个特征的相对权重。这个权重可以是正的,也可以是负的,在计算整体分数时,每个单独的特征不受阈值的限制。

我们可以用向量符号重写这个和,使用 w 作为权重向量(w1T5】, w 2 ,...w n )和 x 为一个测试结果向量( x 1 ,x 2 ,...,x n )。此外,如果我们使其相等,我们可以定义决策边界:

w . x = t

我们可以把 w 想象成一个指向正(快乐)例 P 和负例 N 的“质心”之间的向量。我们可以通过对以下各项求平均值来计算这些质心:

Geometric models

我们现在的目标是在这些质心中间创建一个决策边界。我们可以看到 wP - N 成比例,或者相等, (P + N)/2 将在决策边界上。所以,我们可以写下以下内容:

Geometric models

Geometric models

决策边界图

实际上,真实数据是有噪声的,不一定容易分离。即使数据很容易分离,特定的决策边界也可能没有太大的意义。考虑稀疏的数据,例如在文本分类中,与每个单词的实例数相比,单词数很大。在这个大面积的空实例空间中,可能很容易找到一个决策边界,但是哪个是最好的呢?选择的一种方法是使用一个边距来测量决策边界与其最近实例之间的距离。我们将在本书后面探讨这些技术。

概率模型

概率模型的一个典型例子是贝叶斯分类器,给你一些训练数据( D ,和一个基于初始训练集的概率(一个特定的假设, h ,得到后验概率, P (h/D) )。

Probabilistic models

举个例子,假设我们有一袋弹珠。我们知道其中 40%是红色,60%是蓝色。我们还知道,一半的红色弹珠和所有的蓝色弹珠都有白色斑点。当我们把手伸进包里挑选大理石时,我们可以通过它的纹理感觉到它有斑点。它是红色的几率有多大?

P(RF) 等于随机抽取的带有斑点的大理石为红色的概率:

P(FR) =红色大理石有斑点的概率为 0.5。

P(R) =弹珠为红色的概率为 0.4。

P(F) =大理石有斑点的概率是0.5×0.4+1×0.6 = 0.8

Probabilistic models

概率模型允许我们明确地计算概率,而不仅仅是一个二元真或假。正如我们所知,我们需要做的关键事情是创建一个模型,将一个变量映射或特征化为一个目标变量。当我们采用概率方法时,我们假设有一个潜在的随机过程,它产生了一个定义明确但未知的概率分布。

考虑一个垃圾邮件检测器。我们的特征变量 X 可以由一组单词组成,表示该电子邮件可能是垃圾邮件。目标变量 Y 是实例类,要么是垃圾邮件,要么是火腿。我们对 Y 给定 X 的条件概率感兴趣。对于每个电子邮件实例,将有一个特征向量 X ,由代表我们的垃圾邮件单词的存在的布尔值组成。我们正在试图找出我们的目标布尔值 Y 是否代表垃圾邮件。

现在,考虑我们有两个词, x 1x 2 ,它们构成了我们的特征向量 X 。从我们的训练集中,我们可以构建一个如下所示的表:

|   |

P(Y =垃圾邮件| x1,x2)

|

P(Y =非垃圾邮件| x1,x2)

|
| --- | --- | --- |
| P(Y| x 1 = 0,x 2 = 0) | Zero point one | Zero point nine |
| P(Y| x 1 = 0,x 2 = 1) | Zero point seven | Zero point three |
| P(Y| x 1 = 1,x 2 = 0) | Zero point four | Zero point six |
| P(Y| x 1 = 1,x 2 = 1) | Zero point eight | Zero point two |

表 1.1

我们可以看到,一旦我们开始向我们的特征向量添加更多的单词,它将很快变得难以管理。有了 n 大小的特征向量,我们就有了2nT5】案例来区分。幸运的是,还有其他方法来处理这个问题,我们将在后面看到。

上表中的概率称为后验概率。当我们从先前的分布中获得知识时,就会用到这些。例如,十分之一的电子邮件是垃圾邮件。但是,考虑一种情况,我们可能知道 X 包含 x 2 = 1 ,但是我们不确定 x 1 的值。该实例可能属于第 2 行,其中它是垃圾邮件的概率是 0.7 ,或者属于第 4 行,其中概率是 0.8 。解决方法是在任何情况下,使用 x 1 = 1 的概率对这两行进行平均。也就是说,一个单词 x 1 出现在任何电子邮件中的概率,不管是不是垃圾邮件:

P(Y | x2= 1)= P(Y | x1= 0,x2= 1)P(x1= 0)+P(x1= 1,x2= 1)P(x1= 1)

这被称为似然函数。如果我们知道,从一个训练集中, x 1 为 1 的概率是 0.1,那么它为零的概率是 0.9,因为这些概率之和必须是 1。因此,我们可以计算出一封电子邮件包含垃圾邮件单词的概率为 0.7 * 0.9 + 0.8 * 0.1 = 0.71

这是似然函数的一个例子: P(X|Y) 。那么,为什么我们想知道 X 的概率,这是我们都已经准备好知道的,条件是 Y ,这是我们一无所知的呢?看待这个问题的一种方法是考虑任何包含特定随机段落的电子邮件的概率,比如说《战争与和平》的第 127 段。显然,无论电子邮件是否是垃圾邮件,这种可能性都很小。我们真正感兴趣的不是这些可能性的大小,而是它们的比率。包含特定单词组合的电子邮件是垃圾邮件还是非垃圾邮件的可能性有多大?这些有时被称为生成模型,因为我们可以对所有涉及的变量进行采样。

我们可以使用贝叶斯规则在先验分布和似然函数之间进行转换:

Probabilistic models

P(Y) 是在观测到 X 之前的先验概率,即每个类的可能性有多大。同样的, P(X) 是不考虑 Y 的概率。如果我们只有两个类,我们可以使用比率。例如,如果我们想知道数据对每个类的支持程度,我们可以使用以下方法:

Probabilistic models

如果概率小于 1,我们假设分母中的类最有可能。如果大于 1,则枚举数中的类最有可能。如果我们使用来自表 1.1 的数据,我们计算以下后验概率:

Probabilistic models

Probabilistic models

Probabilistic models

Probabilistic models

可能性函数在机器学习中很重要,因为它创建了一个生成模型。如果我们知道词汇表中每个单词的概率分布,以及每个单词出现在垃圾邮件或非垃圾邮件中的可能性,我们就可以根据条件概率生成一封随机的垃圾邮件, P(X|Y =垃圾邮件)

逻辑模型

逻辑模型基于算法。它们可以被翻译成一套人类可以理解的正式规则。例如,如果 x 1x 2 都为 1,则该电子邮件被归类为垃圾邮件。

这些逻辑规则可以组织成树形结构。在下图中,我们看到实例空间在每个分支被迭代分区。树叶由矩形区域(或更高维度情况下的超矩形)组成,表示实例空间的片段。根据我们正在解决的任务,树叶标有类、概率、实数等等。

Logical models

人物特征树

特征树在表示机器学习问题时非常有用;甚至那些乍一看似乎没有树形结构的。例如,在前面部分的贝叶斯分类器中,我们可以将实例空间划分为尽可能多的区域,只要有特征值的组合。决策树模型通常采用修剪技术来删除给出错误结果的分支。在第 3 章将数据转化为信息中,我们将了解 Python 中表示决策树的多种方法。

请注意,决策规则可能会重叠,并做出矛盾的预测。

然后说它们在逻辑上不一致。当规则没有考虑特征空间中的所有坐标时,它们也可能是不完整的。有许多方法可以解决这些问题,我们将在本书后面详细讨论这些问题。

因为树学习算法通常以自上而下的方式工作,所以第一个任务是在树的顶部找到一个好的特征来分割。我们需要找到一个在后续节点中产生更高纯度的分裂。所谓纯粹,我指的是训练样本都属于同一类的程度。当我们沿着树往下走时,在每一个层次上,我们发现每个节点上的训练例子的纯度都在增加,也就是说,它们越来越多地被分成自己的类,直到我们到达所有例子都属于同一个类的叶子。

换个角度来看,我们对降低决策树中后续节点的熵感兴趣。熵,一种无序的度量,在树的顶部(根)很高,随着数据被分成各自的类,熵在每个节点逐渐降低。

在更复杂的问题中,那些具有更大特征集和决策规则的问题,找到最佳分割有时是不可能的,至少在可接受的时间内是不可能的。我们真正感兴趣的是创造最浅的树,以最短的路径到达我们的叶子。在分析所需的时间内,每个节点随着每个附加特征呈指数级增长,因此找到最佳决策树可能比实际使用次最佳树来执行任务需要更长的时间。

逻辑模型的一个重要性质是,它们可以在某种程度上为它们的预测提供解释。例如,考虑决策树做出的预测。通过追踪从叶子到根的路径,我们可以确定导致最终结果的条件。这是逻辑模型的优势之一:人类可以检查它们,以揭示更多关于问题的信息。

特征

就像决策只和我们在现实生活中获得的信息一样好,在机器学习任务中,模型只和它的特征一样好。从数学上讲,特征是从实例空间映射到特定域中的一组值的函数。在机器学习中,我们进行的大多数测量都是数值的,因此最常见的特征域是实数集。其他常见的领域包括布尔、真或假、整数(比如,当我们计算特定特征的出现时)或有限集合,如一组颜色或形状。

模型是根据它们的特征来定义的。此外,单个特征可以转化为模型,这被称为单变量模型。我们可以区分特征的两种用途。这与分组和分级之间的区别有关。

首先,我们可以通过放大实例空间中的一个区域来对特征进行分组。让 f 成为一个统计一个单词出现次数的功能,X1T5,在一封电子邮件中, X 。我们可以设置如下条件:

其中 f(X)=0 ,表示不包含X1T5】的电子邮件,或者其中 f(X) > 0 表示包含 x 1 一次或多次的电子邮件。这些条件被称为二进制拆分,因为它们将实例空间分为两组:满足条件的和不满足条件的。我们还可以将实例空间拆分为两个以上的段,以创建非二进制拆分。例如其中f(X)= 00<F(X)<5F(X) > 5 以此类推。

其次,我们可以对我们的特征进行分级,以计算每个特征对整体结果的独立贡献。回想一下我们简单的线性分类器,以下形式的决策规则:

Features

因为这个规则是线性的,所以每个特征对实例的得分都有独立的贡献。这个贡献取决于wIT3。如果是正数,那么一个正数xIT7】就会增加分数。如果 w i 为负值,则正值 x i 会降低分数。如果 w i 很小或者为零,那么它对整体结果的贡献可以忽略不计。可以看出,这些特征对最终预测做出了可测量的贡献。**

特征的这两种用途,如分割(分组)和预测(分级),可以组合成一个模型。一个典型的例子发生在我们想要逼近一个非线性函数的时候,比如说 y sin π x ,在区间上, -1 < x < 1 。显然,简单的线性模型是行不通的。当然,简单的答案是将 x 轴拆分为 -1 < x 00 < 。在每一段上,我们都能找到一个合理的线性近似。

Features

使用分组和分级

通过特征构建和转换,可以做大量的工作来提高模型的性能。在大多数机器学习问题中,特征不一定是显式可用的。它们需要从原始数据集构建,然后转换成我们的模型可以利用的东西。这在文本分类等问题中尤为重要。在我们简单的垃圾邮件示例中,我们使用了所谓的单词包表示,因为它忽略了单词的顺序。然而,这样做,我们就失去了关于文本意义的重要信息。

特征构建的一个重要部分是离散化。我们有时可以通过将特征分成相关的块来提取更多的信息,或者与我们的任务更相关的信息。例如,假设我们的数据由人们的精确收入列表组成,我们试图确定财务收入和一个人居住的郊区之间是否存在关系。显然,如果我们的特征集不是由精确的收入组成,而是由收入范围组成,这将是合适的,尽管严格来说,我们会丢失信息。如果我们适当地选择我们的区间,我们将不会丢失与我们的问题相关的信息,我们的模型将表现得更好,并给我们更容易解释的结果。

这突出了特征选择的主要任务:分离信号和噪声。

真实世界的数据总是包含大量我们不需要的信息,以及简单的随机噪声,分离出与我们的需求相关的可能很小的一部分数据对我们模型的成功很重要。当然,重要的是,我们不要丢弃对我们可能重要的信息。

通常,我们的特征将是非线性的,线性回归可能不会给我们好的结果。一个技巧是转换实例空间本身。假设我们有如下图所示的数据。显然,线性回归只能给我们一个合理的拟合,如左侧的图所示。但是,如果我们对实例空间进行平方,就可以改善这个结果,也就是说,我们使x = x2T3】和y = y2T7】,如右侧的图所示:**

Features

方差= .92 方差= .97

转换实例空间

我们可以更进一步,使用一种叫做 内核技巧的技术。其思想是我们可以创建一个更高维的隐式特征空间。成对的数据点通过指定的函数从原始数据集映射到这个更高维的空间,有时称为相似性函数

比如让 x 1 = (x 1 ,y 1 )x 2 = (x 2 ,y 2 )

我们创建一个 2D 到三维的映射,如下所示:

Features

三维空间中与 2D 点x1T3】和x2T7】对应的点如下:**

Features

这两个向量的点积是:

Features

我们可以看到,通过对原始 2D 空间中的点积求平方,我们获得了三维空间中的点积,而实际上没有创建特征向量本身。这里我们定义了内核 k(x 1 ,x 2 ) = (x 1 ,x22。在高维空间中计算点积通常在计算上更便宜,正如我们将看到的,这种技术在机器学习中被相当广泛地使用,从 支持向量机 ( SVM )、主成分分析 ( 主成分分析)和相关性分析。

我们之前看到的基本线性分类器定义了一个决策边界,w x = t。向量 w 等于正例的平均值和负例的平均值之间的差值 p-n 。假设我们有点 n= (0,0)p = (0,1) 。让我们假设我们已经从两个训练示例中获得了正平均值, p1 = (-1,1)p2 = (1,1) 。因此,我们有以下几点:

Features

我们现在可以将决策边界写成如下:

Features

使用内核技巧,我们可以获得以下决策边界:

Features

使用我们前面定义的内核,我们可以得到以下结果:

Features

我们现在可以导出决策边界:

Features

这只是一个围绕原点的圆,半径 √t

另一方面,使用内核技巧,根据每个训练示例评估每个新实例。作为这种更复杂计算的回报,我们获得了更灵活的非线性决策边界。

一个非常有趣和重要的方面是特征之间的交互。相互作用的一种形式是相互关联。例如,一篇博文中的单词,我们可能期望单词冬季寒冷之间存在正相关,而冬季炎热之间存在负相关。这对您的模型意味着什么取决于您的任务。如果您正在进行情感分析,您可能会考虑降低每个单词一起出现时的权重,因为与该单词单独出现时相比,添加另一个相关单词对整体结果的权重会略低。

同样关于情感分析,我们经常需要转换某些特征来捕捉它们的含义。例如,短语不高兴包含一个词,如果我们只使用 1 克,即使它的情绪明显是负面的,也会有助于正面情绪得分。一个解决方案(除了使用 2 克,这可能会不必要地使模型复杂化)是识别这两个单词何时出现在一个序列中,并创建一个新的特征, not_happy ,以及相关的情感评分。

选择和优化特征是很值得花费的时间。它可以是学习系统设计的重要部分。设计的这种迭代性质在两个阶段之间转换。首先,理解你正在研究的现象的性质,其次,用实验来检验你的想法。这种实验让我们对现象有了更深入的了解,使我们能够优化我们的特征并获得更深入的理解,直到我们对我们的模型感到满意,从而准确地反映了现实。

统一建模语言

机器学习系统可能很复杂。人脑通常很难理解一个完整系统的所有相互作用。我们需要某种方式将系统抽象成一组离散的功能组件。这使我们能够用图表和图表来可视化我们系统的结构和行为。

UML 是一种形式主义,它允许我们以精确的方式可视化和交流我们的设计思想。我们用代码实现我们的系统,基本原理用数学表达,但是还有第三个方面,从某种意义上说,是垂直于这些的,那就是我们系统的可视化表示。绘制设计的过程有助于从不同的角度将其概念化。也许我们可以考虑尝试三角测量一个解决方案。

概念模型是描述问题要素的理论工具。它们可以帮助我们澄清假设,证明某些性质,并让我们对系统的结构和相互作用有一个基本的了解。

UML 产生于既要简化这种复杂性,又要允许我们的设计清晰明确地传达给团队成员、客户和其他利益相关者的需要。模型是真实系统的简化表示。在这里,我们使用了更一般意义上的模型这个词,与其更精确的机器学习定义相比。UML 可以用来建模几乎任何可以想象的系统。核心思想是用核心属性和功能的清晰表示来去除任何不相关的和潜在的混淆元素。

类图

类图为系统的静态结构建模。类代表具有共同特征的抽象实体。它们很有用,因为它们表达并实施了面向对象的编程方法。我们可以看到,通过在代码中分离不同的对象,我们可以更清楚地将每个对象作为一个独立的单元来处理。我们可以用一组特定的特征来定义它,并定义它与其他对象的关系。这使得复杂的程序能够分解成独立的功能组件。它还允许我们通过继承来子类化对象。这是非常有用的,反映了我们如何建模我们世界中特别有层次的方面(也就是说,程序员是人类的子类, Python 程序员是程序员的子类)。对象编程可以加快整体开发时间,因为它允许组件的重用。有丰富的开发组件类库可以利用。此外,生成的代码往往更容易维护,因为我们可以替换或更改类,并且能够(通常)理解这将如何影响整个系统。

事实上,对象编码确实倾向于导致更大的代码库,这可能意味着程序运行会更慢。最终,这不是一个“非此即彼”的情况。对于许多简单的任务,如果您可能再也不会使用它,您可能不想花时间创建一个类。一般来说,如果您发现自己键入了相同的代码,或者创建了相同类型的数据结构,那么创建一个类可能是个好主意。对象编程的最大优势是我们可以将数据和对数据进行操作的函数封装在一个对象中。这些软件对象可以以相当直接的方式与现实世界的对象相对应。

最初,设计面向对象的系统可能需要一些时间。然而,在建立可行的类结构和类定义的同时,实现类所需的编码任务变得更加清晰。创建类结构可能是开始系统建模的一种非常有用的方式。当我们定义一个类时,我们感兴趣的是一组特定的属性,作为所有可能属性或实际不相关属性的子集。它应该是真实系统的精确表示,我们需要判断什么是相关的,什么不是。这很难,因为现实世界的现象是复杂的,我们所掌握的关于系统的信息总是不完整的。我们只能依靠我们所知道的,所以我们的领域知识(对我们试图建模的系统的理解),无论是软件、自然的还是人类的,都是至关重要的。

物体图

对象图是运行时系统的逻辑视图。它们是特定时刻的快照,可以理解为类图的一个实例。许多参数和变量随着程序的运行而改变值,对象图的功能就是映射这些。这种运行时绑定是对象图所代表的关键内容之一。通过使用链接将对象联系在一起,我们可以对特定的运行时配置进行建模。对象之间的链接对应于对象类之间的关联。因此,链接受到与它对其对象强制实施的类相同的约束。

Object diagrams

对象图

类图和对象图都由相同的基本元素组成。而类图代表了类的抽象蓝图。对象图表示对象在特定时间点的真实状态。单对象图不能代表每个类实例,所以在画这些图的时候,我们必须把自己局限在重要的实例和覆盖系统基本功能的实例上。对象图应该阐明对象之间的关联,并指出重要变量的值。

活动图

活动图的目的是通过将单独的动作链接在一起来建模系统的工作流,这些动作一起表示一个过程。他们特别擅长对协调任务集进行建模。活动图是 UML 规范中使用最多的图之一,因为它们的格式基于传统的流程图,所以理解起来很直观。活动图的主要组成部分是动作、边(有时称为路径)和决策。动作用圆角矩形表示,边用箭头表示,决策用菱形表示。活动图通常有一个开始节点和一个结束节点。

Activity diagrams

示例活动图的图形

状态图

状态图用于对系统建模,这些系统根据其所处的状态改变行为。它们由状态和转换表示。状态由圆角矩形表示,过渡由箭头表示。每个转换都有一个触发器,这是沿着箭头写的。

许多状态图将包括初始伪状态和最终状态。伪状态是控制流量的状态。另一个例子是选择伪状态。这表明布尔条件决定了转换。

状态转换系统由四个要素组成;它们如下:

  • S = {s 1 ,s 2 ,…} :一组状态
  • A= {a 1 ,a 2 ,...} :一套动作
  • E ={e 1 ,e 2 ,...} :一组事件
  • y: S(A U E)→2s :状态转换功能

第一个元素 S 是世界可以处于的所有可能状态的集合。行动是一个代理可以做的改变世界的事情。事件可以发生在世界上,不受代理人的控制。状态转移函数 y 将两件事作为输入:世界的状态和行动与事件的联合。这给了我们作为应用特定动作或事件的结果的所有可能状态。

假设我们有一个仓库,储存了三种物品。我们认为仓库最多只能储存一种物品。我们可以用下面的矩阵来表示仓库的可能状态:

State diagrams

这可以为 EA 定义类似的二进制矩阵,前者代表卖出的事件,后者是一个行动指令。

在这个简单的例子中,我们的转换函数应用于一个实例( s ,这是 S 中的一列),这是S = S+a-e,其中S‘是系统的最终状态, s 是其初始状态, ae 分别是一个活动和一个事件。

我们可以用下面的转换图来表示:

State diagrams

过渡图的图形

总结

到目前为止,我们已经介绍了广泛的机器学习问题、技术和概念。希望到目前为止,你已经知道如何通过将一个新的独特的问题分解成几个部分来开始解决它。我们回顾了一些基本的数学知识,并探索了可视化设计的方法。我们可以看到,同一个问题可以有很多不同的表现,每一个都可能突出不同的方面。在我们开始建模之前,我们需要一个定义明确的目标,表述为一个具体的、可行的和有意义的问题。我们需要清楚如何用机器能理解的方式表达这个问题。

设计过程,虽然由不同的和独特的活动组成,但不一定是一个线性的过程,而是一个迭代的过程。我们循环通过每个特定的阶段,提出和测试想法,直到我们觉得我们可以跳到下一个阶段。有时我们可能会跳回前一个阶段。我们可能坐在一个平衡点,等待一个特定的事件发生;我们可以循环通过几个阶段,或者并行通过几个阶段。

在下一章中,我们将开始探索各种 Python 库中可用的实用工具。*

二、工具和技术

Python 附带了一个大型的机器学习任务包库。

我们将在本章中看到的软件包如下:

  • IPython 控制台
  • NumPy,它是一个扩展,增加了对多维数组、矩阵和高级数学函数的支持
  • SciPy,这是一个科学公式、常数和数学函数的库
  • Matplotlib,用于创建地块
  • Scikit-learn,这是一个机器学习任务库,如分类、回归和聚类

空间只够给你一个味的这些庞大的库,一个重要的技巧就是能够找到并理解各种包的参考资料。不可能在教程风格的文档中呈现所有不同的功能,重要的是能够在有时密集的 API 引用中找到自己的方法。需要记住的一点是,这些包的大部分是由开源社区组合在一起的。它们并不像您从商业产品中期望的那样是单一的结构,因此,理解各种包分类可能会令人困惑。然而,开源软件方法的多样性,以及想法不断被贡献的事实,给了它一个重要的优势。

然而,开源软件不断发展的质量也有其负面影响,尤其是对于 ML 应用。例如,代表 Python 机器学习用户群体的人相当不愿意从 Python 2 转到 3。因为 Python 3 打破了向后兼容;重要的是,就其数值处理而言,更新相关包并不是一个微不足道的过程。在写作的时候,所有的重要(对我来说很重要!)包,以及本书中使用的所有包,都与 Python 2.7 或 3x 一起工作。Python 的主要发行版有 Python 3 版本,它们的包集略有不同。

机器学习用 Python

Python 是一种通用的编程语言。这是一种解释语言,可以从控制台交互运行。它不需要像 C++或者 Java 那样的编译器,所以开发时间往往会更短。它可以免费下载,并且可以安装在许多不同的操作系统上,包括 UNIX、Windows 和 Macintosh。它特别受科学和数学应用的欢迎。与 C++和 Java 等语言相比,Python 相对容易学习,类似的任务使用更少的代码行。

Python 不是机器学习的唯一平台,但它肯定是使用最多的平台之一。它的主要替代品之一是 R 。像 Python 一样,它是开源的,虽然它在应用机器学习中很受欢迎,但它缺乏 Python 的大型开发社区。r 是机器学习和统计分析的专用工具。Python 是一种通用的、广泛使用的编程语言,也有优秀的机器学习应用库。

另一种选择是 Matlab 。不像 R 和 Python,它是一个商业产品。正如预期的那样,它包含了完善的用户界面和详尽的文档。然而,和 R 一样,它缺乏 Python 的通用性。Python 是一种非常有用的语言,与其他平台相比,您学习它的努力将带来更大的回报。它还拥有优秀的网络、web 开发和微控制器编程库。这些应用可以补充或增强您在机器学习方面的工作,而没有笨拙集成的痛苦,也没有学习或记住不同语言的细节。

IPython 控制台

随着版本 4 的发布,Ipython包有了一些显著的变化。一个以前的单片封装结构,它已经被分成子封装。几个 IPython 项目已经分成了各自独立的项目。大部分仓库已经被转移到朱皮特项目(jupyter.org)。

IPython 的核心是 IPython 控制台:一个强大的交互式解释器,允许您以非常快速和直观的方式测试您的想法。不需要每次测试代码片段时都创建、保存和运行文件,只需将它键入控制台即可。IPython 的一个强大功能是,它解除了大多数计算平台所基于的传统读取-评估-打印循环。IPython 将评估阶段放入自己的过程中:一个内核(不要与机器学习算法中使用的内核函数混淆)。重要的是,不止一个客户端可以访问内核。这意味着您可以在许多文件中运行代码并访问它们,例如,从控制台运行一个方法。此外,内核和客户端不需要在同一台机器上。这对分布式和网络计算有着强大的影响。

IPython 控制台增加了命令行功能,例如标签完成和%magic命令,它们复制终端命令。如果您没有使用已经安装了 IPython 的 Python 发行版,您可以通过在 Python 命令行中键入ipython来启动 IPython。在 IPython 控制台中输入%quickref会给你一个命令列表和它们的功能。

还应该提到 IPython 笔记本。笔记本已经合并到另一个名为 Jupyter(jupyter.org)的项目中。这个网络应用是一个用 40 多种语言进行数值计算的强大平台。笔记本允许您共享和协作实时代码,并发布丰富的图形和文本。

安装 SciPy 堆栈

SciPy 栈由 Python 以及最常用的科学、数学和 ML 库组成。(参观:scipy.org)。这些包括 NumPyMatplotlib 、 SciPy 库本身和 IPython。这些包可以单独安装在现有 Python 安装的基础上,或者作为一个完整的发行版(发行版)。如果你的电脑上没有安装 Python,最简单的入门方法就是使用发行版。主要的 Python 发行版适用于大多数平台,它们在一个包中包含了您需要的一切。单独安装所有包及其依赖项确实需要一些时间,但是如果您的机器上已经配置了 Python 安装,这可能是一个选项。

大多数发行版都为您提供了您需要的所有工具,并且许多都带有强大的开发环境。最好的两个是蟒蛇(www.continuum.io/downloads)和天篷(http://www.enthought.com/products/canopy/)。两者都有免费版和商业版。作为参考,我将使用 Python 的 Anaconda 发行版。

安装主要的发行版通常是一项相当轻松的任务。

型式

请注意,并非所有发行版都包含相同的 Python 模块集,您可能需要安装模块,或者重新安装模块的正确版本。

NumPY

我们应该知道,在 Python 中有一个表示数据的类型层次结构。根是不可变的对象,如整数、浮点数和布尔值。基于此,我们有序列类型。这些是由非负整数索引的有序对象集。它们是迭代对象,包括字符串、列表和元组。序列类型有一组常见的操作,例如返回一个元素( s[i] )或一个切片( s[i:j] ),并找到长度( len(s) )或总和( sum(s) )。最后,我们有映射类型。这些是由另一个关键对象集合索引的对象集合。映射对象是无序的,由数字、字符串或其他对象索引。内置的 Python 映射类型是字典。

NumPy 通过提供另外两个对象来构建这些数据对象:一个 N 维数组对象(ndarray)和一个通用函数对象(ufunc)。ufunc对象提供对ndarray对象的逐元素操作,允许类型转换和数组广播。类型转换是将一种数据类型转换成另一种数据类型的过程,广播描述了在算术运算过程中如何处理不同大小的数组。有线性代数(linalg)、随机数生成(random)、离散傅立叶变换(fft)和单元测试(testing)的子包。

NumPy 使用一个dtype对象来描述数据的各个方面。这包括浮点、整数等数据类型,数据类型中的字节数(如果数据是结构化的),以及字段名称和任何子数组的形状。NumPy 有几种新的数据类型,包括:

  • 8、16、32 和 64 位int
  • 16、32 和 64 位浮点值
  • 64 位和 128 位复杂类型
  • Ndarray结构化数组类型

我们可以使用np.cast对象在类型之间转换。这只是一个字典,根据目标转换类型键入,其值是执行转换的适当函数。这里我们将一个整数转换成一个浮点数 32:

f= np.cast['f'] (2)

可以通过多种方式创建 NumPy 数组,例如使用内置数组创建对象(如arange()ones()zeros())从其他 Python 数据结构中转换它们,或者从文件(如.csv.html)中创建它们。

IndexingslicingNumPy基于序列中使用的切片和索引技术。您应该已经熟悉使用[i:j:k]语法在 Python 中对序列(如列表和元组)进行切片,其中i是开始索引,j是结束,k是步骤。NumPy 将选择元组的概念扩展到了 N 维。

启动 Python 控制台并键入以下命令:

import numpy as np
a=np.arange(60).reshape(3,4,5)
print(a)

您将观察到以下情况:

NumPY

这将打印前面的 3 乘 4 乘 5 数组。您应该知道,我们可以使用像a[2,3,4]这样的符号来访问数组中的每个项目。这返回59。请记住,索引从 0 开始。

我们可以使用切片技术返回数组的切片。

下图显示了A[1:2:]数组:

NumPY

使用椭圆(…),我们可以选择任何剩余的未指定尺寸。例如a[...,1]相当于a[:,:,1]:

NumPY

您也可以使用负数从轴的末端开始计数:

NumPY

通过切片,我们正在创建视图;原始数组保持不变,视图保留对原始数组的引用。这意味着,当我们创建切片时,即使我们将其分配给新的变量,如果我们更改原始数组,这些更改也会反映在新数组中。下图演示了这一点:

NumPY

这里 ab 指的是同一个数组。当我们在a赋值时,这也体现在b上。要复制一个数组而不是简单地引用它,我们使用标准库中copy包中的深度copy()函数:

import copy
c=copy.deepcopy(a)

在这里,我们创建了一个新的独立数组,c。数组a中所做的任何更改都不会反映在数组c中。

数组的构造和转换

这个切片功能也可以和几个 NumPy 类一起使用,作为构造数组的有效方法。例如,numpy.mgrid对象创建了一个 meshgrid对象,在某些情况下,它提供了一个比arange()更方便的选择。它的主要目的是为指定的 N 维体积建立一个坐标阵列。以下图为例:

Constructing and transforming arrays

有时,我们需要以其他方式操纵我们的数据结构。其中包括:

  • concatenating: By using the np.r_ and np.c_ functions, we can concatenate along one or two axes using the slicing constructs. Here is an example:

    Constructing and transforming arrays

    这里我们使用了复数 5j 作为步长,Python 将它解释为点的数量,包括点在内,以在指定范围之间拟合,这里是 -11

  • newaxis: This object expands the dimensions of an array:

    Constructing and transforming arrays

    这将在第一维度上创建一个额外的轴。下面在第二维中创建新轴:

    Constructing and transforming arrays

    您也可以使用布尔运算符进行筛选:

    a[a<5]
    Out[]: array([0, 1, 2, 3, 4])
    
    
  • Find the sum of a given axis:

    Constructing and transforming arrays

    这里我们用轴 2 求和。

数学运算

正如您所料,您可以在 NumPy 数组上执行数学运算,如加法、减法、乘法以及三角函数。不同形状阵列上的算术运算可以通过称为广播的过程来执行。当在两个数组上操作时,NumPy 从尾部维度逐元素比较它们的形状。如果两个尺寸相同,或者其中一个尺寸为 1,则两个尺寸是兼容的。如果不满足这些条件,则抛出ValueError异常。

这都是在后台使用ufunc对象完成的。该对象在逐元素的基础上对ndarrays进行操作。它们本质上是包装器,为标量函数提供一致的接口,以允许它们使用 NumPy 数组。有超过 60 个ufunc对象,涵盖各种各样的操作和类型。当您使用+操作符执行诸如添加两个数组的操作时,会自动调用ufunc对象。

让我们看看一些额外的数学特征:

  • Vectors: We can also create our own vectorized versions of scalar functions using the np.vectorize() function. It takes a Python scalar function or method as a parameter and returns a vectorized version of this function:

    def myfunc(a,b):
    def myfunc(a,b):
    if a > b:
     return a-b
     else:
     return a + b
    vfunc=np.vectorize(myfunc)
    
    

    我们将观察以下输出:

    Mathematical operations

  • Polynomial functions: The poly1d class allows us to deal with polynomial functions in a natural way. It accepts as a parameter an array of coefficients in decreasing powers. For example, the polynomial, 2x2 + 3x + 4, can be entered by the following:

    Mathematical operations

    我们可以看到它以人类可读的方式打印出多项式。我们可以对多项式执行各种操作,例如在某个点进行求值:

    Mathematical operations

  • Find the roots:

    Mathematical operations

我们可以使用asarray(p)给多项式的系数一个数组,这样它就可以用在所有接受数组的函数中。

正如我们将看到的,构建在 NumPy 上的包为机器学习提供了一个强大而灵活的框架。

Matplotlib

Matplotlib,或者更重要的是它的子包PyPlot,是 Python 中可视化二维数据必不可少的工具。我在这里只简单地提到它,因为当我们通过例子工作时,它的使用应该变得显而易见。它被构建为像 Matlab 一样工作,具有命令风格的功能。每个PyPlot函数都会对一个PyPlot实例进行一些更改。PyPlot的核心是plot法。最简单的实现是传递一个列表或一个 1D 数组。如果只有一个参数被传递到绘图,它假设它是一系列 y 值,它将自动生成 x 值。更常见的是,我们传递坐标为 xy 的两个 1D 阵列或列表。plot方法还可以接受一个参数来指示线条属性,如线条宽度、颜色和样式。这里有一个例子:

import numpy as np
import matplotlib.pyplot as plt

x = np.arange(0., 5., 0.2)
plt.plot(x, x**4, 'r', x, x*90, 'bs', x, x**3, 'g^')
plt.show()

该代码打印三行不同样式的:一条红线、蓝色方块和绿色三角形。请注意,我们可以通过一对以上的坐标阵列来绘制多条线。如需线条样式的完整列表,请键入help(plt.plot)功能。

Pyplot 和 Matlab 一样,对当前轴应用绘图命令。使用subplot命令可以创建多个轴。这里有一个例子:

x1 = np.arange(0., 5., 0.2)
x2 = np.arange(0., 5., 0.1)

plt.figure(1)
plt.subplot(211)
plt.plot(x1, x1**4, 'r', x1, x1*90, 'bs', x1, x1**3, 'g^',linewidth=2.0)

plt.subplot(212)
plt.plot(x2,np.cos(2*np.pi*x2), 'k')
plt.show()

前面代码的输出如下:

Matplotlib

另一个有用的图是直方图。hist()对象接受输入值的数组或数组序列。第二个参数是箱的数量。在这个例子中,我们将一个分布分成 10 个箱。当设置为1true时,赋范参数将计数归一化为以形成概率密度。还要注意,在这个代码中,我们已经标记了 xy 轴,并在坐标给定的位置显示了一个标题和一些文本:

mu, sigma = 100, 15
x = mu + sigma * np.random.randn(1000)
n, bins, patches = plt.hist(x, 10, normed=1, facecolor='g')
plt.xlabel('Frequency')
plt.ylabel('Probability')
plt.title('Histogram Example')
plt.text(40,.028, 'mean=100 std.dev.=15')
plt.axis([40, 160, 0, 0.03])
plt.grid(True)
plt.show()

这段代码的输出如下所示:

Matplotlib

我们要看的最后一个 2D 图是散点图。scatter对象采用两个长度相同的序列对象,如数组和可选参数来表示颜色和样式属性。让我们看看这段代码:

N = 100
x = np.random.rand(N)
y = np.random.rand(N)
#colors = np.random.rand(N)
colors=('r','b','g')
area = np.pi * (10 * np.random.rand(N))**2  # 0 to 10 point radiuses
plt.scatter(x, y, s=area, c=colors, alpha=0.5)
plt.show()

我们将观察以下输出:

Matplotlib

Matplotlib 还有一个强大的工具箱,用于渲染 3D 图。以下代码演示是三维线、散点图和曲面图的简单示例。三维地块的创建方式与 2D 地块非常相似。这里我们用gca功能得到当前轴,将投影参数设置为 3D。所有绘图方法的工作原理都与 2D 的方法非常相似,除了它们现在采用第三组输入值用于 z 轴:

import matplotlib as mpl
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm

mpl.rcParams['legend.fontsize'] = 10

fig = plt.figure()
ax = fig.gca(projection='3d')
theta = np.linspace(-3 * np.pi, 6 * np.pi, 100)
z = np.linspace(-2, 2, 100)
r = z**2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)
ax.plot(x, y, z)

theta2 = np.linspace(-3 * np.pi, 6 * np.pi, 20)
z2 = np.linspace(-2, 2, 20)
r2=z2**2 +1
x2 = r2 * np.sin(theta2)
y2 = r2 * np.cos(theta2)

ax.scatter(x2,y2,z2, c= 'r')
x3 = np.arange(-5, 5, 0.25)
y3 = np.arange(-5, 5, 0.25)
x3, y3 = np.meshgrid(x3, y3)
R = np.sqrt(x3**2 + y3**2)
z3 = np.sin(R)
surf = ax.plot_surface(x3,y3,z3, rstride=1, cstride=1, cmap=cm.Greys_r,
 linewidth=0, antialiased=False)
ax.set_zlim(-2, 2)
plt.show()

我们将观察这个输出:

Matplotlib

熊猫

熊猫库通过引入几个有用的数据结构和功能来读取和处理数据,从而在 NumPy 上构建了 T2。熊猫是一般数据收集的好工具。它可以轻松处理常见任务,如处理丢失的数据、操作形状和大小、在数据格式和结构之间转换以及从不同来源导入数据。

熊猫引入的主要数据结构有:

  • 系列
  • 数据框
  • 面板

数据框可能是使用最广泛的。它是一个二维结构,实际上是一个由 NumPy 数组、列表、字典或序列创建的表。您也可以通过读取文件来创建数据帧。

或许对熊猫有所了解的最好方法是通过一个典型的用例。假设我们的任务是发现日最高温度是如何随时间变化的。对于这个例子,我们将使用来自塔斯马尼亚霍巴特气象站的历史天气观测。下载以下 ZIP 文件,并将其内容提取到 Python 工作目录中名为数据的文件夹中:

http://davejulian.net/mlbook/data

我们要做的第一件事是从中创建一个数据帧:

import pandas as pd
df=pd.read_csv('data/sampleData.csv')

检查该数据的前几行:

df.head()

我们可以看到每一行的产品代码和站号都是相同的,这些信息是多余的。此外,我们的目的不需要累积最高温度的天数,因此我们也将删除它们:

del df['Bureau of Meteorology station number']
del df['Product code']
del df['Days of accumulation of maximum temperature']

让我们通过缩短列标签使数据更容易阅读:

df=df.rename(columns={'Maximum temperature (Degree C)':'maxtemp'})

我们只对高质量的数据感兴趣,所以我们只在质量栏中包含有 Y 的记录:

df=df[(df.Quality=='Y')]

我们可以得到数据的统计摘要:

df.describe()

Pandas

如果我们导入matplotlib.pyplot包,我们可以绘制数据:

import matplotlib.pyplot as plt
plt.plot(df.Year, df.maxtemp)

Pandas

请注意,PyPlot 正确地格式化了日期轴,并通过连接任一侧的两个已知点来处理丢失的数据。我们可以使用以下方法将数据帧转换为 NumPy 数组:

ndarray = df.values

如果数据框包含混合的数据类型,那么这个函数将把它们转换成最小公分母类型,这意味着将选择容纳所有值的类型。例如,如果 DataFrame 由 float16 和 float32 混合类型组成,则值将转换为 float 32。

熊猫数据框是查看和处理简单文本和数字数据的绝佳对象。然而,熊猫可能不是更复杂的数字处理的合适工具,比如计算点积,或者寻找线性系统的解。对于数值应用,我们通常使用 NumPy 类。

黑桃

SciPy (发音为唏嘘 pi)在 NumPy 更纯粹的数学构造之上,为 NumPy 添加了一层,包裹了常见的科学和统计应用。SciPy 提供了操作和可视化数据的高级功能,在交互式使用 Python 时尤其有用。SciPy 被组织成涵盖不同科学计算应用的子包。与 ML 及其功能最相关的软件包列表如下所示:

|

包裹

|

描述

|
| --- | --- |
| cluster | 这包含两个子包:cluster.vq用于 K 均值聚类和矢量量化。cluster.hierachy用于分层和聚集聚类,这对于距离矩阵、计算聚类统计以及用树图可视化聚类非常有用。 |
| constants | 这些是物理常数和数学常数,如πe 。 |
| integrate | 这些是微分方程解算器 |
| interpolate | 这些是用于在已知点的范围内创建新数据点的插值函数。 |
| io | 这指的是用于创建字符串、二进制或原始数据流以及读写文件的输入和输出函数。 |
| optimize | 这是指优化和寻根。 |
| linalg | 这指的是线性代数例程,例如基本矩阵计算、求解线性系统、寻找行列式和范数以及分解。 |
| ndimage | 这是 N 维图像处理。 |
| odr | 这就是正交距离回归。 |
| stats | 这指的是统计分布和函数。 |

许多 NumPy 模块与 SciPy 包中的模块具有相同的名称和相似的功能。在很大程度上,SciPy 导入了它的 NumPy 等价物并扩展了它的功能。但是,请注意,与 NumPy 中的功能相比,SciPy 模块中一些名称相同的功能可能会有稍有不同。还应该提到的是,许多 SciPy 类在 scikit-learn 包中都有便利包装器,有时使用它们会更容易。

这些包中的每一个都需要显式导入;这里有一个例子:

import scipy.cluster

您可以从 SciPy 网站(scipy.org)或控制台获取文档,例如help(sicpy.cluster)

正如我们所看到的,在许多不同的 ML 设置中,一个共同的任务是优化。我们在最后一章研究了单纯形算法的数学。下面是使用 SciPy 的实现。我们记得单纯形优化了一组线性方程。我们研究的问题如下:

最大化x1+x2在:2x1T11】+x2≤4x1T19】+2x2≤3**

linprog对象可能是解决这个问题最简单的对象。这是一个最小化算法,所以我们反转目标的符号。

scipy.optimize开始,导入linprog:

objective=[-1,-1]
con1=[[2,1],[1,2]]
con2=[4,3]
res=linprog(objective,con1,con2)
print(res)

您将看到以下输出:

SciPy

还有一个optimisation.minimize对象,适合稍微复杂一点的问题。此对象将求解器作为参数。目前大约有十几个解算器可用,如果你需要一个更具体的解算器,你可以自己写。最常用、最适合大多数问题的是内尔德-米德求解器。该特定解算器使用的下坡单纯形算法,该算法基本上是一种启发式搜索,用位于剩余点质心的一个点替换具有高误差的每个测试点。它迭代这个过程,直到收敛到最小值。

在这个例子中,我们使用 罗森布鲁克函数作为我们的测试问题。这是一个非凸函数,常用于测试优化问题。这个函数的全局最小值在一个长的抛物线谷上,这使得算法在一个大的、相对平坦的谷中寻找最小值具有挑战性。我们将看到更多此功能:

import numpy as np
from scipy.optimize import minimize
def rosen(x):
 return sum(100.0*(x[1:]-x[:-1]**2.0)**2.0 + (1-x[:-1])**2.0)

def nMin(funct,x0):

 return(minimize(rosen, x0, method='nelder-mead', options={'xtol':
 1e-8, 'disp': True}))

x0 = np.array([1.3, 0.7, 0.8, 1.9, 1.2])

nMin(rosen,x0)

前面代码的输出如下:

SciPy

最小化函数采用两个强制参数。这些是目标函数和 x0 的初始值。最小化函数还为求解器方法取了一个可选参数,在本例中我们使用nelder-mead方法。选项是一组特定于求解器的键值对,表示为字典。这里,xtol是收敛可以接受的相对误差,disp设置为打印消息。另一个对机器学习应用非常有用的包是scipy.linalg。这个包增加了执行任务的能力,如矩阵求逆、计算特征值和矩阵分解。

科学学习

这包括最常见的机器学习任务的算法,如分类、回归、聚类、降维、模型选择和预处理。

Scikit-learn 附带了几个真实世界的数据集供我们练习。让我们来看看其中的一个 Iris 数据集:

from sklearn import datasets
iris = datasets.load_iris()
iris_X = iris.data
iris_y = iris.target
iris_X.shape
(150, 4)

数据集包含三种类型的虹膜(濑户鸢尾、云芝和弗吉尼亚鸢尾)的 150 个样本,每个样本具有四个特征。我们可以得到数据集的描述:

iris.DESCR

我们可以看到,这四个属性,或者说特征,分别是萼片宽度、萼片长度、花瓣长度和花瓣宽度,单位为厘米。每个样本与三个类中的一个相关联。濑户、云芝和弗吉尼亚。它们分别由 0、1 和 2 表示。

让我们使用这些数据来看一个简单的分类问题。我们想根据它的特征来预测鸢尾的类型:萼片和花瓣的长度和宽度。典型地,scikit-learn 使用估计器来实现fit(X, y)方法和训练分类器,以及predict(X)方法,如果给定未标记的观测值,X,则返回预测的标记,yfit()predict()方法通常采用类似 2D 阵列的物体。

这里,我们将使用 K 最近邻(K-NN) 技术来解决这个分类问题。K-NN 背后的原理相对简单。我们根据最近邻的分类对未标记样本进行分类。每个数据点根据其最近邻居的少数 k 的多数类被分配类成员资格。K-NN 是基于实例的学习的一个例子,其中分类不是根据一个内置的模型,而是参考一个标记的测试集来完成的。K-NN 算法被称为非泛化,因为它只需记住所有的训练数据,并将其与每个新样本进行比较。尽管表面上看起来很简单,或者可能是因为它的简单,但是 K-NN 是一种非常好的解决各种分类和回归问题的技术。

Sklearn 中有两个不同的 K-NN 分类器。kneighgborksclassifier要求用户指定 k ,最近邻居的数量。另一方面,radiusneigoresclassifier基于每个训练点的固定半径 r 内的邻居数量来实现学习。KNeighborsClassifier 是更常用的一个。 k 的最佳值在很大程度上取决于数据。一般来说,较大的 k 值用于有噪声的数据。作为分类边界的权衡变得不那么明显。如果数据不是均匀采样的,那么 RadiusNeighborsClassifier 可能是更好的选择。由于邻居的数量是基于半径的,因此每个点的 k 会有所不同。在稀疏区域, k 将低于高样本密度区域:

from sklearn.neighbors import KNeighborsClassifier as knn
from sklearn import datasets
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

def knnDemo(X,y, n):

 #cresates the the classifier and fits it to the data
 res=0.05
 k1 = knn(n_neighbors=n,p=2,metric='minkowski')
 k1.fit(X,y)

 #sets up the grid
 x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
 x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
 xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, res),np.arange(x2_min, x2_max, res))

 #makes the prediction
 Z = k1.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
 Z = Z.reshape(xx1.shape)

 #creates the color map
 cmap_light = ListedColormap(['#FFAAAA', '#AAFFAA', '#AAAAFF'])
 cmap_bold = ListedColormap(['#FF0000', '#00FF00', '#0000FF'])

 #Plots the decision surface
 plt.contourf(xx1, xx2, Z, alpha=0.4, cmap=cmap_light)
 plt.xlim(xx1.min(), xx1.max())
 plt.ylim(xx2.min(), xx2.max())

 #plots the samples
 for idx, cl in enumerate(np.unique(y)):
 plt.scatter(X[:, 0], X[:, 1], c=y, cmap=cmap_bold)

 plt.show()

iris = datasets.load_iris()
X1 = iris.data[:, 0:3:2]
X2 = iris.data[:, 0:2]
X3 = iris.data[:,1:3]
y = iris.target
knnDemo(X2,y,15)

以下是上述命令的输出:

Scikit-learn

现在让我们来看看 Sklearn 的回归问题。最简单的解决方案是最小化平方误差的总和。这是由LinearRegression对象执行的。该对象有一个fit()方法,该方法采用两个向量: X 作为特征向量, y 作为目标向量:

from sklearn import linear_model
clf = linear_model.LinearRegression()
clf.fit ([[0, 0], [1, 1], [2, 2]], [0, 1, 2])
clf.coef_
array([ 0.5,  0.5])

LinearRegression对象有四个可选参数:

  • fit_intercept:布尔值,如果设置为false,将假设数据居中,模型在其计算中不会使用截距。默认值为true
  • normalize:如果trueX 回归前归一化为零均值和单位方差。这有时很有用,因为它可以使系数的解释更加明确。默认为false
  • copy_X:默认为true。如果设置为false,将允许 X 被覆盖。
  • n_jobs:用于计算的作业数。这默认为1。这可以用来加快多个处理器上大型问题的计算速度。

其输出具有以下属性:

  • coef_:线性回归问题的估计系数数组。如果 y 是多维的,也就是有多个目标变量,那么coef_将是一个 2D 数组的形式(n_targetsn_features)。如果只传递了一个目标变量,那么coef_将是一个长度为(n_features)的 1D 数组。
  • intercept_:这是线性模型中截距或独立项的数组。

对于普通最小二乘到的工作,我们假设特征是独立的。当这些项相关时,矩阵 X 可以接近奇点。这意味着估计对输入数据的微小变化变得高度敏感。这被称为多重共线性,并导致大的方差和最终的不稳定性。我们稍后会更详细地讨论这个问题,但是现在,让我们来看一个在某种程度上解决这些问题的算法。

岭回归不仅解决了多重共线性的问题,还解决了输入变量的数量大大超过样本数量的情况。linear_model.Ridge()对象使用所谓的 L2 正则化。直观地说,我们可以把这理解为在权重向量的极值上加一个惩罚。这有时被称为收缩,因为使平均重量变小。这往往会使模型更加稳定,因为它降低了对极值的敏感性。

Sklearn 对象linear_model.ridge添加了正则化参数alpha。一般来说,alpha的小正值会提高模型的稳定性。它可以是浮点数,也可以是数组。如果是一个数组,则假设该数组对应于特定的目标,因此,它必须与目标大小相同。我们可以用下面的简单函数来尝试一下:

from sklearn.linear_model import Ridge
import numpy as np

def ridgeReg(alpha):

 n_samples, n_features = 10, 5
 y = np.random.randn(n_samples)
 X = np.random.randn(n_samples, n_features)
 clf = Ridge(.001)
 res=clf.fit(X, y)
 return(res)
res= ridgeReg(0.001)
print (res.coef_)
print (res.intercept_)

现在让我们看看一些用于降维的 scikit-learn 算法。这对机器学习很重要,因为它减少了模型必须考虑的输入变量或特征的数量。这使得模型更加高效,并且可以使结果更容易解释。它还可以通过减少过拟合来提高模型的泛化能力。

当然,重要的是不要丢弃会降低模型准确性的信息。确定什么是冗余的或不相关的是降维算法的主要功能。基本上有两种方法:特征提取和特征选择。特征选择试图找到原始特征变量的子集。另一方面,特征提取通过组合相关变量来创建新的特征变量。

我们先来看看大概最常见的特征提取算法,也就是主成分分析或者 PCA 。这使用正交变换将一组相关变量转换为一组不相关变量。重要的信息、向量的长度以及它们之间的角度不会改变。这些信息在内积中定义,并保存在正交变换中。主成分分析构建特征向量的方式是,第一个成分尽可能多地解释数据的可变性。随后的组成部分解释了变化量的减少。这意味着,对于许多模型,我们可以只选择前几个主要组件,直到我们确信它们在我们的数据中所占的可变性与实验规范所要求的一样多。

可能最通用的核函数,也是在大多数情况下给出好结果的核函数,是径向基函数 ( 径向基函数)。rbf 核取一个参数gamma,这个参数可以松散地解释为每个样本的影响范围的倒数。γ值较低意味着每个样本对模型选择的样本具有较大的影响半径。KernalPCA fit_transform方法获取训练向量,将其拟合到模型,然后将其转换为其主成分。让我们看看命令:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import KernelPCA
from sklearn.datasets import make_circles
np.random.seed(0)
X, y = make_circles(n_samples=400, factor=.3, noise=.05)
kpca = KernelPCA(kernel='rbf', gamma=10)
X_kpca = kpca.fit_transform(X)
plt.figure()
plt.subplot(2, 2, 1, aspect='equal')
plt.title("Original space")
reds = y == 0
blues = y == 1
plt.plot(X[reds, 0], X[reds, 1], "ro")
plt.plot(X[blues, 0], X[blues, 1], "bo")
plt.xlabel("$x_1$")
plt.ylabel("$x_2$")
plt.subplot(2, 2, 3, aspect='equal')
plt.plot(X_kpca[reds, 0], X_kpca[reds, 1], "ro")
plt.plot(X_kpca[blues, 0], X_kpca[blues, 1], "bo")
plt.title("Projection by KPCA")
plt.xlabel("1st principal component in space induced by $\phi$")
plt.ylabel("2nd component")
plt.subplots_adjust(0.02, 0.10, 0.98, 0.94, 0.04, 0.35)
plt.show()
#print('gamma= %0.2f' %gamma)

正如我们所看到的,监督学习算法成功的一个主要障碍是从训练数据到测试数据的转换。标记的训练集可能具有新的未标记数据中不存在的独特特征。我们已经看到,我们可以训练我们的模型在训练数据上相当精确,然而这种精度可能不会转化为我们的未标记测试数据。过拟合是监督学习中的一个重要问题,有许多技术可以用来最小化它。评估模型在训练集上的估计器性能的一种方法是使用交叉验证。让我们使用支持向量机在虹膜数据上进行测试。我们需要做的第一件事是将数据分成训练集和测试集。train_test_split方法采用两种数据结构:数据本身和目标。它们可以是 NumPy 数组、熊猫数据帧列表或 SciPy 矩阵。如您所料,目标需要与数据一样长。test_size参数可以是一个介于01之间的浮点数,表示分割中包含的数据比例,也可以是一个表示测试样本数量的整数。这里,我们使用了一个test_size对象作为.3,表示我们保留了 40%的数据用于测试。

在本例中,我们使用svm.SVC类和.score方法返回预测标签时测试数据的平均准确度:

from sklearn.cross_validation import train_test_split
from sklearn import datasets
from sklearn import svm
from sklearn import cross_validation
iris = datasets.load_iris()
X_train, X_test, y_train, y_test = train_test_split (iris.data, iris.target, test_size=0.4, random_state=0)
clf = svm.SVC(kernel='linear', C=1).fit(X_train, y_train)
scores=cross_validation.cross_val_score(clf, X_train, y_train, cv=5)
print("Accuracy: %0.2f (+/- %0.2f)" % (scores.mean(), scores.std() * 2))

您将看到以下输出:

Scikit-learn

支持向量机有一个penalty参数需要手动设置,很有可能我们会多次运行 SVC 并调整这个参数,直到得到最佳拟合。然而,这样做将信息从训练集泄露到测试集,因此我们可能仍然存在过度拟合的问题。对于任何具有必须手动设置的参数的估计器来说,这都是一个问题,我们将在第 4 章模型–从信息中学习中对此进行进一步探讨。

总结

我们已经看到了一套基本的机器学习工具,以及它们在简单数据集上的一些用法。你可能开始想知道这些工具如何应用于现实世界的问题。我们讨论过的每个库之间都有相当多的重叠。许多人执行相同的任务,但以不同的方式添加或执行相同的功能。为每个问题选择使用哪个库不一定是一个明确的决定。没有最好的图书馆;只有首选库,这因人而异,当然也因应用的具体情况而异。

在下一章中,我们将研究机器学习最重要的,也是经常被忽视的一个方面,那就是数据。

三、将数据转化为信息

原始数据可以有许多不同的格式,数量和质量各不相同。有时,我们被数据淹没,有时我们努力从数据中获取每一滴信息。数据要成为信息,需要一些有意义的结构。我们经常不得不处理不兼容的格式、不一致、错误和丢失的数据。能够访问数据集的不同部分或基于一些关系标准提取数据子集是很重要的。我们需要发现数据中的模式,并了解数据是如何分布的。我们可以使用许多工具从可视化、运行算法或仅仅查看电子表格中的数据中找到隐藏在数据中的信息。

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

  • 大数据
  • 数据属性
  • 数据源
  • 数据处理和分析

但是首先,让我们来看看下面的解释:

什么是数据?

数据可以存储在硬盘上,通过网络传输,或者通过摄像机和麦克风等传感器实时捕捉。如果我们从物理现象中取样,比如视频或录音,那么空间是连续的,实际上是无限的。一旦这个空间被采样,即被数字化,这个空间的有限子集已经被创建,并且至少一些最小的结构已经被强加在它上面。数据在硬盘上,以位编码,给定一些属性,如名称、创建日期等。除此之外,如果要在应用中使用数据,我们需要问,“数据是如何组织的,它有效地支持什么类型的查询?”

当面对看不见的数据集时,第一阶段是探索。数据探索包括检查数据的组成部分和结构。它包含多少个样本,每个样本有多少个维度?每个维度的数据类型是什么?我们还应该了解变量之间的关系以及它们是如何分布的。我们需要检查数据值是否符合我们的预期。数据是否有明显的错误或差距?

数据探索必须在特定问题的范围内进行。显然,首先要弄清楚的是数据集是否有可能提供有用的答案。值得我们花时间继续,还是需要收集更多的数据?探索性数据分析不一定是在特定假设的情况下进行的,但也许是在某种意义上,哪些假设可能会提供有用的信息。

数据是支持或否定假设的证据。这个证据只有在可以与一个相互竞争的假设相比较时才有意义。在任何科学过程中,我们都使用控制。为了检验一个假设,我们需要将其与一个等价系统进行比较,在这个等价系统中,我们感兴趣的变量集保持不变。我们应该试图用一种机制和解释来说明因果关系。我们需要一个合理的理由来进行观察。我们还应该考虑现实世界是由多个相互作用的组件组成的,处理多元数据会导致复杂性呈指数级增长。

正是考虑到这些事情,我们正在寻求探索的领域的草图,我们接近新的数据集。我们有一个目标,一个我们希望达到的点,我们的数据是一张穿越这个未知地形的地图。

大数据

在全球范围内创建和存储的数据量几乎是不可思议的,而且还在持续增长。大数据是一个描述大量数据的术语,包括结构化数据和非结构化数据。现在,让我们从大数据的挑战开始,深入研究大数据。

大数据的挑战

大数据是以三大挑战为特征的。它们如下:

  • 数据量
  • 数据的速度
  • 数据的多样性

数据量

体积问题可以从三个不同的方向来处理:效率伸缩性平行度。效率是关于最小化算法处理一个信息单元所需的时间。其中的一个组成部分是硬件的底层处理能力。另一个组件,也是我们更能控制的组件,是确保我们的算法不会因为不必要的任务而浪费宝贵的处理周期。

可伸缩性实际上是关于蛮力和尽可能多地向问题扔硬件。考虑到摩尔定律,该定律指出计算机功率每两年翻一番的趋势将持续下去,直到达到极限;很明显,可扩展性本身无法跟上不断增长的数据量。在许多情况下,简单地增加更多内存和更快的处理器并不是一个经济高效的解决方案。

并行是机器学习中一个不断发展的领域,它包含了许多不同的方法,从利用多核处理器的能力,到许多不同平台上的大规模分布式计算。最常见的方法可能是在许多机器上简单地运行相同的算法,每台机器都有不同的参数集。另一种方法是将学习算法分解成自适应的查询序列,并并行处理这些查询。这种技术的一个常见实现被称为 MapReduce ,或其开源版本 Hadoop

数据速度

速度问题通常从数据生产者和数据消费者的角度来处理。两者之间的数据传输速率称为速度,可以用交互响应时间来衡量。这是从发出查询到发送响应的时间。响应时间受到延迟的限制,例如硬盘读写时间以及通过网络传输数据所需的时间。

数据的生产速度越来越快,这在很大程度上是由移动网络和设备的快速扩张推动的。日常生活中日益增多的仪器仪表正在彻底改变产品和服务的交付方式。这种不断增加的数据流导致了流处理的想法。当输入数据的速度使其无法完整存储时,必须进行一定程度的分析,因为数据流本质上决定了哪些数据是有用的,哪些数据应该存储,哪些数据可以丢弃。一个极端的例子是欧洲粒子物理研究所的大型强子对撞机,在那里的绝大多数数据被丢弃。一个复杂的算法必须在数据生成时对其进行扫描,在数据干草堆中寻找信息。处理数据流可能很重要的另一个例子是当应用需要立即响应时。这越来越多地用于在线游戏和股市交易等应用。

我们感兴趣的不仅仅是传入数据的速度;在许多应用中,特别是在 web 上,系统输出的速度也很重要。考虑像推荐系统这样的应用,它们需要处理大量数据,并在网页加载所需的时间内给出响应。

数据种类

从不同来源收集数据总是意味着要处理不一致的数据结构和不兼容的格式。这通常也意味着要处理不同的语义,并且必须理解一个可能建立在完全不同的逻辑前提上的数据系统。我们必须记住,通常情况下,数据会被重新用于一个完全不同于其最初用途的应用。有各种各样的数据格式和底层平台。将数据转换为一种一致的格式会花费大量时间。即使这样做了,数据本身也需要对齐,以便每条记录由相同数量的特征组成,并以相同的单位进行测量。

考虑从网页获取数据这一相对简单的任务。数据已经通过使用标记语言进行了结构化,通常是 HTML 或 XML,这可以帮助我们获得一些初始结构。然而,我们只需要细读网络就可以发现,没有标准的方式来以信息相关的方式呈现和标记内容。XML 的目的是将与内容相关的信息包含在标记标签中,例如,通过为作者主题使用标签。然而,这种标签的使用远非普遍和一致的。此外,网络是一个动态的环境,许多网站经历频繁的结构变化。这些变化通常会破坏期望特定页面结构的 web 应用。

下图显示了大数据挑战的两个方面。我已经包括了几个例子,这些领域可能大约位于这个空间。例如,天文学的来源很少。它的望远镜和天文台数量相对较少。然而天文学家处理的数据量是巨大的。另一方面,也许,让我们将其与环境科学进行比较,环境科学的数据来自各种来源,如遥感器、实地调查、经过验证的辅助材料等。

Data variety

集成不同的数据集可能会花费大量的开发时间;在某些情况下高达 90%。每个项目的数据需求都是不同的,设计过程的一个重要部分是根据这三个元素来定位我们的数据集。

数据模型

数据科学家面临的一个基本问题是数据是如何存储的。我们可以谈谈硬件,在这个方面,我们指的是非易失性存储器,比如电脑的硬盘或者闪存盘。解释这个问题的另一种方式(更符合逻辑的方式)是数据是如何组织的?在个人电脑中,最明显的数据存储方式是分层的,在嵌套的文件夹和文件中。数据也可以存储在表格格式或电子表格中。当我们考虑结构时,我们对类别和类别类型以及它们之间的关系感兴趣。在一个表中,我们需要多少列,在一个关系数据库中,表是如何链接的?数据模型不应该试图在数据上强加一个结构,而是应该找到一个最自然地从数据中出现的结构。

数据模型由三个部分组成:

  • 结构:一个表组织成列和行;树形结构有节点和边,字典有键值对的结构。
  • 约束:这定义了有效结构的类型。对于一个表,这将包括这样一个事实,即所有的行都有相同数量的列,并且每一列都包含相同的数据类型。例如,列items sold将只包含整数值。对于层次结构,约束是只能有一个直接父级的文件夹。
  • 操作:这包括查找特定值、给定一个键或查找所有售出商品大于 100 的行等操作。这有时被认为是与数据模型分开的,因为它通常是一个更高层次的软件层。然而,所有这三个组件都是紧密耦合的,因此将操作视为数据模型的一部分是有意义的。

为了用数据模型封装原始数据,我们创建了数据库。数据库解决了一些关键问题:

  • 它们允许我们共享数据:它让多个用户以不同的读写权限访问相同的数据。
  • 他们实施数据模型:这不仅包括结构强加的约束,比如层次结构中的父子关系,还包括更高级别的约束,比如只允许一个名为鲍勃的用户,或者是 1 到 8 之间的数字。
  • 它们允许我们扩展:一旦数据大于我们的易失性存储器的分配大小,就需要机制来促进数据的传输,也允许高效地遍历大量的行和列。
  • 数据库允许灵活性:它们本质上试图隐藏复杂性,并提供与数据交互的标准方式。

数据分布

数据的一个关键特征是它的概率分布。最常见的分布是正态或高斯分布。这种分布见于许多(所有?)物理系统,它是任何随机过程的基础。正态函数可以用概率密度函数来定义:

Data distributions

这里,δ(σ)为标准差(μ)为均值。这个方程简单描述了随机变量 x 取给定值的相对可能性。我们可以把标准差解释为钟形曲线的宽度,平均值解释为钟形曲线的中心。有时使用术语方差,这只是标准差的平方。标准偏差本质上衡量的是数值的分布情况。作为一般经验法则,在正态分布中,68%的值在平均值的 1 个标准差以内,95%的值在平均值的 2 个标准差以内,99.7%的值在平均值的 3 个标准差以内。

**我们可以通过运行下面的代码并使用不同的平均值和方差值调用normal()函数来感受这些术语的作用。在这个例子中,我们创建了一个正态分布图,平均值为1,方差为0.5:

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

def normal(mean = 0, var = 1):
 sigma = np.sqrt(var)
 x = np.linspace(-3,3,100)
 plt.plot(x,mlab.normpdf(x,mean,sigma))
 plt.show()

normal(1,0.5)

Data distributions

与高斯分布相关的是二项式分布。我们实际上通过重复一个二项式过程获得了一个正态分布,比如抛硬币。随着时间的推移,一半的抛投将导致头部的概率接近。

Data distributions

在这个公式中, n 是抛硬币的数量, p 是一半抛硬币是正面的概率, q 是一半抛硬币是反面的概率( 1-p )。在一个典型的实验中,比方说确定一系列抛硬币的各种结果的概率, n ,我们可以执行这个多次,显然我们执行实验的次数越多,我们对系统统计行为的理解就越好:

from scipy.stats import binom
def binomial(x=10,n=10, p=0.5):
 fig, ax = plt.subplots(1, 1)
 x=range(x)
 rv = binom(n, p)
 plt.vlines(x, 0, (rv.pmf(x)), colors='k', linestyles='-')
 plt.show()
binomial()

您将看到以下输出:

Data distributions

离散分布的另一个方面是理解给定数量的事件在特定空间和/或时间内发生的可能性。如果我们知道一个给定的事件以平均速率发生,并且每个事件独立发生,我们可以将其描述为泊松分布。我们可以使用概率质量函数来最好地理解这个分布。这测量了给定事件在给定时间/空间点发生的概率。

泊松分布有两个与之相关的参数:λλ ,一个大于 0 的实数,以及 k ,一个 0、1、2 等整数。

Data distributions

这里,我们使用scipy.stats模块生成泊松分布图:

from scipy.stats import poisson
def pois(x=1000):
 xr=range(x)
 ps=poisson(xr)
 plt.plot(ps.pmf(x/2))
pois()

上述命令的输出如下图所示:

Data distributions

我们可以用概率密度函数来描述连续的数据分布。这描述了连续随机变量取指定值的可能性。对于单变量分布,即只有一个随机变量的分布,在区间( a,b )上找到一个点 X 的概率由下式给出:

Data distributions

这描述了采样总体的一部分,其值 x 位于 ab 之间。密度函数真的只有在它们被整合时才有意义,这将告诉我们一个种群围绕某些值分布有多密集。直观上,我们将其理解为这两点之间其概率函数图下的面积。累积密度函数 ( CDF )定义为其概率密度函数的积分, fx :

Data distributions

CDF 描述了特定变量的值小于 x 的抽样总体的比例。下面的代码显示了一个离散(二项式)累积分布函数。 s1s2 形状参数决定步长:

import scipy.stats as stats
def cdf(s1=50,s2=0.2):

 x = np.linspace(0,s2 * 100,s1 *2)
 cd = stats.binom.cdf
 plt.plot(x,cd(x, s1, s2))
 plt.show()

来自数据库的数据

我们通常通过查询语言与数据库交互。最流行的查询语言之一是 MySQL。Python 有一个数据库规范 PEP 0249,它创建了一种一致的方式来处理众多的数据库类型。这使得我们编写的代码在数据库之间更具可移植性,并允许更丰富的数据库连接范围。为了说明这有多简单,我们将以mysql.connector类为例。MySQL 是最流行的数据库格式之一,它有一种直截了当的、人类可读的查询语言。为了练习使用这个类,你需要在你的机器上安装一个 MySQL 服务器。这可以从https://dev.mysql.com/downloads/mysql/获得。

这也应该伴随着一个名为世界的测试数据库,其中包括世界城市的统计数据。

确保 MySQL 服务器正在运行,并运行以下代码:

import mysql.connector
from mysql.connector import errorcode

cnx = mysql.connector.connect(user='root', password='password',
 database='world', buffered=True)
cursor=cnx.cursor(buffered=True)
query=("select * from city where population > 1000000 order by population")
cursor.execute(query)
worldList=[]
for (city) in cursor:
 worldList.append([city[1],city[4]])
cursor.close()
cnx.close()

来自网络的数据

网络上的信息被组织成 HTML 或 XML 文档。标记标签为我们提供了清晰的钩子,以便我们对数据进行采样。数字数据通常会出现在表格中,这使得它相对容易使用,因为它已经以有意义的方式构建。让我们来看看一个 HTML 文档的典型摘录:

<table border="0" cellpadding="5" cellspacing="2" class="details" width="95%">
  <tbody>

  <th>Species</th>
  <th>Data1</th>
  <th>data2</th>
  </tr>

  <td>whitefly</td>
  <td>24</td>
  <td>76</td>
  </tr>
  </tbody>
</table>

这显示了表格的前两行,有一个标题和一行包含两个值的数据。Python 有一个优秀的库Beautiful Soup,用于从 HTML 和 XML 文档中提取数据。在这里,我们将一些测试数据读入一个数组,并将其转换成适合机器学习算法输入的格式,比如线性分类器:

import urllib
from bs4 import BeautifulSoup
import numpy as np

url = urllib.request.urlopen("http://interthing.org/dmls/species.html");
html = url.read()
soup = BeautifulSoup(html, "lxml")
table = soup.find("table")

headings = [th.get_text() for th in table.find("tr").find_all("th")]

datasets = []
for row in table.find_all("tr")[1:]:
 dataset = list(zip(headings, (td.get_text() for td in row.find_all("td"))))
 datasets.append(dataset)

nd=np.array(datasets)
features=nd[:,1:,1].astype('float')
targets=(nd[:,0,1:]).astype('str')
print(features)
print(targets)

我们可以看到,这是相对直截了当的。我们需要注意的是,我们正在依赖我们的源网页保持不变,至少在它的整体结构方面是如此。以这种方式从网络上获取数据的一个主要困难是,如果网站所有者决定改变他们页面的布局,它可能会破坏我们的代码。

你可能会遇到的另一种数据格式是 JSON 格式。JSON 最初用于序列化 Javascript 对象,但是不依赖于 JavaScript。它只是一种编码格式。JSON 很有用,因为它可以表示分层和多元数据结构。它基本上是键值对的集合:

{"Languages":[{"Language":"Python","Version":"0"},{"Language":"PHP","Version":"5"}],
"OS":{"Microsoft":"Windows 10", "Linux":"Ubuntu 14"},
"Name":"John\"the fictional\" Doe",
"location":{"Street":"Some Street", "Suburb":"Some Suburb"},
"Languages":[{"Language":"Python","Version":"0"},{"Language":"PHP","Version":"5"}]
}

如果我们将前面的 JSON 保存到一个名为jsondata.json的文件中:

import json
from pprint import pprint

with open('jsondata.json') as file: 
 data = json.load(file)

pprint(data)

来自自然语言的数据

自然语言处理是机器学习中比较难做的事情之一,因为它关注的是目前机器不太擅长什么:理解复杂现象中的结构。

作为一个起点,我们可以对我们正在考虑的问题空间做一些陈述。与特定会话中使用的单词子集相比,任何语言中的单词数量通常都很大。与它存在的空间相比,我们的数据是稀疏的。此外,单词往往以预先定义的顺序出现。某些词更有可能一起出现。句子有一定的结构。不同的社交环境,比如在工作、家庭或外出社交;或者在正式的环境中,如与监管机构、政府和官僚机构沟通,都需要使用重叠的词汇子集。从肢体语言、语调、眼神交流等线索来看,社交环境可能是试图从自然语言中提取意义时最重要的因素。

要在 Python 中使用自然语言,我们可以使用自然语言工具包 ( NLTK )。如果没有安装,可以执行pip install -U nltk命令。

NLTK 也附带了一个庞大的词汇资源库。您需要单独下载这些文件,NLTK 有一个下载管理器,可通过以下代码访问:

import nltk
nltk.download()

应该会打开一个窗口,您可以在其中浏览各种文件。这包括一系列书籍和其他书面材料,以及各种词汇模型。要开始,你只需下载软件包,Book

文本语料库是由许多单独的文本文件组成的庞大文本体。NLTK 附带语料库 来自各种来源,如古典文学(古腾堡语料库)、网络和聊天文本、路透社新闻,以及包含按体裁分类的文本的语料库,如新闻、社论、宗教、小说等。您也可以使用以下代码加载任何文本文件集合:

from nltk.corpus import PlaintextCorpusReader
corpusRoot= 'path/to/corpus'
yourCorpus=PlaintextCorpusReader(corpusRoot, '.*')

PlaintextCorpusReader方法的第二个参数是一个正则表达式,指示要包含的文件。在这里,它只是表示该目录中的所有文件都包括在内。第二个参数也可以是文件位置列表,如['file1', 'dir2/file2']

让我们看一下现有的一个语料库,作为示例,我们将加载 Brown 语料库:

from nltk.corpus import brown
cat=brown.categories()
print(cat)

['adventure', 'belles_lettres', 'editorial', 'fiction', 'government', 'hobbies', 'humor', 'learned', 'lore', 'mystery', 'news', 'religion', 'reviews', 'romance', 'science_fiction']

布朗语料库是有用的,因为它使我们能够研究体裁之间的系统差异。这里有一个例子:

from nltk.corpus import brown
cats=brown.categories()
for cat in cats:
 text=brown.words(categories=cat)
 fdist = nltk.FreqDist(w.lower() for w in text)
 posmod = ['love', 'happy', 'good', 'clean']
 negmod = ['hate', 'sad', 'bad', 'dirty']

    pcount=[]
 ncount=[] 
 for m in posmod:
 pcount.append(fdist[m])
 for m in negmod:
 ncount.append(fdist[m])

 print(cat + ' positive: ' + str(sum(pcount)))
 print(cat + ' negative: ' + str(sum(ncount)))
 rat=sum(pcount)/sum(ncount)
 print('ratio= %s'%rat )
 print() 

在这里,我们通过比较四个积极情感词的出现及其反义词,从不同的体裁中提取情感数据。

来自图像的数据

图像是丰富且容易获得的数据来源,并且它们对于学习应用有用,例如对象识别、分组、对象分级以及图像增强。图像,当然,可以放在一起作为一个时间序列。动画图像对演示和分析都很有用;例如,我们可以使用视频来研究轨迹、监控环境和学习动态行为。

图像数据被构造为网格或矩阵,颜色值被分配给每个像素。我们可以通过使用 Python 图像库来了解这是如何工作的。对于本例,您需要执行以下几行:

from PIL import Image
from matplotlib import pyplot as plt
import numpy as np
image= np.array(Image.open('data/sampleImage.jpg'))
plt.imshow(image, interpolation='nearest')
plt.show()
print(image.shape)

Out[10]: (536, 800, 3)

我们可以看到这个特定的图像宽 536 像素,高 800 像素。每个像素有 3 个值,分别代表红色、绿色和蓝色的 0 到 255 之间的颜色值。注意坐标系的原点( 0,0 )是左上角。一旦我们将图像作为 NumPy 阵列,我们就可以开始以有趣的方式使用它们,例如,拍摄切片:

im2=image[0:100,0:100,2]

来自应用编程接口的数据

许多社交网络平台都有应用编程接口(API),让程序员可以访问各种功能。这些接口可以生成大量的流数据。这些 API 中有许多对 Python 3 和其他一些操作系统有可变的支持,所以要准备好做一些关于系统兼容性的研究。

获得对平台应用编程接口的访问通常包括向供应商注册应用,然后使用提供的安全凭据(如公钥和私钥)来验证您的应用。

让我们来看看推特的应用编程接口,它相对容易访问,并且有一个开发良好的 Python 库。首先,我们需要加载推特库。如果还没有,只需在 Python 命令提示符下执行pip install twitter命令。

你需要一个推特账户。登录并前往apps.twitter.com。点击新建应用按钮,在新建应用页面填写详细信息。提交后,您可以通过从应用管理页面点击您的应用,然后点击密钥和访问令牌选项卡来访问您的凭据信息。

这里我们感兴趣的四个项目是应用编程接口密钥、应用编程接口秘密、访问令牌和访问令牌秘密。现在,创建我们的Twitter对象:

from twitter import Twitter, OAuth
#create our twitter object
t = Twitter(auth=OAuth(accesToken, secretToken, apiKey, apiSecret))

#get our home time line
home=t.statuses.home_timeline()

#get a public timeline
anyone= t.statuses.user_timeline(screen_name="abc730")

#search for a hash tag 
pycon=t.search.tweets(q="#pycon")

#The screen name of the user who wrote the first 'tweet'
user=anyone[0]['user']['screen_name']

#time tweet was created
created=anyone[0]['created_at']

#the text of the tweet
text= anyone[0]['text']

当然,您需要填写之前从推特获得的授权凭证。请记住在一个公共可访问的应用中,您永远不会以人类可读的形式拥有这些凭证,当然也不会在文件本身中,最好在公共目录之外加密。

信号

初级科学研究中经常遇到的一种数据形式是各种二进制流。视频和音频传输和存储有特定的编解码器,通常,我们在寻找更高级别的工具来处理每种特定的格式。我们可能会考虑各种信号源,例如来自射电望远镜、相机上的传感器或麦克风的电脉冲。信号都有基于波动力学和谐波运动的相同的基本原理。

通常使用时频分析来研究信号。这里的核心概念是,时间和空间上的连续信号可以分解成频率分量。我们使用所谓的傅立叶变换在时域和频域之间移动。这利用了一个有趣的事实,即任何给定的函数,包括非周期函数,都可以用一系列正弦和余弦函数来表示。这可以通过以下内容来说明:

Signals

为了使其有用,我们需要找到anT3】和bnT7】的值。我们通过将方程的两边乘以余弦、 mx 并积分来实现。这里 m 是整数。**

Signals

这被称为正交函数,类似于我们如何考虑 xyz 在向量空间中是正交的。现在,如果你能记住你所有的三角函数,你就会知道整数系数的正弦乘以余弦在负ππ之间总是零。如果我们做一下计算,原来左手边的中项是零,除了 n 等于 m 的时候。在这种情况下,术语等于π。了解了这一点,我们可以写出以下内容:

**Signals

所以,在第一步中,如果我们乘以 sin mx 而不是余弦 mx ,那么我们就可以推导出 b n 的值。

Signals

我们可以看到,我们已经将一个信号分解为一系列正弦值和余弦值。这使我们能够分离信号的频率成分。

声音数据

音频是最常见且最容易研究的信号之一。我们将使用soundfile模块。没有的话可以通过pip安装。soundfile模块有一个wavfile.read类,它以 NumPy 数组的形式返回.wav文件数据。要尝试以下代码,您将需要一个名为 audioSamp.wav的 16 位短波形文件。可从davejulian.net/mlbook下载。将其保存在您的数据目录中,即您的工作目录中:

import soundfile as sf
import matplotlib.pyplot as plt
import numpy as np

sig, samplerate = sf.read('data/audioSamp.wav')
sig.shape

我们看到声音文件由许多样本表示,每个样本有两个值。这实际上是作为向量的函数,它描述了.wav文件。当然,我们可以创建声音文件的片段:

slice=sig[0:500,:]

在这里,我们对前 500 个样本进行切片。让我们计算切片的傅立叶变换并绘制出来:

ft=np.abs(np.fft.fft(slice))
Finally lets plot the result
plt.plot(ft)
plt.plot(slice)

上述命令的输出如下:

Data from sound

清洁数据

为了了解特定数据集可能需要哪些清理操作,我们需要考虑数据是如何收集的。主要的清理操作之一是处理丢失的数据。我们在上一章已经遇到了这样的例子,当时我们检查了温度数据。在这种情况下,数据有一个质量参数,所以我们可以简单地排除不完整的数据。然而,这可能不是许多应用的最佳解决方案。可能需要填写缺失的数据。我们如何决定使用什么数据?就我们的温度数据而言,我们可以用一年中那个时间的平均值来填充缺失的值。请注意,我们预先假定了一些领域知识,例如,数据或多或少是周期性的;它符合季节周期。因此,这是一个公平的假设,即我们可以对我们有可靠记录的每一年的特定日期取平均值。然而,考虑到我们正试图找到一个代表由于气候变化导致的温度升高的信号。在这种情况下,取所有年份的平均值会扭曲数据,并可能隐藏一个可能表明变暖的信号。同样,这需要额外的知识,并且是具体的关于我们实际上想从数据中学到什么。

另一个考虑因素是缺失数据可能是以下三种类型之一:

  • empty
  • zero
  • null

不同的编程环境可能会对这些稍有不同。在这三个中,只有零是可测量的量。我们知道,零可以放在 1、2、3 等之前的数字行上,我们可以将其他数字与零进行比较。所以,通常零被编码为数字数据。空不一定是数字,尽管是空的,但它们可以传递信息。例如,如果表单中有一个中间名的字段,而填写表单的人没有中间名,那么empty字段就准确地代表了一种特定的情况,即没有中间名。这又一次取决于领域。在我们的温度数据中,empty字段表示缺少数据,因为没有最高温度对于特定的一天没有意义。另一方面,在计算中,空值意味着与日常使用略有不同的东西。对于计算机科学家来说,空值和无值或零不是一回事。空值不能与其他任何值进行比较;它们表明字段没有条目是有正当理由的。空值不同于空值。在我们的中间名示例中,空值表示该人是否有中间名是未知的。

另一个常见的数据清理任务是将数据转换为特定的格式。出于我们这里的目的,我们感兴趣的最终数据格式是 Python 数据结构,例如 NumPy 数组。我们已经研究了从 JSON 和 HTML 格式转换数据,这是相当直接的。

我们可能会遇到的另一种格式是杂技演员的便携式文档格式(T2)。从 PDF 文件导入数据可能相当困难,因为 PDF 文件是基于页面布局原语构建的,与 HTML 或 JSON 不同,它们没有有意义的标记标签。有几种非 Python 工具可以将 pdf 转化为文本,如pdf text。这是一个命令行工具,包含在许多 Linux 发行版中,也适用于 Windows。一旦我们将 PDF 文件转换成文本,我们仍然需要提取数据,嵌入文档中的数据决定了我们如何提取它。如果数据与文档的其他部分分离,比如在一个表中,那么我们可以使用 Python 的文本解析工具来提取它。或者,我们可以使用 Python 库来处理 PDF 文档,如 pdfminer3k

另一个常见的清理任务是在数据类型之间进行转换。在类型之间转换时,总是有丢失数据的风险。当目标类型存储的数据比源类型少时,例如,从浮点数 32 转换为浮点数 16,就会出现这种情况。有时,我们需要在文件级别转换数据。当文件具有隐式类型结构时,例如电子表格,就会出现这种情况。这通常是在创建文件的应用中完成的。例如,Excel 电子表格可以保存为逗号分隔的文本文件,然后导入到 Python 应用中。

可视化数据

我们为什么直观地表示数据有很多原因。在数据探索阶段,我们可以立即了解数据属性。可视化表示用于突出数据中的模式并建议建模策略。探索图通常制作得很快,而且数量很多。我们不太关心审美或风格问题,只是想看看数据是什么样的。

除了使用图表来探索数据,它们还是交流数据信息的主要手段。可视化表示有助于阐明数据属性并激发观众参与度。人类视觉系统是通向大脑的带宽最高的通道,可视化是呈现大量信息的最有效方式。通过创建可视化,我们可以立即获得重要参数的感觉,例如数据中可能存在的最大值、最小值和趋势。当然,这些信息可以通过统计分析从数据中提取,但是,分析可能不会揭示可视化将揭示的数据中的特定模式。目前,人类视觉模式识别系统明显优于机器。除非我们有关于我们在寻找什么的线索,否则算法可能不会挑选出人类视觉系统会选择的重要模式。

数据可视化的核心问题是将数据元素映射到视觉属性。为此,我们首先将数据类型分类为标称、顺序或数量,然后确定哪些视觉属性最有效地表示每种数据类型。名义或分类数据指的是一个名称,如物种、雄性或雌性等等。名义数据没有特定的顺序或数值。序数数据有一个内在的顺序,比如一条街上的门牌号,但它不同于数量数据,因为它并不意味着一个数学区间。例如,房屋号码的乘法或除法没有多大意义。定量数据有一个数值,如大小或体积。显然,某些视觉属性不适合标称数据,例如大小或位置;它们暗示着序数或数量信息。

有时,不能立即清楚特定数据集中的每种数据类型是什么。消除歧义的一种方法是找出适用于每种数据类型的操作。例如,当我们比较名义数据时,我们可以使用 equals,例如,物种白粉虱不是等于物种蓟马。但是,我们不能使用大于或小于等操作。从序数意义上说,一个物种比另一个物种大是没有意义的。有了序数数据,我们可以应用大于或小于等操作。序数数据有一个隐含的顺序,我们可以映射到一个数字线上。对于定量数据,这包括一个间隔,例如日期范围,我们可以对其应用附加操作,例如减法。例如,我们不仅可以说一个特定的日期发生在另一个日期之后,还可以计算两个日期之间的差异。对于具有固定轴的定量数据,即某个固定量与某个区间的比率,我们可以使用除法等运算。我们可以说一个特定物体的重量是另一个物体的两倍或两倍长。

一旦我们清楚了我们的数据类型,我们就可以开始将它们映射到属性。这里,我们将考虑六个视觉属性。它们是位置、大小、纹理、颜色、方向和形状。其中,只有位置和大小可以准确表示所有三种类型的数据。另一方面,纹理、颜色、方向和形状只能准确地表示名义数据。我们不能说一种形状或颜色大于另一种。但是,我们可以将特定的颜色或纹理与名称相关联。

另一个需要考虑的是这些视觉属性的感知属性。心理学和心理物理学的研究已经证实,视觉属性可以根据它们被感知的准确程度来排序。位置感知最准确,其次是长度、角度、坡度、面积、体积,最后是颜色和密度,感知的准确性最低。因此,给最重要的定量数据分配位置和长度是有意义的。最后,还应该提到的是,在某种程度上,我们可以将序数数据编码为颜色值(从暗到亮)或颜色渐变中的连续数据。我们通常不能用色调来编码这些数据。例如,没有理由认为蓝色比红色大,除非你提到它的频率。

Visualizing data

表示序数数据的颜色渐变

接下来要考虑的是我们需要显示的维度数量。对于单变量数据,也就是说,我们只需要显示一个变量,我们有很多选择,如点、线或箱线图。对于双变量数据,我们需要显示两个维度,最常见的是散点图。对于三变量数据,可以使用 3D 绘图,这可用于绘制几何函数,如流形。然而,对于许多数据类型来说,三维图有一些缺点。在三维图上计算相对距离可能是个问题。例如,在下图中,很难测量每个元素的确切位置。但是,如果我们将 z 维度编码为大小,则相对值会变得更加明显:

Visualizing data

三维编码

将数据编码成可视属性有很大的设计空间。挑战在于找到适合我们特定数据集和目的的最佳映射。出发点应该是以感知最准确的方式编码最重要的信息。有效的可视化编码将描述所有的数据,而不是暗示数据中没有的任何东西。例如,长度意味着定量数据,因此将非定量数据编码成长度是不正确的。另一个需要考虑的方面是一致性。我们应该选择对每种数据类型最有意义的属性,并使用一致且定义良好的视觉样式。

总结

您已经了解到有大量的数据源、格式和结构。希望你已经对如何开始与其中一些人合作有了一些了解。重要的是要指出,在任何机器学习项目中,在这个基本级别上处理数据可能占整个项目开发时间的很大一部分。

在下一章中,我们将研究如何通过探索最常见的机器学习模型来使我们的数据发挥作用。****

四、模型——从信息中学习

到目前为止,在这本书里,我们已经检查了一系列的任务和技术。我们介绍了数据类型、结构和属性的基础知识,并熟悉了一些可用的机器学习工具。

在本章中,我们将研究三大类型的模型:

  • 逻辑模型
  • 树形模型
  • 规则模型

下一章将专门讨论另一种重要的模型——线性模型。本章的大部分材料都是理论性的,其目的是介绍机器学习任务所需的一些数学和逻辑工具。我鼓励你研究这些想法,并以可能有助于解决我们遇到的问题的方式来阐述它们。

逻辑模型

逻辑模型将实例空间(即所有可能或允许的实例的集合)划分为段。目标是确保每个数据段中的数据对于特定的任务是同质的。例如,如果任务是分类,那么我们的目标是确保每个片段包含同一个类的大多数实例。

逻辑模型使用逻辑表达式来解释特定的概念。最简单和最通用的逻辑表达式是文字,其中最常见的是等式。等式表达式可以应用于所有类型——主格、数字和序数。对于数字和序数类型,我们可以包括不等式文字:大于或小于。从这里,我们可以使用四个逻辑连接词来构建更复杂的表达式。这些是连词(逻辑 AND),用表示;析取(逻辑或),用表示;寓意;和否定,用表示。这为我们提供了一种表达以下等价关系的方法:

┌┌A ≡ A = A → B ≡ ┌A ∨ B

┌(a∧b)≡┌a∨┌b = ┌(a∨b)≡┌a∧┌b

我们可以用一个简单的例子来应用这些想法。假设你遇到一片看起来都来自同一物种的树林。我们的目标是确定这种树种的定义特征,用于分类任务。为了简单起见,假设我们只处理以下四个特征:

  • 大小:这个有三个值——小、中、大
  • 叶类型:这有两个值-缩放或非缩放
  • 水果:这有两个值——是或不是
  • 支撑物:这有两个值——是或不是

我们确定的第一棵树可以用下面的连词来描述:

大小=大∧叶=有鳞∧果=无∧支撑=是

我们遇到的下一棵树是中等大小的。如果我们去掉大小条件,那么语句就变得更一般了。也就是说,它将覆盖更多的样本:

叶子=有鳞∧果实=无∧支撑物=是

下一棵树也是中等大小的,但是它没有扶壁,所以我们去掉了这个条件,将其推广到下面:

叶=有鳞∧果=无

小树林里的树都满足这种结合,我们断定它们是针叶树。显然,在现实世界的例子中,我们将使用更大范围的特征和值,并采用更复杂的逻辑结构。然而,即使在这个简单的例子中,实例空间是 3 2 2 2,这使得有 24 个可能的实例。如果我们将一个特征的缺失视为一个附加值,那么假设空间,也就是我们可以用来描述这个集合的空间,就是 4 3 3 3 = 108 。可能的实例集或扩展集的数量为 2 24 。例如,如果你随机选择一组。例如,如果你随机选择一组实例,你能找到准确描述它们的合取概念的几率远远超过 100,000 比 1。

一般性排序

我们可以开始将这个假设空间从最一般的语句映射到最具体的语句。例如,在我们的针叶树假设附近,空间看起来像这样:

Generality ordering

这里,我们笼统地对我们的假设进行排序。顶部是最普遍的假设——所有的树都是针叶树。更一般的假设将覆盖更多的实例,因此最一般的假设,即所有的树都是针叶树,适用于所有的实例。现在,虽然这可能适用于我们所处的小树林,但当我们试图将这一假设应用于新数据,即小树林外的树木时,它将失败。在上图的底部,我们有一个最简单的假设。当我们进行更多的观察并在节点中向上移动时,我们可以消除假设并建立下一个最普遍的完整假设。我们能从数据中得出的最保守的概括是这些例子中最不一般的概括是(LGG)。我们可以将其理解为假设空间中从每个实例向上的路径相交的点。

**让我们用表格描述我们的观察结果:

|

大小

|

有鳞的

|

水果

|

支持

|

标签

|
| --- | --- | --- | --- | --- |
| L | Y | 普通 | Y | 第一亲代 |
| M | Y | 普通 | Y | p2 |
| M | Y | 普通 | 普通 | p3 |
| M | Y | 普通 | Y | p4 |

当然,你迟早会走出小树林,观察到负面的例子——显然不是针叶树的树。您注意到以下特征;

|

大小

|

有鳞的

|

水果

|

支持

|

标签

|
| --- | --- | --- | --- | --- |
| S | 普通 | 普通 | 普通 | n1 |
| M | 普通 | 普通 | 普通 | n2 |
| S | 普通 | Y | 普通 | n3 |
| M | Y | 普通 | 普通 | n4 |

所以,随着负例的加入,我们仍然可以看到,我们最不一般的完全假设仍然是标度= Y ∧水果=N 。然而,你会注意到一个负面的例子, n4 被覆盖了。因此,这个假设并不一致。

版本空间

这个简单的例子可能会让你得出只有一个 LGG 的结论。但这不一定是真的。我们可以通过添加一种称为内部析取的受限析取形式来扩展我们的假设空间。在我们之前的例子中,我们有三个中型或大型针叶树的正面例子。我们可以增加一个条件尺寸=中∨尺寸=大,我们可以把这个写成尺寸【m,l】。内部析取仅适用于具有两个以上值的要素,因为类似leaks = Scaled∨leaks = Non Scaled的东西总是true

在前面的针叶树例子中,我们去掉了大小条件,以适应我们的第二个和第三个观察。这给了我们以下的 LGG:

叶=缩放∧叶= =否

考虑到我们的内部分离,我们可以将前面的 LGG 改写如下:

大小[m,l] ∧叶=有鳞∧果=无

现在,考虑第一个非针叶树或负非针叶树的例子:

大小=小∧叶=无鳞∧果=无

我们可以去掉 LGG 的三个条件中的任何一个,但不包括这个反面例子。然而,当我们试图进一步推广到单个条件时,我们看到大小【m,l】叶子=缩放的是可以的,但是果实=不则不行,因为它涵盖了反面的例子。

现在我们感兴趣的是完备一致的假设,即涵盖了所有的正例,没有一个负例。现在让我们只考虑四个正的( p1 - p4 )例子和一个负的例子( n1 )来重新绘制我们的图表。

Version space

这有时被称为版本空间。请注意,我们有一个最不一般的假设,三个中间假设,现在,两个最一般的假设。版本空间形成凸集。这意味着我们可以在这个集合的成员之间进行插值。如果一个元素位于集合中最一般和最不一般的成员之间,那么它也是该集合的成员。这样,我们就可以通过版本空间最一般和最不一般的成员来充分描述版本空间。

考虑一种情况,其中最不一般的概括涵盖了一个或多个负面实例。在这种情况下,我们可以说数据不是合取可分的,版本空间为空。我们可以采用不同的方法来寻找最普遍的一致假设。这里我们感兴趣的是一致性,而不是完整性。这本质上涉及到从最一般的假设空间迭代路径。我们采取向下的步骤,例如,添加一个连词或从内部连词中移除一个值。在每一步,我们最小化结果假设的专门化。

覆盖空间

当我们的数据不能合取分离时,我们需要一种在一致性和完备性之间进行优化的方法。一个有用的方法是映射正负实例的覆盖空间,如下图所示:

Coverage space

我们可以看到,学习一个假设涉及到通过一般性排序的假设空间找到一条路径。逻辑模型包括通过网格结构的假设空间寻找路径。这个空间中的每个假设都包含一组实例。这些集合中的每一个集合都有上界和下界,并且按照一般性进行排序。到目前为止,我们只使用了文字的单个连词。有了丰富的逻辑语言,为什么不在我们的表达中加入各种逻辑连接词呢?基本上有两个原因可以让我们的表达保持简单,如下所示:

  • 更具表达性的语句会导致专门化,这会导致模型过度拟合训练数据,并且在测试数据上表现不佳
  • 复杂的描述在计算上比简单的描述更昂贵

正如我们在学习合取假设时所看到的,未被发现的正面例子允许我们从合取中删除文字,使其更具一般性。另一方面,涵盖的反面例子要求我们通过添加文字来增加专门化。

我们可以用子句的析取来描述每个假设,而不是用单个文字的连词来描述每个假设,其中每个子句可以是 A → B 的形式。这里, A 是文字的连词, B 是单个文字。让我们考虑以下陈述,它涵盖了一个负面的例子:

Butt =Y ∧ Scaled = N ∧ Size = S ∧ ┌水果= N

为了排除这个反面例子,我们可以写下面的子句:

对接= Y ∧缩放= N ∧大小= S →果实= N

当然还有其他排除否定的从句,比如 Butt = Y →水果= N;然而,我们对最具体的条款感兴趣,因为它不太可能排除涵盖的积极因素。

PAC 学习和计算复杂度

考虑到这一点,当我们增加逻辑语言的复杂性时,我们增加了计算成本,我们需要一个度量来衡量语言的可学习性。为了这些目的,我们可以使用 的思想大概正确 ( PAC )学习。

当我们从一组假设中选择一个假设时,目标是确保我们的选择具有高概率的低泛化误差。这将在测试集上以高精度执行。这就引入了 计算复杂度的思想。这是一种形式化方法,用于衡量给定算法的计算成本与其输出精度之间的关系。

PAC 学习考虑到了非典型例子上的错误,这种典型性是由一个不确定的概率分布 D 决定的。我们可以根据这个分布来评估一个假设的错误率。例如,让我们假设我们的数据是无噪声的,并且学习者总是在训练样本中输出完整且一致的假设。让我们选择一个任意误差率ϵ < 0.5 和一个故障率δ= 0.5。我们要求我们的学习算法输出一个概率≥ 1 - δ的假设,这样错误率将小于 ϵ 。事实证明,对于任何大小合理的训练集来说,这都是正确的。例如,如果我们的假设空间 H 包含单个不良假设,那么它在 n 独立训练样本上完整一致的概率小于或等于 (1 - ϵ) n 。对于任何 0 ≤ ϵ ≤ 1 ,该概率小于 e-n ϵ 。我们需要将其保持在我们的错误率 δ 以下,这是我们通过设置 n ≥ 1/ ϵ ln 1/ δ 实现的。现在,如果 H 包含多个不良假设, k ≤ | H | ,那么在 n 个独立样本上至少有一个是完整一致的概率最大:

k(1-a1)n≤h |(1-a1)n≤h | e-n〖t1]

如果满足以下条件,该最大值将小于 f :

PAC learning and computational complexity

这就是所谓的 样本复杂度,你会注意到它在 1/δ 中是对数的,在 1/ϵ 中是线性的。

这意味着降低故障率比降低错误率要便宜很多。

在结束这一部分时,我将进一步说明一点。假设空间 HU 的一个子集,一个解释任何给定现象的宇宙。我们如何知道正确的假设是否真的存在于 H 内部而不是 U 的其他地方?贝叶斯定理显示了 H┌ H 的相对概率以及它们的相对先验概率之间的关系。然而,我们没有真正的方法可以知道 P┌ H 的价值,因为没有办法计算一个尚未构思的假设的概率。此外,这个假设的内容包括一个目前未知的可能物体的宇宙。这种悖论出现在任何使用比较假设检验的描述中,我们对照 H 中的其他假设来评估我们当前的假设。另一种方法是找到评估 H 的方法。我们可以看到,随着我们扩展 H ,其中假设的可计算性变得更加困难。要评价 H ,我们需要把我们的宇宙限制在已知的宇宙。对于一个人来说,这是一种已经印在我们大脑和神经系统中的体验生活;对于一台机器来说,它就是内存库和算法。评估这个全球假设空间的能力是人工智能的关键挑战之一。

树木模型

树模型在机器学习中无处不在。它们自然适合分治迭代算法。决策树模型的主要优势之一是它们自然易于可视化和概念化。它们允许检查,而不仅仅是给出答案。例如,如果我们必须预测一个类别,我们也可以揭示产生特定结果的逻辑步骤。此外,树模型通常比其他模型需要更少的数据准备,并且可以处理数字和分类数据。从负面来看,树模型可能会创建过于复杂的模型,无法很好地推广到新数据。树模型的另一个潜在问题是,它们会对输入数据的变化变得非常敏感,正如我们稍后将看到的,使用它们作为集成学习器可以缓解这个问题。

决策树和上一节中使用的假设映射之间的一个重要区别是,树模型不在具有两个以上值的特征上使用内部析取,而是在每个值上使用分支。我们可以通过下图中的尺寸特征看到这一点:

Tree models

需要注意的另一点是,决策树比合取假设更具表达性,我们可以在这里看到这一点,我们已经能够分离合取假设覆盖负例子的数据。当然,这种表现力是有代价的:过度依赖训练数据的倾向。一种强制推广和减少过度拟合的方法是引入一种对不太复杂的假设的归纳偏差。

我们可以很容易地使用 Sklearn DecisionTreeClassifier实现我们的小例子,并创建结果树的图像:

from sklearn import tree

names=['size','scale','fruit','butt']
labels=[1,1,1,1,1,0,0,0]

p1=[2,1,0,1]
p2=[1,1,0,1]
p3=[1,1,0,0]
p4=[1,1,0,0]
n1=[0,0,0,0]
n2=[1,0,0,0]
n3=[0,0,1,0]
n4=[1,1,0,0]
data=[p1,p2,p3,p4,n1,n2,n3,n4]

def pred(test, data=data):
 dtre=tree.DecisionTreeClassifier()
 dtre=dtre.fit(data,labels)
 print(dtre.predict([test]))
 with open('data/treeDemo.dot', 'w') as f:
 f=tree.export_graphviz(dtre,out_file=f,
 feature_names=names)
pred([1,1,0,1])

运行前面的代码会创建一个treeDemo.dot文件。保存为.dot文件的决策树分类器可以使用 Graphiz 图形可视化软件转换为图像文件,如.png.jpeg.gif。您可以从http://graphviz.org/Download.php下载 Graphviz。安装完成后,使用它将.dot文件转换为您选择的图像文件格式。

这可以让您清楚地了解决策树是如何拆分的。

Tree models

我们可以从完整的树中看到,我们在每个节点上递归拆分,每次拆分都增加了同一类样本的比例。我们继续向下移动节点,直到到达一个叶节点,在那里我们的目标是拥有一组同类实例。纯度的概念很重要,因为它决定了每个节点是如何被分割的,并且它位于上图中基尼系数的后面。

纯度

我们如何理解每个特征对于能够将样本分成包含最少或不包含来自其他类的样本的类的有用性?赋予一个类标签的特征的指示集是什么?为了回答这个问题,我们需要考虑分裂的纯粹性。例如,假设我们有一组布尔实例,其中 D 被分成 D1D2 。如果我们进一步把自己限定在仅仅两个类, D posD neg 的话,我们可以看到最佳的情况是 D 完美地拆分为正反例。这有两种可能:要么是D1pos= DposD1 neg = {} ,要么是D1neg= DnegD1 pos = {}

如果这是真的,那么分裂的孩子就被说成是纯洁的。我们可以通过nposT3】和nnegT7】的相对大小来测量分裂的杂质。这是正类的经验概率,可以用p = npos/(npos+nneg)的比例来定义。杂质函数有几个要求。首先,如果我们切换正负类(即把 p 换成 1-p ,那么杂质应该不会改变。此外,当 p=0p=1 时,该函数应为零,当 p=0.5 时,该函数应达到最大值。为了以有意义的方式分割每个节点,我们需要一个具有这些特征的优化函数。**

有三个函数通常用于杂质测量或分离标准,具有以下特征。

  • 少数类:这只是对误分类例子比例的一种度量,假设我们用多数类来标记每片叶子。这个比例越高,误差越大,分裂的杂质越多。这有时被称为分类错误,被计算为 min(p,1-p)
  • 基尼指数:如果我们把例子标为正,概率 p ,或者负,概率 1-p ,这就是预期误差。有时,基尼指数的平方根也被使用,当处理大部分样本属于一类的高度偏斜数据时,这可能有一些优势。
  • :这个杂质的度量是基于分裂的预期信息含量。考虑一条消息,告诉你一系列随机抽取样本的类别。样本集越纯,这个消息就变得越可预测,因此期望的信息就越小。熵由以下公式衡量:

Purity

这三个分裂标准的概率范围在 01 之间,绘制在下图中。熵标准由 0.5 缩放,以使它们能够与其他两个进行比较。我们可以使用决策树的输出来查看曲线上每个节点的位置。

Purity

规则模型

我们可以利用离散数学的原理来最好地理解规则模型。让我们回顾一下这些原则。

X 为一组特征,特征空间, C 为一组类。我们可以将 X 的理想分类器定义如下:

c: X → C

类别为 c 的特征空间中的一组示例定义如下:

d = {(x【1】、c(x【1】),-我...。,(x n ,c(xn)√x×c

X 的分裂是将 X 分割成一组互斥的子集 X 1 ....X s ,所以我们可以这样说:

X = X1∧..∪ Xs

这会导致 D 分裂成 D 1 ,...D s 。我们定义 Dj ,其中 j = 1,...,s 和 is {(x,c(x) ∈ D | x ∈ Xj)}

这只是在 X 中定义了一个子集,称为 Xj ,其中 Xj 的所有成员都被完美地分类了。

在下表中,我们使用指示函数的总和定义了一些度量。指示器功能使用符号 I[...】如果方括号之间的语句为真,则等于 1,如果为假,则等于 0。这里 τc(x) 是对 c(x) 的估计。

让我们看看下表:

| **阳性数** | ![Rule models](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/design-ml-sys-py/img/B05198_04_11.jpg) | | **底片数** | ![Rule models](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/design-ml-sys-py/img/B05198_04_12.jpg) | | **真阳性** | ![Rule models](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/design-ml-sys-py/img/B05198_04_13.jpg) | | **真底片** | ![Rule models](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/design-ml-sys-py/img/B05198_04_14.jpg) | | **假阳性** | ![Rule models](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/design-ml-sys-py/img/B05198_04_15.jpg) | | **假阴性** | ![Rule models](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/design-ml-sys-py/img/B05198_04_16.jpg) | | **精度** | ![Rule models](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/design-ml-sys-py/img/B05198_04_17.jpg) | | **错误率** | ![Rule models](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/design-ml-sys-py/img/B05198_04_18.jpg) | | **真阳性率(灵敏度、召回)** | ![Rule models](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/design-ml-sys-py/img/B05198_04_19.jpg) | | **真阴性率(阴性回忆)** | ![Rule models](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/design-ml-sys-py/img/B05198_04_20.jpg) | | **精度、信心** | ![Rule models](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/design-ml-sys-py/img/B05198_04_21.jpg) |

规则模型不仅包括规则集或规则列表,而且重要的是,还包括如何组合这些规则以形成预测的规范。它们是一个逻辑模型,但与树方法的不同之处在于,树分裂成互斥的分支,而规则可以重叠,可能携带额外的信息。在监督学习中,规则模型本质上有两种方法。一种是找到文字的组合,就像我们之前做的那样,形成一个假设,覆盖一组足够同质的样本,然后找到一个标签。或者,我们可以反其道而行之;也就是说,我们可以首先选择一个类,然后找到覆盖该类足够大的样本子集的规则。第一种方法倾向于导致规则的有序列表,而在第二种方法中,规则是无序的集合。正如我们将看到的,每一个都以自己特有的方式处理重叠的规则。让我们先看看有序列表方法。

有序列表法

当我们向连接规则添加文字时,我们的目标是增加规则覆盖的每个后续实例集的同质性。这类似于在假设空间中构建一条路径,就像我们在上一节中为逻辑树所做的那样。规则方法的一个关键区别是,我们只对其中一个孩子的纯度感兴趣,即添加的文字为真的孩子。对于基于树的模型,我们使用两个子代的加权平均值来寻找二元分裂的两个分支的纯度。在这里,我们仍然对计算后续规则的纯度感兴趣;然而,我们只遵循每个分裂的一面。我们仍然可以使用相同的方法来寻找纯度,但是我们不再需要对所有的孩子求平均值。与决策树的分治策略相反,基于规则的学习通常被描述为分离和征服。

让我们简单考虑一个例子,使用我们上一节中的针叶树分类问题。

The ordered list approach

有几个选项可以选择最纯粹的拆分规则。假设我们选择了规则如果 scaled = N,那么 class 是负的,我们已经覆盖了四个负样本中的三个。在下一次迭代中,我们将这些样本从考虑中移除,并继续搜索具有最大纯度的文本的过程。实际上,我们正在做的是建立一个有序的规则列表,加入ifelse子句。我们可以将我们的规则重写为互斥的,这意味着不需要对规则集进行排序。这里的折衷是,我们必须使用否定文字或内部析取来处理具有两个以上值的特征。

我们可以对这个模型进行某些改进。例如,我们可以引入一个停止准则,如果满足某些条件,就停止迭代,例如在有噪声数据的情况下,当每个类中的样本数量低于某个数量时,我们可能希望停止迭代。

有序规则模型与决策树有很多共同之处,特别是,它们使用基于纯度概念的目标函数,纯度是每个拆分中正负类实例的相对数量。它们具有易于可视化的结构,并用于许多不同的机器学习环境中。

基于集合的规则模型

使用基于集合的规则模型,规则一次学习一个类,我们的目标函数只是最大化 p ,而不是最小化 min( p,1-p )。使用此方法的算法通常迭代每个类,并且只覆盖在找到规则后移除的每个类的样本。基于集合的模型使用精度(参见表 4-1)作为搜索启发式,这可能会使模型过于关注规则的纯度;它可能会错过近乎纯粹的规则,这些规则可以进一步专门化以形成纯粹的规则。另一种称为 波束搜索的方法使用启发式方法来排序预定数量的最佳部分解。

有序列表为我们提供了训练集的凸覆盖。对于基于未编码集的方法来说,情况未必如此,因为对于给定的规则集,没有全局最优顺序。因此,我们可以访问表示为连词 A∧B 的规则重叠,其中 AB 是两个规则集。如果这两个规则在一个有序列表中,我们有,如果顺序是 ABA = (A∧B) ∨ (A∧┌B) 或者,如果顺序是 BAB = (A∧B) ∨ (┌A∧B) 。这意味着规则空间可能会扩大;然而,因为我们必须估计重叠的覆盖范围,我们牺牲了凸性。

一般来说,规则模型非常适合预测模型。正如我们将在后面看到的,我们可以扩展我们的规则模型来执行诸如聚类和回归之类的任务。规则模型的另一个重要应用是构建描述性模型。当我们构建分类模型时,我们通常会寻找创建训练样本的纯子集的规则。然而,如果我们在寻找特定样本集的其他显著特征,这不一定是真的。这有时被称为 子群发现。在这里,我们对基于类纯度的启发式方法不感兴趣,而是对寻找区别类分布的启发式方法感兴趣。这是使用基于局部异常测试思想的定义的质量函数来完成的。该功能可以采用 q=TP/(FP +g) 的形式。这里 g 是一个泛化因子,它决定了相对于规则覆盖的实例数量,非目标类实例的允许数量。对于较小的 g 值,比如小于 1 ,将会生成更具体的规则,因为每增加一个非目标示例都会产生更大的相对费用g 的较高值,比如说大于 10 ,创建了覆盖更多非目标样本的更一般的规则。 g 没有理论最大值;然而,超过样本数量没有多大意义。 g 的值由数据的大小和阳性样本的比例决定。 g 的值可以变化,从而引导子组发现到 TPFP 空间中的某些点。

我们可以使用主观或客观质量函数。我们可以将主观的兴趣度系数结合到模型中,以反映诸如可理解性、不可预测性或基于描述感兴趣类别的模板的关系模式。客观测量是从数据本身的统计和结构属性中得出的。它们非常适合使用覆盖图来突出具有不同于总体的统计特征的子组。

最后,在这一节基于规则的模型中,我们将考虑可以完全无监督学习的规则。这叫做关联规则学习,它的典型用例包括数据挖掘、推荐系统和自然语言处理。我们以一家卖四样东西的五金店为例:锤子钉子螺丝油漆

让我们看看下表:

|

交易

|

项目

|
| --- | --- |
| 1 | 指甲 |
| 2 | 锤子和钉子 |
| 3 | 锤子、钉子、油漆和螺钉 |
| 4 | 锤子、钉子和油漆 |
| 5 | 螺丝 |
| 6 | 油漆和螺钉 |
| 7 | 螺钉和钉子 |
| 8 | 颜料 |

在此表中,我们将交易与项目分组。我们也可以将每一个项目与它所涉及的交易进行分组。比如交易 12347 涉及钉子,交易 234 涉及锤子等等。我们也可以用成套物品来做,比如锤子钉子都参与了交易 234 。我们可以这样写,因为项目集{hammer,nails}覆盖了交易集[2,3,4]。有 16 个项目集,包括涵盖所有交易的空集。

事务集之间的关系形成了将项目与其各自的集连接起来的网格结构。为了建立关联规则,我们需要创建超过阈值FTT9】的频繁项集。例如F<sub>T</sub> = 3{screws}{hammer,nails}{paint}的频繁项集。这些只是与三个或更多事务相关联的项目集。下面是一个图表,显示了我们示例中的部分网格。以类似的方式,我们在我们的假设空间映射中发现了最不一般的一般化。这里,我们对最大项目集的最低边界感兴趣。在这个例子中,是{nails,hammer}

Set-based rule models

我们现在可以创建表单 if A 然后 B 的关联规则,其中 AB 是在事务中经常一起出现的项目集。如果我们在这个图上选择一条边,比如频率为5{nails}和频率为3{nails, hammer}之间的边,那么我们可以说关联规则置信度如果是钉子那么锤就是3/5。使用频率阈值和规则的置信度,算法可以找到所有超过该阈值的规则。这被称为 关联规则挖掘,它通常包括后处理阶段,在此阶段,不必要的规则被过滤掉,例如——更具体的规则没有比更一般的父规则更高的置信度。

总结

我们从探索逻辑语言和为一个简单的例子创建假设空间映射开始这一章。我们讨论了最小一般概括的思想,以及如何在这个空间中找到一条从最一般到最小一般假设的路径。我们简单看了一下可学性的概念。接下来,我们研究了树模型,发现它们可以应用于广泛的任务,并且既有描述性又易于解释。然而,树本身容易过度拟合,大多数树模型采用的贪婪算法容易对初始条件过度敏感。最后,我们讨论了有序规则列表和基于无序规则集的模型。这两种不同的规则模型通过它们如何处理规则重叠来区分。有序的方法是找到文字的组合,将样本分成更均匀的组。无序方法一次搜索一类假设。

在下一章中,我们将研究一种完全不同的模型——线性模型。这些模型使用几何数学来描述问题空间,正如我们将看到的,形成了支持向量机和神经网络的基础。**

五、线性模型

线性模型是最广泛使用的模型之一,并形成了许多先进的非线性技术的基础,如支持向量机和神经网络。它们可以应用于任何预测任务,如分类、回归或概率估计。

当响应输入数据的微小变化时,并且假设我们的数据由完全不相关的特征组成,线性模型往往比树模型更稳定。正如我们在上一章中提到的,树模型会对训练数据中的微小变化做出过度反应。这是因为树根处的劈叉会产生无法恢复的后果,也就是说,会产生不同的分支,并可能使树的其他部分明显不同。另一方面,线性模型相对稳定,对初始条件不太敏感。然而,正如您所料,这将产生相反的效果,将不太敏感的数据更改为细微差别的数据。这用术语 方差(对于过拟合模型)和偏差(对于欠拟合模型)来描述。线性模型通常是低方差和高偏差的。

线性模型通常最好从几何角度来处理。我们知道,我们可以很容易地在笛卡尔坐标系中绘制二维空间,我们可以用透视的错觉来说明第三个。我们也被教导将时间视为第四维度,但是当我们开始谈论 n 维度时,一个物理类比就失效了。有趣的是,我们仍然可以使用许多我们直观地应用于三维空间的数学工具。虽然这些额外的维度变得很难可视化,但我们仍然可以使用相同的几何概念来描述它们,例如线、平面、角度和距离。对于几何模型,我们将每个实例描述为具有一组实值特征,每个特征都是几何空间中的一个维度。让我们从回顾与线性模型相关的形式主义开始这一章。

我们已经不再使用二元最小二乘法的基本数值线性模型解。在 2D 坐标系统上可视化很简单。当我们试图添加参数时,当我们向模型添加特征时,我们需要一种形式主义来替换或增强直观的视觉表示。在本章中,我们将讨论以下主题:

  • 最小二乘法
  • 正规方程法
  • 逻辑回归
  • 正规化

让我们从基本模型开始。

引入最小二乘法

在一个简单的单特征模型中,我们的假设函数如下:

Introducing least squares

如果我们将此绘制成图,我们可以看到它是一条直线,在w0T3 处穿过 y 轴,斜率为 w 1 。线性模型的目的是找到参数值,这些参数值将创建一条与数据最匹配的直线。我们称这些函数为参数值。我们定义了一个目标函数 J w ,我们希望将其最小化:

Introducing least squares

这里, m 为训练样本数, h w (x (i) )i th 训练样本的估计值, y i 为其实际值。这是 h、成本函数,因为它衡量的是误差的成本;误差越大,成本越高。这种推导成本函数的方法有时被称为平方误差的和,因为它总结了预测值和实际值之间的差异。正如我们将看到的,这笔钱被减半作为一种便利。实际上有两种方法可以解决这个问题。我们可以使用迭代梯度下降算法,或者使用正规方程一步最小化成本函数。我们先看梯度下降。

梯度下降

当我们对照成本函数绘制参数值时,我们得到一个碗形凸函数。随着参数值在两个方向上偏离它们的最优值(从一个最小值开始),我们模型的成本增加。由于假设函数是线性的,所以成本函数是凸的。如果不是这样,那么就无法区分全局局部最小值

梯度下降算法由以下更新规则表示:

Gradient descent

其中 δJwT5 的一阶导数,因为它使用导数的符号来确定该走哪条路。这只是各点切线斜率的符号。算法取一个超参数 α ,这是我们需要设置的学习速率。它被称为 超参数,以区别于我们模型估计的 w 参数。如果我们把学习率设置得太小,找到最小值需要更长的时间;如果设置得太高,就会超调。我们可能会发现,我们需要多次运行模型来确定最佳学习速率。

Gradient descent

当我们将梯度下降应用于线性回归时,可以导出以下公式,它们是我们的模型的参数。我们可以重写导数项,使它更容易计算。派生本身相当复杂,没有必要在这里一一解释。如果你懂微积分,你会发现下面的规则是等价的。这里,我们使用一个停止函数,对假设重复应用两个更新规则。这通常是当后续迭代中参数之间的差异下降到阈值以下时,即 t

初始化w0T3w1T7】并重复:**

Gradient descent

重要的是这些更新规则是同时应用的,也就是说,它们都是在同一个迭代中应用的,所以w0T3】和w1T7】的新值在下一个迭代中被插回。这有时被称为批梯度下降,因为它在一个中更新了所有的训练样本。**

将这些更新规则应用于具有多个特征的线性回归问题是相当简单的。如果我们不担心精确的推导,这是真的。

对于多个特征,我们的假设函数如下所示:

Gradient descent

在这里,添加了 x 0 = 1 ,通常称为我们的偏差特征,以帮助我们进行以下计算。我们看到可以看到,通过使用向量,我们也可以把这个简单地写成参数值的转置乘以特征值向量, x 。对于多个特征梯度下降,我们的成本函数将应用于参数值的向量,而不仅仅是单个参数。这是新的成本函数。

Gradient descent

J(w) 简单来说就是 J(w 0 ,w1……,w n ) ,其中 n 是特征的个数。 J 是参数向量的函数, w 。现在,我们的梯度下降更新规则如下:

Gradient descent

注意我们现在有多个特征。因此,我们用下标 jx 值来表示 j th 特征。我们可以把这个拆开来看,它真的代表了 j + 1 的嵌套更新规则。除了下标之外,每一个都与我们用于单个特征的训练规则相同。

这里要提到的一个要点是,为了让我们的模型更有效地工作,我们可以定义自己的特征,这也是我们将在后面的章节中重新讨论的一个要点。对于一个简单的情况,我们的假设是根据宽度和深度两个特征来估算一块土地的价格,显然,我们可以将这两个特征相乘,得到一个特征,即面积。因此,根据您对某个问题的特定见解,使用派生功能可能更有意义。我们可以更进一步,创建我们自己的特征,使我们的模型适合非线性数据。一种方法是 多项式回归。这包括向我们的假设函数添加幂项,使其成为多项式。这里有一个例子:

Gradient descent

在我们的土地价格示例中,一种应用方法是简单地添加我们的区域特征的正方形和立方体。这些项有许多可能的选择,事实上,在我们的住房例子中,更好的选择可能是取其中一项的平方根,以阻止函数爆炸到无穷大。这就凸显了一个重要的点,那就是在使用多项式回归时,我们必须非常小心特征缩放。我们可以看到,随着 x 变大,函数中的项变得越来越大。

我们现在有一个模型来拟合非线性数据,然而,在这个阶段,我们只是手动尝试不同的多项式。理想情况下,我们需要能够在某种程度上在我们的模型中包含特征选择,而不是让人工尝试找出合适的功能。我们还需要意识到相关特征可能会使我们的模型不稳定,因此我们需要设计将相关特征分解成其组件的方法。我们在第 7 章特征–算法如何看世界中研究这些方面。

下面是批量梯度下降的简单实现。尝试使用不同的学习速率α值运行它,并在具有较大偏差和/或方差的数据上运行,以及在改变迭代次数后运行,看看这对我们模型的性能有什么影响:

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

def gradientDescent(x, y, alpha, numIterations):
 xTrans = x.transpose()
 m, n = np.shape(x)
 theta = np.ones(n)
 for i in range(0, numIterations):
 hwx = np.dot(x, theta)
 loss = hwx - y
 cost = np.sum(loss ** 2) / (2 * m)
 print("Iteration %d | Cost: %f " % (i, cost))
 gradient = np.dot(xTrans, loss) / m
 theta = theta - alpha * gradient
 return theta

def genData(numPoints, bias, variance):
 x = np.zeros(shape=(numPoints, 2))
 y = np.zeros(shape=numPoints)
 for i in range(0, numPoints):
 x[i][0] = 1
 x[i][1] = i
 y[i] = (i + bias) + random.uniform(0, 1) * variance
 return x, y

def plotData(x,y,theta):
 plt.scatter(x[...,1],y)
 plt.plot(x[...,1],[theta[0] + theta[1]*xi for xi in x[...,1]])

x, y = genData(20, 25, 10)
iterations= 10000
alpha = 0.001
theta=gradientDescent(x,y,alpha,iterations)
plotData(x,y,theta)

代码的输出如下图所示:

Gradient descent

这被称为 批量梯度下降,因为在每次迭代中,它一次基于所有训练样本更新参数值。在随机梯度下降的情况下,另一方面,梯度一次由单个例子的梯度近似。在算法收敛之前,可以对数据进行多次。每次通过时,数据都会被打乱,以防止陷入循环。随机梯度下降已成功应用于自然语言处理等大规模学习问题。缺点之一是它需要大量的超参数,尽管这确实提供了调整的机会,例如选择损失函数或应用的正则化类型。随机梯度下降也对特征缩放敏感。这方面的许多实现,例如Sklearn包中的 SGDClassifierSGD gressor,默认情况下将使用自适应学习速率。随着算法越来越接近最小值,这将降低学习速率。为了使这些算法运行良好,通常需要缩放数据,以便输入向量 X 中的每个值在 0 和 1 之间或-1 和 1 之间缩放。或者,确保数据值的平均值为 0,方差为 1。使用来自sklearn.preprocessingStandardScaler类最容易做到这一点。

梯度下降并不是唯一的算法,在很多方面也不是最有效的最小化代价函数的方法。有许多高级库可以比我们手动实现梯度下降更新规则更有效地计算参数值。幸运的是,我们不必太担心细节,因为已经有许多用 Python 编写的复杂而高效的回归算法。以为例,在sklearn.linear_model模块中,有套索弹性线算法,根据您的应用,这些算法可能表现更好。

正规方程

现在让我们从稍微不同的角度来看线性回归问题。如前所述,有一个数值解;因此,我们可以使用所谓的 正规方程一步求解,而不是像梯度下降那样迭代训练集。如果你知道一些微积分,你会记得我们可以通过取一个函数的导数,然后将导数设置为零来求解一个变量,从而最小化这个函数。这是有意义的,因为如果我们考虑凸成本函数,最小值将是切线斜率为零的地方。因此,在具有一个特征的简单情况下,我们区分 J(w)w ,并将其设置为零,然后求解 w 。我们感兴趣的问题是当 w 是一个 n +1 参数向量时,成本函数 J(w) 是这个向量的函数。一种最小化的方法是依次对参数值取 J(w) 的偏导数,然后将这些导数设置为零,对 w 的每个值求解。这给了我们最小化成本函数所需的 w 的值。

事实证明,一个简单的解决方法,可能是一个漫长而复杂的计算,就是所谓的正规方程。为了了解这是如何工作的,我们首先定义一个特征矩阵,如下所示:

The normal equation

这通过 n + 1 矩阵创建了一个 m ,其中 m 是训练示例的数量, n 是特征的数量。请注意,在我们的符号中,我们现在将训练标签向量定义如下:

The normal equation

现在,我们可以通过以下等式计算参数值以最小化成本函数:

The normal equation

这是正常方程。当然,有很多方法可以在 Python 中实现这一点。这里有一个使用 NumPy matrix类的简单方法。大多数实现都有一个正则化参数,除其他外,该参数可以防止试图转置奇异矩阵时出现错误。当我们拥有比训练数据更多的特征时,也就是当 n 大于 m 时,就会出现这种情况;没有正则化的正规方程是行不通的。这是因为矩阵 X T X 是不可换位的,所以,我们的术语(XTX)-1是没有办法计算的。正则化还有其他好处,我们将很快看到:

import numpy as np

def normDemo(la=.9):
 X = np.matrix('1 2 5 ; 1 4 6')
 y=np.matrix('8; 16')
 xtrans=X.T
 idx=np.matrix(np.identity(X.shape[1]))
 xti = (xtrans.dot(X)+la * idx).I
 xtidt = xti.dot(xtrans)
 return(xtidt.dot(y))

使用法向方程的一个优点是您不需要担心特征缩放。具有不同范围的要素(例如,如果一个要素的值介于 1 和 10 之间,而另一个要素的值介于 0 和 1000 之间)可能会导致梯度下降的问题。使用正规方程,你不需要担心这个。正规方程的另一个优点是不需要选择学习速率。我们看到,梯度下降;错误选择的学习速率可能会使模型变得不必要的慢,或者,如果学习速率太大,可能会导致模型超过最小值。这可能需要在我们的梯度下降测试阶段增加一个步骤。

正规方程有其特殊的缺点;最重要的是,当我们有大量特征的数据时,它不能很好地扩展。我们需要计算特征矩阵转置的逆, X 。这种计算通过 n 矩阵产生一个 n 。记住 n 是特征的数量。这实际上意味着,在大多数平台上,矩阵求逆所需的时间大约增长为 n 的立方。因此,对于具有大量特征的数据,比如大于 10,000 的数据,您可能应该考虑使用梯度下降而不是正规方程。使用正规方程时出现的另一个问题是,当我们拥有比训练数据更多的特征时,也就是当 n 大于 m 时,没有正则化的正规方程就不起作用了。这是因为矩阵 X T X 是不可转座的,所以没有办法计算我们的术语(XTX)-1

逻辑回归

用我们的最小平方模型,我们已经应用它来解决最小化问题。我们也可以用这种思想的变体来解决分类问题。考虑当我们将线性回归应用于分类问题时会发生什么。让我们以具有一个特征的二元分类的简单情况为例。我们可以对照 y 轴上的类别标签,在 x 轴上绘制我们的特征。我们的特征变量是连续的,但是我们在 y 轴上的目标变量是离散的。对于二元分类,我们通常表示一个 0 为负类,一个 1 为正类。我们通过数据构建一条回归线,并在 y 轴上使用一个阈值来估计决策边界。这里我们使用 0.5 的阈值。

Logistic regression

在左边的图中,方差很小,并且我们的正负情况很好地分开,我们得到了一个可接受的结果。该算法正确地对训练集进行分类。在右侧的图像中,我们在数据中有一个孤立点。这使得我们的回归线变平,并且将我们的截止线向右移动。显然属于类别 1 的异常值应该不会对模型的预测产生任何影响,但是,现在在相同的截止点下,预测将类别 1 的第一个实例误分类为类别 0

我们处理这个问题的一种方法是制定一个不同的假设表示。对于逻辑回归,我们将使用线性函数作为另一个函数 g 的输入。

Logistic regression

术语 g 被称为 sigmoid逻辑函数。你会从它的图中注意到,在y轴上,它在 0 和 1 处有渐近线,并且它在 0.5 处与该轴相交。

Logistic regression

现在,如果我们将 z 替换为 W T x ,我们可以这样重写我们的假设函数:

Logistic regression

与线性回归一样,我们需要将参数 w、拟合到我们的训练数据中,以给出一个可以进行预测的函数。在我们尝试拟合模型之前,让我们看看如何解释假设函数的输出。由于这将返回一个介于 0 和 1 之间的数字,最自然的解释方式是因为它是正类的概率。既然我们知道,或者假设,每个样本只能属于两类中的一类,那么正类的概率加上负类的概率就一定等于一。因此,如果我们能估计正类,那么我们就能估计负类的概率。因为我们最终试图预测特定样本的类别,所以如果假设函数的输出返回大于或等于 0.5 的值,我们可以将其解释为正,否则为负。现在,给定 sigmoid 函数的特征,我们可以写出以下内容:

Logistic regression

每当我们的假设函数在特定的训练样本上返回一个大于或等于零的数字时,我们就可以预测一个正类。让我们看一个简单的例子。我们还没有将我们的参数拟合到这个模型中,我们将很快这样做,但是为了这个例子,让我们假设我们有一个如下的参数向量:

Logistic regression

因此,我们的假设函数是这样的:

Logistic regression

如果满足以下条件,我们可以预测 y = 1 :

Logistic regression

相当于:

Logistic regression

这可以用下面的图表来描述:

Logistic regression

这只是 x=3y=3 之间的一条直线,代表决策边界。它创建了两个区域,我们可以在其中预测 y = 0y = 1 。当判定边界不是直线时会发生什么?就像我们在线性回归中给假设函数添加多项式一样,我们也可以用逻辑回归来做这件事。让我们编写一个带有一些高阶项的新假设函数,看看如何将其与数据进行拟合:

Logistic regression

这里我们在函数中增加了两个平方项。我们将很快看到如何拟合参数,但是现在,让我们将参数向量设置为以下值:

Logistic regression

所以,我们现在可以写出以下内容:

Logistic regression

或者,我们可以这样写:

Logistic regression

你可能认识到,这是以原点为中心的圆的方程,我们可以用它作为我们的决策边界。我们可以通过添加高阶多项式项来创建更复杂的决策边界。

逻辑回归的成本函数

现在,我们需要看看将参数拟合到数据的重要任务。如果我们更简单地重写我们用于线性回归的成本函数,我们可以看到成本是平方误差的一半:

The Cost function for logistic regression

对此的解释是,在给定某个预测,即 h w (x) 和训练标签 y 的情况下,它只是简单地计算我们希望模型产生的成本。

这在一定程度上适用于逻辑回归,但是,有一个问题。对于逻辑回归,我们的假设函数依赖于非线性 sigmoid 函数,当我们将它与我们的参数进行比较时,它通常会产生一个非凸函数。这意味着,当我们试图将梯度下降等算法应用于代价函数时,它不一定会收敛到全局最小值。一种解决方案是定义一个凸的成本函数,结果证明以下两个函数适合我们的目的,每个类一个:

The Cost function for logistic regression

这为我们提供了以下图表:

The Cost function for logistic regression

直觉上,我们可以看到这是我们需要它做的事情。如果我们考虑正类中的单个训练样本,那就是 y = 1 ,如果我们的假设函数 h w (x) 正确预测 1 ,那么成本,正如你所料,就是 0 。如果假设函数的输出为 0 ,则不正确,因此成本趋近于无穷大。当 y 在负类时,我们的成本函数就是右边的图。这里 h w (x)0 时成本为零,当 h w (x)1 时成本上升到无穷大。我们可以用更简洁的方式写这个,记住 y 不是 0 就是 1 :

The Cost function for logistic regression

我们可以看到,对于每种可能性, y=1y=0 ,无关项乘以 0 ,为每种特定情况留下正确的项。所以,现在我们可以把成本函数写成如下:

The Cost function for logistic regression

那么,如果给我们一个新的、未标记的值 x ,我们如何进行预测呢?与线性回归一样,我们的目标是最小化成本函数, J(w) 。我们可以使用与用于线性回归相同的更新规则,即使用偏导数来寻找斜率,当我们重写导数时,我们得到以下结果:

The Cost function for logistic regression

多类分类

到目前为止,我们只看了二元分类。对于多类分类,我们假设每个实例只属于一个类。一个稍微不同的分类问题是每个样本可以属于多个目标类。这叫做多标签分类。我们可以对每一类问题采用类似的策略。

有两种基本方法:

  • 一个对所有
  • 一对多

在“一个对所有”的方法中,单个多类问题被转化为多个二进制分类问题。这被称为 一对全技术,因为我们依次取每个类,并为该特定类拟合一个假设函数,给其他类分配一个负类。我们最终得到不同的分类器,每个分类器都被训练来识别其中的一个类。我们通过运行所有的分类器并挑选预测具有最高概率的类的分类器,在给定新输入的情况下进行预测。为了使它形式化,我们编写了以下内容:

Multiclass classification

为了做出预测,我们选择最大化以下内容的类:

Multiclass classification

用另一种方法称为一对一方法,为每对类构造一个分类器。当模型做出预测时,得票最多的班级获胜。这种方法通常比一对多方法慢,尤其是当有大量类时。

所有 Sklearn 分类器都实现了多类分类。我们在第 2 章工具和技术中看到了这一点,以 K 近邻为例,我们试图使用虹膜数据集预测三个类别中的一个。Sklearn 使用OneVsRestClassifier类实现一对所有算法,使用OneVsOneClassifier实现一对一算法。这些被称为元估计器,因为它们将另一个估计器作为输入。它们具有的优势,能够允许改变处理两个以上类的方式,这可以导致更好的性能,无论是在计算效率方面,还是在泛化误差方面。

在下面的示例中,我们使用了 SVC:

from sklearn import datasets
from sklearn.multiclass import OneVsRestClassifier, OneVsOneClassifier
from sklearn.svm import LinearSVC

X,y = datasets.make_classification(n_samples=10000, n_features=5)
X1,y1 = datasets.make_classification(n_samples=10000, n_features=5)
clsAll=OneVsRestClassifier(LinearSVC(random_state=0)).fit(X, y)
clsOne=OneVsOneClassifier(LinearSVC(random_state=0)).fit(X1, y1)
print("One vs all cost= %f" % clsAll.score(X,y))
print("One vs one cost= %f" % clsOne.score(X1,y1))

我们将观察以下输出:

Multiclass classification

正规化

我们前面提到如果特征是相关的,线性回归会变得不稳定,也就是说,对训练数据的微小变化高度敏感。考虑两个特征完全负相关的极端情况,这样一个特征的任何增加都会伴随着另一个特征的同等减少。当我们将我们的线性回归算法应用于这两个特征时,它将产生一个常数函数,所以这并没有真正告诉我们任何关于数据的信息。或者,如果特征是正相关的,它们的微小变化将被放大。正规化有助于缓和这种情况。

我们之前看到,我们可以通过添加多项式项来使我们的假设更接近于训练数据。随着我们添加这些项,函数的形状变得更加复杂,这通常会导致假设过度拟合训练数据,并且在测试数据上表现不佳。当我们添加特征时,无论是直接从数据还是我们自己导出的特征,模型更有可能过度填充数据。一种方法是丢弃我们认为不太重要的特征。然而,我们无法事先确定哪些特征可能包含相关信息。更好的方法是不丢弃特征,而是缩小它们。由于我们不知道每个特征包含多少信息,正则化减少了所有参数的大小。

我们可以简单地将该术语添加到成本函数中。

Regularization

超参数λ,控制着两个目标之间的权衡——拟合训练数据的需要,以及保持参数小以避免过度拟合的需要。我们不将正则化参数应用于我们的偏差特征,因此我们分离第一个特征的更新规则,并向所有后续特征添加正则化参数。我们可以这样写:

Regularization

在这里,我们已经添加了正则化项, λ w j /m 。为了更清楚地了解这是如何工作的,我们可以将所有依赖于 wj 的术语分组,我们的更新规则可以改写如下:

Regularization

正则化参数 λ 通常是一个大于零的小数值。为了使其具有期望的效果,将其设置为使得 α λ /m 是比 1 稍小的数字。这将在更新的每次迭代中缩小 w j

现在,让我们看看如何将正则化应用于正规方程。等式如下:

Regularization

这有时被称为闭合形式解。我们加上身份矩阵, I ,乘以正则化参数。单位矩阵是由主对角线上的 1 和其他地方的 0 组成的 (n+1) 矩阵乘以 (n+1)

在一些实现中,我们还可以使矩阵零的第一个条目(左上角)反映我们没有将正则化参数应用于第一个偏差特征的事实。然而,在实践中,这很少会对我们的模型产生很大影响。

当我们将其与单位矩阵相乘时,我们得到一个主对角线包含 λ 值的矩阵,所有其他位置为零。这确保了,即使我们有比训练样本更多的特征,我们仍然能够反转矩阵 X T X 。如果我们有相关变量,这也使我们的模型更加稳定。这种回归形式有时被称为岭回归,我们在第二章工具与技术中看到了这种实现。岭回归的一个有趣的替代方法是 套索回归。它将岭回归正则化项 ∑iwi 2 替换为 ∑i | wi | 。也就是说,它不是使用权重的平方和,而是使用权重平均值的和。结果是一些权重被设置为 0 ,而其他权重被缩小。套索回归往往对正则化参数相当敏感。与岭回归不同,套索回归没有封闭形式的解,因此需要采用其他形式的数值优化。岭回归有时被称为使用 L2 范数、和套索正则化、 L1 范数

最后,我们将研究如何将正则化应用于逻辑回归。与线性回归一样,如果我们的假设函数包含高阶项或许多特征,逻辑回归也会遇到过拟合的问题。我们可以修改我们的逻辑回归成本函数来添加正则化参数,如下所示:

Regularization

为了实现逻辑回归的梯度下降,我们最终得到了一个表面上看起来与线性回归的梯度下降相同的方程。然而,我们必须记住,我们的假设函数是我们用于逻辑回归的函数。

Regularization

使用假设函数,我们得到以下结果:

Regularization

总结

在这一章中,我们研究了机器学习中一些最常用的技术。我们为线性和逻辑回归创建了假设表示。您学习了如何创建成本函数来衡量训练数据假设的性能,以及如何使用梯度下降和正规方程最小化成本函数来拟合参数。我们展示了如何通过在假设函数中使用多项式项来将假设函数拟合到非线性数据。最后,我们研究了正则化,它的用途,以及如何将其应用于逻辑回归和线性回归。

这些是在许多不同的机器学习算法中广泛使用的强大技术。然而,正如你可能已经意识到的,这个故事还有很多。到目前为止,我们所研究的模型通常需要相当多的人工干预才能使它们有效地运行。例如,我们必须设置超参数,如学习率或正则化参数,并且,在非线性数据的情况下,我们必须尝试并找到多项式项,这将迫使我们的假设与数据相匹配。很难确切地确定这些术语是什么,尤其是当我们有很多特征的时候。在下一章中,我们将看看驱动这个星球上一些最强大的学习算法的想法,即神经网络。

六、神经网络

人工神经网络,顾名思义,是基于试图模仿神经元在大脑中工作方式的算法。概念工作始于 20 世纪 40 年代,但直到最近,一些重要的见解,以及运行这些计算成本更高的模型的硬件的可用性,才使神经网络得到了实际应用。它们现在是最先进的技术,是许多高级机器学习应用的核心。

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

  • 后勤单位
  • 神经网络的代价函数
  • 实现神经网络
  • 其他神经网络架构

神经网络入门

我们在上一章中看到了如何通过向假设函数添加多项式项来创建非线性决策边界。我们也可以在线性回归中使用这种技术来拟合非线性数据。然而,由于多种原因,这不是理想的解决方案。首先,我们必须选择多项式项,对于复杂的决策边界,这可能是一个不精确且耗时的过程,可能需要大量的试错。我们还需要考虑当我们拥有大量功能时会发生什么。很难准确理解增加的多项式项将如何改变决策边界。这也意味着衍生特征的可能数量将呈指数级增长。为了适应复杂的边界,我们将需要许多高阶项,我们的模型将变得笨拙、计算昂贵且难以理解。

考虑计算机视觉等应用,在灰度图像中,每个像素都是一个值介于 0 到 255 之间的特征。对于一个小图像,比如 100 像素乘 100 像素,我们有 10,000 个特征。如果我们只包括二次项,我们最终会得到大约 5000 万个可能的特征,为了适应复杂的决策边界,我们可能需要三次项和更高阶的项。显然,这样的模式完全行不通。

当我们处理模仿大脑的问题时,我们面临着许多困难。考虑到大脑做的所有不同的事情,我们可能会首先认为大脑由许多不同的算法组成,每个算法都专门用于完成特定的任务,并且每个算法都被硬连线到大脑的不同部分。这种方法基本上将大脑视为多个子系统,每个子系统都有自己的程序和任务。例如,用于感知声音的听觉皮层有其自己的算法,例如,对进入的声波进行傅立叶变换以检测音调。另一方面,视觉皮层有自己独特的算法来解码视神经的信号并将其转换成视觉。然而,越来越多的证据表明,大脑根本不是这样运作的。

最近在动物身上的实验显示了脑组织的显著适应性。科学家们将动物的视神经重新连接到听觉皮层,发现大脑可以利用听觉皮层的机制来学习视觉。尽管这些动物的视觉皮层被绕过了,但它们还是被测试出拥有完全的视觉。大脑不同部位的脑组织似乎可以重新学习如何解释其输入。因此,大脑不是由被编程来执行特定任务的专门子系统组成,而是使用相同的算法来学习不同的任务。这种单一算法方法有许多优点,尤其是它相对容易实现。这也意味着我们可以创建通用模型,然后训练它们执行专门的任务。就像在真实大脑中使用单一算法来描述每个神经元如何与其周围的其他神经元通信一样,它允许人工神经网络具有适应性,能够执行多个更高级别的任务。但是,这个单一算法的本质是什么?

当试图模仿真实的大脑功能时,我们被迫大大简化了许多事情。例如,没有办法考虑大脑化学状态的作用,或者大脑在不同发育和生长阶段的状态。目前使用的大多数神经网络模型采用离散的人工神经元层或单元,以有序的线性序列或层连接。另一方面,大脑由许多复杂的、嵌套的和相互连接的神经回路组成。在试图模仿这些复杂的反馈系统方面已经取得了一些进展,我们将在本章的最后讨论这些。然而,对于真实的大脑行为以及如何将这种复杂的行为纳入人工神经网络,我们仍然有很多不知道的地方。

后勤单位

作为起点,我们在神经元的简化模型上使用逻辑单元的概念。它由一组输入和输出以及一个激活功能组成。这个激活函数本质上是对输入集进行计算,然后给出输出。这里,我们将激活函数设置为上一章中用于逻辑回归的 sigmoid:

Logistic units

我们有两个输入单元,x 1 和 x 2 和一个偏置单元,x 0 ,设置为 1。这些被输入到假设函数中,该假设函数使用 sigmoid 逻辑函数和权重向量 w ,该权重向量参数化假设函数。由二进制值组成的特征向量和前面示例的参数向量由以下内容组成:

Logistic units

为了了解如何让它执行逻辑功能,让我们给模型一些权重。我们可以把它写成 sigmoid、 g 和我们的权重的函数。首先,我们将选择一些重量。我们将很快学习如何训练模型学习它自己的重量。假设我们设定了权重,使得我们具有以下假设函数:

Logistic units

我们为模型提供一些简单的标记数据,并构建一个真值表:

Logistic units

虽然这个数据看起来相对简单,但是分离类所需要的决策边界并不简单。我们的目标变量 y 与输入变量形成逻辑的 XNOR 。只有当 x 1x 2 均为 01 时,输出才为 1

在这里,我们的假设给了我们一个逻辑。也就是说,当x1T7】和x2T11】都为 1 时,返回一个 1 。通过将权重设置为其他值,我们可以让单个人工神经元形成其他逻辑函数。**

这给了我们逻辑上的功能:

Logistic units

为了执行 XNOR,我们结合了与、或和非函数。要执行否定,即逻辑而非,我们只需为要否定的输入变量选择大的负权重。

物流单元连接在一起形成人工神经网络。这些网络由输入层、一个或多个隐藏层和输出层组成。每个单元都有一个激活函数,这里是 sigmoid,并通过权重矩阵 W 进行参数化:

Logistic units

我们可以写出隐藏层中每个单元的激活函数:

Logistic units

输出层的激活功能如下:

Logistic units

更一般地,我们可以说从给定层 j 到层 j+1 的函数映射是由参数矩阵wjT8】确定的。超级脚本 j 表示 j 层,下标 i 表示该层的单元。我们表示参数或权重矩阵, W (j) ,它控制从图层 j 到图层 j + 1 的映射。我们在矩阵索引的下标中表示各个权重。

注意,每一层的参数矩阵的维数将是下一层的单位数乘以当前层的单位数加上1;这是针对x0T5 的,这是偏向层。更正式地说,我们可以写出给定层的参数矩阵的维数 j ,如下所示:

Logistic units

下标 (j + 1) 指的是下一个输入层和前一层的单位数,djT5】+1指的是当前层的单位数加上 1

现在让我们看看如何使用向量实现来计算这些激活函数。我们可以通过定义一个新的术语 Z 来更简洁地编写这些函数,它由给定层上每个单元的输入值的加权线性组合组成。这里有一个例子:

Logistic units

我们只是用一个单一的函数 Z 来代替我们激活函数内部术语中的一切。这里超级脚本 (2) 表示层数,下标 1 表示该层的单位。因此,更一般地,定义层 j 的激活函数的矩阵如下:

Logistic units

因此,在我们的三层示例中,我们的输出层可以定义如下:

Logistic units

我们可以通过首先仅查看单个隐藏层上的三个单元以及它如何将其输入映射到输出层上的单个单元的输入来学习特征。我们可以看到,它仅使用特征集( a 2 ) 执行逻辑回归。不同之处在于,现在隐藏图层的输入要素本身已经使用从输入图层的原始要素中学习的权重进行了计算。通过隐藏层,我们可以开始拟合更复杂的非线性函数。

我们可以使用下面的神经网络结构来解决我们的 XNOR 问题:

Logistic units

这里,我们在输入层有三个单位,在单个隐藏层有两个单位加偏置单位,在输出层有一个单位。我们可以为隐藏层中的第一个单元(不包括偏置单元)设置权重,以执行逻辑功能 x 1 和 x2T6】。第二单元的砝码执行功能 (NOT x 1 )和(NOT x 2 ) 。最后,我们的输出层执行或函数。

我们可以按如下方式编写激活函数:

Logistic units

该网络的真值表如下所示:

Logistic units

为了用神经网络执行多类分类,我们使用的体系结构为我们试图分类的每个类都有一个输出单元。网络输出一个二进制数向量,其中 1 表示该类存在。这个输出变量是一个 i 维向量,其中 i 是输出类的数量。例如,四个功能的输出空间如下所示:

Logistic units

我们的目标是定义一个假设函数,大约等于这四个向量中的一个:

Logistic units

这本质上是一个一对所有的表现。

我们可以用层数 L 来描述一个神经网络架构,用一个数来描述每层中的单元数sIT5】,其中下标表示层数。为了方便起见,我打算定义一个变量, t ,表示层 l + 1 上的单位数,其中 l + 1 为正向层,即图右侧的层。

成本函数

为了给定训练集拟合神经网络中的权重,我们首先需要定义一个成本函数:

Cost function

这与我们用于逻辑回归的成本函数非常相似,除了现在我们也在对 k 输出单位求和。正则化项中使用的三重求和看起来有点复杂,但它真正做的是对参数矩阵中的每个项求和,并使用它来计算正则化。注意求和, ilj1 开始,而不是0;这是为了反映我们不对偏差单位应用正则化的事实。

最小化成本函数

既然有了成本函数,就要想办法最小化。与梯度下降一样,我们需要计算偏导数来计算成本函数的斜率。这是使用反向传播算法完成的。之所以称之为反向传播,是因为我们首先计算输出层的误差,然后依次计算前一层的误差。我们可以使用成本函数计算的这些导数来计算神经网络中每个单元的参数值。为此,我们需要定义一个错误术语:

Minimizing the cost function

对于这个例子,让我们假设我们总共有三层,包括输入和输出层。输出层的错误可以写为:

Minimizing the cost function

最后一层的激活函数相当于我们的假设函数,我们可以用简单的向量 减法来计算我们假设预测的值和我们训练集中的实际值之间的差值。一旦我们知道了输出层中的错误,我们就能够反向传播来找到先前层中的错误,即增量值:

Minimizing the cost function

这将计算第三层的误差。我们使用当前层的参数向量的转置,在这个例子中是层 2,乘以来自前向层的误差向量,在这个例子中是层 3。然后,我们使用成对乘法,用符号 *** 表示,用激活函数的导数 g ,在由 z (3) 给出的输入值下计算。我们可以通过以下公式计算这个导数项:

Minimizing the cost function

如果你知道微积分,这是一个相当直接的程序来证明这一点,但出于我们的目的,我们在这里不深入讨论。如您所料,当我们有多个隐藏层时,我们可以使用完全相同的方式计算每个隐藏层的增量值,使用参数向量、前向层的增量向量和当前层激活函数的导数。我们不需要计算第 1 层的增量值,因为这些只是要素本身,没有任何错误。最后,通过一个相当复杂的数学证明,我们将不再赘述,我们可以写出代价函数的导数,忽略正则化,如下所示:

Minimizing the cost function

通过使用反向传播计算增量项,我们可以找到每个参数值的偏导数。现在,让我们看看如何将其应用于训练样本数据集。我们需要定义资本 delta,δ,它只是 delta 项的矩阵,有维度, l:i:j 。当算法在每个训练样本中循环时,这将充当神经网络中每个节点的增量值的累加器。在每个循环中,它对每个训练样本执行以下功能:

  1. 它将第一层的激活函数设置为 x 的每个值,即我们的输入特征。

  2. 它在每个后续层上依次向前传播到输出层,以计算每个层的激活函数。

  3. It computes the delta values at the output layer and begins the process of back propagation. This is similar to the process we performed in forward propagation, except that it occurs in reverse. So, for our output layer in our 3-layer example, it is demonstrated as follows:

    Minimizing the cost function

请记住,这一切都是在一个循环中发生的,因此我们一次只处理一个训练样本; y (i) 代表 i th 训练样本的目标值。我们现在可以使用反向传播算法来计算先前图层的增量值。我们现在可以使用更新规则将这些值添加到累加器中:

Minimizing the cost function

这个公式可以用矢量化的形式表示,一次更新所有训练样本,如图所示:

Minimizing the cost function

现在,我们可以添加正则化项:

Minimizing the cost function

最后,我们可以通过执行梯度下降来更新权重:

Minimizing the cost function

记住 α 是学习率,也就是我们设置为 0 到 1 之间的一个小数值的超参数。

实现神经网络

还有一件事我们需要考虑,那就是我们权重的初始化。如果我们将它们初始化为 0,或者全部初始化为相同的数字,则前向层上的所有单元将在输入端计算相同的函数,这使得计算高度冗余,并且无法拟合复杂的数据。本质上,我们需要做的是打破对称性,这样我们给每个单元一个稍微不同的起点,实际上允许网络创建更多有趣的功能。

现在,让我们看看如何在代码中实现这一点。这个实现是 Sebastian Raschka 写的,摘自他的优秀著作, Python 机器学习,由 Packt 出版发行:

import numpy as np
from scipy.special import expit
import sys

class NeuralNetMLP(object):

 def __init__(self, n_output, n_features, n_hidden=30,
 l1=0.0, l2=0.0, epochs=500, eta=0.001, 
 alpha=0.0, decrease_const=0.0, shuffle=True,
 minibatches=1, random_state=None):

 np.random.seed(random_state)
 self.n_output = n_output
 self.n_features = n_features
 self.n_hidden = n_hidden
 self.w1, self.w2 = self._initialize_weights()
 self.l1 = l1
 self.l2 = l2
 self.epochs = epochs
 self.eta = eta
 self.alpha = alpha
 self.decrease_const = decrease_const
 self.shuffle = shuffle
 self.minibatches = minibatches

 def _encode_labels(self, y, k):

 onehot = np.zeros((k, y.shape[0]))
 for idx, val in enumerate(y):
 onehot[val, idx] = 1.0
 return onehot

 def _initialize_weights(self):
 """Initialize weights with small random numbers."""
 w1 = np.random.uniform(-1.0, 1.0, size=self.n_hidden*(self.n_features + 1))
 w1 = w1.reshape(self.n_hidden, self.n_features + 1)
 w2 = np.random.uniform(-1.0, 1.0, size=self.n_output*(self.n_hidden + 1))
 w2 = w2.reshape(self.n_output, self.n_hidden + 1)
 return w1, w2

 def _sigmoid(self, z):

 # return 1.0 / (1.0 + np.exp(-z))
 return expit(z)

 def _sigmoid_gradient(self, z):
 sg = self._sigmoid(z)
 return sg * (1 - sg)

 def _add_bias_unit(self, X, how='column'):

 if how == 'column':
 X_new = np.ones((X.shape[0], X.shape[1]+1))
 X_new[:, 1:] = X
 elif how == 'row':
 X_new = np.ones((X.shape[0]+1, X.shape[1]))
 X_new[1:, :] = X
 else:
 raise AttributeError('`how` must be `column` or `row`')
 return X_new

 def _feedforward(self, X, w1, w2):

 a1 = self._add_bias_unit(X, how='column')
 z2 = w1.dot(a1.T)
 a2 = self._sigmoid(z2)
 a2 = self._add_bias_unit(a2, how='row')
 z3 = w2.dot(a2)
 a3 = self._sigmoid(z3)
 return a1, z2, a2, z3, a3

 def _L2_reg(self, lambda_, w1, w2):
 """Compute L2-regularization cost"""
 return (lambda_/2.0) * (np.sum(w1[:, 1:] ** 2) + np.sum(w2[:, 1:] ** 2))

 def _L1_reg(self, lambda_, w1, w2):
 """Compute L1-regularization cost"""
 return (lambda_/2.0) * (np.abs(w1[:, 1:]).sum() + np.abs(w2[:, 1:]).sum())

 def _get_cost(self, y_enc, output, w1, w2):

 term1 = -y_enc * (np.log(output))
 term2 = (1 - y_enc) * np.log(1 - output)
 cost = np.sum(term1 - term2)
 L1_term = self._L1_reg(self.l1, w1, w2)
 L2_term = self._L2_reg(self.l2, w1, w2)
 cost = cost + L1_term + L2_term
 return cost

 def _get_gradient(self, a1, a2, a3, z2, y_enc, w1, w2):

 # backpropagation
 sigma3 = a3 - y_enc
 z2 = self._add_bias_unit(z2, how='row')
 sigma2 = w2.T.dot(sigma3) * self._sigmoid_gradient(z2)
 sigma2 = sigma2[1:, :]
 grad1 = sigma2.dot(a1)
 grad2 = sigma3.dot(a2.T)

 # regularize
 grad1[:, 1:] += (w1[:, 1:] * (self.l1 + self.l2))
 grad2[:, 1:] += (w2[:, 1:] * (self.l1 + self.l2))

 return grad1, grad2

 def predict(self, X):

 if len(X.shape) != 2:
 raise AttributeError('X must be a [n_samples, n_features] array.\n'
 'Use X[:,None] for 1-feature classification,'
 '\nor X[[i]] for 1-sample classification')

 a1, z2, a2, z3, a3 = self._feedforward(X, self.w1, self.w2)
 y_pred = np.argmax(z3, axis=0)
 return y_pred

 def fit(self, X, y, print_progress=False):

 self.cost_ = []
 X_data, y_data = X.copy(), y.copy()
 y_enc = self._encode_labels(y, self.n_output)

 delta_w1_prev = np.zeros(self.w1.shape)
 delta_w2_prev = np.zeros(self.w2.shape)

 for i in range(self.epochs):

 # adaptive learning rate
 self.eta /= (1 + self.decrease_const*i)

 if print_progress:
 sys.stderr.write('\rEpoch: %d/%d' % (i+1, self.epochs))
 sys.stderr.flush()

 if self.shuffle:
 idx = np.random.permutation(y_data.shape[0])
 X_data, y_data = X_data[idx], y_data[idx]

 mini = np.array_split(range(y_data.shape[0]), self.minibatches)
 for idx in mini:

 # feedforward
 a1, z2, a2, z3, a3 = self._feedforward(X[idx], self.w1, self.w2)
 cost = self._get_cost(y_enc=y_enc[:, idx],
 output=a3,
 w1=self.w1,
 w2=self.w2)
 self.cost_.append(cost)

 # compute gradient via backpropagation
 grad1, grad2 = self._get_gradient(a1=a1, a2=a2,
 a3=a3, z2=z2,
 y_enc=y_enc[:, idx],
 w1=self.w1,
 w2=self.w2)

 delta_w1, delta_w2 = self.eta * grad1, self.eta * grad2
 self.w1 -= (delta_w1 + (self.alpha * delta_w1_prev))
 self.w2 -= (delta_w2 + (self.alpha * delta_w2_prev))
 delta_w1_prev, delta_w2_prev = delta_w1, delta_w2

 return self

现在,让我们将这个神经网络应用于虹膜样本数据集。请记住,这个数据集包含三个类,因此我们将n_output参数(输出层数)设置为 3。数据集中第一个轴的形状指的是要素的数量。我们创建 50 个隐藏层和 100 个纪元,每个纪元是所有训练集的完整循环。在这里,我们将学习率alpha设置为.001,并显示成本与时代数量的关系图:

iris = datasets.load_iris()
X=iris.data
y=iris.target
nn= NeuralNetMLP(3, X.shape[1],n_hidden=50, epochs=100, alpha=.001)
nn.fit(X,y)
plt.plot(range(len(nn.cost_)),nn.cost_)
plt.show()

以下是输出:

Implementing a neural network

该图显示了成本在每个时期是如何降低的。要了解模型是如何工作的,可以花一些时间在其他数据集和各种输入参数上进行实验。测试多类分类问题时经常使用的一个特定数据集是 MNIST 数据集,可在http://yann.lecun.com/exdb/mnist/获得。这由包含 60,000 个手绘字母图像及其标签的数据集组成。它经常被用作机器学习算法的基准。

梯度检查

反向传播和一般的神经网络,有点难以概念化。因此,经常不容易理解改变任何模型(超)参数将如何影响结果。此外,通过不同的实现,有可能获得指示算法正在正确工作的结果,即,在梯度下降的每个级别上,成本函数正在降低。然而,就像任何复杂的软件一样,可能会有隐藏的 bug,这些 bug 可能只在非常特殊的情况下才会表现出来。一种帮助消除这些的方法是通过一种叫做梯度检查的程序。这是一种近似梯度的数值方法,我们可以通过查看下图直观地理解这一点:

Gradient checking

J(w) 相对于 w、的导数可以近似如下:

Gradient checking

当参数为单个值时,上述公式近似于导数。我们需要在一个成本函数上评估这些导数,其中权重是一个向量。我们通过依次对每个权重进行偏导数来实现这一点。这里有一个例子:

Gradient checking

其他神经网络架构

在神经网络模型领域,以及实际上一般的机器学习领域,许多最重要的工作正在使用具有许多层和特征的非常复杂的神经网络。这种方法通常被称为深度架构或深度学习。人类和动物学习的速度和深度是任何机器都无法比拟的。生物学习的许多要素仍然是个谜。目标识别是研究的关键领域之一,也是实际应用中最有用的领域之一。这是生命系统非常基本的东西,高等动物已经进化出一种非凡的能力来学习物体之间的复杂关系。生物大脑有很多层;每个突触事件都存在于一长串突触过程中。为了识别复杂的对象,如人脸或手写数字,需要的一项基本任务是创建一个从原始输入到越来越高的抽象层次的表示层次。目标是将原始数据(如一组像素值)转换成我们可以描述为骑自行车的人的东西。解决这类问题的一种方法是使用稀疏表示,创建更高维的特征空间,其中有许多特征,但只有极少数具有非零值。这种方法很有吸引力,原因有几个。首先,在更高的特征空间中,特征可能变得更加线性可分。此外,在某些模型中已经表明,稀疏性可以用来使训练更加有效,并有助于从非常嘈杂的数据中提取信息。我们将在下一章中更详细地探讨这个想法和特征提取的一般概念。

另一个有趣的想法是 T2 循环神经网络或 T4 循环神经网络。这些在许多方面与我们目前考虑的前馈网络截然不同。RNNs 不仅仅是输入和输出之间的静态映射,它至少有一条循环反馈路径。rnn 给网络引入了时间成分,因为一个单元的输入可能包括它先前通过反馈回路接收的输入。所有的生物神经网络都是高度递归的。人工神经网络在语音和手写识别等领域显示出了前景。然而,一般来说,它们更难训练,因为我们不能简单地反向传播错误。我们必须考虑这种系统的时间成分和动态非线性特征。RNNs 将为未来的研究提供一个非常有趣的领域。

总结

在这一章中,我们介绍了人工神经网络强大的机器学习算法。我们看到了这些网络是如何成为大脑神经元的简化模型的。他们可以执行复杂的学习任务,例如学习高度非线性的决策边界,使用多层人工神经元或单元,从标记数据中学习新特征。在下一章中,我们将研究任何机器学习算法的关键组成部分,也就是它的特征。

七、特征——算法如何看待世界

到目前为止,在本书中,我们为创建、提取或以其他方式操作特征提出了许多方法和理由。在这一章中,我们将直接讨论这个话题。正确的特征,有时被称为属性 T2,是机器学习模型的核心组成部分。一个有错误特征的复杂模型是没有价值的。特征是我们的应用看待世界的方式。对于除了最简单的任务之外的所有任务,我们将在将特征输入模型之前对其进行处理。我们可以通过许多有趣的方式来做到这一点,这是一个非常重要的话题,用整整一章来讨论它是合适的。

直到最近十年左右,机器学习模型才例行公事地使用成千上万甚至更多的特征。这使我们能够解决许多不同的问题,例如与样本数量相比,我们的特征集很大的问题。两个典型的应用是遗传分析和文本分类。对于遗传分析,我们的变量是一组基因表达系数。这些是基于样本中存在的基因的数量,例如,取自组织活检。可以执行分类任务来预测患者是否患有癌症。训练样本和测试样本的总数可以小于 100。另一方面,原始数据中的变量数量可能在 6000 到 60000 之间。这不仅会转化为大量的特征,还意味着特征之间的取值范围也相当大。在本章中,我们将涵盖以下主题:

  • 特征类型
  • 运营和统计
  • 结构化特征
  • 转变特征
  • 主成分分析

特征类型

有三种不同类型的特征:数量特征、顺序特征和分类特征。我们也可以考虑第四种类型的特征——布尔型,因为这种类型确实有一些不同的性质,尽管它实际上是一种分类特征。这些特征类型可以根据它们传递的信息量来排序。数量特征具有最高的信息容量,其次是序数、分类和布尔。

让我们看一下表格分析:

|

特征类型

|

命令

|

规模

|

趋势

|

散布

|

形状

|
| --- | --- | --- | --- | --- | --- |
| 定量 | 是 | 是 | 均值 | 范围、方差和标准偏差 | 偏斜度、峰度 |
| 序数 | 是 | 不 | 中位数 | 有多少人 | 钠 |
| 分类 | 不 | 不 | 方式 | 钠 | 钠 |

上表显示了三种类型的功能、它们的统计数据和属性。每个要素从表中下一行的要素继承统计信息。例如,定量特征的中心趋势测量包括中位数和众数。

数量特征

数量特征的区别特征是连续的,通常涉及到映射到实数。通常,特征值可以映射到实数的子集,例如,以年表示年龄;但是,在计算统计数据(如平均值或标准偏差)时,必须注意使用满刻度。因为定量特征有一个有意义的数字标度,所以它们经常用在几何模型中。当在树模型中使用它们时,它们会导致二进制拆分,例如,使用阈值,其中高于阈值的值归一个子级,等于或低于阈值的值归另一个子级。树模型对比例的单调变换不敏感,即不改变特征值顺序的变换。例如,对于树模型来说,如果我们以厘米或英寸为单位测量长度,或者使用对数或线性比例,这并不重要,我们只需将阈值更改为相同的比例。树模型忽略了数量特征的尺度,将它们视为序数。基于规则的模型也是如此。对于概率模型,如朴素贝叶斯分类器,需要将定量特征离散到有限数量的箱中,并因此转换为分类特征。

序数特征

序数要素是具有不同顺序但没有比例的要素。它们可以被编码为整数值;然而,这样做并不意味着任何规模。一个典型的例子是门牌号。在这里,我们可以通过门牌号码来辨别一所房子在街道上的位置。我们假设 1 号房会在 20 号房之前,10 号和 11 号房会相互靠近。然而,数字的大小并不意味着任何规模;例如,没有理由相信 20 号房会比 1 号房大。序数特征的域是完全有序的集合,例如一组字符或字符串。因为序数特征缺乏线性标度,所以加或减没有意义;因此,对序数要素进行平均等操作通常没有意义,也不会产生任何关于要素的信息。类似于树模型中的数量特征,序数特征导致二元分割。一般来说,序数特征在大多数几何模型中不容易使用。例如,线性模型假设一个欧几里德实例空间,其中的特征值被视为笛卡尔坐标。对于基于距离的模型,如果我们将它们编码为整数,并且它们之间的距离只是它们之间的差异,那么我们可以使用序数特征。这有时被称为 海明距离

分类特征

分类特征,有时被称为名义特征,没有任何顺序或比例,因此,除了表示一个值最频繁出现的模式之外,不允许任何统计汇总。分类特征通常最好用概率模型来处理;但是,它们也可以在基于距离的模型中使用汉明距离,并且对于相等的值将距离设置为 0,对于不相等的值将距离设置为 1。分类特征的一个子类型是布尔特征,映射为真或假的布尔值。

操作和统计

特征可以通过可对其执行的允许操作来定义。考虑两个特征:一个人的年龄和电话号码。虽然这两个特征都可以用整数来描述,但它们实际上代表了两种截然不同的信息类型。当我们看到哪些操作可以有效地执行时,这是显而易见的。比如计算一群人的平均年龄,会给我们一个有意义的结果;计算平均电话号码不会。

我们可以把可以对一个特征执行的可能计算的范围称为它的统计量。这些统计数据描述了数据的三个不同方面。这些是——它的中心倾向,它的 T3】分散,它的 T6】形状。

为了计算数据的中心趋势,我们通常使用以下一个或多个统计量:平均值(或平均值)、中值(或有序列表中的中间值)和模式(或所有值的大多数)。模式是唯一可以应用于所有数据类型的统计信息。为了计算中位数,我们需要能够以某种方式排序的特征值,即序数或数量。为了计算平均值,数值必须以某种比例表示,如线性比例。换句话说,它们需要数量特征。

最常见的计算离差的方式是通过方差或标准差的统计。这两者实际上是相同的度量,但尺度不同,标准差很有用,因为它与要素本身的尺度相同。此外,请记住,平均值和中间值之间的绝对差异永远不会大于标准偏差。测量离差的一个更简单的统计量是范围,它只是最小值和最大值之间的差值。当然,从这里,我们可以通过计算中距点来估计特征的中心趋势。测量离差的另一种方法是使用百分位数或十分位数等单位来测量低于特定值的实例的比率。例如,p百分位是 p 百分比实例低于的值。

测量形状统计稍微复杂一点,可以使用样本的 中心时刻的概念来理解。其定义如下:

Operations and statistics

这里, n 为样本数, μ 为样本均值, k 为整数。当 k = 1 时,第一个中心矩为 0 ,因为这只是平均值的平均偏差,始终为 0 。第二个中心矩是与平均值的平均平方偏差,即方差。我们可以定义偏斜度如下:

Operations and statistics

这里 ơ 是标准差。如果这个公式给出的值是正的,那么更多的情况下值高于平均值而不是低于平均值。当绘制图表时,数据向右倾斜。当偏斜为负时,反之亦然。

我们可以将峰度定义为第四中心时刻的类似关系:

Operations and statistics

可以证明正态分布的峰度为 3。在高于这个值时,分布将更多达到峰值。峰度值低于 3 时,分布将更平坦

*我们之前讨论了三种类型的数据,即分类数据、顺序数据和数量数据。

机器学习模型将以非常不同的方式对待不同的数据类型。例如,分类特征上的决策树分裂将产生与值一样多的子代。对于序数和数量特征,分割将是二元的,每个父母基于一个阈值产生两个孩子。因此,树模型将数量特征视为序数,而忽略特征尺度。当我们考虑像贝叶斯分类器这样的概率模型时,我们可以看到它实际上将序数特征视为分类特征,而它处理数量特征的唯一方法是将它们转化为有限数量的离散值,从而将其转换为分类数据。

一般来说,几何模型需要定量的特征。例如,线性模型在欧几里得实例空间中运行,要素充当笛卡尔坐标。每个要素值都被视为与其他要素值的标量关系。基于距离的模型,如 k-最近邻模型,可以通过将相等值的距离设置为 0,不相等值的距离设置为 1 来合并分类特征。同样,我们可以通过计算两个值之间的值的数量,将序数特征纳入基于距离的模型。如果我们将特征值编码为整数,那么距离就是简单的数值差。通过选择适当的距离度量,可以将序数和分类特征结合到基于距离的模型中。

结构化特征

我们假设每个实例可以表示为特征值的向量,并且所有相关方面都由该向量表示。这有时被称为 抽象,因为我们过滤掉不必要的信息,用向量来表示现实世界的现象。例如,将列夫·托尔斯泰的全部作品表示为词频向量是一种抽象。我们不假装这个抽象将服务于任何超过一个非常特殊的有限的应用。我们可能会了解到托尔斯泰对语言的使用,也许会引出一些关于托尔斯泰创作的情感和主题的信息。然而,我们不太可能对这些作品中描绘的 19 世纪俄国的广阔背景有任何重要的了解。一个人类读者,或者一个更复杂的算法,将不是从每个单词的计数中,而是从这些单词的结构中获得这些见解。

我们可以用类似于在数据库编程语言(如 SQL)中考虑查询的方式来考虑结构化特征。一个 SQL 查询可以表示一个变量的集合,用来做一些事情,比如找到一个特定的短语或者找到所有涉及特定字符的段落。我们在机器学习环境中所做的是用这些集合属性创建另一个特征。

结构化特征可以在构建模型之前创建,也可以作为模型本身的一部分创建。在第一种情况下,这个过程可以理解为从一阶逻辑到命题逻辑的转换。这种方法的一个问题是,由于与现有功能的组合,它可能会导致潜在功能数量的激增。另一个要点是,正如在 SQL 中一个子句可以覆盖另一个子句的子集一样,结构特征也可以是逻辑相关的。这在特别适合自然语言处理的机器学习分支中被利用,称为归纳逻辑编程

变换特征

当我们转换特征时,我们的目标显然是让它们对我们的模型更有用。这可以通过添加、删除或更改特征所表示的信息来实现。一个共同特征变换是改变特征类型的变换。一个典型的例子是二值化,即将一个分类特征转化为一组二值特征。另一个例子是将序数特征变为分类特征。在这两种情况下,我们都会丢失信息。首先,单个分类特征的值是互斥的,这不是通过二进制表示来表达的。在第二种情况下,我们丢失了订购信息。这些类型的转换可以被认为是归纳性的,因为它们由一个定义明确的逻辑过程组成,除了决定首先执行这些转换之外,它不涉及客观的选择。

使用sklearn.preprocessing.Binarizer模块可以轻松进行二值化。让我们来看看以下命令:

from sklearn.preprocessing import Binarizer
from random import randint
bin=Binarizer(5)
X=[randint(0,10) for b in range(1,10)]
print(X)
print(bin.transform(X))

以下是上述命令的输出:

Transforming features

分类特征通常需要编码成整数。考虑一个非常简单的数据集,只有一个分类特征,城市,有三个可能的值,悉尼、珀斯和墨尔本,我们决定将这三个值分别编码为 0、1 和 2。如果这个信息要用于线性分类器,那么我们把约束写成一个带有权重参数的线性不等式。然而,问题是这个权重不能编码为三路选择。假设我们有两个类,东海岸和西海岸,我们需要我们的模型提出一个决策函数,该函数将反映珀斯位于西海岸,悉尼和墨尔本都位于东海岸的事实。用一个简单的线性模型,当特征以这种方式编码时,那么决策函数就不能得出一个将悉尼和墨尔本放在同一个类中的规则。解决方案是将特征空间放大为三个特征,每个特征都有自己的权重。这个叫做一热编码。Sciki-learn 实现OneHotEncoder()功能来执行该任务。这是一个估计器,它将每个分类特征的 m 个可能值转换成 m 个二进制特征。假设我们使用的数据模型由前面示例中描述的城市特征和另外两个特征组成——性别(可以是男性或女性)和职业(可以有三个值——医生、律师或银行家)。因此,举例来说,来自悉尼的女银行家将被表示为【1,2,0】。为以下示例添加了另外三个示例:

from sklearn.preprocessing import OneHotEncoder
enc = OneHotEncoder()
enc.fit([[1,2,0], [1, 1, 0], [0, 2, 1], [1, 0, 2]])
print(enc.transform([1,2,0]).toarray())

我们将获得以下输出:

Transforming features

由于我们在这个数据集中有两个性别、三个城市和三个工作,所以变换数组中的前两个数字代表性别,后三个数字代表城市,最后三个数字代表职业。

离散化

我已经简要地提到了与决策树相关的阈值化的思想,其中我们通过找到合适的特征值来分割,从而将序数或数量特征转换为二进制特征。有许多方法,既有监督的,也有无监督的,可用于在连续数据中找到适当的分割,例如,使用中心趋势(监督的)统计,如平均值或中值,或基于信息增益等标准优化目标函数。

我们可以更进一步,创建多个阈值,将一个数量特征转化为一个序数特征。这里,我们将一个连续的数量特征分成许多离散的序数值。这些值中的每一个被称为一个,每个箱代表原始定量特征上的一个区间。许多机器学习模型需要离散值。使用离散值创建基于规则的模型变得更加容易理解。离散化还使特征更加紧凑,并可能使我们的算法更加高效。

最常见的方法之一是选择箱,使得每个箱具有大约相同数量的实例。这被称为等频离散化,如果我们将其应用于两个面元,那么这与使用中值作为阈值是一样的。这种方法非常有用,因为可以通过代表分位数的方式设置仓位边界。例如,如果我们有 100 个箱,那么每个箱代表一个百分点。

或者,我们可以选择边界,使每个面元具有相同的间隔宽度。这叫做等宽离散化。计算该面元宽度间隔值的一种方法是简单地用面元数量划分特征范围。有时,特征没有上限或下限,我们无法计算其范围。在这种情况下,可以使用高于和低于平均值的标准偏差的整数。宽度和频率离散化都是无监督的。它们不需要任何关于类标签的知识就能工作。

现在让我们把注意力转向监督离散化。基本上有两种方法:自上而下的或分裂,以及聚集或自下而上的方法。顾名思义,分裂的工作原理是首先假设所有样本都属于同一个容器,然后逐步分裂容器。凝聚方法从每个实例的一个箱开始,并逐步合并这些箱。这两种方法都需要一些停止标准来决定是否需要进一步拆分。

通过阈值递归划分特征值的过程是划分离散化的一个例子。为了做到这一点,我们需要一个评分函数来找到特定特征值的最佳阈值。一种常见的方法是计算分裂的信息增益或其熵。通过确定特定分割覆盖了多少正样本和负样本,我们可以基于此标准逐步分割特征。

简单的离散化操作可以通过熊猫切割qcut 方法进行。考虑以下示例:

import pandas as pd
import numpy as np
print(pd.cut(np.array([1,2,3,4]), 3, retbins = True, right = False))

以下是观察到的输出:

Discretization

正常化

阈值化和离散化,两者都去除了定量特征的尺度,根据应用,这可能不是我们想要的。或者,我们可能想给序数或分类特征增加一个尺度。在无监督的环境中,我们称之为规范化。这通常用于处理在不同尺度上测量的定量特征。近似正态分布的特征值可以转换为 z 分数。这只是高于或低于平均值的标准偏差的符号数。阳性 z 分数表示高于平均值的标准偏差数,阴性 z 分数表示低于平均值的标准偏差数。对于某些特征,使用方差可能比使用标准差更方便。

一种更严格的规范化形式以 0 到 1 的比例表示一个特征。如果我们知道一个要素范围,我们可以简单地使用线性缩放,即以最低值和最高值之差除以原始要素值和最低值之差。这表现在以下方面:

Normalization

这里fnT3】为归一化特征, f 为原始特征, lh 分别为最低值和最高值。在许多情况下,我们可能不得不猜测范围。如果我们对某个特定分布有所了解,例如,在正态分布中,超过 99%的值可能落在平均值的+3 或-3 标准偏差范围内,那么我们可以写出如下线性标度:

Normalization

这里, μ 为平均值, ơ 为标准差。

校准

有时,我们需要给一个序数或分类特征增加标度信息。这叫做功能校准。这是一种有监督的特征变换,有许多重要的应用。例如,它允许需要缩放特征的模型(如线性分类器)处理分类和序数数据。它还为模型提供了将特征视为序数、类别或数量的灵活性。对于二元分类,我们可以利用正类的后验概率,给定一个特征值,来计算尺度。对于许多概率模型,如朴素贝叶斯,这种校准方法还有一个额外的优点,即一旦校准了特征,模型就不需要任何额外的训练。对于分类特征,我们可以通过简单地从训练集中收集相对频率来确定这些概率。

在某些情况下我们可能需要将数量特征或序数特征转化为类别特征,同时保持一个顺序。我们这样做的主要方式之一是通过物流校准的流程。如果我们假设特征以相同的方差正态分布,那么我们可以用给定特征值 v 来表示似然比,即正类和负类的比值,如下所示:

Calibration

其中 d 质数是两类平均值除以标准偏差的差值:

Calibration

还有, zz 的分数:

Calibration

为了抵消不均匀类分布的效应,我们可以使用以下公式计算校准特征:

Calibration

你可能会注意到,这正是我们用于逻辑回归的 sigmoid 激活函数。总结逻辑校准,我们主要使用三个步骤:

  1. 估计正类和负类的类均值。
  2. 将特征转换为 z 分数。
  3. 应用 sigmoid 函数给出校准概率。

有时,我们可能会跳过最后一步,特别是如果我们使用基于距离的模型,其中我们期望比例是相加的,以便计算欧几里德距离。您可能会注意到,我们最终校准的特征在比例上是倍增的。

另一种校准技术等渗校准,用于定量和顺序特征。这使用了所谓的 ROC 曲线(代表接收器操作员特征),类似于第 4 章模型–从信息中学习中讨论逻辑模型时使用的覆盖图。不同的是,使用 ROC 曲线,我们将轴归一化为【0,1】

我们可以使用sklearn包来创建 ROC 曲线:

import matplotlib.pyplot as plt
from sklearn import svm, datasets
from sklearn.metrics import roc_curve, auc
from sklearn.cross_validation import train_test_split
from sklearn.preprocessing import label_binarize
from sklearn.multiclass import OneVsRestClassifier

X, y = datasets.make_classification(n_samples=100,n_classes=3,n_features=5, n_informative=3, n_redundant=0,random_state=42)
# Binarize the output
y = label_binarize(y, classes=[0, 1, 2])
n_classes = y.shape[1]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.5)
classifier = OneVsRestClassifier(svm.SVC(kernel='linear', probability=True, ))
y_score = classifier.fit(X_train, y_train).decision_function(X_test)
fpr, tpr, _ = roc_curve(y_test[:,0], y_score[:,0]) 
roc_auc = auc(fpr, tpr)
plt.figure()
plt.plot(fpr, tpr, label='ROC AUC %0.2f' % roc_auc)
plt.plot([0, 1], [0, 1], 'k--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic')
plt.legend(loc="best")
plt.show()

以下是观察到的输出:

Calibration

ROC 曲线绘制了不同阈值的真阳性率与假阳性率。在上图中,这由虚线表示。一旦我们构建了 ROC 曲线,我们就可以计算凸包的每一段中的阳性数, m i ,以及实例总数, n i 。然后使用以下公式计算校准特征值:

Calibration

在这个公式中, c 是先验概率,即正类概率与负类概率的比值。

到目前为止,在我们关于特征转换的讨论中,我们假设我们知道每个特征的所有值。在现实世界中,情况往往并非如此。如果我们使用概率模型,我们可以通过对所有特征值进行加权平均来估计缺失特征的值。一个重要的考虑是缺失特征值的存在可能与目标变量相关。例如,个人病史中的数据反映了所进行的检测类型,而这又与某些疾病的风险因素评估有关。

如果我们使用的是树模型,我们可以随机选择一个缺失的值,允许模型在其上拆分。然而,这不适用于线性模型。在这种情况下,我们需要通过插补的过程来填写缺失的值。对于分类,我们可以简单地使用观察特征的平均值、中值和模式的统计来估算缺失的值。如果我们想考虑特征相关性,我们可以为每个不完整的特征构建一个预测模型来预测缺失值。

由于 scikit-learn 估计器总是假设数组中的所有值都是数字,因此缺失的值,无论是编码为空格、n an 还是其他占位符,都会产生错误。此外,由于我们可能不想丢弃整行或整列,因为这些可能包含有价值的信息,我们需要使用插补策略来完成数据集。在下面的代码片段中,我们将使用Imputer类:

from sklearn.preprocessing import Binarizer, Imputer, OneHotEncoder
imp = Imputer(missing_values='NaN', strategy='mean', axis=0)
print(imp.fit_transform([[1, 3], [4, np.nan], [5, 6]]))

以下是输出:

Calibration

很多机器学习算法要求特征是标准化的。这意味着,当单个特征看起来或多或少像均值和单位方差接近于零的正态分布数据时,它们将发挥最佳作用。最简单的方法是从每个特征中减去平均值,然后用标准偏差除以该平均值。这可以通过sklearn.preprocessing()功能中的scale()功能或standardScaler()功能来实现。虽然这些函数将接受稀疏数据,但它们可能不应该在这种情况下使用,因为将稀疏数据居中可能会破坏其结构。在这些情况下,建议使用MacAbsScaler()maxabs_scale()功能。前者根据每个特征的最大绝对值分别进行缩放和平移。后者将每个特征单独缩放至 [-1,1] 的范围。另一个具体的例子是当我们在数据中有异常值时。在这些情况下,建议使用robust_scale()RobustScaler()功能。

通常,我们可能希望通过添加多项式项来增加模型的复杂性。这可以使用PolynomialFeatures()功能来完成:

from sklearn.preprocessing import PolynomialFeatures
X=np.arange(9).reshape(3,3)
poly=PolynomialFeatures(degree=2)
print(X)
print(poly.fit_transform(X))

我们将观察以下输出:

Calibration

主成分分析

主成分分析 ( PCA )是我们可以应用于特征的最常见的降维形式。考虑由两个要素组成的数据集的示例,我们希望将这个二维数据转换为一维数据。一种自然的方法是画一条最接近的线,并将每个数据点投影到这条线上,如下图所示:

Principle component analysis

主成分分析试图通过最小化数据点和我们试图将数据投影到的直线之间的距离来找到一个表面来投影数据。对于更一般的情况,我们有 n 维,我们想把这个空间缩小到 k 维,我们找到 k 向量 u(1),u(2)、...,u(k) 将数据投影到其上,以最小化投影误差。也就是说,我们试图找到一个 k 维表面来投影数据。

这表面上看起来像线性回归,但在几个重要方面有所不同。对于线性回归,我们试图预测给定输入变量的某个输出变量的值。在主成分分析中,我们不是试图预测一个输出变量,而是试图找到一个子空间来投射我们的输入数据。如前图所示,误差距离不是点和线之间的垂直距离,如线性回归的情况,而是点和线之间最接近的正交距离。因此,误差线与轴成一定角度,并与我们的投影线成直角。

重要的一点是,在大多数情况下,主成分分析需要对特征进行缩放和均值归一化,也就是说,特征的均值为零,并且具有可比较的值范围。我们可以使用以下公式计算平均值:

Principle component analysis

通过替换以下内容计算总和:

Principle component analysis

如果特征具有明显不同的比例,我们可以使用以下方法重新缩放:

Principle component analysis

这些功能可在sklearn.preprocessing模块中获得。

计算低维向量和这些向量上我们投影原始数据的点的数学过程包括首先计算协方差矩阵,然后计算这个矩阵的特征向量。从第一性原理计算这些值是一个相当复杂的过程。幸运的是,sklearn包有一个库来完成这个任务:

from sklearn.decomposition import PCA
import numpy as np
X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
pca = PCA(n_components=1)
pca.fit(X)
print(pca.transform(X))

我们将获得以下输出:

Principle component analysis

总结

我们可以通过多种方式来转换和构建新的特征,以使我们的模型更有效地工作,并给出更准确的结果。一般来说,没有硬性的规则来决定特定模型使用哪种方法。这在很大程度上取决于您使用的特征类型(数量、顺序或分类)。一个好的第一种方法是标准化和缩放特征,如果模型需要,将特征转换为合适的类型,就像我们通过离散化所做的那样。如果模型表现不佳,可能需要应用进一步的预处理,如主成分分析。在下一章中,我们将研究如何通过使用集成来组合不同类型的模型,以提高性能并提供更大的预测能力。*

八、集成学习

创建机器学习集成的动机来自清晰的直觉,并且基于丰富的理论历史。在许多自然和人工系统中,多样性使它们对扰动更有弹性。同样,我们已经看到,对大量测量结果进行平均通常可以得到更稳定的模型,该模型不太容易受到随机波动的影响,例如数据收集中的异常值或误差。

在本章中,我们将把这个相当大而多样的空间分成以下几个主题:

  • “类型”集合
  • 制袋材料
  • 随机森林
  • 助推

集合型式

集成技法可以大致分为两种类型:

  • 平均法:这是法,其中几个估计量独立运行,它们的预测值被平均。这包括随机森林和装袋方法。
  • Boosting 方法:这是中的方法,使用基于错误率的数据的加权分布,依次构建弱学习者。

集成方法使用多个模型来获得比任何单个组成模型更好的性能。其目的不仅是建立多样化和健壮的模型,而且在诸如处理速度和返回时间等限制内工作。当处理大型数据集和快速响应时,这可能是一个重大的发展瓶颈。故障排除和诊断是使用所有机器学习模型的一个重要方面,尤其是当我们处理可能需要几天运行的模型时。

可以创建的机器学习集成的类型和模型本身一样多种多样,主要考虑围绕三件事:我们如何划分数据,我们如何选择模型,以及我们使用什么方法来组合它们的结果。这种简单的说法实际上包含了一个非常大而多样的空间。

装袋

打包,也称为自举聚合,有几种风格,它们是通过从训练数据中抽取随机子集的方式来定义的。最常见的是,装袋是指抽取样品进行替换。因为样本被替换,所以生成的数据集可能包含重复项。这也意味着数据点可能会从特定生成的数据集中排除,即使该生成的数据集与原始数据集大小相同。每个生成的数据集都是不同的,这是一种在集合中的模型之间创建多样性的方法。我们可以使用以下示例计算样本中未选择数据点的概率:

Bagging

这里, n 是自举样本数。每个 n 自举样本产生不同的假设。通过平均模型或者通过选择大多数模型预测的类别来预测类别。考虑一组线性分类器。如果我们使用多数投票来确定预测的类,我们创建一个分段线性分类器边界。如果我们将投票转换为概率,那么我们将实例空间划分为多个片段,每个片段都可能有不同的分数。

还应该提到的是,使用特征的随机子集是可能的,有时也是可取的;这叫做 子空间采样。Bagging 估计器最适合复杂模型,如完全开发的决策树,因为它们可以帮助减少过度拟合。它们提供了一种简单、现成的方法来改进单一模型。

Scikit-learn 实现了一个BaggingClassifierBaggingRegressor对象。以下是它们最重要的一些参数:

|

参数

|

类型

|

描述

|

默认

|
| --- | --- | --- | --- |
| base_estimator | 估计量 | 这就是这个集成所基于的模型。 | 决策图表 |
| n_estimators | (同 Internationalorganizations)国际组织 | 这是基础估计量的数。 | Ten |
| max_samples | Int 或 float | 这是要抽取的样本数量。若浮绘max_samples*X.shape[0]。 | One |
| max_features | Int 或 float | 这是要绘制的特征数量。若浮绘max_features*X.shape[1]。 | One |
| bootstrap | 布尔代数学体系的 | 这些是替换抽取的样本。 | 真实的 |
| bootstrap_features | 布尔代数学体系的 | 这些是替换绘制的特征。 | 错误的 |

作为一个例子,下面的片段实例化了一个 bagging 分类器,该分类器包括 50 个决策树分类器基估计器,每个估计器建立在一半特征和一半样本的随机子集上:

from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn import datasets

bcls=BaggingClassifier(DecisionTreeClassifier(),max_samples=0.5, max_features=0.5, n_estimators=50)
X,y=datasets.make_blobs(n_samples=8000,centers=2, random_state=0, cluster_std=4)
bcls.fit(X,y)
print(bcls.score(X,y))

随机森林

基于树的模型特别适合集成,主要是因为它们对训练数据的变化非常敏感。当与 子空间采样一起使用时,树模型可以非常有效,导致模型更加多样化,并且由于集成中的每个模型只处理特征的子集,因此减少了训练时间。这使用特征的不同随机子集构建每棵树,因此被称为随机森林

随机林通过查找林中单个树中分区的交集来划分实例空间。它定义了一个比林中任何单独的树创建的分区都更精细的分区,也就是说,它将包含更多的细节。原则上,一个随机森林可以映射回一个单独的树,因为每个交集对应于两个不同树的分支的组合。随机森林可以被认为是本质上是基于树的模型的替代训练算法。bagging 集成中的线性分类器能够学习单个线性分类器无法学习的复杂决策边界。

sklearn.ensemble模块有两种基于决策树的算法,随机森林和极随机树。它们都通过在构造中引入随机性来创建不同的分类器,并且都包括用于分类和回归的类。对于RandomForestClassifierRandomForestRegressor类,每个树都是使用自举样本构建的。模型选择的分割不是所有要素中的最佳分割,而是从要素的随机子集中选择的。

多余的树

与随机森林一样,extra trees方法使用特征的随机子集,但是使用随机生成的最佳阈值集而不是使用最具区别性的阈值。这是以偏差的小幅增加为代价来降低方差的。两个班是ExtraTreesClassifierExtraTreesRegressor

我们来看一个分类器和extra trees分类器的例子。在这个例子中,我们使用VotingClassifier来组合不同的分类器。投票分类器可以帮助平衡单个模型的弱点。在本例中,我们向函数传递四个权重。这些权重决定了每个模型对整体结果的贡献。我们可以看到,这两个树模型在训练数据上表现过度,但在测试数据上表现更好。我们还可以看到ExtraTreesClassifier在测试集上取得了比RandomForest对象稍好的结果。此外,VotingClasifier对象在测试集上的表现优于其所有组成分类器。值得一提的是,在不同的权重和不同的数据集上运行时,可以看到每个模型的性能是如何变化的:

from sklearn import cross_validation
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.ensemble import VotingClassifier
from sklearn import datasets

def vclas(w1,w2,w3, w4):

 X , y = datasets.make_classification(n_features= 10, n_informative=4, n_samples=500, n_clusters_per_class=5)
 Xtrain,Xtest, ytrain,ytest= cross_validation.train_test_split(X,y,test_size=0.4)

 clf1 = LogisticRegression(random_state=123)
 clf2 = GaussianNB()
 clf3 = RandomForestClassifier(n_estimators=10,bootstrap=True, random_state=123)
 clf4= ExtraTreesClassifier(n_estimators=10, bootstrap=True,random_state=123)

 clfes=[clf1,clf2,clf3,clf4]

 eclf = VotingClassifier(estimators=[('lr', clf1), ('gnb', clf2), ('rf', clf3),('et',clf4)],
 voting='soft',
 weights=[w1, w2, w3,w4])

 [c.fit(Xtrain, ytrain) for c in (clf1, clf2, clf3,clf4, eclf)]

 N = 5
 ind = np.arange(N)
 width = 0.3
 fig, ax = plt.subplots()

 for i, clf in enumerate(clfes):
 print(clf,i)
 p1=ax.bar(i,clfes[i].score(Xtrain,ytrain,), width=width,color="black")
 p2=ax.bar(i+width,clfes[i].score(Xtest,ytest,), width=width,color="grey")
 ax.bar(len(clfes)+width,eclf.score(Xtrain,ytrain,), width=width,color="black")
 ax.bar(len(clfes)+width *2,eclf.score(Xtest,ytest,), width=width,color="grey")
 plt.axvline(3.8, color='k', linestyle='dashed')
 ax.set_xticks(ind + width)
 ax.set_xticklabels(['LogisticRegression',
 'GaussianNB',
 'RandomForestClassifier',
 'ExtraTrees',
 'VotingClassifier'],
 rotation=40,
 ha='right')
 plt.title('Training and test score for different classifiers')
 plt.legend([p1[0], p2[0]], ['training', 'test'], loc='lower left')
 plt.show()

vclas(1,3,5,4)

您将观察以下输出:

Extra trees

树模型允许我们根据它们贡献的样本的预期分数来评估特征的相对等级。这里,我们使用一个来评估分类任务中每个特征的重要性。特征的相对重要性基于它在树中的表示位置。树顶部的特征有助于最终决定更大比例的输入样本。

以下示例使用ExtraTreesClassifier类来映射要素重要性。我们使用的数据集由 10 张图片组成,每张 40 人,共 400 张图片。每张图片都有一个标签,表明这个人的身份。在这个任务中,每个像素都是一个特征;在输出中,像素的亮度代表特征的相对重要性。像素越亮,特征越重要。请注意,在这个模型中,最亮的像素在前额区域,我们应该小心如何解释这一点。由于大多数照片都是从头顶上方照射的,这些像素的重要性显然很高,这可能是因为前额往往被更好地照射,因此揭示了关于个人的更多细节,而不是一个人前额的内在属性来表明他们的身份:

import matplotlib.pyplot as plt
from sklearn.datasets import fetch_olivetti_faces
from sklearn.ensemble import ExtraTreesClassifier
data = fetch_olivetti_faces()
def importance(n_estimators=500, max_features=128, n_jobs=3, random_state=0):
 X = data.images.reshape((len(data.images), -1))
 y = data.target
 forest = ExtraTreesClassifier(n_estimators,max_features=max_features, n_jobs=n_jobs, random_state=random_state)
 forest.fit(X, y)
 dstring=" cores=%d..." % n_jobs + " features=%s..." % max_features +"estimators=%d..." %n_estimators + "random=%d" %random_state 
 print(dstring)
 importances = forest.feature_importances_
 importances = importances.reshape(data.images[0].shape)
 plt.matshow(importances, cmap=plt.cm.hot)
 plt.title(dstring)
 #plt.savefig('etreesImportance'+ dstring + '.png')
 plt.show()

importance()

前面代码的输出如下:

Extra trees

助推

在本书的前面,我介绍了 PAC 学习模型的思想和概念类的思想。一个相关的想法是 弱可学性。在这里,集成中的每一个学习算法只需要表现得比机会稍好。例如,如果集合中的每个算法至少有 51%的时间是正确的,则弱可学习性的标准得到满足。事实证明,PAC 和弱可学习性的思想本质上是相同的,只是对于后者,我们放弃了算法必须达到任意高精度的要求。然而,它只是比随机假设表现得更好。你可能会问,这有什么用?通常更容易找到粗略的经验法则而不是高度精确的预测法则。这种弱学习模式的表现可能只是略好于偶然性;然而,如果我们通过在数据的不同加权分布上多次运行来提升这个学习者,并通过组合这些学习者,我们有希望构建一个单一的预测规则,它的性能比任何单个弱学习规则都好得多。

助推是一个简单而有力的想法。它通过考虑模型的训练误差来扩展 bagging。例如,如果我们训练一个线性分类器,发现它错误地分类了某一组实例。如果我们在包含这些错误分类实例的副本的数据集上训练一个后续模型,那么我们会期望这个新训练的模型在测试集上会表现得更好。通过在训练集中包含错误分类实例的副本,我们将数据集的平均值向这些实例转移。这迫使学习者专注于最难分类的例子。在实践中,这是通过赋予错误分类的实例更高的权重,然后修改模型来考虑这一点来实现的,例如,在线性分类器中,我们可以使用加权平均来计算类均值。

从一个总和为 1 的统一权重数据集开始,我们运行分类器,可能会对一些实例进行错误分类。为了增加这些实例的权重,我们给它们分配了总权重的一半。例如,考虑一个给我们以下结果的分类器:

|   |

预测阳性

|

预测负值

|

总数

|
| --- | --- | --- | --- |
| 实际位置。 | Twenty-four | Sixteen | Forty |
| 实际负值。 | nine | Fifty-one | Sixty |
| 总计 | Thirty-three | Sixty-seven | One hundred |

误差率为 ɛ = (9 + 16)/100 = 0.25

我们希望将错误权重的一半分配给错误分类的样本,由于我们从总和为 1 的统一权重开始,因此分配给错误分类示例的当前权重只是错误率。因此,为了更新权重,我们将它们乘以因子 1/2ɛ 。假设错误率小于 0.5,这将导致错误分类示例的权重增加。为了确保权重总和仍然为 1,我们将正确分类的示例乘以 (1-ɛ) 。在这个例子中,错误分类样本的初始权重的错误率是. 25,我们希望它是. 5,也就是总权重的一半,所以我们将这个初始错误率乘以 2。正确分类实例的权重为1/2(1-ɛ= 2/3。将这些权重考虑到下表中:

|   |

预测阳性

|

预测负值

|

总数

|
| --- | --- | --- | --- |
| 实际位置。 | Sixteen | Thirty-two | Forty-eight |
| 实际负值。 | Eighteen | Thirty-four | Sixty |
| 总计 | Thirty-three | Sixty-seven | One hundred |

我们需要的最后一块是一个置信因子 α ,它被应用于集合中的每个模型。这用于基于来自每个单独模型的加权平均值进行集合预测。我们希望随着误差的减小,这个数值会增加。确保这种情况发生的常见方法是将置信因子设置为以下值:

Boosting

因此,我们得到了一个数据集,如下所示:

Boosting

然后,我们初始化一个相等的加权分布,如下所示:

Boosting

使用弱分类器 h t ,我们可以编写一个更新的规则如下:

Boosting

使用归一化因子,如下所示:

Boosting

请注意,exp(-yIht(xI)为正且大于 1,如果-yIht(xI)为正,如果 x i 为错误分类,则会出现这种情况。结果是,更新规则将增加错误分类示例的权重,并降低正确分类样本的权重。

我们可以按如下方式编写最终的分类器:

Boosting

Adaboost

其中最流行的增强算法被称为 AdaBoost自适应增强。这里,决策树分类器被用作基础学习器,并且它在不可线性分离的数据上建立决策边界:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import make_blobs

plot_colors = "br"
plot_step = 0.02
class_names = "AB"
tree= DecisionTreeClassifier()
boost=AdaBoostClassifier()
X,y=make_blobs(n_samples=500,centers=2, random_state=0, cluster_std=2)
boost.fit(X,y)
plt.figure(figsize=(10, 5))

# Plot the decision boundaries
plt.subplot(121)
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, plot_step),
 np.arange(y_min, y_max, plot_step))

Z = boost.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
cs = plt.contourf(xx, yy, Z, cmap=plt.cm.Paired)
plt.axis("tight")

for i, n, c in zip(range(2), class_names, plot_colors):
 idx = np.where(y == i)
 plt.scatter(X[idx, 0], X[idx, 1],
 c=c, cmap=plt.cm.Paired,
 label="Class %s" % n)
plt.title('Decision Boundary')

twoclass_output = boost.decision_function(X)
plot_range = (twoclass_output.min(), twoclass_output.max())
plt.subplot(122)
for i, n, c in zip(range(2), class_names, plot_colors):
 plt.hist(twoclass_output[y == i],
 bins=20,
 range=plot_range,
 facecolor=c,
 label='Class %s' % n,
 alpha=.5)
x1, x2, y1, y2 = plt.axis()
plt.axis((x1, x2, y1, y2))
plt.legend(loc='upper left')
plt.ylabel('Samples')
plt.xlabel('Score')
plt.title('Decision Scores')
plt.show()
print("Mean Accuracy =%f" % boost.score(X,y))

以下是前面命令的输出:

Adaboost

梯度助推

梯度树增强对于回归和分类问题都是非常有用的算法。它的一个主要优势是它自然地处理混合数据类型,并且它对异常值也相当健壮。此外,它比许多其他算法具有更好的预测能力;然而,它的顺序体系结构使它不适合并行技术,因此,它不能很好地扩展到大型数据集。对于类数较多的数据集,建议改用RandomForestClassifier。梯度增强通常使用决策树来建立基于弱学习者集合的预测模型,对代价函数应用优化算法。

在下面的示例中,我们创建了一个构建梯度增强分类器的函数,并绘制了其累积损失与迭代次数的关系图。GradientBoostingClassifier类有一个oob_improvement_属性,用于计算每次迭代的测试损失估计值。与之前的迭代相比,这让我们减少了损失。这对于确定最佳迭代次数是非常有用的启发。这里,我们绘制了两个梯度增强分类器的累积改进。每个分类器都是相同的,但是学习速率不同,虚线为 .01 ,实线为 .001

学习率缩小了每棵树的贡献,这意味着在估计器的数量上有一个折衷。在这里,我们实际上看到,与具有较低学习率的模型相比,具有较大学习率的模型似乎更快地达到其最佳性能。然而,这种模式似乎取得了更好的整体效果。在实践中通常发生的是oob_improvement在大量迭代中以悲观的方式偏离。让我们来看看以下命令:

import numpy as np
import matplotlib.pyplot as plt
from sklearn import ensemble
from sklearn.cross_validation import train_test_split
from sklearn import datasets

def gbt(params, X,y,ls):
 clf = ensemble.GradientBoostingClassifier(**params)
 clf.fit(X_train, y_train)
 cumsum = np.cumsum(clf.oob_improvement_)
 n = np.arange(params['n_estimators'])
 oob_best_iter = n[np.argmax(cumsum)]
 plt.xlabel('Iterations')
 plt.ylabel('Improvement')
 plt.axvline(x=oob_best_iter,linestyle=ls)
 plt.plot(n, cumsum, linestyle=ls)

X,y=datasets.make_blobs(n_samples=50,centers=5, random_state=0, cluster_std=5)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=9)

p1 = {'n_estimators': 1200, 'max_depth': 3, 'subsample': 0.5,
 'learning_rate': 0.01, 'min_samples_leaf': 1, 'random_state': 3}
p2 = {'n_estimators': 1200, 'max_depth': 3, 'subsample': 0.5,
 'learning_rate': 0.001, 'min_samples_leaf': 1, 'random_state': 3}

gbt(p1, X,y, ls='--')
gbt(p2, X,y, ls='-')

您将观察到以下输出:

Gradient boosting

集合策略

我们研究了两种广泛的集成技术:打包,应用于随机森林和额外的树,以及增强,特别是 AdaBoost 和梯度树增强。当然还有许多其他的变体以及它们的组合。在这一章的最后一节,我想研究一些为特定任务选择和应用不同系综的策略。

一般来说,在分类任务中,有三个原因可以解释为什么模型可能会对测试实例进行错误分类。首先,如果来自不同类别的特征由相同的特征向量描述,这可能是不可避免的。在概率模型中,当类分布重叠时会发生这种情况,因此一个实例对于几个类具有非零的可能性。这里我们只能近似一个目标假设。

分类错误的第二个原因是模型不具备充分表达目标假设的表达能力。例如,如果数据不能线性分离,即使最好的线性分类器也会对实例进行错误分类。这是由于分类器的偏差。尽管没有单一的一致同意的方法来测量偏差,我们可以看到非线性决策边界比线性决策边界具有更小的偏差,或者更复杂的决策边界比简单的决策边界具有更小的偏差。我们还可以看到,树模型的偏差最小,因为它们可以继续分支,直到每个叶子只覆盖一个实例。

现在,似乎我们应该尽量减少偏见;然而,在大多数情况下,降低偏差往往会增加方差,反之亦然。正如你可能已经猜到的,方差是分类错误的第三个来源。高方差模型高度依赖于训练数据。例如,最近邻分类器将实例空间分割成单个训练点。如果决策边界附近的训练点被移动,那么该边界将改变。树模型也是高方差的,但原因不同。考虑我们以这样一种方式改变训练数据,即在树根处选择不同的特征。这可能会导致树的其余部分不同。

线性分类器的袋装集成能够通过分段构造来学习更复杂的决策边界。集成中的每个分类器创建一段决策边界。这表明 bagging,实际上任何集成方法,都能够减少高偏差模型的偏差。然而,我们在实践中发现,提升通常是减少偏见的更有效方式。

Bagging 主要是一种方差减少技术,boosting 主要是一种偏差减少技术。

Bagging 集成在高方差模型(如复杂树)中最有效,而 boosting 通常用于高偏差模型(如线性分类器)。

我们可以从利润的角度来看提振。这可以理解为距决策边界的有符号距离;正号表示正确的类别,负号表示错误的类别。可以证明的是,即使样本已经在决策边界的正确一侧,提升也可以增加这一裕度。换句话说,即使训练误差为零,boosting 也可以继续提高测试集的性能。

其他方法

集合方法的主要变化是通过改变基本模型的预测组合方式来实现的。我们实际上可以将这定义为一个学习问题,假设一组基本分类器的预测作为特征来学习一个 元模型,该模型最好地组合了它们的预测。学习线性元模型被称为叠加叠加泛化。堆叠使用所有学习者的加权组合,并且在分类任务中,使用诸如逻辑回归的组合器算法来进行最终预测。不像装袋或助推,也像装桶一样,堆叠通常用于不同类型的模型。

典型的堆叠程序包括以下步骤:

  1. 将训练集分成两个不连贯的集合。
  2. 在第一套基础上训练几个基础学习者。
  3. 在第二组测试基础学习者。
  4. 使用上一步的预测来训练更高水平的学习者。

请注意,前三个步骤与交叉验证相同;然而,基础学习者不是采取赢家通吃的方法,而是被组合在一起,可能是非线性的。

这个主题的一个变体是逆势。这里,选择算法用于为每个问题选择最佳模型。例如,这可以通过给每个模型的预测赋予权重来使用感知来选择最佳模型。由于有大量不同的模型,有些模型比其他模型需要更长的训练时间。在集成中使用这种方法的一种方式是首先使用快速但不精确的算法来选择哪个较慢但更精确的算法可能做得最好。

我们可以使用一组不同的基础学习者来融合多样性。这种多样性来自不同的学习算法,而不是数据。这意味着每个模型可以使用相同的训练集。通常,基本模型由相同类型但具有不同超参数设置的集合组成。

总的来说,集成由一组基本模型和一个元模型组成,它们被训练来找到组合这些基本模型的最佳方式。如果我们使用一组加权模型,并以某种方式组合它们的输出,我们假设如果一个模型的权重接近于零,那么它对输出的影响将非常小。可以想象,基础分类器具有负权重,并且在这种情况下,相对于其他基础模型,它的预测将被反转。我们甚至可以更进一步,甚至在训练基础模型之前,就试图预测它的表现。这有时被称为元学习。这包括,首先,在大量数据集合上训练各种模型,并构建一个模型来帮助我们回答一些问题,例如哪个模型在特定数据集上可能优于另一个模型,或者数据是否表明特定(元)参数可能效果最好?

请记住,当在所有可能问题的空间上进行评估时,没有任何学习算法能够胜过另一个算法,例如,如果所有可能的序列都是可能的,则预测下一个数字是一个序列。当然,现实世界中的学习问题具有非均匀分布,这使得我们能够在这些问题上建立预测模型。元学习的重要问题是如何设计元模型所基于的特征。它们需要结合训练模型和数据集的相关特征。该必须包括除特征数量和类型以及样本数量之外的数据方面。

总结

在这一章中,我们研究了 scikit-learn 中的主要集成方法及其实现。很明显,这里有很大的工作空间,找到最适合不同类型问题的技术是关键的挑战。我们看到,偏差和方差的问题都有各自的解决方案,理解每个问题的关键指标至关重要。获得好的结果通常需要大量的实验,使用本章中描述的一些简单技术,你可以开始你的机器学习集成之旅。

在下一章,也是最后一章,我们将介绍最重要的主题——模型选择和评估,并从不同的角度研究一些现实世界的问题。

九、设计策略和案例研究

除了可能的数据例外,评估可能是机器学习科学家花费大部分时间做的事情。盯着数字和图表的列表,满怀希望地看着他们的模型运行,并认真尝试理解它们的输出。评价是一个循环的过程;我们运行模型,评估结果,并插入新的参数,每次都希望这会带来性能提升。随着我们提高每个评估周期的效率,我们的工作变得更加愉快和高效,有一些工具和技术可以帮助我们实现这一点。本章将通过以下主题介绍其中一些:

  • 评估模型性能
  • 型号选择
  • 真实案例研究。
  • 机器学习设计一目了然

评估模型性能

测量模型的性能是一项重要的机器学习任务,并且有许多不同的参数和启发式方法来完成这项任务。定义评分策略的重要性不可低估,在 Sklearn 中,基本上有三种方法:

  • 评估者评分:这个指的是使用评估者内置的score()方法,具体到每个评估者
  • 评分参数:这是指依靠内部评分策略的交叉验证工具
  • 度量功能:这些在度量模块中实现

我们已经看到了估计量score()方法的例子,例如clf.score()。在线性分类器的情况下,score()方法返回平均精度。这是一种快速简单的方法来衡量个人评估者的表现。然而,由于多种原因,这种方法本身通常是不够的。

如果我们记得的话,准确率就是真阳性和真阴性的情况之和除以样本数。以此作为衡量标准表明,如果我们对一些患者进行测试,看看他们是否患有某种疾病,简单地预测每个患者都没有疾病可能会给我们带来很高的准确性。显然,这不是我们想要的。

衡量绩效的更好方法是使用精度、( P )和召回、( R )。如果从记起第 4 章模型–从信息中学习中的表格,精度,或者说特异性,就是预测阳性实例正确的比例,即 TP/(TP+FP) 。回忆,或者说敏感,就是 TP/(TP+FN) 。F-测度定义为 2RP/(R+P) 。这些措施忽略了真实的阴性率,所以他们没有对一个模型处理阴性病例的效果进行评估。

与其使用估计器的评分方法,不如使用特定的评分参数,如cross_val_score对象提供的参数。这有一个cv参数,控制数据如何分割。它通常被设置为 int,它决定了对数据进行多少次随机连续拆分。每个都有不同的分割点。此参数也可以设置为训练和测试拆分的可选项,或者可以用作交叉验证生成器的对象。

cross_val_score中同样重要的是评分参数。这通常由表示评分策略的字符串来设置。分类时默认为精度,一些常用值为f1precisionrecall,以及这些值的微平均、宏平均和加权版本。对于回归估计量,scoring值为mean_absolute_errormean_squared errormedian_absolute_errorr2

下面的代码使用 10 个连续的拆分来估计数据集上三个模型的性能。在这里,我们打印出了每个分数的平均值,使用了四个模型中每一个的几个度量。在现实世界中,我们可能需要以一种或多种方式对数据进行预处理,将这些数据转换应用于我们的测试集和训练集非常重要。为了使这变得更容易,我们可以使用sklearn.pipeline模块。这依次应用了一系列变换和一个最终估计器,它允许我们将几个可以交叉验证的步骤组合在一起。这里,我们还使用StandardScaler()类来缩放数据。通过使用两条管道将缩放应用于逻辑回归模型和决策树:

from sklearn import cross_validation
from sklearn.tree import DecisionTreeClassifier
from sklearn import svm
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import samples_generator
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import StandardScaler
from sklearn.cross_validation import cross_val_score
from sklearn.pipeline import Pipeline
X, y = samples_generator.make_classification(n_samples=1000,n_informative=5, n_redundant=0,random_state=42)
le=LabelEncoder()
y=le.fit_transform(y)
Xtrain, Xtest, ytrain, ytest = cross_validation.train_test_split(X, y, test_size=0.5, random_state=1)
clf1=DecisionTreeClassifier(max_depth=2,criterion='gini').fit(Xtrain,ytrain)
clf2= svm.SVC(kernel='linear', probability=True, random_state=0).fit(Xtrain,ytrain)
clf3=LogisticRegression(penalty='l2', C=0.001).fit(Xtrain,ytrain)
pipe1=Pipeline([['sc',StandardScaler()],['mod',clf1]])
mod_labels=['Decision Tree','SVM','Logistic Regression' ]
print('10 fold cross validation: \n')
for mod,label in zip([pipe1,clf2,clf3], mod_labels):
 #print(label)
 auc_scores= cross_val_score(estimator= mod, X=Xtrain, y=ytrain, cv=10, scoring ='roc_auc')
 p_scores= cross_val_score(estimator= mod, X=Xtrain, y=ytrain, cv=10, scoring ='precision_macro')
 r_scores= cross_val_score(estimator= mod, X=Xtrain, y=ytrain, cv=10, scoring ='recall_macro')
 f_scores= cross_val_score(estimator= mod, X=Xtrain, y=ytrain, cv=10, scoring ='f1_macro')

 print(label)
 print("auc scores %2f +/- %2f " % (auc_scores.mean(), auc_scores.std()))
 print("precision %2f +/- %2f " % (p_scores.mean(), p_scores.std()))
 print("recall %2f +/- %2f ]" % (r_scores.mean(), r_scores.std()))
 print("f scores %2f +/- %2f " % (f_scores.mean(), f_scores.std()))

执行时,您将看到以下输出:

Evaluating model performance

这些技术有几种变体,最常用的是所谓的 k 倍交叉验证。这使用了有时被称为的“留一个出去”的策略。首先,使用折叠的 k —1 作为训练数据来训练模型。剩余的数据用于计算性能度量。对每个折叠重复这一过程。性能计算为所有折叠的平均值。

Sklearn 使用cross_validation.KFold对象实现了这一点。重要参数是一个必需的int,表示元素的总数,以及一个n_folds参数,默认为3,表示折叠的数量。它还带有可选的shufflerandom_state参数,指示在拆分前是否对数据进行洗牌,以及使用什么方法生成随机状态。默认的random_state参数是使用 NumPy 随机数发生器。

在下面的代码片段中,我们使用了LassoCV对象。这是用 L1 正则化训练的线性模型。如果您还记得,正则化线性回归的优化函数包括一个常数(α),它乘以 L1 正则化项。LassoCV对象会自动设置该 alpha 值,为了了解其有效性,我们可以将所选 alpha 与每个 k 折叠上的分数进行比较:

import numpy as np
from sklearn import cross_validation, datasets, linear_model
X,y=datasets.make_blobs(n_samples=80,centers=2, random_state=0, cluster_std=2)
alphas = np.logspace(-4, -.5, 30)
lasso_cv = linear_model.LassoCV(alphas=alphas)
k_fold = cross_validation.KFold(len(X), 5)
alphas = np.logspace(-4, -.5, 30)

for k, (train, test) in enumerate(k_fold):
 lasso_cv.fit(X[train], y[train])
 print("[fold {0}] alpha: {1:.5f}, score: {2:.5f}".
 format(k, lasso_cv.alpha_, lasso_cv.score(X[test], y[test])))

上述命令的输出如下:

Evaluating model performance

有时,需要保留每个文件夹中类别的百分比。这是使用分层交叉验证完成的。当班级不平衡时,也就是说,当一些班级人数较多而其他班级人数很少时,这可能会有所帮助。使用分层cv对象可能有助于纠正模型中可能导致偏差的缺陷,因为一个类没有以足够大的数量表示在一个文件夹中,无法进行准确的预测。然而,这也可能导致不必要的方差增加。

在下面的例子中,我们使用分层交叉验证来测试分类分数的重要性。这是通过在随机化标签后重复分类程序来完成的。 p 值是得分大于最初获得的分类得分的运行百分比。这段代码片段使用cross_validation.permutation_test_score方法,该方法将估计值、数据和标签作为参数。这里,我们打印出初始测试分数、 p 值以及每个排列的分数:

import numpy as np
from sklearn import linear_model
from sklearn.cross_validation import StratifiedKFold, permutation_test_score
from sklearn import datasets

X,y=datasets.make_classification(n_samples=100, n_features=5)
n_classes = np.unique(y).size
cls=linear_model.LogisticRegression()
cv = StratifiedKFold(y, 2)
score, permutation_scores, pvalue = permutation_test_score(cls, X, y, scoring="f1", cv=cv, n_permutations=10, n_jobs=1)

print("Classification score %s (pvalue : %s)" % (score, pvalue))
print("Permutation scores %s" % (permutation_scores))

这给出了以下输出:

Evaluating model performance

车型选择

有许多超参数可以调整以提高性能。这通常不是一个简单的过程,要确定各个参数的效果,包括单独的和相互结合的。常见的尝试包括获取更多的训练示例、添加或移除特征、添加多项式特征以及增加或减少正则化参数。考虑到我们可以花费大量时间收集更多数据,或者以其他方式操纵数据,重要的是您花费的时间很可能会产生富有成效的结果。最重要的方法之一是使用网格搜索。

网格搜索

sklearn.grid_search.GridSearchCV对象用于对指定的参数值进行详尽的搜索。这允许通过定义的参数集进行迭代,并以各种度量的形式报告结果。GridSearchCV对象的重要参数是估计量和参数网格。param_grid参数是一个字典,或字典列表,参数名称作为键,参数设置列表作为值。这使得能够搜索任意序列的估计器参数值。任何估计器的可调参数都可以用于网格搜索。默认情况下,网格搜索使用估计器的score()功能来评估参数值。对于分类来说,这是准确性,正如我们所看到的,这可能不是最好的衡量标准。在本例中,我们将GridSearchCV对象的评分参数设置为f1

在下面的代码中,我们在 L1 和 L2 正则化下,在一系列C值(逆正则化参数)上执行搜索。我们使用metrics.classification_report类打印出详细的分类报告:

from sklearn import datasets
from sklearn.cross_validation import train_test_split
from sklearn.grid_search import GridSearchCV
from sklearn.metrics import classification_report
from sklearn.linear_model import LogisticRegression as lr

X,y=datasets.make_blobs(n_samples=800,centers=2, random_state=0, cluster_std=4)
X_train, X_test, y_train, y_test = train_test_split(
 X, y, test_size=0.5, random_state=0)
tuned_parameters = [{'penalty': ['l1'], 
 'C': [0.01, 0.1, 1, 5]},
 {'penalty': ['l2'], 'C': [0.01, 0.1, 1, 5]}]
scores = ['precision', 'recall','f1']
for score in scores:
 clf = GridSearchCV(lr(C=1), tuned_parameters, cv=5,
 scoring='%s_weighted' % score)
 clf.fit(X_train, y_train)
 print("Best parameters on development set:")
 print()
 print(clf.best_params_)
 print("Grid scores on development set:")
 for params, mean_score, scores in clf.grid_scores_:
 print("%0.3f (+/-%0.03f) for %r"
 % (mean_score, scores.std() * 2, params))
 print("classification report:")
 y_true, y_pred = y_test, clf.predict(X_test)
 print(classification_report(y_true, y_pred))

我们观察到以下输出:

Gridsearch

网格搜索可能是优化超参数最常用的方法,但是,有时它可能不是最佳选择。RandomizedSearchCV对象实现了对可能参数的随机搜索。它使用类似于GridSearchCV对象的字典,然而,对于每个参数,可以设置一个分布,在该分布上将进行值的随机搜索。如果字典包含一个值列表,那么这些值将被统一采样。此外,RandomizedSearchCV对象还包含一个n_iter参数,该参数实际上是采样参数设置数量的计算预算。它默认为 10,在高值时,通常会给出更好的结果。然而,这是以运行时为代价的。

网格搜索的蛮力方法还有其他选择,这些在估算器中提供,如LassoCVElasticNetCV。这里,估计器本身通过沿着正则化路径拟合来优化其正则化参数。这通常比使用网格搜索更有效。

学习曲线

了解模型运行情况的一个重要方法是使用学习曲线。考虑当我们增加样本数量时训练和测试错误会发生什么。考虑一个简单的线性模型。训练样本少,很容易拟合参数,训练误差小。随着训练集的增长,它变得更难适应,平均训练误差可能会增加。另一方面,随着样本的增加,交叉验证误差可能会减小,至少在开始时是这样。有了更多的样本进行训练,模型将能够更好地适应新的样本。考虑具有高偏差的模型,例如,具有两个参数的简单线性分类器。这只是一条直线,因此当我们开始添加训练示例时,交叉验证错误最初会减少。然而,在某一点之后,仅仅因为一条直线的限制,增加训练样本并不会显著降低误差,它根本无法拟合非线性数据。如果我们观察训练误差,我们会发现,像前面一样,它最初随着更多的训练样本而增加,并且在某个点,它将近似等于交叉验证误差。在高偏差的例子中,交叉验证和训练误差都很高。这表明,如果我们知道我们的学习算法有很高的偏差,那么仅仅增加更多的训练例子是不太可能显著改善模型的。

现在,考虑一个具有高方差的模型,比如说具有大量多项式项,并且正则化参数的值很小。随着我们增加更多的样本,训练误差将缓慢增加,但保持相对较小。随着更多训练样本的加入,交叉验证集的误差将会减小。这是过度装配的迹象。具有高方差的模型的指示特征是训练误差和测试误差之间的巨大差异。这表明,增加训练示例将降低交叉验证误差,因此,添加训练样本是改进高方差模型的可能方式。

在下面的代码中,随着样本量的增加,我们使用学习曲线对象来绘制测试误差和训练误差。当一个特定的模型正遭受高偏差或高方差时,这应该给你一个指示。在这种情况下,我们使用的是逻辑回归模型。从这段代码的输出中我们可以看出,模型可能存在偏差,因为两个训练测试的误差都相对较高:

from sklearn.pipeline import Pipeline
from sklearn.learning_curve import learning_curve
import matplotlib.pyplot as plt
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn import cross_validation
from sklearn import datasets

X, y = datasets.make_classification(n_samples=2000,n_informative=2, n_redundant=0,random_state=42)
Xtrain, Xtest, ytrain, ytest = cross_validation.train_test_split(X, y, test_size=0.5, random_state=1)
pipe = Pipeline ([('sc' , StandardScaler()),('clf', LogisticRegression( penalty = 'l2'))])
trainSizes, trainScores, testScores = learning_curve(estimator=pipe, X=Xtrain, y= ytrain,train_sizes=np.linspace(0.1,1,10),cv=10, n_jobs=1)
trainMeanErr=1-np.mean(trainScores, axis=1)
testMeanErr=1-np.mean(testScores, axis=1)
plt.plot(trainSizes, trainMeanErr, color='red', marker='o', markersize=5, label = 'training error') 
plt.plot(trainSizes, testMeanErr, color='green', marker='s', markersize=5, label = 'test error')
plt.grid()
plt.xlabel('Number of Training Samples')
plt.ylabel('Error')
plt.legend(loc=0)
plt.show()

以下是前面代码的输出:

Learning curves

真实世界案例研究

现在,我们将继续讨论一些真实世界的机器学习场景。首先,我们将建立一个推荐系统,然后我们将研究温室中的一些集成害虫管理系统。

建立推荐系统

推荐系统是信息过滤的一种,一般有两种方式:基于内容的过滤协同过滤。在基于内容的过滤中,系统试图模拟用户的长期兴趣,并基于此选择项目。另一方面,协同过滤基于与具有相似偏好的人选择的项目的相关性来选择项目。正如您所料,许多系统使用这两种方法的混合。

基于内容的过滤

基于内容的过滤使用项目的内容,它被表示为一组描述符术语,将它们与用户简档进行匹配。使用从用户先前查看过的项目中提取的相同术语来构建用户简档。一个典型的在线书店将从文本中提取关键词来创建用户档案并提出建议。尽管在需要特定领域知识的情况下,可能需要手动添加这些术语,但是在许多情况下,提取这些术语的过程可以是自动的。在处理非基于文本的项目时,术语的手动添加尤其重要。比如说,通过将护舷放大器与电吉他联系起来,从图书馆中提取关键词相对容易。在许多情况下,这将涉及到人类基于特定的领域知识来创建这些关联,例如通过将挡泥板放大器与电吉他相关联。一旦构建完成,我们需要选择一种能够学习用户配置文件并做出适当推荐的学习算法。最常用的两个模型是向量空间模型和潜在语义索引模型。使用向量空间模型,我们创建了一个表示文档的稀疏向量,其中文档中的每个不同术语对应于向量的一个维度。权重用于指示术语是否出现在文档中。当它出现时,它显示 1 的重量,当它不出现时,它显示 0 的重量。基于单词出现次数的权重也被使用。

替代模型,潜在语义索引,可以在几个方面改进向量模型。考虑这样一个事实,相同的概念经常被许多不同的词描述,也就是说,用同义词。例如,我们需要知道,在大多数情况下,计算机显示器和计算机屏幕是一回事。此外,考虑到许多单词有不止一个不同的含义,例如,单词鼠标可以是动物或计算机界面。语义索引通过建立一个术语文档矩阵来整合这些信息。每个条目代表一个特定术语在文件中出现的次数。一组文档中的每个术语有一行,每个文档有一列。通过被称为单值分解的数学过程,这个单个矩阵可以被分解成三个矩阵,这三个矩阵将文档和术语表示为因子值的向量。本质上,这是一种降维技术,我们通过它创建代表多个单词的单个特征。基于这些派生特征提出了一个建议。该建议基于文档中的语义关系,而不是简单地匹配相同的单词。这种技术的缺点是计算量大,运行速度慢。对于一个必须实时工作的推荐系统来说,这可能是一个很大的限制。

协同过滤

协同过滤采用不同的方法,用于多种设置,尤其是在社交媒体的环境中,有多种实现方式。大多数采取街区的方式。这是基于这样一种想法,即你更有可能相信你朋友的推荐,或者那些有相似兴趣的人,而不是那些你不太熟悉的人。

在这种方法中,使用其他人推荐的加权平均值。权重由个体之间的相关性决定。也就是说,偏好相似的人将比不太相似的人获得更高的权重。在拥有成千上万用户的大型系统中,在运行时计算所有权重变得不可行。取而代之的是使用邻域的推荐。通过使用特定的权重阈值或者通过基于最高相关性的选择来选择该邻域。

在下面的代码中,我们使用了用户字典和他们对音乐专辑的评价。当我们绘制用户对两张专辑的评分时,这种模型的几何性质最为明显。很容易看出,用户在剧情上的距离很好地表明了他们的评分有多相似。欧几里得距离衡量用户之间的距离,即他们的偏好匹配程度。我们还需要一种方法来考虑两个人之间的联系,为此我们使用皮尔逊相关指数。一旦我们能够计算出用户之间的相似度,我们就按照相似度的顺序对他们进行排名。从这里,我们可以计算出哪些专辑可以推荐。这是通过将每个用户的相似性分数乘以他们的评分来实现的。然后将其相加并除以相似性得分,本质上是基于相似性得分计算加权平均值。

另一种方法是找到项目之间的相似性。这叫基于项目的协同过滤;这与我们用来计算相似度得分的基于用户的协同过滤形成对比。基于项目的方法是为每个项目找到相似的项目。一旦我们有了所有专辑之间的相似之处,我们就可以为特定用户生成推荐。

让我们看一下示例代码实现:

import pandas as pd 
from scipy.stats import pearsonr
import matplotlib.pyplot as plt

userRatings={'Dave': {'Dark Side of Moon': 9.0,
 'Hard Road': 6.5,'Symphony 5': 8.0,'Blood Cells': 4.0},'Jen': {'Hard Road': 7.0,'Symphony 5': 4.5,'Abbey Road':8.5,'Ziggy Stardust': 9,'Best Of Miles':7},'Roy': {'Dark Side of Moon': 7.0,'Hard Road': 3.5,'Blood Cells': 4,'Vitalogy': 6.0,'Ziggy Stardust': 8,'Legend': 7.0,'Abbey Road': 4},'Rob': {'Mass in B minor': 10,'Symphony 5': 9.5,'Blood Cells': 3.5,'Ziggy Stardust': 8,'Black Star': 9.5,'Abbey Road': 7.5},'Sam': {'Hard Road': 8.5,'Vitalogy': 5.0,'Legend': 8.0,'Ziggy Stardust': 9.5,'U2 Live': 7.5,'Legend': 9.0,'Abbey Road': 2},'Tom': {'Symphony 5': 4,'U2 Live': 7.5,'Vitalogy': 7.0,'Abbey Road': 4.5},'Kate': {'Horses': 8.0,'Symphony 5': 6.5,'Ziggy Stardust': 8.5,'Hard Road': 6.0,'Legend': 8.0,'Blood Cells': 9,'Abbey Road': 6}}

# Returns a distance-based similarity score for user1 and user2
def distance(prefs,user1,user2):
 # Get the list of shared_items
 si={}
 for item in prefs[user1]:
 if item in prefs[user2]:
 si[item]=1
 # if they have no ratings in common, return 0
 if len(si)==0: return 0
 # Add up the squares of all the differences
 sum_of_squares=sum([pow(prefs[user1][item]-prefs[user2][item],2)
 for item in prefs[user1] if item in prefs[user2]])
 return 1/(1+sum_of_squares)

def Matches(prefs,person,n=5,similarity=pearsonr):
 scores=[(similarity(prefs,person,other),other)
 for other in prefs if other!=person]
 scores.sort( )
 scores.reverse( )
 return scores[0:n]

def getRecommendations(prefs,person,similarity=pearsonr):
 totals={}
 simSums={}
 for other in prefs:
 if other==person: continue
 sim=similarity(prefs,person,other)
 if sim<=0: continue
 for item in prefs[other]:
 # only score albums not yet rated
 if item not in prefs[person] or prefs[person][item]==0:
 # Similarity * Score
 totals.setdefault(item,0)
 totals[item]+=prefs[other][item]*sim
 # Sum of similarities
 simSums.setdefault(item,0)
 simSums[item]+=sim
 # Create a normalized list
 rankings=[(total/simSums[item],item) for item,total in totals.items( )]
 # Return a sorted list
 rankings.sort( )
 rankings.reverse( )
 return rankings

def transformPrefs(prefs):
 result={}
 for person in prefs:
 for item in prefs[person]:
 result.setdefault(item,{})
 # Flip item and person
 result[item][person]=prefs[person][item]
 return result

transformPrefs(userRatings)

def calculateSimilarItems(prefs,n=10):
 # Create a dictionary similar items
 result={}
 # Invert the preference matrix to be item-centric
 itemPrefs=transformPrefs(prefs)
 for item in itemPrefs:
#        if c%100==0: print("%d / %d" % (c,len(itemPrefs)))
 scores=Matches(itemPrefs,item,n=n,similarity=distance)
 result[item]=scores
 return result

def getRecommendedItems(prefs,itemMatch,user):
 userRatings=prefs[user]
 scores={}
 totalSim={}

 # Loop over items rated by this user
 for (item,rating) in userRatings.items( ):

 # Loop over items similar to this one
 for (similarity,item2) in itemMatch[item]:

 # Ignore if this user has already rated this item
 if item2 in userRatings: continue

 # Weighted sum of rating times similarity
 scores.setdefault(item2,0)
 scores[item2]+=similarity*rating

 # Sum of all the similarities
 totalSim.setdefault(item2,0)
 totalSim[item2]+=similarity

 # Divide each total score by total weighting to get an average
 rankings=[(score/totalSim[item],item) for item,score in scores.items( )]

 # Return the rankings from highest to lowest
 rankings.sort( )
 rankings.reverse( )
 return rankings

itemsim=calculateSimilarItems(userRatings)

def plotDistance(album1, album2):
 data=[]
 for i in userRatings.keys():
 try:
 data.append((i,userRatings[i][album1], userRatings[i][album2]))
 except:
 pass
 df=pd.DataFrame(data=data, columns = ['user', album1, album2])
 plt.scatter(df[album1],df[album2])
 plt.xlabel(album1)
 plt.ylabel(album2)
 for i,t in enumerate(df.user):
 plt.annotate(t,(df[album1][i], df[album2][i]))
 plt.show()
 print(df)

plotDistance('Abbey Road', 'Ziggy Stardust')
print(getRecommendedItems(userRatings, itemsim,'Dave'))

您将看到以下输出:

Collaborative filtering

这里我们已经绘制了两张专辑的用户评分,基于此,我们可以看到用户凯特罗布是比较接近的,也就是他们对于这两张专辑的喜好是相似的。另一方面,用户 RobSam 相距甚远,说明对这两张专辑的喜好不同。我们还为用户打印出推荐戴夫以及每张推荐专辑的相似度评分。

由于协同过滤依赖于其他用户的评分,当文档的数量变得比评分的数量大得多时,就会出现问题,因此用户评分的项目数量在所有项目中只占很小的比例。有几种不同的方法可以帮助您解决这个问题。评级可以从他们在网站上浏览的项目类型中推断出来。另一种方法是以混合方式用基于内容的过滤来补充用户的评分。

回顾案例研究

本案例研究的一些重要方面如下:

  • 它是网络应用的一部分。它必须实时运行,并且依赖于用户交互。
  • 有广泛的实践和理论资源可用。这是一个经过深思熟虑的问题,有几个明确的解决方案。我们不必重新发明轮子。
  • 这主要是一个营销项目。它有一个基于推荐的销量成功的量化指标。
  • 失败的代价相对较低。少量的误差是可以接受的。

温室昆虫检测

不断增长的人口和不断增加的气候变化给 21 世纪的农业带来了独特的挑战。温室等受控环境提供最佳生长条件和最大限度地有效利用水和养分等投入的能力,将使我们能够在不断变化的全球气候中继续养活不断增长的人口。

现在有许多食品生产系统基本上是自动化的,这些系统可能相当复杂。水产养殖系统可以在鱼缸和生长架之间循环营养和水,本质上是在人工环境中创造一个非常简单的生态。水的营养成分是受控制的,温度、湿度和二氧化碳含量也是如此。这些特征存在于非常精确的范围内,以优化生产。

温室内的环境条件可能非常有利于疾病和害虫的快速传播。早期发现和检测前体症状,如真菌或昆虫产卵,对控制这些疾病和害虫至关重要。出于环境、食品质量和经济方面的原因,我们希望只应用最低限度的目标控制,因为这主要涉及应用、杀虫剂或任何其他生物制剂。

这里的目标是创建一个自动系统,该系统将检测疾病或昆虫的类型和位置,然后选择并理想地实施控制。这是一项相当大的事业,有许多不同的组成部分。许多技术是分开存在的,但是在这里我们以许多非标准的方式将它们结合起来。该方法主要是实验性的:

Insect detection in greenhouses

通常的检测方法是直接人类观察。这是一项非常耗时的任务,需要一些特殊的技能。它也非常容易出错。自动化这本身将有巨大的好处,也是创建自动化 IPM 系统的重要起点。首要任务之一是为每个目标确定一套指标。一种自然的方法是让专家或专家小组将短视频剪辑分类为无害虫或感染了一种或多种目标物种。接下来,在这些片段上训练分类器,希望它能够获得预测。这种方法在过去已经被使用,例如,温室中的早期害虫检测(马丁,莫伊桑,2004),用于检测害虫。

在典型的设置中,摄像机被放置在整个温室中,以最大化采样面积。为了早期发现害虫,像茎、叶节和其他区域这样的关键植物器官是有针对性的。由于视频和图像分析的计算成本很高,因此可以使用对运动敏感的摄像机,这些摄像机被智能编程,在检测到昆虫运动时开始记录。

早期暴发中的变化相当微妙,可以被指示为植物损伤、变色、生长减少和昆虫或其卵的存在的组合。温室中多变的光照条件加剧了这一困难。处理这些问题的一种方法是使用认知视觉方法。这将问题分成许多子问题,每个子问题都依赖于上下文。例如,当天气晴朗时,或者根据一天中不同时间的光照条件,使用不同的模型。这方面的知识可以在初步的弱学习阶段构建到模型中。这给了它一个内在的启发,在给定的环境中应用适当的学习算法。

一个重要的要求是我们要区分不同的昆虫种类,一种方法是通过捕捉昆虫的动态成分,也就是它们的行为。许多昆虫可以通过它们的运动类型来区分,例如,在紧密的圆圈中飞行,或者在短时间内静止不动。此外,昆虫可能有其他行为,如交配或产卵,这可能是需要控制的重要指标。

监控可以通过多种渠道进行,最著名的是视频和静态摄影,以及使用来自其他传感器的信号,如红外、温度和湿度传感器。所有这些输入都需要打上时间和位置标记,以便在机器学习模型中有意义地使用。

视频处理首先涉及减去背景并分离序列的运动分量。在像素级,照明条件导致强度、饱和度和像素间对比度的变化。在图像级别,阴影等条件只影响图像的一部分,而背光会影响整个图像。

在本例中,我们从视频记录中提取帧,并在系统中的单独路径中处理它们。与视频处理相反,在视频处理中,我们对一段时间内的帧序列感兴趣,以便检测运动,而在视频处理中,我们对来自几个摄像机的单个帧感兴趣,这些帧同时聚焦在同一位置。这样,我们可以建立一个三维模型,这可能是有用的,特别是对于跟踪生物量的变化。

我们的机器学习模型的最终输入是环境传感器。标准控制系统测量温度、相对湿度、二氧化碳水平和光线。此外,高光谱和多光谱传感器能够检测可见光谱之外的频率。这些信号的性质需要它们自己独特的处理路径。作为如何使用它们的一个例子,考虑我们的目标之一是一种我们知道存在于狭窄湿度和温度范围内的真菌。假设温室的一部分中的紫外线传感器短暂地检测到指示真菌的频率范围。我们的模型会记录这一点,如果湿度和温度在这个范围内,那么可以启动控制。这种控制可能只是在可能爆发的地方打开通风口或打开风扇,将该地区局部冷却到真菌无法生存的温度。

显然,系统最复杂的部分是动作控制器。这实际上包括两个元素:输出表示目标害虫存在与否的二进制向量的多标签分类器和输出控制策略的动作分类器本身。

检测各种病原体和害虫需要许多不同的组件和许多不同的系统。标准的方法是为每个目标创建一个单独的学习模型。如果我们把每一个都作为独立的、不相关的活动来进行控制,那么这种多模型的方法是有效的。然而,许多过程,如疾病的发展和传播以及昆虫的突然爆发,可能是由一个共同的原因引起的。

回顾案例研究

本案例研究的一些重要方面如下:

  • 这主要是一个研究项目。它有一个很长的时间表,涉及大量的未知。
  • 它包括许多相互关联的系统。每一个都可以单独工作,但是在某些时候需要集成回整个系统。
  • 它需要大量的领域知识。

机器学习一目了然

物理设计过程(涉及人类、决策、约束,最重要的是:不可预测性)与我们正在构建的机器学习系统有相似之处。分类器的决策边界、数据约束以及使用随机性来初始化或在模型中引入多样性只是我们可以建立的三个连接。更深层次的问题是,我们能把这个类比理解到什么程度。如果我们试图构建人工智能,问题是,“我们是在试图复制人类智能的过程,还是简单地模仿其后果,即做出合理的决策?”这当然是激烈的哲学讨论的时机已经成熟,虽然有趣,但与目前的讨论基本无关。然而,重要的一点是,通过观察自然系统,如大脑,并试图模仿它们的行为,可以学到很多东西。

真正的人类决策发生在更广泛的复杂大脑活动背景下,在设计过程的设定中,我们做出的决策往往是群体决策。与人工神经网络集成的类比是不可抗拒的。就像一群学习能力很弱的候选人一样,在一个项目的整个生命周期中,所做的决定最终会产生比任何个人贡献都大得多的结果。重要的是,一个不正确的决定,类似于决策树中的不良分裂,不会浪费时间,因为弱学习者的部分作用是排除不正确的可能性。在一个复杂的机器学习项目中,意识到许多工作没有直接导致成功的结果可能会令人沮丧。最初的重点应该是提供令人信服的论据,证明积极的结果是可能的。

当然,机器学习系统和设计过程本身之间的类比过于简单。团队动力学中有许多东西不是用机器学习集成来表示的。例如,人类的决策发生在相当虚幻的情感、直觉和一生经历的背景下。此外,团队动态往往是由人员野心、微妙的偏见以及团队成员之间的关系所塑造的。重要的是,管理团队必须融入设计过程。

任何规模的机器学习项目都需要协作。空间太大了,任何一个人都不可能完全认识到所有不同的相互关联的因素。如果不是许多人努力发展理论、编写基本算法、收集和组织数据,即使是本书中概述的简单演示任务也是不可能的。

在时间和资源限制内成功地组织一个主要项目需要大量的技能,而这些不一定是软件工程师或数据科学家的技能。显然,我们必须定义在任何给定的背景下,成功意味着什么。一个理论研究项目,要么以一定程度的确定性,要么以较小程度的不确定性来否定或证明一个特定的理论,被认为是成功的。理解这些限制可能会给我们现实的期望,换句话说,一个可实现的成功标准。

最常见和最持久的限制之一是数据不足或不准确。数据收集方法是如此重要的一个方面,然而在许多项目中却被忽视了。数据收集过程是交互式的。不改变系统,就不可能询问任何动态系统。此外,系统的某些组件比其他组件更容易观察,因此,可能会成为更广泛的未观察或不可观察组件的不准确表示。在许多情况下,我们对复杂系统的了解与我们不了解的相比相形见绌。这种不确定性嵌入在物理现实的随机本质中,也是我们在任何预测任务中必须诉诸概率的原因。对于给定的行动,决定什么样的概率水平是可接受的,比如根据疾病的估计概率来治疗潜在的患者,这取决于治疗疾病与否的后果,而这通常取决于人,无论是医生还是患者,来做出最终的决定。域外有许多问题可能会影响这样的决定。

人类问题的解决,虽然有很多相似之处,但却是与机器问题解决的根本区别。它依赖于很多东西,尤其是情绪和身体状态,也就是神经系统被包裹在其中的化学和电浴。人类的思维不是一个确定性的过程,这其实是一件好事,因为它使我们能够以新颖的方式解决问题。创造性问题解决包括将不同的想法或概念联系起来的能力。通常,这个灵感来自一个完全不相关的事件,众所周知的牛顿的苹果。人类大脑将每天经历的这些经常是随机的事件编织成某种连贯的、有意义的结构的能力,是我们渴望在机器中建立的虚幻能力。

总结

毫无疑问,在机器学习中最难做的事情是将其应用于独特的、以前未解决的问题。我们已经试验了许多示例模型,并使用了一些最流行的机器学习算法。现在的挑战是将这些知识应用到你关心的重要新问题上。我希望这本书以某种方式向您介绍了使用 Python 进行机器学习的可能性。

posted @ 2025-09-03 10:19  绝不原创的飞龙  阅读(47)  评论(0)    收藏  举报