[PYTHON] Déterminez s'il s'agit de mon enfant à partir de l'image du chien Shiba par apprentissage profond (4) Visualisation par Grad-CAM et Grad-CAM guidé

introduction

――Ceci est le résultat de mon propre dossier d'étude sur l'apprentissage automatique et l'apprentissage profond.

Public cible de cet article / Articles référencés

--Cible: identique à la précédente. Pour plus de détails ** ici **. --Article de référence ** ○ Je vais expliquer en détail le code source de Grad-CAM et Guided Grad-CAM au Japon (implémentation Keras) **

À propos de moi

--Acquis ** JDLA Deep Learning pour Engeneer 2019 # 2 ** en septembre 2019. ――Jusqu'à la fin mars 2020, vous occuperez le bureau d'une société d'intérêt public. À partir d'avril 2020, il a changé de carrière en ingénieur de données.

Aperçu de l'analyse précédente (3)

――Nous avons implémenté Grad-CAM et créé une carte thermique sur les caractéristiques qui sont à la base de la classification des photos de chiens Shiba.

Aperçu de la procédure de cette époque (4)

** Procédure 1 Préparation ** ** Étape 2 Enregistrez les fonctions requises pour implémenter Grad-CAM et Guided Grad-CAM ** ** Étape 3 Mise en œuvre du traitement principal de Grad-CAM ** ** Étape 4 Mise en œuvre du traitement principal de Guided Grad-CAM **

Étape 1 Préparation

(1) Montez Google Drive

Montez pour que les données puissent être lues dans Colab à partir du dossier contenant l'image du chien Shiba.

#Monture Google Drive
from google.colab import drive
drive.mount('/content/drive')

(2) Importez la bibliothèque requise

Importez avec le code suivant.

#Importer la bibliothèque
from __future__ import print_function
import keras
from keras.applications import VGG16
from keras.models import Sequential, load_model, model_from_json
from keras import models, optimizers, layers
from keras.optimizers import SGD
from keras.layers import Dense, Dropout, Activation, Flatten
from sklearn.model_selection import train_test_split  
from PIL import Image 
from keras.preprocessing import image as images
from keras.preprocessing.image import array_to_img, img_to_array, load_img
from keras import backend as K 
import os
import numpy as np  
import glob  
import pandas as pd
import cv2

(3) Vérification de la version Keras

Vérifions la version des keras importés (au 19 janvier 2020 au moment de la rédaction de cet article) C'est cette version)

print(keras.__version__)

2.2.5

Il y a un point à noter ici. Le contenu qui m'intéressait était écrit en ** Comment ** joint à l'article original, mais la version de keras est ** 2.2. S'il n'est pas inférieur à .4 **, il semble qu'une erreur se produira lors de l'exécution de la couche source. Quand je l'ai essayé, j'ai reçu le message d'erreur suivant sur la dernière ligne de la série d'exécution du code source. Après tout, il semble que vous deviez abaisser la version de keras avant de l'exécuter.

AttributeError: module 'keras.backend' has no attribute 'image_dim_ordering'

(4) Abaisser la version de keras

Exécutez le code suivant.

#Version spécifique de keras(2.2.4)Changer pour
#Nécessite un redémarrage de l'exécution après l'exécution
!pip install keras==2.2.4

Une fois exécuté, l'écran suivant apparaîtra et la bibliothèque sera remplacée par la version spécifiée. Cependant, comme le texte brun vous avertit, vous devez redémarrer le runtime pour que cette modification prenne effet. 20200119-01.png

Pour redémarrer le runtime, cliquez sur "Restart Runtime" dans "Runtime" dans la barre de menus. 20200119-02.png

(5) Réessayez les étapes (1) à (3)

Après avoir redémarré le runtime, répétez toutes les étapes précédentes de l'étape 1 et vérifiez que la version keras est 2.2.4.

print(keras.__version__)

2.2.4

Étape 2 Enregistrez les fonctions requises pour implémenter Grad-CAM et Guided Grad-CAM

(1) Exécutez le code de définition de fonction publié dans l'article d'origine

Exécutez tout le code ci-dessous.

def target_category_loss(x, category_index, nb_classes):
    return tf.multiply(x, K.one_hot([category_index], nb_classes))

def target_category_loss_output_shape(input_shape):
    return input_shape

def normalize(x):
    # utility function to normalize a tensor by its L2 norm
    return x / (K.sqrt(K.mean(K.square(x))) + 1e-5)

def load_image(path):
    #img_path = sys.argv[1]
    img_path = path
    #Lire le fichier image spécifié par l'argument
    #La taille est redimensionnée à la valeur par défaut de VGG16 de 224x224
    img = image.load_img(img_path, target_size=(224, 224))
    #Convertir l'image au format PIL lue en tableau
    x = image.img_to_array(img)
    #Tenseur 3D (lignes), cols, channels)À
    #Tenseur 4D(samples, rows, cols, channels)Conversion en
    #Puisqu'il n'y a qu'une seule image d'entrée, les échantillons=1 va bien
    x = np.expand_dims(x, axis=0)
    x = preprocess_input(x)
    return x


def register_gradient():
    #Si GuidedBackProp n'est pas enregistré, inscrivez-vous
    if "GuidedBackProp" not in ops._gradient_registry._registry:
        #Décorateur pour enregistrer un dégradé fait maison
        #Cette fois_Fonction GuidedBackProp"GuidedBackProp"Inscrivez-vous en tant que
        @ops.RegisterGradient("GuidedBackProp")
        def _GuidedBackProp(op, grad):
            '''Des gradients qui se sont propagés vers l'arrière, propagation vers l'avant/La rétropropagation est effectuée en définissant uniquement les cellules avec une valeur de rétro-propagation négative à 0'''
            dtype = op.inputs[0].dtype
            # grad :Gradient de rétropropagation
            # tf.cast(grad > 0., dtype) :1 pour les cellules avec une gradation de 0 ou plus,Les cellules inférieures à 0 sont 0 matrices
            # tf.cast(op.inputs[0] > 0., dtype) :1 pour les cellules avec 0 entrées ou plus,Les cellules inférieures à 0 sont 0 matrices
            return grad * tf.cast(grad > 0., dtype) * \
                tf.cast(op.inputs[0] > 0., dtype)


def compile_saliency_function(model, activation_layer='block5_conv3'):
    '''Création d'une fonction qui calcule le gradient de l'entrée par rapport à la valeur maximale dans la direction du canal du calque spécifié'''
    #Modèle d'entrée
    input_img = model.input
    #Conservez la couche après la couche d'entrée comme dictionnaire de noms de couches et d'instances
    layer_dict = dict([(layer.name, layer) for layer in model.layers[1:]])
    #Obtenez la sortie de l'instance avec le nom du calque spécifié par la forme d'argument=(?, 14, 14, 512)
    layer_output = layer_dict[activation_layer].output
    #Forme qui prend la valeur maximale dans la direction du canal=(?, 14, 14)
    max_output = K.max(layer_output, axis=3)
    #Une fonction qui calcule le gradient de l'entrée par rapport à la valeur maximale dans la direction du canal du calque spécifié.
    saliency = K.gradients(K.sum(max_output), input_img)[0]
    return K.function([input_img, K.learning_phase()], [saliency])


def modify_backprop(model, name):
    '''Le gradient de la fonction ReLU"name"Remplacer par un dégradé'''

    #ReLU avec"name"Remplacé par
    g = tf.get_default_graph()
    with g.gradient_override_map({'Relu': name}):

        #▽▽▽▽▽ Question 4:Est-il nécessaire de remplacer le relu du modèle d'argument même si le nouveau modèle est renvoyé?? ▽▽▽▽▽

        #Extraire et organiser uniquement les calques activés
        # get layers that have an activation
        layer_dict = [layer for layer in model.layers[1:]
                      if hasattr(layer, 'activation')]

        #Remplacement de Keras RelU par tensorflow ReLU
        # replace relu activation
        for layer in layer_dict:
            if layer.activation == keras.activations.relu:
                layer.activation = tf.nn.relu

        # △△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△△

        #Instancier un nouveau modèle
        #Modifiez ici lorsque vous utilisez votre propre modèle
        # re-instanciate a new model
        new_model = VGG16(weights='imagenet')
    return new_model

def deprocess_image(x):
    '''
    Same normalization as in:
    https://github.com/fchollet/keras/blob/master/examples/conv_filter_visualization.py

    '''
    if np.ndim(x) > 3:
        x = np.squeeze(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
    if K.image_dim_ordering() == 'th':
        x = x.transpose((1, 2, 0))
    x = np.clip(x, 0, 255).astype('uint8')
    return x

def grad_cam(input_model, image, category_index, layer_name):
    '''
    Parameters
    ----------
    input_model : model
Modèle Keras à évaluer
    image :tuple etc.
Image d'entrée(Nombre de feuilles,Verticale,côté,Canal)
    category_index : int
Classe de classification d'image d'entrée
    layer_name : str
Nom de la couche d'activation après la dernière couche de conv..
Si l'activation est spécifiée dans la dernière couche de conv., Le nom de la couche de conv..
        batch_Si l'activation n'est pas spécifiée dans la couche conv, comme lors de l'utilisation de la normalisation,
Nom de la couche d'activation après cela.

    Returns
    ----------
    cam : tuple
        Grad-Image de la caméra
    heatmap : tuple
Image de la carte thermique
    '''
    #Nombre de classes de classification
    nb_classes = 1000

    # ----- 1.Calculer la classe de prédiction de l'image d'entrée-----

    #Catégorie d'entrée_index est la classe attendue

    # ----- 2.Calculer la perte pour la classe de prédiction-----

    #Données d'entrée x catégorie_Définition du traitement à mettre à 0 autre que l'index spécifié par index
    target_layer = lambda x: target_category_loss(x, category_index, nb_classes)

    #Entrée d'argument_cible après la couche de sortie du modèle_couche Ajouter une couche
    #Si vous prédisez le modèle, les valeurs autres que la classe de prédiction seront 0.
    x = input_model.layers[-1].output
    x = Lambda(target_layer, output_shape=target_category_loss_output_shape)(x)
    model = keras.models.Model(input_model.layers[0].input, x)

    #Étant donné que les valeurs autres que la classe de prédiction sont 0, la somme est prise et seule la valeur de la classe de prédiction est extraite.
    loss = K.sum(model.layers[-1].output)
    #Couche d'argument_couche de nom(Dernière couche de conv.)Obtenez la sortie de
    conv_output = [l for l in model.layers if l.name is layer_name][0].output

    # ----- 3.Rétropropagation de la classe de prédiction Loss à la dernière couche de conv.(Pente)Calculer-----

    #Définissez une fonction pour calculer le gradient de la valeur de la classe attendue à la dernière couche de conv.
    #De la fonction définie
    #contribution: [Image que vous voulez juger.shape=(1, 224, 224, 3)]、
    #production: [Valeur de sortie de la dernière couche de conv..shape=(1, 14, 14, 512),Gradient entre la valeur de classe attendue et la dernière couche de conv..shape=(1, 14, 14, 512)]
    grads = normalize(K.gradients(loss, conv_output)[0])
    gradient_function = K.function([model.layers[0].input], [conv_output, grads])

    #Calculer avec la fonction de calcul de gradient définie et formater les dimensions des données
    #Après la mise en forme
    # output.shape=(14, 14, 512), grad_val.shape=(14, 14, 512)
    output, grads_val = gradient_function([image])
    output, grads_val = output[0, :], grads_val[0, :, :, :]

    # ----- 4.Calculez le gradient moyen pour chaque canal dans la dernière couche de conv et l'importance de chaque canal(poids)À-----

    # weights.shape=(512, )
    # cam.shape=(14, 14)
    #* Question 1: L'initialisation de la caméra n'a-t-elle pas besoin d'être des zéros??
    weights = np.mean(grads_val, axis = (0, 1))
    cam = np.ones(output.shape[0 : 2], dtype = np.float32)
    #cam = np.zeros(output.shape[0 : 2], dtype = np.float32)    #Utilisez ceci dans mon propre modèle

    # ----- 5.La sortie de propagation directe de la dernière couche conv est pondérée pour chaque canal, additionnée et transmise par ReLU.-----

    #La sortie de propagation directe de la dernière couche conv est pondérée pour chaque canal et additionnée.
    for i, w in enumerate(weights):
        cam += w * output[:, :, i]

    #Redimensionner à la taille de l'image d'entrée(14, 14) → (224, 224)
    cam = cv2.resize(cam, (224, 224))
    #Remplacez les valeurs négatives par 0. Le traitement est le même que ReLU.
    cam = np.maximum(cam, 0)
    #Valeur 0~Normalisé à 1.
    #* Question 2: (cam - np.min(cam))/(np.max(cam) - np.min(cam))N'est-ce pas nécessaire?
    heatmap = cam / np.max(cam)
    #heatmap = (cam - np.min(cam))/(np.max(cam) - np.min(cam))    #Utilisez ceci dans mon propre modèle

    # ----- 6.Multipliez l'image d'entrée et la carte thermique-----

    #La valeur de l'image de l'image d'entrée est 0~Normalisé à 255. image.shape=(1, 224, 224, 3) → (224, 224, 3)
    #Return to BGR [0..255] from the preprocessed image
    image = image[0, :]
    image -= np.min(image)
    #* Question 3: np.uint8(image / np.max(image))N'a pas à être?
    image = np.minimum(image, 255)

    #Définissez la valeur de la carte de chaleur sur 0~Faites-en 255 et faites-en une carte de couleurs(3 canaux)
    cam = cv2.applyColorMap(np.uint8(255*heatmap), cv2.COLORMAP_JET)
    #Ajout de l'image d'entrée et de la carte thermique
    cam = np.float32(cam) + np.float32(image)
    #Valeur 0~Normalisé à 255
    cam = 255 * cam / np.max(cam)
    return np.uint8(cam), heatmap

Étape 3 Mise en œuvre du traitement principal de Grad-CAM

(1) Désignation de l'image d'entrée

Entrez le code ci-dessous. Spécifiez une image pour l'image. (Spécifiez mydog7.jpg dans cet exemple)

# cd '/content/drive/'My Drive/'Colab Notebooks'Déplacer vers le dossier de travail dans
%cd '/content/drive/'My Drive/Colab Notebooks/Self_Study/02_mydog_or_otherdogs/

#① Lire l'image d'entrée
#Changez ici pour convertir l'image d'entrée
# preprocessed_input = load_image(sys.argv[1])
preprocessed_input = load_image("./use_data/train/mydog/mydog07.jpg ")

Dans cet exemple, j'ai spécifié cette image. mydog7.jpg

(2) Chargement du modèle VGG16

--Chargez le modèle VGG16. Cette fois, nous utiliserons le modèle formé avec ImageNet tel quel. (Je voudrais mettre en œuvre les poids qui déterminent la différence entre mon enfant et les autres enfants à un autre moment.)

#② Chargez le modèle
#Changez ici si vous utilisez votre propre modèle
model = VGG16(weights='imagenet')
model.summary()

Le modèle devrait ressembler à ceci:

Model: "vgg16"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 224, 224, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 224, 224, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 224, 224, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 112, 112, 64)      0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 112, 112, 128)     73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 112, 112, 128)     147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 56, 56, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 56, 56, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 56, 56, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 56, 56, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 28, 28, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 28, 28, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 28, 28, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 28, 28, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 14, 14, 512)       0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 7, 7, 512)         0         
_________________________________________________________________
flatten (Flatten)            (None, 25088)             0         
_________________________________________________________________
fc1 (Dense)                  (None, 4096)              102764544 
_________________________________________________________________
fc2 (Dense)                  (None, 4096)              16781312  
_________________________________________________________________
predictions (Dense)          (None, 1000)              4097000   
=================================================================
Total params: 138,357,544
Trainable params: 138,357,544
Non-trainable params: 0
_________________________________________________________________

(3) Entrez les étapes restantes.

Exécutez le code suivant.

#③ Probabilité de prédiction de l'image d'entrée(predictions)Et classe prédictive(predicted_class)Calculs de
#Top lors de l'utilisation de modèles autres que VGG16_1=~3 lignes de commentaire
predictions = model.predict(preprocessed_input)
top_1 = decode_predictions(predictions)[0][0]
print('Predicted class:')
print('%s (%s) with probability %.2f' % (top_1[1], top_1[0], top_1[2]))

predicted_class = np.argmax(predictions)

# ④ Grad-Calcul de came
#Dans le cas d'un modèle self-made, l'argument"block5_conv3"Modification du nom de la couche de conversion finale du modèle personnalisé.
cam, heatmap = grad_cam(model, preprocessed_input, predicted_class, "block5_conv3")

#⑤ Enregistrer l'image
cv2.imwrite("gradcam.jpg ", cam)

Une image de heatmap Grad-CAM sera générée dans le dossier ** 02_mydog_or_otherdogs **. gradcam2.jpg

Étape 4 Mise en œuvre du traitement principal de Guided Grad-CAM

(1) Désignation de l'image d'entrée

Exécutez tout le code suivant.

#① Implémentation du gradient pour la propagation arrière guidée
register_gradient()
#(2) Remplacez le calcul du gradient de ReLU par le calcul du gradient de la propagation arrière guidée
guided_model = modify_backprop(model, 'GuidedBackProp')
#③ Définition de la fonction pour le calcul GaidedBackPropagation
#Lorsque vous utilisez votre propre classe, spécifiez le nom de la dernière couche de convection en plus de cet argument
saliency_fn = compile_saliency_function(guided_model)
#④ Calcul de la propagation arrière gaidée
saliency = saliency_fn([preprocessed_input, 0])
# ⑤ Guided Grad-Calcul CAM
gradcam = saliency[0] * heatmap[..., np.newaxis]
#⑥ Enregistrer l'image
cv2.imwrite("guided_gradcam.jpg ", deprocess_image(gradcam))

Une image de carte thermique de Guided Grad-CAM sera générée dans le dossier ** 02_mydog_or_otherdogs **. guided_gradcam2.jpg

Ça sort comme ça. Certes, je pense qu'il capture les caractéristiques sous une forme facile à comprendre pour les humains, plutôt que de regarder la carte thermique.

Ci-dessous, je posterai certains traités par mydog et otherdogs. mydog gradcam3.jpgguided_gradcam3.jpg gradcam4.jpgguided_gradcam4.jpg gradcam5.jpgguided_gradcam5.jpg gradcam6.jpgguided_gradcam6.jpg otherdogs gradcam11.jpgguided_gradcam11.jpg gradcam12.jpgguided_gradcam12.jpg gradcam13.jpgguided_gradcam13.jpg gradcam14.jpgguided_gradcam14.jpg

Cette fois, nous avons créé une image qui capture les fonctionnalités de Grad-CAM et Guided Grad-CAM. Étant donné que l'expression de la quantité de caractéristiques dans chaque méthode qui apparaît dans l'image est très différente, lors de l'explication de la caractéristique capturée par le deeplearning, il est préférable d'utiliser autant que possible diverses perspectives et de rechercher une compréhension globale. Cela semble possible. Je voudrais continuer à publier sur diverses méthodes.

Recommended Posts

Déterminez s'il s'agit de mon enfant à partir de l'image du chien Shiba par apprentissage profond (4) Visualisation par Grad-CAM et Grad-CAM guidé
Déterminer s'il s'agit de mon enfant à partir de l'image du chien Shiba par apprentissage profond (3) Visualisation par Grad-CAM
À en juger par l'image du chien Shiba en apprenant en profondeur si c'est mon enfant (1)
Juger si c'est mon enfant à partir de l'image du chien Shiba par apprentissage en profondeur (2) Augmentation des données / transfert d'apprentissage / réglage fin
Traitement de la voix par apprentissage profond: identifions qui est l'acteur vocal à partir de la voix
Deep Learning from scratch La théorie et la mise en œuvre de l'apprentissage profond appris avec Python Chapitre 3
Apprentissage parallèle du deep learning par Keras et Kubernetes
Othello ~ De la troisième ligne de "Implementation Deep Learning" (4) [Fin]
La méthode de copie de pandas.DataFrame est une copie profonde par défaut
(Deep learning) J'ai collecté des images de l'API Flickr et essayé de discriminer en transférant l'apprentissage avec VGG16
Jugement négatif / positif des phrases et visualisation des motifs par Transformer
Jugement négatif / positif des phrases par BERT et visualisation des motifs
J'ai essayé d'utiliser l'apprentissage en profondeur pour extraire la partie où la plante est montrée de la photo de la véranda, mais cela n'a pas fonctionné, je vais donc résumer le contenu des essais et erreurs. Partie 2