lucasddmc commited on
Commit
c83fac3
Β·
1 Parent(s): 443cfc5

feat(attack-ui): add ground-truth aware evaluation and sample GT auto-selection

Browse files

- add a searchable Ground Truth dropdown in the Adversarial Attack + Attention tab
- show the Ground Truth field only when class labels are available from the loaded model
- auto-populate Ground Truth when a sample image is selected, using data/sample_images/metadata.json mapping
- initialize Ground Truth choices on app load for the default model (not only on change events)
- update attack report logic to use Ground Truth as the reference label when provided
- keep backward-compatible fallback to original prediction when Ground Truth is not set
- make class-dependent metrics Ground Truth aware in utils/metrics (ASR, confidence_drop, topk_drop)
- propagate Ground Truth handling to the sweep runner and add ground_truth column to CSV output
- restore correct tab structure so Apuana Cluster UI remains in its own tab
- update README notes to document Ground Truth behavior in class-related metrics

Files changed (4) hide show
  1. README.md +2 -1
  2. app.py +427 -265
  3. experiments/run_attack_sweep.py +37 -1
  4. utils/metrics.py +19 -14
README.md CHANGED
@@ -111,7 +111,8 @@ cd notebooks && jupyter notebook
111
 
112
  - **PerturbaΓ§Γ£o de imagem**: L∞, PSNR, SSIM, LPIPS, Modified Pixels
113
  - **Deslocamento de atenΓ§Γ£o**: Attention W1 (Rollout, padrΓ£o), Attention JSD (Rollout)
114
- - **Ataque**: ASR, Confidence Drop, Top-k Overlap Drop
 
115
 
116
  ## Apuana Cluster Integration (UI)
117
 
 
111
 
112
  - **PerturbaΓ§Γ£o de imagem**: L∞, PSNR, SSIM, LPIPS, Modified Pixels
113
  - **Deslocamento de atenΓ§Γ£o**: Attention W1 (Rollout, padrΓ£o), Attention JSD (Rollout)
114
+ - **Ataque**: ASR, Confidence Drop, Top-k Overlap Drop
115
+ - Na aba de ataque, quando **Ground Truth Label** Γ© informado, as mΓ©tricas de classe usam esse rΓ³tulo como referΓͺncia; sem GT, usam fallback para a prediΓ§Γ£o original.
116
 
117
  ## Apuana Cluster Integration (UI)
118
 
app.py CHANGED
@@ -145,6 +145,50 @@ def _to_path(file_like: Optional[object]) -> Optional[str]:
145
  return path
146
  return None
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  def _print_model_heads(model):
149
  """Imprime quantidade de heads do ViT ao carregar o modelo. Mais usada para debug."""
150
  try:
@@ -313,6 +357,7 @@ def run_attack(
313
  decay: float,
314
  vit_weight: float,
315
  selected_metrics,
 
316
  ) -> Tuple[List[Image.Image], str, List[List[torch.Tensor]]]:
317
  """
318
  Executa ataque adversarial (FGSM ou PGD) untargeted e extrai atenΓ§Γ£o.
@@ -385,6 +430,16 @@ def run_attack(
385
  adv_class = top_idx_adv[0].item()
386
  adv_prob = top_prob_adv[0].item()
387
 
 
 
 
 
 
 
 
 
 
 
388
  # Resolver seleΓ§Γ£o de mΓ©tricas:
389
  # - sem seleΓ§Γ£o ou com "Select All" => mostra todas
390
  # - caso contrΓ‘rio, mostra apenas as escolhidas
@@ -425,9 +480,9 @@ def run_attack(
425
  if "Modified Pixels" in requested_metrics:
426
  metric_values["Modified Pixels"] = f"{compute_modified_pixels(img_tensor, adv_tensor):.1f}%"
427
  if "Confidence Drop" in requested_metrics:
428
- metric_values["Confidence Drop"] = f"{compute_confidence_drop(orig_probs_full, adv_probs_full, orig_class):.4f}"
429
  if "Top-K Accuracy Drop" in requested_metrics:
430
- topk_drop = compute_topk_drop(orig_probs_full, adv_probs_full, orig_class, k=5)
431
  metric_values["Top-K Accuracy Drop"] = "Yes" if topk_drop >= 0.5 else "No"
432
 
433
  metric_tooltips = {
@@ -438,8 +493,8 @@ def run_attack(
438
  "SSIM": "Structural Similarity Index comparing luminance, contrast, and structure. Values near 1 mean images are structurally similar.",
439
  "LPIPS": "Learned Perceptual Image Patch Similarity based on deep features. Lower values indicate greater perceptual similarity.",
440
  "Modified Pixels": "Percentage of pixels where perturbation exceeds 1e-5 (channel max). It approximates a sparse L0/Hamming-like change count.",
441
- "Confidence Drop": "Decrease in original-class probability after attack: p_orig(original class) - p_adv(original class). Higher means stronger reduction.",
442
- "Top-K Accuracy Drop": "Yes means the original class left top-5 after attack. No means the original class is still in top-5.",
443
  }
444
 
445
  def metric_label_with_info(label: str, tooltip_text: str) -> str:
@@ -458,7 +513,7 @@ def run_attack(
458
  result = f"## {attack_type} Attack Result (Untargeted)\n\n"
459
 
460
  # Status visual
461
- if adv_class != orig_class:
462
  status_icon = ICON_SUCCESS
463
  status_text = "Success"
464
  else:
@@ -470,6 +525,18 @@ def run_attack(
470
  # Prediction Comparison (duas tabelas)
471
  result += f"### {ICON_CHART} Prediction Comparison (Top-5)\n\n"
472
 
 
 
 
 
 
 
 
 
 
 
 
 
473
  top_k_display = min(5, len(top_prob_orig), len(top_prob_adv))
474
  result += "<div class=\"vitviz-panels\">\n"
475
 
@@ -1205,6 +1272,13 @@ def create_app():
1205
  height: 100% !important;
1206
  object-fit: contain !important;
1207
  }
 
 
 
 
 
 
 
1208
 
1209
  @keyframes spin {
1210
  0% { transform: rotate(0deg); }
@@ -1437,6 +1511,13 @@ def create_app():
1437
 
1438
  default_eps_csv = ", ".join(str(v) for v in default_sweep_cfg.get("evaluation", {}).get("epsilons", [0.00784]))
1439
  default_seeds_csv = ", ".join(str(v) for v in default_sweep_cfg.get("evaluation", {}).get("seeds", [42]))
 
 
 
 
 
 
 
1440
  initial_history_choices = _history_choices(limit=100)
1441
  initial_history_value = initial_history_choices[0][1] if initial_history_choices else None
1442
  show_apuana_tab = _should_show_apuana_tab()
@@ -1593,12 +1674,93 @@ def create_app():
1593
  label="Upload Image",
1594
  type="pil"
1595
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1596
 
1597
  model_select_attack.change(
1598
  fn=toggle_model_upload,
1599
  inputs=[model_select_attack],
1600
  outputs=[model_upload_attack]
1601
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1602
  gr.Markdown("---")
1603
 
1604
  # Attack Configuration Row
@@ -1916,7 +2078,7 @@ def create_app():
1916
  inputs=[
1917
  model_upload_attack, model_select_attack, image_upload_attack,
1918
  attack_type, eps_input, alpha_input, steps_input, decay_input, vit_weight_slider,
1919
- metrics_selector
1920
  ],
1921
  outputs=[iteration_images_state, output_text_attack, cached_attentions_state]
1922
  ).then(
@@ -2088,273 +2250,273 @@ def create_app():
2088
  with gr.Tab("Apuana Cluster"):
2089
  gr.Markdown("### Configure experiments and submit jobs to Apuana via SSH")
2090
 
2091
- apuana_bundle_state = gr.State({})
2092
-
2093
- with gr.Row():
2094
- with gr.Column(scale=1):
2095
- remote_user = gr.Textbox(label="Remote User", placeholder="seu_login_cin")
2096
- remote_host = gr.Textbox(label="Remote Host", value=APUANA_DEFAULT_HOST)
2097
- remote_project_dir = gr.Textbox(label="Remote Project Dir", value="~/ViTViz")
2098
- remote_base_dir = gr.Textbox(label="Remote Jobs Base Dir", value="~/vitviz_jobs")
2099
- env_activate = gr.Textbox(label="Env Activate Script", value="$HOME/ViTViz/.venv/bin/activate")
2100
- python_module = gr.Textbox(label="Python Module", value="Python3.10")
2101
-
2102
- with gr.Column(scale=1):
2103
- config_mode = gr.Radio(
2104
- choices=["Use Config File", "Build in UI"],
2105
- value="Use Config File",
2106
- label="Config Source",
2107
- )
2108
- config_file_group = gr.Group(visible=True)
2109
- with config_file_group:
2110
- experiment_config = gr.Dropdown(
2111
- choices=cluster_experiment_choices,
2112
- value=default_cluster_config,
2113
- label="Experiment Config (local)",
2114
- )
2115
-
2116
- job_name = gr.Textbox(label="Job Name", value="vitviz_sweep")
2117
- partition = gr.Dropdown(choices=APUANA_PARTITIONS, value="short-simple", label="Partition")
2118
- qos_note = gr.Markdown(value="QoS auto: `simple`")
2119
-
2120
- with gr.Column(scale=1):
2121
- cpus = gr.Slider(1, 48, value=8, step=1, label="CPUs per task")
2122
- mem = gr.Textbox(label="Memory", value="16G")
2123
- gpus = gr.Slider(0, 4, value=1, step=1, label="GPUs")
2124
- time_limit = gr.Textbox(label="Time Limit", value="02:00:00")
2125
-
2126
- config_builder_group = gr.Group(visible=False)
2127
- with config_builder_group:
2128
- gr.Markdown("#### Dynamic Experiment Builder")
2129
  with gr.Row():
2130
  with gr.Column(scale=1):
2131
- selected_default_models = gr.Dropdown(
2132
- choices=default_model_catalog,
2133
- value=default_model_catalog,
2134
- multiselect=True,
2135
- label="Default Models",
2136
- )
2137
- custom_models_raw = gr.Textbox(
2138
- label="Custom Models (one per line)",
2139
- lines=5,
2140
- placeholder="name|path|img_size|num_classes|type|dataset",
2141
- info="Required fields: name|path. Optional fields use defaults.",
2142
- )
2143
  with gr.Column(scale=1):
2144
- selected_attacks = gr.Dropdown(
2145
- choices=default_attack_catalog,
2146
- value=default_attack_catalog,
2147
- multiselect=True,
2148
- label="Attacks",
2149
- )
2150
- eps_csv = gr.Textbox(
2151
- label="Epsilons (CSV)",
2152
- value=default_eps_csv,
2153
- placeholder="0.00784,0.01568",
2154
- )
2155
- seeds_csv = gr.Textbox(
2156
- label="Seeds (CSV)",
2157
- value=default_seeds_csv,
2158
- placeholder="42,123,456",
2159
  )
 
 
 
 
 
 
 
 
 
 
 
 
2160
  with gr.Column(scale=1):
2161
- selected_metrics = gr.Dropdown(
2162
- choices=SWEEP_METRIC_CHOICES,
2163
- value=SWEEP_METRIC_DEFAULT_SELECTION,
2164
- multiselect=True,
2165
- label="Metrics Included",
2166
- )
2167
- topk_eval = gr.Slider(1, 20, value=int(default_sweep_cfg.get("evaluation", {}).get("topk", 5)), step=1, label="Top-k")
2168
- compute_lpips_eval = gr.Checkbox(
2169
- value=bool(default_sweep_cfg.get("evaluation", {}).get("compute_lpips", True)),
2170
- label="Compute LPIPS",
2171
- )
2172
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2173
  with gr.Row():
2174
- with gr.Column(scale=1):
2175
- fgsm_params_json = gr.Code(
2176
- label="FGSM params (JSON)",
2177
- value=json.dumps(attack_default_params.get("FGSM", {"eps": None}), indent=2),
2178
- language="json",
2179
- lines=8,
2180
- )
2181
- pgd_params_json = gr.Code(
2182
- label="PGD params (JSON)",
2183
- value=json.dumps(attack_default_params.get("PGD", {"eps": None, "alpha_ratio": 0.25, "steps": 10}), indent=2),
2184
- language="json",
2185
- lines=8,
2186
- )
2187
- with gr.Column(scale=1):
2188
- mim_params_json = gr.Code(
2189
- label="MIM params (JSON)",
2190
- value=json.dumps(attack_default_params.get("MIM", {"eps": None, "alpha_ratio": 0.25, "steps": 10, "decay": 1.0}), indent=2),
2191
- language="json",
2192
- lines=8,
2193
- )
2194
- tgr_params_json = gr.Code(
2195
- label="TGR params (JSON)",
2196
- value=json.dumps(attack_default_params.get("TGR", {"eps": None, "alpha_ratio": 0.25, "steps": 10, "gamma_attn": 0.25, "gamma_qkv": 0.75, "gamma_mlp": 0.5}), indent=2),
2197
- language="json",
2198
- lines=8,
2199
- )
2200
- with gr.Column(scale=1):
2201
- saga_params_json = gr.Code(
2202
- label="SAGA params (JSON)",
2203
- value=json.dumps(attack_default_params.get("SAGA", {"eps": None, "alpha_ratio": 0.25, "steps": 10, "cnn_backbone": None}), indent=2),
2204
- language="json",
2205
- lines=8,
2206
- )
2207
-
2208
- def toggle_apuana_config_mode(mode):
2209
- use_builder = mode == "Build in UI"
2210
- return (
2211
- gr.update(visible=not use_builder),
2212
- gr.update(visible=use_builder),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2213
  )
2214
-
2215
- def update_qos_note(partition_value):
2216
- return f"QoS auto: `{_qos_from_partition(partition_value)}`"
2217
-
2218
- partition.change(
2219
- fn=update_qos_note,
2220
- inputs=[partition],
2221
- outputs=[qos_note],
2222
- )
2223
-
2224
- config_mode.change(
2225
- fn=toggle_apuana_config_mode,
2226
- inputs=[config_mode],
2227
- outputs=[config_file_group, config_builder_group],
2228
- )
2229
-
2230
- with gr.Row():
2231
- btn_generate = gr.Button("1) Generate Bundle", variant="secondary")
2232
- btn_sync = gr.Button("2) Sync Bundle", variant="secondary")
2233
- btn_submit = gr.Button("3) Submit Job", variant="primary")
2234
-
2235
- gr.Markdown("#### Saved Bundle History")
2236
- with gr.Row():
2237
- bundle_history = gr.Dropdown(
2238
- label="Saved Bundles",
2239
- choices=initial_history_choices,
2240
- value=initial_history_value,
2241
- allow_custom_value=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2242
  )
2243
- with gr.Row():
2244
- btn_refresh_history = gr.Button("Refresh History", variant="secondary")
2245
- btn_load_selected = gr.Button("Load Selected Bundle", variant="secondary")
2246
- btn_load_last = gr.Button("Load Last Bundle", variant="secondary")
2247
-
2248
- with gr.Row():
2249
- job_id_box = gr.Textbox(label="Job ID", placeholder="Preenchido apos submissao ou manual")
2250
- btn_status = gr.Button("Check Job Status")
2251
- btn_queue = gr.Button("List My Queue")
2252
- btn_logs = gr.Button("Check Last Job Logs")
2253
- btn_cancel = gr.Button("Cancel Job")
2254
-
2255
- gr.Markdown("#### Optional: Download remote outputs")
2256
- with gr.Row():
2257
- remote_download_path = gr.Textbox(label="Remote Path", placeholder="~/vitviz_jobs/.../logs/")
2258
- local_download_path = gr.Textbox(label="Local Destination", value="results/raw/apuana_downloads")
2259
- btn_download = gr.Button("Download via rsync")
2260
-
2261
- apuana_output = gr.Markdown(value="Ready to configure Apuana job.")
2262
-
2263
- btn_generate.click(
2264
- fn=apuana_generate_bundle,
2265
- inputs=[
2266
- remote_user,
2267
- remote_host,
2268
- remote_project_dir,
2269
- remote_base_dir,
2270
- env_activate,
2271
- python_module,
2272
- config_mode,
2273
- experiment_config,
2274
- job_name,
2275
- partition,
2276
- cpus,
2277
- mem,
2278
- gpus,
2279
- time_limit,
2280
- selected_default_models,
2281
- custom_models_raw,
2282
- selected_attacks,
2283
- fgsm_params_json,
2284
- pgd_params_json,
2285
- mim_params_json,
2286
- tgr_params_json,
2287
- saga_params_json,
2288
- eps_csv,
2289
- seeds_csv,
2290
- selected_metrics,
2291
- topk_eval,
2292
- compute_lpips_eval,
2293
- ],
2294
- outputs=[apuana_output, apuana_bundle_state],
2295
- )
2296
-
2297
- btn_sync.click(
2298
- fn=apuana_sync_bundle,
2299
- inputs=[remote_user, remote_host, apuana_bundle_state],
2300
- outputs=[apuana_output],
2301
- )
2302
-
2303
- btn_submit.click(
2304
- fn=apuana_submit_bundle,
2305
- inputs=[remote_user, remote_host, apuana_bundle_state],
2306
- outputs=[apuana_output, job_id_box],
2307
- )
2308
-
2309
- btn_status.click(
2310
- fn=apuana_check_job,
2311
- inputs=[remote_user, remote_host, job_id_box],
2312
- outputs=[apuana_output],
2313
- )
2314
-
2315
- btn_queue.click(
2316
- fn=apuana_list_jobs,
2317
- inputs=[remote_user, remote_host],
2318
- outputs=[apuana_output],
2319
- )
2320
-
2321
- btn_logs.click(
2322
- fn=apuana_check_last_logs,
2323
- inputs=[remote_user, remote_host, apuana_bundle_state, job_id_box],
2324
- outputs=[apuana_output],
2325
- )
2326
-
2327
- btn_cancel.click(
2328
- fn=apuana_cancel,
2329
- inputs=[remote_user, remote_host, job_id_box],
2330
- outputs=[apuana_output],
2331
- )
2332
-
2333
- btn_download.click(
2334
- fn=apuana_download_results,
2335
- inputs=[remote_user, remote_host, remote_download_path, local_download_path],
2336
- outputs=[apuana_output],
2337
- )
2338
-
2339
- btn_refresh_history.click(
2340
- fn=apuana_refresh_history,
2341
- inputs=[],
2342
- outputs=[apuana_output, bundle_history],
2343
- )
2344
-
2345
- btn_load_selected.click(
2346
- fn=apuana_load_selected_bundle,
2347
- inputs=[bundle_history],
2348
- outputs=[apuana_output, apuana_bundle_state, job_id_box, remote_download_path, bundle_history],
2349
- )
2350
-
2351
- btn_load_last.click(
2352
- fn=apuana_load_last_bundle,
2353
- inputs=[],
2354
- outputs=[apuana_output, apuana_bundle_state, job_id_box, remote_download_path, bundle_history],
2355
- )
2356
-
2357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2358
  return app
2359
 
2360
  if __name__ == "__main__":
@@ -2368,4 +2530,4 @@ if __name__ == "__main__":
2368
  )
2369
 
2370
  except KeyboardInterrupt:
2371
- print("\nShutting down gracefully...")
 
145
  return path
146
  return None
147
 
148
+
149
+ def _load_sample_ground_truth_map(metadata_path: Path) -> Dict[str, int]:
150
+ """Load sample filename -> ground truth class index mapping from metadata.json."""
151
+ if not metadata_path.exists():
152
+ return {}
153
+ try:
154
+ with open(metadata_path, "r", encoding="utf-8") as f:
155
+ payload = json.load(f) or {}
156
+ except Exception:
157
+ return {}
158
+
159
+ files = payload.get("downloaded_files") or []
160
+ classes = payload.get("suggested_classes") or []
161
+ n = min(len(files), len(classes))
162
+ out: Dict[str, int] = {}
163
+ for i in range(n):
164
+ filename = str(files[i]).strip()
165
+ cls = classes[i] if isinstance(classes[i], dict) else {}
166
+ cls_id = cls.get("imagenet_id")
167
+ if not filename:
168
+ continue
169
+ try:
170
+ out[filename] = int(cls_id)
171
+ except (TypeError, ValueError):
172
+ continue
173
+ return out
174
+
175
+
176
+ def _build_ground_truth_choices(class_names: Optional[Dict[int, str]]) -> List[Tuple[str, str]]:
177
+ """Create dropdown choices [(display_label, value)] for ground truth selection."""
178
+ if not class_names:
179
+ return []
180
+
181
+ normalized: List[Tuple[int, str]] = []
182
+ for key, value in class_names.items():
183
+ try:
184
+ idx = int(key)
185
+ except (TypeError, ValueError):
186
+ continue
187
+ normalized.append((idx, str(value)))
188
+
189
+ normalized.sort(key=lambda x: x[0])
190
+ return [(f"{idx:04d} - {name}", str(idx)) for idx, name in normalized]
191
+
192
  def _print_model_heads(model):
193
  """Imprime quantidade de heads do ViT ao carregar o modelo. Mais usada para debug."""
194
  try:
 
357
  decay: float,
358
  vit_weight: float,
359
  selected_metrics,
360
+ ground_truth_label,
361
  ) -> Tuple[List[Image.Image], str, List[List[torch.Tensor]]]:
362
  """
363
  Executa ataque adversarial (FGSM ou PGD) untargeted e extrai atenΓ§Γ£o.
 
430
  adv_class = top_idx_adv[0].item()
431
  adv_prob = top_prob_adv[0].item()
432
 
433
+ gt_class: Optional[int] = None
434
+ if ground_truth_label not in (None, ""):
435
+ try:
436
+ gt_class = int(ground_truth_label)
437
+ except (TypeError, ValueError):
438
+ gt_class = None
439
+
440
+ # If GT is present, class-dependent metrics are evaluated against GT.
441
+ eval_label = gt_class if gt_class is not None else orig_class
442
+
443
  # Resolver seleΓ§Γ£o de mΓ©tricas:
444
  # - sem seleΓ§Γ£o ou com "Select All" => mostra todas
445
  # - caso contrΓ‘rio, mostra apenas as escolhidas
 
480
  if "Modified Pixels" in requested_metrics:
481
  metric_values["Modified Pixels"] = f"{compute_modified_pixels(img_tensor, adv_tensor):.1f}%"
482
  if "Confidence Drop" in requested_metrics:
483
+ metric_values["Confidence Drop"] = f"{compute_confidence_drop(orig_probs_full, adv_probs_full, eval_label):.4f}"
484
  if "Top-K Accuracy Drop" in requested_metrics:
485
+ topk_drop = compute_topk_drop(orig_probs_full, adv_probs_full, eval_label, k=5)
486
  metric_values["Top-K Accuracy Drop"] = "Yes" if topk_drop >= 0.5 else "No"
487
 
488
  metric_tooltips = {
 
493
  "SSIM": "Structural Similarity Index comparing luminance, contrast, and structure. Values near 1 mean images are structurally similar.",
494
  "LPIPS": "Learned Perceptual Image Patch Similarity based on deep features. Lower values indicate greater perceptual similarity.",
495
  "Modified Pixels": "Percentage of pixels where perturbation exceeds 1e-5 (channel max). It approximates a sparse L0/Hamming-like change count.",
496
+ "Confidence Drop": "Decrease in reference-class probability after attack: p_orig(reference class) - p_adv(reference class). If Ground Truth is set, reference class = Ground Truth; otherwise it falls back to original prediction.",
497
+ "Top-K Accuracy Drop": "Yes means the reference class left top-5 after attack. If Ground Truth is set, reference class = Ground Truth; otherwise it falls back to original prediction.",
498
  }
499
 
500
  def metric_label_with_info(label: str, tooltip_text: str) -> str:
 
513
  result = f"## {attack_type} Attack Result (Untargeted)\n\n"
514
 
515
  # Status visual
516
+ if adv_class != eval_label:
517
  status_icon = ICON_SUCCESS
518
  status_text = "Success"
519
  else:
 
525
  # Prediction Comparison (duas tabelas)
526
  result += f"### {ICON_CHART} Prediction Comparison (Top-5)\n\n"
527
 
528
+ if gt_class is not None:
529
+ gt_name = None
530
+ if class_names:
531
+ gt_name = class_names.get(gt_class) if isinstance(class_names, dict) else None
532
+ if gt_name is None and isinstance(class_names, dict):
533
+ gt_name = class_names.get(str(gt_class))
534
+ if gt_name is not None:
535
+ gt_display = f"{gt_name} (class {gt_class})"
536
+ else:
537
+ gt_display = f"Class {gt_class}"
538
+ result += f"**Ground Truth:** {gt_display}\n\n"
539
+
540
  top_k_display = min(5, len(top_prob_orig), len(top_prob_adv))
541
  result += "<div class=\"vitviz-panels\">\n"
542
 
 
1272
  height: 100% !important;
1273
  object-fit: contain !important;
1274
  }
1275
+
1276
+ /* Keep native Gradio Gallery captions visible across themes. */
1277
+ .gradio-container .caption-label {
1278
+ color: #111111 !important;
1279
+ background: rgba(255, 255, 255, 0.92) !important;
1280
+ border: 1px solid rgba(0, 0, 0, 0.18) !important;
1281
+ }
1282
 
1283
  @keyframes spin {
1284
  0% { transform: rotate(0deg); }
 
1511
 
1512
  default_eps_csv = ", ".join(str(v) for v in default_sweep_cfg.get("evaluation", {}).get("epsilons", [0.00784]))
1513
  default_seeds_csv = ", ".join(str(v) for v in default_sweep_cfg.get("evaluation", {}).get("seeds", [42]))
1514
+ sample_images_dir = PROJECT_ROOT / "data" / "sample_images"
1515
+ sample_image_paths = sorted(
1516
+ p for p in sample_images_dir.glob("*")
1517
+ if p.suffix.lower() in {".jpg", ".jpeg", ".png", ".bmp", ".webp"}
1518
+ ) if sample_images_dir.exists() else []
1519
+ sample_gallery_items = [(str(p), p.name) for p in sample_image_paths]
1520
+ sample_gt_map = _load_sample_ground_truth_map(sample_images_dir / "metadata.json")
1521
  initial_history_choices = _history_choices(limit=100)
1522
  initial_history_value = initial_history_choices[0][1] if initial_history_choices else None
1523
  show_apuana_tab = _should_show_apuana_tab()
 
1674
  label="Upload Image",
1675
  type="pil"
1676
  )
1677
+ if sample_gallery_items:
1678
+ gr.Markdown("**Or choose a sample image:**")
1679
+ sample_gallery_attack = gr.Gallery(
1680
+ value=sample_gallery_items,
1681
+ label="Sample Image Carousel (click to select)",
1682
+ columns=min(6, max(1, len(sample_gallery_items))),
1683
+ rows=1,
1684
+ object_fit="contain",
1685
+ height=180,
1686
+ allow_preview=False,
1687
+ )
1688
+ else:
1689
+ sample_gallery_attack = None
1690
+ gr.Markdown("_No sample images found in data/sample_images._")
1691
+
1692
+ ground_truth_dropdown = gr.Dropdown(
1693
+ choices=[],
1694
+ value=None,
1695
+ multiselect=False,
1696
+ filterable=True,
1697
+ interactive=True,
1698
+ visible=False,
1699
+ label="Ground Truth Label",
1700
+ info="Visible only when model classes are available. Supports search.",
1701
+ )
1702
+
1703
+ ground_truth_values_state = gr.State([])
1704
 
1705
  model_select_attack.change(
1706
  fn=toggle_model_upload,
1707
  inputs=[model_select_attack],
1708
  outputs=[model_upload_attack]
1709
  )
1710
+
1711
+ def refresh_ground_truth_dropdown(model_file, model_selection):
1712
+ model_path = _get_model_path_from_selection(model_selection, model_file)
1713
+ if model_path is None:
1714
+ return gr.update(visible=False, choices=[], value=None), []
1715
+ try:
1716
+ _, class_names, _, _ = load_model_and_labels(model_path, None, device=DEVICE)
1717
+ except Exception:
1718
+ return gr.update(visible=False, choices=[], value=None), []
1719
+
1720
+ choices = _build_ground_truth_choices(class_names)
1721
+ values = [v for _, v in choices]
1722
+ if not choices:
1723
+ return gr.update(visible=False, choices=[], value=None), []
1724
+ return gr.update(visible=True, choices=choices, value=None), values
1725
+
1726
+ model_select_attack.change(
1727
+ fn=refresh_ground_truth_dropdown,
1728
+ inputs=[model_upload_attack, model_select_attack],
1729
+ outputs=[ground_truth_dropdown, ground_truth_values_state],
1730
+ )
1731
+
1732
+ model_upload_attack.change(
1733
+ fn=refresh_ground_truth_dropdown,
1734
+ inputs=[model_upload_attack, model_select_attack],
1735
+ outputs=[ground_truth_dropdown, ground_truth_values_state],
1736
+ )
1737
+
1738
+ app.load(
1739
+ fn=refresh_ground_truth_dropdown,
1740
+ inputs=[model_upload_attack, model_select_attack],
1741
+ outputs=[ground_truth_dropdown, ground_truth_values_state],
1742
+ )
1743
+
1744
+ if sample_gallery_attack is not None:
1745
+ def select_sample_image_for_attack(valid_values, evt: gr.SelectData):
1746
+ idx = evt.index
1747
+ if isinstance(idx, (tuple, list)):
1748
+ idx = idx[0] if idx else 0
1749
+ try:
1750
+ selected = sample_image_paths[int(idx)]
1751
+ gt_idx = sample_gt_map.get(selected.name)
1752
+ gt_update = None
1753
+ if gt_idx is not None and str(gt_idx) in set(valid_values or []):
1754
+ gt_update = str(gt_idx)
1755
+ return Image.open(selected).convert("RGB"), gr.update(value=gt_update)
1756
+ except Exception:
1757
+ return None, gr.update()
1758
+
1759
+ sample_gallery_attack.select(
1760
+ fn=select_sample_image_for_attack,
1761
+ inputs=[ground_truth_values_state],
1762
+ outputs=[image_upload_attack, ground_truth_dropdown],
1763
+ )
1764
  gr.Markdown("---")
1765
 
1766
  # Attack Configuration Row
 
2078
  inputs=[
2079
  model_upload_attack, model_select_attack, image_upload_attack,
2080
  attack_type, eps_input, alpha_input, steps_input, decay_input, vit_weight_slider,
2081
+ metrics_selector, ground_truth_dropdown
2082
  ],
2083
  outputs=[iteration_images_state, output_text_attack, cached_attentions_state]
2084
  ).then(
 
2250
  with gr.Tab("Apuana Cluster"):
2251
  gr.Markdown("### Configure experiments and submit jobs to Apuana via SSH")
2252
 
2253
+ apuana_bundle_state = gr.State({})
2254
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2255
  with gr.Row():
2256
  with gr.Column(scale=1):
2257
+ remote_user = gr.Textbox(label="Remote User", placeholder="seu_login_cin")
2258
+ remote_host = gr.Textbox(label="Remote Host", value=APUANA_DEFAULT_HOST)
2259
+ remote_project_dir = gr.Textbox(label="Remote Project Dir", value="~/ViTViz")
2260
+ remote_base_dir = gr.Textbox(label="Remote Jobs Base Dir", value="~/vitviz_jobs")
2261
+ env_activate = gr.Textbox(label="Env Activate Script", value="$HOME/ViTViz/.venv/bin/activate")
2262
+ python_module = gr.Textbox(label="Python Module", value="Python3.10")
2263
+
 
 
 
 
 
2264
  with gr.Column(scale=1):
2265
+ config_mode = gr.Radio(
2266
+ choices=["Use Config File", "Build in UI"],
2267
+ value="Use Config File",
2268
+ label="Config Source",
 
 
 
 
 
 
 
 
 
 
 
2269
  )
2270
+ config_file_group = gr.Group(visible=True)
2271
+ with config_file_group:
2272
+ experiment_config = gr.Dropdown(
2273
+ choices=cluster_experiment_choices,
2274
+ value=default_cluster_config,
2275
+ label="Experiment Config (local)",
2276
+ )
2277
+
2278
+ job_name = gr.Textbox(label="Job Name", value="vitviz_sweep")
2279
+ partition = gr.Dropdown(choices=APUANA_PARTITIONS, value="short-simple", label="Partition")
2280
+ qos_note = gr.Markdown(value="QoS auto: `simple`")
2281
+
2282
  with gr.Column(scale=1):
2283
+ cpus = gr.Slider(1, 48, value=8, step=1, label="CPUs per task")
2284
+ mem = gr.Textbox(label="Memory", value="16G")
2285
+ gpus = gr.Slider(0, 4, value=1, step=1, label="GPUs")
2286
+ time_limit = gr.Textbox(label="Time Limit", value="02:00:00")
2287
+
2288
+ config_builder_group = gr.Group(visible=False)
2289
+ with config_builder_group:
2290
+ gr.Markdown("#### Dynamic Experiment Builder")
2291
+ with gr.Row():
2292
+ with gr.Column(scale=1):
2293
+ selected_default_models = gr.Dropdown(
2294
+ choices=default_model_catalog,
2295
+ value=default_model_catalog,
2296
+ multiselect=True,
2297
+ label="Default Models",
2298
+ )
2299
+ custom_models_raw = gr.Textbox(
2300
+ label="Custom Models (one per line)",
2301
+ lines=5,
2302
+ placeholder="name|path|img_size|num_classes|type|dataset",
2303
+ info="Required fields: name|path. Optional fields use defaults.",
2304
+ )
2305
+ with gr.Column(scale=1):
2306
+ selected_attacks = gr.Dropdown(
2307
+ choices=default_attack_catalog,
2308
+ value=default_attack_catalog,
2309
+ multiselect=True,
2310
+ label="Attacks",
2311
+ )
2312
+ eps_csv = gr.Textbox(
2313
+ label="Epsilons (CSV)",
2314
+ value=default_eps_csv,
2315
+ placeholder="0.00784,0.01568",
2316
+ )
2317
+ seeds_csv = gr.Textbox(
2318
+ label="Seeds (CSV)",
2319
+ value=default_seeds_csv,
2320
+ placeholder="42,123,456",
2321
+ )
2322
+ with gr.Column(scale=1):
2323
+ selected_metrics = gr.Dropdown(
2324
+ choices=SWEEP_METRIC_CHOICES,
2325
+ value=SWEEP_METRIC_DEFAULT_SELECTION,
2326
+ multiselect=True,
2327
+ label="Metrics Included",
2328
+ )
2329
+ topk_eval = gr.Slider(1, 20, value=int(default_sweep_cfg.get("evaluation", {}).get("topk", 5)), step=1, label="Top-k")
2330
+ compute_lpips_eval = gr.Checkbox(
2331
+ value=bool(default_sweep_cfg.get("evaluation", {}).get("compute_lpips", True)),
2332
+ label="Compute LPIPS",
2333
+ )
2334
+
2335
+ with gr.Row():
2336
+ with gr.Column(scale=1):
2337
+ fgsm_params_json = gr.Code(
2338
+ label="FGSM params (JSON)",
2339
+ value=json.dumps(attack_default_params.get("FGSM", {"eps": None}), indent=2),
2340
+ language="json",
2341
+ lines=8,
2342
+ )
2343
+ pgd_params_json = gr.Code(
2344
+ label="PGD params (JSON)",
2345
+ value=json.dumps(attack_default_params.get("PGD", {"eps": None, "alpha_ratio": 0.25, "steps": 10}), indent=2),
2346
+ language="json",
2347
+ lines=8,
2348
+ )
2349
+ with gr.Column(scale=1):
2350
+ mim_params_json = gr.Code(
2351
+ label="MIM params (JSON)",
2352
+ value=json.dumps(attack_default_params.get("MIM", {"eps": None, "alpha_ratio": 0.25, "steps": 10, "decay": 1.0}), indent=2),
2353
+ language="json",
2354
+ lines=8,
2355
+ )
2356
+ tgr_params_json = gr.Code(
2357
+ label="TGR params (JSON)",
2358
+ value=json.dumps(attack_default_params.get("TGR", {"eps": None, "alpha_ratio": 0.25, "steps": 10, "gamma_attn": 0.25, "gamma_qkv": 0.75, "gamma_mlp": 0.5}), indent=2),
2359
+ language="json",
2360
+ lines=8,
2361
+ )
2362
+ with gr.Column(scale=1):
2363
+ saga_params_json = gr.Code(
2364
+ label="SAGA params (JSON)",
2365
+ value=json.dumps(attack_default_params.get("SAGA", {"eps": None, "alpha_ratio": 0.25, "steps": 10, "cnn_backbone": None}), indent=2),
2366
+ language="json",
2367
+ lines=8,
2368
+ )
2369
+
2370
+ def toggle_apuana_config_mode(mode):
2371
+ use_builder = mode == "Build in UI"
2372
+ return (
2373
+ gr.update(visible=not use_builder),
2374
+ gr.update(visible=use_builder),
2375
+ )
2376
+
2377
+ def update_qos_note(partition_value):
2378
+ return f"QoS auto: `{_qos_from_partition(partition_value)}`"
2379
+
2380
+ partition.change(
2381
+ fn=update_qos_note,
2382
+ inputs=[partition],
2383
+ outputs=[qos_note],
2384
+ )
2385
+
2386
+ config_mode.change(
2387
+ fn=toggle_apuana_config_mode,
2388
+ inputs=[config_mode],
2389
+ outputs=[config_file_group, config_builder_group],
2390
+ )
2391
+
2392
  with gr.Row():
2393
+ btn_generate = gr.Button("1) Generate Bundle", variant="secondary")
2394
+ btn_sync = gr.Button("2) Sync Bundle", variant="secondary")
2395
+ btn_submit = gr.Button("3) Submit Job", variant="primary")
2396
+
2397
+ gr.Markdown("#### Saved Bundle History")
2398
+ with gr.Row():
2399
+ bundle_history = gr.Dropdown(
2400
+ label="Saved Bundles",
2401
+ choices=initial_history_choices,
2402
+ value=initial_history_value,
2403
+ allow_custom_value=False,
2404
+ )
2405
+ with gr.Row():
2406
+ btn_refresh_history = gr.Button("Refresh History", variant="secondary")
2407
+ btn_load_selected = gr.Button("Load Selected Bundle", variant="secondary")
2408
+ btn_load_last = gr.Button("Load Last Bundle", variant="secondary")
2409
+
2410
+ with gr.Row():
2411
+ job_id_box = gr.Textbox(label="Job ID", placeholder="Preenchido apos submissao ou manual")
2412
+ btn_status = gr.Button("Check Job Status")
2413
+ btn_queue = gr.Button("List My Queue")
2414
+ btn_logs = gr.Button("Check Last Job Logs")
2415
+ btn_cancel = gr.Button("Cancel Job")
2416
+
2417
+ gr.Markdown("#### Optional: Download remote outputs")
2418
+ with gr.Row():
2419
+ remote_download_path = gr.Textbox(label="Remote Path", placeholder="~/vitviz_jobs/.../logs/")
2420
+ local_download_path = gr.Textbox(label="Local Destination", value="results/raw/apuana_downloads")
2421
+ btn_download = gr.Button("Download via rsync")
2422
+
2423
+ apuana_output = gr.Markdown(value="Ready to configure Apuana job.")
2424
+
2425
+ btn_generate.click(
2426
+ fn=apuana_generate_bundle,
2427
+ inputs=[
2428
+ remote_user,
2429
+ remote_host,
2430
+ remote_project_dir,
2431
+ remote_base_dir,
2432
+ env_activate,
2433
+ python_module,
2434
+ config_mode,
2435
+ experiment_config,
2436
+ job_name,
2437
+ partition,
2438
+ cpus,
2439
+ mem,
2440
+ gpus,
2441
+ time_limit,
2442
+ selected_default_models,
2443
+ custom_models_raw,
2444
+ selected_attacks,
2445
+ fgsm_params_json,
2446
+ pgd_params_json,
2447
+ mim_params_json,
2448
+ tgr_params_json,
2449
+ saga_params_json,
2450
+ eps_csv,
2451
+ seeds_csv,
2452
+ selected_metrics,
2453
+ topk_eval,
2454
+ compute_lpips_eval,
2455
+ ],
2456
+ outputs=[apuana_output, apuana_bundle_state],
2457
  )
2458
+
2459
+ btn_sync.click(
2460
+ fn=apuana_sync_bundle,
2461
+ inputs=[remote_user, remote_host, apuana_bundle_state],
2462
+ outputs=[apuana_output],
2463
+ )
2464
+
2465
+ btn_submit.click(
2466
+ fn=apuana_submit_bundle,
2467
+ inputs=[remote_user, remote_host, apuana_bundle_state],
2468
+ outputs=[apuana_output, job_id_box],
2469
+ )
2470
+
2471
+ btn_status.click(
2472
+ fn=apuana_check_job,
2473
+ inputs=[remote_user, remote_host, job_id_box],
2474
+ outputs=[apuana_output],
2475
+ )
2476
+
2477
+ btn_queue.click(
2478
+ fn=apuana_list_jobs,
2479
+ inputs=[remote_user, remote_host],
2480
+ outputs=[apuana_output],
2481
+ )
2482
+
2483
+ btn_logs.click(
2484
+ fn=apuana_check_last_logs,
2485
+ inputs=[remote_user, remote_host, apuana_bundle_state, job_id_box],
2486
+ outputs=[apuana_output],
2487
+ )
2488
+
2489
+ btn_cancel.click(
2490
+ fn=apuana_cancel,
2491
+ inputs=[remote_user, remote_host, job_id_box],
2492
+ outputs=[apuana_output],
2493
+ )
2494
+
2495
+ btn_download.click(
2496
+ fn=apuana_download_results,
2497
+ inputs=[remote_user, remote_host, remote_download_path, local_download_path],
2498
+ outputs=[apuana_output],
2499
+ )
2500
+
2501
+ btn_refresh_history.click(
2502
+ fn=apuana_refresh_history,
2503
+ inputs=[],
2504
+ outputs=[apuana_output, bundle_history],
2505
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2506
 
2507
+ btn_load_selected.click(
2508
+ fn=apuana_load_selected_bundle,
2509
+ inputs=[bundle_history],
2510
+ outputs=[apuana_output, apuana_bundle_state, job_id_box, remote_download_path, bundle_history],
2511
+ )
2512
+
2513
+ btn_load_last.click(
2514
+ fn=apuana_load_last_bundle,
2515
+ inputs=[],
2516
+ outputs=[apuana_output, apuana_bundle_state, job_id_box, remote_download_path, bundle_history],
2517
+ )
2518
+
2519
+
2520
  return app
2521
 
2522
  if __name__ == "__main__":
 
2530
  )
2531
 
2532
  except KeyboardInterrupt:
2533
+ print("\nShutting down gracefully...")
experiments/run_attack_sweep.py CHANGED
@@ -20,6 +20,7 @@ Usage:
20
 
21
  import argparse
22
  import csv
 
23
  import os
24
  import sys
25
  import time
@@ -137,7 +138,7 @@ def create_attack(model, attack_cfg: dict, eps: float):
137
 
138
  CSV_COLUMNS = [
139
  "model", "attack", "epsilon", "seed", "image",
140
- "orig_pred", "orig_conf", "adv_pred", "adv_conf",
141
  # Attack success
142
  "asr", "confidence_drop", "topk_drop",
143
  # Image quality
@@ -155,6 +156,35 @@ ATTENTION_METRIC_COLUMNS = [
155
  ]
156
 
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  # ═══════════════════════════════════════════════════════════════════════════
159
  # Main sweep
160
  # ═══════════════════════════════════════════════════════════════════════════
@@ -209,6 +239,7 @@ def run_single(
209
  img_path: Path, device: torch.device,
210
  eval_cfg: dict, attn_cfg: dict,
211
  selected_metrics: set,
 
212
  ) -> dict:
213
  """Run a single attack and compute all metrics. Returns a dict row."""
214
  set_seed(seed)
@@ -257,12 +288,14 @@ def run_single(
257
 
258
  # Attack success metrics (compute only when selected; keep fixed CSV schema)
259
  atk_metrics = {k: float("nan") for k in ATTACK_METRIC_COLUMNS}
 
260
  if selected_metrics.intersection(ATTACK_METRIC_COLUMNS):
261
  raw_atk_metrics = compute_all_attack_metrics(
262
  orig_probs,
263
  adv_probs,
264
  orig_pred,
265
  adv_pred,
 
266
  k=eval_cfg.get("topk", 5),
267
  )
268
  for k in ATTACK_METRIC_COLUMNS:
@@ -303,6 +336,7 @@ def run_single(
303
  "orig_conf": f"{orig_conf:.6f}",
304
  "adv_pred": str(adv_pred),
305
  "adv_conf": f"{adv_conf:.6f}",
 
306
  **{k: ("" if np.isnan(v) else f"{v:.6f}") for k, v in atk_metrics.items()},
307
  **{k: ("" if np.isnan(v) else f"{v:.6f}") for k, v in img_metrics.items()},
308
  **{k: ("" if np.isnan(v) else f"{v:.6f}") for k, v in attn_metrics.items()},
@@ -335,6 +369,7 @@ def main():
335
  results_dir = Path(config["output"]["results_dir"])
336
  results_dir.mkdir(parents=True, exist_ok=True)
337
  images_dir = Path(config["data"]["images_dir"])
 
338
 
339
  if not images_dir.exists():
340
  images_dir.mkdir(parents=True, exist_ok=True)
@@ -426,6 +461,7 @@ def main():
426
  attack_cfg_copy, eps, seed, img_path, device,
427
  config["evaluation"], config.get("attention", {}),
428
  selected_metrics,
 
429
  )
430
 
431
  # Append to CSV
 
20
 
21
  import argparse
22
  import csv
23
+ import json
24
  import os
25
  import sys
26
  import time
 
138
 
139
  CSV_COLUMNS = [
140
  "model", "attack", "epsilon", "seed", "image",
141
+ "orig_pred", "orig_conf", "adv_pred", "adv_conf", "ground_truth",
142
  # Attack success
143
  "asr", "confidence_drop", "topk_drop",
144
  # Image quality
 
156
  ]
157
 
158
 
159
+ def load_sample_ground_truth_map(sample_dir: Path) -> dict[str, int]:
160
+ """Load sample filename -> class index mapping from metadata.json (if present)."""
161
+ metadata_path = sample_dir / "metadata.json"
162
+ if not metadata_path.exists():
163
+ return {}
164
+
165
+ try:
166
+ with open(metadata_path, "r", encoding="utf-8") as f:
167
+ payload = json.load(f) or {}
168
+ except Exception:
169
+ return {}
170
+
171
+ files = payload.get("downloaded_files") or []
172
+ classes = payload.get("suggested_classes") or []
173
+ n = min(len(files), len(classes))
174
+ mapping: dict[str, int] = {}
175
+ for i in range(n):
176
+ filename = str(files[i]).strip()
177
+ cls = classes[i] if isinstance(classes[i], dict) else {}
178
+ cls_id = cls.get("imagenet_id")
179
+ if not filename:
180
+ continue
181
+ try:
182
+ mapping[filename] = int(cls_id)
183
+ except (TypeError, ValueError):
184
+ continue
185
+ return mapping
186
+
187
+
188
  # ═══════════════════════════════════════════════════════════════════════════
189
  # Main sweep
190
  # ═══════════════════════════════════════════════════════════════════════════
 
239
  img_path: Path, device: torch.device,
240
  eval_cfg: dict, attn_cfg: dict,
241
  selected_metrics: set,
242
+ sample_gt_map: dict[str, int],
243
  ) -> dict:
244
  """Run a single attack and compute all metrics. Returns a dict row."""
245
  set_seed(seed)
 
288
 
289
  # Attack success metrics (compute only when selected; keep fixed CSV schema)
290
  atk_metrics = {k: float("nan") for k in ATTACK_METRIC_COLUMNS}
291
+ ground_truth = sample_gt_map.get(img_path.name)
292
  if selected_metrics.intersection(ATTACK_METRIC_COLUMNS):
293
  raw_atk_metrics = compute_all_attack_metrics(
294
  orig_probs,
295
  adv_probs,
296
  orig_pred,
297
  adv_pred,
298
+ ground_truth=ground_truth,
299
  k=eval_cfg.get("topk", 5),
300
  )
301
  for k in ATTACK_METRIC_COLUMNS:
 
336
  "orig_conf": f"{orig_conf:.6f}",
337
  "adv_pred": str(adv_pred),
338
  "adv_conf": f"{adv_conf:.6f}",
339
+ "ground_truth": "" if ground_truth is None else str(ground_truth),
340
  **{k: ("" if np.isnan(v) else f"{v:.6f}") for k, v in atk_metrics.items()},
341
  **{k: ("" if np.isnan(v) else f"{v:.6f}") for k, v in img_metrics.items()},
342
  **{k: ("" if np.isnan(v) else f"{v:.6f}") for k, v in attn_metrics.items()},
 
369
  results_dir = Path(config["output"]["results_dir"])
370
  results_dir.mkdir(parents=True, exist_ok=True)
371
  images_dir = Path(config["data"]["images_dir"])
372
+ sample_gt_map = load_sample_ground_truth_map(images_dir)
373
 
374
  if not images_dir.exists():
375
  images_dir.mkdir(parents=True, exist_ok=True)
 
461
  attack_cfg_copy, eps, seed, img_path, device,
462
  config["evaluation"], config.get("attention", {}),
463
  selected_metrics,
464
+ sample_gt_map,
465
  )
466
 
467
  # Append to CSV
utils/metrics.py CHANGED
@@ -259,42 +259,42 @@ def compute_modified_pixels(orig: torch.Tensor, adv: torch.Tensor,
259
  # Attack Success Metrics
260
  # ═══════════════════════════════════════════════════════════════════════════
261
 
262
- def compute_asr(orig_pred: int, adv_pred: int) -> float:
263
  """Attack Success Rate for a single sample.
264
 
265
- Returns 1.0 if attack changed the prediction, 0.0 otherwise.
266
  For batch ASR, average across samples.
267
  """
268
- return 1.0 if orig_pred != adv_pred else 0.0
269
 
270
 
271
  def compute_confidence_drop(orig_probs: torch.Tensor,
272
  adv_probs: torch.Tensor,
273
- orig_label: int) -> float:
274
- """Confidence drop on the original class.
275
 
276
  Args:
277
  orig_probs: Softmax probabilities for original image (1D tensor).
278
  adv_probs: Softmax probabilities for adversarial image (1D tensor).
279
- orig_label: Original predicted class index.
280
 
281
  Returns:
282
  Drop in confidence (positive = confidence decreased).
283
  """
284
- return (orig_probs[orig_label] - adv_probs[orig_label]).item()
285
 
286
 
287
  def compute_topk_drop(orig_probs: torch.Tensor,
288
  adv_probs: torch.Tensor,
289
- orig_label: int,
290
  k: int = 5) -> float:
291
  """Top-k accuracy drop.
292
 
293
- Checks if original class remains in top-k predictions after attack.
294
- Returns 1.0 if original class dropped out of top-k, 0.0 if still in.
295
  """
296
  _, adv_topk = torch.topk(adv_probs, min(k, len(adv_probs)))
297
- return 0.0 if orig_label in adv_topk.tolist() else 1.0
298
 
299
 
300
 
@@ -341,17 +341,22 @@ def compute_all_attack_metrics(
341
  adv_probs: torch.Tensor,
342
  orig_label: int,
343
  adv_label: int,
 
344
  k: int = 5,
345
  ) -> Dict[str, float]:
346
  """Compute all attack success metrics at once.
347
 
 
 
 
348
  Returns:
349
  Dictionary with keys: asr, confidence_drop, topk_drop.
350
  """
 
351
  results = {
352
- "asr": compute_asr(orig_label, adv_label),
353
- "confidence_drop": compute_confidence_drop(orig_probs, adv_probs, orig_label),
354
- "topk_drop": compute_topk_drop(orig_probs, adv_probs, orig_label, k=k),
355
  }
356
 
357
  return results
 
259
  # Attack Success Metrics
260
  # ═══════════════════════════════════════════════════════════════════════════
261
 
262
+ def compute_asr(reference_label: int, adv_pred: int) -> float:
263
  """Attack Success Rate for a single sample.
264
 
265
+ Returns 1.0 if attack prediction differs from the reference label, 0.0 otherwise.
266
  For batch ASR, average across samples.
267
  """
268
+ return 1.0 if reference_label != adv_pred else 0.0
269
 
270
 
271
  def compute_confidence_drop(orig_probs: torch.Tensor,
272
  adv_probs: torch.Tensor,
273
+ reference_label: int) -> float:
274
+ """Confidence drop on a reference class.
275
 
276
  Args:
277
  orig_probs: Softmax probabilities for original image (1D tensor).
278
  adv_probs: Softmax probabilities for adversarial image (1D tensor).
279
+ reference_label: Reference class index (ground truth when available).
280
 
281
  Returns:
282
  Drop in confidence (positive = confidence decreased).
283
  """
284
+ return (orig_probs[reference_label] - adv_probs[reference_label]).item()
285
 
286
 
287
  def compute_topk_drop(orig_probs: torch.Tensor,
288
  adv_probs: torch.Tensor,
289
+ reference_label: int,
290
  k: int = 5) -> float:
291
  """Top-k accuracy drop.
292
 
293
+ Checks if reference class remains in top-k predictions after attack.
294
+ Returns 1.0 if reference class dropped out of top-k, 0.0 if still in.
295
  """
296
  _, adv_topk = torch.topk(adv_probs, min(k, len(adv_probs)))
297
+ return 0.0 if reference_label in adv_topk.tolist() else 1.0
298
 
299
 
300
 
 
341
  adv_probs: torch.Tensor,
342
  orig_label: int,
343
  adv_label: int,
344
+ ground_truth: Optional[int] = None,
345
  k: int = 5,
346
  ) -> Dict[str, float]:
347
  """Compute all attack success metrics at once.
348
 
349
+ If ground_truth is provided, class-dependent metrics use it as reference.
350
+ Otherwise, the original predicted label is used (backward-compatible behavior).
351
+
352
  Returns:
353
  Dictionary with keys: asr, confidence_drop, topk_drop.
354
  """
355
+ ref_label = int(ground_truth) if ground_truth is not None else int(orig_label)
356
  results = {
357
+ "asr": compute_asr(ref_label, adv_label),
358
+ "confidence_drop": compute_confidence_drop(orig_probs, adv_probs, ref_label),
359
+ "topk_drop": compute_topk_drop(orig_probs, adv_probs, ref_label, k=k),
360
  }
361
 
362
  return results