| @@ -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', | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -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), | |||
| @@ -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<AutoMLData[]>([]); | |||
| const [total, setTotal] = useState(0); | |||
| const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); | |||
| const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); | |||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| cacheState?.pagination ?? { | |||
| current: 1, | |||
| pageSize: 10, | |||
| }, | |||
| ); | |||
| useEffect(() => { | |||
| getAutoMLList(); | |||
| }, [pagination, searchText]); | |||
| // 获取自主机器学习列表 | |||
| const getAutoMLList = async () => { | |||
| const params: Record<string, any> = { | |||
| 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<AutoMLData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| }; | |||
| const columns: TableProps<AutoMLData>['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 ( | |||
| <Tooltip | |||
| key={index} | |||
| placement="top" | |||
| title={experimentStatusInfo[item as ExperimentStatus].label} | |||
| > | |||
| <img | |||
| style={{ width: '17px', marginRight: '6px' }} | |||
| src={experimentStatusInfo[item as ExperimentStatus].icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| </Tooltip> | |||
| ); | |||
| }) | |||
| : null} | |||
| </> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| width: 360, | |||
| key: 'operation', | |||
| render: (_: any, record: AutoMLData) => ( | |||
| <div> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="start" | |||
| icon={<KFIcon type="icon-yunhang" />} | |||
| onClick={() => startAutoML(record)} | |||
| > | |||
| 运行 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="edit" | |||
| icon={<KFIcon type="icon-bianji" />} | |||
| onClick={() => createAutoML(record, false)} | |||
| > | |||
| 编辑 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="copy" | |||
| icon={<KFIcon type="icon-fuzhi" />} | |||
| onClick={() => createAutoML(record, true)} | |||
| > | |||
| 复制 | |||
| </Button> | |||
| <ConfigProvider | |||
| theme={{ | |||
| token: { | |||
| colorLink: themes['warningColor'], | |||
| }, | |||
| }} | |||
| > | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="remove" | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| onClick={() => handleAutoMLDelete(record)} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| </ConfigProvider> | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| return ( | |||
| <div className={styles['auto-ml-list']}> | |||
| <PageTitle title="自动机器学习列表"></PageTitle> | |||
| <div className={styles['auto-ml-list__content']}> | |||
| <div className={styles['auto-ml-list__content__filter']}> | |||
| <Input.Search | |||
| placeholder="按实验名称筛选" | |||
| onSearch={onSearch} | |||
| onChange={(e) => setInputText(e.target.value)} | |||
| style={{ width: 300 }} | |||
| value={inputText} | |||
| allowClear | |||
| /> | |||
| <Button | |||
| style={{ marginLeft: '20px' }} | |||
| type="default" | |||
| onClick={() => createAutoML()} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 新建实验 | |||
| </Button> | |||
| </div> | |||
| <div | |||
| className={classNames('vertical-scroll-table', styles['auto-ml-list__content__table'])} | |||
| > | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| pagination={{ | |||
| ...pagination, | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| expandable={{ | |||
| expandedRowRender: (record) => ( | |||
| <ExperimentInstance | |||
| experimentInsList={experimentInsList} | |||
| experimentInsTotal={experimentInsTotal} | |||
| onClickInstance={(item) => gotoInstanceInfo(record, item)} | |||
| onRemove={() => { | |||
| refreshExperimentIns(record.id); | |||
| refreshExperimentList(); | |||
| }} | |||
| onTerminate={handleInstanceTerminate} | |||
| onLoadMore={() => loadMoreExperimentIns()} | |||
| ></ExperimentInstance> | |||
| ), | |||
| onExpand: (e, a) => { | |||
| handleExpandChange(e, a); | |||
| }, | |||
| expandedRowKeys: expandedRowKeys, | |||
| rowExpandable: () => true, | |||
| }} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| function AutoMLList() { | |||
| return <ExperimentList type={ExperimentListType.AutoML} />; | |||
| } | |||
| export default AutoMLList; | |||
| @@ -62,10 +62,7 @@ function TrialConfig() { | |||
| > | |||
| <InputNumber placeholder="请输入指标权重" min={0} precision={0} /> | |||
| </Form.Item> | |||
| <Flex | |||
| style={{ width: '76px', marginLeft: '18px', height: '46px' }} | |||
| align="center" | |||
| > | |||
| <Flex className={styles['metrics-weight__operation']} align="center"> | |||
| <Button | |||
| style={{ | |||
| marginRight: '3px', | |||
| @@ -1,9 +1,18 @@ | |||
| .metrics-weight { | |||
| position: relative; | |||
| margin-bottom: 20px; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| } | |||
| &__operation { | |||
| position: absolute; | |||
| left: calc(100% + 10px); | |||
| width: 76px; | |||
| height: 46px; | |||
| margin-left: 6px; | |||
| } | |||
| } | |||
| .add-weight { | |||
| @@ -14,7 +23,7 @@ | |||
| border-color: .addAlpha(@primary-color, 0.5) []; | |||
| box-shadow: none !important; | |||
| &:hover { | |||
| border-style: solid; | |||
| border-style: solid !important; | |||
| } | |||
| } | |||
| } | |||
| @@ -2,11 +2,6 @@ import KFIcon from '@/components/KFIcon'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useCheck } from '@/hooks'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { | |||
| batchDeleteExperimentInsReq, | |||
| deleteExperimentInsReq, | |||
| stopExperimentInsReq, | |||
| } from '@/services/autoML'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { type ExperimentInstance } from '@/types'; | |||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||
| @@ -16,9 +11,11 @@ import { DoubleRightOutlined } from '@ant-design/icons'; | |||
| import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo } from 'react'; | |||
| import { ExperimentListType, experimentListConfig } from '../ExperimentList/config'; | |||
| import styles from './index.less'; | |||
| type ExperimentInstanceProps = { | |||
| type: ExperimentListType; | |||
| experimentInsList?: ExperimentInstance[]; | |||
| experimentInsTotal: number; | |||
| onClickInstance?: (instance: ExperimentInstance) => void; | |||
| @@ -28,6 +25,7 @@ type ExperimentInstanceProps = { | |||
| }; | |||
| function ExperimentInstanceComponent({ | |||
| type, | |||
| experimentInsList, | |||
| experimentInsTotal, | |||
| onClickInstance, | |||
| @@ -48,6 +46,7 @@ function ExperimentInstanceComponent({ | |||
| isSingleChecked, | |||
| checkSingle, | |||
| ] = useCheck(allIntanceIds); | |||
| const config = experimentListConfig[type]; | |||
| useEffect(() => { | |||
| // 关闭时清空 | |||
| @@ -68,7 +67,8 @@ function ExperimentInstanceComponent({ | |||
| // 删除实验实例 | |||
| const deleteExperimentInstance = async (id: number) => { | |||
| const [res] = await to(deleteExperimentInsReq(id)); | |||
| const request = config.deleteInsReq; | |||
| const [res] = await to(request(id)); | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| onRemove?.(); | |||
| @@ -87,7 +87,8 @@ function ExperimentInstanceComponent({ | |||
| // 批量删除实验实例 | |||
| const batchDeleteExperimentInstances = async () => { | |||
| const [res] = await to(batchDeleteExperimentInsReq(selectedIns)); | |||
| const request = config.batchDeleteInsReq; | |||
| const [res] = await to(request(selectedIns)); | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| setSelectedIns([]); | |||
| @@ -97,7 +98,8 @@ function ExperimentInstanceComponent({ | |||
| // 终止实验实例 | |||
| const terminateExperimentInstance = async (instance: ExperimentInstance) => { | |||
| const [res] = await to(stopExperimentInsReq(instance.id)); | |||
| const request = config.stopInsReq; | |||
| const [res] = await to(request(instance.id)); | |||
| if (res) { | |||
| message.success('终止成功'); | |||
| onTerminate?.(instance); | |||
| @@ -0,0 +1,75 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2025-01-08 14:30:58 | |||
| * @Description: 实验列表组件配置 | |||
| */ | |||
| import { | |||
| batchDeleteExperimentInsReq, | |||
| deleteAutoMLReq, | |||
| deleteExperimentInsReq, | |||
| getAutoMLListReq, | |||
| getExperimentInsListReq, | |||
| runAutoMLReq, | |||
| stopExperimentInsReq, | |||
| } from '@/services/autoML'; | |||
| import { | |||
| batchDeleteRayInsReq, | |||
| deleteRayInsReq, | |||
| deleteRayReq, | |||
| getRayInsListReq, | |||
| getRayListReq, | |||
| runRayReq, | |||
| stopRayInsReq, | |||
| } from '@/services/hyperParameter'; | |||
| export enum ExperimentListType { | |||
| AutoML = 'AutoML', | |||
| HyperParameter = 'HyperParameter', | |||
| } | |||
| type ExperimentListInfo = { | |||
| getListReq: (params: any) => Promise<any>; // 获取列表 | |||
| getInsListReq: (params: any) => Promise<any>; // 获取实例列表 | |||
| deleteRecordReq: (params: any) => Promise<any>; // 删除 | |||
| runRecordReq: (params: any) => Promise<any>; // 运行 | |||
| deleteInsReq: (params: any) => Promise<any>; // 删除实例 | |||
| batchDeleteInsReq: (params: any) => Promise<any>; // 批量删除实例 | |||
| stopInsReq: (params: any) => Promise<any>; // 终止实例 | |||
| title: string; // 标题 | |||
| pathPrefix: string; // 路由路径前缀 | |||
| idProperty: string; // ID属性 | |||
| nameProperty: string; // 名称属性 | |||
| descProperty: string; // 描述属性 | |||
| }; | |||
| export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo> = { | |||
| [ExperimentListType.AutoML]: { | |||
| getListReq: getAutoMLListReq, | |||
| getInsListReq: getExperimentInsListReq, | |||
| deleteRecordReq: deleteAutoMLReq, | |||
| runRecordReq: runAutoMLReq, | |||
| deleteInsReq: deleteExperimentInsReq, | |||
| batchDeleteInsReq: batchDeleteExperimentInsReq, | |||
| stopInsReq: stopExperimentInsReq, | |||
| title: '自主机器学习', | |||
| pathPrefix: 'automl', | |||
| nameProperty: 'ml_name', | |||
| descProperty: 'ml_description', | |||
| idProperty: 'autoMlId', | |||
| }, | |||
| [ExperimentListType.HyperParameter]: { | |||
| getListReq: getRayListReq, | |||
| getInsListReq: getRayInsListReq, | |||
| deleteRecordReq: deleteRayReq, | |||
| runRecordReq: runRayReq, | |||
| deleteInsReq: deleteRayInsReq, | |||
| batchDeleteInsReq: batchDeleteRayInsReq, | |||
| stopInsReq: stopRayInsReq, | |||
| title: '超参数自动寻优', | |||
| pathPrefix: 'hyperparameter', | |||
| nameProperty: 'name', | |||
| descProperty: 'description', | |||
| idProperty: 'rayId', | |||
| }, | |||
| }; | |||
| @@ -1,4 +1,4 @@ | |||
| .auto-ml-list { | |||
| .experiment-list { | |||
| height: 100%; | |||
| &__content { | |||
| height: calc(100% - 60px); | |||
| @@ -0,0 +1,428 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2025-01-08 13:58:08 | |||
| * @Description: 自主机器学习和超参数寻优列表组件 | |||
| */ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { AutoMLData } from '@/pages/AutoML/types'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| 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 '../ExperimentInstance'; | |||
| import { ExperimentListType, experimentListConfig } from './config'; | |||
| import styles from './index.less'; | |||
| export { ExperimentListType }; | |||
| type ExperimentListProps = { | |||
| type: ExperimentListType; | |||
| }; | |||
| function ExperimentList({ type }: ExperimentListProps) { | |||
| 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<AutoMLData[]>([]); | |||
| const [total, setTotal] = useState(0); | |||
| const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); | |||
| const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); | |||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| cacheState?.pagination ?? { | |||
| current: 1, | |||
| pageSize: 10, | |||
| }, | |||
| ); | |||
| const config = experimentListConfig[type]; | |||
| useEffect(() => { | |||
| getAutoMLList(); | |||
| }, [pagination, searchText]); | |||
| // 获取自主机器学习或超参数自动优化列表 | |||
| const getAutoMLList = async () => { | |||
| const params: Record<string, any> = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| ml_name: searchText || undefined, | |||
| }; | |||
| const request = config.getListReq; | |||
| const [res] = await to(request(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 request = config.deleteRecordReq; | |||
| const [res] = await to(request(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(`copy/${record.id}`); | |||
| } else { | |||
| navigate(`edit/${record.id}`); | |||
| } | |||
| } else { | |||
| navigate(`create`); | |||
| } | |||
| }; | |||
| // 查看自动机器学习详情 | |||
| const gotoDetail = (record: AutoMLData) => { | |||
| setCacheState({ | |||
| pagination, | |||
| searchText, | |||
| }); | |||
| navigate(`info/${record.id}`); | |||
| }; | |||
| // 启动自动机器学习 | |||
| const startAutoML = async (record: AutoMLData) => { | |||
| const request = config.runRecordReq; | |||
| const [res] = await to(request(record.id)); | |||
| if (res) { | |||
| message.success('运行成功'); | |||
| setExpandedRowKeys([record.id]); | |||
| refreshExperimentList(); | |||
| refreshExperimentIns(record.id); | |||
| } | |||
| }; | |||
| // --------------------------- 实验实例 --------------------------- | |||
| // 获取实验实例列表 | |||
| const getExperimentInsList = async (recordId: number, page: number) => { | |||
| const params = { | |||
| [config.idProperty]: recordId, | |||
| page: page, | |||
| size: 5, | |||
| }; | |||
| const request = config.getInsListReq; | |||
| const [res] = await to(request(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(`instance/${autoML.id}/${record.id}`); | |||
| }; | |||
| // 刷新实验实例列表 | |||
| const refreshExperimentIns = (experimentId: number) => { | |||
| getExperimentInsList(experimentId, 0); | |||
| }; | |||
| // 加载更多实验实例 | |||
| const loadMoreExperimentIns = () => { | |||
| const page = Math.round(experimentInsList.length / 5); | |||
| const recordId = expandedRowKeys[0]; | |||
| getExperimentInsList(recordId, 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<AutoMLData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| }; | |||
| const columns: TableProps<AutoMLData>['columns'] = [ | |||
| { | |||
| title: '实验名称', | |||
| dataIndex: config.nameProperty, | |||
| key: 'ml_name', | |||
| width: '16%', | |||
| render: tableCellRender(false, TableCellValueType.Link, { | |||
| onClick: gotoDetail, | |||
| }), | |||
| }, | |||
| { | |||
| title: '实验描述', | |||
| dataIndex: config.descProperty, | |||
| 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 ( | |||
| <Tooltip | |||
| key={index} | |||
| placement="top" | |||
| title={experimentStatusInfo[item as ExperimentStatus].label} | |||
| > | |||
| <img | |||
| style={{ width: '17px', marginRight: '6px' }} | |||
| src={experimentStatusInfo[item as ExperimentStatus].icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| </Tooltip> | |||
| ); | |||
| }) | |||
| : null} | |||
| </> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| width: 360, | |||
| key: 'operation', | |||
| render: (_: any, record: AutoMLData) => ( | |||
| <div> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="start" | |||
| icon={<KFIcon type="icon-yunhang" />} | |||
| onClick={() => startAutoML(record)} | |||
| > | |||
| 运行 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="edit" | |||
| icon={<KFIcon type="icon-bianji" />} | |||
| onClick={() => createAutoML(record, false)} | |||
| > | |||
| 编辑 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="copy" | |||
| icon={<KFIcon type="icon-fuzhi" />} | |||
| onClick={() => createAutoML(record, true)} | |||
| > | |||
| 复制 | |||
| </Button> | |||
| <ConfigProvider | |||
| theme={{ | |||
| token: { | |||
| colorLink: themes['warningColor'], | |||
| }, | |||
| }} | |||
| > | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="remove" | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| onClick={() => handleAutoMLDelete(record)} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| </ConfigProvider> | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| return ( | |||
| <div className={styles['experiment-list']}> | |||
| <PageTitle title={config.title + '列表'}></PageTitle> | |||
| <div className={styles['experiment-list__content']}> | |||
| <div className={styles['experiment-list__content__filter']}> | |||
| <Input.Search | |||
| placeholder="按实验名称筛选" | |||
| onSearch={onSearch} | |||
| onChange={(e) => setInputText(e.target.value)} | |||
| style={{ width: 300 }} | |||
| value={inputText} | |||
| allowClear | |||
| /> | |||
| <Button | |||
| style={{ marginLeft: '20px' }} | |||
| type="default" | |||
| onClick={() => createAutoML()} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 新建实验 | |||
| </Button> | |||
| </div> | |||
| <div | |||
| className={classNames('vertical-scroll-table', styles['experiment-list__content__table'])} | |||
| > | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| pagination={{ | |||
| ...pagination, | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| expandable={{ | |||
| expandedRowRender: (record) => ( | |||
| <ExperimentInstance | |||
| experimentInsList={experimentInsList} | |||
| experimentInsTotal={experimentInsTotal} | |||
| onClickInstance={(item) => gotoInstanceInfo(record, item)} | |||
| onRemove={() => { | |||
| refreshExperimentIns(record.id); | |||
| refreshExperimentList(); | |||
| }} | |||
| onTerminate={handleInstanceTerminate} | |||
| onLoadMore={() => loadMoreExperimentIns()} | |||
| ></ExperimentInstance> | |||
| ), | |||
| onExpand: (e, a) => { | |||
| handleExpandChange(e, a); | |||
| }, | |||
| expandedRowKeys: expandedRowKeys, | |||
| rowExpandable: () => true, | |||
| }} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default ExperimentList; | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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 ( | |||
| <div className={styles['create-hyperparameter']}> | |||
| <PageTitle title={title}></PageTitle> | |||
| <div className={styles['create-hyperparameter__content']}> | |||
| <div> | |||
| <Form | |||
| name="create-hyperparameter" | |||
| labelCol={{ flex: '160px' }} | |||
| labelAlign="left" | |||
| form={form} | |||
| onFinish={handleSubmit} | |||
| size="large" | |||
| autoComplete="off" | |||
| scrollToFirstError | |||
| initialValues={{ | |||
| mode: 'max', | |||
| parameters: [ | |||
| { | |||
| name: '', | |||
| }, | |||
| ], | |||
| points_to_evaluate: [undefined], | |||
| }} | |||
| > | |||
| <BasicConfig /> | |||
| <ExecuteConfig /> | |||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }}> | |||
| <Button type="primary" htmlType="submit"> | |||
| {buttonText} | |||
| </Button> | |||
| <Button | |||
| type="default" | |||
| htmlType="button" | |||
| onClick={cancel} | |||
| style={{ marginLeft: '20px' }} | |||
| > | |||
| 取消 | |||
| </Button> | |||
| </Form.Item> | |||
| </Form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default CreateHyperparameter; | |||
| @@ -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: ''; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<string>(CommonTabKeys.Public); | |||
| const params = useParams(); | |||
| const autoMLId = safeInvoke(Number)(params.id); | |||
| const [autoMLInfo, setAutoMLInfo] = useState<HyperparameterData | undefined>(undefined); | |||
| const tabItems = [ | |||
| { | |||
| key: CommonTabKeys.Public, | |||
| label: '基本信息', | |||
| icon: <KFIcon type="icon-jibenxinxi" />, | |||
| }, | |||
| { | |||
| key: CommonTabKeys.Private, | |||
| label: 'Trial列表', | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| }, | |||
| ]; | |||
| useEffect(() => { | |||
| if (autoMLId) { | |||
| getAutoMLInfo(); | |||
| } | |||
| }, []); | |||
| // 获取自动机器学习详情 | |||
| const getAutoMLInfo = async () => { | |||
| const [res] = await to(getAutoMLInfoReq({ id: autoMLId })); | |||
| if (res && res.data) { | |||
| setAutoMLInfo(res.data); | |||
| } | |||
| }; | |||
| return ( | |||
| <div className={styles['auto-ml-info']}> | |||
| <PageTitle title="实验详情"></PageTitle> | |||
| <div className={styles['auto-ml-info__content']}> | |||
| <AutoMLBasic info={autoMLInfo} /> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default AutoMLInfo; | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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<string>(TabKeys.Params); | |||
| const [autoMLInfo, setAutoMLInfo] = useState<HyperparameterData | undefined>(undefined); | |||
| const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); | |||
| const params = useParams(); | |||
| // const autoMLId = safeInvoke(Number)(params.autoMLId); | |||
| const instanceId = safeInvoke(Number)(params.id); | |||
| const evtSourceRef = useRef<EventSource | null>(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: <KFIcon type="icon-jibenxinxi" />, | |||
| children: ( | |||
| <AutoMLBasic | |||
| className={styles['auto-ml-instance__basic']} | |||
| info={autoMLInfo} | |||
| runStatus={instanceInfo?.nodeStatus} | |||
| isInstance | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| key: TabKeys.Log, | |||
| label: '日志', | |||
| icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['auto-ml-instance__log']}> | |||
| {instanceInfo && instanceInfo.nodeStatus && ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| pipelineNodeId={instanceInfo.nodeStatus.displayName} | |||
| workflowId={instanceInfo.nodeStatus.id} | |||
| instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} | |||
| instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| )} | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| const resultTabItems = [ | |||
| { | |||
| key: TabKeys.Result, | |||
| label: '实验结果', | |||
| icon: <KFIcon type="icon-shiyanjieguo1" />, | |||
| children: ( | |||
| <ExperimentResult | |||
| fileUrl={instanceInfo?.result_path} | |||
| imageUrl={instanceInfo?.img_path} | |||
| modelPath={instanceInfo?.model_path} | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| key: TabKeys.History, | |||
| label: 'Trial 列表', | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| children: ( | |||
| <ExperimentHistory | |||
| fileUrl={instanceInfo?.run_history_path} | |||
| isClassification={autoMLInfo?.task_type === AutoMLTaskType.Classification} | |||
| /> | |||
| ), | |||
| }, | |||
| ]; | |||
| const tabItems = | |||
| instanceInfo?.status === ExperimentStatus.Succeeded | |||
| ? [...basicTabItems, ...resultTabItems] | |||
| : basicTabItems; | |||
| return ( | |||
| <div className={styles['auto-ml-instance']}> | |||
| <Tabs | |||
| className={styles['auto-ml-instance__tabs']} | |||
| items={tabItems} | |||
| activeKey={activeTab} | |||
| onChange={setActiveTab} | |||
| /> | |||
| </div> | |||
| ); | |||
| } | |||
| export default AutoMLInstance; | |||
| @@ -0,0 +1,13 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 超参数自动寻优 | |||
| */ | |||
| import ExperimentList, { ExperimentListType } from '@/pages/AutoML/components/ExperimentList'; | |||
| function HyperParameter() { | |||
| return <ExperimentList type={ExperimentListType.HyperParameter} />; | |||
| } | |||
| export default HyperParameter; | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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: ( | |||
| <Flex align="center"> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={experimentStatusInfo[runStatus.phase]?.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div | |||
| style={{ | |||
| color: experimentStatusInfo[runStatus?.phase]?.color, | |||
| fontSize: '15px', | |||
| lineHeight: 1.6, | |||
| }} | |||
| > | |||
| {experimentStatusInfo[runStatus?.phase]?.label} | |||
| </div> | |||
| </Flex> | |||
| ), | |||
| ellipsis: true, | |||
| }, | |||
| ]; | |||
| }, [runStatus]); | |||
| return ( | |||
| <div className={classNames(styles['auto-ml-basic'], className)}> | |||
| {isInstance && runStatus && ( | |||
| <ConfigInfo | |||
| title="运行信息" | |||
| data={instanceDatas} | |||
| labelWidth={70} | |||
| style={{ marginBottom: '20px' }} | |||
| /> | |||
| )} | |||
| {!isInstance && ( | |||
| <ConfigInfo | |||
| title="基本信息" | |||
| data={basicDatas} | |||
| labelWidth={70} | |||
| style={{ marginBottom: '20px' }} | |||
| /> | |||
| )} | |||
| <ConfigInfo | |||
| title="配置信息" | |||
| data={configDatas} | |||
| labelWidth={150} | |||
| style={{ marginBottom: '20px' }} | |||
| /> | |||
| <ConfigInfo title="优化指标" data={metricsData} labelWidth={70} /> | |||
| </div> | |||
| ); | |||
| } | |||
| export default AutoMLBasic; | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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 ( | |||
| <InfoGroup title={title} className={classNames(styles['config-info'], className)} style={style}> | |||
| <div className={styles['config-info__content']}> | |||
| <BasicInfo datas={data} labelWidth={labelWidth} /> | |||
| </div> | |||
| </InfoGroup> | |||
| ); | |||
| } | |||
| export default ConfigInfo; | |||
| @@ -0,0 +1,54 @@ | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { Col, Form, Input, Row } from 'antd'; | |||
| function BasicConfig() { | |||
| return ( | |||
| <> | |||
| <SubAreaTitle | |||
| title="基本信息" | |||
| image={require('@/assets/img/mirror-basic.png')} | |||
| style={{ marginBottom: '26px' }} | |||
| ></SubAreaTitle> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="实验名称" | |||
| name="name" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入实验名称', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入实验名称" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={20}> | |||
| <Form.Item | |||
| label="实验描述" | |||
| name="description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入实验描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| placeholder="请输入实验描述" | |||
| maxLength={256} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| </> | |||
| ); | |||
| } | |||
| export default BasicConfig; | |||
| @@ -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 ( | |||
| <> | |||
| <SubAreaTitle | |||
| title="配置信息" | |||
| image={require('@/assets/img/model-deployment.png')} | |||
| style={{ marginTop: '20px', marginBottom: '24px' }} | |||
| ></SubAreaTitle> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="代码配置" | |||
| name="code" | |||
| rules={[ | |||
| { | |||
| validator: requiredValidator, | |||
| message: '请选择代码配置', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <CodeSelect placeholder="请选择代码配置" canInput={false} size="large" /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="主函数代码文件" | |||
| name="main_py" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入主函数代码文件', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入主函数代码文件" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="数据集" | |||
| name="dataset" | |||
| rules={[ | |||
| { | |||
| validator: requiredValidator, | |||
| message: '请选择数据集', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <ResourceSelect | |||
| type={ResourceSelectorType.Dataset} | |||
| placeholder="请选择数据集" | |||
| canInput={false} | |||
| size="large" | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="数据集挂载路径" | |||
| name="dataset_path" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入数据集挂载路径', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入数据集挂载路径" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="总实验次数" | |||
| name="num_samples" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入总实验次数', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入总实验次数" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item label="搜索算法" name="search_alg"> | |||
| <Select | |||
| placeholder="请选择搜索算法" | |||
| options={searchAlgorithms} | |||
| showSearch | |||
| allowClear | |||
| onChange={handleSearchAlgorithmChange} | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item label="调度算法" name="scheduler"> | |||
| <Select | |||
| placeholder="请选择调度算法" | |||
| options={schedulerAlgorithms} | |||
| showSearch | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Form.Item dependencies={['scheduler']} noStyle> | |||
| {({ getFieldValue }) => { | |||
| const schedulerAlgorithm = getFieldValue('scheduler'); | |||
| if (schedulerAlgorithm === 'ASHA' || schedulerAlgorithm === 'HyperBand') { | |||
| return ( | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="单次试验最大时间" | |||
| name="max_t" | |||
| tooltip="每次试验的最大时间单位,单位秒" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '单次试验最大时间', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入单次试验最大时间" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| ); | |||
| } else if (schedulerAlgorithm === 'MedianStopping') { | |||
| return ( | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="最小试验数" | |||
| name="min_samples_required" | |||
| tooltip="计算中位数的最小试验数" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入计算中位数的最小试验数', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入计算中位数的最小试验数" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| ); | |||
| } | |||
| return null; | |||
| }} | |||
| </Form.Item> | |||
| <Form.List name="parameters"> | |||
| {(fields, { add, remove }) => ( | |||
| <> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="参数" | |||
| style={{ marginBottom: 0, marginTop: '-14px' }} | |||
| required | |||
| ></Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <div className={styles['hyper-parameter']}> | |||
| <Flex align="center" className={styles['hyper-parameter__header']}> | |||
| <div className={styles['hyper-parameter__header__name']}>参数名称</div> | |||
| <div className={styles['hyper-parameter__header__type']}>参数类型</div> | |||
| <div className={styles['hyper-parameter__header__space']}>取值范围</div> | |||
| <div className={styles['hyper-parameter__header__operation']}>操作</div> | |||
| </Flex> | |||
| {fields.map(({ key, name, ...restField }, index) => ( | |||
| <Flex key={key} align="flex-start" className={styles['hyper-parameter__body']}> | |||
| <Form.Item | |||
| className={styles['hyper-parameter__body__name']} | |||
| {...restField} | |||
| name={[name, 'name']} | |||
| required | |||
| rules={[ | |||
| { | |||
| validator: (_, value) => { | |||
| 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(); | |||
| }, | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入参数名称" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| className={styles['hyper-parameter__body__name']} | |||
| {...restField} | |||
| name={[name, 'type']} | |||
| rules={[{ required: true, message: '请选择参数类型' }]} | |||
| > | |||
| <Select | |||
| placeholder="请选择参数类型" | |||
| options={paramsTypeOptions} | |||
| onChange={() => { | |||
| form.setFieldValue(['parameters', name, 'range'], undefined); | |||
| }} | |||
| showSearch | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item dependencies={[['parameters', name, 'type']]} noStyle> | |||
| {({ getFieldValue }) => { | |||
| const type = getFieldValue(['parameters', name, 'type']); | |||
| return ( | |||
| <Form.Item | |||
| className={styles['hyper-parameter__body__name']} | |||
| {...restField} | |||
| name={[name, 'range']} | |||
| rules={[{ required: true, message: '请输入取值范围' }]} | |||
| > | |||
| <PopParameterRange type={type}></PopParameterRange> | |||
| </Form.Item> | |||
| ); | |||
| }} | |||
| </Form.Item> | |||
| <div className={styles['hyper-parameter__body__operation']}> | |||
| <Button | |||
| style={{ | |||
| marginRight: '3px', | |||
| }} | |||
| shape="circle" | |||
| disabled={fields.length === 1} | |||
| type="text" | |||
| size="middle" | |||
| icon={<MinusCircleOutlined />} | |||
| onClick={() => { | |||
| modalConfirm({ | |||
| title: '确定要删除该参数吗?', | |||
| onOk: () => { | |||
| remove(name); | |||
| }, | |||
| }); | |||
| }} | |||
| ></Button> | |||
| {index === fields.length - 1 && ( | |||
| <Button | |||
| shape="circle" | |||
| size="middle" | |||
| type="text" | |||
| onClick={() => add()} | |||
| icon={<PlusCircleOutlined />} | |||
| ></Button> | |||
| )} | |||
| </div> | |||
| </Flex> | |||
| ))} | |||
| {fields.length === 0 && ( | |||
| <div className={styles['hyper-parameter__add']}> | |||
| <Button type="link" onClick={() => add()} icon={<KFIcon type="icon-xinjian2" />}> | |||
| 添加一行 | |||
| </Button> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| <Form.Item | |||
| noStyle | |||
| shouldUpdate={(prevValues, curValues) => | |||
| !isEqual(prevValues.parameters, curValues.parameters) | |||
| } | |||
| > | |||
| {({ getFieldValue }) => { | |||
| const parameters = getFieldValue('parameters').filter( | |||
| (item: FormParameter | undefined) => item !== undefined && item !== null && item.name, | |||
| ); | |||
| if (parameters.length === 0) { | |||
| return null; | |||
| } | |||
| return ( | |||
| <Form.List name="points_to_evaluate"> | |||
| {(fields, { add, remove }) => ( | |||
| <> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="手动运行参数" | |||
| style={{ marginBottom: 0, marginTop: '-14px' }} | |||
| required | |||
| ></Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <div className={styles['run-parameter']}> | |||
| {fields.map(({ key, name, ...restField }, index) => ( | |||
| <Flex key={key} align="center" style={{ marginBottom: '20px' }}> | |||
| <div className={styles['run-parameter__body']}> | |||
| {parameters.map((item: FormParameter) => ( | |||
| <Form.Item | |||
| key={item.name} | |||
| label={item.name} | |||
| {...restField} | |||
| labelCol={{ flex: '140px' }} | |||
| name={[name, item.name]} | |||
| preserve={false} | |||
| required | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| ))} | |||
| </div> | |||
| <div className={styles['run-parameter__operation']}> | |||
| <Button | |||
| style={{ | |||
| marginRight: '3px', | |||
| }} | |||
| shape="circle" | |||
| disabled={fields.length === 1} | |||
| type="text" | |||
| size="middle" | |||
| icon={<MinusCircleOutlined />} | |||
| onClick={() => { | |||
| modalConfirm({ | |||
| title: '确定要删除该运行参数吗?', | |||
| onOk: () => { | |||
| remove(name); | |||
| }, | |||
| }); | |||
| }} | |||
| ></Button> | |||
| {index === fields.length - 1 && ( | |||
| <Button | |||
| shape="circle" | |||
| size="middle" | |||
| type="text" | |||
| onClick={() => add()} | |||
| icon={<PlusCircleOutlined />} | |||
| ></Button> | |||
| )} | |||
| </div> | |||
| </Flex> | |||
| ))} | |||
| </div> | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| ); | |||
| }} | |||
| </Form.Item> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="指标" | |||
| name="metric" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入指标内容', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入指标内容" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={0}> | |||
| <Col span={24}> | |||
| <Form.Item | |||
| label="优化方向" | |||
| name="mode" | |||
| rules={[{ required: true, message: '请选择优化方向' }]} | |||
| > | |||
| <Radio.Group> | |||
| <Radio value={'max'}>越大越好</Radio> | |||
| <Radio value={'min'}>越小越好</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="CPU 数" | |||
| name="cpu" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入 CPU 数', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入 CPU 数" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="GPU 数" | |||
| name="gpu" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入 GPU 数', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入 GPU 数" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| </> | |||
| ); | |||
| } | |||
| export default ExecuteConfig; | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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<string, any>); | |||
| 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 ( | |||
| <Form | |||
| labelCol={{ flex: '70px' }} | |||
| wrapperCol={{ flex: '1' }} | |||
| labelAlign="left" | |||
| form={form} | |||
| onFinish={handleFinish} | |||
| size="middle" | |||
| autoComplete="off" | |||
| scrollToFirstError | |||
| initialValues={initialValues} | |||
| className={styles['parameter-range']} | |||
| > | |||
| {isList ? ( | |||
| <div className={styles['parameter-range__list']}> | |||
| <Form.List name="list"> | |||
| {(fields, { add, remove }) => ( | |||
| <> | |||
| {fields.map(({ key, name, ...restField }, index) => ( | |||
| <Flex key={key} align="center"> | |||
| <Form.Item | |||
| style={{ flex: 1, minWidth: 0 }} | |||
| {...restField} | |||
| name={[name, 'value']} | |||
| rules={[{ required: true, message: '必填' }]} | |||
| > | |||
| <Input placeholder="请输入" allowClear /> | |||
| </Form.Item> | |||
| <Flex | |||
| style={{ | |||
| marginLeft: '10px', | |||
| marginBottom: '20px', | |||
| flex: 'none', | |||
| width: '66px', | |||
| }} | |||
| align="center" | |||
| > | |||
| <Button | |||
| shape="circle" | |||
| size="middle" | |||
| type="text" | |||
| disabled={fields.length === 1} | |||
| icon={<MinusCircleOutlined />} | |||
| onClick={() => remove(name)} | |||
| ></Button> | |||
| {index === fields.length - 1 && ( | |||
| <Button | |||
| shape="circle" | |||
| size="middle" | |||
| type="text" | |||
| onClick={() => add()} | |||
| icon={<PlusCircleOutlined />} | |||
| ></Button> | |||
| )} | |||
| </Flex> | |||
| </Flex> | |||
| ))} | |||
| {fields.length === 0 && ( | |||
| <Form.Item className={styles['add-weight']}> | |||
| <Button | |||
| className={styles['add-weight__button']} | |||
| color="primary" | |||
| variant="dashed" | |||
| onClick={() => add()} | |||
| block | |||
| icon={<PlusCircleOutlined />} | |||
| > | |||
| 添加 | |||
| </Button> | |||
| </Form.Item> | |||
| )} | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| </div> | |||
| ) : ( | |||
| formOptions.map((item) => { | |||
| return ( | |||
| <Form.Item | |||
| key={item.name} | |||
| label={item.label} | |||
| name={item.name} | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: `请输入${item.label}`, | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber style={{ width: '100%' }} placeholder={`请输入${item.label}`} /> | |||
| </Form.Item> | |||
| ); | |||
| }) | |||
| )} | |||
| <Form.Item className={styles['parameter-range__button']}> | |||
| <Button type="default" htmlType="button" onClick={onCancel}> | |||
| 取消 | |||
| </Button> | |||
| <Button type="primary" htmlType="submit" style={{ marginLeft: '20px' }}> | |||
| 确定 | |||
| </Button> | |||
| </Form.Item> | |||
| </Form> | |||
| ); | |||
| } | |||
| export default ParameterRange; | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<HTMLDivElement | null>(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 ( | |||
| <div ref={popconfirmRef}> | |||
| <Popconfirm | |||
| id="pop-parameter" | |||
| title="参数范围" | |||
| disabled={disabled} | |||
| description={ | |||
| <ParameterRange | |||
| type={type} | |||
| value={value} | |||
| onCancel={handleCancel} | |||
| onConfirm={handleConfirm} | |||
| ></ParameterRange> | |||
| } | |||
| okText="确定" | |||
| cancelText="取消" | |||
| overlayClassName={styles['parameter-range']} | |||
| icon={null} | |||
| open={open} | |||
| destroyTooltipOnHide | |||
| > | |||
| <div | |||
| className={classNames(styles['parameter-range__input'], { | |||
| [styles['parameter-range__input--disabled']]: disabled, | |||
| })} | |||
| onClick={handleClick} | |||
| > | |||
| <Typography.Text | |||
| ellipsis={{ tooltip: jsonText }} | |||
| style={{ color: 'inherit' }} | |||
| className={styles['parameter-range__input__text']} | |||
| > | |||
| {jsonText} | |||
| </Typography.Text> | |||
| <KFIcon type="icon-bianji" className={styles['parameter-range__input__icon']} /> | |||
| </div> | |||
| </Popconfirm> | |||
| </div> | |||
| ); | |||
| } | |||
| export default PopParameterRange; | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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'; | |||
| } | |||
| }; | |||
| @@ -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%; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<TableData[]>([]); | |||
| 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<TableData>['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 ( | |||
| <div className={styles['experiment-history']}> | |||
| <div className={styles['experiment-history__content']}> | |||
| <div | |||
| className={classNames( | |||
| 'vertical-scroll-table-no-page', | |||
| styles['experiment-history__content__table'], | |||
| )} | |||
| > | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| pagination={false} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default ExperimentHistory; | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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<string | undefined>(''); | |||
| 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 ( | |||
| <div className={styles['experiment-result']}> | |||
| <InfoGroup title="实验结果" height={420} width="100%"> | |||
| <div className={styles['experiment-result__text']}>{result}</div> | |||
| </InfoGroup> | |||
| <InfoGroup title="可视化结果" style={{ margin: '16px 0' }}> | |||
| <div className={styles['experiment-result__images']}> | |||
| <Image.PreviewGroup | |||
| preview={{ | |||
| onChange: (current, prev) => | |||
| console.log(`current index: ${current}, prev index: ${prev}`), | |||
| }} | |||
| > | |||
| {images.map((item) => ( | |||
| <Image | |||
| key={item} | |||
| className={styles['experiment-result__images__item']} | |||
| src={item} | |||
| height={248} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| ))} | |||
| </Image.PreviewGroup> | |||
| </div> | |||
| </InfoGroup> | |||
| {modelPath && ( | |||
| <div className={styles['experiment-result__download']}> | |||
| <span style={{ marginRight: '12px', color: '#606b7a' }}>文件名</span> | |||
| <span>save_model.joblib</span> | |||
| <Button | |||
| type="primary" | |||
| className={styles['experiment-result__download__btn']} | |||
| onClick={() => { | |||
| window.location.href = modelPath; | |||
| }} | |||
| > | |||
| 模型下载 | |||
| </Button> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| export default ExperimentResult; | |||
| @@ -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; | |||
| }; | |||
| @@ -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 | |||
| }); | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| export const xlCols = { span: 12 }; | |||
| export const xllCols = { span: 10 }; | |||
| export const formCols = { xl: xlCols, xxl: xllCols }; | |||