Muhammed Sayeedur Rahman
feat: Add production infrastructure, CI/CD, Docker, tests, and implementation plan
6f2de72 | """ | |
| Train EfficientNet-B4 texture branch on face-aligned portrait crops. | |
| Captures texture artifacts (skin smoothness, hair detail, boundary artifacts) | |
| that complement ViT's structural analysis. | |
| Dataset: Multi-source portrait dataset (GAN + diffusion + real) | |
| Output: models/efficient.pth | |
| Usage: | |
| python training/train_efficientnet_texture.py | |
| """ | |
| import sys | |
| import os | |
| from tqdm import tqdm | |
| ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) | |
| if ROOT_DIR not in sys.path: | |
| sys.path.insert(0, ROOT_DIR) | |
| os.environ["HF_HOME"] = os.path.join(ROOT_DIR, ".hf_cache") | |
| os.environ["HF_DATASETS_CACHE"] = os.path.join(ROOT_DIR, ".hf_cache", "datasets") | |
| import torch | |
| import torch.nn as nn | |
| from torch.utils.data import DataLoader | |
| from torch.optim.lr_scheduler import CosineAnnealingLR | |
| from core_models.efficientnet_texture import EfficientNetTexture | |
| from training.dataset_portraits import ( | |
| load_portrait_dataset, PortraitDataset, | |
| TRAIN_TRANSFORM, VAL_TRANSFORM, | |
| ) | |
| # -------- CONFIG -------- | |
| BATCH_SIZE = 8 # B4 is large, keep batch small for CPU | |
| EPOCHS = 15 | |
| BACKBONE_LR = 5e-6 | |
| HEAD_LR = 5e-4 | |
| MAX_SAMPLES = 20000 | |
| TRAIN_SPLIT = 0.85 | |
| MODEL_PATH = "models/efficient.pth" | |
| EARLY_STOPPING_PATIENCE = 4 | |
| LABEL_SMOOTHING = 0.05 | |
| # ------------------------- | |
| def main(): | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| print(f"Device: {device}") | |
| # -------- Dataset -------- | |
| print("\nLoading portrait dataset for texture training...") | |
| train_data, val_data = load_portrait_dataset( | |
| max_samples=MAX_SAMPLES, | |
| train_split=TRAIN_SPLIT, | |
| face_align=True, | |
| ) | |
| train_loader = DataLoader( | |
| PortraitDataset(train_data, TRAIN_TRANSFORM), | |
| batch_size=BATCH_SIZE, shuffle=True, num_workers=0, | |
| ) | |
| val_loader = DataLoader( | |
| PortraitDataset(val_data, VAL_TRANSFORM), | |
| batch_size=BATCH_SIZE, shuffle=False, num_workers=0, | |
| ) | |
| # -------- Model -------- | |
| model = EfficientNetTexture().to(device) | |
| criterion = nn.BCELoss() | |
| # Differential LR: frozen early layers won't update anyway | |
| trainable_backbone = [p for p in model.features.parameters() if p.requires_grad] | |
| head_params = list(model.classifier.parameters()) | |
| optimizer = torch.optim.AdamW([ | |
| {"params": trainable_backbone, "lr": BACKBONE_LR, "weight_decay": 1e-4}, | |
| {"params": head_params, "lr": HEAD_LR, "weight_decay": 1e-4}, | |
| ]) | |
| scheduler = CosineAnnealingLR(optimizer, T_max=EPOCHS, eta_min=1e-7) | |
| # -------- Training -------- | |
| print(f"\nTraining EfficientNet-B4 Texture (label_smoothing={LABEL_SMOOTHING})...\n") | |
| best_val_loss = float("inf") | |
| patience = 0 | |
| for epoch in range(EPOCHS): | |
| model.train() | |
| total_loss = 0.0 | |
| for imgs, labels in tqdm(train_loader, desc=f"Epoch {epoch+1}/{EPOCHS}"): | |
| imgs = imgs.to(device) | |
| labels = labels.unsqueeze(1).to(device) | |
| smoothed = labels * (1 - LABEL_SMOOTHING) + (1 - labels) * LABEL_SMOOTHING | |
| preds = model(imgs) | |
| loss = criterion(preds, smoothed) | |
| optimizer.zero_grad() | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) | |
| optimizer.step() | |
| total_loss += loss.item() | |
| avg_loss = total_loss / len(train_loader) | |
| # -------- Validation -------- | |
| model.eval() | |
| val_loss = 0.0 | |
| correct, total = 0, 0 | |
| with torch.no_grad(): | |
| for imgs, labels in val_loader: | |
| imgs = imgs.to(device) | |
| labels_t = labels.unsqueeze(1).to(device) | |
| preds = model(imgs) | |
| val_loss += criterion(preds, labels_t).item() | |
| correct += ((preds > 0.5).float() == labels_t).sum().item() | |
| total += labels.size(0) | |
| avg_val = val_loss / len(val_loader) | |
| val_acc = correct / total | |
| scheduler.step() | |
| print(f"Epoch [{epoch+1}/{EPOCHS}] " | |
| f"| Train: {avg_loss:.4f} | Val: {avg_val:.4f} | Acc: {val_acc:.4f}") | |
| if avg_val < best_val_loss: | |
| best_val_loss = avg_val | |
| patience = 0 | |
| os.makedirs("models", exist_ok=True) | |
| torch.save(model.state_dict(), MODEL_PATH) | |
| print(f" -> Best model saved (val_loss={avg_val:.4f})") | |
| else: | |
| patience += 1 | |
| if patience >= EARLY_STOPPING_PATIENCE: | |
| print(f"\nEarly stopping at epoch {epoch+1}") | |
| break | |
| print(f"\nEfficientNet-B4 Texture trained. Saved to: {MODEL_PATH}") | |
| if __name__ == "__main__": | |
| main() | |