SPB Git forge

spb/wp23_uqo

Public
6commits 1branches 0releases
32.7 MBsize
maindefault branch
yesterdaylast push
TeX 62.3% Python 37.6%
6.0 KB · 103 lines python
Raw Blame History
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