Add multi-objective support

This commit is contained in:
Cheng Huzi
2021-06-03 22:25:00 -04:00
parent 3d7b3bcf0f
commit f87cebb46b
6 changed files with 260 additions and 82 deletions
+18 -8
View File
@@ -214,19 +214,29 @@ def create_app(storage: BaseStorage) -> Bottle:
@app.get("/api/studies/<study_id:int>/param_importances")
@handle_json_api_exception
def get_param_importances(study_id: int) -> BottleViewReturn:
# TODO(chenghuzi): add support for selecting params and targets via query parameters.
def get_param_importances(study_id: int,) -> BottleViewReturn:
# TODO(chenghuzi): add support for selecting params via query parameters.
response.content_type = "application/json"
study_name = storage.get_study_name_from_id(study_id)
study = Study(study_name=study_name, storage=storage)
objective_id = int(request.params.get("objective_id", 0))
try:
study_name = storage.get_study_name_from_id(study_id)
study = Study(study_name=study_name, storage=storage)
except KeyError:
response.status = 404 # Not found
return {"reason": f"study_id={study_id} is not found"}
trials = [trial for trial in study.trials if trial.state == TrialState.COMPLETE]
n_directions = len(study.directions)
if objective_id >= n_directions:
response.status = 400 # Bad request
return {"reason": f"study_id={study_id} has only {n_directions} direction(s)."}
completed_trials = [trial for trial in study.trials if trial.state == TrialState.COMPLETE]
evaluator = None
params = None
target = None
if len(trials) > 0:
if len(completed_trials) > 0:
importances = optuna.importance.get_param_importances(
study, evaluator=evaluator, params=params, target=target
study, evaluator=evaluator, params=params, target=lambda t: t.values[objective_id]
)
else:
importances = {}
+7 -2
View File
@@ -179,12 +179,17 @@ interface ParamImportancesResponse {
}
export const getParamImportances = (
studyId: number
studyId: number,
objectiveId: number = 0
): Promise<ParamImportances> => {
return axiosInstance
.get<ParamImportancesResponse>(
`/api/studies/${studyId}/param_importances`,
{}
{
params: {
objective_id: objectiveId,
},
}
)
.then((res) => {
return res.data
+72 -22
View File
@@ -1,36 +1,94 @@
import * as plotly from "plotly.js-dist"
import React, { FC, useEffect } from "react"
import React, { FC, useEffect, useState } from "react"
import {
Grid,
FormControl,
FormLabel,
MenuItem,
Select,
Typography,
} from "@material-ui/core"
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"
const useStyles = makeStyles((theme: Theme) =>
createStyles({
title: {
margin: "1em 0",
},
formControl: {
marginBottom: theme.spacing(2),
marginRight: theme.spacing(5),
},
})
)
const plotDomId = "graph-edf"
export const Edf: FC<{
trials: Trial[]
}> = ({ trials = [] }) => {
study: StudyDetail | null
}> = ({ study = null }) => {
const classes = useStyles()
const [objectiveId, setObjectiveId] = useState<number>(0)
const handleObjectiveChange = (
event: React.ChangeEvent<{ value: unknown }>
) => {
setObjectiveId(event.target.value as number)
}
useEffect(() => {
plotEdf(trials) // TODO(chenghuzi): Support multi-objective studies.
}, [trials])
return <div id={plotDomId} />
if (study != null) {
plotEdf(study, objectiveId)
}
}, [study, objectiveId])
return (
<Grid container direction="row">
<Grid item xs={3}>
<Grid container direction="column">
<Typography variant="h6" className={classes.title}>
EDF
</Typography>
{study !== null && study.directions.length !== 1 ? (
<FormControl component="fieldset" className={classes.formControl}>
<FormLabel component="legend">Objective ID:</FormLabel>
<Select value={objectiveId} onChange={handleObjectiveChange}>
{study.directions.map((d, i) => (
<MenuItem value={i} key={i}>
{i}
</MenuItem>
))}
</Select>
</FormControl>
) : null}
</Grid>
</Grid>
<Grid item xs={9}>
<div id={plotDomId} />
</Grid>
</Grid>
)
}
const plotEdf = (trials: Trial[]) => {
// Notice that this implementation is only for single study case
// as it's designed for single study details.
const plotEdf = (study: StudyDetail, objectiveId: number) => {
if (document.getElementById(plotDomId) === null) {
return
}
if (trials.length === 0) {
const trials: Trial[] = study ? study.trials : []
const completedTrials = trials.filter((t) => t.state === "Complete")
if (completedTrials.length === 0) {
plotly.react(plotDomId, [])
return
}
const target_name = "Objective Value"
const _target = (t: Trial): number => {
return t.values![0]
const target = (t: Trial): number => {
return t.values![objectiveId]
}
const target = _target
const layout: Partial<plotly.Layout> = {
title: "Empirical Distribution Function Plot",
xaxis: {
@@ -46,13 +104,6 @@ const plotEdf = (trials: Trial[]) => {
},
}
const completedTrials = trials.filter((t) => t.state === "Complete")
if (completedTrials.length === 0) {
plotly.react(plotDomId, [])
return
}
const values = completedTrials.map((t) => target(t))
const numValues = values.length
const minX = Math.min(...values)
@@ -75,6 +126,5 @@ const plotEdf = (trials: Trial[]) => {
y: yValues,
},
]
plotly.react(plotDomId, plotData, layout)
}
@@ -1,5 +1,27 @@
import * as plotly from "plotly.js-dist"
import React, { FC, useEffect } from "react"
import React, { FC, useEffect, useState } from "react"
import {
Grid,
FormControl,
FormLabel,
MenuItem,
Select,
Typography,
} from "@material-ui/core"
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"
const useStyles = makeStyles((theme: Theme) =>
createStyles({
title: {
margin: "1em 0",
},
formControl: {
marginBottom: theme.spacing(2),
marginRight: theme.spacing(5),
},
})
)
import { getParamImportances } from "../apiClient"
const plotDomId = "graph-hyperparameter-importances"
@@ -25,18 +47,64 @@ const distributionColors = {
CategoricalDistribution: plotlyColorsSequentialBlues.slice(-4)[0],
}
export const HyperparameterImportances: FC<{
export const GraphHyperparameterImportances: FC<{
study: StudyDetail | null
studyId: number
numOfTrials: number
}> = ({ studyId, numOfTrials = 0 }) => {
}> = ({ study = null, studyId }) => {
const classes = useStyles()
const [objectiveId, setObjectiveId] = useState<number>(0)
const numOfTrials = study?.trials.length || 0
const handleObjectiveChange = (
event: React.ChangeEvent<{ value: unknown }>
) => {
setObjectiveId(event.target.value as number)
}
useEffect(() => {
async function fetchAndPlotParamImportances(studyId: number) {
const paramsImportanceData = await getParamImportances(studyId)
async function fetchAndPlotParamImportances(
studyId: number,
objectiveId: number
) {
const paramsImportanceData = await getParamImportances(
studyId,
objectiveId
)
plotParamImportances(paramsImportanceData)
}
fetchAndPlotParamImportances(studyId)
}, [numOfTrials])
return <div id={plotDomId} />
if (numOfTrials > 0) {
fetchAndPlotParamImportances(studyId, objectiveId)
}
}, [numOfTrials, objectiveId])
return (
<Grid container direction="row">
<Grid item xs={3}>
<Grid container direction="column">
<Typography variant="h6" className={classes.title}>
Hyperparameter Importance
</Typography>
{study !== null && study.directions.length !== 1 ? (
<FormControl component="fieldset" className={classes.formControl}>
<FormLabel component="legend">Objective ID:</FormLabel>
<Select value={objectiveId} onChange={handleObjectiveChange}>
{study.directions.map((d, i) => (
<MenuItem value={i} key={i}>
{i}
</MenuItem>
))}
</Select>
</FormControl>
) : null}
</Grid>
</Grid>
<Grid item xs={9}>
<div id={plotDomId} />
</Grid>
</Grid>
)
}
const plotParamImportances = (paramsImportanceData: ParamImportances) => {
@@ -1,17 +1,74 @@
import * as plotly from "plotly.js-dist"
import React, { FC, useEffect } from "react"
import React, { FC, useEffect, useState } from "react"
import {
Grid,
FormControl,
FormLabel,
MenuItem,
Select,
Typography,
} from "@material-ui/core"
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"
const useStyles = makeStyles((theme: Theme) =>
createStyles({
title: {
margin: "1em 0",
},
formControl: {
marginBottom: theme.spacing(2),
marginRight: theme.spacing(5),
},
})
)
const plotDomId = "graph-parallel-coordinate"
export const GraphParallelCoordinate: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const classes = useStyles()
const [objectiveId, setObjectiveId] = useState<number>(0)
const handleObjectiveChange = (
event: React.ChangeEvent<{ value: unknown }>
) => {
setObjectiveId(event.target.value as number)
}
useEffect(() => {
if (study !== null) {
plotCoordinate(study, 0) // TODO(c-bata): Support multi-objective studies.
plotCoordinate(study, objectiveId)
}
}, [study])
return <div id={plotDomId} />
}, [study, objectiveId])
return (
<Grid container direction="row">
<Grid item xs={3}>
<Grid container direction="column">
<Typography variant="h6" className={classes.title}>
Parallel cooridinate
</Typography>
{study !== null && study.directions.length !== 1 ? (
<FormControl component="fieldset" className={classes.formControl}>
<FormLabel component="legend">Objective ID:</FormLabel>
<Select value={objectiveId} onChange={handleObjectiveChange}>
{study.directions.map((d, i) => (
<MenuItem value={i} key={i}>
{i}
</MenuItem>
))}
</Select>
</FormControl>
) : null}
</Grid>
</Grid>
<Grid item xs={9}>
<div id={plotDomId} />
</Grid>
</Grid>
)
}
const plotCoordinate = (study: StudyDetail, objectiveId: number) => {
@@ -20,7 +20,7 @@ import { Home, Cached } from "@material-ui/icons"
import { DataGridColumn, DataGrid } from "./DataGrid"
import { GraphParallelCoordinate } from "./GraphParallelCoordinate"
import { HyperparameterImportances } from "./HyperparameterImportances"
import { GraphHyperparameterImportances } from "./GraphHyperparameterImportances"
import { Edf } from "./GraphEdf"
import { GraphIntermediateValues } from "./GraphIntermediateValues"
@@ -189,45 +189,33 @@ export const StudyDetail: FC = () => {
</CardContent>
</Card>
) : null}
<Card className={classes.card}>
<CardContent>
<GraphParallelCoordinate study={studyDetail} />
</CardContent>
</Card>
{studyDetail !== null && isSingleObjectiveStudy(studyDetail) ? (
<Grid container direction="row">
<Grid item xs={6}>
<Card className={classes.card}>
<CardContent>
<GraphParallelCoordinate study={studyDetail} />
</CardContent>
</Card>
</Grid>
<Grid item xs={6}>
<Card className={classes.card}>
<CardContent>
<GraphIntermediateValues trials={trials} />
</CardContent>
</Card>
</Grid>
</Grid>
) : null}
{studyDetail !== null && isSingleObjectiveStudy(studyDetail) ? (
<Grid container direction="row">
<Grid item xs={6}>
<Card className={classes.card}>
<CardContent>
<HyperparameterImportances
studyId={studyIdNumber}
numOfTrials={trials.length}
/>
</CardContent>
</Card>
</Grid>
<Grid item xs={6}>
<Card className={classes.card}>
<CardContent>
<Edf trials={trials} />
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card className={classes.card}>
<CardContent>
<GraphIntermediateValues trials={trials} />
</CardContent>
</Card>
</Grid>
) : null}
<Card className={classes.card}>
<CardContent>
<Edf study={studyDetail} />
</CardContent>
</Card>
<Card className={classes.card}>
<CardContent>
<GraphHyperparameterImportances
study={studyDetail}
studyId={studyIdNumber}
/>
</CardContent>
</Card>
{studyDetail !== null ? (
<Card className={classes.card}>
<CardContent>