ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用LSTM预测英雄联盟胜率:时序二分类完整源码与避坑指南

用LSTM预测英雄联盟胜率:时序二分类完整源码与避坑指南 简介这套基于深度学习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_steps20, 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_steps20意味着模型只看比赛最后 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_state42)。英雄联盟的比赛按时间连续发生同一天的比赛受版本、英雄强度、选手状态影响相邻比赛样本高度相关。随机切分等于让模型偷看“未来”测试集准确率会虚高得离谱。正确的做法是按时间切分或者按比赛 ID 分组后做 GroupKFold。世界杯决赛在 11 月你把 10 月的比赛分进训练集、11 月分进测试集这是合理的。但你不能把同一局比赛的两个视角蓝方视角和红方视角一个放进训练集一个放进测试集那叫样本泄露。from sklearn.model_selection import GroupKFold groups all_samples[game_id].values # 同一局比赛视为一组 gkf GroupKFold(n_splits5) 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_steps20, featuresNone, group_colgame_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 队伍 BA 赢了”这种结构。建模前你要决定视角统一要么全部从蓝方视角看要么全部从胜者视角看。从蓝方视角的话特征要带方向符号——蓝方领先为正、落后为负标签是 1 表示蓝方赢。从胜者视角的话所有样本都把“本方”作为正方向标签永远是 1但模型会退化成一个“本方优势多大”的回归问题预测概率不代表真实胜率。正确做法是保持蓝红双方作为正负样本蓝方视角建特征红方视角建负样本。def build_dual_samples(blue_row, red_row): # blue_row 和 red_row 是同一场比赛的两个视角数据 X_blue build_sequence(blue_row, time_steps20) X_red build_sequence(red_row, time_steps20) 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_sequencesTrue让它输出每个时间步的隐藏状态给第二层第二层return_sequencesFalse只输出最后一个时间步的状态再接 sigmoid 分类。import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential([ LSTM(64, return_sequencesTrue, input_shape(time_steps, n_features)), Dropout(0.3), LSTM(32, return_sequencesFalse), Dropout(0.3), Dense(1, activationsigmoid) ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy])第一层input_shape(time_steps, n_features)里time_steps就是滑窗长度 20n_features是特征矩阵的列数。第一层设return_sequencesTrue的原因是要把完整的序列信息传递给第二层 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_weightclass_weight, ...)class_weight把少数类的 loss 放大把多数类的 loss 缩小。计算逻辑是让正负类的权重和为 2保持 loss 量级不变。如果你发现训练出来的模型预测概率普遍低于 0.5多半是负类权重设置得太大可以调回{0: 1.0, 1: 1.0}对比一下。也可以不改 loss改成用AUC作为评估指标。binary_crossentropy对极端概率敏感而 AUC 只关心正样本预测值是否整体高于负样本对类别比例更鲁棒。model.compile(optimizeradam, lossbinary_crossentropy, metrics[tf.keras.metrics.AUC(nameval_auc)])验证集 AUC 是最值得盯的指标。准确率在负类占 60% 时会虚高AUC 不会。4.3 训练参数batch_size、epochs 与早停策略LSTM 训练有几个关键参数踩过坑的人都知道这批参数比网络结构更影响结果参数默认值说明与调整建议batch_size32太小则梯度震荡太大则收敛慢体育比赛数据量通常不大32 或 64 即可epochs200配合 EarlyStopping不要硬 train 满learning_rate1e-3 (Adam)训练到后期 loss 震荡时降到 1e-4patience10验证集 loss 连续 10 个 epoch 不降就停from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau callbacks [ EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue), ModelCheckpoint(best_model.h5, monitorval_auc, modemax, save_best_onlyTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-5) ] model.fit( X_train, y_train, validation_data(X_val, y_val), batch_size32, epochs200, class_weightclass_weight, callbackscallbacks, verbose1 )EarlyStopping的restore_best_weightsTrue会在停止后把权重回滚到验证集最佳 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], labeltrain_loss) plt.plot(history.history[val_loss], labelval_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_steps20): 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, verbose0) auc roc_auc_score(y_val, val_pred) print(fValidation 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(fOptimal 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_days60, step_days5): 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(daysstep_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, epochs30, batch_size32, verbose0) pred model.predict(X_test, verbose0) aucs.append(roc_auc_score(y_test, pred)) cur timedelta(daysstep_days) return np.mean(aucs)这个脚本每次只新训练一次模型数据量不大时是可行的。数据量大时改成model.fit基于上一轮权重继续训练也就是增量学习能省掉大部分重复训练时间。从那以后我每次跑这种序列预测项目都强制自己走一遍“分组切分 → 特征未来数据检查 → AUC 评估 → 滚动回测”这四步。前两步防数据泄露后两步防假指标。这个资源本身已经替你省掉了大部分重复劳动但验证方法这部分最好还是自己动手再压一遍毕竟毕业设计答辩时老师最爱问的问题就是“你怎么证明你的模型是真的会预测而不是背答案”。希望帮到你。本文还有配套的精品资源点击获取
返回列表