eval-unlearn 1.0.0__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.
Files changed (102) hide show
  1. eval_unlearn/__init__.py +1 -0
  2. eval_unlearn/artifacts/__init__.py +3 -0
  3. eval_unlearn/artifacts/writer.py +175 -0
  4. eval_unlearn/cli.py +329 -0
  5. eval_unlearn/config/hf_datasets.yaml +37 -0
  6. eval_unlearn/configs/__init__.py +3 -0
  7. eval_unlearn/configs/base.py +23 -0
  8. eval_unlearn/datasets/__init__.py +13 -0
  9. eval_unlearn/datasets/coco_parquet.py +112 -0
  10. eval_unlearn/datasets/err_composite.py +193 -0
  11. eval_unlearn/datasets/hf_stream.py +28 -0
  12. eval_unlearn/datasets/i2p_csv.py +96 -0
  13. eval_unlearn/datasets/tifa_csv.py +77 -0
  14. eval_unlearn/datasets/ua_ira_csv.py +129 -0
  15. eval_unlearn/hub.py +142 -0
  16. eval_unlearn/logging_utils.py +36 -0
  17. eval_unlearn/metrics/__init__.py +63 -0
  18. eval_unlearn/metrics/_base_models.py +27 -0
  19. eval_unlearn/metrics/_clip_constants.py +59 -0
  20. eval_unlearn/metrics/asr_i2p/__init__.py +4 -0
  21. eval_unlearn/metrics/asr_i2p/config.py +55 -0
  22. eval_unlearn/metrics/asr_i2p/metric.py +321 -0
  23. eval_unlearn/metrics/asr_mma_diffusion/__init__.py +3 -0
  24. eval_unlearn/metrics/asr_mma_diffusion/config.py +95 -0
  25. eval_unlearn/metrics/asr_mma_diffusion/metric.py +377 -0
  26. eval_unlearn/metrics/asr_p4d/__init__.py +4 -0
  27. eval_unlearn/metrics/asr_p4d/config.py +189 -0
  28. eval_unlearn/metrics/asr_p4d/metric.py +418 -0
  29. eval_unlearn/metrics/asr_ring_a_bell/__init__.py +4 -0
  30. eval_unlearn/metrics/asr_ring_a_bell/config.py +61 -0
  31. eval_unlearn/metrics/asr_ring_a_bell/data/nudity_vector.npy +0 -0
  32. eval_unlearn/metrics/asr_ring_a_bell/metric.py +459 -0
  33. eval_unlearn/metrics/clip_score/__init__.py +4 -0
  34. eval_unlearn/metrics/clip_score/config.py +23 -0
  35. eval_unlearn/metrics/clip_score/metric.py +143 -0
  36. eval_unlearn/metrics/err/__init__.py +4 -0
  37. eval_unlearn/metrics/err/config.py +28 -0
  38. eval_unlearn/metrics/err/metric.py +252 -0
  39. eval_unlearn/metrics/fid/__init__.py +4 -0
  40. eval_unlearn/metrics/fid/config.py +19 -0
  41. eval_unlearn/metrics/fid/metric.py +245 -0
  42. eval_unlearn/metrics/tifa/__init__.py +4 -0
  43. eval_unlearn/metrics/tifa/config.py +19 -0
  44. eval_unlearn/metrics/tifa/metric.py +172 -0
  45. eval_unlearn/metrics/ua_ira/__init__.py +4 -0
  46. eval_unlearn/metrics/ua_ira/config.py +39 -0
  47. eval_unlearn/metrics/ua_ira/metric.py +229 -0
  48. eval_unlearn/registry/__init__.py +8 -0
  49. eval_unlearn/registry/entrypoints.py +33 -0
  50. eval_unlearn/registry/local.py +86 -0
  51. eval_unlearn/runners/__init__.py +10 -0
  52. eval_unlearn/runners/core/base_runner.py +109 -0
  53. eval_unlearn/runners/multi_benchmark_runner.py +245 -0
  54. eval_unlearn/runners/single_benchmark_runner.py +210 -0
  55. eval_unlearn/runners/validation.py +96 -0
  56. eval_unlearn/techniques/SAFREE/__init__.py +0 -0
  57. eval_unlearn/techniques/SAFREE/config.py +71 -0
  58. eval_unlearn/techniques/SAFREE/wrapper.py +103 -0
  59. eval_unlearn/techniques/__init__.py +73 -0
  60. eval_unlearn/techniques/_base_models.py +36 -0
  61. eval_unlearn/techniques/advunlearn/__init__.py +3 -0
  62. eval_unlearn/techniques/advunlearn/config.py +279 -0
  63. eval_unlearn/techniques/advunlearn/wrapper.py +97 -0
  64. eval_unlearn/techniques/ca/__init__.py +0 -0
  65. eval_unlearn/techniques/ca/config.py +51 -0
  66. eval_unlearn/techniques/ca/wrapper.py +81 -0
  67. eval_unlearn/techniques/cogfd/__init__.py +0 -0
  68. eval_unlearn/techniques/cogfd/config.py +144 -0
  69. eval_unlearn/techniques/cogfd/wrapper.py +86 -0
  70. eval_unlearn/techniques/concept_steerers/__init__.py +0 -0
  71. eval_unlearn/techniques/concept_steerers/config.py +39 -0
  72. eval_unlearn/techniques/concept_steerers/wrapper.py +69 -0
  73. eval_unlearn/techniques/esd/__init__.py +0 -0
  74. eval_unlearn/techniques/esd/config.py +53 -0
  75. eval_unlearn/techniques/esd/wrapper.py +81 -0
  76. eval_unlearn/techniques/free_run/__init__.py +41 -0
  77. eval_unlearn/techniques/free_run/config.py +23 -0
  78. eval_unlearn/techniques/free_run/wrapper.py +110 -0
  79. eval_unlearn/techniques/mace/__init__.py +0 -0
  80. eval_unlearn/techniques/mace/config.py +38 -0
  81. eval_unlearn/techniques/mace/wrapper.py +77 -0
  82. eval_unlearn/techniques/saeuron/__init__.py +4 -0
  83. eval_unlearn/techniques/saeuron/config.py +54 -0
  84. eval_unlearn/techniques/saeuron/wrapper.py +47 -0
  85. eval_unlearn/techniques/sld/__init__.py +4 -0
  86. eval_unlearn/techniques/sld/config.py +90 -0
  87. eval_unlearn/techniques/sld/wrapper.py +126 -0
  88. eval_unlearn/techniques/ssd/__init__.py +0 -0
  89. eval_unlearn/techniques/ssd/config.py +87 -0
  90. eval_unlearn/techniques/ssd/wrapper.py +82 -0
  91. eval_unlearn/techniques/trasce/__init__.py +4 -0
  92. eval_unlearn/techniques/trasce/config.py +30 -0
  93. eval_unlearn/techniques/trasce/wrapper.py +70 -0
  94. eval_unlearn/techniques/uce/__init__.py +4 -0
  95. eval_unlearn/techniques/uce/config.py +58 -0
  96. eval_unlearn/techniques/uce/wrapper.py +71 -0
  97. eval_unlearn/types.py +32 -0
  98. eval_unlearn-1.0.0.dist-info/METADATA +229 -0
  99. eval_unlearn-1.0.0.dist-info/RECORD +102 -0
  100. eval_unlearn-1.0.0.dist-info/WHEEL +5 -0
  101. eval_unlearn-1.0.0.dist-info/entry_points.txt +35 -0
  102. eval_unlearn-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .writer import ArtifactWriter
2
+
3
+ __all__ = ["ArtifactWriter"]
@@ -0,0 +1,175 @@
1
+ import os
2
+ import json
3
+ from typing import List, Any, Dict, Optional
4
+ from ..logging_utils import get_logger
5
+
6
+ logger = get_logger(__name__)
7
+
8
+
9
+ class ArtifactWriter:
10
+ """
11
+ Handles saving of benchmark artifacts (images and reports).
12
+
13
+ Folder layout::
14
+
15
+ <base_dir>/
16
+ <technique>_<metric>_<run_id>/
17
+ images/
18
+ <category>/ ← only if metadata has categories
19
+ 0.png, 1.png
20
+ 0.png, 1.png ← flat if no categories
21
+ <run_id>_report.json
22
+ """
23
+
24
+ def __init__(self, base_dir: str = "results"):
25
+ self.base_dir = base_dir
26
+
27
+ def save_run(
28
+ self,
29
+ run_id: str,
30
+ technique_name: str,
31
+ metric_name: str,
32
+ images: List[Any],
33
+ report: Optional[Dict[str, Any]] = None,
34
+ metadata: Optional[Dict[str, Any]] = None,
35
+ detailed_report: Optional[Dict[str, Any]] = None,
36
+ image_index_offset: int = 0,
37
+ category_counters_init: Optional[Dict[str, int]] = None,
38
+ ) -> str:
39
+ """
40
+ Save images and report for a benchmark run.
41
+
42
+ Args:
43
+ run_id: Short hash identifying this run.
44
+ technique_name: Name of the technique (e.g. ``"sld"``).
45
+ metric_name: Name of the metric (e.g. ``"asr"``).
46
+ images: List of generated PIL images.
47
+ report: Simplified result dictionary to persist as ``{run_id}_report.json``.
48
+ If None, no report is saved.
49
+ metadata: Dataset metadata. If it contains a ``categories``
50
+ key (list parallel to *images*), images are saved into
51
+ per-category subdirectories.
52
+ detailed_report: Extended result dictionary including technique and metric
53
+ configs, saved alongside the simplified report as
54
+ ``{run_id}_report_full.json``. If None, no detailed report is saved.
55
+ image_index_offset: Starting index for flat image filenames. Pass
56
+ ``total_generated`` before the current batch to avoid overwriting
57
+ images saved in earlier batches.
58
+ category_counters_init: Per-category filename counters accumulated from
59
+ previous batches, used to continue numbering in category-aware saving.
60
+
61
+ Returns:
62
+ Path to the saved simplified report JSON (or where it would be saved).
63
+ """
64
+ metadata = metadata or {}
65
+ categories = metadata.get("categories")
66
+
67
+ image_paths = []
68
+
69
+ # If images provided, save them in structured folder
70
+ if images:
71
+ # Build folder: <base_dir>/<technique>_<metric>_<run_id>/
72
+ folder_name = f"{technique_name}_{metric_name}_{run_id}"
73
+ run_dir = os.path.join(self.base_dir, folder_name)
74
+ images_dir = os.path.join(run_dir, "images")
75
+ os.makedirs(images_dir, exist_ok=True)
76
+
77
+ # Save images — category-aware if metadata provides categories
78
+ logger.info(f"Saving {len(images)} images to {images_dir}...")
79
+
80
+ if categories and len(categories) == len(images):
81
+ # Category-aware: save into subdirectories
82
+ category_counters: Dict[str, int] = dict(category_counters_init or {})
83
+ for img, cat in zip(images, categories):
84
+ cat_dir = os.path.join(images_dir, cat.lower())
85
+ os.makedirs(cat_dir, exist_ok=True)
86
+ idx = category_counters.get(cat.lower(), 0)
87
+ category_counters[cat.lower()] = idx + 1
88
+ path = os.path.join(cat_dir, f"{technique_name}_{metric_name}_{run_id}_{idx}.png")
89
+ image_paths.append(self._save_image(img, path, idx))
90
+ else:
91
+ # Flat: save numbered images directly
92
+ for i, img in enumerate(images):
93
+ global_i = i + image_index_offset
94
+ path = os.path.join(images_dir, f"{technique_name}_{metric_name}_{run_id}_{global_i}.png")
95
+ image_paths.append(self._save_image(img, path, global_i))
96
+
97
+ # Filter out None (failed saves)
98
+ image_paths = [p for p in image_paths if p is not None]
99
+ else:
100
+ logger.info("No images to save")
101
+
102
+ # Resolve the directory where reports are saved
103
+ if images:
104
+ folder_name = f"{technique_name}_{metric_name}_{run_id}"
105
+ report_dir = os.path.join(self.base_dir, folder_name)
106
+ else:
107
+ os.makedirs(self.base_dir, exist_ok=True)
108
+ report_dir = self.base_dir
109
+
110
+ report_path = os.path.join(report_dir, f"{run_id}_report.json")
111
+
112
+ # Save simplified report
113
+ if report is not None:
114
+ try:
115
+ with open(report_path, "w") as f:
116
+ json.dump(report, f, indent=4)
117
+ logger.info(f"Report saved to {report_path}")
118
+ except Exception as e:
119
+ logger.error(f"Failed to save report: {e}")
120
+ else:
121
+ logger.info("Skipping report save (not provided)")
122
+
123
+ # Save detailed report
124
+ if detailed_report is not None:
125
+ detailed_path = os.path.join(report_dir, f"{run_id}_report_full.json")
126
+ try:
127
+ with open(detailed_path, "w") as f:
128
+ json.dump(detailed_report, f, indent=4)
129
+ logger.info(f"Detailed report saved to {detailed_path}")
130
+ except Exception as e:
131
+ logger.error(f"Failed to save detailed report: {e}")
132
+
133
+ if report is not None:
134
+ self._sync_to_final_reports(report_path, report)
135
+
136
+ return report_path
137
+
138
+ def _sync_to_final_reports(self, report_path: str, report: Dict[str, Any]) -> None:
139
+ """Copy report as {technique}_{concept}_latest_report.json in base_dir if newer."""
140
+ technique = report.get("technique_name", "unknown")
141
+ concept = report.get("erase_concept", "unknown")
142
+ timestamp = report.get("timestamp", 0)
143
+
144
+ dest = os.path.join(self.base_dir, f"{technique}_{concept}_latest_report.json")
145
+
146
+ if os.path.exists(dest):
147
+ try:
148
+ with open(dest) as f:
149
+ existing = json.load(f)
150
+ if existing.get("timestamp", 0) >= timestamp:
151
+ logger.info(f"Skipping latest report sync — existing is newer or equal: {dest}")
152
+ return
153
+ except Exception:
154
+ pass
155
+
156
+ try:
157
+ import shutil
158
+ shutil.copy2(report_path, dest)
159
+ logger.info(f"Synced latest report to {dest}")
160
+ except Exception as e:
161
+ logger.error(f"Failed to sync latest report: {e}")
162
+
163
+ @staticmethod
164
+ def _save_image(img: Any, path: str, index: int) -> Optional[str]:
165
+ """Save a single image, returning the path or None on failure."""
166
+ try:
167
+ if hasattr(img, "save"):
168
+ img.save(path)
169
+ return path
170
+ else:
171
+ logger.warning(f"Image {index} does not support save(), skipping.")
172
+ return None
173
+ except Exception as e:
174
+ logger.error(f"Failed to save image {index}: {e}")
175
+ return None
eval_unlearn/cli.py ADDED
@@ -0,0 +1,329 @@
1
+ import argparse
2
+ import sys
3
+ import os
4
+ import json
5
+ import logging
6
+ import warnings
7
+ from typing import Any, Dict, List
8
+
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv(override=True)
12
+
13
+ # Suppress noisy library output before any heavy imports
14
+ logging.getLogger("transformers").setLevel(logging.ERROR)
15
+ logging.getLogger("datasets").setLevel(logging.ERROR)
16
+ logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
17
+ logging.getLogger("onnxruntime").setLevel(logging.ERROR)
18
+ logging.getLogger("tensorflow").setLevel(logging.ERROR)
19
+ warnings.filterwarnings("ignore")
20
+
21
+ from eval_unlearn.runners import SingleBenchmarkRunner, MultiBenchmarkRunner
22
+ from eval_unlearn.logging_utils import get_logger
23
+
24
+ logger = get_logger("cli")
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Helpers
29
+ # ---------------------------------------------------------------------------
30
+
31
+ def _load_config(path: str) -> Dict[str, Any]:
32
+ if not os.path.exists(path):
33
+ logger.error("Config file not found: %s", path)
34
+ sys.exit(1)
35
+ with open(path) as f:
36
+ if path.endswith((".yaml", ".yml")):
37
+ try:
38
+ import yaml
39
+ return yaml.safe_load(f)
40
+ except ImportError:
41
+ logger.error("PyYAML not installed. Use a .json config or: pip install pyyaml")
42
+ sys.exit(1)
43
+ return json.load(f)
44
+
45
+
46
+ def _parse_metrics_list(metrics_list: List[Dict[str, Any]]):
47
+ metric_names, metric_configs = [], {}
48
+ for m in metrics_list:
49
+ name = m.get("name")
50
+ if not name:
51
+ logger.error("Each entry in 'metrics' must have a 'name'")
52
+ sys.exit(1)
53
+ metric_names.append(name)
54
+ cfg = m.get("config", {})
55
+ if cfg:
56
+ metric_configs[name] = cfg
57
+ return metric_names, metric_configs
58
+
59
+
60
+ def _build_single_runner(config: Dict[str, Any], output_dir: str) -> SingleBenchmarkRunner:
61
+ tech = config.get("technique", {})
62
+ tech_name = tech.get("name")
63
+ if not tech_name:
64
+ logger.error("Config must specify 'technique.name'")
65
+ sys.exit(1)
66
+ met = config.get("metric", {})
67
+ met_name = met.get("name")
68
+ if not met_name:
69
+ logger.error("Config must specify 'metric.name'")
70
+ sys.exit(1)
71
+ logger.info("Mode: single | technique=%s | metric=%s", tech_name, met_name)
72
+ return SingleBenchmarkRunner(
73
+ technique_name=tech_name,
74
+ metric_name=met_name,
75
+ technique_config=tech.get("config", {}),
76
+ metric_config=met.get("config", {}),
77
+ output_dir=output_dir,
78
+ )
79
+
80
+
81
+ def _build_multi_runner(config: Dict[str, Any], output_dir: str) -> MultiBenchmarkRunner:
82
+ tech = config.get("technique", {})
83
+ tech_name = tech.get("name")
84
+ if not tech_name:
85
+ logger.error("Config must specify 'technique.name'")
86
+ sys.exit(1)
87
+ metrics_list = config.get("metrics", [])
88
+ if not metrics_list:
89
+ logger.error("Config must specify 'metrics' as a non-empty list")
90
+ sys.exit(1)
91
+ metric_names, metric_configs = _parse_metrics_list(metrics_list)
92
+ logger.info("Mode: multi | technique=%s | metrics=%s", tech_name, metric_names)
93
+ return MultiBenchmarkRunner(
94
+ technique_name=tech_name,
95
+ metric_names=metric_names,
96
+ technique_config=tech.get("config", {}),
97
+ metric_configs=metric_configs,
98
+ output_dir=output_dir,
99
+ )
100
+
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Commands
104
+ # ---------------------------------------------------------------------------
105
+
106
+ def cmd_run(args):
107
+ """Execute a benchmark run, then optionally push results to HF Hub."""
108
+ config = _load_config(args.config)
109
+ output_dir = config.get("output_dir", "results")
110
+
111
+ has_technique = "technique" in config
112
+ has_metrics = "metrics" in config
113
+ has_metric = "metric" in config
114
+
115
+ try:
116
+ if has_technique and has_metrics:
117
+ runner = _build_multi_runner(config, output_dir)
118
+ elif has_technique and has_metric:
119
+ runner = _build_single_runner(config, output_dir)
120
+ else:
121
+ logger.error(
122
+ "Invalid config: must have 'technique'+'metric' (single run) "
123
+ "or 'technique'+'metrics' (multi-metric run)"
124
+ )
125
+ sys.exit(1)
126
+ except ValueError as e:
127
+ logger.error(str(e))
128
+ sys.exit(1)
129
+
130
+ try:
131
+ report = runner.run()
132
+ except Exception:
133
+ logger.exception("Run failed.")
134
+ sys.exit(1)
135
+
136
+ logger.info("Run completed. run_id=%s", report.get("run_id"))
137
+
138
+ if args.hf_repo:
139
+ _push_results(
140
+ output_dir=output_dir,
141
+ hf_repo=args.hf_repo,
142
+ hf_path=args.hf_path,
143
+ create_pr=args.create_pr,
144
+ )
145
+
146
+
147
+ def _push_results(output_dir: str, hf_repo: str, hf_path: str, create_pr: bool):
148
+ from eval_unlearn.hub import HFSync
149
+ remote_path = hf_path or os.path.basename(os.path.normpath(output_dir))
150
+ sync = HFSync(repo_id=hf_repo, create_pr=create_pr)
151
+ try:
152
+ url = sync.push_folder(output_dir, remote_path)
153
+ logger.info("Results pushed → %s/%s (%s)", hf_repo, remote_path, url)
154
+ except Exception as e:
155
+ logger.error("HF push failed: %s", e)
156
+ sys.exit(1)
157
+
158
+
159
+ def cmd_push(args):
160
+ """Push a local directory to HF Hub."""
161
+ from eval_unlearn.hub import HFSync
162
+ remote_path = args.remote_path or os.path.basename(os.path.normpath(args.local_dir))
163
+ sync = HFSync(repo_id=args.repo, create_pr=args.create_pr)
164
+ try:
165
+ url = sync.push_folder(args.local_dir, remote_path)
166
+ logger.info("Pushed %s → %s/%s (%s)", args.local_dir, args.repo, remote_path, url)
167
+ except Exception as e:
168
+ logger.error("Push failed: %s", e)
169
+ sys.exit(1)
170
+
171
+
172
+ def cmd_pull(args):
173
+ """Pull artifacts from HF Hub."""
174
+ from eval_unlearn.hub import HFSync
175
+ local_dir = args.local_dir or "results"
176
+ sync = HFSync(repo_id=args.repo)
177
+ try:
178
+ if args.remote_path:
179
+ path = sync.pull_folder(args.remote_path, local_dir)
180
+ else:
181
+ path = sync.pull_all(local_dir)
182
+ logger.info("Downloaded to %s", path)
183
+ except Exception as e:
184
+ logger.error("Pull failed: %s", e)
185
+ sys.exit(1)
186
+
187
+
188
+ def cmd_plugins(_args):
189
+ """List all registered techniques, metrics, and datasets."""
190
+ from eval_unlearn.registry.entrypoints import load_entrypoints
191
+ from eval_unlearn.registry.local import _TECHNIQUES, _METRICS, _DATASETS
192
+
193
+ load_entrypoints()
194
+
195
+ print("\nTechniques:")
196
+ for name in sorted(_TECHNIQUES):
197
+ print(f" {name}")
198
+ print("\nMetrics:")
199
+ for name in sorted(_METRICS):
200
+ print(f" {name}")
201
+ print("\nDatasets:")
202
+ for name in sorted(_DATASETS):
203
+ print(f" {name}")
204
+ print()
205
+
206
+
207
+ def cmd_models(_args):
208
+ """Show the base model used by each technique and metric."""
209
+ from eval_unlearn.techniques._base_models import TECHNIQUE_BASE_MODELS
210
+ from eval_unlearn.metrics._base_models import METRIC_MODELS
211
+
212
+ # Techniques
213
+ print("\nTechniques:")
214
+ print(f" {'name':<20} {'base model':<45} {'configurable'}")
215
+ print(f" {'-'*20} {'-'*45} {'-'*12}")
216
+ for name in sorted(TECHNIQUE_BASE_MODELS):
217
+ print(f" {name:<20} {TECHNIQUE_BASE_MODELS[name]:<45} no")
218
+ print(f" {'free_run':<20} {'(user-specified via model_id)':<45} required")
219
+
220
+ # Metrics
221
+ print("\nMetrics:")
222
+ print(f" {'name':<20} {'model':<45} {'configurable'}")
223
+ print(f" {'-'*20} {'-'*45} {'-'*12}")
224
+ for name in sorted(METRIC_MODELS):
225
+ info = METRIC_MODELS[name]
226
+ if info.configurable:
227
+ if info.choices:
228
+ choices_str = " | ".join(c.split("/")[-1] for c in sorted(info.choices))
229
+ detail = f"yes (config: {info.config_field}: {choices_str})"
230
+ else:
231
+ detail = f"yes (config: {info.config_field})"
232
+ elif info.note:
233
+ detail = f"no ({info.note})"
234
+ else:
235
+ detail = "no"
236
+ print(f" {name:<20} {info.model:<45} {detail}")
237
+ print()
238
+
239
+
240
+ # ---------------------------------------------------------------------------
241
+ # Argument parser
242
+ # ---------------------------------------------------------------------------
243
+
244
+ def main():
245
+ parser = argparse.ArgumentParser(
246
+ prog="eval-unlearn",
247
+ description="Eval-Learn: unlearning benchmark for text-to-image diffusion models",
248
+ )
249
+ parser.add_argument("--version", action="store_true", help="Print version and exit")
250
+ subparsers = parser.add_subparsers(dest="command")
251
+
252
+ # run
253
+ run_p = subparsers.add_parser("run", help="Execute a benchmark run")
254
+ run_p.add_argument("--config", "-c", required=True, help="Path to config file (JSON/YAML)")
255
+ run_p.add_argument(
256
+ "--hf-repo",
257
+ default=None,
258
+ metavar="REPO_ID",
259
+ help="HF Hub dataset repo to push results to after the run (e.g. org/my-results)",
260
+ )
261
+ run_p.add_argument(
262
+ "--hf-path",
263
+ default=None,
264
+ metavar="REMOTE_PATH",
265
+ help="Remote path inside the repo (default: basename of output_dir from config)",
266
+ )
267
+ run_p.add_argument(
268
+ "--create-pr",
269
+ action="store_true",
270
+ help="Open a pull request instead of committing directly to HF Hub",
271
+ )
272
+
273
+ # push
274
+ push_p = subparsers.add_parser("push", help="Push a local results directory to HF Hub")
275
+ push_p.add_argument("--repo", required=True, metavar="REPO_ID", help="HF Hub dataset repo ID")
276
+ push_p.add_argument("--local-dir", required=True, metavar="PATH", help="Local directory to upload")
277
+ push_p.add_argument(
278
+ "--remote-path",
279
+ default=None,
280
+ metavar="PATH",
281
+ help="Destination path in the repo (default: basename of --local-dir)",
282
+ )
283
+ push_p.add_argument("--create-pr", action="store_true")
284
+
285
+ # pull
286
+ pull_p = subparsers.add_parser("pull", help="Pull artifacts from HF Hub")
287
+ pull_p.add_argument("--repo", required=True, metavar="REPO_ID", help="HF Hub dataset repo ID")
288
+ pull_p.add_argument(
289
+ "--remote-path",
290
+ default=None,
291
+ metavar="PATH",
292
+ help="Remote path to download (omit to pull the entire repo)",
293
+ )
294
+ pull_p.add_argument(
295
+ "--local-dir",
296
+ default=None,
297
+ metavar="PATH",
298
+ help="Local directory to download into (default: results/)",
299
+ )
300
+
301
+ # plugins
302
+ subparsers.add_parser("plugins", help="List all registered techniques, metrics, and datasets")
303
+
304
+ # models
305
+ subparsers.add_parser("models", help="Show the base model used by each technique and metric")
306
+
307
+ args = parser.parse_args()
308
+
309
+ if args.version:
310
+ from eval_unlearn import __version__
311
+ print(f"eval-unlearn {__version__}")
312
+ sys.exit(0)
313
+
314
+ dispatch = {
315
+ "run": cmd_run,
316
+ "push": cmd_push,
317
+ "pull": cmd_pull,
318
+ "plugins": cmd_plugins,
319
+ "models": cmd_models,
320
+ }
321
+
322
+ if args.command in dispatch:
323
+ dispatch[args.command](args)
324
+ else:
325
+ parser.print_help()
326
+
327
+
328
+ if __name__ == "__main__":
329
+ main()
@@ -0,0 +1,37 @@
1
+ # HuggingFace dataset paths and column configs.
2
+ # Loads entire datasets without split specification. HF_TOKEN should be set in your .env file.
3
+
4
+ datasets:
5
+
6
+ coco:
7
+ repo_id: "phiyodr/coco2017"
8
+ split: "train"
9
+ caption_col: "captions"
10
+ url_col: "coco_url"
11
+
12
+ i2p:
13
+ repo_id: "AIML-TUDA/i2p"
14
+ split: "train"
15
+ caption_col: "prompt"
16
+ concept_col: "categories"
17
+
18
+ tifa:
19
+ repo_id: "Unlearningltd/datasets"
20
+ data_files: "tifa/tifa_dataset.csv"
21
+ split: "train"
22
+ caption_col: "caption"
23
+ qa_col: "qas"
24
+
25
+ err_challenge:
26
+ repo_id: "Unlearningltd/datasets"
27
+ data_files: "ERR/raw_csv_data/challenge_dataset.csv"
28
+ split: "train"
29
+ caption_col: "direct_prompt"
30
+ concept_col: "concept_name"
31
+
32
+ ring_a_bell:
33
+ repo_id: "Unlearningltd/datasets"
34
+ data_files: "ring_a_bell/ring_a_bell_dataset.csv"
35
+ split: "train"
36
+ caption_col: "prompt"
37
+ concept_col: "concept"
@@ -0,0 +1,3 @@
1
+ from .base import BaseConfig
2
+
3
+ __all__ = ["BaseConfig"]
@@ -0,0 +1,23 @@
1
+ from dataclasses import dataclass, asdict
2
+ from typing import Dict, Any
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class BaseConfig:
7
+ """
8
+ Base configuration class providing dictionary serialization methods.
9
+ """
10
+
11
+ def to_dict(self) -> Dict[str, Any]:
12
+ """Convert the configuration to a dictionary."""
13
+ return asdict(self)
14
+
15
+ @classmethod
16
+ def from_dict(cls, data: Dict[str, Any]) -> "BaseConfig":
17
+ """
18
+ Create a configuration instance from a dictionary.
19
+ ignores keys that do not match fields in the dataclass to ensure forward compatibility.
20
+ """
21
+ valid_keys = {k for k, f in cls.__dataclass_fields__.items() if f.init}
22
+ filtered_data = {k: v for k, v in data.items() if k in valid_keys}
23
+ return cls(**filtered_data)
@@ -0,0 +1,13 @@
1
+ from .coco_parquet import load_coco_parquet
2
+ from .i2p_csv import load_i2p_csv
3
+ from .err_composite import load_err_composite
4
+ from .tifa_csv import load_tifa_csv
5
+ from .ua_ira_csv import load_ua_ira_csv
6
+
7
+ __all__ = [
8
+ "load_coco_parquet",
9
+ "load_i2p_csv",
10
+ "load_err_composite",
11
+ "load_tifa_csv",
12
+ "load_ua_ira_csv",
13
+ ]