| """Loaders and inference for the C / A / Q code-selection scorers. |
| |
| A scorer set is a flat directory, one per language, such as ``classifiers/cpp``:: |
| |
| <lang>/ |
| C_file_role.bin C_file_role.json # classifier + its class names |
| A_relevance.bin # checkpoint carries its own config |
| Q_quality.bin # architecture read from its shapes |
| Q_scaler.npz # Q's StandardScaler (mean / scale) |
| |
| All five files are required. Layer widths are recovered from the checkpoints' |
| own tensor shapes rather than declared in config, so the only metadata shipped |
| alongside the weights is ``C_file_role.json`` — the index-to-category map, the |
| one thing the weights cannot carry. |
| |
| ``LanguageScorers.load(dir)`` reads the artifacts and configures itself from |
| what they declare; nothing about the featurization is hard-coded per language. |
| |
| All three models consume a precomputed code embedding. Embeddings must come |
| from the same encoder and dimension used at training time — the loader checks |
| the dimension, but cannot detect a wrong encoder at the right dimension. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| from pathlib import Path |
|
|
| import numpy as np |
| import torch |
| import torch.nn as nn |
|
|
| from path_features import PATH_HASH_ALGORITHM, build_feature_matrix |
|
|
|
|
| |
| |
| |
| class FileRoleClassifier(nn.Module): |
| """C: Linear/LayerNorm/GELU/Dropout stack -> num_classes logits.""" |
|
|
| def __init__(self, input_dim, hidden_dims, num_classes, dropout=0.3): |
| super().__init__() |
| layers = [] |
| prev = input_dim |
| for i, h in enumerate(hidden_dims): |
| layers += [ |
| nn.Linear(prev, h), |
| nn.LayerNorm(h), |
| nn.GELU(), |
| nn.Dropout(dropout if i == 0 else dropout * 0.7), |
| ] |
| prev = h |
| layers.append(nn.Linear(prev, num_classes)) |
| self.net = nn.Sequential(*layers) |
|
|
| def forward(self, x): |
| return self.net(x) |
|
|
|
|
| class RelevanceMLP(nn.Module): |
| """A: two hidden ReLU layers -> single logit.""" |
|
|
| def __init__(self, in_dim, hidden1, hidden2): |
| super().__init__() |
| self.fc1 = nn.Linear(in_dim, hidden1) |
| self.fc2 = nn.Linear(hidden1, hidden2) |
| self.fc3 = nn.Linear(hidden2, 1) |
|
|
| def forward(self, x): |
| x = torch.relu(self.fc1(x)) |
| x = torch.relu(self.fc2(x)) |
| return self.fc3(x).squeeze(-1) |
|
|
|
|
| class _QualityBackbone(nn.Module): |
| """Q's shared trunk. |
| |
| Kept as its own module holding a ``self.net`` Sequential because the saved |
| state_dicts are keyed ``backbone.net.0.*``. Inlining the Sequential into |
| QualityMLP would renumber the keys to ``backbone.0.*`` and fail to load. |
| """ |
|
|
| def __init__(self, in_dim, hidden_dim, dropout): |
| super().__init__() |
| mid = max(64, hidden_dim // 2) |
| self.net = nn.Sequential( |
| nn.Linear(in_dim, hidden_dim), |
| nn.GELU(), |
| nn.LayerNorm(hidden_dim), |
| nn.Dropout(dropout), |
| nn.Linear(hidden_dim, mid), |
| nn.GELU(), |
| nn.LayerNorm(mid), |
| nn.Dropout(dropout), |
| ) |
|
|
| def forward(self, x): |
| return self.net(x) |
|
|
|
|
| class QualityMLP(nn.Module): |
| """Q: shared trunk -> zero-score head + 1..10 ordinal head. |
| |
| Final score is ``(1 - sigmoid(zero)) * (1 + sum(sigmoid(ordinal)))``: the |
| zero head gates out config/data/no-logic files, the ordinal head places |
| everything else on 1..10. |
| """ |
|
|
| def __init__(self, in_dim, hidden_dim=512, dropout=0.1, ordinal_num_classes=10): |
| super().__init__() |
| mid = max(64, hidden_dim // 2) |
| self.backbone = _QualityBackbone(in_dim, hidden_dim, dropout) |
| self.zero_head = nn.Linear(mid, 1) |
| self.ordinal_levels = ordinal_num_classes - 1 |
| self.ordinal_head = nn.Linear(mid, self.ordinal_levels) |
|
|
| def forward(self, x): |
| features = self.backbone(x) |
| return self.zero_head(features).squeeze(-1), self.ordinal_head(features) |
|
|
|
|
| |
| |
| |
| class _Scaler: |
| """(x - mean) / scale, loaded from ``Q_scaler.npz``.""" |
|
|
| def __init__(self, mean: np.ndarray, scale: np.ndarray, source: str): |
| self.mean = mean.astype(np.float32) |
| self.scale = scale.astype(np.float32) |
| self.n_features_in = int(self.mean.size) |
| self.source = source |
|
|
| @classmethod |
| def load(cls, scorer_dir: Path) -> "_Scaler": |
| npz = scorer_dir / "Q_scaler.npz" |
| payload = np.load(npz) |
| return cls(payload["mean"], payload["scale"], npz.name) |
|
|
| def transform(self, x: np.ndarray) -> np.ndarray: |
| return ((x - self.mean) / self.scale).astype(np.float32) |
|
|
|
|
| |
| |
| |
| class LanguageScorers: |
| """Loads one language's scorer set and scores batches of embeddings.""" |
|
|
| def __init__(self, scorer_dir, device="cpu"): |
| self.scorer_dir = Path(scorer_dir) |
| self.device = torch.device(device) |
| self.language = self.scorer_dir.name |
|
|
| |
| @classmethod |
| def load(cls, scorer_dir, device="cpu") -> "LanguageScorers": |
| self = cls(scorer_dir, device) |
| self._load_file_role() |
| self._load_relevance() |
| self._load_quality() |
| if self.quality_in_dim != self.embedding_dim: |
| raise ValueError( |
| f"Q expects {self.quality_in_dim}-d input but C implies " |
| f"{self.embedding_dim}-d embeddings" |
| ) |
| return self |
|
|
| def _load_file_role(self) -> None: |
| cfg = json.loads((self.scorer_dir / "C_file_role.json").read_text()) |
|
|
| state = torch.load( |
| self.scorer_dir / "C_file_role.bin", map_location=self.device |
| ) |
| |
| |
| |
| linears = [v.shape for k, v in state.items() |
| if k.endswith("weight") and v.ndim == 2] |
| input_dim = int(linears[0][1]) |
| hidden_dims = [int(s[0]) for s in linears[:-1]] |
| num_classes = int(linears[-1][0]) |
|
|
| model = FileRoleClassifier(input_dim, hidden_dims, num_classes) |
| model.load_state_dict(state) |
| self.file_role_model = model.eval().to(self.device) |
| self.embedding_dim = input_dim |
|
|
| |
| self.idx2cat = {int(k): v for k, v in cfg["idx2cat"].items()} |
| if len(self.idx2cat) != num_classes: |
| raise ValueError( |
| f"C_file_role.json maps {len(self.idx2cat)} categories but the " |
| f"checkpoint has {num_classes} output units" |
| ) |
|
|
| def _load_relevance(self) -> None: |
| |
| |
| ckpt = torch.load( |
| self.scorer_dir / "A_relevance.bin", map_location=self.device |
| ) |
| arch = ckpt["arch"] |
| model = RelevanceMLP(arch["in_dim"], arch["hidden1"], arch["hidden2"]) |
| model.load_state_dict(ckpt["state_dict"]) |
| self.relevance_model = model.eval().to(self.device) |
| self.relevance_in_dim = int(arch["in_dim"]) |
|
|
| cfg = ckpt["feature_config"] |
| if not cfg.get("use_path_feature", False): |
| raise ValueError( |
| "this A checkpoint declares no path features; every scorer set " |
| "in this release feeds A a 1024-d embedding plus a 256-d path hash" |
| ) |
| self.path_hash_dim = int(cfg["path_hash_dim"]) |
| self.path_feature_weight = float(cfg["path_feature_weight"]) |
| self.path_column = "relative_path" |
| self.path_algorithm = PATH_HASH_ALGORITHM |
|
|
| expected = self.embedding_dim + self.path_hash_dim |
| if expected != self.relevance_in_dim: |
| raise ValueError( |
| f"A expects {self.relevance_in_dim}-d input but the configured " |
| f"featurization produces {expected}-d" |
| ) |
|
|
| def _load_quality(self) -> None: |
| self.quality_scaler = _Scaler.load(self.scorer_dir) |
| state = torch.load(self.scorer_dir / "Q_quality.bin", map_location=self.device) |
|
|
| |
| |
| |
| |
| in_dim = self.quality_scaler.n_features_in or int( |
| state["backbone.net.0.weight"].shape[1] |
| ) |
| hidden_dim = int(state["backbone.net.0.weight"].shape[0]) |
| ordinal_num_classes = int(state["ordinal_head.weight"].shape[0]) + 1 |
|
|
| model = QualityMLP(in_dim, hidden_dim, ordinal_num_classes=ordinal_num_classes) |
| model.load_state_dict(state) |
| self.quality_model = model.eval().to(self.device) |
| self.quality_in_dim = in_dim |
|
|
| |
| def predict_file_role(self, embeddings: np.ndarray): |
| with torch.no_grad(): |
| logits = self.file_role_model(torch.from_numpy(embeddings).to(self.device)) |
| probs = torch.softmax(logits, dim=1) |
| confidence, index = probs.max(dim=1) |
| roles = [self.idx2cat.get(int(i), "EXCLUDE") for i in index.cpu().numpy()] |
| return roles, confidence.cpu().numpy() |
|
|
| def predict_relevance(self, embeddings: np.ndarray, paths) -> np.ndarray: |
| features = build_feature_matrix( |
| embeddings, |
| paths, |
| path_hash_dim=self.path_hash_dim, |
| path_feature_weight=self.path_feature_weight, |
| ) |
| with torch.no_grad(): |
| tensor = torch.from_numpy(np.ascontiguousarray(features, dtype=np.float32)) |
| logits = self.relevance_model(tensor.to(self.device)) |
| scores = torch.sigmoid(logits).cpu().numpy() |
| return np.asarray(scores).reshape(-1) |
|
|
| def predict_quality(self, embeddings: np.ndarray) -> np.ndarray: |
| scaled = self.quality_scaler.transform(embeddings) |
| with torch.no_grad(): |
| zero_logit, ordinal_logits = self.quality_model( |
| torch.from_numpy(scaled).to(self.device) |
| ) |
| p_zero = torch.sigmoid(zero_logit) |
| expected_positive = 1.0 + torch.sigmoid(ordinal_logits).sum(dim=1) |
| out = (1.0 - p_zero) * expected_positive |
| return np.asarray(out.cpu().numpy()).reshape(-1) |
|
|
| def score(self, embeddings, paths=None) -> dict: |
| """Score a batch. Returns the four columns as numpy arrays / lists.""" |
| embeddings = np.ascontiguousarray(embeddings, dtype=np.float32) |
| if embeddings.ndim != 2 or embeddings.shape[1] != self.embedding_dim: |
| raise ValueError( |
| f"expected embeddings of shape [N, {self.embedding_dim}], " |
| f"got {tuple(embeddings.shape)}" |
| ) |
| if paths is None: |
| paths = [""] * len(embeddings) |
| roles, confidence = self.predict_file_role(embeddings) |
| return { |
| "category": roles, |
| "cls_confidence": confidence, |
| "algo_rel_score": self.predict_relevance(embeddings, paths), |
| "quality_score": self.predict_quality(embeddings), |
| } |
|
|
| def needs_paths(self) -> bool: |
| """True when any model consumes the file path. Always true: A does.""" |
| return self.path_hash_dim > 0 |
|
|
| def describe(self) -> dict: |
| return { |
| "language": self.language, |
| "scorer_dir": str(self.scorer_dir), |
| "embedding_dim": self.embedding_dim, |
| "C": { |
| "input_dim": self.embedding_dim, |
| "categories": [self.idx2cat[i] for i in sorted(self.idx2cat)], |
| }, |
| "A": { |
| "input_dim": self.relevance_in_dim, |
| "path_hash_dim": self.path_hash_dim, |
| "path_feature_weight": self.path_feature_weight, |
| "path_algorithm": self.path_algorithm, |
| "path_column": self.path_column, |
| }, |
| "Q": { |
| "strategy": "two_stage", |
| "input_dim": self.quality_in_dim, |
| "ordinal_num_classes": self.quality_model.ordinal_levels + 1, |
| "scaler_source": self.quality_scaler.source, |
| }, |
| } |
|
|