粗读《Python 深度学习》(4)

粗读《Python 深度学习》(4)

  • 第五章 深度学习用于计算机视觉
    • 5.1 卷积神经网络简介
      • 5.1.1 卷积运算
      • 5.1.2 最大池化运算
    • 5.2 在小型数据集上从头开始训练一个卷积神经网络
      • 5.2.1 深度学习与小数据问题的相关性
      • 5.2.2 下载数据
      • 5.2.3 构建网络
      • 5.2.4 数据预处理
      • 5.2.5 使用数据增强
    • 5.3 使用预训练的卷积神经网络
      • 5.3.1 特诊提取
      • 5.3.2 微调模型
      • 5.3.3 小结
    • 5.4 卷积神经网络的可视化
      • 5.4.1 可视化中间激活
      • 5.4.2 可视化卷积神经网络的过滤器
      • 5.4.3 可视化类激活的热力图
  • 小结

第五章 深度学习用于计算机视觉

5.1 卷积神经网络简介

简单的卷积神经网络由 Conv2D 层和 MaxPooling2D 层堆叠而成。卷积神经网络接收形状为 (image_height, image_width, image_channels) 的输入张量(不包括批量维度)。

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'))    # 64 通道,卷积核大小为 3×3
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))

输出张量大小为 (3, 3, 64) ,将其展平为 1D 张量,再输入到 Dense 层中进行分类。

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

查看网络架构:

>>> model.summary()
_________________________________________________________________
Layer (type)                      Output Shape          Param # 
=================================================================
conv2d_1 (Conv2D)                 (None, 26, 26, 32)    320 
_________________________________________________________________
max_pooling2d_1 (MaxPooling2D)    (None, 13, 13, 32)    0 
_________________________________________________________________
conv2d_2 (Conv2D)                 (None, 11, 11, 64)    18496 
_________________________________________________________________
max_pooling2d_2 (MaxPooling2D)    (None, 5, 5, 64)      0 
_________________________________________________________________
conv2d_3 (Conv2D)                 (None, 3, 3, 64)      36928 
_________________________________________________________________
flatten_1 (Flatten)               (None, 576)           0 
_________________________________________________________________
dense_1 (Dense)                   (None, 64)            36928 
_________________________________________________________________
dense_2 (Dense)                   (None, 10)            650 
=================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0

以 conv2d_2 层为例,当前层参数量 18496 = (32 × 3 × 3 +1) × 64。指的是,conv2d_2 层将从上一层输出的 32 个特征中组合提取 64 种特征,其中每一种特征通过 3 × 3 × 32 的 过滤器(filter) 扫描 32 层的 特征图(feature map) 输出 32 张 响应图(response map),将其叠加并加上偏置量,生成 输出特征图(output feature map)

Dense 层类似,以 dense_1 为例, 36928 = (576 + 1) × 64

5.1.1 卷积运算

密集连接层和卷积层的根本区别在于,Dense 层从输入特征空间中学到的是全局模式,而卷积层学到的是局部模式。 对于图像来说,学到的就是在输入图像的二维小窗口中发现的 模式

卷积神经网络具有以下两种突出性质:

1、卷积神经网络学到的模式具有 平移不变性(translation invariant)。卷积神经网络是通过过滤器扫描图像,利用过滤器学习到的模式进行特征识别。这意味着无论特征在图像的什么位置都可以被过滤器识别。

2、卷积神经网络可以学到模式的 空间层次结构(spatial hierarchies of patterns)。第一个卷积层将学习较小的局部模式(比如边缘),第二个卷积层将学习由第一层特征组成的更大的模式(视窗逐渐扩大),以此类推。这使得卷积神经网络可以有效地学习越来越复杂、越来越抽象的视觉概念(因为视觉世界从根本上具有空间层次结构)。随着网络加深,过滤器提取的特征越来越抽象、复杂;过滤器的视窗范围越来越大,提取特征的范围越来越广。

粗读《Python 深度学习》(4)_第1张图片

卷积由以下两个关键参数所定义:
1、从输入中提取的图块尺寸:这些图块的大小通常是 3×3 或 5×5;
2、输出特征图的深度:卷积所计算的过滤器的数量。

卷积的工作原理:在 3D 输入特征图上 滑动(slide) 这些 3×3 或 5×5 的窗口,在每个可能的位置停止并提取周围特征的 3D 图块[形状为 (window_height, window_width, input_depth)]。然后每个 3D 图块与学到的同一个权重矩阵[叫作 卷积核(convolution kernel)](3D 张量)做张量积,转换成形状为 (output_depth,) 的 1D 向量。然后对所有这些向量进行空间重组,使其转换为形状为 (height, width, output_depth) 的 3D 输出特征图。输出特征图中的每个空间位置都对应于输入特征图中的相同位置(比如输出的右下角包含了输入右下角的信息)。

粗读《Python 深度学习》(4)_第2张图片

应注意,输出特征图的宽高会因为边界效应和步幅选择而与输入特征图不同。

1、理解边界效应与填充

边界效应指的是,窗口在输入特征图上滑动时,因窗口不能越过边界,而使得输出特征图宽高缩小。以 3×3 的窗口在 5×5 的特征图上滑动为例。

粗读《Python 深度学习》(4)_第3张图片

如果想要输出特征图的空间维度与输入相同,那么可以使用 填充(padding)。填充是在输入特征图的每一边添加适当数目的行和列,使得每个输入方块都能作为卷积窗口的中心(窗口为偶数时,需要设置 锚点)。

粗读《Python 深度学习》(4)_第4张图片

对于 Conv2D 层,可以通过 padding 参数来设置填充,这个参数有两个取值:"valid" 表示不使用填充(只使用有效的窗口位置);"same" 表示“填充后输出的宽度和高度与输入相同”。padding 参数的默认值为 "valid"

2、理解卷积步幅

两个连续窗口的距离,叫作 步幅,默认值为 1。步幅大于 1 的卷积叫做 步进卷积(strided convolution)

使用步进卷积缩小输出特征图,实现特征图的下采样,但通常使用 最大池化(max-pooling) 运算来下采样。

5.1.2 最大池化运算

最大池化 是从输入特征图中提取窗口,并输出每个通道的最大值。它与卷积运算相似,同样是利用窗口在特征图上滑动,只不过不做卷积运算而是取每个窗口中的最大值。

最大池化运算可以极大地减少模型参数,降低计算机的负担。同时,可以保留特征在原特征图上的位置,起到筛选特征的作用。

除了最大池化还有 平均池化,通过取每个窗口的平均值。但效果没有最大池化好,原因在于特征中往往编码了某种模式或概念在特征图的不同位置是否存在(因此得名特征图),而观察不同特征的最大值而不是平均值能够给出更多的信息

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

5.2.1 深度学习与小数据问题的相关性

有时你会听人说,仅在有大量数据可用时,深度学习才有效。这种说法部分正确:深度学习的一个基本特性就是能够独立地在训练数据中找到有趣的特征,无须人为的特征工程,而这只在拥有大量训练样本时才能实现。对于输入样本的维度非常高(比如图像)的问题尤其如此。

但对于初学者来说,所谓 “大量” 样本是相对的,即相对于你所要训练网络的大小和深度而言。只用几十个样本训练卷积神经网络就解决一个复杂问题是不可能的,但如果模型很小,并做了很好的正则化,同时任务非常简单,那么几百个样本可能就足够了。由于卷积神经网络学到的是局部的、平移不变的特征,它对于感知问题可以高效地利用数据。虽然数据相对较少,但在非常小的图像数据集上从头开始训练一个卷积神经网络,仍然可以得到不错的结果,而且无须任何自定义的特征工程。

5.2.2 下载数据

该书从 https://www.kaggle.com/c/dogs-vs-cats/data 下载原始数据集,再将图像复制到训练、验证和测试的目录。

import os, shutil

original_dataset_dir = '/Users/fchollet/Downloads/kaggle_original_data'  # 原始数据集解压目录路径
base_dir = '/Users/fchollet/Downloads/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)

""" 将猫的图像复制到对应路径下 """
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)  # 将文件从 src 复制到 dst,dst 必须是完整的目标文件名

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)

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)

""" 将狗的图像复制到对应路径下 """
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)

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)

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)

5.2.3 构建网络

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

应注意,网络中特征图的深度在逐渐增大(从 32 增大到 128),而特征图的尺寸在逐渐减小(从150×150 减小到 7×7)。这几乎是所有卷积神经网络的模式。

2、配置模型用于训练

from keras import optimizers

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

5.2.4 数据预处理

图像数据预处理步骤包括:

1、读取图像文件;
2、将 JPEG 文件解码为 RGB 像素网格;
3、将这些像素网格转换为浮点数张量;
4、将像素值(0~255 范围内)缩放到 [0, 1] 区间(正如你所知,神经网络喜欢处理较小的输入值)。

Keras 拥有自动完成这些步骤的工具。Keras 有一个图像处理辅助工具的模块,位于 keras.preprocessing.image。特别地,它包含 ImageDataGenerator 类,可以快速创建 Python 生成器,能够将硬盘上的图像文件自动转换为预处理好的张量批量。

1、使用 ImageDataGenerator 从目录中读取图像

from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(rescale=1./255)   # 创建 ImageDataGenerator 类
test_datagen = ImageDataGenerator(rescale=1./255)    # 将图像缩小 1/255,并转换为浮点数

""" 从路径生成数据增强 """
train_generator = train_datagen.flow_from_directory(
        train_dir,                                   # 路径
        target_size=(150, 150),                      # 图像大小调整为 150×150
        batch_size=20,                               # 将数据分批存入内存
        class_mode='binary')                         # 使用二进制标签

validation_generator = test_datagen.flow_from_directory(
        validation_dir,
        target_size=(150, 150),
        batch_size=20,
        class_mode='binary')

Python 生成器(Python generator) 是一个类似于迭代器的对象,一个可以和 for ... in 运算符一起使用的对象。生成器是用 yield 运算符来构造的。

def generator(): 
    i=0
    while True:
        i += 1
        yield i

关于 yield 的理解和使用,可以参考这篇文章。

2、利用批量生成器拟合模型

history = model.fit_generator(
      train_generator,                        # 训练集数据生成器
      steps_per_epoch=100,                    # 100批
      epochs=30,                              # 10轮
      validation_data=validation_generator,   # 验证集数据生成器
      validation_steps=50)                    # 50批

3、保存模型

model.save('cats_and_dogs_small_1.h5')

4、绘制训练过程中的损失曲线和精度曲线

粗读《Python 深度学习》(4)_第5张图片

5.2.5 使用数据增强

数据增强 是从现有的训练样本中生成更多的训练数据,其方法是利用多种能够生成可信图像的随机变换来增加(augment)样本。其目标是,模型在训练时不会两次查看完全相同的图像。这让模型能够观察到数据的更多内容,从而具有更好的泛化能力。

1、利用 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')

参数含义:
1、rotation_range 是角度值(在 0~180 范围内),表示图像随机旋转的角度范围
2、width_shiftheight_shift 是图像在水平或垂直方向上平移的范围(相对于总宽
度或总高度的比例)

3、shear_range随机错切变换的角度;
4、zoom_range 是图像随机缩放的范围;
5、horizontal_flip 是随机将一半图像水平翻转,即镜像;
6、fill_mode 是用于填充新创建像素的方法,这些新像素可能来自于旋转或宽度/高度平移。

2、显示几个随机增强后的训练图像

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))  # 读取图片并调整大小
x = image.img_to_array(img)                             # 转换为(150,150,3)的 Numpy 数组
x = x.reshape((1,) + x.shape)                           # 升维,(1,150,150,3)

i = 0 
for batch in datagen.flow(x, batch_size=1):             # 将图像传入生成器中
    plt.figure(i)
    imgplot = plt.imshow(image.array_to_img(batch[0]))
    i += 1
    if i % 4 == 0:
        break                                           # 生成器循环是无限的,需要终止指令

plt.show()

3、在原有模型中全连接层前加入 Dropout 层

4、利用数据增强生成器训练卷积神经网络

5、保存模型

6、绘制训练过程中的损失曲线和精度曲线

粗读《Python 深度学习》(4)_第6张图片

可见,使用了数据增强和 dropout 之后,模型不再过拟合:训练曲线紧紧跟随着验证曲线。

5.3 使用预训练的卷积神经网络

想要将深度学习应用于小型图像数据集,一种常用且非常高效的方法是使用预训练网络。预训练网络(pretrained network) 是一个保存好的网络,之前已在大型数据集(通常是大规模图像分类任务)上训练好。如果这个原始数据集足够大且足够通用,那么预训练网络学到的特征的空间层次结构可以有效地作为视觉世界的通用模型,因此这些特征可用于各种不同的计算机视觉问题,即使这些新问题涉及的类别和原始任务完全不同

使用预训练网络有两种方法:特征提取(feature extraction)微调模型(fine-tuning)

5.3.1 特诊提取

对于卷积神经网络而言,特征提取 就是取出之前训练好的网络的卷积基(convolutional base),在上面运行新数据,然后在输出上面训练一个新的分类器。卷积基指的是一系列卷积层和池化层的部分。

一、将 VGG16 卷积基实例化

from keras.applications import VGG16

conv_base = VGG16(weights='imagenet',
                  include_top=False, 
                  input_shape=(150, 150, 3))

参数含义:
1、weights 指定模型初始化的权重检查点。
2、include_top 指定模型最后是否包含密集连接分类器。
3、input_shape 是输入到网络中的图像张量的形状。这个参数完全是可选的,如果不传
入这个参数,那么网络能够处理任意形状的输入。

之后有两种运行方法:
1、在数据集上运行卷积基,将输出保存成硬盘中的 Numpy 数组,然后用这个数据作为输入,输入到独立的密集连接分类器中。这种方法速度快,计算代价低,但不能使用数据增强。
2、在顶部添加 Dense 层来扩展已有模型(即 conv_base),并在输入数据上端到端地运行整个模型。这样可以使用数据增强,但这种方法的计算代价比第一种要高很多。

二、不使用数据增强的快速特征提取

1、使用预训练的卷积基提取特征

import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator

""" 训练、验证、测试集路径 """
base_dir = '/Users/fchollet/Downloads/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')

""" 定义 ImageDataGenerator 实例 """
datagen = ImageDataGenerator(rescale=1./255)   # 图片缩小 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)

""" 将特征输出展平为 1D 张量 """
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))

2、定义并训练密集连接分类器

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

3、绘制结果

粗读《Python 深度学习》(4)_第7张图片

虽然 dropout 比率相当大,但模型几乎从一开始就过拟合。这是因为本方法没有使用数据增强,而数据增强对防止小型图像数据集的过拟合非常重要。

三、使用数据增强的特征提取

1、在卷积基上添加一个密集连接分类器

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

在编译和训练模型之前,一定要 “冻结” 卷积基。冻结(freeze) 一个或多个层是指在训练过程中保持其权重不变。如果不这么做,那么卷积基之前学到的表示将会在训练过程中被修改。

在 Keras 中,冻结网络的方法是将其 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

应注意,为了让这些修改生效,必须先编译模型。如果在编译之后修改了权重的 trainable 属性,那么应该重新编译模型,否则这些修改将被忽略。

2、利用冻结的卷积基端到端地训练模型

3、绘制结果

粗读《Python 深度学习》(4)_第8张图片

略优于前一个结果。

5.3.2 微调模型

微调 是指将其深层的几层 “解冻” ,并将这解冻的几层和新增加的部分(全连接分类器)联合训练。之所以叫作微调,是因为它只是略微调整了所复用模型中更加抽象的表示,以便让这些表示与手头的问题更加相关

上节说过,冻结 VGG16 的卷积基是为了能够在上面训练一个随机初始化的分类器。同理,只有上面的分类器已经训练好了,才能微调卷积基的顶部几层。如果分类器没有训练好,那么训练期间通过网络传播的误差信号会特别大,微调的几层之前学到的表示都会被破坏。因此,微调网络的步骤如下:
(1) 在已经训练好的 基网络(base network) 上添加自定义网络。
(2) 冻结基网络。
(3) 训练所添加的部分。
(4) 解冻基网络的一些层。
(5) 联合训练解冻的这些层和添加的部分。

做特征提取时已经完成了前三个步骤,从第四步继续。

1、冻结直到某一层的所有层

微调模型时,选择层数应该考虑到:
1、卷积基中浅层记忆的是更加通用的可复用特征,而深层记忆的是更专业化的特征。
2、训练的参数越多,过拟合的风险越大。

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、微调模型

model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=1e-5),    # 选较小的学习率
              metrics=['acc'])

history = model.fit_generator(
      train_generator,
      steps_per_epoch=100,
      epochs=100,
      validation_data=validation_generator,
      validation_steps=50)

5.3.3 小结

1、卷积神经网络是用于计算机视觉任务的最佳机器学习模型。
2、在小型数据集上的主要问题是过拟合。在处理图像数据时,数据增强是一种降低过拟合的强大方法。
3、利用特征提取,可以很容易将现有的卷积神经网络复用于新的数据集。
4、作为特征提取的补充,可以使用微调进一步提高模型性能。

5.4 卷积神经网络的可视化

深度学习模型像一个 “黑盒” ,即模型学到的表示很难用人类可以理解的方式来提取和呈现。但卷积神经网络学到的表示非常适合可视化,很大程度上是因为它们是**视觉概念
的表示。**该书介绍了三种方法:
1、可视化卷积神经网络的中间输出(中间激活): 有助于理解卷积神经网络连续的层如何对输入进行变换,也有助于初步了解卷积神经网络每个过滤器的含义。
2、可视化卷积神经网络的过滤器: 有助于精确理解卷积神经网络中每个过滤器容易接受的视觉模式或视觉概念。
3、可视化图像中类激活的热力图: 有助于理解图像的哪个部分被识别为属于某个类别,从而可以定位图像中的物体。

5.4.1 可视化中间激活

可视化中间激活,是指对于给定输入,展示网络中各个卷积层和池化层输出的特征图(层的输出通常被称为该层的 激活,即激活函数的输出)。

1、加载模型

from keras.models import load_model

model = load_model('cats_and_dogs_small_2.h5')  # 5.2节保存的模型

2、预处理单张图像

img_path = '/Users/fchollet/Downloads/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)                   # (150, 150, 3)
img_tensor = np.expand_dims(img_tensor, axis=0)        # 扩展数组 (1, 150, 150, 3)
img_tensor /= 255.                                     # 缩小数据,并化为浮点数

3、模型实例化

创建一个 Model 类的实例,以图像批量作为输入,所有卷积层和池化层的激活为输出。与 Sequential 模型不同,Model 类允许模型有多个输出

from keras import models

layer_outputs = [layer.output for layer in model.layers[:8]]  # 此前加载的模型的前八层激活
activation_model = models.Model(inputs=model.input, outputs=layer_outputs)  # 创建 Model 实例

4、以预测模式运行模型

activations = activation_model.predict(img_tensor)  # 返回一个由 8个4D张量组成的列表

5、可视化一个通道

import matplotlib.pyplot as plt

first_layer_activation = activations[0]    # 第一层激活
plt.matshow(first_layer_activation[0, :, :, 4], cmap='viridis')  # 2D矩阵可视化,'viridis'为绿色

6、将每个中间激活的所有通道可视化

""" 记录每层的名字 """
layer_names = [] 
for layer in model.layers[:8]:
    layer_names.append(layer.name)

images_per_row = 16   # 一行16张

""" 将每层激活图像平铺 """
for layer_name, layer_activation in zip(layer_names, activations):  # 层名和激活结果相对应,打包为元组
    n_features = layer_activation.shape[-1]    # 该层激活的特征数量
    size = layer_activation.shape[1]           # 特征图形状为(1,size,size,n_feature)
    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') # 截取数组,使数组在0~255取值
            """ 将图像填入到零矩阵中 """
            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') 

粗读《Python 深度学习》(4)_第9张图片

可以看出:
1、第一层是各种边缘探测器的集合。在这一阶段,激活几乎保留了原始图像中的所有信息。
2、随着层数的加深,激活变得越来越抽象,并且越来越难以直观地理解。层数越深,其表示中关于图像视觉内容的信息就越少,而关于类别的信息就越多
3、激活的 稀疏度(sparsity) 随着层数的加深而增大。在第一层里,所有过滤器都被输入图像激活,但在后面的层里,越来越多的过滤器是空白的。也就是说,输入图像中找不到这些过滤器所编码的模式

深度神经网络可以有效地作为 信息蒸馏管道(information distillation pipeline),输入原始数据(本例中是 RGB 图像),反复对其进行变换,将无关信息过滤掉(比如图像的具体外观),并放大和细化有用的信息(比如图像的类别)。

5.4.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、获取损失相对于输入的梯度

grads = K.gradients(loss, model.input)[0]  # 返回梯度张量列表,取第一个元素,4D

K.gradient 的用法可以参照这篇博客。

3、梯度标准化

""" 除以其 L2 范数 """
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)  # 做除法前加上 1e–5,以防不小心除以 0

4、定义迭代函数

iterate = K.function([model.input], [loss, grads])  # 定义函数 iterate,给定输入返回损失和梯度

import numpy as np
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):          # 迭代40次
    loss_value, grads_value = iterate([input_img_data]) 
    input_img_data += grads_value * step             # 沿梯度上升方向,使得响应最大化

6、将张量转换为有效图像

def deprocess_image(x):
    """ 标准化,均值为0,标准差为0.1 """
    x -= x.mean() 
    x /= (x.std() + 1e-5)
    x *= 0.1
    """ 裁切x至区间[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、整合1~6,创建过滤器可视化函数

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. 
    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]          # 降维,3D数组
    return deprocess_image(img)      # 调用RGB数组转换函数

8、生成某一层中所有过滤器响应模式组成的网格

layer_name = 'block1_conv1'
size = 64        # 一格的大小
margin = 5       # 网格宽度

results = np.zeros((8 * size + 7 * margin, 8 * size + 7 * margin, 3))   # RGB零矩阵

""" 遍历该层所有过滤器 """
for i in range(8):       # 遍历行
    for j in range(8):   # 遍历列
        """ 生成过滤器响应图 """
        filter_img = generate_pattern(layer_name, j + (i * 8), size=size)
        
        """ 计算图像在零矩阵的放置坐标 """
        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

plt.figure(figsize=(20, 20)) 
plt.imshow(results)

下图为 block1_conv1 层的过滤器模式:

粗读《Python 深度学习》(4)_第10张图片

下图为 block4_conv1 层的过滤器模式:

粗读《Python 深度学习》(4)_第11张图片

可以看出:
1、模型第一层(block1_conv1)的过滤器对应简单的方向边缘颜色(还有一些是彩色边缘);
2、模型第四层(block4_conv1)的过滤器类似于自然图像中的纹理:羽毛、眼睛、树叶等;
3、随着模型层数的加深,过滤器识别的模式(包括纹理和颜色的组合)更加复杂。

5.4.3 可视化类激活的热力图

最后一种可视化方法叫作:类激活图(CAM,class activation map)可视化,它是指对输入图像生成类激活的 热力图。换个说法就是,一张图片各个部分较于给定种类的相似程度。

该书介绍的方法是论文 “Grad-CAM: visual explanations from deep networks via gradient-based localization” 中描述的方法。具体思路是:给定一张输入图像,对于一个卷积层的输出特征图,用类别相对于通道的梯度对这个特征图中的每个通道进行加权。可以理解为:用 “每个通道对类别的重要程度” 对 “输入图像对不同通道的激活强度” 的空间图进行加权,从而得到了 “输入图像对类别的激活强度” 的空间图(图像各个部分对类别判定的贡献、激活强度)。

1、加载带有预训练权重的 VGG16 网络

from keras.applications.vgg16 import VGG16

model = VGG16(weights='imagenet')       # 带有全连接层的完整VGG16模型

2、预处理一张输入图像

from keras.preprocessing import image
from keras.applications.vgg16 import preprocess_input, decode_predictions
import numpy as np

img_path = '/Users/fchollet/Downloads/creative_commons_elephant.jpg' 

img = image.load_img(img_path, target_size=(224, 224)) 
x = image.img_to_array(img)        # 形状为 (224, 224, 3) 的float32格式的 Numpy数组
x = np.expand_dims(x, axis=0)      # 升维度,形状为 (1,224, 224, 3)
x = preprocess_input(x)            # 对批量进行预处理(按通道进行颜色标准化)

查看预测结果:

>>> preds = model.predict(x)
>>> print('Predicted:', decode_predictions(preds, top=3)[0])
Predicted:', [(u'n02504458', u'African_elephant', 0.92546833),
(u'n01871265', u'tusker', 0.070257246),
(u'n02504013', u'Indian_elephant', 0.0042589349)]

使用 decode_predictions() 对预测向量解码,top=3 表示输出得分前三的预测结果。

获取第一名的索引:

>>> np.argmax(preds[0])
386

3、应用 Grad-CAM 算法

african_elephant_output = model.output[:, 386]     # 预测向量是one-hot编码,为(samples, categories)

last_conv_layer = model.get_layer('block5_conv3') 

""" 获取最后一层输出结果对非洲象判断的影响程度 """
grads = K.gradients(african_elephant_output, last_conv_layer.output)[0]   # (1,size,size,features)

""" 获取最后一层识别的512个特征对非洲象判断的影响程度 """
pooled_grads = K.mean(grads, axis=(0, 1, 2))     # 对 0~2轴压缩,保留 3轴(特征轴)

""" 创建迭代函数 """
iterate = K.function([model.input],
                     [pooled_grads, last_conv_layer.output[0]])  # 输出结果为(1D,3D)

pooled_grads_value, conv_layer_output_value = iterate([x]) 

""" 遍历最后一层输出结果的512个特征 """
for i in range(512): 
    conv_layer_output_value[:, :, i] *= pooled_grads_value[i]

heatmap = np.mean(conv_layer_output_value, axis=-1)    # 沿特征轴压缩,输出2D热力图

4、热力图后处理

heatmap = np.maximum(heatmap, 0)   # 逐位比较,去除负值
heatmap /= np.max(heatmap)         # 标准化,[0, 1]
plt.matshow(heatmap)               # 矩阵可视化

粗读《Python 深度学习》(4)_第12张图片

5、将热力图与原始图像叠加

import cv2

img = cv2.imread(img_path)     # 加载图片

heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))  # 按原图调整热力图形状,(宽,高)

heatmap = np.uint8(255 * heatmap)                        # 转换[0, 255]中的整数
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)   # 创建伪色彩,第二个参数为模式
superimposed_img = heatmap * 0.4 + img                   # 0.4是热力图强度因子
cv2.imwrite('/Users/fchollet/Downloads/elephant_cam.jpg', superimposed_img)  # 保存图片

cv2.applyColorMap() 的使用可参照这篇博客。

粗读《Python 深度学习》(4)_第13张图片

小结

1、卷积神经网络是解决视觉分类问题的最佳工具;
2、卷积神经网络通过学习 模块化模式概念的层次结构 来表示视觉世界;
3、在小型数据集上从头开始训练一个卷积神经网络,可以使用 数据增强 来防止过拟合;
4、使用 预训练 的卷积神经网络进行 特征提取模型微调,以此提高模型的识别精度;
5、过滤器可视化类激活热力图可视化

你可能感兴趣的:(#,《Python,深度学习》,卷积神经网络)