from typing import Iterable, List, Optional, Tuple, Union
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import MaxNLocator
from ..objective import History
from ..result import Result
from .clust_color import assign_colors
from .misc import process_offset_y, process_result_list, process_y_limits
from .reference_points import ReferencePoint, create_references
[docs]def optimizer_history(results,
ax=None,
size=(18.5, 10.5),
trace_x='steps',
trace_y='fval',
scale_y='log10',
offset_y=None,
colors=None,
y_limits=None,
start_indices=None,
reference=None,
legends=None):
"""
Plot history of optimizer.
Can plot either the history of the cost function or of the gradient
norm, over either the optimizer steps or the computation time.
Parameters
----------
results: pypesto.Result or list
Optimization result obtained by 'optimize.py' or list of those
ax: matplotlib.Axes, optional
Axes object to use.
size: tuple, optional
Figure size (width, height) in inches. Is only applied when no ax
object is specified
trace_x: str, optional
What should be plotted on the x-axis?
Possibilities: 'time', 'steps'
Default: 'steps'
trace_y: str, optional
What should be plotted on the y-axis?
Possibilities: 'fval', 'gradnorm', 'stepsize'
Default: 'fval'
scale_y: str, optional
May be logarithmic or linear ('log10' or 'lin')
offset_y: float, optional
Offset for the y-axis-values, as these are plotted on a log10-scale
Will be computed automatically if necessary
colors: list, or RGBA, optional
list of colors, or single color
color or list of colors for plotting. If not set, clustering is done
and colors are assigned automatically
y_limits: float or ndarray, optional
maximum value to be plotted on the y-axis, or y-limits
start_indices: list or int
list of integers specifying the multistart to be plotted or
int specifying up to which start index should be plotted
reference: list, optional
List of reference points for optimization results, containing et
least a function value fval
legends: list or str
Labels for line plots, one label per result object
Returns
-------
ax: matplotlib.Axes
The plot axes.
"""
if isinstance(start_indices, int):
start_indices = list(range(start_indices))
# parse input
(results, colors, legends) = process_result_list(results, colors, legends)
for j, result in enumerate(results):
# extract cost function values from result
(x_label, y_label, vals) = get_trace(result, trace_x, trace_y)
# compute the necessary offset for the y-axis
(vals, offset_y, y_label) = get_vals(vals, scale_y, offset_y, y_label,
start_indices)
# call lowlevel plot routine
ax = optimizer_history_lowlevel(vals, scale_y=scale_y, ax=ax,
colors=colors[j],
size=size, x_label=x_label,
y_label=y_label,
legend_text=legends[j])
# parse and apply plotting options
ref = create_references(references=reference)
# handle options
ax = handle_options(ax, vals, ref, y_limits, offset_y)
return ax
[docs]def optimizer_history_lowlevel(vals, scale_y='log10', colors=None, ax=None,
size=(18.5, 10.5), x_label='Optimizer steps',
y_label='Objective value', legend_text=None):
"""
Plot optimizer history using list of numpy arrays.
Parameters
----------
vals: list of numpy arrays
list of 2xn-arrays (x_values and y_values of the trace)
scale_y: str, optional
May be logarithmic or linear ('log10' or 'lin')
colors: list, or RGBA, optional
list of colors, or single color
color or list of colors for plotting. If not set, clustering is done
and colors are assigned automatically
ax: matplotlib.Axes, optional
Axes object to use.
size: tuple, optional
see waterfall
x_label: str
label for x-axis
y_label: str
label for y-axis
legend_text: str
Label for line plots
Returns
-------
ax: matplotlib.Axes
The plot axes.
"""
# axes
if ax is None:
ax = plt.subplots()[1]
fig = plt.gcf()
fig.set_size_inches(*size)
# parse input
fvals = []
if isinstance(vals, list):
# convert entries to numpy arrays
for val in vals:
# val is already an numpy array
val = np.asarray(val)
fvals.append(val[1, -1])
else:
# convert to a list of numpy arrays
vals = np.asarray(vals)
if vals.shape[0] != 2 or vals.ndim != 2:
raise ValueError('If numpy array is passed directly to lowlevel '
'routine of optimizer_history, shape needs to '
'be 2 x n.')
fvals = [vals[1, -1]]
vals = [vals]
n_fvals = len(fvals)
# assign colors
# note: this has to happen before sorting
# to get the same colors in different plots
colors = assign_colors(fvals, colors)
# sort
indices = sorted(range(n_fvals), key=lambda j: fvals[j])
# plot
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
for j, val in enumerate(vals):
# collect and parse data
j_fval = indices[j]
color = colors[j_fval]
if j == 0:
tmp_legend = legend_text
else:
tmp_legend = None
# line plots
if scale_y == 'log10':
ax.semilogy(val[0, :], val[1, :], color=color, label=tmp_legend)
else:
ax.plot(val[0, :], val[1, :], color=color, label=tmp_legend)
# set labels
ax.set_xlabel(x_label)
ax.set_ylabel(y_label)
ax.set_title('Optimizer history')
if legend_text is not None:
ax.legend()
return ax
def get_trace(result: Result,
trace_x: Optional[str],
trace_y: Optional[str]) -> Tuple[str, str, List[np.ndarray]]:
"""
Get the values of the optimizer trace from the pypesto.Result object.
Parameters
----------
result: pypesto.Result
Optimization result obtained by 'optimize.py'.
trace_x: str, optional
What should be plotted on the x-axis?
Possibilities: 'time', 'steps'
Default: 'steps'
trace_y: str, optional
What should be plotted on the y-axis?
Possibilities: 'fval'(later also: 'gradnorm', 'stepsize')
Default: 'fval'
Returns
-------
vals:
list of (x,y)-values.
x_label:
label for x-axis to be plotted later.
y_label:
label for y-axis to be plotted later.
"""
# get data frames
histories: List[History] = result.optimize_result.get_for_key('history')
vals = []
x_label = ''
y_label = ''
for history in histories:
options = history.options
if trace_y == 'gradnorm':
# retrieve gradient trace, if saved
if not options.trace_record or not \
options.trace_record_grad:
raise ValueError("No gradient trace has been recorded.")
grads = history.get_grad_trace()
indices = [i for i, val in enumerate(grads)
if val is not None and np.isfinite(val).all()]
grads = np.array([grads[i] for i in indices])
# Get gradient trace, compute norm
y_vals = np.linalg.norm(grads, axis=1)
y_label = 'gradient norm'
else: # trace_y == 'fval':
if not options.trace_record:
raise ValueError("No function value trace has been recorded.")
fvals = history.get_fval_trace()
indices = [i for i, val in enumerate(fvals)
if val is not None and np.isfinite(val)]
y_vals = np.array([fvals[i] for i in indices])
y_label = 'objective value'
# retrieve values from dataframe
if trace_x == 'time':
times = np.array(history.get_time_trace())
x_vals = times[indices]
x_label = 'Computation time [s]'
else: # trace_x == 'steps':
x_vals = np.array(list(range(len(indices))))
x_label = 'Optimizer steps'
# write down values
vals.append(np.vstack([x_vals, y_vals]))
return x_label, y_label, vals
def get_vals(
vals: List[np.ndarray],
scale_y: Optional[str],
offset_y: float,
y_label: str,
start_indices: Iterable[int]
) -> Tuple[List[np.ndarray], float, str]:
"""
Postprocess the values of the optimization history.
Depending on the options set by the user (e.g. scale_y, offset_y,
start_indices).
Parameters
----------
vals: list
list of numpy arrays of dimension 2 x len(start_indices)
scale_y: str, optional
May be logarithmic or linear ('log10' or 'lin')
offset_y: float
offset for the y-axis, as this is supposed to be in log10-scale
y_label: str
Label for y axis
start_indices:
list of integers specifying the multi start indices to be plotted
Returns
-------
vals: list
list of numpy arrays
offset_y:
offset for the y-axis, if this is supposed to be in log10-scale
y_label:
Label for y axis
"""
# get list of indices
if start_indices is None:
start_indices = np.array(range(len(vals)))
else:
# check whether list or maximum value
start_indices = np.array(start_indices)
# check, whether index set is not too big
existing_indices = np.array(range(len(vals)))
start_indices = np.intersect1d(start_indices, existing_indices)
# reduce values to listed values
vals = [val for i, val in enumerate(vals) if i in start_indices]
# get the minimal value which should be plotted
min_val = np.inf
for val in vals:
tmp_min = np.min(val[1, :])
min_val = np.min([min_val, tmp_min])
# check, whether offset can be used with this data
if y_label == 'fval':
offset_y = process_offset_y(offset_y, scale_y, min_val)
else:
offset_y = 0.
if offset_y != 0:
for val in vals:
val[1, :] += offset_y * np.ones(val[1].shape)
y_label = 'offsetted ' + y_label
return vals, offset_y, y_label
def handle_options(ax: plt.Axes,
vals: List[np.ndarray],
ref: List[ReferencePoint],
y_limits: Union[float, np.ndarray],
offset_y: float):
"""
Apply post-plotting transformations to the axis object.
Get the limits for the y-axis, plots the reference points, will do
more at a later time point.
Parameters
----------
ref:
List of reference points for optimization results, containing et
least a function value fval
vals:
list of numpy arrays of size 2 x number of values
ax:
Axes object to use.
y_limits:
maximum value to be plotted on the y-axis, or y-limits
offset_y:
offset for the y-axis, if this is supposed to be in log10-scale
Returns
-------
ax: matplotlib.Axes
The plot axes.
"""
# handle y-limits
ax = process_y_limits(ax, y_limits)
# handle reference points
if len(ref) > 0:
# plot reference points
# get length of longest trajectory
max_len = 0
for val in vals:
max_len = np.max([max_len, val[0, -1]])
for i_ref in ref:
ax.plot([0, max_len],
[i_ref.fval + offset_y, i_ref.fval + offset_y],
'--', color=i_ref.color, label=i_ref.legend)
# create legend for reference points
if i_ref.legend is not None:
ax.legend()
return ax