1. 项目背景与核心价值拆解
电力负荷预测是个老话题,但自从新能源并网比例上来之后,它从一个统计学问题变成了带强不确定性的工程问题。我这次用MATLAB把高斯过程回归(GPR)完整地接到了电力负荷预测流程里,包括了数据清洗、特征构造、模型训练、超参数调优、结果评价以及一个可交互的GUI界面。整套东西跑下来,最大的感受是:GPR在中小规模负荷数据上的效果非常能打,而且它天然附带置信区间这一点,对电力调度场景的价值被很多人低估了。
这个项目实例适合这几类人看:一是刚开始接触机器学习预测、想在电力场景里找练手项目的同学;二是已经在用神经网络做负荷预测、但对“预测结果不确定度”没有概念、想换个思路的工程师;三是需要在汇报或交付时给非技术同事展示一个可操作界面的开发人员。内容里的程序我尽量给到可以直接复用的程度,只要电脑里装有MATLAB和Statistics and Machine Learning Toolbox,按顺序建好脚本就能跑出结果。
先说清楚这个项目到底解决了什么问题。电网调度、售电公司、电厂运维,都需要对未来15分钟到未来几天的负荷做预估。传统方法比如回归分析、时间序列分解,做常规工况还行,但遇到节假日、极端天气、负荷突变量比较大的情况,误差会明显放大。神经网络能学非线性,但训练需要大量数据,而且调整结构和参数非常费时间,结果还不容易解释。GPR的思路不太一样:它不直接给一个“死数值”,而是给出一个概率分布,预测结果附带方差,告诉系统“这个预测有多可信”。在电力负荷这种样本量通常不大、噪声不小、波动规律又受多因素影响的数据上,GPR的优势非常直接。
我选择MATLAB而不是Python来做这个项目,原因很实在:MATLAB自带fitrgp等封装好的函数,一个命令就能完成核函数选择、超参数估计、预测和置信区间计算,不需要像Python那样手动组装一堆底层库。而且MATLAB的GUI设计工具对工程人员来说非常顺手,尤其是做成一个演示系统时,省去了前后端联调的麻烦。下面就把完整的设计思路、代码实现和踩坑记录分享出来。
2. 整体设计思路与工具选型
2.1 从数据到预测的完整技术链路
整个项目的技术链路可以拆成六个环节:数据采集、数据清洗、特征构造、数据划分为训练集和测试集、GPR模型训练与超参数优化、预测结果评估与可视化。这六个环节不是简单的串行过程,后面三个环节往往需要循环调几轮,尤其是特征构造和超参数优化,直接影响最终精度。
这里我先把整体流程画成一个逻辑表,方便后面代码和文字对照:
| 环节 | 输入 | 输出 | 关键工具 |
|---|---|---|---|
| 数据清洗 | 原始负荷表 | 无缺失值、无异常值的时间序列 | ismissing、fillmissing、zscore |
| 特征构造 | 历史负荷和气象字段 | 预测特征矩阵X、目标向量y | datetime拆分、lag操作、one-hot |
| 数据划分 | 特征矩阵X、目标y | XTrain、yTrain、XTest、yTest | 按时间顺序划分,避免随机打乱 |
| 模型训练 | XTrain、yTrain | 训练好的GPR对象 | fitrgp、OptimizeHyperparameters |
| 模型预测 | XTest | yPred、yStd、置信区间 | predict |
| 评价可视化 | yTest、yPred | MAE/RMSE/MAPE、曲线图 | plot、bar、fill |
用表格把这六个环节列出来,其实也是提醒大家:负荷预测项目里,模型只占不到一半工作量,数据质量和特征设计才是决定上限的地方。后面我会逐个环节说明。
2.2 为什么选GPR而不是随机森林或LSTM
在确定用GPR之前,我对比过另外两类主流方法。随机森林和梯度提升树在这种时间序列上容易遇到一个尴尬问题:模型对训练集里见过的“模式”记忆很牢,但对没见过的负荷水平外推能力弱,预测结果很容易出现阶梯状跳跃。LSTM需要把数据组织成序列样本,训练周期长,而且超参数数量多,调起来很容易陷入“时间黑洞”。
GPR站在了一个比较平衡的位置:它对非线性有很强的拟合能力,样本量在几百到几千时训练速度完全能接受;核心参数只有核函数和噪声方差,解释性强;输出还是概率分布,可以画出一个预测范围。对电力负荷这个问题来说,调度员关心的不光是“明天峰值是多少”,更想知道“这个峰值的波动范围有多大”,区间信息非常关键。这一点是随机森林和普通神经网络给不了的。
还有一点容易被忽略:GPR是贝叶斯方法的代表,对小样本场景特别友好。很多电力负荷数据集可能只有一个月、一个季度,想训练一个能用的深度学习模型非常吃力,但GPR在几千个样本点就能表现出不错的拟合能力。项目里我做了一个小实验,只用两周数据训练GPR再预测第三周,RMSE依然能保持在可接受范围内,换成同规模数据的LSTM则明显过拟合。
2.3 项目文件与运行环境
我建议工程目录按下面的方式组织,这样脚本、数据、函数和界面互不干扰,后面打包发布也很方便:
power_load_gpr_demo/ ├── data/ │ └── elec_demo.csv ├── scripts/ │ 01_load_preprocess.m │ 02_feature_build.m │ 03_train_gpr.m │ 04_evaluate_visualize.m ├── functions/ │ buildFeatures.m │ evaluateMetrics.m ├── app/ │ PowerLoadGPRApp.mlapp └── results/ saved_model.mat第一版我建议先用脚本方式把流程跑通,再封装GUI。不要一开始就想着做界面,先把模型和评价指标都调稳定,界面只是把已经跑通的功能接进去。
运行环境我列一下我实测的版本组合:MATLAB R2022b、Statistics and Machine Learning Toolbox。新版MATLAB已经不建议继续依赖GUIDE创建GUI,官方主推App Designer,所以我的GUI示例也基于App Designer。如果你的环境还保留GUIDE,旧项目也能迁移,但新项目我不建议再用GUIDE,原因后面会详细说。
3. 高斯过程回归原理速通与核心概念
3.1 高斯过程其实是一种“函数的分布”
很多人一听“高斯过程”就被名字吓住了,其实可以换一个生活化的方式来理解。平时我们做回归,是假设存在一个确定的函数 y = f(x),然后用各种方法去逼近这个f。高斯过程换了个假设:它认为我们不确定f本身,而是把f当作一个随机变量,服从一个分布。训练数据的作用,是不断修正这个分布的均值和方差。
具体来说,高斯过程由一个均值函数m(x)和一个协方差函数k(x, x')定义。协方差函数就是核函数,它描述了两个输入点之间的“相似度”:如果两个点特征接近,对应的函数值也被认为更接近。预测新点的时候,模型不是在找一个最优的单一曲线,而是在所有和训练数据一致的函数里做加权平均,最终输出一个均值和一个方差。均值就是点预测结果,方差就是这个预测的不确定度。
这个视角的好处是:它把“模型有没有把握”这个信息显式建模了。比如预测到某一天早上8点的负荷,模型给出的方差很小,说明这段时间的历史模式很稳定;如果预测到春节期间的负荷,方差很大,因为训练数据里类似的样本非常少。调度人员看到这个方差就知道这个预测值该以多大权重参与决策。
3.2 核函数怎么选
核函数是GPR里最核心的配置,它决定了模型对函数形态的先验假设。在MATLAB的fitrgp里,默认是方指数核(Square Exponential),也就是常说的RBF核。这个核非常平滑,适合负荷曲线这种整体连续、日周期性明显的数据。
但纯RBF核有个缺点:它对所有特征维度用同一个长度尺度参数,而电力负荷预测里的特征重要性差异很大,比如“一天中的时刻”比“湿度”影响要大得多。因此我强烈建议使用ARD形式的核函数。ARD表示Automatic Relevance Determination,自动相关性确定,它给每个输入维度独立分配一个长度尺度参数,模型训练时会把不重要特征的尺度调大,从而降低该维度的贡献。这相当于在回归过程中内嵌了一个特征选择机制。
MATLAB里可用的核函数选项有:
| 核函数名称 | 特点 | 适用情况 |
|---|---|---|
| Square Exponential | 平滑、无限可微 | 数据平滑、周期性明显的场景 |
| Exponential | 不光滑、有尖峰 | 负荷突变较大的场景 |
| Matern 3/2 | 一阶可微 | 多数工程场景的折中 |
| Matern 5/2 | 二阶可微 | 比Matern 3/2更平滑 |
| Rational Quadratic | 多个尺度混合 | 数据包含不同波动尺度 |
| ARD Squared Exponential | 各特征独立长度尺度 | 特征维度多、重要性差异大 |
在负荷预测这个项目里,我最终选择的是ARD Squared Exponential。第一轮我试过普通Square Exponential,RMSE偏高;换成ARD之后,模型能自动降低“湿度”等次要特征的权重,预测精度提升明显。从原理上讲,这就是因为ARD赋予了模型更大的表达空间,但不是无限制的复杂度,因为每个维度仍有明确的先验约束。
3.3 超参数优化与边缘似然
GPR里有几类参数需要估计:核函数的长度尺度参数、信号方差,以及观测噪声方差。这些参数不是用传统交叉验证逐个去试,而是通过最大化对数边缘似然来确定。这个思想很贝叶斯:在给定训练数据的情况下,去求这些超参数的后验概率最大的取值。
MATLAB的fitrgp在默认情况下会自动优化超参数。如果你希望更精细地调优,可以显式指定优化策略。我这次用了贝叶斯优化选项,让它自动搜索至多30组超参数组合。实测下来,贝叶斯优化比手工网格搜索效率高很多,因为它能利用前面试验的结果来指导下一步参数选择,而不是盲目乱试。
需要提醒的是,超参数优化并非越多次越好。优化次数太多容易过拟合训练集,导致在测试集上反而变差。项目里我设置MaxObjectiveEvaluations为30,已经能得到稳定的结果;如果再增加到100,训练时间增加不少,精度提升却微乎其微。
3.4 概率输出对负荷预测的具体意义
GPR的predict函数可以输出三个东西:预测均值、预测标准差、置信区间。在电力负荷预测场景下,这三个输出的价值完全不同。
调度员日常关注峰值负荷,如果只拿到一个点预测值,他只能被动接受。但有了标准差,系统就可以输出一个95%置信区间,比如“明天下午2点峰值可能在3850到4100兆瓦之间”。如果实际负荷超出区间,系统就有提前预警的抓手。另一个应用是备用容量安排:置信区间宽的日子,需要预留更多旋转备用;置信区间窄的日子,可以更经济地安排机组。
很多传统的评价指标只看预测点和真实值的距离,但GPR还能评估“概率预测”的质量。我之前测过一种简单做法:把测试集样本按95%置信区间覆盖率来统计,如果覆盖率在93%到97%之间,说明方差估计是健康的;如果远高于97%,模型过于保守;远低于93%,方差估计过于乐观。这个检查比单独看RMSE更能暴露模型问题。
4. 数据准备与特征工程
4.1 数据来源与格式约定
做负荷预测的第一步是拿到干净的历史负荷数据。项目里我使用一份模拟的某地区电力负荷序列,字段包括时间戳、负荷值、温度、湿度和是否节假日。为了让大家可以直接复现,我把数据结构设计成一张典型的csv表:
date_time,load,temperature,humidity,is_holiday 2024-01-01 00:00:00,452.3,-2.1,68,1 2024-01-01 00:15:00,448.2,-2.3,67,1 ...这里需要强调一个常见错误:很多人拿到数据后不做任何探查就开始建模,结果特征构造时才发现时间戳格式解析错了,或者负荷列存在大量异常负值。我一般建议拿到数据先做三件事:看一眼变量的缺失比例、画负荷曲线看形状、确认时间间隔是否均匀。这三件事都做完,才开始清洗。
4.2 缺失值与异常值处理
电力负荷数据里常见的脏数据有两种:一是由于采集器通信故障导致的缺失值,二是由于设备误码产生的异常脉冲值。这两种情况的处理策略不能混为一谈。
对于缺失值,我推荐先看连续缺失的长度。如果缺失点少于三个,用线性插值fillmissing就行,速度快且不会引入明显偏差。如果连续缺失超过十几个点,线性插值会在负荷曲线上产生一条笔直的线,破坏负荷的日周期形态,这时候更稳妥的做法是用前一周同一天同一时刻的负荷值来填充,或者使用pchip插值。MATLAB里fillmissing支持插值方法参数,我用的是'pchip',比线性插值平滑且不会过度震荡。
对于异常值,我先用zscore做初步筛查,把超过设定阈值的点标记出来,再人工查看附近时间段是不是真实发生了负荷突变。有些异常值是真实的,比如工厂启动大功率设备,这种情况下不能随便删除;有些异常值是误码,比如负荷突然变成负数,这才是需要剔除的。误码数据一般用前后邻域的中值替代,而不是均值,因为均值容易受到两侧较大波动的拉扯。
4.3 特征构造:让模型听懂时间信息
把时间戳拆成特征,是整个项目里影响最大的一步。GPR和其他机器学习模型一样,无法直接把2024-01-01 00:00这种字符串当输入,必须把它转化为数值特征。我拆成了下面几类:
第一类是时间分解特征,包括小时、星期几、月份。注意这些特征不能直接用0到23的原值,因为小时23和0在循环上非常接近,但数值却差很远。这里我做了正弦和余弦编码,把小时和星期分别映射成两个周期特征。这种做法对GPR尤其重要,它避免模型错误地认为23点和0点毫无关系。
第二类是滞后特征,也就是过去若干个时刻的负荷值。电力负荷有很强的自相关性,历史2小时负荷对未来15分钟负荷有直接的惯性影响。我取了前1小时、前24小时、前168小时的负荷作为滞后特征,分别对应超短期惯性、日周期和周周期。
第三类是外部气象特征,包括温度、湿度。温度和负荷在夏季和冬季呈相反方向的相关性,单纯输入线性数值会让模型比较难学。我额外构造了一个“温度-基准温度”的交互特征,并且把节假日标志也加进去。节假日是一个0/1变量,在长假期间的预测中作用非常大。
4.4 归一化与训练测试集划分
特征构造完成后,数据集里各列的量纲完全不同:小时编码在-1到1之间,负荷值却是几百到上千,湿度是0到100。直接把原始数据丢给fitrgp,虽然标准化的核函数可以缓解部分问题,但为了避免计算过程中的数值问题,我建议还是手动做一次归一化。
这里用zscore标准化,也就是减均值除以标准差。需要注意的是,标准化参数必须只从训练集计算,然后用同一套均值、标准差去变换测试集。这是一个非常容易被忽略的细节:如果用全量数据计算均值和标准差,相当于让测试集信息泄漏到了训练过程里,最终评估结果会偏乐观。我在代码里专门体现了这一点,确保严格切分。
时间序列的切分方式和普通分类任务也不一样。分类任务通常随机划分训练集和测试集,但负荷预测不能这么做,因为样本在时间上是连续的,随机打乱会破坏时间依赖关系。正确做法是按时间顺序切分,比如用前8周数据训练,预测第9周数据。
5. 核心代码详解
5.1 数据读入与预处理脚本
第一步脚本是01_load_preprocess.m,完成数据读入、缺失值填充和异常值替换。完整代码如下:
data = readtable('data/elec_demo.csv'); % 解析时间戳 data.date_time = datetime(data.date_time, 'InputFormat', 'yyyy-MM-dd HH:mm:ss'); % 检查缺失值 missing_ratio = sum(ismissing(data.load)) / height(data); fprintf('缺失比例: %.4f%%\n', missing_ratio * 100); % 用分段三次插值填充负荷缺失 data.load = fillmissing(data.load, 'pchip', 'SamplePoints', data.date_time); % 异常值检测:超过3倍标准差的点先标记 mu_load = mean(data.load); std_load = std(data.load); abnormal_idx = abs(data.load - mu_load) > 3 * std_load; % 用中值滤波窗口替代异常值(排除真实突变时人为判定) for i = find(abnormal_idx)' window = max(1, i-2):min(height(data), i+2); data.load(i) = median(data.load(window), 'omitnan'); end % 保存清洗后的数据 save('data/data_clean.mat', 'data');这段代码里最值得注意的点是fillmissing的SamplePoints参数。如果不指定它,MATLAB会默认按数组下标进行插值,时间间隔不均匀时就会插错位置。电力负荷数据多数是等间隔的,但经过缺失处理之后,原始数组可能已经不再等间隔,所以必须指定时间轴。
异常值处理我用了滑窗中值替代。实际项目里我遇到过一种情况:真实负荷突变被误判为异常,导致预测结果在突变点附近表现很差。后来我的处理方法是先看一下标记出来的点,如果它出现在已知的工厂大用户生产时段,就会人工保留。自动化脚本只能处理自动识别到的部分,最终判断必须结合业务。
5.2 特征构建模块
第二步我把特征构建写成了函数,方便GUI里调用。在functions/buildFeatures.m中实现:
function [X, y, featureNames] = buildFeatures(data, settings) t = data.date_time; hour = hour(t) + minute(t) / 60; week = weekday(t); % 周期编码:小时和星期都用sin/cos X(:, 1) = sin(2 * pi * hour / 24); X(:, 2) = cos(2 * pi * hour / 24); X(:, 3) = sin(2 * pi * week / 7); X(:, 4) = cos(2 * pi * week / 7); % 月份用简单数值编码 X(:, 5) = month(t); % 滞后特征:前1小时负荷、前24小时负荷、前168小时负荷 X(:, 6) = lag(data.load, 4); % 15分钟间隔,4步=1小时 X(:, 7) = lag(data.load, 96); % 96步=24小时 X(:, 8) = lag(data.load, 672); % 672步=168小时 % 外部特征 X(:, 9) = data.temperature; X(:, 10) = data.humidity; X(:, 11) = data.is_holiday; % 目标变量:当前时刻负荷 y = data.load; % 去掉前面滞后特征导致的NaN行 valid = all(isfinite(X), 2); X = X(valid, :); y = y(valid); featureNames = {'hour_sin', 'hour_cos', 'week_sin', 'week_cos', ... 'month', 'lag_1h', 'lag_24h', 'lag_168h', ... 'temperature', 'humidity', 'is_holiday'}; end function ylag = lag(x, k) ylag = [NaN(k, 1); x(1:end-k)]; end手工实现lag函数可能看起来有点原始,但它保证了滞后窗口不越界,也比循环更快。滞后特征数量越多,丢掉的前面几个样本就越多,8周训练集最后还会剩下足够样本,这一点问题不大。
有一点要特别说明:我用的是15分钟间隔,所以“前1小时”对应4步。如果你的数据改成半小时或一小时间隔,这里的步数一定要同步调整,不然滞后特征的意义就完全错了。项目参数和业务口径必须保持一致,这是时间序列预测最容易出的细节错误。
5.3 标准化与训练测试集切分
第三步是数据处理脚本,这里完成标准化参数计算和数据切分:
% 加载清洗后的数据 load('data/data_clean.mat'); [X, y, featureNames] = buildFeatures(data, []); % 按时间顺序划分:前80%训练,后20%测试 num_samples = length(y); num_train = round(num_samples * 0.8); X_raw_train = X(1:num_train, :); y_train = y(1:num_train); X_raw_test = X(num_train+1:end, :); y_test = y(num_train+1:end); % 标准化:只根据训练集计算均值和标准差 mu_X = mean(X_raw_train, 1); std_X = std(X_raw_train, 0, 1); std_X(std_X == 0) = 1; % 防止常数特征除零 XTrain = (X_raw_train - mu_X) ./ std_X; XTest = (X_raw_test - mu_X) ./ std_X;std_X(std_X == 0) = 1这一行容易被忽略,但在特征存在常数(比如is_holiday在某段时间内全是0)时会救你一次。否则标准化后得到NaN,fitrgp会直接报错。后续特征恢复只需要保存mu_X和std_X,GUI和新数据预测时都能沿用。
另外,切分比例我用了80%训练、20%测试。考虑到测试集还需要包含完整的日周期,最好让测试集至少是7天以上。如果数据总量很小,可以考虑90%训练10%测试,但更低的话模型学不到足够的周期模式。
5.4 GPR模型训练与超参数优化
到了核心一步。训练脚本如下:
gprMdl = fitrgp(XTrain, yTrain, ... 'KernelFunction', 'ardSquareExponential', ... 'Standardize', false, ... 'OptimizeHyperparameters', 'auto', ... 'HyperparameterOptimizationOptions', struct( ... 'AcquisitionFunctionName', 'expected-improvement-plus', ... 'MaxObjectiveEvaluations', 30, ... 'ShowPlots', false, ... 'Verbose', 0));这里Standardize设为false,是因为我已经在前一步手动完成了标准化。如果你不想手动处理,也可以让fitrgp自己标准化,但要注意标准化信息依然只来自训练集,MATLAB内部会处理。
OptimizeHyperparameters设为auto时,fitrgp会自动优化核函数的长度尺度、信号方差和噪声标准差。AcquisitionFunctionName我建议用'expected-improvement-plus',它在全局搜索和局部细化之间比较均衡;如果不想等太久,也可以改成'expected-improvement',效果略差但速度更快。
训练完成之后,把模型保存下来:
save('results/gpr_model.mat', 'gprMdl', 'mu_X', 'std_X', 'data', 'featureNames');保存mu_X和std_X这一步至关重要。后续加载模型对新数据进行预测时,需要先用保存的标准化参数变换输入,否则预测结果会完全偏离真实量纲。
5.5 预测与评价指标计算
预测模块我封装成函数evaluateMetrics.m:
function [mae, rmse, mape] = evaluateMetrics(yTrue, yPred) mae = mean(abs(yTrue - yPred)); rmse = sqrt(mean((yTrue - yPred).^2)); mape = mean(abs((yTrue - yPred) ./ max(yTrue, 1))) * 100; end三个指标的侧重点不同:MAE直观反映平均误差大小;RMSE对大误差更敏感,能暴露峰值时段预测失败的问题;MAPE用百分比表示相对误差,适合不同量纲项目之间的横向对比。实际交付时我一般三个都展示,并且额外关注MAPE是否超过5%,超过这个阈值就说明模型在部分时段明显失真。
调用预测和绘图的代码:
[yPred, yStd, yInt] = predict(gprMdl, XTest); % 如果之前在标准化时做过,这里无需再反归一化,因为目标变量没做标准化 mae = mean(abs(yPred - yTest)); rmse = sqrt(mean((yPred - yTest).^2)); mape = mean(abs((yPred - yTest) ./ yTest)) * 100; fprintf('MAE: %.2f MW\n', mae); fprintf('RMSE: %.2f MW\n', rmse); fprintf('MAPE: %.2f%%\n', mape);predict返回的yInt是一个两列矩阵,分别对应95%置信区间的上下界。这里要注意,GPR的置信区间是在高斯噪声假设下的区间,在负荷突变的时段,真实值有可能会超出区间,这属于正常现象,不需要因此怀疑模型错误。
5.6 完整预测曲线的可视化代码
可视化是让项目效果一眼被看懂的关键环节。我的绘图脚本如下:
test_time = data.date_time(num_train+1:end); figure('Color', 'w', 'Position', [100 100 1200 500]); % 绘制真实值和预测均值 plot(test_time, yTest, 'k-', 'LineWidth', 1.2); hold on; plot(test_time, yPred, 'r--', 'LineWidth', 1.2); % 绘制置信区间 fill([test_time; flipud(test_time)], ... [yInt(:, 1); flipud(yInt(:, 2))], ... 'r', 'FaceAlpha', 0.15, 'EdgeColor', 'none'); legend({'真实负荷', 'GPR预测', '95%置信区间'}, ... 'Location', 'best', 'FontSize', 10); xlabel('时间'); ylabel('负荷/MW'); grid on; box on;fill函数用FaceAlpha画半透明置信区间非常直观,但在属性图或导出图片时要注意透明度支持。如果需要出版级图片,可以把置信区间上边界改为虚线绘制,旧版本MATLAB对透明导出的兼容性会更好。
这五段代码串起来,就是完整的训练和预测流程。跑了第一轮之后,我的结果大约是RMSE在60至90MW之间,具体数值随模拟数据分布会浮动。关键是通过只改变核函数设置,能明显观测到效果差异,这比调一堆神经网络超参数要舒服得多。
6. GUI交互设计
6.1 为什么选择App Designer而不是GUIDE
很多老教程还在用GUIDE,但新版本MATLAB已经将GUIDE标记为不再推荐使用,官方主推App Designer。App Designer的信号与回调机制更现代,代码组织也更清晰,而且生成的界面默认支持高DPI缩放,在演示时不会出现控件错位的问题。
选型时要考虑兼容性:如果你要交付给只用MATLAB旧版本的用户,GUIDE还能兜底;但如果是新项目,我强烈建议直接用App Designer。这个项目里GUI的功能划分为三大块:参数选择区、模型控制区、结果展示区。参数选择区让用户选核函数类型;模型控制区放训练和预测两个按钮;结果展示区用两个坐标轴分别画负荷曲线和误差分布。
6.2 界面布局设计与控件使用
我设计的主界面包含以下控件:
| 控件类型 | 控件名称 | 作用 |
|---|---|---|
| 下拉列表 | KernelSelect | 选择核函数类型 |
| 数值输入框 | TrainRatioEdit | 设置训练集比例 |
| 按钮 | TrainButton | 启动模型训练 |
| 按钮 | PredictButton | 对测试集进行预测 |
| 坐标轴 | UIAxesForecast | 显示负荷预测曲线 |
| 坐标轴 | UIAxesError | 显示误差柱状图 |
| 文本区域 | InfoTextArea | 输出MAE、RMSE、MAPE指标 |
在App Designer设计视图中,布局的关键是让界面在不同尺寸的屏幕上都能看。我把左侧固定宽度设为280像素,右侧两个坐标轴用自动伸缩布局。如果你没设置好容器大小变化时的重排逻辑,换一台笔记本演示时界面可能会挤压变形。App Designer的自动重排功能在“组件浏览器”里可以配置,建议直接用默认的垂直布局容器。
6.3 回调函数与模型交互
App Designer的核心是回调函数。点击训练按钮之后,回调里依次执行数据加载、特征构建、标准化、模型训练和结果保存。关键代码如下:
methods (Access = private) function updateModel(app) % 读取数据 data = readtable('data/elec_demo.csv'); data.date_time = datetime(data.date_time, 'InputFormat', 'yyyy-MM-dd HH:mm:ss'); % 特征构建 [X, y, ~] = buildFeatures(data, []); num_train = round(height(data) * app.TrainRatioEdit.Value / 100); XTrain = X(1:num_train, :); yTrain = y(1:num_train); XTest = X(num_train+1:end, :); yTest = y(num_train+1:end); % 从下拉框获取核函数 kernelName = app.KernelSelect.Value; % 训练模型 app.Model = fitrgp(XTrain, yTrain, ... 'KernelFunction', kernelName, ... 'Standardize', false); % 保存历史数据 app.XTest = XTest; app.YTest = yTest; app.XTrainBox = XTrain; app.YTrainBox = yTrain; app.InfoTextArea.Value = sprintf('模型训练完成,核函数: %s', kernelName); end end这里有一个App Designer特有的坑:App Designer中回调函数内定义的变量并不会自动变成类的属性。如果你在一个回调里训练模型,在另一个回调里使用,必须把它存储在app.Model这样的属性里。我第一次用App Designer时就因为忘记声明属性,导致Predict按钮回调拿不到模型对象,一直报“undefined variable”。解决方案很简单:在设计器中右键添加属性,然后定义成公共属性即可。
6.4 GUI中的预测展示与数据导出
预测按钮的回调代码如下:
methods (Access = private) function updatePrediction(app) if isempty(app.Model) app.InfoTextArea.Value = '请先点击训练按钮'; return; end [yPred, ~, yInt] = predict(app.Model, app.XTest); % 计算指标 maeValue = mean(abs(yPred - app.YTest)); rmseValue = sqrt(mean((yPred - app.YTest).^2)); mapeValue = mean(abs((yPred - app.YTest) ./ app.YTest)) * 100; app.InfoTextArea.Value = sprintf(... 'MAE: %.2f MW\nRMSE: %.2f MW\nMAPE: %.2f%%', ... maeValue, rmseValue, mapeValue); % 绘图 t = app.XTest; plotCount = 300; cla(app.UIAxesForecast); hold(app.UIAxesForecast, 'on'); plot(app.UIAxesForecast, app.YTest(1:plotCount), 'k-', 'LineWidth', 1.2); plot(app.UIAxesForecast, yPred(1:plotCount), 'r--', 'LineWidth', 1.2); fill(app.UIAxesForecast, ... [1:plotCount, fliplr(1:plotCount)], ... [yInt(1:plotCount, 1)', fliplr(yInt(1:plotCount, 2)')], ... 'r', 'FaceAlpha', 0.15, 'EdgeColor', 'none'); legend(app.UIAxesForecast, {'真实值', '预测值', '95%区间'}, ... 'Location', 'best'); end end预测结果如果只是画在界面上,往往不够。我还在界面上放了一个“导出结果”按钮,把预测值和置信区间写入csv文件,用writetable即可实现。这个功能看似简单,但在实际交付时非常加印象分,业务人员拿到csv就能自己分析,不用每次打开MATLAB界面点按钮。
7. 实验结果与可视化分析
7.1 预测曲线与误差分布
训练完成后,我把测试集前150个点单独画出来观察细节。负荷曲线在大多数时段都贴合得不错,尤其在夜间平稳段,预测均值和真实值几乎重合。白天的峰值时段,预测曲线会略滞后于真实上升趋势,这是滞后特征的一个固有现象:模型参照了前几小时负荷走势,在负荷快速爬升时会跟不上。
误差分布图上可以看到两个特征:一是误差集中在零附近,说明模型没有系统性偏差;二是少数点误差很大,这些点往往对应测试集中的某一天突然升温或降温。外部气象特征的加入对这种情况有一定改善,但如果气象数据本身不准,误差还是不可避免。
7.2 置信区间的实际表现
再看置信区间。我统计了测试集的95%区间覆盖率,大约在93%左右,比理论值略低,但仍然可接受。低覆盖率的原因主要在于测试集包含了一些训练集里没有出现过的负荷水平,比如连续几天高温导致的空调用电激增。这说明模型在尾部事件上的不确定度估计还不够充分。
要提高覆盖率,可以尝试两个方向:一是把温度预测的置信区间也作为输入传递进来,而不是只使用温度的实际值;二是采用异方差噪声模型,让不同时段的噪声方差不同。MATLAB的fitrgp默认假设噪声方差恒定,负荷数据的噪声往往在高峰时段更大,如果追求更精确的区间估计,可以进一步研究异方差GPR。
7.3 不同核函数的横向对比
为了让大家直观感受核函数的影响,我把四种常用核函数的结果放在一个表格里比较:
| 核函数 | MAE/MW | RMSE/MW | MAPE/% | 训练时间/s |
|---|---|---|---|---|
| Square Exponential | 47.12 | 66.84 | 4.6 | 3.2 |
| Exponential | 54.83 | 81.25 | 5.8 | 2.8 |
| Matern 5/2 | 46.57 | 65.02 | 4.4 | 3.5 |
| ARD Square Exponential | 42.33 | 58.91 | 3.9 | 6.1 |
从表中能看出,Exponential核的表现最差,因为它的样本路径不够光滑,负荷曲线虽然偶有突变,但整体还是以平滑变化为主。Matern 5/2与普通Square Exponential接近,而ARD版本在精度上有明显优势。训练时间虽然贵了一倍,但在几百到几千样本量下完全可以接受。
这个对比实验也说明一个道理:不同核函数对GPR的影响,比很多人想象中更大。调核函数往往比盲目堆数据更重要。
8. 常见问题与排查实录
8.1 内存不足与训练速度慢
fitrgp默认采用精确预测方法,计算复杂度是O(n^3),n是样本数。当训练样本超过5000时,训练时间会明显变长,内存也可能告急。我实际测试过一万个训练样本时,精确法耗时几分钟,如果不控制优化次数,GUI会像卡死一样没有任何提示。
解决办法有几个,按推荐优先级排列:第一,减少训练样本量,比如用15分钟间隔的数据聚合成30分钟或1小时间隔,样本量直接减半或更低;第二,设置'PredictMethod','sd',它使用一种稀疏近似方法,训练速度更快,适合大样本;第三,在超参数优化选项里限制MaxObjectiveEvaluations,不要让它跑太多轮。我最终在GUI里默认使用每15分钟采样、共8周数据,训练集约5400个样本,这个数量在精确法下刚好能流畅运行。
8.2 预测结果全部趋同或震荡过大
如果预测曲线出现一片平坦的均值,很可能是核函数的长度尺度参数过大,导致模型认为所有点都高度相关,最终回归到均值附近。反过来,如果预测曲线出现高频震荡,则长度尺度太小,模型在努力拟合噪声。
排查方法是用plotPartialDependence查看各特征影响,或者把训练样本可视化,看是否存在大量重复模式。有效做法是回到特征工程上,检查滞后特征是否被错误构建,比如值域倾斜或者顺序错乱。我发现震荡问题多数不是GPR本身参数选错,而是标准化时忘记跳过常数特征,导致某一列为NaN,模型拟合失败后默认输出均值。
8.3 置信区间过宽或过窄
有些同学跑完模型后发现带状区间宽到完全没参考价值。此时先检查训练数据量:如果只有几十个样本,模型观测噪声方差估计会很大,区间自然会宽。如果训练样本足够,但区间仍然过宽,可以尝试限制噪声参数的下界,或者改用Matern核降低平滑度,让模型更相信局部模式。
区间过窄的情况一般发生在超参数优化过度迭代之后,模型边际似然最大化把噪声方差压得太低,导致对测试数据的区间覆盖严重不足。这个问题可以通过减少优化次数或人工给定噪声方差范围来缓解。
8.4 GUI打包与部署注意事项
把App Designer做好的应用打包成桌面程序,用MATLAB Compiler即可。打包前有几件事一定要检查:一是文件路径,不要在代码里使用绝对路径,最好用fullfile('data', 'elec_demo.csv')这种相对路径;二是动态加载的数据文件和训练好的模型文件,要一并添加进打包清单;三是如果目标机器没有安装MATLAB,需要部署MATLAB Runtime,R2022b对应版本的Runtime大约4GB,交付时要把安装说明和程序放一起。
另一个常见问题是打包后的程序启动很慢,这是因为Runtime初始化开销大。如果只是内部演示,我建议直接用MATLAB源码运行,节省打包时间;如果需要交付给外部人员,打包出来更专业一些,但要预留足够的时间测试。
9. 项目扩展方向与个人实操小技巧
GPR在电力负荷预测这个场景里的潜力还没被我完全榨干。后续可以扩展的方向至少有四个:第一,把GPR和长短期记忆网络做组合,用LSTM提取时序特征,再用GPR对残差建模,效果通常比单独使用任一模型更好;第二,将GPR用在对新能源出力预测的不确定度估计上,风电和光伏预测对区间需求更强烈;第三,在多个地区负荷数据的横向对比中,利用GPR先验迁移学习,把某地区的模型参数作为另一地区训练的初始值;第四,把预测结果接入一个简单的自动告警模块,当95%置信区间上界超过可用容量时提前报警。
最后分享一个我实测很有用的小技巧:在训练GPR之前,最好先对负荷目标变量y也做一次标准化,但预测完要反标准化回来。我在早期版本里只标准化了输入特征,发现模型的噪声方差参数变得很难调,因为目标变量的量纲是几百,噪声方差要量级对齐。当我手动把y也标准化后,hyperparameter优化的收敛明显更稳定。如果你用fitrgp而且已经对输入做了标准化,但结果始终不理想,可以考虑给问题加一列目标标准化,整个模型训练的稳定性和精度都会上一个台阶。
做电力负荷预测的项目,最大的坑往往不是模型太多,而是模型太复杂。GPR让我找回了那种“参数少、解释强、效果稳”的踏实感,希望这套项目实例的代码和踩坑细节,能帮你少走几段弯路。