Skip to content

Make flexible naming in explanation.save #51

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

Merged
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -15,6 +15,7 @@
* Support OV IR / ONNX model file for Explainer by @goodsong81 in https://github.com/openvinotoolkit/openvino_xai/pull/47
* 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
* 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
76 changes: 76 additions & 0 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 @@ -327,6 +331,78 @@ 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 prefix, suffix, and postfix. 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 openvino.runtime.utils.data_helpers.wrappers import OVDict
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: OVDict):
"""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, "image_name") # image_name_target_aeroplane.jpg
explanation.save(OUTPUT_PATH, prefix_name="image_name") # image_name_target_aeroplane.jpg
explanation.save(OUTPUT_PATH) # target_aeroplane.jpg
# Avoid "target" in names
explanation.save(OUTPUT_PATH, prefix_name="image_name", target_suffix="") # image_name_aeroplane.jpg
explanation.save(OUTPUT_PATH, target_suffix="", postfix_name="class") # aeroplane_class.jpg
explanation.save(OUTPUT_PATH, target_suffix="") # aeroplane.jpg

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


## Example scripts

Expand Down
60 changes: 50 additions & 10 deletions openvino_xai/explainer/explanation.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,21 +132,61 @@ 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,
prefix_name: str | None = "",
suffix_name: str | None = "target",
postfix_name: 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 prefix, suffix, and postfix.
save(output_dir) -> target_aeroplane.jpg
save(output_dir, prefix_name="test_map", target_suffix="") -> test_map_aeroplane.jpg
save(output_dir, prefix_name="test_map") -> test_map_target_aeroplane.jpg
save(output_dir, postfix_name="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 prefix_name: Optional prefix for the file names. Default is an empty string.
:type prefix_name: str | None
:param suffix_name: Optional suffix for the target. Default is "target".
:type suffix_name: str | None
:param postfix_name: Optional postfix for the file names. Default is an empty string.
:type postfix_name: 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 ""

prefix_name = f"{prefix_name}_" if prefix_name != "" else prefix_name
postfix_name = f"_{postfix_name}" if postfix_name != "" else postfix_name
template = f"{{prefix_name}}{{suffix_name}}{{target_name}}{postfix_name}.jpg"

suffix_name = f"{suffix_name}_" if suffix_name != "" else suffix_name
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 suffix_name == "target_":
# Default activation map suffix
suffix_name = "activation_map"
elif suffix_name == "":
# Remove the underscore in case of empty suffix
prefix_name = prefix_name[:-1] if prefix_name.endswith("_") else prefix_name
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(prefix_name=prefix_name, suffix_name=suffix_name, target_name=target_name)
cv2.imwrite(os.path.join(dir_path, image_name), img=map_to_save)


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, prefix_name="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, suffix_name="", postfix_name="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, postfix_name="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, prefix_name="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, prefix_name="test_map", suffix_name="")
assert os.path.isfile(save_path / "test_map.jpg")

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