Browse Source

feat: 新增超参数自动寻优功能

pull/171/head
cp3hnu 1 year ago
parent
commit
62c7f056e4
37 changed files with 3084 additions and 428 deletions
  1. +36
    -0
      react-ui/config/routes.ts
  2. +2
    -3
      react-ui/src/pages/AutoML/Create/index.tsx
  3. +3
    -411
      react-ui/src/pages/AutoML/List/index.tsx
  4. +1
    -4
      react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx
  5. +10
    -1
      react-ui/src/pages/AutoML/components/CreateForm/index.less
  6. +10
    -8
      react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx
  7. +75
    -0
      react-ui/src/pages/AutoML/components/ExperimentList/config.ts
  8. +1
    -1
      react-ui/src/pages/AutoML/components/ExperimentList/index.less
  9. +428
    -0
      react-ui/src/pages/AutoML/components/ExperimentList/index.tsx
  10. +55
    -0
      react-ui/src/pages/HyperParameter/Create/index.less
  11. +165
    -0
      react-ui/src/pages/HyperParameter/Create/index.tsx
  12. +40
    -0
      react-ui/src/pages/HyperParameter/Info/index.less
  13. +61
    -0
      react-ui/src/pages/HyperParameter/Info/index.tsx
  14. +42
    -0
      react-ui/src/pages/HyperParameter/Instance/index.less
  15. +215
    -0
      react-ui/src/pages/HyperParameter/Instance/index.tsx
  16. +13
    -0
      react-ui/src/pages/HyperParameter/List/index.tsx
  17. +13
    -0
      react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less
  18. +308
    -0
      react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.tsx
  19. +20
    -0
      react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less
  20. +26
    -0
      react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx
  21. +54
    -0
      react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx
  22. +504
    -0
      react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx
  23. +13
    -0
      react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less
  24. +141
    -0
      react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx
  25. +47
    -0
      react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less
  26. +97
    -0
      react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx
  27. +108
    -0
      react-ui/src/pages/HyperParameter/components/CreateForm/index.less
  28. +155
    -0
      react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts
  29. +14
    -0
      react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less
  30. +132
    -0
      react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx
  31. +0
    -0
      react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less
  32. +0
    -0
      react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx
  33. +52
    -0
      react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less
  34. +83
    -0
      react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx
  35. +64
    -0
      react-ui/src/pages/HyperParameter/types.ts
  36. +93
    -0
      react-ui/src/services/hyperParameter/index.js
  37. +3
    -0
      react-ui/src/utils/constant.ts

+ 36
- 0
react-ui/config/routes.ts View File

@@ -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',
},
],
},
],
},
{


+ 2
- 3
react-ui/src/pages/AutoML/Create/index.tsx View File

@@ -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
- 411
react-ui/src/pages/AutoML/List/index.tsx View File

@@ -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;

+ 1
- 4
react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx View File

@@ -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',


+ 10
- 1
react-ui/src/pages/AutoML/components/CreateForm/index.less View File

@@ -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;
}
}
}

+ 10
- 8
react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx View File

@@ -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);


+ 75
- 0
react-ui/src/pages/AutoML/components/ExperimentList/config.ts View File

@@ -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',
},
};

react-ui/src/pages/AutoML/List/index.less → react-ui/src/pages/AutoML/components/ExperimentList/index.less View File

@@ -1,4 +1,4 @@
.auto-ml-list {
.experiment-list {
height: 100%;
&__content {
height: calc(100% - 60px);

+ 428
- 0
react-ui/src/pages/AutoML/components/ExperimentList/index.tsx View File

@@ -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;

+ 55
- 0
react-ui/src/pages/HyperParameter/Create/index.less View File

@@ -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;
}
}
}
}

+ 165
- 0
react-ui/src/pages/HyperParameter/Create/index.tsx View File

@@ -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;

+ 40
- 0
react-ui/src/pages/HyperParameter/Info/index.less View File

@@ -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: '';
}
}
}

+ 61
- 0
react-ui/src/pages/HyperParameter/Info/index.tsx View File

@@ -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;

+ 42
- 0
react-ui/src/pages/HyperParameter/Instance/index.less View File

@@ -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;
}
}

+ 215
- 0
react-ui/src/pages/HyperParameter/Instance/index.tsx View File

@@ -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;

+ 13
- 0
react-ui/src/pages/HyperParameter/List/index.tsx View File

@@ -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;

+ 13
- 0
react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less View File

@@ -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;
}
}
}

+ 308
- 0
react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.tsx View File

@@ -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;

+ 20
- 0
react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less View File

@@ -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;
}
}
}
}
}

+ 26
- 0
react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx View File

@@ -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;

+ 54
- 0
react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx View File

@@ -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;

+ 504
- 0
react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx View File

@@ -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;

+ 13
- 0
react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less View File

@@ -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;
}
}

+ 141
- 0
react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx View File

@@ -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;

+ 47
- 0
react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less View File

@@ -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;
}
}
}

+ 97
- 0
react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx View File

@@ -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;

+ 108
- 0
react-ui/src/pages/HyperParameter/components/CreateForm/index.less View File

@@ -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;
}
}

+ 155
- 0
react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts View File

@@ -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';
}
};

+ 14
- 0
react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less View File

@@ -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%;
}
}
}

+ 132
- 0
react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx View File

@@ -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
react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less View File


+ 0
- 0
react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx View File


+ 52
- 0
react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less View File

@@ -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);
}
}
}

+ 83
- 0
react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx View File

@@ -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;

+ 64
- 0
react-ui/src/pages/HyperParameter/types.ts View File

@@ -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;
};

+ 93
- 0
react-ui/src/services/hyperParameter/index.js View File

@@ -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
});
}


+ 3
- 0
react-ui/src/utils/constant.ts View File

@@ -0,0 +1,3 @@
export const xlCols = { span: 12 };
export const xllCols = { span: 10 };
export const formCols = { xl: xlCols, xxl: xllCols };

Loading…
Cancel
Save