简介:面向地下水位预测与水资源管理建模需求,该下载包提供基于径向基神经网络的 MATLAB 实现,适合水文、环境及机器学习方向的研究者快速搭建预测模型。径向基网络以三层层级结构处理非线性映射,能够融合降雨、蒸发、地质结构等多因素数据进行学习,模型思路兼顾数据预处理、中心点聚类和权值调整等关键环节。压缩包内仅有 1 个 .m 功能脚本,体积约 1KB,代码简洁,便于直接查看、修改和嵌入到自有预测流程中。目前已有 382 人学习下载,对需要快速验证径向基算法在地下水位动态预测中效果,或将其作为课程设计与论文实验基础代码的读者,具有参考价值。脚本可作为算法骨架,帮助理解径向基网络的训练机制,并支持按自身数据扩展输入特征与评估指标,进一步优化预测精度。
1. 为什么地下水位预测值得用RBF,而不是LSTM或SVM
一口观测井的水位数据往往只有两三年、几百个日尺度样本,拿去做LSTM,训练集还没喂熟就开始过拟合;用SVM,核函数选错了又像玄学。径向基神经网络(RBF)恰好卡在这个尴尬区间里:它不需要长序列记忆,靠高斯径向基函数把输入映射到高维空间,再用最小二乘解一层线性权重,几个参数就能把短期水位趋势拟合得明明白白。对地下水位这种受降雨、蒸发、开采共同作用、且存在明显滞后响应的序列,RBF的小样本训练速度和可解释性,往往比它名字里的“神经网络”四个字更有价值。这篇笔记就从数据清洗、特征序列化、RBF训练、参数调整写到验证与避坑,目标是让你照着做就能跑通一版能用的水位预测。
2. 地下水位预测的输入怎么造:数据清洗、特征序列化与归一化
RBF网络对输入距离极度敏感,喂进去的数据脏一点,后面所有环节都会跟着失真。很多文章一上来就讲模型结构,实际上地下水位预测里,数据准备占掉七成工作量。这里说的准备不是简单读个csv,而是把观测井的日尺度水位序列处理成监督学习样本,并保证特征里没有未来信息。
2.1 先清洗再构造特征:水位序列的缺失值、异常值与滞后项
观测井数据最常见的三类问题:日期乱序、单日重复、连续缺测。先做排序去重,再用线性插值补缺。水文上水位日变化相对平缓,线性插值足够稳定,但连续缺测超过三天就要人工复核,不能盲目填。
import pandas as pd import numpy as np df = pd.read_csv("well_obs.csv", parse_dates=["date"]) df = df.sort_values("date").drop_duplicates("date").set_index("date") # 水位列做线性插值,允许向两端填充,保证序列头尾不丢 df["wl"] = df["wl"].interpolate(method="linear", limit_direction="both") # 降雨缺失按0处理;如果有蒸发、开采列,同理单独清洗 df["rain"] = df["rain"].fillna(0) # 滞后特征:含水层响应通常不是当天的,lag1抓短期惯性,lag7抓周尺度记忆 for lag in [1, 7]: df[f"lag{lag}"] = df["wl"].shift(lag) # 月份编码,给模型一个粗略的季节先验 df["month"] = df.index.month df = df.dropna()逻辑说明:drop_duplicates去掉重复观测,shift生成滞后列,让模型看到的永远是“过去的水位”。month列虽然是用月份数字直接编码,但对RBF这类基于距离的模型够用,因为水位季节形态主要受降水和蒸发驱动,月份本身只是粗粒度先验。
参数说明:lag取1和7是常见起点,不是唯一解。如果目标含水层是慢响应深层承压水,可以把lag加到15或30;如果是浅层潜水,lag3比lag1更稳。后续可以用验证集误差对比决定取舍,不要一开始就堆十几个滞后特征,径向基网络的输入维度越高,中心点覆盖难度越大。
2.2 滑动窗口切样本与归一化:切分顺序错了等于白干
构造目标列的方法是用shift(-7)把当前水位往后挪7天,也就是用今天及之前的信息预测7天后的水位。注意,这里目标列必须是wl_next7,不能把未来某天的真实水位直接并进特征表,否则就是数据泄漏。
from sklearn.preprocessing import MinMaxScaler FEATURES = ["wl", "lag1", "lag7", "rain", "month"] TARGET = "wl_next7" df[TARGET] = df["wl"].shift(-7) # 按时间顺序切分:前段训练,中间段验证,最后120天做测试 train = df.iloc[:-240] val = df.iloc[-240:-120] test = df.iloc[-120:] scaler_x = MinMaxScaler().fit(train[FEATURES]) scaler_y = MinMaxScaler().fit(train[[TARGET]]) X_train = scaler_x.transform(train[FEATURES]) y_train = scaler_y.transform(train[[TARGET]]).ravel() X_val = scaler_x.transform(val[FEATURES]) y_val = scaler_y.transform(val[[TARGET]]).ravel() X_test = scaler_x.transform(test[FEATURES]) y_test = scaler_y.transform(test[[TARGET]]).ravel()注意:
scaler_x只能对训练集fit,验证集和测试集只能transform。一旦对全量序列先做归一化再切分,验证集就偷看了未来水位分布,评测结果会虚高。
逻辑说明:MinMaxScaler把特征压缩到0-1区间,原因是RBF的高斯函数依赖欧氏距离,量纲不统一会让降雨量完全淹没水位变化。也可以换StandardScaler,但要保持输入输出一致,不能训练用了MinMax、预测时换别的。
参数说明:切分比例按时间顺序取前80%、后20%是保守做法。地下水数据经常只有几百条,不要用随机打乱再切分,那会破坏时间自相关性,验证结果会乐观到失真。目标列shift(-7)的7就是提前预测的天数,后面如果改成15天预报,这里同步改。
特征表如下:
| 特征 | 含义 | 对RBF的意义 |
|---|---|---|
| wl | 当前日水位 | 序列惯性输入,最重要 |
| lag1 | 前1日水位 | 短时惯性,承接昨天状态 |
| lag7 | 前7日水位 | 周周期记忆,抓固定周期波动 |
| rain | 当日降雨量 | 补给驱动,注意降雨对水位影响有延迟 |
| month | 月份编码 | 季节先验,辅助径向基区分不同水位形态 |
3. RBF神经网络的结构与训练原理:中心点、宽度、权重三件事
径向基神经网络的核心就三样东西:中心点、宽度、输出权重。搞清楚这三样,模型就不再是黑匣子。它的隐层神经元每个对应一个高斯径向基函数,输入样本越靠近某个中心点,该神经元响应越强;输出层把各神经元的响应线性加权,得到最终预测值。
3.1 一张图看懂RBF结构:输入层、径向基层、线性输出层
结构上,RBF是一个三层前馈网络。输入层不做变换,直接把特征向量送进隐层;隐层有n_centers个神经元,每个神经元的输出是输入样本到中心点的径向基函数值;输出层对这些值做线性加权求和。数学形式是:
y = Σ wj · exp(-‖x - cj‖² / (2σ²))
其中 cj 是第 j 个中心点,σ 是宽度,wj 是输出权重。高斯函数决定了每个神经元的响应范围:样本离中心越近,响应越大;离得越远,响应指数衰减。这就是RBF“局部逼近”的本质,和BP网络的全局逼近完全不同。
def gaussian_basis(x, center, spread): """单输入样本到单个中心点的高斯径向基响应""" diff = x - center return np.exp(-np.dot(diff, diff) / (2.0 * spread * spread))逻辑说明:np.dot(diff, diff)计算欧氏距离平方,spread控制衰减速度。预测时,输入向量先对每个中心点算一次这个函数,得到隐层输出向量,再和权重做点积。这个函数贯穿整个训练和预测过程。
选型理由:地下水位序列往往只有几百个样本,LSTM需要大规模数据才能发挥门控记忆优势,BP网络又容易在反向传播中陷入局部极小。RBF把非线性映射固定住,只学一层线性权重,参数少、训练快、小样本下更稳。如果数据量大到上万条,LSTM的优势才会明显,但绝大多数井点数据达不到这个量级。
3.2 两阶段训练:聚类定中心,最小二乘求权重
RBF训练不靠反向传播,而是两阶段完成。第一阶段确定中心点和宽度,第二阶段求输出权重。第一阶段常见做法是直接对所有训练样本做KMeans聚类,聚类中心就是径向基中心;宽度可以用“样本到所有中心的距离中位数”来定,避免拍脑袋。
from sklearn.cluster import KMeans def build_phi(X, centers, spread): """构造隐层输出矩阵 Phi,shape = (样本数, 中心数)""" phi = np.zeros((X.shape[0], centers.shape[0])) for j in range(centers.shape[0]): diff = X - centers[j] phi[:, j] = np.exp(-np.sum(diff * diff, axis=1) / (2.0 * spread * spread)) return phi kmeans = KMeans(n_clusters=12, random_state=42, n_init=10).fit(X_train) centers = kmeans.cluster_centers_ # 用样本到所有中心的距离中位数作为初始宽度,避免量级拍脑袋 spread = float(np.median([ np.linalg.norm(x - c) for x in X_train for c in centers ])) Phi_train = build_phi(X_train, centers, spread) # 第二阶段:对隐层输出做线性回归,直接最小二乘求权重 w = np.linalg.lstsq(Phi_train, y_train, rcond=None)[0]逻辑说明:build_phi把原始特征空间映射到径向基空间,在这个空间里目标值和输入的关系被近似为线性,所以np.linalg.lstsq一步就能求出权重。这比BP省掉整个反向传播过程,也基本不会撞上梯度消失。
参数说明:n_clusters=12表示隐层神经元数量。数据量小可以降到6,数据形态复杂可以加到20。n_init=10是KMeans的多次启动参数,用来缓解初始中心随机性,配合random_state保证实验可复现。这里的spread用的是距离中位数,只作起点,后面要结合验证集微调。
4. 用Python从零搭建径向基预测模型:代码框架与关键参数
原理清楚之后,代码就可以收敛成一条完整的pipeline。这一章给出一个能直接落地的完整流程,从训练到预测再到误差评估。会用到前面定义过的build_phi函数,主逻辑集中在rbf_predict里。
4.1 完整训练pipeline:从数据到RBF prediction结果
def rbf_predict(X_train, y_train, X_test, n_centers=12): """训练RBF并返回测试集预测结果(归一化空间)和spread""" kmeans = KMeans(n_clusters=n_centers, random_state=42, n_init=10).fit(X_train) centers = kmeans.cluster_centers_ spread = float(np.median([ np.linalg.norm(x - c) for x in X_train for c in centers ])) Phi_train = build_phi(X_train, centers, spread) w = np.linalg.lstsq(Phi_train, y_train, rcond=None)[0] Phi_test = build_phi(X_test, centers, spread) return Phi_test @ w, spread # 用验证集粗调中心数,选出合适的n_centers后再预测测试集 y_pred_norm, spread = rbf_predict(X_train, y_train, X_test, n_centers=12) # 反归一化回真实水位单位(米) y_pred = scaler_y.inverse_transform(y_pred_norm.reshape(-1, 1)).ravel() y_actual = scaler_y.inverse_transform(y_test.reshape(-1, 1)).ravel() mae = np.mean(np.abs(y_pred - y_actual)) rmse = np.sqrt(np.mean((y_pred - y_actual) ** 2)) print(f"MAE={mae:.3f} m, RMSE={rmse:.3f} m, spread={spread:.3f}")逻辑说明:rbf_predict内部做了四件事:聚类定中心,算spread,构造训练集的隐层输出矩阵并求权重,最后把测试集特征映射进同一组基函数并输出预测。反归一化是必须的,因为训练在0-1区间完成,直接输出的数值没有物理意义。
参数说明:n_centers=12是起步值。spread越大,每个基函数的影响范围越广,预测曲线越平滑;spread越小,局部响应越尖锐,预测曲线细节多但容易震荡。random_state=42保持KMeans初始化一致,否则每次跑出来的中心点不同,调参对比就失去意义。
4.2 三个必调参数:lookback、n_centers、spread
这三个参数决定了RBF在水位预测上的表现上限。lookback在数据准备阶段决定,n_centers和spread在训练阶段决定。下面用一个简单循环在验证集上对比不同中心数,避免用测试集反复调参导致过拟合。
| 参数 | 默认起步值 | 调参方向 | 说明 |
|---|---|---|---|
| lookback | 7天 | 按含水层响应速度设3/7/15/30 | 滞后水位特征,决定模型能看到多久的历史 |
| n_centers | 12 | 6~20逐步加,看验证误差拐点 | 太少欠拟合,太多过拟合 |
| spread | 距离中位数 | 以中位数为基准乘0.5/1/2/4 | 控制基函数重叠程度,影响平滑性 |
candidates = [6, 10, 15, 20] for n in candidates: pred_norm, spread = rbf_predict(X_train, y_train, X_val, n_centers=n) pred_val = scaler_y.inverse_transform(pred_norm.reshape(-1, 1)).ravel() mae_val = np.mean(np.abs(pred_val - scaler_y.inverse_transform(y_val.reshape(-1, 1)).ravel())) print(f"n_centers={n}, val_MAE={mae_val:.3f} m, spread={spread:.3f}")逻辑说明:验证集只用来选参数,选中之后再用测试集做最终评估。这个顺序是防止把测试集变成训练集的一部分,否则报告出来的误差会偏乐观。地下水数据预测里最常见的翻车,就是反复用同一份测试集调参,最后上线就废。
参数说明:如果你发现随着n增大验证误差先降后升,说明已经越过拟合拐点,回头选拐点前的位置。spread的搜索范围建议在“距离中位数×0.5”到“×4”之间,超出这个范围往往是过高或过低。
5. RBF地下水位预测的避坑清单:五个容易翻车的现场
这一章写的是我在实际做过水位预测之后沉淀下来的坑,每一条都按“现象 → 原因 → 解决”来梳理。有些坑不是RBF本身的问题,而是时间序列预测通用的数据泄漏,但在水位预测场景里特别隐蔽。
5.1 数据泄漏类的两个坑:未来水位混进特征,全量标准化
坑一:把未来水位并进特征。现象是验证集MAE接近0.01米,简直完美,但模型一上线就完全失真。原因是构造目标列时用了shift(-7),如果同时又把目标列本身留在特征里,RBF等于提前看到了答案。解决方法是严格区分特征列和目标列,特征只允许由当前及过去数据生成,构造完目标列后把dropna()之前的所有行彻底清掉,确认特征矩阵里不包含未来窗口的数值。
坑二:全量归一化后切分。现象是跨时间验证的曲线拟合得非常好,换到实时预测就崩。原因是MinMaxScaler对整个序列做了fit,训练集已经偷看了未来的最小值和最大值。解决方法是必须先按时间切分,再对训练集做fit,验证集和测试集只做transform。这一步是血泪经验,初学阶段几乎人人都会犯。
5.2 模型形态类的问题:spread失真与中心数走极端
坑三:spread设置失真。现象是spread过大时,预测曲线趋近于均值线,细节全部被抹平;spread过小,预测结果呈锯齿状,相邻两天跳动很大。原因是高斯宽度掌控了神经元响应的重叠范围,太大则每个神经元都在响应,太小则几乎没有重叠。解决方法是先用距离中位数打底,再按0.5倍、1倍、2倍、4倍做网格搜索,找验证集误差最低点。
坑四:n_centers走极端。现象是中心数设成3,预测曲线平滑得像条直线;设成30,训练集误差很小但测试集误差反弹。原因是基函数太少没法表达水位序列的多模态分布,基函数太多则每个中心只覆盖几个样本,把噪声也学进去了。解决方法是让中心数量与样本规模挂钩,几百条样本就从6起步,上千条可以到20,选验证误差的拐点位置。
5.3 工程部署类的坑:换一口井直接复用
坑五:一口井调好的模型换到邻井直接用。现象是训练井MAE只有0.15米,换井后MAE直接到0.6米以上。原因是不同井所在的含水层介质、补给来源、开采强度差异很大,数据分布根本不是一个空间。解决方法是按井单独建模,至少也要按含水层分组训练,用组内数据做迁移。如果不做这一步,模型的工程价值基本为零,这是我在实际项目中吃过亏的地方。
6. 结果验证与多尺度扩展:让RBF prediction结果经得起复核
6.1 时间顺序验证与持久性基线
水位预测里最容易被忽视的对比对象是持久性基线——也就是直接拿当前水位当作7天后的预测值。这个基线在短周期预测里非常强,很多模型跑出来的精度还不如它。
baseline = df["wl"].shift(-7).values[-120:] baseline_mae = np.mean(np.abs(baseline - y_actual)) print(f"Baseline MAE={baseline_mae:.3f} m")RBF的MAE必须明显低于这个基线才有工程意义。否则模型学到的只是惯性,而惯性用shift(-7)就能拿到,不需要任何训练。建议报告结果时把基线、RBF的MAE、RMSE放一起,另外计算纳什效率系数NSE,NSE大于0.5才认为模型勉强合格,大于0.7才值得部署。
6.2 时间序列分解与多尺度预测对比
股票价格预测领域最近流行先做时间序列分解、再做多尺度建模,这个思路套到地下水位预测上同样适用。浅层地下水位的季节性和趋势项很强,与其让RBF直接拟合原始序列,不如先用STL把水位拆成趋势、季节、残差三部分,对每个分量单独做RBF预测,再叠加还原。
from statsmodels.tsa.seasonal import STL stl = STL(df["wl"], period=365).fit() trend, seasonal, resid = stl.trend, stl.seasonal, stl.resid # 对 trend 和 resid 分别构造特征并训练RBF,seasonal 按周期外推趋势分量反映长期开采与补给变化,季节分量有固定周期,残差分量抖动较大。分开建模后,RBF不需要在同一组基函数里同时表达慢趋势和快波动,拟合压力小很多。实际操作时,先看分解后的残差是否平稳,如果残差还有明显趋势,说明分解参数需要调整。
最后分享一个教训:我第一次把RBF水位模型部署到生产环境时,只用了单井的15天数据,验证期表现不错,结果一换井点就露馅。后来老老实实把每口井单独建模,并用STL分解 + 多尺度对比验证,才把误差稳定在可接受范围。这个方向值得投入,但要清楚它的边界:数据量、井点特性、预测步长都决定模型上限,不要指望一个模型通吃所有井。希望帮到你。
本文还有配套的精品资源,点击获取