原文:
annas-archive.org/md5/b038bef44808c012b36120474e9e0841
译者:飞龙
第四章:构建深度卷积神经网络
在本章中,我们将讨论以下内容:
-
传统神经网络在图像平移时的不准确性
-
使用 Python 从零开始构建 CNN
-
使用 CNN 改善图像平移时的准确性
-
使用 CNN 进行性别分类
-
数据增强以提高网络准确性
介绍
在前一章节中,我们介绍了传统的深度前馈神经网络。传统深度前馈神经网络的一个局限性是它不具有平移不变性,即图像右上角的猫图像会被认为与图像中央的猫图像不同。此外,传统神经网络受物体尺度的影响。如果物体在大多数图像中占据较大的位置,而新图像中的物体较小(占据图像的较小部分),传统神经网络很可能在分类图像时失败。
卷积神经网络(CNNs)用于解决这些问题。由于 CNN 能够处理图像中的平移以及图像的尺度问题,因此在物体分类/检测中被认为更为有效。
在本章中,你将学习以下内容:
-
传统神经网络在图像平移时的不准确性
-
使用 Python 从零开始构建 CNN
-
使用 CNN 改善 MNIST 数据集上的图像分类
-
实现数据增强以提高网络准确性
-
使用 CNN 进行性别分类
传统神经网络在图像平移时的不准确性
为了进一步理解 CNN 的必要性,我们将首先了解为什么当图像被平移时,前馈神经网络(NN)不起作用,然后看看 CNN 是如何改进传统前馈神经网络的。
我们来看看以下场景:
-
我们将构建一个神经网络模型来预测 MNIST 数据集中的标签
-
我们将考虑所有标签为 1 的图像,并对它们求平均(生成一张平均的 1 标签图像)
-
我们将使用传统的神经网络预测我们在上一步生成的平均 1 标签图像的标签
-
我们将把平均 1 标签图像平移 1 个像素到左边或右边
-
我们将使用传统神经网络模型对平移后的图像进行预测
如何做到…
上述定义的策略代码如下(请参考 GitHub 中的Issue_with_image translation.ipynb
文件以实现代码)
- 下载数据集并提取训练集和测试集的 MNIST 数据集:
from keras.datasets import mnist
from keras.layers import Flatten, Dense
from keras.models import Sequential
import matplotlib.pyplot as plt
%matplotlib inline
(X_train, y_train), (X_test, y_test) = mnist.load_data()
- 获取对应标签
1
的训练集:
X_train1 = X_train[y_train==1]
- 重新调整和标准化原始训练数据集:
num_pixels = X_train.shape[1] * X_train.shape[2]
X_train = X_train.reshape(X_train.shape[0],num_pixels).astype('float32')
X_test = X_test.reshape(X_test.shape[0],num_pixels).astype('float32')
X_train = X_train / 255
X_test = X_test / 255
- 对输出标签进行独热编码:
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_train.shape[1]
- 构建模型并进行拟合:
model = Sequential()
model.add(Dense(1000, input_dim=num_pixels, activation='relu'))
model.add(Dense(num_classes, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam',metrics=['accuracy'])
model.fit(X_train, y_train, validation_data=(X_test, y_test),epochs=5, batch_size=1024, verbose=1)
- 让我们绘制在第二步中获得的平均 1 标签图像:
pic=np.zeros((28,28))
pic2=np.copy(pic)
for i in range(X_train1.shape[0]):
pic2=X_train1[i,:,:]
pic=pic+pic2
pic=(pic/X_train1.shape[0])
plt.imshow(pic)
在前面的代码中,我们初始化了一个 28x28 大小的空图像,并通过遍历X_train1
对象中的所有值,在标记为 1 的图像的不同像素位置取了平均像素值。
平均 1 图像的绘图如下所示:
需要注意的是,像素越黄色(越厚),人们在该像素上书写的次数越多,而像素越不黄色(更蓝/更薄),人们在该像素上书写的次数就越少。还需要注意的是,图像中央的像素是最黄/最厚的(这是因为大多数人都会在中间的像素上书写,无论整个数字是垂直书写还是向左或向右倾斜)。
传统神经网络的问题
情境 1:让我们创建一个新图像,其中原始图像向左平移了 1 个像素。在下面的代码中,我们遍历图像的列,并将下一列的像素值复制到当前列:
for i in range(pic.shape[0]):
if i<20:
pic[:,i]=pic[:,i+1]
plt.imshow(pic)
左侧翻译后的平均 1 图像如下所示:
让我们继续使用构建好的模型预测图像的标签:
model.predict(pic.reshape(1,784)/255)
模型对翻译后的图像的预测结果如下所示:
我们可以看到预测为 1,尽管它的概率低于像素未翻译时的预测。
情境 2:创建一个新图像,其中原始平均 1 图像的像素向右平移了 2 个像素:
pic=np.zeros((28,28))
pic2=np.copy(pic)
for i in range(X_train1.shape[0]):
pic2=X_train1[i,:,:]
pic=pic+pic2
pic=(pic/X_train1.shape[0])
pic2=np.copy(pic)
for i in range(pic.shape[0]):
if ((i>6) and (i<26)):
pic[:,i]=pic2[:,(i-1)]
plt.imshow(pic)
右侧翻译后的平均 1 图像如下所示:
这张图像的预测结果如下所示:
model.predict(pic.reshape(1,784)/255)
模型对翻译后的图像的预测结果如下所示:
我们可以看到预测结果不正确,输出为 3。这正是我们通过使用 CNN 来解决的问题。
使用 Python 从零开始构建 CNN
在本节中,我们将通过使用 NumPy 从零开始构建一个前馈网络,学习 CNN 是如何工作的。
准备工作
典型的 CNN 有多个组成部分。在本节中,我们将在理解 CNN 如何改善图像翻译预测准确性之前,了解 CNN 的各个组件。
理解卷积
我们已经了解了典型神经网络是如何工作的。在本节中,让我们理解 CNN 中卷积过程的工作原理。
滤波器
卷积是两个矩阵之间的乘法——一个矩阵较大,另一个较小。为了理解卷积,考虑以下例子:
矩阵 A 如下所示:
矩阵 B 如下所示:
在执行卷积操作时,可以将其视为将较小的矩阵滑动到较大的矩阵上,即在较大的矩阵区域内滑动时,可能会出现九种这样的乘法。请注意,这不是矩阵乘法。
较大矩阵与较小矩阵之间的各种乘法如下:
- {1, 2, 5, 6} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
11 + 22 + 53 + 64 = 44
- {2, 3, 6, 7} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
21 + 32 + 63 + 74 = 54
- {3, 4, 7, 8} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
31 + 42 + 73 + 84 = 64
- {5, 6, 9, 10} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
51 + 62 + 93 + 104 = 84
- {6, 7, 10, 11} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
61 + 72 + 103 + 114 = 94
- {7, 8, 11, 12} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
71 + 82 + 113 + 124 = 104
- {9, 10, 13, 14} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
91 + 102 + 133 + 144 = 124
- {10, 11, 14, 15} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
101 + 112 + 143 + 154 = 134
- {11, 12, 15, 16} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
111 + 122 + 153 + 164 = 144
前述步骤的结果将是以下矩阵:
通常,较小的矩阵称为滤波器或卷积核,滤波器的数值通过梯度下降统计得到。滤波器中的数值是其组成权重。
实际上,当图像输入形状为 224 x 224 x 3 时,其中有 3 个通道,一个 3 x 3 的滤波器也会有 3 个通道,这样就能进行矩阵乘法(求和积)。
一个滤波器的通道数与其乘以的矩阵的通道数相同。
步幅
在前述步骤中,由于滤波器每次水平和垂直移动一步,因此滤波器的步幅为 (1, 1)。步幅数值越大,跳过的矩阵乘法值就越多。
填充
在前述步骤中,我们遗漏了将滤波器的最左边值与原矩阵的最右边值相乘。如果我们执行这样的操作,我们需要确保在原矩阵的边缘周围进行零填充(即图像边缘填充零)。这种填充方式称为 有效 填充。我们在 理解卷积 配方的 滤波器 部分进行的矩阵乘法是 相同 填充的结果。
从卷积到激活
在传统的神经网络中,隐藏层不仅通过权重乘以输入值,还对数据应用非线性处理,即将值通过激活函数传递。
在典型的卷积神经网络中也会发生类似的活动,其中卷积通过激活函数处理。CNN 支持我们目前见过的传统激活函数:sigmoid、ReLU、tanh 和 leaky ReLU。
对于前面的输出,我们可以看到当通过 ReLU 激活函数时,输出保持不变,因为所有数字都是正数。
从卷积激活到池化
在前一部分,我们研究了卷积是如何工作的。在这一部分,我们将了解卷积之后的典型下一步:池化。
假设卷积步骤的输出如下(我们不考虑前面的例子,这是一个新的例子,仅用于说明池化是如何工作的):
在前面的情况下,卷积步骤的输出是一个 2 x 2 矩阵。最大池化会考虑这个 2 x 2 块,并将最大值作为输出。同样,假设卷积步骤的输出是一个更大的矩阵,如下所示:
最大池化将大矩阵分成不重叠的 2 x 2 块(当步幅值为 2 时),如下所示:
从每个块中,只有具有最大值的元素被选中。所以,前面矩阵的最大池化操作输出将是以下内容:
在实践中,并不总是需要一个 2 x 2 的窗口,但它比其他类型的窗口更常用。
其他类型的池化包括求和和平均池化——在实践中,与其他类型的池化相比,我们看到最大池化的应用更多。
卷积和池化是如何帮助的?
在 MNIST 示例中,传统神经网络的一个缺点是每个像素都与一个独特的权重相关联。
因此,如果一个相邻的像素(而不是原始像素)被突出显示,而不是原始像素,那么输出就不会非常准确(比如场景 1中的例子,平均值稍微偏左于中心)。
现在这个问题得到了处理,因为像素共享在每个过滤器中构成的权重。
所有像素都与构成滤波器的所有权重相乘。在池化层中,仅选择卷积后的值较大的值。
这样,无论突出显示的像素是否位于中心,或者稍微偏离中心,输出通常都会是预期的值。
然而,当突出显示的像素远离中心时,问题依然存在。
如何操作…
为了更好地理解,我们将使用 Keras 构建基于 CNN 的架构,并通过从头构建 CNN 的前馈传播部分,与使用 Keras 得到的输出进行对比,来验证我们对 CNN 工作原理的理解。
让我们用一个玩具示例来实现 CNN,其中输入和期望的输出数据已定义(代码文件在 GitHub 上可用,名为 CNN_working_details.ipynb
):
- 创建输入和输出数据集:
import numpy as np
X_train=np.array([[[1,2,3,4],[2,3,4,5],[5,6,7,8],[1,3,4,5]],
[[-1,2,3,-4],[2,-3,4,5],[-5,6,-7,8],[-1,-3,-4,-5]]])
y_train=np.array([0,1])
在前面的代码中,我们创建了数据,其中正输入输出 0
,负输入输出 1
。
- 缩放输入数据集:
X_train = X_train / 8
- 重塑输入数据集,使得每个输入图像以宽度
x
高度x
通道数的格式表示:
X_train = X_train.reshape(X_train.shape[0],X_train.shape[1],X_train.shape[1],1 ).astype('float32')
- 构建模型架构:
导入相关方法后实例化模型:
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense
from keras.models import Sequential
model = Sequential()
在下一步中,我们执行卷积操作:
model.add(Conv2D(1, (3,3), input_shape=(4,4,1),activation='relu'))
在前一步中,我们对输入数据执行了二维卷积(在 理解卷积 章节中看到的矩阵乘法),其中使用了 1 个 3 × 3 大小的滤波器。
此外,鉴于这是模型实例化后的第一层,我们指定了输入形状,即 (4, 4, 1)。
最后,我们对卷积的输出执行 ReLU 激活。
在这种情况下,卷积操作的输出形状为 2 × 2 × 1,因为权重与输入的矩阵乘法会得到一个 2 × 2 的矩阵(假设默认步长为 1 × 1)。
此外,输出的大小会缩小,因为我们没有对输入进行填充(即在输入图像周围添加零)。
在下一步中,我们添加一个执行最大池化操作的层,具体如下:
model.add(MaxPooling2D(pool_size=(2, 2)))
我们对来自上一层的输出执行最大池化操作,池化大小为 2 × 2。这意味着计算图像中 2 × 2 部分的最大值。
请注意,在池化层中使用 2 × 2 的步长,在这种情况下不会影响输出,因为前一步的输出是 2 × 2。然而,一般来说,步长大于 1 × 1 的情况会影响输出形状。
让我们展平池化层的输出:
model.add(Flatten())
一旦我们执行展平操作,过程就变得非常类似于我们在标准前馈神经网络中所执行的操作,在这种网络中,输入与隐藏层连接,再到输出层(我们也可以将输入连接到更多的隐藏层!)。
我们将展平层的输出直接连接到输出层,并使用 sigmoid 激活:
model.add(Dense(1, activation='sigmoid'))
可以获得模型的总结,结果如下:
model.summary()
输出的总结如下:
请注意,卷积层中有 10 个参数,因为一个 3 x 3 的滤波器会有 9 个权重和 1 个偏置项。池化层和展平层没有任何参数,因为它们要么在某个区域提取最大值(最大池化),要么展平上一层的输出(展平层),因此在这些层中没有需要修改权重的操作。
输出层有两个参数,因为展平层有一个输出,该输出连接到输出层,输出层有一个值——因此我们将有一个权重和一个偏置项连接展平层和输出层。
- 编译并训练模型:
model.compile(loss='binary_crossentropy', optimizer='adam',metrics=['accuracy'])
在前面的代码中,我们将损失函数指定为二元交叉熵,因为输出结果要么是1
,要么是0
。
- 训练模型:
model.fit(X_train, y_train, epochs = 500)
我们正在训练模型,以获得将输入层与输出层连接的最优权重。
验证 CNN 输出
现在我们已经训练了模型,让我们通过实现 CNN 的前向传播部分来验证我们从模型中获得的输出:
- 让我们提取权重和偏置呈现的顺序:
model.weights
你可以看到卷积层的权重首先被展示,然后是偏置,最后是输出层中的权重和偏置。
还请注意,卷积层中的权重形状是(3, 3, 1, 1),因为滤波器的形状是 3 x 3 x 1(因为图像是三维的:28 x 28 x 1),最后的 1(形状中的第四个值)表示在卷积层中指定的滤波器数量。
如果我们在卷积中指定了 64 个滤波器,则权重的形状将是 3 x 3 x 1 x 64。
类似地,如果卷积操作是在具有 3 个通道的图像上执行的,则每个滤波器的形状将是 3 x 3 x 3。
- 提取各层的权重值:
model.get_weights()
- 让我们提取第一个输入的输出,以便我们能够通过前向传播验证它:
model.predict(X_train[0].reshape(1,4,4,1))
我们运行的迭代输出为 0.0428(当你运行模型时,这个值可能会不同,因为权重的随机初始化可能不同),我们将通过执行矩阵乘法来验证它。
我们在将输入传递给预测方法时正在重新调整输入的形状,因为该方法期望输入的形状为(None, 4, 4, 1),其中 None 表示批次大小可以是任意数字。
- 执行滤波器与输入图像的卷积操作。请注意,输入图像的形状是 4 x 4,而滤波器的形状是 3 x 3。在这里,我们将在代码中沿着行和列执行矩阵乘法(卷积):
sumprod = []
for i in range(X_train[0].shape[0]-model.get_weights()[0].shape[0]+1):
for j in range(X_train[0].shape[0]-model.get_weights()[0].shape[0]+1):
img_subset = np.array(X_train[0,i:(i+3),j:(j+3),0])
filter = model.get_weights()[0].reshape(3,3)
val = np.sum(img_subset*filter) + model.get_weights()[1]
sumprod.append(val)
在前面的代码中,我们初始化了一个名为sumprod
的空列表,用来存储每次滤波器与图像子集(图像子集的大小与滤波器一致)进行矩阵乘法的输出。
- 重新调整
sumprod
的输出形状,以便将其传递给池化层:
sumprod= np.array(sumprod).reshape(2,2,1)
- 在将卷积输出传递到池化层之前,先对其进行激活操作:
sumprod = np.where(sumprod>0,sumprod,0)
- 将卷积输出传递到池化层。然而,在当前的情况下,由于卷积输出是 2 x 2,我们将简单地取出在 第 6 步 中获得的输出的最大值:
pooling_layer_output = np.max(sumprod)
- 将池化层的输出连接到输出层:
intermediate_output_value = pooling_layer_output*model.get_weights()[2]+model.get_weights()[3]
我们将池化层的输出与输出层的权重相乘,并加上输出层的偏置。
- 计算 sigmoid 输出:
1/(1+np.exp(-intermediate_output_value))
前一步操作的输出如下:
你在这里看到的输出将与我们使用 model.predict
方法获得的输出相同,从而验证我们对 CNN 工作原理的理解。
CNN 用于提高图像平移情况下的准确性
在前面的章节中,我们学习了图像平移问题以及 CNN 是如何工作的。在这一节中,我们将利用这些知识,学习 CNN 如何通过改进预测精度来处理图像平移。
准备中
我们将采用的构建 CNN 模型的策略如下:
-
由于输入形状为 28 x 28 x 1,滤波器的大小应为 3 x 3 x 1:
- 请注意,滤波器的大小可以变化,但通道的数量不能变化
-
让我们初始化 10 个滤波器
-
我们将在前一步中对输入图像进行 10 个滤波器卷积得到的输出上执行池化操作:
- 这将导致图像尺寸的减半
-
我们将展平池化操作后的输出
-
展平层将连接到另一个具有 1,000 个单元的隐藏层
-
最后,我们将隐藏层连接到输出层,其中有 10 个可能的类别(因为有 10 个数字,从 0 到 9)
一旦我们构建好模型,我们将对平均 1 图像 1 像素进行平移,然后测试 CNN 模型在平移图像上的预测结果。请注意,在这种情况下,前馈神经网络架构无法预测正确的类别。
如何实现…
让我们通过代码理解如何在 MNIST 数据上使用 CNN(代码文件可在 GitHub 上找到,文件名为 CNN_image_translation.ipynb
):
- 加载并预处理数据:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train = X_train.reshape(X_train.shape[0],X_train.shape[1],X_train.shape[1],1 ).astype('float32')
X_test = X_test.reshape(X_test.shape[0],X_test.shape[1],X_test.shape[1],1).astype('float32')
X_train = X_train / 255
X_test = X_test / 255
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_test.shape[1]
请注意,我们在此步骤中执行的所有步骤与我们在 第二章 构建深度前馈神经网络 中所执行的相同。
- 构建并编译模型:
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense
from keras.models import Sequential
model = Sequential()
model.add(Conv2D(10, (3,3), input_shape=(28, 28,1),activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(1000, activation='relu'))
model.add(Dense(num_classes, activation='softmax'))
我们在前面的代码中初始化的模型的摘要如下:
model.summary()
模型摘要如下:
卷积层总共有 100 个参数,因为有 10 个 3 x 3 x 1 的滤波器,总共有 90 个权重参数。另外,10 个偏置项(每个滤波器一个)加起来形成卷积层的 100 个参数。
请注意,最大池化没有任何参数,因为它是从 2 × 2 大小的区域内提取最大值。
- 训练模型:
model.fit(X_train, y_train, validation_data=(X_test, y_test),epochs=5, batch_size=1024, verbose=1)
前述模型在 5 个训练周期中达到了 98% 的准确率:
- 让我们识别出平均的 1 张图像,然后将其平移
1
个单位:
X_test1 = X_test[y_test[:,1]==1]
在前述代码中,我们筛选出了所有标签为 1
的图像输入:
import numpy as np
pic=np.zeros((28,28))
pic2=np.copy(pic)
for i in range(X_test1.shape[0]):
pic2=X_test1[i,:,:,0]
pic=pic+pic2
pic=(pic/X_test1.shape[0])
在前面的代码中,我们取了平均的 1 张图像:
for i in range(pic.shape[0]):
if i<20:
pic[:,i]=pic[:,i+1]
在前述代码中,我们将平均的 1 张图像中的每个像素向左平移 1 个单位。
- 对翻译后的 1 张图像进行预测:
model.predict(pic.reshape(1,28,28,1))
前一步的输出结果如下:
请注意,当前使用 CNN 进行预测时,相较于深度前馈神经网络模型(在 传统神经网络在图像翻译时的不准确性 部分预测为 0.6335),其预测值(0.9541)对标签 1
的概率更高。
使用 CNN 进行性别分类
在前面的章节中,我们学习了 CNN 是如何工作的,以及 CNN 是如何解决图像翻译问题的。
在本节中,我们将通过构建一个模型,进一步了解 CNN 是如何工作的,目的是检测图像中人物的性别。
准备就绪
在本节中,我们将制定如何解决该问题的策略:
-
我们将收集图像数据集,并根据图像中人物的性别对每张图像进行标签
-
我们只会处理 2,000 张图像,因为数据获取过程对我们的数据集来说耗时较长(因为在这个案例中我们是手动从网站下载图像)。
-
此外,我们还将确保数据集中男性和女性图像的比例相等。
-
一旦数据集准备好,我们将把图像调整为相同的大小,以便它们可以输入到 CNN 模型中。
-
我们将构建 CNN 模型,输出层的类别数为两个标签的数量
-
鉴于这是一个从数据集中预测两个标签之一的案例,我们将最小化二元交叉熵损失。
如何实现…
在本节中,我们将编码之前定义的策略(代码文件已上传至 GitHub,文件名为 Gender classification.ipynb
):
- 下载数据集:
$ wget https://d1p17r2m4rzlbo.cloudfront.net/wp-content/uploads/2017/04/a943287.csv
- 加载数据集并检查其内容:
import pandas as pd, numpy as np
from skimage import io
# Location of file is /content/a943287.csv
# be sure to change to location of downloaded file on your machine
data = pd.read_csv('/content/a943287.csv')
data.head()
数据集中的一些关键字段示例如下:
- 从数据集中提供的 URL 链接获取 1,000 张男性图像和 1,000 张女性图像:
data_male = data[data['please_select_the_gender_of_the_person_in_the_picture']=="male"].reset_index(drop='index')
data_female = data[data['please_select_the_gender_of_the_person_in_the_picture']=="female"].reset_index(drop='index')
final_data = pd.concat([data_male[:1000],data_female[:1000]],axis=0).reset_index(drop='index')
在前述代码中,final_data
包含了 1,000 张男性图像和 1,000 张女性图像的 URL 链接。读取这些 URL 链接并获取对应的图像。确保所有图像的尺寸为 300 × 300 × 3(因为该数据集中大多数图像都是这个尺寸),并且处理任何禁止访问的问题:
x = []
y = []
for i in range(final_data.shape[0]):
try:
image = io.imread(final_data.loc[i]['image_url'])
if(image.shape==(300,300,3)):
x.append(image)
y.append(final_data.loc[i]['please_select_the_gender_of_the_person_in_the_picture'])
except:
continue
输入样本及其对应的情感标签如下所示:
- 创建输入和输出数组:
x2 = []
y2 = []
for i in range(len(x)):
img = cv2.cvtColor(x[i], cv2.COLOR_BGR2GRAY)
img2 = cv2.resize(img,(50,50))
x2.append(img2)
img_label = np.where(y[i]=="male",1,0)
y2.append(img_label)
在前面的步骤中,我们已经将彩色图像转换为灰度图像,因为图像的颜色可能会增加额外的信息(我们将在第五章,迁移学习中验证这个假设)。
此外,我们将图像调整为较小的尺寸(50 x 50 x 1)。结果如下所示:
最后,我们将输出转换为一热编码版本。
- 创建训练集和测试集。首先,我们将输入和输出列表转换为数组,然后调整输入的形状,使其能够作为 CNN 的输入:
x2 = np.array(x2)
x2 = x2.reshape(x2.shape[0],x2.shape[1],x2.shape[2],1)
Y = np.array(y2)
x2
的第一个值的输出如下:
请注意,输入的值在0
到255
之间,因此我们必须对其进行缩放:
X = np.array(x2)/255
Y = np.array(y2)
最后,我们将输入和输出数组分割成训练集和测试集:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,Y, test_size=0.1, random_state=42)
print(X_train.shape, X_test.shape, y_train.shape, y_test.shape)
训练和测试输入、输出数组的形状如下:
- 构建并编译模型:
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense
from keras.models import Sequential
model = Sequential()
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu',input_shape=(50,50,1)))
model.add(MaxPooling2D(pool_size=(5, 5)))
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(256, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(512, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(Flatten())
model.add(Dense(100, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
模型的总结如下:
请注意,卷积层输出的通道数将等于该层中指定的过滤器数量。此外,我们对第一个卷积层的输出进行了稍微更激进的池化。
现在,我们将编译模型,以最小化二元交叉熵损失(因为输出只有两个类别),如下所示:
model.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
- 拟合模型:
history = model.fit(X_train, y_train, batch_size=32, epochs=50,verbose=1,validation_data = (X_test, y_test))
一旦我们拟合模型,就可以看到之前的代码在预测图像中的性别时,准确率约为 80%。
还有更多内容…
可以通过以下方法进一步提高分类的准确性:
-
处理更多图像
-
处理更大的图像(而不是 50 x 50 的图像),这些图像将用于训练更大的网络
-
利用迁移学习(将在第五章中讨论,迁移学习)
-
通过正则化和丢弃法避免过拟合
数据增强以提高网络准确率
如果图像从原始位置移动,则很难准确分类图像。然而,给定一张图像,无论我们如何平移、旋转或缩放图像,图像的标签保持不变。数据增强是一种从给定图像集创建更多图像的方法,即通过旋转、平移或缩放它们,并将它们映射到原始图像的标签。
这个直觉如下:即使图像稍微旋转,或者图像中的人从图像中间移到图像的最右边,图像仍然会对应于该人。
因此,我们应该能够通过旋转和平移原始图像来创建更多的训练数据,而我们已经知道每个图像对应的标签。
准备工作
在这个示例中,我们将使用 CIFAR-10 数据集,该数据集包含 10 个不同类别的物体图像。
我们将使用的策略如下:
-
下载 CIFAR-10 数据集
-
预处理数据集
-
对输入值进行缩放
-
对输出类别进行独热编码
-
-
构建一个包含多个卷积和池化层的深度 CNN
-
编译并拟合模型,测试其在测试数据集上的准确性
-
生成训练数据集中原始图像的随机平移
-
对在上一步中构建的相同模型架构进行拟合,使用全部图像(生成的图像加上原始图像)
-
检查模型在测试数据集上的准确性
我们将使用ImageDataGenerator
方法在keras.preprocessing.image
包中实现数据增强。
如何操作…
为了理解数据增强的好处,让我们通过一个例子来计算 CIFAR-10 数据集在使用和不使用数据增强情况下的准确性(代码文件在 GitHub 中以Data_augmentation_to_improve_network_accuracy.ipynb
提供)。
无数据增强的模型准确性
让我们在以下步骤中计算无数据增强的准确性:
- 导入包和数据:
from matplotlib import pyplot as plt
%matplotlib inline
import numpy as np
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.layers.normalization import BatchNormalization
from keras import regularizers
from keras.datasets import cifar10
(X_train, y_train), (X_val, y_val) = cifar10.load_data()
- 预处理数据:
X_train = X_train.astype('float32')/255.
X_val = X_val.astype('float32')/255.
n_classes = 10
y_train = np_utils.to_categorical(y_train, n_classes)
y_val = np_utils.to_categorical(y_val, n_classes)
以下是图像样本及其对应标签:
- 构建并编译模型:
input_shape = X_train[0].shape
model = Sequential()
model.add(Conv2D(32, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay), input_shape=X_train.shape[1:]))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.2))
model.add(Conv2D(64, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(64, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.3))
model.add(Conv2D(128, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(128, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.4))
model.add(Flatten())
model.add(Dense(10, activation='softmax'))
from keras.optimizers import Adam
adam = Adam(lr = 0.01)
model.compile(loss='categorical_crossentropy', optimizer=adam,metrics=['accuracy'])
我们使用较高的学习率仅仅是为了让模型在更少的轮次内更快地收敛。这使得我们能够更快速地比较数据增强场景与非数据增强场景。理想情况下,我们会使用较小的学习率让模型运行更多的轮次。
- 拟合模型:
model.fit(X_train, y_train, batch_size=32,epochs=10, verbose=1, validation_data=(X_val, y_val))
该网络的准确率约为 66%:
使用数据增强的模型准确性
在以下代码中,我们将实现数据增强:
- 使用
ImageDataGenerator
方法在keras.preprocessing.image
包中:
from keras.preprocessing.image import ImageDataGenerator
datagen = ImageDataGenerator(
rotation_range=20,
width_shift_range=0,
height_shift_range=0,
fill_mode = 'nearest')
datagen.fit(X_train)
在前面的代码中,我们正在生成新图像,这些图像会在 0 到 20 度之间随机旋转。经过数据生成器处理后的图像样本如下:
注意,与之前的图像集相比,这些图像略微倾斜。
- 现在,我们将通过数据生成器将所有数据传递出去,如下所示:
batch_size = 32
model = Sequential()
model.add(Conv2D(32, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay), input_shape=X_train.shape[1:]))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.2))
model.add(Conv2D(64, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(64, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.3))
model.add(Conv2D(128, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(128, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.4))
model.add(Flatten())
model.add(Dense(10, activation='softmax'))
from keras.optimizers import Adam
adam = Adam(lr = 0.01)
model.compile(loss='categorical_crossentropy', optimizer=adam, metrics=['accuracy'])
- 请注意,我们正在重建模型,以便在比较数据增强和非数据增强场景时再次初始化权重:
model.fit_generator(datagen.flow(X_train, y_train, batch_size=batch_size),steps_per_epoch=X_train.shape[0] // batch_size, epochs=10,validation_data=(X_val,y_val))
请注意,fit_generator
方法会在生成新图像的同时拟合模型。
- 此外,
datagen.flow
指定了根据我们在步骤1中初始化的数据生成策略需要生成新的训练数据点。与此同时,我们还指定了每个 epoch 的步数,作为总数据点数与批次大小的比例:
这个代码的准确率约为 80%,比仅使用给定数据集(不进行数据增强)时的 66%准确率更高。
第五章:迁移学习
在上一章中,我们学习了如何识别图像属于哪个类别。本章将介绍 CNN 的一个缺点,以及如何通过使用某些预训练模型来克服这一问题。
本章将涵盖以下内容:
-
使用 CNN 进行图像中人物的性别分类
-
使用基于 VGG16 架构的模型进行图像中人物的性别分类
-
可视化神经网络中间层的输出
-
使用基于 VGG19 架构的模型进行图像中人物的性别分类
-
使用基于 ResNet 架构的模型进行性别分类
-
使用基于 Inception 架构的模型进行性别分类
-
检测人脸图像中的关键点
使用卷积神经网络(CNN)进行图像中人物的性别分类
为了了解 CNN 的一些局限性,让我们通过一个示例来识别给定图像是猫还是狗。
准备就绪
通过以下步骤,我们将直观地了解卷积神经网络如何预测图像中物体的类别:
-
卷积滤波器由图像的某些部分激活:
- 例如,某些滤波器可能会在图像具有某种模式时激活——例如,图像包含一个圆形结构
-
池化层确保图像平移问题得以处理:
- 这确保了即使图像较大,通过多次池化操作,图像的大小变小,物体也可以被检测到,因为物体现在应该出现在图像的较小部分(因为它已经多次池化)
-
最终的扁平层将所有通过不同卷积和池化操作提取的特征进行扁平化
假设训练数据集中的图像数量很少。在这种情况下,模型没有足够的数据点来对测试数据集进行泛化。
此外,考虑到卷积从头开始学习各种特征,如果训练数据集中的图像具有较大的形状(宽度和高度),则可能需要多个周期才能让模型开始适应训练数据集。
因此,在下一部分,我们将编写以下构建 CNN 的情景代码,其中包含一些图像(大约 1,700 张图像),并测试不同形状图像的准确性:
-
在 10 个周期中,当图像尺寸为 300 X 300 时的准确性
-
在 10 个周期中,当图像尺寸为 50 X 50 时的准确性
如何做到……
在本部分中,我们将获取一个数据集并进行分类分析,其中一个情景的图像尺寸为 300 x 300,而另一个情景的图像尺寸为 50 x 50。(在实现代码时,请参考 GitHub 上的Transfer_learning.ipynb
文件。)
情景 1——大图像
- 获取数据集。对于此分析,我们将继续使用在第四章的性别分类案例研究中下载的男性与女性分类数据集,构建深度卷积神经网络:
$ wget https://d1p17r2m4rzlbo.cloudfront.net/wp-content/uploads/2017/04/a943287.csv
import pandas as pd, numpy as np
from skimage import io
# Location of file is /content/a943287.csv
# be sure to change to location of downloaded file on your machine
data = pd.read_csv('/content/a943287.csv')
data_male = data[data['please_select_the_gender_of_the_person_in_the_picture']=="male"].reset_index(drop='index')
data_female = data[data['please_select_the_gender_of_the_person_in_the_picture']=="female"].reset_index(drop='index')
final_data = pd.concat([data_male[:1000],data_female[:1000]],axis=0).reset_index(drop='index')
- 提取图像路径,然后准备输入和输出数据:
x = []
y = []
for i in range(final_data.shape[0]):
try:
image = io.imread(final_data.loc[i]['image_url'])
if(image.shape==(300,300,3)):
x.append(image)
y.append(final_data.loc[i]['please_select_the_gender_of_the_person_in_the_picture'])
except:
continue
- 以下是图像的示例:
请注意,所有图像的大小为 300 x 300 x 3。
- 创建输入和输出数据集数组:
x2 = []
y2 = []
for i in range(len(x)):
x2.append(x[i])
img_label = np.where(y[i]=="male",1,0)
y2.append(img_label)
在前一步中,我们遍历了所有图像(逐一进行),将图像读取到一个数组中(在这次迭代中其实可以跳过此步骤。然而,在下一个调整图像大小的场景中,我们将在此步骤调整图像大小)。此外,我们还存储了每个图像的标签。
- 准备输入数组,以便可以传递给 CNN。此外,准备输出数组:
x2 = np.array(x2)
x2 = x2.reshape(x2.shape[0],x2.shape[1],x2.shape[2],3)
在这里,我们将数组列表转换为 numpy 数组,以便将其传递给神经网络。
缩放输入数组并创建输入和输出数组:
X = np.array(x2)/255
Y = np.array(y2)
- 创建训练和测试数据集:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,Y, test_size=0.1, random_state=42)
- 定义模型并编译它:
from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Activation, Flatten
from keras.layers.convolutional import Conv2D
from keras.layers.pooling import MaxPooling2D
from keras.optimizers import SGD
from keras import backend as K
model = Sequential()
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu',input_shape=(300,300,3)))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(256, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(512, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(Flatten())
model.add(Dense(100, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
在前面的代码中,我们构建了一个模型,其中包含多个卷积层、池化层和 dropout 层。此外,我们将最终 dropout 层的输出传递到一个展平层,然后将展平后的输出连接到一个 512 节点的隐藏层,最后将隐藏层连接到输出层。
模型摘要如下:
在以下代码中,我们编译模型以减少二进制交叉熵损失,如下所示:
model.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
- 训练模型:
history = model.fit(X_train, y_train, batch_size=32,epochs=10,verbose=1,validation_data = (X_test, y_test))
在前一步中,你可以看到,随着训练轮次的增加,模型并没有继续训练,如下图所示(此图的代码与我们在第二章的缩放输入数据部分看到的代码相同,并且可以在本章的 GitHub 仓库中找到):
在前面的图表中,你可以看到模型几乎没有学习到任何东西,因为损失几乎没有变化。而且,准确率卡在了 51% 左右(这大约是原始数据集中男性与女性图像的分布比例)。
场景 2 – 较小的图像
在这个场景中,我们将在模型中做如下修改:
-
输入图像大小:
- 我们将把大小从 300 x 300 缩小到 50 x 50
-
模型架构:
- 架构的结构与我们在场景 1 – 大图像中看到的相同
- 创建一个数据集,输入为调整后图像大小(50 x 50 x 3),输出为标签。为此,我们将继续从场景 1 的第 4 步开始:
import cv2
x2 = []
y2 = []
for i in range(len(x)):
img = cv2.resize(x[i],(50,50))
x2.append(img)
img_label = np.where(y[i]=="male",1,0)
y2.append(img_label)
- 创建训练和测试数据集的输入和输出数组:
x2 = np.array(x2)
x2 = x2.reshape(x2.shape[0],x2.shape[1],x2.shape[2],3)
X = np.array(x2)/255
Y = np.array(y2)
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,Y, test_size=0.1, random_state=42)
- 构建和编译模型:
model = Sequential()
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu',input_shape=(50,50,3)))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(256, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(512, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(Flatten())
model.add(Dense(100, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
model.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
模型摘要如下:
- 训练模型:
history = model.fit(X_train, y_train, batch_size=32,epochs=10,verbose=1,validation_data = (X_test, y_test))
模型在训练和测试数据集上随着训练轮次的增加,准确率和损失情况如下:
请注意,虽然在最初的训练和测试数据集上,准确率有所提高且损失逐渐下降,但随着训练轮次的增加,模型开始在训练数据上过拟合(专注),并且在测试数据集上的准确率为约 76%。
从中我们可以看到,当输入大小较小且卷积核必须从图像的较小部分学习时,CNN 能正常工作。然而,随着图像大小的增加,CNN 在学习上遇到了困难。
鉴于我们已经发现图像大小对模型准确率有影响,在新的场景中,我们将使用激进的池化,以确保较大的图像(300 x 300 形状)能迅速缩小。
场景 3 – 对大图像进行激进的池化
在下面的代码中,我们将保留在场景 1 中执行到第 6 步的分析。然而,唯一的变化将是模型架构;在接下来的模型架构中,我们使用了比场景 1 中更激进的池化方法。
在以下架构中,每一层具有更大的池化窗口,确保我们能够捕捉到比使用较小池化大小时更大的区域的激活。模型架构如下:
model = Sequential()
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu',input_shape=(300,300,3)))
model.add(MaxPooling2D(pool_size=(3, 3)))
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(3, 3)))
model.add(Conv2D(256, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(3, 3)))
model.add(Conv2D(512, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(Flatten())
model.add(Dense(100, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
请注意,在这个架构中,池化大小是 3 x 3,而不是我们在先前场景中使用的 2 x 2:
一旦我们在输入和输出数组上拟合模型,训练和测试数据集上准确率和损失的变化如下:
model.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
history = model.fit(X_train, y_train, batch_size=32,epochs=10,verbose=1,validation_data = (X_test, y_test))
以下是前面代码的输出结果:
我们可以看到,测试数据在正确分类图像中的性别时,准确率约为 70%。
然而,你可以看到,在训练数据集上存在相当大的过拟合现象(因为训练数据集上的损失持续下降,而测试数据集上并未出现类似下降)。
使用基于 VGG16 架构的模型对图像中人的性别进行分类
在前一部分的基于 CNN 的性别分类中,我们看到,当我们从头开始构建 CNN 模型时,可能会遇到以下一些情况:
-
传递的图像数量不足以让模型学习
-
当图像尺寸较大时,卷积层可能无法学习到我们图像中的所有特征
第一个问题可以通过在大数据集上执行我们的分析来解决。第二个问题可以通过在较大数据集上训练更大的网络,并进行更长时间的训练来解决。
然而,虽然我们能够执行所有这些操作,但往往缺乏进行此类分析所需的数据量。使用预训练模型进行迁移学习可以在这种情况下提供帮助。
ImageNet 是一个流行的竞赛,参与者需要预测图像的不同类别,图像的大小各异,并且包含多个类别的对象。
有多个研究团队参与了这场竞争,目标是提出一个能够预测包含数百万图像的数据集中的多类图像的模型。由于数据集中有数百万图像,数据集有限性的问题得以解决。此外,鉴于研究团队们建立了巨大的网络,卷积网络学习多种特征的问题也得到了解决。
因此,我们可以重用在不同数据集上构建的卷积层,在这些卷积层中,网络学习预测图像中的各种特征,然后将这些特征传递通过隐藏层,以便我们能够预测针对特定数据集的图像类别。不同的研究小组开发了多个预训练模型,本文将介绍 VGG16。
准备工作
在这一部分,我们将尝试理解如何利用 VGG16 的预训练网络来进行性别分类练习。
VGG16 模型的架构如下:
请注意,模型的架构与我们在“使用 CNN 进行性别分类”一节中训练的模型非常相似。主要的区别在于,这个模型更深(更多的隐藏层)。此外,VGG16 网络的权重是通过在数百万图像上训练得到的。
我们将确保在训练我们的模型以分类图像中的性别时,VGG16 的权重不会被更新。通过性别分类练习(形状为 300 x 300 x 3 的图像)的输出形状是 9 x 9 x 512。
我们将保留原网络中的权重,提取 9 x 9 x 512 的输出,通过另一个卷积池化操作,进行平坦化,连接到隐藏层,并通过 sigmoid 激活函数来判断图像是男性还是女性。
本质上,通过使用 VGG16 模型的卷积层和池化层,我们是在使用在更大数据集上训练得到的滤波器。最终,我们将对这些卷积层和池化层的输出进行微调,以适应我们要预测的对象。
如何做……
有了这个策略,让我们按照以下方式编写代码(在实现代码时,请参考 GitHub 中的Transfer_learning.ipynb
文件):
- 导入预训练模型:
from keras.applications import vgg16
from keras.utils.vis_utils import plot_model
from keras.applications.vgg16 import preprocess_input
vgg16_model = vgg16.VGG16(include_top=False, weights='imagenet',input_shape=(300,300,3))
请注意,我们在 VGG16 模型中排除了最后一层。这是为了确保我们根据我们要解决的问题对 VGG16 模型进行微调。此外,鉴于我们的输入图像尺寸为 300 X 300 X 3,我们在下载 VGG16 模型时也指定了相同的尺寸。
- 预处理图像集。这个预处理步骤确保图像的处理方式能够被预训练的模型接受作为输入。例如,在下面的代码中,我们对其中一张名为
img
的图像进行预处理:
from keras.applications.vgg16 import preprocess_input
img = preprocess_input(img.reshape(1,224,224,3))
我们使用preprocess_input
方法按照 VGG16 的预处理要求来预处理图像。
- 创建输入和输出数据集。在本练习中,我们将从“使用 CNN 进行性别分类”场景 1 的第 3 步结束继续。在这里,创建输入和输出数据集的过程与我们之前做的一样,唯一的变化是使用 VGG16 模型提取特征。
我们将通过vgg16_model
传递每张图像,以便将vgg16_model
的输出作为处理后的输入。此外,我们还将对输入进行如下预处理:
import cv2
x2_vgg16 = []
for i in range(len(x)):
img = x[i]
img = preprocess_input(img.reshape(1,300,300,3))
现在,我们将预处理后的输入传递给 VGG16 模型以提取特征,如下所示:
img_new = vgg16_model.predict(img.reshape(1,300,300,3))
x2_vgg16.append(img_new)
在前面的代码中,除了将图像传递给 VGG16 模型外,我们还将输入值存储在一个列表中。
- 将输入和输出转换为 NumPy 数组,并创建训练和测试数据集:
x2_vgg16 = np.array(x2_vgg16)
x2_vgg16= x2_vgg16.reshape(x2_vgg16.shape[0],x2_vgg16.shape[2],x2_vgg16.shape[3],x2_vgg16.shape[4])
Y = np.array(y2)
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x2_vgg16,Y, test_size=0.1, random_state=42)
- 构建并编译模型:
model_vgg16 = Sequential()
model_vgg16.add(Conv2D(512, kernel_size=(3, 3), activation='relu',input_shape=(X_train.shape[1],X_train.shape[2],X_train.shape[3])))
model_vgg16.add(MaxPooling2D(pool_size=(2, 2)))
model_vgg16.add(Flatten())
model_vgg16.add(Dense(512, activation='relu'))
model_vgg16.add(Dropout(0.5))
model_vgg16.add(Dense(1, activation='sigmoid'))
model_vgg16.summary()
模型摘要如下:
编译模型:
model_vgg16.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
- 在缩放输入数据时训练模型:
history_vgg16 = model_vgg16.fit(X_train/np.max(X_train), y_train, batch_size=16,epochs=10,verbose=1,validation_data = (X_test/np.max(X_train), y_test))
一旦我们训练模型,我们应该能看到,在前几轮训练中,模型能够在测试数据集上达到约 89%的准确率:
将此与我们在“使用 CNN 进行性别分类”部分中构建的模型进行对比,在那些场景中,我们无法在 10 轮训练内实现 80%的分类准确率。
以下是一些模型误分类的图像示例:
请注意,在前面的图片中,当输入图像是面部的一部分,或者图像中的物体占据了图像总面积的较小部分,或者标签可能不正确时,模型可能会误分类。
可视化神经网络中间层的输出
在前一部分,我们构建了一个可以从图像中学习性别分类的模型,准确率为 89%。然而,到目前为止,在滤波器学到了什么方面,它对我们来说仍然是一个黑箱。
在这一部分,我们将学习如何提取模型中各个滤波器学到了什么。此外,我们还将对比初始层中的滤波器学到的内容与最后几层中的特征学到的内容。
准备就绪
为了理解如何提取各个滤波器学到了什么,让我们采用以下策略:
-
我们将选择一张图像进行分析。
-
我们将选择第一个卷积层,以理解第一个卷积中的各个滤波器学到了什么。
-
计算第一层权重与输入图像卷积的输出:
-
在此步骤中,我们将提取模型的中间输出:
- 我们将提取模型的第一层输出。
-
-
为了提取第一层的输出,我们将使用功能性 API:
- 功能性 API 的输入是输入图像,输出将是第一层的输出。
-
这将返回所有通道(滤波器)中间层的输出。
-
我们将对卷积的第一层和最后一层执行这些步骤。
-
最后,我们将可视化所有通道的卷积操作输出。
-
我们还将可视化给定通道在所有图像上的输出。
如何操作…
在本节中,我们将编写代码,展示初始层和最终层的卷积滤波器所学习的内容。
我们将重用在使用 CNN 进行性别分类食谱的情景 1 中步骤 1到步骤 4准备的数据(在实现代码时,请参考 GitHub 中的Transfer_learning.ipynb
文件):
- 选择一张图像来可视化其中间输出:
plt.imshow(x[3])
plt.grid('off')
- 定义功能性 API,输入为图像,输出为第一卷积层的输出:
from keras.applications.vgg16 import preprocess_input
model_vgg16.predict(vgg16_model.predict(preprocess_input(x[3].reshape(1,300,300,3)))/np.max(X_train))
from keras import models
activation_model = models.Model(inputs=vgg16_model.input,outputs=vgg16_model.layers[1].output)
activations = activation_model.predict(preprocess_input(x[3].reshape(1,300,300,3)))
我们定义了一个名为activation_model
的中间模型,在该模型中,我们将感兴趣的图像作为输入,并提取第一层的输出作为模型的输出。
一旦我们定义了模型,我们将通过将输入图像传递给模型来提取第一层的激活。请注意,我们必须调整输入图像的形状,以便它符合模型的要求。
- 让我们按如下方式可视化输出中的前 36 个滤波器:
fig, axs = plt.subplots(6, 6, figsize=(10, 10))
fig.subplots_adjust(hspace = .5, wspace=.5)
first_layer_activation = activations[0]
for i in range(6):
for j in range(6):
try:
axs[i,j].set_ylim((224, 0))
axs[i,j].contourf(first_layer_activation[:,:,((6*i)+j)],6,cmap='viridis')
axs[i,j].set_title('filter: '+str((6*i)+j))
axs[i,j].axis('off')
except:
continue
- 在上面的代码中,我们创建了一个 6x6 的框架,用于绘制 36 张图像。此外,我们正在循环遍历
first_layer_activation
中的所有通道,并绘制第一层的输出,具体如下:
在这里,我们可以看到某些滤波器提取了原始图像的轮廓(例如滤波器 0、4、7、10)。此外,某些滤波器已经学会了仅识别几个特征,例如耳朵、眼睛和鼻子(例如滤波器 30)。
- 让我们通过检查 36 张图像中滤波器 7 的输出来验证我们对某些滤波器能够提取原始图像轮廓的理解,具体如下:
activation_model = models.Model(inputs=vgg16_model.input,outputs=vgg16_model.layers[1].output)
activations = activation_model.predict(preprocess_input(np.array(x[:36]).reshape(36,300,300,3)))
fig, axs = plt.subplots(6, 6, figsize=(10, 10))
fig.subplots_adjust(hspace = .5, wspace=.5)
first_layer_activation = activations
for i in range(6):
for j in range(6):
try:
axs[i,j].set_ylim((224, 0))
axs[i,j].contourf(first_layer_activation[((6*i)+j),:,:,7],6,cmap='viridis')
axs[i,j].set_title('filter: '+str((6*i)+j))
axs[i,j].axis('off')
except:
continue
在上面的代码中,我们正在循环遍历前 36 张图像,并绘制所有 36 张图像的第一卷积层输出:
请注意,在所有图像中,第七个滤波器正在学习图像中的轮廓。
- 让我们尝试理解最后一个卷积层中的滤波器学到了什么。为了理解我们模型中最后一个卷积层的位置,我们将提取模型中的各种层:
for i, layer in enumerate(model.layers):
print(i, layer.name)
执行上述代码后将显示以下层名称:
- 请注意,最后一个卷积层是我们模型中的第九个输出,可以通过以下方式提取:
activation_model = models.Model(inputs=vgg16_model.input,outputs=vgg16_model.layers[-1].output)
activations = activation_model.predict(preprocess_input(x[3].reshape(1,300,300,3)))
由于在图像上进行了多次池化操作,图像的大小现在已经大幅缩小(到 1, 9,9,512)。以下是最后一个卷积层中各种滤波器学习的可视化效果:
请注意,在此迭代中,不太容易理解最后一个卷积层的滤波器学到了什么(因为轮廓不容易归属于原图的某一部分),这些比第一个卷积层学到的轮廓更为细致。
使用基于 VGG19 架构的模型对图像中的人物进行性别分类
在上一节中,我们了解了 VGG16 的工作原理。VGG19 是 VGG16 的改进版本,具有更多的卷积和池化操作。
准备就绪
VGG19 模型的架构如下:
请注意,前述架构具有更多的层和更多的参数。
请注意,VGG16 和 VGG19 架构中的 16 和 19 分别表示这些网络中的层数。我们通过 VGG19 网络传递每个图像后提取的 9 x 9 x 512 输出将作为我们的模型的输入。
此外,创建输入和输出数据集,然后构建、编译和拟合模型的过程与我们在使用基于 VGG16 模型架构进行性别分类的食谱中看到的过程相同。
如何实现…
在本节中,我们将编码 VGG19 的预训练模型,代码如下(在实现代码时,请参考 GitHub 中的Transfer_learning.ipynb
文件):
- 准备输入和输出数据(我们将继续从性别分类使用 CNN食谱中的步骤 3开始):
import cv2
x2 = []
for i in range(len(x)):
img = x[i]
img = preprocess_input(img.reshape(1,300,300,3))
img_new = vgg19_model.predict(img.reshape(1,300,300,3))
x2.append(img_new)
- 将输入和输出转换为相应的数组,并创建训练集和测试集:
x2 = np.array(x2)
x2= x2.reshape(x2.shape[0],x2.shape[2],x2.shape[3],x2.shape[4])
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x2,Y, test_size=0.1, random_state=42)
- 构建并编译模型:
model_vgg19 = Sequential()
model_vgg19.add(Conv2D(512, kernel_size=(3, 3), activation='relu',input_shape=(X_train.shape[1],X_train.shape[2],X_train.shape[3])))
model_vgg19.add(MaxPooling2D(pool_size=(2, 2)))
model_vgg19.add(Flatten())
model_vgg19.add(Dense(512, activation='relu'))
model_vgg19.add(Dropout(0.5))
model_vgg19.add(Dense(1, activation='sigmoid'))
model_vgg19.summary()
以下是模型的可视化:
model_vgg19.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
- 在对输入数据进行缩放的同时拟合模型:
history_vgg19 = model_vgg19.fit(X_train/np.max(X_train), y_train, batch_size=16,epochs=10,verbose=1,validation_data = (X_test/np.max(X_train), y_test))
让我们绘制训练集和测试集的损失和准确率度量:
我们应该注意,当使用 VGG19 架构时,我们能够在测试数据集上实现约 89%的准确率,这与 VGG16 架构非常相似。
以下是错误分类图像的示例:
请注意,VGG19 似乎根据图像中人物所占空间来错误分类。此外,它似乎更倾向于预测长发男性为女性。
使用 Inception v3 架构的性别分类模型
在之前的食谱中,我们基于 VGG16 和 VGG19 架构实现了性别分类。在本节中,我们将使用 Inception 架构来实现分类。
inception 模型如何派上用场的直观理解如下:
会有一些图像中,物体占据了图像的大部分。同样,也会有一些图像中,物体只占据了图像的一小部分。如果在这两种情况下我们使用相同大小的卷积核,那么就会使模型的学习变得困难——一些图像可能包含较小的物体,而其他图像可能包含较大的物体。
为了解决这个问题,我们将在同一层中使用多个大小的卷积核。
在这种情况下,网络本质上会变得更宽,而不是更深,如下所示:
在上述图示中,注意我们在给定层中执行多个卷积操作。inception v1 模块有九个这样的模块线性堆叠,如下所示:
来源: http://joelouismarino.github.io/images/blog_images/blog_googlenet_keras/googlenet_diagram.png
请注意,这种架构既深又宽。可能会导致梯度消失问题(正如我们在第二章中关于批量归一化的案例中所见,构建深度前馈神经网络)。
为了避免梯度消失的问题,inception v1 在 inception 模块中添加了两个辅助分类器。inception 基础网络的总体损失函数如下所示:
total_loss = real_loss + 0.3 * aux_loss_1 + 0.3 * aux_loss_2
请注意,辅助损失仅在训练期间使用,在预测过程中会被忽略。
Inception v2 和 v3 是对 inception v1 架构的改进,在 v2 中,作者在卷积操作的基础上进行了优化,以加快图像处理速度,而在 v3 中,作者在现有卷积上添加了 7 x 7 的卷积,以便将它们连接起来。
如何实现…
我们实现 inception v3 的过程与构建基于 VGG19 模型的分类器非常相似(在实现代码时,请参考 GitHub 中的Transfer_learning.ipynb
文件):
- 下载预训练的 Inception 模型:
from keras.applications import inception_v3
from keras.applications.inception_v3 import preprocess_input
from keras.utils.vis_utils import plot_model
inception_model = inception_v3.InceptionV3(include_top=False, weights='imagenet',input_shape=(300,300,3))
请注意,我们需要一个至少为 300 x 300 大小的输入图像,才能使 inception v3 预训练模型正常工作。
- 创建输入和输出数据集(我们将从性别分类使用 CNNs食谱中的场景 1 第 3 步继续):
import cv2
x2 = []
for i in range(len(x)):
img = x[i]
img = preprocess_input(img.reshape(1,300,300,3))
img_new = inception_model.predict(img.reshape(1,300,300,3))
x2.append(img_new)
- 创建输入和输出数组,以及训练和测试数据集:
x2 = np.array(x2)
x2= x2.reshape(x2.shape[0],x2.shape[2],x2.shape[3],x2.shape[4])
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x2,Y, test_size=0.1, random_state=42)
- 构建并编译模型:
model_inception_v3 = Sequential()
model_inception_v3.add(Conv2D(512, kernel_size=(3, 3), activation='relu',input_shape=(X_train.shape[1],X_train.shape[2],X_train.shape[3])))
model_inception_v3.add(MaxPooling2D(pool_size=(2, 2)))
model_inception_v3.add(Flatten())
model_inception_v3.add(Dense(512, activation='relu'))
model_inception_v3.add(Dropout(0.5))
model_inception_v3.add(Dense(1, activation='sigmoid'))
model_inception_v3.summary()
model.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
前面的模型可以如下可视化:
- 在缩放输入数据的同时拟合模型:
history_inception_v3 = model_inception_v3.fit(X_train/np.max(X_train), y_train, batch_size=16,epochs=10,verbose=1,validation_data = (X_test/np.max(X_train), y_test))
准确度和损失值的变化如下:
你应该注意到,在这种情况下,准确度也是大约 ~90%。
使用基于 ResNet 50 架构的模型对图像中的人物进行性别分类
从 VGG16 到 VGG19,我们增加了层数,通常来说,神经网络越深,准确度越高。然而,如果仅仅增加层数是技巧,那么我们可以继续增加更多层(同时注意避免过拟合)来获得更准确的结果。
不幸的是,这并不完全正确,梯度消失的问题出现了。随着层数的增加,梯度在网络中传递时变得非常小,以至于很难调整权重,网络性能会下降。
ResNet 的出现是为了应对这一特定情况。
想象一种情况,如果模型没有任何需要学习的内容,卷积层仅仅将前一层的输出传递给下一层。然而,如果模型需要学习一些其他特征,卷积层会将前一层的输出作为输入,并学习需要学习的附加特征来执行分类。
残差是模型期望从一层到下一层学习的附加特征。
一个典型的 ResNet 架构如下所示:
来源:arxiv.org/pdf/1512.03385.pdf
请注意,我们有跳跃连接,这些连接将前一层与后续层连接起来,并且网络中还有传统的卷积层。
此外,ResNet50 中的 50 表示该网络总共有 50 层。
如何执行…
ResNet50 架构的构建如下(在实现代码时,请参考 GitHub 上的Transfer_learning.ipynb
文件):
- 下载预训练的 Inception 模型:
from keras.applications import resnet50
from keras.applications.resnet50 import preprocess_input
resnet50_model = resnet50.ResNet50(include_top=False, weights='imagenet',input_shape=(300,300,3))
请注意,我们需要一个至少为 224 x 224 形状的输入图像,才能使 ResNet50 预训练模型正常工作。
- 创建输入和输出数据集(我们将从性别分类使用 CNNs教程中的步骤 3继续):
import cv2
x2 = []
for i in range(len(x)):
img = x[i]
img = preprocess_input(img.reshape(1,300,300,3))
img_new = resnet50_model.predict(img.reshape(1,300,300,3))
x2.append(img_new)
- 创建输入和输出数组,并准备训练和测试数据集:
x2 = np.array(x2)
x2= x2.reshape(x2.shape[0],x2.shape[2],x2.shape[3],x2.shape[4])
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x2,Y, test_size=0.1, random_state=42)
- 构建并编译模型:
model_resnet50 = Sequential()
model_resnet50.add(Conv2D(512, kernel_size=(3, 3), activation='relu',input_shape=(X_train.shape[1],X_train.shape[2],X_train.shape[3])))
model_resnet50.add(MaxPooling2D(pool_size=(2, 2)))
model_resnet50.add(Conv2D(512, kernel_size=(3, 3), activation='relu'))
model_resnet50.add(MaxPooling2D(pool_size=(2, 2)))
model_resnet50.add(Flatten())
model_resnet50.add(Dense(512, activation='relu'))
model_resnet50.add(Dropout(0.5))
model_resnet50.add(Dense(1, activation='sigmoid'))
model_resnet50.summary()
model_resnet50.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
模型的总结如下:
- 在缩放输入数据的同时拟合模型:
history_resnet50 = model_resnet50.fit(X_train/np.max(X_train), y_train, batch_size=32,epochs=10,verbose=1,validation_data = (X_test/np.max(X_train), y_test))
准确度和损失值的变化如下:
请注意,前面的模型给出的准确率为 92%。
在性别分类的多个预训练模型中,准确度没有显著差异,因为它们可能训练出来的是提取一般特征的模型,而不一定是用来分类性别的特征。
检测图像中的面部关键点
在这个教程中,我们将学习如何检测人脸的关键点,这些关键点包括左右眼的边界、鼻子以及嘴巴的四个坐标。
这里有两张带有关键点的示例图片:
请注意,我们预计要检测的关键点在这张图片中作为点绘制。图像中共检测到 68 个关键点,其中包括面部的关键点——嘴巴、右眉毛、左眉毛、右眼、左眼、鼻子、下巴。
在这个案例中,我们将利用在 使用基于 VGG16 架构的模型进行图像性别分类 部分中学到的 VGG16 迁移学习技术来检测面部的关键点。
准备就绪
对于关键点检测任务,我们将使用一个数据集,在该数据集上我们标注了要检测的点。对于这个练习,输入将是我们要检测关键点的图像,输出将是关键点的 x 和 y 坐标。数据集可以从这里下载:github.com/udacity/P1_Facial_Keypoints
。
我们将遵循以下步骤:
-
下载数据集
-
将图像调整为标准形状
- 在调整图像大小时,确保关键点已修改,以便它们代表修改后的(调整大小的)图像
-
将调整大小的图像传递给 VGG16 模型
-
创建输入和输出数组,其中输入数组是通过 VGG16 模型传递图像的输出,输出数组是修改后的面部关键点位置
-
适配一个模型,最小化预测的面部关键点与实际面部关键点之间的绝对误差值
如何操作…
我们讨论的策略的代码如下(在实现代码时,请参考 GitHub 中的 Facial_keypoints.ipynb
文件):
- 下载并导入数据集:
$ git clone https://github.com/udacity/P1_Facial_Keypoints.git import pandas as pddata = pd.read_csv('/content/P1_Facial_Keypoints/data/training_frames_keypoints.csv')
检查这个数据集。
总共有 137 列,其中第一列是图像的名称,剩余的 136 列代表对应图像中 68 个面部关键点的 x 和 y 坐标值。
- 预处理数据集,提取图像、调整大小后的图像、VGG16 特征以及修改后的关键点位置作为输出:
初始化将被附加以创建输入和输出数组的列表:
import cv2, numpy as np
from copy import deepcopy
x=[]
x_img = []
y=[]
循环读取图像:
for i in range(data.shape[0]):
img_path = '/content/P1_Facial_Keypoints/data/training/' + data.iloc[i,0]
img = cv2.imread(img_path)
捕获关键点值并存储
kp = deepcopy(data.iloc[i,1:].tolist())
kp_x = (np.array(kp[0::2])/img.shape[1]).tolist()
kp_y = (np.array(kp[1::2])/img.shape[0]).tolist()
kp2 = kp_x + kp_y
调整图像大小
img = cv2.resize(img,(224,224))
预处理图像,以便可以通过 VGG16 模型传递并提取特征:
preprocess_img = preprocess_input(img.reshape(1,224,224,3))
vgg16_img = vgg16_model.predict(preprocess_img)
将输入和输出值附加到相应的列表中:
x_img.append(img)
x.append(vgg16_img)
y.append(kp2)
创建输入和输出数组:
x = np.array(x)
x = x.reshape(x.shape[0],7,7,512)
y = np.array(y)
- 构建并编译模型
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout
model_vgg16 = Sequential()
model_vgg16.add(Conv2D(512, kernel_size=(3, 3), activation='relu',input_shape=(x.shape[1],x.shape[2],x.shape[3])))
model_vgg16.add(MaxPooling2D(pool_size=(2, 2)))
model_vgg16.add(Flatten())
model_vgg16.add(Dense(512, activation='relu'))
model_vgg16.add(Dropout(0.5))
model_vgg16.add(Dense(y.shape[1], activation='sigmoid'))
model_vgg16.summary()
编译模型:
model_vgg16.compile(loss='mean_absolute_error',optimizer='adam')
- 拟合模型
history = model_vgg16.fit(x/np.max(x), y, epochs=10, batch_size=32, verbose=1, validation_split = 0.1)
请注意,我们通过输入数组的最大值来对输入数组进行除法运算,以便对输入数据集进行缩放。随着训练轮数增加,训练损失和测试损失的变化如下:
- 对测试图像进行预测。在以下代码中,我们对输入数组中的倒数第二张图像进行预测(注意,
validation_split
为0.1
,因此倒数第二张图像在训练过程中并未提供给模型)。我们确保将图像传入preprocess_input
方法,然后通过VGG16_model
,最后将VGG16_model
输出的缩放版本传递给我们构建的model_vgg16
:
pred = model_vgg16.predict(vgg16_model.predict(preprocess_input(x_img[-2].reshape(1,224,224,3)))/np.max(x))
对测试图像的前述预测可以通过以下方式可视化:
我们可以看到,关键点在测试图像上被非常准确地检测出来。
第六章:检测和定位图像中的物体
在构建深度卷积神经网络和迁移学习的章节中,我们学习了如何使用深度 CNN 检测图像属于哪个类别,也学习了如何利用迁移学习进行检测。
虽然物体分类是有效的,但在现实世界中,我们还会遇到需要定位图像中物体的场景。
例如,在自动驾驶汽车的情况下,我们不仅需要检测到行人出现在汽车的视野中,还需要检测行人与汽车之间的距离,从而可以采取适当的行动。
本章将讨论检测图像中物体的各种技术。本章将涵盖以下案例研究:
-
创建边界框的训练数据集
-
使用选择性搜索生成图像中的区域提议
-
计算两幅图像的交集与并集的比值
-
使用基于区域提议的 CNN 检测物体
-
执行非最大抑制
-
使用基于锚框的算法检测人物
介绍
随着自动驾驶汽车、人脸检测、智能视频监控和人流计数解决方案的兴起,快速且准确的目标检测系统需求巨大。这些系统不仅包括图像中的物体识别和分类,还能通过在物体周围绘制适当的框来定位每个物体。这使得目标检测比其传统的计算机视觉前身——图像分类更具挑战。
为了理解目标检测的输出是什么样的,让我们看一下以下这张图片:
到目前为止,在前面的章节中,我们已经学习了分类。
在本章中,我们将学习如何为图像中的物体生成一个紧密的边界框,这是定位任务。
此外,我们还将学习如何检测图像中的多个物体,这就是目标检测任务。
创建边界框的数据集
我们已经学到,目标检测可以输出一个围绕图像中感兴趣物体的边界框。为了构建一个检测图像中物体边界框的算法,我们需要创建输入输出映射,其中输入是图像,输出是给定图像中围绕物体的边界框。
请注意,当我们检测边界框时,我们实际上是在检测围绕图像的边界框左上角的像素位置,以及边界框的相应宽度和高度。
为了训练一个提供边界框的模型,我们需要图像以及图像中所有物体的对应边界框坐标。
在本节中,我们将重点介绍创建训练数据集的一种方法,其中图像作为输入,相应的边界框存储在 XML 文件中。
我们将使用labelImg
包来标注边界框和相应的类别。
如何操作…
可以通过以下方式准备图像中的物体的边界框:
Windows
-
从以下链接下载
labelImg
的可执行文件:github.com/tzutalin/labelImg/files/2638199/windows_v1.8.1.zip
。 -
提取并打开
labelImg.exe
图形界面,如下图所示:
- 在
data
文件夹中的predefined_classes.txt
文件中指定图像中所有可能的标签。我们需要确保每个类别都列在单独的一行中,如下所示:
- 在 GUI 中点击“打开”以打开图像,并通过点击“创建矩形框”来标注图像,这将弹出如下所示的可选类别:
-
点击“保存”并保存 XML 文件。
-
检查 XML 文件。绘制矩形边界框后的 XML 文件快照如下所示:
从前面的截图中,你应该注意到bndbox
包含了与图像中感兴趣物体相对应的x和y坐标的最小值和最大值。此外,我们还应该能够提取图像中物体对应的类别。
Ubuntu
在 Ubuntu 中,可以通过输入以下命令执行与前述相同的步骤:
$sudo apt-get install pyqt5-dev-tools
$sudo pip3 install -r requirements/requirements-linux-python3.txt
$make qt5py3
$python3 labelImg.py
脚本labelImg.py
可以通过以下 GitHub 链接找到:github.com/tzutalin/labelImg
。
一旦我们执行了前面的代码,我们应该能够进行与Windows部分中看到的相同分析。
MacOS
在 macOS 中,可以通过输入以下命令来执行相同的前述步骤:
$brew install qt # will install qt-5.x.x
$brew install libxml2
$make qt5py3
$python3 labelImg.py
脚本labelImg.py
可以通过以下 GitHub 链接找到:github.com/tzutalin/labelImg
。
一旦我们执行了前面的脚本,我们应该能够进行与Windows部分中看到的相同分析。
在图像中生成区域提议,使用选择性搜索
为了理解什么是区域提议,让我们将这个术语分解为两个组成部分——区域和提议。
区域是图像的一个部分,其中该部分的像素具有非常相似的值。
区域提议是图像的较小部分,在该部分中有更高的可能性属于某个特定物体。
区域提议是有用的,因为我们从图像中生成了候选区域,这些区域内物体出现的概率较高。在目标定位任务中非常有用,我们需要围绕物体生成一个与前一节中图像中类似的边界框。
准备工作
在本节中,我们将探讨如何在一个人的图像中生成一个边界框。
选择性搜索是一种在目标检测中使用的区域提议算法。它旨在快速运行,并具有非常高的召回率。该算法基于计算基于颜色、纹理、大小和形状兼容性的相似区域的层次分组。
可以使用名为selectivesearch
的 Python 包生成区域提议,示例如下。
选择性搜索通过使用 Felzenszwalb 和 Huttenlocher 提出的基于图的分割方法,首先对图像进行过度分割(生成成千上万的区域提议),并基于像素的强度来执行。
选择性搜索算法将这些过度分割作为初始输入,并执行以下步骤:
-
将所有与分割部分对应的边界框添加到区域提议列表中
-
根据相似性将相邻的区域分组
-
进入第一步
在每次迭代中,较大的区域会被形成并添加到区域提议列表中。因此,我们采用自下而上的方法,从较小的区域生成较大的区域提议。
选择性搜索使用四种相似性度量方法,分别基于颜色、纹理、大小和形状兼容性来生成区域提议。
区域提议有助于识别图像中的潜在感兴趣目标。因此,我们可能会将定位的任务转化为分类任务,分类每个区域是否包含感兴趣的物体。
如何进行…
在本节中,我们将展示如何提取区域提议,示例如下(代码文件在 GitHub 上的Selective_search.ipynb
中可用):
- 按如下方式安装
selectivesearch
:
$pip install selectivesearch
- 导入相关的包,见下面的代码:
import matplotlib.pyplot as plt
%matplotlib inline
import selectivesearch
import cv2
- 按如下方式加载图像:
img = cv2.imread('/content/Hemanvi.jpeg')
- 提取区域提议:
img_lbl, regions = selectivesearch.selective_search(img, scale=100, min_size=2000)
参数min_size
提供了一个约束,要求区域提议的大小至少为 2,000 个像素,而参数 scale 有效地设置了观察的尺度,较大的尺度倾向于偏好较大的组件。
- 检查结果中区域的数量,并将其存储在列表中:
print(len(regions))
candidates = set()
for r in regions:
if r['rect'] in candidates:
continue
# excluding regions smaller than 2000 pixels
if r['size'] < 2000:
continue
x, y, w, h = r['rect']
candidates.add(r['rect'])
在前一步中,我们将所有大于 2,000 像素(面积)的区域存储到候选区域集合中。
- 绘制包含候选区域的结果图像:
import matplotlib.patches as mpatches
fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(img)
for x, y, w, h in candidates:
rect = mpatches.Rectangle(
(x, y), w, h, fill=False, edgecolor='red', linewidth=1)
ax.add_patch(rect)
plt.axis('off')
plt.show()
从前面的截图中可以看到,图像中提取了多个区域。
计算两个图像之间的交集与并集
为了理解提议区域的准确性,我们使用一个名为交并比(IoU)的度量。IoU 可以如下可视化:
请注意,在上面的图片中,蓝色框(下方)是实际位置,红色框(上方的矩形)是区域提议。
区域提议的交集与并集的计算方法是将提议区域与真实位置的交集面积除以提议区域与真实位置的并集面积。
如何执行这个操作…
IoU 的计算方式如下(代码文件可以在 GitHub 中的Selective_search.ipynb
找到):
- 定义 IoU 提取函数,以下代码演示了这个过程:
from copy import deepcopy
import numpy as np
def extract_iou(candidate, current_y,img_shape):
boxA = deepcopy(candidate)
boxB = deepcopy(current_y)
img1 = np.zeros(img_shape)
img1[boxA[1]:boxA[3],boxA[0]:boxA[2]]=1
img2 = np.zeros(img_shape)
img2[int(boxB[1]):int(boxB[3]),int(boxB[0]):int(boxB[2])]=1
iou = np.sum(img1*img2)/(np.sum(img1)+np.sum(img2)- np.sum(img1*img2))
return iou
在上述函数中,我们将候选区域、实际物体区域和图像形状作为输入。
此外,我们为候选图像和实际物体位置图像初始化了两个相同形状且值为零的数组。
我们已经覆盖了候选图像和实际物体位置图像,在它们各自的位置上显示图像和物体。
最后,我们计算了候选图像与实际物体位置图像的交集与并集的比值。
- 导入感兴趣的图像:
img = cv2.imread('/content/Hemanvi.jpeg')
- 绘制图像并验证物体的实际位置:
plt.imshow(img)
plt.grid('off')
请注意,感兴趣的区域大约从左下角的 50 个像素开始,延伸到图像的第 290 个像素。此外,在y轴上,它也从大约第 50 个像素开始,直到图像的末端。
所以,物体的实际位置是(50,50,290,500),这是(xmin
,ymin
,xmax
,ymax
)格式。
- 提取区域提议:
img_lbl, regions = selectivesearch.selective_search(img, scale=100, min_size=2000)
从selectivesearch
方法提取的区域格式为(xmin
,ymin
,width
,height
)。因此,在提取区域的 IoU 之前,我们需要确保候选区域和实际位置图像的格式一致,即(xmin
,ymin
,xmax
,ymax
)。
- 将 IoU 提取函数应用于感兴趣的图像。请注意,函数的输入是实际物体的位置和候选图像的形状:
regions =list(candidates)
actual_bb = [50,50,290,500]
iou = []
for i in range(len(regions)):
candidate = list(regions[i])
candidate[2] += candidate[0]
iou.append(extract_iou(candidate, actual_bb, img.shape))
- 确定与实际物体(真实边界框)具有最高重叠的区域:
np.argmax(iou)
对于这个特定图像,前述输出是第十个候选区域,其坐标为 0,0,299,515。
- 让我们打印实际的边界框和候选边界框。为此,我们需要将输出的(
xmin
,ymin
,xmax
,ymax
)格式转换为(xmin
,ymin
,width
,height
):
max_region = list(regions[np.argmax(iou)])
max_region[2] -= max_region[0]
max_region[3] -= max_region[1]
actual_bb[2] -= actual_bb[0]
actual_bb[3] -= actual_bb[1]
让我们附加实际边界框和具有最高 IoU 的候选边界框:
maxcandidate_actual = [max_region,actual_bb]
现在,我们将循环遍历前述列表,并为图像中物体的实际位置分配更大的线宽,以便区分候选区域与实际物体的位置:
import matplotlib.patches as mpatches
fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(img)
for i,(x, y, w, h) in enumerate(maxcandidate_actual):
if(i==0):
rect = mpatches.Rectangle(
(x, y), w, h, fill=False, edgecolor='blue', linewidth=2)
ax.add_patch(rect)
else:
rect = mpatches.Rectangle(
(x, y), w, h, fill=False, edgecolor='red', linewidth=5)
ax.add_patch(rect)
plt.axis('off')
plt.show()
通过这种方式,我们可以确定每个候选区域与图像中物体实际位置的 IoU。此外,我们还可以确定与图像中物体实际位置 IoU 最高的候选区域。
使用基于区域建议的 CNN 进行物体检测
在前一节中,我们已经学习了如何从图像中生成区域建议。 本节中,我们将利用这些区域建议来进行物体检测和定位。
准备工作
我们将采用的基于区域建议的物体检测和定位策略如下:
-
对于当前的练习,我们将基于仅包含一个物体的图像来构建模型
-
我们将从图像中提取不同的区域建议(候选框)
-
我们将计算候选框与实际物体位置的接近程度:
- 本质上,我们计算候选框与物体实际位置的交并比
-
如果交并比大于某个阈值——则该候选框被认为包含目标物体,否则不包含:
- 这将为每个候选框创建标签,其中候选框的图像作为输入,交并比阈值提供输出
-
我们将调整每个候选框的图像大小,并通过 VGG16 模型(我们在前一章节中学习过)提取候选框的特征
-
此外,我们将通过比较候选框的位置与物体实际位置来创建边界框修正的训练数据
-
构建一个分类模型,将候选框的特征映射到区域是否包含物体的输出
-
对于包含图像的区域(根据模型),构建一个回归模型,将候选框的输入特征映射到提取物体准确边界框所需的修正
-
对结果边界框执行非极大值抑制:
- 非极大值抑制确保了重叠度较高的候选框被压缩为 1,最终只保留具有最高概率包含物体的候选框
-
通过执行非极大值抑制,我们将能够复现为包含多个物体的图像构建的模型
以下是前述内容的示意图:
如何做到……
在本节中,我们将编写前一节讨论过的算法(代码文件和相应推荐数据集的链接可在 GitHub 上找到,文件名为Region_proposal_based_object_detection.ipynb
,并附有推荐的数据集):
- 下载包含一组图像、其中包含的物体及图像中物体对应的边界框的数据集。数据集和相应的代码文件可以在 GitHub 上找到,供您使用。
以下是一个示例图像及其对应的边界框坐标和图像中的物体类别:
物体的类别和边界框坐标将保存在 XML 文件中(如何获取 XML 文件的详细信息可参考 GitHub 上的代码文件),可以通过以下方式从 XML 文件中提取:
如果xml["annotation"]["object"]
是一个列表,说明图像中存在多个物体。
xml["annotation"]["object"]["bndbox"]
提取图像中物体的边界框,其中边界框的坐标包括“xmin”、“ymin”、“xmax”和“ymax”。
xml["annotation"]["object"]["name"]
提取图像中物体的类别。
- 按如下方式导入相关包:
import matplotlib.pyplot as plt
%matplotlib inline
import tensorflow as tf, selectivesearch
import json, scipy, os, numpy as np,argparse,time, sys, gc, cv2, xmltodict
from copy import deepcopy
- 定义 IoU 提取函数,如下代码所示:
def extract_iou2(candidate, current_y,img_shape):
boxA = deepcopy(candidate)
boxB = deepcopy(current_y)
boxA[2] += boxA[0]
boxA[3] += boxA[1]
iou_img1 = np.zeros(img_shape)
iou_img1[boxA[1]:boxA[3],boxA[0]:boxA[2]]=1
iou_img2 = np.zeros(img_shape)
iou_img2[int(boxB[1]):int(boxB[3]),int(boxB[0]):int(boxB[2])]=1
iou = np.sum(iou_img1*iou_img2)/(np.sum(iou_img1)+np.sum(iou_img2)-np.sum(iou_img1*iou_img2))
return iou
- 定义候选框提取函数,如下所示:
def extract_candidates(img):
img_lbl, regions = selectivesearch.selective_search(img, scale=100, min_size=100)
img_area = img.shape[0]*img.shape[1]
candidates = []
for r in regions:
if r['rect'] in candidates:
continue
if r['size'] < (0.05*img_area):
continue
x, y, w, h = r['rect']
candidates.append(list(r['rect']))
return candidates
注意,在上述函数中,我们排除了占据图像面积小于 5%的所有候选框。
- 按如下方式导入预训练的 VGG16 模型:
from keras.applications import vgg16
from keras.utils.vis_utils import plot_model
vgg16_model = vgg16.VGG16(include_top=False, weights='imagenet')
- 为包含单一物体的图像创建输入和输出映射。初始化多个列表,随着图像的遍历,它们将被填充:
training_data_size = N = 1000
final_cls = []
final_delta = []
iou_list = []
imgs = []
我们将遍历图像,只处理那些包含单一物体的图像:
for ix, xml in enumerate(XMLs[:N]):
print('Extracted data from {} xmls...'.format(ix), end='\r')
xml_file = annotations + xml
fname = xml.split('.')[0]
with open(xml_file, "rb") as f: # notice the "rb" mode
xml = xmltodict.parse(f, xml_attribs=True)
l = []
if isinstance(xml["annotation"]["object"], list):
#'let us ignore cases with multiple objects...'
continue
在上述代码中,我们提取了图像的xml
属性,并检查图像是否包含多个对象(如果xml["annotation"]["object"]
的输出是一个列表,则图像包含多个对象)。
归一化物体位置坐标,以便我们可以处理归一化后的边界框。这样即使图像形状在进一步处理时发生变化,归一化的边界框也不会变化。例如,如果物体的xmin
在x轴的 20%和y轴的 50%处,即使图像被重新调整大小,其位置也不会变化(但如果处理的是像素值,xmin
的值会发生变化):
bndbox = xml['annotation']['object']['bndbox']
for key in bndbox:
bndbox[key] = float(bndbox[key])
x1, x2, y1, y2 = [bndbox[key] for key in ['xmin', 'xmax', 'ymin', 'ymax']]
img_size = xml['annotation']['size']
for key in img_size:
img_size[key] = float(img_size[key])
w, h = img_size['width'], img_size['height']
#'converting pixel values from bndbox to fractions...'
x1 /= w; x2 /= w; y1 /= h; y2 /= h
label = xml['annotation']['object']['name']
y = [x1, y1, x2-x1, y2-y1, label] # top-left x & y, width and height
在上述代码中,我们已经对边界框坐标进行了归一化处理。
提取图像中的候选框:
filename = jpegs+fname+'.jpg' # Path to jpg files here
img = cv2.resize(cv2.imread(filename), (224,224)) # since VGG's input shape is 224x224
candidates = extract_candidates(img)
在上述代码中,我们使用了extract_candidates
函数来提取调整大小后的图像的区域建议。
遍历候选框,计算每个候选框与图像中实际边界框的交并比(IoU),并计算实际边界框与候选框之间的对应偏差:
for jx, candidate in enumerate(candidates):
current_y2 = [int(i*224) for i in [x1,y1,x2,y2]] # [int(x1*224), int(y1*224), int(x2*224), int(y2*224)]
iou = extract_iou2(candidate, current_y2, (224, 224))
candidate_region_coordinates = c_x1, c_y1, c_w, c_h = np.array(candidate)/224
dx = c_x1 - x1
dy = c_y1 - y1
dw = c_w - (x2-x1)
dh = c_h - (y2-y1)
final_delta.append([dx,dy,dw,dh])
计算每个区域建议的 VGG16 特征,并根据区域建议与实际边界框的 IoU 来分配目标:
if(iou>0.3):
final_cls.append(label)
else:
final_cls.append('background')
#"We'll predict our candidate crop using VGG"
l = int(c_x1 * 224)
r = int((c_x1 + c_w) * 224)
t = int(c_y1 * 224)
b = int((c_y1 + c_h) * 224)
img2 = img[t:b,l:r,:3]
img3 = cv2.resize(img2,(224,224))/255
img4 = vgg16_model.predict(img3.reshape(1,224,224,3))
imgs.append(img4)
- 创建输入和输出数组:
targets = pd.DataFrame(final_cls, columns=['label'])
labels = pd.get_dummies(targets['label']).columns
y_train = pd.get_dummies(targets['label']).values.astype(float)
我们使用get_dummies
方法,因为类别是分类文本值:
x_train = np.array(imgs)
x_train = x_train.reshape(x_train.shape[0],x_train.shape[2],x_train.shape[3],x_train.shape[4])
- 构建并编译模型:
model = Sequential()
model.add(Flatten(input_shape=((7,7,512))))
model.add(Dense(512, activation='relu'))
model.add(Dense(all_classes.shape[1],activation='softmax'))
model.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy'])
- 训练模型,如下所示:
model.fit(xtrain3/x_train.max(),y_train,validation_split = 0.1, epochs=5, batch_size=32, verbose=1)
上述结果在测试数据集上实现了 97%的分类准确率。
我们通过 x_train.max()
来对输入数组进行除法操作,因为输入数组中的最大值大约是 11。通常来说,将输入值缩放至 0 和 1 之间是一个好主意,并且考虑到 VGG16 在缩放输入上的预测最大值大约是 11,所以我们将输入数组除以 x_train.max()
—— 其值大约为 11。
-
对数据集中的类别进行预测(确保我们不考虑用于训练的图像)。
-
选择一个未用于测试的图像:
import matplotlib.patches as mpatches
ix = np.random.randint(N, len(XMLs))
filename = jpegs + XMLs[ix].replace('xml', 'jpg')
- 构建一个函数,执行图像预处理以提取候选区域,对调整大小后的候选区域进行模型预测,过滤掉预测为背景类别的区域,最后绘制出具有最高概率包含非背景类别的区域(候选区域):
def test_predictions(filename):
img = cv2.resize(cv2.imread(filename), (224,224))
candidates = extract_candidates(img)
在前面的代码中,我们正在调整输入图像的大小,并从中提取候选区域:
_, ax = plt.subplots(1, 2)
ax[0].imshow(img)
ax[0].grid('off')
ax[0].set_title(filename.split('/')[-1])
pred = []
pred_class = []
在前面的代码中,我们正在绘制一张图像,并初始化预测的概率和预测类别列表,这些列表将在后续步骤中被填充:
for ix, candidate in enumerate(candidates):
l, t, w, h = np.array(candidate).astype(int)
img2 = img[t:t+h,l:l+w,:3]
img3 = cv2.resize(img2,(224,224))/255
img4 = vgg16_model.predict(img3.reshape(1,224,224,3))
final_pred = model.predict(img4/x_train.max())
pred.append(np.max(final_pred))
pred_class.append(np.argmax(final_pred))
在前面的代码中,我们正在遍历候选区域,调整其大小,并将其通过 VGG16 模型进行处理。此外,我们还将 VGG16 的输出传入我们的模型,后者提供了图像属于各种类别的概率:
pred = np.array(pred)
pred_class = np.array(pred_class)
pred2 = pred[pred_class!=1]
pred_class2 = pred_class[pred_class!=1]
candidates2 = np.array(candidates)[pred_class!=1]
x, y, w, h = candidates2[np.argmax(pred2)]
在前面的代码中,我们正在提取具有最高概率包含非背景物体的候选区域(其中一个预测类别对应于背景):
ax[1].set_title(labels[pred_class2[np.argmax(pred2)]])
ax[1].imshow(img)
ax[1].grid('off')
rect = mpatches.Rectangle((x, y), w, h, fill=False, edgecolor='red', linewidth=1)
ax[1].add_patch(rect)
在前面的代码中,我们正在绘制图像并显示边界框的矩形区域。
- 调用定义的函数并使用一张新图像:
filename = '...' #Path to new image
test_predictions(filename)
请注意,模型准确地预测了图像中物体的类别。此外,具有最高概率包含人的边界框(候选区域)需要稍作修正。
在下一步中,我们将进一步修正边界框。
- 构建并编译一个模型,该模型以图像的 VGG16 特征作为输入,并预测边界框的修正值:
model2 = Sequential()
model2.add(Flatten(input_shape=((7,7,512))))
model2.add(Dense(512, activation='relu'))
model2.add(Dense(4,activation='linear'))
model2.compile(loss='mean_absolute_error',optimizer='adam')
- 构建模型以预测边界框的修正值。然而,我们需要确保只对那些可能包含图像的区域进行边界框修正预测:
for i in range(1000):
samp=random.sample(range(len(x_train)),500)
x_train2=[x_train[i] for i in samp if pred_class[i]!=1]
x_train2 = np.array(x_train2)
final_delta2 = [final_delta[i] for i in samp if pred_class[i]!=1]
model2.fit(x_train2/x_train.max(), np.array(final_delta2), validation_split = 0.1, epochs=1, batch_size=32, verbose=0)
在前面的代码中,我们正在遍历输入数组数据集,并创建一个新的数据集,该数据集仅包含那些可能包含非背景区域的部分。
此外,我们正在重复执行前面的步骤 1,000 次,以微调模型。
- 构建一个函数,输入图像路径并预测图像的类别,同时修正边界框:
'TESTING'
import matplotlib.patches as mpatches
def test_predictions2(filename):
img = cv2.resize(cv2.imread(filename), (224,224))
candidates = extract_candidates(img)
_, ax = plt.subplots(1, 2)
ax[0].imshow(img)
ax[0].grid('off')
ax[0].set_title(filename.split('/')[-1])
pred = []
pred_class = []
del_new = []
for ix, candidate in enumerate(candidates):
l, t, w, h = np.array(candidate).astype(int)
img2 = img[t:t+h,l:l+w,:3]
img3 = cv2.resize(img2,(224,224))/255
img4 = vgg16_model.predict(img3.reshape(1,224,224,3))
final_pred = model.predict(img4/x_train.max())
delta_new = model2.predict(img4/x_train.max())[0]
pred.append(np.max(final_pred))
pred_class.append(np.argmax(final_pred))
del_new.append(delta_new)
pred = np.array(pred)
pred_class = np.array(pred_class)
non_bgs = (pred_class!=1)
pred = pred[non_bgs]
pred_class = pred_class[non_bgs]
del_new = np.array(del_new)
del_new = del_new[non_bgs]
del_pred = del_new*224
candidates = C = np.array(candidates)[non_bgs]
C = np.clip(C, 0, 224)
C[:,2] += C[:,0]
C[:,3] += C[:,1]
bbs_pred = candidates - del_pred
bbs_pred = np.clip(bbs_pred, 0, 224)
bbs_pred[:,2] -= bbs_pred[:,0]
bbs_pred[:,3] -= bbs_pred[:,1]
final_bbs_pred = bbs_pred[np.argmax(pred)]
x, y, w, h = final_bbs_pred
ax[1].imshow(img)
ax[1].grid('off')
rect = mpatches.Rectangle((x, y), w, h, fill=False, edgecolor='red', linewidth=1)
ax[1].add_patch(rect)
ax[1].set_title(labels[pred_class[np.argmax(pred)]])
- 提取仅包含一个物体的测试图像(因为我们已经构建了仅包含单一物体的图像模型):
single_object_images = []
for ix, xml in enumerate(XMLs[N:]):
xml_file = annotations + xml
fname = xml.split('.')[0]
with open(xml_file, "rb") as f: # notice the "rb" mode
xml = xmltodict.parse(f, xml_attribs=True)
l = []
if isinstance(xml["annotation"]["object"], list):
continue
single_object_images.append(xml["annotation"]['filename'])
if(ix>100):
break
在前面的代码中,我们正在遍历图像注释并识别包含单一物体的图像。
- 对单一物体图像进行预测:
test_predictions2(filename)
请注意,第二个模型能够修正边界框以适应人物;然而,边界框仍然需要稍微进一步修正。这可以通过在更多数据点上进行训练来实现。
执行非极大值抑制(NMS)
到目前为止,在前一节中,我们只考虑了没有背景的候选区域,并进一步考虑了具有最高物体兴趣概率的候选。然而,这在图像中存在多个物体的情况下无法正常工作。
在本节中,我们将讨论筛选候选区域提议的方法,以便我们能够提取图像中尽可能多的物体。
准备工作
我们采用的 NMS 策略如下:
-
从图像中提取区域提议。
-
重新调整区域提议的形状,并预测图像中包含的物体。
-
如果物体不是背景类,我们将保留该候选区域。
-
对于所有非背景类的候选区域,我们将按其包含物体的概率排序。
-
第一个候选(按照各类别概率降序排序后的第一个候选)将与其余所有候选区域进行 IoU 比较。
-
如果其他任何区域与第一个候选区域有较大重叠,它们将被丢弃。
-
在剩下的候选中,我们将再次考虑具有最高包含物体概率的候选区域。
-
我们将重复比较第一个候选(在前一步中已过滤且与第一个候选重叠较小的候选列表中的候选)与其余候选区域。
-
该过程将持续进行,直到没有剩余候选区域可以比较为止。
-
我们将绘制前述步骤后剩余候选区域的最终边界框。
如何执行……
非极大值抑制的 Python 代码如下。我们将从前面食谱中的第 14 步继续(代码文件及对应推荐数据集链接可在 GitHub 中的Region_proposal_based_object_detectionn.ipynb
找到)。
- 从图像中提取所有有较高信心包含非背景类物体的区域:
filename = jpegs + single_object_images[ix]
img = cv2.imread(filename)
img = cv2.resize(img,(224,224))
img_area = img.shape[0]*img.shape[1]
candidates = extract_candidates(img)
plt.imshow(img)
plt.grid('off')
我们正在考虑的图像如下:
- 对候选区域进行预处理——将它们通过 VGG16 模型,然后预测每个区域提议的类别以及区域的边界框:
pred = []
pred_class = []
del_new = []
for ix, candidate in enumerate(candidates):
l, t, w, h = np.array(candidate).astype(int)
img2 = img[t:t+h,l:l+w,:3]
img3 = cv2.resize(img2,(224,224))/255
img4 = vgg16_model.predict(img3.reshape(1,224,224,3))
final_pred = model.predict(img4/x_train.max())
delta_new = model2.predict(img4/x_train.max())[0]
pred.append(np.max(final_pred))
pred_class.append(np.argmax(final_pred))
del_new.append(delta_new)
pred = np.array(pred)
pred_class = np.array(pred_class)
- 提取非背景类预测结果及其对应的边界框修正值:
non_bgs = ((pred_class!=1))
pred = pred[non_bgs]
pred_class = pred_class[non_bgs]
del_new = np.array(del_new)
del_new = del_new[non_bgs]
del_pred = del_new*224
在前一步中,我们已过滤掉所有概率、类别和边界框修正值,针对非背景区域(预测类别1
在我们的数据准备过程中属于背景类别)。
- 使用边界框修正值校正候选区域:
candidates = C = np.array(candidates)[non_bgs]
C = np.clip(C, 0, 224)
C[:,2] += C[:,0]
C[:,3] += C[:,1]
bbs_pred = candidates - del_pred
bbs_pred = np.clip(bbs_pred, 0, 224)
此外,我们还确保了xmax
和ymax
坐标不能大于224
。
此外,我们需要确保边界框的宽度和高度不能为负值:
bbs_pred[:,2] -= bbs_pred[:,0]
bbs_pred[:,3] -= bbs_pred[:,1]
bbs_pred = np.clip(bbs_pred, 0, 224)
bbs_pred2 = bbs_pred[(bbs_pred[:,2]>0) & (bbs_pred[:,3]>0)]
pred = pred[(bbs_pred[:,2]>0) & (bbs_pred[:,3]>0)]
pred_class = pred_class[(bbs_pred[:,2]>0) & (bbs_pred[:,3]>0)]
- 绘制包含边界框的图像:
import matplotlib.patches as mpatches
fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(img)
for ix, (x, y, w, h) in enumerate(bbs_pred2):
rect = mpatches.Rectangle(
(x, y), w, h, fill=False, edgecolor='red', linewidth=1)
ax.add_patch(rect)
plt.axis('off')
plt.show()
-
对边界框执行非最大值抑制(NMS)。为此,我们将定义一个函数,该函数通过考虑两个边界框可能具有的最小交集(阈值)、边界框坐标以及与每个边界框相关的概率得分来执行 NMS,详细步骤如下:
- 计算每个边界框的
x
、y
、w
和h
值,它们的对应面积,以及它们的概率顺序:
- 计算每个边界框的
def nms_boxes(threshold, boxes, scores):
x = boxes[:, 0]
y = boxes[:, 1]
w = boxes[:, 2]
h = boxes[:, 3]
areas = w * h
order = scores.argsort()[::-1]
-
- 计算概率最高的候选框与其他候选框之间的交并比(IoU):
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x[i], x[order[1:]])
yy1 = np.maximum(y[i], y[order[1:]])
xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]])
yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]])
w1 = np.maximum(0.0, xx2 - xx1 + 1)
h1 = np.maximum(0.0, yy2 - yy1 + 1)
inter = w1 * h1
iou = inter / (areas[i] + areas[order[1:]] - inter)
-
- 确定那些 IoU 小于阈值的候选框:
inds = np.where(ovr <= threshold)[0]
order = order[inds + 1]
在前面的步骤中,我们确保了接下来的一组候选框(除了第一个候选框)将循环执行相同的步骤(注意函数开始处的while
循环)。
-
- 返回需要保留的候选框的索引:
keep = np.array(keep)
return keep
- 执行前面的函数:
keep_box_ixs = nms_boxes(0.3, bbs_pred2, pred)
- 绘制前一步留下的边界框:
import matplotlib.patches as mpatches
fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(img)
for ix, (x, y, w, h) in enumerate(bbs_pred2):
if ix not in keep_box_ixs:
continue
rect = mpatches.Rectangle((x, y), w, h, fill=False, edgecolor='red', linewidth=1)
ax.add_patch(rect)
centerx = x + w/2
centery = y + h - 10
plt.text(centerx, centery,labels[pred_class[ix]]+" "+str(round(pred[ix],2)),fontsize = 20,color='red')
plt.axis('off')
plt.show()
从前面的截图中,我们看到我们移除了所有其他作为区域提案生成的边界框。
使用基于锚框的算法检测人物
基于区域提案的 CNN 的一个缺点是它无法实现实时物体识别,因为选择性搜索需要相当长的时间来提出区域。这使得基于区域提案的物体检测算法在像自动驾驶汽车这样的实时检测场景中不具备实用性。
为了实现实时检测,我们将从零开始构建一个受You Only Look Once(YOLO)算法启发的模型,该模型可以查看包含人物的图像,并在图像中的人物周围绘制边界框。
准备就绪
为了理解 YOLO 如何克服生成区域提案时所消耗的时间这一缺点,让我们将“YOLO”一词拆解成其组成部分——我们将在神经网络的单次前向传播中完成所有预测(图像类别以及边界框)。与基于区域提案的 CNN 方法相比,我们首先使用选择性搜索算法得到区域提案,然后在其基础上构建分类算法。
为了弄清楚 YOLO 的工作细节,让我们通过一个简单的示例。假设输入图像如下所示——该图像被划分为一个 3 x 3 的网格:
我们的神经网络模型的输出将是 3 x 3 x 5 的大小,其中前 3 x 3 表示图像中的网格数,第一个输出通道对应网格包含物体的概率,另外四个通道是网格对应的* x , y , w , h *坐标的增量。
另一个我们使用的工具是锚框。实质上,我们已经知道,在我们拥有的图像集合中,有些形状是已知的。例如,汽车的形状通常宽度大于高度,而站立的人物通常高度大于宽度。
因此,我们将把图像集中所有的高度和宽度值聚类成五个簇,这样就会得到五个锚框的高度和宽度,用于识别图像中的物体边界框。
如果图像中有五个锚框工作,则输出将是 3 x 3 x 5 x 5,其中 5 x 5 对应每个锚框的五个组成部分(一个物体的概率和四个* x , y , w , h *的增量)。
从前面的内容可以看出,3 x 3 x 5 x 5 的输出可以通过神经网络模型的单次前向传播生成。
在接下来的部分,我们将看到如何生成锚框大小的伪代码:
-
提取数据集中所有图像的宽度和高度。
-
运行一个具有五个簇的 k-means 聚类,来识别图像中宽度和高度的簇。
-
五个簇中心对应五个锚框的宽度和高度,用于构建模型。
此外,在接下来的部分,我们将了解 YOLO 算法是如何工作的:
-
将图像分割成固定数量的网格单元。
-
与图像的真实边界框中心相对应的网格将负责预测边界框。
-
锚框的中心将与网格的中心相同。
-
创建训练数据集:
-
对于包含物体中心的网格,因变量为 1,且需要为每个锚框计算* x , y , w , h *的增量。
-
对于不包含物体中心的网格,因变量为零,且* x , y , w , h *的增量不重要。
-
-
在第一个模型中,我们将预测包含图像中心的锚框和网格单元组合。
-
在第二个模型中,我们预测锚框的边界框修正。
如何实现…
我们将构建用于执行人脸检测的代码(代码文件和相应推荐数据集链接可以在 GitHub 中作为Anchor_box_based_person_detection.ipynb
找到,连同推荐的数据集):
- 下载包含一组图像、图像中物体及其对应边界框的数据集。可以在 GitHub 上找到推荐的数据集及其相关代码文件供你使用。
一个示例图像及其相应的边界框位置输出看起来类似于我们在"基于区域提议的 CNN 物体检测"食谱的第 1 步中看到的内容。
- 导入相关的包,如下所示:
import matplotlib.pyplot as plt
%matplotlib inline
import tensorflow as tf, selectivesearch
import json, scipy, os, numpy as np,argparse,time, sys, gc, cv2, xmltodict
from copy import deepcopy
- 定义 IoU 提取函数,如下所示:
def extract_iou2(candidate, current_y,img_shape):
boxA = deepcopy(candidate)
boxB = deepcopy(current_y)
boxA[2] += boxA[0]
boxA[3] += boxA[1]
iou_img1 = np.zeros(img_shape)
iou_img1[boxA[1]:boxA[3],boxA[0]:boxA[2]]=1
iou_img2 = np.zeros(img_shape)
iou_img2[int(boxB[1]):int(boxB[3]),int(boxB[0]):int(boxB[2])]=1
iou = np.sum(iou_img1*iou_img2)/(np.sum(iou_img1)+np.sum(iou_img2)-np.sum(iou_img1*iou_img2))
return iou
-
将锚框的宽度和高度定义为图像总宽度和高度的百分比:
-
- 确定边界框中人物的所有可能宽度和高度:
y_train = []
for i in mylist[:10000]:
xml_file = xml_filepath +i
arg1=i.split('.')[0]
with open(xml_file, "rb") as f: # notice the "rb" mode
d = xmltodict.parse(f, xml_attribs=True)
l=[]
if type(d["annotation"]["object"]) == type(l):
discard=1
else:
x1=((float(d['annotation']['object']
['bndbox']['xmin'])))/(float(d['annotation']['size']['width']))
x2=((float(d['annotation']['object']
['bndbox']['xmax'])))/(float(d['annotation']['size']['width']))
y1=((float(d['annotation']['object']
['bndbox']['ymin'])))/(float(d['annotation']['size']['height']))
y2=((float(d['annotation']['object']
['bndbox']['ymax'])))/(float(d['annotation']['size']['height']))
cls=d['annotation']['object']['name']
if(cls == 'person'):
y_train.append([x2-x1, y2-y1])
在前面的代码中,我们遍历所有仅包含一个物体的图像,然后计算该图像中的边界框的宽度和高度,前提是图像中包含一个人。
-
- 使用五个中心进行 k-means 聚类:
y_train = np.array(y_train)
from sklearn.cluster import KMeans
km = KMeans(n_clusters=5)
km.fit(y_train)
km.cluster_centers_
前面的步骤产生了如下所示的聚类中心:
anchors = [[[0.84638352, 0.90412013],
[0.28036872, 0.58073186],
[0.45700897, 0.87035502],
[0.15685545, 0.29256264],
[0.59814951, 0.64789503]]]
-
创建训练数据集:
- 初始化空列表,以便在进一步处理时向其中添加数据:
k=-1
pre_xtrain = []
y_train = []
cls = []
xtrain=[]
final_cls = []
dx = []
dy = []
dw= []
dh = []
final_delta = []
av = 0
x_train = []
img_paths = []
label_coords = []
y_delta = []
anc = []
-
- 遍历数据集,处理只包含一个物体且该物体为人的图像:
for i in mylist[:10000]:
xml_file = xml_filepath +i
arg1=i.split('.')[0]
discard=0
with open(xml_file, "rb") as f: # notice the "rb" mode
d = xmltodict.parse(f, xml_attribs=True)
l=[]
if type(d["annotation"]["object"]) == type(l):
discard=1
else:
coords={arg1:[]}
pre_xtrain.append(arg1)
m=pre_xtrain[(k+1)]
k = k+1
if(discard==0):
x1=((float(d['annotation']['object']['bndbox']['xmin'])))/(float(d['annotation']['size']['width']))
x2=((float(d['annotation']['object']['bndbox']['xmax'])))/(float(d['annotation']['size']['width']))
y1=((float(d['annotation']['object']['bndbox']['ymin'])))/(float(d['annotation']['size']['height']))
y2=((float(d['annotation']['object']['bndbox']['ymax'])))/(float(d['annotation']['size']['height']))
cls=d['annotation']['object']['name']
if(cls == 'person'):
coords[arg1].append(x1)
coords[arg1].append(y1)
coords[arg1].append(x2)
coords[arg1].append(y2)
coords[arg1].append(cls)
前面的代码附加了物体的位置(经过归一化的图像宽度和高度)。
-
- 调整人物图像的大小,使所有图像具有相同的形状。此外,将图像缩放至值域在零到一之间:
filename = base_dir+m+'.jpg'
# reference to jpg files here
img = filename
img_size=224
img = cv2.imread(filename)
img2 = cv2.resize(img,(img_size,img_size))
img2 = img2/255
-
- 提取物体的边界框位置,并且还要提取归一化的边界框坐标:
current_y = [int(x1*224), int(y1*224), int(x2*224), int(y2*224)]
current_y2 = [float(d['annotation']['object']['bndbox']['xmin']), float(d['annotation']['object']['bndbox']['ymin']),
float(d['annotation']['object']['bndbox']['xmax'])-float(d['annotation']['object']['bndbox']['xmin']),
float(d['annotation']['object']['bndbox']['ymax'])-float(d['annotation']['object']['bndbox']['ymin'])]
label_center = [(current_y[0]+current_y[2])/2,(current_y[1]+current_y[3])/2]
label = current_y
-
- 提取输入图像的 VGG16 特征:
vgg_predict =vgg16_model.predict(img2.reshape(1,img_size,img_size,3))
x_train.append(vgg_predict)
到这一步,我们已经创建了输入特征。
-
- 让我们创建输出特征——在本例中,我们将为类标签生成 5 x 5 x 5 的输出,为边界框修正标签生成 5 x 5 x 20 的标签:
target_class = np.zeros((num_grids,num_grids,5))
target_delta = np.zeros((num_grids,num_grids,20))
在前面的步骤中,我们为目标类和边界框修正初始化了零数组:
def positive_grid_cell(label,img_width = 224, img_height = 224):
label_center = [(label[0]+label[2])/(2),(label[1]+label[3])/(2)]
a = int(label_center[0]/(img_width/num_grids))
b = int(label_center[1]/(img_height/num_grids))
return a, b
在前面的步骤中,我们定义了一个包含物体中心的函数:
a,b = positive_grid_cell(label)
前面的代码帮助我们将1
类分配给包含物体中心的网格,其他所有网格的标签将为零。
此外,让我们定义一个函数,找到与感兴趣物体形状最接近的锚框:
def find_closest_anchor(label,img_width, img_height):
label_width = (label[2]-label[0])/img_width
label_height = (label[3]-label[1])/img_height
label_width_height_array = np.array([label_width, label_height])
distance = np.sum(np.square(np.array(anchors) - label_width_height_array), axis=1)
closest_anchor = anchors[np.argmin(distance)]
return closest_anchor
前面的代码将图像中感兴趣物体的宽度和高度与所有可能的锚框进行比较,并识别出最接近图像中实际物体宽度和高度的锚框。
最后,我们还将定义一个计算锚框边界框修正的函数,如下所示:
def closest_anchor_corrections(a, b, anchor, label, img_width, img_height):
label_center = [(label[0]+label[2])/(2),(label[1]+label[3])/(2)]
anchor_center = [a*img_width/num_grids , b*img_height/num_grids ]
dx = (label_center[0] - anchor_center[0])/img_width
dy = (label_center[1] - anchor_center[1])/img_height
dw = ((label[2] - label[0])/img_width) / (anchor[0])
dh = ((label[3] - label[1])/img_height) / (anchor[1])
return dx, dy, dw, dh
现在我们已经准备好创建目标数据了:
for a2 in range(num_grids):
for b2 in range(num_grids):
for m in range(len(anchors)):
dx, dy, dw, dh = closest_anchor_corrections(a2, b2, anchors[m], label, 224, 224)
target_class[a2,b2,m] = 0
target_delta[a2,b2,((4*m)):((4*m)+4)] = [dx, dy, dw, dh]
anc.append(anchors[m])
if((anchors[m] == find_closest_anchor(label,224, 224)) & (a2 == a) & (b2 == b)):
target_class[a2,b2,m] = 1
在前面的代码中,当考虑到的锚框是与图像中物体形状最接近的锚框时,我们将目标类赋值为1
。
我们还将边界框修正存储在另一个列表中:
y_train.append(target_class.flatten())
y_delta.append(target_delta)
- 构建一个模型来识别最可能包含物体的网格单元和锚框:
from keras.optimizers import Adam
optimizer = Adam(lr=0.001)
from keras.layers import BatchNormalization
from keras import regularizers
model = Sequential()
model.add(BatchNormalization(input_shape=(7,7,512)))
model.add(Conv2D(1024, (3,3), activation='relu',padding='valid'))
model.add(BatchNormalization())
model.add(Conv2D(5, (1,1), activation='relu',padding='same'))
model.add(Flatten())
model.add(Dense(125, activation='sigmoid'))
model.summary()
- 创建用于分类的输入和输出数组:
y_train = np.array(y_train)
x_train = np.array(x_train)
x_train = x_train.reshape(x_train.shape[0],7,7,512)
- 编译并拟合分类模型:
model.compile(loss='binary_crossentropy', optimizer=optimizer)
model.fit(x_train/np.max(x_train), y_train, epochs=5, batch_size = 32, validation_split = 0.1, verbose = 1)
- 从前面的模型中,我们能够识别出最可能包含人物的网格单元和锚框组合。在此步骤中,我们将构建一个数据集,其中我们修正最有可能包含物体的预测边界框:
delta_x = []
delta_y = []
for i in range(len(x_train)):
delta_x.append(x_train[i])
delta = y_delta[i].flatten()
coord = np.argmax(model.predict(x_train[i].reshape(1,7,7,512)/12))
delta_y.append(delta[(coord*4):((coord*4)+4)])
在前一步中,我们已经准备好了输入(即原始图像的 VGG16 特征)和输出边界框修正,用于预测最有可能包含物体的区域。
请注意,我们将coord
乘以四,因为对于每个网格单元和锚框组合,都有四个可能的修正值,分别对应* x 、 y 、 w * 和 * h *。
-
构建一个模型,用来预测* x 、 y 、 w * 和 * h * 坐标的修正:
- 创建输入和输出数组,并对四个边界框修正值进行标准化,使得所有四个值具有相似的范围:
delta_x = np.array(delta_x)
delta_y = np.array(delta_y)
max_y = np.max(delta_y, axis=0)
delta_y2 = deltay/max_y
-
- 构建一个模型,根据 VGG16 特征预测边界框修正:
model2 = Sequential()
model2.add(BatchNormalization(input_shape=(7,7,512)))
model2.add(Conv2D(1024, (3,3), activation='relu',padding='valid'))
model2.add(BatchNormalization())
model2.add(Conv2D(5, (1,1), activation='relu',padding='same'))
model2.add(Flatten())
model2.add(Dense(2, activation='linear'))
- 编译并拟合模型:
model2.compile(loss = 'mean_absolute_error', optimizer = optimizer)
model2.fit(delta_x/np.max(x_train), delta_y2, epochs = 10, batch_size = 32, verbose = 1, validation_split = 0.1)
-
在新图像上预测边界框:
- 提取最有可能包含物体的位置:
img = cv2.imread('/content/Hemanvi.jpg')
img = cv2.resize(img,(224,224))
img = img/255
img2 = vgg16_model.predict(img.reshape(1,224,224,3))
arg = np.argmax(model.predict(img2/np.max(x_train)))
在前面的步骤中,我们选择了一张包含人物的图像,并调整其大小,以便进一步处理以提取 VGG16 特征。最后,我们识别出最有可能包含人物位置的锚框。
-
- 提取最有可能包含图像的网格单元和锚框组合。
上一步预测了最有可能包含感兴趣物体的网格单元和锚框组合,具体做法如下:
count = 0
for a in range(num_grids):
for b in range(num_grids):
for c in range(len(anchors)):
if(count == arg):
a2 = a
b2 = b
c2 = c
count+=1
在前面的代码中,a2
和b2
将是最有可能包含物体的网格单元(即* x 轴和 y *轴的组合),c2
是可能与物体形状相同的锚框。
-
- 预测* x 、 y 、 w * 和 * h * 坐标的修正:
pred = model2.predict(img2/np.max(delta_x))[0]
-
- 去标准化预测的边界框修正:
pred1 = pred*max_y
-
- 提取最终修正后的* x 、 y 、 w * 和 * h * 坐标:
xmin = pred1[0]*224+a2*224/num_grids - (anchors[c2][0]*pred1[2] * 224)/2
ymin = pred1[1]*224+b2*224/num_grids - (anchors[c2][1]*pred1[3] * 224)/2
w = anchors[c2][0]*pred1[2] * 224
h = anchors[c2][1]*pred1[3] * 224
-
- 绘制图像和边界框:
import matplotlib.patches as mpatches
cand = [xmin, ymin, w, h]
cand = np.clip(cand, 1, 223)
fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(img)
rect = mpatches.Rectangle(
(cand[0], cand[1]), cand[2], cand[3], fill=False, edgecolor='red', linewidth=1)
ax.add_patch(rect)
plt.grid('off')
plt.show()
这种方法的一个缺点是,当我们检测的物体相较于图像大小非常小时,检测变得更加困难。
还有更多…
假设我们考虑一个要检测的物体较小的场景。如果将这张图片传入预训练网络中,该物体会在早期层中被检测到,因为在最后几层中,图像会经过多个池化层处理,导致物体被压缩到一个非常小的空间中。
同样地,如果要检测的物体较大,那么该物体会在预训练网络的最后几层中被检测到。
单次检测器使用一个预训练网络,其中网络的不同层负责检测不同类型的图像:
来源:https://arxiv.org/pdf/1512.02325.pdf
在前面的图示中,你需要注意,不同层的特征会通过一个全连接层,最终被连接在一起,以便构建和微调模型。
此外,YOLO 也可以基于此教程实现:pjreddie.com/darknet/yolo/
。