简介:一套融合模糊逻辑与神经网络的模糊神经网络(FNN)数据预测实践方案,用Python语言实现,面向机器学习初学者、算法研究者及有数据预测需求的工程师,能够处理现实数据中的不确定性与模糊性,从历史样本中学习规律并完成回归预测。压缩包共7个文件,包括两个脚本文件、两个数据集文件和三个参数文件:脚本分别承担模型训练与测试预测,数据集对应训练样本和测试样本,参数文件保存训练得到的隶属度函数中心点、宽度向量和权值;整体仅5KB,结构清晰,便于快速理解与运行。目前已有382人学习下载,适合作为模糊系统与神经网络结合的入门练习。通过这份资源,使用者可完整复现模型训练到预测的流程,查看平均绝对误差、平均绝对百分比误差及预测差值分布情况,直观体会隶属度参数对预测精度的影响,为进一步研究调优提供可扩展的实验基础。
1. 模糊神经网络做数据预测:一个能直接跑源码的 FNN 入手方案
把一份带时间戳的历史数据扔给预测模型,对输出往往只有两条要求:足够准,以及解释得清。普通回归和 BP 网络在带噪声、带模糊边界的真实业务数据上并不稳,黑匣子式的拟合结果也难向业务方交代。这是我对 FNN 的第一印象——模糊神经网络先解决“你敢不敢用”的问题。
FNN 的思路是用模糊规则承担可解释性,用神经网络把规则参数从数据里学出来。这份资源是 FNN 做数据预测的一套完整 Python 实现:FNN.py 用 train.csv 训练并保存中心、宽度、权值等参数,test.py 用训练好的模型预测 test.csv,输出 MAE、MAPE 和预测差值分布。适合刚开始把模糊系统落到代码上的开发者,也适合正在做设备状态、能耗这类指标预测的从业者直接改源码。
2. FNN 的运作机制:T-S 模糊规则如何被神经网络训练出来
2.1 模糊逻辑擅长处理什么:为什么普通 BP 不够
先看一个具体场景。某项目要预测设备的运行温度,特征里有环境湿度、负载电流、上一时刻温度。湿度偏高、负载也偏高时温度一定会升吗?不一定,这里有明显的边界模糊性——高到什么程度才算“偏高”,对预测结果的影响其实是一个渐变过程,而不是一个硬阈值。传统回归树在这里会切出很多繁琐的阈值分支,普通 BP 网络虽然在拟合非线性关系上没问题,但它把训练得到的知识全部压在隐藏层权重里,出了错很难定位到具体某个输入特征的影响,业务方追问“为什么这组数据会被预测成高值”时,你很难给出直观解释。
纯模糊系统恰好反过来:它用“如果环境湿度偏高且负载偏高,那么温度上升较快”这类模糊规则表达因果关系,每条规则都能说清楚。但纯模糊系统的短板也明显——隶属度函数的中心、宽度和规则结论全凭人工设定,特征一多,规则组合数呈指数增长,人工设计根本不可维护。
FNN 的路径就是把这两者拼起来。模糊部分负责提供可解释的结构,神经网络部分负责把结构里的参数当成学习对象,用梯度下降从数据里自动调优。我一般在指标预测上会先用一个两三层的 BP 打底,发现中间层节点很难解释、业务方不接受时才换 FNN。换成 FNN 之后,至少能指着某条规则说:这条规则在控制样本的某个区间,权重是多少,源自哪几个特征的交互。这种透明性在工程报告里相当值钱。
2.2 五层网络结构与三类参数:c1.npy、b1.npy、w1.npy 各自管什么
FNN 的典型结构是五层,对应到这份源码里的 .npy 参数,可以一一对上。
输入层接收一个特征向量 x = (x₁, x₂, …, xₙ)。第二层是隶属度函数层,每个特征对应一组高斯函数,第 k 个特征在第 i 条规则下的隶属度写成:
uₖᵢ = exp(-(xₖ - cₖᵢ)² / (2·bₖᵢ²))
这里 cₖᵢ 就是高斯函数的中心,决定这条规则对特征取值的“关注点”落在哪个区间;bₖᵢ 是宽度,决定这个关注点的影响范围。中心矩阵 c 和宽度矩阵 b 的维度都是 (特征数 × 规则数),分别由 c1.npy 和 b1.npy 保存。
第三层是规则层。每条规则对应一个节点,激活强度取所有特征隶属度的乘积:αᵢ = u₁ᵢ × u₂ᵢ × … × uₙᵢ。这一步意味着如果任何一个特征偏离规则描述的范围,整条规则的激活强度都会明显下降。第四层做归一化:ᾱᵢ = αᵢ / (Σⱼ αⱼ),把所有规则的强度压到总和为 1,保证输出不会因为规则数增加而无界放大。最后一层是输出层,用权值加权求和得到预测值:ŷ = Σᵢ ᾱᵢ·wᵢ。
这里的 w 就是规则的输出权值,由 w1.npy 保存,维度等于规则数。训练日志或者模型文件里如果你还看到一个 b1.npy,它就是宽度向量,对应上面的 b。三类参数合在一起,就完整定义了一个模糊神经网络的预测行为。
| 参数文件 | 内容 | 维度 | 作用 |
|---|---|---|---|
| c1.npy | 隶属度函数中心 | (特征数, 规则数) | 决定每条规则的特征关注区间 |
| b1.npy | 隶属度函数宽度 | (特征数, 规则数) | 决定规则区间的覆盖范围与重叠度 |
| w1.npy | 规则输出权值 | (规则数,) | 决定各条规则的输出贡献 |
2.3 训练链路:从归一化到保存参数的完整流程
FNN.py 拿 train.csv 做训练,链路大致有四步。第一步是读数据,这个项目的 CSV 排布一般是前面若干列是特征,最后一列是标签。我拿到任何数据集都会先跑一次 df.info() 和 df.describe(),确认没有空值、量纲差异大不大,然后再进模型。
第二步是归一化。高斯隶属度对特征的绝对数值非常敏感,如果某个特征数值范围是 10000 到 50000,宽度给 0.5 的话,所有样本的高斯输出都会接近 0,激活度全部退化,训练等于白做。常见做法是把每个特征缩放到 0~1 或均值 0 方差 1 的区间,scaler 只在训练集上拟合,这一点在后面避坑部分还会重点讲。
第三步是初始化参数。中心一般在特征最小值和最大值之间均匀展开,宽度取特征范围的 10%~20%,权值用小的随机数。初始化质量决定训练起点,宽度给太小会让高斯函数太窄,导致大量样本的激活度等于 0,梯度传不下去;宽度给太大又会让所有规则长得差不多,区分度不够。
第四步是迭代训练。前向计算得到预测值,和目标值算均方误差,然后用梯度下降更新 c、b、w 三组参数。迭代完成后用 np.save 把参数写入 c1.npy、b1.npy、w1.npy。预测阶段的 test.py 只负责读这三个文件做推理,不再接触原始训练集,这是把训练和预测分开的合理设计。
3. 源码拆解:FNN.py 与 test.py 的复现步骤和参数设置
3.1 FNN.py 训练主循环:前向传播、损失与梯度更新
先看训练脚本的核心结构。这个版本保留了最直观的数值梯度写法,好处是逻辑透明,每一行都在做一件看得懂的事;代价是计算量大,适合 train.csv 这种几百行规模的数据集。如果要在更大的数据上跑,建议把梯度部分改成解析式。
# -*- coding: utf-8 -*- """ FNN.py 训练主流程(教学简化版) 训练完成后保存 c1.npy(中心)、b1.npy(宽度)、w1.npy(规则权值) """ import numpy as np import pandas as pd N_RULES = 5 # 规则数,即隶属度函数个数 EPOCHS = 200 # 最大迭代轮数 LR = 0.01 # 学习率 def load_data(path): df = pd.read_csv(path) X = df.iloc[:, :-1].values.astype(np.float64) # 特征列 y = df.iloc[:, -1].values.astype(np.float64) # 标签列 return X, y def forward(x, c, b, w): """前向计算:输入一条样本,返回预测值和归一化激活强度""" n_rules = len(w) alpha = np.zeros(n_rules) for i in range(n_rules): u = np.exp(-((x - c[:, i]) ** 2) / (2 * b[:, i] ** 2 + 1e-8)) alpha[i] = np.prod(u) # 第 i 条规则激活强度 alpha_norm = alpha / (np.sum(alpha) + 1e-8) # 归一化 y_hat = np.dot(alpha_norm, w) return y_hat, alpha_norm def numerical_gradient(x, y, c, b, w): """对单个样本计算三组参数的数值梯度""" eps = 1e-6 gc = np.zeros_like(c) gb = np.zeros_like(b) gw = np.zeros_like(w) y_hat, _ = forward(x, c, b, w) coeff = 2 * (y_hat - y) # MSE 对 y_hat 的导数 for i in range(len(w)): wp = w.copy(); wp[i] += eps wm = w.copy(); wm[i] -= eps gw[i] = coeff * (forward(x, c, b, wp)[0] - forward(x, c, b, wm)[0]) / (2 * eps) for k in range(c.shape[0]): for i in range(c.shape[1]): cp = c.copy(); cp[k, i] += eps cm = c.copy(); cm[k, i] -= eps bp = b.copy(); bp[k, i] += eps bm = b.copy(); bm[k, i] -= eps gc[k, i] = coeff * (forward(x, cp, b, w)[0] - forward(x, cm, b, w)[0]) / (2 * eps) gb[k, i] = coeff * (forward(x, c, bp, w)[0] - forward(x, c, bm, w)[0]) / (2 * eps) return gc, gb, gw X, y = load_data("train.csv") n_features = X.shape[1] # 初始化:中心在特征范围内均匀铺开,宽度取特征范围的 15%,权值小随机 c = np.linspace(X.min(axis=0), X.max(axis=0), N_RULES).T b = np.full((n_features, N_RULES), (X.max(axis=0) - X.min(axis=0)).mean() * 0.15) w = np.random.randn(N_RULES) * 0.1 for epoch in range(EPOCHS): gc = np.zeros_like(c) gb = np.zeros_like(b) gw = np.zeros_like(w) total_loss = 0.0 for xi, yi in zip(X, y): y_hat, _ = forward(xi, c, b, w) total_loss += (y_hat - yi) ** 2 dgc, dgb, dgw = numerical_gradient(xi, yi, c, b, w) gc += dgc gb += dgb gw += dgw c -= LR * gc / len(X) b -= LR * gb / len(X) w -= LR * gw / len(X) if epoch % 50 == 0: print(f"epoch {epoch}, mse = {total_loss / len(X):.6f}") np.save("c1.npy", c) np.save("b1.npy", b) np.save("w1.npy", w) print("训练完成,参数已保存:c1.npy, b1.npy, w1.npy")forward 函数是整套代码的核心。它先逐规则计算高斯隶属度并连乘得到激活强度,再归一化,最后点乘权值。注意我加了 1e-8 到分母,避免激活强度全为 0 时除零报错,这在训练前几个 epoch 很容易出现。numerical_gradient 用中心差分逼近偏导数,对每个参数做两次前向,逻辑上最不容易写错。
梯度下降部分用的是全量梯度,也就是把整个训练集每个样本的梯度累加起来再取平均更新。这条代码里值得注意的两个参数是 N_RULES 和 LR。N_RULES 决定模型容量,给大了容易把噪声也拟合进去,给小了预测曲线过于平滑;LR 决定每一步更新的步长,太大会震荡,太小收敛慢。按 train.csv 这种百行规模的数据,N_RULES 取 5~8、LR 取 0.01 是我比较常用的起点。
3.2 test.py 预测评估:MAE、MAPE 与差值分布图的生成
训练完成之后,最重要的就是看模型在未知数据上的表现。test.py 做的事情很直接:加载三个 .npy 参数文件,加载 test.csv,逐条样本计算预测值,然后对预测值和真实值做误差评估。
# -*- coding: utf-8 -*- """ test.py:加载模型参数,对 test.csv 做预测并输出误差指标 """ import numpy as np import pandas as pd import matplotlib.pyplot as plt c = np.load("c1.npy") # 隶属度函数中心 b = np.load("b1.npy") # 隶属度函数宽度 w = np.load("w1.npy") # 规则权值 def forward(x, c, b, w): n_rules = len(w) alpha = np.zeros(n_rules) for i in range(n_rules): u = np.exp(-((x - c[:, i]) ** 2) / (2 * b[:, i] ** 2 + 1e-8)) alpha[i] = np.prod(u) alpha_norm = alpha / (np.sum(alpha) + 1e-8) return np.dot(alpha_norm, w) df = pd.read_csv("test.csv") X_test = df.iloc[:, :-1].values.astype(np.float64) y_test = df.iloc[:, -1].values.astype(np.float64) preds = np.array([forward(x, c, b, w) for x in X_test]) mae = np.mean(np.abs(preds - y_test)) mape = np.mean(np.abs((preds - y_test) / (np.abs(y_test) + 1e-8))) * 100 print(f"MAE = {mae:.6f}") print(f"MAPE = {mape:.4f}%") diff = preds - y_test plt.hist(diff, bins=20, edgecolor="white") plt.xlabel("预测值 - 真实值") plt.ylabel("样本数") plt.title("Prediction Difference Distribution") plt.savefig("diff_dist.png", dpi=120) print("差值分布图已保存为 diff_dist.png")平均绝对误差 MAE = mean(|ŷ - y|),单位与预测目标一致,能直观表达平均偏差的量级。MAPE 在这里加了 1e-8 的保护,避免真实值接近 0 时计算出天文数字,后面避坑部分会说这个问题的完整处理思路。除了两个数值指标,脚本还会画一张预测差值直方图并保存为图片,这一步在工程上很实用,只看误差均值很容易漏掉系统性偏差,直方图能直接暴露预测差值往哪边偏、有没有长尾。
3.3 三个关键参数怎么调:规则数、学习率、宽度初始值
我复现这套代码时会重点调三个参数,而不是盲目跑训练。先看下面的参数表,再按照固定变量法的顺序去尝试调整。
| 参数 | 推荐起点 | 主要影响 | 调整方向 |
|---|---|---|---|
| N_RULES 规则数 | 5~8 | 模型拟合能力 | 欠拟合时增大,预测震荡时减小 |
| LR 学习率 | 0.01 | 参数更新步长 | loss 震荡时调小,收敛过慢时适当调大 |
| 宽度初始值 | 特征范围的 15% 左右 | 高斯函数的覆盖范围 | 激活度为 0 时调大,规则区分度不足时调小 |
| EPOCHS 迭代轮数 | 200~500 | 训练充分程度 | 看 mse 输出,连续多轮不变即可停止 |
调参顺序我一般这样走:先固定 N_RULES=6 和宽度默认值,把 LR 从 0.1 到 0.001 依次试一轮,看每轮打印的 mse 是稳定下降还是来回震荡,从中选一个收敛最稳的 LR。然后再把 N_RULES 从 4 到 10 扫一遍,每个值训练相同轮数后用测试集算一次 MAE,选择测试误差最小的规则数。最后再微调宽度,观察训练初期有没有出现 mse 完全不动的情况,如果有,优先把宽度往上提。
这套码最方便的地方在于三个超参数都集中在脚本头部,改一行就能重训。要提醒一句:数值梯度在小数据集上是能用的,但 train.csv 如果有个几千行、规则数再给到两位数,训练时间会明显拉长,那时候就该把数值梯度替换成解析梯度,或者干脆用自动微分框架重写梯度部分。
4. 训练与预测的常见坑:5 个现象、原因和处理办法
4.1 npy 参数加载后 shape 对不上,forward 直接报错
现象:运行 test.py 报错,提示矩阵维度不一致,比如无法把形状为 (3,) 的数组和 (4,) 的数组做点积。或者 forward 里的 c[:, i] 索引越界。
原因:训练集和测试集的特征列数不一致,或者 CSV 在读取时把行号索引也当成了特征列。训练时 c1.npy 按训练集的特征数初始化,测试集多了或少了一列,两边形状自然对不上。另一种常见情况是原始 CSV 第一列是 ID,pd.read_csv 读进来后没有用 iloc 排除掉。
解决:先把 X_test 的打印出来看 shape,再打印 c1.npy 的 shape,确认两个维度必须相等。读 CSV 的时候统一用 df.iloc[:, :-1] 取特征、df.iloc[:, -1] 取标签,并且保持训练测试两边的列顺序一致。我在跑任何新数据前都会先写一行 print(X.shape, y.shape),这行代码看起来多余,但在排查维度问题上能省半小时。
4.2 归一化把测试集统计量算进来,测试集预测全偏
现象:训练过程 loss 降得很好,训练集上预测误差很漂亮,但 test.py 跑出来的 MAE 明显偏大,预测值整体高于或低于真实值。
原因:数据预处理时把训练集和测试集放在一起计算了最小值和最大值。缩放参数里混入了测试集的信息,属于数据泄漏。模型的训练目标实际上间接包含了测试集的取值范围,测试集作为“没见过的数据”的意义失效了,换一份新数据时预测就会崩。
解决:scaler 只允许在训练集上 fit,然后分别 transform 训练集和测试集。如果 FNN 脚本里直接用了 X.min() 和 X.max() 做初始化和归一化,要注意这些统计量必须在训练阶段单独计算,测试阶段用同一组统计量做缩放,而不是重新算。从那以后我每次都会确认代码里 fit 和 transform 的调用顺序,避免这种安静但致命的泄漏。
4.3 MAPE 除以接近 0 的真实值,误差爆炸
现象:test.py 输出的 MAPE 是几百甚至几千,但 MAE 看起来还合理。比如 MAE=0.82,MAPE=346.5%。
原因:MAPE 的定义是 mean(|ŷ - y| / |y|),当某条样本的真实值 y 接近 0 时,分母趋近于 0,误差比直接放大到十几甚至几十,少数几条这样的样本就能把整体 MAPE 拉爆。数据里只要存在接近 0 的点,这个现象就无法避免。
解决:最简单的做法是在分母加一个小的 epsilon,就像 test.py 里那样。更严格的做法是在计算 MAPE 前先把真实值为 0 或接近 0 的样本过滤掉,单独统计,或者改用对分母不敏感的对称平均绝对百分比误差 SMAPE。注意加 epsilon 只是缓解爆表,它会让 MAPE 在真实值很小时失真,所以在报告指标时应该说明分母保护的处理方式。
4.4 训练 loss 震荡或纹丝不动,先查学习率和宽度
现象:训练时 mse 打印出来忽高忽低,或者连续几十轮完全不变。前一种常见于 LR 偏大,后一种常见于所有规则的激活强度都接近 0。
原因:学习率太大时参数更新跨越了最优区,loss 会来回震荡。激活强度接近 0 则要复杂一些——如果高斯宽度初始值给得太小,样本特征值与中心之间的距离远大于宽度,高斯输出 e 的指数部分是一个很大的负数,结果趋近于 0,所有规则激活强度都为 0,归一化分母靠 1e-8 勉强撑住,梯度基本传不回去。
解决:先在脚本里打印每个 epoch 的 alpha 分布,如果所有 alpha 都小于 1e-3,说明宽度初始值不匹配数据尺度,把宽度调大到特征范围的 20%~30% 再跑。然后看 loss 曲线的形态,震荡就把 LR 除以 10,完全不降就先确认激活强度正常再考虑增大 N_RULES。我一般会从 width 系数 0.15 和 LR 0.01 开始,这两个值在多数归一化后的数据集上不会出现极端情况。
4.5 每次训练结果都不一样,缺一个固定随机种子
现象:同一套代码、同一个数据集,连续跑两次 FNN.py,训练结束后的 MAE 有差异,甚至有时候训练曲线差别都不小。
原因:权值 w 的初始化用了 np.random.randn,没有固定随机种子。FNN 的训练是非凸优化问题,不同的初始点会收敛到不同的局部最优解。模型参数和评估指标每次都不一样,问题排查和结果汇报都没法稳定复现。
解决:在 FNN.py 开头、初始化之前写明 np.random.seed(42) 或任选一个固定整数。种子固定之后,同一份数据每次训练得到的 w 初始值一致,优化路径基本一致,输出结果可以复现。对于正式项目,我还会把随机种子连同 N_RULES、LR 一起记录在一个配置文本里,这样换人接手时能确定性地重跑实验。
5. 把误差指标联合起来读:MAE、MAPE 与差值分布图的判断习惯
5.1 三个指标配合起来看:MAE 看量级、MAPE 看相对、直方图看形态
MAE 给出的是绝对误差的平均水平,单位跟预测目标一致,能直观回答“预测平均偏差多少”。但只看 MAE 会掩盖一种情况:如果模型对多数样本预测得很好,只有少数样本偏差极大,MAE 仍然可能被这些离群点抬得偏高。MAPE 提供一个相对的视角,尤其适合在不同数据集或不同模型之间横向比较预测精度,但它的弱点在于对接近 0 的真实值非常敏感,这一层在避坑部分已经说过。差值分布的直方图补上了前两个指标看不到的信息——偏差的整体形态。对称分布集中在 0 附近说明模型没有系统性倾向;整体左移说明模型系统性低估;出现长尾说明存在少数预测得很差的样本,去看这些样本的特征,往往能发现训练数据里没有覆盖到的模式。
5.2 把 npy 参数当成模型产物,换数据复用
训练完成后真正需要保留的东西很少:c1.npy、b1.npy、w1.npy,外加一份 test.py 预测脚本。换到新的预测数据时,不需要重新训练,只要加载同一套参数做前向计算,前提是新数据的特征列和归一化方式与训练阶段一致。我在实际处理设备状态项目时,会把训练好的三个 npy 文件连同最小值和最大值这些缩放参数一起归档,在部署端只保留 test.py 和参数文件。每次部署前我会强制检查一次:加载参数后打印 c 的最大最小值,确认中心没有漂移到特征范围之外;打印 b 的最小值,确认宽度没有变成负数;再看一眼差值直方图的形态,确认预测偏差没有突然从对称变成偏斜。这套检查习惯帮我挡掉过好几回数据列顺序调错和参数文件覆盖的事故。希望帮到你。
本文还有配套的精品资源,点击获取