Class Activation Map (CAM)#

Visão Computacional | Prof. Dr. Denis Mayr Lima Martins

Modelos Opacos x Modelos Transparentes#


A Necessidade de Explicabilidade#


  • CNNs como “Caixas Pretas”: Modelos de Deep Learning alcançam alta performance, mas suas previsões são difíceis de interpretar.

  • Interpretabilidade e Explicabilidade (XAI): O campo de Explainable AI (XAI) busca desvendar o processo de tomada de decisão desses modelos complexos.

  • Confiança e Transparência: A falta de transparência reduz a confiança, sendo crucial verificar se o modelo está focando nas regiões corretas da imagem.

Interpretabilidade em Modelos. Fonte: Deep Learning of Python.

Falta de Interpretabilidade em CNNs#


  • Perda da Correspondência Espacial: O uso tradicional de camadas totalmente conectadas (FC) após as convoluções “achata” os mapas de características, agindo como uma caixa preta e perdendo a correspondência direta entre a localização espacial da característica e o output final.

  • Localização Discriminativa: A capacidade da CNN de localizar objetos (inerente às camadas convolucionais) é perdida antes da classificação final.

Perda de localização por "achatamento" para camadas densas. Fonte: Joh Fischer.

A Necessidade de Explicabilidade#


Falta de Interpretabilidade em CNNs#


  • Filtros de Camadas Iniciais: Filtros próximos à entrada detectam características de baixo nível (bordas, linhas).

  • Filtros de Camadas Profundas: Em camadas mais profundas, as feature maps (mapas de características) combinam padrões, podendo corresponder a objetos ou conceitos.

Exemplo de ativações de uma rede que reconhece pessoas e cães. Fonte: Joh Fischer.

Class Activation Maps (CAM)#


  • O que é CAM?: Uma técnica para gerar mapas de ativação que indicam as regiões discriminativas de uma imagem usadas pela CNN para identificar uma categoria específica.

  • Explicabilidade Visual: O resultado é um mapa de calor (heatmap) que visualiza onde a CNN está “olhando” ao fazer uma previsão.

  • O CAM permite a localização de objetos (desenhar uma caixa delimitadora) usando apenas rótulos de nível de imagem (classificação), sem a necessidade de anotações de caixas delimitadoras (bounding boxes) durante o treinamento.

  • Proposta Original: O método foi introduzido por Zhou et al. em 2016.

Global Average Pooling (GAP)#


  • Restrição Arquitetural: O método CAM original só pode ser aplicado em modelos com uma arquitetura específica.

  • Estrutura Obrigatória: O modelo deve ter uma camada de Global Average Pooling (GAP) imediatamente após a última camada convolucional, seguida diretamente pela camada de classificação (Softmax ou FC).

  • Alternativa ao Achamento: Essa estrutura evita as camadas FC densas que agem como caixas pretas entre o mapa de características e o output.

CAM via GAP. Fonte: Zhou et al. 2016.

Global Average Pooling (GAP): Funcionamento#


  • Operação GAP: A camada GAP calcula a média espacial (média de todos os pixels) de cada mapa de características na última camada convolucional.

  • Redução Dimensional: Transforma cada mapa de característica \(k\) de dimensão \(H \times W\) em um único valor escalar \(F_k\).

Operação GAP. Fonte: Joh Fischer.

GAP versus Global Max Pooling (GMP)#


  • GAP tende a encorajar a rede a identificar a extensão completa de um objeto, pois a média se beneficia de todas as ativações positivas.

  • GMP, por outro lado, pode se contentar em identificar apenas o ponto mais discriminativo.

Comparação Flatten versus GAP#


  • Flatten perde informação espacial.

  • GAP preserva a informação espacial. Para um tensor \((8, 10, 64)\), GAP produz um tensor \((1, 1, 64)\).

Global Average Pooling (GAP)#


Operação GAP. Fonte: Taeyang Yang.

Global Average Pooling (GAP)#


Exemplo de CAM. Fonte: Taeyang Yang.

Global Average Pooling (GAP): Classificação#


  • Entrada para Classificação: O vetor resultante dos valores \(F_k\) é então alimentado diretamente na camada de classificação (Softmax/FC).

  • Importância do Conceito: O peso \(w_k^c\) representa a importância do mapa de características \(k\) (ou “conceito” \(k\)) para a previsão da classe \(c\).

Operação GAP. Fonte: Joh Fischer.

A Equação do Class Activation Map#


  • O CAM Map \(M_c\) para a classe \(c\) em um local \((x,y)\) é definido pela soma ponderada das ativações de todos os mapas de características:

\[M_c(x,y)=\sum_k w_k^c f_k(x,y)\]
  • Variáveis da Fórmula:

    • \(M_c(x,y)\): Class Activation Map, o valor de importância no local \((x,y)\) para a classe \(c\).

    • \(w_k^c\): Peso da conexão da camada FC, representando a importância do \(k\)-ésimo mapa de características para a classe \(c\).

    • \(f_k(x,y)\): Ativação do \(k\)-ésimo mapa de características na última camada convolucional, na posição \((x,y)\).

    • \(k\): Índice sobre todos os mapas de características da última camada convolucional.

  • Importância do Conceito: O peso \(w_k^c\) representa a importância do mapa de características \(k\) (ou “conceito” \(k\)) para a previsão da classe \(c\).

CAM: Exemplo#


  • Cálculo do CAM: Realizar a soma ponderada das features pelo peso da classe.

  • Normalização e Redimensionamento: O CAM resultante é de baixa resolução (e.g., 7x7) e deve ser normalizado (para mapeamento de cores) e redimensionado (upsampled) para as dimensões originais da imagem.

  • Geração do Mapa de Calor: Usar uma paleta de cores (e.g., jet) para criar o mapa de calor.

Exemplo de CAM. Fonte: Joh Fischer.

CAM: Exemplo#


  • Sobreposição: A sobreposição da imagem original com o mapa de calor visualiza as regiões discriminativas.

  • Note que o CAM foca apenas nas ativações da última camada convolucional, que pode ser mais propensa às características de alto nível, mas ignora as representações localizadas de camadas mais rasas.

Exemplo de CAM. Fonte: Joh Fischer.

Weakly-supervised object localization (WSOL)#


  • WSOL: O CAM foi originalmente proposto como uma forma de realizar Localização de Objeto Supervisionada Fracamente (WSOL).

  • Fluxo de Trabalho de Localização: Após gerar o mapa de calor, uma técnica de limiarização simples é usada: segmentar regiões com valor acima de 20% do valor máximo do CAM.

  • Geração da Bounding Box: A caixa delimitadora é desenhada cobrindo o maior componente conectado na região segmentada, localizando o objeto discriminativo.

Bounding Box via CAM. Fonte: Joh Fischer.

CAM: Implementação Pytorch#


Download da imagem de entrada aqui | Créditos: https://unsplash.com/@sadmax

import matplotlib.pyplot as plt
from PIL import Image

img = Image.open("amber-kipp-75715CVEJhI-unsplash.jpg")
plt.figure(figsize=(4,4))
plt.imshow(img)
plt.show()
A module that was compiled using NumPy 1.x cannot be run in
NumPy 2.2.5 as it may crash. To support both 1.x and 2.x
versions of NumPy, modules must be compiled with NumPy 2.0.
Some module may need to rebuild instead e.g. with 'pybind11>=2.12'.

If you are a user of the module, the easiest solution will be to
downgrade to 'numpy<2' or try to upgrade the affected module.
We expect that some modules will need time to support NumPy 2.

Traceback (most recent call last):  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel_launcher.py", line 17, in <module>
    app.launch_new_instance()
  File "/opt/anaconda3/lib/python3.11/site-packages/traitlets/config/application.py", line 992, in launch_instance
    app.start()
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelapp.py", line 701, in start
    self.io_loop.start()
  File "/opt/anaconda3/lib/python3.11/site-packages/tornado/platform/asyncio.py", line 195, in start
    self.asyncio_loop.run_forever()
  File "/opt/anaconda3/lib/python3.11/asyncio/base_events.py", line 607, in run_forever
    self._run_once()
  File "/opt/anaconda3/lib/python3.11/asyncio/base_events.py", line 1922, in _run_once
    handle._run()
  File "/opt/anaconda3/lib/python3.11/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelbase.py", line 534, in dispatch_queue
    await self.process_one()
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelbase.py", line 523, in process_one
    await dispatch(*args)
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelbase.py", line 429, in dispatch_shell
    await result
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelbase.py", line 767, in execute_request
    reply_content = await reply_content
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/ipkernel.py", line 429, in do_execute
    res = shell.run_cell(
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/zmqshell.py", line 549, in run_cell
    return super().run_cell(*args, **kwargs)
  File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3051, in run_cell
    result = self._run_cell(
  File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3106, in _run_cell
    result = runner(coro)
  File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/async_helpers.py", line 129, in _pseudo_sync_runner
    coro.send(None)
  File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3311, in run_cell_async
    has_raised = await self.run_ast_nodes(code_ast.body, cell_name,
  File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3493, in run_ast_nodes
    if await self.run_code(code, result, async_=asy):
  File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3553, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "/var/folders/p7/p37cm2fj10xgjrjj5rzdm66c0000gn/T/ipykernel_57227/2629158500.py", line 1, in <module>
    import matplotlib.pyplot as plt
  File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/__init__.py", line 161, in <module>
    from . import _api, _version, cbook, _docstring, rcsetup
  File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/rcsetup.py", line 27, in <module>
    from matplotlib.colors import Colormap, is_color_like
  File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/colors.py", line 57, in <module>
    from matplotlib import _api, _cm, cbook, scale
  File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/scale.py", line 22, in <module>
    from matplotlib.ticker import (
  File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/ticker.py", line 143, in <module>
    from matplotlib import transforms as mtransforms
  File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/transforms.py", line 49, in <module>
    from matplotlib._path import (
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
AttributeError: _ARRAY_API not found
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[1], line 1
----> 1 import matplotlib.pyplot as plt
      2 from PIL import Image
      4 img = Image.open("amber-kipp-75715CVEJhI-unsplash.jpg")

File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/__init__.py:161
    157 from packaging.version import parse as parse_version
    159 # cbook must import matplotlib only within function
    160 # definitions, so it is safe to import from it here.
--> 161 from . import _api, _version, cbook, _docstring, rcsetup
    162 from matplotlib.cbook import sanitize_sequence
    163 from matplotlib._api import MatplotlibDeprecationWarning

File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/rcsetup.py:27
     25 from matplotlib import _api, cbook
     26 from matplotlib.cbook import ls_mapper
---> 27 from matplotlib.colors import Colormap, is_color_like
     28 from matplotlib._fontconfig_pattern import parse_fontconfig_pattern
     29 from matplotlib._enums import JoinStyle, CapStyle

File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/colors.py:57
     55 import matplotlib as mpl
     56 import numpy as np
---> 57 from matplotlib import _api, _cm, cbook, scale
     58 from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS
     61 class _ColorMapping(dict):

File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/scale.py:22
     20 import matplotlib as mpl
     21 from matplotlib import _api, _docstring
---> 22 from matplotlib.ticker import (
     23     NullFormatter, ScalarFormatter, LogFormatterSciNotation, LogitFormatter,
     24     NullLocator, LogLocator, AutoLocator, AutoMinorLocator,
     25     SymmetricalLogLocator, AsinhLocator, LogitLocator)
     26 from matplotlib.transforms import Transform, IdentityTransform
     29 class ScaleBase:

File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/ticker.py:143
    141 import matplotlib as mpl
    142 from matplotlib import _api, cbook
--> 143 from matplotlib import transforms as mtransforms
    145 _log = logging.getLogger(__name__)
    147 __all__ = ('TickHelper', 'Formatter', 'FixedFormatter',
    148            'NullFormatter', 'FuncFormatter', 'FormatStrFormatter',
    149            'StrMethodFormatter', 'ScalarFormatter', 'LogFormatter',
   (...)
    155            'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator',
    156            'SymmetricalLogLocator', 'AsinhLocator', 'LogitLocator')

File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/transforms.py:49
     46 from numpy.linalg import inv
     48 from matplotlib import _api
---> 49 from matplotlib._path import (
     50     affine_transform, count_bboxes_overlapping_bbox, update_path_extents)
     51 from .path import Path
     53 DEBUG = False

ImportError: numpy.core.multiarray failed to import
import torch
import torch.nn.functional as F
import torchvision.models as models
import matplotlib.pyplot as plt
from torchvision import transforms
import numpy as np



# O ResNet é já contém a estrutura GAP antes de FC.
model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)
model.eval()

# Armazenar ativações da última camada convolucional ('layer4')
activation = {}
def get_activation(name):
    def hook(model, input, output):
        activation[name] = output.detach()
    return hook

# Registrar o hook na camada alvo (layer4)
model.layer4.register_forward_hook(get_activation('final_conv'))
<torch.utils.hooks.RemovableHandle at 0x150ca9a50>
# Define transformações no padrão da ResNet (224x224)
preprocess = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(
        mean=[0.485, 0.456, 0.406], 
        std=[0.229, 0.224, 0.225])
])

transformada_img = preprocess(img)

# Adiciona dimensão de batch
model_img = transformada_img.unsqueeze(0)
print(model_img.shape)
torch.Size([1, 3, 224, 224])
# Faz o forward pass
outputs = model(model_img)
probs = F.softmax(outputs, dim=1)
class_idx = torch.argmax(probs).item()

# Gera a saída do modelo
output_shape = model(transformada_img.unsqueeze(0)).shape

print(f"Classe Prevista: {class_idx}") 
Classe Prevista: 282
# Extrai Feature Maps
conv_layer_output = activation["final_conv"]
conv_layer_output = conv_layer_output.squeeze(0)

# Recuperar Pesos da Camada FC
fc_weights = model.fc.weight
cat_class_idx = class_idx  # Índice da classe 'cat' na ResNet-18
# Isola os Pesos para a Classe Prevista
cat_fc_weights = fc_weights[cat_class_idx].unsqueeze(1).unsqueeze(1)

# Computa a soma ponderada para gerar o CAM
final_conv_layer_output = cat_fc_weights * conv_layer_output
class_activation_map = final_conv_layer_output.sum(0)
plt.imshow(class_activation_map.to("cpu").detach().numpy())
plt.show()
../_images/06f793d1c44e35248d1ad9365133eb9ee01fd39f7a4e870a13dc4b1f0c9a33ae.png
# Faz o resize do CAM para o tamanho da imagem de entrada
cam_resized = F.interpolate(
    class_activation_map.unsqueeze(0).unsqueeze(0),
      size=tuple(model_img.shape[-2:]), 
      mode='bilinear', 
      align_corners=False)

# Converte CAM para NumPy array
cam_np = cam_resized.squeeze().cpu().detach().numpy()
cam_expanded = np.expand_dims(cam_np, axis=2)

# Converte imagem de entrada para NumPy array
img_np = np.array(img.resize((224,224)))
# Mostra CAM + Imagem
plt.imshow(img_np)
plt.imshow(cam_expanded, alpha=0.5, cmap='jet')
plt.show()
../_images/96573b061640d46cf33cd575bc328a7a1892c696ed09af5b84572c704a13a0e5.png

O cálculo é a implementação direta da fórmula \(M_c(x,y)=\sum_k w_k^c f_k(x,y)\). O redimensionamento é crucial porque os mapas de características são menores do que a imagem original. Uma vez redimensionado, aplicamos um colormap de alto contraste como jet (cores quentes para importância) e o misturamos com a imagem de entrada.

CAM: Limitações#


  • Restrição de Arquitetura: A principal limitação é a exigência de ter GAP seguido diretamente pelo Softmax, o que força modificações arquiteturais em modelos que não o possuem (e.g., AlexNet original ou VGG).

  • Necessidade de Re-treinamento/Ajuste Fino: Se um modelo popular não se adequar à arquitetura CAM, ele deve ser modificado (removendo camadas FC) e então ajustado (fine-tuning).

  • Baixa Resolução: O mapa de calor é inicialmente gerado na baixa resolução dos feature maps da última convolução (e.g., 7x7 ou 14x14).

  • Dependência da Última Camada: O CAM foca apenas nas ativações da última camada convolucional, que pode ser mais propensa a características de alto nível, mas ignora as representações localizadas de camadas mais rasas.

Evolução e Outras Abordagens: Grad-CAM#


  • Grad-CAM (Generalização): O Grad-CAM (Gradient-weighted Class Activation Mapping) foi proposto para superar essa restrição, tornando a localização visual aplicável a qualquer arquitetura de CNN.

  • Em vez de usar os pesos \(w_k^c\) da camada FC (como no CAM), o Grad-CAM usa os gradientes da pontuação da classe alvo em relação aos feature maps da última convolução para calcular os coeficientes de importância \(\alpha_k^c\).

Exemplo de Grad-CAM. Fonte: A Data Odyssey.

Evolução e Outras Abordagens: Grad-CAM++#


Grad-CAM++: Uma melhoria do Grad-CAM que calcula uma média ponderada verdadeira dos gradientes, oferecendo melhores explicações visuais, especialmente em imagens com múltiplas ocorrências do mesmo objeto.

  • Métodos Sem Gradiente (Gradient-Free): Surgiram métodos que eliminam a dependência de gradientes para evitar problemas como saturação de gradiente.

  • Score-CAM: Utiliza o score de forward pass de cada mapa de característica para determinar sua importância, superando as questões de gradiente.

  • Recipro-CAM: Um método mais recente, considerado state-of-the-art em eficiência computacional e precisão em certas métricas (ADCC).

Exemplo de Grad-CAM. Fonte: Tanishq Sardana.

Conclusão#


  • Fundamento de XAI: CAM é um método pioneiro e eficaz para visualizar as decisões de classificação em CNNs.

  • Interpretabilidade por Design: Explora uma arquitetura de rede específica (GAP) que mantém a capacidade de localização.

  • Localização Poderosa: Permite a localização de objetos (WSOL) com alta precisão, utilizando apenas rótulos de nível de imagem.

  • Insight em Pesos: Liga diretamente a importância do conceito (pesos FC) com a presença espacial (feature maps).

  • Evolução: Embora limitado arquiteturalmente, ele pavimentou o caminho para métodos mais flexíveis e generalizados como Grad-CAM.

Material Adicional#


Tarefa 1: Geração e Visualização do CAM#


Gere CAM em um modelo popular (exemplo, VGG16) para localizar a região discriminativa que justifica a previsão de uma classe ImageNet.

Instruções Passo a Passo

  1. Configuração e Imports: Importar bibliotecas essenciais (torch, torchvision, numpy, cv2, matplotlib).

  2. Carregar Modelo: Carregar o modelo pré-treinado models.resnet50(pretrained=True) e colocá-lo em modo de avaliação (.eval()).

  3. Definir Hook: Implementar e registrar um forward hook na última camada convolucional (similar à model.layer4 do ResNet-18) para capturar os mapas de características (conv_features).

  4. Carregar e Pré-processar Imagem:

    • Escolher uma imagem do ImageNet (e.g., de um animal, carro, ou comida).

    • Carregar a imagem usando PIL ou OpenCV.

    • Aplicar as transformações necessárias (redimensionamento para 224x224, conversão para tensor e normalização ImageNet).

  5. Forward Pass e Pesos:

    • Executar o forward pass para obter as previsões e as ativações.

    • Identificar a classe prevista (class_idx).

    • Extrair os pesos da camada model.fc correspondentes a class_idx.

  6. Cálculo e Upsampling:

    • Calcular o CAM: soma ponderada dos feature maps e pesos da classe.

    • Normalizar o CAM para o intervalo.

    • Redimensionar o CAM para o tamanho da imagem de entrada (e.g., 224x224).

  7. Visualização:

    • Usar cv2.applyColorMap com cv2.COLORMAP_JET (ou matplotlib.pyplot.cm.jet) para criar o mapa de calor colorido.

    • Sobrepor o mapa de calor na imagem original (usando uma mistura ponderada, e.g., \(0.5 \times \text{Imagem} + 0.5 \times \text{Heatmap}\)).

    • Salvar a imagem de sobreposição final como resnet50_cam_overlay.png.

Resultado Esperado: Um arquivo PNG mostrando a imagem de entrada com um mapa de calor de alto contraste sobreposto, destacando a região do objeto que levou à classificação.

Tarefa 2: Comparação CAM vs. Grad-CAM#


Compare visualmente e analiticamente a localização fornecida pelo CAM (restrito à arquitetura GAP) e pelo Grad-CAM (abordagem mais generalizada) em diferentes cenários.

Instruções Passo a Passo

  1. Ferramentas: Utilize a implementação de CAM da Tarefa 1 e uma implementação de Grad-CAM (usando uma biblioteca como pytorch-grad-cam ou implementando a lógica do Grad-CAM) no ResNet-50.

  2. Seleção de Imagens: Escolha duas imagens:

    • Imagem A (Simples): Contém um único objeto central (e.g., um cachorro em um gramado).

    • Imagem B (Complexa/Múltiplos Objetos): Contém dois objetos diferentes de classes distintas (e.g., uma pessoa e um cavalo, ou dois objetos da mesma classe).

  3. Geração de Mapas:

    • Para a Imagem A: Gerar CAM e Grad-CAM para a classe principal.

    • Para a Imagem B: Gerar CAM e Grad-CAM para cada uma das classes presentes.

  4. Análise Comparativa: Observar a forma, a nitidez e a distribuição do calor em ambos os métodos para as duas imagens.

  5. Análise Escrita: Escreva um parágrafo curto (aproximadamente 5-7 linhas) no notebook comparando:

    • A diferença de foco entre CAM e Grad-CAM na Imagem A (simples).

    • Qual método (CAM ou Grad-CAM) parece ser mais eficaz na Imagem B (múltiplos objetos) para isolar as regiões discriminativas para cada classe.

Resultado Esperado: Um parágrafo de análise concluindo sobre as vantagens e desvantagens de cada método em cenários simples versus complexos (e.g., Grad-CAM++ ou Grad-CAM podem ser melhores para isolar múltiplos objetos, uma melhoria abordada pela literatura).