在小型数据集上从头开始训练卷积网络
使用非常少的数据来训练图像分类模型是一种常见的情况,如果您曾经在专业环境中使用计算机视觉,那么您可能会在实践中遇到这种情况。
拥有“少数”样本意味着从几百到几万张图像的任何地方。作为一个实际的例子,我们将在包含4000张猫和狗(2000只猫,2000只狗)图片的数据集中,将图像分类为“狗”或“猫”。我们将使用2000张图片进行培训,1000张图片进行验证,最后1000张图片进行测试。
在本节中,我们将回顾解决这个问题的一个基本策略:根据我们仅有的少量数据从头开始训练一个新模型。我们将在2000个训练样本上天真地训练一个小型卷积神经网络,不需要任何正则化,来设定一个可以达到的基线。这将使我们的分类准确率达到71%。在这一点上,我们的主要问题是过度拟合。然后我们将介绍一种强大的数据增强技术,以减轻计算机视觉中的过拟合。通过利用数据扩展,我们将改进我们的网络,使其达到82%的准确率。
在下一节中,我们将回顾将深度学习应用于小数据集的两项更重要的技术:使用预先训练好的网络进行特征提取(这将使我们的准确率达到90%到93%),以及微调预先训练好的网络(这将使我们的最终准确率达到95%)。这三种策略——从零开始训练一个小模型,使用预先训练好的模型进行特征提取,以及对预先训练好的模型进行微调——将构成您未来的工具箱,用于解决使用小数据集进行计算机视觉的问题。
小数据问题的深度学习相关性
有时你会听到,只有当大量数据可用时,深度学习才有效。这在一定程度上是一个有效的观点:深度学习的一个基本特征是它能够在训练数据中自己找到感兴趣的特征,而不需要任何手动特征工程,并且只有在有大量训练示例的情况下才能实现这一点。这尤其适用于输入样本是高维的问题,比如图像
然而,构成“大量”样本的因素是相对的——首先是相对于你试图训练的网络的规模和深度。训练一个convnet来解决一个只有几十个样本的复杂问题是不可能的,但是如果模型很小且正则化良好,并且任务很简单,那么几百个样本可能就足够了。因为convnets学习局部的、平移不变的特征,所以它们在感知问题上非常有效。在一个非常小的图像数据集上从头开始训练convnet仍然会产生合理的结果,尽管数据相对缺乏,而不需要任何自定义的特征工程。您将在本节中看到这一点。
但更重要的是,深度学习模型本质上是高度可重用的:比如,你可以采用在大规模数据集上训练的图像分类或语音到文本模型,然后在一个显著不同的问题上重用它,只需稍加更改。具体来说,在计算机视觉的情况下,许多预先训练的模型(通常在ImageNet数据集上训练)现在可以公开下载,并且可以用来从很少的数据中引导强大的视觉模型。这就是我们下一节要做的。
现在,让我们从掌握数据开始
我们将使用的猫狗数据集没有打包在Keras中。
Keras是一个高层神经网络API,Keras由纯Python编写而成并基Tensorflow、Theano以及CNTK后端。Keras 为支持快速实验而生,能够把你的idea迅速转换为结果,如果你有如下需求,请选择Kera
- 简易和快速的原型设计(keras具有高度模块化,极简,和可扩充特性)
- 支持CNN和RNN,或二者的结合
- 无缝CPU和GPU
它由Kaggle在2013年末公开并作为一项计算视觉竞赛的一部分,当时卷积神经网络还不是主流算法。我们可以从https://www.kaggle.com/c/dogs-vs-cats/data中下载我们需要的数据集。
不出所料,2013年的猫狗Kaggle比赛中,使用卷积神经网络(convnets)的参赛者获得了冠军。最佳参赛作品的正确率可达95%。在我们自己的示例中,我们将相当接近这个精度(在下一节中),尽管我们的模型所使用的数据还不到竞争对手可用数据的10%。这个原始数据集包含25,000张狗和猫的图像(每个类有12,500张),有543MB大(压缩)。下载并解压缩后,我们将创建一个包含三个子集的新数据集:一个包含每个类1000个样本的训练集,一个包含每个类500个样本的验证集,最后一个包含每个类500个样本的测试集。
这些图片是中等分辨率的彩色jpeg。它们是这样的:
import keras
keras.__version__
import os, shutil #复制文件
# 原始目录所在的路径
# 数据集未压缩
original_dataset_dir = 'D:/Workspaces/Jupyter-notebook/datasets/mldata/kaggle_original_data'
# 我们将在其中的目录存储较小的数据集
base_dir = 'D:/Workspaces/Jupyter-notebook/datasets/mldata/cats_and_dogs_small'
os.mkdir(base_dir)
# # 训练、验证、测试数据集的目录
train_dir = os.path.join(base_dir, 'train')
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation')
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test')
os.mkdir(test_dir)
# 猫训练图片所在目录
train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir)
# 狗训练图片所在目录
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)
# 猫验证图片所在目录
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
# 狗验证数据集所在目录
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)
# 猫测试数据集所在目录
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)
# 狗测试数据集所在目录
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_dir)
# 将前1000张猫图像复制到train_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_cats_dir, fname)
shutil.copyfile(src, dst)
# 将下500张猫图像复制到validation_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_cats_dir, fname)
shutil.copyfile(src, dst)
# 将下500张猫图像复制到test_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_cats_dir, fname)
shutil.copyfile(src, dst)
# 将前1000张狗图像复制到train_dogs_dir
fnames = ['dog.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_dogs_dir, fname)
shutil.copyfile(src, dst)
# 将下500张狗图像复制到validation_dogs_dir
fnames = ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_dogs_dir, fname)
shutil.copyfile(src, dst)
# 将下500张狗图像复制到test_dogs_dir
fnames = ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_dogs_dir, fname)
shutil.copyfile(src, dst)
作为健全性检查,让我们计算一下每个训练分组(训练/验证/测试)中有多少张图片:
print('total training cat images:', len(os.listdir(train_cats_dir)))
print('total training dog images:', len(os.listdir(train_dogs_dir)))
print('total validation cat images:', len(os.listdir(validation_cats_dir)))
print('total validation dog images:', len(os.listdir(validation_dogs_dir)))
print('total test cat images:', len(os.listdir(test_cats_dir)))
print('total test dog images:', len(os.listdir(test_dogs_dir)))
所以我们确实有2000个训练图像,然后是1000个验证图像和1000个测试图像。在每个分组中,每个类别中的样本数均相同:这是一个平衡的二进制分类问题,这意味着分类准确性将是成功与否的适当衡量标准。
在上一个示例中,我们已经为MNIST构建了一个小型卷积网络,因此您应该熟悉它们。我们将重用相同的一般结构:我们的convnet将是交替的Conv2D(具有relu激活)和MaxPooling2D层的堆栈。
但是,由于我们要处理更大的图像和更复杂的问题,因此我们将使我们的网络更大:它将具有一个Conv2D + MaxPooling2D阶段。这既可以增加网络的容量,又可以进一步减小要素图的大小,以使当我们到达Flatten层时它们不会太大。在这里,由于我们从150x150大小的输入(某种程度上是任意选择)开始,因此我们在Flatten层之前得到了7x7大小的特征图。
请注意,特征图的深度在网络中逐渐增加(从32到128),而特征图的大小在减小(从148x148到7x7)。在几乎所有的卷积网络中都会看到这种模式。
由于我们正在攻击二进制分类问题,因此我们以单个单元(大小为1的密集层)和S型激活来结束网络。该单元将对网络正在查看一个或另一个类别的概率进行编码。
你面对的是一个二分类问题,所以网络最后一层是使用 sigmoid 激活的单一单元(大小为1 的 Dense 层)。这个单元将对某个类别的概率进行编码。
from keras import layers
from keras import models
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu',
input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
让我们看一下特征贴图的尺寸如何随每个连续层变化:
model.summary()
对于我们的编译步骤,我们将像往常一样使用RMSprop优化器。由于我们以单个S形单位结束网络,因此我们将使用二进制交叉熵作为损失(提醒一下,请参阅第4章第5节中的表格,以了解在各种情况下要使用的损失函数的备忘单)。
from keras import optimizers
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
如你现在所知,在将数据输入我们的网络之前,应将其格式化为经过适当预处理的浮点张量。当前我们的数据以JPEG文件的形式位于驱动器上,因此将其放入网络的步骤大致如下:
from keras.preprocessing.image import ImageDataGenerator
# All images will be rescaled by 1./255
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
# This is the target directory
train_dir,
# All images will be resized to 150x150
target_size=(150, 150),
batch_size=20,
# Since we use binary_crossentropy loss, we need binary labels
class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
让我们看一看这些生成器之一的输出:它生成一批150x150 RGB图像[形状(20,150,150,3)]和二进制标签[形状(20,)]。每个批次中的样本数20(批次大小)。请注意,生成器无限期地产生这些批处理:它只是无限循环地遍历目标文件夹中存在的图像。因此,我们需要在某个时候中断(break)迭代循环。
for data_batch, labels_batch in train_generator:
print('data batch shape:', data_batch.shape)
print('labels batch shape:', labels_batch.shape)
break
让我们使用生成器将模型拟合到数据。我们使用fit_generator方法来拟合它,它在数据生成器上的效果和 fit 相同。它期望Python生成器作为第一个参数,像我们一样将无限期地产生大量输入和目标,比如 train_generator。由于数据是无休止地生成的,因此Keras 模型需要在声明一个时期之前知道示例要从生成器中抽取多少个样本。这是steps_per_epoch参数的作用:从生成器中提取steps_per_epoch批处理后,即运行steps_per_epoch梯度下降步骤后,拟合过程将转到下一个轮次。在我们的示例中,批次为20个样本,所以读取完所有 2000 个样本需要 100 个批量。
使用fit_generator时,可能会传递一个validation_data参数,就像使用fit方法一样。重要的是,该参数本身可以是数据生成器,但也可以是Numpy数组的元组。如果将生成器作为validation_data传递,则预期该生成器会无休止地产生多批验证数据,因此,您还应指定validation_steps参数,说明需要从验证生成器中抽取多少个批次用于评估。
history = model.fit_generator(
train_generator,
steps_per_epoch=100,
epochs=30,
validation_data=validation_generator,
validation_steps=50)
取部分训练轮数:
始终在训练完成后保存模型,这是一种良好的做法。
model.save('cats_and_dogs_small_1.h5')
让我们在训练期间在训练和验证数据上绘制模型的损失和精度:
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(len(acc))
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()
这些图是过度拟合的特征。我们的训练准确性会随着时间线性增加,直到达到近100%,而我们的验证准确性却停留在70~72%之间。我们的验证损失仅在五轮之后停滞,然后达到最小值,而训练损失则一直呈线性下降,直到接近0。
因为我们只有相对较少的训练样本(2000个),所以过度拟合将成为我们的头等大事。您已经知道许多有助于减轻过度拟合的技术,例如,辍学和体重减轻(L2正则化)。现在,我们将介绍一种专门针对计算机视觉的新工具,它在使用深度学习模型处理图像时几乎被普遍使用:数据增强(data augmentation)。
过度拟合是由于要学习的样本太少而导致的,这使我们无法训练能够泛化到新数据的模型。在给定无限数据的情况下,那么模型能够观察到数据分布的所有内容:这样永远不会过拟合。数据增强采用了通过现有的训练样本生成更多训练数据的方法,方法是通过许多随机变换来“增加”样本,以产生看起来可信的图像。目的是模型在训练时不会两次查看完全相同的图像。这让模型能够观察到数据的更多内容,从而具有更好的泛化能力。
在Keras中,这可以通过配置要对ImageDataGenerator实例读取的图像执行的许多随机转换来完成。让我们开始一个例子:
datagen = ImageDataGenerator(
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,
fill_mode='nearest')
这里只选择了几个参数(想了解更多参数,请查阅 Keras 文档)。我们来快速介绍一下这些参数的含义
# This is module with image preprocessing utilities
from keras.preprocessing import image
fnames = [os.path.join(train_cats_dir, fname) for fname in os.listdir(train_cats_dir)]
# We pick one image to "augment"
img_path = fnames[3]
# Read the image and resize it
img = image.load_img(img_path, target_size=(150, 150))
# Convert it to a Numpy array with shape (150, 150, 3)
x = image.img_to_array(img)
# Reshape it to (1, 150, 150, 3)
x = x.reshape((1,) + x.shape)
# The .flow() command below generates batches of randomly transformed images.
# It will loop indefinitely, so we need to `break` the loop at some point!
i = 0
for batch in datagen.flow(x, batch_size=1):
plt.figure(i)
imgplot = plt.imshow(image.array_to_img(batch[0]))
i += 1
if i % 4 == 0:
break
plt.show()
如果我们使用此数据扩充配置训练新网络,则我们的网络将永远不会看到两次相同的输入。但是,它看到的输入仍然是高度相互关联的,因为它们来自少量的原始图像-我们无法产生新信息,我们只能重新混合现有信息。因此,这可能不足以完全摆脱过度拟合的问题。为了进一步解决过度拟合问题,我们还将在紧密连接的分类器之前为模型添加一个Dropout层:
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu',
input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
让我们使用数据增强和dropout来训练我们的网络:
train_datagen = ImageDataGenerator(
rescale=1./255,
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,)
# Note that the validation data should not be augmented!
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
# This is the target directory
train_dir,
# All images will be resized to 150x150
target_size=(150, 150),
batch_size=32,
# Since we use binary_crossentropy loss, we need binary labels
class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=32,
class_mode='binary')
history = model.fit_generator(
train_generator,
steps_per_epoch=100,
epochs=100,
validation_data=validation_generator,
validation_steps=50)
部分训练结果:
我们把模型保存下来,你会在卷积可视化这节用到它。
model.save('cats_and_dogs_small_2.h5')
再次画下我们的结果:
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(len(acc))
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()
使用了数据增强和 dropout 之后,模型不再过拟合:训练曲线紧紧跟随着验证曲线。现在的精度为 82%,比未正则化的模型提高了 15%(相对比例)。
通过进一步利用正则化技术并调整网络参数(例如每个卷积层的过滤器数量或网络中的层数),我们可能能够获得更高的精度,可能高达86~87 %。但是,仅通过从头训练我们自己的卷积神经网络是很难的,要想进一步提高,仅仅是因为我们需要处理的数据很少。作为提高我们在此问题上的准确性的下一步,我们将必须利用预先训练的模型,这将是接下来的重点。
神经网络在实战中遇到的最大的问题就是数据量不够大,数据量不足会导致最后的效果不佳。
有很多机构,构造了自己的网络后,将ImageNet上海量的图片输入到网络中训练,最后得到了识别率很高的网络,入股他们愿意把劳动成果分享出来,我们就可以直接借用。
后面我们将使用一个大型卷积网络,它经过了大量数据的严格训练,这些图片数据来源于ImageNet,该网站包含140万张图片资源,这些图片大多涉及我们日常生活的物品以及常见动物,显然很多不同种类的猫和狗必然包含在内。我们将使用一个训练好的神经网络叫VGG16
在我们构造卷积网络时,一开始先是好几层卷积层和Max Pooling层,然后会调用Flatten()把他们输出的多维向量压扁后,传入到普通层:
from keras import layers
from keras import models
from keras import optimizers
model = models.Sequential()
#输入图片大小是150*150 3表示图片像素用(R,G,B)表示
model.add(layers.Conv2D(32, (3,3), activation='relu', input_shape=(150 , 150, 3)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3,3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3,3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3,3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Flatten())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
model.summary()
我们现在要借用的的VGG16网络,其结构与上面差不多,只不过它的Conv2D和MaxPooling层要比我们上面做的多得多而已。在我们借用别人训练好的网络时,往往要去掉Flatten()后面的网络层,因为那些网络层与别人构造网络时的具体应用场景相关,他们的应用场景与我们肯定不同,我们要借用的是Flatten上面那些由卷积层和Max Pooling层输出的结果,这些结果蕴含着对训练图片本质的认知,这才是我们想要的,去掉Flatten后面的神经层,换上我们自己的神经层,这个行为就叫特征抽取,具体流程如下图:
VGG16网络早已包含在keras框架中,我们可以方便的直接引用:
from keras.applications import VGG16
conv_base = VGG16(weights = 'vgg16_weights_tf_dim_ordering_tf_kernels_notop.h5', include_top = False, input_shape=(150, 150, 3))
conv_base.summary()
weight参数告诉程序将网络的卷积层和max pooling层对应的参数传递过来,并将它们初始化成对应的网络层次。
include_top表示是否也要把Flatten()后面的网络层也下载过来,VGG16对应的这层网络用来将图片划分到1000个不同类别中,由于我们只用来区分猫狗两个类别,因此我们去掉它这一层。
input_shape告诉网络,我们输入图片的大小是150*150像素,每个像素由[R, G, B]三个值表示。
注意:上面这段代码第一次运行会到giuhub上下载一个文件,如果网速不太好可以复制它给的网站到手机上下载,实在不行的话试试科学上网。
从上面输出结果看出,VGG16的网络结构与我们前面做的网络差不多,只不过它的层次要比我们多不少。最后的(None, 4, 4, 512)表示它将输出44的矩阵,而这些矩阵有512层,或者你也可以看成它将输出一个44的矩阵,而矩阵每个元素是包含512个值的向量。
将我们自己的图片读进来,将图片输入上面网络,让它把图片的隐含信息给抽取出来:
import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator
base_dir = 'D:/Workspaces/Jupyter-notebook/datasets/mldata/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):
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:
#把图片输入VGG16卷积层,让它把图片信息抽取出来
features_batch = conv_base.predict(inputs_batch)
#feature_batch 是 4*4*512结构
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 :
#for in 在generator上的循环是无止境的,因此我们必须主动break掉
break
return features , labels
#extract_features 返回数据格式为(samples, 4, 4, 512)
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)
上面代码利用VGG16的卷积层把图片的特征抽取出来,接下来我们就可以吧抽取的特征输入到我们自己的神经层中进行分类,代码如下:
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))
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))
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))
部分结果展示:
由于我们不需要训练卷积层,因此上面代码运行会很快,我们把训练结果和校验结果画出来看看:
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 = 'Train_acc')
plt.plot(epochs, val_acc, 'b', label = 'Validation acc')
plt.title('Trainning 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()
从上面图可以看出,经过一百多万张图片训练的网络,其识别效果就要比我们用4000张图片训练的网络要好很多,网络对图片的校验正确率达到了99%以上,同时对训练数据和校验数据的损失估计完全是一模一样的。
上面的方法叫特征提取,还有一种方法叫参数调优。特征提取时,我们把图片输入VGG16的卷积层,让他直接帮我们把图片中的特征提取出来,我们并没有通过自己的图片去训练更改VGG16的卷积层。
参数调优的做法在于,我们会有限度的通过自己的数据去训练VGG16提供的卷积层,于是让其能从我们的图片中学习到相关信息。我们从VGG16模型中获取了它六层卷积层,我们在调优时,让这六层卷积层中的最高2层也去学习我们的图片,于是最高两层的链路权重参数会根据我们的图片性质而更改,基本情况如下:
上图就是我们从VGG16拿到的卷积层,我们用自己的图片去训练修改它最高的两层,其他层次不做修改,这种只影响模型一部分的方法就叫参数调优。调优必须只对VGG16的卷积层做小范围修改,因为它的模型是经过大数据,反复训练得到的,如果我们对它进行大范围修改,就会破坏它原来训练的结果,这样人家辛苦做出来的工作成果就会被我们毁于一旦。
参数调优的步骤如下:
model = models.Sequential()
#将VGG16的卷积层直接添加到我们的网络
model.add(conv_base)
#添加我们自己的网络层
model.add(layers.Flatten())
model.add(layers.Dense(256, activation = 'relu'))
model.add(layers.Dense(1, activation = 'sigmoid'))
model.summary()
从上面输出结果看,VGG16的卷积层已经有一千多万个参数了!用个人电脑单个CPU是不可能对这个模型进行训练的!但我们可以训练它的其中一部分,我们把它最高三层与我们自己的网络层结合在一起训练,同时冻结最低四层。下面的代码将会把卷积层进行部分冻结:
conv_base.trainable = True
set_trainable = False
#一旦读取到'block5_conv1'时,意味着来到卷积网络的最高三层
#可以使用conv_base.summary()来查看卷积层的信息
for layer in conv_base.layers:
if layer.name == 'block5_conv1':
set_trainable = True
if set_trainable:
#当trainable == True 意味着该网络层可以更改,要不然该网络层会被冻结,不能修改
layer.trainable = True
else:
layer.trainable = False
然后我们把数据传入网络,训练给定的卷积层和我们自己的网络层:
#把图片数据读取进来
test_datagen = ImageDataGenerator(rescale = 1. / 255)
train_generator = test_datagen.flow_from_directory(train_dir, target_size = (150, 150), batch_size = 20,
class_mode = 'binary')
validation_generator = test_datagen.flow_from_directory(validation_dir, target_size = (150,150),
batch_size = 20,
class_mode = 'binary')
model.compile(loss = 'binary_crossentropy', optimizer = optimizers.RMSprop(2e-5),
metrics = ['acc'])
history = model.fit_generator(train_generator, steps_per_epoch = 100, epochs = 30,
validation_data = validation_generator,
validation_steps = 50)