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.
- eval_unlearn/__init__.py +1 -0
- eval_unlearn/artifacts/__init__.py +3 -0
- eval_unlearn/artifacts/writer.py +175 -0
- eval_unlearn/cli.py +329 -0
- eval_unlearn/config/hf_datasets.yaml +37 -0
- eval_unlearn/configs/__init__.py +3 -0
- eval_unlearn/configs/base.py +23 -0
- eval_unlearn/datasets/__init__.py +13 -0
- eval_unlearn/datasets/coco_parquet.py +112 -0
- eval_unlearn/datasets/err_composite.py +193 -0
- eval_unlearn/datasets/hf_stream.py +28 -0
- eval_unlearn/datasets/i2p_csv.py +96 -0
- eval_unlearn/datasets/tifa_csv.py +77 -0
- eval_unlearn/datasets/ua_ira_csv.py +129 -0
- eval_unlearn/hub.py +142 -0
- eval_unlearn/logging_utils.py +36 -0
- eval_unlearn/metrics/__init__.py +63 -0
- eval_unlearn/metrics/_base_models.py +27 -0
- eval_unlearn/metrics/_clip_constants.py +59 -0
- eval_unlearn/metrics/asr_i2p/__init__.py +4 -0
- eval_unlearn/metrics/asr_i2p/config.py +55 -0
- eval_unlearn/metrics/asr_i2p/metric.py +321 -0
- eval_unlearn/metrics/asr_mma_diffusion/__init__.py +3 -0
- eval_unlearn/metrics/asr_mma_diffusion/config.py +95 -0
- eval_unlearn/metrics/asr_mma_diffusion/metric.py +377 -0
- eval_unlearn/metrics/asr_p4d/__init__.py +4 -0
- eval_unlearn/metrics/asr_p4d/config.py +189 -0
- eval_unlearn/metrics/asr_p4d/metric.py +418 -0
- eval_unlearn/metrics/asr_ring_a_bell/__init__.py +4 -0
- eval_unlearn/metrics/asr_ring_a_bell/config.py +61 -0
- eval_unlearn/metrics/asr_ring_a_bell/data/nudity_vector.npy +0 -0
- eval_unlearn/metrics/asr_ring_a_bell/metric.py +459 -0
- eval_unlearn/metrics/clip_score/__init__.py +4 -0
- eval_unlearn/metrics/clip_score/config.py +23 -0
- eval_unlearn/metrics/clip_score/metric.py +143 -0
- eval_unlearn/metrics/err/__init__.py +4 -0
- eval_unlearn/metrics/err/config.py +28 -0
- eval_unlearn/metrics/err/metric.py +252 -0
- eval_unlearn/metrics/fid/__init__.py +4 -0
- eval_unlearn/metrics/fid/config.py +19 -0
- eval_unlearn/metrics/fid/metric.py +245 -0
- eval_unlearn/metrics/tifa/__init__.py +4 -0
- eval_unlearn/metrics/tifa/config.py +19 -0
- eval_unlearn/metrics/tifa/metric.py +172 -0
- eval_unlearn/metrics/ua_ira/__init__.py +4 -0
- eval_unlearn/metrics/ua_ira/config.py +39 -0
- eval_unlearn/metrics/ua_ira/metric.py +229 -0
- eval_unlearn/registry/__init__.py +8 -0
- eval_unlearn/registry/entrypoints.py +33 -0
- eval_unlearn/registry/local.py +86 -0
- eval_unlearn/runners/__init__.py +10 -0
- eval_unlearn/runners/core/base_runner.py +109 -0
- eval_unlearn/runners/multi_benchmark_runner.py +245 -0
- eval_unlearn/runners/single_benchmark_runner.py +210 -0
- eval_unlearn/runners/validation.py +96 -0
- eval_unlearn/techniques/SAFREE/__init__.py +0 -0
- eval_unlearn/techniques/SAFREE/config.py +71 -0
- eval_unlearn/techniques/SAFREE/wrapper.py +103 -0
- eval_unlearn/techniques/__init__.py +73 -0
- eval_unlearn/techniques/_base_models.py +36 -0
- eval_unlearn/techniques/advunlearn/__init__.py +3 -0
- eval_unlearn/techniques/advunlearn/config.py +279 -0
- eval_unlearn/techniques/advunlearn/wrapper.py +97 -0
- eval_unlearn/techniques/ca/__init__.py +0 -0
- eval_unlearn/techniques/ca/config.py +51 -0
- eval_unlearn/techniques/ca/wrapper.py +81 -0
- eval_unlearn/techniques/cogfd/__init__.py +0 -0
- eval_unlearn/techniques/cogfd/config.py +144 -0
- eval_unlearn/techniques/cogfd/wrapper.py +86 -0
- eval_unlearn/techniques/concept_steerers/__init__.py +0 -0
- eval_unlearn/techniques/concept_steerers/config.py +39 -0
- eval_unlearn/techniques/concept_steerers/wrapper.py +69 -0
- eval_unlearn/techniques/esd/__init__.py +0 -0
- eval_unlearn/techniques/esd/config.py +53 -0
- eval_unlearn/techniques/esd/wrapper.py +81 -0
- eval_unlearn/techniques/free_run/__init__.py +41 -0
- eval_unlearn/techniques/free_run/config.py +23 -0
- eval_unlearn/techniques/free_run/wrapper.py +110 -0
- eval_unlearn/techniques/mace/__init__.py +0 -0
- eval_unlearn/techniques/mace/config.py +38 -0
- eval_unlearn/techniques/mace/wrapper.py +77 -0
- eval_unlearn/techniques/saeuron/__init__.py +4 -0
- eval_unlearn/techniques/saeuron/config.py +54 -0
- eval_unlearn/techniques/saeuron/wrapper.py +47 -0
- eval_unlearn/techniques/sld/__init__.py +4 -0
- eval_unlearn/techniques/sld/config.py +90 -0
- eval_unlearn/techniques/sld/wrapper.py +126 -0
- eval_unlearn/techniques/ssd/__init__.py +0 -0
- eval_unlearn/techniques/ssd/config.py +87 -0
- eval_unlearn/techniques/ssd/wrapper.py +82 -0
- eval_unlearn/techniques/trasce/__init__.py +4 -0
- eval_unlearn/techniques/trasce/config.py +30 -0
- eval_unlearn/techniques/trasce/wrapper.py +70 -0
- eval_unlearn/techniques/uce/__init__.py +4 -0
- eval_unlearn/techniques/uce/config.py +58 -0
- eval_unlearn/techniques/uce/wrapper.py +71 -0
- eval_unlearn/types.py +32 -0
- eval_unlearn-1.0.0.dist-info/METADATA +229 -0
- eval_unlearn-1.0.0.dist-info/RECORD +102 -0
- eval_unlearn-1.0.0.dist-info/WHEEL +5 -0
- eval_unlearn-1.0.0.dist-info/entry_points.txt +35 -0
- eval_unlearn-1.0.0.dist-info/top_level.txt +1 -0
eval_unlearn/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -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,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
|
+
]
|