disastersense / train.py
AsmitaG11's picture
Upload 7 files
fcc9242 verified
Raw
History Blame Contribute Delete
6.06 kB
"""
DisasterSense | Image Model Training
Fine-tunes EfficientNet-B0 on CrisisMMD damage severity classification.
"""
import os
import json
import torch
import torch.nn as nn
from pathlib import Path
from torchvision import models
from torch.optim import AdamW
from torch.optim.lr_scheduler import CosineAnnealingLR
from sklearn.metrics import classification_report, confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns
from preprocess import build_dataloaders, compute_class_weights, LABEL_MAP, PROCESSED
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
NUM_CLASSES = len(LABEL_MAP)
EPOCHS = 20
BATCH_SIZE = 32
LR = 1e-4
MODEL_DIR = Path("models/image_model")
MODEL_DIR.mkdir(parents=True, exist_ok=True)
print(f"Device: {DEVICE}")
def build_model():
model = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.DEFAULT)
for param in model.parameters():
param.requires_grad = False
blocks_to_unfreeze = list(model.features.children())[-3:]
for block in blocks_to_unfreeze:
for param in block.parameters():
param.requires_grad = True
in_features = model.classifier[1].in_features
model.classifier = nn.Sequential(
nn.Dropout(p=0.4),
nn.Linear(in_features, 128),
nn.ReLU(),
nn.Dropout(p=0.3),
nn.Linear(128, NUM_CLASSES),
)
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"Trainable params: {trainable:,} / {total:,}")
return model.to(DEVICE)
def train_epoch(model, loader, criterion, optimizer):
model.train()
total_loss, correct, total = 0.0, 0, 0
for images, labels in loader:
images, labels = images.to(DEVICE), labels.to(DEVICE)
optimizer.zero_grad()
loss = criterion(model(images), labels)
loss.backward()
optimizer.step()
total_loss += loss.item() * images.size(0)
correct += (model(images).argmax(1) == labels).sum().item()
total += images.size(0)
return total_loss / total, correct / total
def evaluate(model, loader, criterion):
model.eval()
total_loss, correct, total = 0.0, 0, 0
with torch.no_grad():
for images, labels in loader:
images, labels = images.to(DEVICE), labels.to(DEVICE)
outputs = model(images)
total_loss += criterion(outputs, labels).item() * images.size(0)
correct += (outputs.argmax(1) == labels).sum().item()
total += images.size(0)
return total_loss / total, correct / total
def plot_curves(history):
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
fig.suptitle("Training Curves", fontsize=13, fontweight="bold")
for ax, metric in zip(axes, ["loss", "acc"]):
ax.plot(history[f"train_{metric}"], label="Train")
ax.plot(history[f"val_{metric}"], label="Val")
ax.set_title(metric.capitalize())
ax.set_xlabel("Epoch")
ax.legend()
plt.tight_layout()
plt.savefig(MODEL_DIR / "curves.png", dpi=150, bbox_inches="tight")
plt.show()
def evaluate_test(model, loader):
model.eval()
preds, targets = [], []
with torch.no_grad():
for images, labels in loader:
preds.extend(model(images.to(DEVICE)).argmax(1).cpu().tolist())
targets.extend(labels.tolist())
idx2label = {v: k for k, v in LABEL_MAP.items()}
pred_names = [idx2label[p] for p in preds]
true_names = [idx2label[t] for t in targets]
print("\n── Classification Report ─────────────────────────────")
print(classification_report(true_names, pred_names))
cm = confusion_matrix(true_names, pred_names, labels=sorted(LABEL_MAP.keys()))
fig, ax = plt.subplots(figsize=(7, 6))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues",
xticklabels=sorted(LABEL_MAP.keys()),
yticklabels=sorted(LABEL_MAP.keys()), ax=ax)
ax.set_title("Confusion Matrix β€” Test Set", fontsize=13, fontweight="bold")
ax.set_ylabel("True")
ax.set_xlabel("Predicted")
plt.xticks(rotation=20)
plt.tight_layout()
plt.savefig(MODEL_DIR / "confusion_matrix.png", dpi=150, bbox_inches="tight")
plt.show()
if __name__ == "__main__":
loaders = build_dataloaders(BATCH_SIZE)
weights = compute_class_weights(PROCESSED / "damage_train.csv").to(DEVICE)
model = build_model()
criterion = nn.CrossEntropyLoss(weight=weights)
optimizer = AdamW([
{"params": [p for p in model.features.parameters() if p.requires_grad], "lr": LR * 0.1},
{"params": model.classifier.parameters(), "lr": LR},
], weight_decay=1e-4)
scheduler = CosineAnnealingLR(optimizer, T_max=EPOCHS)
history = {"train_loss": [], "val_loss": [], "train_acc": [], "val_acc": []}
best_val = 0.0
print("\n── Training ──────────────────────────────────────────")
for epoch in range(1, EPOCHS + 1):
tl, ta = train_epoch(model, loaders["train"], criterion, optimizer)
vl, va = evaluate(model, loaders["dev"], criterion)
scheduler.step()
for k, v in zip(["train_loss","val_loss","train_acc","val_acc"], [tl,vl,ta,va]):
history[k].append(v)
print(f"Epoch {epoch:02d}/{EPOCHS} | Train Loss: {tl:.4f} Acc: {ta:.4f} | Val Loss: {vl:.4f} Acc: {va:.4f}")
if va > best_val:
best_val = va
torch.save(model.state_dict(), MODEL_DIR / "best.pt")
print(f" β†’ Saved (val_acc: {best_val:.4f})")
with open(MODEL_DIR / "history.json", "w") as f:
json.dump(history, f, indent=2)
plot_curves(history)
model.load_state_dict(torch.load(MODEL_DIR / "best.pt"))
evaluate_test(model, loaders["test"])
print(f"\nBest val accuracy: {best_val:.4f}")