patra-toolkit 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- patra_toolkit/__init__.py +3 -0
- patra_toolkit/fairlearn_bias.py +32 -0
- patra_toolkit/patra_model_card.py +376 -0
- patra_toolkit/schema/schema.json +274 -0
- patra_toolkit/shap_xai.py +37 -0
- patra_toolkit-0.1.1.dist-info/LICENSE +28 -0
- patra_toolkit-0.1.1.dist-info/METADATA +16 -0
- patra_toolkit-0.1.1.dist-info/RECORD +14 -0
- patra_toolkit-0.1.1.dist-info/WHEEL +5 -0
- patra_toolkit-0.1.1.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_patra_hash_id.py +116 -0
- tests/test_patra_inout_data.py +112 -0
- tests/test_patra_mc.py +159 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from fairlearn.metrics import demographic_parity_difference, equalized_odds_difference
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BiasAnalyzer:
|
|
5
|
+
"""
|
|
6
|
+
Provides automated capture of fairness metrics in a given AI workflow.
|
|
7
|
+
"""
|
|
8
|
+
def __init__(self, dataset, true_labels, predicted_labels, sensitive_feature_name, sensitive_feature_data, model):
|
|
9
|
+
self.dataset = dataset
|
|
10
|
+
self.true_labels = true_labels
|
|
11
|
+
self.predicted_labels = predicted_labels
|
|
12
|
+
self.sensitive_feature = sensitive_feature_name
|
|
13
|
+
self.sensitive_data = sensitive_feature_data
|
|
14
|
+
self.model = model
|
|
15
|
+
|
|
16
|
+
def calculate_bias_metrics(self):
|
|
17
|
+
"""
|
|
18
|
+
Calculate bias metrics.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
None
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
dict: A dictionary of bias
|
|
25
|
+
"""
|
|
26
|
+
demographic_parity = demographic_parity_difference(self.true_labels, self.predicted_labels,
|
|
27
|
+
sensitive_features=self.sensitive_data)
|
|
28
|
+
equal_odds_diff = equalized_odds_difference(self.true_labels, self.predicted_labels,
|
|
29
|
+
sensitive_features=self.sensitive_data)
|
|
30
|
+
|
|
31
|
+
return {"demographic_parity_diff": demographic_parity,
|
|
32
|
+
"equal_odds_difference": equal_odds_diff}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import os.path
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from json import JSONEncoder
|
|
6
|
+
from typing import List, Optional, Dict
|
|
7
|
+
|
|
8
|
+
import jsonschema
|
|
9
|
+
import pkg_resources
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from .fairlearn_bias import BiasAnalyzer
|
|
13
|
+
from .shap_xai import ExplainabilityAnalyser
|
|
14
|
+
|
|
15
|
+
SCHEMA_JSON = os.path.join(os.path.dirname(__file__), 'schema', 'schema.json')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Metric:
|
|
20
|
+
"""
|
|
21
|
+
Data class for storing metric key-value pairs.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
key (str): The name of the metric.
|
|
25
|
+
value (str): The value of the metric.
|
|
26
|
+
"""
|
|
27
|
+
key: str
|
|
28
|
+
value: str
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class AIModel:
|
|
32
|
+
"""
|
|
33
|
+
Represents and stores AI model metadata and its performance metrics.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
name (str): The name of the model.
|
|
37
|
+
version (str): The version identifier of the model.
|
|
38
|
+
description (str): A detailed description of the model.
|
|
39
|
+
owner (str): The owner of the model.
|
|
40
|
+
location (str): The file path or URL where the model is stored.
|
|
41
|
+
license (str): The license under which the model is distributed.
|
|
42
|
+
framework (str): The framework used to build the model (e.g., TensorFlow, PyTorch).
|
|
43
|
+
model_type (str): The type of model (e.g., classifier, regressor).
|
|
44
|
+
test_accuracy (str): The accuracy of the model on a test dataset.
|
|
45
|
+
model_structure (str): The structure of the model as a dictionary (optional).
|
|
46
|
+
metrics (str): A dictionary storing performance metrics for the model.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
.. code-block:: python
|
|
50
|
+
|
|
51
|
+
ai_model = AIModel(
|
|
52
|
+
name="Model Name",
|
|
53
|
+
version="1.0",
|
|
54
|
+
description="Model description",
|
|
55
|
+
owner="Model owner",
|
|
56
|
+
location="Model location",
|
|
57
|
+
license="Model license",
|
|
58
|
+
framework="Model framework",
|
|
59
|
+
model_type="Model type",
|
|
60
|
+
test_accuracy=0.95,
|
|
61
|
+
model_structure={},
|
|
62
|
+
metrics={"accuracy": "0.95"}
|
|
63
|
+
)
|
|
64
|
+
"""
|
|
65
|
+
name: str
|
|
66
|
+
version: str
|
|
67
|
+
description: str
|
|
68
|
+
owner: str
|
|
69
|
+
location: str
|
|
70
|
+
license: str
|
|
71
|
+
framework: str
|
|
72
|
+
model_type: str
|
|
73
|
+
test_accuracy: float
|
|
74
|
+
model_structure: Optional[object] = field(default_factory=dict)
|
|
75
|
+
metrics: Dict[str, str] = field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
def add_metric(self, key: str, value: str) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Adds a performance metric to the model's metrics.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
key (str): The name of the metric.
|
|
83
|
+
value (str): The value of the metric.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
None
|
|
87
|
+
"""
|
|
88
|
+
self.metrics[key] = value
|
|
89
|
+
|
|
90
|
+
def remove_nulls(self, model_structure):
|
|
91
|
+
"""
|
|
92
|
+
Recursively removes null values from the model structure.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
model_structure (object): The model structure as a dictionary or list.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
object: Model structure with null values removed.
|
|
99
|
+
"""
|
|
100
|
+
if isinstance(model_structure, dict):
|
|
101
|
+
return {k: self.remove_nulls(v) for k, v in model_structure.items() if v is not None}
|
|
102
|
+
elif isinstance(model_structure, list):
|
|
103
|
+
return [self.remove_nulls(v) for v in model_structure if v is not None]
|
|
104
|
+
return model_structure
|
|
105
|
+
|
|
106
|
+
def populate_model_structure(self, trained_model):
|
|
107
|
+
"""
|
|
108
|
+
Populates the `model_structure` attribute from a trained model object.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
trained_model (object): A trained machine learning model object.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
None
|
|
115
|
+
"""
|
|
116
|
+
if self.framework == 'tensorflow':
|
|
117
|
+
json_structure = json.loads(trained_model.to_json())
|
|
118
|
+
self.model_structure = self.remove_nulls(json_structure)
|
|
119
|
+
else:
|
|
120
|
+
self.model_structure = {}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class BiasAnalysis:
|
|
125
|
+
"""
|
|
126
|
+
Class to store results from bias analysis.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
demographic_parity_difference (float): The difference in demographic parity between groups.
|
|
130
|
+
equal_odds_difference (float): The difference in equal odds between groups.
|
|
131
|
+
"""
|
|
132
|
+
demographic_parity_difference: float
|
|
133
|
+
equal_odds_difference: float
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class ExplainabilityAnalysis:
|
|
138
|
+
"""
|
|
139
|
+
Class to store explainability metrics.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
name (str): Name of the explainability method used.
|
|
143
|
+
metrics (List[Metric]): List of metrics related to explainability analysis.
|
|
144
|
+
"""
|
|
145
|
+
name: str
|
|
146
|
+
metrics: List[Metric] = field(default_factory=list)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class ModelCard:
|
|
151
|
+
"""
|
|
152
|
+
Represents an AI model card to document model metadata, analyses, and requirements.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
name (str): The name of the model.
|
|
156
|
+
version (str): The model's version.
|
|
157
|
+
short_description (str): A brief description of the model.
|
|
158
|
+
full_description (str): A detailed description of the model.
|
|
159
|
+
keywords (str): Comma-separated keywords for searchability.
|
|
160
|
+
author (str): The model's creator or owner.
|
|
161
|
+
input_type (str): Type of input data (e.g., "Image", "Text").
|
|
162
|
+
category (str): The category of the model (e.g., "Classification", "Regression").
|
|
163
|
+
input_data (Optional[str]): Description of the model's input data.
|
|
164
|
+
output_data (Optional[str]): Description of the model's output data.
|
|
165
|
+
foundational_model (Optional[str]): Reference to any foundational model used.
|
|
166
|
+
ai_model (Optional[AIModel]): An instance of `AIModel` containing model details.
|
|
167
|
+
bias_analysis (Optional[BiasAnalysis]): Instance of `BiasAnalysis` containing bias metrics.
|
|
168
|
+
xai_analysis (Optional[ExplainabilityAnalysis]): Instance of `ExplainabilityAnalysis` with interpretability metrics.
|
|
169
|
+
model_requirements (Optional[List[str]]): List of required packages and dependencies.
|
|
170
|
+
id (Optional[str]): Unique identifier for the model card, generated upon submission.
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
.. code-block:: python
|
|
174
|
+
|
|
175
|
+
model_card = ModelCard(
|
|
176
|
+
name="Model Name",
|
|
177
|
+
version="1.0",
|
|
178
|
+
short_description="A brief description",
|
|
179
|
+
full_description="A detailed description of the model's purpose and usage.",
|
|
180
|
+
keywords="classification, AI, image processing",
|
|
181
|
+
author="Author Name",
|
|
182
|
+
input_type="Image",
|
|
183
|
+
category="Classification",
|
|
184
|
+
input_data="Images of size 28x28.",
|
|
185
|
+
output_data="Prediction probabilities for classes.",
|
|
186
|
+
foundational_model="Base Model Reference",
|
|
187
|
+
ai_model=AIModel(
|
|
188
|
+
name="Model Name",
|
|
189
|
+
version="1.0",
|
|
190
|
+
description="Detailed model description",
|
|
191
|
+
owner="Model owner",
|
|
192
|
+
location="Storage location",
|
|
193
|
+
license="MIT",
|
|
194
|
+
framework="TensorFlow",
|
|
195
|
+
model_type="Classifier",
|
|
196
|
+
test_accuracy=0.95,
|
|
197
|
+
model_structure={},
|
|
198
|
+
metrics={"accuracy": "0.95"}
|
|
199
|
+
),
|
|
200
|
+
bias_analysis=BiasAnalysis(
|
|
201
|
+
demographic_parity_difference=0.05,
|
|
202
|
+
equal_odds_difference=0.1
|
|
203
|
+
),
|
|
204
|
+
xai_analysis=ExplainabilityAnalysis(
|
|
205
|
+
name="SHAP",
|
|
206
|
+
metrics=[Metric(key="Feature A", value="0.1")]
|
|
207
|
+
),
|
|
208
|
+
model_requirements=["numpy>=1.19.2", "tensorflow>=2.4.1"]
|
|
209
|
+
)
|
|
210
|
+
"""
|
|
211
|
+
name: str
|
|
212
|
+
version: str
|
|
213
|
+
short_description: str
|
|
214
|
+
full_description: str
|
|
215
|
+
keywords: str
|
|
216
|
+
author: str
|
|
217
|
+
input_type: str
|
|
218
|
+
category: str
|
|
219
|
+
input_data: Optional[str] = ""
|
|
220
|
+
output_data: Optional[str] = ""
|
|
221
|
+
foundational_model: Optional[str] = ""
|
|
222
|
+
ai_model: Optional[AIModel] = None
|
|
223
|
+
bias_analysis: Optional[BiasAnalysis] = None
|
|
224
|
+
xai_analysis: Optional[ExplainabilityAnalysis] = None
|
|
225
|
+
model_requirements: Optional[List] = None
|
|
226
|
+
id: Optional[str] = field(init=False, default=None)
|
|
227
|
+
|
|
228
|
+
def __str__(self):
|
|
229
|
+
"""
|
|
230
|
+
Returns a JSON string representation of the model card.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
str: A JSON-formatted string representing the model card.
|
|
234
|
+
"""
|
|
235
|
+
return json.dumps(self.__dict__, cls=ModelCardJSONEncoder, indent=4, separators=(',', ': '))
|
|
236
|
+
|
|
237
|
+
def populate_bias(self, dataset, true_labels, predicted_labels, sensitive_feature_name, sensitive_feature_data, model):
|
|
238
|
+
"""
|
|
239
|
+
Calculates and stores fairness metrics.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
dataset (object): The dataset used for bias analysis.
|
|
243
|
+
true_labels (list): The ground truth labels.
|
|
244
|
+
predicted_labels (list): Model's predictions.
|
|
245
|
+
sensitive_feature_name (str): The name of the sensitive attribute.
|
|
246
|
+
sensitive_feature_data (list): Values for the sensitive feature.
|
|
247
|
+
model (object): The model being analyzed.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
None
|
|
251
|
+
"""
|
|
252
|
+
bias_analyzer = BiasAnalyzer(dataset, true_labels, predicted_labels, sensitive_feature_name,
|
|
253
|
+
sensitive_feature_data, model)
|
|
254
|
+
self.bias_analysis = bias_analyzer.calculate_bias_metrics()
|
|
255
|
+
|
|
256
|
+
def populate_xai(self, train_dataset, column_names, model, n_features=10):
|
|
257
|
+
"""
|
|
258
|
+
Computes and stores feature importance metrics.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
train_dataset (object): Training dataset used in the analysis.
|
|
262
|
+
column_names (list): Names of the features.
|
|
263
|
+
model (object): The model being explained.
|
|
264
|
+
n_features (int, optional): Number of features to analyze. Default is 10.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
None
|
|
268
|
+
"""
|
|
269
|
+
xai_analyzer = ExplainabilityAnalyser(train_dataset, column_names, model)
|
|
270
|
+
self.xai_analysis = xai_analyzer.calculate_xai_features(n_features)
|
|
271
|
+
|
|
272
|
+
def populate_requirements(self):
|
|
273
|
+
"""
|
|
274
|
+
Collects package requirements for the model card, excluding specific dependencies.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
None
|
|
278
|
+
"""
|
|
279
|
+
exclude_packages = {"shap", "fairlearn"}
|
|
280
|
+
installed_packages = pkg_resources.working_set
|
|
281
|
+
packages_list = sorted(["%s==%s" % (i.key, i.version) for i in installed_packages])
|
|
282
|
+
self.model_requirements = [pkg for pkg in packages_list if pkg.split("==")[0] not in exclude_packages]
|
|
283
|
+
|
|
284
|
+
def validate(self):
|
|
285
|
+
"""
|
|
286
|
+
Validates the model card against a predefined JSON schema.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
bool: True if the model card is valid according to the schema, False otherwise.
|
|
290
|
+
"""
|
|
291
|
+
mc_json = self.__str__()
|
|
292
|
+
try:
|
|
293
|
+
with open(SCHEMA_JSON, 'r') as schema_file:
|
|
294
|
+
schema = json.load(schema_file)
|
|
295
|
+
jsonschema.validate(instance=json.loads(mc_json), schema=schema)
|
|
296
|
+
return True
|
|
297
|
+
except jsonschema.ValidationError as e:
|
|
298
|
+
print(e.message)
|
|
299
|
+
return False
|
|
300
|
+
except Exception as e:
|
|
301
|
+
print(f"An unexpected error occurred: {e}")
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
def submit(self, patra_server_url):
|
|
305
|
+
"""
|
|
306
|
+
Validates and submits the model card to the specified Patra server.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
patra_server_url (str): The Patra server URL where the model card should be submitted.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
dict: The server's response as a JSON object.
|
|
313
|
+
"""
|
|
314
|
+
if self.validate():
|
|
315
|
+
try:
|
|
316
|
+
self.id = self._get_hash_id(patra_server_url)
|
|
317
|
+
patra_submit_url = f"{patra_server_url}/upload_mc"
|
|
318
|
+
headers = {'Content-Type': 'application/json'}
|
|
319
|
+
response = requests.post(patra_submit_url, json=json.loads(str(self)), headers=headers)
|
|
320
|
+
response.raise_for_status()
|
|
321
|
+
return response.json()
|
|
322
|
+
except requests.exceptions.RequestException as e:
|
|
323
|
+
print("The Patra Server cannot be reached. Please try again.")
|
|
324
|
+
return None
|
|
325
|
+
return {"An error occurred: valid patra_server_url not provided. Unable to upload."}
|
|
326
|
+
|
|
327
|
+
def _get_hash_id(self, patra_server_url):
|
|
328
|
+
"""
|
|
329
|
+
Generates a unique identifier for the model card based on its metadata.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
patra_server_url (str): The Patra server URL used to generate the ID.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
str: A unique hash identifier for the model card.
|
|
336
|
+
"""
|
|
337
|
+
combined_string = f"{self.name}:{self.version}:{self.author}"
|
|
338
|
+
try:
|
|
339
|
+
if patra_server_url:
|
|
340
|
+
patra_hash_url = f"{patra_server_url}/get_hash_id"
|
|
341
|
+
headers = {'Content-Type': 'application/json'}
|
|
342
|
+
response = requests.get(patra_hash_url, params={"combined_string": combined_string}, headers=headers)
|
|
343
|
+
response.raise_for_status()
|
|
344
|
+
return response.json()
|
|
345
|
+
else:
|
|
346
|
+
return hashlib.sha256(combined_string.encode()).hexdigest()
|
|
347
|
+
except requests.exceptions.RequestException as e:
|
|
348
|
+
print("Could not connect to the Patra Server, generating the ID locally")
|
|
349
|
+
return hashlib.sha256(combined_string.encode()).hexdigest()
|
|
350
|
+
|
|
351
|
+
def save(self, file_location):
|
|
352
|
+
"""
|
|
353
|
+
Saves the model card as a JSON file to the specified location.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
file_location (str): The path where the model card JSON file will be saved.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
None
|
|
360
|
+
"""
|
|
361
|
+
with open(file_location, 'w') as json_file:
|
|
362
|
+
json_file.write(str(self))
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class ModelCardJSONEncoder(JSONEncoder):
|
|
366
|
+
"""
|
|
367
|
+
Custom JSON Encoder for ModelCard to handle complex objects.
|
|
368
|
+
|
|
369
|
+
Methods:
|
|
370
|
+
default: Serializes non-serializable fields.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
def default(self, obj):
|
|
374
|
+
if isinstance(obj, (ModelCard, Metric, AIModel, ExplainabilityAnalysis, BiasAnalysis)):
|
|
375
|
+
return obj.__dict__
|
|
376
|
+
return super().default(obj)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Patra Model Card",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"commonFields": {
|
|
6
|
+
"urlOrDoi": {
|
|
7
|
+
"oneOf": [
|
|
8
|
+
{
|
|
9
|
+
"pattern": "^(https?|ftp):\\/\\/[^\\s/$.?#].[\\S]*$",
|
|
10
|
+
"description": "Must be a valid URL"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"pattern": "^10\\.\\d{4,9}/[^\\s]+$",
|
|
14
|
+
"description": "or valid DOI"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"enum": [""],
|
|
18
|
+
"description": "or empty"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "null"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"properties": {
|
|
27
|
+
"id": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "The id of the Model Card"
|
|
30
|
+
},
|
|
31
|
+
"name": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "The name of the Model Card"
|
|
34
|
+
},
|
|
35
|
+
"version": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "The version of the Model Card"
|
|
38
|
+
},
|
|
39
|
+
"short_description": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "A brief description of the Model Card"
|
|
42
|
+
},
|
|
43
|
+
"full_description": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "A comprehensive description of the Model Card"
|
|
46
|
+
},
|
|
47
|
+
"keywords": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "Keywords for the model card"
|
|
50
|
+
},
|
|
51
|
+
"author": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "The author or creator of the Model Card"
|
|
54
|
+
},
|
|
55
|
+
"input_data": {
|
|
56
|
+
"$ref": "#/commonFields/urlOrDoi"
|
|
57
|
+
},
|
|
58
|
+
"input_type": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Type of the input data"
|
|
61
|
+
},
|
|
62
|
+
"output_data": {
|
|
63
|
+
"$ref": "#/commonFields/urlOrDoi"
|
|
64
|
+
},
|
|
65
|
+
"foundational_model": {
|
|
66
|
+
"description": "If a foundational model has been used, provide the model ID",
|
|
67
|
+
"type": "string"
|
|
68
|
+
},
|
|
69
|
+
"category": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"enum": [
|
|
72
|
+
"classification",
|
|
73
|
+
"regression",
|
|
74
|
+
"clustering",
|
|
75
|
+
"anomaly detection",
|
|
76
|
+
"dimensionality reduction",
|
|
77
|
+
"reinforcement learning",
|
|
78
|
+
"natural language processing",
|
|
79
|
+
"computer vision",
|
|
80
|
+
"recommendation systems",
|
|
81
|
+
"time series forecasting",
|
|
82
|
+
"graph learning",
|
|
83
|
+
"graph neural networks",
|
|
84
|
+
"generative modeling",
|
|
85
|
+
"transfer learning",
|
|
86
|
+
"self-supervised learning",
|
|
87
|
+
"semi-supervised learning",
|
|
88
|
+
"unsupervised learning",
|
|
89
|
+
"causal inference",
|
|
90
|
+
"multi-task learning",
|
|
91
|
+
"metric learning",
|
|
92
|
+
"density estimation",
|
|
93
|
+
"multi-label classification",
|
|
94
|
+
"ranking",
|
|
95
|
+
"structured prediction",
|
|
96
|
+
"neural architecture search",
|
|
97
|
+
"sequence modeling",
|
|
98
|
+
"embedding learning",
|
|
99
|
+
"other"
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
"documentation": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"description": "URL for documentation if available"
|
|
105
|
+
},
|
|
106
|
+
"ai_model": {
|
|
107
|
+
"description": "Description of the AI Model",
|
|
108
|
+
"type": [
|
|
109
|
+
"object"
|
|
110
|
+
],
|
|
111
|
+
"properties": {
|
|
112
|
+
"name": {
|
|
113
|
+
"description": "The name of the model.",
|
|
114
|
+
"type": "string"
|
|
115
|
+
},
|
|
116
|
+
"version": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"description": "The version of the Model"
|
|
119
|
+
},
|
|
120
|
+
"description": {
|
|
121
|
+
"description": "Description of the AI Model.",
|
|
122
|
+
"type": "string"
|
|
123
|
+
},
|
|
124
|
+
"owner": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": "The owner of the Model"
|
|
127
|
+
},
|
|
128
|
+
"location": {
|
|
129
|
+
"description": "Downloadable URL of the model",
|
|
130
|
+
"type": "string"
|
|
131
|
+
},
|
|
132
|
+
"license": {
|
|
133
|
+
"description": "Licence the model",
|
|
134
|
+
"type": "string"
|
|
135
|
+
},
|
|
136
|
+
"model_structure": {
|
|
137
|
+
"description": "Structure of the model",
|
|
138
|
+
"type": "object"
|
|
139
|
+
},
|
|
140
|
+
"framework": {
|
|
141
|
+
"type": "string",
|
|
142
|
+
"enum": [
|
|
143
|
+
"sklearn",
|
|
144
|
+
"tensorflow",
|
|
145
|
+
"pytorch",
|
|
146
|
+
"other"
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
"model_type": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"enum": [
|
|
152
|
+
"cnn",
|
|
153
|
+
"decision_tree",
|
|
154
|
+
"dnn",
|
|
155
|
+
"rnn",
|
|
156
|
+
"svm",
|
|
157
|
+
"kmeans",
|
|
158
|
+
"llm",
|
|
159
|
+
"random_forest",
|
|
160
|
+
"lstm",
|
|
161
|
+
"gnn",
|
|
162
|
+
"other"
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
"test_accuracy": {
|
|
166
|
+
"description": "Accuracy of the model for the test data",
|
|
167
|
+
"type": "number"
|
|
168
|
+
},
|
|
169
|
+
"model_metrics": {
|
|
170
|
+
"description": "Metrics of the AI Model",
|
|
171
|
+
"type": "array",
|
|
172
|
+
"items": {
|
|
173
|
+
"$ref": "#/fields/metrics"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"required": [
|
|
178
|
+
"name",
|
|
179
|
+
"version",
|
|
180
|
+
"description",
|
|
181
|
+
"owner",
|
|
182
|
+
"location",
|
|
183
|
+
"license",
|
|
184
|
+
"framework",
|
|
185
|
+
"test_accuracy",
|
|
186
|
+
"model_type"
|
|
187
|
+
]
|
|
188
|
+
},
|
|
189
|
+
"bias_analysis": {
|
|
190
|
+
"type": ["object", "null"],
|
|
191
|
+
"properties": {
|
|
192
|
+
"demographic_parity_diff": {
|
|
193
|
+
"type": "number"
|
|
194
|
+
},
|
|
195
|
+
"equal_odds_difference": {
|
|
196
|
+
"type": "number"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"xai_analysis": {
|
|
201
|
+
"description": "Explainability analysis of the Model Card.",
|
|
202
|
+
"type": [
|
|
203
|
+
"object",
|
|
204
|
+
"null"
|
|
205
|
+
],
|
|
206
|
+
"properties": {
|
|
207
|
+
"bias_metrics": {
|
|
208
|
+
"description": "Metrics of the bias scanner",
|
|
209
|
+
"type": "array",
|
|
210
|
+
"items": {
|
|
211
|
+
"$ref": "#/fields/metrics"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
"model_requirements": {
|
|
217
|
+
"type": [
|
|
218
|
+
"array",
|
|
219
|
+
"null"
|
|
220
|
+
],
|
|
221
|
+
"items": {
|
|
222
|
+
"type": "string"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
"required": [
|
|
227
|
+
"name",
|
|
228
|
+
"version",
|
|
229
|
+
"short_description",
|
|
230
|
+
"full_description",
|
|
231
|
+
"author",
|
|
232
|
+
"keywords",
|
|
233
|
+
"input_data",
|
|
234
|
+
"input_type",
|
|
235
|
+
"output_data",
|
|
236
|
+
"ai_model"
|
|
237
|
+
],
|
|
238
|
+
"fields": {
|
|
239
|
+
"metrics": {
|
|
240
|
+
"type": "object",
|
|
241
|
+
"properties": {
|
|
242
|
+
"key": {
|
|
243
|
+
"description": "Name of the metric",
|
|
244
|
+
"type": "string"
|
|
245
|
+
},
|
|
246
|
+
"value": {
|
|
247
|
+
"description": "The value of the performance metric.",
|
|
248
|
+
"type": "string"
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
"commonFields": {
|
|
254
|
+
"urlOrDoi": {
|
|
255
|
+
"oneOf": [
|
|
256
|
+
{
|
|
257
|
+
"pattern": "^(https?|ftp):\\/\\/[^\\s/$.?#].[\\S]*$",
|
|
258
|
+
"description": "Must be a valid URL"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"pattern": "^10\\.\\d{4,9}/[^\\s]+$",
|
|
262
|
+
"description": "or valid DOI"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"enum": [""],
|
|
266
|
+
"description": "or empty"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"type": "null"
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import shap
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
class ExplainabilityAnalyser:
|
|
6
|
+
"""
|
|
7
|
+
Provides automated capture of xai information in a given AI workflow.
|
|
8
|
+
"""
|
|
9
|
+
def __init__(self, train_dataset, column_names, model):
|
|
10
|
+
self.dataset = train_dataset
|
|
11
|
+
self.column_names = column_names
|
|
12
|
+
self.model = model
|
|
13
|
+
|
|
14
|
+
def calculate_xai_features(self, n_features=10, pytorch = False):
|
|
15
|
+
# calculate the shap values
|
|
16
|
+
if pytorch:
|
|
17
|
+
explainer = shap.DeepExplainer(self.model, self.dataset)
|
|
18
|
+
else:
|
|
19
|
+
explainer = shap.Explainer(self.model, self.dataset)
|
|
20
|
+
|
|
21
|
+
values = explainer(self.dataset)
|
|
22
|
+
shap_values = values.values
|
|
23
|
+
|
|
24
|
+
if len(shap_values.shape) == 3:
|
|
25
|
+
shap_values = abs(shap_values).mean(axis=2)
|
|
26
|
+
|
|
27
|
+
feature_importance_df = pd.DataFrame(shap_values, columns=self.column_names).abs().mean(axis=0)
|
|
28
|
+
top_features = feature_importance_df.sort_values(ascending=False).head(n_features)
|
|
29
|
+
|
|
30
|
+
result_dict = {}
|
|
31
|
+
|
|
32
|
+
for name, importance in top_features.items():
|
|
33
|
+
|
|
34
|
+
filtered_name = re.sub(r'[^a-zA-Z0-9]', '_', name)
|
|
35
|
+
result_dict[filtered_name] = float(importance)
|
|
36
|
+
|
|
37
|
+
return result_dict
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Indiana University Board of Trustees.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: patra-toolkit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Toolkit for semi-automated modelcard creation for AI/ML models.
|
|
5
|
+
Home-page: https://github.com/Data-to-Insight-Center/patra-toolkit.git
|
|
6
|
+
Author: Data to Insight Center
|
|
7
|
+
Author-email: d2i@iu.edu
|
|
8
|
+
License: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: jsonschema>4.18.5
|
|
11
|
+
Requires-Dist: fairlearn~=0.11.0
|
|
12
|
+
Requires-Dist: shap~=0.46.0
|
|
13
|
+
Requires-Dist: pandas>=2.0.0
|
|
14
|
+
Requires-Dist: numpy>2.0.0
|
|
15
|
+
Requires-Dist: requests>2.32.2
|
|
16
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
patra_toolkit/__init__.py,sha256=Ivigf1Cp-Lweizl81EkDOIoo-otYDsfCZbNjY3IjoJc,86
|
|
2
|
+
patra_toolkit/fairlearn_bias.py,sha256=Zd-bItKO3GpX-5rQWEM3GApLFUitGWzXkl5y8fvu2mY,1264
|
|
3
|
+
patra_toolkit/patra_model_card.py,sha256=OeELQrcT4cX47hblkozrYUwqQnCmyN0ng0C5IiQa7Yc,13828
|
|
4
|
+
patra_toolkit/shap_xai.py,sha256=QnEx39rtO-V_QS3FyL4gHmejyG_S-mf14IpCNWn3Lzs,1206
|
|
5
|
+
patra_toolkit/schema/schema.json,sha256=dW9GkAGWT4FVXWPBBlGkHB40P6O4RGntXxuiPG4Udd4,6384
|
|
6
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
tests/test_patra_hash_id.py,sha256=AhQROssw1uFWjEG-asBsS4hQpAH7uflYMAzxw-U89S8,4380
|
|
8
|
+
tests/test_patra_inout_data.py,sha256=36EqCE0f1KWJsRs2oxlLQ_9Ubtp8k6eAtOtA3rjn54o,3628
|
|
9
|
+
tests/test_patra_mc.py,sha256=ucUU5zLu-Fl1PpohDF0j9uSyMkSVkVNoNojrrldp0nY,5553
|
|
10
|
+
patra_toolkit-0.1.1.dist-info/LICENSE,sha256=mvKzUSZOUwnUYJ-kO0fY0JHMNzkSSXML5n4WED8plpc,1524
|
|
11
|
+
patra_toolkit-0.1.1.dist-info/METADATA,sha256=nU5ZSsmNuFtRURCfTbaj1RQ8s_UIbId5KB0HazVhUNE,483
|
|
12
|
+
patra_toolkit-0.1.1.dist-info/WHEEL,sha256=a7TGlA-5DaHMRrarXjVbQagU3Man_dCnGIWMJr5kRWo,91
|
|
13
|
+
patra_toolkit-0.1.1.dist-info/top_level.txt,sha256=bAHB9O6o1rDnP-Cbo3EOXY7wRsi6HS5X5wi9jMuCRaQ,20
|
|
14
|
+
patra_toolkit-0.1.1.dist-info/RECORD,,
|
tests/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest.mock import patch, MagicMock
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from patra_toolkit import ModelCard, AIModel, BiasAnalysis, ExplainabilityAnalysis, Metric
|
|
9
|
+
|
|
10
|
+
SCHEMA_JSON = os.path.join(os.path.dirname(__file__), os.pardir,
|
|
11
|
+
'patra_toolkit/schema/schema.json')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelCardTestCase2(unittest.TestCase):
|
|
15
|
+
|
|
16
|
+
def setUp(self) -> None:
|
|
17
|
+
self.aimodel = AIModel(
|
|
18
|
+
name="DenseNet",
|
|
19
|
+
version="v0.1",
|
|
20
|
+
description="DenseNet CNN model",
|
|
21
|
+
owner="PyTorch",
|
|
22
|
+
location="pytorch.org",
|
|
23
|
+
license="testLicence",
|
|
24
|
+
framework="tensorflow",
|
|
25
|
+
model_type="cnn",
|
|
26
|
+
test_accuracy=0.89
|
|
27
|
+
)
|
|
28
|
+
self.bias_analysis = BiasAnalysis(0.1, 0.2)
|
|
29
|
+
self.xai_analysis = ExplainabilityAnalysis("XAI Test 2", [Metric("xai1", 0.5), Metric("xai2", 0.8)])
|
|
30
|
+
self.mc_save_location = "./test_mc.json"
|
|
31
|
+
|
|
32
|
+
@patch('requests.get')
|
|
33
|
+
def test_get_hash_id_success(self, mock_get):
|
|
34
|
+
"""Test that id is set correctly when server responds successfully."""
|
|
35
|
+
# Mock the server response to return a hash ID
|
|
36
|
+
mock_get.return_value = MagicMock(status_code=200)
|
|
37
|
+
mock_get.return_value.json.return_value = "e6c22bdf9fd3c164a2a9a083fb56fca9328f6ca30f7dcd2ebfc140a7d6f02149"
|
|
38
|
+
|
|
39
|
+
model_card = ModelCard(
|
|
40
|
+
name="icicle-camera-traps",
|
|
41
|
+
version="0.1",
|
|
42
|
+
short_description="Camera Traps CNN inference model card",
|
|
43
|
+
full_description="Camera Traps CNN full descr inference model card",
|
|
44
|
+
keywords="cnn, pytorch, icicle",
|
|
45
|
+
author="Joe",
|
|
46
|
+
input_data="",
|
|
47
|
+
input_type="image",
|
|
48
|
+
output_data="",
|
|
49
|
+
category="classification",
|
|
50
|
+
ai_model=self.aimodel,
|
|
51
|
+
bias_analysis=self.bias_analysis,
|
|
52
|
+
xai_analysis=self.xai_analysis
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
model_card.id = model_card._get_hash_id("http://127.0.0.1:5002")
|
|
56
|
+
self.assertEqual(model_card.id, "e6c22bdf9fd3c164a2a9a083fb56fca9328f6ca30f7dcd2ebfc140a7d6f02149")
|
|
57
|
+
print("Success case id:", model_card.id)
|
|
58
|
+
|
|
59
|
+
@patch('requests.get')
|
|
60
|
+
def test_get_hash_id_server_down(self, mock_get):
|
|
61
|
+
"""Test hash generation when server is down."""
|
|
62
|
+
# Simulate a server failure
|
|
63
|
+
mock_get.side_effect = requests.exceptions.RequestException("Server is down")
|
|
64
|
+
|
|
65
|
+
model_card = ModelCard(
|
|
66
|
+
name="icicle-camera-traps",
|
|
67
|
+
version="0.1",
|
|
68
|
+
short_description="Camera Traps CNN inference model card",
|
|
69
|
+
full_description="Camera Traps CNN full descr inference model card",
|
|
70
|
+
keywords="cnn, pytorch, icicle",
|
|
71
|
+
author="Joe",
|
|
72
|
+
input_data="",
|
|
73
|
+
input_type="image",
|
|
74
|
+
output_data="",
|
|
75
|
+
category="classification",
|
|
76
|
+
ai_model=self.aimodel,
|
|
77
|
+
bias_analysis=self.bias_analysis,
|
|
78
|
+
xai_analysis=self.xai_analysis
|
|
79
|
+
)
|
|
80
|
+
model_card.id = model_card._get_hash_id("http://127.0.0.1:5002")
|
|
81
|
+
|
|
82
|
+
self.assertIsNotNone(model_card.id)
|
|
83
|
+
print("Server down case id:", model_card.id)
|
|
84
|
+
|
|
85
|
+
@patch('requests.get')
|
|
86
|
+
def test_generate_hash_without_base_url(self, mock_get):
|
|
87
|
+
"""Test hash generation when no base URL is provided."""
|
|
88
|
+
|
|
89
|
+
mock_get.assert_not_called()
|
|
90
|
+
|
|
91
|
+
model_card = ModelCard(
|
|
92
|
+
name="icicle-camera-traps",
|
|
93
|
+
version="0.1",
|
|
94
|
+
short_description="Camera Traps CNN inference model card",
|
|
95
|
+
full_description="Camera Traps CNN full descr inference model card",
|
|
96
|
+
keywords="cnn, pytorch, icicle",
|
|
97
|
+
author="Joe",
|
|
98
|
+
input_data="",
|
|
99
|
+
input_type="image",
|
|
100
|
+
output_data="",
|
|
101
|
+
category="classification",
|
|
102
|
+
ai_model=self.aimodel,
|
|
103
|
+
bias_analysis=self.bias_analysis,
|
|
104
|
+
xai_analysis=self.xai_analysis
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
model_card.id = model_card._get_hash_id(None)
|
|
108
|
+
# Generate the expected hash value
|
|
109
|
+
combined_string = f"{model_card.name}:{model_card.version}:{model_card.author}"
|
|
110
|
+
expected_hash = hashlib.sha256(combined_string.encode()).hexdigest()
|
|
111
|
+
|
|
112
|
+
self.assertEqual(model_card.id, expected_hash)
|
|
113
|
+
print("Generated hash id without base_url:", model_card.id)
|
|
114
|
+
|
|
115
|
+
if __name__ == '__main__':
|
|
116
|
+
unittest.main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from patra_toolkit import ModelCard, AIModel, BiasAnalysis, ExplainabilityAnalysis, Metric
|
|
5
|
+
|
|
6
|
+
SCHEMA_JSON = os.path.join(os.path.dirname(__file__), os.pardir,
|
|
7
|
+
'patra_toolkit/schema/schema.json')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModelCardTestCase2(unittest.TestCase):
|
|
11
|
+
|
|
12
|
+
def setUp(self) -> None:
|
|
13
|
+
self.aimodel = AIModel(
|
|
14
|
+
name="DenseNet",
|
|
15
|
+
version="v0.1",
|
|
16
|
+
description="DenseNet CNN model",
|
|
17
|
+
owner="PyTorch",
|
|
18
|
+
location="pytorch.org",
|
|
19
|
+
license="testLicence",
|
|
20
|
+
framework="tensorflow",
|
|
21
|
+
model_type="cnn",
|
|
22
|
+
test_accuracy=0.89
|
|
23
|
+
)
|
|
24
|
+
self.bias_analysis = BiasAnalysis(0.1, 0.2)
|
|
25
|
+
self.xai_analysis = ExplainabilityAnalysis("XAI Test 2", [Metric("xai1", 0.5), Metric("xai2", 0.8)])
|
|
26
|
+
|
|
27
|
+
self.mc = ModelCard(
|
|
28
|
+
name="icicle-camera-traps",
|
|
29
|
+
version="0.1",
|
|
30
|
+
short_description="Camera Traps CNN inference model card",
|
|
31
|
+
full_description="Camera Traps CNN full descr inference model card",
|
|
32
|
+
keywords="cnn, pytorch, icicle",
|
|
33
|
+
author="Joe",
|
|
34
|
+
input_data="",
|
|
35
|
+
input_type="image",
|
|
36
|
+
output_data="",
|
|
37
|
+
category="classification",
|
|
38
|
+
ai_model=self.aimodel,
|
|
39
|
+
bias_analysis=self.bias_analysis,
|
|
40
|
+
xai_analysis=self.xai_analysis
|
|
41
|
+
)
|
|
42
|
+
self.mc_save_location = "./test_mc.json"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_input_data_empty(self):
|
|
46
|
+
"""
|
|
47
|
+
This function tests whether the input data is empty.
|
|
48
|
+
"""
|
|
49
|
+
is_valid = self.mc.validate()
|
|
50
|
+
self.assertTrue(is_valid)
|
|
51
|
+
|
|
52
|
+
def test_input_data_doi(self):
|
|
53
|
+
"""
|
|
54
|
+
This function tests whether the input data is doi.
|
|
55
|
+
"""
|
|
56
|
+
self.mc.input_data = "10.5281/zenodo.11179653"
|
|
57
|
+
is_valid = self.mc.validate()
|
|
58
|
+
self.assertTrue(is_valid)
|
|
59
|
+
|
|
60
|
+
def test_input_data_url(self):
|
|
61
|
+
"""
|
|
62
|
+
This function tests whether the input data is url.
|
|
63
|
+
"""
|
|
64
|
+
self.mc.input_data = "https://archive.ics.uci.edu/dataset/2/adult"
|
|
65
|
+
is_valid = self.mc.validate()
|
|
66
|
+
self.assertTrue(is_valid)
|
|
67
|
+
|
|
68
|
+
def test_input_data_string(self):
|
|
69
|
+
"""
|
|
70
|
+
This function tests whether the input data is string.
|
|
71
|
+
"""
|
|
72
|
+
self.mc.input_data = "cifar10"
|
|
73
|
+
is_valid = self.mc.validate()
|
|
74
|
+
self.assertFalse(is_valid)
|
|
75
|
+
|
|
76
|
+
def test_output_data_empty(self):
|
|
77
|
+
"""
|
|
78
|
+
This function tests whether the output data is empty.
|
|
79
|
+
"""
|
|
80
|
+
self.mc.input_data = "10.5281/zenodo.11179653"
|
|
81
|
+
is_valid = self.mc.validate()
|
|
82
|
+
self.assertTrue(is_valid)
|
|
83
|
+
|
|
84
|
+
def test_output_data_doi(self):
|
|
85
|
+
"""
|
|
86
|
+
This function tests whether the output data is doi.
|
|
87
|
+
"""
|
|
88
|
+
self.mc.input_data = "10.5281/zenodo.11179653"
|
|
89
|
+
self.mc.output_data = "10.5281/zenodo.11179653"
|
|
90
|
+
is_valid = self.mc.validate()
|
|
91
|
+
self.assertTrue(is_valid)
|
|
92
|
+
|
|
93
|
+
def test_output_data_url(self):
|
|
94
|
+
"""
|
|
95
|
+
This function tests whether the output data is url.
|
|
96
|
+
"""
|
|
97
|
+
self.mc.input_data = "10.5281/zenodo.11179653"
|
|
98
|
+
self.mc.output_data = "https://archive.ics.uci.edu/dataset/2/adult"
|
|
99
|
+
is_valid = self.mc.validate()
|
|
100
|
+
self.assertTrue(is_valid)
|
|
101
|
+
|
|
102
|
+
def test_output_data_string(self):
|
|
103
|
+
"""
|
|
104
|
+
This function tests whether the output data is string.
|
|
105
|
+
"""
|
|
106
|
+
self.mc.input_data = "10.5281/zenodo.11179653"
|
|
107
|
+
self.mc.output_data = "hcifar10"
|
|
108
|
+
is_valid = self.mc.validate()
|
|
109
|
+
self.assertFalse(is_valid)
|
|
110
|
+
|
|
111
|
+
if __name__ == '__main__':
|
|
112
|
+
unittest.main()
|
tests/test_patra_mc.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from jsonschema import validate
|
|
6
|
+
|
|
7
|
+
from patra_toolkit import ModelCard, AIModel, BiasAnalysis, ExplainabilityAnalysis, Metric, ModelCardJSONEncoder
|
|
8
|
+
|
|
9
|
+
SCHEMA_JSON = os.path.join(os.path.dirname(__file__), os.pardir,
|
|
10
|
+
'patra_toolkit/schema/schema.json')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ModelCardTestCase(unittest.TestCase):
|
|
14
|
+
|
|
15
|
+
def setUp(self) -> None:
|
|
16
|
+
|
|
17
|
+
self.aimodel = AIModel(
|
|
18
|
+
name="DenseNet",
|
|
19
|
+
version="v0.1",
|
|
20
|
+
description="DenseNet CNN model",
|
|
21
|
+
owner="PyTorch",
|
|
22
|
+
location="pytorch.org",
|
|
23
|
+
license="testLicence",
|
|
24
|
+
framework="tensorflow",
|
|
25
|
+
model_type="cnn",
|
|
26
|
+
test_accuracy=0.89
|
|
27
|
+
)
|
|
28
|
+
self.bias_analysis = BiasAnalysis(0.1, 0.2)
|
|
29
|
+
self.xai_analysis = ExplainabilityAnalysis("XAI Test 2", [Metric("xai1", 0.5), Metric("xai2", 0.8)])
|
|
30
|
+
|
|
31
|
+
self.mc = ModelCard(
|
|
32
|
+
name="icicle-camera-traps",
|
|
33
|
+
version="0.1",
|
|
34
|
+
short_description="Camera Traps CNN inference model card",
|
|
35
|
+
full_description="Camera Traps CNN full descr inference model card",
|
|
36
|
+
keywords="cnn, pytorch, icicle",
|
|
37
|
+
author="Joe",
|
|
38
|
+
input_data="https://archive.ics.uci.edu/dataset/2/adult",
|
|
39
|
+
input_type="image",
|
|
40
|
+
output_data="https://archive.ics.uci.edu/dataset/2/adult",
|
|
41
|
+
category="classification",
|
|
42
|
+
ai_model=self.aimodel,
|
|
43
|
+
bias_analysis=self.bias_analysis,
|
|
44
|
+
xai_analysis=self.xai_analysis
|
|
45
|
+
)
|
|
46
|
+
self.mc_save_location = "./test_mc.json"
|
|
47
|
+
|
|
48
|
+
def test_json_serialization(self):
|
|
49
|
+
"""
|
|
50
|
+
This tests the serialization of the Model Card object.
|
|
51
|
+
:return:
|
|
52
|
+
"""
|
|
53
|
+
# Convert the dataclass object to JSON string using the custom encoder
|
|
54
|
+
mc_json = json.dumps(self.mc, cls=ModelCardJSONEncoder, indent=2)
|
|
55
|
+
|
|
56
|
+
# Parse the JSON string back to a dictionary
|
|
57
|
+
parsed_mc = json.loads(mc_json)
|
|
58
|
+
|
|
59
|
+
self.assertEqual(parsed_mc['name'], "icicle-camera-traps")
|
|
60
|
+
self.assertEqual(parsed_mc['ai_model']['name'], "DenseNet")
|
|
61
|
+
self.assertEqual(parsed_mc['bias_analysis']['demographic_parity_difference'], 0.1)
|
|
62
|
+
|
|
63
|
+
def test_schema_compatibility(self):
|
|
64
|
+
"""
|
|
65
|
+
This tests whether the python classes are inline with the provided model card schema.
|
|
66
|
+
:return:
|
|
67
|
+
"""
|
|
68
|
+
# Convert the dataclass object to JSON string using the custom encoder
|
|
69
|
+
mc_json = json.dumps(self.mc, cls=ModelCardJSONEncoder, indent=4)
|
|
70
|
+
with open(SCHEMA_JSON, 'r') as schema_file:
|
|
71
|
+
schema = json.load(schema_file)
|
|
72
|
+
try:
|
|
73
|
+
validate(json.loads(mc_json), schema)
|
|
74
|
+
except:
|
|
75
|
+
self.fail("Validation Error!")
|
|
76
|
+
|
|
77
|
+
def test_validate(self):
|
|
78
|
+
"""
|
|
79
|
+
This tests the validate function.
|
|
80
|
+
:return:
|
|
81
|
+
"""
|
|
82
|
+
is_valid = self.mc.validate()
|
|
83
|
+
self.assertTrue(is_valid)
|
|
84
|
+
|
|
85
|
+
def test_framework_enum(self):
|
|
86
|
+
"""
|
|
87
|
+
This tests the validate function.
|
|
88
|
+
:return:
|
|
89
|
+
"""
|
|
90
|
+
mc = ModelCard(
|
|
91
|
+
name="icicle-camera-traps",
|
|
92
|
+
version="0.1",
|
|
93
|
+
short_description="Camera Traps CNN inference model card",
|
|
94
|
+
full_description="Camera Traps CNN full descr inference model card",
|
|
95
|
+
keywords="cnn, pytorch, icicle",
|
|
96
|
+
author="Joe",
|
|
97
|
+
input_data="https://archive.ics.uci.edu/dataset/2/adult",
|
|
98
|
+
input_type="image",
|
|
99
|
+
output_data="https://archive.ics.uci.edu/dataset/2/adult",
|
|
100
|
+
category="classification"
|
|
101
|
+
)
|
|
102
|
+
aimodel = AIModel(
|
|
103
|
+
name="DenseNet",
|
|
104
|
+
version="v0.1",
|
|
105
|
+
description="DenseNet CNN model",
|
|
106
|
+
owner="PyTorch",
|
|
107
|
+
location="pytorch.org",
|
|
108
|
+
license="testLicence",
|
|
109
|
+
framework="keras",
|
|
110
|
+
model_type="dnn",
|
|
111
|
+
test_accuracy=0.89
|
|
112
|
+
)
|
|
113
|
+
bias_analysis = BiasAnalysis(0.1, 0.2)
|
|
114
|
+
xai_analysis = ExplainabilityAnalysis("XAI Test 2", [Metric("xai1", 0.5), Metric("xai2", 0.8)])
|
|
115
|
+
|
|
116
|
+
mc.ai_model = aimodel
|
|
117
|
+
mc.bias_analysis = bias_analysis
|
|
118
|
+
|
|
119
|
+
is_valid = mc.validate()
|
|
120
|
+
self.assertFalse(is_valid)
|
|
121
|
+
|
|
122
|
+
def test_json_conversion(self):
|
|
123
|
+
"""
|
|
124
|
+
This tests data insertion via converting it to json, converting back to python.
|
|
125
|
+
:return:
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
# Convert the dataclass object to JSON string using the custom encoder
|
|
129
|
+
mc_json = json.dumps(self.mc, cls=ModelCardJSONEncoder, indent=4)
|
|
130
|
+
|
|
131
|
+
# convert the json string back to python model
|
|
132
|
+
mc_converted = json.loads(mc_json)
|
|
133
|
+
|
|
134
|
+
self.assertEqual(mc_converted['input_data'], "https://archive.ics.uci.edu/dataset/2/adult")
|
|
135
|
+
|
|
136
|
+
def test_json_save(self):
|
|
137
|
+
"""
|
|
138
|
+
This tests if the MC save as json works
|
|
139
|
+
:return:
|
|
140
|
+
"""
|
|
141
|
+
self.mc.save(self.mc_save_location)
|
|
142
|
+
|
|
143
|
+
# file existing verification
|
|
144
|
+
self.assertTrue(os.path.exists(self.mc_save_location), "File not found")
|
|
145
|
+
|
|
146
|
+
# Verify the content in the file is same as the saved data
|
|
147
|
+
with open(self.mc_save_location, 'r') as json_file:
|
|
148
|
+
saved_data = json.load(json_file)
|
|
149
|
+
|
|
150
|
+
self.assertEqual(saved_data['name'], self.mc.name, "Saved JSON data doesn't match the original Model Card")
|
|
151
|
+
|
|
152
|
+
def tearDown(self):
|
|
153
|
+
# Removing the saved file for the model card
|
|
154
|
+
if os.path.exists(self.mc_save_location):
|
|
155
|
+
os.remove(self.mc_save_location)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == '__main__':
|
|
159
|
+
unittest.main()
|