画像処理特徴可視化手法のまとめ(特徴マップ、コンボリューションカーネル、クラス可視化CAM)(コード付き)

I.はじめに

誰もが知っているように、ディープラーニングは「ブラックボックス」システムです。RGB画像などの入力データ、カテゴリラベルや回帰値などの出力対象からなる「エンドツーエンド」方式で動作し、途中のプロセスは不明です。「ブラックボックス」が「グレーのボックス」、さらには「ホワイトボックス」になるためには、どうすれば「ブラックボックス」を開けて調べることができるのでしょうか?そのため、「深層学習の解釈可能性」という分野があり、可視化された特徴を使用して深層畳み込みニューラルネットワークの動作メカニズムや判断基準を探索する特徴可視化技術もその1つです。この記事では、現在一般的に使用されている特徴視覚化技術について、コード解析 (pytorch) を交えながら次の 3 つの側面から説明します。

(1) 特徴マップの可視化

特徴マップの可視化方法には2種類あり、1つはあるレイヤーの特徴マップを0~255の範囲に直接マッピングして画像化する方法です。もう 1 つは、事前トレーニングされたデコンボリューション ネットワーク (デコンボリューション、アンチプーリング) を使用して、特徴マップを画像に変換し、特徴マップを視覚化するという目的を達成することです。

(2) コンボリューションカーネルの可視化

畳み込みのプロセスは特徴抽出のプロセスであり、各畳み込みカーネルが特徴を表すことがわかっています。画像内の特定の領域と特定のコンボリューション カーネルの結果が大きい場合、その領域はコンボリューション カーネルにより「似ている」ことになります。上記の推論に基づいて、特定のコンボリューション カーネルへのこの画像の出力を最大化できる画像を見つけた場合、コンボリューション カーネルが最も関心のある画像を見つけたと言います。

(3) カテゴリアクティベーションの可視化(クラスアクティベーションマッピング、CAM)

CAM (クラス アクティベーション マッピング、カテゴリ アクティベーション マップ)。カテゴリ ヒート マップまたは顕著性マップとも呼ばれます。そのサイズは元の画像と一致しており、ピクセル値は予測出力に対する元の画像の対応する領域の影響の度合いを示し、値が大きいほど寄与が大きくなります。現在一般的に使用されている CAM シリーズには、CAM、Grad-CAM、Grad-CAM++ があります。

(4) アテンション特徴の可視化

CAM と似ていますが、各特徴マップの重みが、最後の層の完全な接続ではなく、注意から得られる点が異なります。近年、注意に基づく特徴視覚化手法に関する研究が増えており、次の記事でそれらを要約します。 。

(5) いくつかの技術ツール

tensorflow フレームワークは、pytorch フレームワークを使用して直接インポートできるモデルと特徴の視覚化ツール tensorboard を提供します。

from torch.utils.tensorboard import SummaryWriter

使用方法の詳細については、https://zhuanlan.zhihu.com/p/60753993を参照してください。

2. 特徴マップの視覚化

1. 最も一般的で直接的な特徴視覚化方法

アイデアは非常にシンプルで、必要なレイヤーの特徴マップの出力を抽出し、そのレイヤーの特徴マップを視覚化するだけです。出力はグレースケール画像であるため、マルチチャネルの場合、追加の処理を行わずにすべてのチャネルの特徴マップが直接視覚化されることに注意してください。

コードに直接移動します。

importtorchfromtorchvisionimportmodels,transformsfromPILimportImageimportmatplotlib.pyplotaspltimportnumpyasnpimportscipy.misc# 导入数据defget_image_info(image_dir):# 以RGB格式打开图像# Pytorch DataLoader就是使用PIL所读取的图像格式# 建议就用这种方法读取图像,当读入灰度图像时convert('')image_info=Image.open(image_dir).convert('RGB')# 数据预处理方法image_transform=transforms.Compose([transforms.Resize(256),transforms.CenterCrop(224),transforms.ToTensor(),transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])])image_info=image_transform(image_info)image_info=image_info.unsqueeze(0)returnimage_info# 获取第k层的特征图defget_k_layer_feature_map(feature_extractor,k,x):withtorch.no_grad():forindex,layerinenumerate(feature_extractor):x=layer(x)ifk==index:returnx#  可视化特征图defshow_feature_map(feature_map):feature_map=feature_map.squeeze(0)feature_map=feature_map.cpu().numpy()feature_map_num=feature_map.shape[0]row_num=np.ceil(np.sqrt(feature_map_num))plt.figure()forindexinrange(1,feature_map_num+1):plt.subplot(row_num,row_num,index)plt.imshow(feature_map[index-1],cmap='gray')plt.axis('off')scipy.misc.imsave(str(index)+".png",feature_map[index-1])plt.show()if__name__=='__main__':# 初始化图像的路径image_dir=r"husky.png"# 定义提取第几层的feature mapk=1# 导入Pytorch封装的AlexNet网络模型model=models.alexnet(pretrained=True)# 是否使用gpu运算use_gpu=torch.cuda.is_available()use_gpu=False# 读取图像信息image_info=get_image_info(image_dir)# 判断是否使用gpuifuse_gpu:model=model.cuda()image_info=image_info.cuda()# alexnet只有features部分有特征图# classifier部分的feature map是向量feature_extractor=model.featuresfeature_map=get_k_layer_feature_map(feature_extractor,k,image_info)show_feature_map(feature_map)

2. デコンボリューション可視化機能

高次元特徴マップの場合、特徴マップの数(チャネル数)が 3 よりもはるかに多くなるため、特徴マップを直接表示する 1 とは異なり、グレースケール画像や RGB 画像の形で視覚化することが困難です。各チャネルを個別にデコンボリューションすることで、高次元の特徴マップを低次元の画像に直接変換し、画像の次元を復元できます。

以下の図に示すように、デコンボリューション ネットワークの目的は、デコンボリューション ネットワークを通じて、学習済みニューラル ネットワーク内の特徴マップの任意の層のピクセル空間を再構築することです。主な操作は、アンプール、補正整流、フィルターです。 、アンチプーリング、アンチアクティベーション、およびデコンボリューション。

ラベル付きデータを取得することは不可能であるため、デコンボリューション ネットワークは、訓練されたネットワーク検出器や複雑なマッピング関数と同様、教師なし、非学習の能力です。

実装の詳細については、「畳み込みネットワークの視覚化と理解」の記事を参照してください。

ガイド付きバックプロパゲーションの改良版「Striving for Simplicity: The All Convolutional Net」もあります。

上記 2 つの方法では、特徴マップの視覚化に明らかな欠陥があり、画像内のどの領域が特定のカテゴリを識別する役割を果たしているかを視覚化することができません。これは主に CAM シリーズを使用する方法です。第 4 章で紹介します。以下にコンボリューションカーネルを可視化する方法を紹介します。

3. コンボリューションカーネルの可視化

コンボリューション カーネルはどのようにオブジェクトを識別するのでしょうか? この問題を解決する 1 つの方法は、コンボリューション カーネルが最も関心のある画像の種類を理解することです。畳み込みのプロセスは特徴抽出のプロセスであり、各畳み込みカーネルが特徴を表すことがわかっています。画像内の特定の領域と特定のコンボリューション カーネルの結果が大きい場合、その領域はコンボリューション カーネルにより「似ている」ことになります。上記の推論に基づいて、特定のコンボリューション カーネルへのこの画像の出力を最大化できる画像を見つけた場合、コンボリューション カーネルが最も関心のある画像を見つけたと言います。

具体的なアイデア: ランダムなノイズを含む画像から開始し、各ピクセル値の色をランダムに選択します。次に、このノイズ マップを CNN ネットワークの入力として使用して順方向に伝播し、ネットワークの i 番目の層にある j コンボリューション カーネルの活性化 a_ij(x) を取得し、逆伝播を実行して勾配 G= ∂F/∂I、目標は、各ピクセルのカラー値を変更することでコンボリューション カーネルの活性化を高め、勾配上昇法で画像 I=I+η∗G を繰り返し更新することです。η は同様です学習率の問題に。

#使用keras可视化卷积核代码
import numpy as np
import tensorflow as tf
from tensorflow import keras
# The dimensions of our input image
img_width = 180
img_height = 180
# Our target layer: we will visualize the filters from this layer.
# See `model.summary()` for list of layer names, if you want to change this.
layer_name = "conv3_block4_out"


# Build a ResNet50V2 model loaded with pre-trained ImageNet weights
model = keras.applications.ResNet50V2(weights="imagenet", include_top=False)
# Set up a model that returns the activation values for our target layerlayer = model.get_layer(name=layer_name)
feature_extractor = keras.Model(inputs=model.inputs, outputs=layer.output)
# loss函数取最大化指定卷积核的响应值的平均值
def compute_loss(input_image, filter_index):
    activation = feature_extractor(input_image)
    # We avoid border artifacts by only involving non-border pixels in the loss.
    filter_activation = activation[:, 2:-2, 2:-2, filter_index]
    return tf.reduce_mean(filter_activation)
    
@tf.function
def gradient_ascent_step(img, filter_index, learning_rate):
    with tf.GradientTape() as tape:
        tape.watch(img)
        loss = compute_loss(img, filter_index)
    # Compute gradients.
    grads = tape.gradient(loss, img)
    # Normalize gradients.
    grads = tf.math.l2_normalize(grads)
    img += learning_rate * grads
    return loss, img


def initialize_image():
    # We start from a gray image with some random noise
    img = tf.random.uniform((1, img_width, img_height, 3))
    # ResNet50V2 expects inputs in the range [-1, +1].
    # Here we scale our random inputs to [-0.125, +0.125]
    return (img - 0.5) * 0.25

def visualize_filter(filter_index):
    # We run gradient ascent for 20 steps
    iterations = 30
    learning_rate = 10.0
    img = initialize_image()
    for iteration in range(iterations):
        loss, img = gradient_ascent_step(img, filter_index, learning_rate)

    # Decode the resulting input image
    img = deprocess_image(img[0].numpy())
    return loss, img

def deprocess_image(img):
    # Normalize array: center on 0., ensure variance is 0.15
    img -= img.mean()
    img /= img.std() + 1e-5
    img *= 0.15

    # Center crop
    img = img[25:-25, 25:-25, :]

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

    # Convert to RGB array
    img *= 255
    img = np.clip(img, 0, 255).astype("uint8")
    return img

コードでは、トレーニングされた VGG16 モデルを使用して、モデルのコンボリューション カーネルを視覚化します。結果は次のとおりです

コードとビジュアライゼーションへの参照リンク

畳み込みニューラル ネットワークは世界をどのように見ているか

http://www.51zixue.net/deeplearning/746.html

4. カテゴリーアクティベーション Visual CAM

CAM の正式名称は Class Activation Mapping で、クラス アクティベーション マップだけでなく、クラス ヒート マップ、顕著性マップなどとも呼ばれます。元の画像と同じサイズの画像で、画像上の各位置の画素値は0から1の範囲で、一般的には0から255のグレースケール画像で表されます。予測出力への寄与度の分布として理解でき、スコアが高いほど応答が高く、元の画像の対応する領域のネットワークへの寄与度が高くなります。一般的に使用される CAM メソッドは次のとおりです。

1.カム

深い畳み込みニューラル ネットワークの場合、複数の畳み込みとプーリングの後、最後の畳み込み層には最も豊富な空間情報と意味論的な情報が含まれ、次に全結合層とソフトマックス層になります。そこに含まれる情報は人間が理解するのが困難です。そしてそれを視覚的に表現するのは難しいです。したがって、畳み込みニューラル ネットワークがその分類結果に対して合理的な説明を与えるためには、最後の畳み込み層を最大限に活用する必要があります。

如上图所示,CAM的结构由CNN特征提取网络,全局平均池化GAP,全连接层和Softmax组成。一张图片在经过CNN特征提取网络后得到feature maps, 再对每一个feature map进行全局平均池化,变成一维向量,再经过全连接层与softmax得到类的概率。假定在GAP前是n个通道,则经过GAP后得到的是一个长度为1x n的向量,假定类别数为m,则全连接层的权值为一个n x m的张量。(注:这里先忽视batch-size)。对于某一个类别C, 现在想要可视化这个模型对于识别类别C,原图像的哪些区域起主要作用,换句话说模型是根据哪些信息得到该图像就是类别C。

做法是取出全连接层中得到类别C的概率的那一维权值,用W表示,即上图的下半部分。然后对GAP前的feature map进行加权求和,由于此时feature map不是原图像大小,在加权求和后还需要进行上采样,即可得到Class Activation Map。

用公式表示如下:(k表示通道,c表示类别,fk(x,y)表示feature map)

参考核心代码:

class CAM(_CAM):
    """Implements a class activation map extractor as described in `"Learning Deep Features for Discriminative
    Localization" <https://arxiv.org/pdf/1512.04150.pdf>`_.
    The Class Activation Map (CAM) is defined for image classification models that have global pooling at the end
    of the visual feature extraction block. The localization map is computed as follows:
    .. math::
        L^{(c)}_{CAM}(x, y) = ReLU\\Big(\\sum\\limits_k w_k^{(c)} A_k(x, y)\\Big)
    where :math:`A_k(x, y)` is the activation of node :math:`k` in the target layer of the model at
    position :math:`(x, y)`,
    and :math:`w_k^{(c)}` is the weight corresponding to class :math:`c` for unit :math:`k` in the fully
    connected layer..
    Example::
        >>> from torchvision.models import resnet18
        >>> from torchcam.cams import CAM
        >>> model = resnet18(pretrained=True).eval()
        >>> cam = CAM(model, 'layer4', 'fc')
        >>> with torch.no_grad(): out = model(input_tensor)
        >>> cam(class_idx=100)
    Args:
        model: input model
        target_layer: name of the target layer
        fc_layer: name of the fully convolutional layer
        input_shape: shape of the expected input tensor excluding the batch dimension
    """

    def __init__(
        self,
        model: nn.Module,
        target_layer: Optional[str] = None,
        fc_layer: Optional[str] = None,
        input_shape: Tuple[int, ...] = (3, 224, 224),
        **kwargs: Any,
    ) -> None:

        super().__init__(model, target_layer, input_shape, **kwargs)

        # If the layer is not specified, try automatic resolution
        if fc_layer is None:
            fc_layer = locate_linear_layer(model)
            # Warn the user of the choice
            if isinstance(fc_layer, str):
                logging.warning(f"no value was provided for `fc_layer`, thus set to '{fc_layer}'.")
            else:
                raise ValueError("unable to resolve `fc_layer` automatically, please specify its value.")
        # Softmax weight
        self._fc_weights = self.submodule_dict[fc_layer].weight.data
        # squeeze to accomodate replacement by Conv1x1
        if self._fc_weights.ndim > 2:
            self._fc_weights = self._fc_weights.view(*self._fc_weights.shape[:2])

    def _get_weights(self, class_idx: int, scores: Optional[Tensor] = None) -> Tensor:
        """Computes the weight coefficients of the hooked activation maps"""

        # Take the FC weights of the target class
        return self._fc_weights[class_idx, :]

2. Grad-CAM

利用 GAP 获取 CAM 的方式有它的局限性:

1)要求模型必须有 GAP 层;

2)只能提取最后一层特征图的热力图。

Grad-CAM 是为了克服上面的缺陷而提出的,Grad-CAM的最大特点就是不再需要修改现有的模型结构了,也不需要重新训练了,直接在原模型上即可可视化,可提取任意层的热力图。

原理:Grad-CAM根据输出向量,进行backward,求取特征图的梯度,得到每个特征图上每个像素点对应的梯度,也就是特征图对应的梯度图,然后再对每个梯度图求平均,这个平均值就对应于每个特征图的权重,然后再将权重与特征图进行加权求和,最后经过relu激活函数就可以得到最终的类激活图。

参考核心代码:

class _GradCAM(_CAM):
    """Implements a gradient-based class activation map extractor
    Args:
        model: input model
        target_layer: name of the target layer
        input_shape: shape of the expected input tensor excluding the batch dimension
    """

    def __init__(
        self,
        model: torch.nn.Module,
        target_layer: Optional[str] = None,
        input_shape: Tuple[int, ...] = (3, 224, 224),
        **kwargs: Any,
    ) -> None:

        super().__init__(model, target_layer, input_shape, **kwargs)
        # Init hook
        self.hook_g: Optional[Tensor] = None
        # Ensure ReLU is applied before normalization
        self._relu = True
        # Model output is used by the extractor
        self._score_used = True
        # Trick to avoid issues with inplace operations cf. https://github.com/pytorch/pytorch/issues/61519
        self.hook_handles.append(self.submodule_dict[self.target_layer].register_forward_hook(self._hook_g))

    def _store_grad(self, grad: Tensor) -> None:
        if self._hooks_enabled:
            self.hook_g = grad.data

    def _hook_g(self, module: torch.nn.Module, input: Tensor, output: Tensor) -> None:
        """Gradient hook"""
        if self._hooks_enabled:
            self.hook_handles.append(output.register_hook(self._store_grad))

    def _backprop(self, scores: Tensor, class_idx: int) -> None:
        """Backpropagate the loss for a specific output class"""

        if self.hook_a is None:
            raise TypeError("Inputs need to be forwarded in the model for the conv features to be hooked")

        # Backpropagate to get the gradients on the hooked layer
        loss = scores[:, class_idx].sum()
        self.model.zero_grad()
        loss.backward(retain_graph=True)

    def _get_weights(self, class_idx, scores):

        raise NotImplementedError


class GradCAM(_GradCAM):
    """Implements a class activation map extractor as described in `"Grad-CAM: Visual Explanations from Deep Networks
    via Gradient-based Localization" <https://arxiv.org/pdf/1610.02391.pdf>`_.
    The localization map is computed as follows:
    .. math::
        L^{(c)}_{Grad-CAM}(x, y) = ReLU\\Big(\\sum\\limits_k w_k^{(c)} A_k(x, y)\\Big)
    with the coefficient :math:`w_k^{(c)}` being defined as:
    .. math::
        w_k^{(c)} = \\frac{1}{H \\cdot W} \\sum\\limits_{i=1}^H \\sum\\limits_{j=1}^W
        \\frac{\\partial Y^{(c)}}{\\partial A_k(i, j)}
    where :math:`A_k(x, y)` is the activation of node :math:`k` in the target layer of the model at
    position :math:`(x, y)`,
    and :math:`Y^{(c)}` is the model output score for class :math:`c` before softmax.
    Example::
        >>> from torchvision.models import resnet18
        >>> from torchcam.cams import GradCAM
        >>> model = resnet18(pretrained=True).eval()
        >>> cam = GradCAM(model, 'layer4')
        >>> scores = model(input_tensor)
        >>> cam(class_idx=100, scores=scores)
    Args:
        model: input model
        target_layer: name of the target layer
        input_shape: shape of the expected input tensor excluding the batch dimension
    """

    def _get_weights(self, class_idx: int, scores: Tensor) -> Tensor:  # type: ignore[override]
        """Computes the weight coefficients of the hooked activation maps"""

        self.hook_g: Tensor
        # Backpropagate
        self._backprop(scores, class_idx)
        # Global average pool the gradients over spatial dimensions
        return self.hook_g.squeeze(0).flatten(1).mean(-1)

3. Grad-CAM++

Grad-CAM是利用目标特征图的梯度求平均(GAP)获取特征图权重,可以看做梯度map上每一个元素的贡献是一样。为了得到更好的效果(特别是在某一分类的物体在图像中不止一个的情况下),Chattopadhyay等认为梯度map上的每一个元素的贡献不同,又进一步提出了Grad-CAM++,主要的变动是在对应于某个分类的特征映射的权重表示中加入了ReLU和权重梯度 :

只需一次反向传播即可计算梯度:

下图是CAM、Grad-CAM、Grad-CAM++架构对比:

核心代码展示:

class GradCAMpp(_GradCAM):
    """Implements a class activation map extractor as described in `"Grad-CAM++: Improved Visual Explanations for
    Deep Convolutional Networks" <https://arxiv.org/pdf/1710.11063.pdf>`_.
    The localization map is computed as follows:
    .. math::
        L^{(c)}_{Grad-CAM++}(x, y) = \\sum\\limits_k w_k^{(c)} A_k(x, y)
    with the coefficient :math:`w_k^{(c)}` being defined as:
    .. math::
        w_k^{(c)} = \\sum\\limits_{i=1}^H \\sum\\limits_{j=1}^W \\alpha_k^{(c)}(i, j) \\cdot
        ReLU\\Big(\\frac{\\partial Y^{(c)}}{\\partial A_k(i, j)}\\Big)
    where :math:`A_k(x, y)` is the activation of node :math:`k` in the target layer of the model at
    position :math:`(x, y)`,
    :math:`Y^{(c)}` is the model output score for class :math:`c` before softmax,
    and :math:`\\alpha_k^{(c)}(i, j)` being defined as:
    .. math::
        \\alpha_k^{(c)}(i, j) = \\frac{1}{\\sum\\limits_{i, j} \\frac{\\partial Y^{(c)}}{\\partial A_k(i, j)}}
        = \\frac{\\frac{\\partial^2 Y^{(c)}}{(\\partial A_k(i,j))^2}}{2 \\cdot
        \\frac{\\partial^2 Y^{(c)}}{(\\partial A_k(i,j))^2} + \\sum\\limits_{a,b} A_k (a,b) \\cdot
        \\frac{\\partial^3 Y^{(c)}}{(\\partial A_k(i,j))^3}}
    if :math:`\\frac{\\partial Y^{(c)}}{\\partial A_k(i, j)} = 1` else :math:`0`.
    Example::
        >>> from torchvision.models import resnet18
        >>> from torchcam.cams import GradCAMpp
        >>> model = resnet18(pretrained=True).eval()
        >>> cam = GradCAMpp(model, 'layer4')
        >>> scores = model(input_tensor)
        >>> cam(class_idx=100, scores=scores)
    Args:
        model: input model
        target_layer: name of the target layer
        input_shape: shape of the expected input tensor excluding the batch dimension
    """

    def _get_weights(self, class_idx: int, scores: Tensor) -> Tensor:  # type: ignore[override]
        """Computes the weight coefficients of the hooked activation maps"""

        self.hook_g: Tensor
        # Backpropagate
        self._backprop(scores, class_idx)
        # Alpha coefficient for each pixel
        grad_2 = self.hook_g.pow(2)
        grad_3 = grad_2 * self.hook_g
        # Watch out for NaNs produced by underflow
        spatial_dims = self.hook_a.ndim - 2  # type: ignore[union-attr]
        denom = 2 * grad_2 + (grad_3 * self.hook_a).flatten(2).sum(-1)[(...,) + (None,) * spatial_dims]
        nan_mask = grad_2 > 0
        alpha = grad_2
        alpha[nan_mask].div_(denom[nan_mask])

        # Apply pixel coefficient in each weight
        return alpha.squeeze_(0).mul_(torch.relu(self.hook_g.squeeze(0))).flatten(1).sum(-1)

本节参考论文:

Deconvolution Visualizing and Understanding Convolutional Networks

Guided-backpropagation Striving for Simplicity: The All Convolutional Net

CAM Learning Deep Features for Discriminative Localization

Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization

Yes, Deep Networks are great, but are they Trustworthy?

五、其他资料

pytorch 完整实现'CAM', 'ScoreCAM', 'SSCAM', 'ISCAM' 'GradCAM', 'GradCAMpp', 'SmoothGradCAMpp', 'XGradCAM', 'LayerCAM' :

https://github.com/ZhugeKongan/TorchCAMgithub.com/ZhugeKongan/TorchCAM

おすすめ

転載: blog.csdn.net/Wzongming/article/details/129350690