| """Training utilities: data loading, training loops, metric computation.""" |
|
|
| import torch |
| import torch.nn as nn |
| import torch.optim as optim |
| from torch.utils.data import DataLoader, TensorDataset |
| import numpy as np |
| from sklearn.model_selection import train_test_split |
| from sklearn.preprocessing import StandardScaler |
| from sklearn.datasets import make_moons, make_circles, load_iris, load_wine, load_breast_cancer |
|
|
|
|
| def load_synthetic_dataset(name="moons", n_samples=1000, noise=0.2, seed=42): |
| """Load synthetic 2D dataset: 'moons', 'circles', or 'xor'.""" |
| rng = np.random.RandomState(seed) |
| if name == "moons": |
| return make_moons(n_samples=n_samples, noise=noise, random_state=seed) |
| elif name == "circles": |
| return make_circles(n_samples=n_samples, noise=noise, factor=0.5, random_state=seed) |
| elif name == "xor": |
| X = rng.randn(n_samples, 2) |
| y = ((X[:, 0] > 0) ^ (X[:, 1] > 0)).astype(int) |
| X += noise * rng.randn(n_samples, 2) |
| return X, y |
| raise ValueError(f"Unknown dataset: {name}") |
|
|
|
|
| def load_uci_dataset(name="iris"): |
| """Load UCI dataset. Returns (X, y, task_type).""" |
| loaders = { |
| "iris": (load_iris, "classification"), |
| "wine": (load_wine, "classification"), |
| "breast_cancer": (load_breast_cancer, "classification"), |
| } |
| if name == "california_housing": |
| from sklearn.datasets import fetch_california_housing |
| d = fetch_california_housing() |
| return d.data, d.target, "regression" |
| if name not in loaders: |
| raise ValueError(f"Unknown dataset: {name}") |
| d = loaders[name][0]() |
| return d.data, d.target, loaders[name][1] |
|
|
|
|
| def prepare_data(X, y, test_size=0.2, batch_size=64, seed=42, scale=True): |
| """Split, scale, and create DataLoaders. Returns (train_loader, test_loader, d_in, n_classes).""" |
| stratify = y if len(np.unique(y)) < 20 else None |
| X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=test_size, random_state=seed, stratify=stratify) |
| if scale: |
| sc = StandardScaler() |
| X_tr, X_te = sc.fit_transform(X_tr), sc.transform(X_te) |
| X_tr_t, X_te_t = torch.FloatTensor(X_tr), torch.FloatTensor(X_te) |
| n_classes = len(np.unique(y)) |
| if n_classes > 20: |
| y_tr_t = torch.FloatTensor(y_tr).unsqueeze(-1) |
| y_te_t = torch.FloatTensor(y_te).unsqueeze(-1) |
| n_classes = 1 |
| else: |
| y_tr_t, y_te_t = torch.LongTensor(y_tr), torch.LongTensor(y_te) |
| train_loader = DataLoader(TensorDataset(X_tr_t, y_tr_t), batch_size=batch_size, shuffle=True) |
| test_loader = DataLoader(TensorDataset(X_te_t, y_te_t), batch_size=batch_size) |
| return train_loader, test_loader, X_tr.shape[1], n_classes |
|
|
|
|
| def train_epoch(model, loader, criterion, optimizer, device): |
| """Train one epoch. Returns average loss.""" |
| model.train() |
| total, n = 0.0, 0 |
| for xb, yb in loader: |
| xb, yb = xb.to(device), yb.to(device) |
| optimizer.zero_grad() |
| loss = criterion(model(xb), yb) |
| loss.backward() |
| optimizer.step() |
| total += loss.item() |
| n += 1 |
| return total / max(n, 1) |
|
|
|
|
| @torch.no_grad() |
| def evaluate(model, loader, criterion, device, task="classification"): |
| """Evaluate model. Returns dict with 'loss' and 'accuracy' or 'rmse'.""" |
| model.eval() |
| total_loss, correct, total = 0.0, 0, 0 |
| preds_all, targs_all = [], [] |
| for xb, yb in loader: |
| xb, yb = xb.to(device), yb.to(device) |
| out = model(xb) |
| total_loss += criterion(out, yb).item() |
| if task == "classification": |
| correct += (out.argmax(-1) == yb).sum().item() |
| total += yb.shape[0] |
| else: |
| preds_all.append(out.cpu()) |
| targs_all.append(yb.cpu()) |
| res = {"loss": total_loss / max(len(loader), 1)} |
| if task == "classification": |
| res["accuracy"] = correct / max(total, 1) |
| else: |
| res["rmse"] = torch.sqrt(((torch.cat(preds_all) - torch.cat(targs_all)) ** 2).mean()).item() |
| return res |
|
|
|
|
| def train_model(model, train_loader, test_loader, n_epochs=100, lr=1e-3, device=None, task="classification", verbose=True, print_every=10): |
| """Full training loop. Returns history dict.""" |
| if device is None: |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| model = model.to(device) |
| criterion = nn.CrossEntropyLoss() if task == "classification" else nn.MSELoss() |
| optimizer = optim.Adam(model.parameters(), lr=lr) |
| history = {"train_losses": [], "test_losses": [], "test_metrics": []} |
| for epoch in range(1, n_epochs + 1): |
| tl = train_epoch(model, train_loader, criterion, optimizer, device) |
| te = evaluate(model, test_loader, criterion, device, task) |
| history["train_losses"].append(tl) |
| history["test_losses"].append(te["loss"]) |
| mk = "accuracy" if task == "classification" else "rmse" |
| history["test_metrics"].append(te[mk]) |
| if verbose and epoch % print_every == 0: |
| print(f"Epoch {epoch:4d} | Train: {tl:.4f} | Test: {te['loss']:.4f} | {mk}: {te[mk]:.4f}") |
| return history |
|
|