-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Draft pointing game implementation * Add insertion deletion auc * Add ADCC * Update auc * Introduce BaseMetric as a parent class * Delete ADCC * Remove adcc tests * Fixes from comments * Add ADCC * Remove scaling logic * Add extra unit test * Update threshold value * Update Changelog
- Loading branch information
1 parent
4e39758
commit 4ce1903
Showing
8 changed files
with
252 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
from typing import Any, Dict, List | ||
|
||
import numpy as np | ||
from scipy import stats as STS | ||
|
||
from openvino_xai import Task | ||
from openvino_xai.explainer.explainer import Explainer, ExplainMode | ||
from openvino_xai.explainer.explanation import Explanation | ||
from openvino_xai.metrics.base import BaseMetric | ||
|
||
|
||
class ADCC(BaseMetric): | ||
""" | ||
Implementation of the e Average Drop-Coherence-Complexity (ADCC) metric by Poppi, Samuele, et al 2021. | ||
References: | ||
Poppi, Samuele, et al. "Revisiting the evaluation of class activation mapping for explainability: | ||
A novel metric and experimental analysis." Proceedings of the IEEE/CVF Conference on | ||
Computer Vision and Pattern Recognition. 2021. | ||
""" | ||
|
||
def __init__(self, model, preprocess_fn, postprocess_fn, explainer=None, device_name="CPU"): | ||
super().__init__( | ||
model=model, preprocess_fn=preprocess_fn, postprocess_fn=postprocess_fn, device_name=device_name | ||
) | ||
if explainer is None: | ||
self.explainer = Explainer( | ||
model=model, | ||
task=Task.CLASSIFICATION, | ||
preprocess_fn=self.preprocess_fn, | ||
explain_mode=ExplainMode.WHITEBOX, | ||
) | ||
else: | ||
self.explainer = explainer | ||
|
||
def average_drop( | ||
self, saliency_map: np.ndarray, class_idx: int, image: np.ndarray, model_output: np.ndarray | ||
) -> float: | ||
""" | ||
Measures the average percentage drop in confidence for the target class when the model sees only the | ||
explanation map (image masked with saliency map), instead of the full image. | ||
The less the better. | ||
""" | ||
confidence_on_input = np.max(model_output) | ||
|
||
masked_image = (image * saliency_map[:, :, None]).astype(np.uint8) | ||
prediction_on_saliency_map = self.model_predict(masked_image) | ||
confidence_on_saliency_map = prediction_on_saliency_map[class_idx] | ||
|
||
return max(0.0, confidence_on_input - confidence_on_saliency_map) / confidence_on_input | ||
|
||
def coherency(self, saliency_map: np.ndarray, class_idx: int, image: np.ndarray) -> float: | ||
""" | ||
Measures the coherency of the saliency map. The explanation map (image masked with saliency map) should contain all the relevant features that explain a prediction and should remove useless features in a coherent way. | ||
Saliency map and saliency map of exlanation map should be similar. | ||
The more the better. | ||
""" | ||
|
||
masked_image = image * saliency_map[:, :, None] | ||
saliency_map_mapped_image = self.explainer(masked_image, targets=[class_idx], colormap=False, scaling=False) | ||
saliency_map_mapped_image = saliency_map_mapped_image.saliency_map[class_idx] | ||
|
||
A, B = saliency_map, saliency_map_mapped_image | ||
# Pearson correlation coefficient | ||
Asq, Bsq = A.flatten(), B.flatten() | ||
y, _ = STS.pearsonr(Asq, Bsq) | ||
y = (y + 1) / 2 | ||
|
||
return y | ||
|
||
@staticmethod | ||
def complexity(saliency_map: np.ndarray) -> float: | ||
""" | ||
Measures the complexity of the saliency map. Less important pixels -> less complexity. | ||
Defined as L1 norm of the saliency map. | ||
The less the better. | ||
""" | ||
return abs(saliency_map).sum() / (saliency_map.shape[-1] * saliency_map.shape[-2]) | ||
|
||
def __call__(self, saliency_map: np.ndarray, class_idx: int, input_image: np.ndarray) -> Dict[str, float]: | ||
""" | ||
Calculate the ADCC metric for a given saliency map and class index. | ||
The more the better. | ||
Parameters: | ||
:param saliency_map: Saliency map for class_idx class (H, W). | ||
:type saliency_map: np.ndarray | ||
:param class_idx: The class index of saliency map. | ||
:type class_idx: int | ||
:param input_image: The input image to the model (H, W, C). | ||
:type input_image: np.ndarray | ||
Returns: | ||
:return: A dictionary containing the ADCC, coherency, complexity, and average drop metrics. | ||
:rtype: Dict[str, float] | ||
""" | ||
if not (0 <= np.min(saliency_map) and np.max(saliency_map) <= 1): | ||
# Scale saliency map to [0, 1] | ||
saliency_map = saliency_map / 255 | ||
|
||
model_output = self.model_predict(input_image) | ||
|
||
avgdrop = self.average_drop(saliency_map, class_idx, input_image, model_output) | ||
coh = self.coherency(saliency_map, class_idx, input_image) | ||
com = self.complexity(saliency_map) | ||
|
||
adcc = 3 / (1 / coh + 1 / (1 - com) + 1 / (1 - avgdrop)) | ||
return {"adcc": adcc, "coherency": coh, "complexity": com, "average_drop": avgdrop} | ||
|
||
def evaluate( | ||
self, explanations: List[Explanation], input_images: List[np.ndarray], **kwargs: Any | ||
) -> Dict[str, float]: | ||
""" | ||
Evaluate the ADCC metric over a list of explanations and input images. | ||
Parameters: | ||
:param explanations: A list of explanations for each image. | ||
:type explanations: List[Explanation] | ||
:param input_images: A list of input images. | ||
:type input_images: List[np.ndarray] | ||
Returns: | ||
:return: A dictionary containing the average ADCC score. | ||
:rtype: Dict[str, float] | ||
""" | ||
results = [] | ||
for input_image, explanation in zip(input_images, explanations): | ||
for class_idx, saliency_map in explanation.saliency_map.items(): | ||
metric_dict = self(saliency_map, int(class_idx), input_image) | ||
results.append(metric_dict["adcc"]) | ||
adcc = np.mean(np.array(results), axis=0) | ||
return {"adcc": adcc} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import json | ||
from typing import Callable, List, Mapping | ||
|
||
import cv2 | ||
import numpy as np | ||
import openvino as ov | ||
import pytest | ||
|
||
from openvino_xai import Task | ||
from openvino_xai.common.utils import retrieve_otx_model | ||
from openvino_xai.explainer.explainer import Explainer, ExplainMode | ||
from openvino_xai.explainer.explanation import Explanation | ||
from openvino_xai.explainer.utils import ( | ||
ActivationType, | ||
get_postprocess_fn, | ||
get_preprocess_fn, | ||
sigmoid, | ||
) | ||
from openvino_xai.methods.black_box.base import Preset | ||
from openvino_xai.metrics.adcc import ADCC | ||
from openvino_xai.metrics.insertion_deletion_auc import InsertionDeletionAUC | ||
from openvino_xai.metrics.pointing_game import PointingGame | ||
from tests.unit.explanation.test_explanation_utils import VOC_NAMES | ||
|
||
MODEL_NAME = "mlc_mobilenetv3_large_voc" | ||
|
||
|
||
class TestADCC: | ||
image = cv2.imread("tests/assets/cheetah_person.jpg") | ||
preprocess_fn = get_preprocess_fn( | ||
change_channel_order=True, | ||
input_size=(224, 224), | ||
hwc_to_chw=True, | ||
) | ||
postprocess_fn = get_postprocess_fn(activation=ActivationType.SIGMOID) | ||
|
||
@pytest.fixture(autouse=True) | ||
def setup(self, fxt_data_root): | ||
self.data_dir = fxt_data_root | ||
retrieve_otx_model(self.data_dir, MODEL_NAME) | ||
model_path = self.data_dir / "otx_models" / (MODEL_NAME + ".xml") | ||
self.model = ov.Core().read_model(model_path) | ||
self.explainer = Explainer( | ||
model=self.model, | ||
task=Task.CLASSIFICATION, | ||
preprocess_fn=self.preprocess_fn, | ||
explain_mode=ExplainMode.WHITEBOX, | ||
) | ||
self.adcc = ADCC(self.model, self.preprocess_fn, self.postprocess_fn, self.explainer) | ||
|
||
def test_adcc_init_wo_explainer(self): | ||
adcc_wo_explainer = ADCC(self.model, self.preprocess_fn, self.postprocess_fn) | ||
assert isinstance(adcc_wo_explainer.explainer, Explainer) | ||
|
||
def test_adcc(self): | ||
input_image = np.random.randint(0, 256, (224, 224, 3), dtype=np.uint8) | ||
saliency_map = np.random.rand(224, 224) | ||
|
||
complexity_score = self.adcc.complexity(saliency_map) | ||
assert complexity_score >= 0.2 | ||
|
||
model_output = self.adcc.model_predict(input_image) | ||
class_idx = np.argmax(model_output) | ||
|
||
average_drop_score = self.adcc.average_drop(saliency_map, class_idx, input_image, model_output) | ||
assert average_drop_score >= 0.2 | ||
|
||
coherency_score = self.adcc.coherency(saliency_map, class_idx, input_image) | ||
assert coherency_score >= 0.2 | ||
|
||
adcc_score = self.adcc(saliency_map, class_idx, input_image)["adcc"] | ||
assert adcc_score >= 0.4 | ||
|
||
def test_evaluate(self): | ||
input_images = [np.random.rand(224, 224, 3) for _ in range(5)] | ||
explanations = [ | ||
Explanation({0: np.random.rand(224, 224), 1: np.random.rand(224, 224)}, targets=[0, 1]) for _ in range(5) | ||
] | ||
|
||
adcc_score = self.adcc.evaluate(explanations, input_images)["adcc"] | ||
|
||
assert isinstance(adcc_score, float) | ||
assert 0 <= adcc_score <= 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters