Download backend/src/cadmorph/classify/model.py from bolleman/CADMorph: direct link, hf CLI and curl.
- Browser
- Download file 3.51 kB
-
https://huggingface.co/spaces/bolleman/CADMorph/resolve/main/backend/src/cadmorph/classify/model.py
- Command line
-
hf download hf://spaces/bolleman/CADMorph/backend/src/cadmorph/classify/model.py
-
curl -L -o model.py https://huggingface.co/spaces/bolleman/CADMorph/resolve/main/backend/src/cadmorph/classify/model.py
3.51 kB
| """GAT symbol-spotting model (T020, research R4). | |
| A two-layer Graph Attention Network over the attributed drawing graph: | |
| node = entity, output = semantic type logits. Weights are pinned artifacts — | |
| loaded in eval mode and hash-checked against models/meta.json so a silent | |
| weight swap cannot change outputs unnoticed (Constitution IV, R11). | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from pathlib import Path | |
| import torch | |
| from torch_geometric.nn import GATConv | |
| from cadmorph.classify.features import CLASSES, FEATURE_DIM | |
| from cadmorph.determinism import check_weights, file_sha256 | |
| CLASSIFIER_WEIGHTS = "gat_classifier.pt" | |
| META_FILE = "meta.json" | |
| def models_dir() -> Path: | |
| override = os.environ.get("CADMORPH_MODELS_DIR") | |
| if override: | |
| return Path(override) | |
| return Path(__file__).resolve().parents[3] / "models" | |
| class GATClassifier(torch.nn.Module): | |
| """GAT with a self-feature skip path. | |
| Pure stacked attention over kNN-proximity edges over-smooths: an | |
| entity's own vector-exact features (size, stroke, segment structure) | |
| are what identify its symbol class, while attention supplies | |
| neighborhood context. The head sees both, concatenated. | |
| """ | |
| def __init__(self, in_dim: int = FEATURE_DIM, hidden: int = 32, heads: int = 4) -> None: | |
| super().__init__() | |
| self.lin_self = torch.nn.Linear(in_dim, hidden * heads) | |
| self.conv1 = GATConv(in_dim, hidden, heads=heads) | |
| self.conv2 = GATConv(hidden * heads, hidden, heads=1) | |
| self.head = torch.nn.Linear(hidden * heads + hidden, len(CLASSES)) | |
| def forward(self, x: torch.Tensor, edge_index: torch.Tensor) -> torch.Tensor: | |
| h_self = torch.nn.functional.relu(self.lin_self(x)) | |
| h_graph = torch.nn.functional.elu(self.conv1(x, edge_index)) | |
| h_graph = torch.nn.functional.elu(self.conv2(h_graph, edge_index)) | |
| return self.head(torch.cat([h_self, h_graph], dim=1)) | |
| def save_pinned(model: torch.nn.Module, weights_name: str, directory: Path | None = None) -> str: | |
| """Save weights and record their hash in meta.json; returns the hash.""" | |
| directory = directory or models_dir() | |
| directory.mkdir(parents=True, exist_ok=True) | |
| path = directory / weights_name | |
| torch.save(model.state_dict(), path) | |
| digest = file_sha256(path) | |
| meta_path = directory / META_FILE | |
| meta = json.loads(meta_path.read_text(encoding="utf-8")) if meta_path.exists() else {} | |
| meta[weights_name] = digest | |
| meta_path.write_text(json.dumps(meta, indent=2, sort_keys=True), encoding="utf-8") | |
| return digest | |
| def pinned_hash(weights_name: str, directory: Path | None = None) -> str: | |
| directory = directory or models_dir() | |
| meta = json.loads((directory / META_FILE).read_text(encoding="utf-8")) | |
| return meta[weights_name] | |
| def load_pinned( | |
| model: torch.nn.Module, weights_name: str, directory: Path | None = None | |
| ) -> torch.nn.Module: | |
| """Load hash-checked weights into ``model`` and freeze it in eval mode.""" | |
| directory = directory or models_dir() | |
| path = directory / weights_name | |
| check_weights(path, pinned_hash(weights_name, directory)) | |
| model.load_state_dict(torch.load(path, map_location="cpu", weights_only=True)) | |
| model.eval() | |
| for param in model.parameters(): | |
| param.requires_grad_(False) | |
| return model | |
| def load_classifier(directory: Path | None = None) -> GATClassifier: | |
| return load_pinned(GATClassifier(), CLASSIFIER_WEIGHTS, directory) # type: ignore[return-value] | |