ML-For-Beginners 实战:使用支持向量回归(SVR)构建时间序列预测模型,并在新数据集上完成建模、评估与调参任务
【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners
本文是 Microsoft ML-For-Beginners 课程「Time Series Forecasting with Support Vector Regressor」(7-TimeSeries/3-SVR/README.md)的配套实战指南,围绕课程结课任务「A new SVR model」(translations/en/7-TimeSeries/3-SVR/assignment.md)展开。你将完整走通一条从数据加载、切分、缩放、时间步重塑到 SVR 训练与 MAPE 评估的流水线,并在此基础上学会如何用一份全新数据集独立重建模型、调节超参数与 timesteps,最终交付一份可打分的 notebook。读完本文,你不仅能复现课程中 MAPE 约 1.3% 的电力负荷预测结果,还能掌握一套可迁移到任意单变量时间序列的 SVR 建模方法论。
一、任务全景:assignment 究竟要求做什么
「A new SVR model」这份作业文档虽然篇幅不长,却是一份定义清晰的动手型任务,其核心要求如下(见 translations/en/7-TimeSeries/3-SVR/assignment.md):
- 用全新数据重建 SVR 模型:在已经跟随课程构建过一个 SVR 模型的基础上,选择一份新的时间序列数据集(例如高校公开的时间序列数据集),不能直接复用课程自带的
energy.csv; - 在 notebook 中完整记录工作:每一步都要有文字注释,说明为什么这么做;
- 可视化数据与模型:至少包含原始数据图、训练/测试划分图、训练集预测图与测试集预测图;
- 用合适的图形与 MAPE 评估精度:将预测曲线与实际曲线叠加绘制,并打印 MAPE 指标;
- 实验超参数与 timesteps:调整
gamma、C、epsilon等超参数,尝试不同的时间步timesteps,观察对测试集精度的影影。
该任务还附带一份评分量表(Rubric),是自我检查的硬标准:
| 标准 | 优秀(Exemplary) | 合格(Adequate) | 需改进(Needs Improvement) |
|---|---|---|---|
| 综合 | notebook 中构建、测试并解释了 SVR 模型,含可视化与明确的精度说明 | notebook 缺少注释或包含 bug | 提交的 notebook 不完整 |
任务文本说明其脱胎于 7-TimeSeries/2-ARIMA/assignment.md 的作业框架,因此完成前建议先掌握上一课 ARIMA 的数据准备流程。要完成这份作业,首先必须吃透课程讲义中"如何构建一个 SVR 模型"的完整代码路径,下面按流水线逐一展开。
二、SVR 为什么适合时间序列预测
在动手之前,先厘清三个关键概念(课程讲义 7-TimeSeries/3-SVR/README.md 的 Introduction 部分有系统阐述):
- 回归(Regression):有监督学习技术,从给定输入集合预测连续值,本质是在特征空间中拟合一条覆盖最多数据点的曲线(或直线);
- 支持向量机(SVM):一类用于分类、回归与异常检测的有监督模型,表现为特征空间中的一个超平面——分类时它充当决策边界,回归时它充当最佳拟合线;SVM 通常借助核函数(Kernel)把数据映射到更高维空间,使其更容易分离;
- 支持向量回归(SVR):SVM 家族中用于回归任务的分支,目标是找到包含最多数据点的最佳拟合线(在 SVM 语境下即超平面)。
为什么用 SVR 而非 ARIMA?上一课的 ARIMA 是统计学的经典线性方法,对时间序列预测非常成功;但现实中的时间序列普遍存在非线性,线性模型难以刻画。SVR 天然具备处理数据非线性关系的能力,因此成为时间序列预测中 ARIMA 的有力补充。这正是本课把 SVR 引入时间序列场景的根本动机。
三、运行环境与数据准备
3.1 两个 notebook 的定位
课程在 7-TimeSeries/3-SVR 目录下提供了两份 notebook:
- working/notebook.ipynb:练习版,关键实现处以
None占位(例如timesteps=None、model = None),供学习者自行填写; - solution/notebook.ipynb:完整答案版,包含所有实现与运行输出,可作为自检参考。
作业要求"用全新数据重建模型",实操中建议以 solution 版为模板,替换数据源后重新走通全流程。
3.2 依赖导入
课程数据加载与评估函数统一封装在 7-TimeSeries/common/utils.py 中,因此 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, mape3.3 数据加载的底层实现
load_data封装在 common/utils.py 中,其实现逻辑值得逐行理解:
def load_data(data_dir): """Load the GEFCom 2014 energy load data""" energy = pd.read_csv(os.path.join(data_dir, 'energy.csv'), parse_dates=['timestamp']) # Reindex the dataframe such that the dataframe has a record for every time point # between the minimum and maximum timestamp in the time series. This helps to # identify missing time periods in the data (there are none in this dataset). energy.index = energy['timestamp'] energy = energy.reindex(pd.date_range(min(energy['timestamp']), max(energy['timestamp']), freq='H')) energy = energy.drop('timestamp', axis=1) return energy关键点:
- 数据集来自GEFCom 2014 能源负荷数据(data/energy.csv),包含
timestamp、load、temp三列,小时级粒度,覆盖约 2012-01-01 至 2014-12-31; parse_dates=['timestamp']把时间列解析为datetime类型;reindex(pd.date_range(..., freq='H'))以小时为频率重建完整时间轴,用于暴露缺失时段(该数据集并无缺失,这是对数据完整性的一次显式校验);- 最后丢弃
timestamp列,让datetime索引成为 dataframe 的唯一索引。
调用与预览:
energy = load_data('../../data')[['load']] energy.head(5)load 2012-01-01 00:00:00 2698.0 2012-01-01 01:00:00 2558.0 2012-01-01 02:00:00 2444.0 2012-01-01 03:00:00 2402.0 2012-01-01 04:00:00 2403.0绘制 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()四、训练集与测试集划分:防止"未来信息泄漏"
时间序列建模与普通监督学习的切分有本质区别:测试集必须严格位于训练集之后,否则模型会从未来时间段"偷看"答案,造成虚假的高精度(即过拟合式错觉)。
课程采用如下切分策略(7-TimeSeries/3-SVR/README.md 的 Create training and testing datasets 一节):
- 训练集:2014-09-01 至 2014-10-31(两个月);
- 测试集:2014-11-01 至 2014-12-31(两个月,注意讲义代码中以
2014-12-30作为测试起始哨兵,实际切片时用>= test_start_dt取得整个 12 月)。
定义边界:
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()五、过滤与缩放:把数据投影到 (0, 1) 区间
SVR 对特征尺度非常敏感,不同量纲的特征会直接扭曲核函数计算的相似度,因此必须先缩放再训练。本课使用MinMaxScaler把负荷数据线性映射到 (0, 1) 区间。
按时间范围过滤出训练集与测试集:
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)对训练集执行fit_transform(拟合最小值/最大值并同时完成变换):
scaler = MinMaxScaler() train['load'] = scaler.fit_transform(train)对测试集只执行transform——这是极易踩坑的一步:缩放器必须沿用训练集上拟合出的 min/max,绝不能对测试集重新拟合,否则就泄漏了测试集的分布信息:
test['load'] = scaler.transform(test)六、时间步(timesteps)重塑:把序列变成监督样本
SVR 本身不"认识"时间,需要把单变量时间序列构造成[batch, timesteps]形式的二维输入:用前timesteps-1个时刻的值预测第timesteps个时刻的值。课程取timesteps = 5,即用前 4 小时预测第 5 小时。
先把 DataFrame 转成 numpy 数组:
# Converting to numpy arrays train_data = train.values test_data = test.values设定时间步:
timesteps=5用嵌套列表推导把序列切成长度为 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列为x,最后一列为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)形状变化的规律值得记牢:样本数 = 原始长度 − timesteps + 1(1416−5+1=1412,48−5+1=44),特征数 = timesteps − 1(4)。
七、构建并训练 SVR 模型
7.1 模型定义与超参数
课程选用RBF 核,并把gamma、C、epsilon分别设为 0.5、10、0.05:
model = SVR(kernel='rbf',gamma=0.5, C=10, epsilon = 0.05)fit之后打印的模型对象完整展示了全部默认参数(solution/notebook.ipynb 中的运行输出):
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)这些参数的含义与调参方向如下表:
| 参数 | 本课取值 | 作用 | 调参直觉 |
|---|---|---|---|
kernel | 'rbf' | 核函数类型,可选'linear'、'poly'、'rbf'、'sigmoid' | RBF 善于捕捉非线性关系;线性核适合近似线性序列 |
gamma | 0.5 | RBF 核的宽度系数,控制单个样本的影响力半径 | 过大易过拟合、过小易欠拟合,是调参重点 |
C | 10 | 正则化参数,权衡训练误差与模型复杂度 | C 越大越贴近训练点,但泛化风险升高 |
epsilon | 0.05 | 不敏感带宽度:带内误差不计入损失 | 越大模型越"宽松",可提高鲁棒性但降低贴合度 |
degree | 3(默认) | 仅对poly核有效 | 使用多项式核时调整 |
tol | 0.001(默认) | 迭代停止的容差 | 一般保持默认 |
cache_size | 200(默认) | 核矩阵缓存(MB) | 数据量大时可增大 |
7.2 拟合与预测
训练:
model.fit(x_train, y_train[:,0])预测训练集与测试集(reshape(-1,1)保证输出为列向量):
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
8.1 把数据还原到原始量纲
预测值是在 (0,1) 区间上产生的,评估前必须用同一个scaler做逆变换,才能与实际负荷(兆瓦级)对比:
# Scaling the predictions 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))# Scaling the original values y_train = scaler.inverse_transform(y_train) y_test = scaler.inverse_transform(y_test) print(len(y_train), len(y_test))8.2 对齐时间戳
由于第一个输出由前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 448.3 训练集评估
叠加绘制实际值(红)与预测值(蓝),并打印 MAPE:
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()print('MAPE for training data: ', mape(y_train_pred, y_train)*100, '%')MAPE for training data: 1.7195710200875551 %8.4 测试集评估
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 约 1.26%,说明模型在未见过的数据上表现良好。
8.5 全量数据集评估
最后把 2012-2014 年全部数据过一遍模型,检验整体拟合水平:
# Extracting load values as numpy array data = energy.copy().values # Scaling data = scaler.transform(data) # Transforming to 2D tensor as per model input requirement 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) # Selecting inputs and outputs from data 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)# Make model predictions Y_pred = model.predict(X).reshape(-1,1) # Inverse scale and reshape 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 %8.6 MAPE 的定义(源码级)
评估指标mape封装在 common/utils.py:
def mape(predictions, actuals): """Mean absolute percentage error""" predictions = np.array(predictions) actuals = np.array(actuals) return (np.absolute(predictions - actuals) / actuals).mean()即平均绝对百分比误差:先逐点计算|预测 − 实际| / 实际,再取均值。之所以在打印时乘以 100,是为了把小数换算成百分比读数。该指标无量纲、可跨数据集比较,是作业中判断模型精度的统一标尺。
九、完成「A new SVR model」作业的分步指引
理解了上述全流程后,按以下步骤在新数据集上独立完成任务:
步骤 1:选择新数据集。从课程建议的公开时间序列数据集(如高校维护的经典时间序列数据集合)中挑选一份单变量序列,可以是气温、流量、销量等任意连续量。注意数据集应具备足够长度(至少数百个时间点),以便切分后训练集仍有充足的滑动窗口样本。
步骤 2:搭建 notebook 骨架。以 working/notebook.ipynb 为模板(其中None占位即练习点),或直接参考 solution/notebook.ipynb 的完整实现,把数据路径替换为新数据,并为每个单元补充中文注释,说明"这一步做了什么、为什么"。
步骤 3:复用数据流水线。依次完成:加载数据 → 全量可视化 → 按时间顺序划分训练/测试集 →MinMaxScaler缩放 → timesteps 滑动窗口重塑。若新数据不是小时粒度,需同步调整pd.date_range的freq参数与时间切分边界。
步骤 4:训练与评估。用 RBF 核构建SVR模型,拟合后在训练集、测试集、全量数据三处分别绘制"实际 vs 预测"对比图并打印 MAPE。若新数据特征差异大,gamma=0.5, C=10, epsilon=0.05未必最优,需要进入下一步调参。
步骤 5:超参数实验。对gamma(如 0.1/0.5/1/5)、C(如 1/10/50/100)、epsilon(如 0.01/0.05/0.1)做网格式对比,以测试集 MAPE 最低为选优准则,在 notebook 中记录每组结果的表格或对比图。
步骤 6:timesteps 实验。把timesteps从 5 改为 3、7、12、24 等值,观察窗口长度对预测精度的影影:窗口太短捕捉不到周期,太长则引入噪声并显著减少样本数(样本数 = 长度 − timesteps + 1)。
步骤 7:对照 Rubric 自查。对照前文的评分量表逐项核验:模型是否构建、测试、解释齐全?可视化是否覆盖数据与预测?MAPE 是否明确陈述?注释是否完整、代码是否可运行?全部满足即为优秀(Exemplary)档。
十、进阶挑战与注意事项
- 换核函数:分别用
linear、poly、sigmoid核训练同一份数据,对比各自在测试集上的 MAPE 与训练耗时,理解核函数选择对非线性拟合能力的影响; - 警惕过拟合:训练集 MAPE(1.72%)与全量 MAPE(2.06%)均略高于测试集,说明模型在"见过的"数据上拟合更好,这是正常现象;若训练集误差远低于测试集误差,则应增大
C或减小gamma的正则化倾向需要反思; - 缩放的一致性:测试集只能用训练集拟合出的 scaler 做
transform,全量评估时也沿用同一 scaler,这是保证评估公正性的底线; - 数据粒度适配:本课数据为小时级,若新数据为日级或分钟级,
timesteps的语义(前 N 个小时/天/分钟)需随之调整,可视化图的尺寸也建议相应缩放。
通过本作业,你将真正掌握"数据清洗 → 缩放 → 滑动窗口构样 → SVR 训练 → 逆缩放评估 → 超参数搜索"这一完整闭环,这套方法可平滑迁移到能源、交通、零售等绝大多数单变量时间序列预测场景。
【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考