mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-06 17:00:30 +08:00
469 lines
15 KiB
Python
469 lines
15 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import json
|
|
from typing import TYPE_CHECKING
|
|
from typing import Union
|
|
import warnings
|
|
|
|
import optuna
|
|
from optuna.version import __version__ as optuna_ver
|
|
from packaging import version
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Any
|
|
from typing import Literal
|
|
from typing import Optional
|
|
from typing import Sequence
|
|
from typing import TypedDict
|
|
|
|
ChoiceWidgetJSON = TypedDict(
|
|
"ChoiceWidgetJSON",
|
|
{
|
|
"type": Literal["choice"],
|
|
"description": Optional[str],
|
|
"choices": list[str],
|
|
"values": list[float],
|
|
"user_attr_key": Optional[str],
|
|
},
|
|
)
|
|
SliderWidgetLabel = TypedDict(
|
|
"SliderWidgetLabel",
|
|
{"value": float, "label": str},
|
|
)
|
|
SliderWidgetJSON = TypedDict(
|
|
"SliderWidgetJSON",
|
|
{
|
|
"type": Literal["slider"],
|
|
"description": Optional[str],
|
|
"min": float,
|
|
"max": float,
|
|
"step": Optional[float],
|
|
"labels": Optional[list[SliderWidgetLabel]],
|
|
"user_attr_key": Optional[str],
|
|
},
|
|
)
|
|
TextInputWidgetJSON = TypedDict(
|
|
"TextInputWidgetJSON",
|
|
{
|
|
"type": Literal["text"],
|
|
"description": Optional[str],
|
|
"user_attr_key": Optional[str],
|
|
"optional": bool,
|
|
},
|
|
)
|
|
UserAttrRefJSON = TypedDict("UserAttrRefJSON", {"type": Literal["user_attr"], "key": str})
|
|
FormWidgetJSON = TypedDict(
|
|
"FormWidgetJSON",
|
|
{
|
|
"output_type": Literal["objective", "user_attr"],
|
|
"widgets": Sequence[
|
|
Union[ChoiceWidgetJSON, SliderWidgetJSON, TextInputWidgetJSON, UserAttrRefJSON]
|
|
],
|
|
},
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ChoiceWidget:
|
|
"""A widget representing a choice with associated values.
|
|
|
|
Args:
|
|
choices: A list of strings representing the available choices.
|
|
values: A list of float values associated with each choice.
|
|
description: A description of the widget. Defaults to None.
|
|
user_attr_key: The key used by `register_user_attr_form_widgets`.
|
|
Form output is saved as `trial.user_attrs[user_attr_key]`. Defaults to None.
|
|
|
|
Example:
|
|
.. code-block:: python
|
|
|
|
from optuna_dashboard import ChoiceWidget
|
|
|
|
|
|
choice_widget = ChoiceWidget(
|
|
choices=["A", "B", "C"], values=[1.0, 2.0, 3.0], description="Choose one"
|
|
)
|
|
"""
|
|
|
|
choices: list[str]
|
|
values: list[float]
|
|
description: Optional[str] = None
|
|
user_attr_key: Optional[str] = None
|
|
|
|
def to_dict(self) -> ChoiceWidgetJSON:
|
|
"""Convert the ChoiceWidget object to a dictionary.
|
|
|
|
Returns:
|
|
ChoiceWidgetJSON: A dictionary representing the ChoiceWidget object.
|
|
"""
|
|
return {
|
|
"type": "choice",
|
|
"description": self.description,
|
|
"choices": self.choices,
|
|
"values": self.values,
|
|
"user_attr_key": self.user_attr_key,
|
|
}
|
|
|
|
@classmethod
|
|
def _from_dict(cls, d: dict[str, Any]) -> ChoiceWidget:
|
|
assert d.get("type") == "choice"
|
|
return cls(
|
|
description=d.get("description"),
|
|
choices=d["choices"],
|
|
values=d["values"],
|
|
user_attr_key=d.get("user_attr_key"),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class SliderWidget:
|
|
"""A widget representing a slider for selecting a value within a range.
|
|
|
|
Args:
|
|
min: The minimum value of the slider.
|
|
max: The maximum value of the slider.
|
|
step: The step size for the slider. Defaults to None.
|
|
labels: A list of tuples containing value and label for the slider. Defaults to None.
|
|
description: A description for the slider. Defaults to None.
|
|
user_attr_key: The key used by `register_user_attr_form_widgets`.
|
|
Form output is saved as `trial.user_attrs[user_attr_key]`. Defaults to None.
|
|
|
|
Example:
|
|
.. code-block:: python
|
|
|
|
from optuna_dashboard import SliderWidget
|
|
|
|
|
|
slide_widget = SliderWidget(min=0, max=10, step=1, description="Example slider")
|
|
"""
|
|
|
|
min: float
|
|
max: float
|
|
step: Optional[float] = None
|
|
labels: Optional[list[tuple[float, str]]] = None
|
|
description: Optional[str] = None
|
|
user_attr_key: Optional[str] = None
|
|
|
|
def to_dict(self) -> SliderWidgetJSON:
|
|
"""Convert the SliderWidget instance to a dictionary.
|
|
|
|
Returns:
|
|
SliderWidgetJSON: A dictionary representation of the SliderWidget instance.
|
|
"""
|
|
labels: Optional[list[SliderWidgetLabel]] = None
|
|
if self.labels is not None:
|
|
labels = [{"value": value, "label": label} for value, label in self.labels]
|
|
return {
|
|
"type": "slider",
|
|
"description": self.description,
|
|
"min": self.min,
|
|
"max": self.max,
|
|
"step": self.step,
|
|
"labels": labels,
|
|
"user_attr_key": self.user_attr_key,
|
|
}
|
|
|
|
@classmethod
|
|
def _from_dict(cls, d: dict[str, Any]) -> SliderWidget:
|
|
assert d.get("type") == "slider"
|
|
labels = d.get("labels")
|
|
if labels is not None:
|
|
labels = [(label["value"], label["label"]) for label in labels]
|
|
return cls(
|
|
description=d.get("description"),
|
|
min=d["min"],
|
|
max=d["max"],
|
|
step=d.get("step"),
|
|
labels=labels,
|
|
user_attr_key=d.get("user_attr_key"),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TextInputWidget:
|
|
"""
|
|
A text input widget class that defines a text input field.
|
|
|
|
Args:
|
|
description: A description of the text input field.
|
|
user_attr_key: The key used by `register_user_attr_form_widgets`.
|
|
Form output is saved as `trial.user_attrs[user_attr_key]`. Defaults to None.
|
|
optional: If True, an empty string is acceptable.
|
|
|
|
Example:
|
|
.. code-block:: python
|
|
|
|
from optuna_dashboard import TextInputWidget
|
|
|
|
|
|
text_input = TextInputWidget(description="Text Input Example")
|
|
"""
|
|
|
|
description: Optional[str] = None
|
|
user_attr_key: Optional[str] = None
|
|
optional: bool = False
|
|
|
|
def to_dict(self) -> TextInputWidgetJSON:
|
|
"""
|
|
Converts the TextInputWidget instance to a dictionary representation.
|
|
|
|
Returns:
|
|
TextInputWidgetJSON: The dictionary representation of the TextInputWidget instance.
|
|
"""
|
|
return {
|
|
"type": "text",
|
|
"description": self.description,
|
|
"user_attr_key": self.user_attr_key,
|
|
"optional": self.optional,
|
|
}
|
|
|
|
@classmethod
|
|
def _from_dict(cls, d: dict[str, Any]) -> TextInputWidget:
|
|
assert d.get("type") == "text"
|
|
return cls(
|
|
description=d.get("description"),
|
|
user_attr_key=d.get("user_attr_key"),
|
|
optional=d.get("optional", False),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ObjectiveUserAttrRef:
|
|
"""
|
|
A class representing a reference to a value of `trial.user_attrs`.
|
|
When combined with `register_objective_form_widgets`, users can tell values that are
|
|
registered to `trial.user_attrs` during the human-in-the-loop optimization.
|
|
|
|
Args:
|
|
key: The key of `trial.user_attrs` being referenced.
|
|
|
|
Example:
|
|
.. code-block:: python
|
|
|
|
from optuna_dashboard import ObjectiveUserAttrRef
|
|
|
|
|
|
user_attr_ref = ObjectiveUserAttrRef(key="key")
|
|
"""
|
|
|
|
key: str
|
|
|
|
def to_dict(self) -> UserAttrRefJSON:
|
|
"""
|
|
Converts the ObjectiveUserAttrRef instance to a dictionary representation.
|
|
|
|
Returns:
|
|
UserAttrRefJSON: The dictionary representation of the ObjectiveUserAttrRef instance.
|
|
"""
|
|
return {
|
|
"type": "user_attr",
|
|
"key": self.key,
|
|
}
|
|
|
|
@classmethod
|
|
def _from_dict(cls, d: dict[str, Any]) -> ObjectiveUserAttrRef:
|
|
assert d.get("type") == "user_attr"
|
|
return cls(
|
|
key=d["key"],
|
|
)
|
|
|
|
|
|
ObjectiveFormWidget = Union[ChoiceWidget, SliderWidget, TextInputWidget, ObjectiveUserAttrRef]
|
|
# For backward compatibility.
|
|
ObjectiveChoiceWidget = ChoiceWidget
|
|
ObjectiveSliderWidget = SliderWidget
|
|
ObjectiveTextInputWidget = TextInputWidget
|
|
FORM_WIDGETS_KEY = "dashboard:form_widgets:v2"
|
|
|
|
|
|
def dict_to_form_widget(d: dict[str, Any]) -> ObjectiveFormWidget:
|
|
"""Restore form widget objects from the dictionary.
|
|
|
|
Args:
|
|
d: A dictionary object.
|
|
|
|
Returns:
|
|
object: an instance of the restored form widget class.
|
|
"""
|
|
widget_type = d.get("type", None)
|
|
if widget_type == "choice":
|
|
return ChoiceWidget._from_dict(d)
|
|
elif widget_type == "slider":
|
|
return SliderWidget._from_dict(d)
|
|
elif widget_type == "text":
|
|
return TextInputWidget._from_dict(d)
|
|
elif widget_type == "user_attr":
|
|
return ObjectiveUserAttrRef._from_dict(d)
|
|
raise ValueError("Unexpected widget type")
|
|
|
|
|
|
def register_objective_form_widgets(
|
|
study: optuna.Study, widgets: list[ObjectiveFormWidget]
|
|
) -> None:
|
|
"""
|
|
Register a list of form widgets to an Optuna study.
|
|
|
|
Submitted values to the forms are told as each trial's objective values.
|
|
|
|
Args:
|
|
study: The Optuna study object to register the form widgets for.
|
|
widgets: A list of ObjectiveFormWidget objects to be registered in the study.
|
|
|
|
Raises:
|
|
ValueError: If the length of study directions is not equal to the length of widgets.
|
|
Warning: If any widget has `user_attr_key` specified, but it will not be used.
|
|
|
|
Examples:
|
|
.. code-block:: python
|
|
|
|
import optuna
|
|
from optuna_dashboard import ChoiceWidget, SliderWidget
|
|
from optuna_dashboard import register_objective_form_widgets
|
|
|
|
|
|
study = optuna.create_study()
|
|
register_objective_form_widgets(
|
|
study,
|
|
widgets=[
|
|
ObjectiveChoiceWidget(
|
|
choices=["Good 👍", "Bad 👎"],
|
|
values=[-1, 1],
|
|
description="Please input your score!",
|
|
),
|
|
ObjectiveSliderWidget(
|
|
min=1,
|
|
max=10,
|
|
step=1,
|
|
description="Higher is better.",
|
|
),
|
|
],
|
|
)
|
|
"""
|
|
if version.parse(optuna_ver) < version.Version("3.2") and isinstance(
|
|
study._storage, optuna.storages._CachedStorage
|
|
):
|
|
warnings.warn(
|
|
"Human-in-the-loop optimization will not work with _CachedStorage in Optuna prior"
|
|
" to v3.2. See https://optuna-dashboard.readthedocs.io/en/latest/errors.html"
|
|
" for details."
|
|
)
|
|
|
|
if len(study.directions) != len(widgets):
|
|
raise ValueError("The length of actions must be the same with the number of objectives.")
|
|
if any(
|
|
not isinstance(w, ObjectiveUserAttrRef) and w.user_attr_key is not None for w in widgets
|
|
):
|
|
warnings.warn("`user_attr_key` specified, but it will not be used.")
|
|
if any(isinstance(w, TextInputWidget) and w.optional for w in widgets):
|
|
raise ValueError("TextInputWidget.optional must be False.")
|
|
form_widgets: FormWidgetJSON = {
|
|
"output_type": "objective",
|
|
"widgets": [w.to_dict() for w in widgets],
|
|
}
|
|
study._storage.set_study_system_attr(
|
|
study._study_id,
|
|
FORM_WIDGETS_KEY,
|
|
# TODO(c-bata): Remove type: ignore comment
|
|
form_widgets, # type: ignore
|
|
)
|
|
|
|
|
|
def register_user_attr_form_widgets(
|
|
study: optuna.Study, widgets: list[ObjectiveFormWidget]
|
|
) -> None:
|
|
"""
|
|
Register a list of form widgets to an Optuna study.
|
|
|
|
Submitted values to the forms are registered as each trial's user_attrs.
|
|
|
|
Args:
|
|
study: The Optuna study object to register the form widgets for.
|
|
widgets: A list of ObjectiveFormWidget objects to be registered in the study.
|
|
|
|
Raises:
|
|
ValueError: If an ObjectiveUserAttrRef is specified or if `user_attr_key` is not specified.
|
|
ValueError: If `user_attr_key` is not unique for each widget.
|
|
|
|
Examples:
|
|
.. code-block:: python
|
|
|
|
import optuna
|
|
from optuna_dashboard import ChoiceWidget, SliderWidget
|
|
from optuna_dashboard import register_user_attr_form_widgets
|
|
|
|
|
|
study = optuna.create_study()
|
|
register_user_attr_form_widgets(
|
|
study,
|
|
widgets=[
|
|
ChoiceWidget(
|
|
choices=["Good 👍", "Bad 👎"],
|
|
values=[-1, 1],
|
|
description="Please input your score!",
|
|
user_attr_key="hitl/choice",
|
|
),
|
|
SliderWidget(
|
|
min=1,
|
|
max=10,
|
|
step=1,
|
|
description="Higher is better.",
|
|
user_attr_key="hitl/slider",
|
|
),
|
|
],
|
|
)
|
|
"""
|
|
if version.parse(optuna_ver) < version.Version("3.2") and isinstance(
|
|
study._storage, optuna.storages._CachedStorage
|
|
):
|
|
warnings.warn(
|
|
"Human-in-the-loop optimization will not work with _CachedStorage in Optuna prior"
|
|
" to v3.2. See https://optuna-dashboard.readthedocs.io/en/latest/errors.html"
|
|
" for details."
|
|
)
|
|
|
|
user_attr_keys = set()
|
|
widget_dicts: list[Union[ChoiceWidgetJSON, SliderWidgetJSON, TextInputWidgetJSON]] = []
|
|
for w in widgets:
|
|
if isinstance(w, ObjectiveUserAttrRef):
|
|
raise ValueError("ObjectiveUserAttrRef can't be specified.")
|
|
if w.user_attr_key is None:
|
|
raise ValueError("`user_attr_key` is not specified.")
|
|
user_attr_keys.add(w.user_attr_key)
|
|
widget_dicts.append(w.to_dict())
|
|
|
|
if len(widget_dicts) != len(user_attr_keys):
|
|
raise ValueError("`user_attr_key` must be unique for each widget.")
|
|
|
|
form_widgets: FormWidgetJSON = {
|
|
"output_type": "user_attr",
|
|
"widgets": widget_dicts,
|
|
}
|
|
study._storage.set_study_system_attr(
|
|
study._study_id,
|
|
FORM_WIDGETS_KEY,
|
|
# TODO(c-bata): Remove type: ignore comment
|
|
form_widgets, # type: ignore
|
|
)
|
|
|
|
|
|
def get_form_widgets_json(study_system_attr: dict[str, Any]) -> Optional[FormWidgetJSON]:
|
|
if FORM_WIDGETS_KEY in study_system_attr:
|
|
return study_system_attr[FORM_WIDGETS_KEY]
|
|
|
|
# For optuna-dashboard v0.9.0 and v0.9.0b6 users
|
|
if "dashboard:objective_form_widgets:v1" in study_system_attr:
|
|
return {
|
|
"output_type": "objective",
|
|
"widgets": study_system_attr["dashboard:objective_form_widgets:v1"],
|
|
}
|
|
|
|
# For optuna-dashboard v0.9.0b5 users
|
|
if "dashboard:objective_form_widgets" in study_system_attr:
|
|
return {
|
|
"output_type": "objective",
|
|
"widgets": json.loads(study_system_attr["dashboard:objective_form_widgets"]),
|
|
}
|
|
return None
|