量預(yù)測實(shí)戰(zhàn):從數(shù)據(jù)清洗到TensorFlow LSTM模型部署)
簡介這是一套基于TensorFlow實(shí)現(xiàn)光伏電站發(fā)電量預(yù)測的完整項(xiàng)目面向機(jī)器學(xué)習(xí)初學(xué)者、能源數(shù)據(jù)分析人員及對時(shí)間序列預(yù)測感興趣的開發(fā)者解決歷史電站生產(chǎn)數(shù)據(jù)與天氣數(shù)據(jù)到未來出力預(yù)測的建模問題。資源包共14個(gè)文件以10個(gè)CSV電站與天氣數(shù)據(jù)集為主體另外包含1個(gè)Python訓(xùn)練腳本、1個(gè)Jupyter Notebook建模流程文件及2個(gè)keep占位文件整體壓縮包僅513KB小巧完整。已有233人學(xué)習(xí)瀏覽適合快速上手實(shí)踐。項(xiàng)目覆蓋數(shù)據(jù)清洗、缺失值處理、歸一化、特征編碼等預(yù)處理方法并示范了基于LSTM/RNN的時(shí)序模型構(gòu)建、訓(xùn)練、評估與調(diào)優(yōu)環(huán)節(jié)。借助附帶的電站分站數(shù)據(jù)集與逐日天氣數(shù)據(jù)讀者可直接運(yùn)行代碼復(fù)現(xiàn)預(yù)測流程理解從原始數(shù)據(jù)到模型輸出的完整鏈路是深度學(xué)習(xí)應(yīng)用于能源預(yù)測場景的良好范例。1. 光伏電站發(fā)量預(yù)測項(xiàng)目真實(shí)工程里難在哪里光伏電站發(fā)量預(yù)測項(xiàng)目真正難的不是搭模型而是把電站生產(chǎn)數(shù)據(jù)、天氣數(shù)據(jù)和TensorFlow代碼接起來以后系統(tǒng)還能穩(wěn)定運(yùn)行。功率序列有強(qiáng)自相關(guān)天氣突變時(shí)常發(fā)生單站樣本量又不像CV那樣動輒幾十萬張這些問題決定了落地路徑先處理兩類原始數(shù)據(jù)再用LSTM建序列模型最后做滾動外推驗(yàn)證。這個(gè)方案適合需要給調(diào)度、檢修提供“未來24小時(shí)發(fā)電預(yù)期”的工程師也適合已入門深度學(xué)習(xí)的Python開發(fā)者拿真實(shí)光伏數(shù)據(jù)走一遍完整流程。下面按這套預(yù)測源代碼的常見結(jié)構(gòu)從數(shù)據(jù)清洗、特征工程、TensorFlow建模講到最終投運(yùn)階段的坑位重在實(shí)際能復(fù)制到本地的細(xì)節(jié)。2. 數(shù)據(jù)基礎(chǔ)電站生產(chǎn)數(shù)據(jù)與天氣數(shù)據(jù)的清洗、對齊與特征工程2.1 光伏預(yù)測的兩類數(shù)據(jù)源先統(tǒng)一時(shí)間頻率再拼接光伏電站發(fā)量預(yù)測項(xiàng)目里數(shù)據(jù)一般由兩部分拼接而成。第一部分來自電站SCADA或逆變器采集系統(tǒng)常見字段有時(shí)間戳、實(shí)時(shí)功率(kW)、日累計(jì)發(fā)電量(kWh)、組件背板溫度(℃)采樣頻率大多是1分鐘或15分鐘。第二部分來自氣象站或氣象服務(wù)接口常用字段包括水平輻照度GHI、散射輻照度DHI、環(huán)境溫度、濕度、風(fēng)速、云量氣象數(shù)據(jù)通常是1小時(shí)一條。兩類數(shù)據(jù)頻率不一樣是動手后遇到的第一個(gè)坑不能直接把兩個(gè)DataFrame按時(shí)間戳橫向拼接因?yàn)楦哳l時(shí)間點(diǎn)上天氣列會大量落空。我一般先把功率數(shù)據(jù)重采樣到15分鐘天氣數(shù)據(jù)再對齊到同樣的15分鐘網(wǎng)格。功率用均值重采樣把1分鐘功率平均成15分鐘值符合調(diào)度口徑天氣則用前向填充。之所以不用線性插值是因?yàn)闅庀蠖虝r(shí)間變化往往是階躍式的插值會人為制造“云量逐漸變化”的假象模型會把這個(gè)假象學(xué)進(jìn)去到了真實(shí)突變天氣就預(yù)測偏。import pandas as pd # 功率重采樣到15分鐘天氣數(shù)據(jù)對齊并用前向填充補(bǔ)齊全空位 power_15 power_df.resample(15min).mean() weather_15 weather_df.resample(15min).ffill() merged power_15.join(weather_15, howinner) merged merged.dropna(subset[power_kw, ghi_w_m2])這里resample(15min).mean()會讓1分鐘功率聚合成15分鐘平均功率如果采集鏈路本身已經(jīng)做了均值處理那么讀取后用asfreq更合適避免二次平滑。ffill()保留氣象數(shù)據(jù)“上一小時(shí)的值延續(xù)到下一小時(shí)”的確定性代碼后可以順手看一眼合并后的索引是否連續(xù)常見的datetime索引重復(fù)會直接導(dǎo)致join行數(shù)膨脹。2.2 缺失值與異常功率樣本的處理原則電站實(shí)測數(shù)據(jù)里最常見的情況是夜間功率為0、低輻照時(shí)功率抖動、停電或傳感器故障導(dǎo)致連續(xù)缺失。處理原則是夜間零值是正常的不能當(dāng)異常刪白天輻照接近零但功率突然跳變的點(diǎn)大概率是計(jì)量故障。連續(xù)缺失不超過3個(gè)點(diǎn)線性插值可以接受超過3個(gè)點(diǎn)我傾向于直接丟棄該段因?yàn)殚L序列缺失會逼模型用上一段時(shí)間的邊界值硬湊。另一個(gè)必須做的操作是把功率除以額定裝機(jī)容量轉(zhuǎn)換為標(biāo)幺值pu。不同容量電站如果共用同一套代碼必須用pu而不是kW否則模型會把“電站容量大小”當(dāng)成特征學(xué)進(jìn)去。# 去除異常負(fù)功率再歸一化到額定容量 power_df[power_kw] power_df[power_kw].clip(lower0) power_df[power_pu] power_df[power_kw] / nominal_capacity_kw power_df power_df[power_df[power_pu].between(0.0, 1.2)]clip(lower0)只處理負(fù)數(shù)而不截?cái)嗌舷奘且驗(yàn)槌^1.2pu的讀數(shù)多為傳感器故障直接截?cái)鄷屇P驼`以為“超發(fā)到1.2封頂”是正常行為用篩選去掉這些樣本模型就只見過常態(tài)區(qū)間。2.3 特征表與時(shí)間編碼避免未來信息泄漏光伏功率與GHI之間接近線性這是預(yù)測的核心抓手但云層遮擋、組件溫升會讓關(guān)系曲線彎曲所以模型需要同時(shí)看到歷史功率、歷史天氣、未來時(shí)段的氣象預(yù)報(bào)。特征構(gòu)造的核心原則是歷史時(shí)點(diǎn)的特征用真實(shí)觀測值未來時(shí)點(diǎn)的氣象特征用天氣預(yù)報(bào)值絕不能在訓(xùn)練時(shí)把未來真實(shí)氣象值偷偷塞進(jìn)輸入。# 加入一天內(nèi)時(shí)刻和一年內(nèi)日期的正余弦編碼 merged[hour_sin] np.sin(2 * np.pi * merged.index.hour / 24) merged[hour_cos] np.cos(2 * np.pi * merged.index.hour / 24) merged[day_sin] np.sin(2 * np.pi * merged.index.dayofyear / 365) merged[day_cos] np.cos(2 * np.pi * merged.index.dayofyear / 365)下表是項(xiàng)目里最常用的一套特征組合按“訓(xùn)練用真實(shí)值/推理用預(yù)測值”做了區(qū)分特征組示例字段數(shù)據(jù)來源訓(xùn)練時(shí)取值推理時(shí)取值歷史出力power_pu電站SCADA真實(shí)值真實(shí)值實(shí)測氣象ghi_w_m2, temp_c, humidity, wind_speed氣象站真實(shí)值觀測值預(yù)報(bào)氣象ghi_fcst, cloud_fcst數(shù)值天氣預(yù)報(bào)實(shí)測值代替預(yù)報(bào)值時(shí)間編碼hour_sin/hour_cos, day_sin/day_cos時(shí)間戳構(gòu)造確定值確定值表格最后一行的時(shí)間編碼是純確定值可以放心加入。需要檢查的是“預(yù)報(bào)氣象”的字段如果訓(xùn)練和推理兩者分布差異大——比如預(yù)報(bào)GHI系統(tǒng)性強(qiáng)偏低——模型的白天峰值會被明顯壓低這種情況用歷史實(shí)測GHI訓(xùn)練得到的權(quán)重上線后還要做一次偏差校核。3. TensorFlow模型搭建光伏發(fā)量預(yù)測源代碼里的LSTM骨架與滑窗數(shù)據(jù)3.1 為什么光伏序列適合LSTM而不是普通回歸或Transformer線性回歸很難刻畫功率的時(shí)間連續(xù)性。光伏出力存在強(qiáng)自相關(guān)上午10點(diǎn)5分的輸出與10點(diǎn)幾乎一致卻和昨天同一時(shí)刻也高度相關(guān)。LSTM的門控機(jī)制能保留多尺度時(shí)序信息對云層快速過境后的功率驟降有一定遲滯感知能力。不選Transformer的理由更實(shí)際單站光伏樣本量通常在數(shù)萬條量級Transformer在這種數(shù)據(jù)規(guī)模下容易過擬合在線推理時(shí)的顯存占用也更大LSTM的訓(xùn)練速度和穩(wěn)定性更適合放到傳統(tǒng)服務(wù)器上定時(shí)跑批。3.2 用timeseries_dataset_from_array把生產(chǎn)數(shù)據(jù)切成Keras窗口構(gòu)造訓(xùn)練樣本的核心是(X, y)對X形狀為(樣本數(shù), 回看窗口長度, 特征數(shù))y形狀為(樣本數(shù), 預(yù)測步數(shù))。常規(guī)配置是回看過去24小時(shí)、預(yù)測未來24小時(shí)15分鐘一個(gè)點(diǎn)兩個(gè)長度都是96。窗口逐點(diǎn)滑動樣本量最大模型能覆蓋每天的起動、攀升、午間平頂、回落、歸零各階段如果內(nèi)存吃緊也可以把滑動步長換成4讓樣本稀疏但會損失細(xì)節(jié)擬合。import tensorflow as tf # 生成滑窗樣本一次給LSTM提供96個(gè)時(shí)間步 dataset tf.keras.utils.timeseries_dataset_from_array( dataX_all, targetsy_all, sequence_length96, sequence_stride1, batch_size256, shuffleTrue, seed42 )sequence_length96決定模型一次看到過去96個(gè)時(shí)點(diǎn)一個(gè)時(shí)點(diǎn)是15分鐘sequence_stride1讓窗口逐點(diǎn)滑動讓連續(xù)樣本之間有96個(gè)時(shí)間步的重疊shuffleTrue打亂的是樣本而不是時(shí)間序列內(nèi)部順序打亂后模型無法記住連續(xù)段落的順序反而能學(xué)到橫跨不同季節(jié)的普遍規(guī)律。3.3 從輸入張量到預(yù)測輸出的網(wǎng)絡(luò)結(jié)構(gòu)參數(shù)表雙LSTM接全連接輸出是這類預(yù)測最常見且最穩(wěn)的結(jié)構(gòu)。第一層return_sequencesTrue保留完整時(shí)間維第二層只輸出最后一個(gè)時(shí)刻的隱狀態(tài)中間夾Dropout防過擬合最后一層Dense直接映射到96個(gè)預(yù)測點(diǎn)。輸出層不加激活函數(shù)因?yàn)楣β驶貧w不是分類不應(yīng)限制在0到1之間。inputs tf.keras.Input(shape(96, X_all.shape[-1])) x tf.keras.layers.LSTM(128, return_sequencesTrue)(inputs) x tf.keras.layers.Dropout(0.2)(x) x tf.keras.layers.LSTM(64, return_sequencesFalse)(x) x tf.keras.layers.Dropout(0.2)(x) outputs tf.keras.layers.Dense(96)(x) model tf.keras.Model(inputs, outputs) model.compile(optimizeradam, lossmse, metrics[mae])層名輸出形狀說明Input(None, 96, feat_count)96個(gè)歷史時(shí)點(diǎn)feat_count由特征數(shù)量決定LSTM(128, return_sequencesTrue)(None, 96, 128)第一層保留完整時(shí)間步給第二層Dropout(0.2)(None, 96, 128)訓(xùn)練時(shí)隨機(jī)丟棄20%降低過擬合LSTM(64, return_sequencesFalse)(None, 64)只保留最后時(shí)刻壓縮時(shí)間信息Dense(96)(None, 96)一次性輸出未來96個(gè)點(diǎn)的功率預(yù)測這里采用“直接多步預(yù)測”而不是“遞歸預(yù)測”的原因需要說明遞歸是把上一步輸出當(dāng)作下一步輸入誤差會沿著96步逐漸累積日出階段容易偏得離譜直接多步輸出一次性生成未來96點(diǎn)誤差分布更平坦雖然需要更多訓(xùn)練數(shù)據(jù)捕捉輸出的多樣性但光伏樣本通常足夠。4. 訓(xùn)練管線與參數(shù)調(diào)優(yōu)歸一化切分、回調(diào)設(shè)置和誤差指標(biāo)分析4.1 先切分再歸一化順序顛倒就是數(shù)據(jù)泄漏訓(xùn)練前必須先切分再歸一化這個(gè)順序比很多參數(shù)都重要。如果先對全體數(shù)據(jù)計(jì)算均值和方差測試集的分布信息就進(jìn)入了訓(xùn)練過程驗(yàn)證誤差會偏低真實(shí)首日預(yù)測掉精度。切分必須按時(shí)間順序前70%訓(xùn)練中間15%驗(yàn)證最后15%測試。光伏數(shù)據(jù)有季節(jié)結(jié)構(gòu)隨機(jī)抽樣會讓模型看到“未來才出現(xiàn)的夏天”等于考試時(shí)偷看答案。from sklearn.preprocessing import StandardScaler, MinMaxScaler n_total len(features) n_train int(n_total * 0.7) n_val int(n_total * 0.85) scaler_X StandardScaler().fit(features[:n_train]) scaler_y MinMaxScaler().fit(labels[:n_train]) X_scaled scaler_X.transform(features) y_scaled scaler_y.transform(labels)特征X使用StandardScaler而不是MinMax因?yàn)镚HI和風(fēng)速的長尾分布明顯零均值單位方差對偶發(fā)極端天氣更穩(wěn)輸出y使用MinMax映射到0~1附近適配MSE損失。推理時(shí)必須把預(yù)測結(jié)果用inverse_transform還原成功率值再上報(bào)漏掉這一步會得到一組永遠(yuǎn)小于1的“神秘?cái)?shù)字”。4.2 訓(xùn)練參數(shù)設(shè)置與EarlyStopping、ReduceLROnPlateau的使用Adam初始學(xué)習(xí)率給1e-3先跑10輪看Loss是否震蕩震蕩就降到5e-4。同時(shí)掛EarlyStopping和ReduceLROnPlateau兩個(gè)回調(diào)一般能在20到40輪內(nèi)收住。early_stop tf.keras.callbacks.EarlyStopping( monitorval_mae, patience12, restore_best_weightsTrue ) reduce_lr tf.keras.callbacks.ReduceLROnPlateau( monitorval_mae, factor0.5, patience3 ) history model.fit( train_ds, validation_dataval_ds, epochs100, callbacks[early_stop, reduce_lr], verbose2 )patience12表示驗(yàn)證集MAE連續(xù)12輪沒有改善就停止訓(xùn)練restore_best_weightsTrue會在停止時(shí)把權(quán)重回滾到最優(yōu)那一輪避免最后幾輪過擬合權(quán)重被保存。ReduceLROnPlateau的patience3是獨(dú)立判斷條件驗(yàn)證指標(biāo)連續(xù)3輪不降時(shí)學(xué)習(xí)率減半讓收斂末期可以用更小步長精細(xì)調(diào)整。下面是這類項(xiàng)目里推薦的一組訓(xùn)練參數(shù)配置直接照抄也能跑通超參數(shù)配置說明optimizerAdam無需額外配置自帶自適應(yīng)學(xué)習(xí)率learning_rate1e-3起步震蕩降到3e-4光伏Loss曲面相對平滑不建議用大于1e-2的值batch_size256樣本多時(shí)穩(wěn)定顯存占用小epochs100 EarlyStopping防止訓(xùn)練時(shí)間浪費(fèi)在過擬合區(qū)間lossmse對峰值誤差懲罰更大比mae更貼合功率預(yù)測metricsmae便于直觀對比驗(yàn)證集誤差4.3 白天時(shí)段單獨(dú)評估MAE與NRMSE的計(jì)算方式夜間出力全為0如果算全時(shí)段RMSE零值會把誤差稀釋掉模型白天不敢出力的問題反而被淹沒了。行業(yè)里通常看白天誤差以GHI大于50W/m2為分界統(tǒng)計(jì)這一時(shí)段的平均絕對誤差MAE以及均方根誤差除以該時(shí)段平均功率得到的NRMSE。MAE反映每一刻預(yù)測與實(shí)際的貼近程度NRMSE反映整體曲線形狀的擬合度。day_mask test_df[ghi_w_m2].values 50 y_true_day y_true[day_mask] y_pred_day y_pred[day_mask] mae_day float(np.mean(np.abs(y_true_day - y_pred_day))) nrmse float(np.sqrt(np.mean((y_true_day - y_pred_day) ** 2)) / (np.mean(y_true_day) 1e-6)) print(f白天MAE{mae_day:.4f} pu, NRMSE{nrmse:.3f})代碼里分母加上1e-6是為了防止白天功率恰好為0時(shí)除零。我常看到的一種誤用是把MAE直接除以額定容量得出“預(yù)測準(zhǔn)確率98%”的數(shù)字這個(gè)數(shù)字忽略了夜間大量零樣本報(bào)到調(diào)度系統(tǒng)會嚴(yán)重高估能力。5. 預(yù)測投運(yùn)滾動外推驗(yàn)證與日出時(shí)刻修正技巧5.1 滾動窗口重預(yù)測還原真實(shí)調(diào)度過程中的誤差模型在測試集上一次性預(yù)測所有96個(gè)點(diǎn)并不能代表投運(yùn)效果。真實(shí)系統(tǒng)每天每隔15分鐘刷新一次未來24小時(shí)預(yù)測所以我習(xí)慣用滾動窗口重預(yù)測來做仿真起始位置取歷史真實(shí)數(shù)據(jù)作為輸入預(yù)測未來96個(gè)點(diǎn)隨后把輸入窗口向后滑動96步用新的真實(shí)數(shù)據(jù)再算下一輪。這種評估方式與部署后的首次預(yù)測流程完全一致。pred_collection [] for start_idx in range(0, len(X_scaled_test) - 96, 96): x_input X_scaled_test[start_idx:start_idx 96] x_batch np.expand_dims(x_input, axis0) y_pred scaler_y.inverse_transform( model.predict(x_batch, verbose0) ).flatten() pred_collection.append(y_pred)inverse_transform把模型的0~1輸出還原回kW再除以額定容量轉(zhuǎn)成pu后畫圖對比能直觀看到連續(xù)十幾天的誤差累積情況。滾動評估得到的誤差會明顯高于單次預(yù)測這是正常現(xiàn)象不要因此懷疑模型寫錯(cuò)對比時(shí)建議把全時(shí)段MAE、白天MAE、NRMSE列成一張表來看滾動過程的第7天、第30天通常因天氣系統(tǒng)變化比頭幾天更差這屬于正常漣漪反應(yīng)不要為了數(shù)據(jù)平滑盲目加大正則項(xiàng)。5.2 日出前后的低出力修正十幾行代碼的啟發(fā)式后處理日出前后12小時(shí)是錯(cuò)誤最集中的時(shí)段模型往往延續(xù)前夜低功率輸出忘記此時(shí)輻照已經(jīng)抬升。項(xiàng)目里的簡單做法是用ephem或pvlib算出預(yù)測日當(dāng)?shù)氐睦碚撊粘鰰r(shí)刻當(dāng)預(yù)測日排在日出后但模型輸出接近0同時(shí)GHI預(yù)報(bào)大于200W/m2時(shí)按前一日同時(shí)段的功率曲線做線性抬升。if pred_time sunrise_minutes and predicted_pu 0.02: if ghi_fcst 200: correction yesterday_curve[start_idx] * 0.6 predicted_pu max(predicted_pu, correction)這里的0.6是保守系數(shù)目的是讓模型在晴天早晨至少能動起來而不是給出一個(gè)全天為0的預(yù)測。日出后一個(gè)小時(shí)真實(shí)出力快速爬升模型學(xué)到的歷史趨勢會接管輸出啟發(fā)式修正的影響自然消失。這個(gè)技巧在陰天轉(zhuǎn)晴的早晨效果非常明顯代碼量不過十來行值得直接收進(jìn)預(yù)測服務(wù)的后處理管線。本文還有配套的精品資源點(diǎn)擊獲取