Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make flexible naming in explanation.save #51

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* Try CNN -> ViT assumption for IR insertion by @goodsong81 in https://github.com/openvinotoolkit/openvino_xai/pull/48
* Enable AISE: Adaptive Input Sampling for Explanation of Black-box Models by @negvet in https://github.com/openvinotoolkit/openvino_xai/pull/49
* Upgrade OpenVINO to 2024.3.0 by @goodsong81 in https://github.com/openvinotoolkit/openvino_xai/pull/52
* Make the naming of saved saliency maps more flexible and return confidence scores by @GalyaZalesskaya in https://github.com/openvinotoolkit/openvino_xai/pull/51

### Known Issues

Expand Down
93 changes: 89 additions & 4 deletions docs/source/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ Content:
- [OpenVINO™ Explainable AI Toolkit User Guide](#openvino-explainable-ai-toolkit-user-guide)
- [OpenVINO XAI Architecture](#openvino-xai-architecture)
- [`Explainer`: the main interface to XAI algorithms](#explainer-the-main-interface-to-xai-algorithms)
- [Create Explainer for OpenVINO Model instance](#create-explainer-for-openvino-model-instance)
- [Create Explainer from OpenVINO IR file](#create-explainer-from-openvino-ir-file)
- [Create Explainer from ONNX model file](#create-explainer-from-onnx-model-file)
- [Basic usage: Auto mode](#basic-usage-auto-mode)
- [Running without `preprocess_fn`](#running-without-preprocess_fn)
- [Specifying `preprocess_fn`](#specifying-preprocess_fn)
- [White-Box mode](#white-box-mode)
- [Black-Box mode](#black-box-mode)
- [XAI insertion (white-box usage)](#xai-insertion-white-box-usage)
- [Saving saliency maps](#saving-saliency-maps)
- [Example scripts](#example-scripts)


Expand Down Expand Up @@ -96,13 +100,13 @@ Here's the example how we can avoid passing `preprocess_fn` by preprocessing dat
```python
import cv2
import numpy as np
from typing import Mapping
import openvino.runtime as ov
from openvino.runtime.utils.data_helpers.wrappers import OVDict

import openvino_xai as xai


def postprocess_fn(x: OVDict):
def postprocess_fn(x: Mapping):
# Implementing our own post-process function based on the model's implementation
# Return "logits" model output
return x["logits"]
Expand Down Expand Up @@ -142,8 +146,8 @@ explanation.save("output_path", "name")
```python
import cv2
import numpy as np
from typing import Mapping
import openvino.runtime as ov
from openvino.runtime.utils.data_helpers.wrappers import OVDict

import openvino_xai as xai

Expand All @@ -154,7 +158,7 @@ def preprocess_fn(x: np.ndarray) -> np.ndarray:
x = np.expand_dims(x, 0)
return x

def postprocess_fn(x: OVDict):
def postprocess_fn(x: Mapping):
# Implementing our own post-process function based on the model's implementation
# Return "logits" model output
return x["logits"]
Expand Down Expand Up @@ -327,6 +331,87 @@ model_xai = xai.insert_xai(
# ***** Downstream task: user's code that infers model_xai and picks 'saliency_map' output *****
```

## Saving saliency maps

You can easily save saliency maps with flexible naming options, including image name as prefix, so saliency maps from the same image will start the same, target prefix, terget suffix.

For the name `image_name_target_aeroplane.jpg`:
- image_name_prefix = `image_name`,
- target_prefix = `target`,
- label name = `aeroplane`,
- target_suffix = ``.

Additionally, you can include the confidence score for each class in the saved saliency map's name.

```python
import cv2
import numpy as np
import openvino.runtime as ov
from typing import Mapping
import openvino_xai as xai

def preprocess_fn(image: np.ndarray) -> np.ndarray:
"""Preprocess the input image."""
resized_image = cv2.resize(src=image, dsize=(224, 224))
expanded_image = np.expand_dims(resized_image, 0)
return expanded_image

def postprocess_fn(output: Mapping):
"""Postprocess the model output."""
return output["logits"]

# Generate and process saliency maps (as many as required, sequentially)
image = cv2.imread("path/to/image.jpg")

# Create ov.Model
MODEL_PATH = "path/to/model.xml"
model = ov.Core().read_model(MODEL_PATH) # type: ov.Model

# Get predicted confidences for the image
compiled_model = core.compile_model(model=model, device_name="AUTO")
logits = compiled_model([preprocess_fn(image)])[0]
postprocessed_logits = postprocess_fn(logits)[0]
result_index = np.argmax(postprocessed_logits)
result_scores = postprocessed_logits[result_index]

# Generate dict {class_index: confidence} to save saliency maps
scores_dict = {i: score for i, score in enumerate(result_scores)}

# The Explainer object will prepare and load the model once in the beginning
explainer = xai.Explainer(
model,
task=xai.Task.CLASSIFICATION,
preprocess_fn=preprocess_fn,
)

voc_labels = [
'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable',
'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor'
]

# Run explanation
explanation = explainer(
image,
explain_mode=ExplainMode.WHITEBOX,
label_names=voc_labels,
target_explain_labels=1, # target classes to explain
)

# Save saliency maps flexibly
OUTPUT_PATH = "output_path"
explanation.save(OUTPUT_PATH) # target_aeroplane.jpg
explanation.save(OUTPUT_PATH, "image_name") # image_name_target_aeroplane.jpg
explanation.save(OUTPUT_PATH, image_name_prefix="image_name") # image_name_target_aeroplane.jpg

# Avoid "target" in salinecy map names
explanation.save(OUTPUT_PATH, target_prefix="") # aeroplane.jpg
explanation.save(OUTPUT_PATH, target_prefix="", target_suffix="class") # aeroplane_class.jpg
explanation.save(OUTPUT_PATH, image_name_prefix="image_name", target_prefix="") # image_name_aeroplane.jpg

# Save saliency maps with confidence scores
explanation.save(OUTPUT_PATH, target_suffix="conf", confidence_scores=scores_dict) # target_aeroplane_conf_0.92.jpg```
```


## Example scripts

Expand Down
65 changes: 55 additions & 10 deletions openvino_xai/explainer/explanation.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,21 +132,66 @@ def _select_target_indices(
raise ValueError("Provided targer index {targer_index} is not available among saliency maps.")
return target_indices

def save(self, dir_path: Path | str, name: str | None = None) -> None:
"""Dumps saliency map."""
def save(
self,
dir_path: Path | str,
image_name_prefix: str | None = "",
target_prefix: str | None = "target",
target_suffix: str | None = "",
confidence_scores: Dict[int, float] | None = None,
) -> None:
"""
Dumps saliency map images to the specified directory.

Allows flexibly name the files with the image_name_prefix, target_prefix, and target_suffix.
For the name 'image_name_target_aeroplane.jpg': prefix = 'image_name',
target_prefix = 'target', label name = 'aeroplane', target_suffix = ''.

save(output_dir) -> target_aeroplane.jpg
save(output_dir, image_name_prefix="test_map", target_prefix="") -> test_map_aeroplane.jpg
save(output_dir, image_name_prefix="test_map") -> test_map_target_aeroplane.jpg
save(output_dir, target_suffix="conf", confidence_scores=scores) -> target_aeroplane_conf_0.92.jpg

Parameters:
:param dir_path: The directory path where the saliency maps will be saved.
:type dir_path: Path | str
:param image_name_prefix: Optional prefix for the file names. Default is an empty string.
:type image_name_prefix: str | None
:param target_prefix: Optional suffix for the target. Default is "target".
:type target_prefix: str | None
:param target_suffix: Optional suffix for the saliency map name. Default is an empty string.
:type target_suffix: str | None
:param confidence_scores: Dict with confidence scores for each class to saliency maps with them1 Default is None.
:type confidence_scores: Dict[int, float] | None

"""

os.makedirs(dir_path, exist_ok=True)
save_name = name if name else ""

image_name_prefix = f"{image_name_prefix}_" if image_name_prefix != "" else image_name_prefix
target_suffix = f"_{target_suffix}" if target_suffix != "" else target_suffix
template = f"{{image_name_prefix}}{{target_prefix}}{{target_name}}{target_suffix}.jpg"

target_prefix = f"{target_prefix}_" if target_prefix != "" else target_prefix
for cls_idx, map_to_save in self._saliency_map.items():
map_to_save = cv2.cvtColor(map_to_save, code=cv2.COLOR_RGB2BGR)
if isinstance(cls_idx, str):
cv2.imwrite(os.path.join(dir_path, f"{save_name}.jpg"), img=map_to_save)
return
target_name = ""
if target_prefix == "target_":
# Default activation map suffix
target_prefix = "activation_map"
elif target_prefix == "":
# Remove the underscore in case of empty suffix
image_name_prefix = image_name_prefix[:-1] if image_name_prefix.endswith("_") else image_name_prefix
else:
if self.label_names:
target_name = self.label_names[cls_idx]
else:
target_name = str(cls_idx)
image_name = f"{save_name}_target_{target_name}.jpg" if save_name else f"target_{target_name}.jpg"
target_name = self.label_names[cls_idx] if self.label_names else str(cls_idx)
if confidence_scores:
class_confidence = confidence_scores[cls_idx]
target_name = f"{target_name}_{class_confidence:.2f}"

image_name = template.format(
image_name_prefix=image_name_prefix, target_prefix=target_prefix, target_name=target_name
)
cv2.imwrite(os.path.join(dir_path, image_name), img=map_to_save)


Expand Down
7 changes: 3 additions & 4 deletions openvino_xai/methods/black_box/aise.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@

import collections
import math
from typing import Callable, Dict, List, Tuple
from typing import Callable, Dict, List, Mapping, Tuple

import numpy as np
import openvino.runtime as ov
from openvino.runtime.utils.data_helpers.wrappers import OVDict
from scipy.optimize import Bounds, direct

from openvino_xai.common.utils import (
Expand All @@ -28,7 +27,7 @@ class AISE(BlackBoxXAIMethod):
:param model: OpenVINO model.
:type model: ov.Model
:param postprocess_fn: Post-processing function that extract scores from IR model output.
:type postprocess_fn: Callable[[OVDict], np.ndarray]
:type postprocess_fn: Callable[[Mapping], np.ndarray]
:param preprocess_fn: Pre-processing function, identity function by default
(assume input images are already preprocessed by user).
:type preprocess_fn: Callable[[np.ndarray], np.ndarray]
Expand All @@ -41,7 +40,7 @@ class AISE(BlackBoxXAIMethod):
def __init__(
self,
model: ov.Model,
postprocess_fn: Callable[[OVDict], np.ndarray],
postprocess_fn: Callable[[Mapping], np.ndarray],
preprocess_fn: Callable[[np.ndarray], np.ndarray] = IdentityPreprocessFN(),
device_name: str = "CPU",
prepare_model: bool = True,
Expand Down
2 changes: 1 addition & 1 deletion openvino_xai/methods/black_box/rise.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class RISE(BlackBoxXAIMethod):
:param model: OpenVINO model.
:type model: ov.Model
:param postprocess_fn: Post-processing function that extract scores from IR model output.
:type postprocess_fn: Callable[[OVDict], np.ndarray]
:type postprocess_fn: Callable[[Mapping], np.ndarray]
:param preprocess_fn: Pre-processing function, identity function by default
(assume input images are already preprocessed by user).
:type preprocess_fn: Callable[[np.ndarray], np.ndarray]
Expand Down
18 changes: 16 additions & 2 deletions tests/unit/explanation/test_explanation.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def test_save(self, tmp_path):
save_path = tmp_path / "saliency_maps"

explanation = self._get_explanation()
explanation.save(save_path, "test_map")
explanation.save(save_path, image_name_prefix="test_map")
assert os.path.isfile(save_path / "test_map_target_aeroplane.jpg")
assert os.path.isfile(save_path / "test_map_target_bird.jpg")

Expand All @@ -55,8 +55,22 @@ def test_save(self, tmp_path):
assert os.path.isfile(save_path / "test_map_target_0.jpg")
assert os.path.isfile(save_path / "test_map_target_2.jpg")

explanation = self._get_explanation()
explanation.save(save_path, target_prefix="", target_suffix="map")
assert os.path.isfile(save_path / "aeroplane_map.jpg")
assert os.path.isfile(save_path / "bird_map.jpg")

explanation = self._get_explanation()
explanation.save(save_path, target_suffix="conf", confidence_scores={0: 0.92, 2: 0.85})
assert os.path.isfile(save_path / "target_aeroplane_0.92_conf.jpg")
assert os.path.isfile(save_path / "target_bird_0.85_conf.jpg")

explanation = self._get_explanation(saliency_maps=SALIENCY_MAPS_IMAGE, label_names=None)
explanation.save(save_path, "test_map")
explanation.save(save_path, image_name_prefix="test_map")
assert os.path.isfile(save_path / "test_map_activation_map.jpg")

explanation = self._get_explanation(saliency_maps=SALIENCY_MAPS_IMAGE, label_names=None)
explanation.save(save_path, image_name_prefix="test_map", target_prefix="")
assert os.path.isfile(save_path / "test_map.jpg")

def _get_explanation(self, saliency_maps=SALIENCY_MAPS, label_names=VOC_NAMES):
Expand Down