Spaces:
Sleeping
Sleeping
Upload 29 files
Browse files- Models_Conversion/base_fusion_model.py +109 -0
- Models_Conversion/base_model_resnet.py +61 -0
- Models_Conversion/base_model_yolo.py +22 -0
- Models_Conversion/base_vs_onnx_model_evaluation.ipynb +532 -0
- Models_Conversion/model_onnx_conversion.ipynb +1171 -0
- Notebooks/EfficientNet_ConvNext_Fusion.ipynb +0 -0
- Notebooks/Resnet18_fine_tuning_final.ipynb +0 -0
- Notebooks/damage_detector_yolo.ipynb +0 -0
- src/config.py +53 -0
- src/data/augmentation.py +90 -0
- src/data/dataset.py +189 -0
- src/data/ingestion.py +55 -0
- src/data/preprocessing.py +58 -0
- src/models/fusion_model.py +112 -0
- src/models/resnet_model.py +64 -0
- src/training/train_fusion.py +92 -0
- src/training/train_resnet.py +68 -0
- src/training/train_yolo.py +85 -0
- src/training/trainer.py +305 -0
- test/test_augmentation.py +40 -0
- test/test_config.py +37 -0
- test/test_dataset.py +57 -0
- test/test_fusion_model.py +42 -0
- test/test_ingestion.py +32 -0
- test/test_preprocessing.py +37 -0
- test/test_resnet_model.py +38 -0
- test/test_train_fusion.py +39 -0
- test/test_train_resnet.py +39 -0
- test/test_train_yolo.py +36 -0
Models_Conversion/base_fusion_model.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torchvision.models as models
|
| 4 |
+
from transformers import ConvNextModel
|
| 5 |
+
from huggingface_hub import hf_hub_download
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# ==========================================
|
| 9 |
+
# 1. MODEL ARCHITECTURE DEFINITION
|
| 10 |
+
# ==========================================
|
| 11 |
+
class FusionClassifier(nn.Module):
|
| 12 |
+
def __init__(self, num_classes, convnext_model_name="facebook/convnext-small-224"):
|
| 13 |
+
super().__init__()
|
| 14 |
+
|
| 15 |
+
# EfficientNet-V2-S backbone
|
| 16 |
+
eff = models.efficientnet_v2_s(weights=models.EfficientNet_V2_S_Weights.IMAGENET1K_V1)
|
| 17 |
+
|
| 18 |
+
for param in eff.parameters():
|
| 19 |
+
param.requires_grad = False
|
| 20 |
+
|
| 21 |
+
for param in eff.features[5].parameters():
|
| 22 |
+
param.requires_grad = True
|
| 23 |
+
for param in eff.features[6].parameters():
|
| 24 |
+
param.requires_grad = True
|
| 25 |
+
for param in eff.features[7].parameters():
|
| 26 |
+
param.requires_grad = True
|
| 27 |
+
|
| 28 |
+
self.eff_features = eff.features
|
| 29 |
+
self.eff_avgpool = eff.avgpool
|
| 30 |
+
self.eff_out_dim = eff.classifier[1].in_features
|
| 31 |
+
|
| 32 |
+
# ConvNeXt-Small backbone
|
| 33 |
+
cnx = ConvNextModel.from_pretrained(convnext_model_name)
|
| 34 |
+
|
| 35 |
+
for param in cnx.parameters():
|
| 36 |
+
param.requires_grad = False
|
| 37 |
+
|
| 38 |
+
for param in cnx.encoder.stages[2].parameters():
|
| 39 |
+
param.requires_grad = True
|
| 40 |
+
for param in cnx.encoder.stages[3].parameters():
|
| 41 |
+
param.requires_grad = True
|
| 42 |
+
for param in cnx.layernorm.parameters():
|
| 43 |
+
param.requires_grad = True
|
| 44 |
+
|
| 45 |
+
self.cnx_backbone = cnx
|
| 46 |
+
self.cnx_out_dim = 768
|
| 47 |
+
|
| 48 |
+
# Fusion head
|
| 49 |
+
fused_dim = self.eff_out_dim + self.cnx_out_dim
|
| 50 |
+
self.fusion_head = nn.Sequential(
|
| 51 |
+
nn.Dropout(p=0.4),
|
| 52 |
+
nn.Linear(fused_dim, 512),
|
| 53 |
+
nn.LayerNorm(512),
|
| 54 |
+
nn.GELU(),
|
| 55 |
+
nn.Dropout(p=0.3),
|
| 56 |
+
nn.Linear(512, 256),
|
| 57 |
+
nn.LayerNorm(256),
|
| 58 |
+
nn.GELU(),
|
| 59 |
+
nn.Dropout(p=0.2),
|
| 60 |
+
nn.Linear(256, num_classes),
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
def forward(self, pixel_values_eff, pixel_values_cnx):
|
| 64 |
+
x_eff = self.eff_features(pixel_values_eff)
|
| 65 |
+
x_eff = self.eff_avgpool(x_eff)
|
| 66 |
+
x_eff = torch.flatten(x_eff, 1)
|
| 67 |
+
|
| 68 |
+
cnx_out = self.cnx_backbone(pixel_values=pixel_values_cnx, return_dict=True)
|
| 69 |
+
x_cnx = cnx_out.pooler_output
|
| 70 |
+
|
| 71 |
+
fused = torch.cat([x_eff, x_cnx], dim=1)
|
| 72 |
+
logits = self.fusion_head(fused)
|
| 73 |
+
|
| 74 |
+
return logits
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# ==========================================
|
| 78 |
+
# 2. HUGGING FACE DOWNLOAD & LOAD FUNCTION
|
| 79 |
+
# ==========================================
|
| 80 |
+
def load_fusion_model_from_hf(
|
| 81 |
+
repo_id: str = "junaid17/best_fusion_model_fp16",
|
| 82 |
+
filename: str = "best_fusion_model_fp16.pt",
|
| 83 |
+
num_classes: int = 10, # Replace with your actual number of classes
|
| 84 |
+
device: str = "cpu",
|
| 85 |
+
) -> nn.Module:
|
| 86 |
+
"""Downloads weights from Hugging Face Hub and loads into FusionClassifier."""
|
| 87 |
+
print(f"Downloading checkpoint from Hugging Face Hub: '{repo_id}/{filename}'...")
|
| 88 |
+
|
| 89 |
+
# Download weights file from Hugging Face Hub
|
| 90 |
+
checkpoint_path = hf_hub_download(repo_id=repo_id, filename=filename)
|
| 91 |
+
|
| 92 |
+
# Instantiate Model
|
| 93 |
+
model = FusionClassifier(num_classes=num_classes)
|
| 94 |
+
|
| 95 |
+
# Load weights
|
| 96 |
+
checkpoint = torch.load(checkpoint_path, map_location=device)
|
| 97 |
+
if isinstance(checkpoint, dict) and "state_dict" in checkpoint:
|
| 98 |
+
model.load_state_dict(checkpoint["state_dict"])
|
| 99 |
+
elif isinstance(checkpoint, dict):
|
| 100 |
+
model.load_state_dict(checkpoint)
|
| 101 |
+
else:
|
| 102 |
+
model = checkpoint
|
| 103 |
+
|
| 104 |
+
# Ensure model is float32 for stable ONNX export
|
| 105 |
+
model = model.float().to(device)
|
| 106 |
+
model.eval()
|
| 107 |
+
|
| 108 |
+
print("✅ Model loaded successfully from Hugging Face.")
|
| 109 |
+
return model
|
Models_Conversion/base_model_resnet.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from torchvision import models
|
| 4 |
+
from huggingface_hub import hf_hub_download
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Car_Classifier_Resnet(nn.Module):
|
| 8 |
+
def __init__(self, num_classes):
|
| 9 |
+
super().__init__()
|
| 10 |
+
|
| 11 |
+
self.model = models.resnet18(weights="DEFAULT")
|
| 12 |
+
|
| 13 |
+
for param in self.model.parameters():
|
| 14 |
+
param.requires_grad = False
|
| 15 |
+
|
| 16 |
+
for param in self.model.layer3.parameters():
|
| 17 |
+
param.requires_grad = True
|
| 18 |
+
|
| 19 |
+
for param in self.model.layer4.parameters():
|
| 20 |
+
param.requires_grad = True
|
| 21 |
+
|
| 22 |
+
self.model.fc = nn.Sequential(
|
| 23 |
+
nn.Dropout(0.5),
|
| 24 |
+
nn.Linear(self.model.fc.in_features, 256),
|
| 25 |
+
nn.ReLU(),
|
| 26 |
+
nn.Dropout(0.3),
|
| 27 |
+
nn.Linear(256, num_classes)
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
def forward(self, x):
|
| 31 |
+
return self.model(x)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def load_resnet_model_from_hf(
|
| 35 |
+
repo_id: str,
|
| 36 |
+
filename: str = "car-damage-classifier.pt",
|
| 37 |
+
num_classes: int = 6,
|
| 38 |
+
device: str = "cpu",
|
| 39 |
+
hf_token: str = None,
|
| 40 |
+
) -> torch.nn.Module:
|
| 41 |
+
"""
|
| 42 |
+
Downloads and loads the state_dict for Car_Classifier_Resnet from Hugging Face Hub.
|
| 43 |
+
"""
|
| 44 |
+
print(f"Downloading checkpoint '{filename}' from Hugging Face repo '{repo_id}'...")
|
| 45 |
+
checkpoint_path = hf_hub_download(
|
| 46 |
+
repo_id=repo_id,
|
| 47 |
+
filename=filename,
|
| 48 |
+
token=hf_token
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
model = Car_Classifier_Resnet(num_classes=num_classes)
|
| 52 |
+
state_dict = torch.load(checkpoint_path, map_location=device)
|
| 53 |
+
|
| 54 |
+
if isinstance(state_dict, dict) and "state_dict" in state_dict:
|
| 55 |
+
state_dict = state_dict["state_dict"]
|
| 56 |
+
|
| 57 |
+
model.load_state_dict(state_dict)
|
| 58 |
+
model.to(device)
|
| 59 |
+
model.eval()
|
| 60 |
+
print("✅ ResNet-18 model loaded successfully.")
|
| 61 |
+
return model
|
Models_Conversion/base_model_yolo.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from ultralytics import YOLO
|
| 3 |
+
from huggingface_hub import hf_hub_download
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def load_yolo_model_from_hf(
|
| 7 |
+
repo_id: str = "junaid17/Yolo_Model",
|
| 8 |
+
filename: str = "damage_detector.pt",
|
| 9 |
+
hf_token: str = None,
|
| 10 |
+
) -> YOLO:
|
| 11 |
+
"""
|
| 12 |
+
Downloads and loads a trained Ultralytics YOLO model from Hugging Face Hub.
|
| 13 |
+
"""
|
| 14 |
+
print(f"Downloading YOLO checkpoint '{filename}' from Hugging Face repo '{repo_id}'...")
|
| 15 |
+
checkpoint_path = hf_hub_download(
|
| 16 |
+
repo_id=repo_id,
|
| 17 |
+
filename=filename,
|
| 18 |
+
token=hf_token
|
| 19 |
+
)
|
| 20 |
+
model = YOLO(checkpoint_path)
|
| 21 |
+
print("✅ YOLO model loaded successfully.")
|
| 22 |
+
return model
|
Models_Conversion/base_vs_onnx_model_evaluation.ipynb
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 3,
|
| 6 |
+
"id": "7c5a22e0",
|
| 7 |
+
"metadata": {},
|
| 8 |
+
"outputs": [],
|
| 9 |
+
"source": [
|
| 10 |
+
"import os\n",
|
| 11 |
+
"import torch\n",
|
| 12 |
+
"import numpy as np\n",
|
| 13 |
+
"import onnxruntime as ort\n",
|
| 14 |
+
"from tqdm import tqdm\n",
|
| 15 |
+
"from torch.utils.data import DataLoader, Dataset\n",
|
| 16 |
+
"from torchvision import transforms\n",
|
| 17 |
+
"from torchvision.datasets import ImageFolder\n",
|
| 18 |
+
"from transformers import ConvNextImageProcessor\n",
|
| 19 |
+
"from huggingface_hub import hf_hub_download\n",
|
| 20 |
+
"from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, classification_report\n",
|
| 21 |
+
"from base_fusion_model import FusionClassifier\n",
|
| 22 |
+
"from base_model_resnet import Car_Classifier_Resnet\n",
|
| 23 |
+
"\n",
|
| 24 |
+
"# ==========================================\n",
|
| 25 |
+
"# IMPORT YOUR BASE PYTORCH MODELS HERE\n",
|
| 26 |
+
"# Example: from my_models import FusionClassifier, Car_Classifier_Resnet\n",
|
| 27 |
+
"# ==========================================\n",
|
| 28 |
+
"\n",
|
| 29 |
+
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
|
| 30 |
+
"HF_TOKEN = os.getenv(\"HF_TOKEN\") # Ensure your Hugging Face token is set in environment\n",
|
| 31 |
+
"\n",
|
| 32 |
+
"# ==========================================\n",
|
| 33 |
+
"# 1. IMAGE PROCESSING & TRANSFORMS\n",
|
| 34 |
+
"# ==========================================\n",
|
| 35 |
+
"\n",
|
| 36 |
+
"# --- Fusion Model Transforms ---\n",
|
| 37 |
+
"IMG_SIZE = 260\n",
|
| 38 |
+
"convnext_model_name = \"facebook/convnext-small-224\"\n",
|
| 39 |
+
"processor = ConvNextImageProcessor.from_pretrained(convnext_model_name)\n",
|
| 40 |
+
"\n",
|
| 41 |
+
"fusion_val_transforms = transforms.Compose([\n",
|
| 42 |
+
" transforms.Resize((IMG_SIZE, IMG_SIZE)),\n",
|
| 43 |
+
"])\n",
|
| 44 |
+
"\n",
|
| 45 |
+
"eff_normalize = transforms.Compose([\n",
|
| 46 |
+
" transforms.ToTensor(),\n",
|
| 47 |
+
" transforms.Normalize([0.485, 0.456, 0.406],\n",
|
| 48 |
+
" [0.229, 0.224, 0.225])\n",
|
| 49 |
+
"])\n",
|
| 50 |
+
"\n",
|
| 51 |
+
"# --- ResNet Model Transforms ---\n",
|
| 52 |
+
"resnet_test_transforms = transforms.Compose([\n",
|
| 53 |
+
" transforms.Resize((224, 224)),\n",
|
| 54 |
+
" transforms.ToTensor(),\n",
|
| 55 |
+
" transforms.Normalize([0.485, 0.456, 0.406],\n",
|
| 56 |
+
" [0.229, 0.224, 0.225])\n",
|
| 57 |
+
"])\n",
|
| 58 |
+
"\n",
|
| 59 |
+
"\n",
|
| 60 |
+
"# ==========================================\n",
|
| 61 |
+
"# 2. DATASETS\n",
|
| 62 |
+
"# ==========================================\n",
|
| 63 |
+
"\n",
|
| 64 |
+
"class FusionDataset(Dataset):\n",
|
| 65 |
+
" def __init__(self, data_dir, processor, transform=None):\n",
|
| 66 |
+
" self.dataset = ImageFolder(root=data_dir, transform=transform)\n",
|
| 67 |
+
" self.processor = processor\n",
|
| 68 |
+
" self.classes = self.dataset.classes\n",
|
| 69 |
+
"\n",
|
| 70 |
+
" def __len__(self):\n",
|
| 71 |
+
" return len(self.dataset)\n",
|
| 72 |
+
"\n",
|
| 73 |
+
" def __getitem__(self, idx):\n",
|
| 74 |
+
" pil_image, label = self.dataset[idx]\n",
|
| 75 |
+
"\n",
|
| 76 |
+
" # EfficientNet branch\n",
|
| 77 |
+
" pixel_eff = eff_normalize(pil_image)\n",
|
| 78 |
+
"\n",
|
| 79 |
+
" # ConvNeXt branch\n",
|
| 80 |
+
" enc = self.processor(images=pil_image, return_tensors='pt')\n",
|
| 81 |
+
" pixel_cnx = enc['pixel_values'].squeeze(0)\n",
|
| 82 |
+
"\n",
|
| 83 |
+
" return {\n",
|
| 84 |
+
" 'pixel_values_eff': pixel_eff,\n",
|
| 85 |
+
" 'pixel_values_cnx': pixel_cnx,\n",
|
| 86 |
+
" 'labels': torch.tensor(label)\n",
|
| 87 |
+
" }\n",
|
| 88 |
+
"\n",
|
| 89 |
+
"class ResNetDataset(Dataset):\n",
|
| 90 |
+
" def __init__(self, data_dir, transform=None):\n",
|
| 91 |
+
" self.dataset = ImageFolder(root=data_dir, transform=transform)\n",
|
| 92 |
+
" self.classes = self.dataset.classes\n",
|
| 93 |
+
"\n",
|
| 94 |
+
" def __len__(self):\n",
|
| 95 |
+
" return len(self.dataset)\n",
|
| 96 |
+
"\n",
|
| 97 |
+
" def __getitem__(self, idx):\n",
|
| 98 |
+
" return self.dataset[idx]\n",
|
| 99 |
+
"\n",
|
| 100 |
+
"\n",
|
| 101 |
+
"# ==========================================\n",
|
| 102 |
+
"# 3. HUGGING FACE MODEL LOADER\n",
|
| 103 |
+
"# ==========================================\n",
|
| 104 |
+
"def load_models_from_hf(repo_id, pt_filename, onnx_filename, model_instance):\n",
|
| 105 |
+
" \"\"\"\n",
|
| 106 |
+
" Downloads PyTorch and ONNX models from HF and loads PyTorch weights into the base instance.\n",
|
| 107 |
+
" \"\"\"\n",
|
| 108 |
+
" print(f\"\\n⬇️ Downloading models from Hugging Face: {repo_id}\")\n",
|
| 109 |
+
" \n",
|
| 110 |
+
" # 1. Download and load PyTorch weights\n",
|
| 111 |
+
" pt_path = hf_hub_download(repo_id=repo_id, filename=pt_filename, token=HF_TOKEN)\n",
|
| 112 |
+
" checkpoint = torch.load(pt_path, map_location=device)\n",
|
| 113 |
+
" \n",
|
| 114 |
+
" # Handle both full checkpoints dicts and direct state_dicts\n",
|
| 115 |
+
" if 'model_state_dict' in checkpoint:\n",
|
| 116 |
+
" model_instance.load_state_dict(checkpoint['model_state_dict'])\n",
|
| 117 |
+
" print(f\"✅ PyTorch model loaded (Val Acc: {checkpoint.get('val_acc', 'N/A')}%)\")\n",
|
| 118 |
+
" else:\n",
|
| 119 |
+
" model_instance.load_state_dict(checkpoint)\n",
|
| 120 |
+
" print(\"✅ PyTorch model loaded\")\n",
|
| 121 |
+
" \n",
|
| 122 |
+
" model_instance.to(device)\n",
|
| 123 |
+
" model_instance.eval()\n",
|
| 124 |
+
"\n",
|
| 125 |
+
" # 2. Download ONNX model\n",
|
| 126 |
+
" onnx_path = hf_hub_download(repo_id=repo_id, filename=onnx_filename, token=HF_TOKEN)\n",
|
| 127 |
+
" print(\"✅ ONNX model downloaded\")\n",
|
| 128 |
+
" \n",
|
| 129 |
+
" return model_instance, onnx_path\n",
|
| 130 |
+
"\n",
|
| 131 |
+
"\n",
|
| 132 |
+
"# ==========================================\n",
|
| 133 |
+
"# 4. EVALUATION ENGINE\n",
|
| 134 |
+
"# ==========================================\n",
|
| 135 |
+
"def evaluate_models(model_type, torch_model, onnx_model_path, dataloader, target_names):\n",
|
| 136 |
+
" \"\"\"\n",
|
| 137 |
+
" Runs evaluation for both PyTorch and ONNX models side-by-side.\n",
|
| 138 |
+
" model_type must be either 'fusion' or 'resnet'.\n",
|
| 139 |
+
" \"\"\"\n",
|
| 140 |
+
" providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']\n",
|
| 141 |
+
" ort_session = ort.InferenceSession(onnx_model_path, providers=providers)\n",
|
| 142 |
+
" \n",
|
| 143 |
+
" all_labels = []\n",
|
| 144 |
+
" torch_preds = []\n",
|
| 145 |
+
" onnx_preds = []\n",
|
| 146 |
+
"\n",
|
| 147 |
+
" print(f\"\\n🚀 Starting Evaluation for {model_type.upper()} Model...\")\n",
|
| 148 |
+
" \n",
|
| 149 |
+
" with torch.no_grad():\n",
|
| 150 |
+
" for batch in tqdm(dataloader, desc=\"Evaluating\"):\n",
|
| 151 |
+
" if model_type == 'fusion':\n",
|
| 152 |
+
" images_eff = batch['pixel_values_eff'].to(device)\n",
|
| 153 |
+
" images_cnx = batch['pixel_values_cnx'].to(device)\n",
|
| 154 |
+
" labels = batch['labels'].numpy()\n",
|
| 155 |
+
" \n",
|
| 156 |
+
" # PyTorch Inference\n",
|
| 157 |
+
" logits_torch = torch_model(images_eff, images_cnx)\n",
|
| 158 |
+
" \n",
|
| 159 |
+
" # ONNX Inference (Requires inputs matching the exported graph)\n",
|
| 160 |
+
" onnx_inputs = {\n",
|
| 161 |
+
" ort_session.get_inputs()[0].name: images_eff.cpu().numpy(),\n",
|
| 162 |
+
" ort_session.get_inputs()[1].name: images_cnx.cpu().numpy()\n",
|
| 163 |
+
" }\n",
|
| 164 |
+
" logits_onnx = ort_session.run(None, onnx_inputs)[0]\n",
|
| 165 |
+
"\n",
|
| 166 |
+
" elif model_type == 'resnet':\n",
|
| 167 |
+
" images, labels_tensor = batch\n",
|
| 168 |
+
" images = images.to(device)\n",
|
| 169 |
+
" labels = labels_tensor.numpy()\n",
|
| 170 |
+
" \n",
|
| 171 |
+
" # PyTorch Inference\n",
|
| 172 |
+
" logits_torch = torch_model(images)\n",
|
| 173 |
+
" \n",
|
| 174 |
+
" # ONNX Inference\n",
|
| 175 |
+
" onnx_inputs = {ort_session.get_inputs()[0].name: images.cpu().numpy()}\n",
|
| 176 |
+
" logits_onnx = ort_session.run(None, onnx_inputs)[0]\n",
|
| 177 |
+
"\n",
|
| 178 |
+
" # Collect Predictions\n",
|
| 179 |
+
" preds_t = torch.argmax(logits_torch, dim=1).cpu().numpy()\n",
|
| 180 |
+
" preds_o = np.argmax(logits_onnx, axis=1)\n",
|
| 181 |
+
"\n",
|
| 182 |
+
" all_labels.extend(labels)\n",
|
| 183 |
+
" torch_preds.extend(preds_t)\n",
|
| 184 |
+
" onnx_preds.extend(preds_o)\n",
|
| 185 |
+
"\n",
|
| 186 |
+
" # Helper function to print detailed metrics\n",
|
| 187 |
+
" def print_metrics(y_true, y_pred, title):\n",
|
| 188 |
+
" acc = accuracy_score(y_true, y_pred)\n",
|
| 189 |
+
" prec = precision_score(y_true, y_pred, average='macro', zero_division=0)\n",
|
| 190 |
+
" rec = recall_score(y_true, y_pred, average='macro', zero_division=0)\n",
|
| 191 |
+
" f1 = f1_score(y_true, y_pred, average='macro', zero_division=0)\n",
|
| 192 |
+
"\n",
|
| 193 |
+
" print(f\"\\n{'='*50}\")\n",
|
| 194 |
+
" print(f\" {title.center(48)} \")\n",
|
| 195 |
+
" print(f\"{'='*50}\")\n",
|
| 196 |
+
" print(f\"Accuracy : {acc:.4f}\")\n",
|
| 197 |
+
" print(f\"Precision : {prec:.4f}\")\n",
|
| 198 |
+
" print(f\"Recall : {rec:.4f}\")\n",
|
| 199 |
+
" print(f\"F1 Score : {f1:.4f}\")\n",
|
| 200 |
+
" print(\"-\" * 50)\n",
|
| 201 |
+
" print(\"Classification Report:\")\n",
|
| 202 |
+
" print(classification_report(y_true, y_pred, target_names=target_names, zero_division=0))\n",
|
| 203 |
+
"\n",
|
| 204 |
+
" print_metrics(all_labels, torch_preds, f\"{model_type.upper()} - PYTORCH MODEL\")\n",
|
| 205 |
+
" print_metrics(all_labels, onnx_preds, f\"{model_type.upper()} - ONNX MODEL\")"
|
| 206 |
+
]
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
"cell_type": "code",
|
| 210 |
+
"execution_count": 5,
|
| 211 |
+
"id": "375a4992",
|
| 212 |
+
"metadata": {},
|
| 213 |
+
"outputs": [
|
| 214 |
+
{
|
| 215 |
+
"data": {
|
| 216 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 217 |
+
"model_id": "c428a2fa573042d590a7663a583b9c97",
|
| 218 |
+
"version_major": 2,
|
| 219 |
+
"version_minor": 0
|
| 220 |
+
},
|
| 221 |
+
"text/plain": [
|
| 222 |
+
"Loading weights: 0%| | 0/342 [00:00<?, ?it/s]"
|
| 223 |
+
]
|
| 224 |
+
},
|
| 225 |
+
"metadata": {},
|
| 226 |
+
"output_type": "display_data"
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
"name": "stderr",
|
| 230 |
+
"output_type": "stream",
|
| 231 |
+
"text": [
|
| 232 |
+
"[transformers] \u001b[1mConvNextModel LOAD REPORT\u001b[0m from: facebook/convnext-small-224\n",
|
| 233 |
+
"Key | Status | | \n",
|
| 234 |
+
"------------------+------------+--+-\n",
|
| 235 |
+
"classifier.bias | UNEXPECTED | | \n",
|
| 236 |
+
"classifier.weight | UNEXPECTED | | \n",
|
| 237 |
+
"\n",
|
| 238 |
+
"Notes:\n",
|
| 239 |
+
"- UNEXPECTED:\tcan be ignored when loading from different task/architecture; not ok if you expect identical arch.\n"
|
| 240 |
+
]
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"name": "stdout",
|
| 244 |
+
"output_type": "stream",
|
| 245 |
+
"text": [
|
| 246 |
+
"\n",
|
| 247 |
+
"⬇️ Downloading models from Hugging Face: junaid17/best_fusion_model_fp16\n",
|
| 248 |
+
"✅ PyTorch model loaded\n",
|
| 249 |
+
"✅ ONNX model downloaded\n"
|
| 250 |
+
]
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"name": "stderr",
|
| 254 |
+
"output_type": "stream",
|
| 255 |
+
"text": [
|
| 256 |
+
"d:\\DamageLensAI\\myvenv\\Lib\\site-packages\\onnxruntime\\capi\\onnxruntime_inference_collection.py:153: UserWarning: Specified provider 'CUDAExecutionProvider' is not in available provider names.Available providers: 'AzureExecutionProvider, CPUExecutionProvider'\n",
|
| 257 |
+
" warnings.warn(\n"
|
| 258 |
+
]
|
| 259 |
+
},
|
| 260 |
+
{
|
| 261 |
+
"name": "stdout",
|
| 262 |
+
"output_type": "stream",
|
| 263 |
+
"text": [
|
| 264 |
+
"\n",
|
| 265 |
+
"🚀 Starting Evaluation for FUSION Model...\n"
|
| 266 |
+
]
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"name": "stderr",
|
| 270 |
+
"output_type": "stream",
|
| 271 |
+
"text": [
|
| 272 |
+
"Evaluating: 100%|██████████| 29/29 [04:30<00:00, 9.31s/it]\n"
|
| 273 |
+
]
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"name": "stdout",
|
| 277 |
+
"output_type": "stream",
|
| 278 |
+
"text": [
|
| 279 |
+
"\n",
|
| 280 |
+
"==================================================\n",
|
| 281 |
+
" FUSION - PYTORCH MODEL \n",
|
| 282 |
+
"==================================================\n",
|
| 283 |
+
"Accuracy : 0.8413\n",
|
| 284 |
+
"Precision : 0.8364\n",
|
| 285 |
+
"Recall : 0.8342\n",
|
| 286 |
+
"F1 Score : 0.8348\n",
|
| 287 |
+
"--------------------------------------------------\n",
|
| 288 |
+
"Classification Report:\n",
|
| 289 |
+
" precision recall f1-score support\n",
|
| 290 |
+
"\n",
|
| 291 |
+
" F_Breakage 0.86 0.86 0.86 100\n",
|
| 292 |
+
" F_Crushed 0.78 0.78 0.78 80\n",
|
| 293 |
+
" F_Normal 0.91 0.92 0.92 100\n",
|
| 294 |
+
" R_Breakage 0.83 0.80 0.81 60\n",
|
| 295 |
+
" R_Crushed 0.74 0.82 0.78 60\n",
|
| 296 |
+
" R_Normal 0.89 0.83 0.86 60\n",
|
| 297 |
+
"\n",
|
| 298 |
+
" accuracy 0.84 460\n",
|
| 299 |
+
" macro avg 0.84 0.83 0.83 460\n",
|
| 300 |
+
"weighted avg 0.84 0.84 0.84 460\n",
|
| 301 |
+
"\n",
|
| 302 |
+
"\n",
|
| 303 |
+
"==================================================\n",
|
| 304 |
+
" FUSION - ONNX MODEL \n",
|
| 305 |
+
"==================================================\n",
|
| 306 |
+
"Accuracy : 0.8413\n",
|
| 307 |
+
"Precision : 0.8364\n",
|
| 308 |
+
"Recall : 0.8342\n",
|
| 309 |
+
"F1 Score : 0.8348\n",
|
| 310 |
+
"--------------------------------------------------\n",
|
| 311 |
+
"Classification Report:\n",
|
| 312 |
+
" precision recall f1-score support\n",
|
| 313 |
+
"\n",
|
| 314 |
+
" F_Breakage 0.86 0.86 0.86 100\n",
|
| 315 |
+
" F_Crushed 0.78 0.78 0.78 80\n",
|
| 316 |
+
" F_Normal 0.91 0.92 0.92 100\n",
|
| 317 |
+
" R_Breakage 0.83 0.80 0.81 60\n",
|
| 318 |
+
" R_Crushed 0.74 0.82 0.78 60\n",
|
| 319 |
+
" R_Normal 0.89 0.83 0.86 60\n",
|
| 320 |
+
"\n",
|
| 321 |
+
" accuracy 0.84 460\n",
|
| 322 |
+
" macro avg 0.84 0.83 0.83 460\n",
|
| 323 |
+
"weighted avg 0.84 0.84 0.84 460\n",
|
| 324 |
+
"\n",
|
| 325 |
+
"\n",
|
| 326 |
+
"⬇️ Downloading models from Hugging Face: junaid17/car-damage-classifier\n"
|
| 327 |
+
]
|
| 328 |
+
},
|
| 329 |
+
{
|
| 330 |
+
"data": {
|
| 331 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 332 |
+
"model_id": "477e596952ee4da99a21c89acb64e70e",
|
| 333 |
+
"version_major": 2,
|
| 334 |
+
"version_minor": 0
|
| 335 |
+
},
|
| 336 |
+
"text/plain": [
|
| 337 |
+
"car-damage-classifier.pt: reconstructing file: 0%| | 0.00B / 45.3MB "
|
| 338 |
+
]
|
| 339 |
+
},
|
| 340 |
+
"metadata": {},
|
| 341 |
+
"output_type": "display_data"
|
| 342 |
+
},
|
| 343 |
+
{
|
| 344 |
+
"data": {
|
| 345 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 346 |
+
"model_id": "eb1e5cde28db4feb9c1c4fba02eaef64",
|
| 347 |
+
"version_major": 2,
|
| 348 |
+
"version_minor": 0
|
| 349 |
+
},
|
| 350 |
+
"text/plain": [
|
| 351 |
+
"car-damage-classifier.pt: downloading bytes: | 0.00B "
|
| 352 |
+
]
|
| 353 |
+
},
|
| 354 |
+
"metadata": {},
|
| 355 |
+
"output_type": "display_data"
|
| 356 |
+
},
|
| 357 |
+
{
|
| 358 |
+
"name": "stderr",
|
| 359 |
+
"output_type": "stream",
|
| 360 |
+
"text": [
|
| 361 |
+
"d:\\DamageLensAI\\myvenv\\Lib\\site-packages\\huggingface_hub\\file_download.py:141: UserWarning: `huggingface_hub` cache-system uses symlinks by default to efficiently store duplicated files but your machine does not support them in C:\\Users\\junai\\.cache\\huggingface\\hub\\models--junaid17--car-damage-classifier. Caching files will still work but in a degraded version that might require more space on your disk. This warning can be disabled by setting the `HF_HUB_DISABLE_SYMLINKS_WARNING` environment variable. For more details, see https://huggingface.co/docs/huggingface_hub/how-to-cache#limitations.\n",
|
| 362 |
+
"To support symlinks on Windows, you either need to activate Developer Mode or to run Python as an administrator. In order to activate developer mode, see this article: https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development\n",
|
| 363 |
+
" warnings.warn(message)\n"
|
| 364 |
+
]
|
| 365 |
+
},
|
| 366 |
+
{
|
| 367 |
+
"name": "stdout",
|
| 368 |
+
"output_type": "stream",
|
| 369 |
+
"text": [
|
| 370 |
+
"✅ PyTorch model loaded\n",
|
| 371 |
+
"✅ ONNX model downloaded\n"
|
| 372 |
+
]
|
| 373 |
+
},
|
| 374 |
+
{
|
| 375 |
+
"name": "stderr",
|
| 376 |
+
"output_type": "stream",
|
| 377 |
+
"text": [
|
| 378 |
+
"d:\\DamageLensAI\\myvenv\\Lib\\site-packages\\onnxruntime\\capi\\onnxruntime_inference_collection.py:153: UserWarning: Specified provider 'CUDAExecutionProvider' is not in available provider names.Available providers: 'AzureExecutionProvider, CPUExecutionProvider'\n",
|
| 379 |
+
" warnings.warn(\n"
|
| 380 |
+
]
|
| 381 |
+
},
|
| 382 |
+
{
|
| 383 |
+
"name": "stdout",
|
| 384 |
+
"output_type": "stream",
|
| 385 |
+
"text": [
|
| 386 |
+
"\n",
|
| 387 |
+
"🚀 Starting Evaluation for RESNET Model...\n"
|
| 388 |
+
]
|
| 389 |
+
},
|
| 390 |
+
{
|
| 391 |
+
"name": "stderr",
|
| 392 |
+
"output_type": "stream",
|
| 393 |
+
"text": [
|
| 394 |
+
"Evaluating: 100%|██████████| 29/29 [00:35<00:00, 1.21s/it]"
|
| 395 |
+
]
|
| 396 |
+
},
|
| 397 |
+
{
|
| 398 |
+
"name": "stdout",
|
| 399 |
+
"output_type": "stream",
|
| 400 |
+
"text": [
|
| 401 |
+
"\n",
|
| 402 |
+
"==================================================\n",
|
| 403 |
+
" RESNET - PYTORCH MODEL \n",
|
| 404 |
+
"==================================================\n",
|
| 405 |
+
"Accuracy : 0.7739\n",
|
| 406 |
+
"Precision : 0.7734\n",
|
| 407 |
+
"Recall : 0.7661\n",
|
| 408 |
+
"F1 Score : 0.7681\n",
|
| 409 |
+
"--------------------------------------------------\n",
|
| 410 |
+
"Classification Report:\n",
|
| 411 |
+
" precision recall f1-score support\n",
|
| 412 |
+
"\n",
|
| 413 |
+
" F_Breakage 0.83 0.78 0.80 100\n",
|
| 414 |
+
" F_Crushed 0.63 0.75 0.69 80\n",
|
| 415 |
+
" F_Normal 0.89 0.85 0.87 100\n",
|
| 416 |
+
" R_Breakage 0.81 0.72 0.76 60\n",
|
| 417 |
+
" R_Crushed 0.67 0.72 0.69 60\n",
|
| 418 |
+
" R_Normal 0.81 0.78 0.80 60\n",
|
| 419 |
+
"\n",
|
| 420 |
+
" accuracy 0.77 460\n",
|
| 421 |
+
" macro avg 0.77 0.77 0.77 460\n",
|
| 422 |
+
"weighted avg 0.78 0.77 0.78 460\n",
|
| 423 |
+
"\n",
|
| 424 |
+
"\n",
|
| 425 |
+
"==================================================\n",
|
| 426 |
+
" RESNET - ONNX MODEL \n",
|
| 427 |
+
"==================================================\n",
|
| 428 |
+
"Accuracy : 0.7739\n",
|
| 429 |
+
"Precision : 0.7734\n",
|
| 430 |
+
"Recall : 0.7661\n",
|
| 431 |
+
"F1 Score : 0.7681\n",
|
| 432 |
+
"--------------------------------------------------\n",
|
| 433 |
+
"Classification Report:\n",
|
| 434 |
+
" precision recall f1-score support\n",
|
| 435 |
+
"\n",
|
| 436 |
+
" F_Breakage 0.83 0.78 0.80 100\n",
|
| 437 |
+
" F_Crushed 0.63 0.75 0.69 80\n",
|
| 438 |
+
" F_Normal 0.89 0.85 0.87 100\n",
|
| 439 |
+
" R_Breakage 0.81 0.72 0.76 60\n",
|
| 440 |
+
" R_Crushed 0.67 0.72 0.69 60\n",
|
| 441 |
+
" R_Normal 0.81 0.78 0.80 60\n",
|
| 442 |
+
"\n",
|
| 443 |
+
" accuracy 0.77 460\n",
|
| 444 |
+
" macro avg 0.77 0.77 0.77 460\n",
|
| 445 |
+
"weighted avg 0.78 0.77 0.78 460\n",
|
| 446 |
+
"\n"
|
| 447 |
+
]
|
| 448 |
+
},
|
| 449 |
+
{
|
| 450 |
+
"name": "stderr",
|
| 451 |
+
"output_type": "stream",
|
| 452 |
+
"text": [
|
| 453 |
+
"\n"
|
| 454 |
+
]
|
| 455 |
+
}
|
| 456 |
+
],
|
| 457 |
+
"source": [
|
| 458 |
+
"if __name__ == \"__main__\":\n",
|
| 459 |
+
" eval_dir = \"D:\\\\DamageLensAI\\\\val\" # Adjust to your evaluation folder\n",
|
| 460 |
+
"\n",
|
| 461 |
+
" # ---------------------------------------------------------\n",
|
| 462 |
+
" # EVALUATE FUSION MODEL\n",
|
| 463 |
+
" # ---------------------------------------------------------\n",
|
| 464 |
+
" fusion_dataset = FusionDataset(data_dir=eval_dir, processor=processor, transform=fusion_val_transforms)\n",
|
| 465 |
+
" fusion_loader = DataLoader(fusion_dataset, batch_size=16, shuffle=False)\n",
|
| 466 |
+
" \n",
|
| 467 |
+
" # Initialize your base Fusion PyTorch architecture\n",
|
| 468 |
+
" # Replace 'FusionClassifier' with your actual class name and arguments\n",
|
| 469 |
+
" base_fusion = FusionClassifier(num_classes=len(fusion_dataset.classes), convnext_model_name=convnext_model_name)\n",
|
| 470 |
+
" \n",
|
| 471 |
+
" # Load weights and get ONNX path from Hub\n",
|
| 472 |
+
" torch_fusion, onnx_fusion_path = load_models_from_hf(\n",
|
| 473 |
+
" repo_id=\"junaid17/best_fusion_model_fp16\",\n",
|
| 474 |
+
" pt_filename=\"best_fusion_model_fp16.pt\", # Update if named differently on HF\n",
|
| 475 |
+
" onnx_filename=\"fusion_model.onnx\",\n",
|
| 476 |
+
" model_instance=base_fusion\n",
|
| 477 |
+
" )\n",
|
| 478 |
+
" \n",
|
| 479 |
+
" evaluate_models('fusion', torch_fusion, onnx_fusion_path, fusion_loader, fusion_dataset.classes)\n",
|
| 480 |
+
"\n",
|
| 481 |
+
" # ---------------------------------------------------------\n",
|
| 482 |
+
" # EVALUATE RESNET MODEL\n",
|
| 483 |
+
" # ---------------------------------------------------------\n",
|
| 484 |
+
" resnet_dataset = ResNetDataset(data_dir=eval_dir, transform=resnet_test_transforms)\n",
|
| 485 |
+
" resnet_loader = DataLoader(resnet_dataset, batch_size=16, shuffle=False)\n",
|
| 486 |
+
" \n",
|
| 487 |
+
" # Initialize your base ResNet PyTorch architecture\n",
|
| 488 |
+
" # Replace 'get_resnet_model' with how you initialize your ResNet\n",
|
| 489 |
+
" base_resnet = Car_Classifier_Resnet(num_classes=len(resnet_dataset.classes))\n",
|
| 490 |
+
" \n",
|
| 491 |
+
" # Load weights and get ONNX path from Hub\n",
|
| 492 |
+
" torch_resnet, onnx_resnet_path = load_models_from_hf(\n",
|
| 493 |
+
" repo_id=\"junaid17/car-damage-classifier\",\n",
|
| 494 |
+
" pt_filename=\"car-damage-classifier.pt\", # Update if named differently on HF\n",
|
| 495 |
+
" onnx_filename=\"car-damage-classifier.onnx\",\n",
|
| 496 |
+
" model_instance=base_resnet\n",
|
| 497 |
+
" )\n",
|
| 498 |
+
" \n",
|
| 499 |
+
" evaluate_models('resnet', torch_resnet, onnx_resnet_path, resnet_loader, resnet_dataset.classes)"
|
| 500 |
+
]
|
| 501 |
+
},
|
| 502 |
+
{
|
| 503 |
+
"cell_type": "code",
|
| 504 |
+
"execution_count": null,
|
| 505 |
+
"id": "36697686",
|
| 506 |
+
"metadata": {},
|
| 507 |
+
"outputs": [],
|
| 508 |
+
"source": []
|
| 509 |
+
}
|
| 510 |
+
],
|
| 511 |
+
"metadata": {
|
| 512 |
+
"kernelspec": {
|
| 513 |
+
"display_name": "myvenv (3.11.0)",
|
| 514 |
+
"language": "python",
|
| 515 |
+
"name": "python3"
|
| 516 |
+
},
|
| 517 |
+
"language_info": {
|
| 518 |
+
"codemirror_mode": {
|
| 519 |
+
"name": "ipython",
|
| 520 |
+
"version": 3
|
| 521 |
+
},
|
| 522 |
+
"file_extension": ".py",
|
| 523 |
+
"mimetype": "text/x-python",
|
| 524 |
+
"name": "python",
|
| 525 |
+
"nbconvert_exporter": "python",
|
| 526 |
+
"pygments_lexer": "ipython3",
|
| 527 |
+
"version": "3.11.0"
|
| 528 |
+
}
|
| 529 |
+
},
|
| 530 |
+
"nbformat": 4,
|
| 531 |
+
"nbformat_minor": 5
|
| 532 |
+
}
|
Models_Conversion/model_onnx_conversion.ipynb
ADDED
|
@@ -0,0 +1,1171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"id": "ac6e10d4",
|
| 6 |
+
"metadata": {},
|
| 7 |
+
"source": [
|
| 8 |
+
"# Fusion Model Conversion"
|
| 9 |
+
]
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"cell_type": "code",
|
| 13 |
+
"execution_count": 4,
|
| 14 |
+
"id": "0f63f311",
|
| 15 |
+
"metadata": {},
|
| 16 |
+
"outputs": [
|
| 17 |
+
{
|
| 18 |
+
"name": "stdout",
|
| 19 |
+
"output_type": "stream",
|
| 20 |
+
"text": [
|
| 21 |
+
"Downloading checkpoint from Hugging Face Hub: 'junaid17/best_fusion_model_fp16/best_fusion_model_fp16.pt'...\n"
|
| 22 |
+
]
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"name": "stderr",
|
| 26 |
+
"output_type": "stream",
|
| 27 |
+
"text": [
|
| 28 |
+
"Loading weights: 100%|██████████| 342/342 [00:00<00:00, 1952.82it/s]\n",
|
| 29 |
+
"[transformers] \u001b[1mConvNextModel LOAD REPORT\u001b[0m from: facebook/convnext-small-224\n",
|
| 30 |
+
"Key | Status | | \n",
|
| 31 |
+
"------------------+------------+--+-\n",
|
| 32 |
+
"classifier.weight | UNEXPECTED | | \n",
|
| 33 |
+
"classifier.bias | UNEXPECTED | | \n",
|
| 34 |
+
"\n",
|
| 35 |
+
"Notes:\n",
|
| 36 |
+
"- UNEXPECTED:\tcan be ignored when loading from different task/architecture; not ok if you expect identical arch.\n"
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"name": "stdout",
|
| 41 |
+
"output_type": "stream",
|
| 42 |
+
"text": [
|
| 43 |
+
"✅ Model loaded successfully from Hugging Face.\n",
|
| 44 |
+
"Exporting Fusion Model to temporary ONNX graph...\n",
|
| 45 |
+
"✅ Base ONNX export successful.\n",
|
| 46 |
+
"Compressing ONNX model to FP16 precision...\n",
|
| 47 |
+
"✅ ONNX graph compressed to FP16 successfully.\n",
|
| 48 |
+
"Uploading 'fusion_model.onnx' to Hugging Face repo: 'junaid17/best_fusion_model_fp16'...\n"
|
| 49 |
+
]
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"name": "stderr",
|
| 53 |
+
"output_type": "stream",
|
| 54 |
+
"text": [
|
| 55 |
+
"Processing Files (1 / 1): 100%|██████████| 142MB / 142MB, 680kB/s \n",
|
| 56 |
+
"New Data Upload: 100%|██████████| 142MB / 142MB, 680kB/s \n"
|
| 57 |
+
]
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"name": "stdout",
|
| 61 |
+
"output_type": "stream",
|
| 62 |
+
"text": [
|
| 63 |
+
"🚀 Successfully uploaded FP16 model to https://huggingface.co/junaid17/best_fusion_model_fp16/blob/main/fusion_model.onnx\n",
|
| 64 |
+
"Downloading 'fusion_model.onnx' from Hugging Face for evaluation...\n",
|
| 65 |
+
"📸 Found 60 images in 'D:\\DamageLensAI\\test_images'. Starting benchmark...\n",
|
| 66 |
+
"\n",
|
| 67 |
+
"===========================================================================\n",
|
| 68 |
+
"IMAGE NAME | PYTORCH (ms) | ONNX (ms) | MATCH? \n",
|
| 69 |
+
"===========================================================================\n",
|
| 70 |
+
"FB_10.jpg | 775.56 | 225.80 | ✅ YES \n",
|
| 71 |
+
"FB_11.jpg | 683.06 | 263.21 | ✅ YES \n",
|
| 72 |
+
"FB_13.jpg | 359.21 | 156.10 | ✅ YES \n",
|
| 73 |
+
"FB_15.jpg | 357.64 | 152.04 | ✅ YES \n",
|
| 74 |
+
"FB_16.jpg | 340.61 | 157.13 | ✅ YES \n",
|
| 75 |
+
"FB_19.jpg | 343.34 | 157.87 | ✅ YES \n",
|
| 76 |
+
"FB_20.jpg | 562.84 | 188.25 | ✅ YES \n",
|
| 77 |
+
"FB_4.jpg | 328.80 | 162.34 | ✅ YES \n",
|
| 78 |
+
"FB_5.jpg | 466.30 | 166.86 | ✅ YES \n",
|
| 79 |
+
"FB_9.jpg | 657.23 | 188.45 | ✅ YES \n",
|
| 80 |
+
"FC_1.jpg | 372.87 | 234.55 | ✅ YES \n",
|
| 81 |
+
"FC_10.jpg | 473.04 | 164.85 | ✅ YES \n",
|
| 82 |
+
"FC_11.jpg | 332.92 | 154.70 | ✅ YES \n",
|
| 83 |
+
"FC_12.jpg | 379.71 | 197.05 | ✅ YES \n",
|
| 84 |
+
"FC_3.jpg | 546.80 | 297.15 | ✅ YES \n",
|
| 85 |
+
"FC_4.jpg | 810.00 | 387.67 | ✅ YES \n",
|
| 86 |
+
"FC_6.jpg | 528.11 | 211.10 | ✅ YES \n",
|
| 87 |
+
"FC_7.jpg | 546.06 | 267.98 | ✅ YES \n",
|
| 88 |
+
"FC_8.jpg | 665.58 | 230.13 | ✅ YES \n",
|
| 89 |
+
"FC_9.jpg | 516.99 | 213.58 | ✅ YES \n",
|
| 90 |
+
"FN_1.jpg | 497.13 | 216.17 | ✅ YES \n",
|
| 91 |
+
"FN_10.jpg | 484.10 | 215.26 | ✅ YES \n",
|
| 92 |
+
"FN_11.jpg | 521.18 | 221.95 | ✅ YES \n",
|
| 93 |
+
"FN_12.jpg | 498.07 | 235.80 | ✅ YES \n",
|
| 94 |
+
"FN_3.jpg | 473.85 | 219.38 | ✅ YES \n",
|
| 95 |
+
"FN_4.jpg | 474.16 | 419.76 | ✅ YES \n",
|
| 96 |
+
"FN_5.jpg | 796.23 | 229.17 | ✅ YES \n",
|
| 97 |
+
"FN_7.jpg | 506.63 | 211.40 | ✅ YES \n",
|
| 98 |
+
"FN_8.jpg | 432.50 | 237.60 | ✅ YES \n",
|
| 99 |
+
"FN_9.jpg | 725.21 | 239.89 | ✅ YES \n",
|
| 100 |
+
"RB_10.jpg | 480.74 | 220.76 | ✅ YES \n",
|
| 101 |
+
"RB_11.jpg | 449.81 | 224.74 | ✅ YES \n",
|
| 102 |
+
"RB_13.jpg | 478.89 | 221.27 | ✅ YES \n",
|
| 103 |
+
"RB_14.jpg | 500.14 | 222.87 | ✅ YES \n",
|
| 104 |
+
"RB_2.jpg | 494.09 | 223.37 | ✅ YES \n",
|
| 105 |
+
"RB_3.jpg | 488.29 | 220.04 | ✅ YES \n",
|
| 106 |
+
"RB_4.jpg | 498.87 | 233.70 | ✅ YES \n",
|
| 107 |
+
"RB_6.jpg | 496.31 | 221.88 | ✅ YES \n",
|
| 108 |
+
"RB_7.jpg | 477.51 | 214.55 | ✅ YES \n",
|
| 109 |
+
"RB_8.jpg | 503.84 | 217.26 | ✅ YES \n",
|
| 110 |
+
"RC_10.jpg | 491.95 | 221.90 | ✅ YES \n",
|
| 111 |
+
"RC_11.jpg | 497.75 | 218.97 | ✅ YES \n",
|
| 112 |
+
"RC_2.jpg | 493.97 | 222.79 | ✅ YES \n",
|
| 113 |
+
"RC_3.jpg | 481.42 | 220.67 | ✅ YES \n",
|
| 114 |
+
"RC_4.jpg | 487.19 | 224.59 | ✅ YES \n",
|
| 115 |
+
"RC_5.jpg | 484.33 | 219.35 | ✅ YES \n",
|
| 116 |
+
"RC_6.jpg | 483.71 | 216.13 | ✅ YES \n",
|
| 117 |
+
"RC_7.jpg | 572.23 | 213.54 | ✅ YES \n",
|
| 118 |
+
"RC_8.jpg | 484.69 | 218.72 | ✅ YES \n",
|
| 119 |
+
"RC_9.jpg | 578.70 | 218.05 | ✅ YES \n",
|
| 120 |
+
"RN_1.jpg | 493.88 | 221.69 | ✅ YES \n",
|
| 121 |
+
"RN_10.jpg | 469.91 | 213.39 | ✅ YES \n",
|
| 122 |
+
"RN_11.jpg | 488.03 | 212.66 | ✅ YES \n",
|
| 123 |
+
"RN_12.jpg | 495.62 | 209.92 | ✅ YES \n",
|
| 124 |
+
"RN_2.jpg | 484.84 | 214.93 | ✅ YES \n",
|
| 125 |
+
"RN_3.jpg | 467.09 | 218.67 | ✅ YES \n",
|
| 126 |
+
"RN_4.jpg | 496.70 | 215.96 | ✅ YES \n",
|
| 127 |
+
"RN_7.jpg | 480.62 | 214.66 | ✅ YES \n",
|
| 128 |
+
"RN_8.jpg | 502.00 | 213.91 | ✅ YES \n",
|
| 129 |
+
"RN_9.jpg | 466.22 | 217.81 | ✅ YES \n",
|
| 130 |
+
"===========================================================================\n",
|
| 131 |
+
" BENCHMARK SUMMARY\n",
|
| 132 |
+
"===========================================================================\n",
|
| 133 |
+
"Total Images Evaluated : 60\n",
|
| 134 |
+
"Prediction Match Rate : 100.00% (60/60)\n",
|
| 135 |
+
"PyTorch Avg Latency : 503.75 ms / image\n",
|
| 136 |
+
"ONNX Runtime Avg Latency: 219.87 ms / image\n",
|
| 137 |
+
"Performance Gain : 56.35% faster with ONNX Runtime\n",
|
| 138 |
+
"===========================================================================\n",
|
| 139 |
+
"\n"
|
| 140 |
+
]
|
| 141 |
+
}
|
| 142 |
+
],
|
| 143 |
+
"source": [
|
| 144 |
+
"import os\n",
|
| 145 |
+
"import glob\n",
|
| 146 |
+
"import time\n",
|
| 147 |
+
"import tempfile\n",
|
| 148 |
+
"import warnings\n",
|
| 149 |
+
"import torch\n",
|
| 150 |
+
"import torchvision.transforms as transforms\n",
|
| 151 |
+
"import onnxruntime as ort\n",
|
| 152 |
+
"import numpy as np\n",
|
| 153 |
+
"import onnx\n",
|
| 154 |
+
"from onnxconverter_common import float16\n",
|
| 155 |
+
"from PIL import Image\n",
|
| 156 |
+
"from dotenv import load_dotenv\n",
|
| 157 |
+
"from transformers import ConvNextImageProcessor\n",
|
| 158 |
+
"from huggingface_hub import HfApi, hf_hub_download\n",
|
| 159 |
+
"\n",
|
| 160 |
+
"# Import model loader from base_fusion_model.py\n",
|
| 161 |
+
"from base_fusion_model import load_fusion_model_from_hf\n",
|
| 162 |
+
"\n",
|
| 163 |
+
"# Filter out PyTorch tracing and transformers load warnings for clean logs\n",
|
| 164 |
+
"warnings.filterwarnings(\"ignore\", category=torch.jit.TracerWarning)\n",
|
| 165 |
+
"warnings.filterwarnings(\"ignore\", category=UserWarning)\n",
|
| 166 |
+
"warnings.filterwarnings(\"ignore\", category=DeprecationWarning)\n",
|
| 167 |
+
"\n",
|
| 168 |
+
"# ==========================================\n",
|
| 169 |
+
"# 1. PREPROCESSING PIPELINE SETUP\n",
|
| 170 |
+
"# ==========================================\n",
|
| 171 |
+
"IMG_SIZE = 260\n",
|
| 172 |
+
"\n",
|
| 173 |
+
"val_transforms = transforms.Compose([\n",
|
| 174 |
+
" transforms.Resize((IMG_SIZE, IMG_SIZE)),\n",
|
| 175 |
+
"])\n",
|
| 176 |
+
"\n",
|
| 177 |
+
"eff_normalize = transforms.Compose([\n",
|
| 178 |
+
" transforms.ToTensor(),\n",
|
| 179 |
+
" transforms.Normalize(\n",
|
| 180 |
+
" mean=[0.485, 0.456, 0.406],\n",
|
| 181 |
+
" std=[0.229, 0.224, 0.225]\n",
|
| 182 |
+
" )\n",
|
| 183 |
+
"])\n",
|
| 184 |
+
"\n",
|
| 185 |
+
"\n",
|
| 186 |
+
"def preprocess_image(pil_image: Image.Image, processor: ConvNextImageProcessor):\n",
|
| 187 |
+
" resized_image = val_transforms(pil_image)\n",
|
| 188 |
+
" pixel_eff = eff_normalize(resized_image).unsqueeze(0)\n",
|
| 189 |
+
" enc = processor(images=resized_image, return_tensors='pt')\n",
|
| 190 |
+
" pixel_cnx = enc['pixel_values']\n",
|
| 191 |
+
"\n",
|
| 192 |
+
" return pixel_eff, pixel_cnx\n",
|
| 193 |
+
"\n",
|
| 194 |
+
"\n",
|
| 195 |
+
"# ==========================================\n",
|
| 196 |
+
"# 2. CONVERT & UPLOAD TO HUGGING FACE\n",
|
| 197 |
+
"# ==========================================\n",
|
| 198 |
+
"def convert_and_upload_onnx(\n",
|
| 199 |
+
" model: torch.nn.Module,\n",
|
| 200 |
+
" processor: ConvNextImageProcessor,\n",
|
| 201 |
+
" repo_id: str,\n",
|
| 202 |
+
" onnx_filename_in_repo: str = \"fusion_model.onnx\",\n",
|
| 203 |
+
" hf_token: str = None,\n",
|
| 204 |
+
"):\n",
|
| 205 |
+
" model.eval()\n",
|
| 206 |
+
"\n",
|
| 207 |
+
" dummy_pil = Image.new(\"RGB\", (IMG_SIZE, IMG_SIZE), color=\"white\")\n",
|
| 208 |
+
" dummy_eff, dummy_cnx = preprocess_image(dummy_pil, processor)\n",
|
| 209 |
+
"\n",
|
| 210 |
+
" with tempfile.TemporaryDirectory() as temp_dir:\n",
|
| 211 |
+
" temp_onnx_path = os.path.join(temp_dir, onnx_filename_in_repo)\n",
|
| 212 |
+
"\n",
|
| 213 |
+
" print(\"Exporting Fusion Model to temporary ONNX graph...\")\n",
|
| 214 |
+
" \n",
|
| 215 |
+
" with warnings.catch_warnings():\n",
|
| 216 |
+
" warnings.simplefilter(\"ignore\")\n",
|
| 217 |
+
" torch.onnx.export(\n",
|
| 218 |
+
" model,\n",
|
| 219 |
+
" (dummy_eff, dummy_cnx),\n",
|
| 220 |
+
" temp_onnx_path,\n",
|
| 221 |
+
" export_params=True,\n",
|
| 222 |
+
" opset_version=18,\n",
|
| 223 |
+
" do_constant_folding=True,\n",
|
| 224 |
+
" input_names=[\"pixel_values_eff\", \"pixel_values_cnx\"],\n",
|
| 225 |
+
" output_names=[\"logits\"],\n",
|
| 226 |
+
" dynamic_axes={\n",
|
| 227 |
+
" \"pixel_values_eff\": {0: \"batch_size\"},\n",
|
| 228 |
+
" \"pixel_values_cnx\": {0: \"batch_size\"},\n",
|
| 229 |
+
" \"logits\": {0: \"batch_size\"},\n",
|
| 230 |
+
" },\n",
|
| 231 |
+
" dynamo=False,\n",
|
| 232 |
+
" )\n",
|
| 233 |
+
" print(\"✅ Base ONNX export successful.\")\n",
|
| 234 |
+
"\n",
|
| 235 |
+
" # Convert ONNX graph weights to FP16 to keep file size ~142 MB\n",
|
| 236 |
+
" print(\"Compressing ONNX model to FP16 precision...\")\n",
|
| 237 |
+
" onnx_model = onnx.load(temp_onnx_path)\n",
|
| 238 |
+
" onnx_model_fp16 = float16.convert_float_to_float16(onnx_model, keep_io_types=True)\n",
|
| 239 |
+
" onnx.save(onnx_model_fp16, temp_onnx_path)\n",
|
| 240 |
+
" print(\"✅ ONNX graph compressed to FP16 successfully.\")\n",
|
| 241 |
+
"\n",
|
| 242 |
+
" # Upload directly to Hugging Face Hub using token from .env\n",
|
| 243 |
+
" api = HfApi(token=hf_token)\n",
|
| 244 |
+
" print(f\"Uploading '{onnx_filename_in_repo}' to Hugging Face repo: '{repo_id}'...\")\n",
|
| 245 |
+
" \n",
|
| 246 |
+
" api.upload_file(\n",
|
| 247 |
+
" path_or_fileobj=temp_onnx_path,\n",
|
| 248 |
+
" path_in_repo=onnx_filename_in_repo,\n",
|
| 249 |
+
" repo_id=repo_id,\n",
|
| 250 |
+
" repo_type=\"model\",\n",
|
| 251 |
+
" )\n",
|
| 252 |
+
" print(f\"🚀 Successfully uploaded FP16 model to https://huggingface.co/{repo_id}/blob/main/{onnx_filename_in_repo}\")\n",
|
| 253 |
+
"\n",
|
| 254 |
+
"\n",
|
| 255 |
+
"# ==========================================\n",
|
| 256 |
+
"# 3. BENCHMARK ON REAL TEST IMAGES\n",
|
| 257 |
+
"# ==========================================\n",
|
| 258 |
+
"def benchmark_fusion_with_folder(\n",
|
| 259 |
+
" pytorch_model: torch.nn.Module,\n",
|
| 260 |
+
" processor: ConvNextImageProcessor,\n",
|
| 261 |
+
" repo_id: str,\n",
|
| 262 |
+
" onnx_filename_in_repo: str,\n",
|
| 263 |
+
" image_folder: str = r\"D:\\DamageLensAI\\test_images\",\n",
|
| 264 |
+
" hf_token: str = None,\n",
|
| 265 |
+
"):\n",
|
| 266 |
+
" print(f\"Downloading '{onnx_filename_in_repo}' from Hugging Face for evaluation...\")\n",
|
| 267 |
+
" onnx_path = hf_hub_download(\n",
|
| 268 |
+
" repo_id=repo_id, \n",
|
| 269 |
+
" filename=onnx_filename_in_repo,\n",
|
| 270 |
+
" token=hf_token\n",
|
| 271 |
+
" )\n",
|
| 272 |
+
" session = ort.InferenceSession(onnx_path, providers=[\"CPUExecutionProvider\"])\n",
|
| 273 |
+
"\n",
|
| 274 |
+
" supported_exts = (\"*.jpg\", \"*.jpeg\", \"*.png\", \"*.bmp\", \"*.webp\")\n",
|
| 275 |
+
" image_paths = []\n",
|
| 276 |
+
" for ext in supported_exts:\n",
|
| 277 |
+
" image_paths.extend(glob.glob(os.path.join(image_folder, ext)))\n",
|
| 278 |
+
"\n",
|
| 279 |
+
" if not image_paths:\n",
|
| 280 |
+
" raise FileNotFoundError(\n",
|
| 281 |
+
" f\"❌ No valid images found in folder '{image_folder}'. \"\n",
|
| 282 |
+
" f\"Please place your test images inside '{image_folder}'.\"\n",
|
| 283 |
+
" )\n",
|
| 284 |
+
"\n",
|
| 285 |
+
" print(f\"📸 Found {len(image_paths)} images in '{image_folder}'. Starting benchmark...\")\n",
|
| 286 |
+
"\n",
|
| 287 |
+
" pytorch_latencies = []\n",
|
| 288 |
+
" onnx_latencies = []\n",
|
| 289 |
+
" matches = 0\n",
|
| 290 |
+
"\n",
|
| 291 |
+
" # Warmup\n",
|
| 292 |
+
" first_img = Image.open(sorted(image_paths)[0]).convert(\"RGB\")\n",
|
| 293 |
+
" warmup_eff, warmup_cnx = preprocess_image(first_img, processor)\n",
|
| 294 |
+
" warmup_eff_np = warmup_eff.numpy()\n",
|
| 295 |
+
" warmup_cnx_np = warmup_cnx.numpy()\n",
|
| 296 |
+
"\n",
|
| 297 |
+
" pytorch_model.eval()\n",
|
| 298 |
+
" with torch.no_grad():\n",
|
| 299 |
+
" _ = pytorch_model(warmup_eff, warmup_cnx)\n",
|
| 300 |
+
" session.run(None, {\"pixel_values_eff\": warmup_eff_np, \"pixel_values_cnx\": warmup_cnx_np})\n",
|
| 301 |
+
"\n",
|
| 302 |
+
" # Benchmark loop\n",
|
| 303 |
+
" print(\"\\n\" + \"=\" * 75)\n",
|
| 304 |
+
" print(f\"{'IMAGE NAME':<25} | {'PYTORCH (ms)':<14} | {'ONNX (ms)':<14} | {'MATCH?':<10}\")\n",
|
| 305 |
+
" print(\"=\" * 75)\n",
|
| 306 |
+
"\n",
|
| 307 |
+
" for img_path in sorted(image_paths):\n",
|
| 308 |
+
" file_name = os.path.basename(img_path)\n",
|
| 309 |
+
" raw_img = Image.open(img_path).convert(\"RGB\")\n",
|
| 310 |
+
" \n",
|
| 311 |
+
" pixel_values_eff, pixel_values_cnx = preprocess_image(raw_img, processor)\n",
|
| 312 |
+
" np_eff = pixel_values_eff.numpy()\n",
|
| 313 |
+
" np_cnx = pixel_values_cnx.numpy()\n",
|
| 314 |
+
"\n",
|
| 315 |
+
" # PyTorch Inference\n",
|
| 316 |
+
" start_pt = time.perf_counter()\n",
|
| 317 |
+
" with torch.no_grad():\n",
|
| 318 |
+
" pt_logits = pytorch_model(pixel_values_eff, pixel_values_cnx)\n",
|
| 319 |
+
" pt_time = (time.perf_counter() - start_pt) * 1000\n",
|
| 320 |
+
" pytorch_latencies.append(pt_time)\n",
|
| 321 |
+
" pt_pred = int(torch.argmax(pt_logits, dim=1)[0])\n",
|
| 322 |
+
"\n",
|
| 323 |
+
" # ONNX Runtime Inference\n",
|
| 324 |
+
" start_onnx = time.perf_counter()\n",
|
| 325 |
+
" onnx_outputs = session.run(\n",
|
| 326 |
+
" None, \n",
|
| 327 |
+
" {\"pixel_values_eff\": np_eff, \"pixel_values_cnx\": np_cnx}\n",
|
| 328 |
+
" )\n",
|
| 329 |
+
" onnx_time = (time.perf_counter() - start_onnx) * 1000\n",
|
| 330 |
+
" onnx_latencies.append(onnx_time)\n",
|
| 331 |
+
" onnx_pred = int(np.argmax(onnx_outputs[0], axis=1)[0])\n",
|
| 332 |
+
"\n",
|
| 333 |
+
" is_match = pt_pred == onnx_pred\n",
|
| 334 |
+
" if is_match:\n",
|
| 335 |
+
" matches += 1\n",
|
| 336 |
+
" match_str = \"✅ YES\" if is_match else \"❌ NO\"\n",
|
| 337 |
+
"\n",
|
| 338 |
+
" print(f\"{file_name:<25} | {pt_time:<14.2f} | {onnx_time:<14.2f} | {match_str:<10}\")\n",
|
| 339 |
+
"\n",
|
| 340 |
+
" avg_pt = np.mean(pytorch_latencies)\n",
|
| 341 |
+
" avg_onnx = np.mean(onnx_latencies)\n",
|
| 342 |
+
" speedup = ((avg_pt - avg_onnx) / avg_pt) * 100\n",
|
| 343 |
+
" accuracy_match_pct = (matches / len(image_paths)) * 100\n",
|
| 344 |
+
"\n",
|
| 345 |
+
" print(\"=\" * 75)\n",
|
| 346 |
+
" print(\" BENCHMARK SUMMARY\")\n",
|
| 347 |
+
" print(\"=\" * 75)\n",
|
| 348 |
+
" print(f\"Total Images Evaluated : {len(image_paths)}\")\n",
|
| 349 |
+
" print(f\"Prediction Match Rate : {accuracy_match_pct:.2f}% ({matches}/{len(image_paths)})\")\n",
|
| 350 |
+
" print(f\"PyTorch Avg Latency : {avg_pt:.2f} ms / image\")\n",
|
| 351 |
+
" print(f\"ONNX Runtime Avg Latency: {avg_onnx:.2f} ms / image\")\n",
|
| 352 |
+
" print(f\"Performance Gain : {speedup:.2f}% faster with ONNX Runtime\")\n",
|
| 353 |
+
" print(\"=\" * 75 + \"\\n\")\n",
|
| 354 |
+
"\n",
|
| 355 |
+
"\n",
|
| 356 |
+
"# ==========================================\n",
|
| 357 |
+
"# MAIN EXECUTION FLOW\n",
|
| 358 |
+
"# ==========================================\n",
|
| 359 |
+
"if __name__ == \"__main__\":\n",
|
| 360 |
+
" # Load environment variables from .env file\n",
|
| 361 |
+
" load_dotenv()\n",
|
| 362 |
+
" HF_TOKEN = os.getenv(\"HF_TOKEN\")\n",
|
| 363 |
+
"\n",
|
| 364 |
+
" if not HF_TOKEN:\n",
|
| 365 |
+
" print(\"⚠️ Warning: HF_TOKEN not found in .env file. Pushing/pulling private repos may fail.\")\n",
|
| 366 |
+
"\n",
|
| 367 |
+
" NUM_CLASSES = 6\n",
|
| 368 |
+
" HF_REPO_ID = \"junaid17/best_fusion_model_fp16\"\n",
|
| 369 |
+
" ONNX_FILENAME = \"fusion_model.onnx\"\n",
|
| 370 |
+
" TEST_IMAGE_FOLDER = r\"D:\\DamageLensAI\\test_images\"\n",
|
| 371 |
+
" CONVNEXT_MODEL_NAME = \"facebook/convnext-small-224\"\n",
|
| 372 |
+
"\n",
|
| 373 |
+
" # Step 1: Initialize ConvNeXt Processor\n",
|
| 374 |
+
" processor = ConvNextImageProcessor.from_pretrained(CONVNEXT_MODEL_NAME)\n",
|
| 375 |
+
"\n",
|
| 376 |
+
" # Step 2: Load PyTorch model from Hugging Face\n",
|
| 377 |
+
" py_model = load_fusion_model_from_hf(\n",
|
| 378 |
+
" repo_id=HF_REPO_ID,\n",
|
| 379 |
+
" filename=\"best_fusion_model_fp16.pt\",\n",
|
| 380 |
+
" num_classes=NUM_CLASSES,\n",
|
| 381 |
+
" device=\"cpu\",\n",
|
| 382 |
+
" )\n",
|
| 383 |
+
"\n",
|
| 384 |
+
" # Step 3: Convert to FP16 ONNX and push directly to Hugging Face\n",
|
| 385 |
+
" convert_and_upload_onnx(\n",
|
| 386 |
+
" model=py_model,\n",
|
| 387 |
+
" processor=processor,\n",
|
| 388 |
+
" repo_id=HF_REPO_ID,\n",
|
| 389 |
+
" onnx_filename_in_repo=ONNX_FILENAME,\n",
|
| 390 |
+
" hf_token=HF_TOKEN,\n",
|
| 391 |
+
" )\n",
|
| 392 |
+
"\n",
|
| 393 |
+
" # Step 4: Run benchmarking on test image folder\n",
|
| 394 |
+
" benchmark_fusion_with_folder(\n",
|
| 395 |
+
" pytorch_model=py_model,\n",
|
| 396 |
+
" processor=processor,\n",
|
| 397 |
+
" repo_id=HF_REPO_ID,\n",
|
| 398 |
+
" onnx_filename_in_repo=ONNX_FILENAME,\n",
|
| 399 |
+
" image_folder=TEST_IMAGE_FOLDER,\n",
|
| 400 |
+
" hf_token=HF_TOKEN,\n",
|
| 401 |
+
" )"
|
| 402 |
+
]
|
| 403 |
+
},
|
| 404 |
+
{
|
| 405 |
+
"cell_type": "markdown",
|
| 406 |
+
"id": "0e10195c",
|
| 407 |
+
"metadata": {},
|
| 408 |
+
"source": [
|
| 409 |
+
"# Resnet Model Conversion"
|
| 410 |
+
]
|
| 411 |
+
},
|
| 412 |
+
{
|
| 413 |
+
"cell_type": "code",
|
| 414 |
+
"execution_count": 12,
|
| 415 |
+
"id": "000b4500",
|
| 416 |
+
"metadata": {},
|
| 417 |
+
"outputs": [
|
| 418 |
+
{
|
| 419 |
+
"name": "stdout",
|
| 420 |
+
"output_type": "stream",
|
| 421 |
+
"text": [
|
| 422 |
+
"Downloading checkpoint 'car-damage-classifier.pt' from Hugging Face repo 'junaid17/car-damage-classifier'...\n",
|
| 423 |
+
"✅ ResNet-18 model loaded successfully.\n",
|
| 424 |
+
"Exporting ResNet-18 Model to temporary FP32 ONNX graph...\n",
|
| 425 |
+
"✅ Base ONNX export successful.\n",
|
| 426 |
+
"Compressing ONNX model to FP16 precision...\n",
|
| 427 |
+
"✅ ONNX graph compressed to FP16 successfully.\n",
|
| 428 |
+
"Uploading 'car-damage-classifier.onnx' to Hugging Face repo: 'junaid17/car-damage-classifier'...\n"
|
| 429 |
+
]
|
| 430 |
+
},
|
| 431 |
+
{
|
| 432 |
+
"name": "stderr",
|
| 433 |
+
"output_type": "stream",
|
| 434 |
+
"text": [
|
| 435 |
+
"Processing Files (1 / 1): 100%|██████████| 22.6MB / 22.6MB, 1.05MB/s \n",
|
| 436 |
+
"New Data Upload: 100%|██████████| 22.6MB / 22.6MB, 1.05MB/s \n"
|
| 437 |
+
]
|
| 438 |
+
},
|
| 439 |
+
{
|
| 440 |
+
"name": "stdout",
|
| 441 |
+
"output_type": "stream",
|
| 442 |
+
"text": [
|
| 443 |
+
"🚀 Successfully uploaded FP16 ONNX model to https://huggingface.co/junaid17/car-damage-classifier/blob/main/car-damage-classifier.onnx\n",
|
| 444 |
+
"Downloading 'car-damage-classifier.onnx' from Hugging Face for evaluation...\n",
|
| 445 |
+
"📸 Found 60 images in 'D:\\DamageLensAI\\test_images'. Starting benchmark...\n",
|
| 446 |
+
"\n",
|
| 447 |
+
"===========================================================================\n",
|
| 448 |
+
"IMAGE NAME | PYTORCH (ms) | ONNX (ms) | MATCH? \n",
|
| 449 |
+
"===========================================================================\n",
|
| 450 |
+
"FB_10.jpg | 46.78 | 14.66 | ✅ YES \n",
|
| 451 |
+
"FB_11.jpg | 56.13 | 15.03 | ✅ YES \n",
|
| 452 |
+
"FB_13.jpg | 45.83 | 15.16 | ✅ YES \n",
|
| 453 |
+
"FB_15.jpg | 44.06 | 14.06 | ✅ YES \n",
|
| 454 |
+
"FB_16.jpg | 54.86 | 15.04 | ✅ YES \n",
|
| 455 |
+
"FB_19.jpg | 61.18 | 15.78 | ✅ YES \n",
|
| 456 |
+
"FB_20.jpg | 47.12 | 13.44 | ✅ YES \n",
|
| 457 |
+
"FB_4.jpg | 49.21 | 17.78 | ✅ YES \n",
|
| 458 |
+
"FB_5.jpg | 74.55 | 15.02 | ✅ YES \n",
|
| 459 |
+
"FB_9.jpg | 48.68 | 14.77 | ✅ YES \n",
|
| 460 |
+
"FC_1.jpg | 54.30 | 14.43 | ✅ YES \n",
|
| 461 |
+
"FC_10.jpg | 50.06 | 15.70 | ✅ YES \n",
|
| 462 |
+
"FC_11.jpg | 48.00 | 13.72 | ✅ YES \n",
|
| 463 |
+
"FC_12.jpg | 42.24 | 14.62 | ✅ YES \n",
|
| 464 |
+
"FC_3.jpg | 51.02 | 14.97 | ✅ YES \n",
|
| 465 |
+
"FC_4.jpg | 47.95 | 15.39 | ✅ YES \n",
|
| 466 |
+
"FC_6.jpg | 46.55 | 14.33 | ✅ YES \n",
|
| 467 |
+
"FC_7.jpg | 59.15 | 15.21 | ✅ YES \n",
|
| 468 |
+
"FC_8.jpg | 48.33 | 16.83 | ✅ YES \n",
|
| 469 |
+
"FC_9.jpg | 45.76 | 15.09 | ✅ YES \n",
|
| 470 |
+
"FN_1.jpg | 58.81 | 15.92 | ✅ YES \n",
|
| 471 |
+
"FN_10.jpg | 46.00 | 15.53 | ✅ YES \n",
|
| 472 |
+
"FN_11.jpg | 56.70 | 16.44 | ✅ YES \n",
|
| 473 |
+
"FN_12.jpg | 52.64 | 14.61 | ✅ YES \n",
|
| 474 |
+
"FN_3.jpg | 52.38 | 14.43 | ✅ YES \n",
|
| 475 |
+
"FN_4.jpg | 50.76 | 17.84 | ✅ YES \n",
|
| 476 |
+
"FN_5.jpg | 51.05 | 14.57 | ✅ YES \n",
|
| 477 |
+
"FN_7.jpg | 49.76 | 14.34 | ✅ YES \n",
|
| 478 |
+
"FN_8.jpg | 45.30 | 16.05 | ✅ YES \n",
|
| 479 |
+
"FN_9.jpg | 58.23 | 16.13 | ✅ YES \n",
|
| 480 |
+
"RB_10.jpg | 58.82 | 16.95 | ✅ YES \n",
|
| 481 |
+
"RB_11.jpg | 86.68 | 19.35 | ✅ YES \n",
|
| 482 |
+
"RB_13.jpg | 88.30 | 22.82 | ✅ YES \n",
|
| 483 |
+
"RB_14.jpg | 76.32 | 18.26 | ✅ YES \n",
|
| 484 |
+
"RB_2.jpg | 63.31 | 17.44 | ✅ YES \n",
|
| 485 |
+
"RB_3.jpg | 58.23 | 14.74 | ✅ YES \n",
|
| 486 |
+
"RB_4.jpg | 55.97 | 15.32 | ✅ YES \n",
|
| 487 |
+
"RB_6.jpg | 157.29 | 16.34 | ✅ YES \n",
|
| 488 |
+
"RB_7.jpg | 124.51 | 19.32 | ✅ YES \n",
|
| 489 |
+
"RB_8.jpg | 63.44 | 15.15 | ✅ YES \n",
|
| 490 |
+
"RC_10.jpg | 79.38 | 15.67 | ✅ YES \n",
|
| 491 |
+
"RC_11.jpg | 54.87 | 13.93 | ✅ YES \n",
|
| 492 |
+
"RC_2.jpg | 37.57 | 13.65 | ✅ YES \n",
|
| 493 |
+
"RC_3.jpg | 42.66 | 13.28 | ✅ YES \n",
|
| 494 |
+
"RC_4.jpg | 40.99 | 14.31 | ✅ YES \n",
|
| 495 |
+
"RC_5.jpg | 55.75 | 17.52 | ✅ YES \n",
|
| 496 |
+
"RC_6.jpg | 47.62 | 13.57 | ✅ YES \n",
|
| 497 |
+
"RC_7.jpg | 43.82 | 14.02 | ✅ YES \n",
|
| 498 |
+
"RC_8.jpg | 47.47 | 14.76 | ✅ YES \n",
|
| 499 |
+
"RC_9.jpg | 50.02 | 14.33 | ✅ YES \n",
|
| 500 |
+
"RN_1.jpg | 45.03 | 14.66 | ✅ YES \n",
|
| 501 |
+
"RN_10.jpg | 44.92 | 14.12 | ✅ YES \n",
|
| 502 |
+
"RN_11.jpg | 47.08 | 13.45 | ✅ YES \n",
|
| 503 |
+
"RN_12.jpg | 52.43 | 15.71 | ✅ YES \n",
|
| 504 |
+
"RN_2.jpg | 49.53 | 16.44 | ✅ YES \n",
|
| 505 |
+
"RN_3.jpg | 43.17 | 14.90 | ✅ YES \n",
|
| 506 |
+
"RN_4.jpg | 43.36 | 14.52 | ✅ YES \n",
|
| 507 |
+
"RN_7.jpg | 50.25 | 14.68 | ✅ YES \n",
|
| 508 |
+
"RN_8.jpg | 44.29 | 13.59 | ✅ YES \n",
|
| 509 |
+
"RN_9.jpg | 44.30 | 15.32 | ✅ YES \n",
|
| 510 |
+
"===========================================================================\n",
|
| 511 |
+
" BENCHMARK SUMMARY\n",
|
| 512 |
+
"===========================================================================\n",
|
| 513 |
+
"Total Images Evaluated : 60\n",
|
| 514 |
+
"Prediction Match Rate : 100.00% (60/60)\n",
|
| 515 |
+
"PyTorch Avg Latency : 55.68 ms / image\n",
|
| 516 |
+
"ONNX Runtime Avg Latency: 15.42 ms / image\n",
|
| 517 |
+
"Performance Gain : 72.31% faster with ONNX Runtime\n",
|
| 518 |
+
"===========================================================================\n",
|
| 519 |
+
"\n"
|
| 520 |
+
]
|
| 521 |
+
}
|
| 522 |
+
],
|
| 523 |
+
"source": [
|
| 524 |
+
"import os\n",
|
| 525 |
+
"import glob\n",
|
| 526 |
+
"import time\n",
|
| 527 |
+
"import tempfile\n",
|
| 528 |
+
"import warnings\n",
|
| 529 |
+
"import torch\n",
|
| 530 |
+
"import torchvision.transforms as transforms\n",
|
| 531 |
+
"import onnxruntime as ort\n",
|
| 532 |
+
"import numpy as np\n",
|
| 533 |
+
"import onnx\n",
|
| 534 |
+
"from onnxconverter_common import float16\n",
|
| 535 |
+
"from PIL import Image\n",
|
| 536 |
+
"from dotenv import load_dotenv\n",
|
| 537 |
+
"from huggingface_hub import HfApi, hf_hub_download\n",
|
| 538 |
+
"\n",
|
| 539 |
+
"# Import ResNet loader\n",
|
| 540 |
+
"from base_model_resnet import load_resnet_model_from_hf\n",
|
| 541 |
+
"\n",
|
| 542 |
+
"# Suppress PyTorch tracing and deprecation warnings\n",
|
| 543 |
+
"warnings.filterwarnings(\"ignore\", category=torch.jit.TracerWarning)\n",
|
| 544 |
+
"warnings.filterwarnings(\"ignore\", category=UserWarning)\n",
|
| 545 |
+
"warnings.filterwarnings(\"ignore\", category=DeprecationWarning)\n",
|
| 546 |
+
"\n",
|
| 547 |
+
"# ==========================================\n",
|
| 548 |
+
"# 1. PREPROCESSING PIPELINE\n",
|
| 549 |
+
"# ==========================================\n",
|
| 550 |
+
"test_transforms = transforms.Compose([\n",
|
| 551 |
+
" transforms.Resize((224, 224)),\n",
|
| 552 |
+
" transforms.ToTensor(),\n",
|
| 553 |
+
" transforms.Normalize(\n",
|
| 554 |
+
" mean=[0.485, 0.456, 0.406],\n",
|
| 555 |
+
" std=[0.229, 0.224, 0.225]\n",
|
| 556 |
+
" )\n",
|
| 557 |
+
"])\n",
|
| 558 |
+
"\n",
|
| 559 |
+
"\n",
|
| 560 |
+
"def preprocess_image(pil_image: Image.Image) -> torch.Tensor:\n",
|
| 561 |
+
" \"\"\"\n",
|
| 562 |
+
" Applies standard ResNet preprocessing and adds a batch dimension.\n",
|
| 563 |
+
" Shape returned: (1, 3, 224, 224)\n",
|
| 564 |
+
" \"\"\"\n",
|
| 565 |
+
" return test_transforms(pil_image).unsqueeze(0)\n",
|
| 566 |
+
"\n",
|
| 567 |
+
"\n",
|
| 568 |
+
"# ==========================================\n",
|
| 569 |
+
"# 2. CONVERT TO FP16 ONNX & UPLOAD\n",
|
| 570 |
+
"# ==========================================\n",
|
| 571 |
+
"def convert_and_upload_onnx_fp16(\n",
|
| 572 |
+
" model: torch.nn.Module,\n",
|
| 573 |
+
" repo_id: str,\n",
|
| 574 |
+
" onnx_filename_in_repo: str = \"car-damage-classifier.onnx\",\n",
|
| 575 |
+
" hf_token: str = None,\n",
|
| 576 |
+
"):\n",
|
| 577 |
+
" \"\"\"\n",
|
| 578 |
+
" Exports PyTorch ResNet model to ONNX, compresses it to FP16,\n",
|
| 579 |
+
" and pushes it directly to Hugging Face Hub.\n",
|
| 580 |
+
" \"\"\"\n",
|
| 581 |
+
" model.eval()\n",
|
| 582 |
+
"\n",
|
| 583 |
+
" # Create dummy tensor for graph tracing (1, 3, 224, 224)\n",
|
| 584 |
+
" dummy_input = torch.randn(1, 3, 224, 224)\n",
|
| 585 |
+
"\n",
|
| 586 |
+
" with tempfile.TemporaryDirectory() as temp_dir:\n",
|
| 587 |
+
" temp_onnx_path = os.path.join(temp_dir, onnx_filename_in_repo)\n",
|
| 588 |
+
"\n",
|
| 589 |
+
" print(\"Exporting ResNet-18 Model to temporary FP32 ONNX graph...\")\n",
|
| 590 |
+
" \n",
|
| 591 |
+
" with warnings.catch_warnings():\n",
|
| 592 |
+
" warnings.simplefilter(\"ignore\")\n",
|
| 593 |
+
" torch.onnx.export(\n",
|
| 594 |
+
" model,\n",
|
| 595 |
+
" dummy_input,\n",
|
| 596 |
+
" temp_onnx_path,\n",
|
| 597 |
+
" export_params=True,\n",
|
| 598 |
+
" opset_version=18,\n",
|
| 599 |
+
" do_constant_folding=True,\n",
|
| 600 |
+
" input_names=[\"pixel_values\"],\n",
|
| 601 |
+
" output_names=[\"logits\"],\n",
|
| 602 |
+
" dynamic_axes={\n",
|
| 603 |
+
" \"pixel_values\": {0: \"batch_size\"},\n",
|
| 604 |
+
" \"logits\": {0: \"batch_size\"},\n",
|
| 605 |
+
" },\n",
|
| 606 |
+
" dynamo=False,\n",
|
| 607 |
+
" )\n",
|
| 608 |
+
" print(\"✅ Base ONNX export successful.\")\n",
|
| 609 |
+
"\n",
|
| 610 |
+
" # Convert ONNX graph weights to FP16 to reduce file size\n",
|
| 611 |
+
" print(\"Compressing ONNX model to FP16 precision...\")\n",
|
| 612 |
+
" onnx_model = onnx.load(temp_onnx_path)\n",
|
| 613 |
+
" onnx_model_fp16 = float16.convert_float_to_float16(onnx_model, keep_io_types=True)\n",
|
| 614 |
+
" onnx.save(onnx_model_fp16, temp_onnx_path)\n",
|
| 615 |
+
" print(\"✅ ONNX graph compressed to FP16 successfully.\")\n",
|
| 616 |
+
"\n",
|
| 617 |
+
" # Upload directly to Hugging Face Hub\n",
|
| 618 |
+
" api = HfApi(token=hf_token)\n",
|
| 619 |
+
" print(f\"Uploading '{onnx_filename_in_repo}' to Hugging Face repo: '{repo_id}'...\")\n",
|
| 620 |
+
" \n",
|
| 621 |
+
" api.upload_file(\n",
|
| 622 |
+
" path_or_fileobj=temp_onnx_path,\n",
|
| 623 |
+
" path_in_repo=onnx_filename_in_repo,\n",
|
| 624 |
+
" repo_id=repo_id,\n",
|
| 625 |
+
" repo_type=\"model\",\n",
|
| 626 |
+
" )\n",
|
| 627 |
+
" print(f\"🚀 Successfully uploaded FP16 ONNX model to https://huggingface.co/{repo_id}/blob/main/{onnx_filename_in_repo}\")\n",
|
| 628 |
+
"\n",
|
| 629 |
+
"\n",
|
| 630 |
+
"# ==========================================\n",
|
| 631 |
+
"# 3. BENCHMARK ON REAL TEST IMAGES\n",
|
| 632 |
+
"# ==========================================\n",
|
| 633 |
+
"def benchmark_resnet_with_folder(\n",
|
| 634 |
+
" pytorch_model: torch.nn.Module,\n",
|
| 635 |
+
" repo_id: str,\n",
|
| 636 |
+
" onnx_filename_in_repo: str,\n",
|
| 637 |
+
" image_folder: str = r\"D:\\DamageLensAI\\test_images\",\n",
|
| 638 |
+
" hf_token: str = None,\n",
|
| 639 |
+
"):\n",
|
| 640 |
+
" \"\"\"\n",
|
| 641 |
+
" Downloads ONNX model from Hugging Face Hub and evaluates PyTorch vs ONNX Runtime performance.\n",
|
| 642 |
+
" \"\"\"\n",
|
| 643 |
+
" print(f\"Downloading '{onnx_filename_in_repo}' from Hugging Face for evaluation...\")\n",
|
| 644 |
+
" onnx_path = hf_hub_download(\n",
|
| 645 |
+
" repo_id=repo_id, \n",
|
| 646 |
+
" filename=onnx_filename_in_repo,\n",
|
| 647 |
+
" token=hf_token\n",
|
| 648 |
+
" )\n",
|
| 649 |
+
" session = ort.InferenceSession(onnx_path, providers=[\"CPUExecutionProvider\"])\n",
|
| 650 |
+
"\n",
|
| 651 |
+
" supported_exts = (\"*.jpg\", \"*.jpeg\", \"*.png\", \"*.bmp\", \"*.webp\")\n",
|
| 652 |
+
" image_paths = []\n",
|
| 653 |
+
" for ext in supported_exts:\n",
|
| 654 |
+
" image_paths.extend(glob.glob(os.path.join(image_folder, ext)))\n",
|
| 655 |
+
"\n",
|
| 656 |
+
" if not image_paths:\n",
|
| 657 |
+
" raise FileNotFoundError(\n",
|
| 658 |
+
" f\"❌ No valid images found in folder '{image_folder}'. \"\n",
|
| 659 |
+
" f\"Please verify your path.\"\n",
|
| 660 |
+
" )\n",
|
| 661 |
+
"\n",
|
| 662 |
+
" print(f\"📸 Found {len(image_paths)} images in '{image_folder}'. Starting benchmark...\")\n",
|
| 663 |
+
"\n",
|
| 664 |
+
" pytorch_latencies = []\n",
|
| 665 |
+
" onnx_latencies = []\n",
|
| 666 |
+
" matches = 0\n",
|
| 667 |
+
"\n",
|
| 668 |
+
" # Warmup run\n",
|
| 669 |
+
" first_img = Image.open(sorted(image_paths)[0]).convert(\"RGB\")\n",
|
| 670 |
+
" warmup_tensor = preprocess_image(first_img)\n",
|
| 671 |
+
" warmup_np = warmup_tensor.numpy()\n",
|
| 672 |
+
"\n",
|
| 673 |
+
" pytorch_model.eval()\n",
|
| 674 |
+
" with torch.no_grad():\n",
|
| 675 |
+
" _ = pytorch_model(warmup_tensor)\n",
|
| 676 |
+
" session.run(None, {\"pixel_values\": warmup_np})\n",
|
| 677 |
+
"\n",
|
| 678 |
+
" # Benchmark Loop\n",
|
| 679 |
+
" print(\"\\n\" + \"=\" * 75)\n",
|
| 680 |
+
" print(f\"{'IMAGE NAME':<25} | {'PYTORCH (ms)':<14} | {'ONNX (ms)':<14} | {'MATCH?':<10}\")\n",
|
| 681 |
+
" print(\"=\" * 75)\n",
|
| 682 |
+
"\n",
|
| 683 |
+
" for img_path in sorted(image_paths):\n",
|
| 684 |
+
" file_name = os.path.basename(img_path)\n",
|
| 685 |
+
" raw_img = Image.open(img_path).convert(\"RGB\")\n",
|
| 686 |
+
" \n",
|
| 687 |
+
" img_tensor = preprocess_image(raw_img)\n",
|
| 688 |
+
" np_input = img_tensor.numpy()\n",
|
| 689 |
+
"\n",
|
| 690 |
+
" # --- PyTorch Inference ---\n",
|
| 691 |
+
" start_pt = time.perf_counter()\n",
|
| 692 |
+
" with torch.no_grad():\n",
|
| 693 |
+
" pt_logits = pytorch_model(img_tensor)\n",
|
| 694 |
+
" pt_time = (time.perf_counter() - start_pt) * 1000\n",
|
| 695 |
+
" pytorch_latencies.append(pt_time)\n",
|
| 696 |
+
" pt_pred = int(torch.argmax(pt_logits, dim=1)[0])\n",
|
| 697 |
+
"\n",
|
| 698 |
+
" # --- ONNX Runtime Inference ---\n",
|
| 699 |
+
" start_onnx = time.perf_counter()\n",
|
| 700 |
+
" onnx_outputs = session.run(None, {\"pixel_values\": np_input})\n",
|
| 701 |
+
" onnx_time = (time.perf_counter() - start_onnx) * 1000\n",
|
| 702 |
+
" onnx_latencies.append(onnx_time)\n",
|
| 703 |
+
" onnx_pred = int(np.argmax(onnx_outputs[0], axis=1)[0])\n",
|
| 704 |
+
"\n",
|
| 705 |
+
" is_match = pt_pred == onnx_pred\n",
|
| 706 |
+
" if is_match:\n",
|
| 707 |
+
" matches += 1\n",
|
| 708 |
+
" match_str = \"✅ YES\" if is_match else \"❌ NO\"\n",
|
| 709 |
+
"\n",
|
| 710 |
+
" print(f\"{file_name:<25} | {pt_time:<14.2f} | {onnx_time:<14.2f} | {match_str:<10}\")\n",
|
| 711 |
+
"\n",
|
| 712 |
+
" avg_pt = np.mean(pytorch_latencies)\n",
|
| 713 |
+
" avg_onnx = np.mean(onnx_latencies)\n",
|
| 714 |
+
" speedup = ((avg_pt - avg_onnx) / avg_pt) * 100\n",
|
| 715 |
+
" accuracy_match_pct = (matches / len(image_paths)) * 100\n",
|
| 716 |
+
"\n",
|
| 717 |
+
" print(\"=\" * 75)\n",
|
| 718 |
+
" print(\" BENCHMARK SUMMARY\")\n",
|
| 719 |
+
" print(\"=\" * 75)\n",
|
| 720 |
+
" print(f\"Total Images Evaluated : {len(image_paths)}\")\n",
|
| 721 |
+
" print(f\"Prediction Match Rate : {accuracy_match_pct:.2f}% ({matches}/{len(image_paths)})\")\n",
|
| 722 |
+
" print(f\"PyTorch Avg Latency : {avg_pt:.2f} ms / image\")\n",
|
| 723 |
+
" print(f\"ONNX Runtime Avg Latency: {avg_onnx:.2f} ms / image\")\n",
|
| 724 |
+
" print(f\"Performance Gain : {speedup:.2f}% faster with ONNX Runtime\")\n",
|
| 725 |
+
" print(\"=\" * 75 + \"\\n\")\n",
|
| 726 |
+
"\n",
|
| 727 |
+
"\n",
|
| 728 |
+
"# ==========================================\n",
|
| 729 |
+
"# MAIN EXECUTION FLOW\n",
|
| 730 |
+
"# ==========================================\n",
|
| 731 |
+
"if __name__ == \"__main__\":\n",
|
| 732 |
+
" # Load token from .env\n",
|
| 733 |
+
" load_dotenv()\n",
|
| 734 |
+
" HF_TOKEN = os.getenv(\"HF_TOKEN\")\n",
|
| 735 |
+
"\n",
|
| 736 |
+
" if not HF_TOKEN:\n",
|
| 737 |
+
" print(\"⚠️ Warning: HF_TOKEN not found in .env file. Pushing/pulling private repos may fail.\")\n",
|
| 738 |
+
"\n",
|
| 739 |
+
" NUM_CLASSES = 6\n",
|
| 740 |
+
" HF_REPO_ID = \"junaid17/car-damage-classifier\"\n",
|
| 741 |
+
" MODEL_FILENAME = \"car-damage-classifier.pt\"\n",
|
| 742 |
+
" ONNX_FILENAME = \"car-damage-classifier.onnx\"\n",
|
| 743 |
+
" TEST_IMAGE_FOLDER = r\"D:\\DamageLensAI\\test_images\"\n",
|
| 744 |
+
"\n",
|
| 745 |
+
" # Step 1: Load PyTorch ResNet Model from Hugging Face\n",
|
| 746 |
+
" py_model = load_resnet_model_from_hf(\n",
|
| 747 |
+
" repo_id=HF_REPO_ID,\n",
|
| 748 |
+
" filename=MODEL_FILENAME,\n",
|
| 749 |
+
" num_classes=NUM_CLASSES,\n",
|
| 750 |
+
" device=\"cpu\",\n",
|
| 751 |
+
" hf_token=HF_TOKEN,\n",
|
| 752 |
+
" )\n",
|
| 753 |
+
"\n",
|
| 754 |
+
" # Step 2: Convert to FP16 ONNX & Push directly to Hugging Face\n",
|
| 755 |
+
" convert_and_upload_onnx_fp16(\n",
|
| 756 |
+
" model=py_model,\n",
|
| 757 |
+
" repo_id=HF_REPO_ID,\n",
|
| 758 |
+
" onnx_filename_in_repo=ONNX_FILENAME,\n",
|
| 759 |
+
" hf_token=HF_TOKEN,\n",
|
| 760 |
+
" )\n",
|
| 761 |
+
"\n",
|
| 762 |
+
" # Step 3: Run benchmarking on test image folder\n",
|
| 763 |
+
" benchmark_resnet_with_folder(\n",
|
| 764 |
+
" pytorch_model=py_model,\n",
|
| 765 |
+
" repo_id=HF_REPO_ID,\n",
|
| 766 |
+
" onnx_filename_in_repo=ONNX_FILENAME,\n",
|
| 767 |
+
" image_folder=TEST_IMAGE_FOLDER,\n",
|
| 768 |
+
" hf_token=HF_TOKEN,\n",
|
| 769 |
+
" )"
|
| 770 |
+
]
|
| 771 |
+
},
|
| 772 |
+
{
|
| 773 |
+
"cell_type": "markdown",
|
| 774 |
+
"id": "0c387aa2",
|
| 775 |
+
"metadata": {},
|
| 776 |
+
"source": [
|
| 777 |
+
"# YOLO Model Conversion"
|
| 778 |
+
]
|
| 779 |
+
},
|
| 780 |
+
{
|
| 781 |
+
"cell_type": "code",
|
| 782 |
+
"execution_count": 11,
|
| 783 |
+
"id": "f0e5e525",
|
| 784 |
+
"metadata": {},
|
| 785 |
+
"outputs": [
|
| 786 |
+
{
|
| 787 |
+
"name": "stdout",
|
| 788 |
+
"output_type": "stream",
|
| 789 |
+
"text": [
|
| 790 |
+
"Downloading YOLO checkpoint 'damage_detector.pt' from Hugging Face repo 'junaid17/Yolo_Model'...\n",
|
| 791 |
+
"✅ YOLO model loaded successfully.\n",
|
| 792 |
+
"Exporting YOLO11 Model directly to FP16 ONNX format (imgsz=640)...\n",
|
| 793 |
+
"WARNING 'half' is deprecated and will be removed in the future. Use 'quantize' instead.\n",
|
| 794 |
+
"Ultralytics 8.4.116 Python-3.11.0 torch-2.13.0+cpu CPU (12th Gen Intel Core i3-1215U)\n",
|
| 795 |
+
"YOLO11l summary (fused): 190 layers, 25,280,083 parameters, 0 gradients, 86.8 GFLOPs\n",
|
| 796 |
+
"\n",
|
| 797 |
+
"\u001b[34m\u001b[1mPyTorch:\u001b[0m starting from 'C:\\Users\\junai\\.cache\\huggingface\\hub\\models--junaid17--Yolo_Model\\snapshots\\0cbc6f112ebc6cb7df05fe93f3f0175cc847bec0\\damage_detector.pt' with input shape (1, 3, 640, 640) BCHW and output shape(s) (1, 5, 8400) (48.8 MB)\n",
|
| 798 |
+
"\n",
|
| 799 |
+
"\u001b[34m\u001b[1mONNX:\u001b[0m starting export with onnx 1.22.0 opset 17...\n",
|
| 800 |
+
"\u001b[34m\u001b[1mONNX:\u001b[0m slimming with onnxslim 0.1.95...\n",
|
| 801 |
+
"\u001b[34m\u001b[1mONNX:\u001b[0m converting to FP16...\n",
|
| 802 |
+
"\u001b[34m\u001b[1mONNX:\u001b[0m export success 7.1s, saved as 'C:\\Users\\junai\\.cache\\huggingface\\hub\\models--junaid17--Yolo_Model\\snapshots\\0cbc6f112ebc6cb7df05fe93f3f0175cc847bec0\\damage_detector.onnx' (48.5 MB)\n",
|
| 803 |
+
"\n",
|
| 804 |
+
"Export complete (11.6s)\n",
|
| 805 |
+
"Results saved to \u001b[1mC:\\Users\\junai\\.cache\\huggingface\\hub\\models--junaid17--Yolo_Model\\snapshots\\0cbc6f112ebc6cb7df05fe93f3f0175cc847bec0\\damage_detector.onnx\u001b[0m\n",
|
| 806 |
+
"Predict: yolo predict task=detect model=C:\\Users\\junai\\.cache\\huggingface\\hub\\models--junaid17--Yolo_Model\\snapshots\\0cbc6f112ebc6cb7df05fe93f3f0175cc847bec0\\damage_detector.onnx imgsz=640 quantize=16\n",
|
| 807 |
+
"Validate: yolo val task=detect model=C:\\Users\\junai\\.cache\\huggingface\\hub\\models--junaid17--Yolo_Model\\snapshots\\0cbc6f112ebc6cb7df05fe93f3f0175cc847bec0\\damage_detector.onnx imgsz=640 data=/content/drive/MyDrive/custom_data/dataset_custom.yaml quantize=16 \n",
|
| 808 |
+
"Visualize: https://netron.app\n",
|
| 809 |
+
"✅ FP16 ONNX export successful.\n",
|
| 810 |
+
"📦 Model File Size: 48.50 MB\n",
|
| 811 |
+
"Uploading 'damage_detector.onnx' to Hugging Face repo: 'junaid17/Yolo_Model'...\n"
|
| 812 |
+
]
|
| 813 |
+
},
|
| 814 |
+
{
|
| 815 |
+
"name": "stderr",
|
| 816 |
+
"output_type": "stream",
|
| 817 |
+
"text": [
|
| 818 |
+
"Processing Files (1 / 1): 100%|██████████| 50.9MB / 50.9MB, 2.79MB/s \n",
|
| 819 |
+
"New Data Upload: 100%|██████████| 8.84MB / 8.84MB, 568kB/s \n"
|
| 820 |
+
]
|
| 821 |
+
},
|
| 822 |
+
{
|
| 823 |
+
"name": "stdout",
|
| 824 |
+
"output_type": "stream",
|
| 825 |
+
"text": [
|
| 826 |
+
"🚀 Successfully uploaded ONNX model to https://huggingface.co/junaid17/Yolo_Model/blob/main/damage_detector.onnx\n",
|
| 827 |
+
"Downloading 'damage_detector.onnx' from Hugging Face for evaluation...\n",
|
| 828 |
+
"📸 Found 60 images in 'D:\\DamageLensAI\\test_images'. Starting benchmark...\n",
|
| 829 |
+
"\n",
|
| 830 |
+
"===========================================================================\n",
|
| 831 |
+
"IMAGE NAME | PYTORCH (ms) | ONNX (ms) | MATCH? \n",
|
| 832 |
+
"===========================================================================\n",
|
| 833 |
+
"FB_10.jpg | 1961.16 | 933.21 | ✅ YES \n",
|
| 834 |
+
"FB_11.jpg | 1645.58 | 821.06 | ✅ YES \n",
|
| 835 |
+
"FB_13.jpg | 1973.07 | 884.90 | ✅ YES \n",
|
| 836 |
+
"FB_15.jpg | 2296.55 | 1064.76 | ✅ YES \n",
|
| 837 |
+
"FB_16.jpg | 1159.60 | 679.19 | ✅ YES \n",
|
| 838 |
+
"FB_19.jpg | 1310.77 | 729.79 | ✅ YES \n",
|
| 839 |
+
"FB_20.jpg | 1263.96 | 678.22 | ✅ YES \n",
|
| 840 |
+
"FB_4.jpg | 1385.82 | 658.67 | ✅ YES \n",
|
| 841 |
+
"FB_5.jpg | 1352.15 | 665.79 | ✅ YES \n",
|
| 842 |
+
"FB_9.jpg | 1281.14 | 654.91 | ✅ YES \n",
|
| 843 |
+
"FC_1.jpg | 1430.82 | 639.33 | ✅ YES \n",
|
| 844 |
+
"FC_10.jpg | 1223.63 | 772.76 | ✅ YES \n",
|
| 845 |
+
"FC_11.jpg | 1229.15 | 665.59 | ✅ YES \n",
|
| 846 |
+
"FC_12.jpg | 1442.46 | 654.07 | ✅ YES \n",
|
| 847 |
+
"FC_3.jpg | 1255.18 | 635.24 | ✅ YES \n",
|
| 848 |
+
"FC_4.jpg | 1381.86 | 646.25 | ✅ YES \n",
|
| 849 |
+
"FC_6.jpg | 1380.10 | 644.81 | ✅ YES \n",
|
| 850 |
+
"FC_7.jpg | 1211.48 | 644.08 | ✅ YES \n",
|
| 851 |
+
"FC_8.jpg | 1201.65 | 761.60 | ✅ YES \n",
|
| 852 |
+
"FC_9.jpg | 1429.97 | 644.52 | ✅ YES \n",
|
| 853 |
+
"FN_1.jpg | 1181.08 | 649.43 | ✅ YES \n",
|
| 854 |
+
"FN_10.jpg | 1190.17 | 637.96 | ✅ YES \n",
|
| 855 |
+
"FN_11.jpg | 1590.08 | 764.77 | ✅ YES \n",
|
| 856 |
+
"FN_12.jpg | 1300.76 | 601.92 | ✅ YES \n",
|
| 857 |
+
"FN_3.jpg | 1182.89 | 662.93 | ✅ YES \n",
|
| 858 |
+
"FN_4.jpg | 1638.14 | 650.37 | ✅ YES \n",
|
| 859 |
+
"FN_5.jpg | 1426.86 | 657.21 | ✅ YES \n",
|
| 860 |
+
"FN_7.jpg | 1440.60 | 717.18 | ✅ YES \n",
|
| 861 |
+
"FN_8.jpg | 1385.24 | 621.54 | ✅ YES \n",
|
| 862 |
+
"FN_9.jpg | 1377.56 | 643.62 | ✅ YES \n",
|
| 863 |
+
"RB_10.jpg | 1362.92 | 694.52 | ✅ YES \n",
|
| 864 |
+
"RB_11.jpg | 1352.90 | 630.82 | ✅ YES \n",
|
| 865 |
+
"RB_13.jpg | 1373.38 | 684.08 | ✅ YES \n",
|
| 866 |
+
"RB_14.jpg | 1395.17 | 693.95 | ✅ YES \n",
|
| 867 |
+
"RB_2.jpg | 1323.94 | 619.14 | ✅ YES \n",
|
| 868 |
+
"RB_3.jpg | 1204.51 | 657.64 | ✅ YES \n",
|
| 869 |
+
"RB_4.jpg | 1468.20 | 673.22 | ✅ YES \n",
|
| 870 |
+
"RB_6.jpg | 1317.30 | 635.59 | ✅ YES \n",
|
| 871 |
+
"RB_7.jpg | 1384.84 | 639.61 | ✅ YES \n",
|
| 872 |
+
"RB_8.jpg | 1476.26 | 668.84 | ✅ YES \n",
|
| 873 |
+
"RC_10.jpg | 1292.19 | 650.32 | ✅ YES \n",
|
| 874 |
+
"RC_11.jpg | 1374.14 | 667.15 | ✅ YES \n",
|
| 875 |
+
"RC_2.jpg | 1403.93 | 776.57 | ✅ YES \n",
|
| 876 |
+
"RC_3.jpg | 1314.82 | 644.49 | ✅ YES \n",
|
| 877 |
+
"RC_4.jpg | 1283.66 | 626.54 | ✅ YES \n",
|
| 878 |
+
"RC_5.jpg | 1343.69 | 682.90 | ✅ YES \n",
|
| 879 |
+
"RC_6.jpg | 1348.45 | 603.56 | ✅ YES \n",
|
| 880 |
+
"RC_7.jpg | 1229.81 | 623.58 | ✅ YES \n",
|
| 881 |
+
"RC_8.jpg | 1312.53 | 670.69 | ✅ YES \n",
|
| 882 |
+
"RC_9.jpg | 1196.52 | 713.91 | ✅ YES \n",
|
| 883 |
+
"RN_1.jpg | 1125.28 | 619.14 | ✅ YES \n",
|
| 884 |
+
"RN_10.jpg | 1177.20 | 745.49 | ✅ YES \n",
|
| 885 |
+
"RN_11.jpg | 1377.85 | 632.45 | ✅ YES \n",
|
| 886 |
+
"RN_12.jpg | 1271.99 | 611.52 | ✅ YES \n",
|
| 887 |
+
"RN_2.jpg | 1166.40 | 715.83 | ✅ YES \n",
|
| 888 |
+
"RN_3.jpg | 1209.96 | 602.54 | ✅ YES \n",
|
| 889 |
+
"RN_4.jpg | 1129.91 | 696.41 | ✅ YES \n",
|
| 890 |
+
"RN_7.jpg | 1165.09 | 642.48 | ✅ YES \n",
|
| 891 |
+
"RN_8.jpg | 1327.08 | 600.25 | ✅ YES \n",
|
| 892 |
+
"RN_9.jpg | 1123.79 | 609.49 | ✅ YES \n",
|
| 893 |
+
"===========================================================================\n",
|
| 894 |
+
" BENCHMARK SUMMARY\n",
|
| 895 |
+
"===========================================================================\n",
|
| 896 |
+
"Total Images Evaluated : 60\n",
|
| 897 |
+
"Detection Match Rate : 100.00% (60/60)\n",
|
| 898 |
+
"PyTorch Avg Latency : 1356.05 ms / image\n",
|
| 899 |
+
"ONNX Runtime Avg Latency: 680.31 ms / image\n",
|
| 900 |
+
"Performance Gain : 49.83% faster with ONNX Runtime\n",
|
| 901 |
+
"===========================================================================\n",
|
| 902 |
+
"\n"
|
| 903 |
+
]
|
| 904 |
+
}
|
| 905 |
+
],
|
| 906 |
+
"source": [
|
| 907 |
+
"import os\n",
|
| 908 |
+
"import glob\n",
|
| 909 |
+
"import time\n",
|
| 910 |
+
"import warnings\n",
|
| 911 |
+
"import torch\n",
|
| 912 |
+
"import torchvision.transforms as transforms\n",
|
| 913 |
+
"import onnxruntime as ort\n",
|
| 914 |
+
"import numpy as np\n",
|
| 915 |
+
"from PIL import Image\n",
|
| 916 |
+
"from dotenv import load_dotenv\n",
|
| 917 |
+
"from huggingface_hub import HfApi, hf_hub_download\n",
|
| 918 |
+
"from ultralytics import YOLO\n",
|
| 919 |
+
"\n",
|
| 920 |
+
"# Import YOLO loader\n",
|
| 921 |
+
"from base_model_yolo import load_yolo_model_from_hf\n",
|
| 922 |
+
"\n",
|
| 923 |
+
"# Suppress warnings\n",
|
| 924 |
+
"warnings.filterwarnings(\"ignore\")\n",
|
| 925 |
+
"\n",
|
| 926 |
+
"# ==========================================\n",
|
| 927 |
+
"# 1. PREPROCESSING PIPELINE\n",
|
| 928 |
+
"# ==========================================\n",
|
| 929 |
+
"IMG_SIZE = 640\n",
|
| 930 |
+
"\n",
|
| 931 |
+
"yolo_transforms = transforms.Compose([\n",
|
| 932 |
+
" transforms.Resize((IMG_SIZE, IMG_SIZE)),\n",
|
| 933 |
+
" transforms.ToTensor(),\n",
|
| 934 |
+
"])\n",
|
| 935 |
+
"\n",
|
| 936 |
+
"def preprocess_image(pil_image: Image.Image) -> torch.Tensor:\n",
|
| 937 |
+
" \"\"\"Preprocesses PIL image for YOLO11 (1, 3, 640, 640).\"\"\"\n",
|
| 938 |
+
" return yolo_transforms(pil_image).unsqueeze(0)\n",
|
| 939 |
+
"\n",
|
| 940 |
+
"\n",
|
| 941 |
+
"# ==========================================\n",
|
| 942 |
+
"# 2. CONVERT, OPTIMIZE TO FP16 & UPLOAD\n",
|
| 943 |
+
"# ==========================================\n",
|
| 944 |
+
"def convert_and_upload_yolo_onnx(\n",
|
| 945 |
+
" yolo_model: YOLO,\n",
|
| 946 |
+
" repo_id: str,\n",
|
| 947 |
+
" onnx_filename_in_repo: str = \"damage_detector.onnx\",\n",
|
| 948 |
+
" hf_token: str = None,\n",
|
| 949 |
+
" imgsz: int = 640,\n",
|
| 950 |
+
"):\n",
|
| 951 |
+
" print(f\"Exporting YOLO11 Model directly to FP16 ONNX format (imgsz={imgsz})...\")\n",
|
| 952 |
+
" \n",
|
| 953 |
+
" # Native Ultralytics FP16 export - lightning fast and avoids converter bugs\n",
|
| 954 |
+
" exported_onnx_path = yolo_model.export(\n",
|
| 955 |
+
" format=\"onnx\",\n",
|
| 956 |
+
" imgsz=imgsz,\n",
|
| 957 |
+
" half=True, # Shrinks the model to ~50MB natively\n",
|
| 958 |
+
" dynamic=False, # Disabling dynamic fixes the CPU type-mismatch errors\n",
|
| 959 |
+
" simplify=True,\n",
|
| 960 |
+
" opset=17\n",
|
| 961 |
+
" )\n",
|
| 962 |
+
" print(\"✅ FP16 ONNX export successful.\")\n",
|
| 963 |
+
"\n",
|
| 964 |
+
" file_size_mb = os.path.getsize(exported_onnx_path) / (1024 * 1024)\n",
|
| 965 |
+
" print(f\"📦 Model File Size: {file_size_mb:.2f} MB\")\n",
|
| 966 |
+
"\n",
|
| 967 |
+
" # Upload to Hugging Face Hub\n",
|
| 968 |
+
" api = HfApi(token=hf_token)\n",
|
| 969 |
+
" print(f\"Uploading '{onnx_filename_in_repo}' to Hugging Face repo: '{repo_id}'...\")\n",
|
| 970 |
+
" \n",
|
| 971 |
+
" api.upload_file(\n",
|
| 972 |
+
" path_or_fileobj=exported_onnx_path,\n",
|
| 973 |
+
" path_in_repo=onnx_filename_in_repo,\n",
|
| 974 |
+
" repo_id=repo_id,\n",
|
| 975 |
+
" repo_type=\"model\",\n",
|
| 976 |
+
" )\n",
|
| 977 |
+
" print(f\"🚀 Successfully uploaded ONNX model to https://huggingface.co/{repo_id}/blob/main/{onnx_filename_in_repo}\")\n",
|
| 978 |
+
"\n",
|
| 979 |
+
"\n",
|
| 980 |
+
"# ==========================================\n",
|
| 981 |
+
"# 3. BENCHMARK ON REAL TEST IMAGES\n",
|
| 982 |
+
"# ==========================================\n",
|
| 983 |
+
"def benchmark_yolo_with_folder(\n",
|
| 984 |
+
" yolo_model: YOLO,\n",
|
| 985 |
+
" repo_id: str,\n",
|
| 986 |
+
" onnx_filename_in_repo: str,\n",
|
| 987 |
+
" image_folder: str = r\"D:\\DamageLensAI\\test_images\",\n",
|
| 988 |
+
" hf_token: str = None,\n",
|
| 989 |
+
" conf_thresh: float = 0.25,\n",
|
| 990 |
+
"):\n",
|
| 991 |
+
" print(f\"Downloading '{onnx_filename_in_repo}' from Hugging Face for evaluation...\")\n",
|
| 992 |
+
" \n",
|
| 993 |
+
" # force_download=True ensures we grab the newly uploaded 50MB file\n",
|
| 994 |
+
" onnx_path = hf_hub_download(\n",
|
| 995 |
+
" repo_id=repo_id, \n",
|
| 996 |
+
" filename=onnx_filename_in_repo,\n",
|
| 997 |
+
" token=hf_token,\n",
|
| 998 |
+
" force_download=True \n",
|
| 999 |
+
" )\n",
|
| 1000 |
+
" \n",
|
| 1001 |
+
" session = ort.InferenceSession(onnx_path, providers=[\"CPUExecutionProvider\"])\n",
|
| 1002 |
+
" input_name = session.get_inputs()[0].name\n",
|
| 1003 |
+
"\n",
|
| 1004 |
+
" supported_exts = (\"*.jpg\", \"*.jpeg\", \"*.png\", \"*.bmp\", \"*.webp\")\n",
|
| 1005 |
+
" image_paths = []\n",
|
| 1006 |
+
" for ext in supported_exts:\n",
|
| 1007 |
+
" image_paths.extend(glob.glob(os.path.join(image_folder, ext)))\n",
|
| 1008 |
+
"\n",
|
| 1009 |
+
" if not image_paths:\n",
|
| 1010 |
+
" raise FileNotFoundError(f\"❌ No valid images found in folder '{image_folder}'.\")\n",
|
| 1011 |
+
"\n",
|
| 1012 |
+
" print(f\"📸 Found {len(image_paths)} images in '{image_folder}'. Starting benchmark...\")\n",
|
| 1013 |
+
"\n",
|
| 1014 |
+
" pytorch_latencies = []\n",
|
| 1015 |
+
" onnx_latencies = []\n",
|
| 1016 |
+
" matches = 0\n",
|
| 1017 |
+
"\n",
|
| 1018 |
+
" # Warmup\n",
|
| 1019 |
+
" first_img = Image.open(sorted(image_paths)[0]).convert(\"RGB\")\n",
|
| 1020 |
+
" warmup_tensor = preprocess_image(first_img)\n",
|
| 1021 |
+
" warmup_np = warmup_tensor.numpy()\n",
|
| 1022 |
+
"\n",
|
| 1023 |
+
" _ = yolo_model.model(warmup_tensor)\n",
|
| 1024 |
+
" session.run(None, {input_name: warmup_np})\n",
|
| 1025 |
+
"\n",
|
| 1026 |
+
" print(\"\\n\" + \"=\" * 75)\n",
|
| 1027 |
+
" print(f\"{'IMAGE NAME':<25} | {'PYTORCH (ms)':<14} | {'ONNX (ms)':<14} | {'MATCH?':<10}\")\n",
|
| 1028 |
+
" print(\"=\" * 75)\n",
|
| 1029 |
+
"\n",
|
| 1030 |
+
" for img_path in sorted(image_paths):\n",
|
| 1031 |
+
" file_name = os.path.basename(img_path)\n",
|
| 1032 |
+
" raw_img = Image.open(img_path).convert(\"RGB\")\n",
|
| 1033 |
+
" \n",
|
| 1034 |
+
" img_tensor = preprocess_image(raw_img)\n",
|
| 1035 |
+
" np_input = img_tensor.numpy()\n",
|
| 1036 |
+
"\n",
|
| 1037 |
+
" start_pt = time.perf_counter()\n",
|
| 1038 |
+
" with torch.no_grad():\n",
|
| 1039 |
+
" pt_out = yolo_model.model(img_tensor)\n",
|
| 1040 |
+
" pt_time = (time.perf_counter() - start_pt) * 1000\n",
|
| 1041 |
+
" pytorch_latencies.append(pt_time)\n",
|
| 1042 |
+
"\n",
|
| 1043 |
+
" start_onnx = time.perf_counter()\n",
|
| 1044 |
+
" onnx_out = session.run(None, {input_name: np_input})\n",
|
| 1045 |
+
" onnx_time = (time.perf_counter() - start_onnx) * 1000\n",
|
| 1046 |
+
" onnx_latencies.append(onnx_time)\n",
|
| 1047 |
+
"\n",
|
| 1048 |
+
" pt_preds = pt_out[0] if isinstance(pt_out, (list, tuple)) else pt_out\n",
|
| 1049 |
+
" pt_max_conf = float(torch.max(pt_preds[0, 4, :])) if pt_preds.ndim == 3 else 0.0\n",
|
| 1050 |
+
" \n",
|
| 1051 |
+
" onnx_preds = onnx_out[0]\n",
|
| 1052 |
+
" onnx_max_conf = float(np.max(onnx_preds[0, 4, :])) if onnx_preds.ndim == 3 else 0.0\n",
|
| 1053 |
+
"\n",
|
| 1054 |
+
" pt_detected = pt_max_conf >= conf_thresh\n",
|
| 1055 |
+
" onnx_detected = onnx_max_conf >= conf_thresh\n",
|
| 1056 |
+
" is_match = (pt_detected == onnx_detected)\n",
|
| 1057 |
+
"\n",
|
| 1058 |
+
" if is_match:\n",
|
| 1059 |
+
" matches += 1\n",
|
| 1060 |
+
" match_str = \"✅ YES\" if is_match else \"❌ NO\"\n",
|
| 1061 |
+
"\n",
|
| 1062 |
+
" print(f\"{file_name:<25} | {pt_time:<14.2f} | {onnx_time:<14.2f} | {match_str:<10}\")\n",
|
| 1063 |
+
"\n",
|
| 1064 |
+
" avg_pt = np.mean(pytorch_latencies)\n",
|
| 1065 |
+
" avg_onnx = np.mean(onnx_latencies)\n",
|
| 1066 |
+
" speedup = ((avg_pt - avg_onnx) / avg_pt) * 100\n",
|
| 1067 |
+
" accuracy_match_pct = (matches / len(image_paths)) * 100\n",
|
| 1068 |
+
"\n",
|
| 1069 |
+
" print(\"=\" * 75)\n",
|
| 1070 |
+
" print(\" BENCHMARK SUMMARY\")\n",
|
| 1071 |
+
" print(\"=\" * 75)\n",
|
| 1072 |
+
" print(f\"Total Images Evaluated : {len(image_paths)}\")\n",
|
| 1073 |
+
" print(f\"Detection Match Rate : {accuracy_match_pct:.2f}% ({matches}/{len(image_paths)})\")\n",
|
| 1074 |
+
" print(f\"PyTorch Avg Latency : {avg_pt:.2f} ms / image\")\n",
|
| 1075 |
+
" print(f\"ONNX Runtime Avg Latency: {avg_onnx:.2f} ms / image\")\n",
|
| 1076 |
+
" print(f\"Performance Gain : {speedup:.2f}% faster with ONNX Runtime\")\n",
|
| 1077 |
+
" print(\"=\" * 75 + \"\\n\")\n",
|
| 1078 |
+
"\n",
|
| 1079 |
+
"\n",
|
| 1080 |
+
"# ==========================================\n",
|
| 1081 |
+
"# MAIN EXECUTION FLOW\n",
|
| 1082 |
+
"# ==========================================\n",
|
| 1083 |
+
"if __name__ == \"__main__\":\n",
|
| 1084 |
+
" load_dotenv()\n",
|
| 1085 |
+
" HF_TOKEN = os.getenv(\"HF_TOKEN\")\n",
|
| 1086 |
+
"\n",
|
| 1087 |
+
" if not HF_TOKEN:\n",
|
| 1088 |
+
" print(\"⚠️ Warning: HF_TOKEN not found in .env file.\")\n",
|
| 1089 |
+
"\n",
|
| 1090 |
+
" HF_REPO_ID = \"junaid17/Yolo_Model\"\n",
|
| 1091 |
+
" MODEL_FILENAME = \"damage_detector.pt\"\n",
|
| 1092 |
+
" ONNX_FILENAME = \"damage_detector.onnx\"\n",
|
| 1093 |
+
" TEST_IMAGE_FOLDER = r\"D:\\DamageLensAI\\test_images\"\n",
|
| 1094 |
+
"\n",
|
| 1095 |
+
" yolo_model = load_yolo_model_from_hf(\n",
|
| 1096 |
+
" repo_id=HF_REPO_ID,\n",
|
| 1097 |
+
" filename=MODEL_FILENAME,\n",
|
| 1098 |
+
" hf_token=HF_TOKEN,\n",
|
| 1099 |
+
" )\n",
|
| 1100 |
+
"\n",
|
| 1101 |
+
" convert_and_upload_yolo_onnx(\n",
|
| 1102 |
+
" yolo_model=yolo_model,\n",
|
| 1103 |
+
" repo_id=HF_REPO_ID,\n",
|
| 1104 |
+
" onnx_filename_in_repo=ONNX_FILENAME,\n",
|
| 1105 |
+
" hf_token=HF_TOKEN,\n",
|
| 1106 |
+
" imgsz=IMG_SIZE,\n",
|
| 1107 |
+
" )\n",
|
| 1108 |
+
"\n",
|
| 1109 |
+
" benchmark_yolo_with_folder(\n",
|
| 1110 |
+
" yolo_model=yolo_model,\n",
|
| 1111 |
+
" repo_id=HF_REPO_ID,\n",
|
| 1112 |
+
" onnx_filename_in_repo=ONNX_FILENAME,\n",
|
| 1113 |
+
" image_folder=TEST_IMAGE_FOLDER,\n",
|
| 1114 |
+
" hf_token=HF_TOKEN,\n",
|
| 1115 |
+
" )"
|
| 1116 |
+
]
|
| 1117 |
+
},
|
| 1118 |
+
{
|
| 1119 |
+
"cell_type": "code",
|
| 1120 |
+
"execution_count": null,
|
| 1121 |
+
"id": "5b257cbc",
|
| 1122 |
+
"metadata": {},
|
| 1123 |
+
"outputs": [],
|
| 1124 |
+
"source": []
|
| 1125 |
+
},
|
| 1126 |
+
{
|
| 1127 |
+
"cell_type": "code",
|
| 1128 |
+
"execution_count": null,
|
| 1129 |
+
"id": "0e9c3a35",
|
| 1130 |
+
"metadata": {},
|
| 1131 |
+
"outputs": [],
|
| 1132 |
+
"source": []
|
| 1133 |
+
},
|
| 1134 |
+
{
|
| 1135 |
+
"cell_type": "code",
|
| 1136 |
+
"execution_count": null,
|
| 1137 |
+
"id": "8bed307b",
|
| 1138 |
+
"metadata": {},
|
| 1139 |
+
"outputs": [],
|
| 1140 |
+
"source": []
|
| 1141 |
+
},
|
| 1142 |
+
{
|
| 1143 |
+
"cell_type": "code",
|
| 1144 |
+
"execution_count": null,
|
| 1145 |
+
"metadata": {},
|
| 1146 |
+
"outputs": [],
|
| 1147 |
+
"source": []
|
| 1148 |
+
}
|
| 1149 |
+
],
|
| 1150 |
+
"metadata": {
|
| 1151 |
+
"kernelspec": {
|
| 1152 |
+
"display_name": "myvenv (3.11.0)",
|
| 1153 |
+
"language": "python",
|
| 1154 |
+
"name": "python3"
|
| 1155 |
+
},
|
| 1156 |
+
"language_info": {
|
| 1157 |
+
"codemirror_mode": {
|
| 1158 |
+
"name": "ipython",
|
| 1159 |
+
"version": 3
|
| 1160 |
+
},
|
| 1161 |
+
"file_extension": ".py",
|
| 1162 |
+
"mimetype": "text/x-python",
|
| 1163 |
+
"name": "python",
|
| 1164 |
+
"nbconvert_exporter": "python",
|
| 1165 |
+
"pygments_lexer": "ipython3",
|
| 1166 |
+
"version": "3.11.0"
|
| 1167 |
+
}
|
| 1168 |
+
},
|
| 1169 |
+
"nbformat": 4,
|
| 1170 |
+
"nbformat_minor": 5
|
| 1171 |
+
}
|
Notebooks/EfficientNet_ConvNext_Fusion.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
Notebooks/Resnet18_fine_tuning_final.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
Notebooks/damage_detector_yolo.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/config.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
# ---------------- PATHS ----------------
|
| 5 |
+
BASE_DIR = Path(__file__).resolve().parents[1]
|
| 6 |
+
|
| 7 |
+
DATASET_DIR = BASE_DIR / "data" / "dataset"
|
| 8 |
+
CHECKPOINT_DIR = BASE_DIR / "checkpoints"
|
| 9 |
+
EXPORT_DIR = BASE_DIR / "exports"
|
| 10 |
+
|
| 11 |
+
CHECKPOINT_DIR.mkdir(exist_ok=True)
|
| 12 |
+
EXPORT_DIR.mkdir(exist_ok=True)
|
| 13 |
+
|
| 14 |
+
# ---------------- TRAINING ----------------
|
| 15 |
+
BATCH_SIZE = 16
|
| 16 |
+
NUM_WORKERS = 4
|
| 17 |
+
LEARNING_RATE = 1e-4
|
| 18 |
+
WEIGHT_DECAY = 1e-5
|
| 19 |
+
VALIDATION_SPLIT = 0.2
|
| 20 |
+
RANDOM_SEED = 42
|
| 21 |
+
|
| 22 |
+
# TEMP DEV SETTING
|
| 23 |
+
EPOCHS = 1
|
| 24 |
+
|
| 25 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 26 |
+
|
| 27 |
+
# ---------------- IMAGE SIZES ----------------
|
| 28 |
+
RESNET_IMAGE_SIZE = 224
|
| 29 |
+
FUSION_IMAGE_SIZE = 260
|
| 30 |
+
YOLO_IMAGE_SIZE = 640
|
| 31 |
+
|
| 32 |
+
# ---------------- YOLO ----------------
|
| 33 |
+
YOLO_BASE_MODEL = "yolo11m.pt"
|
| 34 |
+
YOLO_BATCH_SIZE = 10
|
| 35 |
+
YOLO_EPOCHS = 1
|
| 36 |
+
YOLO_CONFIDENCE_THRESHOLD = 0.05
|
| 37 |
+
|
| 38 |
+
# ---------------- CLASSES ----------------
|
| 39 |
+
CLASS_NAMES = [
|
| 40 |
+
"F_Breakage",
|
| 41 |
+
"F_Crushed",
|
| 42 |
+
"F_Normal",
|
| 43 |
+
"R_Breakage",
|
| 44 |
+
"R_Crushed",
|
| 45 |
+
"R_Normal"
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
CLASS_MAP = {idx: cls for idx, cls in enumerate(CLASS_NAMES)}
|
| 51 |
+
CLASS_TO_IDX = {cls: idx for idx, cls in enumerate(CLASS_NAMES)}
|
| 52 |
+
|
| 53 |
+
NUM_CLASSES = len(CLASS_NAMES)
|
src/data/augmentation.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from torchvision import transforms
|
| 3 |
+
|
| 4 |
+
from src.config import RESNET_IMAGE_SIZE, FUSION_IMAGE_SIZE
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def get_resnet_train_transforms():
|
| 10 |
+
logger.info("Creating ResNet training transforms...")
|
| 11 |
+
|
| 12 |
+
return transforms.Compose([
|
| 13 |
+
transforms.Resize((RESNET_IMAGE_SIZE, RESNET_IMAGE_SIZE)),
|
| 14 |
+
transforms.RandomHorizontalFlip(),
|
| 15 |
+
transforms.RandomRotation(15),
|
| 16 |
+
transforms.ColorJitter(
|
| 17 |
+
brightness=0.2,
|
| 18 |
+
contrast=0.2,
|
| 19 |
+
saturation=0.2
|
| 20 |
+
),
|
| 21 |
+
transforms.ToTensor(),
|
| 22 |
+
transforms.Normalize(
|
| 23 |
+
mean=[0.485, 0.456, 0.406],
|
| 24 |
+
std=[0.229, 0.224, 0.225]
|
| 25 |
+
)
|
| 26 |
+
])
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def get_resnet_val_transforms():
|
| 30 |
+
logger.info("Creating ResNet validation transforms...")
|
| 31 |
+
|
| 32 |
+
return transforms.Compose([
|
| 33 |
+
transforms.Resize((RESNET_IMAGE_SIZE, RESNET_IMAGE_SIZE)),
|
| 34 |
+
transforms.ToTensor(),
|
| 35 |
+
transforms.Normalize(
|
| 36 |
+
mean=[0.485, 0.456, 0.406],
|
| 37 |
+
std=[0.229, 0.224, 0.225]
|
| 38 |
+
)
|
| 39 |
+
])
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def get_fusion_train_transforms():
|
| 43 |
+
logger.info("Creating Fusion training transforms...")
|
| 44 |
+
|
| 45 |
+
return transforms.Compose([
|
| 46 |
+
transforms.Resize((FUSION_IMAGE_SIZE, FUSION_IMAGE_SIZE)),
|
| 47 |
+
transforms.RandomHorizontalFlip(),
|
| 48 |
+
transforms.RandomRotation(10),
|
| 49 |
+
transforms.ColorJitter(
|
| 50 |
+
brightness=0.15,
|
| 51 |
+
contrast=0.15,
|
| 52 |
+
saturation=0.15
|
| 53 |
+
),
|
| 54 |
+
transforms.ToTensor(),
|
| 55 |
+
transforms.Normalize(
|
| 56 |
+
mean=[0.485, 0.456, 0.406],
|
| 57 |
+
std=[0.229, 0.224, 0.225]
|
| 58 |
+
)
|
| 59 |
+
])
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def get_fusion_val_transforms():
|
| 63 |
+
logger.info("Creating Fusion validation transforms...")
|
| 64 |
+
|
| 65 |
+
return transforms.Compose([
|
| 66 |
+
transforms.Resize((FUSION_IMAGE_SIZE, FUSION_IMAGE_SIZE)),
|
| 67 |
+
transforms.ToTensor(),
|
| 68 |
+
transforms.Normalize(
|
| 69 |
+
mean=[0.485, 0.456, 0.406],
|
| 70 |
+
std=[0.229, 0.224, 0.225]
|
| 71 |
+
)
|
| 72 |
+
])
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
if __name__ == "__main__":
|
| 76 |
+
logging.basicConfig(
|
| 77 |
+
level=logging.INFO,
|
| 78 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
resnet_train = get_resnet_train_transforms()
|
| 82 |
+
resnet_val = get_resnet_val_transforms()
|
| 83 |
+
fusion_train = get_fusion_train_transforms()
|
| 84 |
+
fusion_val = get_fusion_val_transforms()
|
| 85 |
+
|
| 86 |
+
print("\nTransforms created successfully:")
|
| 87 |
+
print("ResNet Train:", resnet_train)
|
| 88 |
+
print("ResNet Val:", resnet_val)
|
| 89 |
+
print("Fusion Train:", fusion_train)
|
| 90 |
+
print("Fusion Val:", fusion_val)
|
src/data/dataset.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from PIL import Image
|
| 3 |
+
from torch.utils.data import Dataset, DataLoader
|
| 4 |
+
from transformers import ConvNextImageProcessor
|
| 5 |
+
|
| 6 |
+
from src.config import (
|
| 7 |
+
BATCH_SIZE,
|
| 8 |
+
NUM_WORKERS
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
from src.data.ingestion import collect_image_paths
|
| 12 |
+
from src.data.preprocessing import split_dataset
|
| 13 |
+
from src.data.augmentation import (
|
| 14 |
+
get_resnet_train_transforms,
|
| 15 |
+
get_resnet_val_transforms,
|
| 16 |
+
get_fusion_train_transforms,
|
| 17 |
+
get_fusion_val_transforms
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ResNetDataset(Dataset):
|
| 24 |
+
def __init__(self, samples, transforms=None):
|
| 25 |
+
self.samples = samples
|
| 26 |
+
self.transforms = transforms
|
| 27 |
+
|
| 28 |
+
def __len__(self):
|
| 29 |
+
return len(self.samples)
|
| 30 |
+
|
| 31 |
+
def __getitem__(self, idx):
|
| 32 |
+
image_path, label = self.samples[idx]
|
| 33 |
+
|
| 34 |
+
image = Image.open(image_path).convert("RGB")
|
| 35 |
+
|
| 36 |
+
if self.transforms:
|
| 37 |
+
image = self.transforms(image)
|
| 38 |
+
|
| 39 |
+
return image, label
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class FusionDataset(Dataset):
|
| 43 |
+
def __init__(
|
| 44 |
+
self,
|
| 45 |
+
samples,
|
| 46 |
+
transforms=None,
|
| 47 |
+
convnext_model_name="facebook/convnext-small-224"
|
| 48 |
+
):
|
| 49 |
+
self.samples = samples
|
| 50 |
+
self.transforms = transforms
|
| 51 |
+
|
| 52 |
+
logger.info("Loading ConvNeXt processor...")
|
| 53 |
+
|
| 54 |
+
self.processor = ConvNextImageProcessor.from_pretrained(
|
| 55 |
+
convnext_model_name
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
def __len__(self):
|
| 59 |
+
return len(self.samples)
|
| 60 |
+
|
| 61 |
+
def __getitem__(self, idx):
|
| 62 |
+
image_path, label = self.samples[idx]
|
| 63 |
+
|
| 64 |
+
image = Image.open(image_path).convert("RGB")
|
| 65 |
+
|
| 66 |
+
if self.transforms:
|
| 67 |
+
eff_tensor = self.transforms(image)
|
| 68 |
+
else:
|
| 69 |
+
raise ValueError("Fusion transforms are required.")
|
| 70 |
+
|
| 71 |
+
convnext_inputs = self.processor(
|
| 72 |
+
images=image,
|
| 73 |
+
return_tensors="pt"
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
convnext_tensor = convnext_inputs["pixel_values"].squeeze(0)
|
| 77 |
+
|
| 78 |
+
return {
|
| 79 |
+
"pixel_values_eff": eff_tensor,
|
| 80 |
+
"pixel_values_cnx": convnext_tensor,
|
| 81 |
+
"labels": label
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def create_resnet_dataloaders():
|
| 86 |
+
logger.info("Creating ResNet dataloaders...")
|
| 87 |
+
|
| 88 |
+
samples = collect_image_paths()
|
| 89 |
+
train_data, val_data = split_dataset(samples)
|
| 90 |
+
|
| 91 |
+
train_dataset = ResNetDataset(
|
| 92 |
+
train_data,
|
| 93 |
+
transforms=get_resnet_train_transforms()
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
val_dataset = ResNetDataset(
|
| 97 |
+
val_data,
|
| 98 |
+
transforms=get_resnet_val_transforms()
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
train_loader = DataLoader(
|
| 102 |
+
train_dataset,
|
| 103 |
+
batch_size=BATCH_SIZE,
|
| 104 |
+
shuffle=True,
|
| 105 |
+
num_workers=NUM_WORKERS
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
val_loader = DataLoader(
|
| 109 |
+
val_dataset,
|
| 110 |
+
batch_size=BATCH_SIZE,
|
| 111 |
+
shuffle=False,
|
| 112 |
+
num_workers=NUM_WORKERS
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
logger.info("ResNet dataloaders created successfully.")
|
| 116 |
+
|
| 117 |
+
return train_loader, val_loader
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def create_fusion_dataloaders():
|
| 121 |
+
logger.info("Creating Fusion dataloaders...")
|
| 122 |
+
|
| 123 |
+
samples = collect_image_paths()
|
| 124 |
+
train_data, val_data = split_dataset(samples)
|
| 125 |
+
|
| 126 |
+
train_dataset = FusionDataset(
|
| 127 |
+
train_data,
|
| 128 |
+
transforms=get_fusion_train_transforms()
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
val_dataset = FusionDataset(
|
| 132 |
+
val_data,
|
| 133 |
+
transforms=get_fusion_val_transforms()
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
train_loader = DataLoader(
|
| 137 |
+
train_dataset,
|
| 138 |
+
batch_size=BATCH_SIZE,
|
| 139 |
+
shuffle=True,
|
| 140 |
+
num_workers=NUM_WORKERS
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
val_loader = DataLoader(
|
| 144 |
+
val_dataset,
|
| 145 |
+
batch_size=BATCH_SIZE,
|
| 146 |
+
shuffle=False,
|
| 147 |
+
num_workers=NUM_WORKERS
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
logger.info("Fusion dataloaders created successfully.")
|
| 151 |
+
|
| 152 |
+
return train_loader, val_loader
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
if __name__ == "__main__":
|
| 156 |
+
logging.basicConfig(
|
| 157 |
+
level=logging.INFO,
|
| 158 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
print("\nTesting ResNet dataloaders...\n")
|
| 162 |
+
|
| 163 |
+
train_loader, val_loader = create_resnet_dataloaders()
|
| 164 |
+
|
| 165 |
+
images, labels = next(iter(train_loader))
|
| 166 |
+
|
| 167 |
+
print("ResNet batch shape:", images.shape)
|
| 168 |
+
print("ResNet labels shape:", labels.shape)
|
| 169 |
+
|
| 170 |
+
print("\nTesting Fusion dataloaders...\n")
|
| 171 |
+
|
| 172 |
+
train_loader, val_loader = create_fusion_dataloaders()
|
| 173 |
+
|
| 174 |
+
batch = next(iter(train_loader))
|
| 175 |
+
|
| 176 |
+
print(
|
| 177 |
+
"Fusion EfficientNet batch shape:",
|
| 178 |
+
batch["pixel_values_eff"].shape
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
print(
|
| 182 |
+
"Fusion ConvNeXt batch shape:",
|
| 183 |
+
batch["pixel_values_cnx"].shape
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
print(
|
| 187 |
+
"Fusion labels shape:",
|
| 188 |
+
batch["labels"].shape
|
| 189 |
+
)
|
src/data/ingestion.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
from src.config import DATASET_DIR, CLASS_TO_IDX
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
VALID_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"}
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def collect_image_paths():
|
| 12 |
+
logger.info("Starting dataset ingestion...")
|
| 13 |
+
|
| 14 |
+
if not DATASET_DIR.exists():
|
| 15 |
+
raise FileNotFoundError(f"Dataset directory not found: {DATASET_DIR}")
|
| 16 |
+
|
| 17 |
+
samples = []
|
| 18 |
+
|
| 19 |
+
for class_name, label in CLASS_TO_IDX.items():
|
| 20 |
+
class_dir = DATASET_DIR / class_name
|
| 21 |
+
|
| 22 |
+
if not class_dir.exists():
|
| 23 |
+
logger.warning(f"Missing class folder: {class_dir}")
|
| 24 |
+
continue
|
| 25 |
+
|
| 26 |
+
image_count = 0
|
| 27 |
+
|
| 28 |
+
for image_path in class_dir.iterdir():
|
| 29 |
+
if image_path.suffix.lower() in VALID_EXTENSIONS:
|
| 30 |
+
samples.append((str(image_path), label))
|
| 31 |
+
image_count += 1
|
| 32 |
+
|
| 33 |
+
logger.info(f"{class_name}: {image_count} images found")
|
| 34 |
+
|
| 35 |
+
if not samples:
|
| 36 |
+
raise ValueError("No valid images found in dataset.")
|
| 37 |
+
|
| 38 |
+
logger.info(f"Total images collected: {len(samples)}")
|
| 39 |
+
|
| 40 |
+
return samples
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
if __name__ == "__main__":
|
| 44 |
+
logging.basicConfig(
|
| 45 |
+
level=logging.INFO,
|
| 46 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
data = collect_image_paths()
|
| 50 |
+
|
| 51 |
+
print(f"\nTotal samples: {len(data)}")
|
| 52 |
+
print("First 5 samples:")
|
| 53 |
+
|
| 54 |
+
for sample in data[:5]:
|
| 55 |
+
print(sample)
|
src/data/preprocessing.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from collections import Counter
|
| 3 |
+
from sklearn.model_selection import train_test_split
|
| 4 |
+
|
| 5 |
+
from src.config import VALIDATION_SPLIT, RANDOM_SEED
|
| 6 |
+
from src.data.ingestion import collect_image_paths
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def split_dataset(samples):
|
| 12 |
+
logger.info("Starting dataset preprocessing...")
|
| 13 |
+
|
| 14 |
+
if not samples:
|
| 15 |
+
raise ValueError("Empty dataset provided.")
|
| 16 |
+
|
| 17 |
+
image_paths = [sample[0] for sample in samples]
|
| 18 |
+
labels = [sample[1] for sample in samples]
|
| 19 |
+
|
| 20 |
+
logger.info(f"Total samples before split: {len(samples)}")
|
| 21 |
+
|
| 22 |
+
train_paths, val_paths, train_labels, val_labels = train_test_split(
|
| 23 |
+
image_paths,
|
| 24 |
+
labels,
|
| 25 |
+
test_size=VALIDATION_SPLIT,
|
| 26 |
+
stratify=labels,
|
| 27 |
+
random_state=RANDOM_SEED
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
train_data = list(zip(train_paths, train_labels))
|
| 31 |
+
val_data = list(zip(val_paths, val_labels))
|
| 32 |
+
|
| 33 |
+
logger.info(f"Training samples: {len(train_data)}")
|
| 34 |
+
logger.info(f"Validation samples: {len(val_data)}")
|
| 35 |
+
|
| 36 |
+
logger.info(f"Train distribution: {Counter(train_labels)}")
|
| 37 |
+
logger.info(f"Validation distribution: {Counter(val_labels)}")
|
| 38 |
+
|
| 39 |
+
return train_data, val_data
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
logging.basicConfig(
|
| 44 |
+
level=logging.INFO,
|
| 45 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
samples = collect_image_paths()
|
| 49 |
+
|
| 50 |
+
train_data, val_data = split_dataset(samples)
|
| 51 |
+
|
| 52 |
+
print("\nTrain sample preview:")
|
| 53 |
+
for sample in train_data[:5]:
|
| 54 |
+
print(sample)
|
| 55 |
+
|
| 56 |
+
print("\nValidation sample preview:")
|
| 57 |
+
for sample in val_data[:5]:
|
| 58 |
+
print(sample)
|
src/models/fusion_model.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
from torchvision import models
|
| 5 |
+
from transformers import ConvNextModel
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class FusionClassifier(nn.Module):
|
| 11 |
+
def __init__(
|
| 12 |
+
self,
|
| 13 |
+
num_classes,
|
| 14 |
+
convnext_model_name="facebook/convnext-small-224"
|
| 15 |
+
):
|
| 16 |
+
super().__init__()
|
| 17 |
+
|
| 18 |
+
logger.info("Initializing Fusion model...")
|
| 19 |
+
|
| 20 |
+
# EfficientNet-V2-S
|
| 21 |
+
eff = models.efficientnet_v2_s(
|
| 22 |
+
weights=models.EfficientNet_V2_S_Weights.IMAGENET1K_V1
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
for param in eff.parameters():
|
| 26 |
+
param.requires_grad = False
|
| 27 |
+
|
| 28 |
+
for param in eff.features[5].parameters():
|
| 29 |
+
param.requires_grad = True
|
| 30 |
+
|
| 31 |
+
for param in eff.features[6].parameters():
|
| 32 |
+
param.requires_grad = True
|
| 33 |
+
|
| 34 |
+
for param in eff.features[7].parameters():
|
| 35 |
+
param.requires_grad = True
|
| 36 |
+
|
| 37 |
+
self.eff_features = eff.features
|
| 38 |
+
self.eff_avgpool = eff.avgpool
|
| 39 |
+
self.eff_out_dim = eff.classifier[1].in_features
|
| 40 |
+
|
| 41 |
+
# ConvNeXt
|
| 42 |
+
cnx = ConvNextModel.from_pretrained(convnext_model_name)
|
| 43 |
+
|
| 44 |
+
for param in cnx.parameters():
|
| 45 |
+
param.requires_grad = False
|
| 46 |
+
|
| 47 |
+
for param in cnx.encoder.stages[2].parameters():
|
| 48 |
+
param.requires_grad = True
|
| 49 |
+
|
| 50 |
+
for param in cnx.encoder.stages[3].parameters():
|
| 51 |
+
param.requires_grad = True
|
| 52 |
+
|
| 53 |
+
for param in cnx.layernorm.parameters():
|
| 54 |
+
param.requires_grad = True
|
| 55 |
+
|
| 56 |
+
self.cnx_backbone = cnx
|
| 57 |
+
self.cnx_out_dim = 768
|
| 58 |
+
|
| 59 |
+
fused_dim = self.eff_out_dim + self.cnx_out_dim
|
| 60 |
+
|
| 61 |
+
self.fusion_head = nn.Sequential(
|
| 62 |
+
nn.Dropout(0.4),
|
| 63 |
+
nn.Linear(fused_dim, 512),
|
| 64 |
+
nn.LayerNorm(512),
|
| 65 |
+
nn.GELU(),
|
| 66 |
+
|
| 67 |
+
nn.Dropout(0.3),
|
| 68 |
+
nn.Linear(512, 256),
|
| 69 |
+
nn.LayerNorm(256),
|
| 70 |
+
nn.GELU(),
|
| 71 |
+
|
| 72 |
+
nn.Dropout(0.2),
|
| 73 |
+
nn.Linear(256, num_classes)
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
logger.info("Fusion model initialized successfully.")
|
| 77 |
+
|
| 78 |
+
def forward(self, pixel_values_eff, pixel_values_cnx):
|
| 79 |
+
x_eff = self.eff_features(pixel_values_eff)
|
| 80 |
+
x_eff = self.eff_avgpool(x_eff)
|
| 81 |
+
x_eff = torch.flatten(x_eff, 1)
|
| 82 |
+
|
| 83 |
+
cnx_out = self.cnx_backbone(
|
| 84 |
+
pixel_values=pixel_values_cnx,
|
| 85 |
+
return_dict=True
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
x_cnx = cnx_out.pooler_output
|
| 89 |
+
|
| 90 |
+
fused = torch.cat([x_eff, x_cnx], dim=1)
|
| 91 |
+
|
| 92 |
+
logits = self.fusion_head(fused)
|
| 93 |
+
|
| 94 |
+
return logits
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
import logging
|
| 99 |
+
|
| 100 |
+
logging.basicConfig(
|
| 101 |
+
level=logging.INFO,
|
| 102 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
model = FusionClassifier(num_classes=6)
|
| 106 |
+
|
| 107 |
+
eff_dummy = torch.randn(2, 3, 260, 260)
|
| 108 |
+
cnx_dummy = torch.randn(2, 3, 224, 224)
|
| 109 |
+
|
| 110 |
+
output = model(eff_dummy, cnx_dummy)
|
| 111 |
+
|
| 112 |
+
print("Fusion output shape:", output.shape)
|
src/models/resnet_model.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
from torchvision import models
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class CarClassifierResNet(nn.Module):
|
| 10 |
+
def __init__(self, num_classes):
|
| 11 |
+
super().__init__()
|
| 12 |
+
|
| 13 |
+
logger.info("Initializing ResNet18 model...")
|
| 14 |
+
|
| 15 |
+
self.model = models.resnet18(weights="DEFAULT")
|
| 16 |
+
|
| 17 |
+
# Freeze everything
|
| 18 |
+
for param in self.model.parameters():
|
| 19 |
+
param.requires_grad = False
|
| 20 |
+
|
| 21 |
+
# Unfreeze last layers
|
| 22 |
+
for param in self.model.layer3.parameters():
|
| 23 |
+
param.requires_grad = True
|
| 24 |
+
|
| 25 |
+
for param in self.model.layer4.parameters():
|
| 26 |
+
param.requires_grad = True
|
| 27 |
+
|
| 28 |
+
# Custom classifier head
|
| 29 |
+
self.model.fc = nn.Sequential(
|
| 30 |
+
nn.Dropout(0.5),
|
| 31 |
+
nn.Linear(self.model.fc.in_features, 256),
|
| 32 |
+
nn.ReLU(),
|
| 33 |
+
nn.Dropout(0.3),
|
| 34 |
+
nn.Linear(256, num_classes)
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
logger.info("ResNet18 model initialized successfully.")
|
| 38 |
+
|
| 39 |
+
def forward(self, x):
|
| 40 |
+
return self.model(x)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
if __name__ == "__main__":
|
| 44 |
+
logging.basicConfig(
|
| 45 |
+
level=logging.INFO,
|
| 46 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
model = CarClassifierResNet(num_classes=6)
|
| 50 |
+
|
| 51 |
+
dummy_input = torch.randn(2, 3, 128, 128)
|
| 52 |
+
|
| 53 |
+
output = model(dummy_input)
|
| 54 |
+
|
| 55 |
+
print("Output shape:", output.shape)
|
| 56 |
+
|
| 57 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 58 |
+
trainable_params = sum(
|
| 59 |
+
p.numel() for p in model.parameters()
|
| 60 |
+
if p.requires_grad
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
print("Total params:", total_params)
|
| 64 |
+
print("Trainable params:", trainable_params)
|
src/training/train_fusion.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from torch.optim import AdamW
|
| 4 |
+
|
| 5 |
+
from src.config import DEVICE, EPOCHS, NUM_CLASSES
|
| 6 |
+
from src.models.fusion_model import FusionClassifier
|
| 7 |
+
from src.data.dataset import create_fusion_dataloaders
|
| 8 |
+
from src.training.trainer import train_dual_input_model
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def run_fusion_training():
|
| 14 |
+
logger.info("Initializing Fusion training pipeline...")
|
| 15 |
+
|
| 16 |
+
train_loader, eval_loader = create_fusion_dataloaders()
|
| 17 |
+
|
| 18 |
+
model = FusionClassifier(
|
| 19 |
+
num_classes=NUM_CLASSES
|
| 20 |
+
).to(DEVICE)
|
| 21 |
+
|
| 22 |
+
criterion = nn.CrossEntropyLoss(
|
| 23 |
+
label_smoothing=0.1
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
optimizer = AdamW([
|
| 27 |
+
# EfficientNet unfrozen blocks
|
| 28 |
+
{
|
| 29 |
+
"params": model.eff_features[5].parameters(),
|
| 30 |
+
"lr": 1e-5
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"params": model.eff_features[6].parameters(),
|
| 34 |
+
"lr": 3e-5
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"params": model.eff_features[7].parameters(),
|
| 38 |
+
"lr": 3e-5
|
| 39 |
+
},
|
| 40 |
+
|
| 41 |
+
# ConvNeXt unfrozen blocks
|
| 42 |
+
{
|
| 43 |
+
"params": model.cnx_backbone.encoder.stages[2].parameters(),
|
| 44 |
+
"lr": 3e-5
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"params": model.cnx_backbone.encoder.stages[3].parameters(),
|
| 48 |
+
"lr": 3e-5
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"params": model.cnx_backbone.layernorm.parameters(),
|
| 52 |
+
"lr": 3e-5
|
| 53 |
+
},
|
| 54 |
+
|
| 55 |
+
# Fusion head
|
| 56 |
+
{
|
| 57 |
+
"params": model.fusion_head.parameters(),
|
| 58 |
+
"lr": 1e-4
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
], weight_decay=1e-4)
|
| 62 |
+
|
| 63 |
+
logger.info("Starting Fusion training...")
|
| 64 |
+
|
| 65 |
+
all_preds, all_labels = train_dual_input_model(
|
| 66 |
+
model=model,
|
| 67 |
+
train_loader=train_loader,
|
| 68 |
+
eval_loader=eval_loader,
|
| 69 |
+
optimizer=optimizer,
|
| 70 |
+
criterion=criterion,
|
| 71 |
+
device=DEVICE,
|
| 72 |
+
epochs=EPOCHS,
|
| 73 |
+
checkpoint_model_name="best_fusion_model",
|
| 74 |
+
patience=7
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
logger.info("Fusion training completed.")
|
| 78 |
+
|
| 79 |
+
return all_preds, all_labels
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
logging.basicConfig(
|
| 84 |
+
level=logging.INFO,
|
| 85 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
preds, labels = run_fusion_training()
|
| 89 |
+
|
| 90 |
+
print("\nFusion training completed successfully.")
|
| 91 |
+
print("Prediction samples:", preds[:10])
|
| 92 |
+
print("Label samples:", labels[:10])
|
src/training/train_resnet.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from torch.optim import AdamW
|
| 4 |
+
|
| 5 |
+
from src.config import DEVICE, EPOCHS, NUM_CLASSES
|
| 6 |
+
from src.models.resnet_model import CarClassifierResNet
|
| 7 |
+
from src.data.dataset import create_resnet_dataloaders
|
| 8 |
+
from src.training.trainer import train_single_input_model
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def run_resnet_training():
|
| 14 |
+
logger.info("Initializing ResNet training pipeline...")
|
| 15 |
+
|
| 16 |
+
train_loader, eval_loader = create_resnet_dataloaders()
|
| 17 |
+
|
| 18 |
+
model = CarClassifierResNet(
|
| 19 |
+
num_classes=NUM_CLASSES
|
| 20 |
+
).to(DEVICE)
|
| 21 |
+
|
| 22 |
+
criterion = nn.CrossEntropyLoss()
|
| 23 |
+
|
| 24 |
+
optimizer = AdamW([
|
| 25 |
+
{
|
| 26 |
+
"params": model.model.layer3.parameters(),
|
| 27 |
+
"lr": 1e-5
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"params": model.model.layer4.parameters(),
|
| 31 |
+
"lr": 1e-5
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"params": model.model.fc.parameters(),
|
| 35 |
+
"lr": 1e-4
|
| 36 |
+
}
|
| 37 |
+
])
|
| 38 |
+
|
| 39 |
+
logger.info("Starting ResNet training...")
|
| 40 |
+
|
| 41 |
+
all_preds, all_labels = train_single_input_model(
|
| 42 |
+
model=model,
|
| 43 |
+
train_loader=train_loader,
|
| 44 |
+
eval_loader=eval_loader,
|
| 45 |
+
optimizer=optimizer,
|
| 46 |
+
criterion=criterion,
|
| 47 |
+
device=DEVICE,
|
| 48 |
+
epochs=EPOCHS,
|
| 49 |
+
checkpoint_model_name="best_resnet_model",
|
| 50 |
+
patience=7
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
logger.info("ResNet training completed.")
|
| 54 |
+
|
| 55 |
+
return all_preds, all_labels
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
logging.basicConfig(
|
| 60 |
+
level=logging.INFO,
|
| 61 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
preds, labels = run_resnet_training()
|
| 65 |
+
|
| 66 |
+
print("\nTraining completed successfully.")
|
| 67 |
+
print("Prediction samples:", preds[:10])
|
| 68 |
+
print("Label samples:", labels[:10])
|
src/training/train_yolo.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from shutil import copy2, rmtree
|
| 3 |
+
from ultralytics import YOLO
|
| 4 |
+
|
| 5 |
+
from src.config import BASE_DIR, CHECKPOINT_DIR, DEVICE
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
YOLO_DATASET_CONFIG = BASE_DIR / "data" / "yolo" / "dataset_custom.yaml"
|
| 10 |
+
YOLO_BASE_MODEL = CHECKPOINT_DIR / "yolo11m.pt"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def run_yolo_training():
|
| 14 |
+
logger.info("Initializing YOLO training pipeline...")
|
| 15 |
+
|
| 16 |
+
if not YOLO_DATASET_CONFIG.exists():
|
| 17 |
+
raise FileNotFoundError(
|
| 18 |
+
f"YOLO dataset config not found: {YOLO_DATASET_CONFIG}"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
if not YOLO_BASE_MODEL.exists():
|
| 22 |
+
raise FileNotFoundError(
|
| 23 |
+
f"YOLO base model not found: {YOLO_BASE_MODEL}"
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
yolo_device = 0 if DEVICE == "cuda" else "cpu"
|
| 27 |
+
|
| 28 |
+
checkpoint_root = CHECKPOINT_DIR.resolve()
|
| 29 |
+
temp_run_name = "temp_yolo_run"
|
| 30 |
+
|
| 31 |
+
logger.info("Loading YOLO base model...")
|
| 32 |
+
|
| 33 |
+
model = YOLO(str(YOLO_BASE_MODEL.resolve()))
|
| 34 |
+
|
| 35 |
+
logger.info("Starting YOLO training...")
|
| 36 |
+
|
| 37 |
+
model.train(
|
| 38 |
+
data=str(YOLO_DATASET_CONFIG.resolve()),
|
| 39 |
+
imgsz=416,
|
| 40 |
+
batch=4,
|
| 41 |
+
epochs=1,
|
| 42 |
+
device=yolo_device,
|
| 43 |
+
project=str(checkpoint_root),
|
| 44 |
+
name=temp_run_name,
|
| 45 |
+
exist_ok=True
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
best_model_path = (
|
| 49 |
+
checkpoint_root /
|
| 50 |
+
temp_run_name /
|
| 51 |
+
"weights" /
|
| 52 |
+
"best.pt"
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
if not best_model_path.exists():
|
| 56 |
+
raise FileNotFoundError(
|
| 57 |
+
f"YOLO best model not found: {best_model_path}"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
final_model_path = checkpoint_root / "damage_detector.pt"
|
| 61 |
+
|
| 62 |
+
copy2(best_model_path, final_model_path)
|
| 63 |
+
|
| 64 |
+
logger.info(f"Final YOLO model saved at: {final_model_path}")
|
| 65 |
+
|
| 66 |
+
# cleanup temp training folder
|
| 67 |
+
temp_run_dir = checkpoint_root / temp_run_name
|
| 68 |
+
|
| 69 |
+
if temp_run_dir.exists():
|
| 70 |
+
rmtree(temp_run_dir)
|
| 71 |
+
logger.info("Temporary YOLO training artifacts deleted.")
|
| 72 |
+
|
| 73 |
+
return final_model_path
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
logging.basicConfig(
|
| 78 |
+
level=logging.INFO,
|
| 79 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
model_path = run_yolo_training()
|
| 83 |
+
|
| 84 |
+
print("\nYOLO training completed successfully.")
|
| 85 |
+
print(f"Saved model: {model_path}")
|
src/training/trainer.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import torch
|
| 3 |
+
from tqdm import tqdm
|
| 4 |
+
from transformers import get_cosine_schedule_with_warmup
|
| 5 |
+
|
| 6 |
+
from src.config import CHECKPOINT_DIR
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class EarlyStopping:
|
| 12 |
+
def __init__(self, patience=7, min_delta=0.001):
|
| 13 |
+
self.patience = patience
|
| 14 |
+
self.min_delta = min_delta
|
| 15 |
+
self.counter = 0
|
| 16 |
+
self.best_score = None
|
| 17 |
+
self.early_stop = False
|
| 18 |
+
|
| 19 |
+
def __call__(self, val_acc):
|
| 20 |
+
if self.best_score is None:
|
| 21 |
+
self.best_score = val_acc
|
| 22 |
+
|
| 23 |
+
elif val_acc < self.best_score + self.min_delta:
|
| 24 |
+
self.counter += 1
|
| 25 |
+
|
| 26 |
+
logger.info(
|
| 27 |
+
f"EarlyStopping counter: {self.counter}/{self.patience}"
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
if self.counter >= self.patience:
|
| 31 |
+
self.early_stop = True
|
| 32 |
+
|
| 33 |
+
else:
|
| 34 |
+
self.best_score = val_acc
|
| 35 |
+
self.counter = 0
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def train_single_input_model(
|
| 39 |
+
model,
|
| 40 |
+
train_loader,
|
| 41 |
+
eval_loader,
|
| 42 |
+
optimizer,
|
| 43 |
+
criterion,
|
| 44 |
+
device,
|
| 45 |
+
epochs,
|
| 46 |
+
checkpoint_model_name,
|
| 47 |
+
patience=7
|
| 48 |
+
):
|
| 49 |
+
logger.info("Starting single-input training...")
|
| 50 |
+
|
| 51 |
+
num_training_steps = epochs * len(train_loader)
|
| 52 |
+
num_warmup_steps = int(0.1 * num_training_steps)
|
| 53 |
+
|
| 54 |
+
scheduler = get_cosine_schedule_with_warmup(
|
| 55 |
+
optimizer=optimizer,
|
| 56 |
+
num_warmup_steps=num_warmup_steps,
|
| 57 |
+
num_training_steps=num_training_steps
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
early_stopping = EarlyStopping(patience=patience)
|
| 61 |
+
|
| 62 |
+
best_acc = 0.0
|
| 63 |
+
all_preds = []
|
| 64 |
+
all_labels = []
|
| 65 |
+
|
| 66 |
+
for epoch in range(epochs):
|
| 67 |
+
logger.info(f"Epoch {epoch + 1}/{epochs}")
|
| 68 |
+
|
| 69 |
+
model.train()
|
| 70 |
+
|
| 71 |
+
running_loss = 0
|
| 72 |
+
correct = 0
|
| 73 |
+
total = 0
|
| 74 |
+
|
| 75 |
+
for images, labels in tqdm(
|
| 76 |
+
train_loader,
|
| 77 |
+
desc=f"Epoch {epoch+1} Training"
|
| 78 |
+
):
|
| 79 |
+
images = images.to(device)
|
| 80 |
+
labels = labels.to(device)
|
| 81 |
+
|
| 82 |
+
optimizer.zero_grad(set_to_none=True)
|
| 83 |
+
|
| 84 |
+
logits = model(images)
|
| 85 |
+
|
| 86 |
+
loss = criterion(logits, labels)
|
| 87 |
+
|
| 88 |
+
loss.backward()
|
| 89 |
+
|
| 90 |
+
optimizer.step()
|
| 91 |
+
scheduler.step()
|
| 92 |
+
|
| 93 |
+
running_loss += loss.item()
|
| 94 |
+
|
| 95 |
+
preds = torch.argmax(logits, dim=1)
|
| 96 |
+
|
| 97 |
+
correct += (preds == labels).sum().item()
|
| 98 |
+
total += labels.size(0)
|
| 99 |
+
|
| 100 |
+
train_loss = running_loss / len(train_loader)
|
| 101 |
+
train_acc = 100 * correct / total
|
| 102 |
+
|
| 103 |
+
model.eval()
|
| 104 |
+
|
| 105 |
+
val_running_loss = 0
|
| 106 |
+
val_correct = 0
|
| 107 |
+
val_total = 0
|
| 108 |
+
|
| 109 |
+
all_preds = []
|
| 110 |
+
all_labels = []
|
| 111 |
+
|
| 112 |
+
with torch.no_grad():
|
| 113 |
+
for images, labels in tqdm(
|
| 114 |
+
eval_loader,
|
| 115 |
+
desc=f"Epoch {epoch+1} Validation"
|
| 116 |
+
):
|
| 117 |
+
images = images.to(device)
|
| 118 |
+
labels = labels.to(device)
|
| 119 |
+
|
| 120 |
+
logits = model(images)
|
| 121 |
+
|
| 122 |
+
loss = criterion(logits, labels)
|
| 123 |
+
|
| 124 |
+
val_running_loss += loss.item()
|
| 125 |
+
|
| 126 |
+
preds = torch.argmax(logits, dim=1)
|
| 127 |
+
|
| 128 |
+
val_correct += (preds == labels).sum().item()
|
| 129 |
+
val_total += labels.size(0)
|
| 130 |
+
|
| 131 |
+
all_preds.extend(preds.cpu().numpy())
|
| 132 |
+
all_labels.extend(labels.cpu().numpy())
|
| 133 |
+
|
| 134 |
+
val_loss = val_running_loss / len(eval_loader)
|
| 135 |
+
val_acc = 100 * val_correct / val_total
|
| 136 |
+
|
| 137 |
+
logger.info(
|
| 138 |
+
f"Train Loss: {train_loss:.4f} | "
|
| 139 |
+
f"Train Acc: {train_acc:.2f}% || "
|
| 140 |
+
f"Val Loss: {val_loss:.4f} | "
|
| 141 |
+
f"Val Acc: {val_acc:.2f}%"
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
if val_acc > best_acc:
|
| 145 |
+
best_acc = val_acc
|
| 146 |
+
|
| 147 |
+
checkpoint_path = CHECKPOINT_DIR / f"{checkpoint_model_name}.pt"
|
| 148 |
+
|
| 149 |
+
torch.save(
|
| 150 |
+
{
|
| 151 |
+
"model_state_dict": model.state_dict(),
|
| 152 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 153 |
+
"epoch": epoch,
|
| 154 |
+
"val_acc": val_acc
|
| 155 |
+
},
|
| 156 |
+
checkpoint_path
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
logger.info(f"Best checkpoint saved at: {checkpoint_path}")
|
| 160 |
+
|
| 161 |
+
early_stopping(val_acc)
|
| 162 |
+
|
| 163 |
+
if early_stopping.early_stop:
|
| 164 |
+
logger.info("Early stopping triggered.")
|
| 165 |
+
break
|
| 166 |
+
|
| 167 |
+
return all_preds, all_labels
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def train_dual_input_model(
|
| 171 |
+
model,
|
| 172 |
+
train_loader,
|
| 173 |
+
eval_loader,
|
| 174 |
+
optimizer,
|
| 175 |
+
criterion,
|
| 176 |
+
device,
|
| 177 |
+
epochs,
|
| 178 |
+
checkpoint_model_name,
|
| 179 |
+
patience=7
|
| 180 |
+
):
|
| 181 |
+
logger.info("Starting dual-input training...")
|
| 182 |
+
|
| 183 |
+
num_training_steps = epochs * len(train_loader)
|
| 184 |
+
num_warmup_steps = int(0.1 * num_training_steps)
|
| 185 |
+
|
| 186 |
+
scheduler = get_cosine_schedule_with_warmup(
|
| 187 |
+
optimizer=optimizer,
|
| 188 |
+
num_warmup_steps=num_warmup_steps,
|
| 189 |
+
num_training_steps=num_training_steps
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
early_stopping = EarlyStopping(patience=patience)
|
| 193 |
+
|
| 194 |
+
best_acc = 0.0
|
| 195 |
+
all_preds = []
|
| 196 |
+
all_labels = []
|
| 197 |
+
|
| 198 |
+
for epoch in range(epochs):
|
| 199 |
+
logger.info(f"Epoch {epoch + 1}/{epochs}")
|
| 200 |
+
|
| 201 |
+
model.train()
|
| 202 |
+
|
| 203 |
+
running_loss = 0
|
| 204 |
+
correct = 0
|
| 205 |
+
total = 0
|
| 206 |
+
|
| 207 |
+
for batch in tqdm(
|
| 208 |
+
train_loader,
|
| 209 |
+
desc=f"Epoch {epoch+1} Training"
|
| 210 |
+
):
|
| 211 |
+
images_eff = batch["pixel_values_eff"].to(device)
|
| 212 |
+
images_cnx = batch["pixel_values_cnx"].to(device)
|
| 213 |
+
labels = batch["labels"].to(device)
|
| 214 |
+
|
| 215 |
+
optimizer.zero_grad(set_to_none=True)
|
| 216 |
+
|
| 217 |
+
logits = model(images_eff, images_cnx)
|
| 218 |
+
|
| 219 |
+
loss = criterion(logits, labels)
|
| 220 |
+
|
| 221 |
+
loss.backward()
|
| 222 |
+
|
| 223 |
+
optimizer.step()
|
| 224 |
+
scheduler.step()
|
| 225 |
+
|
| 226 |
+
running_loss += loss.item()
|
| 227 |
+
|
| 228 |
+
preds = torch.argmax(logits, dim=1)
|
| 229 |
+
|
| 230 |
+
correct += (preds == labels).sum().item()
|
| 231 |
+
total += labels.size(0)
|
| 232 |
+
|
| 233 |
+
train_loss = running_loss / len(train_loader)
|
| 234 |
+
train_acc = 100 * correct / total
|
| 235 |
+
|
| 236 |
+
model.eval()
|
| 237 |
+
|
| 238 |
+
val_running_loss = 0
|
| 239 |
+
val_correct = 0
|
| 240 |
+
val_total = 0
|
| 241 |
+
|
| 242 |
+
all_preds = []
|
| 243 |
+
all_labels = []
|
| 244 |
+
|
| 245 |
+
with torch.no_grad():
|
| 246 |
+
for batch in tqdm(
|
| 247 |
+
eval_loader,
|
| 248 |
+
desc=f"Epoch {epoch+1} Validation"
|
| 249 |
+
):
|
| 250 |
+
images_eff = batch["pixel_values_eff"].to(device)
|
| 251 |
+
images_cnx = batch["pixel_values_cnx"].to(device)
|
| 252 |
+
labels = batch["labels"].to(device)
|
| 253 |
+
|
| 254 |
+
logits = model(images_eff, images_cnx)
|
| 255 |
+
|
| 256 |
+
loss = criterion(logits, labels)
|
| 257 |
+
|
| 258 |
+
val_running_loss += loss.item()
|
| 259 |
+
|
| 260 |
+
preds = torch.argmax(logits, dim=1)
|
| 261 |
+
|
| 262 |
+
val_correct += (preds == labels).sum().item()
|
| 263 |
+
val_total += labels.size(0)
|
| 264 |
+
|
| 265 |
+
all_preds.extend(preds.cpu().numpy())
|
| 266 |
+
all_labels.extend(labels.cpu().numpy())
|
| 267 |
+
|
| 268 |
+
val_loss = val_running_loss / len(eval_loader)
|
| 269 |
+
val_acc = 100 * val_correct / val_total
|
| 270 |
+
|
| 271 |
+
logger.info(
|
| 272 |
+
f"Train Loss: {train_loss:.4f} | "
|
| 273 |
+
f"Train Acc: {train_acc:.2f}% || "
|
| 274 |
+
f"Val Loss: {val_loss:.4f} | "
|
| 275 |
+
f"Val Acc: {val_acc:.2f}%"
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
if val_acc > best_acc:
|
| 279 |
+
best_acc = val_acc
|
| 280 |
+
|
| 281 |
+
checkpoint_path = CHECKPOINT_DIR / f"{checkpoint_model_name}.pt"
|
| 282 |
+
|
| 283 |
+
torch.save(
|
| 284 |
+
{
|
| 285 |
+
"model_state_dict": model.state_dict(),
|
| 286 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 287 |
+
"epoch": epoch,
|
| 288 |
+
"val_acc": val_acc
|
| 289 |
+
},
|
| 290 |
+
checkpoint_path
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
logger.info(f"Best checkpoint saved at: {checkpoint_path}")
|
| 294 |
+
|
| 295 |
+
early_stopping(val_acc)
|
| 296 |
+
|
| 297 |
+
if early_stopping.early_stop:
|
| 298 |
+
logger.info("Early stopping triggered.")
|
| 299 |
+
break
|
| 300 |
+
|
| 301 |
+
return all_preds, all_labels
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
if __name__ == "__main__":
|
| 305 |
+
print("Trainer utilities ready.")
|
test/test_augmentation.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from PIL import Image
|
| 3 |
+
|
| 4 |
+
from src.data.augmentation import (
|
| 5 |
+
get_resnet_train_transforms,
|
| 6 |
+
get_fusion_train_transforms
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def test_augmentation():
|
| 13 |
+
logger.info("Testing augmentation pipelines...")
|
| 14 |
+
|
| 15 |
+
dummy_image = Image.new("RGB", (300, 300))
|
| 16 |
+
|
| 17 |
+
resnet_transform = get_resnet_train_transforms()
|
| 18 |
+
fusion_transform = get_fusion_train_transforms()
|
| 19 |
+
|
| 20 |
+
resnet_tensor = resnet_transform(dummy_image)
|
| 21 |
+
fusion_tensor = fusion_transform(dummy_image)
|
| 22 |
+
|
| 23 |
+
assert resnet_tensor.shape == (3, 128, 128), \
|
| 24 |
+
f"Unexpected ResNet shape: {resnet_tensor.shape}"
|
| 25 |
+
|
| 26 |
+
assert fusion_tensor.shape == (3, 260, 260), \
|
| 27 |
+
f"Unexpected Fusion shape: {fusion_tensor.shape}"
|
| 28 |
+
|
| 29 |
+
logger.info("Augmentation test passed.")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
if __name__ == "__main__":
|
| 33 |
+
logging.basicConfig(
|
| 34 |
+
level=logging.INFO,
|
| 35 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
test_augmentation()
|
| 39 |
+
|
| 40 |
+
print("Augmentation test completed successfully.")
|
test/test_config.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
from src.config import (
|
| 4 |
+
BASE_DIR,
|
| 5 |
+
CHECKPOINT_DIR,
|
| 6 |
+
DEVICE,
|
| 7 |
+
BATCH_SIZE,
|
| 8 |
+
EPOCHS,
|
| 9 |
+
NUM_CLASSES
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_config():
|
| 16 |
+
logger.info("Testing config settings...")
|
| 17 |
+
|
| 18 |
+
assert BASE_DIR.exists(), "BASE_DIR missing"
|
| 19 |
+
assert CHECKPOINT_DIR.exists(), "CHECKPOINT_DIR missing"
|
| 20 |
+
|
| 21 |
+
assert DEVICE in ["cpu", "cuda"], "Invalid device"
|
| 22 |
+
assert BATCH_SIZE > 0, "Invalid batch size"
|
| 23 |
+
assert EPOCHS > 0, "Invalid epochs"
|
| 24 |
+
assert NUM_CLASSES == 6, "NUM_CLASSES mismatch"
|
| 25 |
+
|
| 26 |
+
logger.info("Config test passed.")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
if __name__ == "__main__":
|
| 30 |
+
logging.basicConfig(
|
| 31 |
+
level=logging.INFO,
|
| 32 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
test_config()
|
| 36 |
+
|
| 37 |
+
print("Config test completed successfully.")
|
test/test_dataset.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
from src.data.dataset import (
|
| 4 |
+
create_resnet_dataloaders,
|
| 5 |
+
create_fusion_dataloaders
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_dataset():
|
| 12 |
+
logger.info("Testing dataset loaders...")
|
| 13 |
+
|
| 14 |
+
# ---------------- ResNet ----------------
|
| 15 |
+
resnet_loader, _ = create_resnet_dataloaders()
|
| 16 |
+
|
| 17 |
+
images, labels = next(iter(resnet_loader))
|
| 18 |
+
|
| 19 |
+
assert images.shape[1:] == (3, 128, 128), \
|
| 20 |
+
f"Unexpected ResNet image shape: {images.shape}"
|
| 21 |
+
|
| 22 |
+
assert len(labels.shape) == 1, \
|
| 23 |
+
f"Unexpected ResNet labels shape: {labels.shape}"
|
| 24 |
+
|
| 25 |
+
logger.info("ResNet dataloader test passed.")
|
| 26 |
+
|
| 27 |
+
# ---------------- Fusion ----------------
|
| 28 |
+
fusion_loader, _ = create_fusion_dataloaders()
|
| 29 |
+
|
| 30 |
+
batch = next(iter(fusion_loader))
|
| 31 |
+
|
| 32 |
+
assert "pixel_values_eff" in batch, "Missing EfficientNet input"
|
| 33 |
+
assert "pixel_values_cnx" in batch, "Missing ConvNeXt input"
|
| 34 |
+
assert "labels" in batch, "Missing labels"
|
| 35 |
+
|
| 36 |
+
assert batch["pixel_values_eff"].shape[1:] == (3, 260, 260), \
|
| 37 |
+
f"Unexpected Fusion EfficientNet shape: {batch['pixel_values_eff'].shape}"
|
| 38 |
+
|
| 39 |
+
assert batch["pixel_values_cnx"].shape[1:] == (3, 224, 224), \
|
| 40 |
+
f"Unexpected Fusion ConvNeXt shape: {batch['pixel_values_cnx'].shape}"
|
| 41 |
+
|
| 42 |
+
assert len(batch["labels"].shape) == 1, \
|
| 43 |
+
f"Unexpected Fusion labels shape: {batch['labels'].shape}"
|
| 44 |
+
|
| 45 |
+
logger.info("Fusion dataloader test passed.")
|
| 46 |
+
logger.info("Dataset test passed successfully.")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
logging.basicConfig(
|
| 51 |
+
level=logging.INFO,
|
| 52 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
test_dataset()
|
| 56 |
+
|
| 57 |
+
print("Dataset test completed successfully.")
|
test/test_fusion_model.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from src.models.fusion_model import FusionClassifier
|
| 5 |
+
from src.config import NUM_CLASSES
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_fusion_model():
|
| 11 |
+
logger.info("Testing Fusion model architecture...")
|
| 12 |
+
|
| 13 |
+
model = FusionClassifier(
|
| 14 |
+
num_classes=NUM_CLASSES
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
model.eval()
|
| 18 |
+
|
| 19 |
+
eff_dummy = torch.randn(2, 3, 260, 260)
|
| 20 |
+
cnx_dummy = torch.randn(2, 3, 224, 224)
|
| 21 |
+
|
| 22 |
+
with torch.no_grad():
|
| 23 |
+
output = model(
|
| 24 |
+
eff_dummy,
|
| 25 |
+
cnx_dummy
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
assert output.shape == (2, NUM_CLASSES), \
|
| 29 |
+
f"Unexpected output shape: {output.shape}"
|
| 30 |
+
|
| 31 |
+
logger.info("Fusion model test passed.")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
if __name__ == "__main__":
|
| 35 |
+
logging.basicConfig(
|
| 36 |
+
level=logging.INFO,
|
| 37 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
test_fusion_model()
|
| 41 |
+
|
| 42 |
+
print("Fusion model test completed successfully.")
|
test/test_ingestion.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
from src.data.ingestion import collect_image_paths
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_ingestion():
|
| 10 |
+
logger.info("Testing ingestion...")
|
| 11 |
+
|
| 12 |
+
samples = collect_image_paths()
|
| 13 |
+
|
| 14 |
+
assert len(samples) > 0, "No samples found"
|
| 15 |
+
|
| 16 |
+
image_path, label = samples[0]
|
| 17 |
+
|
| 18 |
+
assert os.path.exists(image_path), "Image path missing"
|
| 19 |
+
assert isinstance(label, int), "Label invalid"
|
| 20 |
+
|
| 21 |
+
logger.info("Ingestion test passed.")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
if __name__ == "__main__":
|
| 25 |
+
logging.basicConfig(
|
| 26 |
+
level=logging.INFO,
|
| 27 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
test_ingestion()
|
| 31 |
+
|
| 32 |
+
print("Ingestion test completed successfully.")
|
test/test_preprocessing.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
from src.data.ingestion import collect_image_paths
|
| 4 |
+
from src.data.preprocessing import split_dataset
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_preprocessing():
|
| 10 |
+
logger.info("Testing preprocessing...")
|
| 11 |
+
|
| 12 |
+
samples = collect_image_paths()
|
| 13 |
+
|
| 14 |
+
train_data, val_data = split_dataset(samples)
|
| 15 |
+
|
| 16 |
+
assert len(train_data) > 0, "Train split is empty"
|
| 17 |
+
assert len(val_data) > 0, "Validation split is empty"
|
| 18 |
+
|
| 19 |
+
train_paths = set(x[0] for x in train_data)
|
| 20 |
+
val_paths = set(x[0] for x in val_data)
|
| 21 |
+
|
| 22 |
+
overlap = train_paths.intersection(val_paths)
|
| 23 |
+
|
| 24 |
+
assert len(overlap) == 0, "Train and validation overlap found"
|
| 25 |
+
|
| 26 |
+
logger.info("Preprocessing test passed.")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
if __name__ == "__main__":
|
| 30 |
+
logging.basicConfig(
|
| 31 |
+
level=logging.INFO,
|
| 32 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
test_preprocessing()
|
| 36 |
+
|
| 37 |
+
print("Preprocessing test completed successfully.")
|
test/test_resnet_model.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from src.models.resnet_model import CarClassifierResNet
|
| 5 |
+
from src.config import NUM_CLASSES
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_resnet_model():
|
| 11 |
+
logger.info("Testing ResNet model architecture...")
|
| 12 |
+
|
| 13 |
+
model = CarClassifierResNet(
|
| 14 |
+
num_classes=NUM_CLASSES
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
model.eval()
|
| 18 |
+
|
| 19 |
+
dummy_input = torch.randn(2, 3, 128, 128)
|
| 20 |
+
|
| 21 |
+
with torch.no_grad():
|
| 22 |
+
output = model(dummy_input)
|
| 23 |
+
|
| 24 |
+
assert output.shape == (2, NUM_CLASSES), \
|
| 25 |
+
f"Unexpected output shape: {output.shape}"
|
| 26 |
+
|
| 27 |
+
logger.info("ResNet model test passed.")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
if __name__ == "__main__":
|
| 31 |
+
logging.basicConfig(
|
| 32 |
+
level=logging.INFO,
|
| 33 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
test_resnet_model()
|
| 37 |
+
|
| 38 |
+
print("ResNet model test completed successfully.")
|
test/test_train_fusion.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
from src.training.train_fusion import run_fusion_training
|
| 4 |
+
from src.config import CHECKPOINT_DIR
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_train_fusion():
|
| 10 |
+
logger.info("Testing Fusion training pipeline...")
|
| 11 |
+
|
| 12 |
+
checkpoint_path = CHECKPOINT_DIR / "best_fusion_model.pt"
|
| 13 |
+
|
| 14 |
+
if checkpoint_path.exists():
|
| 15 |
+
checkpoint_path.unlink()
|
| 16 |
+
|
| 17 |
+
preds, labels = run_fusion_training()
|
| 18 |
+
|
| 19 |
+
assert checkpoint_path.exists(), \
|
| 20 |
+
"Fusion checkpoint was not created"
|
| 21 |
+
|
| 22 |
+
assert len(preds) > 0, \
|
| 23 |
+
"No predictions returned"
|
| 24 |
+
|
| 25 |
+
assert len(labels) > 0, \
|
| 26 |
+
"No labels returned"
|
| 27 |
+
|
| 28 |
+
logger.info("Fusion training test passed.")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
if __name__ == "__main__":
|
| 32 |
+
logging.basicConfig(
|
| 33 |
+
level=logging.INFO,
|
| 34 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
test_train_fusion()
|
| 38 |
+
|
| 39 |
+
print("Fusion training test completed successfully.")
|
test/test_train_resnet.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
from src.training.train_resnet import run_resnet_training
|
| 4 |
+
from src.config import CHECKPOINT_DIR
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_train_resnet():
|
| 10 |
+
logger.info("Testing ResNet training pipeline...")
|
| 11 |
+
|
| 12 |
+
checkpoint_path = CHECKPOINT_DIR / "best_resnet_model.pt"
|
| 13 |
+
|
| 14 |
+
if checkpoint_path.exists():
|
| 15 |
+
checkpoint_path.unlink()
|
| 16 |
+
|
| 17 |
+
preds, labels = run_resnet_training()
|
| 18 |
+
|
| 19 |
+
assert checkpoint_path.exists(), \
|
| 20 |
+
"ResNet checkpoint was not created"
|
| 21 |
+
|
| 22 |
+
assert len(preds) > 0, \
|
| 23 |
+
"No predictions returned"
|
| 24 |
+
|
| 25 |
+
assert len(labels) > 0, \
|
| 26 |
+
"No labels returned"
|
| 27 |
+
|
| 28 |
+
logger.info("ResNet training test passed.")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
if __name__ == "__main__":
|
| 32 |
+
logging.basicConfig(
|
| 33 |
+
level=logging.INFO,
|
| 34 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
test_train_resnet()
|
| 38 |
+
|
| 39 |
+
print("ResNet training test completed successfully.")
|
test/test_train_yolo.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
from src.training.train_yolo import run_yolo_training
|
| 4 |
+
from src.config import CHECKPOINT_DIR
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_train_yolo():
|
| 10 |
+
logger.info("Testing YOLO training pipeline...")
|
| 11 |
+
|
| 12 |
+
checkpoint_path = CHECKPOINT_DIR / "damage_detector.pt"
|
| 13 |
+
|
| 14 |
+
if checkpoint_path.exists():
|
| 15 |
+
checkpoint_path.unlink()
|
| 16 |
+
|
| 17 |
+
output_path = run_yolo_training()
|
| 18 |
+
|
| 19 |
+
assert checkpoint_path.exists(), \
|
| 20 |
+
"YOLO checkpoint was not created"
|
| 21 |
+
|
| 22 |
+
assert output_path.exists(), \
|
| 23 |
+
"Returned YOLO model path invalid"
|
| 24 |
+
|
| 25 |
+
logger.info("YOLO training test passed.")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
if __name__ == "__main__":
|
| 29 |
+
logging.basicConfig(
|
| 30 |
+
level=logging.INFO,
|
| 31 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
test_train_yolo()
|
| 35 |
+
|
| 36 |
+
print("YOLO training test completed successfully.")
|