# 实例化一个小型的卷积神经网络
from keras import layers
from 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'))
卷积神经网络接收形状为 (image_height, image_width, image_channels) 的输入张量(不包括批量维度)。
本例中设置卷积神经网络处理大小为 (28, 28, 1) 的输入张量,这正是 MNIST 图像的格式。我们向第一层传入参数 input_shape=(28, 28, 1) 来完成此设置。
我们来看一下卷积神经网络的架构:
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) (None, 26, 26, 32) 320
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 13, 13, 32) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 11, 11, 64) 18496
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 5, 5, 64) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 3, 3, 64) 36928
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
_________________________________________________________________
可以看到,每个 Conv2D 层和 MaxPooling2D 层的输出都是一个形状为 (height, width, channels) 的 3D 张量。宽度和高度两个维度的尺寸通常会随着网络加深而变小。通道数量由传入 Conv2D 层的第一个参数所控制( 32 或 64)。
下一步是将最后的输出张量[大小为 (3, 3, 64)]输入到一个密集连接分类器网络中,即 Dense 层的堆叠;
这些分类器可以处理 1D 向量,而当前的输出是 3D 张量。首先,我们需要将 3D 输出展平为 1D。
# 在卷积神经网络上添加分类器
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) (None, 26, 26, 32) 320
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 13, 13, 32) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 11, 11, 64) 18496
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 5, 5, 64) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 3, 3, 64) 36928
_________________________________________________________________
flatten (Flatten) (None, 576) 0
_________________________________________________________________
dense (Dense) (None, 64) 36928
_________________________________________________________________
dense_1 (Dense) (None, 10) 650
=================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0
_________________________________________________________________
# 在 MNIST 图像上训练卷积神经网络
from keras.datasets import mnist
from 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)
# 拟合模型
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64)
# 在测试数据上对模型进行评估
test_loss, test_acc = model.evaluate(test_images, test_labels)
# 313/313 [==============================] - 2s 6ms/step - loss: 0.0291 - accuracy: 0.9909
test_acc
# 0.9908999800682068
密集连接层和卷积层的根本区别在于, Dense 层从输入特征空间中学到的是全局模式(比如对于 MNIST 数字,全局模式就是涉及所有像素的模式),而卷积层学到的是局部模式。对于图像来说,学到的就是在输入图像的二维小窗口中发现的模式。在上面的例子中,这些窗口的大小都是 3× 3。
这个重要特性使卷积神经网络具有以下两个有趣的性质:
对于包含两个空间轴( 高度和宽度)和一个深度轴(也叫通道轴)的 3D 张量,其卷积也叫特征图( feature map)。
卷积运算从输入特征图中提取图像块,并对所有这些图块应用相同的变换,生成输出特征图( output feature map)。
该输出特征图仍是一个 3D 张量,具有宽度和高度,其深度可以任意取值,因为输出深度是层的参数,深度轴的不同通道不再像 RGB 输入那样代表特定颜色,而是代表过滤器( filter)。过滤器对输入数据的某一方面进行编码。
在 MNIST 示例中,第一个卷积层接收一个大小为 (28, 28, 1) 的特征图,并输出一个大小为 (26, 26, 32) 的特征图,即它在输入上计算 32 个过滤器。
对于这 32 个输出通道,每个通道都包含一个 26× 26 的数值网格,它是过滤器对输入的响应图( response map),表示这个过滤器模式在输入中不同位置的响应。
这也是特征图这一术语的含义:深度轴的每个维度都是一个特征(或过滤器),而 2D 张量 output[:, :, n] 是这个过滤器在输入上的响应的二维空间图(map)。
卷积由以下两个关键参数所定义:
对于 Keras 的 Conv2D 层,这些参数都是向层传入的前几个参数:
Conv2D(output_depth, (window_height, window_width))
卷积的工作原理:
输出特征图中的每个空间位置都对应于输入特征图中的相同位置(比如输出的右下角包含了输入右下角的信息)。
假设有一个 5× 5 的特征图(共 25 个方块)。其中只有 9 个方块可以作为中心放入一个3× 3 的窗口,这 9 个方块形成一个 3× 3 的网格。
因此,输出特征图的尺寸是 3× 3。它比输入尺寸小了一点,在本例中沿着每个维度都正好缩小了 2 个方块。
在前一个例子中你也可以看到这种边界效应的作用:开始的输入尺寸为 28× 28,经过第一个卷积层之后尺寸变为
26× 26。
如果你希望输出特征图的空间维度与输入相同,那么可以使用填充( padding)。
填充是在输入特征图的每一边添加适当数目的行和列,使得每个输入方块都能作为卷积窗口的中心。
对于 3× 3 的窗口,在左右各添加一列,在上下各添加一行。对于 5× 5 的窗口,各添加两行和两列。
对于 Conv2D 层,可以通过 padding 参数来设置填充,这个参数有两个取值:
padding 参数的默认值为 “valid”。
影响输出尺寸的另一个因素是步幅的概念。
两个连续窗口的距离是卷积的一个参数,叫作步幅,默认值为 1。
也可以使用步进卷积( strided convolution),即步幅大于 1 的卷积。
用步幅为 2 的 3× 3 卷积从 5× 5 输入中提取的图块(无填充):
步幅为 2 意味着特征图的宽度和高度都被做了 2 倍下采样(除了边界效应引起的变化)。
虽然步进卷积对某些类型的模型可能有用,但在实践中很少使用。
为了对特征图进行下采样,我们不用步幅,而是通常使用最大池化( max-pooling)运算。
在卷积神经网络示例中,你可能注意到,在每个 MaxPooling2D 层之后,特征图的尺寸都会减半。
这就是最大池化的作用:对特征图进行下采样,与步进卷积类似。
最大池化是从输入特征图中提取窗口,并输出每个通道的最大值。
它的概念与卷积类似,但是最大池化使用硬编码的 max 张量运算对局部图块进行变换,而不是使用学到的线性变换(卷积核)。
最大池化与卷积的最大不同之处在于,最大池化通常使用 2× 2 的窗口和步幅 2,其目的是将特征图下采样 2 倍。与此相对的是,卷积通常使用 3× 3 窗口和步幅 1。
为什么要用这种方式对特征图下采样?为什么不删除最大池化层, 一直保留较大的特征图?
简而言之,使用下采样的原因,一是减少需要处理的特征图的元素个数,二是通过让连续卷积层的观察窗口越来越大(即窗口覆盖原始输入的比例越来越大),从而引入空间过滤器的层级结构。
最大池化不是实现这种下采样的唯一方法;
但最大池化的效果往往比这些替代方法更好。
简而言之,原因在于特征中往往编码了某种模式或概念在特征图的不同位置是否存在(因此得名特征图),而观察不同特征的最大值而不是平均值能够给出更多的信息。
“很少的”样本可能是几百张图像,也可能是几万张图像
数据集中包含 4000 张猫和狗的图像( 2000 张猫的图像, 2000 张狗的图像)。我们将 2000 张图像用于训练, 1000 张用于验证, 1000 张用于测试;
解决这一问题的基本策略:
使用已有的少量数据从头开始训练一个新模型:
首先,在 2000 个训练样本上训练一个简单的小型卷积神经网络,不做任何正则化,为模型目标设定一个基准。这会得到 71% 的分类精度;此时主要的问题在于过拟合。
数据增强( data augmentation):
它在计算机视觉领域是一种非常强大的降低过拟合的技术。使用数据增强之后,网络精度将提高到 82%;
用预训练的网络做特征提取:
得到的精度范围在 90%~96%;
对预训练的网络进行微调:
最终精度为 97%。
总而言之,这三种策略——从头开始训练一个小型模型、使用预训练的网络做特征提取、对预训练的网络进行微调——构成了你的工具箱,未来可用于解决小型数据集的图像分类问题。
仅在有大量数据可用时,深度学习才有效。这种说法部分正确:
深度学习的一个基本特性就是能够独立地在训练数据中找到有趣的特征,无须人为的特征工程,而这只在拥有大量训练样本时才能实现。
所谓“大量”样本是相对的,即相对于你所要训练网络的大小和深度而言:
只用几十个样本训练卷积神经网络就解决一个复杂问题是不可能的,但如果模型很小,并做了很好的正则化,同时任务非常简单,那么几百个样本可能就足够了。
由于卷积神经网络学到的是局部的、平移不变的特征,它对于感知问题可以高效地利用数据。
虽然数据相对较少,但在非常小的图像数据集上从头开始训练一个卷积神经网络,仍然可以得到不错的结果,而且无须任何自定义的特征工程。
深度学习模型本质上具有高度的可复用性:
已有一个在大规模数据集上训练的图像分类模型或语音转文本模型,你只需做很小的修改就能将其复用于完全不同的问题。
import os, shutil
# The path to the directory where the original
# dataset was uncompressed
original_dataset_dir = 'G:\\practice\\python\\kaggle\\train'
# The directory where we will
# store our smaller dataset
base_dir = 'G:\\practice\\python\\kaggle\\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)
有 2000 张训练图像、 1000 张验证图像和 1000 张测试图像。
每个分组中两个类别的样本数相同,这是一个平衡的二分类问题,分类精度可作为衡量成功的指标。
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()
Model: "sequential_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_3 (Conv2D) (None, 148, 148, 32) 896
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 74, 74, 32) 0
_________________________________________________________________
conv2d_4 (Conv2D) (None, 72, 72, 64) 18496
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 36, 36, 64) 0
_________________________________________________________________
conv2d_5 (Conv2D) (None, 34, 34, 128) 73856
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 17, 17, 128) 0
_________________________________________________________________
conv2d_6 (Conv2D) (None, 15, 15, 128) 147584
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 7, 7, 128) 0
_________________________________________________________________
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
_________________________________________________________________
# 配置模型用于训练
from keras import optimizers
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
将数据输入神经网络之前,应该将数据格式化为经过预处理的浮点数张量。
现在,数据以 JPEG 文件的形式保存在硬盘中,所以数据预处理步骤大致如下:
Keras 拥有自动完成这些步骤的工具。 Keras 有一个图像处理辅助工具的模块,位于 keras.preprocessing.image。特别地,它包含 ImageDataGenerator 类,可以快速创建 Python 生成器,能够将硬盘上的图像文件自动转换为预处理好的张量批量。
# 使用 ImageDataGenerator 从目录中读取图像
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')
# 利用批量生成器拟合模型
history = model.fit(
train_generator,
steps_per_epoch=100,
epochs=30,
validation_data=validation_generator,
validation_steps=50)
过拟合的原因是学习样本太少,导致无法训练出能够泛化到新数据的模型。
数据增强是从现有的训练样本中生成更多的训练数据,其方法是利用多种能够生成可信图像的随机变换来增加( augment)样本。
其目标是,模型在训练时不会两次查看完全相同的图像。
在 Keras 中,这可以通过对 ImageDataGenerator 实例读取的图像执行多次随机变换来实现。
# 利用 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')
如果你使用这种数据增强来训练一个新网络,那么网络将不会两次看到同样的输入;
但网络看到的输入仍然是高度相关的,因为这些输入都来自于少量的原始图像;你无法生成新信息,而只能混合现有信息。因此,这种方法可能不足以完全消除过拟合。
为了进一步降低过拟合,你还需要向模型中添加一个 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))
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,)
# 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(
train_generator,
steps_per_epoch=100,
epochs=100,
validation_data=validation_generator,
validation_steps=50)