机器学习实战:模型构建与应用
上QQ阅读APP看书,第一时间看更新

3.5 创建一个CNN来区分马和人

本节我们将探索一个比Fashion MNIST分类器更复杂的场景。我们将对学习到的与卷积和卷积神经网络相关的知识进行扩展来分类特征位置并不固定的图像内容。为此,我创建了Horses or Humans数据集。

3.5.1 Horses or Humans 数据集

本节中的数据集(https://oreil.ly/E5kbc)包含超过1000个300×300像素的图像,马和人大约各占一半,渲染为不同的姿势。你可以在图3-7中看到几个例子。

图3-7:马和人

可以看到,每个主体包含不同的朝向和姿势,并且图像构图会变化。以这两只马为例,它们的脑袋朝向不同,其中一只被缩小到展示了整个动物,而另一只被放大到只展示了头部和一部分身体。同样,人像的光照不同,肤色不同,并且姿势不同。这个男人把手放在了他的臀部,而这个女人把她的手伸向外侧。这些图像同样包含背景,例如树和海滩,因此分类器不得不判别图像中的哪些部分是重要的特征,从而在不被背景干扰的情况下判断什么是马和什么是人。

虽然之前的例子中预测了Y=2X-1或者分类小的单色衣物图像可能可以通过传统的编程实现,但很明显这个例子更加复杂,并且你正跨过分界线,这里机器学习成为解决问题的关键。

一个有趣的边注是这些图像都是通过计算机生成的。原理是一匹马的CGI图像中所有被识别的特征都应该适用于一张真实的图像。在本章的后面你将看到它工作得有多好。

3.5.2 Keras ImageDataGenerator

目前为止你一直使用的Fashion MNIST数据集本身包含标签。每一个图像文件都有一个对应的文件包含标签细节。许多基于图像的数据集不包含这些标签文件,Horses or Humans数据集也不例外。图像是根据每个类型的子文件夹进行排序的,而不是标签。通过使用TensorFlow中的Keras,一个叫作ImageDataGenerator的工具可以使用这个结构来自动给图像分配标签。

为了使用ImageDataGenerator,你只需确保你的文件夹结构中包含一组命名的子文件夹,每个子文件夹的名字是一个标签。例如,Horses or Humans数据集是一组ZIP文件,一个包含训练数据(1000多张图像),另一个包含验证数据(256张图像)。当你下载并把它们解压缩到一个本地文件夹中用于训练和验证时,要确保它们是在一个像图3-8那样的文件结构中。

图3-8:确保图像在命名的子文件夹中

下面是得到训练数据并将它提取到适当命名的子文件夹中的代码:

这只是下载了训练数据的ZIP文件,并把它解压缩到horse-or-human/training文件夹中(我们之后很快会处理下载验证数据)。这个文件夹将包含不同图像类型的子文件夹。

为了使用ImageDataGenerator,我们只需使用下面的代码:

我们首先创建一个叫作train_datagenImageDataGenerator实例,接下来指定这将生成训练过程中所用的图像,并将这些图像传入一个文件夹中。这个文件夹是之前声明的training_dir。我们会同时指定一些与数据有关的超参数,例如目标大小(在这种情况下图像的大小是300×300,并且类的模型是binary)。如果只有两种图像类型(就像这种情况),模式就是binary;如果有多于两个类型,模式就是categorical

3.5.3 Horses or Humans的CNN架构

这个数据集和Fashion MNIST数据集之间存在一些重大差异,你在设计分类图像模型的架构时不得不考虑。首先,图像变大了许多(300×300像素),因此需要更多层。第二,图像是彩色的而不是灰度的,因此每一张图像会有三个通道而不是一个。第三,只有两个图像类型,因此我们需要的是一个二分类器,只需要一个输出神经元来实现,当它接近0时代表其中一类,1代表另一类。在探索模型架构时,记住这些要点:

这里有一些需要注意的事情。首先是第一层。我们定义了16个滤波器,每一个是3×3,但是输入图像的形状是(300,300,3)。记住这是因为输入图像的大小是300×300并且它是彩色的,因此有三个通道,而不是像我们之前使用的单色Fashion MNIST数据集只有一个通道。

在另一端,注意到输出层只有一个神经元。这是因为我们使用的是二分类器,如果使用sigmoid函数来激活它,就可以通过一个神经元来得到一个二元分类。sigmoid函数的目的是将一组数值逼近于0,而另一组逼近于1,这对于二元分类来说是完美的。

接下来,注意如何叠加更多的卷积层。这么做是因为图像源太大了,因此我们希望随着时间的推移获得更小的图像,并且图像的特征被突出。如果我们看一下model.summary的结果,会看到以下的表格:

注意,当数据通过所有的卷积和池化层之后,它最终变为7×7大小的元素。原理是这些被激活的特征图相对简单,并且只包含49像素。这些特征图接下来可以被传递到全连接的神经网络,以将它们匹配到合适的标签。

当然,这让我们拥有了比之前的网络更多的参数,因此会训练得更慢。在这个架构中,我们将学习170万个参数。

为了训练这个模型,我们需要一个损失函数和一个优化器来编译它。在这种情况下,损失函数可以是二元交叉熵,因为这里只有两个类,并且从名字可以看出这个损失函数正是为此场景设计的。我们可以尝试一个新的优化器root mean square propagation(RMSprop),它读入一个学习率(lr)参数,让我们可以改变学习的过程。代码如下:

我们通过使用fit_generator来训练模型,并将之前创建的training_generator传递给它:

这个例子可以在Colab中运行,但是如果你想让它运行在你自己的机器上,请确保你通过使用pip install pillow安装了Pillow库。

注意,通过使用TensorFlow Keras,你可以使用model.fit将训练数据拟合到训练标签。当使用生成器时,旧版本需要你使用model.fit_generator。之后版本的TensorFlow允许你使用任何一个函数。

经过仅仅15个回合,这个架构在训练集上就达到了95%的准确率。当然,这只是训练数据,并不能代表网络在其他未见过的数据上的性能。

接下来,我们会看一下使用生成器来添加验证集并度量模型的性能,以给我们一个很好的指示,看看模型在真实世界中会如何表现。

3.5.4 给Horses or Humans数据集添加验证

为了添加验证集,你需要一个与训练数据集不同的验证数据集。在某些情况下,你需要有一个自己分割的总数据集,但是在Horses or Humans数据集中,你可以下载一个单独的验证集。

equa你可能会疑惑为什么我们在这里讨论验证集而不是测试集,以及它们是否相同。对于简单模型,通常把数据分为训练部分和测试部分就足够了。但是对于复杂模型,则需要创建独立的验证集和测试集。区别是什么呢?训练数据用于教神经网络如何将数据和标签匹配在一起。验证数据用于在训练过程中查看模型在之前未见过的数据上表现如何—即,它不是用于将数据拟合到标签,而是检查拟合的过程进行得怎么样。测试数据在训练之后使用,查看网络在之前未见过的数据上表现如何。有些数据集包含三种分割,在其他情况下你会想把测试集分为测试部分和验证部分。因此,你需要下载更多的图像来测试模型。

你可以使用与训练图像非常相似的代码来下载验证集并把它解压缩到不同的文件夹中:

有了验证数据,你可以设置另一个ImageDataGenerator来管理这些图像:

为了让TensorFlow帮你完成验证,你只需要更新model.fit_generator函数,来指定你想使用这些验证数据来在每一个回合中测试模型。你可以使用validation_data参数并把它传入刚创建的验证生成器:

训练了15个回合之后,你可以看到模型在训练集上达到了超过99%的准确率,但是在验证集上只有88%。这表示模型过拟合了。

尽管如此,考虑到训练所使用的图像太少,且这些图像都不同,这个表现并不算差。由于缺少数据,你开始陷入困境,但是仍然有许多方法可以提升模型的性能。现在我们先看一下如何使用这个模型。

3.5.5 测试Horses or Humans图像

能够创建一个模型就很好了,但是你当然想要试一试它。当我开始AI旅程时遇到了一个很大的挫折,就是我可以找到很多展示如何创建模型的代码以及展示模型如何表现的图表,但是很难找到代码来测试模型。我在本书中会尽量避免这种情况!

测试模型最容易的方法是使用Colab。我已经在GitHub上提供了一个Horses or Humans notebook,你可以在Colab中直接打开(http://bit.ly/horsehuman)。

当你训练完模型,会看到一节叫作“Running the Model”。在运行它之前,在网上找几张马或者人的照片,并把它们下载到你的计算机中。Pixabay.com是一个很好的网站,提供了许多免费的图像。记得首先将你的测试图像放在一起,因为在你搜索的时候,Colab中的节点可能会超时。

图3-9展示了几张我从Pixabay.com下载的照片。

图3-9:测试图像

如图3-10所示,上传它们之后,模型可以正确地将第一张图像分类为人并将第三张图分类为马,但是中间那张图像,即使很明显是一个人,却被错误地分类为马!

图3-10:运行模型

你还可以同时上传多张图像,让模型对所有的图像进行预测。你可能会发现模型倾向于过拟合为马。如果人并不是完整的形态(即你无法看到整个人的身体),那图像可能会被预测为马。这就是现在这个例子所发生的情况。第一个人类模型是完全可见的,并且数据集中包含许多类似姿势的图像,因此模型可以将她正确分类。第二个模特正对摄像机,但是只有她的上半身图像,训练数据中没有这样的图像,因此模型无法正确识别她。

现在我们来探索一些代码:

这里,我们加载Colab中所写的路径中的图像。值得注意的是,我们指定目标大小是300×300。正在上传的图像可能是任意形状的,但是我们将要把它传送到模型中,它们必须是300×300,因为那是模型被训练时所要识别的大小。因此,第一行代码加载图像并把它变为300×300的大小。

下一行代码将图像转换为一个2D数组。但是模型期待的是3D数组,就像模型架构中input_shape所指定的那样。幸运的是,Numpy提供了expand_dims函数来处理这种情况,并允许我们很容易地给一个数组添加一维。

现在我们的图像是一个3D数组,只需要确保它是竖直叠加在一起的,这样它就和训练数据的形状一样:

有了正确格式的图像,分类就很简单:

模型返回一个包含分类信息的数组。由于在这个例子中只有一个分类,所以它实际上是一个包含一个数组的数组。你可以在图3-10中看到,对于第一个(人类)模型它看起来像[[1.]]

因此现在只需要检查那个数组中的第一个元素的值。如果它大于0.5,那么我们看到的就是人类:

这里需要考虑几个重要的点。首先,即使网络在合成的、计算机生成的图像上训练,它在真实的照片中也可以非常好地识别出马或者人类。这是一个潜在的福利,你不需要几千张照片来训练一个模型,可以使用CGI来相对简便地实现。

但是这个数据集同样展示了一个你会遇到的根本性问题。你的训练集无法代表你的模型在现实中可能会遇到的每一种情况,因此模型总会有一定程度的偏差。这里有一个清晰简单的例子,图3-9中间的那个人被错误地分类了。训练集不包含一个那样姿势的人,因此模型无法“学习”出一个人可以看起来像那个样子。因此,它很有可能会把人看作一匹马,在这个例子中,它确实这么做了。

解决方法是什么呢?显而易见的一种方法是添加更多的训练数据,特别是那个特定姿势的人以及在一开始没有被代表的其他人,即使这并不一定是可行的。幸运的是,在TensorFlow中有一个简洁的技巧可以用于虚拟地扩展你的数据集,它叫作图像增强。