| @@ -4,9 +4,9 @@ import classNames from 'classnames'; | |||||
| import './index.less'; | import './index.less'; | ||||
| type FormInfoProps = { | type FormInfoProps = { | ||||
| /** 自定义类名 */ | |||||
| /** 值 */ | |||||
| value?: any; | value?: any; | ||||
| /** 如果 `value` 是对象时,取对象的哪个属性作为值 */ | |||||
| /** 如果 `value` 是对象,取对象的哪个属性作为值 */ | |||||
| valuePropName?: string; | valuePropName?: string; | ||||
| /** 是否是多行文本 */ | /** 是否是多行文本 */ | ||||
| textArea?: boolean; | textArea?: boolean; | ||||
| @@ -32,11 +32,11 @@ function FormInfo({ | |||||
| style, | style, | ||||
| textArea = false, | textArea = false, | ||||
| }: FormInfoProps) { | }: FormInfoProps) { | ||||
| let data = value; | |||||
| let showValue = value; | |||||
| if (value && typeof value === 'object' && valuePropName) { | if (value && typeof value === 'object' && valuePropName) { | ||||
| data = value[valuePropName]; | |||||
| showValue = value[valuePropName]; | |||||
| } else if (select === true && options) { | } else if (select === true && options) { | ||||
| data = formatEnum(options)(value); | |||||
| showValue = formatEnum(options)(value); | |||||
| } | } | ||||
| return ( | return ( | ||||
| @@ -50,8 +50,8 @@ function FormInfo({ | |||||
| )} | )} | ||||
| style={style} | style={style} | ||||
| > | > | ||||
| <Typography.Paragraph ellipsis={textArea ? false : { tooltip: data }}> | |||||
| {data} | |||||
| <Typography.Paragraph ellipsis={textArea ? false : { tooltip: showValue }}> | |||||
| {showValue} | |||||
| </Typography.Paragraph> | </Typography.Paragraph> | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| @@ -22,6 +22,8 @@ enum TabKeys { | |||||
| History = 'history', | History = 'history', | ||||
| } | } | ||||
| const NodePrefix = 'auto-hpo'; | |||||
| function AutoMLInstance() { | function AutoMLInstance() { | ||||
| const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); | const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); | ||||
| const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); | const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); | ||||
| @@ -66,7 +68,7 @@ function AutoMLInstance() { | |||||
| const nodeStatusJson = parseJsonText(node_status); | const nodeStatusJson = parseJsonText(node_status); | ||||
| if (nodeStatusJson) { | if (nodeStatusJson) { | ||||
| Object.keys(nodeStatusJson).forEach((key) => { | Object.keys(nodeStatusJson).forEach((key) => { | ||||
| if (key.startsWith('auto-ml')) { | |||||
| if (key.startsWith(NodePrefix)) { | |||||
| const value = nodeStatusJson[key]; | const value = nodeStatusJson[key]; | ||||
| info.nodeStatus = value; | info.nodeStatus = value; | ||||
| } | } | ||||
| @@ -100,7 +102,7 @@ function AutoMLInstance() { | |||||
| const nodes = dataJson?.result?.object?.status?.nodes; | const nodes = dataJson?.result?.object?.status?.nodes; | ||||
| if (nodes) { | if (nodes) { | ||||
| const statusData = Object.values(nodes).find((node: any) => | const statusData = Object.values(nodes).find((node: any) => | ||||
| node.displayName.startsWith('auto-ml'), | |||||
| node.displayName.startsWith(NodePrefix), | |||||
| ) as NodeStatus; | ) as NodeStatus; | ||||
| if (statusData) { | if (statusData) { | ||||
| setInstanceInfo((prev) => ({ | setInstanceInfo((prev) => ({ | ||||
| @@ -25,6 +25,7 @@ | |||||
| } | } | ||||
| &__text { | &__text { | ||||
| font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; | |||||
| white-space: pre-wrap; | white-space: pre-wrap; | ||||
| } | } | ||||
| @@ -18,26 +18,6 @@ | |||||
| background-color: white; | background-color: white; | ||||
| border-radius: 10px; | 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 { | :global { | ||||
| .ant-table-container { | .ant-table-container { | ||||
| border: none !important; | border: none !important; | ||||
| @@ -20,7 +20,8 @@ | |||||
| padding: 15px; | padding: 15px; | ||||
| color: white; | color: white; | ||||
| font-size: 14px; | font-size: 14px; | ||||
| white-space: pre-line; | |||||
| font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; | |||||
| white-space: pre-wrap; | |||||
| text-align: left; | text-align: left; | ||||
| word-break: break-all; | word-break: break-all; | ||||
| background: #19253b; | background: #19253b; | ||||
| @@ -1,4 +1,4 @@ | |||||
| .create-hyperparameter { | |||||
| .create-hyper-parameter { | |||||
| height: 100%; | height: 100%; | ||||
| &__content { | &__content { | ||||
| @@ -11,11 +11,6 @@ | |||||
| background-color: white; | background-color: white; | ||||
| border-radius: 10px; | border-radius: 10px; | ||||
| &__type { | |||||
| color: @text-color; | |||||
| font-size: @font-size-input-lg; | |||||
| } | |||||
| :global { | :global { | ||||
| .ant-input-number { | .ant-input-number { | ||||
| width: 100%; | width: 100%; | ||||
| @@ -118,9 +118,9 @@ function CreateHyperParameter() { | |||||
| } | } | ||||
| return ( | return ( | ||||
| <div className={styles['create-hyperparameter']}> | |||||
| <div className={styles['create-hyper-parameter']}> | |||||
| <PageTitle title={title}></PageTitle> | <PageTitle title={title}></PageTitle> | ||||
| <div className={styles['create-hyperparameter__content']}> | |||||
| <div className={styles['create-hyper-parameter__content']}> | |||||
| <div> | <div> | ||||
| <Form | <Form | ||||
| name="create-hyperparameter" | name="create-hyperparameter" | ||||
| @@ -1,4 +1,4 @@ | |||||
| .auto-ml-info { | |||||
| .hyper-parameter-info { | |||||
| position: relative; | position: relative; | ||||
| height: 100%; | height: 100%; | ||||
| &__tabs { | &__tabs { | ||||
| @@ -35,9 +35,9 @@ function HyperparameterInfo() { | |||||
| }; | }; | ||||
| return ( | return ( | ||||
| <div className={styles['auto-ml-info']}> | |||||
| <div className={styles['hyper-parameter-info']}> | |||||
| <PageTitle title="实验详情"></PageTitle> | <PageTitle title="实验详情"></PageTitle> | ||||
| <div className={styles['auto-ml-info__content']}> | |||||
| <div className={styles['hyper-parameter-info__content']}> | |||||
| <HyperParameterBasic info={hyperparameterInfo} /> | <HyperParameterBasic info={hyperparameterInfo} /> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -1,4 +1,4 @@ | |||||
| .auto-ml-instance { | |||||
| .hyper-parameter-instance { | |||||
| height: 100%; | height: 100%; | ||||
| &__tabs { | &__tabs { | ||||
| @@ -1,5 +1,5 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { AutoMLTaskType, ExperimentStatus } from '@/enums'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import LogList from '@/pages/Experiment/components/LogList'; | import LogList from '@/pages/Experiment/components/LogList'; | ||||
| import { getRayInsReq } from '@/services/hyperParameter'; | import { getRayInsReq } from '@/services/hyperParameter'; | ||||
| import { NodeStatus } from '@/types'; | import { NodeStatus } from '@/types'; | ||||
| @@ -12,7 +12,7 @@ import { useEffect, useRef, useState } from 'react'; | |||||
| import ExperimentHistory from '../components/ExperimentHistory'; | import ExperimentHistory from '../components/ExperimentHistory'; | ||||
| import ExperimentResult from '../components/ExperimentResult'; | import ExperimentResult from '../components/ExperimentResult'; | ||||
| import HyperParameterBasic from '../components/HyperParameterBasic'; | import HyperParameterBasic from '../components/HyperParameterBasic'; | ||||
| import { AutoMLInstanceData, HyperParameterData } from '../types'; | |||||
| import { HyperParameterData, HyperParameterInstanceData } from '../types'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| enum TabKeys { | enum TabKeys { | ||||
| @@ -22,10 +22,16 @@ enum TabKeys { | |||||
| History = 'history', | History = 'history', | ||||
| } | } | ||||
| const NodePrefix = 'auto-hpo'; | |||||
| function HyperParameterInstance() { | function HyperParameterInstance() { | ||||
| const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); | const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); | ||||
| const [experimentInfo, setExperimentInfo] = useState<HyperParameterData | undefined>(undefined); | const [experimentInfo, setExperimentInfo] = useState<HyperParameterData | undefined>(undefined); | ||||
| const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); | |||||
| const [instanceInfo, setInstanceInfo] = useState<HyperParameterInstanceData | undefined>( | |||||
| undefined, | |||||
| ); | |||||
| // 超参数寻优运行有3个节点,运行状态取工作流状态,而不是 auto-hpo 节点状态 | |||||
| const [workflowStatus, setWorkflowStatus] = useState<NodeStatus | undefined>(undefined); | |||||
| const params = useParams(); | const params = useParams(); | ||||
| const instanceId = safeInvoke(Number)(params.id); | const instanceId = safeInvoke(Number)(params.id); | ||||
| const evtSourceRef = useRef<EventSource | null>(null); | const evtSourceRef = useRef<EventSource | null>(null); | ||||
| @@ -43,11 +49,26 @@ function HyperParameterInstance() { | |||||
| const getExperimentInsInfo = async (isStatusDetermined: boolean) => { | const getExperimentInsInfo = async (isStatusDetermined: boolean) => { | ||||
| const [res] = await to(getRayInsReq(instanceId)); | const [res] = await to(getRayInsReq(instanceId)); | ||||
| if (res && res.data) { | 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 { param, node_status, argo_ins_name, argo_ins_ns, status } = info; | ||||
| // 解析配置参数 | // 解析配置参数 | ||||
| const paramJson = parseJsonText(param); | const paramJson = parseJsonText(param); | ||||
| if (paramJson) { | 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); | setExperimentInfo(paramJson); | ||||
| } | } | ||||
| @@ -65,13 +86,17 @@ function HyperParameterInstance() { | |||||
| const nodeStatusJson = parseJsonText(node_status); | const nodeStatusJson = parseJsonText(node_status); | ||||
| if (nodeStatusJson) { | if (nodeStatusJson) { | ||||
| Object.keys(nodeStatusJson).forEach((key) => { | 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); | setInstanceInfo(info); | ||||
| // 运行中或者等待中,开启 SSE | // 运行中或者等待中,开启 SSE | ||||
| if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { | if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { | ||||
| setupSSE(argo_ins_name, argo_ins_ns); | setupSSE(argo_ins_name, argo_ins_ns); | ||||
| @@ -98,19 +123,29 @@ function HyperParameterInstance() { | |||||
| if (dataJson) { | if (dataJson) { | ||||
| const nodes = dataJson?.result?.object?.status?.nodes; | const nodes = dataJson?.result?.object?.status?.nodes; | ||||
| if (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; | ) as NodeStatus; | ||||
| if (statusData) { | |||||
| const workflowStatus = Object.values(nodes).find((node: any) => | |||||
| node.displayName.startsWith('workflow'), | |||||
| ) as NodeStatus; | |||||
| // 节点状态 | |||||
| if (nodeStatus) { | |||||
| setInstanceInfo((prev) => ({ | setInstanceInfo((prev) => ({ | ||||
| ...prev!, | ...prev!, | ||||
| nodeStatus: statusData, | |||||
| nodeStatus: nodeStatus, | |||||
| })); | })); | ||||
| } | |||||
| // 设置工作流状态 | |||||
| if (workflowStatus) { | |||||
| setWorkflowStatus(workflowStatus); | |||||
| // 实验结束,关闭 SSE | // 实验结束,关闭 SSE | ||||
| if ( | if ( | ||||
| statusData.phase !== ExperimentStatus.Pending && | |||||
| statusData.phase !== ExperimentStatus.Running | |||||
| workflowStatus.phase !== ExperimentStatus.Pending && | |||||
| workflowStatus.phase !== ExperimentStatus.Running | |||||
| ) { | ) { | ||||
| closeSSE(); | closeSSE(); | ||||
| getExperimentInsInfo(true); | getExperimentInsInfo(true); | ||||
| @@ -140,9 +175,9 @@ function HyperParameterInstance() { | |||||
| icon: <KFIcon type="icon-jibenxinxi" />, | icon: <KFIcon type="icon-jibenxinxi" />, | ||||
| children: ( | children: ( | ||||
| <HyperParameterBasic | <HyperParameterBasic | ||||
| className={styles['auto-ml-instance__basic']} | |||||
| className={styles['hyper-parameter-instance__basic']} | |||||
| info={experimentInfo} | info={experimentInfo} | ||||
| runStatus={instanceInfo?.nodeStatus} | |||||
| runStatus={workflowStatus} | |||||
| isInstance | isInstance | ||||
| /> | /> | ||||
| ), | ), | ||||
| @@ -152,7 +187,7 @@ function HyperParameterInstance() { | |||||
| label: '日志', | label: '日志', | ||||
| icon: <KFIcon type="icon-rizhi1" />, | icon: <KFIcon type="icon-rizhi1" />, | ||||
| children: ( | children: ( | ||||
| <div className={styles['auto-ml-instance__log']}> | |||||
| <div className={styles['hyper-parameter-instance__log']}> | |||||
| {instanceInfo && instanceInfo.nodeStatus && ( | {instanceInfo && instanceInfo.nodeStatus && ( | ||||
| <LogList | <LogList | ||||
| instanceName={instanceInfo.argo_ins_name} | instanceName={instanceInfo.argo_ins_name} | ||||
| @@ -174,23 +209,14 @@ function HyperParameterInstance() { | |||||
| label: '实验结果', | label: '实验结果', | ||||
| icon: <KFIcon type="icon-shiyanjieguo1" />, | icon: <KFIcon type="icon-shiyanjieguo1" />, | ||||
| children: ( | children: ( | ||||
| <ExperimentResult | |||||
| fileUrl={instanceInfo?.result_path} | |||||
| imageUrl={instanceInfo?.img_path} | |||||
| modelPath={instanceInfo?.model_path} | |||||
| /> | |||||
| <ExperimentResult fileUrl={instanceInfo?.result_txt} fileList={instanceInfo?.file_list} /> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| key: TabKeys.History, | key: TabKeys.History, | ||||
| label: 'Trial 列表', | label: 'Trial 列表', | ||||
| icon: <KFIcon type="icon-Trialliebiao" />, | icon: <KFIcon type="icon-Trialliebiao" />, | ||||
| children: ( | |||||
| <ExperimentHistory | |||||
| fileUrl={instanceInfo?.run_history_path} | |||||
| isClassification={experimentInfo?.task_type === AutoMLTaskType.Classification} | |||||
| /> | |||||
| ), | |||||
| children: <ExperimentHistory trialList={instanceInfo?.trial_list} />, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -200,9 +226,9 @@ function HyperParameterInstance() { | |||||
| : basicTabItems; | : basicTabItems; | ||||
| return ( | return ( | ||||
| <div className={styles['auto-ml-instance']}> | |||||
| <div className={styles['hyper-parameter-instance']}> | |||||
| <Tabs | <Tabs | ||||
| className={styles['auto-ml-instance__tabs']} | |||||
| className={styles['hyper-parameter-instance__tabs']} | |||||
| items={tabItems} | items={tabItems} | ||||
| activeKey={activeTab} | activeKey={activeTab} | ||||
| onChange={setActiveTab} | onChange={setActiveTab} | ||||
| @@ -10,5 +10,26 @@ | |||||
| &__table { | &__table { | ||||
| height: 100%; | height: 100%; | ||||
| } | } | ||||
| :global { | |||||
| .ant-table-container { | |||||
| border: none !important; | |||||
| } | |||||
| .ant-table-thead { | |||||
| .ant-table-cell { | |||||
| background-color: rgb(247, 247, 247); | |||||
| border-color: @border-color !important; | |||||
| } | |||||
| } | |||||
| .ant-table-tbody { | |||||
| .ant-table-cell { | |||||
| border-right: none !important; | |||||
| border-left: none !important; | |||||
| } | |||||
| } | |||||
| .ant-table-tbody-virtual::after { | |||||
| border-bottom: none !important; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,101 +1,44 @@ | |||||
| import { getFileReq } from '@/services/file'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import tableCellRender from '@/utils/table'; | |||||
| import { Table, type TableProps } from 'antd'; | |||||
| import { HyperParameterTrialList } from '@/pages/HyperParameter/types'; | |||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { Table, Tooltip, type TableProps } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useState } from 'react'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ExperimentHistoryProps = { | type ExperimentHistoryProps = { | ||||
| fileUrl?: string; | |||||
| isClassification: boolean; | |||||
| trialList?: HyperParameterTrialList[]; | |||||
| }; | }; | ||||
| 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); | |||||
| } | |||||
| }; | |||||
| function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||||
| const first: HyperParameterTrialList | undefined = trialList[0]; | |||||
| const config: Record<string, any> = first?.config ?? {}; | |||||
| const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; | |||||
| const paramsNames = Object.keys(config); | |||||
| const metricNames = Object.keys(metricAnalysis); | |||||
| 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 }, | |||||
| }, | |||||
| const columns: TableProps<HyperParameterTrialList>['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 }, | 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: ( | |||||
| <Tooltip title={name}> | |||||
| <span>{name}</span> | |||||
| </Tooltip> | |||||
| ), | |||||
| 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: ( | |||||
| <Tooltip title={name}> | |||||
| <span>{name}</span> | |||||
| </Tooltip> | |||||
| ), | |||||
| dataIndex: ['metric_analysis', name], | |||||
| key: name, | |||||
| width: 120, | |||||
| align: 'center', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| showSorterTooltip: false, | |||||
| })), | |||||
| }); | |||||
| } | |||||
| return ( | return ( | ||||
| <div className={styles['experiment-history']}> | <div className={styles['experiment-history']}> | ||||
| <div className={styles['experiment-history__content']}> | <div className={styles['experiment-history__content']}> | ||||
| @@ -117,11 +106,12 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||||
| )} | )} | ||||
| > | > | ||||
| <Table | <Table | ||||
| dataSource={tableData} | |||||
| dataSource={trialList} | |||||
| columns={columns} | columns={columns} | ||||
| pagination={false} | pagination={false} | ||||
| scroll={{ y: 'calc(100% - 55px)' }} | |||||
| rowKey="id" | |||||
| bordered={true} | |||||
| scroll={{ y: 'calc(100% - 110px)', x: '100%' }} | |||||
| rowKey="trial_id" | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -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: <KFIcon type="icon-jibenxinxi" />, | |||||
| 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> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| key: 'git-clone-2', | |||||
| 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> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| key: 'auto-hpo', | |||||
| 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> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| useEffect(() => {}, []); | |||||
| return ( | |||||
| <div className={styles['experiment-log']}> | |||||
| <Tabs className={styles['auto-ml-instance__tabs']} items={tabItems} /> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ExperimentLog; | |||||
| @@ -6,47 +6,12 @@ | |||||
| background-color: white; | background-color: white; | ||||
| border-radius: 10px; | 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 { | &__text { | ||||
| font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; | |||||
| white-space: pre-wrap; | 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); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -1,25 +1,44 @@ | |||||
| import InfoGroup from '@/components/InfoGroup'; | import InfoGroup from '@/components/InfoGroup'; | ||||
| import { HyperParameterFileList } from '@/pages/HyperParameter/types'; | |||||
| import { getFileReq } from '@/services/file'; | import { getFileReq } from '@/services/file'; | ||||
| import { to } from '@/utils/promise'; | 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'; | import styles from './index.less'; | ||||
| type ExperimentResultProps = { | type ExperimentResultProps = { | ||||
| fileList?: HyperParameterFileList[]; | |||||
| fileUrl?: string; | fileUrl?: string; | ||||
| imageUrl?: string; | |||||
| modelPath?: string; | |||||
| }; | }; | ||||
| function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) { | |||||
| function ExperimentResult({ fileList, fileUrl }: ExperimentResultProps) { | |||||
| const [result, setResult] = useState<string | undefined>(''); | const [result, setResult] = useState<string | undefined>(''); | ||||
| const images = useMemo(() => { | |||||
| if (imageUrl) { | |||||
| return imageUrl.split(',').map((item) => item.trim()); | |||||
| } | |||||
| return []; | |||||
| }, [imageUrl]); | |||||
| const columns: TableProps<HyperParameterFileList>['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(() => { | useEffect(() => { | ||||
| if (fileUrl) { | if (fileUrl) { | ||||
| @@ -37,45 +56,25 @@ function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProp | |||||
| return ( | return ( | ||||
| <div className={styles['experiment-result']}> | <div className={styles['experiment-result']}> | ||||
| <InfoGroup title="文件列表" style={{ margin: '16px 0' }}> | |||||
| <div | |||||
| className={classNames( | |||||
| 'vertical-scroll-table-no-page', | |||||
| styles['experiment-result__table'], | |||||
| )} | |||||
| > | |||||
| <Table | |||||
| dataSource={fileList} | |||||
| columns={columns} | |||||
| pagination={false} | |||||
| scroll={{ y: 'calc(100% - 55px)', x: '100%' }} | |||||
| rowKey="name" | |||||
| /> | |||||
| </div> | |||||
| </InfoGroup> | |||||
| <InfoGroup title="实验结果" height={420} width="100%"> | <InfoGroup title="实验结果" height={420} width="100%"> | ||||
| <div className={styles['experiment-result__text']}>{result}</div> | <div className={styles['experiment-result__text']}>{result}</div> | ||||
| </InfoGroup> | </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> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -90,7 +90,7 @@ function HyperParameterBasic({ | |||||
| return [ | return [ | ||||
| { | { | ||||
| label: '代码', | label: '代码', | ||||
| value: info.code, | |||||
| value: info.code_config, | |||||
| format: formatCodeConfig, | format: formatCodeConfig, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -42,13 +42,11 @@ export type HyperParameterData = { | |||||
| } & FormData; | } & FormData; | ||||
| // 自动机器学习实验实例 | // 自动机器学习实验实例 | ||||
| export type AutoMLInstanceData = { | |||||
| export type HyperParameterInstanceData = { | |||||
| id: number; | id: number; | ||||
| auto_ml_id: number; | |||||
| ray_id: number; | |||||
| result_path: string; | result_path: string; | ||||
| model_path: string; | |||||
| img_path: string; | |||||
| run_history_path: string; | |||||
| result_txt: string; | |||||
| state: number; | state: number; | ||||
| status: string; | status: string; | ||||
| node_status: string; | node_status: string; | ||||
| @@ -60,5 +58,22 @@ export type AutoMLInstanceData = { | |||||
| create_time: string; | create_time: string; | ||||
| update_time: string; | update_time: string; | ||||
| finish_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<string, any>; | |||||
| metric_analysis?: Record<string, any>; | |||||
| metric: string; | |||||
| }; | |||||
| export type HyperParameterFileList = { | |||||
| name?: string; | |||||
| size?: string; | |||||
| }; | }; | ||||