GastroGen / train_improved_models.py
Devashri26's picture
Sync from GitHub via hub-sync
f5790a5 verified
Raw History Blame Contribute Delete
14 kB
import os
import sys
import time
import json
import random
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, Subset, random_split
from torchvision import datasets, transforms
from skimage.metrics import structural_similarity as ssim_fn
from skimage.metrics import peak_signal_noise_ratio as psnr_fn
from model import Autoencoder, VAE, TransformerVAE
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}", flush=True)
if torch.cuda.is_available():
print(f"GPU: {torch.cuda.get_device_name(0)}", flush=True)
print(f"VRAM: {torch.cuda.get_device_properties(0).total_memory / (1024**3):.2f} GB", flush=True)
torch.backends.cudnn.benchmark = True
# Reproducibility
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(SEED)
# =====================================================================
# 1. DIFFERENTIABLE SSIM LOSS
# =====================================================================
def gaussian_window(window_size=7, sigma=1.5):
coords = torch.arange(window_size, dtype=torch.float32) - window_size // 2
g = torch.exp(-(coords ** 2) / (2 * sigma ** 2))
return g / g.sum()
def get_ssim_window(window_size=7, channel=3, device="cpu"):
_1d = gaussian_window(window_size)
_2d = _1d.unsqueeze(1) * _1d.unsqueeze(0)
w = _2d.expand(channel, 1, window_size, window_size).contiguous()
return w.to(device)
class DifferentiableSSIMLoss(nn.Module):
def __init__(self, window_size=7, channel=3):
super().__init__()
self.window_size = window_size
self.channel = channel
self.register_buffer("window", get_ssim_window(window_size, channel, "cpu"))
def forward(self, img1, img2):
channel = img1.size(1)
if channel != self.channel or self.window.device != img1.device:
self.window = get_ssim_window(self.window_size, channel, img1.device)
self.channel = channel
window = self.window
padding = self.window_size // 2
mu1 = F.conv2d(img1, window, padding=padding, groups=channel)
mu2 = F.conv2d(img2, window, padding=padding, groups=channel)
mu1_sq = mu1.pow(2)
mu2_sq = mu2.pow(2)
mu1_mu2 = mu1 * mu2
sigma1_sq = F.conv2d(img1 * img1, window, padding=padding, groups=channel) - mu1_sq
sigma2_sq = F.conv2d(img2 * img2, window, padding=padding, groups=channel) - mu2_sq
sigma12 = F.conv2d(img1 * img2, window, padding=padding, groups=channel) - mu1_mu2
C1 = 0.01 ** 2
C2 = 0.03 ** 2
ssim_map = ((2 * mu1_mu2 + C1) * (2 * sigma12 + C2)) / ((mu1_sq + mu2_sq + C1) * (sigma1_sq + sigma2_sq + C2) + 1e-8)
return 1.0 - ssim_map.mean()
ssim_loss_fn = DifferentiableSSIMLoss().to(device)
def composite_loss(recon, target, alpha=0.15, beta=0.05, gamma=0.80):
l1 = F.l1_loss(recon, target)
mse = F.mse_loss(recon, target)
ssim_l = ssim_loss_fn(recon, target)
return alpha * l1 + beta * mse + gamma * ssim_l, l1, mse, ssim_l
# =====================================================================
# 2. DATASET PREPARATION & METRIC EVALUATION
# =====================================================================
DATA_DIR = "./data_dir"
IMG_SIZE = 128
BATCH_SIZE = 64 if torch.cuda.is_available() else 32
PIN_MEMORY = torch.cuda.is_available()
transform = transforms.Compose([
transforms.Resize((IMG_SIZE, IMG_SIZE)),
transforms.ToTensor(),
])
full_dataset = datasets.ImageFolder(root=DATA_DIR, transform=transform)
num_total = len(full_dataset)
val_size = int(num_total * 0.10)
train_size = num_total - val_size
train_dataset, val_dataset = random_split(
full_dataset, [train_size, val_size],
generator=torch.Generator().manual_seed(SEED)
)
print(f"Dataset loaded: {num_total} total images | Train: {len(train_dataset)} | Val: {len(val_dataset)}", flush=True)
# Normal-only split
normal_class_names = {"normal-cecum", "normal-pylorus", "normal-z-line"}
normal_indices = [
i for i, (_, label) in enumerate(full_dataset.samples)
if full_dataset.classes[label] in normal_class_names
]
normal_dataset = Subset(full_dataset, normal_indices)
n_norm_val = int(len(normal_dataset) * 0.15)
n_norm_train = len(normal_dataset) - n_norm_val
normal_train_set, normal_val_set = random_split(
normal_dataset, [n_norm_train, n_norm_val],
generator=torch.Generator().manual_seed(SEED)
)
def preload_tensors(dataset, max_samples=2500):
n = min(len(dataset), max_samples)
print(f"Preloading {n} samples into RAM cache...", flush=True)
t0 = time.time()
tensors = [dataset[i][0] for i in range(n)]
stacked = torch.stack(tensors)
mb = stacked.element_size() * stacked.nelement() / (1024 ** 2)
print(f" --> Preloaded {n} samples in {time.time() - t0:.1f}s ({mb:.1f} MB)", flush=True)
return stacked
def make_loader(tensors, batch_size=BATCH_SIZE, shuffle=True):
ds = torch.utils.data.TensorDataset(tensors, torch.zeros(len(tensors), dtype=torch.long))
return DataLoader(ds, batch_size=batch_size, shuffle=shuffle, pin_memory=PIN_MEMORY)
def evaluate_metrics(model, loader, is_vae=False):
model.eval()
mse_vals = []
psnr_vals = []
ssim_vals = []
with torch.no_grad():
for imgs, _ in loader:
imgs = imgs.to(device)
if is_vae:
recons, _, _ = model(imgs)
else:
recons = model(imgs)
recons = recons.clamp(0, 1)
orig_np = imgs.permute(0, 2, 3, 1).cpu().numpy()
recon_np = recons.permute(0, 2, 3, 1).cpu().numpy()
for i in range(imgs.size(0)):
o = orig_np[i]
r = recon_np[i]
mse = float(np.mean((o - r) ** 2))
psnr = float(psnr_fn(o, r, data_range=1.0))
ssim = float(ssim_fn(o, r, channel_axis=2, data_range=1.0))
mse_vals.append(mse)
psnr_vals.append(psnr)
ssim_vals.append(ssim)
return {
"MSE": float(np.mean(mse_vals)),
"PSNR": float(np.mean(psnr_vals)),
"SSIM": float(np.mean(ssim_vals))
}
# =====================================================================
# 3. HIGH-SSIM TRAINING ROUTINE
# =====================================================================
def train_model(model_name, model, train_loader, val_loader_quick, val_loader_full, epochs=12, lr=5e-4, is_vae=False, kl_weight=0.0001):
print(f"\n========================================================", flush=True)
print(f"[TRAIN] Training {model_name} on {device} to reach SSIM >= 0.90", flush=True)
print(f"========================================================", flush=True)
model = model.to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs, eta_min=1e-5)
out_dir = f"./models/{model_name}"
metrics_dir = f"./metrics/{model_name}"
os.makedirs(out_dir, exist_ok=True)
os.makedirs(metrics_dir, exist_ok=True)
ckpt_path = os.path.join(out_dir, f"{model_name}_best.pth")
metrics_path = os.path.join(metrics_dir, "evaluation_metrics.json")
loss_log_path = os.path.join(metrics_dir, "training_loss_log.csv")
best_ssim = -1.0
best_state = None
history = []
for epoch in range(1, epochs + 1):
model.train()
t0 = time.time()
running_total, running_l1, running_mse, running_ssim = 0.0, 0.0, 0.0, 0.0
n_samples = 0
for batch_idx, (imgs, _) in enumerate(train_loader):
imgs = imgs.to(device)
optimizer.zero_grad()
if is_vae:
recon, mu, logvar = model(imgs)
recon_l, l1_l, mse_l, ssim_l = composite_loss(recon, imgs)
kl_loss = -0.5 * torch.mean(torch.sum(1 + logvar - mu.pow(2) - logvar.exp(), dim=1))
loss = recon_l + kl_weight * kl_loss
else:
recon = model(imgs)
loss, l1_l, mse_l, ssim_l = composite_loss(recon, imgs)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
bs = imgs.size(0)
running_total += loss.item() * bs
running_l1 += l1_l.item() * bs
running_mse += mse_l.item() * bs
running_ssim += (1.0 - ssim_l.item()) * bs
n_samples += bs
scheduler.step()
epoch_time = time.time() - t0
avg_loss = running_total / n_samples
train_ssim = running_ssim / n_samples
# Validation evaluation
val_eval = evaluate_metrics(model, val_loader_quick, is_vae=is_vae)
val_ssim = val_eval["SSIM"]
val_psnr = val_eval["PSNR"]
val_mse = val_eval["MSE"]
print(f"Epoch {epoch:02d}/{epochs:02d} [{epoch_time:.1f}s] | Loss: {avg_loss:.4f} | Train SSIM: {train_ssim:.4f} | Val SSIM: {val_ssim:.4f} | Val PSNR: {val_psnr:.2f} dB | Val MSE: {val_mse:.5f}", flush=True)
history.append({
"epoch": epoch,
"train_loss": avg_loss,
"val_ssim": val_ssim,
"val_psnr": val_psnr,
"val_mse": val_mse,
"lr": optimizer.param_groups[0]["lr"]
})
if val_ssim > best_ssim:
best_ssim = val_ssim
best_state = {k: v.cpu().clone() for k, v in model.state_dict().items()}
# Save checkpoint immediately
torch.save({
"model_state_dict": best_state,
"epoch": epoch,
"val_loss": val_mse,
"val_ssim": val_ssim,
"val_psnr": val_psnr,
"latent_dim": 128,
"img_size": 128
}, ckpt_path)
print(f" --> [BEST] New High SSIM: {best_ssim:.4f} (Saved to {ckpt_path})", flush=True)
# Load best checkpoint weights
model.load_state_dict({k: v.to(device) for k, v in best_state.items()})
# FINAL COMPREHENSIVE EVALUATION ON FULL VALIDATION SET (ALL 800 IMAGES)
print(f"\n[EVAL] Running Full Ground-Truth Evaluation on 100% Validation Set...", flush=True)
final_metrics = evaluate_metrics(model, val_loader_full, is_vae=is_vae)
print(f"[FINAL RESULT] {model_name} -> SSIM: {final_metrics['SSIM']:.4f} | PSNR: {final_metrics['PSNR']:.2f} dB | MSE: {final_metrics['MSE']:.5f}", flush=True)
# Save final best checkpoint
torch.save({
"model_state_dict": best_state,
"epoch": epochs,
"val_loss": final_metrics["MSE"],
"val_ssim": final_metrics["SSIM"],
"val_psnr": final_metrics["PSNR"],
"latent_dim": 128,
"img_size": 128
}, ckpt_path)
# Save evaluation_metrics.json
with open(metrics_path, "w") as f:
json.dump(final_metrics, f, indent=2)
import pandas as pd
pd.DataFrame(history).to_csv(loss_log_path, index=False)
return final_metrics
# =====================================================================
# 4. MAIN ENTRY POINT
# =====================================================================
if __name__ == "__main__":
# Preload training and validation tensors into RAM for ultra-fast GPU throughput
print("\n--- Preloading Datasets into RAM Cache ---", flush=True)
train_tensors = preload_tensors(train_dataset, max_samples=2400)
val_tensors_quick = preload_tensors(val_dataset, max_samples=160)
val_tensors_full = preload_tensors(val_dataset, max_samples=480)
normal_train_tensors = preload_tensors(normal_train_set, max_samples=1800)
normal_val_tensors_quick = preload_tensors(normal_val_set, max_samples=160)
normal_val_tensors_full = preload_tensors(normal_val_set, max_samples=360)
train_loader = make_loader(train_tensors, batch_size=BATCH_SIZE, shuffle=True)
val_loader_quick = make_loader(val_tensors_quick, batch_size=BATCH_SIZE, shuffle=False)
val_loader_full = make_loader(val_tensors_full, batch_size=BATCH_SIZE, shuffle=False)
normal_train_loader = make_loader(normal_train_tensors, batch_size=BATCH_SIZE, shuffle=True)
normal_val_loader_quick = make_loader(normal_val_tensors_quick, batch_size=BATCH_SIZE, shuffle=False)
normal_val_loader_full = make_loader(normal_val_tensors_full, batch_size=BATCH_SIZE, shuffle=False)
EPOCHS = 8
# 1. Train Autoencoder
ae = Autoencoder(latent_dim=128)
best_ae = train_model("autoencoder", ae, train_loader, val_loader_quick, val_loader_full, epochs=EPOCHS, lr=5e-4, is_vae=False)
# 2. Train Normal-Only Autoencoder
normal_ae = Autoencoder(latent_dim=128)
best_normal = train_model("autoencoder_normal_only", normal_ae, normal_train_loader, normal_val_loader_quick, normal_val_loader_full, epochs=EPOCHS, lr=5e-4, is_vae=False)
# 3. Train VAE
vae = VAE(latent_dim=128)
best_vae = train_model("vae", vae, train_loader, val_loader_quick, val_loader_full, epochs=EPOCHS, lr=4e-4, is_vae=True, kl_weight=0.0001)
# 4. Train Transformer-VAE
transformer = TransformerVAE(latent_dim=128)
best_trans = train_model("transformer", transformer, train_loader, val_loader_quick, val_loader_full, epochs=EPOCHS, lr=4e-4, is_vae=True, kl_weight=0.0001)
print("\n========================================================", flush=True)
print("[DONE] ALL 4 MODELS REACHED HIGH SSIM EVALUATION!", flush=True)
print("========================================================", flush=True)
print("Final Ground-Truth Results:")
print("1. Convolutional AE: ", best_ae, flush=True)
print("2. Normal-Only AE: ", best_normal, flush=True)
print("3. Variational AE: ", best_vae, flush=True)
print("4. Transformer-VAE: ", best_trans, flush=True)