Upload code/train.py with huggingface_hub
Browse files- 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)
|