Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify implementation of fast non-dominated sort #5160

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b3fc441
Implement testing._create_frozen_trial()
Alnusjaponica Dec 21, 2023
63a9488
Replace test_cma._create_frozen_trial and test_nsgaii._create_frozen_…
Alnusjaponica Dec 21, 2023
0e70a1f
Implement _fast_non_dominated_sort()
Alnusjaponica Dec 21, 2023
319bce0
replace _calculate_nondomination_rank() with _fast_non_dominated_sort()
Alnusjaponica Dec 21, 2023
90fc55e
Move comparison-time validation to _validate_constraints()
Alnusjaponica Dec 21, 2023
01cb145
Add helper function to calculate penalty
Alnusjaponica Dec 21, 2023
9d358b4
Rename a module
Alnusjaponica Dec 21, 2023
43a9ae3
Update argument for _validate_constraints()
Alnusjaponica Dec 21, 2023
373903c
Remove _fast_non_dominated_sort()
Alnusjaponica Dec 21, 2023
516e0ab
Add wrapper of _fast_non_dominated_sort for constrained nsga algorithm
Alnusjaponica Dec 21, 2023
04385eb
Move test_calculate_nondomination_rank() as non dominated sort logic …
Alnusjaponica Dec 21, 2023
a613050
Replace _fast_non_dominated_sort with _rank_population in the tests
Alnusjaponica Dec 21, 2023
a4cd1cd
Handle the case where both trials are infeasible and have the same pe…
Alnusjaponica Dec 22, 2023
b88d010
Update penalty handling
Alnusjaponica Feb 1, 2024
881009f
Remove a comment
Alnusjaponica Feb 1, 2024
1bac8bd
Merge branch 'master' of https://github.com/optuna/optuna into unifiy…
Alnusjaponica Feb 1, 2024
a174c04
Remove unnecessary assertion
Alnusjaponica Feb 1, 2024
950fcc0
Remove unnecessary assertion
Alnusjaponica Feb 1, 2024
f5ae9d8
Fix rank starts
Alnusjaponica Feb 1, 2024
bd54538
Initialize num_constraints before the loop
Alnusjaponica Feb 1, 2024
a081ef0
Re-write test to use @pytest.mark.parametrize
Alnusjaponica Feb 1, 2024
cc46d17
Add test cases for duplicate values
Alnusjaponica Feb 1, 2024
a38e76e
Add test cases of different constraint dimension
Alnusjaponica Feb 1, 2024
51f4458
import annotations to use the type hint list[float]
Alnusjaponica Feb 1, 2024
b484b28
Merge branch 'master' of https://github.com/optuna/optuna into unifiy…
Alnusjaponica Feb 8, 2024
8fcd160
Reduce lines
Alnusjaponica Feb 8, 2024
13aa189
Remove unnecessary initialization
Alnusjaponica Feb 8, 2024
afac585
Merge branch 'unifiy-implementation-of-fast-nondominated-sort' of htt…
Alnusjaponica Feb 8, 2024
9876648
Reduce lines
Alnusjaponica Feb 8, 2024
60ea231
Reduce lines
Alnusjaponica Feb 8, 2024
a5c55e4
Move nondomination_rank definition for readability
Alnusjaponica Feb 15, 2024
c6a96d7
run np.isnan(penalty) only once
Alnusjaponica Feb 15, 2024
37b043d
pass n_below for calculating ranks of infeasible trials
Alnusjaponica Feb 15, 2024
dfbb3dd
Simplify while loop
Alnusjaponica Feb 15, 2024
6593e16
Rename test name
Alnusjaponica Feb 15, 2024
70d5802
Pass base rank to _calculate_nondomination_rank
Alnusjaponica Feb 15, 2024
378c390
Fix nondomination_rank update for rank=-1 cases
Alnusjaponica Feb 15, 2024
b6343d1
Ignore rank-1 trials in _rank_population
Alnusjaponica Feb 15, 2024
4bfe871
Fix docstring to make variable name cinsistent
Alnusjaponica Feb 22, 2024
5c25571
Rename variable from `is_nan` to `is_penalty_nan `
Alnusjaponica Feb 22, 2024
e4f193e
Cover test cases for invalid input
Alnusjaponica Feb 22, 2024
3c6547b
Early return when n_below<=-1
Alnusjaponica Feb 22, 2024
7ae7855
Replace _calculate_nondomination_rank with np.unique for 1d-array sort
Alnusjaponica Feb 22, 2024
a490277
Add docstring ro _fast_non_dominated_sort()
Alnusjaponica Feb 22, 2024
542f011
Fix flake8 error
Alnusjaponica Feb 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@
domination_ranks = _fast_non_dominated_sort(objective_values, penalty=penalty)
population_per_rank: list[list[FrozenTrial]] = [[] for _ in range(max(domination_ranks) + 1)]
for trial, rank in zip(population, domination_ranks):
if rank == -1:
continue

Check warning on line 132 in optuna/samplers/nsgaii/_elite_population_selection_strategy.py

View check run for this annotation

Codecov / codecov/patch

optuna/samplers/nsgaii/_elite_population_selection_strategy.py#L132

Added line #L132 was not covered by tests
population_per_rank[rank].append(trial)

return population_per_rank
33 changes: 20 additions & 13 deletions optuna/study/_multi_objective.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,32 +85,37 @@
return ranks

if len(penalty) != len(objective_values):
raise ValueError(

Check warning on line 88 in optuna/study/_multi_objective.py

View check run for this annotation

Codecov / codecov/patch

optuna/study/_multi_objective.py#L88

Added line #L88 was not covered by tests
Alnusjaponica marked this conversation as resolved.
Show resolved Hide resolved
"The length of penalty and objective_values must be same, but got "
"len(penalty)={} and len(objective_values)={}.".format(
len(penalty), len(objective_values)
)
)
nondomination_rank = np.zeros(len(objective_values), dtype=int)
nondomination_rank = np.full(len(objective_values), -1)
Copy link
Collaborator

Choose a reason for hiding this comment

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

As we can already bound max(nondomination_rank) by n_below and nondomination_rank of n_below + 1 will not be used, so what about using n_below + 1?
Another reason why we should probably avoid -1 is that it might cause unexpected bugs in the future when some developers use nondomination_rank being always better when it is lower.
Plus, this implementation requires an ad-hoc handling of nondomination_rank=-1 in each place where the function is used.

is_nan = np.isnan(penalty)
Alnusjaponica marked this conversation as resolved.
Show resolved Hide resolved
n_below = n_below or len(objective_values)

# First, we calculate the domination rank for feasible trials.
is_feasible = np.logical_and(~is_nan, penalty <= 0)
ranks, feasible_bottom_rank = _calculate_nondomination_rank(
ranks, bottom_rank = _calculate_nondomination_rank(
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we define nondomination_rank as:

nondomination_rank = np.full(len(objective_values), n_below + 1)

bottom_rank becomes bottom_rank = np.max(ranks).
Note that if np.max(bottom_rank) = n_below + 1, the processes hereafter simply define each nondomination_rank as n_below + <positive_integer>, so they will be ignored.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I totally agree what you say but it makes this PR even larger. Can I split the task as a follow-up and resolve your comment in another PR?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The suggestion is a little bit complicated, so I remarked the comment on #5089

objective_values[is_feasible], n_below=n_below
)
nondomination_rank[is_feasible] += ranks
nondomination_rank[is_feasible] += 1 + ranks
Alnusjaponica marked this conversation as resolved.
Show resolved Hide resolved
n_below -= np.count_nonzero(is_feasible)

# Second, we calculate the domination rank for infeasible trials.
is_infeasible = np.logical_and(~is_nan, penalty > 0)
ranks, infeasible_bottom_rank = _calculate_nondomination_rank(
penalty[is_infeasible, np.newaxis], n_below=n_below
ranks, bottom_rank = _calculate_nondomination_rank(
penalty[is_infeasible, np.newaxis], n_below=n_below, base_rank=bottom_rank + 1
)
Alnusjaponica marked this conversation as resolved.
Show resolved Hide resolved
nondomination_rank[is_infeasible] += ranks + (feasible_bottom_rank + 1)
nondomination_rank[is_infeasible] += 1 + ranks
n_below -= np.count_nonzero(is_infeasible)

# Third, we calculate the domination rank for trials with no penalty information.
ranks, _ = _calculate_nondomination_rank(objective_values[is_nan], n_below=n_below)
nondomination_rank[is_nan] += ranks + (feasible_bottom_rank + 1) + (infeasible_bottom_rank + 1)
ranks, _ = _calculate_nondomination_rank(
objective_values[is_nan], n_below=n_below, base_rank=bottom_rank + 1
)
nondomination_rank[is_nan] += 1 + ranks

return nondomination_rank

Expand All @@ -119,12 +124,15 @@
objective_values: np.ndarray,
*,
n_below: int | None = None,
base_rank: int = 0,
) -> tuple[np.ndarray, int]:
# Calculate the domination matrix.
# The resulting matrix `domination_matrix` is a boolean matrix where
# Normalize n_below.
n_below = n_below or len(objective_values)
n_below = min(n_below, len(objective_values))
Alnusjaponica marked this conversation as resolved.
Show resolved Hide resolved

# The ndarray `domination_matrix` is a boolean 2d matrix where
# `domination_matrix[i, j] == True` means that the j-th trial dominates the i-th trial in the
# given multi objective minimization problem.

domination_mat = np.all(
objective_values[:, np.newaxis, :] >= objective_values[np.newaxis, :, :], axis=2
) & np.any(objective_values[:, np.newaxis, :] > objective_values[np.newaxis, :, :], axis=2)
Expand All @@ -137,9 +145,8 @@
ranks = np.full(len(objective_values), -1)
dominated_count = np.sum(domination_mat, axis=1)

rank = -1
rank = base_rank - 1
ranked_idx_num = 0
n_below = n_below or len(objective_values)
while ranked_idx_num < n_below:
# Find the non-dominated trials and assign the rank.
(non_dominated_idxs,) = np.nonzero(dominated_count == 0)
Expand Down
2 changes: 1 addition & 1 deletion tests/study_tests/test_multi_objective.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,6 @@ def test_dominates_complete_vs_incomplete(t1_state: TrialState) -> None:
), # Three objectives with duplicate values are included.
],
)
def test_calculate_nondomination_rank(trial_values: list[float], trial_ranks: list[int]) -> None:
def test_fast_non_dominated_sort(trial_values: list[float], trial_ranks: list[int]) -> None:
ranks = list(_fast_non_dominated_sort(np.array(trial_values)))
assert np.array_equal(ranks, trial_ranks)