☰
用LSTM预测英雄联盟胜率:时序二分类完整源码与避坑指南
2026/10/3 10:37:20 网站建设 项目流程

简介:这套基于深度学习LSTM的英雄联盟胜率预测项目源码与文档说明,主要面向计算机相关专业正在准备毕业设计的学生,以及需要完整实战练习的初中级学习者,可直接用于课程设计、期末大作业。项目围绕LPL赛事数据,涵盖爬虫采集、JSON/CSV数据整理、BILSTM_Att模型构建与训练、预测管线及Spark客户端生产消费等模块,完整展示从数据获取到模型部署的流程,有助于理解深度学习在电竞数据预测中的实际运用。压缩包约8.6MB,共56个文件,以Python源码为主,辅以模型权重(pt)、配置文件(xml/json)、前端页面(js/css/html)和说明文档,便于按目录快速定位。已有94人学习下载。资源附带训练好的模型、运行日志与项目说明,代码经过严格调试,既可作为毕业设计直接使用,也适合学习者深入掌握LSTM模型的设计思路、训练调优方法及胜率预测项目的完整实现细节。

1. 把英雄联盟胜率预测交给 LSTM:一份能直接跑的毕业设计源码

用深度学习预测英雄联盟胜负,最容易被质疑的一句话是“这玩意儿是不是玄学”。实际上它是个标准的时间序列二分类问题:比赛从 BP 到推塔是一个逐分钟推进的过程,前 20 分钟的经济差、击杀差、视野得分,天然是带时间步的序列数据。表格型的平均 KDA 会丢掉“翻盘”这个信息,而 LSTM 的细胞状态恰好能把第 8 分钟的劣势和第 28 分钟的翻盘串起来。这套基于深度学习 LSTM 的英雄联盟胜率预测项目源码,正是按这个思路完整的——数据清洗、滑窗建序列、Keras 搭模型、训练评估一步不缺,还附了文档说明。适合作 Python 毕业设计,也适合想用 LSTM 做时序二分类但不想从零摸爬滚打的人。你拿到手不是看 demo,是能直接换数据跑 baseline 的那种。

2. 为什么偏偏是 LSTM:从比赛过程到三维张量的建模逻辑

2.1 胜负预测本质是二分类,但数据天生是序列

我拆过不少游戏比赛的预测项目,最常见的翻车是把每一局比赛压成一行特征,比如平均击杀、平均经济、总推塔数,然后丢给 XGBoost。这样建模型有个致命伤:时间维度被抹平了。一局比赛打到 40 分钟,前 10 分钟被压着打、后 30 分钟翻盘,和全程碾压,平均经济差可能是同一个数字,但对胜负的指示意义完全不同。

LSTM 的输入要求是三维张量(样本数, 时间步长, 特征数),这正好把比赛过程还原成“每过一分钟记一笔账”。网络能学到的不是“这局均势”,而是“第 8 分钟落后 3000 经济,第 15 分钟追平,第 22 分钟反超”这种动态过程。

这个资源里的做法是:把一场比赛按分钟切时间片,每个时间片内取当前累计的经济差、击杀差、推塔差、视野得分等特征,构成一个(time_steps, features)的矩阵。一局比赛就是一个样本,标签是 0 或 1。

def build_sequence(match_df, time_steps=20, feature_cols=['gold_diff', 'kill_diff', 'tower_diff']): # 按分钟排序,取最后 time_steps 个时间片的特征 seq = match_df.sort_values('minute')[feature_cols].values[-time_steps:] # 如果比赛时长不足 time_steps,前面补 0 if len(seq) < time_steps: pad = np.zeros((time_steps - len(seq), len(feature_cols))) seq = np.vstack([pad, seq]) return seq

这个函数的逻辑是:把一场比赛视为一条分钟级时间线,只保留最后 20 个时间片。补零操作是为了让所有样本形状对齐,time_steps=20意味着模型只看比赛最后 20 分钟的趋势,这是建模时的一个先验假设——如果你觉得前期的 BP 压制更重要,可以把窗口拉长到 30 或 40,代价是样本数不变、但每个样本的信息量变大。

2.2 从 RNN 到 LSTM:三个门解决了梯度消失,也解决了“记不住前面”

普通 RNN 处理这种分钟级序列会遇到梯度消失:第 1 分钟的 BP 信息传到第 30 分钟时,梯度已经小到几乎不更新权重,模型等于失忆。LSTM 的思路是在隐藏状态之外加一条“细胞状态”通道,类似传送带,信息可以原样从上一个时间步传到下一个时间步,中间只被三个门修改。

  • 遗忘门:决定上一个细胞状态哪些信息要丢弃,由当前输入和上一隐藏状态经过 sigmoid 得到 0~1 的门值。
  • 输入门:决定当前时间步的新信息有多少写入细胞状态。
  • 输出门:决定当前细胞状态有多少暴露给隐藏状态。

实操里你不需要手动实现这些公式,Keras 一行LSTM(units)就封装好了,但理解门机制对调参有意义:units可以理解为细胞状态的维度,越大记忆容量越大,也越容易过拟合。这个资源里默认用 64 个单元,属于中等容量,先跑通再调。

2.3 数据切分是第一个最容易翻车的地方

时序数据切分不能直接train_test_split(random_state=42)。英雄联盟的比赛按时间连续发生,同一天的比赛受版本、英雄强度、选手状态影响,相邻比赛样本高度相关。随机切分等于让模型偷看“未来”,测试集准确率会虚高得离谱。

正确的做法是按时间切分,或者按比赛 ID 分组后做 GroupKFold。世界杯决赛在 11 月,你把 10 月的比赛分进训练集、11 月分进测试集,这是合理的。但你不能把同一局比赛的两个视角(蓝方视角和红方视角)一个放进训练集一个放进测试集,那叫样本泄露。

from sklearn.model_selection import GroupKFold groups = all_samples['game_id'].values # 同一局比赛视为一组 gkf = GroupKFold(n_splits=5) for train_idx, val_idx in gkf.split(all_samples, all_samples['label'], groups): train_set = all_samples.iloc[train_idx] val_set = all_samples.iloc[val_idx] # 确保同一 game_id 不会同时出现在 train 和 val 中 assert set(train_set['game_id']).isdisjoint(set(val_set['game_id']))

GroupKFold的关键在groups参数:它要求同一组的样本必须全部进训练集或全部进验证集。assert那行是用来验证切分结果没有混组的,这行检查代码建议保留,后面换数据时能少踩很多坑。验证集在这里只负责调参,最终评估最好再留一段最新时间的数据做测试集,模拟“拿过去预测未来”的真实场景。

3. 特征工程与数据管道:从原始比赛记录到喂给 LSTM 的三维张量

3.1 原始数据里到底留哪些字段

英雄联盟的公开比赛数据字段非常多,常见的平台导出的数据结构大致有这些:

字段说明是否保留
game_id比赛唯一 ID,用于分组切分保留,做分组用
minute游戏内时间(分钟)保留,作为时间步索引
gold_diff累计经济差保留,核心特征
kill_diff击杀差保留,核心特征
tower_diff推塔差保留,核心特征
dragon_count小龙数保留,版本强势时权重高
baron_count大龙数保留,Baron 后胜率骤升
vision_score_diff视野得分差保留,低分段尤其有用
blue_side是否蓝方保留,蓝方有先选优势
team_kda队伍总 KDA建议剔除,KDA 与经济差高度共线
player_names/champion_names选手名/英雄名另做编码,不直接进数值特征

一个很重要的原则是:只保留比赛进程中能实时获取的信息。有些字段(比如最终胜负、最终 MVP、比赛时长)不能用,它们是结算后才有的,属于典型的未来数据。

3.2 数值特征的归一化:和你想的不太一样

LSTM 使用 tanh 和 sigmoid 作为激活函数,输入绝对值太大容易让门控饱和。比如经济差动辄上万,击杀差通常只有个位数,这两个量级放一起,gold_diff会直接淹没kill_diff。

常见做法是做 min-max 归一化到[-1, 1]或[0, 1]。但要小心:必须用训练集的 min 和 max 去变换验证集和测试集,不能拿全集算好再切分,那又是一次泄露。

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(-1, 1)) train_scaled = scaler.fit_transform(train_set[feature_cols]) val_scaled = scaler.transform(val_set[feature_cols]) # 用训练集的参数 test_scaled = scaler.transform(test_set[feature_cols])

这里fit_transform只出现在训练集上,验证集和测试集只用transform,保证分布参数来源唯一。如果你的数据是跨版本收集的,比如同时包含 13.10 和 14.5 版本,建议分版本统计 min/max,因为版本更迭会改变整体经济节奏,合在一起算会让新版本数据被压到很窄的区间。

3.3 滑窗构建时间步:窗口长度和步长的选择

如果原始数据是“每分钟一行”,直接按比赛 ID 分组后取连续分钟数即可。但如果原始数据是“每 30 秒一个快照”,你需要决定时间步粒度。我一般会用resample把细粒度数据聚合成分钟级,比如minute取整,组内取均值。

窗口长度是个需要试的超参数。窗口太短(比如 5 分钟)模型看不到翻盘过程;窗口太长(比如 40 分钟)很多比赛总共也就 30 分钟,补零比例过高,样本会被大量空行“稀释”。这个资源默认 20 分钟是比较平衡的选择:既能覆盖大部分比赛的转折阶段,又不会让短局样本补太多零。

def make_3d_dataset(df, time_steps=20, features=None, group_col='game_id'): X, y, groups = [], [], [] for gid, grp in df.groupby(group_col): seq = build_sequence(grp, time_steps, features) X.append(seq) y.append(int(grp['label'].iloc[0])) groups.append(gid) return np.array(X), np.array(y), np.array(groups)

这段代码把分组构建序列和标签整合到一个函数里,返回的groups数组供GroupKFold使用。label在原始数据里是每行重复的整场胜负,取iloc[0]是因为同一场比赛所有时间片的 label 必然相同。

3.4 类别标签与样本方向的坑

常见的数据集通常给的是“队伍 A vs 队伍 B,A 赢了”这种结构。建模前你要决定视角统一:要么全部从蓝方视角看,要么全部从胜者视角看。

从蓝方视角的话,特征要带方向符号——蓝方领先为正、落后为负,标签是 1 表示蓝方赢。从胜者视角的话,所有样本都把“本方”作为正方向,标签永远是 1,但模型会退化成一个“本方优势多大”的回归问题,预测概率不代表真实胜率。

正确做法是保持蓝红双方作为正负样本:蓝方视角建特征,红方视角建负样本。

def build_dual_samples(blue_row, red_row): # blue_row 和 red_row 是同一场比赛的两个视角数据 X_blue = build_sequence(blue_row, time_steps=20) X_red = build_sequence(red_row, time_steps=20) y_blue = 1 # 蓝方胜 y_red = 0 # 红方负 return [(X_blue, y_blue), (X_red, y_red)]

这样每个比赛产出两个样本,彼此互为镜像:蓝方数列和红方数列数值完全相反,标签相反。模型学到的其实是“某一方相对优势程度”,而不是“某一方绝对实力”。

4. 基于 Keras 的 LSTM 模型搭建与训练:输入形状、损失函数与早停

4.1 双层 LSTM 的标准结构:什么时候 return_sequences

这个资源里的模型用的是两层 LSTM 加一个 Dense 输出头。第一层设return_sequences=True,让它输出每个时间步的隐藏状态给第二层;第二层return_sequences=False,只输出最后一个时间步的状态,再接 sigmoid 分类。

import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model = Sequential([ LSTM(64, return_sequences=True, input_shape=(time_steps, n_features)), Dropout(0.3), LSTM(32, return_sequences=False), Dropout(0.3), Dense(1, activation='sigmoid') ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])

第一层input_shape=(time_steps, n_features)里time_steps就是滑窗长度 20,n_features是特征矩阵的列数。第一层设return_sequences=True的原因是要把完整的序列信息传递给第二层 LSTM;如果你只堆一层 LSTM,这个参数就不需要。Dropout(0.3)在 LSTM 层之间加的是变体 dropout,它会同时作用在输入和循环连接上,比普通 Dropout 更适合 RNN 结构。

4.2 样本不均衡:胜率不是五五开

英雄联盟蓝红双方的胜率并不均等,蓝方通常有 52% 左右的胜率,如果你的数据里蓝方样本偏多,模型会学会“无脑猜蓝方赢”。处理方式有两种,这个资源里用的是加权交叉熵:

import numpy as np neg_weight = len(y_train) / (2 * np.sum(y_train == 0)) pos_weight = len(y_train) / (2 * np.sum(y_train == 1)) class_weight = {0: neg_weight, 1: pos_weight} model.fit(X_train, y_train, class_weight=class_weight, ...)

class_weight把少数类的 loss 放大,把多数类的 loss 缩小。计算逻辑是让正负类的权重和为 2,保持 loss 量级不变。如果你发现训练出来的模型预测概率普遍低于 0.5,多半是负类权重设置得太大,可以调回{0: 1.0, 1: 1.0}对比一下。

也可以不改 loss,改成用AUC作为评估指标。binary_crossentropy对极端概率敏感,而 AUC 只关心正样本预测值是否整体高于负样本,对类别比例更鲁棒。

model.compile(optimizer='adam', loss='binary_crossentropy', metrics=[tf.keras.metrics.AUC(name='val_auc')])

验证集 AUC 是最值得盯的指标。准确率在负类占 60% 时会虚高,AUC 不会。

4.3 训练参数:batch_size、epochs 与早停策略

LSTM 训练有几个关键参数,踩过坑的人都知道这批参数比网络结构更影响结果:

参数默认值说明与调整建议
batch_size32太小则梯度震荡,太大则收敛慢;体育比赛数据量通常不大,32 或 64 即可
epochs200配合 EarlyStopping,不要硬 train 满
learning_rate1e-3 (Adam)训练到后期 loss 震荡时降到 1e-4
patience10验证集 loss 连续 10 个 epoch 不降就停
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau callbacks = [ EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True), ModelCheckpoint('best_model.h5', monitor='val_auc', mode='max', save_best_only=True), ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, min_lr=1e-5) ] model.fit( X_train, y_train, validation_data=(X_val, y_val), batch_size=32, epochs=200, class_weight=class_weight, callbacks=callbacks, verbose=1 )

EarlyStopping的restore_best_weights=True会在停止后把权重回滚到验证集最佳 epoch 的状态,这个参数必须开,不开的话拿到的是停止时那个已经过拟合的模型。ModelCheckpoint监控val_auc,因为它比val_loss对类别不均衡更不敏感。ReduceLROnPlateau在 loss 平台期把学习率减半,比手动调学习率省心。

4.4 训练曲线怎么读:三种典型形态

训练结束后画 loss 曲线,常见情况就三种:

  • train_loss持续下降,val_loss先降后升:过拟合。Dropout 调大,或 LSTM 单元数调小。
  • train_loss和val_loss都下降但val_loss波动剧烈:batch_size 太小,或学习率太高。
  • train_loss和val_loss都下不去:特征没选对,或者序列长度过长导致有效信息被稀释。先检查数据管道,再看模型。
import matplotlib.pyplot as plt history = model.fit(...) # 上面的代码 plt.plot(history.history['loss'], label='train_loss') plt.plot(history.history['val_loss'], label='val_loss') plt.legend() plt.savefig('loss_curve.png')

注意看保存出来的loss_curve.png:如果训练集 loss 低于 0.1 而验证集 loss 在 0.6 以上,说明模型把训练集背下来了,此时候加大 Dropout 比加深网络更有效。

5. 常见问题与避坑:四个让预测准确率掉十个点的隐藏陷阱

5.1 现象:准确率高达 85%,换成真实比赛数据直接崩

用时间范围外的比赛做预测,准确率只剩 55%。原因:训练验证切分用了随机shuffle,相邻时间段的比赛高度相似,模型看到的是“熟悉的比赛”,而不是“学会规律”。

解决:改成按时间切分,或GroupKFold。比赛 ID 是分组依据,时间顺序是第二道保险。我一般会再加一条:把game_id放进索引,训练前按game_start_time排序后再切分,保证验证集时间全部晚于训练集。

5.2 现象:验证集 AUC 很高,但预测出来的概率全是 0.5 附近

原因:特征里混入了结算字段。最常见的是把match_duration(比赛总时长)或winner的滞后变量带进了序列构建。LSTM 发现只要看到时长接近 40 分钟就猜某一边赢,结果真实预测时没有这个字段,模型退化。

解决:逐一检查特征列,确认所有特征都是比赛进行中可以实时获取的。minute本身也尽量不要作为特征直接输入,它和比赛进程强相关,让模型学会“时间越晚越接近终局”,而不是“领先优势有多大”。

5.3 现象:英雄名称做 OneHot 后特征维度过大,模型训不动

英雄数量常年 160+,如果直接 OneHot 会得到 160 维稀疏输入,LSTM 对这种高维稀疏输入非常不敏感。

解决:不把英雄代码进时间序列,而是作为静态特征在序列外部处理,或者用 Embedding 层自动学英雄向量。简单做法是只保留出场率 TOP 30 的英雄做 OneHot,其余归为other类;更高级的做法是拉取英雄属性(射程、定位、强势期)做成连续特征。

top_champs = df['champion'].value_counts().index[:30].tolist() df['champ_enc'] = df['champion'].apply(lambda x: x if x in top_champs else 'other') onehot = pd.get_dummies(df['champ_enc'])

get_dummies会把 30 个高频英雄加other共 31 列,每场比赛按双方各 5 个英雄加总成 10 维统计特征。注意这 31 列要加在序列特征之后,而不是挤占时间步内的核心特征。

5.4 现象:每次训练结果都不一样,同一个模型跑两次 AUC 差 0.05

原因:没有固定随机种子。LSTM 的权重初始化、Dropout、数据 shuffle 都带随机性。

解决:显式固定种子。

import random import numpy as np import tensorflow as tf random.seed(42) np.random.seed(42) tf.random.set_seed(42)

这行代码放在数据加载之前。tf.random.set_seed管的是算子和初始化,np.random管的是数据管道里的 shuffle。两者都固定后,同一份数据跑两次结果应当完全一致。注意GroupKFold的划分也受随机种子影响,要在数据加载前统一设置。

5.5 现象:控制台一堆 WARNING,训练速度极慢

原因:数据管道用了 Python 的for循环逐场构建序列,build_sequence又被反复调用,生成 10 万条样本耗时几十分钟。

解决:向量化或用 TensorFlow 的tf.data.Dataset做预取。小数据量(一万场以内)用numpy批量构建足够,但要注意把build_sequence的循环移到numpy切片层面:

def build_all_sequences(df, time_steps=20): games = df.groupby('game_id') total = len(games) n_features = df[['gold_diff', 'kill_diff', 'tower_diff']].shape[1] X = np.zeros((total, time_steps, n_features)) y = np.zeros(total) for i, (gid, grp) in enumerate(games): grp = grp.sort_values('minute').tail(time_steps) X[i, -len(grp):, :] = grp[['gold_diff', 'kill_diff', 'tower_diff']].values y[i] = grp['label'].iloc[0] return X, y

先np.zeros分配好整个三维数组,再往里填数,比反复np.vstack快一个量级。tail(time_steps)直接截取最后 N 分钟,天然完成窗口切片,不需要逐行补零判断。如果数据超过几十万行,再考虑tf.data。

6. 从准确率到 AUC 和滚动回测:验证模型真实水平的三个习惯

6.1 准确率会骗人,AUC 不会

英雄联盟的红蓝胜率天然偏差大约 2~3 个百分点,在测试集不够大的时候,准确率小数点后一位的变化可能完全是噪声。我习惯以 AUC 为主要评估指标,原因很朴素:AUC 不依赖阈值,它衡量的是“随机抽一个正样本和随机抽一个负样本,模型给正样本打更高分的概率”。这个指标对类别比例不敏感,更稳定。

from sklearn.metrics import roc_auc_score, roc_curve val_pred = model.predict(X_val, verbose=0) auc = roc_auc_score(y_val, val_pred) print(f"Validation AUC: {auc:.4f}")

6.2 阈值不要默认 0.5,用验证集找最优切分点

sigmoid 输出的 0.5 只是一个默认参考值。如果你的使用场景是“预测蓝方胜率超过 60% 才下注”或“胜率低于 40% 才认为要输”,阈值应该在验证集上搜索:

fpr, tpr, thresholds = roc_curve(y_val, val_pred) optimal_idx = np.argmax(tpr - fpr) optimal_threshold = thresholds[optimal_idx] print(f"Optimal threshold: {optimal_threshold:.3f}")

tpr - fpr最大化等价于在验证集上找平衡敏感性和特异性的点。实际使用时,阈值偏好取决于业务需求:宁可错过也不能猜错的场景,阈值要调高;反之调低。

6.3 滚动回测:模拟真实预测的“时间之旅”

最后一个习惯是用滚动窗口做回测,而不是一次性在固定测试集上评估。做法是:用第 1~100 天的数据训练,预测第 101~105 天;再用第 1~105 天训练,预测第 106~110 天…… 这样每一步都模拟“用已知预测未知”,和真实应用场景完全一致。

资源里的文档说明如果没写这个,建议自己补一个脚本:

from datetime import timedelta def rolling_backtest(df, start_date, window_days=60, step_days=5): cur = start_date aucs = [] while cur < df['date'].max(): train_df = df[df['date'] < cur] test_df = df[(df['date'] >= cur) & (df['date'] < cur + timedelta(days=step_days))] X_train, y_train = build_all_sequences(train_df) X_test, y_test = build_all_sequences(test_df) model = build_model(X_train.shape[1], X_train.shape[2]) model.fit(X_train, y_train, epochs=30, batch_size=32, verbose=0) pred = model.predict(X_test, verbose=0) aucs.append(roc_auc_score(y_test, pred)) cur += timedelta(days=step_days) return np.mean(aucs)

这个脚本每次只新训练一次模型,数据量不大时是可行的。数据量大时改成model.fit基于上一轮权重继续训练,也就是增量学习,能省掉大部分重复训练时间。

从那以后,我每次跑这种序列预测项目,都强制自己走一遍“分组切分 → 特征未来数据检查 → AUC 评估 → 滚动回测”这四步。前两步防数据泄露,后两步防假指标。这个资源本身已经替你省掉了大部分重复劳动,但验证方法这部分,最好还是自己动手再压一遍,毕竟毕业设计答辩时老师最爱问的问题就是“你怎么证明你的模型是真的会预测,而不是背答案”。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询