文章代码来源:《deep learning on keras》,非常好的一本书,大家如果英语好,推荐直接阅读该书,如果时间不够,可以看看此系列文章,文章为我自己翻译的内容加上自己的一些思考,水平有限,多有不足,请多指正,翻译版权所有,若有转载,请先联系本人。
个人方向为数值计算,日后会向深度学习和计算问题的融合方面靠近,若有相近专业人士,欢迎联系。
系列文章:
一、搭建属于你的第一个神经网络
二、训练完的网络去哪里找
三、【keras实战】波士顿房价预测
四、keras的function API
五、keras callbacks使用
六、机器学习基础Ⅰ:机器学习的四个标签
七、机器学习基础Ⅱ:评估机器学习模型
八、机器学习基础Ⅲ:数据预处理、特征工程和特征学习
九、机器学习基础Ⅳ:过拟合和欠拟合
十、机器学习基础Ⅴ:机器学习的一般流程十一、计算机视觉中的深度学习:卷积神经网络介绍
十二、计算机视觉中的深度学习:从零开始训练卷积网络
十三、计算机视觉中的深度学习:使用预训练网络
十四、计算机视觉中的神经网络:可视化卷积网络所学到的东西
用很少的数据来训练图像分类模型在实践中很常见,如果你是计算机视觉背景专业出身的。
有少量样本意味着有几百到几万张图片。作为实例教学,我们将会集中注意力于猫狗分类,数据集中含有2000张猫,2000张狗。我们将会使用2000张用来训练,1000张用来验证,最后1000张用来测试。
在这部分,我们将回顾一个基本的解决这个问题的方法:从零训练一个新的模型。我们从直接在我们的2000个训练样本上训练一个小卷积网络开始,没有任何正则化,建立一个能达到的最低水平。我们的分类准确率达到了71%。这那一点,我们的主要问题在过拟合上。我们将会介绍数据增加,一种在计算机视觉中有效预防过拟合的方法。通过利用数据增加,我们把我们的网络的准确率提升到了82%。
在接下来的部分,我们将会回顾两个重要的应用在深度学习小样本的方法:在预训练的网络上做特征提取(这将帮助我们达到90%到96%的准确率)以及调好参数的预训练网络(可以将准确率提升到97%)。一起来说,这三个方法——从零训练小模型,使用预训练模型来做特征提取,调节预处理模型的参数——将会组成你以后解决计算机视觉问题中的小数据集时的工具包。
小数据问题的深度学习关联
你有的时候会听到深度学习只有当有很多数据的时候才起作用。这在一定程度上是一个有效的点:一个深度学习的基本特征是它能找到训练数据本身的有意思的特征,不需要任何人工特征工程,这也只能在有很多训练样本的时候是可行的。这对于输入样本有比较高的维数时尤为正确,比如说图像。
然而,构成很多样本的都是相关的。不可能通过十来个样本就训练一个网络去解决复杂的问题,但是对于比较小的,正则化好的模型,数百个样本也足够了。因为卷积网络学习局部,具有平移不变性的特征,具有很高的数据效率。在一个很小的图像数据集上从零开始训练一个卷积网络,仍将产生合理的结果,尽管缺少数据,无需任何自定义的特征工程。你将在这一部分看到。
但是,深度学习模型是自然能高度重新设计的:你能将一个模型用到不同的数据集上,只需要一丁点的改动即可。特别的,很多训练好的模型都能下载了,能够用来引导小数据的情况。
下载数据
The cats vs. dogs dataset在keras里面没有,但是在Kaggle里面的2013下载到。
下载后的数据如下所示:
不出意料的,在2013的Kaggle竞赛中,使用convnets的赢得了比赛。达到了95%的准确率,接下来我们得到的会很接近这个准确率,我们实际使用的样本还不足原本竞赛给出数据的10%,竞赛包含25000个猫狗图,大小为543MB(压缩后)。在下载和解压后,我们将会生成一个新的数据集,包含三个子集:一个猫狗各有1000个样本的训练集,各有500个样本的验证集,和各有500个样本的测试集。
接下来就是几行做这个的代码:
import os, shutil
# The path to the directory where the original
# dataset was uncompressed
original_dataset_dir = '/Users/fchollet/Downloads/kaggle_original_data'
# The directory where we will
# store our smaller dataset
base_dir = '/Users/fchollet/Downloads/cats_and_dogs_small'
os.mkdir(base_dir)
# Directories for our training,
# validation and test splits
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)
# Directory with our training cat pictures
train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir)
# Directory with our training dog pictures
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)
# Directory with our validation cat pictures
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
# Directory with our validation dog pictures
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)
# Directory with our validation cat pictures
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)
# Directory with our validation dog pictures
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_dir)
# Copy first 1000 cat images to 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)
# Copy next 500 cat images to 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)
# Copy next 500 cat images to 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)
# Copy first 1000 dog images to 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)
# Copy next 500 dog images to 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)
# Copy next 500 dog images to 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)
首先给出原始数据和新数据集的存放位置。
再在新数据集目录下分别写训练集、验证集、测试集的位置。
再分别在三个集合下面建立猫和狗的文件夹。
最后,把原数据集里面的前1000个数据放进新训练集,接下来的500个放进验证集,再接下来五百个放进测试集。
最后使用程序数一数我们放对了吗?
>>> print('total training cat images:', len(os.listdir(train_cats_dir)))
total training cat images: 1000
>>> print('total training dog images:', len(os.listdir(train_dogs_dir)))
total training dog images: 1000
>>> print('total validation cat images:', len(os.listdir(validation_cats_dir)))
total validation cat images: 500
>>> print('total validation dog images:', len(os.listdir(validation_dogs_dir)))
total validation dog images: 500
>>> print('total test cat images:', len(os.listdir(test_cats_dir)))
total test cat images: 500
>>> print('total test dog images:', len(os.listdir(test_dogs_dir)))
total test dog images: 500
这样一来,我们就得到了所需小数据集。
构建我们的神经网络
我们已经在MNIST中构建了一个小的卷积神经网络,所以你应该对这个很熟。我们将会重复使用相同的生成框架:我们的卷积网络就是一些卷积层和最大池化层的堆叠。
然而,由于我们在解决大点的图像和更加复杂的问题,我们要让我们的网络相应的也更大:将会有更多的卷积层和最大池化层的组合。这将扩大网络的容量,并减少特征映射的大小,使得他们在拉伸层不会过大。这里,由于我们输入的大小从开始(随便选的一个),我们最终得到了的特征映射。
注意特征映射的深度从32提升到了128,同时特征映射的大小在下降(从到)
由于我们在攻击一个二分类问题,我们的网络最终只需要一个单元。
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_1 (Conv2D) (None, 148, 148, 32) 896
________________________________________________________________
maxpooling2d_1 (MaxPooling2D) (None, 74, 74, 32) 0
________________________________________________________________
conv2d_2 (Conv2D) (None, 72, 72, 64) 18496
________________________________________________________________
maxpooling2d_2 (MaxPooling2D) (None, 36, 36, 64) 0
________________________________________________________________
conv2d_3 (Conv2D) (None, 34, 34, 128) 73856
________________________________________________________________
maxpooling2d_3 (MaxPooling2D) (None, 17, 17, 128) 0
________________________________________________________________
conv2d_4 (Conv2D) (None, 15, 15, 128) 147584
________________________________________________________________
maxpooling2d_4 (MaxPooling2D) (None, 7, 7, 128) 0
________________________________________________________________
flatten_1 (Flatten) (None, 6272) 0
________________________________________________________________
dense_1 (Dense) (None, 512) 3211776
________________________________________________________________
dense_2 (Dense) (None, 1) 513
================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
在最后compilation的步骤,我们会像往常一样,使用RMSprop优化器,因为我们使用一个sigmoid单元在我们的模型最后,我们将使用二进交叉熵作为损失函数,记住,你要是不知道怎么选这些东西了,可以翻一翻之前列的表。
from keras import optimizers
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
数据预处理
你现在已经知道,数据在喂进网络之前需要预处理成浮点数张量。目前我们的数据来自于JPEG文件,所以其处理步骤大致为:
- 读图片文件
- 将JPEG解码为RBG
- 将它们转化为浮点张量
- 将像素点的值归一化
这看起来有点冗杂,但所幸,keras能够自动做完上述步骤。keras有一个图像处理帮助工具,位于keras.preprocessing.image。特别的,其包括ImageDataGenerator类,能够快速设置Pyhon的生成器,从而快速将磁盘上图片文件加入预处理张量批次。这就是我们将要使用的。
注意:理解Python中的生成器(generators)
Python的生成器是一个对象,像一个迭代器一样工作,即一个对象可以使用for/in操作符。生成器使用yield操作符来建成。
这里有一个用生成器生成整数的例子:
def generator():
i = 0
while True:
i += 1
yield i
for item in generator():
print(item)
if item > 4:
break
1
2
3
4
5
使用图像数据生成器来从目录中读取图片
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')
讲解一下代码:
首先定义train_datagen这个生成器,重置像素点大小的命令写在括号中,然后validation_generator实际得到的是一个张量,张量形状为(20,150,150,3),而生成这个所用的就是生成器的flow_from_directory属性,第一个参数填文件目录,第二个参数填将图片重置的大小,第三个参数填每一批次取得个数,最后一个参数填标签类别。
展示数据和标签
>>> for data_batch, labels_batch in train_generator:
>>> print('data batch shape:', data_batch.shape)
>>> print('labels batch shape:', labels_batch.shape)
>>> break
data batch shape: (20, 150, 150, 3)
labels batch shape: (20,)
让我们开始用生成器来拟合我们的模型。我们使用fit_generator方法来进行,这个我们的fit是等价的。先放代码:
history = model.fit_generator(
train_generator,
steps_per_epoch=100,
epochs=30,
validation_data=validation_generator,
validation_steps=50)
第一个参数是我们生成器,第二个参数是每一批需要进行的步数,由于我们生成器每次生成20个数据,所以需要100步才能遍历完2000个数据,验证集的类似知道为什么是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(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()
这组图表明过拟合了,我们的训练准确率随时间线性增加,直到最后接近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')
这里只列出了一小部分选项,想要了解更多,请看keras文档。
让我们很快看一遍我们写了什么:
- rotation_range是一个角度值(0-180),是随机转动图片的角度范围。
- width_shift和height_shift是随机改变图片对应维度的比例。
- shear_range是随机剪切的比例
- zoom_range是在图片内随机缩放的比例
- horizontal_flip是随机将图片水平翻转,当没有水平对称假设时。
- fill_mode在新出来像素以后,我们选择填充的策略。
让我们看一看图像增加:
# 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')
让我们再画出训练和验证的结果看看:
多亏了数据增强和dropout,我们不再过拟合了:训练曲线和验证曲线十分相近。我们现在能够达到82%的准确率,比未正则化的模型要提高了15%。
通过利用正则化方法,或者更进一步:调参数,我们能达到更好的准确率,近乎86-87%。然而,这证明从零开始训练我们的卷积网络已经难以更好了,因为我们只有很少的数据来处理。下一步我们提高准确率的方法是利用预训练的网络,这将在接下来两部分进行讲解。