間序列預(yù)測(cè)實(shí)戰(zhàn):從數(shù)據(jù)窗口構(gòu)造到模型調(diào)參避坑)
簡(jiǎn)介這份資源面向高校學(xué)生與Python初學(xué)者提供一套可直接運(yùn)行的LSTM時(shí)間序列預(yù)測(cè)完整項(xiàng)目適用于期末大作業(yè)、課程設(shè)計(jì)及入門(mén)級(jí)深度學(xué)習(xí)實(shí)踐。項(xiàng)目以空氣質(zhì)量等真實(shí)數(shù)據(jù)為樣本覆蓋數(shù)據(jù)預(yù)處理、模型搭建、訓(xùn)練與預(yù)測(cè)全流程代碼注釋詳盡新手也能看懂并快速部署。壓縮包共129個(gè)文件包含78個(gè)py源碼、26個(gè)csv數(shù)據(jù)集、16個(gè)txt說(shuō)明及checkpoint、h5等模型權(quán)重文件整體約5.42MB目錄結(jié)構(gòu)清晰便于按模塊查閱與二次修改。已有169人學(xué)習(xí)下載可作為高分作業(yè)參考。讀者可獲得完整可復(fù)現(xiàn)的預(yù)測(cè)方案、配套數(shù)據(jù)集與訓(xùn)練腳本并借助注釋理解LSTM核心邏輯節(jié)省從零搭建的時(shí)間同時(shí)掌握時(shí)間序列建模的排錯(cuò)與調(diào)參思路。1. 從一份期末大作業(yè)說(shuō)起LSTM 時(shí)間序列預(yù)測(cè)到底能跑出什么結(jié)果很多人第一次接觸 LSTM 時(shí)間序列預(yù)測(cè)都是被一份「python源碼全部數(shù)據(jù)」的期末大作業(yè)逼出來(lái)的。打開(kāi)壓縮包看到train.py、data.csv、model.pth三個(gè)文件心里想的是「跑起來(lái)就交差」結(jié)果環(huán)境一裝就是一下午訓(xùn)練一跑 loss 就發(fā)散預(yù)測(cè)曲線(xiàn)畫(huà)出來(lái)跟真實(shí)值差了一個(gè)數(shù)量級(jí)。這篇筆記不聊虛的就把這套「LSTM 做時(shí)間序列預(yù)測(cè)」的完整鏈路拆開(kāi)講數(shù)據(jù)怎么切、模型怎么搭、參數(shù)怎么調(diào)、結(jié)果怎么驗(yàn)以及那些只有真正跑過(guò)一遍才會(huì)遇到的坑。適合誰(shuí)看如果你手上有類(lèi)似的大作業(yè)、課程設(shè)計(jì)或者想用 LSTM 做一個(gè)單變量/多變量的趨勢(shì)預(yù)測(cè)銷(xiāo)量、溫度、負(fù)荷、股價(jià)走勢(shì)這類(lèi)連續(xù)數(shù)值這篇能讓你從零把一條可復(fù)現(xiàn)的 pipeline 搭起來(lái)。前提是你已經(jīng)裝好 Python會(huì)用 pip能看懂基本的 numpy 數(shù)組操作。至于 LSTM 背后的門(mén)控?cái)?shù)學(xué)推導(dǎo)我會(huì)在必要的地方點(diǎn)一句但不會(huì)展開(kāi)成教科書(shū)——那是另一篇文章的事。核心結(jié)論先放這里L(fēng)STM 時(shí)間序列預(yù)測(cè)的成敗八成取決于數(shù)據(jù)窗口的構(gòu)造方式而不是模型層數(shù)。很多人把 LSTM 堆到三四層結(jié)果還不如一個(gè)單層 正確滑窗的版本。下面按「數(shù)據(jù) → 模型 → 訓(xùn)練 → 評(píng)估 → 避坑 → 進(jìn)階」的順序走一遍。2. 數(shù)據(jù)準(zhǔn)備與窗口構(gòu)造決定預(yù)測(cè)上限的一步2.1 時(shí)間序列預(yù)測(cè)的任務(wù)定義與數(shù)據(jù)格式時(shí)間序列預(yù)測(cè)的本質(zhì)是用過(guò)去一段時(shí)間的觀(guān)測(cè)值去推斷未來(lái)某個(gè)時(shí)刻的值。形式化一點(diǎn)給定序列 $x_1, x_2, ..., x_T$我們要學(xué)一個(gè)映射 $f$使得 $\hat{x}{t1} f(x{t-L1}, ..., x_t)$其中 $L$ 是回看窗口長(zhǎng)度look-back window。LSTM 在這里的角色就是把這個(gè)長(zhǎng)度為 $L$ 的序列編碼成一個(gè)隱狀態(tài)再解碼出下一步的預(yù)測(cè)。數(shù)據(jù)格式上最常見(jiàn)的是單列 CSV第一列是時(shí)間戳第二列是數(shù)值。也有多變量的情況比如同時(shí)有溫度、濕度、風(fēng)速三列預(yù)測(cè)其中一列或全部。期末大作業(yè)里 90% 是單變量所以我們先按單變量講多變量的差異在 2.3 里補(bǔ)。拿到數(shù)據(jù)第一件事不是急著喂模型而是畫(huà)圖。用 matplotlib 把整條曲線(xiàn)畫(huà)出來(lái)看三件事有沒(méi)有明顯的周期日周期、周周期、有沒(méi)有突變點(diǎn)傳感器故障、促銷(xiāo)活動(dòng)、量綱范圍是多少。這一步花五分鐘能省后面兩小時(shí)調(diào)參。import pandas as pd import matplotlib.pyplot as plt # 讀取數(shù)據(jù)假設(shè)第一列是時(shí)間第二列是數(shù)值 df pd.read_csv(data.csv, parse_dates[timestamp]) df df.sort_values(timestamp).reset_index(dropTrue) # 快速體檢缺失值、量綱、周期 print(df.describe()) print(缺失值數(shù)量:, df[value].isna().sum()) plt.figure(figsize(14, 4)) plt.plot(df[timestamp], df[value], linewidth0.8) plt.title(Raw Time Series) plt.show()這段代碼做了三件事按時(shí)間排序防止原始數(shù)據(jù)亂序?qū)е禄板e(cuò)位、打印統(tǒng)計(jì)量看均值和標(biāo)準(zhǔn)差判斷是否需要?dú)w一化、畫(huà)原始曲線(xiàn)。parse_dates參數(shù)把時(shí)間列轉(zhuǎn)成 datetime 類(lèi)型后面做重采樣或按時(shí)間切分時(shí)不會(huì)出錯(cuò)。如果describe()出來(lái)的 std 是 mean 的幾十倍說(shuō)明量綱跨度大必須歸一化否則 LSTM 的梯度會(huì)被大數(shù)值主導(dǎo)。2.2 滑動(dòng)窗口構(gòu)造把序列變成監(jiān)督學(xué)習(xí)樣本LSTM 吃的是三維張量(batch, timesteps, features)而原始數(shù)據(jù)是一維序列。中間這步轉(zhuǎn)換就是滑窗也是最容易寫(xiě)錯(cuò)的地方。思路很直白用一個(gè)長(zhǎng)度為 $L$ 的窗口在序列上滑動(dòng)窗口內(nèi)的 $L$ 個(gè)值作為輸入窗口后一個(gè)值作為標(biāo)簽。import numpy as np def create_sequences(data, look_back24, pred_step1): data: 一維 numpy 數(shù)組已歸一化 look_back: 回看窗口長(zhǎng)度 pred_step: 預(yù)測(cè)未來(lái)第幾步 返回: X shape(N, look_back, 1), y shape(N,) X, y [], [] for i in range(len(data) - look_back - pred_step 1): X.append(data[i : i look_back]) y.append(data[i look_back pred_step - 1]) X np.array(X).reshape(-1, look_back, 1) y np.array(y) return X, y # 歸一化到 [0,1]用 MinMaxScaler from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(df[[value]]).flatten() X, y create_sequences(scaled, look_back24, pred_step1) print(X shape:, X.shape, y shape:, y.shape)關(guān)鍵參數(shù)說(shuō)明look_back24表示用過(guò)去 24 個(gè)點(diǎn)預(yù)測(cè)下一個(gè)點(diǎn)。如果你的數(shù)據(jù)是小時(shí)級(jí)24 就是「用過(guò)去一天預(yù)測(cè)下一小時(shí)」如果是日級(jí)24 就是「用過(guò)去 24 天預(yù)測(cè)下一天」。這個(gè)值沒(méi)有萬(wàn)能答案一般從數(shù)據(jù)周期長(zhǎng)度的 1~2 倍開(kāi)始試。pred_step1是單步預(yù)測(cè)改成 3 就是預(yù)測(cè)未來(lái)第 3 個(gè)點(diǎn)注意此時(shí)樣本數(shù)會(huì)減少pred_step-1個(gè)。注意reshape(-1, look_back, 1)里的最后一個(gè) 1 是特征維度。單變量是 1多變量就改成變量個(gè)數(shù)。這個(gè)維度寫(xiě)錯(cuò)是新手最常見(jiàn)的報(bào)錯(cuò)來(lái)源LSTM 層會(huì)直接告訴你輸入維度不匹配。2.3 訓(xùn)練集/測(cè)試集切分時(shí)間序列不能隨機(jī)打亂這是時(shí)間序列和普通機(jī)器學(xué)習(xí)最大的區(qū)別。圖像分類(lèi)可以隨機(jī) shuffle時(shí)間序列絕對(duì)不行——你用未來(lái)的數(shù)據(jù)訓(xùn)練再用過(guò)去的數(shù)據(jù)測(cè)試這叫數(shù)據(jù)泄漏指標(biāo)會(huì)好看得離譜上線(xiàn)就翻車(chē)。正確做法是按時(shí)間順序切前 80% 做訓(xùn)練后 20% 做測(cè)試。如果數(shù)據(jù)量夠大再?gòu)那?80% 里切 10% 做驗(yàn)證集用于早停。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:] # 驗(yàn)證集從訓(xùn)練集尾部再切 10% val_size int(len(X_train) * 0.1) X_val, y_val X_train[-val_size:], y_train[-val_size:] X_train, y_train X_train[:-val_size], y_train[:-val_size] print(f訓(xùn)練集: {X_train.shape}, 驗(yàn)證集: {X_val.shape}, 測(cè)試集: {X_test.shape})多變量場(chǎng)景下create_sequences里的data換成二維數(shù)組(T, n_features)reshape 時(shí)最后一維改成n_features。歸一化要對(duì)每一列單獨(dú)做或者統(tǒng)一做——取決于各列量綱是否接近。如果溫度是 0~40、風(fēng)速是 0~100建議分開(kāi)歸一化否則風(fēng)速會(huì)壓制溫度的信號(hào)。數(shù)據(jù)準(zhǔn)備這塊做完你應(yīng)該拿到四個(gè)數(shù)組X_train, y_train, X_val, y_val外加測(cè)試集。形狀對(duì)不上、維度搞錯(cuò)、忘了歸一化是后面 80% 報(bào)錯(cuò)的根源務(wù)必在這里 print 確認(rèn)。3. 用 PyTorch 搭一個(gè)能收斂的 LSTM 預(yù)測(cè)模型3.1 模型結(jié)構(gòu)選型單層還是堆疊hidden_size 怎么定PyTorch 的nn.LSTM是最常用的實(shí)現(xiàn)。一個(gè)最小可用的預(yù)測(cè)模型長(zhǎng)這樣LSTM 層 → 取最后一個(gè)時(shí)間步的隱狀態(tài) → 全連接層映射到輸出維度。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers1, output_size1, dropout0.0): super().__init__() self.hidden_size hidden_size self.num_layers num_layers self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, # 輸入格式 (batch, seq, feature) dropoutdropout if num_layers 1 else 0.0 ) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) self.lstm(x) # 取最后一個(gè)時(shí)間步的輸出 last out[:, -1, :] return self.fc(last)參數(shù)怎么定hidden_size從 32 或 64 起步數(shù)據(jù)量大、模式復(fù)雜再往上加但超過(guò) 256 在小數(shù)據(jù)集上基本是過(guò)擬合。num_layers建議先用 1效果不夠再加到 2加到 3 以上收益遞減且訓(xùn)練變慢。dropout只在num_layers 1時(shí)生效單層 LSTM 加 dropout 是無(wú)效的——這是 PyTorch 的一個(gè)設(shè)計(jì)細(xì)節(jié)很多人不知道。batch_firstTrue這個(gè)參數(shù)必須顯式設(shè)置否則輸入格式是(seq, batch, feature)和大多數(shù)人習(xí)慣的維度順序相反會(huì)導(dǎo)致莫名其妙的形狀錯(cuò)誤。3.2 訓(xùn)練循環(huán)損失函數(shù)、優(yōu)化器與早停時(shí)間序列回歸用 MSELoss優(yōu)化器用 Adam學(xué)習(xí)率從 1e-3 開(kāi)始。訓(xùn)練循環(huán)里加上驗(yàn)證集監(jiān)控和早停防止過(guò)擬合。from torch.utils.data import DataLoader, TensorDataset # 轉(zhuǎn)成 Tensor X_train_t torch.FloatTensor(X_train) y_train_t torch.FloatTensor(y_train) X_val_t torch.FloatTensor(X_val) y_val_t torch.FloatTensor(y_val) train_loader DataLoader(TensorDataset(X_train_t, y_train_t), batch_size32, shuffleTrue) device torch.device(cuda if torch.cuda.is_available() else cpu) model LSTMPredictor(input_size1, hidden_size64, num_layers1).to(device) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) best_val_loss float(inf) patience, counter 10, 0 for epoch in range(200): model.train() train_loss 0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() pred model(xb).squeeze(-1) loss criterion(pred, yb) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() * xb.size(0) train_loss / len(X_train_t) # 驗(yàn)證 model.eval() with torch.no_grad(): val_pred model(X_val_t.to(device)).squeeze(-1) val_loss criterion(val_pred, y_val_t.to(device)).item() if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model.pth) counter 0 else: counter 1 if counter patience: print(fEarly stop at epoch {epoch}) break if epoch % 20 0: print(fEpoch {epoch}: train_loss{train_loss:.6f}, val_loss{val_loss:.6f})幾個(gè)關(guān)鍵點(diǎn)clip_grad_norm_是 LSTM 訓(xùn)練的后悔藥梯度爆炸時(shí)它能救你一命max_norm1.0是常用值。batch_size32是起點(diǎn)數(shù)據(jù)量小就降到 16 或 8。早停的patience10表示驗(yàn)證損失連續(xù) 10 輪不降就停避免無(wú)效訓(xùn)練。提示如果訓(xùn)練 loss 一直不降先檢查學(xué)習(xí)率是不是太大loss 震蕩或太小loss 幾乎不動(dòng)再檢查數(shù)據(jù)歸一化是否做了。這兩點(diǎn)排查完90% 的「模型不收斂」問(wèn)題就解決了。3.3 預(yù)測(cè)與反歸一化把結(jié)果還原成真實(shí)量綱模型輸出的是歸一化后的值必須用 scaler 反變換回原始量綱才能和真實(shí)值對(duì)比。model.load_state_dict(torch.load(best_model.pth)) model.eval() with torch.no_grad(): pred_scaled model(torch.FloatTensor(X_test).to(device)).squeeze(-1).cpu().numpy() # 反歸一化 pred scaler.inverse_transform(pred_scaled.reshape(-1, 1)).flatten() true scaler.inverse_transform(y_test.reshape(-1, 1)).flatten() # 評(píng)估指標(biāo) from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(true, pred) rmse np.sqrt(mean_squared_error(true, pred)) print(fMAE: {mae:.4f}, RMSE: {rmse:.4f}) plt.figure(figsize(14, 4)) plt.plot(true, labelTrue, linewidth1) plt.plot(pred, labelPred, linewidth1) plt.legend() plt.title(Prediction vs Ground Truth) plt.show()反歸一化這步經(jīng)常被忘導(dǎo)致畫(huà)出來(lái)的預(yù)測(cè)曲線(xiàn)和真實(shí)曲線(xiàn)量綱對(duì)不上看著像「預(yù)測(cè)完全錯(cuò)了」其實(shí)只是沒(méi)還原。MAE 和 RMSE 是最常用的兩個(gè)指標(biāo)RMSE 對(duì)大誤差更敏感如果 RMSE 遠(yuǎn)大于 MAE說(shuō)明存在個(gè)別預(yù)測(cè)偏差很大的點(diǎn)值得單獨(dú)看是哪些時(shí)刻。4. 調(diào)參與評(píng)估讓預(yù)測(cè)曲線(xiàn)真正貼合真實(shí)走勢(shì)4.1 look_back 與 hidden_size 的聯(lián)合調(diào)參這兩個(gè)參數(shù)是影響效果最直接的一對(duì)。look_back 決定模型能看到多長(zhǎng)的歷史hidden_size 決定模型能記住多少信息。經(jīng)驗(yàn)做法是固定一個(gè)調(diào)另一個(gè)做網(wǎng)格搜索。look_backhidden_size驗(yàn)證集 RMSE訓(xùn)練耗時(shí)12320.042快24640.031中48640.029中481280.028慢961280.035慢這張表是典型形態(tài)look_back 太小模型看不到完整周期欠擬合太大引入過(guò)多噪聲且訓(xùn)練樣本數(shù)減少。hidden_size 從 64 加到 128 收益很小但耗時(shí)翻倍。一般找到驗(yàn)證集 RMSE 最低的組合就停不要盲目堆大。4.2 多步預(yù)測(cè)的兩種策略與誤差累積單步預(yù)測(cè)pred_step1每次只預(yù)測(cè)下一個(gè)點(diǎn)多步預(yù)測(cè)要預(yù)測(cè)未來(lái)多個(gè)點(diǎn)。兩種做法直接多輸出模型輸出維度改成 pred_step和滾動(dòng)預(yù)測(cè)預(yù)測(cè)一個(gè)點(diǎn)后把它拼回輸入再預(yù)測(cè)下一個(gè)。滾動(dòng)預(yù)測(cè)實(shí)現(xiàn)簡(jiǎn)單但誤差會(huì)累積預(yù)測(cè) 10 步之后基本就飄了。直接多輸出訓(xùn)練難度大一些但誤差不會(huì)累積。期末大作業(yè)一般單步就夠了如果要求預(yù)測(cè)未來(lái)一周建議用直接多輸出把output_size改成 7。# 直接多輸出標(biāo)簽從單值變成序列 def create_multi_step_sequences(data, look_back24, pred_step7): X, y [], [] for i in range(len(data) - look_back - pred_step 1): X.append(data[i : i look_back]) y.append(data[i look_back : i look_back pred_step]) return np.array(X).reshape(-1, look_back, 1), np.array(y)注意此時(shí)y的形狀是(N, pred_step)模型最后的fc層輸出維度要改成pred_step損失函數(shù)不變。4.3 殘差分析與失敗案例定位預(yù)測(cè)曲線(xiàn)畫(huà)出來(lái)之后不要只看整體。把殘差真實(shí)值減預(yù)測(cè)值單獨(dú)畫(huà)一條曲線(xiàn)看它有沒(méi)有規(guī)律。如果殘差在某個(gè)時(shí)間段系統(tǒng)性偏正或偏負(fù)說(shuō)明模型沒(méi)學(xué)到那個(gè)時(shí)段的模式可能是訓(xùn)練集里這類(lèi)樣本太少。如果殘差在突變點(diǎn)附近特別大說(shuō)明模型對(duì)突變不敏感——這是 LSTM 的固有短板它對(duì)平滑趨勢(shì)預(yù)測(cè)好對(duì)突發(fā)跳變反應(yīng)慢。定位失敗案例的方法把殘差絕對(duì)值最大的 10 個(gè)時(shí)間點(diǎn)找出來(lái)回看原始數(shù)據(jù)在這些點(diǎn)附近發(fā)生了什么。是節(jié)假日是傳感器異常還是數(shù)據(jù)本身有錯(cuò)這一步做完你對(duì)「模型能預(yù)測(cè)什么、不能預(yù)測(cè)什么」會(huì)有清晰的認(rèn)識(shí)寫(xiě)報(bào)告時(shí)也有話(huà)可說(shuō)。5. 避坑與排查L(zhǎng)STM 時(shí)間序列預(yù)測(cè)的 5 個(gè)血淚教訓(xùn)5.1 現(xiàn)象loss 變成 nan訓(xùn)練幾輪后直接崩原因?qū)W習(xí)率過(guò)大導(dǎo)致梯度爆炸或者數(shù)據(jù)里有 inf/nan 值沒(méi)清理。LSTM 的循環(huán)結(jié)構(gòu)對(duì)梯度爆炸特別敏感。解決先把學(xué)習(xí)率降到 1e-4 試加上clip_grad_norm_(model.parameters(), max_norm1.0)。然后檢查數(shù)據(jù)np.isnan(data).sum()和np.isinf(data).sum()有的話(huà)用前向填充或插值補(bǔ)上。歸一化之前一定要做這步。5.2 現(xiàn)象訓(xùn)練 loss 很低測(cè)試 loss 很高預(yù)測(cè)曲線(xiàn)滯后原因過(guò)擬合或者 look_back 太大導(dǎo)致模型在訓(xùn)練集上記住了噪聲。另一個(gè)常見(jiàn)原因是數(shù)據(jù)泄漏——切分時(shí)不小心把未來(lái)數(shù)據(jù)混進(jìn)了訓(xùn)練集。解決先確認(rèn)切分是按時(shí)間順序的沒(méi)有 shuffle。然后加 dropout注意只在多層時(shí)生效、減小 hidden_size、加早停。如果預(yù)測(cè)曲線(xiàn)整體滯后于真實(shí)曲線(xiàn)這是單步預(yù)測(cè)的固有特性可以嘗試預(yù)測(cè)差分而不是原值。5.3 現(xiàn)象預(yù)測(cè)值幾乎是一條直線(xiàn)完全不跟隨波動(dòng)原因模型欠擬合通常是 hidden_size 太小或訓(xùn)練輪數(shù)不夠。也可能是歸一化后數(shù)據(jù)方差太小模型學(xué)不到變化。解決把 hidden_size 從 32 加到 64 或 128訓(xùn)練輪數(shù)加到 200 以上。檢查歸一化后的數(shù)據(jù)標(biāo)準(zhǔn)差如果小于 0.01說(shuō)明原始數(shù)據(jù)波動(dòng)本來(lái)就小或者歸一化方式不對(duì)比如用了錯(cuò)誤的 scaler。5.4 現(xiàn)象換了臺(tái)機(jī)器跑結(jié)果完全不一樣原因隨機(jī)種子沒(méi)固定。PyTorch 的權(quán)重初始化、DataLoader 的 shuffle 都帶隨機(jī)性。解決在代碼開(kāi)頭固定種子。import torch, numpy as np, random def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True set_seed(42)這樣每次跑的結(jié)果可復(fù)現(xiàn)寫(xiě)報(bào)告時(shí)數(shù)據(jù)才站得住。5.5 現(xiàn)象GPU 顯存不夠batch_size 降到 1 還是 OOM原因序列太長(zhǎng)或 hidden_size 太大LSTM 的中間狀態(tài)占用顯存和序列長(zhǎng)度成正比。解決減小 look_back或者用torch.utils.data.DataLoader的pin_memory配合梯度累積小 batch 多次累加再更新。實(shí)在不行就回 CPU 跑小數(shù)據(jù)量下 CPU 和 GPU 差距沒(méi)那么大。6. 進(jìn)階技巧讓 LSTM 預(yù)測(cè)再上一個(gè)臺(tái)階單靠一個(gè)裸 LSTM效果往往卡在某個(gè)瓶頸。想讓預(yù)測(cè)曲線(xiàn)更貼合有幾個(gè)經(jīng)過(guò)驗(yàn)證的改進(jìn)方向。第一個(gè)是殘差連接 層歸一化。在 LSTM 外面包一層殘差讓模型學(xué)「變化量」而不是「絕對(duì)值」對(duì)趨勢(shì)明顯的序列特別有效。層歸一化放在 LSTM 輸出后能穩(wěn)定訓(xùn)練。class ResidualLSTM(nn.Module): def __init__(self, input_size1, hidden_size64): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, batch_firstTrue) self.norm nn.LayerNorm(hidden_size) self.fc nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) out self.norm(out[:, -1, :]) return self.fc(out)第二個(gè)是輸入特征工程。除了原始值把「小時(shí)」「星期幾」「是否周末」這類(lèi)時(shí)間特征拼進(jìn)去多變量輸入能讓模型捕捉周期性。做法是把input_size從 1 改成特征數(shù)create_sequences里對(duì)應(yīng)擴(kuò)展。第三個(gè)是驗(yàn)證方法。不要只看一條測(cè)試集的曲線(xiàn)用滾動(dòng)預(yù)測(cè)的方式在測(cè)試集上逐點(diǎn)預(yù)測(cè)模擬真實(shí)上線(xiàn)場(chǎng)景。具體做法是每次預(yù)測(cè)一個(gè)點(diǎn)后把真實(shí)值不是預(yù)測(cè)值拼回輸入窗口這樣評(píng)估的是「給定真實(shí)歷史預(yù)測(cè)下一步」的能力比一次性預(yù)測(cè)整段更接近實(shí)際使用。我自己的習(xí)慣是任何 LSTM 項(xiàng)目先跑通單變量單步的 baseline記錄 MAE 和 RMSE然后再逐個(gè)加改進(jìn)每次只改一個(gè)變量對(duì)比指標(biāo)。這樣能清楚知道哪個(gè)改動(dòng)真正有用而不是一鍋亂燉之后不知道誰(shuí)起了作用。數(shù)據(jù)準(zhǔn)備階段多花的那半小時(shí)永遠(yuǎn)比后面調(diào)參的兩小時(shí)值。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取