diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 821fe15f..bb820455 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -24,6 +24,6 @@ jobs: days-before-pr-close: 7 # default number stale-issue-label: 'stale' stale-pr-label: 'stale' - exempt-issue-labels: 'no-stale' + exempt-issue-labels: 'no-stale,good-first-issue,contribution-welcome' exempt-pr-labels: 'no-stale' operations-per-run: 1000 diff --git a/optuna_dashboard/preferential/_study.py b/optuna_dashboard/preferential/_study.py index 6e093683..d715b97b 100644 --- a/optuna_dashboard/preferential/_study.py +++ b/optuna_dashboard/preferential/_study.py @@ -37,6 +37,10 @@ class PreferentialStudy: To create and load a study, please refer to the documentation of :func:`~optuna_dashboard.preferential.create_study` and :func:`~optuna_dashboard.preferential.load_study` respectively. + + .. note:: + Preferential optimization is an experimental feature (introduced in v0.13.0). + The interface may change in newer versions without prior notice. """ def __init__(self, study: optuna.Study) -> None: @@ -359,6 +363,10 @@ def create_study( Returns: A :class:`~optuna_dashboard.preferential.PreferentialStudy` object. + + .. note:: + Preferential optimization is an experimental feature (introduced in v0.13.0). + The interface may change in newer versions without prior notice. """ try: study = optuna.create_study( @@ -441,6 +449,10 @@ def load_study( Returns: A :class:`~optuna_dashboard.preferential.PreferentialStudy` object. + + .. note:: + Preferential optimization is an experimental feature (introduced in v0.13.0). + The interface may change in newer versions without prior notice. """ study = optuna.load_study( study_name=study_name, storage=storage, sampler=sampler or RandomSampler() diff --git a/optuna_dashboard/preferential/samplers/gp.py b/optuna_dashboard/preferential/samplers/gp.py index d023e741..2e782b68 100644 --- a/optuna_dashboard/preferential/samplers/gp.py +++ b/optuna_dashboard/preferential/samplers/gp.py @@ -4,7 +4,7 @@ import itertools import math from typing import Any from typing import Callable -from typing import cast +import warnings import botorch.acquisition.analytic import botorch.models.model @@ -17,6 +17,8 @@ import numpy as np import optuna import optuna._transform from optuna.distributions import CategoricalDistribution +from optuna.distributions import FloatDistribution +from optuna.distributions import IntDistribution import torch from torch import Tensor @@ -156,6 +158,18 @@ def _truncnorm_mean_var_logz(alpha: Tensor) -> tuple[Tensor, Tensor, Tensor]: return mean, var, logz +def _observation(var0: Tensor, mean0: Tensor, noise_var: Tensor) -> tuple[Tensor, Tensor, Tensor]: + obs_var = var0 + noise_var + obs_sigma = torch.sqrt(obs_var) + alpha = -mean0 / torch.clamp_min(obs_sigma, min=1e-20) + mean_norm, var_norm, logz = _truncnorm_mean_var_logz(alpha) + + denom_factor = 1 / torch.clamp_min(noise_var + var_norm * var0, min=1e-20) + da = (1 - var_norm) * denom_factor + db = (mean0 * (1 - var_norm) + obs_sigma * mean_norm) * denom_factor + return (da, db, logz) + + def _orthants_MVN_EP( cov0: Tensor, preferences: Tensor, noise_var: Tensor, cycles: int ) -> tuple[Tensor, Tensor, Tensor]: @@ -176,25 +190,17 @@ def _orthants_MVN_EP( r0 = (1 - var1 * virtual_obs_a[i]).reciprocal() var0 = var1 * r0 - mean0 = (mean1 + var1 * virtual_obs_b[i]) * r0 + mean0 = (mean1 - var1 * virtual_obs_b[i]) * r0 - obs_var = var0 + noise_var - obs_sigma = torch.sqrt(obs_var) - alpha = -mean0 / torch.clamp_min(obs_sigma, min=1e-20) - mean_norm, var_norm, logz = _truncnorm_mean_var_logz(alpha) + virtual_obs_a2, virtual_obs_b2, logz = _observation(var0, mean0, noise_var) - kalman_factor = var0 / torch.clamp_min(obs_var, min=1e-20) - mean2 = mean0 + obs_sigma * mean_norm * kalman_factor - var2 = kalman_factor * (noise_var + var_norm * var0) - - var1_var2_inv = torch.clamp_min(var1 * var2, min=1e-20).reciprocal() - db = (mean1 * var2 - mean2 * var1) * var1_var2_inv - da = (var1 - var2) * var1_var2_inv - virtual_obs_b[i] = virtual_obs_b[i] + db - virtual_obs_a[i] = virtual_obs_a[i] + da + da = virtual_obs_a2 - virtual_obs_a[i] + db = virtual_obs_b2 - virtual_obs_b[i] + virtual_obs_a[i] = virtual_obs_a2 + virtual_obs_b[i] = virtual_obs_b2 dr = (1 + var1 * da).reciprocal() - mu = mu - Sxy * ((db + mean1 * da) * dr) + mu = mu + Sxy * ((db - mean1 * da) * dr) cov = cov - (Sxy[:, None] * (da * dr)) @ Sxy[None, :] log_zs[i] = logz return mu, cov, torch.sum(log_zs) @@ -359,11 +365,54 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler): ) # TODO: Make it possible to apply it on mixed search space - if all(isinstance(dist, CategoricalDistribution) for dist in search_space.values()): + def get_all_possible_params(dist: optuna.distributions.BaseDistribution) -> list[Any]: + if isinstance(dist, CategoricalDistribution): + return list(dist.choices) + elif isinstance(dist, (IntDistribution, FloatDistribution)): + return list(np.arange(dist.low, dist.high, dist.step)) + else: + return [] + + all_possible_params = { + name: get_all_possible_params(dist) for name, dist in search_space.items() + } + + is_all_discrete = all( + len(possible_params) > 0 for possible_params in all_possible_params.values() + ) + search_space_size = np.prod( + [len(possible_params) for possible_params in all_possible_params.values()] + ) + # TODO(contramundum53): Fix this arbitrarily chosen limit. + size_limit = 1e6 + can_evaluate_all = is_all_discrete and search_space_size <= size_limit + + if ( + any(isinstance(dist, CategoricalDistribution) for dist in search_space.values()) + and not can_evaluate_all + ): + if is_all_discrete: + warnings.warn( + "The objective function has categorical parameters, " + "but the total search space is too large to be enumerated. " + f"(Search space size: {search_space_size} > limit: {size_limit})" + "This may result in significantly bad performance." + ) + else: + warnings.warn( + "The objective function has categorical parameters, " + "but the search space cannot be enumerated because " + "it also contains continuous parameters. " + "This may result in significantly bad performance. " + "You can work around this problem by specifying 'step' " + "in each continuous parameter." + ) + + if is_all_discrete and can_evaluate_all: all_param_combinations = itertools.product( *[ - [(name, choice) for choice in cast(CategoricalDistribution, dist).choices] - for name, dist in search_space.items() + [(name, choice) for choice in possible_params] + for name, possible_params in all_possible_params.items() ] ) choices = torch.tensor( @@ -395,6 +444,11 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler): param_name: str, param_distribution: optuna.distributions.BaseDistribution, ) -> Any: + warnings.warn( + "Dynamic search space detected. " + f"Falling back to {self.independent_sampler.__class__.__name__}." + ) + return self.independent_sampler.sample_independent( study, trial, param_name, param_distribution ) diff --git a/optuna_dashboard/ts/components/AppDrawer.tsx b/optuna_dashboard/ts/components/AppDrawer.tsx index 8740c68a..31eeec69 100644 --- a/optuna_dashboard/ts/components/AppDrawer.tsx +++ b/optuna_dashboard/ts/components/AppDrawer.tsx @@ -17,7 +17,7 @@ import ListItemText from "@mui/material/ListItemText" import { drawerOpenState, reloadIntervalState, - useStudyIsPreferencial, + useStudyIsPreferential, } from "../state" import { Link } from "react-router-dom" import AutoGraphIcon from "@mui/icons-material/AutoGraph" @@ -130,7 +130,7 @@ export const AppDrawer: FC<{ const [open, setOpen] = useRecoilState(drawerOpenState) const reloadInterval = useRecoilValue(reloadIntervalState) const isPreferential = - studyId !== undefined ? useStudyIsPreferencial(studyId) : null + studyId !== undefined ? useStudyIsPreferential(studyId) : null const styleListItem = { display: "block", diff --git a/optuna_dashboard/ts/components/ArtifactCardMedia.tsx b/optuna_dashboard/ts/components/ArtifactCardMedia.tsx index ebbbd751..6326cb16 100644 --- a/optuna_dashboard/ts/components/ArtifactCardMedia.tsx +++ b/optuna_dashboard/ts/components/ArtifactCardMedia.tsx @@ -21,6 +21,18 @@ export const ArtifactCardMedia: FC<{ filetype={artifact.filename.split(".").pop()} /> ) + } else if (artifact.mimetype.startsWith("video")) { + return ( + + ) } else if (artifact.mimetype.startsWith("audio")) { return (