ARIMA-CNN-LSTM組合模型:時序預(yù)測殘差校正實戰(zhàn))
做時間序列預(yù)測這幾年我最大的感受是模型的組合不是把算法往一個文件里堆而是讓每一層模型干它最擅長的事。這次做的“ARIMA-CNN-LSTM預(yù)測模型”我用Python把三個模型串成了一個完整的pipeline——ARIMA負(fù)責(zé)抓線性趨勢和季節(jié)性骨架CNN負(fù)責(zé)從局部窗口里挖波形特征LSTM負(fù)責(zé)把時序依賴存下來并輸出殘差預(yù)測最后把三者的結(jié)果合成最終的預(yù)測值。這套組合非常適合有明顯周期性、又有非線性波動的日粒度或小時粒度數(shù)據(jù)比如區(qū)域電力負(fù)荷、空氣質(zhì)量、交通流量、商品銷量。如果你手里的序列是純線性、純平穩(wěn)的直接用ARIMA或者SARIMA就夠了用不著上深度學(xué)習(xí)但一旦數(shù)據(jù)里有尖峰、突變、復(fù)雜的周期疊加ARIMACNN-LSTM的容錯能力會明顯好過任何單模型。這篇文章我會從模型分工談起兼顧每一步的代碼實現(xiàn)和實驗對比適合已經(jīng)會基礎(chǔ)Python、想自己搭一套組合時序模型的讀者。1. 為什么是“ARIMACNNLSTM”這個組合三個模型的分工與銜接很多人看到這種組合名第一反應(yīng)是“把三個模型的結(jié)果平均一下”。如果你真這么做了大概率會得到一坨平庸的預(yù)測。正確的問題是這三個模型各自擅長什么信號在什么樣的輸入上它們能發(fā)揮最大價值以及它們之間的信息應(yīng)該怎么接力而不是撞車。1.1 ARIMA的強(qiáng)項與死穴線性趨勢和季節(jié)性的原住民ARIMA的全稱是差分自回歸移動平均模型。它的核心思路是用過去觀測值的線性組合自回歸項、過去預(yù)測誤差的線性組合移動平均項再加上d階差分來描述一個時間序列。這個模型有非常強(qiáng)的可解釋性對樣本量的要求也比深度學(xué)習(xí)低得多。拿我這次用的電力負(fù)荷數(shù)據(jù)來說日負(fù)荷有明顯的周周期性工作日高、周末低和年周期性夏冬高、春秋低ARIMA配合適當(dāng)?shù)牟罘挚梢院芎玫刈プ∵@類趨勢骨架。但ARIMA有一個天然的死穴它對非線性突變基本無能為力。比如寒潮突然來襲空調(diào)負(fù)荷在兩天內(nèi)猛增ARIMA用歷史同期的均值去外推必然低估這個尖峰。它的預(yù)測在遠(yuǎn)期甚至?xí)諗康揭粭l平坦的直線這是模型特性不是你寫錯了代碼。1.2 CNN在時間序列里到底在提取什么CNN做圖像識別大家都熟但一維卷積用在時間序列上干的其實是同一件事在局部窗口內(nèi)提取模式。假設(shè)卷積核大小是3它每次掃描三個連續(xù)時刻的值本質(zhì)上就是在拿一個長度為3的模板去匹配波形——尖峰、拐點、平臺段、快速上升沿這些局部的形態(tài)特征都能被不同的卷積核響應(yīng)出來。在這個組合里CNN負(fù)責(zé)的是“從短窗口里找形狀”。你可以用不同大小的卷積核并行提取不同尺度的形態(tài)比如kernel_size3抓極短期的拐點kernel_size7抓稍長的波動形態(tài)再把兩者拼接起來這就是多尺度CNN的基本思路。CNN還有一個隱藏的好處通過卷積和池化它能把input序列長度壓縮讓后面的LSTM面對更短、更抽象的時間步訓(xùn)練負(fù)擔(dān)小很多。1.3 LSTM長短期記憶到底記住了什么LSTM是RNN的改進(jìn)版本通過遺忘門、輸入門、輸出門三個門控結(jié)構(gòu)來控制信息的保留和丟棄。用人話說它能夠在很長的序列里記住“哪些歷史信息值得保留”同時避免RNN在反向傳播時梯度消失的問題。但單用LSTM做預(yù)測也有麻煩它對強(qiáng)趨勢和強(qiáng)季節(jié)性成分的學(xué)習(xí)速度很慢經(jīng)常要在訓(xùn)練后期才慢慢追上而且LSTM對超參數(shù)非常敏感units數(shù)量、層數(shù)、dropout比例、學(xué)習(xí)率任何一個設(shè)置不對不是欠擬合就是過擬合。如果直接把原始序列扔給LSTM它需要同時消化線性趨勢、季節(jié)波動和非線性殘差任務(wù)太重容易學(xué)成一個“只會把昨天數(shù)值抄到明天”的滯后預(yù)測器。1.4 我的組合方式串聯(lián)殘差校正而不是簡單平均組合模型常見有兩種架構(gòu)。第一種是并聯(lián)集成每個模型獨立預(yù)測然后加權(quán)平均。這種方式看著簡單但有兩個問題——如果幾個模型的誤差相關(guān)性很高平均之后提升很有限ARIMA的線性強(qiáng)假設(shè)還會把整體的預(yù)測方差壓得過于保守。第二種是串聯(lián)殘差校正也是我這次采用的方案。核心邏輯是先用ARIMA解釋序列里的線性主成分然后把ARIMA擬合后剩下的殘差序列交給CNN-LSTM去學(xué)習(xí)。最終預(yù)測值等于ARIMA的預(yù)測值加上CNN-LSTM對殘差的預(yù)測值y_hat(t) ARIMA_forecast(t) CNNLSTM_forecast(e(t))其中 e(t) y(t) - ARIMA_fit(t)。這個設(shè)計就像配眼鏡先根據(jù)度數(shù)定一個大致的球鏡再用插片精調(diào)散光。ARIMA負(fù)責(zé)把眼睛能看到的大框架定下來CNN-LSTM專門去補(bǔ)那些ARIMA看不到的散光細(xì)節(jié)。如果你把散光直接混在度數(shù)里一次性配反而哪個都搞不干凈。2. 數(shù)據(jù)準(zhǔn)備差分、歸一化、滑窗這三步藏著最多的坑數(shù)據(jù)準(zhǔn)備這一步?jīng)Q定了模型預(yù)測質(zhì)量的80%。很多人模型訓(xùn)練半天效果不好回頭排查才發(fā)現(xiàn)是數(shù)據(jù)預(yù)處理階段埋了雷。2.1 數(shù)據(jù)劃分時間序列不能隨機(jī)打亂我這次用的是某地區(qū)公開的日平均電力負(fù)荷數(shù)據(jù)一共三年粒度到天。劃分比例是訓(xùn)練集70%、驗證集15%、測試集15%嚴(yán)格保持時間順序。這里要特別提醒訓(xùn)練集、驗證集、測試集必須按時間先后切分絕不能隨機(jī)抽樣。交叉驗證也最好不要用普通的K折否則未來的數(shù)據(jù)會跑到訓(xùn)練集里指標(biāo)會虛高得讓你誤以為模型是神。我通常在pandas里這么劃import pandas as pd df pd.read_csv(load.csv, parse_dates[date], index_coldate) n len(df) train_end int(n * 0.7) val_end int(n * 0.85) train_df df.iloc[:train_end] val_df df.iloc[train_end:val_end] test_df df.iloc[val_end:]2.2 平穩(wěn)性檢驗與差分ADF的p值不是看熱鬧ARIMA要求序列平穩(wěn)。最常用的檢驗是ADF檢驗原假設(shè)是“序列存在單位根”。如果p值大于0.05說明序列非平穩(wěn)需要差分。from statsmodels.tsa.stattools import adfuller load_values df[load].values adf_result adfuller(load_values) print(ADF p-value:, adf_result[1])對日粒度數(shù)據(jù)我一般會先檢查是否需要一階差分再看看是否需要季節(jié)性差分比如周周期做7步差分。我在項目里做了一步差分之后p值已經(jīng)很小就繼續(xù)走了。如果你發(fā)現(xiàn)季節(jié)性很強(qiáng)可以考慮用SARIMA或者直接在ARIMA的order里處理但要注意差分階數(shù)別過頭過度差分會讓序列丟失真實信號的幅度信息。2.3 歸一化與滑窗構(gòu)造先切后scale順序不能錯歸一化我用的是MinMaxScaler把數(shù)據(jù)壓縮到0到1之間。這一步有兩個關(guān)鍵點。第一必須先劃分?jǐn)?shù)據(jù)集再在訓(xùn)練集上計算MinMaxScaler的min和max然后用同一個scaler去transform驗證集和測試集絕對不能在整個數(shù)據(jù)集上先fit再切分。否則測試集的極值信息會滲透進(jìn)歸一化參數(shù)等于模型在訓(xùn)練時就偷看了未來數(shù)據(jù)這種數(shù)據(jù)泄漏會讓你跑測試時指標(biāo)漂亮上線后一塌糊涂。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() train_scaled scaler.fit_transform(train_df[[load]]) val_scaled scaler.transform(val_df[[load]]) test_scaled scaler.transform(test_df[[load]])第二滑窗構(gòu)造必須發(fā)生在歸一化之后。我用的窗口長度look_back30意思是拿過去30天的負(fù)荷值預(yù)測下一天。如果你的數(shù)據(jù)有明顯周期強(qiáng)烈建議窗口長度至少覆蓋一個完整周期——日粒度數(shù)據(jù)最好用7的倍數(shù)30也還行能覆蓋一個月左右的局部形態(tài)。import numpy as np def create_sequences(data, look_back30): X, y [], [] for i in range(len(data) - look_back): X.append(data[i:i look_back]) y.append(data[i look_back]) return np.array(X), np.array(y) X_train, y_train create_sequences(train_scaled, look_back30) X_val, y_val create_sequences(val_scaled, look_back30) X_test, y_test create_sequences(test_scaled, look_back30)構(gòu)造出來的X維度是樣本數(shù), look_back, 1這個三維結(jié)構(gòu)是Conv1D和LSTM的標(biāo)準(zhǔn)輸入格式第一維是樣本第二維是時間步第三維是特征數(shù)。很多人第一次寫的時候漏了reshape成三維直接報錯這里提前打好預(yù)防針。3. ARIMA部分定階、擬合、殘差提取別急著把數(shù)據(jù)丟給神經(jīng)網(wǎng)絡(luò)很多人做組合模型的時候ARIMA部分就是走個過場隨便order(2,1,2)一擬合就往下跑了。但事實上ARIMA擬合得越干凈后面CNN-LSTM拿到的殘差就越接近一個“純非線性波動序列”學(xué)習(xí)起來越容易。ARIMA定階的目的就是要讓線性成分被盡可能充分地解釋掉。3.1 定階先用圖做初判再用AIC網(wǎng)格搜索ARIMA的階數(shù)由(p,d,q)決定。p是自回歸階數(shù)d是差分階數(shù)q是移動平均階數(shù)。教科書會教你畫ACF和PACF圖看截尾和拖尾特征來定p和q。實際項目里我很少只靠看圖因為數(shù)據(jù)一復(fù)雜圖往往解讀不出唯一答案。我的做法是先用ACF/PACF看個大概范圍然后用網(wǎng)格搜索在AIC最小的原則下選階。import itertools import warnings from statsmodels.tsa.arima.model import ARIMA warnings.filterwarnings(ignore) p q range(0, 6) d [1] best_aic float(inf) best_order None for order in itertools.product(p, d, q): try: model ARIMA(train_scaled, orderorder) fitted model.fit() if fitted.aic best_aic: best_aic fitted.aic best_order order except Exception: continue print(best order:, best_order, AIC:, best_aic)AIC和BIC都是越小越好區(qū)別在于BIC對參數(shù)數(shù)量的懲罰更重。時序預(yù)測的樣本量通常不大參數(shù)太多容易過擬合所以我習(xí)慣在AIC相近的情況下優(yōu)先選參數(shù)更少的模型也就是“簡約優(yōu)先”。3.2 擬合與預(yù)測注意fittedvalues開頭有NaN選好階數(shù)之后在訓(xùn)練集上重新擬合模型然后對測試期的每一步做預(yù)測。model ARIMA(train_scaled, orderbest_order) arima_fit model.fit() # 訓(xùn)練集擬合值注意差分后開頭幾個值可能為NaN train_fitted arima_fit.fittedvalues.copy() train_fitted[np.isnan(train_fitted)] train_scaled[:sum(np.isnan(train_fitted))].flatten() # 預(yù)測測試期長度 n_test len(test_scaled) arima_forecast arima_fit.forecast(stepsn_test)很多人在這一步會踩坑fittedvalues的長度和原始訓(xùn)練集一致但前幾個位置因為差分計算被填了NaN。如果不處理后面計算殘差時會帶著一堆NaN進(jìn)入深度學(xué)習(xí)模型Keras會直接把loss算成nan然后你的手冊上就多了一條“模型不收斂”的排查記錄。處理方式很簡單用訓(xùn)練集開頭的原始值填充這些NaN。3.3 殘差提取這就是CNN-LSTM的“教材”ARIMA擬合完之后訓(xùn)練集的殘差就是真實值減去擬合值residual_train train_scaled.flatten() - train_fitted.flatten()這里的residual_train就是CNN-LSTM要學(xué)習(xí)的對象。它和原始負(fù)荷值有一個很大的不同序列的線性趨勢已經(jīng)被ARIMA拿走了所以殘差的均值通常非常接近0波動幅度也相對穩(wěn)定更像一個“零均值波動包絡(luò)”。LSTM對這種序列的收斂速度快很多因為不需要同時兼顧趨勢項和季節(jié)項只需學(xué)習(xí)波動的局部規(guī)律。坦白講如果是純線性序列殘差就等于白噪聲CNN-LSTM學(xué)了也沒用但在真實電力負(fù)荷、交通流量這類數(shù)據(jù)里殘差里往往還藏著氣溫、節(jié)假日、突發(fā)事件等因素驅(qū)動的非線性模式——這才是組合模型能獲得收益的根本來源。4. Python代碼實現(xiàn)CNN-LSTM模型結(jié)構(gòu)、訓(xùn)練與滾動預(yù)測核心邏輯ARIMA的部分跑完之后接下來就是CNN-LSTM登場。這一章的代碼是整個項目的核心我會把網(wǎng)絡(luò)結(jié)構(gòu)、訓(xùn)練配置、滾動預(yù)測三步拆開講并且每一步都說明為什么這么設(shè)計。4.1 網(wǎng)絡(luò)結(jié)構(gòu)設(shè)計Conv1D壓縮LSTM記憶Dense輸出我用的是Keras的Sequential模型網(wǎng)絡(luò)拓?fù)淙缦聦虞敵鲂螤钫f明Input隱含(30, 1)30個時間步1個特征Conv1D(filters64, kernel_size3)(28, 64)提取長度為3的局部波形特征Conv1D(filters32, kernel_size3)(26, 32)第二層卷積進(jìn)一步抽象特征MaxPooling1D(pool_size2)(13, 32)壓縮時間步保留主要響應(yīng)LSTM(units64)64學(xué)習(xí)壓縮后的時序依賴Dropout(0.2)64緩解過擬合Dense(units32, activationrelu)32非線性變換Dense(units1)1輸出殘差預(yù)測值這里Conv1D的kernel_size取3是一個很常規(guī)的起點。如果你想要多尺度可以做兩條卷積分支一條kernel_size3一條kernel_size7然后在后面拼接理論上能同時捕捉短突變和中長度波動。我在主模型里用單尺度多尺度作為后續(xù)擴(kuò)展我在第七章再講。4.2 模型定義代碼from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense, Dropout look_back 30 model Sequential([ Conv1D(filters64, kernel_size3, activationrelu, input_shape(look_back, 1)), Conv1D(filters32, kernel_size3, activationrelu), MaxPooling1D(pool_size2), LSTM(units64, return_sequencesFalse), Dropout(0.2), Dense(units32, activationrelu), Dense(units1) ])需要解釋一下為什么卷積之后沒有立刻接Flatten再接LSTM而是讓LSTM直接吃卷積的輸出。因為一維卷積輸出的維度依然是樣本數(shù)時間步通道數(shù)時間步這個維度還在LSTM可以把每個通道在時間步上的響應(yīng)串起來學(xué)習(xí)依賴關(guān)系。如果用了Flatten時間維度就丟了LSTM就沒有意義。我第一次在這個項目之前也犯過這個錯誤白白增加參數(shù)量還掉點。4.3 訓(xùn)練配置早停、學(xué)習(xí)率衰減、shuffleFalse編譯和訓(xùn)練配置是這套代碼里最容易忽略的部分。from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau from tensorflow.keras.losses import MeanSquaredError model.compile(optimizerAdam(learning_rate0.001), lossMeanSquaredError()) early_stop EarlyStopping(monitorval_loss, patience15, restore_best_weightsTrue) reduce_lr ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-5) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size32, callbacks[early_stop, reduce_lr], shuffleFalse )這里有幾個經(jīng)驗。第一損失函數(shù)用MSE因為預(yù)測任務(wù)輸出是連續(xù)值MSE對大的誤差懲罰更重這正好符合我們“盡量避免較大偏差”的業(yè)務(wù)直覺。第二EarlyStopping必須開patience我設(shè)在15等驗證集loss連續(xù)15輪不降就停下并且restore_best_weights恢復(fù)到驗證集最好的那輪權(quán)重。第三也是最關(guān)鍵的fit函數(shù)默認(rèn)shuffleTrue記得顯式設(shè)成False。時間序列一旦被打亂訓(xùn)練集和驗證集之間的時間依賴關(guān)系就斷了模型的記憶能力就會被無效化。我在跑這個模型的時候訓(xùn)練集loss前10個epoch下降得很快但驗證集loss在20輪以后就開始震蕩如果不加早停后面驗證loss一定會反彈——這是很典型的過擬合信號dropout和早停一起上陣才穩(wěn)得住。4.4 滾動預(yù)測讓模型像在真實環(huán)境里一樣工作CNN-LSTM訓(xùn)練好之后有人直接用model.predict(X_test)一把梭。這樣做的預(yù)測效果往往很差因為測試集的X是由測試集真實值構(gòu)成的窗口而測試集真實值本來就是要預(yù)測的未來值。正確的做法是滾動預(yù)測給定訓(xùn)練期最后look_back個殘差作為初始窗口逐日預(yù)測每次把新預(yù)測出的殘差拼接進(jìn)窗口推出下一天。def recursive_predict(model, init_window, n_steps): preds [] window init_window.copy().reshape(1, look_back, 1) for _ in range(n_steps): p model.predict(window, verbose0)[0, 0] preds.append(p) window np.append(window[:, 1:, :], np.array([[[p]]]), axis1) return np.array(preds) # 用訓(xùn)練集殘差最后look_back個值作為初始窗口 init_window residual_train[-look_back:] residual_forecast recursive_predict(model, init_window, n_test)滾動預(yù)測的代價是誤差會累積但因為殘差序列本身均值接近0、波動相對平穩(wěn)累積速度可控。如果你覺得累積太嚴(yán)重可以考慮多步直接預(yù)測也就是把最后的Dense層改成輸出h個時間步相當(dāng)于seq2seq但這樣會犧牲單步精度。我在項目里先保證單步精度滾動預(yù)測能接受。5. 實驗對比與結(jié)果解讀提升到底來自哪里模型跑完不能只貼一張loss曲線就說“效果很好”。我習(xí)慣用四個指標(biāo)聯(lián)合看RMSE、MAE、MAPE、R2。這四個指標(biāo)各有側(cè)重RMSE放大誤差大的樣本的懲罰MAE更穩(wěn)健MAPE看相對偏差R2看模型對總體波動的解釋力。5.1 評價指標(biāo)的實現(xiàn)def rmse(y_true, y_pred): return np.sqrt(np.mean((y_true - y_pred) ** 2)) def mae(y_true, y_pred): return np.mean(np.abs(y_true - y_pred)) def mape(y_true, y_pred): return np.mean(np.abs((y_true - y_pred) / y_true)) * 100 def r2(y_true, y_pred): ss_res np.sum((y_true - y_pred) ** 2) ss_tot np.sum((y_true - np.mean(y_true)) ** 2) return 1 - ss_res / ss_tot注意MAPE有個明顯問題如果真實值里有0或者接近0的數(shù)MAPE會直接爆炸。電力負(fù)荷是正數(shù)且不會有接近0的值所以能用。你要是換成PM2.5濃度也得小心那些接近0的樣本會主導(dǎo)MAPE讓指標(biāo)失真。5.2 同一份測試集上的實測結(jié)果以下數(shù)字來自我在本地負(fù)荷數(shù)據(jù)上的一次典型實驗結(jié)果。測試集沒有被模型在訓(xùn)練時見過所有結(jié)果都是滾動預(yù)測得到的。模型RMSEMAEMAPER2ARIMA-only15.6211.482.94%0.912LSTM-only13.7810.022.56%0.931CNN-LSTM11.358.422.13%0.948ARIMA-CNN-LSTM9.276.981.74%0.965從這張表能看出三件事。第一單用ARIMA在具有明顯非線性波動的負(fù)荷數(shù)據(jù)上確實是最弱的它的RMSE比CNN-LSTM高了整整4個點。第二CNN-LSTM比只用LSTM好說明卷積層提取的局部特征對LSTM是有正貢獻(xiàn)的。第三組合模型在全部四個指標(biāo)上都領(lǐng)先但提升幅度并不是“從90分到100分”的暴力提升而是相對穩(wěn)健的幾個百分點。5.3 提升到底來自哪里我仔細(xì)對比了預(yù)測曲線組合模型最大的收益不是把每個點的誤差都變小而是把峰和谷的位置踩得更準(zhǔn)。ARIMA預(yù)測的曲線普遍“太平”該尖的地方尖不起來CNN-LSTM單跑的時候某些尖峰擬合得很好但周期性位置會偏。組合之后ARIMA提供了周期骨架CNN-LSTM在里面填尖峰細(xì)節(jié)兩者分擔(dān)了不同的誤差來源。如果看到效果一般的情況也不要覺得模型沒用。有一個很現(xiàn)實的原因如果原始序列的線性成分占比本來就很低ARIMA能解釋的部分有限組合模型和純CNN-LSTM的差距就會非常小甚至因為誤差疊加而更差。所以這個組合適合的是“線性骨架非線性細(xì)節(jié)”并存的數(shù)據(jù)。做實驗之前先看一眼序列形態(tài)再做決定。6. 復(fù)現(xiàn)時最容易踩的6個坑與排查建議單獨模型你可能已經(jīng)跑得很順一旦進(jìn)入組合模型各種莫名其妙的問題就冒出來了。我把自己實際踩過的坑按頻率從高到低列出來。6.1 在歸一化時偷看了未來數(shù)據(jù)這是最隱蔽、也最坑的一個。如果代碼寫成“先把整個數(shù)據(jù)集歸一化再劃分訓(xùn)練集”測試集的min/max已經(jīng)進(jìn)入了訓(xùn)練階段使用的scaler參數(shù)。這樣模型在訓(xùn)練時實際上已經(jīng)知道了測試集的取值范圍測試集上RMSE會很好看但換了新數(shù)據(jù)立馬現(xiàn)原形。正確做法在第二章已經(jīng)強(qiáng)調(diào)過先切分再fit訓(xùn)練集scaler再transform驗證和測試集。排查方法也簡單訓(xùn)練時記錄的scaler.data_min_和data_max_對比一下是否包含測試集樣本的極值。如果包含了說明代碼順序?qū)戝e了。6.2 差分還原的順序錯誤如果你用的是ARIMA內(nèi)部差分statsmodels會自動處理不需要你手動還原。但如果你自己做了差分再預(yù)測完要還原時很多人會在累加順序上出錯。比如一階差分的還原是 cumulative_sum 第一個真實值如果你用cumsum時方向反了或者把第一個值的位置搞錯預(yù)測值會整體偏移。以我個人的經(jīng)驗最穩(wěn)妥的辦法是讓數(shù)據(jù)統(tǒng)一在歸一化空間里處理ARIMA、殘差、CNN-LSTM都在這個空間運(yùn)行最后只有最終預(yù)測結(jié)果需要scaler.inverse_transform。這樣能少一層單位換算的出錯風(fēng)險。6.3 滑動窗口導(dǎo)致訓(xùn)練和驗證樣本數(shù)量驟減look_back30意味著每個序列會丟掉前30條不能作為樣本。如果你的數(shù)據(jù)只有300條滑窗后訓(xùn)練集可能只剩下180條這會讓LSTM很快過擬合。我的建議是數(shù)據(jù)量少于2000條時look_back不要設(shè)太大如果必須用30可以考慮用步長為1的滑窗同時配合早停和dropout。樣本量太少的時候LSTM的優(yōu)勢發(fā)揮不出來CNN-LSTM也會變得不穩(wěn)定。6.4 預(yù)測結(jié)果“滯后一拍”這是時序預(yù)測里最經(jīng)典的現(xiàn)象預(yù)測曲線和真實曲線看起來很像仔細(xì)一看預(yù)測值比真實值晚了一到兩天。這種模型其實學(xué)到的只是“把昨天的值復(fù)制到今天”本質(zhì)上不是一個有效預(yù)測器RMSE看著還不錯但沒有任何業(yè)務(wù)意義。排查滯后問題有個直觀方法把預(yù)測值和真實值做滯后相關(guān)性分析計算預(yù)測值跟真實值錯位0天、1天、2天的相關(guān)系數(shù)。如果錯位1天的相關(guān)性明顯高于錯位0天說明模型已經(jīng)在做滯后復(fù)制了。我遇到這種情況通常是模型結(jié)構(gòu)太簡單或者數(shù)據(jù)里的噪聲太大解決辦法是適當(dāng)加大卷積核、增加LSTM的容量同時檢查數(shù)據(jù)有沒有被過度平滑。有趣的是ARIMA-CNN-LSTM組合在這個問題上反而比純CNN-LSTM好很多因為ARIMA的線性預(yù)測本身對趨勢外推有正向作用能抵消一部分滯后慣性。6.5 隨機(jī)種子沒有固定結(jié)果一天一個樣深度學(xué)習(xí)模型的結(jié)果和隨機(jī)種子高度相關(guān)這是一個常被忽視的復(fù)現(xiàn)問題。Keras里要同時固定三個地方Python的random庫、NumPy的seed、TensorFlow的seed。import random import numpy as np import tensorflow as tf SEED 42 random.seed(SEED) np.random.seed(SEED) tf.random.set_seed(SEED)加了這三行之后同一臺機(jī)器上相同環(huán)境下訓(xùn)練出來的結(jié)果基本可以復(fù)現(xiàn)。但也要說明白換一臺機(jī)器或換GPU型號后結(jié)果仍然會有微小差異這是浮點計算的正?,F(xiàn)象不是模型不穩(wěn)定別浪費時間追求百分百一致。6.6 驗證集合被shuffle搞亂Keras的fit默認(rèn)會在每個epoch前做一次shuffle這是為了訓(xùn)練隨機(jī)梯度下降的穩(wěn)定性。但時間序列一旦被打亂模型就會看到“昨天預(yù)測后天”這種錯亂的時間關(guān)系驗證集和訓(xùn)練集之間的順序依賴被徹底破壞。設(shè)shuffleFalse之后我立刻看到驗證集loss曲線變得連續(xù)、可解釋早停判斷也靠譜了。這個坑小但影響非常大。7. 還能怎么改從ARIMA-CNN-LSTM發(fā)散出去最后聊點擴(kuò)展方向。ARIMA-CNN-LSTM這套框架其實是一個“線性骨架非線性細(xì)節(jié)”的通用范式你完全可以在每個環(huán)節(jié)換裝。第一ARIMA可以升級成SARIMA。如果數(shù)據(jù)有強(qiáng)季節(jié)性SARIMA加入季節(jié)性差分項P,D,Q,s能更徹底地抽走季節(jié)成分殘差里剩下的非線性信息會更純粹。代價是網(wǎng)格搜索參數(shù)空間變大訓(xùn)練時間增加但預(yù)測精度通常有提升。第二CNN部分可以改成多尺度卷積。前面提到的雙分支并行一條kernel_size3一條kernel_size7合并后接池化能讓模型同時捕捉短突變和中長波動。如果你的數(shù)據(jù)既有小時級別的跳變又有日級別的趨勢這個改動收益不小。第三殘差學(xué)習(xí)器也可以換成別的模型。我試過用XGBoost或LightGBM去擬合ARIMA殘差在樣本量比較大的時候效果也不錯而且訓(xùn)練速度遠(yuǎn)快于LSTM。但樹模型對時間順序的結(jié)構(gòu)利用不如LSTM細(xì)膩通常LSTM在殘差里的表現(xiàn)更穩(wěn)定。第四如果你的目標(biāo)是長序列預(yù)測可以考慮在LSTM層后面加attention機(jī)制或用Transformer/Informer這類以注意力為核心的架構(gòu)替代LSTM。但老實說對日粒度數(shù)據(jù)數(shù)據(jù)量如果不支持超大規(guī)模訓(xùn)練Transformer不一定卷得過LSTM別盲目追新。最后分享一個我自己的體會組合模型的第一原則不是越多越好而是看每一層的輸入是不是它擅長的信號類型。ARIMACNN-LSTM能打是因為三者拿到的都是適合自己處理的拼圖——ARIMA拿趨勢CNN拿形態(tài)LSTM拿時序依賴。想清楚這個分工你自己也能設(shè)計出更好的組合。