您的位置:首页 > 其它

面向小数据集构建图像分类模型

2017-12-16 12:33 525 查看
  本文为keras中文文档中《面向小数据集构建图像分类模型》的学习笔记,因此在全文对文档中出现的内容多次引用。原始博客为《Building powerful image classification models using very little data》,感谢原作者及翻译者的辛苦付出。

  欢迎大家在评论区多多留言互动~~~~

1. 概述

  在本文中,我们将提供一些面向小数据集(几百张到几千张图片)构造高效、实用的图像分类器的方法。在这里主要是对猫与狗的图像进行分类。

  本文将探讨如下几种方法:

  (1)从图片中直接训练一个小网络(作为基准方法)

  (2)利用预训练网络的bottleneck(瓶颈)特征(即层迁移)

  (3)fine-tune预训练网络的高层

  本文需要使用的Keras模块有:

  (1)fit_generator :用于从Python生成器中训练网络

  (2)ImageDataGenerator :用于实时数据提升

  (3)层参数冻结和模型fine-tune

  数据集的存放方式如图1 所示,实际上可以是对各种图像进行分类可以不仅仅是二分类,也可使多分类,只需要按照下面的形式多添加几个文件夹,设计的时候网络输出是多分类器就可以了。



2. 针对小数据集的深度学习

  本段主要介绍为什么小数据集同样可以用于深度学习。我经常听到的一种说法是,深度学习只有在你拥有海量数据时才有意义。因为当输入的数据维度很高(如图片)时,深度学习强调从数据中自动学习特征的能力,没有足够的训练样本,这几乎是不可能的。但是这种说法并不是完全不对,并且具有较强的误导性,原因在于一下两点。卷积神经网络作为深度学习的支柱,被设计为针对“感知”问题最好的模型之一(如图像分类问题),即使只有很少的数据,网络也能把特征学的不错。针对小数据集的神经网络依然能够得到合理的结果,并不需要任何手工的特征工程。另一方面,深度学习模型天然就具有可重用的特性:比方说,你可以把一个在大规模数据上训练好的图像分类或语音识别的模型重用在另一个很不一样的问题上,而只需要做有限的一点改动。尤其在计算机视觉领域,许多预训练的模型现在都被公开下载,并被重用在其他问题上以提升在小数据集上的性能。

  也就是说一方面可以通过层数较少的卷积层进行学习,另一方面可以通过各类公开好的模型进行迁移学习。所以在本文中所涉及的三种方法中,第一种实际上不算一个深度网络,或者即使算深度网络,他的深度也不够深。剩下两种实际上是迁移学习的两种方法。

3. 数据预处理与数据提升

  为了尽量利用我们有限的训练数据,我们将通过一系列随机变换堆数据进行提升,这样我们的模型将看不到任何两张完全相同的图片,这有利于我们抑制过拟合,使得模型的泛化能力更好。

  在Keras中,这个步骤可以通过
keras.preprocessing.image.ImageGenerator
来实现,这个类使你可以

在训练过程中,设置要对图像进行的随机变换

通过
.flow
.flow_from_directory(directory)
方法实例化一个针对图像batch的生成器,这些生成器可以被用作 keras 模型相关方法的输入,如
fit_generator
evaluate_generator
predict_generator


  具体的使用方法可以参见 keras 中的《图片预处理》部分。

  下面我们使用这个工具来生成图片,并将它们保存在一个临时文件夹中,这样我们可以感觉一下数据提升究竟做了什么事。为了使图片能够展示出来,这里没有使用
rescaling


from keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_img

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')

img = load_img('data/train/cats/cat.0.jpg') # 导入一个 PIL 图像
x = img_to_array(img) # 将图像转化为 Numpy 矩阵的形式 (3, 150, 150)
x = x.reshape((1,) + x.shape) # 将图像转化为另一种 Numpy 矩阵的形式 (1, 3, 150, 150)

# 下面的 the .flow() 命令随机产生变化后的图片(以 batch 为单位) ,并且把结果保存到`preview/`文件夹中。

i = 0
for batch in datagen.flow(x, batch_size=1,save_to_dir='preview', save_prefix='cat', save_format='jpeg'):
i += 1
if i > 20:
break # 否则生成器将无限循环


  下面是一张图片被提升以后得到的多个结果:



4. 直接训练一个小网络

  进行图像分类的正确工具是卷积网络,所以我们来试试用卷积神经网络搭建一个初级的模型。因为我们的样本数很少,所以我们应该对过拟合的问题多加注意。

  数据提升是对抗过拟合问题的一个武器,但还不够,因为提升过的数据仍然是高度相关的。对抗过拟合的你应该主要关注的是模型的“熵容量”——模型允许存储的信息量。能够存储更多信息的模型能够利用更多的特征取得更好的性能,但也有存储不相关特征的风险。另一方面,只能存储少量信息的模型会将存储的特征主要集中在真正相关的特征上,并有更好的泛化性能。

  有很多不同的方法来调整模型的“熵容量”,常见的一种选择是调整模型的参数数目,即模型的层数和每层的规模。另一种方法是对权重进行正则化约束,如L1或L2.这种约束会使模型的权重偏向较小的值。

  在我们的模型里,我们使用了很小的卷积网络,只有很少的几层,每层的滤波器数目也不多。再加上数据提升和Dropout,就差不多了。Dropout通过防止一层看到两次完全一样的模式来防止过拟合,相当于也是一种数据提升的方法。(你可以说dropout和数据提升都在随机扰乱数据的相关性)

  完整代码如下

'''This script goes along the blog post
"Building powerful image classification models using very little data"
from blog.keras.io.
It uses data that can be downloaded at: https://www.kaggle.com/c/dogs-vs-cats/data In our setup, we:
- created a data/ folder
- created train/ and validation/ subfolders inside data/
- created cats/ and dogs/ subfolders inside train/ and validation/
- put the cat pictures index 0-999 in data/train/cats
- put the cat pictures index 1000-1400 in data/validation/cats
- put the dogs pictures index 12500-13499 in data/train/dogs
- put the dog pictures index 13500-13900 in data/validation/dogs
So that we have 1000 training examples for each class, and 400 validation examples for each class.
In summary, this is our directory structure:


data/

train/

dogs/

dog001.jpg

dog002.jpg



cats/

cat001.jpg

cat002.jpg



validation/

dogs/

dog001.jpg

dog002.jpg



cats/

cat001.jpg

c
1072e
at002.jpg



from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras import backend as K

# dimensions of our images.
img_width, img_height = 150, 150

train_data_dir = 'data/train'
validation_data_dir = 'data/validation'
nb_train_samples = 2000
nb_validation_samples = 800
epochs = 50
batch_size = 16

if K.image_data_format() == 'channels_first':
input_shape = (3, img_width, img_height)
else:
input_shape = (img_width, img_height, 3)

model = Sequential()
model.add(Conv2D(32, (3, 3), input_shape=input_shape))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(32, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(64, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Flatten())
model.add(Dense(64))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(1))
model.add(Activation('sigmoid'))

model.compile(loss='binary_crossentropy',
optimizer='rmsprop',
metrics=['accuracy'])

# this is the augmentation configuration we will use for training
train_datagen = ImageDataGenerator(
rescale=1. / 255,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True)

# this is the augmentation configuration we will use for testing:
# only rescaling
test_datagen = ImageDataGenerator(rescale=1. / 255)

train_generator = train_datagen.flow_from_directory(
train_data_dir,
target_size=(img_width, img_height),
batch_size=batch_size,
class_mode='binary')

validation_generator = test_datagen.flow_from_directory(
validation_data_dir,
target_size=(img_width, img_height),
batch_size=batch_size,
class_mode='binary')

model.fit_generator(
train_generator,
steps_per_epoch=nb_train_samples // batch_size,
epochs=epochs,
validation_data=validation_generator,
validation_steps=nb_validation_samples // batch_size)

model.save_weights('first_try.h5')


  这个模型在50个epoch后的准确率为79%~81%,别忘了我们只用了8%的数据,也没有花时间来做模型和超参数的优化。注意这个准确率的变化可能会比较大,因为准确率本来就是一个变化较高的评估参数,而且我们只有800个样本用来测试。比较好的验证方法是使用K折交叉验证,但每轮验证中我们都要训练一个模型。

5. 使用预训练网络的bottleneck特征

  一个稍微讲究一点的办法是,利用在大规模数据集上预训练好的网络进行层迁移学习。我们将使用vgg-16网络,该网络在ImageNet数据集上进行训练。因为ImageNet数据集包含多种“猫”类和多种“狗”类,这个模型已经能够学习与我们这个数据集相关的特征了。事实上,简单的记录原来网络的输出而不用bottleneck特征就已经足够把我们的问题解决的不错了。不过我们这里讲的方法对其他的类似问题有更好的推广性,包括在ImageNet中没有出现的类别的分类问题。

  在这里可以直接使用平静特征进行层的迁移学习,主要原因是因为 target domian 与 source domain 之间的数据相关性较大,甚至在这里是包含的关系(imagenet中的图像包含了我们用来训练的图像)。如果说现在的target domian 与 source domain 相关性较差,如 imagenet 与 医学切片图像,那么直接利用瓶颈特征进行迁移学习的话,效果不会很好,甚至可能出现负迁移

  VGG-16的网络结构如下:



  我们的方法是这样的,我们将利用网络的卷积层部分,把全连接以上的部分抛掉。然后在我们的训练集和测试集上跑一遍,将得到的输出(即“bottleneck feature”,网络在全连接之前的最后一层激活的feature map)记录在两个numpy array里。然后我们基于记录下来的特征训练一个全连接网络。

  我们将这些特征保存为离线形式,而不是将我们的全连接模型直接加到网络上并冻结之前的层参数进行训练的原因是处于计算效率的考虑。运行VGG网络的代价是非常高昂的,尤其是在CPU上运行,所以我们只想运行一次。这也是我们不进行数据提升的原因。

  全部代码如下

import numpy as np
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Dropout, Flatten, Dense
from keras import applications

# dimensions of our images.
img_width, img_height = 150, 150

top_model_weights_path = 'bottleneck_fc_model.h5'
train_data_dir = 'data/train'
validation_data_dir = 'data/validation'
nb_train_samples = 2000
nb_validation_samples = 800
epochs = 50
batch_size = 16

def save_bottlebeck_features():
datagen = ImageDataGenerator(rescale=1. / 255)

# build the VGG16 network
model = applications.VGG16(include_top=False, weights='imagenet')

generator = datagen.flow_from_directory(
train_data_dir,
target_size=(img_width, img_height),
batch_size=batch_size,
class_mode=None,
shuffle=False)
bottleneck_features_train = model.predict_generator(
generator, nb_train_samples // batch_size)
np.save(open('bottleneck_features_train.npy', 'w'),
bottleneck_features_train)

generator = datagen.flow_from_directory(
validation_data_dir,
target_size=(img_width, img_height),
batch_size=batch_size,
class_mode=None,
shuffle=False)
bottleneck_features_validation = model.predict_generator(
generator, nb_validation_samples // batch_size)
np.save(open('bottleneck_features_validation.npy', 'w'),
bottleneck_features_validation)

def train_top_model():
train_data = np.load(open('bottleneck_features_train.npy'))
train_labels = np.array(
[0] * (nb_train_samples / 2) + [1] * (nb_train_samples / 2))

validation_data = np.load(open('bottleneck_features_validation.npy'))
validation_labels = np.array(
[0] * (nb_validation_samples / 2) + [1] * (nb_validation_samples / 2))

model = Sequential()
model.add(Flatten(input_shape=train_data.shape[1:]))
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop',
loss='binary_crossentropy', metrics=['accuracy'])

model.fit(train_data, train_labels,
epochs=epochs,
batch_size=batch_size,
validation_data=(validation_data, validation_labels))
model.save_weights(top_model_weights_path)

save_bottlebeck_features()
train_top_model()


  因为特征的 size 很小,模型在 CPU 上跑的也会很快,大概 1s 一个 epoch,最后我们的准确率是90%~91%,这么好的结果多半归功于预训练的 vgg 网络帮助我们提取特征。

6. 在预训练的网络上

  为了进一步提高之前的结果,我们可以试着fine-tune网络的后面几层。Fine-tune以一个预训练好的网络为基础,在新的数据集上重新训练一小部分权重。在这个实验中,fine-tune分三个步骤

搭建vgg-16并载入权重

将之前定义的全连接网络加在模型的顶部,并载入权重(应该是通过层训练的得到的权重)

冻结vgg16网络的一部分参数

  如下图所示



其中需要注意的是

搭建vgg-16并载入权重为了进行fine-tune,所有的层都应该以训练好的权重为初始值,例如,你不能将随机初始的全连接放在预训练的卷积层之上,这是因为由随机权重产生的大梯度将会破坏卷积层预训练的权重。在我们的情形中,这就是为什么我们首先训练顶层分类器,然后再基于它进行fine-tune的原因

我们选择只fine-tune最后的卷积块,而不是整个网络,这是为了防止过拟合。整个网络具有巨大的熵容量,因此具有很高的过拟合倾向。由底层卷积模块学习到的特征更加一般,更加不具有抽象性,因此我们要保持前两个卷积块(学习一般特征)不动,只fine-tune后面的卷积块(学习特别的特征)

fine-tune应该在很低的学习率下进行,通常使用SGD优化而不是其他自适应学习率的优化算法,如RMSProp。这是为了保证更新的幅度保持在较低的程度,以免毁坏预训练的特征

  全部代码如下所示

from keras import applications
from keras.preprocessing.image import ImageDataGenerator
from keras import optimizers
from keras.models import Sequential
from keras.layers import Dropout, Flatten, Dense

# path to the model weights files.
weights_path = '../keras/examples/vgg16_weights.h5'
top_model_weights_path = 'fc_model.h5'
# dimensions of our images.
img_width, img_height = 150, 150

train_data_dir = 'cats_and_dogs_small/train'
validation_data_dir = 'cats_and_dogs_small/validation'
nb_train_samples = 2000
nb_validation_samples = 800
epochs = 50
batch_size = 16

# build the VGG16 network
model = applications.VGG16(weights='imagenet', include_top=False)
print('Model loaded.')

# build a classifier model to put on top of the convolutional model
top_model = Sequential()
top_model.add(Flatten(input_shape=model.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(1, activation='sigmoid'))

# note that it is necessary to start with a fully-trained
# classifier, including the top classifier,
# in order to successfully do fine-tuning
top_model.load_weights(top_model_weights_path)

# add the model on top of the convolutional base
model.add(top_model)

# set the first 25 layers (up to the last conv block)
# to non-trainable (weights will not be updated)
for layer in model.layers[:25]:
layer.trainable = False

# compile the model with a SGD/momentum optimizer
# and a very slow learning rate.
model.compile(loss='binary_crossentropy',
optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
metrics=['accuracy'])

# prepare data augmentation configuration
train_datagen = ImageDataGenerator(
rescale=1. / 255,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1. / 255)

train_generator = train_datagen.flow_from_directory(
train_data_dir,
target_size=(img_height, img_width),
batch_size=batch_size,
class_mode='binary')

validation_generator = test_datagen.flow_from_directory(
validation_data_dir,
target_size=(img_height, img_width),
batch_size=batch_size,
class_mode='binary')

# fine-tune the model
model.fit_generator(
train_generator,
samples_per_epoch=nb_train_samples,
epochs=epochs,
validation_data=validation_generator,
nb_val_samples=nb_validation_samples)


  在50个epoch之后该方法的准确率为94%,非常成功。通过下面的方法你可以达到95%以上的正确率:

更加强烈的数据提升

更加强烈的dropout

使用L1和L2正则项(也称为权重衰减)

fine-tune更多的卷积块(配合更大的正则)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息