citysyntaxlab commited on
Commit
e12ec89
·
verified ·
1 Parent(s): a64bfbe

Upload code/train.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. code/train.py +177 -0
code/train.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Data loading + training for Campus Weather VAE.
3
+ """
4
+ import os, glob, time, json
5
+ import numpy as np
6
+ import pandas as pd
7
+ import torch
8
+ import torch.optim as optim
9
+ from torch.utils.data import Dataset, DataLoader
10
+ from model import WeatherVAE, get_config
11
+
12
+ VAR_COLS = ['WindSpeed Ave (m/s)', 'WindDir Ave (degrees)', 'AirTemp Ave (C)',
13
+ 'RelHum Ave (%)', 'AtmPress Ave (hPa)', 'GlobalRad Ave (W/m2)']
14
+ VAR_NAMES = ['WindSpeed', 'WindDir', 'AirTemp', 'RelHum', 'AtmPress', 'GlobalRad']
15
+ VAR_UNITS = ['m/s', '°', '°C', '%', 'hPa', 'W/m²']
16
+
17
+
18
+ def load_nus40(imputed_dir: str, station_ids=None):
19
+ """
20
+ Load NUS-40 data.
21
+ station_ids: list of ints (1-40) to load, or None for all.
22
+ Returns: data (T,N,V), coords (N,2), datetimes
23
+ """
24
+ files = sorted(glob.glob(os.path.join(imputed_dir, '*.csv')))
25
+ if station_ids is not None:
26
+ files = [f for f in files if int(f.split('WS')[1][:2]) in station_ids]
27
+
28
+ dfs = [pd.read_csv(f) for f in files]
29
+ N = len(dfs)
30
+ T = len(dfs[0])
31
+ V = len(VAR_COLS)
32
+
33
+ data = np.full((T, N, V), np.nan, dtype=np.float32)
34
+ coords = np.zeros((N, 2), dtype=np.float32)
35
+ for i, df in enumerate(dfs):
36
+ data[:, i, :] = df[VAR_COLS].values.astype(np.float32)
37
+ coords[i] = [df['Latitude'].iloc[0], df['Longitude'].iloc[0]]
38
+
39
+ # Fill remaining NaN with per-variable mean
40
+ for v in range(V):
41
+ col = data[:, :, v]
42
+ m = np.nanmean(col)
43
+ col[np.isnan(col)] = m
44
+ data[:, :, v] = col
45
+
46
+ datetimes = pd.to_datetime(dfs[0]['Datetime'])
47
+ print(f"Loaded {N} stations, T={T}, V={V} | {datetimes.iloc[0]} to {datetimes.iloc[-1]}")
48
+ return data, coords, datetimes
49
+
50
+
51
+ class FlatDataset(Dataset):
52
+ """Flattens (T, N, V) → individual observation vectors for VAE training."""
53
+ def __init__(self, data):
54
+ # data: (T, N, V) → (T*N, V)
55
+ T, N, V = data.shape
56
+ self.X = torch.from_numpy(data.reshape(T * N, V))
57
+ def __len__(self): return len(self.X)
58
+ def __getitem__(self, i): return self.X[i]
59
+
60
+
61
+ class WindowDataset(Dataset):
62
+ """Sliding windows for temporal tasks."""
63
+ def __init__(self, data, window=24, stride=6):
64
+ self.data = data
65
+ self.window = window
66
+ self.indices = list(range(0, data.shape[0] - window + 1, stride))
67
+ def __len__(self): return len(self.indices)
68
+ def __getitem__(self, i):
69
+ s = self.indices[i]
70
+ return torch.from_numpy(self.data[s:s + self.window])
71
+
72
+
73
+ def train_model(data_dir, config_size='base', epochs=100, batch_size=256,
74
+ lr=5e-4, device='cpu', save_dir='checkpoints', station_ids=None):
75
+ """Full training pipeline."""
76
+ os.makedirs(save_dir, exist_ok=True)
77
+
78
+ data, coords, datetimes = load_nus40(data_dir, station_ids)
79
+ T, N, V = data.shape
80
+ t_tr, t_va = int(T * 0.7), int(T * 0.85)
81
+
82
+ tr_ds = FlatDataset(data[:t_tr])
83
+ va_ds = FlatDataset(data[t_tr:t_va])
84
+ tr_ld = DataLoader(tr_ds, batch_size=batch_size, shuffle=True, drop_last=True)
85
+ va_ld = DataLoader(va_ds, batch_size=batch_size)
86
+
87
+ cfg = get_config(config_size)
88
+ model = WeatherVAE(**cfg).to(device)
89
+ mean = torch.tensor(data[:t_tr].mean(axis=(0, 1)), dtype=torch.float32).to(device)
90
+ std = torch.tensor(data[:t_tr].std(axis=(0, 1)), dtype=torch.float32).to(device)
91
+ model.set_normalisation(mean, std)
92
+
93
+ opt = optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01)
94
+ warmup = 5
95
+ sched = optim.lr_scheduler.LambdaLR(opt, lambda ep:
96
+ min((ep + 1) / warmup, 0.5 * (1 + np.cos(np.pi * max(0, ep - warmup) / max(1, epochs - warmup)))))
97
+
98
+ params = sum(p.numel() for p in model.parameters())
99
+ print(f"Model: {params:,} params | d_latent={cfg['d_latent']} | device={device}")
100
+
101
+ best_val = float('inf')
102
+ history = []
103
+
104
+ for ep in range(epochs):
105
+ t0 = time.time()
106
+ model.train()
107
+ tr_r, tr_k, n = 0, 0, 0
108
+ for batch in tr_ld:
109
+ out = model(batch.to(device))
110
+ out['loss'].backward()
111
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
112
+ opt.step(); opt.zero_grad()
113
+ tr_r += out['loss_recon']; tr_k += out['loss_kl']; n += 1
114
+
115
+ model.eval()
116
+ va_r, va_k, vn = 0, 0, 0
117
+ with torch.no_grad():
118
+ for batch in va_ld:
119
+ out = model(batch.to(device))
120
+ va_r += out['loss_recon']; va_k += out['loss_kl']; vn += 1
121
+
122
+ sched.step()
123
+ val_loss = va_r / vn + cfg['beta'] * va_k / vn
124
+ history.append({'train_recon': tr_r/n, 'train_kl': tr_k/n, 'val_recon': va_r/vn, 'val_kl': va_k/vn})
125
+
126
+ if val_loss < best_val:
127
+ best_val = val_loss
128
+ torch.save({'model': model.state_dict(), 'config': cfg,
129
+ 'mean': mean.cpu(), 'std': std.cpu(),
130
+ 'coords': coords, 'station_ids': station_ids,
131
+ 'history': history, 'epoch': ep}, f'{save_dir}/best.pt')
132
+
133
+ if (ep + 1) % 10 == 0 or ep == 0:
134
+ print(f"Ep {ep+1:3d}/{epochs} ({time.time()-t0:.1f}s) | "
135
+ f"Train R={tr_r/n:.4f} KL={tr_k/n:.3f} | Val R={va_r/vn:.4f} KL={va_k/vn:.3f}")
136
+
137
+ print(f"Done. Best val loss: {best_val:.4f}")
138
+
139
+ # Extract and save embeddings for entire dataset
140
+ model.eval()
141
+ all_emb = []
142
+ with torch.no_grad():
143
+ for t in range(0, T, 1000):
144
+ chunk = torch.from_numpy(data[t:t+1000].reshape(-1, V)).to(device)
145
+ emb = model.get_embedding(chunk).cpu().numpy()
146
+ all_emb.append(emb)
147
+ embeddings = np.concatenate(all_emb).reshape(T, N, -1)
148
+ np.savez_compressed(f'{save_dir}/embeddings.npz',
149
+ embeddings=embeddings, data=data, coords=coords,
150
+ datetimes=datetimes.values.astype(str))
151
+ print(f"Embeddings saved: {embeddings.shape}")
152
+
153
+ return model, data, coords, datetimes, embeddings
154
+
155
+
156
+ def load_trained(save_dir, device='cpu'):
157
+ """Load trained model and embeddings."""
158
+ ckpt = torch.load(f'{save_dir}/best.pt', map_location=device, weights_only=False)
159
+ model = WeatherVAE(**ckpt['config']).to(device)
160
+ model.load_state_dict(ckpt['model'])
161
+ model.set_normalisation(ckpt['mean'].to(device), ckpt['std'].to(device))
162
+ model.eval()
163
+
164
+ npz = np.load(f'{save_dir}/embeddings.npz', allow_pickle=True)
165
+ return model, npz['data'], npz['coords'], npz['embeddings'], ckpt
166
+
167
+
168
+ if __name__ == '__main__':
169
+ import argparse
170
+ p = argparse.ArgumentParser()
171
+ p.add_argument('--data', default='/app/data_tmp/imputed')
172
+ p.add_argument('--epochs', type=int, default=100)
173
+ p.add_argument('--config', default='base')
174
+ p.add_argument('--device', default='cpu')
175
+ p.add_argument('--save', default='/app/campus_weather/results/checkpoints')
176
+ args = p.parse_args()
177
+ train_model(args.data, args.config, args.epochs, device=args.device, save_dir=args.save)