文章目录
1 卷积神经网络简介
首先通过一个简单的卷积神经网络示例,对MNIST数字进行分类。
- 实例化一个小型的卷积神经网络
- 首先展示一个简单的卷积神经网络,是Conv2D层和MaxPooling2D层的堆叠。
# 实例化一个小型的卷积神经网络
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图像的格式。 -
目前卷积神经网络的架构如下,可以看出,Conv2D层和MaxPooling2D层的输出都是形为(height,width,channels)的3D张量。通道数量由传入Conv2D层的第一个参数控制。
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
_________________________________________________________________
"""
- 在卷积神经网络上添加分类器
- 接下来将最后的输出张量输入到一个密集连接分类器网络中,即Dense层的堆叠,它可以处理1D向量,因此需要将当前输出的3D张量展平为1D,然后添加Dense层。由于将进行10类别分类,因此最后一层使用带10个输出的softmax激活。
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('float') / 255
test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float') / 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)
1.1 卷积运算
1.密集连接层和卷积层的区别
- Dense层从输入特征空间中学到的是全局模式;
- 卷积层学到的是局部模式。
2.卷积神经网络的两个性质
- 平移不变性,卷积神经网络在图像右下角学到某个模式后,可以在任何地方识别这个模式,因此可以用更少的训练样本学到泛化能力更强的数据表示。
- 卷积神经网络可以学到模式的空间层次结构。
3.特征图
- 定义:对于包含两个空间轴(高度和宽度)和一个深度轴(也叫通道轴)的 3D张量,其卷积也叫特征图。
- 卷积运算:从输入特征中提取图块,并对所有这些图块应用相同的变换,生成输出特征图,它仍然是一个 3D张量,深度可以取任意值,因为输出深度是层的参数。深度轴的不同通道代表的是过滤器(filter),它对输入数据的某一方面进行编码。
- 实例解释
第一个卷积层接收一个大小为(28,28,1)的特征图,输出大小为(26,26,32)的特征图,即它在输入上计算32个过滤器。对于32个输出通道,每个都包含一个 26 * 26 的数值网格,它是过滤器对输入的响应图,表示这个过滤器模式在输入中不同位置的响应。
4.卷积的两个关键参数
- 从输入中提取的图块尺寸;
- 输出特征图的深度,即过滤器数量。
Keras——Conv2D(output_depth,window_height,window_width)
5. 卷积的工作原理
- 在 3D 输入特征图上滑动窗口,在每个可能位置停止并提取周围特征的3D图块 [ 形如(window_height,window_width,input_depth)]。
- 每个 3D 图块与学到的同一个权重矩阵(卷积核)做张量积,转为形如(output_depth,)的1D向量;
- 对所有 1D 向量进行空间重组,使其转换为形如(height,width,output_depth)的 3D 输出特征图。
6. 边界效应与填充
- 定义:在输入特征的每一边添加适当数目的行和列,使得每个输入方块都能作为卷积窗口的中心。
- 作用:使得输出特征图的空间维度与输入相同。
- 对于Conv2D层,通过padding参数来设置填充,该参数的两个取值:
- valid,不适用填充(默认)
- same,填充后输出的宽度和高度与输入相同
7. 卷积步幅
- 定义:两个连续窗口的距离,默认值为1。
- 步进卷积,步幅大于1的卷积。
- 作用:对特征图进行下采样。
1.2 最大池化运算
1.作用
最大池化从输入特征图中提取窗口,并输出每个通道的最大值,能够对特征图进行下采样。
2.下采样
- 使用下采样的原因:
- 减少需要处理的特征图的元素个数;
- 通过让连续卷积层的观察窗口越来越大,从而引入空间过滤器的层级结构;
- 方式:最大池化、平均池化、步幅实现。
2 在小型数据集上从头开始训练一个卷积神经网络
本节将会介绍一个小型数据集的实例,数据集中包含4000张猫和狗的图像(2000张猫的图像,2000张狗的图像),将2000张图像用于训练,1000张用于验证,1000张用于测试。
本节将介绍如何使用少量数据从头训练一个新模型。
首先,在2000个样本上训练一个简单的小型卷积神经网络,不做任何正则化,为模型目标设定一个基准,这会得到71%的分类精度,此时会出现过拟合;
然后介绍数据增强,降低过拟合。
2.1 下载数据
下载地址:猫狗分类数据集
1. 将图像复制到训练、验证和测试的目录
import os, shutil
# 原始数据集解压目录的路径
original_dataset_dir = '/User/fchollet/Downloads/Kaggle'
# 保存较小数据集的目录
base_dir = ''
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)
# 将前1000张猫的图像复制到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)
# 将接下来500张猫的图像复制到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)
# 将接下来500张猫的图像复制到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)
# 将前1000张狗的图像复制到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)
# 将接下来500张狗的图像复制到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)
# 将接下来500张狗的图像复制到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)
2.检查每个分组(训练/验证/测试)中分别包含多少张图像。
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
2.2 构建网络
由于本例要处理的是更大的图像和更复杂的问题,因此在上一个例子(MNIST数据集)的基础上,再增加一个Conv2D + MaxPooling2D的组合。这样既可以增大网络容量,也可以进一步减小特征图的尺寸,使其在连接Flatten层时尺寸不会太大。
本例中初始输入的尺寸为 150 * 150,所以在Flatten层之前的特征图为 7 * 7 。
网络中特征图的深度在逐渐增大,而特征图的尺寸在逐渐减小。
1. 将猫狗分类的小型卷积神经网络实例化
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'))
2.观察特征图维度变化
model.summary()
"""
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) (None, 148, 148, 32) 896
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 74, 74, 32) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 72, 72, 64) 18496
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 36, 36, 64) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 34, 34, 128) 73856
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 17, 17, 128) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 15, 15, 128) 147584
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 7, 7, 128) 0
_________________________________________________________________
flatten (Flatten) (None, 6272) 0
_________________________________________________________________
dense (Dense) (None, 512) 3211776
_________________________________________________________________
dense_1 (Dense) (None, 1) 513
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
_________________________________________________________________
"""
3.配置模型用于训练
from keras import optimizers
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
2.3 数据预处理
1.数据预处理步骤
- 读取图像文件;
- 将 JPEG 文件解码为 RGB 像素网格;
- 将像素网格转换为浮点数向量;
- 将像素值(0~255范围内)缩放到 [0,1] 区间。
Keras 拥有一个 图像处理辅助工具 的模块,位于 keras.preprocessing.image ,能够自动完成这些步骤。 它包含 ImageDataGenerator类 ,可以快速创建 Python 生成器,将硬盘上的图像文件自动转换成为预处理好的张量批量。
from keras.preprocessing.image import ImageDataGenerator
# 将所有图像乘以 1/255 缩放
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), # 将所有图像的大小调整为 150*150
batch_size=20,
# 因为使用了binary_crossentropy损失,所以用二进制标签
class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
Python生成器
类似于迭代器的对象,可以和 for … in 运算符一起使用,生成器用 yield运算符 来构造。
2. 查看生成器输出
for data_batch, labels_batch in train_generator:
print('data batch shape:', data_batch.shape)
print('labels batch shape:', labels_batch.shape)
break
其中一个生成器的输出:生成了150 * 150 的 RGB 图像 [形状为(20,150,150,3)] 与二进制标签[ 形状为(20,)] 组成的批量。每个批量包含 20 个样本(批量大小)。
3.利用批量生成器拟合模型
使用 fit_generator方法 来拟合。
它的第一个参数是一个 Python 生成器 ,比如train_generator;
第二个参数 steps_per_epoch,表示每一轮从生成器中抽取多少个样本;
第四个参数 validation_data 可以是一个数据生成器,也可以是 Numpy 数组组成的元组;如果传入的是一个生成器,那么它应该能够不断地生成验证数据,因此还需要指定 validation_steps 参数,指明需要从验证生成器中抽取多少批次用于评估。
history = model.fit_generator(
train_generator,
steps_per_epoch=100,
epochs=30,
validation_data=validation_generator,
validation_steps=50)
4. 保存模型
在训练完成之后保存模型,之后可以直接使用。
model.save('cats_and_dogs_small_1.h5')
5. 绘制训练过程中的损失曲线和精度曲线
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()
从图像中可以看出,训练精度随着时间线性增加,直到接近 100% ,而验证精度则停留在 70%~72% 。验证损失仅在 5 轮之后就达到最小值,然后保持不变,而训练损失则一直线性下降,直到接近于 0 。
因为训练样本较少,所以容易达到过拟合。
接下来将介绍这一种针对于计算机视觉领域的新方法, 数据增强。
2.4 数据增强
1. 数据增强: 从现有样本中生成更多的训练数据,利用多种能够生成可信图像的随机变换来增加样本。
2. 利用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')
- rotation_range 是角度值(0~180),表示图像随机旋转的角度范围;
- width_shift 和 height_shift 是图像在水平或垂直方向上平移的范围(相对于总宽度或总高度的比例)
- shear_range 是随机错切变换的角度;
- zoom_range 是图像随机缩放的范围;
- horizontal_flip 是随机将一半图像水平翻转,如果没有水平不对称的假设,这种做法是有意义的;
- fill_mode 用于填充新创建像素的方法,新像素可能来自于旋转或宽度/高度平移。
3. 随机增强后的训练图像
# 图像预处理工具模块
from 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))
# 将其转换为形状为(150,150,3)的Numpy数组
x = image.img_to_array(img)
# 将其形状改变为(1,150,150,3)
x = x.reshape((1,) + x.shape)
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()
4. 定义一个包含 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'])
5. 利用数据增强生成器训练卷积神经网络
拟合函数进行了改动,原来的steps_per_epoch=100,运行会出错,原因是数据集量变小,结合运行错误提示,上限可以到63,因此这里改为steps_per_epoch=63;
同理, validation_steps也应该随着改变,改为 validation_steps=32,以下代码已做更正。
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=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=63,
epochs=100,
validation_data=validation_generator,
validation_steps=32
)
6. 保存模型
model.save('cats_and_dogs_small_2.h5')
7. 绘制训练过程中的损失曲线和精度曲线
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()
3 使用预训练的卷积神经网络
1. 预训练网络: 已经在大型数据集(通常是大规模图像分类任务)上训练好的网络。
2. 预训练网络的两种方法
- 特征提取
- 微调模型
3.1 特征提取
1.定义
特征提取是使用之前训练好的 卷积基 「一系列池化层和卷积层」,在上面运行新数据,然后输出一个新的分类器。
2.重复使用卷积基的原因
卷积基学到的表示可能更加通用,更适合重复使用。
然而不重复使用密集连接分类器,因为分类器学到的是针对模型训练的类别,仅包含某个类别出现在整张图像中的概率信息。此外,密集连接层的表示不再包含物体在输入图像中的位置信息。
3.卷积层提取的表示的通用性取决于该层在模型中的深度。
更靠近底部的层提取的是局部的、高度通用的特征图,更靠近顶部的层提取的是更加抽象的概念。
因此,如果新数据集与原始模型训练的数据集有很大差异,最好只使用模型的前几层做特征提取。
4. 使用在ImageNet上训练的VGG16网络的卷积基从猫狗图像中提取特征,并在这些特征上训练一个猫狗分类器。
- VGG16 等模型内置于 Keras中,从 keras.applications模块 导入。
- 下面是该模块中的一部分图像分类模型(都是在ImageNet数据集上预训练得到的):
- Xception
- Inception V3
- RestNet50
- VGG16
- VGG19
- MobileNet
(1)将VGG16卷积基实例化
from keras.applications import VGG16
conv_base = VGG16(weights='imagenet',
include_top=False,
input_shape=(150, 150, 3))
- weights: 指定模型初始化的权重检查点;
- include_top: 指定模型最后是否要包含密集连接分类器,默认情况下,这个分类器对应于 ImageNet 的 1000 个类别。
- input_shape: 输入到网络中的图像张量的形状,如果不传入这个参数,那么网络可以处理任意形状的输入。
(2)VGG16 的详细架构
最后的特征图形状为(4, 4, 512),我们在这个特征上添加一个密集连接分类器。
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) [(None, 150, 150, 3)] 0
_________________________________________________________________
block1_conv1 (Conv2D) (None, 150, 150, 64) 1792
_________________________________________________________________
block1_conv2 (Conv2D) (None, 150, 150, 64) 36928
_________________________________________________________________
block1_pool (MaxPooling2D) (None, 75, 75, 64) 0
_________________________________________________________________
block2_conv1 (Conv2D) (None, 75, 75, 128) 73856
_________________________________________________________________
block2_conv2 (Conv2D) (None, 75, 75, 128) 147584
_________________________________________________________________
block2_pool (MaxPooling2D) (None, 37, 37, 128) 0
_________________________________________________________________
block3_conv1 (Conv2D) (None, 37, 37, 256) 295168
_________________________________________________________________
block3_conv2 (Conv2D) (None, 37, 37, 256) 590080
_________________________________________________________________
block3_conv3 (Conv2D) (None, 37, 37, 256) 590080
_________________________________________________________________
block3_pool (MaxPooling2D) (None, 18, 18, 256) 0
_________________________________________________________________
block4_conv1 (Conv2D) (None, 18, 18, 512) 1180160
_________________________________________________________________
block4_conv2 (Conv2D) (None, 18, 18, 512) 2359808
_________________________________________________________________
block4_conv3 (Conv2D) (None, 18, 18, 512) 2359808
_________________________________________________________________
block4_pool (MaxPooling2D) (None, 9, 9, 512) 0
_________________________________________________________________
block5_conv1 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv2 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv3 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_pool (MaxPooling2D) (None, 4, 4, 512) 0
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
_________________________________________________________________
(3)添加密集连接分类器
- 1)方法一:使用数据增强的快速特征提取
- 思路:在数据集上运行卷积基,将输出保存成 Numpy 数组,然后以这个数据作为输入,输入到独立的密集连接分类器中。
- 特点:因为对于每个输入图像只需运行一次卷积基,所以速度快,计算代价低,同时也无法使用数据增强。
①首先运行 ImageDataGenerator 的实例,将图像及其标签提取为Numpy数组。
import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator
base_dir = 'D:\SEU\\202211\Dataset\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')
# 将所有图像乘以 1/255 缩放
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, 2000)
validation_features, validation_labels = extract_features(validation_dir, 1000)
test_features, test_labels = extract_features(test_dir, 1000)
②目前提取的特征形状为 (samples, 4, 4, 512),我们要将其输入到密集连接分类器中,因此需要将形状展平:
train_features = np.reshape(train_features, (2000, 4 * 4 * 512))
validation_features = np.reshape(validation_features, (1000, 4 * 4 * 512))
test_features = np.reshape(test_features, (1000, 4 * 4 * 512))
③定义密集连接分类器(需要使用 dropout 正则化),并在刚刚保存的数据和标签上训练这个分类器。
from keras import models
from keras import layers
from 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))
④查看损失曲线和精度曲线。
- 验证精度达到了90%,比上一节从头开始训练的小型模型效果好得多。
- 虽然 dropout 比率相当大,但是模型几乎一开始就过拟合, 因为本方法没有使用数据增强。
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()
- 2)方法一:使用数据增强的特征提取
- 思路:在顶部添加 Dense 层来扩展已有模型(conv_base),并在输入数据上端到端地运行整个模型,这样就可以使用数据增强,因为每个输入图像在进入模型的时候都会经过卷积基。
- 特点:速度更慢,计算代价更高。
本方法代价很高,只有在 GPU 的情况下才能尝试运行。
①在卷积基上添加一个密集连接分类器。
from keras import models
from 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: "sequential_4"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
vgg16 (Functional) (None, 4, 4, 512) 14714688
_________________________________________________________________
flatten (Flatten) (None, 8192) 0
_________________________________________________________________
dense_6 (Dense) (None, 256) 2097408
_________________________________________________________________
dense_7 (Dense) (None, 1) 257
=================================================================
Total params: 16,812,353
Trainable params: 16,812,353
Non-trainable params: 0
_________________________________________________________________
③冻结卷积基
冻结一个或多个层是指 「在训练中保持其权重不变」,这样做能保证卷积基之前学到的表示不被修改。
在 Keras 中,冻结网络的方法是将其 trainable 属性 设置为 False。
如此设置之后,只有添加的两个 Dense 层的权重才会被训练。总共有 4 个权重张量,每层 2 个(主权重矩阵和偏置向量)。为了使得修改生效,必须先编译模型。
④利用冻结的卷积基端到端地训练模型
from keras.preprocessing.image import ImageDataGenerator
from 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),
batch_size=batch_size,
class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=batch_size,
class_mode='binary')
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=2e-5),
metrics=['acc'])
history = model.fit(
train_generator,
steps_per_epoch=100,
epochs=30,
validation_data=validation_generator,
validation_steps=50)
⑤查看损失曲线和精度曲线。
从图中可以看出,验证精度约为96%,比从头开始训练的小型卷积神经网络要好得多。
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()
3.2 微调模型
1.微调
微调是指将 「冻结的卷积基」的顶部几层“解冻”,并将这解冻的几层和新增加的部分(本例中是全连接分类器)联合训练。
2.步骤
- 在已经训练好的基网络上添加自定义网络;
- 冻结基网络;
- 训练所添加的部分;
- 解冻基网络的一些层;
- 联合训练解冻的这些层和添加的部分。
其中,特征提取已经完成了前三个步骤,接下来将完成剩下两步。
对于VGG16网络,我们将微调 卷积块 5 ,卷积块1 - 4 仍然冻结。
卷积块 5 如下:
block5_conv1 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv2 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv3 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_pool (MaxPooling2D) (None, 4, 4, 512) 0
=================================================================
3. 只微调卷积块 5 的原因
- 卷积基中更靠近底部的层编码的是更加通用的可复用特征,更靠近顶部的层编码的是更专业化的特征。微调专业化的特征更有用,因为它们需要在新问题上改变用途;
- 训练的参数越多,过拟合风险越大。
4.从特征提取结束的部分,继续实现此方法
(1)冻结直到某一层的所有层
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
(2)微调模型,使用学习率非常小的 RMSProp 优化器实现。之所以让学习率很小,是因为对于微调的三层表示,如果权重更新太大可能破坏这些表示。
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-5),
metrics=['acc'])
history = model.fit(
train_generator,
steps_per_epoch=100,
epochs=100,
validation_data=validation_generator,
validation_steps=50
)
(3)查看验证精度和验证损失。
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()
(4)使曲线变得光滑。
这些曲线包含噪声,为了使图像更具可读性,将每个损失和精度都替换为指数移动平均值,从而让曲线变得光滑。
从图中可以看出,精度值提高了 1%,从约 96% 提高到 97% 。
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()
(5)在测试集上评估模型。
test_generator = test_datagen.flow_from_dictionary(
test_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
test_loss, test_acc = model.evaluate(test_generator, steps=50)
print('test acc:', test_acc)
得到了 97% 的测试精度,在关于 Kaggle 数据集中,这个结果是最佳结果之一,但是利用现代深度学习技术,只用 10% 的训练数据就得到了这个结果。
3.3 小结
- 卷积神经网络是用于计算机视觉任务的最佳机器学习模型,即使在非常小的数据集上也可以从头训练一个卷积神经网络,而且得到的结果还不错。
- 在小型数据集上的主要问题是过拟合。在处理图像数据时,数据增强是一种降低过拟合的方法。
- 利用特征提取,可以很容易将现有的卷积神经网络复用于新的数据集,特别适用于小型数据集。
- 作为特征提取的补充,还可以使用微调,进一步提高模型性能。
4 卷积神经网络的可视化
三种方法:
- 可视化卷积神经网络的中间输出(中间激活),有助于理解卷积神经网络连续的层如何对输入进行变换,也有助于初步了解卷积神经网络每个过滤器的含义;
- 可视化卷积神经网络的过滤器,有助于理解卷积神经网络每个过滤器容易接受的视觉模式或视觉概念;
- 可视化图像中类激活的热力图,有助于理解图像的哪个部分被识别为属于某个类别,从而定位图像中的物体。
第一种方法将使用第 2 节在猫狗分类问题上从头开始训练的小型卷积神经网络。对于另外两种方法,将使用第 3 节介绍的 VGG16 模型。
4.1 可视化中间激活
1.定义
可视化中间激活,指对于给定输入,展示网络中各个卷积层和池化层输出的特征图(层的输出通常称为该层的激活,即激活函数的输出)。
这可以看到输入如何被分解为网络学到的不同过滤器。
在三个维度上对特征图进行可视化:宽度、高度和深度(通道)。每个通道都对应相对独立的特征,所以将特征图可视化的正确方法是 「将每个通道的内容分别绘制成二维图像」。
2.具体操作
(1)加载模型并展现架构
from keras.models import load_model
model = load_model('cats_and_dogs_small_2.h5')
model.summary()
Model: "sequential_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_8 (Conv2D) (None, 148, 148, 32) 896
_________________________________________________________________
max_pooling2d_8 (MaxPooling2 (None, 74, 74, 32) 0
_________________________________________________________________
conv2d_9 (Conv2D) (None, 72, 72, 64) 18496
_________________________________________________________________
max_pooling2d_9 (MaxPooling2 (None, 36, 36, 64) 0
_________________________________________________________________
conv2d_10 (Conv2D) (None, 34, 34, 128) 73856
_________________________________________________________________
max_pooling2d_10 (MaxPooling (None, 17, 17, 128) 0
_________________________________________________________________
conv2d_11 (Conv2D) (None, 15, 15, 128) 147584
_________________________________________________________________
max_pooling2d_11 (MaxPooling (None, 7, 7, 128) 0
_________________________________________________________________
flatten_2 (Flatten) (None, 6272) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 6272) 0
_________________________________________________________________
dense_4 (Dense) (None, 512) 3211776
_________________________________________________________________
dense_5 (Dense) (None, 1) 513
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
_________________________________________________________________
(2)预处理单张图像,使其成为 4D 张量。
# r"file":避免\xx是一个转义字符而导致的错误
img_path = r'D:\SEU\202211\Dataset\cats_and_dogs_small\test\cats\cat.1700.jpg'
from keras.preprocessing import image
import numpy as np
# 训练模型的输入数据都要使用这种方法进行预处理
img = image.load_img(img_path, target_size=(150, 150))
img_tensor = image.img_to_array(img)
img_tensor = np.expand_dims(img_tensor, axis=0)
img_tensor /= 255.
print(img_tensor.shape)
# (1, 150, 150, 3)
(3)显示测试图像
import matplotlib.pyplot as plt
plt.imshow(img_tensor[0])
plt.show()
(4)模型实例化
为了查看特征图,需要创建一个 Keras 模型,以「图像批量」作为输入,并输出所有「卷积层和池化层的激活」。
为此,需要使用 Keras 的 Model 类。模型实例化需要两个参数:一个输入张量(或输入张量的列表),一个输出张量(或输出张量的列表)。得到的类是一个 Keras 模型,和 Sequential 模型一样,将特定输入映射为特定输出。
输入一张图像,模型将有 8 个输出,每层激活对应一个输出,返回原始模型的前 8 层的激活值。
from keras import models
# 提取前8层的输出
layer_outputs = [layer.output for layer in model.layers[:8]]
# 创建一个模型,给定模型输入,返回这些输出
activation_model = models.Model(inputs=model.input, outputs=layer_outputs)
(5)以预测模式运行模型
返回 8 个 Numpy 数组组成的列表,每个层激活对应一个 Numpy 数组。
activations = activation_model.predict(img_tensor)
比如,对于输入的猫的图像,第一个卷积层的激活如下:
first_layer_activation = activations[0]
print(first_layer_activation.shape)
# (1, 148, 148, 32)
(6)将第 4 个通道可视化
这个通道看起来是 对角边缘检测器。
import matplotlib.pyplot as plt
plt.matshow(first_layer_activation[0, :, :, 4], cmap='viridis')
(7)将每个中间激活的所有通道可视化
- 「第一层」是各种边缘探测器的结合。在这一阶段,激活几乎保留了原始图像的所有信息。
- 随着「层数的加深」,激活变得越来越抽象,开始表示高层次的概念,比如“猫耳朵”和“猫眼睛”,关于类别的信息越来越多。
- 随着「层数的加深」,激活的稀疏度也增大。第一层中所有过滤器都被输入到图像激活,在后面的层里,越来越多的过滤器是空白的,输入图像找不到图像的编码模式。
深度神经网络可以作为 「信息蒸馏管道」,输入原始数据,反复变换,过滤无关信息,并放大和细化有用信息。
# 层的名称,有助于将名称放到画中
layer_names = []
for layer in model.layers[:8]:
layer_names.append(layer.name)
# 每行16个图像
images_per_row = 16
# 显示特征图
for layer_name, layer_activation in zip(layer_names, activations):
# 特征图中的特征个数,即通道数
n_features = layer_activation.shape[-1]
# 特征图形状为(1,size,size,n_features)
size = layer_activation.shape[1]
# 将激活通道平铺
n_cols = n_features // images_per_row
display_grid = np.zeros((size * n_cols, images_per_row * size))
# 将每个过滤器平铺到一个大的水平网格中
for col in range(n_cols):
for row in range(images_per_row):
channel_image = layer_activation[0, :, :, col * images_per_row + row]
# 特征处理,使其看起来更美观
channel_image -= channel_image.mean()
channel_image /= channel_image.std()
channel_image *= 64
channel_image += 128
channel_image = np.clip(channel_image, 0, 255).astype('uint8')
# 显示网格
display_grid[col * size : (col + 1) * size,
row * size : (row + 1) * size] = channel_image
scale = 1. / size
plt.figure(figsize=(scale * display_grid.shape[1],
scale * display_grid.shape[0]))
plt.title(layer_name)
plt.grid(False)
plt.imshow(display_grid, aspect='auto', cmap='viridis')
4.2 可视化卷积神经网络的过滤器
1.实现思路及方法
从空白输入图像开始,将梯度下降应用于卷积神经网络输入图像的值,让某个过滤器的响应最大化。
首先构建一个「损失函数」,让某个卷积层的某个过滤器的值最大化;然后,使用「随机梯度下降」来调节输入图像的值,让激活值最大化。
2.具体操作
(1)为过滤器的可视化定义损失张量
from keras.applications import VGG16
from keras import backend as K
model = VGG16(weights='imagenet', include_top=False)
layer_name = 'block3_conv1'
filter_index = 0
layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])
(2)获取损失相对于输入的梯度,实现梯度下降。
调用gradients返回一个张量列表(本例中列表长度为1)。因此,只保留第一个元素,它是一个张量。
grads = K.gradients(loss, model.input)[0]
(3)梯度标准化
为了让梯度下降过程顺利进行,将梯度张量除以其 L2 范数(张量中所有值的平方的平均值的平方根)来标准化,确保输入图像的更新大小始终位于相同范围。
# 做除法前加上1e-5,以防不小心除以0
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
(4)给定输入图像,计算损失张量和梯度张量的值
定义一个 Keras 后端函数来实现:iterate 函数,将一个 Numpy 张量(表示为长度为 1 的张量列表)转换为两个 Numpy 张量组成的列表,这两个张量分别是损失值和梯度值。
iterate = K.function([model.input], [loss, grads])
loss_value, grads_value = iterate([np.zeros((1, 150, 150, 3))])
(5)通过随机梯度下降让损失最大化
# 从一张带有噪声的灰度图像开始
input_img_data = np.random.random((1, 150, 150, 3)) * 20 + 128.
step = 1. # 每次梯度更新的步长
for i in range(40):
# 计算损失值和梯度值
loss_value, grads_value = iterate([input_img_data])
# 沿着让损失最大化的方向调节输入图像
input_img_data += grads_value * step
得到的图像张量是形状为 (1,150,150,3)的浮点数张量,其取值可能不是[0, 255]区间内的整数。因此,需要对这个张量进行后处理,将其转换为可显示的图像。
(6)将张量转换为有效图像的函数
def deprocess_image(x):
# 标准化,使其均值为0,标准差为0.1
x -= x.mean()
x /= (x.std() + 1e-5)
x *= 0.1
# 将x裁切clip到[0,1]区间
x += 0.5
x = np.clip(x, 0, 1)
# 将x转换为RGB数组
x *= 255
x = np.clip(x, 0, 255).astype('uint8')
return x
(7)生成过滤器可视化的函数
构建一个损失函数,将该层的第 n 个过滤器的激活最大化。
输入一个层的名称和一个过滤器索引,将返回一个有效的图像张量。
def generate_pattern(layer_name, filter_index, size=150):
layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])
# 计算损失相对于输入图像的梯度
grads = K.gradients(loss, model.input)[0]
# 梯度标准化
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
# 返回给定输入图像的损失和梯度
iterate = K.function([model.input], [loss, grads])
# 带有噪声的灰度图像
input_img_data = np.random.random((1, size, size, 3)) * 20 + 128.
# 运行40次梯度上升
step = 1.
for i in range(40):
loss_value, grads_value = iterate([input_img_data])
input_img_data += grads_value * step
img = input_img_data[0]
return deprocess_image(img)
可视化 block3_conv1层 第 0 个通道的过滤器。看起来,该过滤器响应的是波尔卡点图案。
plt.imshow(generate_pattern('block3_conv1', 0))
(8)生成某一层中所有过滤器响应模式组成的网络
将每一层的每个过滤器都可视化。为了简单起见,只查看每一层的前 64 个过滤器,并只查看每个卷积块的第一层(即block1_conv1、block2_conv1、block3_conv1、block4_conv1、block5_conv1)。
将输出放到一个 8 * 8 的网格中,每个网格是一个 64像素 * 64 像素 的过滤器模式,两个过滤器模式之间留有黑边。
随着层数的加深,卷积神经网络的过滤器变得越来越复杂,越来越精细。
layer_name = 'block2_conv1'
size = 64
margin = 5
# 空图像(全黑),用于保存结果
results = np.zeros((8 * size + 8 * margin, 8 * size + 8 * margin, 3))
# 遍历results网格的行
for i in range(8):
# 遍历results网格的列
for j in range(8):
# 生成layer_name层第i+(j * 8)个过滤器的模式
filter_img = generate_pattern(layer_name, i + (j * 8), size=size)
# 将结果放到results网格第(i,j)个方块中
horizontal_start = i * size + i * margin
horizontal_end = horizontal_start + size
vertical_start = j * size + j * margin
vertical_end = vertical_start + size
results[horizontal_start : horizontal_end,
vertical_start : vertical_end, :] = filter_img
# 显示results网格
plt.figure(figsize=(20, 20))
plt.imshow(results)
3.3 可视化类激活的热力图
1.定义
类激活图(CAM, class activation map)可视化,指对输入图像生成类激活的热力图。
「类激活热力图」是与 特定输出类别相关 的 二维分数网格, 对任何输入图像的每个位置都要计算,表示每个位置对该类别的重要程度。
2.实现方式
给定一张输入图像,对于上一个卷积层的输出特征图,用类别相对于通道的梯度对这个特征图中的每个通道进行加权。也就是说,用 每个通道对类别的重要程度 对 输入图像对不同通道的激活强度 的空间图进行加权,从而得到 输入图像对类别的激活强度 的空间图。
3.具体步骤
(1)加载带有预训练权重的VGG16网络
from keras.applications.vgg16 import VGG16
# 网络中包含密集连接分类器
# 我们之前的例子都舍弃了这个分类器
model = VGG16(weights='imagenet')
(2)为VGG模型预处理一张输入图像
将图像转换为 VGG16模型 能够读取的格式,模式在大小为 224 * 224 的图像上进行训练,根据 keras.applications.vgg16.preprocess_input 函数中内置的规则进行预处理。
from keras.preprocessing import image
from keras.applications.vgg16 import preprocess_input, decode_predictions
import numpy as np
img_path = r'D:\SEU\202211\Dataset\creative_commons_elephant.png'
# 大小为224*224的Python图像库图像
img = image.load_img(img_path, target_size=(224, 224))
# 形状为(224,224,3)的float32格式的Numpy数组
x = image.img_to_array(img)
# 添加一个维度,将数组转换为(1,224,224,3)形状的批量
x = preprocess_input(x)
(3)在图像上运行预训练的 VGG16网络,并将其预测向量解码为人类可读的格式。
网络识别出图像中包含数量不确定的非洲象。预测向量中被最大激活的元素是“非洲象”类别的元素,索引编号为386。
(4)使用 Grad-CAM算法,展示图像中哪些部分最像非洲象。
# 预测向量中“非洲象”的元素
african_elephant_output = model.output[:, 386]
# block5_conv3层的输出特征图,它是VGG16的最后一个卷积层
last_conv_layer = model.get_layer('block5_conv3')
# 非洲象类别相对于block5_conv3输出特征图的梯度
grads = K.gradients(african_elephant_output, last_conv_layer.output)[0]
# 形状为(512,)的向量,每个元素是特定特征图通道的梯度平均大小
pooled_grads = K.mean(grads, axis=(0, 1, 2))
# 访问刚刚定义的量
iterate = K.function([model.input], [pooled_grads, last_conv_layer.output[0]])
# 对于两个大象的样本图像,这两个量都是Numpy数组
pooled_grads_value, conv_layer_output_value = iterate([x])
# 将特征图数组的两个通道乘以
# “这个通道对‘大象’类别的重要程度”
for i in range(512):
conv_layer_output_value[;, ;, i] *= pooled_grads_value[i]
# 得到的特征图的通道平均值即为类激活的热力图
heatmap = np.mean(conv_layer_output_value, axis=-1)
(5)热力图后处理
heatmap = np.maximum(heatmap, 0)
heatmap /= np.max(heatmap)
plt.matshow(heatmap)
(6)将热力图与原始图像叠加
import cv2
# 用cv2加载原始图像
img = cv2.imread(img_path)
# 将热力图的大小调整为与原始图像相同
heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
# 将热力图转换为RGB格式
heatmap = np.uint(255 * heatmap)
# 将热力图应用于原始图像
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
# 0.4是热力图强度因子
superimposed_img = heatmap * 0.4 + img
# 将图像保存到硬盘
cv2.imwrite(r'D:\SEU\202211\Dataset\creative_commons_elephant.png')
这种可视化方法回答了两个重要问题:
- 网络为什么认为图像中包含一头非洲象?
- 非洲象在图像中的什么位置?