周三下午,增材制造实验室,CT 扫描室门口。
"这批航空支架又废了,"工艺工程师小周把 CT 扫描报告拍在桌上,"激光选区熔化(SLM)打印的 Ti6Al4V 零件,内部孔隙率 2.3%,标准要求 ≤0.5%。拉伸强度直接掉了 30%,根本过不了质检。"
我翻开创口贴盒大小的零件样件。
"打印参数调过吗?"我问。
"调了,"小周调出 Excel 表,"激光功率 200W、扫描速度 1200mm/s、层厚 30μm、 hatch 间距 0.1mm、激光能量密度 68J/mm³、基板预热 80℃、保护气流量 15L/min、扫描策略 条带式、粉末粒径分布 D50=35μm、相对湿度 35%。上次试了功率拉到 250W,孔隙率降到 0.8%,但表面球化严重。又试了扫描速度降到 800,孔隙率 0.6% 但热应力开裂。"
"这表里有什么?"我问。
"每次工艺试验记录的 12 个参数 + CT 测出来的孔隙率(%),"小周说,"但它就是一张参数记录表。你能看到高功率低速度孔隙率低,但 12 个参数互相耦合——能量密度是功率/速度/层厚的组合,扫描间距又跟能量密度交互。工艺窗口窄得像刀刃,凭经验一个一个试,试一次打印 8 小时 + CT 扫描 2 小时,一周最多试 3 组。"
"我就想干一件事,"小周说,"给我一个程序:把过去两年 86 组工艺试验数据灌进去,自动筛出'对孔隙率影响最大的 3-4 个关键参数',告诉我'你别瞎调扫描策略和湿度了,那俩对孔隙率贡献不到 5%,死磕激光功率和扫描速度就行'。我拿去跟领导说'按这个方向缩参,试验次数从 50 组砍到 15 组,两周出最优工艺'。"
"比如这是个高维参数筛选问题——12 个输入 1 个输出,参数间有强相关性,"我接话,"用 pandas 做数据清洗+相关性分析,numpy 做方差分析,scikit-learn 的 RandomForest + Permutation Importance 做全局特征重要性排序,scipy 做偏相关+显著性检验,matplotlib 画相关性热力图+特征重要性+参数 vs 孔隙率散点矩阵+三维参数曲面+孔隙率分布+决策网络,networkx 建'参数→孔隙率→关键因子'推理链路。"
"对,"小周点头,"别给我黑盒,要能说清楚'为什么扫描策略不重要'。我看得懂,能拿去跟评审专家解释'激光功率贡献 38%、扫描速度 24%、层厚 15%,这三个加起来 77%,其余参数贡献均 <5%,p 值 >0.05 不显著'。"
"用 pandas 做特征工程,sklearn RandomForest + PermutationImportance 双保险排序,scipy 偏相关+ANOVA,matplotlib 出 6 图+报告,存 results/,"我开工程,"数据自包含,合成一批 86 组 SLM 工艺试验数据,下载就能跑。"
敲了行原型:
# 目标: 从 12 个工艺参数中筛选对孔隙率贡献最大的关键因子
# 方法: RandomForest 特征重要性 + Permutation Importance + 偏相关检验
# 输出: 关键参数排序 + 显著性标记 + 缩参建议
"完整版 OOP 封好,"我说,"数据加载器、特征分析器、参数筛选器、可视化器、决策链路,输出关键因子排名+6图+报告。"
小周凑近看:"那以后看报告:激光功率贡献 41.2%(p<0.001),扫描速度 22.8%(p<0.01),层厚 13.5%(p<0.01), hatch 间距 8.1%(p<0.05),其余 7 个参数贡献合计 <15% 且不显著。建议:DOE 聚焦功率(180-280W)×速度(800-1500mm/s)×层厚(20-40μm) 三因子响应面,其余参数固定在中值。试验矩阵从 12 因子全析交缩到 3 因子 Box-Behnken,试验组数从 50+ 砍到 15 组。"
"对,"我接话,"增材制造不是'试出来的',是'算出来的'。数字孪生里挂工艺参数敏感度节点,这套就是工艺员的'缩参望远镜'——12 个旋钮,一眼看出哪三个值得拧。"
一、实际应用场景(真实痛点)
场景设定:金属 3D 打印(SLM/EBM)中,内部孔隙率直接影响零件致密度和力学性能。工艺参数多达 10-15 个(激光功率、扫描速度、层厚、 hatch 间距、能量密度、基板温度、保护气流量、扫描策略、粉末特性、环境湿度等),参数间存在强耦合,传统"单因子轮换试错"效率极低且难以识别关键因子。
现场原话(叙事化):
"不是我们不想系统优化,"小周说,"是参数太多了。激光功率、扫描速度、层厚、扫描间距、基板预热、保护气流量、扫描策略、粉末粒径……12 个旋钮,每个拧 3 档就是 3¹² = 53 万种组合,穷举到宇宙热寂。只能凭经验一个一个试,但试一次 8 小时打印加 2 小时 CT,一周最多 3 组。两年攒了 86 组数据,但没人从里面筛过'到底哪些参数真正影响孔隙率'。"
"最坑的是参数耦合,"小周补充,"能量密度 = 功率/(速度×层厚×间距),你调了功率等于同时动了能量密度。扫描速度快了热输入不够→未熔合孔隙,慢了过热→球化+气孔。 hatch 间距大了层间未熔合,小了热应力累积开裂。这些效应互相打架,凭经验根本分不清谁的贡献大。"
核心矛盾:"12 参数全析交试错 + 凭经验拍脑袋" 与 "数据驱动的参数筛选 + 全局特征重要性排序 + 统计显著性检验 + 缩参聚焦" 之间的断层。
二、痛点分析(映射到滨州职业学院《先进制造技术》课程模型)
《先进制造技术》模块 本篇痛点对应
增材制造(3D打印)技术:工艺参数优化 孔隙率与工艺窗口
先进制造技术基础:材料成型原理 熔池动力学与致密化
智能制造与数字孪生:工艺参数敏感度 高维参数降维筛选
柔性制造系统FMS与先进生产管理:试验设计(DOE) 缩参→聚焦关键因子
一句话总结:我们需要一个"SLM 孔隙试验数据→相关性分析+RandomForest+Permutation Importance+偏相关检验+ANOVA 参数筛选程序",用
"pandas" 做数据清洗/特征构造,
"numpy" 做方差分析,
"scikit-learn" RandomForest + PermutationImportance 全局排序,
"scipy" 偏相关+显著性检验,
"matplotlib" 画热力图+特征重要性+散点矩阵+三维曲面+分布+决策网络,
"networkx" 建推理链路,实现从"12 参数盲调"到"数据驱动的关键因子筛选+缩参建议+DOE 聚焦"。
三、核心逻辑讲解(大白话)
3.1 问题本质:把 3D 打印想成"烤蛋糕"
把工艺参数想成烤蛋糕时调的旋钮:
* 激光功率 = 烤箱温度(高→烤焦/低→夹生)
* 扫描速度 = 烘烤时间(长→干/短→湿)
* 层厚 = 蛋糕胚厚度(厚→里面不熟/薄→太慢)
* 孔隙率 = 蛋糕里的气泡(气泡多→一捏就塌)
* 你的目标 = 从 12 个旋钮里找出"真正影响气泡数量的 3 个",其余别管
3.2 业务逻辑 → 代码映射
加载 SLM 孔隙试验数据
│
▼ SLMDataLoader (pandas)
读取表:
激光功率(W), 扫描速度(mm/s), 层厚(μm),
hatch间距(mm), 能量密度(J/mm³),
基板预热(℃), 保护气流量(L/min),
扫描策略, 粉末D50(μm), 相对湿度(%),
孔隙率(%)
│
▼ FeatureAnalyzer (pandas + numpy)
特征工程:
能量密度 = 功率/(速度×层厚×间距)
比能量 = 功率/速度
热输入指数 = 功率×层厚/速度
扫描策略编码 (条带/棋盘/同心)
粉末粒径分组
│
▼ ParameterSelector (scikit-learn + scipy)
参数筛选(三重保险):
① RandomForest 特征重要性 → 全局排序
② Permutation Importance → 抗过拟合验证
③ 偏相关分析 + ANOVA → 显著性检验(p值)
综合得分 = 0.4×RF + 0.4×Perm + 0.2×偏相关
│
▼ SLMVisualizer (matplotlib + networkx)
可视化:
1. 参数-孔隙率相关性热力图
2. 特征重要性条形图(RF + Permutation)
3. 关键参数 vs 孔隙率散点(带回归线)
4. 三维参数曲面(功率×速度→孔隙率)
5. 孔隙率分布直方图
6. 参数筛选决策网络
│
▼ SyntheticSLMData (numpy)
合成数据:
86组工艺试验
孔隙率 = f(功率,速度,层厚,间距,...) + 噪声
关键因子: 功率(主效)>速度>层厚>间距
可复现
3.3 为什么不能"全参数 DOE"
视角 问题
12 因子全析交 2¹² = 4096 组,打印到明年
单因子轮换 忽略交互效应,找到的是假最优
数据驱动筛选 从历史数据找关键因子
三重排序 RF + Permutation + 统计检验
缩参聚焦 12→3 因子,DOE 组数砍 70%
3.4 分析前后对比
维度 传统方式 本程序
参数筛选 凭经验/文献 数据驱动三重排序
试验效率 盲调 50+ 组 聚焦 3 因子 15 组
置信度 "大概是这个" p 值 + 置信区间
决策依据 拍脑袋 统计显著性量化
四、OOP 代码实现
4.1 项目结构
slm_porosity_selector/
├── slm_porosity_selector/
│ ├── __init__.py
│ ├── slm_data_loader.py # 数据加载
│ ├── feature_analyzer.py # 特征工程+相关性
│ ├── parameter_selector.py # 参数筛选(三重保险)
│ ├── slm_visualizer.py # 可视化
│ └── synthetic_slm_data.py # 合成数据
├── tests/
│ ├── __init__.py
│ └── test_slm.py
├── results/
│ ├── correlation_heatmap.png
│ ├── feature_importance.png
│ ├── key_param_scatter.png
│ ├── parameter_surface.png
│ ├── porosity_distribution.png
│ ├── selection_network.png
│ ├── selection_detail.csv
│ └── porosity_report.txt
└── run_slm.py
4.2 核心源码
<details>
<summary></summary>
"""SLM 孔隙试验数据加载器"""
import pandas as pd
from pathlib import Path
from typing import Optional
class SLMDataLoader:
"""读取 SLM 工艺试验数据"""
def __init__(self, filepath: str = "slm_porosity_data.csv",
encoding: str = "utf-8"):
self.filepath = Path(filepath)
self.encoding = encoding
def load(self) -> pd.DataFrame:
if not self.filepath.exists():
raise FileNotFoundError(self.filepath)
df = pd.read_csv(self.filepath, encoding=self.encoding)
req = ["laser_power_w", "scan_speed_mms", "layer_thickness_um",
"hatch_spacing_mm", "porosity_pct"]
miss = [c for c in req if c not in df.columns]
if miss:
raise ValueError(f"缺列: {miss}")
num_cols = ["laser_power_w", "scan_speed_mms", "layer_thickness_um",
"hatch_spacing_mm", "porosity_pct"]
for c in num_cols:
df[c] = pd.to_numeric(df[c], errors="coerce")
df = df.dropna(subset=["porosity_pct"]).reset_index(drop=True)
return df
def summary(self, df: pd.DataFrame) -> str:
s = f"试验组数: {len(df)}\n"
s += f"孔隙率范围: {df['porosity_pct'].min():.3f}% - {df['porosity_pct'].max():.3f}%\n"
s += f"功率范围: {df['laser_power_w'].min():.0f} - {df['laser_power_w'].max():.0f} W\n"
s += f"速度范围: {df['scan_speed_mms'].min():.0f} - {df['scan_speed_mms'].max():.0f} mm/s"
return s
</details>
<details>
<summary></summary>
"""特征工程 + 相关性分析 (pandas + numpy)"""
import numpy as np
import pandas as pd
from typing import List, Dict
from scipy import stats
class FeatureAnalyzer:
"""构造 SLM 物理特征 + 相关性矩阵"""
def __init__(self):
pass
def engineer(self, df: pd.DataFrame) -> pd.DataFrame:
result = df.copy()
# 能量密度 (J/mm³)
result["energy_density"] = (
result["laser_power_w"] /
(result["scan_speed_mms"] * result["layer_thickness_um"] / 1000 *
result["hatch_spacing_mm"])
)
# 比能量 (J/mm)
result["specific_energy"] = (result["laser_power_w"] /
result["scan_speed_mms"])
# 热输入指数
result["thermal_input"] = (result["laser_power_w"] *
result["layer_thickness_um"] /
result["scan_speed_mms"])
# 扫描策略编码
if "scan_strategy" in result.columns:
strat_map = {"strip": 0, "checker": 1, "spiral": 2, "island": 3}
result["strategy_code"] = result["scan_strategy"].map(
lambda x: strat_map.get(str(x).lower(), 0))
else:
result["strategy_code"] = 0
# 粉末粒径分组
if "powder_d50_um" in result.columns:
result["powder_fine"] = (result["powder_d50_um"] < 30).astype(int)
else:
result["powder_fine"] = 0
# 湿度等级
if "humidity_pct" in result.columns:
result["humidity_high"] = (result["humidity_pct"] > 45).astype(int)
else:
result["humidity_high"] = 0
return result
def get_feature_columns(self) -> List[str]:
return [
"laser_power_w", "scan_speed_mms", "layer_thickness_um",
"hatch_spacing_mm", "energy_density", "specific_energy",
"thermal_input", "strategy_code", "powder_fine",
"humidity_high",
]
def correlation_matrix(self, df: pd.DataFrame,
features: List[str]) -> pd.DataFrame:
"""Pearson 相关矩阵"""
cols = features + ["porosity_pct"]
return df[cols].corr(method="pearson")
def partial_correlation(self, df: pd.DataFrame,
features: List[str]) -> Dict:
"""偏相关分析(控制其余变量后, 单变量与孔隙率的关系)"""
results = {}
y = df["porosity_pct"].values
for feat in features:
x = df[feat].values
# 简单偏相关: 残差法
other_feats = [f for f in features if f != feat]
if len(other_feats) == 0:
r, p = stats.pearsonr(x, y)
else:
X_other = df[other_feats].values
# 回归 x ~ X_other
beta_x = np.linalg.lstsq(X_other, x, rcond=None)[0]
resid_x = x - X_other @ beta_x
# 回归 y ~ X_other
beta_y = np.linalg.lstsq(X_other, y, rcond=None)[0]
resid_y = y - X_other @ beta_y
r, p = stats.pearsonr(resid_x, resid_y)
results[feat] = {"partial_r": float(r), "p_value": float(p)}
return results
</details>
<details>
<summary></summary>
"""参数筛选 (scikit-learn + scipy)"""
import numpy as np
from typing import Dict, List
from sklearn.ensemble import RandomForestRegressor
from sklearn.inspection import permutation_importance
from sklearn.model_selection import cross_val_score
from scipy import stats
class ParameterSelector:
"""三重保险参数筛选"""
def __init__(self, random_state: int = 42):
self.random_state = random_state
self.rf_model_ = None
def fit_rf(self, X: np.ndarray, y: np.ndarray) -> RandomForestRegressor:
"""RandomForest 训练"""
self.rf_model_ = RandomForestRegressor(
n_estimators=200,
max_depth=8,
min_samples_leaf=3,
random_state=self.random_state,
)
self.rf_model_.fit(X, y)
return self.rf_model_
def rf_importance(self, feature_names: List[str]) -> Dict:
"""RF 特征重要性"""
if self.rf_model_ is None:
raise ValueError("模型未训练")
imp = dict(zip(feature_names,
self.rf_model_.feature_importances_))
return dict(sorted(imp.items(), key=lambda x: x[1], reverse=True))
def permutation_importance_score(self, X: np.ndarray,
y: np.ndarray,
feature_names: List[str]) -> Dict:
"""Permutation Importance"""
if self.rf_model_ is None:
raise ValueError("模型未训练")
perm = permutation_importance(
self.rf_model_, X, y,
n_repeats=20,
random_state=self.random_state,
scoring="r2"
)
result = {}
for i, name in enumerate(feature_names):
result[name] = {
"importance_mean": float(perm.importances_mean[i]),
"importance_std": float(perm.importances_std[i]),
}
return dict(sorted(result.items(),
key=lambda x: x[1]["importance_mean"],
reverse=True))
def anova_f_test(self, X: np.ndarray, y: np.ndarray,
feature_names: List[str]) -> Dict:
"""ANOVA F-test (单变量显著性)"""
results = {}
for i, name in enumerate(feature_names):
f_stat, p_val = stats.f_oneway(
X[:, i], y # 简化: 实际应分组
)
# 用 pearsonr 替代(连续变量)
r, p = stats.pearsonr(X[:, i], y)
results[name] = {
"f_statistic": float(f_stat),
"p_value": float(p),
"pearson_r": float(r),
}
return results
def composite_score(self, rf_imp: Dict, perm_imp: Dict,
partial_corr: Dict,
feature_names: List[str]) -> Dict:
"""综合得分 = 0.4×RF + 0.4×Perm + 0.2×|偏相关|"""
composite = {}
for feat in feature_names:
rf_val = rf_imp.get(feat, 0)
perm_val = perm_imp.get(feat, {}).get("importance_mean", 0)
# 归一化 perm_val
max_perm = max([v.get("importance_mean", 0)
for v in perm_imp.values()] + [1e-6])
perm_norm = perm_val / max_perm
# 偏相关
partial_r = abs(partial_corr.get(feat, {}).get("partial_r", 0))
p_val = partial_corr.get(feat, {}).get("p_value", 1.0)
score = 0.4 * rf_val + 0.4 * perm_norm + 0.2 * partial_r
composite[feat] = {
"score": float(score),
"rf_importance": float(rf_val),
"perm_importance": float(perm_norm),
"partial_r": float(partial_r),
"p_value": float(p_val),
"significant": p_val < 0.05,
}
return dict(sorted(composite.items(),
key=lambda x: x[1]["score"], reverse=True))
def recommend_doe_factors(self, composite: Dict,
top_n: int = 3) -> List[str]:
"""推荐 DOE 聚焦的关键因子"""
sorted_feats = sorted(composite.items(),
key=lambda x: x[1]["score"], reverse=True)
return [feat for feat, _ in sorted_feats[:top_n]]
</details>
<details>
<summary></summary>
"""可视化 (matplotlib + networkx)"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
import networkx as nx
from typing import Dict, List
plt.rcParams["font.sans-serif"] = ["SimHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
class SLMVisualizer:
def __init__(self, results_dir: str = "results"):
self.results_dir = Path(results_dir)
self.results_dir.mkdir(exist_ok=True)
def correlation_heatmap(self, corr_matrix: pd.DataFrame):
"""相关性热力图"""
fig, ax = plt.subplots(figsize=(12, 10))
im = ax.imshow(corr_matrix.values, cmap="RdBu_r",
vmin=-1, vmax=1, aspect="auto")
ax.set_xticks(range(len(corr_matrix.columns)))
ax.set_yticks(range(len(corr_matrix.columns)))
ax.set_xticklabels(corr_matrix.columns, rotation=45, ha="right",
fontsize=9)
ax.set_yticklabels(corr_matrix.columns, fontsize=9)
# 标注数值
for i in range(len(corr_matrix.columns)):
for j in range(len(corr_matrix.columns)):
val = corr_matrix.values[i, j]
color = "white" if abs(val) > 0.6 else "black"
ax.text(j, i, f"{val:.2f}", ha="center", va="center",
color=color, fontsize=8)
plt.colorbar(im, ax=ax, label="Pearson r")
ax.set_title("参数-孔隙率相关性热力图", fontsize=13, fontweight="bold")
plt.tight_layout()
plt.savefig(self.results_dir/"correlation_heatmap.png",
dpi=150, bbox_inches="tight")
plt.close()
def feature_importance(self, rf_imp: Dict, perm_imp: Dict):
"""特征重要性(RF + Permutation)"""
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# RF
names = list(rf_imp.keys())[:8]
vals = [rf_imp[n] for n in names]
axes[0].barh(range(len(names)), vals[::-1],
color="#3498DB", edgecolor="black", height=0.6)
axes[0].set_yticks(range(len(names)))
axes[0].set_yticklabels(names[::-1], fontsize=9)
axes[0].set_xlabel("RF 特征重要性")
axes[0].set_title("RandomForest 重要性", fontsize=11, fontweight="bold")
axes[0].grid(axis="x", alpha=0.3)
# Permutation
perm_names = list(perm_imp.keys())[:8]
perm_vals = [perm_imp[n]["importance_mean"] for n in perm_names]
perm_errs = [perm_imp[n]["importance_std"] for n in perm_names]
axes[1].barh(range(len(perm_names)), perm_vals[::-1],
xerr=perm_errs[::-1],
color="#E74C3C", edgecolor="black", height=0.6)
axes[1].set_yticks(range(len(perm_names)))
axes[1].set_yticklabels(perm_names[::-1], fontsize=9)
axes[1].set_xlabel("Permutation Importance (±std)")
axes[1].set_title("Permutation 重要性", fontsize=11, fontweight="bold")
axes[1].grid(axis="x", alpha=0.3)
plt.suptitle("参数重要性排序 (双重验证)", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.savefig(self.results_dir/"feature_importance.png",
dpi=150, bbox_inches="tight")
plt.close()
def key_param_scatter(self, df: pd.DataFrame,
key_params: List[str]):
"""关键参数 vs 孔隙率散点"""
n = len(key_params)
fig, axes = plt.subplots(1, min(n, 4), figsize=(5*min(n,4), 5))
if n == 1:
axes = [axes]
for i, param in enumerate(key_params[:4]):
if param not in df.columns:
continue
ax = axes[i]
ax.scatter(df[param], df["porosity_pct"],
c="#3498DB", alpha=0.6, s=40, edgecolors="black")
# 回归线
z = np.polyfit(df[param], df["porosity_pct"], 1)
p = np.poly1d(z)
x_line = np.linspace(df[param].min(), df[param].max(), 100)
ax.plot(x_line, p(x_line), "r--", linewidth=2)
ax.set_xlabel(param, fontsize=10)
ax.set_ylabel("孔隙率 (%)", fontsize=10)
ax.set_title(f"{param} vs 孔隙率\nR²≈{np.corrcoef(df[param], df['porosity_pct'])[0,1]**2:.3f}",
fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(self.results_dir/"key_param_scatter.png",
dpi=150, bbox_inches="tight")
plt.close()
def parameter_surface(self, df: pd.DataFrame):
"""三维参数曲面(功率×速度→孔隙率)"""
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection="3d")
scatter = ax.scatter(df["laser_power_w"], df["scan_speed_mms"],
df["porosity_pct"],
c=df["porosity_pct"], cmap="viridis",
s=50, edgecolors="black")
ax.set_xlabel("激光功率 (W)")
ax.set_ylabel("扫描速度 (mm/s)")
ax.set_zlabel("孔隙率 (%)")
ax.set_title("功率×速度→孔隙率 三维分布", fontsize=12, fontweight="bold")
plt.colorbar(scatter, ax=ax, label="孔隙率 (%)")
plt.tight_layout()
plt.savefig(self.results_dir/"parameter_surface.png",
dpi=150, bbox_inches="tight")
plt.close()
def porosity_distribution(self, df: pd.DataFrame):
"""孔隙率分布"""
fig, ax = plt.subplots(figsize=(10, 6))
ax.hist(df["porosity_pct"], bins=20, color="#3498DB",
edgecolor="black", alpha=0.7, density=True)
mean_p = df["porosity_pct"].mean()
ax.axvline(mean_p, color="red", linestyle="--", linewidth=2,
label=f"均值 {mean_p:.3f}%")
ax.axvline(0.5, color="orange", linestyle="--", linewidth=2,
label="标准要求 ≤0.5%")
ax.set_xlabel("孔隙率 (%)", fontsize=12)
ax.set_ylabel("密度", fontsize=12)
ax.set_title("孔隙率分布", fontsize=13, fontweight="bold")
ax.legend(fontsize=11)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(self.results_dir/"porosity_distribution.png",
dpi=150, bbox_inches="tight")
plt.close()
def selection_network(self):
"""参数筛选决策网络"""
fig, ax = plt.subplots(figsize=(12, 8))
G = nx.DiGraph()
nodes = [
("试验数据", "input"),
("特征工程", "process"),
("RF重要性", "model"),
("Permutation", "model"),
("偏相关检验", "model"),
("综合排序", "optimize"),
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!