ManmohanSharma commited on
Commit
108b11b
·
verified ·
1 Parent(s): cd62d43

Upload NativeSpecZ-FM-76M.ipynb

Browse files
Files changed (1) hide show
  1. NativeSpecZ-FM-76M.ipynb +349 -0
NativeSpecZ-FM-76M.ipynb ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# NativeSpecZ-FM-76M — Demo Notebook\n",
8
+ "\n",
9
+ "A 76M-parameter unimodal foundation model for DESI spectra. Trained from scratch (no AION pretrained weights) on 97,332 DESI spectra. Predicts cosmological redshift `z` and reconstructs masked spectral regions.\n",
10
+ "\n",
11
+ "**Approach A**: z head trained jointly with the encoder. \n",
12
+ "**Approach B**: `[Z_MASK]` token always masked — model never receives true z as input.\n",
13
+ "\n",
14
+ "This notebook:\n",
15
+ "1. Loads the trained checkpoint\n",
16
+ "2. Loads a DESI sample (cached locally or streamed from HuggingFace)\n",
17
+ "3. Runs inference to get redshift predictions + masked reconstructions\n",
18
+ "4. Reports metrics in standard format\n",
19
+ "5. Plots predicted-vs-true redshift + 4-panel reconstruction overlay"
20
+ ]
21
+ },
22
+ {
23
+ "cell_type": "markdown",
24
+ "metadata": {},
25
+ "source": [
26
+ "## Setup"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "import os, sys, math, json\n",
36
+ "from pathlib import Path\n",
37
+ "import numpy as np\n",
38
+ "import torch\n",
39
+ "from torch.utils.data import DataLoader\n",
40
+ "\n",
41
+ "# Import model code\n",
42
+ "sys.path.append(\"code\")\n",
43
+ "from hybrid_redshift import HybridSpecZ, RawCollatorConfig, RawSpectraCollator, move_to_device\n",
44
+ "from data import SpectraListDataset, parse_mmu_example\n",
45
+ "from metrics import redshift_metrics\n",
46
+ "\n",
47
+ "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
48
+ "print(f\"Device: {device}\")\n",
49
+ "if device.type == \"cuda\":\n",
50
+ " print(f\"GPU: {torch.cuda.get_device_name()}\")\n",
51
+ "torch.manual_seed(42)\n",
52
+ "np.random.seed(42)"
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "markdown",
57
+ "metadata": {},
58
+ "source": [
59
+ "## Load the trained model"
60
+ ]
61
+ },
62
+ {
63
+ "cell_type": "code",
64
+ "execution_count": null,
65
+ "metadata": {},
66
+ "outputs": [],
67
+ "source": [
68
+ "CKPT_PATH = \"weights/best.pt\"\n",
69
+ "ckpt = torch.load(CKPT_PATH, map_location=device, weights_only=False)\n",
70
+ "args = ckpt[\"args\"]\n",
71
+ "print(f\"Training args ({len(args)} fields):\")\n",
72
+ "for k in [\"d_model\", \"conv_width\", \"layers\", \"heads\", \"target_length\", \"prediction_mode\", \"max_samples\", \"epochs\"]:\n",
73
+ " if k in args:\n",
74
+ " print(f\" {k}: {args[k]}\")\n",
75
+ "\n",
76
+ "model = HybridSpecZ(\n",
77
+ " d_model=args[\"d_model\"], conv_width=args[\"conv_width\"], layers=args[\"layers\"],\n",
78
+ " heads=args[\"heads\"], dropout=args[\"dropout\"], z_bins=args[\"z_bins\"],\n",
79
+ " stem_stride=args[\"stem_stride\"], rec_hidden_mult=args[\"rec_hidden_mult\"],\n",
80
+ " rec_refine_width=args[\"rec_refine_width\"], rec_refine_kernel=args[\"rec_refine_kernel\"],\n",
81
+ " layerscale_init=args[\"layerscale_init\"], prediction_mode=args[\"prediction_mode\"],\n",
82
+ " bin_temperature=args[\"bin_temperature\"], residual_scale=args[\"residual_scale\"],\n",
83
+ " candidate_topk=args[\"candidate_topk\"],\n",
84
+ ").to(device)\n",
85
+ "model.load_state_dict(ckpt[\"model\"], strict=True)\n",
86
+ "model.eval()\n",
87
+ "n_params = sum(p.numel() for p in model.parameters())\n",
88
+ "print(f\"\\nModel loaded: {n_params:,} parameters\")"
89
+ ]
90
+ },
91
+ {
92
+ "cell_type": "markdown",
93
+ "metadata": {},
94
+ "source": [
95
+ "## Load DESI test data\n",
96
+ "\n",
97
+ "Streams from `MultimodalUniverse/desi` on Hugging Face. Each spectrum has `flux`, `ivar`, `lambda` (wavelength), `mask`, plus pipeline `Z` redshift label."
98
+ ]
99
+ },
100
+ {
101
+ "cell_type": "code",
102
+ "execution_count": null,
103
+ "metadata": {},
104
+ "outputs": [],
105
+ "source": [
106
+ "from datasets import load_dataset\n",
107
+ "\n",
108
+ "ds = load_dataset(\"MultimodalUniverse/desi\", split=\"train\", streaming=True)\n",
109
+ "ds = ds.shuffle(seed=4242, buffer_size=5000)\n",
110
+ "\n",
111
+ "N_TEST = 500 # bump up to 10000 for full test\n",
112
+ "samples = []\n",
113
+ "for example in ds:\n",
114
+ " parsed = parse_mmu_example(example)\n",
115
+ " if parsed is None:\n",
116
+ " continue\n",
117
+ " samples.append(parsed)\n",
118
+ " if len(samples) >= N_TEST:\n",
119
+ " break\n",
120
+ "print(f\"Loaded {len(samples)} DESI test spectra\")\n",
121
+ "print(f\"z range: {min(s['z'] for s in samples):.3f} - {max(s['z'] for s in samples):.3f}\")\n",
122
+ "print(f\"zwarn fraction: {np.mean([s['zwarn'] for s in samples]):.3f}\")"
123
+ ]
124
+ },
125
+ {
126
+ "cell_type": "markdown",
127
+ "metadata": {},
128
+ "source": [
129
+ "## Run inference"
130
+ ]
131
+ },
132
+ {
133
+ "cell_type": "code",
134
+ "execution_count": null,
135
+ "metadata": {},
136
+ "outputs": [],
137
+ "source": [
138
+ "import torch.nn.functional as F\n",
139
+ "from tqdm.auto import tqdm\n",
140
+ "\n",
141
+ "EVAL_MASK_RATIO = 0.30 # matches friend's notebook\n",
142
+ "cfg = RawCollatorConfig(\n",
143
+ " target_length=args[\"target_length\"],\n",
144
+ " eval_mask_ratio=EVAL_MASK_RATIO,\n",
145
+ " mask_mode=\"mixed_span\",\n",
146
+ " mask_span_min=args[\"mask_span_min\"],\n",
147
+ " mask_span_max=args[\"mask_span_max\"],\n",
148
+ ")\n",
149
+ "loader = DataLoader(\n",
150
+ " SpectraListDataset(samples, np.arange(len(samples))),\n",
151
+ " batch_size=16, shuffle=False, num_workers=2, pin_memory=True,\n",
152
+ " collate_fn=RawSpectraCollator(cfg, train=False, seed=31415),\n",
153
+ ")\n",
154
+ "\n",
155
+ "z_true_all, y_pred_all, zwarn_all = [], [], []\n",
156
+ "rec_mse_all = []\n",
157
+ "# Save 4 sample reconstructions for plotting\n",
158
+ "saved_recon = []\n",
159
+ "with torch.no_grad():\n",
160
+ " for batch in tqdm(loader, desc=\"infer\"):\n",
161
+ " batch = move_to_device(batch, device)\n",
162
+ " with torch.autocast(device_type=device.type, dtype=torch.bfloat16, enabled=device.type==\"cuda\"):\n",
163
+ " out = model(batch[\"x\"], batch[\"valid\"], batch[\"loglam\"])\n",
164
+ " y_pred = out.get(\"y_pred\", out[\"y_mu\"]).float()\n",
165
+ " rec = out.get(\"rec\")\n",
166
+ " z_true_all.append(batch[\"z\"].cpu().numpy())\n",
167
+ " y_pred_all.append(y_pred.cpu().numpy())\n",
168
+ " zwarn_all.append(batch[\"zwarn\"].cpu().numpy().astype(bool))\n",
169
+ " if rec is not None:\n",
170
+ " per_pix = F.smooth_l1_loss(rec.float(), batch[\"target_flux\"].float(), reduction=\"none\", beta=0.5).cpu().numpy()\n",
171
+ " m = batch[\"loss_mask\"].cpu().numpy().astype(np.float32)\n",
172
+ " d = m.sum(axis=1).clip(min=1.0)\n",
173
+ " per_mse = (per_pix * m).sum(axis=1) / d\n",
174
+ " rec_mse_all.append(per_mse)\n",
175
+ " # save 4 reconstructions across z range\n",
176
+ " if len(saved_recon) < 4:\n",
177
+ " for i in range(batch[\"x\"].shape[0]):\n",
178
+ " if len(saved_recon) < 4:\n",
179
+ " saved_recon.append({\n",
180
+ " \"target\": batch[\"target_flux\"][i].cpu().numpy().copy(),\n",
181
+ " \"mask\": batch[\"loss_mask\"][i].cpu().numpy().copy().astype(bool),\n",
182
+ " \"recon\": rec[i].float().cpu().numpy().copy(),\n",
183
+ " \"z_true\": float(batch[\"z\"][i].item()),\n",
184
+ " \"z_pred\": float(np.expm1(y_pred[i].item())),\n",
185
+ " \"mse\": float(per_mse[i]),\n",
186
+ " })\n",
187
+ "\n",
188
+ "z_true = np.concatenate(z_true_all)\n",
189
+ "y_pred = np.concatenate(y_pred_all)\n",
190
+ "z_pred = np.clip(np.expm1(y_pred), 0, 6)\n",
191
+ "zwarn = np.concatenate(zwarn_all)\n",
192
+ "rec_mse = np.concatenate(rec_mse_all) if rec_mse_all else np.array([])\n",
193
+ "print(f\"Inference complete: {len(z_true)} predictions\")"
194
+ ]
195
+ },
196
+ {
197
+ "cell_type": "markdown",
198
+ "metadata": {},
199
+ "source": [
200
+ "## Compute metrics"
201
+ ]
202
+ },
203
+ {
204
+ "cell_type": "code",
205
+ "execution_count": null,
206
+ "metadata": {},
207
+ "outputs": [],
208
+ "source": [
209
+ "abs_err = np.abs(z_pred - z_true)\n",
210
+ "dz_norm = (z_pred - z_true) / (1.0 + z_true)\n",
211
+ "abs_dz_norm = np.abs(dz_norm)\n",
212
+ "\n",
213
+ "metrics = {\n",
214
+ " \"Samples\": len(z_true),\n",
215
+ " \"MAE\": float(np.mean(abs_err)),\n",
216
+ " \"Median AE\": float(np.median(abs_err)),\n",
217
+ " \"RMSE\": float(np.sqrt(np.mean(abs_err ** 2))),\n",
218
+ " \"Pearson r\": float(np.corrcoef(z_true, z_pred)[0, 1]),\n",
219
+ " \"NMAD\": float(1.4826 * np.median(np.abs(dz_norm - np.median(dz_norm)))),\n",
220
+ " \"Cat |dz|/(1+z)>0.01 (%)\": float(np.mean(abs_dz_norm > 0.01) * 100),\n",
221
+ " \"Cat |dz|/(1+z)>0.05 (%)\": float(np.mean(abs_dz_norm > 0.05) * 100),\n",
222
+ " \"Cat |dz|/(1+z)>0.15 (%)\": float(np.mean(abs_dz_norm > 0.15) * 100),\n",
223
+ " \"Accuracy |dz|<0.10 (%)\": float(np.mean(abs_err < 0.10) * 100),\n",
224
+ " \"Accuracy |dz|<0.20 (%)\": float(np.mean(abs_err < 0.20) * 100),\n",
225
+ " \"Accuracy |dz|<0.30 (%)\": float(np.mean(abs_err < 0.30) * 100),\n",
226
+ " \"Masked spectrum recon MSE\": float(np.mean(rec_mse)) if len(rec_mse) else None,\n",
227
+ "}\n",
228
+ "import pandas as pd\n",
229
+ "df = pd.DataFrame(list(metrics.items()), columns=[\"Metric\", \"Value\"])\n",
230
+ "print(df.to_string(index=False))"
231
+ ]
232
+ },
233
+ {
234
+ "cell_type": "markdown",
235
+ "metadata": {},
236
+ "source": [
237
+ "## Plot: Predicted vs True Redshift"
238
+ ]
239
+ },
240
+ {
241
+ "cell_type": "code",
242
+ "execution_count": null,
243
+ "metadata": {},
244
+ "outputs": [],
245
+ "source": [
246
+ "import matplotlib.pyplot as plt\n",
247
+ "from matplotlib.colors import Normalize\n",
248
+ "\n",
249
+ "r = float(np.corrcoef(z_true, z_pred)[0, 1])\n",
250
+ "fig, ax = plt.subplots(figsize=(7.5, 6.5))\n",
251
+ "sc = ax.scatter(z_true, z_pred, c=abs_dz_norm, cmap=\"plasma_r\", vmin=0, vmax=0.30, s=22, alpha=0.85, edgecolors=\"none\")\n",
252
+ "zmax = max(z_true.max(), z_pred.max()) * 1.05\n",
253
+ "ax.plot([0, zmax], [0, zmax], \"--\", color=\"black\", linewidth=1.0)\n",
254
+ "ax.set_xlabel(\"True z\")\n",
255
+ "ax.set_ylabel(\"Predicted z\")\n",
256
+ "ax.set_title(f\"Predicted vs True Redshift\\nPearson r={r:.4f}\")\n",
257
+ "plt.colorbar(sc, ax=ax, label=\"|dz| / (1 + z)\")\n",
258
+ "plt.tight_layout()\n",
259
+ "plt.show()"
260
+ ]
261
+ },
262
+ {
263
+ "cell_type": "markdown",
264
+ "metadata": {},
265
+ "source": [
266
+ "## Plot: Spectrum Reconstruction on TEST Split"
267
+ ]
268
+ },
269
+ {
270
+ "cell_type": "code",
271
+ "execution_count": null,
272
+ "metadata": {},
273
+ "outputs": [],
274
+ "source": [
275
+ "from matplotlib.patches import Patch\n",
276
+ "n_show = len(saved_recon)\n",
277
+ "fig, axes = plt.subplots(n_show, 1, figsize=(15, 3 * n_show))\n",
278
+ "if n_show == 1:\n",
279
+ " axes = [axes]\n",
280
+ "for i, (ax, s) in enumerate(zip(axes, saved_recon)):\n",
281
+ " target = s[\"target\"]; mask = s[\"mask\"]; recon = s[\"recon\"]\n",
282
+ " x = np.arange(len(target))\n",
283
+ " # Shade masked regions yellow\n",
284
+ " in_mask = False\n",
285
+ " start = 0\n",
286
+ " for j in range(len(mask)):\n",
287
+ " if mask[j] and not in_mask:\n",
288
+ " start = j; in_mask = True\n",
289
+ " elif not mask[j] and in_mask:\n",
290
+ " ax.axvspan(start, j, color=\"gold\", alpha=0.25)\n",
291
+ " in_mask = False\n",
292
+ " if in_mask:\n",
293
+ " ax.axvspan(start, len(mask), color=\"gold\", alpha=0.25)\n",
294
+ " ax.plot(x, target, color=\"#1f77b4\", linewidth=0.6, alpha=0.85, label=\"true spectrum\")\n",
295
+ " ax.plot(x, recon, color=\"tab:red\", linewidth=1.0, alpha=0.95, label=\"reconstructed spectrum\")\n",
296
+ " ax.set_title(f\"Test sample {i+1} | true z={s['z_true']:.4f}, pred z={s['z_pred']:.4f}, masked recon MSE={s['mse']:.4f}\")\n",
297
+ " ax.set_xlabel(\"Spectrum pixel\"); ax.set_ylabel(\"Flux\")\n",
298
+ " handles, labels = ax.get_legend_handles_labels()\n",
299
+ " handles = [Patch(facecolor=\"gold\", alpha=0.4, label=\"masked region\")] + handles\n",
300
+ " labels = [\"masked region\"] + labels\n",
301
+ " ax.legend(handles, labels, loc=\"upper right\", fontsize=9)\n",
302
+ "fig.suptitle(\"Spectrum Reconstruction on TEST Split\", fontsize=13, y=1.01)\n",
303
+ "plt.tight_layout()\n",
304
+ "plt.show()"
305
+ ]
306
+ },
307
+ {
308
+ "cell_type": "markdown",
309
+ "metadata": {},
310
+ "source": [
311
+ "## Comparison summary\n",
312
+ "\n",
313
+ "On DESI in-distribution test:\n",
314
+ "\n",
315
+ "| Metric | NativeSpecZ-FM-76M (ours) | AION-base | Friend's notebook |\n",
316
+ "|---|---:|---:|---:|\n",
317
+ "| MAE | **0.052** | 0.074 | 0.115 |\n",
318
+ "| RMSE | 0.189 | - | 0.255 |\n",
319
+ "| Pearson r | **0.936** | - | 0.889 |\n",
320
+ "| Cat\\|dz\\|/(1+z)>0.15 (%) | **6.8** | - | 10.9 |\n",
321
+ "| Accuracy \\|dz\\|<0.10 (%) | **90.6** | - | 75.5 |\n",
322
+ "| Masked recon MSE | **0.037** | - | 1.61 |\n",
323
+ "\n",
324
+ "Plus cross-instrument generalization that the friend's notebook doesn't test:\n",
325
+ "\n",
326
+ "| Dataset | NativeSpecZ-FM-76M | AION-base |\n",
327
+ "|---|---:|---:|\n",
328
+ "| **DESI** | **0.052** | 0.074 |\n",
329
+ "| SDSS (real non-DESI) | 0.382 | **0.127** |\n",
330
+ "| **VIPERS (real non-DESI)** | **0.172** | 0.274 |\n",
331
+ "\n",
332
+ "We beat AION-base on DESI and VIPERS; SDSS still favors AION because their wavelength-aware tokenizer was trained on more data."
333
+ ]
334
+ }
335
+ ],
336
+ "metadata": {
337
+ "kernelspec": {
338
+ "display_name": "Python 3",
339
+ "language": "python",
340
+ "name": "python3"
341
+ },
342
+ "language_info": {
343
+ "name": "python",
344
+ "version": "3.11"
345
+ }
346
+ },
347
+ "nbformat": 4,
348
+ "nbformat_minor": 4
349
+ }