TeX 62.3%
Python 37.6%
1# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca2"""Task A neural models (PyTorch). Never import lightgbm / xgboost in the same process (OpenMP conflict on macOS)."""3from __future__ import annotations45import os67import numpy as np8import torch9import torch.nn as nn1011from wp23.models_a import TAUS1213torch.set_num_threads(int(os.environ.get("WP23_TORCH_THREADS", "6")))141516def _pinball(pred, y, taus):17 d = y[:, None] - pred; return torch.mean(torch.maximum(taus * d, (taus - 1) * d))181920class MLP:21 """Multilayer perceptron with a quantile head (pinball loss on 5 quantiles; the median is the point forecast)."""22 name = "mlp"23 def __init__(self, hidden=(256, 128), epochs=8, lr=2e-3, batch=4096, seed=0):24 self.hidden, self.epochs, self.lr, self.batch, self.seed = hidden, epochs, lr, batch, seed25 def _net(self, d):26 layers = []; last = d27 for h in self.hidden:28 layers += [nn.Linear(last, h), nn.GELU(), nn.Dropout(0.1)]; last = h29 layers += [nn.Linear(last, len(TAUS))]; return nn.Sequential(*layers)30 def fit(self, X, y, meta=None):31 torch.manual_seed(self.seed); self.mu = X.mean(0); self.sd = X.std(0) + 1e-932 Xt = torch.tensor((X - self.mu) / self.sd, dtype=torch.float32); yt = torch.tensor(y, dtype=torch.float32); taus = torch.tensor(TAUS, dtype=torch.float32)33 self.net = self._net(X.shape[1]); opt = torch.optim.AdamW(self.net.parameters(), lr=self.lr, weight_decay=1e-4)34 sched = torch.optim.lr_scheduler.OneCycleLR(opt, max_lr=self.lr, total_steps=self.epochs * ((len(Xt) + self.batch - 1) // self.batch))35 self.net.train()36 for ep in range(self.epochs):37 perm = torch.randperm(len(Xt))38 for i in range(0, len(Xt), self.batch):39 b = perm[i:i + self.batch]; opt.zero_grad(); loss = _pinball(self.net(Xt[b]), yt[b], taus); loss.backward(); opt.step(); sched.step()40 self.net.eval(); return self41 def predict_quantiles(self, X, meta=None, taus=TAUS):42 with torch.no_grad():43 Q = self.net(torch.tensor((X - self.mu) / self.sd, dtype=torch.float32)).numpy()44 return np.sort(Q, axis=1)45 def predict(self, X, meta=None):46 return self.predict_quantiles(X, meta)[:, 2]474849class HSHN(MLP):50 """Hierarchical spatiotemporal hedonic network: the MLP of the hedonic + market features plus learned embeddings of the51 municipality, MRC and region (nested: the municipality embedding is a *deviation* penalised toward zero, so thin52 municipalities fall back on their MRC and region — partial pooling by weight decay), a horizon embedding, and an53 interaction block between the horizon/market state and the hedonic representation. pooling ∈ {partial, full, none}."""54 name = "hshn"55 def __init__(self, pooling="partial", emb=8, hidden=(256, 128), epochs=8, lr=2e-3, batch=4096, seed=0, mun_decay=5e-3):56 super().__init__(hidden, epochs, lr, batch, seed); self.pooling, self.emb, self.mun_decay = pooling, emb, mun_decay57 self.name = f"hshn_{pooling}"58 def _codes(self, meta, fit=False):59 out = []60 for k in ("mun_code", "mrc_code", "region_code"):61 v = np.asarray(meta[k]).astype(str)62 if fit:63 self.maps = getattr(self, "maps", {}); self.maps[k] = {c: i + 1 for i, c in enumerate(sorted(set(v)))}64 out.append(torch.tensor([self.maps[k].get(c, 0) for c in v]))65 h = torch.tensor(np.clip(np.asarray(meta["h"]).astype(int), 0, 12)); return out + [h]66 def fit(self, X, y, meta=None):67 torch.manual_seed(self.seed); self.mu = X.mean(0); self.sd = X.std(0) + 1e-968 Xt = torch.tensor((X - self.mu) / self.sd, dtype=torch.float32); yt = torch.tensor(y, dtype=torch.float32); taus = torch.tensor(TAUS, dtype=torch.float32)69 codes = self._codes(meta, fit=True)70 n_mun, n_mrc, n_reg = (len(self.maps["mun_code"]) + 1, len(self.maps["mrc_code"]) + 1, len(self.maps["region_code"]) + 1)71 e = self.emb72 self.emb_mun = nn.Embedding(n_mun, e); self.emb_mrc = nn.Embedding(n_mrc, e); self.emb_reg = nn.Embedding(n_reg, e); self.emb_h = nn.Embedding(13, 4)73 for em in (self.emb_mun, self.emb_mrc, self.emb_reg, self.emb_h):74 nn.init.normal_(em.weight, std=0.02)75 d_in = X.shape[1] + e + 476 self.enc = nn.Sequential(nn.Linear(d_in, self.hidden[0]), nn.GELU(), nn.Dropout(0.1), nn.Linear(self.hidden[0], self.hidden[1]), nn.GELU())77 self.inter = nn.Sequential(nn.Linear(self.hidden[1] + e + 4, 64), nn.GELU())78 self.head = nn.Linear(self.hidden[1] + 64, len(TAUS))79 params = [p for m in (self.emb_mrc, self.emb_reg, self.emb_h, self.enc, self.inter, self.head) for p in m.parameters()]80 groups = [{"params": params, "weight_decay": 1e-4}]81 if self.pooling != "full":82 groups.append({"params": self.emb_mun.parameters(), "weight_decay": self.mun_decay if self.pooling == "partial" else 0.0})83 opt = torch.optim.AdamW(groups, lr=self.lr)84 steps = self.epochs * ((len(Xt) + self.batch - 1) // self.batch); sched = torch.optim.lr_scheduler.OneCycleLR(opt, max_lr=self.lr, total_steps=steps)85 for ep in range(self.epochs):86 perm = torch.randperm(len(Xt))87 for i in range(0, len(Xt), self.batch):88 b = perm[i:i + self.batch]; opt.zero_grad(); loss = _pinball(self._forward(Xt[b], [c[b] for c in codes]), yt[b], taus); loss.backward(); opt.step(); sched.step()89 return self90 def _forward(self, x, codes):91 mun, mrc, reg, h = codes92 g = self.emb_reg(reg) + self.emb_mrc(mrc) + (self.emb_mun(mun) if self.pooling != "full" else 0.0)93 hv = self.emb_h(h); z = self.enc(torch.cat([x, g, hv], 1)); it = self.inter(torch.cat([z, g, hv], 1))94 return self.head(torch.cat([z, it], 1))95 def predict_quantiles(self, X, meta=None, taus=TAUS):96 with torch.no_grad():97 for m in (self.enc, self.inter, self.head):98 m.eval()99 Q = self._forward(torch.tensor((X - self.mu) / self.sd, dtype=torch.float32), self._codes(meta)).numpy()100 return np.sort(Q, axis=1)101102103