OpenCV4-机器学习-全-

OpenCV4 机器学习(全)

原文:annas-archive.org/md5/0c2af040fc5817ca47a36b96d3af6e5d

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

随着世界的改变和人类建造更智能、更好的机器,对机器学习和计算机视觉专家的需求增加。正如其名所示,机器学习是机器学习到根据一组输入参数进行预测的过程。另一方面,计算机视觉给机器赋予视觉;也就是说,它使机器意识到视觉信息。当你结合这些技术时,你得到一个可以使用视觉数据进行预测的机器,这使机器在拥有人类能力方面又迈出一步。当你加入深度学习,机器甚至可以在预测方面超越人类的能力。这听起来可能有些牵强,但随着基于 AI 的系统接管决策系统,这实际上已经成为现实。你有 AI 摄像头、AI 显示器、AI 声音系统、AI 驱动的处理器等等。我们无法保证你阅读这本书后能够构建一个 AI 摄像头,但我们确实打算提供你完成这一目标的必要工具。我们将要介绍的最强大的工具是 OpenCV 库,这是世界上最大的计算机视觉库。尽管它在机器学习中的应用并不常见,但我们提供了一些示例和概念,说明它可以如何用于机器学习。我们在本书中采用了实践方法,并建议你尝试本书中出现的每一行代码,以构建一个展示你知识的应用程序。世界正在改变,这本书是我们帮助年轻思想将其变得更好的方式。

这本书面向谁

我们试图从零开始解释所有概念,使这本书既适合初学者也适合高级读者。我们建议读者具备一些 Python 编程的基本知识,但这不是强制性的。无论何时你遇到一些你无法理解的 Python 语法,确保你在互联网上查找它。对于那些寻求帮助的人来说,总是有提供的帮助。

为了最大限度地利用这本书

如果你是一个 Python 的初学者,我们建议你阅读任何好的 Python 编程书籍或在线教程或视频。你也可以查看 DataCamp (www.datacamp.com),使用交互式课程学习 Python。

我们还建议你学习一些关于 Python 中的 Matplotlib 库的基本概念。你可以尝试这个教程:www.datacamp.com/community/tutorials/matplotlib-tutorial-python

在开始这本书之前,你不需要在你的系统上安装任何东西。我们将在第一章中涵盖所有安装步骤。

下载示例代码文件

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

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

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

  2. 选择“支持”标签。

  3. 点击“代码下载”。

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

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

  • 适用于 Windows 的 WinRAR/7-Zip

  • 适用于 Mac 的 Zipeg/iZip/UnRarX

  • 适用于 Linux 的 7-Zip/PeaZip

本书代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition ...

下载彩色图像

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

使用的约定

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

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“我们可以将max_samples<1.0max_features<1.0都设置为实施随机补丁方法。”

代码块设置如下:

In [1]: from sklearn.ensemble import BaggingClassifier... from sklearn.neighbors import KNeighborsClassifier... bag_knn = BaggingClassifier(KNeighborsClassifier(),... n_estimators=10)

任何命令行输入或输出都应如下所示:

$ conda install package_name

粗体:表示新术语、重要单词或您在屏幕上看到的单词。

警告 ...

联系我们

我们欢迎读者的反馈。

总体反馈:如果您对本书的任何方面有任何疑问,请在邮件主题中提及书名,并将邮件发送至 customercare@packtpub.com

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

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

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

评论

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

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

第一部分:机器学习与 OpenCV 的基础

在本书的第一部分,我们将介绍机器学习与 OpenCV 的基础知识,从安装所需的库开始,然后继续到基本的 OpenCV 函数,监督学习的基础及其应用,最后是使用 OpenCV 进行特征检测和识别。

本节包括以下章节:

  • 第一章,机器学习的初体验

  • 第二章,在 OpenCV 中使用数据

  • 第三章,监督学习的第一步

  • 第四章,数据表示与特征工程

第一章:机器学习初体验

因此,您已经决定进入机器学习领域。这太棒了!

现在,机器学习无处不在——从保护我们的电子邮件,到自动在照片中标记我们的朋友,再到预测我们喜欢什么电影。作为一种人工智能形式,机器学习使计算机能够通过经验学习;使用过去收集的数据对未来做出预测。此外,计算机视觉是当今机器学习最令人兴奋的应用领域之一,深度学习和卷积神经网络推动了自动驾驶汽车和谷歌 DeepMind 等创新系统的发展。

然而,不必担心;您提供的应用程序不需要如此大规模或改变世界 ...

技术要求

您可以参考以下链接中本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter01

以下是软件和硬件要求的简要总结:

  • OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都可以正常工作)。

  • Python 版本 3.6(任何 3.x 版本的 Python 都可以)。

  • Anaconda Python 3 用于安装 Python 和所需的模块。

  • 您可以使用任何操作系统——macOS、Windows 和基于 Linux 的操作系统——使用这本书。我们建议您的系统至少有 4 GB 的 RAM。

  • 您不需要 GPU 就可以运行本书提供的代码。

开始学习机器学习

机器学习已经存在至少 60 年了。它起源于对人工智能的追求,早期的机器学习系统通过推断手编的 if...else 规则来处理数据和做出决策。想象一下一个垃圾邮件过滤器,其任务是解析传入的电子邮件并将不需要的消息移动到垃圾邮件文件夹,如下面的图中所示:

图片

我们可以制定一个黑名单,其中包含一些单词,每当它们出现在消息中时,就会将电子邮件标记为垃圾邮件。这是一个简单的手编专家系统示例。(我们将在第七章实现垃圾邮件...中构建一个更智能的系统。)

机器学习可以解决的问题

大多数机器学习问题属于以下三个主要类别之一:

  • 在监督学习中,我们有一个被称为数据点标签的东西。现在,这可以是图像中捕获的对象的类别,围绕脸部的一个边界框,图像中的数字,或者任何其他东西。想象一下,这是一个既教书又告诉你问题正确答案的老师。现在,学生可以尝试设计一个模型或方程,它考虑了所有问题和它们的正确答案,并找出有(或没有)正确答案的问题的答案。用于学习模型的输入数据称为训练数据,而用于测试过程/模型的输入数据称为测试数据。这些预测有两种类型,比如识别带有正确动物的新照片(称为分类问题)或为其他二手车分配准确的售价(称为回归问题)。现在不要担心这看起来有点超出你的理解——我们将在整本书中详细阐述。

  • 在无监督学习中,数据点没有与之关联的标签(第八章,使用无监督学习发现隐藏结构)。想象一下,就像一个课堂上,讲师给你一个乱糟糟的拼图,然后让你自己想办法解决问题。在这里,最常见的结果是聚类,它包含具有相似特性的对象。它还可以以不同的方式查看高维数据(复杂数据),使其看起来更简单。

  • 强化学习是关于在问题中最大化奖励。所以,如果老师给你糖果作为每个正确答案的奖励,并惩罚每个错误答案,他/她通过让你增加获得糖果的次数而不是受到惩罚的次数来强化概念。

以下图表展示了这三个主要类别:

图片

现在我们已经涵盖了主要机器学习类别,让我们来看看一些在本书的旅程中非常有用的 Python 概念。

开始使用 Python

由于 Python 拥有大量用于数据加载、数据可视化、统计、图像处理和自然语言处理等过程的开源库,它已成为许多数据科学和机器学习应用的通用语言。使用 Python 的主要优势是能够直接与代码交互,使用终端或其他工具,如Jupyter Notebook,我们将在稍后探讨。

如果你主要使用 C++与 OpenCV 结合,我强烈建议你切换到 Python,至少为了学习这本书的目的。这个决定并不是出于恶意!恰恰相反:我做了我应得的 C/C++编程——特别是...

开始使用 OpenCV

既然我相信您是 OpenCV 的热情用户,我可能不需要说服您 OpenCV 的强大功能。

OpenCV 是为了为计算机视觉应用提供通用基础设施而构建的,已经成为了一套既经典又最先进的计算机视觉和机器学习算法的综合集。根据其自己的文档,OpenCV 拥有超过 47,000 人的用户社区,下载量超过七百万次。这相当令人印象深刻!作为一个开源项目,研究人员、企业和政府部门很容易利用和修改现有的代码。

话虽如此,随着最近机器学习热潮的兴起,许多开源机器学习库作为其中的一部分出现,它们提供了比 OpenCV 更多的功能。一个突出的例子是scikit-learn,它提供了一系列最先进的机器学习算法以及丰富的在线教程和代码片段。由于 OpenCV 主要是为了提供计算机视觉算法而开发的,它的机器学习功能仅限于一个名为 ml 的单个模块。正如我们将在本书中看到的那样,OpenCV 仍然提供了一些最先进的算法,但有时在功能上略有不足。在这些罕见的情况下,我们不会重新发明轮子,而会简单地使用 scikit-learn 来满足我们的需求。

最后但同样重要的是,使用 Python Anaconda 发行版安装 OpenCV 实际上就像我们在以下章节中将看到的那样,只需要一行命令。

如果您是希望构建实时应用的更高级用户,OpenCV 的算法对此任务进行了很好的优化,Python 提供了多种方法来加快必要的计算速度(例如,使用 Cython 或并行处理库如 joblib 或 dask)。

安装

在我们开始之前,让我们确保我们已经安装了所有必要的工具和库,以创建一个完全功能的数据科学环境。在从 GitHub 下载本书的最新代码后,我们将安装以下软件:

  • 基于 Python 3.6 或更高版本的 Python Anaconda 发行版

  • OpenCV 4.1

  • 一些支持性包

不想安装任何东西?您也可以访问 mybinder.org/v2/gh/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/master,在那里您将找到一个交互式、可执行的环境中的所有本书代码,并且 100%免费和开源,这要归功于Binder项目。

获取本书的最新代码

您可以从 GitHub 获取本书的最新代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition。您可以选择下载一个 .zip 包(适用于初学者)或使用 Git 克隆仓库(适用于中级用户)。

Git 是一个版本控制系统,它允许您跟踪文件中的更改,并与他人协作编写代码。此外,网络平台 GitHub 使得人们可以在公共服务器上轻松地将他们的代码与您共享。随着我对代码的改进,您可以轻松地更新您的本地副本,提交错误报告或建议代码更改。

如果您选择使用 git,第一步是确保它已安装(git-scm.com/downloads)。

然后,打开终端(或在 Windows 上称为命令提示符):

  • 在 Windows 10 上,右键单击开始菜单按钮,然后选择命令提示符。

  • 在 macOS X 上,按 Cmd + Space 打开 spotlight 搜索,然后输入 terminal,然后按 Enter

  • 在 Ubuntu、Linux/Unix 和其他系统上,按 Ctrl + Alt + T。在 Red Hat 上,右键单击桌面,然后从菜单中选择打开终端。

导航到您想要下载代码的目录:

cd Desktop

然后,您可以通过输入以下命令来获取最新代码的本地副本:

git clone https://github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition.git

这将在名为 OpenCV-ML 的文件夹中下载最新的代码。

一段时间后,代码可能会在线上更改。在这种情况下,您可以通过在 OpenCV-ML 目录中运行以下命令来更新您的本地副本:

git pull origin master

熟悉 Python 的 Anaconda 发行版

Anaconda 是由 Continuum Analytics 开发的一个免费的 Python 发行版,专为科学计算而设计。它适用于 Windows、Linux 和 macOS X 平台,并且免费,即使是商业用途。然而,最好的是它附带了一些预安装的包,这些包对于数据科学、数学和工程至关重要。这些包包括以下内容:

  • NumPy:Python 中科学计算的基本包,提供了多维数组、高级数学函数和伪随机数生成器的功能

  • SciPy:Python 中用于科学计算的函数集合,提供了高级线性代数例程,...

在 conda 环境中安装 OpenCV

我们将执行以下步骤来安装 OpenCV:

  1. 在终端中,导航到您下载以下代码的目录:
$ cd Desktop/OpenCV-ML
  1. 然后,运行以下命令以创建一个基于 Python 3.6 的 conda 环境,这将一次性安装 environment.yml 文件中列出的所有必要包(该文件可在 GitHub 仓库中找到):
$ conda create env -f environment.yml
  1. 您还可以查看以下 environment.yml 文件:
name: OpenCV-ML
channels:
  - conda-forge
dependencies:
  - python==3.6
  - numpy==1.15.4
  - scipy==1.1.0
  - scikit-learn==0.20.1
  - matplotlib
  - jupyter==1.0
  - notebook==5.7.4
  - pandas==0.23.4
  - theano
  - keras==2.2.4
  - mkl-service==1.1.2
  - pip
  - pip:
    - opencv-contrib-python==4.1.0.25

注意,环境的名称将是 OpenCV-ML。此代码将使用 conda-forge 通道下载所有基于 conda 的依赖项,并使用 pip 安装 OpenCV 4.0(包括 opencv_contrib)

  1. 要激活环境,请根据您的平台输入以下命令之一:
$ source activate OpenCV-ML  # on Linux / Mac OS X
$ activate OpenCV-ML         # on Windows
  1. 当我们关闭终端时,会话将被注销——因此,下次我们打开新的终端时,我们必须再次运行此最后一个命令。我们也可以手动注销环境:
$ source deactivate  # on Linux / Mac OS X
$ deactivate         # on Windows

完成了!让我们验证所有这些安装是否成功。

验证安装

仔细检查我们的安装是个好主意。当我们的终端仍然打开时,我们开始运行 IPython,这是一个用于运行 Python 命令的交互式外壳:

$ ipython

接下来,确保你正在运行(至少)Python 3.6 而不是 Python 2.7。你可能会在 IPython 的欢迎信息中看到版本号。如果没有,你可以运行以下命令:

In [1]: import sys...     print(sys.version)        3.6.0 | packaged by conda-forge | (default, Feb 9 2017, 14:36:55) [GCC 4.8.2 20140120 (Red Hat 4.8.2-15)]

现在尝试以下方式导入 OpenCV:

In [2]: import cv2

你不应该收到任何错误信息。然后,尝试找出版本号,如下所示:

In [3]: cv2.__version__Out[3]: '4.0.0'

确保 OpenCV 的版本...

一窥 OpenCV 的 ml 模块

从 OpenCV 3.1 版本开始,所有与机器学习相关的函数在 OpenCV 中都被归类到了ml模块。对于 C++ API 来说,这种情况已经持续了一段时间。你可以通过显示ml模块中的所有函数来一窥未来:

In [4]: dir(cv2.ml)
Out[4]: ['ANN_MLP_ANNEAL',
 'ANN_MLP_BACKPROP',
 'ANN_MLP_GAUSSIAN',
 'ANN_MLP_IDENTITY',
 'ANN_MLP_LEAKYRELU',
 'ANN_MLP_NO_INPUT_SCALE',
 'ANN_MLP_NO_OUTPUT_SCALE',
 ...
 '__spec__']

如果你安装了较旧的 OpenCV 版本,ml模块可能不存在。例如,k 最近邻算法(我们将在第三章,监督学习的第一步)曾经被称为cv2.KNearest(),但现在被称为cv2.ml.KNearest_create()。为了避免在整个书中产生混淆,我建议使用 OpenCV 4.0。

这一切看起来都很好,但你现在可能想知道为什么你应该学习机器学习,以及它的应用是什么?让我们在下一节回答这个问题。

机器学习应用

机器学习、人工智能、深度学习和数据科学是四个我认为将会改变我们看待事物方式的术语。让我们看看我能否说服你为什么我相信这一点。

从让计算机学习如何玩围棋并击败该游戏的全球冠军,到仅通过查看他们的脑部 CT 扫描来检测一个人是否有肿瘤,机器学习在每一个领域都留下了它的印记。我参与的一个项目是使用机器学习来确定热电厂锅炉水冷壁管的剩余使用寿命。所提出的解决方案通过使用...成功节省了大量资金。

OpenCV 4.0 的新特性有哪些?

因此,我们来到了第一章的最后一节。我会尽量简短并直截了当,因为作为读者,你可以安全地跳过这一部分。我们讨论的主题是OpenCV 4.0

OpenCV 4.0 是 OpenCV 团队经过三年半的辛勤工作和错误修复的结果,并于 2018 年 11 月最终发布。在本节中,我们将探讨 OpenCV 4.0 的一些主要变化和新特性:

  • 随着 OpenCV 4.0 的发布,OpenCV 正式成为了一个 C++11 库。这意味着当你尝试编译 OpenCV 4.0 时,你必须确保你的系统中有一个符合 C++11 的编译器。

  • 在上一点的延续中,许多 C API 已被移除。受影响的模块包括视频 IO 模块(videoio)、目标检测模块(objdetect)等。XML、YAML 和 JSON 的文件 IO 也移除了 C API。

  • OpenCV 4.0 在 DNN 模块(深度学习模块)方面也有很多改进。ONNX 支持已被添加。Intel OpenVINO也在新的 OpenCV 版本中留下了它的足迹。我们将在后面的章节中更详细地探讨这一点。

  • OpenCL 加速已经在 AMD 和 NVIDIA GPU 上得到修复。

  • OpenCV Graph API 也已添加,这是一个用于图像处理和其他操作的高效引擎。

  • 正如每次 OpenCV 发布一样,有很多变化都是为了提高性能。一些新功能,如 QR 码检测和解码,也被添加进来。

简而言之,OpenCV 4.0 中有很多变化,它们都有各自的用途。例如,ONNX 支持有助于模型在各种语言和框架之间的可移植性,OpenCL 减少了计算机视觉应用的运行时间,Graph API 有助于提高应用的效率,而 OpenVINO 工具包利用 Intel 处理器和模型库来提供高效的深度学习模型。我们将在后面的章节中主要关注 OpenVINO 工具包和 DLDT,以及加速计算机视觉应用。但是,我也应该在这里指出,OpenCV 3.4.4 和 OpenCV 4.0.0 都在以高速修改以修复错误。所以,如果你打算在任何应用中使用它们中的任何一个,请准备好修改你的代码和安装以包含所做的更改。同样,OpenCV 4.0.1 和 OpenCV 3.4.5 也在其前辈发布后的几个月内推出。

摘要

在本章中,我们从高抽象层次讨论了机器学习:它是什么,为什么它很重要,以及它可以解决哪些类型的问题。我们了解到机器学习问题分为三种类型:监督学习、无监督学习和强化学习。我们讨论了监督学习的突出地位,以及这个领域可以进一步分为两个子领域:分类和回归。分类模型使我们能够将对象分类到已知的类别中(例如,将动物分类为猫和狗),而回归分析可以用来预测目标变量的连续结果(例如,二手车的销售价格)。

我们还学习了如何使用...来设置数据科学环境。

第二章:在 OpenCV 中处理数据

现在我们已经激发了大家对机器学习的兴趣,是时候深入探讨构成典型机器学习系统的不同部分了。

太频繁了,你可能会听到有人随意抛出“Just apply machine learning to your data!”这样的短语,好像这样就能立刻解决你所有的问题。你可以想象,现实远比这复杂得多,尽管,我必须承认,如今,仅通过从互联网上复制粘贴几行代码,就极其容易构建自己的机器学习系统。然而,要构建一个真正强大和有效的系统,掌握底层概念以及深入了解每种方法的优缺点是至关重要的。所以,如果你还没有把自己视为机器学习专家,请不要担心。好事多磨。

之前,我把机器学习描述为人工智能的一个子领域。这可能确实如此——主要是出于历史原因——但大多数情况下,机器学习只是关于理解数据。因此,将机器学习视为数据科学的一个子领域可能更为合适,在那里我们构建数学模型来帮助我们理解数据。

因此,本章全部关于数据。我们想要了解数据如何与机器学习相结合,以及如何使用我们选择的工具(OpenCV 和 Python)来处理数据。

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

  • 理解机器学习工作流程

  • 理解训练数据和测试数据

  • 学习如何使用 OpenCV 和 Python 加载、存储、编辑和可视化数据

技术要求

你可以从以下链接获取本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter02

下面是软件和硬件要求的总结:

  • 你需要 OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都完全可以)。

  • 你需要 Python 版本 3.6(任何 3.x 版本的 Python 都行)。

  • 你需要安装 Anaconda Python 3 来安装 Python 和所需的模块。

  • 你可以使用任何操作系统——macOS、Windows 以及基于 Linux 的操作系统——配合本书使用。我们建议你的系统至少有 4GB 的 RAM。

  • 运行本书提供的代码不需要 GPU。

理解机器学习工作流程

如前所述,机器学习就是构建数学模型来理解数据。当我们赋予机器学习模型调整其内部参数的能力时,学习过程就进入了这个环节;我们可以调整这些参数,使模型更好地解释数据。从某种意义上说,这可以理解为模型从数据中学习。一旦模型学习到足够多的东西——无论这意味着什么——我们就可以要求它解释新观察到的数据。

以下是一个典型的分类过程的示意图:

图片

让我们一步一步地分解它。

首先要注意的是,机器学习问题总是分为(至少)两个不同的阶段:

  • 一个训练阶段,在这个阶段,我们旨在使用我们称之为训练数据集的一组数据来训练机器学习模型

  • 一个测试阶段,在这个阶段,我们评估学习到的(或最终确定的)机器学习模型在称为测试数据集的一组从未见过的新数据上的表现

将我们的数据分为训练集和测试集的重要性不容忽视。我们总是在一个独立的测试集上评估我们的模型,因为我们感兴趣的是了解我们的模型如何泛化到新的数据。最终,这不就是学习的全部意义所在——无论是机器学习还是人类学习吗?回想一下学校的时候,当你自己还是一名学习者时:作为家庭作业一部分需要解决的问题在期末考试中绝不会以完全相同的形式出现。同样的严格性应该应用于机器学习模型;我们并不那么关心我们的模型能够多好地记住一组数据点(例如家庭作业问题),而是想知道我们的模型将如何利用他们所学的知识来解决新的问题(例如在期末考试中出现的问题)并解释新的数据点。

高级机器学习问题的工作流程通常包括第三组数据,称为验证数据集。目前,这种区别并不重要。验证集通常是通过进一步划分训练数据集形成的。当我们已经熟练构建机器学习系统时,我们将在第十一章选择合适的模型与超参数调整中讨论的模型选择等高级概念中使用它。

接下来要注意的是,机器学习实际上完全是关于数据的。数据以原始形式进入之前描述的工作流程图——无论这意味着什么——并在训练和测试阶段中使用。数据可以是图像、电影、文本文档或音频文件等任何东西。因此,在原始形式下,数据可能由像素、字母、单词甚至更糟糕的是:纯比特组成。很容易看出,这种原始形式的数据可能不太方便处理。相反,我们必须找到方法来预处理数据,使其以易于解析或使用的形式出现。

数据预处理分为两个阶段:

  • 特征选择:这是识别数据中重要属性(或特征)的过程。图像的可能特征可能包括边缘、角点或脊的位置。你可能已经熟悉 OpenCV 提供的一些更高级的特征描述符,如加速鲁棒特征(SURF)或方向梯度直方图(HOG)。尽管这些特征可以应用于任何图像,但它们可能对我们的特定任务并不那么重要(或效果并不好)。例如,如果我们的任务是区分干净和脏水,最重要的特征可能是水的颜色,而使用 SURF 或 HOG 特征可能对我们帮助不大。

  • 特征提取:这是将原始数据转换为所需特征空间的实际过程。一个例子是哈里斯算子,它允许我们在图像中提取角点(即选定的特征)。

一个更高级的话题是发明信息特征的过程,这被称为特征工程。毕竟,在人们可以从流行的特征中选择之前,必须先有人发明它们。这对于我们算法的成功往往比算法本身的选择更重要。我们将在第四章表示数据和特征工程中广泛讨论特征工程。

不要让命名规范混淆你!有时候,特征选择和特征提取难以区分,主要是因为命名方式。例如,SURF 既可以指特征提取器,也可以指实际的特征名称。同样,尺度不变特征变换(SIFT)也是如此,它是一种特征提取器,可以产生所谓的SIFT特征。不幸的是,这两个算法都获得了专利,不能用于商业目的。我们不会分享任何关于这两个算法的代码。

最后一点是,在监督学习中,每个数据点都必须有一个标签。标签可以识别数据点属于某个类别(如猫或狗)或具有某个值(如房屋的价格)。最终,监督机器学习系统的目标是预测测试集中所有数据点的标签(如前图所示)。我们通过学习训练数据中的规律性,使用随附的标签,然后在测试集上测试我们的性能来实现这一点。

因此,为了构建一个功能齐全的机器学习系统,我们首先必须了解如何加载、存储和处理数据。你如何在 OpenCV 中使用 Python 做到这一点呢?

使用 OpenCV 和 Python 处理数据

数据的世界充满了各种类型的数据。有时,这会让用户很难区分特定值应使用的数据类型。在这里,我们将尝试将其保持简单,将所有内容视为数组,除了标量值,它们将保留其标准数据类型。因此,图像将成为二维数组,因为它们有宽度和高度。一维数组可能是一个随时间强度变化的音频剪辑。

如果你主要使用 OpenCV 的 C++ 应用程序编程接口API)并且计划继续这样做,你可能会发现处理 C++ 中的数据可能会有些痛苦。你不仅必须处理语法开销,...

启动一个新的 IPython 或 Jupyter 会话

在我们能够接触到 NumPy 之前,我们需要打开一个 IPython 壳或启动一个 Jupyter 笔记本:

  1. 打开一个终端,就像我们在上一章中做的那样,并导航到 OpenCV-ML 目录:
 $ cd Desktop/OpenCV-ML
  1. 激活我们在上一章中创建的 conda 环境:
 $ source activate OpenCV-ML  # Mac OS X / Linux
 $ activate OpenCV-ML         # Windows
  1. 启动一个新的 IPython 或 Jupyter 会话:
 $ ipython           # for an IPython session
      $ jupyter notebook  # for a Jupyter session

如果你选择启动一个 IPython 会话,程序应该会以如下欢迎信息问候你:

$ ipython
Python 3.6.0 | packaged by conda-forge | (default, Feb 9 2017, 14:36:55) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.2.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: 

In [1] 开头的行是输入常规 Python 命令的地方。此外,在输入变量和函数名称时,你也可以使用 Tab 键让 IPython 自动完成。

有限数量的 Unix 和 macOS 系统外壳命令也可以使用——例如 lspwd。你可以通过在命令前加上 ! 来运行任何外壳命令,例如 !ping www.github.com。更多信息,请查看官方 IPython 参考文档:ipython.org/ipython-doc/3/interactive/tutorial.html

如果你选择启动一个 Jupyter 会话,你的网络浏览器应该会打开一个指向 http://localhost:8888 的新窗口。你想要通过点击右上角的“新建”并选择“Notebooks (Python 3)”来创建一个新的笔记本:

图片

这将打开一个看起来像这样的新窗口:

图片

标记为 In [ ] 的单元格(看起来像前面的文本框)与 IPython 会话中的命令行相同。现在你可以开始输入你的 Python 代码了!

使用 Python 的 NumPy 包处理数据

假设如果你已经安装了 Anaconda,那么你已经在虚拟环境中安装了 NumPy。如果你使用了 Python 的标准发行版或任何其他发行版,你可以访问 www.numpy.org 并遵循那里提供的安装说明。

如前所述,如果你还不是 Python 专家,这完全没问题。谁知道呢,也许你刚刚从 OpenCV 的 C++ API 转换过来。我只是想给你一个关于如何开始使用 NumPy 的快速概述。如果你是更高级的 Python 用户,你可以简单地跳过这一部分。

一旦你熟悉了 NumPy,你会发现 Python 世界中的大多数科学计算工具都是围绕 ...

导入 NumPy

一旦你开始一个新的 IPython 或 Jupyter 会话,你可以导入 NumPy 模块并验证其版本,如下所示:

In [1]: import numpy
In [2]: numpy.__version__
Out[2]: '1.15.4'

回想一下,在 Jupyter Notebook 中,一旦你输入了命令,你可以按 Ctrl + Enter 来执行一个单元格。或者,按 Shift + Enter 将执行单元格并自动插入或选择下面的单元格。通过点击 帮助 | 键盘快捷键 或点击 帮助 | 用户界面巡游 来查看所有键盘快捷键。

对于本节讨论的包的各个部分,我建议使用 NumPy 版本 1.8 或更高版本。按照惯例,你会发现科学 Python 世界中的大多数人都会使用 np 作为别名来导入 NumPy:

In [3]: import numpy as np
In [4]: np.__version__
Out[4]: '1.15.4'

在本章以及本书的其余部分,我们将坚持使用相同的约定。

理解 NumPy 数组

你可能已经 知道 Python 是一种 弱类型语言。这意味着你不必在创建新变量时指定数据类型。例如,以下内容将自动表示为整数:

In [5]: a = 5

你可以通过输入以下内容来再次确认:

In [6]: type(a)Out[6]: int

由于标准的 Python 实现是用 C 编写的,每个 Python 对象基本上都是伪装成 C 结构的。这在 Python 中的整数也是一样,实际上它们是指向包含不仅仅是原始整数值的复合 C 结构的指针。因此,用于表示 Python 整数的默认 C 数据类型将取决于你的系统架构(即,它是否是 32 位 ...

通过索引访问单个数组元素

如果你之前使用过 Python 的标准列表索引,那么你不会在 NumPy 的索引中遇到许多问题。在 1D 数组中,第 i 个值(从零开始计数)可以通过指定所需的索引来访问,就像 Python 列表一样:

In [13]: int_arr
Out[13]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [14]: int_arr[0]
Out[14]: 0
In [15]: int_arr[3]
Out[15]: 3

要从数组的末尾进行索引,你可以使用负索引:

In [16]: int_arr[-1]
Out[16]: 9
In [17]: int_arr[-2]
Out[17]: 8

对于切片数组还有一些其他酷炫的技巧,如下所示:

In [18]: int_arr[2:5]  # from index 2 up to index 5 - 1
Out[18]: array([2, 3, 4])
In [19]: int_arr[:5]    # from the beginning up to index 5 - 1
Out[19]: array([0, 1, 2, 3, 4])
In [20]: int_arr[5:]    # from index 5 up to the end of the array
Out[20]: array([5, 6, 7, 8, 9])
In [21]: int_arr[::2]   # every other element
Out[21]: array([0, 2, 4, 6, 8])
In [22]: int_arr[::-1]  # the entire array in reverse order
Out[22]: array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])

我鼓励你自己尝试操作这些数组!

NumPy 中切片数组的一般形式与标准 Python 列表中的相同。要访问数组的切片,使用 x[start:stop:step]。如果其中任何一个未指定,则默认为 start=0stop=维度大小step=1 值。

创建多维数组

数组不必局限于列表。实际上,它们可以有任意数量的维度。在机器学习中,我们通常会处理至少 2D 数组,其中列索引代表特定特征的值,而行包含实际的特征值。

使用 NumPy,从头创建多维数组很容易。假设我们想要创建一个有 3 行 5 列的数组,所有元素都初始化为零。如果我们没有指定数据类型,NumPy 将默认使用浮点数:

In [23]: arr_2d = np.zeros((3, 5))...      arr_2dOut[23]: array([[0., 0., 0., 0., 0.],                [0., 0., 0., 0., 0.],                [0., 0., 0., 0., 0.]])

如您从 OpenCV 时代可能知道的,这 ...

在 Python 中加载外部数据集

感谢 SciPy 社区,有许多资源可以帮助我们获取数据。

一个特别有用的资源来自 scikit-learnsklearn.datasets 包。这个包预先安装了一些小型数据集,我们不需要从外部网站下载任何文件。这些数据集包括以下内容:

  • load_boston:波士顿数据集包含波士顿不同郊区的房价,以及一些有趣的特征,如城镇人均犯罪率、住宅用地比例和非零售商业数量

  • load_iris:爱丽丝花数据集包含三种不同类型的爱丽丝花(塞托萨、变色和维吉尼卡),以及描述萼片和花瓣宽度和长度的四个特征

  • load_diabetes:糖尿病数据集让我们可以根据患者的年龄、性别、体重指数、平均血压和六项血液血清测量值来分类患者是否患有糖尿病

  • load_digits:数字数据集包含 0-9 数字 8 x 8 像素的图像

  • load_linnerud:Linnerud 数据集包含 3 个生理变量和 3 个锻炼变量,这些变量是在健身俱乐部对 20 名中年男性测量的

此外,scikit-learn 允许我们直接从外部存储库下载数据集,如下所示:

  • fetch_olivetti_faces:Olivetti 面部数据集包含 10 个不同图像,每个图像有 40 个不同的主题

  • fetch_20newsgroups:20 个新闻组数据集包含大约 18,000 个关于 20 个主题的新闻组帖子

更好的是,可以直接从机器学习数据库 openml.org 下载数据集。例如,要下载爱丽丝花数据集,只需输入以下命令:

In [1]: from sklearn import datasets
In [2]: iris = datasets.fetch_openml('iris', version=1)
In [3]: iris_data = iris['data']
In [4]: iris_target = iris['target']

爱丽丝花数据库包含总共 150 个样本,具有 4 个特征——花瓣长度、花瓣宽度、萼片长度和萼片宽度。数据分为三个类别——爱丽丝花塞托萨、爱丽丝花变色和爱丽丝花维吉尼卡。数据和标签分别存放在两个独立的容器中,我们可以如下检查:

In [5]: iris_data.shape 
Out[5]: (150, 4)
In [6]: iris_target.shape 
Out[6]: (150,)

在这里,我们可以看到 iris_data 包含 150 个样本,每个样本有 4 个特征(这就是为什么数字 4 出现在形状中)。标签存储在 iris_target 中,每个样本只有一个标签。

我们可以进一步检查所有目标值的值,但我们不想打印所有值。相反,我们感兴趣的是查看所有不同的目标值,这使用 NumPy 很容易做到:

In [7]: import numpy as np
In [8]: np.unique(iris_target) # Find all unique elements in array
Out[8]: array(['Iris-setosa', 'Iris-versicolor', 'Iris-virginica'], dtype=object)

另一个你应该听说过的 Python 数据分析库是 pandas (pandas.pydata.org)。pandas 实现了数据库和电子表格的几个强大的数据操作。尽管这个库很强大,但就我们的目的而言,pandas 现在可能有点过于高级了。

使用 Matplotlib 可视化数据

如果我们不知道如何查看数据,那么知道如何加载数据就有限的价值。幸运的是,有Matplotlib

Matplotlib 是一个基于 NumPy 数组的跨平台数据可视化库——看,我承诺 NumPy 会再次出现。它由 John Hunter 在 2002 年构思,最初设计为 IPython 的一个补丁,用于从命令行启用交互式 MATLAB 风格的绘图。在最近几年,出现了更多更炫酷的工具来最终取代 Matplotlib(例如 R 语言中的ggplotggvis),但 Matplotlib 仍然是一个经过良好测试的跨平台图形引擎。

导入 Matplotlib

你可能又走运了:如果你遵循了上一章中概述的建议并安装了 Python Anaconda 堆栈,那么你已经有 Matplotlib 安装好了,可以开始使用了。否则,你可能需要访问matplotlib.org以获取安装说明。

正如我们使用np简写来表示 NumPy 一样,我们将为 Matplotlib 导入使用一些标准简写:

In [1]: import matplotlib as mpl
In [2]: import matplotlib.pyplot as plt

plt 接口是我们将最常使用的,正如我们将在整本书中看到的那样。

生成一个简单的图形

不再拖延,让我们创建我们的第一个图形。

假设我们想要生成一个简单的正弦函数sin(x)的线形图。我们希望函数在x轴上所有0 < x < 10的点上进行评估。我们将使用 NumPy 的linspace函数在x轴上创建线性间距,从x010,总共100个采样点:

In [3]: import numpy as npIn [4]: x = np.linspace(0, 10, 100)

我们可以使用 NumPy 的sin函数在所有点x上评估sin函数,并通过调用pltplot函数来可视化结果:

In [5]: plt.plot(x, np.sin(x))

你自己试了吗?发生了什么?有什么显示出来吗?

问题是,根据你运行此脚本的位置,你可能不会 ...

可视化外部数据集的数据

作为本章的最终测试,让我们可视化来自外部数据集的一些数据,例如 scikit-learn 中的digits数据集。

具体来说,我们需要三个工具来进行可视化:

  • scikit-learn 用于实际数据

  • NumPy 用于数据处理

  • Matplotlib

因此,让我们首先导入所有这些:

In [1]: import numpy as np
...     from sklearn import datasets
...     import matplotlib.pyplot as plt
...     %matplotlib inline

第一步是实际加载数据:

In [2]: digits = datasets.load_digits()

如果我们记得正确的话,digits应该有两个不同的字段:一个包含实际图像数据的data字段和一个包含图像标签的target字段。与其依赖我们的记忆,我们不如简单地调查digits对象。我们通过键入其名称,添加一个点,然后按Tab键来完成:digits.<TAB>。这将揭示digits对象还包含一些其他字段,例如一个名为images的字段。这两个字段imagesdata似乎只是在形状上有所不同:

In [3]: print(digits.data.shape)
... print(digits.images.shape)
Out[3]: (1797, 64)
 (1797, 8, 8)

在这两种情况下,第一个维度对应于数据集中图像的数量。然而,data将所有像素排列在一个大向量中,而images则保留了每个图像的 8 x 8 空间排列。

因此,如果我们想绘制单个图像,images 字段将更为合适。首先,我们使用 NumPy 的数组切片从数据集中获取单个图像:

In [4]: img = digits.images[0, :, :]

在这里,我们表示要从 1,797 项长数组中抓取第一行以及所有相应的 8 x 8 = 64 像素。然后我们可以使用 pltimshow 函数来绘制图像:

In [5]: plt.imshow(img, cmap='gray') 
...     plt.savefig('figures/02.04-digit0.png') 
Out[5]: <matplotlib.image.AxesImage at 0x7efcd27f30f0>

前一个命令给出了以下输出。请注意,由于我们将图像调整到了更大的尺寸,所以图像是模糊的。原始图像的大小仅为 8 x 8:

此外,我还指定了一个使用 cmap 参数的颜色映射。默认情况下,Matplotlib 使用 MATLAB 的默认颜色映射 jet。然而,对于灰度图像,gray 颜色映射更有意义。

最后,我们可以使用 pltsubplot 函数绘制大量数字样本。subplot 函数与 MATLAB 中的相同,我们指定行数、列数和当前子图索引(从 1 开始计数)。我们将使用 for 循环遍历数据集中的前 10 张图像,每张图像都会分配一个自己的子图:

In [6]: plt.figure(figsize=(14,4))
...
...     for image_index in range(10):
...         # images are 0-indexed, but subplots are 1-indexed
...         subplot_index = image_index + 1
...         plt.subplot(2, 5, subplot_index)
...         plt.imshow(digits.images[image_index, :, :], cmap='gray')

这导致了以下输出:

对于各种数据集,我母校加州大学欧文分校的机器学习存储库也是一个极好的资源:archive.ics.uci.edu/ml/index.php

使用 OpenCV 的 TrainData 容器在 C++ 中处理数据

为了完整性和那些坚持使用 OpenCV 的 C++ API 的人,让我们快速了解一下 OpenCV 的 TrainData 容器,它允许我们从 .csv 文件中加载数值数据。

在其他方面,在 C++ 中,ml 模块包含一个名为 TrainData 的类,它提供了一个用于在 C++ 中处理数据的容器。其功能仅限于从 .csv 文件(包含逗号分隔值)中读取(最好是)数值数据。因此,如果您想要处理的数据来自整洁的 .csv 文件,这个类将为您节省大量时间。如果您的数据来自不同的来源,恐怕您最好的选择可能是手动创建一个 .csv 文件,使用 ...

摘要

在本章中,我们讨论了处理机器学习问题的典型工作流程:我们如何从原始数据中提取信息性特征,我们如何使用数据和标签来训练机器学习模型,以及我们如何使用最终模型来预测新的数据标签。我们了解到,将数据分为训练集和测试集是至关重要的,因为这是唯一了解模型如何泛化到新数据点的途径。

在软件方面,我们显著提高了我们的 Python 技能。我们学习了如何使用 NumPy 数组来存储和处理数据,以及如何使用 Matplotlib 进行数据可视化。我们讨论了 scikit-learn 及其许多有用的数据资源。最后,我们还提到了 OpenCV 自带的TrainData容器,它为 OpenCV 的 C++ API 用户提供了一些缓解。

拥有这些工具在手,我们现在已经准备好实现我们的第一个真正的机器学习模型了!在下一章中,我们将专注于监督学习及其两个主要问题类别,分类和回归。

第三章:监督学习的第一步

这就是您一直等待的时刻,不是吗?

我们已经涵盖了所有的基础——我们有一个运行的 Python 环境,我们已经安装了 OpenCV,并且我们知道如何在 Python 中处理数据。现在,是时候构建我们的第一个机器学习系统了!还有什么比专注于最常见且最成功的机器学习类型之一:监督学习更好的开始方式呢?

从上一章,我们已经知道监督学习主要是通过使用与之相关的标签来学习训练数据中的规律性,以便我们可以预测一些新的、从未见过的测试数据的标签。在本章中,我们想要深入一点,学习如何将我们的理论知识 ...

技术要求

您可以在此链接中找到本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter03

以下是对软件和硬件要求的全球性总结:

  • 您需要 OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都可以正常工作)。

  • 您需要 Python 版本 3.6(任何 Python 3.x 版本都可以)。

  • 您需要 Anaconda Python 3 来安装 Python 和所需的模块。

  • 您可以使用任何操作系统——macOS、Windows 和基于 Linux 的操作系统——与本书一起使用。我们建议您的系统至少有 4 GB RAM。

  • 您不需要 GPU 就可以运行本书提供的代码。

理解监督学习

我们之前已经确定,监督学习的目标始终是预测数据的标签(或目标值)。然而,根据这些标签的性质,监督学习可以有两种不同的形式:

  • 分类:当我们使用数据来预测类别时,监督学习被称为分类。一个很好的例子是我们试图预测一张图片是否包含猫或狗。在这里,数据的标签是分类的,要么是猫,要么是狗,但永远不会是类别的混合。例如,一张图片要么包含猫,要么包含狗,永远不会是 50%猫和 50%狗(在你问之前,不,这里我们不考虑卡通角色猫狗的图片),我们的工作 ...

在 OpenCV 中查看监督学习

如果我们不能将其付诸实践,仅仅知道如何进行监督学习是没有用的。幸运的是,OpenCV 为其所有的统计学习模型提供了一个相当直观的接口,这包括所有监督学习模型。

在 OpenCV 中,每个机器学习模型都源自cv::ml::StatModel基类。这听起来很复杂,意思是说,如果我们想在 OpenCV 中使用机器学习模型,我们必须提供StatModel告诉我们的所有功能。这包括一个用于训练模型的方法(称为train)和一个用于衡量模型性能的方法(称为calcError)。

面向对象编程OOP)中,我们主要处理对象或类。一个对象由几个函数组成,称为方法,以及变量,称为成员属性。您可以在 Python 中了解更多关于 OOP 的信息,请参阅docs.python.org/3/tutorial/classes.html

由于这种软件组织方式,在 OpenCV 中设置机器学习模型总是遵循相同的逻辑,我们稍后会看到:

  • 初始化:我们通过名称调用模型以创建一个空的模型实例。

  • 设置参数:如果模型需要一些参数,我们可以通过 setter 方法来设置它们,这些方法对于每个模型可能都不同。例如,对于 k-NN 算法要工作,我们需要指定其 open 参数,k(我们稍后会了解到)。

  • 训练模型:每个模型都必须提供一个名为train的方法,用于将模型拟合到某些数据。

  • 预测新标签:每个模型都必须提供一个名为predict的方法,用于预测新数据的标签。

  • 评分模型:每个模型都必须提供一个名为calcError的方法,用于衡量性能。这种计算对于每个模型可能都不同。

由于 OpenCV 是一个庞大且由社区驱动的项目,并非每个算法都像我们作为用户期望的那样遵循这些规则。例如,k-NN 算法的大部分工作都在findNearest方法中完成,尽管predict仍然有效。我们将确保在处理不同示例时指出这些差异。

由于我们将偶尔使用 scikit-learn 来实现 OpenCV 不提供的某些机器学习算法,因此指出 scikit-learn 中的学习算法几乎遵循相同的逻辑是值得的。最显著的区别是,scikit-learn 在初始化步骤中设置所有必需的模型参数。此外,它调用训练函数fit而不是train,以及评分函数score而不是calcError

使用评分函数衡量模型性能

构建机器学习系统最重要的部分之一是找到一种方法来衡量模型预测的质量。在现实场景中,模型很少能完全正确。从前面的章节中,我们知道我们应该使用测试集的数据来评估我们的模型。但这是如何实现的呢?

简短但不太有帮助的回答是,这取决于模型。人们已经想出了各种各样的评分函数,可以用来评估在所有可能的场景下训练好的模型。好消息是,其中许多实际上是 scikit-learn 的metrics模块的一部分。

让我们快速看一下一些最重要的评分函数。...

使用准确度、精确度和召回率评分分类器

在一个只有两个不同类别标签的二分类任务中,有几种不同的方式来衡量分类性能。以下是一些常见的指标:

  • accuracy_score:准确度计算测试集中被正确预测的数据点的数量,并以测试集大小的分数形式返回该数量。坚持将图片分类为猫或狗的例子,准确度表示被正确分类为包含猫或狗的图片的比例。这是分类器的最基本评分函数。

  • precision_score:精确度描述了分类器不将包含狗的图片标记为猫的能力。换句话说,在测试集中,分类器认为包含猫的所有图片中,精确度是实际包含猫的图片的比例。

  • recall_score:召回率(或灵敏度)描述了分类器检索所有包含猫的图片的能力。换句话说,在测试集中所有猫的图片中,召回率是正确识别为猫的图片的比例。

假设我们有一些ground truth(根据我们拥有的数据集是正确的)类别标签,这些标签要么是零,要么是一。我们可以使用 NumPy 的随机数生成器随机生成它们。显然,这意味着,每次我们重新运行代码时,都会生成新的随机数据点。然而,为了本书的目的,这并不太有帮助,因为我希望你能运行代码并总是得到和我一样的结果。实现这一点的一个好方法是将随机数生成器的种子固定。这将确保每次运行脚本时生成器都以相同的方式初始化:

  1. 我们可以使用以下代码来设置随机数生成器的种子:
In [1]: import numpy as np
In [2]: np.random.seed(42)
  1. 然后,我们可以通过在范围(0,2)中随机选择整数来生成五个随机标签,这些标签要么是零,要么是一:
In [3]: y_true = np.random.randint(0, 2, size=5)
...     y_true
Out[3]: array([0, 1, 0, 0, 0])

在文献中,这两个类别有时也被称为正类(所有具有类别标签1的数据点)和负类(所有其他数据点)。

让我们假设我们有一个试图预测前面提到的类别标签的分类器。为了辩论的目的,让我们假设这个分类器不是很聪明,总是预测标签1。我们可以通过硬编码预测标签来模拟这种行为:

In [4]: y_pred = np.ones(5, dtype=np.int32)
...     y_pred
Out[4]: array([1, 1, 1, 1, 1], dtype=int32)

我们的预测准确度是多少?

如前所述,准确率计算测试集中被正确预测的数据点的数量,并将其作为测试集大小的分数返回。我们只正确预测了第二个数据点(其中真实标签是1)。在其他所有情况下,真实标签是0,但我们预测了1。因此,我们的准确率应该是 1/5 或 0.2。

一个简单的准确率度量实现可能只是将所有预测类别标签与真实类别标签匹配的实例相加:

In [5]: test_set_size = len(y_true)
In [6]: predict_correct = np.sum(y_true == y_pred)
In [7]: predict_correct / test_set_size
Out[7]: 0.2

一个更智能且更方便的实现由 scikit-learn 的metrics模块提供:

In [8]: from sklearn import metrics
In [9]: metrics.accuracy_score(y_true, y_pred)
Out[9]: 0.2

这并不太难,对吧?然而,要理解精确度和召回率,我们需要对 I 型错误和 II 型错误有一个一般性的理解。让我们回忆一下,具有类别标签1的数据点通常被称为正例,而具有类别标签0(或-1)的数据点通常被称为负例。然后,对特定数据点的分类可能有四种可能的结局,如下面的混淆矩阵所示:

是否真正为正 是否真正为负
预测为正 真阳性 假阳性
预测为负 假阴性 真阴性

让我们分解一下。如果一个数据点是真正正的,并且我们预测了正的,我们就完全做对了!在这种情况下,结果被称为真阳性。如果我们认为数据点是正的,但实际上它是负的,我们就错误地预测了正的(因此有这个术语,假阳性)。类似地,如果我们认为数据点是负的,但实际上它是正的,我们就错误地预测了负的(假阴性)。最后,如果我们预测了负的,而数据点确实是负的,我们就找到了一个真阴性

在统计假设检验中,假阳性也被称为I 型错误,而假阴性也被称为II 型错误

让我们快速计算这四个指标在我们的模拟数据上。我们有一个真阳性,其中真实标签是1,我们预测了1

In [10]: truly_a_positive = (y_true == 1)
In [11]: predicted_a_positive = (y_pred == 1)
In [12]: true_positive = np.sum(predicted_a_positive * truly_a_positive )
...      true_positive
Out[12]: 1

同样,假阳性是指我们预测了1,但ground truth实际上是0

In [13]: false_positive = np.sum((y_pred == 1) * (y_true == 0))
...      false_positive
Out[13]: 4

我相信到现在你已经掌握了这个。但我们真的需要做数学运算来了解预测的负数吗?我们不太智能的分类器从未预测0,所以(y_pred == 0)永远不会为真:

In [14]: false_negative = np.sum((y_pred == 0) * (y_true == 1))
...      false_negative
Out[14]: 0
In [15]: true_negative = np.sum((y_pred == 0) * (y_true == 0))
...      true_negative
Out[15]: 0

让我们再绘制一下混淆矩阵:

是否真正为正 是否真正为负
预测为正 1 4
预测为负 0 0

为了确保我们一切都做得正确,让我们再次计算准确率。准确率应该是真阳性数加上真阴性数(即我们做对的一切)除以数据点的总数:

In [16]: accuracy = (true_positive + true_negative) / test_set_size
...      accuracy
Out[16]: 0.2

成功!精确度就是真阳性数除以所有真预测数:

In [17]: precision = true_positive / (true_positive + false_positive)
...      precision
Out[17]: 0.2

结果表明,在我们的情况下,精确度并不比准确度更好。让我们用 scikit-learn 来检查我们的数学计算:

In [18]: metrics.precision_score(y_true, y_pred)
Out[18]: 0.2

最后,recall表示我们正确分类为正例的所有正例的比例:

In [19]: recall = true_positive / (true_positive + false_negative)
...      recall
Out[19]: 1.0
In [20]: metrics.recall_score(y_true, y_pred)
Out[20]: 1.0

完美回忆!但是,回到我们的模拟数据,应该很清楚,这个出色的召回分数只是运气。由于我们的模拟数据集中只有一个1标签,而且我们恰好正确地分类了它,所以我们得到了完美的召回分数。这意味着我们的分类器是完美的吗?不是的!但我们已经找到了三个有用的指标,它们似乎衡量了我们分类性能的互补方面。

使用均方误差、解释方差和 R 平方评分回归器

当涉及到回归模型时,我们之前提到的指标不再适用。毕竟,我们现在预测的是连续的输出值,而不是不同的分类标签。幸运的是,scikit-learn 提供了一些其他有用的评分函数:

  • mean_squared_error:回归问题中最常用的误差指标是测量训练集中每个数据点的预测值与真实目标值之间的平方误差,然后对所有数据点进行平均。

  • explained_variance_score:一个更复杂的指标是衡量模型在多大程度上可以解释测试数据的变异或分散程度。通常,解释的...

使用分类模型预测类别标签

拥有这些工具在手,我们现在可以开始处理我们的第一个真正的分类示例。

考虑一下 Randomville 这个小城镇,这里的人们疯狂地喜爱他们的两支运动队,Randomville Reds 和 Randomville Blues。Reds 队历史悠久,深受人们喜爱。然而,后来一位外地百万富翁出现了,他买下了 Reds 队的最佳射手,并组建了一支新队,Blues 队。让大多数 Reds 球迷不满的是,这位最佳射手最终带着 Blues 队赢得了冠军头衔。多年后,他回到了 Reds 队,尽管一些球迷因为他的早期职业选择而对他怀恨在心。但无论如何,你可以理解为什么 Reds 队的球迷并不一定和 Blues 队的球迷相处融洽。事实上,这两个球迷群体如此分裂,以至于他们甚至从不住在隔壁。我甚至听说过红队球迷在蓝队球迷搬进来后故意搬走的故事。这是真的!

无论如何,我们刚搬到这个城镇,正试图挨家挨户地推销一些 Blues 队的商品。然而,时不时地我们会遇到一个狂热的 Reds 球迷,他们对我们卖 Blues 队的商品大喊大叫,并把我们赶出自己的草坪。真不友好!避免这些房子,只访问 Blues 队的球迷,会轻松得多,也能更好地利用我们的时间。

我们有信心学会预测 Reds 球迷的居住地,于是开始记录我们的遭遇。如果我们经过一个 Reds 球迷的家,我们在手头的城镇地图上画一个红色三角形;否则,我们画一个蓝色正方形。过了一段时间,我们就能很好地了解每个人的居住地:

图片

然而,现在,我们来到地图上标记为绿色圆圈的房屋。我们应该敲门吗?我们试图找到一些线索,了解他们更喜欢哪个球队(也许是在后门廊上挂着的球队旗帜),但我们什么也没看到。我们怎么知道敲门是否安全呢?

这个愚蠢的例子所说明的正是监督学习算法可以解决的问题。我们有一系列观察结果(房屋、位置和颜色),构成了我们的训练数据。我们可以使用这些数据从经验中学习,以便当我们面对预测新房屋颜色的任务时,我们可以做出明智的估计。

正如我们之前提到的,红队球迷对他们的球队非常热情,所以他们绝不会搬到蓝队球迷的隔壁。我们难道不能利用这些信息,查看所有相邻的房屋,以了解新房子里住的是哪种球迷吗?

这正是 k-NN 算法会做的事情。

理解 k-NN 算法

k-NN 算法可以说是最简单的机器学习算法之一。原因在于我们基本上只需要存储训练数据集。然后,为了预测一个新的数据点,我们只需要在训练数据集中找到最近的数据点:它的最近邻。

简而言之,k-NN 算法认为一个数据点很可能属于与其邻居相同的类别。想想看:如果我们的邻居是红队球迷,我们可能也是红队球迷;否则,我们早就搬走了。对蓝队也是如此。

当然,一些社区可能要复杂一些。在这种情况下,我们不仅会考虑我们的最近邻(当k=1时),而是...

在 OpenCV 中实现 k-NN

使用 OpenCV,我们可以通过cv2.ml.KNearest_create()函数轻松创建 k-NN 模型。构建模型涉及以下步骤:

  1. 生成一些训练数据。

  2. 为给定的数字k创建一个 k-NN 对象。

  3. 找到我们想要分类的新数据点的k个最近邻。

  4. 通过多数投票为新数据点分配类别标签。

  5. 绘制结果。

我们首先导入所有必要的模块:OpenCV 用于 k-NN 算法,NumPy 用于数据处理,Matplotlib 用于绘图。如果你在一个 Jupyter Notebook 中工作,别忘了调用%matplotlib inline魔法:

In [1]: import numpy as np
...     import cv2
...     import matplotlib.pyplot as plt
...     %matplotlib inline
In [2]: plt.style.use('ggplot')

生成训练数据

第一步是生成一些训练数据。为此,我们将使用 NumPy 的随机数生成器。正如前文所述,我们将固定随机数生成器的种子,以便重新运行脚本时总是生成相同的值:

In [3]: np.random.seed(42)

好的,现在让我们开始吧。我们的训练数据应该是什么样子呢?

在前面的例子中,每个数据点都是城镇地图上的一个房子。每个数据点有两个特征(即,它在城镇地图上的位置xy坐标)和一个类别标签(即,如果住在这里的是蓝球队球迷,则显示蓝色方块;如果是红队球迷,则显示红色三角形)。

因此,单个数据点的特征可以表示为 ...

训练分类器

与所有其他机器学习函数一样,k-NN 分类器是 OpenCV 3.1 的ml模块的一部分。我们可以使用以下命令创建一个新的分类器:

In [15]: knn = cv2.ml.KNearest_create()

在 OpenCV 的旧版本中,此函数可能被称为cv2.KNearest()

然后,我们将我们的训练数据传递给train方法:

In [16]: knn.train(train_data, cv2.ml.ROW_SAMPLE, labels)
Out[16]: True

在这里,我们必须告诉knn我们的数据是一个N x 2的数组(即,每一行是一个数据点)。成功后,该函数返回True

预测新数据点的标签

knn提供的另一个非常有用的方法是findNearest。它可以用来根据新数据点的最近邻预测其标签。

多亏了我们的generate_data函数,生成新的数据点实际上非常简单!我们可以将新的数据点视为大小为1的数据集:

In [17]: newcomer, _ = generate_data(1)...      newcomerOut[17]: array([[91., 59.]], dtype=float32)

我们的功能还会返回一个随机标签,但我们对此不感兴趣。相反,我们想使用我们的训练分类器来预测它!我们可以告诉 Python 忽略一个输出值(下划线_)。

让我们再次看看我们的城镇地图。我们将像之前一样绘制训练集,但 ...

使用回归模型预测连续结果

现在,让我们将注意力转向一个回归问题。我相信你现在可以倒背如流,回归完全是关于预测连续结果,而不是预测离散的类别标签。

理解线性回归

最简单的回归模型称为线性回归。线性回归背后的思想是用特征的线性组合来描述目标变量(例如波士顿房价——回想一下我们在第一章,“机器学习的味道”中研究的各种数据集)。

为了保持简单,让我们只关注两个特征。假设我们想使用两个特征来预测明天的股票价格:今天的股票价格和昨天的股票价格。我们将今天的股票价格表示为第一个特征,f[1],将昨天的股票价格表示为f[2]。那么,线性回归的目标就是学习两个权重系数,w[1]w[2],以便我们可以按照以下方式预测明天的股票价格:

这里,  是

OpenCV 中的线性回归

在尝试在真实数据集上使用线性回归之前,让我们了解如何使用cv2.fitLine函数将线拟合到二维或三维点集:

  1. 让我们从生成一些点开始。我们将通过向位于线上的点添加噪声来生成它们!图片
In [1]: import cv2
...     import numpy as np
...     import matplotlib.pyplot as plt
...     from sklearn import linear_model
...     from sklearn.model_selection import train_test_split
...     plt.style.use('ggplot')
...     %matplotlib inline
In [2]: x = np.linspace(0,10,100)
...     y_hat = x*5+5
...     np.random.seed(42)
...     y = x*5 + 20*(np.random.rand(x.size) - 0.5)+5
  1. 我们还可以使用以下代码可视化这些点:
In [3]: plt.figure(figsize=(10, 6))
...     plt.plot(x, y_hat, linewidth=4)
...     plt.plot(x,y,'x')
...     plt.xlabel('x')
...     plt.ylabel('y')

这给出了以下图表,其中红色线是真实函数:

图片

  1. 接下来,我们将点分为训练集和测试集。在这里,我们将数据分为 70:30 的比例,这意味着,70%的点将用于训练,30%用于测试:
In [4]: x_train, x_test, y_train, y_test = train_test_split(x,y,test_size=0.3,random_state=42)
  1. 现在,让我们使用cv2.fitLine将一条线拟合到这个二维点集。这个函数接受以下参数:

    • points:这是需要拟合直线的点的集合。

    • distType:这是 M-估计器使用的距离。

    • param:这是数值参数(C),在某种类型的距离中使用。我们将将其保持为 0,以便选择最佳值。

    • reps:这是原点到直线的距离的精度。"0.01"是reps的一个很好的默认值。

    • aeps:这是角度的精度。"0.01"是aeps的一个很好的默认值。

更多信息,请参阅文档

  1. 让我们看看使用不同的距离类型选项会得到什么样的结果:
In [5]: distTypeOptions = [cv2.DIST_L2,\
...                 cv2.DIST_L1,\
...                 cv2.DIST_L12,\
...                 cv2.DIST_FAIR,\
...                 cv2.DIST_WELSCH,\
...                 cv2.DIST_HUBER]

In [6]: distTypeLabels = ['DIST_L2',\
...                 'DIST_L1',\
...                 'DIST_L12',\
...                 'DIST_FAIR',\
...                 'DIST_WELSCH',\
...                 'DIST_HUBER']

In [7]: colors = ['g','c','m','y','k','b']
In [8]: points = np.array([(xi,yi) for xi,yi in zip(x_train,y_train)])
  1. 我们还将使用 scikit-learn 的LinearRegression来拟合训练点,然后使用predict函数预测它们的y-值:
In [9]: linreg = linear_model.LinearRegression()
In [10]: linreg.fit(x_train.reshape(-1,1),y_train.reshape(-1,1))
Out[10]:LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None,normalize=False)
In [11]: y_sklearn = linreg.predict(x.reshape(-1,1))
In [12]: y_sklearn = list(y_sklearn.reshape(1,-1)[0])
  1. 我们使用reshape(-1,1)reshape(1,-1)将 NumPy 数组转换为列向量,然后再将其转换回行向量:
In [13]: plt.figure(figsize=(10, 6))
...      plt.plot(x, y_hat,linewidth=2,label='Ideal')
...      plt.plot(x,y,'x',label='Data')

...      for i in range(len(colors)):
...          distType = distTypeOptions[i]
...          distTypeLabel = distTypeLabels[i]
...          c = colors[i]

...          [vxl, vyl, xl, yl] = cv2.fitLine(np.array(points, dtype=np.int32), distType, 0, 0.01, 0.01)
...          y_cv = [vyl[0]/vxl[0] * (xi - xl[0]) + yl[0] for xi in x]
...          plt.plot(x,y_cv,c=c,linewidth=2,label=distTypeLabel)

...      plt.plot(x,list(y_sklearn),c='0.5',\
linewidth=2,label='Scikit-Learn API')
...      plt.xlabel('x')
...      plt.ylabel('y')
...      plt.legend(loc='upper left')

这段前面(且相当长)的代码的唯一目的就是创建一个可以用来比较使用不同距离度量获得的结果的图表。

让我们看看这个图:

图片

如我们清楚地看到,scikit-learn 的LinearRegression模型比 OpenCV 的fitLine函数表现要好得多。现在,让我们使用 scikit-learn 的 API 来预测波士顿房价。

使用线性回归预测波士顿房价

为了更好地理解线性回归,我们想要构建一个简单的模型,该模型可以应用于最著名的机器学习数据集之一:波士顿房价数据集。在这里,目标是使用诸如犯罪率、财产税率、到就业中心的距离和高速公路可及性等信息来预测 20 世纪 70 年代几个波士顿地区的房价。

加载数据集

我们可以再次感谢 scikit-learn 为我们提供了轻松访问数据集的方式。我们首先导入所有必要的模块,就像我们之前做的那样:

In [14]: from sklearn import datasets
...      from sklearn import metrics

加载数据集只需要一行代码:

In [15]: boston = datasets.load_boston()

boston对象的结构与前面命令中讨论的iris对象相同。我们可以在'DESCR'中获取更多关于数据集的信息,在'data'中找到所有数据,在'feature_names'中找到所有特征名称,在'filename'中找到波士顿 CSV 数据集的物理位置,在'target'中找到所有目标值:

In [16]: dir(boston)
Out[16]: ['DESCR', 'data', 'feature_names', 'filename', 'target']

该数据集包含总共506个数据点,每个点都有13个特征:

In [17]: boston.data.shape
Out[17]: (506, 13)

当然,我们只有一个目标值,那就是房价:

In [18]: boston.target.shape
Out[18]: (506,)

训练模型

现在让我们创建一个LinearRegression模型,然后将在训练集上对其进行训练:

In [19]: linreg = linear_model.LinearRegression()

在前面的命令中,我们想要将数据分为训练集和测试集。我们可以自由地按照我们的意愿进行分割,但通常保留 10%到 30%用于测试是个好主意。在这里,我们选择 10%,使用test_size参数:

In [20]: X_train, X_test, y_train, y_test = train_test_split(...            boston.data, boston.target, test_size=0.1,...            random_state=42...      )

在 scikit-learn 中,train函数被称为fit,但除此之外的行为与 OpenCV 中的完全相同:

In [21]: linreg.fit(X_train, y_train)Out[21]: LinearRegression(copy_X=True, fit_intercept=True, ...

测试模型

为了测试模型的泛化性能,我们在测试数据上计算均方误差:

In [24]: y_pred = linreg.predict(X_test)
In [25]: metrics.mean_squared_error(y_test, y_pred)
Out[25]: 14.995852876582541

我们注意到,测试集上的均方误差比训练集略低。这是一个好消息,因为我们主要关心测试误差。然而,从这些数字中很难理解模型真正有多好。也许绘制数据会更清楚:

In [26]: plt.figure(figsize=(10, 6))
...      plt.plot(y_test, linewidth=3, label='ground truth')
...      plt.plot(y_pred, linewidth=3, label='predicted')
...      plt.legend(loc='best')
...      plt.xlabel('test data points')
...      plt.ylabel('target value')
Out[26]: <matplotlib.text.Text at 0x7ff46783c7b8>

这将生成以下图表:

这更有意义!在这里,我们可以看到所有测试样本的ground truth房价用红色表示,我们的预测房价用蓝色表示。如果你问我,这已经很接近了。不过,值得注意的是,模型对于极高或极低的房价往往偏离最大,例如数据点的峰值121842。我们可以通过计算 R 平方来形式化我们能够解释的数据中的方差量:

In [27]: plt.figure(figsize=(10, 6))
...      plt.plot(y_test, y_pred, 'o')
...      plt.plot([-10, 60], [-10, 60], 'k--')
...      plt.axis([-10, 60, -10, 60])
...      plt.xlabel('ground truth')
...      plt.ylabel('predicted')

这将在x轴上绘制ground truth价格,即y_test,并在y轴上绘制我们的预测值,即y_pred。我们还会绘制一条参考线(使用黑色虚线'k--'),很快我们就会看到。但我们还希望在文本框中显示 R²分数和均方误差:

...      scorestr = r'R$²$ = %.3f' % linreg.score(X_test, y_test)
...      errstr = 'MSE = %.3f' % metrics.mean_squared_error(y_test, y_pred)
...      plt.text(-5, 50, scorestr, fontsize=12)
...      plt.text(-5, 45, errstr, fontsize=12)
Out[27]: <matplotlib.text.Text at 0x7ff4642d0400>

这将生成以下图表,这是一种专业绘制模型拟合的方法:

如果我们的模型是完美的,那么所有数据点都会位于虚线对角线上,因为y_pred将始终等于y_true。与对角线偏离表示模型犯了错误,或者数据中存在模型无法解释的方差。实际上,表明我们能够解释数据中的 76%的散点,均方误差为 14.996。这些都是我们可以用来比较线性回归模型和一些更复杂模型的性能指标。

应用 Lasso 和岭回归

机器学习中一个常见的问题是,一个算法可能在训练集上表现得非常好,但当应用到未见过的数据上时,会犯很多错误。你可以看到这有多麻烦,因为我们通常最感兴趣的是模型如何泛化到新数据。一些算法(如决策树)比其他算法更容易出现这种现象,但即使是线性回归也可能受到影响。

这种现象也被称为过拟合,我们将在第五章使用决策树进行医疗诊断和第十一章使用超参数调整选择合适的模型中详细讨论。

减少过拟合的常见技术被称为正则化,它涉及...

使用逻辑回归对菊花物种进行分类

世界上机器学习领域另一个著名的数据集被称为菊花数据集。菊花数据集包含来自三种不同物种(Setosa、Versicolor 和 Viriginica)的 150 朵菊花测量值。这些测量值包括花瓣和萼片的长度和宽度,所有测量值都以厘米为单位。

我们的目标是构建一个机器学习模型,该模型可以学习这些已知物种的菊花花径测量值,以便我们可以预测新菊花的物种。

理解逻辑回归

在我们开始本节之前,我要发出警告——尽管名为逻辑回归,但实际上它是一个分类模型,特别是当你有两个类别时。它得名于它使用的逻辑函数(或 Sigmoid 函数),它将任何实值输入x转换为介于01之间的预测输出值ŷ,如下面的图所示:

图片

ŷ四舍五入到最接近的整数实际上将输入分类为属于类别01

当然,我们的大部分问题通常有多个输入或特征值,x。例如,菊花数据集提供了总共...

加载训练数据

菊花数据集包含在 scikit-learn 中。我们首先加载所有必要的模块,就像我们之前的例子一样:

In [1]: import numpy as np
...     import cv2
...     from sklearn import datasets
...     from sklearn import model_selection
...     from sklearn import metrics
...     import matplotlib.pyplot as plt
...     %matplotlib inline
In [2]: plt.style.use('ggplot')

然后加载数据集只需一行代码:

In [3]: iris = datasets.load_iris()

此函数返回一个我们称为iris的字典,其中包含许多不同的字段:

In [4]: dir(iris)
Out[4]: ['DESCR', 'data', 'feature_names', 'filename', 'target', 'target_names']

在这里,所有数据点都包含在'data'中。有150个数据点,每个数据点都有4个特征值:

In [5]: iris.data.shape
Out[5]: (150, 4)

这四个特征对应于之前提到的萼片和花瓣尺寸:

In [6]: iris.feature_names
Out[6]: ['sepal length (cm)',
         'sepal width (cm)',
         'petal length (cm)',
         'petal width (cm)']

对于每个数据点,我们都有一个存储在target中的类别标签:

In [7]: iris.target.shape
Out[7]: (150,)

我们还可以检查类别标签,发现总共有三个类别:

In [8]: np.unique(iris.target)
Out[8]: array([0, 1, 2])

将其转换为二分类问题

为了简化问题,我们目前只想关注一个二分类问题,其中我们只有两个类别。最简单的方法是丢弃属于特定类别(例如类别标签 2)的所有数据点,通过选择不属于类别2的所有行来实现:

In [9]: idx = iris.target != 2...     data = iris.data[idx].astype(np.float32)...     target = iris.target[idx].astype(np.float32)

接下来,让我们检查数据。

检查数据

在开始设置模型之前,总是先查看数据是一个好主意。我们之前在城镇地图示例中这样做过,所以这里也重复一下。使用 Matplotlib,我们创建一个散点图,其中每个数据点的颜色对应于类别标签:

In [10]: plt.scatter(data[:, 0], data[:, 1], c=target,  
                     cmap=plt.cm.Paired, s=100)
...      plt.xlabel(iris.feature_names[0])
...      plt.ylabel(iris.feature_names[1])
Out[10]: <matplotlib.text.Text at 0x23bb5e03eb8>

为了使绘图更简单,我们限制自己只使用前两个特征(iris.feature_names[0]代表花萼长度,iris.feature_names[1]代表花萼宽度)。我们可以在下面的图中看到类别的良好分离:

图片

上一张图显示了 Iris 数据集前两个特征的绘图。

将数据分为训练集和测试集

在上一章中,我们了解到保持训练数据和测试数据分离是至关重要的。我们可以使用 scikit-learn 的许多辅助函数之一轻松地分割数据:

In [11]: X_train, X_test, y_train, y_test = model_selection.train_test_split(...            data, target, test_size=0.1, random_state=42...      )

在这里,我们希望将数据分为 90%的训练数据和 10%的测试数据,我们通过test_size=0.1来指定。通过检查返回的参数,我们注意到我们最终得到了恰好90个训练数据点和10个测试数据点:

In [12]: X_train.shape, y_train.shapeOut[12]: ((90, 4), (90,))In [13]: X_test.shape, y_test.shapeOut[13]: ((10, 4), (10,))

训练分类器

创建逻辑回归分类器涉及的过程几乎与设置 k-NN 相同:

In [14]: lr = cv2.ml.LogisticRegression_create()

然后我们必须指定所需的训练方法。在这里,我们可以选择cv2.ml.LogisticRegression_BATCHcv2.ml.LogisticRegression_MINI_BATCH。目前,我们只需要知道我们希望在每次数据点之后更新模型,这可以通过以下代码实现:

In [15]: lr.setTrainMethod(cv2.ml.LogisticRegression_MINI_BATCH)
...      lr.setMiniBatchSize(1)

我们还希望指定算法在终止前应该运行的迭代次数:

In [16]: lr.setIterations(100)

然后,我们可以调用对象的train方法(与之前完全相同),在成功时将返回True

In [17]: lr.train(X_train, cv2.ml.ROW_SAMPLE, y_train)
Out[17]: True

正如我们刚才看到的,训练阶段的目标是找到一组权重,这些权重可以将特征值最佳地转换为输出标签。一个数据点由其四个特征值(f[0]f[1]f[2]f[3])给出。由于我们有四个特征,我们也应该得到四个权重,以便x = w[0] f[0] + w[1] f[1] + w[2] f[2] + w[3] f[3],并且ŷ=σ(x)。然而,正如之前讨论的,算法添加了一个额外的权重,它充当偏移或偏差,因此x = w[0] f[0] + w[1] f[1] + w[2] f[2] + w[3] f[3] + w[4]。我们可以如下检索这些权重:

In [18]: lr.get_learnt_thetas()
Out[18]: array([[-0.04090132, -0.01910266, -0.16340332, 0.28743777, 0.11909772]], dtype=float32)

这意味着逻辑函数的输入是x = -0.0409 f[0] - 0.0191 f[1] - 0.163 f[2] + 0.287 f[3] + 0.119。然后,当我们输入一个属于类别 1 的新数据点(f[0]f[1]f[2]f[3])时,输出ŷ=σ(x)应该接近 1。但这实际上效果如何呢?

测试分类器

让我们亲自计算训练集上的准确率分数来验证:

In [19]: ret, y_pred = lr.predict(X_train)In [20]: metrics.accuracy_score(y_train, y_pred)Out[20]: 1.0

完美分数!然而,这仅仅意味着模型能够完美地记忆训练数据集。这并不意味着模型能够对新的、未见过的数据点进行分类。为此,我们需要检查测试数据集:

In [21]: ret, y_pred = lr.predict(X_test)...      metrics.accuracy_score(y_test, y_pred)Out[21]: 1.0

幸运的是,我们又得到了一个满分!现在我们可以确信,我们构建的模型确实是出色的。

摘要

在本章中,我们覆盖了相当多的内容,不是吗?

简而言之,我们学习了关于不同监督学习算法的很多知识,如何将它们应用于真实数据集,以及如何在 OpenCV 中实现一切。我们介绍了分类算法,如 k-NN 和逻辑回归,并讨论了它们如何被用来预测两个或更多离散类别标签。我们还介绍了线性回归的各种变体(如 Lasso 回归和岭回归),并讨论了它们如何被用来预测连续变量。最后但同样重要的是,我们熟悉了机器学习历史上的两个经典数据集:Iris 和 Boston。

在接下来的章节中,我们将更深入地探讨这些主题,并探索一些这些概念可以发挥作用的更有趣的例子。

但首先,我们需要讨论机器学习中的另一个重要主题,即特征工程。通常,数据不会以整洁的格式出现,我们有责任以有意义的方式表示数据。因此,下一章将讨论如何表示特征和工程数据。

第四章:表示数据和特征工程

在上一章中,我们构建了我们第一个监督学习模型,并将其应用于一些经典数据集,如IrisBoston数据集。然而,在现实世界中,数据很少以整洁的<n_samples x n_features>特征矩阵的形式出现,这是预包装数据库的一部分。相反,我们的责任是找到一种有意义的表示数据的方法。寻找最佳数据表示方式的过程被称为特征工程,这是数据科学家和试图解决现实世界问题的机器学习从业者的一项主要任务。

我知道您更愿意直接跳到结尾,构建人类有史以来最深的神经网络。 ...

技术要求

您可以从以下链接中获取本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter04

这里是软件和硬件要求的一个总结:

  • 您将需要 OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都可以正常工作)。

  • 您将需要 Python 版本 3.6(任何 Python 3.x 版本都可以)。

  • 您需要 Anaconda Python 3 来安装 Python 和所需的模块。

  • 您可以使用任何操作系统——macOS、Windows 以及基于 Linux 的操作系统,配合本书使用。我们建议您的系统至少有 4GB RAM。

  • 您不需要拥有 GPU 来运行本书提供的代码。

理解特征工程

信不信由你,一个机器学习系统能够学习的好坏主要取决于训练数据的质量。尽管每个学习算法都有其优势和劣势,但性能的差异通常归结于数据准备或表示的方式。因此,特征工程可以理解为一种数据表示的工具。机器学习算法试图从样本数据中学习解决问题的解决方案,而特征工程则问:使用什么最佳表示的样本数据来学习解决问题的解决方案?

记住,在前几章中,我们讨论了整个机器学习流程。在那里,我们已经提到了特征提取,但我们也 ...

数据预处理

我们在处理数据方面越有纪律,最终可能获得的结果就越好。这个程序的第一步被称为数据预处理,它至少有三种不同的风味:

  • 数据格式化:数据可能不是我们能够工作的格式;例如,数据可能以专有文件格式提供,而我们最喜欢的机器学习算法无法理解。

  • 数据清洗:数据可能包含无效或缺失条目,需要清理或删除。

  • 数据采样:数据可能远远超出了我们特定目的的需求,迫使我们智能地采样数据。

一旦数据预处理完成,我们就可以准备进行实际的特征工程:将预处理后的数据转换以适应我们特定的机器学习算法。这一步通常涉及以下三种可能过程之一或多个:

  • 缩放:某些机器学习算法通常要求数据在公共范围内,例如具有零均值和单位方差。缩放是将所有特征(可能具有不同的物理单位)带入一个共同值范围的过程。

  • 分解:数据集通常具有比我们能够处理更多的特征。特征分解是将数据压缩成更少但高度信息丰富的数据组件的过程。

  • 聚合:有时,可以将多个特征组合成一个更有意义的单一特征。例如,数据库可能包含每个用户登录基于网络的系统的日期和时间。根据任务的不同,这些数据可能通过简单地计算每个用户的登录次数来更好地表示。

让我们更详细地看看这些过程。

标准化特征

标准化指的是将数据缩放到具有零均值和单位方差的过程。这对于广泛的机器学习算法来说是一个常见的要求,如果单个特征不满足这一要求,这些算法可能会表现不佳。我们可以通过从每个数据点中减去所有数据的平均值(μ)并除以数据的方差(σ)来手动标准化我们的数据;也就是说,对于每个特征x,我们会计算(x - μ) / σ

或者,scikit-learn 在其preprocessing模块中提供了一个直接实现此过程的简单方法。

让我们考虑一个 3 x 3 的数据矩阵,X,代表三个数据点(行)以及每个数据点三个任意选择的特征值(列):...

归一化特征

与标准化类似,归一化是缩放单个样本以具有单位范数的过程。我确信你知道范数代表向量的长度,并且可以以不同的方式定义。我们在上一章讨论了其中的两种:L1 范数(或曼哈顿距离)和 L2 范数(或欧几里得距离)。

在 scikit-learn 中,我们可以使用normalize函数来对数据矩阵X进行归一化,并通过norm关键字指定l1范数:

In [5]: X_normalized_l1 = preprocessing.normalize(X, norm='l1')
...     X_normalized_l1
Out[5]: array([[ 0.2, -0.4, 0.4],
               [ 1\. , 0\. , 0\. ],
               [ 0\. , 0.5, -0.5]])

同样,可以通过指定norm='l2'来计算 L2 范数:

In [6]: X_normalized_l2 = preprocessing.normalize(X, norm='l2')
...     X_normalized_l2
Out[6]: array([[ 0.33333333, -0.66666667, 0.66666667],
               [ 1\. , 0\. , 0\. ],
               [ 0\. , 0.70710678, -0.70710678]])

特征缩放到一个范围

缩放特征到零均值和单位方差的一个替代方法是让特征位于给定的最小值和最大值之间。通常,这些值是零和一,这样每个特征的绝对最大值就被缩放为单位大小。在 scikit-learn 中,这可以通过使用MinMaxScaler实现:

In [7]: min_max_scaler = preprocessing.MinMaxScaler()...     X_min_max = min_max_scaler.fit_transform(X)...     X_min_maxOut[7]: array([[ 0.33333333, 0\. , 1\. ],               [ 1\. , 0.66666667, 0.33333333],               [ 0\. , 1\. , 0\. ]])

默认情况下,数据将被缩放到 0 和 1 之间。我们可以通过向MinMaxScaler构造函数传递一个关键字参数feature_range来指定不同的范围:

In [8]: min_max_scaler = preprocessing.MinMaxScaler(feature_range ...

二值化特征

最后,我们可能不太关心数据的精确特征值。相反,我们可能只想知道一个特征是否存在。通过阈值处理特征值可以实现数据的二值化。让我们快速回顾一下我们的特征矩阵X

In [9]: X
Out[9]: array([[ 1., -2., 2.],
               [ 3., 0., 0.],
               [ 0., 1., -1.]])

假设这些数字代表我们银行账户中的数千美元。如果账户中有超过 0.5 千美元,我们将其视为富人,用 1 表示。否则,我们用 0 表示。这类似于使用threshold=0.5对数据进行阈值处理:

In [10]: binarizer = preprocessing.Binarizer(threshold=0.5)
...      X_binarized = binarizer.transform(X)
...      X_binarized
Out[10]: array([[ 1., 0., 1.],
                [ 1., 0., 0.],
                [ 0., 1., 0.]])

结果是一个由全 1 和全 0 组成的矩阵。

处理缺失数据

在特征工程中,另一个常见的要求是处理缺失数据。例如,我们可能有一个看起来像这样的数据集:

In [11]: from numpy import nan...      X = np.array([[ nan, 0,   3 ],...                    [ 2,   9,  -8 ],...                    [ 1,   nan, 1 ],...                    [ 5,   2,   4 ],...                    [ 7,   6,  -3 ]])

大多数机器学习算法都无法处理非数字(Python 中的nan)值。因此,我们首先必须用一些适当的填充值替换所有nan值。这被称为缺失值的插补

scikit-learn 提供了三种不同的策略来插补缺失值:

  • mean: 使用矩阵指定轴上的平均值替换所有nan值(默认:axis = 0

  • median: 使用...

理解降维

数据集通常具有比我们可能处理的更多的特征。例如,让我们假设我们的工作是预测一个国家的贫困率。我们可能会首先将一个国家的名字与其贫困率相匹配,但这不会帮助我们预测新国家的贫困率。因此,我们开始思考贫困的可能原因。但有多少可能的贫困原因呢?可能包括一个国家的经济、缺乏教育、高离婚率、人口过剩等等。如果每个原因都是一个用于帮助预测贫困率的特征,我们最终会得到无数个特征。如果你是一位数学家,你可能会将这些特征视为高维空间中的,而每个国家的贫困率在这个高维空间中就是一个单独的点。

如果你不是数学家,从小处着手可能会有所帮助。比如说,我们首先只看两个特征:一个国家的国内生产总值GDP)和公民数量。我们将 GDP 解释为x 轴,公民数量解释为y 轴,在一个二维空间中。然后,我们来看第一个国家。它有一个较小的 GDP 和平均数量的公民。我们在x-y平面上画一个点来代表这个国家。我们再添加第二个、第三个和第四个国家。第四个国家恰好既有高 GDP 又有大量公民。因此,我们的四个数据点可能会像以下截图那样在x-y平面上分布得很开:

图片

然而,如果我们开始添加第三个特征,比如国家的离婚率,到我们的分析中会发生什么?这将给我们的图表添加一个第三个轴(z 轴)。突然,我们发现数据不再非常均匀地分布在x-y-z立方体上,因为立方体的大部分仍然是空的。在二维空间中,我们似乎已经覆盖了大部分的x-y正方形,但在三维空间中,我们需要更多的数据点来填补数据点 1 到 3 之间的空白,以及右上角孤独的数据点 4。

这个问题也被称为维度灾难:填充可用空间所需的数据点数量随着维度(或图表轴)数量的指数增长。如果一个分类器没有提供跨越整个特征空间的数据点(如前述立方体示例所示),那么当出现一个位于所有先前遇到的数据点都很远的新数据点时,分类器将不知道该怎么办。

维度灾难意味着,在一定的特征数量(或维度)之后,分类器的性能将开始下降。让我们来理解这一点。更多的特征本质上意味着可以解释数据集中的更多变化。但是,考虑超过所需特征的数量会导致分类器甚至考虑任何异常值或过度拟合数据集。因此,分类器的性能将开始下降而不是提高:

图片

但是,我们如何找到数据集的看似最优的维度数量?

这就是降维发挥作用的地方。这是一系列技术,允许我们找到高维数据的紧凑表示,而不会丢失太多信息。

在 OpenCV 中实现主成分分析(PCA)

最常见的降维技术之一被称为PCA

与前面展示的 2D 和 3D 示例类似,我们可以将图像视为高维空间中的一个点。如果我们通过堆叠所有列将高度为m和宽度为n的 2D 灰度图像展平,我们得到一个长度为m x n x 1的(特征)向量。这个向量中第i个元素的值是图像中第i个像素的灰度值。现在,想象一下,如果我们想用这些精确的维度来表示每个可能的 2D 灰度图像,那会有多少个图像?

由于灰度像素通常取值在 0 到 255 之间,总共有 256 的m x n次方个图像。机会 ...

实现独立成分分析 (ICA)

scikit-learn 提供了与 PCA 密切相关但不是 OpenCV 的其他有用的降维技术。我们在这里提及它们是为了完整性。ICA 执行与 PCA 相同的数学步骤,但它选择分解的组件尽可能相互独立,而不是像 PCA 那样按预测变量选择。

在 scikit-learn 中,ICA 可以从decomposition模块中获取:

In [9]:  from sklearn import decomposition
In [10]: ica = decomposition.FastICA(tol=0.005)

为什么我们使用tol=0.005?因为我们希望 FastICA 收敛到某个特定的值。有两种方法可以实现这一点——增加迭代次数(默认值为200)或减少容差(默认值为0.0001)。我尝试增加迭代次数,但不幸的是,它不起作用,所以我选择了另一种方法。你能想出为什么它没有收敛吗?

如前所述,数据转换发生在fit_transform函数中:

In [11]: X2 = ica.fit_transform(X)

在我们的情况下,绘制旋转后的数据会导致与之前使用 PCA 获得的结果相似,这可以在随后的代码块后的图中验证。

In [12]: plt.figure(figsize=(10, 6))
...      plt.plot(X2[:, 0], X2[:, 1], 'o')
...      plt.xlabel('first independent component')
...      plt.ylabel('second independent component')
...      plt.axis([-0.2, 0.2, -0.2, 0.2])
Out[12]: [-0.2, 0.2, -0.2, 0.2]

这可以在以下图中看到:

图片

实现非负矩阵分解 (NMF)

另一种有用的降维技术称为NMF。它再次实现了与 PCA 和 ICA 相同的基本数学运算,但它有一个额外的约束,即它仅对非负数据进行操作。换句话说,如果我们想使用 NMF,我们的特征矩阵中不能有负值;分解的结果也将全部具有非负值。

在 scikit-learn 中,NMF 与 ICA 的工作方式完全相同:

In [13]: nmf = decomposition.NMF()In [14]: X2 = nmf.fit_transform(X)In [15]: plt.plot(X2[:, 0], X2[:, 1], 'o')...      plt.xlabel('first non-negative component')...      plt.ylabel('second non-negative component')...      plt.axis([-5, 20, -5, 10])Out[15]: [-5, 20, ...

使用 t-Distributed Stochastic Neighbor Embedding (t-SNE)可视化降维

t-SNE 是一种降维技术,非常适合高维数据的可视化。

在本节中,我们将看到一个如何使用 t-SNE 可视化高维数据集的例子。以数字数据集为例,其中包含从 0 到 9 的手写数字图像。这是一个公开可用的数据集,通常被称为 MNIST 数据集。我们将看到如何使用 t-SNE 在这个数据集上可视化降维:

  1. 首先,让我们加载数据集:
In [1]: import numpy as np
In [2]: from sklearn.datasets import load_digits
In [3]: digits = load_digits()
In [4]: X, y = digits.data/255.0, digits.target
In [5]: print(X.shape, y.shape)
Out[5]: (1797, 64) (1797,)
  1. 您应该首先应用 PCA 等降维技术将高维数减少到较低的维数,然后使用 t-SNE 等技术来可视化数据。但是,在这种情况下,让我们使用所有维度并直接使用 t-SNE:
In [6]: from sklearn.manifold import TSNE
In [7]: tsne = TSNE(n_components=2, verbose=1, perplexity=40, n_iter=300)
In [8]: tsne_results = tsne.fit_transform(df.loc[:,features].values)
Out[8]: [t-SNE] Computing 121 nearest neighbors...
... [t-SNE] Indexed 1797 samples in 0.009s...
... [t-SNE] Computed neighbors for 1797 samples in 0.395s...
... [t-SNE] Computed conditional probabilities for sample 1000 / 1797
... [t-SNE] Computed conditional probabilities for sample 1797 / 1797
... [t-SNE] Mean sigma: 0.048776
... [t-SNE] KL divergence after 250 iterations with early exaggeration: 61.094833
... [t-SNE] KL divergence after 300 iterations: 0.926492
  1. 最后,让我们使用散点图帮助我们可视化使用 t-SNE 提取的两个维度:
In [9]: import matplotlib.pyplot as plt
In [10]: plt.scatter(tsne_results[:,0],tsne_results[:,1],c=y/10.0)
...      plt.xlabel('x-tsne')
...      plt.ylabel('y-tsne')
...      plt.title('t-SNE')
In [11]: plt.show()

我们得到以下输出:

图片

现在,让我们在下一节讨论如何表示分类变量。

表示分类变量

在构建机器学习系统时,我们可能会遇到的最常见的数据类型之一是分类特征(也称为离散特征),例如水果的颜色或公司的名称。分类特征的挑战在于它们不是以连续的方式变化的,这使得用数字表示它们变得困难。

例如,香蕉要么是绿色的,要么是黄色的,但不会同时是两者。一个产品要么属于服装部门,要么属于书籍部门,很少同时属于两者,等等。

你会如何表示这样的特征?

例如,假设我们正在尝试编码一个包含机器学习和人工智能先驱名单的数据集:...

表示文本特征

与分类特征类似,scikit-learn 提供了一个简单的方法来编码另一种常见的特征类型——文本特征。当处理文本特征时,通常方便将单个单词或短语编码为数值。

让我们考虑一个包含少量文本短语的语料库数据集:

In [1]: sample = [
...        'feature engineering',
...        'feature selection',
...        'feature extraction'
...     ]

编码此类数据的最简单方法之一是通过词频统计;对于每个短语,我们只需统计其中每个单词的出现次数。在 scikit-learn 中,这可以通过使用CountVectorizer轻松完成,其功能类似于DictVectorizer

In [2]: from sklearn.feature_extraction.text import CountVectorizer
...     vec = CountVectorizer()
...     X = vec.fit_transform(sample)
...     X
Out[2]: <3x4 sparse matrix of type '<class 'numpy.int64'>'
                with 6 stored elements in Compressed Sparse Row format>

默认情况下,这将把我们的特征矩阵X存储为稀疏矩阵。如果我们想手动检查它,我们需要将其转换为常规数组:

In [3]: X.toarray()
Out[3]: array([[1, 0, 1, 0],
               [0, 0, 1, 1],
               [0, 1, 1, 0]], dtype=int64)

要了解这些数字的含义,我们必须查看特征名称:

In [4]: vec.get_feature_names()
Out[4]: ['engineering', 'extraction', 'feature', 'selection']

现在,X中的整数含义变得清晰。如果我们查看X顶部行表示的短语,我们会看到它包含一次engineering这个词的出现,以及一次feature这个词的出现。另一方面,它不包含extractionselection这两个词。这有意义吗?快速查看我们的原始数据sample揭示,这个短语确实是feature engineering

只看X数组(不要作弊!),你能猜出sample中的最后一个短语是什么吗?

这种方法的一个可能的缺点是我们可能会过分重视出现频率非常高的单词。解决这个问题的一个方法被称为词频-逆文档频率TF-IDF)。TF-IDF 所做的事情可能比它的名字更容易理解,它基本上是通过衡量单词在整个数据集中出现的频率来权衡单词计数。

TF-IDF 的语法与之前的命令非常相似:

In [5]: from sklearn.feature_extraction.text import TfidfVectorizer
...     vec = TfidfVectorizer()
...     X = vec.fit_transform(sample)
...     X.toarray()
Out[5]: array([[ 0.861037 , 0\. , 0.50854232, 0\. ],
               [ 0\. , 0\. , 0.50854232, 0.861037 ],
               [ 0\. , 0.861037 , 0.50854232, 0\. ]])

我们注意到,现在的数字比以前小了,第三列的降幅最大。这是有道理的,因为第三列对应于所有三个短语中最频繁出现的单词feature

In [6]: vec.get_feature_names()
Out[6]: ['engineering', 'extraction', 'feature', 'selection']

如果你对 TF-IDF 背后的数学感兴趣,可以从这篇论文开始:citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.121.1424&rep=rep1&type=pdf。有关 scikit-learn 中其具体实现的更多信息,请参阅 API 文档:scikit-learn.org/stable/modules/feature_extraction.html#tfidf-term-weighting

在第七章中,我们将讨论表示文本特征的重要性,该章节名为使用贝叶斯学习实现垃圾邮件过滤器

表示图像

计算机视觉中最常见且重要的数据类型当然是图像。表示图像最直接的方式可能是使用图像中每个像素的灰度值。通常,灰度值并不能很好地表明它们所描述的数据。例如,如果我们看到一个灰度值为 128 的单个像素,我们能否判断这个像素属于哪个物体?可能不能。因此,灰度值并不是非常有效的图像特征。

使用颜色空间

或者,我们可能会发现颜色包含一些原始灰度值无法捕捉到的信息。最常见的情况是,图像以传统的 RGB 颜色空间出现,其中图像中的每个像素都有一个表示其显色红色R)、绿色G)和蓝色B)强度的值。然而,OpenCV 提供了一系列其他颜色空间,例如色调饱和度值HSV)、色调饱和度亮度HSL)和 Lab 颜色空间。让我们快速了解一下它们。

在 RGB 空间中编码图像

我相信你已经熟悉 RGB 颜色空间了,它通过不同色调的红、绿、蓝的加色混合来产生不同的合成颜色。RGB 颜色空间在日常生活中非常有用,因为它覆盖了人眼可以看到的大部分颜色空间。这就是为什么彩色电视或彩色计算机显示器只需要关注产生红、绿、蓝光的混合。

在 OpenCV 中,RGB 图像直接支持。你需要知道或需要提醒的是,在 OpenCV 中,彩色图像实际上是存储为 BGR 图像;也就是说,颜色通道的顺序是蓝-绿-红,而不是红-绿-蓝。这样做的原因是...

在 HSV 和 HLS 空间中编码图像

然而,自从 RGB 颜色空间被创建以来,人们已经意识到它实际上是对人类视觉的一种相当差的表示。因此,研究人员已经开发了许多替代表示。其中之一被称为HSV(代表色调、饱和度和亮度)和另一个被称为HLS色调、亮度和饱和度)。你可能已经在颜色选择器和常见的图像编辑软件中看到过这些颜色空间。在这些颜色空间中,颜色的色调由一个单独的色调通道捕获,颜色的饱和度由饱和度通道捕获,而亮度或亮度由亮度或值通道捕获。

在 OpenCV 中,可以使用cv2.cvtColor轻松将 RGB 图像转换为 HSV 颜色空间:

In [5]: img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)

对于 HLS 颜色空间也是如此。实际上,OpenCV 提供了一系列额外的颜色空间,这些颜色空间可以通过cv2.cvtColor访问。我们只需要将颜色标志替换为以下之一:

  • 使用cv2.COLOR_BGR2HLS的 HLS

  • 使用cv2.COLOR_BGR2LAB的 LAB(亮度、绿色-红色和蓝色-黄色)

  • YUV(整体亮度、蓝色亮度和红色亮度)使用cv2.COLOR_BGR2YUV

在图像中检测角点

在图像中找到的最简单直观的传统特征可能是角点(几个边缘相交的位置)。OpenCV 提供了至少两种不同的算法来在图像中找到角点:

  • Harris-Dorner 检测:知道边缘是所有方向上强度变化高的区域,Harris 和 Stephens 提出了一种快速找到这种位置的方法。这个算法在 OpenCV 中实现为cv2.cornerHarris

  • Shi-Tomasi 角点检测:Shi 和 Tomasi 有他们自己对构成良好跟踪特征的想法,并且他们通常通过找到N个最强的角点,比 Harris 角点检测做得更好。这个算法在 OpenCV 中实现为cv2.goodFeaturesToTrack

Harris ...

使用星形检测器和 BRIEF 描述符

然而,当图像的尺度发生变化时,角点检测是不够的。已经发表了多篇论文,描述了不同的特征检测和描述算法。我们将查看Speeded Up Robust FeaturesSURF)检测器(更多信息请参阅en.wikipedia.org/wiki/Speeded_up_robust_features)和Binary Robust Independent Elementary FeaturesBRIEF)描述符的组合。特征检测器识别图像中的关键点,特征描述符计算所有关键点的实际特征值。

这些算法的细节超出了本书的范围。高级用户可以参考详细描述这些算法的论文。

更多细节,您可以参考以下链接:

整个过程从读取图像开始,将其转换为灰度图,使用星形特征检测器找到有趣点,最后使用 BRIEF 描述符计算特征值。

  1. 让我们先读取图像并将其转换为灰度图:
In [23]: img = cv2.imread('data/rubic-cube.png')
In [24]: gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
  1. 现在,我们将创建特征检测器和描述符:
In [25]: star = cv2.xfeatures2d.StarDetector_create()
In [26]: brief = cv2.xfeatures2d.BriefDescriptorExtractor_create()
  1. 接下来,是时候使用星形检测器获取关键点并将它们传递给 BRIEF 描述符了:
In [27]: keyPoints = star.detect(gray, None)
In [28]: keyPoints, descriptors = brief.compute(img, keyPoints)

这里有一个问题。在撰写这本书的时候,OpenCV 4.0 版本还没有cv2.drawKeypoints函数的解决版本。因此,我编写了一个类似的功能,我们可以用它来绘制关键点。您不需要担心函数中涉及的步骤——它只是为了您的参考。如果您已经安装了本书中指定的 OpenCV 版本(OpenCV 4.1.0 或 OpenCV 4.1.1),您可以直接使用cv2.drawKeypoints函数:

In [29]: def drawKeypoint (img, keypoint, color):
...          draw_shift_bits = 4
...          draw_multiplier = 1 << draw_shift_bits

...          center = (int(round(keypoint.pt[0])),int(round(keypoint.pt[1])))

...          radius = int(round(keypoint.size/2.0))

...          # draw the circles around keypoints with the keypoints size
...          cv2.circle(img, center, radius, color, 1, cv2.LINE_AA)

...          # draw orientation of the keypoint, if it is applicable
...          if keypoint.angle != -1:

...              srcAngleRad = keypoint.angle * np.pi/180.0

...              orient = (int(round(np.cos(srcAngleRad)*radius)), \
                 int(round(np.sin(srcAngleRad)*radius)))

...              cv2.line(img, center, (center[0]+orient[0],\
                               center[1]+orient[1]),\
                 color, 1, cv2.LINE_AA)
...          else:
...              # draw center with R=1
...              radius = 1 * draw_multiplier
...              cv2.circle(img, center, radius,\
                  color, 1, cv2.LINE_AA)

...          return img
In [30]: from random import randint
...      def drawKeypoints(image, keypoints):
...          for keypoint in keypoints:
...              color = (randint(0,256),randint(0,256),randint(0,256))
...              image = drawKeypoint(image, keypoint, color)
...          return image
  1. 现在我们使用这个函数来绘制检测到的关键点:
In [31]: result = drawKeypoints(img, keyPoints)
In [32]: print("Number of keypoints = {}".format(len(keyPoints)))
Out[32]: Number of keypoints = 453
In [33]: plt.figure(figsize=(18,9))
...      plt.imshow(result)

我们得到了以下输出:

这真的很棒,对吧?

BRIEF 既简单又快速,但它不适用于图像的旋转。您可以尝试旋转图像(更多信息请参阅www.pyimagesearch.com/2017/01/02/rotate-images-correctly-with-opencv-and-python/),然后运行 BRIEF。让我们看看 ORB 如何帮助我们解决这个问题。

使用 Oriented FAST and Rotated BRIEF (ORB)

说到个人观点,我非常喜爱 ORB。它是免费的,是 SIFT 和 SURF 的良好替代品,这两者都受到专利法的保护。ORB 实际上比 SURF 表现得更好。值得注意的是,Gary Bradski 是论文《ORB:SIFT 和 SURF 的有效替代品》的作者之一。你能想出这有什么有趣的地方吗?搜索 Gary Bradski 和 OpenCV,你将找到答案。

整个过程大致相同,所以让我们快速浏览一下代码:

In [34]: img = cv2.imread('data/rubic-cube.png')...      gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)In [35]: orb = cv2.ORB_create()In [36]: keyPoints = orb.detect(gray,None)In [37]: keyPoints, descriptors ...

摘要

在本章中,我们深入探讨了机器学习中的特征工程技术,重点关注特征选择和特征提取。我们成功地对数据进行格式化、清理和转换,使其能够被常见的机器学习算法理解。我们了解了维度诅咒,并在 OpenCV 中实现了 PCA 进行了一些维度约简的尝试。最后,我们简要游览了 OpenCV 为图像数据提供的常见特征提取技术。

拥有这些技能在手,我们现在可以应对任何类型的数据,无论是数值型、分类型、文本型还是图像型数据。当我们遇到缺失数据时,我们清楚知道该怎么做,也知道如何转换我们的数据以适应我们偏好的机器学习算法。

在下一章中,我们将迈出下一步,讨论一个具体的用例,即如何使用我们新获得的知识通过决策树进行医学诊断。

第二部分:OpenCV 操作

本节重点介绍高级机器学习概念以及如何使用 OpenCV 和 scikit-learn 来实现这些概念。我们将涵盖一些高级机器学习概念,例如决策树、支持向量机和贝叶斯学习,然后最后讨论第二种机器学习问题——无监督学习。

本节包括以下章节:

  • 第五章,使用决策树进行医疗诊断

  • 第六章,使用支持向量机检测行人

  • 第七章,使用贝叶斯学习实现垃圾邮件过滤器

  • 第八章,使用无监督学习发现隐藏结构

第五章:使用决策树进行医疗诊断

现在我们已经知道如何处理各种形状和形式的数据,无论是数值、分类、文本还是图像数据,现在是时候将我们新获得的知识付诸实践了。

在本章中,我们将学习如何构建一个可以进行医疗诊断的机器学习系统。我们可能不是医生,但我们在生活中某个时刻可能都去看过医生。通常,医生会尽可能多地获取有关患者病史和症状的信息,以便做出明智的诊断。我们将借助所谓的决策树来模拟医生的决策过程。我们还将涵盖基尼系数、信息增益和方差减少,以及过拟合和剪枝。

决策树是一个简单但强大的监督学习算法,类似于流程图;我们将在下一分钟内更多地讨论这一点。除了在医学领域,决策树还广泛应用于天文学(例如,从哈勃太空望远镜图像中过滤噪声或对星系团进行分类)、制造和生产(例如,波音公司用于发现制造过程中的缺陷)以及物体识别(例如,用于识别 3D 物体)等领域。

在本章中,我们特别想了解以下内容:

  • 从数据中构建简单的决策树,并使用它们进行分类或回归

  • 使用基尼系数、信息增益和方差减少来决定下一步要做的决策

  • 剪枝决策树及其好处

但首先,让我们来谈谈决策树实际上是什么。

技术要求

您可以从以下链接中找到本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter05

下面是软件和硬件要求的总结:

  • 您需要 OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都可以正常工作)。

  • 您需要 Python 版本 3.6(任何 3.x 版本的 Python 都可以)。

  • 您需要 Anaconda Python 3 来安装 Python 和所需的模块。

  • 您可以使用任何操作系统——macOS、Windows 和基于 Linux 的操作系统,以及这本书。我们建议您的系统至少有 4 GB 的 RAM。

  • 您不需要 GPU 来运行本书提供的代码。

理解决策树

决策树是一个简单但强大的监督学习模型。正如其名所示,我们可以将其想象为一棵树,信息沿着不同的分支流动——从树干开始,一直延伸到单个叶子,在每个节点做出关于选择哪个分支的决定。

这基本上就是一个决策树!下面是一个简单的决策树示例:

图片

决策树由关于数据(也称为决策节点)及其可能后果的一系列问题或测试的层次结构组成。

构建决策树的真正困难之一是如何从数据中提取合适的特征。为了使这一点更清楚,让我们用一个具体的例子来说明。假设我们有一个由单个电子邮件组成的数据集:

In [1]: data = [
...       'I am Mohammed Abacha, the son of the late Nigerian Head of '
...       'State who died on the 8th of June 1998\. Since i have been '
...       'unsuccessful in locating the relatives for over 2 years now '
...       'I seek your consent to present you as the next of kin so '
...       'that the proceeds of this account valued at US$15.5 Million '
...       'Dollars can be paid to you. If you are capable and willing '
...       'to assist, contact me at once via email with following '
...       'details: 1\. Your full name, address, and telephone number. '
...       '2\. Your Bank Name, Address. 3.Your Bank Account Number and '
...       'Beneficiary Name - You must be the signatory.'
...     ]

这封电子邮件可以像我们在上一章中做的那样,使用 scikit-learn 的CountVectorizer进行向量化:

In [2]: from sklearn.feature_extraction.text import CountVectorizer
... vec = CountVectorizer()
... X = vec.fit_transform(data)

从上一章,我们知道我们可以使用以下函数查看X中的特征名称:

In [3]: function:vec.get_feature_names()[:5]
Out[3]: ['15', '1998', '8th', 'abacha', 'account']

为了清晰起见,我们只关注前五个词,这些词按字母顺序排序。然后,可以找到相应的出现次数如下:

In [4]: X.toarray()[0, :5]
Out[4]: array([1, 1, 1, 1, 2], dtype=int64)

这告诉我们,五分之四的词在电子邮件中只出现了一次,但account这个词(在Out[3]中列出的最后一个词)实际上出现了两次。在上一章中,我们输入了X.toarray()来将稀疏数组X转换为人类可读的数组。结果是二维数组,其中行对应于数据样本,列对应于前一个命令中描述的特征名称。由于数据集中只有一个样本,我们只限于数组的第 0 行(即第一个数据样本)和数组的前五列(即前五个词)。

那么,我们如何检查这封电子邮件是否来自尼日利亚王子?

做这件事的一种方法就是看看电子邮件中是否同时包含nigerianprince这两个词:

In [5]: 'nigerian' in vec.get_feature_names()
Out[5]: True
In [6]: 'prince' in vec.get_feature_names()
Out[6]: False

令我们惊讶的是,我们发现prince这个词在电子邮件中并没有出现。

这是否意味着这条消息是合法的?

不,当然不是。电子邮件中没有使用“王子”,而是用了“国家元首”这样的词,有效地绕过了我们过于简单的垃圾邮件检测器。

同样,我们甚至不知道如何开始建模树中的第二个决策:想要我给他汇款吗? 文本中没有直接回答这个问题的特征。因此,这是一个特征工程问题,即以这种方式组合消息中实际出现的单词,以便我们能够回答这个问题。当然,一个好的迹象是寻找像US$money这样的字符串,但这样我们仍然不知道这些词被提及的上下文。据我们所知,也许它们是句子的一部分:别担心,我不想让你给我汇款。

更糟糕的是,结果发现我们提问的顺序实际上可以影响最终结果。例如,如果我们先问最后一个问题:我实际上认识一个尼日利亚王子吗? 假设我们有一个尼日利亚王子作为叔叔,那么在电子邮件中找到“尼日利亚王子”这个词可能就不再可疑了。

如您所见,这个看似简单的例子很快就变得难以控制了。

幸运的是,决策树背后的理论框架帮助我们找到正确的决策规则以及下一步要解决的问题。

然而,为了理解这些概念,我们必须深入挖掘。

构建我们的第一个决策树

我想我们已经准备好了一个更复杂的例子。如之前所承诺的,现在让我们进入医疗领域。

让我们考虑一个例子,其中几位患者患了同一种疾病,比如一种罕见的厌食症。让我们进一步假设,直到今天,这种疾病的真正原因仍然未知,而我们所能获得的所有信息都是一些生理测量值。例如,我们可能可以访问以下信息:

  • 患者的血压(BP

  • 患者的胆固醇水平(cholesterol

  • 患者的性别(sex

  • 患者的年龄(age

  • 患者的血钠浓度(Na

  • 患者的血钾浓度(K

基于所有...

生成新数据

在进行下一步之前,让我们快速了解每个机器学习工程师都非常关键的一步——数据生成。我们知道所有机器学习和深度学习技术都需要大量的数据——简单来说:越大越好。但如果你没有足够的数据呢?好吧,你可能会得到一个不够准确的模型。如果你无法生成任何新数据,常用的技术是将大部分数据用于训练。这种方法的重大缺点是,你得到的模型没有泛化能力,或者说,遭受了过拟合。

解决前面问题的一个方案是生成新的数据,或者通常所说的合成数据。这里需要注意的是,合成数据应该具有与真实数据相似的特征。它们与真实数据越相似,对作为机器学习工程师的你来说就越好。这种技术被称为数据增强,其中我们使用各种技术,如旋转和镜像图像,来生成基于现有数据的新数据。

由于我们在这里处理的是一个假设案例,我们可以编写简单的 Python 代码来生成随机数据——因为我们这里没有固定的特征。在现实世界的案例中,你会使用数据增强来生成看起来真实的新数据样本。让我们看看我们如何处理我们的案例。

在这里,数据集实际上是一个字典列表,其中每个字典代表一个单独的数据点,包含一个患者的血液检查结果、年龄和性别,以及所开具的药物。因此,我们知道我们想要创建新的字典,也知道在这个字典中要使用的键。接下来要关注的是字典中值的类型。

我们从年龄开始,它是一个整数,然后是性别,它可以是MF。类似地,对于其他值,我们可以推断数据类型,在某些情况下,运用常识,我们甚至可以推断出应该使用的值的范围。

非常重要的是要注意,常识和深度学习大多数时候并不兼容。这是因为你希望你的模型能够理解何时某个值是异常值。例如,我们知道一个人年龄为 130 岁是非常不可能的,但一个通用的模型应该理解这个值是异常值,不应该被考虑在内。这就是为什么你应该始终保留一小部分具有这种不合理值的数据。

让我们看看我们如何为我们的案例生成一些合成数据:

import random

def generateBasorexiaData(num_entries):
    # We will save our new entries in this list 
    list_entries = []
    for entry_count in range(num_entries):
        new_entry = {}
        new_entry['age'] = random.randint(20,100)
        new_entry['sex'] = random.choice(['M','F'])
        new_entry['BP'] = random.choice(['low','high','normal'])
        new_entry['cholestrol'] = random.choice(['low','high','normal'])
        new_entry['Na'] = random.random()
        new_entry['K'] = random.random()
        new_entry['drug'] = random.choice(['A','B','C','D'])
        list_entries.append(new_entry)
    return list_entries

如果我们想生成五个新的条目,可以使用entries = generateBasorexiaData (5)调用前面的函数。

现在我们知道了如何生成数据,让我们看看我们可以用这些数据做什么。我们能弄清楚医生为什么开药ABCD吗?我们能看到患者的血液值和医生所开药物之间的关系吗?

很可能,这个问题对你来说和对我来说一样难以回答。尽管数据集乍一看可能看起来是随机的,但实际上,我已经在患者血液值和所开药物之间建立了一些明确的关系。让我们看看决策树能否揭示这些隐藏的关系。

通过理解数据来理解任务

解决一个新的机器学习问题时的第一步总是什么?

你说得完全正确:对数据的感知。我们越了解数据,就越能理解我们试图解决的问题。在我们的未来努力中,这也有助于我们选择合适的机器学习算法。

首先要意识到的是,drug列实际上并不是像其他列那样的特征值。由于我们的目标是根据患者的血液值预测将被开处的药物,drug列实际上变成了目标标签。换句话说,我们机器学习算法的输入将是血液值、年龄和性别 ...

数据预处理

为了让我们的数据能够被决策树算法理解,我们需要将所有分类特征(sexBPcholesterol)转换为数值特征。最好的方法是什么?

正确:我们使用 scikit-learn 的DictVectorizer。就像我们在上一章中做的那样,我们将要转换的数据集输入到fit_transform方法中:

In [10]: from sklearn.feature_extraction import DictVectorizer
...      vec = DictVectorizer(sparse=False)
...      data_pre = vec.fit_transform(data)

然后,data_pre包含预处理后的数据。如果我们想查看第一个数据点(即data_pre的第一行),我们将特征名称与相应的特征值进行匹配:

In [12]: vec.get_feature_names()
Out[12]: ['BP=high', 'BP=low', 'BP=normal', 'K', 'Na', 'age',
...       'cholesterol=high', 'cholesterol=normal',
...       'sex=F', 'sex=M']
In [13]: data_pre[0]
Out[13]: array([ 1\. , 0\. , 0\. , 0.06, 0.66, 33\. , 1\. , 0\. ,
                 1\. , 0\. ])

从这里,我们可以看到三个分类变量——血压(BP)、胆固醇水平(cholesterol)和性别(sex)——已经使用独热编码进行编码。

为了确保我们的数据变量与 OpenCV 兼容,我们需要将所有内容转换为浮点值:

In [14]: import numpy as np
...      data_pre = np.array(data_pre, dtype=np.float32)
...      target = np.array(target, dtype=np.float32)

然后,剩下的工作就是像我们在第三章中做的那样,将数据分为训练集和测试集。记住,我们总是希望保持训练集和测试集的分离。由于在这个例子中我们只有 20 个数据点可以工作,我们可能需要为测试保留超过 10%的数据。在这里,15-5 的分割似乎很合适。我们可以明确地命令split函数产生恰好五个测试样本:

In [15]: import sklearn.model_selection as ms
...      X_train, X_test, y_train, y_test =
...      ms.train_test_split(data_pre, target, test_size=5,
...      random_state=42)

构建树形图

使用 OpenCV 构建决策树与第三章中提到的监督学习入门步骤非常相似。回想一下,所有的机器学习函数都位于 OpenCV 3.1 的ml模块中:

  1. 我们可以使用以下代码创建一个空的决策树:
In [16]: import cv2...      dtree = cv2.ml.dtree_create()
  1. 要在训练数据上训练决策树,我们使用train方法。这就是为什么我们之前将数据转换为浮点数的原因——这样我们就可以在train方法中使用它:
In [17]: dtree.train(X_train, cv2.ml.ROW_SAMPLE, y_train)

在这里,我们必须指定X_train中的数据样本是占据行(使用cv2.ml.ROW_SAMPLE)还是列(cv2.ml.COL_SAMPLE)。

  1. 然后,我们可以预测...的标签。

可视化训练好的决策树

如果你刚开始使用 OpenCV 的决策树实现,并且不太关心底层的工作原理,那么它的实现已经足够好了。然而,在接下来的几节中,我们将转向 scikit-learn。它的实现允许我们自定义算法,使得调查树的内部工作原理变得容易得多。它的文档也更好。

在 scikit-learn 中,决策树可以用于分类和回归。它们位于tree模块中:

  1. 让我们先从sklearn导入tree模块:
In [21]: from sklearn import tree
  1. 与 OpenCV 类似,我们接下来使用DecisionTreeClassifier构造函数创建一个空的决策树:
In [22]: dtc = tree.DecisionTreeClassifier()
  1. 然后,可以使用fit方法训练树:
In [23]: dtc.fit(X_train, y_train)
Out[23]: DecisionTreeClassifier(class_weight=None, criterion='gini',
            max_depth=None, max_features=None, max_leaf_nodes=None,
            min_impurity_split=1e-07, min_samples_leaf=1,
            min_samples_split=2, min_weight_fraction_leaf=0.0,
            presort=False, random_state=None, splitter='best')
  1. 然后,我们可以使用score方法在训练集和测试集上计算准确度得分:
In [24]: dtc.score(X_train, y_train)
Out[24]: 1.0
In [25]: dtc.score(X_test, y_test)
Out[25]: 0.40000000000000002

现在,这里有个有趣的事情:如果你想了解树的外观,你可以使用 GraphViz 从树结构创建一个 PDF 文件(或任何其他支持的文件类型)。为了使这成为可能,你首先需要安装 GraphViz。不用担心,因为它已经包含在我们这本书开头创建的环境中。

  1. 然后,回到 Python 中,你可以使用 scikit-learn 的export_graphviz导出器将树以 GraphViz 格式导出到一个文件,例如tree.dot
In [26]: with open("tree.dot", 'w') as f:
... tree.export_graphviz(clf, out_file=f)
  1. 然后,回到命令行,你可以使用 GraphViz 将tree.dot转换为(例如)一个 PNG 文件:
$ dot -Tpng tree.dot -o tree.png

或者,你也可以指定-Tpdf或任何其他支持的图像格式。前一个树的结果看起来像这样:

图片

这一切意味着什么?让我们一步一步地分解这个图表。

探究决策树的内部工作原理

我们之前已经建立了一个决策树基本上是一个做出一系列数据决策的流程图。这个过程从根节点开始(这是最顶端的节点),根据某些决策规则将数据分成两组(仅适用于二叉树),然后重复这个过程,直到所有剩余的样本都具有相同的目标标签,此时我们就到达了一个叶节点。

在之前的垃圾邮件过滤器示例中,决策是通过提出真/假问题来做出的。例如,我们询问一封电子邮件是否包含某个特定的单词。如果包含,我们就沿着标记为 true 的边前进,并提出下一个问题。然而,这不仅仅适用于分类特征,...

评估特征的重要性

我还没有告诉你的事情是,你如何选择用于分割数据的特征。前面的根节点根据 Na <= 0.72 来分割数据,但谁告诉这棵树首先关注钠呢?此外,0.72 这个数字又是从哪里来的呢?

显然,某些特征可能比其他特征更重要。实际上,scikit-learn 提供了一个评估特征重要性的函数,该函数为每个特征提供一个介于 01 之间的数值,其中 0 表示在所有决策中完全没有使用,而 1 表示完美预测目标。特征重要性被归一化,以便它们的总和为 1:

In [27]: dtc.feature_importances_
Out[27]: array([ 0\.        , 0\.   , 0\.        , 0.13554217, 0.29718876,
                 0.24096386, 0\.   , 0.32630522, 0\.        , 0\. ])

如果我们回想一下特征名称,就会变得清楚哪个特征似乎是最重要的。一个图表可能最有信息量:

In [28]: plt.barh(range(10), dtc.feature_importances_, align='center',
...      tick_label=vec.get_feature_names())

这将导致以下条形图:

图片

现在,很明显,最重要的特征是知道给患者服用哪种药物,实际上是否患者有正常的胆固醇水平。年龄、钠水平和钾水平也很重要。另一方面,性别和血压似乎没有任何区别。然而,这并不意味着性别或血压是无信息的。这只意味着这些特征没有被决策树选中,可能是因为另一个特征会导致相同的分割。

但是,等等。如果胆固醇水平如此重要,为什么它没有被选为树中的第一个特征(即根节点)?为什么你会选择首先根据钠水平进行分割?这就是我需要告诉你之前图中那个不祥的 gini 标签的地方。

特征重要性告诉我们哪些特征对分类很重要,但并不告诉我们它们指示的是哪个类标签。例如,我们只知道胆固醇水平很重要,但我们不知道它是如何导致开处不同药物的。实际上,特征和类别之间可能没有简单的关联。

理解决策规则

要构建完美的树,你希望在最有信息量的特征处分割树,从而得到最纯净的女儿节点。然而,这个简单的想法带来了一些实际挑战:

  • 实际上并不清楚什么是最具信息量的。我们需要一个具体的值,一个评分函数,或者一个可以描述特征信息量的数学方程。

  • 为了找到最佳的分割点,我们必须在每个决策节点搜索所有可能性。

幸运的是,决策树算法实际上为你做了这两步。scikit-learn 支持的两个最常用的标准如下:

  • criterion='gini':基尼不纯度是误分类的度量,目的是...

控制决策树的复杂性

如果你继续生长树直到所有叶子都是纯净的,你通常会得到一个过于复杂的树,难以解释。纯净叶子的存在意味着树在训练数据上 100%正确,就像我们之前展示的树一样。因此,该树在测试数据集上的表现可能非常差,就像我们之前展示的树一样。我们说树对训练数据过拟合了。

避免过拟合有两种常见的方法:

  • 预剪枝:这是在早期停止树创建的过程。

  • 后剪枝(或简称剪枝):这是首先构建树,然后移除或合并只包含少量信息的节点的过程。

有几种方法可以预剪枝一棵树,所有这些都可以通过传递可选参数给DecisionTreeClassifier构造函数来实现:

  • 通过max_depth参数限制树的最大深度

  • 通过max_leaf_nodes限制最大叶节点数

  • 通过min_samples_split要求节点中至少有足够多的点以继续分割

通常预剪枝足以控制过拟合。

在我们的玩具数据集上试一试!你能否提高测试集上的分数?当你开始调整早期参数时,树布局是如何变化的?

在更复杂的现实世界场景中,预剪枝不再足够用来控制过拟合。在这种情况下,我们希望将多个决策树组合成所谓的随机森林。我们将在第十章集成分类方法中讨论这一点。

使用决策树诊断乳腺癌

现在我们已经构建了我们的第一个决策树,是时候将我们的注意力转向一个真实的数据集:威斯康星乳腺癌数据集 (archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+(Diagnostic))。

这个数据集是医学影像研究的一个直接结果,并且现在被认为是经典的。这个数据集是从健康(良性)和癌变(恶性)组织的数字化图像中创建的。不幸的是,我无法找到原始研究中任何公有领域的示例,但图像看起来与以下截图相似:

截图

研究的目的是对组织进行分类...

加载数据集

整个数据集是 scikit-learn 示例数据集的一部分。我们可以使用以下命令导入它:

  1. 首先,让我们使用load_breast_cancer函数加载数据集:
In [1]: from sklearn import datasets
...     data = datasets.load_breast_cancer()
  1. 如前所述,所有数据都包含在一个 2D 特征矩阵data.data中,其中行表示数据样本,列是特征值:
In [2]: data.data.shape
Out[2]: (569, 30)
  1. 通过查看提供的特征名称,我们认出了一些我们之前提到过的:
In [3]: data.feature_names
Out[3]: array(['mean radius', 'mean texture', 'mean perimeter',
               'mean area', 'mean smoothness', 'mean compactness',
               'mean concavity', 'mean concave points',
               'mean symmetry', 'mean fractal dimension',
               'radius error', 'texture error', 'perimeter error',
               'area error', 'smoothness error',
               'compactness error', 'concavity error',
               'concave points error', 'symmetry error',
               'fractal dimension error', 'worst radius',
               'worst texture', 'worst perimeter', 'worst area',
               'worst smoothness', 'worst compactness',
               'worst concavity', 'worst concave points',
               'worst symmetry', 'worst fractal dimension'], 
              dtype='<U23')
  1. 由于这是一个二元分类任务,我们预计会找到恰好两个目标名称:
In [4]: data.target_names
Out[4]: array(['malignant', 'benign'], dtype='<U9')
  1. 让我们保留所有数据样本的 20%用于测试:
In [5]: import sklearn.model_selection as ms
...     X_train, X_test, y_train, y_test =
...     ms.train_test_split(data_pre, target, test_size=0.2,
...     random_state=42)
  1. 你当然可以选择不同的比例,但最常见的是使用 70-30、80-20 或 90-10。这取决于数据集的大小,但最终不应有太大的差异。将数据分割为 80-20 应产生以下集合大小:
In [6]: X_train.shape, X_test.shape
Out[6]: ((455, 30), (114, 30))

构建决策树

如前所述,我们可以使用 scikit-learn 的tree模块创建一个决策树。现在,让我们不要指定任何可选参数:

  1. 我们将首先创建一个决策树:
In [5]: from sklearn import tree...     dtc = tree.DecisionTreeClassifier()
  1. 你还记得如何训练决策树吗?我们将使用fit函数来完成:
In [6]: dtc.fit(X_train, y_train)Out[6]: DecisionTreeClassifier(class_weight=None, criterion='gini',                               max_depth=None, max_features=None,                               max_leaf_nodes=None,                               min_impurity_split=1e-07,                               min_samples_leaf=1,                               min_samples_split=2,                               min_weight_fraction_leaf=0.0,                               presort=False, random_state=None,                               splitter='best')
  1. 由于我们没有指定任何预剪枝参数,我们预计这...

使用决策树进行回归

尽管我们迄今为止一直专注于在分类任务中使用决策树,但你也可以将其用于回归。但你需要再次使用 scikit-learn,因为 OpenCV 不提供这种灵活性。因此,我们在这里将只简要回顾其功能:

  1. 假设我们想使用决策树来拟合一个 sin 波。为了使事情更有趣,我们还将使用 NumPy 的随机数生成器对数据点添加一些噪声:
In [1]: import numpy as np
...     rng = np.random.RandomState(42)
  1. 然后,我们创建 100 个在 0 到 5 之间的随机间隔的x值,并计算相应的 sin 值:
In [2]: X = np.sort(5 * rng.rand(100, 1), axis=0)
...     y = np.sin(X).ravel()
  1. 然后,我们在y中的每个其他数据点添加噪声(使用y[::2]),并按0.5的比例缩放,这样我们不会引入太多的抖动:
In [3]: y[::2] += 0.5 * (0.5 - rng.rand(50))
  1. 然后,你可以创建一个像其他树一样的回归树。

一个小的不同之处在于,ginientropy分割标准不适用于回归任务。相反,scikit-learn 提供了两种不同的分割标准:

    • mse(也称为方差减少):此标准计算真实值和预测值之间的平均平方误差MSE),并分割导致最小 MSE 的节点。

    • mae:此标准计算真实值和预测值之间的平均绝对误差MAE),并分割导致最小 MAE 的节点。

  1. 使用 MSE 标准,我们将构建两个树。让我们首先构建一个深度为 2 的树:
In [4]: from sklearn import tree
In [5]: regr1 = tree.DecisionTreeRegressor(max_depth=2,
...     random_state=42)
...     regr1.fit(X, y)
Out[5]: DecisionTreeRegressor(criterion='mse', max_depth=2,
                              max_features=None, max_leaf_nodes=None,
                              min_impurity_split=1e-07,
                              min_samples_leaf=1, min_samples_split=2,
                              min_weight_fraction_leaf=0.0,
                              presort=False, random_state=42,
                              splitter='best')
  1. 接下来,我们将构建一个最大深度为 5 的决策树:
In [6]: regr2 = tree.DecisionTreeRegressor(max_depth=5,
...     random_state=42)
...     regr2.fit(X, y)
Out[6]: DecisionTreeRegressor(criterion='mse', max_depth=5,
                              max_features=None, max_leaf_nodes=None,
                              min_impurity_split=1e-07,
                              min_samples_leaf=1, min_samples_split=2,
                              min_weight_fraction_leaf=0.0,
                              presort=False, random_state=42,
                              splitter='best')

然后,我们可以像第三章中的线性回归器一样使用决策树。

  1. 为了这个目的,我们创建了一个测试集,其中x值在整个 0 到 5 的范围内密集采样:
In [7]: X_test = np.arange(0.0, 5.0, 0.01)[:, np.newaxis]
  1. 可以使用predict方法获得预测的y值:
In [8]: y_1 = regr1.predict(X_test)
...     y_2 = regr2.predict(X_test)
  1. 如果我们将所有这些放在一起绘制,我们可以看到决策树之间的差异:
In [9]: import matplotlib.pyplot as plt
... %matplotlib inline
... plt.style.use('ggplot')

... plt.scatter(X, y, c='k', s=50, label='data')
... plt.plot(X_test, y_1, label="max_depth=2", linewidth=5)
... plt.plot(X_test, y_2, label="max_depth=5", linewidth=3)
... plt.xlabel("data")
... plt.ylabel("target")
... plt.legend()
Out[9]: <matplotlib.legend.Legend at 0x12d2ee345f8>

这将产生以下图表:

图片

在这里,粗红色的线代表深度为 2 的回归树。你可以看到树是如何通过这些粗略的步骤来近似数据的。较细的蓝色线属于深度为 5 的回归树;增加的深度使得树能够进行更多更精细的近似。因此,这棵树可以更好地近似数据。然而,由于这种增加的力量,树也更易受到拟合噪声值的影响,尤其是在图表右侧的尖峰中可以明显看出。

摘要

在本章中,我们学习了所有关于决策树的知识以及如何将它们应用于分类和回归任务。我们简要讨论了数据生成、过拟合以及通过调整预剪枝和后剪枝设置来避免这种现象的方法。我们还学习了如何使用基尼不纯度和信息增益等指标来评估节点分裂的质量。最后,我们将决策树应用于医学数据以检测癌组织。我们将在本书的末尾回到决策树,届时我们将多个树组合成所谓的随机森林。但现在,让我们继续探讨新的主题。

在下一章中,我们将介绍机器学习世界中的另一个基本概念:支持向量...

第六章:使用支持向量机检测行人

在上一章中,我们讨论了如何使用决策树进行分类和回归。在这一章中,我们想要将注意力转向机器学习世界中另一个已确立的监督学习器:支持向量机(SVMs)。在 1990 年代初引入后不久,SVMs 迅速在机器学习社区中流行起来,这主要归功于它们在早期手写数字分类中的成功。它们至今仍然相关,尤其是在应用领域,如计算机视觉。

本章的目标是将支持向量机(SVMs)应用于计算机视觉中一个流行的问题:行人检测。与识别任务(我们命名对象的类别)不同,检测任务的目标是判断特定对象(在我们的案例中,是行人)是否在图像中存在。你可能已经知道 OpenCV 可以在两到三行代码中完成这项任务。但是,如果我们那样做,我们将一无所获。所以,我们将从头开始构建整个流程!我们将获取一个真实世界的数据集,使用方向梯度直方图(HOG)进行特征提取,并将其应用于 SVM。

在本章中,我们将使用 Python 在 OpenCV 中实现 SVMs。我们将学习如何处理非线性决策边界并理解核技巧。到本章结束时,我们将学习如何在野外检测行人。

在此过程中,我们将涵盖以下主题:

  • 使用 Python 在 OpenCV 中实现 SVMs

  • 处理非线性决策边界

  • 理解核技巧

  • 在野外检测行人

激动吗?那么让我们开始吧!

技术要求

您可以从以下链接获取本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter06.

这里是软件和硬件要求的简要总结:

  • OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都完全可以工作)。

  • Python 版本 3.6(任何 3.x 版本的 Python 都可以)。

  • 安装 Python 和所需模块的 Anaconda Python 3。

  • 您可以使用任何操作系统——macOS、Windows 和基于 Linux 的操作系统——使用这本书。我们建议您系统至少有 4GB RAM。

  • 您不需要 GPU 来运行书中提供的代码。

理解线性 SVMs

为了理解 SVMs 是如何工作的,我们必须考虑决策边界。当我们使用早期章节中的线性分类器或决策树时,我们的目标始终是尽量减少分类错误。我们通过使用均方误差来评估准确性来实现这一点。SVM 试图实现低分类错误,但它只是隐式地这样做。SVM 的明确目标是最大化数据点之间的间隔

学习最优决策边界

让我们来看一个简单的例子。考虑一些只有两个特征(x值和y值)和相应的目标标签(正(+)或负(-))的训练样本。由于标签是分类的,我们知道这是一个分类任务。此外,因为我们只有两个不同的类别(+和-),所以这是一个二分类任务。

在二分类任务中,决策边界是一条将训练集分割成两个子集的线,每个类别一个。一个最优决策****边界将数据分割,使得一个类别的所有数据样本(比如+)位于决策边界左侧,而所有其他数据样本(比如-)位于其右侧。

SVM 更新其决策选择 ...

实现我们的第一个 SVM

但理论就到这里吧。让我们来写一些代码!

可能有一个好主意,放慢我们的步伐。对于我们的第一个 SVM,我们可能应该专注于一个简单的数据集,也许是一个二分类任务。

关于 scikit-learn 的datasets模块的一个酷技巧,我没有告诉你的是,你可以生成具有可控大小和复杂度的随机数据集。以下是一些值得注意的例子:

  • datasets.make_classification([n_samples, ...]): 这个函数生成一个随机的n类分类问题,其中我们可以指定样本数量、特征数量和目标标签数量。

  • datasets.make_regression([n_samples, ...]): 这个函数生成一个随机的回归问题。

  • datasets.make_blobs([n_samples, n_features, ...]): 这个函数生成一系列高斯云团,我们可以用它们来进行聚类。

这意味着我们可以使用make_classification为二分类任务构建一个自定义数据集。

生成数据集

就像我们现在可以睡梦中背诵一样,一个二分类问题恰好有两个不同的目标标签(n_classes=2)。为了简化,让我们只限制自己使用两个特征值(n_features=2;例如,一个x值和一个y值)。假设我们想要创建 100 个数据样本:

In [1]: from sklearn import datasets...     X, y = datasets.make_classification(n_samples=100, n_features=2,...                                         n_redundant=0, n_classes=2,...                                         random_state=7816)

我们期望X有 100 行(数据样本)和 2 列(特征),而y向量应该只有一个列,包含所有目标标签:

In [2]: X.shape, y.shapeOut[2]: ((100, 2), (100,))

可视化数据集

我们可以使用 Matplotlib 将这些数据点绘制在散点图上。在这里,我们的想法是将x值(在X的第一个列中找到,X[:, 0])与y值(在X的第二个列中找到,X[:, 1])进行绘图。一个巧妙的技巧是将目标标签作为颜色值传递(c=y):

In [3]: import matplotlib.pyplot as plt
...     %matplotlib inline
...     plt.scatter(X[:, 0], X[:, 1], c=y, s=100)
...     plt.xlabel('x values')
...     plt.ylabel('y values')
Out[3]: <matplotlib.text.Text at 0x24f7ffb00f0>

这将产生以下输出:

图片

前面的输出显示了用于二分类问题的随机生成数据。你可以看到,大部分数据点的两个类别是明显分开的。然而,有几个区域(尤其是图表的左下角附近)两个类别的数据点交织在一起。这些区域将很难正确分类,正如我们将在下一秒看到的那样。

预处理数据集

下一步是将数据点划分为训练集和测试集,就像我们之前做的那样。但在我们这样做之前,我们必须按照以下方式准备数据:

  • X中的所有特征值必须是 32 位浮点数

  • 目标标签必须是-1 或+1

我们可以通过以下代码实现这一点:

In [4]: import numpy as np...     X = X.astype(np.float32)...     y = y * 2 - 1

现在,我们可以像在早期章节中做的那样,将数据传递给 scikit-learn 的train_test_split函数:

In [5]: from sklearn import model_selection as ms...     X_train, X_test, y_train, y_test = ms.train_test_split(...         X, y, test_size=0.2, random_state=42...     )

在这里,我选择保留所有数据点的 20%作为测试集,但...

构建支持向量机

在 OpenCV 中,SVM 的构建、训练和评分与迄今为止我们遇到的所有其他学习算法完全相同,使用以下四个步骤:

  1. 调用create方法来构建一个新的 SVM:
In [6]: import cv2
...     svm = cv2.ml.SVM_create()

如以下命令所示,有几种不同的modes可以操作 SVM。目前,我们只关心之前讨论过的例子:一个试图用直线划分数据的 SVM。这可以通过setKernel方法指定:

In [7]: svm.setKernel(cv2.ml.SVM_LINEAR)
  1. 调用分类器的train方法以找到最佳决策边界:
In [8]: svm.train(X_train, cv2.ml.ROW_SAMPLE, y_train)
      Out[8]: True
  1. 调用分类器的predict方法来预测测试集中所有数据样本的目标标签:
In [9]: _, y_pred = svm.predict(X_test)
  1. 使用 scikit-learn 的metrics模块对分类器进行评分:
In [10]: from sklearn import metrics
...      metrics.accuracy_score(y_test, y_pred)
Out[10]: 0.80000000000000004

恭喜,我们得到了 80%正确分类的测试样本!

当然,到目前为止,我们并不知道内部发生了什么。据我们所知,我们可能只是从网络搜索中获取了这些命令,并将它们输入到终端中,而实际上并不了解我们在做什么。但这不是我们想要成为的人。让系统工作是一回事,而理解它是另一回事。让我们来了解这一点!

可视化决策边界

在尝试理解我们的数据时是正确的,对于尝试理解我们的分类器也是正确的:可视化是理解系统的第一步。我们知道 SVM 以某种方式找到了一个决策边界,使我们能够正确分类 80%的测试样本。但我们如何找出这个决策边界实际上是什么样子呢?

为了做到这一点,我们将从 scikit-learn 背后的那些人那里借用一个技巧。想法是生成一个细密的xy坐标网格,并通过 SVM 的predict方法运行它。这将使我们能够知道,对于每个(x, y)点,分类器会预测什么目标标签。

我们将在一个专门的函数中完成这项工作,我们称之为plot_decision_boundary ...

处理非线性决策边界

如果数据无法使用线性决策边界进行最佳划分,会怎样呢?在这种情况下,我们说数据是不可线性划分的.

处理不可线性划分的数据的基本想法是创建原始特征的非线性组合。这相当于说我们希望将我们的数据投影到一个更高维的空间(例如,从 2D 到 3D),在那里数据突然变得线性可分。

这个概念在以下图中得到了说明:

图片

上述图示展示了如何在更高维空间中找到线性超平面。如果原始输入空间(左侧)中的数据不能线性分离,我们可以应用一个映射函数ϕ(.),将数据从 2D 投影到 3D(或高维)空间。在这个更高维的空间中,我们可能会发现现在有一个线性决策边界(在 3D 中是一个平面),可以分离数据。

在一个n维空间中的线性决策边界被称为超平面。例如,在 6D 特征空间中的决策边界是一个 5D 超平面;在 3D 特征空间中,它是一个常规的 2D 平面;在 2D 空间中,它是一条直线。

然而,这种映射方法的一个问题是,在大维度中它不太实用,因为它在维度之间进行数学投影时增加了许多额外的项。这就是所谓的核技巧发挥作用的地方。

理解核技巧

当然,我们没有时间开发出真正理解核技巧所需的所有数学。一个更现实的章节标题可能是承认存在所谓的核技巧并接受它有效,但这会有些冗长。

这就是所谓的核技巧的精髓。

为了确定高维空间中决策超平面的斜率和方向,我们必须将所有特征值与适当的权重值相乘并将它们全部加起来。我们的特征空间维度越多,我们就要做更多的工作。

然而,比我们聪明的数学家早已意识到,支持向量机(SVM)没有必要在更高维空间中显式地工作...

了解我们的核

OpenCV 提供了一系列 SVM 核以供实验。其中一些最常用的包括以下内容:

  • cv2.ml.SVM_LINEAR:这是我们之前使用的核。它提供了原始特征空间(xy值)中的线性决策边界。

  • cv2.ml.SVM_POLY:这个核提供了一个在原始特征空间中的多项式函数作为决策边界。为了使用这个核,我们还需要通过svm.setCoef0(通常设置为0)指定一个系数,并通过svm.setDegree指定多项式的次数。

  • cv2.ml.SVM_RBF:这个核实现了我们之前讨论过的那种高斯函数。

  • cv2.ml.SVM_SIGMOID:这个核实现了与我们在第三章中讨论逻辑回归时遇到的 sigmoid 函数类似的功能。

  • cv2.ml.SVM_INTER:这个核是 OpenCV 3 的新增功能。它根据类之间直方图的相似性来分离类。

实现非线性 SVM

为了测试我们刚才提到的某些 SVM 核,我们将回到之前提到的代码示例。我们想要重复在之前生成的数据集上构建和训练 SVM 的过程,但这次,我们想要使用一系列不同的核:

In [13]: kernels = [cv2.ml.SVM_LINEAR, cv2.ml.SVM_INTER,...                 cv2.ml.SVM_SIGMOID, cv2.ml.SVM_RBF]

你还记得所有这些代表什么吗?

设置不同的 SVM 核相对简单。我们从kernels列表中取一个条目,并将其传递给 SVM 类的setKernels方法。就是这样。

重复事物的最懒惰的方法是使用像这里所示的for循环:

In [14]: for idx, kernel in enumerate(kernels):

然后步骤如下:...

在野外检测行人

我们简要地讨论了检测和识别之间的区别。虽然识别关注的是对对象进行分类(例如,作为行人、汽车、自行车等),但检测基本上是在回答这样一个问题:这张图片中是否有行人存在?

大多数检测算法背后的核心思想是将图像分割成许多小块,然后对每个图像块进行分类,判断其是否包含行人。这正是我们将在本节中要做的。为了达到我们自己的行人检测算法,我们需要执行以下步骤:

  1. 建立一个包含行人的图像数据库。这些将成为我们的正数据样本。

  2. 建立一个不包含行人的图像数据库。这些将成为我们的负数据样本。

  3. 在数据集上训练一个支持向量机(SVM)。

  4. 将 SVM 应用于测试图像的每个可能的块,以判断整体图像是否包含行人。

获取数据集

为了本节的目的,我们将使用麻省理工学院人群数据集,我们可以自由地用于非商业目的。所以请确保在获得相应的软件许可之前,不要将其用于您开创性的自主初创公司。

然而,如果您遵循我们之前提供的安装说明并在 GitHub 上检查了代码,您已经拥有了数据集,并准备好了!文件可以在github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/blob/master/data/chapter6/pedestrians128x64.tar.gz找到。

通过参考以下步骤,您将学习如何在野外检测行人:

  1. 由于我们打算从 Jupyter Notebook 中运行此代码,...

看一眼方向梯度直方图(HOG)

HOG 可能正是我们为了完成这个项目所需要的帮助。HOG 是一种图像特征描述符,就像我们在第四章中讨论的那样,表示数据和工程特征。它已经在计算机视觉的许多不同任务中得到了成功应用,但似乎在分类行人方面特别有效。

HOG 特征背后的基本思想是,图像中对象的局部形状和外观可以通过边缘方向的分布来描述。图像被分成小的连接区域,在这些区域内,编译了梯度方向(或边缘方向)的直方图。然后,通过连接不同的直方图来组装描述符。为了提高性能,局部直方图也可以进行对比度归一化,这有助于提高对光照和阴影变化的鲁棒性。

在 OpenCV 中,通过 cv2.HOGDescriptor 可以方便地访问 HOG 描述符,它接受一系列输入参数,例如检测窗口大小(要检测的对象的最小尺寸,48 x 96)、块大小(每个框的大小,16 x 16)、单元格大小(8 x 8)和单元格步长(从一个单元格移动到下一个单元格的像素数,8 x 8)。对于这些单元格中的每一个,HOG 描述符然后使用九个桶计算方向梯度直方图:

In [7]: win_size = (48, 96)
...     block_size = (16, 16)
...     block_stride = (8, 8)
...     cell_size = (8, 8)
...     num_bins = 9
...     hog = cv2.HOGDescriptor(win_size, block_size, block_stride,
...                             cell_size, num_bins)

虽然这个函数调用看起来相当复杂,但实际上这些是 HOG 描述符实现的唯一值。最重要的参数是窗口大小(win_size)。

剩下的只是调用 hog.compute 对我们的数据样本进行操作。为此,我们通过从我们的数据目录中随机选择行人图像来构建正样本数据集(X_pos)。在下面的代码片段中,我们从超过 900 张图片中随机选择 400 张,并应用 HOG 描述符:

In [8]: import numpy as np
...     import random
...     random.seed(42)
...     X_pos = []
...     for i in random.sample(range(900), 400):
...         filename = "%s/per%05d.ppm" % (extractdir, i)
...         img = cv2.imread(filename)
...         if img is None:
...             print('Could not find image %s' % filename)
...             continue
...         X_pos.append(hog.compute(img, (64, 64)))

我们也应该记住,OpenCV 希望特征矩阵包含 32 位浮点数,目标标签是 32 位整数。我们并不介意,因为转换为 NumPy 数组将允许我们轻松地调查我们创建的矩阵的大小:

In [9]: X_pos = np.array(X_pos, dtype=np.float32)
...     y_pos = np.ones(X_pos.shape[0], dtype=np.int32)
...     X_pos.shape, y_pos.shape
Out[9]: ((399, 1980, 1), (399,))

看起来我们总共选择了 399 个训练样本,每个样本有 1,980 个特征值(这些是 HOG 特征值)。

生成负样本

然而,真正的挑战是找到非行人的完美示例。毕竟,想到行人的示例图像很容易。但行人的对立面是什么?

这实际上是在尝试解决新的机器学习问题时遇到的一个常见问题。研究实验室和公司都花费大量时间创建和注释新的数据集,以满足他们的特定目的。

如果你遇到了难题,让我给你一个提示,如何来处理这个问题。找到一个行人的相反的一个好的初步近似是组装一个数据集,其中的图像看起来像正类图像,但不包含行人。这些图像可能包含任何像汽车、自行车、街道、房屋等的东西...

实现 SVM

我们已经知道如何在 OpenCV 中构建 SVM,所以这里没有太多可看的。提前规划,我们将训练过程封装成一个函数,这样在未来重复该过程会更方便:

In [15]: def train_svm(X_train, y_train):
...          svm = cv2.ml.SVM_create()
...          svm.train(X_train, cv2.ml.ROW_SAMPLE, y_train)
...          return svm

对于评分函数也可以这样做。在这里,我们传递一个特征矩阵,X和一个标签向量,y,但我们没有指定我们是在谈论训练集还是测试集。实际上,从函数的角度来看,数据样本属于哪个集合并不重要,只要它们有正确的格式:

In [16]: def score_svm(svm, X, y):
...          from sklearn import metrics
...          _, y_pred = svm.predict(X)
...          return metrics.accuracy_score(y, y_pred)

然后,我们可以通过两个简短的功能调用来训练和评分 SVM:

In [17]: svm = train_svm(X_train, y_train)
In [18]: score_svm(svm, X_train, y_train)
Out[18]: 1.0
In [19]: score_svm(svm, X_test, y_test)
Out[19]: 0.64615384615384619

多亏了 HOG 特征描述符,我们在训练集上没有犯错误。然而,我们的泛化性能相当糟糕(64.6%),这比训练性能(100%)低得多。这表明模型正在对数据进行过度拟合。它在训练集上的表现远好于测试集,这意味着模型已经退而求其次,只是记住训练样本,而不是试图将其抽象成一个有意义的决策规则。我们该如何提高模型性能?

对模型进行 bootstrapping

提高我们模型性能的一个有趣方法是使用bootstrapping。这个想法实际上被应用在第一篇关于使用 SVMs 结合 HOG 特征进行行人检测的论文中。所以,让我们向这些先驱者表示一点敬意,并尝试理解他们所做的事情。

他们的想法非常简单。在训练集上训练 SVM 后,他们对模型进行了评分,发现模型产生了一些误报。记住,误报意味着模型对一个实际上是负例(-)的样本预测了正例(+)。在我们的上下文中,这意味着 SVM 错误地认为一个图像包含行人。如果这种情况发生在数据集的某个特定图像中,这个例子...

在较大图像中检测行人

剩下的工作是将 SVM 分类过程与检测过程连接起来。要做到这一点,我们需要对图像中的每个可能的补丁重复我们的分类。这与我们之前可视化决策边界时所做的类似;我们创建了一个精细的网格,并对网格上的每个点进行了分类。同样的想法也适用于这里。我们将图像划分为补丁,并判断每个补丁是否包含行人。

通过遵循这些步骤,你将能够在图像中检测到行人:

  1. 我们首先必须遍历图像中所有可能的补丁,如下所示,每次将我们的感兴趣区域移动一小段stride像素:
In [23]: stride = 16
...      found = []
...      for ystart in np.arange(0, img_test.shape[0], stride):
...          for xstart in np.arange(0, img_test.shape[1], stride):
  1. 我们要确保我们不会超出图像边界:
...              if ystart + hroi > img_test.shape[0]:
...                  continue
...              if xstart + wroi > img_test.shape[1]:
...                  continue
  1. 然后,我们裁剪出 ROI,对其进行预处理,并进行分类:
...              roi = img_test[ystart:ystart + hroi,
...                             xstart:xstart + wroi, :]
...              feat = np.array([hog.compute(roi, (64, 64))])
...              _, ypred = svm.predict(feat)
  1. 如果那个特定的补丁被分类为行人,我们就将其添加到成功列表中:
...              if np.allclose(ypred, 1):
...                  found.append((ystart, xstart, hroi, wroi))
  1. 因为行人不仅可能出现在各种位置,也可能出现在各种大小,我们可能需要重新缩放图像并重复整个过程。幸运的是,OpenCV 有一个方便的函数用于这种多尺度检测任务,即detectMultiScale函数。这有点像是一个技巧,但我们可以将所有 SVM 参数传递给hog对象:
In [24]: rho, _, _ = svm.getDecisionFunction(0)
...      sv = svm.getSupportVectors()
...      hog.setSVMDetector(np.append(sv.ravel(), rho))
  1. 然后,我们可以调用检测函数:
In [25]: found = hog.detectMultiScale(img_test)

该函数将返回一个包含检测到行人的边界框列表。

这似乎只适用于线性 SVM 分类器。在这一点上,OpenCV 的文档在不同版本之间非常不一致,因此我不确定这个功能是从哪个版本开始或停止工作的。请小心!

  1. 在实践中,当人们面对标准任务,如行人检测时,他们通常会依赖于 OpenCV 中内置的预扫描 SVM 分类器。这正是我在本章开头暗示的方法。通过加载cv2.HOGDescriptor_getDaimlerPeopleDetector()cv2.HOGDescriptor_getDefaultPeopleDetector(),我们只需几行代码就可以开始:
In [26]: hogdef = cv2.HOGDescriptor()
...      pdetect = cv2.HOGDescriptor_getDefaultPeopleDetector()
In [27]: hogdef.setSVMDetector(pdetect)
In [28]: found, _ = hogdef.detectMultiScale(img_test)
  1. 使用 matplotlib 绘制测试图像非常简单,如下所示:
In [29]: from matplotlib import patches
...      fig = plt.figure()
...      ax = fig.add_subplot(111)
...      ax.imshow(cv2.cvtColor(img_test, cv2.COLOR_BGR2RGB))
  1. 接着,我们可以通过遍历found中的边界框来在图像中标记检测到的行人:
...      for f in found:
...          ax.add_patch(patches.Rectangle((f[0], f[1]), f[2], f[3],
...                                         color='y', linewidth=3,
...                                         fill=False))

结果看起来像这样:

前面的截图显示了测试图像中检测到的行人。

进一步改进模型

虽然径向基函数核是一个很好的默认核,但它并不总是我们问题的最佳选择。了解哪种核最适合我们的数据的唯一真正方法是尝试所有核并比较模型间的分类性能。有策略的方法来执行这种所谓的超参数调整,我们将在第十一章选择合适的模型:超参数调整中详细讨论。

如果我们还不懂得如何正确地进行超参数调整怎么办?

嗯,我相信你记得数据理解的第一步,可视化数据。可视化数据可以帮助我们了解线性 SVM 是否足够强大以对数据进行分类,在这种情况下,将不会有……

使用 SVM 进行多分类

SVMs 本质上是二分类器。特别是,实践中最普遍的多分类方法一直是创建|C|个一对多分类器(通常称为一对多OVA)分类),其中|C|是类的数量,并选择分类测试数据具有最高边界的类别。另一种方法是开发一组一对一分类器,并选择被最多分类器选择的类别。虽然这涉及到构建|C|(|C| - 1)/2个分类器,但由于每个分类器的训练数据集要小得多,因此训练分类器的时间可能会减少。

现在,让我们快速了解一下如何使用真实数据集应用 SVM 的多类分类。

为了本节的目的,我们将使用 UCI 智能手机人体活动识别数据集,我们可以免费用于非商业目的。所以请确保在获得相应的软件许可之前,不要将其用于您开创性的自主初创公司。

数据集可以从 Kaggle 网站获取,www.kaggle.com/uciml/human-activity-recognition-with-smartphones。在那里你应该找到一个下载按钮,它会带你到一个名为www.kaggle.com/uciml/human-activity-recognition-with-smartphones/downloads/human-activity-recognition-with-smartphones.zip/1的文件。

然而,如果您遵循我们之前提供的安装说明并在 GitHub 上检查了代码,您已经拥有了数据集,并且已经准备好开始了!文件可以在notebooks/data/multiclass中找到。

关于数据

在 19-48 岁年龄组内选择了 30 名志愿者,并在他们身上进行了实验。每个人进行了六项活动,即步行上楼行走下楼行走坐着站立躺下,这些活动是在腰间固定智能手机的帮助下完成的。主要捕获了以 50 Hz 恒定速率的三轴线性加速度和三轴角速度。为了标记数据,实验已被录像。数据集已被随机分为两组,其中 70%的志愿者用于生成训练数据,30%用于测试数据。

属性信息

对于数据集中的每个条目,以下信息提供:

  • 来自加速度计的三轴加速度和身体的近似加速度

  • 来自陀螺仪的三轴角速度

  • 时间域和频率域变量,561 特征向量

  • 活动的各种标签

  • 被观察者的标识符

通过参考以下步骤,您将学习如何使用 SVM 构建多类分类:

  1. 让我们快速导入实现 SVM 多类分类所需的所有必要库:
In [1]: import numpy as np
...     import pandas as pd
...     import matplotlib.pyplot as plt 
...     %matplotlib inline
...     from sklearn.utils import shuffle
...     from sklearn.svm import SVC
...     from sklearn.model_selection import cross_val_score, GridSearchCV
  1. 接下来,您将加载数据集。由于我们打算从notebooks/目录中的 Jupyter Notebook 运行此代码,因此数据目录的相对路径只是data/
In [2]: datadir = "data"
...     dataset = "multiclass"
...     train = shuffle(pd.read_csv("data/dataset/train.csv"))
...     test = shuffle(pd.read_csv("data/dataset/test.csv"))
  1. 让我们检查训练和测试数据集中是否有任何缺失值;如果有,我们将简单地从数据集中删除它们:
In [3]: train.isnull().values.any()
Out[3]: False
In [4]: test.isnull().values.any()
Out[4]: False 
  1. 接下来,我们将找到数据集中类别的频率分布,这意味着我们将检查有多少样本属于六个类别中的每一个:
In [5]: train_outcome = pd.crosstab(index=train["Activity"], # Make a crosstab
 columns="count") # Name the count column
... train_outcome

从下面的截图可以看出,LAYING类有最多的样本,但总体上,数据分布得相当均匀,没有明显的类别不平衡迹象:

图片

  1. 接下来,我们将从训练和测试数据集中分离出预测值(输入值)和结果值(类别标签):
In [6]: X_train = pd.DataFrame(train.drop(['Activity','subject'],axis=1))
...     Y_train_label = train.Activity.values.astype(object)
...     X_test = pd.DataFrame(test.drop(['Activity','subject'],axis=1)) 
...     Y_test_label = test.Activity.values.astype(object)
  1. 由于 SVM 期望数值输入和标签,你现在需要将非数值标签转换为数值标签。但首先,你必须从sklearn库中导入一个preprocessing模块:
In [7]: from sklearn import preprocessing
... encoder = preprocessing.LabelEncoder()
  1. 现在,我们将训练和测试标签编码成数值:
In [8]: encoder.fit(Y_train_label)
...     Y_train = encoder.transform(Y_train_label)
...     encoder.fit(Y_test_label)
...     Y_test = encoder.transform(Y_test_label) 
  1. 接下来,我们将对训练和测试特征集进行缩放(归一化),为此,你需要从sklearn导入StandardScaler
In [9]: from sklearn.preprocessing import StandardScaler
...     scaler = StandardScaler()
...     X_train_scaled = scaler.fit_transform(X_train)
...     X_test_scaled = scaler.transform(X_test)
  1. 一旦数据被缩放,标签格式正确,现在是我们拟合数据的时候了。但在那之前,我们将定义一个包含 SVM 在训练过程中将使用的不同参数设置的字典,这种技术称为GridSearchCV。参数网格将基于随机搜索的结果:
In [10]: params_grid = [{'kernel': ['rbf'], 'gamma': [1e-3, 1e-4],
                     'C': [1, 10, 100, 1000]},
                    {'kernel': ['linear'], 'C': [1, 10, 100, 1000]}]
  1. 最后,我们将使用前面提到的参数在数据上调用GridSearchCV以获得最佳的 SVM 拟合:
In [11]: svm_model = GridSearchCV(SVC(), params_grid, cv=5)
...      svm_model.fit(X_train_scaled, Y_train)
  1. 是时候检查 SVM 模型在数据上的训练效果了;简而言之,我们将找到准确率。不仅如此,我们还将检查哪个参数设置下的 SVM 表现最佳:
In [12]: print('Best score for training data:', svm_model.best_score_,"\n") 
...      print('Best C:',svm_model.best_estimator_.C,"\n") 
...      print('Best Kernel:',svm_model.best_estimator_.kernel,"\n")
...      print('Best Gamma:',svm_model.best_estimator_.gamma,"\n")
Out[12]: Best score for training data: 0.986
...      Best C: 100
...      Best Kerne: rbf
...      Best Gamma: 0.001

哇!正如我们所见,在多类分类问题上,SVM 在训练数据上达到了 98.6%的准确率。但请稍等,直到我们找到测试数据的准确率。所以,让我们快速检查一下:

In [13]: final_model = svm_model.best_estimator_
... print("Training set score for SVM: %f" % final_model.score(X_train_scaled , Y_train))
... print("Testing set score for SVM: %f" % final_model.score(X_test_scaled , Y_test ))
Out[13]: Training set score for SVM: 1.00
... Testing set score for SVM: 0.9586

哇!这不令人惊叹吗?我们能够在测试集上达到 95.86%的准确率;这就是 SVM 的力量。

摘要

在这一章中,我们学习了所有形式的 SVM。我们现在知道如何在二维空间中绘制决策边界,在高维空间中绘制超平面。我们学习了不同的 SVM 核,并探讨了如何在 OpenCV 中实现它们。

此外,我们还把新获得的知识应用到行人检测的实际例子中。为此,我们必须学习 HOG 特征描述符,以及如何收集适合这项任务的数据。我们使用 bootstrapping 来提高分类器的性能,并将分类器与 OpenCV 的多尺度检测机制相结合。

不仅这一章的内容很多,而且你已经读完了这本书的一半。恭喜你!

在下一章中,...

第七章:使用贝叶斯学习实现垃圾邮件过滤器

在我们深入探讨高级主题,例如聚类分析、深度学习和集成模型之前,让我们将注意力转向一个我们迄今为止尚未注意到的简单模型:朴素贝叶斯分类器。

朴素贝叶斯分类器的根源在于贝叶斯推理,以著名的统计学家和哲学家托马斯·贝叶斯(1701-1761)的名字命名。贝叶斯定理著名地描述了基于可能导致事件的条件先验知识的事件概率。我们可以使用贝叶斯定理构建一个统计模型,该模型不仅可以对数据进行分类,还可以为我们提供关于我们的分类是否正确的概率估计。在我们的情况下,我们可以使用贝叶斯推理以高置信度将电子邮件视为垃圾邮件,并确定在阳性筛查测试的情况下女性患有乳腺癌的概率。

我们现在在实现机器学习方法的机制方面已经积累了足够的经验,因此我们不应再害怕尝试并理解其背后的理论。不用担心,我们不会为此写一本书,但我们需要对理论有一些了解,以便欣赏模型的内部工作原理。之后,我相信您会发现朴素贝叶斯分类器易于实现,计算效率高,并且在相对较小的数据集上表现相当好。在本章中,我们将了解朴素贝叶斯分类器,然后实现我们的第一个贝叶斯分类器。然后,我们将使用朴素贝叶斯分类器对电子邮件进行分类。

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

  • 理解朴素贝叶斯分类器

  • 实现您的第一个贝叶斯分类器

  • 使用朴素贝叶斯分类器对电子邮件进行分类

技术要求

您可以从以下链接获取本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter07

以下是软件和硬件要求的总结:

  • 您需要 OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都可以正常工作)。

  • 您需要 Python 3.6 版本(任何 3.x 版本的 Python 都可以)。

  • 您需要 Anaconda Python 3 来安装 Python 和所需的模块。

  • 您可以使用任何操作系统——macOS、Windows 和基于 Linux 的操作系统——使用本书。我们建议您的系统至少有 4 GB 的 RAM。

  • 您不需要 GPU 来运行本书提供的代码。

理解贝叶斯推理

尽管贝叶斯分类器相对容易实现,但它们背后的理论一开始可能相当反直觉,尤其是如果你对概率理论还不够熟悉的话。然而,贝叶斯分类器的美妙之处在于,它们比我们迄今为止遇到的所有分类器更能理解底层数据。例如,标准分类器,如k最近邻算法或决策树,可能能够告诉我们一个从未见过的数据点的目标标签。然而,这些算法没有概念去理解它们的预测是正确还是错误的可能性。我们称它们为判别模型。另一方面,贝叶斯模型理解了导致数据的底层概率分布。我们称它们为生成模型,因为它们不仅对现有数据点进行标记——它们还可以生成具有相同统计数据的新的数据点。

如果最后一段让你感到有些难以理解,你可能喜欢以下关于概率理论的简要介绍。它对于接下来的章节将非常重要。

通过概率理论进行短暂的小憩

为了欣赏贝叶斯定理,我们需要掌握以下技术术语:

  • 随机变量:这是一个其值取决于偶然性的变量。一个很好的例子是抛硬币的行为,它可能显示正面或反面。如果一个随机变量只能取有限数量的值,我们称其为离散的(如抛硬币或掷骰子);否则,我们称其为连续随机变量(如某一天的温度)。随机变量通常用大写字母表示。

  • 概率:这是衡量一个事件发生可能性的度量。我们用p(e)表示事件e发生的概率,它必须是一个介于 0 和 1 之间的数(或者介于...

理解贝叶斯定理

在很多情况下,知道我们的分类器出错的可能性是非常有用的。例如,在第五章,使用决策树进行医疗诊断中,我们训练了一个决策树,根据一些医学测试来诊断患有乳腺癌的女性。你可以想象,在这种情况下,我们无论如何都想避免误诊;将健康女性误诊为乳腺癌(假阳性)不仅会让人心碎,还会导致不必要的、昂贵的医疗程序,而错过女性的乳腺癌(假阴性)可能会最终导致女性失去生命。

知道我们有贝叶斯模型可以依赖是件好事。让我们通过一个具体(并且相当著名)的例子来了解一下,来自yudkowsky.net/rational/bayes

"40 岁参加常规筛查的 1%的女性患有乳腺癌。80%的乳腺癌女性会得到阳性乳腺钼靶检查结果。9.6%的没有乳腺癌的女性也会得到阳性乳腺钼靶检查结果。这个年龄组的女性在常规筛查中得到了阳性乳腺钼靶检查结果。她实际上患有乳腺癌的概率是多少?"

你认为答案是什么?

好吧,鉴于她的乳腺钼靶检查结果为阳性,你可能会认为她患有癌症的概率相当高(大约在 80%左右)。这位女性属于 9.6%的假阳性女性的可能性似乎要小得多,所以真正的概率可能在大约 70%到 80%之间。

我恐怕这不是正确的。

这里有一种思考这个问题的方法。为了简化,让我们假设我们正在观察一些具体的患者数量,比如说 10,000。在乳腺钼靶检查之前,这 10,000 名女性可以分为两组:

  • X 组:100 名患有乳腺癌的女性

  • Y 组:9,900 名没有乳腺癌的女性

到目前为止,一切顺利。如果我们把两组的人数加起来,我们得到总共 10,000 名患者,这证实了在数学上没有人丢失。在乳腺钼靶检查之后,我们可以将这 10,000 名女性分成四个组:

  • 第一组:80 名患有乳腺癌且乳腺钼靶检查结果为阳性的女性

  • 第二组:20 名患有乳腺癌且乳腺钼靶检查结果为阴性的女性

  • 第三组:大约 950 名没有乳腺癌且乳腺钼靶检查结果为阳性的女性

  • 第四组:大约 8,950 名没有乳腺癌且乳腺钼靶检查结果为阴性的女性

从前面的分析中,你可以看到所有四个组的总和是 10,000。第一组第二组(患有乳腺癌)的总和对应于 X,而第三组第四组(没有乳腺癌)的总和对应于 Y

这可能在我们画出来时会更清晰:

在这个图表中,上半部分对应于X 组,下半部分对应于Y 组。类似地,左半部分对应于所有阳性乳腺钼靶检查的女性,右半部分对应于所有阴性乳腺钼靶检查的女性。

现在,更容易看出我们正在寻找的只与图表的左半部分有关。在所有阳性结果患者组中,癌症患者阳性结果的比率是组 1 在组 1 和组 3 中的比率:

80 / (80 + 950) = 80 / 1,030 = 7.8%

换句话说,如果你向 10,000 名患者提供乳腺钼靶检查,那么在 1,030 名阳性乳腺钼靶检查结果中,会有 80 名患者患有癌症。如果一位阳性乳腺钼靶检查患者询问她患乳腺癌的机会,医生应该给出的答案:在 13 名询问这个问题的患者中,大约有 1 名会患有癌症。

我们刚才计算的这个被称为条件概率:在(我们也可以说给定)乳腺摄影结果为阳性的情况下,一个女性患有乳腺癌的信念程度是多少?正如上一个子节中所述,我们用 p(cancer|mammography)p(C|M) 来表示。再次使用大写字母是为了强调健康和乳腺摄影都可以有几种结果,这取决于几个潜在(可能未知)的原因。因此,它们是随机变量。

然后,我们可以用以下公式表示 P(C|M)

在这里,p(C, M) 表示 CM 都为真的概率(意味着一个女性既有癌症又有阳性乳腺摄影的概率)。这相当于前面提到的属于第 1 组的女性概率。

逗号()表示逻辑,波浪号(*)表示逻辑**非**。因此,*p(C, M) 表示 C 不为真且 M 为真的概率(意味着一个女性没有癌症但有阳性乳腺摄影)。这相当于属于第 3 组的女性概率。因此,分母基本上是第 1 组(p(C, M))和第 3 组(p(~C, M))中女性的总和。

但是等等!这两个组合在一起简单地表示一个女性有阳性乳腺摄影的概率,p(M)。因此,我们可以简化前面的方程:

贝叶斯版本是对 p(C, M) 的重新解释。我们可以将 p(C, M) 表达如下:

现在有点复杂了。在这里,p(C) 只是女性患有癌症的概率(对应于前面提到的组 X)。考虑到一个女性患有癌症,她的乳腺摄影结果为阳性的概率是多少?从问题中我们知道是 80%。这是 p(M|C),即在 C 的条件下 M 的概率。

将第一个方程中的 p(C, M) 用这个新公式替换,我们得到以下方程:

在贝叶斯世界中,这些术语都有它们特定的名称:

  • p(C|M) 被称为后验,这是我们总是想要计算的东西。在我们的例子中,这对应于在乳腺摄影结果为阳性时,一个女性患有乳腺癌的信念程度。

  • p(C) 被称为先验,因为它对应于我们对乳腺癌有多普遍的初始知识。我们也将这称为我们对 C 的初始信念程度。

  • p(M|C) 被称为似然

  • p(M) 被称为证据

因此,你可以再次重写方程,如下所示:

通常,人们只对那个分数的分子感兴趣,因为分母不依赖于 C,所以分母是常数,可以忽略不计。

理解朴素贝叶斯分类器

到目前为止,我们只讨论了一个证据。然而,在大多数实际场景中,我们必须在给定多个证据(例如随机变量 X[1]X[2])的情况下预测一个结果(例如随机变量 Y)。因此,我们通常需要计算 p(Y|X[1], X[2], ..., X[n]),而不是计算 p(Y|X)。不幸的是,这使得数学变得非常复杂。对于两个随机变量 X[1]X[2],联合概率可以这样计算:

图片

丑陋的部分是术语 p(X[1]|X[2], C),它表示 X[1] 的条件概率依赖于所有其他变量,包括 C。这甚至 ...

实现你的第一个贝叶斯分类器

但数学就到这里吧,让我们来做一些编码!

在上一章中,我们学习了如何使用 scikit-learn 生成多个高斯云团。你还记得是如何做到这一点的吗?

创建一个玩具数据集

我所指的是 scikit-learn 的 datasets 模块中的函数。让我们创建 100 个数据点,每个数据点属于两个可能类别中的一个,并将它们分组成两个高斯云团。为了使实验可重复,我们指定一个整数来选择 random_state 的种子。你可以选择你喜欢的任何数字。在这里,我选择了托马斯·贝叶斯出生的那一年(只是为了好玩):

In [1]: from sklearn import datasets...     X, y = datasets.make_blobs(100, 2, centers=2,        random_state=1701, cluster_std=2)

让我们使用我们忠实的朋友 Matplotlib 查看我们刚刚创建的数据集:

In [2]: import matplotlib.pyplot as plt...     plt.style.use('ggplot')...     %matplotlib inlineIn [3]: plt.scatter(X[:, 0], X[:, ...

使用正态贝叶斯分类器对数据进行分类

我们将使用与早期章节中相同的程序来训练一个 正态贝叶斯分类器。等等,为什么不使用朴素贝叶斯分类器呢?好吧,结果是 OpenCV 并没有真正提供朴素贝叶斯分类器。相反,它提供了一个贝叶斯分类器,这个分类器并不一定期望特征是独立的,而是期望数据被聚类成高斯云团。这正是我们之前创建的数据集类型!

通过遵循这些步骤,你将学习如何使用正态贝叶斯分类器构建一个分类器:

  1. 我们可以使用以下函数创建一个新的分类器:
In [5]: import cv2
...     model_norm = cv2.ml.NormalBayesClassifier_create()
  1. 然后,通过 train 方法进行训练:
In [6]: model_norm.train(X_train, cv2.ml.ROW_SAMPLE, y_train)
Out[6]: True
  1. 一旦分类器成功训练,它将返回 True。我们像以前成千上万次做的那样,进行预测和评分分类器:
In [7]: _, y_pred = model_norm.predict(X_test)
In [8]: from sklearn import metrics
...     metrics.accuracy_score(y_test, y_pred)
Out[8]: 1.0
  1. 更好——我们可以重用上一章中的绘图函数来检查决策边界!如果你还记得,想法是创建一个网格,它将包含所有数据点,然后对网格上的每个点进行分类。网格是通过具有相同名称的 NumPy 函数创建的:
In [9]: def plot_decision_boundary(model, X_test, y_test):
...         # create a mesh to plot in
...         h = 0.02 # step size in mesh
...         x_min, x_max = X_test[:, 0].min() - 1, X_test[:, 0].max() +
            1
...         y_min, y_max = X_test[:, 1].min() - 1, X_test[:, 1].max() +
            1
...         xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
...                              np.arange(y_min, y_max, h))
  1. meshgrid 函数将返回两个浮点矩阵,xxyy,它们包含网格上每个坐标点的 xy 坐标。我们可以使用 ravel 函数将这些矩阵展平成列向量,并将它们堆叠起来形成一个新矩阵,X_hypo
...         X_hypo = np.column_stack((xx.ravel().astype(np.float32),
...                                   yy.ravel().astype(np.float32)))
  1. X_hypo 现在包含 X_hypo[:, 0] 中的所有 x 值和 X_hypo[:, 1] 中的所有 y 值。这是 predict 函数可以理解的格式:
...         ret = model.predict(X_hypo)
  1. 然而,我们希望能够使用来自 OpenCV 和 scikit-learn 的模型。这两个之间的区别在于 OpenCV 返回多个变量(一个表示成功/失败的布尔标志和预测的目标标签),而 scikit-learn 只返回预测的目标标签。因此,我们可以检查 ret 输出是否是一个元组,如果是,我们知道我们正在处理 OpenCV。在这种情况下,我们存储元组的第二个元素(ret[1])。否则,我们正在处理 scikit-learn,并且不需要索引到 ret
...         if isinstance(ret, tuple):
...             zz = ret[1]
...         else:
...             zz = ret
...         zz = zz.reshape(xx.shape)
  1. 剩下的工作就是创建一个等高线图,其中 zz 表示网格上每个点的颜色。在此基础上,我们使用我们信任的散点图来绘制数据点:
...         plt.contourf(xx, yy, zz, cmap=plt.cm.coolwarm, alpha=0.8)
...         plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, s=200)
  1. 我们通过传递一个模型(model_norm)、一个特征矩阵(X)和一个目标标签向量(y)来调用该函数:
In [10]: plot_decision_boundary(model_norm, X, y)

输出看起来像这样:

到目前为止,一切顺利。有趣的部分是,贝叶斯分类器还返回每个数据点被分类的概率:

In [11]: ret, y_pred, y_proba = model_norm.predictProb(X_test)

该函数返回一个布尔标志(成功时为True,失败时为False),预测的目标标签(y_pred)和条件概率(y_proba)。在这里,y_proba是一个 N x 2 矩阵,它表示对于 N 个数据点中的每一个,它被分类为类别 0 或类别 1 的概率:

In [12]: y_proba.round(2)
Out[12]: array([[ 0.15000001,  0.05      ],
                [ 0.08      ,  0\.        ],
                [ 0\.        ,  0.27000001],
                [ 0\.        ,  0.13      ],
                [ 0\.        ,  0\.        ],
                [ 0.18000001,  1.88      ],
                [ 0\.        ,  0\.        ],
                [ 0\.        ,  1.88      ],
                [ 0\.        ,  0\.        ],
                [ 0\.        ,  0\.        ]], dtype=float32)

这意味着,对于第一个数据点(顶部行),属于类别 0 的概率(即 p(C[0]|X))是 0.15(或 15%)。同样,属于类别 1 的概率是 p(C[1]|X) = 0.05

一些行显示的值大于 1 的原因是 OpenCV 并不真正返回概率值。概率值总是在 0 和 1 之间,并且前面矩阵中的每一行都应该加起来等于 1。相反,报告的是 似然性,这基本上是条件概率方程的分子,p(C) p(M|C)。分母 p(M) 不需要计算。我们只需要知道 0.15 > 0.05(顶部行)。因此,数据点最有可能属于类别 0。

使用朴素贝叶斯分类器对数据进行分类

以下步骤将帮助您构建一个朴素贝叶斯分类器:

  1. 我们可以通过向 scikit-learn 求助来将结果与真正的朴素贝叶斯分类器进行比较:
In [13]: from sklearn import naive_bayes...      model_naive = naive_bayes.GaussianNB()
  1. 通常,通过 fit 方法进行分类器的训练:
In [14]: model_naive.fit(X_train, y_train)Out[14]: GaussianNB(priors=None)
  1. 分类器的评分是内置的:
In [15]: model_naive.score(X_test, y_test)Out[15]: 1.0
  1. 再次获得完美分数!然而,与 OpenCV 相比,这个分类器的 predict_proba 方法返回的是真正的概率值,因为所有值都在 0 和 1 之间,并且所有行加起来等于 1:
In [16]: yprob = model_naive.predict_proba(X_test) ...

可视化条件概率

通过参考以下步骤,您将能够可视化条件概率:

  1. 为了做到这一点,我们将对前一个示例中的绘图函数进行轻微修改。我们首先在 (x_minx_max) 和 (y_miny_max) 之间创建一个网格图:
In [18]: def plot_proba(model, X_test, y_test):
...          # create a mesh to plot in
...          h = 0.02 # step size in mesh
...          x_min, x_max = X_test[:, 0].min() - 1, X_test[:, 0].max() + 1
...          y_min, y_max = X_test[:, 1].min() - 1, X_test[:, 1].max() + 1
...          xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
...                               np.arange(y_min, y_max, h))
  1. 然后,我们将 xxyy 展平,并将它们按列添加到特征矩阵 X_hypo 中:

...          X_hypo = np.column_stack((xx.ravel().astype(np.float32),
...                                    yy.ravel().astype(np.float32)))
  1. 如果我们想让这个函数同时与 OpenCV 和 scikit-learn 一起工作,我们需要为 predictProb(在 OpenCV 的情况下)和 predict_proba(在 scikit-learn 的情况下)实现一个开关。为此,我们检查 model 是否有一个名为 predictProb 的方法。如果该方法存在,我们可以调用它;否则,我们假设我们正在处理来自 scikit-learn 的模型:
...          if hasattr(model, 'predictProb'):
...             _, _, y_proba = model.predictProb(X_hypo)
...          else:
...             y_proba = model.predict_proba(X_hypo)
  1. 如同在 In [16] 中我们所看到的,y_proba 将是一个二维矩阵,对于每个数据点,它包含数据属于类别 0(在 y_proba[:, 0] 中)和类别 1(在 y_proba[:, 1] 中)的概率。将这些两个值转换为 contour 函数可以理解的颜色的简单方法就是简单地取这两个概率值的差:
...          zz = y_proba[:, 1] - y_proba[:, 0]
...          zz = zz.reshape(xx.shape)
  1. 最后一步是将 X_test 作为散点图绘制在彩色网格图之上:
... plt.contourf(xx, yy, zz, cmap=plt.cm.coolwarm, alpha=0.8)
... plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, s=200)
  1. 现在,我们已经准备好调用该函数:
In [19]: plot_proba(model_naive, X, y)

结果看起来像这样:

上一张截图显示了朴素贝叶斯分类器的条件概率。

使用朴素贝叶斯分类器对电子邮件进行分类

本章的最终任务将是将我们新获得的知识应用到实际的垃圾邮件过滤器中!这个任务涉及使用朴素贝叶斯算法解决二元分类(垃圾邮件/ham)分类问题。

朴素贝叶斯分类器实际上是电子邮件过滤中一个非常流行的模型。它们的朴素性非常适合文本数据的分析,其中每个特征都是一个单词(或一个 词袋),并且不可能对每个单词与其他每个单词的依赖关系进行建模。

现在有大量好的电子邮件数据集,例如以下这些:

加载数据集

你可以参考以下步骤来加载数据集:

  1. 如果你从 GitHub 下载了最新代码,你将在 notebooks/data/chapter7 目录下找到几个 .zip 文件。这些文件包含原始电子邮件数据(包含 To:、Cc: 和正文字段),这些数据要么被分类为垃圾邮件(带有 SPAM = 1 类别标签),要么不是(也称为 ham,HAM = 0 类别标签)。

  2. 我们创建了一个名为 sources 的变量,它包含所有原始数据文件:

In [1]: HAM = 0
...     SPAM = 1
...     datadir = 'data/chapter7'
...     sources = [
...        ('beck-s.tar.gz', HAM),
...        ('farmer-d.tar.gz', HAM),
...        ('kaminski-v.tar.gz', HAM),
...        ('kitchen-l.tar.gz', HAM),
...        ('lokay-m.tar.gz', HAM),
...        ('williams-w3.tar.gz', HAM),
...        ('BG.tar.gz', SPAM),
...        ('GP.tar.gz', SPAM),
...        ('SH.tar.gz', SPAM)
...     ]
  1. 第一步是从子目录中提取这些文件。为此,我们可以使用我们在上一章中编写的 extract_tar 函数:
In [2]: def extract_tar(datafile, extractdir):
...         try:
...             import tarfile
...         except ImportError:
...             raise ImportError("You do not have tarfile installed. "
...                               "Try unzipping the file outside of "
...                               "Python.")
...         tar = tarfile.open(datafile)
...         tar.extractall(path=extractdir)
...         tar.close()
...         print("%s successfully extracted to %s" % (datafile,
...                                                    extractdir))
  1. 为了将函数应用于源中的所有数据文件,我们需要运行一个循环。extract_tar函数期望一个指向.tar.gz文件的路径——我们通过datadir和一个sources中的条目构建它——以及一个提取文件的目录(datadir)。这将把例如data/chapter7/beck-s.tar.gz中的所有电子邮件提取到data/chapter7/beck-s/子目录中:
In [3]: for source, _ in sources:
...         datafile = '%s/%s' % (datadir, source)
...         extract_tar(datafile, datadir)
Out[3]: data/chapter7/beck-s.tar.gz successfully extracted to data/chapter7
        data/chapter7/farmer-d.tar.gz successfully extracted to
            data/chapter7
        data/chapter7/kaminski-v.tar.gz successfully extracted to
            data/chapter7
        data/chapter7/kitchen-l.tar.gz successfully extracted to
            data/chapter7
        data/chapter7/lokay-m.tar.gz successfully extracted to
            data/chapter7
        data/chapter7/williams-w3.tar.gz successfully extracted to
            data/chapter7
        data/chapter7/BG.tar.gz successfully extracted to data/chapter7
        data/chapter7/GP.tar.gz successfully extracted to data/chapter7
        data/chapter7/SH.tar.gz successfully extracted to data/chapter7

现在是有点棘手的部分。这些子目录中的每一个都包含许多其他目录,其中包含文本文件。因此,我们需要编写两个函数:

  • read_single_file(filename):这是一个从名为filename的单个文件中提取相关内容的函数。

  • read_files(path):这是一个从名为path的特定目录中的所有文件中提取相关内容的函数。

为了从单个文件中提取相关内容,我们需要了解每个文件的结构。我们知道的唯一事情是电子邮件的标题部分(From:, To:, 和 Cc:)和正文文本通过换行符'\n'分隔。因此,我们可以遍历文本文件中的每一行,只保留属于正文文本的行,这些行将被存储在变量lines中。我们还想保留一个布尔标志past_header,它最初被设置为False,但一旦我们通过了标题部分,它将被切换到True

  1. 我们首先初始化这两个变量:
In [4]: import os
...     def read_single_file(filename):
...         past_header, lines = False, []
  1. 然后,我们检查名为filename的文件是否存在。如果存在,我们开始逐行遍历它:
...         if os.path.isfile(filename):
...             f = open(filename, encoding="latin-1")
...             for line in f:

你可能已经注意到了encoding="latin-1"部分。由于一些电子邮件不是 Unicode,这是尝试正确解码文件的一种尝试。

我们不想保留标题信息,因此我们持续循环,直到遇到'\n'字符,此时我们将past_headerFalse切换到True

  1. 在这一点上,以下if-else语句的第一个条件得到满足,我们将文本文件中剩余的所有行追加到lines变量中:
...                 if past_header:
...                     lines.append(line)
...                 elif line == '\n':
...                     past_header = True
...             f.close()
  1. 最后,我们将所有行连接成一个单独的字符串,由换行符分隔,并返回文件的完整路径和实际内容:
...         content = '\n'.join(lines)
...         return filename, content
  1. 第二个函数的任务将是遍历文件夹中的所有文件并对它们调用read_single_file
In [5]: def read_files(path):
...         for root, dirnames, filenames in os.walk(path):
...             for filename in filenames:
...                 filepath = os.path.join(root, filename)
...                 yield read_single_file(filepath)

这里,yield是一个与return类似的关键字。区别在于yield返回一个生成器而不是实际值,如果你预期要遍历大量项目,这是所希望的。

使用 pandas 构建数据矩阵

现在,是时候介绍另一个 Python Anaconda 预安装的必要数据科学工具了:pandas。pandas 建立在 NumPy 之上,并为 Python 中的数据结构提供了几个有用的工具和方法。就像我们通常使用别名np导入 NumPy 一样,通常使用别名pd导入 pandas:

In [6]: import pandas as pd

pandas 提供了一个有用的数据结构,称为 DataFrame,它可以理解为 2D NumPy 数组的推广,如下所示:

In [7]: pd.DataFrame({...         'model': [...             'Normal Bayes',...             'Multinomial Bayes',...             'Bernoulli Bayes'...         ],...         'class': ...             'cv2.ml.NormalBayesClassifier_create()',...             'sklearn.naive_bayes.MultinomialNB()',... 'sklearn.naive_bayes.BernoulliNB()' ...

数据预处理

Scikit-learn 在编码文本特征方面提供了几个选项,我们在[第四章,“表示数据和特征工程”中讨论了这些选项。如您所回忆的,编码文本数据的最简单方法之一是通过词频;对于每个短语,您计算其中每个单词出现的次数。在 scikit-learn 中,这可以通过使用CountVectorizer轻松完成:

In [10]: from sklearn import feature_extraction
...      counts = feature_extraction.text.CountVectorizer()
...      X = counts.fit_transform(data['text'].values)
...      X.shape
Out[10]: (52076, 643270)

结果是一个巨大的矩阵,它告诉我们我们总共收集了 52,076 封电子邮件,这些电子邮件总共包含 643,270 个不同的单词。然而,scikit-learn 很聪明,它将数据保存为稀疏矩阵:

In [11]: X
Out[11]: <52076x643270 sparse matrix of type '<class 'numpy.int64'>'
                 with 8607632 stored elements in Compressed Sparse Row 
                 format>

要构建目标标签向量(y),我们需要访问 pandas DataFrame 中的数据。这可以通过将 DataFrame 视为一个字典来完成,其中values属性将使我们能够访问底层的 NumPy 数组:

In [12]: y = data['class'].values

训练普通贝叶斯分类器

从现在开始,事情(几乎)就像以前一样。我们可以使用 scikit-learn 将数据分为训练集和测试集(让我们保留所有数据点的 20%用于测试):

In [13]: from sklearn import model_selection as ms...      X_train, X_test, y_train, y_test = ms.train_test_split(...          X, y, test_size=0.2, random_state=42...      )

我们可以使用 OpenCV 实例化一个新的普通贝叶斯分类器:

In [14]: import cv2...      model_norm = cv2.ml.NormalBayesClassifier_create()

然而,OpenCV 不知道稀疏矩阵(至少它的 Python 接口不知道)。如果我们像之前一样将X_trainy_train传递给train函数,OpenCV 会抱怨数据矩阵不是一个 NumPy 数组。 ...

在完整数据集上训练

然而,如果您想对整个数据集进行分类,我们需要一个更复杂的方法。我们转向 scikit-learn 的朴素贝叶斯分类器,因为它理解如何处理稀疏矩阵。事实上,如果您没有注意,并将X_train像之前的每个 NumPy 数组一样对待,您可能甚至都没有注意到有任何不同:

In [17]: from sklearn import naive_bayes
...      model_naive = naive_bayes.MultinomialNB()
...      model_naive.fit(X_train, y_train)
Out[17]: MultinomialNB(alpha=1.0, class_prior=None, fit_prior=True)

在这里,我们使用了来自naive_bayes模块的MultinomialNB,这是最适合处理分类数据(如词频)的朴素贝叶斯分类器版本。

分类器几乎立即训练完成,并返回训练集和测试集的分数:

In [18]: model_naive.score(X_train, y_train)
Out[18]: 0.95086413826212191
In [19]: model_naive.score(X_test, y_test)
Out[19]: 0.94422043010752688

好了,这就是了:测试集上的准确率达到了 94.4%!对于没有做太多其他事情(除了使用默认值)来说,这已经相当不错了,不是吗?

然而,如果我们对自己的工作非常挑剔,并希望进一步提高结果,我们可以做几件事情。

使用 n-gram 来提高结果

其中一件事情是使用n-gram 计数而不是简单的词频。到目前为止,我们依赖于所谓的词袋模型:我们只是将电子邮件中的每个单词扔进一个袋子,并计算其出现的次数。然而,在真实的电子邮件中,单词出现的顺序可以携带大量信息!

这正是n-gram 计数试图传达的内容。你可以将n-gram 想象成一个由n个单词组成的短语。例如,短语Statistics has its moments包含了以下 1-gram:Statisticshasitsmoments。它还包含了以下 2-gram:Statistics hashas itsits moments。它还有两个 3-gram(Statistics has itshas its moments)以及仅有一个...

第八章:使用无监督学习发现隐藏结构

到目前为止,我们一直专注于监督学习问题,其中数据集中的每个数据点都有一个已知的标签或目标值。然而,当没有已知输出或没有教师来监督学习算法时,我们该怎么办?

这就是无监督学习的全部内容。在无监督学习中,学习过程仅体现在输入数据中,并要求从这些数据中提取知识,而不需要进一步的指令。我们已经讨论了无监督学习的一种形式——降维。另一个流行的领域是聚类分析,其目的是将数据划分为不同的相似项组。

聚类技术可能有用的一些问题包括文档分析、图像检索、查找垃圾邮件、识别虚假新闻、识别犯罪活动等等。

在本章中,我们希望了解如何使用不同的聚类算法从简单的未标记数据集中提取隐藏结构。这些隐藏结构具有许多好处,无论是用于特征提取、图像处理,还是作为监督学习任务的预处理步骤。作为一个具体的例子,我们将学习如何将聚类应用于图像,以将它们的颜色空间减少到 16 位。

更具体地说,我们将涵盖以下主题:

  • k-means 聚类期望最大化,以及在 OpenCV 中实现这些算法

  • 将聚类算法排列成层次树,以及由此带来的好处

  • 使用无监督学习进行预处理、图像处理和分类

让我们开始吧!

使用 TF-IDF 来提高结果

它被称为词频-逆文档频率TF-IDF),我们在第四章中遇到了它,表示数据和工程特征。如果你还记得,TF-IDF 的基本作用是通过对整个数据集中单词出现频率的度量来权衡单词计数。这种方法的一个有用副作用是 IDF 部分——单词的逆频率。这确保了像thebut这样的常用词在分类中只占很小的权重。

我们通过在现有的特征矩阵X上调用fit_transform来应用 TF-IDF:

In [24]: tfidf = feature_extraction.text.TfidfTransformer()In [25]: X_new = tfidf.fit_transform(X)

不要忘记分割数据;另外,...

概述

在本章中,我们首次接触了概率论,了解了随机变量和条件概率,这使我们得以一瞥贝叶斯定理——朴素贝叶斯分类器的基石。我们讨论了离散随机变量和连续随机变量、似然性和概率、先验和证据、以及正态和朴素贝叶斯分类器之间的区别。

最后,如果我们不将理论知识应用于实际例子,那么我们的理论知识将毫无用处。我们获得了一组原始电子邮件数据集,对其进行了解析,并在其上训练了贝叶斯分类器,以使用各种特征提取方法将电子邮件分类为垃圾邮件或非垃圾邮件(非垃圾邮件)。

在下一章中,我们将转换方向,并讨论一次,如果我们必须处理未标记的数据时应该做什么。

技术要求

您可以从以下链接获取本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter08

以下是软件和硬件要求的总结:

  • 您需要 OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都可以正常工作)。

  • 您需要 Python 版本 3.6(任何 3.x 版本的 Python 都可以)。

  • 您需要 Anaconda Python 3 来安装 Python 和所需的模块。

  • 您可以使用任何操作系统——macOS、Windows 和基于 Linux 的操作系统——以及本书。我们建议您的系统至少有 4 GB 的 RAM。

  • 您不需要 GPU 来运行本书附带提供的代码。

理解无监督学习

无监督学习可能以多种形式出现,但其目标始终是将原始数据转换为更丰富、更有意义的表示,无论是使人类更容易理解,还是使机器学习算法更容易解析。

无监督学习的常见应用包括以下内容:

  • 降维:这尝试将包含许多特征的高维数据表示压缩,以便可以用少量高度信息化的特征解释其主要特征。例如,当应用于波士顿地区的房价时,降维可能能够告诉我们,我们应该最关注的指标是房产税和该地区的犯罪率。

  • 因子分析:这试图找到导致观察数据的隐藏原因或未观察到的成分。例如,当应用于 1970 年代电视节目《Scooby-Doo,Where Are You!》的所有剧集时,因子分析可能能够告诉我们(剧透警告!)节目中的每个鬼魂或怪物本质上都是某个不满的伯爵在镇上玩的一个复杂的恶作剧。

  • 聚类分析:这试图将数据划分为不同的相似项目组。这是我们将在本章中关注的无监督学习类型。例如,当应用于 Netflix 上的所有电影时,聚类分析可能能够自动将它们按类型分组。

要使事情更复杂,这些分析必须在未标记的数据上执行,我们事先不知道正确的答案应该是什么。因此,无监督学习中的一个主要挑战是确定算法是否表现良好或学到了有用的东西。通常,评估无监督学习算法结果的方法是手动检查并确定结果是否有意义。

话虽如此,无监督学习可以非常有帮助,例如,作为预处理或特征提取步骤。你可以把无监督学习看作是一种数据转换——一种将数据从其原始表示转换为更信息丰富的形式的方法。学习新的表示可能会让我们对数据有更深的洞察,有时,它甚至可能提高监督学习算法的准确性。

理解 k-means 聚类

OpenCV 提供的核心聚类算法是 k-means 聚类,它从一个未标记的多维数据中搜索预定的 k 个簇(或组)。

它通过使用关于最佳聚类应该看起来像的两个简单假设来实现这一点:

  • 每个簇的中心基本上是该簇所有点的平均值,也称为质心。

  • 该簇中的每个数据点与其中心比与其他所有簇中心更近。

通过查看一个具体的例子,最容易理解该算法。

实现我们的第一个 k-means 示例

首先,让我们生成一个包含四个不同 blob 的 2D 数据集。为了强调这是一个无监督方法,我们将省略可视化中的标签:

  1. 我们将继续使用matplotlib来完成所有的可视化目的:
In [1]: import matplotlib.pyplot as plt
...     %matplotlib inline
...     plt.style.use('ggplot')
  1. 按照前面章节的相同方法,我们将创建总共 300 个属于四个不同簇的 blob(n_samples=300):
In [2]: from sklearn.datasets.samples_generator import make_blobs
...     X, y_true = make_blobs(n_samples=300, centers=4,
...                            cluster_std=1.0, random_state=10)
...     plt.scatter(X[:, 0], X[:, 1], s=100);

这将生成以下图表:

图片

上述图表显示了 300 个未标记点组成的示例数据集,这些点组织成四个不同的簇。即使没有将目标标签分配给数据,也可以通过肉眼轻松地挑选出这四个簇。k-means 算法也能做到这一点,而无需任何关于目标标签或潜在数据分布的信息。

  1. 虽然 k-means 当然是一个统计模型,但在 OpenCV 中,它不是通过ml模块和常见的trainpredict API 调用提供的。相反,它直接作为cv2.kmeans可用。要使用该模型,我们必须指定一些参数,例如终止准则和一些初始化标志。在这里,我们将告诉算法,当误差小于 1.0(cv2.TERM_CRITERIA_EPS)或执行了十个迭代时(cv2.TERM_CRITERIA_MAX_ITER)终止:
In [3]: import cv2
...     criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
...                 10, 1.0)
...     flags = cv2.KMEANS_RANDOM_CENTERS
  1. 然后,我们可以将前面的数据矩阵(X)传递给cv2.means。我们还需要指定簇的数量(4)以及算法应该尝试的不同随机初始猜测的数量(10),如下面的代码片段所示:
In [4]: import numpy as np
...     compactness, labels, centers = cv2.kmeans(X.astype(np.float32),
...                                               4, None, criteria,
...                                               10, flags)

返回三个不同的变量。

  1. 第一个指标,紧密度,返回每个点到其对应簇中心的距离平方和。高紧密度分数表明所有点都靠近其簇中心,而低紧密度分数则表明不同的簇可能没有很好地分离:
In [5]: compactness
Out[5]: 527.01581170992
  1. 当然,这个数字强烈依赖于X中的实际值。如果点之间的距离一开始就很大,我们就不期望有一个任意小的紧密度分数。因此,将数据点着色到它们分配的簇标签上绘制会更具有信息量:
In [6]: plt.scatter(X[:, 0], X[:, 1], c=labels, s=50, cmap='viridis')
...     plt.scatter(centers[:, 0], centers[:, 1], c='black', s=200,
...                 alpha=0.5);
  1. 这会产生一个散点图,所有数据点根据它们所属的簇进行着色,相应的簇中心在每个簇的中心用较深的阴影表示:

图片

上述图表显示了k-means 聚类对于k=4的结果。好消息是,k-means 算法(至少在这个简单的情况下)将点分配到簇中,与我们用肉眼做的工作非常相似。但算法是如何如此快速地找到这些不同簇的呢?毕竟,簇分配的可能组合数量是数据点数量的指数级!手动尝试所有可能的组合肯定需要很长时间。

幸运的是,不需要进行穷举搜索。相反,k-means 通常采用迭代算法,也称为期望最大化

理解期望最大化

k-means 聚类是更一般算法的一个具体应用,该算法称为期望最大化。简而言之,算法的工作原理如下:

  1. 从一些随机的簇中心开始。

  2. 重复直到收敛:

    • 期望步骤:将所有数据点分配到它们最近的簇中心。

    • 最大化步骤:通过取簇中所有点的平均值来更新簇中心。

这里,期望步骤之所以称为期望步骤,是因为它涉及到更新我们对数据集中每个点所属簇的期望。最大化步骤之所以称为最大化步骤,是因为它涉及到最大化一个定义簇中心位置的适应度函数。在 k-means 的情况下,最大化...

实现我们的期望最大化解决方案

期望最大化算法足够简单,以至于我们可以自己编写代码来实现它。为此,我们将定义一个函数,find_clusters(X, n_clusters, rseed=5),它接受一个数据矩阵(X)、我们想要发现的聚类数量(n_clusters)和一个随机种子(可选,rseed)。在接下来的内容中将会变得清晰,scikit-learn 的 pairwise_distances_argmin 函数将会非常有用:

In [7]: from sklearn.metrics import pairwise_distances_argmin
...     def find_clusters(X, n_clusters, rseed=5):

我们可以在五个基本步骤中实现 k-means 的期望最大化:

  1. 初始化:随机选择一个聚类中心数量,n_clusters。我们不仅仅选择任何随机数,而是选择实际的数据点作为聚类中心。我们通过沿其第一个轴对 X 进行排列,并选择这个随机排列中的前 n_clusters 个点来实现这一点:
        ...         rng = np.random.RandomState(rseed)
        ...         i = rng.permutation(X.shape[0])[:n_clusters]
        ...         centers = X[i]
  1. while 循环无限进行:根据最近的聚类中心分配标签。在这里,scikit-learn 的 pairwise_distance_argmin 函数正好做了我们想要的事情。它计算 X 中每个数据点在 centers 中的最近聚类中心的索引:
        ...         while True:
        ...         labels = pairwise_distances_argmin(X, centers)
  1. 找到新的聚类中心:在这个步骤中,我们必须计算属于特定聚类(X[labels == i])的所有数据点的算术平均值:
        ...          new_centers = np.array([X[labels ==
                     i].mean(axis=0)
  1. 检查收敛性并在必要时中断 while 循环:这是确保算法在任务完成后停止执行的最后一步。我们通过检查所有新的聚类中心是否等于旧的聚类中心来确定任务是否完成。如果是这样,我们退出循环;否则,我们继续循环:
        ...             for i in range(n_clusters)])
        ...             if np.all(centers == new_centers):
        ...                break
        ...             centers = new_centers
  1. 退出函数并返回结果:
        ...             return centers, labels

我们可以将我们的函数应用于之前创建的数据矩阵,X。由于我们知道数据的样子,我们知道我们正在寻找四个聚类:

In [8]: centers, labels = find_clusters(X, 4)
...     plt.scatter(X[:, 0], X[:, 1], c=labels, s=100, cmap='viridis');

这将生成以下图表。从以下图表中观察到的关键点是,在应用 k-means 聚类之前,所有数据点都被分类为相同的颜色;然而,在使用 k-means 聚类之后,每种颜色代表一个不同的聚类(相似的数据点被聚集成一个颜色):

上述图表显示了我们的自制 k-means 使用期望最大化算法的结果。正如我们所看到的,我们的自制算法完成了任务!当然,这个特定的聚类示例相当简单,大多数现实生活中的 k-means 聚类实现将在幕后做更多的工作。但就目前而言,我们很满意。

了解期望最大化的局限性

尽管期望最大化非常简单,但在一系列场景中表现惊人。然而,我们必须意识到一些潜在的局限性:

  • 期望最大化不能保证我们找到全局最优解。

  • 我们必须事先知道所需聚类的数量。

  • 算法的决策边界是线性的。

  • 对于大数据集,算法运行缓慢。

让我们更详细地快速讨论这些潜在的注意事项。

第一个注意事项——无法保证找到全局最优解

尽管数学家已经证明期望最大化步骤在每一步都会提高结果,但仍然不能保证最终我们会找到全局最佳解。例如,如果我们在我们简单的例子中使用不同的随机种子(例如使用种子 10 而不是 5),我们突然得到非常差的结果:

In [9]: centers, labels = find_clusters(X, 4, rseed=10)
...     plt.scatter(X[:, 0], X[:, 1], c=labels, s=100, cmap='viridis');

这将生成以下图:

前面的图显示了 k-means 未能找到全局最优解的例子。发生了什么?

简短的回答是,聚类中心的随机初始化是不幸的。它导致黄色聚类的中心在两个顶部云团之间迁移,实际上将它们合并成一个。结果,其他聚类因为突然需要将两个视觉上明显不同的云团分成三个聚类而感到困惑。

因此,算法通常会在多个初始状态下运行。实际上,OpenCV 默认就是这样做的(通过可选的 attempts 参数设置)。

第二个注意事项——我们必须事先选择聚类数量

另一个潜在的局限性是,k-means 无法从数据中学习聚类数量。相反,我们必须事先告诉它我们期望的聚类数量。你可以看到,对于你还不完全理解的真实世界复杂数据,这可能会带来问题。

k-means 的角度来看,没有错误或无意义的聚类数量。例如,如果我们要求算法在前一节生成的数据集中识别六个聚类,它将愉快地继续并找到最佳的六个聚类:

In [10]: criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,...                  10, 1.0)...      flags = cv2.KMEANS_RANDOM_CENTERS... compactness, labels, centers ...

第三个注意事项——聚类边界是线性的

k-means 算法基于一个简单的假设,即点将比其他点更接近其自身的聚类中心。因此,k-means 总是假设聚类之间的边界是线性的,这意味着当聚类的几何形状比这更复杂时,它将失败。

通过生成一个稍微复杂一些的数据集,我们可以看到这个限制。我们不想从高斯云团生成数据点,而是想将数据组织成两个重叠的半圆。我们可以使用 scikit-learn 的 make_moons 来做到这一点。在这里,我们选择属于两个半圆的 200 个数据点,并结合一些高斯噪声:

In [14]: from sklearn.datasets import make_moons
...      X, y = make_moons(200, noise=.05, random_state=12)

这一次,我们告诉 k-means 寻找两个聚类:

In [15]: criteria = (cv2.TERM_CRITERIA_EPS +
...                  cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
...      flags = cv2.KMEANS_RANDOM_CENTERS
...      compactness, labels, centers = cv2.kmeans(X.astype(np.float32),
...                                                2, None, criteria,
...                                                10, flags)
...      plt.scatter(X[:, 0], X[:, 1], c=labels, s=100, cmap='viridis');

生成的散点图看起来像这个图:

前面的图显示了 k-means 在非线性数据中找到线性边界的例子。从图中可以看出,k-means 未能识别出两个半圆,而是用看起来像对角线的直线(从左下角到右上角)分割了数据。

这种场景应该让你想起什么。当我们谈到第六章中的线性 SVM 时,我们遇到了相同的问题,第六章,使用支持向量机检测行人.那里的想法是使用核技巧将数据转换到更高维的特征空间。我们在这里能做同样的事情吗?

我们当然可以。有一种核化的k-means 形式,类似于 SVM 的核技巧,称为谱聚类。不幸的是,OpenCV 没有提供谱聚类的实现。幸运的是,scikit-learn 提供了:

In [16]: from sklearn.cluster import SpectralClustering

该算法使用与其他所有统计模型相同的 API:我们在构造函数中设置可选参数,然后在数据上调用fit_predict。在这里,我们想使用最近邻图来计算数据的高维表示,然后使用k-means 分配标签:

In [17]: model = SpectralClustering(n_clusters=2,
...                                 affinity='nearest_neighbors',
...                                 assign_labels='kmeans')
...      labels = model.fit_predict(X)
...      plt.scatter(X[:, 0], X[:, 1], c=labels, s=100, cmap='viridis');

谱聚类的输出看起来像这样:

图片

我们看到谱聚类完成了工作。或者,我们也可以自己将数据转换成更合适的表示,然后应用 OpenCV 的线性k-means。所有这一切的教训是,也许,再次,特征工程挽救了这一天。

第四个注意事项——对于大量样本,k-means 很慢

k-means 的最后一个限制是它对于大数据集来说相对较慢。你可以想象很多算法可能会遇到这个问题。然而,k-means 受到的影响尤其严重:k-means 的每次迭代都必须访问数据集中的每个数据点,并将其与所有聚类中心进行比较。

你可能会想知道在每次迭代中访问所有数据点是否真的必要。例如,你可能只需使用数据的一个子集来在每一步更新聚类中心。确实,这正是被称为基于批次的k-means的算法背后的确切想法。不幸的是,这个算法尚未实现...

使用 k-means 压缩颜色空间

k-means 的一个有趣用例是图像颜色空间的压缩。例如,一个标准的彩色图像带有 24 位颜色深度,提供了总共 16,777,216 种颜色变化。然而,在大多数图像中,大量的颜色将不会被使用,并且图像中的许多像素将具有相似值。压缩后的图像可以以更快的速度通过互联网发送,在接收端,它可以被解压缩以恢复原始图像。因此,减少了存储和传输成本。然而,图像颜色空间压缩将是有损的,你可能在压缩后不会注意到图像中的细微细节。

或者,我们也可以使用k-means 来减少调色板。这里的想法是将聚类中心视为减少后的调色板。然后,k-means 将原始图像中的数百万种颜色组织成适当数量的颜色。

可视化真彩色调色板

通过执行以下步骤,您将能够可视化彩色图像的真实彩色调色板:

  1. 让我们看看一张特定的图像:
In [1]: import cv2...     import numpy as np...     lena = cv2.imread('data/lena.jpg', cv2.IMREAD_COLOR)
  1. 到现在为止,我们知道如何在睡梦中启动 Matplotlib:
In [2]: import matplotlib.pyplot as plt...     %matplotlib inline...     plt.style.use('ggplot')
  1. 然而,这次,我们希望禁用ggplot选项通常在图像上显示的网格线:
In [3]: plt.rc('axes', **{'grid': False})
  1. 然后,我们可以使用以下命令可视化 Lena(别忘了将颜色通道的 BGR 顺序切换为 RGB):
In [4]: plt.imshow(cv2.cvtColor(lena, cv2.COLOR_BGR2RGB)) ...

使用 k-means 减少调色板

通过参考以下步骤,您将能够使用k-means 聚类将彩色图像投影到简化后的调色板:

  1. 现在,让我们将 1600 万种颜色减少到仅仅 16 种,通过指示k-means 将所有 1600 万种颜色变化聚类到 16 个不同的聚类中。我们将使用之前提到的程序,但现在将 16 定义为聚类数:
In [9]: criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
...                 10, 1.0)
...     flags = cv2.KMEANS_RANDOM_CENTERS
...     img_data = img_data.astype(np.float32)
...     compactness, labels, centers = cv2.kmeans(img_data,
...                                               16, None, criteria,
...                                               10, flags)
  1. 您简化后的调色板中的 16 种不同颜色对应于结果聚类。centers数组的输出显示所有颜色都有三个条目—BGR—其值介于 0 和 1 之间:
In [10]: centers
Out[10]: array([[ 0.29973754,  0.31500012,  0.48251548],
                [ 0.27192295,  0.35615689,  0.64276862],
                [ 0.17865284,  0.20933454,  0.41286203],
                [ 0.39422086,  0.62827665,  0.94220853],
                [ 0.34117648,  0.58823532,  0.90196079],
                [ 0.42996961,  0.62061119,  0.91163337],
                [ 0.06039202,  0.07102439,  0.1840712 ],
                [ 0.5589878 ,  0.6313886 ,  0.83993536],
                [ 0.37320262,  0.54575169,  0.88888896],
                [ 0.35686275,  0.57385623,  0.88954246],
                [ 0.47058824,  0.48235294,  0.59215689],
                [ 0.34346411,  0.57483661,  0.88627452],
                [ 0.13815609,  0.12984112,  0.21053818],
                [ 0.3752504 ,  0.47029912,  0.75687987],
                [ 0.31909946,  0.54829341,  0.87378371],
                [ 0.40409693,  0.58062142,  0.8547557 ]], dtype=float32)
  1. labels向量包含与 16 个聚类labels对应的 16 种颜色。因此,所有标签为 0 的数据点将根据centers数组中的第 0 行着色;同样,所有标签为 1 的数据点将根据centers数组中的第 1 行着色,依此类推。因此,我们希望将labels用作centers数组中的索引—这些就是我们的新颜色:
In [11]: new_colors = centers[labels].reshape((-1, 3))
  1. 我们可以再次绘制数据,但这次,我们将使用new_colors相应地着色数据点:
In [12]: plot_pixels(img_data, colors=new_colors, 
...      title="Reduce color space: 16 colors")   

结果是原始像素的重新着色,其中每个像素都被分配了其最近聚类中心的颜色:

  1. 为了观察重新着色的影响,我们必须将new_colors作为一个图像来绘制。我们之前将图像展平以从图像到数据矩阵转换。现在要回到图像,我们需要进行逆操作,即根据 Lena 图像的形状重塑new_colors
In [13]: lena_recolored = new_colors.reshape(lena.shape)
  1. 然后,我们可以像其他任何图像一样可视化重新着色的 Lena 图像:
In [14]: plt.figure(figsize=(10, 6))
...      plt.imshow(cv2.cvtColor(lena_recolored, cv2.COLOR_BGR2RGB));
...      plt.title('16-color image')

结果看起来像这样:

真的很棒,对吧?

总体来说,前面的截图非常清晰可辨,尽管一些细节可能有所丢失。鉴于您将图像压缩了大约一百万倍,这相当了不起。

您可以为任何期望的颜色数重复此过程。

另一种减少图像调色板的方法是使用双边滤波器。结果图像通常看起来像原始图像的卡通版本。你可以在 M. Beyeler 的《OpenCV with Python Blueprints》,Packt Publishing 出版的书中找到一个例子。

k-means 的另一个潜在应用可能不是你预期的:将其用于图像分类。

使用 k-means 对手写数字进行分类

虽然最后一个应用是k-means 的一个相当有创意的使用,但我们还可以做得更好。我们之前在无监督学习的上下文中讨论了k-means,我们试图在数据中找到一些隐藏的结构。

然而,这个概念难道不适用于大多数分类任务吗?比如说,我们的任务是分类手写数字。难道大多数零看起来不相似,甚至完全相同吗?而且,所有的零难道不是在类别上与所有可能的“一”都不同吗?这不正是我们使用无监督学习试图发现的隐藏结构吗?这不意味着我们也可以使用聚类进行分类吗?

让我们一起来看看。在本节中,我们将尝试...

加载数据集

从前面的章节中,你可能还记得,scikit-learn 通过其load_digits实用函数提供了一系列手写数字。该数据集包含 1,797 个样本,每个样本有 64 个特征,其中每个特征代表一个8 x 8图像中一个像素的亮度:

In [1]: from sklearn.datasets import load_digits
...     digits = load_digits()
...     digits.data.shape
Out[1]: (1797, 64)

运行 k-means

设置k-means 与之前的例子完全相同。我们告诉算法最多执行 10 次迭代,如果我们的聚类中心预测在距离1.0内没有改进,则停止过程:

In [2]: import cv2...     criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,...                 10, 1.0)...     flags = cv2.KMEANS_RANDOM_CENTERS

然后,我们像之前一样对数据进行 k-means 处理。由于有 10 个不同的数字(0-9),我们告诉算法寻找 10 个不同的聚类:

In [3]: import numpy as np...     digits.data = digits.data.astype(np.float32)...     compactness, clusters, centers = cv2.kmeans(digits.data, 10, None,...                                                 criteria, 10, flags)

我们就完成了!

N x 3矩阵类似...

将聚类组织成层次树

k-means 的一个替代方法是层次聚类。层次聚类的一个优点是它允许我们将不同的聚类组织成层次结构(也称为树状图),这可以使结果更容易解释。另一个有用的优点是我们不需要事先指定聚类的数量。

理解层次聚类

层次聚类有两种方法:

  • 聚合层次聚类中,我们开始时假设每个数据点可能是一个单独的聚类,然后我们随后合并最近的两个聚类,直到只剩下一个聚类。

  • 分裂层次聚类中,情况正好相反;我们首先将所有数据点分配到同一个聚类,然后我们随后将聚类分割成更小的聚类,直到每个聚类只包含一个样本。

当然,如果我们希望的话,我们可以指定所需的聚类数量。在下面的屏幕截图中,我们要求算法找到总共三个聚类:

上一张截图显示了层次聚类的逐步示例:

实现层次聚类

尽管 OpenCV 没有提供层次聚类算法的实现,但它是一种应该无论如何都属于我们机器学习技能集的流行算法:

  1. 我们首先生成 10 个随机数据点,就像上一张截图所示:
In [1]: from sklearn.datasets import make_blobs
...     X, y = make_blobs(random_state=100, n_samples=10)
  1. 使用熟悉的统计建模 API,我们导入AgglomerativeClustering算法并指定所需的聚类数量:
In [2]: from sklearn import cluster
...     agg = cluster.AgglomerativeClustering(n_clusters=3)
  1. 将模型拟合到数据,通常通过fit_predict方法进行:
In [3]: labels = agg.fit_predict(X)
  1. 我们可以生成一个散点图,其中每个数据点都根据预测的标签着色:
In [4]: import matplotlib.pyplot as plt
... %matplotlib inline
... plt.style.use('ggplot')
... plt.scatter(X[:, 0], X[:, 1], c=labels, s=100)

得到的聚类结果等同于以下图示:

最后,在我们结束这一章之前,让我们看看如何比较聚类算法并选择适合你数据的正确聚类算法!

比较聚类算法

sklearn库中大约有十三个不同的聚类算法。拥有十三种不同的选择集,问题是:你应该使用哪些聚类算法?答案是你的数据。你有什么类型的数据以及你希望对其应用哪种聚类,这就是你选择算法的方式。话虽如此,对于你拥有的问题和数据,可能有许多可能的算法可能是有用的。sklearn中的每个十三类都是针对特定任务专门化的(例如,协同聚类和双聚类或聚类特征而不是数据点)。专注于文本聚类的算法将是聚类文本数据的正确选择。因此,如果 ...

摘要

在本章中,我们讨论了一些无监督学习算法,包括k-means、球形聚类和层次聚类。我们了解到k-means 只是更一般化的期望最大化算法的一个特定应用,并讨论了其潜在的局限性。此外,我们将k-means 应用于两个具体的应用,这些应用是减少图像调色板和对手写数字进行分类。

在下一章中,我们将回到监督学习的世界,并讨论一些目前最强大的机器学习算法:神经网络和深度学习。

第三部分:使用 OpenCV 的高级机器学习

本书最后一节将涵盖重要和高级主题,如深度学习、集成机器学习方法以及超参数调整。我们还将介绍 OpenCV 的最新补充——英特尔 OpenVINO 工具包。我们将简要介绍 OpenVINO,如何安装它,以及它的各种组件,然后最后看看它如何与 OpenCV 一起用于图像分类问题。

本节包括以下章节:

  • 第九章,使用深度学习对手写数字进行分类

  • 第十章,分类的集成方法

  • 第十一章,使用超参数调整选择合适的模型

  • 第十二章,使用 OpenVINO 与 OpenCV 结合使用

  • 第十三章,结论

第九章:使用深度学习来分类手写数字

现在让我们回到监督学习,并讨论一组被称为人工神经网络的算法。神经网络早期的研究可以追溯到 20 世纪 40 年代,当时沃伦·麦克洛奇(Warren McCulloch)和沃尔特·皮茨(Walter Pitts)首次描述了大脑中生物神经细胞(或神经元)可能的工作方式。最近,在深度学习的热潮下,人工神经网络得到了复兴,这推动了诸如谷歌的 DeepMind 和 Facebook 的 DeepFace 算法等最先进的技术。

在本章中,我们想要了解一些简单的人工神经网络版本,例如麦克洛奇-皮茨神经元、感知器和多层感知器。一旦我们熟悉了基础知识,我们就可以准备实现一个更复杂的深度神经网络,用于从流行的MNIST 数据库(简称混合国家标准与技术研究院数据库)中分类手写数字。为此,我们将使用 Keras,这是一个高级神经网络库,也被研究人员和技术公司广泛使用。

在过程中,我们将解决以下问题:

  • 在 OpenCV 中实现感知器和多层感知器

  • 区分随机梯度下降和批量梯度下降,以及它们如何与反向传播相结合

  • 确定神经网络的大小

  • 使用 Keras 构建复杂的深度神经网络

激动吗?那么,让我们开始吧!

技术要求

您可以在此链接中找到本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter09

下面是软件和硬件要求的简要总结:

  • OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都可以正常工作)。

  • Python 版本 3.6(任何 3.x 版本的 Python 都可以)。

  • 安装 Python 和所需模块的 Anaconda Python 3。

  • 您可以使用任何操作系统——macOS、Windows 或基于 Linux 的系统——使用本书。我们建议您的系统至少有 4 GB 的 RAM。

  • 运行本书提供的代码不需要 GPU。

理解麦克洛奇-皮茨神经元

1943 年,沃伦·麦克洛奇(Warren McCulloch)和沃尔特·皮茨(Walter Pitts)发表了对神经元数学描述的文章,他们相信神经元在大脑中是这样运作的。神经元通过其树突树上的连接从其他神经元接收输入,这些输入在细胞体(或胞体)处综合产生输出。然后,通过一根长长的电线(或轴突)将输出传递给其他神经元,这根电线最终分支出来,在其他神经元的树突树上形成一个或多个连接(在轴突末端)。

下面的图中展示了示例神经元:

图片

麦克洛奇和皮茨描述了这种神经元的内部工作原理,将其视为一个简单的逻辑门,它要么开启,要么关闭,这取决于它在其树突上的输入。具体来说,神经元会将其所有输入相加,如果总和超过某个阈值,就会产生一个输出信号,并通过轴突传递。

然而,今天我们知道,真实的神经元比这要复杂得多。生物神经元在数千个输入上执行复杂的非线性数学运算,并且可以根据上下文、重要性或输入信号的新颖性动态地改变其反应性。你可以将真实的神经元想象得像计算机一样复杂,将人脑想象得像互联网一样复杂。

让我们考虑一个简单的人工神经元,它恰好接收两个输入,x[0]x[1]。人工神经元的任务是计算两个输入的总和(通常以加权总和的形式),如果这个总和超过某个阈值(通常是零),则该神经元将被认为是活跃的并输出一个一;否则,它将被认为是沉默的并输出一个负一(或零)。用更数学的话来说,这个麦克洛奇-皮茨神经元的输出 y 可以描述如下:

图片

在前面的方程中,w[0]w[1] 是权重系数,它们与 x[0]x[1] 一起构成了加权总和。在教科书中,输出 y 要么是 +1 要么是 -1 的两种不同情况,通常会被一个激活函数 ϕ 所掩盖,该函数可以取两个不同的值:

图片

在这里,我们引入一个新的变量 z(所谓的网络输入),它等同于加权总和:z = w[0]x[0] + w[1]x[1]。然后,加权总和与阈值 θ 进行比较,以确定 ϕ 的值,进而确定 y 的值。除此之外,这两个方程与前面的方程完全相同。

如果这些方程看起来很熟悉,你可能会想起我们在谈论线性分类器时提到的第一章,《机器学习的味道》。

你说得对,一个麦克洛奇-皮茨神经元本质上是一个线性的、二进制分类器!

你可以这样想:x[0]x[1] 是输入特征,w[0]w[1] 是需要学习的权重,分类是通过激活函数 ϕ 来执行的。如果我们能很好地学习权重,这通常需要借助合适的训练集,我们就能将数据分类为正样本或负样本。在这种情况下,ϕ(z)=θ 将充当决策边界。

帮助理解以下图表可能会使这一切更加清晰:

图片

在左侧,你可以看到神经元的激活函数,ϕ,与 z 的关系图。记住,z 仅仅是两个输入 x[0]x[1] 的加权总和。规则是,只要加权总和低于某个阈值,θ,神经元的输出为 -1;高于 θ,输出为 +1。

在右侧,你可以看到由 ϕ(z)=θ 表示的决策边界,它将数据分为两个区域,ϕ(z)<θ(其中所有数据点都被预测为负样本)和 ϕ(z)>θ(其中所有数据点都被预测为正样本)。

决策边界不需要是垂直或水平的,它可以像前面图示那样倾斜。但在单个 McCulloch-Pitts 神经元的情况下,决策边界始终是一条直线。

当然,魔法在于学习权重系数,w[0]w[1],使得决策边界正好位于所有正数据和所有负数据点之间。

要训练一个神经网络,我们通常需要三样东西:

  • 训练数据:了解到我们需要一些数据样本来验证我们分类器的有效性,这并不令人惊讶。

  • 代价函数(也称为损失函数):代价函数提供了一个衡量当前权重系数好坏的指标。有各种各样的代价函数可供选择,我们将在本章末尾讨论。一个解决方案是计算误分类的数量。另一个解决方案是计算 平方误差之和

  • 学习规则:学习规则从数学上指定了如何从一次迭代更新权重系数到下一次迭代。这个学习规则通常取决于我们在训练数据上观察到的错误(由损失函数衡量)。

这就是著名研究员弗兰克·罗森布拉特的工作所在。

理解感知器

在 20 世纪 50 年代,美国心理学家和人工智能研究员弗兰克·罗森布拉特发明了一个算法,该算法可以自动学习执行准确二元分类所需的最佳权重系数 w[0]w[1]:感知器学习规则。

罗森布拉特原始的感知器算法可以总结如下:

  1. 将权重初始化为零或一些小的随机数。

  2. 对于每个训练样本,s[i],执行以下步骤:

    1. 计算预测的目标值,ŷ**[i].

    2. ŷ**[i] 与真实值,y**[i],进行比较,并相应地更新权重:

      • 如果两者相同(预测正确),则跳过。

      • 如果两者不同(预测错误),则推动权重系数,w[0]

实现你的第一个感知器

感知器足够简单,可以从头开始实现。我们可以通过创建感知器对象来模拟典型的 OpenCV 或 scikit-learn 分类器的实现。这将允许我们初始化新的感知器对象,通过 fit 方法从数据中学习,并通过单独的 predict 方法进行预测。

当我们初始化一个新的感知器对象时,我们希望传递一个学习率(lr,或前一部分中的η)以及算法应该在多少次迭代后终止的次数(n_iter):

In [1]: import numpy as np
In [2]: class Perceptron(object):
...     def __init__(self, lr=0.01, n_iter=10):
...     self.lr = lr
...     self.n_iter = n_iter
... 

fit方法是大部分工作的地方。该方法应接受一些数据样本(X)及其相关的目标标签(y)。然后我们将创建一个权重数组(self.weights),每个特征一个,初始化为零。为了方便,我们将偏差项(self.bias)与权重向量分开,并将其也初始化为零。初始化偏差为零的一个原因是因为权重中的小随机数在网络中提供了不对称性破坏:

...         def fit(self, X, y):
...             self.weights = np.zeros(X.shape[1])
...             self.bias = 0.0

predict方法应该接受多个数据样本(X),并为每个样本返回一个目标标签,即+1 或-1。为了执行这种分类,我们需要实现ϕ(z)>θ。这里我们将选择θ = 0,加权求和可以使用 NumPy 的点积来计算:

...         def predict(self, X):
...             return np.where(np.dot(X, self.weights) + self.bias >= 0.0,
...                             1, -1)

然后,我们将计算数据集中每个数据样本(xiyi)的Δw项,并重复此步骤多次迭代(self.n_iter)。为此,我们需要将真实标签(yi)与预测标签(前面提到的self.predict(xi))进行比较。产生的 delta 项将用于更新权重和偏差项:

...             for _ in range(self.n_iter):
...                 for xi, yi in zip(X, y):
...                     delta = self.lr * (yi - self.predict(xi))
...                     self.weights += delta * xi
...                     self.bias += delta

就这样!

生成玩具数据集

在以下步骤中,你将学习如何创建和绘制一个玩具数据集:

  1. 为了测试我们的感知器分类器,我们需要创建一些模拟数据。现在让我们保持简单,并生成 100 个数据样本(n_samples),属于两个 blob 之一(centers),再次依赖于 scikit-learn 的make_blobs函数:
In [3]: from sklearn.datasets.samples_generator import make_blobs...     X, y = make_blobs(n_samples=100, centers=2,...                       cluster_std=2.2, random_state=42)
  1. 需要注意的一件事是,我们的感知器分类器期望目标标签为+1 或-1,而make_blobs返回01。调整标签的一个简单方法是以下方程:
In [4]: y = 2 * y - 1
  1. 在下面的代码中,我们将...

将感知器拟合到数据

在以下步骤中,你将学习如何在给定的数据上拟合感知器算法:

  1. 我们可以像使用 OpenCV 中遇到的其他分类器一样实例化我们的感知器对象:
In [6]: p = Perceptron(lr=0.1, n_iter=10)

在这里,我们选择了一个学习率为 0.1,并告诉感知器在 10 次迭代后终止。这些值目前是相当任意选择的,尽管我们很快就会回到它们。

选择适当的学习率至关重要,但并不总是清楚最合适的选择是什么。学习率决定了我们以多快或多慢的速度向最优权重系数移动。如果学习率太大,我们可能会意外地跳过最优解。如果它太小,我们需要大量的迭代才能收敛到最佳值。

  1. 一旦感知器设置完成,我们可以调用fit方法来优化权重系数:
In [7]: p.fit(X, y)
  1. 它是否有效?让我们看看学习到的权重值:
In [8]: p.weights
Out[8]: array([ 2.20091094, -0.4798926 ])
  1. 并且不要忘记查看偏差项:
In [9]: p.bias
Out[9]: 0.20000000000000001

如果我们将这些值代入我们的ϕ方程中,就可以清楚地看到感知器学习到了形如2.2 x[1] - 0.48 x[2] + 0.2 >= 0的决策边界。

评估感知器分类器

在以下步骤中,你将对训练好的感知器在测试数据上进行评估:

  1. 为了了解我们的感知器表现如何,我们可以在所有数据样本上计算准确率:
In [10]: from sklearn.metrics import accuracy_score...      accuracy_score(p.predict(X), y)Out[10]: 1.0

完美得分!

  1. 让我们通过将之前章节中的plot_decision_boundary函数调回来,来看看决策景观:
In [10]: def plot_decision_boundary(classifier, X_test, y_test):...          # create a mesh to plot in...          h = 0.02 # step size in mesh...          x_min, x_max = X_test[:, 0].min() - 1, X_test[:, 0].max() + 1...          y_min, y_max = X_test[:, 1].min() - 1, X_test[:, 1].max() + 1... xx, yy = np.meshgrid(np.arange(x_min, ...

将感知器应用于非线性可分的数据

在以下步骤中,你将学习如何构建一个感知器来分离非线性数据:

  1. 由于感知器是一个线性分类器,你可以想象它会在尝试对非线性可分的数据进行分类时遇到困难。我们可以通过增加我们玩具数据集中两个团块的扩散(cluster_std)来测试这一点,使得两个团块开始重叠:
In [12]: X, y = make_blobs(n_samples=100, centers=2,
...      cluster_std=5.2, random_state=42)
...      y = 2 * y - 1
  1. 我们可以使用 matplotlib 的scatter函数再次绘制数据集:
In [13]: plt.scatter(X[:, 0], X[:, 1], s=100, c=y);
...      plt.xlabel('x1')
...      plt.ylabel('x2')

如以下截图所示,这些数据不再是线性可分的,因为没有一条直线可以完美地分离这两个团块:

之前的截图展示了一个非线性可分的数据集的例子。那么,如果我们将感知器分类器应用于这个数据集,会发生什么呢?

  1. 我们可以通过重复前面的步骤来找到这个问题的答案:
In [14]: p = Perceptron(lr=0.1, n_iter=10)
...      p.fit(X, y)
  1. 然后,我们找到了一个准确率为 81%的评分:
In [15]: accuracy_score(p.predict(X), y)
Out[15]: 0.81000000000000005
  1. 为了找出哪些数据点被错误分类,我们可以再次使用我们的辅助函数可视化决策景观:
In [16]: plot_decision_boundary(p, X, y)
...      plt.xlabel('x1')
...      plt.ylabel('x2')

以下图表清楚地展示了感知器分类器的局限性。作为一个线性分类器,它试图使用一条直线来分离数据,但最终失败了。它失败的主要原因是因为数据本身不是线性可分的,尽管我们达到了 81%的准确率。然而,从以下图中可以看出,许多红色点位于蓝色区域,反之亦然。因此,与感知器不同,我们需要一个非线性算法,它可以创建的不是直线而是非线性(圆形)的决策边界:

幸运的是,有方法可以使感知器更强大,并最终创建非线性决策边界。

理解多层感知器

为了创建非线性决策边界,我们可以将多个感知器组合成更大的网络。这也被称为多层感知器MLP)。MLP 通常至少包含三个层,其中第一层为数据集的每个输入特征都有一个节点(或神经元),最后一层为每个类别标签都有一个节点。中间的层被称为隐藏层

以下图表展示了这种前馈神经网络架构的示例:

在这个网络中,每一个圆圈都是一个人工神经元(或者说,本质上是一个感知器),一个人工神经元的输出 ...

理解梯度下降

在本章前面讨论感知器时,我们确定了训练所需的三个基本要素:训练数据、代价函数和学习规则。虽然学习规则对单个感知器效果很好,但不幸的是,它并没有推广到多层感知器(MLPs),因此人们必须提出一个更通用的规则。

如果你考虑我们如何衡量分类器的成功,我们通常借助代价函数来衡量。一个典型的例子是网络的误分类数量或均方误差。这个函数(也称为损失函数)通常取决于我们试图调整的参数。在神经网络中,这些参数是权重系数。

假设一个简单的神经网络有一个可以调整的权重,w。然后我们可以将代价视为权重的函数:

图片

在训练开始时,在时间零,我们可能开始时位于这张图的左侧(w[t=0])。但从图中我们知道,对于 w 来说,会有一个更好的值,即 w[optimal],这将最小化代价函数。最小的代价意味着最低的错误,因此,通过学习达到 w[optimal] 应该是我们的最高目标。

这正是梯度下降所做的。你可以把梯度想象成一个指向山上的向量。在梯度下降中,我们试图沿着梯度的反方向行走,实际上是在下山,从山顶走到山谷:

图片

一旦到达山谷,梯度变为零,这就完成了训练。

有几种方法可以到达山谷——我们可以从左侧接近,或者我们可以从右侧接近。我们下降的起点由初始权重值决定。此外,我们必须小心不要迈出太大的步子,否则我们可能会错过山谷:

图片

因此,在随机梯度下降(有时也称为迭代或在线梯度下降)中,目标是采取小步,但要尽可能频繁地采取这些步子。有效的步长由算法的学习率决定。

具体来说,我们会反复执行以下程序:

  1. 向网络呈现少量训练样本(称为批量大小)。

  2. 在这个小批量数据上,计算代价函数的梯度。

  3. 通过在梯度的反方向上迈一小步,朝着山谷的方向更新权重系数。

  4. 重复步骤 1-3,直到权重代价不再下降。这是我们已经到达山谷的迹象。

改进 SGD 的一些其他方法包括在 Keras 框架中使用学习率查找器,在 epoch 中减小步长(学习率),以及在前一个点中讨论的,使用批量大小(或小批量),这将更快地计算权重更新。

你能想到一个这个流程可能会失败的情况吗?

一个可以想到的场景是成本函数有多个山谷,其中一些比其他更深,如下面的图所示:

图片

如果我们从左边开始,我们应该到达之前相同的山谷——没问题。但是,如果我们的起点在右边,我们可能会在途中遇到另一个山谷。梯度下降会直接把我们带到山谷,但它没有爬出来的方法。

这也被称为陷入局部最小值。研究人员已经想出不同的方法来尝试避免这个问题,其中之一就是在过程中添加噪声。

拼图中还缺一块。给定我们当前的权重系数,我们如何知道成本函数的斜率?

使用反向传播训练 MLP

这就是反向传播的作用,它是一种用于估计神经网络中成本函数梯度的算法。有些人可能会说,这基本上是链式法则的一个花哨的词,链式法则是计算依赖于多个变量的函数的偏导数的一种方法。尽管如此,它是一种帮助人工神经网络领域重生的方法,因此我们应该为此感到感激。

理解反向传播需要相当多的微积分知识,所以我只会在这里给你一个简要的介绍。

让我们提醒自己,成本函数及其梯度取决于真实输出(y[i])和当前输出(ŷ[i])之间的差异

在 OpenCV 中实现 MLP

在 OpenCV 中实现 MLP 使用我们之前至少见过一次的相同语法。为了了解 MLP 与单个感知器相比如何,我们将使用之前相同的玩具数据操作:

In [1]: from sklearn.datasets.samples_generator import make_blobs
...     X_raw, y_raw = make_blobs(n_samples=100, centers=2,
...                               cluster_std=5.2, random_state=42)

数据预处理

然而,由于我们正在使用 OpenCV,这次我们想确保输入矩阵由 32 位浮点数组成,否则代码会出错:

In [2]: import numpy as np... X = X_raw.astype(np.float32)

此外,我们需要回顾第四章,表示数据和工程特征,并记住如何表示分类变量。我们需要找到一种方法来表示目标标签,而不是整数,而是使用独热编码。实现这一点最简单的方法是使用 scikit-learn 的preprocessing模块:

In [3]: from sklearn.preprocessing import OneHotEncoder...     enc = OneHotEncoder(sparse=False, dtype=np.float32)...     y = enc.fit_transform(y_raw.reshape(-1, 1))

在 OpenCV 中创建 MLP 分类器

在 OpenCV 中创建 MLP 的语法与所有其他分类器相同:

In [4]: import cv2
...     mlp = cv2.ml.ANN_MLP_create()

然而,现在我们需要指定网络中要包含多少层以及每层有多少个神经元。我们通过一个整数列表来完成这项工作,该列表指定了每层的神经元数量。由于数据矩阵 X 有两个特征,第一层也应该有两个神经元(n_input)。由于输出有两个不同的值,最后一层也应该有两个神经元(n_output)。

在这两层之间,我们可以放置任意数量的隐藏层,每层包含任意数量的神经元。让我们选择一个包含任意数量 10 个神经元的单个隐藏层(n_hidden):

In [5]: n_input = 2
...     n_hidden = 10
...     n_output = 2
...     mlp.setLayerSizes(np.array([n_input, n_hidden, n_output]))

定制 MLP 分类器

在我们开始训练分类器之前,我们可以通过一系列可选设置来定制 MLP 分类器:

  • mlp.setActivationFunction: 这定义了网络中每个神经元要使用的激活函数。

  • mlp.setTrainMethod: 这定义了一个合适的训练方法。

  • mlp.setTermCriteria: 这设置了训练阶段的终止标准。

而我们的自制感知器分类器使用的是线性激活函数,OpenCV 提供了两个额外的选项:

  • cv2.ml.ANN_MLP_IDENTITY: 这是一个线性激活函数,f(x) = x

  • cv2.ml.ANN_MLP_SIGMOID_SYM: 这是一个对称的 Sigmoid 函数(也称为双曲正切),f(x) = β (1 - exp(-α x)) / (1 + exp(-α x)). 而 ...

训练和测试 MLP 分类器

这部分很简单。训练 MLP 分类器与所有其他分类器相同:

In [11]: mlp.train(X, cv2.ml.ROW_SAMPLE, y)
Out[11]: True

对于预测目标标签也是一样:

In [12]: _, y_hat = mlp.predict(X)

测量准确率最简单的方法是使用 scikit-learn 的辅助函数:

In [13]: from sklearn.metrics import accuracy_score
...      accuracy_score(y_hat.round(), y)
Out[13]: 0.88

看起来我们能够将性能从单个感知器的 81% 提高到由 10 个隐藏层神经元和 2 个输出神经元组成的 MLP 的 88%。为了看到发生了什么变化,我们可以再次查看决策边界:

In [14]: def plot_decision_boundary(classifier, X_test, y_test):
... # create a mesh to plot in
... h = 0.02 # step size in mesh
... x_min, x_max = X_test[:, 0].min() - 1, X_test[:, 0].max() + 1
... y_min, y_max = X_test[:, 1].min() - 1, X_test[:, 1].max() + 1
... xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
... np.arange(y_min, y_max, h))
... 
... X_hypo = np.c_[xx.ravel().astype(np.float32),
... yy.ravel().astype(np.float32)]
... _, zz = classifier.predict(X_hypo)

然而,这里有一个问题,那就是 zz 现在是一个 one-hot 编码的矩阵。为了将 one-hot 编码转换为对应于类别标签(零或一)的数字,我们可以使用 NumPy 的 argmax 函数:

...          zz = np.argmax(zz, axis=1)

然后其余部分保持不变:

...          zz = zz.reshape(xx.shape)
...          plt.contourf(xx, yy, zz, cmap=plt.cm.coolwarm, alpha=0.8)
...          plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, s=200)

然后,我们可以这样调用函数:

In [15]: plot_decision_boundary(mlp, X, y_raw)

输出看起来是这样的:

前面的输出显示了一个具有单个隐藏层的 MLP 的决策边界。

哇!决策边界不再是直线了。话虽如此,你获得了很大的性能提升,可能还期望有更大的性能提升。但没有人说过我们必须止步于此!

从这里开始,我们可以尝试至少两种不同的方法:

  • 我们可以在隐藏层中添加更多的神经元。你可以通过在第六行将 n_hidden 替换为更大的值并再次运行代码来实现这一点。一般来说,你放入网络中的神经元越多,MLP 的能力就越强。

  • 我们可以添加更多的隐藏层。结果证明,这正是神经网络真正获得其力量的地方。

因此,这就是我应该告诉你关于深度学习的地方。

了解深度学习

在深度学习还没有一个响亮的名字之前,它被称为人工神经网络。所以你已经对它了解很多了!

1986 年,当 David Rumelhart、Geoffrey Hinton 和 Ronald Williams 参与上述反向传播算法的(再)发现和普及时,对神经网络的研究兴趣再次被点燃。然而,直到最近,计算机才足够强大,能够在大型网络上实际执行反向传播算法,从而引发了深度学习研究的激增。

你可以在以下科学文章中找到有关深度学习的历史和起源的更多信息:王和拉吉(2017 年),关于起源...

了解 Keras

Keras 的核心数据结构是模型,它类似于 OpenCV 的分类器对象,但它只关注神经网络。最简单的模型类型是序列模型,它将神经网络的各个层线性堆叠起来,就像我们在 OpenCV 中对 MLP 所做的那样:

In [1]: from keras.models import Sequential
...     model = Sequential()
Out[1]: Using TensorFlow backend.

然后,可以逐个将不同的层添加到模型中。在 Keras 中,层不仅包含神经元,还执行一个函数。一些核心层类型包括以下内容:

  • 密集层:这是一个密集连接层。这正是我们在设计 MLP 时使用的:一个与前一层的每个神经元都连接的神经元层。

  • 激活:此操作对输出应用激活函数。Keras 提供了一系列激活函数,包括 OpenCV 的恒等函数(linear)、双曲正切(tanh)、S 形压缩函数(sigmoid)、softmax 函数(softmax)等。

  • 重塑:此操作将输出重塑为特定的形状。

还有其他层可以对它们的输入进行算术或几何运算:

  • 卷积层:这些层允许你指定一个内核,与输入层进行卷积。这允许你执行诸如 Sobel 滤波器或应用 1D、2D 甚至 3D 的高斯核等操作。

  • 池化层:这些层对其输入执行最大池化操作,其中输出神经元的活性由最活跃的输入神经元给出。

深度学习中流行的其他层如下:

  • Dropout:此层在每个更新时随机将一部分输入单元设置为 0。这是将噪声注入训练过程的一种方式,使其更加鲁棒。

  • 嵌入:此层对分类数据进行编码,类似于 scikit-learn 的preprocessing模块中的某些函数。

  • 高斯噪声:此层应用加性零均值高斯噪声。这是将噪声注入训练过程的一种方式,使其更加鲁棒。

可以使用具有两个输入和一个输出的密集层实现与前面类似的前馈感知器。保持与之前示例的一致性,我们将权重初始化为零,并使用双曲正切作为激活函数:

In [2]: from keras.layers import Dense
...     model.add(Dense(1, activation='tanh', input_dim=2,
...                     kernel_initializer='zeros'))

最后,我们想要指定训练方法。Keras 提供了多种优化器,包括以下几种:

  • 随机梯度下降(SGD):这是我们之前讨论过的。

  • 均方根传播(RMSprop):这是一种为每个参数调整学习率的方法。

  • 自适应动量估计(Adam):这是对均方根传播的更新。

此外,Keras 还提供了一系列不同的损失函数:

  • 均方误差(mean_squared_error):这是我们之前讨论过的。

  • Hinge 损失(hinge):这是一种最大间隔分类器,通常与 SVM 一起使用,如第六章[419719a8-3340-483a-86be-1d9b94f4a682.xhtml]中所述,使用支持向量机检测行人

你可以看到有许多参数需要指定和许多方法可以选择。为了保持与我们之前提到的感知器实现的一致性,我们将选择 SGD 作为优化器,均方误差作为损失函数,准确率作为评分函数:

In [3]: model.compile(optimizer='sgd',
...                   loss='mean_squared_error',
...                   metrics=['accuracy'])

为了比较 Keras 实现与我们的自制版本的性能,我们将分类器应用于相同的数据集:

In [4]: from sklearn.datasets.samples_generator import make_blobs
...     X, y = make_blobs(n_samples=100, centers=2,
...     cluster_std=2.2, random_state=42)

最后,使用非常熟悉的语法将 Keras 模型拟合到数据中。在这里,我们还可以选择训练的迭代次数(epochs)、在计算误差梯度之前展示的样本数量(batch_size)、是否打乱数据集(shuffle)以及是否输出进度更新(verbose):

In [5]: model.fit(X, y, epochs=400, batch_size=100, shuffle=False,
...               verbose=0)

训练完成后,我们可以如下评估分类器:

In [6]: model.evaluate(X, y)
Out[6]: 32/100 [========>.....................] - ETA: 0s
        [0.040941802412271501, 1.0]

在这里,第一个报告的值是均方误差,而第二个值表示准确率。这意味着最终的均方误差为 0.04,我们达到了 100% 的准确率。比我们自己的实现要好得多!

你可以在 keras.io 上找到更多关于 Keras、源代码文档和许多教程的信息。

拥有这些工具在手,我们现在可以开始处理真实世界的数据集了!

手写数字分类

在上一节中,我们介绍了关于神经网络的大量理论,如果你对这个主题是新手,可能会觉得有点令人不知所措。在本节中,我们将使用著名的 MNIST 数据集,它包含 60,000 个手写数字样本及其标签。

我们将在这个数据集上训练两个不同的网络:

  • 使用 OpenCV 的 MLP

  • 使用 Keras 的深度神经网络

加载 MNIST 数据集

获取 MNIST 数据集最简单的方法是使用 Keras:

In [1]: from keras.datasets import mnist
...     (X_train, y_train), (X_test, y_test) = mnist.load_data()
Out[1]: Using TensorFlow backend.
        Downloading data from
        https://s3.amazonaws.com/img-datasets/mnist.npz

这将从 Amazon Cloud 下载数据(根据您的网络连接速度可能需要一段时间)并自动将数据分为训练集和测试集。

MNIST 提供了自己的预定义的训练-测试分割。这样,比较不同分类器的性能更容易,因为它们将使用相同的数据进行训练和测试。

这些数据以我们已熟悉的格式出现:

In [2]: X_train.shape, y_train.shape
Out[2]: ((60000, 28, 28), (60000,))

我们应该注意,标签是以零到九之间的整数形式出现的(对应于数字 0-9):

In [3]: import numpy as np
...     np.unique(y_train)
Out[3]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)

我们可以查看一些示例数字:

In [4]: import matplotlib.pyplot as plt
...     %matplotlib inline
In [5]: for i in range(10):
...         plt.subplot(2, 5, i + 1)
...         plt.imshow(X_train[i, :, :], cmap='gray')
...         plt.axis('off')

这些数字看起来是这样的:

实际上,MNIST 数据集是 scikit-learn 之前提供的 NIST 数字数据集的后继者,我们之前使用过(sklearn.datasets.load_digits;参见第二章,在 OpenCV 中处理数据)。以下是一些显著的不同点:

  • MNIST 图像的尺寸显著大于 NIST 图像(28 x 28 像素),因此需要更加关注诸如扭曲和相同数字图像之间的个体差异等细微细节。

  • MNIST 数据集比 NIST 数据集大得多,提供了 60,000 个训练样本和 10,000 个测试样本(相比之下,NIST 总共有 5,620 个图像)。

预处理 MNIST 数据集

正如我们在第四章中学习的,表示数据和工程特征,我们可能希望应用以下预处理步骤:

  • 居中:所有数字都应居中在图像中。例如,看看前面图表中所有数字 1 的示例图像,它们几乎都是垂直打击。如果图像未对齐,打击点可以位于图像的任何位置,这使得神经网络难以在训练样本中找到共性。幸运的是,MNIST 中的图像已经居中。

  • 缩放:对数字进行缩放以使它们具有相同的大小也是一样的。这样,打击点、曲线和环的位置就很重要。 ...

使用 OpenCV 训练 MLP

我们可以使用以下方法在 OpenCV 中设置和训练一个 MLP:

  1. 实例化一个新的 MLP 对象:
In [9]: import cv2
...     mlp = cv2.ml.ANN_MLP_create()
  1. 指定网络中每一层的尺寸。我们可以添加尽可能多的层,但我们需要确保第一层有与输入特征相同的神经元数量(在我们的例子中是784),并且最后一层有与类别标签相同的神经元数量(在我们的例子中是10),同时有两个隐藏层,每个隐藏层有512个节点:
In [10]: mlp.setLayerSizes(np.array([784, 512, 512, 10]))
  1. 指定激活函数。在这里,我们使用之前使用的 S 型激活函数:
In [11]: mlp.setActivationFunction(cv2.ml.ANN_MLP_SIGMOID_SYM,
      ...                                2.5, 1.0)
  1. 指定训练方法。在这里,我们使用之前描述的逆传播算法。我们还需要确保我们选择足够小的学习率。由于我们有大约 10⁵个训练样本,将学习率设置为最多 10^(-5)是一个好主意:
In [12]: mlp.setTrainMethod(cv2.ml.ANN_MLP_BACKPROP)
...      mlp.setBackpropWeightScale(0.00001)
  1. 指定终止条件。在这里,我们使用之前相同的条件:运行 10 次迭代 (term_max_iter) 或直到错误不再显著增加 (term_eps):
In [13]: term_mode = (cv2.TERM_CRITERIA_MAX_ITER + 
...                   cv2.TERM_CRITERIA_EPS)
...      term_max_iter = 10
...      term_eps = 0.01
...      mlp.setTermCriteria((term_mode, term_max_iter,
...                           term_eps))
  1. 在训练集 (X_train_pre) 上训练网络:
In [14]: mlp.train(X_train_pre, cv2.ml.ROW_SAMPLE, y_train_pre)
Out[14]: True

在调用 mlp.train 之前,请注意:这可能会根据您的计算机配置需要几个小时才能运行!为了比较,在我的笔记本电脑上只需要不到一个小时。我们现在处理的是一个包含 60,000 个样本的真实世界数据集:如果我们运行 100 个训练轮次,我们必须计算 600 万个梯度!所以请小心。

当训练完成后,我们可以在训练集上计算准确率分数,看看我们达到了什么程度:

In [15]: _, y_hat_train = mlp.predict(X_train_pre)
In [16]: from sklearn.metrics import accuracy_score
...      accuracy_score(y_hat_train.round(), y_train_pre)
Out[16]: 0.92976666666666663

但是,当然,真正重要的是我们在未参与训练过程的保留测试数据上得到的准确率分数:

In [17]: _, y_hat_test = mlp.predict(X_test_pre)
...      accuracy_score(y_hat_test.round(), y_test_pre)
Out[17]: 0.91690000000000005

如果问我,91.7% 的准确率绝对不错!你应该尝试的第一件事是更改前面 In [10] 中的层大小,看看测试分数如何变化。随着你向网络添加更多的神经元,你应该看到训练分数增加——希望随之而来的是测试分数的增加。然而,在单个层中有 N 个神经元与它们分布在几个层中是不同的!你能证实这个观察结果吗?

使用 Keras 训练深度神经网络

尽管我们使用前面的 MLP 取得了令人印象深刻的分数,但我们的结果并不符合最先进的结果。目前,最佳结果接近 99.8% 的准确率——优于人类表现!这就是为什么现在,将手写数字分类的任务在很大程度上被认为已经解决。

为了接近最先进的结果,我们需要使用最先进的技术。因此,我们回到了 Keras。

预处理 MNIST 数据集

在以下步骤中,你将学习在将数据馈送到神经网络之前预处理数据:

  1. 为了确保每次运行实验时都能得到相同的结果,我们将为 NumPy 的随机数生成器选择一个随机种子。这样,从 MNIST 数据集的随机训练样本将始终以相同的顺序进行洗牌:
In [1]: import numpy as np
...     np.random.seed(1337)
  1. Keras 提供了一个类似于 scikit-learn 的 model_selection 模块中的 train_test_split 的加载函数。它的语法可能看起来很熟悉:
In [2]: from keras.datasets import mnist
...     (X_train, y_train), (X_test, y_test) = mnist.load_data()

与我们迄今为止遇到的其他数据集相比,MNIST 预定义了训练-测试分割。这使得数据集可以用作基准,因为不同算法报告的测试分数将始终适用于相同的测试样本。

  1. Keras 中的神经网络在处理特征矩阵时与标准的 OpenCV 和 scikit-learn 估计器略有不同。在 Keras 中,特征矩阵的行仍然对应于样本数量(以下代码中的 X_train.shape[0]),我们可以通过向特征矩阵添加更多维度来保留输入图像的二维性质:
In [3]: img_rows, img_cols = 28, 28
...     X_train = X_train.reshape(X_train.shape[0], img_rows, img_cols, 1)
...     X_test = X_test.reshape(X_test.shape[0], img_rows, img_cols, 1)
...     input_shape = (img_rows, img_cols, 1)
  1. 在这里,我们将特征矩阵重塑为具有n_features x 28 x 28 x 1 维度的四维矩阵。我们还需要确保我们在[0, 1]范围内的 32 位浮点数上操作,而不是[0, 255]范围内的无符号整数:
...     X_train = X_train.astype('float32') / 255.0
...     X_test = X_test.astype('float32') / 255.0
  1. 然后,我们可以像之前一样对训练标签进行 one-hot 编码。这将确保每个目标标签类别都可以分配到输出层中的一个神经元。我们可以使用 scikit-learn 的preprocessing来完成这个任务,但在这个情况下,使用 Keras 自己的实用函数更容易:
In [4]: from keras.utils import np_utils
...     n_classes = 10
...     Y_train = np_utils.to_categorical(y_train, n_classes)
...     Y_test = np_utils.to_categorical(y_test, n_classes)

创建卷积神经网络

在以下步骤中,你将创建一个神经网络,并使用你之前预处理的数据进行训练:

  1. 一旦我们预处理了数据,就是时候定义实际模型了。在这里,我们再次依赖Sequential模型来定义一个前馈神经网络:
In [5]: from keras.model import Sequential... model = Sequential()
  1. 然而,这次我们将更聪明地处理各个层。我们将围绕一个卷积层设计我们的神经网络,其中核是一个 3 x 3 像素的二维卷积:
In [6]: from keras.layers import Convolution2D...     n_filters = 32...     kernel_size = (3, 3)...     model.add(Convolution2D(n_filters, kernel_size[0], kernel_size[1],... border_mode='valid', ...

模型摘要

你还可以可视化模型的摘要,它将列出所有层及其相应的维度和每个层所包含的权重数量。它还将提供有关网络中总参数数(权重和偏差)的信息:

我们可以看到,总共有 600,810 个参数将接受训练,并且需要相当大的计算能力!请注意,我们如何计算每个层的参数数量超出了本书的范围。

模型拟合

我们像处理所有其他分类器一样拟合模型(注意,这可能需要一段时间):

In [12]: model.fit(X_train, Y_train, batch_size=128, nb_epoch=12,...                verbose=1, validation_data=(X_test, Y_test))

训练完成后,我们可以评估分类器:

In [13]: model.evaluate(X_test, Y_test, verbose=0)Out[13]: 0.99

我们达到了 99%的准确率!这与我们之前实现的 MLP 分类器相去甚远。而且这只是做事情的一种方式。正如你所见,神经网络提供了大量的调整参数,而且并不清楚哪些参数将导致最佳性能。

摘要

在这一章中,我们为机器学习实践者的技能清单增加了很多。我们不仅涵盖了人工神经网络的基础,包括感知器和 MLP,我们还接触了一些高级深度学习软件。我们学习了如何从头开始构建一个简单的感知器,以及如何使用 Keras 构建最先进的网络。此外,我们还了解了神经网络的所有细节:激活函数、损失函数、层类型和训练方法。总的来说,这可能是迄今为止最密集的一章。

既然你已经了解了大多数基本的有监督学习算法,现在是时候讨论如何将不同的算法组合成一个更强大的算法了。因此,在下一章中,我们将讨论如何构建集成分类器。

第十章:用于分类的集成方法

到目前为止,我们已经研究了多种有趣的机器学习算法,从经典的线性回归到更高级的技术如深度神经网络。在各个点上,我们指出了每种算法都有其自身的优点和缺点——并且我们注意到了如何发现和克服这些缺点。

然而,如果我们能够简单地堆叠一组平均分类器来形成一个更强大的集成分类器,那岂不是很好?

在本章中,我们将做这件事。集成方法是结合多个不同模型来解决共同问题的技术。它们在竞争性机器学习中已成为一种常见的做法——使...

技术要求

您可以从以下链接获取本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter10

这里是软件和硬件要求的一个简要总结:

  • OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都完全可以)。

  • Python 版本 3.6(任何 Python 3.x 版本都行)。

  • Anaconda Python 3 用于安装 Python 和所需的模块。

  • 您可以使用任何操作系统——macOS、Windows 或基于 Linux 的操作系统——使用这本书。我们建议您的系统至少有 4 GB RAM。

  • 您不需要 GPU 来运行书中提供的代码。

理解集成方法

集成方法的目的是结合使用给定学习算法构建的几个单个估计器的预测,以解决一个共同的问题。通常,一个集成由两个主要组件组成:

  • 一组模型

  • 一组决策规则,用于控制这些模型的结果如何组合成一个单一输出。

集成方法背后的想法与“群体智慧”概念有很大关系。而不是单个专家的意见,我们考虑一群个人的集体意见。在机器学习的背景下,这些个人将是分类器或回归器。想法是,如果我们询问足够多的分类器,其中之一应该能够得到...

理解平均集成

平均方法在机器学习中有悠久的历史,通常应用于分子动力学和音频信号处理等领域。这样的集成通常被视为给定系统的精确复制品。

平均集成本质上是一组在相同数据集上训练的模型。然后,他们的结果以多种方式汇总。

一种常见的方法是创建多个模型配置,这些配置以不同的参数子集作为输入。采用这种方法的技巧被称为集合方法。

集成方法有很多不同的风味。然而,它们通常只在抽取训练集随机子集的方式上有所不同:

  • 粘贴方法在不替换数据样本的情况下抽取样本的随机子集。

  • 袋装方法在抽取样本时进行数据样本的替换。

  • 随机子空间方法从特征中抽取随机子集,但训练所有数据样本。

  • 随机补丁方法从样本和特征中抽取随机子集。

可以使用集成平均来减少模型性能的变异性。

在 scikit-learn 中,可以使用BaggingClassifierBaggingRegressor元估计器来实现袋装方法。这些是元估计器,因为它们允许我们从任何其他基础估计器构建集成。

实现一个袋装分类器

例如,我们可以从一组 10 个k-NN 分类器中构建一个集成,如下所示:

In [1]: from sklearn.ensemble import BaggingClassifier...     from sklearn.neighbors import KNeighborsClassifier...     bag_knn = BaggingClassifier(KNeighborsClassifier(),...                                 n_estimators=10)

BaggingClassifier类提供了一些选项来自定义集成:

  • n_estimators:如前述代码所示,这指定了集成中基础估计器的数量。

  • max_samples:这表示从数据集中抽取用于训练每个基础估计器的样本数量(或分数)。我们可以设置bootstrap=True以进行有替换的抽样(有效地实现袋装),或者我们可以设置bootstrap=False以实现...

实现一个袋装回归器

同样,我们可以使用BaggingRegressor类来形成回归器的集成。

例如,我们可以从第三章的波士顿数据集(Chapter 3,监督学习的第一步)中构建一个决策树集成来预测房价。

在以下步骤中,你将学习如何使用袋装回归器来形成回归器的集成:

  1. 语法几乎与设置袋装分类器相同:
In [7]: from sklearn.ensemble import BaggingRegressor
...     from sklearn.tree import DecisionTreeRegressor
...     bag_tree = BaggingRegressor(DecisionTreeRegressor(),
...                                 max_features=0.5, n_estimators=10, 
...                                 random_state=3)
  1. 当然,我们需要像处理乳腺癌数据集那样加载数据集并进行分割:
In [8]: from sklearn.datasets import load_boston
...     dataset = load_boston()
...     X = dataset.data
...     y = dataset.target
In [9]: from sklearn.model_selection import train_test_split
...     X_train, X_test, y_train, y_test = train_test_split(
...         X, y, random_state=3
...     )
  1. 然后,我们可以在X_train上拟合袋装回归器,并在X_test上进行评分:
In [10]: bag_tree.fit(X_train, y_train)
...      bag_tree.score(X_test, y_test)
Out[10]: 0.82704756225081688

在前一个例子中,我们发现性能提升了大约 5%,从单个决策树的 77.3%准确率提升到 82.7%准确率。

当然,我们不会仅仅止步于此。没有人说集成必须由 10 个单独的估计器组成,因此我们可以自由地探索不同大小的集成。除此之外,max_samplesmax_features参数允许进行大量的定制。

袋装决策树的更复杂版本称为随机森林,我们将在本章后面讨论。

理解提升集成

构建集成的一种另一种方法是提升。提升模型使用多个单独的学习者在序列中迭代地提升集成的性能。

通常,在提升中使用的学习器相对简单。一个好的例子是只有一个节点的决策树——决策树桩。另一个例子可以是简单的线性回归模型。想法不是拥有最强的单个学习器,而是相反——我们希望个体是弱学习器,这样当我们考虑大量个体时,才能获得更好的性能。

在该过程的每次迭代中,训练集都会进行调整,以便下一个分类器应用于数据点,...

弱学习器

弱学习器是与实际分类仅略有相关性的分类器;它们可以比随机预测略好。相反,强学习器与正确分类有任意好的相关性。

这里的想法是,你不仅仅使用一个,而是一系列广泛的弱学习器,每个都比随机略好。许多弱学习器的实例可以通过提升、袋装等方法一起汇总,以创建一个强大的集成分类器。好处是,最终的分类器不会导致训练数据上的过拟合

例如,AdaBoost 在具有不同加权训练数据的弱学习器上拟合一系列。它首先预测训练数据集,并给每个观察/样本相同的权重。如果第一个学习器预测错误,那么它会给预测错误的观察/样本更高的权重。由于这是一个迭代过程,它会继续添加学习器,直到模型数量或准确率达到限制。

实现提升分类器

例如,我们可以从一组 10 个决策树中构建一个提升分类器,如下所示:

In [11]: from sklearn.ensemble import GradientBoostingClassifier...      boost_class = GradientBoostingClassifier(n_estimators=10,...                                               random_state=3)

这些分类器支持二进制和多类分类。

BaggingClassifier类类似,GradientBoostingClassifier类提供了一些选项来自定义集成:

  • n_estimators:这表示集成中基估计器的数量。通常,估计器的数量越多,性能越好。

  • loss:这表示要优化的损失函数(或成本函数)。设置loss='deviance'实现逻辑回归...

实现提升回归器

实现提升回归器的语法与提升分类器相同:

In [15]: from sklearn.ensemble import GradientBoostingRegressor
...      boost_reg = GradientBoostingRegressor(n_estimators=10,
...                                            random_state=3)

我们之前看到,单个决策树在波士顿数据集上可以达到 79.3%的准确率。由 10 个单独回归树组成的袋装决策树分类器达到了 82.7%的准确率。但是提升回归器是如何比较的呢?

让我们重新加载波士顿数据集,并将其分为训练集和测试集。我们想确保使用相同的random_state值,以便最终在相同的数据子集上进行训练和测试:

In [16]: dataset = load_boston()
...      X = dataset.data
...      y = dataset.target
In [17]: X_train, X_test, y_train, y_test = train_test_split(
...          X, y, random_state=3
...     )

结果表明,提升决策树集成实际上比之前的代码表现更差:

In [18]: boost_reg.fit(X_train, y_train)
...      boost_reg.score(X_test, y_test)
Out[18]: 0.71991199075668488

这个结果一开始可能会让人困惑。毕竟,我们使用的分类器比单棵决策树多 10 倍。为什么我们的数字会变差?

你可以看到这是一个专家分类器比一群弱学习器更聪明的良好例子。一个可能的解决方案是使集成更大。实际上,在提升集成中通常使用大约 100 个弱学习器:

In [19]: boost_reg = GradientBoostingRegressor(n_estimators=100)

然后,当我们用波士顿数据集重新训练集成时,我们得到了 89.8%的测试分数:

In [20]: boost_reg.fit(X_train, y_train)
...      boost_reg.score(X_test, y_test)
Out[20]: 0.89984081091774459

当你增加到n_estimators=500时会发生什么?我们可以通过调整可选参数做更多的事情。

如你所见,提升是一种强大的过程,它允许你通过结合大量相对简单的学习器来实现巨大的性能提升。

提升决策树的一个特定实现是 AdaBoost 算法,我们将在本章后面讨论。

理解堆叠集成

我们迄今为止所看到的所有集成方法都共享一个共同的设计理念:将多个个体分类器拟合到数据中,并借助一些简单的决策规则(如平均或提升)将它们的预测合并到一个最终预测中。

相反,堆叠集成通过层次结构构建集成。在这里,个体学习器被组织成多个层次,其中一层学习器的输出被用作下一层模型训练的数据。这样,就有可能成功融合数百种不同的模型。

不幸的是,详细讨论堆叠集成超出了本书的范围。

然而,这些模型可以非常强大,正如我们所看到的,...

将决策树组合成随机森林

带包裹的决策树的一个流行变体是所谓的随机森林。这些本质上是一系列决策树,其中每棵树都与其他树略有不同。与带包裹的决策树不同,随机森林中的每棵树都在略微不同的数据特征子集上训练。

虽然一棵无限深度的单树可能对预测数据做相对较好的工作,但它也容易过拟合。随机森林背后的想法是构建大量树,每棵树都在随机子集的数据样本和特征上训练。由于过程的随机性,森林中的每棵树将以略微不同的方式过拟合数据。然后可以通过平均单个树的预测来减少过拟合的影响。

理解决策树的不足

决策树经常遇到的过拟合数据集的影响,最好通过一个简单的例子来展示。

对于这个,我们将回到 scikit-learn 的 datasets 模块中的 make_moons 函数,我们在第八章,“使用无监督学习发现隐藏结构”中之前使用过,将数据组织成两个交替的半圆。在这里,我们选择生成 100 个属于两个半圆的数据样本,并结合一些标准差为 0.25 的高斯噪声:

In [1]: from sklearn.datasets import make_moons...     X, y = make_moons(n_samples=100, noise=0.25,...                       random_state=100)

我们可以使用 matplotlib 和 scatter 可视化这些数据

实现我们的第一个随机森林

在 OpenCV 中,可以使用 ml 模块中的 RTrees_create 函数构建随机森林:

In [7]: import cv2
...     rtree = cv2.ml.RTrees_create()

树对象提供了一些选项,其中最重要的是以下这些:

  • setMaxDepth: 这设置集成中每个树的最大可能深度。如果首先满足其他终止标准,实际获得的深度可能更小。

  • setMinSampleCount: 这设置一个节点可以包含的最小样本数,以便它可以被分割。

  • setMaxCategories: 这设置允许的最大类别数。将类别数设置为小于数据中实际类别数的小值会导致子集估计。

  • setTermCriteria: 这设置算法的终止标准。这也是你设置森林中树的数量的地方。

虽然我们可能希望有一个 setNumTrees 方法来设置森林中的树的数量(这可能是所有参数中最重要的一个,不是吗?),但我们实际上需要依赖于 setTermCriteria 方法。令人困惑的是,树的数量与 cv2.TERM_CRITERA_MAX_ITER 相混淆,这通常是为算法运行的迭代次数保留的,而不是用于集成中的估计器数量。

我们可以通过将整数 n_trees 传递给 setTermCriteria 方法来指定森林中的树的数量。这里,我们还想告诉算法,如果分数在连续迭代中没有至少增加 eps,则退出:

In [8]: n_trees = 10
...     eps = 0.01
...     criteria = (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS,
...                 n_trees, eps)
...     rtree.setTermCriteria(criteria)

然后,我们就可以使用前面代码中的数据来训练分类器了:

In [9]: rtree.train(X_train.astype(np.float32), cv2.ml.ROW_SAMPLE,
                    y_train);

可以使用 predict 方法预测测试标签:

In [10]: _, y_hat = rtree.predict(X_test.astype(np.float32))

使用 scikit-learn 的 accuracy_score,我们可以在测试集上评估模型:

In [11]: from sklearn.metrics import accuracy_score
...      accuracy_score(y_test, y_hat)
Out[11]: 0.83999999999999997

训练后,我们可以将预测标签传递给 plot_decision_boundary 函数:

In [12]: plot_decision_boundary(rtree, X_test, y_test)

这将生成以下图表:

前面的图像显示了随机森林分类器的决策景观。

使用 scikit-learn 实现随机森林

或者,我们可以使用 scikit-learn 实现随机森林:

In [13]: from sklearn.ensemble import RandomForestClassifier...      forest = RandomForestClassifier(n_estimators=10, random_state=200)

这里,我们有多个选项来自定义集成:

  • n_estimators: 这指定森林中的树的数量。

  • criterion: 这指定了节点分割的标准。将 criterion='gini' 设置为 Gini 不纯度,而将 criterion='entropy' 设置为信息增益。

  • max_features: 这指定了在每个节点分裂时考虑的特征数量(或分数)。

  • max_depth: 这指定了每个树的最大深度。

  • min_samples: 这指定了最小数量 ...

实现极端随机树

随机森林已经很随意了。但如果我们想将随机性推向极致呢?

在极端随机树(请参阅ExtraTreesClassifierExtraTreesRegressor类)中,随机性比随机森林还要强。记得决策树通常为每个特征选择一个阈值,以使节点分裂的纯度最大化吗?另一方面,极端随机树则随机选择这些阈值。然后,使用这些随机生成的阈值中的最佳值作为分裂规则。

我们可以按照以下方式构建一个极端随机树:

In [16]: from sklearn.ensemble import ExtraTreesClassifier
...      extra_tree = ExtraTreesClassifier(n_estimators=10, random_state=100)

为了说明单个决策树、随机森林和极端随机树之间的差异,让我们考虑一个简单的数据集,例如鸢尾花数据集:

In [17]: from sklearn.datasets import load_iris
...      iris = load_iris()
...      X = iris.data[:, [0, 2]]
...      y = iris.target
In [18]: X_train, X_test, y_train, y_test = train_test_split(
...          X, y, random_state=100
...      )

然后,我们可以像之前一样拟合和评分树对象:

In [19]: extra_tree.fit(X_train, y_train)
...      extra_tree.score(X_test, y_test)
Out[19]: 0.92105263157894735

为了进行比较,使用随机森林将产生相同的表现:

In [20]: forest = RandomForestClassifier(n_estimators=10,
                                        random_state=100)
...      forest.fit(X_train, y_train)
...      forest.score(X_test, y_test)
Out[20]: 0.92105263157894735

实际上,对于单个树也是如此:

In [21]: tree = DecisionTreeClassifier()
...      tree.fit(X_train, y_train)
...      tree.score(X_test, y_test)
Out[21]: 0.92105263157894735

那么,它们之间有什么区别呢?为了回答这个问题,我们必须看看决策边界。幸运的是,我们已经在前面章节中导入了我们的plot_decision_boundary辅助函数,所以我们只需要将不同的分类器对象传递给它。

我们将构建一个分类器列表,其中列表中的每个条目都是一个包含索引、分类器名称和分类器对象的元组:

In [22]: classifiers = [
...          (1, 'decision tree', tree),
...          (2, 'random forest', forest),
...          (3, 'extremely randomized trees', extra_tree)
...      ]

然后,很容易将分类器列表传递给我们的辅助函数,以便每个分类器的决策景观都在其自己的子图中绘制:

In [23]: for sp, name, model in classifiers:
...      plt.subplot(1, 3, sp)
...      plot_decision_boundary(model, X_test, y_test)
...      plt.title(name)
...      plt.axis('off')

结果看起来像这样:

图片

现在三个分类器之间的差异变得更加清晰。我们看到单个树绘制了迄今为止最简单的决策边界,使用水平决策边界分割景观。随机森林能够更清楚地分离决策景观左下角的数据点云。然而,只有极端随机树能够从各个方向将数据点云推向景观的中心。

现在我们已经了解了树集成所有不同的变体,让我们转向一个真实世界的数据集。

使用随机森林进行人脸识别

我们还没有过多讨论的一个流行数据集是 Olivetti 人脸数据集。

Olivetti 人脸数据集是在 1990 年由 AT&T 实验室剑桥收集的。该数据集包含 40 个不同主题的人脸图像,这些图像在不同的时间和不同的光照条件下拍摄。此外,主题人物还改变了他们的面部表情(睁眼/闭眼,微笑/不微笑)和面部细节(戴眼镜/不戴眼镜)。

然后将图像量化为 256 个灰度级别,并存储为无符号 8 位整数。由于有 40 个不同的主题,数据集包含 40 个不同的目标标签。因此,识别面部构成了多类分类任务的例子。

加载数据集

就像许多其他经典数据集一样,Olivetti 人脸数据集可以使用 scikit-learn 加载:

In [1]: from sklearn.datasets import fetch_olivetti_faces
...     dataset = fetch_olivetti_faces()
In [2]: X = dataset.data
...     y = dataset.target

尽管原始图像由 92 x 112 像素的图像组成,但通过 scikit-learn 提供的版本包含下采样到64 x 64像素的图像。

为了对数据集有一个大致的了解,我们可以绘制一些示例图像。让我们从数据集中随机选择八个索引:

In [3]: import numpy as np
...     np.random.seed(21)
...     idx_rand = np.random.randint(len(X), size=8)

我们可以使用 matplotlib 绘制这些示例图像,但我们需要确保在绘图之前将列向量重塑为 64 x 64 像素的图像:

In [4]: import matplotlib.pyplot as plt
...     %matplotlib inline
...     for p, i in enumerate(idx_rand):
...         plt.subplot(2, 4, p + 1)
... plt.imshow(X[i, :].reshape((64, 64)), cmap='gray')
...         plt.axis('off')

上述代码产生以下输出:

图片

您可以看到所有面孔都是对着深色背景拍摄的,而且是肖像。从一张图像到另一张图像,面部表情变化很大,这使得这是一个有趣的分类问题。尝试不要对其中的一些发笑!

预处理数据集

在我们可以将数据集传递给分类器之前,我们需要按照第四章,表示数据和工程特征的最佳实践对其进行预处理。

具体来说,我们想要确保所有示例图像具有相同的平均灰度级别:

In [5]: n_samples, n_features = X.shape[:2]...     X -= X.mean(axis=0)

我们对每个图像重复此过程,以确保每个数据点的特征值(即X中的一行)都围绕零中心:

In [6]: X -= X.mean(axis=1).reshape(n_samples, -1)

可以使用以下代码可视化预处理后的数据:

In [7]: for p, i in enumerate(idx_rand):...         plt.subplot(2, 4, p + 1)...         plt.imshow(X[i, :].reshape((64, 64)), cmap='gray')... plt.axis('off') ...

训练和测试随机森林

我们继续遵循我们的最佳实践,将数据分为训练集和测试集:

In [8]: from sklearn.model_selection import train_test_split
...     X_train, X_test, y_train, y_test = train_test_split(
...         X, y, random_state=21
...     )

然后,我们就准备好将随机森林应用于数据:

In [9]: import cv2
...     rtree = cv2.ml.RTrees_create()

在这里,我们想要创建一个包含 50 个决策树的集成:

In [10]: n_trees = 50
...      eps = 0.01
...      criteria = (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS,
...                  n_trees, eps)
...      rtree.setTermCriteria(criteria)

由于我们有大量的类别(即 40 个),我们想要确保随机森林能够相应地处理它们:

In [10]: rtree.setMaxCategories(len(np.unique(y)))

我们可以尝试其他可选参数,例如在节点可以分裂之前所需的数据点数量:

In [11]: rtree.setMinSampleCount(2)

然而,我们可能不想限制每棵树的深度。这又是一个我们最终必须实验的参数。但就目前而言,让我们将其设置为一个大的整数值,使深度实际上不受约束:

In [12]: rtree.setMaxDepth(1000)

然后,我们可以将分类器拟合到训练数据:

In [13]: rtree.train(X_train, cv2.ml.ROW_SAMPLE, y_train);

我们可以使用以下函数来检查树的最终深度:

In [13]: rtree.getMaxDepth()
Out[13]: 25

这意味着尽管我们允许树达到深度 1,000,但最终只需要 25 层。

分类器的评估是通过首先预测标签(y_hat),然后将它们传递到accuracy_score函数来完成的:

In [14]: _, y_hat = tree.predict(X_test)
In [15]: from sklearn.metrics import accuracy_score
...      accuracy_score(y_test, y_hat)
Out[15]: 0.87

我们发现准确率为 87%,这比使用单个决策树要好得多:

In [16]: from sklearn.tree import DecisionTreeClassifier
...      tree = DecisionTreeClassifier(random_state=21, max_depth=25)
...      tree.fit(X_train, y_train)
...      tree.score(X_test, y_test)
Out[16]: 0.46999999999999997

还不错!我们可以玩玩可选参数,看看我们是否能得到更好的结果。其中最重要的一个似乎是森林中的树的数量。我们可以用由 1,000 棵树组成的森林而不是 50 棵树的森林重复实验:

In [18]: num_trees = 1000
... eps = 0.01
... criteria = (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS,
... num_trees, eps)
... rtree.setTermCriteria(criteria)
... rtree.train(X_train, cv2.ml.ROW_SAMPLE, y_train);
... _, y_hat = rtree.predict(X_test)
... accuracy_score(y_test, y_hat)
Out[18]: 0.94

使用这个配置,我们得到了 94%的准确率!

在这里,我们试图通过创造性的试错来提高我们模型的性能:我们改变了一些我们认为重要的参数,并观察性能的变化,直到我们找到一个满足我们期望的配置。我们将在第十一章选择合适的模型与超参数调整中学习更多用于提高模型性能的复杂技术。

决策树集成的一个有趣的应用案例是 AdaBoost。

实现 AdaBoost

当森林中的树是深度为 1 的树(也称为决策树)并且我们执行提升而不是袋装时,得到的算法称为AdaBoost

AdaBoost 在每次迭代中通过执行以下操作调整数据集:

  • 选择决策树

  • 增加决策树错误标记的案例的权重,同时减少正确标记的案例的权重

这种迭代权重调整导致集成中的每个新分类器都会优先训练被错误标记的案例。因此,模型通过针对高权重的数据点进行调整。

最终,这些树桩被组合成一个最终的分类器。

在 OpenCV 中实现 AdaBoost

虽然 OpenCV 提供了 AdaBoost 的非常高效的实现,但它隐藏在 Haar 级联分类器之下。Haar 级联分类器是面部检测中一个非常流行的工具,我们可以通过 Lena 图像的例子来说明:

In [1]: img_bgr = cv2.imread('data/lena.jpg', cv2.IMREAD_COLOR)
...     img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)

在加载彩色和灰度图像后,我们加载了一个预训练的 Haar 级联:

In [2]: import cv2
...     filename = 'data/haarcascade_frontalface_default.xml'
...     face_cascade = cv2.CascadeClassifier(filename)

分类器将使用以下函数调用检测图像中存在的面部:

In [3]: faces = face_cascade.detectMultiScale(img_gray, 1.1, 5)

注意,该算法仅在灰度图像上操作。这就是为什么我们保存了两张 Lena 的图片,一张可以应用分类器(img_gray),另一张可以绘制结果边界框(img_bgr):

In [4]: color = (255, 0, 0)
...     thickness = 2
...     for (x, y, w, h) in faces:
...         cv2.rectangle(img_bgr, (x, y), (x + w, y + h),
...                       color, thickness)

然后,我们可以使用以下代码绘制图像:

In [5]: import matplotlib.pyplot as plt
...     %matplotlib inline
...     plt.imshow(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB));

这导致了以下输出,其中面部的位置由一个蓝色边界框指示:

图片

显然,这个截图只包含一个面部。然而,前面的代码即使在可以检测到多个面部的图像上也能工作。试试看!

在 scikit-learn 中实现 AdaBoost

在 scikit-learn 中,AdaBoost 只是另一个集成估计器。我们可以创建一个由 50 个决策树组成的集成,如下所示:

In [6]: from sklearn.ensemble import AdaBoostClassifier...     ada = AdaBoostClassifier(n_estimators=50,...                              random_state=456)

我们可以再次加载乳腺癌数据集,并将其分成 75-25:

In [7]: from sklearn.datasets import load_breast_cancer...     cancer = load_breast_cancer()...     X = cancer.data...     y = cancer.targetIn [8]: from sklearn.model_selection import train_test_split...     X_train, X_test, y_train, y_test = train_test_split(...         X, y, random_state=456...     )

然后,使用熟悉的程序fitscore AdaBoost:

In [9]: ada.fit(X_train, y_train)...     ada.score(X_test, y_test)

将不同的模型组合成一个投票分类器

到目前为止,我们已经看到了如何将同一分类器或回归器的不同实例组合成一个集成。在本章中,我们将把这个想法更进一步,将概念上不同的分类器组合成所谓的投票分类器

投票分类器背后的想法是,集成中的单个学习器不一定需要是同一类型的。毕竟,无论单个分类器如何得出预测,最终我们都会应用一个决策规则,该规则整合了所有单个分类器的投票。这也被称为投票方案

理解不同的投票方案

在投票分类器中,有两种不同的投票方案是常见的:

  • 硬投票(也称为多数投票)中,每个单独的分类器为某个类别投票,多数获胜。从统计学的角度来看,集成预测的目标标签是各个单独预测标签分布的众数。

  • 软投票中,每个单独的分类器提供一个概率值,表示特定数据点属于特定目标类别的可能性。预测结果根据分类器的重要性加权并求和。然后,加权概率总和最大的目标标签赢得投票。

例如,假设我们在集成中有三个不同的分类器执行...

实现投票分类器

让我们看看一个简单的投票分类器示例,它结合了三种不同的算法:

  • 来自第三章,监督学习的第一步的逻辑回归分类器

  • 来自第七章,使用贝叶斯学习实现垃圾邮件过滤器的高斯朴素贝叶斯分类器

  • 来自本章的随机森林分类器

我们可以将这三个算法组合成一个投票分类器,并按照以下步骤将其应用于乳腺癌数据集:

  1. 加载数据集,并将其分为训练集和测试集:
In [1]: from sklearn.datasets import load_breast_cancer
...     cancer = load_breast_cancer()
...     X = cancer.data
...     y = cancer.target
In [2]: from sklearn.model_selection import train_test_split
...     X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=13)
  1. 实例化单个分类器:
 In [3]: from sklearn.linear_model import LogisticRegression
...     model1 = LogisticRegression(random_state=13)
 In [4]: from sklearn.naive_bayes import GaussianNB
...     model2 = GaussianNB()
In [5]: from sklearn.ensemble import RandomForestClassifier
...     model3 = RandomForestClassifier(random_state=13)
  1. 将单个分类器分配给投票集成。在这里,我们需要传递一个元组列表(estimators),其中每个元组都包含分类器的名称(表示每个分类器简短名称的字母字符串)和模型对象。投票方案可以是voting='hard'voting='soft'。现在,我们将选择voting='hard'
In [6]: from sklearn.ensemble import VotingClassifier
...     vote = VotingClassifier(estimators=[('lr', model1),
...                                ('gnb', model2),('rfc', model3)],voting='hard')
  1. 将集成拟合到训练数据,并在测试数据上评分:
In [7]: vote.fit(X_train, y_train)
...     vote.score(X_test, y_test)
Out[7]: 0.95104895104895104

为了让我们相信 95.1%是一个非常好的准确率,我们可以将集成性能与每个单独分类器的理论性能进行比较。我们通过将单独的分类器拟合到数据上来做到这一点。然后,我们将看到逻辑回归模型本身达到了 94.4%的准确率:

In [8]: model1.fit(X_train, y_train)
...     model1.score(X_test, y_test)
Out[8]: 0.94405594405594406

同样,朴素贝叶斯分类器达到了 93.0%的准确率:

In [9]:  model2.fit(X_train, y_train)
...      model2.score(X_test, y_test)
Out[9]:  0.93006993006993011

最后但同样重要的是,随机森林分类器也达到了 94.4%的准确率:

In [10]: model3.fit(X_train, y_train)
... model3.score(X_test, y_test)
Out[10]: 0.94405594405594406

总的来说,我们仅仅通过将三个无关的分类器组合成一个集成,就能够在性能上获得一个很好的百分比。每个分类器可能在训练集上犯不同的错误,但这没关系,因为平均来说,我们只需要三个分类器中有两个是正确的。

多数投票

在前面的章节中,我们讨论了集成方法。我们之前没有提到的是,如何通过集成技术准备的单个模型来汇总结果。用于此的概念被称为多数投票,这实际上就是投票。一个类别获得的投票越多,它成为最终类别的可能性就越高。想象一下,如果我们有在集成技术中准备的三种模型和 10 个可能的类别(可以将它们视为从 0 到 9 的数字)。每个模型会根据获得最高概率的类别来选择一个类别。最后,获得最多投票的类别将被选中。这就是多数投票的概念。在实践中,多数投票试图为k-NN 和朴素贝叶斯等算法带来好处。

摘要

在本章中,我们讨论了如何通过将它们组合成一个集成来提高各种分类器的性能。我们讨论了如何使用袋装法平均不同分类器的预测,以及如何使用提升法让不同的分类器纠正彼此的错误。我们花费了大量时间讨论了所有可能的将决策树组合起来的方法,无论是决策树桩(AdaBoost)、随机森林还是极端随机树。最后,我们学习了如何通过构建投票分类器来将不同类型的分类器在集成中结合起来。

在下一章中,我们将更深入地讨论如何通过深入模型选择和超参数调整的世界来比较不同分类器的结果。

第十一章:使用超参数调整选择合适的模型

现在我们已经探索了各种机器学习算法,我相信你已经意识到,大多数算法都提供了大量的设置选项。这些设置或调整旋钮,所谓的超参数,帮助我们控制算法的行为,以便在尝试最大化性能时进行控制。

例如,我们可能想要选择决策树的深度或分割标准,或者在神经网络中调整神经元的数量。找到模型的重要参数值是一个棘手但几乎所有模型和数据集都必需的任务。

在本章中,我们将更深入地探讨模型评估超参数调整。假设我们有两个不同的模型 ...

技术要求

您可以从以下链接获取本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter11

这里是软件和硬件要求的一个总结:

  • 您需要 OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都完全可以)。

  • 您需要 Python 版本 3.6(任何 3.x 版本的 Python 都行)。

  • 您需要 Anaconda Python 3 来安装 Python 和所需的模块。

  • 您可以使用任何操作系统——macOS、Windows 以及基于 Linux 的操作系统,与本书一起使用。我们建议您的系统至少有 4GB 的 RAM。

  • 您不需要拥有 GPU 来运行本书附带提供的代码。

评估模型

模型评估策略形式多样。因此,在接下来的章节中,我们将重点介绍三种最常用的技术,用于比较模型之间的差异:

  • k 折交叉验证

  • 自举

  • 麦克尼马尔测试

原则上,模型评估很简单:在某个数据集上训练一个模型后,我们可以通过将模型预测与某些真实值进行比较来估计其有效性。我们很早就了解到应该将数据分为训练集和测试集,并且尽可能遵循这一指示。但为什么我们再次这样做呢?

错误评估模型的方法

我们从未在训练集上评估模型的原因是,原则上,如果我们向任何数据集投入足够强大的模型,任何数据集都可以被学习。

可以通过 Iris 数据集的快速演示来展示这一点,我们在第三章“监督学习的第一步”中详细讨论了它。在那里,目标是根据物理尺寸对鸢尾花物种进行分类。我们可以使用 scikit-learn 加载 Iris 数据集:

In [1]: from sklearn.datasets import load_iris
...     iris = load_iris()

对于这个问题的一个天真方法是,将所有数据点存储在矩阵X中,所有类别标签存储在向量y中:

In [2]: import numpy as np
...     X = iris.data.astype(np.float32)
...     y = iris.target

接下来,我们选择一个模型及其超参数。例如,让我们使用第三章中的算法,监督学习的第一步,它只提供了一个超参数:邻居的数量,k。当k=1时,我们得到一个非常简单的模型,它将未知点的标签分类为其最近邻所属的同一类别。

在以下步骤中,你将学习如何构建一个k-最近邻k-NN)模型并计算其准确率:

  1. 在 OpenCV 中,kNN 实例化如下:
In [3]: import cv2
...     knn = cv2.ml.KNearest_create()
...     knn.setDefaultK(1)
  1. 然后,我们训练模型并使用它来预测已知数据的标签:
In [4]: knn.train(X, cv2.ml.ROW_SAMPLE, y)
...     _, y_hat = knn.predict(X)
  1. 最后,我们计算正确标记点的比例:
In [5]: from sklearn.metrics import accuracy_score
...     accuracy_score(y, y_hat)
Out[5]: 1.0

如我们所见,准确率分数是1.0,这表明我们的模型正确地标记了 100%的点。

如果一个模型在训练集上达到 100%的准确率,我们说该模型已经记住了数据。

但预期的准确率真的被测量了吗?我们是否提出了一个我们期望 100%正确率的模型?

如你所可能推测的,答案是否定的。这个例子表明,即使是简单的算法也能够记住现实世界的数据集。想象一下,对于深度神经网络来说,这项任务是多么容易!通常,模型具有的参数越多,它的能力就越强。我们很快就会回到这一点。

正确评估模型

可以通过使用所谓的测试集来更好地了解模型的表现,但你已经知道了这一点。当面对从训练过程中分离出来的数据时,我们可以检查模型是否已经学会了数据中的一些跨领域的依赖关系,或者它只是记住了训练集。

我们可以使用来自 scikit-learn 的model_selection模块中的熟悉函数train_test_split将数据分为训练集和测试集:

In [6]: from sklearn.model_selection import train_test_split

但我们如何选择正确的训练-测试比例?是否存在这样一个正确的比例?或者这是模型的一个超参数?

这里存在两个相互竞争的考虑因素:

  • 如果我们的 ...

选择最佳模型

当一个模型表现不佳时,通常不清楚如何使其变得更好。在这本书中,我提出了一条经验法则,例如,如何选择神经网络中的层数。更糟糕的是,答案往往是反直觉的!例如,向网络中添加另一个层可能会使结果变得更差,而添加更多的训练数据可能根本不会改变性能。

你可以理解为什么这些问题是机器学习最重要的方面之一。最终,确定哪些步骤会或不会提高我们的模型的能力,这是区分成功的机器学习实践者和其他人的关键。

让我们来看一个具体的例子。记得第五章,使用决策树进行医疗诊断,在那里我们使用了决策树进行回归任务?我们拟合了两个不同的树到正弦函数上——一个深度为 2,一个深度为 5。作为提醒,回归结果看起来是这样的:

图片

应该很明显,这两种拟合都不是特别好。然而,两个决策树以两种不同的方式失败了!

深度为 2 的决策树(前一张截图中的粗线)试图通过数据拟合四条直线。因为数据本质上比几条直线更复杂,所以这个模型失败了。我们可以尽可能多地训练它,在尽可能多的训练样本上训练它——它永远无法很好地描述这个数据集。这样的模型被称为欠拟合数据。换句话说,模型没有足够的复杂性来解释数据中的所有特征。因此,这个模型具有很高的偏差。

另一个决策树(细线,深度 5)犯了一个不同的错误。这个模型有足够的灵活性,几乎完美地解释了数据中的细微结构。然而,在某些点上,模型似乎遵循了噪声的特定模式;我们添加到正弦函数中,而不是正弦函数本身。您可以在图表的右侧看到这一点,那里蓝色曲线(细线)会有很多波动。这样的模型被称为过度拟合数据。换句话说,模型过于复杂,最终解释了数据中的随机误差。因此,这个模型具有很高的方差。

简而言之——这是秘密配方:从根本上说,选择正确的模型归结为在偏差和方差之间找到一个甜蜜点。

模型的灵活性(也称为模型复杂性)主要是由其超参数决定的。这就是为什么调整它们如此重要的原因!

让我们回到 kNN 算法和鸢尾花数据集。如果我们对所有可能的k值重复拟合模型的程序,并计算训练和测试分数,我们预计结果将类似于以下内容:

图片

上一张图片显示了模型分数作为模型复杂度的函数。如果我想让您从这一章记住一件事,那将是这张图。让我们来分析一下。

图表描述了模型得分(无论是训练分数还是测试分数)作为模型复杂度的函数。正如前面图表中提到的,神经网络的模型复杂度大致与网络中的神经元数量成正比。在 kNN 的情况下,逻辑相反——k的值越大,决策边界越平滑,因此复杂度越低。换句话说,k=1的 kNN 在前面图表中会完全位于最右侧,即训练分数完美。难怪我们在训练集上得到了 100%的准确率!

从前面图表中,我们可以得出结论,模型复杂度景观中有三个阶段:

  • 非常低的模型复杂度(高偏差模型)会欠拟合训练数据。在这个阶段,无论我们训练多长时间,模型在训练集和测试集上的得分都很低。

  • 复杂度非常高的模型(或高方差)会过度拟合训练数据,这表明模型可以很好地预测训练数据,但在未见过的数据上失败。在这个阶段,模型已经开始学习只出现在训练数据中的复杂细节或特殊性。由于这些特殊性不适用于未见过的数据,训练分数会越来越低。

  • 对于某个中间值,测试分数达到最大。我们正在寻找的就是这个中间阶段,即测试分数达到最大值。这是偏差和方差权衡中的最佳点!

这意味着我们可以通过绘制模型复杂度景观图来找到当前任务的最佳算法。具体来说,我们可以使用以下指标来了解我们目前处于哪个阶段:

  • 如果训练分数和测试分数都低于我们的预期,我们可能处于前面图表中最左侧的阶段,即模型欠拟合数据。在这种情况下,一个好的想法可能是增加模型复杂度并再次尝试。

  • 如果训练分数远高于测试分数,我们可能处于前面图表中最右侧的阶段,即模型过度拟合数据。在这种情况下,一个好的想法可能是降低模型复杂度并再次尝试。

尽管这个程序在一般情况下是有效的,但还有更复杂的模型评估策略,这些策略比简单的训练-测试分割更全面,我们将在接下来的章节中讨论。

理解交叉验证

交叉验证是一种评估模型泛化性能的方法,通常比将数据集分为训练集和测试集的方法更稳定、更全面。

最常用的交叉验证版本是k 折交叉验证,其中k是用户指定的数字(通常是五或十)。在这里,数据集被分成k个大小大致相等的部分,称为。对于一个包含N个数据点的数据集,每个折应该大约有N / k个样本。然后,在数据上训练一系列模型,使用k - 1个折进行训练,剩余的一个折进行测试。这个过程重复k次迭代,每次选择不同的折进行...

在 OpenCV 中手动实现交叉验证

在 OpenCV 中执行交叉验证的最简单方法是手动进行数据分割。

例如,要实现两折交叉验证,我们会执行以下程序:

  1. 加载数据集:
      In [1]: from sklearn.datasets import load_iris
      ...     import numpy as np
      ...     iris = load_iris()
      ...     X = iris.data.astype(np.float32)
      ...     y = iris.target
  1. 将数据分成两个大小相等的部分:
      In [2]: from sklearn.model_selection import model_selection
      ...     X_fold1, X_fold2, y_fold1, y_fold2 = train_test_split(
      ...         X, y, random_state=37, train_size=0.5
      ...     )
  1. 实例化分类器:
      In [3]: import cv2
      ...     knn = cv2.ml.KNearest_create()
      ...     knn.setDefaultK(1)
  1. 在第一个折上训练分类器,然后预测第二个折的标签:
      In [4]: knn.train(X_fold1, cv2.ml.ROW_SAMPLE, y_fold1)
      ...     _, y_hat_fold2 = knn.predict(X_fold2)
  1. 在第二个折上训练分类器,然后预测第一个折的标签:
      In [5]: knn.train(X_fold2, cv2.ml.ROW_SAMPLE, y_fold2)
      ...     _, y_hat_fold1 = knn.predict(X_fold1)
  1. 计算两个折的准确度分数:
      In [6]: from sklearn.metrics import accuracy_score
      ...     accuracy_score(y_fold1, y_hat_fold1)
      Out[6]: 0.92000000000000004
      In [7]: accuracy_score(y_fold2, y_hat_fold2)
      Out[7]: 0.88

这个过程将产生两个准确度分数,一个用于第一个折(92%的准确度)和一个用于第二个折(88%的准确度)。因此,我们的分类器在未见过的数据上平均实现了 90%的准确度。

使用 scikit-learn 进行 k 折交叉验证

在 scikit-learn 中,交叉验证可以分三步进行:

  1. 加载数据集。由于我们之前已经做了这个操作,所以我们不需要再次进行。

  2. 实例化分类器:

      In [8]: from sklearn.neighbors import KNeighborsClassifier      ...     model = KNeighborsClassifier(n_neighbors=1)
  1. 使用cross_val_score函数进行交叉验证。此函数接受一个模型、完整的数据集(X)、目标标签(y)以及表示折数(cv)的整数值。不需要手动分割数据——该函数会根据折数自动进行。交叉验证完成后,该函数返回测试分数:
 In [9]: from sklearn.model_selection ...

实现留一法交叉验证

另一种流行的实现交叉验证的方法是选择与数据集中数据点数量相等的折数。换句话说,如果有N个数据点,我们将k=N。这意味着我们最终需要进行N次交叉验证迭代,但在每次迭代中,训练集将只包含一个数据点。这种过程的优点是我们可以使用除了一个数据点之外的所有数据点进行训练。因此,这种方法也被称为留一法交叉验证

在 scikit-learn 中,此功能由model_selection模块中的LeaveOneOut方法提供:

In [11]: from sklearn.model_selection import LeaveOneOut

此对象可以直接按以下方式传递给cross_val_score函数:

In [12]: scores = cross_val_score(model, X, y, cv=LeaveOneOut())

由于每个测试集现在都包含一个数据点,我们预计评分器将返回 150 个值——对应于数据集中的每个数据点。我们得到的每个点可能是正确的或错误的。因此,我们预计scores将是一个包含10的列表,分别对应于正确的和错误的分类:

In [13]: scores
Out[13]: array([ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                 1., 1., 1., 1., 1., 0., 1., 0., 1., 1., 1., 1., 1.,
                 1., 1., 1., 1., 1., 0., 1., 1., 1., 1., 1., 1., 1.,
                 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                 1., 1., 0., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                 1., 1., 0., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                 1., 1., 1., 0., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                 1., 1., 1., 1., 1., 1., 1.])

如果我们想了解分类器的平均性能,我们仍然会计算分数的平均值和标准差:

In [14]: scores.mean(), scores.std()
Out[14]: (0.95999999999999996, 0.19595917942265423)

我们可以看到这种评分方案返回的结果与五折交叉验证非常相似。

你可以在scikit-learn.org/stable/modules/cross_validation.html了解更多关于其他有用的交叉验证过程。

使用自助法估计鲁棒性

与 k 折交叉验证的另一种方法是自助法

与将数据分割成折不同,自助法通过从数据集中随机抽取样本来构建训练集。通常,自助法是通过替换抽取样本形成的。想象一下将所有数据点放入一个袋子,然后从袋子中随机抽取。抽取一个样本后,我们会将其放回袋子中。这允许某些样本在训练集中出现多次,这是交叉验证不允许的。

然后将分类器测试在所有不属于自助法的样本上(所谓的袋外示例),并重复进行大量次 ...

在 OpenCV 中手动实现自助法

自助法可以通过以下步骤实现:

  1. 加载数据集。由于我们之前已经这样做过了,所以我们不需要再次进行。

  2. 实例化分类器:

      In [15]: knn = cv2.ml.KNearest_create()
      ...      knn.setDefaultK(1)
  1. 从包含N个样本的数据集中,随机选择N个样本进行替换以形成自助法。这可以通过 NumPy 的random模块中的choice函数最简单地完成。我们告诉函数从[0, len(X)-1]范围内抽取len(X)个样本,并替换(replace=True)。然后函数返回一个索引列表,我们据此形成我们的自助法:
      In [16]: idx_boot = np.random.choice(len(X), size=len(X),
      ...                                  replace=True)
      ...      X_boot = X[idx_boot, :]
      ...      y_boot = y[idx_boot]
  1. 将所有未出现在自助法中的样本放入袋外集:
      In [17]: idx_oob = np.array([x not in idx_boot
      ...      for x in np.arange(len(X))],dtype=np.bool)
      ...      X_oob = X[idx_oob, :]
      ...      y_oob = y[idx_oob]
  1. 在自助法样本上训练分类器:
      In [18]: knn.train(X_train, cv2.ml.ROW_SAMPLE, y_boot)
      Out[18]: True
  1. 在袋外样本上测试分类器:
      In [19]: _, y_hat = knn.predict(X_oob)
      ...      accuracy_score(y_oob, y_hat)
      Out[19]: 0.9285714285714286
  1. 对特定次数的迭代重复步骤 3-6

  2. 自助法的迭代。重复这些步骤多达 10,000 次以获得 10,000 个准确度分数,然后平均这些分数以了解分类器的平均性能。

为了方便起见,我们可以从步骤 3-6构建一个函数,以便于运行n_iter次数的迭代过程。我们还传递一个模型(我们的 kNN 分类器,model)、特征矩阵(X)和包含所有类别标签的向量(y):

In [20]: def yield_bootstrap(model, X, y, n_iter=10000):
...          for _ in range(n_iter):

for循环内的步骤本质上是从前面提到的代码中的步骤 3-6。这涉及到在自助法上训练分类器并在袋外示例上测试它:

...              # train the classifier on bootstrap
...              idx_boot = np.random.choice(len(X), size=len(X),
...                                          replace=True)
...              X_boot = X[idx_boot, :]
...              y_boot = y[idx_boot]
...              knn.train(X_boot, cv2.ml.ROW_SAMPLE, y_boot)
... 
...              # test classifier on out-of-bag examples
...              idx_oob = np.array([x not in idx_boot
...                                  for x in np.arange(len(X))],
...                                 dtype=np.bool)
...              X_oob = X[idx_oob, :]
...              y_oob = y[idx_oob]
...              _, y_hat = knn.predict(X_oob)

然后,我们需要返回准确率分数。你可能在这里期望一个return语句。然而,更优雅的方法是使用yield语句,这会自动将函数转换为生成器。这意味着我们不需要初始化一个空列表(acc = []),然后在每次迭代中追加新的准确率分数(acc.append(accuracy_score(...)))。记录工作会自动完成:

...              yield accuracy_score(y_oob, y_hat)

为了确保我们得到相同的结果,让我们固定随机数生成器的种子:

In [21]: np.random.seed(42)

现在,让我们通过将函数输出转换为列表来运行n_iter=10次的程序:

In [22]: list(yield_bootstrap(knn, X, y, n_iter=10))
Out[22]: [0.98333333333333328,
          0.93650793650793651,
          0.92452830188679247,
          0.92307692307692313,
          0.94545454545454544,
          0.94736842105263153,
          0.98148148148148151,
          0.96078431372549022,
          0.93220338983050843,
          0.96610169491525422]

如你所见,对于这个小样本,我们得到的准确率分数在 92%到 98%之间。为了更可靠地估计模型性能,我们重复该程序 1,000 次,并计算结果的均值和标准差:

In [23]: acc = list(yield_bootstrap(knn, X, y, n_iter=1000))
...      np.mean(acc), np.std(acc)
Out[23]: (0.95524155136419198, 0.022040380995646654)

你随时可以增加重复次数。但一旦n_iter足够大,该程序应该对采样过程的随机性具有鲁棒性。在这种情况下,我们预计随着我们将n_iter增加到例如 10,000 次迭代,分数值的分布不会发生更多变化:

In [24]: acc = list(yield_bootstrap(knn, X, y, n_iter=10000))
...      np.mean(acc), np.std(acc)
Out[24]: (0.95501528733009422, 0.021778543317079499)

通常,使用自助法得到的分数会在统计检验中用来评估我们结果的显著性。让我们看看这是如何操作的。

评估我们结果的显著性

假设我们为我们的 kNN 分类器的两个版本实现了交叉验证程序。得到的测试分数是——模型 A 为 92.34%,模型 B 为 92.73%。我们如何知道哪个模型更好?

根据这里介绍的逻辑,我们可能会支持模型 B,因为它有更好的测试分数。但如果两个模型没有显著差异呢?这可能有两个潜在原因,这两个原因都是我们测试程序随机性的结果:

  • 就我们所知,模型 B 可能只是运气好。也许我们在交叉验证程序中选择了非常低的 k 值。也许模型 B 最终得到了一个有益的训练-测试分割,使得模型在分类时没有问题...

实现 Student 的 t 检验

最著名的统计检验之一是Student 的 t 检验。你可能之前听说过它:它允许我们确定两组数据是否显著不同。这对威廉·西利·高斯来说非常重要,他是这项测试的发明者,他在 Guinness 酿酒厂工作,想知道两批黑啤的质量是否有所不同。

注意,“Student”在这里是大写的。尽管高斯因公司政策不允许发表他的测试,但他还是以笔名 Student 发表了。

在实践中,t 检验允许我们确定两个数据样本是否来自具有相同均值或期望值的潜在分布。

对于我们的目的来说,这意味着我们可以使用 t 检验来确定两个独立分类器的测试分数是否有相同的平均值。我们首先假设两组测试分数是相同的。我们称这个假设为零假设,因为这是我们想要消除的假设,也就是说,我们正在寻找证据来拒绝这个假设,因为我们想确保一个分类器比另一个分类器显著更好。

我们根据 t 检验返回的称为p 值的参数来接受或拒绝零假设。p 值介于01之间。p 值为0.05意味着零假设只在 100 次中有 5 次是正确的。因此,一个小的 p 值表明有强有力的证据可以安全地拒绝假设。通常,我们使用p=0.05作为拒绝零假设的截止值。

如果这听起来太复杂,可以这样想:当我们运行 t 检验来比较分类器的测试分数时,我们希望获得一个小的 p 值,因为这意味着两个分类器给出了显著不同的结果。

我们可以使用 SciPy 的ttest_ind函数从stats模块来实现 Student 的 t 检验:

In [25]: from scipy.stats import ttest_ind

让我们从简单的例子开始。假设我们对两个分类器进行了五折交叉验证,并获得了以下分数:

In [26]: scores_a = [1, 1, 1, 1, 1]
...      scores_b = [0, 0, 0, 0, 0]

这意味着模型 A 在所有五个折上都达到了 100%的准确率,而模型 B 则得到了 0%的准确率。在这种情况下,两个结果显然是显著不同的。如果我们对这个数据进行 t 检验,我们应该因此找到一个非常小的 p 值:

In [27]: ttest_ind(scores_a, scores_b)
Out[27]: Ttest_indResult(statistic=inf, pvalue=0.0)

我们确实做到了!我们实际上得到了最小的可能的 p 值,p=0.0

另一方面,如果两个分类器在除不同折之外的所有方面都得到了完全相同的数字,那会怎样?在这种情况下,我们预计这两个分类器将是等效的,这可以通过一个非常大的 p 值来表示:

In [28]: scores_a = [0.9, 0.9, 0.9, 0.8, 0.8]
...      scores_b = [0.8, 0.8, 0.9, 0.9, 0.9]
...      ttest_ind(scores_a, scores_b)
Out[28]: Ttest_indResult(statistic=0.0, pvalue=1.0)

类似于上述情况,我们得到了最大的可能的 p 值,p=1.0

为了看到更现实的情况,让我们回到之前例子中的 kNN 分类器。使用从十折交叉验证过程中获得的测试分数,我们可以使用以下程序比较两个不同的 kNN 分类器:

  1. 获取模型 A 的一组测试分数。我们选择模型 A 作为之前提到的 kNN 分类器(k=1):
      In [29]: k1 = KNeighborsClassifier(n_neighbors=1)
      ...      scores_k1 = cross_val_score(k1, X, y, cv=10)
      ...      np.mean(scores_k1), np.std(scores_k1)
      Out[29]: (0.95999999999999996, 0.053333333333333323)
  1. 获取模型 B 的一组测试分数。让我们选择模型 B 为一个 kNN 分类器,k=3
      In [30]: k3 = KNeighborsClassifier(n_neighbors=3)
      ...      scores_k3 = cross_val_score(k3, X, y, cv=10)
      ...      np.mean(scores_k3), np.std(scores_k3)
      Out[30]: (0.96666666666666656, 0.044721359549995787)
  1. 对这两组分数应用 t 检验:
      In [31]: ttest_ind(scores_k1, scores_k3)
      Out[31]: Ttest_indResult(statistic=-0.2873478855663425,
               pvalue=0.77712784875052965)

如您所见,这是一个很好的例子,两个分类器给出了不同的交叉验证分数(96.0%和 96.7%),结果却并不显著不同!因为我们得到了一个大的 p 值(p=0.777),我们预计这两个分类器中有 77 次是等效的。

实现 McNemar 的检验

一种更高级的统计技术是 McNemar 的测试。这种测试可以用于成对数据,以确定两个样本之间是否存在任何差异。正如在 t-test 的情况下,我们可以使用 McNemar 的测试来确定两个模型是否给出了显著不同的分类结果。

McNemar 的测试操作在数据点的对上。这意味着我们需要知道,对于两个分类器,它们如何对每个数据点进行分类。基于第一个分类器正确分类但第二个分类器错误分类的数据点数量以及反之亦然,我们可以确定两个分类器是否等效。

假设先前的模型 A 和模型 B 都应用于相同的五个数据点。而模型 ...

使用网格搜索调整超参数

用于超参数调整最常用的工具是 网格搜索,这基本上是一个术语,意思是我们将尝试所有可能的参数组合,使用 for 循环。

让我们看看在实践中是如何做到这一点的。

实现简单的网格搜索

回到我们的 kNN 分类器,我们发现我们只有一个超参数需要调整:k。通常,你会有更多的开放参数可以调整,但 kNN 算法足够简单,我们可以手动实现网格搜索。

在我们开始之前,我们需要将数据集分割成训练集和测试集,就像我们之前做的那样:

  1. 在这里,我们选择 75-25 的分割:
In [1]: from sklearn.datasets import load_iris...     import numpy as np...     iris = load_iris()...     X = iris.data.astype(np.float32)...     y = iris.targetIn [2]: X_train, X_test, y_train, y_test = train_test_split(...          X, y, random_state=37...      )
  1. 然后,目标是遍历所有可能的 k 值。当我们这样做的时候,我们希望保持 ...

理解验证集的价值

按照我们分割数据为训练集和测试集的最佳实践,我们可能会告诉人们我们已经找到了一个在数据集上表现达到 97.4% 准确率的模型。然而,我们的结果可能并不一定推广到新数据。这个论点与本书前面提到的相同,当我们保证需要独立的评估数据集时,我们需要进行训练-测试分割。

然而,当我们上一节实现了网格搜索时,我们使用了测试集来评估网格搜索的结果并更新超参数,k。这意味着我们不能再使用测试集来评估最终数据!基于测试集准确率所做的任何模型选择都会导致测试集信息泄露到模型中。

解决这种数据的一种方法是将数据再次分割并引入所谓的 验证集。验证集与训练集和测试集不同,并且仅用于选择模型的最佳参数。在验证集上进行所有探索性分析和模型选择是一种良好的实践,并保留一个单独的测试集,该测试集仅用于最终评估。

换句话说,我们应该将数据分成三个不同的集合:

  • 训练集,用于构建模型

  • 验证集,用于选择模型的参数

  • 测试集,用于评估最终模型的性能

以下图表展示了这种三方分割:

图片

上述图表展示了如何将数据集分为训练集、验证集和测试集的示例。在实践中,三方分割通常分为两个步骤:

  1. 将数据分成两部分:一部分包含训练集和验证集,另一部分包含测试集:
      In [6]: X_trainval, X_test, y_trainval, y_test =
      ...        train_test_split(X, y, random_state=37)
      In [7]: X_trainval.shape
      Out[7]: (112, 4)
  1. 再次将X_trainval分割成合适的训练集和验证集:
      In [8]: X_train, X_valid, y_train, y_valid = train_test_split(
      ...         X_trainval, y_trainval, random_state=37
      ...     )
      In [9]: X_train.shape
      Out[9]: (84, 4)

然后,我们重复执行前面的手动网格搜索,但这次,我们将使用验证集来找到最佳的k(见代码高亮):

In [10]: best_acc = 0.0
...      best_k = 0
...      for k in range(1, 20):
...          knn = cv2.ml.KNearest_create()
...          knn.setDefaultK(k)
...          knn.train(X_train, cv2.ml.ROW_SAMPLE, y_train)
...          _, y_valid_hat = knn.predict(X_valid)
...          acc = accuracy_score(y_valid, y_valid_hat)
...          if acc >= best_acc:
...              best_acc = acc
...              best_k = k
...      best_acc, best_k
Out[10]: (1.0, 7)

我们现在发现,使用k=7best_k)可以达到 100%的验证分数(best_acc)!然而,请记住,这个分数可能过于乐观。为了了解模型的实际性能,我们需要在测试集的保留数据上对其进行测试。

为了得到最终的模型,我们可以使用在网格搜索中找到的k值,并在训练集和验证集上重新训练模型。这样,我们在构建模型时尽可能多地使用了数据,同时仍然遵守了训练-测试分割原则。

这意味着我们应该在包含训练集和验证集的X_trainval上重新训练模型,并在测试集上对其进行评分:

In [25]: knn = cv2.ml.KNearest_create()
...      knn.setDefaultK(best_k)
...      knn.train(X_trainval, cv2.ml.ROW_SAMPLE, y_trainval)
...      _, y_test_hat = knn.predict(X_test)
...      accuracy_score(y_test, y_test_hat), best_k
Out[25]: (0.94736842105263153, 7)

通过这个流程,我们在测试集上找到了令人印象深刻的 94.7%准确率。因为我们遵守了训练-测试分割原则,现在我们可以确信,这就是当分类器应用于新数据时我们可以期望的性能。虽然它没有验证阶段报告的 100%准确率高,但这仍然是一个非常不错的分数!

将网格搜索与交叉验证相结合

我们刚刚实施的网格搜索的一个潜在危险是,结果可能相对敏感于我们如何具体分割数据。毕竟,我们可能意外地选择了一个将大多数易于分类的数据点放在测试集中的分割,导致分数过于乐观。虽然一开始我们会很高兴,但一旦我们尝试在新的保留数据上使用该模型,我们会发现分类器的实际性能远低于预期。

相反,我们可以将网格搜索与交叉验证相结合。这样,数据会在网格搜索的每一步被多次分割成训练集和验证集,并在每个步骤上执行交叉验证来评估……

将网格搜索与嵌套交叉验证相结合

尽管带有交叉验证的网格搜索可以使得模型选择过程更加稳健,但你可能已经注意到我们仍然只进行了一次将数据集分为训练集和验证集的操作。因此,我们的结果可能仍然过于依赖于数据的精确训练-验证分割。

我们可以进一步将数据一次分为训练集和验证集,并使用多个分割进行交叉验证。这将导致所谓的嵌套交叉验证,其过程在下图中说明:

图片

在嵌套交叉验证中,有一个外层循环遍历网格搜索框,反复将数据分为训练集和验证集。对于这些分割中的每一个,都会运行一次网格搜索,并返回一组最佳参数值。然后,对于每个外部分割,我们使用最佳设置得到一个测试分数。

在许多参数和大数据集上运行网格搜索可能会非常计算密集。在特定的交叉验证分割上的特定参数设置可以完全独立于其他参数设置和模型进行。因此,对于网格搜索和交叉验证,跨多个 CPU 核心或集群的并行化非常重要。

现在我们知道了如何找到模型的最佳参数,让我们更详细地看看我们可以用来评分模型的不同评估指标。

使用不同的评估指标评分模型

到目前为止,我们使用准确率(正确分类样本的分数)评估分类性能,使用 R²评估回归性能。然而,这些只是许多可能的总结监督模型在给定数据集上表现良好的方法之一。在实践中,这些评估指标可能不适合我们的应用,因此在选择模型和调整参数时选择正确的指标非常重要。

在选择指标时,我们应始终牢记机器学习应用的最终目标。在实践中,我们通常不仅对做出准确的预测感兴趣,而且希望将这些预测作为更大系统的一部分 ...

选择正确的分类指标

我们在第三章,“监督学习的第一步”中讨论了几个重要的评分函数。在分类的最基本指标中包括以下内容:

  • 准确率:这计算测试集中被正确预测的数据点的数量,并将其作为测试集大小的分数返回(sklearn.metrics.accuracy_score)。这是分类器最基本的成绩函数,我们在整本书中广泛使用了它。

  • 精确率:这描述了分类器不将正样本标记为负样本的能力(sklearn.metrics.precision_score)。

  • 召回率 (或灵敏度):这描述了分类器检索所有正样本的能力(sklearn.metrics.recall_score)。

虽然精确度和召回率是重要的度量,但只看其中一个并不能给我们一个对整体情况的良好理解。总结这两个度量的一种方法被称为f 分数f 度量sklearn.metrics.f1_score),它计算精确度和召回率的调和平均值,即2(精确度 x 召回率) / (精确度 + 召回率)

有时候我们需要做的不仅仅是最大化准确性。例如,如果我们在一个商业应用中使用机器学习,那么决策应该由业务目标驱动。这些目标之一可能保证至少 90%的召回率。那么挑战就变成了开发一个模型,它仍然具有合理的准确性,同时满足所有次要要求。设定这样的目标通常被称为设定操作点

然而,在开发新系统时,往往不清楚操作点应该是什么。为了更好地理解问题,重要的是要调查所有可能的精确度和召回率的权衡,并一次性记录下来。这可以通过一个名为精确度-召回率曲线sklearn.metrics.precision_recall_curve)的工具来实现。

分析分类器行为的另一个常用工具是接收者操作特征ROC)曲线。

ROC 曲线考虑了给定分类器的所有可能阈值,类似于精确度-召回率曲线,但它显示的是假阳性率真阳性率之间的关系,而不是报告精确度和召回率。

选择合适的回归度量

回归的评估可以像我们在分类中做的那样进行详细的评估。在第三章,监督学习的第一步中,我们也讨论了一些回归的基本度量:

  • 均方误差:回归问题中最常用的误差度量是测量训练集中每个数据点的预测值和真实目标值之间的平方误差,并对所有数据点进行平均(sklearn.metrics.mean_squared_error)。

  • 解释方差:一个更复杂的度量是测量模型可以解释测试数据变异或分散的程度(sklearn.metrics.explained_variance_score)。通常,解释方差的量是通过...来测量的。

将算法链接起来形成管道

我们迄今为止讨论的大多数机器学习问题至少包括一个预处理步骤和一个分类步骤。问题越复杂,这个处理链可能就越长。一个方便的方法是将多个处理步骤粘合在一起,甚至使用Pipeline类从 scikit-learn 中进行网格搜索。

在 scikit-learn 中实现管道

Pipeline类本身具有fitpredictscore方法,这些方法的行为与 scikit-learn 中的任何其他估算器完全一样。Pipeline类的最常见用法是将不同的预处理步骤与一个监督模型(如分类器)一起串联。

让我们回到第五章中的乳腺癌数据集,使用决策树进行医疗诊断。使用 scikit-learn,我们导入数据集并将其分为训练集和测试集:

In [1]: from sklearn.datasets import load_breast_cancer...     import numpy as np...     cancer = load_breast_cancer()...     X = cancer.data.astype(np.float32)...     y = cancer.targetIn [2]: X_train, X_test, y_train, y_test = train_test_split(... X, y, random_state=37 ...

在网格搜索中使用管道

在网格搜索中使用管道的方式与使用任何其他估算器相同。

我们定义一个参数网格进行搜索,并从管道和参数网格中构建GridSearchCV。然而,在指定参数网格时,有一个细微的变化。我们需要指定每个参数属于管道的哪个步骤。我们想要调整的参数Cgamma都是SVC的参数。在前一节中,我们给这个步骤命名为"svm"。为管道定义参数网格的语法是,为每个参数指定步骤名称,然后是__(双下划线),然后是参数名称。

因此,我们将构建以下参数网格:

In [8]: param_grid = {'svm__C': [0.001, 0.01, 0.1, 1, 10, 100],
...                   'svm__gamma': [0.001, 0.01, 0.1, 1, 10, 100]}

使用这个参数网格,我们可以像往常一样使用GridSearchCV

In [9]: grid = GridSearchCV(pipe, param_grid=param_grid, cv=10)
...     grid.fit(X_train, y_train);

网格搜索中的最佳分数存储在best_score_中:

In [10]: grid.best_score_
Out[10]: 0.97652582159624413

同样,最佳参数存储在best_params_中:

In [11]: grid.best_params_
Out[11]: {'svm__C': 1, 'svm__gamma': 1}

但请记住,交叉验证的分数可能过于乐观。为了了解分类器的真实性能,我们需要在测试集上对其进行评分:

In [12]: grid.score(X_test, y_test)
Out[12]: 0.965034965034965

与我们之前做的网格搜索相比,现在,对于交叉验证的每个分割,MinMaxScaler仅使用训练分割进行重新拟合,并且测试分割的信息不会泄露到参数搜索中。

这使得构建一个将各种步骤串联起来的管道变得非常简单!你可以在管道中随意混合和匹配估算器,只需确保管道中的每个步骤都提供了一个transform方法(除了最后一个步骤)。这允许管道中的估算器生成数据的新表示,反过来,这个新表示可以用作下一个步骤的输入。

Pipeline类不仅限于预处理和分类,实际上可以连接任意数量的估算器。例如,我们可以构建一个包含特征提取、特征选择、缩放和分类的管道,总共有四个步骤。同样,最后一个步骤可以是回归或聚类而不是分类。

摘要

在本章中,我们试图通过讨论模型选择和超参数调整的最佳实践来补充我们现有的机器学习技能。你学习了如何使用网格搜索和交叉验证在 OpenCV 和 scikit-learn 中调整模型的超参数。我们还讨论了广泛的评估指标以及如何将算法链接到一个管道中。现在,你几乎准备好开始独立解决一些实际问题了。

在下一章中,你将接触到一项既令人兴奋又全新的主题,那就是 OpenVINO 工具包,它是 OpenCV 4.0 中的关键发布之一。

第十二章:使用 OpenVINO 与 OpenCV

在第一章中,我们讨论了 OpenCV 4.0 版本中的各种新功能。值得注意的是,OpenVINO 工具包。值得注意的是,OpenVINO 工具包被嵌入式视觉联盟选为 2019 年开发者工具年度大奖。

在本章中,我们将专注于如何使用 OpenVINO 工具包与 OpenCV。我们将从安装 OpenVINO 工具包开始,然后进行交互式人脸检测演示。我们还将学习如何使用 OpenVINO 模型动物园与 OpenCV 以及 OpenVINO 推理引擎IE)与 OpenCV。在本章结束时,我们还将学习如何使用 OpenCV 和 OpenVINO IE 进行图像分类。

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

  • OpenVINO 工具包安装

  • 交互式人脸检测演示

  • 使用 OpenVINO 模型动物园与 OpenCV

  • 使用 OpenVINO IE 与 OpenCV

  • 使用 OpenVINO IE 进行 OpenCV 图像分类

技术要求

您可以在此链接中查看本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter12

这里是软件和硬件要求的总结:

  • 您需要 OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都可以正常工作)。

  • 您需要 Python 3.6 版本(任何 3.x 版本的 Python 都可以)。

  • 您需要 Anaconda Python 3 来安装 Python 和所需的模块。

  • 您可以使用任何操作系统——macOS、Windows 和基于 Linux 的操作系统——使用这本书。我们建议您的系统至少有 4 GB 的 RAM。

  • 您不需要 GPU 就可以运行本书提供的代码。

OpenVINO 简介

OpenVINO(代表Open Visual Inferencing and Neural Network Optimization)。它旨在优化各种神经网络以加快推理阶段。推理,正如我们在前面的章节中讨论的,是使用训练好的神经网络生成未见输入数据的成果的过程。例如,如果一个网络被训练来分类狗或猫,那么如果我们提供 Tuffy(我们邻居的狗)的图像,它应该能够推断出该图像是狗的图像。

考虑到图像和视频在当今世界的普遍性,有很多深度神经网络被训练来执行各种操作,例如多标签分类和运动跟踪。世界上大多数推理都是在 CPU 上进行的,因为 GPU 非常昂贵,通常不适合个人 AI 工程师的预算。在这些情况下,OpenVINO 工具包提供的加速非常关键。

OpenVINO 工具包提供的加速包括两个步骤。第一步关注硬件规格;它使用随 OpenVINO 工具包一起提供的模型优化器以硬件无关的方式优化网络。下一步涉及使用 OpenVINO IE 的特定硬件加速。

OpenVINO 工具包是由英特尔开发的,英特尔以其优化的工具和硬件而闻名,专注于深度学习和人工智能。了解到 VPUs、GPU 和 FPGA 也由英特尔制造并不令人惊讶。

OpenVINO 还为 OpenCV 和 OpenVX 库提供了优化调用——这两个最著名的计算机视觉库。

OpenVINO 工具包安装

在本节中,我们将使用英特尔官方说明来安装 OpenVINO 工具包:

  1. 要开始,首先访问 OpenVINO 工具包下载页面(software.intel.com/en-us/openvino-toolkit/choose-download),根据您的系统规格选择并下载安装程序。您必须首先注册工具包的副本。

  2. 使用安装说明(docs.openvinotoolkit.org/latest/index.html)在您的系统上安装 OpenVINO 工具包。

OpenVINO 工具包还将安装其自己的英特尔优化版本的 OpenCV。如果您已经在系统上安装了 OpenCV,安装程序将显示已安装了另一个版本的 OpenCV。最好是安装 ...

OpenVINO 组件

OpenVINO 工具包包括以下主要组件:

  • 深度学习部署工具包DLDT)包括模型优化器、IE、预训练模型以及一些帮助您测量模型精度的工具。

  • 有针对英特尔库(这些库也是经过优化的)编译的 OpenCV 优化版本。

  • 有 OpenCL 库。

  • 您将获得英特尔媒体 SDK 以加速视频处理。

  • 也有针对 OpenVX 的优化版本。

交互式人脸检测演示

OpenVINO 工具包安装还提供了各种演示和示例应用程序。为了测试安装,让我们看看我们是否可以运行交互式人脸检测演示。

首先,我们将移动到 deployment_tools/inference_engine 文件夹中的 samples 目录。在这里,您可以找到各种演示应用程序,例如图像分类和推理管道。

交互式人脸检测演示接受视频作为输入,并执行人脸检测以及年龄、性别、头部姿态、情绪和面部特征点检测。根据您要执行的类型的人脸检测,您可以使用以下预训练模型列表中的一个模型:

  • 您可以使用以下方式仅进行人脸检测

使用 OpenVINO 推理引擎与 OpenCV

在前面的章节中,我们讨论了如何运行交互式人脸检测演示。那很好,但仍然存在的问题是如何利用 OpenVINO 的力量与您现有的 OpenCV 代码相结合。请注意,在这里,我们强调的是在代码中尽可能少地修改的情况下利用 OpenVINO 的优势。这非常重要,因为 OpenVINO 并未出现在 OpenCV 的早期版本中,包括更常用的版本 3.4.3。作为一个优秀的开发者,你的任务是确保你的程序支持尽可能多的系统和库。

幸运的是,我们只需一行代码就可以开始使用 OpenVINO 推理引擎对 OpenCV 模型的推理代码进行操作,如下面的代码片段所示:

cv::dnn::setPreferableBackend(DNN_BACKEND_INFERENCE_ENGINE); // C++
setPreferableBackend(cv2.dnn.DNN_BACKEND_INFERENCE_ENGINE) # Python

就这样!在一个完整的示例中,你将这样使用它:

net = cv2.dnn.readNetFromCaffe(prototxt,model)
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_INFERENCE_ENGINE)

在这里,你可以使用读取你的神经网络的任何其他方法。在这种情况下,我们是从 .prototxt.caffemodel 文件中读取 Caffe 模型。

类似地,在 C++ 的情况下,我们可以这样使用它:

Net net = readNetFromCaffe(prototxt, model);
net.setPreferableBackend(DNN_BACKEND_INFERENCE_ENGINE);

使用 OpenVINO 模型库与 OpenCV

在前面的章节中,我们简要讨论了 OpenVINO 模型库以及如何使用 OpenVINO IE 与 OpenCV。在本节中,我们将更深入地了解模型库及其提供的功能。

OpenVINO 模型库是一个包含优化后的预训练模型的集合,可以直接导入 OpenVINO 进行推理。这一功能的重要性在于,OpenVINO 加速的主要原因是它用于推理的优化模型文件。其底层推理原理与大多数深度学习推理工具包和语言(如 OpenCV)相同。OpenCV 的 dnn 模块通过将其作为所有推理任务的后端默认选项来使用 OpenVINO 的这一加速原理。...

使用 OpenVINO 推理引擎进行图像分类的 OpenCV

本章我们将讨论的最后一个主题是使用 OpenCV 和 OpenVINO 推理引擎进行图像分类。

在我们深入细节之前,让我们简要地看一下图像分类问题。图像分类,也称为图像识别,是深度学习任务集的一部分,可能是最常见的一种。在这个任务中,一组图像作为输入提供给模型,模型输出输入图像的类别或标签。

这类的一个常见例子是猫狗分类问题,其中模型在大量猫和狗的图像上训练,然后在测试阶段,模型预测输入图像是猫还是狗的图像。

虽然这看起来可能是一个非常简单的问题,但在工业应用中,图像分类具有很大的重要性。例如,如果你的相机声称拥有 AI 功能,这意味着它可以识别图像中的物体,并相应地更改图像设置——无论是自然风景的图像还是 Instagram 上值得分享的食物照片。以下图像显示了 AI 手机摄像头的输出:

考虑我之前拍摄的房屋屋顶的图像。请注意,当相机切换到 AI 模式时,它能够检测到我在拍摄植物,并自动更改设置以匹配。所有这一切都只因为图像分类。现在,考虑一下,作为计算机视觉工程师的你,正在尝试训练一个模型,该模型可以识别图像是植物、瀑布还是人类。

如果你的模型无法在几毫秒内推断出图像的类别或标签,你投入的所有训练模型的努力都将白费。没有用户愿意等待几秒钟,让相机检测物体并更改设置。

这使我们回到了 OpenVINO 推理引擎的重要性。OpenVINO 有一个自己的图像分类工具包版本,可以如下使用。

使用 OpenVINO 进行图像分类

让我们看看我们如何使用 OpenVINO 安装目录中现有的图像分类演示:

  1. 首先,移动到你的 OpenVINO 安装目录中的deployment_tools/demo目录。

  2. 接下来,让我们在目录中已经存在的示例图像上运行图像分类:

./demo_squeezenet_download_convert_run.sh

这是我的结果:

让我们再运行另一个使用相同图像的演示,即推理管道演示,它很好地展示了 OpenVINO 推理引擎的速度:

./demo_security_barrier_camera.sh

这是输出图像:

由于我们正在使用相同的图像在 ...

使用 OpenCV 和 OpenVINO 进行图像分类

让我们首先使用 OpenCV 创建一个图像分类推理代码。由于我们只关心推理,我们将使用一个预训练的模型:

  1. 首先,让我们下载 Caffe 模型文件,deploy.prototxtbvlc_reference_caffenet.caffemodel,这些可以从伯克利视觉的仓库中获得(github.com/BVLC/caffe/tree/master/models/bvlc_reference_caffenet)。确保你在当前工作目录中下载这两个文件。我们还需要一个包含提到的类别标签的文本文件。你可以从github.com/torch/tutorials/blob/master/7_imagenet_classification/synset_words.txt获取它。

  2. 让我们再使用一张长颈鹿的样本图像进行图像分类:

接下来,让我们开始编写使用 OpenCV 和 OpenVINO 进行图像分类的代码:

  1. 让我们从导入一些模块开始:
import numpy as np
import cv2
  1. 接下来,让我们指定模型文件:
image = cv2.imread("animal-barbaric-brown-1319515.jpg")
labels_file = "synset_words.txt"
prototxt = "deploy.prototxt"
caffemodel = "bvlc_reference_caffenet.caffemodel"

  1. 现在,让我们从标签文本文件中读取标签:
rows = open(labels_file).read().strip().split("\n")
classes = [r[r.find(" ") + 1:].split(",")[0] for r in rows]
  1. 让我们指定我们将用于推理的后端:
net = cv2.dnn.readNetFromCaffe(prototxt,caffemodel)
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_INFERENCE_ENGINE)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
  1. 让我们在输入图像上执行一些基本的图像处理:
blob = cv2.dnn.blobFromImage(image,1,(224,224),(104,117,123))
  1. 最后,让我们将此图像传递给模型并获取输出:
net.setInput(blob)
predictions = net.forward()
  1. 让我们获取我们传递给模型的 giraffe 图像的前 10 个预测结果:
indices = np.argsort(predictions[0])[::-1][:5]
  1. 最后,让我们显示前 10 个预测结果:
for index in indices:
 print("label: {}, prob.: {:.5}".format(classes[index], predictions[0][index]))

意想不到的是,这里是我们获得的结果:

label: cheetah, prob.: 0.98357
label: leopard, prob.: 0.016108
label: snow leopard, prob.: 7.2455e-05
label: jaguar, prob.: 4.5286e-05
label: prairie chicken, prob.: 3.8205e-05

注意,我们的模型认为我们作为输入传递的 giraffe 图像实际上是一张 cheetah 图像。你认为这是为什么?那是因为 giraffe 并不在我们拥有的类别列表中。因此,模型找到了最接近的匹配项,这是由于在猎豹和长颈鹿上都有相似的彩色斑点。所以,下次你进行图像分类时,请确保类别确实存在于标签列表中。

我们还可以比较各种后端,以查看使用 OpenVINO 推理引擎作为后端获得的速度提升。以下是实现方法。我们只需要更改前面代码中的一行:

net.setPreferableBackend(cv2.dnn.DNN_BACKEND_INFERENCE_ENGINE)

我们可以在以下后端之间进行选择:

  • cv2.dnn.DNN_BACKEND_DEFAULT:如果你已安装 OpenVINO 并将其用作默认后端。

  • cv2.dnn.DNN_BACKEND_HALIDE:这需要使用 Halide 构建 OpenCV。你可以在docs.opencv.org/4.1.0/de/d37/tutorial_dnn_halide.html找到有关此内容的详细文档。

  • cv2.dnn.DNN_BACKEND_OPENCV:这是在两个后端之间进行比较的最佳选择。

因此,你需要做的就是运行相同的代码,但将前面的代码行替换为以下内容:

net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)

就这样!你现在可以使用 OpenVINO 的推理引擎作为后端进行速度比较了。

你不会在速度上看到太大的差异。为了获得明显的差异,使用 for 循环进行 100 次推理,然后将每个步骤所花费的总时间相加,最后除以 100 以获得平均值。

摘要

在本章中,我们简要介绍了 OpenVINO 工具包——它是什么,它用于什么,以及我们如何安装它。我们还探讨了如何运行工具包提供的演示和示例,以了解和见证 OpenVINO 的强大功能。最后,我们看到了如何通过仅添加一行指定用于模型推理的后端来在我们的现有 OpenCV 代码中利用这种功能。

你可能也注意到,我们在这章中没有涵盖太多动手实践的内容。这是因为 OpenVINO 更适合深度学习应用,而这本书的范围不包括这些内容。如果你是深度学习的爱好者,你绝对应该阅读英特尔提供的关于 OpenVINO 工具包的文档,并开始学习。 ...

第十三章:结论

恭喜!您已经迈出了成为机器学习实践者的重要一步。您不仅熟悉各种基础机器学习算法,而且知道如何将它们应用于监督学习和无监督学习问题。此外,您还接触到了一个新颖且令人兴奋的主题——OpenVINO 工具包。在前一章中,我们学习了如何安装 OpenVINO 并运行交互式的人脸检测和图像分类演示等。我相信您已经享受了学习这些主题的过程。

在我们分别之前,我想给您一些建议,指引您一些额外的资源,并给出一些如何进一步提升您的机器学习和数据科学技能的建议。在本章中,我们将学习如何处理机器学习问题并构建自己的估计器。我们将学习如何使用 C++编写基于 OpenCV 的分类器,以及使用 Python 编写基于 scikit-learn 的分类器。

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

  • 处理机器学习问题

  • 在 C++中编写自己的 OpenCV 分类器

  • 在 Python 中编写自己的 scikit-learn 分类器

  • 接下来该做什么

技术要求

您可以从以下链接中获取本章的代码:github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/Chapter13

以下是软件和硬件要求的总结:

  • 您需要 OpenCV 版本 4.1.x(4.1.0 或 4.1.1 都完全可以)。

  • 您需要 Python 3.6 版本(任何 3.x 版本的 Python 都适用)。

  • 您需要 Anaconda Python 3 来安装 Python 和所需的模块。

  • 您可以使用任何操作系统——macOS、Windows 以及基于 Linux 的操作系统,配合这本书使用。我们建议您的系统至少拥有 4 GB 的 RAM。

  • 您不需要 GPU 来运行本书附带提供的代码。

处理机器学习问题

当您在野外遇到一个新的机器学习问题时,您可能会迫不及待地跳进去,用您最喜欢的算法来解决问题——可能是您最理解或最享受实现的一个。但事先知道哪种算法会在您特定的问题上表现最佳通常是不可能的。

而不是,你需要退一步,从更大的角度去看待问题。在你深入之前,你将想要定义你试图解决的真正问题。例如,你已经有了一个具体的目标,还是只是想要进行一些探索性分析,在数据中找到一些有趣的东西?通常,你可能会从一个一般的目标开始,比如检测垃圾邮件、制作电影推荐,或者自动在社交媒体平台上上传的图片中标记你的朋友。然而,正如我们在整本书中看到的,解决一个问题的方法往往有很多种。例如,我们使用逻辑回归、k-means 聚类和深度学习来识别手写数字。定义问题将帮助你提出正确的问题,并在过程中做出正确的选择。

作为一种经验法则,你可以使用以下五个步骤来处理现实世界中的机器学习问题:

  1. 对问题进行分类:这是一个两步的过程:

    • 按输入分类:简单来说,如果你有标记过的数据,那么这是一个监督学习问题。如果你有未标记的数据,并希望找到结构,那么这是一个无监督学习问题。如果你想要通过与环境的交互来优化目标函数,那么这是一个强化学习问题。

    • 按输出分类:如果你的模型输出是一个数字,那么这是一个回归问题。如果你的模型输出是一个类别(或分类),那么这是一个分类问题。如果你的模型输出是一组输入组,那么这是一个聚类问题。

  2. 找到可用的算法:现在你已经对问题进行了分类,你可以识别出可以使用我们拥有的工具实现的适用且实用的算法。微软创建了一个方便的算法速查表,显示了哪些算法可以用于哪些类别的问题。尽管速查表是为微软 Azure量身定制的,但你可能会发现它通常很有帮助。

机器学习算法速查表 PDF(由微软 Azure 提供)可以从aka.ms/MLCheatSheet下载。

  1. 实现所有适用的算法原型设计):对于任何给定的问题,通常有一小批候选算法可以完成这项工作。那么,你如何知道选择哪一个呢?通常,这个问题的答案并不直接,所以你必须求助于试错。原型设计最好分两步进行:

    1. 你应该追求快速且粗略地实现几个算法,并尽量减少特征工程。在这个阶段,你应该主要关注看到哪个算法在粗略尺度上表现更好。这一步有点像招聘:你正在寻找任何理由来缩短候选算法的列表。一旦你将列表缩减到几个候选算法,真正的原型设计就开始了。

    2. 理想情况下,你想要设置一个机器学习管道,使用一组精心选择的评估标准来比较每个算法在数据集上的性能(参见第十一章,使用超参数调整选择正确的模型)。在这个阶段,你应该只处理少数几个算法,因此你可以将注意力转向真正的魔法所在:特征工程。

  2. 特征工程:选择正确的算法可能比选择正确的特征来表示数据更为重要。你可以在第四章,表示数据和特征工程中了解有关特征工程的所有内容。

  3. 优化超参数:最后,你还需要优化算法的超参数。例如,可能包括 PCA 的主成分数量、k 最近邻算法中的参数k,或者神经网络中的层数和学习率。你可以参考第十一章,使用超参数调整选择正确的模型,以获取灵感。

构建自己的估计器

在这本书中,我们探讨了 OpenCV 提供的各种机器学习工具和算法。如果出于某种原因,OpenCV 没有提供我们想要的,我们总是可以退回到 scikit-learn。

然而,当处理更高级的问题时,你可能会发现自己想要执行一些非常具体的数据处理,而这些处理既不是 OpenCV 也不是 scikit-learn 提供的,或者你可能想要对现有算法进行轻微的调整。在这种情况下,你可能想要创建自己的估计器。

使用 C++编写自己的 OpenCV 分类器

由于 OpenCV 是那些底层不包含任何 Python 代码的 Python 库之一(我在开玩笑,但确实如此),你将不得不在 C++中实现自定义估计器。这可以通过以下四个步骤完成:

  1. 实现一个包含主要源代码的 C++源文件。你需要包含两个头文件,一个包含所有 OpenCV 的核心功能(opencv.hpp),另一个包含机器学习模块(ml.hpp):
#include <opencv2/opencv.hpp>
#include <opencv2/ml/ml.hpp>
#include <stdio.h>

然后,可以通过从StatModel类继承来创建一个估计器类:

class MyClass : public cv::ml::StatModel
{
    public:

接下来,你定义类的构造函数析构函数

MyClass()
{
    print("MyClass constructor\n");
}
~MyClass() {}

然后,你还必须定义一些方法。这些是你需要填写以使分类器真正做一些工作的内容:

int getVarCount() const
{
    // returns the number of variables in training samples
    return 0;
}

bool empty() const
{
    return true;
}

bool isTrained() const
{
    // returns true if the model is trained
    return false;
}

bool isClassifier() const
{
    // returns true if the model is a classifier
    return true;
}

主要工作是在train方法中完成的,该方法有两种形式(接受cv::ml::TrainDatacv::InputArray作为输入):

bool train(const cv::Ptr<cv::ml::TrainData>& trainData,
          int flags=0) const
{
    // trains the model
    return false;
}

bool train(cv::InputArray samples, int layout, 
          cv::InputArray responses)
{
    // trains the model
    return false;
}

你还需要提供一个predict方法和一个scoring函数:

        float predict(cv::InputArray samples,
                     cv::OutputArray results=cv::noArray(),
                     int flags=0) const
        {
            // predicts responses for the provided samples
            return 0.0f;
        }

        float calcError(const cv::Ptr<cv::ml::TrainData>& data,
                       bool test, cv::OutputArray resp)
        {
            // calculates the error on the training or test dataset
            return 0.0f;
        }
   };

最后一件要做的事情是包含一个main函数,该函数实例化类:

   int main()
   {
       MyClass myclass;
       return 0;
   }
  1. 编写一个名为CMakeLists.txt的 CMake 文件:
cmake_minimum_required(VERSION 2.8)
project(MyClass)
find_package(OpenCV REQUIRED)
add_executable(MyClass MyClass.cpp)
target_link_libraries(MyClass ${OpenCV_LIBS})
  1. 通过在命令行中输入以下命令来编译文件:
$ cmake
$ make
  1. 运行由最后一个命令生成的可执行文件MyClass方法,它应该导致以下输出:
$ ./MyClass
MyClass constructor

在 Python 中编写基于 scikit-learn 的分类器

或者,你可以使用 scikit-learn 库编写自己的分类器。

你可以通过导入BaseEstimatorClassifierMixin来实现这一点。后者将提供一个相应的score方法,它适用于所有分类器:

  1. 可选地,首先,你可以重写score方法以提供你自己的score指标方法:
In [1]: import numpy as np...     from sklearn.base import BaseEstimator, ClassifierMixin
  1. 然后,你可以定义一个继承自BaseEstimatorClassifierMixin的类:
In [2]: class MyClassifier(BaseEstimator, ClassifierMixin):...         """An example classifier"""
  1. 你需要提供一个构造函数、fitpredict方法。构造函数定义了所有参数...

接下来该怎么做

本书的目标是让你了解机器学习的世界,并为你成为机器学习从业者做好准备。现在你已经了解了所有基本算法,你可能想要深入研究一些主题。

虽然理解我们在本书中实现的所有算法的所有细节不是必要的,但了解它们背后的某些理论可能会让你成为一个更好的数据科学家。

如果你正在寻找更高级的材料,那么你可能想要考虑以下一些经典作品:

  • Stephen Marsland,机器学习:算法视角第二版,Chapman and Hall/Crc,ISBN 978-146658328-3,2014

  • Christopher M. Bishop,模式识别与机器学习。Springer,ISBN 978-038731073-2,2007

  • Trevor Hastie,Robert Tibshirani,和 Jerome Friedman,统计学习元素:数据挖掘、推理和预测第二版,Springer,ISBN 978-038784857-0,2016

当谈到软件库时,我们已经学习了两个基本库——OpenCV 和 scikit-learn。通常,使用 Python 非常适合尝试和评估模型,但更大的网络服务和应用程序更常见地使用 Java 或 C++编写。

例如,C++包是Vowpal Wabbit(VW),它自带命令行界面。对于在集群上运行机器学习算法,人们通常使用基于SparkScalamllib。如果你不坚持使用 Python,你也可以考虑使用 R,这是数据科学家常用的另一种语言。R 是一种专门为统计分析设计的语言,以其可视化能力和许多(通常是高度专业化的)统计建模包而闻名。

无论你接下来选择哪种软件,我认为最重要的建议是持续练习你的技能。但这一点你已经知道了。有许多优秀的数据集正等着你去分析:

  • 在整本书中,我们充分利用了 scikit-learn 内置的示例数据集。此外,scikit-learn 提供了一种从外部服务加载数据集的方法,例如mldata.org。有关更多信息,请参阅scikit-learn.org/stable/datasets/index.html

  • Kaggle 是一家在其网站上托管各种数据集和竞赛的公司,www.kaggle.com。竞赛通常由各种公司、非营利组织和大学举办,获胜者可以赢得一些相当可观的现金奖励。竞赛的缺点是它们已经提供了一种特定的指标来优化,并且通常是一个固定、预处理的数据集。

  • OpenML 平台(www.openml.org)托管了超过 20,000 个数据集,与超过 50,000 个相关的机器学习任务相关联。

  • 另一个流行的选择是 UC Irvine 机器学习仓库(archive.ics.uci.edu/ml/index.php),通过可搜索的界面托管了 370 多个流行且维护良好的数据集。

最后,如果你在寻找更多的 Python 示例代码,现在有许多优秀的书籍都附带了自己的 GitHub 仓库:

摘要

在这一章中,我们学习了如何处理机器学习问题,并构建了自己的估计器。我们学习了如何用 C++编写基于 OpenCV 的分类器,以及用 Python 编写基于 scikit-learn 的分类器。

在这本书中,我们涵盖了大量的理论和实践。我们讨论了各种基本的机器学习算法,无论是监督学习还是无监督学习,还介绍了最佳实践以及避免常见陷阱的方法,并且触及了数据分析、机器学习和可视化的各种命令和包。

如果你已经走到这一步,你已经朝着机器学习精通迈出了重要的一步。从现在开始,我坚信你将能够独立完成得很好。

剩下的只有告别了!...

posted @ 2025-09-21 12:11  绝不原创的飞龙  阅读(55)  评论(0)    收藏  举报