纯Python手写BP神经网络:从txt权重到疲劳寿命预测实战 📅 发布时间:2026/9/14 2:22:34 👁 浏览次数: 简介压缩包NeuralNetwork-master提供了一套基于神经网络NNA的疲劳预测完整工程适合机械与结构工程研究人员及有机器学习基础的开发者用来解决周期性载荷作用下寿命难以精确估算的问题。包内共有32个文件大小约15.96MB包含6个Python脚本覆盖模型定义、训练与三个演示程序MNIST手写识别、曲线拟合、逻辑回归另有22个txt权重参数文件和4个idx格式MNIST数据集分别用于查看各层网络参数和快速验证模型效果。项目中partyuhw相关的疲劳子程序通过归一化、雨流计数等预处理将原始载荷历程转换为网络输入打通了从数据准备到预测输出的完整链路。已有183人学习下载后可直接运行脚本复现实验也可以借鉴其代码结构将网络替换为自定义疲劳数据再做预测适合作为深度学习在工程领域落地的参考范例。1. 纯 Python 神经网络 NNA疲劳预测之前先搞清楚它是什么拿到NeuralNetwork-master这个压缩包时我原本以为又是某个依赖 TensorFlow 的玩具例程。解压之后看到demo_mnist.py、demo_curve_fitting.py、NeuralNetwork.py、tools.py以及一堆nn_layer_*_W.txt/nn_layer_*_B.txt权重文件反倒松了口气这是一个不依赖深度学习框架、手写 BP 前馈网络的完整工程训练好的权重以纯文本保存能在不装 PyTorch 的机器上重新加载也方便把前向计算翻译成 C 或 Fortran嵌入到疲劳寿命预测子程序里。工程里三个 demo 分别对应分类、回归、逻辑回归覆盖了神经网络的三种典型用法而“疲劳子程序”这类场景需要的恰恰是回归和分类之外的数值稳定性这正是这套轻量实现值得拆开看的原因。这套代码适合两类人一类是想把神经网络用到工程预测、但不希望被框架黑盒挡在门外的人另一类是在做材料疲劳、结构寿命预测需要把训练好的模型部署到自编子程序里的人。它不是一个成品疲劳分析软件而是一个可被替换、可被嵌入的前馈神经网络内核做一些常见的回归预测完全够用。2. NeuralNetwork.py 核心BP 前馈网络的结构与权重存储2.1 三层模型的参数记忆方式从文件名可以复原出网络的拓扑nn_layer_0_W.txt、nn_layer_1_B.txt、nn_layer_2_W.txt等默认是 3 层或 4 层网络每个_W是权重矩阵每个_B是偏置向量。以demo_curve_fitting.py里model_reg目录为例它导出了nn_layer_0_W.txt到nn_layer_3_W.txt说明网络是 4 层结构也就是两个隐藏层。这种命名方式和 PyTorch 的state_dict类似只是去掉了框架序列化格式直接用numpy.savetxt落盘便于人工检查。常见的手写 BP 网络会这样组织参数import numpy as np class NeuralNetwork: def __init__(self, layers, learning_rate0.01, activationsigmoid): self.layers layers self.lr learning_rate self.activation activation self.weights [] self.biases [] for i in range(len(layers) - 1): # 参数初始化小随机数能避免对称性问题 w np.random.randn(layers[i], layers[i 1]) * np.sqrt(2.0 / layers[i]) b np.zeros((1, layers[i 1])) self.weights.append(w) self.biases.append(b) def forward(self, x): a x for i in range(len(self.weights)): z a self.weights[i] self.biases[i] if i len(self.weights) - 1: a self._activate(z) else: a z # 输出层使用线性激活适合回归 return a def _activate(self, z): if self.activation sigmoid: return 1.0 / (1.0 np.exp(-z)) elif self.activation tanh: return np.tanh(z) elif self.activation relu: return np.maximum(0, z)代码里layers列表像[4, 16, 8, 1]那样定义输入、两个隐藏层和输出节点数。权重矩阵的行数等于上一层节点数列数等于当前层节点数前向计算用a W b完成。输出层默认线性激活这是做疲劳寿命这类连续值预测的关键如果输出层用 sigmoid输出被限制在 0 到 1 之间必须做小数标定模型很容易学偏。2.2 反向传播与误差分配BP 的核心是按链式法则分配误差普通二阶梯度法遇到深层网络容易梯度消失但这个项目只有三四层直接用梯度下降是安全的。训练时最常用的损失是均方误差def fit(self, X, y, epochs2000, batch_size32): for epoch in range(epochs): idx np.random.permutation(len(X)) for start in range(0, len(X), batch_size): batch_idx idx[start:start batch_size] x_batch X[batch_idx] y_batch y[batch_idx] # 前向保存每层输出反向计算梯度 activations [x_batch] zs [] a x_batch for w, b in zip(self.weights, self.biases): z a w b zs.append(z) a self._activate(z) if zs.index(z) len(self.weights) - 1 else z activations.append(a) # 输出层误差 delta (activations[-1] - y_batch) / batch_size # 反向逐层更新 for layer in range(len(self.weights) - 1, -1, -1): self.weights[layer] - self.lr * activations[layer].T delta self.biases[layer] - self.lr * np.sum(delta, axis0, keepdimsTrue) if layer 0: delta delta self.weights[layer].T * self._activate_deriv(zs[layer - 1])反向传播的顺序是越靠近输出层越先更新因此weights[layer]的变化量受前一层的激活值和当前层误差共同影响。learning_rate一般取 0.001 到 0.1 之间疲劳样本往往量级差异大学习率太高会导致权重发散太低则收敛慢。2.3 这套实现与主流框架的关键区别用框架写网络时权重是封装好的张量没人关心存储格式但这个工程把权重全部落为文本文件本质上暴露了“模型 矩阵 偏置”这个事实。表格可以直观对应文件模式含义维度约定nn_layer_{i}_W.txt第 i 层到第 i1 层之间的权重行数为第 i 层节点数列数为第 i1 层节点数nn_layer_{i}_B.txt第 i1 层偏置1 × 第 i1 层节点数model_reg/model分类或回归模型目录目录名区分任务类型新手容易把_W读成反向的实际上前向计算是上一输出 W B如果发现维度不匹配先检查输入特征数量是否等于第一个权重文件的行数。另一点是激活函数的选择demo_mnist.py做手写数字分类输出层大概率用 softmaxdemo_curve_fitting.py做函数拟合输出层必须是线性。疲劳寿命预测的本质是连续值回归所以后面接疲劳子程序时要按曲线拟合的配置走而不是照搬 MNIST 的分类配置。3. 用 demo_curve_fitting 练手训练、导出 txt 权重与参数调优3.1 最小可复现流程把压缩包解压后在命令行进入根目录先跑自带的拟合示例python demo_curve_fitting.py这个脚本的作用是让网络学习一个已知函数比如y x * sin(2x)之类。运行结束后model_reg目录下会生成多组nn_layer_*_W.txt和nn_layer_*_B.txt表示训练完成的权重已经导出。用同样的方式跑分类示例python demo_mnist.py会得到model目录下的权重。如果希望清空之前的输出重新训练把model_reg目录删掉再运行即可不要手动改文件名。训练结束后可以写一段代码验证加载例如import numpy as np def load_model(model_dir, layer_count): weights [] biases [] for i in range(layer_count - 1): w np.loadtxt(f{model_dir}/nn_layer_{i}_W.txt) b np.loadtxt(f{model_dir}/nn_layer_{i}_B.txt) if b.ndim 1: b b.reshape(1, -1) weights.append(w) biases.append(b) return weights, biases w, b load_model(model_reg, 4) print(w[0].shape) # 第一层权重矩阵维度load_model的layer_count必须与训练时的layers列表长度一致这里 4 代表输入层加两个隐藏层再加一个输出层。np.loadtxt的默认格式是空格分隔正好匹配savetxt的输出。偏置向量保存后是 1 维数组手动.reshape(1, -1)是为了前向计算广播时不出错。3.2 训练参数怎么定做疲劳预测时输入特征包括应力幅、平均应力、循环次数、材料抗拉强度等数量级可能从 1 到 1e7 不等。如果不做标准化第一层权重很快就爆炸。项目里tools.py大概率承担数据预处理工作自己写时通常这样做def normalize(x, meanNone, stdNone): if mean is None: mean np.mean(x, axis0) std np.std(x, axis0) return (x - mean) / (std 1e-12), mean, std中间层的节点数不是越多越好。根据经验疲劳寿命样本一般只有几百到几千条两个隐藏层各 832 个节点足够拟合再多就会把噪声也学进去。学习率从 0.01 开始观察损失曲线如果损失震荡降到 0.001如果下降极慢尝试增大到 0.05。正则化在这个手写实现里不一定内建可以自己加一个简单的 L2 项# 在损失函数中加入 0.5 * lambda * sum(w^2) reg_loss 0.5 * 0.001 * sum(np.sum(wi ** 2) for wi in weights)正则项会约束权重幅度防止某个特征的主导权重过大。疲劳数据里循环次数和应力幅常呈指数关系隐藏层激活函数建议用tanh而非sigmoid因为tanh零中心化收敛更稳定。输出层必须保持线性否则预测寿命会被截断在 sigmoid 的值域内。3.3 从 txt 权重反推网络层数目录model_reg里的权重文件编号到 3也就是有nn_layer_0_W、nn_layer_1_W、nn_layer_2_W、nn_layer_3_W共 4 组权重矩阵对应 4 个层之间的连接所以网络总层数是 5这里需要区分“网络层数”和“权重矩阵数量”。权重矩阵数量 隐藏层数 1。如果看到nn_layer_0_W到nn_layer_3_W说明有 4 个权重矩阵即 3 个连接跳转也就是输入层 2 个隐藏层 输出层。文件编号到layer_3只是说最后一个权重矩阵的下标是 3并不代表有 4 个隐藏层。我在拆这个包时被这个坑绊过一次。用load_model时layer_count 4对应的应是要读 3 个_W文件但如果目录里出现了nn_layer_3_W.txt说明实际保存的是 4 个_W文件此时layer_count应设为 4读到的权重矩阵个数也是 4。务必以文件列表为准不要只凭 demo 的“三层网络”描述去猜。4. 把神经网络接进疲劳子程序从雨流计数到寿命预测的完整链路4.1 疲劳子程序到底承担什么在材料疲劳分析中子程序通常不是指一个独立软件而是被嵌入有限元求解器或疲劳分析主程序的一段代码。它需要把随时间的载荷谱压缩成循环计数再把每个循环的应力幅、均值、循环次数等特征交给预测模型输出疲劳寿命或损伤。神经网络在这里的作用替代传统 S-N 曲线插值优点是能同时考虑多个影响因子不再局限于单一应力幅与寿命的关系。常见处理流程是原始载荷序列 → 雨流计数法统计循环 → 提取每个循环的应力幅Sa、平均应力Sm、循环数n→ 输入网络 → 输出该循环的允许寿命Nf→ 按 Miner 线性累积损伤公式计算总损伤。雨流计数是典型的“疲劳子程序”逻辑它做的是数据压缩不直接参与寿命预测但它的输出质量决定了网络输入可靠性。4.2 构造神经网络训练集假设我们要用NeuralNetwork.py训练一个寿命预测模型输入层可以设计成# 特征顺序应力幅 Sa平均应力 Sm循环次数 n材料抗拉强度 Su X np.column_stack([Sa, Sm, n, Su]) # 输出为对数寿命 log10(Nf)而不是原始寿命 y np.log10(Nf)为什么用对数寿命因为疲劳寿命跨度从 1e3 到 1e7直接回归线性寿命会让模型把注意力集中在 1e7 的大数值上对数变换后输出量级更均衡。训练后预测得到的是log10(Nf)要得到实际寿命值需要10 ** pred反变换。训练代码中调用拟合函数nn NeuralNetwork(layers[4, 16, 8, 1], learning_rate0.005, activationtanh) # 这里用 load_model 加载已有权重或重新训练 for epoch in range(3000): nn.fit(X_train, y_train, epochs1, batch_size16)fit只传一个 epoch是为了在每轮之后手动验证验证集误差而不是等 3000 轮跑完才发现过拟合。batch_size取 16 或 32疲劳样本通常不超过几千条小批量能让梯度更新更稳定。4.3 与雨流计数代码衔接雨流计数的实现有很多版本这里用一个简化版示意思路提取峰值谷值后按幅值配对循环。真正工程上建议用成熟的疲劳分析库或自己实现完整的四点法下面只展示接口风格def extract_sa_sm_with_rainflow(load_series): # 简化的雨流计数逻辑返回两个列表 # 实际实现需要考虑载荷序列的峰值谷值配对 peaks [] for i in range(1, len(load_series) - 1): if (load_series[i] load_series[i-1] and load_series[i] load_series[i1]) or \ (load_series[i] load_series[i-1] and load_series[i] load_series[i1]): peaks.append(load_series[i]) cycles [] for j in range(0, len(peaks) - 1, 2): sa abs(peaks[j1] - peaks[j]) / 2.0 sm (peaks[j1] peaks[j]) / 2.0 cycles.append((sa, sm, 1)) return cycles得到每个循环的(Sa, Sm, n)后先做标准化再调用网络# 假设用训练时的均值 mean_x, std_x 标准化 X_pred np.array([[sa, sm, n, su]]) X_pred_norm (X_pred - mean_x) / std_x logNf_pred nn.forward(X_pred_norm)[0, 0] Nf_pred 10 ** logNf_pred # Miner 损伤累积 D D n / Nf_pred这里的nn.forward返回的是一个二维数组取第[0,0]个元素就是预测值。注意标准化时一定要复用训练集的mean_x/std_x不能用预测时刻重新计算的均值否则输入分布不一致输出完全失真。Miner 损伤累积算出的 D 超过 1.0 时工程上视为发生疲劳破坏。4.4 嵌入 UMAT/VUMAT 的思路要在有限元子程序里调用这个网络不能直接跑 Python。常见做法是训练好后把权重导出为 C 数组再在子程序里实现前向计算。比如把nn_layer_0_W.txt转成二维静态数组代码框架如下static const float w0[4][16] { ... }; static const float b0[16] { ... }; float predict(float sa, float sm, float n, float su) { float h0[16]; for (int j 0; j 16; j) { h0[j] tanhf(sa * w0[0][j] sm * w0[1][j] n * w0[2][j] su * w0[3][j] b0[j]); } // 继续第二层和输出层输出层无激活 }这种转换方式称不上高明但胜在完全可控。子程序每调用一次就完成一次前向传播耗时可以忽略不计。把 Python 训练权和 C 推理分开既保住了训练时的便利也满足子程序对执行效率的要求。5. 验证与部署交叉验证、C 数组移植与数值一致性检查5.1 量化验证模型是否真的可用疲劳预测最怕“训练集分数很高真实载荷一测就偏”。我一般会把数据集按时间顺序切分而不是随机切分模拟未来工况前 70% 的载荷谱做训练后 30% 做验证。同时检查预测结果的分布是否合理from sklearn.metrics import r2_score, mean_absolute_error logNf_true y_test logNf_pred nn.forward(X_test_norm)[:, 0] print(R2:, r2_score(logNf_true, logNf_pred)) print(MAE:, mean_absolute_error(logNf_true, logNf_pred))R2 达到 0.9 以上才有工程参考价值如果 R2 低于 0.8优先检查输入特征和标准化方式不要立刻加深网络。另一个常见陷阱是输出层激活函数如果nn.forward里最后一层还经过 tanh预测值会被限制在 -1 到 1 之间那么10 ** pred就毫无意义。拿到别人的权重文件时先单点测试一个已知样本比看训练损失可靠得多。5.2 权重导出为 C 数组的自动化脚本手工从 txt 复制到 C 数组容易错位我习惯用一行 Python 生成静态数组声明这样后续部署子程序时不需要人工干预import numpy as np for layer in range(4): w np.loadtxt(fmodel_reg/nn_layer_{layer}_W.txt) with open(flayer_{layer}.h, w) as f: f.write(fstatic const float w{layer}[{w.shape[0]}][{w.shape[1]}] {{\n) for row in w: f.write( { , .join(f{v:.9g}f for v in row) },\n) f.write(};\n)生成的头文件可以直接被 C/C 代码#include然后把前向计算里的np.dot替换成两层 for 循环。需要注意的是 C 数组按行优先存储和 numpy 的默认C顺序一致索引w[i][j]对应 Python 里的W[i, j]不需要转置。5.3 用查找表做交叉验证为了确认 C 移植没有 bug我会做一个针对性测试在输入范围内随机生成 100 组数据分别用 Python 的nn.forward和 C 函数计算比较最大相对误差。这个环节能暴露出数组边界、激活函数实现不一致等问题。常见错误是 C 里用了pow(10, logNf)而 Python 里用10 ** logNf浮点差异通常小于 1e-6如果差异超过 1%就要检查权重矩阵是否读反了。疲劳场景下模型输出的是对数寿命1% 的预测误差对应约 1.02 倍的寿命差异工程上可以接受但如果发现某个载荷段的误差系统偏大建议把雨流计数的计数结果打印出来和网络的输入特征一起做相关性分析。很多时候不是网络本身的问题而是载荷谱压缩时把高应力幅循环漏掉了。最后再提一句这类纯前馈神经网络预测疲劳寿命本质是在已知材料 S-N 数据基础上做多因子插值外推到训练集覆盖范围之外的载荷区段时结果只可参考不可替代真实疲劳试验。本文还有配套的精品资源点击获取