測(cè)成績(jī):Python實(shí)現(xiàn)與常見坑)
簡(jiǎn)介面向機(jī)器學(xué)習(xí)初學(xué)者的LSTM序列預(yù)測(cè)實(shí)戰(zhàn)資源聚焦多變量預(yù)測(cè)與成績(jī)預(yù)測(cè)等應(yīng)用場(chǎng)景系統(tǒng)覆蓋時(shí)間序列數(shù)據(jù)預(yù)處理、監(jiān)督轉(zhuǎn)換、模型定義、訓(xùn)練與評(píng)估的完整流程。壓縮包共33個(gè)文件由19個(gè)CSV數(shù)據(jù)集和14個(gè)Python腳本組成腳本針對(duì)單變量、多變量、多步預(yù)測(cè)分別給出可運(yùn)行實(shí)現(xiàn)CSV數(shù)據(jù)則配套各類實(shí)驗(yàn)場(chǎng)景方便對(duì)照學(xué)習(xí)與二次改造。資源包僅3.88MB輕量易下載已有3419人學(xué)習(xí)使用。內(nèi)容由淺入深從基礎(chǔ)的時(shí)間序列轉(zhuǎn)監(jiān)督學(xué)習(xí)數(shù)據(jù)開始逐步實(shí)現(xiàn)觀測(cè)值縮放、穩(wěn)定性處理、LSTM模型搭建與多步預(yù)測(cè)多變量部分重點(diǎn)演示如何利用多個(gè)輸入特征共同預(yù)測(cè)目標(biāo)適合希望通過Python快速掌握LSTM時(shí)序建模的讀者。借助這套代碼可以清晰理解滑窗構(gòu)造樣本、數(shù)據(jù)穩(wěn)定化、多步預(yù)測(cè)等關(guān)鍵操作并在自帶數(shù)據(jù)集上完成訓(xùn)練、預(yù)測(cè)與效果驗(yàn)證。1. LSTM多變量預(yù)測(cè)成績(jī)?yōu)槭裁催@個(gè)方向值得花一個(gè)周末復(fù)現(xiàn)想把一個(gè)學(xué)生的歷次考試記錄變成下一場(chǎng)考試的分?jǐn)?shù)預(yù)測(cè)LSTM多變量預(yù)測(cè)是目前在 Python 生態(tài)里最容易落地、也最容易翻車的方案。直接丟給線性回歸通常只能得到一條平均趨勢(shì)線碰上考前突擊和成績(jī)起伏就徹底失效。LSTM 要解決的是這類序列問題把成績(jī)、出勤率、作業(yè)完成率等多個(gè)特征按時(shí)間順序喂進(jìn)循環(huán)網(wǎng)絡(luò)讓模型自己記住低谷之后常有反彈這類模式。這篇筆記把一個(gè)可復(fù)現(xiàn)的 Python 方案完整講清楚成績(jī)數(shù)據(jù)怎么切片、LSTM 模型代碼怎么寫、訓(xùn)練時(shí)哪些坑會(huì)翻車以及預(yù)測(cè)結(jié)果到底能不能用到真實(shí)決策里。適合拿小數(shù)據(jù)集練手、想在一兩天內(nèi)跑通整個(gè)時(shí)間序列預(yù)測(cè)流程的從業(yè)者和學(xué)習(xí)者。2. 從成績(jī)數(shù)據(jù)到LSTM輸入滑窗、歸一化與數(shù)據(jù)集劃分2.1 成績(jī)預(yù)測(cè)的本質(zhì)是序列問題不是回歸問題常見做法是把最近一次成績(jī)出勤率作業(yè)完成率拼成一行扁平特征去做回歸。這種做法丟失了最關(guān)鍵的信息時(shí)間順序。連續(xù)兩周下滑之后第三周反彈的概率和連續(xù)五周平穩(wěn)后的概率完全不同但扁平特征向量里看不出這個(gè)順序。LSTM 之所以適合成績(jī)預(yù)測(cè)是因?yàn)樗诿總€(gè)時(shí)間步接收一組特征同時(shí)維護(hù)一個(gè)隱藏狀態(tài)把前幾步的信息帶到當(dāng)前步天然適合這類有前后關(guān)聯(lián)的數(shù)據(jù)。以預(yù)測(cè)下次綜合測(cè)驗(yàn)成績(jī)?yōu)槔龁螛颖据斎胄螤钍?(seq_len, features)。seq_len 取 5代表過去 5 周features 取 5代表每周的數(shù)學(xué)成績(jī)、英語(yǔ)成績(jī)、出勤率、作業(yè)完成率、自習(xí)時(shí)長(zhǎng)。輸出是下一周綜合測(cè)驗(yàn)的分?jǐn)?shù)。這就是標(biāo)準(zhǔn)的多變量時(shí)間序列預(yù)測(cè)多個(gè)歷史變量一個(gè)未來目標(biāo)。和傳統(tǒng)時(shí)間序列模型對(duì)比一下更能看清選型邊界。ARIMA 這類模型要求數(shù)據(jù)平穩(wěn)、需要手動(dòng)確定階數(shù)多變量支持也相對(duì)笨拙XGBoost 這類樹模型能處理多特征但如果不人工構(gòu)造滯后特征它看不到時(shí)間順序。LSTM 的優(yōu)點(diǎn)是多特征直接作為序列輸入缺點(diǎn)是需要更多數(shù)據(jù)、訓(xùn)練不確定性大。成績(jī)記錄只有幾十條時(shí)ARIMA 可能更穩(wěn)但多變量場(chǎng)景下 LSTM 的擴(kuò)展性最好這也是本篇選擇它的核心理由。2.2 用滑窗把成績(jī)歷史轉(zhuǎn)成監(jiān)督學(xué)習(xí)樣本假設(shè)你已經(jīng)準(zhǔn)備好一張表每行是一個(gè)學(xué)生某一周的多維記錄列依次是數(shù)學(xué)成績(jī)、英語(yǔ)成績(jī)、出勤率、作業(yè)完成率、自習(xí)時(shí)長(zhǎng)最后一列是當(dāng)周綜合測(cè)驗(yàn)成績(jī)。LSTM 不能直接吃整張表得先把長(zhǎng)序列切成 (seq_len, features) 的小窗口這一步叫滑窗。import numpy as np def make_windows(X, y, seq_len5): # X: 按時(shí)間排序的多維特征(n_samples, n_features) # y: 每個(gè)時(shí)間步對(duì)應(yīng)的成績(jī)標(biāo)簽(n_samples,) windows_x, windows_y [], [] for i in range(len(X) - seq_len): windows_x.append(X[i:i seq_len]) # 過去 seq_len 周的特征 windows_y.append(y[i seq_len]) # 下一周綜合測(cè)驗(yàn)成績(jī) return np.array(windows_x), np.array(windows_y)邏輯上第 i 到 iseq_len-1 行的特征作為輸入第 iseq_len 行的成績(jī)作為目標(biāo)。窗口之間允許重疊成績(jī)表數(shù)據(jù)量小時(shí)靠重疊才能湊出足夠樣本。len(X) 個(gè)時(shí)間步能切出 len(X)-seq_len 個(gè)窗口如果某個(gè)學(xué)生只有 30 周記錄seq_len 取 5 最后只有 25 條窗口所以歷史記錄不足時(shí)別急著把 seq_len 調(diào)大。這里特意沒做隨機(jī)打亂。一旦打亂時(shí)間順序信息就廢了模型在驗(yàn)證集上的表現(xiàn)會(huì)虛高因?yàn)橥粭l成績(jī)曲線的多段窗口可能被拆進(jìn)了兩個(gè)集合。多變量預(yù)測(cè)的數(shù)據(jù)劃分必須是先按時(shí)間切開再做窗口順序反了后面所有指標(biāo)都沒有參考價(jià)值。2.3 歸一化與數(shù)據(jù)集劃分先切分再 fit 歸一化器LSTM 對(duì)輸入尺度很敏感。成績(jī)是 0 到 100 的大數(shù)出勤率是 0 到 1 的小數(shù)自習(xí)時(shí)長(zhǎng)是 0 到 300 的更大數(shù)直接混在一起梯度會(huì)被大數(shù)值特征主導(dǎo)。用 MinMaxScaler 把全部特征壓到 [0,1] 區(qū)間輸出也壓到 [0,1]模型收斂會(huì)穩(wěn)定很多。from sklearn.preprocessing import MinMaxScaler n_train int(len(X) * 0.7) X_train_raw, X_test_raw X[:n_train], X[n_train:] y_train_raw, y_test_raw y[:n_train], y[n_train:] scaler_x MinMaxScaler() scaler_y MinMaxScaler() X_train_norm scaler_x.fit_transform(X_train_raw) X_test_norm scaler_x.transform(X_test_raw) y_train_norm scaler_y.fit_transform(y_train_raw.reshape(-1, 1)).ravel() y_test_norm scaler_y.transform(y_test_raw.reshape(-1, 1)).ravel() # 劃分完成后再構(gòu)造窗口測(cè)試集樣本不會(huì)被訓(xùn)練集窗口“看到” X_tr, y_tr make_windows(X_train_norm, y_train_norm, seq_len5) X_te, y_te make_windows(X_test_norm, y_test_norm, seq_len5)這里有一個(gè)一步都不能省的細(xì)節(jié)歸一化統(tǒng)計(jì)量只從訓(xùn)練集 fit測(cè)試集調(diào)用 transform 而不是 fit_transform。如果先歸一化再劃分測(cè)試集的最大最小值會(huì)混進(jìn) scaler模型在訓(xùn)練時(shí)等于偷看了未來的數(shù)據(jù)范圍預(yù)測(cè)分?jǐn)?shù)會(huì)被人為抬高。這種錯(cuò)誤在成績(jī)預(yù)測(cè)項(xiàng)目里非常隱蔽因?yàn)閾p失曲線看起來一切正常真實(shí)原因只有換新數(shù)據(jù)時(shí)才會(huì)暴露。為什么用 MinMaxScaler 而不是 StandardScalerLSTM 內(nèi)部使用 tanh 激活輸入范圍落在 [0,1] 能避開 tanh 的飽和區(qū)梯度傳遞更順暢。類別型特征不要直接塞進(jìn) MinMaxScaler課程類型、班級(jí)編號(hào)這類離散列要么刪掉要么做 one-hot 后單獨(dú)處理。3. 用PyTorch搭多變量LSTM模型結(jié)構(gòu)與三處關(guān)鍵配置3.1 為什么選 PyTorch 而不是 Keras成績(jī)數(shù)據(jù)集通常很小幾十到幾百條幾十個(gè) epoch 也就幾秒鐘框架間的運(yùn)算速度差距完全感覺不到。選 PyTorch 的真正理由是動(dòng)態(tài)圖和調(diào)試體驗(yàn)訓(xùn)練時(shí)可以在任意一行打斷點(diǎn)看 hidden state 的形狀方便定位輸入輸出維度在哪一步出錯(cuò)。后面要加注意力、改成多步預(yù)測(cè)時(shí)PyTorch 的代碼改動(dòng)量比 Keras 小得多。如果你更熟悉 Keras用 Sequential 堆 LSTM 層也能跑通但遇到預(yù)測(cè)值是一條直線這類問題時(shí)Keras 對(duì)中間層狀態(tài)的黑匣子會(huì)讓排查多繞一圈。PyTorch 模型代碼直白調(diào)試時(shí)的血淚經(jīng)驗(yàn)更少。另外成績(jī)預(yù)測(cè)這種小數(shù)據(jù)任務(wù)用 CPU 訓(xùn)練完全夠不必強(qiáng)求 CUDA環(huán)境配置成本也降低了。3.2 LSTM 模型代碼與輸入輸出形狀PyTorch 里 LSTM 層的輸入形狀是 (batch, seq_len, input_size)把 batch_firstTrue 打開后更直觀。模型定義如下import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size5, hidden_size32, num_layers1, output_size1): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue ) 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) last_hidden h_n[-1] # 取最后一層最后一個(gè)時(shí)間步的隱狀態(tài) return self.fc(last_hidden) # (batch, output_size)input_size 必須等于特征數(shù) 5。hidden_size 取 32 是成績(jī)這類小數(shù)據(jù)集的常見起點(diǎn)太小記不住趨勢(shì)太大容易過擬合。num_layers 先用 1兩層雖然理論上能建模更復(fù)雜的關(guān)系樣本量小的時(shí)候反而會(huì)讓模型更難收斂。forward 里為什么不直接用 out[:, -1, :]因?yàn)槎鄬?LSTM 時(shí) out 表示最后一層的所有時(shí)間步輸出h_n[-1] 表示最后一層最后一個(gè)時(shí)間步的隱狀態(tài)兩者在取最后一時(shí)間步這件事上等價(jià)但 h_n 的語(yǔ)義更明確后面加層數(shù)不用改動(dòng)。單層模型里怎么取都行按 h_n[-1] 寫更穩(wěn)妥。LSTM 對(duì)輸入輸出維度極其嚴(yán)格新手最容易在這里翻車。x 傳入時(shí)必須是 float32如果原始數(shù)據(jù)是 inttorch.tensor 默認(rèn)保留 int 類型進(jìn) LSTM 直接報(bào) dtype 錯(cuò)誤。構(gòu)造輸入時(shí)養(yǎng)成加 dtypetorch.float32 的習(xí)慣能省掉半個(gè)小時(shí)的排查時(shí)間。3.3 損失函數(shù)、優(yōu)化器與學(xué)習(xí)率成績(jī)預(yù)測(cè)是回歸任務(wù)MSE 是默認(rèn)選擇。MSE 對(duì)大誤差是平方級(jí)懲罰某次預(yù)測(cè)偏 20 分會(huì)比偏 5 分多出 16 倍的損失這符合成績(jī)預(yù)測(cè)寧可保守也不允許離譜的實(shí)際需求。device torch.device(cuda if torch.cuda.is_available() else cpu) model LSTMPredictor(input_size5, hidden_size32, num_layers1).to(device) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, patience5, factor0.5 )Adam 配 lr0.001 是 LSTM 訓(xùn)練最穩(wěn)的起點(diǎn)。成績(jī)數(shù)據(jù)量小梯度本來就波動(dòng)大把 lr 調(diào)到 0.01 很容易在幾個(gè) epoch 內(nèi)把損失打成 NaN。scheduler 的作用是當(dāng)驗(yàn)證損失連續(xù) 5 個(gè) epoch 不再下降時(shí)學(xué)習(xí)率減半等于給訓(xùn)練留了后悔藥。為了防止實(shí)驗(yàn)不可復(fù)現(xiàn)在模型定義前固定隨機(jī)種子def set_seed(seed42): torch.manual_seed(seed) np.random.seed(seed) set_seed(42)不固定種子的話同一份代碼跑三次會(huì)得到三個(gè)不同結(jié)果調(diào)參時(shí)根本無法判斷某個(gè)改動(dòng)是真實(shí)提升還是隨機(jī)波動(dòng)。成績(jī)預(yù)測(cè)這類小數(shù)據(jù)集隨機(jī)種子對(duì)結(jié)果的影響可能比 hidden_size 還大。4. 訓(xùn)練循環(huán)與成績(jī)預(yù)測(cè)評(píng)估指標(biāo)怎么設(shè)才算能用4.1 訓(xùn)練循環(huán)與早停小數(shù)據(jù)集上訓(xùn)練 LSTM最怕的是過擬合和反復(fù)震蕩。固定 epoch 數(shù)不靠譜因?yàn)椴煌卣鹘M合的收斂速度差異很大。寫入早停機(jī)制后驗(yàn)證損失連續(xù) patience 個(gè) epoch 不降就停同時(shí)把出現(xiàn)過的最小驗(yàn)證損失對(duì)應(yīng)的權(quán)重存下來。from torch.utils.data import DataLoader, TensorDataset train_ds TensorDataset(torch.tensor(X_tr, dtypetorch.float32), torch.tensor(y_tr, dtypetorch.float32)) val_ds TensorDataset(torch.tensor(X_te, dtypetorch.float32), torch.tensor(y_te, dtypetorch.float32)) train_loader DataLoader(train_ds, batch_size16, shuffleTrue) val_loader DataLoader(val_ds, batch_size16, shuffleFalse) def evaluate(model, loader): model.eval() total_loss 0.0 with torch.no_grad(): for xb, yb in loader: xb, yb xb.to(device), yb.to(device) pred model(xb).squeeze(-1) total_loss criterion(pred, yb).item() * xb.size(0) return total_loss / len(loader.dataset) def train_model(model, train_loader, val_loader, epochs80, patience10): best_val_loss float(inf) wait 0 for epoch in range(epochs): model.train() epoch_loss 0.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() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() epoch_loss loss.item() * xb.size(0) val_loss evaluate(model, val_loader) scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_lstm.pt) wait 0 else: wait 1 if wait patience: print(fepoch {epoch} 觸發(fā)早停) breakDataLoader 里 shuffleTrue 會(huì)不會(huì)打亂時(shí)間順序不會(huì)。窗口化之后每個(gè)樣本內(nèi)部已經(jīng)保留了 seq_len 的時(shí)間結(jié)構(gòu)打亂的是樣本間的出場(chǎng)順序反而能幫模型減少記憶偏置。測(cè)試集必須 shuffleFalse保證預(yù)測(cè)順序與真實(shí)序列一致。clip_grad_norm_ 這行最容易被省略但小數(shù)據(jù)集上必須加。成績(jī)序列偶爾會(huì)出現(xiàn)極端值反向傳播梯度范數(shù)可能瞬間翻倍歸一化到 [0,1] 緩不住這種尖峰clip 之后訓(xùn)練就穩(wěn)了。max_norm1.0 是保守值如果發(fā)現(xiàn)訓(xùn)練欠擬合可以放寬到 5.0。早停 patience 取 10 意味著至少多等 10 個(gè) epoch 才肯認(rèn)輸。如果 val_loss 前 20 個(gè) epoch 一直在緩慢下降patience 太小會(huì)提前停這時(shí)把 patience 改成 15 再跑一遍。小數(shù)據(jù)集上這種反復(fù)試錯(cuò)很正常。4.2 評(píng)估指標(biāo)MAE、RMSE 與 R2訓(xùn)練損失是歸一化空間的數(shù)字不能直接拿去說損失 0.02 所以效果很好。預(yù)測(cè)值和真實(shí)值都要反歸一化回原始分?jǐn)?shù)再算有業(yè)務(wù)意義的指標(biāo)from sklearn.metrics import mean_absolute_error, r2_score def evaluate_scores(y_true_norm, y_pred_norm, scaler_y): y_true scaler_y.inverse_transform(y_true_norm.reshape(-1, 1)).ravel() y_pred scaler_y.inverse_transform(y_pred_norm.reshape(-1, 1)).ravel() mae mean_absolute_error(y_true, y_pred) rmse float(np.sqrt(((y_true - y_pred) ** 2).mean())) r2 r2_score(y_true, y_pred) return mae, rmse, r2MAE 最容易向不懂模型的人解釋平均差 3.8 分。RMSE 比 MAE 大多少可以判斷誤差分布是否有長(zhǎng)尾——如果 RMSE 接近 MAE 的三倍說明有個(gè)別樣本預(yù)測(cè)得離譜需要回去查那條樣本。R2 在成績(jī)預(yù)測(cè)里到 0.6 到 0.8 就算可用因?yàn)榭荚嚦煽?jī)本身帶有隨機(jī)因素想逼近 1.0 反而說明數(shù)據(jù)里有問題。提示反歸一化時(shí)務(wù)必保證 y_pred 和 y_true 的順序完全對(duì)齊不要在中間做任何排序或隨機(jī)抽樣后再算指標(biāo)否則 MAE 會(huì)被嚴(yán)重低估。4.3 反歸一化與預(yù)測(cè)曲線對(duì)照指標(biāo)只能給結(jié)論看不到問題。把測(cè)試集預(yù)測(cè)結(jié)果畫成曲線能一眼看出模型是不是在追著上一步走或者無腦輸出平均分。成績(jī)按周記錄的話用折線圖加散點(diǎn)標(biāo)記就夠。import matplotlib.pyplot as plt model.load_state_dict(torch.load(best_lstm.pt)) model.eval() y_pred_norm, y_true_norm [], [] with torch.no_grad(): for xb, yb in val_loader: xb xb.to(device) pred model(xb).squeeze(-1) y_pred_norm.append(pred.cpu().numpy()) y_true_norm.append(yb.numpy()) y_pred_norm np.concatenate(y_pred_norm) y_true_norm np.concatenate(y_true_norm) mae, rmse, r2 evaluate_scores(y_true_norm, y_pred_norm, scaler_y) print(fMAE{mae:.2f}, RMSE{rmse:.2f}, R2{r2:.2f}) plt.figure(figsize(10, 4)) plt.plot(y_true_norm, label真實(shí)成績(jī), markero, linewidth1.5) plt.plot(y_pred_norm, labelLSTM預(yù)測(cè), markerx, linewidth1.5) plt.legend() plt.xlabel(測(cè)試樣本編號(hào)按時(shí)間排序) plt.ylabel(成績(jī)分) plt.title(LSTM多變量預(yù)測(cè)成績(jī)對(duì)照) plt.show()對(duì)照?qǐng)D里出現(xiàn)兩類問題需要警惕一是預(yù)測(cè)曲線的波峰全部被削平說明模型學(xué)到了均值回歸沒學(xué)到尖峰模式二是預(yù)測(cè)曲線相對(duì)真實(shí)值整體平移說明可能存在數(shù)據(jù)泄露或者特征里缺少當(dāng)前狀態(tài)信息。這兩類問題單看 MAE 看不出來必須看圖。5. LSTM多變量預(yù)測(cè)常見問題排查五個(gè)踩坑記錄5.1 測(cè)試集指標(biāo)很高但換到下學(xué)期就失效現(xiàn)象在歷史成績(jī)劃分的測(cè)試集上 MAE 只有 3 分左右模型看起來已經(jīng)能用了換到新學(xué)期的數(shù)據(jù)預(yù)測(cè)誤差飆到 8 分以上。原因成績(jī)數(shù)據(jù)里隱藏著時(shí)間結(jié)構(gòu)。不同學(xué)期教師出題難度、班級(jí)整體水平都在變化模型很可能把第幾周學(xué)期編號(hào)這類周期性信息當(dāng)成了硬規(guī)則記住。更常見的元兇是歸一化訓(xùn)練集和測(cè)試集混在一起 fit scaler或者未來數(shù)據(jù)被提前劃進(jìn)訓(xùn)練集屬于典型的數(shù)據(jù)泄露。解決嚴(yán)格按時(shí)間順序切分訓(xùn)練集只占前 70%scaler 只 fit 訓(xùn)練集刪除周次學(xué)期號(hào)這類周期性列避免模型把時(shí)間索引當(dāng)特征硬記。成績(jī)預(yù)測(cè)項(xiàng)目里九成假高精度都是這個(gè)原因。5.2 預(yù)測(cè)值是一條幾乎水平的直線現(xiàn)象測(cè)試集預(yù)測(cè)結(jié)果標(biāo)準(zhǔn)差極小曲線貼著均值走M(jìn)AE 看著還行但完全沒抓住成績(jī)波動(dòng)。原因LSTM 在小數(shù)據(jù)集上很容易學(xué)到輸出平均成績(jī)這種最優(yōu)策略。平方誤差下輸出均值對(duì)大多數(shù)普通樣本已經(jīng)很安全只有少數(shù)尖峰樣本被犧牲掉。hidden_size 過大、num_layers 過多會(huì)加劇模型把波動(dòng)當(dāng)成噪聲濾掉了。解決先把 hidden_size 降到 16 或 8強(qiáng)制模型記憶更少的模式把 dropout 調(diào)低甚至去掉給訓(xùn)練更多自由度。如果確實(shí)需要更強(qiáng)的序列建??紤]加注意力而不是加深層數(shù)。成績(jī)預(yù)測(cè)這里不是越深越好我在這上面翻過車。5.3 損失在前幾個(gè) epoch 變成 NaN現(xiàn)象訓(xùn)練到第 3 到第 5 個(gè) epochloss 突然變成 nan后續(xù)無法恢復(fù)。原因?qū)W習(xí)率過大梯度在某個(gè)極端樣本上爆炸或者原始成績(jī)數(shù)據(jù)里存在缺失值NaN 經(jīng)過歸一化和窗口拼接后沒有報(bào)錯(cuò)直到損失函數(shù)里被放大。解決先檢查數(shù)據(jù)里有沒有 np.isnan(X).any()有就先填充或刪除再把 lr 從 0.001 降到 0.0003同時(shí)保留 clip_grad_norm_。如果還是 NaN看成績(jī)列有沒有無窮值MinMaxScaler 對(duì)無窮值不會(huì)報(bào)錯(cuò)但會(huì)把其他正常值壓成 0。5.4 seq_len 到底取 3、5 還是 10現(xiàn)象seq_len 取 5 時(shí) MAE 是 4.2取 10 時(shí)變成 4.8取 3 時(shí)變成 4.5看不出規(guī)律。原因seq_len 是模型對(duì)多長(zhǎng)的歷史記憶最敏感的超參數(shù)。取太短看不見連續(xù)下滑后的反彈模式取太長(zhǎng)早期信息對(duì)下一周成績(jī)的作用趨近于零反而引入噪聲。成績(jī)數(shù)據(jù)通常不存在長(zhǎng)程依賴5 到 7 周覆蓋一個(gè)月的學(xué)習(xí)節(jié)奏已經(jīng)是極限。解決拿驗(yàn)證集做小網(wǎng)格搜索seq_len 在 [3, 5, 7, 10] 里各跑一遍每次固定 3 個(gè)隨機(jī)種子取平均。這個(gè)超參數(shù)沒有理論最優(yōu)值數(shù)據(jù)決定一切靠玄學(xué)猜不如直接跑表。5.5 預(yù)測(cè)成績(jī)跑出 120 分現(xiàn)象真實(shí)成績(jī)都在 40 到 95 分之間預(yù)測(cè)值卻出現(xiàn) 120 分甚至負(fù)數(shù)。原因輸出層是線性激活LSTM 隱狀態(tài)經(jīng)過 fc 層后可以映射到任意值。歸一化只約束訓(xùn)練目標(biāo)測(cè)試時(shí)模型完全可能外推加上極端歷史成績(jī)的帶動(dòng)預(yù)測(cè)自然脫離合理區(qū)間。解決預(yù)測(cè)后做邊界裁剪低于 0 按 0 算高于 100 按 100 算更穩(wěn)的做法是把輸出層改成 Sigmoid 再乘 100讓模型結(jié)構(gòu)上就不能越界。后一種會(huì)略微壓縮中間區(qū)間如果 R2 因此下降超過 0.05 就退回線性輸出加裁剪。6. 進(jìn)階給LSTM加注意力層專門改善尖峰學(xué)生的預(yù)測(cè)成績(jī)預(yù)測(cè)里另一個(gè)痛點(diǎn)是尖峰連續(xù)低迷后突然考出高分以及穩(wěn)定優(yōu)秀生的偶發(fā)失誤普通 LSTM 都容易把這類劇烈變化平滑掉。一個(gè)改動(dòng)小、收益明顯的方法是給 LSTM 輸出加一層注意力機(jī)制讓模型自動(dòng)對(duì)不同時(shí)間步的歷史狀態(tài)加權(quán)。每個(gè)時(shí)間步的重要程度不再默認(rèn)相等而是由一個(gè)小網(wǎng)絡(luò)根據(jù)當(dāng)前隱狀態(tài)計(jì)算權(quán)重。class AttentionLSTM(nn.Module): def __init__(self, input_size5, hidden_size32, output_size1): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, batch_firstTrue) self.attn nn.Linear(hidden_size, 1) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): out, _ self.lstm(x) # (batch, seq_len, hidden) weights torch.softmax(self.attn(out).squeeze(-1), dim1) context (out * weights.unsqueeze(-1)).sum(dim1) return self.fc(context)代碼把輸出層之前的所有隱狀態(tài)做加權(quán)求和權(quán)重由 softmax 歸一化到和為 1。訓(xùn)練時(shí)網(wǎng)絡(luò)會(huì)自動(dòng)學(xué)會(huì)給考前一周的狀態(tài)更高權(quán)重給兩個(gè)月前的記錄更低權(quán)重。訓(xùn)練循環(huán)、早停、評(píng)估流程全部復(fù)用前面第 4 章的代碼只需把模型類換掉。第一次跑別急著替換原模型先拿基礎(chǔ) LSTM 的輸出當(dāng) baseline。同一套測(cè)試集上同時(shí)評(píng)估兩版注意力版本 MAE 下降超過 0.5 再考慮保留。數(shù)據(jù)量小于 200 條時(shí)這個(gè)提升完全可能是隨機(jī)波動(dòng)用 3 個(gè)隨機(jī)種子跑完取均值再看。我自己的經(jīng)驗(yàn)是先把基礎(chǔ) LSTM 跑通、指標(biāo)能穩(wěn)定復(fù)現(xiàn)再加注意力一次只加一個(gè)變量。這個(gè)順序幫我避開了很多自我感動(dòng)式的調(diào)參。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取