做负荷预测这行,干过一段时间的人基本都有个体会:模型决定下限,特征决定上限。这两年用LSTM做短期负荷预测的团队和个人越来越多,但很多人直接拉几十个特征一股脑扔进网络,训出来的模型要么泛化差,要么训练慢,结果还未必比传统方法好多少。问题通常不是LSTM不行,而是输入信息太杂。我现在做负荷预测项目,第一步永远是先用互信息(Mutual Information)做特征筛选,把真正和负荷强相关的变量挑出来,再交给LSTM去学时序规律。这套“基于互信息和LSTM深度学习”的流程,我在多个短期负荷预测场景里反复验证过,稳定性和可解释性都让人放心。这篇文章会把完整的思路、Matlab实现、参数配置和踩坑经验全部摊开讲,适合正在做毕设、论文复现,或者刚接触电力数据挖掘的读者。代码是通用模板,改改数据格式就能跑。
1. 为什么互信息+LSTM能打,先看这两个关键点
1.1 负荷预测的难点到底卡在哪
电力负荷序列不是一条平稳的直线。它带有明显的日周期性、周周期性,还会被天气、节假日、经济活动和一些偶发事件持续扰动。夏天温度一升,空调负荷跟着往上冲;工作日的峰谷形态和周末完全不一样;遇上重大活动和极端天气,负荷还会跳出历史模式。这种高波动、强周期、非平稳的特性,让预测任务非常棘手。
传统方法里,线性回归只能捕捉线性关系,碰到非线性关联就抓瞎;ARIMA虽然对时间序列友好,但面对强非平稳和长周期依赖也力不从心。LSTM这类深度学习模型能通过门控机制自动提取时序特征,但对输入特征的依赖比想象中更大。如果输入里塞满了冗余变量、噪声变量,模型不仅学得慢,还可能把无关模式当成规律学进去。所以负荷预测要做出效果,关键之一就是把“喂给模型什么”这个问题想清楚。
1.2 互信息:不只是相关系数的替代品
很多人做特征筛选,第一反应就是算皮尔逊相关系数。但相关系数本质衡量的是线性相关性,两个变量如果是曲线关系或者阈值关系,相关系数可能非常低,而实际上它们强相关。举个实际的例子:温度对负荷的影响,在供暖和制冷切换的临界温度附近往往呈现非线性甚至V型关系,这种情况用线性相关系数去度量就不太靠谱。
互信息定义是 I(X;Y) = Σ p(x,y) log[p(x,y) / (p(x)p(y))],衡量的是知道X之后,Y的不确定性减少了多少。用大白话讲:相关系数是在问“能不能用一条直线解释两个变量的关系”,互信息是在问“知道其中一个变量的取值后,另一个变量的答案还剩多少不确定性”。所以互信息对非线性关系更敏感,而这恰恰是负荷与温度、湿度、风速等气象因素之间最常见的关联形态。另外互信息不假设数据分布,直接基于概率分布计算,适应性很强,这是它比很多传统统计指标更适合做负荷特征选择的原因。
1.3 LSTM凭什么适合时间序列预测
RNN在处理短序列时还行,序列一长就容易出现梯度消失,靠前的历史信息传不到后面。LSTM通过遗忘门、输入门、输出门三个门控结构,让网络可以自主决定哪些历史信息要长期保留、哪些该丢弃,因此能捕捉长时间跨度的依赖关系。负荷预测里,“昨天此时段的负荷”对“今天此时段”影响很大,类似这种时间间隔较远的周期依赖,LSTM比普通RNN和传统模型都擅长学习。
Matlab的Deep Learning Toolbox对LSTM的封装很完善,不需要自己写反向传播和梯度更新逻辑,lstmLayer加trainNetwork就能搭出可用的模型。这也是我选择Matlab来完成这个流程的原因:代码短、调试直观,训练过程还能可视化。
2. 数据准备与特征工程:决定预测上限的核心环节
2.1 一份能用于训练的负荷数据集长什么样
做负荷预测,数据集至少应该包含时间戳、目标负荷以及一系列可能的影响因素。比较常见的特征包括温度、湿度、风速、气压、节假日标志、星期几等。除了原始气象特征,还要构造滞后特征,也就是把过去时刻的负荷值作为新特征加入,比如t-1时刻负荷、t-24时刻负荷。这样做是因为负荷自身的历史状态对当前状态有极强的影响。
| 时间 | 负荷(MW) | 温度(℃) | 湿度(%) | 风速(m/s) | 节假日 |
|---|---|---|---|---|---|
| 2024-07-01 00:00 | 582.3 | 26.5 | 78 | 2.1 | 0 |
| 2024-07-01 01:00 | 545.1 | 25.8 | 80 | 1.9 | 0 |
| 2024-07-01 02:00 | 512.7 | 25.1 | 82 | 1.6 | 0 |
实际项目中,数据往往不止这几个字段,还会包含光照、降雨量、当地经济活动指标等。但特征不是越多越好,这就要靠互信息来筛选。我常用的做法是先构造一个宽表,把所有候选特征都放进去,然后用互信息排序,再决定最终保留哪些。
2.2 数据清洗:没有干净的输入就没有可靠输出
LSTM是数据驱动模型,对异常值和缺失值非常敏感。缺失值处理相对简单,短时间段的缺失可以用线性插值填补;如果是长时间段连续缺失,我会倾向于直接舍弃该段,或者用前后相同周期的平均负荷做替换。异常值处理,我习惯用3σ原则:超过均值加减3倍标准差的点,判定为异常,用前后邻近值的平均值做平滑替换,或者做封顶处理。
这里要提醒一句:清洗不是越狠越好。负荷数据里有一些真实波动,比如电网故障、极端天气导致的负荷突增,这些数据虽然“异常”,但可能包含有效信息。到底替换还是保留,要结合数据含义做判断。我通常先画负荷曲线图,肉眼扫一遍整体形态,再决定清洗策略,而不是盲目套公式。
2.3 连续变量如何离散化才能算互信息
互信息的计算基于概率分布,连续变量不能直接套求和公式。常见做法是分箱,也就是把连续变量离散成若干个区间,然后统计联合分布。分箱方式有等宽分箱和等频分箱,我强烈推荐等频分箱,也就是让每个区间包含大致相同的样本数。原因很简单:等宽分箱对数据分布敏感,如果数据集中在某个区间,其他区间几乎没有样本,概率估计就会失真。
分箱数量也有讲究。样本量几千到几万条时,bins取15到20比较稳妥;样本量更大可以适当增加到30。有个经验公式是bins数大致取sqrt(n),但不绝对。bins太大会导致每个区间样本稀疏,概率估计抖动大;太小又会抹掉变量间的细微关系。工程上,我会在几个不同bins下分别计算互信息值,如果特征排名稳定,说明结果可靠;如果排名来回跳,那就需要调整离散化方式或者检查数据质量。
2.4 时序数据的划分与标准化要格外小心
时序预测的数据划分和普通机器学习完全不同。普通分类任务可以随机打乱样本,但负荷数据不能这么做,打乱等于把未来的信息混进训练集,模型学到的时序规律是假的,实际部署时会崩塌。正确做法是按时间顺序切分,比如前70%做训练,接下来15%做验证,最后15%做测试。
标准化也是这里最容易出问题的地方。zscore需要用训练段的均值和方法,再应用到验证段和测试段,不能拿全量数据的均值方差去标准化。原因很直接:测试段代表未来,未来数据的信息不应该参与模型准备的任何环节,否则就是数据泄漏,测试结果会被高估。我见过不少新手在这个细节上栽跟头,模型在验证集上各种完美,上线后一塌糊涂,查到最后都是标准化的时候偷看了全局统计量。
3. Matlab实操:互信息特征选择与LSTM建模全流程
3.1 手写一个互信息估计函数
Matlab的Statistics and Machine Learning Toolbox在部分版本里提供了mutualinfo函数,但不是所有版本都有,而且对连续变量的处理也不够透明。所以我习惯自己写一个基于等频分箱的互信息估计函数,代码完全可控。
function mi = calcMI(x, y, nbins) % 基于等频分箱的互信息估计 % x, y: 列向量; nbins: 分箱数 edgesX = [-inf, quantile(x, linspace(0, 1, nbins+1)), inf]; edgesY = [-inf, quantile(y, linspace(0, 1, nbins+1)), inf]; idxX = discretize(x, edgesX); idxY = discretize(y, edgesY); joint = accumarray([idxX(:), idxY(:)], 1); joint = joint / sum(joint(:)); margX = sum(joint, 2); margY = sum(joint, 1); mi = 0; for ii = 1:numel(joint) p = joint(ii); if p == 0 continue; end [a, b] = ind2sub(size(joint), ii); mi = mi + p * log(p / (margX(a) * margY(b))); end end这个函数核心思路是先等频分箱,再用accumarray构造联合概率表,最后对每个非零联合概率累加p * log(p/(px*py))。性能不算最优,但几万条数据跑下来完全没问题。如果你的Matlab版本比较新,直接用内置mutualinfo也没毛病,自己写的好处是逻辑清楚,出了问题能定位。
3.2 批量筛选特征并对结果排序
有了calcMI函数,批量计算每个候选特征与负荷目标之间的互信息就很简单了。
% 假设 dataTable 包含特征和负荷,第一列是负荷 features = dataTable{:, 2:end}; target = dataTable{:, 1}; featureNames = dataTable.Properties.VariableNames(2:end); nbins = 15; mi_scores = zeros(size(features, 2), 1); for i = 1:size(features, 2) mi_scores(i) = calcMI(features(:, i), target, nbins); end [mi_sorted, idx] = sort(mi_scores, 'descend'); resultTable = table(featureNames(idx)', mi_sorted, ... 'VariableNames', {'Feature', 'MI'}); disp(resultTable);输出就是一个按互信息值从高到低排好的表。我取特征的经验是选前5到8个,具体看总特征数量。一般情况下,负荷自身的滞后特征比如t-24负荷,互信息值会排在最前面,这个符合物理直觉。气象因素里,温度和负荷的互信息通常也很高。选完特征之后,还要看一眼剩余特征在业务上有没有必要性,比如电价、节假日这类变量即便互信息排名不是前几,有时也会因为实际业务需要而保留,互信息是辅助决策的工具,不是唯一标准。
3.3 LSTM网络结构与参数配置
特征选好了,接下来就是搭LSTM网络。Matlab的lstmLayer用起来很简单,但参数配不好,同样一堆问题。
numFeatures = numel(selectedIdx); numHiddenUnits = 100; layers = [ sequenceInputLayer(numFeatures) lstmLayer(numHiddenUnits, 'OutputMode', 'last') fullyConnectedLayer(1) regressionLayer ]; options = trainingOptions('adam', ... 'MaxEpochs', 150, ... 'MiniBatchSize', 64, ... 'InitialLearnRate', 0.005, ... 'GradientThreshold', 1, ... 'Shuffle', 'never', ... 'ValidationData', {XVal, YVal}, ... 'Verbose', 1, ... 'Plots', 'training-progress');逐个说参数。sequenceInputLayer(numFeatures)的numFeatures就是上一节选出来的特征数量。lstmLayer的隐藏单元数,我建议从100起步,数据量小就减到50,数据量大可以加到200。隐藏单元越多,模型表达能力越强,但也越容易过拟合、训练越慢。OutputMode设为last是因为我们做的是单步预测,只输出序列最后一个时刻的结果;如果你要预测一段连续序列,就改成sequence。
InitialLearnRate取0.005是一个比较稳的起点,太高损失容易震荡,太低训练半天不收敛。GradientThreshold设为1可以防止梯度爆炸,LSTM训练里偶尔会出现loss突然变成NaN的情况,这个参数能兜底。Shuffle我建议设成never,保持样本的时间顺序性。ValidationData用来实时观察验证集损失,防止过拟合。
3.4 数据切片、训练与滚动预测的完整流程
这里给出一套从数据预处理到预测评估的完整流程。首先是把数据整理成Matlab训练要求的格式:每个样本是一个numFeatures × lookback的矩阵,所有样本放在cell数组中。
lookback = 24; % 用过去24个时刻预测下一时刻 dataAll = [target, features]; % 第一列是负荷 dataAllNorm = dataAll; % 先完成标准化后再切片 % 生成输入输出序列样本 numSamples = size(dataAllNorm, 1) - lookback; XCell = cell(numSamples, 1); YSeq = zeros(numSamples, 1); for t = 1:numSamples XCell{t} = dataAllNorm(t:t+lookback-1, selectedIdx)'; YSeq(t) = dataAllNorm(t+lookback, 1); end % 按时间顺序划分训练集、验证集、测试集 trainNum = round(numSamples * 0.7); valNum = round(numSamples * 0.15); XTrain = XCell(1:trainNum); YTrain = YSeq(1:trainNum); XVal = XCell(trainNum+1:trainNum+valNum); YVal = YSeq(trainNum+1:trainNum+valNum); XTest = XCell(trainNum+valNum+1:end); YTest = YSeq(trainNum+valNum+1:end);这段代码注意几点:标准化要在切片之前完成,但标准化参数只能从训练段计算。实际操作中,我会先把dataAll按训练/验证/测试时间段切好,再用训练段的均值和方差去标准化所有数据,严格避免泄漏。代码里的selectedIdx是前面互信息筛选出来的特征索引。
训练和滚动预测:
net = trainNetwork(XTrain, YTrain, layers, options); % 滚动预测 net = resetState(net); YPredVal = zeros(size(YVal)); for i = 1:numel(YVal) [net, YPredVal(i)] = predictAndUpdateState(net, XVal{i}); end YPredTest = zeros(size(YTest)); for i = 1:numel(YTest) [net, YPredTest(i)] = predictAndUpdateState(net, XTest{i}); end % 反标准化 mu_load = mu(1); sigma_load = sigma(1); YPredTestDenorm = YPredTest * sigma_load + mu_load; YTestDenorm = YTest * sigma_load + mu_load;predictAndUpdateState是滚动预测的关键。每预测完一个时刻,网络内部状态会更新,保留对历史序列的记忆,这样预测更接近真实部署场景。如果只用predict,每个样本是独立预测,网络状态不会传递,效果会差不少。
模型训练好之后,用save('lstm_model.mat', 'net')保存,后续直接加载使用,不用每次都重新训练。
4. 怎么判断模型好不好:评估指标与结果分析
4.1 三个指标说明:RMSE、MAE、MAPE
模型训练完,评估是必不可少的环节。我用得最多的是RMSE、MAE和MAPE三个指标。
RMSE = sqrt(mean((yTrue - yPred).^2)); MAE = mean(abs(yTrue - yPred)); MAPE = mean(abs((yTrue - yPred) ./ yTrue)) * 100; fprintf('RMSE=%.4f, MAE=%.4f, MAPE=%.2f%%\n', RMSE, MAE, MAPE);RMSE对大误差更敏感,因为它先把误差平方再开方,一个特别大的预测偏差会把整体指标拉高,适合评估模型有没有出现离谱的预测。MAE是平均绝对误差,更稳定,不容易被个别极端值带偏。MAPE是百分比误差,行业里交流最常用,因为直观——误差占真实负荷的百分之几,一听就能理解。需要注意的是,如果负荷真实值接近0,MAPE会被异常放大,但电力负荷通常不会为0,所以这个顾虑一般不大。我出报告的时候通常三个指标都给,RMSE偏工程视角,MAPE偏业务视角。
4.2 预测曲线与误差展示
数值指标再好看,也不如一张图来得直观。我习惯把测试集的实际负荷和预测负荷画在同一条时间轴上。
figure; plot(yTrue, 'LineWidth', 1.5); hold on; plot(yPred, 'LineWidth', 1.5); legend('实际负荷', '预测负荷'); xlabel('时间点'); ylabel('负荷(MW)'); title('LSTM短期负荷预测结果');画完图之后,重点关注几个位置:每天的峰谷时段是否对得上,预测值是紧贴实际线还是明显平滑滞后,夜间低负荷时段误差如何。负荷预测里最常见的现象是预测曲线整体比实际曲线滞后一步,这个问题后面章节会专门讲怎么排查。如果只在峰谷处有少量偏差,整体趋势贴合,这个模型基本可以进入业务评估阶段了。
4.3 互信息选特征 vs 全部特征:一个对照实验思路
为了验证互信息特征选择到底有多大价值,我建议做一个简单对照实验:一组输入全部特征,另一组只输入互信息排名前5的特征,LSTM结构和训练参数完全一致。很多项目做下来,结果是精度基本持平,有时互信息组甚至略好,但训练时间明显缩短。这说明了互信息的一个核心价值——它不一定是让模型变聪明,而是让模型专心。特征减少之后,网络需要学的参数少了,噪声干扰少了,收敛更快,泛化也更稳。
还有一个隐含收益是异常鲁棒性。全特征模型里,某个无关特征只要在测试期出现异常波动,预测就可能被带偏;互信息筛选后的模型只依赖少数强相关特征,整体抗干扰能力强很多。为了结论更严谨,我还会加一组“随机选5个特征”作为对照。毕竟互信息排序和随机选特征的差距,才是特征选择方法真正贡献的部分。
4.4 与其他模型对比的结果呈现思路
论文里或者项目汇报里,通常还要求跟SVR、ARIMA等模型做对比。对比实验要注意三点:同一数据集、同一时段、同一指标。先跑一遍ARIMA和SVR,再跑LSTM,然后把RMSE、MAE、MAPE都放进一张表。
| 模型 | RMSE | MAE | MAPE |
|---|---|---|---|
| ARIMA | 待填 | 待填 | 待填 |
| SVR | 待填 | 待填 | 待填 |
| LSTM | 待填 | 待填 | 待填 |
LSTM在数据量充足、序列依赖明显的时候优势大;数据量少时则未必比SVR好。这个结果不丢人,反而说明方法论是科学的。如果LSTM表现不佳,优先怀疑特征工程和训练参数,而不是立刻换模型。
5. 常见问题、调试经验与避坑指南
5.1 训练损失不降或震荡
这个问题遇到太多了。第一反应先看学习率:0.01以上很容易震荡发散,降到0.005甚至0.001再试。第二看数据归一化:LSTM对输入尺度非常敏感,如果某个特征数值动辄上千,另一个只有0.几,训练基本不会稳定,必须做zscore或者归一化。第三看GradientThreshold:设成1能有效防止梯度爆炸。还有一种情况是MiniBatchSize太大,内存吃力也会导致loss跳,调小一半再试。
如果以上都调了还不收敛,检查一下数据里是不是有NaN。缺失值没有处理干净,损失计算就会出现NaN,训练曲线直接飞掉。这种情况在数据清洗环节就该拦住,但偶尔从外部读入的数据会带着不可见字符混进数值列,这种隐蔽问题用isnan(sum(data))扫一遍就能发现。
5.2 预测结果整体滞后一拍
这个现象在LSTM负荷预测里特别常见:预测曲线比实际曲线晚一个时间点,峰谷都对不上。本质原因是模型学到的更多是“用上一时刻的负荷推断当前时刻”,当历史惯性很强时,这种策略在训练集上损失很小,模型就躺平了。要缓解,最有效的方法是增加外部特征,尤其是未来时刻的气象预报值,这样模型不能只靠历史负荷混日子。另一个思路是构造差分序列,把预测目标从负荷本身改成负荷增量,削弱序列的自相关主导地位。还有一种办法是减少输入序列中最近时刻的滞后特征权重,逼着模型去学更长期的模式。
5.3 互信息排序不稳定
如果你在不同bins设置下跑出来的特征排名差别很大,先不要怀疑算法,大概率是数据分布问题。连续变量里如果有大量重复值,比如数据精度不够、很多样本取整后一模一样,等频分箱时大量样本会落到同一个区间,联合概率估计就失真。处理方法是在分箱前给特征加一点微小的随机噪声,打破重复值;或者改用基于KNN的互信息估计方法,对连续值的利用更充分。
样本量太小也会导致排序不稳定,互信息估计本质上依赖概率统计,几千条有效样本是底线。如果数据量确实少,不要硬选太多特征,top3到top5足够。
5.4 训练速度慢或内存不足
MaxEpochs设太多、隐藏单元太多、lookback太长,都会让训练时间长到怀疑人生。一个常见的折中方案是lookback从24减少到12或8,对大多数短期负荷预测场景,8到24的性能差距不大。隐藏单元从100降到50,训练时间差不多缩短四倍。如果你的机器有支持CUDA的显卡,把trainingOptions里的ExecutionEnvironment设成'auto',Matlab会自动调用GPU,速度提升非常明显。没有GPU也不代表不能做,减少数据量、减小miniBatchSize,CPU也能跑,就是时间要耐心等。
5.5 Matlab版本和工具箱问题
这套流程依赖Deep Learning Toolbox和Statistics and Machine Learning Toolbox。Matlab R2020b以上的版本跑起来基本没有障碍,更早的版本需要注意lstmLayer的接口细节和trainNetwork对cell数组输入的支持。mutualinfo函数不是所有版本都有,所以我自己写了一个calcMI,彻底回避版本兼容问题。如果你在自己的版本里遇到函数找不到的报错,优先检查工具箱是否安装完整,其次确认路径里没有自己的函数文件覆盖了内置函数名。
最后再分享一点个人习惯
拿到一个负荷预测项目,我不会急着上模型,而是先花一晚上把数据可视化一遍:负荷曲线、自相关图、特征散点图。互信息给出的是数值排序,但图能暴露很多代码看不见的细节,比如某个时段的数据明显异常、节假日切换处形态突变,这些都会直接影响预测效果。等特征选好、模型跑通,再回头调网络参数,效率高得多。这套“互信息筛特征+LSTM建模”的流程,我实践过多次,不只是负荷预测,光伏功率预测、风电预测、交通流量预测这类强周期性时间序列问题,都可以直接套用。最开始可以先从一份小数据、20个特征、一个LSTM层开始跑通全流程,再逐步加复杂度。相信你跑通第一次之后,就会理解为什么我强调特征工程的重要性了。