深度学习高级应用指南(三)

原文:Advanced Applied Deep Learning

协议:CC BY-NC-SA 4.0

六、对象分类:简介

在这一章中,我们将研究用神经网络可以实现的更高级的图像处理任务。我们将着眼于语义分割、定位、检测和实例分割。本章的目标不是让你成为专家,因为你可以很容易地阅读许多关于这个主题的书籍,而是给你足够的信息,以便能够理解算法和阅读原始论文。我希望,在这一章结束的时候,你会明白这些方法之间的区别,你会对这些方法的构建模块有一个直观的理解。

这些算法需要许多先进的技术,如多损失函数和多任务学习,我们在前面的章节中已经讨论过了。我们将在这一章中再看几个。请记住,在某些情况下,关于方法的原始论文只有几年的历史,所以要掌握这个主题,您需要亲自动手阅读原始论文。

训练和使用论文中描述的网络在简单的笔记本电脑上是不可行的,因此你会在本章(和下一章)中发现更少的代码和例子。我试图为您指出正确的方向,并告诉您在撰写本文时有哪些经过预先培训的库和网络可用,以防您想在自己的项目中使用这些技术。那将是下一章的主题。在相关的地方,我试图指出不同方法的区别、优点和缺点。我们将以非常肤浅的方式来看待最先进的方法,因为细节非常复杂,只有研究原始论文才能给你自己实现那些算法所需的所有信息。

什么是对象定位?

让我们从直观的理解什么是对象定位开始。我们已经看到了许多形式的图像分类:它告诉我们图像的内容是什么。这听起来很容易,但在很多情况下这很难,而且不是因为算法。例如,考虑当你在一个图像中同时有一只狗和一只猫的情况。图像的类别是什么:猫还是狗?图像的内容是什么:一只猫还是一只狗?当然,两者都在那里,但是分类算法只给你一个类别,所以它们不能告诉你你在图像中有两种动物。如果你有很多猫和狗呢?如果你有几个对象呢?你明白了。

知道猫和狗在图像中的位置可能很有趣。考虑一下自动驾驶汽车的问题:知道一个人在哪里很重要,因为这可能意味着一个死去的路人和一个活着的人之间的区别。正如我们在前面章节中所看到的,分类通常不能单独用于解决图像的实际问题。通常,识别图像中的对象有许多实例需要找到它们在图像中的位置,并能够区分它们。为此,我们需要能够找到图像中每个实例的位置及其边界。这是图像识别技术中最有趣(也是最困难)的任务之一,可以用 CNN 来解决。

通常,对于对象定位,我们希望确定一个对象(例如,一个人或一辆车)在图像中的位置,并在其周围绘制一个矩形边界框。

注意

对于对象定位,我们希望确定一个或多个对象(例如,人或汽车)在图像中的位置,并在其周围绘制一个矩形边界框。

有时在文献中,当图像只包含一个对象实例(例如,只有一个人或一辆车)时,研究人员使用术语定位,当图像包含多个对象实例时,使用术语检测

注意

定位通常是指当一幅图像只包含一个对象的实例时,而检测是指当一幅图像中有多个对象的实例时。

为了对术语进行总结和澄清,以下是所有使用的词语和术语的概述(图 6-1 中显示了直观的解释):

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

图 6-1

描述在图像中定位一个或多个对象的一般任务的不同术语的直观解释

  • 分类:给一张图片贴上标签,换句话说,就是“理解”图片里的东西。例如,一只猫的图像可能有“猫”的标签(在前面的章节中我们已经看到了几个这样的例子)。

  • 分类和定位:给一幅图像加一个标签,确定其中包含的对象的边界(通常在对象周围画一个矩形)。

  • 对象检测:当一幅图像中有一个对象的多个实例时,使用这个术语。在对象检测中,您想要确定几个对象(例如,人、汽车、标志等)的所有实例。)并在它们周围绘制边界框。

  • 实例分割:你要为每一个单独的实例用特定的类来标记图像的每一个像素,以便能够找到对象实例的确切界限。

  • 语义分割:你要给图像的每一个像素点贴上特定的类别标签。实例分段的不同之处在于,您不在乎是否有几个汽车实例作为实例。属于汽车的所有像素将被标记为“汽车”。在实例分割中,您仍然能够知道一辆汽车有多少个实例,以及它们的确切位置。为了理解其中的区别,参见图 6-1 。

分割通常是所有任务中最困难的,并且在特定情况下分割尤其困难。许多先进的技术结合起来解决这些问题。需要记住的一点是,获得足够的训练数据并不容易。请记住,这比简单的分类要困难得多,因为有人需要标记物体的位置。对于分割,需要有人对图像中的每个像素进行分类,这意味着训练数据非常昂贵且难以收集。

最重要的可用数据集

可以用来解决这些问题的一个众所周知的数据集是位于 http://cocodataset.org 的微软 COCO 数据集。该数据集包含 91 种对象类型,在 328,000 幅图像中总共有 250 万个标记实例。 1 为了让您对所使用的标注类型有个概念,图 6-2 显示了数据集中的一些例子。您可以看到对象的特定实例(如人和猫)是如何在像素级别分类的。

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

图 6-2

COCO 数据集中的图像示例

关于大小的快速说明:2017 年的训练图像大约为 118,000 个,需要 18GB 2 的硬盘空间,所以请记住这一点。用如此大量的数据训练一个网络并不简单,需要时间和大量的计算能力。有一个 API 可以下载 COCO 图像,您可以使用它,Python 中也有这个 API。更多信息可以在主网页或 API GitHub 库 https://github.com/cocodataset/cocoapi 找到。图像有五种注释类型:对象检测、关键点检测、填充分割、全景分割和图像字幕。更多信息请访问 http://cocodataset.org/#format-data

你可能遇到的另一个数据集是 Pascal VOC 数据集。不幸的是,该网站并不稳定,因此镜像存在于你可以找到文件的地方。一面镜子是 https://pjreddie.com/projects/pascal-voc-dataset-mirror/ 。请注意,这是一个比 COCO 数据集小得多的数据集。

在这一章和下一章,我们将主要集中在对象分类和定位。我们将假设在图像中我们只有一个特定对象的实例,任务是确定它是什么类型的对象,并围绕它绘制一个边界框(矩形)。这些现在已经足够挑战了!我们将简要地看一下分段是如何工作的,但我们不会深入研究它的许多细节,因为它的问题极难解决。我会提供参考资料,你可以自己检查和研究。

并集交集(IoU)

让我们考虑对图像进行分类,然后在其中的对象周围绘制一个边界框的任务。在图 6-3 中,你可以看到一个我们期望的输出示例(这里的类是cat)。

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

图 6-3

对象分类和定位的例子 3

这是一项完全监督的任务。这意味着我们需要知道边界框在哪里,并将它们与一些给定的事实进行比较。我们需要一个度量来量化预测的边界框和实际情况之间的重叠程度。这通常是通过 Union 上的交集)完成的。在图 6-4 中,你可以看到它的直观解释。作为一个公式,我们可以写成

)

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

图 6-4

IOU 指标的直观解释

在完美重叠的理想情况下,我们有 IOU = 1,而如果完全没有重叠,我们有 IOU = 0。你会在博客和书籍中找到这个术语,所以知道如何使用基本事实测量边界框是个好主意。

一种解决目标定位的简单方法(滑动窗口方法)

解决定位问题的一个简单方法如下(剧透:这不是一个好主意,但是看看为什么会有启发性):

  1. 您从左上角开始剪切输入图像的一小部分。假设你的图像有维度 xy ,你的那部分有维度 w xw y ,有wx<xwy

** 你使用一个预先训练好的网络(你如何训练它或者你如何得到它在这里是不相关的),你让它对你剪切的图像部分进行分类。

 *   你移动这个窗口一个我们称之为*步距*的量,然后用 *s* 向右下方指示。你用网络来分类这第二部分。

 *   一旦滑动窗口覆盖了整个图像,你就选择给你最高分类概率的窗口位置。这个位置会给你对象的边界框(记住你的窗口有尺寸 *w* <sub>*x*</sub> , *w* <sub>*y*</sub> )。

 *

在图 6-5 中,可以看到算法的图解说明(我们假设w*x=wy=s)。

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

图 6-5

解决目标定位问题的滑动窗口方法的图解说明

如图 6-5 所示,我们从左上角开始,向右滑动窗口。一旦我们到达图像的右边界,并且我们没有任何空间将窗口进一步向右移动,我们回到左边界,但是我们将它向下移动像素。我们继续以这种方式,直到我们到达图像的右下角。

您可能会立即发现这种方法的一些问题:

  • 根据wxT5、 w ys 的选择,我们可能无法覆盖整个图像。(您是否看到图 6-5 中窗口 4 右侧的一小部分图像未被分析?)

  • 如何选择wxT5、 w ys ?这是一个相当棘手的问题,因为我们的对象的边界框将正好具有尺寸 w xw y 。如果物体更大或更小呢?我们通常不知道它的尺寸,如果我们想要精确的边界框,这是一个很大的问题。

  • 如果我们的对象流过两个窗口呢?在图 6-5 中,你可以想象物体一半在窗口 2,一半在窗口 3。那么你的边界框将不会是正确的,如果你按照所描述的算法。

我们可以通过使用 s = 1 来解决第三个问题,以确保我们涵盖了所有可能的情况,但是前两个问题并不那么容易解决。为了解决窗口大小的问题,我们应该尝试所有可能的大小和所有可能的比例。你觉得这里有什么问题吗?你需要对你的网络进行的进化的数量正在失去控制,并且很快在计算上变得不可行。

滑动窗口方法的问题和局限性

在本书的 GitHub 资源库中,在 Chapter 6 文件夹中,您可以找到滑动窗口算法的实现。为了使事情变得更简单,我决定使用 MNIST 数据集,因为在这一点上你应该非常了解它,并且它是一个易于使用的数据集。作为第一步,我建立了一个在 MNIST 数据集上训练的 CNN,准确率达到了 99.3%。然后,我开始将模型和权重保存在磁盘上。我使用的 CNN 具有以下结构:

_______________________________________________________________
Layer (type)                 Output Shape              Param #
===============================================================
conv2d_1 (Conv2D)            (None, 26, 26, 32)        320
_______________________________________________________________
conv2d_2 (Conv2D)            (None, 24, 24, 64)        18496
_______________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 12, 12, 64)        0
_______________________________________________________________
dropout_1 (Dropout)          (None, 12, 12, 64)        0
_______________________________________________________________
flatten_1 (Flatten)          (None, 9216)              0
_______________________________________________________________
dense_1 (Dense)              (None, 128)               1179776
_______________________________________________________________
dropout_2 (Dropout)          (None, 128)               0
_______________________________________________________________
dense_2 (Dense)              (None, 10)                1290
===============================================================
Total params: 1,199,882
Trainable params: 1,199,882
Non-trainable params: 0
_______________________________________________________________

然后,我使用下面的代码保存了模型和权重(我们已经讨论过如何做到这一点):

model_json = model.to_json()
with open("model_mnist.json", "w") as json_file:
    json_file.write(model_json)
model.save_weights("model_mnist.h5")

您可以在图 6-6 中看到网络训练和准确度如何随着历元数的变化而变化。

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

图 6-6

训练(实线)和验证(虚线)数据集的损失函数值和准确度与历元数的关系

权重和模型可以在 GitHub 存储库中找到。我这样做是为了避免每次都要重新训练 CNN。我每次都可以通过重新加载来重用模型。你可以用这段代码来做这件事(如果你想像我一样在 Google Colab 中运行这段代码,就在你安装了 Google drive 之后):

model_path = '/content/drive/My Drive/pretrained-models/model_mnist.json'
weights_path = '/content/drive/My Drive/pretrained-models/model_mnist.h5'

json_file = open(model_path, 'r')
loaded_model_json = json_file.read()
json_file.close()
loaded_model = model_from_json(loaded_model_json)
loaded_model.load_weights(weights_path)

让事情变得简单。我决定创建一个中间有一个数字的更大的图像,看看如何有效地在它周围放置一个边界框。为了创建图像,我使用了以下代码:

from PIL import Image, ImageOps
src_img = Image.fromarray(x_test[5].reshape(28,28))
newimg = ImageOps.expand(src_img,border=56,fill='black')

生成的图像为 140x140 像素。在图 6-7 中可以看到。

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

图 6-7

通过在 MNIST 数据集中的一个数字周围添加 56 像素的白色边框创建的新图像

现在让我们从一个 28x28 像素的滑动窗口开始。我们可以编写一个函数来尝试定位数字,并获取图像作为输入,步幅 s ,以及值wxw y :

def localize_digit(bigimg, stride, wx, wy):
  slidx, slidy = wx, wy

  digit_found = -1
  max_prob = -1
  bbx = -1 # Bounding box x upper left
  bby = -1 # Bounding box y upper left
  max_prob_ = 0.0
  bbx_ = -1
  bby_ = -1
  most_prob_digit = -1

  maxloopx = (bigimg.shape[0] -wx) // stride
  maxloopy = (bigimg.shape[1] -wy) // stride
  print((maxloopx, maxloopy))

  for slicey in range (0, maxloopx*stride, stride):
    for slicex in range (0, maxloopy*stride, stride):
      slice_ = bigimg[slicex:slicex+wx, slicey:slicey+wx]
      img_ = Image. fromarray(slice_).resize((28, 28), Image.NEAREST)
      probs = loaded_model.predict(np.array(img_).reshape(1,28,28,1))
      if (np.max(probs > 0.2)):
        most_prob_digit = np.argmax(probs)
        max_prob_ = np.max(probs)
        bbx_ = slicex
        bby_ = slicey

      if (max_prob_ > max_prob):
        max_prob = max_prob_
        bbx = bbx_
        bby = bby_
        digit_found = most_prob_digit

  print("Digit "+str(digit_found)+ " found, with probability "+str(max_prob)+" at coordinates "+str(bbx)+" "+str(bby))

  return (max_prob, bbx, bby, digit_found)

我们的形象是这样的:

localize_digit(np.array(newimg), 28, 28, 28)

返回此代码:

Digit 1 found, with probability 1.0 at coordinates 56 56
(1.0, 56, 56, 1)

由此产生的边界框如图 6-8 所示。

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

图 6-8

滑动窗口法找到的边界框 w x = 28, w y = 28,步距 s = 28

所以这很有效。但是您可能已经注意到,我们使用了值为 28 的 w xw ys ,这是我们图像的大小。如果我们改变了会发生什么?例如,考虑图 6-9 中描述的情况。您可以清楚地看到,一旦窗口的大小和比例变为不同于 28 的值,这种方法就会停止工作。

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

图 6-9

w xw ys 不同值的滑动窗口算法结果

检查左下框中图 6-9 中分类的置信度。挺低的。例如,对于 40x40 的窗口和 10 的跨距,数字的分类是正确的(a 1 ),但是以 21%的概率完成。那就是低价值!在右下角的方框中,分类完全错误。请记住,您需要调整从图像中剪切的小部分的大小,因此它看起来可能与您使用的训练数据不同。

在这种情况下,选择正确的窗口大小和比例似乎很容易,因为您知道图像看起来像什么,但通常您不知道什么值会起作用。你必须测试不同的比例和大小,得到几个可能的边界框和分类,然后决定哪一个是最好的。您可以很容易地看到,对于可能包含几个具有不同尺寸和比例的对象的真实图像,这在计算上是不可行的。

分类和定位

我们已经看到滑动窗口方法是一个坏主意。更好的方法是使用多任务学习。这个想法是,我们可以建立一个网络,同时学习类和边界框的位置。我们可以通过在 CNN 的最后一层之后增加两层致密层来实现。一个具有(例如) N c 神经元(用于分类 N c 类)将预测具有交叉熵损失函数的类(我们将用 J 分类 来表示),一个具有四个神经元,将学习具有 2 损失函数的边界框(即你可以在图 6-10 中看到网络图。

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

图 6-10

描述可以同时预测类和边界框位置的网络的图

由于这将是一个多任务学习问题,我们将需要最小化两个损失函数的线性组合:

)

当然, α 是一个需要调优的附加超参数。正如参考 a2 损失与 MSE 成正比

)

像往常一样,我们用 m 表示我们所拥有的观察数据的数量。同样的想法也非常成功地用于人体姿态估计,这可以找到人体的特定点(例如关节),如图 6-11 所示。

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

图 6-11

人体姿态估计的一个例子。CNN 可以被训练来寻找人体的重要点,例如关节。

在这个领域有很多研究正在进行,在接下来的章节中,我们将看看这些方法是如何工作的。实现变得非常复杂和耗时。如果你想和这些算法打交道,最好的办法就是看看原始论文,研究一下。不幸的是,没有即插即用的库可以用来完成这些任务,尽管您可以找到一个 GitHub 库来帮助您。在这一章中,我们将看看最常见的 CNN 变体来进行对象定位——R-CNN、快速 R-CNN 和更快 R-CNN。在下一章,我们将研究 YOLO(你只看一次)算法。接下来的部分应该只作为相关论文的指针,并且会让你对网络的构建有一个基本的了解。这绝不是对这些实现的详尽分析,因为这需要大量的空间。

基于区域的 CNN (R-CNN)

基于区域的 CNN(也称为 R-CNN)的基本思想非常简单(但实现起来却不简单)。正如我们所讨论的,简单方法的主要问题是你需要测试大量的窗口来找到最佳匹配的边界框。搜索每个可能的位置在计算上是不可行的,因为它测试所有可能的纵横比和窗口大小。

因此,Girshick 等人 4 提出了一种方法,他们使用一种称为选择性搜索 5 的算法,首先从图像中提出 2000 个区域(称为区域建议),然后,他们不是对大量区域进行分类,而是仅对这 2000 个区域进行分类。

选择性搜索与机器学习无关,使用经典方法来确定哪些区域可能包含对象。该算法的第一步是使用像素强度和基于图形的方法分割图像(例如,Felzenszwalb 和 Huttenlocher 6 的方法)。你可以在图 6-12 中看到这种分割的结果。

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

图 6-12

应用于图像的分割示例(图像来源: http://cs.brown.edu/people/pfelzens/segment/ )

在此步骤之后,基于以下特征的相似性将相邻区域分组在一起:

  • 颜色相似性

  • 纹理相似性

  • 尺寸相似性

  • 形状兼容性

如何做到这一点的具体细节超出了本书的范围,因为这些技术通常用在图像处理算法中。

在 OpenCV 7 库中,有算法的实现,可以试试。在图 6-13 中,你可以看到一个例子。我将该算法应用于我拍摄的一张照片,并要求该算法提出 40 个区域。

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

图 6-13

OpenCV 库中实现的选择性搜索算法的输出示例

我用的 Python 代码可以在以下网站找到: https://www.learnopencv.com/selective-search-for-object-detection-cpp-python/ 。R-CNN 的主要思想是使用 CNN 来标记该算法提出的区域,然后使用支持向量机进行最终分类。

例如,在图 6-9 中,您可以看到笔记本电脑未被识别为物体。但这就是为什么在 R-CNN 中使用 2000 个区域,以确保有足够的区域被提出。人工检查许多区域不能由人用肉眼完成。区域的数量及其重叠如此之大,以至于该任务不再可行。如果您尝试该算法的 OpenCV 实现,您会注意到它相当慢。这是开发其他方法的主要原因之一。例如,手动方法不适合实时对象检测(例如,在自动驾驶汽车中)。

R-CNN 可以概括为以下几个步骤(这些步骤已从 http://toe.lt/d )开始:

  1. 拿一个预先训练好的imagenet CNN(比如 Alexnet)。

  2. 用需要检测的对象和“无对象”类重新训练最后一个完全连接的层。

  3. 从选择性搜索中获得所有建议(每张图片大约 2000 个地区建议),并调整它们的大小以匹配 CNN 的输入。

  4. 训练 SVM 对物体和背景之间的每个区域进行分类(每个类别一个二元 SVM)。

  5. 使用边界框回归。训练一个线性回归分类器,它将为边界框输出一些校正因子。

快速 R-CNN

Girshick 改进了它的算法,创造了所谓的“快速 R-CNN”。 8 这个算法背后的主要思想如下

  1. 图像通过 CNN 并提取特征图(卷积层的输出)。

  2. 提出区域,不是基于初始图像,而是基于特征图。

  3. 然后,相同的特征图和建议的区域被用于传递到分类器,该分类器决定哪个对象在哪个区域中。

解释这些步骤的图表如图 6-14 所示。

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

图 6-14

描述快速 R-CNN 算法的主要步骤的图

这种算法比 R-CNN 更快的原因是,你不必每次 9 都向卷积神经网络馈送 2000 个区域提议——你只需要做一次。

更快的 R-CNN

注意,R-CNN 和快速 R-CNN 都使用选择性搜索来提议区域,因此相对较慢。即使快速的 R-CNN 对于每个图像也需要大约两秒钟,使得这种变化不适合实时对象检测。R-CNN 需要 50 秒左右,快速 R-CNN 需要两秒左右。但事实证明,我们可以做得更好,通过消除使用选择性搜索的需要,因为这是两种算法的瓶颈。

任等人 10 提出了一个新的想法:使用神经网络从标记数据中学习区域,完全去除了缓慢的选择性搜索算法。更快的 R-CNN 需要大约 0.2 秒,使它们成为对象检测的快速算法。在 http://toe.lt/e 有一个非常好的图表,描述了更快的 R-CNN 的主要步骤。 11 我们在图 6-15 中为您报告了它,因为我认为它确实有助于直观地理解更快的 R-CNN 的主要构件。细节往往相当复杂,因此直观和肤浅的描述不会为你服务。要理解这些步骤和微妙之处,你需要更多的时间和经验。

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

图 6-15

描述快速 R-CNN 主要部分的图表。图片来源: http://toe.lt/e

在下一章,我们将看另一个算法(YOLO ),看看你如何在你自己的项目中使用这些技术。

描述数据集的原始论文是:宗-林逸、迈克尔·梅尔、塞尔日·贝隆吉、卢博米尔·布尔德夫、罗斯·吉尔希克、詹姆斯·海斯、皮埃特罗·佩罗娜、德瓦·拉曼南、c·劳伦斯·齐特尼克、皮奥特·多拉尔、微软可可:上下文中的常见对象、 https://arxiv.org/abs/1405.0312

2

http://cocodataset.org/#download

3

图片来源: http://www.cbsr.ia.ac.cn/users/ynyu/detection.html

4

https://arxiv.org/pdf/1311.2524.pdf

5

Jasper R. R. Uijlings,Koen E. A. van de Sande,Theo Gevers,Arnold w . m . smulders*《国际计算机视觉杂志》,*第 104 卷(2),第 154-171 页,2013 年[ http://toe.lt/b ]

6

页(page 的缩写)Felzenszwalb,D. Huttenlocher *,有效的基于图的图像分割,国际计算机视觉杂志,*第 59 卷,第 2 期,2004 年 9 月

7

https://opencv.org

8

https://arxiv.org/pdf/1504.08083.pdf

9

http://toe.lt/c

10

https://arxiv.org/pdf/1506.01497.pdf

11

图像的一部分出现在 Ren 的原始论文中,但是额外的标签和信息由 Leonardo Araujo dos Santos(https://legacy.gitbook.com/@leonardoaraujosantos)添加。

*

七、对象定位:Python 中的一个实现

在这一章,我们将看看 YOLO(你只看一次)方法的对象检测。这一章分为两部分:第一部分我们学习算法是如何工作的,第二部分我将给出一个例子,说明如何在自己的 Python 项目中使用它。

请记住,YOLO 是非常复杂的,所以对于 99%的人来说,预先训练的模型是进行对象检测的最佳选择。对于处于研究前沿的 1%,你可能不需要这本书,你应该知道如何从头开始做物体检测。

这一章(和前一章一样)应该为你指出正确的方向,给你理解算法所需的基础知识,并给你对象检测的第一次经验。你很快就会注意到这些方法很慢,很难实现,并且有很多限制。这是一个非常活跃的研究领域,也非常年轻。描述 YOLO 版本 3(我们将在 Python 代码的本章稍后使用)的论文刚刚在 2018 年 4 月发表。写这篇文章的时候,还不到两岁!那些算法很难实现,很难理解,也很难训练。我希望在这一章结束的时候,你会理解它的基础,并且你可以用模型执行你的第一次测试。

注意

那些算法很难实现,很难理解,也很难训练。

你只看一次(YOLO)法

在上一章中,我们看了几种物体检测的方法。我还向您展示了为什么使用滑动窗口是一个坏主意,以及困难在哪里。2015 年,Redmon J .等人提出了一种新的方法来进行物体检测:他们将其称为 YOLO(你只看一次)。他们开发了一个网络,可以执行所有必要的任务(检测物体在哪里,对多个物体进行分类等。)一气呵成。这是这种方法速度快并且经常用于实时应用的原因之一。

在文献中,您会发现该算法的三个版本:YOLOv1、YOLOv2 和 YOLOv3。v2 和 v3 是对 v1 的改进(稍后会详细介绍)。最初的网络是用 darknet 开发和训练的,darknet 是由最初算法的作者 Redmon J 开发的神经网络框架,你不会找到一个可以与 Keras 一起使用的易于下载、预训练的模型。稍后我会给你一个例子,告诉你如何在你的项目中使用它。

读关于 YOLO 的论文原文很有启发,可以在 https://arxiv.org/abs/1506.02640 找到。

注意

该方法的主要思想是将检测问题重构为一个单一的回归问题,从作为输入的图像像素到边界框坐标和类别概率 1

我们来详细看看它是如何工作的。

YOLO 是如何运作的

为了理解 YOLO 是如何工作的,最好一步一步地研究这个算法。

将图像划分为单元格

第一步是将图像分成 S × S 个细胞。对于每个单元格,我们预测单元格中有什么(以及是否有)对象。每个单元只能预测一个对象,因此一个单元不能预测多个对象。然后,对于每个单元,预测应该包含对象的一定数量( B )的边界框。在图 7-1 中,你可以看到网络可能预测的网格和边界框(作为一个例子)。原论文中,图像被划分为 7 × 7 的网格,但为了图 7-1 的清晰起见,我将图像划分为 5×5 的网格。

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

图 7-1

分成 5 × 5 网格的图像。对于单元格 D3,我们将预测鼠标,并将预测边界框(黄色框)。对于单元格 B2,我们将预测一个瓶子及其边界框(红色矩形)。

让我们以图 7-1 中的单元格 D3 为例。该单元将预测鼠标的存在,然后它将预测一定数量的边界框(黄色矩形)的 B 。类似地,单元格 B2 将同时预测瓶子和 B 边界框(图 7-1 中的红色矩形)的存在。此外,该模型预测每个边界框的类别置信度(一个数字)。准确地说,每个单元的模型输出如下:

  • 对于每个包围盒(总共 B ,有四个值: xywh 。这些是中心的位置,宽度和高度。注意,中心的位置是相对于单元位置给出的,而不是绝对值。

  • For each bounding box (B in total), there is a confidence score, which is a number that reflects how likely the box contains the object. In particular, at training time, if we indicate the probability of the cell containing the object as Pr(Object), the confidence is calculated as follows:

    )

    其中 IOU 表示并集上的交集,这是使用训练数据计算的(有关该术语的解释以及如何计算,请参见上一章)。该数字同时编码了特定对象在框中的概率以及边界框与该对象的适合程度。

因此,假设我们有 S = 5, B = 2,并且假设网络可以分类 N c = 80 个类别,网络将具有如下大小的输出:

)

在原始论文中,作者使用了 S = 7, B = 2,并使用了具有 20 个标记类的 VOC 数据集 2 。因此,网络的输出如下:

)

网络结构相当简单。它只是几个卷积层的集合(其中有一些 maxpool ),最后有一个大的密集层来预测必要的值(记住这个问题是作为回归问题提出的)。在最初的论文中,作者受到了 GoogLeNet 模型的启发。该网络有 24 层,后面是两个密集层(最后一层有 1470 个神经元;你知道为什么吗?).正如作者提到的,训练持续了整整一周。他们在培训中使用了一些技巧,如果你感兴趣,我强烈建议你阅读原文。这很有启发性(例如,他们还以一种不寻常的方式使用学习率衰减,在开始时增加学习率的值,然后在后来降低它)。他们还使用了辍学和广泛的数据扩充。训练这些模型不是一件小事。

YOLOv2(也称为 YOLO9000)

最初的 YOLO 版本有一些缺点。例如,它不太擅长检测太近的物体。在第二个版本中, 3 作者引入了一些优化,其中最重要的是锚盒。该网络给出预先确定的框集,而不是完全从零开始预测边界框,它只是预测与锚框集的偏差。可以根据您想要预测的对象类型来选择锚框,从而使网络更好地完成某些特定任务(例如,小或大的对象)。

在这个版本中,他们还改变了网络结构,使用了 19 层,然后又增加了 11 层专门用于对象检测,总共 30 层。这个版本也很难处理小对象(也是在使用锚盒的时候)。这是因为图层对图像进行了向下采样,并且在向前传递的过程中,网络信息丢失了,这使得检测微小的事物变得困难。

约洛夫 3 号

最后一个版本 4 引入了一些新的概念,使得这个模型非常强大。以下是主要的改进:

  • 预测不同比例的盒子:可以说,该模型预测了不同维度的盒子(比这要复杂一点,但这应该会让您对正在发生的事情有一个直观的了解)。

  • 网络要大得多:一共 53 层。

  • 网络使用跳跃连接。基本上,这意味着一层的输出不仅会被馈送到下一层,而且还会被馈送到网络中的下一层。这样,尚未下采样的信息将在以后使用,以使检测小对象更容易。跳过连接在 ResNets 中使用(本书不讨论),在 http://toe.lt/w 可以找到很好的介绍。

  • 这个版本使用九个锚盒,每个音阶三个。

  • 这个版本为每个单元格预测了更多的边界框。

所有这些改进使得 YOLOv3 相当不错,但也相当慢,因为处理所有这些数字需要增加计算能力。

非极大值抑制

一旦你有了所有预测的边界框,你需要选择一个最好的。请记住,对于每个单元格和对象,模型会预测几个边界框(无论您使用哪个版本)。基本上,你通过以下步骤选择最佳边界框(称为非最大值抑制):

  1. 它首先丢弃对象存在的概率小于给定阈值(通常为 0.6)的所有单元。

  2. 它将所有最有可能有物体存在的细胞都包括在内。

  3. 它采用具有最高分数的边界框,并且彼此移除 IOU 大于特定阈值(通常为 0.5)的所有其他边界框。这意味着它会删除所有与所选边框非常相似的边框。

损失函数

注意,前面提到的网络有大量的输出,所以不要指望简单的损失函数就能起作用。还要注意,最后一层的不同部分有非常不同的含义。一部分是边界框位置,一部分是类别概率,等等。损失函数有三个部分:

  • 分类损失

  • 定位损失(预测边界框和预期结果之间的误差)

  • 信心损失(盒子里是否有物体)

让我们仔细看看这三个方面的损失。

分类损失

所用的分类损失由下式确定

)

在哪里

)如果一个对象在单元格 i 中,否则为 0。

)表示在单元格 i 中拥有类别 c 的概率。

定位损失

该损失测量预测的边界框相对于预期边界框的误差。

)

信心丧失

置信度损失度量了当决定一个对象是否在盒子中时的误差。

)

在哪里

)是盒子 j 在单元格 i 的置信度。

)如果单元格中的 j * th * 包围盒 i 负责检测物体。

由于大多数细胞不包含一个物体,我们必须小心。网络可以知道背景是重要的。我们需要在成本函数中增加一项来弥补这一点。这是通过附加术语实现的:

)

其中)是)的反义词。

总损失函数

总损失函数就是所有项的总和:

)

如您所见,这是一个实现起来很复杂的公式。这是进行对象检测的最简单方法是下载并使用预训练模型的原因之一。从头开始需要一些时间和努力。相信我。

在接下来的章节中,我们将看看如何在自己的 Python 项目中使用 YOLO 算法(尤其是 YOLOv3)。

YOLO 在 Python 和 OpenCV 中的实现

YOLO 的暗网实现

如果您遵循了前面的章节,您会理解从头开始为 YOLO 开发您自己的模型对于初学者(以及几乎所有的从业者)来说是不可行的,因此,正如我们在前面的章节中所做的,我们需要使用预训练的模型来在您的项目中使用对象检测。你可以找到你想要的所有预训练模型的网页是 https://pjreddie.com 。这是黑暗网络的维护者约瑟夫·c·雷德蒙的主页。

注意

Darknet 是用 C 和 CUDA 编写的开源神经网络框架。它速度快,易于安装,支持 CPU 和 GPU 计算。

在一个子页( https://pjreddie.com/darknet/yolo/ )上,你会找到你需要的关于 YOLO 算法的所有信息。你可以从这个页面下载几个预训练模型的重量。对于每个型号,您将始终需要两个文件:

  • 一个.cfg文件,里面基本包含了网络的结构。

  • 一个.weights文件,包含训练后得到的权重。

为了让你对文件的内容有个概念,.cfg文件包含了所有使用的层的信息。下面是一个例子:

[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky

这说明了特定卷积层的结构。文件中包含的最重要的信息是关于:

  • 网络体系结构

  • 锚箱

  • 班级数量

  • 学习率和使用的其他参数

  • 批量

另一个文件(.weights)包含执行推理所需的预训练权重。注意,它们不是以 Keras 兼容的格式保存的(就像我们到目前为止使用的.h5文件),所以它们不能被加载到 Keras 模型中,除非你首先转换它们。

没有标准的工具或实用程序来转换这些文件,因为格式不是恒定的(例如,它在 YOLOv2 和 YOLOv3 之间发生了变化)。如果你有兴趣使用 YOLO 到 v2,你可以使用 YAD2K 库(另一个 Darknet 2 Keras),可以在 https://github.com/allanzelener/YAD2K 找到。

请注意,这不适用于 YOLOv3 .cfg文件。相信我,我试过了。但是如果您对 YOLOv2 满意,您可以使用这个存储库中的代码将.weight文件转换成一种更加 Keras 友好的格式。

我还想指出另一个 GitHub 库,它在 https://github.com/qqwweee/keras-yolo3 为 YOLOv3 实现了一个转换器。它有一些限制(例如,您必须使用标准锚点),但是它可能是转换文件的一个好的起点。然而,使用预训练模型有一个更简单的方法,那就是使用 OpenCV,我们将在本章后面看到。

用暗网测试目标检测

如果你只是想对一幅图像进行分类,最简单的方法就是按照 darknet 网站上的说明去做。让我们看看这是如何工作的。请注意,如果您使用的是 Linux 或 MacOS X 系统,这些说明仍然有效。在 Windows 上,您需要安装makegcc和其他几个工具。如网站所述,安装只需要几行代码:

git clone https://github.com/pjreddie/darknet
cd darknet
make
wget https://pjreddie.com/media/files/yolov3.weights

在这一点上,你可以简单地执行你的对象检测与此: 5

./darknet detect cfg/yolov3.cfg yolov3.weights table.jpg

注意,.weight文件非常大(大约 237MB)。下载时请记住这一点。在 CPU 上,这相当慢;一台非常现代的 2018 款 MacBook Pro 用了 18 秒就下载完了。你可以在图 7-2 中看到结果。

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

图 7-2

YOLOv3 与测试图像上的暗网一起使用

默认情况下,使用阈值 0.25。但是您可以使用-thresh XYZ参数指定一个不同的值。您必须将XYZ更改为您想要使用的阈值。

这种方法很适合进行对象检测,但是很难在 Python 项目中使用。为此,您需要能够在代码中使用预先训练好的模型。有几种方法可以做到这一点,但最简单的方法是使用opencv库。如果您正在处理图像,很可能您已经在处理这个库了。如果你从未听说过它,我强烈建议你去看看,因为它是一个很棒的图像库。你可以在 https://opencv.org 找到官方网页。

像往常一样,你可以在 GitHub 资源库中找到完整的代码,在本书的 Chapter 7 文件夹中。为了简洁起见,我们将只讨论最重要的部分。

你需要安装最新的opencv库。我们在这里讨论的代码是用版本 4.1.0 开发的。要确定您拥有的版本,请使用以下命令:

import cv2
print (cv2.__version__)

要尝试我们在这里讨论的代码,您需要来自 https://pjreddie.com 网站的三个文件:

  • coco.names

  • yolov3.cfg

  • yolov3.weights

coco.names包含预训练模型可以分类的类别的标签。yolov3.cfgyolov3.weights文件包含模型配置参数(正如我们已经讨论过的)和我们需要使用的权重。为了您的方便,由于yolov3.weights大约 240MB,无法上传到 GitHub,您可以在 http://toe.lt/r 下载三者的 ZIP 文件。在代码中,我们需要指定文件的位置。例如,您可以使用以下代码:

weightsPath = "yolo-coco/yolov3.weights"
configPath = "yolo-coco/yolov3.cfg"

您需要将位置更改为您在系统上保存文件的位置。OpenCV 提供了一个加载权重而无需转换权重的函数:

net = cv2.dnn.readNetFromDarknet(configPath, weightsPath)

这是很容易做到的,因为你不需要分析或者编写你自己的加载函数。它返回一个模型对象,我们稍后将使用它进行推理。如果你还记得本章开始时关于方法的讨论,我们需要得到输出层,以便得到我们需要的所有信息,比如边界框或预测类。我们可以通过下面的代码轻松做到这一点:

ln = net.getLayerNames()
ln = [ln[i[0] - 1] for i in net.getUnconnectedOutLayers()]

getUnconnectedOutLayers()函数返回未连接输出的层的索引,这正是我们要寻找的。ln变量将包含以下层:

['yolo_82', 'yolo_94', 'yolo_106']

然后,我们需要在一个 416x416 的正方形图像中调整图像的大小,并通过将像素值除以 255.0 对其进行归一化:

blob = cv2.dnn.blobFromImage(image, 1 / 255.0, (416, 416), swapRB=True, crop=False)

然后我们需要使用它作为保存在net模型中的模型的输入:

net.setInput(blob)

然后我们可以使用forward()调用对预训练模型进行正向传递:

layerOutputs = net.forward(ln)

我们还没有完成,所以不要放松。我们需要提取边界框,我们将把它们保存在boxes列表中,然后是置信度,保存在confidences列表中,然后是预测类,保存在classIDs列表中。

我们首先如下初始化列表:

boxes = []
confidences = []
classIDs = []

然后我们循环遍历这些层,提取我们需要的信息。我们可以执行如下循环:

for output in layerOutputs:
    for detection in output:

现在分数保存在从第五个开始的元素中,在detection变量中,我们可以用np.argmax(scores)提取预测的类:

scores = detection[5:]
classID = np.argmax(scores)

置信度当然是预测类的分数:

confidence = scores[classID]

我们希望预测的可信度大于零。在这里使用的代码中,我们选择了 0.15 的限值。预测边界框包含在detection变量的前四个值中:

box = detection[0:4] * np.array([W, H, W, H])
(centerX, centerY, width, height) = box.astype("int")

如果你还记得,YOLO 预测边界框的中心,所以我们需要提取左上角的位置:

x = int(centerX - (width / 2))
y = int(centerY - (height / 2))

然后我们可以简单地将找到的值添加到列表中:

boxes.append([x, y, int(width), int(height)])
confidences.append(float(confidence))
classIDs.append(classID)

然后,我们需要使用非最大值抑制(如前几节所述)。OpenCV 还为它提供了一个函数 6 :

idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.6,0.2)

该函数需要以下参数:

  • 一组边界框(保存在boxes变量中)

  • 一组置信度(保存在confidences变量中)

  • 一个阈值,用于根据分数过滤框(在前面的代码中为0.6)

  • 非最大抑制中使用的阈值(前面代码中的0.2)

然后我们可以用这个简单的代码获得正确的坐标:

for i in idxs.flatten():
        # extract the bounding box coordinates
        (x, y) = (boxes[i][0], boxes[i][1])
        (w, h) = (boxes[i][2], boxes[i][3])

你可以在图 7-3 中看到这段代码的结果。

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

图 7-3

使用 OpenCV 获得的 YOLOv3 结果

这完全是应该的——与图 7-2 中的结果相同。另外,我们有盒子上预测的概率。你可以看到这是多么容易。您只需将这几行代码添加到您的项目中。

请记住,我们使用预训练权重构建的模型将仅检测包含在图像数据集中的对象,预训练模型已通过该图像数据集进行了训练。如果您需要在不同的对象上使用模型,您需要微调模型,或者为您的对象从头开始训练模型。描述如何完全从头开始训练模型超出了本书的范围,但是在下一节中,我会提供一些提示,以防您需要这样做。

为你的特定图像训练一个 YOLO 模型

我不会描述你训练你自己的 YOLO 模型所需要的不同程序,因为那本身就需要几个章节,但是我希望我能给你指出正确的方向。让我们假设您想要专门为您的图像训练一个模型。作为第一步,您需要训练数据。假设你有足够多的图片,你首先需要给它们贴上标签。请记住,您需要为每张图像标记正确的边界框。手动完成这项任务几乎是不可能的,因此我建议两个项目来帮助您标记训练数据。

如果您想尝试根据您的数据训练您的 YOLO 模型,请查看它们。由于描述整个过程远远超出了本书的范围,我建议你阅读下面这篇中型文章,它很好地描述了如何做到这一点: http://toe.lt/v 。记住,您需要修改一个cfg文件,这样您就可以指定您试图识别的类的正确数量。例如,在yolov3.cfg文件中,您会发现这一行(在第 610 行):

classes=80

它告诉你有多少类你可以用模型来识别。您需要修改这一行来反映问题中的类的数量。

在 YOLO 的官方网站上,有关于如何做的详细描述: https://pjreddie.com/darknet/yolo/ 。向下滚动,直到找到用您自己的数据集训练模型的部分。不要低估这项任务的复杂性。需要大量的阅读和测试。

结束语

您可能已经注意到,使用这些高级技术是相当复杂的,不仅仅是复制几行代码的问题。你需要确保你理解算法是如何工作的,以便能够在你自己的项目中使用它们。根据您需要检测的对象,您可能需要花费相当多的时间来构建适合您的问题的定制模型。这将需要大量的测试和编码。这并非易事。我写这一章的目的是给你足够的工具来帮助你并给你指明正确的方向。

在前面的章节之后,你现在已经对高级技术有了足够的理解,能够自己重新实现像 YOLO 那样复杂的算法,尽管这需要时间和努力。你会遭受很多,但如果你不放弃,你会得到成功的回报。我确信这一点。

在下一章中,我们看一个在真实数据上使用 CNN 的完整例子,在这里我们使用到目前为止学到的所有技术。把第八章当作一个练习。尝试处理数据并重现那里描述的结果。希望你玩得开心!

Redmon J .等人《你只看一次:统一的、实时的物体检测》, https://arxiv.org/abs/1506.02640

2

http://host.robots.ox.ac.uk/pascal/VOC/

3

雷德蒙 j .、法尔哈迪 a .,“YOLO9000:更好、更快、更强”, https://arxiv.org/abs/1612.08242

4

雷德蒙·j .、法尔哈迪·a .,“约洛夫 3:一种增量改进”, https://arxiv.org/pdf/1804.02767.pdf

5

您可以在第七章的 GitHub 库中找到用于测试的图像。

6

你可以在 http://toe.lt/t 找到官方文档。

八、组织学组织分类

现在是时候把我们所学的东西放在一起,看看我们到目前为止所学的技术是如何在真实数据集上使用的。我们将使用一个数据集,这个数据集是我在关于深度学习的大学课程中成功使用的最终项目:“结直肠癌组织学中的纹理集合”。 1 这个数据集可以在几个网站找到:

  • http://toe.lt/f :zenodo.org

  • http://toe.lt/g :关于 Kaggle(该数据集最初由凯文·马德 2 和我准备,用于我们在苏黎世应用科技大学 2018 年秋季学期举办的大学课程)

  • http://toe.lt/h :从 TensorFlow 2.0 开始,这也可以作为预读数据集使用(链接指向数据集 API 的 TensorFlow GitHub 存储库)

先不要下载数据。我为你准备了一个 pickle (稍后会详细介绍)文件,其中包含所有可以使用的数据。你会在下一部分找到所有的信息。

本章中我们将使用的是Kather_texture_2016_image_tiles_5000文件夹,它包含 5000 张 150 x 150 px(74x 74 μm)的组织学图像。每张图像都属于八个组织类别中的一个类别(由 Zenodo 网站上的文件夹名称指定)。在代码中,我假设在你放 Jupyter 笔记本的文件夹中,有一个data文件夹,在那个data文件夹下,有一个Kather_texture_2016_image_tiles_5000文件夹。

在本书的 GitHub 存储库中,第八章的文件夹包含了您可以使用的完整代码。在这一章中,我们将只看与我们的讨论相关的部分。如果你想试试这个,请使用 GitHub 库。代码是完整的,可以直接使用。这个项目的目标是建立一个分类器,可以将不同的图像分为八类。我们将在接下来的部分中研究它们,看看困难在哪里。像往常一样,让我们从数据开始。

大部分代码是由杨奇煜·塔拉德( https://www.linkedin.com/in/fabientarrade/ )为我的大学课程开发的,他很友好地允许我使用它。我对它进行了相当多的更新,使它可以在这个例子中使用。注意,所有的工作都要感谢杨奇煜,所有的错误都是我的错。

数据分析和准备

这一节的代码包含在名为01- Data exploration and preparation.ipynb的笔记本中,该笔记本位于本书的 GitHub 资源库中的第八章文件夹中。您可以在您的计算机上打开一个窗口来尝试该代码,然后继续讨论。由于我们将图像放在不同的文件夹中,我们需要将它们加载到 pandas 数据框架中,并根据文件夹名称自动生成一个标签。例如,文件夹01_TUMOR中的图像1A11_CRC-Prim-HE-07_022.tif_Row_601_Col_151.tif is contained,因此必须将"TUMOR"作为其标签。

我们可以用一种非常简单的方式来自动化这个过程。我们从这段代码开始(所有的import请查看 GitHub 中的代码):

df = pd.DataFrame({'path': glob(os.path.join(base_dir, '*', '*.tif'))})

这会生成一个只有一列'path'的数据帧。该列包含我们要加载的每个图像的路径。变量base_dir包含了Kather_texture_2016_image_tiles_5000文件夹的路径。例如,我在 Google Colab 中运行代码,我的base_dir看起来像这样:

base_dir = '/content/drive/My Drive/Book2-ch8/data/Kather_texture_2016_image_tiles_5000'

我的数据帧的前五条记录如下所示:

/content/drive/My Drive/Book2-ch8/data/Kather_texture_2016_image_tiles_5000/05_DEBRIS/5434_CRC-Prim-HE-04_002.tif_Row_451_Col_1351.tif
/content/drive/My Drive/Book2-ch8/data/Kather_texture_2016_image_tiles_5000/05_DEBRIS/626A_CRC-Prim-HE-08_024.tif_Row_451_Col_1.tif
/content/drive/My Drive/Book2-ch8/data/Kather_texture_2016_image_tiles_5000/05_DEBRIS/148A7_CRC-Prim-HE-04_004.tif_Row_151_Col_901.tif
/content/drive/My Drive/Book2-ch8/data/Kather_texture_2016_image_tiles_5000/05_DEBRIS/6B37_CRC-Prim-HE-08_024.tif_Row_1501_Col_301.tif
/content/drive/My Drive/Book2-ch8/data/Kather_texture_2016_image_tiles_5000/05_DEBRIS/6B44_CRC-Prim-HE-03_010.tif_Row_301_Col_451.tif

现在我们可以使用.map()函数提取我们需要的所有信息并创建新的列。

df['file_id'] = df['path'].map(lambda x: os.path.splitext(os.path.basename(x))[0])
df['cell_type'] = df['path'].map(lambda x: os.path.basename(os.path.dirname(x)))
df['cell_type_idx'] = df['cell_type'].map(lambda x: int(x.split('_')[0]))
df['cell_type'] = df['cell_type'].map(lambda x: x.split('_')[1])
df['full_image_name'] = df['file_id'].map(lambda x: x.split('_Row')[0])
df['full_image_row'] = df['file_id'].map(lambda x: int(x.split('_')[-3]))
df['full_image_col'] = df['file_id'].map(lambda x: int(x.split('_')[-1]))

你可以很容易地检查每个调用在做什么。列名应该告诉你在每一列中你将有什么。在图 8-1 中,你可以看到目前为止数据帧的前两条记录。

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

图 8-1

加载图像前数据帧 df 的前两条记录

此时,我们必须用imread()读取图像。为此,我们可以简单地使用

df['image'] = df['path'].map(imread)

请记住,这可能需要一些时间(取决于您在哪里运行它)。这将创建一个名为image的新列,其中将包含图像。为了方便起见,我使用了to_pickle() pandas 调用将数据帧保存到磁盘。酸洗是将 Python 对象层次转换成字节流 3 然后保存到磁盘上的过程。这个文件叫做dataframe_Kather_texture_2016_image_tiles_5000.pkl。您可以加载以下内容:

df=pd.read_pickle('/content/drive/My Drive/Book2-ch8/data/dataframe_Kather_texture_2016_image_tiles_5000.pkl')

这样,你可以节省很多时间。你甚至不需要下载数据,因为你可以简单地使用我为你准备的泡菜。注意,pickles 对于 GitHub 来说太大了,所以我把它们保存在一个服务器上,你可以从那里下载。你可以在 GitHub 和本节末尾找到链接。首先:这个数据集中有哪些类?我们可以用这个代码检查我们的标签:

df['cell_type'].unique()

这将为我们提供以下信息:

array(['DEBRIS', 'ADIPOSE', 'LYMPHO', 'EMPTY', 'STROMA', 'TUMOR',
       'MUCOSA', 'COMPLEX'], dtype=object)

这是我们的八个班级。我们有 5000 张图片,我们可以用这个来检查:

df.shape

它给了我们这个:

(5000, 8)

下一步是检查我们是否有一个平衡的班级分布。我们可以数一数每门课有多少张图片:

df['cell_type'].value_counts()

幸运的是,我们每个班正好有 625 张图片。

EMPTY      625
ADIPOSE    625
STROMA     625
COMPLEX    625
LYMPHO     625
DEBRIS     625
TUMOR      625
MUCOSA     625
Name: cell_type, dtype: int64

奇怪的是,有五个重复的图像。您可以使用以下代码来检查:

df['full_image_name'][df.duplicated('full_image_name')]

这将报告出现两次的图像的名称。你可以在图 8-2 中看到它们。既然只有五个,我们就干脆忽略这个问题。

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

图 8-2

五幅图像在数据集中出现两次

在图 8-3 中,你可以看到每个类的几个例子。

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

图 8-3

每个类别中的图像示例

正如所料,每个图像的大小为(150,150,3):

df['image'][0].shape
(150, 150, 3)

请注意这些类是如何排序的,这取决于我们加载数据的方式。首先是DEBRIS类,然后是ADIPOSE,以此类推。如图 8-4 所示,可以使用类别标签与索引的关系图进行检查。

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

图 8-4

显示数据帧中的图像如何排序的图

现在我们可以随机打乱元素:

import random
rows = df.index.values
random.shuffle(rows)
print(rows)

那会给你

array([1115, 4839, 3684, ...,  187, 1497, 2375])

您可以看到索引现在被随机打乱了。我们需要采取的最后一步是修改实际的数据帧:

df=df.reindex(rows)
df.sort_index(inplace=True)

至此,元素被洗牌。现在我们需要对标签进行一次热编码。熊猫为这一过程提供了一个非常有用且易于使用的方法:

df_label = pd.get_dummies(df['cell_type'])

它会给你一个热编码标签,如图 8-5 所示。

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

图 8-5

使用 get_dummies() pandas 函数对标签进行一次热编码的结果

在 Keras 中使用数据需要几个步骤。一是我们需要将数据帧转换成 numpy 数组:

data=np.array(df['image'].tolist())

然后,像往常一样,我们需要创建一个培训、测试和开发数据集来进行所有常规检查:

x, x_test, y, y_test = train_test_split(data, label, test_size=0.2,train_size=0.8)
x_train, x_val, y_train, y_val = train_test_split(x, y, test_size = 0.25,train_size =0.75)

您可以使用以下代码轻松检查三个数据集的维度:

print('1- Training set:', x_train.shape, y_train.shape)
print('2- Validation set:', x_val.shape, y_val.shape)
print('3- Testing set:', x_test.shape, y_test.shape)

这将为您提供以下内容:

1- Training set: (3000, 150, 150, 3) (3000, 8)
2- Validation set: (1000, 150, 150, 3) (1000, 8)
3- Testing set: (1000, 150, 150, 3) (1000, 8)

现在,您将看到数据的类型是 integer。我们需要将它们转换成浮点数,因为我们希望以后对它们进行规范化。为此,我们使用以下代码:

x_train = np.array(x_train, dtype=np.float32)
x_test = np.array(x_test, dtype=np.float32)
x_val = np.array( x_val, dtype=np.float32)

然后我们可以标准化数据集(记住每个像素的最大值是 255):

x_train /= 255.0
x_test /= 255.0
x_val /= 255.0

为了您的方便,我将所有准备好的数据集保存为 pickles。如果您想从这里开始使用数据,您需要使用以下命令加载 pickles(您需要更改保存文件的文件夹名称):

x_train=pickle.load(open('/content/drive/My Drive/Book2-ch8/data/x_train.pkl', 'rb'))
x_test=pickle.load(open('/content/drive/My Drive/Book2-ch8/data/x_test.pkl', 'rb'))
x_val=pickle.load(open('/content/drive/My Drive/Book2-ch8/data/x_val.pkl', 'rb'))
y_train=pickle.load(open('/content/drive/My Drive/Book2-ch8/data/y_train.pkl', 'rb'))
y_test=pickle.load(open('/content/drive/My Drive/Book2-ch8/data/y_test.pkl', 'rb'))
y_val=pickle.load(open('/content/drive/My Drive/Book2-ch8/data/y_val.pkl', 'rb'))

然后你会准备好一切。请记住,包含数据的文件(x_trainx_testx_val)是大文件,其中x_train被解压缩为 800MB。如果你打算下载这些文件或者把它们上传到你的 Google drive 上,请记住这一点。当然,您需要更改保存数据的文件夹。这会节省你的时间。通常会保存 Pickles,因为您不想在每次试验数据时都重新运行整个数据准备过程。在01- Data explorationpreparation.ipynb文件中,你还会发现一些直方图分析和数据扩充的例子。出于篇幅原因,为了保持本章简洁,我们将不讨论直方图分析,但我们将在本章后面讨论数据扩充,因为这是一种非常有效的对抗过度拟合的方法。

文件对 GitHub 来说太大了,所以我把它们放在了一个服务器上,你可以在那里下载。在 GitHub 资源库(第章第 8 文件夹)中,你会找到所有的信息。如果您无法访问 GitHub,但仍想下载文件,以下是链接:

模型结构

是时候建立一些模型了。你会在本书的 GitHub 资源库中找到所有代码(第章第 8 文件夹,在02_Model_building.ipynb笔记本中),所以我们不会在这里查看所有细节。最好的方法是打开笔记本,在阅读本文的同时尝试代码。如前所述,我们首先需要加载 pickle 文件。我们可以用下面的代码做到这一点:

x_train=pickle.load(open(base_dir+'x_train.pkl', 'rb'))
x_test=pickle.load(open(base_dir+'x_test.pkl', 'rb'))
x_val=pickle.load(open(base_dir+'x_val.pkl', 'rb'))
y_train=pickle.load(open(base_dir+'y_train.pkl', 'rb'))
y_test=pickle.load(open(base_dir+'y_test.pkl', 'rb'))
y_val=pickle.load(open(base_dir+'y_val.pkl', 'rb'))

然后我们需要定义 CNN 需要的input_shape变量。在代码中,我们总是定义返回 Keras 模型的函数。例如,我们的第一次尝试是这样的:

def model_cnn_v1():

    # must define the input shape in the first layer of the neural network
    model = tf.keras.models.Sequential()
    model.add(tf.keras.layers.Conv2D(32, 3, 3, input_shape=input_shape))
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))

    model.add(tf.keras.layers.Conv2D(64, 3, 3))
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))

    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(64))
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.Dropout(0.5))
    model.add(tf.keras.layers.Dense(8))
    model.add(tf.keras.layers.Activation('sigmoid'))

    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])
    return model

这是一个简单的网络,您可以使用summary()功能进行检查:

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d (Conv2D)              (None, 50, 50, 32)        896
_________________________________________________________________
activation (Activation)      (None, 50, 50, 32)        0
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 25, 25, 32)        0
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 8, 8, 64)          18496
_________________________________________________________________
activation_1 (Activation)    (None, 8, 8, 64)          0
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 4, 4, 64)          0
_________________________________________________________________
flatten (Flatten)            (None, 1024)              0
_________________________________________________________________
dense (Dense)                (None, 64)                65600
_________________________________________________________________
activation_2 (Activation)    (None, 64)                0
_________________________________________________________________
dropout (Dropout)            (None, 64)                0
_________________________________________________________________
dense_1 (Dense)              (None, 8)                 520
_________________________________________________________________
activation_3 (Activation)    (None, 8)                 0
=================================================================
Total params: 85,512
Trainable params: 85,512
Non-trainable params: 0
_________________________________________________________________

为了确保会话被重置,我们总是使用:

tf.keras.backend.clear_session()

然后我们创建模型的一个实例,如下所示:

model_cnn_v1=model_cnn_v1()

然后,我们还保存初始重量,以确保如果我们稍后运行,我们从这些相同的重量开始:

initial_weights = model_cnn_v1.get_weights()

然后我们用这个来训练模型:

model_cnn_v1.set_weights(initial_weights)

# define path to save the mnodel
path_model=base_dir+'model_cnn_v1.weights.best.hdf5'
shutil.rmtree(path_model, ignore_errors=True)

checkpointer = ModelCheckpoint(filepath=path_model,
                               verbose = 1,
                               save_best_only=True)
EPOCHS=200
BATCH_SIZE=256

history=model_cnn_v1.fit(x_train,
                         y_train,
                         batch_size=BATCH_SIZE,
                         epochs=EPOCHS,
                         validation_data=(x_test, y_test),
                         callbacks=[checkpointer])

请注意以下几点:

  • 我们创建一个定制的回调类ModelCheckpoint,它将在每次损失函数减小时保存训练期间网络的权重。

  • 我们使用fit()调用训练网络,并将其输出保存在history变量中,以便能够在以后绘制损耗和指标。

注意

如果你在笔记本电脑或台式机上训练这样的网络可能会非常慢,这取决于你所拥有的硬件。我强烈建议你在 Google Colab 上这样做,因为这会加快你的测试速度。该书 GitHub 资源库中的所有笔记本都已经在 Google Colab 上进行了测试,可以直接从 GitHub 在 Google Colab 中打开。

在 Google Colab 上,训练之前的网络大约需要三分钟。它将达到以下精度:

  • 训练数据集的准确率:85%

  • 验证数据集上的准确率:82.7%

这些结果还不错,我们也没有太多的过拟合(你可以在图 8-6 中看到精度和损耗是如何随着历元变化的)。

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

图 8-6

文中描述的第一个网络的精度和损失函数

让我们来看一个不同的模型,我们称之为v2。这个比之前的有更多的参数:

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d (Conv2D)              (None, 150, 150, 128)     9728
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 75, 75, 128)       0
_________________________________________________________________
dropout (Dropout)            (None, 75, 75, 128)       0
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 75, 75, 64)        73792
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 37, 37, 64)        0
_________________________________________________________________
dropout_1 (Dropout)          (None, 37, 37, 64)        0
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 37, 37, 64)        36928
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 18, 18, 64)        0
_________________________________________________________________
dropout_2 (Dropout)          (None, 18, 18, 64)        0
_________________________________________________________________
flatten (Flatten)            (None, 20736)             0
_________________________________________________________________
dense (Dense)                (None, 256)               5308672
_________________________________________________________________
dense_1 (Dense)              (None, 64)                16448
_________________________________________________________________
dense_2 (Dense)              (None, 32)                2080
_________________________________________________________________
dense_3 (Dense)              (None, 8)                 264
=================================================================
Total params: 5,447,912
Trainable params: 5,447,912
Non-trainable params: 0
_________________________________________________________________

同样,您可以在 GitHub 资源库中找到所有代码。我们将再次对其进行训练,但这一次,由于时间的原因,将训练 50 个历元,并且批次大小稍小,为 64。

EPOCHS=50
BATCH_SIZE=64

history=model_cnn_v2.fit(x_train,
                         y_train,
                         batch_size=BATCH_SIZE,
                         epochs=EPOCHS,
                         validation_data=(x_test, y_test),
                         callbacks=[checkpointer])

否则,一切照旧。这一次,由于大量的参数,您会注意到我们得到了一个明显的过度拟合。事实上,我们得到了以下精度:

  • 训练数据集上的准确率:99.5%

  • 验证数据集的准确率:74%

在图 8-7 中,您可以清楚地看到过拟合,查看精度与周期数的关系图。

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

图 8-7

v2 网络的精度和损失函数与历元数的关系

我们需要做更多的工作来获得更合理的结果。现在让我们使用一个参数更少的网络(特别是内核更少的网络):

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d (Conv2D)              (None, 150, 150, 16)      448
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 150, 150, 16)      2320
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 150, 150, 16)      2320
_________________________________________________________________
dropout (Dropout)            (None, 150, 150, 16)      0
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 50, 50, 16)        0
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 50, 50, 32)        4640
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 50, 50, 32)        9248
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 50, 50, 32)        9248
_________________________________________________________________
dropout_1 (Dropout)          (None, 50, 50, 32)        0
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 16, 16, 32)        0
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 16, 16, 64)        18496
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 16, 16, 64)        36928
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 16, 16, 64)        36928
_________________________________________________________________
dropout_2 (Dropout)          (None, 16, 16, 64)        0
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 64)          0
_________________________________________________________________
conv2d_9 (Conv2D)            (None, 5, 5, 128)         73856
_________________________________________________________________
conv2d_10 (Conv2D)           (None, 5, 5, 128)         147584
_________________________________________________________________
conv2d_11 (Conv2D)           (None, 5, 5, 256)         295168
_________________________________________________________________
dropout_3 (Dropout)          (None, 5, 5, 256)         0
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 1, 1, 256)         0
_________________________________________________________________
global_max_pooling2d (Global (None, 256)               0
_________________________________________________________________
dense (Dense)                (None, 8)                 2056
=================================================================
Total params: 639,240
Trainable params: 639,240
Non-trainable params: 0

我们将称这个网络为v3。这一次,情况也好不到哪里去,如图 8-8 所示。

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

图 8-8

精度和损失函数与。v3 网络的纪元数量。

我们为什么不利用目前所学的知识呢?我们用迁移学习,看看能不能用一个预先训练好的网络。让我们下载VGG16网络并用我们的数据重新训练最后几层。为此,我们需要使用下面的代码(我们称这个网络为vgg-v4):

def model_vgg16_v4():

    # load the VGG model
    vgg_conv = tf.keras.applications.VGG16(weights='imagenet', include_top=False, input_shape = input_shape)

    # freeze the layers except the last 4 layers
    for layer in vgg_conv.layers[:-4]:
          layer.trainable = False

    # Check the trainable status of the individual layers
    for layer in vgg_conv.layers:
        print(layer, layer.trainable)

    # create the model
    model = tf.keras.models.Sequential()

    # add the vgg convolutional base model
    model.add(vgg_conv)

    # add new layers
    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(1024, activation="relu"))
    model.add(tf.keras.layers.Dropout(0.5))
    model.add(tf.keras.layers.Dense(8, activation="softmax"))

    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])

    return model

请注意我们是如何下载预先训练好的网络(正如我们在前面章节中看到的)的代码:

vgg_conv = tf.keras.applications.VGG16(weights='imagenet', include_top=False, input_shape = input_shape)

我们使用了include_top=False参数,因为我们想要移除最后的密集层,并在它们的位置放置我们自己的层。我们在最后添加一个有 1024 个神经元的层:

model.add(tf.keras.layers.Dense(1024, activation="relu"))

然后我们添加一个输出层,用8作为分类的softmax激活函数:

model.add(tf.keras.layers.Dense(8, activation="softmax"))

summary()通话将为您提供以下概述:

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
vgg16 (Model)                (None, 4, 4, 512)         14714688
_________________________________________________________________
flatten (Flatten)            (None, 8192)              0
_________________________________________________________________
dense (Dense)                (None, 1024)              8389632
_________________________________________________________________
dropout (Dropout)            (None, 1024)              0
_________________________________________________________________
dense_1 (Dense)              (None, 8)                 8200
=================================================================
Total params: 23,112,520
Trainable params: 15,477,256
Non-trainable params: 7,635,264
_________________________________________________________________

整个vgg16网络浓缩成一条线(vgg16 (Model))。在这个网络中,我们有 15’477’256 个可训练参数。相当多。事实上,在 Google Colab 上训练这个网络 30 个纪元需要大约 11 分钟。你可以在图 8-9 中看到精度和损耗是如何随着历元数而变化的。

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

图 8-9

vgg-v4 网络的精度和损失函数与历元数的关系

如您所见,情况有所改善,但我们仍然过度适应。没有之前那么戏剧化,但还是挺引人注目的。我们唯一能与之对抗的策略就是数据增强。在接下来的章节中,我们将看到在 Keras 中进行数据扩充是多么容易,以及它所带来的影响。

日期增加

对抗过度拟合的一个显而易见的策略(尽管在现实生活中很少可行)是获取更多的训练数据。在我们这里,这是不可能的。给出的图像是唯一可用的。但是这种情况下我们还是可以做一些事情:数据增强。我们这样说到底是什么意思?通常,数据扩充包括通过对现有图像应用某种变换来从现有图像生成新图像,并将它们用作额外的训练数据。

注意

数据扩充包括通过对现有图像应用某种变换来从现有图像生成新图像,并将它们用作额外的训练数据。

最常见的转换如下:

  • 将图像水平或垂直移动一定数量的像素

  • 旋转图像

  • 改变它的亮度

  • 更改缩放比例

  • 改变对比度

  • 剪切图像4

让我们看看如何在 Keras 中进行数据扩充,并看看数据集中的几个例子。我们需要用到的函数是ImageDataGenerator。首先,您需要从keras_preprocessing.image导入它:

from keras_preprocessing.image import ImageDataGenerator

请注意,该功能不会生成新图像并将它们保存到磁盘,但会在训练期间以随机方式为您及时创建增强图像数据(稍后将会清楚如何使用它)。这不需要太多额外的内存,但会增加模型训练的时间。这个函数可以做很多转换,发现它们的最好方法是查看 https://keras.io/preprocessing/image/ 的官方文档。我们会用例子来看最重要的。

水平和垂直移动

要水平和垂直移动图像,可以使用以下代码:

datagen = ImageDataGenerator(width_shift_range=.2,
                             height_shift_range=.2,
                             fill_mode='nearest')

# fit parameters from data
datagen.fit(x_train)

结果如图 8-10 中的几张随机图像所示。

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

图 8-10

水平和垂直移动图像的结果

如果你检查图像,你会注意到边界处出现了奇怪的特征。因为我们要移动图像,所以我们需要告诉 Keras 如何填充图像中的空白部分。考虑图 8-11 ,这里我们水平移动图像。您可能会注意到,图像中用 A 标记的部分仍然是空的,我们可以使用fill_mode参数告诉 Keras 如何填充该部分。

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

图 8-11

在水平方向上移动图像的例子。A 标记了结果图像中将保持空白的部分。

理解fill_mode不同可能性的最佳方式是考虑一维情况。解释来自该函数的官方文档。假设我们有一组四个像素,这些像素有一些值,我们用 a、b、c 和 d 表示。假设我们有需要填充的边界。需要填充的部分标有o。图 8-12 显示了四种可能性的图形解释:常量、最近、反射和环绕。

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

图 8-12

fill_mode参数的可能值和可能性的图形解释

图 8-11 中的图像是使用nearest填充模式生成的。虽然这种变换引入了人工特征,但使用这些额外的图像进行训练可以提高模型的准确性,并非常有效地防止过度拟合,这一点我们将在本章后面看到。最常见的填充空零件的方法是nearest

垂直翻转图像

要垂直翻转图像,可以使用以下代码:

datagen = ImageDataGenerator(vertical_flip=True)

# fit parameters from data
datagen.fit(x_train)

随机旋转图像

您可以使用以下代码随机旋转图像:

datagen = ImageDataGenerator(rotation_range=40, fill_mode = 'constant')

# fit parameters from data
datagen.fit(x_train)

而且,与移位变换一样,您可以选择不同的方式来填充空白区域。你可以在图 8-13 中看到这段代码的效果。

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

图 8-13

将图像沿随机方向旋转最多 40 度的效果(旋转量随机选择,最多 40 度)。图像中因旋转而留下的空白部分已经用常数值填充。

在图 8-14 中,你可以看到填充fill_mode = 'nearest'后的旋转效果。通常,这是填充图像的首选方式,以避免将图像的黑色(或纯色)部分提供给网络。

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

图 8-14

将图像随机旋转 40 度的效果。旋转留下的空白图像部分已经用最近的模式填充。

放大图像

您现在应该明白这些图像转换是如何工作的了。缩放与之前的转换一样简单:

datagen = ImageDataGenerator(zoom_range=0.2)

# fit parameters from data
datagen.fit(x_train)

把所有的放在一起

Keras 的一个优点是,您不需要一次执行一个转换。你可以一蹴而就。例如,考虑以下代码:

datagen = ImageDataGenerator(rotation_range=40,
                             width_shift_range=0.2,
                             height_shift_range=0.2,
                             shear_range=0.2,
                             zoom_range=0.2,
                             horizontal_flip=True,
                             fill_mode="nearest")

这将极大地增强您的数据集,同时完成几个转换:

  • 循环

  • 变化

  • 大剪刀

  • 一款云视频会议软件

  • 翻转

让我们把所有的东西放在一起,看看这个技术有多有效。

具有数据增强功能的 VGG16

现在是时候用迁移学习和图像增强来训练我们的vgg16网络了。对我们之前看到的代码的唯一修改是我们如何输入数据来训练模型。

现在我们需要使用以下代码:

history=model_vgg16_v4.fit_generator(datagen.flow(x_train, y_train, batch_size=BATCH_SIZE),
                                     validation_data=(x_test, y_test),
                                     epochs=EPOCHS,
                                     callbacks=[checkpointer])

代替经典的fit()调用,我们需要使用fit_generator()。为了解释这两个函数之间的主要区别,有必要稍微离题一下。Keras 包括不是两个而是三个可用于训练模型的函数:

  • fit()

  • fit_generator()

  • train_on_batch()

fit()函数

到目前为止,我们在训练我们的 Keras 模型时使用了fit()函数。使用此方法时,主要的隐含假设是您提供给模型的数据集将完全适合内存。我们不需要将批处理移入和移出内存。这是一个相当大的假设,尤其是如果你正在处理大数据集,而你的笔记本电脑或台式机没有太多的可用内存。此外,假设不需要进行实时数据扩充(正如我们在这里想要做的)。

注意

fit()函数适用于可以放入系统内存且不需要实时数据扩充的小型数据集。

函数的作用是

当数据不再适合内存时,我们需要一个更智能的函数来帮助我们处理它。请注意,我们之前创建的ImageDataGenerator将以随机的方式生成需要提供给模型的批次。fit_generator()函数假设有一个函数为它生成数据。使用fit_generator()时,Keras 遵循以下流程:

  1. Keras 调用生成批处理的函数。在我们的代码中,那是datagen.flow()

  2. 这个生成器函数返回一个批处理,其大小由参数batch_size=BATCH_SIZE指定。

  3. 然后,fit.generator()函数执行反向传播并更新权重。

  4. 这一过程一直重复,直到达到所需的历元数。

注意

fit_generator()函数旨在用于不适合内存的较大数据集,以及当您需要进行数据扩充时。

注意,在我们的代码中有一个重要的参数没有使用:steps_per_epochdatagen.flow()函数每次都会生成一批图像,但是 Keras 需要知道我们每个时期需要多少批图像,因为datagen.flow()可以继续生成我们需要的数量(记住它们是随机生成的)。我们需要决定在宣布每个时期结束之前需要多少批次。可以用steps_per_epoch参数决定,但是如果不指定,Keras 会用len(generator) 5 作为步数。

函数的作用是

如果您需要微调您的训练,可以使用train_on_batch()功能。

注意

train_on_batch()函数接受一批数据,执行反向传播,然后更新模型参数。

该批数据可以任意调整大小,理论上可以是您需要的任何格式。例如,当您需要执行标准 Keras 函数无法完成的自定义数据扩充时,您需要这个函数。

注意

正如他们所说——如果你不知道你是否需要train_on_batch()函数,你可能不需要。

您可以在 https://keras.io/models/sequential/ 的官方文档中找到更多信息。

训练网络

我们终于可以训练我们的网络,看看它表现如何。对其进行 50 个时期的训练,批次大小为 128,得出以下准确度:

  • 训练数据集上的准确率:93.3%

  • 验证数据集的准确率:91%

这是一个伟大的结果。实际上没有过拟合和高精度。这个网络在 Google Colab 上花了大约 15 分钟,相当快。图 8-15 显示了精度和损耗与历元数的关系。

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

图 8-15

具有迁移学习和数据扩充的 VGG16 网络的准确度和损失函数与历元数的关系

总的来说,我们从一个简单的 CNN 开始,这个 CNN 还不算太差,但是我们很快意识到越深入(更多层)和增加复杂性(更多内核)会导致过度拟合。增加辍学并没有真正的帮助,所以唯一的解决方案是使用数据增强。

请注意,由于篇幅原因,我们没有展示本章中描述的第一个具有数据扩充功能的网络,但是您应该这样做。如果你尝试,你会意识到你非常有效地对抗过度拟合,但是精度下降了。使用预先训练的网络给了我们一个非常好的起点,并允许我们在几个时期内进入 90%的准确度范围。

现在玩得开心点…

在这本书里,你学到了强大的技术,可以让你阅读研究论文,理解它们,并开始实现更先进的网络,超越你在博客和网站上找到的简单的 CNN。我希望你喜欢这本书,它将帮助你走向深度学习的掌握。深度学习真的很有趣,是一个非常有创造力的研究领域。我希望你现在对算法的可能性和其中的创造性有所了解。我喜欢反馈,也希望收到您的反馈。不要犹豫,联系我,告诉我这本书是如何(尤其是如果)帮助你学习那些算法的。

翁贝托·米其奇,杜本多夫,2019 年 6 月

Kather JN,Weis CA,比安科尼 F,Melchers SM,Schad LR,Gaiser T,Marx A,Zollner F:结肠直肠癌组织学中的多级纹理分析 (2016),科学报告(正在出版中)

2

https://www.linkedin.com/in/kevinmader/

3

来自 Python 官方文档: https://docs.python.org/2/library/pickle.html

4

在平面几何中,剪切映射是一个线性映射,它在一个固定的方向上移动每个点,移动的量与它与平行于该方向并通过原点的直线的有符号距离成比例。 https://en.wikipedia.org/wiki/Shear_mapping

5

https://keras.io/models/sequential/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值