盟勝率預(yù)測畢設(shè):LSTM時(shí)間序列建模與PyTorch實(shí)戰(zhàn))
簡介面向計(jì)算機(jī)相關(guān)專業(yè)畢業(yè)設(shè)計(jì)場景這份基于深度學(xué)習(xí)LSTM的英雄聯(lián)盟勝率預(yù)測項(xiàng)目包含完整源碼與文檔說明覆蓋數(shù)據(jù)采集、存儲(chǔ)、預(yù)處理、模型訓(xùn)練、預(yù)測到頁面展示的端到端流程。資源共56個(gè)文件、8.6MB以16個(gè)Python腳本為主體涵蓋爬蟲抓取、MongoDB存儲(chǔ)、JSON合并、BILSTM_Att模型實(shí)現(xiàn)與訓(xùn)練、預(yù)測消費(fèi)者/生產(chǎn)者等環(huán)節(jié)另搭配前端頁面、配置文件和說明文檔便于理解項(xiàng)目的工程組織方式。項(xiàng)目經(jīng)過嚴(yán)格調(diào)試并可直接運(yùn)行適合作為畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)或期末大作業(yè)也能為入門深度學(xué)習(xí)的讀者提供從數(shù)據(jù)處理到模型部署的完整實(shí)戰(zhàn)范例。壓縮包目錄結(jié)構(gòu)清晰模型權(quán)重、依賴清單、日志和前端資源分區(qū)明確已有94人學(xué)習(xí)結(jié)合文檔說明可快速跑通并在此基礎(chǔ)上做擴(kuò)展。1. 把英雄聯(lián)盟勝率預(yù)測做成畢設(shè)LSTM能在對局?jǐn)?shù)據(jù)里學(xué)到什么英雄聯(lián)盟的對局過程是一條天然的時(shí)間序列經(jīng)濟(jì)差、人頭數(shù)、小龍、大龍、防御塔都在隨時(shí)間變化前后時(shí)刻高度關(guān)聯(lián)。用深度學(xué)習(xí)里的 LSTM長短期記憶網(wǎng)絡(luò)做勝率預(yù)測通常能達(dá)到 60% 到 70% 的驗(yàn)證集準(zhǔn)確率比用全場比賽平均數(shù)值喂給 MLP 的“平均值流”高出不少。這個(gè)方向之所以適合做成 Python 畢業(yè)設(shè)計(jì)是因?yàn)樗褧r(shí)間序列預(yù)測、特征工程、深度學(xué)習(xí)訓(xùn)練、模型評(píng)估四塊內(nèi)容都串在了一個(gè)具體場景里工作量飽滿且每塊都有明確產(chǎn)出。如果你是計(jì)算機(jī)、數(shù)據(jù)科學(xué)、人工智能方向的畢設(shè)學(xué)生或者想自己完整跑一個(gè) LSTM 項(xiàng)目練手順著這個(gè)標(biāo)題做下來得到的不只是“一個(gè)能用的預(yù)測模型”還有一套帶源碼、帶數(shù)據(jù)說明、帶文檔的畢業(yè)設(shè)計(jì)交付物。2. 對局?jǐn)?shù)據(jù)怎么喂給 LSTM從原始特征到滑動(dòng)窗口樣本2.1 拿到一張對局表先想清楚預(yù)測對象是什么做勝率預(yù)測第一步不是寫模型而是把“預(yù)測什么”定義清楚。常見的做法是把一場比賽切成多個(gè)時(shí)間切片每兩分鐘記錄一次當(dāng)前的狀態(tài)特征然后讓模型根據(jù)前 N 個(gè)切片的狀態(tài)預(yù)測這局游戲的最終勝負(fù)。以藍(lán)色方視角為例一個(gè)典型的時(shí)間切片特征向量長這樣# 單條對局記錄示例單位分鐘 [ 5.0, # 游戲進(jìn)行到第 5 分鐘 3, # 藍(lán)色方總擊殺數(shù) 1, # 藍(lán)色方總死亡數(shù) 2, # 藍(lán)色方總助攻數(shù) 850, # 藍(lán)色方團(tuán)隊(duì)經(jīng)濟(jì)百為單位即 850*100 金幣 1, # 藍(lán)色方推掉的防御塔數(shù) 0, # 藍(lán)色方拿到的峽谷先鋒數(shù) 1, # 藍(lán)色方拿到的元素龍數(shù) 0, # 藍(lán)色方是否拿到大龍0/1 1 # 本局最終結(jié)果藍(lán)色方勝利 1失敗 0標(biāo)簽 ]特征未必需要太多核心是“團(tuán)隊(duì)視角 隨時(shí)間變化”。如果拿的是選手個(gè)人數(shù)據(jù)還要按隊(duì)伍聚合如果自定義特征建議起步階段控制在 8 到 12 維方便后面做特征重要性分析。拿到數(shù)據(jù)之后先按game_id 時(shí)間切片的順序排序然后檢查時(shí)間點(diǎn)是否對齊。很多公開數(shù)據(jù)集的原始 CSV 里不同場次的時(shí)間戳是錯(cuò)開的有的從 1 分鐘開始記錄有的從 3 分鐘開始。我一般會(huì)先用df.groupby(game_id)看一眼每局的行數(shù)和時(shí)間分布行數(shù)不齊的就先按時(shí)間重采樣。2.2 滑動(dòng)窗口切序列為什么單條樣本進(jìn)不了 LSTMLSTM 接收的是“一段序列”不是“一行記錄”。如果用第 5 分鐘的單個(gè)切片去預(yù)測整局勝負(fù)信息量明顯不夠但如果用整局從第 1 分鐘到第 30 分鐘的全部切片去預(yù)測模型偷看到了最后的態(tài)勢實(shí)戰(zhàn)意義又消失了。中間態(tài)的做法是滑動(dòng)窗口切樣本把最近 K 個(gè)時(shí)間切片拼成一條輸入序列。import numpy as np def make_sequences(features, labels, window_size5, step2): 把 (N, n_features) 的對局記錄切成長度為 window_size 的序列樣本。 features形狀 (N, n_features)要求已經(jīng)按 game_id 和時(shí)間排序。 labels形狀 (N,)每條時(shí)間切片對應(yīng)的本局最終勝負(fù)標(biāo)簽。 step相鄰兩個(gè)樣本之間的步長控制樣本密度。 sequences, seq_labels [], [] i 0 while i window_size len(features): sequences.append(features[i:i window_size]) # 取窗口內(nèi)最后一條切片的標(biāo)簽作為該窗口的勝負(fù)標(biāo)簽 seq_labels.append(labels[i window_size - 1]) i step return np.stack(sequences), np.array(seq_labels)這段代碼的核心邏輯有三個(gè)。第一窗口window_size5意味著模型每次看最近 5 個(gè)時(shí)間切片通常對應(yīng) 10 分鐘游戲進(jìn)程第二step2是為了讓相鄰窗口有重疊擴(kuò)充樣本量第三標(biāo)簽取窗口最后一條切片的標(biāo)簽也就是這場游戲最終的勝負(fù)。如果一條對局有 20 個(gè)時(shí)間切片window_size5、step2可以切出 8 個(gè)左右的窗口樣本對于上千場對局來說樣本量完全夠 LSTM 訓(xùn)練。窗口大小是最值得調(diào)的參數(shù)之一。窗口太小模型只看到最近幾分鐘的態(tài)勢容易把前期優(yōu)勢當(dāng)結(jié)果窗口太大比如把整局都包進(jìn)去又會(huì)出現(xiàn)“上帝視角”問題。畢設(shè)場景里我一般建議先定 5 或 6等模型跑通后再試 3 和 10 做對比分析。2.3 特征歸一化與訓(xùn)練/驗(yàn)證集切分LSTM 最敏感的兩件事LSTM 內(nèi)部的激活函數(shù)是 tanh 和 sigmoid輸入數(shù)值過大或過小梯度都會(huì)出問題。做歸一化時(shí)不能只對每個(gè)特征列單獨(dú)歸一化還要考慮“時(shí)間切片之間”的先后關(guān)系。常見做法是先在全體訓(xùn)練數(shù)據(jù)上計(jì)算每個(gè)特征列的均值和標(biāo)準(zhǔn)差然后用這組統(tǒng)計(jì)量同時(shí)去歸一化訓(xùn)練集、驗(yàn)證集和測試集。from sklearn.preprocessing import StandardScaler # 假設(shè) train_seqs 是切好并展平后的訓(xùn)練窗口形狀 (num_samples, window_size, n_features) num_samples, window_size, n_features train_seqs.shape train_flat train_seqs.reshape(-1, n_features) scaler StandardScaler() scaler.fit(train_flat) # 只 fit 訓(xùn)練集的統(tǒng)計(jì)量 def normalize_seq(seqs): flat seqs.reshape(-1, n_features) flat_scaled scaler.transform(flat) return flat_scaled.reshape(seqs.shape) train_seqs_norm normalize_seq(train_seqs) val_seqs_norm normalize_seq(val_seqs) test_seqs_norm normalize_seq(test_seqs)這段代碼的關(guān)鍵點(diǎn)有兩個(gè)。一是fit只放在訓(xùn)練集上防止驗(yàn)證集的信息泄漏到訓(xùn)練過程這一點(diǎn)很多初學(xué)者會(huì)漏二是 reshape 回原形狀時(shí)窗口的先后順序不能亂否則會(huì)出現(xiàn)“跨窗口混特征”的問題。訓(xùn)練/驗(yàn)證集的劃分不能隨機(jī)打亂后切分。同一局游戲切出來的多個(gè)窗口樣本高度相關(guān)如果一部分窗口進(jìn)了訓(xùn)練集、另一部分進(jìn)了驗(yàn)證集驗(yàn)證準(zhǔn)確率會(huì)虛高到 80% 以上。正確做法是按game_id分組切分比如前 80% 的場次作為訓(xùn)練集剩下 20% 的場次作為驗(yàn)證集。3. 用 PyTorch 搭一個(gè)能跑的 LSTM 二分類模型3.1 定義模型input_size、hidden_size、num_layers 怎么給模型定義是整個(gè)項(xiàng)目里最有“工作量感”的部分。用 PyTorch 實(shí)現(xiàn)一個(gè)單隱藏層 LSTM再在后接兩層全連接代碼量不大但每個(gè)參數(shù)都要對著數(shù)據(jù)形狀來給。import torch import torch.nn as nn class MatchLSTM(nn.Module): def __init__(self, input_size, hidden_size64, num_layers1, dropout0.2): super(MatchLSTM, self).__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0 ) self.fc1 nn.Linear(hidden_size, 32) self.fc2 nn.Linear(32, 1) self.dropout nn.Dropout(dropout) self.relu nn.ReLU() def forward(self, x): # x: (batch_size, window_size, n_features) lstm_out, _ self.lstm(x) # 返回每個(gè)時(shí)間步的輸出和隱層狀態(tài) # 取最后一個(gè)時(shí)間步的輸出形狀 (batch_size, hidden_size) last_out lstm_out[:, -1, :] out self.dropout(self.relu(self.fc1(last_out))) out self.fc2(out) return out.squeeze(-1) # 輸出形狀 (batch_size,)模型結(jié)構(gòu)里需要注意三個(gè)點(diǎn)。第一batch_firstTrue讓輸入張量的第一維是 batchPyTorch 默認(rèn)是第二維是 batch經(jīng)常有人在這里翻車喂進(jìn)去直接報(bào)形狀錯(cuò)誤第二lstm_out[:, -1, :]表示取最后一個(gè)時(shí)間步的輸出作為整條序列的語義表示這是序列分類任務(wù)的標(biāo)準(zhǔn)做法比取隱層狀態(tài)h_n更直觀第三模型的最終輸出是一個(gè)標(biāo)量 logit不是 0/1 概率后面接BCEWithLogitsLoss時(shí)不需要手動(dòng)加 sigmoid。hidden_size是 LSTM 記憶容量畢設(shè)項(xiàng)目 64 起步完全夠調(diào)大不一定會(huì)帶來明顯提升num_layers層數(shù)超過 2 之后訓(xùn)練難度會(huì)明顯上升數(shù)據(jù)量不夠的時(shí)候更傾向于過擬合。所以這里默認(rèn)值給的是 1 層、64 個(gè)隱藏單元。3.2 訓(xùn)練循環(huán)與 BCEWithLogitsLoss分類被當(dāng)成回歸就翻車勝率預(yù)測是二分類問題標(biāo)簽是 0 或 1但很多人會(huì)順手用 MSELoss 當(dāng)損失函數(shù)做出來之后預(yù)測值永遠(yuǎn)在 0.5 附近浮動(dòng)。正確做法是用帶 logits 的二元交叉熵?fù)p失import torch.optim as optim def train_one_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss, total_correct, total_samples 0.0, 0, 0 for batch_x, batch_y in dataloader: batch_x batch_x.to(device) batch_y batch_y.to(device).float() optimizer.zero_grad() logits model(batch_x) # (batch_size,) loss criterion(logits, batch_y) # BCEWithLogitsLoss loss.backward() optimizer.step() preds (torch.sigmoid(logits) 0.5).long() total_loss loss.item() * batch_x.size(0) total_correct (preds batch_y.long()).sum().item() total_samples batch_x.size(0) return total_loss / total_samples, total_correct / total_samples訓(xùn)練函數(shù)的幾個(gè)細(xì)節(jié)值得展開。criterion用nn.BCEWithLogitsLoss()它把 sigmoid 和交叉熵合成一步計(jì)算數(shù)值上比“先 sigmoid 再 BCE”更穩(wěn)定。batch_y需要轉(zhuǎn)成 float因?yàn)?BCE loss 要求標(biāo)簽和 logits 同類型。torch.sigmoid(logits) 0.5是把 logit 轉(zhuǎn)成預(yù)測類別0.5 這個(gè)閾值在正負(fù)樣本不均衡時(shí)可能需要調(diào)整。數(shù)據(jù)加載部分用 PyTorch 的TensorDataset和DataLoader包一層from torch.utils.data import TensorDataset, DataLoader train_dataset TensorDataset( torch.tensor(train_seqs_norm, dtypetorch.float32), torch.tensor(train_labels, dtypetorch.float32) ) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue)batch_size64通常是個(gè)穩(wěn)妥的起點(diǎn)顯存占用不高收斂也平穩(wěn)。如果訓(xùn)練集很大可以提高到 128過小比如 16會(huì)讓 loss 曲線劇烈震蕩。3.3 保存最佳權(quán)重畢設(shè)最常用的 Checkpoint 寫法訓(xùn)練過程中驗(yàn)證集準(zhǔn)確率不是單調(diào)上升的后面幾個(gè) epoch 往往會(huì)震蕩甚至回退。所以最佳策略不是拿最后一輪模型而是每輪跑完驗(yàn)證集只要指標(biāo)變好就把權(quán)重存下來。best_val_acc 0.0 best_epoch -1 for epoch in range(epochs): train_loss, train_acc train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) if val_acc best_val_acc: best_val_acc val_acc best_epoch epoch torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), val_acc: val_acc }, best_model.pt) print(fEpoch {epoch1}/{epochs} | train loss {train_loss:.4f} | train acc {train_acc:.4f} | val acc {val_acc:.4f})把optimizer_state_dict也存進(jìn)去是為了以后想從這個(gè)斷點(diǎn)繼續(xù)訓(xùn)練時(shí)不用重新構(gòu)建優(yōu)化器狀態(tài)。畢設(shè)答辯或者寫文檔時(shí)這組 checkpoint 可以直接用來分析“模型在第幾個(gè) epoch 取得最佳結(jié)果”比只存權(quán)重更有說服力。4. 訓(xùn)練與調(diào)參讓驗(yàn)證集準(zhǔn)確率從 55% 到 68%4.1 八個(gè)關(guān)鍵超參數(shù)對照表與啟動(dòng)值LSTM 項(xiàng)目的調(diào)參最先盯住的不是學(xué)習(xí)率而是“窗口大小”和“隱藏單元數(shù)”。下表是我實(shí)際調(diào)這個(gè)方向時(shí)習(xí)慣的啟動(dòng)值和調(diào)整順序參數(shù)啟動(dòng)值調(diào)整順序調(diào)參說明window_size51太小丟上下文太大引入上帝視角按特征采樣間隔定hidden_size642數(shù)據(jù)量小幾千樣本不建議直接上 128num_layers13數(shù)據(jù)量不夠時(shí)2 層以上極易過擬合batch_size644訓(xùn)練不穩(wěn)就降到 32訓(xùn)練太慢就加到 128learning_rate0.0015使用 Adam 時(shí)0.001 是通用起點(diǎn)dropout0.26過擬合時(shí)建議直接調(diào)到 0.4 觀察epochs307每 5 輪看一次驗(yàn)證曲線不一定要跑到 30weight_decay1e-48驗(yàn)證集準(zhǔn)確率上不去且訓(xùn)練集已經(jīng) 90% 時(shí)再加調(diào)參順序的核心邏輯是先改“數(shù)據(jù)形狀相關(guān)”的參數(shù)窗口大小再改“模型容量”的參數(shù)隱藏單元和層數(shù)最后才動(dòng)優(yōu)化器相關(guān)參數(shù)。一上來就折騰學(xué)習(xí)率常常會(huì)掩蓋結(jié)構(gòu)問題。4.2 訓(xùn)練曲線怎么看過擬合和欠擬合在 loss 圖上的長相LSTM 訓(xùn)練很容易出現(xiàn)過擬合因?yàn)閿?shù)據(jù)本質(zhì)上是“同局不同窗口”樣本冗余度很高。判斷過擬合不能只看訓(xùn)練集準(zhǔn)確率要看訓(xùn)練損失和驗(yàn)證損失的差距是不是持續(xù)擴(kuò)大。典型場景是前 5 個(gè) epoch 兩邊都在下降第 8 個(gè) epoch 之后訓(xùn)練損失繼續(xù)降、驗(yàn)證損失開始回升。這時(shí)候當(dāng)前權(quán)重已經(jīng)不是最佳狀態(tài)但仍能觀察到模型在學(xué)習(xí)——學(xué)習(xí)的是訓(xùn)練局里的噪聲。解決辦法按優(yōu)先級(jí)排第一把 dropout 從 0.2 調(diào)到 0.4第二加weight_decay1e-4第三把num_layers退回到 1 層第四檢查驗(yàn)證集切分是不是混入了同一場對局的多個(gè)窗口。還有一種翻車情況是欠擬合驗(yàn)證集準(zhǔn)確率長期低于 55%訓(xùn)練集也只有 60% 上下。這往往不是模型問題而是特征本身不夠強(qiáng)。建議先回到數(shù)據(jù)層確認(rèn)特征是不是真的隨時(shí)間變化、窗口切片是不是有空值或者前 10 個(gè)時(shí)間切片的特征是否能區(qū)分勝負(fù)。特征工程補(bǔ)強(qiáng)比調(diào)模型參數(shù)有效得多。4.3 用 AUC 代替準(zhǔn)確率當(dāng)最終指標(biāo)才不會(huì)被閾值騙到在正負(fù)樣本剛好各占一半的數(shù)據(jù)集里準(zhǔn)確率沒什么大問題。但英雄聯(lián)盟對局?jǐn)?shù)據(jù)經(jīng)過滑動(dòng)窗口切分后優(yōu)勢方的樣本天然更多最終標(biāo)簽分布可能偏向某一類。這時(shí)候 65% 的準(zhǔn)確率到底是真的預(yù)測能力強(qiáng)還是模型在無腦輸出優(yōu)勢類很難看出來。from sklearn.metrics import roc_auc_score def evaluate_auc(model, dataloader, device): model.eval() all_labels [] all_probs [] with torch.no_grad(): for batch_x, batch_y in dataloader: batch_x batch_x.to(device) logits model(batch_x) probs torch.sigmoid(logits).cpu().numpy() all_probs.extend(probs) all_labels.extend(batch_y.numpy()) return roc_auc_score(all_labels, all_probs)AUC 的含義是“隨機(jī)抽一個(gè)正樣本和一個(gè)負(fù)樣本模型給正樣本打更高分的概率”它不依賴閾值選擇能更真實(shí)地反映模型排序能力。畢設(shè)里如果準(zhǔn)確率 68%、AUC 0.74這個(gè)組合比準(zhǔn)確率 72%、AUC 0.60 更有說服力。調(diào)參時(shí)優(yōu)先盯 AUC最后再按 0.5 的閾值去報(bào)告準(zhǔn)確率、精確率、召回率和 F1這樣整套指標(biāo)體系是完整的。5. 避坑記錄把 LSTM 上手時(shí)踩過的坑按現(xiàn)象、原因、解決列清楚5.1 驗(yàn)證集準(zhǔn)確率 95%一上真實(shí)場景就廢數(shù)據(jù)泄漏是最常見的翻車現(xiàn)象訓(xùn)練完的模型在驗(yàn)證集上準(zhǔn)確率高達(dá) 90% 以上興奮地拿去預(yù)測新對局的前 10 分鐘數(shù)據(jù)結(jié)果預(yù)測結(jié)果幾乎等于隨機(jī)。原因這是典型的“上帝視角”數(shù)據(jù)泄漏。常見泄漏源有兩個(gè)。第一特征里混入了整場比賽結(jié)束后才能統(tǒng)計(jì)到的數(shù)據(jù)比如“整場總經(jīng)濟(jì)”“最終擊殺數(shù)”這些信息在一個(gè)時(shí)間切片的特征里其實(shí)包含了全局信息第二驗(yàn)證集的切分沒有按game_id分組同一局游戲切出來的窗口一部分在訓(xùn)練集一部分在驗(yàn)證集模型等于直接見過了答案。解決清洗特征時(shí)凡是涉及“整場”“最終”的字段全部去掉只保留“截至當(dāng)前時(shí)刻的累計(jì)值”。驗(yàn)證集切分必須按game_id分層保證同一局的所有窗口只出現(xiàn)在同一側(cè)。做完這兩步驗(yàn)證集準(zhǔn)確率會(huì)跌到 60% 上下這才是真實(shí)水平。5.2 訓(xùn)練到一半 loss 變成 NaN特征沒歸一化或梯度爆炸現(xiàn)象訓(xùn)練前幾個(gè) batch 正常隨后loss突然變成nan準(zhǔn)確率也跟著顯示 0。原因LSTM 對輸入尺度極其敏感。如果某個(gè)特征比如團(tuán)隊(duì)經(jīng)濟(jì)數(shù)值范圍是 0 到幾萬而另一個(gè)特征是否拿大龍只有 0 和 1LSTM 的輸入門和遺忘門會(huì)瞬間進(jìn)入飽和區(qū)反向傳播時(shí)梯度要么消失要么爆炸。另一個(gè)可能原因是學(xué)習(xí)率過大Adam 在初始階段的二階動(dòng)量估計(jì)不夠穩(wěn)定也會(huì)放大梯度。解決先檢查輸入張量的數(shù)值范圍確認(rèn)每個(gè)特征都經(jīng)過 StandardScaler 歸一化而不是只做了 min-max 縮放。然后把學(xué)習(xí)率從 0.001 降到 0.0003并在clip_grad_norm_(model.parameters(), max_norm1.0)加一道梯度裁剪。兩件事都做基本上能解決 NaN 問題。5.3 正負(fù)樣本不均衡模型變成“無腦預(yù)測優(yōu)勢方”現(xiàn)象訓(xùn)練結(jié)束時(shí)準(zhǔn)確率 65%看起來不錯(cuò)但打印分類報(bào)告發(fā)現(xiàn)負(fù)類劣勢方獲勝的召回率只有 0.1模型幾乎把所有樣本都判成正類。原因滑動(dòng)窗口切分制造了大量“優(yōu)勢方視角”的樣本。前二十分鐘經(jīng)濟(jì)領(lǐng)先的隊(duì)伍獲勝概率超過 70%這樣切出來的窗口正樣本比例可能達(dá)到 75% 以上。模型學(xué)到的最優(yōu)策略就是全部預(yù)測正類。解決一種辦法是在DataLoader里做加權(quán)采樣讓正負(fù)樣本在每個(gè) batch 里大致平衡另一種更推薦的做法是調(diào)整決策閾值——在訓(xùn)練結(jié)束后遍歷 0.3 到 0.7 之間的若干閾值選能在驗(yàn)證集上讓 F1 最高的閾值作為最終分類邊界。AUC 不需要重算因?yàn)?AUC 本身與閾值無關(guān)。5.4 同一場對局的窗口擠進(jìn)同一個(gè) batch驗(yàn)證指標(biāo)一路虛高現(xiàn)象驗(yàn)證集準(zhǔn)確率比測試集高出一大截而且每次運(yùn)行結(jié)果波動(dòng)極大。原因窗口重疊的樣本之間不是獨(dú)立的。相鄰窗口共享了大量時(shí)間切片數(shù)據(jù)把它們同時(shí)放進(jìn)訓(xùn)練集和驗(yàn)證集相當(dāng)于考試前看了答案即使在驗(yàn)證集內(nèi)部同局的幾十個(gè)窗口也讓評(píng)估結(jié)果嚴(yán)重偏向某些特定對局。解決切分?jǐn)?shù)據(jù)時(shí)先用game_id.unique()把對局編號(hào)列表打亂再按比例切分切窗口的動(dòng)作應(yīng)在劃分之后執(zhí)行不能先切好窗口再隨機(jī)劃分。這樣能確保驗(yàn)證集里出現(xiàn)的全是“沒見過的對局”結(jié)果才接近真實(shí)預(yù)測場景。6. 把畢設(shè)做漂亮用對照實(shí)驗(yàn)證明“序列信息”真的有用很多畢設(shè)評(píng)審老師會(huì)問一個(gè)問題你用了 LSTM那么 LSTM 到底比普通模型強(qiáng)在哪要回答好這個(gè)問題得做一個(gè)“均值特征 MLP 對照實(shí)驗(yàn)”而不是只展示 LSTM 的準(zhǔn)確率數(shù)字。具體做法是把每個(gè)窗口的 5 個(gè)時(shí)間切片取平均得到一條(batch_size, n_features)的普通特征向量然后用兩層的全連接網(wǎng)絡(luò)訓(xùn)練一個(gè)二分類模型最后把 MLP 的驗(yàn)證集 AUC 與 LSTM 的驗(yàn)證集 AUC 放到同一張表格里。# 對照模型用窗口均值特征替代序列輸入 mean_features train_seqs_norm.mean(axis1) # (num_samples, n_features) val_mean_features val_seqs_norm.mean(axis1) # 訓(xùn)練一個(gè)簡單 MLP2 層全連接 ReLU class BaselineMLP(nn.Module): def __init__(self, input_size): super().__init__() self.net nn.Sequential( nn.Linear(input_size, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): return self.net(x).squeeze(-1)對比結(jié)果如果顯示 LSTM 的 AUC 比均值 MLP 高 0.05 到 0.1就說明“隨時(shí)間變化的態(tài)勢走向”確實(shí)比“一局的平均狀態(tài)”更能預(yù)示勝負(fù)LSTM 的序列建模能力就有了直接證據(jù)。如果兩者差不多說明你的預(yù)測信息主要藏在特征本身而不是時(shí)序變化里這時(shí)候可以換window_size或者挑一些連續(xù)變化特征重新做對比。最后一章落到這里是想說一個(gè)習(xí)慣我接手任何 LSTM 項(xiàng)目都會(huì)至少做一個(gè)非序列基線否則調(diào)出來的參數(shù)很難判斷是“模型的功勞”還是“數(shù)據(jù)的功勞”。做畢業(yè)設(shè)計(jì)更該如此——對照組不復(fù)雜卻能讓整篇文檔的論證站穩(wěn)。希望這些步驟和踩坑記錄能幫你在自己的數(shù)據(jù)上順利跑通。本文還有配套的精品資源點(diǎn)擊獲取