
简介这份压缩包是一个基于Python实现的多层神经网络实战项目适用于希望结合代码学习神经网络原理、预测建模以及模型参数存储的开发者也可作为疲劳预测等工程回归问题的参考实现。资源共32个文件、大小约15.96MB其中6个py脚本覆盖MNIST手写数字识别、曲线拟合、逻辑回归等训练与预测流程20余个txt文件保存了各网络层的权重与偏置参数便于直接加载复用4个idx格式文件为经典MNIST数据集可用于快速验证模型效果整体目录结构清晰、开箱即用。目前已有183人学习下载适合具备一定Python基础、正处在机器学习和神经网络入门到进阶阶段的读者。通过阅读NeuralNetwork.py、demo脚本及配套模型文件可以理解网络层如何定义、参数如何持久化并学习将神经网络应用到回归预测或分类任务中的完整思路是一份兼顾教学演示与二次开发的实用代码包。1. 用神经网络做疲劳子程序预测卡住的不是模型而是数据切片疲劳子程序在结构强度分析里通常是个黑盒给一组应力幅、均值、循环次数返回一个损伤值或寿命。传统做法是查S-N曲线再用Miner线性累积但变幅载荷下误差经常超过一倍遇到多轴应力或缺口件就更不可靠。神经网络NNA做疲劳预测并不是新鲜事真正让它从论文落到子程序的难点在如何把载荷谱切成网络能学的样本以及训练完以后怎么嵌入到现有的Fortran或C仿真流程里。这篇博文按我实际踩过的路径来讲先定输入输出切片再选网络结构和损失函数最后给出一个能直接调用的子程序接口。适合做结构疲劳、轴承寿命预测和设备预测性维护的工程师也适合想把MATLAB故障诊断和寿命预测那套经验迁移到Python里的读者。2. 疲劳子程序预测的输入输出设计先定特征再谈网络2.1 疲劳载荷谱与寿命标签的构建方法神经网络预测疲劳子程序第一件事不是搭网络而是把连续载荷谱变成有监督样本。原始信号是时间-应力序列网络不能直接学“整个序列对应多少寿命”——数据量不够物理意义也弱。常见做法是先用雨流计数把载荷谱拆成循环每个循环提取幅值、均值、循环次数再组合成窗口。2.1.1 雨流计数后提取循环幅值与均值# 用rainflow库做雨流计数提取循环特征 import numpy as np import rainflow # pip install rainflow # signal是传感器采集的应力时间序列单位MPa signal np.array([100, 95, 120, 80, 150, 60, 110, 105, 90, 130]) cycles list(rainflow.extract_cycles(signal)) # cycles每个元素是 (range, mean, count) 范围、均值、计数 ranges np.array([c[0] for c in cycles]) means np.array([c[1] for c in cycles]) counts np.array([c[2] for c in cycles]) # 生成样本每个样本是窗口内n个循环的统计特征 def make_features(ranges, means, counts, window8): X [] for i in range(len(ranges) - window 1): w_r ranges[i:iwindow] w_m means[i:iwindow] w_c counts[i:iwindow] X.append([ np.mean(w_r), np.std(w_r), np.max(w_r), np.mean(w_m), np.std(w_m), np.mean(w_c), np.sum(w_c) ]) return np.array(X)这段代码把雨流计数得到的循环序列滑窗每个窗口内的统计量作为特征。窗口长度window是第一个要调的参数太小网络看不到载荷相互作用太大样本量骤减。我一般先按载荷谱的最小重复块长度设定比如一个起飞着陆循环是8个峰谷就直接用8。2.1.2 用Miner准则生成虚拟标签的陷阱标签怎么来最简单的做法是用Miner线性损伤公式累加得到总损伤然后折算成等效寿命。省事但有陷阱这等于让神经网络去拟合一个本身就是线性近似的标签结果网络学到的还是Miner的误差只不过用非线性曲线包装了一下。另一种做法是直接拿台架试验的寿命数据做标签但工程现场大量部件没有那么多试验样本。所以实际工程里我习惯做混合标签有试验寿命的用试验值没有的用Miner值打底再叠加一个随机噪声模拟模型误差让网络知道边界不是精确的。这个噪声幅度控制在±15%以内太大网络学不到规律太小容易过拟合试验噪声。2.2 NNA网络结构选型MLP足够LSTM才需要“NNA”在工程语境里常指神经网络近似Neural Network Approximation不特指某个库。疲劳子程序的输入是窗口统计特征不是原始波形所以绝大多数场景用多层感知机MLP就够。我见过不少同事一上来就上LSTM理由是载荷谱是时间序列但输入窗口只有十几维LSTM完全杀鸡用牛刀而且训练慢、子程序部署难。只有两种情况我会换LSTM或Transformer一是载荷谱本身没有稳定的统计特征比如随机振动信号二是有大量原始波形样本想省掉人工提特征这一步。对于常规疲劳子程序MLP是更可控的选择。# 用Keras搭一个3层MLP做疲劳寿命预测 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout, BatchNormalization from tensorflow.keras.optimizers import Adam model Sequential([ Dense(64, activationtanh, input_shape(7,)), BatchNormalization(), Dropout(0.2), Dense(32, activationtanh), BatchNormalization(), Dropout(0.1), Dense(1, activationrelu) # 寿命输出恒为正 ]) model.compile(optimizerAdam(learning_rate1e-3), losshuber, # 对离群试件数据更稳 metrics[mae])隐藏层激活用tanh而不是ReLU后面第4章会说原因。输出层用relu保证寿命非负。输入特征是7维隐含层6432对疲劳样本几百到几千条不会严重欠拟合也足够小到能嵌入子程序。2.3 输入特征标准化与数据切片疲劳预测的特征量纲差异很大应力幅值有几十到几百MPa标准差可能只有个位数循环计数又是小数。不标准化网络训练会非常不稳定。标准做法是先fit训练集再transform验证集和测试集绝对不能用全样本fit后再切分否则会数据泄露。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X make_features(ranges, means, counts, window8) y np.array([equivalent_life(feature) for feature in X]) # 标签 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, shuffleTrue) scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) # 只用transform这里shuffleTrue是默认行为但疲劳数据往往来自不同载荷谱如果同一个载荷谱的样本同时出现在训练和测试里验证结果会虚高。更严格的做法是按载荷谱文件分组split比如按部件编号或工况编号划分而不是按行随机划分。我会用GroupShuffleSplit替代train_test_split防止同一工况的循环样本互相泄漏。3. 从Python原型到疲劳子程序嵌入用ONNX或纯NumPy部署3.1 训练好的模型如何变成Fortran/C可调用的子程序疲劳子程序最终要跑进现有强度仿真流程常见宿主是ANSYS Workbench的APDL、Abaqus UMAT或自研Fortran程序。Python模型不能直接放进去三条路导出ONNX再转C调用、用NumPy手写推理代码、或者把模型量化成查表。第三条路精度损失大我一般不推荐。最省事的是ONNX。Keras模型导出ONNX后可以在C里用ONNX Runtime调用生成一个动态链接库再封装成Fortran能调的接口。但ONNX Runtime本身有体积和内存开销在小规模疲劳子程序里有点重。3.2 最小实现用joblib加载模型预测疲劳寿命如果你的疲劳子程序运行在Windows/Linux的Python环境里很多数据后处理系统就是这么部署的最简单的做法是用joblib把训练好的模型打包在子程序接口里直接加载预测。# 在疲劳子程序模块中加载模型并预测 import joblib import numpy as np # 训练完成后保存 # joblib.dump(model, fatigue_nna.joblib) # joblib.dump(scaler, fatigue_scaler.joblib) model joblib.load(fatigue_nna.joblib) scaler joblib.load(fatigue_scaler.joblib) def fatigue_damage_subroutine(mean_stress, amp_stress, num_cycles, counts): 模仿Fortran子程序接口的Python函数 # 输入来自外部载荷统计组装成模型输入格式 features np.array([[ amp_stress, amp_stress * 0.1, amp_stress * 1.2, mean_stress, mean_stress * 0.05, counts, num_cycles ]]) features_s scaler.transform(features) # 预测寿命循环次数 life_pred float(model.predict(features_s)[0][0]) # 累加损伤 damage num_cycles / max(life_pred, 1e-6) return min(damage, 10.0) # 损伤上限保护这段代码的接口模拟了传统疲劳子程序传入一个循环的均值应力、幅值、循环次数和计数返回累积损伤。注意counts是雨流计数里的循环计数通常等于1但在合并小循环时可能大于1。损伤计算用预测寿命做分母超过10就截断防止数值爆炸。3.3 子程序接口设计输入应力幅、均值、循环次数输出损伤在真实Fortran/C环境里接口要更简单不能传Python对象。我把模型权重导出为纯NumPy数组写一个不依赖任何第三方库的推理函数方便直接翻译成C语言。这个函数可以放到共享库里供APDL或UMAT调用。# 纯NumPy推理不依赖Keras def mlp_predict(weights, biases, x): 手动实现3层MLP前向传播x为标准化后的样本 layer x for i in range(3): layer np.tanh(np.dot(layer, weights[i]) biases[i]) if i 2 else np.dot(layer, weights[i]) biases[i] return layer # 调用方式每个疲劳子程序入口调一次 weights [layer.get_weights()[0] for layer in model.layers if len(layer.get_weights()) 0] biases [layer.get_weights()[1] for layer in model.layers if len(layer.get_weights()) 0] np.savez(nna_weights.npz, *weights, *biases)这里有一个要注意的细节Keras的BatchNormalization层在推理时是固定均值和方差不能直接忽略。如果手动实现需要把BatchNorm的参数折叠进前一层的权重里。最简单的方法是在导出手动权重之前将模型设置为inference模式或者直接用ONNX导出让它处理BatchNorm的折叠。4. 疲劳预测模型的三个关键参数与过拟合抑制4.1 神经元数与激活函数tanh比ReLU更平稳疲劳寿命预测的输入输出关系通常是单调但存在平台区。ReLU在负区间梯度为零容易让神经元在训练中死亡尤其输入标准化后均值接近零会有大约一半的神经元处于死区导致预测曲线出现阶梯状。我改用tanh后曲面平滑很多尤其是在低应力幅值段寿命陡增的区域tanh的饱和特性刚好贴合材料疲劳极限附近的拐弯。神经元数量上6432已经足够。疲劳样本一般不超过一万条若神经元超过128训练集精度飙升但验证集会迅速变差。我一般用50% dropout加BatchNorm来控制过拟合而不是盲目加神经元。4.2 损失函数用Huber损失替代MSE抑制离群点疲劳数据里总有少数试件寿命异常偏低或偏高比如内部夹杂物导致过早断裂。MSE会把这种离群样本的误差平方放大模型被迫去适应极端值损失了对大多数样本的拟合精度。Huber损失在误差小的时候是平方误差大的时候变线性天然对离群点不敏感。def huber_loss(y_true, y_pred, delta1.0): error tf.abs(y_true - y_pred) quadratic 0.5 * tf.square(error) linear delta * error - 0.5 * delta**2 return tf.reduce_mean(tf.where(error delta, quadratic, linear))delta这个参数决定了“平方”和“线性”的切换点。对寿命取对数之后再算损失比直接算寿命损失更合理。因为寿命的分布跨越几个数量级直接MSE会让高寿命样本主导梯度。我会先把y取log10输出也解释成对数寿命预测时再10**还原。4.3 训练策略EarlyStopping与学习率衰减疲劳预测模型的训练很容易在几十个epoch后过拟合。常见做法是EarlyStopping加ReduceLROnPlateau。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau callbacks [ EarlyStopping(monitorval_loss, patience20, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience10, min_lr1e-5) ] history model.fit( X_train_s, y_log_train, validation_data(X_test_s, y_log_test), epochs500, batch_size32, callbackscallbacks, verbose0 )这里patience是连续多少个epoch验证损失不下降就停止。设为20是因为疲劳数据噪声大验证损失会抖动设太小容易早停。ReduceLROnPlateau在loss平台期自动减半学习率能帮模型钻进更细的局部最优点。restore_best_weights必须设成True否则返回的是最后一次epoch的权重而不是验证集最好的那组。4.4 验证指标R²、均方根误差以及寿命对数误差疲劳预测不能只看R²。R²接近1可能只是因为数据点范围宽模型预测一个大趋势就得分很高。我除了R²还看对数均方根误差它度量的是预测寿命和真实寿命的数量级偏差。指标公式工程含义R²1 - SS_res / SS_tot整体拟合优度0.95算合格RMSE(log10)sqrt(mean((log10 y_pred - log10 y_true)^2))寿命数量级误差0.2即误差在1.58倍以内误差倍数10^RMSE(log10)实际寿命偏差范围2表示能用于工程筛选我在交付子程序时会额外输出一个“3倍误差覆盖率”即预测寿命落在真实寿命1/3到3倍之间的样本占比。这个指标比RMSE更直观客户更关心到底有多少比例的预测是“能用”而非精确到天数。5. 把疲劳子程序接入现有仿真流程的一个技巧增量更新与不确定性输出疲劳子程序一旦上线不要指望它永远可靠。材料特性随温度、腐蚀环境、加工状态漂移长期使用后误差会越来越大。所以我会在子程序里同时输出预测寿命和预测方差让上层系统决定是继续信任还是重新训练。5.1 用滚动预测方式在线更新子程序常见做法是每隔固定周期把新产生的试验数据加入训练集重新训练并替换部署权重。但每次全量训练代价高。我采用增量更新的策略保留模型结构只微调最后两层用新数据做低学习率的短期训练同时用旧数据做一个正则化限制参数变化幅度。# 加载旧模型并冻结前两层只微调输出层 from tensorflow.keras.models import load_model old_model load_model(fatigue_nna.h5) for layer in old_model.layers[:2]: layer.trainable False new_model old_model new_model.compile(optimizerAdam(learning_rate1e-4), losshuber, metrics[mae]) new_model.fit(new_data, new_labels, epochs30, validation_split0.1)这个技巧的好处是保留原来的底层特征提取能力只让输出层适应材料老化趋势。要注意的是新数据量必须远小于旧数据否则会灾难性遗忘。我通常把新旧样本按1:10混合训练而不是只投新数据。5.2 输出预测区间而非点估计疲劳寿命预测点估计本身有不确定性但工程决策需要知道下限或者区间。我采用分位数回归在输出层同时输出三个节点P10、P50、P90寿命。训练时分别用0.1、0.5、0.9分位损失函数。def quantile_loss(q): def loss(y_true, y_pred): e y_true - y_pred return tf.reduce_mean(tf.maximum(q * e, (q - 1) * e)) return loss # 输出层3个节点对应P10/P50/P90 # model.compile(loss[quantile_loss(0.1), quantile_loss(0.5), quantile_loss(0.9)])这样在子程序接口里可以直接返回“保守寿命”和“中位寿命”维护人员用P10做检修周期用P50做规划寿命。相比传统安全系数法这种量化不确定性的方式更透明也更容易被验证。5.3 现场验证方法对已知试件做盲测子程序部署后我习惯留一批已知寿命的试件不参与训练也不参与开发期间的测试只在现场交接时跑一次盲测。盲测通过标准就是前面提的“3倍误差覆盖率”超过85%并且P10区间覆盖真实寿命的比例超过90%。这个验证方法能挡住数据预处理在训练和推理时不一致的问题——比如标准化参数用错、特征顺序串位这些问题在单元测试里往往查不出来但盲测一跑全暴露。盲测脚本要固定随机种子保存训练时的Scaler参数和特征列顺序加载时严格按同一管线推理。我的经验是80%的疲劳子程序部署失败都发生在这一步而不是模型训练环节。把盲测脚本写进CI流程以后每次更新模型都要跑一遍比口头承诺可靠得多。本文还有配套的精品资源点击获取