入行这几年,做了不少风电功率预测的项目,最深的感受是:风电场的数据看起来规整,跑起来全是坑。单说风速,不同天气过程下,同样的平均风速对应的功率能差出20%甚至更多。单纯扔一个LSTM进去,模型很容易抓住某一段工况的规律,换个天气过程就崩。后来把“高斯混合模型聚类”作为前置环节,用聚类结果辅助CNN-BiLSTM-attention模型做短期功率预测,效果提升非常明显。这篇文章就完整复盘一下这个方案的原理、实现细节和我在复现中踩过的坑,给正在做风功率预测、时序预测的同学一个可以直接参考的路线。本文同时给出了Python和MATLAB两套实现的关键代码思路,环境准备、数据清洗、聚类建模、网络训练和结果评估都会覆盖到。
1. 为什么风机功率预测要先对“天气”做聚类?——把问题拆开看
1.1 风电功率预测的核心难点
风电功率预测本质上是一个“把大气过程映射到功率曲线”的问题。物理上,风速、风向、湍流强度、温度、气压共同作用,最终决定一台风机能发出多少电。麻烦的是,这个映射关系并不稳定:低温高密度空气下,同样风速对应的功率会偏高;强湍流时,功率曲线会被明显拉宽;阵风过程里,风轮转速响应滞后,功率序列会出现明显的非平稳段。
如果在建模时把这些不同工况混在一起训练,模型内部的表示空间就要容纳多个形态差异很大的功率-风速关系。对线性模型这是灾难,对LSTM这类序列模型也好不到哪去——它虽然能记住时序上下文,但要让同一组权重同时拟合多个物理模态,代价是精度被平均掉,极端天气下误差特别大。
我最早用纯BiLSTM跑某风电场的数据时,连续晴天工况下预测误差表现不错,可一旦碰到一个锋面过境样本,预测值直接偏离真实功率一大截。这就是典型的“工况混淆”问题。把聚类加进来,本质上是先把样本按天气形态分组,让模型在更纯净的工况子集上学习,或者在输入中加入工况标识让网络有条件地调整映射关系。
1.2 聚类在这里解决的不是“分类”而是“工况对齐”
很多同学理解聚类,习惯把它当成“把数据贴标签、分类”。但在功率预测这个场景里,聚类的角色更像是“把一个连续变化的大气状态场,切分成若干内部相对一致的工况片段”。
举个例子:假设风速历史窗口中平均风速都是 8 m/s,但一组样本的风速方差很小,另一组样本的风速方差很大。前者多是稳定大风过程,功率平稳;后者往往伴随阵风,功率波动频繁。如果只用平均风速作为特征,这两类样本在特征空间完全重叠,模型没法区分。而加入风速标准差、湍流强度、风向变化率等特征后,GMM聚类可以自动把这些样本分成两组,并且给出“样本属于某一个高斯分量的概率”。
这带来一个重要好处:在模型输入中加入聚类概率向量(soft labels)而不是硬编码的0/1类别,可以让CNN知道“当前样本以75%的概率属于稳定大风工况,以25%的概率属于阵风过程”。网络学习到的映射关系就有了工况连续过渡能力,比硬切分更平滑。
1.3 总体架构:聚类-序列建模-注意力融合
整套预测流程可以拆成四段。第一段是数据清洗与特征工程,重点是把SCADA里的坏数据剔除干净,构造天气工况特征;第二段是GMM聚类,对样本窗口做工况划分,输出聚类标签或概率;第三段是特征序列构建,把历史功率、历史风速、气象特征和聚类特征拼接成多变量时间序列;第四段是CNN-BiLSTM-attention模型,先让CNN提取多变量特征的局部关联结构,再让BiLSTM建模双向时序依赖,最后用注意力机制动态聚焦对预测最关键的几个时刻。
这里要特别说一句,聚类结果并不是只用来做“分群训练”,更稳妥的做法是把聚类信息作为额外特征送入网络。这样既保留了工况信息,又让网络能够共享不同工况间共有的时序模式,数据利用率更高。
2. 数据清洗与天气工况特征工程:脏数据比模型更伤预测精度
2.1 原始SCADA数据质量问题和处理方式
SCADA系统记录的数据质量,真的需要用“残酷”两个字形容。一次我在准备训练集时,发现某台风机的功率曲线图上有一大片散点远低于理论功率,排查下来发现是限电期间的样本,风机被调度指令限制出力;另一段时间功率为零但风速正常,是风机检修停机。这类样本如果直接进训练集,模型会被带偏。
因此第一步一定是数据清洗。我习惯按以下顺序处理:
- 剔除功率为负、风速为负或超过切出风速的物理不可行样本;
- 剔除功率超过机组额定功率 1.05 倍的异常记录;
- 剔除风速正常但功率为零且持续超过30分钟的数据段(判断为停机或限电);
- 对风速、功率做滑动窗口的3σ剔除,去除传感器瞬时毛刺;
- 对缺失值采用前后2小时线性插值,缺失比例超过10%的时段直接丢弃。
做完清洗后,还要做重采样。原始SCADA一般按秒级或分钟级记录,短期功率预测通常用15分钟分辨率。千万不要直接把秒级数据降采样到15分钟取平均值那么简单,因为风速的脉动信息在平均后丢失了。我在降采样时还会额外保存窗口内的风速标准差、风速最大值、功率标准差,这些“波动统计量”对后续聚类非常有用。
2.2 特征构造:风速向量化、风向正余弦编码、湍流强度
特征工程里最容易翻车的是风向处理。风向是角度量,350度和10度只差20度,数值上却相差340。如果直接把风向原始值作为特征喂给模型,聚类时350度和10度会被分割到距离很远的区域,这是完全错误的空间几何关系。解决办法是正余弦编码:生成 wind_dir_sin 和 wind_dir_cos 两个特征,既保留了角度连续性,又规避了0度边界跳变问题。
我使用的天气工况特征集合如下:
| 特征 | 说明 | 计算方式 |
|---|---|---|
| wind_speed_mean | 窗口平均风速 | 原始风速均值 |
| wind_speed_std | 窗口风速标准差 | 反映阵风程度 |
| wind_speed_max | 窗口内最大风速 | 刻画极端阵风 |
| wind_dir_sin / cos | 风向正余弦编码 | 保留角度连续性 |
| turbulence_intensity | 湍流强度 | 风速标准差 / 平均风速 |
| temperature | 气温 | 影响空气密度,进而影响功率 |
| pressure | 气压 | 同上 |
| power_std | 功率波动标准差 | 直接反映出力波动性 |
这些特征既参与聚类建模,也作为序列模型的输入特征之一。实践经验是:湍流强度这个特征对聚类效果的影响最大。稳定层结下湍流强度通常小于0.1,对流旺盛时可能超过0.3,这两种工况下的功率曲线差异非常显著。如果不加入这个特征,GMM聚类很容易退化成“按风速分箱”的朴素结果,失去天气工况划分的意义。
2.3 样本窗口构建与训练/验证划分
短期功率预测的输入通常是过去几小时的观测序列。我在实验中用过去6小时、15分钟一条记录,即24个时间步预测未来1小时4个时间步的功率,输出取4步均值或分别输出后加权。特征序列维度包括历史风速、历史功率、温度、气压、风向正余弦、湍流强度、聚类概率向量等,最终形成的输入张量形状为 (batch_size, 24, feature_dim)。
划分训练集和验证集时,最容易犯的错误是随机打乱。时间序列的强自相关性决定了我们必须按时间顺序切分:比如前80%时间段的样本做训练,后20%做验证。而且要注意“踩踏”问题——训练集最后一段和验证集开头一段之间需要留出至少24小时的间隔,否则验证集样本和训练集样本高度重叠,验证分数会虚高。
3. 高斯混合模型聚类的原理、实现与聚类数选择
3.1 为什么是GMM而不是K-means
如果只做硬聚类,K-means确实够用,而且运行快。但它有两个问题:一是只能得到球形簇,对狭长分布的天气特征拟合不好;二是只能输出0/1的硬标签,无法表达“样本同时部分属于多个工况”的过渡状态。
GMM假设数据由若干个高斯分布的加权组合生成。每个高斯分量都有独立的均值向量和协方差矩阵,因此可以拟合不同形状的分布。更重要的一点是,GMM通过EM算法求出每个样本属于各分量的后验概率,这正好可以转化成软标签输入给预测模型。从风电场实际数据来看,天气过程往往存在连续过渡,软标签比硬标签更符合物理直觉。
3.2 EM算法与高斯分量的工程细节
GMM的EM算法每次迭代分两步。E步根据当前参数计算每个样本属于每个高斯分量的后验概率;M步根据这些后验概率重新估计每个分量的均值、协方差和权重。迭代至对数似然收敛即可。
实现上可以直接调用scikit-learn的GaussianMixture,也可以自己写EM。工程上需要注意三点:
- 协方差类型选择
full,而不是默认的tied或diag。风速标准差和湍流强度之间存在相关结构,full协方差能捕捉这种关系; - 迭代次数不要用默认值,最好设置到200,并设定对数似然的收敛阈值,防止EM在复杂数据下提前停止;
- 因为EM可能陷入局部最优,需要设置多个随机初始化次数。用
n_init=10,最终取对数似然最大的那次结果。
3.3 用BIC与轮廓系数确定分量个数
聚类数 K 的选择不能拍脑袋。我同时看两个指标:BIC(贝叶斯信息准则)和轮廓系数。BIC越小说明模型在拟合能力和复杂度之间平衡得越好,但它有时会倾向选择较多分量,所以要结合轮廓系数一起判断。
实际操作中,我对K从2到10做循环,每个K跑完GMM后计算BIC和轮廓系数。通常会看到某个K之后BIC下降变得平缓,也就是出现了“肘部”,同时轮廓系数达到局部峰值。以我某次实验为例,K=5时轮廓系数0.58,K=6时为0.55,BIC在K=6后下降幅度明显变小,因此最终选择K=5。选择聚类数后,要把GMM在全部数据上重新拟合一次,并且固定随机种子,保证后续建模结果可复现。
3.4 聚类结果不稳定的处理和修复技巧
GMM的一个麻烦是结果受随机种子影响。同一个数据集,random_state不同,聚类结果可能漂移,尤其是两个高斯分量在特征空间中有重叠时。这个问题在预测任务中不算致命,但如果要把聚类结果作为输入特征,不稳定的标签会让模型学到无意义的波动。
我的处理办法是:先跑多次聚类,通过配对标签的方法计算各个运行的ARI(调整兰德指数),选取与多数运行一致性最高的那一次作为最终结果。如果ARI普遍低于0.7,说明数据本身聚类结构很弱,这时候应该回头检查特征,而不是硬调聚类参数。
4. CNN-BiLSTM-attention结构设计与参数依据
4.1 CNN部分:多变量特征的空间相关性提取
很多人有个误解,觉得CNN只能用在图像上。实际上,一维卷积在时间序列上非常有效。对输入张量 (batch_size, time_steps, features),Conv1D沿着时间维度滑动卷积核,每个卷积核同时覆盖所有特征通道。
在我的网络里,第一层是Conv1D,卷积核大小设为3,64个滤波器。卷积核大小为3意味着每次只看相邻3个时刻的多变量信息,这有点像一个滑动窗口特征提取器,把“局部时刻组合”的低层模式抓出来。这里我特意不使用池化层。因为在时间序列预测中,池化会压缩时间分辨率,而我们需要保持与输入等长的时间维,方便后续BiLSTM输出每个时刻的隐状态。
实现时我通常连接两层卷积。第一层64个滤波器,第二层128个滤波器,激活函数都用ReLU。卷积之后接BatchNormalization,加快收敛、抑制过拟合。
4.2 BiLSTM部分:前后向时序依赖的必要性
BiLSTM是LSTM的双向版本。标准LSTM只按时间正向传播,隐状态 $h_t$ 只能利用 $t$ 时刻之前的信息。BiLSTM增加了一条反向传播链,$h_t$ 由正向隐状态 $\overrightarrow{h}_t$ 和反向隐状态 $\overleftarrow{h}_t$ 拼接得到,因此每一个时间步都同时利用了过去和未来的上下文。
很多人会问,预测任务里未来信息不可知,为什么还要用双向?关键点是:这里的输入是历史窗口。在历史窗口内部,某个时刻的风速变化既受到之前趋势的影响,也影响着之后功率的走势。双向结构能在窗口内部更好地建模这种“因果-反因果”耦合。比如一个风速尖峰出现后,功率爬坡可能需要几分钟才能跟上;反向传播能更早识别出这种“即将到来的爬坡”模式。
BiLSTM层的单元数我设为128,返回每个时间步的隐状态序列,形状保持为 (batch_size, time_steps, 256)。
4.3 Attention机制:怎么让它把注意力放到关键时段
BiLSTM输出的是一个包含所有历史时刻隐状态的序列。最后的预测只应该重点参考其中少数关键时刻,比如爬坡启动点、峰值点。Attention机制做的就是对每个时刻的隐状态计算一个权重,然后加权求和得到上下文向量。
我用的是经典的加性注意力(additive attention),实现代码大致如下:
import tensorflow as tf from tensorflow.keras.layers import Layer class AttentionLayer(Layer): def __init__(self, **kwargs): super().__init__(**kwargs) def build(self, input_shape): self.W = self.add_weight(name='att_weight', shape=(input_shape[-1], input_shape[-1]), initializer='glorot_uniform', trainable=True) self.b = self.add_weight(name='att_bias', shape=(input_shape[-1],), initializer='zeros', trainable=True) self.v = self.add_weight(name='att_vec', shape=(input_shape[-1], 1), initializer='glorot_uniform', trainable=True) super().build(input_shape) def call(self, x): # x shape: (batch_size, time_steps, hidden_dim) u = tf.tanh(tf.tensordot(x, self.W, axes=1) + self.b) scores = tf.tensordot(u, self.v, axes=1) # (batch_size, time_steps, 1) scores = tf.squeeze(scores, -1) weights = tf.nn.softmax(scores, axis=-1) context = tf.reduce_sum(x * tf.expand_dims(weights, -1), axis=1) return context权重本质上是在训练中自动学习哪些时间步更重要。从实验里看,强阵风样本的注意力权重往往集中在风速突变的那几个时刻,验证了注意力机制确实学到了有物理意义的时间聚焦。
4.4 网络超参数与训练策略设计
我最终的网络结构可以这样概括:输入 (24, 28),经过两层Conv1D后进入BiLSTM(128),再接Attention层得到上下文向量,最后经过两个全连接层输出预测功率。全连接层第一层64个神经元,接Dropout(0.3),输出层为单个神经元。
训练时使用Adam优化器,初始学习率1e-3,并配合ReduceLROnPlateau调度器,在验证损失连续5轮不下降时把学习率乘以0.5。批次大小64,最大训练轮数80,使用EarlyStopping监控验证损失,patience设为10。损失函数用均方误差MSE,因为功率预测要惩罚大误差,MSE的二次惩罚特性正合适。
5. 仿真结果、评价指标与对比实验:数字说话
5.1 评价指标体系
短期功率预测的评估不能只看一个指标。我使用四个指标:
- RMSE:均方根误差,对大误差敏感;
- MAE:平均绝对误差,反映整体偏差水平;
- MAPE:平均绝对百分比误差,但在功率接近0时没有意义,需谨慎使用;
- R²:决定系数,衡量模型解释数据方差的比例。
此外,行业里常把RMSE除以风电场装机容量得到标幺化指标,便于不同规模风电场横向比较。
5.2 利用Python训练与MATLAB验证的工作流
很多团队的环境里既有Python又有MATLAB。我的做法是:Python负责数据清洗、聚类和深度学习模型的训练与推理,MATLAB负责功率曲线拟合、不确定性分析和结果可视化。两者通过CSV文件交换数据,不强行把流程都塞进一个环境。
MATLAB里做GMM聚类很简单:
% 数据矩阵 X:每行是一个样本的天气工况特征 gmmModel = fitgmdist(X, 5, 'RegularizationValue', 0.01, 'Options', statset('MaxIter', 200)); idx = cluster(gmmModel, X);注意加一个小的RegularizationValue,防止协方差矩阵在特征近似共线时奇异。模型输出传给Python时,保存为带表头的CSV,Python端用pandas读取,按时间戳对齐后拼接特征。
5.3 几种模型的对比结果和其背后原因
我用某风电场的历史数据做了一组对比实验,输入特征完全相同。为了演示,这里给出一组典型数值:
| 模型 | RMSE (kW) | MAE (kW) | R² |
|---|---|---|---|
| 物理功率曲线法 | 183.4 | 142.6 | 0.84 |
| 标准LSTM | 121.7 | 89.3 | 0.91 |
| BiLSTM | 117.2 | 85.1 | 0.92 |
| CNN-BiLSTM-attention | 104.8 | 76.4 | 0.93 |
| GMM+CNN-BiLSTM-attention | 92.5 | 66.8 | 0.95 |
物理功率曲线法在稳定工况下其实很不错,但阵风工况误差很大。标准LSTM比物理法好了不少,但缺少反向上下文建模;加上Attention后,模型对爬坡段的响应明显加快。最终加上GMM聚类信息后,RMSE相对纯CNN-BiLSTM-attention又下降了约12%。说明天气工况信息对时序模型确实有显著的辅助作用。
5.4 聚类数变化对结果的影响
聚类数K不是越大越好。我在同一数据集上测了K从3到8的RMSE变化。K=3时RMSE约为96 kW,K=5时降到了92.5 kW,K=7时反而回升到95 kW左右。这是因为K过小时工况划分不充分,K过大时每个聚类分量的样本变少,噪声增加,聚类本身也变得不稳定。实际项目中,我通常把K设在4到6之间,并配合BIC和轮廓系数一起确定。
6. 复现过程中的坑与个人经验总结
6.1 坑1:数据归一化泄漏导致成绩虚高
第一次跑通时,验证集RMSE比预想低很多,让我兴奋了一阵。后来才发现问题出在归一化:我用全量数据的均值和标准差去归一化训练集和验证集,这属于典型的数据泄漏。因为验证集的信息通过统计量传导到了训练过程,评估结果当然好看。正确的做法是先只对训练集fit归一化器,再transform验证集。如果是滑动窗口预测,最好按时间段滚动更新归一化参数,模拟真实部署场景。
6.2 坑2:attention维度写错导致信息坍缩
自实现AttentionLayer时,我一开始把tensordot(x, self.W, axes=1)的维度理解错了,导致算出的权重矩阵维度不对,后续广播规则也乱了,最终输出几乎等于对全部时刻取平均,没有任何聚焦效果。排查了整整半天,最后打印每一步的shape才发现问题。建议所有自己写attention的同学,先构造一个随机batch,逐层打印张量形状,确认从输入到context输出的shape变化完全符合预期,再进入训练。
6.3 坑3:聚类结果随随机种子漂移怎么办
前面说过GMM的EM算法对初始化敏感。在我某个数据集上,换一个随机种子,聚类结果的ARI只有0.6。这种情况下如果直接使用聚类标签,模型每次训练学到的东西都不一样。最终解决方案是多次运行、选出代表性聚类结果,并固定随机种子固化整个流程。如果你需要更严谨的结果,可以试试多次集成,比如跑20次聚类,对概率输出做平均,这样既稳定又保留了软标签的连续信息。
6.4 我的最终建议
最后分享一个我实际操作中养成的习惯:每次做功率预测项目,我都会先把功率曲线散点图画出来,按聚类标签着色。这个图能非常直观地告诉我聚类是否有意义——如果不同颜色的散点在功率曲线上分层清晰,聚类的工况划分就是有效的;如果颜色混杂成一团,说明特征构造或者聚类参数需要重新调整。这个可视化检查只需要几十秒,但能省下后面模型调试的大量时间。
整个GMM+CNN-BiLSTM-attention方案,从数据清洗到最终评估,流程不复杂,但每个环节都有容易忽略的细节。把这套流程固化下来,后续换数据、换风电场都能快速迁移。我也建议你在自己的数据上先把基础模型跑通,再逐步加入聚类信息和注意力机制,每一步都用评价指标验证效果增量,这样既容易定位问题,也能更清楚每个模块到底贡献了多少。