Model code and training recipe
Browse files- README.md +120 -0
- example.py +39 -0
- model.py +149 -0
README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-4.0
|
| 3 |
+
library_name: pytorch
|
| 4 |
+
tags:
|
| 5 |
+
- time-series
|
| 6 |
+
- panel-data
|
| 7 |
+
- missing-data
|
| 8 |
+
- smoothing
|
| 9 |
+
- imputation
|
| 10 |
+
- air-quality
|
| 11 |
+
- pytorch
|
| 12 |
+
datasets:
|
| 13 |
+
- prteek/sparse-panel-demo
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# Prevailing level network for sparse, gappy panels
|
| 17 |
+
|
| 18 |
+
A small PyTorch network that estimates a smooth **prevailing level** for every location and day of a panel,
|
| 19 |
+
for two related quantities at once, when the observations are noisy daily means with long gaps and some series
|
| 20 |
+
missing entirely. It was developed for the post
|
| 21 |
+
[PM2.5, PM10 ~ station + time](https://prteek.github.io/regression_room/posts/beijing_air_quality/) and powers the
|
| 22 |
+
[sparse panel demo Space](https://huggingface.co/spaces/prteek/sparse-panel-demo).
|
| 23 |
+
|
| 24 |
+
**This repo holds the code and the training recipe, not trained weights.** The network learns an embedding for
|
| 25 |
+
each location and a time basis over a fixed date range, so a fitted model only applies to the panel it was fitted
|
| 26 |
+
on. To use it, fit it to your own panel: on the 12-station, four-year Beijing data a fit takes a few seconds on a
|
| 27 |
+
CPU.
|
| 28 |
+
|
| 29 |
+
## Files
|
| 30 |
+
|
| 31 |
+
- `model.py`: the encoding, the network, the loss and `fit` / `predict`
|
| 32 |
+
- `example.py`: fits the network to a small synthetic panel and predicts every cell (runs as is)
|
| 33 |
+
|
| 34 |
+
## Model
|
| 35 |
+
|
| 36 |
+
Each row is one location and day, with up to two targets $y_1, y_2$ (log values, standardised).
|
| 37 |
+
|
| 38 |
+
$$
|
| 39 |
+
\begin{aligned}
|
| 40 |
+
x &= \big[\, e_{\text{area}} + e_{\text{station}},\ b(t) \,\big] \\
|
| 41 |
+
h &= \operatorname{SiLU}\!\big(W_2 \operatorname{SiLU}(W_1 x)\big) \\
|
| 42 |
+
\mu_1 &= w_1^\top h, \quad \mu_2 = w_2^\top h
|
| 43 |
+
\end{aligned}
|
| 44 |
+
$$
|
| 45 |
+
|
| 46 |
+
- **Location**: an 8-dim area embedding plus a station offset that starts near zero and is penalised, so a
|
| 47 |
+
station with little data falls back to its area.
|
| 48 |
+
- **Time**: fixed Gaussian bumps $b_k(t) = \exp\{-\tfrac12((t - c_k)/w)^2\}$ with centres every $w$ days, so the
|
| 49 |
+
level is a smooth function of time.
|
| 50 |
+
- **Shared trunk, two heads**: two layers of 64 with SiLU, then one linear head per target. A day where only one
|
| 51 |
+
target was observed still trains everything the other depends on.
|
| 52 |
+
|
| 53 |
+
**Loss.** An observed value is a mean of $n$ readings, so its variance around the level is $\sigma_j^2 / n + \tau_j^2$,
|
| 54 |
+
with $\sigma_j$, $\tau_j$ learned per target. The Gaussian negative log-likelihood is averaged over observed cells
|
| 55 |
+
only: a missing target is **masked**, not dropped. Added to it:
|
| 56 |
+
|
| 57 |
+
- a **curvature penalty** $\lambda \cdot \overline{(\mu_{t+1} - 2\mu_t + \mu_{t-1})^2}$ over every location's
|
| 58 |
+
predicted series on the **whole** day grid, observed or not, which keeps the level smooth through gaps;
|
| 59 |
+
- a small L2 penalty on the station offsets.
|
| 60 |
+
|
| 61 |
+
## Training recipe
|
| 62 |
+
|
| 63 |
+
Full-batch Adam with a cosine learning-rate schedule. The settings below were chosen by look-ahead
|
| 64 |
+
cross-validation in the post:
|
| 65 |
+
|
| 66 |
+
| Setting | Value |
|
| 67 |
+
|---|---|
|
| 68 |
+
| curvature penalty $\lambda$ (`rough`) | 1e5 |
|
| 69 |
+
| bump spacing $w$ (`spacing`) | 30 days |
|
| 70 |
+
| hidden width | 64 |
|
| 71 |
+
| learning rate | 5e-3 |
|
| 72 |
+
| weight decay | 1e-4 |
|
| 73 |
+
| station offset L2 (`station_l2`) | 1e-3 |
|
| 74 |
+
| schedule length | 1000 |
|
| 75 |
+
| training steps | 100 |
|
| 76 |
+
| seed | 0 |
|
| 77 |
+
|
| 78 |
+
For a single-target model, pass `use=["PM2.5"]` (or the other target) to `fit`.
|
| 79 |
+
|
| 80 |
+
## Input format
|
| 81 |
+
|
| 82 |
+
Long format, one row per location × date × target that has data:
|
| 83 |
+
|
| 84 |
+
| column | meaning |
|
| 85 |
+
|---|---|
|
| 86 |
+
| `station` | location id |
|
| 87 |
+
| `area` | group the location belongs to (it falls back to this when its own data is thin) |
|
| 88 |
+
| `date` | daily timestamp |
|
| 89 |
+
| `pollutant` | target name, one of `model.TARGETS` |
|
| 90 |
+
| `value` | the daily mean (> 0; modelled on the log scale) |
|
| 91 |
+
| `n` | number of readings behind that mean (weights the observation) |
|
| 92 |
+
|
| 93 |
+
The two target names are set in `model.TARGETS` (`["PM2.5", "PM10"]`); change them for other quantities.
|
| 94 |
+
|
| 95 |
+
```python
|
| 96 |
+
from model import Encoding, fit, predict
|
| 97 |
+
|
| 98 |
+
enc = Encoding.fit(panel, end=panel["date"].max())
|
| 99 |
+
model = fit(enc, enc.wide(panel), CFG, steps=100)
|
| 100 |
+
level = predict(model, enc, enc.grid()) # every location x day, observed or not
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
## Limitations
|
| 104 |
+
|
| 105 |
+
- **Not transferable.** Location embeddings and the time basis are specific to one panel and date range. Refit
|
| 106 |
+
for new locations or dates.
|
| 107 |
+
- **Smoothness is a choice.** Cross-validation that scores against noisy readings rewards following short
|
| 108 |
+
episodes, so it tends to under-smooth; the post discusses this. Treat `rough` and `spacing` as decisions about
|
| 109 |
+
how smooth the level should be.
|
| 110 |
+
- **Log-scale bias.** The model is fitted to logs of means over few readings, so its level sits slightly below a
|
| 111 |
+
moving average of full data (Jensen's inequality).
|
| 112 |
+
- **A GAM is a strong alternative.** In the post, a mixed-effects GAM with hand-set smoothness does as well or
|
| 113 |
+
better.
|
| 114 |
+
- **Memory.** Training is full batch and the penalty covers every location × day, so memory grows with panel size.
|
| 115 |
+
|
| 116 |
+
## Data used in the post
|
| 117 |
+
|
| 118 |
+
Beijing Multi-Site Air Quality, Song Chen (2017), UCI Machine Learning Repository,
|
| 119 |
+
[doi:10.24432/C5RK5G](https://doi.org/10.24432/C5RK5G), CC BY 4.0. The demo's sparse readings and predictions
|
| 120 |
+
are in the dataset [prteek/sparse-panel-demo](https://huggingface.co/datasets/prteek/sparse-panel-demo).
|
example.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fit the prevailing-level network to a panel of your own and predict a smooth level for every cell.
|
| 2 |
+
|
| 3 |
+
Input is long format, one row per station x date x pollutant that has data:
|
| 4 |
+
station, area, date, pollutant (one of model.TARGETS), value (> 0, a daily mean), n (readings behind it)
|
| 5 |
+
Here a small synthetic panel stands in for real data.
|
| 6 |
+
"""
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pandas as pd
|
| 9 |
+
|
| 10 |
+
from model import TARGETS, Encoding, fit, predict
|
| 11 |
+
|
| 12 |
+
# Settings chosen by look-ahead cross-validation in the post
|
| 13 |
+
CFG = dict(rough=1e5, spacing=30, lr=5e-3, hidden=64, weight_decay=1e-4, station_l2=1e-3, schedule=1000, seed=0)
|
| 14 |
+
STEPS = 100
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def synthetic_panel(seed: int = 0) -> pd.DataFrame:
|
| 18 |
+
rng = np.random.default_rng(seed)
|
| 19 |
+
days = pd.date_range("2020-01-01", periods=365)
|
| 20 |
+
stations = {"s1": "urban", "s2": "urban", "s3": "suburban", "s4": "suburban"}
|
| 21 |
+
t = np.arange(len(days))
|
| 22 |
+
rows = []
|
| 23 |
+
for s, area in stations.items():
|
| 24 |
+
level = 4 + 0.5 * np.sin(2 * np.pi * t / 365) + (0.3 if area == "urban" else 0) + rng.normal(0, 0.1)
|
| 25 |
+
for j, pollutant in enumerate(TARGETS):
|
| 26 |
+
n = rng.poisson(1.2, len(days)) # hours observed per day; 0 means no reading
|
| 27 |
+
keep = n > 0
|
| 28 |
+
value = np.exp(level + 0.4 * j + rng.normal(0, 0.5 / np.sqrt(np.maximum(n, 1))))
|
| 29 |
+
rows.append(pd.DataFrame({"station": s, "area": area, "date": days[keep], "pollutant": pollutant,
|
| 30 |
+
"value": value[keep], "n": n[keep]}))
|
| 31 |
+
return pd.concat(rows, ignore_index=True)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
panel = synthetic_panel()
|
| 35 |
+
enc = Encoding.fit(panel, end=panel["date"].max())
|
| 36 |
+
model = fit(enc, enc.wide(panel), CFG, STEPS) # joint model; use=["PM2.5"] for a single-target one
|
| 37 |
+
level = predict(model, enc, enc.grid()) # every station x day, observed or not
|
| 38 |
+
print(level.head())
|
| 39 |
+
print(f"{len(panel)} readings in, {len(level)} predicted cells out")
|
model.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Joint two-target network for a smooth 'prevailing' pollution level per station and day.
|
| 2 |
+
|
| 3 |
+
Copied from the regression_room post (posts/beijing_air_quality/model.py):
|
| 4 |
+
- one row per station x day with up to two targets (log PM2.5, log PM10); a missing target is masked, not dropped;
|
| 5 |
+
- location = area embedding + small station offset (station falls back to its area when data is thin);
|
| 6 |
+
- time enters only through fixed Gaussian bumps, so the level is a smooth function of time;
|
| 7 |
+
- loss = Gaussian NLL of an observed daily mean of n hours, variance sigma^2 / n + tau^2;
|
| 8 |
+
- a roughness penalty on the second difference of every station's predicted series over the full day grid.
|
| 9 |
+
"""
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pandas as pd
|
| 14 |
+
import torch
|
| 15 |
+
from torch import nn
|
| 16 |
+
|
| 17 |
+
TARGETS = ["PM2.5", "PM10"]
|
| 18 |
+
KEY = ["station", "date"]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class Encoding:
|
| 23 |
+
stations: list
|
| 24 |
+
areas: list
|
| 25 |
+
station_area: dict
|
| 26 |
+
start: pd.Timestamp
|
| 27 |
+
n_days: int
|
| 28 |
+
y_mean: np.ndarray
|
| 29 |
+
y_std: np.ndarray
|
| 30 |
+
|
| 31 |
+
@classmethod
|
| 32 |
+
def fit(cls, df: pd.DataFrame, end: pd.Timestamp) -> "Encoding":
|
| 33 |
+
start = df["date"].min()
|
| 34 |
+
logv = np.log(df["value"])
|
| 35 |
+
return cls(stations=sorted(df["station"].unique()), areas=sorted(df["area"].unique()),
|
| 36 |
+
station_area=df.drop_duplicates("station").set_index("station")["area"].to_dict(),
|
| 37 |
+
start=start, n_days=(end - start).days + 1,
|
| 38 |
+
y_mean=np.array([logv[df["pollutant"] == t].mean() for t in TARGETS]),
|
| 39 |
+
y_std=np.array([logv[df["pollutant"] == t].std() for t in TARGETS]))
|
| 40 |
+
|
| 41 |
+
def wide(self, df: pd.DataFrame) -> pd.DataFrame:
|
| 42 |
+
w = df.pivot_table(index=KEY, columns="pollutant", values=["value", "n"], aggfunc="first")
|
| 43 |
+
w.columns = [f"{v}_{p}" for v, p in w.columns]
|
| 44 |
+
for t in TARGETS:
|
| 45 |
+
for v in ["value", "n"]:
|
| 46 |
+
if f"{v}_{t}" not in w:
|
| 47 |
+
w[f"{v}_{t}"] = np.nan
|
| 48 |
+
return w.reset_index()
|
| 49 |
+
|
| 50 |
+
def inputs(self, keys: pd.DataFrame) -> dict:
|
| 51 |
+
s_idx = {s: i for i, s in enumerate(self.stations)}
|
| 52 |
+
a_idx = {a: i for i, a in enumerate(self.areas)}
|
| 53 |
+
return {"station": torch.tensor(keys["station"].map(s_idx).to_numpy()),
|
| 54 |
+
"area": torch.tensor(keys["station"].map(self.station_area).map(a_idx).to_numpy()),
|
| 55 |
+
"day": torch.tensor((keys["date"] - self.start).dt.days.to_numpy())}
|
| 56 |
+
|
| 57 |
+
def targets(self, w: pd.DataFrame, use=TARGETS):
|
| 58 |
+
val = w[[f"value_{t}" for t in TARGETS]].to_numpy(float)
|
| 59 |
+
n = w[[f"n_{t}" for t in TARGETS]].to_numpy(float)
|
| 60 |
+
mask = ~np.isnan(val)
|
| 61 |
+
mask[:, [t not in use for t in TARGETS]] = False # single-target models ignore the other pollutant
|
| 62 |
+
y = (np.log(np.where(mask, val, 1.0)) - self.y_mean) / self.y_std
|
| 63 |
+
return (torch.tensor(np.where(mask, y, 0.0), dtype=torch.float32), torch.tensor(mask),
|
| 64 |
+
torch.tensor(np.where(mask, n, 1.0), dtype=torch.float32))
|
| 65 |
+
|
| 66 |
+
def to_value(self, mu: torch.Tensor) -> np.ndarray:
|
| 67 |
+
return np.exp(mu.detach().numpy() * self.y_std + self.y_mean)
|
| 68 |
+
|
| 69 |
+
def grid(self) -> pd.DataFrame:
|
| 70 |
+
days = pd.date_range(self.start, periods=self.n_days)
|
| 71 |
+
return pd.DataFrame({"station": self.stations}).merge(pd.DataFrame({"date": days}), how="cross")
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class TimeBasis(nn.Module):
|
| 75 |
+
def __init__(self, n_days: int, spacing: int):
|
| 76 |
+
super().__init__()
|
| 77 |
+
self.register_buffer("centres", torch.arange(-spacing, n_days + 2 * spacing, spacing, dtype=torch.float32))
|
| 78 |
+
self.width = float(spacing)
|
| 79 |
+
|
| 80 |
+
def forward(self, day):
|
| 81 |
+
return torch.exp(-0.5 * ((day.float()[:, None] - self.centres[None, :]) / self.width) ** 2)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class PrevailingNet(nn.Module):
|
| 85 |
+
def __init__(self, enc: Encoding, dim_loc: int = 8, hidden: int = 64, spacing: int = 30):
|
| 86 |
+
super().__init__()
|
| 87 |
+
self.time = TimeBasis(enc.n_days, spacing)
|
| 88 |
+
self.area = nn.Embedding(len(enc.areas), dim_loc)
|
| 89 |
+
nn.init.normal_(self.area.weight, std=0.1) # small init so the area vectors are actually learned
|
| 90 |
+
self.station = nn.Embedding(len(enc.stations), dim_loc)
|
| 91 |
+
nn.init.normal_(self.station.weight, std=0.01)
|
| 92 |
+
self.trunk = nn.Sequential(nn.Linear(dim_loc + len(self.time.centres), hidden), nn.SiLU(),
|
| 93 |
+
nn.Linear(hidden, hidden), nn.SiLU())
|
| 94 |
+
self.heads = nn.ModuleList([nn.Linear(hidden, 1) for _ in TARGETS])
|
| 95 |
+
self.log_sigma = nn.Parameter(torch.zeros(len(TARGETS)))
|
| 96 |
+
self.log_tau = nn.Parameter(torch.full((len(TARGETS),), -1.0))
|
| 97 |
+
self.register_buffer("area_of", torch.tensor([enc.areas.index(enc.station_area[s]) for s in enc.stations]))
|
| 98 |
+
self.n_days = enc.n_days
|
| 99 |
+
|
| 100 |
+
def forward(self, x):
|
| 101 |
+
loc = self.area(x["area"]) + self.station(x["station"])
|
| 102 |
+
h = self.trunk(torch.cat([loc, self.time(x["day"])], dim=1))
|
| 103 |
+
return torch.cat([head(h) for head in self.heads], dim=1)
|
| 104 |
+
|
| 105 |
+
def all_series(self):
|
| 106 |
+
"""Predictions for every station over the full day grid, shape (stations, days, targets)."""
|
| 107 |
+
s = torch.arange(len(self.area_of))
|
| 108 |
+
x = {"station": s.repeat_interleave(self.n_days), "area": self.area_of[s].repeat_interleave(self.n_days),
|
| 109 |
+
"day": torch.arange(self.n_days).repeat(len(s))}
|
| 110 |
+
return self(x).view(len(s), self.n_days, len(TARGETS))
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def nll(mu, model, y, mask, n):
|
| 114 |
+
var = torch.exp(2 * model.log_sigma) / n + torch.exp(2 * model.log_tau)
|
| 115 |
+
out = 0.5 * (torch.log(var) + (y - mu) ** 2 / var)
|
| 116 |
+
return (out * mask).sum() / mask.sum()
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def roughness(model):
|
| 120 |
+
mu = model.all_series()
|
| 121 |
+
return (mu[:, 2:] - 2 * mu[:, 1:-1] + mu[:, :-2]).pow(2).mean()
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def fit(enc, w, cfg, steps, use=TARGETS, on_checkpoint=None, checkpoints=()):
|
| 125 |
+
"""Full-batch Adam on a cosine schedule spanning `cfg['schedule']` steps (same meaning in CV and final fit)."""
|
| 126 |
+
torch.manual_seed(cfg.get("seed", 0))
|
| 127 |
+
model = PrevailingNet(enc, hidden=cfg["hidden"], spacing=cfg["spacing"])
|
| 128 |
+
opt = torch.optim.Adam(model.parameters(), lr=cfg["lr"], weight_decay=cfg["weight_decay"])
|
| 129 |
+
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, cfg["schedule"])
|
| 130 |
+
x, (y, mask, n) = enc.inputs(w), enc.targets(w, use)
|
| 131 |
+
for step in range(1, steps + 1):
|
| 132 |
+
model.train()
|
| 133 |
+
loss = (nll(model(x), model, y, mask, n) + cfg["rough"] * roughness(model)
|
| 134 |
+
+ cfg["station_l2"] * model.station.weight.pow(2).mean())
|
| 135 |
+
opt.zero_grad()
|
| 136 |
+
loss.backward()
|
| 137 |
+
opt.step()
|
| 138 |
+
sched.step()
|
| 139 |
+
if on_checkpoint and step in checkpoints:
|
| 140 |
+
model.eval()
|
| 141 |
+
on_checkpoint(step, model)
|
| 142 |
+
model.eval()
|
| 143 |
+
return model
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def predict(model, enc, keys: pd.DataFrame) -> pd.DataFrame:
|
| 147 |
+
with torch.no_grad():
|
| 148 |
+
v = enc.to_value(model(enc.inputs(keys)))
|
| 149 |
+
return pd.concat([keys[KEY].assign(pollutant=t, pred=v[:, i]) for i, t in enumerate(TARGETS)], ignore_index=True)
|