Skip to content

Commit 4c8dddc

Browse files
version 4
1 parent bbbe6f7 commit 4c8dddc

25 files changed

Lines changed: 731 additions & 146 deletions

cybench/conf/dataset/default.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ framework: ${model.framework}
2525

2626
# Optional: (true or false) whether to cache the final dataset or load the cached dataset if it got created before
2727
# This creates extra files in cybench/data/cache/ but can save a lot of time when running multiple experiments on the same data
28-
use_cache: true
28+
use_cache: false

cybench/conf/dataset/europe.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ framework: ${model.framework}
4343

4444
# Optional: (true or false) whether to cache the final dataset or load the cached dataset if it got created before
4545
# This creates extra files in cybench/data/cache/ but can save a lot of time when running multiple experiments on the same data
46-
use_cache: true
46+
use_cache: false

cybench/conf/dataset/global.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,4 @@ framework: ${model.framework}
6565

6666
# Optional: (true or false) whether to cache the final dataset or load the cached dataset if it got created before
6767
# This creates extra files in cybench/data/cache/ but can save a lot of time when running multiple experiments on the same data
68-
use_cache: true
68+
use_cache: false

cybench/models/torch/trainer.py

Lines changed: 116 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from cybench.models.model import BaseModel
2828
from cybench.models.torch.utils.augmentation import create_collate_fn, AugmentationComposer
2929
from cybench.models.torch.utils.early_stopping import EarlyStopping
30+
from cybench.util.config_utils import deterministic_torch_training, set_seed
3031

3132
# init logger
3233
log = logging.getLogger(__name__)
@@ -164,6 +165,11 @@ def __init__(
164165
# Internal helpers
165166
# ------------------------------------------------------------------
166167

168+
def _dataloader_generator(self) -> torch.Generator:
169+
generator = torch.Generator()
170+
generator.manual_seed(self.seed)
171+
return generator
172+
167173
def _create_dataloader(
168174
self, dataset: TorchDataset, augment: bool, shuffle: bool
169175
) -> DataLoader[Any]:
@@ -179,15 +185,17 @@ def _create_dataloader(
179185
Configured DataLoader instance.
180186
"""
181187
dataloader = self.dataloader
188+
loader_kwargs: dict[str, Any] = {"dataset": dataset, "shuffle": shuffle}
189+
if shuffle:
190+
loader_kwargs["generator"] = self._dataloader_generator()
182191

183192
if self.augmentation is not None and augment:
184193
collate_fn = create_collate_fn(
185194
augmentation=self.augmentation,
186195
context_columns=dataset.x_context_columns,
187196
)
188-
return dataloader(dataset=dataset, collate_fn=collate_fn, shuffle=shuffle)
189-
else:
190-
return dataloader(dataset=dataset, shuffle=shuffle)
197+
loader_kwargs["collate_fn"] = collate_fn
198+
return dataloader(**loader_kwargs)
191199

192200
# ------------------------------------------------------------------
193201
# BaseModel API
@@ -211,6 +219,8 @@ def fit( # pyright: ignore[reportIncompatibleMethodOverride]
211219
Returns:
212220
A tuple containing the fitted model and a dict with training history.
213221
"""
222+
set_seed(self.seed)
223+
214224
epochs = fit_params.get("epochs", self.epochs)
215225
val_dataset = fit_params.get("val_dataset", None)
216226
val_every_n_epochs = fit_params.get("val_every_n_epochs", 1)
@@ -242,110 +252,111 @@ def fit( # pyright: ignore[reportIncompatibleMethodOverride]
242252
total_batches = epochs * len(train_loader)
243253
pbar = tqdm(range(total_batches), desc=self.__class__.__name__) if self.verbose else None
244254

245-
self.model.train()
246-
# TODO delete time tracking. Only for debugging
247-
tt = 0
248-
start_training = time.time()
249-
for epoch in range(epochs):
250-
epochs_run = epoch + 1
251-
total_loss = 0.0
252-
num_batches = 0
253-
254-
for batch in train_loader:
255-
y, x_ctx, x_ts, doy_ts = batch
256-
if (not self.preload_to_device) and (self.device.type != "cpu"):
257-
y = y.to(self.device, non_blocking=True)
258-
x_ctx = x_ctx.to(self.device, non_blocking=True)
259-
x_ts = x_ts.to(self.device, non_blocking=True)
260-
doy_ts = doy_ts.to(self.device, non_blocking=True)
261-
262-
self.optimizer.zero_grad(set_to_none=True)
263-
start = time.time()
264-
pred = self.model(x_ctx, x_ts, doy_ts)
265-
# DEBUG Model:
266-
#print(self.model.state_dict()["regression_head.net.3.weight"][0, 0])
267-
#print(self.model.context_encoder(x_ctx[0]))
268-
#print(self.model.temporal_encoder(x_ts[0]))
255+
with deterministic_torch_training():
256+
self.model.train()
257+
# TODO delete time tracking. Only for debugging
258+
tt = 0
259+
start_training = time.time()
260+
for epoch in range(epochs):
261+
epochs_run = epoch + 1
262+
total_loss = 0.0
263+
num_batches = 0
264+
265+
for batch in train_loader:
266+
y, x_ctx, x_ts, doy_ts = batch
267+
if (not self.preload_to_device) and (self.device.type != "cpu"):
268+
y = y.to(self.device, non_blocking=True)
269+
x_ctx = x_ctx.to(self.device, non_blocking=True)
270+
x_ts = x_ts.to(self.device, non_blocking=True)
271+
doy_ts = doy_ts.to(self.device, non_blocking=True)
272+
273+
self.optimizer.zero_grad(set_to_none=True)
274+
start = time.time()
275+
pred = self.model(x_ctx, x_ts, doy_ts)
276+
# DEBUG Model:
277+
#print(self.model.state_dict()["regression_head.net.3.weight"][0, 0])
278+
#print(self.model.context_encoder(x_ctx[0]))
279+
#print(self.model.temporal_encoder(x_ts[0]))
280+
281+
if pred.ndim > 1:
282+
pred = pred.squeeze(-1)
283+
284+
loss = self.loss_fn(pred, y.squeeze(-1))
285+
loss.backward()
286+
tt += time.time() - start
287+
if self.max_grad_norm is not None:
288+
torch.nn.utils.clip_grad_norm_(
289+
self.model.parameters(),
290+
self.max_grad_norm,
291+
)
292+
293+
self.optimizer.step()
294+
295+
total_loss += loss.item()
296+
num_batches += 1
297+
298+
if pbar is not None:
299+
pbar.update(1)
300+
301+
avg_loss = total_loss / num_batches
302+
history["train_loss"].append(avg_loss)
303+
304+
val_loss = None
305+
# Validate every N epochs
306+
if val_loader is not None and (epoch + 1) % val_every_n_epochs == 0:
307+
val_loss = self._evaluate_loss(val_loader)
308+
history["val_loss"].append(val_loss)
309+
else:
310+
history["val_loss"].append(None)
311+
312+
if self.early_stopping is not None:
313+
monitor_loss = None
314+
if early_stopping_monitor == "val" and val_loss is not None:
315+
monitor_loss = val_loss
316+
elif early_stopping_monitor == "train":
317+
monitor_loss = avg_loss
318+
if monitor_loss is not None:
319+
self.early_stopping(monitor_loss, self.model, epoch + 1)
320+
if self.early_stopping.early_stop:
321+
log.info("Early stopping triggered.")
322+
if self.verbose:
323+
print(f"Early stopping triggered: after epoch {epoch + 1}")
324+
break
325+
326+
# Step Scheduler (ReduceLROnPlateau requires val loss)
327+
if self.scheduler is not None:
328+
if isinstance(self.scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
329+
# Only step if we have a valid metric this epoch
330+
if val_loss is not None:
331+
self.scheduler.step(val_loss)
332+
else:
333+
self.scheduler.step()
334+
335+
if self.verbose:
336+
lr = self.optimizer.param_groups[0]['lr']
337+
msg = f"Epoch {epoch + 1:4d}/{epochs} | train {avg_loss:.4f}"
338+
if val_loss is not None:
339+
msg += f" | val {val_loss:.4f}"
340+
msg += f" | lr {lr:.2e}"
341+
tqdm.write(msg)
342+
elif epoch_log_interval and (
343+
(epoch + 1) % int(epoch_log_interval) == 0
344+
or epoch + 1 == epochs
345+
or (self.early_stopping is not None and self.early_stopping.early_stop)
346+
):
347+
msg = f"Epoch {epoch + 1}/{epochs} | train {avg_loss:.4f}"
348+
if val_loss is not None:
349+
msg += f" | val {val_loss:.4f}"
350+
log.info(msg)
269351

270-
if pred.ndim > 1:
271-
pred = pred.squeeze(-1)
352+
log.debug("Forward and backward pass took", np.round(tt / (time.time() - start_training) * 100), "% of training time.")
353+
if pbar is not None:
354+
pbar.close()
272355

273-
loss = self.loss_fn(pred, y.squeeze(-1))
274-
loss.backward()
275-
tt += time.time() - start
276-
if self.max_grad_norm is not None:
277-
torch.nn.utils.clip_grad_norm_(
278-
self.model.parameters(),
279-
self.max_grad_norm,
280-
)
281-
282-
self.optimizer.step()
283-
284-
total_loss += loss.item()
285-
num_batches += 1
286-
287-
if pbar is not None:
288-
pbar.update(1)
289-
290-
avg_loss = total_loss / num_batches
291-
history["train_loss"].append(avg_loss)
292-
293-
val_loss = None
294-
# Validate every N epochs
295-
if val_loader is not None and (epoch + 1) % val_every_n_epochs == 0:
296-
val_loss = self._evaluate_loss(val_loader)
297-
history["val_loss"].append(val_loss)
298-
else:
299-
history["val_loss"].append(None)
300-
301-
if self.early_stopping is not None:
302-
monitor_loss = None
303-
if early_stopping_monitor == "val" and val_loss is not None:
304-
monitor_loss = val_loss
305-
elif early_stopping_monitor == "train":
306-
monitor_loss = avg_loss
307-
if monitor_loss is not None:
308-
self.early_stopping(monitor_loss, self.model, epoch + 1)
309-
if self.early_stopping.early_stop:
310-
log.info("Early stopping triggered.")
311-
if self.verbose:
312-
print(f"Early stopping triggered: after epoch {epoch + 1}")
313-
break
314-
315-
# Step Scheduler (ReduceLROnPlateau requires val loss)
316-
if self.scheduler is not None:
317-
if isinstance(self.scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
318-
# Only step if we have a valid metric this epoch
319-
if val_loss is not None:
320-
self.scheduler.step(val_loss)
321-
else:
322-
self.scheduler.step()
323-
324-
if self.verbose:
325-
lr = self.optimizer.param_groups[0]['lr']
326-
msg = f"Epoch {epoch + 1:4d}/{epochs} | train {avg_loss:.4f}"
327-
if val_loss is not None:
328-
msg += f" | val {val_loss:.4f}"
329-
msg += f" | lr {lr:.2e}"
330-
tqdm.write(msg)
331-
elif epoch_log_interval and (
332-
(epoch + 1) % int(epoch_log_interval) == 0
333-
or epoch + 1 == epochs
334-
or (self.early_stopping is not None and self.early_stopping.early_stop)
335-
):
336-
msg = f"Epoch {epoch + 1}/{epochs} | train {avg_loss:.4f}"
337-
if val_loss is not None:
338-
msg += f" | val {val_loss:.4f}"
339-
log.info(msg)
340-
341-
log.debug("Forward and backward pass took", np.round(tt / (time.time() - start_training) * 100), "% of training time.")
342-
if pbar is not None:
343-
pbar.close()
344-
345-
# Restore Best Weights (Critical Step)
346-
if self.early_stopping is not None and self.early_stopping.best_model_state is not None:
347-
log.info(f"Restoring best model weights (Loss: {self.early_stopping.best_loss:.3f})")
348-
self.model.load_state_dict(self.early_stopping.best_model_state)
356+
# Restore Best Weights (Critical Step)
357+
if self.early_stopping is not None and self.early_stopping.best_model_state is not None:
358+
log.info(f"Restoring best model weights (Loss: {self.early_stopping.best_loss:.3f})")
359+
self.model.load_state_dict(self.early_stopping.best_model_state)
349360

350361
if self.early_stopping is not None and self.early_stopping.best_epoch is not None:
351362
history["best_epoch"] = self.early_stopping.best_epoch

cybench/runs/analysis/audit_benchmark_status.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def main(argv: list[str] | None = None) -> int:
187187
parser.add_argument("--countries", nargs="+")
188188
parser.add_argument("--all-countries", action="store_true")
189189
parser.add_argument("--horizon", default="eos")
190-
parser.add_argument("--version", type=int, default=3)
190+
parser.add_argument("--version", type=int, default=4)
191191
parser.add_argument("--output-root", type=Path)
192192
parser.add_argument("--repo-root", type=Path, default=_REPO_ROOT)
193193
parser.add_argument("--publish-root", type=Path)

0 commit comments

Comments
 (0)