Spaces:
Sleeping
Sleeping
| """ | |
| 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}") | |