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