Skip to content

Fix a minor bug in GPSampler for objective that returns inf#5995

Merged
gen740 merged 9 commits intooptuna:masterfrom
nabenabe0928:debug-inf-filter-in-gp
Mar 14, 2025
Merged

Fix a minor bug in GPSampler for objective that returns inf#5995
gen740 merged 9 commits intooptuna:masterfrom
nabenabe0928:debug-inf-filter-in-gp

Conversation

@nabenabe0928
Copy link
Copy Markdown
Contributor

@nabenabe0928 nabenabe0928 commented Feb 27, 2025

Motivation

This PR fixes a minor bug in GPSampler.
The bug of my interest was embedded here:

image

In principle, this change does not affect most users because this statement is triggered only if a user-defined objective function returns non-finite values.

The bug was originally introduced because of the following misunderstanding:

  • whether to give the initial argument in np.max and np.min does not affect the result as long as the array of interest is not empty.

However, this is not actually true.
For example, np.max([1], initial=10) gives us 10 instead of 1, which we would like to yield in fact.

To this end, I address this issue by modifying the corresponding part.
Note that emmr also needs to be fixed due to the same issue.

Description of the changes

  • Address the issue stated above

@nabenabe0928 nabenabe0928 added the bug Issue/PR about behavior that is broken. Not for typos/examples/CI/test but for Optuna itself. label Feb 28, 2025
@nabenabe0928 nabenabe0928 added this to the v4.3.0 milestone Feb 28, 2025
@nabenabe0928 nabenabe0928 changed the title Fix a bug in GPSampler Fix a minor bug in GPSampler for objective that returns inf Feb 28, 2025
@HideakiImamura
Copy link
Copy Markdown
Member

@c-bata @gen740 @sawa3030 Could you review this PR?

@sawa3030
Copy link
Copy Markdown
Collaborator

sawa3030 commented Mar 6, 2025

I believe the previous logic computed the maximum over all elements in values, whereas the new implementation takes the maximum along axis=0. When values.shape = (n, m) with m >= 2, this change affects the return values. For example, if values = [[inf, 10], [inf, 10]], the previous implementation would return [[10, 10], [10, 10]], while the new implementation returns [[0, 10], [0, 10]]. Here, consstraint_vals can have multiple columns when multiple constraints are provided, which I think may lead to unintended changes in behavior.

@nabenabe0928
Copy link
Copy Markdown
Contributor Author

Verification Code
import numpy as np


def original(values: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    finite_vals = values[np.isfinite(values)]
    best_finite_val = np.max(finite_vals, axis=0, initial=0.0)
    worst_finite_val = np.min(finite_vals, axis=0, initial=0.0)
    return best_finite_val, worst_finite_val


def this_pr(values: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    finite_vals_with_nan = np.where(np.isfinite(values), values, np.nan)
    is_any_finite = np.any(np.isfinite(finite_vals_with_nan), axis=0)
    best_finite_vals = np.where(is_any_finite, np.nanmax(finite_vals_with_nan, axis=0), 0.0)
    worst_finite_vals = np.where(is_any_finite, np.nanmin(finite_vals_with_nan, axis=0), 0.0)
    return best_finite_vals, worst_finite_vals

@nabenabe0928
Copy link
Copy Markdown
Contributor Author

nabenabe0928 commented Mar 6, 2025

I believe the previous logic computed the maximum over all elements in values, whereas the new implementation takes the maximum along axis=0. When values.shape = (n, m) with m >= 2, this change affects the return values. For example, if values = [[inf, 10], [inf, 10]], the previous implementation would return [[10, 10], [10, 10]], while the new implementation returns [[0, 10], [0, 10]]. Here, consstraint_vals can have multiple columns when multiple constraints are provided, which I think may lead to unintended changes in behavior.

@sawa3030
In fact, this is a good point, and it seems the original implementation embedded the bug you pointed out.

First of all, we expect the shapes of best_finite_vals and worst_finite_vals to be (m, ) while we actually get the shape of (1, ), which is already incorrect based on the verification code:

So we should probably add some unit tests for this part.

But again, this routine isn't really executed for most use cases, so negative user impacts are very limited.

@nabenabe0928
Copy link
Copy Markdown
Contributor Author

@sawa3030
I added some unit tests to ensure the code behavior, PTAL")

@sawa3030
Copy link
Copy Markdown
Collaborator

sawa3030 commented Mar 6, 2025

Thank you for the explanation and for adding the unit tests. LGTM

Copy link
Copy Markdown
Member

@c-bata c-bata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nabenabe0928 Changes look almost good to me. I left one comment though.

Comment on lines +13 to +42
@pytest.mark.parametrize(
"values,ans",
[
(np.array([-1, 0, 1]), np.array([-1, 0, 1])),
(np.array([-1, -np.inf, 0, np.inf, 1]), np.array([-1, -1, 0, 1, 1])),
(np.array([[-1, 2], [0, -2], [1, 0]]), np.array([[-1, 2], [0, -2], [1, 0]])),
(
np.array([[-1, 2], [-np.inf, np.inf], [0, -np.inf], [np.inf, -2], [1, 0]]),
np.array([[-1, 2], [-1, 2], [0, -2], [1, -2], [1, 0]]),
),
(
np.array(
[
[-100, np.inf, 10],
[-np.inf, np.inf, 100],
[-10, -np.inf, np.inf],
[np.inf, np.inf, -np.inf],
]
),
np.array([[-100, 0, 10], [-100, 0, 100], [-10, 0, 100], [-10, 0, 10]]),
),
(np.array([-np.inf, np.inf]), np.array([0, 0])),
(np.array([]), np.array([])),
],
)
def test_warn_and_convert_inf(values: np.ndarray, ans: np.ndarray) -> None:
assert np.allclose(warn_and_convert_inf(values), ans)
if len(values.shape) == 1:
# Test also with the shape of (n, 1) to ensure the batched version.
assert np.allclose(warn_and_convert_inf(values[:, np.newaxis]), ans[:, np.newaxis])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test code would be more readable if it were split into two test cases: one for single-dimensional arrays and another for two-dimensional arrays. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I addressed your comment, PTAL!

Copy link
Copy Markdown
Member

@c-bata c-bata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM after CI passes!

@c-bata c-bata removed their assignment Mar 13, 2025
Copy link
Copy Markdown
Member

@gen740 gen740 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@gen740 gen740 merged commit 63179d9 into optuna:master Mar 14, 2025
14 checks passed
@gen740 gen740 removed their assignment Jun 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Issue/PR about behavior that is broken. Not for typos/examples/CI/test but for Optuna itself.