Upload 3 files
Browse files- manifest.json +7 -0
- model.py +125 -0
- weights.pt +3 -0
manifest.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"entry_class": "model.VanillaModel",
|
| 3 |
+
"output_base": 10,
|
| 4 |
+
"framework": "pytorch",
|
| 5 |
+
"model_description": "3-layer feedforward MLP (input -> 512 hidden ReLU -> output). Input: zero-padded decimal digits of a mod p, b mod p, and p concatenated into a 30-float vector. Output: 10 digit positions x 10 classes (base-10 digits of the answer, MSB first). ~16K parameters.",
|
| 6 |
+
"training_description": "Trained from random init on 200K synthetic (a, b, p) -> (a*b mod p) examples spanning tiers 1-4 (p up to 32 bits). Adam optimizer, lr=1e-3 with cosine annealing, 30 epochs, batch size 512. Best checkpoint saved by validation accuracy."
|
| 7 |
+
}
|
model.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Vanilla 3-layer feedforward ANN for modular multiplication.
|
| 2 |
+
|
| 3 |
+
Input: digit-encoded (a_red, b_red, p) — each zero-padded to MAX_DIGITS
|
| 4 |
+
Hidden: one fully-connected layer with ReLU
|
| 5 |
+
Output: MAX_OUT_DIGITS * 10 logits — one 10-way class per output digit position
|
| 6 |
+
|
| 7 |
+
At inference, a and b are reduced mod p inside predict_digits (allowed by rules)
|
| 8 |
+
before being encoded and fed to the network.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
|
| 18 |
+
from modchallenge.interface.base_model import ModularMultiplicationModel
|
| 19 |
+
|
| 20 |
+
MAX_DIGITS = 10 # decimal digits per input slot (covers p up to ~4 billion = tier 4)
|
| 21 |
+
MAX_OUT_DIGITS = 10 # decimal digits in the answer (answer < p <= tier-4 max)
|
| 22 |
+
INPUT_SIZE = 3 * MAX_DIGITS # 30
|
| 23 |
+
HIDDEN_SIZE = 512
|
| 24 |
+
OUTPUT_SIZE = MAX_OUT_DIGITS * 10 # 100
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ---------------------------------------------------------------------------
|
| 28 |
+
# Architecture
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
+
|
| 31 |
+
class VanillaMLP(nn.Module):
|
| 32 |
+
def __init__(
|
| 33 |
+
self,
|
| 34 |
+
input_size: int = INPUT_SIZE,
|
| 35 |
+
hidden_size: int = HIDDEN_SIZE,
|
| 36 |
+
output_size: int = OUTPUT_SIZE,
|
| 37 |
+
):
|
| 38 |
+
super().__init__()
|
| 39 |
+
self.net = nn.Sequential(
|
| 40 |
+
nn.Linear(input_size, hidden_size),
|
| 41 |
+
nn.ReLU(),
|
| 42 |
+
nn.Linear(hidden_size, output_size),
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 46 |
+
"""x: (B, INPUT_SIZE) floats in [0, 1]. returns (B, MAX_OUT_DIGITS, 10) logits."""
|
| 47 |
+
out = self.net(x) # (B, OUTPUT_SIZE)
|
| 48 |
+
return out.view(-1, MAX_OUT_DIGITS, 10) # (B, D, 10)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# ---------------------------------------------------------------------------
|
| 52 |
+
# Encoding helpers (shared by train.py and predict_digits)
|
| 53 |
+
# ---------------------------------------------------------------------------
|
| 54 |
+
|
| 55 |
+
def encode_int(n: int, length: int = MAX_DIGITS) -> list[int]:
|
| 56 |
+
"""Integer -> zero-padded decimal digit list of fixed length, MSB first."""
|
| 57 |
+
s = str(int(n)).zfill(length)
|
| 58 |
+
if len(s) > length:
|
| 59 |
+
s = s[-length:] # truncate if somehow too long
|
| 60 |
+
return [int(c) for c in s]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def digits_to_tensor(n: int, length: int = MAX_DIGITS) -> list[float]:
|
| 64 |
+
"""Encode integer n as normalized floats in [0, 1]."""
|
| 65 |
+
return [d / 9.0 for d in encode_int(n, length)]
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ---------------------------------------------------------------------------
|
| 69 |
+
# Submission entry point
|
| 70 |
+
# ---------------------------------------------------------------------------
|
| 71 |
+
|
| 72 |
+
class VanillaModel(ModularMultiplicationModel):
|
| 73 |
+
def __init__(self):
|
| 74 |
+
self.model: VanillaMLP | None = None
|
| 75 |
+
self.device: torch.device | None = None
|
| 76 |
+
|
| 77 |
+
def load(self, model_dir: str) -> None:
|
| 78 |
+
if torch.cuda.is_available():
|
| 79 |
+
self.device = torch.device("cuda")
|
| 80 |
+
else:
|
| 81 |
+
self.device = torch.device("cpu")
|
| 82 |
+
|
| 83 |
+
ckpt = torch.load(
|
| 84 |
+
Path(model_dir) / "weights.pt",
|
| 85 |
+
map_location=self.device,
|
| 86 |
+
weights_only=True,
|
| 87 |
+
)
|
| 88 |
+
cfg = ckpt.get("config", {})
|
| 89 |
+
self.model = VanillaMLP(
|
| 90 |
+
input_size=cfg.get("input_size", INPUT_SIZE),
|
| 91 |
+
hidden_size=cfg.get("hidden_size", HIDDEN_SIZE),
|
| 92 |
+
output_size=cfg.get("output_size", OUTPUT_SIZE),
|
| 93 |
+
)
|
| 94 |
+
self.model.load_state_dict(ckpt["state_dict"])
|
| 95 |
+
self.model.to(self.device)
|
| 96 |
+
self.model.eval()
|
| 97 |
+
|
| 98 |
+
def preprocess_a(self, a: str):
|
| 99 |
+
return a
|
| 100 |
+
|
| 101 |
+
def preprocess_b(self, b: str):
|
| 102 |
+
return b
|
| 103 |
+
|
| 104 |
+
def preprocess_p(self, p: str):
|
| 105 |
+
return p
|
| 106 |
+
|
| 107 |
+
@torch.no_grad()
|
| 108 |
+
def predict_digits(self, a_enc, b_enc, p_enc) -> list[int]:
|
| 109 |
+
assert self.model is not None
|
| 110 |
+
|
| 111 |
+
p = int(p_enc)
|
| 112 |
+
a_red = int(a_enc) % p
|
| 113 |
+
b_red = int(b_enc) % p
|
| 114 |
+
|
| 115 |
+
x = digits_to_tensor(a_red) + digits_to_tensor(b_red) + digits_to_tensor(p)
|
| 116 |
+
inp = torch.tensor([x], dtype=torch.float32, device=self.device) # (1, 30)
|
| 117 |
+
|
| 118 |
+
logits = self.model(inp) # (1, MAX_OUT_DIGITS, 10)
|
| 119 |
+
preds = logits[0].argmax(-1).tolist() # [d0, d1, ..., d9]
|
| 120 |
+
|
| 121 |
+
# Strip leading zeros, return at least [0]
|
| 122 |
+
result = preds
|
| 123 |
+
while len(result) > 1 and result[0] == 0:
|
| 124 |
+
result = result[1:]
|
| 125 |
+
return result
|
weights.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:47fd133c738e17e2288accbcccc6ebe65b3987772823adfcff362ca0576e10bd
|
| 3 |
+
size 271133
|