做能源管理这些年,我被问得最多的问题已经不是“今天用了多少电”,而是“明天大概用多少电、峰值落在几点、要不要提前做负荷响应”。我目前用的底层平台是开源能源管理系统 MyEMS,预测模型选的是 LSTM 神经网络,经过几轮迭代,连续三个月的日负荷预测精度稳定在 95% 左右(按 1 减去 MAPE 折算)。这篇文章不绕弯子,把我从数据清洗、特征工程、模型训练到部署进 MyEMS 的完整过程拆开讲,适合正在做电力负荷预测、能耗管理平台或者刚接触 LSTM 时间序列预测的工程师参考。
1. 先把“95%准确率”这个说法掰开:负荷预测到底在预测什么
1.1 我是在什么场景下碰到这个问题的
当时客户的园区里有十几块智能电表,通过 Modbus 协议接到 MyEMS,采集粒度是 15 分钟一条,一天下来每块表 96 个数据点。平台本身已经能看历史曲线、算能耗报告,但客户不满足于“回顾”,他们需要提前知道第二天的负荷情况,因为厂里打算做需求响应和购电计划。需求一旦明确,我面临的问题就变成:怎么用过去一段时间的历史数据,预测未来 24 小时甚至未来 7 天的负荷曲线。
负荷预测本质上是时间序列预测,预测对象是未来若干个时刻的功率值。比如给定过去 168 个小时的负荷,预测未来 24 个小时的负荷;或者给定过去一周的 15 分钟级数据,预测未来一天 96 个点。这个任务看着简单,真正做起来坑很多:负荷曲线受工作日、休息日、天气、生产工艺、设备启停影响,不是一条平稳的曲线。单纯算个平均值,或者用昨天的数据平移过来,误差会非常大。
我把目标定为短期负荷预测,预测周期是未来 24 小时,刷新频率每天一次。为什么选这个周期?因为客户的需求响应窗口、购电计划、设备预冷/预热都需要提前一天安排,再短的预测对调度的意义不大,再长的预测可靠性又不够。后面所有模型设计、特征选择、评估口径,都是围绕“提前 24 小时预测”这个场景展开的。
1.2 为什么是 LSTM,而不是 ARIMA 或 SVR
决定用 LSTM 之前,我先试过传统时间序列方法。ARIMA 对平稳序列效果不错,但负荷数据有明显的周周期性:今天下午两点的负荷,跟昨天同一时刻相关,跟一周前同一时刻也相关。这种多周期叠加,ARIMA 处理起来要不断差分、识别阶数,非常繁琐,而且一旦遇到节假日,模型基本失效。
SVR(支持向量回归)我也跑过,核函数选择、参数调优成本高,对缺失值敏感,输入多个特征时容易过拟合。相比之下,LSTM 这类循环神经网络天然适合序列数据。它的门控机制可以选择性记住长期信息:既能记住一天前的同时刻负荷,也能捕捉最近几个小时的突变趋势。对负荷预测这种“日周期 + 周周期 + 外部因素”叠加的信号,LSTM 的结构匹配度确实更高。
另一个考虑是扩展性。数据源从一块表扩展到几十块表,特征从负荷扩展到温度、湿度、节假日、排产计划时,LSTM 可以在不变更整体架构的情况下继续接入,传统统计模型往往要重新设计。当然,LSTM 不是万能药,后面我会专门说哪些场景其实不值得用它。
1.3 95% 准确率到底怎么算的
先说清楚口径,不然这个数字很容易被挑战。回归问题严格来说没有“准确率”这种说法,常见指标是 MAPE(平均绝对百分比误差)、RMSE(均方根误差)和 R²。我这里提到的 95% 准确率,实际是“日预测准确率”,计算公式是:
预测准确率 = (1 - MAPE) × 100%
MAPE = (1 / n) × Σ |实际值 - 预测值| / 实际值 × 100%
举个例子:某时刻实际负荷 1000 kW,预测 950 kW,那么这个时刻的绝对百分比误差是 5%,对应的准确率是 95%。一天 24 个点或 96 个点平均下来,得到当天的预测准确率。我在园区项目里连续跑了三个月,非极端天气日平均准确率稳定在 95% 左右。
但只看 MAPE 有个隐患:夜间负荷基数小,比如实际 100 kW,预测 80 kW,绝对偏差只有 20 kW,百分比误差却达到 20%,会拖累整体 MAPE;白天高峰段实际 1000 kW,预测 900 kW,绝对偏差 100 kW,百分比误差只有 10%。所以我在调优阶段除了看 MAPE,还额外看峰值时段的绝对误差,避免模型为了把均值做漂亮而牺牲高峰预测能力。后文提到的所有优化,也都是围绕“降低 MAPE 且不让峰值误差失控”来做的。
2. 数据底子决定模型上限:从 MyEMS 到训练集
2.1 MyEMS 里能挖到什么数据
MyEMS 这类开源能源管理平台,核心工作是数据采集、存储和展示。我这边部署的版本使用 MySQL 保存采集数据,电表通过 Modbus 等协议接入,平台按照点位配置定时读取数据。实际建模时,我主要用了两类数据:
- 模拟量:电压、电流、有功功率、无功功率等瞬时值
- 电能量:有功电度等累计值
有功功率是预测的直接目标。不过 MyEMS 默认表结构里,模拟量和电能量是分开存储的,建模前需要先把瞬时功率按时间对齐,重采样成统一的时间序列。
我做的第一个决定是把 15 分钟数据聚合成小时级数据。原因有三点:第一,客户调度决策不需要分钟级曲线,小时级足够;第二,小时级数据对缺失值和通讯抖动的容忍度更高,模型更稳定;第三,模型输入输出维度变小,训练和推理速度都快很多。如果你想做更细的日内峰值控制,可以保留 15 分钟级单独建模,但那需要更高质量的数据和更复杂的特征,前期不建议一上来就啃硬骨头。
2.2 清洗三步:零负值、缺失、尖峰
原始数据直接进模型肯定不行。我整理数据时固定做三步清洗,每一步都有明确理由。
第一步是剔除零值和负值。电表断电、通讯中断、设备检修时,数据库中可能会出现 0 或负的功率读数。但这里的“剔除”不是物理删除,而是先标记出来,根据前后数据判断是真实停机还是异常值。如果前后几十个点都接近 0,那是正常停机;如果前后负荷都很高,中间突然出现 0,那就是采集问题,需要修正。
第二步是缺失值填充。MyEMS 断采导致时间序列出现空洞时,我采用分段处理:缺失不超过 2 小时,用前后线性插值;连续缺失超过 6 小时,直接丢弃该天数据。原因很简单:线性插值在短时间空洞里表现还可以,但空洞过长时插值会人为制造一条并不存在的负荷曲线,反而给模型输入虚假模式。
第三步是异常尖峰处理。电力数据里偶尔会出现一个点突然飙升或骤降,比如某台大设备启动瞬间的冲击电流。检测方法是用滚动窗口的中位数和绝对中位差,把偏离窗口值超过 3 倍标准差以内的点视为异常,然后替换为窗口分位数。这种处理不能直接删点,因为时间序列需要连续,删了之后序列长度断裂,后续构造滑动窗口会很麻烦。
2.3 特征工程:别只喂历史负荷
很多初学者做负荷预测,只把历史负荷作为输入,丢给 LSTM 完事。我第一版也是这么干的,结果 MAPE 一直在 9% 左右,后来逐一加特征才把误差降下来。
我最后使用的特征可以分为四类。第一类是历史负荷序列本身,这是最基本的预测依据;第二类是时间特征,包括小时编号、星期几、是否工作日、是否节假日;第三类是气象特征,包括气温、湿度、体感温度,以及滞后温度(比如过去 6 小时平均温度、过去 24 小时平均温度);第四类是生产特征,如果客户能提供排产计划,就把计划编码作为特征输入。
时间特征为什么重要?因为 LSTM 没有内置的时间概念,它只知道“每隔一个时间步输入一行数据”,并不知道第 5 步是凌晨、第 20 步是傍晚。如果不把小时和星期几编码进去,模型只能靠纯数据自己摸索周期性,收敛慢且效果差。节假日特征更重要:厂休日的负荷曲线和工作日完全不一样,没有这个特征,模型遇到节假日就会把工作日模式套上去。
气象特征同理。夏天午后气温升高,空调负荷快速上升,这种关系在历史负荷里已经有体现,但模型很难自己提炼出“温度每升高 1 度对应负荷增加多少”的规律。把温度作为外生变量直接喂进去,相当于帮模型把这层关系显式拆出来。
2.4 滑动窗口、样本构造与时间切分
特征准备好之后,要把连续时间序列切分成“样本”。我的方案是用过去 168 小时(7 天)预测未来 24 小时(1 天),切分步长为 1 小时。这样 90 天数据能产生约 2000 个样本,训练一个中小规模的 LSTM 足够。
切分代码并不复杂,核心是构造输入矩阵和目标矩阵:
import numpy as np def make_samples(df, input_len=168, output_len=24): X, y = [], [] for i in range(len(df) - input_len - output_len + 1): X.append(df.iloc[i:i + input_len].values) y.append(df.iloc[i + input_len:i + input_len + output_len].values) return np.array(X), np.array(y)这里的 df 必须是一列按时间排好序的特征向量,不能混入标签列。切分时还有一个关键原则:训练集、验证集、测试集必须按时间顺序划分,绝对不能随机打乱。随机打乱虽然能提高交叉验证的分数,但会带来“时间泄漏”:模型在训练时已经见过未来数据,测试集上表现再好,上线后也会立刻崩掉。我按前 70% 训练、中间 15% 验证、最后 15% 测试来切,确保测试集时间完全在训练集之后。
3. LSTM 模型搭建:把理论公式落成工程
3.1 网络结构选型逻辑
我最终采用的网络结构是两层 LSTM 加一个全连接输出层,具体如下:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model = Sequential([ LSTM(128, return_sequences=True, input_shape=(168, n_features)), Dropout(0.2), LSTM(64, return_sequences=False), Dropout(0.2), Dense(32, activation='relu'), Dense(24) ])输入形状是 (168, n_features),168 是过去 168 个小时,n_features 是特征数量,包括负荷、温度、湿度、小时编码、星期几编码等。输出层有 24 个神经元,对应未来 24 小时的负荷预测值。
选两层 LSTM 而不是一层的考虑是:第一层 LSTM 负责提取短期的局部动态,比如最近几小时负荷的变化趋势;第二层 LSTM 在局部特征之上再抽象一层,捕捉更长的依赖关系。负荷预测场景的数据模式相对平滑,两层已经足够,再加第三层不仅训练变慢,还可能过拟合。Dense 层的作用是把 LSTM 输出的高维向量映射到具体的功率值,最后用线性激活输出即可,不需要在最后一层套 sigmoid 之类的非线性,因为负荷预测是回归问题,输出范围不受限。
3.2 归一化与反归一化的坑
LSTM 内部用到 tanh 和 sigmoid 激活函数,输入数据必须缩放到合适的范围,否则梯度很容易消失或爆炸。我使用 sklearn 的 MinMaxScaler,把每个特征缩放到 [0, 1] 区间,目标值(未来 24 小时负荷)也单独缩放。
这里有一个我在项目里吃过亏的细节:scaler 必须在训练集上 fit,然后再 transform 训练集、验证集和测试集。如果偷懒直接对整个数据集 fit,测试集的统计信息会泄漏到训练过程中,验证集和测试集的效果都会被高估。推理阶段同样要用训练好的 scaler 去处理新数据,不能重新 fit。
反归一化也要小心。模型输出的 24 个值是在 [0, 1] 区间内的,必须用目标值的 scaler 反变换回实际功率单位。很多同学训练时指标看起来不错,部署到线上预测结果却大得离谱或小得不合理,排查到最后往往就是归一化和反归一化没有一一对应。
3.3 训练配置:损失函数、早停与过拟合
损失函数我一开始用的是均方误差 MSE,后来改成 Huber 损失。MSE 对离群点非常敏感,数据里一旦残留少量尖峰,模型就会花大力气去拟合这些异常点,反而牺牲正常区间的预测精度。Huber 损失在误差较小时表现像 MSE,误差较大时退化为线性,对离群点稳健得多。
from tensorflow.keras.callbacks import EarlyStopping from tensorflow.keras.losses import Huber from tensorflow.keras.optimizers import Adam model.compile(optimizer=Adam(learning_rate=1e-3), loss=Huber(delta=1.0)) early_stop = EarlyStopping(monitor='val_loss', patience=15, restore_best_weights=True) model.fit(X_train, y_train, validation_data=(X_val, y_val), epochs=100, batch_size=64, callbacks=[early_stop])早停是必须的。如果不加早停,模型在训练集上 loss 会一直下降,但验证集 loss 在某个点之后开始上升,这就是过拟合信号。patience 我设置为 15 个 epoch,意思是验证集 loss 连续 15 个 epoch 没有下降时,就停止训练并恢复验证集表现最好的权重。batch_size 设 64,太小训练不稳定,太大内存吃紧。训练上限 100 个 epoch,一般 40 个 epoch 左右就会触发早停。
判断过拟合还有一个实用方法:训练集最终 loss 远低于验证集 loss,比如训练 loss 是 0.01,验证 loss 是 0.1,说明模型把训练数据背下来了。这时我会调小 LSTM 单元数,或者把 Dropout 从 0.2 提高到 0.3。能源负荷数据量通常不大,过拟合的坑很容易踩。
4. 从 90% 到 95%:调优过程全记录
4.1 第一版:只有历史负荷,精度 90% 上下
第一版模型结构跟现在差不多,但特征只有历史负荷,加上小时和星期几编码,没有温度和节假日特征。训练完成后,测试集 MAPE 在 9% 到 10% 之间,折算准确率大约 90% 到 91%。
这个结果在客户看来勉强能接受,但我知道问题很多。翻看预测曲线发现,周末和节假日的预测误差明显偏大,最高到 15% 以上;夏季午后的高峰段,预测值总是偏低,感觉模型反应总是慢半拍。原因是纯历史负荷模型只能学到“以前这个时刻是多少”,学不到“温度升高导致负荷升高”这种因果关系。节假日没有对应特征,模型只能把工作日模式硬套上去,自然误差大。
4.2 加入温度之后:直接跳到 93% 以上
第二版我接入了天气数据,包括当日逐小时气温、湿度和体感温度。测试集 MAPE 立刻降到 7% 左右,准确率到了 93%。这个提升很明显,但也暴露了新问题:空调负荷对温度变化有滞后性,下午温度最高时负荷还在涨,傍晚温度降了负荷还在高位。只用当前时刻温度作为特征,模型跟不上这种滞后。
解决办法是增加滞后温度特征。我把过去 6 小时平均温度、过去 24 小时平均温度作为额外输入,等于告诉模型“最近这段时间整体热不热”。加入这两个特征后,MAPE 又降到 5% 到 6%。高温累积效应在大型厂房里非常明显:连续几天高温后,室内设备和墙体都积累了热量,即使当天温度小幅下降,负荷也不会立刻回落。滞后温度特征把这种惯性表达出来了。
4.3 分场景建模:白天、夜间、节假日分开处理
第三个关键优化是分场景处理。统一模型在夜间低负荷段的问题很大:夜间实际负荷可能只有 200 kW,预测 180 kW,绝对偏差只有 20 kW,但相对误差已经 10%,把整体 MAPE 拉高了。这不是模型笨,而是百分比误差在基数小的时段天然容易被放大。
我做了两个调整。一是对夜间低负荷段做后校准:预测值低于训练集 5% 分位数时,用分位数兜底,避免预测值低到不合理的程度。二是把节假日单独建模:节假日样本少,用复杂模型容易过拟合,我只用了一个单层 LSTM,输入特征也简化成历史负荷加节假日类型,效果反而比套统一模型好。工作日模型和节假日模型分开输出,再由上层服务根据日期选择调用。
经过这几轮调整,测试集的 MAPE 稳定在 5% 左右,折算准确率 95%。这里有个前提:如果遇到极端天气、设备大修、临时限电这类突发事件,模型误差还是会上冲到 10% 以上。机器学习模型处理不了“从没见过的场景”,业务上要把这些情况作为异常事件单独管理,不能指望模型自己适应。
4.4 横向对比:ARIMA、SVR、单层 LSTM 都跑了一遍
为了确认双层 LSTM 加特征工程不是白费功夫,我在同一份数据上对比了四种方案,结果如下:
| 模型 | 输入特征 | 测试集 MAPE | 折算准确率 |
|---|---|---|---|
| ARIMA | 历史负荷 | 12% | 88% |
| SVR | 历史负荷 + 温度 | 10% | 90% |
| 单层 LSTM | 历史负荷 + 时间 | 7% | 93% |
| 双层 LSTM + 特征工程 | 历史负荷 + 时间 + 天气 + 滞后温度 | 5% | 95% |
表格数据是我自己项目里的实测结果,不代表所有场景都这样。但这个对比至少说明:模型结构重要,特征工程往往更重要。从 ARIMA 到双层 LSTM,模型复杂度提升带来的收益,不如从“只喂负荷”到“加入温度和滞后特征”带来的收益大。做类似项目的朋友,建议先把重心放在数据和特征上,不要一上来就追求花哨的网络结构。
5. 部署进 MyEMS:预测值如何变成业务动作
5.1 预测服务的接口设计
模型训练和离线评估只是第一步,真正产生价值的是把预测能力部署到业务系统里。我把训练好的 Keras 模型导出为 SavedModel 格式,然后用 FastAPI 写了一个轻量预测服务。服务对外只暴露两个接口:一个用于预测未来 24 小时曲线,一个用于查询预测结果历史数据。
预测接口的处理流程大致是:从 MyEMS 数据库读取最近 168 小时的实际负荷数据,调用天气 API 获取未来 24 小时温度预报,拼装特征、归一化、推理、反归一化,最后返回带时间戳的功率序列。这里有个容易被忽略的点:天气 API 返回的是逐小时预报,但预报数据有延迟,接口里需要做时间对齐,把温度预报插值到跟负荷序列一致的时刻。
服务本身不保存模型状态,每次请求都重新加载一次特征模板。原因是为了让重训和上线互不影响:模型更新时替换 SavedModel 目录即可,服务代码不用改。我用 Python 的 joblib 保存训练好的 scaler 和特征配置,推理时一起加载。
5.2 定时任务与预测结果写回平台
预测服务上线后,我用 Linux crontab 配置了每天定时任务,早上 6 点执行一次,生成当天 7 点到次日 6 点的预测曲线。为什么选早上 6 点?因为客户要看的是“当天用电安排”,7 点前需要拿到结果,而天气 API 的预报数据要到凌晨才更新完,太早跑没有可靠温度输入。
# crontab 每天 06:00 执行 0 6 * * * cd /opt/myems-lstm && python run_daily_forecast.py >> logs/forecast.log 2>&1run_daily_forecast.py 脚本做的事情很直接:调用预测服务拿到结果,写入 MyEMS 数据库的一张自定义表 predict_load,字段包含预测时间点和对应的功率值。MyEMS 自带的看板支持自定义 SQL 查询,我把实际负荷曲线和预测负荷曲线放在同一张图里,业务人员每天打开平台就能看到“预测今天负荷大概这样走,实际到目前为止偏差多少”。
预测结果写回平台这一步很关键,它让 AI 能力变成了业务系统里看得见、用得上的功能,而不是停留在 notebook 里的实验。客户不会打开 Jupyter 看模型效果,但他们每天都会打开能源管理平台看曲线。
5.3 实时偏差告警与自动重训
预测结果上线后,我还加了一个滚动偏差告警机制。每 15 分钟,平台会收到一次实际负荷数据,我把实际值和预测曲线上对应时刻的值做比较,如果连续三个点偏差超过 15%,就通过企业微信机器人发告警。这个告警有两层含义:要么是预测模型出了问题,要么是实际用电出现了异常,比如设备故障、临时增加生产任务。不管哪种情况,都值得人工关注。
模型不是训一次就一劳永逸。负荷模式会随着产线调整、季节变化和用能结构改变而漂移。我最初是在模型上线后第二个月发现 MAPE 从 5% 慢慢爬到了 7%,排查之后发现是产线新增了几台设备,夜间负荷整体抬升,旧模型始终预测偏低。后来我加了自动重训机制:每周日凌晨用最近 90 天的数据重新训练一次,自动在测试集上评估 MAPE,如果超过 8% 就发送告警邮件给管理员。
重训任务用 Python 的 APScheduler 调度,代码量不大。重训完成后自动替换 SavedModel 目录,服务下一次请求就会加载新模型。整个链路跑通后,模型基本不需要人工干预,运营人员只需要关注告警信息。
6. 踩过的坑和几条判断标准
6.1 时间泄漏:离线指标虚高的头号元凶
做时间序列预测,最常见的坑是时间泄漏。我第一次做这个项目时,为了快速验证模型,直接把数据随机打乱切分了训练集和测试集。结果测试集 MAPE 只有 2%,模型“预测”得好像神乎其神。后来用时间顺序重新切分,MAPE 立刻变成 9%。原因就是随机打乱后,每个样本的训练集里包含了未来时刻的数据,模型等于直接背了答案。
时间泄漏还有一种隐蔽形式:归一化时用全量数据 fit scaler。虽然训练集和测试集没有交叉,但测试集的统计信息通过 MinMaxScaler 的最大最小值传给了训练过程。正确的做法是只用训练集 fit,测试集只做 transform。这个判断标准可以记一下:任何特征加工步骤,如果使用了全局统计量,都要确保这些统计量只来自训练阶段。
6.2 峰值预测偏低与损失函数选择
负荷预测的“经典病”是预测曲线整体偏低,尤其高峰段。根子在损失函数:MSE 会把大误差值的样本权重放得更大,模型为了降低总 loss,会倾向于把预测值往均值方向收缩,不敢预测太高或太低。结果就是高峰被低估,低谷被高估。
我的处理办法是引入峰值加权损失,在普通 MSE 基础上,把一天中负荷排名前 20% 的时段的误差权重提高 1.5 倍。这个改动不大,但峰值段的绝对误差明显下降。实际业务中,客户最关心的往往就是峰值:需求响应要盯着峰值削减,购电计划要按峰值容量预留。如果只看 MAPE,模型可能在峰值段一塌糊涂却仍然显得“准确率高”。
6.3 天气预报误差会传导进模型
加入气象特征后,模型精度上去了,但我也发现一个尴尬问题:模型输入用的是温度预报,不是实测温度,预报本身就有误差。天气 API 预报的温度跟当天实测温度差 2 到 3 度很常见,在夏天空调负荷主导的场景里,这就足够让预测偏差 5% 以上。
缓解办法有两个。一是对接天气数据时尽量用更新更频繁的短时预报源,预测时刻越近,预报越准。二是训练时给温度特征加入小扰动,相当于做数据增强,让模型对温度偏差不那么敏感。实际操作中,我更多是靠后校准:每天凌晨先跑一版预测,白天实时温度出来后,每隔 4 小时用最新温度预报刷新一次未来 12 小时的预测曲线,把误差控制住。
6.4 什么时候其实不用上 LSTM
说了这么多 LSTM 的好处,最后补一个反向判断标准。不是所有场景都适合上 LSTM,至少满足以下三个条件再考虑:
- 有连续一年以上、15 分钟或小时级的历史数据,样本量足够大
- 负荷曲线存在明显的周期性,且受工作日、季节、天气等因素影响
- 业务上真正需要提前 24 小时以上的预测,模型提前量能兑现业务价值
如果只是做未来一两个小时的超短期滚动预测,用简单线性回归或指数平滑,效果可能跟 LSTM 差不多,成本和维护难度却低得多。如果历史数据只有三个月,LSTM 很难学到完整的季节模式,不如用统计方法先顶住。如果业务只要求一个粗略的月度总量预测,也不需要上 LSTM,直接做多元回归就够了。
判断标准很简单:先把数据画出来,如果曲线没有明显规律,或者数据量很小,LSTM 很难变出花样;如果有规律、数据量够、预测时长远,LSTM 才值得投入。
最后分享一点个人体会。负荷预测项目做到后期,我的精力几乎全花在数据质量和业务口径上,模型结构反而没怎么动。谁来做预测都会遇到“数据脏、口径不一致、业务预期过高”这些事,先把准确率的口径跟业务方对清楚,再把数据洗干净、特征做扎实,LSTM 才能真正跑出 95% 的效果。如果你也在做类似的事,建议按这个顺序推进:定口径、清数据、加特征、调模型。这样踩坑最少,见效最快。