使用很少的数据来训练一个和图像分类模型,这是很常见的情况。”很少的“样本可能是几百张图像,也可能是几万张图像。
看一个实例,讨论猫狗图像分类,数据几种包括4000张猫和狗的图像(2000张猫的,2000张狗的)。将两千张用于训练,1000张用于验证,1000张用于测试。
这一问题的基本策略,即使用已有的少量数据从头开始训练一个新模型。首先,在2000个训练样本上训练一个简单的小型卷积神经网络,不做任何正则化,为模型目标定一个基准。这会得到71%的分类精度。此时主要的问题在于过拟合。然后,我们会介绍数据增强,它在计算机视觉领域是一种非常强大的降低过拟合的技术。使用数据增强之后,网络精度会提高到82%。
再之后还有另外两个技巧:用预训练的网络做特征提取(得到的精度范围会在90%~96%),对预训练的网络进行微调(最终精度为97%)。
总而言之,三种策略——从头开始训练一个小型模型、使用预训练的的网络做特征提取、对训练的网络进行微调——可用于解决小型数据集的图像分类问题。
**仅在有大量数据可用时,深度学习才有效。**这种说法部分正确:深度学习的基本特性就是能够独立地在训练数据中找到有趣的特征,无需人为地特征工程,而这只在拥有大量训练样本时才能实现。对于输入样本的维度非常高(比如图像)的问题尤其如此。
但是所谓”大量“样本是相对的,即相对于你所要训练网络的大小和深度而言。由于卷积神经网络学到的是局部的、平移不变的特征,它对于感知问题可以高效的利用数据。虽然数据相对较少,但在非常小的图像数据集上从头开始训练一个卷积神经网络,仍然可以得到不错的结果,而且无需任何自定义的特征工程。
此外,深度学习模型本质上具有高度的可复用性,比如,已有一个大规模数据集上训练的图像分类模型或语音转文本模型,你只需要做很小的修改就能将其服用于完全不同的问题。
从https://www.kaggle.com/c/dogs-vs-cats/data中下载即可,这些图像都是中等分辨率的彩色JPEG图像。
import os
import shutil
# 原始数据集解压目录的路径
original_dataset_dir = 'D:\\dogs-vs-cats\\dogs-vs-cats\\train'
# 保存较小的数据集的目录
base_dir = 'D:\dogs-vs-cats\dogs-vs-cats\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)import os
import shutil
# 原始数据集解压目录的路径
original_dataset_dir = 'D:\dogs-vs-cats\dogs-vs-cats\train'
# 保存较小的数据集的目录
base_dir = 'D:\dogs-vs-cats\dogs-vs-cats\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_dog_dir)
# 猫的验证图像目录
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
# 狗的验证图像目录
validation_dogs_dir = os.path.join(validtion_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(orignal_dataset_dir, fname)
dst = os.path.join(validation_cats_dir, fanme)
shutil.copyfile(src, dst)
# 将接下来的500张猫图像复制到test_cats_dir
fnames = ['cat.{}.jpg'.fomat(i) for i in range(1500,2000)]
for fname in fnames:
src = os.path.join(orignal_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(orignal_dataset_dir, fname)
dst = os.path.join(validation_dogs_dir, fanme)
shutil.copyfile(src, dst)
# 将接下来的500张猫图像复制到test_dogs_dir
fnames = ['dog.{}.jpg'.fomat(i) for i in range(1500,2000)]
for fname in fnames:
src = os.path.join(orignal_dataset_dir, fname)
dst = os.path.join(test_dogs_dir, fname)
shutil.copyfile(src, dst)
现在以及有了数据。每个分组中两个类别的样本数相同,这是一个平衡的二分类问题,分类精度可作为衡量成功的标准。
相对于MNIST示例中,要处理更大的图像和更复杂的问题,需要相应的增大网络,即再增加一个Conv2D+MaxPooling2D的组合。这既可以增大网络容量,也可以进一步减小特征图的尺寸,使其在连接Flatten层时尺寸不会太大。本例中初始输入的尺寸为150×150,所以最后在Flatten层之前的特征图大小为7×7。
注意:网络中特征图的深度在逐渐增大(从32增大到128),而特征图的尺寸在逐渐减小(从150×150减小到7×7)。这几乎是所有卷积神经网络的模式。
该问题为二分类问题,所以网络的最后一层应该是使用 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()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_5 (Conv2D) (None, 148, 148, 32) 896
max_pooling2d_4 (MaxPooling (None, 74, 74, 32) 0
2D)
conv2d_6 (Conv2D) (None, 72, 72, 64) 18496
max_pooling2d_5 (MaxPooling (None, 36, 36, 64) 0
2D)
conv2d_7 (Conv2D) (None, 34, 34, 128) 73856
max_pooling2d_6 (MaxPooling (None, 17, 17, 128) 0
2D)
conv2d_8 (Conv2D) (None, 15, 15, 128) 147584
max_pooling2d_7 (MaxPooling (None, 7, 7, 128) 0
2D)
flatten_1 (Flatten) (None, 6272) 0
dense_2 (Dense) (None, 512) 3211776
dense_3 (Dense) (None, 1) 513
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
在编译这一步,和前面一样,我们将使用 RMSprop 优化器。因为网络最后一层是单一 sigmoid 单元,所以我们将使用二元交叉熵作为损失函数。
from keras import optimizers
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
在数据输入神经网络之前,应该将数据格式化为经过预处理的负电性张量。现在的数据是以JPEG文件的形式保存在硬盘之中,所以数据预处理的步骤如下:
可以使用 Keras 的图像处理辅助工具的模块,keras.preprocessing.image
。它包含ImageDataGenerator
类,可以快速创建Python生成器,能够将硬盘上的图像文件自动转换为预处理好的张量批量。
from keras.preprocessing.image import ImageDataGenerator
# 将所有图像乘以 1/255 缩放
train_datagen = ImageDataGenderator(rescale=1./255)
test_datagen = ImageDataGenderator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
train_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')# 因为使用了binary_crossentropy损失,所以需要用二进制标签
validation_generator = test_datagend.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
我们来看一下生成器的输出:它生成了150×150的RGB图像【形状为(20,150,150,3)】与二进制标签【形状为(20,)】组成的批量。每个批量中包含20个样本(批量大小batch_size)。
for data_batch, labels_batch in train_generator:
print('data batch shape:', data_batch.shape)
print('labels batch shape:', labels_batch.shape)
break# 注意,胜澈国企会不停的生成这些批量,他会不断循环目标文件夹中的图像。因此需要在某个时刻终止(break)迭代循环。
data batch shape: (20, 150, 150, 3)
label batch shape: (20,)
利用生成器,可以让模型对数据进行拟合。我们将使用 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_smail_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(1, len(acc) + 1)
figure, axis = plt.subplots(nrows=1, ncols=2, figsize=(18, 6))
axis[0].plot(epochs, acc, 'bo', label='Training acc')
axis[0].plot(epochs, val_acc, 'b', label='Validation acc')
axis[0].set_title('Training and Validation accuracy')
axis[0].legend()
axis[1].plot(epochs, loss, 'bo', label='Training loss')
axis[1].plot(epochs, val_loss, 'b', label='Validation loss')
axis[1].set_title('Training and Validation loss')
axis[1].legend()
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title
从图像中可以看出过拟合的特征。训练精度随着时间线性增加,直到接近100%。而验证精度则停留在70%~72%。验证损失仅在5论之后就达到了最小值,然后保持不变,而训练损失则一致线性下降,直到接近于0。
因为训练样本相对较小(2000个),所以过拟合是最重要的问题,减少过拟合的方法有dropout和权重衰减(L2正则化)。现在要使用的就是数据增强。
过拟合的原因是学习样本太少,导致无法训练出能够泛化到新数据的模型。如果拥有无限的数据,那么模型能够观察到数据分布的所有内容,这样就永远不会过拟合。**数据增强是从现有的训练样本中生成更多的训练数据,其方法是利用多种能够生成可信图像的随机变化来增加样本。**其目标是,模型在训练时不会两次查看完全相同的图像。这让模型能够观察到数据的更多内容,从而有更好地泛化能力。
在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'
)
以上参数的含义:
rotation_range
是角度值(在0~180范围内),表示图像随机旋转的角度范围。width_shift
和height_shift
是图像在水平或垂直向上方向上平移的角度范围(相对于总宽度或总高度的比例)。shear_range
是随机错切变换的角度。zoom_range
是图像随机缩放的范围。horizontal_flip
是随机将一半图像水平翻转。如果没有水平不对称的假设(比如真实世界的图像),这种做法是有意义的。fill_mode
适用于填充新创建像素的方法,这些新像素可能来自于旋转或宽度/高度平移。from keras.preprocessing import image
fnames = [os.path.join(train_cats_dir, fname) for fname in os.listdir(train_cats_dir)
img_path = fnames[3]
img = image.load_img(img_path, target_size=(150, 150))
x = image.img_to_array(img)
x = x.reshape((1,) + x.shape)
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()
# 错误名:module 'keras.preprocessing.image' has no attribute 'load_img'
# 如果你报了以上的错误,可能是版本问题,请使用以下方法
import keras
keras.utils.load_img(img_path)
如果你使用这种数据增强来训练一个新网络,那么网络将不会两次看到同样的输入。但网络看到的输入仍然是高度相关的,因为这些输入都来自于少量的原始图像。你无法生成新信息,而只能混合现有信息。因此,这种方法可能不足以完全消除过拟合。为了进一步降低过拟合,你还需要向模型中添加一个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,)
# 注意,不能增强验证数据
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
train_dir,
target_size=(150, 150),
batch_size=32,
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=63,
epochs=100,
validation_data=validation_generator,
validation_steps=32)
再次绘制图像如下