2727from cybench .models .model import BaseModel
2828from cybench .models .torch .utils .augmentation import create_collate_fn , AugmentationComposer
2929from cybench .models .torch .utils .early_stopping import EarlyStopping
30+ from cybench .util .config_utils import deterministic_torch_training , set_seed
3031
3132# init logger
3233log = 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
0 commit comments