SiemonCha commited on
Commit
3ce21dc
·
1 Parent(s): 5428478

fix DataParallel state dict loading

Browse files
Files changed (1) hide show
  1. src/models/inference.py +61 -7
src/models/inference.py CHANGED
@@ -4,26 +4,52 @@ from src.models.model import build_model
4
  from src.data.transforms import val_transforms
5
  from src.data.generator_loader import CLASS_NAMES
6
 
 
7
  MODEL_PATH = "saved_models/best_model.pth"
 
 
 
 
8
 
9
  def load_model(model_path=MODEL_PATH):
 
 
 
 
 
 
10
  model = build_model(pretrained=False)
11
  model.load_state_dict(torch.load(model_path, map_location="cpu"))
12
  model.eval()
13
  return model
14
 
 
15
  def predict(image_path: str, model=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  if model is None:
17
  model = load_model()
18
 
19
  image = Image.open(image_path).convert("RGB")
20
- tensor = val_transforms(image).unsqueeze(0)
21
 
22
- with torch.no_grad():
23
  output = model(tensor)
24
- prob = torch.sigmoid(output).item()
25
 
26
  label = "AI-Generated" if prob >= 0.5 else "Real"
 
27
  confidence = prob if prob >= 0.5 else 1 - prob
28
 
29
  return {
@@ -32,16 +58,43 @@ def predict(image_path: str, model=None):
32
  "raw_score": round(prob, 4)
33
  }
34
 
35
- GENERATOR_MODEL_PATH = "saved_models/generator_model.pth"
 
36
 
37
  def load_generator_model(model_path=GENERATOR_MODEL_PATH):
 
 
 
 
 
38
  from src.models.train_generator import build_multiclass_model
39
  model = build_multiclass_model(num_classes=4, pretrained=False)
40
- model.load_state_dict(torch.load(model_path, map_location="cpu"))
 
 
 
 
 
 
 
41
  model.eval()
42
  return model
43
 
 
44
  def predict_generator(image_path: str, model=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  if model is None:
46
  model = load_generator_model()
47
 
@@ -50,13 +103,14 @@ def predict_generator(image_path: str, model=None):
50
 
51
  with torch.no_grad():
52
  output = model(tensor)
53
- probs = torch.softmax(output, dim=1)[0]
54
- pred_class = probs.argmax().item()
55
  confidence = probs[pred_class].item()
56
 
57
  return {
58
  "generator_type": CLASS_NAMES[pred_class],
59
  "confidence": round(confidence * 100, 2),
 
60
  "class_probabilities": {
61
  CLASS_NAMES[i]: round(probs[i].item() * 100, 2)
62
  for i in range(4)
 
4
  from src.data.transforms import val_transforms
5
  from src.data.generator_loader import CLASS_NAMES
6
 
7
+ # Default paths for saved model checkpoints
8
  MODEL_PATH = "saved_models/best_model.pth"
9
+ GENERATOR_MODEL_PATH = "saved_models/generator_model.pth"
10
+
11
+
12
+ # --- Binary Classifier (Real vs Fake) ---
13
 
14
  def load_model(model_path=MODEL_PATH):
15
+ """
16
+ Loads the binary classifier (ResNet18) from a saved checkpoint.
17
+ pretrained=False because we're loading our own trained weights, not ImageNet.
18
+ map_location="cpu" ensures the model loads on any machine regardless of GPU availability.
19
+ model.eval() disables dropout for deterministic inference.
20
+ """
21
  model = build_model(pretrained=False)
22
  model.load_state_dict(torch.load(model_path, map_location="cpu"))
23
  model.eval()
24
  return model
25
 
26
+
27
  def predict(image_path: str, model=None):
28
+ """
29
+ Predicts whether an image is real or AI-generated.
30
+
31
+ Flow:
32
+ 1. Load image and apply val_transforms (resize to 224x224, normalize)
33
+ 2. unsqueeze(0) adds batch dimension: [3, 224, 224] -> [1, 3, 224, 224]
34
+ 3. Forward pass returns raw logit (unbounded number)
35
+ 4. sigmoid converts logit to probability (0.0 to 1.0)
36
+ 5. prob >= 0.5 means AI-Generated, else Real
37
+ 6. Confidence = how far from 0.5 the probability is
38
+
39
+ Returns dict with label, confidence percentage, and raw sigmoid score.
40
+ """
41
  if model is None:
42
  model = load_model()
43
 
44
  image = Image.open(image_path).convert("RGB")
45
+ tensor = val_transforms(image).unsqueeze(0) # add batch dimension
46
 
47
+ with torch.no_grad(): # disable gradient tracking for inference
48
  output = model(tensor)
49
+ prob = torch.sigmoid(output).item() # convert logit to probability
50
 
51
  label = "AI-Generated" if prob >= 0.5 else "Real"
52
+ # Confidence = distance from decision boundary (0.5)
53
  confidence = prob if prob >= 0.5 else 1 - prob
54
 
55
  return {
 
58
  "raw_score": round(prob, 4)
59
  }
60
 
61
+
62
+ # --- Generator Type Classifier (4-class) ---
63
 
64
  def load_generator_model(model_path=GENERATOR_MODEL_PATH):
65
+ """
66
+ Loads the 4-class generator type classifier from a saved checkpoint.
67
+ Classes: Real, GAN, Diffusion, Other (defined in generator_loader.CLASS_NAMES)
68
+ Handles DataParallel prefix (module.) if model was trained with multiple GPUs.
69
+ """
70
  from src.models.train_generator import build_multiclass_model
71
  model = build_multiclass_model(num_classes=4, pretrained=False)
72
+
73
+ state_dict = torch.load(model_path, map_location="cpu")
74
+
75
+ # Remove 'module.' prefix added by DataParallel when training on multiple GPUs
76
+ if any(k.startswith("module.") for k in state_dict.keys()):
77
+ state_dict = {k.replace("module.", ""): v for k, v in state_dict.items()}
78
+
79
+ model.load_state_dict(state_dict)
80
  model.eval()
81
  return model
82
 
83
+
84
  def predict_generator(image_path: str, model=None):
85
+ """
86
+ Predicts the generator type of an image (Real, GAN, Diffusion, Other).
87
+
88
+ Flow:
89
+ 1. Same preprocessing as binary classifier
90
+ 2. Forward pass returns 4 raw logits (one per class)
91
+ 3. softmax converts logits to probabilities summing to 1.0
92
+ 4. argmax picks the class with highest probability
93
+ 5. Returns predicted class, confidence, and all class probabilities
94
+
95
+ Unlike binary classifier which uses sigmoid (single output),
96
+ multi-class uses softmax (4 outputs) so probabilities sum to 100%.
97
+ """
98
  if model is None:
99
  model = load_generator_model()
100
 
 
103
 
104
  with torch.no_grad():
105
  output = model(tensor)
106
+ probs = torch.softmax(output, dim=1)[0] # convert logits to probabilities
107
+ pred_class = probs.argmax().item() # index of highest probability class
108
  confidence = probs[pred_class].item()
109
 
110
  return {
111
  "generator_type": CLASS_NAMES[pred_class],
112
  "confidence": round(confidence * 100, 2),
113
+ # All 4 class probabilities for display in UI
114
  "class_probabilities": {
115
  CLASS_NAMES[i]: round(probs[i].item() * 100, 2)
116
  for i in range(4)