akcanca commited on
Commit
e728fa2
·
verified ·
1 Parent(s): dfc4443

Upload basic_explainer.py

Browse files
Files changed (1) hide show
  1. src/explainers/basic_explainer.py +68 -16
src/explainers/basic_explainer.py CHANGED
@@ -128,14 +128,32 @@ class BasicExplainer:
128
  supports_fake += 1
129
 
130
  conflict = (supports_fake > 0 and supports_real > 0)
 
 
 
 
 
 
 
131
 
132
  # -------------------- Triage decision (narrative only) --------------------
133
  triage_label = base_label_str
134
  if self.enable_triage and conflict and confidence < self.triage_conf_threshold:
135
  triage_label = "UNCERTAIN"
 
 
 
136
 
137
  # Intro sentence
138
- if triage_label == "UNCERTAIN":
 
 
 
 
 
 
 
 
139
  explanation_parts.append(
140
  f"The detector predicts this image is **{base_label_str}** "
141
  f"with {confidence_str} confidence ({confidence:.2f}), "
@@ -170,12 +188,12 @@ class BasicExplainer:
170
  if prediction_label == 0:
171
  explanation_parts.append(
172
  f"- **Noiseprint**: fingerprint lies within the range seen in training real images "
173
- f"(mismatch={nm:.2f} {thr_nm:.2f}), supporting the REAL hypothesis."
174
  )
175
  else:
176
  explanation_parts.append(
177
  f"- **Noiseprint**: fingerprint lies within the range seen in training real images "
178
- f"(mismatch={nm:.2f} {thr_nm:.2f}), but other forensic cues indicate synthesis."
179
  )
180
  cues_used += 1
181
 
@@ -225,25 +243,59 @@ class BasicExplainer:
225
  f"and conflicts with the REAL prediction."
226
  )
227
  cues_used += 1
228
- # If fp ≤ thr_fp we treat it as weak / neutral, so we skip.
229
-
230
- if cues_used == 0:
231
- explanation_parts.append(
232
- "No individual forensic cue strongly deviated from the training distribution; "
233
- "the decision is based on a subtle combination of features."
234
- )
 
235
 
236
- # -------------------- Data-driven drivers (optional) --------------------
237
  if contributions:
238
  sorted_contribs = sorted(contributions.items(), key=lambda x: abs(x[1]), reverse=True)
239
- top = sorted_contribs[:top_k_contributions]
240
- pos = [f"{name} ({val:+.2f})" for name, val in top if val > 0]
241
- neg = [f"{name} ({val:+.2f})" for name, val in top if val < 0]
 
242
 
243
  if pos:
244
- explanation_parts.append(f"- **Top pushes toward FAKE**: {', '.join(pos)}")
 
 
 
245
  if neg:
246
- explanation_parts.append(f"- **Top pushes toward REAL**: {', '.join(neg)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
  # In high-conflict cases, add a final triage note
249
  if triage_label == "UNCERTAIN" and not is_ood:
 
128
  supports_fake += 1
129
 
130
  conflict = (supports_fake > 0 and supports_real > 0)
131
+
132
+ # -------------------- Suspiciously clean detection --------------------
133
+ # If ALL forensic cues are below threshold (supports_real > 0 and supports_fake == 0),
134
+ # AND the prediction is REAL, this could indicate a modern generator that evades detection.
135
+ # Flag as potentially suspicious if all cues are "clean" but confidence isn't very high.
136
+ suspiciously_clean = (supports_fake == 0 and supports_real >= 2 and
137
+ prediction_label == 0 and confidence < 0.98)
138
 
139
  # -------------------- Triage decision (narrative only) --------------------
140
  triage_label = base_label_str
141
  if self.enable_triage and conflict and confidence < self.triage_conf_threshold:
142
  triage_label = "UNCERTAIN"
143
+ elif self.enable_triage and suspiciously_clean and confidence < 0.95:
144
+ # Modern generators like Flux may evade all forensic cues
145
+ triage_label = "UNCERTAIN"
146
 
147
  # Intro sentence
148
+ if triage_label == "UNCERTAIN" and suspiciously_clean:
149
+ explanation_parts.append(
150
+ f"⚠️ **CAUTION**: The detector predicts this image is **{base_label_str}** "
151
+ f"with {confidence_str} confidence ({confidence:.2f}), "
152
+ f"but ALL forensic cues are below threshold. This could indicate a modern generator "
153
+ f"(like Flux, DALL-E 3, or Midjourney v6) that evades traditional forensic detection. "
154
+ f"**Manual review recommended.**"
155
+ )
156
+ elif triage_label == "UNCERTAIN":
157
  explanation_parts.append(
158
  f"The detector predicts this image is **{base_label_str}** "
159
  f"with {confidence_str} confidence ({confidence:.2f}), "
 
188
  if prediction_label == 0:
189
  explanation_parts.append(
190
  f"- **Noiseprint**: fingerprint lies within the range seen in training real images "
191
+ f"(mismatch={nm:.2f} <= {thr_nm:.2f}), supporting the REAL hypothesis."
192
  )
193
  else:
194
  explanation_parts.append(
195
  f"- **Noiseprint**: fingerprint lies within the range seen in training real images "
196
+ f"(mismatch={nm:.2f} <= {thr_nm:.2f}), but other forensic cues indicate synthesis."
197
  )
198
  cues_used += 1
199
 
 
243
  f"and conflicts with the REAL prediction."
244
  )
245
  cues_used += 1
246
+ elif prediction_label == 1:
247
+ # Even if below threshold, mention it if prediction is FAKE and it's close to threshold
248
+ if fp > thr_fp * 0.8: # Within 80% of threshold
249
+ explanation_parts.append(
250
+ f"- **Frequency spectrum**: peakiness ({fp:.2f}) is moderately elevated "
251
+ f"(threshold: {thr_fp:.2f}), contributing to the FAKE classification."
252
+ )
253
+ cues_used += 1
254
 
255
+ # -------------------- Data-driven drivers (show what actually drove the decision) --------------------
256
  if contributions:
257
  sorted_contribs = sorted(contributions.items(), key=lambda x: abs(x[1]), reverse=True)
258
+ # Show top 5-8 features for better explanation
259
+ top = sorted_contribs[:max(top_k_contributions, 8)]
260
+ pos = [(name, val) for name, val in top if val > 0]
261
+ neg = [(name, val) for name, val in top if val < 0]
262
 
263
  if pos:
264
+ explanation_parts.append(f"\n**Features driving FAKE classification:**")
265
+ # Show top 5-8 features that push toward FAKE
266
+ pos_display = [f"{name} ({val:+.3f})" for name, val in pos[:8]]
267
+ explanation_parts.append(f"- {', '.join(pos_display)}")
268
  if neg:
269
+ explanation_parts.append(f"\n**Features supporting REAL classification:**")
270
+ # Show top 3-5 features that push toward REAL
271
+ neg_display = [f"{name} ({val:+.3f})" for name, val in neg[:5]]
272
+ explanation_parts.append(f"- {', '.join(neg_display)}")
273
+ elif not contributions and (cues_used == 0 or (prediction_label == 1 and cues_used < 2)):
274
+ # If no strong forensic cues but high confidence, explain it's a combination
275
+ explanation_parts.append(
276
+ f"\n**Note**: While the primary forensic cues (Noiseprint, Residuals, FFT) don't individually "
277
+ f"strongly indicate synthesis, the model's decision is based on a combination of many features "
278
+ f"including DCT coefficients, FFT radial profiles, residual statistics, and other frequency-domain "
279
+ f"characteristics. The high confidence ({confidence:.1%}) suggests these subtle patterns collectively "
280
+ f"indicate synthetic generation."
281
+ )
282
+
283
+ # List some of the other features that might be contributing
284
+ other_features = []
285
+ if 'dct_mean' in features:
286
+ other_features.append("DCT coefficients")
287
+ if 'fft_radial_mean' in features:
288
+ other_features.append("FFT radial profiles")
289
+ if 'residual_skew' in features:
290
+ other_features.append("residual statistics")
291
+ if 'residual_kurtosis' in features:
292
+ other_features.append("residual distribution shape")
293
+
294
+ if other_features:
295
+ explanation_parts.append(
296
+ f"The model analyzes {', '.join(other_features)} and other frequency-domain patterns "
297
+ f"that collectively indicate synthetic generation, even when individual cues are subtle."
298
+ )
299
 
300
  # In high-conflict cases, add a final triage note
301
  if triage_label == "UNCERTAIN" and not is_ood: