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

Fast pareto-front calculation for 2D #687

Merged
merged 14 commits into from
Dec 21, 2023
65 changes: 53 additions & 12 deletions optuna_dashboard/ts/components/GraphParetoFront.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,58 @@ const makeMarker = (
}
}

const getIsDominatedTrialND = (normalizedValues: number[][]) => {
// Fallback for straight-forward pareto front algorithm (O(N^2) complexity).
const dominatedTrials: boolean[] = []
normalizedValues.forEach((values0: number[]) => {
const dominated = normalizedValues.some((values1: number[]) => {
if (values0.every((value0: number, k: number) => values1[k] === value0)) {
return false
}
return values0.every((value0: number, k: number) => values1[k] <= value0)
})
dominatedTrials.push(dominated)
})
return dominatedTrials
}

const getIsDominatedTrial2D = (normalizedValues: number[][]) => {
// Fast pareto front algorithm (O(N log N) complexity).
const sorted = normalizedValues
.map((values, i) => [values[0], values[1], i])
.sort((a, b) => a[0] - b[0])
let minValue1 = sorted[0][1]
const dominatedTrials: boolean[] = new Array(normalizedValues.length).fill(
true
)

sorted.forEach((values) => {
if (values[1] <= minValue1) {
Copy link
Member

Choose a reason for hiding this comment

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

This condition is buggy when the values between trials are the same.

Copy link
Member Author

Choose a reason for hiding this comment

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

In that case, both trials should be considered undominated.

Copy link
Collaborator

@nabenabe0928 nabenabe0928 Dec 13, 2023

Choose a reason for hiding this comment

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

Now given a sequence of 2D vectors $\{(a_n, b_n)\}_{n=1}^N$ such that $a_1 \leq a_2 \leq \dots \leq a_N$ and $b_i \leq b_j$ for all $\{(i, j) \in [N] \times [N] \mid a_i = a_j, i &lt; j\}$ where $[N] \coloneqq \{1,2,\dots,N\}$, the condition here is a bit strange when the equality holds.

For example, $a_i \leq a_j$ holds from the assumption and we now assume that $b_i = b_j$.
As mentioned above, let's assume that $a_i = a_j$ and $b_i = b_j$ do not dominate each other.
However, $a_i &lt; a_j$ and $\min_{n \in [i]} b_i = b_j$ could still hold.
It means that there could exist $(a_i, b_i)$ such that $a_i &lt; a_j$ and $b_i \leq b_j$, which weakly dominates $(a_j, b_j)$.

Btw, I guessed you are trying to get "weak dominance" from getIsDominatedTrialND.

Maybe I am wrong, if so, correct me please!

const normalizedValues = [[0, 1], [1, 1], [2, 1]]

const getIsDominatedTrial2D = (normalizedValues: number[][]) => {
    const sortedValues = normalizedValues
      .map((values, i) => [values[0], values[1], i])
      .sort((a, b) => a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1])
    let minValue1 = sortedValues[0][1]
    const dominatedTrials: boolean[] = new Array(normalizedValues.length).fill(
      true
    )
    sortedValues.forEach((values) => {
      if (values[1] <= minValue1) {
        dominatedTrials[values[2]] = false
        minValue1 = values[1]
      }
    })
    return dominatedTrials
}

console.log(getIsDominatedTrial2D(normalizedValues))

Output

[ false, false, false ]

I guess (if we need weakly dominated trials) what we want is:

[ false, true, true ]

Copy link
Collaborator

@nabenabe0928 nabenabe0928 Dec 13, 2023

Choose a reason for hiding this comment

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

Quick solution to this is the following:

const normalizedValues = [[0, 1], [1, 1], [2, 1]]

const getIsDominatedTrial2D = (normalizedValues: number[][]) => {
    const sortedValues = normalizedValues
      .map((values, i) => [values[0], values[1], i])
      .sort((a, b) => a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1])
    let minValue1 = sortedValues[0][1]
    let latestParetoSolution = sortedValues[0].slice(0, 2)
    const dominatedTrials: boolean[] = new Array(normalizedValues.length).fill(
      true
    )
    sortedValues.forEach((values) => {
      if (values[1] < minValue1 || latestParetoSolution.every((v, i) => v === values[i])) {
        dominatedTrials[values[2]] = false
        minValue1 = values[1]
        latestParetoSolution = values.slice(0, 2)
      }
    })
    return dominatedTrials
}

console.log(getIsDominatedTrial2D(normalizedValues))

Copy link
Member

Choose a reason for hiding this comment

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

See https://github.com/optuna/optuna/blob/master/optuna/study/_multi_objective.py#L101. Optuna uses weakly dominates for comparison. It seems that @nabenabe0928 is correct.

Copy link
Member

Choose a reason for hiding this comment

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

Thank you for pointing out. My statement is incorrect and @nabenabe0928's one is correct, I think.

Copy link
Collaborator

@nabenabe0928 nabenabe0928 Dec 14, 2023

Choose a reason for hiding this comment

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

The new modification fails with the following case.

const normalizedValues = [[0, 1], [1, 2], [2, 1], [3, 0], [3, 0]]

We get:

[ false, true, false, false, false ]

But we would like to have:

[ false, true, true, false, false ]

I think the suggestion here is sufficient.

dominatedTrials[values[2]] = false
minValue1 = values[1]
}
})
return dominatedTrials
}

const getIsDominatedTrial1D = (normalizedValues: number[][]) => {
const best_value = Math.min(...normalizedValues.map((values) => values[0]))
return normalizedValues.map((value) => value[0] !== best_value)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return normalizedValues.map((value) => value[0] !== best_value)
return normalizedValues.map((values) => values[0] !== best_value)

}

const getIsDominatedTrial = (normalizedValues: number[][]) => {
if (normalizedValues.length == 0) {
return []
}
if (normalizedValues[0].length == 1) {
return getIsDominatedTrial1D(normalizedValues)
} else if (normalizedValues[0].length == 2) {
return getIsDominatedTrial2D(normalizedValues)
} else {
return getIsDominatedTrialND(normalizedValues)
}
}

const plotParetoFront = (
study: StudyDetail,
objectiveXId: number,
Expand Down Expand Up @@ -218,18 +270,7 @@ const plotParetoFront = (
}
})

const dominatedTrials: boolean[] = []
normalizedValues.forEach((values0: number[], i: number) => {
const dominated = normalizedValues.some((values1: number[], j: number) => {
if (i === j) {
return false
}
return values0.every((value0: number, k: number) => {
return values1[k] <= value0
})
})
dominatedTrials.push(dominated)
})
const dominatedTrials: boolean[] = getIsDominatedTrial(normalizedValues)

const plotData: Partial<plotly.PlotData>[] = [
makeScatterObject(
Expand Down