ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

模糊神经网络FNN用于数据预测:Python源码与实战(附数据集)

模糊神经网络FNN用于数据预测:Python源码与实战(附数据集)

简介:这是一套模糊神经网络FNN数据预测的Python源码与配套数据集,面向希望用智能算法完成回归预测任务的开发者、学习者,也适合作为模糊逻辑与神经网络交叉应用的入门实验。压缩包共7个文件,整体仅5KB,包含两个核心脚本:FNN.py读取train.csv训练集,通过反向传播算法自动优化模糊规则与隶属度函数,最终把中心点、宽度向量、权值等参数保存为.npy文件;test.py加载这些参数对test.csv测试集执行预测,输出MAE、MAPE等误差值及预测差值分布,帮助判断模型在不同样本上的误差模式,为后续优化提供依据。配套train.csv与test.csv构成完整训练-测试闭环,运行后可直接得到评估结果。FNN模型兼具模糊系统的可解释性和神经网络的学习能力,能处理真实数据中的不确定与非线性成分。已有382人学习,代码量小、结构清晰,不仅适合快速跑通FNN预测流程,还能在此基础之上扩展特征、调整隶属度函数或对比其他算法;无论是学术验证还是工程快速原型,都能借助这套代码搭建出可复用的预测基线。

1. 模糊神经网络 FNN 做数据预测,为什么值得自己用 Python 搭一套

做数据预测的工程师大多遇到过这种局面:传统 BP 神经网络拟合效果还行,但训完就是个黑匣子,领导问“为什么在这个点突然跳上去”,你只能拿测试集误差说话;换成纯模糊系统,每条规则都能解释,可隶属度参数全靠人工调,调三天也追不上非线性数据的真实走势。模糊神经网络(FNN)就是把这两者接到一起——用神经网络的反向传播去学习模糊规则里的隶属度参数,让模型既保留“如果输入偏高,则输出偏大”这类可读规则,又具备连续逼近任意非线性函数的能力。这次要聊的标题是“模糊神经网络(FNN)用于数据预测(Python源码+数据集)”,它面向的正是要做风速预测、电力负荷预测、设备退化趋势预测或金融时序预测的人。你不需要懂模糊数学的完备推导,但需要明白 FNN 的结构是怎么设计的、数据集怎么加工才不会让模型学到噪声、以及训练时哪些参数会让模型直接翻车。这篇就把这条路线从选型到落地拆开讲。

2. FNN 的结构设计与选型:模糊系统做骨架、神经网络做学习

2.1 五层结构里每一层在干什么

FNN 的典型结构是五层,第一层是输入层,节点数等于特征维度。第二层是模糊化层,每个输入变量被若干隶属度函数切分,比如把“温度”分成低、中、高三个模糊集合,常用高斯函数计算当前输入对每个集合的隶属度。第三层是规则层,每个节点代表一条模糊规则,把来自不同输入变量的隶属度取小或用乘积合成,得到这条规则的激活强度。第四层是归一化层,把各条规则的激活强度按总和归一,避免某些规则因输入范围不同而主导输出。第五层是输出层,对归一化后的激活强度做加权求和,得到最终预测值。

这套结构和 BP 网络的本质差异在于:中间层节点不再是无意义的加权和加激活函数,每个节点都被赋予了一个可解释的语义标签。训练前你就能告诉业务方,“这套模型里有 27 条规则,规则一的意思是温度低且湿度高时输出偏高”,训练只是把这些规则里“低”“高”的具体数值范围学得更准。这个特性在工业场景里非常值钱,因为预测结果要能解释,调试才有方向。

我习惯用高斯隶属度函数,公式是 exp(-(x-c)²/(2σ²)),其中 c 是中心,σ 是宽度。选高斯而不是三角或梯形,是因为它处处可导,反向传播时梯度计算干净,不会像梯形函数那样在拐点处出现导数不连续,训练容易震荡。如果你做的是分类任务,输出层换成 Softmax 即可,但这次聚焦回归预测,输出层用纯线性节点就够了。

2.2 三种 FNN 实现路线,怎么选

用 Python 搭 FNN 常见有三条路线。第一条是用 scikit-fuzzy 这类现成库搭模糊推理系统,再用网格搜索调隶属度参数,好处是代码量小,坏处是参数多时网格搜索跑不动。第二条是拿 Keras/PyTorch 自定义层,把模糊化层、规则层写成可训练的神经网络层,训练工具链成熟,但需要你理解自定义层的梯度传播,调试门槛略高。第三条是我最常用的方式——直接用 NumPy 从零实现一个精简 FNN,结构完全可控,参数更新逻辑透明,方便后面加正则项、早停甚至遗传算法优化,代码不过一两百行,维护成本并不高。

从零实现的意义不只是练手。现成库的模糊规则层往往不支持批量训练,或者归一化方式固定死,一旦你的数据分布特殊,想改一个激活强度的合成方式都要翻源码。自己实现的类可以随时加一个“规则权重衰减项”,或者把固定 σ 改成可训练变量,这些都是做项目时真正会遇到的定制需求。下面第 4 章给的就是这种实现。

2.3 规则数是 FNN 的参数天花板

规则数量由输入变量的模糊集合数相乘得到。两个输入变量各分三个集合,规则数就是 3×3=9;五个输入变量各分三个集合,规则数冲到 243,参数规模直接爆炸。这就是 FNN 最典型的“维度诅咒”。所以做数据预测时,输入特征不能一上来就塞十几个。我的经验是:先用相关性分析筛到 3~6 个核心特征,每个变量分 3~5 个模糊集合,把规则数控制在几十条以内。如果你想保留更多特征,就得用聚类初始化规则中心,或者用 T-S 模糊模型把每条规则的输出从常数改成线性函数,但这会把可解释性削弱一些,属于后话。

注意:规则数和预测精度不是单调正相关。规则太多会在训练集上拟合得异常光滑,测试集上一塌糊涂,这就是模糊系统版的过拟合。先定规则数,再调参数,不要两头同时动。

3. 数据集的构造与预处理:FNN 预测效果一半由数据决定

3.1 时间序列预测怎么组织输入输出

FNN 做数据预测,通常处理的是时间序列回归。原始数据是一列或多列按时间排序的观测值,不能直接扔进模型。常见做法是滑窗法:设定一个窗口长度 L,用前 L 个时刻的值预测第 L+1 个时刻的值。窗口每向后滑动一步,就生成一个训练样本。下面的代码演示了如何把一维序列转成监督学习格式。

import numpy as np def make_sequences(data, window=12, horizon=1): """ 将一维时间序列转为 FNN 训练用的监督样本。 data: 一维数组,按时间顺序排列 window: 用过去多少个点做输入 horizon: 预测未来第几个点,默认 1 表示预测下一个时刻 返回 X shape=(样本数, window), y shape=(样本数,) """ X, y = [], [] for i in range(len(data) - window - horizon + 1): X.append(data[i:i+window]) y.append(data[i+window+horizon-1]) return np.array(X), np.array(y)

这个函数里有两个参数要重点说明。window 决定模型看到多长的历史,风速、电价这类有周期性的数据,window 至少覆盖一个完整周期,比如日数据就设 24。horizon 是预测步长,多步预测有两种做法,一种是把 horizon 直接设成目标步数,另一种是滚动预测——用预测值当输入继续往后推。后者误差会累计,前三步还行,推到第十步基本不能看。我一般先把 horizon 设成 1,把单步预测做准了再考虑多步。

3.2 归一化的正确姿势

FNN 的隶属度函数用的高斯函数里指数部分对输入数值范围非常敏感。输入在 0~1 之间和高斯参数初始值匹配;输入是风速原始值,动辄 20 以上,指数部分直接溢出,所有隶属度都变成 0,梯度消失,训练几步就 NaN。所以必须先归一化。

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0.05, 0.95)) # 用训练集的均值和极值做归一化,测试集不能用自身统计量 scaled_train = scaler.fit_transform(train_raw.reshape(-1, 1)) scaled_test = scaler.transform(test_raw.reshape(-1, 1))

注意上面代码里的 fit_transform 和 transform 的区别:测试集必须复用训练集的缩放器。很多新手在这里犯错,对测试集再 fit 一次,导致训练集和测试集的数值范围不一致,模型预测结果看着误差很大,其实只是归一化尺度变了。另外 feature_range 我特意写成 0.05~0.95 而不是 0~1,是为了避免归一化后出现 0 和 1 这样的极端值,高斯函数的 exp 里输入为 0 时置信度最大、为 1 时可能落到两侧曲线末端,给隶属度函数一点缓冲空间。

数据集的选择上,公开的电力负荷、风速、帕德劳恩轴承故障数据集都可以用。只提醒一点,别拿鸢尾花这类静态分类数据硬套 FNN 做时序预测,它的样本之间没有时间依赖,滑窗出来的样本全是乱的。FNN 的预测价值在连续变化的过程数据上才体现得出来。

3.3 训练集测试集切分:时间序列禁止随机打乱

分类任务里把数据随机打乱再切 8:2 没问题,但时间序列预测如果也这么做,等于让模型从“未来”偷看了“过去”的模式。滑窗生成的相邻样本高度重叠,随机打乱后测试集里可能混入与某个训练样本只差一个时刻的数据,测试误差会严重失真。

正确切法是把滑窗后的样本按时间顺序直接切分,前 80% 做训练、后 20% 做测试。如果数据有季节性,比如一年 12 个月,要按完整周期切,别切到一半把冬天切进训练、夏天切进测试。还有一个进阶做法:切出一段连续验证集用于早停,验证集从训练集尾部切出或者从训练集中间隔取样,再或者用 walk-forward 方式滚动验证。三者我都在项目里用过,最省事的是在训练集尾部切最后 10% 做验证,但要注意这段验证集不能参与训练,否则早停失去意义。

4. 用 Python 从零实现 FNN 核心代码:训练循环、参数与梯度检查

4.1 FNN 类定义与正向传播

下面的类实现了三输入变量的 FNN,结构为输入层(3)—模糊化层(每个变量 3 个高斯函数)—规则层(27)—归一化—输出层。这里用 NumPy 实现,目的是把梯度逻辑亮出来,方便你根据自己的数据改结构。

import numpy as np class FNN: def __init__(self, n_features=3, n_mf=3, lr=0.01): self.n_features = n_features self.n_mf = n_mf self.n_rules = n_mf ** n_features self.lr = lr # 高斯隶属度参数:中心 c 和宽度 sigma self.c = np.random.uniform(-1, 1, (n_features, n_mf)) self.sigma = np.random.uniform(0.5, 1.5, (n_features, n_mf)) self.w = np.random.randn(self.n_rules) * 0.1 def forward(self, x): """ x: shape=(n_features,) 返回隶属度矩阵、各规则激活强度、归一化强度、预测值 """ # 第2层:高斯隶属度,输出 shape=(n_features, n_mf) mf = np.exp(-0.5 * ((x[:, None] - self.c) / self.sigma) ** 2) # 第3层:计算每条规则的激活强度(乘积合成) alpha = np.zeros(self.n_rules) idx = 0 for i in range(self.n_mf): for j in range(self.n_mf): for k in range(self.n_mf): alpha[idx] = mf[0, i] * mf[1, j] * mf[2, k] idx += 1 # 第4,5层:归一化+加权输出 alpha_sum = np.sum(alpha) + 1e-8 beta = alpha / alpha_sum y_pred = np.dot(beta, self.w) return mf, alpha, beta, y_pred

代码里的正向传播逻辑对应前面讲的五层结构。x[:, None] - self.c 利用广播机制一次性算出每个输入对每个模糊集合的距离,所以 mf 矩阵的第 i 行第 j 列表示第 i 个输入变量对第 j 个模糊集合的隶属度。三条规则嵌套循环把所有组合枚举一遍,规则总数是 3×3×3=27。最后归一化时我加了 1e-8 的极小项防止除零,这在训练后期某些规则激活强度趋近 0 时能避免 NaN,属于保命操作。

4.2 反向传播与训练循环

这个 FNN 的反向传播不需要像全连接网络那样推导复杂链式法则,因为输出层对规则参数只有两条导数路径:输出对权重 w 的导数是归一化强度 beta,以及 beta 对隶属度参数的导数。下面代码实现了完整的参数更新。

def train_step(self, x, y_true): mf, alpha, beta, y_pred = self.forward(x) error = y_pred - y_true # 输出层权重梯度 grad_w = beta * error # 计算 alpha 对 mf 的梯度(乘积规则的偏导) d_alpha = np.zeros((self.n_features, self.n_mf)) idx = 0 for i in range(self.n_mf): for j in range(self.n_mf): for k in range(self.n_mf): combos = [(0, i), (1, j), (2, k)] for feat, m_idx in combos: prod_others = 1.0 for f2, m2 in combos: if f2 != feat: prod_others *= mf[f2, m2] d_alpha[feat, m_idx] += prod_others idx += 1 # 链式法则计算 c 和 sigma 的梯度 alpha_sum = np.sum(alpha) + 1e-8 beta = alpha / alpha_sum d_beta = (d_alpha * alpha_sum - alpha * np.sum(d_alpha, axis=0)) / (alpha_sum ** 2) gauss_grad = mf * ((x[:, None] - self.c) / (self.sigma ** 2)) grad_c = -d_beta * error * gauss_grad * (1 / self.sigma) grad_sigma = -d_beta * error * gauss_grad * ((x[:, None] - self.c) ** 2 / (self.sigma ** 3)) # 更新参数,使用简单 SGD self.w -= self.lr * grad_w self.c -= self.lr * grad_c self.sigma -= self.lr * grad_sigma return float(error ** 2)

写这个反向传播时最重要的一步是 d_alpha 的推导。因为规则层用的是乘积运算,某条规则对第 i 个变量的第 j 个隶属度求偏导,等于这条规则里其他两个变量的隶属度相乘,与当前变量无关。combo 循环干的正是这件事。如果你把乘积改为取小运算,梯度就要改成指示函数,只在隶属度最小的那个变量上回传梯度,其他变量梯度为 0,这会让训练稀疏很多,收敛慢,所以我最终选了乘积。

训练时我一般跑 300~1000 个 epoch,学习率从 0.01 起步。FNN 的高斯函数对学习率非常敏感,超过 0.05 很容易震荡,低于 0.005 又太慢。如果你发现损失在初期下降后期开始上下跳动,先减学习率,不要调结构。

4.3 训练主循环与评估

def train_fnn(model, X_train, y_train, epochs=500): losses = [] for epoch in range(epochs): epoch_loss = 0.0 for x, y in zip(X_train, y_train): epoch_loss += model.train_step(x, y) losses.append(epoch_loss / len(X_train)) if epoch % 100 == 0: print(f"epoch {epoch}, loss {losses[-1]:.4f}") return losses # 假设 X_tr, y_tr 已经由滑窗函数生成 model = FNN(n_features=3, n_mf=3, lr=0.01) losses = train_fnn(model, X_tr, y_tr, epochs=800)

这个训练循环用的是纯样本逐个更新,而不是小批量。原因在于 FNN 的规则层参数不多,样本量在几千到几万条时纯 SGD 的收敛足够稳。如果你的数据量超过十万条,可以改成一次吃 32 个样本的 mini-batch 求平均梯度,代码改动不大。逐个更新的好处是每条样本立即修正参数,反向传播代码调试起来更直观。训练完成后,用测试集跑一遍 forward,把预测值用开头保存的 scaler 做 inverse_transform 还原成原始尺度,再算 MAE 和 RMSE。

5. FNN 训练中的踩坑与排查:5 个让我翻过车的问题

5.1 预测结果整体偏移一个常数,误差却很大

现象:测试集上画出预测曲线,形态和真实曲线几乎一样,但整体高出一截或低了一截,MAE 比预期大很多。

原因:归一化后训练没收敛到最优,输出层权重 w 的初始值设置不当。w 初始化为均值 0 的高斯分布,如果数据归一化后均值在 0.5 附近,模型初期输出接近 0,误差很大,如果训练 epochs 不足,来不及把 w 拉到位。

解决:把 w 初始化为 beta 的平均水平,或者在归一化时把目标值 y 也做 mean-std 标准化而不是 min-max。我更常用的是对 y 做均值方差标准化,这样模型初始输出 0 就对应均值,训练压力小很多。改完以后训练速度快了接近一半。

5.2 损失降到一定数值后纹丝不动

现象:训练到约 200 轮时,损失卡在某个值不再下降,调学习率也没明显变化。

原因:高斯隶属度函数的 σ 被更新到很小,所有规则对输入的响应都集中在极窄区间,模型退化成“对大部分输入输出相同值”。σ 在梯度更新里很容易被压在接近 0 的位置,因为梯度方向总在提高当前样本的隶属度峰值。

解决:给 σ 加下界限制,每次更新后做一次 clip,比如 σ 最小不低于 0.2。这个约束在物理意义上也成立,隶属度函数宽度太窄意味着变量的某个模糊集合只覆盖极少数样本,泛化能力必然差。常见做法是加一个简单正则项,loss 里加入 -λ·ln(σ),让 σ 别缩太狠,λ 取 1e-4 先试。

5.3 规则数爆炸导致训练极慢

现象:把窗口特征从 5 个增加到 8 个,每个变量 3 个集合,训练时间成倍增长,最后慢到不可接受。

原因:规则数是指数增长的。前面说过 5 个变量 3 集合是 243 条规则,8 个变量 3 集合是 6561 条,每条规则的激活强度都要在正向传播里计算,反向传播的 d_alpha 更是三重循环,计算量直接起飞。

解决:先用特征重要性筛选输入,把特征压到 3~4 个。如果确实要保留 8 个变量,把每个变量的模糊集合数从 3 降到 2,规则数变成 256,模型表达能力会降一些但训练速度回到正常。这个取舍很常见,做 FNN 预测本质上是拿特征数量换规则可解释性。

5.4 相邻样本高度重叠导致验证集虚高

现象:按时间切分后,验证集第一天的数据和训练集最后一天的输入只差一个时刻,重叠度极高,验证 MAE 非常漂亮,但换成全新时间段的数据预测误差立刻放大。

原因:滑窗法生成的相邻样本本身就存在信息重叠,时间切分没有从根本上消除这种泄漏,只是避免了随机打乱导致的跨越式泄漏。验证集与训练集紧密相连时,模型几乎是在“记住”最近的历史模式,而不是学到规律。

解决:切验证集时在时间上留出空隙,比如训练集取前 70%、验证集取后 15%、中间 15% 直接丢弃不用,或用 walk-forward 方式,用前 12 个月训练预测下 1 个月,滚动推进。我一般两个方法各跑一遍,对比结果差异,差异大到超过 20% 就说明模型学习的是近期记忆而非稳定规律。

5.5 高斯函数在反向传播里梯度消失

现象:训练几十轮后损失变成 NaN,打印隶属度矩阵发现全部为 0 或 1,梯度完全消失。

原因:输入值距离所有高斯中心都太远。比如高斯中心初始范围是 -1~1,而输入有个离群值达到 3,exp(-(3-0.5)²/2) 算出来几乎是 0,隶属度全为 0,alpha 为 0,beta 的除法直接出问题。这也是为什么必须先归一化再训练,且最好检查归一化后的数据分布有没有异常值。

解决:在 forward 里对 alpha 加入最大值保护,比如 alpha[alpha < 1e-6] = 1e-6。更根本的办法是在数据预处理阶段用分位数截断异常值,把极端离群值压到 [5%, 95%] 分位数之间。FNN 对异常值比树模型敏感得多,先修剪数据再进模型,不是可选步骤而是必做步骤。

注意:以上问题的排查顺序我通常固定为——先看归一化,再看规则数,最后调学习率。一半的“模型坏了”其实是数据没弄干净,不必先怀疑算法。

6. FNN 预测效果怎么验证:滚动预测、误差分解与基准对比

模型训练完不是画一条拟合曲线就交差。我做 FNN 预测项目必做三件验证:一是滚动预测稳定性测试,二是多步预测误差分解,三是跟传统基线模型对比。

滚动预测测试的方法是:取测试集最前面一个窗口做输入,预测下一时刻,再把这个预测值拼进输入窗口、丢掉窗口最旧的值,继续预测下一步。这样连续滚动预测 10~20 步,看误差随步长增加的趋势。如果单步误差很小、五步之后误差快速膨胀,说明模型依赖近期记忆,没有学到中期规律,此时可以增大窗口长度或加入外部特征改善。

误差分解是我自己的习惯——把 MAE 按不同数据区间拆开看。比如风速预测,把真实风速按 0~5、5~10、10~15、15 以上分段统计误差,经常发现模型在低风速段误差小、高风速段误差大。这说明隶属度函数集中在低值区间,高值区间规则覆盖不足。相应的调整是给高风速区间增加一个模糊集合,或在高值段增加训练样本权重。

最后是基准对比。纯 FNN 不一定是这个任务的最优解,我会至少跑一个 ARIMA 或带正则的线性回归做参照。用同一份滑窗数据、同一个测试集切分,比较 RMSE。FNN 比线性模型好 10% 以上,说明非线性建模有价值;如果差不多,那不如用简单模型,方便维护和解释。这个对比结果也是我写结论报告时最有说服力的部分。

最后说一个长期的教训:FNN 的参数初始化非常影响结果,同样的模型初始化不同,测试集误差波动有时能达到 20%。所以每次改数据或改结构后,我会固定随机种子,多组初始化各跑一遍取均值,再谈结论。不要被一次好运气的训练结果迷惑——这行当里很多“效果惊艳”其实是初始化的运气。测试三件套做全,结果稳了,模型才敢上线。希望帮到你。

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

返回列表