首先通过一个简单的卷积神经网络示例,对MNIST数字进行分类。
# 实例化一个小型的卷积神经网络
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
_________________________________________________________________
"""
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
_________________________________________________________________
"""
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.密集连接层和卷积层的区别
2.卷积神经网络的两个性质
3.特征图
4.卷积的两个关键参数
Keras——Conv2D(output_depth,window_height,window_width)
5. 卷积的工作原理
6. 边界效应与填充
7. 卷积步幅
1.作用
最大池化从输入特征图中提取窗口,并输出每个通道的最大值,能够对特征图进行下采样。
2.下采样
本节将会介绍一个小型数据集的实例,数据集中包含4000张猫和狗的图像(2000张猫的图像,2000张狗的图像),将2000张图像用于训练,1000张用于验证,1000张用于测试。
本节将介绍如何使用少量数据从头训练一个新模型。
首先,在2000个样本上训练一个简单的小型卷积神经网络,不做任何正则化,为模型目标设定一个基准,这会得到71%的分类精度,此时会出现过拟合;
然后介绍数据增强,降低过拟合。
下载地址:猫狗分类数据集
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
由于本例要处理的是更大的图像和更复杂的问题,因此在上一个例子(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'])
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 。
因为训练样本较少,所以容易达到过拟合。
接下来将介绍这一种针对于计算机视觉领域的新方法, 数据增强。
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')
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()
1. 预训练网络: 已经在大型数据集(通常是大规模图像分类任务)上训练好的网络。
2. 预训练网络的两种方法
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)添加密集连接分类器
①首先运行 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))
④查看损失曲线和精度曲线。
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()
本方法代价很高,只有在 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()
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% 的训练数据就得到了这个结果。
三种方法:
第一种方法将使用第 2 节在猫狗分类问题上从头开始训练的小型卷积神经网络。对于另外两种方法,将使用第 3 节介绍的 VGG16 模型。
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')
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)
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')
这种可视化方法回答了两个重要问题: