Skip to content

Commit 6ec8936

Browse files
authored
Merge pull request #130 from BitMind-AI/data/multiclass-taxonomy
data: v23 3-class image / 4-class video taxonomy with audited labels
2 parents 0567c65 + aa15727 commit 6ec8936

17 files changed

Lines changed: 1939 additions & 1105 deletions

‎src/gasbench/benchmarks/common.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ def build_plan(
223223
"semisynthetic": len(
224224
[d for d in available_datasets if d.media_type == "semisynthetic"]
225225
),
226+
"rendered": len(
227+
[d for d in available_datasets if d.media_type == "rendered"]
228+
),
226229
},
227230
)
228231
logger.info(f"Sampling configuration: {json.dumps(sampling_summary.__dict__)}")

‎src/gasbench/benchmarks/utils/inference.py‎

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -81,37 +81,30 @@ def _create_onnx_session(model_path: str, model_type: str):
8181

8282

8383
def process_model_output(logits: np.ndarray) -> Tuple[int, np.ndarray]:
84-
"""
85-
Process model output logits into a binary prediction and probabilities.
86-
87-
Handles 3-class, 2-class, and single-output model formats.
88-
For 3-class models, synthetic and semisynthetic are conflated into AI-generated.
89-
84+
"""Process model output logits into a class prediction and probabilities.
85+
86+
Handles 4-class, 3-class, 2-class, and single-output model formats.
87+
The returned class index is argmax (0=real for all multimodal heads).
88+
Binary SN34 collapse (real vs not-real) happens in Metrics.update.
89+
9090
Args:
9191
logits: Raw model output logits (1D array from single sample)
92-
92+
9393
Returns:
94-
Tuple of (binary_prediction, probabilities)
95-
- binary_prediction: 0=real, 1=AI-generated
96-
- probabilities: softmax probabilities as 1D array (length 1,2, or 3)
94+
Tuple of (predicted_class, probabilities)
95+
- predicted_class: argmax class, or 0/1 for a single-logit head
96+
- probabilities: sigmoid (len 1) or softmax (len 2/3/4)
9797
"""
9898
logits = np.atleast_1d(logits).flatten()
9999

100100
if len(logits) == 1:
101101
p = 1.0 / (1.0 + np.exp(-logits[0]))
102102
pred_probs = np.array([p], dtype=np.float64)
103-
predicted_binary = int(p > 0.5)
104-
return predicted_binary, pred_probs
103+
predicted_class = int(p > 0.5)
104+
return predicted_class, pred_probs
105105

106106
exp_x = np.exp(logits - np.max(logits))
107107
pred_probs = exp_x / np.sum(exp_x)
108-
109-
if len(pred_probs) == 3:
110-
predicted_binary = 0 if np.argmax(pred_probs) == 0 else 1
111-
elif len(pred_probs) == 2:
112-
predicted_binary = int(np.argmax(pred_probs))
113-
else:
114-
predicted_binary = int(pred_probs[0] > 0.5)
115-
116-
return predicted_binary, pred_probs
108+
predicted_class = int(np.argmax(pred_probs))
109+
return predicted_class, pred_probs
117110

‎src/gasbench/benchmarks/utils/metrics.py‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,25 @@ def update(
3030
of the same sample would. Default weight=1.0 reproduces unweighted
3131
behavior.
3232
"""
33-
if label == 1 and pred == 1:
33+
# SN34 stays real vs not-real. Multiclass labels/preds (semi=2, rendered=3)
34+
# collapse onto the same binary confusion matrix.
35+
binary_label = 0 if label == 0 else 1
36+
binary_pred = 0 if pred == 0 else 1
37+
38+
if binary_label == 1 and binary_pred == 1:
3439
self.true_positives += weight
35-
elif label == 0 and pred == 0:
40+
elif binary_label == 0 and binary_pred == 0:
3641
self.true_negatives += weight
37-
elif label == 0 and pred == 1:
42+
elif binary_label == 0 and binary_pred == 1:
3843
self.false_positives += weight
39-
elif label == 1 and pred == 0:
44+
elif binary_label == 1 and binary_pred == 0:
4045
self.false_negatives += weight
4146

4247
if pred_probs is not None:
43-
self.binary_y_true.append(label)
48+
self.binary_y_true.append(binary_label)
4449
self.binary_weights.append(weight)
45-
if len(pred_probs) == 3:
46-
binary_prob = 1.0 - pred_probs[0]
47-
self.binary_probs.append(binary_prob)
48-
elif len(pred_probs) == 2:
49-
self.binary_probs.append(pred_probs[1])
50+
if len(pred_probs) >= 2:
51+
self.binary_probs.append(float(1.0 - pred_probs[0]))
5052
else:
5153
self.binary_probs.append(pred_probs[0])
5254

‎src/gasbench/benchmarks/utils/pytorch_session.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,9 @@ def run(self, output_names: Any, input_dict: dict) -> List[np.ndarray]:
182182
)
183183

184184
last_dim = output.shape[-1]
185-
if last_dim not in (1, 2, 3) and last_dim != self._num_classes:
185+
if last_dim not in (1, 2, 3, 4) and last_dim != self._num_classes:
186186
raise ValueError(
187-
f"Model output last dimension is {last_dim}, expected 1, 2, 3, "
187+
f"Model output last dimension is {last_dim}, expected 1, 2, 3, 4, "
188188
f"or num_classes ({self._num_classes}). Check that model.py returns "
189189
f"logits and that model_config.yaml declares the correct num_classes."
190190
)

‎src/gasbench/constants.py‎

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,68 @@
11
"""Constants used throughout the gasbench package."""
22

3-
# Media type to label mapping for binary classification
4-
# 0 = real, 1 = AI-generated (synthetic or semisynthetic)
5-
# Note: Dataset configs still distinguish synthetic vs semisynthetic for provenance tracking
3+
# Per-modality class indices. Class 0 is always real so binary collapse is 1 - p[0].
4+
#
5+
# Image is 3-class (no rendered): 0=real, 1=synthetic, 2=semisynthetic.
6+
# Video is 4-class: 0=real, 1=synthetic, 2=semisynthetic, 3=rendered.
7+
# Audio stays binary: 0=real, 1=synthetic (semisynthetic collapsed).
8+
#
9+
# Semisynthetic media must derive materially from captured source media. An edit
10+
# of exclusively synthetic or rendered inputs retains that fully synthetic
11+
# provenance; the operation name alone (i2i, inpaint, etc.) does not determine
12+
# the class. The captured contribution may be spatial, temporal, or global.
13+
#
14+
# Image has no rendered class: CGI/game-engine stills are excluded from image configs.
15+
IMAGE_MEDIA_TYPE_TO_LABEL = {
16+
"real": 0,
17+
"synthetic": 1,
18+
"semisynthetic": 2,
19+
}
20+
VIDEO_MEDIA_TYPE_TO_LABEL = {
21+
"real": 0,
22+
"synthetic": 1,
23+
"semisynthetic": 2,
24+
"rendered": 3,
25+
}
26+
AUDIO_MEDIA_TYPE_TO_LABEL = {
27+
"real": 0,
28+
"synthetic": 1,
29+
"semisynthetic": 1,
30+
}
31+
32+
VALID_MEDIA_TYPES = {
33+
"image": frozenset(IMAGE_MEDIA_TYPE_TO_LABEL),
34+
"video": frozenset(VIDEO_MEDIA_TYPE_TO_LABEL),
35+
"audio": frozenset({"real", "synthetic", "semisynthetic"}),
36+
}
37+
38+
_MODALITY_LABELS = {
39+
"image": IMAGE_MEDIA_TYPE_TO_LABEL,
40+
"video": VIDEO_MEDIA_TYPE_TO_LABEL,
41+
"audio": AUDIO_MEDIA_TYPE_TO_LABEL,
42+
}
43+
44+
# Legacy binary map (real vs not-real). Prefer media_type_to_label(media_type, modality).
645
MEDIA_TYPE_TO_LABEL = {
746
"real": 0,
847
"synthetic": 1,
948
"semisynthetic": 1,
1049
}
1150

51+
52+
def media_type_to_label(media_type: str, modality: str) -> int:
53+
"""Map a dataset media_type to the integer class index for this modality."""
54+
table = _MODALITY_LABELS.get(modality)
55+
if table is None:
56+
raise KeyError(f"Unknown modality '{modality}'")
57+
try:
58+
return table[media_type]
59+
except KeyError:
60+
raise KeyError(
61+
f"Invalid media_type '{media_type}' for modality '{modality}'. "
62+
f"Valid: {sorted(table)}"
63+
) from None
64+
65+
1266
# Video evaluation limits
1367
# Caps num_frames to prevent submitted models from overwhelming the eval system.
1468
MAX_VIDEO_NUM_FRAMES = 64
15-

‎src/gasbench/dataset/config.py‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import hashlib
88

99
from ..logger import get_logger
10+
from ..constants import VALID_MEDIA_TYPES
1011

1112
logger = get_logger(__name__)
1213

@@ -46,7 +47,7 @@ class BenchmarkDatasetConfig:
4647
name: str
4748
path: str
4849
modality: str # "image", "video", or "audio"
49-
media_type: str # "real", "synthetic", or "semisynthetic"
50+
media_type: str # image: real/synthetic/semisynthetic; video also allows rendered
5051

5152
# Download parameters
5253
media_per_archive: int = 100
@@ -324,11 +325,14 @@ def validate_dataset_config(
324325
)
325326

326327
if "media_type" in config_dict:
327-
valid_media_types = ["real", "synthetic", "semisynthetic"]
328+
modality = config_dict.get("modality", "image")
329+
valid_media_types = VALID_MEDIA_TYPES.get(
330+
modality, VALID_MEDIA_TYPES["image"]
331+
)
328332
if config_dict["media_type"] not in valid_media_types:
329333
errors.append(
330-
f"Dataset '{dataset_name}': Invalid media_type '{config_dict['media_type']}'. "
331-
f"Must be one of {valid_media_types}"
334+
f"Dataset '{dataset_name}': Invalid media_type '{config_dict['media_type']}' "
335+
f"for modality '{modality}'. Must be one of {sorted(valid_media_types)}"
332336
)
333337

334338
if "source" in config_dict:
@@ -609,7 +613,8 @@ def _load_modality_config(modality: str, custom_path: Optional[str] = None) -> l
609613
data = yaml.safe_load(f)
610614
return data.get("datasets", [])
611615

612-
# Load from split real + synthetic configs and merge
616+
# Load from split real + synthetic configs and merge.
617+
# legacy_images.yaml / legacy_videos.yaml are bundled but not loaded here.
613618
SPLIT_CONFIGS = {
614619
"image": ("real_images.yaml", "synthetic_images.yaml"),
615620
"video": ("real_videos.yaml", "synthetic_videos.yaml"),
@@ -742,12 +747,13 @@ def get_benchmark_dataset_summary() -> Dict:
742747
summary = {
743748
"image": {
744749
"total": len(image_datasets),
745-
"active": len(
746-
[d for d in image_datasets if d.media_type in ["real", "synthetic"]]
747-
),
750+
"active": len(image_datasets),
748751
"synthetic": len(
749752
[d for d in image_datasets if d.media_type == "synthetic"]
750753
),
754+
"semisynthetic": len(
755+
[d for d in image_datasets if d.media_type == "semisynthetic"]
756+
),
751757
"real": len([d for d in image_datasets if d.media_type == "real"]),
752758
"datasets": [
753759
{"name": d.name, "path": d.path, "media_type": d.media_type}
@@ -756,12 +762,16 @@ def get_benchmark_dataset_summary() -> Dict:
756762
},
757763
"video": {
758764
"total": len(video_datasets),
759-
"active": len(
760-
[d for d in video_datasets if d.media_type in ["real", "synthetic"]]
761-
),
765+
"active": len(video_datasets),
762766
"synthetic": len(
763767
[d for d in video_datasets if d.media_type == "synthetic"]
764768
),
769+
"semisynthetic": len(
770+
[d for d in video_datasets if d.media_type == "semisynthetic"]
771+
),
772+
"rendered": len(
773+
[d for d in video_datasets if d.media_type == "rendered"]
774+
),
765775
"real": len([d for d in video_datasets if d.media_type == "real"]),
766776
"datasets": [
767777
{"name": d.name, "path": d.path, "media_type": d.media_type}

0 commit comments

Comments
 (0)