简介:一套基于支持向量机算法的降水量预测模型代码,适合气象、水文等领域研究者,以及希望掌握支持向量机回归流程的机器学习初学者。完整工程覆盖数据读取、缺失处理、核函数选择、惩罚系数与核参数调优、模型评估与预测输出等环节,可复现对历史降水数据的学习和未来降水量预测。压缩包大小282KB,共55个文件。核心为26个Matlab脚本,含多个主程序、自定义函数与扩展工具箱;另配有C/C++源文件、编译好的mexw32文件、说明文档、txt记录、wps笔记及makefile,便于在Matlab中直接运行,也便于查看算法底层实现并做二次开发。已有1917人学习下载。使用者可结合样例数据开展对比实验,观察不同核函数和参数设置对预测精度的影响;若正在做时间序列预测或支持向量机回归落地,这套代码提供的目录结构和函数注释也能成为可迁移的建模参考。
1. 降水预测用上SVM:先分清分类和回归再做模型
把“降水量预测”丢给支持向量机(SVM),最常踩的坑是拿分类的思维去套回归的问题。很多初版代码里,降水被贴成“有雨/无雨”的标签,SVM分类器在历史样本上准确率能到 90% 以上,一旦要输出具体毫米数就完全不能用。这份基于 SVM 的降水量预测模型,核心是把 SVR(Support Vector Regression)当作回归器来用,让算法拟合的是降水量序列的数值函数而不是类别边界。适合手里有站点观测数据、想把机器学习用进气象场景的开发者,也适合想理解 SVM 在连续值预测上怎么落地的读者。下面从原理、代码到调参完整过一遍能直接跑的方案。
2. SVM为什么能对降水量做预测:损失函数、核函数和标准化
2.1 分类SVM与回归SVR的差异:差在一条管道上
分类 SVM 做的事是找一个最大间隔超平面,把不同类别样本分开。回归版本的 SVR 思路完全不同:它寻找一个函数 f(x),让大部分样本的预测值与真实值之间的偏差落在 epsilon 范围内,超过这个范围的样本才被计入损失。这个“epsilon 不敏感”机制决定了 SVR 对小波动的降水噪声不敏感,不会因为某一天 0.1mm 和 0.3mm 的差异就剧烈调整模型。
SVM 的预测对象是连续降水量数值时,输入特征通常是历史降水、气温、湿度、气压等气象要素。SVR 输出的就是一个浮点数,代表下一时段降水量估计值。模型没有做任何分类降级,保留了原始量纲。
from sklearn.svm import SVR # 回归器实例化:三要素在构造时就定好 model = SVR( kernel='rbf', # 核函数类型,后面详述 C=1.0, # 正则化参数,越大越拟合训练集 epsilon=0.1, # 不敏感带的宽度,控制误差容忍度 gamma='scale' # rbf核的系数,scale根据特征标准差自动算 )C 值控制误分类惩罚力度,对回归任务可以理解为对超差样本的重视程度。C 过小模型欠拟合,容易把所有天的预测都拉向均值附近;C 过大则完全跟随训练集中的每一处波动,到新数据上反而误差更大。epsilon 直接影响预测结果的平滑程度,降水数据离散且非线性强,一般先设 0.1 再通过网格搜索微调。
2.2 核函数在气象时间序列场景下的选型
SVM 在降水预测里几乎都是非线性问题,线性核的前期处理成本太高,需要手工构造大量交互特征。常用核函数有三类值得对比:
| 核函数 | 适用场景 | 降水预测中的表现 |
|---|---|---|
| 线性核 | 特征维度高、样本量大 | 降水特征维度不高,线性核欠拟合常见 |
| 多项式核 | 特征间有明确交互 | 需要调 degree 和 coef0,训练慢,易过拟合 |
| RBF径向基核 | 非线性且无先验结构 | 降水非线性强,默认首选,泛化能力均衡 |
选择时优先 RBF 核。原因是降水序列受多个气象要素耦合影响,RBF 核可以隐式映射到高维空间,不需要人工猜测降水与气温、湿度的具体函数关系。gamma 参数控制单个样本的影响半径,gamma 越大决策边界越复杂。
# 对比不同核函数在同一份数据上的表现 from sklearn.svm import SVR from sklearn.metrics import mean_absolute_error for kernel_name in ['linear', 'poly', 'rbf']: if kernel_name == 'poly': model = SVR(kernel=kernel_name, degree=2, C=1.0, epsilon=0.1) else: model = SVR(kernel=kernel_name, C=1.0, epsilon=0.1) model.fit(X_train, y_train) pred = model.predict(X_test) print(kernel_name, round(mean_absolute_error(y_test, pred), 3))提示:多项式核在 sklearn 里不能和 gamma='scale' 同时使用,显式传了 gamma 就会在 poly 核上冲突。所以上面代码里 poly 分支不传 gamma。
实际跑下来 rbf 核的 MAE(平均绝对误差)通常是三项中最低的,尤其当样本量在几百到几千这个区间时,线性核很难捕捉到极端降水的长尾分布。
2.3 降水数据标准化:不同量纲对SVM的影响
SVM 依赖样本之间的距离计算,特征尺度不一致会让距离度量被量纲大的特征主导。降水量以大值主导,气温特征本身只有 30 以内的浮动,湿度特征是 0 到 100 的百分比。不标准化的后果是模型几乎只关注数值大的特征,核函数计算出的相似度全部失效。
from sklearn.preprocessing import StandardScaler # 初始化两个scaler,分别处理特征和目标值 scaler_X = StandardScaler() scaler_y = StandardScaler() # fit_transform在训练集上计算均值和标准差 X_train_scaled = scaler_X.fit_transform(X_train) y_train_scaled = scaler_y.fit_transform(y_train.reshape(-1, 1)).ravel() # transform只做变换,不重新计算统计量 X_test_scaled = scaler_X.transform(X_test) y_test_scaled = scaler_y.transform(y_test.reshape(-1, 1)).ravel()这段代码的关键在最后的预测阶段:模型输出的结果是标准化后的数值,要还原成真实降水量必须用 scaler_y.inverse_transform 反变换。很多初版代码漏掉这一步,拿到的预测值全部在 0 附近浮动的极小值。
最优做法是把 StandardScaler 放进 sklearn 的 Pipeline 里,这样交叉验证时不会发生数据泄露——标准化参数只在训练折上计算。
2.4 数据集怎么构造成机器学习能用的形状
原始气象站点数据是典型的表格结构,包含日期、站点号、降水量、气温、湿度等列。SVM 不直接吃时间序列,它要求输入形式为 (样本数, 特征数)。所以要先把时序数据转换成监督学习样本:用过去 n 个时刻的降水量加上其他气象要素构造一条特征向量,预测下一个时刻的降水量。
import pandas as pd import numpy as np def create_dataset(data, lookback=3): """ 将气象表格数据构造成监督学习格式 data: DataFrame,必须包含降水量列 lookback: 用过去几个时刻的特征预测下一个时刻 返回 X, y 两个数组 """ X, y = [], [] for i in range(len(data) - lookback): # 取出从 i 到 i+lookback-1 的多列特征 features = data.iloc[i:i + lookback].values.reshape(-1) # 目标值是第 i+lookback 行的降水量 target = data.iloc[i + lookback]['precipitation'] X.append(features) y.append(target) return np.array(X), np.array(y)lookback 窗口选的越大,特征维度越高,SVM 训练耗时按特征维度非线性增长。降水预测里常用 3 到 7 天的窗口,特征数等于窗口大小乘以气象要素个数。特征列选择上,优先保留历史降水量、湿度、气压差,这些变量与降水机制直接相关。
3. 写一套可运行的SVM降水量预测Python代码:从数据到模型
3.1 数据准备:模拟一份真实感的气象观测数据
没有现成数据文件时,用 numpy 模拟一份日尺度降水序列,结构上对齐真实气象站数据格式。模拟数据要保留降水的两个典型特征:大部分天接近 0,少部分天爆发式增长。
import numpy as np import pandas as pd np.random.seed(42) n = 500 # 模拟日期序列 dates = pd.date_range('2021-01-01', periods=n, freq='D') # 生成降水:指数分布模拟降水长尾特征,部分天叠加随机扰动 precipitation = np.random.exponential(scale=3.0, size=n).round(1) precipitation[precipitation < 0.2] = 0 # 生成辅助气象特征,与降水保持一定相关性 humidity = 60 + 20 * np.random.rand(n) + precipitation * 1.5 temperature = 15 + 10 * np.sin(np.arange(n) * (2 * np.pi / 365)) + np.random.randn(n) * 2 pressure = 1013 + np.random.randn(n) * 5 df = pd.DataFrame({ 'date': dates, 'precipitation': precipitation, 'humidity': humidity, 'temperature': temperature, 'pressure': pressure }) df.head()这段模拟数据里 np.random.exponential 是核心,指数分布产生的数值集中在 0 附近但存在长尾,和真实日降水量的概率分布相似。湿度特征与降水正相关,气温加了年周期正弦分量模拟季节变化,气压是随机波动加噪声。
3.2 训练SVR模型的最小代码路径
特征矩阵构造用上一章的 create_dataset 函数,注意要给特征列,不要只给降水量单一序列。真实项目里多变量特征对预测的提升远大于单序列加长窗口。
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.svm import SVR from sklearn.pipeline import make_pipeline # 选择参与预测的特征列 feature_columns = ['precipitation', 'humidity', 'temperature', 'pressure'] data_for_train = df[feature_columns].copy() # 构造监督学习样本,lookback=3 lookback = 3 X, y = create_dataset(data_for_train, lookback) # 切分训练集和测试集,时间序列数据不要随机打乱 split_idx = int(len(X) * 0.8) X_train, X_test = X[:split_idx], X[split_idx:] y_train, y_test = y[:split_idx], y[split_idx:] # 用Pipeline把标准化和SVR串起来,交叉验证时避免数据泄露 pipeline = make_pipeline( StandardScaler(), SVR(kernel='rbf', C=1.0, epsilon=0.1, gamma='scale') ) # 训练 pipeline.fit(X_train, y_train)训练测试集切分不能使用 train_test_split 的默认 shuffle=True,时间序列一旦随机打乱,模型会看到未来的数据,预测结果虚高。代码里手动用 split_idx 顺序切分,前 80% 训练,后 20% 测试,模拟真实环境下的滚动预测。
Pipeline 在这里解决了一个隐蔽问题:SVR 对特征尺度敏感,如果单独 scaler 后训练再单独 scaler 后预测,交叉验证时每一折都会用到全量数据的均值方差,造成数据泄露。make_pipeline 让每一折只在训练部分重算标准化参数。
3.3 预测和误差评估:输出降水量的具体数值
from sklearn.metrics import mean_absolute_error, mean_squared_error # 预测并在原尺度上评估 pred = pipeline.predict(X_test) mae = mean_absolute_error(y_test, pred) rmse = np.sqrt(mean_squared_error(y_test, pred)) print(f'MAE: {mae:.2f} mm') print(f'RMSE: {rmse:.2f} mm')RMSE 对极端降水值的误差更敏感,因为误差被平方放大了。降水行业更关注大雨和暴雨事件,所以 RMSE 会比 MAE 更受重视。初版模型的 RMSE 通常偏大,原因集中在特征太少和 C 参数没调好。
3.4 完整流程里的坑:数据泄露
数据泄露是时间序列机器学习里最隐蔽的问题。除了前面说的标准化泄露,还有一种常见错误:特征构造时用了未来信息。比如 create_dataset 中如果目标值列也被用作特征且没有错开时间步,模型等于直接偷看了答案。
# 错误示范:目标值参与特征构造且未错位 def bad_create_dataset(data, lookback=3): X, y = [], [] for i in range(len(data) - lookback): features = data.iloc[i:i + lookback].values.reshape(-1) # 问题:目标行的降水量在特征里已经出现 target = data.iloc[i + lookback]['precipitation'] X.append(features) y.append(target) return np.array(X), np.array(y)修正方式是在特征切片时去掉目标行的降水量列,或者像 3.1 节那样把特征列单独复制,确保特征矩阵里的降水数据全部来自 i 时刻之前。
4. 网格搜索调参和多步预测:把SVM预测精度往业务场景上拉
4.1 网格搜索找最优C、epsilon和gamma
默认参数 C=1.0、epsilon=0.1、gamma='scale' 能跑通流程,但达不到业务精度。降水序列的方差大,C 要给得更小一些抑制过拟合,epsilon 要给得更大一些容忍噪声。
from sklearn.model_selection import GridSearchCV # 定义参数网格 param_grid = { 'svr__C': [0.1, 1.0, 10], 'svr__epsilon': [0.01, 0.1, 0.5], 'svr__gamma': [0.01, 0.1, 'scale'] } # 注意:时间序列必须用带gap的划分方式,网格搜索内部默认K折不适合时序 from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) # Pipeline嵌套下参数名要带前缀 grid = GridSearchCV( pipeline, param_grid, cv=tscv, scoring='neg_mean_absolute_error', n_jobs=-1 ) grid.fit(X_train, y_train) print('Best params:', grid.best_params_) print('Best MAE:', -grid.best_score_)TimeSeriesSplit 做时间序列交叉验证时,训练集永远是验证集之前的数据,不会出现未来信息泄漏。这里不能使用默认 KFold,因为默认 KFold 是随机切分,降水数据里的季节性和趋势性会让随机折的结果虚高。
参数前缀 svr__ 是 sklearn 对 Pipeline 嵌套参数的固定写法,写到网格里时必须带上。长期改模型的人通常会维护一个参数记录表:
| 参数组合 | C | epsilon | gamma | 验证集MAE |
|---|---|---|---|---|
| 基线 | 1.0 | 0.1 | scale | 1.86 |
| 调C+epsilon | 0.1 | 0.5 | scale | 1.52 |
| 调gamma | 0.1 | 0.5 | 0.1 | 1.61 |
从这张表能看到规律:epsilon 调大对 MAE 的改善最明显,因为降水噪声大,给一个更宽的容忍带反而让模型的回归曲线更稳定。
4.2 多步预测:滚动预测和直接多步预测
业务上往往不只预测明天,而是预测未来 3 到 7 天的降水趋势。SVM 模型天然是一次性预测一个值,做多步预测有两种方案。
直接多步预测:训练多个模型,每个模型固定预测未来第 k 天。滚动预测:用预测出的某一天出力作为新特征,输入模型预测下一天。滚动预测的优势是只用维护一个模型,但误差会逐步累积,预测天数越多精度衰减越快。
# 滚动预测实现:用已预测值拼接历史窗口 def rolling_predict(model, last_window, steps=3): """ last_window: 最近的lookback条特征向量 steps: 预测未来几步 """ current = last_window.copy() predictions = [] for _ in range(steps): # 模型重新标准化在新窗口上 next_val = model.predict(current.reshape(1, -1))[0] predictions.append(next_val) # 窗口滚动:丢掉最早的位置 current = np.roll(current, -1) current[-1] = next_val return np.array(predictions)实际业务中建议两种方案都验证再选。降水数据的自相关性随时间快速衰减,滚动预测到第 3 步之后的精度提升已经不明显,此时直接多步预测的误差可能更可控。
4.3 业务视角的指标解释
回归指标在降水预测里要结合降雨量分级来解释才有意义。MAE = 1.5mm 在日降水量里算中等偏差,但假如把零降水日全部预测为零、有雨日全部预测为 0.5mm,模型业务价值就非常有限。
# 计算降水预测的命中率(预测方向和实际方向一致的比率) hit = np.mean((pred > 0.1) == (y_test > 0.1)) print(f'降水发生命中率: {hit:.2f}') # 计算极端降水(>25mm)的召回率 extreme_mask = y_test > 25 extreme_recall = np.mean(pred[extreme_mask] > 0.1) print(f'大雨日预测命中率: {extreme_recall:.2f}')这两段代码对应降水预测场景里的真实诉求。命中率衡量的是“方向对不对”,大雨日预测命中率衡量的是“极端天气能不能报出来”。SVM 作为核方法在大雨样本极少的情况下容易把大雨日也平滑成普通降水,此时要在训练时给大雨样本加权。
5. 源码实战里的三个核心排错技巧
5.1 scaler 反变换位置的Bug
SVR 模型在标准化空间上训练,预测完必须反变换回原始毫米数。常见错误是在 Pipeline 外单独标准化数据,预测结束只反变换了目标值,忽略了特征也需要同步。排查方法:输出 pred 的数值范围,如果预测值全部在 -0.5 到 0.5 之间,说明 Pipeline 内部的标准化没对齐。修正方式是用 scaler_y.inverse_transform(pred.reshape(-1, 1)).ravel() 还原真实降水量。
5.2 预测值收敛到常数的排查路径
模型输出基本恒定,比如所有预测值都在某个小数附近,原因一般有三个:C 值过小导致模型过于平滑、特征全部接近零方差、lookback 窗口过长特征维度爆炸。从数据下手顺序排查:先统计训练集 y 的均值方差,再输出模型 C 值,最后检查特征列是否存在全零列。经验上 epsilon 设置超过 y 标准差的一半就会出现严重欠拟合。
5.3 冷启动预测的处理
没有历史观测数据的新站点需要做预测,SVM 模型没有现成特征可用。常见做法是引入邻站数据作替代特征。将参考站点的气压、湿度、历史降水和本站少量观测拼接成特征向量,SVM 模型训练时就把邻站特征放入训练集,冷启动时邻站数据可以直接补齐。
# 冷启动场景:本站无历史,用邻站特征补位 # 特征列在训练和预测时必须保持完全一致 cold_start_features = { 'precipitation_neighbor': 1.2, # 邻站当日降水 'humidity_neighbor': 80.0, # 邻站湿度 'temperature_neighbor': 14.5, # 邻站温度 'pressure_neighbor': 1008.0 # 邻站气压 } # 特征值送入已训练模型之前必须做同样的标准化变换 new_sample = np.array(list(cold_start_features.values())).reshape(1, -1) new_pred = scaler_y.inverse_transform( best_model.predict(scaler_X.transform(new_sample)).reshape(-1, 1) ).ravel()这里注意:新样本特征向量进入模型前必须使用训练时保存的 scaler_X 做 transform,不能用新数据的均值重新 fit。冷启动方案在气象业务中非常常见,相当于用邻近站点的观测特征间接构建本站的历史上下文。
本文还有配套的精品资源,点击获取