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

[GSK-2569, GSK-2585, GSK-2658] Scan for computer vision #27

Merged
merged 73 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
1b3bb4a
ENH: add report generation for vision tasks
bmalezieux Jan 16, 2024
4d66288
UPD: add missing files
bmalezieux Jan 17, 2024
3804f1d
FIX: linting and format
bmalezieux Jan 17, 2024
70c3982
Merge branch 'main' into GSK-2569-report-gen
rabah-khalek Jan 17, 2024
d7702e2
UPD: change API for metrics
bmalezieux Jan 17, 2024
fe5425a
UPD: forgot to save the file
bmalezieux Jan 17, 2024
805f18f
UPD: add detectors in the package + example notebook
bmalezieux Jan 17, 2024
084247c
Merge branch 'GSK-2569-report-gen' of github.com:Giskard-AI/giskard-v…
bmalezieux Jan 17, 2024
c99216b
UPD: new base method for vision detectors, custom detectors only requ…
bmalezieux Jan 17, 2024
44f3925
FIX: add all facial parts in detector
bmalezieux Jan 17, 2024
6fe9c16
Merge branch 'main' into GSK-2569-report-gen
rabah-khalek Jan 17, 2024
00de9f9
added proposition with headpose estimation as example
rabah-khalek Jan 17, 2024
8921fc9
UPG: complete new API for detectors + add examples
bmalezieux Jan 18, 2024
cdbda72
FIX: remove useless init file
bmalezieux Jan 18, 2024
9ed8655
UPD: add decorators
bmalezieux Jan 18, 2024
5b86011
removed copied decorator.py
rabah-khalek Jan 18, 2024
0b89904
UPD: usenew scan from giskard
bmalezieux Jan 19, 2024
53ccccb
FIX: dependencies
bmalezieux Jan 19, 2024
786e614
FIX: notebook
bmalezieux Jan 19, 2024
a43568f
UPD: update info displayed in report
bmalezieux Jan 19, 2024
4e71c01
UPD: safe import + docstrings
bmalezieux Jan 19, 2024
21d4c53
small fix to glob imports
rabah-khalek Jan 19, 2024
01a1f6e
Merge pull request #29 from Giskard-AI/GSK-2569-report-gen-prop
rabah-khalek Jan 19, 2024
fb72c99
pdm format
rabah-khalek Jan 19, 2024
618eeda
TEST: add test for detectors
bmalezieux Jan 19, 2024
2fa1347
Merge branch 'GSK-2569-report-gen' of github.com:Giskard-AI/giskard-v…
bmalezieux Jan 19, 2024
63eb666
FIX: safe import in test
bmalezieux Jan 19, 2024
3f20b8a
UPD: new decorator for safe import
bmalezieux Jan 19, 2024
285a948
FIX: remove useless print
bmalezieux Jan 19, 2024
cd43f32
Update giskard_vision/landmark_detection/detectors/cropping_detector.py
bmalezieux Jan 22, 2024
0984fdf
Update giskard_vision/landmark_detection/detectors/cropping_detector.py
bmalezieux Jan 22, 2024
7663f2d
Update giskard_vision/landmark_detection/detectors/base.py
bmalezieux Jan 22, 2024
2c778ca
Update giskard_vision/landmark_detection/detectors/transformation_blu…
bmalezieux Jan 22, 2024
5cba860
Update giskard_vision/landmark_detection/detectors/transformation_col…
bmalezieux Jan 22, 2024
07ba80d
Update giskard_vision/landmark_detection/detectors/transformation_res…
bmalezieux Jan 22, 2024
4552db2
Update giskard_vision/landmark_detection/detectors/ethnicity_bias_det…
bmalezieux Jan 22, 2024
5045625
Update giskard_vision/landmark_detection/detectors/head_pose_detector.py
bmalezieux Jan 22, 2024
01f39a2
UPD: fix comments from review
bmalezieux Jan 22, 2024
e2f272c
ENH: add custom scan report for images
bmalezieux Jan 23, 2024
da43a39
FIX: format
bmalezieux Jan 23, 2024
ac9b268
UPD: fixed rgb + decreased size
bmalezieux Jan 23, 2024
a851683
UPD: display only one image instead of two for slicing
bmalezieux Jan 23, 2024
4a0ceb2
FIX: format
bmalezieux Jan 23, 2024
6d9f91d
WIP: try adding score sorting to display images
bmalezieux Jan 23, 2024
e48775c
FIX: selection of worst image working
bmalezieux Jan 24, 2024
202b440
UPD: comply with new changes in giskard + review comments
bmalezieux Jan 24, 2024
e5886e7
FIX: change to comply with new Issue API
bmalezieux Jan 24, 2024
20b2ffe
FIX: pdm files
bmalezieux Jan 24, 2024
24fc825
UPD: add custom warning messages
bmalezieux Jan 25, 2024
599cc17
Merge pull request #30 from Giskard-AI/GSK-2658-custom-scan
rabah-khalek Jan 25, 2024
3afb549
WIP: trying to display images in notebooks
bmalezieux Jan 25, 2024
b3cb393
FIX: tests
bmalezieux Jan 25, 2024
092e83c
UPD: embedding images into html so that they show up in notebooks
bmalezieux Jan 25, 2024
f9f8cc4
Merge branch 'main' into GSK-2569-report-gen
rabah-khalek Jan 26, 2024
c857c5c
UPD: add html file for all 300w
bmalezieux Jan 26, 2024
99812a9
Merge branch 'GSK-2569-report-gen' of github.com:Giskard-AI/giskard-v…
bmalezieux Jan 26, 2024
0e8c75f
FIX: bug in merge
bmalezieux Jan 26, 2024
3a02f9d
UPD: results for 300w indoor
bmalezieux Jan 26, 2024
ec75712
FIX: remove marks for ffhq
bmalezieux Jan 26, 2024
662b287
Merge branch 'main' into GSK-2569-report-gen
rabah-khalek Jan 26, 2024
d15268d
Merge branch 'main' into GSK-2569-report-gen
bmalezieux Jan 29, 2024
b58137d
UPD: fix review comments + add scan API
bmalezieux Jan 29, 2024
f104192
FIX: review
bmalezieux Jan 29, 2024
40d9e9f
FIX: adding typings to scan
bmalezieux Jan 29, 2024
9929b28
FIX: linting
bmalezieux Jan 29, 2024
64daa13
FIX: remove rank_data from several metrics
bmalezieux Jan 30, 2024
2b214a8
FIX: reviews
bmalezieux Jan 31, 2024
b533052
Regenerating pdm.lock
Jan 31, 2024
477a958
FIX: format
bmalezieux Jan 31, 2024
3fb6ffe
FIX: import in scanner.__init__.py
bmalezieux Jan 31, 2024
9369573
FIX: change variable name dataloader -> dataset
bmalezieux Jan 31, 2024
bc410cb
Update giskard_vision/__init__.py
rabah-khalek Jan 31, 2024
636dae1
FIX: thresholds + other small fixes from reviews
bmalezieux Jan 31, 2024
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
996 changes: 885 additions & 111 deletions examples/landmark_detection/example_scan.ipynb

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions examples/landmark_detection/example_scan.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# %%
from pathlib import Path

from giskard_vision.landmark_detection.models.wrappers import OpenCVWrapper
from giskard_vision.landmark_detection.dataloaders.loaders import DataLoader300W

from giskard_vision.scanner.scanner import Scanner
from giskard_vision.scanner import scan

# %%
model = OpenCVWrapper()
dl_ref = DataLoader300W(dir_path=str(Path(__file__).parent / "300W/sample"))
dl_ref = DataLoader300W(dir_path="./datasets/300W/sample/")

results = scan(model, dl_ref)

scanner = Scanner()
results = scanner.analyze(model, dl_ref)

# %%

results.to_html(filename="example_vision.html")
results.to_html(filename="example_vision_300w.html")
# %%

# %%
47 changes: 24 additions & 23 deletions giskard_vision/detectors/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ class DetectorVisionBase:
"""

group: str
warning_messages: dict = {}
threshold: Optional[float] = 0.1
warning_messages: dict
issue_level_threshold: float = 0.2
deviation_threshold: float = 0.05

def run(
self,
Expand Down Expand Up @@ -101,30 +102,30 @@ def get_issues(

from .example_manager import ImagesExampleManager

if issue_levels is None:
issue_levels = (IssueLevel.MAJOR, IssueLevel.MEDIUM)

for result in results:
if result.issue_level in issue_levels:
issues.append(
Issue(
model,
dataset,
level=result.issue_level,
slicing_fn=result.name,
group=IssueGroup(
result.group,
self.warning_messages[result.group] if result.group in self.warning_messages else "",
),
meta=result.get_meta_required(),
scan_examples=ImagesExampleManager(result.filename_examples, embed=embed),
display_footer_info=False,
)
)

except (ImportError, ModuleNotFoundError) as e:
raise GiskardImportError(["giskard"]) from e

if issue_levels is None:
issue_levels = (IssueLevel.MAJOR, IssueLevel.MEDIUM)

for result in results:
if result.issue_level in issue_levels:
issues.append(
Issue(
model,
dataset,
level=result.issue_level,
slicing_fn=result.name,
group=IssueGroup(
result.group,
self.warning_messages[result.group] if result.group in self.warning_messages else "",
),
meta=result.get_meta_required(),
scan_examples=ImagesExampleManager(result.filename_examples, embed=embed),
display_footer_info=False,
)
)

return issues

@abstractmethod
Expand Down
22 changes: 12 additions & 10 deletions giskard_vision/landmark_detection/detectors/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@

from giskard_vision.detectors.base import DetectorVisionBase, ScanResult
from giskard_vision.landmark_detection.tests.base import TestDiff
from giskard_vision.landmark_detection.tests.performance import NMEMean, NMEs
from giskard_vision.landmark_detection.tests.performance import NMEMean
from giskard_vision.utils.errors import GiskardImportError

WARNING_MESSAGES: dict = {
"Cropping": "Cropping involves evaluating the landmark detection model on specific face areas.",
"Ethical": "The data are filtered by ethnicity to detect ethical biases in the landmark detection model.",
"Head Pose": "The data are filtered by head pose to detect biases in the landmark detection model.",
"Robustness": "Images from the dataset are blurred, recolored and resized to test the robustness of the model to transformations.",
}


class LandmarkDetectionBaseDetector(DetectorVisionBase):
"""
Expand All @@ -27,12 +34,7 @@ class LandmarkDetectionBaseDetector(DetectorVisionBase):
Convert TestResult to ScanResult
"""

warning_messages: dict = {
"Cropping": "Cropping involves evaluating the landmark detection model on specific face areas.",
"Ethical": "The data are filtered by ethnicity to detect ethical biases in the landmark detection model.",
"Head Pose": "The data are filtered by head pose to detect biases in the landmark detection model.",
"Robustness": "Images from the dataset are blurred, recolored and resized to test the robustness of the model to transformations.",
}
warning_messages: dict = WARNING_MESSAGES

@abstractmethod
def get_dataloaders(self, dataset: Any) -> Sequence[Any]:
Expand All @@ -43,7 +45,7 @@ def get_results(self, model: Any, dataset: Any) -> Sequence[ScanResult]:

results = []
for dl in dataloaders:
test_result = TestDiff(metric=NMEMean, threshold=1, metric_for_examples=NMEs).run(
test_result = TestDiff(metric=NMEMean, threshold=1).run(
model=model,
dataloader=dl,
dataloader_ref=dataset,
Expand Down Expand Up @@ -81,9 +83,9 @@ def get_scan_result(self, test_result, filename_examples, name, size_data) -> Sc

relative_delta = (test_result.metric_value_test - test_result.metric_value_ref) / test_result.metric_value_ref

if relative_delta > self.threshold:
if relative_delta > self.issue_level_threshold:
issue_level = IssueLevel.MAJOR
elif relative_delta > 0:
elif relative_delta > self.deviation_threshold:
issue_level = IssueLevel.MEDIUM
else:
issue_level = IssueLevel.MINOR
Expand Down
20 changes: 16 additions & 4 deletions giskard_vision/landmark_detection/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ def definition(prediction_result: PredictionResult, marks: np.ndarray, **kwargs)
"""
...

@staticmethod
@abstractmethod
def rank_data(prediction_result: PredictionResult, marks: np.ndarray, **kwargs) -> List[int]:
"""Abstract method to define how the mtric ranks data samples from worse to best

Args:
prediction_result (PredictionResult): The prediction result to evaluate.
marks (np.ndarray): Ground truth facial landmarks.

Returns:
List[int]: Indexes of data samples from worse to best
"""
...

@classmethod
def validation(cls, prediction_result: PredictionResult, marks: np.ndarray, **kwargs) -> None:
"""Validate the input types for the metric calculation.
Expand Down Expand Up @@ -265,7 +279,6 @@ class TestDiff:
metric: Metric
threshold: float
relative: bool = True
metric_for_examples: Optional[Metric] = None

def run(
self,
Expand Down Expand Up @@ -301,9 +314,8 @@ def run(
ground_truth_ref = dataloader_ref.all_marks
metric_value_ref = self.metric.get(prediction_result_ref, ground_truth_ref)

if self.metric_for_examples is not None:
metrics_examples = self.metric_for_examples.get(prediction_result, ground_truth)
indexes = sorted(range(len(metrics_examples)), key=metrics_examples.__getitem__)[::-1]
if hasattr(self.metric, "rank_data"):
indexes = self.metric.rank_data(prediction_result, ground_truth)
else:
indexes = None

Expand Down
6 changes: 6 additions & 0 deletions giskard_vision/landmark_detection/tests/performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ class NMEMean(Metric):
def definition(prediction_result: PredictionResult, marks: np.ndarray):
return np.nanmean(NMEs.get(prediction_result, marks))

@staticmethod
def rank_data(prediction_result: PredictionResult, marks: np.ndarray):
predictions = NMEs.get(prediction_result, marks)
indexes = sorted(range(len(predictions)), key=predictions.__getitem__)[::-1]
return indexes


@dataclass
class NMEStd(Metric):
Expand Down
49 changes: 49 additions & 0 deletions giskard_vision/scanner/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from typing import Any, List, Optional

from .scanner import Scanner


def scan(
model,
dataset,
detectors: Optional[List[Any]] = None,
params=None,
only=None,
verbose=True,
raise_exceptions=False,
):
"""Automatically detects model vulnerabilities.

See :class:`Scanner` for more details.

Parameters
----------
model : BaseModel
A Giskard model object.
dataset : Dataset
A Giskard dataset object.
detectors : List[Any]
A list of detectors to use for the scan. If not specified, all detectors that correspond to the model type will be used.
params : dict
Advanced scanner configuration. See :class:`Scanner` for more details.
only : list
A tag list to limit the scan to a subset of detectors. For example,
``giskard.scan(model, dataset, only=["performance"])`` will only run detectors for performance issues.
verbose : bool
Whether to print detailed info messages. Enabled by default.
raise_exceptions : bool
Whether to raise an exception if detection errors are encountered. By default, errors are logged and
handled gracefully, without interrupting the scan.

Returns
-------
ScanReport
A scan report object containing the results of the scan.
"""
scanner = Scanner(params, only=only)
return scanner.analyze(
model, dataset=dataset, detectors=detectors, verbose=verbose, raise_exceptions=raise_exceptions
)


__all__ = ["scan", "Scanner"]
9 changes: 7 additions & 2 deletions giskard_vision/scanner/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
from time import perf_counter
from typing import Any, Optional, Sequence

from giskard.scanner.registry import DetectorRegistry
from giskard.scanner.report import ScanReport
from ..utils.errors import GiskardImportError

try:
from giskard.scanner.registry import DetectorRegistry
from giskard.scanner.report import ScanReport
except (ImportError, ModuleNotFoundError) as e:
raise GiskardImportError(["giskard"]) from e


def warning(content: str):
Expand Down
Loading