workbench 0.8.202__py3-none-any.whl → 0.8.220__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.
Potentially problematic release.
This version of workbench might be problematic. Click here for more details.
- workbench/algorithms/dataframe/compound_dataset_overlap.py +321 -0
- workbench/algorithms/dataframe/feature_space_proximity.py +168 -75
- workbench/algorithms/dataframe/fingerprint_proximity.py +421 -85
- workbench/algorithms/dataframe/projection_2d.py +44 -21
- workbench/algorithms/dataframe/proximity.py +78 -150
- workbench/algorithms/graph/light/proximity_graph.py +5 -5
- workbench/algorithms/models/cleanlab_model.py +382 -0
- workbench/algorithms/models/noise_model.py +388 -0
- workbench/algorithms/sql/outliers.py +3 -3
- workbench/api/__init__.py +3 -0
- workbench/api/df_store.py +17 -108
- workbench/api/endpoint.py +13 -11
- workbench/api/feature_set.py +111 -8
- workbench/api/meta_model.py +289 -0
- workbench/api/model.py +45 -12
- workbench/api/parameter_store.py +3 -52
- workbench/cached/cached_model.py +4 -4
- workbench/core/artifacts/artifact.py +5 -5
- workbench/core/artifacts/df_store_core.py +114 -0
- workbench/core/artifacts/endpoint_core.py +228 -237
- workbench/core/artifacts/feature_set_core.py +185 -230
- workbench/core/artifacts/model_core.py +34 -26
- workbench/core/artifacts/parameter_store_core.py +98 -0
- workbench/core/pipelines/pipeline_executor.py +1 -1
- workbench/core/transforms/features_to_model/features_to_model.py +22 -10
- workbench/core/transforms/model_to_endpoint/model_to_endpoint.py +41 -10
- workbench/core/transforms/pandas_transforms/pandas_to_features.py +11 -2
- workbench/model_script_utils/model_script_utils.py +339 -0
- workbench/model_script_utils/pytorch_utils.py +405 -0
- workbench/model_script_utils/uq_harness.py +278 -0
- workbench/model_scripts/chemprop/chemprop.template +428 -631
- workbench/model_scripts/chemprop/generated_model_script.py +432 -635
- workbench/model_scripts/chemprop/model_script_utils.py +339 -0
- workbench/model_scripts/chemprop/requirements.txt +2 -10
- workbench/model_scripts/custom_models/chem_info/fingerprints.py +87 -46
- workbench/model_scripts/custom_models/proximity/feature_space_proximity.py +194 -0
- workbench/model_scripts/custom_models/proximity/feature_space_proximity.template +6 -6
- workbench/model_scripts/custom_models/uq_models/feature_space_proximity.py +194 -0
- workbench/model_scripts/meta_model/generated_model_script.py +209 -0
- workbench/model_scripts/meta_model/meta_model.template +209 -0
- workbench/model_scripts/pytorch_model/generated_model_script.py +374 -613
- workbench/model_scripts/pytorch_model/model_script_utils.py +339 -0
- workbench/model_scripts/pytorch_model/pytorch.template +370 -609
- workbench/model_scripts/pytorch_model/pytorch_utils.py +405 -0
- workbench/model_scripts/pytorch_model/requirements.txt +1 -1
- workbench/model_scripts/pytorch_model/uq_harness.py +278 -0
- workbench/model_scripts/script_generation.py +6 -5
- workbench/model_scripts/uq_models/generated_model_script.py +65 -422
- workbench/model_scripts/xgb_model/generated_model_script.py +372 -395
- workbench/model_scripts/xgb_model/model_script_utils.py +339 -0
- workbench/model_scripts/xgb_model/uq_harness.py +278 -0
- workbench/model_scripts/xgb_model/xgb_model.template +366 -396
- workbench/repl/workbench_shell.py +0 -5
- workbench/resources/open_source_api.key +1 -1
- workbench/scripts/endpoint_test.py +2 -2
- workbench/scripts/meta_model_sim.py +35 -0
- workbench/scripts/training_test.py +85 -0
- workbench/utils/chem_utils/fingerprints.py +87 -46
- workbench/utils/chem_utils/projections.py +16 -6
- workbench/utils/chemprop_utils.py +36 -655
- workbench/utils/meta_model_simulator.py +499 -0
- workbench/utils/metrics_utils.py +256 -0
- workbench/utils/model_utils.py +192 -54
- workbench/utils/pytorch_utils.py +33 -472
- workbench/utils/shap_utils.py +1 -55
- workbench/utils/xgboost_local_crossfold.py +267 -0
- workbench/utils/xgboost_model_utils.py +49 -356
- workbench/web_interface/components/model_plot.py +7 -1
- workbench/web_interface/components/plugins/model_details.py +30 -68
- workbench/web_interface/components/plugins/scatter_plot.py +4 -8
- {workbench-0.8.202.dist-info → workbench-0.8.220.dist-info}/METADATA +6 -5
- {workbench-0.8.202.dist-info → workbench-0.8.220.dist-info}/RECORD +76 -60
- {workbench-0.8.202.dist-info → workbench-0.8.220.dist-info}/entry_points.txt +2 -0
- workbench/core/cloud_platform/aws/aws_df_store.py +0 -404
- workbench/core/cloud_platform/aws/aws_parameter_store.py +0 -296
- workbench/model_scripts/custom_models/meta_endpoints/example.py +0 -53
- workbench/model_scripts/custom_models/proximity/proximity.py +0 -410
- workbench/model_scripts/custom_models/uq_models/meta_uq.template +0 -377
- workbench/model_scripts/custom_models/uq_models/proximity.py +0 -410
- workbench/model_scripts/uq_models/mapie.template +0 -605
- workbench/model_scripts/uq_models/requirements.txt +0 -1
- {workbench-0.8.202.dist-info → workbench-0.8.220.dist-info}/WHEEL +0 -0
- {workbench-0.8.202.dist-info → workbench-0.8.220.dist-info}/licenses/LICENSE +0 -0
- {workbench-0.8.202.dist-info → workbench-0.8.220.dist-info}/top_level.txt +0 -0
|
@@ -21,7 +21,7 @@ from workbench.utils.aws_utils import newest_path, pull_s3_data
|
|
|
21
21
|
from workbench.utils.s3_utils import compute_s3_object_hash
|
|
22
22
|
from workbench.utils.shap_utils import shap_values_data, shap_feature_importance
|
|
23
23
|
from workbench.utils.deprecated_utils import deprecated
|
|
24
|
-
from workbench.utils.model_utils import
|
|
24
|
+
from workbench.utils.model_utils import published_proximity_model, get_model_hyperparameters
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class ModelType(Enum):
|
|
@@ -44,9 +44,10 @@ class ModelFramework(Enum):
|
|
|
44
44
|
SKLEARN = "sklearn"
|
|
45
45
|
XGBOOST = "xgboost"
|
|
46
46
|
LIGHTGBM = "lightgbm"
|
|
47
|
-
|
|
47
|
+
PYTORCH = "pytorch"
|
|
48
48
|
CHEMPROP = "chemprop"
|
|
49
49
|
TRANSFORMER = "transformer"
|
|
50
|
+
META = "meta"
|
|
50
51
|
UNKNOWN = "unknown"
|
|
51
52
|
|
|
52
53
|
|
|
@@ -62,7 +63,8 @@ class ModelImages:
|
|
|
62
63
|
"inference": "py312-general-ml-inference",
|
|
63
64
|
"pytorch_training": "py312-pytorch-training",
|
|
64
65
|
"pytorch_inference": "py312-pytorch-inference",
|
|
65
|
-
"
|
|
66
|
+
"meta_training": "py312-meta-training",
|
|
67
|
+
"meta_inference": "py312-meta-inference",
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
@classmethod
|
|
@@ -263,21 +265,25 @@ class ModelCore(Artifact):
|
|
|
263
265
|
else:
|
|
264
266
|
self.log.important(f"No inference data found for {self.model_name}!")
|
|
265
267
|
|
|
266
|
-
def get_inference_metrics(self, capture_name: str = "
|
|
268
|
+
def get_inference_metrics(self, capture_name: str = "auto") -> Union[pd.DataFrame, None]:
|
|
267
269
|
"""Retrieve the inference performance metrics for this model
|
|
268
270
|
|
|
269
271
|
Args:
|
|
270
|
-
capture_name (str, optional): Specific capture_name
|
|
272
|
+
capture_name (str, optional): Specific capture_name (default: "auto")
|
|
271
273
|
Returns:
|
|
272
274
|
pd.DataFrame: DataFrame of the Model Metrics
|
|
273
275
|
|
|
274
276
|
Note:
|
|
275
|
-
If a capture_name isn't specified this will try to
|
|
277
|
+
If a capture_name isn't specified this will try to the 'first' available metrics
|
|
276
278
|
"""
|
|
277
279
|
# Try to get the auto_capture 'training_holdout' or the training
|
|
278
|
-
if capture_name == "
|
|
279
|
-
|
|
280
|
-
|
|
280
|
+
if capture_name == "auto":
|
|
281
|
+
metric_list = self.list_inference_runs()
|
|
282
|
+
if metric_list:
|
|
283
|
+
return self.get_inference_metrics(metric_list[0])
|
|
284
|
+
else:
|
|
285
|
+
self.log.warning(f"No performance metrics found for {self.model_name}!")
|
|
286
|
+
return None
|
|
281
287
|
|
|
282
288
|
# Grab the metrics captured during model training (could return None)
|
|
283
289
|
if capture_name == "model_training":
|
|
@@ -299,11 +305,11 @@ class ModelCore(Artifact):
|
|
|
299
305
|
self.log.warning(f"Performance metrics {capture_name} not found for {self.model_name}!")
|
|
300
306
|
return None
|
|
301
307
|
|
|
302
|
-
def confusion_matrix(self, capture_name: str = "
|
|
308
|
+
def confusion_matrix(self, capture_name: str = "auto") -> Union[pd.DataFrame, None]:
|
|
303
309
|
"""Retrieve the confusion_matrix for this model
|
|
304
310
|
|
|
305
311
|
Args:
|
|
306
|
-
capture_name (str, optional): Specific capture_name or "training" (default: "
|
|
312
|
+
capture_name (str, optional): Specific capture_name or "training" (default: "auto")
|
|
307
313
|
Returns:
|
|
308
314
|
pd.DataFrame: DataFrame of the Confusion Matrix (might be None)
|
|
309
315
|
"""
|
|
@@ -315,7 +321,7 @@ class ModelCore(Artifact):
|
|
|
315
321
|
raise ValueError(error_msg)
|
|
316
322
|
|
|
317
323
|
# Grab the metrics from the Workbench Metadata (try inference first, then training)
|
|
318
|
-
if capture_name == "
|
|
324
|
+
if capture_name == "auto":
|
|
319
325
|
cm = self.confusion_matrix("auto_inference")
|
|
320
326
|
return cm if cm is not None else self.confusion_matrix("model_training")
|
|
321
327
|
|
|
@@ -537,6 +543,17 @@ class ModelCore(Artifact):
|
|
|
537
543
|
else:
|
|
538
544
|
self.log.error(f"Model {self.model_name} is not a classifier!")
|
|
539
545
|
|
|
546
|
+
def summary(self) -> dict:
|
|
547
|
+
"""Summary information about this Model
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
dict: Dictionary of summary information about this Model
|
|
551
|
+
"""
|
|
552
|
+
self.log.info("Computing Model Summary...")
|
|
553
|
+
summary = super().summary()
|
|
554
|
+
summary["hyperparameters"] = get_model_hyperparameters(self)
|
|
555
|
+
return summary
|
|
556
|
+
|
|
540
557
|
def details(self) -> dict:
|
|
541
558
|
"""Additional Details about this Model
|
|
542
559
|
|
|
@@ -561,6 +578,7 @@ class ModelCore(Artifact):
|
|
|
561
578
|
details["status"] = self.latest_model["ModelPackageStatus"]
|
|
562
579
|
details["approval_status"] = self.latest_model.get("ModelApprovalStatus", "unknown")
|
|
563
580
|
details["image"] = self.container_image().split("/")[-1] # Shorten the image uri
|
|
581
|
+
details["hyperparameters"] = get_model_hyperparameters(self)
|
|
564
582
|
|
|
565
583
|
# Grab the inference and container info
|
|
566
584
|
inference_spec = self.latest_model["InferenceSpecification"]
|
|
@@ -571,16 +589,6 @@ class ModelCore(Artifact):
|
|
|
571
589
|
details["transform_types"] = inference_spec["SupportedTransformInstanceTypes"]
|
|
572
590
|
details["content_types"] = inference_spec["SupportedContentTypes"]
|
|
573
591
|
details["response_types"] = inference_spec["SupportedResponseMIMETypes"]
|
|
574
|
-
details["model_metrics"] = self.get_inference_metrics()
|
|
575
|
-
if self.model_type == ModelType.CLASSIFIER:
|
|
576
|
-
details["confusion_matrix"] = self.confusion_matrix()
|
|
577
|
-
details["predictions"] = None
|
|
578
|
-
elif self.model_type in [ModelType.REGRESSOR, ModelType.UQ_REGRESSOR, ModelType.ENSEMBLE_REGRESSOR]:
|
|
579
|
-
details["confusion_matrix"] = None
|
|
580
|
-
details["predictions"] = self.get_inference_predictions()
|
|
581
|
-
else:
|
|
582
|
-
details["confusion_matrix"] = None
|
|
583
|
-
details["predictions"] = None
|
|
584
592
|
|
|
585
593
|
# Grab the inference metadata
|
|
586
594
|
details["inference_meta"] = self.get_inference_metadata()
|
|
@@ -869,7 +877,7 @@ class ModelCore(Artifact):
|
|
|
869
877
|
return self.df_store.get(f"/workbench/models/{self.name}/shap_data")
|
|
870
878
|
else:
|
|
871
879
|
# Loop over the SHAP data and return a dict of DataFrames
|
|
872
|
-
shap_dfs = self.df_store.
|
|
880
|
+
shap_dfs = self.df_store.list(f"/workbench/models/{self.name}/shap_data")
|
|
873
881
|
shap_data = {}
|
|
874
882
|
for df_location in shap_dfs:
|
|
875
883
|
key = df_location.split("/")[-1]
|
|
@@ -888,19 +896,19 @@ class ModelCore(Artifact):
|
|
|
888
896
|
except (KeyError, IndexError, TypeError):
|
|
889
897
|
return None
|
|
890
898
|
|
|
891
|
-
def publish_prox_model(self, prox_model_name: str = None,
|
|
899
|
+
def publish_prox_model(self, prox_model_name: str = None, include_all_columns: bool = False):
|
|
892
900
|
"""Create and publish a Proximity Model for this Model
|
|
893
901
|
|
|
894
902
|
Args:
|
|
895
903
|
prox_model_name (str, optional): Name of the Proximity Model (if not specified, a name will be generated)
|
|
896
|
-
|
|
904
|
+
include_all_columns (bool): Include all DataFrame columns in results (default: False)
|
|
897
905
|
|
|
898
906
|
Returns:
|
|
899
907
|
Model: The published Proximity Model
|
|
900
908
|
"""
|
|
901
909
|
if prox_model_name is None:
|
|
902
910
|
prox_model_name = self.model_name + "-prox"
|
|
903
|
-
return
|
|
911
|
+
return published_proximity_model(self, prox_model_name, include_all_columns=include_all_columns)
|
|
904
912
|
|
|
905
913
|
def delete(self):
|
|
906
914
|
"""Delete the Model Packages and the Model Group"""
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""ParameterStoreCore: Manages Workbench parameters in a Cloud Based Parameter Store."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
# Workbench Imports
|
|
6
|
+
from workbench.core.cloud_platform.aws.aws_account_clamp import AWSAccountClamp
|
|
7
|
+
|
|
8
|
+
# Workbench Bridges Import
|
|
9
|
+
from workbench_bridges.api import ParameterStore as BridgesParameterStore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ParameterStoreCore(BridgesParameterStore):
|
|
13
|
+
"""ParameterStoreCore: Manages Workbench parameters in a Cloud Based Parameter Store.
|
|
14
|
+
|
|
15
|
+
Common Usage:
|
|
16
|
+
```python
|
|
17
|
+
params = ParameterStoreCore()
|
|
18
|
+
|
|
19
|
+
# List Parameters
|
|
20
|
+
params.list()
|
|
21
|
+
|
|
22
|
+
['/workbench/abalone_info',
|
|
23
|
+
'/workbench/my_data',
|
|
24
|
+
'/workbench/test',
|
|
25
|
+
'/workbench/pipelines/my_pipeline']
|
|
26
|
+
|
|
27
|
+
# Add Key
|
|
28
|
+
params.upsert("key", "value")
|
|
29
|
+
value = params.get("key")
|
|
30
|
+
|
|
31
|
+
# Add any data (lists, dictionaries, etc..)
|
|
32
|
+
my_data = {"key": "value", "number": 4.2, "list": [1,2,3]}
|
|
33
|
+
params.upsert("my_data", my_data)
|
|
34
|
+
|
|
35
|
+
# Retrieve data
|
|
36
|
+
return_value = params.get("my_data")
|
|
37
|
+
pprint(return_value)
|
|
38
|
+
|
|
39
|
+
{'key': 'value', 'list': [1, 2, 3], 'number': 4.2}
|
|
40
|
+
|
|
41
|
+
# Delete parameters
|
|
42
|
+
param_store.delete("my_data")
|
|
43
|
+
```
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
"""ParameterStoreCore Init Method"""
|
|
48
|
+
session = AWSAccountClamp().boto3_session
|
|
49
|
+
|
|
50
|
+
# Initialize parent with workbench config
|
|
51
|
+
super().__init__(boto3_session=session)
|
|
52
|
+
self.log = logging.getLogger("workbench")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
"""Exercise the ParameterStoreCore Class"""
|
|
57
|
+
|
|
58
|
+
# Create a ParameterStoreCore manager
|
|
59
|
+
param_store = ParameterStoreCore()
|
|
60
|
+
|
|
61
|
+
# List the parameters
|
|
62
|
+
print("Listing Parameters...")
|
|
63
|
+
print(param_store.list())
|
|
64
|
+
|
|
65
|
+
# Add a new parameter
|
|
66
|
+
param_store.upsert("/workbench/test", "value")
|
|
67
|
+
|
|
68
|
+
# Get the parameter
|
|
69
|
+
print(f"Getting parameter 'test': {param_store.get('/workbench/test')}")
|
|
70
|
+
|
|
71
|
+
# Add a dictionary as a parameter
|
|
72
|
+
sample_dict = {"key": "str_value", "awesome_value": 4.2}
|
|
73
|
+
param_store.upsert("/workbench/my_data", sample_dict)
|
|
74
|
+
|
|
75
|
+
# Retrieve the parameter as a dictionary
|
|
76
|
+
retrieved_value = param_store.get("/workbench/my_data")
|
|
77
|
+
print("Retrieved value:", retrieved_value)
|
|
78
|
+
|
|
79
|
+
# List the parameters
|
|
80
|
+
print("Listing Parameters...")
|
|
81
|
+
print(param_store.list())
|
|
82
|
+
|
|
83
|
+
# List the parameters with a prefix
|
|
84
|
+
print("Listing Parameters with prefix '/workbench':")
|
|
85
|
+
print(param_store.list("/workbench"))
|
|
86
|
+
|
|
87
|
+
# Delete the parameters
|
|
88
|
+
param_store.delete("/workbench/test")
|
|
89
|
+
param_store.delete("/workbench/my_data")
|
|
90
|
+
|
|
91
|
+
# Out of scope tests
|
|
92
|
+
param_store.upsert("test", "value")
|
|
93
|
+
param_store.delete("test")
|
|
94
|
+
|
|
95
|
+
# Recursive delete test
|
|
96
|
+
param_store.upsert("/workbench/test/test1", "value1")
|
|
97
|
+
param_store.upsert("/workbench/test/test2", "value2")
|
|
98
|
+
param_store.delete_recursive("workbench/test/")
|
|
@@ -123,7 +123,7 @@ class PipelineExecutor:
|
|
|
123
123
|
if "model" in workbench_objects and (not subset or "endpoint" in subset):
|
|
124
124
|
workbench_objects["model"].to_endpoint(**kwargs)
|
|
125
125
|
endpoint = Endpoint(kwargs["name"])
|
|
126
|
-
endpoint.auto_inference(
|
|
126
|
+
endpoint.auto_inference()
|
|
127
127
|
|
|
128
128
|
# Found something weird
|
|
129
129
|
else:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""FeaturesToModel: Train/Create a Model from a Feature Set"""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Union
|
|
4
5
|
from sagemaker.estimator import Estimator
|
|
5
6
|
import awswrangler as wr
|
|
6
7
|
from datetime import datetime, timezone
|
|
@@ -83,12 +84,17 @@ class FeaturesToModel(Transform):
|
|
|
83
84
|
self.inference_arch = inference_arch
|
|
84
85
|
|
|
85
86
|
def transform_impl(
|
|
86
|
-
self,
|
|
87
|
+
self,
|
|
88
|
+
target_column: Union[str, list[str]],
|
|
89
|
+
description: str = None,
|
|
90
|
+
feature_list: list = None,
|
|
91
|
+
train_all_data=False,
|
|
92
|
+
**kwargs,
|
|
87
93
|
):
|
|
88
94
|
"""Generic Features to Model: Note you should create a new class and inherit from
|
|
89
95
|
this one to include specific logic for your Feature Set/Model
|
|
90
96
|
Args:
|
|
91
|
-
target_column (str): Column name of the target variable
|
|
97
|
+
target_column (str or list[str]): Column name(s) of the target variable(s)
|
|
92
98
|
description (str): Description of the model (optional)
|
|
93
99
|
feature_list (list[str]): A list of columns for the features (default None, will try to guess)
|
|
94
100
|
train_all_data (bool): Train on ALL (100%) of the data (default False)
|
|
@@ -105,9 +111,11 @@ class FeaturesToModel(Transform):
|
|
|
105
111
|
s3_training_path = feature_set.create_s3_training_data()
|
|
106
112
|
self.log.info(f"Created new training data {s3_training_path}...")
|
|
107
113
|
|
|
108
|
-
# Report the target column
|
|
114
|
+
# Report the target column(s)
|
|
109
115
|
self.target_column = target_column
|
|
110
|
-
|
|
116
|
+
# Normalize target_column to a list for internal use
|
|
117
|
+
target_list = [target_column] if isinstance(target_column, str) else (target_column or [])
|
|
118
|
+
self.log.info(f"Target column(s): {self.target_column}")
|
|
111
119
|
|
|
112
120
|
# Did they specify a feature list?
|
|
113
121
|
if feature_list:
|
|
@@ -134,7 +142,7 @@ class FeaturesToModel(Transform):
|
|
|
134
142
|
"is_deleted",
|
|
135
143
|
"event_time",
|
|
136
144
|
"training",
|
|
137
|
-
] +
|
|
145
|
+
] + target_list
|
|
138
146
|
feature_list = [c for c in all_columns if c not in filter_list]
|
|
139
147
|
|
|
140
148
|
# AWS Feature Store has 3 user column types (String, Integral, Fractional)
|
|
@@ -157,12 +165,14 @@ class FeaturesToModel(Transform):
|
|
|
157
165
|
self.log.important(f"Feature List for Modeling: {self.model_feature_list}")
|
|
158
166
|
|
|
159
167
|
# Set up our parameters for the model script
|
|
168
|
+
# ChemProp expects target_column as a list; other templates expect a string
|
|
169
|
+
target_for_template = target_list if self.model_framework == ModelFramework.CHEMPROP else self.target_column
|
|
160
170
|
template_params = {
|
|
161
171
|
"model_imports": self.model_import_str,
|
|
162
172
|
"model_type": self.model_type,
|
|
163
173
|
"model_framework": self.model_framework,
|
|
164
174
|
"model_class": self.model_class,
|
|
165
|
-
"target_column":
|
|
175
|
+
"target_column": target_for_template,
|
|
166
176
|
"feature_list": self.model_feature_list,
|
|
167
177
|
"compressed_features": feature_set.get_compressed_features(),
|
|
168
178
|
"model_metrics_s3_path": self.model_training_root,
|
|
@@ -202,11 +212,13 @@ class FeaturesToModel(Transform):
|
|
|
202
212
|
# Metric Definitions for Classification
|
|
203
213
|
elif self.model_type == ModelType.CLASSIFIER:
|
|
204
214
|
# We need to get creative with the Classification Metrics
|
|
215
|
+
# Note: Classification only supports single target
|
|
216
|
+
class_target = target_list[0] if target_list else self.target_column
|
|
205
217
|
|
|
206
218
|
# Grab all the target column class values (class labels)
|
|
207
219
|
table = feature_set.data_source.table
|
|
208
|
-
self.class_labels = feature_set.query(f'select DISTINCT {
|
|
209
|
-
|
|
220
|
+
self.class_labels = feature_set.query(f'select DISTINCT {class_target} FROM "{table}"')[
|
|
221
|
+
class_target
|
|
210
222
|
].to_list()
|
|
211
223
|
|
|
212
224
|
# Sanity check on the targets
|
|
@@ -216,7 +228,7 @@ class FeaturesToModel(Transform):
|
|
|
216
228
|
raise ValueError(msg)
|
|
217
229
|
|
|
218
230
|
# Dynamically create the metric definitions
|
|
219
|
-
metrics = ["precision", "recall", "f1"]
|
|
231
|
+
metrics = ["precision", "recall", "f1", "support"]
|
|
220
232
|
metric_definitions = []
|
|
221
233
|
for t in self.class_labels:
|
|
222
234
|
for m in metrics:
|
|
@@ -242,7 +254,7 @@ class FeaturesToModel(Transform):
|
|
|
242
254
|
image = ModelImages.get_image_uri(self.sm_session.boto_region_name, self.training_image)
|
|
243
255
|
|
|
244
256
|
# Use GPU instance for ChemProp/PyTorch, CPU for others
|
|
245
|
-
if self.model_framework in [ModelFramework.CHEMPROP, ModelFramework.
|
|
257
|
+
if self.model_framework in [ModelFramework.CHEMPROP, ModelFramework.PYTORCH]:
|
|
246
258
|
train_instance_type = "ml.g6.xlarge" # NVIDIA L4 GPU, ~$0.80/hr
|
|
247
259
|
self.log.important(f"Using GPU instance {train_instance_type} for {self.model_framework.value}")
|
|
248
260
|
else:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""ModelToEndpoint: Deploy an Endpoint for a Model"""
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
+
from botocore.exceptions import ClientError
|
|
4
5
|
from sagemaker import ModelPackage
|
|
5
6
|
from sagemaker.serializers import CSVSerializer
|
|
6
7
|
from sagemaker.deserializers import CSVDeserializer
|
|
@@ -102,10 +103,21 @@ class ModelToEndpoint(Transform):
|
|
|
102
103
|
# Is this a serverless deployment?
|
|
103
104
|
serverless_config = None
|
|
104
105
|
if self.serverless:
|
|
106
|
+
# For PyTorch or ChemProp we need at least 4GB of memory
|
|
107
|
+
from workbench.api import ModelFramework
|
|
108
|
+
|
|
109
|
+
self.log.info(f"Model Framework: {workbench_model.model_framework}")
|
|
110
|
+
if workbench_model.model_framework in [ModelFramework.PYTORCH, ModelFramework.CHEMPROP]:
|
|
111
|
+
if mem_size < 4096:
|
|
112
|
+
self.log.important(
|
|
113
|
+
f"{workbench_model.model_framework} needs at least 4GB of memory (setting to 4GB)"
|
|
114
|
+
)
|
|
115
|
+
mem_size = 4096
|
|
105
116
|
serverless_config = ServerlessInferenceConfig(
|
|
106
117
|
memory_size_in_mb=mem_size,
|
|
107
118
|
max_concurrency=max_concurrency,
|
|
108
119
|
)
|
|
120
|
+
self.log.important(f"Serverless Config: Memory={mem_size}MB, MaxConcurrency={max_concurrency}")
|
|
109
121
|
|
|
110
122
|
# Configure data capture if requested (and not serverless)
|
|
111
123
|
data_capture_config = None
|
|
@@ -126,16 +138,35 @@ class ModelToEndpoint(Transform):
|
|
|
126
138
|
|
|
127
139
|
# Deploy the Endpoint
|
|
128
140
|
self.log.important(f"Deploying the Endpoint {self.output_name}...")
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
try:
|
|
142
|
+
model_package.deploy(
|
|
143
|
+
initial_instance_count=1,
|
|
144
|
+
instance_type=self.instance_type,
|
|
145
|
+
serverless_inference_config=serverless_config,
|
|
146
|
+
endpoint_name=self.output_name,
|
|
147
|
+
serializer=CSVSerializer(),
|
|
148
|
+
deserializer=CSVDeserializer(),
|
|
149
|
+
data_capture_config=data_capture_config,
|
|
150
|
+
tags=aws_tags,
|
|
151
|
+
)
|
|
152
|
+
except ClientError as e:
|
|
153
|
+
# Check if this is the "endpoint config already exists" error
|
|
154
|
+
if "Cannot create already existing endpoint configuration" in str(e):
|
|
155
|
+
self.log.warning("Endpoint config already exists, deleting and retrying...")
|
|
156
|
+
self.sm_client.delete_endpoint_config(EndpointConfigName=self.output_name)
|
|
157
|
+
# Retry the deploy
|
|
158
|
+
model_package.deploy(
|
|
159
|
+
initial_instance_count=1,
|
|
160
|
+
instance_type=self.instance_type,
|
|
161
|
+
serverless_inference_config=serverless_config,
|
|
162
|
+
endpoint_name=self.output_name,
|
|
163
|
+
serializer=CSVSerializer(),
|
|
164
|
+
deserializer=CSVDeserializer(),
|
|
165
|
+
data_capture_config=data_capture_config,
|
|
166
|
+
tags=aws_tags,
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
raise
|
|
139
170
|
|
|
140
171
|
def post_transform(self, **kwargs):
|
|
141
172
|
"""Post-Transform: Calling onboard() for the Endpoint"""
|
|
@@ -68,6 +68,15 @@ class PandasToFeatures(Transform):
|
|
|
68
68
|
self.output_df = input_df.copy()
|
|
69
69
|
self.one_hot_columns = one_hot_columns or []
|
|
70
70
|
|
|
71
|
+
# Warn about known AWS Iceberg bug with event_time_column
|
|
72
|
+
if event_time_column is not None:
|
|
73
|
+
self.log.warning(
|
|
74
|
+
f"event_time_column='{event_time_column}' specified. Note: AWS has a known bug with "
|
|
75
|
+
"Iceberg FeatureGroups where varying event times across multiple days can cause "
|
|
76
|
+
"duplicate rows in the offline store. Setting event_time_column=None."
|
|
77
|
+
)
|
|
78
|
+
self.event_time_column = None
|
|
79
|
+
|
|
71
80
|
# Now Prepare the DataFrame for its journey into an AWS FeatureGroup
|
|
72
81
|
self.prep_dataframe()
|
|
73
82
|
|
|
@@ -400,7 +409,7 @@ class PandasToFeatures(Transform):
|
|
|
400
409
|
|
|
401
410
|
# Set Hold Out Ids (if we got them during creation)
|
|
402
411
|
if self.incoming_hold_out_ids:
|
|
403
|
-
self.output_feature_set.set_training_holdouts(self.
|
|
412
|
+
self.output_feature_set.set_training_holdouts(self.incoming_hold_out_ids)
|
|
404
413
|
|
|
405
414
|
def ensure_feature_group_created(self, feature_group):
|
|
406
415
|
status = feature_group.describe().get("FeatureGroupStatus")
|
|
@@ -462,7 +471,7 @@ if __name__ == "__main__":
|
|
|
462
471
|
|
|
463
472
|
# Create my DF to Feature Set Transform (with one-hot encoding)
|
|
464
473
|
df_to_features = PandasToFeatures("test_features")
|
|
465
|
-
df_to_features.set_input(data_df, id_column="id", one_hot_columns=["food"])
|
|
474
|
+
df_to_features.set_input(data_df, id_column="id", event_time_column="date", one_hot_columns=["food"])
|
|
466
475
|
df_to_features.set_output_tags(["test", "small"])
|
|
467
476
|
df_to_features.transform()
|
|
468
477
|
|