Initial release: Unified Dual-Expert ViT balanced AI detector
Browse files- README.md +27 -0
- config.json +27 -0
- guardian/config.json +24 -0
- guardian/model.safetensors +3 -0
- hunter/config.json +24 -0
- hunter/model.safetensors +3 -0
- pipeline.py +35 -0
- preprocessor_config.json +23 -0
README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language:
|
| 3 |
+
- en
|
| 4 |
+
- id
|
| 5 |
+
license: apache-2.0
|
| 6 |
+
tags:
|
| 7 |
+
- vision
|
| 8 |
+
- image-classification
|
| 9 |
+
- deepfake-detection
|
| 10 |
+
- ai-generated
|
| 11 |
+
- defactify
|
| 12 |
+
- dual-expert
|
| 13 |
+
pipeline_tag: image-classification
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# Dual-Expert Vision Transformer (Defactify Balanced)
|
| 17 |
+
|
| 18 |
+
Arsitektur ensemble spesialis untuk memecahkan classifier bias pada deteksi gambar AI modern.
|
| 19 |
+
- **Pakar Real (Guardian):** Mencegah false positive pada foto asli (Weight [3.0, 1.0]).
|
| 20 |
+
- **Pakar AI (Hunter):** Menangkap artefak generator difusi modern (Weight [1.0, 3.0]).
|
| 21 |
+
|
| 22 |
+
## Benchmark Evaluasi (500 Sampel Defactify Test)
|
| 23 |
+
- Balanced Accuracy: 79.00%
|
| 24 |
+
- Macro F1: 79.00%
|
| 25 |
+
- Recall Real: 80.00%
|
| 26 |
+
- Recall AI: 78.00%
|
| 27 |
+
- Latency: ~6.57 ms (Concurrent CUDA Streams)
|
config.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "dual_expert_vision_transformer",
|
| 3 |
+
"base_architecture": "google/vit-base-patch16-224",
|
| 4 |
+
"num_classes": 2,
|
| 5 |
+
"id2label": {
|
| 6 |
+
"0": "Real Camera",
|
| 7 |
+
"1": "AI-Generated"
|
| 8 |
+
},
|
| 9 |
+
"label2id": {
|
| 10 |
+
"Real Camera": 0,
|
| 11 |
+
"AI-Generated": 1
|
| 12 |
+
},
|
| 13 |
+
"calibration": {
|
| 14 |
+
"method": "Linear Weighted Consensus",
|
| 15 |
+
"w_hunter": 0.6,
|
| 16 |
+
"w_guardian": 0.4,
|
| 17 |
+
"threshold": 0.46
|
| 18 |
+
},
|
| 19 |
+
"metrics_benchmark": {
|
| 20 |
+
"dataset": "Defactify Balanced (500 Test)",
|
| 21 |
+
"accuracy": 0.79,
|
| 22 |
+
"macro_f1": 0.79,
|
| 23 |
+
"recall_real": 0.8,
|
| 24 |
+
"recall_ai": 0.78,
|
| 25 |
+
"latency_ms": 6.57
|
| 26 |
+
}
|
| 27 |
+
}
|
guardian/config.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"ViTForImageClassification"
|
| 4 |
+
],
|
| 5 |
+
"attention_probs_dropout_prob": 0.0,
|
| 6 |
+
"dtype": "float32",
|
| 7 |
+
"encoder_stride": 16,
|
| 8 |
+
"hidden_act": "gelu",
|
| 9 |
+
"hidden_dropout_prob": 0.0,
|
| 10 |
+
"hidden_size": 768,
|
| 11 |
+
"image_size": 224,
|
| 12 |
+
"initializer_range": 0.02,
|
| 13 |
+
"intermediate_size": 3072,
|
| 14 |
+
"layer_norm_eps": 1e-12,
|
| 15 |
+
"model_type": "vit",
|
| 16 |
+
"num_attention_heads": 12,
|
| 17 |
+
"num_channels": 3,
|
| 18 |
+
"num_hidden_layers": 12,
|
| 19 |
+
"patch_size": 16,
|
| 20 |
+
"pooler_act": "tanh",
|
| 21 |
+
"pooler_output_size": 768,
|
| 22 |
+
"qkv_bias": true,
|
| 23 |
+
"transformers_version": "5.0.0"
|
| 24 |
+
}
|
guardian/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:697950f2f90f29120db62f55dbfce8ef801b28adff7ea6e05b5528886b5a5945
|
| 3 |
+
size 343223968
|
hunter/config.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"ViTForImageClassification"
|
| 4 |
+
],
|
| 5 |
+
"attention_probs_dropout_prob": 0.0,
|
| 6 |
+
"dtype": "float32",
|
| 7 |
+
"encoder_stride": 16,
|
| 8 |
+
"hidden_act": "gelu",
|
| 9 |
+
"hidden_dropout_prob": 0.0,
|
| 10 |
+
"hidden_size": 768,
|
| 11 |
+
"image_size": 224,
|
| 12 |
+
"initializer_range": 0.02,
|
| 13 |
+
"intermediate_size": 3072,
|
| 14 |
+
"layer_norm_eps": 1e-12,
|
| 15 |
+
"model_type": "vit",
|
| 16 |
+
"num_attention_heads": 12,
|
| 17 |
+
"num_channels": 3,
|
| 18 |
+
"num_hidden_layers": 12,
|
| 19 |
+
"patch_size": 16,
|
| 20 |
+
"pooler_act": "tanh",
|
| 21 |
+
"pooler_output_size": 768,
|
| 22 |
+
"qkv_bias": true,
|
| 23 |
+
"transformers_version": "5.0.0"
|
| 24 |
+
}
|
hunter/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e25d011f8ce70594c1a6577fdbbb735592c671008293e816d5e909b86dcbdba4
|
| 3 |
+
size 343223968
|
pipeline.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
from transformers import AutoImageProcessor, AutoModelForImageClassification
|
| 6 |
+
from PIL import Image
|
| 7 |
+
|
| 8 |
+
class DualExpertDetector(nn.Module):
|
| 9 |
+
def __init__(self, model_dir: str):
|
| 10 |
+
super().__init__()
|
| 11 |
+
self.processor = AutoImageProcessor.from_pretrained(model_dir)
|
| 12 |
+
self.guardian = AutoModelForImageClassification.from_pretrained(os.path.join(model_dir, 'guardian'))
|
| 13 |
+
self.hunter = AutoModelForImageClassification.from_pretrained(os.path.join(model_dir, 'hunter'))
|
| 14 |
+
with open(os.path.join(model_dir, 'config.json'), 'r') as f:
|
| 15 |
+
cfg = json.load(f)['calibration']
|
| 16 |
+
self.w_hunter = cfg['w_hunter']
|
| 17 |
+
self.w_guardian = cfg['w_guardian']
|
| 18 |
+
self.threshold = cfg['threshold']
|
| 19 |
+
|
| 20 |
+
def predict(self, image: Image.Image, device: str = 'cuda'):
|
| 21 |
+
self.to(device)
|
| 22 |
+
self.eval()
|
| 23 |
+
inputs = self.processor(images=image.convert('RGB'), return_tensors='pt').to(device)
|
| 24 |
+
with torch.no_grad():
|
| 25 |
+
p_g = torch.softmax(self.guardian(**inputs).logits, dim=-1)[0, 1].item()
|
| 26 |
+
p_h = torch.softmax(self.hunter(**inputs).logits, dim=-1)[0, 1].item()
|
| 27 |
+
combined = (self.w_hunter * p_h) + (self.w_guardian * p_g)
|
| 28 |
+
is_ai = combined >= self.threshold
|
| 29 |
+
return {
|
| 30 |
+
'verdict': 'AI-Generated' if is_ai else 'Real Camera',
|
| 31 |
+
'is_ai': bool(is_ai),
|
| 32 |
+
'confidence_ai': round(combined * 100, 2),
|
| 33 |
+
'confidence_real': round((1.0 - combined) * 100, 2),
|
| 34 |
+
'raw_scores': {'hunter_ai': round(p_h * 100, 2), 'guardian_ai': round(p_g * 100, 2)}
|
| 35 |
+
}
|
preprocessor_config.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"do_convert_rgb": null,
|
| 3 |
+
"do_normalize": true,
|
| 4 |
+
"do_rescale": true,
|
| 5 |
+
"do_resize": true,
|
| 6 |
+
"image_mean": [
|
| 7 |
+
0.5,
|
| 8 |
+
0.5,
|
| 9 |
+
0.5
|
| 10 |
+
],
|
| 11 |
+
"image_processor_type": "ViTImageProcessor",
|
| 12 |
+
"image_std": [
|
| 13 |
+
0.5,
|
| 14 |
+
0.5,
|
| 15 |
+
0.5
|
| 16 |
+
],
|
| 17 |
+
"resample": 2,
|
| 18 |
+
"rescale_factor": 0.00392156862745098,
|
| 19 |
+
"size": {
|
| 20 |
+
"height": 224,
|
| 21 |
+
"width": 224
|
| 22 |
+
}
|
| 23 |
+
}
|