Компьютерное зрение для глубокого обучения Python (часть 2)

глубокое обучение Python

Это пятый день моего участия в августовском испытании обновлений, подробности о мероприятии:Испытание августовского обновления

Эта статья является продолжением предыдущих двух статей:

"Компьютерное зрение для глубокого обучения Python (часть 1)

"Компьютерное зрение для глубокого обучения Python (средний уровень)

Визуализация сверточных нейронных сетей

5.4 Visualizing what convnets learn

Мы часто говорим, что глубокое обучение — это черный ящик, и нам трудно извлечь удобочитаемые представления из его процесса обучения. Однако это не относится к сверточной нейронной сети для компьютерного зрения, сверточная нейронная сеть может быть визуализирована, и мы можем ее понять, потому что сверточная нейронная сеть изначально предназначена для извлечения «представления визуальных понятий».

Существует много разных способов визуализации сверточных нейронных сетей с разных точек зрения и понимания их смысла. Некоторые из них описаны ниже.

Визуализируйте промежуточную активацию

Визуализируйте промежуточную активацию(Визуализация промежуточных активаций), то есть визуализация промежуточных выходов сверточных нейронных сетей. Эта визуализация может помочь нам понять, как ряд последовательных мультисверточных слоев преобразует входные данные, и основное значение каждого фильтра.

На самом деле это предназначено для отображения карт объектов, выводимых слоем свертки/объединения (выходные данные слоя также можно назвать активацией). Конкретный метод заключается в отображении каждого выходного канала в виде двухмерного изображения.

В качестве примера возьмем ранее сгенерированную модель:

from tensorflow.keras.models import load_model

model = load_model('/CDFMLR/dataset/dogs-vs-cats/cats_and_dogs_small_2.h5')
model.summary()
Model: "sequential_5"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_20 (Conv2D)           (None, 148, 148, 32)      896       
_________________________________________________________________
max_pooling2d_20 (MaxPooling (None, 74, 74, 32)        0         
_________________________________________________________________
conv2d_21 (Conv2D)           (None, 72, 72, 64)        18496     
_________________________________________________________________
max_pooling2d_21 (MaxPooling (None, 36, 36, 64)        0         
_________________________________________________________________
conv2d_22 (Conv2D)           (None, 34, 34, 128)       73856     
_________________________________________________________________
max_pooling2d_22 (MaxPooling (None, 17, 17, 128)       0         
_________________________________________________________________
conv2d_23 (Conv2D)           (None, 15, 15, 128)       147584    
_________________________________________________________________
max_pooling2d_23 (MaxPooling (None, 7, 7, 128)         0         
_________________________________________________________________
flatten_5 (Flatten)          (None, 6272)              0         
_________________________________________________________________
dropout (Dropout)            (None, 6272)              0         
_________________________________________________________________
dense_10 (Dense)             (None, 512)               3211776   
_________________________________________________________________
dense_11 (Dense)             (None, 1)                 513       
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
_________________________________________________________________

Затем находим картинку, которую сеть не видела во время обучения:

img_path = '/Volumes/WD/Files/dataset/dogs-vs-cats/cats_and_dogs_small/test/cats/cat.1750.jpg'

from tensorflow.keras.preprocessing import image    # 把图片搞成 4D 张量
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)

отображать изображение:

import matplotlib.pyplot as plt

plt.imshow(img_tensor[0])
plt.show()

png

Чтобы извлечь карты объектов, создайте экземпляр модели с входным тензором и списком выходных тензоров:

from tensorflow.keras import models

layer_outputs = [layer.output for layer in model.layers[:8]]    # 提取前 8 层的输出
activation_model = models.Model(inputs=model.input, outputs=layer_outputs)

При вводе изображения модель возвращает активации первых 8 слоев исходной модели. Все наши предыдущие модели давали входные данные и возвращали выходные данные, но на самом деле модель может давать любые входные данные и возвращать любые выходные данные.

Затем введите изображение, которое вы только что нашли:

activations = activation_model.predict(img_tensor)
# 返回 8 个 Numpy 数组组成的列表,每层一个,里面放着激活
first_layer_activation = activations[0]
print(first_layer_activation.shape)
(1, 148, 148, 32)

У этой штуки 32 канала, давайте просто напечатаем один и посмотрим:

# 将第 4 个 channel 可视化

import matplotlib.pyplot as plt
plt.matshow(first_layer_activation[0, :, :, 4], cmap='viridis')

png

Что такое основы каждого канала случайно, я отличаюсь от книги.

Далее нарисуем полную визуализацию всех активаций во всей сети. Мы рисуем 8 карт характеристик, каждая со всеми каналами, и размещаем их на большом изображении:

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

layer_names = []
for layer in model.layers[:8]:
    layer_names.append(layer.name)
    
images_per_row = 16

for layer_name, layer_activation in zip(layer_names, activations):
    # 对于每个 layer_activation,其形状为 (1, size, size, n_features)
    n_features = layer_activation.shape[-1]    # 特征图里特征(channel)的个数
    
    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')

png

png

png

png

png

png

png

png

Как видите, функции становятся все более и более абстрактными. При этом черных становится все больше (разреженности все больше), это закодированные этими фильтрами паттерны, которых нельзя найти на входном изображении, поэтому они пустые.

Вот важная общая черта: по мере увеличения количества слоев извлекаемые слоями признаки становятся все более и более абстрактными. Активации более высокого уровня содержат все меньше и меньше информации о конкретном входе и все больше информации о цели.

Подобно тому, как люди и животные воспринимают мир: понаблюдав за сценой в течение нескольких секунд, люди могут вспомнить, какие абстрактные объекты (например, велосипеды, деревья) в ней находятся, но не конкретный вид этих объектов. Ваш мозг автоматически полностью абстрагирует визуальный ввод, то есть преобразует его в визуальные концепции более высокого уровня, отфильтровывая ненужные визуальные детали.

Глубокие нейронные сети используют конвейер обработки информации, который принимает входные необработанные данные (в данном случае RGB-изображение), итеративно преобразует их, отфильтровывает ненужную информацию (например, конкретный внешний вид изображения), а также усиливает и уточняет полезную информацию. например, категория изображения) и, наконец, завершить использование информации (например, решить, является ли изображение кошкой или собакой).

Визуализация фильтров сверточных нейронных сетей

Визуализация фильтров сверточных нейронных сетей(Визуализация фильтров convnets), чтобы помочь понять, какие визуальные шаблоны (шаблон, я думаю, что слово «шаблон» не является хорошей интерпретацией значения шаблона) или концепции, которые хорошо воспринимает каждый фильтр.

Чтобы наблюдать за фильтрами, изученными сверточной нейронной сетью, мы можем отобразить визуальные паттерны, на которые реагирует каждый фильтр. Это можно сделать с помощью градиентного восхождения на входном пространстве:

Чтобы максимизировать отклик определенного фильтра, начиная с пустого входного изображения, можно применить градиентный спуск к значениям входного изображения сверточной нейронной сети. Результирующее входное изображение является изображением с наибольшим откликом на фильтр.

Это легко сделать, построив функцию потерь, целью которой является максимизация значения фильтра в сверточном слое. Затем используйте стохастический градиентный спуск, чтобы настроить значение входного изображения, чтобы максимизировать значение активации.

Например, потери для 0-й активации фильтра слоя block3_conv1 сети VGG16, предварительно обученной на ImageNet, составляют:

from tensorflow.keras.applications import VGG16
from tensorflow.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])

print(loss)
Tensor("Mean_11:0", shape=(), dtype=float32)

Чтобы сделать градиентный спуск, чтобы получить градиент потерь относительно входных данных модели. Сделайте это с помощью функции градиентов бэкэнда Keras:

import tensorflow as tf
tf.compat.v1.disable_eager_execution()  # See https://github.com/tensorflow/tensorflow/issues/33135

grads = K.gradients(loss, model.input)[0]
# 这东西返回的是一个装着一系列张量的 list,在此处,list 长度为1,所以取 [0] 就是一个张量
print(grads)
Tensor("gradients_1/block1_conv1_4/Conv2D_grad/Conv2DBackpropInput:0", shape=(None, None, None, 3), dtype=float32)

Вот небольшой трюк, чтобы сделать процесс спуска градиента плавно работать: нормализовать тензор градиента, разделив его по сравнению с его нормой L2 (квадратный корень среднего значения квадратов тензоров). Эта операция сохраняет обновленный размер входного изображения в том же диапазоне:

grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)    # + 1e-5 防止除以0
print(grads)
Tensor("truediv_1:0", shape=(None, None, None, 3), dtype=float32)

Проблема, к которой нужно подойти сейчас, состоит в том, чтобы, учитывая входное изображение, вычислить значение потери и градации. Это можно сделать с помощью функции iterate: она преобразует тензор Numpy в список из двух тензоров Numpy, значений потерь и градиента:

iterate = K.function([model.input], [loss, grads])

import numpy as np
loss_value, grads_value = iterate([np.zeros((1, 150, 150, 3))])
print(loss_value, grads_value)
0.0 [[[[0. 0. 0.]
   [0. 0. 0.]
   [0. 0. 0.]
   ...
   ...
   [0. 0. 0.]
   [0. 0. 0.]
   [0. 0. 0.]]]]

Затем напишем градиентный спуск:

# 通过随机梯度下降让损失最大化

input_img_data = np.random.random((1, 150, 150, 3)) * 20 + 128.   # 随便一个有燥点的灰度图

plt.imshow(input_img_data[0, :, :, :] / 225.)
plt.figure()

step = 1.    # 梯度更新的步长
for i in range(40):
    loss_value, grads_value = iterate([input_img_data])
    input_img_data += grads_value * step    # 沿着让损失最大化的方向调节输入图像
    
plt.imshow(input_img_data[0, :, :, :] / 225.)
plt.show()
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).

png

png

Эммм, я просто случайно написал этот рисунок, поэтому я сделал предупреждение.Чтобы нарисовать рисунок формально, давайте позаботимся об этом ниже:

# 将张量转换为有效图像的 utility 函数

def deprocess_image(x):
    # 标准化,均值为 0, 标准差为 0.1
    x -= x.mean()
    x /= (x.std() + 1e-5)
    x *= 0.1
    
    # 裁剪到 [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

Объединение всего вышеперечисленного дает полную функцию для создания визуализаций фильтра:

# 生成过滤器可视化的函数

import tensorflow as tf
tf.compat.v1.disable_eager_execution()  # See https://github.com/tensorflow/tensorflow/issues/33135

def generate_pattern(layer_name, filter_index, size=150):
    # 构建一个损失函数,将 layer_name 层第 filter_index 个过滤器的激活最大化
    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])
    
    # 从带有噪声的灰度图开始,梯度上升 40 次
    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]
    return deprocess_image(img)

Затем его можно использовать, или только что приведенный пример |:

plt.imshow(generate_pattern('block3_conv1', 1))

png

Этот график является откликом 0-го фильтра слоя block3_conv1. Этот узор называется горошек.

Далее мы визуализируем каждый фильтр каждого слоя. Чтобы быть быстрее, мы визуализируем только первые 64 фильтра первого слоя каждого сверточного блока (предотвращать это не имеет смысла, просто посмотрите, просто сделайте несколько):

# 生成一层中所有过滤器响应模式组成的网格
def generate_patterns_in_layer(layer_name, size=64, margin=5):
    results = np.zeros((8 * size + 7 * margin, 8 * size + 7 * margin, 3), dtype=np.int)
    for i in range(8):
        for j in range(8):
            filter_img = generate_pattern(layer_name, i + (j * 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
            
    return results


layer_names = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1']
for layer in layer_names:
    img = generate_patterns_in_layer(layer)
    plt.figure(figsize=(20, 20))
    plt.title(layer)
    plt.imshow(img)

png

png

png

png

На этих рисунках показано, как слои сверточной нейронной сети наблюдают за информацией на картинке: каждый слой сверточной нейронной сети изучает набор фильтров, а затем вход представляется в виде комбинации фильтров, что на самом деле похоже на Процесс преобразования Фурье. По мере увеличения количества слоев фильтры в сверточной нейронной сети становятся все более сложными и тонкими, поэтому появляется возможность извлекать и «понимать» абстрактную информацию.

Визуализируйте тепловую карту активаций класса на изображении

Визуализируйте тепловую карту активаций класса на изображении(Визуализация тепловых карт активации класса на изображении), который используется для понимания того, на какую часть изображения полагается сеть для идентификации класса, а также помогает узнать, где находится объект на изображении.

Метод, используемый для создания тепловых карт активаций классов для входных изображений, называется визуализацией CAM (карта активации классов). Тепловая карта активации класса рассчитывается для каждого местоположения любого входного изображения, указывая, насколько важно каждое местоположение для этого класса. Например, в нашей сети классификации кошек и собак тепловая карта активации класса генерируется для изображения кошки, и мы можем получить, насколько разные части изображения напоминают кошек (это играет чрезмерную роль в модели, думая, что это фото кота).

В частности, мы используем [Град-CAM](arxiv.org/abs/1610.02391.) Этот метод: при заданном входном изображении для выходной карты объектов сверточного слоя взвешивайте каждый канал в этой карте объектов с помощью градиента класса по отношению к каналу. Говоря по-человечески, нужно использовать «важность каждого канала для классификации» для взвешивания пространственной карты «интенсивности активации входного изображения для разных каналов», чтобы получить пространственную карту «интенсивности активации входного изображения». к классификации». (эммм, такое сверхдлинное предложение лучше читать в исходном тексте ?: Интуитивно, один из способов понять этот трюк состоит в том, что вы взвешиваете пространственную карту того, «насколько интенсивно входное изображение активирует различные каналы», по «насколько важно каждый канал относится к классу», в результате чего получается пространственная карта того, «насколько интенсивно входное изображение активирует класс».)

Мы демонстрируем этот метод в модели VGG16:

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

from tensorflow.keras.applications.vgg16 import VGG16

model = VGG16(weights='imagenet')    # 注意这个是带有分类器的,比较大,下载稍慢(有500+MB)
model.summary()
Model: "vgg16"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, 224, 224, 3)]     0         
_________________________________________________________________
... (篇幅限制,中间这些就省略了)
_________________________________________________________________
fc2 (Dense)                  (None, 4096)              16781312  
_________________________________________________________________
predictions (Dense)          (None, 1000)              4097000   
=================================================================
Total params: 138,357,544
Trainable params: 138,357,544
Non-trainable params: 0
_________________________________________________________________

Затем делаем снимок для проверки:

creative_commons_elephant

(Изображение взято из «Глубокого обучения с помощью Python» (второе издание, Франсуа Шолле), извините за его использование без разрешения, нарушение и удаление)

Вот два азиатских слона ? О, переработайте это изображение в то, что нужно модели VGG16:

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

img_path = './creative_commons_elephant.jpg'
img = image.load_img(img_path, target_size=(224, 224))

img = image.load_img(img_path, target_size=(224, 224))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)

Угадайте, что изображено на картинке:

preds = model.predict(x)
print('Predicted:', decode_predictions(preds, top=3)[0])

Выходной результат:

Predicted: [('n02504458', 'African_elephant', 0.909421),
            ('n01871265', 'tusker', 0.086182885), 
            ('n02504013', 'Indian_elephant', 0.0043545826)]

На 90% уверен, что это азиатский слон, неплохой. Следующий шаг — использовать алгоритм Grad-CAM, чтобы показать, какие части изображения больше всего напоминают африканских слонов.

from tensorflow.keras import backend as K

import tensorflow as tf
tf.compat.v1.disable_eager_execution()  # See https://github.com/tensorflow/tensorflow/issues/33135

african_elephant_output = model.output[:, 386]    # 这个是输出向量中代表“非洲象”的元素

last_conv_layer = model.get_layer('block5_conv3')   # VGG16 最后一个卷积层的输出特征图

grads = K.gradients(african_elephant_output, last_conv_layer.output)[0]

pooled_grads = K.mean(grads, axis=(0, 1, 2))    # 特定特征图通道的梯度平均大小,形状为 (512,) 

iterate = K.function([model.input], [pooled_grads, last_conv_layer.output[0]])

pooled_grads_value, conv_layer_output_value = iterate([x])    # 对刚才的测试?图片计算出梯度和特征图

for i in range(512):
    # 将特征图数组的每个 channel 乘以「这个 channel 对‘大象’类别的重要程度」
    conv_layer_output_value[:, :, i] *= pooled_grads_value[i]
    
heatmap = np.mean(conv_layer_output_value, axis=-1)  # 处理后的特征图的逐通道平均值即为类激活的热力图

Нарисуй и увидишь:

import matplotlib.pyplot as plt

heatmap = np.maximum(heatmap, 0)
heatmap /= np.max(heatmap)
plt.matshow(heatmap)

png

Эммм, я не понимаю, поэтому давайте используем OpenCV, чтобы наложить это на исходное изображение:

import cv2

img = cv2.imread(img_path)    # 加载原图
heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))    # 调整热力图大小,符合原图
heatmap = np.uint8(255 * heatmap)    # 转换为 RGB 格式
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
superimposed_img = heatmap * 0.4 + img    # 叠加
cv2.imwrite('./elephant_cam.jpg', superimposed_img)    # 保存

Полученное изображение выглядит следующим образом:

叠加到原图的热力图

Видно, что сеть VGG16 на самом деле распознала только маленького слона.Обратите внимание, что интенсивность активации головы маленького слона очень высока, что может быть уникальностью африканского слона, обнаруженного сетью.