Python-高级机器学习-全-

Python 高级机器学习(全)

原文:Advanced Machine Learning with Python

协议:CC BY-NC-SA 4.0

零、前言

你好!欢迎阅读本使用 Python 的高级机器学习指南。有可能你一开始对这个感兴趣,但不太确定会发生什么。简而言之,学习和使用机器学习技术从未有过如此激动人心的时刻,在这个领域工作只会获得更多的回报。如果你想跟上一些更先进的数据建模技术,并获得使用它们解决挑战性问题的经验,这是一本适合你的好书!

什么是高级机器学习?

计算能力的不断进步(根据摩尔定律)已经开始使机器学习,曾经主要是一门研究学科,在商业环境中更加可行。这导致了新应用和新的或重新发现的技术的爆发,将数据科学、人工智能和机器学习的模糊概念投射到国际公司的公众意识和战略规划中。

机器学习应用程序的快速发展是由一系列研究实验室正在进行的不断创新的斗争所推动的。这些先驱开发的技术正在播种新的应用领域,并经历着越来越多的公众意识。虽然在人工智能和应用机器学习中寻求的一些创新还远未准备就绪,但其他创新已经成为现实。自动驾驶汽车、复杂的图像识别和改变能力、遗传学研究的更大进步,也许最普遍的是,我们的数字商店、电子邮件收件箱和在线生活中量身定制的内容越来越多。

随着所有这些可能性以及更多的可能性掌握在忠诚的数据科学家手中,这个行业正在看到一个飞速的(如果笨拙的话)增长。现在不仅数据科学家和人工智能从业者远多于两年前(2014 年初),而且机器学习研究高端解决方案的可访问性和开放性也有所提高。

谷歌和脸书的研究团队开始越来越多地分享他们的架构、语言、模型和工具,希望看到它们被不断增长的数据科学家群体所应用和改进。

随着流行算法的定义或重新发现,机器学习社区足够成熟,开始看到趋势。更准确地说,来自一个主要研究社区的现有趋势开始受到工业界的极大关注,其中一个产品是一群横跨工业界和学术界的机器学习专家。另一个产品,这一部分的主题,是对先进算法的日益增长的认识,这些算法可以用来破解当今的前沿问题。一个月又一个月,我们看到新的进步,分数上升,前沿越走越远。

所有这一切意味着,可能没有比现在更好的时机进入数据科学领域,开发你的机器学习技能了。介绍性算法(包括聚类、回归模型和神经网络架构)和工具在网络课程和博客内容中被广泛覆盖。虽然数据科学前沿的技术(包括深度学习、半监督算法和集成)仍然不太容易获得,但这些技术本身现在可以通过多种语言的软件库获得。所有需要的是理论知识和实践指导的结合,以正确实施模型。这就是写这本书的目的。

你应该对这本书有什么期待?

你已经开始阅读一本书,该书侧重于教授近年来出现的一些高级建模技术。这本书面向任何想了解这些算法的人,无论你是经验丰富的数据科学家还是希望将现有技能运用到新环境中的开发人员。

我的目标首先是确保你理解正在讨论的算法。其中一些相当棘手,并与统计学和机器学习中的其他概念联系在一起。

对于新手读者,我明确建议收集对关键概念的初步理解,包括以下内容:

  • 神经网络体系结构,包括 MLP 体系结构
  • 学习方法组件包括梯度下降和反向传播
  • 网络性能测量,例如,均方根误差
  • k-均值聚类

有时,这本书无法给予一个主题应有的关注。我们在这本书里涉及了很多内容,因此速度相当快!在每一章的最后,我建议你进一步阅读,在一本书或在线文章中,这样你就可以建立一个更广泛的相关知识基础。我建议,在阅读这本书时,围绕任何不熟悉的概念进行额外的阅读是值得的,因为机器学习知识往往会协同结合在一起;当你扩展工具包时,你拥有的越多,你就越容易理解新概念。

这种扩展技能工具包的概念是我试图用这本书实现的目标的基础。每章介绍一种或多种算法,并着眼于实现几个目标:

  • 在高层次上解释算法的作用,它能很好地解决什么问题,以及你应该如何应用它
  • 遍历算法的关键组件,包括拓扑结构、学习方法和性能测量
  • 确定如何通过查看模型输出来提高性能

除了知识和实践技能的转移,这本书看起来要实现一个更重要的目标;具体来说,讨论并传达一些熟练的机器学习从业者共有的品质。这些包括创造力,在复杂架构的定义和特定问题的清洁技术中均有体现。严谨性是另一个关键品质,这本书通篇强调的重点是对照有意义的目标衡量绩效,并批判性地评估早期努力。

最后,这本书没有试图掩盖解决数据挑战的现实:早期试验的混合结果、大量迭代计数和频繁的僵局。然而,与此同时,通过混合使用玩具示例、专家方法剖析,以及在书的结尾,更多的现实世界的挑战,我们展示了一种创造性的、顽强的和严格的方法如何能够打破这些障碍并带来有意义的结果。

在我们进行的过程中,我祝你好运,并鼓励你尽情享受,处理为你准备的内容,并将你学到的知识应用到新的领域或数据中。

我们开始吧!

这本书涵盖了什么

第 1 章无监督机器学习,向您展示了如何应用无监督学习技术来识别数据集内的模式和结构。

第二章深度信念网络,解释了 RBM 和 DBN 算法是如何工作的;你会知道如何使用它们,并对自己提高结果质量的能力充满信心。

第 3 章栈式去噪自编码器通过应用栈式去噪自编码器来学习高维输入数据的特征表示,继续利用深度架构构建我们的技能。

第四章卷积神经网络,向您展示如何应用卷积神经网络(或称 Convnet)。

第五章半监督学习,讲解如何应用几种半监督学习技术,包括 CPLE、自学习、S3VM。

第 6 章文本特征工程讨论了数据准备技巧,这些技巧显著提高了我们之前讨论的所有模型的有效性。

第 7 章特征工程第二部分,向您展示了如何询问数据以剔除或减轻质量问题,将其转换为有利于机器学习的形式,并创造性地增强该数据。

第 8 章集成方法,着眼于构建更复杂的模型集成,以及将健壮性构建到模型解决方案中的方法。

第 9 章附加 Python 机器学习工具,回顾了数据科学家最近可用的一些最佳工具,确定了它们提供的优势,并讨论了如何在一致的工作过程中,将它们与本书前面讨论的工具和技术一起应用。

附录 A章节代码要求讨论了该书的工具要求,确定了每章所需的库。

这本书你需要什么

这本书的全部内容利用了公开可用的数据和代码,包括开源 Python 库和框架。虽然每一章的示例代码都附有一个自述文件,其中记录了运行该章附带脚本中提供的代码所需的所有库,但为了方便起见,这些文件的内容在此进行了整理。

建议在处理任何后面章节的代码时,可以使用前面章节所需的一些库。这些要求使用粗体文本标识。特别是,为本书后面的任何内容建立第一章所需的库是很重要的。

这本书是给谁的

这个标题是给 Python 开发人员和分析师或数据科学家的,他们希望通过访问数据科学中一些最强大的最新趋势来增加他们现有的技能。例如,如果你曾经考虑过建立自己的图像或文本标签解决方案,或者参加一个卡格尔竞赛,这本书就是为你准备的!

先前的 Python 经验和机器学习的一些核心概念的基础将会有所帮助。

惯例

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

文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄如下所示:“我们将使用以下代码开始对手写的digits数据集应用 PCA。”

代码块设置如下:

import numpy as np
from sklearn.datasets import load_digits
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.preprocessing import scale
from sklearn.lda import LDA
import matplotlib.cm as cm

digits = load_digits()
data = digits.data

n_samples, n_features = data.shape
n_digits = len(np.unique(digits.target))
labels = digits.target

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

[ 0.39276606  0.49571292  0.43933243  0.53573558  0.42459285 
 0.55686854  0.4573401   0.49876358  0.50281585  0.4689295 ]

0.4772857426

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

型式

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

读者反馈

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

要给我们发送一般反馈,只需发送电子邮件<[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

这本书的代码包也托管在 GitHub 上,网址为 https://GitHub . com/packt publishing/Advanced-Machine-Learning-with-Python。我们还从丰富的图书和视频目录中获得了其他代码包,可在。https://github.com/PacktPublishing/快看!

下载本书的彩色图片

我们还为您提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。彩色图像将帮助您更好地理解输出中的变化。您可以从https://www . packtpub . com/sites/default/files/downloads/advancedmachinelearning with python _ color images . pdf下载此文件。

勘误表

尽管我们尽了最大努力来确保我们内容的准确性,但错误还是会发生。如果你在我们的某本书里发现了错误——可能是文本或代码中的错误——如果你能向我们报告,我们将不胜感激。通过这样做,你可以让其他读者免受挫折,并帮助我们改进这本书的后续版本。如果您发现任何勘误表,请访问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)>联系我们,我们将尽最大努力解决问题。

一、无监督机器学习

在本章中,您将学习如何应用无监督学习技术来识别数据集内的模式和结构。

无监督学习技术是一套有价值的探索性分析工具。它们揭示了数据集中的模式和结构,这些模式和结构产生的信息可能本身就具有信息价值,或者可以作为进一步分析的指南。拥有一套坚实的无监督学习工具至关重要,您可以应用这些工具来帮助将不熟悉或复杂的数据集分解为可操作的信息。

我们将首先回顾 【主成分分析】 ( 主成分分析),这是一种具有一系列降维应用的基本数据处理技术。接下来,我们将讨论 k-means 聚类,一种广泛使用且平易近人的无监督学习技术。然后,我们将讨论 Kohenen 的 自组织图 ( SOM ),这是一种拓扑聚类方法,能够将复杂数据集投影到二维。

在这一章中,我们将花一些时间讨论如何有效地应用这些技术来使高维数据集变得易于访问。我们将使用【UCI 手写数字】数据集来演示每种算法的技术应用。在讨论和应用每种技术的过程中,我们将回顾实际应用和方法问题,特别是关于如何校准和验证每种技术以及哪些性能测量是有效的。综上所述,我们将依次讨论以下主题:

  • 主成分分析
  • k-均值聚类
  • 自组织地图

主成分分析

为了有效地处理高维数据集,重要的是要有一套技术,可以将这个维度降低到可管理的水平。这种降维的优势包括能够以二维方式绘制多元数据,在最少数量的要素中捕获数据集的大部分信息内容,以及在某些情况下识别共线模型组件。

对于那些需要复习的人来说,机器学习上下文中的共线性指的是共享近似线性关系的模型特征。由于显而易见的原因,这些特性往往没有帮助,因为相关的特性不太可能相互添加任何一个独立提供的信息。此外,共线特征可能会强调局部最小值或其他虚假线索。

可能今天最广泛使用的降维技术是主成分分析。由于我们将在本书的多个上下文中应用主成分分析,因此我们应该回顾这一技术,了解其背后的理论,并编写 Python 代码来有效地应用它。

PCA–第一

主成分分析是一种强大的分解技术;它允许人们将高度多元的数据集分解成一组正交分量。当把足够多的数据放在一起时,这些成分可以解释几乎所有数据集的差异。本质上,这些组件提供了数据集的简短描述。主成分分析有广泛的应用,其广泛的实用性使其非常值得我们花时间去研究。

请注意这里稍微谨慎的措辞——给定的一组长度小于原始数据集中变量数量的组件几乎总是会丢失源数据集中的一些信息内容。如果给定足够的分量,这种损失通常是最小的,但是在少量主分量由非常高维的数据集组成的情况下,可能会有相当大的损失。因此,在执行主成分分析时,考虑需要多少组件来有效地对所讨论的数据集建模总是合适的。

主成分分析的工作原理是连续识别数据集中最大方差的轴(主成分)。它是这样做的:

  1. 识别数据集的中心点。
  2. 计算数据的协方差矩阵。
  3. 计算协方差矩阵的特征向量。
  4. 特征向量的正交化。
  5. 计算每个特征向量所代表的方差比例。

让我们简单地解释一下这些概念:

  • 协方差 是应用于多个维度的有效方差;它是两个或多个变量之间的方差。虽然单个值可以捕捉一维或变量的方差,但需要使用2×2矩阵来捕捉两个变量之间的协方差,使用3×3矩阵来捕捉三个变量之间的协方差,以此类推。所以 PCA 的第一步就是计算这个协方差矩阵。
  • 特征向量 是特定于数据集和线性变换的向量。具体地,在执行变换之前和之后,它是方向不改变的向量。为了更好地理解这是如何工作的,想象你拿着一根橡皮筋,伸直,放在双手之间。假设你把带子拉长,直到它在你的双手之间绷紧。特征向量是在拉伸之前和拉伸期间没有改变方向的向量;在这种情况下,它是从一只手到另一只手直接穿过带中心的向量。
  • 正交化 是寻找两个相互正交(成直角)的向量的过程。在 n 维数据空间中,正交化过程采用一组向量并产生一组正交向量。
  • 正交化 是一个正交化过程,也是对产品进行归一化。
  • 特征值 (大致对应特征向量的长度)用于计算每个特征向量所代表的方差比例。这是通过将每个特征向量的特征值除以所有特征向量的特征值之和来实现的。

总之,协方差矩阵用于计算特征向量。进行正交归一化处理,从特征向量产生正交的归一化向量。具有最大特征值的特征向量是连续分量具有较小特征值的第一主分量。这样,主成分分析算法具有获取数据集并将其转换为新的低维坐标系的效果。

采用主成分分析

现在我们已经在高层次上回顾了主成分分析算法,我们将直接进入并应用主成分分析到一个关键的 Python 数据集——UCI 手写digits数据集,作为 scikit-learn 的一部分分发。

该数据集由从 44 个不同作者收集的 1,797 个手写数字实例组成。来自这些作者作品的输入(压力和位置)在一个8×8网格上被重新采样两次,从而产生如下图所示的那种地图:

Employing PCA

这些图可以被转换成长度为 64 的特征向量,然后可以很容易地用作分析输入。有了 64 个要素的输入数据集,使用像主成分分析这样的技术将变量集减少到可管理的数量就有了直接的吸引力。就目前的情况来看,我们无法用探索性可视化有效地探索数据集!

我们将使用以下代码开始对手写的digits数据集应用主成分分析:

import numpy as np
from sklearn.datasets import load_digits
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.preprocessing import scale
from sklearn.lda import LDA
import matplotlib.cm as cm

digits = load_digits()
data = digits.data

n_samples, n_features = data.shape
n_digits = len(np.unique(digits.target))
labels = digits.target

这段代码为我们做了几件事:

  1. 首先,它加载了一组必要的库,包括numpy,一组来自 scikit-learn 的组件,包括digits数据集本身,PCA 和数据缩放函数,以及 matplotlib 的绘图能力。

  2. 然后代码开始准备digits数据集。它按顺序做了几件事:

    • 首先,它在创建有用的变量之前加载数据集
    • 创建data变量供后续使用,target向量( 09 )中不同的digits的数量被保存为一个变量,我们可以方便地访问该变量进行后续分析
    • target向量也保存为标签以备后用
    • 所有这些变量的创建都是为了简化后续分析
  3. 准备好数据集后,我们可以初始化我们的 PCA 算法,并将其应用于数据集:

    pca = PCA(n_components=10)
    data_r = pca.fit(data).transform(data)
    
    print('explained variance ratio (first two components): %s' % str(pca.explained_variance_ratio_))
    print('sum of explained variance (first two components): %s' % str(sum(pca.explained_variance_ratio_)))
    
  4. 该代码输出由解释力排序的前十个主成分中的每一个解释的方差。

在这个组10主成分的情况下,它们共同解释了整个数据集方差的 0.589 。考虑到这是从 64 变量到10分量的缩减,这其实不算太坏。然而,这确实说明了常设仲裁院的潜在损失。然而,关键问题是,这一组简化的组件是否使后续分析或分类更容易实现;也就是说,剩余的组件中是否有许多包含干扰分类尝试的方差。

创建了一个包含在digits数据集上执行的pca输出的data_r对象,让我们可视化输出。为此,我们将首先为类着色创建一个colors向量。然后我们简单地创建一个带有彩色类的散点图:

X = np.arange(10)
ys = [i+x+(i*x)**2 for i in range(10)]

plt.figure()
colors = cm.rainbow(np.linspace(0, 1, len(ys)))
for c, i target_name in zip(colors, [1,2,3,4,5,6,7,8,9,10], labels):
   plt.scatter(data_r[labels == I, 0], data_r[labels == I, 1],     
   c=c, alpha = 0.4)
   plt.legend()
   plt.title('Scatterplot of Points plotted in first \n'
   '10 Principal Components')
   plt.show()

得到的散点图如下所示:

Employing PCA

这个图向我们展示了,虽然在前两个主成分中类之间有一些分离,但是用这个数据集高度精确地分类可能很棘手。然而,类看起来确实是聚类的,我们可以通过使用聚类分析得到相当好的结果。这样,主成分分析让我们对数据集的结构有了一些了解,并为我们的后续分析提供了信息。

在这一点上,让我们抓住这一点,继续通过应用 k-means 聚类算法来检查聚类。

引入 k-均值聚类

在前一节中,您了解到无监督机器学习算法用于从大型、可能复杂的数据集提取关键的结构或信息内容。这些算法是在很少或没有人工输入的情况下实现的,并且无需训练数据(训练算法以识别期望的分类边界所需的一组带标签的解释和响应变量)。这意味着无监督算法是生成关于新的或不熟悉的数据集的结构和内容的信息的有效工具。它们允许分析师在很短的时间内建立强大的理解。

聚类–入门

聚类可能是典型的无监督学习技术,原因有几个。

大量的开发时间已经投入到优化聚类算法中,包括 Python 在内的大多数数据科学语言都有高效的实现。

聚类算法往往非常快,平滑的实现在多项式时间内运行。这使得即使在大型数据集上运行多个集群配置也不复杂。还存在可扩展的聚类实现,其将算法并行化,以在 1tb 规模的“T2”数据集上运行。

聚类算法通常很容易理解,因此如果必要的话,它们的操作很容易解释。

最流行的聚类算法是 k-means;该算法通过首先在数据空间中随机启动 k 个聚类作为 k 个点来形成 k 个聚类。这些点中的每一个都是一个簇的平均值。然后会出现一个迭代过程,运行如下:

  • 每个点根据最小(群内)平方和被分配到一个群,这直观上是最接近的平均值。
  • 每个聚类的中心(质心)成为新的平均值。这导致每种方法都发生了变化。

经过足够的迭代,质心移动到最小化性能度量的位置(最常用的性能度量是“簇内最小二乘和”度量)。一旦这个度量被最小化,在迭代过程中观察就不再被重新分配;此时,算法已经收敛到一个解。

启动聚类分析

现在我们已经回顾了聚类算法,让我们运行代码,看看聚类能为我们做些什么:

from time import time
import numpy as np
import matplotlib.pyplot as plt

np.random.seed()

digits = load_digits()
data = scale(digits.data)

n_samples, n_features = data.shape
n_digits = len(np.unique(digits.target))
labels = digits.target

sample_size = 300

print("n_digits: %d, \t n_samples %d, \t n_features %d"
   % (n_digits, n_samples, n_features))

print(79 * '_')
print('% 9s' % 'init''         time   inertia   homo   compl   v-meas   ARI     AMI  silhouette')

def bench_k_means(estimator, name, data):
   t0 = time()
   estimator.fit(data)
   print('% 9s %.2fs %i %.3f %.3f %.3f %.3f %.3f %.3f'
      % (name, (time() - t0), estimator.inertia_,
         metrics.homogeneity_score(labels, estimator.labels_),
         metrics.completeness_score(labels, estimator.labels_),
         metrics.v_measure_score(labels, estimator.labels_),
         metrics.adjusted_rand_score(labels, estimator.labels_),
         metrics.silhouette_score(data, estimator.labels_,
            metric='euclidean',
            sample_size=sample_size)))

该代码与我们之前看到的 PCA 代码之间的一个关键区别是,该代码首先对digits数据集应用一个比例函数。该函数将数据集中的值缩放至 01 之间。在任何需要的地方对数据进行缩放至关重要,无论是按对数比例还是按界限比例,以防止不同特征值的幅度对数据集产生不成比例的强大影响。确定数据是否需要缩放(以及需要什么样的缩放,在哪个范围内,等等)的关键在很大程度上取决于数据的形状和性质。如果数据分布显示异常值或大范围内的变化,则应用对数标度可能是合适的。无论这是通过可视化和探索性分析技术手动完成的,还是通过使用汇总统计数据完成的,围绕缩放的决策都与被检查的数据和要使用的分析技术相关联。在第 7 章特征工程第二部分中可以找到关于缩放决策和考虑的进一步讨论。

有益的是,scikit-learn 默认使用 k-means++算法,该算法在运行时间和避免不良聚类的成功率方面都优于原始的 k-means 算法。

该算法通过运行一个初始化过程来找到近似类内最小方差的聚类质心来实现这一点。

您可能已经从前面的代码中发现,我们正在使用一组性能估计器来跟踪我们的 k-means 应用程序的性能。基于单一的正确率或使用与其他算法通常使用的相同性能度量来度量聚类算法的性能是不切实际的。聚类算法成功的定义是,它们提供了输入数据如何分组的解释,在几个因素之间进行权衡,包括类分离、组内相似性和跨组差异。

同质性分数是一个简单的零到一的有界度量,用来衡量聚类在多大程度上只包含给定类的赋值。得分为 1 表示所有聚类都包含单个类的度量。这个度量由 完备性分数来补充,它是给定类的所有成员被分配到相同簇的程度的类似有界度量。因此,完整性分数和同质性分数为 1 表示完美的聚类解决方案。

有效性测度(v-测度)是同质性和完备性得分的调和均值,与二元分类的 F-测度完全相似。本质上,它提供了一个单一的 0-1 标度值来监控同质性和完整性。

调整后的兰德指数 ( ARI )是一种相似性度量,用于跟踪各组作业之间的一致性。当应用于聚类时,它测量真实的、预先存在的观察标签和作为聚类算法输出预测的标签之间的一致性。兰德指数在 0-1 范围内测量标注相似度,其中一个等于完美的预测标注。

所有前面的性能度量以及其他类似的度量(例如,Akaike 的互信息标准)的主要挑战是,它们需要对基本事实的理解,也就是说,它们需要对被检查的部分或全部数据进行标记。如果标签不存在并且无法生成,这些措施将不起作用。实际上,这是一个相当大的缺点,因为只有很少的数据集进行了预先标记,并且创建标签可能非常耗时。

在没有标记数据的情况下,测量 k 均值聚类解决方案性能的一个选项是 轮廓系数。这是一种衡量模型中的集群定义有多好的方法。给定数据集的轮廓系数是每个样本系数的平均值,该系数计算如下:

Kick-starting clustering analysis

每个术语的定义如下:

  • a :样本与同一个聚类中所有其他点之间的平均距离
  • b :样本与下一个最近聚类中所有其他点之间的平均距离

这个分数在 -11 之间,其中 -1 表示聚类不正确, 1 表示聚类非常密集, 0 附近的分数表示聚类重叠。这往往符合我们对一个好的集群解决方案是如何组成的预期。

digits数据集的情况下,我们可以使用这里描述的所有性能度量。因此,我们将通过在digits数据集上初始化我们的bench_k_means函数来完成前面的示例:

bench_k_means(KMeans(init='k-means++', n_clusters=n_digits, n_init=10), name="k-means++", data=data)
print(79 * '_')

这将产生以下输出(请注意,随机种子意味着您的结果将与我的不同!):

Kick-starting clustering analysis

让我们更详细地看看这些结果。

0.123处的剪影得分相当低,但这并不奇怪,因为手写数字数据本身就有噪声,并且确实倾向于重叠。然而,其他一些分数并没有那么令人印象深刻。0.619处的 V-测度是合理的,但是在这种情况下被较差的同质性测度所阻碍,这表明簇形质心没有完全解析。此外,0.465的 ARI 并不伟大。

让我们把这放在上下文中。最坏的分类尝试,随机分配,最多只能给出 10%的分类精度。因此,我们所有的绩效指标都将非常低。虽然我们确实做得比这好得多,但我们仍然远远落后于最好的计算分类尝试。正如我们将在第 4 章卷积神经网络中看到的,卷积网络在手写数字数据集上实现了分类误差极低的结果。我们不太可能用传统的 k-means 聚类达到这个精度水平!

总而言之,认为我们可以做得更好是有道理的。

为了再试一次,我们将应用额外的处理阶段。为了学习如何做到这一点,我们将应用主成分分析——我们之前介绍过的技术——来降低输入数据集的维度。实现这一点的代码非常简单,如下所示:

pca = PCA(n_components=n_digits).fit(data)
bench_k_means(KMeans(init=pca.components_, n_clusters=10),
name="PCA-based",
data=data) 

该代码简单地将PCA应用于digits数据集,产生与类一样多的主成分(在这种情况下,是数字)。在继续之前,查看PCA的输出可能是明智的,因为任何小的主成分的存在都可能暗示数据集包含共线性,或者值得进一步检查。

这个集群实例显示了显著的改进:

Kick-starting clustering analysis

V-measure 和 ARI 大约增加了 0.08 点,V-measure 读数相当可观0.693。轮廓系数没有显著变化。考虑到digits数据集中的复杂性和类间重叠,这些是很好的结果,特别是源于如此简单的代码添加!

对叠加了聚类的digits数据集的检查表明,一些有意义的聚类似乎已经形成。从下图中也可以明显看出,从输入特征向量中实际检测字符可能是一项具有挑战性的任务:

Kick-starting clustering analysis

调整集群配置

前面的示例描述了如何应用 k-means,遍历了相关代码,展示了如何绘制聚类分析的结果,并确定了适当的性能指标。然而,当将 k-means 应用于真实数据集时,需要采取一些额外的预防措施,我们将对此进行讨论。

另一个关键的实用点是如何为 k 选择一个合适的值。用特定的 k 值初始化 k-means 聚类可能没有害处,但是在许多情况下,最初并不清楚您可能找到多少个聚类,或者 k 的哪些值可能有帮助。

我们可以为一批中的多个 k 值重新运行前面的代码,并查看性能指标,但这不会告诉我们 k 的哪个实例最有效地捕获了数据中的结构。风险在于,随着 k 的增加,轮廓系数或无法解释的方差可能会急剧下降,而没有形成有意义的聚类。这种情况的极端情况是如果 k = o ,其中 o 是样本中的观察次数;每个点都有自己的聚类,轮廓系数会很低,但结果不会有意义。然而,由于过高的 k 值,可能会出现过度拟合的情况,这种情况不太常见。

为了减轻这种风险,建议使用支持技术来激励选择 k 。这方面一个有用的技巧是 肘法。肘法是一种非常简单的技术;对于 k 的每个实例,绘制解释的差异相对于 k 的百分比。这通常会导致一个情节,往往看起来像一个弯曲的手臂。

对于主成分分析缩减的数据集,这段代码看起来像下面的代码片段:

import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import load_digits
from scipy.spatial.distance import cdist
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.preprocessing import scale

digits = load_digits()
data = scale(digits.data)

n_samples, n_features = data.shape
n_digits = len(np.unique(digits.target))
labels = digits.target

K = range(1,20)
explainedvariance= []
for k in K:
   reduced_data = PCA(n_components=2).fit_transform(data)
   kmeans = KMeans(init = 'k-means++', n_clusters = k, n_init = k)
   kmeans.fit(reduced_data)
   explainedvariance.append(sum(np.min(cdist(reduced_data, 
   kmeans.cluster_centers_, 'euclidean'), axis =   
   1))/data.shape[0])
   plt.plot(K, meandistortions, 'bx-')
   plt.show()

肘方法的这种应用从先前的代码样本中进行PCA约简,并对解释的方差进行测试(具体来说,对聚类内的方差进行测试)。输出结果,作为规定范围内 k 每个值的未解释方差的度量。在这种情况下,当我们使用digits数据集(我们知道它有十个类)时,指定的range120:

Tuning your clustering configurations

肘部方法包括选择 k 的值,该值在最小化K的同时最大化解释的方差;也就是肘弯处 k 的值。这背后的技术意义在于,在更大的 k 值下,解释方差的最小增益被越来越大的过拟合风险所抵消。

肘部曲线可能或多或少明显,肘部可能不总是清晰可辨。该示例显示了比在其他数据集的其他情况下可能观察到的更渐进的进展。值得注意的是,虽然我们知道数据集中的类数量为十个,但在 k 上,肘关节方法开始显示收益递减,几乎立即增加,肘关节位于五个类左右。这与我们在前面的图中看到的类之间的大量重叠有很大关系。虽然有十个类别,但越来越难清楚地识别五个左右以上的类别。

考虑到这一点,值得注意的是,肘形方法旨在用作一种启发式方法,而不是某种客观原则。使用主成分分析作为预处理来提高聚类性能也有助于平滑图形,提供比其他方式更平缓的曲线。

除了使用肘方法之外,像我们在本章前面所做的那样,使用主成分分析来降低数据的维度,查看聚类本身也是有价值的。通过绘制数据集并将聚类分配投影到数据上,当 k-means 实现适合局部极小值或过度填充数据时,有时会非常明显。下图展示了我们之前的 K 均值聚类算法对digits数据集的过度拟合,这是通过使用 K = 150 人工提示的。在这个例子中,一些集群包含单个观察;这个输出不可能很好地推广到其他样本:

Tuning your clustering configurations

绘制肘形函数或集群分配很快就能实现,而且解释起来也很简单。然而,我们已经从启发的角度谈到了这些技术。如果一个数据集包含确定性数量的类,我们可能不确定启发式方法会产生可推广的结果。

另一个缺点是视觉图检查是一种非常手工的技术,这使得它不太适合生产环境或自动化。在这种情况下,找到一个基于代码的、自动的方法是理想的。在这种情况下,一个可靠的选择是 v 型折叠交叉验证,一种广泛使用的验证技术。

交叉验证很容易进行。为了使它工作,我们将数据集分成两部分。其中一个零件被单独留出作为测试集。根据训练数据训练模型,训练数据是除测试集之外的所有部分。现在让我们再次使用digits数据集来尝试一下:

import numpy as np
from sklearn import cross_validation
from sklearn.cluster import KMeans
from sklearn.datasets import load_digits
from sklearn.preprocessing import scale

digits = load_digits()
data = scale(digits.data)

n_samples, n_features = data.shape
n_digits = len(np.unique(digits.target))
labels = digits.target

kmeans = KMeans(init='k-means++', n_clusters=n_digits, n_init=n_digits)
cv = cross_validation.ShuffleSplit(n_samples, n_iter = 10, test_size = 0.4, random_state = 0)
scores = cross_validation.cross_val_score(kmeans, data, labels, cv = cv, scoring = 'adjusted_rand_score')
print(scores)
print(sum(scores)/cv.n_iter)

这段代码执行一些现在熟悉的数据加载和准备,并初始化 k-means 聚类算法。然后定义交叉验证参数cv。这包括迭代次数的说明、n_iter,以及每个文件夹中应该使用的数据量。在这种情况下,我们使用 60%的数据样本作为训练数据,40%作为测试数据。

然后,我们应用我们在交叉验证评分函数中指定的 k 均值模型和cv参数,并将结果打印为scores。现在让我们来看看这些分数:

[ 0.39276606  0.49571292  0.43933243  0.53573558  0.42459285 
 0.55686854  0.4573401   0.49876358  0.50281585  0.4689295 ]

0.4772857426

这个输出按顺序给出了交叉验证的 k-means++聚类的调整后的 Rand 分数,该聚类按顺序在每个10折叠上执行。我们可以看到结果确实在0.40.55之间波动;没有 PCA 的 k-means++的早期 ARI 评分落在这个范围内(在0.465)。那么,我们所创建的是代码,我们可以将它合并到我们的分析中,以便在持续的基础上自动检查我们的聚类质量。

正如本章前面提到的,你对成功标准的选择取决于你已经掌握的信息。在大多数情况下,您无法从数据集中访问地面真实标签,并且必须使用我们之前讨论过的轮廓系数等度量。

有时,即使使用交叉验证和可视化也不能提供结论性的结果。尤其是对于不熟悉的数据集,遇到一些噪声或二次信号在不同的 k 值下比您试图分析的信号解决得更好的问题并不少见。

与本书中讨论的其他算法一样,理解希望使用的数据集是非常必要的。没有这种洞察力,即使是技术上正确和严格的分析也完全有可能得出不恰当的结论。第 6 章文本特征工程将更全面地讨论检查和准备不熟悉数据集的原则和技术。

自组织地图

自组织映射是一种生成降维数据拓扑表示的技术。这是此类应用的众多技术之一,其中一个更广为人知的替代方法是主成分分析。然而,作为降维技术和可视化格式,SOMs 提供了独特的机会。

声音–第一

SOM 算法涉及许多简单操作的迭代。当以较小的规模应用时,它的行为类似于 k 均值聚类(我们将很快看到)。在更大的范围内,SOMs 以一种强大的方式揭示了复杂数据集的拓扑结构。

自组织映射由一个网格(通常是矩形或六边形)节点组成,其中每个节点包含一个与输入数据集具有相同维数的权重向量。节点可以随机初始化,但是粗略近似数据集分布的初始化将倾向于训练得更快。

该算法迭代,因为观察值作为输入呈现。迭代采用以下形式:

  • 识别当前配置中的获胜节点-最佳匹配单元 ( BMU )。通过测量所有权重向量在数据空间中的欧几里得距离来识别 BMU。
  • BMU 向输入向量调整(移动)。
  • 相邻节点也被调整,通常调整量较小,相邻移动的幅度由邻域函数决定。(邻域函数各不相同。在本章中,我们将使用高斯邻域函数。)

这个过程可能会重复多次迭代,如果合适的话使用采样,直到网络收敛(到达一个位置,在这个位置呈现新的输入不能提供最小化损失的机会)。

自组织神经网络中的节点与神经网络中的节点并无不同。它通常拥有长度等于输入数据集维数的权重向量。这意味着输入数据集的拓扑可以通过低维映射来保留和可视化。

这个 SOM 类实现的代码可以在som.py脚本的图书仓库中找到。现在,让我们在熟悉的环境中开始使用 SOM 算法。

采用自组织神经网络

如前所述,基于向量的欧几里德距离比较,自组织映射算法是迭代的。

这种映射倾向于形成一个相当易读的 2D 网格。在通常使用的 Iris 教程数据集的情况下,SOM 将非常清晰地绘制出来:

Employing SOM

在这个图中,类被分开了,并且在空间上也是有序的。这种情况下的背景颜色是一种聚类密度度量。蓝色和绿色类别之间有一些最小的重叠,其中 SOM 执行了不完美的分离。在 Iris 数据集上,SOM 将倾向于接近 100 次迭代量级的收敛解决方案,在 1000 次之后几乎没有可见的改进。对于包含不太清晰可分案例的更复杂数据集,此过程可能需要数万次迭代。

令人尴尬的是,像 scikit-learn 这样的现有 Python 包中没有 SOM 算法的实现。这使得我们有必要使用自己的实现。

为此,我们将使用的 SOM 代码位于相关的 GitHub 存储库中。现在,让我们看一下相关的脚本,了解一下代码是如何工作的:

import numpy as np
from sklearn.datasets import load_digits
from som import Som
from pylab import plot,axis,show,pcolor,colorbar,bone

digits = load_digits()
data = digits.data
labels = digits.target

此时,我们已经加载了digits数据集并将labels识别为一组单独的数据。这样做将使我们能够观察到当将类分配给map时,SOM 算法是如何分离类的:

som = Som(16,16,64,sigma=1.0,learning_rate=0.5)
som.random_weights_init(data)
print("Initiating SOM.")
som.train_random(data,10000) 
print("\n. SOM Processing Complete")

bone()
pcolor(som.distance_map().T) 
colorbar()

在这一点上,我们已经利用了一个Som类,该类在存储库中的一个单独的文件Som.py中提供。这个类包含交付我们在本章前面讨论的 SOM 算法所需的方法。作为这个函数的参数,我们提供了地图的维度(在尝试了一系列选项之后,在这种情况下,我们将从 16 x 16 开始——这个网格大小为要素地图提供了足够的空间来展开,同时保留了组之间的一些重叠。)和输入数据的维度。(这个参数决定了 SOM 节点内权重向量的长度。)我们还提供西格玛和学习率的值。

在这种情况下,适马定义了邻域函数的扩展。如前所述,我们使用的是高斯邻域函数。西格玛的合适值因网格大小而异。对于8×8网格,我们通常希望对适马使用 1.0 的值,而在本例中,我们对16×16网格使用 1.3 。当一个人的西格玛值偏离时,这是相当明显的;如果该值太小,则值倾向于聚集在网格中心附近。如果值太大,网格通常会以几个朝向中心的大空格结束。

学习速率自解释定义了自组织映射的初始学习速率。随着地图的不断迭代,学习速率会根据以下函数进行调整:

Employing SOM

这里, t 为迭代指数。

接下来,我们首先用随机权重初始化我们的 SOM。

与 k 均值聚类一样,这种初始化方法比基于数据分布近似值的初始化慢。类似于 k-means++算法的预处理步骤将加速 SOM 的运行时间。我们的 SOM 在digits数据集上运行得足够快,以至于现在没有必要进行这种优化。

接下来,我们为每个类设置标签和颜色分配,这样我们就可以在绘制的 SOM 上区分类。接下来,我们遍历每个数据点。

在每次迭代中,我们为 BMU 绘制了一个类特定的标记,正如我们的 SOM 算法所计算的那样。

当 SOM 完成迭代时,我们添加一个 U 矩阵(相对观察密度的彩色矩阵)作为单色缩放的绘图层:

labels[labels == '0'] = 0
labels[labels == '1'] = 1
labels[labels == '2'] = 2
labels[labels == '3'] = 3
labels[labels == '4'] = 4
labels[labels == '5'] = 5
labels[labels == '6'] = 6
labels[labels == '7'] = 7
labels[labels == '8'] = 8
labels[labels == '9'] = 9

markers = ['o', 'v', '1', '3', '8', 's', 'p', 'x', 'D', '*']
colors = ["r", "g", "b", "y", "c", (0,0.1,0.8), (1,0.5,0), (1,1,0.3), "m", (0.4,0.6,0)]
for cnt,xx in enumerate(data):
   w = som.winner(xx) 
   plot(w[0]+.5,w[1]+.5,markers[labels[cnt]],    
   markerfacecolor='None', markeredgecolor=colors[labels[cnt]], 
   markersize=12, markeredgewidth=2)
   axis([0,som.weights.shape[0],0,som.weights.shape[1]])
   show()

此代码生成类似于以下内容的图:

Employing SOM

该代码提供了一个 16×16 节点的 SOM 图。正如我们所看到的,地图在将每个集群分成地图上拓扑上不同的区域方面做得相当好。某些类别(特别是青色圆圈中的五位数和绿色星星中的九位数)位于 SOM 空间的多个部分。然而,在大多数情况下,每个类都占据一个不同的区域,公平地说,SOM 相当有效。U 矩阵显示,具有高密度点的区域由来自多个类别的数据共同居住。这实际上并不令人惊讶,因为我们看到了 k-means 和 PCA 绘图的类似结果。

进一步阅读

Victor Powell 和 Lewis 乐和在http://setosa.io/ev/principal-component-analysis/为 PCA 提供了一个奇妙的互动、可视化的解释,这对于不熟悉 PCA 核心概念或不太懂的读者来说是理想的选择。

谷歌研究公司的黄邦贤·施伦斯在 http://arxiv.org/abs/1404.1100 提供了一个清晰透彻的解释,对主成分分析进行了更长时间、更数学化的处理,涉及到底层的矩阵变换。

要获得将黄邦贤的描述翻译成清晰 Python 代码的完整工作示例,请考虑 Sebastian Raschka 在http://sebastianaschka . com/Articles/2015 _ PCA _ in _ 3 _ steps . html上使用 Iris 数据集的演示。

最后,在http://sci kit-learn . org/stable/modules/generated/sklearn . declaration . PCA . html上查看 sklearn 文档了解更多关于 PCA 类参数的详细信息。

对于 k-means 生动而专业的处理,包括导致其失败的条件的详细调查,以及在这种情况下的潜在替代方案,考虑大卫·罗宾逊的神奇博客,方差解释在http://varianceexplained.org/r/kmeans-free-lunch/

里克·戈夫在https://bl.ocks.org/rpgove/0060ff3b656618e9136b提供了关于肘法的具体讨论。

最后,考虑 sklearn 关于无监督学习算法的另一个视图的文档,包括位于的 k-means http://sci kit-learn . org/stable/tutorial/statistical _ 推论/unsupervise _ learning . html

科霍宁 SOM 上的许多现有材料要么相当陈旧,非常高级,要么是正式表达的。约翰·布里纳里亚在 http://www.cs.bham.ac.uk/~jxb/NN/l16.pdf 提供了本书中描述的一个不错的替代方案。

对于有兴趣更深入了解底层数学的读者,我建议直接阅读 Tuevo Kohonen 的作品。2012 年版的自组织地图是一个很好的起点。

本章中提到的多重共线性的概念,对不熟悉的https://onlinecourses.science.psu.edu/stat501/node/344给出了清晰的解释。

总结

在本章中,我们回顾了预处理和降维的三种广泛应用的技术。通过这样做,您了解了很多关于不熟悉的数据集的知识。

我们开始应用主成分分析,一种广泛使用的降维技术,来帮助我们理解和可视化高维数据集。随后,我们使用 k-means 聚类对数据进行聚类,通过性能指标、肘形方法和交叉验证来确定改进和衡量 k-means 分析的方法。我们发现digits数据集上的 k-means,照现在的情况来看,并没有给出特别的结果。这是由于我们通过主成分分析发现的类重叠。我们通过应用主成分分析作为预处理来改善后续的聚类结果,从而克服了这个缺点。

最后,我们开发了一个自组织映射算法,它提供了比主成分分析更清晰的分类。

已经学习了一些关于无监督学习技术和分析方法的关键基础知识,让我们深入研究一些更强大的无监督学习算法的使用。

二、深度信念网络

在前一章中,我们研究了一些广泛使用的降维技术,这些技术使数据科学家能够更深入地了解数据集的本质。

接下来的几章将集中在一些更复杂的技术上,借鉴深度学习的领域。本章致力于建立对如何应用受限玻尔兹曼机器 ( RBM )和管理深度学习架构的理解,人们可以通过链接 RBMs 来创建深度学习架构——深度信念网络 ( DBN )。DBNs 是可训练的,可以有效解决文本、图像和声音识别中的复杂问题。它们被领先的公司用于物体识别、智能图像搜索和机器人空间识别。

我们要做的第一件事是在 DBN 的算法中获得坚实的基础;与聚类或 PCA 不同,这段代码并没有被数据科学家广泛知晓,我们将对其进行一定深度的回顾,以构建强大的工作知识。一旦我们完成了理论的工作,我们将在此基础上通过代码逐步推进,使理论成为焦点,并允许我们将技术应用于现实世界的数据。对这些技术的诊断并不琐碎,需要严格,因此我们将强调思维过程和诊断技术,它们使我们能够有效地观察和控制您的实现的成功。

到本章结束时,您将了解 RBM 和 DBN 算法是如何工作的,知道如何使用它们,并对自己提高结果质量的能力充满信心。综上所述,本章内容如下:

  • 神经网络——入门
  • 受限玻尔兹曼机器
  • 深层信念网络

神经网络——入门

RBM 是递归神经网络的一种形式。为了理解 RBM 是如何工作的,有必要对神经网络有一个更全面的了解。了解人工神经网络(为了简单起见,以下称为神经网络)算法的读者将在以下描述中找到熟悉的元素。

有许多关于神经网络的非常详细的理论描述;我们不会详细讨论翻新这块土地。为了本章的目的,我们将首先描述神经网络的组成部分、常见的体系结构和流行的学习过程。

神经网络的组成

对于不熟悉的读者来说,神经网络是一类数学模型,用于训练在一组输入特征上产生和优化函数(或分布)的定义。给定神经网络应用的具体目标可以由操作员使用性能度量(通常是成本函数)来定义;这样,神经网络可以用于分类、预测或转换它们的输入。

神经网络中神经这个词的使用是从高压生物隐喻中汲取灵感来启发机器学习研究的长期传统的产物。因此,人工神经网络算法最初是从生物神经元结构中提取的(现在仍然经常提取)。

神经网络由以下要素组成:

  • 一个学习过程:一个神经网络通过调整其节点权重函数内的参数进行学习。这是通过将性能度量的输出(如前所述,在监督学习环境中,这通常是成本函数,相对于网络的目标输出的某种不准确性度量)馈送到网络的学习函数中来实现的。该学习函数输出所需的权重调整(从技术上讲,它通常计算偏导数——梯度下降所需的项。)来最小化成本函数。
  • 一组神经元或权重:每个包含一个权重函数(激活函数)操纵输入数据。激活函数在网络之间可以有很大变化(一个众所周知的例子是双曲正切)。关键要求是权重必须是自适应的,也就是说,可以根据学习过程的更新进行调整。为了进行非参数建模(也就是说,在不定义概率分布细节的情况下进行有效建模),有必要同时使用可见单元和隐藏单元。隐藏的单位永远不会被观察到。
  • 连接功能:它们控制哪些节点可以将数据中继到其他哪些节点。节点可能能够以不受限制或受限制的方式自由地相互中继输入,或者它们可能在输入数据必须以定向方式流过的层中更结构化。有各种各样的互连模式,不同的模式产生非常不同的网络属性和可能性。

利用这一组元素,我们能够构建广泛的神经网络,从熟悉的有向无环图(也许最著名的例子是 【多层感知器】(【MLP】)到创造性的替代方案。我们在前一章中使用的自组织映射 ( SOM )是一种神经网络,具有独特的学习过程。我们将在本章后面讨论的算法,RBM 算法,是另一种具有一些独特性质的神经网络算法。

网络拓扑

神经网络中的神经元是如何连接的有许多变化,结构决定是决定网络学习能力的一个重要因素。无监督学习中的常见拓扑往往不同于有监督学习中的常见拓扑。我们在上一章中讨论的 SOM 是一种常见且现在很熟悉的无监督学习拓扑。

正如我们所看到的,SOM 直接将单个输入案例投影到每个节点包含的权重向量上。然后,它继续对这些节点重新排序,直到数据集的适当映射收敛为止。SOM 的实际结构是基于训练细节、给定训练实例的具体结果以及在构建网络时做出的设计决策的变体,但是正方形或六边形网格结构正变得越来越普遍。

监督学习中非常常见的拓扑类型是三层前馈网络,经典情况是 MLP。在这个网络拓扑模型中,网络中的神经元被分成层,每一层都与“超越”它的层通信。第一层包含输入到隐藏层。隐藏的层使用权重激活(使用正确的激活函数,例如 sigmoid 或 gauss,MLP 可以充当通用函数逼近器)开发数据表示,激活值被传送到输出层。输出层通常提供网络结果。因此,该拓扑如下所示:

Network topologies

其他网络拓扑提供不同的功能。例如,玻尔兹曼机器的拓扑结构与前面描述的不同。玻尔兹曼机器包含隐藏和可见的神经元,就像三层网络中的神经元一样,但是所有这些神经元在有向循环图中相互连接:

Network topologies

这种拓扑结构使得玻尔兹曼机器是随机的——概率性的而不是确定性的——并且能够在给定足够复杂的问题的情况下以几种方式之一发展。玻尔兹曼机器也是生成的,这意味着它能够完全(概率地)建模所有的输入变量,而不是使用观察到的变量来专门建模目标变量。

哪种网络拓扑合适在很大程度上取决于您的具体挑战和所需的输出。每一个在某些领域都很强大。此外,这里描述的每种拓扑结构都伴随着一个学习过程,使网络能够迭代地收敛到(理想的)最优解。

有各种各样的学习过程,具体的过程和拓扑或多或少是相互兼容的。学习过程的目的是使网络能够迭代地调整其权重,从而创建输入数据的越来越精确的表示。

与网络拓扑一样,需要考虑大量的学习过程。有些熟悉是假设的,并且存在大量关于学习过程的优秀资源(本章末尾给出了一些好的例子)。这一部分将重点介绍学习过程的一般特征,而在本章的后面,我们将更详细地看一个具体的例子。

如上所述,神经网络中学习的目标是迭代地改善模型上的权重分布,使得它以越来越高的精度逼近输入数据下的函数。这个过程需要一个性能度量。这可能是一种分类误差度量,通常用在有监督的分类环境中(即 MLP 网络中的反向传播学习算法)。在随机网络中,它可能是概率最大化项(例如基于能量的网络中的能量)。

在任一情况下,一旦有了增加概率的措施,网络就有效地尝试使用优化方法来减少该措施。在许多情况下,网络的优化是使用梯度下降来实现的。就梯度下降算法方法而言,在给定的训练迭代中,性能度量值的大小类似于梯度的斜率。因此,最小化性能度量是一个将梯度下降到该组权重的误差度量最低的点的问题。

下一次迭代的网络更新的大小(算法的学习速率)可能会受到性能度量的影响,也可能是硬编码的。

网络调整的权重更新可能来自误差表面本身;如果是这样,您的网络通常会有一种计算梯度的方法,即导出更新需要调整网络激活的权重函数参数的值,以便继续降低性能度量。

在回顾了网络拓扑和学习方法的一般概念之后,让我们开始讨论一个特定的神经网络,RBM。正如我们将看到的,RBM 是强大的深度学习算法的关键部分。

受限玻尔兹曼机

RBM 是本章主题“深度学习架构——DBN”的基础部分。以下部分将首先介绍 RBM 背后的理论,包括建筑结构和学习过程。

接下来,我们将直接进入 RBM 类的代码,在代码中的理论元素和函数之间建立联系。最后,我们将讨论成果管理制的应用以及与实施 RBM 相关的实际因素。

介绍 RBM

玻尔兹曼机器是一种特殊类型的随机递归神经网络。它是一个基于能量的模型,这意味着它使用能量函数将能量值与网络的每个配置相关联。

在前一节中,我们简要讨论了玻尔兹曼机器的结构。如上所述,玻尔兹曼机器是一个有向循环图,其中每个节点都连接到所有其他节点。这个属性使它能够以一种循环的方式建模,这样模型的输出就可以随着时间的推移而变化和查看。

玻尔兹曼机器中的学习循环包括最大化训练数据集的概率 X 。如上所述,所使用的具体性能度量是能量,其特征是数据集 X 的概率的负对数,给定模型参数向量θ。该度量被计算并用于更新网络的权重,以最小化网络中的自由能。

玻尔兹曼机器在处理图像数据方面特别成功,包括照片、面部特征和笔迹分类上下文。

不幸的是,玻尔兹曼机器对于更具挑战性的最大似然问题并不实用。这是因为机器的扩展能力存在挑战;随着节点数量的增加,计算时间呈指数级增长,最终使我们处于无法计算网络自由能的境地。

对于那些对底层形式推理感兴趣的人来说,发生这种情况是因为一个数据点的概率, xp(x;θ),必须全部集成到 1x 。要实现这一点,我们需要使用一个分区函数 Z ,用作归一化常数。( Z 是一个常数,使得非负函数乘以 Z 将使非负函数在所有输入上积分为1;在这种情况下,越过所有 x 。)

概率模型函数是一组正态分布的函数。为了得到我们模型的能量,我们需要区分每个模型的参数;然而,由于分区功能,这变得复杂。每个模型参数都会产生依赖于其他模型参数的方程,如果没有(潜在的)非常昂贵的计算,我们最终会发现自己无法计算能量,计算成本会随着网络规模的扩大而增加。

为了克服玻尔兹曼机的弱点,需要对网络拓扑和训练过程都进行调整。

拓扑

带来效率提升的主要拓扑变化是节点间连通性的限制。首先,必须防止同一层内节点之间的连接。此外,必须防止所有跳过层的连接(即非连续层之间的直接连接)。具有这种结构的玻尔兹曼机器被称为 RBM,如下图所示:

Topology

这种拓扑的一个优点是隐藏层和可见层在给定的条件下彼此独立。因此,可以使用另一层的激活从一层取样。

训练

我们之前观察到,对于玻尔兹曼机器,机器的训练时间缩放非常差,因为机器被放大到额外的节点,使我们处于无法评估我们试图在训练中使用的能量函数的位置。

RBM 通常是使用一种以不同学习算法为核心的程序进行训练的,即 【永久对比发散】 ( PCD )算法,该算法提供了最大似然的近似值。PCD 不评估能量函数本身,而是允许我们估计能量函数的梯度。有了这些信息,我们可以在最陡的梯度方向上进行非常小的调整,通过这些调整,我们可以根据需要朝着局部最小值前进。

PCD 算法由两个阶段组成。这些被称为正相和负相,每个相对模型的能量有相应的影响。正相位增加了训练数据集 X 的概率,从而降低了模型的能量。接下来,负相位使用来自模型的采样方法来估计负相位梯度。负相位的总体效果是降低模型生成样本的概率。

在负相位和整个更新过程中的采样是使用一种称为 吉布斯采样的采样形式来实现的。

吉布斯抽样是算法的马氏链蒙特卡罗 ( MCMC )族的变体,并且从近似的多元概率分布中抽样。这意味着,在构建我们的概率模型时,不是使用求和计算(就像我们可能做的那样,例如,当我们抛硬币一定次数时;在这种情况下,我们可以将头部尝试的次数相加,作为所有尝试总数的一部分),而是近似积分的值。如何通过逼近一个积分来创建一个概率模型的主题值得比这本书给它更多的时间。因此,本章的进一步阅读部分提供了一个很好的论文参考。现在要记住的要点(并且剥离出很多重要的细节!)是,我们不是对每种情况精确地求和一次,而是基于所讨论的数据的(通常是非均匀的)分布进行采样。吉布斯抽样是一种基于模型中所有其他参数值的概率抽样方法。一旦获得新的参数值,它将立即用于其他参数的采样计算。

你们中的一些人可能会问为什么 PCD 是必要的。为什么不用更熟悉的方法,比如带线搜索的梯度下降?简单地说,我们不能轻易计算网络的自由能,因为这种计算涉及到网络所有节点的集成。当我们指出玻尔兹曼机器的巨大弱点时,我们认识到了这一局限性——随着节点数量的增加,计算时间呈指数级增长,这使我们处于一种情况,即我们试图最小化一个我们无法计算其值的函数!

PCD 提供的是一种估计能量函数梯度的方法。这使得网络的自由能近似成为可能,这对于应用来说是足够快的,并且已经被证明通常是准确的。(有关性能比较,请参考进一步阅读部分。)

正如我们之前看到的,RBM 概率模型函数是我们模型参数的联合分布,使得 Gibbs 抽样是合适的!

初始化 RBM 中的训练循环包括几个步骤:

  1. 我们获得当前迭代激活的隐藏层权重值。
  2. 我们使用上一次迭代的吉布斯链的状态作为输入,执行正相位脉冲编码。
  3. 我们使用吉布斯链的预先存在的状态来执行光子晶体的负相位。这给了我们自由能值。
  4. 我们使用我们计算的能量值更新隐藏层上激活的权重。

该算法允许 RBM 迭代地向降低的自由能值前进。RBM 继续训练,直到训练数据集的概率积分为 1,自由能等于 0,此时 RBM 收敛。

现在我们有机会回顾一下 RBM 的拓扑和训练过程,让我们应用该算法对一个真实数据集进行分类。

RBM 的应用

现在我们已经有了 RBM 算法的一般工作知识,让我们通过代码创建一个 RBM。我们将使用一个 RBM 类来对 MNIST 手写数字数据集进行分类。我们将要查看的代码执行以下操作:

  • 它设置了 RBM 的初始参数,包括层大小、可共享的偏差向量和与外部网络结构连接的可共享权重矩阵(这支持深度信念网络)
  • 它定义了隐藏层和可见层之间的通信和推理功能
  • 它定义了允许我们更新网络节点参数的功能
  • 它定义了为学习过程处理有效采样的函数,使用 PCD-k 来加速采样(使得在合理的时间范围内进行计算成为可能)
  • 它定义了计算模型自由能的函数(用于计算 PCD-k 更新所需的梯度)
  • 它确定了伪似然 ( PL ),可用作对数似然代理来指导选择合适的超参数

让我们开始检查我们的RBM课:

class RBM(object):
    def __init__(
        self,
        input=None,
        n_visible=784,
        n_hidden=500,
        w=None,
        hbias=None,
        vbias=None,
        numpy_rng=None,
        theano_rng=None
    ):

我们需要构建的第一个元素是一个RBM构造器,我们可以用它来定义模型的参数,比如可见和隐藏节点的数量(n_visiblen_hidden)以及可以用来调整 RBM 的推理函数和 CD 更新执行方式的附加参数。

w参数可以用作共享权重矩阵的指针。正如我们将在本章后面看到的,这在实现 DBN 时变得更加相关;在这种架构中,权重矩阵需要在网络的不同部分之间共享。

hbiasvbias参数类似地用作共享隐藏和可见(分别)单元偏置向量的可选参考。同样,这些在数据库网络中使用。

input参数使 RBM 能够从上到下连接到其他图形元素。例如,这允许人们将限制性商业惯例连锁化。

设置好这个构造函数后,我们接下来需要充实前面的每个参数:

        self.n_visible = n_visible
        self.n_hidden = n_hidden

        if numpy_rng is None:
            numpy_rng = numpy.random.RandomState(1234)

        if theano_rng is None:
            theano_rng = RandomStreams(numpy_rng.randint(2 ** 30))

这是相当简单的事情;我们为 RBM 设置了可见节点和隐藏节点,并设置了两个随机数生成器。theano_rng参数稍后将在我们的代码中用于从 RBM 的隐藏单位中取样:

        if W is None:
            initial_W = numpy.asarray(
                numpy_rng.uniform(
                    low=-4 * numpy.sqrt(6\. / (n_hidden + n_visible)),
                    high=4 * numpy.sqrt(6\. / (n_hidden + n_visible)),
                    size=(n_visible, n_hidden)
                ),
                dtype=theano.config.floatX
            )

该代码切换W的数据类型,以便在 GPU 上运行。接下来,我们使用theano.shared设置共享变量,这允许变量的存储在它出现的函数之间共享。在当前示例中,我们创建的共享变量将是权重向量(W)以及隐藏和可见单元的偏差变量(hbiasvbias)。当我们继续创建具有多个组件的深度网络时,以下代码将允许我们在网络的各个部分之间共享组件:

            W = theano.shared(value=initial_W, name='W', borrow=True)

        if hbias is None: 
            hbias = theano.shared(
                value=numpy.zeros(
                    n_hidden,
                    dtype=theano.config.floatX
                ),
                name='hbias',
                borrow=True
            )

        if vbias is None:
            vbias = theano.shared(
                value=numpy.zeros(
                    n_visible,
                    dtype=theano.config.floatX
                ),
                name='vbias',
                borrow=True
            )

此时,我们准备如下初始化输入层:

        self.input = input
        if not input:
            self.input = T.matrix('input')

        self.W = W
        self.hbias = hbias
        self.vbias = vbias
        self.theano_rng = theano_rng
        self.params = [self.W, self.hbias, self.vbias]

由于我们现在有了一个初始化的input层,我们的下一个任务是创建我们在本章前面描述的符号图。实现这一点需要创建功能来管理网络的层间传播和激活计算操作:

def propup(self, vis):
        pre_sigmoid_activation = T.dot(vis, self.W) + self.hbias
        return [pre_sigmoid_activation, T.nnet.sigmoid(pre_sigmoid_activation)]

    def propdown(self, hid):
        pre_sigmoid_activation = T.dot(hid, self.W.T) + self.vbias
        return [pre_sigmoid_activation, T.nnet.sigmoid(pre_sigmoid_activation)]

这两个功能将一层单元的激活传递给另一层。第一个函数将可见单元的激活向上传递给隐藏单元,以便隐藏单元可以根据可见单元的样本来计算它们的激活。第二个函数执行相反的操作——将隐藏层的激活向下传播到可见单元。

或许值得问一下为什么我们要同时创造propuppropdown。正如我们回顾的那样,PCD 只要求我们从隐藏单元中执行采样。那么propup有什么价值呢?

简而言之,当我们想要从 RBM 取样以回顾其进展时,从可见层取样变得有用。在我们的 RBM 正在处理视觉数据的大多数应用中,定期从可见层获取采样输出并绘制出来是非常有价值的,如下例所示:

Applications of the RBM

正如我们在这里看到的,在迭代的过程中,我们的网络开始改变它的标签;在第一种情况下,7变形为9,而在其他地方9变成了6,网络逐渐达到了 3 度的定义。

正如我们之前所讨论的,对您的 RBM 的运营有尽可能多的看法是有帮助的,以确保它提供有意义的结果。从它产生的输出中取样是提高这种可见性的一种方法。

有了关于可见层激活的信息,我们可以从隐藏层传递一个单元激活的样本,给定隐藏节点的激活:

    def sample_h_given_v(self, v0_sample):

 pre_sigmoid_h1, h1_mean = self.propup(v0_sample)
    h1_sample = self.theano_rng.binomial(size=h1_mean.shape,
    n=1, p=h1_mean, dtype=theano.config.floatX)

    return [pre_sigmoid_h1, h1_mean, h1_sample]

同样,我们现在可以从给定隐藏单元激活信息的可见层进行采样:

    def sample_v_given_h(self, h0_sample):
    pre_sigmoid_v1, v1_mean = self.propdown(h0_sample)
      v1_sample = self.theano_rng.binomial(size=v1_mean.shape,
      n=1, p=v1_mean, dtype=theano.config.floatX)

      return [pre_sigmoid_v1, v1_mean, v1_sample]

我们现在已经实现了执行吉布斯采样步骤所需的连接和更新循环,如本章前面所述。接下来,我们应该定义这个采样步骤!

    def gibbs_hvh(self, h0_sample):

        pre_sigmoid_v1, v1_mean, v1_sample = 
        self.sample_v_given_h(h0_sample)
        pre_sigmoid_h1, h1_mean, h1_sample = 
        self.sample_h_given_v(v1_sample)
        return [pre_sigmoid_v1, v1_mean, v1_sample,
                pre_sigmoid_h1, h1_mean, h1_sample]

如上所述,我们需要一个类似的函数来从可见层进行采样:

    def gibbs_vhv(self, v0_sample):

        pre_sigmoid_h1, h1_mean, h1_sample = 
        self.sample_h_given_v(v0_sample)
        pre_sigmoid_v1, v1_mean, v1_sample = 
        self.sample_v_given_h(h1_sample)
        return [pre_sigmoid_h1, h1_mean, h1_sample,
                pre_sigmoid_v1, v1_mean, v1_sample]

到目前为止,我们已经编写的代码为我们提供了一些模型。它建立了节点和层以及层与层之间的连接。我们已经编写了所需的代码,以便根据隐藏层的吉布斯采样来更新网络。

我们仍然缺少的是允许我们执行以下操作的代码:

  • 计算模型的自由能。正如我们所讨论的,该模型使用能量作为术语来执行以下操作:
    • 使用我们的 Gibbs 抽样步长代码实现 PCD,并设置 Gibbs 步长计数参数 k = 1 ,计算梯度下降的参数梯度
    • 创建一种方法,将 PCD(计算的梯度)的输出馈送到我们之前定义的网络更新代码
  • 开发在整个培训过程中跟踪我们 RBM 的进展和成功的方法。

首先,我们将创建计算 RBM 自由能的方法。请注意,这是隐藏层概率分布的逆对数,我们之前讨论过:

  def free_energy(self, v_sample):

        wx_b = T.dot(v_sample, self.W) + self.hbias
        vbias_term = T.dot(v_sample, self.vbias)
        hidden_term = T.sum(T.log(1 + T.exp(wx_b)), axis=1)
        return -hidden_term - vbias_term

接下来,我们将实现 PCD。此时,我们将设置几个有趣的参数。lr是学习速率的缩写,是一个用于调整学习速度的可调参数。k参数指向 PCD 要执行的步骤数(还记得本章前面的 PCD-k 符号吗?).

我们讨论的 PCD 包含两个阶段,正和负。以下代码计算光子晶体二极管的正相位:

def get_cost_updates(self, lr=0.1, persistent = , k=1):

        pre_sigmoid_ph, ph_mean, ph_sample =  
        self.sample_h_given_v(self.input)

            chain_start = persistent

同时,以下代码实现了 PCD 的负相。为此,我们使用安诺的扫描操作扫描gibbs_hvh功能k次,每次扫描执行一个吉布斯采样步骤。完成负相后,我们获得自由能值:

        (
            [
                pre_sigmoid_nvs,
                nv_means,
                nv_samples,
                pre_sigmoid_nhs,
                nh_means,
                nh_samples
            ],
            updates
        ) = theano.scan(
            self.gibbs_hvh,
            outputs_info=[None, None, None, None, None, chain_start],
            n_steps=k
        )

        chain_end = nv_samples[-1]

        cost = T.mean(self.free_energy(self.input)) - T.mean(
            self.free_energy(chain_end))

        gparams = T.grad(cost, self.params, 
        consider_constant=[chain_end])

写完执行整个 PCD 过程的代码后,我们需要一种方法将输出馈送到我们的网络。在这一点上,我们能够将我们的 PCD 学习过程与代码联系起来,以更新我们之前回顾的网络。前面的更新字典指向gibbs_hvh功能的theano.scan。大家可能还记得,gibbs_hvh目前包含theano_rng随机状态的规则。我们现在需要做的是将新的参数值和包含 Gibbs 链状态的变量添加到字典中(即updates变量):

        for gparam, param in zip(gparams, self.params):
            updates[param] = param - gparam * T.cast(
                lr,
                dtype=theano.config.floatX
            )

            updates = nh_samples[-1]
            monitoring_cost =  
            self.get_pseudo_likelihood_cost(updates)

        return monitoring_cost, updates

我们现在几乎拥有了让我们的 RBM 运转起来所需的所有部件。显然缺少的是一种在培训期间或完成后检查培训的方法,以确保我们的 RBM 正在学习数据的适当表示。

我们之前讨论过如何训练 RBM,特别是分区函数带来的挑战。此外,在代码的前面,我们实现了一种方法,通过它我们可以在训练期间检查 RBM;我们创建了gibbs_vhv函数来从模型中执行吉布斯采样。

在我们之前关于如何验证 RBM 的讨论中,我们讨论了可视化绘制 RBM 创建的过滤器。我们将很快回顾如何实现这一点。

最后一种可能性是使用 PL 的逆对数作为可能性本身的更易处理的代理。从技术上讲,对数概率是以所有其他数据点为条件的每个数据点(每个x)的对数概率之和。如前所述,对于更大维度的数据集,这变得过于昂贵,因此使用了 log-PL 的随机近似。

我们引用了一个函数,该函数将使我们能够在get_cost_updates函数中获得 PL 成本,特别是get_pseudo_likelihood_cost函数。现在是充实这个函数并获得伪似然的时候了:

def get_pseudo_likelihood_cost(self, updates):

        bit_i_idx = theano.shared(value=0, name='bit_i_idx')
        xi = T.round(self.input)

        fe_xi = self.free_energy(xi)

        xi_flip = T.set_subtensor(xi[:, bit_i_idx], 1 - xi[:, 
        bit_i_idx])

        fe_xi_flip = self.free_energy(xi_flip)

        cost = T.mean(self.n_visible * 
        T.log(T.nnet.sigmoid(fe_xi_flip - fe_xi)))

        updates[bit_i_idx] = (bit_i_idx + 1) % self.n_visible

        return cost

我们现在已经填写了缺失组件列表中的每一个元素,并且已经完整地回顾了RBM类。我们已经探索了每个元素是如何与 RBM 背后的理论联系在一起的,现在应该对 RBM 算法的工作原理有了一个彻底的了解。我们了解我们的 RBM 将会取得什么样的成果,并将很快能够对其进行审查和评估。简而言之,我们准备训练我们的 RBM。开始 RBM 的训练就是运行下面的代码,它会触发train_set_x功能。我们将在本章后面更深入地讨论这个函数:

    train_rbm = theano.function(
        [index],
        cost,
        updates=updates,
        givens={
            x: train_set_x[index * batch_size: (index + 1) * 
            batch_size]
        },
        name='train_rbm'
    )

    plotting_time = 0.
    start_time = time.clock()

更新了 RBM 的更新和训练集后,我们开始了训练。在每个时期内,我们先对训练数据进行训练,然后将权重绘制为矩阵(如本章前面所述):

    for epoch in xrange(training_epochs):

        mean_cost = []
        for batch_index in xrange(n_train_batches):
            mean_cost += [train_rbm(batch_index)]

        print 'Training epoch %d, cost is ' % epoch, 
        numpy.mean(mean_cost)

        plotting_start = time.clock()
        image = Image.fromarray(
            tile_raster_images(
                X=rbm.W.get_value(borrow=True).T,
                img_shape=(28, 28),
                tile_shape=(10, 10),
                tile_spacing=(1, 1)
            )
        )
        image.save('filters_at_epoch_%i.png' % epoch)
        plotting_stop = time.clock()
        plotting_time += (plotting_stop - plotting_start)

    end_time = time.clock()

    pretraining_time = (end_time - start_time) - plotting_time

    print ('Training took %f minutes' % (pretraining_time / 60.))

权重往往绘制得相当清晰,类似于 Gabor 滤波器(通常用于图像边缘检测的线性滤波器)。如果数据集是在相当低噪声的背景上手写的字符,您往往会发现权重跟踪使用的笔画。对于照片,过滤器将近似跟踪图像中的边缘。下图显示了输出示例:

Applications of the RBM

最后,我们创建持久吉布斯链,我们需要它来导出我们的样本。如前所述,以下函数执行单个 Gibbs 步骤,然后更新链:

plot_every = 1000

    (
        [
            presig_hids,
            hid_mfs,
            hid_samples,
            presig_vis,
            vis_mfs,
            vis_samples
        ],
        updates
    ) = theano.scan(
        rbm.gibbs_vhv,
        outputs_info=[None, None, None, None, None, persistent_vis_chain],
        n_steps=plot_every
    )

该代码运行我们之前描述的gibbs_vhv功能,绘制网络输出样本供我们检查:

    updates.update({persistent_vis_chain: vis_samples[-1]})
    sample_fn = theano.function(
        [],
        [
            vis_mfs[-1],
            vis_samples[-1]
        ],
        updates=updates,
        name='sample_fn'
    )

    image_data = numpy.zeros(
        (29 * n_samples + 1, 29 * n_chains - 1),
        dtype='uint8'
    )
    for idx in xrange(n_samples):

        vis_mf, vis_sample = sample_fn()
        print ' ... plotting sample ', idx
        image_data[29 * idx:29 * idx + 28, :] = tile_raster_images(
            X=vis_mf,
            img_shape=(28, 28),
            tile_shape=(1, n_chains),
            tile_spacing=(1, 1)
        )

    image = Image.fromarray(image_data)
    image.save('samples.png')

在这一点上,我们有一整个 RBM。我们有 PCD 算法和使用该算法和吉布斯采样更新网络的能力。我们有几个可见的输出方法,这样我们就可以评估我们的 RBM 训练得有多好。

然而,我们还没有完成!接下来,我们将开始了解 RBM 最常见和最强大的应用。

RBM 的进一步应用

我们可以使用 RBM 作为最大似然算法本身。它的功能与其他算法相当好。有利的是,它可以被放大到可以学习高维数据集的程度。然而,这并不是 RBM 真正的优势所在。

RBM 最常用作称为 DBN 的高效深层网络架构的预处理机制。数据库网络是学习和分类一系列图像数据集的极其强大的工具。它们具有很好的归纳未知案例的能力,是可用的最佳图像学习工具之一。由于这个原因,数据库网络在世界上许多顶尖的技术和数据科学公司都在使用,主要用于图像搜索和识别。

深度信念网络

DBN 是一个图形模型,使用多个堆叠的径向基函数构建。当第一 RBM 基于来自训练数据的像素的输入训练特征层时,后续层将先前层的激活视为像素,并尝试学习后续隐藏层中的特征。这经常被描述为学习数据的表示,并且是深度学习中的一个常见主题。

应该有多少个多重成果管理制取决于手头的问题需要什么。从实用的角度来看,这是在提高精度和增加计算成本之间的权衡。每层径向基函数都会提高训练数据对数概率的下限。换句话说;随着每增加一层功能,DBN 几乎不可避免地变得不那么糟糕。

就层大小而言,减少连续径向基函数的隐藏层中的节点数量通常是有利的。人们应该避免这样的情况,即 RBM 的可见单位至少与它之前的 RBM 的隐藏单位一样多(这增加了简单地学习网络的身份功能的风险)。

当连续的径向基函数在层大小上减小时,直到最终的 RBM 具有接近数据中方差维数的层大小,这可能是有利的(但决不是必要的)。将 MLP 附加到层具有太多节点的 DBN 的末尾会损害分类性能;这就像试图把吸管固定在软管的末端!即使是拥有许多神经元的 MLP 也可能无法在这样的环境中成功训练。与此相关,已经注意到,即使层不包含非常多的节点,只要有足够的层,或多或少任何功能都可以被建模。

确定数据中方差的维度不是一项简单的任务。可以支持这项任务的一个工具是 PCA 正如我们在上一章中看到的,PCA 可以使我们对输入数据中存在多少有意义大小的分量有一个合理的概念。

训练一个 DBN

训练一个 DBN 通常是贪婪地进行的,也就是说,它训练在每一层进行局部优化,而不是试图达到全局最优。学习过程如下:

  • DBN 的第一层是使用我们在前面讨论 RBM 学习时看到的方法训练的。因此,第一层使用隐藏单元上的吉布斯采样将其数据分布转换为后验分布。
  • 这个分布比输入数据本身更有利于 RBM 训练,所以下一个 RBM 层学习这个分布!
  • 连续的 RBM 层继续在前一层输出的样本上训练。
  • 该架构中的所有参数都是使用性能度量来调整的。

这种性能度量可能会有所不同。正如本章前面所讨论的,它可能是梯度下降中使用的对数似然代理。在有监督的上下文中,可以添加分类器(例如,MLP)作为体系结构的最后一层,预测精度可以用作微调深层体系结构的性能度量。

让我们继续在实践中使用 DBN。

应用 DBN

讨论了 DBN 和围绕它的理论,是时候建立我们自己的了。我们将以类似于 RBM 的方式工作,在初始化和训练我们的网络以看到它的实际运行之前,先通过一堂DBN课,将代码与理论联系起来,讨论应该期待什么以及如何检查网络的性能。

让我们来看看我们的DBN课:

class DBN(object):

    def __init__(self, numpy_rng, theano_rng=None, n_ins=784,
                 hidden_layers_sizes=[500, 500], n_outs=10):

        self.sigmoid_layers = []
        self.rbm_layers = []
        self.params = []
        self.n_layers = len(hidden_layers_sizes)

        assert self.n_layers > 0

        if not theano_rng:
            theano_rng = RandomStreams(numpy_rng.randint(2 ** 30))

        self.x = T.matrix('x')
        self.y = T.ivector('y')

DBN类包含许多需要进一步解释的参数。用于确定初始权重的numpy_rngtheano_rng参数在我们对RBM课程的考试中已经很熟悉了。n_ins参数是指向 DBN 输入的尺寸(特征)的指针。hidden_layers_sizes参数是隐藏图层大小的列表。该列表中的每个值将指导DBN构造器创建相关大小的 RBM 图层;您会注意到,n_layers参数是指网络中的层数,由hidden_layers_sizes设置。调整该列表中的值使我们能够使其图层大小从输入图层大小逐渐减小到越来越简洁的表示,如本章前面所讨论的。

还值得注意的是self.sigmoid_layers将存储 MLP 组件(DBN 的最后一层),而self.rbm_layers存储用于预训练 MLP 的 RBM 层。

完成此操作后,我们执行以下操作来完成我们的 DBN 体系结构:

  • 我们创建n_layers乙状结肠层
  • 我们连接乙状结肠层形成 MLP
  • 我们为每个 sigmoid 层构造一个 RBM,在每个 sigmoid 层和 RBM 之间有一个共享的权重矩阵和隐藏的偏差

下面的代码用 sigmoid 激活创建n_layers多个层;首先创建输入图层,然后创建隐藏图层,其大小对应于我们的hidden_layers_sizes列表中的值:

       for i in xrange(self.n_layers):

            if i == 0:
                input_size = n_ins
            else:
                       input_size = hidden_layers_sizes[i - 1]
            if i == 0:
                layer_input = self.x
            else:
                layer_input = self.sigmoid_layers[-1].output

            sigmoid_layer = HiddenLayer(rng=numpy_rng,
                                        input=layer_input,
                                        n_in=input_size,
                                        n_out=hidden_layers_sizes[i],
                                        activation=T.nnet.sigmoid)
            self.sigmoid_layers.append(sigmoid_layer)

            self.params.extend(sigmoid_layer.params)

接下来,我们创建一个与 sigmoid 层共享权重的 RBM。这直接利用了我们之前描述的RBM类:

            rbm_layer = RBM(numpy_rng=numpy_rng,
                            theano_rng=theano_rng,
                            input=layer_input,
                            n_visible=input_size,
                            n_hidden=hidden_layers_sizes[i],
                            W=sigmoid_layer.W,
                            hbias=sigmoid_layer.b)
            self.rbm_layers.append(rbm_layer)

最后,我们在 DBN 的末端添加一个逻辑回归层,从而形成一个 MLP:

        self.logLayer = LogisticRegression(
            input=self.sigmoid_layers[-1].output,
            n_in=hidden_layers_sizes[-1],
            n_out=n_outs)
        self.params.extend(self.logLayer.params)

        self.finetune_cost = self.logLayer.negative_log_likelihood(self.y)

        self.errors = self.logLayer.errors(self.y)

既然我们已经把放在了我们的MLP类中,让我们构建DBN。以下代码使用28 * 28输入(即 MNIST 图像数据中的28*28像素)、三个尺寸逐渐减小的隐藏层和10输出值(针对 MNIST 数据集中的每个10手写数字类别)构建网络:

    numpy_rng = numpy.random.RandomState(123)
    print '... building the model'
    dbn = DBN(numpy_rng=numpy_rng, n_ins=28 * 28,
              hidden_layers_sizes=[1000, 800, 720],
              n_outs=10)

正如本节前面所讨论的,DBN 分两个阶段进行训练——逐层预训练,其中每一层获取前一层的输出进行训练,然后是微调步骤(反向传播),允许在整个网络中调整权重。第一阶段,预处理,通过在每一层的 RBM 内执行一步 PCD 来实现。以下代码将执行此预处理步骤:

    print '... getting the pretraining functions'
    pretraining_fns = 
    dbn.pretraining_functions(train_set_x=train_set_x,
    batch_size=batch_size, k=k)

    print '... pre-training the model'
    start_time = time.clock()

    for i in xrange(dbn.n_layers):
        for epoch in xrange(pretraining_epochs):
            c = []
            for batch_index in xrange(n_train_batches):
                c.append(pretraining_fns[i](index=batch_index,
                                            lr=pretrain_lr))
            print 'Pre-training layer %i, epoch %d, cost ' % (i, epoch),
            print numpy.mean(c)

    end_time = time.clock()

然后通过以下命令运行预训练的 DBN:

python code/DBN.py

请注意,即使使用 GPU 加速,这段代码也会花费相当多的时间进行预处理,因此建议您连夜运行。

验证 DBN

整个 DBN 的验证是以一种非常熟悉的方式完成的。我们可以使用交叉验证的最小验证误差作为一个误差度量。然而,最小交叉验证误差可能会低估交叉验证数据的预期误差,因为元参数可能会过度适应新数据。

因此,我们应该使用交叉验证错误来调整元参数,直到交叉验证错误最小化。然后,我们应该将我们的 DBN 暴露给坚持的测试集,使用测试误差作为我们的验证度量。我们的DBN班正是执行这个训练过程。

然而,这并没有告诉我们,如果网络不能充分训练,该怎么办。如果我们的 DBN 表现不佳,我们该怎么办?

首先要做的是识别潜在的原因,在这方面,有一些常见的罪犯。我们知道底层 RBM 的训练也相当棘手,任何一个单独的层都可能无法训练。令人欣慰的是,我们的RBM类让我们能够利用和查看每一层生成的权重(过滤器),我们可以绘制这些权重来查看我们的网络试图表示什么。

此外,我们想问我们的网络是过度匹配,还是匹配不足。这两种情况都是完全可能的,认识到这是如何以及为什么会发生是很有用的。在拟合不足的情况下,训练过程可能根本无法为模型找到好的参数。当您使用较大的网络来解决较大的问题空间时,这种情况尤其常见,但即使在一些较小的模型中也可以看到。如果你认为你的 DBN 可能会出现装配不足,你有几个选择。首先是简单地缩小隐藏层的大小。这可能行得通,也可能行不通。一个更好的选择是逐渐缩小你的隐藏层,这样每一层都可以学习到前一层的精细版本。如何做到这一点,如何急剧缩减,何时停止都是第一种情况下的试错和长期基于经验的学习的问题。

过拟合是一种众所周知的现象,在这种现象中,您的算法会过度专门针对所提供的训练数据进行训练。这类问题通常是在交叉验证时发现的(在交叉验证时,您的错误率会急剧增加),但可能非常有害。解决过度拟合问题的方法确实存在;可以增加训练数据集的大小。一种更严格的贝叶斯方法是附加一个额外的标准(例如,先验),用于降低拟合训练数据的价值。提高分类性能的一些最有效的方法是预处理方法,我们将在第 6 章文本特征工程第 7 章特征工程第二部分中讨论。

尽管该代码将从一个预定义的位置(给定一个种子值)初始化,但模型的随机特性意味着它将迅速发散,结果可能会有所不同。在我的系统上运行时,这个 DBN 实现了 1.19%的最小交叉验证误差。更重要的是,在 46 个监督时期后,它实现了 1.30%的测试误差。这些都是好结果;的确,它们可以与领先领域的例子相媲美!

进一步阅读

对于神经网络入门,从一系列来源阅读是有意义的。有许多问题需要注意,不同的作者强调不同的材料。凯文·格尼在《神经网络导论》中提供了一个坚实的介绍。

一篇关于马尔可夫链蒙特卡罗基础直觉的优秀文章可以在 http://twiecki.github.io/blog/2015/11/10/mcmc-sampling/获得。

对于那些对支持吉布斯采样的直觉感兴趣的读者来说,菲利普·雷斯尼克和埃里克·哈迪斯蒂的论文《外行的吉布斯采样》提供了一个关于吉布斯如何工作的技术性但清晰的描述。特别值得注意的是有一些非常好的类比!在https://www.umiacs.umd.edu/~resnik/pubs/LAMP-TR-153.pdf找到他们。

对比发散没有很多好的解释,我喜欢的一个解释是奥利弗·伍德福德在 http://www.robots.ox.ac.uk/~ojw/files/NotesOnCD.pdf 提供的。如果你对大量使用形式表达有点畏惧,我仍然建议你阅读它,因为它清晰地描述了相关的理论和实际问题。

本章使用在 http://deeplearning.net/tutorial/contents.html 可获得的安诺文档作为讨论和实施 RBM 和 DBN 课程的基础。

总结

这一章我们已经讲了很多内容!在深入研究 RBM 算法和 RBM 代码本身之前,我们首先概述了神经网络,重点介绍了拓扑和学习方法的一般特性。我们带着这种坚实的理解去创造一个 DBN。在这样做的时候,我们将 DBN 理论和代码联系在一起,然后启动我们的 DBN 来处理 MNIST 数据集。我们在一个 10 类问题中进行了图像分类,取得了极具竞争力的结果,分类误差在 2%以下!

在下一章中,我们将通过向您介绍另一种深度学习架构来继续巩固您对深度学习的掌握— 栈式去噪自编码器 ( SDA )。

三、栈式去噪自编码器

在本章中,我们将通过应用栈式去噪自编码器 ( SdA )来学习高维输入数据的特征表示,从而继续利用深度架构构建我们的技能。

和以前一样,我们将从深入了解支撑自编码器的理论和概念开始。作为数据科学工具包的一部分,我们将确定相关技术,并指出自编码器的优势。我们将讨论 去噪自编码器 ( dA )的使用,这是一种算法的变体,它将随机损坏引入到输入数据中,迫使自编码器去干扰输入,从而构建更有效的特征表示。

像以前一样,我们将通过遍历阿达类的代码来跟进理论,将理论和实现细节联系起来,以建立对该技术的深刻理解。

在这一点上,我们将进行一次与前一章非常相似的旅程——通过堆叠 dA,我们将创建一个可用于预处理 MLP 网络的深度架构,该架构在包括语音数据处理在内的一系列无监督学习应用中提供了显著的性能提升。

自编码器

自编码器(也称为 空竹网络)是深度架构的另一个关键组件。自编码器与 RBM 有关,自编码器训练类似于 RBM 训练;然而,自编码器可能比具有对比差异的径向基函数更容易训练,因此在径向基函数训练效率较低的情况下更受欢迎。

介绍自编码器

自编码器是一个简单的三层神经网络,其输出单元直接连接回输入单元。自编码器的目的是在输出层重构(解码)输入之前,将 i 维输入编码为 h 维表示,其中 h < i 。训练过程包括在这个过程中迭代,直到重建误差最小化——此时应该已经达到输入数据的最有效表示(应该,排除了达到局部最小值的可能性!).

在前一章中,我们讨论了主成分分析作为一种强大的降维技术。这种将自编码器描述为寻找输入数据的最有效的降维表示无疑是熟悉的,你可能会问为什么我们要探索另一种技术来完成同样的任务。

简单的答案是,像自组织映射一样,自编码器可以提供非线性约简,这使它们能够比主成分分析更有效地处理高维输入数据。这又恢复了我们之前问题的一种形式——如果自编码器实现了 SOM 的功能,却没有提供有启发性的视觉呈现,为什么还要讨论自编码器呢?

简单地说,自编码器是一套更发达、更复杂的技术;与我们在第 1 章无监督机器学习中讨论的技术相比,去噪和叠加技术的使用能够相对容易地将高维多模态数据减少到更高的精度和更大的规模。

在高层次讨论了自编码器的功能后,让我们深入了解一下自编码器的拓扑结构以及它们的培训内容。

拓扑

如本章前面所述,自编码器具有相对简单的结构。它是一个三层神经网络,输入隐藏输出层。与大多数神经网络架构一样,输入前馈到隐藏的层,然后是输出层。一个值得一提的拓扑特征是隐藏的层通常比输入输出层节点少。(然而,如前所述,所需的隐藏节点的数量实际上是输入数据的复杂度的函数;隐藏的层的目标是限制来自输入的信息内容,并迫使网络识别捕获底层统计属性的表示。准确表示非常复杂的输入可能需要大量的隐藏节点。)

自编码器的主要特点是输出通常设置为输入;自编码器的性能度量是其在隐藏的层内编码后重构输入的精度。自编码器拓扑往往采用以下形式:

Topology

出现在输入隐藏层之间的编码功能是输入( x )到新形式( y 的映射。一个简单的示例映射函数可能是输入的非线性(在本例中为 sigmoid, s )函数,如下所示:

Topology

然而,可能存在或开发更复杂的编码来适应特定的主题领域。当然,在这种情况下, W 代表分配给 xb 的权重值,是一个可调变量,可以对其进行调整以使重建误差最小化。

然后,自编码器解码以传送其输出。该重建旨在采用与 x 相同的形状,并将通过类似的变换进行,如下所示:

Topology

这里,b’W’通常也是可配置的,以允许网络优化。

训练

如上所述,网络通过最小化重构误差来训练。测量该误差的一种流行方法是简单的平方误差测量,如下式所示:

Training

但是,对于输入格式不太通用的情况(如一组位概率),存在不同的更合适的错误度量。

虽然目的是自编码器捕捉输入数据集中变化的主轴,但是自编码器有可能学习到远没有那么有用的东西——输入的身份函数。

去噪自编码器

虽然自编码器在某些应用中可以很好地工作,但它们在应用于输入数据包含必须在高维度中建模的复杂分布的问题时可能具有挑战性。主要的挑战是,对于具有n 维输入和至少 n 编码的自编码器,自编码器很有可能只是学习输入的身份函数。在这种情况下,编码是输入的文字副本。这种自编码器称为 过完备

**### 注

当训练机器学习技术时,最重要的属性之一是理解隐藏层的维度如何影响结果模型的质量。如果输入数据很复杂,而隐藏层的节点太少,无法有效地捕捉到这种复杂性,那么结果是显而易见的——网络无法像有更多节点时那样进行训练。

为了捕捉输入数据中的复杂分布,您可能希望使用大量隐藏节点。在隐藏层具有至少与输入一样多的节点的情况下,网络很有可能会获知输入的身份;在这种情况下,输入的每个元素都是作为一个特定的唯一案例来学习的。自然,一个已经被训练这样做的模型将在训练数据上工作得非常好,但是由于它已经学会了一个不能推广到不熟悉的数据的琐碎模式,当被验证时,它很可能会灾难性地失败。

这在对复杂数据(如语音数据)建模时尤其相关。这种数据通常分布复杂,因此语音信号的分类需要多模态编码和高维隐藏层。当然,这增加了自编码器(或大量模型中的任何一个,因为这不是自编码器特有的问题)学习身份函数的风险。

虽然(相当令人惊讶的)过完全自编码器可以并且确实在某些配置下学习误差最小化表示(即,其中第一隐藏层需要非常小的权重以迫使隐藏单元进入线性方向,并且后续权重具有大的值),但是这种配置难以优化,并且希望找到另一种方法来防止过完全自编码器学习恒等式函数。

有几种不同的方法可以防止过完备的自编码器在学习身份函数的同时仍然捕获其表示中有用的东西。到目前为止,最流行的方法是将噪声引入到输入数据中,并通过学习分布和统计规律而不是同一性,迫使自编码器在噪声数据上进行训练。这可以通过多种方法有效地实现,包括使用稀疏约束或缺失技术(其中输入值被随机设置为零)。

在本章中,我们将使用向输入端引入噪声的过程是压差。通过这种方法,多达一半的输入被随机设置为零。为了实现这一点,我们创建了一个对输入数据进行操作的随机损坏过程:

def get_corrupted_input(self, input, corruption_level):

   return self.theano_rng.binomial(size=input.shape, n=1, p=1 -   
   corruption_level, dtype=theano.config.floatX) * input

为了对输入数据进行精确建模,自编码器必须从未损坏的值中预测损坏的值,从而学习有意义的统计特性(即分布)。

除了防止自编码器学习数据的身份值之外,添加去噪过程还倾向于产生对输入变化或失真更加鲁棒的模型。这被证明对于本来就有噪声的输入数据特别有用,例如语音或图像数据。本书前言中提到的深度学习技术的一个公认优势是,深度学习算法最大限度地减少了对特征工程的需求。许多学习算法需要对输入数据进行冗长而复杂的预处理(对图像进行滤波或对音频信号进行处理)来重建去噪后的输入,并使模型能够进行训练,而阿达可以用最少的预处理有效地工作。这可以大大减少在输入数据上训练模型达到实际精确度所需的时间。

最后,值得注意的是,学习输入数据集标识函数的自编码器可能在根本上配置错误。由于自编码器的主要附加价值是找到特征集的低维表示,所以已经学习了输入数据的标识函数的自编码器可能只是有太多的节点。如果有疑问,可以考虑减少隐藏层中的节点数量。

既然我们已经讨论了自编码器的拓扑结构——有效训练自编码器的方法以及去噪在提高自编码器性能中的作用——让我们回顾一下阿达的代码,以便将前面的理论付诸实践。

应用阿达

在这一点上,我们准备逐步实施阿达。我们再次利用安诺库来申请一个dA类。

与我们在上一章中探讨的RBM类不同,DenoisingAutoencoder 相对简单,将 dA 的功能与我们在本章前面研究的理论和数学联系起来相对简单。

第 2 章深度信念网络中,我们应用了一个RBM类,该类有许多元素,虽然对于 RBM 本身的正确运行不是必需的,但是支持多层、深度架构中的共享参数。我们将使用的dA类拥有类似的共享元素,这些元素将在本章后面为我们提供构建多层自编码器架构的方法。

我们从初始化一个dA类开始。我们指定可见单位的数量,n_visible,以及隐藏单位的数量,n_hidden。此外,我们还指定了输入配置的变量(input)以及权重(W)和隐藏及可见偏差值(bhidbvis)。这四个附加变量使自编码器能够从深度架构的其他元素接收配置参数:

class dA(object):

    def __init__(
        self,
        numpy_rng,
        theano_rng=None,
        input=None,
        n_visible=784,
        n_hidden=500,
        W=None,
        bhid=None,
        bvis=None
):

        self.n_visible = n_visible
        self.n_hidden = n_hidden

我们通过初始化权重和偏差变量来跟进。我们将权重向量W设置为初始值initial_W,该值是通过从以下范围随机均匀采样获得的:

Applying a dA

然后,我们使用numpy.zeros将可见和隐藏的偏置变量设置为零数组:

   if not theano_rng:
      theano_rng = RandomStreams(numpy_rng.randint(2 ** 30))

   if not W:
      initial_W = numpy.asarray(
         numpy_rng.uniform(
            low=-4 * numpy.sqrt(6\. / (n_hidden + n_visible)),
            high=4 * numpy.sqrt(6\. / (n_hidden + n_visible)),
            size=(n_visible, n_hidden)
         ),
         dtype=theano.config.floatX
      )
      W = theano.shared(value=initial_W, name='W', borrow=True)

   if not bvis:
      bvis = theano.shared(
         value=numpy.zeros(
            n_visible,
            dtype=theano.config.floatX
         ),
         borrow=True
      )

   if not bhid:
      bhid = theano.shared(
         value=numpy.zeros(
            n_hidden,
            dtype=theano.config.floatX
         ),
         name='b',
         borrow=True
      )

在本章前面,我们描述了自编码器如何通过映射(如Applying a dA)在可见层和隐藏层之间转换。为了实现这种转换,需要定义WbW'b'与前面描述的自编码器参数、bhidbvisW相关。W'b'在以下代码中称为W_primeb_prime:

self.W = W
self.b = bhid
self.b_prime = bvis
self.W_prime = self.W.T
self.theano_rng = theano_rng
if input is None:
   self.x = T.dmatrix(name='input')
else:
   self.x = input

self.params = [self.W, self.b, self.b_prime]

前面的代码分别将bb_prime设置为bhidbvis,而W_prime设置为W的转置;换句话说,重量是并列的。由于以下几个原因,自编码器中有时会使用捆绑重量,但并不总是这样:

  • 捆绑权重提高了几种情况下的结果质量(尽管通常在最优解是 PCA 的情况下,PCA 是具有捆绑权重的自编码器倾向于达到的解决方案)
  • 通过减少需要存储的参数数量,捆绑权重提高了自编码器的内存消耗
  • 最重要的是,捆绑权重提供了正则化效果;他们需要少一个参数进行优化(因此少一件可能出错的事情!)

然而,在其他情况下,使用无附加条件的砝码既常见又合适。例如,在输入数据是多模态的并且最优解码器对一组非线性统计规律建模的情况下,这是正确的。在这种情况下,线性模型,如主成分分析,将无法有效地模拟非线性趋势,您将倾向于使用无约束权重获得更好的结果。

为我们的自编码器配置好参数后,下一步是定义使其能够学习的功能。在本章的前面,我们确定自编码器通过向输入数据添加噪声来有效学习,然后尝试学习该输入的编码表示,该编码表示又可以重构到输入中。那么,我们接下来需要的是提供这种功能的函数。我们从破坏输入数据开始:

def get_corrupted_input(self, input, corruption_level):

   return self.theano_rng.binomial(size=input.shape, n=1, p=1 – 
   corruption_level, dtype=theano.config.floatX) * input

损坏程度可使用corruption_level参数配置;正如我们之前所认识到的,通过压差导致的输入损坏通常不超过 50%,即 0.5。该函数随机取一组病例,其中病例数为size等于corruption_levelinput的比例。该函数产生长度等于输入的01的损坏向量,其中向量的corruption_level大小比例为 0 。损坏的输入向量只是自编码器输入向量和损坏向量的倍数:

def get_hidden_values(self, input):
   return T.nnet.sigmoid(T.dot(input, self.W) + self.b)

接下来,我们获得隐藏值。这是通过执行等式Applying a dA以获得 y (隐藏值)的代码来完成的。为了获得自编码器的输出( z ),我们通过使用先前定义的b_primeW_prime执行Applying a dA的代码重建隐藏层:

defget_reconstructed_input(self, hidden):
   returnT.nnet.sigmoid(T.dot(hidden, self.W_prime) +   
   self.b_prime)

最后缺少的部分是成本更新的计算。我们之前回顾了一个成本函数,一个简单的平方误差度量:Applying a dA。让我们使用这个成本函数来计算我们的成本更新,基于输入(x)和重构(z):

def get_cost_updates(self, corruption_level, learning_rate):

   tilde_x = self.get_corrupted_input(self.x, corruption_level)
   y = self.get_hidden_values(tilde_x)
   z = self.get_reconstructed_input(y)
   E = (0.5 * (T.z – T.self.x)) ^ 2
   cost = T.mean(E)

   gparams = T.grad(cost, self.params)
   updates = [
      (param, param - learning_rate * gparam)
      for param, gparam in zip(self.params, gparams)
   ]

return (cost, updates)

至此,我们有了一个功能性的 dA!它可以用于建模输入数据的非线性属性,并且可以作为学习输入数据的有效和低维表示的有效工具。然而,自编码器的真正威力来自于它们堆叠在一起时显示的属性,这些属性是深层架构的构建块。

栈式去噪自编码器

虽然自编码器本身是有价值的工具,但是通过堆叠自编码器以形成深度网络,可以获得显著的精度。这是通过将编码器在一层上创建的表示作为该层的输入馈送到下一层的编码器中来实现的。

栈式去噪自编码器 ( SdAs )目前在许多领先的数据科学团队中用于复杂的自然语言分析以及大量的信号、图像和文本分析。

在上一章讨论了深度信念网络之后,SdA 的实现将会非常熟悉。SdA 的使用方式与我们深度信念网络中的 RBM 的使用方式非常相似。深层架构的每一层都有阿达和 sigmoid 组件,自编码器组件用于对 sigmoid 网络进行预处理。栈式去噪自编码器使用的性能度量是训练集误差,在最后一段微调时间之前,使用密集的逐层(逐层)预处理来逐步调整网络参数。在微调期间,使用验证和测试数据对网络进行训练,训练时间较短,但更新步骤较多。目标是让网络在微调结束时收敛,以便提供准确的结果。

除了提供深度网络的典型优势(学习复杂或高维数据集的特征表示的能力,以及在没有大量特征工程的情况下训练模型的能力)之外,堆叠自编码器还有一个额外的有趣特性。

正确配置的栈式自编码器可以捕获其输入数据的分层分组。栈式去噪自编码器的连续层可以学习越来越高级的特征。在第一层可以从输入数据中学习一些一阶特征(例如学习照片图像中的边缘)的情况下,第二层可以学习一些一阶特征的分组(例如,通过学习对应于输入图像中的轮廓或结构元素的给定的边缘配置)。

对于给定的问题,没有黄金法则来确定应该有多少层或多大层。最佳解决方案通常是试验这些模型参数,直到找到最佳点。这个实验最好用超参数优化技术或遗传算法来完成(我们将在本书后面的章节中讨论这些主题)。

更高层可以学习越来越高阶的配置,使得栈式去噪自编码器能够学习识别面部特征、字母数字字符或物体的广义形式(例如鸟)。这是 sda 学习输入数据的非常复杂的高级抽象的独特能力。

自编码器可以无限期堆叠,并且已经证明继续堆叠自编码器可以提高深度架构的有效性(主要限制因素是计算时间成本)。在本章中,我们将研究堆叠三个自编码器来解决自然语言处理的挑战。

应用 SdA

既然我们有机会了解 SdA 作为深度学习架构的优势和力量,让我们在真实数据集上测试我们的技能。

在这一章中,让我们远离图像数据集,使用opinirank Review数据集,这是一个包含来自猫途鹰的大约 259,000 篇酒店评论的文本数据集,可通过 UCI 机器学习数据集存储库访问。这个免费提供的数据集提供了各种酒店的评论分数(从 1 到 5 的浮点数)和评论文本;我们将应用我们的堆叠 dA 来尝试从每个酒店的评论文本中识别其评分。

我们将应用我们的自编码器来分析这个数据的预处理版本,它可以从本章附带的 GitHub 共享中访问。我们将在下一章讨论准备文本数据的技巧。感兴趣的读者可以在https://archive . ics . UCI . edu/ml/datasets/opincrank+Review+Dataset获取源数据。

为了启动,我们需要一个堆叠的去噪自编码器(以下简称SdA)类:

class SdA(object):

    def __init__(
        self,
        numpy_rng,
        theano_rng=None,
        n_ins=280,
        hidden_layers_sizes=[500, 500],
        n_outs=5,
        corruption_levels=[0.1, 0.1]
):

如前所述,SdA是通过将一层自编码器的编码作为输入提供给后续层而创建的。这个类支持层计数的配置(反映在hidden_layers_sizescorruption_levels向量的长度中,但不是由它们设置的)。它还支持每层的不同层大小(以节点为单位),可以使用hidden_layers_sizes进行设置。正如我们所讨论的,配置自编码器的连续层的能力对于开发成功的表示是至关重要的。

接下来,我们需要参数来存储SdA的 MLP ( self.sigmoid_layers)和 dA ( self.dA_layers)元素。为了指定我们架构的深度,我们使用self.n_layers参数来指定所需的 sigmoid 和 dA 层数:

self.sigmoid_layers = []
self.dA_layers = []
self.params = []
self.n_layers = len(hidden_layers_sizes)

assertself.n_layers> 0

接下来,我们需要构建我们的 sigmoid 和 dA 层。我们从设置隐藏层的大小开始,可以从输入向量的大小开始,也可以通过激活前面的层来设置。接下来,创建sigmoid_layerdA_layer组件,dA 图层来自我们在本章前面讨论的dA类:

for i in xrange(self.n_layers):
   if i == 0:
      input_size = n_ins
   else:
      input_size = hidden_layers_sizes[i - 1]

if i == 0:
   layer_input = self.x
else:
   layer_input = self.sigmoid_layers[-1].output

sigmoid_layer = HiddenLayer(rng=numpy_rng, input=layer_input, n_in=input_size, n_out=hidden_layers_sizes[i], activation=T.nnet.sigmoid)

self.sigmoid_layers.append(sigmoid_layer)
self.params.extend(sigmoid_layer.params)

dA_layer = dA(numpy_rng=numpy_rng, theano_rng=theano_rng, input=layer_input, n_visible=input_size, n_hidden=hidden_layers_sizes[i], W=sigmoid_layer.W, bhid=sigmoid_layer.b)

self.dA_layers.append(dA_layer)

实现了堆叠 dA 的各层后,我们需要一个最终的逻辑回归层来完成网络的 MLP 部分:

self.logLayer = LogisticRegression(
   input=self.sigmoid_layers[-1].output,
   n_in=hidden_layers_sizes[-1],
   n_out=n_outs
)

self.params.extend(self.logLayer.params)
self.finetune_cost = self.logLayer.negative_log_likelihood(self.y)
self.errors = self.logLayer.errors(self.y)

这就完成了我们 SdA 的架构。接下来,我们需要生成SdA类使用的训练函数。每个功能都将迷你批次索引(index)作为一个参数,以及其他几个元素——这里启用了corruption_levellearning_rate,这样我们就可以在训练过程中调整它们(例如,逐渐增加或减少它们)。此外,我们还确定了有助于确定批次开始和结束位置的变量——batch_beginbatch_end:

动态调整学习速度的能力特别有帮助,可以通过两种方式之一来应用。一旦一种技术开始收敛到一个合适的解决方案,那么能够降低学习速率是非常有帮助的。如果你不这样做,你就有可能造成一种情况,即网络在位于最佳值附近的值之间振荡,而永远不会达到最佳值。在某些情况下,将学习率与网络的性能指标联系起来可能会有所帮助。如果错误率很高,在错误率开始降低之前进行较大的调整是有意义的!

def pretraining_functions(self, train_set_x, batch_size):
    index = T.lscalar('index')  
    corruption_level = T.scalar('corruption')  
    learning_rate = T.scalar('lr')  
    batch_begin = index * batch_size
    batch_end = batch_begin + batch_size

    pretrain_fns = []
    for dA in self.dA_layers:
        cost, updates = dA.get_cost_updates(corruption_level, learning_rate)
        fn = theano.function(
            inputs=[
                index,
                theano.Param(corruption_level, default=0.2),
                theano.Param(learning_rate, default=0.1)
            ],
            outputs=cost,
            updates=updates,
            givens={
                self.x: train_set_x[batch_begin: batch_end]
            }
         )
         pretrain_fns.append(fn)

    return pretrain_fns

我们创建的预处理函数采用迷你批处理index,并且可以选择采用损坏级别或学习速率。它执行预处理的一个步骤,并输出成本值和权重更新向量。

除了预处理之外,我们还需要构建函数来支持微调阶段,其中网络在验证和测试数据上迭代运行,以优化网络参数。下面代码中的训练函数(train_fn)实现了一步微调。valid_score是一个 Python 函数,使用SdA对验证数据产生的误差度量来计算验证分数。类似地,test_score计算测试数据的误差分数。

为了启动这个过程,我们首先需要建立训练、验证和测试数据集。每个阶段需要两个数据集(集合x和集合y),分别包含特征和类标签。确定验证和测试所需的小批次数量,并创建索引来跟踪批次大小(并提供识别批次开始和结束条目的方法)。每个批次都进行培训、验证和测试,之后所有批次都计算valid_scoretest_score:

def build_finetune_functions(self, datasets, batch_size,learning_rate):

   (train_set_x, train_set_y) = datasets[0]
   (valid_set_x, valid_set_y) = datasets[1]
   (test_set_x, test_set_y) = datasets[2]

   n_valid_batches = valid_set_x.get_value(borrow=True).shape[0]
   n_valid_batches /= batch_size
   n_test_batches = test_set_x.get_value(borrow=True).shape[0]
   n_test_batches /= batch_size

   index = T.lscalar('index')  

   gparams = T.grad(self.finetune_cost, self.params)

   updates = [
       (param, param - gparam * learning_rate)
       For param, gparam in zip(self.params, gparams)
]

train_fn = theano.function(
   inputs=[index],
   outputs=self.finetune_cost,
   updates=updates,
   givens={
      self.x: train_set_x[
         index * batch_size: (index + 1) * batch_size
      ],
      self.y: train_set_y[
         index * batch_size: (index + 1) * batch_size
      ]
   },
   name='train'
)

test_score_i = theano.function(
    [index],
   self.errors,
   givens={
      self.x: test_set_x[
      index * batch_size: (index + 1) * batch_size
   ],
      self.y: test_set_y[
      index * batch_size: (index + 1) * batch_size
   ]
},
   name='test'
)

valid_score_i = theano.function(
   [index],
   self.errors,
   givens={
      self.x: valid_set_x[
         index * batch_size: (index + 1) * batch_size
      ],
      self.y: valid_set_y[
         index * batch_size: (index + 1) * batch_size
      ]
   },
   name='valid'
)

def valid_score():
   return [valid_score_i(i) for i inxrange(n_valid_batches)]

def test_score():
   return [test_score_i(i) for i inxrange(n_test_batches)]

return train_fn, valid_score, test_score

有了培训功能,下面的代码将启动我们的堆栈式 dA:

numpy_rng = numpy.random.RandomState(89677)
print '... building the model'
   sda = SdA(
      numpy_rng=numpy_rng,
      n_ins=280,
      hidden_layers_sizes=[240, 170, 100],
      n_outs=5
   )

应该注意的是在这一点上,我们应该尝试层大小的初始配置,看看我们如何做。在这种情况下,使用的图层大小是一些初始测试的结果。正如我们所讨论的,训练SdA分两个阶段进行。第一个是逐层的预处理过程,它在所有SdA's层上循环。第二个是对验证和测试数据进行微调的过程。

为了对SdA进行预处理,我们提供了训练每一层所需的损坏级别,并使用我们之前定义的pretraining_fns对各层进行迭代:

print '... getting the pretraining functions'
pretraining_fns = sda.pretraining_functions(train_set_x=train_set_x,
batch_size=batch_size)

print '... pre-training the model'
start_time = time.clock()
corruption_levels = [.1, .2, .2]
for i in xrange(sda.n_layers):

   for epoch in xrange(pretraining_epochs):
      c = []
      for batch_index in xrange(n_train_batches):
         c.append(pretraining_fns[i](index=batch_index,
         corruption=corruption_levels[i],
         lr=pretrain_lr))
print 'Pre-training layer %i, epoch %d, cost ' % (i, epoch),

print numpy.mean(c)

end_time = time.clock()

print(('The pretraining code for file ' +
os.path.split(__file__)[1] + ' ran for %.2fm' % ((end_time - start_time) / 60.)), file = sys.stderr)

在这一点上,我们能够通过调用存储在本书的 GitHub 存储库中的前面的代码来初始化我们的SdA类:MasteringMLWithPython/Chapter3/SdA.py

评估 SdA 性能

SdA 将需要相当长的时间来运行。每层有 15 个时代,每层通常平均需要 11 分钟,网络将在现代桌面系统上运行大约 500 分钟,该系统具有图形处理器加速和单线程 GotoBLAS。

在没有 GPU 加速的系统上,网络将需要更长的时间来训练,建议您使用替代方案,该方案运行在明显更小的输入数据集上:MasteringMLWithPython/Chapter3/SdA_no_blas.py

结果质量较高,验证误差分数为 3.22%,测试误差分数为 3.14%。鉴于自然语言处理应用的模糊性和有时具有挑战性,这些结果尤其令人印象深刻。

值得注意的是,该网络对 1 星和 5 星评级案例的分类比中间级别更为正确。这在很大程度上是由于非极化或非情感语言的模糊性。

该输入数据可分类的部分原因是通过重要的特征工程。虽然耗时且有时存在问题,但我们已经看到,良好执行的功能工程与优化模型相结合,可以提供出色的准确性。在第 6 章文本特征工程中,我们将应用用于自己准备这个数据集的技术。

进一步阅读

谷歌大脑团队的 Quoc V. Le 提供了一个关于自编码器(以及其他主题)的全面概述。在https://cs.stanford.edu/~quocle/tutorial2.pdf阅读。

本章使用了在 http://deeplearning.net/tutorial/contents.html 可获得的音频文档作为讨论的基础,因为音频是本章中使用的主要库。

总结

在本章中,我们介绍了自编码器,这是一种有效的降维技术,具有一些独特的应用。我们关注的是栈式去噪自编码器背后的理论,它是自编码器的扩展,任何数量的自编码器都堆叠在一个深度架构中。我们能够将栈式去噪自编码器应用于一个具有挑战性的自然语言处理问题,并取得了巨大的成功,为酒店评论提供了高度准确的情感分析。

在下一章中,我们将讨论有监督的深度学习方法,包括卷积神经网络 ( CNN )。**

四、卷积神经网络

在本章中,您将通过以下步骤学习如何应用卷积神经网络(也称为 CNN 或 convnet),这可能是最著名的深度架构:

  • 看看 convnet 的拓扑和学习过程,包括卷积层和池层
  • 了解我们如何将 convnet 组件结合到成功的网络架构中
  • 使用 Python 代码应用 convnet 架构,以解决一个众所周知的图像分类任务

介绍美国有线电视新闻网

在机器学习领域,人们一直倾向于开发并行生物结构的代码结构。最明显的例子之一是 MLP 神经网络,它的拓扑结构和学习过程受到人脑神经元的启发。

事实证明,这种偏好非常有效;擅长特定任务集的专门优化生物结构的可用性为我们提供了丰富的模板和线索,从中可以设计和创建有效的学习模型。

卷积神经网络的设计灵感来自视觉皮层——大脑中处理视觉输入的区域。视觉皮层有几个特化,使它能够有效地处理视觉数据;它包含许多在视野重叠区域检测光的受体细胞。所有的受体细胞都受到相同的卷积运算,也就是说,它们都以相同的方式处理它们的输入。这些专门化被结合到 convnets 的设计中,使得它们的拓扑结构明显不同于其他神经网络。

可以有把握地说,美国有线电视新闻网(简称 convnets)是当前人工智能和机器学习领域许多最有影响力的进步的基础。美国有线电视新闻网的变体被应用于现有的一些最复杂的视觉、语言和解决问题的应用程序。一些例子包括:

  • 谷歌开发了一系列专门的 convnet 架构,包括 22 层 convnet 架构。此外,谷歌的 DeepDream 程序也使用了卷积神经网络,该程序因过度训练、致幻图像而闻名。
    *** 卷积网被教授玩游戏 Go (一个长期的人工智能挑战),对高排名玩家的胜率在 85%到 91%之间。* 脸书在人脸验证中使用卷积网(深度人脸)。* 百度、微软研究公司、国际商用机器公司和推特是使用 convnets 解决围绕试图交付下一代智能应用程序的挑战的许多其他团队之一。**

**近年来,物体识别挑战,如 2014 年 ImageNet 挑战,一直由采用专门 convnet 实现或结合 con vnet 与其他架构的多模型集成的赢家主导。

虽然我们将在第 8 章集成方法中介绍如何创建和有效应用集成,但本章重点介绍卷积神经网络在大规模视觉分类上下文中的成功应用。

了解 convnet 拓扑

卷积神经网络的体系结构应该相当熟悉;网络是一个非循环的图,由越来越少的节点组成的层组成,其中每一层都馈入下一层。这在许多著名的网络拓扑中是非常熟悉的,例如 MLP。

也许卷积神经网络和大多数其他网络之间最直接的区别是,convnet 中的所有神经元都是相同的!所有神经元拥有相同的参数和权重值。如您所见,这将立即减少由网络控制的参数值的数量,带来显著的效率节约。它通常还能提高网络学习率,因为需要管理和计算的自由参数更少。正如我们将在本章后面看到的,共享权重还使 convnet 能够学习特性,而不管它们在输入中的位置如何(例如,输入图像或音频信号)。

卷积网络和其他体系结构之间的另一个很大的区别是节点之间的连通性是有限的,例如开发空间局部连通性模式。换句话说,给定节点的输入将只限于那些受体场连续的节点。这可以是空间连续的,如在图像数据的情况下;在这种情况下,每个神经元的输入最终将从图像的连续子集中提取。在音频信号数据的情况下,输入可能是连续的时间窗口。

为了更清楚地说明这一点,让我们以一个输入图像为例,讨论卷积网络如何处理特定节点上的部分图像。卷积神经网络第一层的节点将被分配输入图像的子集。在这种情况下,假设他们每个人取图像的 3×3 像素子集。我们的覆盖范围覆盖了整个图像,在节点输入的区域之间没有任何重叠,也没有任何间隙。(请注意,对于 convnet 实现,这些条件都不是自动成立的。)每个节点被分配一个 3×3 像素的图像子集(节点的感受野),并输出该输入的变换版本。我们暂时不考虑这种转变的细节。

该输出通常由第二层节点接收。在这种情况下,假设我们的第二层从第一层的节点获取所有输出的子集。例如,它可能获取原始图像的连续 6×6 像素子集;也就是说,它有一个感受野,正好覆盖来自层之前的的四个节点的输出。当直观地解释时,这变得更加直观:

Understanding the convnet topology

每一层都是 可组合的;一个卷积层的输出可以作为输入馈送到下一层。这提供了与我们在第 3 章栈式去噪自编码器中看到的效果相同的效果;连续的层开发了越来越高级的抽象特征的表示。此外,随着我们向下构建——添加层——表示对更大的像素空间区域产生响应。最终,通过堆叠层,我们可以朝着整个输入的全局表示的方向努力。

理解卷积层

如上所述,为了防止每个节点学习一个不可预测的(而且很难调的!)设置非常局部的自由参数,层中的权重在整个层中共享。完全准确地说,卷积层中应用的滤波器是一组滤波器,它们在输入数据集中滑动(卷积)。这产生了输入的二维激活图,称为特征图。

过滤器本身受制于四个超参数:大小、深度、步幅和零填充。过滤器的大小是相当不言自明的,是过滤器的面积(显然,通过乘以高度和宽度找到;过滤器不必是方形的!).更大的过滤器往往会重叠更多,正如我们将看到的,这可以提高分类的准确性。然而,至关重要的是,增加滤波器尺寸将产生越来越大的输出。正如我们将看到的,管理卷积层的输出大小是控制网络效率的一个重要因素。

深度定义层中连接到输入的同一区域的节点数。理解深度的诀窍是认识到观察图像(对于人或网络)涉及处理多种不同类型的属性。任何看过 Photoshop 中所有图像调整滑块的人都知道这可能意味着什么。深度有时本身就是一个维度;它几乎与图像的复杂性有关,不是根据图像的内容,而是根据准确描述图像所需的通道数量。

深度有可能描述颜色通道,节点被映射以识别输入中的绿色、蓝色或红色。顺便说一句,这导致了深度被设置为 3(特别是在第一卷积层)的常见惯例。非常重要的是要认识到,一些节点通常学习表达输入图像的不太容易描述的属性,这恰好使 convnet 能够更准确地学习该图像。增加深度超参数有助于使节点能够编码更多关于输入的信息,以及随之而来的问题和好处。

因此,将深度参数设置为过小的值往往会导致较差的结果,因为网络不具备准确表征输入数据所需的表达深度(就通道数而言)。这是一个类似于没有足够功能的问题,只是它更容易修复;人们可以向上调整网络的深度,以提高 convnet 的表达深度。

同样,将深度参数设置为过小的值可能是多余的,或者对性能有害。如果有疑问,可以考虑在网络配置期间通过超参数优化、弯头方法或其他技术来测试适当的深度值。

步幅 是神经元间距的度量。步幅值为 1 将导致输入的每个元素(对于图像,可能是每个像素)成为过滤器实例的中心。这自然会导致高度重叠和非常大的产出。增加步幅会减少感受野的重叠,输出的大小也会减少。虽然调整 convnet 的步长是一个权衡精度与输出大小的问题,但使用较小的步长通常是个好主意,这样效果会更好。此外,步长值为 1 使我们能够在池层管理下采样和规模缩减(我们将在本章后面讨论)。

下图以图形方式显示了深度步幅:

Understanding convolution layers

最后一个超参数,零填充,提供了一个有趣的便利。零填充是将每个感受野的外部值(边界)设置为零的过程,其效果是减小该层的输出大小。可以将场边界周围的一个或多个像素设置为零,从而相应地减小输出大小。当然,也有限制;显然,设置零填充和跨步以使输入区域不被过滤器触摸不是一个好主意!更一般地说,增加零填充度会导致效率下降,这与通过粗编码学习特征的难度增加有关。(参见本章理解汇集层部分。)

然而,零填充非常有帮助,因为它使我们能够将输入和输出大小调整为相同。这是很常见的做法;使用零填充来确保输入层和输出层的大小相等,我们能够轻松管理步幅和深度值。如果不以这种方式使用零填充,我们将需要做大量的工作来跟踪输入大小和管理网络参数,以使网络正常运行。此外,零填充还可以提高性能,因为如果没有零填充,convnet 会逐渐降低滤波器边缘的内容质量。

当我们定义 convnet 时,为了校准连续层的节点数、适当的跨距和填充,我们需要知道前一层输出的大小。我们可以计算图层输出的空间大小( O )作为输入图像大小( W )、滤镜大小( F )、步幅( S )和应用的零填充量( P )的函数,如下所示:

Understanding convolution layers

如果 O 不是整数,过滤器不会整齐地平铺在输入上,而是延伸到输入的边缘。这可能会在训练时导致一些问题(通常涉及抛出异常)!通过调整步幅值,可以找到 O 的整数解,有效训练。在给定其他超参数值和输入大小的情况下,步幅被限制在可能的范围内是正常的。

我们已经讨论了正确配置卷积层所涉及的超参数,但是我们还没有讨论卷积过程本身。卷积是一种数学运算符,就像加法或求导一样,在信号处理应用和许多其他有助于简化复杂方程的应用中大量使用。

粗略地说,卷积是对两个函数的运算,例如产生第三个函数,它是两个原始函数之一的修改版本。对于 convnet 中的卷积,第一个分量是网络的输入。在卷积应用于图像的情况下,卷积应用于二维(图像的宽度和高度)。输入图像通常是三个像素矩阵,红色、蓝色和绿色通道各一个,每个通道的值介于 0 和中的 255 之间。

此时,值得引入一个 张量的概念。Tensor 是一个常用来指代输入数据的 n 维数组或矩阵的术语,通常应用于深度学习环境中。它实际上类似于矩阵或数组。我们将在本章和第 9 章附加 Python 机器学习工具中更详细地讨论张量(我们将在这里查看 张量流库)。值得注意的是,术语 tensor 正在注意到机器学习社区中使用的复苏,主要是通过谷歌机器智能研究团队的影响。

卷积运算的第二个输入是卷积核,它是一个浮点数矩阵,作为输入矩阵的过滤器。该卷积运算的输出是特征图。卷积运算的工作原理是在输入中滑动过滤器,在每个实例中计算两个参数的点积,并将其写入要素图。在卷积层的步距为 1 的情况下,该操作将在输入图像的每个像素上执行。

卷积的主要优点是减少了对特征工程的需求。创建和管理复杂的内核并执行所需的高度专业化的特征工程过程是一项艰巨的任务,由于在一种环境中工作良好的特征工程过程在大多数其他环境中可能工作不佳,这一任务变得更具挑战性。当我们在第 7 章特征工程第二部分中详细讨论特征工程时,卷积网提供了一个强大的替代方案。

然而,美国有线电视新闻网逐步提高他们的内核过滤给定输入的能力,从而自动优化他们的内核。这个过程是通过同时并行学习多个内核来加速的。这就是我们在前面章节中遇到的特性学习。特征学习可以在时间和增加许多问题的可访问性方面提供巨大的优势。与我们早期的 SDA 和 DBN 实现一样,我们希望将我们学习的特征传递给一个简单得多的浅层神经网络,该网络使用这些特征对输入图像进行分类。

了解汇集层

堆叠卷积层允许我们创建一个拓扑,该拓扑可以有效地为复杂、有噪声的输入数据创建作为特征图的特征。然而,卷积层不是深度网络的唯一组成部分。用汇集层编织卷积层是很常见的。汇集是对要素地图的操作,其中多个要素值被聚合为单个值,主要使用最大值(最大汇集)、平均值(平均值汇集)或求和(总和汇集)操作。

共享是一种相当自然的方法,它提供了巨大的优势。如果我们不聚合要素地图,我们往往会发现自己拥有大量的要素。本章稍后我们将分类的 CIFAR-10 数据集包含 60,000 张 32 x 32 像素的图像。如果我们假设学习了每幅图像的 200 个特征——超过 8×8 个输入——那么在每次卷积时,我们会发现自己的输出向量大小为(32–8+1)(32–8+1)* 200 个*,或者每幅图像的 125,000 个特征。卷积产生大量的特征,这些特征往往会使计算非常昂贵,并且还会引入严重的过拟合问题。

池操作提供的另一个主要优势是,它提供了一定程度的健壮性,可以应对建模高噪声、高维数据时出现的许多小偏差和差异。具体来说,池化防止网络过于具体地学习特征的位置(过拟合),这显然是图像处理和识别设置中的关键要求。有了池,网络不再关注输入中特征的精确位置,获得了更强的概括能力。这叫做平移不变性

最大池化是最常用的池化操作。这是因为它专注于所讨论的最具响应性的特征,理论上,这些特征应该使它成为图像识别和分类目的的最佳候选。通过类似的逻辑,最小池往往适用于需要采取额外步骤来防止过度敏感的分类或过度拟合发生的情况。

出于显而易见的原因,谨慎的做法是开始使用快速应用和直接的池化方法(如最大池化)进行建模。然而,当在以后的迭代中寻求网络性能的额外提高时,重要的是要看看您的池操作是否可以改进。在定义自己的池操作方面没有任何真正的限制。事实上,找到一种更有效的子采样方法或替代聚合可以大大提高模型的性能。

theano代码而言,最大池实现非常简单,可能如下所示:

from theano.tensor.signal import downsample

input = T.dtensor4('input')
maxpool_shape = (2, 2)
pool_out = downsample.max_pool_2d(input, maxpool_shape, ignore_border=True)
f = theano.function([input],pool_out)

max_pool_2d函数取 n 维tensor和降尺度因子,在本例中为inputmaxpool_shape,后者是长度的元组2,包含输入图像的宽度和高度降尺度因子。max_pool_2d操作然后在向量的两个尾部维度上执行最大池化:

invals = numpy.random.RandomState(1).rand(3, 2, 5, 5)

pool_out = downsample.max_pool_2d(input, maxpool_shape, ignore_border=False)
f = theano.function([input],pool_out)

ignore_border决定边界值是被考虑还是被丢弃。假设ignore_border = True,该最大池操作产生以下结果:

[[ 0.72032449  0.39676747]
[ 0.6852195   0.87811744]]

如您所见,池化是一个简单的操作,可以提供引人注目的结果(在本例中,输入是一个 5×5 的矩阵,简化为 2×2)。然而,汇集并非没有批评者。特别是,杰弗里·辛顿提供了这个非常令人愉快的声音:

“卷积神经网络中使用的汇集操作是一个很大的错误,它工作如此出色的事实是一场灾难。

如果池没有重叠,那么池会丢失关于东西在哪里的有价值的信息。我们需要这些信息来检测物体各部分之间的精确关系。确实,如果池足够重叠,特征的位置将通过“粗略编码”被精确地保留(参见我在 1986 年关于“分布式表示”的论文,以获得这种效果的解释)。但我不再相信粗略编码是表示对象相对于观察者的姿态的最佳方式(我所说的姿态是指位置、方向和比例)。”

这是一个大胆的声明,但它有意义。Hinton 告诉我们,作为一种聚合,池操作完成了任何聚合必须做的事情——它将数据简化为一种更简单、信息量更少的格式。这不会有太大的伤害,除了韩丁走得更远。

即使我们将每个池的数据减少到单个值,我们仍然可以希望多个池在空间上重叠的事实仍然会呈现特征编码。(这是韩丁提到的粗编码。)这也是相当直观的概念。想象一下,你正在收听一个嘈杂的无线电频率信号。即使你只听懂了三分之一的单词,你也有可能从航运预报中分辨出遇险信号!

然而,Hinton 接着观察到,粗编码在学习姿势(位置、方向和比例)方面没有那么有效。相对于一个对象,视点有如此多的排列,以至于两幅图像不太可能是相似的,而且各种各样的可能构成对使用池的卷积网络来说是一个挑战。这表明,不克服这一挑战的架构可能无法突破图像分类的上限。

然而,至少目前的普遍共识是,即使承认了所有这些,在效率和翻译不变性方面,继续在 convnets 中使用池操作仍然是非常有利的。现在,争论是这是我们最好的了!

与此同时,辛顿提出了一种转换自编码器的替代方案。转换自编码器为需要高精度的学习任务(如面部识别)提供了精度改进,在这种情况下,合并操作会导致精度降低。如果您有兴趣了解有关转换自编码器的更多信息,请阅读本章的进一步阅读部分。

因此,我们花了相当多的时间来研究卷积神经网络——它的组成部分,它们是如何工作的,以及它们的超参数。在我们继续将理论付诸行动之前,值得讨论一下如何将所有这些理论组成部分整合到一个工作架构中。为此,让我们讨论一下训练 convnet 是什么样子的。

训练 convnet

训练卷积网络的方法对于前面章节的读者来说是熟悉的。卷积架构本身用于预处理更简单的网络结构(例如,MLP)。反向传播算法是预处理时计算梯度的标准方法。在这个过程中,每一层承担三项任务:

  • 向前传递:每个特征图被计算为与相应权重核卷积的所有特征图的总和
  • 向后传递:通过相对于输出将转置的权重核与梯度进行卷积来计算输入的梯度
  • 计算每个内核的损失,根据需要调整每个内核的权重

重复这个过程可以让我们提高内核性能,直到达到一个收敛点。在这一点上,我们希望已经开发了一组特征,足以使覆盖网络能够有效地对这些特征进行分类。

即使在相当先进的图形处理器上,这个过程也可能执行缓慢。最近的一些发展有助于加速训练过程,包括使用快速傅立叶变换 来加速卷积过程(对于卷积核与输入图像大小大致相等的情况)。

把所有的放在一起

到目前为止,我们已经讨论了创建 CNN 所需的一些元素。下一个讨论主题应该是我们如何着手组合这些组件来创建有能力的卷积网络,以及哪些组件组合可以很好地工作。我们将从一些预先运行的 convnet 实现中获得指导,因为我们对通常做的事情以及可能做的事情有了一个了解。

可能最著名的卷积网络实现是 Yann LeCun 的 LeNet 。自从 1980 年末的 LeNet-1 以来,LeNet 已经经历了几次迭代,但是在执行包括手写数字和图像分类在内的任务方面越来越有效。LeNet 使用交替卷积和汇集层构建,由 MLP 覆盖,如下所示:

Putting it all together

每一层都是部分连接的,正如我们之前讨论的,MLP 是一个完全连接的层。在每一层,使用多个特征图(通道);这给了我们能够创建更复杂的过滤器组的优势。正如我们将看到的,在一个层中使用多个通道是高级用例中使用的一种强大的技术。

通常使用最大池层来降低输出的维度以匹配输入,并通常管理输出量。如何实现池化,特别是关于卷积层和池化层的相对位置,是一个在不同实现之间往往会有所不同的因素。通常情况下,将一个层开发为一组操作,这些操作馈入并被馈入单个 完全连接的层,如下例所示:

Putting it all together

虽然这种网络结构在实践中行不通,但它很好地说明了一个事实,即网络可以通过多种方式由您所了解的组件构建而成。这个网络是如何构建的,它变得有多复杂,应该由网络要解决的挑战来驱动。不同的问题需要截然不同的解决方案。

对于我们将在本章后面讨论的 LeNet 实现,每一层都包含多个并行的卷积层,每个卷积层后面都有一个最大池层。示意性地,LeNet 层如下图所示:

Putting it all together

这种架构将使我们能够快速轻松地开始查看一些初始用例,但是一般来说,对于我们将在本书后面遇到的一些最先进的应用程序来说,这种架构的性能并不好。鉴于这一事实,有一些更广泛的深度学习架构设计来解决最具挑战性的问题,其拓扑值得讨论。最著名的 convnet 架构之一是谷歌的 【盗梦空间】网络,现在更普遍地被称为谷歌网。

GoogLeNet 旨在应对涉及互联网质量图像数据的计算机视觉挑战,即在真实环境中捕获的图像,其中图像的姿态、光照、遮挡和杂乱变化很大。GoogLeNet 应用于 2014 年 ImageNet 挑战赛取得了值得注意的成功,在测试数据集上仅实现了 6.7%的错误率。ImageNet 图像是小的、高粒度的图像,取自许多不同的类。多个类可能看起来非常相似(例如树的变种),网络架构必须能够找到越来越具有挑战性的类区别才能成功。举一个具体的例子,考虑下面的 ImageNet 图像:

Putting it all together

考虑到这个问题的需求,用于赢得 ImageNet 14 的 GoogLeNet 架构在几个关键方面偏离了 LeNet 模型。谷歌网的基本层设计被称为初始模块,由以下组件组成:

Putting it all together

这里使用的 1×1 卷积层后面是整流线性单元 ( ReLU )。这种方法在语音和音频建模环境中大量使用,因为 ReLU 可以用来有效地训练深度模型,而无需预处理,也不会面临一些挑战其他激活类型的梯度消失问题。有关 ReLU 的更多信息,请参见本章的进一步阅读部分。 DepthConcat 元素提供了一个连接函数,该函数合并了多个单元的输出,大大提高了训练时间。

GoogLeNet 将这种类型的层链接起来,以创建一个完整的网络。的确,盗梦空间模块通过 GoogLeNet 的重复(九次!)表明网络中的网络中的 ( NIN )(由链式网络模块创建的深度架构)方法将继续成为深度学习领域的有力竞争者。本章的“进一步阅读”部分提供了描述谷歌网络并演示初始模型如何集成到网络中的论文。

除了盗梦空间模块堆叠的规律性之外,谷歌网还有一些其他的惊喜要给我们。前几层通常更简单,首先使用单通道卷积和最大池层。此外,在几个点上,谷歌网引入了一个使用平均池层的主结构的分支,馈入辅助 softmax 分类器。这些分类器的目的是改善在网络低层传播回来的梯度信号,从而在早期和中期网络层实现更强的性能。谷歌网没有一个庞大且潜在模糊的反向传播过程,而是有几个中间更新源。

从这个实现中真正重要的是,GoogLeNet 和其他顶级 convnet 架构主要是成功的,因为它们能够使用我们在本章中讨论的高可用性组件找到有效的配置。既然我们已经有机会讨论了卷积网络的体系结构和组件,并有机会讨论如何使用这些组件来构建一些高度先进的网络,现在是时候应用这些技术来解决我们自己的问题了!

申请美国有线电视新闻网

我们将使用图像数据来测试我们的 convnet。我们在前面几章中处理的图像数据,包括 MNIST 数字数据集,是一个有用的训练数据集(具有许多有价值的现实应用,如自动支票读取!).然而,它在一个重要方面不同于几乎所有的照片或视频数据;大多数视觉数据都非常嘈杂。

问题变量可以包括姿势、光照、遮挡和杂乱,这些变量可以独立表达,也可以大量组合表达。这意味着创建一个对数据集中所有噪声属性都不变的函数的任务具有挑战性;该函数通常非常复杂和非线性。在第 7 章特征工程第二部分中,我们将讨论美白等技术如何帮助缓解其中一些挑战,但正如我们将看到的,即使是这样的技术本身也不足以产生良好的分类(至少,不需要非常大的时间投入!).到目前为止,图像数据中噪声问题的最有效解决方案,正如我们已经在多个上下文中看到的,是使用深度架构,而不是广泛的架构(也就是说,具有很少的高维层的神经网络,容易出现有问题的过拟合和泛化问题)。

从前面几章的讨论中,深层架构的原因可能已经很清楚了;深层体系结构的连续层重用前面层中执行的推理和计算。因此,深度体系结构可以构建一个由网络的连续层顺序改进的表示,而无需对任何单个层执行大量的重新计算。这使得在没有大量特征工程的情况下,在相对短的时间内以高精度实现对大数据集的噪声照片数据进行分类的挑战性任务。

既然我们已经讨论了建模图像数据的挑战以及在这种环境下深度架构的优势,让我们将 convnet 应用于现实世界的分类问题。

和前面的章节一样,我们将从一个玩具示例开始,我们将使用它来熟悉我们的深层网络的架构。这一次,我们将接受一个经典的图像处理挑战,CIFAR-10。CIFAR-10 是一个由 10 类 60,000 个 32 x 32 彩色图像组成的数据集,每个类包含 6,000 个图像。数据已经分成五个训练批次,一个测试批次。每个数据集中的类和一些图像如下:

Applying a CNN

虽然在某种程度上,该行业已经着手处理其他数据集,如 ImageNet,但长期以来,CIFAR-10 一直被认为是图像分类方面的障碍,许多数据科学家试图创建将数据集分类到人类精确水平的体系结构,其中人类错误率估计约为 6%。

2014 年 11 月,卡格尔举办了一场竞赛,其目标是尽可能准确地对 CIFAR-10 进行分类。本次比赛的最高得分条目产生了 95.55%的分类准确率,结果使用了卷积网络和网络中的网络方法。我们将在第 8 章集成方法中讨论对该数据集进行分类的挑战,以及我们可以采用的一些更先进的技术;现在,让我们从卷积网络的分类开始。

对于我们的第一次尝试,我们将应用一个相当简单的卷积网络,目标如下:

  • 对图像应用滤镜并查看输出
  • 看到我们的转换创造的重量
  • 理解有效和无效网络的输出之间的差异

在这一章中,我们将采取一种我们以前没有采取过的方法,当你在野外使用这些技术时,这将对你非常重要。我们在本章前面看到了为解决不同问题而开发的深度体系结构在许多方面可能存在结构差异。

能够创建特定于问题的网络体系结构非常重要,这样我们就可以调整我们的实现来适应一系列现实问题。为了做到这一点,我们将使用模块化的组件来构建我们的网络,这些组件可以以几乎任何必要的方式进行重组,而无需太多额外的努力。我们在本章前面已经看到了模块化的影响,如何将这种影响应用到我们自己的网络中是值得探索的。

正如我们在本章前面所讨论的那样,当任务是对多达数万或数十万张图像的非常大且各种各样的数据集进行分类时,convnets 变得特别强大。因此,让我们有点雄心勃勃,看看我们是否可以应用 convnet 来对 CIFAR-10 进行分类。

在建立卷积网络时,我们将首先定义一个可用的类,并初始化相关的网络参数,特别是权重和偏差。这种方法对于前面章节的读者来说是熟悉的。

class LeNetConvPoolLayer(object):

    def __init__(self, rng, input, filter_shape, image_shape,   
    poolsize=(2, 2)):

        assert image_shape[1] == filter_shape[1]
        self.input = input

        fan_in = numpy.prod(filter_shape[1:])
        fan_out = (filter_shape[0] * numpy.prod(filter_shape[2:])                               
                  numpy.prod(poolsize))

        W_bound = numpy.sqrt(6\. / (fan_in + fan_out))
        self.W = theano.shared(
            numpy.asarray(
                rng.uniform(low=-W_bound, high=W_bound, 
                size=filter_shape),
                dtype=theano.config.floatX
            ),
            borrow=True
        )

在进入创造偏见之前,值得回顾一下我们目前所掌握的情况。LeNetConvPoolLayer类旨在根据 LeNet 层结构实现一个完整的卷积和池层。这个类包含几个有用的初始参数。

从前面的章节中,我们熟悉了用于将权重初始化为随机值的rng参数。我们也可以识别input参数。在大多数情况下,图像输入往往采取符号图像张量的形式。该图像输入由image_shape参数进行整形;这是描述输入维度的长度为 4 的元组或列表。当我们穿过连续的层时,image_shape会逐渐减少。作为一个元组,image_shape的维度只是指定输入的高度和宽度。作为长度 4 的列表,参数按顺序如下:

  • 批次大小
  • 输入要素图的数量
  • 输入图像的高度
  • 输入图像的宽度

image_shape指定输入的大小,filter_shape指定过滤器的尺寸。作为长度 4 的列表,参数按顺序如下:

  • 要应用的过滤器(通道)数量
  • 输入要素图的数量
  • 过滤器的高度
  • 过滤器的宽度

然而,高度和宽度可以在没有任何附加参数的情况下输入。这里的最后一个参数poolsize描述了缩小系数。这表示为长度为 2 的列表,第一个元素是行数,第二个元素是列数。

定义了这些值之后,我们立即应用它们来更好地定义LeNetConvPoolLayer类。在定义fan_in时,我们将每个隐藏单元的输入设置为输入要素图数量的倍数,即过滤器高度和宽度。简单来说,我们还定义了fan_out,一个梯度,它被计算为输出要素地图数量(要素高度和宽度)除以池大小的倍数。

接下来,我们继续将偏差定义为一组一维张量,每个一维张量对应一个输出特征图:

        b_values = numpy.zeros((filter_shape[0],),  
        dtype=theano.config.floatX)
        self.b = theano.shared(value=b_values, borrow=True)

        conv_out = conv.conv2d(
            input=input,
            filters=self.W,
            filter_shape=filter_shape,
            image_shape=image_shape
        )

通过这个函数调用,我们定义了一个卷积运算,它使用了我们之前定义的过滤器。有时,看到需要知道多少理论才能有效地应用单个函数,这可能有点令人吃惊!下一步是使用max_pool_2d创建类似的池操作:

        pooled_out = downsample.max_pool_2d(
            input=conv_out,
            ds=poolsize,
            ignore_border=True
        )

        self.output = T.tanh(pooled_out + self.b.dimshuffle('x', 
                      0, 'x', 'x'))

        self.params = [self.W, self.b]

        self.input = input

最后,我们加入偏置项,首先将其重塑为形状张量(1n_filters11)。这具有简单的效果,导致偏差影响每个要素地图和迷你地图。此时,我们已经拥有了构建基本 convnet 所需的所有组件。让我们继续创建自己的网络:

    x = T.matrix('x')   
    y = T.ivector('y') 

这个过程相当简单。我们按顺序构建层,将参数传递给我们之前指定的类。让我们从构建第一层开始:

    layer0_input = x.reshape((batch_size, 1, 32, 32))

    layer0 = LeNetConvPoolLayer(
        rng,
        input=layer0_input,
        image_shape=(batch_size, 1, 32, 32),
        filter_shape=(nkerns[0], 1, 5, 5),
        poolsize=(2, 2)
    )

我们从重塑输入开始,将其扩展到所有预期的迷你批次。由于 CIFAR-10 图像的尺寸为 32 x 32,因此我们将此输入尺寸用于高度和宽度尺寸。过滤过程将每个维度的输入大小减少到 32- 5+1,即 28。汇集在每个维度中减少一半,以创建形状的输出层(batch_size, nkerns[0], 14, 14)

这是一个完整的第一层。接下来,我们可以使用相同的代码在上面附加第二层:

    layer1 = LeNetConvPoolLayer(
        rng,
        input=layer0.output,
        image_shape=(batch_size, nkerns[0], 14, 14),
        filter_shape=(nkerns[1], nkerns[0], 5, 5),
        poolsize=(2, 2)
    )

按照之前的图层,该图层的输出形状为(batch_size, nkerns[1], 5, 5))。到目前为止,一切顺利!让我们将此输出馈入下一个完全连接的 sigmoid 层。首先,我们需要将输入形状展平为二维。根据我们到目前为止输入网络的值,输入将是一个形状矩阵 (500,1250) 。因此,我们将设置一个合适的layer2:

    layer2_input = layer1.output.flatten(2)

    layer2 = HiddenLayer(
        rng,
        input=layer2_input,
        n_in=nkerns[1] * 5 * 5
        n_out=500,
        activation=T.tanh
    )

这使我们处于一个很好的位置来完成这个网络的架构,通过添加一个最终的逻辑回归层来计算完全连接的 sigmoid 层的值。

让我们试试这段代码:

    x = T.matrix(CIFAR-10_train)   
    y = T.ivector(CIFAR-10_test)

Chapter_4/convolutional_mlp.py

我们获得的结果如下:

Optimization complete.
Best validation score of 0.885725 % obtained at iteration 17400, with test performance 0.902508 %
The code for file convolutional_mlp.py ran for 26.50m

这个准确度分数在验证时相当不错。这不是人类水平的准确性,正如我们所确定的,大约是 94%。同样,这不是我们用 convnet 能达到的最好分数。

例如,本章的进一步阅读部分提到了在 Torch 中实现的 convnet,该 conv net 使用了 dropout(我们在第 3 章栈式去噪自编码器)和 批处理归一化的组合(一种旨在减少训练过程中协变量漂移的归一化技术;关于这项技术的进一步技术说明和论文,请参考进一步阅读部分),验证准确率为 92.45%。

然而,88.57%的得分也在这个范围内,这可以让我们相信,我们距离解决 CIFAR-10 问题的有效网络架构已经不远了。更重要的是,你已经学到了很多关于如何有效地配置和训练卷积神经网络的知识。

进一步阅读

最近对卷积网络的兴趣过剩意味着我们被宠坏了,无法选择进一步阅读。对于不熟悉的读者来说,一个很好的选择是安德烈·卡普西的课程笔记:http://cs231n.github.io/convolutional-networks/

对于对特定同类最佳实施的更深入细节感兴趣的读者,本章中提到的一些网络如下:

谷歌的谷歌网(http://www.cs.unc.edu/~wliu/papers/GoogLeNet.pdf)

谷歌深度思维的围棋程序 alpha Go(https://gogameguru.com/i/2016/03/deepmind-mastering-go.pdf)

脸书面部识别的深度人脸架构

ImageNet LSVRC-2010 竞赛获奖网络,这里由 Krizhevsky、Sutskever 和 hint on(http://www.cs.toronto.edu/~fritz/absps/imagenet.pdf)描述

最后,Sergey Zagoruyko 的带有批处理规范化的 ConvNet 的 Torch 实现可以在这里获得:http://torch.ch/blog/2015/07/30/cifar.html

总结

在这一章中,我们涉及了很多方面。我们首先介绍了一种新的神经网络,convnet。我们以最普遍的形式探索了 convnet 的理论和架构,并讨论了一些最先进的网络设计原则,这些原则直到 2015 年年中才在谷歌和百度等组织中得到发展。我们建立了对拓扑以及网络运行方式的理解。

在此之后,我们开始使用 convnet 本身,将其应用于 CIFAR-10 数据集。我们使用模块化 convnet 代码创建了一个功能架构,在对 10 类图像数据进行分类时达到了合理的精度水平。虽然我们肯定离人类的准确性水平还有一段距离,但我们正在逐渐缩小差距!第八章合奏法将从你在这里学到的东西中吸取,将这些技巧及其应用提升到一个新的水平。**

五、半监督学习

简介

在前几章中,我们已经使用先进的技术解决了一系列数据挑战。在每种情况下,我们都成功地将我们的技术应用于数据集。

然而,在许多方面,我们已经很容易了。我们的数据很大程度上来自规范的和准备充分的来源,所以我们不必做大量的准备。然而,在现实世界中,很少有这样的数据集(也许除了那些我们自己能够指定的数据集!).特别是,很少也不可能在野外遇到有类标签的数据集。如果数据集的足够部分没有标签,我们发现自己无法构建一个能够准确预测验证或测试数据标签的分类器。那么,我们该怎么办?

常见的解决方案是尝试手动标记我们的数据;这不仅耗时,而且还会遭受某些类型的人为错误(这在高维数据集上尤其常见,因为人类观察者无法像计算方法那样识别类边界)。

一种相当新且相当令人兴奋的替代方法是使用半监督学习 通过捕捉底层分布的形状,将标签应用于未标记的数据。半监督学习在过去十年中因其能够节省大量注释时间而越来越受欢迎,如果可能的话,注释可能需要人工专业知识或专业设备。这被证明特别有价值的背景是自然语言分析和语音信号分析;在这两个领域,人工注释都被证明是复杂和耗时的。

在本章中,您将学习如何应用几种半监督学习技术,包括:对比悲观似然估计 ( CPLE )、自我学习和 S3VM。这些技术将使我们能够在一系列有问题的环境中标记训练数据。您将学会识别半监督技术的能力和局限性。我们将使用最近在 scikit 之上开发的一些 Python 库——学习将半监督技术应用于几个用例,包括音频信号数据。

我们开始吧!

理解半监督学习

执行机器学习最持久的成本是为训练目的创建标记数据。由于情况的循环性,数据集往往不带有提供的类标签;人们需要一种经过训练的分类技术来生成类标签,但是如果没有经过标记的训练和测试数据,就无法训练这种技术。如上所述,手动或通过测试过程标记数据是一种选择,但这可能非常耗时、昂贵(尤其是对于医学测试)、难以组织并且容易出错(对于大型或复杂的数据集)。半监督技术提出了打破这种僵局的更好方法。

半监督学习技术使用未标记和标记的数据来创建比单独使用未标记或标记的数据更好的学习技术。有一系列技术存在于有监督(有标记数据)和无监督(无标记数据)学习之间。

该组中存在的主要技术类型是半监督技术、转导技术和主动学习技术,以及一系列其他方法。

半监督技术将一组测试数据留在训练过程之外,以便在稍后阶段执行测试。与此同时,转导技术纯粹是为了给未标记的数据开发标签。转换技术中可能没有嵌入测试过程,也可能没有可使用的标记数据。

在本章中,我们将重点介绍一组半监督技术,这些技术以非常熟悉的格式提供强大的数据集标注功能。我们将要讨论的许多技术可用作熟悉的、预先存在的分类器的包装器,从线性回归分类器到支持向量机。因此,它们中的许多可以使用来自 Scikit-learn 的估计器来运行。我们将首先将线性回归分类器应用于测试用例,然后继续应用带有半监督扩展的 SVM。

半监督算法在起作用

我们已经讨论了什么是半监督学习,为什么我们要参与其中,以及使用半监督算法的一些普遍现实是什么。我们已经尽我们所能做了大概的描述。在接下来的几页中,我们将从这个一般的理解开始,发展有效使用半监督应用程序的能力。

自我训练

自我训练是最简单的半监督学习方法,也可以是最快的。自我训练算法在多种环境中看到一个应用,包括自然语言处理和计算机视觉;正如我们将看到的,它们既能带来巨大的价值,也能带来巨大的风险。

自我训练的目标是将来自未标记案例的信息与已标记案例的信息相结合,以迭代地为数据集的未标记示例识别标签。在每次迭代中,标记的训练集都会被放大,直到整个数据集都被标记。

自训练算法通常用作基础模型的包装器。在本章中,我们将使用 SVM 作为自我训练模型的基础。自训练算法非常简单,包含的步骤非常少,如下所示:

  1. 一组标记数据用于预测一组未标记数据的标签。(这可能是所有未标记的数据或部分数据。)
  2. 为所有新标记的病例计算置信度。
  3. 从新标记的数据中选择案例,为下一次迭代保留。
  4. 该模型在所有标记的案例上进行训练,包括在以前的迭代中选择的案例。
  5. 模型迭代到步骤 1 到 4,直到成功收敛。

该过程以图形方式呈现,如下所示:

Self-training

完成培训后,将对自我培训的模型进行测试和验证。这可以通过交叉验证来完成,如果存在的话,甚至可以使用保留的、标记的数据。

自我训练提供真正的力量和时间节省,但也是一个有风险的过程。为了理解要注意什么,以及如何将自我训练应用到自己的分类算法中,让我们更详细地看看算法是如何工作的。

为了支持这个讨论,我们将使用来自 semi up-learn GitHub 存储库的代码。为了使用这些代码,我们需要克隆相关的 GitHub 存储库。相关说明见附录 A

实施自我培训

自我训练的每个迭代的第一步是为未标记的案例生成类标签。这是通过首先创建一个SelfLearningModel类来实现的,该类以基本监督模型(basemodel)和迭代限制作为参数。正如我们将在本章后面看到的,迭代极限可以作为分类精度(即收敛性)的函数来明确指定或提供。prob_threshold参数为标签验收提供了最低质量标准;分数低于此级别的任何预计标签都将被拒绝。同样,我们将在后面的示例中看到,除了提供硬编码阈值,还有其他选择。

class SelfLearningModel(BaseEstimator): 

def __init__(self, basemodel, max_iter = 200, prob_threshold = 0.8): 
   self.model = basemodel 
   self.max_iter = max_iter 
   self.prob_threshold = prob_threshold  

定义了SelfLearningModel类的外壳后,下一步是为半监督模型拟合过程定义函数:

def fit(self, X, y): 
   unlabeledX = X[y==-1, :] 
   labeledX = X[y!=-1, :] 
   labeledy = y[y!=-1] 

   self.model.fit(labeledX, labeledy) 
   unlabeledy = self.predict(unlabeledX) 
   unlabeledprob = self.predict_proba(unlabeledX)
   unlabeledy_old = [] 

   i = 0

X参数是输入数据的矩阵,其形状相当于[n_samples, n_features]X用于创建[n_samples, n_samples]大小的矩阵。与此同时,y参数是一系列标签。未标记的点在y中标记为-1。从X开始,unlabeledXlabeledX参数通过在X上选择X中位置对应于y-1标签的元素的操作而非常简单地创建。labeledy参数执行与y类似的选择。(自然,我们对y的未标记样本作为变量没那么感兴趣,但是我们需要确实存在的标签来进行分类尝试!)

标签预测的实际过程是这样实现的:首先,使用 sklearn 的预测操作。使用 sklearn 的predict方法生成unlabeledy参数,而predict_proba方法用于计算每个投影标签的概率。这些概率存储在unlabeledprob中。

Scikit-learn 的predictpredict_proba方法分别用于predict类标签和类标签正确的概率。因为我们将在几个半监督算法中应用这两种方法,所以了解它们实际上是如何工作的会很有帮助。

predict方法为输入数据生成类预测。它通过一组二进制分类器(即,试图只区分两个类的分类器)来实现这一点。具有 n 个多类的完整模型包含一组二元分类器,如下所示:

Implementing self-training

为了对一个给定的案例进行预测,所有分数超过零的分类器都投票选出一个应用于该案例的类别标签。具有最多票数(而不是最高和分类器分数)的类被识别。这被称为一对一预测方法,是一种相当常见的方法。

与此同时,predict_proba通过调用 普拉特校准来工作,这是一种允许将分类模型的输出转换为类的概率分布的技术。这包括首先训练有问题的基础模型,将回归模型拟合到分类器的分数:

Implementing self-training

然后可以使用最大似然法优化该模型(通过标量参数 AB )。就我们的自训练模型而言,predict_proba允许我们将回归模型拟合到分类器的分数,从而计算每个类别标签的概率。这非常有帮助!

接下来,我们需要一个循环进行迭代。下面的代码描述了一个while循环,该循环一直执行到unlabeledy_old(T2 的副本)中没有剩余案例,或者直到达到最大迭代次数。在每次迭代中,对于没有概率超过概率阈值(prob_threshold)的标签的每种情况进行标记尝试:

   while (len(unlabeledy_old) == 0 or       
      numpy.any(unlabeledy!=unlabeledy_old)) and i < self.max_iter: 
      unlabeledy_old = numpy.copy(unlabeledy) 
      uidx = numpy.where((unlabeledprob[:, 0] > self.prob_threshold)    
      | (unlabeledprob[:, 1] > self.prob_threshold))[0] 

self.model.fit方法然后尝试将模型拟合到未标记的数据。这些未标记的数据以尺寸矩阵[n_samples, n_samples]的形式呈现(如本章前面所述)。该矩阵是通过附加(带有vstackhstack)未标记的案例而创建的:

      self.model.fit(numpy.vstack((labeledX, unlabeledX[uidx, :])),     
      numpy.hstack((labeledy, unlabeledy_old[uidx])))

最后,迭代执行标签预测,然后是这些标签的概率预测。

      unlabeledy = self.predict(unlabeledX) 
      unlabeledprob = self.predict_proba(unlabeledX) 
      i += 1 

在下一次迭代中,模型将执行相同的过程,这次将概率预测超过阈值的新标记的数据作为model.fit步骤中使用的数据集的一部分。

如果一个人的模型还没有包括一个可以生成标签预测的分类方法(像 sklearn 的 SVM 实现中可用的predict_proba方法),那么引入一个是可能的。以下代码检查predict_proba方法,如果找不到该方法,则引入生成标签的Platt scaling:

if not getattr(self.model, "predict_proba", None): 
   self.plattlr = LR() 
   preds = self.model.predict(labeledX) 
   self.plattlr.fit( preds.reshape( -1, 1 ), labeledy ) 

return self

def predict_proba(self, X): 
         if getattr(self.model, "predict_proba", None): 
         return self.model.predict_proba(X) 
         else: 
            preds = self.model.predict(X) 
            return self.plattlr.predict_proba(preds.reshape( -1, 1 ))

一旦我们有了这么多,我们就可以开始应用我们的自我训练架构。为此,让我们获取一个数据集并开始工作!

对于这个例子,我们将使用一个简单的线性回归分类器,以随机梯度下降 ( SGD )作为我们的学习组件作为我们的基础模型(basemodel)。输入数据集将是 statlog heart数据集,从www.mldata.org获得。本章随附的 GitHub 存储库中提供了该数据集。

heart数据集是一个两类数据集,其中类是心脏病的存在与否。它的 13 个特性中的任何一个在 270 个案例中都没有缺失值。这些数据是无标签的,许多需要的变量通常是通过昂贵且有时不方便的测试获得的。变量如下:

  • age
  • sex
  • chest pain type (4 values)
  • resting blood pressure
  • serum cholestoral in mg/dl
  • fasting blood sugar > 120 mg/dl
  • resting electrocardiographic results (values 0,1,2)
  • maximum heart rate achieved
  • exercise induced angina
  • 10\. oldpeak = ST depression induced by exercise relative to rest
  • the slope of the peak exercise ST segment
  • number of major vessels (0-3) colored by flourosopy
  • thal: 3 = normal; 6 = fixed defect; 7 = reversable defect

让我们从Heart数据集开始,加载数据,然后拟合一个模型:

heart = fetch_mldata("heart")
X = heart.data
ytrue = np.copy(heart.target)
ytrue[ytrue==-1]=0

labeled_N = 2
ys = np.array([-1]*len(ytrue)) # -1 denotes unlabeled point
random_labeled_points = random.sample(np.where(ytrue == 0)[0], labeled_N/2)+\random.sample(np.where(ytrue == 1)[0], labeled_N/2)
ys[random_labeled_points] = ytrue[random_labeled_points]

basemodel = SGDClassifier(loss='log', penalty='l1') 

basemodel.fit(X[random_labeled_points, :], ys[random_labeled_points])
print "supervised log.reg. score", basemodel.score(X, ytrue)

ssmodel = SelfLearningModel(basemodel)
ssmodel.fit(X, ys)
print "self-learning log.reg. score", ssmodel.score(X, ytrue)

尝试这种方法会产生中等但不出色的结果:

self-learning log.reg. score 0.470347

然而,在 1000 多次试验中,我们发现我们的输出质量差异很大:

Implementing self-training

考虑到我们正在查看真实世界和未标记数据集的分类准确率分数,这并不是一个可怕的结果,但我认为我们不应该对此感到满意。我们仍有一半以上的病例标签不正确!

我们需要更好地理解这个问题;目前,还不清楚哪里出了问题,也不清楚我们如何改善我们的结果。让我们通过回到自我训练的理论来解决这个问题,以了解我们如何诊断和改进我们的实现。

完善你的自我训练实施

在前一节中,我们讨论了自我训练算法的创建,并尝试了一个实现。然而,我们在第一次试验中看到的是,我们的结果,虽然展示了自我训练的潜力,但留下了成长的空间。我们结果的准确性和差异都值得怀疑。

自我训练可能是一个脆弱的过程。如果算法的某个元素配置不当或输入数据包含特性,迭代过程很可能会失败一次,并通过将错误标记的数据重新引入未来的标记步骤而继续加剧该错误。随着自训练算法的不断迭代,垃圾进入和垃圾排出是一个非常现实的问题。

有几种常见的风险类型应该被指出。在某些情况下,带标签的数据可能不会添加更多有用的信息。这在最初的几次迭代中尤其常见,这是可以理解的!一般来说,最容易标记的未标记案例是与现有已标记案例最相似的案例。然而,虽然为这些情况生成高概率标签很容易,但不能保证将它们添加到标签集中会使后续迭代中的标签更容易。

不幸的是,这种有时会导致添加对分类没有实际影响的案例,而分类精度通常会下降。更糟糕的是,添加与预先存在的案例在足够多的方面相似的案例,以使它们易于标记,但这实际上误导了分类器的决策边界,会导致错误分类的增加。

诊断自我训练模型出了什么问题有时会很困难,但一如既往,一些精心选择的情节会让情况变得更加清晰。由于这种类型的错误在最初的几次迭代中出现得特别频繁,因此只需在标签预测循环中添加一个元素来写入当前的分类精度,就可以让我们了解精度在早期迭代中是如何变化的。

一旦发现问题,就有一些可能的解决方案。如果存在足够多的标记数据,一个简单的解决方案是尝试使用一组更多样的标记数据来启动这个过程。

虽然冲动可能是使用所有标记的数据,但我们将在本章后面看到,自训练模型容易过度拟合——这种风险迫使我们保留一些数据用于验证目的。一个有前途的选择是使用我们数据集的多个子集来训练多个自训练模型实例。这样做,特别是在几次试验中,可以帮助我们理解输入数据对自我训练模型性能的影响。

第 8 章集成方法中,我们将探索一些围绕集成的选项,这些选项将使我们能够一起使用多个自训练模型来产生预测。当我们可以使用集合时,我们甚至可以考虑并行应用多种采样技术。

如果我们不想用数量来解决这个问题,也许我们可以通过提高质量来解决。一种解决方案是通过选择来创建适当多样的标记数据子集。有标签的案例的数量没有硬性限制,作为启动自我培训实施的最低数量。虽然你可以假设每节课只使用一个标记的案例(就像我们在前面的训练示例中所做的那样),但是很快就会明白,针对更多样化和重叠的一组课进行训练会受益于更多的标记数据。

自我训练模型特别容易出现的另一类错误是有偏见的选择。我们天真的假设是,在每一次迭代过程中,数据的选择在最坏的情况下只是略微有偏差(只比其他类稍微偏向一类)。现实是,这不是一个安全的假设。有几个因素可以影响有偏见的选择的可能性,最有可能的罪魁祸首是来自一个类别的不成比例的抽样。

如果数据集作为一个整体,或者使用的标记子集偏向于一个类,那么你的自训练分类器会过度匹配的风险就会增加。这只会使问题复杂化,因为为下一次迭代提供的案例可能不够多样,不足以解决问题;无论自训练算法设置了什么不正确的决策边界,它都将被设置在原来的位置——覆盖到数据的一个子集。每个类的病例数之间的数字差异是这里的主要症状,但是发现过度拟合的更常见的方法也有助于诊断围绕选择偏差的问题。

这种对发现过度拟合的常用方法的参考值得进一步扩展,因为识别过度拟合的技术非常有价值!这些技术通常被称为验证技术。支撑验证技术的基本概念是,一个有两组数据——一组用于构建模型,另一组用于测试模型。

最有效的验证技术是独立验证,最简单的形式是等待确定预测是否准确。这显然不总是(或者甚至经常)可能的!

鉴于不可能进行独立验证,最好的办法是保留样本的一个子集。这被称为样本分裂,是现代验证技术的基础。大多数机器学习实现涉及训练、测试和验证数据集;这是一个多层次验证的案例。

第三个也是关键的验证工具是重采样,其中数据子集被迭代地用于重复验证数据集。在第 1 章无监督机器学习中,我们看到了 v-fold 交叉验证的使用;交叉验证技术也许是行动中重新采样的最好例子。

除了适用的技术之外,注意数据有效建模所需的样本大小是一个好主意。这里没有普遍的原则,但我一直很喜欢下面的经验法则:

如果需要 m 个点来确定一条具有足够精度的单变量回归线,那么至少需要 mn 个观测值,或许还需要 n 个观测值!mn 观察,以适当地表征和评估具有 n 变量的回归模型。

请注意,这个问题的建议解决方案(重采样、样本分割和包括交叉验证在内的验证技术)和前面的解决方案之间存在一些矛盾。也就是说,过拟合需要更有节制地使用标记的训练数据的子集,而使用更多的训练数据不太可能出现不好的开始。对于每一个具体的问题,取决于所分析数据的复杂性,将会有一个适当的平衡来达成。通过监控任一类型问题的迹象,可以在正确的时间采取适当的措施(无论是增加还是减少迭代中同时使用的标记数据量)。

自我训练引入的另一类风险是,引入未标记的数据几乎总是会引入噪声。如果处理数据集,其中部分或全部未标记的情况是高噪声的,引入的噪声量可能足以降低分类精度。

使用数据复杂性和噪声度量来了解一个人的数据集中的噪声程度的想法并不新鲜。对我们来说幸运的是,已经有相当多的好的估计器存在,我们可以加以利用。

有两组主要的相对复杂性度量。有些人试图测量不同类别的值的重叠或可分性;该组中的度量试图描述每个类相对于其他类的模糊程度。这种情况的一个很好的度量是最大 费希尔判别比,尽管最大个体特征效率也是有效的。

或者(有时更简单),可以使用线性分类器的误差函数来理解数据集的类之间的可分性。通过尝试在数据集上训练一个简单的线性分类器并观察训练误差,人们可以立即很好地理解类是如何线性分离的。此外,与该分类器相关的度量(例如类边界中的点的分数或平均类内/类间最近邻距离的比率)也非常有帮助。

还有其他数据复杂性度量,专门用于度量数据集的密度或几何形状。一个很好的例子是最大覆盖球的分数。同样,通过应用线性分类器并包括该分类器的非线性,可以获得有用的度量。

改进选择过程

自我训练算法正确工作的关键是精确计算每个标签投影的置信度。自信计算是自我训练成功的关键。

在我们对自我训练的第一次解释中,我们对某些参数使用了一些简单的值,包括一个与置信度计算密切相关的参数。在选择我们标记的案例时,我们使用了一个固定的置信水平与预测概率进行比较,我们可以采用几种不同策略中的任何一种:

  • 将所有投影标签添加到标签数据集
  • 使用置信度阈值仅选择集合中最有信心的几个标签
  • 将所有投影标签添加到标记的数据集中,并对每个标签进行置信度加权

总的来说,我们已经看到自我训练的实现存在相当大的风险。他们容易多次训练失败,也容易过度训练。更糟糕的是,随着未标记数据量的增加,自训练分类器的准确性面临越来越大的风险。

我们的下一步将是看看一个非常不同的自我培训实施。虽然在概念上类似于我们在本章前面使用的算法,但我们将研究的下一种技术在不同的假设下运行,产生非常不同的结果。

对比悲观似然估计

在我们之前的自我训练技术的发现和应用中,我们发现自我训练是一种具有重大风险的强大技术。特别是,我们发现需要多种诊断工具和一些非常严格的数据集条件。虽然我们可以通过细分、识别最佳标记数据和专注地跟踪某些数据集的性能来解决这些问题,但对于自我训练将带来最大好处的数据来说,其中一些操作仍然是不可能的——标记需要昂贵的测试,无论是医学测试还是科学测试,都需要专业知识和设备。

在某些情况下,我们最终得到了一些自我训练的分类器,它们的性能优于它们的监督对手,这是一种非常糟糕的情况。更糟糕的是,尽管带有标记数据的监督分类器在额外的情况下会倾向于提高精度,但半监督分类器的性能会随着数据集大小的增加而下降。因此,我们需要的是一种不那么幼稚的半监督学习方法。我们的目标应该是找到一种方法,利用半监督学习的优势,同时保持至少与监督方法下相同分类器相当的性能。

最近(2015 年 5 月)的自监督学习方法 CPLE 提供了一种更通用的方式来执行半监督参数估计。CPLE 提供了一个相当显著的优势:它产生的标签预测已经被证明始终优于由等效的半监督分类器或由基于标签数据的监督分类器创建的预测!换句话说,例如,当执行线性判别分析时,建议您执行基于 CPLE 的半监督分析,而不是监督分析,因为您将始终获得至少同等的性能。

这是一个相当大的要求,需要证实。让我们先了解一下 CPLE 是如何工作的,然后再来展示它在实际案例中的卓越表现。

CPLE 使用熟悉的最大对数似然度量进行参数优化。这可以认为是成功的条件;我们将开发的模型旨在优化模型参数的最大对数似然性。正是 CPLE 所包含的具体保证和假设使这项技术有效。

为了创建一个更好的半监督学习者——一个改进其监督替代的学习者——CPLE 明确地考虑了监督估计,使用半监督和监督模型之间产生的损失作为训练性能度量:

Contrastive Pessimistic Likelihood Estimation

CPLE 计算任何半监督估计相对于监督解的相对改进。当监督解优于半监督估计时,损失函数表明了这一点,并且模型可以训练来调整半监督模型以减少这一损失。当半监督解优于监督解时,模型可以通过调整模型参数从半监督模型中学习。

然而,尽管到目前为止这听起来很好,但理论中有一个缺陷必须解决。半监督解不存在数据标签这一事实意味着后验分布(CPLE 用来计算损失)是不可获得的。CPLE 对此的解决方案是悲观。CPLE 算法采用所有标签/预测组合的笛卡尔乘积,然后选择后验分布,使似然性增益最小。

在现实世界的机器学习环境中,这是一种非常安全的方法。它提供了监督方法的分类精度,通过保守假设得到了半监督的性能改进。在实际应用中,这些保守的假设能够在测试中实现高性能。更好的是,CPLE 可以在一些最具挑战性的无监督学习情况下提供特定的性能改进,在这些情况下,标记的数据是未标记数据的不良表示(由于来自一个或多个类的不良采样或者仅仅因为缺少未标记的情况)。

为了理解 CPLE 比半监督或监督方法更有效,让我们把这个技术应用到一个实际问题上。我们将再次与 semi up-learn 库合作,这是一个专注于半监督学习的专业 Python 库,它扩展了 scikit-learn,在任何 scikit-learn 提供的分类器上提供 CPLE。我们从 CPLE 课程开始:

class CPLELearningModel(BaseEstimator):

    def __init__(self, basemodel, pessimistic=True, predict_from_probabilities = False, use_sample_weighting = True, max_iter=3000, verbose = 1):
        self.model = basemodel
        self.pessimistic = pessimistic
        self.predict_from_probabilities = predict_from_probabilities
        self.use_sample_weighting = use_sample_weighting
        self.max_iter = max_iter
        self.verbose = verbose

我们已经熟悉了basemodel的概念。在本章的前面,我们使用了 S3VMs 和半监督的 LDE。在这种情况下,我们将再次使用 LDE;第一次检测的目标是尝试并超过本章前面半监督 LDE 获得的结果。事实上,我们要把那些结果吹出来!

但是,在此之前,让我们回顾一下其他参数选项。pessimistic论点给了我们一个使用非悲观(乐观)模型的机会。一个乐观的模型旨在最大化似然性,而不是遵循pessimistic方法来最小化未标记和标记区分似然性之间的损失。这可以产生更好的结果(主要是在训练期间),但风险明显更大。在这里,我们将使用悲观模型。

predict_from_probabilities参数通过允许同时从多个数据点的概率生成预测来实现优化。如果我们将此设置为真,如果我们用于预测的概率大于平均值,我们的 CPLE 将把预测设置为1,否则设置为0。另一种选择是使用基本模型概率,出于性能原因,这通常是更可取的,除非我们在许多情况下调用predict

我们也可以选择use_sample_weighting,或者称为软标签(但我们最熟悉的是后验概率)。我们通常会抓住这个机会,因为软标签比硬标签具有更大的灵活性,并且通常是首选的(除非模型只支持硬类标签)。

前几个参数提供了一种停止 CPLE 训练的方法,要么在最大迭代时,要么在对数似然停止改进后(通常是因为收敛)。bestdl提供最佳鉴别似然值和相应的软标签;这些值会在每次训练迭代中更新:

        self.it = 0 
        self.noimprovementsince = 0 
        self.maxnoimprovementsince = 3 

        self.buffersize = 200
        self.lastdls = [0]*self.buffersize

        self.bestdl = numpy.infty
        self.bestlbls = []

        self.id = str(unichr(numpy.random.randint(26)+97))+str(unichr(numpy.random.randint(26)+97))

discriminative_likelihood函数计算输入的可能性(对于区分模型,即目标概率最大化的模型— y = 1 ,以输入为条件, X )。

在这种情况下,值得你注意生成模型和区分模型之间的区别。虽然这不是一个基本的概念,但它对于理解为什么许多分类器有它们的目标是非常重要的。

分类模型获取输入数据并尝试对案例进行分类,为每个案例分配一个标签。这样做的方法不止一种。

一种方法是以案例为例,试图在它们之间划定一个决策边界。然后,我们可以在每个新案例出现时,识别它落在边界的哪一侧。这是一种区别性学习方法。

另一种方法是尝试对每个类的分布进行单独建模。一旦生成了模型,算法就可以使用贝叶斯规则来计算给定输入数据的标签上的后验分布。这种方法是生成性的,是一种非常强大的方法,但有很大的弱点(其中大部分都与我们对类建模的能力有关)。生成方法包括高斯判别模型(是的,这是一个稍微令人困惑的名字)和广泛的贝叶斯模型。更多信息,包括一些优秀的推荐阅读,请参见本章的进一步阅读部分。

在这种情况下,该函数将在每次迭代中用于计算预测标签的可能性:

    def discriminative_likelihood(self, model, labeledData, labeledy = None, unlabeledData = None, unlabeledWeights = None, unlabeledlambda = 1, gradient=[], alpha = 0.01):
        unlabeledy = (unlabeledWeights[:, 0]<0.5)*1
        uweights = numpy.copy(unlabeledWeights[:, 0]) 

        uweights[unlabeledy==1] = 1-uweights[unlabeledy==1] 

        weights = numpy.hstack((numpy.ones(len(labeledy)), uweights))
        labels = numpy.hstack((labeledy, unlabeledy))

定义了 CPLE 的大部分内容后,我们还需要为我们的监督模型定义拟合过程。这使用了熟悉的组件,即model.fitmodel.predict_proba,用于概率预测:

        if self.use_sample_weighting:
            model.fit(numpy.vstack((labeledData, unlabeledData)), labels, sample_weight=weights)
        else:
            model.fit(numpy.vstack((labeledData, unlabeledData)), labels)

        P = model.predict_proba(labeledData)

为了执行悲观 CPLE,我们需要导出标记和未标记的鉴别对数似然。然后,我们依次对已标记和未标记的数据执行predict_proba:

        try:

            labeledDL = -sklearn.metrics.log_loss(labeledy, P)
        except Exception, e:
            print e
            P = model.predict_proba(labeledData)

        unlabeledP = model.predict_proba(unlabeledData)  

        try: 
            eps = 1e-15
            unlabeledP = numpy.clip(unlabeledP, eps, 1 - eps)
            unlabeledDL = numpy.average((unlabeledWeights*numpy.vstack((1-unlabeledy, unlabeledy)).T*numpy.log(unlabeledP)).sum(axis=1))
        except Exception, e:
            print e
            unlabeledP = model.predict_proba(unlabeledData)

一旦我们能够计算标记和未标记分类尝试的鉴别对数似然性,我们就可以通过discriminative_likelihood_objective函数设置目标。这里的目标是在每次迭代中使用悲观(或乐观,根据选择)方法计算dl,直到模型收敛或达到最大迭代次数。

在每次迭代中,执行 t 检验以确定可能性是否已经改变。可能性应该在每次迭代收敛前继续变化。敏锐的读者可能在本章前面已经注意到,三个连续的 t 检验显示没有变化将导致迭代停止(这可通过maxnoimprovementsince参数配置):

        if self.pessimistic:
            dl = unlabeledlambda * unlabeledDL - labeledDL
        else: 
            dl = - unlabeledlambda * unlabeledDL - labeledDL

        return dl

    def discriminative_likelihood_objective(self, model, labeledData, labeledy = None, unlabeledData = None, unlabeledWeights = None, unlabeledlambda = 1, gradient=[], alpha = 0.01):
        if self.it == 0:
            self.lastdls = [0]*self.buffersize

        dl = self.discriminative_likelihood(model, labeledData, labeledy, unlabeledData, unlabeledWeights, unlabeledlambda, gradient, alpha)

        self.it += 1
        self.lastdls[numpy.mod(self.it, len(self.lastdls))] = dl

        if numpy.mod(self.it, self.buffersize) == 0: # or True:
            improvement = numpy.mean((self.lastdls[(len(self.lastdls)/2):])) - numpy.mean((self.lastdls[:(len(self.lastdls)/2)]))

            _, prob = scipy.stats.ttest_ind(self.lastdls[(len(self.lastdls)/2):], self.lastdls[:(len(self.lastdls)/2)])

            noimprovement = prob > 0.1 and numpy.mean(self.lastdls[(len(self.lastdls)/2):]) < numpy.mean(self.lastdls[:(len(self.lastdls)/2)])
            if noimprovement:
                self.noimprovementsince += 1
                if self.noimprovementsince >= self.maxnoimprovementsince:

                    self.noimprovementsince = 0
                    raise Exception(" converged.") 
            else:
                self.noimprovementsince = 0

在每次迭代中,算法保存最佳鉴别似然性和最佳权重集,供下一次迭代使用:

        if dl < self.bestdl:
            self.bestdl = dl
            self.bestlbls = numpy.copy(unlabeledWeights[:, 0])

        return dl

另一个值得讨论的因素是软标签是如何创建的。我们在本章前面已经讨论过了。这是它们在代码中的样子:

f = lambda softlabels, grad=[]: self.discriminative_likelihood_objective(self.model, labeledX, labeledy=labeledy, unlabeledData=unlabeledX, unlabeledWeights=numpy.vstack((softlabels, 1-softlabels)).T, gradient=grad) 

lblinit = numpy.random.random(len(unlabeledy))

简而言之,softlabels提供了判别似然计算的概率版本。换句话说,它们充当权重,而不是硬的二进制类标签。软标签可使用optimize方法计算:

        try:
            self.it = 0
            opt = nlopt.opt(nlopt.GN_DIRECT_L_RAND, M)
            opt.set_lower_bounds(numpy.zeros(M))
            opt.set_upper_bounds(numpy.ones(M))
            opt.set_min_objective(f)
            opt.set_maxeval(self.max_iter)
            self.bestsoftlbl = opt.optimize(lblinit)
            print " max_iter exceeded."
        except Exception, e:
            print e
            self.bestsoftlbl = self.bestlbls

        if numpy.any(self.bestsoftlbl != self.bestlbls):
            self.bestsoftlbl = self.bestlbls
        ll = f(self.bestsoftlbl)

        unlabeledy = (self.bestsoftlbl<0.5)*1
        uweights = numpy.copy(self.bestsoftlbl)

        uweights[unlabeledy==1] = 1-uweights[unlabeledy==1] 

        weights = numpy.hstack((numpy.ones(len(labeledy)), uweights))
        labels = numpy.hstack((labeledy, unlabeledy))

对于感兴趣的读者,优化使用牛顿共轭梯度计算梯度下降的方法来找到最佳权重值。本章末尾的进一步阅读部分提供了牛顿共轭梯度的参考。

一旦我们理解了这是如何工作的,剩下的计算就是直接比较最佳监督标签和软标签,将bestsoftlabel参数设置为最佳标签集。随后,针对最佳标签集计算辨别似然性,并计算fit函数:

        if self.use_sample_weighting:
            self.model.fit(numpy.vstack((labeledX, unlabeledX)), labels, sample_weight=weights)
        else:
            self.model.fit(numpy.vstack((labeledX, unlabeledX)), labels)

        if self.verbose > 1:
            print "number of non-one soft labels: ", numpy.sum(self.bestsoftlbl != 1), ", balance:", numpy.sum(self.bestsoftlbl<0.5), " / ", len(self.bestsoftlbl)
            print "current likelihood: ", ll

既然我们已经有机会了解 CPLE 的实现,那么就让我们自己动手操作一个有趣的数据集吧!这一次,我们将通过使用哥伦比亚大学的百万歌曲数据集来改变现状。

该算法的核心特征是百万首歌曲的特征分析和元数据。数据是预先准备好的,由自然特征和衍生特征组成。可用的功能包括诸如艺术家的姓名和 ID、持续时间、响度、时间签名和每首歌曲的节奏等,以及其他度量,包括人群分级的可跳舞性分数和与音频相关联的标签。

这个数据集通常被标记(通过标签),但是我们在这种情况下的目标是根据提供的数据为不同的歌曲生成流派标签。由于完整的百万首歌曲数据集是相当令人生畏的 300 GB,让我们使用 1% (1.8 GB)的 10,000 条记录的子集。此外,我们并不特别需要这些数据,因为它目前已经存在;这是一种无益的格式,很多字段对我们来说没什么用。

位于我们的“掌握 Python 机器学习”第 6 章文本特征工程文件夹中的10000_songs数据集是来自多种流派的经过清理、准备(也相当大)的音乐数据子集。在本分析中,我们将尝试从作为目标提供的流派标签中预测流派。我们将把标签的一个子集作为标记数据,用于启动我们的学习,并将尝试为未标记的数据生成标签。

在这个迭代中,我们将按照如下方式提高我们的游戏:

  • 使用更多带标签的数据。这一次,我们将使用总数据集大小的 1%(100 首歌曲),随机获取,作为标记数据。
  • 使用具有线性核的 SVM 作为我们的分类器,而不是我们在本章前面天真的自我训练实现中使用的简单线性判别分析。

那么,让我们开始吧:

import sklearn.svm
import numpy as np
import random

from frameworks.CPLELearning import CPLELearningModel
from methods import scikitTSVM
from examples.plotutils import evaluate_and_plot

kernel = "linear"

songs = fetch_mldata("10000_songs")
X = songs.data
ytrue = np.copy(songs.target)
ytrue[ytrue==-1]=0

labeled_N = 20
ys = np.array([-1]*len(ytrue))
random_labeled_points = random.sample(np.where(ytrue == 0)[0], labeled_N/2)+\
                        random.sample(np.where(ytrue == 1)[0], labeled_N/2)
ys[random_labeled_points] = ytrue[random_labeled_points]

作为比较,我们将在 CPLE 实现的同时运行一个受监督的 SVM。为了进行比较,我们还将运行我们在本章前面看到的天真的自我监督实现:

basemodel = SGDClassifier(loss='log', penalty='l1') # scikit logistic regression
basemodel.fit(X[random_labeled_points, :], ys[random_labeled_points])
print "supervised log.reg. score", basemodel.score(X, ytrue)

ssmodel = SelfLearningModel(basemodel)
ssmodel.fit(X, ys)
print "self-learning log.reg. score", ssmodel.score(X, ytrue)

ssmodel = CPLELearningModel(basemodel)
ssmodel.fit(X, ys)
print "CPLE semi-supervised log.reg. score", ssmodel.score(X, ytrue)

我们在这次迭代中获得的结果非常强:

# supervised log.reg. score 0.698
# self-learning log.reg. score 0.825
# CPLE semi-supervised log.reg. score 0.833

Contrastive Pessimistic Likelihood Estimation

CPLE 半监督模型成功地以 84%的准确率进行了分类,这个分数相当于人类的估计,比天真的半监督实现高出 10%以上。值得注意的是,它的表现也优于受监管的 SVM。

进一步阅读

开始理解半监督学习方法的一个坚实的地方是朱晓金非常全面的文献调查,可在http://pages.cs.wisc.edu/~jerryzhu/pub/ssl_survey.pdf获得。

我也推荐同一作者的教程,在http://pages.cs.wisc.edu/~jerryzhu/pub/sslicml07.pdf以幻灯片形式提供。

关于污染悲观似然估计的关键论文是 Loog 2015 年的论文http://arxiv.org/abs/1503.00269

本章提到了生成模式和区分模式的区别。吴恩达(http://cs229.stanford.edu/notes/cs229-notes2.pdf)和迈克尔·乔丹(http://www . ics . UCI . edu/~ s myth/courses/cs 274/readings/Jordan _ logistics . pdf)对生成算法和判别算法之间的区别提供了一些相对清晰的解释。

对于对贝叶斯统计感兴趣的读者来说,艾伦·唐尼的书《思考贝叶斯》是一个了不起的介绍(也是我最喜欢的统计学书籍之一):https://www.google.co.uk/#q=think+bayes

对于有兴趣了解更多梯度下降的读者,我推荐 Sebastian Ruder 在http://sebastianruder.com/optimizing-gradient-descent/的博客。

对于有兴趣深入了解共轭下降内部的读者,Jonathan Shewchuk 的介绍在https://www . cs . CMU . edu/~ quake-papers/无痛-共轭梯度. pdf 上为一些关键概念提供了清晰而令人愉快的定义。

总结

在这一章中,我们利用了机器学习中一个非常强大但鲜为人知的范例——半监督学习。我们从探索直推式学习和自我训练的基本概念开始,并通过使用一个天真的自我训练实现来提高我们对后一类技术的理解。

我们很快开始发现自我训练中的弱点,并寻找有效的解决方案,我们以 CPLE 的形式发现了这一点。CPLE 是一个非常优雅且高度适用的半监督学习框架,除了用作基础模型的分类器之外,它不做任何假设。作为回报,我们发现 CPLE 始终以最小的风险提供超过天真的半监督和监督实施的性能。关于机器学习中最有用的最新发展之一,我们已经获得了大量的理解。

在下一章中,我们将开始讨论数据准备技巧,这些技巧可以显著提高我们之前讨论过的所有模型的有效性。

六、文本特征工程

简介

在前面的章节中,我们花时间评估了能够分析复杂或挑战性数据的强大技术。然而,对于最困难的问题,正确的技巧只会让你走得更远。

深度学习和监督学习试图解决的持续挑战是,找到解决方案通常需要相关团队的多项重大投资。在旧的模式下,人们经常不得不执行特定的准备任务,这需要时间、专业技能和知识。通常,甚至使用的技术也是特定于领域和/或特定于数据类型的。通过这个过程,特征被导出,被称为特征工程。

到目前为止,我们研究的大部分深度学习算法都是为了帮助寻找方法,避免需要执行大量的特征工程。然而,与此同时,特征工程继续被视为顶级 ML 从业者的一项非常重要的技能。以下引述来自卡格尔的主要竞争对手,通过大卫·科福德·温德对卡格尔博客的贡献:

|   | “你使用的功能比其他任何东西都更能影响结果。据我所知,没有一种算法能够补充正确的特征工程所带来的信息增益。” |   |
|   | --(Luca massaron) |

|   | “特征工程当然是 Kaggle 比赛中最重要的方面之一,也是一个人应该花最多时间的部分。数据中通常有一些隐藏的特征,可以大大提高你的表现,如果你想在排行榜上获得好的位置,你必须找到它们。如果你在这里搞砸了,你多半不会再赢了;总有一个人会发现所有的秘密。然而,还有其他重要的部分,比如你如何表述这个问题。你会使用回归模型或分类模型,甚至两者结合,还是需要某种排名。这一点以及功能工程对于在这些比赛中取得好成绩至关重要。也有一些比赛不再需要(手动)特征工程;比如图像处理比赛。当前最先进的深度学习算法可以为您做到这一点。” |   |
|   | -- (约瑟夫·费格尔) |

这里有几个关键主题;特征工程功能强大,即使是极少量的特征工程也能对一个人的分类器产生很大的影响。如果你想得到最好的结果,经常需要使用特征工程技术。最大化机器学习算法的有效性需要一定数量的特定领域和特定数据类型的知识(秘密)。

再引用一句话:

|   | “对于大多数卡格尔竞赛来说,最重要的部分是功能工程,这很容易学习。” |   |
|   | - (蒂姆萨利曼) |

蒂姆没有错;在本章中,您将学到的大部分内容都是直观、有效的技巧和转换。本章将从自然语言处理和金融时间序列应用程序中,向您介绍一些应用于文本和时间序列数据的最有效和最常用的准备技术。我们将介绍这些技术是如何工作的,人们应该期望看到什么,以及人们如何诊断它们是否如期望的那样工作。

文本特征工程

在前面的章节中,我们已经讨论了一些方法,通过这些方法我们可以获取数据集并提取有价值特征的子集。这些方法具有广泛的适用性,但是在处理非数值/非分类数据,或者不容易转换为数值或分类数据的数据时,这些方法的帮助较小。特别是,在处理文本数据时,我们需要应用不同的技术。

我们将在本节中研究的技术分为两大类——清洁技术和功能准备技术。这些通常以大致的顺序实现,我们将相应地研究它们。

清理文本数据

当我们处理自然文本数据时,应用了一组不同的方法。这是因为在现实世界的上下文中,自然干净的文本数据集的想法是非常不安全的;文本数据充斥着拼写错误、表情符号之类的非字典构造,在某些情况下,还有 HTML 标记。因此,我们需要非常彻底地清洁。

在这一节中,我们将使用一个相当粗糙的真实数据集,使用一些有效的文本清理技术。具体来说,我们将使用 2012 年卡格尔竞赛的无常数据集,该竞赛的目标是创建一个模型,准确检测社会评论中的侮辱。

是的,我指的是网络巨魔检测。

我们开始吧!

用漂亮的字体清理文字

我们的第一步应该是手动检查输入数据。这非常关键;有了文本数据,需要尝试初步了解数据中存在哪些问题,从而识别出需要清理的地方。

通读一个充满可恶的互联网评论的数据集有点痛苦,所以这里有一个示例条目:

|

身份证明

|

日期

|

评论

|
| --- | --- | --- |
| 132 | 20120531031917Z | """\xa0@Flip\xa0how are you not ded""" |

我们有一个似乎不需要太多工作的 ID 字段和日期字段。然而,文本字段非常具有挑战性。从这一个案例中,我们已经可以看到拼写错误和 HTML 包含。此外,数据集中的许多条目包含绕过发誓过滤的尝试,通常是在单词中间包含一个空格或标点元素。其他数据质量问题包括多个元音字母(扩展一个单词)、非 ascii 字符、超链接...名单还在继续。

清理此数据集的一个选项是使用正则表达式,该表达式在输入数据上运行以消除数据质量问题。然而,问题格式的数量和种类使得使用基于正则表达式的方法不切实际,至少从一开始是这样。我们很可能会错过很多案例,也会误判所需准备的数量,导致我们过于积极地清理,或者不够积极;具体来说,我们可能会切入真实的文本内容或留下部分标签。我们需要的是一个解决方案,它将首先解决大多数常见的数据质量问题,这样我们就可以用基于脚本的方法专注于剩余的问题。

进入BeautifulSoupBeautifulSoup是一个非常强大的文本清理库,除了其他功能之外,它还可以删除 HTML 标记。让我们来看看这个关于巨魔数据的库:

from bs4 import BeautifulSoup
import csv

trolls = []
with open('trolls.csv',  'rt') as f:
    reader = csv.DictReader(f)
    for line in reader:
        trolls.append(BeautifulSoup(str(line["Comment"]), "html.parser"))

print(trolls[0])

eg = BeautifulSoup(str(trolls), "html.parser")

print(eg.get_text())
|

身份证明

|

日期

|

评论

|
| --- | --- | --- |
| 132 | 20120531031917Z | @Flip how are you not ded |

正如我们所看到的,我们已经在提高文本数据的质量方面取得了进展。然而,从这些例子中也可以清楚地看出,还有很多工作要做!如上所述,让我们继续使用正则表达式来帮助进一步清理和标记我们的数据。

管理标点和标记

标记化是从文本流中创建一组标记的过程。许多标记是单词,而其他标记可能是字符集(例如笑脸或其他标点字符串,例如????????)。

现在,我们已经从初始数据集中移除了许多 HTML 丑陋之处,我们可以采取措施进一步提高文本数据的整洁度。为此,我们将利用re模块,它允许我们对正则表达式使用操作,例如子串替换。在这次传递中,我们将对输入文本执行一系列操作,主要集中在用标记替换变量或有问题的文本元素。让我们从一个简单的例子开始,用_EM令牌替换电子邮件地址:

text = re.sub(r'[\w\-][\w\-\.]+@[\w\-][\w\-\.]+[a-zA-Z]{1,4}', '_EM', text)

同样,我们可以移除 URL,用_U标记替换它们:

text = re.sub(r'\w+:\/\/\S+', r'_U', text)

我们可以自动删除多余或有问题的空白和换行符、连字符和下划线。此外,我们将开始处理多个字符的问题,这些字符通常用于非正式对话中的强调。扩展系列的标点符号在这里使用_BQBX等编码进行编码;这些较长的标签用于区别于更直接的_Q_X标签(分别指问号和感叹号的使用)。

我们还可以使用正则表达式来管理额外的字母;通过将这样的字符串最多减少到两个字符,我们能够将组合的数量减少到可管理的数量,并使用_EL标记对减少的组进行标记:

# Format whitespaces
text = text.replace('"', ' ')
text = text.replace('\'', ' ')
text = text.replace('_', ' ')
text = text.replace('-', ' ')
text = text.replace('\n', ' ')
text = text.replace('\\n', ' ')
text = text.replace('\'', ' ')
text = re.sub(' +',' ', text) 
text = text.replace('\'', ' ')

#manage punctuation
text = re.sub(r'([^!\?])(\?{2,})(\Z|[^!\?])', r'\1 _BQ\n\3', text)
text = re.sub(r'([^\.])(\.{2,})', r'\1 _SS\n', text) 
text = re.sub(r'([^!\?])(\?|!){2,}(\Z|[^!\?])', r'\1 _BX\n\3', text) 
text = re.sub(r'([^!\?])\?(\Z|[^!\?])', r'\1 _Q\n\2', text) 
text = re.sub(r'([^!\?])!(\Z|[^!\?])', r'\1 _X\n\2', text) 
text = re.sub(r'([a-zA-Z])\1\1+(\w*)', r'\1\1\2 _EL', text) 
text = re.sub(r'([a-zA-Z])\1\1+(\w*)', r'\1\1\2 _EL', text)
text = re.sub(r'(\w+)\.(\w+)', r'\1\2', text)
text = re.sub(r'[^a-zA-Z]','', text)

接下来,我们希望开始创建其他感兴趣的标记。其中一个更有用的指标是骂人的_SW标记。我们还将使用正则表达式来帮助识别并标记四个桶中的一个;大而快乐的微笑(_BS)、小而快乐的微笑(_S)、大而悲伤的微笑(_BF)和小而悲伤的微笑(_F):

text = re.sub(r'([#%&\*\$]{2,})(\w*)', r'\1\2 _SW', text)

text = re.sub(r' [8x;:=]-?(?:\)|\}|\]|>){2,}', r' _BS', text) 
text = re.sub(r' (?:[;:=]-?[\)\}\]d>])|(?:<3)', r' _S', text) 
text = re.sub(r' [x:=]-?(?:\(|\[|\||\\|/|\{|<){2,}', r' _BF', text) 
text = re.sub(r' [x:=]-?[\(\[\|\\/\{<]', r' _F', text)

由于它们的用法经常变化,所以表情符号很复杂;虽然这一系列的角色相当流行,但绝不完整;例如,有关一系列非 ascii 表示形式,请参见表情符号。出于几个原因,我们将从这个例子中删除非 ascii 文本(类似的方法是使用字典来强制遵从),但是这两种方法都有一个明显的缺点,即它们从数据集中删除了案例,这意味着任何解决方案都是不完美的。在某些情况下,这种方法可能会导致删除大量数据。因此,一般来说,明智的做法是意识到文本内容中基于字符的图像面临的普遍挑战。

接下来,我们要开始将文本拆分成短语。这是str.split的一个简单应用,它使输入能够被视为单词(单词)的向量,而不是长字符串(re):

phrases = re.split(r'[;:\.()\n]', text) 
phrases = [re.findall(r'[\w%\*&#]+', ph) for ph in phrases] 
phrases = [ph for ph in phrases if ph] 

words = []

for ph in phrases:
      words.extend(ph)

这给了我们以下信息:

|

身份证明

|

日期

|

评论

|
| --- | --- | --- |
| 132 | 20120531031917Z | [['Flip', 'how', 'are', 'you', 'not', 'ded']] |

接下来,我们对单字母序列执行搜索。有时,为了强调,互联网交流包括使用间隔的单字母链。这可以尝试作为一种避免检测诅咒词的方法:

tmp = words
words = []
new_word = ''
for word in tmp:
   if len(word) == 1:
      new_word = new_word + word
   else:
      if new_word:
         words.append(new_word)
         new_word = ''
      words.append(word)

到目前为止,我们在清理和提高输入数据的质量方面已经走了很长的路。然而,仍然存在悬而未决的问题。让我们重新考虑我们开始的例子,现在看起来如下:

|

身份证明

|

日期

|

|
| --- | --- | --- |
| 132 | 20120531031917Z | ['_F', 'how', 'are', 'you', 'not', 'ded'] |

我们早期的清理已经忽略了这个例子,但是我们可以看到向量化句子内容以及现在清理的 HTML 标签的效果。我们还可以看到使用的表情已经通过_F标签捕捉到了。当我们看一个更复杂的测试用例时,我们会看到更实质性的变化结果:

|

生的

|

清洁并分离

|
| --- | --- |
| GALLUP DAILY\nMay 24-26, 2012 \u2013 Updates daily at 1 p.m. ET; reflects one-day change\nNo updates Monday, May 28; next update will be Tuesday, May 29.\nObama Approval48%-\nObama Disapproval45%-1\nPRESIDENTIAL ELECTION\nObama47%-\nRomney45%-\n7-day rolling average\n\n It seems the bump Romney got is over and the president is on his game。 | ['GALLUP', 'DAILY', 'May', 'u', 'Updates', 'daily', 'pm', 'ET', 'reflects', 'one', 'day', 'change', 'No', 'updates', 'Monday', 'May', 'next', 'update', 'Tuesday', 'May', 'Obama', 'Approval', 'Obama', 'Disapproval', 'PRESIDENTIAL', 'ELECTION', 'Obama', 'Romney', 'day', 'rolling', 'average', 'It', 'seems', 'bump', 'Romney', 'got', 'president', 'game'] |

但是有两个显著的问题在两个例子中仍然很明显。在第一种情况下,我们有一个拼错的单词;我们需要找到消除这种情况的方法。其次,两个例子中的很多单词(例如。pm)本身并没有太多的信息。我们发现的问题是,特别是对于较短的文本样本,清理后剩下的内容可能只包含一两个有意义的术语。如果这些术语在整个语料库中并不十分常见,那么训练一个分类器来识别这些术语的重要性可能会非常困难。

对单词进行标记和分类

我想我们都知道英语单词有几种类型——名词、动词、副词等等。这些通常被称为词类。如果我们知道某个单词是形容词,而不是动词或停止词(如 a、the 或 of),我们可以对其进行不同的处理,或者更重要的是,我们的算法可以!

如果我们能够通过将词类识别和编码为分类变量来执行词性标注,我们就能够通过仅保留有价值的内容来提高数据质量。文本标记选项和技术的范围太广,本章的某一部分无法有效涵盖,因此我们将查看一些适用的标记技术。具体来说,我们将关注 n-gram 标记和 backoff taggers,这是一对互补的技术,允许我们创建强大的递归标记算法。

我们将使用一个名为 自然语言工具包 ( NLTK )的 Python 库。NLTK 提供了广泛的功能,我们将在本章的几个地方依赖它。现在,我们将使用 NLTK 来执行某些单词类型的标记和移除。具体来说,我们将过滤掉停止词。

先来回答显而易见的问题(为什么要消除停止词?),停止词对大多数文本分析来说几乎没有什么作用,并且可能会造成一定程度的噪音和训练差异,这是事实。幸运的是,过滤停止词非常简单。我们将简单地导入 NLTK,下载并导入字典,然后对预先存在的单词向量中的所有单词执行扫描,删除任何找到的停止单词:

import nltk
nltk.download()
from nltk.corpus import stopwords 

words = [w for w in words if not w in stopwords.words("english")]

我相信你会同意这很简单!让我们继续讨论更多的 NLTK 功能,特别是标记。

用 NLTK 标记

标记是识别词类的过程,正如我们前面所描述的,并对每个术语应用标记。

在最简单的形式中,标记可以像对输入数据应用字典一样简单,就像我们之前对 stopwords 所做的那样:

tagged = ntlk.word_tokenize(words)

然而,即使是简单的考虑也会清楚地表明,我们对语言的使用要比这允许的复杂得多。我们可以用一个词(如 ferry)作为几个词类之一,决定如何对待每个话语中的每个词可能并不简单。很多时候,只有在给定其他单词及其在短语中的位置的情况下,才能理解正确的标签。

谢天谢地,我们有许多有用的技术可以帮助我们解决语言挑战。

顺序标记

一种顺序标记算法是通过从左到右和逐个标记地运行输入数据集来工作的(因此是顺序的!),连续标记每个令牌。分配哪个令牌的决定是基于该令牌、其前面的令牌以及这些前面的令牌的预测标签做出的。

在本节中,我们将使用一个 n-gram tagger 。n-gram 标记器是一种顺序标记器,用于识别适当的标记。n-gram 标记器在生成标记时会考虑到(n-1)-多个先前的位置标记和当前标记。

为了清楚起见,n-gram 是用于给定元素集合中 n 个元素的连续序列的术语。这可能是字母、单词、数字代码(例如,状态变化)或其他元素的连续序列。n-gram 被广泛用作一种手段,通过使用 n-multi 元素来捕捉元素集合的联合含义——无论是那些短语还是编码的状态转换。

n-gram tagger 最简单的形式是 n = 1 ,被称为 unigram tagger 。通过为每个令牌维护一个有条件的频率分布,单程序标记器的操作非常简单。这种条件频率分布是从术语的训练语料库中建立的;我们可以使用属于 NLTK 中NgramTagger类的有帮助的训练方法来实现训练。标记器假设在给定序列中给定标记最频繁出现的标记很可能是该标记的正确标记。如果术语 carp 在训练语料库中作为名词出现了四次,作为动词出现了两次,那么单语法标记器会将名词标记分配给任何类型为 carp 的标记。

这对于第一遍标记尝试来说可能足够了,但是很明显,一个只为每组同音异义词提供一个标记的解决方案并不总是理想的。我们可以利用的解决方案是使用数值更大的 n 克 n 。例如,通过 n = 3 (一个 T5】三元标记器,我们可以看到标记器如何更容易区分输入他倾向于在大量上鲤鱼,而不是他钓到了一条华丽的鲤鱼

然而,这里又一次在标记的准确性和标记的能力之间进行了权衡。随着我们增加 n ,我们正在创造越来越长的 n 克,这变得越来越罕见。在很短的时间内,我们最终处于 n-grams 没有出现在训练数据中的情况,导致我们的标记器无法为当前令牌找到任何合适的标记!

在实践中,我们发现我们需要的是一套标签。我们希望最可靠、最准确的标记器在尝试标记给定数据集时有第一次机会,对于任何失败的情况,我们都可以尝试使用更可靠但可能不太准确的标记器。

令人高兴的是,我们想要的已经以退避标签的形式存在了。让我们了解更多!

回退标记

有时,给定的标记器可能会执行不可靠。当标记器具有高精度要求和有限的训练数据时,这尤其常见。在这种时候,我们通常希望构建一个集合结构,让我们同时使用几个标记器。

为此,我们在两种类型的标记器之间进行了区分:子标记器回退标记器。子标签就像我们之前看到的标签一样,依次为Brill 标签。标记结构可以包含一种或多种标记符。

**如果子标记器不能确定给定令牌的标记,则可以参考回退标记器。回退标记器专门用于组合(一个或多个)子标记的结果,如下图所示:

Backoff tagging

在简单的实现中,退避标记器将简单地按顺序轮询子标记器,接受提供的第一个非空标记。如果给定令牌的所有子标记都返回 null,则退避标记器将为该令牌分配一个 none 标记。顺序可以确定。

回退一般是和多个不同类型的子进程一起使用;这使得数据科学家能够同时利用多种标记器的优势。根据需要,回退可能指其他回退,这可能会产生高度冗余或复杂的标记结构:

Backoff tagging

一般来说,回退标记器提供了冗余,使您能够在复合解决方案中使用多个标记器。为了解决我们眼前的问题,让我们实现一系列嵌套的 n-gram 标记器。我们将从三元模型标记器开始,它将使用二元模型标记器作为它的回退标记器。如果这两个标记器都没有解决方案,我们将有一个单一的标记器作为额外的补偿。这可以非常简单地完成,如下所示:

brown_a = nltk.corpus.brown.tagged_sents(categories= 'a')

tagger = None
for n in range(1,4):
  tagger = NgramTagger(n, brown_a, backoff = tagger)

words  = tagger.tag(words)

从文本数据创建特征

一旦我们参与到深思熟虑的文本清理实践中,我们需要采取额外的步骤来确保我们的文本成为有用的特性。为了做到这一点,我们将研究 NLP 中的另一组主要技术:

  • 堵塞物
  • 引理
  • 用随机森林装袋

堵塞

当处理语言数据集时,另一个挑战是许多词干存在多种词形。例如,根舞是其他多个词的词干——舞蹈、舞者、舞蹈等等。通过找到一种将这种多种形式简化为词干的方法,我们发现自己能够改进我们的 n-gram 标记,并应用新技术,如词条统计。

使我们能够将单词缩到词干的技术称为词干分析器。词干分析器通过将单词解析为辅音/元音串并应用一系列规则来工作。最受欢迎的词干器是 搬运工词干器,它通过执行以下步骤工作;

  1. 通过将(例如, ies 变成 i 来简化后缀的范围)减少到一个更小的集合。
  2. 在几个过程中删除后缀,每个过程都删除一组后缀类型(例如,过去分词或复数后缀,如 y 或 alism)。
  3. 删除所有后缀后,通过在需要的地方添加“e”来清理词尾(例如,ceas 变为 stop)。
  4. 移除双 l。

搬运工工作效率很高。为了确切了解它的工作原理,让我们来看看它的实际应用吧!

from nltk.stem import PorterStemmer

stemmer = PorterStemmer()

stemmer.stem(words)

这个stemmer的输出,正如我们先前存在的例子所展示的,是单词的根形式。这可能是一个真实的词,也可能不是;例如,跳舞变成了舞蹈。这还可以,但不是很理想。我们可以做得更好!

为了一致地达到一个真实的单词形式,让我们应用一个稍微不同的技术,引理。引理是一个更复杂的确定词干的过程;与波特词干不同,它对不同的词类使用不同的归一化过程。与波特词干不同,它还寻求找到单词的实际词根。在词干不一定是真词的地方,引理必须是真词。引理化也承担了将同义词简化到词根的挑战。例如,词干分析器可能会将术语书转换为术语书,但它并没有配备处理术语书的工具。引理者可以同时处理书和我,把两个术语都简化为书。

作为必要的先决条件,我们需要每个输入令牌的 POS。谢天谢地,我们已经应用了 POS 标记器,并且可以直接从该过程的结果中工作!

from nltk.stem import PorterStemmer, WordNetLemmatizer

lemmatizer = WordNetLemmatizer()

words = lemmatizer.lemmatize(words, pos = 'pos')

现在的输出是我们期望看到的:

|

源程序正文

|

后记忆障碍

|
| --- | --- |
| The laughs you two heard were triggered by memories of his own high-flying exits off moving beasts | ['The', 'laugh', 'two', 'hear', 'trigger', 'memory', 'high', 'fly', 'exit', 'move', 'beast'] |

我们现在已经成功地提取了输入文本数据,极大地提高了查找算法(如许多基于字典的方法)处理这些数据的效率。我们已经删除了停止字,并用正则表达式方法标记了一系列其他噪声元素。我们还删除了任何 HTML 标记。我们的文本数据已经达到合理的处理状态。我们还需要学习另一项关键技术,它可以让我们从文本数据中生成特征。具体来说,我们可以使用打包来帮助量化术语的使用。

让我们了解更多!

套袋和随机林

打包是技术家族的一部分,这些技术统称为子空间方法。方法有几种形式,每种都有一个单独的名称。如果我们从样本案例中抽取随机子集,那么我们正在执行粘贴。如果我们是从有替换的病例中取样,这被称为装袋。如果我们不是从案例中提取,而是使用特征的子集,那么我们就执行属性打包。最后,如果我们选择从样本案例和特征中抽取,我们将采用一种被称为 随机面片的技术。

基于特征的技术、属性打包和随机补丁方法在某些环境中非常有价值,尤其是在高维环境中。医学和遗传学领域都倾向于看到大量的高维数据,因此基于特征的方法在这些领域非常有效。

在自然语言处理环境中,专门使用打包是很常见的。在语言数据的上下文中,我们要处理的东西被恰当地称为一袋单词。单词包是一种文本数据准备方法,它通过识别数据集中所有不同的单词(或标记),然后计算它们在每个样本中的出现次数来工作。让我们从一个演示开始,该演示在数据集的几个示例案例上执行:

|

身份证明

|

日期

|

|
| --- | --- | --- |
| 132 | 20120531031917Z | ['_F', 'how', 'are', 'you', 'not', 'ded'] |
| 69 | 20120531173030Z | ['you', 'are', 'living', 'proof', 'that', 'bath', 'salts', 'effect', 'thinking'] |

这为我们提供了以下 12 部分的术语列表:

[
 "_F"
 "how"
 "are"
 "you"
 "not"
 "ded"
 "living"
 "proof"
 "that"
 "bath"
 "salts"
 "effect"
 "thinking"
]

使用这个列表的索引,我们可以为前面的每个句子创建一个 12 部分的向量。这个向量的值是通过遍历前面的列表并计算数据集中每个句子的每个术语出现的次数来填充的。给定我们先前存在的例子句子和我们从它们创建的列表,我们最终创建了以下包:

|

身份证明

|

日期

|

评论

|

一大堆单词

|
| --- | --- | --- | --- |
| 132 | 20120531031917Z | _F how are you not ded | [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0] |
| 69 | 20120531173030Z | you are living proof that bath salts effect thinking | [0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1] |

这是一袋字实现的核心。自然,一旦我们将文本的语言内容翻译成数字向量,我们就能够开始使用技术来增加我们在分类中使用这些文本的复杂性。

一种选择是使用加权条款。我们可以使用术语加权方案来修改每个向量中的值,以便强调指示性的或有助于分类的术语。加权方案可以是简单的掩码,例如指示存在与否的二进制掩码。

如果某些术语的使用频率比正常情况高得多,二进制掩码可能会很有用;在这种情况下,如果不使用二进制掩码,可能需要特定的缩放(例如,对数缩放)。然而,与此同时,术语使用的频率可以提供信息(例如,它可以指示强调),并且关于是否应用二进制掩码的决定并不总是简单地做出。

另一个加权选项是术语频率-逆文档频率,或 tf-idf。该方案将特定句子和数据集内的使用频率作为一个整体进行比较,如果某个术语在给定样本中的使用频率高于在整个语料库中的使用频率,则使用的值会增加。

tf-idf 的变体经常用于文本挖掘上下文,包括搜索引擎。Scikit-learn 提供了一个 tf-idf 实现,TfidfVectoriser,我们将很快使用它来为我们自己使用 tf-idf。

既然我们已经理解了单词包背后的理论,并且可以看到我们可以利用的技术选项的范围,一旦我们开发了单词使用的载体,我们应该讨论如何实现单词包。单词包可以很容易地用作熟悉模型的包装。虽然一般来说,子空间方法可以使用一系列基本模型中的任何一个(支持向量机和线性回归模型是常见的),但是在一包单词实现中使用随机森林是非常常见的,将准备和学习总结成一个简洁的脚本。在这种情况下,我们将暂时独立使用单词包,通过随机森林实现为下一节保存分类!

虽然我们将在第 8 章集成方法中更详细地讨论随机森林(描述了我们可以创建的各种类型的集成),但现在注意到随机森林是一组决策树是有帮助的。它们是强大的集成模型,要么并行运行(产生投票或其他净结果),要么相互促进(通过迭代添加一棵新树来模拟解决方案中现有树集无法很好地模拟的部分)。

由于随机森林的强大和易用性,它们通常被用作基准算法。

同样,实现单词包的过程相当简单。我们初始化我们的打包工具(事实上称为矢量器)。注意,在这个例子中,我们对特征向量的大小进行了限制。这很大程度上是为了给自己节省一些时间;每个文档必须与特性列表中的每个项目进行比较,所以当我们开始运行我们的分类器时,这可能需要一点时间!

from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer(analyzer = "word",   \
                             tokenizer = None,    \
                             preprocessor = None, \
                             stop_words = None,   \
                             max_features = 5000) 

我们的下一步是通过fit_transform在我们的单词数据上安装矢量器;作为拟合过程的一部分,我们的数据被转换成特征向量:

train_data_features = vectorizer.fit_transform(words)

train_data_features = train_data_features.toarray()

这就完成了对文本数据的预处理。我们已经通过一整套文本挖掘技术获取了这个数据集,遍历了每种技术背后的理论和推理作为,并使用了一些强大的 Python 脚本来处理我们的测试数据集。我们现在处于一个很好的位置来尝试一下卡格尔的侮辱检测挑战!

测试我们准备的数据

那么,现在我们已经完成了数据集的一些初始准备,让我们来试一试真正的问题,看看我们是如何做的。为了帮助设置场景,让我们考虑一下无常的指导和数据描述:

这是一个单类分类问题。标签或者是 0 表示中性评论,或者是 1 表示侮辱性评论(中性可以被认为不属于侮辱类。您的预测必须是[0,1]范围内的实数,其中 1 表示 100%有信心预测评论是一种侮辱。

  • 我们正在寻找旨在侮辱作为更大的博客/论坛对话一部分的人的评论。
  • 我们不寻找针对非参与者(如名人、公众人物等)的侮辱。).
  • 侮辱可能包含亵渎、种族诽谤或其他冒犯性语言。但通常情况下,他们不会。
  • 含有亵渎或种族诋毁的言论,但不一定是对另一个人的侮辱,则被认为不具有侮辱性。
  • 评论的侮辱性要明显,不能含蓄。
  • 标签中可能有少量噪音,因为它们没有经过仔细清洁。然而,参赛者可以确信训练和测试数据中的误差是< 1%。

还应提醒参赛选手,这一问题往往会严重超标。所提供的数据通常代表完整的测试集,但无论如何都不是详尽的。无常将根据从广泛样本中提取的一组未公布的数据进行最终评估。

这是非常好的指导,因为它提出了两个特别的注意点。期望的分数是曲线 ( AUC )下的 区域,这是一个对假阳性和不正确阴性结果(特异性和敏感性)都非常敏感的指标。

指南明确指出,需要连续预测,而不是二进制 0/1 输出。这在使用 AUC 时变得至关重要;如果你只使用分类值,即使给出少量不正确的预测也会从根本上降低一个人的分数。这表明,与其使用RandomForestClassifier算法,我们更希望使用RandomForestRegressor,一种专注于回归的替代方法,然后在 0 和 1 之间重新调整结果。

真正的卡格尔竞赛是在一个更具挑战性和现实的环境中进行的——一个没有正确解决方案的环境。在第 8 章集成方法中,我们将探索顶级数据科学家如何在这样的环境中做出反应并茁壮成长。目前,我们将利用这一能力来确认我们在测试数据集上是否做得很好。请注意,这一优势也存在风险;如果问题过于严重,我们将需要遵守纪律,以确保我们没有在测试数据上过度训练!

此外,我们还有一个好处,就是能够看到真正的选手表现得有多好。虽然我们将真正的讨论留到第 8 章合奏方法中,但是可以合理地预计每个高排名选手都提交了相当多的失败尝试;有一个基准将帮助我们判断我们是否朝着正确的方向前进。

具体来说,私人(测试)排行榜的前 14 名参与者设法达到了超过 0.8 的 AUC 分数。最佳射手获得了令人印象深刻的 0.84 分和 0.84 分,而参赛的 50 支球队中有一半以上的得分超过了 0.77 分

*正如我们之前讨论的,让我们从随机森林回归模型开始。

一个随机森林是决策树的集合。

虽然单个决策树可能会受到方差或偏差相关问题的影响,但随机森林能够使用多个平行试验的加权平均值来平衡建模结果。

随机森林的应用非常简单,是应对新数据挑战的良好的第一步技术;在早期对数据应用随机森林分类器使您能够很好地理解初始、基线分类精度是什么样的,并对分类边界是如何形成的给出有价值的见解;在使用数据集的初始阶段,这种洞察力是非常宝贵的。

Scikit-learn 提供了RandomForestClassifier来实现随机森林算法的简单应用。

对于第一遍,我们将使用 100 棵树;增加树的数量可以提高分类精度,但需要额外的时间。一般来说,在模型创建的早期阶段尝试快速迭代是明智的;你重复运行模型的速度越快,你就能越快地了解你的结果是什么样的,以及如何改进它们!

我们从开始初始化和训练我们的模型:

trollspotter = RandomForestRegressor(n_estimators = 100, max_depth = 10, max_features = 1000)

y = trolls["y"]

trollspotted = trollspotter.fit(train_data_features, y)

然后,我们获取测试数据,并应用我们的模型来预测每个测试用例的得分。我们使用一种简单的拉伸技术重新调整这些分数:

moretrolls = pd.read_csv('moretrolls.csv', header=True, names=['y', 'date', 'Comment', 'Usage'])
moretrolls["Words"] = moretrolls["Comment"].apply(cleaner)

y = moretrolls["y"]

test_data_features = vectorizer.fit_transform(moretrolls["Words"])
test_data_features = test_data_features.toarray()

pred = pred.predict(test_data_features)
pred = (pred - pred.min())/(pred.max() - pred.min())

最后,我们应用roc_auc函数计算模型的 AUC 分数:

fpr, tpr, _ = roc_curve(y, pred)
roc_auc = auc(fpr, tpr)
print("Random Forest benchmark AUC score, 100 estimators")
print(roc_auc)

正如我们所看到的,结果肯定没有达到我们希望的水平:

Random Forest benchmark AUC score, 100 estimators
0.537894912105

值得庆幸的是,我们可以尝试在这里配置许多选项:

  • 我们处理输入的方法(预处理步骤和标准化)
  • 我们的随机森林中估计量的数量
  • 我们选择使用的分类器
  • 我们的单词包实现的属性(尤其是最大术语数)
  • 我们的 n-gram 标记器的结构

接下来,让我们调整单词实现包的大小,将术语上限从稍微任意的 5000 个术语增加到最多 8000 个术语;我们将在一个范围内运行,看看我们能学到什么,而不是只挑选一个值。我们还会将树的数量增加到更合理的数量(在本例中,我们增加到1000):

Random Forest benchmark AUC score, 1000 estimators
0.546439310772

这些结果略好于前一组,但并不显著。他们离我们想去的地方绝对有一段距离!让我们更进一步,设置一个不同的分类器。让我们尝试一个相当熟悉的选择——SVM。我们将建立我们自己的 SVM 对象来处理:

class SVM(object):

    def __init__(self, texts, classes, nlpdict=None):

        self.svm = svm.LinearSVC(C=1000, class_weight='auto')
        if nlpdict:
            self.dictionary = nlpdict
        else:
            self.dictionary = NLPDict(texts=texts)
        self._train(texts, classes)

    def _train(self, texts, classes):
        vectors = self.dictionary.feature_vectors(texts)
        self.svm.fit(vectors, classes)

    def classify(self, texts):
        vectors = self.dictionary.feature_vectors(texts)
        predictions = self.svm.decision_function(vectors)
        predictions = p.transpose(predictions)[0:len(predictions)]
        predictions = predictions / 2 + 0.5
        predictions[predictions > 1] = 1
        predictions[predictions < 0] = 0
        return predictions

虽然SVM的工作方式对人类评估来说几乎是不可理解的,但作为一种算法,它可以有效地运行,迭代地将数据集转换成多个额外的维度,以便在最佳类边界创建复杂的超平面。因此,看到我们的分类质量有所提高并不令人感到惊讶:

SVM AUC score
0.625245653817

也许我们没有充分了解我们的结果正在发生什么。让我们尝试用不同的方式来衡量绩效。具体来说,让我们看看模型的标签预测和实际目标之间的差异,看看在某些类型的输入下,模型是否更频繁地失败。

所以我们的预测已经走了很远。虽然我们仍有许多选择,但值得考虑使用更复杂的模型集合作为可靠的选择。在这种情况下,利用多个模型而不是一个模型可以使我们获得每个模型的相对优势。要针对此示例尝试合奏,请运行score_trolls_blendedensemble.py脚本。

这套服装是一套混合/叠加服装。我们将在第八章合奏方法中花更多的时间讨论这个合奏是如何工作的!

绘制结果时,我们可以看到性能有所提高,但幅度远低于我们的预期:

Testing our prepared data

我们显然在根据这些数据构建模型时遇到了一些问题,但是在这一点上,用一个更发达的模型来解决这个问题并没有太大的价值。我们需要回到我们的特性,并致力于扩展特性集。

在这一点上,值得从这场特殊的卡格尔竞赛中最成功的参赛者之一那里得到一些启示。一般来说,得分最高的条目往往是通过发现输入数据周围的所有技巧来开发的。这个数据集来自于一个名为 tuzzeg 的用户。这位选手在 https://github.com/tuzzeg/detect_insults 提供了一个可用的代码库。

Tuzzeg 的实现与我们的不同之处在于更彻底。除了我们使用词性标注构建的基本特征之外,他还使用了基于词性的二元模型和三元模型以及子序列(从 N 个术语的滑动窗口创建)。他处理了多达 7 克的 n 克,并创建了长度为 2、3 和 4 的字符 n 克。

此外,tuzzeg 花时间创建了两种类型的复合模型,这两种模型都被纳入了他的解决方案——句子级别和排名模型。通过将数据中的案例转化为排序的连续值,排名将我们围绕问题本质的合理化向前推进了一步。

同时,他开发的创新句级模型在训练数据中专门针对单句案例进行了训练。为了对测试数据进行预测,他将案例分成几个句子,分别对每个句子进行评估,只对案例中的句子进行最高分。这是为了适应这样一种预期,即在自然语言中,说话者经常将侮辱性评论限制在他们讲话的一个部分。

Tuzzeg 的模型创建了 100 多个特征组(其中基于词干的二元模型是一个示例特征组——二元模型过程创建了一个特征向量意义上的组),最重要的特征组(按影响排序)如下:

stem subsequence based         0.66
stem based (unigrams, bigrams) 0.18
char ngrams based (sentence)   0.07
char ngrams based              0.04
all syntax                     0.006
all language models            0.004
all mixed                      0.002

这很有趣,因为它表明我们目前没有使用的一组功能翻译对于生成可用的解决方案很重要。特别是,基于子序列的特征距离我们的初始特征集只有很短的一步,这使得添加额外的特征变得简单明了:

def subseq2(n, xs):
  l = len(xs)
  return ['%s %s' % (xs[i], xs[j]) for i in xrange(l-1) for j in xrange(i+1, i+n+1) if j < l]

def getSubseq2(seqF, n):
  def f(row):
    seq = seqF(row)
    return set(seq + subseq2(n, seq))
  return f

Subseq2test = getSubseq2(line, 2)

这种方法产生了极好的结果。虽然我鼓励您导出 Tuzzeg 自己的解决方案并应用它,但您也可以查看本项目存储库中提供的score_trolls_withsubseq.py脚本,了解如何整合强大的附加功能。

添加了这些附加功能后,我们看到 AUC 分数有了显著提高:

Testing our prepared data

运行此代码可以提供非常健康的0.834 AUC 分数。这只是为了展示深思熟虑和创新的特征工程的力量;虽然本章中生成的特定特征在其他上下文中会很好地为您服务,但是特定的假设(例如多句评论中的敌对评论被隔离到特定的句子中)会导致非常有效的特征。

由于我们在本章中有幸对照测试数据检查了我们的推理,我们不能合理地说我们已经在类似生活的条件下工作了。我们没有通过自己查看测试数据来利用获得测试数据的机会,但是可以公平地说,知道私人排行榜在这次挑战中的得分会让我们更容易找到正确的解决方案。在第 8 章集合方法中,我们将以更加严谨和现实的方式处理另一个棘手的卡格尔问题。我们还将深入讨论合奏!

进一步阅读

本章开头的引文来自可读性很强的卡格尔博客《没有免费预感》。参考http://blog.kaggle.com/2014/08/01/learning-from-the-best/

理解 NLP 任务有很多好的资源。一篇相当全面的八部分文章可以在网上找到。

如果您热衷于入门,一个很好的选择是尝试 Kaggle 的 for Knowledge NLP 任务,它非常适合作为本章中描述的技术的测试平台:https://www . Kaggle . com/c/word 2 vec-NLP-tutorial/details/part-1-for-初学者-单词包

本章引用的卡格尔竞赛可在https://www . Kaggle . com/c/检测-社交侮辱-评论获得。

对于有兴趣进一步描述 ROC 曲线和 AUC 测量的读者,请考虑 Tom Fawcett 的精彩介绍,可在https://ccrma . Stanford . edu/workshop/mir 2009/references/rocintro . pdf获得。

总结

在本章中,我们已经了解了许多有用且高度适用的技能。在这一章中,我们采用了一组凌乱、复杂的文本数据,并通过一系列严格的步骤,将其转化为一大组有效的特性。我们首先学习了一套数据清理技巧,剔除了大量的噪音和问题元素,然后我们使用词性标注和单词包将文本转化为特征。在这个过程中,你学会了应用一套广泛适用且非常有效的技术,使我们能够在许多自然语言处理环境中解决困难的问题。

通过对多个单个模型和集成的实验,我们发现,在一个更智能的算法可能不会产生强有力的结果的地方,彻底和创造性的特征工程可以在模型性能方面产生巨大的改进。***

七、特征工程第二部分

简介

我们已经认识到特征工程的重要性。在前一章中,我们讨论了一些技术,这些技术使我们能够从一系列特征中进行选择,并有效地将原始数据转换为特征,这些特征可以通过我们迄今为止讨论的高级 ML 算法进行有效处理。

格言垃圾进来,垃圾出去在这种情况下是相关的。在前面的章节中,我们已经看到了图像识别和自然语言处理任务是如何需要精心准备的数据的。在这一章中,我们将看到一种更普遍的数据类型:从现实应用程序中收集的定量或分类数据。

我们将在本章中使用的数据类型在许多上下文中都很常见。我们可以讨论从森林、游戏机或金融交易中的传感器获取的遥测数据。我们可以利用通过研究收集的地质调查信息或生物测定数据。无论如何,核心原则和技术保持不变。

在本章中,您将学习如何询问这些数据以剔除或减轻质量问题,如何将其转换为有利于机器学习的形式,以及如何创造性地增强这些数据。

总的来说,我们将在本章中讨论的概念如下:

  • 特征集创建的不同方法及特征工程的局限性
  • 如何使用大量技术来增强和改进初始数据集
  • 如何结合和使用领域知识来理解有效的选项,以转换和提高现有数据的清晰度
  • 我们如何测试单个功能和功能组合的价值,以便只保留我们需要的东西

虽然我们将从底层概念的详细讨论开始,但在本章结束时,我们将进行多次迭代试验,并使用专门的测试来了解我们正在创建的特性对我们的帮助有多大。

创建特征集

成功的机器学习最重要的因素是你输入数据的质量。一个好的模型,如果有误导性的、不恰当的规范化的或不具信息性的数据,那么在模型运行适当准备的数据时,将不会看到同样的成功水平。

在某些情况下,您可以指定数据收集,或者访问一组有用的、大量的、多样的源数据。有了正确的知识和技能,您可以使用这些数据来创建非常有用的特征集。

一般来说,对于如何构建好的特征集有很强的知识是非常有帮助的,因为它使您能够审计和评估任何新的数据集,以发现错过的机会。在本章中,我们将介绍一个设计过程和技术集,使创建有效的特征集变得更加容易。

因此,我们将从讨论一些我们可以用来扩展或重新解释现有特性的技术开始,潜在地创建大量有用的参数来包含在我们的模型中。

然而,正如我们将会看到的,有效使用特征工程技术是有限制的,我们需要注意工程数据集周围的风险。

最大似然应用的工程特性

我们已经讨论了您可以做些什么来修补数据中的数据质量问题,并且我们已经讨论了如何在您必须加入到外部数据中的维度中创造性地使用维度。

一旦你面前有了一组相当好理解和经过质量检查的数据,在你能够从这些数据中产生有效的模型之前,通常还需要大量的工作。

使用重新缩放技术来提高特征的可学习性

将未准备好的数据直接输入许多机器学习模型的主要挑战是算法对不同变量的相对大小敏感。如果数据集有多个范围不同的参数,一些算法会将方差较大的变量视为比具有较小值和较小方差的算法更显著的变化。

解决这个潜在问题的关键是重新缩放,这是一个调整参数值相对大小的过程,同时保留每个参数中值的初始顺序(单调转换)。

如果在训练之前对输入数据进行缩放,梯度下降算法(包括大多数深度学习算法—http://sebastianruder.com/optimizing-gradient-descent/)的效率会显著提高。为了理解为什么,我们将求助于画一些图片。给定的一系列培训步骤可能如下所示:

Using rescaling techniques to improve the learnability of features

当应用于未缩放的数据时,这些训练步骤可能无法有效收敛(如下图中的左侧示例所示)。

由于每个参数具有不同的标度,模型试图训练的参数空间可能会高度失真和复杂。这个空间越复杂,在其中训练模型就越困难。总的来说,这是一个可以通过隐喻有效描述的复杂主题,但是对于寻求更全面解释的读者来说,在本章的进一步阅读部分有一个很好的参考。就目前而言,将训练中的梯度下降模型视为像大理石滚下斜坡一样的行为并不是没有道理的。这些弹珠容易卡在斜坡上的鞍点或其他复杂几何形状中(在这种情况下,这是由我们的模型的目标函数创建的表面——我们的模型通常训练其输出最小化的学习函数)。然而,通过缩放数据,表面变得更加规则,训练可以变得更加有效:

Using rescaling techniques to improve the learnability of features

经典的例子是 01 之间的线性重新缩放;用这种方法,最大的参数值被重新调整到 1 ,最小的被调整到 0 ,中间值落在 0-1 区间,与它们相对于最大和最小值的原始大小成比例。在这样的变换下,矢量【0,10,25,20,18】将变为【0,0.4,1,0.8,0.72】

这种转换的特殊价值在于,对于原始形式中幅度可能不同的多个数据点,重新缩放的特征将位于相同的范围内,从而使您的机器学习算法能够在有意义的信息内容上进行训练。

这是最直接的缩放选项,但是有一些非线性缩放选项,在正确的情况下会更有帮助;其中包括平方缩放、平方根缩放,最常见的可能是对数缩放。

参数值的对数标度在物理学和底层数据经常受幂律影响的环境中非常常见(例如, y 的指数增长与 x 的线性增长一致)。

与线性重新缩放不同,对数缩放调整数据案例之间的相对间距。这可能是一把双刃剑。一方面,对数标度能很好地处理外围情况。让我们来看一个描述虚构群体成员个人净财富的示例数据集,由以下汇总统计数据描述:

Using rescaling techniques to improve the learnability of features

在重新调整之前,这一群体严重倾向于拥有荒谬净资产的个人。每十分之一的病例分布如下:

Using rescaling techniques to improve the learnability of features

在对数标度之后,这种分布更加友好:

Using rescaling techniques to improve the learnability of features

我们可以选择进一步缩放,并通过这样做来绘制这个分布的前半部分。在这种情况下,log-10 规范化显著降低了这些外围值的影响,使我们能够在数据集中保留异常值,而不会丢失低端的细节。

尽管如此,重要的是要注意,在某些情况下,聚类情况的相同增强会增强不同参数值中的噪声,并产生值之间更大间距的错误印象。这往往不会对对数标度处理异常值的方式产生负面影响;这种影响通常出现在原始值非常相似的小值案例组中。

通过对数标度引入非线性带来的挑战是巨大的,一般来说,非线性标度只推荐用于你理解的变量,并且它们之间有非线性关系或趋势。

创建有效的衍生变量

在许多机器学习应用中(例如,几乎所有的神经网络),重新缩放是预处理的标准部分。除了重新缩放之外,还有其他准备技术,可以通过战略性地减少输入模型的参数数量来提高模型性能。最常见的例子是派生度量,它采用多个现有数据点,并在单个度量中表示它们。

这些是极其普遍的;例子包括加速度(作为来自两个时间点的速度值的函数)、体重指数(作为身高、体重和年龄的函数)和股票评分的 市盈率 ( 市盈率)比率。本质上,你曾经遇到的任何派生分数、比率或复杂度量都是由多个组成部分形成的组合分数。

对于熟悉环境中的数据集,许多这些预先存在的度量将是众所周知的。然而,即使在相对知名的领域,使用领域知识和现有数据的混合来寻找新的支持措施或转换也是非常有效的。思考派生度量选项时,一些有用的概念如下:

  • 两个变量组合:作为 m 参数的函数的 n 参数的乘法、除法或归一化。
  • 随时间变化的度量:这里的一个经典例子是一个度量中的加速度或 7D 变化。在更复杂的情况下,基础时间序列函数的斜率可能是一个有用的参数,而不是直接使用当前和过去的值。
  • 减去基线:使用基本预期(一个平坦的预期,如基线流失率)根据该基线重新预测一个参数,可以更直接地了解同一变量。对于流失示例,我们可以生成一个参数,该参数根据与预期的偏差来描述流失。同样,在股票交易的情况下,我们可以根据开盘价来看收盘价。
  • 归一化:根据前面的情况,基于另一个参数或基线的值对参数值进行归一化,该参数或基线是在给定其他变量属性的情况下动态计算的。这里的一个例子是失败交易率;除了将此值视为原始(或重新缩放的)计数之外,根据尝试的事务对其进行规范化通常也是有意义的。

这些不同元素的创造性重组让我们建立非常有效的分数。例如,有时,告诉我们客户参与度(下降或增加)随时间变化的斜率的参数需要以该客户之前是高度参与还是几乎不参与为条件,因为参与度的轻微下降在每种情况下可能意味着非常不同的事情。数据科学家的工作是有效地和创造性地为给定的领域捕获这些微妙的特征集。

到目前为止,这种讨论主要集中在数字数据上。然而,有用的数据通常被锁在非数字参数中,如代码或分类数据。因此,我们接下来将讨论一组将非数字特征转化为可用参数的有效技术。

重新解释非数字特征

一个常见的挑战是如何处理非数字特征,这可能是有问题的,也可能是特定问题的。通常,有价值的信息被编码在非数字速记值中。例如,在股票交易中,股票本身的身份(例如,AAPL)以及买方和卖方的身份是有趣的信息,我们期望这些信息与我们的问题有意义地联系起来。进一步举这个例子,我们可能还会期望一些股票的交易与其他股票不同,即使是在行业内,公司内部的组织差异也提供了重要的背景,这些差异可能发生在某些或所有时间点。

在某些情况下,一个简单的选择是构建一个聚合或一系列聚合。最明显的例子是出现次数,可以创建扩展度量(两个时间窗口之间的计数变化),如前一节所述。

构建汇总统计数据并减少数据集中的行数会带来减少模型可用信息量的风险(增加模型脆弱性和过度拟合的风险)。因此,广泛地聚合和减少输入数据通常不是一个好主意。深度学习技术更是如此,比如第 2-4 章中讨论和使用的算法。

与其大量使用基于聚合的方法,不如让我们看看将字符串编码值转换为数字数据的另一种方法。另一类非常流行的技术是编码,最常见的编码策略是一次性编码。One-hot 编码是将一系列分类响应(例如,年龄组)转换为一组二进制变量的过程,每个响应选项(例如,18-30)都由其自己的二进制变量表示。这在视觉上更直观:

Reinterpreting non-numeric features

编码后,这个分类变量和连续变量的数据集成为二元变量的张量:

Reinterpreting non-numeric features

这呈现的优势是显著的;它使我们能够挖掘包含在大量数据集中的非常有价值的标签信息,而不会聚合或降低数据的信息内容。此外,one-hot 允许我们将编码变量的特定响应代码分成单独的特征,这意味着我们可以为特定变量识别或多或少有意义的代码,并且只保留重要的值。

另一种非常有效的技术,主要用于文本代码,被称为哈希技巧。简单来说,散列是将数据转换成数字表示的函数。散列对许多人来说是一个熟悉的概念,因为它们经常被用来编码敏感的参数和总结庞大的数据。然而,为了最大限度地利用哈希技巧,了解技巧是如何工作的以及可以用它做什么是很重要的。

我们可以使用散列法将一个文本短语转换成一个数值,用作该短语的标识符。虽然不同的散列算法有许多应用,但在这种情况下,即使是简单的散列也可以直接将字符串键和代码转换为我们可以有效建模的数字参数。

一个非常简单的散列可以把每个字母字符变成一个相应的数字。 a 会变成 1b 会变成 2 ,以此类推。通过对这些值求和,可以为单词和短语生成哈希值。短语卡特彼勒 gif在此方案下的翻译如下:

Cat: 3 + 1 + 20
Gifs: 7 + 9 + 6 + 19
Total: 65

这是一个可怕的散列,原因有二(完全无视输入包含垃圾词的事实!).首先,它可以呈现多少输出没有真正的限制。当人们记得散列技巧的全部要点是提供降维时,从散列中可能输出的数量必须是有限的,这是理所当然的!大多数散列限制了它们输出的数字的范围,因此选择散列的部分决定与您希望模型具有的特征的数量有关。

一种常见的行为是选择 2 的幂作为散列范围;这有助于在哈希过程中允许按位运算,从而加快速度。

这种杂凑很糟糕的另一个原因是对单词的改变影响很小,而不是很大。如果变成了蝙蝠,我们希望我们的哈希输出发生实质性的变化。而是变化一(变成 64 )。一般来说,一个好的散列函数是输入文本中的一个小变化会导致输出中的一个大变化。这部分是因为语言结构趋向于非常一致(因此得分相似),但是给定结构内稍微不同的名词和动词集合趋向于赋予彼此非常不同的含义(猫坐在垫子上对比汽车坐在猫身上)。

所以我们已经描述了散列。哈希技巧让事情更进一步。假设,把每个单词都变成一个散列的数字代码将导致大量的散列冲突——两个单词具有相同散列值的情况。自然,这些是相当糟糕的。

很容易地,不同术语使用频率的分布对我们有利。称为 齐夫分布,它要求遇到第 n 个最常见项的概率近似为 P(n) = 0.1/n 直到大约 1000(齐夫定律)。这意味着每一项都比前一项更不容易遇到。在 n = 1000 之后,术语往往足够模糊,以至于在一个数据集中不太可能遇到两个具有相同散列的术语。

同时,一个好的散列函数的范围有限,并且会受到输入的微小变化的显著影响。这些属性使得哈希冲突机会在很大程度上与术语使用频率无关。

这两个概念——齐夫定律和一个好的散列与散列冲突机会和术语使用频率的独立性——意味着散列冲突的机会非常小,并且当一个散列冲突发生时,它极有可能在两个不常用的单词之间。

这给了哈希技巧一个特殊的属性。也就是说,与在未处理的词包特征上的训练相比,在不降低在散列数据上训练的模型的性能的情况下,可以大规模地降低一组文本输入数据的维度(从数万个自然出现的词到几百个或更少)。

正确使用哈希技巧可以实现很多可能性,包括对我们讨论的技术的扩展(特别是单词包)。本章末尾的进一步阅读一节中包含了对不同哈希实现的参考。

使用特征选择技术

现在我们有了一个很好的特征创建选项选择,以及对创造性特征工程可能性的理解,我们可以开始将我们现有的特征构建成更有效的变体。鉴于这一新发现的功能工程技能集,我们面临创建大量难以管理的数据集的风险。

无限制地添加特征会增加模型脆弱性和对某些类型模型过度拟合的风险。这与你试图模拟的趋势的复杂性有关。在最简单的情况下,如果您试图识别两个大组之间的显著区别,那么您的模型可能支持大量功能。但是,随着您需要适应的模型变得更加复杂,以及您必须处理的组变得越来越小,添加越来越多的特征会损害模型一致有效地分类的能力。

这一挑战因以下事实而变得更加复杂:哪个参数或变体最适合该任务并不总是显而易见的。适用性可能因基础模型而异;例如,决策森林在单调变换(也就是说,保持数据案例初始排序的变换)中表现不佳;一个例子是对数缩放)而不是未缩放的基础数据;但是,对于其他算法,选择重新缩放和使用的重新缩放方法都是非常有影响的选择。

传统上,特征的数量和参数数量的限制与开发将关键输入与期望的结果分数相关联的数学函数的愿望相关联。在这种情况下,需要加入额外的参数作为移动或有害变量。

每个新参数都引入了另一个维度,这使得建模的关系更加复杂,结果模型更有可能过度拟合现有数据。一个简单的例子是,如果您引入一个参数,它只是每个案例的唯一标签;在这一点上,您的算法将只学习那些标签,使得当您的模型被引入新的数据集时,它很可能完全失败。

不那么琐碎的例子同样问题重重;当您的功能将案例分成非常小的组时,案例与功能的比例变得非常重要。简而言之,增加建模函数的复杂性会导致模型更容易过度拟合,而添加特征会加剧这种影响。根据这个原则,我们应该从非常小的数据集开始,并且只有在证明它们改进了模型之后才添加参数。

然而,在最近,一种相反的方法论——现在被普遍认为是做数据科学的一种常见方式的一部分——已经取得了进展。这种方法表明,增加非常大的特征集来整合每一个潜在的有价值的特征是一个好主意,并且降低到一个更小的特征集来完成这项工作。

这种方法得到了一些技术的支持,这些技术能够在庞大的特征集(可能有数百或数千个特征)上做出决策,并且倾向于以蛮力的方式进行操作。这些技术将彻底测试特征组合,串联或并联运行模型,直到识别出最有效的参数子集。

这些技术起作用,这就是为什么这种方法变得流行。如果不使用这些技术,了解它们肯定是值得的,所以在本章的后面,您将学习如何应用它们。

使用暴力技术进行特征选择的主要缺点是,很容易相信算法的结果,而不管它选择的特征实际上意味着什么。明智的做法是在高效黑盒算法的使用和领域知识以及对正在进行的工作的理解之间取得平衡。因此,本章将使您能够使用两种范式(构建构建)的技术,以便您能够适应不同的上下文。我们将从学习如何缩小您必须处理的特征集开始,从许多特征到最有价值的子集。

执行特征选择

构建了一个大型数据集后,人们面临的下一个挑战往往是如何缩小选项范围,只保留最有效的数据。在这一节中,我们将讨论支持特征选择的各种技术,它们可以自己工作,也可以作为熟悉算法的包装器。

这些技术包括相关分析、正则化技术和递归特征消除 ( RFE )。当我们完成后,您将能够自信地使用这些技术来支持您的特征集选择,每次使用新数据集时,都有可能为自己节省大量工作!

相关性

我们将从寻找回归模型主要问题的简单来源:多重共线性开始我们对特征选择的讨论。多重共线性是数据集中要素之间中度或高度相关性的奇特名称。一个显而易见的例子是披萨切片计数如何与披萨价格共线。

多重共线性有两种类型:结构性的和基于数据的。当创建新要素(例如来自要素 f 的要素 f1 )时,会出现结构多重共线性,这可能会导致多个要素之间高度相关。当两个变量受同一致病因素影响时,基于数据的多重共线性倾向于发生。

这两种多重共线性都会造成一些不良影响。特别是,我们的模型的性能往往会受到所使用的特征组合的影响;当使用共线特征时,我们模型的性能将会下降。

无论是哪种情况,我们的方法都很简单:我们可以测试多重共线性,并移除表现不佳的特征。自然,性能不佳的特性对模型性能的贡献很小。它们可能表现不佳,因为它们复制了其他功能中可用的信息,或者它们可能根本没有提供对当前问题有意义的数据。有多种方法可以测试弱特征,因为许多特征选择技术会筛选出多共线特征组合,如果表现不佳,建议将其移除。

此外,还有一个具体的多重共线性检验值得考虑;即检查数据相关矩阵的特征值。特征向量和特征值是矩阵理论中的基本概念,有许多突出的应用。更多细节将在本章末尾给出。就目前而言,可以说数据集生成的相关矩阵中的特征值为我们提供了多重共线性的量化度量。考虑一组特征值来表示我们的特征给数据集带来了多少“新信息内容”;低特征值表明数据可能与其他特征相关。例如,在工作中,考虑以下代码,该代码创建一个特征集,然后将共线性添加到特征 024 :

import numpy as np

x = np.random.randn(100, 5) 
noise = np.random.randn(100)
x[:,4] = 2 * x[:,0] + 3 * x[:,2] + .5 * noise 

当我们生成相关矩阵并计算特征值时,我们发现如下:

corr = np.corrcoef(x, rowvar=0)
w, v = np.linalg.eig(corr)

print('eigenvalues of features in the dataset x')
print(w)

eigenvalues of features in the dataset x
[ 0.00716428  1.94474029  1.30385565  0.74699492  0.99724486]

显然,我们的第 0 个特征是可疑的!然后我们可以通过调用v来检查这个特征的特征值:

print('eigenvalues of eigenvector 0')
print(v[:,0])

eigenvalues of eigenvector 0
[-0.35663659 -0.00853105 -0.62463305  0.00959048  0.69460718]

从位置一和位置三的特征的小值,我们可以看出特征 24 与特征 0 高度多共线。在继续之前,我们应该删除这三个特征中的两个!

套索

正则化方法是最有帮助的特征选择技术,因为它们提供了稀疏的解决方案:较弱的特征返回零,只留下具有真实系数值的特征子集。

两种最常用的正则化模型是 L1 正则化和 L2 正则化,在线性回归环境中分别称为 LASSO 和岭回归。

正则化方法通过在损失函数中增加一个惩罚来发挥作用。该惩罚导致 E(X,Y) + a||w|| ,而不是最小化损失函数 E(X,Y) 。超参数 a 与正则化的数量有关(使我们能够调整正则化的强度,从而调整所选原始特征集的比例)。

在 LASSO 正则化中,使用的具体罚函数是 α∑ni=1|wi| 。每个非零系数增加了惩罚项的大小,迫使较弱的特征返回 0 的系数。使用 scikit-learn 对超参数的参数优化支持,可以选择合适的惩罚项。在这种情况下,我们将使用estimator.get_params()来执行网格搜索,以获得合适的超参数值。有关网格搜索如何操作的更多信息,请参见本章末尾的进一步阅读部分。

在 scikit-learn 中,逻辑回归为分类提供了 L1 惩罚。同时,LASSO 模块是为线性回归提供的。现在,让我们从将 LASSO 应用于示例数据集开始。在本例中,我们将使用波士顿住房数据集:

fromsklearn.linear_model import Lasso
fromsklearn.preprocessing import StandardScaler
fromsklearn.datasets import load_boston

boston = load_boston()
scaler = StandardScaler()
X = scaler.fit_transform(boston["data"])
Y = boston["target"]
names = boston["feature_names"]

lasso = Lasso(alpha=.3)
lasso.fit(X, Y)

print "Lasso model: ", pretty_print_linear(lasso.coef_, names, sort = True)

Lasso model: -3.707 * LSTAT + 2.992 * RM + -1.757 * PTRATIO + -1.081 * DIS + -0.7 * NOX + 0.631 * B + 0.54 * CHAS + -0.236 * CRIM + 0.081 * ZN + -0.0 * INDUS + -0.0 * AGE + 0.0 * RAD + -0.0 * TAX

原始集合中的几个特征返回了0.0的相关性。增加相关性会使解决方案越来越稀疏。例如,当alpha = 0.4时,我们会看到以下结果:

Lasso model: -3.707 * LSTAT + 2.992 * RM + -1.757 * PTRATIO + -1.081 * DIS + -0.7 * NOX + 0.631 * B + 0.54 * CHAS + -0.236 * CRIM + 0.081 * ZN + -0.0 * INDUS + -0.0 * AGE + 0.0 * RAD + -0.0 * TAX

我们可以立即看到 L1 正则化作为特征选择技术的价值。然而,重要的是要注意,L1 正则化回归是不稳定的。当数据中的特征相关时,即使数据变化很小,系数也会有很大变化。

这个问题可以通过 L2 正则化或岭回归有效地解决,岭回归开发了具有不同应用的特征系数。L2 归一化在损失函数中增加了一个额外的惩罚,即 L2 范数惩罚。这种处罚的形式为( a∑ni=1w2i )。目光敏锐的读者会注意到,与 L1 罚函数不同( α∑ni=1|wi| ),L2 罚函数使用平方系数。这使得系数值更均匀地分布,并且具有附加效果,即相关特征倾向于接收相似的系数值。这显著提高了稳定性,因为系数不再因小的数据变化而波动。

然而,L2 归一化对特征选择没有 L1 那么直接有用。相反,由于有趣的特征(具有预测能力)往往具有非零系数,L2 作为一种探索性工具更有用,它允许推断分类中特征的质量。它具有比 L1 正则化更稳定和可靠的额外优点。

递归特征消除

RFE 是一个贪婪的迭代过程,充当另一个模型的包装器,比如 SVM (SVM-RFE),它反复运行输入数据的不同子集。

与 LASSO 和岭回归一样,我们的目标是找到性能最好的特征子集。顾名思义,在每次迭代中,都会留出一个要素,允许对其余要素集重复该过程,直到数据集中的所有要素都被消除。消除特征的顺序成为它们的等级。在用增量较小的子集进行多次迭代之后,每个特征都被精确地评分,并且可以选择相关的子集来使用。

为了更好地理解这是如何工作的,让我们看一个简单的例子。我们将使用(现在已经很熟悉的)数字数据集来理解这种方法在实践中是如何工作的:

print(__doc__)

from sklearn.svm import SVC
fromsklearn.datasets import load_digits
fromsklearn.feature_selection import RFE
importmatplotlib.pyplot as plt

digits = load_digits()
X = digits.images.reshape((len(digits.images), -1))
y = digits.target

我们将使用 SVM 作为我们的基础估计器,通过SVC算子进行支持向量分类 ( 支持向量机)。我们然后在这个模型上应用 RFE 包装。RFE 提出了几个论点,第一个是对选择估计量的引用。第二个论点是n_features_to_select,相当不言自明。如果特征集包含许多相互关联的特征,这些特征的子集具有高效分类特征的多元分布,则可以选择两个或多个特征的组合。

步进允许在每次迭代中移除多个特征。当给定一个介于 0.01.0 之间的值时,每一步都允许移除特征集的一个百分比,对应于步骤参数中给出的比例:

svc = SVC(kernel="linear", C=1)
rfe = RFE(estimator=svc, n_features_to_select=1, step=1)
rfe.fit(X, y)
ranking = rfe.ranking_.reshape(digits.images[0].shape)

plt.matshow(ranking)
plt.colorbar()
plt.title("Ranking of pixels with RFE")
plt.show()

假设我们熟悉数字数据集,我们知道每个实例都是一个 8×8 的手写数字图像,如下图所示。每个图像位于 8×8 网格的中心:

Recursive Feature Elimination

当我们对数字数据集应用 RFE 时,我们可以看到它在应用排名时广泛地捕获了这些信息:

Recursive Feature Elimination

要剪切的第一个像素在图像的垂直边缘(通常是空的)内部和周围。接下来,算法开始剔除图像垂直边缘或顶部附近的空白区域。保留时间最长的像素是那些能够最大程度区分不同字符的像素,这些像素对于某些数字是存在的,而对于其他数字是不存在的。

这个例子给了我们很好的视觉确认 RFE 的作品。它没有给我们的是该技术如何持续工作的证据。RFE 的稳定性取决于基本模型的稳定性,在某些情况下,岭回归将提供更稳定的解。(有关涉及哪些情况和条件的更多信息,请参考本章末尾的进一步阅读部分。)

遗传模型

在这一章的前面,我们讨论了能够在非常大的参数集下进行特征选择的算法的存在。这种类型的一些最突出的技术是遗传算法,它模拟自然选择来生成越来越有效的模型。

用于特征选择的遗传解决方案大致工作如下:

  • 将一组初始变量(预测因子是此上下文中通常使用的术语)组合成多个子集(候选),并为每个候选计算性能度量
  • 来自具有最佳性能的候选的预测器被随机重组到新的迭代(一代)模型中
  • 在该重组步骤中,对于每个子集,都有突变的概率,由此可以从子集添加或移除预测因子

该算法通常迭代多代。适当的迭代量取决于数据集的复杂性和所需的模型。与梯度下降技术一样,遗传算法的性能和迭代次数之间存在典型的关系,其中性能的提高随着迭代次数的增加而非线性下降,最终在过拟合风险增加之前达到最小值。

为了找到有效的迭代次数,我们可以使用训练数据进行测试;通过大量迭代运行模型并绘制均方根误差 ( RMSE ),我们能够在给定输入数据和模型配置的情况下找到合适的迭代次数。

让我们更详细地谈谈每一代人身上发生的事情。具体来说,我们来谈谈候选人是如何产生的,绩效是如何评分的,重组是如何进行的。

候选项最初被配置为使用可用预测值的随机样本。关于在第一代中使用多少预测器,没有硬性规定;这取决于有多少功能可用,但通常会看到第一代候选人使用 50%到 80%的可用功能(在功能较多的情况下使用较小的百分比)。

适合性度量可能很难定义,但是通常的做法是使用两种形式的交叉验证。内部交叉验证(仅在其自身参数的上下文中测试每个模型,而不比较模型)通常用于跟踪给定迭代的性能;来自内部交叉验证的适应度度量用于选择模型,以便在下一代中重新组合。还需要外部交叉验证(针对未在任何迭代中用于验证的数据集进行测试),以确认搜索过程生成的模型没有过度适应内部训练数据。

重组由三个关键参数控制:突变、交叉概率和精英化。后者是一个可选参数,人们可以使用它来保留当前世代中 n-许多表现最好的模型;通过这样做,可以防止特别有效的候选基因在重组过程中完全丢失。这可以在突变变体中使用该候选基因和/或将其用作下一代候选基因的亲本的同时进行。

突变概率定义了下一代模型被随机重新调整的机会(通过一些预测器,通常是一个,被添加或删除)。变异有助于遗传算法保持候选变量的广泛覆盖,降低陷入参数局部解的风险。

交叉概率定义了一对候选者被选择重组到下一代模型中的可能性。有几种交叉算法:可以将每个父要素集的一部分拼接(例如,前半部分/后半部分)到子要素中,或者可以随机选择每个父要素。默认情况下,也可能使用父母双方共有的功能。从父母的唯一预测者集合中随机抽样是一种常见的默认方法。

这些是通用遗传算法的主要部分,可以用作现有模型(逻辑回归、SVM 等)的包装器。这里描述的技术可以以许多不同的方式变化,并且与在多个定量领域中稍微不同地使用的特征选择技术相关。让我们把到目前为止已经讨论过的理论应用到一个实际的例子中。

实践中的特色工程

根据您正在使用的建模技术,其中一些工作可能比其他部分更有价值。深度学习算法在工程设计较少的数据上比在较浅的模型上表现更好,可能需要较少的工作来改进结果。

理解需要什么的关键是快速迭代从数据集获取到建模的整个过程。在第一次有明确的模型精度目标时,找到可接受的最小处理量并执行。尽可能了解结果,并为下一次迭代制定计划。

为了展示这在实践中的样子,我们将使用一个不熟悉的高维数据集,使用迭代过程来生成越来越有效的建模。

我最近住在温哥华。虽然它有许多积极的品质,但生活在城市中最糟糕的事情之一是有些不可预测的通勤。无论我是坐汽车旅行,还是乘坐 Translink 的 Skytrain 系统(一条单轨列车和过山车的高速线路),我都发现自己受到难以预测的延误和拥堵问题的困扰。

本着将我们的新功能工程技能付诸实践的精神,让我们看看是否可以通过采取以下步骤来改善这种体验:

  • 编写代码以从多个 API 获取数据,包括文本和气候流
  • 使用我们的特征工程技术从这个初始数据中导出变量
  • 通过生成通勤延迟风险评分来测试我们的功能集

不同寻常的是,在这个例子中,我们将不再关注构建和评分一个高性能的模型。相反,我们的重点是创建一个自给自足的解决方案,您可以根据自己的本地情况进行调整和应用。虽然采取这种方法符合本章的目标,但还有另外两个重要的动机。

首先,围绕分享和利用推特数据存在一些挑战。使用推特应用编程接口的部分条款是开发者有义务确保对时间线或数据集状态的任何调整(例如,包括删除推文)都在从推特上提取并公开共享的数据集中重现。这使得在本章的 GitHub 存储库中包含真实的 Twitter 数据变得不切实际。最终,由于用户需要构建自己的流并积累数据点,以及环境的变化(如季节变化)可能会影响模型性能,因此很难根据流数据提供任何下游模型的可再现结果。

这里的第二个要素很简单:不是每个人都住在温哥华!为了给最终用户带来一些有价值的东西,我们应该考虑一个可调整的通用解决方案,而不是一个特定地域的解决方案。

因此,下一节中介绍的代码旨在作为构建和开发的基础。它提供了作为成功的商业应用的基础的潜力,或者仅仅是一个有用的、数据驱动的生活帮。考虑到这一点,请查看本章的内容(并利用相关代码目录中的代码),以便找到并创建适合您自己的情况、本地可用数据和个人需求的新应用程序。

通过 RESTful APIs 获取数据

为了开始,我们需要收集一些数据!我们需要寻找以足够的频率(最好每个通勤周期至少一个记录)捕获的丰富的、有时间戳的数据,以便进行模型训练。

一个自然的开始是推特应用编程接口,它允许我们收集最近的推文数据。我们可以将这个应用编程接口分为两种用途。

首先,我们可以从官方交通机构(特别是公交和火车公司)获得推文。这些公司提供有关延误和服务中断的运输服务信息,对我们有帮助的是,这些信息采用了有利于标记工作的一致格式。

其次,我们可以通过收听感兴趣地理区域的推文来挖掘通勤情绪,使用定制的字典来收听与中断案例或其原因相关的术语。

除了挖掘数据的推特应用编程接口来支持我们的模型,我们还可以利用其他应用编程接口来提取丰富的信息。一个特别有价值的数据来源是 必应流量 API 。这个应用编程接口可以很容易地被调用来提供跨用户指定的地理区域的交通拥堵或中断事件。

此外,我们可以利用来自 雅虎天气 API 的天气数据。该应用编程接口提供给定位置的当前天气,采用邮政编码或位置输入。它提供了丰富的当地气候信息,包括但不限于温度、风速度、湿度、大气压力和能见度。此外,它还提供了当前条件的文本字符串描述以及预测信息。

虽然我们可以考虑将其他数据源结合到我们的分析中,但我们将从这些数据开始,看看我们是如何做的。

测试我们模型的性能

为了有意义地评估我们的通勤中断预测尝试,我们应该尝试定义测试标准和适当的绩效评分。

我们试图做的是识别每天当天通勤中断的风险。最好,我们想知道通勤风险,并提前通知我们可以采取行动来减轻风险(例如,提前离开家)。

为了做到这一点,我们需要三样东西:

  • 了解我们的模型将输出什么
  • 我们可以用来量化模型性能的度量
  • 我们可以使用一些目标数据,根据我们的衡量标准对模型性能进行评分

我们可以就为什么这很重要进行有趣的讨论。可以有效地说,有些模型是有目的的信息。可以说,我们的通勤风险评分是有用的,因为它产生了我们以前没有的信息。

然而,现实情况是,不可避免地会有一个性能标准。在这种情况下,可能只是我对模型输出的结果感到满意,但重要的是要意识到,总有一些性能标准在起作用。因此,量化性能是有价值的,即使在模型看起来是信息性的(甚至更好,不受监督)的情况下。这使得抵制放弃性能测试的诱惑变得谨慎;至少这样,你就有了一个量化的性能度量来迭代地改进。

一个合理的起点是断言我们的模型旨在输出给定日期出站(从家到工作)通勤的数值分数在 0-1 范围内。我们有几个关于如何呈现这个分数的选择;也许最明显的选择是对数据应用日志重新缩放。有充分的理由进行对数标度,在这种情况下,这可能不是一个坏主意。(通勤延迟时间的分布服从幂律并非不可能。)目前,我们不会重塑这组分数。相反,我们将等待查看我们模型的输出。

就提供实际指导而言, 0-1 的分数不一定很有帮助。我们可能会发现自己想要使用桶边界在 0-1 范围内的桶边界的桶系统(如高风险、中风险或低风险)。简而言之,我们将过渡到将问题视为具有分类输出(类标签)的多类分类问题,而不是具有连续输出的回归问题。

这可能会提高模型性能。(更具体地说,因为它将把自由误差幅度提高到相关桶的最大宽度,这是一个非常慷慨的性能度量。)同样,在第一次迭代中引入这种变化可能不是一个好主意。直到我们回顾了真实通勤延迟的分布,我们才知道阶级之间的界限在哪里!

接下来,我们需要考虑如何衡量模型的性能。选择合适的评分标准通常取决于问题的特点。我们有很多关于分类器性能评分的选项。(有关机器学习算法性能度量的更多信息,请参见本章末尾的进一步阅读部分。)

决定哪种性能度量适合手头的任务的一种方法是考虑混淆矩阵。混淆矩阵是一个偶然事件表;在统计建模的背景下,他们通常描述标签预测与实际标签的对比。为一个训练好的模型输出一个混淆矩阵是很常见的(特别是对于有更多类的多类问题),因为它可以产生关于按故障类型和类分类故障的有价值的信息。

在这种情况下,参考混淆矩阵更能说明问题。我们可以考虑以下简化矩阵来评估是否有我们不关心的意外情况:

Testing the performance of our model

在这种情况下,我们关心所有四种应急类型。假阴性会让我们陷入意想不到的延误,而假阳性会让我们提前出发去上班。这意味着我们需要一个既重视高灵敏度(真阳性率)又重视高特异性(假阳性率)的性能指标。考虑到这一点,理想的衡量标准是曲线下的面积(T2)。

第二个挑战是如何衡量这个分数;我们需要一些可以预测的目标。谢天谢地,这很容易获得。毕竟我每天都有通勤要做!我只是用秒表、一致的开始时间和一致的路线开始自我记录我的通勤时间。

重要的是认识到这种方法的局限性。作为一个数据源,我受制于自己的内部趋势。例如,我在早上喝咖啡之前有些懒散。同样,我自己的一贯通勤路线可能拥有其他路线所没有的本地趋势。从许多人和许多路线收集通勤数据会好得多。

然而,在某些方面,我对这个目标数据的使用感到满意。不仅仅是因为我试图对自己通勤路线的中断进行分类,并且不希望我的通勤时间的自然差异通过培训被误解,比如说,与其他通勤者群体或路线设定的目标相比较。此外,考虑到预期的日常轻微自然变化,功能模型应不予考虑。

就模型性能而言,很难判断什么足够好。更准确地说,不容易知道这个模型什么时候超过了我自己的预期。不幸的是,关于我自己的通勤延迟预测的准确性,我不仅没有任何非常可靠的数据,而且一个人的预测似乎不太可能推广到其他地方的其他通勤。训练一个模型超过一个相当主观的目标似乎是不明智的。

相反,让我们尝试超越一个相当简单的阈值——一个天真地认为每一天都不会包含通勤延迟的模型。这个目标具有反映我们实际行为的相当令人愉快的特性(因为我们倾向于每天起床,表现得好像不会有交通中断)。

在 85 个目标数据案例中,观察到 14 个通勤延误。基于这个目标数据和我们创建的评分标准,我们的目标是 0.5

推特

鉴于我们正在将这个示例分析的重点放在温哥华市,我们有机会利用第二个推特数据源。具体来说,我们可以使用温哥华公共交通管理局 Translink 的服务公告。

如上所述,该数据已经结构良好,有利于文本挖掘和后续分析;通过使用我们在前两章中回顾的技术处理这些数据,我们可以清理文本,然后将其编码为有用的特征。

我们将应用推特应用编程接口在很长一段时间内收集 Translink 的推文。推特应用编程接口是一个非常友好的工具包,很容易从 Python 中使用。(有关如何使用推特应用编程接口的扩展指导,请参见本章末尾的进一步阅读部分!)在这种情况下,我们希望从推文中提取日期和正文。正文几乎包含了我们需要知道的一切,包括以下内容:

  • 推文的性质(延迟或不延迟)
  • 车站受到影响
  • 关于延迟性质的一些信息

增加一点复杂性的一个因素是,同一个 Translink 账户在推特上发布了天空列车线路和公交线路的服务中断信息。幸运的是,在描述每种服务类型和主题的服务问题时,该帐户通常非常统一。特别是,推特账户使用特定的标签 (#RiderAlert 用于公交路线信息, #SkyTrain 用于列车相关信息, #TransitAlert 用于两种服务的一般警报,如法定假日)来区分服务中断的主题。

类似地,我们可以期望延迟总是用延迟这个词来描述,迂回这个词来描述迂回,而分流这个词来描述。这意味着我们可以使用特定的关键词过滤掉不想要的推文。干得好,特兰林克!

本章中使用的数据在本章随附的 GitHub 解决方案中的translink_tweet_data.json文件中提供。章节代码中还提供了刮擦脚本;为了利用它,您需要在 Twitter 上设置一个开发人员帐户。这很容易实现;此处记录了流程,您可以在此处注册。

一旦我们获得了推文数据,我们就知道下一步该做什么了——我们需要清理并规范正文!根据第六章文本特征工程,我们对输入数据运行BeautifulSoupNLTK:

from bs4 import BeautifulSoup

tweets = BeautifulSoup(train["TranslinkTweets.text"])  

tweettext = tweets.get_text()

brown_a = nltk.corpus.brown.tagged_sents(categories= 'a')

tagger = None
for n in range(1,4):
   tagger = NgramTagger(n, brown_a, backoff = tagger)

taggedtweettext = tagger.tag(tweettext)

我们可能不需要像上一章中的巨魔数据集那样进行密集的清理。Translink 的推文高度公式化,不包含非 ascii 字符或表情符号,所以我们在第六章文本特征工程中需要用到的具体“深度清洗”regex 脚本,这里就不需要了。

这为我们提供了一个包含小写、正则化和字典检查术语的数据集。我们已经准备好开始认真思考我们应该从这些数据中构建什么特性。

我们知道,检测数据中服务中断问题的基本方法是在推文中使用延迟术语。延迟以下列方式发生:

  • 在给定的位置
  • 在给定的时间
  • 出于某种原因
  • 在给定的持续时间内

在前三个因素中的每一个都在 Translink 推文中被持续跟踪,但是有一些数据质量问题值得认识。

位置根据第 22 街的受影响街道或车站给出。对于我们的目的来说,这不是一个完美的描述,因为我们不太可能在不做大量额外工作的情况下将街道名称和路线起点/终点变成一般的受影响区域(因为不存在允许我们基于该信息绘制边界框的方便参考)。

推文日期时间给出的时间并不完美。虽然我们不清楚推文是否在服务中断后的一致时间内发出,但 Translink 很可能有服务通知的目标。目前,在推文时间可能足够准确的假设下进行是明智的。

例外情况是可能是长期运行的问题或改变严重程度的问题(预计很小但变得重要的延迟)。在这些情况下,推文可能会被推迟,直到 Translink 团队认识到这个问题已经变得值得推文。数据质量问题的另一个可能原因是 Translink 内部通信不一致;工程或平台团队可能不会总是以相同的速度通知客户服务通知团队。

不过,我们必须有一定的信心,因为如果没有实时、准确的跨链路服务延迟数据集,我们无法测量这些延迟影响。(如果我们有,我们会用它来代替!)

Translink 始终如一地描述了天空列车服务延迟的原因,这些原因可分为以下几类:

  • 铁路
  • 火车
  • 转换
  • 控制
  • 未知的
  • 闯入
  • 医学的
  • 警察
  • 力量

在推文正文中使用前面列表中给出的特定术语描述每个类别。显然,其中一些类别(警察、电力、医疗)不太可能相关,因为它们不会告诉我们任何关于道路状况的有用信息。列车、轨道和道岔故障率可能与绕行可能性相关;这表明,出于分类目的,我们可能希望保留这些案例。

与此同时,公交路线服务延误包含一组类似的代码,其中许多与我们的目的非常相关。这些代码如下:

  • 机动车事故 ( MVA )
  • 建筑
  • 运水人
  • 交通

编码这些事件类型很可能会被证明有用!特别是,某些服务延迟类型可能比其他类型更有影响,从而增加了服务延迟更长的风险。我们希望对服务延迟类型进行编码,并在后续建模中将它们用作参数。

为此,让我们应用一种热编码的变体,它执行以下操作:

  • 它为每种服务风险类型创建一个条件变量,并将所有值设置为零
  • 它检查每项服务风险类型条款的推文内容
  • 它将包含特定风险术语的每条推文的相关条件变量设置为 1

这有效地执行了一次性编码,而没有采取麻烦的中间步骤,即创建我们通常要处理的阶乘变量:

from sklearn import preprocessing

enc = preprocessing.OneHotEncoder(categorical_features='all', dtype= 'float', handle_unknown='error', n_values='auto', sparse=True)

tweets.delayencode = enc.transform(tweets.delaytype).toarray()

除了我们可以在每个事件的基础上使用的功能之外,我们还可以查看服务中断风险和中断频率之间的关系。如果我们在一周内看到两次中断,第三次中断的可能性更大还是更小?

虽然这些问题很有趣,而且可能很有成果,但通常更谨慎的做法是在第一遍就建立一个有限的特征集和简单的模型,而不是过度设计一个庞大的特征集。因此,我们将运行初始发生率特性,并查看最终结果。

消费者评论

2010 年的一个主要文化发展是广泛使用公共在线域名进行自我表达。如果我们知道如何利用这一点,这其中最令人高兴的产品之一就是可以获得大量关于任意数量主题的自我报告信息。

通勤中断是激发个人反应的频繁发生的事件,这意味着它们往往会在社交媒体上被广泛报道。如果我们为关键词搜索编写一个合适的字典,我们就可以开始使用推特,尤其是作为一个关于城市交通和运输问题的有时间戳的信息来源。

为了收集这些数据,我们将使用基于字典的搜索方法。我们对所讨论时期的大多数推文不感兴趣(由于我们使用的是 RESTful API,因此需要考虑回报限制)。相反,我们感兴趣的是识别包含与拥塞或延迟相关的关键术语的推文数据。

不幸的是,从大量用户那里获得的推文往往不符合有助于分析的一致风格。我们将不得不应用我们在前一章中开发的一些技术,将这些数据分解成更容易分析的格式。

除了使用基于字典的搜索,我们还可以做一些工作来缩小搜索范围。实现这一点最权威的方法是使用坐标边界框作为推特应用编程接口的参数,这样任何相关的查询都只返回从这个区域收集的结果。

一如既往,在我们第一次通过时,我们会保持简单。在这种情况下,我们将统计当前时段的流量中断推文数量。在随后的迭代中,我们可以利用这些数据做一些额外的工作。正如 Translink 数据包含明确定义的延迟原因类别一样,我们可以尝试使用专门的字典来基于关键术语(例如,与构造相关的术语和同义词的字典)隔离延迟类型。

我们还可以考虑定义一个比简单的近期统计更细致入微的颠覆性推文率量化。例如,我们可以考虑创建一个加权计数功能,通过非线性加权来增加多个并发推文的影响(可能表示严重中断)。

必应流量 API

我们要进入的下一个应用编程接口是必应流量应用编程接口。这个 API 的优点是很容易访问;它是免费提供的(而一些竞争对手的 API 坐在付费墙后面),返回数据,并提供良好的细节水平。除其他外,该应用编程接口还返回事故位置代码、事故的一般描述以及拥堵信息、事故类型代码和开始/结束时间戳。

有益的是,此 API 提供的事件类型代码描述了一组广泛的事件类型,如下所示:

  1. Accident
  2. Congestion
  3. DisabledVehicle
  4. MassTransit
  5. Miscellaneous
  6. OtherNews
  7. PlannedEvent
  8. RoadHazard
  9. Construction
  10. Alert
  11. Weather

此外,还提供了严重性代码,其严重性值翻译如下:

  1. LowImpact
  2. Minor
  3. Moderate
  4. Serious

然而,一个缺点是,这个应用编程接口不能接收区域之间一致的信息。例如,在法国查询会返回多个其他事件类型的代码,(我观察了法国北部一个城镇一个月的时间,得到 1、3、5、8。)但似乎没有显示所有代码。在其他地方,可用的数据甚至更少。可悲的是,温哥华倾向于只显示代码 9 或 5 的数据,但即使是杂项编码的事件似乎也与建筑有关:

Closed between Victoria Dr and Commercial Dr - Closed. Construction work. 5

这是一个有些麻烦的限制。不幸的是,这不是我们可以轻易解决的事情;必应的应用编程接口并没有提供我们想要的所有数据!除非我们为更完整的数据集付费(或者在您所在的地区有更全面的数据采集应用编程接口!),我们将到需要继续使用我们所拥有的。

查询该应用编程接口的示例如下:

importurllib.request, urllib.error, urllib.parse
import json

latN = str(49.310911)
latS = str(49.201444)
lonW = str(-123.225544)
lonE = str(-122.903931)

url = 'http://dev.virtualearth.net/REST/v1/Traffic/Incidents/'+latS+','+lonW+','+latN+','+lonE+'?key='GETYOUROWNKEYPLEASE'

response = urllib.request.urlopen(url).read()
data = json.loads(response.decode('utf8'))
resources = data['resourceSets'][0]['resources']

print('----------------------------------------------------')
print('PRETTIFIED RESULTS')
print('----------------------------------------------------')
for resourceItem in resources:
    description = resourceItem['description']
typeof = resourceItem['type']
    start = resourceItem['start']
    end = resourceItem['end']
print('description:', description);
print('type:', typeof);
print('starttime:', start);
print('endtime:', end);
print('----------------------------------------------------')

This example yields the following data;

----------------------------------------------------
PRETTIFIED RESULTS
----------------------------------------------------
description: Closed between Boundary Rd and PierviewCres - Closed due to roadwork.
type: 9
severity 4
starttime: /Date(1458331200000)/
endtime: /Date(1466283600000)/
----------------------------------------------------
description: Closed between Commercial Dr and Victoria Dr - Closed due to roadwork.
type: 9
severity 4
starttime: /Date(1458327600000)/
endtime: /Date(1483218000000)/
----------------------------------------------------
description: Closed between Victoria Dr and Commercial Dr - Closed. Construction work.
type: 5
severity 4
starttime: /Date(1461780543000)/
endtime: /Date(1481875140000)/
----------------------------------------------------
description: At Thurlow St - Roadwork.
type: 9
severity 3
starttime: /Date(1461780537000)/
endtime: /Date(1504112400000)/
----------------------------------------------------

即使在认识到不同地理区域代码可用性不均衡的缺点后,来自这个 API 的数据应该会给我们提供一些价值。对交通中断事件有一个局部的了解仍然能给我们一个合理时期的数据。在我们自己定义的区域内定位交通事故并返回与当前日期相关的数据的能力可能有助于我们模型的性能。

使用特征工程技术推导和选择变量

在我们第一次通过输入数据时,我们反复选择保持初始特征集小。虽然我们在数据中看到了许多机会,但我们优先考虑的是查看初步结果,而不是跟进这些机会。

然而,很可能我们的第一个数据集不会帮助我们非常有效地解决问题或达到我们的目标。在这种情况下,我们需要迭代我们的特征集,通过创建新的特征和筛选我们的特征集来减少特征创建过程的有价值的输出。

一个有用的例子涉及到一个热点编码和 RFE。在本章中,我们将使用 one-hot 将天气数据和推文词典转换为 mn 大小的张量。产生了 m 个新的数据列后,我们希望减少我们的模型被这些新特性误导的可能性(例如,在多个特性强化相同信号的情况下,或者误导性但常用的术语没有被我们在第 6 章文本特性工程*中描述的数据清理过程清除的情况下)。RFE 可以非常有效地做到这一点,这是我们在本章前面讨论的特征选择技术。

总的来说,使用扩展-收缩过程应用上两章中的技术的方法工作会很有帮助。首先,使用能够生成潜在有价值的新特性的技术,例如转换和编码,来扩展特性集。然后,使用能够识别这些特性中性能最好的子集的技术来移除性能不佳的特性。在整个过程中,测试不同的目标特征计数,以确定在不同特征数量下的最佳可用特征集。

一些数据科学家解释了这是如何不同于其他人。一些人将使用我们已经讨论过的特征创建技术的重复迭代来构建他们所有的特征,然后减少那个特征集——其动机是这个工作流最小化了丢失数据的风险。其他人将迭代执行整个过程。你选择怎么做完全取决于你自己!

在我们最初传递输入数据时,我们有一个如下所示的特征集:

{
  'DisruptionInformation': {
    'Date': '15-05-2015',
    'TranslinkTwitter': [{
      'Service': '0',
      'DisruptionIncidentCount': '4'
  }, {
      'Service': '1',
      'DisruptionIncidentCount': '0'
    }]
  },
  'BingTrafficAPI': {
    'NewIncidentCount': '1',
    'SevereIncidentCount': '1',
    'IncidentCount': '3'
  },
  'ConsumerTwitter': {
    'DisruptionTweetCount': '4'
  }
}

这个数据集不太可能表现良好。尽管如此,让我们通过一个基本的初始算法来运行它,并大致了解我们离目标有多近;这样,我们可以用最少的开销快速学习!

为了方便起见,让我们从使用非常简单的回归算法运行第一遍开始。技术越简单,我们运行它的速度就越快(通常,它对我们来说就越清楚出了什么问题以及原因)。出于这个原因(因为我们处理的是具有连续输出的回归问题,而不是分类问题),第一遍我们将使用一个简单的线性回归模型:

from sklearn import linear_model

tweets_X_train = tweets_X[:-20]
tweets_X_test = tweets_X[-20:]

tweets_y_train = tweets.target[:-20]
tweets_y_test = tweets.target[-20:]

regr = linear_model.LinearRegression()

regr.fit(tweets_X_train, tweets_y_train)

print('Coefficients: \n', regr.coef_)
print("Residual sum of squares: %.2f" % np.mean((regr.predict(tweets_X_test) - tweets_y_test) ** 2))

print('Variance score: %.2f' % regr.score(tweets_X_test, tweets_y_test))

plt.scatter(tweets_X_test, tweets_y_test,  color='black')
plt.plot(tweets_X_test, regr.predict(tweets_X_test), color='blue',linewidth=3)

plt.xticks(())
plt.yticks(())
plt.show()

在这一点上,我们的 AUC 相当烂;我们看到的是 AUC 为 0.495 的车型。我们实际上比我们的目标做得更糟!让我们打印出一个混淆矩阵,看看这个模型做错了什么:

Deriving and selecting variables using feature engineering techniques

根据这个矩阵,它做什么都不太好。事实上,它声称几乎所有的记录都没有显示任何事件,以至于错过了 90%的真正中断!

考虑到我们的模型和特性处于早期阶段,以及一些输入数据的不确定效用,这实际上一点也不坏。与此同时,我们应该预计发生率为 6%(因为我们的培训数据表明,事件大约每 16 次通勤发生一次)。我们仍然会做得更好一点,因为我们猜测每天的通勤都会中断(如果我们忽略了每天早退对我们生活方式的影响)。

让我们考虑下一轮我们能做什么改变。

  1. 首先,我们可以进一步改进我们的输入数据。我们确定了许多新特性,可以使用一系列转换技术从现有资源中创建这些特性。
  2. 其次,我们可以考虑使用附加信息来扩展数据集。特别是,描述温度和湿度的天气数据集可以帮助我们改进模型。
  3. 最后,我们可以升级我们的算法,让它更咕噜咕噜,随机森林或 SVM 就是明显的例子。有充分的理由暂时不这样做。主要原因是我们可以从线性回归中继续学到很多东西;我们可以与早期的结果进行比较,以了解我们的更改增加了多少价值,同时保留快速迭代循环和简单的评分方法。一旦我们开始在功能准备上获得最低回报,我们就应该考虑升级我们的模型。

目前,我们将继续升级数据集。我们有很多选择。我们可以将位置编码到来自必应应用编程接口“描述”字段的交通事件数据和 Translink 的推文中。就 Translink 而言,对于公交线路而言,这可能比天车线路更有用(鉴于我们将分析范围限制为仅关注交通通勤)。

我们可以通过两种方式中的一种来实现这个目标;

  • 使用街道名称/位置的语料库,我们可以解析输入数据并构建一个热门矩阵
  • 我们可以简单地对整个推文和整个 API 数据集进行一次性编码

有趣的是,如果我们打算在执行一次热编码后使用降维技术,我们可以对两条文本信息的整个主体进行编码,而没有任何重大问题。如果与推文和文本中使用的其他单词相关的功能不相关,它们将在 RFE 会议期间被删除。

这是一种稍微放任的方法,但有一个微妙的优势。也就是说,如果任何一个数据源中有一些其他潜在有用的内容,而这些内容是我们迄今为止忽略的潜在特性,那么这个过程将产生基于这些信息创建特性的额外好处。

让我们以编码延迟类型的相同方式编码位置:

from sklearn import preprocessing

enc = preprocessing.OneHotEncoder(categorical_features='all', dtype= 'float', handle_unknown='error', n_values='auto', sparse=True)

tweets.delayencode = enc.transform(tweets.location).toarray()

此外,我们应该跟进我们的意图,从 Translink 和 Bing 地图事件日志中创建最近的计数变量。本章随附的 GitHub 存储库中提供了这种聚合的代码!

用这个更新的数据重新运行我们的模型产生的结果略有改善;预测方差得分上升到 0.56。虽然不引人注目,但这绝对是朝着正确方向迈出的一步。

接下来,让我们继续我们的第二个选项——添加一个提供天气数据的新数据源。

天气空气污染指数

我们之前已经获取了数据,这些数据将帮助我们判断通勤中断是否正在发生——识别现有延误的反应性数据源。我们现在要做一些改变,试图找到与延误和拥堵原因相关的数据。道路工程和施工信息肯定属于这一类(还有其他一些必应交通应用编程接口代码)。

一个因素是(坊间流传!)与通勤时间增加相关的是坏天气。有时候这很明显;严寒或大风对通勤时间有明显影响。然而,在许多其他情况下,不清楚对于给定的通勤,气候因素和中断可能性之间的关系的强度和性质是什么。

通过从具有足够粒度和地理覆盖范围的来源提取相关天气数据,我们有望使用强天气信号来帮助改进我们对中断的正确预测。

出于我们的目的,我们将使用雅虎天气应用编程接口,它提供一系列温度、大气、压力相关和其他气候数据,包括当前和预测的。我们可以查询雅虎天气应用编程接口,而不需要密钥或登录过程,如下所示:

import urllib2, urllib, json

baseurl = https://query.yahooapis.com/v1/public/yql?

yql_query = "select item.condition from weather.forecast where woeid=9807"
yql_url = baseurl + urllib.urlencode({'q':yql_query}) + "&format=json"
result = urllib2.urlopen(yql_url).read()
data = json.loads(result)
print data['query']['results']

为了理解应用编程接口能提供什么,用*替换item.condition(在本质上是一个嵌入式的 SQL 查询中)。该查询会输出大量信息,但深入挖掘会发现有价值的信息,包括当前条件:

{
   'channel': {
     'item': {
      'condition': {
         'date': 'Thu, 14 May 2015 03:00 AM PDT', 'text': 'Cloudy', 'code': '26', 'temp': '46'
      }
    }
  }
}

包含以下信息的 7 天预测:

{
    'item': {
      'forecast': {
        'code': '39', 'text': 'Scattered Showers', 'high': '60', 'low': '44', 'date': '16 May 2015', 'day': 'Sat'
      }
    }
}

和其他当前天气信息:

'astronomy': {
     'sunset': '8:30 pm', 'sunrise': '5:36 am'

   'wind': {
     'direction':  '270', 'speed': '4', 'chill': '46'

为了建立一个训练数据集,我们每天通过一个从 2015 年 5 月到 2016 年 1 月运行的自动化脚本提取数据。这些预测可能对我们没有太大的用处,因为我们的模型可能会每天重新运行当前的数据,而不是依赖于预测。但是,我们肯定会使用wind.directionwind.speedwind.chill变量,以及condition.temperaturecondition.text变量。

关于如何进一步处理这些信息,有一个选项跃入脑海。天气标签的一次性编码将使我们能够使用天气条件信息作为分类变量,就像我们在前面一章中所做的那样。这似乎是一个必要的步骤。这极大地扩充了我们的功能集,为我们留下了以下数据:

{
  'DisruptionInformation': {
    'Date': '15-05-2015',
    'TranslinkTwitter': [{
      'Service': '0',
      'DisruptionIncidentCount': '4'
    }, {
      'Service': '1',
      'DisruptionIncidentCount': '0'
    }]
  },
  'BingTrafficAPI': {
    'NewIncidentCount': '1',
    'SevereIncidentCount': '1',
    'IncidentCount': '3'
  },
  'ConsumerTwitter': {
    'DisruptionTweetCount': '4'
  },
  'YahooWeather':{
    'temp: '45'
    'tornado': '0',
    'tropical storm': '0',
    'hurricane': '0',
    'severe thunderstorms': '0',
    'thunderstorms': '0',
    'mixed rain and snow': '0',
    'mixed rain and sleet': '0',
    'mixed snow and sleet': '0',
'freezing drizzle': '0',
'drizzle': '0',
'freezing rain': '0',
'showers': '0',
'snow flurries': '0',
'light snow showers': '0',
'blowing snow': '0',
'snow': '0',
'hail': '0',
'sleet': '0',
'dust': '0',
'foggy': '0',
'haze': '0',
'smoky': '0',
'blustery': '0',
'windy': '0',
'cold': '0',
'cloudy': '1',
'mostly cloudy (night)': '0',
'mostly cloudy (day)': '0',
'partly cloudy (night)': '0',
'partly cloudy (day)': '0',
'clear (night)': '0',
'sunny': '0',
'fair (night)': '0',
'fair (day)': '0',
'mixed rain and hail': '0',
'hot': '0',
'isolated thunderstorms': '0',
'scattered thunderstorms': '0',
'scattered showers': '0',
'heavy snow': '0',
'scattered snow showers': '0',
'partly cloudy': '0',
'thundershowers': '0',
'snow showers': '0',
'isolated thundershowers': '0',
'not available': '0',
}

很有可能很多时间会被有价值地投入到进一步丰富雅虎天气应用编程接口提供的天气数据中。对于第一遍,一如既往,我们将继续专注于构建一个采用我们之前描述的特性的模型。

我们将如何利用这些数据做进一步的工作,这绝对值得考虑。在这种情况下,区分跨列数据转换和跨行转换非常重要。

跨列转换是指来自同一输入案例中不同特征的变量基于彼此进行转换。例如,我们可以获取案例的开始日期和结束日期,并使用它来计算持续时间。有趣的是,我们在本书中学习的大多数技术不会从许多这样的转换中获得很多。大多数能够绘制非线性决策边界的机器学习技术倾向于在数据集建模中对变量之间的关系进行编码。深度学习技术通常会使这种能力更进一步。这是一些特征工程技术(尤其是基本转换)对深度学习应用程序增加较少价值的部分原因。

同时,跨行转换通常是一种聚合。例如,最后 n 多持续时间值的中心趋势是一个可以通过对多行的操作得到的特征。自然,一些特性可以通过列式和行式操作的组合来导出。跨行转换的有趣之处在于,模型通常不太可能训练识别它们,这意味着它们倾向于在非常特殊的环境中继续增加价值。

当然,这些信息相关的原因是最近的天气是一个背景,在这个背景下,来自跨行操作的特征可能会给我们的模型增加新的信息。例如,过去 n 小时内大气压力或温度的变化可能是比当前压力或温度更有用的变量。(特别是,当我们的模型旨在预测当天晚些时候的通勤时!)

下一步是重新运行我们的模型。这一次,我们的 AUC 高了一点;我们得分 0.534 。查看我们的困惑矩阵,我们也看到了改进:

The weather API

如果问题与天气因素相关联,继续提取天气数据是个好主意;将该解决方案设置为在一段较长的时间内运行,将逐渐从每个来源收集纵向输入,逐渐为我们提供更可靠的预测。

在这一点上,我们离我们的 MVP 目标只有很短的距离。我们可以继续扩展我们的输入数据集,但明智的解决方案是找到另一种方法来解决问题。我们可以有意义地采取两种行动。

作为人类,数据科学家倾向于从简化假设的角度来思考。其中一个经常出现的例子是帕累托原则在成本/收益分析决策中的应用。从根本上说,帕累托原则指出,对于许多事件,大约 80%的价值或效果来自大约 20%的投入努力或原因,遵循所谓的帕累托分布。这个概念在软件工程环境中非常流行,因为它可以指导效率的提高。

The weather API

为了将这一理论应用于当前的情况,我们知道我们可以花更多的时间来完善我们的特征工程。有些技术我们还没有应用,有些功能我们可以创建。然而,与此同时,我们知道有整个领域我们还没有触及:尤其是外部数据搜索和模型更改,我们可以快速尝试。在深入研究额外的数据集准备之前,在我们的下一轮中探索这些便宜但可能有影响的选项是有意义的。

在我们的探索性分析中,我们注意到我们的一些变量相当稀疏。目前还不清楚它们的帮助有多大(尤其是对于特定类型事故发生较少的车站)。

让我们使用本章前面使用的一些技术来测试我们的变量集。具体来说,让我们将Lasso应用于将我们的特征集简化为性能子集的问题:

fromsklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X = scaler.fit_transform(DisruptionInformation["data"])
Y = DisruptionInformation["target"]
names = DisruptionInformation["feature_names"]

lasso = Lasso(alpha=.3)
lasso.fit(X, Y)

print "Lasso model: ", pretty_print_linear(lasso.coef_, names, sort = True)

这种输出立即有价值。很明显,许多天气特征(要么没有足够频繁地出现,要么在出现时没有告诉我们任何有用的信息)对我们的模型没有任何帮助,应该删除。此外,我们没有从我们的流量总量中获得很多价值。虽然这些可以暂时保留下来(希望收集更多的数据将提高它们的有用性),但是对于我们的下一步,我们将重新运行我们的模型,没有我们使用 LASSO 所揭示的得分很低的特征。

有一个相当便宜的额外变化,我们应该做:我们应该升级我们的模型,可以非线性拟合,从而可以拟合任何函数。这是值得做的,因为正如我们所观察到的,我们的一些特征显示了一系列表明非线性潜在趋势的偏斜分布。让我们对这个数据集应用一个随机森林:

fromsklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier
rf = RandomForestRegressor(n_jobs = 3, verbose = 3, n_estimators=20)
rf.fit(DisruptionInformation_train.targets,DisruptionInformation_train.data)

r2 = r2_score(DisruptionInformation.data, rf.predict(DisruptionInformation.targets))
mse = np.mean((DisruptionInformation.data - rf.predict(DisruptionInformation.targets))**2)

pl.scatter(DisruptionInformation.data, rf.predict(DisruptionInformation.targets))
pl.plot(np.arange(8, 15), np.arange(8, 15), label="r^2=" + str(r2), c="r")
pl.legend(loc="lower right")
pl.title("RandomForest Regression with scikit-learn")
pl.show()

让我们再次回到我们的困惑矩阵:

The weather API

在这一点上,我们做得相当好。我们模型的简单升级带来了显著的改进,我们的模型正确识别了几乎 40%的通勤延迟事件(足以开始对我们有用!),同时对少量案例进行错误分类。

令人沮丧的是,这种模式仍然会让我们不正确地比正确地早起更多次。当然,黄金标准是,如果它预测更多的通勤延误,而不是导致错误(提前)开工!如果我们持续收集特征数据,我们有理由希望实现这一目标;这种模式的主要弱点是,考虑到通勤中断事件的罕见性,可供样本的案例非常少。

然而,我们已经成功地从不同的来源收集和整理了一系列数据,以便从免费获得的数据中创建一个模型,产生一个可识别的、真实世界的好处(将上班迟到的人数减少 40%)。这绝对是值得高兴的成就!

进一步阅读

我建议对特征选择进行介绍,这是安藤萨巴斯对广泛的特征选择技术的四部分探索。它充满了 Python 代码片段和明智的评论。从开始。

有关第 6 章和第 7 章中涵盖各种材料的功能选择和工程的讨论,请参考 Alexandre Bourhard-科特迪瓦在上的幻灯片 http://people . eecs . Berkeley . edu/~ Jordan/courses/294-fall 09/讲座/功能/幻灯片. pdf 。也可以考虑一下杰夫·豪伯特在http://courses.washington.edu/css490/2012.的幻灯片 winter/讲座 _ slides/05a _ feature _ creation _ selection . pdf

缺乏对特征创建的全面讨论,大量可用资料讨论了降维技术或特定领域所需的非常具体的特征创建。要更全面地了解可能的转换范围,一种方法是阅读代码文档。在您现有知识的基础上,一个不错的地方是 Spark ML 的特征转换算法文档,位于https://Spark . Apache . org/docs/1 . 5 . 1/ML-features . html # feature-transformers,它描述了数字和文本特征的一系列可能的转换。但是请记住,特性创建通常是特定于问题、特定于领域的,并且是一个高度创造性的过程。一旦你学会了一系列技术选项,诀窍就在于弄清楚如何将这些技术应用到手头的问题上!

对于对超参数优化感兴趣的读者,我推荐大家阅读 Alice Zheng 在 Turi 博客上的帖子,作为一个很好的起点:http://blog . Turi . com/how-evaluation-machine-learning-models-part-4-超参数-tuning

我还发现 scikit-learn 文档是网格搜索的有用参考,特别是:http://scikit-learn.org/stable/modules/grid_search.html

总结

在本章中,您学习并应用了一套技术,使我们能够从非常少的初始数据开始,有效地构建和精细化机器学习数据集。这些强大的技术使数据科学家能够将看似浅薄的数据集转化为机遇。我们使用一组客户服务推文来展示这种能力,以创建一个旅行中断预测器。

但是,为了将该解决方案投入生产,我们需要添加一些功能。在倒数第二步移除一些位置是一个有问题的决定;如果此解决方案旨在识别旅程中断风险,那么移除位置似乎是不可能的!鉴于我们没有全年的数据,因此无法确定季节性或纵向趋势的影响(如延长的维护工程或计划中的车站关闭),这一点尤其正确。我们在删除这些元素时有点仓促,更好的解决方案是将它们保留更长时间。

基于这些担忧,我们应该认识到有必要开始为我们的解决方案注入一些活力。当春天来临,我们的数据集开始包含新的气候条件时,我们的模型完全有可能无法有效地适应。在下一章中,我们将着眼于构建更复杂的模型集成,并讨论在模型解决方案中构建健壮性的方法。

八、集成方法

随着本书前面章节的深入,您学习了如何应用一些新技术。我们开发了几种先进的机器学习算法,并获得了广泛的配套技术,通过更有效的特征选择和准备来提高您对学习技术的使用。本章试图使用集成方法来增强您现有的技术集:将多个不同的模型绑定在一起以解决现实问题的技术。

集成技术已经成为数据科学家工具集的一个基本部分。在竞争的机器学习环境中,集成的使用已经成为一种常见的做法,集成现在被认为是许多环境中不可或缺的工具。我们将在本章中开发的技术为我们的模型提供了性能优势,同时增强了它们对底层数据变化的鲁棒性。

我们将研究一系列集合选项,讨论这些技术的代码和应用。我们将通过指导和参考现实世界的应用程序,包括由成功的卡格尔斯创建的模型来丰富这个解释。

我们在本标题中回顾的任何模型的开发都允许我们解决广泛的数据问题,但是将我们的模型应用于生产环境会带来一系列额外的问题。我们的解决方案仍然容易受到潜在观察结果变化的影响。无论是在不同的个体群体中、在时间变化中(例如,被捕捉的现象的季节性变化)还是通过潜在条件的其他变化来表达,最终结果往往是相同的——在他们被训练的条件下运行良好的模型通常不能推广并继续表现良好久而久之。

本章的最后一节描述了将本书中的技术转移到操作环境的方法,以及如果您的预期应用程序必须能够适应变化,您应该考虑的附加监控和支持的种类。

引入合奏

|   | “这就是你赢得 ML 比赛的方式:你拿着别人的作品,一起合奏。” |   |
|   | - 维塔利·库兹涅佐夫 nip 2014 |

在机器学习的上下文中,集成是一组用于解决共享问题的模型。集成由两个部分组成:一组模型和一组决定规则,这些决定规则决定了如何将这些模型的结果组合成单个输出。

集成为数据科学家提供了为给定问题构建多个解决方案的能力,然后将这些解决方案组合成单个最终结果,该结果从每个输入解决方案的最佳元素中提取。这提供了对噪声的鲁棒性,这反映在针对初始数据集的更有效的训练(导致更低水平的过拟合和训练误差的减少)以及针对前面部分讨论的那种数据变化。

毫不夸张地说,集成是机器学习中最重要的最新发展。

此外,集成使人们能够更灵活地解决给定的问题,因为它们使数据科学家能够测试解决方案的不同部分,并解决特定于输入数据子集或正在使用的模型部分的问题,而无需完全重新调整整个模型。正如我们将看到的,这可以让生活变得更容易!

根据所使用的决策规则的性质,系综通常被认为属于几个类别之一。主要的合奏类型如下:

  • 平均方法:他们并行开发模型,然后使用平均或投票技术来开发组合估计器
  • 堆叠(或混合)方法:它们使用多个分类器的加权输出作为下一层模型的输入
  • 增强方法:它们涉及按顺序构建模型,其中每个添加的模型旨在提高组合估计器的得分

考虑到这两类集成方法的重要性和实用性,我们将依次讨论每一个:讨论理论、算法选项和真实世界的例子。

理解平均系综

平均系综在物理科学和统计建模领域有着悠久而丰富的历史,在包括分子动力学和音频信号处理在内的许多领域都有着广泛的应用。这种集合通常被视为给定系统的几乎完全相同的复制情况。该系统中病例间的平均值和方差是整个系统的关键值。

在机器学习环境中,平均集成是在同一数据集上训练的模型的集合,其结果以一系列方式聚集。根据实现目标,平均集成可以带来几个好处。

平均系综可用于降低模型性能的可变性。一种常见的方法是创建多个模型配置,这些配置采用不同的参数子集作为输入。采用这种方法的技术统称为打包算法。

使用打包算法

不同的打包实现将有不同的操作,但是共享随机获取特征空间的子集的共同属性。打包方法有四种主要类型。粘贴绘制样本的随机子集,而不进行替换。当替换完成后,这种方法简单地称为装袋。粘贴在计算上通常比打包便宜,并且可以在更简单的应用程序中产生类似的结果。

当以特征方式采集样本时,该方法被称为 随机子空间。随机子空间方法提供了稍微不同的能力;它们基本上减少了对广泛的、高度优化的特征选择的需求。在这种活动通常导致具有优化输入的单个模型的情况下,随机子空间允许并行使用多个配置,并使任何一个解决方案的方差变平。

虽然使用合奏来减少模型性能的可变性听起来像是一个性能打击(自然的反应可能是,但是为什么不在合奏中选择一个表现最好的模型呢?),这种方法有很大的优势。

首先,如前所述,平均提高了模型集适应不熟悉的噪声的能力(也就是说,它减少了过拟合)。其次,可以使用集成来针对输入数据集的不同元素进行有效建模。这是竞争机器学习环境中的一种常见方法,其中数据科学家将基于分类结果和特定类型的故障案例迭代调整集成。在某些情况下,这是一个详尽的过程,包括检查模型结果(通常作为正常的迭代模型开发过程的一部分),但是许多数据科学家更喜欢他们将首先实现的技术或解决方案。

随机子空间可以是一种非常强大的方法,尤其是如果有可能使用多个子空间大小并彻底检查特征组合的话。随机子空间方法的成本随着数据集的大小非线性地增加,超过某个点,测试多个子空间大小的每个参数配置将变得昂贵。

最后,可以用一种称为 随机面片的方法,从样本和特征抽取的子集创建一个集合的估计器。在相似的情况下,随机补丁的性能通常与随机子空间技术的性能大致相同,内存消耗显著降低。

由于我们已经讨论了打包套装背后的理论,让我们看看如何实现一个。以下代码描述了使用 sklearn 的BaggingClassifier类实现的随机补丁分类器:

from sklearn.cross_validation import cross_val_score
from sklearn.ensemble import BaggingClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import load_digits
from sklearn.preprocessing import scale

digits = load_digits()
data = scale(digits.data)
X = data
y = digits.target

bagging = BaggingClassifier(KNeighborsClassifier(), max_samples=0.5, max_features=0.5)
scores = cross_val_score(bagging, X, y)
mean = scores.mean() 
print(scores)
print(mean)

与许多 sklearn 分类器一样,所需的核心代码非常简单;分类器被初始化并用于对数据集进行评分。交叉验证(通过cross_val_score)不会增加任何有意义的复杂性。

这个打包分类器使用了一个 K 近邻 ( KNN )分类器(KNeighboursClassifier)作为基础,特征和案例的采样率各设置为 50%。这相对于数字数据集输出了非常强的结果,在交叉验证后正确地对 93%的病例进行了平均分类:

[ 0.94019934  0.92320534  0.9295302 ]

0.930978293043

使用随机森林

另一组平均集合技术统称为随机森林。随机森林可能是竞争数据科学家使用的最成功的集成技术,它开发了并行的决策树分类器集。通过给分类器结构引入两个主要的随机性来源,森林最终包含了不同的树。用于构建每个树的数据通过替换从训练集中采样,而树创建过程不再使用来自所有特征的最佳分割,而是从特征的随机子集选择最佳分割。

使用sklearn中的RandomForestClassifier类可以很容易地调用随机森林。举个简单的例子,考虑以下内容:

import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_digits
from sklearn.preprocessing import scale

digits = load_digits()
data = scale(digits.data)

n_samples, n_features = data.shape
n_digits = len(np.unique(digits.target))
labels = digits.target

clf = RandomForestClassifier(n_estimators=10)
clf = clf.fit(data, labels)
scores = clf.score(data,labels)
print(scores)

这个合奏输出的分数 0.999,很难打。事实上,我们在前面几章中使用的任何单个模型都没有看到这种水平的性能。

随机森林的变体,称为 极随机树(extracrees),使用相同的随机特征子集方法来选择树中每个分支的最佳分割。然而,它也随机化了辨别阈值;决策树通常选择最有效的类间分割,而提取树以随机值分割。

由于决策树的训练相对有效,随机森林算法可以潜在地支持大量不同的树,分类器的有效性随着节点数量的增加而提高。引入的随机性为噪声或数据变化提供了一定程度的鲁棒性;然而,就像我们前面回顾的 bagging 算法一样,这种增益通常是以性能略微下降为代价的。在提取树的情况下,稳健性可能会进一步提高,而性能度量会提高(通常偏差值会降低)。

下面的代码描述了提取树在实践中是如何工作的。就像我们的随机子空间实现一样,代码非常简单。在这种情况下,我们将开发一组模型来比较树外树和随机森林方法的效果:

from sklearn.cross_validation import cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_digits
from sklearn.preprocessing import scale

digits = load_digits()
data = scale(digits.data)
X = data
y = digits.target

clf = DecisionTreeClassifier(max_depth=None, min_samples_split=1,
    random_state=0)
scores = cross_val_score(clf, X, y)                      
print(scores)

clf = RandomForestClassifier(n_estimators=10, max_depth=None,
    min_samples_split=1, random_state=0)
scores = cross_val_score(clf, X, y)       
print(scores)

clf = ExtraTreesClassifier(n_estimators=10, max_depth=None,
    min_samples_split=1, random_state=0)
scores = cross_val_score(clf, X, y)
print(scores)

分数分别如下:

[ 0.74252492  0.82136895  0.75671141]
[ 0.88372093  0.9015025   0.8909396 ]
[ 0.91694352  0.93489149  0.91778523]

假设我们在这里使用的是完全基于树的方法,分数就是正确标注的案例的比例。我们可以在这里看到,这两种森林方法之间没有太大的区别,它们都表现强劲,平均得分为 0.9 。在这个例子中,随机森林实际上比提取树略胜一筹(大约增加了 0.002 ,而这两种技术都大大优于基本决策树,基本决策树的平均得分为 0.77

使用随机森林时的一个缺点是(特别是随着森林规模的增加)很难检查或调整给定实现的有效性。虽然单独的树非常容易处理,但是一个开发的集合中的树的数量以及随机分裂所产生的混淆会使改进随机森林实现变得非常困难。一种选择是开始查看单个模型所画的决策边界。通过对比一个集合中的模型,可以更容易地识别出一个模型在划分类时比其他模型表现更好的地方。

例如,在这个例子中,我们可以很容易地看到我们的模型在高水平上的表现,而不需要挖掘具体的细节:

Using random forests

虽然超越简单的层次(使用高层次的图和汇总分数)理解随机森林实现的表现可能是具有挑战性的,但困难是值得的。随机森林的性能非常强,只需要最小的额外计算成本。在早期阶段,当一个人还在确定攻击角度时,他们往往是解决问题的好方法,因为他们快速产生强有力结果的能力可以提供一个有用的基准。一旦您知道了随机森林实现的性能,您就可以开始优化和扩展您的集成。

为此,我们应该继续探索不同的集合技术,以便进一步构建我们的集合选项工具包。

应用助推方法

系综创建的另一种方法是构建增强模型。这些模型的特点是它们按顺序使用多个模型来迭代地“提升”或提高集合的性能。

增强模型经常使用一系列弱学习者,与随机猜测相比,这些模型只能提供边际收益。在每次迭代中,一个新的弱学习者在一个调整过的数据集上被训练。在多次迭代中,集成在每次迭代中用一个新的树(优化集成性能分数的树)扩展。

也许最著名的提升方法是 AdaBoost ,它通过执行以下操作在每次迭代时调整数据集:

  • 选择一个决策树桩(一个浅的、通常是一级的决策树,实际上是所讨论数据集最重要的决策边界)
  • 增加决策树桩标注不正确的案例的权重,同时减少标注正确的案例的权重

这种迭代权重调整使得集成中的每个新分类器优先训练错误标记的案例;该模型通过瞄准高度加权的数据点进行调整。最终,树桩被组合成最终的分类器。

AdaBoost 可以在分类和回归上下文中使用,并获得令人印象深刻的结果。以下示例显示了在heart数据集上运行的 AdaBoost 实现:

import numpy as np

from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.datasets.mldata import fetch_mldata
from sklearn.cross_validation import cross_val_score

n_estimators = 400
# A learning rate of 1\. may not be optimal for both SAMME and SAMME.R
learning_rate = 1.

heart = fetch_mldata("heart")
X = heart.data
y = np.copy(heart.target)
y[y==-1]=0

X_test, y_test = X[189:], y[189:]
X_train, y_train = X[:189], y[:189]

dt_stump = DecisionTreeClassifier(max_depth=1, min_samples_leaf=1)
dt_stump.fit(X_train, y_train)
dt_stump_err = 1.0 - dt_stump.score(X_test, y_test)

dt = DecisionTreeClassifier(max_depth=9, min_samples_leaf=1)
dt.fit(X_train, y_train)
dt_err = 1.0 - dt.score(X_test, y_test)

ada_discrete = AdaBoostClassifier(
    base_estimator=dt_stump,
    learning_rate=learning_rate,
    n_estimators=n_estimators,
    algorithm="SAMME")
ada_discrete.fit(X_train, y_train)

scores = cross_val_score(ada_discrete, X_test, y_test)
print(scores)                  
means = scores.mean()
print(means)

在这种情况下,n_estimators参数指示使用的弱学习者的数量;在平均方法的情况下,添加估计器总是会降低模型的偏差,但会增加模型过度训练其训练数据的概率。base_estimator参数可以用来定义不同的弱学习者;默认值是决策树(因为训练一棵弱树很简单,可以使用树桩,非常浅的树)。当应用于heart数据集时,如本例所示,AdaBoost 在略高于 79%的情况下实现了正确标注,这对于第一遍来说是相当可靠的性能:

[ 0.77777778  0.81481481  0.77777778]

0.79012345679

增压模型比平均模型具有显著优势;它们使得创建识别问题案例或问题案例类型并解决它们的集合变得容易得多。增强模型通常会首先针对最容易预测的案例,每个添加的模型都适合剩余的错误预测案例的子集。

由此产生的一个风险是增强模型开始过度拟合(在最极端的情况下,你可以想象集成组件已经适合特定的情况!)的训练数据。管理集合组件的正确数量是一个棘手的问题,但谢天谢地我们可以借助一种熟悉的技术来解决它。在第 1 章无监督机器学习中,我们讨论了一种称为 肘关节法的视觉启发式方法。在的情况下,该图是 K (平均值的数量),而不是集群实现的性能度量。在这种情况下,我们可以使用类似的过程,使用估计量的数量( n )和总体的偏差或误差率(我们称之为 e)。对于一系列不同的增强估计器,我们可以将它们的输出绘制如下:

Applying boosting methods

通过确定曲线开始变平的点,我们可以降低我们的模型过度拟合的风险,随着曲线开始变平,这种风险变得越来越有可能。这是真的,原因很简单,随着曲线水平,这必然意味着来自每个新的估计器的附加增益是越来越少的情况的正确分类!

这种视觉辅助工具的部分吸引力在于,它使我们能够感受到我们的解决方案可能会过度拟合。我们可以(也应该!)尽可能地应用验证技术,但在某些情况下(例如,当目标是实现模型实现的特定 MVP 目标时,无论是通过用例还是 Kaggle 公共排行榜上的分数分布来通知),我们可能会倾向于推进性能实现。当我们添加每一个新的估计量时,准确理解我们所获得的收益是如何衰减的,这对于理解过度拟合的风险至关重要。

使用 XGBoost

2015 年年中,一种解决结构化机器学习问题的新算法——XGboost,在竞争激烈的数据科学领域掀起了一阵风暴。极限梯度增强 ( XGBoost )是一个编写良好的性能库,提供了一个通用的增强算法(梯度增强)。

XGBoost 的工作方式很像 AdaBoost,但有一个关键区别——改进模型的方式不同。

在每次迭代中,XGBoost 都试图通过减少该集合的残差(目标和标签预测之间的差异)来提高现有模型集的性能。每次迭代,所添加的模型都是根据它是否最能减少现有集合的残差来选择的。这类似于梯度下降(通过逆着损失梯度移动来迭代地最小化函数);因此,这个名字叫做梯度增强。

事实证明,Gradient Boosting 在最近的 Kaggle 竞赛中非常成功,它在 2015 年下半年支持了 CrowdFlower 竞赛和微软恶意软件分类挑战赛以及许多其他结构化数据竞赛的获胜者。

要应用 XGBoost,让我们获取 XGBoost 库。最好的方法是通过pip,命令行上有pip install xgboost命令。对于 Windows 用户,pip安装目前(2015 年末)在 Windows 上被禁用。为了您的利益,在本书的 GitHub 资源库的Chapter 8文件夹中提供了一份 XGBoost 的冷拷贝。

应用 XGBoost 相当简单。在这种情况下,我们将使用 UCI 皮肤病学数据集将该库应用于多类分类任务。该数据集包含一个年龄变量和大量分类变量。示例数据行如下所示:

3,2,0,2,0,0,0,0,0,0,0,0,1,2,0,2,1,1,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,10,2

少数年龄值(倒数第二特征)缺失,由?编码。使用该数据集的目的是根据以下类别分布正确分类六种不同皮肤状况中的一种:

 Database:  Dermatology

 Class code:   Class:                  Number of instances:
 1             psoriasis      112
 2             seboreic dermatitis             61
 3             lichen planus                   72
 4             pityriasis rosea                49
 5             cronic dermatitis               52 
 6             pityriasis rubra pilaris        20

我们将通过加载数据并通过 70/30 分割将其划分为测试和训练案例来开始对这个问题应用 XGBoost:

import numpy as np
import xgboost as xgb

data = np.loadtxt('./dermatology.data', delimiter=',',converters={33: lambda x:int(x == '?'), 34: lambda x:int(x)-1 } )
sz = data.shape

train = data[:int(sz[0] * 0.7), :]
test = data[int(sz[0] * 0.7):, :]

train_X = train[:,0:33]
train_Y = train[:, 34]

test_X = test[:,0:33]
test_Y = test[:, 34]

此时,我们初始化并参数化我们的模型。eta参数定义步长收缩。在梯度下降算法中,使用收缩参数来减小更新的大小是非常常见的。梯度下降算法有一种趋势(特别接近收敛)在最优值上来回曲折;使用收缩参数来缩小变化的大小可以使梯度下降的效果更加精确。常见的(也是默认的)缩放值是0.3。在这个例子中,eta已经被设置为0.1以获得更高的精度(以更多迭代的可能代价)。

max_depth参数直观;它定义了示例中任何树的最大深度。给定六个输出类,六是一个合理的开始值。num_round参数定义了算法将执行多少轮梯度增强。同样,对于有更多类的多类问题,通常需要更多轮次。同时,nthread参数定义了代码将运行多少个 CPU 线程。

这里使用的DMatrix结构纯粹是为了训练速度和记忆优化。使用 XGBoost 时使用这些通常是个好主意;它们可以从numpy.arrays开始建造。使用DMatrix启用watchlist功能,解锁一些高级功能。特别是,watchlist允许我们监控所提供列表中所有数据的评估结果:

xg_train = xgb.DMatrix( train_X, label=train_Y)
xg_test = xgb.DMatrix(test_X, label=test_Y)

param = {}

param['objective'] = 'multi:softmax'

param['eta'] = 0.1
param['max_depth'] = 6
param['nthread'] = 4
param['num_class'] = 6

watchlist = [ (xg_train,'train'), (xg_test, 'test') ]
num_round = 5
bst = xgb.train(param, xg_train, num_round, watchlist );

我们训练我们的模型bst,以生成初始预测。然后,我们重复训练过程,生成启用softmax的预测(通过multi:softprob):

pred = bst.predict( xg_test );

print ('predicting, classification error=%f' % (sum( int(pred[i]) != test_Y[i] for i in range(len(test_Y))) / float(len(test_Y)) ))

param['objective'] = 'multi:softprob'
bst = xgb.train(param, xg_train, num_round, watchlist );

yprob = bst.predict( xg_test ).reshape( test_Y.shape[0], 6 )
ylabel = np.argmax(yprob, axis=1)

print ('predicting, classification error=%f' % (sum( int(ylabel[i]) != test_Y[i] for i in range(len(test_Y))) / float(len(test_Y)) ))

使用堆叠集合

我们在本章前面看到的传统集成都有一个共同的设计理念:它们涉及多个经过训练的分类器来适应一组目标标签,并涉及模型本身被应用来通过包括模型投票和增强在内的策略生成一些元函数。

关于整体创作,有一种替代的设计理念,称为堆叠,或者称为混合。堆叠涉及配置中的多层模型,其中一层模型的输出被用作下一层模型的训练数据。有可能成功地融合数百种不同的模式。

堆叠系综还可以从多个子混合(有时称为混合)中组成图层输出的混合要素集。为了增加乐趣,还可以从堆叠集合的模型中提取特别有效的参数,并在不同级别的混合或子混合中将其用作元特征。

**所有这些结合在一起,使堆叠集成成为一种非常强大和可扩展的技术。卡格尔网飞奖(以及相关的 100 万美元奖金)的获奖者在数百个特写镜头上使用了叠加合奏,效果非常好。他们使用了一些额外的技巧来提高预测的有效性:

  • 他们在保留一些数据的同时训练和优化了他们的整体。然后,他们使用保留的数据进行再培训,并在将模型应用于测试数据集之前再次优化。这并不是一个罕见的做法,但它产生了良好的结果,值得记住。
  • 他们使用梯度下降和 RMSE 作为性能函数进行训练。至关重要的是,他们使用整体的 RMSE,而不是任何模型的,作为相关的性能指标(残差的度量)。无论何时与合奏团合作,这都应该被视为一种健康的做法。
  • 他们使用已知的模型组合来改善其他模型的残差。例如,基于邻域的方法改进了 RBM 残差,我们在本书前面已经讨论过了。通过了解机器学习算法的相对优势和劣势,您可以找到理想的集成配置。
  • 他们使用 k 倍交叉验证计算混合的残差,这是我们在本书前面探索和应用的另一种技术。这有助于克服这样一个事实,即他们已经使用与最终混合相同的数据集训练了混合的组成模型。

从曾经获得网飞奖的务实混沌模型的高度定制化本质中抽离出来的要点是,一流的模型通常是密集迭代和一些创造性的网络配置变化的产物。另一个关键要点是堆叠集合的基本架构模式如下:

Using stacking ensembles

既然你已经学习了堆叠集合如何工作的基本原理,让我们尝试应用它们来解决数据问题。为了让我们开始,我们将使用Chapter 8附带的 GitHub 存储库中提供的blend.py代码。这种混合代码的版本已经被多个比赛中得分较高的卡格勒使用。

首先,我们将研究如何应用堆叠系综来解决一个真正的数据科学问题:卡格尔竞赛预测生物反应旨在建立一个尽可能有效的模型,以预测给定化学性质的分子的生物反应。我们将关注本次竞赛中一个特别成功的参赛作品,以了解堆叠合奏如何在实践中发挥作用。

在这个数据集中,每行代表一个分子,而 1,776 个特征中的每一个都描述了所讨论的分子的特征。考虑到这些特性,我们的目标是预测相关分子的二元反应。

我们将应用的代码来自该锦标赛中的一个竞争对手,他使用堆叠集成来组合五个分类器:两个不同配置的随机森林分类器、两个额外的树分类器和一个梯度提升分类器,这有助于产生与其他四个组件略有不同的预测。

重复的分类器具有不同的划分标准。其中一个使用了基尼不纯度 T2(基尼),这是一种衡量随机记录被错误标记的频率的方法,如果它根据潜在的有问题的分支中的标记分布被随机标记。另一棵树使用信息增益(熵),一种衡量信息内容的方法。潜在分支的信息内容可以通过对其编码所需的比特数来测量。使用熵作为衡量标准来确定适当的分割会导致分支变得越来越不多样化,但重要的是要认识到熵和gini标准会产生完全不同的结果:

if __name__ == '__main__':

    np.random.seed(0)

    n_folds = 10
    verbose = True
    shuffle = False

    X, y, X_submission = load_data.load()

    if shuffle:
        idx = np.random.permutation(y.size)
        X = X[idx]
        y = y[idx]

    skf = list(StratifiedKFold(y, n_folds))

    clfs = [RandomForestClassifier(n_estimators=100, n_jobs=-1, 
criterion='gini'),
            RandomForestClassifier(n_estimators=100, n_jobs=-1, 
criterion='entropy'),
            ExtraTreesClassifier(n_estimators=100, n_jobs=-1, 
criterion='gini'),
            ExtraTreesClassifier(n_estimators=100, n_jobs=-1, 
criterion='entropy'),
            GradientBoostingClassifier(learning_rate=0.05, 
subsample=0.5, max_depth=6, n_estimators=50)]

    print "Creating train and test sets for blending."

    dataset_blend_train = np.zeros((X.shape[0], len(clfs)))
    dataset_blend_test = np.zeros((X_submission.shape[0], len(clfs)))

    for j, clf in enumerate(clfs):
        print j, clf
        dataset_blend_test_j = np.zeros((X_submission.shape[0], 
len(skf)))
        for i, (train, test) in enumerate(skf):
            print "Fold", i
            X_train = X[train]
            y_train = y[train]
            X_test = X[test]
            y_test = y[test]
            clf.fit(X_train, y_train)
            y_submission = clf.predict_proba(X_test)[:,1]
            dataset_blend_train[test, j] = y_submission
            dataset_blend_test_j[:, i] = 
clf.predict_proba(X_submission)[:,1]
        dataset_blend_test[:,j] = dataset_blend_test_j.mean(1)

    print
    print "Blending."
    clf = LogisticRegression()
    clf.fit(dataset_blend_train, y)
    y_submission = clf.predict_proba(dataset_blend_test)[:,1]

    print "Linear stretch of predictions to [0,1]"
    y_submission = (y_submission - y_submission.min()) / 
(y_submission.max() - y_submission.min())

    print "Saving Results."
    np.savetxt(fname='test.csv', X=y_submission, fmt='%0.9f')

当我们尝试在私人排行榜上运行这个提交时,我们发现自己处于相当令人印象深刻的第 12 位位置(在 699 个竞争对手中)!自然,我们不能从完成后进入的竞赛中得出太多结论,但是,考虑到代码的简单性,这仍然是一个相当令人印象深刻的结果!

在实践中应用合奏

在应用集成方法时需要注意的一个特别重要的品质是,您的目标是调整集成的性能,而不是组成集成的模型。因此,你的方法应该主要集中在建立一个强有力的合奏表演得分上,而不是最强的单个模型表演。

你对整体中的模特的关注程度会有所不同。对于单一类型(例如,随机森林)的不同配置或初始化模型的排列,明智的做法是几乎完全专注于集合的性能和塑造它的元参数。

对于更具挑战性的问题,我们经常需要更密切地关注我们整体中的单个模型。当我们试图为更具挑战性的问题创建更小的集成时,这显然是正确的,但是要构建真正优秀的集成,通常需要考虑您构建的结构背后的参数和算法。

说了这么多,你就会一直在看合奏的表现以及布景中模特的表现。你将检查你的模型的结果,试图找出每个模型做得好的地方。您还将寻找影响集合性能的不太明显的因素,最显著的是模型预测的相关性。人们普遍认为,一个更有效的合奏往往包含有表演性但不相关的成分。

要理解这种说法,可以考虑相关度量和主成分分析等技术,我们可以使用这些技术来度量数据集变量中存在的信息量。同样,我们可以使用皮尔逊相关系数与我们每个模型输出的预测进行比较,以了解每个模型的性能和相关性之间的关系。

具体地说,让我们回到堆叠系综,我们的系综模型输出元特征,这些元特征然后被用作下一层模型的输入。就像我们检查更传统的神经网络所使用的特征一样,我们希望确保由我们的集成组件输出的特征作为数据集工作良好。在这方面,计算模型输出之间的皮尔逊相关系数并在模型选择中使用结果是一个很好的起点。

当我们处理单模型问题时,我们几乎总是要花一些时间来检查问题并确定一个合适的学习算法。如果我们面临一个两类分类问题,其中有适量的特征( 10 个)和标记的训练案例,我们可能会选择逻辑回归、SVM 或其他适合上下文的算法。不同的方法将适用于不同的问题,并通过反复试验,平行测试和经验(个人和网上发布!),您将确定给定特定输入数据的特定目标的适当方法。

类似的逻辑也适用于合奏创作。挑战不是识别单一的适当模型,而是识别有效描述输入数据集不同元素的模型组合,从而充分描述数据集整体。通过了解您的组件模型的优势和劣势,以及通过探索和可视化您的数据集,您将能够得出关于如何通过多次迭代有效地开发您的集成的结论。

最终,在这个层面上,数据科学是一个拥有大量技术的领域。最好的实践者能够应用他们自己的算法和选项的知识,在多次迭代中开发出非常有效的解决方案。

这些解决方案涉及算法知识和模型组合的交互、模型参数调整、数据集转换和集成操作。同样重要的是,它们需要一种无拘无束和创造性的心态。

这方面的一个很好的例子是著名的卡格尔竞争对手亚历山大·古斯钦的作品。关注一个具体的例子——奥托产品分类竞赛——可以让我们了解自信而有创造力的数据科学家可以选择的范围。

大多数模型开发过程都是从一个阶段开始的,在这个阶段中,您会针对问题抛出不同的解决方案,试图找到数据背后的技巧,并找出有效的方法。亚历山大决定采用堆叠模型,开始构建图元特征。虽然我们将 XGBoost 视为一个独立的集成,但在这种情况下,它被用作堆叠集成的一个组件,以便生成一些元特征供最终模型使用。除了梯度增强树之外,还使用了神经网络,因为这两种算法都倾向于产生好的结果。

为了给混合物添加一些对比,Alexander 添加了一个 KNN 实现,特别是因为 KNN 生成的结果(以及元参数)往往与已经包含的模型有很大不同。这种拾取输出往往不同的组件的方法对于创建有效的堆叠集合(以及大多数集合类型)至关重要。

为了进一步开发这个模型,亚历山大在他的模型的第二层增加了一些定制元素。在结合 XGBoost 和神经网络预测的同时,他还在这一层增加了装袋。在这一点上,我们在本章中讨论的大多数技术已经在这个模型的某些部分出现了。除了模型开发之外,一些特征工程(特别是在一半的训练和测试数据中使用 TF-IDF)和使用绘图技术来识别类别差异也被贯穿始终。

一个真正成熟的模型可以解决最重要的数据科学挑战,它结合了我们在本书中看到的技术,通过对底层算法以及这些技术如何相互作用的可能性的深入理解而创建。

到目前为止,这本书已经教授了许多从业者必须收集的基础知识。它使用了许多例子和越来越多的真实案例来展示广泛的知识基础如何变得越来越强大,让你开发出解决困难问题的有效方法。

作为一名数据科学家,你需要做的是首先应用这一系列广泛的技术来发展一种经验,了解他们如何表现以及他们能为你做些什么。接下来就看你如何培养那种创造力和实验思维,这种思维让一些最优秀的数据科学家与众不同。

在动态应用中使用模型

我们花了这一章讨论在条件下管理模型性能的技术的使用,这些条件可能被视为理想的;具体来说,所有数据提前可用的条件,以便可以在所有数据上训练模型。这些假设在研究环境中或在处理一次性问题时通常是有效的,但在许多情况下,它们是不安全的假设。不安全环境的范围超出了数据根本不可用的情况,例如数据科学竞赛,使用一个保留的数据集来建立最终的排行榜。

回到本章前面的主题,你会想起获得网飞奖的实用混沌算法?当网飞开始评估实现算法时,业务环境和需求都发生了巨大的变化,以至于该算法提供的最小精度增益无法证明实现成本是合理的。100 万美元的算法是多余的,从未在生产中实现过!从这个例子中可以看出,在商业环境中,我们的模型尽可能具有适应性是至关重要的。

机器学习算法真正具有挑战性的应用是跨时间(或其他维度)发生真实数据变化的应用,在这些应用中,我们现有的运行一次的方法变得不那么有价值。在这些情况下,人们知道将会发生实质性的数据变化,并且现有的模型不容易被训练来适应这种数据变化。在这一点上,需要新的技术和新的信息。

为了适应和收集这些信息,我们需要更好地预测数据变化可能发生的方式。有了这些信息,我们的模型构建和集合的内容可以开始改变,以涵盖我们看到的最有可能的数据变化场景。这种自适应让我们能够抢先进行数据更改,并减少所需的调整时间。正如我们将在本章后面看到的,在现实世界的应用程序中,任何基于数据变化的数据透视时间的减少都是有价值的。

在下一节中,我们将研究可以用来使我们的模型对不断变化的数据更加健壮的工具。我们将讨论如何维护一组广泛的模型选项,同时适应一个或多个数据更改场景,而不降低模型的性能。

理解模型的鲁棒性

重要的是要准确理解这里的问题是什么,以及它是如何和何时出现的。这包括定义两件事;首先是鲁棒性,因为它适用于机器学习算法。第二,当然是数据变化。本节第一部分的一些内容处于入门水平,但是有经验的数据科学家可能仍然会发现回顾本节的价值!

用学术术语来说,机器学习算法的健壮性是一个属性,它描述了当应用于数据集而不是训练它的数据集时,你的算法有多有效。

健壮性测试是任何环境下机器学习方法的核心部分。k-fold 交叉验证等验证技术的重要性以及在为最简单的上下文开发模型时使用测试是机器学习算法易受数据变化影响的结果。

大多数数据集包含信号和噪声。噪音可能是可预测的(因此更容易管理),也可能是随机的,难以处理。数据集可能包含或多或少的噪声。通常,具有或多或少的可预测噪声的数据集在去除该噪声的相同数据集上更难训练和测试(可以容易地测试)。

当一个人在给定的数据集上训练了一个模型时,几乎不可避免的是,这个模型是基于信号和噪声来学习的。过拟合的概念通常用于描述一个模型,该模型非常适合给定的数据集,以至于它学会了基于信号和噪声进行预测,这使得它对其他样本的预测能力不如拟合不太精确的模型。

训练模型的部分目标是尽可能减少任何局部噪声对学习的影响。保留一组数据进行测试的验证技术的目的是确保在训练期间对噪声的任何学习只发生在训练集本地的噪声上。训练误差和测试误差之间的差异可以用来理解模型实现之间的过度拟合程度。

我们已经在第 1 章无监督机器学习中应用了交叉验证。测试过拟合模型的另一种有用的方法是以抖动的形式直接向训练数据集中添加随机噪声。2015 年 10 月,亚历山大·安希金通过卡格尔笔记本引入了这项技术,并提供了一个非常有趣的测试。概念简单;通过添加抖动并查看训练数据的预测精度,我们可以区分过度拟合的模型(随着我们添加抖动,其训练误差将更快增加)和拟合良好或拟合不良的模型:

Understanding model robustness

在这种情况下,我们能够绘制抖动测试的结果,以轻松识别模型是否过度抖动。从非常强的初始位置开始,随着少量抖动的增加,overfit 模型的性能通常会迅速下降。对于拟合较好的模型,增加抖动时的性能损失会大大降低,在低水平的增加抖动时,模型的过拟合程度尤其明显(拟合较好的模型往往优于过拟合的模型)。

让我们看看如何实现过度拟合的抖动测试。我们用一个熟悉的分数,accuracy_score,定义为正确预测的类标签比例,作为考试评分的依据。抖动是通过简单地向数据添加随机噪声(使用np.random.normal)来定义的,噪声量由可配置的scale参数定义:

from sklearn.metrics import accuracy_score

def jitter(X, scale):
    if scale > 0:        
        return X + np.random.normal(0, scale, X.shape)
    return X

def jitter_test(classifier, X, y, metric_FUNC = accuracy_score, sigmas = np.linspace(0, 0.5, 30), averaging_N = 5):
    out = []

    for s in sigmas:
        averageAccuracy = 0.0
        for x in range(averaging_N):
            averageAccuracy += metric_FUNC( y, classifier.predict(jitter(X, s)))

        out.append( averageAccuracy/averaging_N)

    return (out, sigmas, np.trapz(out, sigmas))

allJT = {}

给定一个分类器、训练数据和一组目标标签,jitter_test本身就是定义为正常 sklearn 分类的包装器。然后调用分类器,根据首先调用jitter操作的数据版本进行预测。

此时,我们将开始创建大量数据集来运行抖动测试。我们将使用 sklearn 的make_moons数据集,通常用作可视化聚类和分类算法性能的数据集。这个数据集由两个类组成,它们的数据点形成交错的半圆。通过向make_moons添加不同数量的噪声并使用不同数量的样本,我们可以创建一系列示例来运行抖动测试:

import sklearn
import sklearn.datasets

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

Xs = []
ys = []

#low noise, plenty of samples, should be easy
X0, y0 = sklearn.datasets.make_moons(n_samples=1000, noise=.05)
Xs.append(X0)
ys.append(y0)

#more noise, plenty of samples
X1, y1 = sklearn.datasets.make_moons(n_samples=1000, noise=.3)
Xs.append(X1)
ys.append(y1)

#less noise, few samples
X2, y2 = sklearn.datasets.make_moons(n_samples=200, noise=.05)
Xs.append(X2)
ys.append(y2)

#more noise, less samples, should be hard
X3, y3 = sklearn.datasets.make_moons(n_samples=200, noise=.3)
Xs.append(X3)
ys.append(y3)

完成后,我们接着创建一个plotter对象,我们将使用该对象直接根据输入数据显示模型的性能:

def plotter(model, X, Y, ax, npts=5000):

    xs = []
    ys = []
    cs = []
    for _ in range(npts):
        x0spr = max(X[:,0])-min(X[:,0])
        x1spr = max(X[:,1])-min(X[:,1])
        x = np.random.rand()*x0spr + min(X[:,0])
        y = np.random.rand()*x1spr + min(X[:,1])
        xs.append(x)
        ys.append(y)
        cs.append(model.predict([x,y]))
    ax.scatter(xs,ys,c=list(map(lambda x:'lightgrey' if x==0 else 'black', cs)), alpha=.35)
    ax.hold(True)
    ax.scatter(X[:,0],X[:,1],
                 c=list(map(lambda x:'r' if x else 'lime',Y)), 
                 linewidth=0,s=25,alpha=1)
    ax.set_xlim([min(X[:,0]), max(X[:,0])])
    ax.set_ylim([min(X[:,1]), max(X[:,1])])
    return

我们将使用 SVM 分类器作为抖动测试的基础模型:

import sklearn.svm
classifier = sklearn.svm.SVC()

allJT[str(classifier)] = list()

fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(11,13))
i=0
for X,y in zip(Xs,ys): 
    classifier.fit(X,y)
    plotter(classifier,X,y,ax=axes[i//2,i%2])
    allJT[str(classifier)].append (jitter_test(classifier, X, y))
    i += 1
plt.show()

抖动测试为评估模型过拟合提供了一种有效的手段,其性能与交叉验证相当;事实上,Minushkin 提供的证据表明,作为衡量模型拟合质量的工具,它的表现可以超过交叉验证。

这两种减轻过度拟合的工具在您的算法一次性处理数据或者底层趋势变化不大的情况下都能很好地工作。对于大多数单数据集问题(如大多数学术或网络存储库数据集)或底层趋势变化缓慢的数据问题来说,情况确实如此。

然而,在许多情况下,建模中涉及的数据可能会随着时间的推移在一个或几个维度上发生变化。这可能是因为数据采集方法的改变,通常是因为使用了新的仪器或技术。例如,自 2005 年以来的十年间,普通设备捕获的视频数据在分辨率和质量(以及大小!)的数据有所增加。无论您是使用视频帧本身,还是使用文件大小作为参数,您都将观察到特性的性质、质量和分布的显著变化。

或者,数据集变量的变化可能是由潜在趋势的差异引起的。度量和维度的经典数据模式概念又回来了,因为我们可以通过考虑哪些维度影响我们的度量来更好地理解数据变化是如何受到影响的。

关键的例子是时间。根据具体情况,许多变量会受到星期几、月份或季节变化的影响。在许多情况下,一个有用的选择可能是参数化这些变量(正如我们在上一章中所讨论的,诸如 one-hot 编码之类的技术可以帮助我们的算法学习解析这样的趋势),特别是如果我们处理的是容易预测的周期性趋势(例如,一年中某个月份对给定位置围巾销售的影响)并且容易建模的话。

更成问题的类型的时间序列趋势是非周期性变化。就像前面的摄像机例子一样,某些类型的时间序列趋势会不可逆转地发生变化,而且变化的方式可能不容易预测。来自软件的遥测往往会受到发射遥测时软件实时构建的质量和功能的影响。随着构建随时间变化,遥测发送的值和从这些值创建的变量可能会在一夜之间以难以预测的方式发生根本变化。

人类行为是许多数据集中非常重要的因素,它有助于周期性和非周期性地变化。人们更多地在季节性假期购物,但也会根据新的社会或技术发展永久地改变他们的购物习惯。

这里增加的一些复杂性不仅来自于单个变量及其分布受时间序列趋势影响的事实,还来自相关因素及其相关变量之间的关系将如何变化。变量之间的关系可能会以可量化的方式发生变化。一个例子是,对于人类来说,身高和体重是两个变量,它们的关系因时间和地点而异。我们可以使用身体质量指数特征来跟踪这种关系,当在不同时间段或不同位置进行采样时,它显示出不同的分布。

此外,变量可以以另一种严肃的方式变化;也就是说,它们对高性能建模算法的重要性可能会随着时间而变化!某些变量的值在某些时间段高度相关,但在其他时间段相关性较低。例如,考虑气候和天气变量如何影响农业市场。对于一些作物和经营这些作物的公司来说,这些变量在一年的大部分时间里都相当不重要。然而,在作物生长和收获的时候,它们变得至关重要。更复杂的是,这些因素的重要性还与位置(和当地气候)有关。

建模的挑战显而易见。对于经过一次训练并在新数据上再次运行的模型,管理数据更改可能会带来严重的挑战。对于基于新输入数据动态重新计算的模型,随着变量分布和关系的变化以及可用变量在生成有效解决方案时变得或多或少有价值,数据变化仍然会产生问题。

在您的 ML 应用程序中,成功管理数据变更的部分关键是识别变更可能影响特性分布、关系和特性重要性的维度(也有共同的过失),模型将尝试了解这些维度。

一旦您了解了数据中哪些因素可能会影响过度拟合,您就可以更好地开发有效管理这些因素的解决方案。

尽管如此,构建一个能够解决任何潜在问题的单一模型仍将极具挑战性。对此的简单回答是,如果一个人面临严重的数据更改问题,解决方案可能不是试图用单一模型来解决它们!在下一节中,我们将研究集成方法来提供更好的答案。

识别建模风险因素

虽然在许多情况下,随着时间的推移,识别哪些元素会给模型带来风险是非常简单的,但是使用结构化的过程进行识别会有所帮助。本节简要描述了一些启发式方法和技术,您可以使用它们来筛选模型中的数据变更风险。

大多数数据科学家都有一个数据字典,用于一般用途或自动化应用的数据集。如果数据或应用程序很复杂,这种情况尤其可能发生,但是保存数据字典通常是一种很好的做法。在识别风险因素时,您可以做的一些最有效的工作是浏览这些功能,并根据不同的风险类型对它们进行标记。

我倾向于使用的一些标签包括:

  • 纵向变量:该参数是否会由于纵向趋势而在很长一段时间内发生变化,而这些趋势在您现有的训练数据范围内并不完全可见?最明显的例子是生态季节,它影响着人类行为的许多领域,以及许多依赖于一些更基本的气候变量的事物。其他纵向趋势包括财政年度和工作月份,但扩展到包括与您的调查领域相关的许多其他纵向趋势。新 iPhone 机型的生命周期或田鼠的种群流动可能是一个重要的纵向因素,这取决于你的工作性质。
  • 缓慢变化:随着时间的推移,这个分类参数有可能获得新的值吗?这个概念是从数据仓库最佳实践中借用来的。经典意义上缓慢变化的维度将获得新的参数代码(例如,当一家新店开业或一个新案例被识别时)。如果管理不当或者出现的数量足够多,这些可以完全抛弃你的模型。缓慢变化的数据的另一个影响是,它会开始影响你的特性的分布,这可能会更难处理。这可能会对模型的有效性产生重大影响。
  • 关键参数:数据值监控和决策边界/回归方程重新计算的组合通常可以很好地处理一定数量的缓慢变化的数据和季节性方差,但是如果您看到意外大量的新案例或案例类型,特别是当它们影响您的模型严重依赖的变量时,请考虑采取行动。因此,也要确保你知道你的解决方案最依赖哪些变量!

以这种方式标记的过程是有帮助的(不仅仅是作为你自己记忆的输出),主要是因为它帮助你做以下事情:

  • 组织你的期望,并为你监控准备的发展制定一个清单。如果您不能至少跟踪您的纵向变量和缓慢变化的参数变化,那么除了重新计算时支持的参数变化及其(可能缓慢下降的)性能度量之外,您实际上对模型的任何输出都是盲目的。
  • 调查缓解措施(例如,改进的规范化或额外的参数,这些参数编码了数据变化的维度)。在许多方面,缓解和添加参数是处理数据更改的最佳解决方案。
  • 使用构建的数据集设置稳健性测试,其中您的风险特征被故意改变以模拟数据变化。在这些条件下对你的模型进行压力测试,找出它到底能承受多大的方差。有了这些信息,您可以轻松地将自己设置为使用您的监控值作为早期警报系统;一旦数据变化超过某个安全阈值,您就知道模型性能会下降多少。

管理模型稳健性的策略

我们已经讨论了许多有效的集成技术,这些技术使我们能够平衡对高性能和健壮模型的双重需求。然而,在我们阐述和使用这些技术的过程中,我们必须决定如何以及何时降低模型的性能以提高健壮性。

事实上,本章的一个共同主题是如何平衡创建一个有效的、高性能的模型的冲突目标,同时又不会使这个模型过于不灵活而无法响应数据变化。到目前为止,我们看到的许多解决方案都要求我们权衡一种结果和另一种结果,这并不理想。

在这一点上,值得我们从更广的角度来看待我们的选择,并借鉴互补的技术。在不断发展的商业环境中,对稳健的、高性能的统计模型的需求既不是新的,也不是未得到处理的;信用风险建模等领域在不断变化的领域中应用统计建模的历史悠久,并且已经开发出有效的决策管理方法以取得成功。数据科学家可以通过使用这些既定技术来帮助组织我们自己的模型,从而将其中一些技术转化为我们自己的利益。

一种有效的方法是 冠军/挑战者,一种以测试为中心的方法,包括运行多个并行模型配置。除了其输出被应用的模型(用于指导业务活动或信息报告),冠军/挑战者方法培训一个或多个替代模型配置。

通过维护和监控多个模型,可以安排在替代模型性能超过当前模型时替换当前模型。这通常是通过维护所有模型的性能评分过程并观察结果来完成的,这样就可以手动决定是否以及何时切换到挑战者。

虽然最简单的实现可能涉及到在性能超过主模型时立即切换到挑战者,但这很少实现,因为特定挑战者模型存在暴露于局部最小值的风险(例如,一周中的某一天或一年中的某一月的局部趋势)。花费大量时间评估挑战者模型是正常的,尤其是在敏感应用程序之前。在复杂的真实案例中,人们甚至可能希望通过向有希望的挑战者提供治疗案例的样本来进行额外的测试,以确定它是否对冠军产生了显著的提升。

除了简单的“取代挑战者”继任规则,还有一些创新的空间。基于投票的方法非常常见,其中训练好的集合的顶级子集在个案的基础上提供分数,这些分数被视为(加权或未加权)投票。另一种方法是使用“T2”投票系统,即每个投票人按照偏好对候选方案进行排序。在集合的上下文中,人们通常会给每个单独模型的预测分配一个与其逆秩相等的点值(保持每个模型独立!).然后人们可以将这些投票组合起来(通常尝试一系列不同的权重)以产生一个结果。

投票可以在大量模型的情况下相当好地执行,但是取决于特定的建模环境和因素,例如不同投票者的相似性。正如我们在本章前面所讨论的,使用皮尔逊相关系数等测试来确保您的模型集既有性能又不相关是至关重要的。

人们可能发现特定类别的输入数据(例如,具有特定分段标签的用户)被给定的挑战者更有效地对待,并且可能实现一个案例路由系统,其中多个冠军处理不同的用户子组。这种方法与增强集成的好处有些重叠,但可以通过分离关注点来帮助生产环境。然而,维护多个冠军将增加您的数据团队的监控和监督负担,因此如果不是完全必要的话,最好避免这种选择。

需要解决的一个主要问题是我们如何对我们的模型进行评分,尤其是因为存在直接的实际挑战。特别是,考虑到类标签(用于指导正确性)通常不可用,很难在实际环境中比较多个模型。在预测环境中,这个问题由于冠军模型的预测通常用于采取改变预测事件的行动而变得更加复杂。这项活动使得很难断言挑战者模型的预测会有怎样的表现;根据冠军的预测采取行动,我们无法确认我们模型的结果!

最常见的实施过程是为每个挑战者模型提供一个统计上可行的输入数据样本,然后比较每种方法的升力。这种方法固有地限制了一些建模问题可以支持的挑战者的数量。另一个选择是从任何治疗活动中只留下一个统计上可行的样本,并使用它来创建一个单一的回归测试。这项测试适用于冠军和挑战者的整套模型,为比较提供了有意义的基础。

这种方法的缺点是,无论为测试用例生成正确的类标签需要多长时间,到一个更有效的模型的变化总是会跟踪数据的变化。虽然在许多情况下,这并不严重(冠军模型在生成精确模型所需的时间内保持不变),但在基础条件与模型的训练时间相比变化迅速的背景下,它可能会出现问题。

模型训练时间和数据变化频率之间的关系值得简单评论一下。它并不总是如此明确地陈述,但是应用机器学习环境中的典型目标是将训练时间与数据变化频率的因子减少到尽可能小的值。从最坏的情况来看,如果训练一个模型所需的时间长于该模型精确的时间长度(并且该比率等于或大于 1),那么您的模型将永远不会生成可以直接驱动当前动作的当前结果。一般而言,高比率应促进审查和调整活动(要么调查在较低置信度下更快的分数交付是否带来更多价值,要么调整可控环境变量的变化速度)。

这个比率变得越小,你的团队就有越多的余地来应用你的模型的输出来驱动行动和产生价值。根据这个比率在您的建模环境中的变化和可量化程度,它可以作为您的自动化建模解决方案的健康度量在您的组织中推广。

这些替代模型可能只是下一个性能最好的集合配置;他们可能是老型号,留在周围观察。在复杂的操作中,一些挑战者被配置为处理不同的假设场景(例如,如果该地区的温度比预期低 2°C 怎么办如果销售额明显低于预期怎么办)。这些模型可能是在与主模型相同的数据上训练的,或者是在模拟假设情景的故意扭曲或准备好的数据上训练的。

更多的挑战者往往更好(提供改进的健壮性和性能),前提是挑战者不都是同一主题的微小变化。挑战者模型还为创新和测试提供了一个安全的场所,同时观察有效的挑战者可以提供有用的见解,了解您的冠军团队对一系列可能的环境变化有多强大。

您在本节中学习应用的技术为我们提供了工具,可以将我们现有的模型工具包应用到不断发展的环境中的实际应用中。本章还讨论了将 ML 模型应用于生产时可能出现的复杂情况;样本之间或跨维度的数据变化将导致我们的模型变得越来越无效。通过彻底解开数据变化的概念,我们变得能够更好地描述这种风险,并认识到它可能出现在哪里以及如何出现。

一章的剩余部分专门介绍了提高模型鲁棒性的技术。我们讨论了如何通过查看底层数据来识别模型降级风险,并讨论了一些有用的启发式方法。我们从现有的决策管理方法中学习和使用 Champion/Challenger,这是一个在包括应用机器学习在内的环境中有着悠久历史的备受关注的过程。冠军/挑战者帮助我们在良性竞争中组织和测试多个模型。结合有效的性能监控,模型替代的主动战术计划将为您提供更快、更可控的模型生命周期和质量管理,同时提供大量有价值的运营见解。

进一步阅读

也许最广泛和信息最丰富的合奏和合奏类型之旅是由卡格勒的竞争对手特里克里昂在 http://mlwave.com/kaggle-ensembling-guide/提供的。

关于 Netflix 获奖模式《务实的混沌》的讨论,请参考http://www . stat . OSU . edu/~ dmsl/grand Prize 2009 _ BPC _ bellkor . pdf。关于网飞对不断变化的商业环境如何让这种 100 万美元的模式变得多余的解释,请参考网飞理工大学的博客。

关于将随机森林集合应用于商业环境的演练,为所有重要的诊断图表和推理提供了大量空间,请考虑 Arshavir Blackwell 的博客,网址为https://citizen net . com/blog/2012/11/10/random-forests-ensembles-and-performance-metrics/

关于随机森林的更多信息,我发现 scikit-learn 文档很有帮助:http://sci kit-learn . org/stable/modules/generated/sklearn . ensemble . randomforestclaider . html

http://xgboost.readthedocs.io/en/latest/model.html的 XGBoost 文档中提供了一个关于梯度增强树的很好的介绍。

有关 Alexander Guschin 参加 Otto 产品分类挑战赛的报道,请参考 No Free Hunch 博客:http://blog . kaggle . com/2015/06/09/Otto-产品-分类-获奖者-面试-第二名-alexander-guschin/

Alexander Minushkin 的过拟合抖动测试描述在https://www . kaggle . com/miniushkin/introduction-kaggle-scripts/抖动-过拟合测试-笔记本中。

总结

在这一章中,我们涉及了很多方面。我们从引入集成开始,集成是竞争机器学习环境中一些最强大和最受欢迎的技术。我们结合专家知识和实际例子,介绍了将集成应用于机器学习项目所需的理论和代码。

此外,本章还专门用一节来讨论当您一次运行几周或几个月的模型时出现的独特注意事项。我们讨论了数据变化意味着什么,如何识别它,以及如何考虑防范它。我们特别考虑了如何创建并行运行的模型集的问题,您可以根据模型集中的季节变化或性能漂移在这些模型集之间进行切换。

在我们回顾这些技术的过程中,我们花了大量时间研究现实世界中的例子,具体目的是了解最佳数据科学家所需的创造性思维和广泛的知识。

这本书中的技术已经达到了这样一个程度,有了技术知识、可以重新应用的代码和对可能性的理解,你真的能够接受任何数据建模挑战。**

九、其他 Python 机器学习工具

在前面八章的过程中,我们研究并应用了一系列技术,这些技术帮助我们丰富和建模许多应用程序的数据。

我们使用 Python 库的组合来处理这些章节中的内容,特别是 NumPy 和 antao,而其他库是在我们需要访问特定算法时使用的。我们没有花太多时间讨论工具方面还存在哪些选项,这些工具的独特优势是什么,或者我们为什么会感兴趣。

这最后一章的主要目标是突出一些您可以使用的其他关键库和框架。这些工具简化了创建和应用模型的过程。本章介绍了这些工具,演示了它们的应用,并提供了大量关于进一步阅读的建议。

成功解决数据科学挑战和成功成为数据科学家的一个主要贡献者是对算法和库的最新发展有很好的理解。作为专业人士,数据科学家往往高度依赖他们使用的数据质量,但拥有最佳可用工具也非常重要。

在本章中,我们将回顾数据科学家最近可用的一些最佳工具,确定它们提供的好处,并讨论如何在一致的工作过程中,将它们与本书前面讨论的工具和技术一起应用。

替代发展工具

在过去的几年里,出现了许多新的机器学习框架,它们在工作流方面具有优势。通常,这些框架高度关注特定的用例或目标。这使得它们非常有用,甚至可能是必备的工具,但这也意味着您可能需要使用多个工作流改进库。

随着越来越多的新 Python ML 项目被点亮以解决特定的工作流挑战,值得讨论两个库,它们增加了我们现有的工作流,并加速或改进了我们在前面几章中所做的工作。在本章中,我们将介绍千层面TensorFlow ,讨论每个库的代码和功能,并确定为什么每个框架都值得作为工具集的一部分来考虑。

千层面介绍

让我们面对它;有时候用 Python 创建模型的时间比我们希望的要长。然而,它们对于更复杂的模型来说可能是有效的,并提供了很大的好处(例如图形处理器加速和可配置性)。当处理简单的情况时,类似于 Anano 的库可能相对复杂。这是不幸的,因为我们经常希望使用简单的模型,例如,当我们建立基准时。

千层面是一个由深度学习和音乐数据挖掘研究人员团队开发的图书馆,作为 Anano 的界面。它是专门为确定一个特定的目标而设计的——允许快速有效地建立新模型的原型。

这个焦点决定了如何创建 Lasagne,以一种比用本机代码编写的相同操作简单得多且更容易理解的方式调用函数并返回表达式或数据类型。

在这一节中,我们将看一下 Lasagne 底层的概念模型,应用一些 Lasagne 代码,并了解该库为我们现有的实践添加了什么。

了解千层面

千层面使用层的概念进行操作,这是机器学习中常见的概念。层是一组神经元和操作规则,它们接受输入并生成分数、标签或其他转换。神经网络通常充当一组层,在一端输入数据,在另一端输出值(尽管实现方式各不相同)。

在深度学习环境中,开始将单个层视为一等公民已经变得非常流行。传统上,在机器学习工作中,网络将仅使用几个参数规格(如节点数、偏差和权重值)从层建立。

近年来,寻求额外优势的科学家们开始对单个层的配置越来越感兴趣。如今,在高级机器学习环境中,看到包含子模型和转换输入的层并不罕见。如今,即使是要素也可能根据需要跳过图层,新要素可能会在模型的中途添加到图层中。作为这种改进的一个例子,考虑谷歌用来解决图像识别挑战的卷积神经网络架构。这些网络在层级别进行了广泛的改进,以提高性能。

因此,千层面将图层视为其基本模型组件是有意义的。千层面为模型创建过程增加的是快速直观地将不同层堆叠到模型中的能力。你可以简单地在lasagne.layers内调用一个类,将一个类堆叠到你的模型上。这方面的代码非常高效,如下所示:

l0 = lasagne.layers.InputLayer(shape=X.shape)

l1 = lasagne.layers.DenseLayer(
l0, num_units=10, nonlinearity=lasagne.nonlinearities.tanh)

l2 = lasagne.layers.DenseLayer(l1, num_units=N_CLASSES, nonlinearity=lasagne.nonlinearities.softmax)

在三个简单的语句中,我们使用简单且可配置的函数创建了网络的基本结构。

这段代码使用三层创建一个模型。层l0调用InputLayer类,作为我们模型的输入层。该图层基于输入的预期形状(使用shape参数定义),将我们的输入数据集转换为张量。

接下来的层l1l2都是完全连接(密集)的层。图层l2定义为输出图层,单位数等于类数,而l1使用相同的DenseLayer类创建10单位的隐藏图层。

除了配置DenseLayer类可用的标准参数(权重、偏差、单位计数和非线性类型)之外,还可以使用使用不同类别的完全不同的网络类型。千层面为一系列常见的层提供分类,包括密集层、卷积层和汇集层、循环层、归一化层和噪声层等。此外,还有一个特殊用途的层类,它提供了一系列附加功能。

当然,如果需要比这些类提供的更多的定制,用户可以很容易地定义自己的图层类型,并将其与其他千层面类结合使用。然而,对于大多数原型开发和快速迭代开发环境来说,这是大量的预先准备的功能。

千层面提供了类似简洁的界面来定义网络的损失计算:

true_output = T.ivector('true_output')
objective = lasagne.objectives.Objective(l2, loss_function=lasagne.objectives.categorical_crossentropy)

loss = objective.get_loss(target=true_output)

这里定义的loss函数是许多可用函数中的一个,包括平方误差、二进制和多类情况下的铰链损失以及crossentropy函数。还提供了用于验证的准确性评分功能。

有了这两个组件,一个loss功能和一个网络架构,我们再次拥有了训练网络所需的一切。为此,我们需要编写更多的代码:

all_params = lasagne.layers.get_all_params(l2)
updates = lasagne.updates.sgd(loss, all_params, learning_rate=1)
train = theano.function([l0.input_var, true_output], loss, updates=updates)

get_output = theano.function([l0.input_var], net_output)

for n in xrange(100):
 train(X, y)

这段代码利用theano功能来训练我们的示例网络,使用我们的loss功能来迭代训练给定的一组输入数据。

张量流简介

当我们回顾谷歌在第四章卷积神经网络中对 卷积神经网络 ( CNN )的看法时,我们发现了一个令人费解的多层野兽。如何创建和监控此类网络的问题变得越来越重要,因为网络在层数和复杂性上不断扩展,以应对更复杂的挑战。

为了应对这一挑战,谷歌的机器智能研究组织开发并分发了一个名为 TensorFlow 的库,该库的存在是为了能够更容易地细化和建模非常复杂的机器学习模型。

TensorFlow 通过提供两个主要好处来做到这一点;一个清晰简单的编程接口(在本例中是一个 Python API)到熟悉的结构上(例如 NumPy 对象),以及强大的诊断和图形可视化工具,例如 TensorBoard ,以实现对数据架构的明智调整。

了解张量流

TensorFlow 使数据科学家能够将数据转换操作设计为跨计算图的流。该图可以扩展和修改,同时可以广泛调整单个节点,从而实现对单个层或模型组件的详细细化。TensorFlow 工作流通常包括两个阶段。其中的第一个阶段被称为构建阶段,在此期间组装图表。

在构建阶段,我们可以使用针对 Tensorflow 的 Python API 编写代码。像 Lasagne 一样,TensorFlow 提供了一个相对简单的界面来编写网络层,只需要我们在创建层之前指定权重和偏差。以下示例显示了在创建(使用一行代码)卷积层和简单的最大池层之前,权重和偏差变量的初始设置。此外,我们使用tf.placeholder为输入数据生成占位符变量。

x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])

W = tf.Variable(tf.zeros([5, 5, 1, 32]))
b = tf.Variable(tf.zeros([32]))

h_conv = tf.nn.relu(conv2d(x_image, W) + b)
h_pool = max_pool_2x2(h_conv)

这个结构可以扩展到包括一个softmax输出层,就像我们对千层面所做的那样。

W_out = tf.Variable(tf.zeros([1024,10]))
B_out = tf.Variable(tf.zeros([10]))

y = tf.nn.softmax(tf.matmul(h_conv, W_out) + b_out)

同样,我们可以看到在迭代时间上比直接在 antao 和 Python 库中编写有显著的改进。由于是用 C++编写的,TensorFlow 还提供了优于 Python 的性能提升,在执行时间上具有优势。

接下来,我们需要训练和评估我们的模型。在这里,我们需要写一点代码来定义我们用于训练的loss函数(在这种情况下是交叉熵),用于验证的accuracy函数和优化方法(在这种情况下是最陡梯度下降)。

cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))

train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)

correct_prediction = tf.equal(tf.argmax(y_,1), tf.argmax(y_,1))

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

接下来,我们可以简单地开始迭代运行我们的模型。这一切简洁明了:

sess.run(tf.initialize_all_variables())
for i in range(20000):
  batch = mnist.train.next_batch(50)
  if i%100 == 0:
    train_accuracy = accuracy.eval(feed_dict={
        x:batch[0], y_: batch[1], keep_prob: 1.0})
    print("step %d, training accuracy %g"%(i, train_accuracy))
  train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

print("test accuracy %g"%accuracy.eval(feed_dict={
    x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))

使用张量流迭代改进我们的模型

甚至从前面部分的单个例子中,我们应该能够认识到张量流给表带来了什么。它为开发复杂的体系结构和训练方法的任务提供了一个简单的界面,使我们更容易访问我们在本书前面学习过的算法。

然而,正如我们所知,开发初始模型只是模型开发过程的一小部分。我们通常需要反复测试和剖析我们的模型,以提高它们的性能。然而,这往往是一个领域,我们的工具在单一的库或技术中不太统一,测试和监控解决方案在不同的模型中不太一致。

TensorFlow 致力于解决如何在迭代过程中更好地洞察我们的模型的问题,即所谓的模型开发的执行阶段。在执行阶段,我们可以利用 TensorFlow 团队提供的工具来探索和改进我们的模型。

也许这些工具中最重要的是 TensorBoard,它为我们构建的模型提供了一个可探索的可视化表示。TensorBoard 提供了几种功能,包括显示基本模型信息(包括测试和/或培训的每次迭代期间的性能测量)的仪表板。

Using TensorFlow to iteratively improve our models

此外,TensorBoard 仪表板提供了较低层次的信息,包括每个模型层的权重、偏差和激活值的值范围图;迭代过程中非常有用的诊断信息。访问这些数据的过程非常简单,而且非常有用。

Using TensorFlow to iteratively improve our models

除此之外,张量积提供了给定模型的张量流的详细图形。张量是一个 n 维的数据数组(在这种情况下,由 n 个特征组成);当我们使用术语输入数据集时,我们往往会想到这一点。应用于张量的一系列运算被称为张量流,在张量流中,这是一个基本概念,原因很简单,也很有说服力。当细化和调试机器学习模型时,重要的是即使在很低的级别上也要有关于模型及其操作的信息。

Using TensorFlow to iteratively improve our models

TensorBoard 图以可变的细节显示了模型的结构。从这个初始视图中,可以深入到模型的每个组件和连续的子元素中。在这种情况下,我们能够查看在第二个网络层的丢弃功能中发生的具体操作。我们可以看到发生了什么,并确定下一次迭代需要调整什么。

这种级别的透明度是不寻常的,当我们想要调整模型组件时,尤其是当模型元素或层表现不佳时(例如,从显示层元参数值的 TensorBoard 图或从整体网络性能来看),这种透明度非常有帮助。

可以从事件日志中创建 TensorBoards,并在运行 TensorFlow 时生成。这使得在使用 TensorFlow 的日常开发过程中,很容易获得 TensorBoards 的好处。

Using TensorFlow to iteratively improve our models

截至 2016 年 4 月下旬,DeepMind 团队加入了谷歌大脑团队和其他一系列研究人员和开发人员使用 TensorFlow。通过让 TensorFlow 开源并免费提供,谷歌承诺继续支持 TensorFlow,将其作为模型开发和完善的强大工具。

知道何时使用这些库

在这一章的一两点,我们可能遇到了的问题好吧,那么,你为什么不一开始就教我们这个库呢?当本章呈现出让生活变得更轻松的完美界面时,公平地问一下我们为什么要花时间在蚂蚁函数和其他低级信息中挖掘。

自然,我主张使用现有的最佳工具,尤其是对于原型任务,在这些任务中,工作的价值更多的是理解你所处的大致范围,或者识别特定的问题类别。值得一提的是,在本书的前面,没有使用这两个库展示内容的三个原因。

第一个原因是这些工具只能让你走这么远。他们可以做很多事情,同意,所以根据领域和该领域的问题的性质,一些数据科学家可能能够依靠他们来满足大多数深度学习需求。当然,除了一定程度的性能和问题复杂性之外,您还需要了解在 antio 中构建模型需要什么,从头开始创建自己的评分函数,或者利用本书中描述的其他技术。

决定关注教学底层实现的另一部分是关于相关技术的发展成熟度。在这一点上,千层面和 TensorFlow 绝对值得大家讨论和推荐。在此之前,当这本书的大部分内容被写出来时,在本章中讨论图书馆的风险更大。基于 antio 的项目很多(本章没有讨论的一些更突出的框架有 KerasBlocks 和 T7】派尔恩 2

即使是现在,不同的库和工具完全有可能在一两年后成为讨论的主题或默认的工作环境。这一领域的发展速度极快,这主要是由于关键公司和研究团体的影响,他们不得不随着旧工具达到其有用的极限而不断构建新工具……或者只是变得清楚如何做得更好。

老实说,从更低的层面挖掘的另一个原因是,这是一本复杂的书。它把理论和代码放在一起,用代码来教授理论。抽象出算法是如何工作的,并简单地讨论如何应用它们来破解一个特定的例子可能很有诱惑力。本章中讨论的工具使实践者能够在一些问题上获得非常好的分数,而不必理解被调用的函数。我的观点是,这不是一个很好的培养数据科学家的方法。

如果你要处理微妙而困难的数据问题,你需要能够修改和定义自己的算法。你需要了解如何选择合适的解决方案。要做这些事情,你需要本书提供的细节,甚至更具体的信息,由于(页面)空间和时间的限制,我没有提供。在这一点上,你可以灵活而有知识地应用深度学习算法。

同样,认识到这些工具做得好或不好也很重要。目前,Lasagne 非常适合这个用例,在这个用例中,一个新的模型正在被开发用于基准测试或者早期通过,优先考虑的应该是迭代速度和获得结果。

与此同时,TensorFlow 适合模型的后期开发寿命。当轻松的收益消失,需要花费大量时间调试和改进模型时,TensorFlow 相对快速的迭代是一个明显的优势,但 TensorBoard 提供的诊断工具带来了巨大的附加值。

因此,这两个库在您的工具集中都有一席之地。根据手头问题的性质,这些库和更多库将被证明是有价值的资产。

进一步阅读

千层面用户指南非常全面,值得一读。在http://lasagne.readthedocs.io/en/latest/index.html找到。

同样,可以在https://www . TensorFlow . org/versions/r 0 . 9/get _ start/index . html找到 TensorFlow 教程。

总结

在这最后一章中,我们离开了之前关于算法、配置和诊断的讨论,转而考虑在实现深度学习算法时改善我们体验的工具。

我们发现了使用千层面的优势,千层面是一个与人工智能的接口,旨在加速和简化我们模型的早期原型。同时,我们检查了 TensorFlow,这是谷歌开发的帮助深度学习模型调整和优化的库。TensorFlow 以最少的努力为我们提供了模型性能的显著可见性,并使诊断和调试复杂的深层模型结构的任务不那么具有挑战性。

这两种工具在我们的过程中都有自己的位置,每一种都适合于一组特定的问题。

在这本书的整个过程中,我们已经浏览并回顾了一系列先进的机器学习技术。我们从一个理解一些基本算法和概念的位置,到自信地使用一个非常流行、强大和受欢迎的工具集。

然而,除了技术之外,这本书试图教一个更进一步的概念,一个更难教和更难学的概念,但它支撑了机器学习的最佳表现。

机器学习领域发展非常迅速。这种速度在几乎每周发布在学术期刊或行业白皮书上的新的和改进的分数中显而易见。从 MNIST 这样的训练例子如何从被视为有意义的挑战迅速转变为 Iris 数据集的深度学习版本玩具问题就可以看出这一点。与此同时,该领域进入下一个重大挑战;CIFAR-10,CIFAR-100。

同时,磁场循环运动。像 Yann LeCun 这样的学者在 80 年代提出的概念正在复兴,因为计算架构和资源的增长使得它们的使用比大规模的真实数据更可行。为了最大限度地使用许多最新的技术,有必要理解几十年前定义的概念,这些概念本身是在更久以前定义的其他概念的基础上定义的。

这本书试图平衡这些问题。了解前沿和现有技术至关重要;理解将定义新技术的概念或两三年后做出的调整同样重要。

然而,最重要的是,这本书让你了解了这些架构和方法的延展性。在数据科学实践的顶端一直看到的一个概念是,特定问题的最佳解决方案是特定问题的解决方案。

这就是为什么顶级的卡格尔竞赛获胜者会进行大量的特性准备并调整他们的架构。这就是为什么 TensorFlow 被写为允许清晰地看到一个人的架构的粒度属性。拥有熟练调整实现或组合算法的知识和技能是真正掌握机器学习技术的必要条件。

通过本书中回顾的许多技术和例子,我希望关于数据问题的思考方式以及对操作和配置这些算法的信心已经传递给了作为一名实践数据科学家的你。本书中许多推荐的进一步阅读的例子很大程度上是为了进一步扩展这些知识,并帮助你发展本书中教授的技能。

除此之外,我祝你在模型构建和配置方面一切顺利。我希望你能亲身体会到这个领域是多么令人愉快和值得!

十、附录 a:章节代码要求

这本书的内容利用了公开可用的数据和代码,包括开源 Python 库和框架。虽然每一章的示例代码都附有一个README文件,该文件记录了运行该章随附的脚本中提供的代码所需的所有库,但为了您的方便,这些文件的内容在此进行了整理。

建议您在使用后面章节中的代码时,已经拥有前面章节所需的一些库。这些需求是使用关键字来识别的。对于本书后面提供的任何内容,设置第 1 章无监督机器学习中提到的库尤为重要。下表给出了每一章的要求:

|

章节号

|

要求

|
| --- | --- |
| one | * Python 3(推荐 3.4)* 硬化(NumPy,scipy)* 【matplot lib】 |
| 2-4 |

  • 【theano】

|
| five |

  • Half-complement-study

|
| six |

  • Natural Language Toolkit ( NLTK )
  • Beautify group

|
| seven |

  • Twitter account

|
| eight |

  • 【xgboost】

|
| nine |

|

posted @ 2025-09-03 10:18  绝不原创的飞龙  阅读(66)  评论(0)    收藏  举报