使用预处理的卷积神经网络来检测猫狗图片
前言
本文是前一篇“使用深度学习分类猫狗图片”的续篇,在原文的基础上对神经网络进行改进。需要先阅读前文。
在前文的模型上,通过进一步使用正则化方法以及调节网络参数(比如每个卷积层的过滤器个数或网络中的 4 层数),可以得到更高的精度,可以达到 86% 或 87%。但只靠从头开始训练自己的卷积神经网络, 再想提高精度就十分困难,因为可用的数据太少。想要在这个问题上进一步提高精度,下一步 需要使用预训练的模型。
一、预训练神经网络是什么?
想要将神经网络运用在小型的数据集上,预训练神经网络是一种较高效的方法。预训练神经网络(pretrained network)是一种已经在大型数据集(通常是大型图像分类任务)上训练好的,保存下来的神经网络模型。如果这个原始数据集足够大且足够通用,那么预训练神经网络学到的特征的空间层次结构可以有效的作为计算机视觉方向上的通用模型。因此这些特征可以各种不同计算机的视觉问题上,即使这些新问题涉及的任务与原始问题完全不同,但还能达到较好的效果。
比如在ImageNet上训练了一个用于分类日常用品的神经网络,然后将这个训练好的神经网络应用于另一个不相关的问题上,比如识别图像中的鸟类。这种学到的特征在不同问题之间的可移植性,是深度学习与许多 早期浅层学习方法相比的重要优势,它使得深度学习对小数据问题非常有效。
假如有一个在ImageNet的数据集(140万张图片,1000个分类)上训练好的神经网络模型,ImageNet 中包含许多动物类别,其中包括不同种类的猫和狗,因此可 以认为它在猫狗分类问题上也能有良好的表现。
这里我们使用VGG16 架构。
二、特征提取
1. 概念介绍
特征提取是使用之前网络学到的表示来从新样本中提取出有趣的特征。然后将这些特征输 入一个新的分类器,从头开始训练。
众所周知,用于图像分类的卷积神经网络包含两部分: 1.一系列卷基层+池化层 2. 密集连接分类器。 第一部分叫做模型的卷积基(convolutional base)。对于卷积神经网络而言,特征提取就是取出之前训练好的网络的卷积基,在上面运行新数据,然后在输出上面训练一个新的分类器,如下图所示,保持卷积基部变,替换新的分类器。
一般来说,我们都会重复使用卷积基,而避免重复使用分类器,因为卷积基学到的表示更加通用。某个卷积层提取的表示的通用性(以及可复用性)取决于该层在模型中的深度。模型中更靠近底部的层提取的是局部的、高度通用的特征图(比如视觉边缘、颜色和纹理),而更 靠近顶部的层提取的是更加抽象的概念(比如“猫耳朵”或“狗眼睛”)。注意⚠️这里更靠近底部的层是指在定义模型时先添加到模型中的层,而更靠近顶部的层则是后添加到模型中的层。
如果你的新数据集与原始模型训练的数据集有很大差异,那么最好只使用模型的前几层来做特征提取,而不是使用整个卷积基。
1.将VGG16卷积基实例化
本例中,由于 ImageNet 的类别中包含多种狗和猫的类别,所以重复使用原始模型密集连接 层中所包含的信息可能很有用。但我们选择不这么做,以便涵盖新问题的类别与原始模型的类 别不一致的更一般情况。我们来实践一下,使用在 ImageNet 上训练的 VGG16 网络的卷积基从 猫狗图像中提取有趣的特征,然后在这些特征上训练一个猫狗分类器。
VGG16 等模型内置于 Keras 中。你可以从 keras.applications 模块中导入。下面是 keras.applications 中的一部分图像分类模型(都是在 ImageNet 数据集上预训练得到的)
代码如下(示例):
from keras.applications import VGG16
"""
weights 指定模型初始化的权重检查点。
include_top 指定模型最后是否包含密集连接分类器(不提倡包含)
input_shape 是输入到网络中的图像张量的形状,若不传入该参数,则该网络可以处理任何形状的输入
"""
conv_base = VGG16(weights='imagenet',
include_top=False,
input_shape=(150, 150, 3))
VGG16 卷积基的详细架构如下所示。它和你已经熟悉的简单卷积神经网络很相似。
>>> conv_base.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 150, 150, 3) 0
_________________________________________________________________
block1_conv1 (Conv2D) (None, 150, 150, 64) 1792
_________________________________________________________________
block1_conv2 (Conv2D) (None, 150, 150, 64) 36928
_________________________________________________________________
block1_pool (MaxPooling2D) (None, 75, 75, 64) 0
_________________________________________________________________
block2_conv1 (Conv2D) (None, 75, 75, 128) 73856
_________________________________________________________________
block2_conv2 (Conv2D) (None, 75, 75, 128) 147584
_________________________________________________________________
block2_pool (MaxPooling2D) (None, 37, 37, 128) 0
_________________________________________________________________
block3_conv1 (Conv2D) (None, 37, 37, 256) 295168
_________________________________________________________________
block3_conv2 (Conv2D) (None, 37, 37, 256) 590080
_________________________________________________________________
block3_conv3 (Conv2D) (None, 37, 37, 256) 590080
_________________________________________________________________
block3_pool (MaxPooling2D) (None, 18, 18, 256) 0
_________________________________________________________________
block4_conv1 (Conv2D) (None, 18, 18, 512) 1180160
_________________________________________________________________
block4_conv2 (Conv2D) (None, 18, 18, 512) 2359808
_________________________________________________________________
block4_conv3 (Conv2D) (None, 18, 18, 512) 2359808
_________________________________________________________________
block4_pool (MaxPooling2D) (None, 9, 9, 512) 0
_________________________________________________________________
block5_conv1 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv2 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv3 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_pool (MaxPooling2D) (None, 4, 4, 512) 0
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
最后的特征图形状为 (4, 4, 512)。我们将在这个特征上添加一个密集连接分类器。
在你的数据集上运行卷积基,将输出保存成硬盘中的 Numpy 数组,然后用这个数据作为输入,输入到独立的密集连接分类器中(与本书第一部分介绍的分类器类似)。这种 方法速度快,计算代价低,因为对于每个输入图像只需运行一次卷积基,而卷积基是目 前流程中计算代价最高的。但出于同样的原因,这种方法不允许你使用数据增强
2.使用预训练的卷积基提取特征
首先,运行 ImageDataGenerator 实例,将图像及其标签提取为 Numpy 数组。我们需要调用 conv_base 模型的 predict 方法来从这些图像中提取特征。
代码如下(示例):
import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator
"""
此部分的文件夹中的图片在上一篇文章中已经分类完成,因此这里只需要定义文件夹名即可使用
对此不熟悉的朋友可以查看上一篇文章
"""
base_dir = '/Users/fchollet/Downloads/cats_and_dogs_small'
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')
datagen = ImageDataGenerator(rescale=1./255)
batch_size = 20
def extract_features(directory, sample_count):
"""
根据上一部观察conv_base.summary的输出可以得到conv_base的输出形状为(4,4,512)
"""
features = np.zeros(shape=(sample_count,4,4,512))
labels = np.zeros(shape=(sample_count))
generator = datagen.flow_from_directory(
directory,
target_size = (150,150),
batch_size = batch_size,
class_mode = 'binary'
)
i=0
for inputs_batch, labels_batch in generator:
"""
在这里调用conv_base的predict函数,来将图片的特征提取为 Numpy 数组
"""
features_batch = conv_base.predict(inputs_batch)
features[i*batch_size : (i + 1) * batch_size] = features_batch
labels[i * batch_size : (i + 1) * batch_size] = labels_batch
i += 1
if i * batch_size >= sample_count:
"""
注意,这些生成器在循环中不断 生成数据,所以你必须在读取完 所有图像后终止循环
"""
break
return features, labels
接下来分别调用extract_features函数即可获得训练、验证、测试集中图片的特征:
train_features, train_labels = extract_features(train_dir, 2000)
validation_features, validation_labels = extract_features(validation_dir, 1000)
test_features, test_labels = extract_features(test_dir, 1000)
"""
目前,提取的特征形状为 (samples, 4, 4, 512)。
我们要将其输入到密集连接分类器中, 所以首先必须将其形状展平为 (samples, 8192)。
"""
train_features = np.reshape(train_features, (2000, 4 * 4 * 512))
validation_features = np.reshape(validation_features, (1000, 4 * 4 * 512))
test_features = np.reshape(test_features, (1000, 4 * 4 * 512))
现在你可以定义你的密集连接分类器(注意要使用 dropout 正则化),并在刚刚保存的数据 和标签上训练这个分类器。
from keras import models from keras
import layers from keras
import optimizers
model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_dim=4 * 4 * 512))
model.add(layers.Dropout(0.5))# 使用 dropout 正则化
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer=optimizers.RMSprop(lr=2e-5), loss='binary_crossentropy',
metrics=['acc'])
history = model.fit(train_features, train_labels, epochs=30,
batch_size=20,
validation_data=(validation_features, validation_labels))
3. 绘制图像
训练速度非常快,因为你只需处理两个 Dense 层。即使在 CPU 上运行,每轮的时间也不 到一秒钟。我们来看一下训练期间的损失曲线和精度曲线
import matplotlib.pyplot as plt
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()
简单特征提取的训练精度和验证精度:
简单特征提取的训练损失和验证损失:
我们的验证精度达到了约 90%,比上一篇文章从头开始训练的小型模型效果要好得多。但从图中也可以看出,虽然 dropout 比率相当大,但模型几乎从一开始就过拟合。这是因为本方法没有 使用数据增强,而数据增强对防止小型图像数据集的过拟合非常重要。
总结
本文主要是对上一篇“利用深度学习预测猫狗照片”中提到的模型的升级,采用预训练的神经网络来训练数据。从上一篇文章中的85%的准确率可以提升至90%以上