用 SVR 支持向量回归做时间序列预测:ML-For-Beginners 电力负荷预测实战指南
SVR(Support Vector Regressor,支持向量回归)是把支持向量机从分类推广到回归的一类监督学习模型,擅长捕捉时间序列中线性模型难以刻画的变化规律。本文以 ML-For-Beginners 第 7 章第 3 课(7-TimeSeries/3-SVR/README.md,含阿拉伯语译版 translations/ar/7-TimeSeries/3-SVR/README.md)为核心,围绕真实的 GEFCom2014 电力负荷数据,完整演示"数据加载 → 训练/测试划分 → 归一化 → 时间步窗口化 → SVR 建模 → MAPE 评估"的全流程。读完你将掌握用 scikit-learn 的 SVR 拟合连续值时间序列、构造 [batch, timesteps] 监督样本,以及用 MAPE 与可视化验证预测精度的完整方法。
在课程体系里,7-TimeSeries/2-ARIMA/README.md 先介绍了 ARIMA 这一经典的统计线性方法;本课则回答一个问题:当数据中存在非线性时,如何改用支持向量回归来获得更好的预测效果。
预备知识:回归、SVM 与 SVR
在理解 SVR 之前,需要先厘清三个层层递进的概念:
- 回归(Regression):一种监督学习技术,用一组输入去预测连续值。核心思想是在特征空间中拟合一条经过尽可能多数据点的曲线(或直线)。它与分类的本质区别在于输出是连续数值而非离散类别。
- 支持向量机(SVM):一类可用于分类、回归与异常点检测的监督学习模型。模型在特征空间中表现为一个超平面——分类时它充当决策边界,回归时它充当最佳拟合线。通常还会引入 核函数(Kernel) 将数据变换到更高维空间,使原本难以分离的数据更容易被处理。
- 支持向量回归(SVR):SVM 家族中用于回归的分支,目标是找出一条能包含尽可能多数据点的最佳拟合线(在 SVM 语境下即超平面)。
为什么要用 SVR 而不是 ARIMA
上一课的 ARIMA 在预测时间序列上非常成功,但它本质上是一个统计线性方法。实际问题中,很多时间序列数据都带有**非线性(non-linearity)**特征,线性模型无法刻画这类结构。而 SVM 恰恰具备在回归任务中把非线性纳入考量的能力——这正是 SVR 在时间序列预测中取得成功的关键原因。
从仓库源码看,课程使用的数据是逐小时记录的电力负荷,天然带有日周期、周周期与季节波动等复杂形态(见下图 2012–2014 全量负荷曲线),非常适合用来对比"线性统计方法 vs 非线性核方法"的建模差异。
实战第一步:加载数据与引入依赖
课程配套的可执行 Notebook 位于 7-TimeSeries/3-SVR/working/notebook.ipynb(填空题形式的练习版),带完整答案与运行输出的是 7-TimeSeries/3-SVR/solution/notebook.ipynb。先运行 Notebook 并导入必要库:
import sys
sys.path.append('../../')
import os
import warnings
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import datetime as dt
import math
from sklearn.svm import SVR
from sklearn.preprocessing import MinMaxScaler
from common.utils import load_data, mape
随后从 /data/energy.csv 把数据读入 Pandas DataFrame:
energy = load_data('../../data')[['load']]
这里调用的 load_data 定义在 7-TimeSeries/common/utils.py 中:它读取 7-TimeSeries/data/energy.csv(GEFCom 2014 电力负荷数据,含 timestamp、load、temp 三列),把 timestamp 解析为时间并设为索引,再用 pd.date_range(..., freq='H') 按小时频率重索引整个序列,从而保证从 2012-01-01 到 2014-12-31 的每个整点都有一条记录(缺失的时间点会被显式暴露出来)。
把 2012 年 1 月到 2014 年 12 月全部负荷数据画出来观察:
energy.plot(y='load', subplots=True, figsize=(15, 8), fontsize=12)
plt.xlabel('timestamp', fontsize=12)
plt.ylabel('load', fontsize=12)
plt.show()
构造训练集与测试集
数据加载完成后,第一步是切分出训练集和测试集,再对数据做"时间步"重塑——这是 SVR 建模所必需的。模型在训练集上训练,训练结束后分别在训练集、测试集、全量数据集上评估精度以观察整体性能。
切分时有一个必须满足的约束:测试集在时间上必须晚于训练集,确保模型不会从未来时间段"偷看"信息(即避免 过拟合 / 数据泄漏 式的虚假高精度)。
train_start_dt = '2014-11-01 00:00:00'
test_start_dt = '2014-12-30 00:00:00'
把两段时间可视化对比:
energy[(energy.index < test_start_dt) & (energy.index >= train_start_dt)][['load']].rename(columns={'load':'train'}) \
.join(energy[test_start_dt:][['load']].rename(columns={'load':'test'}), how='outer') \
.plot(y=['train', 'test'], figsize=(15, 8), fontsize=12)
plt.xlabel('timestamp', fontsize=12)
plt.ylabel('load', fontsize=12)
plt.show()
训练前准备:过滤与缩放
训练前需要做两件事:过滤——只保留每个集合对应时间段与需要的 load 列;缩放——把数据投影到 [0, 1] 区间。SVR 依赖样本间距离/相似度,特征尺度差异过大会直接影响核函数计算,因此 Min-Max 归一化是必要步骤。
train = energy.copy()[(energy.index >= train_start_dt) & (energy.index < test_start_dt)][['load']]
test = energy.copy()[energy.index >= test_start_dt][['load']]
print('Training data shape: ', train.shape)
print('Test data shape: ', test.shape)
Training data shape: (1416, 1)
Test data shape: (48, 1)
注:训练集约 1416 个逐小时样本(2014-11-01 起),测试集为 48 个逐小时点(2014-12-30 起,接近两天)。原文档正文对月份的文字描述与代码注释略有出入,一切以代码与输出形状为准。
对训练集做 fit_transform 学到缩放参数,再用同一套参数对测试集做 transform(注意:不能对测试集单独 fit,否则会引入未来信息):
scaler = MinMaxScaler()
train['load'] = scaler.fit_transform(train)
test['load'] = scaler.transform(test)
构造带时间步的监督样本
对于 SVR,需要把输入数据变换成 [batch, timesteps] 的形式。也就是说,要为 train_data 和 test_data 引入一个代表"时间步"的新维度。
先转成 numpy 数组,并设定回看窗口 timesteps = 5:模型输入为前 4 个时间步的负荷值,输出为第 5 个时间步的值——这是一种典型的滑动窗口回归构造方式。
train_data = train.values
test_data = test.values
timesteps=5
用嵌套列表推导把训练数据转成二维张量:对每个位置 i 截取长度 5 的连续窗口,[:, :, 0] 负责去掉最后多余的单值维度:
train_data_timesteps=np.array([[j for j in train_data[i:i+timesteps]] for i in range(0,len(train_data)-timesteps+1)])[:,:,0]
train_data_timesteps.shape
(1412, 5)
测试数据同样处理:
test_data_timesteps=np.array([[j for j in test_data[i:i+timesteps]] for i in range(0,len(test_data)-timesteps+1)])[:,:,0]
test_data_timesteps.shape
(44, 5)
从窗口张量中切出输入与输出:[:, :timesteps-1] 取每个窗口的前 4 步作特征 x,[:,[timesteps-1]] 取末位作标签 y。
x_train, y_train = train_data_timesteps[:,:timesteps-1],train_data_timesteps[:,[timesteps-1]]
x_test, y_test = test_data_timesteps[:,:timesteps-1],test_data_timesteps[:,[timesteps-1]]
print(x_train.shape, y_train.shape)
print(x_test.shape, y_test.shape)
(1412, 4) (1412, 1)
(44, 4) (44, 1)
注意窗口滑动的后果:len(data) - timesteps + 1 决定了样本数——训练样本从 1416 减为 1412,测试样本从 48 减为 44,损失的数量恰为 timesteps - 1,这正是后续画图时时间戳要对齐偏移的原因。
实现 SVR 模型
scikit-learn 的 SVR 实现遵循固定三步流程:
- 定义模型:调用
SVR()并传入超参数kernel、gamma、C与epsilon; - 拟合模型:对训练数据调用
fit(); - 预测:调用
predict()。
本课使用 RBF 核(径向基核函数),超参数设为 gamma=0.5, C=10, epsilon=0.05:
model = SVR(kernel='rbf',gamma=0.5, C=10, epsilon = 0.05)
其中各超参数含义如下(也是 🚀 挑战 一节重点调优的对象):
| 超参数 | 本课取值 | 作用 |
|---|---|---|
kernel='rbf' |
径向基核 | 定义样本在特征空间中的相似度度量,适合非线性回归 |
gamma |
0.5 | RBF 核的带宽系数,控制单个训练样本的影响半径 |
C |
10 | 正则化系数,权衡拟合误差与模型复杂度(越大越允许少犯误差,也越易过拟合) |
epsilon |
0.05 | ε-不敏感损失带的宽度,带内误差不计入损失 |
在训练数据上拟合
model.fit(x_train, y_train[:,0])
SVR(C=10, cache_size=200, coef0=0.0, degree=3, epsilon=0.05, gamma=0.5,
kernel='rbf', max_iter=-1, shrinking=True, tol=0.001, verbose=False)
从打印出的完整超参数摘要可以看到大量默认值:cache_size=200(核缓存 MB)、degree=3 与 coef0=0.0(多项式核专用,RBF 下不生效)、max_iter=-1(不限迭代次数)、shrinking=True(启用启发式收缩)、tol=0.001(停止容差)。这些默认参数在调参时不必改动,重点观察 gamma / C / epsilon。
用模型做预测
y_train_pred = model.predict(x_train).reshape(-1,1)
y_test_pred = model.predict(x_test).reshape(-1,1)
print(y_train_pred.shape, y_test_pred.shape)
(1412, 1) (44, 1)
评估模型:反缩放 + 绘图 + MAPE
fit 与 predict 都是在 [0,1] 的缩放空间进行的,评估前必须先 inverse_transform 把预测值和真实值还原到原始负荷量纲:
# 反缩放预测值
y_train_pred = scaler.inverse_transform(y_train_pred)
y_test_pred = scaler.inverse_transform(y_test_pred)
print(len(y_train_pred), len(y_test_pred))
# 反缩放真实值
y_train = scaler.inverse_transform(y_train)
y_test = scaler.inverse_transform(y_test)
print(len(y_train), len(y_test))
提取用于 x 轴的时间戳。因为每个输出的前 timesteps-1 个值被用作首个输入的窗口内容,所以输出的时间戳要从 timesteps-1 之后开始:
train_timestamps = energy[(energy.index < test_start_dt) & (energy.index >= train_start_dt)].index[timesteps-1:]
test_timestamps = energy[test_start_dt:].index[timesteps-1:]
print(len(train_timestamps), len(test_timestamps))
1412 44
训练集与测试集上的表现
绘制训练集上真实值(红色)与预测值(蓝色)的时序对比:
plt.figure(figsize=(25,6))
plt.plot(train_timestamps, y_train, color = 'red', linewidth=2.0, alpha = 0.6)
plt.plot(train_timestamps, y_train_pred, color = 'blue', linewidth=0.8)
plt.legend(['Actual','Predicted'])
plt.xlabel('Timestamp')
plt.title("Training data prediction")
plt.show()
计算训练集 MAPE:
print('MAPE for training data: ', mape(y_train_pred, y_train)*100, '%')
MAPE for training data: 1.7195710200875551 %
再绘制测试集预测结果并计算 MAPE:
plt.figure(figsize=(10,3))
plt.plot(test_timestamps, y_test, color = 'red', linewidth=2.0, alpha = 0.6)
plt.plot(test_timestamps, y_test_pred, color = 'blue', linewidth=0.8)
plt.legend(['Actual','Predicted'])
plt.xlabel('Timestamp')
plt.show()
print('MAPE for testing data: ', mape(y_test_pred, y_test)*100, '%')
MAPE for testing data: 1.2623790187854018 %
🏆 测试集上取得了相当好的结果。
其中 mape(predictions, actuals) 定义于 7-TimeSeries/common/utils.py:即 mean( |pred - actual| / actual ),乘以 100 得到百分比形式的平均绝对百分比误差——它对量纲不敏感,是衡量负荷这类连续预测任务的常用指标。测试集 MAPE 约 1.26%、训练集约 1.72%,说明模型在未见过的未来数据上依然保持了很好的泛化精度,没有明显过拟合。
全量数据集上的表现
最后,把整个数据集(不只是训练窗口)送入模型,检验端到端的拟合能力。数据集共 26305 行(含表头),即 26304 个逐小时记录,因此窗口化后得到 26300 个样本:
# 提取 load 值为 numpy 数组
data = energy.copy().values
# 缩放
data = scaler.transform(data)
# 按模型输入要求转成二维张量
data_timesteps=np.array([[j for j in data[i:i+timesteps]] for i in range(0,len(data)-timesteps+1)])[:,:,0]
print("Tensor shape: ", data_timesteps.shape)
# 切分输入输出
X, Y = data_timesteps[:,:timesteps-1],data_timesteps[:,[timesteps-1]]
print("X shape: ", X.shape,"\nY shape: ", Y.shape)
Tensor shape: (26300, 5)
X shape: (26300, 4)
Y shape: (26300, 1)
# 模型预测
Y_pred = model.predict(X).reshape(-1,1)
# 反缩放并重塑
Y_pred = scaler.inverse_transform(Y_pred)
Y = scaler.inverse_transform(Y)
plt.figure(figsize=(30,8))
plt.plot(Y, color = 'red', linewidth=2.0, alpha = 0.6)
plt.plot(Y_pred, color = 'blue', linewidth=0.8)
plt.legend(['Actual','Predicted'])
plt.xlabel('Timestamp')
plt.show()
print('MAPE: ', mape(Y_pred, Y)*100, '%')
MAPE: 2.0572089029888656 %
🏆 图表非常漂亮,展示出一个精度良好的模型。全量数据 MAPE 约 2.06%。
值得说明的是,全量评估包含了训练窗口内外的所有样本,窗口内的数据模型在训练时已经见过,因此 2.06% 的整体 MAPE 主要反映模型对该负荷序列的整体刻画能力;而更严格的泛化证据应看测试集上约 1.26% 的结果。
🚀 挑战:调参与换核实验
按课程设计,学完基础流程后可尝试以下三个方向的扩展实验:
- 调整超参数:在创建模型时改动
gamma、C、epsilon,观察哪一组参数在测试数据上效果最好(可重点围绕 RBF 核的参数进行网格式搜索); - 更换核函数:尝试不同的 kernel(如
linear、poly、sigmoid),对比它们在该负荷数据集上的表现差异; - 改变窗口长度:尝试不同的
timesteps取值(即模型"回看"多少个历史点做预测),观察窗口大小对预测精度的影响。
完整的独立练习任务(用新数据集重新构建一个 SVR 模型并在 Notebook 中记录、可视化与评估)见 7-TimeSeries/3-SVR/assignment.md。
仓库配套资源索引
以下是复现与深入研究本文全部内容可直接使用的仓库文件:
- 课程正文(英文原版 / 阿拉伯语译版):7-TimeSeries/3-SVR/README.md、translations/ar/7-TimeSeries/3-SVR/README.md
- 练习 Notebook(填空版,随堂完成):7-TimeSeries/3-SVR/working/notebook.ipynb
- 参考答案 Notebook(含全部输出与图表):7-TimeSeries/3-SVR/solution/notebook.ipynb
- 通用工具函数
load_data/mape:查看 7-TimeSeries/common/utils.py - 实验数据 GEFCom2014 电力负荷:查看 7-TimeSeries/data/energy.csv
- 本节配图目录:7-TimeSeries/3-SVR/images/(
full-data.png、train-test.png、train-data-predict.png、test-data-predict.png、full-data-predict.png) - 前置课程(ARIMA 线性方法对比):7-TimeSeries/2-ARIMA/README.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


