測實戰(zhàn):時序特征處理決定模型上限)
做股票預(yù)測的人十個里有八個在第一步就栽了跟頭——不是模型不夠強而是喂給模型的時序數(shù)據(jù)根本沒處理好。我去年做了一個項目任務(wù)是從雅虎財經(jīng)接口拉取美股歷史行情用LSTM預(yù)測未來幾個交易日的走勢項目標(biāo)題就叫“基于時序特征處理與LSTM結(jié)合的雅虎美股數(shù)據(jù)預(yù)測”。做完之后最大的體感是LSTM本身不神秘真正決定預(yù)測質(zhì)量上限的是數(shù)據(jù)落地前那套時序特征處理有沒有做到位。這篇文章我把整個項目的設(shè)計思路、數(shù)據(jù)管線、模型搭建和踩坑記錄全部攤開講適合想入門時間序列深度學(xué)習(xí)或者正在做金融數(shù)據(jù)預(yù)測但效果一直不理想的人直接照著復(fù)現(xiàn)。1. 項目整體設(shè)計與思路拆解1.1 這到底是個什么問題不只是“預(yù)測股價”表面上看任務(wù)很清晰給一段歷史行情預(yù)測明天的收盤價。但一旦真正動手你就會發(fā)現(xiàn)“預(yù)測股價”這個表述充滿了陷阱。股價序列是非平穩(wěn)的長期趨勢、周期性波動、突發(fā)事件產(chǎn)生的跳空缺口全部混在一起更麻煩的是金融序列的信噪比極低大部分“規(guī)律”可能只是噪聲的隨機排列。所以這個項目的核心難點從來不是“跑通一個LSTM”而是怎么把原始行情轉(zhuǎn)換成模型能真正學(xué)出模式的時序特征。我還想強調(diào)一點這個項目適合作為算法研究和工程實操樣例不是說照著做就能穩(wěn)定盈利。目標(biāo)應(yīng)該放在“能不能建立一套可復(fù)現(xiàn)的預(yù)測流水線”“能不能正確評估模型效果”上而不是指望一個模型成為搖錢樹。帶著這個預(yù)期去看技術(shù)細節(jié)會理性很多。1.2 為什么是LSTM序列建模的選型邏輯處理時間序列的經(jīng)典方法很多ARIMA、GARCH、狀態(tài)空間模型都在傳統(tǒng)金融分析里占據(jù)過重要位置。但這些模型通常對數(shù)據(jù)有較強的假設(shè)比如線性關(guān)系、平穩(wěn)性、特定分布而股價數(shù)據(jù)恰恰在大部分時間不滿足這些假設(shè)。傳統(tǒng)機器學(xué)習(xí)模型如果直接拿“過去幾天的數(shù)值”當(dāng)特征特征之間高度相關(guān)而且完全丟失了“時間順序”信息模型無法區(qū)分“三天前的高點”和“昨天的高點”對今天的影響有什么不同。LSTM長短期記憶網(wǎng)絡(luò)是RNN的一種變體它的核心優(yōu)勢在于門控機制。簡單說細胞狀態(tài)負責(zé)記住長期信息遺忘門決定哪些歷史信息可以丟棄輸入門決定哪些新信息值得寫入輸出門控制當(dāng)前狀態(tài)輸出多少。這種設(shè)計讓模型能在幾十個時間步的窗口內(nèi)保留有效信息同時避免普通RNN訓(xùn)練時常見的梯度消失問題。用個生活化的類比LSTM就像一個有記憶管理習(xí)慣的人重要的信息長期不遺忘無效信息自動清理從而在長序列上不“斷片”。當(dāng)然LSTM也不是銀彈。它對時間步長的長度敏感對特征的量綱和分布敏感如果直接把原始收盤價一股腦塞進去模型通常只會學(xué)到“預(yù)測值約等于上一個時間步的值”這種偷懶的映射。因此在LSTM之前必須做時序特征處理。1.3 時序特征處理在整個流程中的角色時序特征處理在這個項目里包含幾層意思第一是讓數(shù)據(jù)平穩(wěn)化比如差分、計算收益率第二是讓量綱統(tǒng)一比如歸一化第三是構(gòu)造更有信息量的特征比如滾動統(tǒng)計量、技術(shù)指標(biāo)、日歷特征第四是把“過去一段時間”組織成監(jiān)督學(xué)習(xí)的樣本也就是滑動窗口。這一系列步驟決定了模型看到的是什么。如果輸入是原始價格網(wǎng)絡(luò)很容易學(xué)到“明天的價格約等于今天的價格”因為相鄰交易日價格變化往往很小這個“復(fù)制上一天”的預(yù)測在MSE指標(biāo)上已經(jīng)足夠低。但這樣的模型沒有實際價值它只是在擬合一個近乎恒等的映射。反過來如果用收益率作為輸入和預(yù)測目標(biāo)模型被迫去學(xué)習(xí)“漲跌的趨勢和幅度”信號的辨識度更高雖然預(yù)測難度更大但至少方向是對的。我個人的經(jīng)驗是時序預(yù)測項目里特征工程的收益往往比換模型結(jié)構(gòu)更明顯。這也是我把“時序特征處理”和“LSTM”放在同等位置的原因。1.4 項目整體技術(shù)路線整個項目分為五個環(huán)節(jié)數(shù)據(jù)獲取、數(shù)據(jù)清洗與特征構(gòu)造、數(shù)據(jù)集生成、LSTM建模與訓(xùn)練、結(jié)果評估與可視化。數(shù)據(jù)層用雅虎財經(jīng)接口拉取日線行情處理層用pandas完成清洗、差分、滾動統(tǒng)計和歸一化建模層用Keras搭建LSTM評估層同時看回歸誤差和方向準(zhǔn)確率。下文我會按這個鏈路逐步展開并提供完整代碼和參數(shù)選擇依據(jù)。2. 數(shù)據(jù)獲取與時序特征工程實操2.1 從雅虎財經(jīng)拉取歷史行情數(shù)據(jù)雅虎財經(jīng)的公共歷史數(shù)據(jù)接口非常方便Python里一般直接用yfinance封裝。安裝很簡單pip install yfinance pandas numpy matplotlib scikit-learn tensorflow拉取數(shù)據(jù)的代碼如下import yfinance as yf import pandas as pd # 示例標(biāo)的代碼實際運行時請?zhí)鎿Q為你要研究的美股代碼 df yf.download(DEMO, start2015-01-01, end2024-12-31, auto_adjustTrue) df df[[Open, High, Low, Close, Volume]].copy() df.dropna(inplaceTrue) print(df.head()) print(df.tail())有幾個細節(jié)值得注意。auto_adjustTrue表示自動處理復(fù)權(quán)也就是把分紅、拆股造成的價格跳空修正掉。如果不做復(fù)權(quán)模型會把一次拆股導(dǎo)致的“價格腰斬”當(dāng)作文本上的真實下跌明顯是錯誤信號。收盤價這里用調(diào)整后價格。拉數(shù)據(jù)時可能遇到兩種情況一是標(biāo)的代碼過期或拼寫錯誤會返回空數(shù)據(jù)二是網(wǎng)絡(luò)環(huán)境不穩(wěn)定導(dǎo)致下載失敗。我的處理方案是先判斷df是否為空為空就直接退出避免后續(xù)流程在一堆NaN上跑出可疑結(jié)果。不要迷信接口的完整性拉下來之后必須做基礎(chǔ)體檢檢查行數(shù)、檢查日期是否連續(xù)、檢查有無NaN。如果網(wǎng)絡(luò)實在不穩(wěn)定還有一個穩(wěn)妥的替代方案直接把歷史行情導(dǎo)出成CSV放在本地讀取或者使用本地財經(jīng)數(shù)據(jù)庫。數(shù)據(jù)源不必死磕接口關(guān)鍵是數(shù)據(jù)質(zhì)量和時間跨度。2.2 數(shù)據(jù)清洗與預(yù)處理先做減法再做加法行情數(shù)據(jù)本身是比較規(guī)整的但仍然有幾件必須做的事。第一檢查并處理缺失值。美股偶爾會因為停牌、數(shù)據(jù)源異常產(chǎn)生空行。處理方式通常是刪除或者用前向填充。我個人傾向于刪除因為金融數(shù)據(jù)造假填充反而會引入虛假規(guī)律尤其是連續(xù)停牌期間前向填充會把“無交易”偽裝成“價格不變”這是錯誤的。第二刪除極端離群值。某些異常交易日會出現(xiàn)遠超正常波動幅度的價格跳動可能是數(shù)據(jù)錯誤也可能是真實的極端行情。這一塊要謹慎我的原則是只有明顯超出歷史波動范圍且無法合理解釋的數(shù)據(jù)才刪除幅度在正常范圍內(nèi)的一律保留因為模型需要見過各種市場狀態(tài)。第三做平穩(wěn)化轉(zhuǎn)換。最常用的是對收盤價取差分或計算百分比收益率。差分公式是 diff_t close_t - close_{t-1}百分比收益率是 return_t (close_t - close_{t-1}) / close_{t-1}。收益率天然消除了絕對價格水平的影響也更接近金融研究的常用口徑。第四量綱歸一化。LSTM對輸入特征的尺度敏感如果Close是幾千美元Volume是幾千萬股模型訓(xùn)練時大數(shù)值特征會主導(dǎo)梯度。我用MinMaxScaler把所有特征壓縮到0到1之間。這里要特別強調(diào)一個很多教程都會犯的錯誤縮放器必須在訓(xùn)練集上fit再transform訓(xùn)練集和測試集而不是先對全量數(shù)據(jù)fit再劃分。如果先fit全量數(shù)據(jù)均值和最小最大值里包含了未來信息測試集信息在訓(xùn)練階段就被模型間接看到了這屬于信息泄漏會讓評估結(jié)果虛高。2.3 時序特征構(gòu)造滑動窗口、差分、技術(shù)指標(biāo)這一節(jié)是整個項目里最能體現(xiàn)“時序特征處理”價值的地方也是我和只調(diào)LSTM的做法的核心區(qū)別。我構(gòu)造了下面幾類特征。第一類價格本身。收盤價是最基礎(chǔ)的信號但我不直接使用原始值而是使用滾動均值、滾動標(biāo)準(zhǔn)差等方式描述它。滾動均值MA10代表近期趨勢方向滾動標(biāo)準(zhǔn)差Std10代表近期波動程度。第二類收益率相關(guān)特征。包括單日收益率、滾動收益率均值、滾動收益率標(biāo)準(zhǔn)差。收益率把絕對價格變成相對變化能明顯緩解LSTM“復(fù)制上一幀”的惰性問題。第三類交易量特征。成交量是價格之外最核心的市場參與度信息。量價配合往往能揭示市場情緒比如放量突破、縮量回調(diào)等。我構(gòu)造了成交量變化率以及成交量的滾動均值。第四類趨勢特征??梢杂肕ACD、RSI等技術(shù)指標(biāo)作為額外特征。技術(shù)上要注意的是這些指標(biāo)必須只能用歷史數(shù)據(jù)計算不能用未來數(shù)據(jù)否則就引入了未來函數(shù)。我用pandas的rolling、ewm等方法計算天然只使用當(dāng)前及過去的數(shù)據(jù)不會前視。構(gòu)造代碼示意df[return_1] df[Close].pct_change() df[diff_1] df[Close].diff() df[ma10] df[Close].rolling(10).mean() df[std10] df[Close].rolling(10).std() df[vol_ret] df[Volume].pct_change() df[vol_ma10] df[Volume].rolling(10).mean() # 去掉因為滾動計算產(chǎn)生的NaN行 df.dropna(inplaceTrue)這里的窗口10天是一個經(jīng)驗值代表大約兩周的交易周期。你可以用20、30做對比實驗但不要一次引入太多不同窗口的同類特征會造成特征冗余。特征不是越多越好LSTM吃進去的每一維特征都要承擔(dān)信息冗余特征只會增加過擬合風(fēng)險。2.4 數(shù)據(jù)切分請務(wù)必不要隨機打亂時間序列監(jiān)督學(xué)習(xí)的第一步是把“過去N天”的數(shù)據(jù)拼成一個樣本把“未來第N1天”作為標(biāo)簽。我用60天的窗口。為什么是60因為美股一個季度大約有63個交易日60天窗口能讓模型同時看到兩個月的趨勢和一個季度的周期輪廓。窗口太短模型學(xué)不到周期太長又會引入陳舊信息60只是一個合理的起點你可以用30、90做敏感性測試。構(gòu)建樣本的代碼import numpy as np def make_sequences(data, window60): X [] y [] for i in range(window, len(data)): X.append(data[i-window:i]) # 默認預(yù)測下一個時間步的Close y.append(data[i, 0]) return np.array(X), np.array(y)注意這個函數(shù)里data的列順序很關(guān)鍵我習(xí)慣把Close放在第一列這樣y取第0列就是Close。然后是數(shù)據(jù)集劃分。這里有一條鐵律絕對不能使用隨機切分必須按時間順序切分。金融時序里有強烈的時間依賴性如果隨機打亂訓(xùn)練集里會出現(xiàn)未來數(shù)據(jù)測試集里會出現(xiàn)過去數(shù)據(jù)模型相當(dāng)于在“開著天眼考試”評估分數(shù)再好看也沒有意義。train_size int(len(X) * 0.8) X_train, X_test X[:train_size], X[train_size:] y_train, y_test y[:train_size], y[train_size:]比例用80/20。80%作為訓(xùn)練20%作為測試這20%要是最近期的數(shù)據(jù)因為股票預(yù)測的實際場景就是“用歷史擬合對最新時刻做推斷”。3. LSTM模型設(shè)計與訓(xùn)練細節(jié)3.1 模型輸入到底長什么樣三維張量很多人第一次寫LSTM都會在輸入維度上卡住。LSTM的輸入必須是三維張量形狀是(samples, timesteps, features)。在我們這個項目里samples是樣本數(shù)量timesteps是窗口長度60features是特征維度比如5個。也就是說每一個訓(xùn)練樣本是一個60×5的矩陣代表過去60天每天的5個特征值。X_train.shape應(yīng)該長這樣(樣本數(shù), 60, 特征數(shù))。如果這個形狀不對模型根本跑不起來。常見錯誤是把二維數(shù)組直接丟給LSTMKeras會直接報維度錯誤。構(gòu)建完數(shù)據(jù)后先打印一下X_train.shape這一步能省掉很多調(diào)試時間。3.2 網(wǎng)絡(luò)結(jié)構(gòu)設(shè)計從單層到多層我采用兩層LSTM加Dropout的結(jié)構(gòu)。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential() model.add(LSTM(64, return_sequencesTrue, input_shape(X_train.shape[1], X_train.shape[2]))) model.add(Dropout(0.2)) model.add(LSTM(32, return_sequencesFalse)) model.add(Dropout(0.2)) model.add(Dense(16, activationrelu)) model.add(Dense(1))第一層LSTM有64個單元return_sequencesTrue表示返回所有時間步的隱藏狀態(tài)這樣第二層LSTM能繼續(xù)處理完整的時間信息。第二層32個單元return_sequencesFalse只返回最后一個時間步的輸出接一個全連接層做最終回歸。Dropout設(shè)置在0.2左右防止過擬合。這里有個經(jīng)驗不要一上來就搭三層四層LSTM。金融時序樣本量一般不大層數(shù)加深之后參數(shù)量激增很容易在訓(xùn)練集上表現(xiàn)很好、測試集上崩塌。我在這個項目里嘗試過三層LSTM結(jié)果是訓(xùn)練Loss降得很快但測試集方向準(zhǔn)確率反而下降。所以兩層是更穩(wěn)妥的起點。3.3 訓(xùn)練參數(shù)學(xué)習(xí)率、批次大小、早停編譯階段我用Adam優(yōu)化器默認學(xué)習(xí)率0.001損失函數(shù)用MSE同時監(jiān)控MAE。MSE對“大誤差”敏感能讓模型優(yōu)先擬合那些偏離明顯的樣本MAE則便于直觀理解平均誤差水平。訓(xùn)練時我設(shè)置了EarlyStopping監(jiān)控驗證集Loss連續(xù)20個epoch沒有改善就停止并且恢復(fù)最優(yōu)權(quán)重。這樣可以省掉很多無效訓(xùn)練時間也避免過擬合。from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping( monitorval_loss, patience20, restore_best_weightsTrue ) history model.fit( X_train, y_train, epochs100, batch_size32, validation_split0.1, callbacks[early_stop], verbose1 )batch_size32是均衡穩(wěn)定性和訓(xùn)練速度的選擇。金融時序樣本通常上萬條32到64之間都可以。學(xué)習(xí)率對收斂影響很大如果發(fā)現(xiàn)Loss劇烈震蕩可以降到0.0005如果收斂太慢可以適當(dāng)提升但一般不建議超過0.01。注意validation_split0.1是在訓(xùn)練集內(nèi)部再切出一部分做驗證它依然保持了時序順序Keras會取最后10%當(dāng)驗證集不會亂序。3.4 損失函數(shù)與評估指標(biāo)的選擇股價預(yù)測的評估不能只看MSE。因為股價本身數(shù)值很大MSE的絕對值動輒幾十幾百看不出模型好壞。我同時看四個指標(biāo)RMSE均方根誤差單位跟股價一致直觀。MAE平均絕對誤差同樣直觀。MAPE平均絕對百分比誤差反映相對誤差便于在代碼之間橫向?qū)Ρ?。DA方向準(zhǔn)確率統(tǒng)計預(yù)測方向與真實方向一致的比例這才是交易場景里更關(guān)心的指標(biāo)。方向準(zhǔn)確率計算需要先判斷真實價格是漲還是跌然后判斷預(yù)測價格是漲還是跌兩者一致的比例就是DA。方向準(zhǔn)確率的計算代碼有很多細節(jié)我后續(xù)在完整代碼里給一個可用的實現(xiàn)。很多LSTM股價預(yù)測項目只看RMSE結(jié)果RMSE很低大家很高興實際看曲線發(fā)現(xiàn)預(yù)測值只是把真實線向右平移了一天方向準(zhǔn)確率可能不到50%這種模型一點實戰(zhàn)價值都沒有。所以評估體系必須把方向準(zhǔn)確率放進來。4. 完整實現(xiàn)與關(guān)鍵代碼解析4.1 環(huán)境準(zhǔn)備在開始完整代碼前先列一下依賴pip install yfinance pandas numpy matplotlib scikit-learn tensorflow版本方面TensorFlow用2.xKeras集成在TensorFlow里不需要單獨安裝。yfinance建議用最新版老版本偶爾有接口字段變化問題。硬件方面這個項目的數(shù)據(jù)量用CPU也能跑但訓(xùn)練LSTM時如果有GPU會快很多。我用的是普通筆記本訓(xùn)練100個epoch加早停大約幾分鐘到十幾分鐘完全可以接受。4.2 數(shù)據(jù)獲取與特征構(gòu)建代碼完整特征構(gòu)建代碼import yfinance as yf import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler # 1. 獲取數(shù)據(jù) df yf.download(DEMO, start2015-01-01, end2024-12-31, auto_adjustTrue) df df[[Open, High, Low, Close, Volume]].copy() df.dropna(inplaceTrue) # 2. 時序特征工程 df[return_1] df[Close].pct_change() df[diff_1] df[Close].diff() df[ma10] df[Close].rolling(10).mean() df[std10] df[Close].rolling(10).std() df[vol_ret] df[Volume].pct_change() df[vol_ma10] df[Volume].rolling(10).mean() df.dropna(inplaceTrue) # 3. 特征列 feature_cols [Close, return_1, diff_1, ma10, std10, vol_ret, vol_ma10] # 4. 先切分再歸一化 train_size int(len(df) * 0.8) df_train df.iloc[:train_size] df_test df.iloc[train_size:] scaler MinMaxScaler() scaled_train scaler.fit_transform(df_train[feature_cols]) scaled_test scaler.transform(df_test[feature_cols])這里的關(guān)鍵就是先切分再fit。scaled_test用的scaler是訓(xùn)練集fit出來的而不是在全部數(shù)據(jù)上fit的。為什么這么重要因為MinMaxScaler需要知道特征的最小值和最大值如果測試集提前參與了計算模型在訓(xùn)練階段就“見過”測試集的范圍分布相當(dāng)于考試時偷看了一眼答案。4.3 數(shù)據(jù)集構(gòu)建代碼構(gòu)建序列數(shù)據(jù)集時要處理好兩個問題第一窗口長度第二訓(xùn)練集和測試集的構(gòu)造不能交叉。def make_sequences_from_scaled(scaled, window60): X [] y [] for i in range(window, len(scaled)): X.append(scaled[i-window:i]) y.append(scaled[i, 0]) # 第0列是歸一化后的Close return np.array(X), np.array(y) X_train, y_train make_sequences_from_scaled(scaled_train, window60) X_test, y_test make_sequences_from_scaled(scaled_test, window60) print(X_train.shape, y_train.shape) print(X_test.shape, y_test.shape)有人會擔(dān)心測試集的前60行因為沒有完整的歷史窗口而無法生成樣本這是正常的。測試集的有效樣本是從第60天開始的前面的數(shù)據(jù)作為窗口背景被消耗掉了。這里還有一個細節(jié)因為歸一化是在切分后分別進行的df_train和df_test被分別轉(zhuǎn)換但轉(zhuǎn)換后都扔進了同一個滑動窗口函數(shù)窗口內(nèi)的特征值域都是0到1一致性沒有問題。4.4 模型構(gòu)建與訓(xùn)練代碼把前面的模型定義和訓(xùn)練代碼整合在一起from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping model Sequential() model.add(LSTM(64, return_sequencesTrue, input_shape(X_train.shape[1], X_train.shape[2]))) model.add(Dropout(0.2)) model.add(LSTM(32, return_sequencesFalse)) model.add(Dropout(0.2)) model.add(Dense(16, activationrelu)) model.add(Dense(1)) model.compile(optimizeradam, lossmse, metrics[mae]) early_stop EarlyStopping( monitorval_loss, patience20, restore_best_weightsTrue ) history model.fit( X_train, y_train, epochs100, batch_size32, validation_split0.1, callbacks[early_stop], verbose1 )訓(xùn)練過程里我習(xí)慣觀察兩個東西val_loss驗證集損失和train_loss訓(xùn)練集損失。如果train_loss一路下降但val_loss先降后升說明過擬合開始如果兩個都不降先檢查數(shù)據(jù)是否包含NaN再嘗試降低學(xué)習(xí)率。訓(xùn)練完之后可以畫出Loss曲線import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.legend() plt.show()這條曲線能直觀反映模型的收斂狀態(tài)。如果train_loss和val_loss都穩(wěn)定下降并且最終貼近說明訓(xùn)練基本健康。4.5 預(yù)測、反歸一化與可視化模型預(yù)測得到的結(jié)果是歸一化后的Close值不能直接用來跟真實股價對比必須先反歸一化。這里我單獨為Close建一個scaler更簡潔。思路是從原始df中取Close列單獨fit一個MinMaxScaler然后用它完成y的逆變換。close_scaler MinMaxScaler() close_values df[Close].values.reshape(-1, 1) close_scaler.fit(close_values) # 注意這里用訓(xùn)練集fit的scaler對y_train也做一遍逆變換測試集同理 # 但更嚴謹?shù)氖怯糜?xùn)練集Close單獨fit的scaler上面這版有一個隱患close_scaler是全量數(shù)據(jù)的Close擬合的測試集信息又泄漏了。如果要徹底嚴謹應(yīng)該只用訓(xùn)練集Close擬合。train_close_values df[Close].values[:train_size].reshape(-1, 1) close_scaler MinMaxScaler() close_scaler.fit(train_close_values) y_train_inv close_scaler.inverse_transform(y_train.reshape(-1, 1)) y_pred_inv close_scaler.inverse_transform(y_pred.reshape(-1, 1)) y_test_inv close_scaler.inverse_transform(y_test.reshape(-1, 1))這里我用了一個單獨scaler而不是前面全部特征的scaler是因為只關(guān)心Close的還原。如果用全特征scaler對y做逆變換需要把y向量還原成完整特征向量的形狀再取第一列非常麻煩也沒必要。單獨訓(xùn)練一個Close縮放器代碼更清晰也不會影響特征縮放的效果。最后可視化預(yù)測序列和真實序列plt.figure(figsize(12, 5)) plt.plot(y_test_inv, labelReal Close, colorblack) plt.plot(y_pred_inv, labelPredicted Close, colorred, linestyle--) plt.legend() plt.title(DEMO Stock Close Price Prediction) plt.show()看到這張圖你就知道模型有沒有出現(xiàn)“滯后預(yù)測”的問題。如果預(yù)測線和真實線看起來非常貼近但仔細看預(yù)測線整體向右平移了一天那不是預(yù)測成功而是模型學(xué)會了“照抄上一天”這是LSTM股價預(yù)測里最典型的假成功。4.6 方向準(zhǔn)確率計算我加上一個方向準(zhǔn)確率指標(biāo)def direction_accuracy(y_true, y_pred): true_diff np.diff(y_true.flatten()) pred_diff np.diff(y_pred.flatten()) correct np.sign(true_diff) np.sign(pred_diff) return np.mean(correct) da direction_accuracy(y_test_inv, y_pred_inv) print(fDirection Accuracy: {da:.4f})這個指標(biāo)比RMSE更貼近交易場景。我之前做過一個模型RMSE低到以為自己要“起飛”了結(jié)果方向準(zhǔn)確率只有49.6%等于拋硬幣。后來我換成收益率特征加多步預(yù)測方向準(zhǔn)確率才勉強到53%左右。在金融預(yù)測里能把方向準(zhǔn)確率穩(wěn)定做到52%以上已經(jīng)算有信息量了。5. 常見問題、踩坑記錄與排查思路5.1 預(yù)測結(jié)果“神預(yù)測”滯后性問題我在前面反復(fù)提的滯后問題是LSTM股票預(yù)測項目里最普遍的假象。具體表現(xiàn)是真實價格下跌時預(yù)測線也在下跌但總是慢一步真實曲線出現(xiàn)拐點預(yù)測線還在沿用前一天的方向。整體看起來“高度擬合”實際模型什么都沒學(xué)會只是把上一時刻的價格稍微平滑了一下輸出等于輸入的延遲版本。造成這個現(xiàn)象的根本原因是輸入里有強自相關(guān)的原始價格或滯后特征模型只要學(xué)到p_{t1}p_t就能讓MSE足夠低。優(yōu)化器很聰明它發(fā)現(xiàn)這個“作弊解”能讓損失快速下降于是不再去學(xué)習(xí)更復(fù)雜的模式。處理方法我試下來比較有效的有幾招。第一預(yù)測目標(biāo)從“收盤價”改成“未來收益率”避免模型直接回歸絕對價格。第二輸入特征里盡可能使用差分、收益率、滾動統(tǒng)計量減少原始價格的主導(dǎo)性。第三預(yù)測多步而不是單步比如預(yù)測未來3天收益的方向模型無法靠簡單滯后通過評估。第四用方向準(zhǔn)確率作為主評估指標(biāo)如果DA低于51%直接判定模型無效。5.2 Loss不下降或震蕩嚴重訓(xùn)練時Loss完全不下降最常見的原因是數(shù)據(jù)里有NaN或inf梯度計算結(jié)果變成NaN模型直接罷工。解決辦法很簡單檢查數(shù)據(jù)在特征構(gòu)造后打印df.isnull().sum()確認沒有缺失值再進入訓(xùn)練。Loss震蕩嚴重通常是學(xué)習(xí)率設(shè)置不當(dāng)。Adam默認0.001在多數(shù)情況下可以但如果數(shù)據(jù)分布復(fù)雜震蕩明顯就把學(xué)習(xí)率降到0.0005或0.0001。還有一種情況是batch_size太小單個batch的梯度噪聲太大可以嘗試把batch_size調(diào)到64或128。還有一個容易被忽略的問題窗口長度太短。如果窗口只有5天模型看到的信息太少很難學(xué)到穩(wěn)定模式。窗口長度影響訓(xùn)練穩(wěn)定性如果Loss一直不降可以試試拉長窗口到60或90。5.3 數(shù)據(jù)泄漏歸一化和切分順序錯了我在網(wǎng)上看過很多LSTM教程都在全量數(shù)據(jù)上做MinMaxScaler然后切分訓(xùn)練集和測試集。這個做法在傳統(tǒng)機器學(xué)習(xí)里影響可能小一些但在時序預(yù)測里是明確的bug。測試集參與了scaler的fit等于測試集的價格區(qū)間、趨勢信息在訓(xùn)練階段就被模型間接知道了。正確順序是先按時間切分再在訓(xùn)練集上fit縮放器用同一個縮放器transform測試集。評估時反歸一化用的Close縮放器也只能在訓(xùn)練集上fit。這個原則同樣適用于標(biāo)準(zhǔn)化、PCA等所有需要從數(shù)據(jù)中學(xué)習(xí)參數(shù)的預(yù)處理步驟。5.4 模型過擬合與泛化問題金融數(shù)據(jù)噪聲高LSTM很容易過擬合。我的經(jīng)驗是如果訓(xùn)練集Loss持續(xù)下降而驗證集Loss在某個epoch后開始反彈說明模型開始背誦訓(xùn)練數(shù)據(jù)。這時優(yōu)先降低模型復(fù)雜度比如LSTM單元從64降到32或者直接刪掉一層Dropout可以提高到0.3訓(xùn)練epochs不要硬拉靠早停機制兜底。另外不要只看一組數(shù)據(jù)的評估結(jié)果。市場狀態(tài)會變化同一個模型在上漲趨勢測試集上可能DA很高在橫盤階段可能直接失效。穩(wěn)妥的做法是把時間窗口滾動起來比如用過去3年訓(xùn)練未來3個月測試然后滾動到下一段重復(fù)多次取平均指標(biāo)。這樣得到的評估結(jié)論才比較可信。5.5 接口拉取失敗和數(shù)據(jù)質(zhì)量問題yfinance偶爾會拉不到數(shù)據(jù)原因包括標(biāo)的代碼不存在、時間范圍超出數(shù)據(jù)覆蓋、網(wǎng)絡(luò)環(huán)境受限等。我的排查步驟是先打印原始返回數(shù)據(jù)如果df為空檢查代碼是否拼寫正確嘗試更換時間范圍如果仍然失敗就改從本地CSV讀取。數(shù)據(jù)源本身不是這個項目的核心代碼跑通和流程正確更重要。數(shù)據(jù)質(zhì)量方面還要留意一點部分交易日會出現(xiàn)極低的成交量或價格紋絲不動的情況。這些通常來自長期停牌或者流動性極差的標(biāo)的。做預(yù)測項目時盡量選擇成交活躍、歷史長度足夠的大票樣本質(zhì)量直接決定模型能不能學(xué)到有效模式。6. 一些后續(xù)可以嘗試的方向這次項目做完之后我嘗試過幾個擴展方向簡單說一下結(jié)論。一個是加更多類型特征比如把開盤價、最高價、最低價都納入特征矩陣并構(gòu)造高低價差、振幅、日內(nèi)波動率等。特征是增加了但模型提升并不明顯因為原始OHLC之間本身就高度相關(guān)增加維度主要是給模型更多“觀察角度”而非新信息。另一個方向是多步預(yù)測。單步預(yù)測只能看明天實操價值有限??梢园涯P透某奢敵鑫磥?天的預(yù)測結(jié)果或者用序列到序列的方式用過去60天預(yù)測未來5天。這樣做的難點是標(biāo)簽構(gòu)造和輸出層設(shè)計但一旦跑通就能直接用于短周期決策參考。還有一個方向是嘗試Transformer或注意力機制。注意力機制可以顯式建模“過去哪些時間步對當(dāng)前預(yù)測更重要”比LSTM的隱式記憶更直觀。但Transformer對數(shù)據(jù)量要求更高在中小規(guī)模數(shù)據(jù)集上不一定比LSTM強而且訓(xùn)練更耗時。如果是為了學(xué)習(xí)完全可以玩一玩如果是求穩(wěn)LSTM加特征處理已經(jīng)夠用了。我個人在實際操作中的體會是時序預(yù)測項目的成敗模型結(jié)構(gòu)最多決定上限的一小部分數(shù)據(jù)質(zhì)量、特征構(gòu)造和評估方式才是真正的分水嶺。尤其是“預(yù)測滯后”這個問題十個LSTM股票預(yù)測項目里至少有五個會栽在這一步。如果你也在做類似項目我建議先不要急著一上來堆算力堆模型先把特征工程做扎實把方向準(zhǔn)確率指標(biāo)擺到桌面上再談優(yōu)化。另外所有金融模型的效果都必須經(jīng)過滾動回測和嚴謹評估千萬不要因為單次測試集上的數(shù)字好看就過度樂觀。最后再分享一個小技巧實驗過程中把每次的特征組合、超參數(shù)和評估指標(biāo)記錄在一個表格里復(fù)現(xiàn)和調(diào)優(yōu)會高效很多這個習(xí)慣能幫你省掉大量重復(fù)勞動。