From ed0bb92c11334016e691049a36fedb08b8343ed4 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Thu, 27 Feb 2025 13:59:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B6=85=E5=8F=82=E6=95=B0=E5=AF=BB?= =?UTF-8?q?=E4=BC=98-=E5=AE=9E=E9=AA=8C=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/components/FormInfo/index.tsx | 14 +- react-ui/src/pages/AutoML/Instance/index.tsx | 6 +- .../components/ExperimentResult/index.less | 1 + .../pages/Experiment/Comparison/index.less | 20 --- .../Experiment/components/LogGroup/index.less | 3 +- .../pages/HyperParameter/Create/index.less | 7 +- .../src/pages/HyperParameter/Create/index.tsx | 4 +- .../src/pages/HyperParameter/Info/index.less | 2 +- .../src/pages/HyperParameter/Info/index.tsx | 4 +- .../pages/HyperParameter/Instance/index.less | 2 +- .../pages/HyperParameter/Instance/index.tsx | 84 ++++++--- .../components/ExperimentHistory/index.less | 21 +++ .../components/ExperimentHistory/index.tsx | 168 ++++++++---------- .../components/ExperimentLog/index.tsx | 83 +++++++++ .../components/ExperimentResult/index.less | 41 +---- .../components/ExperimentResult/index.tsx | 93 +++++----- .../components/HyperParameterBasic/index.tsx | 2 +- react-ui/src/pages/HyperParameter/types.ts | 27 ++- 18 files changed, 330 insertions(+), 252 deletions(-) diff --git a/react-ui/src/components/FormInfo/index.tsx b/react-ui/src/components/FormInfo/index.tsx index 7e8e95ad..c1e23cbe 100644 --- a/react-ui/src/components/FormInfo/index.tsx +++ b/react-ui/src/components/FormInfo/index.tsx @@ -4,9 +4,9 @@ import classNames from 'classnames'; import './index.less'; type FormInfoProps = { - /** 自定义类名 */ + /** 值 */ value?: any; - /** 如果 `value` 是对象时,取对象的哪个属性作为值 */ + /** 如果 `value` 是对象,取对象的哪个属性作为值 */ valuePropName?: string; /** 是否是多行文本 */ textArea?: boolean; @@ -32,11 +32,11 @@ function FormInfo({ style, textArea = false, }: FormInfoProps) { - let data = value; + let showValue = value; if (value && typeof value === 'object' && valuePropName) { - data = value[valuePropName]; + showValue = value[valuePropName]; } else if (select === true && options) { - data = formatEnum(options)(value); + showValue = formatEnum(options)(value); } return ( @@ -50,8 +50,8 @@ function FormInfo({ )} style={style} > - - {data} + + {showValue} ); diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx index aefee532..19a6414d 100644 --- a/react-ui/src/pages/AutoML/Instance/index.tsx +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -22,6 +22,8 @@ enum TabKeys { History = 'history', } +const NodePrefix = 'auto-hpo'; + function AutoMLInstance() { const [activeTab, setActiveTab] = useState(TabKeys.Params); const [autoMLInfo, setAutoMLInfo] = useState(undefined); @@ -66,7 +68,7 @@ function AutoMLInstance() { const nodeStatusJson = parseJsonText(node_status); if (nodeStatusJson) { Object.keys(nodeStatusJson).forEach((key) => { - if (key.startsWith('auto-ml')) { + if (key.startsWith(NodePrefix)) { const value = nodeStatusJson[key]; info.nodeStatus = value; } @@ -100,7 +102,7 @@ function AutoMLInstance() { const nodes = dataJson?.result?.object?.status?.nodes; if (nodes) { const statusData = Object.values(nodes).find((node: any) => - node.displayName.startsWith('auto-ml'), + node.displayName.startsWith(NodePrefix), ) as NodeStatus; if (statusData) { setInstanceInfo((prev) => ({ diff --git a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less index 342817c3..bcb52314 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less +++ b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less @@ -25,6 +25,7 @@ } &__text { + font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; white-space: pre-wrap; } diff --git a/react-ui/src/pages/Experiment/Comparison/index.less b/react-ui/src/pages/Experiment/Comparison/index.less index 4dce8268..b0984b91 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.less +++ b/react-ui/src/pages/Experiment/Comparison/index.less @@ -18,26 +18,6 @@ background-color: white; border-radius: 10px; - &__footer { - display: flex; - align-items: center; - padding-top: 20px; - color: @text-color-secondary; - font-size: 12px; - background-color: white; - - div { - flex: 1; - height: 1px; - background-color: @border-color; - } - - p { - flex: none; - margin: 0 8px; - } - } - :global { .ant-table-container { border: none !important; diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.less b/react-ui/src/pages/Experiment/components/LogGroup/index.less index 2962a2c6..48012951 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.less +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.less @@ -20,7 +20,8 @@ padding: 15px; color: white; font-size: 14px; - white-space: pre-line; + font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; + white-space: pre-wrap; text-align: left; word-break: break-all; background: #19253b; diff --git a/react-ui/src/pages/HyperParameter/Create/index.less b/react-ui/src/pages/HyperParameter/Create/index.less index 145be0d1..ae065195 100644 --- a/react-ui/src/pages/HyperParameter/Create/index.less +++ b/react-ui/src/pages/HyperParameter/Create/index.less @@ -1,4 +1,4 @@ -.create-hyperparameter { +.create-hyper-parameter { height: 100%; &__content { @@ -11,11 +11,6 @@ background-color: white; border-radius: 10px; - &__type { - color: @text-color; - font-size: @font-size-input-lg; - } - :global { .ant-input-number { width: 100%; diff --git a/react-ui/src/pages/HyperParameter/Create/index.tsx b/react-ui/src/pages/HyperParameter/Create/index.tsx index 79e45582..bd7aedb9 100644 --- a/react-ui/src/pages/HyperParameter/Create/index.tsx +++ b/react-ui/src/pages/HyperParameter/Create/index.tsx @@ -118,9 +118,9 @@ function CreateHyperParameter() { } return ( -
+
-
+
+
-
+
diff --git a/react-ui/src/pages/HyperParameter/Instance/index.less b/react-ui/src/pages/HyperParameter/Instance/index.less index 889faeb5..8d57a98d 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.less +++ b/react-ui/src/pages/HyperParameter/Instance/index.less @@ -1,4 +1,4 @@ -.auto-ml-instance { +.hyper-parameter-instance { height: 100%; &__tabs { diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index 6a1d3931..5cc1fded 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -1,5 +1,5 @@ import KFIcon from '@/components/KFIcon'; -import { AutoMLTaskType, ExperimentStatus } from '@/enums'; +import { ExperimentStatus } from '@/enums'; import LogList from '@/pages/Experiment/components/LogList'; import { getRayInsReq } from '@/services/hyperParameter'; import { NodeStatus } from '@/types'; @@ -12,7 +12,7 @@ import { useEffect, useRef, useState } from 'react'; import ExperimentHistory from '../components/ExperimentHistory'; import ExperimentResult from '../components/ExperimentResult'; import HyperParameterBasic from '../components/HyperParameterBasic'; -import { AutoMLInstanceData, HyperParameterData } from '../types'; +import { HyperParameterData, HyperParameterInstanceData } from '../types'; import styles from './index.less'; enum TabKeys { @@ -22,10 +22,16 @@ enum TabKeys { History = 'history', } +const NodePrefix = 'auto-hpo'; + function HyperParameterInstance() { const [activeTab, setActiveTab] = useState(TabKeys.Params); const [experimentInfo, setExperimentInfo] = useState(undefined); - const [instanceInfo, setInstanceInfo] = useState(undefined); + const [instanceInfo, setInstanceInfo] = useState( + undefined, + ); + // 超参数寻优运行有3个节点,运行状态取工作流状态,而不是 auto-hpo 节点状态 + const [workflowStatus, setWorkflowStatus] = useState(undefined); const params = useParams(); const instanceId = safeInvoke(Number)(params.id); const evtSourceRef = useRef(null); @@ -43,11 +49,26 @@ function HyperParameterInstance() { const getExperimentInsInfo = async (isStatusDetermined: boolean) => { const [res] = await to(getRayInsReq(instanceId)); if (res && res.data) { - const info = res.data as AutoMLInstanceData; + const info = res.data as HyperParameterInstanceData; const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; // 解析配置参数 const paramJson = parseJsonText(param); if (paramJson) { + // 实例详情返回的参数是字符串,需要转换 + if (typeof paramJson.parameters === 'string') { + paramJson.parameters = parseJsonText(paramJson.parameters); + } + if (!Array.isArray(paramJson.parameters)) { + paramJson.parameters = []; + } + + // 实例详情返回的运行参数是字符串,需要转换 + if (typeof paramJson.points_to_evaluate === 'string') { + paramJson.points_to_evaluate = parseJsonText(paramJson.points_to_evaluate); + } + if (!Array.isArray(paramJson.points_to_evaluate)) { + paramJson.points_to_evaluate = []; + } setExperimentInfo(paramJson); } @@ -65,13 +86,17 @@ function HyperParameterInstance() { const nodeStatusJson = parseJsonText(node_status); if (nodeStatusJson) { Object.keys(nodeStatusJson).forEach((key) => { - if (key.startsWith('auto-ml')) { - const value = nodeStatusJson[key]; - info.nodeStatus = value; + if (key.startsWith(NodePrefix)) { + const nodeStatus = nodeStatusJson[key]; + info.nodeStatus = nodeStatus; + } else if (key.startsWith('workflow')) { + const workflowStatus = nodeStatusJson[key]; + setWorkflowStatus(workflowStatus); } }); } setInstanceInfo(info); + // 运行中或者等待中,开启 SSE if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { setupSSE(argo_ins_name, argo_ins_ns); @@ -98,19 +123,29 @@ function HyperParameterInstance() { if (dataJson) { const nodes = dataJson?.result?.object?.status?.nodes; if (nodes) { - const statusData = Object.values(nodes).find((node: any) => - node.displayName.startsWith('auto-ml'), + const nodeStatus = Object.values(nodes).find((node: any) => + node.displayName.startsWith(NodePrefix), ) as NodeStatus; - if (statusData) { + const workflowStatus = Object.values(nodes).find((node: any) => + node.displayName.startsWith('workflow'), + ) as NodeStatus; + + // 节点状态 + if (nodeStatus) { setInstanceInfo((prev) => ({ ...prev!, - nodeStatus: statusData, + nodeStatus: nodeStatus, })); + } + + // 设置工作流状态 + if (workflowStatus) { + setWorkflowStatus(workflowStatus); // 实验结束,关闭 SSE if ( - statusData.phase !== ExperimentStatus.Pending && - statusData.phase !== ExperimentStatus.Running + workflowStatus.phase !== ExperimentStatus.Pending && + workflowStatus.phase !== ExperimentStatus.Running ) { closeSSE(); getExperimentInsInfo(true); @@ -140,9 +175,9 @@ function HyperParameterInstance() { icon: , children: ( ), @@ -152,7 +187,7 @@ function HyperParameterInstance() { label: '日志', icon: , children: ( -
+
{instanceInfo && instanceInfo.nodeStatus && ( , children: ( - + ), }, { key: TabKeys.History, label: 'Trial 列表', icon: , - children: ( - - ), + children: , }, ]; @@ -200,9 +226,9 @@ function HyperParameterInstance() { : basicTabItems; return ( -
+
([]); - 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); - } - }; +function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { + const first: HyperParameterTrialList | undefined = trialList[0]; + const config: Record = first?.config ?? {}; + const metricAnalysis: Record = first?.metric_analysis ?? {}; + const paramsNames = Object.keys(config); + const metricNames = Object.keys(metricAnalysis); - const columns: TableProps['columns'] = [ - { - title: 'ID', - dataIndex: 'id', - key: 'id', - width: 80, - render: tableCellRender(false), - }, - { - title: '准确率', - dataIndex: 'accuracy', - key: 'accuracy', - render: tableCellRender(true), - ellipsis: { showTitle: false }, - }, + const columns: TableProps['columns'] = [ { - title: '耗时', - dataIndex: 'duration', - key: 'duration', - render: tableCellRender(true), - ellipsis: { showTitle: false }, + title: '序号', + dataIndex: 'index', + key: 'index', + width: 100, + align: 'center', + render: tableCellRender(false, TableCellValueType.Index), }, { - 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: 'training_iteration', + key: 'training_iteration', + width: 120, + render: tableCellRender(false), }, { - title: '算法', - dataIndex: 'althorithm', - key: 'althorithm', - render: tableCellRender(true), + title: '平均时长(秒)', + dataIndex: 'time_avg', + key: 'time_avg', + width: 150, + render: tableCellRender(false, TableCellValueType.Custom, { + format: (value = 0) => Number(value).toFixed(2), + }), ellipsis: { showTitle: false }, }, { @@ -107,6 +50,52 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps }, ]; + if (paramsNames.length) { + columns.push({ + title: '运行参数', + dataIndex: 'config', + key: 'config', + align: 'center', + children: paramsNames.map((name) => ({ + title: ( + + {name} + + ), + dataIndex: ['config', name], + key: name, + width: 120, + align: 'center', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + showSorterTooltip: false, + })), + }); + } + + if (metricNames.length) { + columns.push({ + title: `指标分析(${first.metric ?? ''})`, + dataIndex: 'metrics', + key: 'metrics', + align: 'center', + children: metricNames.map((name) => ({ + title: ( + + {name} + + ), + dataIndex: ['metric_analysis', name], + key: name, + width: 120, + align: 'center', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + showSorterTooltip: false, + })), + }); + } + return (
@@ -117,11 +106,12 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps )} > diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx index e69de29b..9991a65c 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx @@ -0,0 +1,83 @@ +import KFIcon from '@/components/KFIcon'; +import { ExperimentStatus } from '@/enums'; +import LogList from '@/pages/Experiment/components/LogList'; +import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; +import { Tabs } from 'antd'; +import { useEffect } from 'react'; +import styles from './index.less'; + +type ExperimentLogProps = { + instanceInfo: HyperParameterInstanceData; +}; + +function ExperimentLog({ instanceInfo }: ExperimentLogProps) { + const tabItems = [ + { + key: 'git-clone-1', + label: '框架代码日志', + icon: , + children: ( +
+ {instanceInfo && instanceInfo.nodeStatus && ( + + )} +
+ ), + }, + { + key: 'git-clone-2', + label: '训练代码日志', + icon: , + children: ( +
+ {instanceInfo && instanceInfo.nodeStatus && ( + + )} +
+ ), + }, + { + key: 'auto-hpo', + label: '超参寻优日志', + icon: , + children: ( +
+ {instanceInfo && instanceInfo.nodeStatus && ( + + )} +
+ ), + }, + ]; + + useEffect(() => {}, []); + + return ( +
+ +
+ ); +} + +export default ExperimentLog; diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less index 342817c3..55ea8aed 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less @@ -6,47 +6,12 @@ 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; - } + &__table { + height: 400px; } &__text { + font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; white-space: pre-wrap; } - - &__images { - display: flex; - align-items: flex-start; - width: 100%; - overflow-x: auto; - - :global { - .ant-image { - margin-right: 20px; - - &:last-child { - margin-right: 0; - } - } - } - - &__item { - height: 248px; - border: 1px solid rgba(96, 107, 122, 0.3); - } - } } diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx index a826155d..7fa22912 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx @@ -1,25 +1,44 @@ import InfoGroup from '@/components/InfoGroup'; +import { HyperParameterFileList } from '@/pages/HyperParameter/types'; import { getFileReq } from '@/services/file'; import { to } from '@/utils/promise'; -import { Button, Image } from 'antd'; -import { useEffect, useMemo, useState } from 'react'; +import tableCellRender, { TableCellValueType } from '@/utils/table'; +import { Table, type TableProps } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useState } from 'react'; import styles from './index.less'; type ExperimentResultProps = { + fileList?: HyperParameterFileList[]; fileUrl?: string; - imageUrl?: string; - modelPath?: string; }; -function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) { +function ExperimentResult({ fileList, fileUrl }: ExperimentResultProps) { const [result, setResult] = useState(''); - const images = useMemo(() => { - if (imageUrl) { - return imageUrl.split(',').map((item) => item.trim()); - } - return []; - }, [imageUrl]); + const columns: TableProps['columns'] = [ + { + title: '序号', + dataIndex: 'index', + key: 'index', + width: 120, + align: 'center', + render: tableCellRender(false, TableCellValueType.Index), + }, + { + title: '文件名称', + dataIndex: 'name', + key: 'name', + render: tableCellRender(false), + }, + { + title: '文件大小', + dataIndex: 'size', + key: 'size', + width: 200, + render: tableCellRender(false), + }, + ]; useEffect(() => { if (fileUrl) { @@ -37,45 +56,25 @@ function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProp return (
+ +
+
+ +
{result}
- -
- - console.log(`current index: ${current}, prev index: ${prev}`), - }} - > - {images.map((item) => ( - - ))} - -
-
- {modelPath && ( -
- 文件名 - save_model.joblib - -
- )} ); } diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index 817d8418..47e5534e 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -90,7 +90,7 @@ function HyperParameterBasic({ return [ { label: '代码', - value: info.code, + value: info.code_config, format: formatCodeConfig, }, { diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index f8508a88..3a14861d 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -42,13 +42,11 @@ export type HyperParameterData = { } & FormData; // 自动机器学习实验实例 -export type AutoMLInstanceData = { +export type HyperParameterInstanceData = { id: number; - auto_ml_id: number; + ray_id: number; result_path: string; - model_path: string; - img_path: string; - run_history_path: string; + result_txt: string; state: number; status: string; node_status: string; @@ -60,5 +58,22 @@ export type AutoMLInstanceData = { create_time: string; update_time: string; finish_time: string; - nodeStatus?: NodeStatus; + nodeStatus?: NodeStatus; // json之后的节点状态 + trial_list?: HyperParameterTrialList[]; + file_list?: HyperParameterFileList[]; +}; + +export type HyperParameterTrialList = { + trial_id?: string; + training_iteration?: number; + time?: number; + status?: string; + config?: Record; + metric_analysis?: Record; + metric: string; +}; + +export type HyperParameterFileList = { + name?: string; + size?: string; };