From 62c7f056e41004a2a677efd08813c8a4ca12104e Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Fri, 10 Jan 2025 14:47:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E8=87=AA=E5=8A=A8=E5=AF=BB=E4=BC=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/config/routes.ts | 36 ++ react-ui/src/pages/AutoML/Create/index.tsx | 5 +- react-ui/src/pages/AutoML/List/index.tsx | 414 +------------- .../components/CreateForm/TrialConfig.tsx | 5 +- .../AutoML/components/CreateForm/index.less | 11 +- .../components/ExperimentInstance/index.tsx | 18 +- .../components/ExperimentList/config.ts | 75 +++ .../ExperimentList}/index.less | 2 +- .../components/ExperimentList/index.tsx | 428 +++++++++++++++ .../pages/HyperParameter/Create/index.less | 55 ++ .../src/pages/HyperParameter/Create/index.tsx | 165 ++++++ .../src/pages/HyperParameter/Info/index.less | 40 ++ .../src/pages/HyperParameter/Info/index.tsx | 61 +++ .../pages/HyperParameter/Instance/index.less | 42 ++ .../pages/HyperParameter/Instance/index.tsx | 215 ++++++++ .../src/pages/HyperParameter/List/index.tsx | 13 + .../components/AutoMLBasic/index.less | 13 + .../components/AutoMLBasic/index.tsx | 308 +++++++++++ .../components/ConfigInfo/index.less | 20 + .../components/ConfigInfo/index.tsx | 26 + .../components/CreateForm/BasicConfig.tsx | 54 ++ .../components/CreateForm/ExecuteConfig.tsx | 504 ++++++++++++++++++ .../CreateForm/ParameterRange/index.less | 13 + .../CreateForm/ParameterRange/index.tsx | 141 +++++ .../CreateForm/PopParameterRange/index.less | 47 ++ .../CreateForm/PopParameterRange/index.tsx | 97 ++++ .../components/CreateForm/index.less | 108 ++++ .../components/CreateForm/utils.ts | 155 ++++++ .../components/ExperimentHistory/index.less | 14 + .../components/ExperimentHistory/index.tsx | 132 +++++ .../components/ExperimentLog/index.less | 0 .../components/ExperimentLog/index.tsx | 0 .../components/ExperimentResult/index.less | 52 ++ .../components/ExperimentResult/index.tsx | 83 +++ react-ui/src/pages/HyperParameter/types.ts | 64 +++ react-ui/src/services/hyperParameter/index.js | 93 ++++ react-ui/src/utils/constant.ts | 3 + 37 files changed, 3084 insertions(+), 428 deletions(-) create mode 100644 react-ui/src/pages/AutoML/components/ExperimentList/config.ts rename react-ui/src/pages/AutoML/{List => components/ExperimentList}/index.less (94%) create mode 100644 react-ui/src/pages/AutoML/components/ExperimentList/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/Create/index.less create mode 100644 react-ui/src/pages/HyperParameter/Create/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/Info/index.less create mode 100644 react-ui/src/pages/HyperParameter/Info/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/Instance/index.less create mode 100644 react-ui/src/pages/HyperParameter/Instance/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/List/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts create mode 100644 react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/types.ts create mode 100644 react-ui/src/services/hyperParameter/index.js create mode 100644 react-ui/src/utils/constant.ts diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index eaddb001..45c65760 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -181,6 +181,42 @@ export default [ }, ], }, + { + name: '超参数自动寻优', + path: 'hyperparameter', + routes: [ + { + name: '超参数寻优', + path: '', + component: './HyperParameter/List/index', + }, + { + name: '实验详情', + path: 'info/:id', + component: './HyperParameter/Info/index', + }, + { + name: '创建实验', + path: 'create', + component: './HyperParameter/Create/index', + }, + { + name: '编辑实验', + path: 'edit/:id', + component: './HyperParameter/Create/index', + }, + { + name: '复制实验', + path: 'copy/:id', + component: './HyperParameter/Create/index', + }, + { + name: '实验实例详情', + path: 'instance/:autoMLId/:id', + component: './HyperParameter/Instance/index', + }, + ], + }, ], }, { diff --git a/react-ui/src/pages/AutoML/Create/index.tsx b/react-ui/src/pages/AutoML/Create/index.tsx index 19fedd19..30699063 100644 --- a/react-ui/src/pages/AutoML/Create/index.tsx +++ b/react-ui/src/pages/AutoML/Create/index.tsx @@ -1,7 +1,7 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 13:58:08 - * @Description: 创建服务版本 + * @Description: 创建实验 */ import PageTitle from '@/components/PageTitle'; import { AutoMLEnsembleClass, AutoMLTaskType } from '@/enums'; @@ -11,7 +11,6 @@ import { safeInvoke } from '@/utils/functional'; import { to } from '@/utils/promise'; import { useLocation, useNavigate, useParams } from '@umijs/max'; import { App, Button, Form } from 'antd'; -import { omit } from 'lodash'; import { useEffect } from 'react'; import BasicConfig from '../components/CreateForm/BasicConfig'; import DatasetConfig from '../components/CreateForm/DatasetConfig'; @@ -106,7 +105,7 @@ function CreateAutoML() { // 根据后台要求,修改表单数据 const object = { - ...omit(formData), + ...formData, include_classifier: convertEmptyStringToUndefined(include_classifier), include_feature_preprocessor: convertEmptyStringToUndefined(include_feature_preprocessor), include_regressor: convertEmptyStringToUndefined(include_regressor), diff --git a/react-ui/src/pages/AutoML/List/index.tsx b/react-ui/src/pages/AutoML/List/index.tsx index 13e3dcbe..a4488e4d 100644 --- a/react-ui/src/pages/AutoML/List/index.tsx +++ b/react-ui/src/pages/AutoML/List/index.tsx @@ -3,419 +3,11 @@ * @Date: 2024-04-16 13:58:08 * @Description: 自主机器学习列表 */ -import KFIcon from '@/components/KFIcon'; -import PageTitle from '@/components/PageTitle'; -import { ExperimentStatus } from '@/enums'; -import { useCacheState } from '@/hooks/pageCacheState'; -import { experimentStatusInfo } from '@/pages/Experiment/status'; -import { - deleteAutoMLReq, - getAutoMLListReq, - getExperimentInsListReq, - runAutoMLReq, -} from '@/services/autoML'; -import themes from '@/styles/theme.less'; -import { type ExperimentInstance as ExperimentInstanceData } from '@/types'; -import { to } from '@/utils/promise'; -import tableCellRender, { TableCellValueType } from '@/utils/table'; -import { modalConfirm } from '@/utils/ui'; -import { useNavigate } from '@umijs/max'; -import { - App, - Button, - ConfigProvider, - Input, - Table, - Tooltip, - type TablePaginationConfig, - type TableProps, -} from 'antd'; -import { type SearchProps } from 'antd/es/input'; -import classNames from 'classnames'; -import { useEffect, useState } from 'react'; -import ExperimentInstance from '../components/ExperimentInstance'; -import { AutoMLData } from '../types'; -import styles from './index.less'; -function AutoMLList() { - const navigate = useNavigate(); - const { message } = App.useApp(); - const [cacheState, setCacheState] = useCacheState(); - const [searchText, setSearchText] = useState(cacheState?.searchText); - const [inputText, setInputText] = useState(cacheState?.searchText); - const [tableData, setTableData] = useState([]); - const [total, setTotal] = useState(0); - const [experimentInsList, setExperimentInsList] = useState([]); - const [expandedRowKeys, setExpandedRowKeys] = useState([]); - const [experimentInsTotal, setExperimentInsTotal] = useState(0); - const [pagination, setPagination] = useState( - cacheState?.pagination ?? { - current: 1, - pageSize: 10, - }, - ); - - useEffect(() => { - getAutoMLList(); - }, [pagination, searchText]); - - // 获取自主机器学习列表 - const getAutoMLList = async () => { - const params: Record = { - page: pagination.current! - 1, - size: pagination.pageSize, - ml_name: searchText || undefined, - }; - const [res] = await to(getAutoMLListReq(params)); - if (res && res.data) { - const { content = [], totalElements = 0 } = res.data; - setTableData(content); - setTotal(totalElements); - } - }; - - // 搜索 - const onSearch: SearchProps['onSearch'] = (value) => { - setSearchText(value); - setPagination((prev) => ({ - ...prev, - current: 1, - })); - }; - - // 删除模型部署 - const deleteAutoML = async (record: AutoMLData) => { - const [res] = await to(deleteAutoMLReq(record.id)); - if (res) { - message.success('删除成功'); - // 如果是一页的唯一数据,删除时,请求第一页的数据 - // 否则直接刷新这一页的数据 - // 避免回到第一页 - if (tableData.length > 1) { - setPagination((prev) => ({ - ...prev, - current: 1, - })); - } else { - getAutoMLList(); - } - } - }; - - // 处理删除 - const handleAutoMLDelete = (record: AutoMLData) => { - modalConfirm({ - title: '删除后,该实验将不可恢复', - content: '是否确认删除?', - onOk: () => { - deleteAutoML(record); - }, - }); - }; - - // 创建、编辑、复制自动机器学习 - const createAutoML = (record?: AutoMLData, isCopy: boolean = false) => { - setCacheState({ - pagination, - searchText, - }); - - if (record) { - if (isCopy) { - navigate(`/pipeline/autoML/copy/${record.id}`); - } else { - navigate(`/pipeline/autoML/edit/${record.id}`); - } - } else { - navigate(`/pipeline/autoML/create`); - } - }; - - // 查看自动机器学习详情 - const gotoDetail = (record: AutoMLData) => { - setCacheState({ - pagination, - searchText, - }); - - navigate(`/pipeline/autoML/info/${record.id}`); - }; - - // 启动自动机器学习 - const startAutoML = async (record: AutoMLData) => { - const [res] = await to(runAutoMLReq(record.id)); - if (res) { - message.success('运行成功'); - setExpandedRowKeys([record.id]); - refreshExperimentList(); - refreshExperimentIns(record.id); - } - }; +import ExperimentList, { ExperimentListType } from '../components/ExperimentList'; - // --------------------------- 实验实例 --------------------------- - // 获取实验实例列表 - const getExperimentInsList = async (autoMLId: number, page: number) => { - const params = { - autoMlId: autoMLId, - page: page, - size: 5, - }; - const [res] = await to(getExperimentInsListReq(params)); - if (res && res.data) { - const { content = [], totalElements = 0 } = res.data; - try { - if (page === 0) { - setExperimentInsList(content); - } else { - setExperimentInsList((prev) => [...prev, ...content]); - } - setExperimentInsTotal(totalElements); - } catch (error) { - console.error('JSON parse error: ', error); - } - } - }; - // 展开实例 - const handleExpandChange = (expanded: boolean, record: AutoMLData) => { - setExperimentInsList([]); - if (expanded) { - setExpandedRowKeys([record.id]); - getExperimentInsList(record.id, 0); - } else { - setExpandedRowKeys([]); - } - }; - - // 跳转到实验实例详情 - const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { - navigate({ pathname: `/pipeline/automl/instance/${autoML.id}/${record.id}` }); - }; - - // 刷新实验实例列表 - const refreshExperimentIns = (experimentId: number) => { - getExperimentInsList(experimentId, 0); - }; - - // 加载更多实验实例 - const loadMoreExperimentIns = () => { - const page = Math.round(experimentInsList.length / 5); - const autoMLId = expandedRowKeys[0]; - getExperimentInsList(autoMLId, page); - }; - - // 实验实例终止 - const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { - // 刷新实验列表 - refreshExperimentList(); - setExperimentInsList((prevList) => { - return prevList.map((item) => { - if (item.id === experimentIns.id) { - return { - ...item, - status: ExperimentStatus.Terminated, - }; - } - return item; - }); - }); - }; - - // 刷新实验列表状态, - // 目前是直接刷新实验列表,后续需要优化,只刷新状态 - const refreshExperimentList = () => { - getAutoMLList(); - }; - - // --------------------------- Table --------------------------- - // 分页切换 - const handleTableChange: TableProps['onChange'] = ( - pagination, - _filters, - _sorter, - { action }, - ) => { - if (action === 'paginate') { - setPagination(pagination); - } - }; - - const columns: TableProps['columns'] = [ - { - title: '实验名称', - dataIndex: 'ml_name', - key: 'ml_name', - width: '16%', - render: tableCellRender(false, TableCellValueType.Link, { - onClick: gotoDetail, - }), - }, - { - title: '实验描述', - dataIndex: 'ml_description', - key: 'ml_description', - render: tableCellRender(true), - ellipsis: { showTitle: false }, - }, - - { - title: '创建时间', - dataIndex: 'update_time', - key: 'update_time', - width: '20%', - render: tableCellRender(true, TableCellValueType.Date), - ellipsis: { showTitle: false }, - }, - { - title: '最近五次运行状态', - dataIndex: 'status_list', - key: 'status_list', - width: 200, - render: (text) => { - const newText: string[] = text && text.replace(/\s+/g, '').split(','); - return ( - <> - {newText && newText.length > 0 - ? newText.map((item, index) => { - return ( - - - - ); - }) - : null} - - ); - }, - }, - { - title: '操作', - dataIndex: 'operation', - width: 360, - key: 'operation', - render: (_: any, record: AutoMLData) => ( -
- - - - - - - -
- ), - }, - ]; - - return ( -
- -
-
- setInputText(e.target.value)} - style={{ width: 300 }} - value={inputText} - allowClear - /> - -
-
- `共${total}条`, - }} - onChange={handleTableChange} - expandable={{ - expandedRowRender: (record) => ( - gotoInstanceInfo(record, item)} - onRemove={() => { - refreshExperimentIns(record.id); - refreshExperimentList(); - }} - onTerminate={handleInstanceTerminate} - onLoadMore={() => loadMoreExperimentIns()} - > - ), - onExpand: (e, a) => { - handleExpandChange(e, a); - }, - expandedRowKeys: expandedRowKeys, - rowExpandable: () => true, - }} - rowKey="id" - /> - - - - ); +function AutoMLList() { + return ; } export default AutoMLList; diff --git a/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx index f0fdfa5d..0d8008fb 100644 --- a/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx +++ b/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx @@ -62,10 +62,7 @@ function TrialConfig() { > - + + + + + + + + + ), + }, + ]; + + return ( +
+ +
+
+ setInputText(e.target.value)} + style={{ width: 300 }} + value={inputText} + allowClear + /> + +
+
+
`共${total}条`, + }} + onChange={handleTableChange} + expandable={{ + expandedRowRender: (record) => ( + gotoInstanceInfo(record, item)} + onRemove={() => { + refreshExperimentIns(record.id); + refreshExperimentList(); + }} + onTerminate={handleInstanceTerminate} + onLoadMore={() => loadMoreExperimentIns()} + > + ), + onExpand: (e, a) => { + handleExpandChange(e, a); + }, + expandedRowKeys: expandedRowKeys, + rowExpandable: () => true, + }} + rowKey="id" + /> + + + + ); +} + +export default ExperimentList; diff --git a/react-ui/src/pages/HyperParameter/Create/index.less b/react-ui/src/pages/HyperParameter/Create/index.less new file mode 100644 index 00000000..0325570e --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Create/index.less @@ -0,0 +1,55 @@ +.create-hyperparameter { + height: 100%; + + &__content { + height: calc(100% - 60px); + margin-top: 10px; + padding: 30px 30px 10px; + overflow: auto; + color: @text-color; + font-size: @font-size-content; + background-color: white; + border-radius: 10px; + + &__type { + color: @text-color; + font-size: @font-size-input-lg; + } + + :global { + .ant-input-number { + width: 100%; + } + + .ant-form-item { + margin-bottom: 20px; + } + + .image-url { + margin-top: -15px; + .ant-form-item-label > label::after { + content: ''; + } + } + + .ant-btn-variant-text:disabled { + color: rgba(0, 0, 0, 0.25); + } + + .ant-btn-variant-text { + color: #565658; + } + + .ant-btn.ant-btn-icon-only .anticon { + font-size: 20px; + } + + .anticon-question-circle { + margin-top: -12px; + margin-left: 1px !important; + color: @text-color-tertiary !important; + font-size: 12px !important; + } + } + } +} diff --git a/react-ui/src/pages/HyperParameter/Create/index.tsx b/react-ui/src/pages/HyperParameter/Create/index.tsx new file mode 100644 index 00000000..dfe367ba --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Create/index.tsx @@ -0,0 +1,165 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 创建实验 + */ +import PageTitle from '@/components/PageTitle'; +import { addRayReq, getRayInfoReq, updateRayReq } from '@/services/hyperParameter'; +import { safeInvoke } from '@/utils/functional'; +import { to } from '@/utils/promise'; +import { useLocation, useNavigate, useParams } from '@umijs/max'; +import { App, Button, Form } from 'antd'; +import { useEffect } from 'react'; +import BasicConfig from '../components/CreateForm/BasicConfig'; +import ExecuteConfig from '../components/CreateForm/ExecuteConfig'; +import { getReqParamName } from '../components/CreateForm/utils'; +import { FormData, HyperparameterData } from '../types'; +import styles from './index.less'; + +function CreateHyperparameter() { + const navigate = useNavigate(); + const [form] = Form.useForm(); + const { message } = App.useApp(); + const params = useParams(); + const id = safeInvoke(Number)(params.id); + const { pathname } = useLocation(); + const isCopy = pathname.includes('copy'); + + useEffect(() => { + // 编辑,复制 + if (id && !Number.isNaN(id)) { + getHyperparameterInfo(id); + } + }, [id]); + + // 获取服务详情 + const getHyperparameterInfo = async (id: number) => { + const [res] = await to(getRayInfoReq({ id })); + if (res && res.data) { + const info: HyperparameterData = res.data; + const { name: name_str, parameters, points_to_evaluate, ...rest } = info; + const name = isCopy ? `${name_str}-copy` : name_str; + if (parameters && Array.isArray(parameters)) { + parameters.forEach((item) => { + item.range = item.bounds || item.values || item.value; + delete item.bounds; + delete item.values; + delete item.value; + }); + } + + const formData = { + ...rest, + name, + parameters, + points_to_evaluate: points_to_evaluate ?? [undefined], + }; + + form.setFieldsValue(formData); + } + }; + + // 创建、更新、复制实验 + const createExperiment = async (formData: FormData) => { + // 按后台接口要求,修改参数表单数据结构,将 "value" 参数改为 "bounds"/"values"/"value" + const parameters = formData['parameters']; + // const points_to_evaluate = formData['points_to_evaluate']; + // const runParameters = formData['parameters']; + parameters.forEach((item) => { + const paramName = getReqParamName(item.type); + item[paramName] = item.range; + delete item.range; + }); + + // 根据后台要求,修改表单数据 + const object = { + ...formData, + parameters: parameters, + }; + + const params = + id && !isCopy + ? { + id: id, + ...object, + } + : object; + + const request = id && !isCopy ? updateRayReq : addRayReq; + const [res] = await to(request(params)); + if (res) { + message.success('操作成功'); + navigate(-1); + } + }; + + // 提交 + const handleSubmit = (values: FormData) => { + createExperiment(values); + }; + + // 取消 + const cancel = () => { + navigate(-1); + }; + + let buttonText = '新建'; + let title = '新建实验'; + if (id) { + if (isCopy) { + title = '复制实验'; + buttonText = '确定'; + } else { + title = '编辑实验'; + buttonText = '更新'; + } + } + + return ( +
+ +
+
+
+ + + + + + + + +
+
+
+ ); +} + +export default CreateHyperparameter; diff --git a/react-ui/src/pages/HyperParameter/Info/index.less b/react-ui/src/pages/HyperParameter/Info/index.less new file mode 100644 index 00000000..e27756ef --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Info/index.less @@ -0,0 +1,40 @@ +.auto-ml-info { + position: relative; + height: 100%; + &__tabs { + height: 50px; + padding-left: 25px; + background-image: url(@/assets/img/page-title-bg.png); + background-repeat: no-repeat; + background-position: top center; + background-size: 100% 100%; + } + + &__content { + height: calc(100% - 60px); + margin-top: 10px; + } + + &__tips { + position: absolute; + top: 11px; + left: 256px; + padding: 3px 12px; + color: #565658; + font-size: @font-size-content; + background: .addAlpha(@primary-color, 0.09) []; + border-radius: 4px; + + &::before { + position: absolute; + top: 10px; + left: -6px; + width: 0; + height: 0; + border-top: 4px solid transparent; + border-right: 6px solid .addAlpha(@primary-color, 0.09) []; + border-bottom: 4px solid transparent; + content: ''; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/Info/index.tsx b/react-ui/src/pages/HyperParameter/Info/index.tsx new file mode 100644 index 00000000..8b2fded3 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Info/index.tsx @@ -0,0 +1,61 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 自主机器学习详情 + */ +import KFIcon from '@/components/KFIcon'; +import PageTitle from '@/components/PageTitle'; +import { CommonTabKeys } from '@/enums'; +import { getAutoMLInfoReq } from '@/services/autoML'; +import { safeInvoke } from '@/utils/functional'; +import { to } from '@/utils/promise'; +import { useParams } from '@umijs/max'; +import { useEffect, useState } from 'react'; +import AutoMLBasic from '../components/AutoMLBasic'; +import { HyperparameterData } from '../types'; +import styles from './index.less'; + +function AutoMLInfo() { + const [activeTab, setActiveTab] = useState(CommonTabKeys.Public); + const params = useParams(); + const autoMLId = safeInvoke(Number)(params.id); + const [autoMLInfo, setAutoMLInfo] = useState(undefined); + + const tabItems = [ + { + key: CommonTabKeys.Public, + label: '基本信息', + icon: , + }, + { + key: CommonTabKeys.Private, + label: 'Trial列表', + icon: , + }, + ]; + + useEffect(() => { + if (autoMLId) { + getAutoMLInfo(); + } + }, []); + + // 获取自动机器学习详情 + const getAutoMLInfo = async () => { + const [res] = await to(getAutoMLInfoReq({ id: autoMLId })); + if (res && res.data) { + setAutoMLInfo(res.data); + } + }; + + return ( +
+ +
+ +
+
+ ); +} + +export default AutoMLInfo; diff --git a/react-ui/src/pages/HyperParameter/Instance/index.less b/react-ui/src/pages/HyperParameter/Instance/index.less new file mode 100644 index 00000000..889faeb5 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Instance/index.less @@ -0,0 +1,42 @@ +.auto-ml-instance { + height: 100%; + + &__tabs { + height: 100%; + :global { + .ant-tabs-nav-list { + width: 100%; + height: 50px; + padding-left: 15px; + background-image: url(@/assets/img/page-title-bg.png); + background-repeat: no-repeat; + background-position: top center; + background-size: 100% 100%; + } + + .ant-tabs-content-holder { + height: calc(100% - 50px); + .ant-tabs-content { + height: 100%; + .ant-tabs-tabpane { + height: 100%; + } + } + } + } + } + + &__basic { + height: calc(100% - 10px); + margin-top: 10px; + } + + &__log { + height: calc(100% - 10px); + margin-top: 10px; + padding: 20px calc(@content-padding - 8px); + overflow-y: visible; + background-color: white; + border-radius: 10px; + } +} diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx new file mode 100644 index 00000000..355ced01 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -0,0 +1,215 @@ +import KFIcon from '@/components/KFIcon'; +import { AutoMLTaskType, ExperimentStatus } from '@/enums'; +import LogList from '@/pages/Experiment/components/LogList'; +import { getExperimentInsReq } from '@/services/autoML'; +import { NodeStatus } from '@/types'; +import { parseJsonText } from '@/utils'; +import { safeInvoke } from '@/utils/functional'; +import { to } from '@/utils/promise'; +import { useParams } from '@umijs/max'; +import { Tabs } from 'antd'; +import { useEffect, useRef, useState } from 'react'; +import AutoMLBasic from '../components/AutoMLBasic'; +import ExperimentHistory from '../components/ExperimentHistory'; +import ExperimentResult from '../components/ExperimentResult'; +import { AutoMLInstanceData, HyperparameterData } from '../types'; +import styles from './index.less'; + +enum TabKeys { + Params = 'params', + Log = 'log', + Result = 'result', + History = 'history', +} + +function AutoMLInstance() { + const [activeTab, setActiveTab] = useState(TabKeys.Params); + const [autoMLInfo, setAutoMLInfo] = useState(undefined); + const [instanceInfo, setInstanceInfo] = useState(undefined); + const params = useParams(); + // const autoMLId = safeInvoke(Number)(params.autoMLId); + const instanceId = safeInvoke(Number)(params.id); + const evtSourceRef = useRef(null); + + useEffect(() => { + if (instanceId) { + getExperimentInsInfo(false); + } + return () => { + closeSSE(); + }; + }, []); + + // 获取实验实例详情 + const getExperimentInsInfo = async (isStatusDetermined: boolean) => { + const [res] = await to(getExperimentInsReq(instanceId)); + if (res && res.data) { + const info = res.data as AutoMLInstanceData; + const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; + // 解析配置参数 + const paramJson = parseJsonText(param); + if (paramJson) { + setAutoMLInfo(paramJson); + } + + // 这个接口返回的状态有延时,SSE 返回的状态是最新的 + // SSE 调用时,不需要解析 node_status, 也不要重新建立 SSE + if (isStatusDetermined) { + setInstanceInfo((prev) => ({ + ...info, + nodeStatus: prev!.nodeStatus, + })); + return; + } + + // 进行节点状态 + const nodeStatusJson = parseJsonText(node_status); + if (nodeStatusJson) { + Object.keys(nodeStatusJson).forEach((key) => { + if (key.startsWith('auto-ml')) { + const value = nodeStatusJson[key]; + info.nodeStatus = value; + } + }); + } + setInstanceInfo(info); + // 运行中或者等待中,开启 SSE + if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { + setupSSE(argo_ins_name, argo_ins_ns); + } + } + }; + + const setupSSE = (name: string, namespace: string) => { + let { origin } = location; + if (process.env.NODE_ENV === 'development') { + origin = 'http://172.20.32.181:31213'; + } + const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); + const evtSource = new EventSource( + `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, + { withCredentials: false }, + ); + evtSource.onmessage = (event) => { + const data = event?.data; + if (!data) { + return; + } + const dataJson = parseJsonText(data); + if (dataJson) { + const nodes = dataJson?.result?.object?.status?.nodes; + if (nodes) { + const statusData = Object.values(nodes).find((node: any) => + node.displayName.startsWith('auto-ml'), + ) as NodeStatus; + if (statusData) { + setInstanceInfo((prev) => ({ + ...prev!, + nodeStatus: statusData, + })); + + // 实验结束,关闭 SSE + if ( + statusData.phase !== ExperimentStatus.Pending && + statusData.phase !== ExperimentStatus.Running + ) { + closeSSE(); + getExperimentInsInfo(true); + } + } + } + } + }; + evtSource.onerror = (error) => { + console.error('SSE error: ', error); + }; + + evtSourceRef.current = evtSource; + }; + + const closeSSE = () => { + if (evtSourceRef.current) { + evtSourceRef.current.close(); + evtSourceRef.current = null; + } + }; + + const basicTabItems = [ + { + key: TabKeys.Params, + label: '基本信息', + icon: , + children: ( + + ), + }, + { + key: TabKeys.Log, + label: '日志', + icon: , + children: ( +
+ {instanceInfo && instanceInfo.nodeStatus && ( + + )} +
+ ), + }, + ]; + + const resultTabItems = [ + { + key: TabKeys.Result, + label: '实验结果', + icon: , + children: ( + + ), + }, + { + key: TabKeys.History, + label: 'Trial 列表', + icon: , + children: ( + + ), + }, + ]; + + const tabItems = + instanceInfo?.status === ExperimentStatus.Succeeded + ? [...basicTabItems, ...resultTabItems] + : basicTabItems; + + return ( +
+ +
+ ); +} + +export default AutoMLInstance; diff --git a/react-ui/src/pages/HyperParameter/List/index.tsx b/react-ui/src/pages/HyperParameter/List/index.tsx new file mode 100644 index 00000000..5ebfcde9 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/List/index.tsx @@ -0,0 +1,13 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 超参数自动寻优 + */ + +import ExperimentList, { ExperimentListType } from '@/pages/AutoML/components/ExperimentList'; + +function HyperParameter() { + return ; +} + +export default HyperParameter; diff --git a/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less b/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less new file mode 100644 index 00000000..cbd05bcc --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less @@ -0,0 +1,13 @@ +.auto-ml-basic { + height: 100%; + padding: 20px @content-padding; + overflow-y: auto; + background-color: white; + border-radius: 10px; + + :global { + .kf-basic-info__item__value__text { + white-space: pre; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.tsx new file mode 100644 index 00000000..854c6035 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.tsx @@ -0,0 +1,308 @@ +import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums'; +import { AutoMLData } from '@/pages/AutoML/types'; +import { experimentStatusInfo } from '@/pages/Experiment/status'; +import { type NodeStatus } from '@/types'; +import { parseJsonText } from '@/utils'; +import { elapsedTime } from '@/utils/date'; +import { Flex } from 'antd'; +import classNames from 'classnames'; +import { useMemo } from 'react'; +import ConfigInfo, { + formatBoolean, + formatDate, + formatEnum, + type BasicInfoData, +} from '../ConfigInfo'; +import styles from './index.less'; + +// 格式化数据集 +const formatDataset = (dataset: { name: string; version: string }) => { + if (!dataset || !dataset.name || !dataset.version) { + return '--'; + } + return `${dataset.name}:${dataset.version}`; +}; + +// 格式化优化方向 +const formatOptimizeMode = (value: boolean) => { + return value ? '越大越好' : '越小越好'; +}; + +const formatMetricsWeight = (value: string) => { + if (!value) { + return '--'; + } + const json = parseJsonText(value); + if (!json) { + return '--'; + } + return Object.entries(json) + .map(([key, value]) => `${key}:${value}`) + .join('\n'); +}; + +type AutoMLBasicProps = { + info?: AutoMLData; + className?: string; + isInstance?: boolean; + runStatus?: NodeStatus; +}; + +function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLBasicProps) { + const basicDatas: BasicInfoData[] = useMemo(() => { + if (!info) { + return []; + } + + return [ + { + label: '实验名称', + value: info.ml_name, + ellipsis: true, + }, + { + label: '实验描述', + value: info.ml_description, + ellipsis: true, + }, + { + label: '创建人', + value: info.create_by, + ellipsis: true, + }, + { + label: '创建时间', + value: info.create_time, + ellipsis: true, + format: formatDate, + }, + { + label: '更新时间', + value: info.update_time, + ellipsis: true, + format: formatDate, + }, + ]; + }, [info]); + + const configDatas: BasicInfoData[] = useMemo(() => { + if (!info) { + return []; + } + return [ + { + label: '任务类型', + value: info.task_type, + ellipsis: true, + format: formatEnum(autoMLTaskTypeOptions), + }, + { + label: '特征预处理算法', + value: info.include_feature_preprocessor, + ellipsis: true, + }, + { + label: '排除的特征预处理算法', + value: info.exclude_feature_preprocessor, + ellipsis: true, + }, + { + label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', + value: + info.task_type === AutoMLTaskType.Regression + ? info.include_regressor + : info.include_classifier, + ellipsis: true, + }, + { + label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', + value: + info.task_type === AutoMLTaskType.Regression + ? info.exclude_regressor + : info.exclude_classifier, + ellipsis: true, + }, + { + label: '集成方式', + value: info.ensemble_class, + ellipsis: true, + format: formatEnum(autoMLEnsembleClassOptions), + }, + { + label: '集成模型数量', + value: info.ensemble_size, + ellipsis: true, + }, + { + label: '集成最佳模型数量', + value: info.ensemble_nbest, + ellipsis: true, + }, + { + label: '最大数量', + value: info.max_models_on_disc, + ellipsis: true, + }, + { + label: '内存限制(MB)', + value: info.memory_limit, + ellipsis: true, + }, + { + label: '单次时间限制(秒)', + value: info.per_run_time_limit, + ellipsis: true, + }, + { + label: '搜索时间限制(秒)', + value: info.time_left_for_this_task, + ellipsis: true, + }, + { + label: '重采样策略', + value: info.resampling_strategy, + ellipsis: true, + }, + { + label: '交叉验证折数', + value: info.folds, + ellipsis: true, + }, + { + label: '是否打乱', + value: info.shuffle, + ellipsis: true, + format: formatBoolean, + }, + { + label: '训练集比率', + value: info.train_size, + ellipsis: true, + }, + { + label: '测试集比率', + value: info.test_size, + ellipsis: true, + }, + { + label: '计算指标', + value: info.scoring_functions, + ellipsis: true, + }, + { + label: '随机种子', + value: info.seed, + ellipsis: true, + }, + + { + label: '数据集', + value: info.dataset, + ellipsis: true, + format: formatDataset, + }, + { + label: '预测目标列', + value: info.target_columns, + ellipsis: true, + }, + ]; + }, [info]); + + const metricsData = useMemo(() => { + if (!info) { + return []; + } + return [ + { + label: '指标名称', + value: info.metric_name, + ellipsis: true, + }, + { + label: '优化方向', + value: info.greater_is_better, + ellipsis: true, + format: formatOptimizeMode, + }, + { + label: '指标权重', + value: info.metrics, + ellipsis: true, + format: formatMetricsWeight, + }, + ]; + }, [info]); + + const instanceDatas = useMemo(() => { + if (!runStatus) { + return []; + } + + return [ + { + label: '启动时间', + value: formatDate(runStatus.startedAt), + ellipsis: true, + }, + { + label: '执行时长', + value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), + ellipsis: true, + }, + { + label: '状态', + value: ( + + +
+ {experimentStatusInfo[runStatus?.phase]?.label} +
+
+ ), + ellipsis: true, + }, + ]; + }, [runStatus]); + + return ( +
+ {isInstance && runStatus && ( + + )} + {!isInstance && ( + + )} + + +
+ ); +} + +export default AutoMLBasic; diff --git a/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less b/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less new file mode 100644 index 00000000..33fb3314 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less @@ -0,0 +1,20 @@ +.config-info { + :global { + .kf-basic-info { + width: 100%; + + &__item { + width: calc((100% - 80px) / 3); + &__label { + font-size: @font-size; + text-align: left; + text-align-last: left; + } + &__value { + min-width: 0; + font-size: @font-size; + } + } + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx b/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx new file mode 100644 index 00000000..10e042e4 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx @@ -0,0 +1,26 @@ +import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; +import InfoGroup from '@/components/InfoGroup'; +import classNames from 'classnames'; +import styles from './index.less'; +export * from '@/components/BasicInfo/format'; +export type { BasicInfoData }; + +type ConfigInfoProps = { + title: string; + data: BasicInfoData[]; + labelWidth: number; + className?: string; + style?: React.CSSProperties; +}; + +function ConfigInfo({ title, data, labelWidth, className, style }: ConfigInfoProps) { + return ( + +
+ +
+
+ ); +} + +export default ConfigInfo; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx new file mode 100644 index 00000000..8829f12a --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx @@ -0,0 +1,54 @@ +import SubAreaTitle from '@/components/SubAreaTitle'; +import { Col, Form, Input, Row } from 'antd'; + +function BasicConfig() { + return ( + <> + + +
+ + + + + + + + + + + + + + ); +} + +export default BasicConfig; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx new file mode 100644 index 00000000..92e7f961 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx @@ -0,0 +1,504 @@ +import CodeSelect from '@/components/CodeSelect'; +import KFIcon from '@/components/KFIcon'; +import ResourceSelect, { + ResourceSelectorType, + requiredValidator, +} from '@/components/ResourceSelect'; +import SubAreaTitle from '@/components/SubAreaTitle'; +import { modalConfirm } from '@/utils/ui'; +import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select } from 'antd'; +import { isEqual } from 'lodash'; +import PopParameterRange from './PopParameterRange'; +import styles from './index.less'; +import { axParameterOptions, parameterOptions, type FormParameter } from './utils'; + +// 搜索算法 +const searchAlgorithms = ['HyperOpt', 'HEBO', 'BayesOpt', 'Optuna', 'ZOOpt', 'Ax'].map((name) => ({ + label: name, + value: name, +})); + +// 调度算法 +const schedulerAlgorithms = ['ASHA', 'HyperBand', 'MedianStopping', 'PopulationBased', 'PB2'].map( + (name) => ({ label: name, value: name }), +); + +function ExecuteConfig() { + const form = Form.useFormInstance(); + const searchAlgorithm = Form.useWatch('search_alg', form); + const paramsTypeOptions = searchAlgorithm === 'Ax' ? axParameterOptions : parameterOptions; + // const parameters = Form.useWatch('parameters', form); + // console.log('parameters', parameters); + + const handleSearchAlgorithmChange = (value: string) => { + if ( + (value === 'Ax' && searchAlgorithm !== 'Ax') || + (value !== 'Ax' && searchAlgorithm === 'Ax') + ) { + form.setFieldValue('parameters', [{ name: '' }]); + } + }; + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {({ getFieldValue }) => { + const schedulerAlgorithm = getFieldValue('scheduler'); + if (schedulerAlgorithm === 'ASHA' || schedulerAlgorithm === 'HyperBand') { + return ( + + + + + + + + ); + } else if (schedulerAlgorithm === 'MedianStopping') { + return ( + + + + + + + + ); + } + return null; + }} + + + + {(fields, { add, remove }) => ( + <> + + + + + +
+ +
参数名称
+
参数类型
+
取值范围
+
操作
+
+ + {fields.map(({ key, name, ...restField }, index) => ( + + { + if (!value) { + return Promise.reject(new Error('请输入参数名称')); + } + // 判断不能重名 + const list = form + .getFieldValue('parameters') + .filter( + (item: FormParameter | undefined) => + item !== undefined && item !== null, + ); + + const names = list.map((item: FormParameter) => item.name); + if (new Set(names).size !== names.length) { + return Promise.reject(new Error('名称不能重复')); + } + return Promise.resolve(); + }, + }, + ]} + > + + + + + + ))} +
+
+ + {index === fields.length - 1 && ( + + )} +
+ + ))} + + + )} + + ); + }} + + + + + + + + + + + + + + + 越大越好 + 越小越好 + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default ExecuteConfig; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less new file mode 100644 index 00000000..d49089f5 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less @@ -0,0 +1,13 @@ +.parameter-range { + width: 300px; + &__list { + width: 100%; + max-height: 300px; + overflow-x: visible; + overflow-y: auto; + } + &__button { + margin-bottom: 0; + text-align: center; + } +} diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx new file mode 100644 index 00000000..240e90e6 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx @@ -0,0 +1,141 @@ +import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import { Button, Flex, Form, Input, InputNumber } from 'antd'; +import { ParameterType, getFormOptions } from '../utils'; +import styles from './index.less'; + +type ParameterRangeProps = { + type?: ParameterType; + value?: any[]; + onCancel?: () => void; + onConfirm?: (value: any[]) => void; +}; + +function ParameterRange({ type, value, onCancel, onConfirm }: ParameterRangeProps) { + const [form] = Form.useForm(); + const isList = type === ParameterType.Choice || type === ParameterType.Grid; + const formOptions = getFormOptions(type, value); + + const initialValues = isList + ? { list: value && value.length > 0 ? value.map((item) => ({ value: item })) : [{ value: '' }] } + : formOptions.reduce((prev, item) => { + prev[item.name] = item.value; + return prev; + }, {} as Record); + + const handleFinish = (values: any) => { + if (type === ParameterType.Choice || type === ParameterType.Grid) { + const array = values.list.map((item: any) => item.value); + onConfirm?.(array); + } else { + const numbers = Object.values(values).map((item: any) => Number(item)); + onConfirm?.(numbers); + } + }; + + return ( +
+ {isList ? ( +
+ + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }, index) => ( + + + + + + + {index === fields.length - 1 && ( + + )} + + + ))} + {fields.length === 0 && ( + + + + )} + + )} + +
+ ) : ( + formOptions.map((item) => { + return ( + + + + ); + }) + )} + + + + + + ); +} + +export default ParameterRange; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less new file mode 100644 index 00000000..01faf3d0 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less @@ -0,0 +1,47 @@ +.parameter-range { + :global { + .ant-popconfirm-description { + padding-top: 20px; + } + + .ant-popconfirm-buttons { + display: none; + } + } + + &__input { + display: flex; + align-items: center; + width: 100%; + min-height: 46px; + padding: 10px 11px; + font-size: @font-size-input-lg; + line-height: 1.5; + background-color: white; + border: 1px solid #d9d9d9; + border-radius: 8px; + cursor: pointer; + + &:hover { + border-color: #4086ff; + } + + &--disabled { + background-color: rgba(0, 0, 0, 0.04); + cursor: not-allowed; + } + + &__text { + flex: 1; + margin-right: 10px; + } + + &__icon { + flex: none; + } + + &--disabled &__icon { + color: @text-color-tertiary; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx new file mode 100644 index 00000000..ca97b252 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx @@ -0,0 +1,97 @@ +import KFIcon from '@/components/KFIcon'; +import { Popconfirm, Typography } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useRef, useState } from 'react'; +import ParameterRange from '../ParameterRange'; +import { ParameterType } from '../utils'; +import styles from './index.less'; + +type ParameterRangeProps = { + type?: ParameterType; + value?: any[]; + onChange?: (value: any[]) => void; +}; + +function PopParameterRange({ type, value, onChange }: ParameterRangeProps) { + const [open, setOpen] = useState(false); + const popconfirmRef = useRef(null); + const disabled = !type; + const jsonText = JSON.stringify(value); + + const handleClickOutside = (event: MouseEvent) => { + // 判断点击是否在 Popconfirm 内 + const popconfirmNode = document.getElementById('pop-parameter'); + if (popconfirmNode && !popconfirmNode.contains(event.target as Node)) { + setOpen(false); + } + }; + + useEffect(() => { + if (open) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + // 清理事件监听器 + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [open]); + + const handleClick = () => { + if (!disabled) { + setOpen(true); + } + }; + + const handleCancel = () => { + setOpen(false); + }; + + const handleConfirm = (value: number[]) => { + onChange?.(value); + setOpen(false); + }; + + return ( +
+ + } + okText="确定" + cancelText="取消" + overlayClassName={styles['parameter-range']} + icon={null} + open={open} + destroyTooltipOnHide + > +
+ + {jsonText} + + +
+
+
+ ); +} + +export default PopParameterRange; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less new file mode 100644 index 00000000..fcb77fdd --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less @@ -0,0 +1,108 @@ +.metrics-weight { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } +} + +.add-weight { + margin-bottom: 0 !important; + + // 增加样式权重 + & &__button { + border-color: .addAlpha(@primary-color, 0.5) []; + box-shadow: none !important; + &:hover { + border-style: solid; + } + } +} + +.hyper-parameter { + width: 83.33%; + margin-bottom: 20px; + border: 1px solid rgba(234, 234, 234, 0.8); + border-radius: 4px; + &__header { + height: 50px; + padding-left: 8px; + color: @text-color; + font-size: @font-size; + background: #f8f8f9; + border-radius: 4px 4px 0px 0px; + + &__name, + &__type, + &__space { + flex: 1; + min-width: 0; + margin-right: 15px; + + &::before { + display: inline-block; + color: #c73131; + font-size: 14px; + font-family: SimSun, sans-serif; + line-height: 1; + content: '*'; + margin-inline-end: 4px; + } + } + + &__operation { + flex: none; + width: 100px; + } + } + &__body { + padding: 8px; + border-bottom: 1px solid rgba(234, 234, 234, 0.8); + + &:last-child { + border-bottom: none; + } + + &__name, + &__type, + &__space { + flex: 1; + min-width: 0; + margin-right: 15px; + margin-bottom: 0 !important; + } + + &__operation { + display: flex; + flex: none; + align-items: center; + width: 100px; + height: 46px; + } + } + + &__add { + display: flex; + align-items: center; + justify-content: center; + padding: 15px 0; + } +} + +.run-parameter { + width: calc(41.66% + 126px); + margin-bottom: 20px; + border-radius: 8px; + &__body { + flex: 1; + margin-right: 10px; + padding: 20px 20px 0; + border: 1px dashed @border-color-base; + } + &__operation { + display: flex; + flex: none; + align-items: center; + width: 100px; + } +} diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts b/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts new file mode 100644 index 00000000..95aa3651 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts @@ -0,0 +1,155 @@ +export enum ParameterType { + Uniform = 'uniform', + QUniform = 'quniform', + LogUniform = 'loguniform', + QLogUniform = 'qloguniform', + Randn = 'randn', + QRandn = 'qrandn', + RandInt = 'randint', + QRandInt = 'qrandint', + LogRandInt = 'lograndint', + QLogRandInt = 'qlograndint', + Choice = 'choice', + Grid = 'grid', + Range = 'range', + Fixed = 'fixed', +} + +export const parameterOptions = [ + 'uniform', + 'quniform', + 'loguniform', + 'qloguniform', + 'randn', + 'qrandn', + 'randint', + 'qrandint', + 'lograndint', + 'qlograndint', + 'choice', + 'grid', +].map((name) => ({ + label: name, + value: name, +})); + +export const axParameterOptions = ['fixed', 'range', 'choice'].map((name) => ({ + label: name, + value: name, +})); + +export type ParameterData = { + label: string; + name: string; + value?: number; +}; + +// 参数表单数据 +export type FormParameter = { + name: string; // 参数名称 + type: ParameterType; // 参数类型 + range: any; // 参数值 + [key: string]: any; +}; + +export const getFormOptions = (type?: ParameterType, value?: number[]): ParameterData[] => { + const numbers = + value?.map((item) => { + const num = Number(item); + if (isNaN(num)) { + return undefined; + } + return num; + }) ?? []; + switch (type) { + case ParameterType.Uniform: + case ParameterType.LogUniform: + case ParameterType.RandInt: + case ParameterType.LogRandInt: + case ParameterType.Range: + return [ + { + name: 'min', + label: '最小值', + value: numbers?.[0], + }, + { + name: 'max', + label: '最大值', + value: numbers?.[1], + }, + ]; + case ParameterType.QUniform: + case ParameterType.QLogUniform: + case ParameterType.QRandInt: + case ParameterType.QLogRandInt: + return [ + { + name: 'min', + label: '最小值', + value: numbers?.[0], + }, + { + name: 'max', + label: '最大值', + value: numbers?.[1], + }, + { + name: 'q', + label: '间隔', + value: numbers?.[2], + }, + ]; + case ParameterType.Randn: + return [ + { + name: 'mean', + label: '均值', + value: numbers?.[0], + }, + { + name: 'std', + label: '方差', + value: numbers?.[1], + }, + ]; + case ParameterType.QRandn: + return [ + { + name: 'mean', + label: '均值', + value: numbers?.[0], + }, + { + name: 'std', + label: '方差', + value: numbers?.[1], + }, + { + name: 'q', + label: '间隔', + value: numbers?.[2], + }, + ]; + case ParameterType.Fixed: + return [ + { + name: 'value', + label: '值', + value: numbers?.[0], + }, + ]; + default: + return []; + } +}; + +export const getReqParamName = (type: ParameterType) => { + if (type === ParameterType.Fixed) { + return 'value'; + } else if (type === ParameterType.Choice || type === ParameterType.Grid) { + return 'values'; + } else { + return 'bounds'; + } +}; diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less new file mode 100644 index 00000000..beac2a8a --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less @@ -0,0 +1,14 @@ +.experiment-history { + height: calc(100% - 10px); + margin-top: 10px; + &__content { + height: 100%; + padding: 20px @content-padding; + background-color: white; + border-radius: 10px; + + &__table { + height: 100%; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx new file mode 100644 index 00000000..e95ccd42 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -0,0 +1,132 @@ +import { getFileReq } from '@/services/file'; +import { to } from '@/utils/promise'; +import tableCellRender from '@/utils/table'; +import { Table, type TableProps } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useState } from 'react'; +import styles from './index.less'; + +type ExperimentHistoryProps = { + fileUrl?: string; + isClassification: boolean; +}; + +type TableData = { + id?: string; + accuracy?: number; + duration?: number; + train_loss?: number; + status?: string; + feature?: string; + althorithm?: string; +}; + +function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { + const [tableData, setTableData] = useState([]); + useEffect(() => { + if (fileUrl) { + getHistoryFile(); + } + }, [fileUrl]); + + // 获取实验运行历史记录 + const getHistoryFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + const data: any[] = res.data; + const list: TableData[] = data.map((item) => { + return { + id: item[0]?.[0], + accuracy: item[1]?.[5]?.accuracy, + duration: item[1]?.[5]?.duration, + train_loss: item[1]?.[5]?.train_loss, + status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], + }; + }); + list.forEach((item) => { + if (!item.id) return; + const config = (res as any).configs?.[item.id]; + item.feature = config?.['feature_preprocessor:__choice__']; + item.althorithm = isClassification + ? config?.['classifier:__choice__'] + : config?.['regressor:__choice__']; + }); + setTableData(list); + } + }; + + const columns: TableProps['columns'] = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 80, + render: tableCellRender(false), + }, + { + title: '准确率', + dataIndex: 'accuracy', + key: 'accuracy', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '耗时', + dataIndex: 'duration', + key: 'duration', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '训练损失', + dataIndex: 'train_loss', + key: 'train_loss', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '特征处理', + dataIndex: 'feature', + key: 'feature', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '算法', + dataIndex: 'althorithm', + key: 'althorithm', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 120, + render: tableCellRender(false), + }, + ]; + + return ( +
+
+
+
+ + + + ); +} + +export default ExperimentHistory; diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less new file mode 100644 index 00000000..e69de29b diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less new file mode 100644 index 00000000..342817c3 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less @@ -0,0 +1,52 @@ +.experiment-result { + height: calc(100% - 10px); + margin-top: 10px; + padding: 20px @content-padding; + overflow-y: auto; + background-color: white; + border-radius: 10px; + + &__download { + padding-top: 16px; + padding-bottom: 16px; + + padding-left: @content-padding; + color: @text-color; + font-size: 13px; + background-color: #f8f8f9; + border-radius: 4px; + + &__btn { + display: block; + height: 36px; + margin-top: 15px; + font-size: 14px; + } + } + + &__text { + white-space: pre-wrap; + } + + &__images { + display: flex; + align-items: flex-start; + width: 100%; + overflow-x: auto; + + :global { + .ant-image { + margin-right: 20px; + + &:last-child { + margin-right: 0; + } + } + } + + &__item { + height: 248px; + border: 1px solid rgba(96, 107, 122, 0.3); + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx new file mode 100644 index 00000000..a826155d --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx @@ -0,0 +1,83 @@ +import InfoGroup from '@/components/InfoGroup'; +import { getFileReq } from '@/services/file'; +import { to } from '@/utils/promise'; +import { Button, Image } from 'antd'; +import { useEffect, useMemo, useState } from 'react'; +import styles from './index.less'; + +type ExperimentResultProps = { + fileUrl?: string; + imageUrl?: string; + modelPath?: string; +}; + +function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) { + const [result, setResult] = useState(''); + + const images = useMemo(() => { + if (imageUrl) { + return imageUrl.split(',').map((item) => item.trim()); + } + return []; + }, [imageUrl]); + + useEffect(() => { + if (fileUrl) { + getResultFile(); + } + }, [fileUrl]); + + // 获取实验运行历史记录 + const getResultFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + setResult(res as any as string); + } + }; + + return ( +
+ +
{result}
+
+ +
+ + console.log(`current index: ${current}, prev index: ${prev}`), + }} + > + {images.map((item) => ( + + ))} + +
+
+ {modelPath && ( +
+ 文件名 + save_model.joblib + +
+ )} +
+ ); +} + +export default ExperimentResult; diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts new file mode 100644 index 00000000..3eec723c --- /dev/null +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -0,0 +1,64 @@ +import { type ParameterInputObject } from '@/components/ResourceSelect'; +import { type NodeStatus } from '@/types'; +import { type FormParameter } from './components/CreateForm/utils'; + +// 操作类型 +export enum OperationType { + Create = 'Create', // 创建 + Update = 'Update', // 更新 +} + +// 表单数据 +export type FormData = { + name: string; // 实验名称 + description: string; // 实验描述 + code: ParameterInputObject; // 代码 + dataset: ParameterInputObject; // 数据集 + dataset_path: string; // 数据集路径 + main_py: string; // 主函数代码文件 + metrics: string; // 指标 + mode: string; // 优化方向 + search_alg?: string; // 搜索算法 + scheduler?: string; // 调度算法 + num_samples: number; // 总实验次数 + max_t: number; // 单次试验最大时间 + min_samples_required: number; // 计算中位数的最小试验数 + cpu: number; // cpu 数 + gpu: number; // gpu 数 + parameters: FormParameter[]; + points_to_evaluate: { [key: string]: any }[]; +}; + +export type HyperparameterData = { + id: number; + progress: number; + run_state: string; + state: number; + create_by?: string; + create_time?: string; + update_by?: string; + update_time?: string; + status_list: string; // 最近五次运行状态 +} & FormData; + +// 自动机器学习实验实例 +export type AutoMLInstanceData = { + id: number; + auto_ml_id: number; + result_path: string; + model_path: string; + img_path: string; + run_history_path: string; + state: number; + status: string; + node_status: string; + node_result: string; + param: string; + source: string | null; + argo_ins_name: string; + argo_ins_ns: string; + create_time: string; + update_time: string; + finish_time: string; + nodeStatus?: NodeStatus; +}; diff --git a/react-ui/src/services/hyperParameter/index.js b/react-ui/src/services/hyperParameter/index.js new file mode 100644 index 00000000..c97e617d --- /dev/null +++ b/react-ui/src/services/hyperParameter/index.js @@ -0,0 +1,93 @@ +/* + * @Author: 赵伟 + * @Date: 2024-11-18 10:18:27 + * @Description: 超参数自动寻优请求 + */ + +import { request } from '@umijs/max'; + + +// 分页查询超参数自动寻优 +export function getRayListReq(params) { + return request(`/api/mmp/ray`, { + method: 'GET', + params, + }); +} + +// 查询超参数自动寻优详情 +export function getRayInfoReq(params) { + return request(`/api/mmp/ray/getRayDetail`, { + method: 'GET', + params, + }); +} + +// 新增超参数自动寻优 +export function addRayReq(data) { + return request(`/api/mmp/ray`, { + method: 'POST', + data, + }); +} + +// 编辑超参数自动寻优 +export function updateRayReq(data) { + return request(`/api/mmp/ray`, { + method: 'PUT', + data, + }); +} + +// 删除超参数自动寻优 +export function deleteRayReq(id) { + return request(`/api/mmp/ray/${id}`, { + method: 'DELETE', + }); +} + +// 运行超参数自动寻优 +export function runRayReq(id) { + return request(`/api/mmp/ray/run/${id}`, { + method: 'POST', + }); +} + +// ----------------------- 实验实例 ----------------------- +// 获取实验实例列表 +export function getRayInsListReq(params) { + return request(`/api/mmp/rayIns`, { + method: 'GET', + params, + }); +} + +// 查询实验实例详情 +export function getRayInsReq(id) { + return request(`/api/mmp/rayIns/${id}`, { + method: 'GET', + }); +} + +// 停止实验实例 +export function stopRayInsReq(id) { + return request(`/api/mmp/rayIns/${id}`, { + method: 'PUT', + }); +} + +// 删除实验实例 +export function deleteRayInsReq(id) { + return request(`/api/mmp/rayIns/${id}`, { + method: 'DELETE', + }); +} + +// 批量删除实验实例 +export function batchDeleteRayInsReq(data) { + return request(`/api/mmp/rayIns/batchDelete`, { + method: 'DELETE', + data + }); +} + diff --git a/react-ui/src/utils/constant.ts b/react-ui/src/utils/constant.ts new file mode 100644 index 00000000..4fe1ea9b --- /dev/null +++ b/react-ui/src/utils/constant.ts @@ -0,0 +1,3 @@ +export const xlCols = { span: 12 }; +export const xllCols = { span: 10 }; +export const formCols = { xl: xlCols, xxl: xllCols };