TechnoSamurai02 commited on
Commit
010eb03
·
1 Parent(s): e5fa792

Optimize localized manipulation decisions

Browse files
training/optimize_manipulation_decision.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import csv
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ AUTHENTIC_LABELS = {"authentic", "real", "real_camera"}
11
+ GENERATED_LABELS = {"generated", "ai_generated"}
12
+ MANIPULATED_LABELS = {"manipulated", "ai_manipulated"}
13
+
14
+
15
+ def parse_args() -> argparse.Namespace:
16
+ parser = argparse.ArgumentParser(
17
+ description=(
18
+ "Select a manipulation score and localized-area rule on tuning predictions "
19
+ "under fixed precision and false-warning constraints."
20
+ )
21
+ )
22
+ parser.add_argument("predictions", type=Path)
23
+ parser.add_argument("--output", type=Path)
24
+ parser.add_argument("--minimum-precision", type=float, default=0.95)
25
+ parser.add_argument("--authentic-false-warning-limit", type=float, default=0.01)
26
+ parser.add_argument("--generated-false-warning-limit", type=float, default=0.01)
27
+ parser.add_argument("--max-view-range", type=float, default=0.18)
28
+ return parser.parse_args()
29
+
30
+
31
+ def read_rows(path: Path) -> list[dict[str, Any]]:
32
+ with path.open("r", encoding="utf-8-sig", newline="") as handle:
33
+ values = list(csv.DictReader(handle))
34
+ rows: list[dict[str, Any]] = []
35
+ for value in values:
36
+ label = str(value.get("label") or "").strip().lower()
37
+ try:
38
+ score = float(value["manipulation_score"])
39
+ area = float(value["predicted_region_area_ratio"])
40
+ view_range = float(value["view_score_range"])
41
+ except (KeyError, TypeError, ValueError):
42
+ continue
43
+ rows.append(
44
+ {
45
+ "label": label,
46
+ "score": score,
47
+ "area": area,
48
+ "view_range": view_range,
49
+ "localized_support": _boolean(value.get("localized_or_persistent_support")),
50
+ "stable": _boolean(value.get("stable_across_views")),
51
+ }
52
+ )
53
+ return rows
54
+
55
+
56
+ def optimize(
57
+ rows: list[dict[str, Any]],
58
+ *,
59
+ minimum_precision: float,
60
+ authentic_false_warning_limit: float,
61
+ generated_false_warning_limit: float,
62
+ max_view_range: float,
63
+ ) -> dict[str, Any]:
64
+ counts = {
65
+ "authentic": sum(row["label"] in AUTHENTIC_LABELS for row in rows),
66
+ "generated": sum(row["label"] in GENERATED_LABELS for row in rows),
67
+ "manipulated": sum(row["label"] in MANIPULATED_LABELS for row in rows),
68
+ }
69
+ if not all(counts.values()):
70
+ raise ValueError("Optimization requires authentic, generated, and manipulated tuning rows.")
71
+ area_candidates = sorted({0.0, *(float(row["area"]) for row in rows)})
72
+ best: dict[str, Any] | None = None
73
+ feasible_rules = 0
74
+ for area_threshold in area_candidates:
75
+ eligible = [
76
+ row
77
+ for row in rows
78
+ if row["localized_support"]
79
+ and row["stable"]
80
+ and float(row["view_range"]) <= max_view_range
81
+ and float(row["area"]) >= area_threshold
82
+ ]
83
+ eligible.sort(key=lambda row: float(row["score"]), reverse=True)
84
+ true_manipulated = false_authentic = false_generated = 0
85
+ for index, row in enumerate(eligible):
86
+ label = row["label"]
87
+ true_manipulated += int(label in MANIPULATED_LABELS)
88
+ false_authentic += int(label in AUTHENTIC_LABELS)
89
+ false_generated += int(label in GENERATED_LABELS)
90
+ next_score = float(eligible[index + 1]["score"]) if index + 1 < len(eligible) else None
91
+ threshold = float(row["score"])
92
+ if next_score is not None and next_score == threshold:
93
+ continue
94
+ predicted = index + 1
95
+ precision = true_manipulated / predicted
96
+ authentic_rate = false_authentic / counts["authentic"]
97
+ generated_rate = false_generated / counts["generated"]
98
+ if (
99
+ precision < minimum_precision
100
+ or authentic_rate > authentic_false_warning_limit
101
+ or generated_rate > generated_false_warning_limit
102
+ ):
103
+ continue
104
+ feasible_rules += 1
105
+ candidate = {
106
+ "manipulation_score_threshold": round(threshold, 8),
107
+ "minimum_localized_area_ratio": round(area_threshold, 8),
108
+ "max_view_score_range": max_view_range,
109
+ "predicted_manipulated": predicted,
110
+ "true_manipulated": true_manipulated,
111
+ "false_manipulation_warnings": false_authentic + false_generated,
112
+ "precision": precision,
113
+ "recall": true_manipulated / counts["manipulated"],
114
+ "authentic_false_warning_rate": authentic_rate,
115
+ "generated_false_manipulation_rate": generated_rate,
116
+ }
117
+ if best is None or _rank(candidate) > _rank(best):
118
+ best = candidate
119
+ return {
120
+ "record_count": len(rows),
121
+ "class_counts": counts,
122
+ "constraints": {
123
+ "minimum_precision": minimum_precision,
124
+ "authentic_false_warning_limit": authentic_false_warning_limit,
125
+ "generated_false_warning_limit": generated_false_warning_limit,
126
+ "max_view_score_range": max_view_range,
127
+ },
128
+ "feasible_rule_count": feasible_rules,
129
+ "best_rule": best,
130
+ "status": "candidate_requires_calibration" if best else "no_rule_meets_constraints",
131
+ "warning": (
132
+ "This rule was selected on tuning data. Freeze it, evaluate it on a separate "
133
+ "calibration split, and do not promote it from this report alone."
134
+ ),
135
+ }
136
+
137
+
138
+ def _rank(candidate: dict[str, Any]) -> tuple[float, float, float, float]:
139
+ return (
140
+ float(candidate["recall"]),
141
+ float(candidate["precision"]),
142
+ -float(candidate["false_manipulation_warnings"]),
143
+ -float(candidate["minimum_localized_area_ratio"]),
144
+ )
145
+
146
+
147
+ def _boolean(value: object) -> bool:
148
+ if isinstance(value, bool):
149
+ return value
150
+ return str(value or "").strip().lower() in {"1", "true", "yes"}
151
+
152
+
153
+ def main() -> None:
154
+ args = parse_args()
155
+ report = optimize(
156
+ read_rows(args.predictions),
157
+ minimum_precision=args.minimum_precision,
158
+ authentic_false_warning_limit=args.authentic_false_warning_limit,
159
+ generated_false_warning_limit=args.generated_false_warning_limit,
160
+ max_view_range=args.max_view_range,
161
+ )
162
+ rendered = json.dumps(report, indent=2, sort_keys=True)
163
+ if args.output:
164
+ args.output.parent.mkdir(parents=True, exist_ok=True)
165
+ args.output.write_text(rendered + "\n", encoding="utf-8")
166
+ print(rendered)
167
+ if report["best_rule"] is None:
168
+ raise SystemExit(3)
169
+
170
+
171
+ if __name__ == "__main__":
172
+ main()
training/tests/test_image_training_pipeline.py CHANGED
@@ -17,6 +17,7 @@ from training.prepare_diffusion_manipulation_pairs import (
17
  SPLIT_MODEL_SPECS,
18
  _random_inpainting_mask,
19
  )
 
20
  from training.evaluate_manipulation_localizer import _localized_support, _threshold_metrics
21
  from training.train_image_detector import (
22
  _augment_image,
@@ -319,6 +320,28 @@ class ImageTrainingPipelineTests(unittest.TestCase):
319
  self.assertEqual(metrics["precision"], 1.0)
320
  self.assertEqual(metrics["recall"], 1.0)
321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  def test_image_level_loss_penalizes_a_hot_false_positive_region(self) -> None:
323
  import torch
324
  import torch.nn.functional as functional
 
17
  SPLIT_MODEL_SPECS,
18
  _random_inpainting_mask,
19
  )
20
+ from training.optimize_manipulation_decision import optimize
21
  from training.evaluate_manipulation_localizer import _localized_support, _threshold_metrics
22
  from training.train_image_detector import (
23
  _augment_image,
 
320
  self.assertEqual(metrics["precision"], 1.0)
321
  self.assertEqual(metrics["recall"], 1.0)
322
 
323
+ def test_manipulation_optimizer_can_require_a_meaningful_region(self) -> None:
324
+ rows = [
325
+ {"label": "real_camera", "score": 0.95, "area": 0.01},
326
+ {"label": "ai_generated", "score": 0.94, "area": 0.01},
327
+ {"label": "ai_manipulated", "score": 0.90, "area": 0.20},
328
+ {"label": "ai_manipulated", "score": 0.85, "area": 0.15},
329
+ ]
330
+ for row in rows:
331
+ row.update({"view_range": 0.02, "localized_support": True, "stable": True})
332
+
333
+ report = optimize(
334
+ rows,
335
+ minimum_precision=0.95,
336
+ authentic_false_warning_limit=0.01,
337
+ generated_false_warning_limit=0.01,
338
+ max_view_range=0.18,
339
+ )
340
+
341
+ self.assertEqual(report["best_rule"]["precision"], 1.0)
342
+ self.assertEqual(report["best_rule"]["recall"], 1.0)
343
+ self.assertEqual(report["best_rule"]["minimum_localized_area_ratio"], 0.15)
344
+
345
  def test_image_level_loss_penalizes_a_hot_false_positive_region(self) -> None:
346
  import torch
347
  import torch.nn.functional as functional