面向-OpenCV-的机器学习-全-
面向 OpenCV 的机器学习(全)
原文:Machine Learning for OpenCV
协议:CC BY-NC-SA 4.0
零、前言
随着世界的变化和人类制造更智能更好的机器,对机器学习和计算机视觉专家的需求也在增加。机器学习,顾名思义,就是给定某一组参数作为输入,进行预测的机器学习过程。另一方面,计算机视觉给出了机器视觉;也就是说,它使机器意识到视觉信息。当你结合这些技术时,你会得到一台可以使用视觉数据进行预测的机器,这让机器离拥有人类能力又近了一步。当你加入深度学习,机器甚至可以在预测方面超越人类的能力。这看似牵强,但随着人工智能系统取代基于决策的系统,这实际上已经成为现实。你有人工智能摄像机、人工智能显示器、人工智能音响系统、人工智能处理器等等。我们不能向您承诺,在阅读本书后,您将能够构建一个人工智能相机,但我们确实打算为您提供这样做所需的工具。我们将要介绍的最强大的工具是 OpenCV 库,它是世界上最大的计算机视觉库。尽管它在机器学习中的使用并不常见,但我们已经提供了一些关于如何将其用于机器学习的示例和概念。在本书中,我们采用了动手的方法,我们建议您尝试本书中的每一段代码来构建一个展示您的知识的应用程序。世界在变化,这本书是我们帮助年轻人变得更好的方法。
这本书是给谁的
我们试图从头开始解释所有的概念,使这本书既适合初学者,也适合高级读者。我们建议读者具备一些 Python 编程的基础知识,但这不是强制性的。每当你遇到一些你无法理解的 Python 语法时,一定要在网上查找。帮助总是提供给寻找帮助的人。
充分利用这本书
如果您是 Python 初学者,我们建议您阅读任何好的 Python 编程书籍或在线教程或视频。也可以看看 DataCamp(www.datacamp.com
)用互动课学习 Python。
我们还建议您学习 Python 中 Matplotlib 库的一些基本概念。你可以试试这个教程:https://www . datacamp . com/community/tutories/matplotlib-tutory-python。
在开始阅读这本书之前,你不需要在你的系统上安装任何东西。我们将在第一章中介绍所有的安装步骤。
下载示例代码文件
你可以从你在www.packt.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
- 登录或注册www.packt.com。
- 选择“支持”选项卡。
- 点击代码下载。
- 在搜索框中输入图书的名称,并按照屏幕指示进行操作。
下载文件后,请确保使用最新版本的解压缩文件夹:
- 视窗系统的 WinRAR/7-Zip
- zipeg/izp/un ARX for MAC
- 适用于 Linux 的 7-Zip/PeaZip
这本书的代码包也在 GitHub 上发布,网址为 https://GitHub . com/packt publishing/Machine-Learning for-OpenCV-第二版...
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:www . packtpub . com/sites/default/files/downloads/9781789536300 _ color images . pdf
。
使用的约定
本书通篇使用了许多文本约定。
CodeInText
:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“我们可以设置max_samples<1.0
和max_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
粗体:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。
Warnings ...
取得联系
我们随时欢迎读者的反馈。
一般反馈:如果你对这本书的任何方面有疑问,在你的信息主题中提到书名,发邮件给我们customercare@packtpub.com
。
勘误表:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata,选择您的图书,点击勘误表提交链接,并输入详细信息。
盗版:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com
联系我们,并提供材料链接。
如果你有兴趣成为一名作者:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问authors.packtpub.com。
复习
请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家!
更多关于 Packt 的信息,请访问packt.com。
一、机器学习的风格
所以,你已经决定进入机器学习领域。太好了!
如今,机器学习无处不在——从保护我们的电子邮件,到在图片中自动标记我们的朋友,再到预测我们喜欢什么电影。机器学习作为人工智能的一种形式,使计算机能够通过经验进行学习;利用从过去收集的数据来预测未来。最重要的是,计算机视觉是当今机器学习最令人兴奋的应用领域之一,深度学习和卷积神经网络驱动着自动驾驶汽车和谷歌的 DeepMind 等创新系统。
然而,不要烦恼;您的应用程序不需要如此大规模或改变世界...
技术要求
可以通过以下链接查阅本章代码:github . com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/chapter 01
。
以下是软件和硬件要求的简短总结:
- OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以正常工作)。
- Python 3.6 版本(任何 Python 3 . x 版本都可以)。
- Anaconda Python 3,用于安装 Python 和所需的模块。
- 这本书可以使用任何操作系统——苹果操作系统、视窗操作系统和基于 Linux 的操作系统。我们建议您的系统中至少有 4 GB 内存。
- 运行本书提供的代码不需要 GPU。
机器学习入门
机器学习已经存在至少 60 年了。出于对人工智能的追求,早期的机器学习系统推断出if...else
语句的手动编码规则来处理数据和做出决策。设想一个垃圾邮件过滤器,其工作是解析传入的电子邮件,并将不需要的邮件移动到垃圾邮件文件夹中,如下图所示:
我们可以想出一个单词黑名单,当它们出现在邮件中时,会将电子邮件标记为垃圾邮件。这是一个手工编码专家系统的简单例子。(我们将在第七章中构建一个更智能的实施垃圾邮件...
机器学习可以解决的问题
大多数机器学习问题属于以下三个主要类别之一:
在监督学习中,我们有一个数据点的标签。现在,这可以是图像中捕获的对象的类别、面部周围的边界框、图像中出现的数字或任何其他东西。把它想象成一个老师,他不仅教书,还告诉你问题的正确答案是什么。现在,学生可以尝试设计一个模型或方程,考虑所有的问题及其正确答案,并找出一个有(或没有)正确答案的问题的答案。进入模型学习的数据称为训练数据,测试过程/模型的数据称为测试数据。这些预测有两种风格,例如用正确的动物识别新照片(称为分类 问题)或为其他二手车分配准确的销售价格(称为回归 问题)。不要担心这是否有点超出你的理解范围——我们会用整本书来确定细节。
在无监督学习中,数据点没有关联的标签(第八章,用发现隐藏结构无监督学习)。把它想象成一堂课,老师给你一个混乱的谜题,让你自己去想该怎么做。在这里,最常见的结果是 c 光泽,其中包含具有相似特征的对象。它还会导致以不同的方式查看高维数据(复杂数据),从而使其看起来更简单。
强化学习是关于在一个问题中获得最大回报。所以,如果老师每答对一个问题就给你一颗糖,每答错一个问题就惩罚你,那他/她就是在强化概念,让你增加收到糖果的次数,而不是让你受到惩罚的次数。
下图说明了这三个主要类别:
既然我们已经介绍了主要的机器学习类别,那么让我们回顾一下 Python 中的一些概念,这些概念在本书的旅程中将会非常有用。
Python 入门
Python 已经成为许多数据科学和机器学习应用程序的通用语言,这要归功于它为数据加载、数据可视化、统计、图像处理和自然语言处理等过程提供的大量开源库。使用 Python 的主要优势之一是能够直接与代码交互,使用终端或其他工具,如 Jupyter Notebook ,我们将在稍后介绍。
如果你大部分时间都在结合 C使用 OpenCV,我强烈建议你改用 Python,至少是为了学习这本书。这个决定不是出于恶意做出的!恰恰相反:我已经完成了相当一部分 C/C编程——尤其是...
OpenCV 入门
作为 OpenCV 的狂热用户,我相信你是,我可能不需要说服你 OpenCV 的力量。
OpenCV 旨在为计算机视觉应用程序提供通用基础设施,现已成为一套全面的经典和最先进的计算机视觉和机器学习算法。根据他们自己的文档,OpenCV 拥有超过 47,000 人的用户社区,下载量超过 700 万次。太令人印象深刻了!作为一个开源项目,研究人员、企业和政府机构很容易利用和修改已经可用的代码。
也就是说,作为最近机器学习热潮的一部分,出现了许多开源机器学习库,它们提供的功能比 OpenCV 多得多。一个突出的例子是 scikit-learn ,它提供了许多最先进的机器学习算法以及大量的在线教程和代码片段。由于 OpenCV 的开发主要是为了提供计算机视觉算法,其机器学习功能被限制在一个名为ml
的模块中。正如我们将在本书中看到的,OpenCV 仍然提供了许多最先进的算法,但有时在功能上有点欠缺。在这些罕见的情况下,我们将简单地使用 scikit-learn 来达到我们的目的,而不是重新发明轮子。
最后但并非最不重要的一点是,使用 Python Anaconda 发行版安装 OpenCV 本质上是一个单行程序,我们将在下面的章节中看到。
If you are a more advanced user who wants to build real-time applications, OpenCV's algorithms are well-optimized for this task, and Python provides several ways to speed up computations where it is necessary (using, for example, Cython or parallel processing libraries such as joblib or dask).
装置
在开始之前,让我们确保已经安装了创建一个全面运行的数据科学环境所必需的所有工具和库。从 GitHub 下载本书的最新代码后,我们将安装以下软件:
- Python 的 Anaconda 发行版,基于 Python 3.6 或更高版本
- OpenCV 4.1
- 一些支持包
Don't feel like installing stuff? You can also visit mybinder.org/v2/gh/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/master
, where you will find all the code for this book in an interactive, executable environment and 100% free and open source, thanks to the Binder project.
获取这本书的最新代码
你可以从 GitHub 获得这本书的最新代码:GitHub . com/PacktPublishing/机器学习 for OpenCV-第二版
。您可以下载一个.zip
包(初学者)或者使用 Git(中间用户)克隆存储库。
Git is a version control system that allows you to track changes in files and collaborate with others on your code. In addition, the web platform GitHub makes it easy for people to share their code with you on a public server. As I make improvements to the code, you can easily update your local copy, file bug reports, or suggest code changes.
如果选择用 git 的话,第一步就是要确定安装了(git-scm.com/downloads
)。
然后,打开一个终端(或命令提示符,在窗口中称为):
- 在 Windows 10 上,右键单击开始菜单按钮,然后选择命令提示符。
- 在 macOS X 上,按 Cmd + Space 打开聚光灯搜索,然后输入
terminal
,点击进入。 - 在 Ubuntu、Linux/Unix 和朋友上,按 Ctrl + Alt + T 。在红帽上,右键单击桌面并从菜单中选择打开终端。
导航到要下载代码的目录:
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:
- 在终端中,导航到下载以下代码的目录:
$ cd Desktop/OpenCV-ML
- 然后,运行以下命令创建一个基于 Python 3.6 的 conda 环境,该环境还将一次性安装
environment.yml
文件(可在 GitHub 存储库中获得)中列出的所有必要的包:
$ conda create env -f environment.yml
- 你也可以看看下面的
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
Notice that the environment's name will be OpenCV-ML
. This code will use the conda-forge
channel to download all the conda
based dependencies and use pip
to install OpenCV 4.0 (along with opencv_contrib)
.
- 要激活环境,请根据您的平台键入以下内容之一:
$ source activate OpenCV-ML # on Linux / Mac OS X
$ activate OpenCV-ML # on Windows
- 当我们关闭终端时,会话将被停用,因此下次我们打开新的终端时,必须再次运行最后一个命令。我们还可以手动关闭环境:
$ 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__']
If you have installed an older version of OpenCV, the ml
module might not be present. For example, the k-nearest neighbor algorithm (which we will talk about in Chapter 3, First Steps in Supervised Learning) used to be called cv2.KNearest()
but is now called cv2.ml.KNearest_create()
. In order to avoid confusion throughout the book, I recommend using OpenCV 4.0.
这一切都很好,但你现在会想为什么你甚至应该学习机器学习,以及它的应用是什么?让我们在下一节回答这个问题。
机器学习的应用
机器学习、人工智能、深度学习和数据科学是我认为将改变我们一直以来看待事物的方式的四个术语。让我们看看我是否能说服你我为什么这么相信。
从让计算机学习如何玩围棋并击败同一个游戏的世界冠军,到使用同一个分支仅仅通过观察一个人的大脑的电脑断层扫描来检测他是否有肿瘤,机器学习在每个领域都留下了印记。我参与的项目之一是利用机器学习来确定火电厂锅炉水冷壁管的剩余寿命周期。所提出的解决方案通过使用...
OpenCV 4.0 有什么新功能?
所以,我们来到了第一章的最后一节。我将保持简短和中肯,因为你作为一个读者可以安全地跳过它。我们讨论的话题是 OpenCV 4.0 。
OpenCV 4.0 是 OpenCV 经过三年半的努力和 bug 修复的结果,最终在 2018 年 11 月发布。在本节中,我们将了解 OpenCV 4.0 中的一些主要变化和新功能:
- 随着 OpenCV 4.0 的发布,OpenCV 正式成为 C11 库。这意味着当您试图编译 OpenCV 4.0 时,您必须确保系统中存在符合 C11 的编译器。
- 延续上一点,删除了很多 C 语言的 API。受到影响的一些模块包括视频输入输出模块(
videoio
)、物体检测模块(objdetect
)等。XML、YAML 和 JSON 的文件 IO 也删除了 C API。 - OpenCV 4.0 在 DNN 模块(深度学习模块)中也有很多改进。增加了 ONNX 支持。英特尔 OpenVINO 也标志着其在全新 OpenCV 版本中的出现。我们将在后面的章节中更详细地探讨这个问题。
- OpenCL 加速已经在 AMD 和 NVIDIA GPUs 上修复。
- 还增加了 OpenCV Graph API,这是一个高效的图像处理和其他操作的引擎。
- 正如在每一个 OpenCV 版本中一样,为了提高性能,有很多变化。还增加了一些新功能,如二维码检测和解码。
简而言之,OpenCV 4.0 有很多变化,它们有自己的用途。例如,ONNX 支持有助于模型跨各种语言和框架的可移植性,OpenCL 减少了计算机视觉应用程序的运行时间,Graph API 有助于提高应用程序的效率,OpenVINO 工具包使用英特尔的处理器和模型动物园来提供高效的深度学习模型。在后面的章节中,我们将主要关注 OpenVINO 工具包和 DLDT 以及加速计算机视觉应用。但是,我也应该在这里指出,OpenCV 3.4.4 和 OpenCV 4.0.0 都在高速修改以修复 bug。因此,如果您打算在任何应用程序中使用它们中的任何一个,请准备好修改您的代码和安装,以纳入所做的更改。同样,OpenCV 4.0.1 和 OpenCV 3.4.5 也是在前几个月内推出的。
摘要
在这一章中,我们在高抽象层次上讨论了机器学习:它是什么,为什么它很重要,以及它能解决什么样的问题。我们了解到机器学习问题有三种形式:监督学习、无监督学习和强化学习。我们谈到了监督学习的突出性,认为这个领域可以进一步分为两个子领域:分类和回归。分类模型允许我们将对象分类为已知的类别(例如将动物分类为猫和狗),而回归分析可用于预测目标变量的连续结果(例如二手车的销售价格)。
我们还学习了如何使用建立数据科学环境...
二、在 OpenCV 中处理数据
既然我们已经激起了对机器学习的兴趣,现在是时候深入研究构成典型机器学习系统的不同部分了。
太多时候,你会听到有人抛出这样一句话, J 必须将机器学习应用到你的数据中!,好像那会瞬间解决你所有的问题。你可以想象这种情况的现实要复杂得多,尽管,我会承认,如今,仅仅通过从互联网上剪切和粘贴几行代码,就可以非常容易地构建自己的机器学习系统。然而,要建立一个真正强大和有效的系统,必须牢牢掌握基本概念,并深入了解每种方法的优缺点。所以,如果你还不认为自己是机器学习专家,不要担心。好事需要时间。
早些时候,我将机器学习描述为人工智能的一个子领域。这可能是真的——主要是出于历史原因——但大多数情况下,机器学习只是为了理解数据。因此,将机器学习视为数据科学的一个分支可能更合适,在这个分支中,我们构建数学模型来帮助我们理解数据。
因此,这一章是关于数据的。我们想学习数据如何与机器学习相适应,以及如何使用我们选择的工具来处理数据:OpenCV 和 Python。
在本章中,我们将涵盖以下主题:
- 了解机器学习工作流程
- 了解培训数据和测试数据
- 学习如何使用 OpenCV 和 Python 加载、存储、编辑和可视化数据
技术要求
可以从以下链接查阅本章代码:github . com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/chapter 02
。
以下是软件和硬件要求的总结:
- 您将需要 OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以)。
- 您将需要 Python 3.6 版本(任何 Python 3 . x 版本都可以)。
- 您将需要 Anaconda Python 3 来安装 Python 和所需的模块。
- 除了这本书,你可以使用任何操作系统——苹果操作系统、视窗操作系统和基于 Linux 的操作系统。我们建议您的系统中至少有 4 GB 内存。
- 你不需要一个图形处理器来运行本书提供的代码。
了解机器学习工作流程
如前所述,机器学习就是建立数学模型来理解数据。当我们赋予机器学习模型调整其内部参数的能力时,学习方面就进入了这个过程;我们可以调整这些参数,以便模型更好地解释数据。从某种意义上说,这可以理解为模型从数据中学习。一旦模型学到了足够的东西——无论这意味着什么——我们就可以要求它解释新观察到的数据。
下图说明了一个典型的分类过程:
让我们一步一步地分解它。
首先要注意的是,机器学习问题总是被分成(至少)两个不同的阶段:
- 一个训练阶段,在此期间我们的目标是在一组我们称之为训练数据集的数据上训练一个机器学习模型
- 测试阶段,在此期间,我们在一组从未见过的新数据上评估所学习的(或最终确定的)机器学习模型,我们称之为测试数据集
将我们的数据分成训练集和测试集的重要性不可低估。我们总是在一个独立的测试集上评估我们的模型,因为我们有兴趣知道我们的模型对新数据的概括程度。最后,这不就是学习的全部吗——无论是机器学习还是人类学习?回想一下你自己还是学生时的学校:作为家庭作业的一部分,你必须解决的问题永远不会在期末考试中以完全相同的形式出现。同样的审查应该适用于机器学习模型;我们对我们的模型能多好地记住一组数据点(比如一个家庭作业问题)不太感兴趣,但我们想知道我们的模型将如何利用他们所学的知识来解决新问题(比如期末考试中出现的问题)并解释新的数据点。
The workflow of an advanced machine learning problem will typically include a third set of data termed a validation dataset. For now, this distinction is not important. A validation set is typically formed by further partitioning the training dataset. It is used in advanced concepts such as model selection, which we will talk about in Chapter 11, Selecting the Right Model with Hyperparameter Tuning, when we have become proficient in building machine learning systems.
接下来要注意的是,机器学习实际上是关于数据的。数据以原始形式进入前面描述的工作流图——无论这意味着什么——并在培训和测试阶段使用。数据可以是任何东西,从图像和电影到文本文档和音频文件。因此,在其原始形式中,数据可能由像素、字母、单词组成,甚至更糟:纯比特。很容易看出,这种原始形式的数据可能不太方便处理。相反,我们必须找到方法预处理数据,使其成为易于解析或使用数据的形式。
数据预处理分为两个阶段:
- 特征选择:这是识别数据中重要属性(或特征)的过程。图像的可能特征可能是边缘、角或脊的位置。您可能已经熟悉 OpenCV 提供的一些更高级的特征描述符,例如加速健壮特征 ( SURF )或方向梯度直方图 ( HOG )。尽管这些特性可以应用于任何图像,但对于我们的特定任务来说,它们可能并不那么重要(或工作得那么好)。例如,如果我们的任务是区分干净的水和脏的水,最重要的特征可能是水的颜色,SURF 或 HOG 特征的使用可能对我们没有太大帮助。
- 特征提取:这是将原始数据转化为期望的特征空间的实际过程。一个例子是哈里斯算子,它允许我们提取图像中的角点(也就是一个选定的特征)。
一个更高级的话题是发明信息特征的过程,这被称为特征工程。毕竟,在人们能够从流行的功能中进行选择之前,必须有人先发明它们。这对于我们算法的成功往往比算法本身的选择更重要。我们将在第四章、中详细讨论特征工程,代表数据和工程特征。
Don't let naming conventions confuse you! Sometimes, feature selection and feature extraction are hard to distinguish, mainly because of how things are named. For example, SURF stands for both the feature extractor as well as the actual name of the features. The same is true for the S****cale-Invariant Feature Transform (SIFT), which is a feature extractor that yields what is known as SIFT features. Unfortunately, both the algorithms are patented and cannot be used for commercial purposes. We won't be sharing any code about either algorithms.
最后要说明的一点是,在监督学习中,每个数据点都必须有一个标签。标签标识属于某一类事物(如猫或狗)或具有某一价值(如房价)的数据点。说到底,有监督的机器学习系统的目标是预测测试集中所有数据点的标签(如上图所示)。我们通过学习训练数据中的规律性,使用随之而来的标签,然后在测试集上测试我们的性能来做到这一点。
因此,要构建一个运行良好的机器学习系统,我们首先必须涵盖如何加载、存储和操作数据。用 Python 在 OpenCV 中你是怎么做到的?
用 OpenCV 和 Python 处理数据
数据世界充满了各种各样的数据类型。这有时会使用户很难区分用于特定值的数据类型。在这里,我们将尝试通过将除标量值之外的所有内容都视为数组来保持简单,标量值将保留其标准数据类型。因此,图像将成为 2D 阵列,因为它们有宽度和高度。1D 阵列可以是强度随时间变化的声音片段。
如果你大部分时间都在使用 OpenCV 的 C++ 应用程序编程接口 ( API )并计划继续这样做,你可能会发现用 C++处理数据会有点痛苦。不仅要处理...
开始新的 IPython 或 Jupyter 会话
在我们拿到 NumPy 之前,我们需要打开一个 IPython 外壳或启动一个 Jupyter 笔记本:
- 像我们在上一章中所做的那样打开一个终端,并导航到
OpenCV-ML
目录:
$ cd Desktop/OpenCV-ML
- 激活我们在上一章中创建的
conda
环境:
$ source activate OpenCV-ML # Mac OS X / Linux
$ activate OpenCV-ML # Windows
- 开始新的 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 命令的地方。此外,您还可以在键入变量和函数的名称时使用选项卡键,让 IPython 自动完成它们。
A limited number of Unix and macOS system shell commands work too—such as ls
and pwd
. You can run any shell command by prefixing it with !
, such as !ping www.github.com
. For more information, check out the official IPython reference at ipython.org/ipython-doc/3/interactive/tutorial.html
.
如果您选择启动 Jupyter 会话,您的网络浏览器中应该会打开一个指向http://localhost:8888
的新窗口。要创建新笔记本,请单击右上角的新建,然后选择笔记本(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'
Recall that in the Jupyter Notebook, you can hit Ctrl + Enter to execute a cell once you have typed the command. Alternatively, Shift + Enter executes the cell and automatically inserts or selects the cell below it. Check out all of the keyboard shortcuts by clicking on Help | Keyboard Shortcut or take a quick tour by clicking on Help | User Interface Tour.
对于这里讨论的软件包部分,我建议使用 NumPy 版或更高版本。按照惯例,你会发现科学 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
As the standard Python implementation is written in C, every Python object is basically a C structure in disguise. This is true even for integers in Python, which are actually pointers to compound C structures that contain more than just the raw integer value. Therefore, the default C data type used to represent Python integers will depend on your system architecture (that is, whether it is a 32-bit ...
通过索引访问单个数组元素
如果您以前使用过 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])
我鼓励你自己玩这些阵列!
The general form of slicing arrays in NumPy is the same as it is for standard Python lists. To access a slice of an array, x
, use x[start:stop:step]
. If any of these are unspecified, they default to the start=0
, stop=size of dimension
, step=1
values.
创建多维数组
数组不必局限于列表。事实上,它们可以有任意数量的维度。在机器学习中,我们通常至少处理 2D 数组,其中列索引代表特定特征的值,行包含实际的特征值。
有了 NumPy,从头开始创建多维数组就很容易了。假设我们要创建一个三行五列的数组,所有元素都初始化为零。如果我们不指定数据类型,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-learn 的sklearn.datasets
包。这个包预装了一些小数据集,不需要我们从外部网站下载任何文件。这些数据集包括以下内容:
load_boston
:波士顿数据集包含波士顿不同郊区的房价,以及几个有趣的特征,如城镇人均犯罪率、住宅用地比例和非零售商业数量load_iris
:鸢尾数据集包含三种不同类型的鸢尾花(濑户鸢尾、云芝和弗吉尼亚鸢尾),以及描述萼片和花瓣的宽度和长度的四个特征load_diabetes
:糖尿病数据集让我们可以根据患者年龄、性别、体重指数、平均血压和六项血清指标等特征,将患者分为糖尿病患者和非糖尿病患者load_digits
:数字数据集包含数字 0-9 的 8×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']
鸢尾花数据库包含总共具有4
特征的150
样品——萼片长度、萼片宽度、花瓣长度和花瓣宽度。数据分为三类——濑户鸢尾、彩叶鸢尾和北美鸢尾。数据和标签在两个独立的容器中交付,我们可以按如下方式进行检查:
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)
Another Python library for data analysis that you should have heard about is pandas (pandas.pydata.org
). pandas implements several powerful data operations for both databases and spreadsheets. However great the library, at this point, pandas is a bit too advanced for our purposes.
使用 Matplotlib 可视化数据
如果我们不知道如何查看数据,那么知道如何加载数据的用处有限。谢天谢地,有 Matplotlib !
Matplotlib 是一个建立在 NumPy 数组上的多平台数据可视化库——看,我向你保证 NumPy 会再次出现。它是由约翰·亨特在 2002 年构思的,最初是作为 IPython 的补丁设计的,以便能够从命令行进行交互式 MATLAB 风格的绘图。近年来,更新、更闪亮的工具不断涌现,最终取代了 Matplotlib(如 R 语言中的ggplot
和ggvis
),但 Matplotlib 作为一个久经考验的跨平台图形引擎仍然至关重要。
正在导入 Matplotlib
你可能又走运了:如果你遵循上一章中概述的建议,安装了 Python Anaconda 堆栈,那么你已经安装了 Matplotlib,并准备好了。否则,您可能需要访问matplotlib.org
了解安装说明。
就像我们使用 NumPy 的np
简写一样,我们将使用 Matplotlib 导入的一些标准简写:
In [1]: import matplotlib as mpl
In [2]: import matplotlib.pyplot as plt
plt
界面是我们最常使用的,我们将在本书中看到。
制作一个简单的情节
不用多说,让我们创建我们的第一个情节。
假设我们想要产生正弦函数的简单线图sin(x)
。我们希望在 x 轴上的所有点对函数进行评估,其中0 < x < 10
。我们将使用 NumPy 的linspace
功能在 x 轴上创建一个线性间距,从x
值0
到10
,总共有100
个采样点:
In [3]: import numpy as npIn [4]: x = np.linspace(0, 10, 100)
我们可以使用 NumPy 的sin
函数在所有点x
评估sin
函数,并通过调用plt
的plot
函数来可视化结果:
In [5]: plt.plot(x, np.sin(x))
你自己试过吗?发生了什么事?有什么发现吗?
问题是,根据您运行该脚本的位置,您可能不会...
可视化外部数据集中的数据
作为本章的最后一个测试,让我们可视化一些来自外部数据集的数据,例如 scikit-learn 的digits
数据集。
具体来说,我们需要三种可视化工具:
- sci kit-了解实际数据
- 用于数据处理的数字
- 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
对象。我们通过键入它的名称,添加一个句点,然后点击选项卡键:digits.<TAB>
来实现。这将揭示digits
对象还包含一些其他字段,例如名为images
的字段。images
和data
这两个字段似乎只是形状不同:
In [3]: print(digits.data.shape)
... print(digits.images.shape)
Out[3]: (1797, 64)
(1797, 8, 8)
在这两种情况下,第一维对应于数据集中的图像数量。然而,data
将所有像素排列在一个大向量中,而images
保留了每个图像的 8×8 空间排列。
因此,如果我们想要绘制单个图像,则images
字段会更合适。首先,我们使用 NumPy 的数组切片从数据集中获取单个图像:
In [4]: img = digits.images[0, :, :]
这里,我们说我们想要抓取 1,797 项长数组中的第一行和所有对应的 8 x 8 = 64 像素。然后,我们可以使用plt
的imshow
功能绘制图像:
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 。然而,在灰度图像的情况下,灰色彩色图更有意义。
最后,我们可以使用plt
的subplot
功能绘制整数个数字样本。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')
这将导致以下输出:
Another great resource for all sorts of datasets is the machine learning repository of my alma mater, the University of California, Irvine: archive.ics.uci.edu/ml/index.php
.
用 C++中 OpenCV 的 TrainData 容器处理数据
为了完整起见,对于坚持使用 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/chapter 03
。
以下是软件和硬件要求的全球总结:
- 您将需要 OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以)。
- 您将需要 Python 3.x 版本(任何 Python 3.x 版本都可以)。
- 您将需要 Anaconda Python 3 来安装 Python 和所需的模块。
- 这本书可以使用任何操作系统——苹果操作系统、视窗操作系统和基于 Linux 的操作系统。我们建议您的系统中至少有 4 GB 内存。
- 运行本书提供的代码不需要 GPU。
理解监督学习
我们之前已经确定,监督学习的目标总是预测数据的标签(或目标值)。然而,根据这些标签的性质,监督学习可以有两种不同的形式:
- 分类:每当我们使用数据预测类别时,监督学习就被称为分类。这方面的一个很好的例子是,当我们试图预测一个图像是否包含一只猫或一只狗。在这里,数据的标签是分类的,不是一个就是另一个,但绝不是类别的混合。例如,一张图片包含一只猫或一只狗,从不包含 50%的猫和 50%的狗(在你问之前,不,这里我们不考虑卡通人物 CatDog 的图片),以及我们的工作...
看看 OpenCV 中的监督学习
如果我们不能将其付诸实践,仅仅知道监督学习是如何工作的是没有任何用处的。值得庆幸的是,OpenCV 为其所有的统计学习模型提供了一个相当简单的界面,其中包括所有的监督学习模型。
在 OpenCV 中,每一个机器学习模型都源自cv::ml::StatModel
基类。这是一个花哨的说法,说如果我们想在 OpenCV 中使用机器学习模型,我们必须提供StatModel
告诉我们的所有功能。这包括训练模型的方法(称为train
)和测量模型性能的方法(称为calcError
)。
In Object-Oriented Programming (OOP), we deal primarily with objects or classes. An object consists of several functions, called methods, as well as variables, called members or attributes. You can learn more about OOP in Python at docs.python.org/3/tutorial/classes.html
.
由于软件的这种组织,在 OpenCV 中建立机器学习模型总是遵循相同的逻辑,我们将在后面看到:
- 初始化:我们通过名称来调用模型,创建一个模型的空实例。
- 设置参数:如果模型需要一些参数,我们可以通过 setter 方法进行设置,每个模型可以不同。例如,要使 k-NN 算法工作,我们需要指定其开放参数 k (我们将在后面找到)。
- 训练模型:每个模型都必须提供一个叫做
train
的方法,用来将模型拟合到一些数据。 - 预测新标签:每个模型都必须提供一个叫做
predict
的方法,用来预测新数据的标签。 - 给模型打分:每个模型都必须提供一个叫做
calcError
的方法,用来衡量性能。这种计算可能对每个模型都不同。
Because OpenCV is a vast and community-driven project, not every algorithm follows these rules to the extent that we as users might expect. For example, the k-NN algorithm does most of its work in a findNearest
method, although predict
still works. We will make sure to point out these discrepancies as we work through different examples.
由于我们会偶尔使用 scikit-learn 来实现一些 OpenCV 没有提供的机器学习算法,因此值得指出的是,scikit-learn 中的学习算法遵循几乎相同的逻辑。最显著的区别是 scikit-learn 在初始化步骤中设置了所有必需的模型参数。另外,它调用训练函数fit
,而不是train
,调用评分函数score
,而不是calcError
。
用评分函数测量模型性能
构建机器学习系统最重要的部分之一是找到一种方法来衡量模型预测的质量。在现实生活中,一个模型很少会把所有事情都做好。从前面的章节中,我们知道我们应该使用测试集中的数据来评估我们的模型。但是这到底是怎么回事呢?
简短但不太有用的答案是,这取决于模型。人们提出了各种各样的评分函数,可以用来评估所有可能场景中的训练模型。好消息是,它们中的许多实际上是 scikit-learn 的metrics
模块的一部分。
让我们快速了解一些最重要的评分功能。...
使用准确度、精确度和召回率对分类器进行评分
在只有两个不同类别标签的二进制分类任务中,有几种不同的方法来衡量分类性能。一些常见的指标如下:
accuracy_score
:准确度计算测试集中已被正确预测的数据点的数量,并将该数量作为测试集大小的一部分返回。坚持图片分类为猫或狗的例子,准确性表示图片被正确分类为包含猫或狗的部分。这是分类器最基本的评分功能。precision_score
: Precision 描述分类器不将包含狗的图片标记为猫的能力。换句话说,在测试集中分类器认为包含猫的所有图片中,精度是实际包含猫的图片的分数。recall_score
:回忆(或敏感度)描述分类器检索包含猫的所有图片的能力。换句话说,在测试集中的所有猫的图片中,回忆是被正确识别为猫的图片的比例。
假设我们有一些ground truth
(根据我们拥有的数据集正确)类标签不是 0 就是 1。我们可以使用 NumPy 的随机数生成器随机生成它们。显然,这意味着,每当我们重新运行代码时,都会随机生成新的数据点。然而,对于这本书的目的来说,这不是很有帮助,因为我希望您能够运行代码,并总是得到与我相同的结果。一个很好的技巧是固定随机数发生器的种子。这将确保生成器在每次运行脚本时以相同的方式初始化:
- 我们可以使用以下代码修复随机数生成器的种子:
In [1]: import numpy as np
In [2]: np.random.seed(42)
- 然后,我们可以通过在范围
(0,2)
中选择随机整数来生成五个随机标签,它们要么是 0,要么是 1:
In [3]: y_true = np.random.randint(0, 2, size=5)
... y_true
Out[3]: array([0, 1, 0, 0, 0])
In the literature, these two classes are sometimes also called positives (all data points with the class label, 1
) and negatives (all other data points).
让我们假设我们有一个分类器,试图预测前面提到的类标签。为了论证,假设分类器不是很聪明,总是预测标签,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
这并不太难,是吗?然而,要理解精度和召回率,我们需要对ⅰ型和ⅱ型错误有一个大致的了解。我们回想一下,带有类标签1
的数据点通常被称为阳性,带有类标签0
(或-1)的数据点通常被称为阴性。然后,对特定数据点进行分类可以有四种可能的结果之一,如下面的混淆矩阵所示:
| | 是真正的正 | 是真正的负 |
| 预测阳性 | 正确肯定 | 假阳性 |
| 预测负值 | 假阴性 | 正确否定 |
让我们把它分解一下。如果一个数据点真的是正的,并且我们预测是正的,那么我们就做对了!在这种情况下,结果被称为真阳性。如果我们认为数据点是正的,但实际上是负的,我们就错误地预测了正的(因此有了这个术语,假阳性)。类似地,如果我们认为数据点是负的,但它确实是正的,我们就错误地预测了负的(假负的)。最后,如果我们预测了一个负数,而数据点确实是负数,我们就找到了一个真正的负数。
In statistical hypothesis testing, false positives are also known as type I errors and false negatives are also known as type II errors.
让我们根据模型数据快速计算这四个指标。我们有一个真正的正,其中真正的标签是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
让我们也画出混淆矩阵:
| | 是真正的正 | 是真正的负 |
| 预测阳性 | one | four |
| 预测负值 | Zero | Zero |
为了确保我们做对了所有的事情,让我们再计算一次精确度。准确性应该是真阳性的数量加上真阴性的数量(也就是我们得到的所有正确的东西)除以数据点的总数:
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
:一个更复杂的度量是衡量一个模型能在多大程度上解释测试数据的变化或分散。通常,解释的数量...
使用分类模型预测类别标签
有了这些工具,我们现在可以进行第一个真正的分类示例。
以兰多姆维尔小镇为例,那里的人们对他们的两支运动队——兰多姆维尔红军和兰多姆维尔蓝军——非常着迷。红军已经存在很长时间了,人们喜欢他们。但是后来,一些外地的百万富翁出现了,买下了红军的最佳射手,并组建了一支新的球队,蓝军。令大多数红魔球迷不满的是,这位最佳射手将随蓝军赢得冠军。几年后,他将回到红军,尽管球迷们对他之前的职业选择有些反感,但他们永远不会原谅他。但无论如何,你可以看到为什么红魔的球迷不一定能和蓝军的球迷相处融洽。事实上,这两个粉丝群是如此的分裂,以至于他们甚至从来没有住在一起。我甚至听过这样的故事:一旦蓝军球迷搬进隔壁,红球迷就故意搬走。真实故事!
不管怎样,我们是新来的,正试着挨家挨户地向人们推销一些蓝调商品。然而,我们时不时会遇到一个心碎的红军球迷,他会因为我们卖蓝军的东西而对我们大喊大叫,并把我们赶出他们的草坪。不好听!完全避开这些房子,转而去拜访蓝军球迷,压力会小得多,也能更好地利用我们的时间。
自信我们可以学会预测红军球迷住在哪里,我们开始记录我们的遭遇。如果我们路过一个红军球迷的家,我们会在手边的城镇地图上画一个红色三角形;否则,我们画一个蓝色的正方形。过了一会儿,我们对每个人的居住地有了一个很好的了解:
然而,现在,我们接近了前面地图中标记为绿色圆圈的房子。我们应该敲他们的门吗?我们试图找到一些他们更喜欢哪支球队的线索(也许是挂在后门廊的球队旗帜),但我们看不到任何线索。我们怎么知道敲他们的门是否安全?
这个愚蠢的例子说明的正是监督学习算法可以解决的那种问题。我们有一堆观察数据(房子、它们的位置和颜色)组成了我们的训练数据。我们可以利用这些数据从经验中学习,这样,当我们面临预测新房子颜色的任务时,我们就可以做出明智的估计。
正如我们前面提到的,红魔的球迷对他们的球队真的很有激情,所以他们永远不会靠近蓝军球迷。难道我们不能利用这些信息,看看所有邻近的房子,找出什么样的风扇住在新房子里?
这正是 k-NN 算法要做的。
理解 k-NN 算法
k-NN 算法可以说是最简单的机器学习算法之一。这样做的原因是我们基本上只需要存储训练数据集。然后,为了预测一个新的数据点,我们只需要在训练数据集中找到最近的数据点:它的最近邻居。
简而言之,k-NN 算法认为一个数据点可能与其邻居属于同一类。想想看:如果我们的邻居是红魔球迷,我们可能也是红魔球迷;否则,我们早就搬走了。蓝军也是如此。
当然,有些街区可能更复杂一点。在这种情况下,我们不仅要考虑我们最近的邻居(其中 k=1 ,而是...
在 OpenCV 中实现 k-NN
使用 OpenCV,我们可以通过cv2.ml.KNearest_create()
函数轻松创建 k-NN 模型。然后,构建模型包括以下步骤:
生成一些训练数据。
为给定的数字 k 创建一个 k-NN 对象。
找到我们要分类的新数据点的 k 最近邻。
通过多数票分配新数据点的类别标签。
画出结果。
我们首先导入所有必要的模块:用于 k-NN 算法的 OpenCV、用于数据处理的 NumPy 和用于绘图的 Matplotlib。如果你在 Jupyter 笔记本上工作,别忘了召唤%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)
好了,现在我们开始吧。我们的训练数据到底应该是什么样的?
在前面的示例中,每个数据点都是城镇地图上的一所房子。每个数据点都有两个特征(即其在城镇地图上的位置的 x 和 y 坐标)和一个类别标签(即,如果蓝调粉丝住在那里,则为蓝色正方形,如果红调粉丝住在那里,则为红色三角形)。
因此,可以表示单个数据点的特征...
训练分类器
与所有其他机器学习功能一样,k-NN 分类器是 OpenCV 3.1 ml
模块的一部分。我们可以使用以下命令创建一个新的分类器:
In [15]: knn = cv2.ml.KNearest_create()
In older versions of OpenCV, this function might be called cv2.KNearest()
instead.
然后,我们将训练数据传递给train
方法:
In [16]: knn.train(train_data, cv2.ml.ROW_SAMPLE, labels)
Out[16]: True
在这里,我们必须告诉knn
我们的数据是一个N×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
函数将直线拟合到 2D 或三维点集:
- 让我们从生成一些点开始。我们将通过给位于线上的点添加噪声来生成它们
:
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
- 我们还可以使用以下代码来可视化这些点:
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')
这给了我们下图,红线是真实函数:
- 接下来,我们将把这些点分成训练集和测试集。在这里,我们将数据分成 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)
- 现在,让我们使用
cv2.fitLine
将一条线拟合到这个 2D 点集。此函数接受以下参数:points
:这是一条线必须拟合的一组点。distType
:这是 M 估计器使用的距离。param
:这是数值参数(C),用于某些类型的距离。我们将它保持在 0,以便可以选择一个最佳值。reps
:这是原点到直线距离的精度。0.01
对于reps
来说是一个不错的默认值。aeps
:这是角度的精度。0.01
对于aeps
来说是一个不错的默认值。
For more information, have a look at the documentation.
- 让我们看看使用不同的距离类型选项会得到什么样的结果:
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)])
- 我们还将使用 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])
- 我们使用
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
房价为红色,我们预测的房价为蓝色。如果你问我的话,这很近。不过,有趣的是,对于真正高或真正低的房价,该模型往往偏离得最多,例如数据点 12 、 18 和 42 的峰值。我们可以通过计算 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 2 分数和均方误差:
... 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。这些是我们可以用来比较线性回归模型和一些更复杂的模型的一些性能度量。
应用套索和岭回归
机器学习中的一个常见问题是,一个算法可能在训练集上运行得很好,但是,当应用于看不见的数据时,它会犯很多错误。你可以看到这是多么的有问题,因为,通常,我们最感兴趣的是模型如何推广到新的数据。一些算法(如决策树)比其他算法更容易受到这种现象的影响,但甚至线性回归也会受到影响。
This phenomenon is also known as overfitting, and we will talk about it extensively in Chapter 5, Using Decision Trees to Make a Medical Diagnosis, and Chapter 11, Selecting the Right Model with Hyperparameter Tuning.
减少过拟合的常用技术称为正则化,包括...
利用逻辑回归对鸢尾属植物进行分类
机器学习领域另一个著名的数据集叫做 Iris 数据集。鸢尾数据集包含来自三个不同物种的 150 朵鸢尾花的测量值:濑户鸢尾、云芝和绿鸢尾。这些尺寸包括花瓣的长度和宽度以及萼片的长度和宽度,都以厘米为单位。
我们的目标是建立一个机器学习模型,可以学习这些鸢尾花的测量值,它们的种类是已知的,这样我们就可以预测新鸢尾花的种类。
理解逻辑回归
在我们开始这一部分之前,让我发出警告——逻辑回归,不管它的名字是什么,实际上是一个分类模型,特别是当你有两个类的时候。它的名称来源于逻辑函数(或 sigmoid ),该函数用于将任何实值输入 x 转换为预测输出值 ŷ ,其取值介于 0 和 1 之间,如下图所示:
将 ŷ 舍入到最接近的整数有效地将输入分类为属于类别 0 或 1 。
当然,大多数情况下,我们的问题有不止一个输入或特征值, x 。例如,Iris 数据集提供了一个总数...
加载训练数据
Iris 数据集包含在 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]
是萼片宽度)。我们可以在下图中看到很好的类分离:
上图显示了虹膜数据集的前两个特征。
将数据分成训练集和测试集
在上一章中,我们了解到将训练数据和测试数据分开是非常重要的。我们可以使用 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_BATCH
或者cv2.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
正如我们刚刚看到的,训练阶段的目标是找到一组权重,最好地将特征值转换为输出标签。单个数据点由其四个特征值给出(f0??、f1??、f2??、f3)。既然我们有四个特征,那么我们也应该得到四个权重,这样x = w0f0+w1f1+w2f2+w3f3和 ŷ=σ(x) 。然而,如前所述,该算法增加了一个额外的权重,作为偏移或偏差,从而使x = w0f0+w1f1+w2f2+w3f3+w4】。我们可以按如下方式检索这些权重:****
In [18]: lr.get_learnt_thetas()
Out[18]: array([[-0.04090132, -0.01910266, -0.16340332, 0.28743777, 0.11909772]], dtype=float32)
这意味着逻辑函数的输入为x =-0.0409 f0-0.0191 f1-0.163 f2+0.287 f3+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 和逻辑回归,并讨论了它们如何用于预测两个或多个离散类别的标签。我们介绍了线性回归的各种变体(如套索回归和岭回归),并讨论了它们如何用于预测连续变量。最后但同样重要的是,我们熟悉了 Iris 和 Boston 数据集,这是机器学习历史上的两个经典。
在接下来的章节中,我们将深入探讨这些主题,并探索一些更有趣的例子来说明这些概念的用处。
但是首先,我们需要谈谈机器学习中的另一个基本话题,特征工程。通常,数据不会出现在格式良好的数据集中,以有意义的方式表示数据是我们的责任。因此,下一章将讨论表示特征和工程数据。
四、表示数据和工程特征
在最后一章中,我们构建了第一个监督学习模型,并将其应用于一些经典数据集,如 Iris 和 Boston 数据集。然而,在现实世界中,数据很少出现在一个整洁的<n_samples x n_features>
特征矩阵中,该矩阵是预打包数据库的一部分。相反,我们有责任找到一种以有意义的方式表示数据的方法。寻找表示我们数据的最佳方式的过程被称为特征工程,它是数据科学家和机器学习从业者试图解决现实世界问题的主要任务之一。
我知道你宁愿直接跳到最后,建立人类有史以来最深的神经网络。...
技术要求
可以从以下链接查阅本章代码:github . com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/chapter 04
。
以下是软件和硬件要求的总结:
- 您将需要 OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以)。
- 您将需要 Python 3.6 版本(任何 Python 3 . x 版本都可以)。
- 您将需要 Anaconda Python 3 来安装 Python 和所需的模块。
- 除了这本书,你可以使用任何操作系统——苹果操作系统、视窗操作系统和基于 Linux 的操作系统。我们建议您的系统中至少有 4 GB 内存。
- 你不需要一个图形处理器来运行本书提供的代码。
理解特征工程
信不信由你,一个机器学习系统学习的好坏主要取决于训练数据的质量。尽管每种学习算法都有其优缺点,但性能的差异往往归结于数据的准备或表示方式。因此,特征工程可以理解为数据表示的工具。机器学习算法试图从样本数据中学习一个问题的解决方案,而特征工程会问:样本数据的最佳表示是什么,用来学习问题的解决方案?
记住,前几章,我们讨论了整个机器学习管道。在这里,我们已经提到了特征提取,但是我们...
预处理数据
我们在处理数据时越有纪律性,最终可能会取得更好的结果。这个过程的第一步被称为数据预处理,它有(至少)三种不同的味道:
数据格式化:数据可能不是适合我们工作的格式;例如,数据可能以专有文件格式提供,而我们最喜欢的机器学习算法不理解这一点。
数据清理:数据可能包含无效或缺失的条目,需要清理或删除。
数据采样:数据对于我们的特定目的来说可能太大了,迫使我们对数据进行智能采样。
一旦对数据进行了预处理,我们就为实际的特征工程做好了准备:对预处理后的数据进行转换,以适合我们特定的机器学习算法。该步骤通常涉及三个可能程序中的一个或多个:
- 缩放:某些机器学习算法往往要求数据在一个共同的范围内,比如均值和单位方差为零。缩放是将所有要素(可能具有不同的物理单位)纳入一个共同的值范围的过程。
- 分解:数据集的特征通常比我们可能处理的要多得多。特征分解是将数据压缩成少量信息量大的数据成分的过程。
- 聚合:有时候,可以将多个特征组合成一个单一的、更有意义的特征。例如,数据库可能包含登录到基于网络的系统的每个用户的日期和时间。根据任务的不同,简单地计算每个用户的登录次数可以更好地表示这些数据。
让我们更详细地看看其中的一些过程。
标准化特征
标准化是指对数据进行缩放,使其均值和单位方差为零的过程。这是对各种机器学习算法的共同要求,如果单个特征不能满足这一要求,机器学习算法可能会表现不佳。我们可以通过从每个数据点减去所有数据的平均值( μ )并除以数据的方差( σ )来手动标准化我们的数据;也就是说,对于每个特征 x ,我们会计算 (x - μ) / σ 。
或者,scikit-learn 在其preprocessing
模块中提供了这一过程的简单实现。
让我们考虑一个 3×3 的数据矩阵,X
,代表三个数据点(行),每个数据点(行)有三个任意选择的特征值(列):...
标准化特征
与标准化类似,标准化是缩放单个样本以获得单位范数的过程。我相信你知道范数代表向量的长度,可以用不同的方式定义。我们在前一章讨论了其中的两个:L1 范数(或曼哈顿距离)和 L2 范数(或欧几里德距离)。
在 scikit-learn 中,我们的数据矩阵X
可以使用normalize
函数进行归一化,而l1
范数由norm
关键字指定:
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]])
将要素缩放到一个范围
将要素缩放至零均值和单位方差的替代方法是让要素位于给定的最小值和最大值之间。通常,这些值是 0 和 1,因此每个特征的最大绝对值都按单位大小进行缩放。在 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 ]])
大多数机器学习算法无法处理非数字 ( NAN )值(【Python 中的 )。相反,我们首先必须用一些适当的填充值替换所有的nan
值。这被称为缺失值的插补。
scikit-learn 提供了三种不同的估算缺失值的策略:
mean
:用沿矩阵指定轴的平均值替换所有nan
值(默认:轴= 0 )median
:替换全部...
理解降维
数据集通常具有比我们可能处理的更多的特征。例如,假设我们的工作是预测一个国家的贫困率。我们可能会从匹配一个国家的名字和它的贫困率开始,但这不会帮助我们预测一个新国家的贫困率。所以,我们开始思考贫困的可能原因。但是贫困的可能原因有多少呢?因素可能包括一个国家的经济、缺乏教育、高离婚率、人口过剩等等。如果这些原因中的每一个都是用来帮助预测贫困率的特征,那么我们最终会得到无数个特征。如果你是数学家,你可能会把这些特征看作是高维空间中的轴,然后每个国家的贫困率就是这个高维空间中的一个点。
如果你不是数学家,从小处着手可能会有帮助。假设,我们首先只看两个特征:一个国家的国内生产总值 ( GDP )和公民数量。在 2D 的空间里,我们把 GDP 解释为 x 轴,把公民人数解释为 y 轴。然后,我们看第一个国家。它的国内生产总值很小,公民人数也很平均。我们在 x-y 平面上画一个点,代表这个国家。我们添加了第二、第三和第四个国家。第四个国家恰好既有很高的 GDP,又有大量的公民。因此,我们的四个数据点可能分布在 x-y 平面上,如下图所示:
然而,如果我们开始在我们的分析中增加第三个特征,比如该国的离婚率,会发生什么?这将为我们的图添加第三个轴( z 轴)。突然,我们发现数据不再很好地在 x-y-z 立方体中传播,因为立方体的大部分仍然是空的。而在二维中,我们似乎覆盖了大部分的 x-y 方块,在三维中,我们需要更多的数据点来填充数据点 1 到 3 和右上角的孤立数据点 4 之间的空白。
This problem is also known as the curse of dimensionality: the number of data points needed to fill the available space grows exponentially with the number of dimensions (or plot axes). If a classifier is not fed with data points that span the entire feature space (such as shown in the preceding cube example), the classifier will not know what to do once a new data point is presented that lies far away from all of the previously encountered data points.
维数灾难意味着,在一定数量的特征(或维度)之后,分类器的性能将开始下降。让我们试着理解这一点。更多的特征本质上意味着可以考虑更多的数据集变化。但是,如果考虑的特征超过了要求的特征,分类器甚至会考虑任何异常值,或者过度填充数据集。因此,分类器的性能将开始下降,而不是提高:
但是,我们如何为数据集找到这个看似最优的维数呢?
这就是降维发挥作用的地方。这些是一系列技术,允许我们找到高维数据的紧凑表示,而不会丢失太多信息。
在 OpenCV 中实现主成分分析
最常见的降维技术之一叫做主成分分析。
类似于前面显示的 2D 和 3D 例子,我们可以将图像视为高维空间中的一个点。如果我们通过堆叠所有的列来展平高度 m 和宽度 n 的 2D 灰度图像,我们得到长度 m x n x 1 的(特征)向量。该向量中第I元素的值是图像中第I像素的灰度值。现在,假设我们想要用这些精确的维度来表示每一个可能的 2D 灰度图像。这会给出多少图像?
由于灰度像素通常取 0 到 255 之间的值,因此总共有 256 个提升到 m x n 图像的幂。机会...
实现独立分量分析
scikit-learn 提供了与主成分分析密切相关的其他有用的降维技术,但没有 OpenCV。为了完整起见,我们在这里提到它们。独立分量分析执行与主成分分析相同的数学步骤,但是它选择分解的分量尽可能彼此独立,而不是像主成分分析那样按照预测器。
在 scikit-learn 中,ICA 可从decomposition
模块获得:
In [9]: from sklearn import decomposition
In [10]: ica = decomposition.FastICA(tol=0.005)
Why do we use tol=0.005
? Because we want the FastICA to converge to some particular value. There are two methods to do that—increase the number of iterations (the default value is 200
) or decrease the tolerance (the default value is 0.0001
). I tried to increase the iterations but, unfortunately, it didn't work, so I went ahead with the other option. Can you figure out why it didn't converge?
如前所述,数据转换发生在fit_transform
函数中:
In [11]: X2 = ica.fit_transform(X)
在我们的例子中,绘制旋转的数据导致了与先前使用主成分分析获得的结果相似的结果,这可以在这个代码块后面的图表中得到验证。
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 。它再次实现了与主成分分析和独立分量分析相同的基本数学运算,但它有一个额外的限制,即它只对非负数据进行运算。换句话说,如果我们想使用 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 分布随机邻域嵌入(t-SNE)可视化降维
t-SNE 是一种降维技术,最适合于高维数据的可视化。
在本节中,我们将看到一个如何使用 t-SNE 可视化高维数据集的示例。让我们在这种情况下使用数字数据集,它有从 0 到 9 的数字手写图像。这是一个公开的数据集,通常被称为 MNIST 数据集。我们将看到如何使用 t-SNE 可视化这个数据集的降维:
- 首先,让我们加载数据集:
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,)
- 您应该首先应用一种降维技术(如 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
- 最后,让我们在散点图的帮助下,可视化我们使用 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
的一个出现。另一方面,它不包含extraction
或selection
字样。这有意义吗?快速浏览一下我们的原始数据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']
If you're interested in the math behind TF-IDF, you can start with this paper: citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.121.1424&rep=rep1&type=pdf
. For more information about its specific implementation in scikit-learn, have a look at the API documentation at 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 图像。你只需要知道,或者需要提醒的是,彩色图像实际上是作为 BGR 图像存储在 OpenCV 中的;也就是说,颜色通道的顺序是蓝-绿-红,而不是红-绿-蓝。其原因是...
在 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
获得。我们需要做的就是用以下内容之一替换颜色标志:
- HLS 使用
cv2.COLOR_BGR2HLS
- 使用
cv2.COLOR_BGR2LAB
的 LAB(亮度、绿-红和蓝-黄) - 使用
cv2.COLOR_BGR2YUV
的 YUV(整体亮度、蓝色亮度和红色亮度)
检测图像中的角点
在图像中最容易找到的传统特征之一可能是角(几条边相交的位置)。OpenCV 提供了至少两种不同的算法来查找图像中的角点:
- 哈里斯·唐纳探测:哈里斯和斯蒂芬斯知道边缘是所有方向都有高强度变化的区域,他们想出了一个快速找到这些位置的方法。该算法在 OpenCV 中实现为
cv2.cornerHarris
。 - Shi-Tomasi 角点检测 : Shi 和 Tomasi 对什么构成好的特征进行跟踪有自己的想法,通常通过找到 N 最强的角点,比 Harris 角点检测做得更好。该算法在 OpenCV 中实现为
cv2.goodFeaturesToTrack
。
哈里斯...
使用恒星探测器和简要描述符
然而,当图像的比例改变时,角点检测是不够的。已经发表了多篇论文,描述了用于特征检测和描述的不同算法。我们将查看加速鲁棒特征 ( SURF )检测器(更多信息,请参见en.wikipedia.org/wiki/Speeded_up_robust_features
)和二进制鲁棒独立基本特征 ( 简报)描述符的组合。特征检测器识别图像中的关键点,特征描述符计算所有关键点的实际特征值。
这些算法的细节超出了本书的范围。高级用户可以参考详细描述这些算法的论文。
有关更多详细信息,您可以参考以下链接:
- 冲浪:https://www.vision.ee.ethz.ch/~surf/eccv06.pdf
- 简要说明:
www.cs.ubc.ca/~lowe/525/papers/calonder_eccv10.pdf
整个过程从读取图像开始,将其转换为灰度,使用恒星特征检测器找到感兴趣的点,最后使用简要描述符计算特征值。
- 让我们首先读取图像并将其转换为灰度:
In [23]: img = cv2.imread('data/rubic-cube.png')
In [24]: gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
- 现在,我们将创建特征检测器和描述符:
In [25]: star = cv2.xfeatures2d.StarDetector_create()
In [26]: brief = cv2.xfeatures2d.BriefDescriptorExtractor_create()
- 接下来,是时候使用恒星探测器来获取关键点并将它们传递给简要描述符了:
In [27]: keyPoints = star.detect(gray, None)
In [28]: keyPoints, descriptors = brief.compute(img, keyPoints)
这里有个陷阱。在写这本书的时候,OpenCV 版本还没有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
- 现在让我们使用这个函数来绘制检测到的关键点:
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)
我们得到如下输出:
很棒,对吧?
虽然简单快捷,但它不适用于图像的旋转。您可以通过旋转图像(更多信息请访问www . pyimagesearch . com/2017/01/02/rotate-images-with-opencv-and-python/
)然后运行 LISTER 来尝试。让我们看看 ORB 如何帮助我们解决这个问题。
使用定向快速旋转简报
个人而言,我是 ORB 的超级粉丝。它是免费的,也是 SIFT 和 SURF 的一个很好的替代品,它们都受到专利法的保护。ORB 实际上比 SURF 好用。有趣的是,加里·布拉德斯基是题为ORB:SIFT 和 SURF 的有效替代品的论文作者之一。你能理解为什么这很有趣吗?谷歌加里·布拉德斯基和 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 为图像数据提供的常见特征提取技术。
有了这些技能,我们现在可以接受任何数据,无论是数字数据、分类数据、文本数据还是图像数据。当我们遇到丢失的数据时,我们知道该做什么,我们知道如何传输我们的数据,使其适合我们首选的机器学习算法。
在下一章中,我们将进行下一步,并讨论一个具体的用例,即如何使用我们新获得的知识,使用决策树进行医学诊断。
五、利用决策树的医学诊断
既然我们知道如何处理各种形状和形式的数据,无论是数字数据、分类数据、文本数据还是图像数据,现在是时候好好利用我们新获得的知识了。
在本章中,我们将学习如何构建一个可以进行医学诊断的机器学习系统。我们并不都是医生,但在我们一生中的某个时刻,我们可能都去过一次。通常,医生会获得尽可能多的关于患者病史和症状的信息,以做出明智的诊断。我们将借助所谓的决策树来模拟医生的决策过程。我们还将涵盖基尼系数、信息增益和方差减少,以及过度拟合和修剪。
决策树是一种简单而强大的监督学习算法,类似于流程图;我们将在一分钟内详细讨论这个问题。除了在医学领域,决策树通常用于天文学(例如,从哈勃太空望远镜图像中过滤噪声或对恒星-星系团进行分类)、制造和生产(例如,由波音公司发现制造过程中的缺陷)以及物体识别(例如,识别 3D 物体)等领域。
具体来说,我们希望在本章中了解以下内容:
- 从数据中构建简单的决策树,并将其用于分类或回归
- 使用基尼系数、信息增益和方差缩减来决定下一步做什么决定
- 规划决策树及其好处
但首先,让我们谈谈决策树实际上是什么。
技术要求
可以从以下链接查阅本章代码:github . com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/chapter 05
。
以下是软件和硬件要求的总结:
- 您将需要 OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以)。
- 您将需要 Python 3.6 版本(任何 Python 3 . x 版本都可以)。
- 您将需要 Anaconda Python 3 来安装 Python 和所需的模块。
- 除了这本书,你可以使用任何操作系统——苹果操作系统、视窗操作系统和基于 Linux 的操作系统。我们建议您的系统中至少有 4 GB 内存。
- 你不需要一个图形处理器来运行本书提供的代码。
理解决策树
决策树是一个简单而强大的有监督学习问题的模型。顾名思义,我们可以把它想象成一棵树,其中信息沿着不同的分支流动——从树干开始,一直到单个的叶子,决定在每个连接点采取哪个分支。
这基本上是一个决策树!下面是决策树的一个简单示例:
决策树由关于数据(也称为决策节点)及其可能后果的问题或测试的层次结构组成。
构建决策树的真正困难之一是如何从数据中提取合适的特征。为了说明这一点,让我们用一个具体的例子。假设我们有一个由一封电子邮件组成的数据集:
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
转换为人类可读的数组。结果是一个 2D 数组,其中行对应于数据样本,列对应于前面命令中描述的要素名称。由于数据集中只有一个样本,我们将自己限制在数组的第 0 行(即第一个数据样本)和数组的前五列(即前五个字)。
那么,我们如何检查电子邮件是否来自尼日利亚王子?
一种方法是查看电子邮件是否同时包含单词nigerian
和prince
:
In [5]: 'nigerian' in vec.get_feature_names()
Out[5]: True
In [6]: 'prince' in vec.get_feature_names()
Out[6]: False
让我们惊讶的是,我们发现了什么?prince
这个词在邮件中没有出现。
这是否意味着信息是合法的?
不,当然不是。这封电子邮件没有标上'prince'
,而是标上了head of state
,有效地绕过了我们过于简单的垃圾邮件检测器。
同样,我们甚至如何开始模拟树中的第二个决定:*想让我给他寄钱?*文本中没有直截了当的特征来回答这个问题。因此,这是一个特征工程的问题,即以允许我们回答这个问题的方式组合消息中实际出现的单词。当然,一个好的迹象是寻找像US$
和money
这样的字符串,但是我们仍然不知道这些单词被提到的上下文。据我们所知,也许它们是句子的一部分:别担心,我不想你给我寄钱。
更糟糕的是,事实证明,我们问这些问题的顺序实际上会影响最终结果。比如,如果我们先问最后一个问题:我真的认识一个尼日利亚王子吗?假设我们有一个尼日利亚王子作为叔叔,那么在电子邮件中找到尼日利亚王子可能就不再可疑了。
如你所见,这个看似简单的例子很快就失控了。
幸运的是,决策树背后的理论框架帮助我们找到正确的决策规则以及下一步要处理的决策。
然而,要理解这些概念,我们还得再深入一点。
构建我们的第一棵决策树
我认为我们已经为一个更复杂的例子做好了准备。如前所述,现在让我们进入医疗领域。
让我们考虑一个例子,几个病人患有同样的疾病,例如一种罕见的底栖病。让我们进一步假设这种疾病的真正原因至今仍不为人所知,并且我们所能获得的所有信息都由一系列生理测量组成。例如,我们可能可以访问以下信息:
- 患者的血压(
BP
) - 患者的胆固醇水平(
cholesterol
) - 患者的性别(
sex
) - 患者年龄(
age
) - 一个病人的血钠浓度(
Na
) - 一个病人的血钾浓度(
K
)
基于所有的...
生成新数据
在继续下一步之前,让我们快速了解每个机器学习工程师的一个非常关键的步骤——数据生成。我们知道,所有的机器学习和深度学习技术都需要大量的数据——简单来说:越大越好。但是如果你没有足够的数据呢?你可能会得到一个不够精确的模型。常用的技术(如果您不能生成任何新数据)是使用大部分数据进行训练。这样做的主要缺点是,你的模型不是一般化的,或者换句话说,存在过度拟合的问题。
解决上述问题的一个办法是生成新数据,或者通常所说的合成数据。这里需要注意的关键点是,合成数据应该具有与真实数据相似的特征。它们与真实数据越相似,对作为 ML 工程师的你越有利。这种技术被称为数据增强,在这里我们使用各种技术,如旋转和镜像来生成基于现有数据的新数据。
由于我们在这里处理的是一个假设的情况,我们可以编写简单的 Python 代码来生成随机数据——因为这里没有为我们设置的特性。在现实世界中,您将使用数据扩充来生成看起来真实的新数据样本。让我们看看如何处理我们的案例。
在这里,数据集实际上是一个字典列表,其中每个字典构成一个数据点,包含患者的血液工作、年龄和性别,以及处方药物。因此,我们知道我们想要创建新的字典,我们知道在这本字典中使用的键。接下来要关注的是字典中值的数据类型。
我们从age
开始,?? 是一个整数,然后是性别,不是M
就是F
。同样,对于其他值,我们可以推断数据类型,在某些情况下,使用常识,我们甚至可以推断要使用的值的范围。
It is very important to note that common sense and deep learning don't go well together most of the time. This is because you want your model to understand when something is an outlier. For example, we know that it's highly unlikely for someone to have an age of 130 but a generalized model should understand that this value is an outlier and should not be taken into account. This is why you should always have a small portion of data with such illogical values.
让我们看看如何为我们的案例生成一些合成数据:
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)
调用前面的函数。
既然我们知道了如何生成数据,让我们看看我们可以用这些数据做些什么。我们能想出医生开药的理由A
、B
、C
或D
吗?我们能看到病人的血液值和医生开的药之间的关系吗?
很有可能,这个问题对你来说和对我来说一样难回答。尽管数据集乍一看可能是随机的,但事实上,我已经在患者的血液值和处方药物之间建立了一些明确的关系。让我们看看决策树是否能揭示这些隐藏的关系。
通过理解数据来理解任务
解决一个新的机器学习问题的第一步总是什么?
你完全正确:获得数据的感觉。我们越了解数据,就越了解我们试图解决的问题。在我们未来的努力中,这也将帮助我们选择合适的机器学习算法。
首先要意识到的是drug
列实际上并不像所有其他列一样是一个特征值。由于我们的目标是根据患者的血液值预测将开出哪种药物,因此drug
栏实际上成为了目标标签。换句话说,机器学习算法的输入将是 a 的血液值、年龄和性别...
预处理数据
为了让决策树算法理解我们的数据,我们需要将所有分类特征(sex
、BP
、cholesterol
)转换为数字特征。最好的方法是什么?
没错:我们使用 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
模块中:
- 我们可以使用以下代码创建一个空决策树:
In [16]: import cv2... dtree = cv2.ml.dtree_create()
- 为了在训练数据上训练决策树,我们使用
train
方法。这就是我们之前将数据转换为浮点的原因——这样我们就可以在train
方法中使用它:
In [17]: dtree.train(X_train, cv2.ml.ROW_SAMPLE, y_train)
这里,我们必须指定X_train
中的数据样本是占据行(使用cv2.ml.ROW_SAMPLE
)还是列(cv2.ml.COL_SAMPLE
)。
- 然后,我们可以预测...
可视化经过训练的决策树
如果你刚刚起步,不太关心引擎盖下发生的事情,OpenCV 对决策树的实现已经足够好了。然而,在接下来的部分中,我们将切换到 scikit-learn。它的实现允许我们定制算法,并使研究树的内部工作变得更加容易。它的用法也有更好的记录。
在 scikit-learn 中,决策树可以用于分类和回归。它们位于tree
模块中:
- 我们先从
sklearn
导入tree
模块:
In [21]: from sklearn import tree
- 类似于 OpenCV,然后我们使用
DecisionTreeClassifier
构造函数创建一个空决策树:
In [22]: dtc = tree.DecisionTreeClassifier()
- 然后可以使用
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')
- 然后,我们可以使用
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。不要担心,因为它已经存在于我们在本书开头创造的环境中。
- 然后,回到 Python 中,您可以使用 scikit-learn 的
export_graphviz
导出器将 GraphViz 格式的树导出到文件tree.dot
:
In [26]: with open("tree.dot", 'w') as f:
... tree.export_graphviz(clf, out_file=f)
- 然后,回到命令行,您可以使用 GraphViz 将
tree.dot
转换为(例如)一个 PNG 文件:
$ dot -Tpng tree.dot -o tree.png
或者,您也可以指定-Tpdf
或任何其他支持的图像格式。前面的树的结果如下所示:
这一切意味着什么?让我们一步一步地分解这个图表。
调查决策树的内部工作方式
我们之前已经确定决策树基本上是一个流程图,它对数据做出一系列决策。这个过程从根节点(位于最顶端的节点)开始,在这里,我们根据某种决策规则将数据分成两组(仅适用于二叉树)。然后,重复该过程,直到所有剩余的样本具有相同的目标标签,此时我们已经到达叶节点。
在前面的垃圾邮件过滤器示例中,通过询问真/假问题来做出决定。例如,我们问一封电子邮件是否包含某个单词。如果是的话,我们沿着标记为真的边问下一个问题。然而,这不仅适用于分类特征,...
评定功能的重要性
我还没有告诉你的是你如何选择分割数据的特征。前面的根节点按照 *Na < = 0.72,*拆分数据,但是谁让树先关注钠呢?还有,0.72 这个数字到底是从哪里来的?
显然,有些功能可能比其他功能更重要。事实上,scikit-learn 提供了一个对特征重要性进行评级的功能,这是一个介于每个特征的 0 和 1 之间的数字,其中 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
标签的地方。
Feature importances tell us which features are important for classification, but not which class label they are indicative of. For example, we only know that the cholesterol level is important, but we don't know how that led to different drugs being prescribed. In fact, there might not be a simple relationship between features and classes.
理解决策规则
为了构建完美的树,您可能希望在信息量最大的特征处拆分树,从而得到最纯的子节点。然而,这个简单的想法带来了一些实际的挑战:
- 实际上并不清楚什么是最有信息量的。我们需要一个具体的值,一个分数函数,或者一个数学方程来描述一个特征的信息量。
- 为了找到最佳分割,我们必须在每个决策节点搜索所有的可能性。
幸运的是,决策树算法实际上为您完成了这两个步骤。scikit-learn 支持的两个最常用的标准如下:
criterion='gini'
:基尼不纯是一种错误分类的度量,目的是...
控制决策树的复杂性
如果你继续种植一棵树,直到所有的叶子都是纯净的,你通常会得到一棵太复杂而无法解释的树。纯叶的存在意味着树在训练数据上是 100%正确的,就像我们前面展示的树一样。因此,该树在测试数据集上的表现很可能很差,就像我们前面展示的树一样。我们说树超过了训练数据。
有两种常见的方法可以避免过度拟合:
- 预修剪:这是提前停止树的创建的过程。
- 后期修剪 (或只是修剪):这是先构建树,然后移除或折叠只包含少量信息的节点的过程。
有几种方法可以预先修剪一棵树,所有这些方法都可以通过向DecisionTreeClassifier
构造函数传递可选参数来实现:
- 通过
max_depth
参数限制树的最大深度 - 通过
max_leaf_nodes
限制叶节点的最大数量 - 通过
min_samples_split
需要节点中的最小点数来保持分裂
通常预修剪足以控制过度拟合。
试试我们的玩具数据集吧!你能让考试的分数有所提高吗?当您开始使用早期参数时,树布局如何变化?
In more complicated real-world scenarios, pre-pruning is no longer sufficient to control overfitting. In such cases, we want to combine multiple decision trees into what is known as a random forest. We will talk about this in Chapter 10, Ensemble Methods for Classification.
利用决策树诊断乳腺癌
现在我们已经构建了第一个决策树,是时候将我们的注意力转向一个真实的数据集了:乳腺癌威斯康星数据集(archive . ics . UCI . edu/ml/datasets/乳腺癌+癌症+威斯康星+(Diagnostic)
)。
该数据集是医学影像研究的直接结果,被认为是当今的经典。数据集是从健康(良性)和癌性(恶性)组织的数字化图像创建的。不幸的是,我没能从最初的研究中找到任何公共领域的例子,但是图像看起来类似于下面的截图:
这项研究的目的是对组织进行分类...
正在加载数据集
完整数据集是 scikit-learn 示例数据集的一部分。我们可以使用以下命令导入它:
- 首先,让我们使用
load_breast_cancer
函数加载数据集:
In [1]: from sklearn import datasets
... data = datasets.load_breast_cancer()
- 与前面的示例一样,所有数据都包含在 2D 特征矩阵
data.data
中,其中行代表数据样本,列是特征值:
In [2]: data.data.shape
Out[2]: (569, 30)
- 通过查看提供的功能名称,我们认识到前面提到的一些功能:
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')
- 由于这是一个二进制分类任务,我们希望找到两个目标名称:
In [4]: data.target_names
Out[4]: array(['malignant', 'benign'], dtype='<U9')
- 让我们保留大约 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)
- 你当然可以选择不同的比例,但大多数人通常使用 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
模块创建决策树。现在,我们不要指定任何可选参数:
- 我们将从创建决策树开始:
In [5]: from sklearn import tree... dtc = tree.DecisionTreeClassifier()
- 你还记得如何训练决策树吗?我们将为此使用
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')
- 因为我们没有指定任何预修剪参数,所以我们希望这样...
使用决策树进行回归
虽然到目前为止,我们一直专注于在分类任务中使用决策树,但是您也可以将它们用于回归。但是您需要再次使用 scikit-learn,因为 OpenCV 不提供这种灵活性。因此,我们在此仅简要回顾其功能:
- 假设我们想用决策树来拟合一个正弦波。为了让事情变得有趣,我们还将使用 NumPy 的随机数生成器向数据点添加一些噪声:
In [1]: import numpy as np
... rng = np.random.RandomState(42)
- 然后我们创建 100 个随机间隔的 x 值,介于 0 和 5 之间,并计算相应的 sin 值:
In [2]: X = np.sort(5 * rng.rand(100, 1), axis=0)
... y = np.sin(X).ravel()
- 然后,我们在
y
(使用y[::2]
)中的每隔一个数据点添加噪声,由0.5
缩放,因此我们不会引入太多抖动:
In [3]: y[::2] += 0.5 * (0.5 - rng.rand(50))
- 然后,您可以像以前创建任何其他树一样创建一个回归树。
一个小的区别是gini
和entropy
的分割标准不适用于回归任务。相反,scikit-learn 提供了两种不同的拆分标准:
- 使用最小均方误差准则,我们将建立两棵树。让我们首先构建一个深度为 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')
- 接下来,我们将构建一个最大深度为 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')
然后我们可以像线性回归器一样使用决策树,从第三章、监督学习的第一步。
- 为此,我们创建了一个测试集,其中 x 值在 0 到 5 的整个范围内密集采样:
In [7]: X_test = np.arange(0.0, 5.0, 0.01)[:, np.newaxis]
- 预测的 y 值可以通过
predict
方法获得:
In [8]: y_1 = regr1.predict(X_test)
... y_2 = regr2.predict(X_test)
- 如果我们将所有这些绘制在一起,我们可以看到决策树是如何不同的:
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 年初推出后不久,就迅速在机器学习社区中流行起来,这主要是因为它们在早期手写数字分类中的成功。它们至今仍然适用,尤其是在计算机视觉等应用领域。
本章的目标是将支持向量机应用于计算机视觉中的一个流行问题:行人检测。与识别任务(我们命名对象的类别)相反,检测任务的目标是说明图像中是否存在特定的对象(或者在我们的情况下,行人)。您可能已经知道 OpenCV 可以用两到三行代码来实现这一点。但是,如果我们这样做,我们将不会学到任何东西。因此,我们将从头开始构建整个管道!我们将获得一个真实的数据集,使用方向梯度的直方图 ( HOG )执行特征提取,并对其应用 SVM。
在本章中,我们将使用 Python 在 OpenCV 中实现支持向量机。我们将学习处理非线性决策边界和理解核心技巧。在这一章的最后,我们将学习在野外探测行人。
在此过程中,我们将涵盖以下主题:
- 用 Python 在 OpenCV 中实现支持向量机
- 处理非线性决策边界
- 理解内核技巧
- 在野外探测行人
兴奋吗?那我们走吧!
技术要求
您可以从以下链接查阅本章的代码:github . com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/chapter 06
。
以下是软件和硬件要求的简短总结:
- OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以正常工作)。
- Python 3.6 版本(任何 Python 3 . x 版本都可以)。
- Anaconda Python 3,用于安装 Python 和所需的模块。
- 这本书可以使用任何操作系统——苹果操作系统、视窗操作系统和基于 Linux 的操作系统。我们建议您的系统中至少有 4 GB 内存。
- 你不需要一个图形处理器来运行书中提供的代码。
理解线性支持向量机
为了理解支持向量机是如何工作的,我们必须考虑决策边界。当我们在前面的章节中使用线性分类器或决策树时,我们的目标总是最小化分类错误。我们通过使用均方误差评估准确性来做到这一点。一个 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>
这将产生以下输出:
前面的输出显示了二进制分类问题随机生成的数据。可以看到,大部分情况下,两个类的数据点是明显分开的。但是,有几个区域(特别是靠近图的左侧和底部)两个类别的数据点混合在一起。这些很难正确分类,我们马上就会看到。
预处理数据集
下一步是将数据点分成训练集和测试集,就像我们之前做的那样。但是,在此之前,我们必须为 OpenCV 准备如下数据:
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 中,支持向量机的构建、训练和评分方式与我们迄今为止遇到的其他学习算法完全相同,使用以下四个步骤:
- 调用
create
方法构建新 SVM:
In [6]: import cv2
... svm = cv2.ml.SVM_create()
如下图所示,我们可以在不同的模式下操作 SVM。目前,我们所关心的是我们在前面的例子中讨论过的情况:一个试图用直线分割数据的 SVM。这可以用setKernel
方法指定:
In [7]: svm.setKernel(cv2.ml.SVM_LINEAR)
- 调用分类器的
train
方法找到最优决策边界:
In [8]: svm.train(X_train, cv2.ml.ROW_SAMPLE, y_train)
Out[8]: True
- 调用分类器的
predict
方法预测测试集中所有数据样本的目标标签:
In [9]: _, y_pred = svm.predict(X_test)
- 使用 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 背后的人那里借用一个技巧。这个想法是生成一个由 x 和 y 坐标组成的精细网格,并通过 SVM 的predict
方法运行。这将允许我们知道,对于每个 (x,y) 点,分类器会预测什么目标标签。
我们将在一个专门的函数中这样做,我们称之为plot_decision_boundary ...
处理非线性决策边界
如果无法使用线性决策边界对数据进行最佳分区,该怎么办?在这种情况下,我们说数据不是线性可分的*。*
处理不可线性分离的数据的基本思想是创建原始特征的非线性组合。这就好比说我们想把数据投影到一个更高维的空间(比如从 2D 到 3D),在这个空间里数据突然变成线性可分的。
下图说明了这一概念:
上图展示了如何在高维空间中找到线性超平面。如果原始输入空间(左)中的数据不能线性分离,我们可以应用映射函数 *ϕ(.)*将 2D 的数据投影到三维(或高维)空间。在这个高维空间中,我们可能会发现现在有一个线性决策边界(在 3D 中,它是一个平面)可以分隔数据。
A linear decision boundary in an n-dimensional space is called a hyperplane. For example, a decision boundary in 6D feature space is a 5D hyperplane; in 3D feature space, it's a regular 2D plane; and in 2D space, it's a straight line.
然而,这种映射方法的一个问题是,它在大维度上是不切实际的,因为它增加了许多额外的项来进行维度之间的数学投影。这就是所谓的核心绝招发挥作用的地方。
理解内核技巧
当然,我们没有时间开发真正理解内核技巧所需的所有数学。一个更现实的部分标题应该是*承认有一个叫做内核技巧的东西存在,并且接受它是有效的,*但是那样会有点罗嗦。
简单来说,这是核心技巧。
为了计算出决策超平面在高维空间中的斜率和方向,我们必须将所有特征值乘以适当的权重值,并将它们相加。我们的特征空间的维度越多,我们要做的工作就越多。
然而,比我们聪明的数学家早就意识到,SVM 不需要明确地在高维空间工作...
了解我们的内核
OpenCV 提供了一系列的 SVM 内核来进行实验。一些最常用的方法包括:
cv2.ml.SVM_LINEAR
:这是我们之前用的内核。它在原始特征空间中提供了一个线性决策边界(即 x 和 y 值)。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 的过程,但这次,我们希望使用一系列不同的内核:
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):
那么步骤如下:...
在野外探测行人
我们简单谈了一下检测和识别的区别。而识别关注的是对物体进行分类(例如,作为行人、汽车、自行车等),检测基本上是回答这个问题:在这个图像中是否存在行人?
大多数检测算法背后的核心思想是将图像分割成许多小块,然后将每个图像块分类为包含行人或不包含行人。这正是我们在这一部分要做的。为了得到我们自己的行人检测算法,我们需要执行以下步骤:
- 建立一个包含行人的图像数据库。这些将是我们的正面数据样本。
- 建立一个不包含行人的图像数据库。这些将是我们的负数据样本。
- 在数据集上训练 SVM。
- 将 SVM 应用于测试图像的每个可能的补丁,以确定整个图像是否包含行人。
获取数据集
出于本节的目的,我们将使用麻省理工学院的人员数据集,我们可以将其免费用于非商业目的。因此,在获得相应的软件许可之前,请确保不要在您开创性的自主创业公司中使用这种软件。
However, if you followed our installation instructions from earlier and checked out the code on GitHub, you already have the dataset and are ready to go! The file can be found at github.com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/blob/master/data/chapter6/pedestrians128x64.tar.gz
.
通过参考以下步骤,您将学会在野外检测行人:
- 因为我们应该从...
看一眼方向梯度的直方图
HOG 可能只是提供我们正在寻找的帮助,以便完成这个项目。HOG 是图像的特征描述符,很像我们在第四章、中讨论的代表数据和工程特征的描述符。它已经成功地应用于计算机视觉中的许多不同任务,但似乎在对人进行分类方面特别有效。
HOG 特征背后的本质思想是图像中物体的局部形状和外观可以通过边缘方向的分布来描述。图像被分成小的连接区域,在这些区域内,梯度方向(或边缘方向)的直方图被编译。然后,通过连接不同的直方图来组装描述符。为了提高性能,局部直方图也可以进行对比度归一化,这导致对光照和阴影变化的更好的不变性。
HOG 描述符在 OpenCV 中通过cv2.HOGDescriptor
相当容易访问,它接受一堆输入参数,例如检测窗口大小(要检测的对象的最小大小,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%)。这表明模型过度拟合了数据。事实上,它在训练集上的表现比测试集好得多,这意味着模型已经求助于记忆训练样本,而不是试图将其抽象成有意义的决策规则。我们能做些什么来提高模型性能?
引导模型
提高模型性能的一个有趣方法是使用自举。这一思想实际上被应用在第一篇关于将支持向量机与 HOG 特征结合用于行人检测的论文中。因此,让我们向先驱们致敬,并试着了解他们做了什么。
他们的想法很简单。在训练集上训练 SVM 后,他们对模型进行评分,发现模型产生了一些误报。请记住,假阳性意味着模型预测的样本阳性(+)实际上是阴性(-)。在我们的上下文中,这意味着 SVM 错误地认为图像包含行人。如果数据集内的特定图像出现这种情况,则本例...
在更大的图像中检测行人
剩下要做的是将 SVM 分类程序与检测过程联系起来。这样做的方法是对图像中的每个可能的补丁重复我们的分类。这类似于我们之前可视化决策边界时所做的事情;我们创建了一个精细的网格,并对网格上的每个点进行分类。同样的想法也适用于这里。我们将图像分成多个小块,并将每个小块分类为是否包含行人。
通过执行以下步骤,您将能够检测到图像中的行人:
- 我们首先必须在图像中循环所有可能的面片,如下所示,每次将我们感兴趣的区域移动少量
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):
- 我们希望确保我们不会超越图像边界:
... if ystart + hroi > img_test.shape[0]:
... continue
... if xstart + wroi > img_test.shape[1]:
... continue
- 然后我们切割出感兴趣区域,对其进行预处理,并对其进行分类:
... roi = img_test[ystart:ystart + hroi,
... xstart:xstart + wroi, :]
... feat = np.array([hog.compute(roi, (64, 64))])
... _, ypred = svm.predict(feat)
- 如果该特定补丁恰好被归类为行人,我们会将其添加到成功列表中:
... if np.allclose(ypred, 1):
... found.append((ystart, xstart, hroi, wroi))
- 因为行人不仅可能出现在不同的位置,而且可能出现在不同的大小,我们将不得不重新缩放图像并重复整个过程。谢天谢地,OpenCV 以
detectMultiScale
函数的形式为这个多尺度检测任务提供了便利功能。这有点难,但我们可以将所有 SVM 参数传递给hog
对象:
In [24]: rho, _, _ = svm.getDecisionFunction(0)
... sv = svm.getSupportVectors()
... hog.setSVMDetector(np.append(sv.ravel(), rho))
- 然后可以调用检测函数:
In [25]: found = hog.detectMultiScale(img_test)
该函数将返回包含检测到的行人的边界框列表。
This seems to work only for linear SVM classifiers. The OpenCV documentation is terribly inconsistent across versions in this regard, so I'm not sure at which version this started or stopped working. Be careful!
- 实际上,当人们面临诸如行人检测的标准任务时,他们通常依赖于内置于 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)
- 使用 matplotlib 绘制测试图像很容易,如下所示:
In [29]: from matplotlib import patches
... fig = plt.figure()
... ax = fig.add_subplot(111)
... ax.imshow(cv2.cvtColor(img_test, cv2.COLOR_BGR2RGB))
- 然后我们可以通过在
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 是否足够强大,可以对数据进行分类,在这种情况下,就不会有...
基于支持向量机的多类分类
支持向量机本质上是两类分类器。具体而言,实践中最流行的多类分类方法是创建 |C| 一对其余分类器(通常称为一对所有 ( OVA )分类),其中 |C| 是类的数量,并选择对测试数据进行分类的类具有最高的余量。另一种方法是开发一组一对一的分类器,并选择由最多分类器选择的类别。虽然这涉及构建 |C|(|C| - 1)/2 分类器,但训练分类器的时间可能会减少,因为每个分类器的训练数据集要小得多。
现在让我们快速跳到如何在真实数据集的帮助下使用支持向量机应用多类分类。
就本节而言,我们将使用智能手机数据集与 UCI 人类活动识别合作,我们可以将其免费用于非商业目的。因此,在获得相应的软件许可之前,请确保不要在您开创性的自主创业公司中使用这种软件。
数据集可从 Kaggle 网站www . Kaggle . com/UCI ml/人类活动-智能手机识别
获得。在那里你会发现一个下载按钮,它会引导你找到一个名为的文件。
However, if you followed our installation instructions from earlier and checked out the code on GitHub, you already have the dataset and are ready to go! The file can be found at notebooks/data/multiclass
.
关于日期
在 19-48 岁的年龄组中选择了一组 30 名志愿者,并对他们进行了实验。每人借助系在腰间的智能手机进行了 6 项活动,分别是Walking
、Walking_Upstairs
、Walking_Downstairs
、Sitting
、Standing
和Laying
。主要使用嵌入式加速度计和陀螺仪捕获恒定速率为 50 Hz 的三轴线性加速度和三轴角速度。为了给这些数据贴上标签,这些实验被录了下来。数据集被随机分成两组,其中 70%的志愿者被选中生成训练数据,30%的志愿者被选中生成测试数据。
属性信息
对于数据集中的每个条目,提供了以下内容:
- 加速度计的三轴加速度和物体的近似加速度
- 陀螺仪的三轴角速度
- 具有 561 特征向量的时域和频域变量
- 各种活动标签
- 被观察对象的标识符
通过参考以下步骤,您将了解如何使用支持向量机构建多类分类:
- 让我们快速导入您需要的所有必要库,以便实现具有多类分类的 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
- 接下来,您将加载数据集。因为我们应该从
notebooks/
目录中的 Jupyter 笔记本运行这段代码,所以数据目录的相对路径只是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"))
- 让我们检查训练和测试数据集中是否有任何缺失值;如果有,我们将简单地从数据集中删除它们:
In [3]: train.isnull().values.any()
Out[3]: False
In [4]: test.isnull().values.any()
Out[4]: False
- 接下来,我们将找到数据中类的频率分布,这意味着我们将检查有多少样本属于六个类中的每一个:
In [5]: train_outcome = pd.crosstab(index=train["Activity"], # Make a crosstab
columns="count") # Name the count column
... train_outcome
从下面的截图中,可以观察到LAYING
类样本最多,但总体来说,数据分布大致均匀,没有出现类不平衡的主要迹象:
- 接下来,我们将从训练和测试数据集中分离出预测值(输入值)和结果值(类标签):
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)
- 由于 SVM 期望数字输入和标签,您现在将把非数字标签转换为数字标签。但是首先,我们必须从
sklearn
库中导入一个preprocessing
模块:
In [7]: from sklearn import preprocessing
... encoder = preprocessing.LabelEncoder()
- 现在,我们将列车和测试标签编码为数值:
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)
- 接下来,我们将缩放(标准化)列车和测试特征集,为此,您将从
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)
- 一旦数据被缩放并且标签的格式正确,现在就是我们拟合数据的时候了。但在此之前,我们将定义一个字典,该字典具有 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]}]
- 最后,我们将使用前面的参数对数据调用
GridSearchCV
以获得最佳 SVM 拟合:
In [11]: svm_model = GridSearchCV(SVC(), params_grid, cv=5)
... svm_model.fit(X_train_scaled, Y_train)
- 是时候检查一下 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%的准确率;这就是支持向量机的力量。
摘要
在本章中,我们了解了各种形式和风格的支持向量机。我们现在知道如何在 2D 和高维空间中绘制决策边界和超平面。我们了解了不同的 SVM 内核,并研究了如何在 OpenCV 中实现它们。
此外,我们还将新获得的知识应用到行人检测的实际例子中。为此,我们必须了解 HOG 特征描述符,以及如何为任务收集合适的数据。我们使用自举来提高分类器的性能,并将分类器与 OpenCV 的多尺度检测机制相结合。
这不仅是一章要消化的内容,而且你已经读完了这本书的一半。恭喜你!
在下一章中,...
七、使用贝叶斯学习实现垃圾邮件过滤器
在我们开始掌握高级主题(如聚类分析、深度学习和集成模型)之前,让我们将注意力转向一个迄今为止被我们忽略的更简单的模型:朴素贝叶斯分类器。
朴素贝叶斯分类器源于贝叶斯推理,以著名的统计学家和哲学家托马斯·贝叶斯(1701-1761)的名字命名。贝叶斯定理以描述基于可能导致事件的条件的先验知识的事件概率而闻名。我们可以使用贝叶斯定理来建立一个统计模型,该模型不仅可以对数据进行分类,还可以为我们提供对我们的分类正确的可能性的估计。在我们的案例中,我们可以使用贝叶斯推断,以高可信度驳回作为垃圾邮件的电子邮件,并在筛查测试呈阳性的情况下,确定女性患乳腺癌的概率。
我们现在已经在实现机器学习方法的机制方面获得了足够的经验,因此我们不应该再害怕尝试和理解它们背后的理论。别担心,我们不会写一本关于它的书,但我们需要对理论有所了解才能理解模型的内部工作。之后,我相信你会发现贝叶斯分类器易于实现,计算效率高,并且在相对较小的数据集上表现得相当好。在本章中,我们将了解朴素贝叶斯分类器,然后实现我们的第一个贝叶斯分类器。然后,我们将使用朴素贝叶斯分类器对电子邮件进行分类。
在本章中,我们将涵盖以下主题:
- 理解朴素贝叶斯分类器
- 实现你的第一个贝叶斯分类器
- 使用朴素贝叶斯分类器对电子邮件进行分类
技术要求
可以从以下链接查阅本章代码:github . com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/chapter 07
。
以下是软件和硬件要求的总结:
- 您将需要 OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以)。
- 您将需要 Python 3.6 版本(任何 Python 3 . x 版本都可以)。
- 您将需要 Anaconda Python 3 来安装 Python 和所需的模块。
- 这本书可以使用任何操作系统——苹果操作系统、视窗操作系统和基于 Linux 的操作系统。我们建议您的系统中至少有 4 GB 内存。
- 你不需要一个图形处理器来运行本书提供的代码。
理解贝叶斯推理
虽然贝叶斯分类器实现起来相对简单,但它们背后的理论一开始可能会相当反直觉,尤其是如果你还不太熟悉概率论的话。然而,贝叶斯分类器的美妙之处在于,它们比我们迄今为止遇到的所有分类器都更好地理解底层数据。例如,标准分类器,如k-最近邻算法或决策树,可能能够告诉我们从未见过的数据点的目标标签。然而,这些算法不知道他们的预测是对是错的可能性有多大。我们称之为辨别模型。另一方面,贝叶斯模型了解导致数据的潜在概率分布。我们称它们为生成模型,因为它们不只是在现有的数据点上贴标签——它们还可以用相同的统计数据生成新的数据点。
如果这最后一段有点超出你的理解范围,你可能会喜欢下面关于概率论的简介。这对接下来的部分很重要。
绕过概率论一小段路
为了理解贝叶斯定理,我们需要掌握以下技术术语:
- 随机变量:这是一个值取决于偶然性的变量。一个很好的例子是抛硬币的行为,这可能会出现正面或反面。如果一个随机变量只能取有限数量的值,我们称之为离散(如掷硬币或掷骰子);否则,我们称之为连续随机变量(如某一天的温度)。随机变量通常用大写字母排版。
- 概率:这是衡量一个事件发生的可能性。我们将事件发生的概率 e 表示为 p(e) ,它必须是 0 和 1 之间的数字(或介于 0 和 1 之间...
理解贝叶斯定理
在很多情况下,知道我们的分类器出错的可能性有多大真的很好。例如在第五章、利用决策树进行医学诊断中,我们训练了一个决策树,根据一些医学测试来诊断女性乳腺癌。你可以想象,在这种情况下,我们会不惜一切代价避免一次误诊;诊断一名患有乳腺癌的健康女性(假阳性)将是令人心碎的,并导致不必要的、昂贵的医疗程序,而错过一名女性的乳腺癌(假阴性)最终可能会让该女性付出生命。
很高兴知道我们可以依靠贝叶斯模型。让我们从yudkowsky.net/rational/bayes
来看一个具体的(也是相当有名的)例子:
"1% of women at age forty who participate in routine screening have breast cancer. 80% of women with breast cancer will get positive mammographies. 9.6% of women without breast cancer will also get positive mammographies. A woman in this age group had a positive mammography in a routine screening. What is the probability that she actually has breast cancer?"
你认为答案是什么?
嗯,考虑到她的乳房 x 光检查是阳性的,你可能会认为她患癌症的概率很高(接近 80%)。这个女人属于 9.6%假阳性的可能性似乎要小得多,所以真正的概率可能在 70%到 80%之间。
恐怕这是不对的。
这是思考这个问题的一种方法。为了简单起见,让我们假设我们看到的是一些具体的病人数量,比如 10,000 人。在乳房 x 光检查之前,10,000 名妇女可以分为两组:
- 第十组 : 100 名患有乳腺癌的女性
** Y 组:9900 名女性无乳腺癌*
*目前为止,一切顺利。如果我们将两组的数字相加,我们总共得到了 10,000 名患者,这证实了没有人在数学上输了。乳房 x 光检查后,我们可以将 10,000 名妇女分为四组:
- 第 1 组 : 80 名乳腺癌患者,乳腺钼靶检查阳性
- 第 2 组 : 20 名乳腺癌患者,乳腺钼靶检查阴性
- 第 3 组:约 950 名无乳腺癌且乳腺钼靶检查阳性的女性
- 第 4 组:约。8950 名无乳腺癌且乳房 x 光检查阴性的妇女
从前面的分析可以看出,四组之和都是一万。第 1 组****第 2 组(有乳腺癌)之和对应第组 X ,第 3 组和第 4 组(无乳腺癌)之和对应第 Y 组。
当我们把它画出来时,这可能会变得更清楚:
该图中,上半部分对应X 组,下半部分对应Y 组。类似地,左半部分对应于所有乳房 x 光检查阳性的妇女,右半部分对应于所有乳房 x 光检查阴性的妇女。
现在,更容易看到,我们正在寻找的只涉及图的左半部分。阳性结果的癌症患者在所有阳性结果患者组中的比例是第 1 组在第 1 组和第 3 组中的比例:
80 / (80 + 950) = 80 / 1,030 = 7.8%
换句话说,如果你为 10,000 名患者提供乳房 x 光检查,那么在 1030 名乳房 x 光检查阳性的患者中,将有 80 名乳房 x 光检查阳性的患者患有癌症。如果医生问她患乳腺癌的可能性,她应该给一个乳房 x 光检查阳性的病人答案:考虑到 13 个病人问这个问题,大约 13 个人中有 1 个会患癌症。
我们刚才计算的叫做一个条件概率:在(我们也说给一个阳性乳腺摄影的情况下,我们对一个女性得乳腺癌的信任度是多少?如最后一小节,我们用 p(癌症|乳腺摄影)或 p(C|M) 简称来表示。使用大写字母再次强调了健康和乳房 x 光检查都可能有几种结果的观点,这取决于几种潜在的(也可能是未知的)原因。因此,它们是随机变量。**
然后,我们可以用以下公式表示 P(C|M) :
这里, p(C,M) 表示 C 和 M 都为真的概率(指女性既有癌症又有乳房 x 光检查阳性的概率)。如前所示,这相当于女性属于第 1 组的概率。
逗号(、)表示逻辑*、,颚化符( ~ )表示逻辑而非*。因此, p(~C,M) 表示 C 不为真的概率, M 为真的概率(指女性没有癌症但钼靶检查呈阳性的概率)。这相当于一个女性属于第三组的概率。所以,分母基本上加起来就是第 1 组( p(C,M) )和第 3 组( p(~C,M) )的女性。
但是等等!这两组加在一起只是表示女性乳房 x 光检查阳性的概率, p(M) 。因此,我们可以简化前面的等式:
贝叶斯版本是重新解读 p(C,M) 的含义。我们可以将 p(C,M) 表达如下:
现在有点混乱了。这里, p(C) 简单来说就是女性患癌的概率(对应于前面提到的 X 组)。考虑到一名妇女患有癌症,她的乳房 x 光检查呈阳性的概率是多少?从问题题来看,我们知道是 80%。这是 p(M|C) , M 给出 C 的概率。
用这个新公式代替第一个公式中的 p(C,M) ,我们得到如下公式:
在贝叶斯世界中,这些术语都有其特定的名称:
- p(C|M) 被称为后路,始终是我们要计算的东西。在我们的例子中,这对应于在乳房 x 光检查呈阳性的情况下,认为女性患有乳腺癌的程度。
- p(C) 被称为先验,因为它对应于我们关于乳腺癌有多常见的初步知识。我们也称之为我们对 C 的最初信仰程度。
- p(M|C) 称为似。
- p(M) 称为证据。
因此,您可以再次重写等式,如下所示:
大多数情况下,只对该分数的分子感兴趣,因为分母不依赖于 C,,所以分母是常数,可以忽略不计。
理解朴素贝叶斯分类器
到目前为止,我们只谈了一个证据。然而,在大多数现实场景中,我们必须在多条证据(如随机变量X1和X2的情况下预测一个结果(如随机变量 Y )。所以,不是计算p(Y | X)而是经常要计算 p(Y|X 1 ,X 2 ,...,X n ) 。不幸的是,这使得数学非常复杂。对于两个随机变量, 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 并没有真正提供一个真正的朴素贝叶斯分类器。相反,它带有贝叶斯分类器,不一定期望特征是独立的,而是期望数据聚集成高斯斑点。这正是我们之前创建的数据集!
通过以下步骤,您将了解如何使用普通贝叶斯分类器构建分类器:
- 我们可以使用以下函数创建一个新的分类器:
In [5]: import cv2
... model_norm = cv2.ml.NormalBayesClassifier_create()
- 然后,通过
train
方法进行训练:
In [6]: model_norm.train(X_train, cv2.ml.ROW_SAMPLE, y_train)
Out[6]: True
- 一旦分类器训练成功,它将返回
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
- 更好的是——我们可以重用上一章的绘图功能来检查决策边界!如果你还记得的话,这个想法是创建一个包含所有数据点的网格,然后对网格上的每个点进行分类。网格是通过同名的 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))
meshgrid
函数将返回两个浮点矩阵xx
和yy
,它们包含网格上每个坐标点的 x 和 y 坐标。我们可以使用ravel
函数将这些矩阵展平为列向量,并将它们堆叠起来形成一个新矩阵X_hypo
:
... X_hypo = np.column_stack((xx.ravel().astype(np.float32),
... yy.ravel().astype(np.float32)))
X_hypo
现在包含X_hypo[:, 0]
中的所有 x 值和X_hypo[:, 1]
中的所有 y 值。这是predict
功能可以理解的格式:
... ret = model.predict(X_hypo)
- 然而,我们希望能够同时使用 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)
- 剩下要做的就是创建一个等高线图,其中
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)
- 我们通过传递模型(
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(C1| X)=0.05。
The reason why some of the rows show values greater than 1 is that OpenCV does not really return probability values. Probability values are always between 0 and 1, and each row in the preceding matrix should add up to 1. Instead, what is being reported is a likelihood, which is basically the numerator of the conditional probability equation, p(C) p(M|C). The denominator, p(M), does not need to be computed. All we need to know is that 0.15 > 0.05 (top row). Hence, the data point most likely belongs to class 0.
用朴素贝叶斯分类器对数据进行分类
以下步骤将帮助您构建朴素贝叶斯分类器:
- 我们可以通过向 scikit-learn 寻求帮助,将结果与真正的朴素贝叶斯分类器进行比较:
In [13]: from sklearn import naive_bayes... model_naive = naive_bayes.GaussianNB()
- 像往常一样,通过
fit
方法训练分类器:
In [14]: model_naive.fit(X_train, y_train)Out[14]: GaussianNB(priors=None)
- 分类器的评分内置于:
In [15]: model_naive.score(X_test, y_test)Out[15]: 1.0
- 又是满分!然而,与 OpenCV 相反,这个分类器的
predict_proba
方法返回真实的概率值,因为所有的值都在 0 和 1 之间,并且因为所有的行加起来是 1:
In [16]: yprob = model_naive.predict_proba(X_test) ...
可视化条件概率
通过参考以下步骤,您将能够可视化条件概率:
- 为此,我们将稍微修改前面示例中的绘图函数。我们首先在(
x_min
、x_max
)和(y_min
、y_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))
- 然后,我们展平
xx
和yy
,并按列将它们添加到特征矩阵中,X_hypo
:
... X_hypo = np.column_stack((xx.ravel().astype(np.float32),
... yy.ravel().astype(np.float32)))
- 如果我们想让这个函数与 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)
- 就像我们之前看到的
In [16]
一样,y_proba
将是一个 2D 矩阵,对于每个数据点,包含数据属于 0 类(在y_proba[:, 0]
中)和 1 类(在y_proba[:, 1]
中)的概率。将这两个值转换成轮廓函数可以理解的颜色的一种简单方法是简单地取两个概率值的差:
... zz = y_proba[:, 1] - y_proba[:, 0]
... zz = zz.reshape(xx.shape)
- 最后一步是将
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)
- 现在,我们准备调用函数:
In [19]: plot_proba(model_naive, X, y)
结果是这样的:
上面的截图显示了朴素贝叶斯分类器的条件概率。
使用朴素贝叶斯分类器对电子邮件进行分类
这一章的最终任务将是把我们新获得的技能应用到真正的垃圾邮件过滤器上!该任务涉及使用朴素贝叶斯算法解决二进制分类问题。
朴素贝叶斯分类器实际上是一个非常流行的电子邮件过滤模型。他们的天真很好地适用于文本数据的分析,其中每个特征都是一个单词(或一袋单词**),并且建立每个单词对其他单词的依赖模型是不可行的。**
**有很多好的电子邮件数据集,例如:
- 惠普垃圾邮件数据库:
archive . ics . UCI . edu/ml/机器学习-数据库/spambase
- 恩罗-垃圾邮件数据集:
www.aueb.gr/users/ion/data/enron-spam...
正在加载数据集
您可以参考以下步骤来加载数据集:
- 如果你从 GitHub 下载了最新的代码,你会在
notebooks/data/chapter7
目录下找到几个.zip
文件。这些文件包含原始电子邮件数据(带有“收件人”、“抄送”和“正文”字段),这些数据要么被归类为垃圾邮件(带有SPAM = 1
类别标签),要么不被归类为垃圾邮件(也称为 ham,HAM = 0
类别标签)。 - 我们构建了一个名为
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)
... ]
- 第一步是将这些文件提取到子目录中。为此,我们可以使用我们在上一章中编写的
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))
- 要将该函数应用于源中的所有数据文件,我们需要运行一个循环。
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
的特定目录下的所有文件中提取相关内容的功能。
要从单个文件中提取相关内容,我们需要了解每个文件的结构。我们唯一知道的是,电子邮件的标题部分(发件人:,收件人:,和抄送:)和正文由一个换行符'\n'
隔开。因此,我们可以做的是迭代文本文件中的每一行,只保留那些属于主文本主体的行,这些行将存储在变量行中。我们还想在周围保留一个布尔标志past_header
,它最初被设置为False
,但是当我们通过标题部分时,它将被翻转到True
:
- 我们从初始化这两个变量开始:
In [4]: import os
... def read_single_file(filename):
... past_header, lines = False, []
- 然后,我们检查名称为
filename
的文件是否存在。如果有,我们就开始一行一行地循环:
... if os.path.isfile(filename):
... f = open(filename, encoding="latin-1")
... for line in f:
你可能已经注意到了encoding="latin-1"
部分。由于某些电子邮件不是 Unicode 格式,这是为了正确解码文件。
我们不想保留标题信息,所以我们一直循环,直到遇到'\n'
字符,此时我们将past_header
从False
翻转到True
。
- 此时,满足以下
if-else
子句的第一个条件,我们将文本文件中剩余的所有行追加到lines
变量中:
... if past_header:
... lines.append(line)
... elif line == '\n':
... past_header = True
... f.close()
- 最后,我们将所有行连接成一个字符串,用换行符分隔,并返回文件的完整路径和文件的实际内容:
... content = '\n'.join(lines)
... return filename, content
- 第二个功能的工作是循环一个文件夹中的所有文件,并对它们调用
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
返回一个生成器,而不是实际值,如果您期望有大量的项目要迭代,这是可取的。
用熊猫建立数据矩阵
现在,是时候介绍 Python Anaconda 预装的另一个基本数据科学工具了:熊猫。pandas 建立在 NumPy 之上,提供了几种有用的工具和方法来处理 Python 中的数据结构。就像我们一般用别名np
导入 NumPy 一样,用pd
别名导入熊猫也很常见:
In [6]: import pandas as pd
熊猫提供了一个有用的数据结构,称为数据帧,可以理解为 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
),我们需要访问熊猫数据框中的数据。这可以通过将数据框视为字典来实现,其中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_train
和y_train
传递给train
函数,OpenCV 会抱怨数据矩阵不是 NumPy 数组。...
在整个数据集上进行训练
但是,如果您想要对整个数据集进行分类,我们需要一种更复杂的方法。我们转向 scikit-learn 的朴素贝叶斯分类器,因为它了解如何处理稀疏矩阵。事实上,如果你之前没有像对待每一个 NumPy 数组一样去关注和对待X_train
,你甚至可能不会注意到有什么不同:
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 克来提高结果
一件事就是用n-克数代替普通字数。到目前为止,我们一直依赖于所谓的单词包:我们只是把一封电子邮件的每个单词都扔进一个包里,并计算它出现的次数。然而,在真实的电子邮件中,单词出现的顺序可以携带大量信息!
这正是 n 克数想要传达的信息。你可以把一个 n 克想象成一个短语,它有 n 个单词长。比如短语统计有其矩包含以下 1 克:统计有、有、有其、矩。还有以下 2 克:统计有、有其、有其瞬间。它还有两个 3 克(统计有它的和有它的时刻)并且只有一个...***
八、利用无监督学习发现隐藏结构
到目前为止,我们的注意力完全集中在监督学习问题上,其中数据集中的每个数据点都有一个已知的标签或目标值。然而,当没有已知的输出或没有老师监督学习算法时,我们该怎么办?
这就是无监督学习的意义所在。在无监督学习中,学习过程仅在输入数据中显示,并被要求从该数据中提取知识,而无需进一步指导。我们已经讨论了无监督学习的众多形式之一——降维。另一个流行的领域是聚类分析,旨在将数据划分为相似项目的不同组。
聚类技术可能有用的一些问题是文档分析、图像检索、查找垃圾邮件、识别假新闻、识别犯罪活动等。
在这一章中,我们想了解如何使用不同的聚类算法来提取简单、未标记的数据集中的隐藏结构。无论是用于特征提取、图像处理,还是作为监督学习任务的预处理步骤,这些隐藏结构都有很多好处。作为一个具体的例子,我们将学习如何将聚类应用于图像,以将其颜色空间减少到 16 位。
更具体地说,我们将涵盖以下主题:
- k 均值聚类和期望最大化并在 OpenCV 中实现
- 在层次树中安排聚类算法,这样做有什么好处
- 使用无监督学习进行预处理、图像处理和分类
我们开始吧!
使用 TF-IDF 来改进结果
它被称为术语频率-逆文档频率 ( TF -IDF ),我们在第四章、中遇到了它,代表数据和工程特性。如果您还记得,TF-IDF 所做的基本上是通过衡量单词在整个数据集中出现的频率来衡量单词数。这种方法的一个有用的副作用是 IDF 部分——单词出现的频率相反。这就保证了频繁词,如但在分类中只占很小的权重。
**我们通过调用现有特征矩阵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/chapter 08
。
以下是软件和硬件要求的总结:
- 您将需要 OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以)。
- 您将需要 Python 3.6 版本(任何 Python 3 . x 版本都可以)。
- 您将需要 Anaconda Python 3 来安装 Python 和所需的模块。
- 除了这本书,你可以使用任何操作系统——苹果操作系统、视窗操作系统和基于 Linux 的操作系统。我们建议您的系统中至少有 4 GB 内存。
- 你不需要一个图形处理器来运行本书提供的代码。
理解无监督学习
无监督学习可能有多种形式,但目标总是将原始数据转换为更丰富、更有意义的表示,无论这意味着让人类更容易理解还是让机器学习算法更容易解析。
无监督学习的一些常见应用包括:
降维:这是对由许多特征组成的数据进行高维表示,并试图压缩数据,使其主要特征可以用少量信息丰富的特征来解释。例如,当应用于波士顿附近的房价时,降维也许可以告诉我们,我们最应该关注的指标是房产税和附近的犯罪率。
因素分析:这是试图找出产生观测数据的隐藏原因或未观测到的成分。例如,当应用于 20 世纪 70 年代电视剧《??》的所有剧集时,史酷比-杜,你在哪里!,因子分析或许能告诉我们(剧透预警!)节目中的每一个鬼魂或怪物本质上都是一些心怀不满的伯爵在镇上精心设计的骗局。
聚类分析:这试图将数据划分为相似项目的不同组。这就是我们将在本章重点讨论的无监督学习类型。例如,当应用于网飞的所有电影时,聚类分析可能能够自动将它们分成不同的类型。
为了让事情变得更复杂,这些分析必须在没有标签的数据上进行,我们事先不知道正确的答案应该是什么。因此,无监督学习的一个主要挑战是确定一个算法是做得好还是学到了什么有用的东西。通常,评估无监督学习算法结果的唯一方法是手动检查它,并手动确定结果是否有意义。
也就是说,无监督学习非常有用,例如,作为预处理或特征提取步骤。你可以把无监督学习想象成一种数据转换——一种将数据从其原始表示转换成信息更丰富的形式的方法。学习一种新的表示可能会让我们对数据有更深入的了解,有时,它甚至可能会提高监督学习算法的准确性。
理解 k-均值聚类
OpenCV 提供的基本聚类算法是k-意味着聚类,它从未标记的多维数据中搜索预定数量的 k- 聚类(或组)。
它通过使用关于最佳聚类应该是什么样子的两个简单假设来实现这一点:
- 每个聚类的中心基本上是属于该聚类的所有点的平均值,也称为质心。
- 该集群中的每个数据点都比所有其他集群中心更靠近其中心。
看一个具体的例子最容易理解算法。
实现我们的第一个 k 均值示例
首先,让我们生成一个包含四个不同斑点的 2D 数据集。为了强调这是一种无监督的方法,我们将把标签排除在可视化之外:
- 我们将继续使用
matplotlib
来实现我们所有的可视化目的:
In [1]: import matplotlib.pyplot as plt
... %matplotlib inline
... plt.style.use('ggplot')
- 遵循前面章节中的相同方法,我们将创建总共 300 个斑点(
n_samples=300
)属于四个不同的集群(centers=4
):
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-意味着算法也可以做到这一点,而不需要任何关于目标标签或底层数据分布的信息。
- 虽然 k -means 当然是一个统计模型,但是在 OpenCV 中,它不是通过
ml
模块和常见的train
和predict
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
- 然后,我们可以将前面的数据矩阵(
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)
返回三个不同的变量。
- 第一个,
compactness
返回从每个点到它们对应的聚类中心的平方距离的总和。高紧密度分数表示所有点都接近它们的聚类中心,而低紧密度分数表示不同的聚类可能没有很好地分开:
In [5]: compactness
Out[5]: 527.01581170992
- 当然,这个数字很大程度上取决于
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);
- 这将生成所有数据点的散点图,这些数据点根据它们所属的聚类进行着色,相应的聚类中心在每个聚类的中心用较暗的斑点表示:
上图为 k 的结果-表示 k=4 的聚类。这里的好消息是k-意思是算法(至少在这个简单的例子中)将点分配给聚类,非常类似于我们可能做的,如果我们用眼睛做的话。但是算法是如何这么快找到这些不同的聚类的呢?毕竟,集群分配的可能组合的数量与数据点的数量成指数关系!用手,尝试所有可能的组合肯定需要很长时间。
幸运的是,没有必要进行彻底的搜索。取而代之的是, k -means 采用的典型方法是使用迭代算法,也称为期望最大化。
理解期望最大化
k-意味着聚类只是被称为期望最大化的更一般算法的一个具体应用。简而言之,该算法的工作原理如下:
- 从一些随机的集群中心开始。
- 重复直到收敛:
- 期望步骤:将所有数据点分配到它们最近的聚类中心。
- 最大化步骤:取聚类中所有点的平均值更新聚类中心。
这里,期望步骤如此命名,因为它涉及更新我们对数据集中每个点属于哪个聚类的期望。最大化步骤之所以如此命名,是因为它涉及最大化定义聚类中心位置的适应度函数。在 k 的情况下-意味着,最大化...
实现我们的期望最大化解决方案
期望最大化算法很简单,我们可以自己编码。为此,我们将定义一个函数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 的期望最大化:
- 初始化:随机选择若干个集群中心,
n_clusters
。我们不只是选择任何随机数,而是选择实际的数据点作为聚类中心。我们通过沿着第一个轴排列X
并在这个随机排列中选择第一个n_clusters
点来实现:
... rng = np.random.RandomState(rseed)
... i = rng.permutation(X.shape[0])[:n_clusters]
... centers = X[i]
while
永远循环:根据最近的聚类中心分配标签。在这里,scikit-learn 的pairwise_distance_argmin
功能正是我们想要的。它为X
中的每个数据点计算centers
中最近的聚类中心的索引:
... while True:
... labels = pairwise_distances_argmin(X, centers)
- 寻找新的聚类中心:这一步我们要取
X
中属于特定聚类(X[labels == i]
)的所有数据点的算术平均值:
... new_centers = np.array([X[labels ==
i].mean(axis=0)
- 检查收敛情况,必要时打破
while
循环:这是最后一步,确保一旦工作完成,我们就停止算法的执行。我们通过检查所有新的集群中心是否与旧的集群中心相等来确定工作是否完成。如果这是真的,我们退出循环;否则,我们继续循环:
... for i in range(n_clusters)])
... if np.all(centers == new_centers):
... break
... centers = new_centers
- 退出函数并返回结果:
... 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 均值聚类之前,所有数据点都被归类为同一种颜色;然而,在使用k-意味着聚类之后,每种颜色是不同的聚类(相似的数据点被聚类或分组为一种颜色) :
上图展示了我们自制 k 的结果——意味着使用期望最大化。我们可以看到,我们自制的算法完成了任务!诚然,这个特定的集群例子相当简单,而且大多数 ?? k 的实际实现意味着集群在幕后会做得更多。但现在,我们很幸福。
了解期望最大化的局限性
尽管简单,期望最大化在一系列场景中表现得非常好。也就是说,我们需要注意一些潜在的限制:
- 期望最大化不能保证我们会找到全局最优解。
- 我们必须事先知道期望的簇的数量。
- 算法的决策边界是线性的。
- 对于大型数据集,该算法速度较慢。
让我们快速详细地讨论一下这些潜在的警告。
第一个警告——不能保证找到全局最优
尽管数学家已经证明期望最大化步骤在每一步都改善了结果,但仍然不能保证最终我们会找到全局最优解。例如,如果我们在我们的简单示例中使用不同的随机种子(例如使用种子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 的一个例子——意味着错过了全局最优。发生了什么事?
简单地说,集群中心的随机初始化是不幸的。它导致黄色星系团的中心在两个顶端星系团之间移动,基本上将它们合并成一个。结果,其他集群变得混乱,因为他们突然不得不将两个视觉上不同的斑点分成三个集群。
由于这个原因,算法在多个初始状态下运行是很常见的。实际上,OpenCV 默认会这样做(由可选的attempts
参数设置)。
第二个警告——我们必须事先选择集群的数量
另一个潜在的限制是k-意味着不能从数据中学习聚类的数量。相反,我们必须事先告诉它我们期望有多少个集群。您可以看到,对于您尚未完全理解的复杂现实数据,这可能会有问题。
从 k 的角度来看,意味着没有错误或无意义的簇数。例如,如果我们要求算法在前面部分生成的数据集中识别六个聚类,它将愉快地继续并找到最佳的六个聚类:
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-意味着总是假设集群之间的线性边界,这意味着每当集群的几何形状比这更复杂时,它就会失败。
通过生成稍微复杂一点的数据集,我们看到了这种局限性。我们希望将数据组织成两个重叠的半圆,而不是从高斯斑点生成数据点。我们可以使用 scikit-learn 的make_moons
来做到这一点。这里,我们选择属于两个半圆的 200 个数据点,并结合一些高斯噪声:
In [14]: from sklearn.datasets import make_moons
... X, y = make_moons(200, noise=.05, random_state=12)
这一次,我们告诉k-意思是寻找两个集群:
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 的一个例子——意思是在非线性数据中寻找线性边界。从图中可以明显看出,k-表示未能识别两个半圆,而是用看起来像对角线的直线分割数据(从左下角到右上角)。
这种情况应该会让人想起。当我们在第六章*中讨论使用支持向量机检测行人时,我们遇到了同样的问题。*当时的想法是利用核技巧将数据转换成更高维的特征空间。我们能在这里做同样的事情吗?
我们当然可以。有一种内核化的形式k-意思是类似于支持向量机的内核技巧,叫做谱聚类。不幸的是,OpenCV 没有提供谱聚类的实现。幸运的是,scikit-learn 做到了:
In [16]: from sklearn.cluster import SpectralClustering
该算法使用与所有其他统计模型相同的 API:我们在构造函数中设置可选参数,然后对数据调用fit_predict
。在这里,我们希望使用最近邻居的图来计算数据的更高维表示,然后使用 k 来分配标签-意思是:
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 均值是缓慢的
k-的最后一个限制是,对于大数据集来说相对较慢。你可以想象,相当多的算法可能会遇到这个问题。但是, k -means 受到的影响尤其严重: k -means 的每次迭代都必须访问数据集中的每个数据点,并将其与所有聚类中心进行比较。
您可能想知道在每次迭代期间访问所有数据点的需求是否真的有必要。例如,您可以只使用数据的子集在每个步骤更新集群中心。事实上,这正是一种叫做基于批次的 k 的算法变体的基本思想——意思是。不幸的是,这个算法没有实现...
使用 k 均值压缩颜色空间
k -means 的一个有趣的用例是图像颜色空间的压缩。例如,标准的彩色图像具有 24 位色深,总共提供 16,777,216 种颜色。然而,在大多数图像中,大量的颜色将不会被使用,并且图像中的许多像素将具有相似的值。然后,压缩后的图像可以以更快的速度通过互联网发送,在接收端,它可以被解压缩以恢复原始图像。因此,降低了存储和传输成本。但是,图像色彩空间压缩将是有损的,并且您可能不会注意到压缩后图像中的细微细节。
或者,我们也可以使用k-手段来减少调色板。这里的想法是把集群中心想象成减少的调色板。然后,k-表示将原始图像中的数百万种颜色组织成适当数量的颜色。
可视化真彩色调色板
通过执行以下步骤,您将能够可视化彩色图像的真彩色调色板:
- 让我们来看看一个特殊的图像:
In [1]: import cv2... import numpy as np... lena = cv2.imread('data/lena.jpg', cv2.IMREAD_COLOR)
- 现在,我们知道如何在睡眠中启动 Matplotlib:
In [2]: import matplotlib.pyplot as plt... %matplotlib inline... plt.style.use('ggplot')
- 但是,这一次,我们希望禁用
ggplot
选项通常在图像上显示的网格线:
In [3]: plt.rc('axes', **{'grid': False})
- 然后,我们可以使用以下命令来可视化 Lena(不要忘记将颜色通道的 BGR 顺序切换到 RGB):
In [4]: plt.imshow(cv2.cvtColor(lena, cv2.COLOR_BGR2RGB)) ...
使用 k 均值缩小调色板
通过参考以下步骤,您将能够使用k-意味着聚类将彩色图像投影到缩小的调色板中:
- 现在,让我们通过指示 k 来将 1600 万种颜色减少到仅仅 16 种,这意味着将所有 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)
- 缩小调色板中的 16 种不同颜色对应于生成的簇。
centers
数组的输出显示所有颜色都有三个条目—B
、G
和R
—值介于 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)
labels
向量包含对应于 16 簇labels
的 16 种颜色。因此,标签为 0 的所有数据点将根据centers
数组中的第 0 行进行着色;同样,标签为 1 的所有数据点将根据centers
数组中的第 1 行进行着色,以此类推。因此,我们希望使用labels
作为centers
数组中的索引—这些是我们的新颜色:
In [11]: new_colors = centers[labels].reshape((-1, 3))
- 我们可以再次绘制数据,但这一次,我们将使用
new_colors
对数据点进行相应的着色:
In [12]: plot_pixels(img_data, colors=new_colors,
... title="Reduce color space: 16 colors")
结果是原始像素的重新着色,其中每个像素被分配其最近的聚类中心的颜色:
- 为了观察重新着色的效果,我们必须将
new_colors
绘制为图像。我们展平了之前的图像,从图像到数据矩阵。现在回到图像,我们需要做反,就是根据 Lena 图像的形状重塑new_colors
:
In [13]: lena_recolored = new_colors.reshape(lena.shape)
- 然后,我们可以像任何其他图像一样可视化重新着色的 Lena 图像:
In [14]: plt.figure(figsize=(10, 6))
... plt.imshow(cv2.cvtColor(lena_recolored, cv2.COLOR_BGR2RGB));
... plt.title('16-color image')
结果是这样的:
很棒,对吧?
总的来说,前面的截图非常清晰可辨,尽管有些细节可能丢失了。假设您将图像压缩了大约 100 万倍,这是非常了不起的。
您可以对任意数量的颜色重复此过程。
Another way to reduce the color palette of images involves the use of bilateral filters. The resulting images often look like cartoon versions of the original image. You can find an example of this in the book, OpenCV with Python Blueprints, by M. Beyeler, Packt Publishing.
k -means 的另一个潜在应用是您可能没有想到的:将其用于图像分类。
用 k-均值对手写数字进行分类
虽然上一个应用程序非常有创意地使用了 k -means,但我们还可以做得更好。我们之前已经讨论过k-意思是在无监督学习的背景下,我们试图发现数据中的一些隐藏结构。
然而,同样的概念难道不适用于大多数分类任务吗?假设我们的任务是对手写数字进行分类。如果不是一样的话,大多数零看起来不都是相似的吗?所有的 0 看起来不是和所有可能的 1 完全不同吗?这不正是我们着手用无监督学习去发现的那种隐藏结构吗?这不意味着我们也可以使用聚类进行分类吗?
让我们一起去发现。在本节中,我们将尝试...
正在加载数据集
从前面的章节中,您可能还记得 scikit-learn 通过其load_digits
实用功能提供了一系列手写数字。数据集由 1,797 个样本组成,每个样本具有 64 个特征,其中每个特征在8×8图像中具有一个像素的亮度:
In [1]: from sklearn.datasets import load_digits
... digits = load_digits()
... digits.data.shape
Out[1]: (1797, 64)
运行 k 均值
设置k-意味着工作方式与前面的例子完全相同。我们告诉算法最多执行 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×3矩阵...
将集群组织为分层树
替代 k 的方法是层次聚类。层次聚类的一个优点是,它允许我们在层次结构中组织不同的聚类(也称为树图,这可以使结果更容易解释。另一个有用的优点是,我们不需要预先指定集群的数量。
理解层次聚类
分层聚类有两种方法:
- 在凝聚层次聚类中,我们从每个数据点可能是它自己的聚类开始,然后我们合并最接近的聚类对,直到只剩下一个聚类。
- 在分裂的层次聚类中,情况正好相反;我们首先将所有数据点分配给同一个集群,然后将集群分成更小的集群,直到每个集群只包含一个样本。
当然,如果我们愿意,我们可以指定所需集群的数量。在下面的截图中,我们要求算法总共找到三个聚类:
前面的截图显示了一个凝聚的分步示例...
实现聚集层次聚类
虽然 OpenCV 没有提供凝聚层次聚类的实现,但它是一种流行的算法,应该属于我们的机器学习技能集:
- 我们从生成 10 个随机数据点开始,就像前面的截图一样:
In [1]: from sklearn.datasets import make_blobs
... X, y = make_blobs(random_state=100, n_samples=10)
- 使用熟悉的统计建模应用编程接口,我们导入
AgglomerativeClustering
算法并指定所需的聚类数:
In [2]: from sklearn import cluster
... agg = cluster.AgglomerativeClustering(n_clusters=3)
- 像往常一样,通过
fit_predict
方法将模型拟合到数据:
In [3]: labels = agg.fit_predict(X)
- 我们可以生成散点图,其中每个数据点都根据预测的标签进行着色:
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 应用于两个特定的应用,即减少图像的调色板和对手写数字进行分类。
在下一章中,我们将回到监督学习的世界,并谈论一些当前最强大的机器学习算法:神经网络和深度学习。**
九、利用深度学习分类手写数字
现在让我们回到监督学习,讨论一族被称为人工神经网络的算法。对神经网络的早期研究可以追溯到 20 世纪 40 年代,当时沃伦·麦卡洛克和沃尔特·皮茨首次描述了大脑中的生物神经细胞(或神经元)是如何工作的。最近,人工神经网络在流行词“深度学习”下复兴,深度学习为最先进的技术提供动力,如谷歌的深度思维和脸书的深度人脸算法。
在这一章中,我们想把我们的头包在人工神经网络的一些简单版本上,例如麦卡洛克-皮茨神经元、感知器和多层感知器。一旦我们熟悉了基础知识,我们将准备好实现一个更复杂的深度神经网络来对来自流行的 MNIST 数据库(简称国家标准与技术研究院混合数据库)的手写数字进行分类。为此,我们将利用高级神经网络库 Keras,这也是研究人员和科技公司经常使用的库。
在此过程中,我们将讨论以下主题:
- 在 OpenCV 中实现感知器和多层感知器
- 区分随机和批量梯度下降,以及它们如何适应反向传播
- 找到你的神经网络的大小
- 使用 Keras 构建复杂的深层神经网络
兴奋吗?那我们走吧!
技术要求
您可以在以下链接查阅本章的代码:github . com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/chapter 09
。
以下是软件和硬件要求的简短总结:
- OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以正常工作)。
- Python 3.6 版本(任何 Python 3 . x 版本都可以)。
- Anaconda Python 3,用于安装 Python 和所需的模块。
- 你可以在这本书里使用任何操作系统——苹果电脑、视窗或基于 Linux 的。我们建议您的系统中至少有 4 GB 内存。
- 你不需要一个图形处理器来运行书中提供的代码。
理解麦卡洛克-皮茨神经元
1943 年,沃伦·麦卡洛克和沃尔特·皮茨发表了一篇关于神经元的数学描述,因为它们被认为在大脑中运行。一个神经元通过其树突树上的连接从其他神经元接收输入,这些连接被整合以在细胞体(或躯体)产生输出。然后,输出通过一根长电线(或轴突)传递给其他神经元,最终分支到其他神经元的树突树上形成一个或多个连接(在轴突末端)。
下图显示了一个神经元示例:
麦卡洛克和皮茨将这种神经元的内部工作原理描述为一个简单的逻辑门,它可以打开或关闭,这取决于它在树状结构上接收到的输入。具体来说,神经元会将所有输入相加,如果总和超过某个阈值,就会产生一个输出信号,并由轴突传递。
However, today we know that real neurons are much more complicated than that. Biological neurons perform intricate nonlinear mathematical operations on thousands of inputs and can change their responsiveness dynamically depending on the context, importance, or novelty of the input signal. You can think of real neurons being as complex as computers and of the human brain being as complex as the internet.
让我们考虑一个简单的人工神经元,它正好接收两个输入, x 0 和 x 1 。人工神经元的工作是计算两个输入的和(通常以加权和的形式),如果这个和超过某个阈值(通常为零),神经元将被认为是活动的,并输出一个 1;否则它将被认为是无声的,并输出一个负 1(或零)。用更数学的术语来说,这个麦卡洛克-皮茨神经元的输出 y 可以描述如下:
在上式中, w 0 和 w 1 是权重系数,与 x 0 和 x 1 一起构成加权和。在教科书中,输出 y 为 +1 和 -1 的两种不同情况通常会被激活函数 ϕ 所掩盖,该函数可能采用两种不同的值:
这里我们引入一个新的变量, z (所谓的网络输入,相当于加权和:z = w0x0+w1x1。然后将加权和与阈值 θ 进行比较,以确定 ϕ 的值,随后确定 y 的值。除此之外,这两个方程说的和前面一个完全一样。
如果这些方程看起来奇怪地熟悉,你可能会想起第一章,机器学习的味道,当我们谈论线性分类器的时候。
你是对的,麦卡洛克-皮茨神经元本质上是一个线性的二元分类器!
你可以这样想:x0和 x 1 是输入特征,w0和w1是待学习的权重,分类是通过激活功能 ϕ.进行的如果我们在学习权重方面做得很好,我们会在合适的训练集的帮助下做到这一点,我们可以将数据分为正样本或负样本。在这种情况下, ϕ(z)=θ 将作为决策边界。**
借助下图,这可能更有意义:
在左边,你可以看到神经元的激活功能 ϕ ,相对于 z 绘制。记住 z 只不过是两个输入 x 0 和 x 1 的加权和。规则是只要加权和低于某个阈值, θ ,神经元的输出为-1;在 θ 以上,输出为+1。
在右侧,您可以看到由 ϕ(z)=θ 表示的决策边界,它将数据分为两个状态, ϕ(z) < θ (其中所有数据点被预测为负样本)和 ϕ(z) > θ (其中所有数据点被预测为正样本)。
The decision boundary does not need to be vertical or horizontal, it can be tilted as shown in the preceding diagram. But in the case of a single McCulloch-Pitts neuron, the decision boundary will always be a straight line.
当然,神奇之处在于学习权重系数, w 0 和 w 1 ,使得决策边界恰好位于所有正数据点和所有负数据点之间。
为了训练神经网络,我们通常需要三样东西:
- 训练数据:得知我们需要一些数据样本来验证我们的分类器的有效性也就不足为奇了。
- 成本函数(也称为损失函数):成本函数提供了当前权重系数有多好的度量。有各种各样的成本函数可供使用,我们将在本章末尾讨论。一种解决方法是计算错误分类的数量。另一个是计算误差平方和。
- 学习规则:学习规则从数学上规定了我们如何从一次迭代到下一次迭代更新权重系数。这种学习规则通常取决于我们在训练数据上观察到的误差(由成本函数衡量)。
这就是著名研究员弗兰克·罗森布拉特的工作。
理解感知器
20 世纪 50 年代,美国心理学家和人工智能研究员弗兰克·罗森布拉特发明了一种算法,可以自动学习执行精确二进制分类所需的最佳权重系数 w 0 和 w 1 :感知器学习规则。
罗森布拉特最初的感知器算法可以总结如下:
- 将权重初始化为零或一些小随机数。
- 对于每个训练样本, s i ,执行以下步骤:
- 计算预测目标值, ŷ i 。
- 将 ŷ 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
),每个特征一个(X.shape[1]
),初始化为零。为了方便起见,我们将偏差项(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)
然后,我们将计算数据集中每个数据样本(xi
、yi
)的δw项,并重复该步骤多次迭代(self.n_iter
)。为此,我们需要将基础事实标签(yi
)与预测标签(前面提到的self.predict(xi)
)进行比较。得到的增量项将用于更新权重和偏差项:
... 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
就这样!
生成玩具数据集
在以下步骤中,您将学习如何创建和绘制玩具数据集:
- 为了测试我们的感知器分类器,我们需要创建一些模拟数据。现在让我们保持简单,生成属于两个 blob(
centers
)之一的 100 个数据样本(n_samples
),再次依赖 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,而
make_blobs
返回0
和1
。调整标签的一个简单方法是使用以下等式:
In [4]: y = 2 * y - 1
- 在下面的代码中,我们...
将感知器与数据拟合
在以下步骤中,您将学习在给定数据上拟合感知器算法:
- 我们可以实例化我们的感知器对象,类似于我们在 OpenCV 中遇到的其他分类器:
In [6]: p = Perceptron(lr=0.1, n_iter=10)
这里,我们选择了 0.1 的学习率,并告诉感知器在 10 次迭代后终止。这些值是在这一点上相当随意地选择的,尽管我们过一会儿会回到它们。
Choosing an appropriate learning rate is critical, but it's not always clear what the most appropriate choice is. The learning rate determines how quickly or slowly we move toward the optimal weight coefficients. If the learning rate is too large, we might accidentally skip the optimal solution. If it is too small, we will need a large number of iterations to converge to the best values.
- 一旦建立了感知器,我们可以调用
fit
方法来优化权重系数:
In [7]: p.fit(X, y)
- 有用吗?让我们来看看学习到的重量值:
In [8]: p.weights
Out[8]: array([ 2.20091094, -0.4798926 ])
- 别忘了偷看一下偏见这个词:
In [9]: p.bias
Out[9]: 0.20000000000000001
如果我们将这些值插入到我们的 ϕ 的等式中,很明显,感知器学习到了形式2.2 x1-0.48 x2+0.2>= 0的决策边界。
评估感知器分类器
在以下步骤中,您将根据测试数据评估经过训练的感知器:
- 为了找出我们的感知器表现有多好,我们可以计算所有数据样本的准确度分数:
In [10]: from sklearn.metrics import accuracy_score... accuracy_score(p.predict(X), y)Out[10]: 1.0
满分!
- 让我们通过回顾前面章节中的
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, ...
将感知器应用于不可线性分离的数据
在以下步骤中,您将学习构建感知器来分离非线性数据:
- 由于感知器是一个线性分类器,您可以想象它在尝试对不可线性分离的数据进行分类时会有困难。我们可以通过增加玩具数据集中两个斑点的扩散(
cluster_std
)来测试这一点,以便两个斑点开始重叠:
In [12]: X, y = make_blobs(n_samples=100, centers=2,
... cluster_std=5.2, random_state=42)
... y = 2 * y - 1
- 我们可以使用 matplotlib 的
scatter
函数再次绘制数据集:
In [13]: plt.scatter(X[:, 0], X[:, 1], s=100, c=y);
... plt.xlabel('x1')
... plt.ylabel('x2')
从下面的截图中可以明显看出,这些数据不再是线性可分的,因为没有直线可以完美地将两个斑点分开:
前面的截图显示了一个不可线性分离的数据示例。那么,如果我们将感知器分类器应用于这个数据集,会发生什么呢?
- 我们可以通过重复前面的步骤找到这个问题的答案:
In [14]: p = Perceptron(lr=0.1, n_iter=10)
... p.fit(X, y)
- 然后我们发现准确率为 81%:
In [15]: accuracy_score(p.predict(X), y)
Out[15]: 0.81000000000000005
- 为了找出哪些数据点被错误分类,我们可以使用我们的辅助函数再次可视化决策场景:
In [16]: plot_decision_boundary(p, X, y)
... plt.xlabel('x1')
... plt.ylabel('x2')
下图显示了感知器分类器的局限性。作为一个线性分类器,它试图用直线分离数据,但最终失败了。它失败的主要原因是因为数据不是线性可分的,尽管我们达到了 81%的准确率。然而,从下面的图中,很明显许多红点位于蓝色区域,反之亦然。因此,与感知器不同,我们需要一种非线性算法,它可以创建非线性(圆形)决策边界,而不是直线:
幸运的是,有办法使感知器更强大,并最终创建非线性决策边界。
理解多层感知器
为了创建非线性决策边界,我们可以组合多个感知器来形成一个更大的网络。这也被称为一个多层感知器 ( MLP )。MLPs 通常至少由三层组成,其中第一层对数据集的每个输入要素都有一个节点(或神经元),最后一层对每个类标签都有一个节点。中间的一层叫做隐藏层。
下图显示了这种前馈神经网络架构的一个示例:
在这个网络中,每个圆都是一个人工神经元(或者说,本质上是一个感知器),以及一个人工的输出...
理解梯度下降
当我们在本章前面谈到感知器时,我们确定了训练所需的三个基本要素:训练数据、成本函数和学习规则。虽然学习规则对单个感知器非常有效,但不幸的是,它不能推广到多层感知器,因此人们不得不提出一个更通用的规则。
如果你想一想我们如何衡量一个分类器的成功,我们通常是在成本函数的帮助下这样做的。一个典型的例子是网络的错误分类数或均方误差。这个函数(也称为损失函数)通常取决于我们试图调整的参数。在神经网络中,这些参数是权重系数。
让我们假设一个简单的神经网络有一个要调整的权重, w 。然后我们可以将成本可视化为重量的函数:
训练开始时,在时间零点,我们可能会从这个图左边的路开始( w t=0 )。但是从图中我们知道 w 会有一个更好的值,即 w 最优,这样会使成本函数最小化。最小的成本意味着最低的误差,所以通过学习达到 w 最优 应该是我们的最高目标。
这正是梯度下降的作用。你可以把梯度想象成一个指向山上的向量。在梯度下降中,我们试图走在梯度的对面,有效地走下山,从山峰到山谷:
一旦你到达山谷,坡度就变为零,训练就完成了。
有几种方法可以到达山谷——我们可以从左边接近,也可以从右边接近。我们下降的起点由初始重量值决定。此外,我们必须小心不要走太大的一步,否则我们可能会错过山谷:
因此,在随机梯度下降(有时也称为迭代或在线梯度下降)中,目标是采取小步骤,但尽可能频繁地采取这些步骤。有效步长由算法的学习速率决定。
具体来说,我们将反复执行以下过程:
- 向网络呈现少量训练样本(称为批量)。
- 在这一小批数据上,计算成本函数的梯度。
- 通过在梯度的相反方向朝着山谷走一小步来更新权重系数。
- 重复步骤 1-3,直到重量成本不再下降。这表明我们已经到达了山谷。
其他一些改进 SGD 的方法是使用 Keras 框架中的学习速率查找器,减少各个时期的步长(学习速率),并且如前所述,使用批量(或小批量),这将更快地计算权重更新。
你能想出一个这个程序可能失败的例子吗?
想到的一个场景是,成本函数有多个谷,一些比另一些更深,如下图所示:
如果我们从左边开始,我们应该会像以前一样到达同一个山谷——没问题。但是,如果我们的起点一直向右,我们可能会在路上遇到另一个山谷。梯度下降会把我们直接带到山谷,但它没有任何办法爬出来。
This is also known as getting stuck in a local minimum. Researchers have come up with different ways to try and avoid this issue, one of them being to add noise to the process.
拼图中还剩一块。给定我们当前的权重系数,我们如何知道成本函数的斜率?
用反向传播训练 MLPs
这就是反向传播的用武之地,它是一种用于估计神经网络中成本函数梯度的算法。有人可能会说,这基本上是链规则的一个花哨词,链规则是一种计算依赖于多个变量的函数的偏导数的方法。尽管如此,这是一种帮助人工神经网络领域起死回生的方法,所以我们应该为此感到庆幸。
理解反向传播涉及到相当多的微积分,这里我只给大家简单介绍一下。
让我们提醒自己,成本函数及其梯度取决于真实输出(yI??)和当前输出(ŷ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
现在是一个单热编码矩阵。为了将一热编码转换为对应于类别标签(零或一)的数字,我们可以使用 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 的决策边界。
瞧啊。决策边界不再是一条直线。也就是说,您获得了巨大的性能提升,并且可能预期会有更大幅度的性能提升。但是没人说我们必须停在这里!
从现在开始,我们至少可以尝试两种不同的方式:
- 我们可以在隐藏层增加更多的神经元。您可以通过用更大的值替换第 6 行的
n_hidden
并再次运行代码来实现这一点。一般来说,你放入网络的神经元越多,MLP 就越强大。 - 我们可以添加更多的隐藏层。事实证明,这才是神经网络真正发挥作用的地方。
因此,这是我应该告诉你关于深度学习的地方。
熟悉深度学习
当深度学习还没有一个花哨的名字时,它被称为人工神经网络。所以你已经知道很多了!
最终,对神经网络的兴趣在 1986 年被重新点燃,当时大卫·鲁梅尔哈特、杰弗里·辛顿和罗纳德·威廉姆斯参与了上述反向传播算法的(重新)发现和普及。然而,直到最近,计算机才变得足够强大,因此它们实际上可以在大规模网络上执行反向传播算法,从而导致深度学习研究的激增。
You can find more information on the history and origin of deep learning in the following scientific article: Wang and Raj (2017), On the Origin ...
结识喀拉斯
Keras 的核心数据结构是一个模型,类似于 OpenCV 的分类器对象,只是它只关注神经网络。最简单的模型是序列模型,它将神经网络的不同层排列成线性堆栈,就像我们在 OpenCV 中对 MLP 所做的那样:
In [1]: from keras.models import Sequential
... model = Sequential()
Out[1]: Using TensorFlow backend.
然后,可以将不同的层逐个添加到模型中。在 Keras 中,层不仅仅包含神经元,它们还执行一种功能。一些核心层类型包括:
- 密集:这是一个密集连接的层。这正是我们在设计 MLP 时使用的方法:一层神经元与前一层的每个神经元相连。
- 激活:这将激活功能应用于输出。Keras 提供了一系列的激活功能,包括 OpenCV 的识别功能(
linear
)、双曲正切(tanh
)、乙状结肠挤压功能(sigmoid
)、softmax 功能(softmax
)等等。 - 重塑:这将输出重塑为某个形状。
还有其他层对其输入进行算术或几何运算:
- 卷积层:这些层允许您指定一个内核,输入层与该内核卷积。这允许你在 1D、2D 甚至 3D 中执行诸如 Sobel 滤波器或应用高斯核的操作。
- 汇集层:这些层对其输入执行最大汇集操作,其中输出神经元的活动由最活跃的输入神经元给出。
深度学习中流行的其他一些层如下:
- 丢弃:该层在每次更新时随机将输入单位的分数设置为零。这是一种在训练过程中注入噪声的方法,使其更加健壮。
- 嵌入 g :这个层编码分类数据,类似于 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 _ square _ error):这是前面讨论过的。
- 铰链损失(铰链):这是 SVM 常用的最大余量分类器,如第六章、支持向量机检测行人所述。
您可以看到有太多的参数需要指定,有太多的方法可供选择。为了忠实于我们前面提到的感知器实现,我们将选择 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%的准确性。比我们自己的实现好得多!
You can find more information on Keras, source code documentation, and a number of tutorials at keras.io
.
有了这些工具,我们现在可以接近真实世界的数据集了!
手写数字分类
在前一节中,我们介绍了很多关于神经网络的理论,如果你是这个话题的新手,这些理论可能会有点让人不知所措。在本节中,我们将使用著名的 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
这将从亚马逊云下载数据(可能需要一段时间,取决于您的互联网连接),并自动将数据分割成训练集和测试集。
MNIST provides its own predefined train-test split. This way, it is easier to compare the performance of different classifiers because they will all use the same data for training and the same data for testing.
这些数据采用我们已经熟悉的格式:
In [2]: X_train.shape, y_train.shape
Out[2]: ((60000, 28, 28), (60000,))
我们应该注意标签是 0 到 9 之间的整数值(对应于数字 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 图像(8 x 8 像素)大得多(28 x 28 像素),因此更关注细微的细节,如失真和相同数字的图像之间的个体差异。
- MNIST 数据集比 NIST 数据集大得多,提供了 60,000 个训练样本和 10,000 个测试样本(相比之下,NIST 图像的总数为 5,620 个)。
预处理 MNIST 数据集
正如我们在第四章、中了解到的,表示数据和工程特性有许多预处理步骤,我们可能希望在这里应用:
- 居中:重要的是图像中所有的数字都居中。例如,看一下上图中数字 1 的所有示例图像,它们都是由几乎垂直的一击组成的。如果图像没有对齐,打击可能位于图像的任何地方,这使得神经网络很难在训练样本中找到共性。幸运的是,MNIST 的图像已经居中。
- 缩放:对数字进行缩放也是如此,这样它们都有相同的大小。这样,冲击、曲线和循环的位置就很重要了。...
使用 OpenCV 训练 MLP
我们可以使用以下方法在 OpenCV 中设置和训练 MLP:
- 实例化一个新的 MLP 对象:
In [9]: import cv2
... mlp = cv2.ml.ANN_MLP_create()
- 指定网络中每一层的大小。我们可以随意添加任意数量的图层,但是我们需要确保第一个图层具有与输入要素相同数量的神经元(在我们的示例中为
784
),最后一个图层具有与类标签相同数量的神经元(在我们的示例中为10
),同时有两个隐藏图层,每个图层都具有512
节点:
In [10]: mlp.setLayerSizes(np.array([784, 512, 512, 10]))
- 指定激活功能。这里我们使用之前的 sigmoidal 激活函数:
In [11]: mlp.setActivationFunction(cv2.ml.ANN_MLP_SIGMOID_SYM,
... 2.5, 1.0)
- 指定培训方法。这里,我们使用前面描述的反向传播算法。我们还需要确保选择足够小的学习率。由于我们有大约 10 个 5 个训练样本,所以最好将学习率设置为最多 10 个 -5 个:
In [12]: mlp.setTrainMethod(cv2.ml.ANN_MLP_BACKPROP)
... mlp.setBackpropWeightScale(0.00001)
- 指定终止标准。这里,我们使用与之前相同的标准:运行 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))
- 在训练集(
X_train_pre
)上训练网络:
In [14]: mlp.train(X_train_pre, cv2.ml.ROW_SAMPLE, y_train_pre)
Out[14]: True
Before you call mlp.train
, here is a word of caution: this might take several hours to run, depending on your computer setup! For comparison, it took just under an hour on my own laptop. We are now dealing with a real-world dataset of 60,000 samples: if we run 100 training epochs, we have to compute 6 million gradients! So beware.
训练完成后,我们可以计算训练集上的准确度分数,看看我们取得了多大的进步:
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%,比人类的表现还要好!这就是为什么,如今,手写数字分类的任务在很大程度上被认为已经解决。
为了更接近最先进的结果,我们需要使用最先进的技术。因此,我们回到喀拉斯。
预处理 MNIST 数据集
在以下步骤中,您将学习在数据被输入神经网络之前对其进行预处理:
- 为了确保每次运行实验都得到相同的结果,我们将为 NumPy 的随机数生成器挑选一个随机种子。这样,从 MNIST 数据集中洗牌训练样本将总是产生相同的顺序:
In [1]: import numpy as np
... np.random.seed(1337)
- 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()
In contrast to other datasets we have encountered so far, MNIST comes with a predefined train-test split. This allows the dataset to be used as a benchmark, as the test score reported by different algorithms will always apply to the same test samples.
- 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)
- 在这里,我们将特征矩阵重塑为四维矩阵,尺寸为
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
- 然后,我们可以像以前一样对训练标签进行一次性编码。这将确保每一类目标标签都可以分配给输出层中的一个神经元。我们可以用 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)
创建卷积神经网络
在以下步骤中,您将创建一个神经网络,并对之前预处理的数据进行训练:
- 一旦我们对数据进行了预处理,就该定义实际的模型了。这里,我们将再次依靠
Sequential
模型来定义前馈神经网络:
In [5]: from keras.model import Sequential... model = Sequential()
- 然而,这一次,我们将对单个层更聪明。我们将围绕卷积层设计我们的神经网络,其中核心是一个 3×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 分类器截然不同。这只是做事的一种方式。正如你所看到的,神经网络提供了过多的调谐参数,而且根本不清楚哪一个会导致最好的性能。
摘要
在这一章中,作为一名机器学习实践者,我们在列表中添加了一大堆技能。我们不仅涵盖了人工神经网络的基础知识,包括感知器和 MLPs,我们还获得了一些先进的深度学习软件。我们学习了如何从头开始构建一个简单的感知器,以及如何使用 Keras 构建最先进的网络。此外,我们了解了神经网络的所有细节:激活函数、损失函数、层类型和训练方法。总而言之,这可能是迄今为止最密集的一章。
现在你已经知道了大多数基本的监督学习者,是时候谈谈如何将不同的算法组合成一个更强大的算法了。因此,在下一章中,我们将讨论如何构建集成分类器。
十、分类的集成方法
到目前为止,我们已经研究了许多有趣的机器学习算法,从线性回归等经典方法到深度神经网络等更先进的技术。在不同的地方,我们指出每种算法都有自己的优点和缺点——我们注意到了如何发现和克服这些缺点。
然而,如果我们可以简单地将一堆平均分类器堆叠在一起,形成一个更强大的分类器集成,那不是很棒吗?
在本章中,我们将这样做。集成方法是将多个不同的模型绑定在一起以解决一个共享问题的技术。它们的使用已经成为竞争性机器学习的一种常见做法...
技术要求
可以从以下链接查阅本章代码:github . com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/chapter 10
。
以下是软件和硬件要求的简短总结:
- OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以正常工作)。
- Python 3.6 版本(任何 Python 3 . x 版本都可以)。
- Anaconda Python 3,用于安装 Python 和所需的模块。
- 这本书可以使用任何操作系统——苹果操作系统、视窗操作系统或基于 Linux 的操作系统。我们建议您的系统中至少有 4 GB 内存。
- 你不需要一个图形处理器来运行书中提供的代码。
理解集成方法
集成方法的目标是结合用给定学习算法构建的几个独立估计器的预测,以解决一个共享问题。通常,一个合奏由两个主要部分组成:
- 一套模型
- 一组决策规则,用于控制如何将这些模型的结果组合成单个输出
The idea behind ensemble methods has much to do with the wisdom of the crowd concept. Rather than the opinion of a single expert, we consider the collective opinion of a group of individuals. In the context of machine learning, these individuals would be classifiers or regressors. The idea is that if we just ask a large enough number of classifiers, one of them ought to get ...
理解平均系综
平均方法在机器学习中有很长的历史,通常应用于分子动力学和音频信号处理等领域。这种集合通常被视为给定系统的精确复制品。
平均集合本质上是在同一数据集上训练的模型的集合。然后以多种方式汇总他们的结果。
一种常见的方法是创建多个模型配置,这些配置采用不同的参数子集作为输入。采用这种方法的技术统称为装袋方法。
装袋方法有许多不同的口味。但是,它们的不同之处通常在于它们绘制训练集随机子集的方式:
- 粘贴方法绘制样本的随机子集,而不替换数据样本。
- Bagging 方法通过替换数据样本来抽取样本的随机子集。
- 随机子空间方法绘制特征的随机子集,但在所有数据样本上进行训练。
- 随机面片方法绘制样本和特征的随机子集。
Averaging ensembles can be used to reduce the variability of a model's performance.
在 scikit-learn 中,打包方法可以使用BaggingClassifier
和BaggingRegressor
元估计器来实现。这些是元估计器,因为它们允许我们从任何其他基础估计器构建一个集合。
实现打包分类器
例如,我们可以从 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
类来形成回归器的集合。
例如,我们可以构建一个决策树集合,从第三章、的波士顿数据集预测房价,这是监督学习的第一步。
在以下步骤中,您将学习如何使用 bagging 回归器来形成回归器的集合:
- 语法几乎与设置 bagging 分类器相同:
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)
- 当然,我们需要像对乳腺癌数据集那样加载和拆分数据集:
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
... )
- 然后,我们可以在
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_samples
和max_features
参数允许大量定制。
A more sophisticated version of bagged decision trees is called random forests, which we will talk about later in this chapter.
理解增强合奏
另一种构建合奏的方法是通过增强。增强模型按顺序使用多个个体学习者来迭代地增强集成的性能。
通常,用于增强的学习者相对简单。一个很好的例子是只有一个节点的决策树——一个决策树桩。另一个例子可以是简单的线性回归模型。我们的想法不是拥有最强的个人学习者,而是相反的——我们希望个人成为弱学习者,这样我们只有在考虑大量个人时才能获得优异的表现。
在该过程的每次迭代中,训练集被调整,使得下一个分类器被应用于...
弱学习者
弱学习者是与实际分类只有轻微关联的分类器;它们可能比随机预测要好一些。相反,强有力的学习者与正确的分类任意相关。
这里的想法是,你不能只使用一个,而是使用一组广泛的弱学习者,每一个都比随机的稍好。弱学习者的许多实例可以使用 boosting、bagging 等集合在一起,以创建强集成分类器。好处是最终的分类器不会导致在你的训练数据上过度拟合。
例如,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
时会发生什么?通过使用可选参数,我们可以做更多的事情。
如您所见,boosting 是一个强大的过程,它允许您通过组合大量相对简单的学习者来获得巨大的性能提升。
A specific implementation of boosted decision trees is the AdaBoost algorithm, which we will talk about later in this chapter.
理解堆叠集合
到目前为止,我们看到的所有集成方法都有一个共同的设计理念:将多个单独的分类器与数据相匹配,并借助一些简单的决策规则(如平均或提升)将其预测合并到最终预测中。
另一方面,堆叠系综构建了具有层次的系综。这里,个体学习者被组织成多个层,其中一层学习者的输出被用作下一层模型的训练数据。这样,就有可能成功地融合数百种不同的模式。
不幸的是,详细讨论堆叠集合超出了本书的范围。
然而,正如所见,这些模型可能非常强大,...
将决策树组合成随机森林
袋装决策树的一种流行变体是所谓的随机森林。这些本质上是决策树的集合,其中每个树与其他树略有不同。与袋装决策树不同,随机森林中的每棵树都是在稍微不同的数据特征子集上训练的。
尽管单个无限深度的树可能在预测数据方面做得相对较好,但它也容易过度拟合。随机森林背后的想法是建立大量的树,每个树都在数据样本和特征的随机子集上训练。由于过程的随机性,森林中的每棵树都会以稍微不同的方式对数据进行过度填充。过度拟合的影响可以通过对单棵树的预测取平均值来降低。
理解决策树的缺点
决策树经常遭受数据集过拟合的影响,通过一个简单的例子可以最好地证明这一点。
为此,我们将从 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
:设置算法的终止条件。这也是您设置森林中树木数量的地方。
Although we might have hoped for a setNumTrees
method to set the number of trees in the forest (kind of the most important parameter of them all, no?), we instead need to rely on the setTermCriteria
method. Confusingly, the number of trees is conflated with cv2.TERM_CRITERA_MAX_ITER
, which is usually reserved for the number of iterations that an algorithm is run for, not for the number of estimators in an ensemble.
我们可以通过向setTermCriteria
方法传递一个整数n_trees
来指定森林中的树木数量。在这里,我们还想告诉算法,一旦分数从一次迭代到下一次迭代至少没有增加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'
实现基尼杂质,设置criterion='entropy'
实现信息增益。max_features
:指定每个节点分割时要考虑的特征数量(或分数)。max_depth
:指定每棵树的最大深度。min_samples
:指定最小数量...
实现极度随机化的树
随机森林已经相当随意了。但是如果我们想把随机性发挥到极致呢?
在极其随机的树中(见ExtraTreesClassifier
和ExtraTreesRegressor
类),随机性甚至比随机森林更进一步。还记得决策树通常如何为每个特征选择一个阈值,以使节点分裂的纯度最大化吗?另一方面,极度随机化的树会随机选择这些阈值。然后,这些随机生成的阈值中的最佳阈值被用作拆分规则。
我们可以构建一个极其随机的树,如下所示:
In [16]: from sklearn.ensemble import ExtraTreesClassifier
... extra_tree = ExtraTreesClassifier(n_estimators=10, random_state=100)
为了说明单个决策树、随机森林和极随机树之间的区别,让我们考虑一个简单的数据集,例如 Iris 数据集:
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')
结果是这样的:
现在三个分类器之间的区别变得更加清晰。我们看到单棵树绘制了迄今为止最简单的决策边界,使用水平决策边界分割景观。随机森林能够更清楚地将决策图左下角的数据点云分开。然而,只有极其随机的树能够从四面八方将数据点云转向景观中心。
现在我们已经知道了树集合的所有不同变体,让我们继续看真实世界的数据集。
使用随机森林进行人脸识别
我们还没怎么讨论的一个流行数据集是奥利韦蒂人脸数据集。
奥利维蒂人脸数据集是剑桥美国电话电报公司实验室在 1990 年收集的。该数据集包括 40 个不同受试者在不同时间和不同光照条件下拍摄的面部图像。此外,受试者的面部表情(睁开/闭上眼睛、微笑/不微笑)和面部细节(戴眼镜/不戴眼镜)各不相同。
然后,图像被量化为 256 个灰度级,并存储为无符号 8 位整数。因为有 40 个不同的主题,所以数据集带有 40 个不同的目标标签。因此,识别人脸构成了多类分类任务的一个例子。
正在加载数据集
像许多其他经典数据集一样,可以使用 scikit-learn 加载 Olivetti 人脸数据集:
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
这意味着,虽然我们允许树上升到深度 1000,但最终只需要 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
还不错!我们可以玩可选的参数,看看我们是否会变得更好。最重要的似乎是森林中的树木数量。我们可以用一个由 1000 棵树而不是 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%的准确率!
Here, we tried to improve the performance of our model through creative trial and error: we varied some of the parameters we deemed important and observed the resulting change in performance until we found a configuration that satisfied our expectations. We will learn more sophisticated techniques for improving a model in Chapter 11, Selecting the Right Model with Hyperparameter Tuning.
决策树集成的另一个有趣的用例是 AdaBoost。
实现 AdaBoost
当森林中的树是深度为 1 的树(也称为决策树桩)并且我们执行助推而不是装袋时,得到的算法被称为 AdaBoost 。
AdaBoost 通过执行以下操作在每次迭代时调整数据集:
- 选择决策树桩
- 增加决策树桩标注不正确的案例的权重,同时减少标注正确的案例的权重
这种迭代权重调整使得集成中的每个新分类器优先训练错误标记的案例。因此,该模型通过瞄准高度加权的数据点进行调整。
最终,树桩被组合成最终的分类器。
在 OpenCV 中实现 AdaBoost
虽然 OpenCV 提供了非常高效的 AdaBoost 实现,但它隐藏在哈尔级联分类器下。哈尔级联分类器是非常流行的人脸检测工具,我们可以通过 Lena 图像的例子来说明:
In [1]: img_bgr = cv2.imread('data/lena.jpg', cv2.IMREAD_COLOR)
... img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
在加载彩色和灰度图像后,我们加载一个预处理的哈尔级联:
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... )
然后,使用熟悉的程序fit
和score
AdaBoost:
In [9]: ada.fit(X_train, y_train)... ada.score(X_test, y_test)
将不同的模型组合成投票分类器
到目前为止,我们已经看到了如何将同一个分类器或回归器的不同实例组合成一个集合。在这一章中,我们将把这个想法更进一步,将概念上不同的分类器组合成一个被称为投票分类器。
投票分类器背后的想法是,集成中的单个学习者不一定需要属于同一类型。毕竟,不管单个分类器如何得出它们的预测,最终,我们将应用一个决策规则,该规则集成了单个分类器的所有投票。这也被称为投票方案。
了解不同的投票方案
两种不同的投票方案在投票分类器中很常见:
- 在硬投票(也称多数投票)中,每个个体分类器为一个类投票,多数获胜。在统计学术语中,集合的预测目标标签是单独预测标签的分布模式。
- 在软投票中,每个单独的分类器提供特定数据点属于特定目标类的概率值。预测通过分类器的重要性进行加权,并进行汇总。然后,加权概率之和最大的目标标签赢得投票。
例如,让我们假设集合中有三个不同的分类器执行...
实现投票分类器
让我们看一个投票分类器的简单例子,它结合了三种不同的算法:
- 来自第三章的逻辑回归分类器:监督学习的第一步
- 来自第七章、的高斯朴素贝叶斯分类器利用贝叶斯学习实现垃圾邮件过滤器
- 本章中的随机森林分类器
我们可以将这三种算法组合成投票分类器,并通过以下步骤将其应用于乳腺癌数据集:
- 加载数据集,并将其拆分为训练集和测试集:
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)
- 实例化各个分类器:
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)
- 将单个分类器分配给投票集合。这里,我们需要传递一个元组列表(
estimators
),其中每个元组由分类器的名称(描述每个分类器的简称的一串字母)和模型对象组成。投票方案可以是voting='hard'
也可以是voting='soft'
。目前,我们将选择**??:**
In [6]: from sklearn.ensemble import VotingClassifier
... vote = VotingClassifier(estimators=[('lr', model1),
... ('gnb', model2),('rfc', model3)],voting='hard')
- 将集合与训练数据相匹配,并在测试数据上进行评分:
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 的数字)。每个模型将根据它获得的最高概率选择一个类。最后,得票最多的班级将被选中。这就是多元的概念。在实践中,多元化试图给 ?? 和天真带来好处...
摘要
在本章中,我们讨论了如何通过将各种分类器组合成一个集成来改进它们。我们讨论了如何使用 bagging 对不同分类器的预测进行平均,以及如何使用 boosting 让不同的分类器纠正彼此的错误。我们花了很多时间来讨论组合决策树的所有可能方法,无论是决策树桩(AdaBoost)、随机森林还是极随机树。最后,我们学习了如何通过构建投票分类器来组合集成中不同类型的分类器。
在下一章中,我们将更多地讨论如何通过深入模型选择和超参数调整的世界来比较不同分类器的结果。
十一、使用超参数调整选择正确的模型
既然我们已经探索了各种各样的机器学习算法,我相信你已经意识到它们中的大多数都有大量的设置可供选择。这些设置或调谐旋钮,即所谓的超参数,帮助我们在试图最大化性能时控制算法的行为。
例如,我们可能想要选择决策树中的深度或分裂标准,或者调整神经网络中的神经元数量。寻找模型的重要参数值是一项棘手的任务,但对于几乎所有模型和数据集都是必要的。
在本章中,我们将深入探讨模型评估和超参数调整。假设我们有两个不同的...
技术要求
可以从以下链接查阅本章代码:github . com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/chapter 11
。
以下是软件和硬件要求的总结:
- 您将需要 OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以)。
- 您将需要 Python 3.6 版本(任何 Python 3 . x 版本都可以)。
- 您将需要 Anaconda Python 3 来安装 Python 和所需的模块。
- 除了这本书,你可以使用任何操作系统——苹果操作系统、视窗操作系统和基于 Linux 的操作系统。我们建议您的系统中至少有 4 GB 内存。
- 你不需要一个图形处理器来运行本书提供的代码。
评估模型
模型评估策略有许多不同的形式和形状。因此,在接下来的章节中,我们将重点介绍三种最常用的相互比较模型的技术:
- 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 )并计算其精度:
- 在 OpenCV 中,kNN 实例化如下:
In [3]: import cv2
... knn = cv2.ml.KNearest_create()
... knn.setDefaultK(1)
- 然后,我们训练模型,并使用它来预测我们已经知道的数据的标签:
In [4]: knn.train(X, cv2.ml.ROW_SAMPLE, y)
... _, y_hat = knn.predict(X)
- 最后,我们计算正确标记点的分数:
In [5]: from sklearn.metrics import accuracy_score
... accuracy_score(y, y_hat)
Out[5]: 1.0
我们可以看到,准确率得分为1.0
,说明我们的模型 100%正确标注了点。
If a model gets 100% accuracy on the training set, we say the model memorized the data.
但是预期的准确性真的被测量了吗?我们是否已经提出了一个我们期望 100%正确的模型?
正如你可能已经收集到的,答案是否定的。这个例子表明,即使是一个简单的算法也能够记忆真实世界的数据集。想象一下,对于一个深度神经网络来说,这个任务是多么容易!通常,模型的参数越多,它就越强大。我们将很快回到这个问题。
以正确的方式评估模型
使用所谓的测试集可以更好地了解模型的性能,但是你已经知道这一点了。当展示了从训练过程中获得的数据时,我们可以检查一个模型是否已经学习了数据中的一些相关性,或者它是否已经记住了训练集。
我们可以使用 scikit-learn 的model_selection
模块中熟悉的train_test_split
将数据分成训练集和测试集:
In [6]: from sklearn.model_selection import train_test_split
但是我们如何选择正确的列车测试比率呢?甚至有正确比率这种东西吗?还是认为这是模型的另一个超参数?
这里有两个相互矛盾的问题:
- 如果我们的...
选择最佳型号
当一个模型表现不佳时,通常不清楚如何让它变得更好。在这本书里,我宣布了一个拇指规则,例如,如何选择神经网络的层数。更糟糕的是,答案往往是反直觉的!例如,向网络中添加另一层可能会使结果更糟,而添加更多的训练数据可能根本不会改变性能。
你可以看到为什么这些问题是机器学习最重要的方面。归根结底,确定哪些步骤将改进或不改进我们的模型的能力是成功的机器学习实践者与众不同的地方。
我们来看一个具体的例子。还记得第五章、使用决策树进行医疗诊断吗,我们在回归任务中使用决策树?我们将两个不同的树拟合到一个 sin 函数中——一个深度为 2,另一个深度为 5。提醒一下,回归结果如下所示:
应该清楚的是,这两种配合都不是特别好。然而,这两个决策树以两种不同的方式失败!
深度为 2 的决策树(上一张截图中的粗线)试图拟合数据中的四条直线。因为数据本质上比几条直线更复杂,这个模型失败了。我们可以尽可能多地训练它,尽可能多地生成训练样本——它永远无法很好地描述这个数据集。这种模型被称为数据不足。换句话说,模型没有足够的复杂性来考虑数据中的所有特征。因此,该模型具有很高的偏差。
另一个决策树(细线,深度 5)犯了不同的错误。该模型具有足够的灵活性,几乎可以完美地解释数据中的精细结构。然而,在某些点上,模型似乎遵循噪声的特定模式;我们增加了原罪函数而不是原罪函数本身。你可以在图的右边看到,蓝色曲线(细线)会抖动很多。据说这样的模型会使数据过度膨胀。换句话说,这个模型太复杂了,以至于最终要考虑数据中的随机误差。因此,该模型具有较高的方差。
长话短说——秘密就在这里:从根本上说,选择正确的模型归结为在偏见和差异之间找到一个平衡点。
The amount of flexibility a model has (also known as the model complexity) is mostly dictated by its hyperparameters. That is why it is so important to tune them!
让我们回到 kNN 算法和 Iris 数据集。如果我们针对所有可能的值 k 重复将模型拟合到 Iris 数据的过程,并计算训练和测试分数,我们将期望结果如下所示:
上图显示了作为模型复杂性函数的模型得分。如果这一章有什么我想让你记住的,那就是这个图表。让我们打开它。
该图将模型得分(训练或测试得分)描述为模型复杂性的函数。如前图所述,神经网络的模型复杂性大致随着网络中神经元的数量而增长。在 kNN 的情况下,适用相反的逻辑——值 k 越大,决策边界越平滑,因此复杂度越低。换句话说, k=1 的 kNN 将在前面的图中一直向右,此时训练分数是完美的。难怪我们在训练集上获得了 100%的准确率!
从上图中,我们可以看出在模型复杂性领域有三种状态:
- 训练数据下的模型复杂度非常低(高偏差模型)。在这种情况下,无论我们训练了多长时间,模型在训练集和测试集上都只能获得很低的分数。
- 一个具有很高复杂度(或高方差)的模型会过度训练数据,这表明该模型可以很好地预测训练数据,但不能预测未知数据。在这种情况下,模型开始学习仅出现在训练数据中的复杂性或特殊性。因为这些特性不适用于看不见的数据,所以训练分数越来越低。
- 对于某些中间值,测试分数最大。我们试图找到的正是这种中间状态,即考试分数最高的状态。这是偏见和差异之间权衡的最佳点!
这意味着我们可以通过绘制模型复杂性图来为手头的任务找到最佳算法。具体来说,我们可以使用以下指标来了解我们目前所处的状态:
- 如果训练和测试分数都低于我们的预期,我们可能处于上图中最左边的状态,此时模型对数据的拟合不足。在这种情况下,一个好主意可能是增加模型的复杂性,然后再试一次。
- 如果训练分数比测试分数高得多,那么我们可能处于上图中最右边的状态,此时模型正在过度拟合数据。在这种情况下,一个好主意可能是降低模型的复杂性,然后再试一次。
虽然这个过程在总体上是可行的,但是有更复杂的模型评估策略,比简单的训练-测试分割更彻底,这一点我们将在后面的章节中讨论。
理解交叉验证
交叉验证是一种评估模型泛化性能的方法,通常比将数据集拆分为训练集和测试集更稳定和彻底。
交叉验证最常用的版本是 k 倍交叉验证,其中 k 是用户指定的数字(通常是 5 或 10)。这里,数据集被分割成大小大致相等的 k 部分,称为折叠。对于包含 N 数据点的数据集,每个折叠应该具有大约 N / k 个样本。然后,在数据上训练一系列模型,使用 k - 1 折叠进行训练,剩余一个折叠进行测试。对 k 迭代重复该过程,每次选择不同的折叠...
在 OpenCV 中手动实现交叉验证
在 OpenCV 中执行交叉验证最简单的方法是手工进行数据拆分。
例如,为了实现双重交叉验证,我们将执行以下过程:
- 加载数据集:
In [1]: from sklearn.datasets import load_iris
... import numpy as np
... iris = load_iris()
... X = iris.data.astype(np.float32)
... y = iris.target
- 将数据分成大小相等的两部分:
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
... )
- 实例化分类器:
In [3]: import cv2
... knn = cv2.ml.KNearest_create()
... knn.setDefaultK(1)
- 在第一个折叠上训练分类器,然后预测第二个折叠的标签:
In [4]: knn.train(X_fold1, cv2.ml.ROW_SAMPLE, y_fold1)
... _, y_hat_fold2 = knn.predict(X_fold2)
- 在第二个折叠上训练分类器,然后预测第一个折叠的标签:
In [5]: knn.train(X_fold2, cv2.ml.ROW_SAMPLE, y_fold2)
... _, y_hat_fold1 = knn.predict(X_fold1)
- 计算两次折叠的准确度分数:
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 中,交叉验证可以分三步进行:
- 加载数据集。既然我们之前已经这么做了,就不用再做了。
- 实例化分类器:
In [8]: from sklearn.neighbors import KNeighborsClassifier ... model = KNeighborsClassifier(n_neighbors=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
是 1(1
)和 0(0
)的列表,它们分别对应于正确和不正确的分类:
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)
我们可以看到这个评分方案返回了非常类似于五重交叉验证的结果。
You can learn more about other useful cross-validation procedures at scikit-learn.org/stable/modules/cross_validation.html
.
使用自举估计鲁棒性
k 倍交叉验证的另一个程序是引导。
引导不是将数据拆分成折叠,而是通过从数据集中随机抽取样本来构建训练集。通常,通过抽取替换样本来形成引导。想象一下,把所有的数据点放进一个袋子里,然后从袋子里随机抽取。抽取样本后,我们会把它放回袋子里。这允许一些样本在训练集中出现多次,这是交叉验证所不允许的。
然后在不属于引导程序的所有样本上测试分类器(所谓的袋外例子),并且该过程被重复大量次...
在 OpenCV 中手动实现引导
引导可以通过以下过程实现:
- 加载数据集。既然我们之前已经这么做了,就不用再做了。
- 实例化分类器:
In [15]: knn = cv2.ml.KNearest_create()
... knn.setDefaultK(1)
- 从我们的带有 N 样本的数据集中,随机选择带有替换的 N 样本,形成一个自举。这可以通过 NumPy 的
random
模块中的choice
功能最容易地完成。我们告诉函数用替换(replace=True
)在[0, len(X)-1]
范围内抽取len(X)
样本。然后,该函数返回一个索引列表,我们从该列表中构建引导数据库:
In [16]: idx_boot = np.random.choice(len(X), size=len(X),
... replace=True)
... X_boot = X[idx_boot, :]
... y_boot = y[idx_boot]
- 将自举中未显示的所有样本放入袋外样品组:
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]
- 在引导样本上训练分类器:
In [18]: knn.train(X_train, cv2.ml.ROW_SAMPLE, y_boot)
Out[18]: True
- 在袋外样品上测试分类器:
In [19]: _, y_hat = knn.predict(X_oob)
... accuracy_score(y_oob, y_hat)
Out[19]: 0.9285714285714286
- 重复步骤 3-6 特定的迭代次数。
- 自举的迭代。重复这些步骤多达 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%之间。为了更可靠地估计模型的性能,我们重复该过程 1000 次,并计算所得分数的平均值和标准偏差:
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 最终得到了一个有益的火车测试分割,这样模型在分类时就没有问题了...
实施学生测验
最著名的统计测试之一是学生 t-test 。你可能以前听说过它:它允许我们确定两组数据是否有明显的不同。这对威廉·希利·戈塞来说是一个非常重要的测试,他是这项测试的发明者,在吉尼斯啤酒厂工作,想知道两批黑啤的质量是否不同。
Note that "Student" here is capitalized. Although Gosset wasn't allowed to publish his test due to company policy, he did so anyway under his pen name, Student.
在实践中,t 检验允许我们确定两个数据样本是否来自具有相同均值或期望值的基础分布。
出于我们的目的,这意味着我们可以使用 t 检验来确定两个独立分类器的测试分数是否具有相同的平均值。我们从假设两组考试成绩相同开始。我们称之为零假设,因为这是我们想要废掉的假设,也就是说,我们正在寻找证据拒绝假设,因为我们想要确保一个分类器明显优于另一个。
我们接受或拒绝基于 t 检验返回的参数 p 值的无效假设。p 值取0
和1
之间的值。0.05
的 p 值意味着零假设 100 次中只有 5 次是正确的。因此,一个小的 p 值表明有力的证据表明该假设可以被安全地拒绝。习惯上使用 p=0.05 作为截止值,低于该值我们拒绝零假设。
如果这太令人困惑,可以这样想:当我们运行 t-test 来比较分类器测试分数时,我们希望获得一个小的 p 值,因为这意味着两个分类器给出的结果明显不同。
我们可以从stats
模块用 SciPy 的ttest_ind
功能实现学生的 t-test:
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 分类器:
- 获取模型 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)
- 获取模型 B 的一组测试分数,让我们选择模型 B 作为 k=3 的 kNN 分类器:
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)
- 将 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 ,所以我们期望这两个分类器 100 次中有 77 次是等价的。
实施麦克内尔的测试
更先进的统计技术是麦克内尔的测试。该测试可用于配对数据,以确定两个样本之间是否有任何差异。与 t 检验的情况一样,我们可以使用 McNemar 的检验来确定两个模型是否给出了显著不同的分类结果。
McNemar 的测试对成对的数据点进行操作。这意味着我们需要知道,对于两个分类器,它们是如何对每个数据点进行分类的。根据第一个分类器正确但第二个分类器错误的数据点数量,以及反之,我们可以确定两个分类器是否等价。
让我们假设前面的模型 A 和模型 B 应用于相同的五个数据点。而模型...
用网格搜索调整超参数
超参数调整最常用的工具是网格搜索,这基本上是一个花哨的术语,表示我们将使用for
循环尝试所有可能的参数组合。
让我们看看这是如何在实践中做到的。
实现简单的网格搜索
回到我们的 kNN 分类器,我们发现我们只有一个超参数可以调整: k 。通常,您会有大量的开放参数需要处理,但是 kNN 算法对于我们来说足够简单,可以手动实现网格搜索。
在开始之前,我们需要像以前一样将数据集分割成训练集和测试集:
- 这里我们选择 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... )
- 然后,目标是循环所有可能的 k 值。当我们这样做的时候,我们希望保持...
理解验证集的价值
按照我们将数据分成训练集和测试集的最佳实践,我们可能会告诉人们,我们已经找到了一个在数据集上以 97.4%的准确率运行的模型。然而,我们的结果不一定能推广到新的数据。这个论点与本书前面我们保证训练-测试分离时的论点相同,即我们需要一个独立的数据集进行评估。
然而,当我们在最后一节实现网格搜索时,我们使用测试集来评估网格搜索的结果,并更新超参数 k 。这意味着我们不能再使用测试集来评估最终数据了!基于测试集准确性做出的任何模型选择都会将信息从测试集泄露到模型中。
解决该数据的一种方法是再次分割数据,并引入所谓的验证集。验证集不同于训练集和测试集,专门用于选择模型的最佳参数。对这个验证集进行所有的探索性分析和模型选择,并保留一个单独的测试集,只用于最终评估,这是一个很好的做法。
换句话说,我们最终应该将数据分成三个不同的集合:
- 一个训练集,用来建立模型
- 一个验证集,用于选择模型的参数
- 一个测试集,用于评估最终模型的性能
下图说明了这种三向分割:
上图显示了如何将数据集拆分为训练集、验证集和测试集的示例。实际上,三向分割分两步实现:
- 将数据分成两部分:一部分包含训练和验证集,另一部分包含测试集:
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)
- 再次将
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=7 ( best_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%准确率高,但还是很不错的分数!
将网格搜索与交叉验证相结合
我们刚刚实现的网格搜索的一个潜在危险是,结果可能对我们如何精确地分割数据相对敏感。毕竟,我们可能无意中选择了一个分割,将大部分易于分类的数据点放入测试集中,导致得分过于乐观。虽然一开始我们会很高兴,但当我们在一些新的数据上尝试该模型时,我们会发现分类器的实际性能远低于预期。
相反,我们可以将网格搜索和交叉验证结合起来。这样,数据被多次分割成训练集和验证集,并在网格搜索的每一步执行交叉验证以进行评估...
将网格搜索与嵌套交叉验证相结合
尽管带有交叉验证的网格搜索使得模型选择过程更加健壮,但是您可能已经注意到,我们仍然只执行了一次分割成训练集和验证集的操作。因此,我们的结果可能仍然过于依赖于数据的精确训练-验证分割。
我们可以更进一步,使用多个分割进行交叉验证,而不是将数据分割成训练集和验证集。这将导致所谓的嵌套交叉验证,过程如下图所示:
在嵌套交叉验证中,网格搜索框上有一个外部循环,重复地将数据分割成训练集和验证集。对于这些拆分中的每一个,将运行网格搜索,这将报告一组最佳参数值。然后,对于每个外部分割,我们使用最佳设置获得测试分数。
Running a grid search over many parameters and on large datasets can be computationally intensive. A particular parameter setting on a particular cross-validation split can be done completely independently from the other parameter settings and models. Hence, parallelization over multiple CPU cores or a cluster is very important for grid search and cross-validation.
现在我们知道了如何找到模型的最佳参数,让我们更仔细地看看我们可以用来给模型评分的不同评估指标。
使用不同评估指标的评分模型
到目前为止,我们已经使用准确性(正确分类样本的比例)评估了分类性能,使用 R 2 评估了回归性能。然而,这只是总结监督模型在给定数据集上表现如何的众多可能方法中的两种。实际上,这些评估指标可能不适合我们的应用,在模型之间进行选择和调整参数时,选择正确的指标非常重要。
选择指标时,我们应该始终牢记机器学习应用程序的最终目标。在实践中,我们通常不仅对做出准确的预测感兴趣,而且对将这些预测作为更大的...
选择正确的分类标准
我们在第三章、监督学习的第一步中讨论了几个基本的评分函数。最基本的分类指标如下:
- 准确性:这将计算测试集中已被正确预测的数据点的数量,并将该数量作为测试集大小的一部分(
sklearn.metrics.accuracy_score
)返回。这是分类器最基本的评分函数,我们在整本书中都广泛使用了它。 - 精度:这描述了分类器不将阳性样本标记为阴性(
sklearn.metrics.precision_score
)的能力。 - 回忆 (或灵敏度):这描述了分类器检索所有阳性样本的能力(
sklearn.metrics.recall_score
)。
虽然精度和召回率是重要的衡量标准,但只看其中一项并不能让我们对全局有一个很好的了解。总结这两种度量的一种方法是 f-score 或 f-measure ( sklearn.metrics.f1_score
),它将精度和召回率的调和平均值计算为 2(精度 x 召回率)/(精度+召回率)。
有时我们需要做的不仅仅是最大限度地提高准确性。例如,如果我们在商业应用中使用机器学习,那么决策应该由业务目标驱动。这些目标之一可能是保证至少 90%的召回率。接下来的挑战是开发一个在满足所有次要需求的同时仍然具有合理精度的模型。像这样设定目标,通常叫做设定操作点。
然而,在开发新系统时,通常不清楚操作点应该是什么。为了更好地理解这个问题,重要的是调查所有可能的精度权衡,并立即召回它们。这可以通过一种叫做精确-回忆曲线 ( sklearn.metrics.precision_recall_curve
)的工具来实现。
Another commonly used tool to analyze the behavior of classifiers is the Receiver Operating Characteristic (ROC) curve. The ROC curve considers all possible thresholds for a given classifier similar to the precision-recall curve, but it shows the false positive rate against the true positive rate instead of reporting precision and recall.
选择正确的回归度量
回归的评估可以像分类一样详细地完成。在第三章、监督学习的第一步中,我们也谈到了回归的一些基本度量:
- 均方误差:回归问题最常用的误差度量是测量训练集中每个数据点的预测值和真实目标值之间的平方误差,在所有数据点上取平均值(
sklearn.metrics.mean_squared_error
)。 - 解释方差:一个更复杂的度量是衡量一个模型可以在多大程度上解释测试数据的变化或分散(
sklearn.metrics.explained_variance_score
)。通常,解释的差异量是通过使用...
将算法链接在一起以形成流水线
到目前为止,我们讨论的大多数机器学习问题至少包括一个预处理步骤和一个分类步骤。问题越复杂,这个加工链可能会越长。将多个处理步骤粘合在一起,甚至在网格搜索中使用它们的一种方便方法是使用 scikit-learn 中的Pipeline
类。
在 scikit-learn 中实现管道
Pipeline
类本身有一个fit
、predict
和一个score
方法,它们的行为就像 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
。但是,在指定参数网格时,会有细微的变化。我们需要为每个参数指定它属于管道的哪个步骤。我们要调整的两个参数C
和gamma
,都是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
方法(除了最后一步)。这允许流水线中的估计器产生数据的新表示,这又可以用作下一步的输入。
The Pipeline
class is not restricted to preprocessing and classification but can, in fact, join any number of estimators together. For example, we could build a pipeline containing feature extraction, feature selection, scaling, and classification, for a total of four steps. Similarly, the last step could be regression or clustering instead of classification.
摘要
在本章中,我们试图通过讨论模型选择和超参数调整的最佳实践来补充我们现有的机器学习技能。您已经学习了如何在 OpenCV 和 scikit-learn 中使用网格搜索和交叉验证来调整模型的超参数。我们还讨论了各种各样的评估指标,以及如何将算法链接到管道中。现在,你几乎准备好开始自己解决一些现实世界的问题了。
在下一章中,将向您介绍一个激动人心的新主题,即 OpenVINO toolkit,它是 OpenCV 4.0 的关键版本之一。
十二、将 OpenVINO 与 OpenCV 一起使用
在第一章中,我们讨论了 OpenCV 4.0 版本中的各种新增功能。需要注意的关键版本之一是 OpenVINO 工具包。有趣的是,OpenVINO 工具包还被嵌入式视觉联盟选为 2019 年度开发者工具。
在本章中,我们将只关注如何在 OpenCV 中使用 OpenVINO 工具包。我们将从安装 OpenVINO 工具包开始,然后使用它进行交互式人脸检测演示。我们还将学习使用带有 OpenCV 的 OpenVINO Model Zoo 和带有 OpenCV 的 OpenVINO 推理机 ( IE )。在本章的最后,我们还将学习如何使用 OpenCV 和 OpenVINO IE 进行图像分类。
在本章中,我们将涵盖以下主题:
- OpenVINO 工具包安装
- 交互式人脸检测演示
- 用 OpenCV 使用 OpenVINO 模型动物园
- 使用 OpenVINO IE 搭配 OpenCV
- 基于 OpenCV 和 OpenVINO IE 的图像分类
技术要求
您可以在以下链接查阅本章的代码:github . com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/chapter 12
。
以下是软件和硬件要求的总结:
- 您将需要 OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以)。
- 您将需要 Python 3.6 版本(任何 Python 3 . x 版本都可以)。
- 您将需要 Anaconda Python 3 来安装 Python 和所需的模块。
- 这本书可以使用任何操作系统——苹果操作系统、视窗操作系统和基于 Linux 的操作系统。我们建议您的系统中至少有 4 GB 内存。
- 运行本书提供的代码不需要 GPU。
OpenVINO 简介
OpenVINO (简称 Open 视觉推理和神经网络优化)。它旨在优化各种神经网络,以加快推理阶段。正如我们在前面几章中所讨论的,推理是一个过程,在这个过程中,一个训练好的神经网络被用来用看不见的输入数据产生结果。例如,如果一个网络被训练为对狗或猫进行分类,那么如果我们喂养泰菲(我们邻居的狗)的图像,它应该能够推断图像是狗。
考虑到图像和视频在当今世界已经变得如此普遍,有许多深度神经网络被训练来执行各种操作,例如多标签分类和运动跟踪。世界上执行的大多数推理都发生在中央处理器上,因为中央处理器非常昂贵,通常不适合单个人工智能工程师的预算。在这些情况下,OpenVINO 工具包提供的加速非常关键。
The speedup provided by OpenVINO toolkit consists of two steps. The first step focuses on the hardware specifications; it optimizes the network in a hardware-agnostic way using a Model Optimizer, which ships along with OpenVINO toolkit. The next step involves hardware-specific acceleration using OpenVINO IE.
OpenVINO 工具包由英特尔开发,英特尔以其优化的工具和硬件而闻名,专注于深度学习和人工智能。知道 vpu、GPU 和 FPGAs 也是英特尔制造的也就不足为奇了。
OpenVINO 还为 OpenCV 和 OpenVX 库提供了优化调用——这是两个最著名的计算机视觉库。
OpenVINO 工具包安装
在本节中,我们将使用英特尔的官方说明来安装 OpenVINO 工具包:
- 首先,访问 OpenVINO 工具包下载页面(
software . Intel . com/en-us/OpenVINO-toolkit/choose-download
),根据您的系统规格,选择并下载安装程序。您必须首先注册工具包的副本。 - 使用安装说明(
docs.openvinotoolkit.org/latest/index.html
)在您的系统上安装 OpenVINO 工具包。
OpenVINO toolkit will also install its own Intel-optimized version of OpenCV. If you already have OpenCV installed on your system, the installer will show that another version of OpenCV is already installed. It's better to install the ...
OpenVINO 组件
OpenVINO 工具包由以下主要组件组成:
- 深度学习部署工具包 ( DLDT )由模型优化器、IE、预训练模型和一些工具组成,帮助您测量模型的准确性。
- 有一个为英特尔库编译的优化版本的 OpenCV(也进行了优化)。
- 有 OpenCL 库。
- 您可以获得英特尔的媒体软件开发工具包来加快视频处理速度。
- OpenVX 有一个优化版本。
交互式人脸检测演示
OpenVINO 工具包安装还提供了各种演示和示例应用程序。只是为了测试安装,让我们看看是否可以运行交互式人脸检测演示。
首先,我们将移动到deployment_tools/inference_engine
文件夹中的samples
目录。您将在这里找到各种演示应用程序,例如图像分类和推理管道。
交互式人脸检测演示将视频作为输入,并结合年龄、性别、头部姿势、情绪和面部标志检测来执行人脸检测。根据您要执行的检测类型,您可以使用以下预训练模型列表中的模型:
- 您只能使用执行人脸检测
用 OpenCV 使用 OpenVINO 推理机
在前一节中,我们讨论了如何运行交互式人脸检测演示。这一切都很好,但仍然存在的问题是如何利用 OpenVINO 的力量与您已经存在的 OpenCV 代码。请注意,在这里,我们强调的是利用 OpenVINO 的优势,只需对代码进行最少的更改。这一点非常重要,因为 OpenVINO 并没有出现在 OpenCV 的早期版本中,包括更常用的 3.4.3 版本。作为一名优秀的开发人员,你的工作是确保你的程序支持最大数量的系统和库。
对我们来说幸运的是,只需要一行代码就可以开始为您的 OpenCV 模型的推理代码使用 OpenVINO 推理引擎,如下面的代码片段所示:
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);
用 OpenCV 使用 OpenVINO 模型动物园
在前几节中,我们简要讨论了 OpenVINO Model Zoo 以及如何将 OpenVINO IE 与 OpenCV 一起使用。在本节中,我们将了解更多关于模型动物园及其提供的内容。
OpenVINO Model Zoo 是一个优化的预训练模型的集合,可以直接导入 OpenVINO 进行推理。这个特性的重要性在于,OpenVINO 加速背后的一个主要原因是它用于推理的优化模型文件。底层的推理原理还是和大多数深度学习推理工具包和语言一样,比如 OpenCV。OpenCV 的dnn
模块利用 OpenVINO 的这种加速原理,将其作为所有推理任务的默认后端。...
基于 OpenCV 和 OpenVINO 推理机的图像分类
本章我们要讨论的最后一个主题是如何使用 OpenCV 和 OpenVINO 推理机进行图像分类。
在我们切入细节之前,让我们先简单看一下一个图像分类问题。图像分类,也称为图像识别,是深度学习任务集的一部分,也可能是最常见的任务之一。在该任务中,提供一组图像作为模型的输入,模型输出输入图像的类别或标签。
这方面的一个常见例子是狗和猫的分类问题,其中在大量的猫和狗的图像上训练模型,然后在测试阶段,模型预测输入图像是猫的图像还是狗的图像。
虽然这看起来可能是一个非常幼稚的问题,但图像分类在工业应用中有很大的重要性。例如,如果你的相机吹嘘拥有人工智能能力,这意味着它可以识别图像中存在的物体,并相应地更改图像设置——无论是自然风景的图像还是一些食物的 Instagram 级快照。下图显示了人工智能手机摄像头的输出:
想想我之前拍的我家屋顶的照片。请注意,当切换到人工智能模式时,相机能够检测到我正在拍摄植物照片,并自动更改设置以匹配它。所有这些都是可能的,只是因为图像分类。现在,考虑一下你,作为一个计算机视觉工程师,正在尝试训练一个模型,它可以识别图像是植物、瀑布还是人。
如果您的模型不能在几毫秒内推断出图像的类别或标签,那么您在训练模型时付出的所有努力都将付诸东流。没有人愿意等待哪怕几秒钟,让摄像头检测到物体并更改设置。
这让我们回到 OpenVINO 的推理机的重要性。OpenVINO 有自己版本的图像分类工具包,可以如下使用。
使用 OpenVINO 的图像分类
让我们看看如何使用 OpenVINO 安装目录中的图像分类演示:
- 首先,移动到 OpenVINO 安装目录中的
deployment_tools/demo
目录。 - 接下来,让我们在目录中已经存在的演示图像上运行图像分类:
./demo_squeezenet_download_convert_run.sh
这是我得到的结果:
让我们也运行另一个使用相同图像的演示,推理管道演示,它很好地展示了 OpenVINO 的推理引擎的速度:
./demo_security_barrier_camera.sh
以下是输出图像:
因为我们在...
基于 OpenCV 和 OpenVINO 的图像分类
让我们首先使用 OpenCV 创建一个图像分类推断代码。由于我们只关心推理,我们将使用一个预先训练好的模型:
- 首先我们下载一下 Caffe 模型文件,
deploy.prototxt
和bvlc_reference_caffenet.caffemodel
,可以从 Berkley Visions 的资源库(github . com/BVLC/Caffe/tree/master/models/bvlc _ reference _ caffenet
)中获取。确保在当前工作目录下下载这两个文件。我们还需要一个带有所提到的类标签的文本文件。可以从github . com/torch/tutors/blob/master/7 _ imagenet _ classification/synset _ words . txt
获取。 - 让我们也使用长颈鹿的样本图像进行图像分类:
接下来,让我们开始用 OpenCV 和 OpenVINO 编写一些图像分类的代码。
- 让我们从导入一些模块开始:
import numpy as np
import cv2
- 接下来,让我们指定模型文件:
image = cv2.imread("animal-barbaric-brown-1319515.jpg")
labels_file = "synset_words.txt"
prototxt = "deploy.prototxt"
caffemodel = "bvlc_reference_caffenet.caffemodel"
- 现在,让我们从标签文本文件中读取标签:
rows = open(labels_file).read().strip().split("\n")
classes = [r[r.find(" ") + 1:].split(",")[0] for r in rows]
- 让我们指定用于推断的后端:
net = cv2.dnn.readNetFromCaffe(prototxt,caffemodel)
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_INFERENCE_ENGINE)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
- 让我们对输入图像进行一些基本的图像处理:
blob = cv2.dnn.blobFromImage(image,1,(224,224),(104,117,123))
- 最后,让我们将此图像传递给模型并获得输出:
net.setInput(blob)
predictions = net.forward()
- 让我们获得传递给模型的长颈鹿图像的前 10 个预测:
indices = np.argsort(predictions[0])[::-1][:5]
- 最后,让我们显示十大预测:
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
:这需要 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 的推理引擎作为后端来进行比较,以查看您获得的加速。
You won't be able to see much difference in speed. To get a noticeable difference, use a for
loop to carry out the inference 100 times, add up the total time taken during each step, and then divide it by 100 to obtain an average.
摘要
在这一章中,我们简要地看了 OpenVINO 工具包——它是什么,它用于什么,以及我们如何安装它。我们还研究了如何运行工具包提供的演示和示例,以了解和见证 OpenVINO 的强大功能。最后,我们看到了如何在我们预先存在的 OpenCV 代码中利用这种能力,只需添加一行来指定用于模型推理的后端。
您可能还注意到,我们在本章中没有介绍太多的实践内容。这是因为 OpenVINO 更适合深度学习应用程序,这不在本书的讨论范围内。如果你是一个深度学习爱好者,你绝对应该浏览英特尔在 OpenVINO 工具包上提供的文档并开始学习。...
十三、总结
恭喜你!你刚刚朝着成为机器学习实践者迈出了一大步。您不仅熟悉各种各样的基本机器学习算法,还知道如何将它们应用于监督和非监督学习问题。此外,还向您介绍了一个新的令人兴奋的主题,OpenVINO Toolkit。在前一章中,我们学习了如何安装 OpenVINO 并运行交互式人脸检测和图像分类演示等。我相信你很喜欢了解这些话题。
在我们分道扬镳之前,我想给你一些最后的建议,给你一些额外的资源,并给你一些如何进一步提高你的机器学习和数据科学技能的建议。在本章中,我们将学习如何处理机器学习问题并构建我们自己的估计器。我们将学习如何用 C++编写自己的基于 OpenCV 的分类器,以及用 Python 编写基于 scikit-learn 的分类器。
在本章中,我们将涵盖以下主题:
- 探讨机器学习问题
- 用 C++编写自己的基于 OpenCV 的分类器
- 用 Python 编写自己的基于 scikit-learn 的分类器
- 从这里去哪里
技术要求
可以从以下链接查阅本章代码:github . com/PacktPublishing/Machine-Learning-for-OpenCV-Second-Edition/tree/master/chapter 13
。
以下是软件和硬件要求的总结:
- 您将需要 OpenCV 版本 4.1.x (4.1.0 或 4.1.1 都可以)。
- 您将需要 Python 3.6 版本(任何 Python 3 . x 版本都可以)。
- 您将需要 Anaconda Python 3 来安装 Python 和所需的模块。
- 除了这本书,你可以使用任何操作系统——苹果操作系统、视窗操作系统和基于 Linux 的操作系统。我们建议您的系统中至少有 4 GB 内存。
- 你不需要一个图形处理器来运行本书提供的代码。
探讨机器学习问题
当你在野外看到一个新的机器学习问题时,你可能会忍不住跳到前面,把你最喜欢的算法扔向这个问题——也许是你最理解的或者最有趣的算法。但是事先知道哪种算法在你的特定问题上表现最好通常是不可能的。
相反,你需要退一步,看看更大的图景。在你陷入太深之前,你会想要定义你试图解决的实际问题。例如,你是否已经有了一个明确的目标,或者你只是想做一些探索性的分析,并在数据中找到一些有趣的东西?通常,你会从一个总体目标开始,比如检测垃圾邮件,制作电影推荐,或者在上传到社交媒体平台的图片中自动标记你的朋友。然而,正如我们在本书中看到的,解决问题通常有几种方法。例如,我们使用逻辑回归、k-均值聚类和深度学习来识别手写数字。定义问题将帮助你提出正确的问题,并在这个过程中做出正确的选择。
根据经验,您可以使用以下五个步骤来处理野外的机器学习问题:
- 将问题分类:这是一个两步走的过程:
- 按输入分类:简单来说,如果你有标注数据,那就是监督学习的问题。如果你有未标记的数据,想要找到结构,这是一个无监督的学习问题。如果你想通过与环境交互来优化目标函数,这是一个强化学习问题。
- 按输出分类:如果你模型的输出是一个数字,那就是回归问题。如果模型的输出是一个类(或类别),这就是一个分类问题。如果模型的输出是一组输入组,这就是一个聚类问题。
- 找到可用的算法:现在您已经对问题进行了分类,您可以使用我们掌握的工具来识别适用且实用的算法。微软已经创建了一个方便的算法备忘单,显示哪些算法可以用于哪类问题。尽管备忘单是为微软 Azure 量身定制的,但你可能会发现它通常很有帮助。
The machine learning algorithm cheat sheet PDF (by Microsoft Azure) can be downloaded from aka.ms/MLCheatSheet
.
- 实现所有适用的算法 ( 原型):对于任何给定的问题,通常有少数候选算法可以完成这项工作。那么,你怎么知道选哪一个呢?通常,这个问题的答案并不简单,所以你不得不求助于反复试验。原型制作最好分两步完成:
- 你的目标应该是用最少的特征工程快速而肮脏地实现几个算法。在这个阶段,您应该主要感兴趣的是看哪种算法在粗略的尺度下表现得更好。这一步有点像招聘:你在寻找任何理由来缩短候选算法的列表。一旦你把列表减少到几个候选算法,真正的原型就开始了。
- 理想情况下,您可能希望建立一个机器学习管道,使用一组精心选择的评估标准来比较数据集上每个算法的性能(参见第十一章、使用超参数调整选择正确的模型)。在这个阶段,你应该只处理少数几个算法,这样你就可以把注意力转向真正神奇的地方:特征工程。
- 特征工程:也许比选择正确的算法更重要的是选择正确的特征来表示数据。您可以在第四章、中阅读所有关于特征工程的内容,代表数据和工程特征。
- 优化超参数:最后,你还要优化一个算法的超参数。例子可能包括主成分分析的主成分数,参数,k-最近邻算法中的 k ,或者神经网络中的层数和学习率。你可以看看第十一章、用超参数调谐选择合适的型号,获取灵感。
构建自己的评估器
在这本书里,我们参观了 OpenCV 提供的各种机器学习工具和算法。而且,如果出于某种原因,OpenCV 没有提供我们想要的东西,我们总是可以依靠 scikit-learn。
然而,在处理更高级的问题时,您可能会发现自己想要执行一些 OpenCV 或 scikit-learn 都不提供的非常具体的数据处理,或者您可能想要对现有的算法进行细微的调整。在这种情况下,您可能想要创建自己的估算器。
用 C++编写自己的基于 OpenCV 的分类器
由于 OpenCV 是那些引擎盖下不包含任何一行 Python 代码的 Python 库之一(我开玩笑的,但已经很接近了),您将不得不在 C++中实现您的定制估计器。这可以通过四个步骤来完成:
- 实现一个包含主要源代码的 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:
接下来,定义类的constructor
和destructor
:
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::TrainData
或cv::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;
}
- 写一个名为
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})
- 通过键入以下命令,在命令行上编译文件:
$ cmake
$ make
- 运行可执行的
MyClass
方法,该方法是由最后一个命令生成的,应该会导致以下输出:
$ ./MyClass
MyClass constructor
用 Python 编写自己的基于 scikit-learn 的分类器
或者,您可以使用 scikit-learn 库编写自己的分类器。
可以通过导入BaseEstimator
和ClassifierMixin
来实现。后者将提供相应的score
方法,适用于所有分类器:
- 或者,首先,您可以覆盖
score
方法以提供您自己的度量score
方法:
In [1]: import numpy as np... from sklearn.base import BaseEstimator, ClassifierMixin
- 然后,可以定义一个继承自
BaseEstimator
和ClassifierMixin
的类:
In [2]: class MyClassifier(BaseEstimator, ClassifierMixin):... """An example classifier"""
- 你需要提供一个构造函数,
fit
和predict
方法。构造函数定义了所有的参数...
从这里去哪里
这本书的目标是向你介绍机器学习的世界,并让你做好成为机器学习实践者的准备。现在,您已经了解了基本算法的所有知识,您可能想要更深入地研究一些主题。
虽然没有必要了解我们在本书中实现的所有算法的所有细节,但了解它们背后的一些理论可能会让你成为一名更好的数据科学家。
If you are looking for more advanced material, then you might want to consider some of the following classics:
- 斯蒂芬·马斯兰,机器学习:算法视角, 第二版,查普曼和霍尔/Crc,ISBN 978-146658328-3,2014
- 模式识别和机器学习。斯普林格,ISBN 978-038731073-2,2007
- 特雷弗·哈斯蒂、罗伯特·蒂布希拉尼和杰罗姆·弗里德曼,《统计学习的要素:数据挖掘、推理和预测》。第二版,斯普林格,ISBN 978-038784857-0,2016
说到软件库,我们已经了解了两个基本库——Opencv 和 scikit-learn。通常,使用 Python 对于尝试和评估模型非常有用,但是更大的 web 服务和应用程序通常是用 Java 或 C++编写的。
例如,C++包是 Vowpal Wabbit (大众),自带命令行界面。为了在集群上运行机器学习算法,人们经常使用mllib
,一个建立在 Spark 之上的 Scala 库。如果你没有和 Python 结婚,你也可以考虑使用另一种数据科学家的通用语言——R。r 是一种专门为统计分析设计的语言,以其可视化能力和许多(通常是高度专业化的)统计建模包的可用性而闻名。
不管你选择哪种软件,我想最重要的建议是继续练习你的技能。但你已经知道了。有许多优秀的数据集等着你去分析:
在本书中,我们充分利用了 scikit-learn 内置的示例数据集。此外,scikit-learn 提供了一种从外部服务加载数据集的方法,例如mldata.org。更多信息请参考
scikit-learn.org/stable/datasets/index.html
。Kaggle 是一家在其网站 http://www.kaggle.com 举办各种数据集和竞赛的公司。比赛通常由各种公司、非营利组织和大学主办,获胜者可以获得一些严肃的奖金。竞争的一个缺点是,它们已经提供了一个特定的优化指标,通常是一个固定的预处理数据集。
OpenML 平台(
www.openml.org
)拥有超过 20,000 个数据集和超过 50,000 个相关的机器学习任务。另一个受欢迎的选择是加州大学欧文分校机器学习资源库(
archive.ics.uci.edu/ml/index.php
),通过一个可搜索的界面托管超过 370 个受欢迎且维护良好的数据集。
Finally, if you are looking for more example code in Python, a number of excellent books nowadays come with their own GitHub repository:
- 《Python 数据科学手册:处理数据的基本工具》。奥赖利,ISBN 978-149191205-8,2016,
github.com/jakevdp/PythonDataScienceHandbook
- Andreas Muller 和 Sarah Guido,Python 机器学习入门:数据科学家指南。奥赖利,ISBN 978-144936941-5,2016,
github.com/amueller/introduction_to_ml_with_python
- Sebastian Raschka, Python 机器学习。Packt,ISBN 978-178355513-0,2015,
github.com/rasbt/python-machine-learning-book
摘要
在本章中,我们学习了如何处理机器学习问题,并构建了自己的估计器。我们学习了如何用 C++编写自己的基于 OpenCV 的分类器,用 Python 编写基于 scikit-learn 的分类器。
在这本书里,我们涵盖了大量的理论和实践。我们讨论了各种各样的基本机器学习算法,既有监督的也有无监督的,并举例说明了最佳实践以及避免常见陷阱的方法,我们还涉及了各种用于数据分析、机器学习和可视化的命令和包。
如果你做到了这一步,你已经朝着机器学习的掌握迈出了一大步。从现在开始,我相信你自己会做得很好。
剩下要说的就是再见了!...
第一部分:机器学习和 OpenCV 的基础
在这本书的第一部分,我们将复习机器学习和 OpenCV 的基础知识,首先是安装所需的库,然后是基本的 OpenCV 函数、监督学习的基础知识及其应用,最后是使用 OpenCV 进行特征检测和识别。
本节包括以下章节:
- 第一章机器学习的一种尝试
- 第二章,在 OpenCV 中处理数据
- 第三章监督学习的第一步
- 第四章,代表数据和工程特性
第二部分:使用 OpenCV 的操作
本节重点介绍高级机器学习概念,以及如何使用 OpenCV 和 scikit-learn 实现这些概念。我们将介绍一些先进的机器学习概念,如决策树、支持向量机和贝叶斯学习,然后最后讨论第二类机器学习问题——无监督学习。
本节包括以下章节:
- 第五章利用决策树进行医疗诊断
- 第六章、支持向量机检测行人
- 第七章,使用贝叶斯学习实现垃圾邮件过滤器
- 第八章,用无监督学习发现隐藏结构
第三部分:使用 OpenCV 的高级机器学习
本书的最后一部分将涵盖重要和高级的主题,如深度学习、集成机器学习方法和超参数调整。我们还将介绍 OpenCV 的最新成员——英特尔的 OpenVINO 工具包。我们将简要介绍 OpenVINO,如何安装它,以及它的各种组件是什么,然后最后看看它如何与 OpenCV 一起用于图像分类问题。
本节包括以下章节:
- 第九章,利用深度学习对手写数字进行分类
- 第十章、集成分类方法
- 第十一章、用超参数调谐选择正确的模型
- 第十二章,使用 OpenVINO 搭配 OpenCV
- 第十三章结论