十二、计算机视觉中的深度学习:从零开始训练卷积网络


文章代码来源:《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下载到。
下载后的数据如下所示:

Samples from the cats vs. dogs dataset. Sizes were not modified: the samples are heterogenous in size, appearance, etc.

不出意料的,在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中构建了一个小的卷积神经网络,所以你应该对这个很熟。我们将会重复使用相同的生成框架:我们的卷积网络就是一些卷积层和最大池化层的堆叠。
然而,由于我们在解决大点的图像和更加复杂的问题,我们要让我们的网络相应的也更大:将会有更多的卷积层和最大池化层的组合。这将扩大网络的容量,并减少特征映射的大小,使得他们在拉伸层不会过大。这里,由于我们输入的大小从150\times 150开始(随便选的一个),我们最终得到了7\times 7的特征映射。
注意特征映射的深度从32提升到了128,同时特征映射的大小在下降(从148\times 1487\times 7)
由于我们在攻击一个二分类问题,我们的网络最终只需要一个单元。

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()
Training and validation accuracy (training values as dots, validation values as solid lines)

Training and validation loss (training values as dots, validation values as solid lines)

这组图表明过拟合了,我们的训练准确率随时间线性增加,直到最后接近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()
Generation of cat pictures via random data augmentation

虽然我们可以保证训练过程中,模型不会看到相同的两张图,但是毕竟我们只是对原图混合了一下,并没有增加什么新的信息,所以无法完全避免过拟合,为了进一步抗击过拟合,我们加入了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')

让我们再画出训练和验证的结果看看:


Training and validation accuracy (training values as dots, validation values as solid lines)

Training and validation loss (training values as dots, validation values as solid lines)

多亏了数据增强和dropout,我们不再过拟合了:训练曲线和验证曲线十分相近。我们现在能够达到82%的准确率,比未正则化的模型要提高了15%。
通过利用正则化方法,或者更进一步:调参数,我们能达到更好的准确率,近乎86-87%。然而,这证明从零开始训练我们的卷积网络已经难以更好了,因为我们只有很少的数据来处理。下一步我们提高准确率的方法是利用预训练的网络,这将在接下来两部分进行讲解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容