Browse Source

feat: 添加主动学习功能

pull/197/head
cp3hnu 11 months ago
parent
commit
d05057c976
27 changed files with 1870 additions and 5 deletions
  1. +38
    -2
      react-ui/config/routes.ts
  2. +21
    -0
      react-ui/src/components/PageTitle/index.less
  3. +13
    -2
      react-ui/src/components/PageTitle/index.tsx
  4. +55
    -0
      react-ui/src/pages/ActiveLearn/Create/index.less
  5. +141
    -0
      react-ui/src/pages/ActiveLearn/Create/index.tsx
  6. +40
    -0
      react-ui/src/pages/ActiveLearn/Info/index.less
  7. +45
    -0
      react-ui/src/pages/ActiveLearn/Info/index.tsx
  8. +42
    -0
      react-ui/src/pages/ActiveLearn/Instance/index.less
  9. +215
    -0
      react-ui/src/pages/ActiveLearn/Instance/index.tsx
  10. +13
    -0
      react-ui/src/pages/ActiveLearn/List/index.tsx
  11. +13
    -0
      react-ui/src/pages/ActiveLearn/components/BasicInfo/index.less
  12. +194
    -0
      react-ui/src/pages/ActiveLearn/components/BasicInfo/index.tsx
  13. +54
    -0
      react-ui/src/pages/ActiveLearn/components/CreateForm/BasicConfig.tsx
  14. +262
    -0
      react-ui/src/pages/ActiveLearn/components/CreateForm/ExecuteConfig.tsx
  15. +145
    -0
      react-ui/src/pages/ActiveLearn/components/CreateForm/index.less
  16. +97
    -0
      react-ui/src/pages/ActiveLearn/components/CreateForm/utils.ts
  17. +14
    -0
      react-ui/src/pages/ActiveLearn/components/ExperimentHistory/index.less
  18. +132
    -0
      react-ui/src/pages/ActiveLearn/components/ExperimentHistory/index.tsx
  19. +0
    -0
      react-ui/src/pages/ActiveLearn/components/ExperimentLog/index.less
  20. +0
    -0
      react-ui/src/pages/ActiveLearn/components/ExperimentLog/index.tsx
  21. +52
    -0
      react-ui/src/pages/ActiveLearn/components/ExperimentResult/index.less
  22. +83
    -0
      react-ui/src/pages/ActiveLearn/components/ExperimentResult/index.tsx
  23. +61
    -0
      react-ui/src/pages/ActiveLearn/types.ts
  24. +0
    -1
      react-ui/src/pages/AutoML/Instance/index.tsx
  25. +24
    -0
      react-ui/src/pages/AutoML/components/ExperimentList/config.ts
  26. +93
    -0
      react-ui/src/services/activeLearn/index.js
  27. +23
    -0
      react-ui/src/stories/PageTitle.stories.tsx

+ 38
- 2
react-ui/config/routes.ts View File

@@ -176,7 +176,7 @@ export default [
},
{
name: '实验实例详情',
path: 'instance/:autoMLId/:id',
path: 'instance/:experimentId/:id',
component: './AutoML/Instance/index',
},
],
@@ -212,11 +212,47 @@ export default [
},
{
name: '实验实例详情',
path: 'instance/:autoMLId/:id',
path: 'instance/:experimentId/:id',
component: './HyperParameter/Instance/index',
},
],
},
{
name: '主动学习',
path: 'active-learn',
routes: [
{
name: '超参数寻优',
path: '',
component: './ActiveLearn/List/index',
},
{
name: '实验详情',
path: 'info/:id',
component: './ActiveLearn/Info/index',
},
{
name: '创建实验',
path: 'create',
component: './ActiveLearn/Create/index',
},
{
name: '编辑实验',
path: 'edit/:id',
component: './ActiveLearn/Create/index',
},
{
name: '复制实验',
path: 'copy/:id',
component: './ActiveLearn/Create/index',
},
{
name: '实验实例详情',
path: 'instance/:experimentId/:id',
component: './ActiveLearn/Instance/index',
},
],
},
],
},
{


+ 21
- 0
react-ui/src/components/PageTitle/index.less View File

@@ -7,4 +7,25 @@
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;

&__tips {
position: relative;
margin-left: 18px;
padding: 3px 15px;
color: @primary-color;
background: .addAlpha(@primary-color, 0.1) [];
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.1) [];
border-bottom: 4px solid transparent;
content: '';
}
}
}

+ 13
- 2
react-ui/src/components/PageTitle/index.tsx View File

@@ -3,6 +3,7 @@
* @Date: 2024-04-17 14:01:46
* @Description: 页面标题
*/
import KFIcon from '@/components/KFIcon';
import classNames from 'classnames';
import React from 'react';
import './index.less';
@@ -10,19 +11,29 @@ import './index.less';
type PageTitleProps = {
/** 标题 */
title: React.ReactNode;
/** 图标 */
tooltip?: string;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
/** 自定义标题 */
titleRender?: () => React.ReactNode;
};

/**
* 页面标题
*/
function PageTitle({ title, style, className = '' }: PageTitleProps) {
function PageTitle({ title, style, className, tooltip }: PageTitleProps) {
return (
<div className={classNames('kf-page-title', className)} style={style}>
{title}
<div>{title}</div>
{tooltip && (
<div className="kf-page-title__tips">
<KFIcon type="icon-tishi" font={14} />
<span style={{ marginLeft: '8px', fontSize: '14px' }}>{tooltip}</span>
</div>
)}
</div>
);
}


+ 55
- 0
react-ui/src/pages/ActiveLearn/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: @text-disabled-color;
}

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

+ 141
- 0
react-ui/src/pages/ActiveLearn/Create/index.tsx View File

@@ -0,0 +1,141 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 创建实验
*/
import PageTitle from '@/components/PageTitle';
import {
addActiveLearnReq,
getActiveLearnInfoReq,
updateActiveLearnReq,
} from '@/services/activeLearn';
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 { ActiveLearnData, FormData } from '../types';
import styles from './index.less';

function CreateActiveLearn() {
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)) {
getActiveLearnInfo(id);
}
}, [id]);

// 获取服务详情
const getActiveLearnInfo = async (id: number) => {
const [res] = await to(getActiveLearnInfoReq({ id }));
if (res && res.data) {
const info: ActiveLearnData = res.data;
const { name: name_str, ...rest } = info;
const name = isCopy ? `${name_str}-copy` : name_str;
const formData = {
...rest,
name,
};

form.setFieldsValue(formData);
}
};

// 创建、更新、复制实验
const createExperiment = async (formData: FormData) => {
// 根据后台要求,修改表单数据
const object = {
...formData,
};

const params =
id && !isCopy
? {
id: id,
...object,
}
: object;

const request = id && !isCopy ? updateActiveLearnReq : addActiveLearnReq;
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} tooltip="仅支持二分类及多分类任务"></PageTitle>
<div className={styles['create-hyperparameter__content']}>
<div>
<Form
name="create-active-learn"
labelCol={{ flex: '160px' }}
labelAlign="left"
form={form}
onFinish={handleSubmit}
size="large"
autoComplete="off"
scrollToFirstError
initialValues={{
test_ratio: 0.3,
initial_label_rate: 0.1,
}}
>
<BasicConfig />
<ExecuteConfig />

<Form.Item wrapperCol={{ offset: 0, span: 16 }} style={{ marginTop: '40px' }}>
<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 CreateActiveLearn;

+ 40
- 0
react-ui/src/pages/ActiveLearn/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: '';
}
}
}

+ 45
- 0
react-ui/src/pages/ActiveLearn/Info/index.tsx View File

@@ -0,0 +1,45 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 主动学习实验详情
*/
import PageTitle from '@/components/PageTitle';
import { getActiveLearnInfoReq } from '@/services/activeLearn';
import { safeInvoke } from '@/utils/functional';
import { to } from '@/utils/promise';
import { useParams } from '@umijs/max';
import { useEffect, useState } from 'react';
import BasicInfo from '../components/BasicInfo';
import { ActiveLearnData } from '../types';
import styles from './index.less';

function ActiveLearnInfo() {
const params = useParams();
const id = safeInvoke(Number)(params.id);
const [info, setInfo] = useState<ActiveLearnData | undefined>(undefined);

useEffect(() => {
if (id) {
getActiveLearnInfo();
}
}, []);

// 获取主动学习详情
const getActiveLearnInfo = async () => {
const [res] = await to(getActiveLearnInfoReq({ id: id }));
if (res && res.data) {
setInfo(res.data);
}
};

return (
<div className={styles['auto-ml-info']}>
<PageTitle title="实验详情"></PageTitle>
<div className={styles['auto-ml-info__content']}>
<BasicInfo info={info} />
</div>
</div>
);
}

export default ActiveLearnInfo;

+ 42
- 0
react-ui/src/pages/ActiveLearn/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/ActiveLearn/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 BasicInfo from '../components/BasicInfo';
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: (
<BasicInfo
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/ActiveLearn/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 ActiveLearn() {
return <ExperimentList type={ExperimentListType.ActiveLearn} />;
}

export default ActiveLearn;

+ 13
- 0
react-ui/src/pages/ActiveLearn/components/BasicInfo/index.less View File

@@ -0,0 +1,13 @@
.hyper-parameter-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;
}
}
}

+ 194
- 0
react-ui/src/pages/ActiveLearn/components/BasicInfo/index.tsx View File

@@ -0,0 +1,194 @@
import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo';
import {
classifierAlgorithms,
performanceMetrics,
queryStrategies,
stoppingCriterions,
StoppingCriterionsType,
} from '@/pages/ActiveLearn/components/CreateForm/utils';
import { ActiveLearnData } from '@/pages/ActiveLearn/types';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { type NodeStatus } from '@/types';
import { elapsedTime } from '@/utils/date';
import { formatDataset, formatDate, formatEnum } from '@/utils/format';
import { Flex } from 'antd';
import classNames from 'classnames';
import { useMemo } from 'react';
import styles from './index.less';

type BasicInfoProps = {
info?: ActiveLearnData;
className?: string;
isInstance?: boolean;
runStatus?: NodeStatus;
};

function BasicInfo({ info, className, runStatus, isInstance = false }: BasicInfoProps) {
const basicDatas: BasicInfoData[] = useMemo(() => {
if (!info) {
return [];
}

return [
{
label: '实验名称',
value: info.name,
},
{
label: '实验描述',
value: info.description,
},
{
label: '创建人',
value: info.create_by,
},
{
label: '创建时间',
value: info.create_time,
format: formatDate,
},
{
label: '更新时间',
value: info.update_time,
format: formatDate,
},
];
}, [info]);

const configDatas: BasicInfoData[] = useMemo(() => {
if (!info) {
return [];
}
let stopping_criterion_params_title = '';
let stopping_criterion_params_value: number | undefined = undefined;
if (info.stopping_criterion === StoppingCriterionsType.NumOfQueries) {
stopping_criterion_params_title = '查询次数';
stopping_criterion_params_value = info.num_of_queries;
} else if (info.stopping_criterion === StoppingCriterionsType.PercentOfUnlabel) {
stopping_criterion_params_title = '未标记比例';
stopping_criterion_params_value = info.percent_of_unlabel;
} else {
stopping_criterion_params_title = '时间限制';
stopping_criterion_params_value = info.time_limit;
}

return [
{
label: '分类算法',
value: info.classifier_type,
format: formatEnum(classifierAlgorithms),
},
{
label: '停止判则',
value: info.stopping_criterion,
format: formatEnum(stoppingCriterions),
},
{
label: stopping_criterion_params_title,
value: stopping_criterion_params_value,
},
{
label: '查询策略',
value: info.query_strategy,
format: formatEnum(queryStrategies),
},
{
label: '试验次数',
value: info.num_of_experiment,
},
{
label: '指标',
value: info.performance_metric,
format: formatEnum(performanceMetrics),
},
{
label: '数据集',
value: info.dataset,
format: formatDataset,
},
{
label: '预测目标列',
value: info.target_columns,
},
{
label: '测试集比率',
value: info.test_ratio,
},
{
label: '初始标记数据比率',
value: info.initial_label_rate,
},
];
}, [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['hyper-parameter-basic'], className)}>
{isInstance && runStatus && (
<ConfigInfo
title="运行信息"
datas={instanceDatas}
labelWidth={70}
style={{ marginBottom: '20px' }}
/>
)}
{!isInstance && (
<ConfigInfo
title="基本信息"
datas={basicDatas}
labelWidth={70}
style={{ marginBottom: '20px' }}
/>
)}
<ConfigInfo
title="配置信息"
datas={configDatas}
labelWidth={120}
style={{ marginBottom: '20px' }}
></ConfigInfo>
</div>
);
}

export default BasicInfo;

+ 54
- 0
react-ui/src/pages/ActiveLearn/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;

+ 262
- 0
react-ui/src/pages/ActiveLearn/components/CreateForm/ExecuteConfig.tsx View File

@@ -0,0 +1,262 @@
import ResourceSelect, {
requiredValidator,
ResourceSelectorType,
} from '@/components/ResourceSelect';
import SubAreaTitle from '@/components/SubAreaTitle';
import { Col, Form, Input, InputNumber, Row, Select } from 'antd';
import {
classifierAlgorithms,
performanceMetrics,
queryStrategies,
stoppingCriterions,
StoppingCriterionsType,
} from './utils';

function ExecuteConfig() {
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="classifier_type"
rules={[
{
required: true,
message: '请选择分类算法',
},
]}
>
<Select
placeholder="请选择分类算法"
options={classifierAlgorithms}
showSearch
allowClear
/>
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="停止判则"
name="stopping_criterion"
rules={[
{
required: true,
message: '请选择停止判则',
},
]}
>
<Select
placeholder="请选择停止判则"
options={stoppingCriterions}
showSearch
allowClear
/>
</Form.Item>
</Col>
</Row>

<Form.Item dependencies={['stopping_criterion']} noStyle>
{({ getFieldValue }) => {
const stopping_criterion = getFieldValue('stopping_criterion');
if (stopping_criterion === StoppingCriterionsType.NumOfQueries) {
return (
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="查询次数"
name="num_of_queries"
rules={[
{
required: true,
message: '请输入查询次数',
},
]}
>
<InputNumber placeholder="请输入查询次数" min={0} precision={0} />
</Form.Item>
</Col>
</Row>
);
} else if (stopping_criterion === StoppingCriterionsType.PercentOfUnlabel) {
return (
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="未标记比例"
name="percent_of_unlabel"
rules={[
{
required: true,
message: '请输入未标记比例',
},
]}
>
<InputNumber placeholder="请输入未标记比例" min={0} />
</Form.Item>
</Col>
</Row>
);
} else if (stopping_criterion === StoppingCriterionsType.TimeLimit) {
return (
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="时间限制"
name="time_limit"
rules={[
{
required: true,
message: '请输入时间限制',
},
]}
>
<InputNumber placeholder="请输入时间限制" min={0} />
</Form.Item>
</Col>
</Row>
);
} else {
return null;
}
}}
</Form.Item>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="查询策略"
name="query_strategy"
rules={[
{
required: true,
message: '请选择查询策略',
},
]}
>
<Select placeholder="请选择查询策略" options={queryStrategies} showSearch allowClear />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="试验次数"
name="num_of_experiment"
rules={[
{
required: true,
message: '请输入试验次数',
},
]}
>
<InputNumber placeholder="请输入试验次数" min={0} precision={0} />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="指标"
name="performance_metric"
rules={[
{
required: true,
message: '请选择指标',
},
]}
>
<Select placeholder="请选择指标" options={performanceMetrics} showSearch 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="target_columns"
rules={[
{
required: true,
message: '请输入预测目标列',
},
]}
>
<Input placeholder="请输入预测目标列" maxLength={256} showCount allowClear />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="测试集比率"
name="test_ratio"
rules={[
{
required: true,
message: '请输入测试集比率',
},
]}
>
<InputNumber placeholder="请输入测试集比率" min={0} />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="初始标记数据比率"
name="initial_label_rate"
rules={[
{
required: true,
message: '请输入初始标记数据比率',
},
]}
>
<InputNumber placeholder="请输入初始标记数据比率" min={0} />
</Form.Item>
</Col>
</Row>
</>
);
}

export default ExecuteConfig;

+ 145
- 0
react-ui/src/pages/ActiveLearn/components/CreateForm/index.less View File

@@ -0,0 +1,145 @@
.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: @error-color;
font-size: 14px;
font-family: SimSun, sans-serif;
line-height: 1;
content: '*';
margin-inline-end: 4px;
}

:global {
.anticon-question-circle {
vertical-align: middle;
cursor: help;
}
}
}

&__tooltip {
max-width: 600px;
:global {
.ant-tooltip-inner {
max-height: 400px;
overflow-y: auto;
white-space: pre-line;

&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.5);
}
}
}
}

&__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;

&__body {
flex: 1;
margin-right: 10px;
padding: 20px 20px 0;
border: 1px dashed #e0e0e0;
border-radius: 8px;

:global {
.ant-form-item-label {
label {
width: calc(100% - 10px);
}
}
}
}

&__operation {
display: flex;
flex: none;
align-items: center;
width: 100px;
}

&__error {
margin-top: -20px;
color: @error-color;
}
}

+ 97
- 0
react-ui/src/pages/ActiveLearn/components/CreateForm/utils.ts View File

@@ -0,0 +1,97 @@
// 分类算法
export const classifierAlgorithms = [
{
label: 'logistic_regression(逻辑回归)',
value: 'logistic_regression',
},
{
label: 'decision_tree(决策树)',
value: 'decision_tree',
},
{
label: 'random_forest(随机森林)',
value: 'random_forest',
},
{
label: 'SVM(支持向量机)',
value: 'SVM',
},
{
label: 'naive_bayes(朴素贝叶斯)',
value: 'naive_bayes',
},
{
label: 'GBM(梯度提升树)',
value: 'GBM',
},
];

export enum StoppingCriterionsType {
NumOfQueries = 'num_of_queries',
PercentOfUnlabel = 'percent_of_unlabel',
TimeLimit = 'time_limit',
}

// 停止判则
export const stoppingCriterions = [
{
label: 'num_of_queries(查询次数)',
value: 'num_of_queries',
},
{
label: 'percent_of_unlabel(未标记样本比例)',
value: 'percent_of_unlabel',
},
{
label: 'time_limit(时间限制)',
value: 'time_limit',
},
];

// 查询策略
export const queryStrategies = [
{
label: 'Uncertainty(不确定性)',
value: 'Uncertainty',
},
{
label: 'QBC(委员会查询)',
value: 'QBC',
},
{
label: 'Random(随机)',
value: 'Random',
},
{
label: 'GraphDensity(图密度)',
value: 'GraphDensity',
},
];

// 指标
export const performanceMetrics = [
{
label: 'accuracy_score',
value: 'accuracy_score',
},
{
label: 'roc_auc_score',
value: 'roc_auc_score',
},
{
label: 'get_fps_tps_thresholds',
value: 'get_fps_tps_thresholds',
},
{
label: 'hamming_loss',
value: 'hamming_loss',
},
{
label: 'one_error',
value: 'one_error',
},
{
label: 'coverage_error',
value: 'coverage_error',
},
];

+ 14
- 0
react-ui/src/pages/ActiveLearn/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/ActiveLearn/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/ActiveLearn/components/ExperimentLog/index.less View File


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


+ 52
- 0
react-ui/src/pages/ActiveLearn/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/ActiveLearn/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;

+ 61
- 0
react-ui/src/pages/ActiveLearn/types.ts View File

@@ -0,0 +1,61 @@
import { type ParameterInputObject } from '@/components/ResourceSelect';
import { type NodeStatus } from '@/types';

// 操作类型
export enum OperationType {
Create = 'Create', // 创建
Update = 'Update', // 更新
}

// 表单数据
export type FormData = {
name: string; // 实验名称
description: string; // 实验描述
dataset: ParameterInputObject; // 数据集
classifier_type: string; // 分类算法
stopping_criterion: string; // 停止判则
query_strategy: string; // 查询策略
num_of_experiment: number; // 试验次数
performance_metric: string; // 指标
target_columns: string; // 预测目标列
test_ratio: number; // 测试集比率
initial_label_rate: number; // 初始标记数据比率
num_of_queries?: number; // 查询次数
percent_of_unlabel: number; // 未标记比例
time_limit: number; // 时间限制
};

// 主动学习
export type ActiveLearnData = {
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 ActiveLearnInstanceData = {
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
- 1
react-ui/src/pages/AutoML/Instance/index.tsx View File

@@ -27,7 +27,6 @@ function AutoMLInstance() {
const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | 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);



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

@@ -4,6 +4,15 @@
* @Description: 实验列表组件配置
*/

import {
batchDeleteActiveLearnInsReq,
deleteActiveLearnInsReq,
deleteActiveLearnReq,
getActiveLearnInsListReq,
getActiveLearnListReq,
runActiveLearnReq,
stopActiveLearnInsReq,
} from '@/services/activeLearn';
import {
batchDeleteExperimentInsReq,
deleteAutoMLReq,
@@ -26,6 +35,7 @@ import {
export enum ExperimentListType {
AutoML = 'AutoML',
HyperParameter = 'HyperParameter',
ActiveLearn = 'ActiveLearn',
}

type ExperimentListInfo = {
@@ -72,4 +82,18 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo
descProperty: 'description',
idProperty: 'rayId',
},
[ExperimentListType.ActiveLearn]: {
getListReq: getActiveLearnListReq,
getInsListReq: getActiveLearnInsListReq,
deleteRecordReq: deleteActiveLearnReq,
runRecordReq: runActiveLearnReq,
deleteInsReq: deleteActiveLearnInsReq,
batchDeleteInsReq: batchDeleteActiveLearnInsReq,
stopInsReq: stopActiveLearnInsReq,
title: '自动学习',
pathPrefix: 'active-learn',
nameProperty: 'name',
descProperty: 'description',
idProperty: 'activeLearnId',
},
};

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

@@ -0,0 +1,93 @@
/*
* @Author: 赵伟
* @Date: 2024-11-18 10:18:27
* @Description: 主动学习
*/

import { request } from '@umijs/max';


// 分页查询超参数自动寻优
export function getActiveLearnListReq(params) {
return request(`/api/mmp/activeLearn`, {
method: 'GET',
params,
});
}

// 查询超参数自动寻优详情
export function getActiveLearnInfoReq(params) {
return request(`/api/mmp/activeLearn/getActiveLearnDetail`, {
method: 'GET',
params,
});
}

// 新增超参数自动寻优
export function addActiveLearnReq(data) {
return request(`/api/mmp/activeLearn`, {
method: 'POST',
data,
});
}

// 编辑超参数自动寻优
export function updateActiveLearnReq(data) {
return request(`/api/mmp/activeLearn`, {
method: 'PUT',
data,
});
}

// 删除超参数自动寻优
export function deleteActiveLearnReq(id) {
return request(`/api/mmp/activeLearn/${id}`, {
method: 'DELETE',
});
}

// 运行超参数自动寻优
export function runActiveLearnReq(id) {
return request(`/api/mmp/activeLearn/run/${id}`, {
method: 'POST',
});
}

// ----------------------- 实验实例 -----------------------
// 获取实验实例列表
export function getActiveLearnInsListReq(params) {
return request(`/api/mmp/activeLearnIns`, {
method: 'GET',
params,
});
}

// 查询实验实例详情
export function getActiveLearnInsReq(id) {
return request(`/api/mmp/activeLearnIns/${id}`, {
method: 'GET',
});
}

// 停止实验实例
export function stopActiveLearnInsReq(id) {
return request(`/api/mmp/activeLearnIns/${id}`, {
method: 'PUT',
});
}

// 删除实验实例
export function deleteActiveLearnInsReq(id) {
return request(`/api/mmp/activeLearnIns/${id}`, {
method: 'DELETE',
});
}

// 批量删除实验实例
export function batchDeleteActiveLearnInsReq(data) {
return request(`/api/mmp/activeLearnIns/batchDelete`, {
method: 'DELETE',
data
});
}


+ 23
- 0
react-ui/src/stories/PageTitle.stories.tsx View File

@@ -1,5 +1,6 @@
import PageTitle from '@/components/PageTitle';
import type { Meta, StoryObj } from '@storybook/react';
import { Tabs } from 'antd';

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
@@ -28,3 +29,25 @@ export const Primary: Story = {
title: '数据集列表',
},
};

/* 带有提示信息 */
export const WithTooltip: Story = {
args: {
title: '数据集列表',
tooltip: '其它提示',
},
};

/* Title 可以是 ReactNode */
export const Custom: Story = {
args: {
title: (
<Tabs
items={[
{ label: 'Tab 1', key: '1' },
{ label: 'Tab 2', key: '2' },
]}
></Tabs>
),
},
};

Loading…
Cancel
Save