面向 OpenCV 的机器学习(二)

原文:Machine Learning for OpenCV

协议:CC BY-NC-SA 4.0

六、基于支持向量机的行人检测

在前一章中,我们讨论了如何使用决策树进行分类和回归。在这一章中,我们想把注意力转向机器学习世界中另一个成熟的监督学习者:支持向量机 ( 支持向量机)。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 的明确目标是最大化数据点之间的边际

学习最优决策边界

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

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

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 值(位于XX[:, 0]的第一列)与 y 值(位于XX[:, 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 中,支持向量机的构建、训练和评分方式与我们迄今为止遇到的其他学习算法完全相同,使用以下四个步骤:

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

如下图所示,我们可以在不同的模式下操作 SVM。目前,我们所关心的是我们在前面的例子中讨论过的情况:一个试图用直线分割数据的 SVM。这可以用setKernel方法指定:

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

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

当然,到目前为止,我们还不知道引擎盖下发生了什么。据我们所知,我们还不如从网络搜索中获取这些命令,并将其输入终端,而不真正知道我们在做什么。但这不是我们想要的样子。让一个系统工作是一回事,理解它是另一回事。我们开始吧!

可视化决策边界

试图理解我们的数据是正确的,试图理解我们的分类器也是正确的:可视化是理解系统的第一步。我们知道 SVM 不知何故提出了一个决策边界,允许我们对 80%的测试样本进行正确分类。但是,我们如何才能发现决策边界实际上是什么样子的呢?

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

我们将在一个专门的函数中这样做,我们称之为plot_decision_boundary ...

处理非线性决策边界

如果无法使用线性决策边界对数据进行最佳分区,该怎么办?在这种情况下,我们说数据不是线性可分的*。*

处理不可线性分离的数据的基本思想是创建原始特征的非线性组合。这就好比说我们想把数据投影到一个更高维的空间(比如从 2D 到 3D),在这个空间里数据突然变成线性可分的。

下图说明了这一概念:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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:这是我们之前用的内核。它在原始特征空间中提供了一个线性决策边界(即 xy 值)。
  • cv2.ml.SVM_POLY:这个核提供了一个决策边界,它是原始特征空间中的多项式函数。为了使用这个内核,我们还必须通过svm.setCoef0指定一个系数(通常设置为0)并通过svm.setDegree指定多项式的次数。
  • cv2.ml.SVM_RBF:这个内核实现了我们之前讨论的那种高斯函数。
  • cv2.ml.SVM_SIGMOID:这个内核实现了一个 sigmoid 函数,类似于我们在第三章、监督学习的第一步中讨论逻辑回归时遇到的函数。
  • cv2.ml.SVM_INTER:这个内核是 OpenCV 3 的新增功能。它根据直方图的相似性来分类。

实现非线性支持向量机

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

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

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

设置不同的 SVM 内核相对简单。我们从kernels列表中获取一个条目,并将其传递给 SVM 类的setKernels方法。仅此而已。

重复事情最懒的方法是使用如下所示的for循环:

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

那么步骤如下:…

在野外探测行人

我们简单谈了一下检测和识别的区别。而识别关注的是对物体进行分类(例如,作为行人、汽车、自行车等),检测基本上是回答这个问题:在这个图像中是否存在行人?

大多数检测算法背后的核心思想是将图像分割成许多小块,然后将每个图像块分类为包含行人或不包含行人。这正是我们在这一部分要做的。为了得到我们自己的行人检测算法,我们需要执行以下步骤:

  1. 建立一个包含行人的图像数据库。这些将是我们的正面数据样本。
  2. 建立一个不包含行人的图像数据库。这些将是我们的负数据样本。
  3. 在数据集上训练 SVM。
  4. 将 SVM 应用于测试图像的每个可能的补丁,以确定整个图像是否包含行人。

获取数据集

出于本节的目的,我们将使用麻省理工学院的人员数据集,我们可以将其免费用于非商业目的。因此,在获得相应的软件许可之前,请确保不要在您开创性的自主创业公司中使用这种软件。

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.

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

  1. 因为我们应该从…

看一眼方向梯度的直方图

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 分类程序与检测过程联系起来。这样做的方法是对图像中的每个可能的补丁重复我们的分类。这类似于我们之前可视化决策边界时所做的事情;我们创建了一个精细的网格,并对网格上的每个点进行分类。同样的想法也适用于这里。我们将图像分成多个小块,并将每个小块分类为是否包含行人。

通过执行以下步骤,您将能够检测到图像中的行人:

  1. 我们首先必须在图像中循环所有可能的面片,如下所示,每次将我们感兴趣的区域移动少量stride像素:
In [23]: stride = 16
...      found = []
...      for ystart in np.arange(0, img_test.shape[0], stride):
...          for xstart in np.arange(0, img_test.shape[1], stride):
  1. 我们希望确保我们不会超越图像边界:
...              if ystart + hroi > img_test.shape[0]:
...                  continue
...              if xstart + wroi > img_test.shape[1]:
...                  continue
  1. 然后我们切割出感兴趣区域,对其进行预处理,并对其进行分类:
...              roi = img_test[ystart:ystart + hroi,
...                             xstart:xstart + wroi, :]
...              feat = np.array([hog.compute(roi, (64, 64))])
...              _, ypred = svm.predict(feat)
  1. 如果该特定补丁恰好被归类为行人,我们会将其添加到成功列表中:
...              if np.allclose(ypred, 1):
...                  found.append((ystart, xstart, hroi, wroi))
  1. 因为行人不仅可能出现在不同的位置,而且可能出现在不同的大小,我们将不得不重新缩放图像并重复整个过程。谢天谢地,OpenCV 以detectMultiScale函数的形式为这个多尺度检测任务提供了便利功能。这有点难,但我们可以将所有 SVM 参数传递给hog对象:
In [24]: rho, _, _ = svm.getDecisionFunction(0)
...      sv = svm.getSupportVectors()
...      hog.setSVMDetector(np.append(sv.ravel(), rho))
  1. 然后可以调用检测函数:
In [25]: found = hog.detectMultiScale(img_test)

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

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!

  1. 实际上,当人们面临诸如行人检测的标准任务时,他们通常依赖于内置于 OpenCV 中的预扫描 SVM 分类器。这就是我在本章开头暗示的方法。通过加载cv2.HOGDescriptor_getDaimlerPeopleDetector()cv2.HOGDescriptor_getDefaultPeopleDetector(),我们可以从几行代码开始:
In [26]: hogdef = cv2.HOGDescriptor()
...      pdetect = cv2.HOGDescriptor_getDefaultPeopleDetector()
In [27]: hogdef.setSVMDetector(pdetect)
In [28]: found, _ = hogdef.detectMultiScale(img_test)
  1. 使用 matplotlib 绘制测试图像很容易,如下所示:
In [29]: from matplotlib import patches
...      fig = plt.figure()
...      ax = fig.add_subplot(111)
...      ax.imshow(cv2.cvtColor(img_test, cv2.COLOR_BGR2RGB))
  1. 然后我们可以通过在found中循环包围盒来标记图像中检测到的行人:
...      for f in found:
...          ax.add_patch(patches.Rectangle((f[0], f[1]), f[2], f[3],
...                                         color='y', linewidth=3,
...                                         fill=False))

结果是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

进一步改进模型

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

如果我们还不知道如何正确调整超参数呢?

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

基于支持向量机的多类分类

支持向量机本质上是两类分类器。具体而言,实践中最流行的多类分类方法是创建 |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 项活动,分别是WalkingWalking_UpstairsWalking_DownstairsSittingStandingLaying。主要使用嵌入式加速度计和陀螺仪捕获恒定速率为 50 Hz 的三轴线性加速度和三轴角速度。为了给这些数据贴上标签,这些实验被录了下来。数据集被随机分成两组,其中 70%的志愿者被选中生成训练数据,30%的志愿者被选中生成测试数据。

属性信息

对于数据集中的每个条目,提供了以下内容:

  • 加速度计的三轴加速度和物体的近似加速度
  • 陀螺仪的三轴角速度
  • 具有 561 特征向量的时域和频域变量
  • 各种活动标签
  • 被观察对象的标识符

通过参考以下步骤,您将了解如何使用支持向量机构建多类分类:

  1. 让我们快速导入您需要的所有必要库,以便实现具有多类分类的 SVM:
In [1]: import numpy as np
...     import pandas as pd
...     import matplotlib.pyplot as plt 
...     %matplotlib inline
...     from sklearn.utils import shuffle
...     from sklearn.svm import SVC
...     from sklearn.model_selection import cross_val_score, GridSearchCV
  1. 接下来,您将加载数据集。因为我们应该从notebooks/目录中的 Jupyter 笔记本运行这段代码,所以数据目录的相对路径只是data/:
In [2]: datadir = "data"
...     dataset = "multiclass"
...     train = shuffle(pd.read_csv("data/dataset/train.csv"))
...     test = shuffle(pd.read_csv("data/dataset/test.csv"))
  1. 让我们检查训练和测试数据集中是否有任何缺失值;如果有,我们将简单地从数据集中删除它们:
In [3]: train.isnull().values.any()
Out[3]: False
In [4]: test.isnull().values.any()
Out[4]: False 
  1. 接下来,我们将找到数据中类的频率分布,这意味着我们将检查有多少样本属于六个类中的每一个:
In [5]: train_outcome = pd.crosstab(index=train["Activity"], # Make a crosstab
 columns="count") # Name the count column
... train_outcome

从下面的截图中,可以观察到LAYING类样本最多,但总体来说,数据分布大致均匀,没有出现类不平衡的主要迹象:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 接下来,我们将从训练和测试数据集中分离出预测值(输入值)和结果值(类标签):
In [6]: X_train = pd.DataFrame(train.drop(['Activity','subject'],axis=1))
...     Y_train_label = train.Activity.values.astype(object)
...     X_test = pd.DataFrame(test.drop(['Activity','subject'],axis=1)) 
...     Y_test_label = test.Activity.values.astype(object)
  1. 由于 SVM 期望数字输入和标签,您现在将把非数字标签转换为数字标签。但是首先,我们必须从sklearn库中导入一个preprocessing模块:
In [7]: from sklearn import preprocessing
... encoder = preprocessing.LabelEncoder()
  1. 现在,我们将列车和测试标签编码为数值:
In [8]: encoder.fit(Y_train_label)
...     Y_train = encoder.transform(Y_train_label)
...     encoder.fit(Y_test_label)
...     Y_test = encoder.transform(Y_test_label) 
  1. 接下来,我们将缩放(标准化)列车和测试特征集,为此,您将从sklearn导入StandardScaler:
In [9]: from sklearn.preprocessing import StandardScaler
...     scaler = StandardScaler()
...     X_train_scaled = scaler.fit_transform(X_train)
...     X_test_scaled = scaler.transform(X_test)
  1. 一旦数据被缩放并且标签的格式正确,现在就是我们拟合数据的时候了。但在此之前,我们将定义一个字典,该字典具有 SVM 在训练自身时将使用的不同参数设置,这种技术被称为GridSearchCV。参数网格将基于随机搜索的结果:
In [10]: params_grid = [{'kernel': ['rbf'], 'gamma': [1e-3, 1e-4],
                     'C': [1, 10, 100, 1000]},
                    {'kernel': ['linear'], 'C': [1, 10, 100, 1000]}]
  1. 最后,我们将使用前面的参数对数据调用GridSearchCV以获得最佳 SVM 拟合:
In [11]: svm_model = GridSearchCV(SVC(), params_grid, cv=5)
...      svm_model.fit(X_train_scaled, Y_train)
  1. 是时候检查一下 SVM 模型在数据上的训练效果了;总之,我们会找到准确性。不仅如此,我们还将检查 SVM 表现最好的参数设置:
In [12]: print('Best score for training data:', svm_model.best_score_,"\n") 
...      print('Best C:',svm_model.best_estimator_.C,"\n") 
...      print('Best Kernel:',svm_model.best_estimator_.kernel,"\n")
...      print('Best Gamma:',svm_model.best_estimator_.gamma,"\n")
Out[12]: Best score for training data: 0.986
...      Best C: 100
...      Best Kerne: rbf
...      Best Gamma: 0.001

瞧啊。如我们所见,SVM 在多类分类问题的训练数据上达到了 98.6%的准确率。但是在我们找到测试数据的准确性之前,不要着急。所以,让我们快速检查一下:

In [13]: final_model = svm_model.best_estimator_
... print("Training set score for SVM: %f" % final_model.score(X_train_scaled , Y_train))
... print("Testing set score for SVM: %f" % final_model.score(X_test_scaled , Y_test ))
Out[13]: Training set score for SVM: 1.00
... Testing set score for SVM: 0.9586

哇哦!是不是很神奇?我们能够在测试集上达到 95.86%的准确率;这就是支持向量机的力量。

摘要

在本章中,我们了解了各种形式和风格的支持向量机。我们现在知道如何在 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) 表示 CM 都为真的概率(指女性既有癌症又有乳房 x 光检查阳性的概率)。如前所示,这相当于女性属于第 1 组的概率。

逗号()表示逻辑*、,颚化符( ~ )表示逻辑而非*。因此, p(~C,M) 表示 C 不为真的概率, M 为真的概率(指女性没有癌症但钼靶检查呈阳性的概率)。这相当于一个女性属于第三组的概率。所以,分母基本上加起来就是第 1 组( p(C,M) )和第 3 组( p(~C,M) )的女性。

但是等等!这两组加在一起只是表示女性乳房 x 光检查阳性的概率, p(M) 。因此,我们可以简化前面的等式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

贝叶斯版本是重新解读 p(C,M) 的含义。我们可以将 p(C,M) 表达如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在有点混乱了。这里, 简单来说就是女性患癌的概率(对应于前面提到的 X 组)。考虑到一名妇女患有癌症,她的乳房 x 光检查呈阳性的概率是多少?从问题题来看,我们知道是 80%。这是 p(M|C)M 给出 C 的概率。

用这个新公式代替第一个公式中的 p(C,M) ,我们得到如下公式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在贝叶斯世界中,这些术语都有其特定的名称:

  • p(C|M) 被称为后路,始终是我们要计算的东西。在我们的例子中,这对应于在乳房 x 光检查呈阳性的情况下,认为女性患有乳腺癌的程度。
  • 被称为先验,因为它对应于我们关于乳腺癌有多常见的初步知识。我们也称之为我们对 C 的最初信仰程度。
  • p(M|C) 称为
  • p(M) 称为证据

因此,您可以再次重写等式,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

大多数情况下,只对该分数的分子感兴趣,因为分母不依赖于 C,,所以分母是常数,可以忽略不计。

理解朴素贝叶斯分类器

到目前为止,我们只谈了一个证据。然而,在大多数现实场景中,我们必须在多条证据(如随机变量X1X2的情况下预测一个结果(如随机变量 Y )。所以,不是计算p(Y | X)而是经常要计算 p(Y|X 1 ,X 2 ,…,X n ) 。不幸的是,这使得数学非常复杂。对于两个随机变量, X 1X 2 ,联合概率的计算如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

丑陋的部分是术语 p(X 1 |X 2 ,C) ,表示 X 1 的条件概率取决于所有其他变量,包括 C 。这就扯平了…

实现你的第一个贝叶斯分类器

但是算够了,让我们做一些编码吧!

在前一章中,我们学习了如何使用 scikit-learn 生成大量高斯斑点。你记得这是怎么做到的吗?

创建玩具数据集

我所指的功能位于 scikit-learn 的datasets模块中。让我们创建 100 个数据点,每个数据点属于两个可能的类之一,并将它们分组为两个高斯斑点。为了使实验具有可重复性,我们指定一个整数来为random_state挑选种子。你可以再次选择你喜欢的号码。在这里,我选择了托马斯·贝叶斯的出生年份(只是为了好玩):

In [1]: from sklearn import datasets...     X, y = datasets.make_blobs(100, 2, centers=2,        random_state=1701, cluster_std=2)

让我们来看看我们刚刚使用我们值得信赖的朋友 Matplotlib 创建的数据集:

In [2]: import matplotlib.pyplot as plt...     plt.style.use('ggplot')...     %matplotlib inlineIn [3]: plt.scatter(X[:, 0], X[:, ...

用普通贝叶斯分类器对数据进行分类

然后,我们将使用与前面章节相同的过程来训练一个普通贝叶斯分类器。等等,为什么不是朴素贝叶斯分类器?事实证明,OpenCV 并没有真正提供一个真正的朴素贝叶斯分类器。相反,它带有贝叶斯分类器,不一定期望特征是独立的,而是期望数据聚集成高斯斑点。这正是我们之前创建的数据集!

通过以下步骤,您将了解如何使用普通贝叶斯分类器构建分类器:

  1. 我们可以使用以下函数创建一个新的分类器:
In [5]: import cv2
...     model_norm = cv2.ml.NormalBayesClassifier_create()
  1. 然后,通过train方法进行训练:
In [6]: model_norm.train(X_train, cv2.ml.ROW_SAMPLE, y_train)
Out[6]: True
  1. 一旦分类器训练成功,它将返回True。我们经历了预测和给分类器打分的过程,就像我们以前做过一百万次一样:
In [7]: _, y_pred = model_norm.predict(X_test)
In [8]: from sklearn import metrics
...     metrics.accuracy_score(y_test, y_pred)
Out[8]: 1.0
  1. 更好的是——我们可以重用上一章的绘图功能来检查决策边界!如果你还记得的话,这个想法是创建一个包含所有数据点的网格,然后对网格上的每个点进行分类。网格是通过同名的 NumPy 函数创建的:
In [9]: def plot_decision_boundary(model, X_test, y_test):
...         # create a mesh to plot in
...         h = 0.02 # step size in mesh
...         x_min, x_max = X_test[:, 0].min() - 1, X_test[:, 0].max() +
            1
...         y_min, y_max = X_test[:, 1].min() - 1, X_test[:, 1].max() +
            1
...         xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
...                              np.arange(y_min, y_max, h))
  1. meshgrid函数将返回两个浮点矩阵xxyy,它们包含网格上每个坐标点的 xy 坐标。我们可以使用ravel函数将这些矩阵展平为列向量,并将它们堆叠起来形成一个新矩阵X_hypo:
...         X_hypo = np.column_stack((xx.ravel().astype(np.float32),
...                                   yy.ravel().astype(np.float32)))
  1. X_hypo现在包含X_hypo[:, 0]中的所有 x 值和X_hypo[:, 1]中的所有 y 值。这是predict功能可以理解的格式:
...         ret = model.predict(X_hypo)
  1. 然而,我们希望能够同时使用 OpenCV 和 scikit-learn 的模型。两者的区别在于 OpenCV 返回多个变量(一个指示成功/失败的布尔标志和预测的目标标签),而 scikit-learn 只返回预测的目标标签。因此,我们可以检查ret输出是否是一个元组,在这种情况下,我们知道我们正在处理 OpenCV。在这种情况下,我们存储元组的第二个元素(ret[1])。否则,我们处理的是 scikit-learn,不需要索引到ret:
...         if isinstance(ret, tuple):
...             zz = ret[1]
...         else:
...             zz = ret
...         zz = zz.reshape(xx.shape)
  1. 剩下要做的就是创建一个等高线图,其中zz表示网格上每个点的颜色。除此之外,我们使用可靠的散点图绘制数据点:
...         plt.contourf(xx, yy, zz, cmap=plt.cm.coolwarm, alpha=0.8)
...         plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, s=200)
  1. 我们通过传递模型(model_norm)、特征矩阵(X)和目标标签向量(y)来调用函数:
In [10]: plot_decision_boundary(model_norm, X, y)

输出如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

目前为止,一切顺利。有趣的是,贝叶斯分类器还返回每个数据点被分类的概率:

In [11]: ret, y_pred, y_proba = model_norm.predictProb(X_test)

该函数返回一个布尔标志(True表示成功,False表示失败)、预测目标标签(y_pred)和条件概率(y_proba)。这里,y_proba是一个 N x 2 矩阵,对于每一个 N 数据点,它被分类为 0 类或 1 类的概率为:

In [12]: y_proba.round(2)
Out[12]: array([[ 0.15000001,  0.05      ],
                [ 0.08      ,  0\.        ],
                [ 0\.        ,  0.27000001],
                [ 0\.        ,  0.13      ],
                [ 0\.        ,  0\.        ],
                [ 0.18000001,  1.88      ],
                [ 0\.        ,  0\.        ],
                [ 0\.        ,  1.88      ],
                [ 0\.        ,  0\.        ],
                [ 0\.        ,  0\.        ]], dtype=float32)

这意味着,对于第一个数据点(顶行),其属于 0 类(即 p(C 0 |X) )的概率为 0.15(或 15%)。同样,属于 1 类的概率为p(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(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.

用朴素贝叶斯分类器对数据进行分类

以下步骤将帮助您构建朴素贝叶斯分类器:

  1. 我们可以通过向 scikit-learn 寻求帮助,将结果与真正的朴素贝叶斯分类器进行比较:
In [13]: from sklearn import naive_bayes...      model_naive = naive_bayes.GaussianNB()
  1. 像往常一样,通过fit方法训练分类器:
In [14]: model_naive.fit(X_train, y_train)Out[14]: GaussianNB(priors=None)
  1. 分类器的评分内置于:
In [15]: model_naive.score(X_test, y_test)Out[15]: 1.0
  1. 又是满分!然而,与 OpenCV 相反,这个分类器的predict_proba方法返回真实的概率值,因为所有的值都在 0 和 1 之间,并且因为所有的行加起来是 1:
In [16]: yprob = model_naive.predict_proba(X_test) ...

可视化条件概率

通过参考以下步骤,您将能够可视化条件概率:

  1. 为此,我们将稍微修改前面示例中的绘图函数。我们首先在(x_minx_max)和(y_miny_max)之间创建一个网格:
In [18]: def plot_proba(model, X_test, y_test):
...          # create a mesh to plot in
...          h = 0.02 # step size in mesh
...          x_min, x_max = X_test[:, 0].min() - 1, X_test[:, 0].max() + 1
...          y_min, y_max = X_test[:, 1].min() - 1, X_test[:, 1].max() + 1
...          xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
...                               np.arange(y_min, y_max, h))
  1. 然后,我们展平xxyy,并按列将它们添加到特征矩阵中,X_hypo:

...          X_hypo = np.column_stack((xx.ravel().astype(np.float32),
...                                    yy.ravel().astype(np.float32)))
  1. 如果我们想让这个函数与 OpenCV 和 scikit-learn 一起工作,我们需要为predictProb(在 OpenCV 的情况下)和predict_proba(在 scikit-learn 的情况下)实现一个开关。为此,我们检查一下model是否有一个叫predictProb的方法。如果方法存在,我们可以调用它;否则,我们假设我们面对的是 scikit-learn 的模型:
...          if hasattr(model, 'predictProb'):
...             _, _, y_proba = model.predictProb(X_hypo)
...          else:
...             y_proba = model.predict_proba(X_hypo)
  1. 就像我们之前看到的In [16]一样,y_proba将是一个 2D 矩阵,对于每个数据点,包含数据属于 0 类(在y_proba[:, 0]中)和 1 类(在y_proba[:, 1]中)的概率。将这两个值转换成轮廓函数可以理解的颜色的一种简单方法是简单地取两个概率值的差:
...          zz = y_proba[:, 1] - y_proba[:, 0]
...          zz = zz.reshape(xx.shape)
  1. 最后一步是将X_test绘制为彩色网格顶部的散点图:
... plt.contourf(xx, yy, zz, cmap=plt.cm.coolwarm, alpha=0.8)
... plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, s=200)
  1. 现在,我们准备调用函数:
In [19]: plot_proba(model_naive, X, y)

结果是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上面的截图显示了朴素贝叶斯分类器的条件概率。

使用朴素贝叶斯分类器对电子邮件进行分类

这一章的最终任务将是把我们新获得的技能应用到真正的垃圾邮件过滤器上!该任务涉及使用朴素贝叶斯算法解决二进制分类问题。

朴素贝叶斯分类器实际上是一个非常流行的电子邮件过滤模型。他们的天真很好地适用于文本数据的分析,其中每个特征都是一个单词(或一袋单词**),并且建立每个单词对其他单词的依赖模型是不可行的。**

**有很多好的电子邮件数据集,例如:

正在加载数据集

您可以参考以下步骤来加载数据集:

  1. 如果你从 GitHub 下载了最新的代码,你会在notebooks/data/chapter7目录下找到几个.zip文件。这些文件包含原始电子邮件数据(带有“收件人”、“抄送”和“正文”字段),这些数据要么被归类为垃圾邮件(带有SPAM = 1类别标签),要么不被归类为垃圾邮件(也称为 ham,HAM = 0类别标签)。
  2. 我们构建了一个名为sources的变量,它包含了所有的原始数据文件:
In [1]: HAM = 0
...     SPAM = 1
...     datadir = 'data/chapter7'
...     sources = [
...        ('beck-s.tar.gz', HAM),
...        ('farmer-d.tar.gz', HAM),
...        ('kaminski-v.tar.gz', HAM),
...        ('kitchen-l.tar.gz', HAM),
...        ('lokay-m.tar.gz', HAM),
...        ('williams-w3.tar.gz', HAM),
...        ('BG.tar.gz', SPAM),
...        ('GP.tar.gz', SPAM),
...        ('SH.tar.gz', SPAM)
...     ]
  1. 第一步是将这些文件提取到子目录中。为此,我们可以使用我们在上一章中编写的extract_tar函数:
In [2]: def extract_tar(datafile, extractdir):
...         try:
...             import tarfile
...         except ImportError:
...             raise ImportError("You do not have tarfile installed. "
...                               "Try unzipping the file outside of "
...                               "Python.")
...         tar = tarfile.open(datafile)
...         tar.extractall(path=extractdir)
...         tar.close()
...         print("%s successfully extracted to %s" % (datafile,
...                                                    extractdir))
  1. 要将该函数应用于源中的所有数据文件,我们需要运行一个循环。extract_tar函数需要一个到.tar.gz文件的路径,这是我们从datadirsources中的一个条目构建的,以及一个将文件提取到的目录(datadir)。这将提取所有电子邮件,例如,data/chapter7/beck-s.tar.gzdata/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:

  1. 我们从初始化这两个变量开始:
In [4]: import os
...     def read_single_file(filename):
...         past_header, lines = False, []
  1. 然后,我们检查名称为filename的文件是否存在。如果有,我们就开始一行一行地循环:
...         if os.path.isfile(filename):
...             f = open(filename, encoding="latin-1")
...             for line in f:

你可能已经注意到了encoding="latin-1"部分。由于某些电子邮件不是 Unicode 格式,这是为了正确解码文件。

我们不想保留标题信息,所以我们一直循环,直到遇到'\n'字符,此时我们将past_headerFalse翻转到True

  1. 此时,满足以下if-else子句的第一个条件,我们将文本文件中剩余的所有行追加到lines变量中:
...                 if past_header:
...                     lines.append(line)
...                 elif line == '\n':
...                     past_header = True
...             f.close()
  1. 最后,我们将所有行连接成一个字符串,用换行符分隔,并返回文件的完整路径和文件的实际内容:
...         content = '\n'.join(lines)
...         return filename, content
  1. 第二个功能的工作是循环一个文件夹中的所有文件,并对它们调用read_single_file:
In [5]: def read_files(path):
...         for root, dirnames, filenames in os.walk(path):
...             for filename in filenames:
...                 filepath = os.path.join(root, filename)
...                 yield read_single_file(filepath)

这里yield是一个类似于return的关键词。不同的是yield返回一个生成器,而不是实际值,如果您期望有大量的项目要迭代,这是可取的。

用熊猫建立数据矩阵

现在,是时候介绍 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_trainy_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 数据集。为了强调这是一种无监督的方法,我们将把标签排除在可视化之外:

  1. 我们将继续使用matplotlib来实现我们所有的可视化目的:
In [1]: import matplotlib.pyplot as plt
...     %matplotlib inline
...     plt.style.use('ggplot')
  1. 遵循前面章节中的相同方法,我们将创建总共 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-意味着算法也可以做到这一点,而不需要任何关于目标标签或底层数据分布的信息。

  1. 虽然 k -means 当然是一个统计模型,但是在 OpenCV 中,它不是通过ml模块和常见的trainpredict API 调用来的。而是直接作为cv2.kmeans提供。为了使用该模型,我们必须指定一些参数,例如终止条件和一些初始化标志。这里,只要误差小于 1.0 ( cv2.TERM_CRITERIA_EPS)或者已经执行了十次迭代(cv2.TERM_CRITERIA_MAX_ITER)我们就告诉算法终止:
In [3]: import cv2
...     criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
...                 10, 1.0)
...     flags = cv2.KMEANS_RANDOM_CENTERS
  1. 然后,我们可以将前面的数据矩阵(X)传递给cv2.means。我们还指定了聚类的数量(4)以及算法在不同的随机初始猜测下应该进行的尝试次数(10),如下面的代码片段所示:
In [4]: import numpy as np
...     compactness, labels, centers = cv2.kmeans(X.astype(np.float32),
...                                               4, None, criteria,
...                                               10, flags)

返回三个不同的变量。

  1. 第一个,compactness返回从每个点到它们对应的聚类中心的平方距离的总和。高紧密度分数表示所有点都接近它们的聚类中心,而低紧密度分数表示不同的聚类可能没有很好地分开:
In [5]: compactness
Out[5]: 527.01581170992
  1. 当然,这个数字很大程度上取决于X中的实际值。如果点与点之间的距离很大,首先,我们不能期望一个任意小的紧凑性分数。因此,绘制数据点的图更有意义,这些数据点被着色为它们分配的聚类标签:
In [6]: plt.scatter(X[:, 0], X[:, 1], c=labels, s=50, cmap='viridis')
...     plt.scatter(centers[:, 0], centers[:, 1], c='black', s=200,
...                 alpha=0.5);
  1. 这将生成所有数据点的散点图,这些数据点根据它们所属的聚类进行着色,相应的聚类中心在每个聚类的中心用较暗的斑点表示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上图为 k 的结果-表示 k=4 的聚类。这里的好消息是k-意思是算法(至少在这个简单的例子中)将点分配给聚类,非常类似于我们可能做的,如果我们用眼睛做的话。但是算法是如何这么快找到这些不同的聚类的呢?毕竟,集群分配的可能组合的数量与数据点的数量成指数关系!用手,尝试所有可能的组合肯定需要很长时间。

幸运的是,没有必要进行彻底的搜索。取而代之的是, k -means 采用的典型方法是使用迭代算法,也称为期望最大化

理解期望最大化

k-意味着聚类只是被称为期望最大化的更一般算法的一个具体应用。简而言之,该算法的工作原理如下:

  1. 从一些随机的集群中心开始。
  2. 重复直到收敛:
    • 期望步骤:将所有数据点分配到它们最近的聚类中心。
    • 最大化步骤:取聚类中所有点的平均值更新聚类中心。

这里,期望步骤如此命名,因为它涉及更新我们对数据集中每个点属于哪个聚类的期望。最大化步骤之所以如此命名,是因为它涉及最大化定义聚类中心位置的适应度函数。在 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 的期望最大化:

  1. 初始化:随机选择若干个集群中心,n_clusters。我们不只是选择任何随机数,而是选择实际的数据点作为聚类中心。我们通过沿着第一个轴排列X并在这个随机排列中选择第一个n_clusters点来实现:
        ...         rng = np.random.RandomState(rseed)
        ...         i = rng.permutation(X.shape[0])[:n_clusters]
        ...         centers = X[i]
  1. while永远循环:根据最近的聚类中心分配标签。在这里,scikit-learn 的pairwise_distance_argmin功能正是我们想要的。它为X中的每个数据点计算centers中最近的聚类中心的索引:
        ...         while True:
        ...         labels = pairwise_distances_argmin(X, centers)
  1. 寻找新的聚类中心:这一步我们要取X中属于特定聚类(X[labels == i])的所有数据点的算术平均值:
        ...          new_centers = np.array([X[labels ==
                     i].mean(axis=0)
  1. 检查收敛情况,必要时打破 while 循环:这是最后一步,确保一旦工作完成,我们就停止算法的执行。我们通过检查所有新的集群中心是否与旧的集群中心相等来确定工作是否完成。如果这是真的,我们退出循环;否则,我们继续循环:
        ...             for i in range(n_clusters)])
        ...             if np.all(centers == new_centers):
        ...                break
        ...             centers = new_centers
  1. 退出函数并返回结果:
        ...             return centers, labels

我们可以将我们的函数应用于前面我们创建的数据矩阵X。既然我们知道数据是什么样的,我们就知道我们在寻找四个集群:

In [8]: centers, labels = find_clusters(X, 4)
...     plt.scatter(X[:, 0], X[:, 1], c=labels, s=100, cmap='viridis');

这将生成下面的图。从下图中观察到的要点是,在应用 k 均值聚类之前,所有数据点都被归类为同一种颜色;然而,在使用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-表示将原始图像中的数百万种颜色组织成适当数量的颜色。

可视化真彩色调色板

通过执行以下步骤,您将能够可视化彩色图像的真彩色调色板:

  1. 让我们来看看一个特殊的图像:
In [1]: import cv2...     import numpy as np...     lena = cv2.imread('data/lena.jpg', cv2.IMREAD_COLOR)
  1. 现在,我们知道如何在睡眠中启动 Matplotlib:
In [2]: import matplotlib.pyplot as plt...     %matplotlib inline...     plt.style.use('ggplot')
  1. 但是,这一次,我们希望禁用ggplot选项通常在图像上显示的网格线:
In [3]: plt.rc('axes', **{'grid': False})
  1. 然后,我们可以使用以下命令来可视化 Lena(不要忘记将颜色通道的 BGR 顺序切换到 RGB):
In [4]: plt.imshow(cv2.cvtColor(lena, cv2.COLOR_BGR2RGB)) ...

使用 k 均值缩小调色板

通过参考以下步骤,您将能够使用k-意味着聚类将彩色图像投影到缩小的调色板中:

  1. 现在,让我们通过指示 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)
  1. 缩小调色板中的 16 种不同颜色对应于生成的簇。centers数组的输出显示所有颜色都有三个条目— BGR—值介于 0 和 1 之间:
In [10]: centers
Out[10]: array([[ 0.29973754,  0.31500012,  0.48251548],
                [ 0.27192295,  0.35615689,  0.64276862],
                [ 0.17865284,  0.20933454,  0.41286203],
                [ 0.39422086,  0.62827665,  0.94220853],
                [ 0.34117648,  0.58823532,  0.90196079],
                [ 0.42996961,  0.62061119,  0.91163337],
                [ 0.06039202,  0.07102439,  0.1840712 ],
                [ 0.5589878 ,  0.6313886 ,  0.83993536],
                [ 0.37320262,  0.54575169,  0.88888896],
                [ 0.35686275,  0.57385623,  0.88954246],
                [ 0.47058824,  0.48235294,  0.59215689],
                [ 0.34346411,  0.57483661,  0.88627452],
                [ 0.13815609,  0.12984112,  0.21053818],
                [ 0.3752504 ,  0.47029912,  0.75687987],
                [ 0.31909946,  0.54829341,  0.87378371],
                [ 0.40409693,  0.58062142,  0.8547557 ]], dtype=float32)
  1. labels向量包含对应于 16 簇labels的 16 种颜色。因此,标签为 0 的所有数据点将根据centers数组中的第 0 行进行着色;同样,标签为 1 的所有数据点将根据centers数组中的第 1 行进行着色,以此类推。因此,我们希望使用labels作为centers数组中的索引—这些是我们的新颜色:
In [11]: new_colors = centers[labels].reshape((-1, 3))
  1. 我们可以再次绘制数据,但这一次,我们将使用new_colors对数据点进行相应的着色:
In [12]: plot_pixels(img_data, colors=new_colors, 
...      title="Reduce color space: 16 colors")   

结果是原始像素的重新着色,其中每个像素被分配其最近的聚类中心的颜色:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 为了观察重新着色的效果,我们必须将new_colors绘制为图像。我们展平了之前的图像,从图像到数据矩阵。现在回到图像,我们需要做反,就是根据 Lena 图像的形状重塑new_colors:
In [13]: lena_recolored = new_colors.reshape(lena.shape)
  1. 然后,我们可以像任何其他图像一样可视化重新着色的 Lena 图像:
In [14]: plt.figure(figsize=(10, 6))
...      plt.imshow(cv2.cvtColor(lena_recolored, cv2.COLOR_BGR2RGB));
...      plt.title('16-color image')

结果是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

很棒,对吧?

总的来说,前面的截图非常清晰可辨,尽管有些细节可能丢失了。假设您将图像压缩了大约 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 没有提供凝聚层次聚类的实现,但它是一种流行的算法,应该属于我们的机器学习技能集:

  1. 我们从生成 10 个随机数据点开始,就像前面的截图一样:
In [1]: from sklearn.datasets import make_blobs
...     X, y = make_blobs(random_state=100, n_samples=10)
  1. 使用熟悉的统计建模应用编程接口,我们导入AgglomerativeClustering算法并指定所需的聚类数:
In [2]: from sklearn import cluster
...     agg = cluster.AgglomerativeClustering(n_clusters=3)
  1. 像往常一样,通过fit_predict方法将模型拟合到数据:
In [3]: labels = agg.fit_predict(X)
  1. 我们可以生成散点图,其中每个数据点都根据预测的标签进行着色:
In [4]: import matplotlib.pyplot as plt
... %matplotlib inline
... plt.style.use('ggplot')
... plt.scatter(X[:, 0], X[:, 1], c=labels, s=100)

生成的聚类相当于下图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最后,在我们结束本章之前,让我们看看如何比较聚类算法,并为您拥有的数据选择正确的聚类算法!

比较聚类算法

sklearn库中大约有十三种不同的聚类算法。有十三种不同的选择,问题是:你应该使用什么样的聚类算法?答案是你的数据。你有什么类型的数据,你想在其上应用哪个聚类,这就是你如何选择算法。话虽如此,有许多可能的算法可能对你的问题和数据有用。sklearn中的十三个类中的每一个都专门用于特定的任务(如共聚类和双聚类或聚类特征而不是数据点)。专门用于文本聚类的算法将是聚类文本数据的正确选择。因此,如果…

摘要

在这一章中,我们讨论了一些无监督学习算法,包括 k -means、球形聚类和凝聚层次聚类。我们看到 k -means 只是更一般的期望最大化算法的一个具体应用,我们讨论了它的潜在局限性。此外,我们将 k -means 应用于两个特定的应用,即减少图像的调色板和对手写数字进行分类。

在下一章中,我们将回到监督学习的世界,并谈论一些当前最强大的机器学习算法:神经网络和深度学习。**

九、利用深度学习分类手写数字

现在让我们回到监督学习,讨论一族被称为人工神经网络的算法。对神经网络的早期研究可以追溯到 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 0x 1 。人工神经元的工作是计算两个输入的和(通常以加权和的形式),如果这个和超过某个阈值(通常为零),神经元将被认为是活动的,并输出一个 1;否则它将被认为是无声的,并输出一个负 1(或零)。用更数学的术语来说,这个麦卡洛克-皮茨神经元的输出 y 可以描述如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在上式中, w 0w 1 是权重系数,与 x 0x 1 一起构成加权和。在教科书中,输出 y+1-1 的两种不同情况通常会被激活函数 ϕ 所掩盖,该函数可能采用两种不同的值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里我们引入一个新的变量, z (所谓的网络输入,相当于加权和:z = w0x0+w1x1。然后将加权和与阈值 θ 进行比较,以确定 ϕ 的值,随后确定 y 的值。除此之外,这两个方程说的和前面一个完全一样。

如果这些方程看起来奇怪地熟悉,你可能会想起第一章,机器学习的味道,当我们谈论线性分类器的时候。

你是对的,麦卡洛克-皮茨神经元本质上是一个线性的二元分类器!

你可以这样想:x0x 1 是输入特征,w0w1是待学习的权重,分类是通过激活功能 ϕ.进行的如果我们在学习权重方面做得很好,我们会在合适的训练集的帮助下做到这一点,我们可以将数据分为正样本或负样本。在这种情况下, ϕ(z)=θ 将作为决策边界。**

借助下图,这可能更有意义:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在左边,你可以看到神经元的激活功能 ϕ ,相对于 z 绘制。记住 z 只不过是两个输入 x 0x 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 0w 1 ,使得决策边界恰好位于所有正数据点和所有负数据点之间。

为了训练神经网络,我们通常需要三样东西:

  • 训练数据:得知我们需要一些数据样本来验证我们的分类器的有效性也就不足为奇了。
  • 成本函数(也称为损失函数):成本函数提供了当前权重系数有多好的度量。有各种各样的成本函数可供使用,我们将在本章末尾讨论。一种解决方法是计算错误分类的数量。另一个是计算误差平方和
  • 学习规则:学习规则从数学上规定了我们如何从一次迭代到下一次迭代更新权重系数。这种学习规则通常取决于我们在训练数据上观察到的误差(由成本函数衡量)。

这就是著名研究员弗兰克·罗森布拉特的工作。

理解感知器

20 世纪 50 年代,美国心理学家和人工智能研究员弗兰克·罗森布拉特发明了一种算法,可以自动学习执行精确二进制分类所需的最佳权重系数 w 0w 1 :感知器学习规则。

罗森布拉特最初的感知器算法可以总结如下:

  1. 将权重初始化为零或一些小随机数。
  2. 对于每个训练样本, s i ,执行以下步骤:
    1. 计算预测目标值, ŷ i
    2. ŷ i 与地面实况、 y i 进行比较,并相应更新权重:
      • 如果两者相同(预测正确),请跳过。
      • 如果两者不同(错误预测),则推权重系数, w 0

实现你的第一个感知机

感知器很容易从头开始实现。我们可以通过创建一个感知器对象来模仿典型的分类器的 OpenCV 或 scikit-learn 实现。这将允许我们初始化新的感知器对象,这些对象可以通过fit方法从数据中学习,并通过单独的predict方法进行预测。

当我们初始化一个新的感知器对象时,我们希望传递一个学习速率(上一节中的lrη )和算法应该终止的迭代次数(n_iter):

In [1]: import numpy as np
In [2]: class Perceptron(object):
...     def __init__(self, lr=0.01, n_iter=10):
...     self.lr = lr
...     self.n_iter = n_iter
... 

fit方法是完成大部分工作的地方。该方法应该将一些数据样本(X)及其关联的目标标签(y)作为输入。然后我们将创建一个权重数组(self.weights),每个特征一个(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)

然后,我们将计算数据集中每个数据样本(xiyi)的δ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

就这样!

生成玩具数据集

在以下步骤中,您将学习如何创建和绘制玩具数据集:

  1. 为了测试我们的感知器分类器,我们需要创建一些模拟数据。现在让我们保持简单,生成属于两个 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 或-1,而make_blobs返回01。调整标签的一个简单方法是使用以下等式:
In [4]: y = 2 * y - 1
  1. 在下面的代码中,我们…

将感知器与数据拟合

在以下步骤中,您将学习在给定数据上拟合感知器算法:

  1. 我们可以实例化我们的感知器对象,类似于我们在 OpenCV 中遇到的其他分类器:
In [6]: p = Perceptron(lr=0.1, n_iter=10)

这里,我们选择了 0.1 的学习率,并告诉感知器在 10 次迭代后终止。这些值是在这一点上相当随意地选择的,尽管我们过一会儿会回到它们。

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.

  1. 一旦建立了感知器,我们可以调用fit方法来优化权重系数:
In [7]: p.fit(X, y)
  1. 有用吗?让我们来看看学习到的重量值:
In [8]: p.weights
Out[8]: array([ 2.20091094, -0.4798926 ])
  1. 别忘了偷看一下偏见这个词:
In [9]: p.bias
Out[9]: 0.20000000000000001

如果我们将这些值插入到我们的 ϕ 的等式中,很明显,感知器学习到了形式2.2 x1-0.48 x2+0.2>= 0的决策边界。

评估感知器分类器

在以下步骤中,您将根据测试数据评估经过训练的感知器:

  1. 为了找出我们的感知器表现有多好,我们可以计算所有数据样本的准确度分数:
In [10]: from sklearn.metrics import accuracy_score...      accuracy_score(p.predict(X), y)Out[10]: 1.0

满分!

  1. 让我们通过回顾前面章节中的plot_decision_boundary来看看决策环境:
In [10]: def plot_decision_boundary(classifier, X_test, y_test):...          # create a mesh to plot in...          h = 0.02 # step size in mesh...          x_min, x_max = X_test[:, 0].min() - 1, X_test[:, 0].max() + 1...          y_min, y_max = X_test[:, 1].min() - 1, X_test[:, 1].max() + 1... xx, yy = np.meshgrid(np.arange(x_min, ...

将感知器应用于不可线性分离的数据

在以下步骤中,您将学习构建感知器来分离非线性数据:

  1. 由于感知器是一个线性分类器,您可以想象它在尝试对不可线性分离的数据进行分类时会有困难。我们可以通过增加玩具数据集中两个斑点的扩散(cluster_std)来测试这一点,以便两个斑点开始重叠:
In [12]: X, y = make_blobs(n_samples=100, centers=2,
...      cluster_std=5.2, random_state=42)
...      y = 2 * y - 1
  1. 我们可以使用 matplotlib 的scatter函数再次绘制数据集:
In [13]: plt.scatter(X[:, 0], X[:, 1], s=100, c=y);
...      plt.xlabel('x1')
...      plt.ylabel('x2')

从下面的截图中可以明显看出,这些数据不再是线性可分的,因为没有直线可以完美地将两个斑点分开:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

前面的截图显示了一个不可线性分离的数据示例。那么,如果我们将感知器分类器应用于这个数据集,会发生什么呢?

  1. 我们可以通过重复前面的步骤找到这个问题的答案:
In [14]: p = Perceptron(lr=0.1, n_iter=10)
...      p.fit(X, y)
  1. 然后我们发现准确率为 81%:
In [15]: accuracy_score(p.predict(X), y)
Out[15]: 0.81000000000000005
  1. 为了找出哪些数据点被错误分类,我们可以使用我们的辅助函数再次可视化决策场景:
In [16]: plot_decision_boundary(p, X, y)
...      plt.xlabel('x1')
...      plt.ylabel('x2')

下图显示了感知器分类器的局限性。作为一个线性分类器,它试图用直线分离数据,但最终失败了。它失败的主要原因是因为数据不是线性可分的,尽管我们达到了 81%的准确率。然而,从下面的图中,很明显许多红点位于蓝色区域,反之亦然。因此,与感知器不同,我们需要一种非线性算法,它可以创建非线性(圆形)决策边界,而不是直线:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

幸运的是,有办法使感知器更强大,并最终创建非线性决策边界。

理解多层感知器

为了创建非线性决策边界,我们可以组合多个感知器来形成一个更大的网络。这也被称为一个多层感知器 ( MLP )。MLPs 通常至少由三层组成,其中第一层对数据集的每个输入要素都有一个节点(或神经元),最后一层对每个类标签都有一个节点。中间的一层叫做隐藏层

下图显示了这种前馈神经网络架构的一个示例:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个网络中,每个圆都是一个人工神经元(或者说,本质上是一个感知器),以及一个人工的输出…

理解梯度下降

当我们在本章前面谈到感知器时,我们确定了训练所需的三个基本要素:训练数据、成本函数和学习规则。虽然学习规则对单个感知器非常有效,但不幸的是,它不能推广到多层感知器,因此人们不得不提出一个更通用的规则。

如果你想一想我们如何衡量一个分类器的成功,我们通常是在成本函数的帮助下这样做的。一个典型的例子是网络的错误分类数或均方误差。这个函数(也称为损失函数)通常取决于我们试图调整的参数。在神经网络中,这些参数是权重系数。

让我们假设一个简单的神经网络有一个要调整的权重, w 。然后我们可以将成本可视化为重量的函数:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

训练开始时,在时间零点,我们可能会从这个图左边的路开始( w t=0 )。但是从图中我们知道 w 会有一个更好的值,即 w 最优,这样会使成本函数最小化。最小的成本意味着最低的误差,所以通过学习达到 w 最优 应该是我们的最高目标。

这正是梯度下降的作用。你可以把梯度想象成一个指向山上的向量。在梯度下降中,我们试图走在梯度的对面,有效地走下山,从山峰到山谷:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一旦你到达山谷,坡度就变为零,训练就完成了。

有几种方法可以到达山谷——我们可以从左边接近,也可以从右边接近。我们下降的起点由初始重量值决定。此外,我们必须小心不要走太大的一步,否则我们可能会错过山谷:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因此,在随机梯度下降(有时也称为迭代或在线梯度下降)中,目标是采取小步骤,但尽可能频繁地采取这些步骤。有效步长由算法的学习速率决定。

具体来说,我们将反复执行以下过程:

  1. 向网络呈现少量训练样本(称为批量)。
  2. 在这一小批数据上,计算成本函数的梯度。
  3. 通过在梯度的相反方向朝着山谷走一小步来更新权重系数。
  4. 重复步骤 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:

  1. 实例化一个新的 MLP 对象:
In [9]: import cv2
...     mlp = cv2.ml.ANN_MLP_create()
  1. 指定网络中每一层的大小。我们可以随意添加任意数量的图层,但是我们需要确保第一个图层具有与输入要素相同数量的神经元(在我们的示例中为784),最后一个图层具有与类标签相同数量的神经元(在我们的示例中为10),同时有两个隐藏图层,每个图层都具有512节点:
In [10]: mlp.setLayerSizes(np.array([784, 512, 512, 10]))
  1. 指定激活功能。这里我们使用之前的 sigmoidal 激活函数:
In [11]: mlp.setActivationFunction(cv2.ml.ANN_MLP_SIGMOID_SYM,
      ...                                2.5, 1.0)
  1. 指定培训方法。这里,我们使用前面描述的反向传播算法。我们还需要确保选择足够小的学习率。由于我们有大约 10 个 5 个训练样本,所以最好将学习率设置为最多 10 个 -5 个:
In [12]: mlp.setTrainMethod(cv2.ml.ANN_MLP_BACKPROP)
...      mlp.setBackpropWeightScale(0.00001)
  1. 指定终止标准。这里,我们使用与之前相同的标准:运行 10 次迭代的训练(term_max_iter)或直到误差不再显著增加(term_eps):
In [13]: term_mode = (cv2.TERM_CRITERIA_MAX_ITER + 
...                   cv2.TERM_CRITERIA_EPS)
...      term_max_iter = 10
...      term_eps = 0.01
...      mlp.setTermCriteria((term_mode, term_max_iter,
...                           term_eps))
  1. 在训练集(X_train_pre)上训练网络:
In [14]: mlp.train(X_train_pre, cv2.ml.ROW_SAMPLE, y_train_pre)
Out[14]: True

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 数据集

在以下步骤中,您将学习在数据被输入神经网络之前对其进行预处理:

  1. 为了确保每次运行实验都得到相同的结果,我们将为 NumPy 的随机数生成器挑选一个随机种子。这样,从 MNIST 数据集中洗牌训练样本将总是产生相同的顺序:
In [1]: import numpy as np
...     np.random.seed(1337)
  1. Keras 从 scikit-learn 的model_selection模块中提供了类似于train_test_split的加载功能。您可能会奇怪地熟悉它的语法:
In [2]: from keras.datasets import mnist
...     (X_train, y_train), (X_test, y_test) = mnist.load_data()

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.

  1. Keras 中的神经网络对特征矩阵的作用与标准的 OpenCV 和 scikit-learn 估计器略有不同。尽管 Keras 中特征矩阵的行仍然对应于样本的数量(在下面的代码中为X_train.shape[0]),但是我们可以通过向特征矩阵添加更多维度来保持输入图像的二维特性:
In [3]: img_rows, img_cols = 28, 28
...     X_train = X_train.reshape(X_train.shape[0], img_rows, img_cols, 1)
...     X_test = X_test.reshape(X_test.shape[0], img_rows, img_cols, 1)
...     input_shape = (img_rows, img_cols, 1)
  1. 在这里,我们将特征矩阵重塑为四维矩阵,尺寸为n_features x 28 x 28 x 1。我们还需要确保我们操作的是[0,1]之间的 32 位浮点数,而不是[0,255]中的无符号整数:
...     X_train = X_train.astype('float32') / 255.0
...     X_test = X_test.astype('float32') / 255.0
  1. 然后,我们可以像以前一样对训练标签进行一次性编码。这将确保每一类目标标签都可以分配给输出层中的一个神经元。我们可以用 scikit-learn 的preprocessing来实现这一点,但是在这种情况下,使用 Keras 自己的实用函数更容易:
In [4]: from keras.utils import np_utils
...     n_classes = 10
...     Y_train = np_utils.to_categorical(y_train, n_classes)
...     Y_test = np_utils.to_categorical(y_test, n_classes)

创建卷积神经网络

在以下步骤中,您将创建一个神经网络,并对之前预处理的数据进行训练:

  1. 一旦我们对数据进行了预处理,就该定义实际的模型了。这里,我们将再次依靠Sequential模型来定义前馈神经网络:
In [5]: from keras.model import Sequential... model = Sequential()
  1. 然而,这一次,我们将对单个层更聪明。我们将围绕卷积层设计我们的神经网络,其中核心是一个 3×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 构建最先进的网络。此外,我们了解了神经网络的所有细节:激活函数、损失函数、层类型和训练方法。总而言之,这可能是迄今为止最密集的一章。

现在你已经知道了大多数基本的监督学习者,是时候谈谈如何将不同的算法组合成一个更强大的算法了。因此,在下一章中,我们将讨论如何构建集成分类器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值