diffeval 0.1.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.
- diffeval/__init__.py +7 -0
- diffeval/__main__.py +6 -0
- diffeval/aggregate/__init__.py +1 -0
- diffeval/aggregate/mean.py +7 -0
- diffeval/aggregate/names.py +11 -0
- diffeval/aggregate/row.py +26 -0
- diffeval/aggregate/sample_std.py +9 -0
- diffeval/aggregate/series.py +22 -0
- diffeval/aggregate/table.py +8 -0
- diffeval/backends/__init__.py +4 -0
- diffeval/backends/diffusers/__init__.py +1 -0
- diffeval/backends/diffusers/attention_apply.py +16 -0
- diffeval/backends/diffusers/attention_eager.py +6 -0
- diffeval/backends/diffusers/attention_name.py +15 -0
- diffeval/backends/diffusers/attention_sdpa.py +6 -0
- diffeval/backends/diffusers/attention_xformers.py +5 -0
- diffeval/backends/diffusers/backend.py +29 -0
- diffeval/backends/diffusers/call_kwargs.py +15 -0
- diffeval/backends/diffusers/coerce_dtype.py +10 -0
- diffeval/backends/diffusers/describe.py +6 -0
- diffeval/backends/diffusers/dtype_name.py +23 -0
- diffeval/backends/diffusers/from_config.py +17 -0
- diffeval/backends/diffusers/generate.py +18 -0
- diffeval/backends/diffusers/kwargs_generator.py +6 -0
- diffeval/backends/diffusers/kwargs_prompt.py +11 -0
- diffeval/backends/diffusers/kwargs_sampling.py +18 -0
- diffeval/backends/diffusers/kwargs_size.py +18 -0
- diffeval/backends/diffusers/load_kwargs.py +10 -0
- diffeval/backends/diffusers/load_pipe.py +23 -0
- diffeval/backends/diffusers/missing_dep.py +10 -0
- diffeval/backends/diffusers/one_batch.py +11 -0
- diffeval/backends/diffusers/pick_device.py +13 -0
- diffeval/backends/diffusers/presets.py +8 -0
- diffeval/backends/diffusers/sampling.py +18 -0
- diffeval/backends/dummy/__init__.py +1 -0
- diffeval/backends/dummy/backend.py +29 -0
- diffeval/backends/dummy/box.py +9 -0
- diffeval/backends/dummy/colors.py +10 -0
- diffeval/backends/dummy/describe.py +18 -0
- diffeval/backends/dummy/draw.py +16 -0
- diffeval/backends/dummy/from_config.py +14 -0
- diffeval/backends/dummy/generate.py +17 -0
- diffeval/backends/dummy/render.py +18 -0
- diffeval/backends/noop_prepare.py +5 -0
- diffeval/backends/protocol.py +15 -0
- diffeval/bench/__init__.py +1 -0
- diffeval/bench/cmmd_check.py +20 -0
- diffeval/bench/cmmd_samples.py +15 -0
- diffeval/bench/coco30k_cli.py +23 -0
- diffeval/bench/coco30k_export.py +19 -0
- diffeval/bench/coco30k_files.py +16 -0
- diffeval/bench/coco30k_images.py +21 -0
- diffeval/bench/coco30k_suite.py +16 -0
- diffeval/bench/fetch.py +12 -0
- diffeval/bench/published.py +10 -0
- diffeval/bench/table_cli.py +16 -0
- diffeval/bench/table_row.py +24 -0
- diffeval/cli/__init__.py +1 -0
- diffeval/cli/command_list.py +13 -0
- diffeval/cli/command_run.py +25 -0
- diffeval/cli/config.py +14 -0
- diffeval/cli/csv_ints.py +8 -0
- diffeval/cli/csv_names.py +8 -0
- diffeval/cli/main.py +12 -0
- diffeval/cli/output_dir.py +12 -0
- diffeval/cli/parser.py +17 -0
- diffeval/cli/print_paths.py +12 -0
- diffeval/cli/print_registered.py +6 -0
- diffeval/cli/run_parser_eval.py +14 -0
- diffeval/cli/run_parser_model.py +13 -0
- diffeval/metrics/__init__.py +7 -0
- diffeval/metrics/clip/__init__.py +1 -0
- diffeval/metrics/clip/features.py +14 -0
- diffeval/metrics/clip/formula.py +6 -0
- diffeval/metrics/clip/load.py +17 -0
- diffeval/metrics/clip/metric.py +20 -0
- diffeval/metrics/clip/protocol.py +10 -0
- diffeval/metrics/clip/sample_image.py +10 -0
- diffeval/metrics/clip/score.py +25 -0
- diffeval/metrics/cmmd/__init__.py +1 -0
- diffeval/metrics/cmmd/embed_batch.py +26 -0
- diffeval/metrics/cmmd/embed_paths.py +30 -0
- diffeval/metrics/cmmd/metric.py +20 -0
- diffeval/metrics/cmmd/mmd.py +23 -0
- diffeval/metrics/cmmd/protocol.py +11 -0
- diffeval/metrics/cmmd/resize.py +17 -0
- diffeval/metrics/cmmd/score.py +24 -0
- diffeval/metrics/count_warning.py +7 -0
- diffeval/metrics/distribution/__init__.py +1 -0
- diffeval/metrics/distribution/call.py +24 -0
- diffeval/metrics/distribution/kwargs_folder.py +16 -0
- diffeval/metrics/distribution/kwargs_stats.py +8 -0
- diffeval/metrics/distribution/protocol.py +12 -0
- diffeval/metrics/distribution/seed_numpy.py +12 -0
- diffeval/metrics/fid/__init__.py +1 -0
- diffeval/metrics/fid/guard.py +13 -0
- diffeval/metrics/fid/metric.py +20 -0
- diffeval/metrics/geneval/__init__.py +1 -0
- diffeval/metrics/geneval/metric.py +22 -0
- diffeval/metrics/geneval/protocol.py +5 -0
- diffeval/metrics/geneval/tags.py +3 -0
- diffeval/metrics/images.py +12 -0
- diffeval/metrics/kid/__init__.py +1 -0
- diffeval/metrics/kid/guard.py +3 -0
- diffeval/metrics/kid/metric.py +20 -0
- diffeval/metrics/ok.py +9 -0
- diffeval/metrics/protocol.py +17 -0
- diffeval/metrics/skipped.py +10 -0
- diffeval/plugins.py +7 -0
- diffeval/registry/__init__.py +1 -0
- diffeval/registry/lookup.py +9 -0
- diffeval/registry/names.py +5 -0
- diffeval/registry/register.py +11 -0
- diffeval/registry/tables.py +5 -0
- diffeval/registry/unregister.py +5 -0
- diffeval/report/__init__.py +1 -0
- diffeval/report/console.py +18 -0
- diffeval/report/console_widths.py +5 -0
- diffeval/report/direction.py +7 -0
- diffeval/report/html_figure.py +9 -0
- diffeval/report/html_grid.py +19 -0
- diffeval/report/html_page.py +15 -0
- diffeval/report/html_row.py +12 -0
- diffeval/report/markdown.py +11 -0
- diffeval/report/markdown_row.py +10 -0
- diffeval/report/number.py +7 -0
- diffeval/report/templates/page.html +35 -0
- diffeval/runner/__init__.py +1 -0
- diffeval/runner/generate_save.py +19 -0
- diffeval/runner/metric_device.py +16 -0
- diffeval/runner/one_seed.py +20 -0
- diffeval/runner/options.py +23 -0
- diffeval/runner/payload.py +26 -0
- diffeval/runner/relpath.py +7 -0
- diffeval/runner/requests.py +7 -0
- diffeval/runner/run.py +24 -0
- diffeval/runner/safe_metric.py +14 -0
- diffeval/runner/save_png.py +8 -0
- diffeval/runner/seeds.py +23 -0
- diffeval/runner/write_json.py +11 -0
- diffeval/runner/write_reports.py +14 -0
- diffeval/system/__init__.py +1 -0
- diffeval/system/describe.py +10 -0
- diffeval/system/gpu.py +11 -0
- diffeval/system/gpu_cuda.py +9 -0
- diffeval/system/gpu_mps.py +8 -0
- diffeval/system/sdp_flags.py +15 -0
- diffeval/system/version_of.py +10 -0
- diffeval/system/versions.py +20 -0
- diffeval/tasks/__init__.py +3 -0
- diffeval/tasks/build_suite.py +7 -0
- diffeval/tasks/core.py +15 -0
- diffeval/tasks/errors.py +5 -0
- diffeval/tasks/fields_metrics.py +14 -0
- diffeval/tasks/fields_paths.py +25 -0
- diffeval/tasks/fields_prompts.py +15 -0
- diffeval/tasks/fields_seeds.py +14 -0
- diffeval/tasks/fields_size.py +12 -0
- diffeval/tasks/fields_text.py +13 -0
- diffeval/tasks/from_mapping.py +22 -0
- diffeval/tasks/load_bundled.py +9 -0
- diffeval/tasks/load_path.py +12 -0
- diffeval/tasks/one_prompt.py +17 -0
- diffeval/tasks/prompt_id.py +13 -0
- diffeval/tasks/read_yaml.py +14 -0
- diffeval/tasks/resolve.py +18 -0
- diffeval/tasks/suites/core.yaml +12 -0
- diffeval/tasks/suites/references/core/blue_sphere.png +0 -0
- diffeval/tasks/suites/references/core/green_triangle.png +0 -0
- diffeval/tasks/suites/references/core/red_cube.png +0 -0
- diffeval/tasks/suites/references/core/yellow_star.png +0 -0
- diffeval/types/__init__.py +1 -0
- diffeval/types/context.py +15 -0
- diffeval/types/prompt.py +10 -0
- diffeval/types/request.py +13 -0
- diffeval/types/result.py +14 -0
- diffeval/types/result_rules.py +13 -0
- diffeval/types/sample.py +15 -0
- diffeval/types/suite.py +21 -0
- diffeval-0.1.0.dist-info/METADATA +378 -0
- diffeval-0.1.0.dist-info/RECORD +185 -0
- diffeval-0.1.0.dist-info/WHEEL +5 -0
- diffeval-0.1.0.dist-info/entry_points.txt +6 -0
- diffeval-0.1.0.dist-info/licenses/LICENSE +190 -0
- diffeval-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""diffeval-check-cmmd: score the cmmd-pytorch samples and expect 7.696."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from diffeval.bench.cmmd_samples import cmmd_samples
|
|
7
|
+
|
|
8
|
+
EXPECTED = 7.696
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> int:
|
|
12
|
+
from diffeval.metrics.cmmd.embed_batch import load_vision
|
|
13
|
+
from diffeval.metrics.cmmd.embed_paths import embed_dir
|
|
14
|
+
from diffeval.metrics.cmmd.mmd import mmd
|
|
15
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
16
|
+
ref_dir, gen_dir = cmmd_samples(Path(tmp))
|
|
17
|
+
model, processor = load_vision("openai/clip-vit-large-patch14-336")
|
|
18
|
+
value = round(mmd(embed_dir(model, processor, ref_dir, "cpu"), embed_dir(model, processor, gen_dir, "cpu")), 3)
|
|
19
|
+
print(f"CMMD {value} (expected {EXPECTED})")
|
|
20
|
+
return 0 if abs(value - EXPECTED) < 0.001 else 1
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""The six cmmd-pytorch sample PNGs (Apache-2.0) behind its 7.696 check."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from diffeval.bench.fetch import fetch
|
|
6
|
+
|
|
7
|
+
BASE = "https://raw.githubusercontent.com/sayakpaul/cmmd-pytorch/main"
|
|
8
|
+
FILES = {"reference_images": "glowing_512_1", "generated_images": "glowing_512_2"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmmd_samples(root: Path) -> tuple[Path, Path]:
|
|
12
|
+
for folder, stem in FILES.items():
|
|
13
|
+
for suffix in ("", "_copy", "_copy_2"):
|
|
14
|
+
fetch(f"{BASE}/{folder}/{stem}{suffix}.png", root / folder / f"{stem}{suffix}.png")
|
|
15
|
+
return root / "reference_images", root / "generated_images"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""diffeval-coco30k: write <out>/suite.yaml for the cmmd-pytorch COCO-30k protocol."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from diffeval.bench.coco30k_files import coco30k_captions
|
|
9
|
+
from diffeval.bench.coco30k_suite import coco30k_suite
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv=None) -> int:
|
|
13
|
+
parser = argparse.ArgumentParser(prog="diffeval-coco30k")
|
|
14
|
+
parser.add_argument("--out", required=True, type=Path)
|
|
15
|
+
parser.add_argument("--limit", type=int, default=30000, help="first N captions (default all 30000)")
|
|
16
|
+
parser.add_argument("--seed", type=int, default=2024, help="cmmd-pytorch uses 2024")
|
|
17
|
+
parser.add_argument("--reference-images", type=Path, default=None, help="COCO image folder; adds fid and kid")
|
|
18
|
+
args = parser.parse_args(argv)
|
|
19
|
+
refs = None if args.reference_images is None else args.reference_images.resolve()
|
|
20
|
+
suite = coco30k_suite(coco30k_captions(args.out, args.limit), args.seed, refs)
|
|
21
|
+
(args.out / "suite.yaml").write_text(yaml.safe_dump(suite, sort_keys=False, allow_unicode=True), encoding="utf-8")
|
|
22
|
+
print(f"wrote {args.out / 'suite.yaml'} with {len(suite['prompts'])} prompts and metrics {suite['metrics']}")
|
|
23
|
+
return 0
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Write the COCO-30k reference images from the dataset's parquet shards."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def coco30k_export(out: Path) -> int:
|
|
10
|
+
import pyarrow.parquet as pq
|
|
11
|
+
from huggingface_hub import snapshot_download
|
|
12
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
root = Path(snapshot_download("sayakpaul/coco-30-val-2014", repo_type="dataset", allow_patterns=["data/*.parquet"]))
|
|
14
|
+
count = 0
|
|
15
|
+
for shard in sorted((root / "data").glob("*.parquet")):
|
|
16
|
+
for row in pq.read_table(shard, columns=["image"]).column("image").to_pylist():
|
|
17
|
+
Image.open(io.BytesIO(row["bytes"])).convert("RGB").save(out / f"ref_{count:05d}.png")
|
|
18
|
+
count += 1
|
|
19
|
+
return count
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""The captions and reference embeddings cmmd-pytorch publishes for COCO-30k."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from diffeval.bench.fetch import fetch
|
|
7
|
+
|
|
8
|
+
CSV_URL = "https://huggingface.co/datasets/sayakpaul/sample-datasets/raw/main/coco_30k_randomly_sampled_2014_val.csv"
|
|
9
|
+
EMB_URL = "https://huggingface.co/datasets/sayakpaul/coco-30-val-2014/resolve/main/ref_embs_coco_30k.npy"
|
|
10
|
+
EMB_NAME = "ref_embs_coco_30k.npy"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def coco30k_captions(out: Path, limit: int) -> list[str]:
|
|
14
|
+
fetch(EMB_URL, out / EMB_NAME)
|
|
15
|
+
with fetch(CSV_URL, out / "captions.csv").open(encoding="utf-8") as handle:
|
|
16
|
+
return [row["caption"].strip() for row in csv.DictReader(handle)][:limit]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""diffeval-coco30k-images: write the 30k COCO reference PNGs for fid and kid (about 5 GB).
|
|
2
|
+
|
|
3
|
+
Needs the benchmarks extra: pip install "diffeval[benchmarks]".
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import importlib.util
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from diffeval.bench.coco30k_export import coco30k_export
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv=None) -> int:
|
|
14
|
+
parser = argparse.ArgumentParser(prog="diffeval-coco30k-images")
|
|
15
|
+
parser.add_argument("--out", required=True, type=Path)
|
|
16
|
+
out = parser.parse_args(argv).out
|
|
17
|
+
if any(importlib.util.find_spec(name) is None for name in ("pyarrow", "huggingface_hub")):
|
|
18
|
+
print('install the benchmarks extra: pip install "diffeval[benchmarks]"')
|
|
19
|
+
return 2
|
|
20
|
+
print(f"wrote {coco30k_export(out)} images to {out}")
|
|
21
|
+
return 0
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""The suite mapping for the first N COCO-30k captions."""
|
|
2
|
+
|
|
3
|
+
from diffeval.bench.coco30k_files import EMB_NAME
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def coco30k_suite(captions: list[str], seed: int, reference_images=None) -> dict:
|
|
7
|
+
name = "coco30k" if len(captions) >= 30000 else f"coco30k-{len(captions)}"
|
|
8
|
+
suite = {"name": name, "description": f"cmmd-pytorch COCO-30k captions (COCO 2014 val), first {len(captions)}"}
|
|
9
|
+
suite.update(seeds=[seed], width=512, height=512, cmmd_ref_embed=EMB_NAME)
|
|
10
|
+
metrics = ["clip_score", "cmmd"]
|
|
11
|
+
if reference_images is not None:
|
|
12
|
+
suite["reference_dir"] = str(reference_images)
|
|
13
|
+
metrics += ["fid", "kid"]
|
|
14
|
+
suite["metrics"] = metrics
|
|
15
|
+
suite["prompts"] = [{"id": f"c{index:05d}", "prompt": text} for index, text in enumerate(captions)]
|
|
16
|
+
return suite
|
diffeval/bench/fetch.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Download a URL to a path once."""
|
|
2
|
+
|
|
3
|
+
import urllib.request
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def fetch(url: str, path: Path) -> Path:
|
|
8
|
+
if not path.exists():
|
|
9
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
10
|
+
print(f"downloading {path.name}")
|
|
11
|
+
urllib.request.urlretrieve(url, path)
|
|
12
|
+
return path
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""cmmd-pytorch's published COCO-30k CMMD by model id (github.com/sayakpaul/cmmd-pytorch)."""
|
|
2
|
+
|
|
3
|
+
PUBLISHED_CMMD = {
|
|
4
|
+
"runwayml/stable-diffusion-v1-5": 0.582,
|
|
5
|
+
"stable-diffusion-v1-5/stable-diffusion-v1-5": 0.582,
|
|
6
|
+
"stabilityai/sdxl-turbo": 0.548,
|
|
7
|
+
"stabilityai/stable-diffusion-xl-base-1.0": 0.696,
|
|
8
|
+
"segmind/SSD-1B": 0.669,
|
|
9
|
+
"PixArt-alpha/PixArt-XL-2-1024-MS": 1.140,
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""diffeval-table: print a Markdown table from results.json files."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from diffeval.bench.table_row import HEADER, table_row
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv=None) -> int:
|
|
9
|
+
parser = argparse.ArgumentParser(prog="diffeval-table")
|
|
10
|
+
parser.add_argument("results", nargs="+")
|
|
11
|
+
args = parser.parse_args(argv)
|
|
12
|
+
print("| " + " | ".join(HEADER) + " |")
|
|
13
|
+
print("|" + " --- |" * len(HEADER))
|
|
14
|
+
for path in args.results:
|
|
15
|
+
print(table_row(path))
|
|
16
|
+
return 0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""One Markdown row from one results.json."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from diffeval.bench.published import PUBLISHED_CMMD
|
|
7
|
+
|
|
8
|
+
METRICS = ("clip_score", "cmmd", "fid", "kid")
|
|
9
|
+
HEADER = ["model", "steps", "guidance", "size", "device", "images", *METRICS, "published CMMD (30k)"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _cell(row) -> str:
|
|
13
|
+
if row is None or row.get("mean") is None:
|
|
14
|
+
return "-"
|
|
15
|
+
return f"{row['mean']:.4g}" if row.get("std") is None else f"{row['mean']:.4g} +/- {row['std']:.2g}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def table_row(path) -> str:
|
|
19
|
+
body = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
20
|
+
backend, suite, agg = body["backend"], body["suite"], body["aggregate"]
|
|
21
|
+
model = backend.get("pipeline") or backend["name"]
|
|
22
|
+
cells = [model, str(backend.get("steps")), str(backend.get("guidance")), f"{suite['width']}x{suite['height']}", str(backend.get("device")), str(len(suite["prompts"]))]
|
|
23
|
+
published = PUBLISHED_CMMD.get(model)
|
|
24
|
+
return "| " + " | ".join(cells + [_cell(agg.get(name)) for name in METRICS] + ["-" if published is None else str(published)]) + " |"
|
diffeval/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command line interface."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""List tasks, metrics, or backends."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from diffeval.cli.print_registered import print_registered
|
|
6
|
+
from diffeval.registry.tables import BACKENDS, METRICS, TASKS
|
|
7
|
+
|
|
8
|
+
_TABLES = {"tasks": TASKS, "metrics": METRICS, "backends": BACKENDS}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def command_list(args, stream=None) -> int:
|
|
12
|
+
print_registered(_TABLES[args.kind], stream or sys.stdout)
|
|
13
|
+
return 0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Resolve a suite, run it, and print output paths."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from diffeval.cli.config import backend_config, backend_name
|
|
6
|
+
from diffeval.cli.output_dir import output_dir
|
|
7
|
+
from diffeval.cli.print_paths import print_paths
|
|
8
|
+
from diffeval.runner.options import RunOptions
|
|
9
|
+
from diffeval.runner.run import run_evaluation
|
|
10
|
+
from diffeval.tasks.resolve import resolve
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def command_run(args, stream=None) -> int:
|
|
14
|
+
out = stream or sys.stdout
|
|
15
|
+
try:
|
|
16
|
+
suite = resolve(args.suite)
|
|
17
|
+
name = backend_name(args)
|
|
18
|
+
folder = output_dir(args.output, suite.name, name)
|
|
19
|
+
options = RunOptions(suite, name, backend_config(args), folder, args.seeds, args.metrics, args.width, args.height)
|
|
20
|
+
body = run_evaluation(options)
|
|
21
|
+
except Exception as exc:
|
|
22
|
+
print(exc, file=out)
|
|
23
|
+
return 2
|
|
24
|
+
print_paths(folder, out, body)
|
|
25
|
+
return 2 if args.strict and any(row["status"] != "ok" for row in body["aggregate"].values()) else 0
|
diffeval/cli/config.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Backend name and config dict from CLI args."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def backend_name(args) -> str:
|
|
5
|
+
if args.backend:
|
|
6
|
+
return args.backend
|
|
7
|
+
if args.pipeline:
|
|
8
|
+
return "diffusers"
|
|
9
|
+
return "dummy"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def backend_config(args) -> dict:
|
|
13
|
+
raw = {"pipeline": args.pipeline, "dtype": args.dtype, "steps": args.steps, "guidance": args.guidance, "device": args.device, "attention": getattr(args, "attention", None), "variant": getattr(args, "variant", None), "batch_size": getattr(args, "batch_size", None)}
|
|
14
|
+
return {key: value for key, value in raw.items() if value is not None}
|
diffeval/cli/csv_ints.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Parse a comma-separated list of seeds."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def csv_ints(text: str) -> tuple[int, ...]:
|
|
5
|
+
parts = [item.strip() for item in text.split(",") if item.strip()]
|
|
6
|
+
if not parts or any(not item.lstrip("-").isdigit() for item in parts):
|
|
7
|
+
raise ValueError(f"expected integers, got {text!r}")
|
|
8
|
+
return tuple(int(item) for item in parts)
|
diffeval/cli/main.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Dispatch diffeval commands and return an exit code."""
|
|
2
|
+
|
|
3
|
+
from diffeval.cli.command_list import command_list
|
|
4
|
+
from diffeval.cli.command_run import command_run
|
|
5
|
+
from diffeval.cli.parser import build_parser
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv: list[str] | None = None) -> int:
|
|
9
|
+
args = build_parser().parse_args(argv)
|
|
10
|
+
if args.command == "list":
|
|
11
|
+
return command_list(args)
|
|
12
|
+
return command_run(args)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Default directory outputs/<utc>-<suite>-<backend>."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def output_dir(explicit, suite_name: str, backend: str) -> Path:
|
|
8
|
+
if explicit:
|
|
9
|
+
return Path(explicit)
|
|
10
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
11
|
+
label = f"{stamp}-{suite_name}-{backend}".replace(" ", "_").replace("/", "_")
|
|
12
|
+
return Path("outputs") / label
|
diffeval/cli/parser.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""argparse for run and list."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from diffeval.cli.run_parser_eval import add_eval_flags
|
|
6
|
+
from diffeval.cli.run_parser_model import add_model_flags
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
10
|
+
parser = argparse.ArgumentParser(prog="diffeval")
|
|
11
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
12
|
+
run = commands.add_parser("run")
|
|
13
|
+
add_model_flags(run)
|
|
14
|
+
add_eval_flags(run)
|
|
15
|
+
listed = commands.add_parser("list")
|
|
16
|
+
listed.add_argument("kind", choices=("tasks", "metrics", "backends"))
|
|
17
|
+
return parser
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Print the table and the written output paths."""
|
|
2
|
+
|
|
3
|
+
from diffeval.report.console import console
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def print_paths(folder, stream, body=None) -> None:
|
|
7
|
+
if body is not None:
|
|
8
|
+
print(console(body), end="", file=stream)
|
|
9
|
+
print(folder / "results.json", file=stream)
|
|
10
|
+
print(folder / "report.md", file=stream)
|
|
11
|
+
print(folder / "report.html", file=stream)
|
|
12
|
+
print(folder / "images", file=stream)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Evaluation flags for diffeval run."""
|
|
2
|
+
|
|
3
|
+
from diffeval.cli.csv_ints import csv_ints
|
|
4
|
+
from diffeval.cli.csv_names import csv_names
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def add_eval_flags(parser) -> None:
|
|
8
|
+
parser.add_argument("--suite", required=True)
|
|
9
|
+
parser.add_argument("--seeds", type=csv_ints, default=None)
|
|
10
|
+
parser.add_argument("--metrics", type=csv_names, default=None)
|
|
11
|
+
parser.add_argument("--width", type=int, default=None)
|
|
12
|
+
parser.add_argument("--height", type=int, default=None)
|
|
13
|
+
parser.add_argument("--output", default=None)
|
|
14
|
+
parser.add_argument("--strict", action="store_true")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Model flags for diffeval run."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def add_model_flags(parser) -> None:
|
|
5
|
+
parser.add_argument("--backend", default=None)
|
|
6
|
+
parser.add_argument("--pipeline", default=None)
|
|
7
|
+
parser.add_argument("--dtype", default=None)
|
|
8
|
+
parser.add_argument("--steps", type=int, default=None)
|
|
9
|
+
parser.add_argument("--guidance", type=float, default=None)
|
|
10
|
+
parser.add_argument("--device", default=None)
|
|
11
|
+
parser.add_argument("--attention", choices=("sdpa", "xformers", "eager"), default=None)
|
|
12
|
+
parser.add_argument("--variant", default=None)
|
|
13
|
+
parser.add_argument("--batch-size", dest="batch_size", type=int, default=None)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLIPScore metric."""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""L2-normalized CLIP image and text features."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _norm(tensor):
|
|
5
|
+
return tensor / tensor.norm(dim=-1, keepdim=True)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def features(model, processor, images, texts, device):
|
|
9
|
+
import torch
|
|
10
|
+
batch = processor(text=texts, images=images, return_tensors="pt", padding=True, truncation=True, max_length=77)
|
|
11
|
+
batch = batch.to(device)
|
|
12
|
+
with torch.no_grad():
|
|
13
|
+
output = model(**batch)
|
|
14
|
+
return _norm(output.image_embeds), _norm(output.text_embeds)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Load CLIPModel and CLIPProcessor."""
|
|
2
|
+
|
|
3
|
+
_CACHE = {}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def load_clip(model_name: str):
|
|
7
|
+
if model_name in _CACHE:
|
|
8
|
+
return _CACHE[model_name]
|
|
9
|
+
try:
|
|
10
|
+
from transformers import CLIPModel, CLIPProcessor
|
|
11
|
+
except ImportError:
|
|
12
|
+
return None
|
|
13
|
+
model = CLIPModel.from_pretrained(model_name)
|
|
14
|
+
model.eval()
|
|
15
|
+
processor = CLIPProcessor.from_pretrained(model_name)
|
|
16
|
+
_CACHE[model_name] = (model, processor)
|
|
17
|
+
return _CACHE[model_name]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Register clip_score."""
|
|
2
|
+
|
|
3
|
+
from diffeval.metrics.clip.protocol import PROTOCOL
|
|
4
|
+
from diffeval.metrics.clip.score import score_seed
|
|
5
|
+
from diffeval.registry.register import register
|
|
6
|
+
from diffeval.registry.tables import METRICS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ClipScore:
|
|
10
|
+
name = "clip_score"
|
|
11
|
+
description = "CLIPScore (Hessel et al. 2021), ViT-B/32, weight 2.5."
|
|
12
|
+
higher_is_better = True
|
|
13
|
+
requires_references = False
|
|
14
|
+
protocol = PROTOCOL
|
|
15
|
+
|
|
16
|
+
def compute(self, samples, context):
|
|
17
|
+
return score_seed(samples, context, self.protocol)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
register(METRICS, "clip_score")(ClipScore)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""CLIPScore definition text."""
|
|
2
|
+
|
|
3
|
+
PROTOCOL = {
|
|
4
|
+
"paper": "Hessel et al. 2021",
|
|
5
|
+
"model": "openai/clip-vit-base-patch32",
|
|
6
|
+
"weight": 2.5,
|
|
7
|
+
"crop": "shorter-side resize, center crop 224",
|
|
8
|
+
"text": "raw prompt, truncated to 77 tokens, no prefix",
|
|
9
|
+
"formula": "mean of 2.5 * max(cosine, 0)",
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""A sample's pixels, read back from its saved PNG when the runner dropped them."""
|
|
2
|
+
|
|
3
|
+
from PIL import Image
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sample_image(sample, folder):
|
|
7
|
+
if sample.image is not None:
|
|
8
|
+
return sample.image.convert("RGB")
|
|
9
|
+
with Image.open(folder / f"{sample.prompt_id}.png") as image:
|
|
10
|
+
return image.convert("RGB")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Per-image CLIP scores for one seed."""
|
|
2
|
+
|
|
3
|
+
from diffeval.metrics.clip.features import features
|
|
4
|
+
from diffeval.metrics.clip.formula import clip_score
|
|
5
|
+
from diffeval.metrics.clip.load import load_clip
|
|
6
|
+
from diffeval.metrics.clip.sample_image import sample_image
|
|
7
|
+
from diffeval.metrics.ok import ok
|
|
8
|
+
from diffeval.metrics.skipped import skipped
|
|
9
|
+
|
|
10
|
+
CHUNK = 32
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def score_seed(samples, context, protocol):
|
|
14
|
+
loaded = load_clip(protocol["model"])
|
|
15
|
+
if loaded is None:
|
|
16
|
+
return skipped("clip_score", "metrics extra is not installed", True, protocol)
|
|
17
|
+
model, processor = loaded
|
|
18
|
+
model.to(context.device)
|
|
19
|
+
cosines = []
|
|
20
|
+
for start in range(0, len(samples), CHUNK):
|
|
21
|
+
part = samples[start:start + CHUNK]
|
|
22
|
+
images = [sample_image(sample, context.generated_dir) for sample in part]
|
|
23
|
+
image_f, text_f = features(model, processor, images, [sample.prompt for sample in part], context.device)
|
|
24
|
+
cosines.extend((image_f * text_f).sum(dim=-1).tolist())
|
|
25
|
+
return ok("clip_score", float(clip_score(cosines, protocol["weight"])), True, protocol)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CMMD metric."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""One CLIP vision batch, L2-normalized like the official CMMD."""
|
|
2
|
+
|
|
3
|
+
_CACHE = {}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def load_vision(model_name: str):
|
|
7
|
+
if model_name in _CACHE:
|
|
8
|
+
return _CACHE[model_name]
|
|
9
|
+
try:
|
|
10
|
+
from transformers import CLIPImageProcessor, CLIPModel
|
|
11
|
+
except ImportError:
|
|
12
|
+
return None
|
|
13
|
+
model = CLIPModel.from_pretrained(model_name)
|
|
14
|
+
model.eval()
|
|
15
|
+
processor = CLIPImageProcessor.from_pretrained(model_name)
|
|
16
|
+
processor.do_resize = processor.do_center_crop = processor.do_rescale = False
|
|
17
|
+
_CACHE[model_name] = (model, processor)
|
|
18
|
+
return _CACHE[model_name]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def embed_batch(model, processor, images, device):
|
|
22
|
+
import torch
|
|
23
|
+
pixels = processor(images=images, return_tensors="pt")["pixel_values"].to(device)
|
|
24
|
+
with torch.no_grad():
|
|
25
|
+
embeds = model.visual_projection(model.vision_model(pixel_values=pixels).pooler_output)
|
|
26
|
+
return (embeds / embeds.norm(dim=-1, keepdim=True)).detach().cpu().numpy()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Embed image files and the reference bank."""
|
|
2
|
+
|
|
3
|
+
from PIL import Image
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from diffeval.metrics.cmmd.embed_batch import embed_batch
|
|
8
|
+
from diffeval.metrics.cmmd.resize import resize_image
|
|
9
|
+
from diffeval.metrics.images import list_images
|
|
10
|
+
|
|
11
|
+
CHUNK = 32
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def embed_paths(model, processor, paths, device):
|
|
15
|
+
chunks = []
|
|
16
|
+
for start in range(0, len(paths), CHUNK):
|
|
17
|
+
images = [resize_image(Image.open(path)) for path in paths[start:start + CHUNK]]
|
|
18
|
+
chunks.append(embed_batch(model, processor, images, device))
|
|
19
|
+
return np.concatenate(chunks) if chunks else None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def embed_dir(model, processor, folder, device):
|
|
23
|
+
return embed_paths(model, processor, list_images(folder), device)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def paired(context, model, processor):
|
|
27
|
+
generated = embed_dir(model, processor, context.generated_dir, context.device)
|
|
28
|
+
if context.cmmd_ref_embed is not None:
|
|
29
|
+
return generated, np.load(context.cmmd_ref_embed)
|
|
30
|
+
return generated, embed_dir(model, processor, context.reference_dir, context.device)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Register cmmd."""
|
|
2
|
+
|
|
3
|
+
from diffeval.metrics.cmmd.protocol import PROTOCOL
|
|
4
|
+
from diffeval.metrics.cmmd.score import score_cmmd
|
|
5
|
+
from diffeval.registry.register import register
|
|
6
|
+
from diffeval.registry.tables import METRICS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Cmmd:
|
|
10
|
+
name = "cmmd"
|
|
11
|
+
description = "CMMD (Jayasumana et al. 2024), ViT-L/14 at 336, L2-normalized embeddings."
|
|
12
|
+
higher_is_better = False
|
|
13
|
+
requires_references = True
|
|
14
|
+
protocol = PROTOCOL
|
|
15
|
+
|
|
16
|
+
def compute(self, samples, context):
|
|
17
|
+
return score_cmmd(context)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
register(METRICS, "cmmd")(Cmmd)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Minimum-variance RBF MMD, sigma 10, scale 1000, in row blocks."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
_SIGMA = 10
|
|
6
|
+
_SCALE = 1000
|
|
7
|
+
BLOCK = 2048
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _kernel(x, y, x_sq, y_sq, gamma):
|
|
11
|
+
total = 0.0
|
|
12
|
+
for start in range(0, len(x), BLOCK):
|
|
13
|
+
dist = -2 * (x[start:start + BLOCK] @ y.T) + x_sq[start:start + BLOCK, None] + y_sq[None, :]
|
|
14
|
+
total += np.exp(-gamma * dist).sum()
|
|
15
|
+
return total / (len(x) * len(y))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def mmd(x, y) -> float:
|
|
19
|
+
x, y = np.asarray(x, dtype=np.float64), np.asarray(y, dtype=np.float64)
|
|
20
|
+
gamma = 1 / (2 * _SIGMA**2)
|
|
21
|
+
x_sq, y_sq = np.sum(x * x, axis=1), np.sum(y * y, axis=1)
|
|
22
|
+
cross = _kernel(x, y, x_sq, y_sq, gamma)
|
|
23
|
+
return float(_SCALE * (_kernel(x, x, x_sq, x_sq, gamma) + _kernel(y, y, y_sq, y_sq, gamma) - 2 * cross))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""CMMD definition text."""
|
|
2
|
+
|
|
3
|
+
PROTOCOL = {
|
|
4
|
+
"paper": "Jayasumana et al. 2024",
|
|
5
|
+
"model": "openai/clip-vit-large-patch14-336",
|
|
6
|
+
"resize": "bicubic resize of the 8-bit image to 336 (PIL), then divided by 255",
|
|
7
|
+
"crop": "center square crop",
|
|
8
|
+
"embeddings": "vision projection, L2-normalized",
|
|
9
|
+
"check": "cmmd-pytorch sample folders give 7.696",
|
|
10
|
+
"distance": "minimum-variance RBF MMD, sigma 10, scale 1000, diagonal included",
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Center square crop, 8-bit bicubic resize to 336, then divide by 255."""
|
|
2
|
+
|
|
3
|
+
from PIL import Image
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
SIZE = 336
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resize_image(image: Image.Image) -> np.ndarray:
|
|
11
|
+
image = image.convert("RGB")
|
|
12
|
+
width, height = image.size
|
|
13
|
+
side = min(width, height)
|
|
14
|
+
top, left = (height - side) // 2, (width - side) // 2
|
|
15
|
+
square = image.crop((left, top, left + side, top + side))
|
|
16
|
+
resized = square.resize((SIZE, SIZE), Image.Resampling.BICUBIC)
|
|
17
|
+
return np.asarray(resized, dtype=np.float32) / 255.0
|