Download packages/softasp-solver/src/softasp_solver/sudoku/objective.py from wabdalm/AS2: direct link, hf CLI and curl.
- Browser
- Download file 8.66 kB
-
https://huggingface.co/wabdalm/AS2/resolve/main/packages/softasp-solver/src/softasp_solver/sudoku/objective.py
- Command line
-
hf download hf://wabdalm/AS2/packages/softasp-solver/src/softasp_solver/sudoku/objective.py
-
curl -L -o objective.py https://huggingface.co/wabdalm/AS2/resolve/main/packages/softasp-solver/src/softasp_solver/sudoku/objective.py
8.66 kB
| """Objectives and diagnostics owned by the Sudoku instantiation.""" | |
| from typing import Any | |
| import torch | |
| import torch.nn.functional as F | |
| from torch import nn | |
| class UniquenessLoss(nn.Module): | |
| """Soft uniqueness regulariser: penalises high-entropy blank-cell distributions. | |
| A Sudoku puzzle has a *unique* solution when every blank cell's value is | |
| fully determined by the declared constraints and the given clues. A model | |
| that spreads probability across multiple equally-valid digit assignments | |
| for blank cells implicitly encodes multiple valid solutions. | |
| We proxy the uniqueness constraint by minimising the mean normalised | |
| entropy of post-reasoning distributions over blank cells, driving the | |
| model to commit to specific symbol assignments rather than remaining | |
| ambiguous. This is a fully-differentiable continuous analog of the | |
| iterative nogood-based uniqueness enforcement in Mulamba et al. (2024, | |
| *Constraints*, doi:10.1007/s10601-024-09372-9). | |
| Loss:: | |
| L_uniq = mean_{i β blank} H(p_i) / log(|Ξ£|) | |
| where H(p_i) = βΞ£_s p_{i,s} log p_{i,s} is per-cell entropy and |Ξ£| is | |
| the symbol-domain size. Normalising by log(|Ξ£|) maps the range to [0, 1]: | |
| 0 when every blank cell is one-hot (fully committed), 1 when every blank | |
| cell is uniform (maximally ambiguous). | |
| """ | |
| def forward( | |
| self, | |
| probs: torch.Tensor, | |
| blank_mask: torch.Tensor, | |
| ) -> torch.Tensor: | |
| """ | |
| Args: | |
| probs: (B, 81, |Ξ£|) softmax probabilities β post-reasoning. | |
| blank_mask: (B, 81) bool tensor, True for blank (non-clue) cells. | |
| Returns: | |
| Scalar normalised entropy loss in [0, 1]. | |
| """ | |
| flat_blank = blank_mask.reshape(-1) # (B*81,) | |
| flat_probs = probs.reshape(-1, probs.shape[-1]) # (B*81, |Ξ£|) | |
| blank_probs = flat_probs[flat_blank] # (n_blank, |Ξ£|) | |
| if blank_probs.shape[0] == 0: | |
| return probs.sum() * 0.0 # differentiable zero | |
| entropy = -(blank_probs * (blank_probs + 1e-8).log()).sum(-1) # (n_blank,) | |
| log_sigma = torch.log(torch.tensor(float(probs.shape[-1]), device=probs.device)) | |
| return entropy.mean() / log_sigma | |
| def compute_sudoku_objective( | |
| pre_logits: torch.Tensor, | |
| post_logits: torch.Tensor, | |
| labels: torch.Tensor, | |
| puzzle_mask: torch.Tensor, | |
| *, | |
| current_epoch: int, | |
| lambda_ic: float, | |
| lambda_blank: float, | |
| blank_decay_epochs: int, | |
| ic_warmup_epochs: int, | |
| lambda_uniq: float, | |
| constraint_loss: nn.Module, | |
| uniqueness_loss: nn.Module, | |
| ) -> dict[str, Any]: | |
| """Sudoku supervision, curriculum and diagnostics; separate from AS2 reasoning.""" | |
| # ββ 6. Loss computation βββββββββββββββββββββββββββββββββββββββββββ | |
| # loss_cnn β direct CE on the CNN concept head (pre_logits) for clue | |
| # cells. PRIMARY gradient signal for the CNN. | |
| # | |
| # loss_sup β CE on the Transformer output (post_logits) for clue | |
| # cells. Keeps the Transformer grounded at clue positions. | |
| # | |
| # loss_blank β CE on blank-cell Transformer outputs against GT labels. | |
| # Provides direct supervision for blank cells (the old | |
| # model had NONE). Weighted by a curriculum schedule | |
| # that decays from full to 0.1Γ over training so the | |
| # model gradually shifts to relying on constraint reasoning. | |
| # | |
| # loss_ic β T_P fixed-point constraint energy on ALL 81 cells. | |
| # Penalises deviation from the T_P operator's fixed point. | |
| # Ramps up from 0 over ic_warmup_epochs. | |
| clue_flat = puzzle_mask.reshape(-1).bool() # (B*81,) | |
| blank_flat = ~clue_flat | |
| labels_flat = labels.reshape(-1) | |
| # Direct CNN supervision (most important for learning digits) | |
| loss_cnn = F.cross_entropy( | |
| pre_logits.reshape(-1, 9)[clue_flat], | |
| labels_flat[clue_flat], | |
| ) | |
| # Transformer output supervision at clue positions | |
| loss_sup = F.cross_entropy( | |
| post_logits.reshape(-1, 9)[clue_flat], | |
| labels_flat[clue_flat], | |
| ) | |
| # Blank-cell CE supervision with curriculum decay | |
| if blank_flat.any(): | |
| loss_blank = F.cross_entropy( | |
| post_logits.reshape(-1, 9)[blank_flat], | |
| labels_flat[blank_flat], | |
| ) | |
| else: | |
| loss_blank = torch.tensor(0.0, device=post_logits.device) | |
| # Curriculum weight: linear decay from 1.0 to 0.1 over blank_decay_epochs | |
| epoch = current_epoch | |
| blank_scale = max(0.1, 1.0 - 0.9 * epoch / max(1, blank_decay_epochs)) | |
| # T_P constraint loss with warmup ramp | |
| soft_model = F.softmax(post_logits, dim=-1) # (B, 81, 9) | |
| loss_ic = constraint_loss(soft_model) | |
| ic_scale = min(1.0, epoch / max(1, ic_warmup_epochs)) | |
| # Uniqueness regulariser: penalise high blank-cell entropy post-reasoning | |
| blank_mask_2d = ~puzzle_mask.bool() # (B, 81) bool | |
| loss_uniq = uniqueness_loss(soft_model, blank_mask_2d) | |
| loss = ( | |
| loss_cnn | |
| + loss_sup | |
| + lambda_blank * blank_scale * loss_blank | |
| + lambda_ic * ic_scale * loss_ic | |
| + lambda_uniq * ic_scale * loss_uniq | |
| ) | |
| # ββ 7. Diagnostics ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Blank-cell entropy: high β model is uncertain, low β confident | |
| if blank_flat.any(): | |
| blank_probs = soft_model.reshape(-1, 9)[blank_flat] # (n_blank, 9) | |
| blank_entropy = -(blank_probs * (blank_probs + 1e-8).log()).sum(-1).mean() | |
| else: | |
| blank_entropy = torch.tensor(0.0, device=post_logits.device) | |
| # ββ 8. Predictions ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| predictions = post_logits.argmax(dim=-1) # (B, 81) | |
| # Flat clue-cell tensors: | |
| # Use pre_logits (CNN output) so that clue_accuracy reflects CNN quality | |
| # independent of the Transformer. | |
| clue_logits = pre_logits.reshape(-1, 9)[clue_flat] # (n_clue, 9) | |
| clue_labels = labels_flat[clue_flat] # (n_clue,) | |
| # ββ 9. Perception-vs-reasoning diagnostic tensors βββββββββββββββββ | |
| # Pre-reasoning CNN logits for blank cells (measures perception quality | |
| # on the cells that actually require reasoning to fill). | |
| blank_pre_logits = pre_logits.reshape(-1, 9)[blank_flat] # (n_blank, 9) | |
| blank_labels = labels_flat[blank_flat] # (n_blank,) | |
| # Post-reasoning logits split by clue / blank (measures how much | |
| # the Transformer improves on CNN quality, and whether it corrupts | |
| # the confident clue-cell predictions). | |
| blank_post_logits = post_logits.reshape(-1, 9)[blank_flat] # (n_blank, 9) | |
| clue_post_logits = post_logits.reshape(-1, 9)[clue_flat] # (n_clue, 9) | |
| return { | |
| "loss": loss, | |
| "logits": post_logits, # (B, 81, 9) β for board-level metrics | |
| "predictions": predictions, # (B, 81) | |
| # ββ CNN (pre-reasoning) split βββββββββββββββββββββββββββββββββ | |
| "clue_logits": clue_logits, # (n_clue, 9) CNN on clue cells | |
| "clue_labels": clue_labels, # (n_clue,) | |
| "blank_pre_logits": blank_pre_logits, # (n_blank, 9) CNN on blank cells | |
| "blank_labels": blank_labels, # (n_blank,) | |
| # ββ Transformer (post-reasoning) split βββββββββββββββββββββββ | |
| "blank_post_logits": blank_post_logits, # (n_blank, 9) | |
| "clue_post_logits": clue_post_logits, # (n_clue, 9) | |
| # ββ Scalar losses βββββββββββββββββββββββββββββββββββββββββββββ | |
| "loss_cnn": loss_cnn.detach(), | |
| "loss_sup": loss_sup.detach(), | |
| "loss_blank": loss_blank.detach(), | |
| "loss_ic": loss_ic.detach(), | |
| "loss_uniq": loss_uniq.detach(), | |
| # Weighted contributions (what actually drives the total loss) | |
| "loss_blank_w": (lambda_blank * blank_scale * loss_blank).detach(), | |
| "loss_ic_w": (lambda_ic * ic_scale * loss_ic).detach(), | |
| "loss_uniq_w": (lambda_uniq * ic_scale * loss_uniq).detach(), | |
| # Curriculum scales for diagnostics | |
| "blank_scale": blank_scale, | |
| "ic_scale": ic_scale, | |
| "blank_entropy": blank_entropy.detach(), | |
| } | |