1. LightGBM简介
1.1 简介
LightGBM是的XGBoost的升级版,与XGBoost有近似精度的前提下,又大大提高了训练速度。
-
LightGBM的主要优点:
- 简单易用。提供了主流的Python\C++\R语言接口,用户可以轻松使用LightGBM建模并获得相当不错的效果。
- 高效可扩展。在处理大规模数据集时高效迅速、高准确度,对内存等硬件资源要求不高。
- 鲁棒性强。相较于深度学习模型不需要精细调参便能取得近似的效果。
- LightGBM直接支持缺失值与类别特征,无需对数据额外进行特殊处理
-
LightGBM的主要缺点:
- 相对于深度学习模型无法对时空位置建模,不能很好地捕获图像、语音、文本等高维数据。
- 在拥有海量训练数据,并能找到合适的深度学习模型时,深度学习的精度可以遥遥领先LightGBM。
1.2 应用
LightGBM在机器学习与数据挖掘领域有着极为广泛的应用。据统计LightGBM模型自2016到2019年在Kaggle平台上累积获得数据竞赛前三名三十余次,其中包括CIKM2017 AnalytiCup、IEEE Fraud Detection等知名竞赛。这些竞赛来源于各行各业的真实业务,这些竞赛成绩表明LightGBM具有很好的可扩展性,在各类不同问题上都可以取得非常好的效果。
同时,LightGBM还被成功应用在工业界与学术界的各种问题中。例如金融风控、购买行为识别、交通流量预测、环境声音分类、基因分类、生物成分分析等诸多领域。虽然领域相关的数据分析和特性工程在这些解决方案中也发挥了重要作用,但学习者与实践者对LightGBM的一致选择表明了这一软件包的影响力与重要性。
2. 实战
2.1 大体思路
本次实战的大体思路为:
- 给定特定场次的数据:击杀数、死亡数、金币数量、经验值、等级……等信息
- 通过01中给定的数据,预测本场次是否为蓝方获胜
- 需要注意的是,LightGBM可以支持缺项数据,无需额外处理
2.2 数据准备
-
数据集:train.csv
-
数据准备阶段的常规思路如下:
- 首先确定是否存在数据不平衡的问题
比如,数据中99%的是蓝方获胜,1%是红方获胜,那么这种数据估计难以起到训练的效果
- 可以利用热力图剔除相关性极强的特征
- 利用小提琴图分析某个特征与是否获胜之间的相关性
- 利用散点图进一步分析
- 去除与结论关联性不大的特征
- 对相关性较高的特征,进行组合
- 训练模型
- 评估模型
- 参数优化
- 评估模型
2.3 代码讲解
读取数据
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
#%%
df = pd.read_csv("3_dataset/high_diamond_ranked_10min.csv")
df
数据常用统计
拿到数据之后,一般会info(),valuecount()和describe()等函数初步查看数据的分布。
y = df.blueWins
y
# %%
df.info()
从info的结果中可以看到,各个数据的notnull都是相同的,说明没有空数据。无需进行填充处理。
df.describe()
从数据的describe可以查看数据的常见统计量(计数、均值、标准差、最小值、最大值,和四分位值)
y = df.blueWins
y
#%%
y.value_counts()
从以上数据分布中可知,数据的正负样本数量基本一致,不存在数据不平衡的问题
。
数据清洗
首先我们需要去掉与结果无关的特征列,比如gameId,blueWins
.
drop_cols = ["gameId","blueWins"]
x = df.drop(drop_cols,axis=1)
x.describe()
从上图的结果中可以看到:
- 我们发现不同对局中插眼数和拆眼数的取值范围存在明显差距,甚至有前十分钟插了250个眼的异常值。
- 我们发现EliteMonsters的取值相当于Deagons + Heralds。
- 我们发现TotalGold 等变量在大部分对局中差距不大。
- 我们发现两支队伍的经济差和经验差是相反数。
- 我们发现红队和蓝队拿到首次击杀的概率大概都是50%
其实我并没有发现,是天池原文作者发现的
## 根据上面的描述,我们可以去除一些重复变量,比如只要知道蓝队是否拿到一血,我们就知道红队有没有拿到,可以去除红队的相关冗余数据。
drop_cols = ['redFirstBlood','redKills','redDeaths'
,'redGoldDiff','redExperienceDiff', 'blueCSPerMin',
'blueGoldPerMin','redCSPerMin','redGoldPerMin']
x.drop(drop_cols, axis=1, inplace=True)
可视化描述数据
data = x
data_std = (data - data.mean())/data.std()
data = pd.concat([y, data_std.iloc[:, 0:9]], axis=1)
data = pd.melt(data, id_vars="blueWins",
var_name="Features", value_name="Values")
fig, ax = plt.subplots(1, 2, figsize=(15, 5))
# 绘制小提琴图
sns.violinplot(x="Features", y="Values", hue="blueWins", data=data,
split=True, inner="quart", ax=ax[0], palette="Blues")
fig.autofmt_xdate(rotation=45)
首先,先让数据正则化一下。
我们知道,如果 X ∼ N ( μ , σ ) X \sim N(\mu,\sigma) X∼N(μ,σ) , 那么 X − μ σ ∼ N ( 0 , 1 ) \frac{X-\mu}{\sigma} \sim N(0,1) σX−μ∼N(0,1),代码中的前两行正是利用这这样一种特点来对数据进行正则化。
下面针对所有数据绘制一遍小提琴图:
data = x
data_std = (data - data.mean())/data.std()
data = pd.concat([y, data_std.iloc[:, 0:9]], axis=1)
data = pd.melt(data, id_vars="blueWins",
var_name="Features", value_name="Values")
fig, ax = plt.subplots(1, 2, figsize=(15, 5))
# 绘制小提琴图
sns.violinplot(x="Features", y="Values", hue="blueWins", data=data,
split=True, inner="quart", ax=ax[0], palette="Blues")
fig.autofmt_xdate(rotation=45)
data = x
data_std = (data - data.mean()) / data.std()
data = pd.concat([y, data_std.iloc[:, 9:18]], axis=1)
data = pd.melt(data, id_vars='blueWins', var_name='Features', value_name='Values')
# 绘制小提琴图
sns.violinplot(x='Features', y='Values', hue='blueWins',
data=data, split=True, inner='quart', ax=ax[1], palette='Blues')
fig.autofmt_xdate(rotation=45)
plt.show()
小提琴图怎么看呢?见下图:
小提琴图 (Violin Plot)是用来展示多组数据的分布状态以及概率密度。这种图表结合了箱形图和密度图的特征,主要用来显示数据的分布形状。
从图中我们可以看出:
- 击杀英雄数量越多更容易赢,死亡数量越多越容易输(bluekills与bluedeaths左右的区别)。
- 助攻数量与击杀英雄数量形成的图形状类似,说明他们对游戏结果的影响差不多。
- 一血的取得情况与获胜有正相关,但是相关性不如击杀英雄数量明显。
- 经济差与经验差对于游戏胜负的影响较小。
- 击杀野怪数量对游戏胜负的影响并不大
其实我并没有看出来,还是天池的原文作者看出来的
plt.figure(figsize=(18, 14))
sns.heatmap(round(x.corr(), 2), cmap="Blues", annot=True)
plt.show()
从热力图中可以看到相互关联强的特征。然后去除一部分。
#%%
# 去除冗余特征
drop_cols = ['redAvgLevel','blueAvgLevel']
x.drop(drop_cols, axis=1, inplace=True)
sns.set(style='whitegrid', palette='muted')
# 构造两个新特征
x['wardsPlacedDiff'] = x['blueWardsPlaced'] - x['redWardsPlaced']
x['wardsDestroyedDiff'] = x['blueWardsDestroyed'] - x['redWardsDestroyed']
data = x[['blueWardsPlaced','blueWardsDestroyed','wardsPlacedDiff','wardsDestroyedDiff']].sample(1000)
data_std = (data - data.mean()) / data.std()
data = pd.concat([y, data_std], axis=1)
data = pd.melt(data, id_vars='blueWins', var_name='Features', value_name='Values')
plt.figure(figsize=(10,6))
sns.swarmplot(x='Features', y='Values', hue='blueWins', data=data)
plt.xticks(rotation=45)
plt.show()
我们画出了插眼数量的散点图,发现不存在插眼数量与游戏胜负间的显著规律。猜测由于钻石分段以上在哪插眼在哪好排眼都是套路,所以数据中前十分钟插眼数拔眼数对游戏的影响不大。所以我们暂时先把这些特征去掉。
## 去除和眼位相关的特征
drop_cols = ['blueWardsPlaced','blueWardsDestroyed','wardsPlacedDiff',
'wardsDestroyedDiff','redWardsPlaced','redWardsDestroyed']
x.drop(drop_cols, axis=1, inplace=True)
x['killsDiff'] = x['blueKills'] - x['blueDeaths']
x['assistsDiff'] = x['blueAssists'] - x['redAssists']
x[['blueKills','blueDeaths','blueAssists','killsDiff','assistsDiff','redAssists']].hist(figsize=(12,10), bins=20)
plt.show()
我们发现击杀、死亡与助攻数的数据分布差别不大。但是击杀减去死亡、助攻减去死亡的分布与原分布差别很大,因此我们新构造这么两个特征。
data = x[['blueKills','blueDeaths','blueAssists','killsDiff','assistsDiff','redAssists']].sample(1000)
data_std = (data - data.mean()) / data.std()
data = pd.concat([y, data_std], axis=1)
data = pd.melt(data, id_vars='blueWins', var_name='Features', value_name='Values')
plt.figure(figsize=(10,6))
sns.swarmplot(x='Features', y='Values', hue='blueWins', data=data)
plt.xticks(rotation=45)
plt.show()
从上图我们可以发现击杀数与死亡数与助攻数,以及我们构造的特征对数据都有较好的分类能力。
一些特征的两两组合对于数据划分能力也有提升。
# %%
x["dragonDiff"] = x["blueDragons"] - x["redDragons"]
x["heraldDiff"] = x["blueHeralds"] - x["redHeralds"]
x["eliteDiff"] = x["blueEliteMonsters"] - x["redEliteMonsters"]
data = pd.concat([y, x], axis=1)
eliteGroup = data.groupby(['eliteDiff'])['blueWins'].mean()
dragonGroup = data.groupby(['dragonDiff'])['blueWins'].mean()
heraldGroup = data.groupby(['heraldsDiff'])['blueWins'].mean()
fig , ax = plt.subplots(1,3,figsize=(15,4))
eliteGroup.plot(kind="bar",ax=ax[0])
dragonGroup.plot(kind="bar",ax=ax[1])
heraldGroup.plot(kind="bar",ax=ax[2])
print(eliteGroup)
print(dragonGroup)
print(heraldGroup)
plt.show()
之后又判断了两个队伍是否拿到了龙,是否拿到了峡谷先锋、击杀大型野怪的数量差值,发现在游戏的前期拿到龙比拿到峡谷先锋更容易取得胜利。
x['towerDiff'] = x['blueTowersDestroyed'] - x['redTowersDestroyed']
data = pd.concat([y, x], axis=1)
towerGroup = data.groupby(['towerDiff'])["blueWins"]
print(towerGroup.count())
print(towerGroup.mean())
fig, ax = plt.subplots(1, 2, figsize=(15, 5))
towerGroup.mean().plot(kind="line", ax=ax[0])
ax[0].set_title("Proportion of Blue Wins")
ax[0].set_ylabel("Proportion")
towerGroup.mean().plot(kind="line",ax=ax[1])
ax[1].set_title("Count of Towers Destroyed")
ax[1].set_ylabel("Count")
plt.show()
模型训练
数据划分
前面已经清洗完成的数据,在这里应该进行切分了。训练集:测试集 = 4:1
from sklearn.model_selection import train_test_split
data_target_part = y
data_feature_part = x
x_train, x_test, y_train, y_test = train_test_split(
data_feature_part, data_target_part, test_size=0.2, random_state=20210311)
模型训练
from lightgbm import LGBMClassifier
clf = LGBMClassifier()
clf.fit(x_train, y_train)
模型评估
对模型进行训练。因为lightgbm的强大,会发现数据训练的非常快。训练完成之后我们对结果进行预测。
train_predict = clf.predict(x_train)
test_predict = clf.predict(x_test)
print(f"train_predict:{train_predict} \n test_predict:{test_predict}")
直接看两个数组没什么好玩的,我们应该看混淆矩阵
from sklearn import metrics
print(f"accuracy of train:{metrics.accuracy_score(y_train,train_predict)}")
print(f"accuracy of test:{metrics.accuracy_score(y_test,test_predict)}")
confusion_matrix_result = metrics.confusion_matrix(test_predict,y_test)
print(f"confusion_matrix:{confusion_matrix_result}")
plt.figure()
sns.heatmap(confusion_matrix_result,annot=True,cmap="Blues")
plt.xlabel("Predict labels")
plt.ylabel("True labels")
plt.show()
混淆矩阵的结果如下图所示:
从热力图的颜色分布来看,这个结果还是可以接受的。
2.4 利用LightGBM进行特征选择
我们可以利用seaborn来直观的查看特征的重要性,来决定今后进行类似的任务的时候应该利用什么特征。
sns.barplot(y=data_feature_part.columns,x=clf.feature_importances_)
2.5 模型优化
模型优化的本质就是调整超参数。
LightGBM中包括但不限于下列对模型影响较大的参数:
- learning_rate
也叫eta,系统默认0.3
- num_leves
控制每棵树中最大叶子节点的数量
- feature_fraction
系统默认值为1,我们一般设置成0.8左右。用来控制每棵随机采用的列数的占比(每一列是一个特征)
- max-depth
系统默认值为6,一般取3-10之间。这个值为树的最大深度,用来控制过拟合。max-depth越大,模型的学习更加具体
常用的调参方法有贪心算法、网格调参、贝叶斯调参等。网格搜索是穷举,下面尝试网格搜索方法。
from sklearn.model_selection import GridSearchCV
learning_rate = [0.1,0.3,0.6]
feature_fraction = [0.5,0.8,1]
num_leves = [16,32,64]
max_depth = [-1,3,5,8]
parameters = {
"learning_rate":learning_rate,
"feature_fraction":feature_fraction,
"num_leves":num_leves,
"max_depth":max_depth
}
model = LGBMClassifier(n_estimators = 50)
clf = GridSearchCV(model,parameters,cv=3,scoring="accuracy",verbose=3,n_jobs=-1)
clf = clf.fit(x_train,y_train)
clf.best_params_
最后形成的超参数集合如下:
优化后的模型重新评估
clf = LGBMClassifier(feature_fraction=1, learning_rate=0.1, max_depth=3, num_leves=16)
clf.fit(x_train,y_train)
train_predict = clf.predict(x_train)
test_predict = clf.predict(x_test)
print(f"train accuracy:{metrics.accuracy_score(y_train,train_predict)}")
print(f"test accuracy:{metrics.accuracy_score(y_test,test_predict)}")
confusion_matrix_result = metrics.confusion_matrix(y_test,test_predict)
print(f"confusion matrix:{confusion_matrix_result}")
plt.figure(figsize=(8,6))
sns.heatmap(confusion_matrix_result,annot=True,cmap="Blues")
plt.xlabel("Predicted labels")
plt.ylabel("True labels")
plt.show()