from collections.abc import Mapping, Sequence
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import keras.src.callbacks
from ...utils.plot_utils import make_figure, add_titles_and_labels
[docs]
def loss(
history: keras.callbacks.History,
train_key: str = "loss",
val_key: str = "val_loss",
show_components: bool = False,
component_names: Mapping[str, str] = None,
smoothing_factor: float = 0.8,
figsize: Sequence[float] = None,
train_color: str = "#132a70",
val_color: str = "black",
val_marker: str = "o",
val_marker_size: float = 5,
lw_train: float = 2.0,
lw_val: float = 2.0,
grid_alpha: float = 0.2,
legend_fontsize: int = 14,
label_fontsize: int = 14,
title_fontsize: int = 16,
) -> plt.Figure:
"""
Plot the training (and validation) loss of a series of training epochs and runs.
Parameters
----------
history : keras.src.callbacks.History
History object as returned by `keras.Model.fit`.
train_key : str, optional, default: "loss"
The training loss key to look for in the history
val_key : str, optional, default: "val_loss"
The validation loss key to look for in the history
show_components : bool, optional, default: False
If True, every other metric in the history (e.g., regularization losses) is
plotted in its own panel below the total loss, with its validation counterpart
(``"val_"`` prefix) overlaid if present.
component_names : dict, optional, default: None
Maps history keys to panel labels, e.g. ``{"layer_loss": "Regularization"}``.
Keys not in the mapping are labeled by their title-cased key.
smoothing_factor : float, optional, default: 0.8
If greater than zero, smooth the loss curves by applying an exponential moving average.
figsize : tuple or None, optional, default: None
The figure size passed to the ``matplotlib`` constructor.
Inferred if ``None``
train_color : str, optional, default: '#132a70'
The color for the train loss trajectory
val_color : str, optional, default: None
The color for the optional validation loss trajectory
val_marker: str
Marker style for the validation loss curve. Default is "o".
val_marker_size: float
Marker size for the validation loss curve. Default is 5.
lw_train : int, optional, default: 2
The line width for the training loss curve
lw_val : int, optional, default: 2
The line width for the validation loss curve
grid_alpha : float, optional, default: 0.2
The transparency of the background grid
legend_fontsize : int, optional, default: 14
The font size of the legend text
label_fontsize : int, optional, default: 14
The font size of the y-label text
title_fontsize : int, optional, default: 16
The font size of the title text
Returns
-------
f : plt.Figure - the figure instance for optional saving
"""
keys = [train_key]
ylabels = ["Loss"]
if show_components:
component_names = component_names or {}
components = [k for k in history.history if k != train_key and not k.startswith("val_")]
keys += components
ylabels = ["Total Loss"] + [component_names.get(k, k.replace("_", " ").title()) for k in components]
train_losses = []
val_losses = []
for key in keys:
train_losses.append(_first_column(history.history[key]))
val_key_ = val_key if key == train_key else f"val_{key}"
val = history.history.get(val_key_)
val_losses.append(_first_column(val) if val is not None else None)
has_val = any(v is not None for v in val_losses) and val_color is not None
num_row = len(keys)
fig, axes = make_figure(num_row=num_row, num_col=1, figsize=(16, int(4 * num_row)) if figsize is None else figsize)
for ax, train, val, ylabel in zip(axes.flat, train_losses, val_losses, ylabels):
ax.set_ylabel(ylabel, fontsize=label_fontsize)
train_step_index = np.arange(1, len(train) + 1)
if smoothing_factor > 0:
ax.plot(train_step_index, train, color=train_color, lw=lw_train, alpha=0.3, label="Training")
smoothed_train = train.ewm(alpha=1.0 - smoothing_factor, adjust=True).mean()
ax.plot(
train_step_index,
smoothed_train,
color=train_color,
lw=lw_train,
alpha=0.8,
label="Training (Moving Average)",
)
else:
ax.plot(train_step_index, train, color=train_color, lw=lw_train, alpha=0.8, label="Training")
if val is not None and val_color is not None:
val_step = int(np.floor(len(train) / len(val)))
val_step_index = train_step_index[(val_step - 1) :: val_step][: len(val)]
ax.plot(
val_step_index,
val,
color=val_color,
lw=lw_val,
alpha=0.3 if smoothing_factor > 0 else 0.8,
linestyle="--",
marker=val_marker,
markersize=val_marker_size,
label="Validation",
)
if smoothing_factor > 0:
smoothed_val = val.ewm(alpha=1.0 - smoothing_factor, adjust=True).mean()
ax.plot(
val_step_index,
smoothed_val,
color=val_color,
linestyle="--",
lw=lw_val,
alpha=0.8,
label="Validation (Moving Average)",
)
sns.despine(ax=ax)
ax.grid(alpha=grid_alpha)
ax.set_xlim(train_step_index[0], train_step_index[-1])
# single legend below the figure, only if there's at least one validation curve or smoothing was on
legend = None
if has_val or smoothing_factor > 0:
handles, labels = axes.flat[0].get_legend_handles_labels()
legend = fig.legend(
handles,
labels,
loc="lower center",
ncol=min(len(labels), 2) if fig.get_figwidth() < 12 else len(labels),
frameon=True,
fontsize=legend_fontsize,
)
add_titles_and_labels(
axes=axes,
num_row=num_row,
num_col=1,
title=["Loss Trajectories" if num_row > 1 else "Loss Trajectory"],
xlabel="Training epoch #",
title_fontsize=title_fontsize,
label_fontsize=label_fontsize,
)
if legend is None:
fig.tight_layout()
else:
# reserve exactly the legend's height (plus a small gap) below the panels
fig.canvas.draw()
legend_height = legend.get_window_extent().transformed(fig.transFigure.inverted()).height
fig.tight_layout(rect=(0, legend_height + 0.03, 1, 1))
return fig
def _first_column(values) -> pd.Series:
values = np.asarray(values)
return pd.Series(values[:, 0] if values.ndim > 1 else values)