ARTICLE DETAIL

资讯详情

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

飞桨MLP实战:英雄联盟段位预测中的特征工程与多分类调优

飞桨MLP实战:英雄联盟段位预测中的特征工程与多分类调优 1. 为什么选这道题赛事背景与赛题拆解飞桨学习赛是百度AI Studio平台上很经典的一类入门实战赛事我这次报的是“英雄联盟大师预测”。说白了赛题给了一批英雄联盟玩家的对局统计特征要求参赛者构建模型预测玩家最可能达到的段位。段位标签里包含大师这个高段位所以赛题叫“英雄联盟大师预测”。我选这道题主要基于三个判断第一这是一道典型的表格数据分类问题特征全部是数值型统计量和很多工业场景里的用户画像、风险评分任务同构做一遍能沉淀一套可复用的方法论第二数据集规模不大不像CV、NLP赛道那样动辄几十个G的数据和昂贵的训练资源普通笔记本的CPU也能跑通全流程第三段位预测天然带多分类、样本不均衡、特征相关性高等特点很适合用来练手深度学习框架下的调优基本功。先说赛题的具体目标。训练集里每行是一个玩家的行为快照列包含类似击杀数、死亡数、助攻数、总场次、胜场、常用位置、每局平均经济等在内的统计指标。标签是玩家的当前段位从黑铁到王者共9个级别。这里的难点在于特征本身是高度冗余的比如击杀数和场均击杀存在强线性关系段位分布又严重不平衡王者、大师这种头部段位样本占比可能不到1%。如果直接用朴素的多分类交叉熵训练模型很容易把所有样本都预测成占比最高的“黄金”“铂金”得到一个看似很高、实则无意义的准确率。一句话概括我在这道题里的解题主线先用EDA把数据分布和特征关系摸清楚再围绕游戏机制设计出有区分度的人工特征最后落到飞桨框架里用MLP类模型做多分类并在验证集上反复验证每一处改动是否真的有效。下面我把每个环节的细节和踩过的坑展开讲。2. 拿到数据第一件事数据初探与预处理细节2.1 先别急着训练把数据读进来看5分钟很多新手拿到CSV后第一反应是直接丢给模型训练这是最容易走弯路的地方。我习惯先做一轮快速探查主要看三样东西字段类型是否合理、缺失值多不多、标签分布是否极度倾斜。import pandas as pd train_df pd.read_csv(train.csv) test_df pd.read_csv(test.csv) print(train_df.shape, test_df.shape) print(train_df.dtypes.value_counts()) print(train_df.isnull().sum().sort_values(ascendingFalse).head(10)) print(train_df[rank].value_counts(normalizeTrue))我这次跑出来的结果有几个值得注意的点特征列有四十多个绝大多数是float64部分是int64没有object型的离散文本列但有一个“most_played_position”字段是典型的低频类别特征后面需要单独处理缺失值整体不多集中在三四个特征里缺失比例在1%到5%之间处理策略不需要太复杂标签分布确认是长尾形态“黄金”“铂金”两个段位占了将近一半样本“大师”“王者”合计不足2%。2.2 缺失值处理均值填充未必是最优解缺失值处理看似简单实则会直接影响最终效果。我的原则是数值型连续特征用中位数填充而不是均值。原因是这些统计量大多呈右偏分布个别“大号”玩家的击杀数可能远超中位数均值会被极端值拉高用均值填充相当于给缺失样本注入了偏高的信号。num_cols train_df.select_dtypes(include[float64, int64]).columns for col in num_cols: med train_df[col].median() train_df[col] train_df[col].fillna(med) test_df[col] test_df[col].fillna(med)对于“most_played_position”这种低频类别特征我采用众数填充然后做序数编码或独热编码。不要小看这个字段在英雄联盟里打野和中单的游走节奏、参团率差异非常大后续特征工程里它经常和KDA、击杀参与率产生有意义的交互。2.3 标签编码的正确姿势段位是有序的0到8分别对应黑铁到王者。这里有两种编码思路一是当作无序类别做普通Softmax多分类二是利用段位的天然顺序做回归或有序分类。我实测下来直接回归段位等级的效果并不理想因为段位之间的“距离”并不均匀——钻石到大师的跨度远大于白银到黄金的跨度。所以最终方案还是多分类但我会在损失函数上做文章这部分放到模型章节细说。提示标签编码时一定要保证训练集和测试集使用同一套映射表不要在测试集上重新fit否则极容易在提交阶段踩到“类别对不上”的坑。3. 特征工程从通用统计量到带机制的特征设计3.1 原始特征里的信息密度其实很低赛题给的特征虽然数量不少但信息密度并不高。比如“总场次”“胜场”和“胜率”三个特征本质上就是同一个信息的三种表达又比如“总击杀”“总死亡”“总助攻”和“总场次”组合起来才能算出真正有意义的KDA。直接把这些原始列喂给模型模型要花很多层才能隐式学到这些比值关系不如我们直接替它算好。我第一版特征工程的主线是“机制驱动”也就是围绕英雄联盟的游戏逻辑构造特征而不是盲目堆砌。英雄联盟胜负的核心变量有三类个人操作击杀、死亡、助攻、资源获取能力补刀、经济、等级、团队协作能力参团率、视野得分。所以我的特征设计也按这三类展开。3.2 亲手算出来的核心特征组第一组是个体效率特征包括KDA、(击杀助攻)/死亡、(击杀助攻)/场次等。这里有个小技巧KDA公式里的死亡数要加一个极小值平滑项防止出现除以0的inf。eps 1e-5 train_df[kda] (train_df[kills] train_df[assists]) / (train_df[deaths] eps) train_df[kill_participation] (train_df[kills] train_df[assists]) / (train_df[team_total_kills] eps)第二组是位置与风格特征做法是把“最常玩位置”进行独热编码后再与KDA、场均经济等指标做交叉。比如中单玩家的KDA与段位的相关性明显强于辅助玩家的KDA与段位的相关性如果不做交叉模型很难捕捉这种条件关系。第三组是稳定性和成长性特征比如场次等级变动率、胜率与KDA的比值。用胜率除以KDA可以刻画“混子型玩家”——胜率高但KDA低的玩家说明他更偏团队型打法反过来KDA高但胜率低说明是“独狼型”玩家。这类特征对段位预测的区分度非常可观。3.3 特征筛选不是越多越好特征工程做完后维度大概到了一百多个。我不建议直接把所有特征丢进模型因为部分交叉特征之间相关性极高既拖慢训练速度也增加过拟合风险。我习惯用两步筛选第一步看相关系数矩阵把相关系数超过0.95的特征合并或剔除第二步用LightGBM的feature_importance做一轮快速初筛保留重要性排序前80%的特征再落到飞桨模型里训练。这样做的逻辑很简单不同模型对特征的敏感度不同树模型擅长处理非线性且对特征尺度不敏感而深度学习模型对方差大的特征更敏感。先用树模型筛一轮再用网络训练可以在减少维度的同时保留绝大多数有效信号。4. 飞桨模型搭建从基准MLP到调优路径4.1 飞桨的建模方式和我为什么选MLP飞桨PaddlePaddle在AI Studio上支持动态图模式API设计和PyTorch非常接近上手成本很低。这道题的特征全部是结构化表格数据没有空间结构也没有时序依赖所以不需要上CNN、RNN、Transformer这类重型结构一个精心调过的多层感知机就能达到相当好的效果。我第一版基准模型用的是三层MLP输入层维度接特征数量中间两层每层256个神经元激活函数用ReLU每个全连接层后接Dropout输出层用Softmax输出9个段位的概率。评估指标以准确率为主同时参考Macro F1避免被头部段位的大样本主导。import paddle import paddle.nn as nn class RankModel(nn.Layer): def __init__(self, feat_dim, num_classes9): super().__init__() self.fc1 nn.Linear(feat_dim, 256) self.fc2 nn.Linear(256, 256) self.fc3 nn.Linear(256, 64) self.out nn.Linear(64, num_classes) self.drop nn.Dropout(0.3) self.relu nn.ReLU() def forward(self, x): x self.relu(self.fc1(x)) x self.drop(x) x self.relu(self.fc2(x)) x self.drop(x) x self.relu(self.fc3(x)) return self.out(x)4.2 损失函数的针对性设计加权交叉熵前面提到段位分布严重不均衡直接用nn.CrossEntropyLoss训练模型会偏向预测大类别。针对这个赛题我用了带类别权重的交叉熵损失。权重怎么算采用“1 / 类别频率开根号”的方式做平滑避免权重被头部段位压得过小。import numpy as np label_counts train_df[rank].value_counts().sort_index().values weights 1.0 / np.sqrt(label_counts) weights weights / weights.sum() * len(weights) weight_tensor paddle.to_tensor(weights, dtypefloat32) criterion nn.CrossEntropyLoss(weightweight_tensor)这里有个关键点类别权重的调整要适度。权重如果拉得过大模型会把预测结果推往小众段位整体准确率下降Macro F1却可能提升。比赛评价指标如果只看准确率权重就不宜过猛如果同时关注F1则需要做一个折中。我在实际调参中是用验证集同时看两个指标找到一个平衡点。4.3 训练配置与监控方法优化器我选Adam初始学习率设3e-4配合CosineAnnealingDecay做学习率衰减。批次大小64训练轮数50轮。每轮结束后用验证集评估一次准确率并保存验证集指标最优的那版模型而不是最后一轮模型——深度学习训练后期容易过拟合最后一轮往往不是泛化最好的点。scheduler paddle.optimizer.lr.CosineAnnealingDecay( learning_rate3e-4, T_max50, verboseTrue) optimizer paddle.optimizer.Adam( parametersmodel.parameters(), learning_ratescheduler)训练日志里我除了看loss还会打印准确率、Macro F1、以及每个段位的召回率。只看总loss容易产生“明明在下降但效果没变化”的错觉细分到每个类别的召回率能帮助定位模型是否放弃了某些段位。5. 踩坑实录训练阶段最耗时间的三个问题5.1 特征拼接没对齐训练集和测试集维度不一致这是我这次比赛踩的第一个坑。计数类特征在构造时如果训练集和测试集分开处理某些特征的值域范围不同导致fillna时填充值不一致甚至拼接后列数都对不上。最后训练时报了个“shape mismatch”的错定位半天才发现是预处理阶段的问题。后来我把特征工程封装成了一个函数先对训练集和测试集做concat处理整体算填充值、整体做编码再按原索引切分回两部分。这个习惯现在已经成为我所有表格赛事的标准流程。特征工程必须保证训练和推理走同一套逻辑否则线上分数和本地验证结果完全是两回事。5.2 Dropout过大导致验证集指标震荡第二版模型为了压制过拟合我把Dropout提到了0.5结果训练loss下降变慢验证集指标出现明显的震荡。原因是表格数据本身不是超大容量模型Dropout过大相当于每轮都在训练一个残缺的子网络在样本量不够大的时候反而损害了模型的稳定性。我把Dropout从0.5调回0.3同时把隐藏层从256缩减到128验证集指标立刻稳定下来。这里想说明一个原则正则化手段不是加得越多越好而是要在“模型容量”和“数据量”之间找平衡。飞桨里还有其它正则化工具比如Weight Decay和Label Smoothing我最后只用了轻量Weight Decay没有再叠加Label Smoothing避免改动过多导致无法判断哪项改动起效。5.3 验证集划分不当导致复现结果虚高另一个隐蔽的坑是验证集划分。赛题数据按玩家分组特征里没有显式的时间戳但不同行之间可能存在同一个玩家的重复采样。如果随机切分同一个玩家的相似对局会同时出现在训练集和验证集里造成验证集分数虚高。我处理的办法是按玩家ID分组划分。如果赛题没有提供玩家ID就退而求其次用特征聚类先找出可能重复的样本把同一类的样本放进同一个折里再分别划分训练和验证。这一步做完后验证集准确率比随机划分低了大概两个百分点但线下分数和线上提交结果的一致性明显提高。提示在任何表格类比赛中划分验证集前先确认样本之间是否存在分组相关是决定你调参方向是否可信的关键一环。宁可线下分低一点也要保证验证集能真实反映线上情况。6. 复现要点与我的赛后复盘6.1 完整流程串起来是什么样最后把整个流程按可复现的方式整理一遍。第一步读入数据检查字段类型、缺失值和标签分布第二步缺失值用中位数/众数填充类别特征做序数编码第三步围绕游戏机制构造三组特征个体效率、位置风格、稳定性成长性再做相关性筛选和树模型初筛第四步按玩家维度做分组划分拆出验证集第五步搭建飞桨MLP模型使用加权交叉熵损失Adam优化器加余弦退火训练50轮并保存验证集最优模型第六步在测试集上推理按官方格式输出提交文件。这套流程跑下来我的最终成绩大约在0.912的准确率Macro F1在0.74左右。作为对照完全不做特征工程、直接用原始特征训同样结构的模型准确率只有0.87左右说明人工特征在表格竞赛里的增益依旧非常可观。后续再提升的空间主要在两个方向一是用LightGBM和MLP做模型加权融合树模型和神经网络在表格数据上的错误模式差异较大融合后通常还能再涨一到两个点二是针对小众段位做更精细的上采样或数据增强比如用SMOTE合成了一些大师和王者样本但效果提升有限需要谨慎控制比例避免引入噪声。6.2 我在这道题上悟到的几点经验第一表格类任务里“特征机制”比“模型结构”重要得多。我试过把MLP换成更深的残差网络准确率几乎没有变化但特征工程的两个小改动就可能带来两三个点的提升。原因在于神经网络在表格数据里很难凭空学到特征之间的显式比值关系把这些关系直接喂给模型相当于替模型降低了拟合难度。第二样本不均衡问题的处理要跟随评价指标。如果评价指标是准确率模型偏向多数类其实是合理的如果评价指标是Macro F1就必须通过类别权重或重采样来平衡。赛前先搞清楚评价指标能省掉大量无用功。第三飞桨的动态图模式对调试非常友好所有的中间张量都可以直接打印和观察。我在调损失函数时就用这个特性打印过每一批样本的预测概率分布直观看到了模型初期把概率全部堆在“黄金”段位上的现象然后才针对性引入类别权重。最后再分享一个小技巧每次改动特征或者模型结构后固定随机种子在同一个验证集上跑至少三遍取平均值避免因为随机初始化导致的波动误判改动效果。这个习惯能帮你节省大量无效调参时间尤其是在数据量不算大的学习赛里单次验证结果的可信度真的很有限。
返回列表