@@ -571,24 +571,32 @@ def on_train_end(self):
571571class TrainingMonitor (Callback ):
572572 """Live-plot the predicted solution and the loss history during training.
573573
574- Every `period` epochs, this callback redraws two subplots: the current
575- network prediction along `x_plot` (optionally against a reference
576- solution), and the train/test loss history on a log scale. Unlike
577- ``MovieDumper``, which only writes an animation to disk in
578- ``on_train_end``, or ``dde.saveplot``, which produces a single static
579- plot after training finishes, this callback gives feedback while the
580- model is still training.
574+ Every `period` epochs, this callback redraws the current network
575+ prediction over `x_plot` (optionally against a reference solution), and
576+ the train/test loss history on a log scale. Unlike ``MovieDumper``,
577+ which only writes an animation to disk in ``on_train_end``, or
578+ ``dde.saveplot``, which produces a single static plot after training
579+ finishes, this callback gives feedback while the model is still
580+ training.
581+
582+ Both ODEs (`x_plot` of shape (N, 1), e.g. `y = f(t)`) and 2D
583+ space-time PDEs (`x_plot` of shape (N, 2), e.g. `y = f(x, t)` as in
584+ ``diffusion_1d.py``) are supported. For the 2D case, the solution is
585+ shown as a scatter plot colored by `y`, with a matching panel for
586+ `y_reference` (if given) sharing the same color scale for an
587+ at-a-glance comparison.
581588
582589 Args:
583590 period (int): Interval (number of epochs) between plot updates.
584591 component (int): Which component of the solution to plot.
585- x_plot: Points (array of shape (N, dim)) at which the solution is
586- evaluated and plotted.
592+ x_plot: Points at which the solution is evaluated and plotted, of
593+ shape (N, 1) for `y = f(t)`-like problems, or (N, 2) for
594+ `y = f(x, t)`-like problems.
587595 y_reference: A function `y_reference(x_plot)` returning the
588596 reference (e.g., exact) solution for comparison. If ``None``,
589597 only the predicted solution is shown.
590598 show_loss (bool): If True, also plot the training/testing loss
591- history in a second subplot.
599+ history in an additional subplot.
592600
593601 Warning:
594602 Live plotting requires an interactive Matplotlib backend. In
@@ -611,6 +619,16 @@ def __init__(
611619 self .period = period
612620 self .component = component
613621 self .x_plot = np .asarray (x_plot , dtype = config .real (np ))
622+ if self .x_plot .ndim == 1 :
623+ self .x_plot = self .x_plot [:, None ]
624+ if self .x_plot .shape [1 ] not in (1 , 2 ):
625+ raise ValueError (
626+ "TrainingMonitor only supports 1D (e.g. y = f(t)) or 2D "
627+ "(e.g. y = f(x, t)) `x_plot`, got {} columns." .format (
628+ self .x_plot .shape [1 ]
629+ )
630+ )
631+ self .dim = self .x_plot .shape [1 ]
614632 self .y_reference = y_reference
615633 self .show_loss = show_loss
616634
@@ -619,7 +637,10 @@ def __init__(
619637 self .plt = None
620638 self .fig = None
621639 self .ax_sol = None
640+ self .ax_ref = None
622641 self .ax_loss = None
642+ self .cbar_sol = None
643+ self .cbar_ref = None
623644
624645 def on_train_begin (self ):
625646 self .epochs_since_last = 0
@@ -640,11 +661,20 @@ def on_train_begin(self):
640661
641662 self .plt = plt
642663 plt .ion ()
643- if self .show_loss :
644- self .fig , (self .ax_sol , self .ax_loss ) = plt .subplots (1 , 2 , figsize = (10 , 4 ))
645- else :
646- self .fig , self .ax_sol = plt .subplots (figsize = (5 , 4 ))
647- self .ax_loss = None
664+ self .ax_ref = None
665+ self .cbar_sol = None
666+ self .cbar_ref = None
667+ n_sol_axes = 1 if (self .dim == 1 or self .y_reference is None ) else 2
668+ n_axes = n_sol_axes + (1 if self .show_loss else 0 )
669+ self .fig , axes = plt .subplots (1 , n_axes , figsize = (5 * n_axes , 4 ))
670+ axes = np .atleast_1d (axes )
671+ i = 0
672+ self .ax_sol = axes [i ]
673+ i += 1
674+ if n_sol_axes == 2 :
675+ self .ax_ref = axes [i ]
676+ i += 1
677+ self .ax_loss = axes [i ] if self .show_loss else None
648678 self ._redraw ()
649679
650680 @staticmethod
@@ -673,36 +703,76 @@ def _update_plot(self):
673703 try :
674704 y_pred = self .model .predict (self .x_plot )[:, self .component ]
675705
676- x = np .ravel (self .x_plot )
677- self .ax_sol .cla ()
678- self .ax_sol .plot (x , y_pred , "--r" , label = "Predicted" )
679- if self .y_reference is not None :
680- y_ref = np .ravel (self .y_reference (self .x_plot ))
681- self .ax_sol .plot (x , y_ref , "-k" , label = "Reference" )
682- self .ax_sol .set_xlabel ("x" )
683- self .ax_sol .set_ylabel ("y" )
684- self .ax_sol .set_title (
685- "Epoch {}" .format (self .model .train_state .iteration )
686- )
687- self .ax_sol .legend ()
706+ if self .dim == 1 :
707+ self ._plot_1d (y_pred )
708+ else :
709+ self ._plot_2d (y_pred )
688710
689711 if self .show_loss :
690- loss_history = self .model .losshistory
691- loss_train = [np .sum (loss ) for loss in loss_history .loss_train ]
692- loss_test = [np .sum (loss ) for loss in loss_history .loss_test ]
693- self .ax_loss .cla ()
694- self .ax_loss .semilogy (loss_history .steps , loss_train , label = "Train loss" )
695- self .ax_loss .semilogy (loss_history .steps , loss_test , label = "Test loss" )
696- self .ax_loss .set_xlabel ("# Steps" )
697- self .ax_loss .set_ylabel ("Loss" )
698- self .ax_loss .legend ()
712+ self ._plot_loss ()
699713
700714 self ._redraw ()
701715 except Exception as e : # pylint: disable=broad-except
702716 # Never let a plotting error interrupt training.
703717 self .enabled = False
704718 print ("TrainingMonitor: disabling live plot due to error: {}" .format (e ))
705719
720+ def _plot_1d (self , y_pred ):
721+ x = np .ravel (self .x_plot )
722+ self .ax_sol .cla ()
723+ self .ax_sol .plot (x , y_pred , "--r" , label = "Predicted" )
724+ if self .y_reference is not None :
725+ y_ref = np .ravel (self .y_reference (self .x_plot ))
726+ self .ax_sol .plot (x , y_ref , "-k" , label = "Reference" )
727+ self .ax_sol .set_xlabel ("x" )
728+ self .ax_sol .set_ylabel ("y" )
729+ self .ax_sol .set_title ("Epoch {}" .format (self .model .train_state .iteration ))
730+ self .ax_sol .legend ()
731+
732+ def _plot_2d (self , y_pred ):
733+ x0 , x1 = self .x_plot [:, 0 ], self .x_plot [:, 1 ]
734+ y_ref = None
735+ if self .y_reference is not None :
736+ y_ref = np .ravel (self .y_reference (self .x_plot ))
737+ vmin = min (y_pred .min (), y_ref .min ())
738+ vmax = max (y_pred .max (), y_ref .max ())
739+ else :
740+ vmin , vmax = y_pred .min (), y_pred .max ()
741+
742+ if self .cbar_sol is not None :
743+ self .cbar_sol .remove ()
744+ self .ax_sol .cla ()
745+ sc = self .ax_sol .scatter (x0 , x1 , c = y_pred , cmap = "jet" , vmin = vmin , vmax = vmax , s = 10 )
746+ self .ax_sol .set_xlabel ("x" )
747+ self .ax_sol .set_ylabel ("t" )
748+ self .ax_sol .set_title (
749+ "Predicted, epoch {}" .format (self .model .train_state .iteration )
750+ )
751+ self .cbar_sol = self .fig .colorbar (sc , ax = self .ax_sol )
752+
753+ if self .ax_ref is not None :
754+ if self .cbar_ref is not None :
755+ self .cbar_ref .remove ()
756+ self .ax_ref .cla ()
757+ sc_ref = self .ax_ref .scatter (
758+ x0 , x1 , c = y_ref , cmap = "jet" , vmin = vmin , vmax = vmax , s = 10
759+ )
760+ self .ax_ref .set_xlabel ("x" )
761+ self .ax_ref .set_ylabel ("t" )
762+ self .ax_ref .set_title ("Reference" )
763+ self .cbar_ref = self .fig .colorbar (sc_ref , ax = self .ax_ref )
764+
765+ def _plot_loss (self ):
766+ loss_history = self .model .losshistory
767+ loss_train = [np .sum (loss ) for loss in loss_history .loss_train ]
768+ loss_test = [np .sum (loss ) for loss in loss_history .loss_test ]
769+ self .ax_loss .cla ()
770+ self .ax_loss .semilogy (loss_history .steps , loss_train , label = "Train loss" )
771+ self .ax_loss .semilogy (loss_history .steps , loss_test , label = "Test loss" )
772+ self .ax_loss .set_xlabel ("# Steps" )
773+ self .ax_loss .set_ylabel ("Loss" )
774+ self .ax_loss .legend ()
775+
706776 def _redraw (self ):
707777 self .fig .tight_layout ()
708778 self .fig .canvas .draw ()
0 commit comments