猫或不是 - 使用 Python 和 Keras 的图像分类器






4.69/5 (7投票s)
神经网络的第一个代码介绍。
注意:本文是 CodeProject 图像分类挑战赛的一部分。
第一部分:引言
我们将使用 Python、Keras 和 Tensorflow 构建一个基于神经网络的图像分类器。利用现有数据集,我们将训练我们的神经网络来判断一张图像是否包含一只猫。
如果您是 HBO 剧集《硅谷》的粉丝,这个概念听起来可能会很熟悉。在该剧最受欢迎的剧集之一中,一位角色创建了一个名为 Not Hotdog 的应用程序——该应用程序接收一张图像,能够判断出该图像是否是一张热狗的照片。该剧的制作人使用了 Python、Keras 和 Tensorflow 来创建这个应用程序——这正是我们将要使用的技术栈。
我们的方法
这将是一个代码优先的教程。在开始处理神经网络和深度学习时,人们往往想在尝试创建任何东西之前学习所有理论。我们将采取不同的方法。
我们将直接深入代码,在遇到概念时学习它们。这不会深入探讨神经网络和深度学习的细微差别。相反,这将是一个面向开发者的、动手实践的 AI 视角。作为一名开发者,我发现这通常是学习的最佳方式:首先,从制作出能工作的東西开始。先看到实际效果,以后更容易理解其背后的理论。
关于我们将要涵盖的所有主题,已经有免费的优秀资源,我将在文章中链接到它们,供任何想深入探索的人参考。然而,本文的大部分内容将集中在编写使我们的图像分类器工作的代码上。
本文不会全面介绍神经网络、深度学习或图像分类。我将向您展示一个能取得结果的解决方案,我希望它能作为您 AI 之旅的一个良好起点。对于全面但易于理解的神经网络和深度学习介绍,我推荐斯坦福大学的 CS231n 课程笔记。
安装
安装 Python
开始很简单——我们需要安装 Python,然后使用 pip 安装几个包。请注意,最近的 Tensorflow 版本需要 64 位 Python 版本,因此请确保您下载的是 64 位版本。
我建议安装 Python 3.7——尽管 3.6 应该也可以。如果您使用的是 Windows 或 MacOS,可以从 Python 网站下载安装程序包:
运行安装程序时,请务必选择将 Python 添加到系统路径的选项。这将使您通过终端使用 Python 更加方便——我们将在本教程中经常这样做。
如果您使用的是 Linux,很可能已经安装了最新版本的 Python。如果没有,我建议通过您的系统包管理器进行更新。Linux 发行版太多,无法在此一一列出安装说明,因此 Linux 用户可能需要自行搜索或阅读文档来正确安装 Python。
安装包
Python 安装完成后,打开终端窗口(在 MacOS 和 Linux 上)或命令提示符窗口(在 Windows 上)。Windows 上的 Powershell 也可以。接下来,运行以下命令:
pip install tensorflow keras numpy pillow
这将安装完成本教程所需的所有包。有经验的 Python 用户会注意到我们是全局安装这些包,而不是使用虚拟环境。如果您知道如何设置虚拟环境,请随意这样做。
拥有 Nvidia GPU 的读者的注意事项
如果您拥有最新的 Nvidia GPU,很可能可以使用 GPU 加速版本的 Tensorflow。这将大大加快我们神经网络的训练速度。如果您拥有 GTX 9 系列或更高型号的 GPU,我强烈建议充分利用它。即使是我笔记本上普通的 GTX 1050,也能比中端 i5 CPU 快大约 50 倍的速度训练神经网络。
要利用 GPU 支持,您需要从 Nvidia 安装一些额外的软件,以及 tensorflow-gpu
pip 包。说明在此处过于复杂,无法涵盖,但 Tensorflow 网站上有一个 很好的解释,说明了使用 GPU 加速与 Tensorflow 所需的步骤。
库
让我们快速看一下我们将要使用的 Python 库。
Tensorflow
Tensorflow 是 Google 创建的一个开源机器学习库。我们不会直接使用它。相反,我们将使用 Keras,它在后台使用 Tensorflow。您也可以将 Keras 与其他后端一起使用,例如 Microsoft 的 Cognitive Toolkit。我选择 Tensorflow 是因为 Tensorflow-Keras 组合是最常用的,并且如果您遇到麻烦,也最容易找到帮助。
Pillow
Pillow 是一个易于使用的图像处理库。我们将使用它来调整我们要用来训练神经网络的图像大小。Pillow 是 PIL(Python Imaging Library)的一个分支。PIL 很棒,但它停止了更新。Pillow 项目接过了 PIL 的火炬并继续改进它。它仍然以向后兼容 PIL 为目标——这就是为什么当我们使用 Pillow 时,我们会导入 PIL。
NumPy
NumPy 是一个科学计算库,可以轻松高效地对大型数组和矩阵执行计算。
Keras
Keras 可以轻松构建和训练多种类型的神经网络。与在 Tensorflow 等低级库中执行手动计算不同,使用 Keras,我们只需使用友好的 API 定义我们的网络架构,然后将训练数据输入其中。
参赛代码
感谢您阅读到这里!本次比赛第一部分的参赛代码就是您的 CodeProject 会员号。您可以在您的 CodeProject 会员页面找到它。请 点击此处 提交您的参赛代码。在下拉框中,请务必选择第一轮。
第二部分:代码
首先,您可以在 GitHub 上找到本文所有代码的副本,网址是 https://github.com/rpeden/cat-or-not。我还包含了一个预训练模型,如果您想尝试它而不必自己训练的话。如果您没有 GPU,并且发现训练在 CPU 上耗时太长,这可能是一个好主意。
您还需要数据集来训练和测试神经网络。您也可以在 GitHub 上找到它:https://github.com/rpeden/cat-or-not/releases。.zip 文件包含一个数据目录,您必须将其解压到您克隆 cat-or-not 的目录——或者您自行创建的目录。
好了,让我们开始编写代码。在一个新目录中,创建一个名为 train.py 的文件。这将是我们训练神经网络的地方。
我们将首先添加所有需要的 import
语句
from keras.models import Sequential, load_model
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.layers.normalization import BatchNormalization
from PIL import Image
from random import shuffle, choice
import numpy as np
import os
接下来,我们定义一些常量和几个有助于我们导入图像的辅助函数
IMAGE_SIZE = 256
IMAGE_DIRECTORY = './data/test_set'
def label_img(name):
if name == 'cats': return np.array([1, 0])
elif name == 'notcats' : return np.array([0, 1])
def load_data():
print("Loading images...")
train_data = []
directories = next(os.walk(IMAGE_DIRECTORY))[1]
for dirname in directories:
print("Loading {0}".format(dirname))
file_names = next(os.walk(os.path.join(IMAGE_DIRECTORY, dirname)))[2]
for i in range(200):
image_name = choice(file_names)
image_path = os.path.join(IMAGE_DIRECTORY, dirname, image_name)
label = label_img(dirname)
if "DS_Store" not in image_path:
img = Image.open(image_path)
img = img.convert('L')
img = img.resize((IMAGE_SIZE, IMAGE_SIZE), Image.ANTIALIAS)
train_data.append([np.array(img), label])
return train_data
我们来稍微解析一下。我们首先定义代表图像大小和我们存储图像的目录的常量。目录不言自明,但为什么我们需要图像大小呢?事实证明,神经网络的输入必须长度相同。因此,我们将调整所有图像的大小以使其具有通用尺寸。
一般来说,图像尺寸越大,网络的准确性就越高。但也有一个缺点——图像尺寸越大,训练模型所需的时间就越长。如果使用仅 CPU 的 Tensorflow 版本,256 x 256 的图像尺寸将已经导致训练时间缓慢。如果您拥有强大的 GPU,可以随意尝试不同的图像尺寸,看看它对模型准确性的影响。
接下来,我们创建一个 label_img
函数。此函数为猫的图像分配标签 [1, 0]
,为非猫的图像分配标签 [0, 1]
。以这种方式将我们的图像类别编码为二进制向量称为 独热编码。这是必需的,因为神经网络通过使用数字向量和大量向量乘法来工作。它们无法处理像“cat
”或“hot dog
”这样的文本标签。
之后,我们定义一个函数,该函数遍历我们的图像目录,并从每个目录加载 200 张图像。我们只加载 200 张而不是整个数据集,因为我们大多数人使用的计算机和 GPU 无法一次处理所有图像。别担心——稍后我们将讨论如何进行额外的训练运行,使用更多图像来提高模型的准确性。
对于每张图像,我们打开它,将其转换为灰度,然后调整其大小以达到我们所需的图像尺寸。将图像转换为灰度是减少我们要处理的数据量的一种方法。减小图像尺寸和转换为灰度是已知称为降维的技术的一部分。
在这种情况下,转换为灰度是基于这样的猜测:图像中的形状和像素强度对于猫与非猫的决策比图像中的颜色更能提供有意义的信息。处理后,每张图像都会被添加到数组中。在加载完所有图像后,该数组将被返回。
接下来,我们定义一个创建我们神经网络模型的函数
def create_model():
model = Sequential()
model.add(Conv2D(32, kernel_size = (3, 3), activation='relu',
input_shape=(IMAGE_SIZE, IMAGE_SIZE, 1)))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(BatchNormalization())
model.add(Conv2D(64, kernel_size=(3,3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(BatchNormalization())
model.add(Conv2D(128, kernel_size=(3,3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(BatchNormalization())
model.add(Conv2D(128, kernel_size=(3,3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(BatchNormalization())
model.add(Conv2D(64, kernel_size=(3,3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(BatchNormalization())
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(64, activation='relu'))
model.add(Dense(2, activation = 'softmax'))
return model
让我们来看看这里发生了什么。我们首先创建一个 Keras 的 Sequential
类实例。顺序模型正是其名称所示——一种神经网络,数据将按顺序通过,按照将层添加到网络的顺序。
接下来,您会看到一个模式——我们反复添加一个 Conv2D
层,然后是一个 MaxPooling2D
层,然后是一个 BatchNormalization
层。这是构建图像分类神经网络的常见模式。虽然这些层类型的详细解释超出了本文代码优先介绍的范围,但让我们简要看一下每个层:
卷积层——此处由 Keras 的 Conv2D
类表示——根据输入向量中的每个数据点以及相邻数据点来调整每个神经元的权重。这在直觉上是有意义的;图像中的特征往往由线条和边缘定义,只有当您将一个像素与附近的像素进行比较时,这些线条和边缘才有意义。
池化层——此处由 Keras 的 MaxPooling2D
层表示——减少了训练和使用模型所需的总体计算能力,并且有助于模型泛化以学习特征,而不依赖于这些特征总是出现在图像的特定位置。这在构建猫检测器时很有用,因为理想情况下我们希望我们的分类器无论猫出现在图像的哪个位置都能识别出来。
这些解释并没有完全涵盖这些层的全部内容。如果您有兴趣了解更多,我推荐 这篇文章。
接下来,我们看到一个 BatchNormalization
层。批量归一化是一种可以大大减少训练深度神经网络所需时间的技术。我将不尝试用一段话来解释它,而是将您引荐至 这篇文章,它出色地解释了批量归一化——并配有 Keras 示例代码。
然后,我们看到一个 Dropout
层。Dropout 层会获取其看到的输入数据的一部分,并将这些值设置为零。这似乎有些反直觉——毕竟,我们刚花时间训练了这个网络。为什么要删除一些数据?实际上,dropout 层可以防止过拟合,过拟合是指神经网络对输入的学习过于“好”。当发生过拟合时,一个网络将在分类已训练图像方面表现出色,但这种准确性不会泛化,导致在分类从未见过的图像时性能不佳。
Flatten
层顾名思义,将我们之前的多维层展平成一个一维向量。这是因为我们最后添加了几个 Dense 层,它们以一维向量作为输入,并输出一个一维向量。Dense 层通常用于创建传统的、非卷积神经网络。关于不同层类型如何比较的良好讨论,我喜欢 这个 fast.ai 讨论帖。
接下来,我们将使用我们定义的函数来加载图像并训练我们的神经网络
training_data = load_data()
training_images = np.array([i[0] for i in training_data]).reshape(-1, IMAGE_SIZE, IMAGE_SIZE, 1)
training_labels = np.array([i[1] for i in training_data])
print('creating model')
model = create_model()
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
print('training model')
model.fit(training_images, training_labels, batch_size=50, epochs=10, verbose=1)
model.save("model.h5")
我们开始加载我们的训练数据。然后将其分成两个数组。首先,我们将每张图像放入一个 NumPy 数组,然后重塑数组中的图像以匹配我们网络第一层期望的数据形状。
接下来,我们将我们的独热编码标签提取到一个 NumPy 数组中。然后我们调用 create_model
函数,并调用 compile 模型以准备使用。损失函数和优化器是深度学习的重要组成部分,但为了让我们快速启动和运行,我将把它们的解释外包给 这篇文章。您也可以阅读 Keras 关于 损失函数 和优化器的文档,以了解它支持的内容。
最后,我们调用 model.fit
来使用我们加载的图像训练我们的模型。我们将传入加载的训练图像和标签,并将它们以 50 个为一批地输入到我们的模型中。我们运行 10 个 epoch 的训练,这意味着训练图像集将被输入到神经网络 10 次。
我将提前警告您——在训练集子集上进行 10 个 epoch 的训练运行不会产生一个很棒的图像分类器。我选择了一个较小的数字开始,因为我想让仅 CPU 用户能够观看他们的训练在合理的时间内完成。如果您正在使用 GPU,请随意增加 epoch 的数量和一次加载的图像数量。这将加快训练过程。
要进行额外的训练运行,请参见上面链接的 GitHub 仓库中的 retrain.py。它不会从头开始创建一个新网络,而是从磁盘加载现有网络,使用一组新的随机选择的图像进行训练,然后将结果保存到磁盘。一般来说,每次运行 retrain.py,您的网络就会变得更准确一些。您会偶尔看到准确性下降的训练运行,但不要因此而气馁,继续进行更多的训练运行。您的模型准确性会在相当长一段时间内呈上升趋势,然后才会趋于平缓。但请小心不要重新运行 train.py,除非您的目的是用一个新模型覆盖现有模型,因为那正是会发生的事情。
最后,test.py 加载测试图像——神经网络从未见过这些图像——并使用它们来测试网络的准确性。
参赛代码
要生成此部分比赛的参赛代码,您需要使用 GitHub 仓库中的 entry.py 文件。要生成代码,请运行 python entry.py 12345678
,将 12345678
替换为您的 CodeProject 会员号。请 点击此处 提交您的参赛代码。请务必在下拉框中选择第二轮。
结论
我们已经在一短时间内走了很长的路!
我们从零开始,使用 Python、Keras 和 Tensorflow 构建了一个图像分类器。我们还训练了它来判断一张图像是否为猫。
我们只是触及了可能性的皮毛。对于接下来尝试的一些想法,可以考虑在像 CIFAR-10 这样的著名数据集上训练一个网络,或者尝试使用像 ImageNet 这样的预训练模型来构建一个应用程序。
如果您好奇,可以尝试向我们刚刚构建的网络添加层,看看会发生什么。您甚至可以尝试删除层,或者修改层的参数,看看它们如何改变您得到的结果。您可能会注意到“非猫”数据集并不大,而且大部分由狗、花朵和家居用品组成。这些只是非猫的一部分。网络上有很多免费的图像数据集。另一个好的实验是使用这些数据集来增强您的“非猫”数据集,然后对您的模型进行额外的训练,看看它的准确性是否有所提高。