纯Python手写BP神经网络:从txt权重到疲劳寿命预测实战
2026/9/14 2:22:31 网站建设 项目流程

简介:压缩包NeuralNetwork-master提供了一套基于神经网络(NNA)的疲劳预测完整工程,适合机械与结构工程研究人员及有机器学习基础的开发者,用来解决周期性载荷作用下寿命难以精确估算的问题。包内共有32个文件,大小约15.96MB,包含6个Python脚本,覆盖模型定义、训练与三个演示程序(MNIST手写识别、曲线拟合、逻辑回归);另有22个txt权重参数文件和4个idx格式MNIST数据集,分别用于查看各层网络参数和快速验证模型效果。项目中partyuhw相关的疲劳子程序,通过归一化、雨流计数等预处理将原始载荷历程转换为网络输入,打通了从数据准备到预测输出的完整链路。已有183人学习,下载后可直接运行脚本复现实验,也可以借鉴其代码结构,将网络替换为自定义疲劳数据再做预测,适合作为深度学习在工程领域落地的参考范例。

1. 纯 Python 神经网络 NNA:疲劳预测之前先搞清楚它是什么

拿到NeuralNetwork-master这个压缩包时,我原本以为又是某个依赖 TensorFlow 的玩具例程。解压之后看到demo_mnist.pydemo_curve_fitting.pyNeuralNetwork.pytools.py以及一堆nn_layer_*_W.txt/nn_layer_*_B.txt权重文件,反倒松了口气:这是一个不依赖深度学习框架、手写 BP 前馈网络的完整工程,训练好的权重以纯文本保存,能在不装 PyTorch 的机器上重新加载,也方便把前向计算翻译成 C 或 Fortran,嵌入到疲劳寿命预测子程序里。工程里三个 demo 分别对应分类、回归、逻辑回归,覆盖了神经网络的三种典型用法;而“疲劳子程序”这类场景,需要的恰恰是回归和分类之外的数值稳定性,这正是这套轻量实现值得拆开看的原因。

这套代码适合两类人:一类是想把神经网络用到工程预测、但不希望被框架黑盒挡在门外的人;另一类是在做材料疲劳、结构寿命预测,需要把训练好的模型部署到自编子程序里的人。它不是一个成品疲劳分析软件,而是一个可被替换、可被嵌入的前馈神经网络内核,做一些常见的回归预测完全够用。

2. NeuralNetwork.py 核心:BP 前馈网络的结构与权重存储

2.1 三层模型的参数记忆方式

从文件名可以复原出网络的拓扑:nn_layer_0_W.txtnn_layer_1_B.txtnn_layer_2_W.txt等,默认是 3 层或 4 层网络,每个_W是权重矩阵,每个_B是偏置向量。以demo_curve_fitting.pymodel_reg目录为例,它导出了nn_layer_0_W.txtnn_layer_3_W.txt,说明网络是 4 层结构,也就是两个隐藏层。这种命名方式和 PyTorch 的state_dict类似,只是去掉了框架序列化格式,直接用numpy.savetxt落盘,便于人工检查。

常见的手写 BP 网络会这样组织参数:

import numpy as np class NeuralNetwork: def __init__(self, layers, learning_rate=0.01, activation='sigmoid'): 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, epochs=2000, batch_size=32): 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, axis=0, keepdims=True) 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 层到第 i+1 层之间的权重行数为第 i 层节点数,列数为第 i+1 层节点数
nn_layer_{i}_B.txt第 i+1 层偏置1 × 第 i+1 层节点数
model_reg/model分类或回归模型目录目录名区分任务类型

新手容易把_W读成反向的,实际上前向计算是上一输出 @ W + B,如果发现维度不匹配,先检查输入特征数量是否等于第一个权重文件的行数。另一点是激活函数的选择:demo_mnist.py做手写数字分类,输出层大概率用 softmax;demo_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.txtnn_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_modellayer_count必须与训练时的layers列表长度一致,这里 4 代表输入层加两个隐藏层再加一个输出层。np.loadtxt的默认格式是空格分隔,正好匹配savetxt的输出。偏置向量保存后是 1 维数组,手动.reshape(1, -1)是为了前向计算广播时不出错。

3.2 训练参数怎么定

做疲劳预测时,输入特征包括应力幅、平均应力、循环次数、材料抗拉强度等,数量级可能从 1 到 1e7 不等。如果不做标准化,第一层权重很快就爆炸。项目里tools.py大概率承担数据预处理工作,自己写时通常这样做:

def normalize(x, mean=None, std=None): if mean is None: mean = np.mean(x, axis=0) std = np.std(x, axis=0) return (x - mean) / (std + 1e-12), mean, std

中间层的节点数不是越多越好。根据经验,疲劳寿命样本一般只有几百到几千条,两个隐藏层各 8~32 个节点足够拟合,再多就会把噪声也学进去。学习率从 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_Wnn_layer_1_Wnn_layer_2_Wnn_layer_3_W,共 4 组权重矩阵,对应 4 个层之间的连接,所以网络总层数是 5?这里需要区分“网络层数”和“权重矩阵数量”。权重矩阵数量 = 隐藏层数 + 1。如果看到nn_layer_0_Wnn_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_rate=0.005, activation='tanh') # 这里用 load_model 加载已有权重,或重新训练 for epoch in range(3000): nn.fit(X_train, y_train, epochs=1, batch_size=16)

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[i+1]) or \ (load_series[i] <= load_series[i-1] and load_series[i] <= load_series[i+1]): peaks.append(load_series[i]) cycles = [] for j in range(0, len(peaks) - 1, 2): sa = abs(peaks[j+1] - peaks[j]) / 2.0 sm = (peaks[j+1] + 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(f"model_reg/nn_layer_{layer}_W.txt") with open(f"layer_{layer}.h", "w") as f: f.write(f"static 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 数据基础上做多因子插值,外推到训练集覆盖范围之外的载荷区段时,结果只可参考,不可替代真实疲劳试验。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询