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


文章代码来源:《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%。然而,这证明从零开始训练我们的卷积网络已经难以更好了,因为我们只有很少的数据来处理。下一步我们提高准确率的方法是利用预训练的网络,这将在接下来两部分进行讲解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容