Download notebooks/src/winners_demo.py from Karar-git/hack4dev-building-age-winners: direct link, hf CLI and curl.
- Browser
- Download file 7.94 kB
-
https://huggingface.co/Karar-git/hack4dev-building-age-winners/resolve/main/notebooks/src/winners_demo.py
- Command line
-
hf download hf://Karar-git/hack4dev-building-age-winners/notebooks/src/winners_demo.py
-
curl -L -o winners_demo.py https://huggingface.co/Karar-git/hack4dev-building-age-winners/resolve/main/notebooks/src/winners_demo.py
7.94 kB
| # %% [markdown] | |
| # # The two winners, end to end | |
| # | |
| # Building age class (1-4) per 30 m Landsat pixel. Train on Madrid, predict | |
| # Amsterdam, scored by macro F1 at n = 5, 25, 50, 100, 200 labelled Amsterdam | |
| # pixels per class. | |
| # | |
| # * **Zero-shot LightGBM** - trained on Madrid only, never sees an Amsterdam | |
| # label. Best model if you may not use Amsterdam at all: **0.593**. | |
| # * **Retrained LightGBM** - refit on Madrid plus the n labelled Amsterdam pixels | |
| # per class the protocol gives you, those pixels weighted 30x. Best average | |
| # (**0.656**) and best peak (**0.720** at n=200) over the whole protocol. | |
| # | |
| # Everything here runs on a laptop CPU. The heavy lifting is in `winners.py`; | |
| # this notebook just calls it. All numbers are on ALL of Amsterdam minus the | |
| # labelled pixels of each draw. | |
| # %% | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| ROOT = Path.cwd() if (Path.cwd() / "winners.py").exists() else Path.cwd().parent | |
| sys.path.insert(0, str(ROOT)) | |
| import winners as W | |
| d = W.load_data() | |
| print("Madrid ", d["X_mad"].shape, " Amsterdam", d["X_ams"].shape, | |
| " features", len(d["feature_names"])) | |
| print("Madrid class shares ", np.round(d["madrid_prior"], 3)) | |
| print("Amsterdam class shares", np.round([(d["y_ams"] == c).mean() for c in W.CLASSES], 3)) | |
| # %% [markdown] | |
| # ## 1. Zero-shot LightGBM: Madrid only | |
| # | |
| # 1,200 boosting rounds, class-balanced, raw features (per-city scaling made | |
| # things *worse* on Madrid's own spatial cross-validation, so none is applied). | |
| # The final step is the decision rule: tau = 0.5 logit adjustment by the Madrid | |
| # class prior, then the macro-F1 plug-in rule. It uses no Amsterdam labels, only | |
| # the predicted class shares of the batch being labelled. | |
| # %% | |
| zs = W.load_zero_shot() | |
| P = W.predict(zs, d["X_ams"]) | |
| lab = W.decide(P, d["madrid_prior"]) | |
| from sklearn.metrics import confusion_matrix, f1_score | |
| print(f"macro F1, all {len(lab):,} Amsterdam pixels: {W.macro_f1(d['y_ams'], lab):.4f}") | |
| print(f" same model, plain argmax instead of the rule: " | |
| f"{W.macro_f1(d['y_ams'], W.CLASSES[P.argmax(1)]):.4f}") | |
| print("per-class F1:", np.round(f1_score(d["y_ams"], lab, labels=W.CLASSES, average=None), 3)) | |
| cm = confusion_matrix(d["y_ams"], lab, labels=W.CLASSES, normalize="true") | |
| print(pd.DataFrame(cm, index=[f"true {c}" for c in W.CLASSES], | |
| columns=[f"pred {c}" for c in W.CLASSES]).round(3).to_string()) | |
| # %% [markdown] | |
| # ## 2. Retrained LightGBM: one draw | |
| # | |
| # The protocol hands you n labelled Amsterdam pixels per class. Here n = 25, | |
| # draw 0 - the exact pixels behind the reported number. A fresh LightGBM | |
| # (400 rounds) is fit on all 76k Madrid pixels plus these 100, the 100 weighted | |
| # 30x. Each city is robust-z scaled with its own median and IQR, Amsterdam's | |
| # taken from its unlabelled features. The 100 pixels are removed before scoring. | |
| # %% | |
| import time | |
| n, rep = 25, 0 | |
| shot = W.shots_for(d["y_ams"], n, rep) | |
| keep = np.setdiff1d(np.arange(len(d["y_ams"])), shot) | |
| print(f"{len(shot)} labelled pixels, classes {np.bincount(d['y_ams'][shot])[1:]}; " | |
| f"scoring the other {len(keep):,}") | |
| t0 = time.time() | |
| model, ams_stats = W.train_retrained(d["X_mad"], d["y_mad"], d["X_ams"], | |
| d["X_ams"][shot], d["y_ams"][shot]) | |
| P = W.predict_retrained(model, ams_stats, d["X_ams"][keep]) | |
| lab = W.decide(P, d["madrid_prior"]) | |
| print(f"trained in {time.time() - t0:.0f} s; macro F1 {W.macro_f1(d['y_ams'][keep], lab):.4f}") | |
| print("per-class F1:", np.round(f1_score(d["y_ams"][keep], lab, labels=W.CLASSES, average=None), 3)) | |
| # %% [markdown] | |
| # ## 3. The full protocol, as reported | |
| # | |
| # Every n, several draws, scored on all of Amsterdam. These come from | |
| # `results/full_score_*.json` (produced by `tools/full_score.py`); to recompute | |
| # them run `python winners.py protocol --model retrained --reps 5` (~35 min). | |
| # The zero-shot and organisers' rows come from an earlier full-city run | |
| # (`results/logs/03_gbdt_models_fullscore_run.log`, 20 draws per n). | |
| # %% | |
| N = [5, 25, 50, 100, 200] | |
| R = ROOT / "results" | |
| rows = {} | |
| for key, label in [("upweighted", "retrained LightGBM (5 draws per n)"), | |
| ("prototype", "prototype, within-class Mahalanobis (12 draws)")]: | |
| s = json.loads((R / f"full_score_{key}.json").read_text())["summary"]["full"] | |
| rows[label] = {n: s[str(n)]["mean"] for n in N} | {"std@25": s["25"]["std"]} | |
| rows["zero-shot LightGBM (20 draws)"] = dict(zip(N, [0.5928, 0.5925, 0.5922, 0.5918, 0.5903])) | |
| rows["organisers' prototype, our re-run on 393 features (20 draws)"] = dict(zip(N, [0.5066, 0.5821, 0.5965, 0.6034, 0.5972])) | |
| table = pd.DataFrame(rows).T | |
| table["average"] = table[N].mean(axis=1) | |
| table["peak"] = table[N].max(axis=1) | |
| print(table.round(4).to_string()) | |
| # %% | |
| import matplotlib.pyplot as plt | |
| INK, INK2, MUTED, GRID, SURF = "#0b0b0b", "#52514e", "#898781", "#e1e0d9", "#fcfcfb" | |
| series = [ # fixed categorical order: blue, orange, aqua | |
| ("upweighted", "Retrained LightGBM", "#2a78d6"), | |
| ("prototype", "Prototype (within-class Mahalanobis)", "#eb6834"), | |
| ] | |
| fig, ax = plt.subplots(figsize=(8.5, 5), facecolor=SURF) | |
| ax.set_facecolor(SURF) | |
| x = np.log2(N) | |
| for key, name, col in series: | |
| s = json.loads((R / f"full_score_{key}.json").read_text())["summary"]["full"] | |
| m = np.array([s[str(n)]["mean"] for n in N]) | |
| e = np.array([s[str(n)]["std"] for n in N]) | |
| ax.fill_between(x, m - e, m + e, color=col, alpha=0.12, lw=0) | |
| ax.plot(x, m, color=col, lw=2, marker="o", ms=8, mec=SURF, mew=2, label=name) | |
| dy = 7 if key == "upweighted" else -7 # keep the two end labels apart | |
| ax.annotate(name, (x[-1], m[-1]), xytext=(10, dy), textcoords="offset points", | |
| va="center", color=INK2, fontsize=9) | |
| zs_m = np.array(list(rows["zero-shot LightGBM (20 draws)"].values())) | |
| ax.plot(x, zs_m, color="#1baf7a", lw=2, marker="o", ms=8, mec=SURF, mew=2, | |
| label="Zero-shot LightGBM (Madrid only)") | |
| ax.annotate("Zero-shot LightGBM", (x[-1], zs_m[-1]), xytext=(10, -9), | |
| textcoords="offset points", va="center", color=INK2, fontsize=9) | |
| base = np.array(list(rows["organisers' prototype, our re-run on 393 features (20 draws)"].values())) | |
| ax.plot(x, base, color=MUTED, lw=1.5, ls="--", label="Organisers' prototype (our re-run)") | |
| ax.annotate("Organisers' prototype (re-run)", (x[-1], base[-1]), xytext=(10, 8), | |
| textcoords="offset points", va="center", color=MUTED, fontsize=9) | |
| ax.set_xticks(x, [str(n) for n in N]) | |
| ax.set_xlabel("Labelled Amsterdam pixels per class (log scale)", color=INK2) | |
| ax.set_ylabel("Macro F1, all of Amsterdam", color=INK2) | |
| ax.set_title("Macro F1 against labelling budget", loc="left", color=INK, fontsize=12) | |
| ax.grid(axis="y", color=GRID, lw=0.8) | |
| ax.set_axisbelow(True) | |
| for sp in ("top", "right"): | |
| ax.spines[sp].set_visible(False) | |
| for sp in ("left", "bottom"): | |
| ax.spines[sp].set_color(GRID) | |
| ax.tick_params(colors=MUTED) | |
| ax.set_xlim(x[0] - 0.3, x[-1] + 2.6) | |
| ax.legend(loc="lower right", frameon=False, fontsize=8, labelcolor=INK2) | |
| plt.tight_layout() | |
| fig.savefig(R / "f1_vs_n_full_city.png", dpi=160, facecolor=SURF) | |
| plt.show() | |
| # %% [markdown] | |
| # ## 4. Using them on your own data | |
| # | |
| # Your features must be the same 393 columns, in `models/meta.json` order | |
| # (`W.feature_names()`); `src/features.py` / notebook 1 builds them from the | |
| # raw hackathon parquet files. | |
| # | |
| # ```python | |
| # # zero-shot: no target labels at all | |
| # P = W.predict(W.load_zero_shot(), X_target) | |
| # labels = W.decide(P, madrid_prior) # or P.argmax(1) + 1 | |
| # | |
| # # retrained: you have n labelled target pixels per class | |
| # model, stats = W.train_retrained(X_mad, y_mad, X_target_all, X_shot, y_shot) | |
| # labels = W.decide(W.predict_retrained(model, stats, X_rest), madrid_prior) | |
| # ``` | |
| # | |
| # To compare with your own model on identical draws, use `W.shots_for(y_ams, n, | |
| # rep)` for the labelled pixels and score on the rest of Amsterdam - that is the | |
| # setup behind every number in the table above. | |