Download source/train.py from ARotting/meshgraph-gcn: direct link, hf CLI and curl.
- Browser
- Download file 6.39 kB
-
https://huggingface.co/ARotting/meshgraph-gcn/resolve/main/source/train.py
- Command line
-
hf download hf://ARotting/meshgraph-gcn/source/train.py
-
curl -L -o train.py https://huggingface.co/ARotting/meshgraph-gcn/resolve/main/source/train.py
6.39 kB
| from __future__ import annotations | |
| import json | |
| import random | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import trackio | |
| from model import MeshGraphGCN, normalize_adjacency, parameter_count | |
| from safetensors.torch import save_file | |
| from sklearn.linear_model import LogisticRegression | |
| from sklearn.metrics import ( | |
| accuracy_score, | |
| average_precision_score, | |
| confusion_matrix, | |
| f1_score, | |
| precision_score, | |
| recall_score, | |
| roc_auc_score, | |
| ) | |
| from torch.nn import functional as F | |
| PROJECT_DIR = Path(__file__).resolve().parent | |
| DATA_DIR = PROJECT_DIR / "data" | |
| ARTIFACT_DIR = PROJECT_DIR / "artifacts" / "meshgraph-gcn" | |
| def seed_everything(seed: int) -> None: | |
| random.seed(seed) | |
| np.random.seed(seed) | |
| torch.manual_seed(seed) | |
| def metrics(labels: np.ndarray, scores: np.ndarray, threshold: float = 0.5) -> dict: | |
| predictions = scores >= threshold | |
| return { | |
| "accuracy": float(accuracy_score(labels, predictions)), | |
| "roc_auc": float(roc_auc_score(labels, scores)), | |
| "average_precision": float(average_precision_score(labels, scores)), | |
| "precision": float(precision_score(labels, predictions, zero_division=0)), | |
| "recall": float(recall_score(labels, predictions, zero_division=0)), | |
| "f1": float(f1_score(labels, predictions, zero_division=0)), | |
| "confusion_matrix": confusion_matrix(labels, predictions).tolist(), | |
| } | |
| def best_threshold(labels: np.ndarray, scores: np.ndarray) -> float: | |
| candidates = np.quantile(scores, np.linspace(0.05, 0.95, 300)) | |
| return float( | |
| max( | |
| candidates, | |
| key=lambda threshold: f1_score( | |
| labels, | |
| scores >= threshold, | |
| zero_division=0, | |
| ), | |
| ) | |
| ) | |
| def main() -> None: | |
| seed_everything(2033) | |
| graph = np.load(DATA_DIR / "meshgraph.npz") | |
| features = graph["features"].astype(np.float32) | |
| labels = graph["labels"].astype(np.int64) | |
| train_indices = graph["train_indices"] | |
| validation_indices = graph["validation_indices"] | |
| test_indices = graph["test_indices"] | |
| mean = features[train_indices].mean(axis=0) | |
| scale = np.maximum(features[train_indices].std(axis=0), 1e-5) | |
| features = (features - mean) / scale | |
| feature_tensor = torch.from_numpy(features) | |
| label_tensor = torch.from_numpy(labels) | |
| adjacency = torch.from_numpy(graph["adjacency"].astype(np.float32)) | |
| normalized = normalize_adjacency(adjacency) | |
| baseline = LogisticRegression( | |
| class_weight="balanced", | |
| max_iter=2000, | |
| random_state=2033, | |
| ) | |
| baseline.fit(features[train_indices], labels[train_indices]) | |
| baseline_validation = baseline.predict_proba(features[validation_indices])[:, 1] | |
| baseline_threshold = best_threshold( | |
| labels[validation_indices], | |
| baseline_validation, | |
| ) | |
| baseline_test = baseline.predict_proba(features[test_indices])[:, 1] | |
| model = MeshGraphGCN(features=features.shape[1]) | |
| positive_weight = (labels[train_indices] == 0).sum() / max( | |
| 1, (labels[train_indices] == 1).sum() | |
| ) | |
| class_weights = torch.tensor([1.0, positive_weight], dtype=torch.float32) | |
| optimizer = torch.optim.AdamW(model.parameters(), lr=0.01, weight_decay=0.002) | |
| scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=350) | |
| best_validation_auc = -1.0 | |
| best_epoch = 0 | |
| best_state = None | |
| trackio.init( | |
| project="meshgraph-gcn", | |
| name="two-layer-gcn-v1", | |
| config={ | |
| "parameters": parameter_count(model), | |
| "nodes": len(labels), | |
| "edges": int(adjacency.sum().item() // 2), | |
| "train_labels": len(train_indices), | |
| "transductive": True, | |
| }, | |
| ) | |
| for epoch in range(1, 351): | |
| model.train() | |
| logits = model(feature_tensor, normalized) | |
| loss = F.cross_entropy( | |
| logits[train_indices], | |
| label_tensor[train_indices], | |
| weight=class_weights, | |
| ) | |
| optimizer.zero_grad(set_to_none=True) | |
| loss.backward() | |
| optimizer.step() | |
| scheduler.step() | |
| model.eval() | |
| with torch.no_grad(): | |
| probabilities = model(feature_tensor, normalized).softmax(dim=1)[:, 1] | |
| validation_auc = roc_auc_score( | |
| labels[validation_indices], | |
| probabilities[validation_indices].numpy(), | |
| ) | |
| if validation_auc > best_validation_auc: | |
| best_validation_auc = validation_auc | |
| best_epoch = epoch | |
| best_state = { | |
| key: value.detach().cpu().clone() | |
| for key, value in model.state_dict().items() | |
| } | |
| if epoch == 1 or epoch % 10 == 0: | |
| trackio.log( | |
| { | |
| "epoch": epoch, | |
| "train_loss": float(loss.detach()), | |
| "validation_roc_auc": validation_auc, | |
| "learning_rate": scheduler.get_last_lr()[0], | |
| } | |
| ) | |
| trackio.finish() | |
| assert best_state is not None | |
| model.load_state_dict(best_state) | |
| model.eval() | |
| with torch.no_grad(): | |
| gcn_scores = model(feature_tensor, normalized).softmax(dim=1)[:, 1].numpy() | |
| gcn_threshold = best_threshold( | |
| labels[validation_indices], | |
| gcn_scores[validation_indices], | |
| ) | |
| results = { | |
| "model": "MeshGraph GCN", | |
| "parameters": parameter_count(model), | |
| "nodes": len(labels), | |
| "edges": int(adjacency.sum().item() // 2), | |
| "best_epoch": best_epoch, | |
| "best_validation_roc_auc": best_validation_auc, | |
| "gcn_threshold": gcn_threshold, | |
| "gcn_test": metrics( | |
| labels[test_indices], | |
| gcn_scores[test_indices], | |
| gcn_threshold, | |
| ), | |
| "feature_only_logistic_test": metrics( | |
| labels[test_indices], | |
| baseline_test, | |
| baseline_threshold, | |
| ), | |
| } | |
| ARTIFACT_DIR.mkdir(parents=True, exist_ok=True) | |
| save_file(model.state_dict(), ARTIFACT_DIR / "model.safetensors") | |
| np.savez( | |
| ARTIFACT_DIR / "preprocessing.npz", | |
| mean=mean, | |
| scale=scale, | |
| ) | |
| (ARTIFACT_DIR / "evaluation.json").write_text( | |
| json.dumps(results, indent=2), | |
| encoding="utf-8", | |
| ) | |
| print(json.dumps(results, indent=2)) | |
| if __name__ == "__main__": | |
| main() | |