Python深度学习之计算机视觉(上)

1,030 阅读17分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

Deep Learning with Python for computer vision

这篇文章是我学习《Deep Learning with Python》(第二版,François Chollet) 第5章 深度学习用于计算机视觉 (Chapter 5. Deep learning for computer vision) 的笔记。

文章的内容是从 Jupyter notebooks 转成 Markdown 的,你可以去我的 GitHub 或 Gitee 找到原始的 .ipynb 笔记本。

你可以到这个网站在线阅读这本书的正版原文(英文)。同时这本书的作者也给出了配套的 Jupyter notebooks。

卷积神经网络简介

5.1 Introduction to convnets

卷积神经网络处理计算机视觉问题很厉害啦。

首先看一个最简单的卷积神经网络处理 MNIST 完爆第二章里的全连接网络的例子:

from tensorflow.keras import layers
from tensorflow.keras import models

model = models.Sequential()

model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
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(64, (3, 3), activation='relu'))

model.add(layers.Flatten())
model.add(layers.Dense(64, activation="relu"))
model.add(layers.Dense(10, activation="softmax"))

这里我们用的 Conv2D 层要的 input_shape 是 (image_height, image_width, image_channels) 这种格式的。

Conv2D 和 MaxPooling2D 层的输出都是 3D 张量 (height, width, channels), height 和 width 会逐层减小,channels 是由 Conv2D 的第一个参数控制的。

最后的三层里,我们是把最后一个 Conv2D 层的 (3, 3, 64) 的张量用一系列全连接层变成想要的结果向量:Flatten 层是用来把我们的 3D 张量展平(flatten,其实我想写成“压”、“降”之类的,这才是flatten的本意,但标准的中文翻译是展平)到 1D 的。 然后后面的两个 Dense 层就行我们在第二章做的那种,最后得到一个 10 路的分类。

最后,看一下模型结构:

model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_3 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 3, 3, 64)          36928     
_________________________________________________________________
flatten_1 (Flatten)          (None, 576)               0         
_________________________________________________________________
dense_2 (Dense)              (None, 64)                36928     
_________________________________________________________________
dense_3 (Dense)              (None, 10)                650       
=================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0
_________________________________________________________________

好了,网络就建成这样了,还是很简单的,接下来就训练它了,大致和之前第二章里的是一样的(但注意reshape的形状不一样):

# Load the TensorBoard notebook extension
# TensorBoard 可以可视化训练过程
%load_ext tensorboard
# Clear any logs from previous runs
!rm -rf ./logs/ 
# 在 MNIST 图像上训练卷积神经网络

import datetime
import tensorflow as tf

from tensorflow.keras.datasets import mnist
from tensorflow.keras.utils import to_categorical

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype('float32') / 255

test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float32') / 255

train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)

# 准备 TensorBoard
log_dir = "logs/fit/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1)

model.compile(optimizer='rmsprop',
              loss="categorical_crossentropy",
              metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64,
          callbacks=[tensorboard_callback])
Train on 60000 samples
Epoch 1/5
60000/60000 [==============================] - 36s 599us/sample - loss: 0.0156 - accuracy: 0.9953
Epoch 2/5
60000/60000 [==============================] - 33s 554us/sample - loss: 0.0127 - accuracy: 0.9960
Epoch 3/5
60000/60000 [==============================] - 31s 524us/sample - loss: 0.0097 - accuracy: 0.9971
Epoch 4/5
60000/60000 [==============================] - 32s 529us/sample - loss: 0.0092 - accuracy: 0.9974
Epoch 5/5
60000/60000 [==============================] - 31s 523us/sample - loss: 0.0095 - accuracy: 0.9971

注:这里还可以用 tensorboard 显示的可视化模型训练情况: %tensorboard --logdir logs/fit。

来在测试集看一下结果:

test_loss, test_acc = model.evaluate(test_images, test_labels, verbose=2)
print(test_loss, test_acc)

输出:

10000/1 - 1s - loss: 0.0172 - accuracy: 0.9926
0.03441549262946125 0.9926

卷积

卷积神经网络

我们之前用的密集连接层是在整个输入特征空间(在 MNIST 中就是所有的像素)中学习全局模式的;而这里的卷积层是学习局部模式的。也就是说,Dense 是学整个图像的,而 Conv 是学图像的局部,比如在我们刚才写的代码里是学了 3x3 的窗口:

卷积层学习局部模式

这种卷积神经网络具有两个性质:

  • 卷积神经网络学到的模式是平移不变的(translation invariant):卷积神经网络学习到某个模式之后,在其他地方又看到了这个一样的模式,它就会认出它已经学过这个了,不用再去学一次了。而对于 Dense 的网络,即使遇到有一样的局部部分依然要去重新学习一次。这个性质让卷积神经网络可以高效利用数据,它只需要更少的训练样本就可以学到泛化比较好的数据表示(一个个局部都记住了嘛,而不是靠整体去映射)。

  • 卷积神经网络可以学到模式的空间层次结构(spatial hierarchies of patterns):卷积神经网络在第一层学完了一个一个小的局部模式之后,下一层又可以用这些小局部拼出大一些的模式。然后这样多搞几层,卷积神经网络就可以学到越来越复杂、越来越抽象的视觉概念了,就是下面图片这个意思:

卷积神经网络可以学到模式的空间层次结构

卷积层

我们刚才例子中用来表示图片的那种 3D 张量,包括两个空间轴 height、width 和一个深度轴 depth(也叫 channels 轴),对于 RGB 图片,深度轴的维度就是3,分别表示3种颜色嘛;而对于 MNIST 这种灰度图片,深度就是1,只用一个数去表示灰度值。在这种3D张量和在上面做的卷积运算的结果被称作 feature map(特征图)。

卷积运算会从输入特征图中提取出一个个小分块,并对所有这些分块施加一个相同的变换,得到输出特征图。输出特征图仍是一个 3D 张量:具有宽度和高度,其深度可能是任意值,深度的大小是该层的一个参数,深度轴里的每个 channel 都代表一个 filter (过滤器)。filter 会对输入数据的某一方面进行编码,比如,某个过滤器可以编码“输入中包含一张脸”这种概念。

在刚才的 MNIST 例子中,第一个卷积层接受尺寸为 (28, 28, 1) 的输入特征图,输出一个尺寸为 (26, 26, 32) 的特征图。这个输出中包含 32 个 filter,在每个深度轴中的 channel 都包含有 26x26 的值,叫做 filter 对输入的响应图(response map),表示 filter 在输入中不同位置上的运算结果。这也就是特征图为什么叫特征图的原因了:深度轴的每个维度都是一个特征(或过滤器),而 2D 张量 output[:, :, n] 是这个过滤器在输入上的响应的二维空间图。

响应图的示意图

卷积运算

关于卷积,,emmm,复变没怎么听懂,我主要是看「知乎: 如何通俗易懂地解释卷积?」来理解的。这里我们主要用的是这种作用:

卷积

Keras 的 Conv2D 层初始化写成:

Conv2D(output_depth, (window_height, window_width))

其中包含了卷积运算有两个核心参数:

  • 输出特征图的深度:在我们刚才的 MNIST 例子里用了 32 和 64;
  • 从输入中提取的每个块(滑窗)的尺寸:一般是 3x3 或者 5x5;

卷积运算会像滑动窗口一样的遍历所有可能的位置,把输入中每一小块的特征 (window_height, window_width, input_depth) 通过与一个称作卷积核(convolution kernel)的要学习的权重矩阵做点乘,变化得到一个向量 (output_depth, )。所有的这种结果向量拼在一起就得到了一个 3D 的最终输出 (height, width, output_depth),其中的每个值就是输入对应过来的,比如取 3x3 的滑窗,则 output[i, j, :] 来自 input[i-1:i+1, j-1:j+1, :]。

卷积的工作原理

关于卷积和 CNN 可以去看看这篇文章:Convolutional Neural Networks - Basics, An Introduction to CNNs and Deep Learning

注意,因为边界效应(border effects)和使用了步幅(strides),我们输出的宽度和高度可能与输入的宽度和高度不同。

边界效应和填充

边界效应就是你在做滑窗之后得到的矩阵大小会缩小一圈(边界没了)。例如现输入一个 5x5 的图片,取 3x3 的小块只能取出 9 块来,因此输出的结果为 3x3 的:

边界效应

之前我们做的 MNIST 也是类似的,一开始输入 28x28,第一层取 3x3 的,结果就是 26x26 了。

如果不希望这种减小发生,即希望保持输出的空间维度与输入的一致,则需要做填充(padding)。填充就是在输入的图片边界上加一些行和列,3x3 加1圈,5x5 要加2圈:

填充

Keras 的 Conv2D 层里可以用 padding 参数来设置使用填充。padding 可以设为:

  • "valid"(默认值):不做填充,只取“有效”的块。例如在 5×5 的输入特征图中,可以提取 3×3 图块的有效位置;
  • "same": 做填充,使输出和输入的 width、height 相等。
卷积步幅

卷积的步幅就是一次滑窗移多少,之前我们一直做的都是步幅为1的。我们把步幅大于 1 的卷积叫做步进卷积(strided convolution),比如下面这个是步幅为 2 的:

步幅为 2 的步进卷积

然而步进卷积在实际里面用的并不多😂,要做这种对特征图的下采样(downsampled)我们一般用最大池化。

注:

下采样:对于一个样值序列间隔几个样值取样一次,这样得到新序列就是原序列的下采样。

From 百度百科

最大池化

与步进卷积类似,最大池化是用来对特征图进行下采样的。在一开始的 MNIST 例子中,我们用了 MaxPooling2D 层之后,特征图的尺寸就减半了。

最大池化是在一个窗口里,从输入特征图取出每个 channel 的最大值,然后输出出来。这个运算和卷积很类似,不过施加的函数是一个 max。

最大池化我们一般都是用 2x2 的窗口,步幅为 2,这样取可以将特征图下采样2倍。(卷积是一般取3x3窗口和步幅1)

如果不用最大池化,直接把一大堆卷积层堆起来,会有两个问题:

  • 特征图尺寸下降的慢,搞到后面参数太多了,会加重过拟合;
  • 不利于空间层级结构的学习:一直一小点一小点的用卷积层去学,不利于看到更抽象的整体。

除了最大池化,下采样的方式还有很多:比如步进卷积、平均池化之类的。但一般用最大池化效果比较好,我们要的是知道有没有某个特征嘛,如果用平均去就把这个特征“减淡”了,如果用步进卷积又可能把这个信息错过了。

总而言之,使用最大池化/其他下采样的原因,一是减少需要处理的特征图的元素个数,二是通过让一系列的卷积层观察到越来越大的窗口(看到的覆盖越来越多比例的原始输入),从而学到空间层级结构。

在小型数据集上从头训练一个卷积神经网络

5.2 Training a convnet from scratch on a small dataset

我们搞计算机视觉的时候,经常要处理的问题就是在很小的数据集上训练一个图像分类的模型。emmm,这里的“很小”可以是几百到几万。

从这一节开始到后面几节,我们要搞的就是从头开始训练一个小型模型、使用预训练的网络做特征提取、对预训练的网络进行微调,这些步骤合起来就可以用于解决小型数据集的图像分类问题了。

我们这一节要做的是从头开始训练一个小型模型来分类猫狗的图片。不做正则化,先不管过拟合的问题。

下载数据

我们将使用 Dogs vs. Cats dataset 来训练模型,这个数据集里面是一大堆猫、狗的照片。这个数据集就不是 Keras 内置的了,我们可以从 Kaggle 下载:www.kaggle.com/c/dogs-vs-c…

下载下来解压缩,,,(emmmm有点大我的 MBP 都装不下了,放移动硬盘上才解出来的😂,emmmm,又想起来该买个新固态了)。

然后来创建我们要用的数据集:训练集猫狗各1000个样本,验证集各500个,测试集各500个。编程来完成这个工作:

# 将图像复制到训练、验证和测试的目录
import os, shutil

original_dataset_dir = '/Volumes/WD/Files/dataset/dogs-vs-cats/dogs-vs-cats/train'    # 原始数据集

base_dir = '/Volumes/WD/Files/dataset/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)

validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)

test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)

# 接下来把上面这段代码一遍,cat 改成 dog
...

# 复制猫的图片
fnames = [f'cat.{i}.jpg' for i in range(1000)]    # 这里用了 f-String,要求 Python >= 3.6,老版本可以用 'cat.{}.jpg'.format(i)
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(train_cats_dir, fname)
    shutil.copyfile(src, dst)
    
fnames = [f'cat.{i}.jpg' 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)
    
fnames = [f'cat.{i}.jpg' 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)
    
# 复制狗的图片: 和上面猫的类似,把 cat 改成 dog 就行了。
...
    
# 检查
print('total training cat images:', len(os.listdir(train_cats_dir)))
print('total validation cat images:', len(os.listdir(validation_cats_dir)))
print('total test cat images:', len(os.listdir(test_cats_dir)))

print('total training dog images:', len(os.listdir(train_dogs_dir)))
print('total validation dog images:', len(os.listdir(validation_dogs_dir)))
print('total test dog images:', len(os.listdir(test_dogs_dir)))

输出结果:

total training cat images: 1000
total validation cat images: 500
total test cat images: 500
total training dog images: 1000
total validation dog images: 500
total test dog images: 500

构建网络

在几乎所有的卷积神经网络里,我们都是让特征图的深度逐渐增大,而尺寸逐渐减小。所以这次我们也是这样的。

我们现在的这个问题是个二分类,所以最后一层用一个1单元的 sigmoid 激活的 Dense:

# 将猫狗分类的小型卷积神经网络实例化

from tensorflow.keras import layers
from tensorflow.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()
Model: "sequential_4"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_16 (Conv2D)           (None, 148, 148, 32)      896       
_________________________________________________________________
max_pooling2d_16 (MaxPooling (None, 74, 74, 32)        0         
_________________________________________________________________
conv2d_17 (Conv2D)           (None, 72, 72, 64)        18496     
_________________________________________________________________
max_pooling2d_17 (MaxPooling (None, 36, 36, 64)        0         
_________________________________________________________________
conv2d_18 (Conv2D)           (None, 34, 34, 128)       73856     
_________________________________________________________________
max_pooling2d_18 (MaxPooling (None, 17, 17, 128)       0         
_________________________________________________________________
conv2d_19 (Conv2D)           (None, 15, 15, 128)       147584    
_________________________________________________________________
max_pooling2d_19 (MaxPooling (None, 7, 7, 128)         0         
_________________________________________________________________
flatten_4 (Flatten)          (None, 6272)              0         
_________________________________________________________________
dense_8 (Dense)              (None, 512)               3211776   
_________________________________________________________________
dense_9 (Dense)              (None, 1)                 513       
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
_________________________________________________________________

然后就要编译这个网络了,做二分类嘛,所以损失函数用 binary crossentropy(二元交叉熵),优化器还是用 RMSprop (我们之前都是写 optimizer='rmsprop',这次要传点参数,所以用 optimizers.RMSprop 实例):

from tensorflow.keras import optimizers

model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=1e-4),
              metrics=['acc'])

数据预处理

我们要把那些图片搞成浮点数张量才能喂给神经网络。步骤如下:

  1. 读取图片文件
  2. 把 JPEG 文件内容解码成 RGB 像素网格
  3. 转化成浮点数张量
  4. 把像素的值从 [0, 255] 缩放到 [0, 1]

Keras 提供了一些工具可以自动完成这些:

# 使用 ImageDataGenerator 从目录中读取图像

from tensorflow.keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(
    train_dir,
    target_size=(150, 150),
    batch_size=20,
    class_mode='binary')    # 用二分类的标签

validation_generator = test_datagen.flow_from_directory(
    validation_dir,
    target_size=(150, 150),
    batch_size=20,
    class_mode='binary')
Found 2000 images belonging to 2 classes.
Found 1000 images belonging to 2 classes.

这个搞出来的 train_generator 和 validation_generator 就是 Python 的那种 Generator,惰性计算的那种。这个生成器一次 yield 出来一个 batch,所以把它叫做“batch generator”,迭代出一个来看一下:

for data_batch, labels_batch in train_generator:
    print('data batch shape:', data_batch.shape)
    print('labels batch shape:', labels_batch.shape)
    print('labels_batch:', labels_batch)
    break
data batch shape: (20, 150, 150, 3)
labels batch shape: (20,)
labels_batch: [1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 1. 1. 1. 1. 0. 0. 1. 1. 0. 1.]
# 利用 batch 生成器拟合模型
history = model.fit_generator(
    train_generator,
    steps_per_epoch=100,
    epochs=30,
    validation_data=validation_generator,
    validation_steps=50)

跑训练:

Epoch 1/30
100/100 [==============================] - 97s 967ms/step - loss: 0.6901 - acc: 0.5450 - val_loss: 0.6785 - val_acc: 0.5270
......
Epoch 30/30
100/100 [==============================] - 162s 2s/step - loss: 0.0460 - acc: 0.9885 - val_loss: 1.0609 - val_acc: 0.7150

这里因为是从 generator 读取 batch 来 fit,所以把我们平时用的 fit 改成了 fit_generator。里面传训练数据生成器、一个轮次要从 train_generator 里 yield 出来的次数(steps_per_epoch)、轮次、验证集生成器、一个轮次要从 validation_generator 里 yield 出来的次数(validation_steps)。

steps_per_epoch = 训练集数据总数 / 构建generator时指定的batch_size

validation_steps 和 steps_per_epoch 类似,只是是对验证集的。

用下面这行代码把训练好的模型保存下来:

# 保存模型
model.save('/Volumes/WD/Files/dataset/dogs-vs-cats/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, 'sr-', 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, 'sr-', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

png

png

不出所料,过拟合了,从差不多第5轮就开始过了。

接下来,我们要用 data augmentation (数据增强) 来降低过拟合。

数据增强

data augmentation (数据增强) 是用深度学习处理图像一般都会用到的一个方法。

过拟合是由于训练的数据太少导致的(只要样本足够多,模型就能看遍几乎所以可能,从而几乎不会犯错)。数据增强是一种以现有的样本为基础生成更多的训练数据的一种方法,这个方法利用多种能够生成可信图像的随机变换来增加。

在 Keras 中,我们用 ImageDataGenerator 的时候设几个参数就可以完成数据增强了:

datagen = ImageDataGenerator(
      rotation_range=40,      # 随机旋转图片的范围,0~180
      width_shift_range=0.2,  # 随机水平移动的比例
      height_shift_range=0.2, # 随机竖直移动的比例
      shear_range=0.2,        # 随机错切变换(shearing transformations)的角度
      zoom_range=0.2,         # 随机缩放的范围
      horizontal_flip=True,   # 是否做随机水平反转
      fill_mode='nearest')    # 填充新创建像素的方法

找张图片增强了试试:

from tensorflow.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)    # shape (150, 150, 3)
x = x.reshape((1,) + x.shape)  # shape (1, 150, 150, 3)

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

png

png

png

png

注意数据增强并没有带来新的信息,只是把原本就有的信息 remix 一下。所以在数据特别少的情况下,光用数据增强不足以消除过拟合,所以我们还需要在 Dense 层之前用上 Dropout。

# 定义一个包含 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))    # 👈 新增的 Dropout

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'])
# 利用数据增强生成器来训练卷积神经网络

# 数据生成器
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=100,
    epochs=100,
    validation_data=validation_generator,
    validation_steps=50)

# 保存模型
model.save('/Volumes/WD/Files/dataset/dogs-vs-cats/cats_and_dogs_small_2.h5')
Found 2000 images belonging to 2 classes.
Found 1000 images belonging to 2 classes.
Epoch 1/100
100/100 [==============================] - 142s 1s/step - loss: 0.6909 - acc: 0.5265 - val_loss: 0.6799 - val_acc: 0.5127
......
Epoch 100/100
100/100 [==============================] - 130s 1s/step - loss: 0.3140 - acc: 0.8624 - val_loss: 0.4295 - val_acc: 0.8274
# 绘制训练过程中的损失曲线和精度曲线
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, 'r-', 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, 'r-', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

png

png

👌用了 Data Augmentation 和 Dropout 之后,过拟合就好多了,精度也有所提升。

接下来,我们还会用一些技术来进一步优化模型。

【未完待续】