利用python一層一層可視化卷積神經網絡,以ResNet50爲例

引言

一直以來,卷積神經網絡對人們來說都是一個黑箱,我們只知道它識別圖片準確率很驚人,但是具體是怎麼做到的,它究竟使用了什麼特徵來分辨圖像,我們一無所知。無數的學者、研究人員都想弄清楚CNN內部運作的機制,甚至試圖找到卷積神經網絡和生物神經網絡的聯繫。2013年,紐約大學的Matthew Zeiler和Rob Fergus的論文Visualizing and Understanding Convolutional Neural Networks用可視化的方法揭示了CNN的每一層識別出了什麼特徵,也揭開了CNN內部的神祕面紗。之後,也有越來越多的學者使用各種方法將CNN的每一層的激活值、filters等等可視化,讓我們從各個方面瞭解到CNN內部的祕密。

分爲兩部分

  • 可視化卷積神經網絡提取的特徵
  • 可視化卷積神經網的

卷積神經網絡提取的圖像是什麼特徵

我們以ResNet50爲例對每個卷積層提取的特徵進行可視化。

首先讀取網絡結構和預訓練參數:
from keras.applications.resnet50 import ResNet50
model = ResNet50(weights=None,
                 include_top=False,)
model.load_weights('resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5')
model.summary()
接下來讀取一張圖片,這裏是以Hinton大佬圖片爲目標進行提取特徵
from keras.preprocessing import image
import numpy as np
img_path = 'Hinton.jpg'
img = image.load_img(img_path, target_size=(500, 333))
img_tensor = image.img_to_array(img)
img_tensor = np.expand_dims(img_tensor, axis=0)
# Remember that the model was trained on inputs
# that were preprocessed in the following way:
img_tensor /= 255.

# Its shape is (1, 500, 333, 3)
print(img_tensor.shape)

結果:

(1, 500, 333, 3)

(1, 500, 333, 3)所代表的的含義是:

  • 1,代表輸入圖片的個數,我們這裏只輸入了一個圖片,所以是1;
  • 500, 333,代表圖片的大小;
  • 3,代表該層有多少個filters(或者是通道數)。 所以,相當於我們的這一層輸出了64張單通道圖片。
圖像展示
import matplotlib.pyplot as plt

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

在這裏插入圖片描述

獲取ReSNet的層的輸出
from keras import models
# 提取2-50層的特徵
layer_outputs = [layer.output for layer in model.layers[2:51]]
# Creates a model that will return these outputs, given the model input:
activation_model = models.Model(inputs=model.input, outputs=layer_outputs)

爲了提取要查看的特徵圖,我們將創建一個Keras模型,該模型將成批圖像作爲輸入,並輸出所有卷積和池化層的激活。 爲此,我們將使用Keras類模型。 使用兩個參數實例化一個Model:輸入張量(或輸入張量的列表)和輸出張量(或輸出張量的列表)。 生成的類是Keras模型,就像您熟悉的順序模型一樣,將指定的輸入映射到指定的輸出。 與順序類不同,使Model類與衆不同的是,它允許具有多個輸出的模型。

# This will return a list of 5 Numpy arrays:
# one array per layer activation
activations = activation_model.predict(img_tensor)

這個activations裏面,就裝好了各層的所有的激活值。我們可以隨便找一層的activation打印出來它的形狀看看:

first_layer_activation = activations[0]
print(first_layer_activation.shape)

結果:

(1, 250, 167, 64)

這是一個具有64個通道的250*167特徵圖,其中64代表着通道數。

可視化它的第三個通道:

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

在這裏插入圖片描述
可視化它的第50個通道:

plt.matshow(first_layer_activation[0, :, :, 50], cmap='viridis')
plt.show()

在這裏插入圖片描述
讓我們對網絡中所有激活進行完整的可視化繪製。 我們將提取並繪製每一個激活圖中每個通道,並將結果堆疊在一個大圖像張量中,通道並排堆疊。

import keras

# These are the names of the layers, so can have them as part of our plot
layer_names = []
for layer in model.layers[2:51]:
    layer_names.append(layer.name)

images_per_row = 16

# Now let's display our feature maps
for layer_name, layer_activation in zip(layer_names, activations):
    # This is the number of features in the feature map
    n_features = layer_activation.shape[-1]

    # The feature map has shape (1, size, size, n_features)
    size = layer_activation.shape[1]

    # We will tile the activation channels in this matrix
    n_cols = n_features // images_per_row
    display_grid = np.zeros((size * n_cols, images_per_row * size))

    # We'll tile each filter into this big horizontal grid
    for col in range(n_cols):
        for row in range(images_per_row):
            channel_image = layer_activation[0,
                                             :, :,
                                             col * images_per_row + row]
            # Post-process the feature to make it visually palatable
            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

    # Display the grid
    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')
    
plt.show()

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
等等。。。。。。

可視化卷積濾波器

這個過程很簡單:我們將建立一個損失函數,使給定卷積層中給定濾波器的值最大化,然後我們將使用隨機梯度下降來調整輸入圖像的值,以使該激活值最大化。 例如,這是在ImageNet上預訓練的ResNet50網絡的“ bn2a_branch2b ”層中激活過濾器0的損失:

from keras import backend as K
from keras.applications.resnet50 import ResNet50
model = ResNet50(weights=None,
                 include_top=False,)
model.load_weights('resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5')
layer_name = 'bn2a_branch2b'
filter_index = 0
layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])

爲了實現梯度下降,我們需要相對於模型輸入的這種損失的梯度。 爲此,我們將使用Keras後端模塊附帶的漸變函數:

# The call to `gradients` returns a list of tensors (of size 1 in this case)
# hence we only keep the first element -- which is a tensor.
grads = K.gradients(loss, model.input)[0]

要使梯度下降過程順利進行的一個顯而易見的技巧是通過將梯度張量除以其L2範數(張量中值的平方的平均值的平方根)來歸一化。 這樣可以確保對輸入圖像進行的更新幅度始終在同一範圍內。

# We add 1e-5 before dividing so as to avoid accidentally dividing by 0.
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)

現在,在給定輸入圖像的情況下,我們需要一種方法來計算損耗張量和梯度張量的值。 我們可以定義一個Keras後端函數來做到這一點:iterate是一個接受Numpy張量(作爲大小爲1的張量的列表)並返回兩個Numpy張量的列表的函數:損失值和梯度值。
在這一點上,我們可以定義一個Python循環來進行隨機梯度下降:

# We start from a gray image with some noise
input_img_data = np.random.random((1, 150, 150, 3)) * 20 + 128.

# Run gradient ascent for 40 steps
step = 1.  # this is the magnitude of each gradient update
for i in range(40):
    # Compute the loss value and gradient value
    loss_value, grads_value = iterate([input_img_data])
    # Here we adjust the input image in the direction that maximizes the loss
    input_img_data += grads_value * step

生成的圖像張量將是形狀爲(1,150,150,3)的浮點張量,其值不能爲[0,255]內的整數。 因此,我們需要對該張量進行後處理,以將其轉變爲可顯示的圖像。 我們使用以下簡單的實用程序功能來實現:

def deprocess_image(x):
    # normalize tensor: center on 0., ensure std is 0.1
    x -= x.mean()
    x /= (x.std() + 1e-5)
    x *= 0.1

    # clip to [0, 1]
    x += 0.5
    x = np.clip(x, 0, 1)

    # convert to RGB array
    x *= 255
    x = np.clip(x, 0, 255).astype('uint8')
    return x

現在我們擁有了所有內容,讓我們將它們放到一個Python函數中,該函數接受一個圖層名和一個過濾器索引作爲輸入,並返回一個有效的圖像張量,該張量表示使指定過濾器的激活最大化的模式:

def generate_pattern(layer_name, filter_index, size=150):
    # Build a loss function that maximizes the activation
    # of the nth filter of the layer considered.
    layer_output = model.get_layer(layer_name).output
    loss = K.mean(layer_output[:, :, :, filter_index])

    # Compute the gradient of the input picture wrt this loss
    grads = K.gradients(loss, model.input)[0]

    # Normalization trick: we normalize the gradient
    grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)

    # This function returns the loss and grads given the input picture
    iterate = K.function([model.input], [loss, grads])
    
    # We start from a gray image with some noise
    input_img_data = np.random.random((1, size, size, 3)) * 20 + 128.

    # Run gradient ascent for 40 steps
    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('bn2a_branch2b', 0))
plt.show()

在這裏插入圖片描述
似乎bn2a_branch2b層中的過濾器0響應於圓點圖案。

現在有趣的部分:我們可以開始可視化每一層中的每個濾鏡。 爲簡單起見,我們將僅查看每層卷積塊的前64個濾波器。 我們將輸出排列在64x64濾鏡模式的8x8網格上,每個濾鏡模式之間有一些黑色邊距。

for layer_name in ['conv1', 'res2a_branch2a', 'res2b_branch2a', 'res4e_branch2c']:
    size = 64
    margin = 5

    # This a empty (black) image where we will store our results.
    results = np.zeros((8 * size + 7 * margin, 8 * size + 7 * margin, 3))

    for i in range(8):  # iterate over the rows of our results grid
        for j in range(8):  # iterate over the columns of our results grid
            # Generate the pattern for filter `i + (j * 8)` in `layer_name`
            filter_img = generate_pattern(layer_name, i + (j * 8), size=size)

            # Put the result in the square `(i, j)` of the results grid
            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

    # Display the results grid
    plt.figure(figsize=(20, 20))
    plt.imshow(results)
    plt.show()

在這裏插入圖片描述

參考文獻:https://nbviewer.jupyter.org/github/fchollet/deep-learning-with-python-notebooks/blob/master/5.4-visualizing-what-convnets-learn.ipynb

發佈了113 篇原創文章 · 獲贊 107 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章