Skip to content

Commit ceba228

Browse files
committed
METRICS: CONFIG: figs for multiple runs
1 parent d493db1 commit ceba228

3 files changed

Lines changed: 83 additions & 22 deletions

File tree

‎SARIAD/config/run.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,17 @@ def run_experiments(config: dict):
9494
"""
9595
global_config = config.get("global", {})
9696
experiments_list = config.get("experiments", [])
97-
benchmark_runs = config.get("benchmark_runs", 1)
9897

9998
all_runs_data = {}
10099

101100
for experiment in experiments_list:
102101
exp_name = experiment["name"]
103-
print(f"\n--- Starting experiment group: {exp_name} (x{benchmark_runs} runs) ---")
102+
# Get the number of runs for the specific experiment, default to 1 if not found
103+
runs = experiment.get("runs", 1)
104+
print(f"\n--- Starting experiment group: {exp_name} (x{runs} runs) ---")
104105

105106
individual_runs = []
106-
for i in tqdm(range(benchmark_runs), desc=f"Benchmarking {exp_name}"):
107+
for i in tqdm(range(runs), desc=f"Benchmarking {exp_name}"):
107108
try:
108109
metrics = run_single_experiment(experiment, config)
109110
individual_runs.append(metrics)

‎SARIAD/utils/inf.py‎

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def save_all(self, output_dir: str = "."):
183183
confmat = BinaryConfusionMatrix()(self.pred_labels, self.gt_labels)
184184
plt.figure(figsize=(8, 6))
185185
sns.heatmap(confmat.numpy(), annot=True, fmt='d', cmap='Blues',
186-
xticklabels=['Normal', 'Anomaly'], yticklabels=['Normal', 'Anomaly'])
186+
xticklabels=['Normal', 'Anomaly'], yticklabels=['Normal', 'Anomaly'])
187187
plt.title('Confusion Matrix')
188188
plt.xlabel('Predicted Label')
189189
plt.ylabel('True Label')
@@ -273,10 +273,74 @@ def _is_better(metric_name: str, val1, val2) -> bool:
273273
else:
274274
return val1 > val2
275275

276+
def _plot_roc_curve(self, run_data: dict[str, "Metrics | list"], output_dir: str = "."):
277+
"""
278+
Generates and saves a single ROC curve plot, showing the average curve and variability
279+
if multiple runs are provided for a single model.
280+
"""
281+
output_path = Path(output_dir)
282+
output_path.mkdir(exist_ok=True)
283+
284+
plt.figure(figsize=(8, 6))
285+
286+
for name, data in run_data.items():
287+
if isinstance(data, list):
288+
# Calculate mean ROC and standard deviation for a list of runs
289+
all_fprs = []
290+
all_tprs = []
291+
all_aucs = []
292+
293+
# We need to use a common set of false positive rates to average the true positive rates
294+
mean_fpr = np.linspace(0, 1, 100)
295+
296+
for run in data:
297+
fpr, tpr, _ = roc_curve(run.gt_labels.cpu().numpy(), run.pred_scores.cpu().numpy())
298+
all_fprs.append(fpr)
299+
all_tprs.append(tpr)
300+
all_aucs.append(auc(fpr, tpr))
301+
302+
interp_tprs = []
303+
for idx in range(len(data)):
304+
interp_tpr = np.interp(mean_fpr, all_fprs[idx], all_tprs[idx])
305+
interp_tpr[0] = 0.0
306+
interp_tprs.append(interp_tpr)
307+
308+
mean_tpr = np.mean(interp_tprs, axis=0)
309+
mean_tpr[-1] = 1.0
310+
mean_auc = auc(mean_fpr, mean_tpr)
311+
std_auc = np.std(all_aucs)
312+
std_tpr = np.std(interp_tprs, axis=0)
313+
314+
tprs_upper = np.minimum(mean_tpr + std_tpr, 1)
315+
tprs_lower = np.maximum(mean_tpr - std_tpr, 0)
316+
317+
# Plot mean ROC curve
318+
plt.plot(mean_fpr, mean_tpr, lw=2, label=f'{name} (Mean AUC = {mean_auc:.2f} $\pm$ {std_auc:.2f})')
319+
320+
# Plot variability area
321+
plt.fill_between(mean_fpr, tprs_lower, tprs_upper, alpha=0.2, label=r"$\pm$ 1 std. dev.")
322+
323+
else:
324+
# Plot single ROC curve
325+
fpr, tpr, _ = roc_curve(data.gt_labels.cpu().numpy(), data.pred_scores.cpu().numpy())
326+
roc_auc = auc(fpr, tpr)
327+
plt.plot(fpr, tpr, lw=2, label=f'{name} (AUC = {roc_auc:.2f})')
328+
329+
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', label='Chance Level')
330+
plt.xlim([0.0, 1.0])
331+
plt.ylim([0.0, 1.05])
332+
plt.xlabel('False Positive Rate')
333+
plt.ylabel('True Positive Rate')
334+
plt.title('Receiver Operating Characteristic (ROC) Curves')
335+
plt.legend(loc="lower right")
336+
plt.savefig(output_path / 'roc_curve_comparison.png')
337+
plt.close()
338+
276339
@classmethod
277340
def compare_multiple_runs(cls, runs_data: dict[str, "Metrics | list"]) -> str:
278341
"""
279-
Generates a single LaTeX table comparing multiple runs, with the best value bolded.
342+
Generates a single LaTeX table comparing multiple runs, with the best value bolded,
343+
and plots a combined ROC curve.
280344
281345
Args:
282346
runs_data (dict[str, "Metrics" | list]): A dictionary where keys are run names and values are either
@@ -288,6 +352,10 @@ def compare_multiple_runs(cls, runs_data: dict[str, "Metrics | list"]) -> str:
288352
if not runs_data:
289353
return ""
290354

355+
# Plot the combined ROC curve
356+
metrics_instance = cls()
357+
metrics_instance._plot_roc_curve(runs_data, output_dir="comparison_output")
358+
291359
run_names = list(runs_data.keys())
292360
processed_metrics_data = {}
293361
for name, data in runs_data.items():
@@ -296,7 +364,9 @@ def compare_multiple_runs(cls, runs_data: dict[str, "Metrics | list"]) -> str:
296364
all_run_metrics = [run.get_all_metrics() for run in data]
297365
avg_metrics = {}
298366
std_dev_metrics = {}
299-
for metric_key in next(iter(all_run_metrics)).keys():
367+
# Use a stable key order from the first run's metrics
368+
first_metrics_keys = next(iter(all_run_metrics)).keys() if all_run_metrics else []
369+
for metric_key in first_metrics_keys:
300370
values = [m.get(metric_key) for m in all_run_metrics if isinstance(m.get(metric_key), (int, float))]
301371
if values:
302372
avg_metrics[metric_key] = np.mean(values)
@@ -318,7 +388,7 @@ def compare_multiple_runs(cls, runs_data: dict[str, "Metrics | list"]) -> str:
318388

319389
best_val = None
320390
for val in metric_values:
321-
if val is not None:
391+
if isinstance(val, (int, float)):
322392
if best_val is None or cls._is_better(metric_key, val, best_val):
323393
best_val = val
324394
best_values[metric_key] = best_val
@@ -348,10 +418,10 @@ def compare_multiple_runs(cls, runs_data: dict[str, "Metrics | list"]) -> str:
348418
# Format value, include std dev if available, and bold if it's the best
349419
formatted_val = f"${val:.4f}$"
350420
if std_dev_metrics and std_dev_metrics.get(metric_key) is not None:
351-
formatted_val = f"${val:.4f} \pm {std_dev_metrics.get(metric_key):.4f}$"
421+
formatted_val = f"${val:.4f} \pm {std_dev_metrics.get(metric_key):.4f}$"
352422

353423
if val == best_values.get(metric_key):
354-
formatted_val = f"\\boldmath {{formatted_val}}"
424+
formatted_val = f"\\textbf{{{formatted_val}}}"
355425
row_values.append(formatted_val)
356426

357427
latex_str += f"{display_name} & " + " & ".join(row_values) + " \\\\\n"

‎demo/demo.yaml‎

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,26 @@
33

44
# Global settings
55
seed: 42
6-
benchmark_runs: 3 # Number of times to repeat a run for averaging metrics. Set to 1 for a single run.
76
output_dir: "results"
87

9-
# A list of experiment runs to perform.
10-
# Each entry in this list defines a complete training and evaluation pipeline.
118
experiments:
129
- name: "Padim_on_MSTAR_without_preproc"
1310
dataset: SSDD
1411
preprocessor: null
1512
model: Padim
13+
runs: 2
1614

1715
- name: "Padim_on_MSTAR_with_NLM_preproc"
1816
dataset: SSDD
1917
preprocessor: NLM
2018
model: Padim
19+
runs: 2
2120

2221
- name: "Padim_on_MSTAR_with_MF_preproc"
2322
dataset: SSDD
2423
preprocessor: MedianFilter
2524
model: Padim
26-
27-
# You can add more experiments here with different combinations.
28-
# - name: "MedianFilter_on_SSDD"
29-
# dataset: SSDD
30-
# preprocessor: MedianFilter
31-
# model: Padim
32-
33-
# Configuration for models, datasets, and preprocessors
34-
# The names here must match the `experiments` list.
25+
runs: 2
3526

3627
datasets:
3728
MSTAR:
@@ -43,7 +34,6 @@ datasets:
4334

4435
models:
4536
Padim: null
46-
# Add other model-specific hyperparameters here.
4737

4838
preprocessors:
4939
NLM: null

0 commit comments

Comments
 (0)