Skip to content

Commit c40f95e

Browse files
[SYSTEMDS-3937] Add score ranking function to Scuro
This patch adds a new functionality to rank representations via different metrics (runtime, performance metric) in Scuro.
1 parent 6658c8f commit c40f95e

File tree

11 files changed

+311
-110
lines changed

11 files changed

+311
-110
lines changed

src/main/python/systemds/scuro/dataloader/json_loader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ def extract(self, file: str, index: Optional[Union[str, List[str]]] = None):
5555
except:
5656
text = json_file[self.field]
5757

58+
text = " ".join(text)
5859
self.data.append(text)
5960
self.metadata[idx] = self.modality_type.create_metadata(len(text), text)

src/main/python/systemds/scuro/drsearch/hyperparameter_tuner.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,13 @@ def visit_node(node_id):
174174
all_results.append(result)
175175

176176
if self.maximize_metric:
177-
best_params, best_score = max(all_results, key=lambda x: x[1])
177+
best_params, best_score = max(
178+
all_results, key=lambda x: x[1].scores[self.scoring_metric]
179+
)
178180
else:
179-
best_params, best_score = min(all_results, key=lambda x: x[1])
181+
best_params, best_score = min(
182+
all_results, key=lambda x: x[1].scores[self.scoring_metric]
183+
)
180184

181185
tuning_time = time.time() - start_time
182186

src/main/python/systemds/scuro/drsearch/multimodal_optimizer.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import threading
2525
from dataclasses import dataclass
2626
from typing import List, Dict, Any, Generator
27-
from systemds.scuro.drsearch.task import Task
27+
from systemds.scuro.drsearch.task import Task, PerformanceMeasure
2828
from systemds.scuro.drsearch.representation_dag import (
2929
RepresentationDag,
3030
RepresentationDAGBuilder,
@@ -87,7 +87,8 @@ def _evaluate_dag_worker(dag_pickle, task_pickle, modalities_pickle, debug=False
8787
val_score=scores[1],
8888
runtime=total_time,
8989
task_name=task_copy.model.name,
90-
evaluation_time=eval_time,
90+
task_time=eval_time,
91+
representation_time=total_time - eval_time,
9192
)
9293
except Exception:
9394
if debug:
@@ -390,8 +391,9 @@ def _evaluate_dag(self, dag: RepresentationDag, task: Task) -> "OptimizationResu
390391
train_score=scores[0],
391392
val_score=scores[1],
392393
runtime=total_time,
394+
representation_time=total_time - eval_time,
393395
task_name=task_copy.model.name,
394-
evaluation_time=eval_time,
396+
task_time=eval_time,
395397
)
396398

397399
except Exception as e:
@@ -475,8 +477,10 @@ def store_results(self, file_name=None):
475477
@dataclass
476478
class OptimizationResult:
477479
dag: RepresentationDag
478-
train_score: float
479-
val_score: float
480-
runtime: float
481-
task_name: str
482-
evaluation_time: float = 0.0
480+
train_score: PerformanceMeasure = None
481+
val_score: PerformanceMeasure = None
482+
runtime: float = 0.0
483+
task_time: float = 0.0
484+
representation_time: float = 0.0
485+
task_name: str = ""
486+
tradeoff_score: float = 0.0
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# -------------------------------------------------------------
2+
#
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
#
20+
# -------------------------------------------------------------
21+
22+
from dataclasses import replace
23+
from typing import Callable, Iterable, List, Optional
24+
25+
26+
def rank_by_tradeoff(
27+
entries: Iterable,
28+
*,
29+
weights=(0.7, 0.3),
30+
performance_metric_name: str = "accuracy",
31+
runtime_accessor: Optional[Callable[[object], float]] = None,
32+
cache_scores: bool = True,
33+
score_attr: str = "tradeoff_score",
34+
) -> List:
35+
entries = list(entries)
36+
if not entries:
37+
return []
38+
39+
performance_score_accessor = lambda entry: getattr(entry, "val_score")[
40+
performance_metric_name
41+
]
42+
if runtime_accessor is None:
43+
44+
def runtime_accessor(entry):
45+
if hasattr(entry, "runtime"):
46+
return getattr(entry, "runtime")
47+
rep = getattr(entry, "representation_time", 0.0)
48+
task = getattr(entry, "task_time", 0.0)
49+
return rep + task
50+
51+
performance = [float(performance_score_accessor(e)) for e in entries]
52+
runtimes = [float(runtime_accessor(e)) for e in entries]
53+
54+
perf_min, perf_max = min(performance), max(performance)
55+
run_min, run_max = min(runtimes), max(runtimes)
56+
57+
def safe_normalize(values, vmin, vmax):
58+
if vmax - vmin == 0.0:
59+
return [1.0] * len(values)
60+
return [(v - vmin) / (vmax - vmin) for v in values]
61+
62+
norm_perf = safe_normalize(performance, perf_min, perf_max)
63+
norm_run = safe_normalize(runtimes, run_min, run_max)
64+
norm_run = [1.0 - r for r in norm_run]
65+
66+
acc_w, run_w = weights
67+
total_w = (acc_w or 0.0) + (run_w or 0.0)
68+
if total_w == 0.0:
69+
acc_w = 1.0
70+
run_w = 0.0
71+
else:
72+
acc_w /= total_w
73+
run_w /= total_w
74+
75+
scores = [acc_w * a + run_w * r for a, r in zip(norm_perf, norm_run)]
76+
77+
if cache_scores:
78+
for entry, score in zip(entries, scores):
79+
if hasattr(entry, score_attr):
80+
try:
81+
new_entry = replace(entry, **{score_attr: score})
82+
entries[entries.index(entry)] = new_entry
83+
except TypeError:
84+
setattr(entry, score_attr, score)
85+
else:
86+
setattr(entry, score_attr, score)
87+
88+
return sorted(
89+
entries, key=lambda entry: getattr(entry, score_attr, 0.0), reverse=True
90+
)

src/main/python/systemds/scuro/drsearch/task.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,37 @@
2828
from sklearn.model_selection import KFold
2929

3030

31+
class PerformanceMeasure:
32+
def __init__(self, name, metrics, higher_is_better=True):
33+
self.average_scores = None
34+
self.name = name
35+
self.metrics = metrics
36+
self.higher_is_better = higher_is_better
37+
self.scores = {}
38+
39+
if isinstance(metrics, list):
40+
for metric in metrics:
41+
self.scores[metric] = []
42+
else:
43+
self.scores[metrics] = []
44+
45+
def add_scores(self, scores):
46+
if isinstance(self.metrics, list):
47+
for metric in self.metrics:
48+
self.scores[metric].append(scores[metric])
49+
else:
50+
self.scores[self.metrics].append(scores[self.metrics])
51+
52+
def compute_averages(self):
53+
self.average_scores = {}
54+
if isinstance(self.metrics, list):
55+
for metric in self.metrics:
56+
self.average_scores[metric] = np.mean(self.scores[metric])
57+
else:
58+
self.average_scores[self.metrics] = np.mean(self.scores[self.metrics])
59+
return self
60+
61+
3162
class Task:
3263
def __init__(
3364
self,
@@ -38,6 +69,7 @@ def __init__(
3869
val_indices: List,
3970
kfold=5,
4071
measure_performance=True,
72+
performance_measures="accuracy",
4173
):
4274
"""
4375
Parent class for the prediction task that is performed on top of the aligned representation
@@ -59,8 +91,9 @@ def __init__(
5991
self.inference_time = []
6092
self.training_time = []
6193
self.expected_dim = 1
62-
self.train_scores = []
63-
self.val_scores = []
94+
self.performance_measures = performance_measures
95+
self.train_scores = PerformanceMeasure("train", performance_measures)
96+
self.val_scores = PerformanceMeasure("val", performance_measures)
6497

6598
def create_model(self):
6699
"""
@@ -74,8 +107,12 @@ def create_model(self):
74107
def get_train_test_split(self, data):
75108
X_train = [data[i] for i in self.train_indices]
76109
y_train = [self.labels[i] for i in self.train_indices]
77-
X_test = [data[i] for i in self.val_indices]
78-
y_test = [self.labels[i] for i in self.val_indices]
110+
if self.val_indices is None:
111+
X_test = None
112+
y_test = None
113+
else:
114+
X_test = [data[i] for i in self.val_indices]
115+
y_test = [self.labels[i] for i in self.val_indices]
79116

80117
return X_train, y_train, X_test, y_test
81118

@@ -101,25 +138,28 @@ def run(self, data):
101138
self._run_fold(model, train_X, train_y, test_X, test_y)
102139
fold += 1
103140

104-
return [np.mean(self.train_scores), np.mean(self.val_scores)]
141+
return [
142+
self.train_scores.compute_averages(),
143+
self.val_scores.compute_averages(),
144+
]
105145

106146
def _reset_params(self):
107147
self.inference_time = []
108148
self.training_time = []
109-
self.train_scores = []
110-
self.val_scores = []
149+
self.train_scores = PerformanceMeasure("train", self.performance_measures)
150+
self.val_scores = PerformanceMeasure("val", self.performance_measures)
111151

112152
def _run_fold(self, model, train_X, train_y, test_X, test_y):
113153
train_start = time.time()
114154
train_score = model.fit(train_X, train_y, test_X, test_y)
115155
train_end = time.time()
116156
self.training_time.append(train_end - train_start)
117-
self.train_scores.append(train_score)
157+
self.train_scores.add_scores(train_score[0])
118158
test_start = time.time()
119159
test_score = model.test(np.array(test_X), test_y)
120160
test_end = time.time()
121161
self.inference_time.append(test_end - test_start)
122-
self.val_scores.append(test_score)
162+
self.val_scores.add_scores(test_score[0])
123163

124164
def create_representation_and_run(
125165
self,

0 commit comments

Comments
 (0)