TB-Guard-XAI / train_ensemble.py
Vignesh-19's picture
Upload train_ensemble.py with huggingface_hub
9623539 verified
Raw
History Blame Contribute Delete
10.4 kB
# Train Ensemble Model with Multi-Dataset
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from pathlib import Path
from tqdm import tqdm
import json
from sklearn.metrics import roc_auc_score, accuracy_score, precision_recall_fscore_support
import numpy as np
from ensemble_models import TBEnsemble
from preprocessing import PreprocessedDataset, get_train_transforms, get_val_transforms
class FocalLoss(nn.Module):
"""Focal Loss for handling class imbalance"""
def __init__(self, alpha=0.25, gamma=2.0):
super().__init__()
self.alpha = alpha
self.gamma = gamma
def forward(self, inputs, targets):
# TBEnsemble.forward() already applies sigmoid, so inputs are probabilities
# Use binary_cross_entropy (NOT _with_logits) to avoid double-sigmoid
inputs = inputs.clamp(1e-7, 1 - 1e-7) # numerical stability
bce_loss = nn.functional.binary_cross_entropy(inputs, targets, reduction='none')
pt = torch.exp(-bce_loss)
focal_loss = self.alpha * (1 - pt) ** self.gamma * bce_loss
return focal_loss.mean()
# Config
PROCESSED_DIR = Path("datasets_processed")
MODELS_DIR = Path("models")
MODELS_DIR.mkdir(exist_ok=True)
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
# Training parameters
BATCH_SIZE = 64
EPOCHS = 25
LEARNING_RATE = 1e-4
IMAGE_SIZE = 224
print(f"Device: {DEVICE}")
def load_dataset_split(split_dir):
"""Load images and labels from split directory"""
image_paths = []
labels = []
# TB images
tb_dir = split_dir / "TB"
for img_path in tb_dir.glob("*"):
if img_path.suffix.lower() in ['.png', '.jpg', '.jpeg']:
image_paths.append(img_path)
labels.append(1)
# Normal images
normal_dir = split_dir / "Normal"
for img_path in normal_dir.glob("*"):
if img_path.suffix.lower() in ['.png', '.jpg', '.jpeg']:
image_paths.append(img_path)
labels.append(0)
return image_paths, labels
def train_epoch(model, loader, criterion, optimizer, scaler):
"""Train one epoch"""
model.train()
total_loss = 0
for images, labels in tqdm(loader, desc="Training"):
images = images.to(DEVICE)
labels = labels.float().to(DEVICE)
optimizer.zero_grad()
if DEVICE == "cuda":
with torch.cuda.amp.autocast():
outputs = model(images).squeeze()
# Compute loss outside autocast (binary_cross_entropy isn't autocast-safe)
outputs = outputs.float()
loss = criterion(outputs, labels)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
else:
outputs = model(images).squeeze()
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
return total_loss / len(loader)
def evaluate(model, loader, threshold=0.5):
"""Evaluate model"""
model.eval()
all_preds = []
all_labels = []
with torch.no_grad():
for images, labels in tqdm(loader, desc="Evaluating"):
images = images.to(DEVICE)
outputs = model(images).squeeze()
# Ensemble already applies sigmoid, outputs are probabilities
probs = outputs.cpu().numpy()
all_preds.extend(probs)
all_labels.extend(labels.numpy())
all_preds = np.array(all_preds)
all_labels = np.array(all_labels)
# Metrics
preds_binary = (all_preds > threshold).astype(int)
acc = accuracy_score(all_labels, preds_binary)
auc = roc_auc_score(all_labels, all_preds)
precision, recall, f1, _ = precision_recall_fscore_support(
all_labels, preds_binary, average='binary'
)
return {
'accuracy': acc,
'auc': auc,
'precision': precision,
'recall': recall,
'f1': f1,
'predictions': all_preds,
'labels': all_labels
}
def find_best_threshold(predictions, labels):
"""Find optimal threshold"""
best_thresh = 0.5
best_f1 = 0
for thresh in np.arange(0.3, 0.7, 0.01):
preds_binary = (predictions > thresh).astype(int)
_, _, f1, _ = precision_recall_fscore_support(
labels, preds_binary, average='binary'
)
if f1 > best_f1:
best_f1 = f1
best_thresh = thresh
return best_thresh, best_f1
def main():
print("="*60)
print("TB-Guard-XAI Ensemble Training")
print("="*60)
# Load datasets
print("\nLoading datasets...")
train_paths, train_labels = load_dataset_split(PROCESSED_DIR / "train")
val_paths, val_labels = load_dataset_split(PROCESSED_DIR / "val")
test_paths, test_labels = load_dataset_split(PROCESSED_DIR / "test")
print(f"Train: {len(train_paths)} images")
print(f"Val: {len(val_paths)} images")
print(f"Test: {len(test_paths)} images")
# Create datasets
train_dataset = PreprocessedDataset(
train_paths, train_labels,
transforms=get_train_transforms(IMAGE_SIZE),
use_preprocessing=True
)
val_dataset = PreprocessedDataset(
val_paths, val_labels,
transforms=get_val_transforms(IMAGE_SIZE),
use_preprocessing=True
)
test_dataset = PreprocessedDataset(
test_paths, test_labels,
transforms=get_val_transforms(IMAGE_SIZE),
use_preprocessing=True
)
# Calculate class distribution
num_tb = sum(train_labels)
num_normal = len(train_labels) - num_tb
print(f"\nClass distribution - TB: {num_tb}, Normal: {num_normal}")
print(f"Imbalance ratio: 1:{num_normal/num_tb:.2f}")
print("Using Focal Loss to handle class imbalance")
# Create dataloaders - use regular shuffle, no weighted sampling
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=0, pin_memory=True)
val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, num_workers=0, pin_memory=True)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, num_workers=0, pin_memory=True)
# Initialize model
print("\nInitializing ensemble model...")
model = TBEnsemble(use_mc_dropout=True).to(DEVICE)
# Use Focal Loss with alpha tuned for class imbalance
# alpha=0.75 gives higher weight to TB (minority class) to combat imbalance
criterion = FocalLoss(alpha=0.75, gamma=2.0)
optimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE, weight_decay=1e-5)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', patience=3, factor=0.5)
scaler = torch.cuda.amp.GradScaler() if DEVICE == "cuda" else None
# Training loop
best_val_auc = 0
history = {'train_loss': [], 'val_metrics': []}
print("\n" + "="*60)
print("Starting Training")
print("="*60)
for epoch in range(EPOCHS):
print(f"\nEpoch {epoch+1}/{EPOCHS}")
# Train
train_loss = train_epoch(model, train_loader, criterion, optimizer, scaler)
# Validate
val_metrics = evaluate(model, val_loader)
# Update scheduler
scheduler.step(val_metrics['auc'])
# Log
print(f"Train Loss: {train_loss:.4f}")
print(f"Val Acc: {val_metrics['accuracy']:.4f}")
print(f"Val AUC: {val_metrics['auc']:.4f}")
print(f"Val F1: {val_metrics['f1']:.4f}")
print(f"Val Precision: {val_metrics['precision']:.4f}")
print(f"Val Recall: {val_metrics['recall']:.4f}")
# Calculate per-class accuracy for better insight
val_preds_binary = (val_metrics['predictions'] > 0.5).astype(int)
val_labels_array = val_metrics['labels']
tb_mask = val_labels_array == 1
normal_mask = val_labels_array == 0
tb_acc = (val_preds_binary[tb_mask] == val_labels_array[tb_mask]).mean()
normal_acc = (val_preds_binary[normal_mask] == val_labels_array[normal_mask]).mean()
print(f"Val TB Accuracy: {tb_acc:.4f}, Normal Accuracy: {normal_acc:.4f}")
history['train_loss'].append(train_loss)
history['val_metrics'].append({
'accuracy': val_metrics['accuracy'],
'auc': val_metrics['auc'],
'f1': val_metrics['f1']
})
# Save best model
if val_metrics['auc'] > best_val_auc:
best_val_auc = val_metrics['auc']
torch.save(model.state_dict(), MODELS_DIR / "ensemble_best.pth")
print("Best model saved!")
# Threshold tuning
print("\n" + "="*60)
print("Threshold Tuning")
print("="*60)
model.load_state_dict(torch.load(MODELS_DIR / "ensemble_best.pth"))
val_metrics = evaluate(model, val_loader)
best_thresh, best_f1 = find_best_threshold(
val_metrics['predictions'],
val_metrics['labels']
)
print(f"Best Threshold: {best_thresh:.3f}")
print(f"Best F1 Score: {best_f1:.4f}")
# Final test evaluation
print("\n" + "="*60)
print("Final Test Evaluation")
print("="*60)
test_metrics = evaluate(model, test_loader, threshold=best_thresh)
print(f"Test Accuracy: {test_metrics['accuracy']:.4f}")
print(f"Test AUC: {test_metrics['auc']:.4f}")
print(f"Test F1: {test_metrics['f1']:.4f}")
print(f"Test Precision: {test_metrics['precision']:.4f}")
print(f"Test Recall: {test_metrics['recall']:.4f}")
# Save results
results = {
'best_threshold': float(best_thresh),
'test_metrics': {
'accuracy': float(test_metrics['accuracy']),
'auc': float(test_metrics['auc']),
'f1': float(test_metrics['f1']),
'precision': float(test_metrics['precision']),
'recall': float(test_metrics['recall'])
},
'history': history
}
with open(MODELS_DIR / "training_results.json", 'w') as f:
json.dump(results, f, indent=2)
print(f"\n✅ Training complete!")
print(f"📁 Model saved: {MODELS_DIR}/ensemble_best.pth")
print(f"📊 Results saved: {MODELS_DIR}/training_results.json")
if __name__ == "__main__":
main()