測:從數(shù)據(jù)預(yù)處理到多步預(yù)測的完整實踐)
簡介針對地鐵站點日常客流預(yù)測需求這份基于LSTM的交通客流預(yù)測壓縮包給出了從數(shù)據(jù)處理到模型訓(xùn)練與可視化的完整方案。項目以2019年某地鐵站平日客流量及天氣因素為輸入按8:2劃分訓(xùn)練集與測試集借助numpy、pandas完成預(yù)處理再通過sklearn、LSTM神經(jīng)網(wǎng)絡(luò)建模預(yù)測并利用matplotlib、seaborn輸出結(jié)果適合機器學(xué)習(xí)初學(xué)者與交通數(shù)據(jù)分析人員學(xué)習(xí)參考。壓縮包共17個文件含5個csv原始數(shù)據(jù)、2個h5訓(xùn)練模型、1個py預(yù)測腳本還有doc/ppt/xlsx等說明與展示材料整體3.95MB。目前已有1292人瀏覽學(xué)習(xí)。通過該資源可系統(tǒng)掌握客流量預(yù)測的完整流程獲得可直接運行的代碼、預(yù)訓(xùn)練模型及答辯PPT能夠快速復(fù)現(xiàn)實驗并延伸應(yīng)用到同類場景。1. 基于 LSTM 的交通客流預(yù)測資源包一個能直接跑通的時間序列項目如果你做過客流預(yù)測一定見過這種場景早高峰地鐵閘機的出站量、節(jié)假日前一天的高速收費站車流、商場促銷時的進店人數(shù)波動大、周期強、突發(fā)情況多用傳統(tǒng) ARIMA 或移動平均很難追上突變。這份《基于LSTM交通客流預(yù)測.zip》正是一個把 LSTM 應(yīng)用到短時客流預(yù)測的完整資源核心鏈路是「時序數(shù)據(jù) → 滑窗構(gòu)造樣本 → LSTM 訓(xùn)練 → 反歸一化評估」不依賴第三方付費數(shù)據(jù)源用公開的客流序列就能復(fù)現(xiàn)。適合三類人剛?cè)腴T時間序列預(yù)測的學(xué)生想把 LSTM 落到實際業(yè)務(wù)但不想從零搭框架的數(shù)據(jù)工程師以及需要一份基線模型做對比的實驗人員。2. 數(shù)據(jù)預(yù)處理把原始客流序列變成模型能吃的時間窗LSTM 吃的是「序列」不是單條記錄。原始數(shù)據(jù)通常是按 15 分鐘或 1 小時間隔記錄的客流數(shù)值比如[2024-01-01 08:00, 352]。這個結(jié)構(gòu)離模型需要的輸入形狀還差兩步構(gòu)造滑窗、做歸一化。順序不能反先滑窗再歸一化是常規(guī)做法但更穩(wěn)的是先歸一化再滑窗理由后面第五章節(jié)會說。2.1 拿到數(shù)據(jù)先別急著建模型先做缺失值與異常值客流數(shù)據(jù)最常見的臟數(shù)據(jù)有兩種。第一種是設(shè)備斷傳導(dǎo)致的整段缺失常見于閘機或攝像頭采集鏈路表現(xiàn)為連續(xù)數(shù)小時數(shù)值為 0 或直接沒有記錄第二種是瞬時尖峰比如某天早高峰因為列車晚點導(dǎo)致出站量突然翻倍這類點不一定是噪聲但對訓(xùn)練影響很大。我的處理習(xí)慣是先做探索性可視化把序列畫出來用肉眼確認缺失段和異常段的位置再決定填充策略。處理缺失值時客流序列按周和按天都有明顯周期性所以用前一周同一天同時段的均值填充比用線性插值更符合業(yè)務(wù)規(guī)律。異常值用滾動中位數(shù)檢測以 24 個點為一個窗口如果某個點的值超過窗口內(nèi)中位數(shù)的 3 倍或低于其 1/3就標記為異常然后用前后兩個正常點的均值替換。注意這里不要用整體均值替換節(jié)假日或促銷日的高峰值一旦被整體均值拉平模型訓(xùn)練出來會系統(tǒng)性低估峰期客流。import pandas as pd import numpy as np def load_and_clean(df): # df 必須包含兩列: timestamp 和 flow df[timestamp] pd.to_datetime(df[timestamp]) df df.set_index(timestamp).sort_index() # 缺失填充: 前一周同一時刻的均值 df[flow] df[flow].replace(0, np.nan) # 0 值當作缺失處理 for idx in df[df[flow].isna()].index: week_ago idx - pd.Timedelta(days7) df.loc[idx, flow] df.loc[week_ago, flow] # 異常值替換: 滾動中位數(shù) 3 倍閾值 median df[flow].rolling(24, min_periods1).median() diff (df[flow] - median).abs() threshold median * 3 df.loc[diff threshold, flow] median.loc[diff threshold] return df這段代碼兩條核心邏輯缺失值用「上周同期」語義填充而不是簡單的前向填充這是考慮到了客流數(shù)據(jù)的周期屬性異常值檢測用的是滾動中位數(shù)而非滾動均值因為中位數(shù)對尖峰本身不敏感不會出現(xiàn)「均值被尖峰拉高、結(jié)果尖峰反而看起來正?!沟淖晕颐?。rolling(24)里的 24 對應(yīng)小時粒度下的一天如果你數(shù)據(jù)是 15 分鐘粒度就填 96。2.2 滑窗構(gòu)造樣本window 大小和步長的取舍邏輯滑窗是把連續(xù)序列切成「過去 N 個時間點預(yù)測未來 M 個時間點」的樣本對。窗口大小是這份資源里最值得調(diào)的第一個參數(shù)。我的經(jīng)驗法則窗口至少覆蓋一個完整的日周期小時粒度至少 2415 分鐘粒度至少 96如果數(shù)據(jù)包含周末和工作日兩種模式可以考慮把窗口拉到 48 小時讓模型看到跨天規(guī)律。但窗口不是越大越好LSTM 對這種長序列的記憶能力有限超長窗口反而會引入大量無關(guān)歷史信息把訓(xùn)練時間拉長的同時還不見得提升精度。步長step控制樣本的重疊程度。步長等于 1 時相鄰兩個樣本只錯開一個時間點數(shù)據(jù)量最大但相鄰樣本幾乎一樣訓(xùn)練耗時高步長等于預(yù)測長度時樣本完全不重疊數(shù)據(jù)量小但能覆蓋更長的時間跨度。我的默認配置是窗口 48、步長 24這樣既保留了日周期特征又不會讓訓(xùn)練集膨脹到十萬級。下面是滑窗構(gòu)造的核心代碼。def create_sequences(data, window48, step24, pred_len1): X, y [], [] # 按時間順序滑動不能打亂 for i in range(0, len(data) - window - pred_len 1, step): X.append(data[i : i window]) y.append(data[i window : i window pred_len]) return np.array(X), np.array(y) # 歸一化必須在滑窗之前且 fit 只能用在訓(xùn)練集上 from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_raw.reshape(-1, 1)) val_scaled scaler.transform(val_raw.reshape(-1, 1)) X_train, y_train create_sequences(train_scaled.flatten(), window48, step24) X_val, y_val create_sequences(val_scaled.flatten(), window48, step24)create_sequences返回的X形狀是(樣本數(shù), window, 1)第三個維度是特征數(shù)單變量預(yù)測時就是 1。這里有個新手最容易忽略的點scaler.fit_transform只對訓(xùn)練集做驗證集和測試集用transform讓驗證集的數(shù)據(jù)分布「模型沒見過」評估結(jié)果才可信。歸一化到 [0,1] 是 LSTM 這類梯度敏感模型的標準做法客流數(shù)值動輒幾千上萬的量級如果直接喂給模型激活函數(shù)的梯度會迅速飽和。3. 模型訓(xùn)練搭 LSTM 網(wǎng)絡(luò)與調(diào)參的落地路徑數(shù)據(jù)處理完下一步就是把滑窗樣本喂進 PyTorch 的nn.LSTM。這章先說網(wǎng)絡(luò)結(jié)構(gòu)怎么定再說訓(xùn)練循環(huán)怎么寫全程按能跑出合理結(jié)果的標準來配置不給花活。3.1 網(wǎng)絡(luò)結(jié)構(gòu)nn.LSTM 四個關(guān)鍵參數(shù)的選型nn.LSTM里有四個參數(shù)直接決定模型容量input_size、hidden_size、num_layers、batch_first。input_size等于每個時間步的特征維度單變量客流預(yù)測就是 1如果你加了天氣、節(jié)假日特征這里就變成特征總數(shù)。hidden_size是隱狀態(tài)維度客流預(yù)測任務(wù) 64 到 128 之間是甜點區(qū)間太小擬合不了周期性波動太大容易過擬合且訓(xùn)練時間成倍上漲。num_layers一般取 1 或 2客流序列的規(guī)律還沒復(fù)雜到需要三層以上堆疊。batch_firstTrue讓輸入形狀變?yōu)?batch, seq_len, input_size)符合直覺代碼不容易繞暈。需要注意nn.LSTM的輸出是一個元組(output, (h_n, c_n))做單步回歸時常規(guī)做法是取最后一層在最后一個時間步的隱狀態(tài)h_n[-1]再過一個全連接層把維度壓到預(yù)測長度。下面是我在這類任務(wù)上常用的一種結(jié)構(gòu)。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, output_size1, dropout0.2): super().__init__() self.lstm nn.LSTM(input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch, window, input_size) out, (h_n, c_n) self.lstm(x) # h_n[-1]: 最后一層最后一個時間步的隱狀態(tài) last_hidden h_n[-1] # (batch, hidden_size) pred self.fc(last_hidden) # (batch, output_size) return prednum_layers2時如果dropout傳 0.2PyTorch 只會在兩個 LSTM 層之間丟不會作用于輸出層這個dropout是層間隨機失活不是給輸入數(shù)據(jù)加噪聲理解錯容易誤調(diào)。last_hidden h_n[-1]這個寫法的含義是取最深那一層在所有 batch 樣本上的最后時刻隱狀態(tài)它保留了整個輸入序列壓縮后的信息是序列到單點回歸的標準取法。3.2 訓(xùn)練流程損失函數(shù)、優(yōu)化器與早??土黝A(yù)測本質(zhì)是回歸任務(wù)損失函數(shù)用均方誤差nn.MSELoss()即可它對大誤差值比如峰期預(yù)測偏差幾百人的懲罰呈平方級放大倒逼模型把峰期擬合得更準。優(yōu)化器選 Adam學(xué)習(xí)率默認 0.001這是時間序列任務(wù)里最不容易翻車的起手配置。訓(xùn)練循環(huán)里比「前向傳播」更需要留意的是數(shù)據(jù)形狀要對齊X_train的形狀是(樣本數(shù), 窗口, 特征數(shù))但到了模型里 PyTorch 自動把第一維當 batch這正好對應(yīng)設(shè)計。早停early stopping建議自己寫一個最簡版本盯驗證集損失連續(xù) 20 個 epoch 沒有下降就保存歷史最優(yōu)權(quán)重并終止訓(xùn)練??土鲾?shù)據(jù)本身噪聲大訓(xùn)練集損失降到很低也不代表泛化驗證集才是模型真實能力的照妖鏡。def train_model(model, X_train, y_train, X_val, y_val, epochs100, lr0.001, patience20): optimizer torch.optim.Adam(model.parameters(), lrlr) criterion nn.MSELoss() best_val_loss float(inf) wait 0 for epoch in range(epochs): model.train() optimizer.zero_grad() pred model(X_train) loss criterion(pred, y_train) loss.backward() optimizer.step() model.eval() with torch.no_grad(): val_pred model(X_val) val_loss criterion(val_pred, y_val).item() # 早停: 驗證損失連續(xù) patience 輪不降則終止 if val_loss best_val_loss: best_val_loss val_loss wait 0 torch.save(model.state_dict(), best_lstm.pt) else: wait 1 if wait patience: break model.load_state_dict(torch.load(best_lstm.pt, weights_onlyTrue)) return modeloptimizer.zero_grad()必須在loss.backward()之前漏掉這行梯度就會跨 batch 累積訓(xùn)練損失曲線會呈現(xiàn)不規(guī)則的鋸齒。每個 epoch 結(jié)束切到model.eval()再算驗證損失是為了讓 dropout 層在驗證階段不生效否則驗證指標每次前向傳播都帶隨機性沒法做早停判斷。torch.save保存的是網(wǎng)絡(luò)參數(shù)訓(xùn)練完用load_state_dict恢復(fù)到驗證集最優(yōu)狀態(tài)避免因最后幾輪過擬合把模型帶偏。4. 評估驗證RMSE、滯后效應(yīng)與預(yù)測結(jié)果的可信度訓(xùn)練跑通只是第一步模型到底能不能用要看它在測試集上的表現(xiàn)。這章講三件事指標怎么算、預(yù)測曲線怎么看、以及評估里最容易被忽略的滯后效應(yīng)。4.1 評估指標RMSE、MAE 與 MAPE 各看什么評估 LSTM 客流預(yù)測推薦同時算三個指標各有側(cè)重點只看一個容易自欺欺人。RMSE 對大幅偏差敏感能反映峰期預(yù)測失誤的嚴重程度MAE 是平均絕對誤差單位與人次一致最直觀MAPE 是相對誤差但由于客流分母可能接近 0深夜低谷時段會導(dǎo)致 MAPE 虛高所以建議只算白天時段的 MAPE。計算指標前必須先把預(yù)測結(jié)果做反歸一化inverse_transform直接在 [0,1] 尺度上算誤差得到的數(shù)值沒有任何業(yè)務(wù)含義。常見錯誤是把驗證集的預(yù)測值和真實值各自inverse_transform但注意真實值在歸一化時是按「整體訓(xùn)練集最大最小值」縮放的反變換時也要用同一個 scaler不能重新 fit。def evaluate(y_true, y_pred): # y_true, y_pred 傳入時是原始人次尺度 rmse np.sqrt(np.mean((y_true - y_pred) ** 2)) mae np.mean(np.abs(y_true - y_pred)) mask y_true 0 mape np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100 return {RMSE: round(rmse, 2), MAE: round(mae, 2), MAPE: round(mape, 2)} # 反歸一化示例 pred_org scaler.inverse_transform(pred_scaled.reshape(-1, 1)).flatten() true_org scaler.inverse_transform(y_val_scaled.reshape(-1, 1)).flatten() print(evaluate(true_org, pred_org))mask y_true 0這行是為了剔除深夜零客流的時段否則這些時段真實值可能是 5、10預(yù)測值即使只差 3 人次MAPE 也會暴漲到 60% 以上直接把整體指標搞失真。RMSE、MAE、MAPE 三個值建議同時看如果 MAE 不大但 RMSE 明顯偏高說明存在少數(shù)預(yù)測極差的時段這些時段大概率集中在早晚高峰需要單獨拿出來分析。4.2 滯后效應(yīng)評估圖里最容易被忽視的黑匣子訓(xùn)練完成、指標也打印了但如果你只是埋頭看數(shù)字很可能漏掉最嚴重的問題——預(yù)測曲線滯后。把預(yù)測值和真實值畫在同一張圖上如果預(yù)測曲線整體比真實曲線「晚了一兩個時間點」像貼著真實曲線平移了一段距離說明模型學(xué)到的是「復(fù)制前一個時刻的值」而不是真正預(yù)測未來。這種現(xiàn)象在客流預(yù)測里極其常見尤其是單一客流序列輸入時模型發(fā)現(xiàn)最優(yōu)策略就是把最后的觀測值原樣輸出因為客流本身有強自相關(guān)性這么做損失函數(shù)已經(jīng)很低了。判斷方法很簡單計算預(yù)測序列和真實序列的交叉相關(guān)性看峰值出現(xiàn)在哪個 lag 上更直接的辦法是把預(yù)測結(jié)果整體向前平移一個時間點再算一次 RMSE。如果平移后誤差顯著變小基本可以確認模型在偷懶。解決滯后效應(yīng)有兩個方向一是把窗口拉開到 48 甚至 72讓模型有更多歷史信息去做模態(tài)判斷而不是貼身復(fù)制二是加入多步預(yù)測損失讓模型同時輸出未來 1、2、3 步的預(yù)測值從訓(xùn)練信號上逼它學(xué)到趨勢而非復(fù)制。這段屬于我自己的血淚經(jīng)驗早先做第一個客流模型時指標好看到不行畫圖才發(fā)現(xiàn)全滯后了一拍。5. 避坑指南客流序列在 LSTM 里的五個翻車點這章集中寫我復(fù)現(xiàn)這類資源時踩過、以及周圍同事踩過的具體坑。每一條都是「現(xiàn)象 → 原因 → 解決」三步走建議在自己機器上跑的時候逐條對照。5.1 數(shù)據(jù)泄漏歸一化 fit 到了全量數(shù)據(jù)現(xiàn)象驗證集指標極好RMSE 低得離譜但一到上線或者換新數(shù)據(jù)預(yù)測就崩盤。原因?qū)懘a時圖省事先對整個數(shù)據(jù)集做了MinMaxScaler.fit_transform再接滑窗和劃分訓(xùn)練驗證。這樣驗證集的最小最大值已經(jīng)被模型在歸一化階段「見過」了數(shù)據(jù)分布的邊界信息泄漏到了訓(xùn)練流程里評估結(jié)果虛高。解決嚴格按「先劃分訓(xùn)練/驗證/測試再在訓(xùn)練子集上fit驗證集和測試集只做transform」。我在第 2.2 節(jié)的代碼里就是這個順序照抄就不會踩。數(shù)據(jù)泄漏是這類時序項目里最隱蔽的坑因為指標不會報錯只會給一個不真實的好結(jié)果。5.2 驗證集劃分用了隨機打亂現(xiàn)象訓(xùn)練損失正常下降驗證損失也正常但模型在測試集上一塌糊涂而且沒有滯后問題的曲線也看不出明顯原因。原因train_test_split默認shuffleTrue把時間序列隨機抽成了訓(xùn)練集和驗證集。LSTM 訓(xùn)練時驗證集里混著訓(xùn)練集時刻前后的樣本相當于讓模型提前「看了看」未來數(shù)據(jù)周期規(guī)律被完整泄露。解決時序項目的劃分強制按時間序訓(xùn)練集取前 70%~80%驗證集取再往后的 10%測試集取最后 10%。如果擔(dān)心季節(jié)覆蓋不全可以按「滾動劃分」做多輪評估但每輪內(nèi)部依然嚴格按時間切分不能用隨機抽樣的思路。5.3 訓(xùn)練損失降了驗證 MAPE 卻極高現(xiàn)象訓(xùn)練集 RMSE 很小驗證集 RMSE 也不大但業(yè)務(wù)方問「平均誤差百分之幾」時MAPE 一算 30% 以上直接沒法看。原因前面提過夜間低谷時段的真實客流可能只有 20 人次預(yù)測值 35 人次絕對誤差只有 15RMSE 貢獻微乎其微但 MAPE 是 75%。如果按全天 24 小時包含低谷一起算MAPE 被幾個深夜點拉爆。解決評估指標按業(yè)務(wù)時段區(qū)分。做交通客流運營預(yù)測重點關(guān)注早高峰 7:00~9:00、晚高峰 17:00~19:00 和全天非低谷時段分組計算 MAPE。深夜段用 MAE 衡量即可不要混在一個數(shù)里。5.4 LSTM 輸入形狀 shape 報錯三維變二維的維度錯位現(xiàn)象model(X_train)報錯Expected 3D input, got 2D或者到了nn.Linear時報 hidden_size 和輸入維度對不上。原因滑窗構(gòu)造時X_train是(樣本數(shù), window)的二維數(shù)組而nn.LSTM要求(batch, seq_len, input_size)三維輸入。另一種情況是取隱狀態(tài)時用了h_n而不是h_n[-1]導(dǎo)致 fc 層的輸入維度變成了(batch, num_layers, hidden_size)的展平值。解決構(gòu)造完序列后做一次X_train X_train.reshape(-1, window, 1)確認形狀取最后隱狀態(tài)寫h_n[-1]這是 last layer 的 hidden state維度正好是(batch, hidden_size)和前面的nn.Linear(hidden_size, output_size)能對上。5.5 多特征拼接時忘了對齊時間戳現(xiàn)象加了天氣溫度、是否是節(jié)假日這些特征后訓(xùn)練直接崩或者 Loss 變成 NaN。原因特征表和客流表的索引沒有對齊比如客流是 15 分鐘粒度天氣是小時粒度兩者直接concat后產(chǎn)生了時間偏移模型讀到的「當前時刻溫度」其實是 45 分鐘前的。解決先統(tǒng)一時間粒度客流 15 分鐘粒度時天氣特征按小時向前填充forward fill到 15 分鐘粒度節(jié)假日特征要保證按天對齊不要用未來日期的標簽。拼接前打印df.shape和df.isna().sum()確認沒有形狀錯位。6. 多步預(yù)測與多特征融合把單步預(yù)報擴展成實用方案前面整條鏈路做的是「輸入 48 個小時預(yù)測未來 1 個小時」但實際業(yè)務(wù)里更常見的問題是「現(xiàn)在下午 4 點想預(yù)測今晚 6 點到 8 點的出站客流」。這就涉及多步預(yù)測有兩條路線遞歸預(yù)測和直接多輸出。遞歸預(yù)測的策略是把上一步的預(yù)測值當作下一步的輸入循環(huán)預(yù)測到目標步數(shù)實現(xiàn)簡單但誤差會逐步累積預(yù)測到第 6 步以后曲線會趨于平滑因為模型把不確定性平均化了。直接多輸出則是修改模型最后一層把output_size設(shè)為待預(yù)測步數(shù)一次輸出多步結(jié)果。這兩種方案各有場景做未來 1~3 小時的客流調(diào)配建議用直接多輸出模型一層輸出就能拿到完整預(yù)測曲線。注意直接多輸出時前面滑窗構(gòu)造的pred_len要相應(yīng)調(diào)大訓(xùn)練標簽y的形狀變?yōu)?樣本數(shù), pred_len)。多特征融合是把預(yù)測質(zhì)量往上提一檔的關(guān)鍵。單用客流序列訓(xùn)練模型本質(zhì)上只能學(xué)「過去的客流模式在未來重演」遇到節(jié)假日、暴雨天氣這些外部擾動必然失效。常見做法是加入 4 個特征是否為工作日、是否為節(jié)假日、小時序號0~23 的正弦/余弦編碼、天氣等級0~3 映射。特征拼接的時機是在歸一化之后把所有特征與客流序列一起構(gòu)成input_size5的輸入。加入外部特征后滯后效應(yīng)通常會減輕因為模型不再只依賴歷史客流做貼身復(fù)制而是能感知「今天是工作日且下雨」這種全局狀態(tài)。在那之后我跑客流預(yù)測養(yǎng)成了一個習(xí)慣每換一個數(shù)據(jù)集或改一個特征都強制先看一眼預(yù)測對齊圖再算指標。圖和數(shù)字一起通過才敢把結(jié)果拿出去。模型的結(jié)構(gòu)、參數(shù)、數(shù)據(jù)處理全都可以在這一份資源包里直接改希望這份拆解能幫你在自己的客流數(shù)據(jù)上少走幾段彎路。# 直接多輸出 多特征的訓(xùn)練入口示意 # 假設(shè) feature_cols 已與客流序列按時間對齊并歸一化 X_multi, y_multi create_sequences(multi_feature_data, window48, step24, pred_len4) model LSTMPredictor(input_sizeX_multi.shape[2], hidden_size128, num_layers2, output_size4) model train_model(model, X_multi, y_multi, X_val_multi, y_val_multi, epochs80, lr0.001, patience15)這份資源包里附帶的數(shù)據(jù)集結(jié)構(gòu)、模型初始參數(shù)和訓(xùn)練腳本都是以此為基準寫的。把input_size、hidden_size、window三項按你的數(shù)據(jù)量做加減法就能從一個「能跑的 demo」變成一個「業(yè)務(wù)上敢用的預(yù)測模塊」。希望幫到你。本文還有配套的精品資源點擊獲取