简介:这份资源面向材料科学、机械工程与增材制造领域的研究人员及工程师,聚焦NiTi形状记忆合金激光金属沉积工艺参数的多目标优化难题。内容以BP神经网络结合NSGA-II遗传算法为主线,通过30组小样本单道实验建立显微硬度、粗糙度与沉积速率的预测模型,并对比响应面法与随机森林的预测效果,最终给出激光功率1292.14W、扫描速度8.79mm/s、送粉速率16.78g/min的最优参数组合,使显微硬度提升5.23%、粗糙度降低20.04%且保持高沉积速率。资源包为1个PDF文件,约686KB,内含完整Python复现代码,覆盖数据准备、模型训练、多目标优化与结果可视化全流程,并讨论多相耦合关系、工艺参数敏感度及工业应用前景。已有64人学习,适合希望快速上手LMD工艺优化实验设计与数据分析的读者参考。
1. 增材制造里的 NiTi 合金:为什么工艺参数不能靠试错硬扛
NiTi 形状记忆合金在激光金属沉积里是个出了名的“脾气大”的材料。它同时要满足三个互相拉扯的指标:显微硬度得够高、表面粗糙度得够低、沉积速率还不能掉下来。做过增材制造的人都知道,这三者本质上是一组矛盾——激光功率拉高,沉积速率上去了,但热输入过大又会让晶粒粗化、硬度下降,表面球化也更严重;扫描速度加快,粗糙度可能改善,但熔池不稳定,致密度和硬度又保不住。传统做法是靠正交实验或者单因素轮换来逼近一个“还行”的参数组合,问题是实验成本极高,NiTi 粉末一公斤动辄上千,一台 LMD 设备开机就是钱,试错几十组下来周期和预算都吃不消。
神经网络加遗传算法的组合,恰好是冲着这个痛点来的。用前馈神经网络(BP 网络)去拟合“激光功率、扫描速度、送粉率、搭接率”这类工艺参数到“显微硬度、粗糙度、沉积速率”的映射关系,再用 NSGA-II 这类多目标遗传算法在拟合出来的代理模型上做寻优,最后拿到一组 Pareto 前沿解。这套思路在论文里很常见,但真正落地复现时,坑集中在数据质量、网络结构、目标权重和 Pareto 解的选择上。这篇笔记就按“数据怎么整、网络怎么搭、遗传算法怎么调、结果怎么验”的顺序,把这条链路拆开讲清楚,适合做金属增材、材料工艺优化、或者想把代理模型加进化算法用到实际工程里的从业者。
2. 从工艺参数到性能指标:数据表怎么建、网络怎么选
2.1 输入输出变量的确定与数据预处理
做任何代理模型,第一步不是写网络,而是把数据表定死。LMD 工艺里常见的可控输入有:激光功率 P(W)、扫描速度 v(mm/s)、送粉率 f(g/min)、搭接率 η(%)、离焦量 d(mm)。输出指标按标题锁定三个:显微硬度 HV(HV0.2 或 HV0.5)、表面粗糙度 Ra(μm)、沉积速率 DR(mm³/s 或 g/min)。数据来源一般是三块:已发表论文里的表格数据、自己实验室的正交实验、以及设备日志里能扒出来的稳定工况记录。
数据预处理有两个必须做的动作。第一是归一化,因为功率是几百瓦、速度是几百毫米每秒、送粉率是个位数,量纲差两三个数量级,不归一化 BP 网络的梯度会被大数值特征主导。第二是异常值剔除,LMD 实验里经常出现某组参数下熔池塌陷或者粉末堵塞,导致硬度或粗糙度明显偏离趋势,这种点留着会让网络学偏。
import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler # 读取原始实验数据,列名按实际表头调整 df = pd.read_csv("lmd_niti_data.csv") # 输入特征与输出目标 feature_cols = ["power_W", "scan_speed_mm_s", "powder_feed_g_min", "overlap_pct", "defocus_mm"] target_cols = ["microhardness_HV", "roughness_Ra_um", "deposition_rate_mm3_s"] # 用 IQR 方法剔除异常值,阈值 1.5 倍四分位距 mask = pd.Series(True, index=df.index) for col in feature_cols + target_cols: q1, q3 = df[col].quantile(0.25), df[col].quantile(0.75) iqr = q3 - q1 lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr mask &= df[col].between(lower, upper) df_clean = df[mask].reset_index(drop=True) print(f"原始样本 {len(df)} 条,清洗后 {len(df_clean)} 条") # 归一化,输入输出都缩到 [0,1] scaler_X = MinMaxScaler() scaler_y = MinMaxScaler() X = scaler_X.fit_transform(df_clean[feature_cols]) y = scaler_y.fit_transform(df_clean[target_cols]) np.save("X_scaled.npy", X) np.save("y_scaled.npy", y)这段代码的逻辑是:先按四分位距把每个维度的离群点去掉,再做 MinMax 归一化。参数上,IQR 的 1.5 倍是通用经验值,如果数据本身很少(比如少于 30 组),可以放宽到 2.0 倍,避免把有效样本误删。归一化器要保存下来,因为后续遗传算法找到的 Pareto 解是在归一化空间里的,必须用同一个 scaler 反变换回物理量纲才能拿去指导实验。
2.2 BP 网络结构:几层、多少神经元、怎么防过拟合
标题里说的是“神经网络”,在工艺参数拟合这个场景下,最稳的选型是前馈 BP 网络,而不是 LSTM 或 Transformer。原因很直接:工艺参数到性能指标是一个静态映射,没有时序依赖,样本量通常也就几十到几百条,上复杂结构只会过拟合。常见做法是“输入层 5 个节点 → 1 到 2 个隐藏层 → 输出层 3 个节点”,隐藏层神经元数量按经验公式sqrt(n_in + n_out) + a(a 取 1 到 10)来试,或者直接网格搜。
激活函数隐藏层用 ReLU 或 tanh,输出层用 linear,因为输出是连续物理量。损失函数用 MSE,优化器用 Adam,学习率 1e-3 起步。防过拟合的手段有三个:早停(验证集 loss 连续若干轮不降就停)、L2 正则、以及 Dropout(小样本下 0.1 到 0.2 即可,再大就欠拟合)。
import torch import torch.nn as nn from torch.utils.data import TensorDataset, DataLoader class LMDNet(nn.Module): def __init__(self, n_in=5, n_out=3): super().__init__() self.net = nn.Sequential( nn.Linear(n_in, 32), nn.ReLU(), nn.Dropout(0.1), nn.Linear(32, 32), nn.ReLU(), nn.Linear(32, n_out) # 输出层不加激活,回归任务 ) def forward(self, x): return self.net(x) X_tensor = torch.tensor(np.load("X_scaled.npy"), dtype=torch.float32) y_tensor = torch.tensor(np.load("y_scaled.npy"), dtype=torch.float32) dataset = TensorDataset(X_tensor, y_tensor) loader = DataLoader(dataset, batch_size=16, shuffle=True) model = LMDNet() criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) best_loss = float("inf") patience, wait = 50, 0 for epoch in range(2000): model.train() for xb, yb in loader: optimizer.zero_grad() loss = criterion(model(xb), yb) loss.backward() optimizer.step() # 简单用训练 loss 做早停演示,实际应划验证集 if loss.item() < best_loss: best_loss, wait = loss.item(), 0 torch.save(model.state_dict(), "lmd_bp_best.pth") else: wait += 1 if wait >= patience: print(f"早停于 epoch {epoch}") break网络结构上,32-32 是两个隐藏层各 32 个神经元,这个规模对几十到几百条样本足够。Dropout 只加在第一层之后,是因为小样本下每层都加会削弱拟合能力。早停的 patience 设 50,是因为 Adam 在后期 loss 波动很正常,patience 太小会提前掐断。权重衰减 1e-5 是轻量正则,如果发现训练 loss 和验证 loss 差距大,可以加到 1e-4。
2.3 用 R² 和 MAE 判断网络是否可信
网络训完不能只看 loss 曲线,得回到物理量纲上看误差。做法是把预测值反归一化,然后对三个输出分别算 R² 和 MAE。显微硬度的 MAE 最好控制在 15 HV 以内,粗糙度控制在 0.5 μm 以内,沉积速率控制在 0.05 mm³/s 以内,超过这个范围说明网络还没学到趋势,拿去给遗传算法寻优就是“垃圾进垃圾出”。
from sklearn.metrics import r2_score, mean_absolute_error model.load_state_dict(torch.load("lmd_bp_best.pth")) model.eval() with torch.no_grad(): y_pred_scaled = model(X_tensor).numpy() y_true = scaler_y.inverse_transform(y_tensor.numpy()) y_pred = scaler_y.inverse_transform(y_pred_scaled) for i, name in enumerate(target_cols): r2 = r2_score(y_true[:, i], y_pred[:, i]) mae = mean_absolute_error(y_true[:, i], y_pred[:, i]) print(f"{name}: R2={r2:.3f}, MAE={mae:.3f}")R² 低于 0.85 就别急着进遗传算法,先回头查数据:是不是某组参数下实验记录有误,或者输入特征漏了关键变量(比如保护气流量、层间等待时间)。这一步是很多复现翻车的地方——网络在归一化空间里 loss 看着很小,一反变换发现硬度误差几十 HV,根源往往是数据里混进了不同批次的粉末或者设备状态漂移。
3. NSGA-II 多目标寻优:目标函数、约束和参数怎么设
3.1 把三个指标写成可优化的目标函数
遗传算法要的是数值化的目标函数。显微硬度和沉积速率是“越大越好”,粗糙度是“越小越好”。NSGA-II 默认是最小化,所以要把硬度和沉积速率取负。同时,工艺参数有物理边界:功率不能超过设备上限,扫描速度不能低到熔池无法连续,送粉率不能高到堵嘴。这些边界在遗传算法里就是变量的上下界。
import numpy as np import torch # 加载训练好的网络和归一化器 model.load_state_dict(torch.load("lmd_bp_best.pth")) model.eval() def predict_properties(x_phys): """输入物理量纲的工艺参数,返回物理量纲的三个性能指标""" x_scaled = scaler_X.transform(np.array(x_phys).reshape(1, -1)) with torch.no_grad(): y_scaled = model(torch.tensor(x_scaled, dtype=torch.float32)).numpy() return scaler_y.inverse_transform(y_scaled)[0] def objectives(x): hv, ra, dr = predict_properties(x) # NSGA-II 最小化,硬度和沉积速率取负 return [-hv, ra, -dr] # 变量边界:[功率, 扫描速度, 送粉率, 搭接率, 离焦量] bounds = np.array([ [300, 800], # 功率 W [4, 12], # 扫描速度 mm/s [2, 8], # 送粉率 g/min [30, 60], # 搭接率 % [-3, 3], # 离焦量 mm ])目标函数里直接调网络预测,意味着遗传算法每评估一个个体就要过一次网络。NSGA-II 种群规模 100、迭代 200 代的话,评估次数在两万次左右,纯 Python 循环会慢,但网络很小,实际跑起来几分钟到十几分钟能出结果。如果嫌慢,可以把网络导出成 ONNX 或者用 numpy 手写前向传播,能再快一个量级。
3.2 NSGA-II 的关键参数:种群、交叉、变异
NSGA-II 在工艺优化里常用的配置是:种群规模 100 到 200,迭代代数 200 到 500,交叉概率 0.9,变异概率 1/变量数(这里是 0.2 左右),交叉算子用模拟二进制交叉(SBX),变异用多项式变异。这些参数不是拍脑袋,种群太小 Pareto 前沿稀疏,太大评估成本高;变异概率过高会退化成随机搜索,过低则种群多样性不足,容易卡在局部前沿。
from pymoo.algorithms.moo.nsga2 import NSGA2 from pymoo.core.problem import Problem from pymoo.operators.crossover.sbx import SBX from pymoo.operators.mutation.pm import PM from pymoo.operators.sampling.rnd import FloatRandomSampling class LMDProblem(Problem): def __init__(self): super().__init__(n_var=5, n_obj=3, n_constr=0, xl=bounds[:, 0], xu=bounds[:, 1]) def _evaluate(self, X, out, *args, **kwargs): F = np.array([objectives(x) for x in X]) out["F"] = F algorithm = NSGA2( pop_size=120, sampling=FloatRandomSampling(), crossover=SBX(prob=0.9, eta=15), mutation=PM(eta=20), eliminate_duplicates=True ) from pymoo.optimize import minimize res = minimize(LMDProblem(), algorithm, ("n_gen", 300), seed=1, verbose=True) np.save("pareto_F.npy", res.F) np.save("pareto_X.npy", res.X)这里 SBX 的 eta 设 15、多项式变异 eta 设 20,是 pymoo 里的常用值,eta 越大子代越靠近父代,探索能力越弱。如果发现 Pareto 前沿分布很窄,可以把交叉 eta 降到 10 左右,增加探索。eliminate_duplicates=True很重要,因为网络预测是确定性的,相同输入必然相同输出,不去重会浪费评估次数。
3.3 Pareto 解怎么选:别只看一个点
NSGA-II 跑完给的是一组 Pareto 前沿解,不是单一最优。选哪个点取决于工程偏好。常见做法有三种:一是看哪个解在三个目标上都不差(折中解,用 TOPSIS 或熵权法排序);二是固定沉积速率下限,在满足速率的前提下选硬度最高、粗糙度最低的;三是画三维 Pareto 前沿图,人工挑。
import numpy as np F = np.load("pareto_F.npy") # 列:[-HV, Ra, -DR] X = np.load("pareto_X.npy") # 还原成物理量纲 HV = -F[:, 0] Ra = F[:, 1] DR = -F[:, 2] # 策略:沉积速率不低于 0.8 倍最大值,在此约束下选硬度最高 dr_threshold = 0.8 * DR.max() mask = DR >= dr_threshold idx_candidates = np.where(mask)[0] best_idx = idx_candidates[np.argmax(HV[idx_candidates])] print("推荐工艺参数:") print(f"功率 {X[best_idx,0]:.1f} W, 扫描速度 {X[best_idx,1]:.2f} mm/s, " f"送粉率 {X[best_idx,2]:.2f} g/min, 搭接率 {X[best_idx,3]:.1f}%, " f"离焦量 {X[best_idx,4]:.2f} mm") print(f"预测硬度 {HV[best_idx]:.1f} HV, 粗糙度 {Ra[best_idx]:.3f} μm, " f"沉积速率 {DR[best_idx]:.3f} mm³/s")选解这一步最容易出的问题是:直接选硬度最高的点,结果沉积速率低到没有工程意义。所以一定要先加工程约束再排序。另外,Pareto 前沿上的解彼此不可支配,没有“绝对最优”,论文里如果只报一个点,通常就是加了偏好或者约束之后挑出来的。
4. 复现时最容易翻车的几个地方
4.1 数据量太少还硬上复杂网络
现象:训练集 R² 能到 0.98,验证集 R² 只有 0.6,Pareto 解拿去实验完全对不上。 原因:样本只有二三十组,网络参数却有上千个,直接记住了训练数据。 解决:把网络缩到单隐藏层 16 个神经元,加 L2 正则和早停,或者改用高斯过程回归这类小样本友好的代理模型。数据少于 50 组时,BP 网络不是首选。
4.2 归一化器只对训练集 fit,预测时忘了 transform
现象:遗传算法找到的解反变换后功率变成负数或者几千瓦。 原因:预测时直接把物理量喂给网络,没有用训练时的 scaler 做 transform。 解决:把 scaler_X 和 scaler_y 用 joblib 保存,预测和寻优全程走同一套变换。代码里predict_properties必须先 transform 再进网络,出来再 inverse_transform。
4.3 目标函数方向搞反
现象:Pareto 前沿上硬度越优化越低,粗糙度越优化越高。 原因:NSGA-II 默认最小化,硬度没取负,或者粗糙度取了负。 解决:在 objectives 函数里明确注释每个目标的优化方向,硬度和沉积速率取负,粗糙度保持正。跑完先检查 F 矩阵的符号,再画图。
4.4 遗传算法参数照搬教程,种群和代数不匹配
现象:迭代 50 代就收敛,Pareto 前沿只有几个点,分布还挤在一起。 原因:种群 20、代数 50,搜索空间根本没覆盖。 解决:5 个变量、3 个目标,种群至少 100,代数至少 200。先跑一次看 Pareto 前沿点数,少于 20 个点就加种群或加代数。
4.5 忽略工艺约束,解出物理上不可行的参数
现象:优化结果里扫描速度 12 mm/s 配功率 300 W,实际打出来全是球化。 原因:目标函数只用了网络预测,没有加熔池稳定性、粉末利用率这类隐式约束。 解决:在 Problem 里加 n_constr,把“功率/速度比”或者“线能量”限制在实验验证过的区间内。或者后处理时把不可行解过滤掉,再在前沿上选点。
5. 用实验验证 Pareto 解:从代理模型到真实工件
代理模型加遗传算法给出的终究是预测值,最后一步必须落到实验验证。我一般会从 Pareto 前沿上挑 3 个点:一个偏硬度、一个偏粗糙度、一个折中,再加上原始实验里最好的那组作为对照,一共 4 组,每组做 3 个试样。打印完测硬度用维氏硬度计、粗糙度用白光干涉仪或接触式轮廓仪、沉积速率用称重法算。把实测值和预测值放一起算误差,如果硬度误差在 10% 以内、粗糙度在 15% 以内,说明代理模型可信,这套参数可以进工艺库。
验证时有个细节容易被忽略:LMD 打印的试样要统一后处理状态。如果一组打印完直接测,另一组去应力退火后测,硬度数据没有可比性。NiTi 合金尤其敏感,打印态和热处理态的显微硬度能差几十 HV。所以验证实验要么全部打印态测,要么全部同制度热处理后测,别混。
# 验证数据与预测数据对比 import numpy as np predicted = np.array([ [420, 3.2, 1.05], [380, 2.1, 1.20], [400, 2.6, 1.12], [360, 3.8, 0.95], ]) measured = np.array([ [408, 3.4, 1.01], [372, 2.3, 1.15], [391, 2.8, 1.08], [350, 4.0, 0.92], ]) for i, name in enumerate(["硬度 HV", "粗糙度 μm", "沉积速率 mm³/s"]): err = np.abs(measured[:, i] - predicted[:, i]) / predicted[:, i] * 100 print(f"{name} 平均相对误差 {err.mean():.1f}%")如果误差超标,优先查两件事:一是实验时粉末批次和网络训练数据是否一致,二是设备实际输出功率和设定值有没有偏差。LMD 设备的激光功率衰减、送粉器标定漂移都会让实测偏离预测,这类系统误差不是算法能修的。
这套流程我前后跑过几轮,最大的体会是:神经网络和遗传算法本身都不难,难的是数据表里的每一行都经得起推敲。工艺参数优化不是把代码跑通就完事,Pareto 解选出来之后敢不敢上机、上机之后误差能不能收敛,才是判断这套方案值不值得投入的标准。希望帮到你。
本文还有配套的精品资源,点击获取