Skip to content

Commit

Permalink
Merge pull request optuna#4748 from y0z/feature/plot_hypervolume
Browse files Browse the repository at this point in the history
Implement hypervolume history plot for matplotlib backend.
  • Loading branch information
HideakiImamura committed Jun 22, 2023
2 parents 2b6addc + 593f557 commit 67d976e
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/source/reference/visualization/matplotlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ optuna.visualization.matplotlib

optuna.visualization.matplotlib.plot_contour
optuna.visualization.matplotlib.plot_edf
optuna.visualization.matplotlib.plot_hypervolume_history
optuna.visualization.matplotlib.plot_intermediate_values
optuna.visualization.matplotlib.plot_optimization_history
optuna.visualization.matplotlib.plot_parallel_coordinate
Expand Down
2 changes: 2 additions & 0 deletions optuna/visualization/matplotlib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from optuna.visualization.matplotlib._contour import plot_contour
from optuna.visualization.matplotlib._edf import plot_edf
from optuna.visualization.matplotlib._hypervolume_history import plot_hypervolume_history
from optuna.visualization.matplotlib._intermediate_values import plot_intermediate_values
from optuna.visualization.matplotlib._optimization_history import plot_optimization_history
from optuna.visualization.matplotlib._parallel_coordinate import plot_parallel_coordinate
Expand All @@ -17,6 +18,7 @@
"plot_contour",
"plot_edf",
"plot_intermediate_values",
"plot_hypervolume_history",
"plot_optimization_history",
"plot_parallel_coordinate",
"plot_param_importances",
Expand Down
167 changes: 167 additions & 0 deletions optuna/visualization/matplotlib/_hypervolume_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from __future__ import annotations

from typing import NamedTuple
from typing import Sequence

import numpy as np

from optuna._experimental import experimental_func
from optuna._hypervolume import WFG
from optuna.logging import get_logger
from optuna.samplers._base import _CONSTRAINTS_KEY
from optuna.study import Study
from optuna.study._multi_objective import _get_pareto_front_trials_by_trials
from optuna.study._study_direction import StudyDirection
from optuna.trial import TrialState
from optuna.visualization.matplotlib._matplotlib_imports import _imports


if _imports.is_successful():
from optuna.visualization.matplotlib._matplotlib_imports import Axes
from optuna.visualization.matplotlib._matplotlib_imports import plt

_logger = get_logger(__name__)


class _HypervolumeHistoryInfo(NamedTuple):
trial_numbers: list[int]
values: list[float]


@experimental_func("3.3.0")
def plot_hypervolume_history(
study: Study,
reference_point: Sequence[float],
) -> "Axes":
"""Plot hypervolume history of all trials in a study with Matplotlib.
Example:
The following code snippet shows how to plot optimization history.
.. plot::
import optuna
import matplotlib.pyplot as plt
def objective(trial):
x = trial.suggest_float("x", 0, 5)
y = trial.suggest_float("y", 0, 3)
v0 = 4 * x ** 2 + 4 * y ** 2
v1 = (x - 5) ** 2 + (y - 5) ** 2
return v0, v1
study = optuna.create_study(directions=["minimize", "minimize"])
study.optimize(objective, n_trials=50)
reference_point=[100, 50]
optuna.visualization.matplotlib.plot_hypervolume_history(study, reference_point)
plt.tight_layout()
.. note::
You need to adjust the size of the plot by yourself using ``plt.tight_layout()`` or
``plt.savefig(IMAGE_NAME, bbox_inches='tight')``.
Args:
study:
A :class:`~optuna.study.Study` object whose trials are plotted for their hypervolumes.
``study.n_objectives`` must be 2 or more.
reference_point:
A reference point to use for hypervolume computation.
The dimension of the reference point must be the same as the number of objectives.
Returns:
A :class:`matplotlib.axes.Axes` object.
"""

_imports.check()

if not study._is_multi_objective():
raise ValueError(
"Study must be multi-objective. For single-objective optimization, "
"please use plot_optimization_history instead."
)

if len(reference_point) != len(study.directions):
raise ValueError(
"The dimension of the reference point must be the same as the number of objectives."
)

info = _get_hypervolume_history_info(study, np.asarray(reference_point, dtype=np.float64))
return _get_hypervolume_history_plot(info)


def _get_hypervolume_history_plot(
info: _HypervolumeHistoryInfo,
) -> "Axes":
# Set up the graph style.
plt.style.use("ggplot") # Use ggplot style sheet for similar outputs to plotly.
_, ax = plt.subplots()
ax.set_title("Hypervolume History Plot")
ax.set_xlabel("Trial")
ax.set_ylabel("Hypervolume")
cmap = plt.get_cmap("tab10") # Use tab10 colormap for similar outputs to plotly.

ax.plot(
info.trial_numbers,
info.values,
marker="o",
color=cmap(0),
alpha=0.5,
)
return ax


def _get_hypervolume_history_info(
study: Study,
reference_point: np.ndarray,
) -> _HypervolumeHistoryInfo:
completed_trials = study.get_trials(deepcopy=False, states=(TrialState.COMPLETE,))

if len(completed_trials) == 0:
_logger.warning("Your study does not have any completed trials.")

# Only feasible trials are considered in hypervolume computation.
trial_numbers = []
best_trials_history = []
feasible_trials = []
for trial in completed_trials:
has_constraints = _CONSTRAINTS_KEY in trial.system_attrs
if has_constraints:
constraints_values = trial.system_attrs[_CONSTRAINTS_KEY]
if all(map(lambda x: x <= 0.0, constraints_values)):
feasible_trials.append(trial)
else:
feasible_trials.append(trial)

trial_numbers.append(trial.number)
best_trials_history.append(
_get_pareto_front_trials_by_trials(feasible_trials, study.directions)
)

if len(feasible_trials) == 0:
_logger.warning("Your study does not have any feasible trials.")

# Our hypervolume computation module assumes that all objectives are minimized.
# Here we transform the objective values and the reference point.
signs = np.asarray([1 if d == StudyDirection.MINIMIZE else -1 for d in study.directions])
minimization_reference_point = signs * reference_point
values = []
for best_trials in best_trials_history:
solution_set = np.asarray(
list(
filter(
lambda v: (v <= minimization_reference_point).all(),
[signs * trial.values for trial in best_trials],
)
)
)
hypervolume = 0.0
if solution_set.size > 0:
hypervolume = WFG().compute(solution_set, minimization_reference_point)
values.append(hypervolume)
return _HypervolumeHistoryInfo(trial_numbers, values)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from typing import Sequence

import numpy as np
import pytest

from optuna.samplers import NSGAIISampler
from optuna.study import create_study
from optuna.trial import FrozenTrial
from optuna.trial import Trial
from optuna.visualization.matplotlib._hypervolume_history import _get_hypervolume_history_info
from optuna.visualization.matplotlib._hypervolume_history import _HypervolumeHistoryInfo


@pytest.mark.parametrize(
"directions",
[
["minimize", "minimize"],
["minimize", "maximize"],
["maximize", "minimize"],
["maximize", "maximize"],
],
)
def test_get_optimization_history_info(directions: str) -> None:
signs = [1 if d == "minimize" else -1 for d in directions]

def objective(trial: Trial) -> Sequence[float]:
def impl(trial: Trial) -> Sequence[float]:
if trial.number == 0:
return 1.5, 1.5 # dominated by the reference_point
elif trial.number == 1:
return 0.75, 0.75
elif trial.number == 2:
return 0.5, 0.5 # dominates Trial #1
elif trial.number == 3:
return 0.5, 0.5 # dominates Trial #1
elif trial.number == 4:
return 0.75, 0.25 # incomparable
return 0.0, 0.0 # dominates all

values = impl(trial)
return signs[0] * values[0], signs[1] * values[1]

def constraints(trial: FrozenTrial) -> Sequence[float]:
if trial.number == 2:
return (1,) # infeasible

return (0,) # feasible

sampler = NSGAIISampler(constraints_func=constraints)
study = create_study(directions=directions, sampler=sampler)
study.optimize(objective, n_trials=6)

reference_point = np.asarray(signs)
info = _get_hypervolume_history_info(study, reference_point)

assert info == _HypervolumeHistoryInfo(
trial_numbers=[0, 1, 2, 3, 4, 5], values=[0, 0.0625, 0.0625, 0.25, 0.3125, 1.0]
)

0 comments on commit 67d976e

Please sign in to comment.