From eb09dc906a0633e539b11dc1fac806003dd7b46e Mon Sep 17 00:00:00 2001 From: c-bata Date: Sat, 7 Jan 2023 13:12:11 +0900 Subject: [PATCH 01/10] Refactor GraphEdf --- optuna_dashboard/ts/components/GraphEdf.tsx | 19 +++++++++---------- .../ts/components/StudyDetail.tsx | 4 ++-- .../ts/components/StudyDetailBeta.tsx | 4 ++-- optuna_dashboard/ts/trialFilter.ts | 9 ++++++--- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphEdf.tsx b/optuna_dashboard/ts/components/GraphEdf.tsx index c0a32cf9..34f88377 100644 --- a/optuna_dashboard/ts/components/GraphEdf.tsx +++ b/optuna_dashboard/ts/components/GraphEdf.tsx @@ -1,5 +1,5 @@ import * as plotly from "plotly.js-dist-min" -import React, { FC, useEffect, useState } from "react" +import React, { FC, useEffect } from "react" import { Grid, FormControl, @@ -16,23 +16,22 @@ import { Target, useFilteredTrials, useObjectiveTargets } from "../trialFilter" const plotDomId = "graph-edf" -export const Edf: FC<{ +export const GraphEdf: FC<{ study: StudyDetail | null }> = ({ study = null }) => { const theme = useTheme() - const [objectiveId, setObjectiveId] = useState(0) - const targets = useObjectiveTargets(study) - const trials = useFilteredTrials(study, [targets[objectiveId]], false, false) + const [targets, selected, setTarget] = useObjectiveTargets(study) + const trials = useFilteredTrials(study, [selected], false, false) const handleObjectiveChange = (event: SelectChangeEvent) => { - setObjectiveId(event.target.value as number) + setTarget(event.target.value as number) } useEffect(() => { if (study != null) { - plotEdf(trials, targets[objectiveId], theme.palette.mode) + plotEdf(trials, selected, theme.palette.mode) } - }, [trials, targets, objectiveId, theme.palette.mode]) + }, [trials, selected, theme.palette.mode]) return ( {study !== null && study.directions.length !== 1 ? ( - Objective ID: - {targets.map((target, i) => ( {target.toLabel(study?.objective_names)} diff --git a/optuna_dashboard/ts/components/StudyDetail.tsx b/optuna_dashboard/ts/components/StudyDetail.tsx index b71d8591..8e15d58d 100644 --- a/optuna_dashboard/ts/components/StudyDetail.tsx +++ b/optuna_dashboard/ts/components/StudyDetail.tsx @@ -18,7 +18,7 @@ import Brightness7Icon from "@mui/icons-material/Brightness7" import { GraphParallelCoordinate } from "./GraphParallelCoordinate" import { GraphHyperparameterImportances } from "./GraphHyperparameterImportances" -import { Edf } from "./GraphEdf" +import { GraphEdf } from "./GraphEdf" import { Contour } from "./GraphContour" import { GraphIntermediateValues } from "./GraphIntermediateValues" import { GraphSlice } from "./GraphSlice" @@ -200,7 +200,7 @@ export const StudyDetail: FC<{ {graphVisibility.edf ? ( - + ) : null} diff --git a/optuna_dashboard/ts/components/StudyDetailBeta.tsx b/optuna_dashboard/ts/components/StudyDetailBeta.tsx index a98c3d04..6c753dc1 100644 --- a/optuna_dashboard/ts/components/StudyDetailBeta.tsx +++ b/optuna_dashboard/ts/components/StudyDetailBeta.tsx @@ -32,7 +32,7 @@ import { GraphSlice } from "./GraphSlice" import { GraphParetoFront } from "./GraphParetoFront" import { DataGrid, DataGridColumn } from "./DataGrid" import { GraphIntermediateValues } from "./GraphIntermediateValues" -import { Edf } from "./GraphEdf" +import { GraphEdf } from "./GraphEdf" import { TrialList } from "./TrialList" import { BestTrialsCard } from "./BestTrialsCard" @@ -175,7 +175,7 @@ export const StudyDetailBeta: FC<{ - + diff --git a/optuna_dashboard/ts/trialFilter.ts b/optuna_dashboard/ts/trialFilter.ts index 204fa817..9128a330 100644 --- a/optuna_dashboard/ts/trialFilter.ts +++ b/optuna_dashboard/ts/trialFilter.ts @@ -1,4 +1,4 @@ -import { useMemo } from "react" +import {useMemo, useState} from "react" import { mergeUnionSearchSpace } from "./searchSpace" type TargetKind = "objective" | "user_attr" | "params" @@ -113,14 +113,17 @@ export const useFilteredTrials = ( }) }, [study?.trials, targets, filterComplete, filterPruned]) -export const useObjectiveTargets = (study: StudyDetail | null): Target[] => - useMemo(() => { +export const useObjectiveTargets = (study: StudyDetail | null): [Target[], Target, (index: number) => void] => { + const [targetIndex, setTargetIndex] = useState(0) + const targetList = useMemo(() => { if (study !== null) { return study.directions.map((v, i) => new Target("objective", i)) } else { return [new Target("objective", 0)] } }, [study?.directions]) + return [targetList, targetList[targetIndex], setTargetIndex] +} export const useParamTargets = ( study: StudyDetail | null From 4452c28ef245325a266d2814fd5ce4d40773cdf8 Mon Sep 17 00:00:00 2001 From: c-bata Date: Sat, 7 Jan 2023 13:26:15 +0900 Subject: [PATCH 02/10] Refactor GraphHistory --- optuna_dashboard/ts/components/GraphEdf.tsx | 11 ++++-- .../ts/components/GraphHistory.tsx | 28 ++++++------- optuna_dashboard/ts/trialFilter.ts | 39 +++++++++++++++---- 3 files changed, 52 insertions(+), 26 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphEdf.tsx b/optuna_dashboard/ts/components/GraphEdf.tsx index 34f88377..db92bde3 100644 --- a/optuna_dashboard/ts/components/GraphEdf.tsx +++ b/optuna_dashboard/ts/components/GraphEdf.tsx @@ -23,8 +23,8 @@ export const GraphEdf: FC<{ const [targets, selected, setTarget] = useObjectiveTargets(study) const trials = useFilteredTrials(study, [selected], false, false) - const handleObjectiveChange = (event: SelectChangeEvent) => { - setTarget(event.target.value as number) + const handleObjectiveChange = (event: SelectChangeEvent) => { + setTarget(event.target.value) } useEffect(() => { @@ -47,9 +47,12 @@ export const GraphEdf: FC<{ {study !== null && study.directions.length !== 1 ? ( Objective: - {targets.map((target, i) => ( - + {target.toLabel(study?.objective_names)} ))} diff --git a/optuna_dashboard/ts/components/GraphHistory.tsx b/optuna_dashboard/ts/components/GraphHistory.tsx index 8c38fc48..e4b8a5ba 100644 --- a/optuna_dashboard/ts/components/GraphHistory.tsx +++ b/optuna_dashboard/ts/components/GraphHistory.tsx @@ -33,12 +33,10 @@ export const GraphHistory: FC<{ const [filterCompleteTrial, setFilterCompleteTrial] = useState(false) const [filterPrunedTrial, setFilterPrunedTrial] = useState(false) - const objectiveNames: string[] = study?.objective_names || [] - const targetList = useObjectiveAndSystemAttrTargets(study) - const [targetIndex, setTargetIndex] = useState(0) + const [targets, selected, setTarget] = useObjectiveAndSystemAttrTargets(study) const trials = useFilteredTrials( study, - [targetList[targetIndex]], + [selected], filterCompleteTrial, filterPrunedTrial ) @@ -48,7 +46,7 @@ export const GraphHistory: FC<{ plotHistory( trials, study.directions, - targetList[targetIndex], + selected, xAxis, logScale, theme.palette.mode @@ -57,8 +55,7 @@ export const GraphHistory: FC<{ }, [ trials, study?.directions, - targetIndex, - targetList, + selected, logScale, xAxis, filterPrunedTrial, @@ -66,8 +63,8 @@ export const GraphHistory: FC<{ theme.palette.mode, ]) - const handleObjectiveChange = (event: SelectChangeEvent) => { - setTargetIndex(event.target.value as number) + const handleObjectiveChange = (event: SelectChangeEvent) => { + setTarget(event.target.value) } const handleXAxisChange = (e: ChangeEvent) => { @@ -98,16 +95,19 @@ export const GraphHistory: FC<{ History - {study !== null && targetList.length >= 2 ? ( + {targets.length >= 2 ? ( y Axis - + {targets.map((t, i) => ( + + {t.toLabel(study?.objective_names)} ))} diff --git a/optuna_dashboard/ts/trialFilter.ts b/optuna_dashboard/ts/trialFilter.ts index 9128a330..f2e82e27 100644 --- a/optuna_dashboard/ts/trialFilter.ts +++ b/optuna_dashboard/ts/trialFilter.ts @@ -1,4 +1,4 @@ -import {useMemo, useState} from "react" +import { useMemo, useState } from "react" import { mergeUnionSearchSpace } from "./searchSpace" type TargetKind = "objective" | "user_attr" | "params" @@ -29,6 +29,10 @@ export class Target { return true } + identifier(): string { + return `${this.kind}:${this.key}` + } + toLabel(objectiveNames?: string[]): string { if (this.kind === "objective") { const objectiveId: number = this.key as number @@ -113,16 +117,25 @@ export const useFilteredTrials = ( }) }, [study?.trials, targets, filterComplete, filterPruned]) -export const useObjectiveTargets = (study: StudyDetail | null): [Target[], Target, (index: number) => void] => { - const [targetIndex, setTargetIndex] = useState(0) +export const useObjectiveTargets = ( + study: StudyDetail | null +): [Target[], Target, (ident: string) => void] => { + const defaultTarget = new Target("objective", 0) + const [selected, setTargetIdent] = useState( + defaultTarget.identifier() + ) const targetList = useMemo(() => { if (study !== null) { return study.directions.map((v, i) => new Target("objective", i)) } else { - return [new Target("objective", 0)] + return [defaultTarget] } }, [study?.directions]) - return [targetList, targetList[targetIndex], setTargetIndex] + const selectedTarget = useMemo( + () => targetList.find((t) => t.identifier() === selected) || defaultTarget, + [targetList, selected] + ) + return [targetList, selectedTarget, setTargetIdent] } export const useParamTargets = ( @@ -140,8 +153,12 @@ export const useParamTargets = ( export const useObjectiveAndSystemAttrTargets = ( study: StudyDetail | null -): Target[] => - useMemo(() => { +): [Target[], Target, (ident: string) => void] => { + const defaultTarget = new Target("objective", 0) + const [selected, setTargetIdent] = useState( + defaultTarget.identifier() + ) + const targetList = useMemo(() => { if (study !== null) { return [ ...study.directions.map((v, i) => new Target("objective", i)), @@ -150,6 +167,12 @@ export const useObjectiveAndSystemAttrTargets = ( .map((attr) => new Target("user_attr", attr.key)), ] } else { - return [new Target("objective", 0)] + return [defaultTarget] } }, [study?.directions, study?.union_user_attrs]) + const selectedTarget = useMemo( + () => targetList.find((t) => t.identifier() === selected) || defaultTarget, + [targetList, selected] + ) + return [targetList, selectedTarget, setTargetIdent] +} From b61d83bd4a521779466fbf0cf2346c4f5d960918 Mon Sep 17 00:00:00 2001 From: c-bata Date: Sat, 7 Jan 2023 13:29:19 +0900 Subject: [PATCH 03/10] Fix GraphSlice --- optuna_dashboard/ts/components/GraphSlice.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphSlice.tsx b/optuna_dashboard/ts/components/GraphSlice.tsx index 4b8f8021..4d148c34 100644 --- a/optuna_dashboard/ts/components/GraphSlice.tsx +++ b/optuna_dashboard/ts/components/GraphSlice.tsx @@ -34,13 +34,12 @@ export const GraphSlice: FC<{ }> = ({ study = null }) => { const theme = useTheme() - const [objectiveId, setObjectiveId] = useState(0) - const objectiveTargets = useObjectiveTargets(study) + const [objectiveTargets, selectedObjective, setObjectiveTarget] = useObjectiveTargets(study) const [paramTargetsIndex, setParamTargetsIndex] = useState(0) const [paramTargets, searchSpace] = useParamTargets(study) const [logYScale, setLogYScale] = useState(false) - const filterTargets: Target[] = [objectiveTargets[objectiveId]] + const filterTargets: Target[] = [selectedObjective] if (paramTargets.length > paramTargetsIndex) filterTargets.push(paramTargets[paramTargetsIndex]) const trials = useFilteredTrials(study, filterTargets, false, false) @@ -48,7 +47,7 @@ export const GraphSlice: FC<{ useEffect(() => { plotSlice( trials, - objectiveTargets[objectiveId], + selectedObjective, searchSpace.length > paramTargetsIndex ? searchSpace[paramTargetsIndex] : null, @@ -57,15 +56,15 @@ export const GraphSlice: FC<{ ) }, [ trials, - objectiveTargets[objectiveId], + selectedObjective, searchSpace, paramTargetsIndex, logYScale, theme.palette.mode, ]) - const handleObjectiveChange = (event: SelectChangeEvent) => { - setObjectiveId(event.target.value as number) + const handleObjectiveChange = (event: SelectChangeEvent) => { + setObjectiveTarget(event.target.value) } const handleSelectedParam = (e: SelectChangeEvent) => { @@ -90,10 +89,10 @@ export const GraphSlice: FC<{ {study !== null && study.directions.length !== 1 && ( - Objective ID: - {objectiveTargets.map((t, i) => ( - + {t.toLabel(study?.objective_names)} ))} From 0a4afc32a893df107c66e0f28d9b5d9dce1250f3 Mon Sep 17 00:00:00 2001 From: c-bata Date: Sat, 7 Jan 2023 13:31:14 +0900 Subject: [PATCH 04/10] Rename labels of GraphParetoFront --- optuna_dashboard/ts/components/GraphParetoFront.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphParetoFront.tsx b/optuna_dashboard/ts/components/GraphParetoFront.tsx index 22c4007c..54f49d0e 100644 --- a/optuna_dashboard/ts/components/GraphParetoFront.tsx +++ b/optuna_dashboard/ts/components/GraphParetoFront.tsx @@ -52,7 +52,7 @@ export const GraphParetoFront: FC<{ {study !== null && study.directions.length !== 1 ? ( <> - Objective X ID: + Objective X: - Objective Y ID: + Objective Y: + )} - {paramTargets.length !== 0 && paramTargetsIndex !== null && ( + {paramTargets.length !== 0 && selectedParamTarget !== null && ( Parameter: - {paramTargets.map((t, i) => ( - + {t.toLabel()} ))} @@ -130,7 +142,8 @@ export const GraphSlice: FC<{ const plotSlice = ( trials: Trial[], objectiveTarget: Target, - selected: SearchSpaceItem | null, + selectedParamTarget: Target | null, + selectedParamSpace: SearchSpaceItem | null, logYScale: boolean, mode: string ) => { @@ -146,8 +159,8 @@ const plotSlice = ( b: 0, }, xaxis: { - title: selected?.name || "", - type: selected !== null && isLogScale(selected) ? "log" : "linear", + title: selectedParamTarget?.toLabel() || "", + type: selectedParamSpace !== null && isLogScale(selectedParamSpace) ? "log" : "linear", gridwidth: 1, automargin: true, }, @@ -160,11 +173,7 @@ const plotSlice = ( showlegend: false, template: mode === "dark" ? plotlyDarkTemplate : {}, } - if (selected === null) { - plotly.react(plotDomId, [], layout) - return - } - if (trials.length === 0) { + if (selectedParamSpace === null || selectedParamTarget === null || trials.length === 0) { plotly.react(plotDomId, [], layout) return } @@ -172,11 +181,10 @@ const plotSlice = ( const objectiveValues: number[] = trials.map( (t) => objectiveTarget.getTargetValue(t) as number ) - const paramTarget = new Target("params", selected.name) - const values = trials.map((t) => paramTarget.getTargetValue(t) as number) + const values = trials.map((t) => selectedParamTarget.getTargetValue(t) as number) const trialNumbers: number[] = trials.map((t) => t.number) - if (selected.distribution.type !== "CategoricalDistribution") { + if (selectedParamSpace.distribution.type !== "CategoricalDistribution") { const trace: plotly.Data[] = [ { type: "scatter", @@ -198,14 +206,14 @@ const plotSlice = ( }, ] layout["xaxis"] = { - title: selected.name, - type: selected.distribution.log ? "log" : "linear", + title: selectedParamTarget.toLabel(), + type: isLogScale(selectedParamSpace) ? "log" : "linear", gridwidth: 1, automargin: true, // Otherwise the label is outside of the plot } plotly.react(plotDomId, trace, layout) } else { - const vocabArr = selected.distribution.choices.map((c) => c.value) + const vocabArr = selectedParamSpace.distribution.choices.map((c) => c.value) const tickvals: number[] = vocabArr.map((v, i) => i) const trace: plotly.Data[] = [ { @@ -228,7 +236,7 @@ const plotSlice = ( }, ] layout["xaxis"] = { - title: selected.name, + title: selectedParamTarget.toLabel(), type: "linear", gridwidth: 1, tickvals: tickvals, diff --git a/optuna_dashboard/ts/trialFilter.ts b/optuna_dashboard/ts/trialFilter.ts index f2e82e27..73d28258 100644 --- a/optuna_dashboard/ts/trialFilter.ts +++ b/optuna_dashboard/ts/trialFilter.ts @@ -1,5 +1,4 @@ import { useMemo, useState } from "react" -import { mergeUnionSearchSpace } from "./searchSpace" type TargetKind = "objective" | "user_attr" | "params" @@ -139,17 +138,21 @@ export const useObjectiveTargets = ( } export const useParamTargets = ( - study: StudyDetail | null -): [Target[], SearchSpaceItem[]] => - useMemo<[Target[], SearchSpaceItem[]]>(() => { - if (study !== null) { - const searchSpace = mergeUnionSearchSpace(study.union_search_space) - const targets = searchSpace.map((s) => new Target("params", s.name)) - return [targets, searchSpace] - } else { - return [[], []] - } - }, [study?.union_search_space]) + searchSpace: SearchSpaceItem[] +): [Target[], Target | null, (ident: string) => void] => { + const [selected, setTargetIdent] = useState("") + const targetList = useMemo(() => { + const targets = searchSpace.map((s) => new Target("params", s.name)) + if (selected === "" && targets.length > 0) + setTargetIdent(targets[0].identifier()) + return targets + }, [searchSpace]) + const selectedTarget = useMemo( + () => targetList.find((t) => t.identifier() === selected) || null, + [targetList, selected] + ) + return [targetList, selectedTarget, setTargetIdent] +} export const useObjectiveAndSystemAttrTargets = ( study: StudyDetail | null From 8bb63f6a7ea7d4e807906153f48daaeace488555 Mon Sep 17 00:00:00 2001 From: c-bata Date: Sat, 7 Jan 2023 14:57:15 +0900 Subject: [PATCH 06/10] Refactor Parallel Coordinate --- .../ts/components/GraphParallelCoordinate.tsx | 90 ++++++++++--------- optuna_dashboard/ts/components/GraphSlice.tsx | 15 +++- 2 files changed, 60 insertions(+), 45 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx index 7347f42e..976bb218 100644 --- a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx +++ b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx @@ -1,5 +1,5 @@ import * as plotly from "plotly.js-dist-min" -import React, { FC, useEffect, useState } from "react" +import React, { FC, useEffect, useMemo } from "react" import { Grid, FormControl, @@ -12,6 +12,11 @@ import { Box, } from "@mui/material" import { plotlyDarkTemplate } from "./PlotlyDarkMode" +import { + Target, + useFilteredTrials, + useObjectiveAndSystemAttrTargets, +} from "../trialFilter" const plotDomId = "graph-parallel-coordinate" @@ -19,18 +24,29 @@ export const GraphParallelCoordinate: FC<{ study: StudyDetail | null }> = ({ study = null }) => { const theme = useTheme() - const [objectiveId, setObjectiveId] = useState(0) - const objectiveNames: string[] = study?.objective_names || [] + const [targets, selected, setTarget] = useObjectiveAndSystemAttrTargets(study) + const filterTargets = useMemo( + () => [ + ...(study !== null + ? study.intersection_search_space.map( + (s) => new Target("params", s.name) + ) + : []), + ...(selected !== null ? [selected] : []), + ], + [study?.intersection_search_space, selected] + ) + const trials = useFilteredTrials(study, filterTargets, false, false) - const handleObjectiveChange = (event: SelectChangeEvent) => { - setObjectiveId(event.target.value as number) + const handleObjectiveChange = (event: SelectChangeEvent) => { + setTarget(event.target.value) } useEffect(() => { if (study !== null) { - plotCoordinate(study, objectiveId, theme.palette.mode) + plotCoordinate(study, trials, selected, theme.palette.mode) } - }, [study, objectiveId, theme.palette.mode]) + }, [study, trials, selected, theme.palette.mode]) return ( @@ -44,15 +60,16 @@ export const GraphParallelCoordinate: FC<{ Parallel Coordinate - {study !== null && study.directions.length !== 1 ? ( + {study !== null && targets.length >= 2 ? ( - Objective ID: - + {targets.map((t, i) => ( + + {t.toLabel(study.objective_names)} ))} @@ -66,23 +83,10 @@ export const GraphParallelCoordinate: FC<{ ) } -const filterFunc = (trial: Trial, objectiveId: number): boolean => { - if (trial.state !== "Complete" && trial.state !== "Pruned") { - return false - } - if (trial.values === undefined) { - return false - } - return ( - trial.values.length > objectiveId && - trial.values[objectiveId] !== "inf" && - trial.values[objectiveId] !== "-inf" - ) -} - const plotCoordinate = ( study: StudyDetail, - objectiveId: number, + trials: Trial[], + target: Target | null, mode: string ) => { if (document.getElementById(plotDomId) === null) { @@ -98,14 +102,11 @@ const plotCoordinate = ( }, template: mode === "dark" ? plotlyDarkTemplate : {}, } - - if (study.trials.length === 0) { + if (trials.length === 0 || target === null) { plotly.react(plotDomId, [], layout) return } - const filteredTrials = study.trials.filter((t) => filterFunc(t, objectiveId)) - const maxLabelLength = 40 const breakLength = maxLabelLength / 2 const ellipsis = "…" @@ -125,25 +126,25 @@ const plotCoordinate = ( } // Intersection param names - const objectiveValues: number[] = filteredTrials.map( - (t) => t.values![objectiveId] as number + const objectiveValues: number[] = trials.map( + (t) => target.getTargetValue(t) as number ) const dimensions = [ { - label: "Objective value", + label: target.toLabel(study.objective_names), values: objectiveValues, range: [Math.min(...objectiveValues), Math.max(...objectiveValues)], }, ] study.intersection_search_space.forEach((s) => { - const values: number[] = filteredTrials.map( + const values: number[] = trials.map( (t) => t.params.find((p) => p.name === s.name)!.param_internal_value ) if (s.distribution.type !== "CategoricalDistribution") { dimensions.push({ label: breakLabelIfTooLong(s.name), values: values, - range: [Math.min(...values), Math.max(...values)], + range: [s.distribution.low, s.distribution.high], }) } else { // categorical @@ -152,13 +153,18 @@ const plotCoordinate = ( dimensions.push({ label: breakLabelIfTooLong(s.name), values: values, - range: [Math.min(...values), Math.max(...values)], + range: [0, s.distribution.choices.length - 1], // @ts-ignore tickvals: tickvals, ticktext: vocabArr, }) } }) + const objectiveId = target.getObjectiveId() + const reversescale = + objectiveId !== null && study.directions.length > objectiveId + ? study.directions[objectiveId] === "maximize" + : "minimize" const plotData: Partial[] = [ { type: "parcoords", @@ -170,10 +176,10 @@ const plotCoordinate = ( // @ts-ignore colorscale: "Blues", colorbar: { - title: "Objective value", + title: target.toLabel(study.objective_names), }, showscale: true, - reversescale: study.directions[objectiveId] === "maximize", + reversescale: reversescale, }, }, ] diff --git a/optuna_dashboard/ts/components/GraphSlice.tsx b/optuna_dashboard/ts/components/GraphSlice.tsx index 10dae613..3afd0aa6 100644 --- a/optuna_dashboard/ts/components/GraphSlice.tsx +++ b/optuna_dashboard/ts/components/GraphSlice.tsx @@ -160,7 +160,10 @@ const plotSlice = ( }, xaxis: { title: selectedParamTarget?.toLabel() || "", - type: selectedParamSpace !== null && isLogScale(selectedParamSpace) ? "log" : "linear", + type: + selectedParamSpace !== null && isLogScale(selectedParamSpace) + ? "log" + : "linear", gridwidth: 1, automargin: true, }, @@ -173,7 +176,11 @@ const plotSlice = ( showlegend: false, template: mode === "dark" ? plotlyDarkTemplate : {}, } - if (selectedParamSpace === null || selectedParamTarget === null || trials.length === 0) { + if ( + selectedParamSpace === null || + selectedParamTarget === null || + trials.length === 0 + ) { plotly.react(plotDomId, [], layout) return } @@ -181,7 +188,9 @@ const plotSlice = ( const objectiveValues: number[] = trials.map( (t) => objectiveTarget.getTargetValue(t) as number ) - const values = trials.map((t) => selectedParamTarget.getTargetValue(t) as number) + const values = trials.map( + (t) => selectedParamTarget.getTargetValue(t) as number + ) const trialNumbers: number[] = trials.map((t) => t.number) if (selectedParamSpace.distribution.type !== "CategoricalDistribution") { From adc55a445fb811f51c33c6d34f99bd8c32a8f271 Mon Sep 17 00:00:00 2001 From: c-bata Date: Sat, 7 Jan 2023 16:37:56 +0900 Subject: [PATCH 07/10] List all objectives on Parallel Coordinate --- .../ts/components/GraphContour.tsx | 2 +- .../ts/components/GraphHistory.tsx | 4 +- .../ts/components/GraphParallelCoordinate.tsx | 168 ++++++++---------- optuna_dashboard/ts/trialFilter.ts | 2 +- 4 files changed, 81 insertions(+), 95 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphContour.tsx b/optuna_dashboard/ts/components/GraphContour.tsx index 11ccf617..002950d2 100644 --- a/optuna_dashboard/ts/components/GraphContour.tsx +++ b/optuna_dashboard/ts/components/GraphContour.tsx @@ -85,7 +85,7 @@ export const Contour: FC<{ {study !== null && study.directions.length !== 1 ? ( - Objective ID: + Objective: - {targets.map((t, i) => ( - - {t.toLabel(study.objective_names)} - - ))} - - - ) : null} @@ -86,7 +59,7 @@ export const GraphParallelCoordinate: FC<{ const plotCoordinate = ( study: StudyDetail, trials: Trial[], - target: Target | null, + targets: Target[], mode: string ) => { if (document.getElementById(plotDomId) === null) { @@ -102,7 +75,7 @@ const plotCoordinate = ( }, template: mode === "dark" ? plotlyDarkTemplate : {}, } - if (trials.length === 0 || target === null) { + if (trials.length === 0) { plotly.react(plotDomId, [], layout) return } @@ -125,46 +98,59 @@ const plotCoordinate = ( .join("") } - // Intersection param names - const objectiveValues: number[] = trials.map( - (t) => target.getTargetValue(t) as number - ) - const dimensions = [ - { - label: target.toLabel(study.objective_names), - values: objectiveValues, - range: [Math.min(...objectiveValues), Math.max(...objectiveValues)], - }, - ] - study.intersection_search_space.forEach((s) => { - const values: number[] = trials.map( - (t) => t.params.find((p) => p.name === s.name)!.param_internal_value - ) - if (s.distribution.type !== "CategoricalDistribution") { - dimensions.push({ - label: breakLabelIfTooLong(s.name), - values: values, - range: [s.distribution.low, s.distribution.high], - }) - } else { - // categorical - const vocabArr: string[] = s.distribution.choices.map((c) => c.value) - const tickvals: number[] = vocabArr.map((v, i) => i) - dimensions.push({ - label: breakLabelIfTooLong(s.name), - values: values, - range: [0, s.distribution.choices.length - 1], - // @ts-ignore - tickvals: tickvals, - ticktext: vocabArr, - }) - } - }) - const objectiveId = target.getObjectiveId() - const reversescale = - objectiveId !== null && study.directions.length > objectiveId - ? study.directions[objectiveId] === "maximize" - : "minimize" + const dimensions = targets + .map((target) => { + if (target.kind === "objective" || target.kind === "user_attr") { + const values: number[] = trials.map( + (t) => target.getTargetValue(t) as number + ) + return { + label: target.toLabel(study.objective_names), + values: values, + range: [Math.min(...values), Math.max(...values)], + } + } else { + const s = study.intersection_search_space.find( + (s) => s.name === target.key + ) as SearchSpaceItem // Must be already filtered. + + const values: number[] = trials.map( + (t) => target.getTargetValue(t) as number + ) + if (s.distribution.type !== "CategoricalDistribution") { + return { + label: breakLabelIfTooLong(s.name), + values: values, + range: [s.distribution.low, s.distribution.high], + } + } else { + // categorical + const vocabArr: string[] = s.distribution.choices.map((c) => c.value) + const tickvals: number[] = vocabArr.map((v, i) => i) + return { + label: breakLabelIfTooLong(s.name), + values: values, + range: [0, s.distribution.choices.length - 1], + // @ts-ignore + tickvals: tickvals, + ticktext: vocabArr, + } + } + } + }) + if (dimensions.length === 0) { + console.log("Must not reach here.") + plotly.react(plotDomId, [], layout) + return + } + let reversescale = false + if ( + targets[0].kind === "objective" && + (targets[0].getObjectiveId() as number) < study.directions.length && + study.directions[targets[0].getObjectiveId() as number] === "maximize" + ) { + reversescale = true + } const plotData: Partial[] = [ { type: "parcoords", @@ -176,7 +162,7 @@ const plotCoordinate = ( // @ts-ignore colorscale: "Blues", colorbar: { - title: target.toLabel(study.objective_names), + title: targets[0].toLabel(study.objective_names), }, showscale: true, reversescale: reversescale, diff --git a/optuna_dashboard/ts/trialFilter.ts b/optuna_dashboard/ts/trialFilter.ts index 73d28258..e3424956 100644 --- a/optuna_dashboard/ts/trialFilter.ts +++ b/optuna_dashboard/ts/trialFilter.ts @@ -154,7 +154,7 @@ export const useParamTargets = ( return [targetList, selectedTarget, setTargetIdent] } -export const useObjectiveAndSystemAttrTargets = ( +export const useObjectiveAndUserAttrTargets = ( study: StudyDetail | null ): [Target[], Target, (ident: string) => void] => { const defaultTarget = new Target("objective", 0) From 4d6d1a1ef8b5f1b9f0523af9a952fce3cfd0ec2f Mon Sep 17 00:00:00 2001 From: c-bata Date: Sat, 7 Jan 2023 18:22:04 +0900 Subject: [PATCH 08/10] Add Checkboxes in ParallelCoordinate --- .../ts/components/GraphParallelCoordinate.tsx | 130 ++++++++++++------ 1 file changed, 89 insertions(+), 41 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx index 4b762efe..296becd1 100644 --- a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx +++ b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx @@ -1,6 +1,14 @@ import * as plotly from "plotly.js-dist-min" -import React, { FC, useEffect } from "react" -import { Typography, useTheme, Box, Grid } from "@mui/material" +import React, { FC, ReactNode, useEffect, useState } from "react" +import { + Typography, + useTheme, + Box, + Grid, + FormGroup, + FormControlLabel, + Checkbox, +} from "@mui/material" import { plotlyDarkTemplate } from "./PlotlyDarkMode" import { Target, @@ -11,19 +19,59 @@ import { const plotDomId = "graph-parallel-coordinate" -const useTargets = (study: StudyDetail | null): Target[] => { +const useTargets = (study: StudyDetail | null): [Target[], () => ReactNode] => { const [targets1, _target1, _setter1] = useObjectiveAndUserAttrTargets(study) const [targets2, _target2, _setter2] = useParamTargets( study?.intersection_search_space || [] ) - return [...targets1, ...targets2] + const [checked, setChecked] = useState([true]) + + const allTargets = [...targets1, ...targets2] + useEffect(() => { + if (allTargets.length !== checked.length) { + setChecked(allTargets.map((_) => true)) + } + }, [allTargets]) + + const handleOnChange = (event: React.ChangeEvent) => { + setChecked( + checked.map((c, i) => + i.toString() === event.target.name ? event.target.checked : c + ) + ) + } + + const renderCheckBoxes = (): ReactNode => ( + + {allTargets.map((t, i) => { + return ( + i ? checked[i] : true} + onChange={handleOnChange} + name={i.toString()} + /> + } + label={t.toLabel(study?.objective_names)} + /> + ) + })} + + ) + + const targets = allTargets.filter((t, i) => + checked.length > i ? checked[i] : true + ) + return [targets, renderCheckBoxes] } export const GraphParallelCoordinate: FC<{ study: StudyDetail | null }> = ({ study = null }) => { const theme = useTheme() - const targets = useTargets(study) + const [targets, renderCheckBoxes] = useTargets(study) const trials = useFilteredTrials(study, targets, false, false) useEffect(() => { @@ -42,12 +90,13 @@ export const GraphParallelCoordinate: FC<{ sx={{ paddingRight: theme.spacing(2), display: "flex", - flexDirection: "row", + flexDirection: "column", }} > Parallel Coordinate + {renderCheckBoxes()} @@ -75,7 +124,7 @@ const plotCoordinate = ( }, template: mode === "dark" ? plotlyDarkTemplate : {}, } - if (trials.length === 0) { + if (trials.length === 0 || targets.length === 0) { plotly.react(plotDomId, [], layout) return } @@ -98,46 +147,45 @@ const plotCoordinate = ( .join("") } - const dimensions = targets - .map((target) => { - if (target.kind === "objective" || target.kind === "user_attr") { - const values: number[] = trials.map( - (t) => target.getTargetValue(t) as number - ) + const dimensions = targets.map((target) => { + if (target.kind === "objective" || target.kind === "user_attr") { + const values: number[] = trials.map( + (t) => target.getTargetValue(t) as number + ) + return { + label: target.toLabel(study.objective_names), + values: values, + range: [Math.min(...values), Math.max(...values)], + } + } else { + const s = study.intersection_search_space.find( + (s) => s.name === target.key + ) as SearchSpaceItem // Must be already filtered. + + const values: number[] = trials.map( + (t) => target.getTargetValue(t) as number + ) + if (s.distribution.type !== "CategoricalDistribution") { return { - label: target.toLabel(study.objective_names), + label: breakLabelIfTooLong(s.name), values: values, - range: [Math.min(...values), Math.max(...values)], + range: [s.distribution.low, s.distribution.high], } } else { - const s = study.intersection_search_space.find( - (s) => s.name === target.key - ) as SearchSpaceItem // Must be already filtered. - - const values: number[] = trials.map( - (t) => target.getTargetValue(t) as number - ) - if (s.distribution.type !== "CategoricalDistribution") { - return { - label: breakLabelIfTooLong(s.name), - values: values, - range: [s.distribution.low, s.distribution.high], - } - } else { - // categorical - const vocabArr: string[] = s.distribution.choices.map((c) => c.value) - const tickvals: number[] = vocabArr.map((v, i) => i) - return { - label: breakLabelIfTooLong(s.name), - values: values, - range: [0, s.distribution.choices.length - 1], - // @ts-ignore - tickvals: tickvals, - ticktext: vocabArr, - } + // categorical + const vocabArr: string[] = s.distribution.choices.map((c) => c.value) + const tickvals: number[] = vocabArr.map((v, i) => i) + return { + label: breakLabelIfTooLong(s.name), + values: values, + range: [0, s.distribution.choices.length - 1], + // @ts-ignore + tickvals: tickvals, + ticktext: vocabArr, } } - }) + } + }) if (dimensions.length === 0) { console.log("Must not reach here.") plotly.react(plotDomId, [], layout) From ec7d24f9dbb4e95045e21f7e597a6b47d5bbeb88 Mon Sep 17 00:00:00 2001 From: c-bata Date: Sat, 7 Jan 2023 18:45:19 +0900 Subject: [PATCH 09/10] Plot EDF for every objectives by default --- optuna_dashboard/ts/components/GraphEdf.tsx | 45 ++++++++++++++++--- .../ts/components/StudyDetailBeta.tsx | 20 ++++++--- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphEdf.tsx b/optuna_dashboard/ts/components/GraphEdf.tsx index db92bde3..0a341619 100644 --- a/optuna_dashboard/ts/components/GraphEdf.tsx +++ b/optuna_dashboard/ts/components/GraphEdf.tsx @@ -1,5 +1,5 @@ import * as plotly from "plotly.js-dist-min" -import React, { FC, useEffect } from "react" +import React, { FC, useEffect, useMemo } from "react" import { Grid, FormControl, @@ -15,6 +15,34 @@ import { plotlyDarkTemplate } from "./PlotlyDarkMode" import { Target, useFilteredTrials, useObjectiveTargets } from "../trialFilter" const plotDomId = "graph-edf" +const getPlotDomId = (objectiveId: number) => `graph-edf-${objectiveId}` + +export const GraphEdfBeta: FC<{ + study: StudyDetail | null + objectiveId: number +}> = ({ study, objectiveId }) => { + const theme = useTheme() + const domId = getPlotDomId(objectiveId) + const target = useMemo( + () => new Target("objective", objectiveId), + [objectiveId] + ) + const trials = useFilteredTrials(study, [target], false, false) + + useEffect(() => { + if (study !== null) { + plotEdf(trials, target, domId, theme.palette.mode) + } + }, [trials, target, domId, theme.palette.mode]) + return ( + + + {`EDF for ${target.toLabel(study?.objective_names)}`} + + + + ) +} export const GraphEdf: FC<{ study: StudyDetail | null @@ -29,7 +57,7 @@ export const GraphEdf: FC<{ useEffect(() => { if (study != null) { - plotEdf(trials, selected, theme.palette.mode) + plotEdf(trials, selected, plotDomId, theme.palette.mode) } }, [trials, selected, theme.palette.mode]) return ( @@ -67,12 +95,17 @@ export const GraphEdf: FC<{ ) } -const plotEdf = (trials: Trial[], target: Target, mode: string) => { - if (document.getElementById(plotDomId) === null) { +const plotEdf = ( + trials: Trial[], + target: Target, + domId: string, + mode: string +) => { + if (document.getElementById(domId) === null) { return } if (trials.length === 0) { - plotly.react(plotDomId, [], { + plotly.react(domId, [], { template: mode === "dark" ? plotlyDarkTemplate : {}, }) return @@ -117,5 +150,5 @@ const plotEdf = (trials: Trial[], target: Target, mode: string) => { y: yValues, }, ] - plotly.react(plotDomId, plotData, layout) + plotly.react(domId, plotData, layout) } diff --git a/optuna_dashboard/ts/components/StudyDetailBeta.tsx b/optuna_dashboard/ts/components/StudyDetailBeta.tsx index 6c753dc1..9e2f8f73 100644 --- a/optuna_dashboard/ts/components/StudyDetailBeta.tsx +++ b/optuna_dashboard/ts/components/StudyDetailBeta.tsx @@ -32,7 +32,7 @@ import { GraphSlice } from "./GraphSlice" import { GraphParetoFront } from "./GraphParetoFront" import { DataGrid, DataGridColumn } from "./DataGrid" import { GraphIntermediateValues } from "./GraphIntermediateValues" -import { GraphEdf } from "./GraphEdf" +import { GraphEdfBeta } from "./GraphEdf" import { TrialList } from "./TrialList" import { BestTrialsCard } from "./BestTrialsCard" @@ -173,11 +173,19 @@ export const StudyDetailBeta: FC<{ Empirical Distribution of the Objective Value - - - - - + + {studyDetail !== null + ? studyDetail.directions.map((d, i) => ( + + + + + + + + )) + : null} + ) } else if (page === "trialTable") { From 23f85e8e00a0396ed1726f566e877b4ec0127fba Mon Sep 17 00:00:00 2001 From: c-bata Date: Sat, 7 Jan 2023 19:18:51 +0900 Subject: [PATCH 10/10] Use merged union search space in ParallelCoordinate --- .../ts/components/GraphParallelCoordinate.tsx | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx index 296becd1..7b953906 100644 --- a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx +++ b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx @@ -16,20 +16,34 @@ import { useObjectiveAndUserAttrTargets, useParamTargets, } from "../trialFilter" +import { useMergedUnionSearchSpace } from "../searchSpace" const plotDomId = "graph-parallel-coordinate" -const useTargets = (study: StudyDetail | null): [Target[], () => ReactNode] => { +const useTargets = ( + study: StudyDetail | null +): [Target[], SearchSpaceItem[], () => ReactNode] => { const [targets1, _target1, _setter1] = useObjectiveAndUserAttrTargets(study) - const [targets2, _target2, _setter2] = useParamTargets( - study?.intersection_search_space || [] - ) + const searchSpace = useMergedUnionSearchSpace(study?.union_search_space) + const [targets2, _target2, _setter2] = useParamTargets(searchSpace) const [checked, setChecked] = useState([true]) const allTargets = [...targets1, ...targets2] useEffect(() => { if (allTargets.length !== checked.length) { - setChecked(allTargets.map((_) => true)) + setChecked( + allTargets.map((t) => { + if (t.kind !== "params" || study === null) { + return true + } + // By default, params that is not included in intersection search space should be disabled, + // otherwise all trials are filtered. + return ( + study.intersection_search_space.find((s) => s.name === t.key) !== + undefined + ) + }) + ) } }, [allTargets]) @@ -64,21 +78,21 @@ const useTargets = (study: StudyDetail | null): [Target[], () => ReactNode] => { const targets = allTargets.filter((t, i) => checked.length > i ? checked[i] : true ) - return [targets, renderCheckBoxes] + return [targets, searchSpace, renderCheckBoxes] } export const GraphParallelCoordinate: FC<{ study: StudyDetail | null }> = ({ study = null }) => { const theme = useTheme() - const [targets, renderCheckBoxes] = useTargets(study) + const [targets, searchSpace, renderCheckBoxes] = useTargets(study) const trials = useFilteredTrials(study, targets, false, false) useEffect(() => { if (study !== null) { - plotCoordinate(study, trials, targets, theme.palette.mode) + plotCoordinate(study, trials, targets, searchSpace, theme.palette.mode) } - }, [study, trials, targets, theme.palette.mode]) + }, [study, trials, targets, searchSpace, theme.palette.mode]) return ( @@ -109,6 +123,7 @@ const plotCoordinate = ( study: StudyDetail, trials: Trial[], targets: Target[], + searchSpace: SearchSpaceItem[], mode: string ) => { if (document.getElementById(plotDomId) === null) { @@ -158,7 +173,7 @@ const plotCoordinate = ( range: [Math.min(...values), Math.max(...values)], } } else { - const s = study.intersection_search_space.find( + const s = searchSpace.find( (s) => s.name === target.key ) as SearchSpaceItem // Must be already filtered.