5.3 使用预训练的卷积神经网络
预训练网络(pretrained network):
- 是一个之前已在大型数据集(通常是大规模图像分类任务)上 训 练 好 、 保 存 好 的 网 络 \color{red}训练好、保存好的网络 训练好、保存好的网络。
- 预训练网络 学 到 的 特 征 的 空 间 层 次 结 构 \color{red}学到的特征的空间层次结构 学到的特征的空间层次结构可以有效地作为视觉世界的通用模型,在不同问题之间具有 可 移 植 性 \color{red}可移植性 可移植性。
- 由于预训练网络,使得深度学习 对 小 数 据 问 题 非 常 有 效 \color{red}对小数据问题非常有效 对小数据问题非常有效。
使用预训练网络有两种方法: 特 征 提 取 ( f e a t u r e e x t r a c t i o n ) \color{red}特征提取(feature\;extraction) 特征提取(featureextraction)和 微 调 模 型 ( f i n e − t u n i n g ) \color{red}微调模型(fine-tuning) 微调模型(fine−tuning)。
5.3.1 特征提取
-
定义:
特 征 提 取 是 使 用 之 前 网 络 学 到 的 表 示 来 从 新 样 本 中 提 取 出 有 趣 的 特 征 。 \color{red}特征提取是使用之前网络学到的表示来从新样本中提取出有趣的特征。 特征提取是使用之前网络学到的表示来从新样本中提取出有趣的特征。 -
卷 积 基 \color{red}卷积基 卷积基:
- 图像分类的卷积神经网络包含两部分:首先是一系列池化层和卷积层,最后是一个密集连接分类器。第一部分叫作模型的 卷 积 基 ( c o n v o l u t i o n a l b a s e ) \color{red}卷积基(convolutional\;base) 卷积基(convolutionalbase)。
- 特征提取就是取出之前训练好的网络的卷积基,在上面运行新数据,然后
在
输
出
上
面
训
练
一
个
新
的
分
类
器
\color{red}在输出上面训练一个新的分类器
在输出上面训练一个新的分类器。
- 为什么不用密集层:密集连接层的表示不再包含物体在输入图像中的 位 置 信 息 \color{red}位置信息 位置信息。密集连接层舍弃了空间的概念,而物体位置信息仍然由卷积特征图所描述。
- 某个卷积层提取的
表
示
的
通
用
性
\color{red}表示的通用性
表示的通用性(以及可复用性)取决于
该
层
在
模
型
中
的
深
度
\color{red}该层在模型中的深度
该层在模型中的深度。
模型型中更靠近底部的层提取的是局部的、高度通用的特征图(比如视觉边缘、颜色和纹理),而更靠近顶部的层提取的是更加抽象的概念(比如“猫耳朵”或“狗眼睛”)。
- 如果你的新数据集与原始模型训练的数据集有很大差异,那么最好只使用模型的前几层来做特征提取,而不是使用整个卷积基。
- 常用的模型内置于Keras 中。你可以从
keras.applications
模块中导入。
-
VGG16模型
from tensorflow.keras.applications import VGG16 conv_base = VGG16(weights='imagenet', include_top=False, input_shape=(150, 150, 3))
这里向构造函数中传入了三个参数。
weights
指定模型初始化的权重检查点。include_top
指定模型最后是否包含密集连接分类器。默认情况下,这个密集连接分类器对应于 ImageNet 的 1000 个类别。因为我们打算使用自己的密集连接分类器(只有两个类别: cat 和 dog),所以不需要包含它。input_shape
是输入到网络中的图像张量的形状。这个参数完全是可选的,如果不传入这个参数,那么网络能够处理任意形状的输入。
conv_base.summary()
最后的特征图形状为
(4, 4, 512)
。我们将在这个特征上添加一个密集连接分类器。下一步有两种方法可供选择。- 不使用数据增强的快速特征提取。
这种方法速度快,计算代价低,因为对于每个输入图像只需运行一次卷积基,而卷积基是目前流程中计算代价最高的。 - 使用数据增强的特征提取
在顶部添加Dense
层来扩展已有模型(即conv_base
),并在输入数据上端到端地运行整个模型。
5.3.1.1. 不使用数据增强的快速特征提取
首先,运行ImageDataGenerator
实例,将图像及其标签提取为Numpy
数组。调用 conv_base
模型的predict
方法来从这些图像中提取特征。第一种方法的代码:
保
存
你
的
数
据
在
\color{red}保存你的数据在
保存你的数据在 conv_base 中的输出
,
然
后
将
这
些
输
出
作
为
输
入
用
于
新
模
型
\color{red}然后将这些输出作为输入用于新模型
然后将这些输出作为输入用于新模型。
-
使用预训练的卷积基提取特征
import os import numpy as np from tensorflow.keras.preprocessing.image import ImageDataGenerator base_dir = 'C:\\Users\\Administrator\\deep-learning-with-python-notebooks-master\\cats_and_dogs_small' train_dir = os.path.join(base_dir, 'train') validation_dir = os.path.join(base_dir, 'validation') test_dir = os.path.join(base_dir, 'test') datagen = ImageDataGenerator(rescale=1./255) batch_size = 20 def extract_features(directory, sample_count): features = np.zeros(shape=(sample_count, 4, 4, 512)) labels = np.zeros(shape=(sample_count)) generator = datagen.flow_from_directory( directory, target_size=(150, 150), batch_size=batch_size, class_mode='binary') i = 0 for inputs_batch, labels_batch in generator: features_batch = conv_base.predict(inputs_batch) features[i * batch_size : (i + 1) * batch_size] = features_batch labels[i * batch_size : (i + 1) * batch_size] = labels_batch i += 1 if i * batch_size >= sample_count: break # 注意,这些生成器在循环中不断生成数据, # 所以你必须在读取完所有图像后终止循环 return features, labels train_features, train_labels = extract_features(train_dir, 4000) validation_features, validation_labels = extract_features(validation_dir, 2000) test_features, test_labels = extract_features(test_dir, 2000) # 提取的特征形状为 (samples, 4, 4, 512) 。我们要将其输入到密集连接分类器中, # 所以首先必须将其形状展平为 (samples, 8192) train_features = np.reshape(train_features, (4000, 4 * 4 * 512)) validation_features = np.reshape(validation_features, (2000, 4 * 4 * 512)) test_features = np.reshape(test_features, (2000, 4 * 4 * 512))
-
定义并训练密集连接分类器
需要使用dropout
正则化,并在刚刚保存的数据和标签上训练这个分类器。from tensorflow.keras import models from tensorflow.keras import layers from tensorflow.keras import optimizers model = models.Sequential() model.add(layers.Dense(256, activation='relu', input_dim=4 * 4 * 512)) model.add(layers.Dropout(0.5)) model.add(layers.Dense(1, activation='sigmoid')) model.compile(optimizer=optimizers.RMSprop(lr=2e-5), loss='binary_crossentropy', metrics=['acc']) history = model.fit(train_features, train_labels, epochs=30, batch_size=20, validation_data=(validation_features, validation_labels))
-
绘制结果
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()
虽然 dropout 比率相当大,但模型几乎从一开始就过拟合。这是因为 本 方 法 没 有 使 用 数 据 增 强 \color{red}本方法没有使用数据增强 本方法没有使用数据增强,而 数 据 增 强 对 防 止 小 型 图 像 数 据 集 的 过 拟 合 非 常 重 要 \color{red}数据增强对防止小型图像数据集的过拟合非常重要 数据增强对防止小型图像数据集的过拟合非常重要。
5.3.1.2.使用数据增强的特征提取
它的速度更慢,计算代价更高,但在训练期间可以使用数据增强。这种方法就是:
扩
展
\color{red}扩展
扩展 conv_base 模型
,
然
后
在
输
入
数
据
上
端
到
端
地
运
行
模
型
\color{red}然后在输入数据上端到端地运行模型
然后在输入数据上端到端地运行模型。
本方法计算代价很高,只在有 GPU 的情况下才能尝试运行。它在 CPU 上是绝对难以运行的。如果你无法在
GPU
上运行代码,那么就采用第一种方法。
- 在卷积基上添加一个密集连接分类器
from tensorflow.keras import models from tensorflow.keras import layers model = models.Sequential() model.add(conv_base) model.add(layers.Flatten()) model.add(layers.Dense(256, activation='relu')) model.add(layers.Dense(1, activation='sigmoid'))
model.summary()
- VGG16 的卷积基有 14 714 688 个参数,非常多。在其上添加的分类器有 200 万个参数。
- 在编译和训练模型之前,一定要“冻结”卷积基。 冻 结 \color{red}冻结 冻结(freeze)一个或多个层是指在训练过程中保持其权重不变。如果不这么做,那么卷积基之前学到的表示将会在训练过程中被修改。因为其上添加的 Dense 层是随机初始化的,所以非常大的权重更新将会在网络中传播,对之前学到的表示造成很大破坏。
- 冻结模型
在 Keras 中, 冻 结 网 络 的 方 法 是 将 其 t r a i n a b l e 属 性 设 为 F a l s e \color{red}冻结网络的方法是将其 trainable 属性设为 False 冻结网络的方法是将其trainable属性设为False。
设置之后,只有添加的两个>>> print('This is the number of trainable weights ' 'before freezing the conv base:', len(model.trainable_weights)) This is the number of trainable weights before freezing the conv base: 30 >>> conv_base.trainable = False >>> print('This is the number of trainable weights ' 'after freezing the conv base:', len(model.trainable_weights)) This is the number of trainable weights after freezing the conv base: 4
Dense
层的权重才会被训练。总共有 4 个权重张量,每层2 个(主权重矩阵和偏置向量)。在编译之后修改了权重的 trainable 属性,那么应该重新编译模型,否则这些修改将被忽略。
- 利用冻结的卷积基端到端地训练模型
from tensorflow.keras.preprocessing.image import ImageDataGenerator from tensorflow.keras import optimizers 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, fill_mode='nearest') # 注意,不能增强验证数据 test_datagen = ImageDataGenerator(rescale=1./255) train_generator = train_datagen.flow_from_directory( train_dir,#目标目录 target_size=(150, 150),# 将所有图像的大小调整为 150×150 batch_size=20, class_mode='binary') #因为使用了 binary_crossentropy损失,所以需要用二进制标签 validation_generator = test_datagen.flow_from_directory( validation_dir, target_size=(150, 150), batch_size=20, class_mode='binary') model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=2e-5), metrics=['acc']) history = model.fit_generator( train_generator, steps_per_epoch=200,#数据是4000 epochs=30, validation_data=validation_generator, validation_steps=50)
5.3.2 微调模型
-
模 型 微 调 ( f i n e − t u n i n g ) \color{red}模型微调(fine-tuning) 模型微调(fine−tuning)与特征提取 互 为 补 充 \color{red}互为补充 互为补充。
-
微 调 模 型 定 义 \color{red}微调模型定义 微调模型定义:
对于用于特征提取的冻结的模型基,微调是指将其 顶 部 的 几 层 “ 解 冻 ” \color{red}顶部的几层“解冻” 顶部的几层“解冻”,并将 解 冻 的 几 层 和 新 增 加 的 部 分 联 合 训 练 \color{red}解冻的几层和新增加的部分联合训练 解冻的几层和新增加的部分联合训练。对于本例中,微调是调原有最后的部分和新增的部分。- 冻 结 V G G 16 的 卷 积 基 是 为 了 能 够 在 上 面 训 练 一 个 随 机 初 始 化 的 分 类 器 。 \color{red}冻结 VGG16 的卷积基是为了能够在上面训练一个随机初始化的分类器。 冻结VGG16的卷积基是为了能够在上面训练一个随机初始化的分类器。
-
只
有
上
面
的
分
类
器
已
经
训
练
好
了
,
才
能
微
调
卷
积
基
的
顶
部
几
层
。
\color{red}只有上面的分类器已经训练好了,才能微调卷积基的顶部几层。
只有上面的分类器已经训练好了,才能微调卷积基的顶部几层。
-
微调网络的步骤
- 在 已 经 训 练 好 的 基 网 络 ( b a s e n e t w o r k ) 上 添 加 自 定 义 网 络 \color{red}在已经训练好的基网络(base network)上添加自定义网络 在已经训练好的基网络(basenetwork)上添加自定义网络。
- 冻 结 基 网 络 \color{red}冻结基网络 冻结基网络。
- 训 练 所 添 加 的 部 分 \color{red}训练所添加的部分 训练所添加的部分。
- 解 冻 基 网 络 的 一 些 层 \color{red}解冻基网络的一些层 解冻基网络的一些层。
- 联 合 训 练 解 冻 的 这 些 层 和 添 加 的 部 分 \color{red}联合训练解冻的这些层和添加的部分 联合训练解冻的这些层和添加的部分。
前三步是特征提取的步骤。
-
为什么选择 更 靠 顶 层 做 微 调 \color{blue}更靠顶层做微调 更靠顶层做微调?
- 卷积基中 更 靠 底 部 的 层 编 码 的 是 更 加 通 用 的 可 复 用 特 征 \color{red}更靠底部的层编码的是更加通用的可复用特征 更靠底部的层编码的是更加通用的可复用特征,而更靠顶部的层编码的是更专业化的特征。
- 训 练 的 参 数 越 多 , 过 拟 合 的 风 险 越 大 \color{red}训练的参数越多,过拟合的风险越大 训练的参数越多,过拟合的风险越大。
-
冻结直到某一层的所有层
conv_base.trainable = True set_trainable = False for layer in conv_base.layers: if layer.name == 'block5_conv1': set_trainable = True if set_trainable: layer.trainable = True else: layer.trainable = False
-
微调模型
model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-5), metrics=['acc']) history = model.fit_generator( train_generator, steps_per_epoch=200, epochs=100, validation_data=validation_generator, validation_steps=50)
-
使曲线变得平滑
import matplotlib.pyplot as plt def smooth_curve(points, factor=0.8): smoothed_points = [] for point in points: if smoothed_points: previous = smoothed_points[-1] smoothed_points.append(previous * factor + point * (1 - factor)) else: smoothed_points.append(point) return smoothed_points plt.plot(epochs, smooth_curve(acc), 'bo', label='Smoothed training acc') plt.plot(epochs, smooth_curve(val_acc), 'b', label='Smoothed validation acc') plt.title('Training and validation accuracy') plt.legend() plt.figure() plt.plot(epochs, smooth_curve(loss), 'bo', label='Smoothed training loss') plt.plot(epochs, smooth_curve(val_loss), 'b', label='Smoothed validation loss') plt.title('Training and validation loss') plt.legend() plt.show()
Epoch 97/100 200/200 [==============================] - 224s 1s/step - loss: 0.0158 - acc: 0.9950 - val_loss: 0.3688 - val_acc: 0.9450 Epoch 98/100 200/200 [==============================] - 223s 1s/step - loss: 0.0153 - acc: 0.9945 - val_loss: 0.4035 - val_acc: 0.9410 Epoch 99/100 200/200 [==============================] - 223s 1s/step - loss: 0.0260 - acc: 0.9901 - val_loss: 0.3588 - val_acc: 0.9440 Epoch 100/100 200/200 [==============================] - 223s 1s/step - loss: 0.0155 - acc: 0.9946 - val_loss: 0.4835 - val_acc: 0.9410
精度提高到94%,损失曲线并没有很大变化。
从损失曲线上看不出与之前相比有任何真正的提高(实际上还在变差)。如果损失没有降低,那么精度怎么能保持稳定或提高呢?答案很简单:图中展示的是逐点(pointwise)损失值的平均值,但 影 响 精 度 的 是 损 失 值 的 分 布 \color{red}影响精度的是损失值的分布 影响精度的是损失值的分布,而不是平均值,因为精度是模型预测的类别概率的二进制阈值。即使从平均损失中无法看出,但模型也仍然可能在改进。
5.3.2.1 最终评估这个模型
test_generator = test_datagen.flow_from_directory(
test_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
test_loss, test_acc = model.evaluate_generator(test_generator, steps=50)
print('test acc:', test_acc)
5.3.3 小结
图像分类问题,特别是对于 小 型 数 据 集 \color{red}小型数据集 小型数据集:
- 卷 积 神 经 网 络 是 用 于 计 算 机 视 觉 任 务 的 最 佳 机 器 学 习 模 型 \color{red}卷积神经网络是用于计算机视觉任务的最佳机器学习模型 卷积神经网络是用于计算机视觉任务的最佳机器学习模型。即使在非常小的数据集上也可以从头开始训练一个卷积神经网络,而且得到的结果还不错。
- 在 小 型 数 据 集 上 的 主 要 问 题 是 过 拟 合 \color{red}在小型数据集上的主要问题是过拟合 在小型数据集上的主要问题是过拟合。在处理图像数据时, 数 据 增 强 \color{red}数据增强 数据增强是一种降低过拟合的强大方法。
- 利用 特 征 提 取 \color{red}特征提取 特征提取,可以很容易将现有的卷积神经网络 复 用 \color{red}复用 复用于新的数据集。对于小型图像数据集,这是一种很有价值的方法。
- 作为特征提取的补充,你还可以使用 微 调 \color{red}微调 微调,将现有模型之前学到的一些数据表示应用于新问题。这种方法可以进一步提高模型性能。
完整代码
## 数据准备
import os, shutil
# 原始数据集解压目录的路径
original_dataset_dir = 'C:\\Users\\Administrator\\Python_learning\\kaggle_original_data'
# 保存较小数据集的目录
base_dir = 'C:\\Users\\Administrator\\Python_learning\\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)
# 狗的训练图像目录
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)
# 猫的验证图像目录
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
# 狗的验证图像目录
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)
# 猫的测试图像目录
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)
# 狗的测试图像目录
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_dir)
# 将前 2000 张猫的图像复制到 train_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_cats_dir, fname)
shutil.copyfile(src, dst)
# 将接下来 1000 张猫的图像复制到 validation_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(2000, 3000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_cats_dir, fname)
shutil.copyfile(src, dst)
# 将接下来的 1000 张猫的图像复制到 test_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(3000, 4000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_cats_dir, fname)
shutil.copyfile(src, dst)
# 将前 2000 张狗的图像复制到 train_dogs_dir
fnames = ['dog.{}.jpg'.format(i) for i in range(2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_dogs_dir, fname)
shutil.copyfile(src, dst)
# 将接下来 1000 张狗的图像复制到 validation_dogs_dir
fnames = ['dog.{}.jpg'.format(i) for i in range(2000, 3000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_dogs_dir, fname)
shutil.copyfile(src, dst)
# 将接下来 1000 张狗的图像复制到 test_dogs_dir
fnames = ['dog.{}.jpg'.format(i) for i in range(3000, 3000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_dogs_dir, fname)
shutil.copyfile(src, dst)
## 数据预处理, 使用数据增强
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), # 将所有图像的大小调整为 150×150
batch_size=20,
# 书上是32,但是32*200(steps_per_epoch)大于了4000,运行会报错,所以改为20.
class_mode='binary')
# 因为使用了 binary_crossentropy损失,所以需要用二进制标签
validation_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=20, # 书上是32,但是32*200(steps_per_epoch)大于了4000,运行会报错,所以改为20.
class_mode='binary')
## 调用VGG16
from tensorflow.keras.applications import VGG16
conv_base = VGG16(weights='imagenet',
include_top=False,
input_shape=(150, 150, 3))
## 使用数据增强的特征提取
# 在卷积基上添加一个密集连接分类器
from tensorflow.keras import models
from tensorflow.keras import layers
model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
## 微调模型
conv_base.trainable = True
set_trainable = False
for layer in conv_base.layers:
if layer.name == 'block5_conv1':
set_trainable = True
if set_trainable:
layer.trainable = True
else:
layer.trainable = False
# 训练模型
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-5),
metrics=['acc'])
history = model.fit_generator(
train_generator,
steps_per_epoch=200,
epochs=100,
validation_data=validation_generator,
validation_steps=50)
## 绘制光滑曲线
def smooth_curve(points, factor=0.8):
smoothed_points = []
for point in points:
if smoothed_points:
previous = smoothed_points[-1]
smoothed_points.append(previous * factor + point * (1 - factor))
else:
smoothed_points.append(point)
return smoothed_points
plt.plot(epochs,
smooth_curve(acc), 'bo', label='Smoothed training acc')
plt.plot(epochs,
smooth_curve(val_acc), 'b', label='Smoothed validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs,
smooth_curve(loss), 'bo', label='Smoothed training loss')
plt.plot(epochs,
smooth_curve(val_loss), 'b', label='Smoothed validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()
## 在测试集上评估模型
test_generator = test_datagen.flow_from_directory(
test_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
test_loss, test_acc = model.evaluate_generator(test_generator, steps=50)
print('test acc:', test_acc)