CC BY 4.0 (除特别声明或转载文章外)
如果这篇博客帮助到你,可以请我喝一杯咖啡~
决策树手动实现
本教程来源于学堂在线 本次以英雄联盟对局胜负预测任务为基础,要求实现决策树算法相关细节,加深对算法的理解,并了解做机器学习任务的大致流程。
任务介绍
英雄联盟(League of Legends,LoL)是一个多人在线竞技游戏,由拳头游戏(Riot Games)公司出品。在游戏中,每位玩家控制一位有独特技能的英雄,红蓝两支队伍各有五位玩家进行对战,目标是摧毁对方的基地水晶。水晶有多座防御塔保护,通常需要先摧毁一些防御塔再摧毁水晶。玩家所控制的英雄起初非常弱,需要不断击杀小兵、野怪和对方英雄来获得金币、经验。经验可以提升英雄等级和技能等级,金币可以用来购买装备提升攻击、防御等属性。对战过程中一般没有己方单位在附近的地点是没有视野的,即无法看到对面单位,双方可以通过使用守卫来监视某个地点,洞察对面走向、制定战术。 本数据集来自Kaggle,包含了9879场钻一到大师段位的单双排对局,对局双方几乎是同一水平。每条数据是前10分钟的对局情况,每支队伍有19个特征,红蓝双方共38个特征。这些特征包括英雄击杀、死亡,金钱、经验、等级情况等等。一局游戏一般会持续30至40分钟,但是实际前10分钟的局面很大程度上影响了之后胜负的走向。作为最成功的电子竞技游戏之一,对局数据、选手数据的量化与研究具有重要意义,可以启发游戏将来的发展和改进。
本任务是希望同学们依据注释的要求,对代码中空缺部分进行填写,完成决策树模型的详细实现,根据已有的对局前10分钟特征信息,预测最后获胜方是蓝色方还是红色方,了解执行一个机器学习任务的大致流程。第一次作业也是一个机器学习小实验的例子,之后的作业可能不再提供预处理等流程代码,由同学们自己设计实验完成代码编写。
导入工具包
pandas是数据分析和处理常用的工具包,非常适合处理行列表格数据。numpy是数学运算工具包,支持高效的矩阵、向量运算。sklearn是机器学习常用工具包,包括了一些已经实现好的简单模型和一些常用数据处理方法、评价指标等函数。
from collections import Counter
import pandas as pd # 数据处理
import numpy as np # 数学运算
from sklearn.model_selection import train_test_split, cross_validate, GridSearchCV # 划分数据集函数
from sklearn.metrics import accuracy_score # 准确率函数
from sklearn.tree import DecisionTreeClassifier # sklearn的决策树模型
RANDOM_SEED = 2020 # 固定随机种子
读入数据
假设数据文件放在./data/
目录下,标准的csv文件可以用pandas里的read_csv()
函数直接读入。文件共有40列,38个特征(红蓝方各19),1个标签列(blueWins),和一个对局标号(gameId)。对局标号不是标签也不是特征,可以舍去。
csv_data = './data/high_diamond_ranked_10min.csv' # 数据路径
data_df = pd.read_csv(csv_data, sep=',') # 读入csv文件为pandas的DataFrame
data_df = data_df.drop(columns='gameId') # 舍去对局标号列
数据概览
对于一个机器学习问题,在拿到任务和数据后,首先需要观察数据的情况,比如我们可以通过.iloc[0]
取出数据的第一行并输出。不难看出每个特征都存成了float64浮点数,该对局蓝色方开局10分钟有小优势。同时也可以发现有些特征列是重复冗余的,比如blueGoldDiff表示蓝色队金币优势,redGoldDiff表示红色方金币优势,这两个特征是完全对称的互为相反数。blueCSPerMin是蓝色方每分钟击杀小兵数,它乘10就是10分钟所有小兵击杀数blueTotalMinionsKilled。在之后的特征处理过程中可以考虑去除这些冗余特征。
另外,pandas有非常方便的describe()
函数,可以直接通过DataFrame进行调用,可以展示每一列数据的一些统计信息,对数据分布情况有大致了解,比如blueKills蓝色方击杀英雄数在前十分钟的平均数是6.14、方差为2.93,中位数是6,百分之五十以上的对局中该特征在4-8之间,等等。
print(data_df.iloc[0]) # 输出第一行数据
data_df.describe() # 每列特征的简单统计信息
blueWins 0.0
blueWardsPlaced 28.0
blueWardsDestroyed 2.0
blueFirstBlood 1.0
blueKills 9.0
blueDeaths 6.0
blueAssists 11.0
blueEliteMonsters 0.0
blueDragons 0.0
blueHeralds 0.0
blueTowersDestroyed 0.0
blueTotalGold 17210.0
blueAvgLevel 6.6
blueTotalExperience 17039.0
blueTotalMinionsKilled 195.0
blueTotalJungleMinionsKilled 36.0
blueGoldDiff 643.0
blueExperienceDiff -8.0
blueCSPerMin 19.5
blueGoldPerMin 1721.0
redWardsPlaced 15.0
redWardsDestroyed 6.0
redFirstBlood 0.0
redKills 6.0
redDeaths 9.0
redAssists 8.0
redEliteMonsters 0.0
redDragons 0.0
redHeralds 0.0
redTowersDestroyed 0.0
redTotalGold 16567.0
redAvgLevel 6.8
redTotalExperience 17047.0
redTotalMinionsKilled 197.0
redTotalJungleMinionsKilled 55.0
redGoldDiff -643.0
redExperienceDiff 8.0
redCSPerMin 19.7
redGoldPerMin 1656.7
Name: 0, dtype: float64
blueWins | blueWardsPlaced | blueWardsDestroyed | blueFirstBlood | blueKills | blueDeaths | blueAssists | blueEliteMonsters | blueDragons | blueHeralds | ... | redTowersDestroyed | redTotalGold | redAvgLevel | redTotalExperience | redTotalMinionsKilled | redTotalJungleMinionsKilled | redGoldDiff | redExperienceDiff | redCSPerMin | redGoldPerMin | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | ... | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 | 9879.000000 |
mean | 0.499038 | 22.288288 | 2.824881 | 0.504808 | 6.183925 | 6.137666 | 6.645106 | 0.549954 | 0.361980 | 0.187974 | ... | 0.043021 | 16489.041401 | 6.925316 | 17961.730438 | 217.349226 | 51.313088 | -14.414111 | 33.620306 | 21.734923 | 1648.904140 |
std | 0.500024 | 18.019177 | 2.174998 | 0.500002 | 3.011028 | 2.933818 | 4.064520 | 0.625527 | 0.480597 | 0.390712 | ... | 0.216900 | 1490.888406 | 0.305311 | 1198.583912 | 21.911668 | 10.027885 | 2453.349179 | 1920.370438 | 2.191167 | 149.088841 |
min | 0.000000 | 5.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 0.000000 | 11212.000000 | 4.800000 | 10465.000000 | 107.000000 | 4.000000 | -11467.000000 | -8348.000000 | 10.700000 | 1121.200000 |
25% | 0.000000 | 14.000000 | 1.000000 | 0.000000 | 4.000000 | 4.000000 | 4.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 0.000000 | 15427.500000 | 6.800000 | 17209.500000 | 203.000000 | 44.000000 | -1596.000000 | -1212.000000 | 20.300000 | 1542.750000 |
50% | 0.000000 | 16.000000 | 3.000000 | 1.000000 | 6.000000 | 6.000000 | 6.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 0.000000 | 16378.000000 | 7.000000 | 17974.000000 | 218.000000 | 51.000000 | -14.000000 | 28.000000 | 21.800000 | 1637.800000 |
75% | 1.000000 | 20.000000 | 4.000000 | 1.000000 | 8.000000 | 8.000000 | 9.000000 | 1.000000 | 1.000000 | 0.000000 | ... | 0.000000 | 17418.500000 | 7.200000 | 18764.500000 | 233.000000 | 57.000000 | 1585.500000 | 1290.500000 | 23.300000 | 1741.850000 |
max | 1.000000 | 250.000000 | 27.000000 | 1.000000 | 22.000000 | 22.000000 | 29.000000 | 2.000000 | 1.000000 | 1.000000 | ... | 2.000000 | 22732.000000 | 8.200000 | 22269.000000 | 289.000000 | 92.000000 | 10830.000000 | 9333.000000 | 28.900000 | 2273.200000 |
8 rows × 39 columns
增删特征
传统的机器学习模型大部分都是基于特征的,因此特征工程是机器学习中非常重要的一步。有时构造一个好的特征比改进一个模型带来的提升更大。这里简单展示一些特征处理的例子。首先,上面提到,特征列中有些特征信息是完全冗余的,会给模型带来不必要的计算量,可以去除。其次,相比于红蓝双方击杀、助攻的绝对值,可能双方击杀英雄的差值更能体现出当前对战的局势。因此,我们可以构造红蓝双方对应特征的差值。数据文件中已有的差值是金币差GoldDiff和经验差ExperienceDiff,实际上每个对应特征都可以构造这样的差值特征。
drop_features = ['blueGoldDiff', 'redGoldDiff',
'blueExperienceDiff', 'redExperienceDiff',
'blueCSPerMin', 'redCSPerMin',
'blueGoldPerMin', 'redGoldPerMin'] # 需要舍去的特征列
df = data_df.drop(columns=drop_features) # 舍去特征列
info_names = [c[3:] for c in df.columns if c.startswith('red')] # 取出要作差值的特征名字(除去red前缀)
for info in info_names: # 对于每个特征名字
df['br' + info] = df['blue' + info] - df['red' + info] # 构造一个新的特征,由蓝色特征减去红色特征,前缀为br
# 其中FirstBlood为首次击杀最多有一只队伍能获得,brFirstBlood=1为蓝,0为没有产生,-1为红
df = df.drop(columns=['blueFirstBlood', 'redFirstBlood']) # 原有的FirstBlood可删除
特征离散化
决策树ID3算法一般是基于离散特征的,本例中存在很多连续的数值特征,例如队伍金币。直接应用该算法每个值当作一个该特征的一个取值可能造成严重的过拟合,因此需要对特征进行离散化,即将一定范围内的值映射成一个值,例如对用户年龄特征,将0-10映射到0,11-18映射到1,19-25映射到2,25-30映射到3,等等类似,然后在决策树构建时使用映射后的值计算信息增益。
本小节要求实现特征离散化,请补全相关代码
DISCRETE_N = 10
discrete_df = df.copy() # 先复制一份数据
for c in df.columns[1:]: # 遍历每一列特征,跳过标签列
if len(df[c].unique()) <= DISCRETE_N: # 对原本取值可能性就较少的列,比如大龙数目,可不作处理
continue
else:
# precsion=0表示区间间隔数字的精度保持和数据原本精度相同,duplicates='drop'表示去除某些重复的分隔数(即空区间)
# 调用pandas的qcut函数,表示等密度区间,尽量让每个区间里的样本数相当,但每个区间长度可能不同
discrete_df[c] = pd.qcut(df[c], DISCRETE_N, precision=0, labels=False, duplicates='drop')
# 调用pandas的cut函数,表示等长的区间,尽量让每个区间的长度相当,但每个区间里的样本数可能不同
# discrete_df[c] = pd.cut(df[c], 10, precision=0, duplicates='drop')
数据集准备
构建机器学习模型前要构建训练和测试的数据集。在本例中首先需要分开标签和特征,标签是不能作为模型的输入特征的,就好比作业和试卷答案不能在做题和考试前就告诉学生。测试一个模型在一个任务上的效果至少需要训练集和测试集,训练集用来训练模型的参数,好比学生做作业获得知识,测试集用来测试模型效果,好比期末考试考察学生学习情况。测试集的样本不应该出现在训练集中,否则会造成模型效果估计偏高,好比考试时出的题如果是作业题中出现过的,会造成考试分数不能准确衡量学生的学习情况,估计值偏高。划分训练集和测试集有多种方法,下面首先介绍的是随机取一部分如20%作测试集,剩下作训练集。sklearn提供了相关工具函数train_test_split
。sklearn的输入输出一般为numpy的array矩阵,需要先将pandas的DataFrame取出为numpy的array矩阵。
all_y = discrete_df['blueWins'].values # 所有标签数据
feature_names = discrete_df.columns[1:] # 所有特征的名称
all_x = discrete_df[feature_names].values # 所有原始特征值,pandas的DataFrame.values取出为numpy的array矩阵
# 划分训练集和测试集
x_train, x_test, y_train, y_test = train_test_split(all_x, all_y, test_size=0.2, random_state=RANDOM_SEED)
all_y.shape, all_x.shape, x_train.shape, x_test.shape, y_train.shape, y_test.shape # 输出数据行列信息
((9879,), (9879, 43), (7903, 43), (1976, 43), (7903,), (1976,))
决策树模型的实现
本小节要求实现决策树模型,请补全算法代码
# 定义决策树类
class DecisionTree(object):
def __init__(self, classes, features,
max_depth=10, min_samples_split=10,
impurity_t='entropy'):
'''
传入一些可能用到的模型参数,也可能不会用到
classes表示模型分类总共有几类
features是每个特征的名字,也方便查询总的共特征数
max_depth表示构建决策树时的最大深度
min_samples_split表示构建决策树分裂节点时,如果到达该节点的样本数小于该值则不再分裂
impurity_t表示计算混杂度(不纯度)的计算方式,例如entropy或gini
'''
self.classes = classes
self.features = features
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.impurity_t = impurity_t
self.root = None # 定义根节点,未训练时为空
def impurity(self, data):
'''
计算混杂度
参数data是一个numpy一维数组
'''
cnt = Counter(data) # 计数每个值出现的次数
ps = [1.0 * cnt[v]/len(data) for v in cnt] # 计算每个值出现的比例
if self.impurity_t == 'entropy': # 如果是信息熵
return -np.sum([p * np.log2(p) for p in ps if p > 0]), cnt # 同时返回每个值出现的次数可能方便之后运算
return 1 - np.sum([p*p for p in ps]), cnt # 返回gini系数
def gain(self, feature, label):
'''
计算某个特征下的信息增益
feature是特征的值,numpy一维数组
label是对应的标签,numpy一维数组
'''
c_impurity, _ = self.impurity(label) # 不考虑特征时标签的混杂度
# 记录特征的每种取值所对应的数组下标
f_index = {}
for idx, v in enumerate(feature):
if v not in f_index:
f_index[v] = []
f_index[v].append(idx)
# 计算根据该特征分裂后的不纯度,根据特征的每种值的数目加权和
f_impurity = 0
for v in f_index:
f_l = label[f_index[v]] # 取出该特征取值对应的数组下标
f_impurity += self.impurity(f_l)[0] * len(f_l)/len(label) # 计算不纯度并乘以该特征取值的比例
# 有些特征取值很多,天然不纯度高、信息增益高,模型会偏向于取值很多的特征比如日期,但很可能过拟合
# 计算信息增益率可以缓解该问题
r = self.impurity(feature)[0] # 计算该特征在标签无关时的不纯度
r = (c_impurity - f_impurity)/r if r > 0 else c_impurity - f_impurity # 除数不为0时为信息增益率
return r, f_index # 返回信息增益率,以及每个特征取值的数组下标,方便之后使用
def expand_node(self, feature, label, depth, skip_features=set()):
'''
分裂节点,feature和label为到达该节点的样本
feature为二维numpy(n*m)数组,每行表示一个样本,有m个特征
label为一维numpy(n)数组,表示每个样本的分类标签
depth记录了当前节点的深度
skip_features表示当前路径已经用到的特征
在当前ID3算法离散特征的实现下,一条路径上已经用过的特征不会再用(其他实现有可能会选重复特征)
'''
l_cnt = Counter(label) # 计数每个类别的样本出现次数
if len(l_cnt) == 0: # 如果只有一种类别了,无需分裂,已经是叶节点
return label[0] # 只需记录类别
if len(label) < self.min_samples_split or depth > self.max_depth: # 如果达到了最小分裂的样本数或者最大深度的阈值
return l_cnt.most_common(1)[0][0] # 则只记录当前样本中最多的类别
f_idx, max_gain, fv_index = -1, -1, None # 准备挑选分裂特征
for idx in range(len(self.features)): # 遍历所有特征
if idx in skip_features: # 如果当前路径已经用到,不用再算
continue
f_gain, fv = self.gain(feature[:, idx], label) # 计算特征的信息增益,fv是特征每个取值的样本下标
# if f_gain <= 0: # 如果信息增益不为正,跳过该特征
# continue
if f_idx < 0 or f_gain > max_gain: # 如果个更好的分裂特征
f_idx, max_gain, fv_index = idx, f_gain, fv # 则记录该特征
if f_idx < 0: # 如果没有找到合适的特征,即所有特征都没有信息增益
return l_cnt.most_common(1)[0][0] # 则只记录当前样本中最多的类别
decision = {} # 用字典记录每个特征取值所对应的子节点,key是特征取值,value是子节点
skip_features = set([f_idx]+[f for f in skip_features]) # 子节点要跳过的特征包括当前选择的特征
for v in fv_index: # 遍历特征的每种取值
decision[v] = self.expand_node(feature[fv_index[v], :], label[fv_index[v]], # 取出该特征取值所对应的样本
depth=depth + 1, skip_features=skip_features) # 深度+1,递归调用节点分裂
# 返回一个元组,有三个元素
# 第一个是选择的特征下标,第二个特征取值和对应的子节点,第三个是到达当前节点的样本中最多的类别
return (f_idx, decision, l_cnt.most_common(1)[0][0])
def traverse_node(self, node, feature):
'''
预测样本时从根节点开始遍历节点,根据特征路由。
node表示当前到达的节点,例如self.root
feature是长度为m的numpy一维数组
'''
assert len(self.features) == len(feature) # 要求输入样本特征数和模型定义时特征数目一致
if type(node) is not tuple: # 如果到达了一个节点是叶节点(不再分裂),则返回该节点类别
return node
fv = feature[node[0]] # 否则取出该节点对应的特征值,node[0]记录了特征的下标
if fv in node[1]: # 根据特征值找到子节点,注意需要判断训练节点分裂时到达该节点的样本是否有该特征值(分支)
return self.traverse_node(node[1][fv], feature) # 如果有,则进入到子节点继续遍历
return node[-1] # 如果没有,返回训练时到达当前节点的样本中最多的类别
def fit(self, feature, label):
'''
训练模型
feature为二维numpy(n*m)数组,每行表示一个样本,有m个特征
label为一维numpy(n)数组,表示每个样本的分类标签
'''
assert len(self.features) == len(feature[0]) # 输入数据的特征数目应该和模型定义时的特征数目相同
self.root = self.expand_node(feature, label, depth=1) # 从根节点开始分裂,模型记录根节点
def predict(self, feature):
'''
预测
输入feature可以是一个一维numpy数组也可以是一个二维numpy数组
如果是一维numpy(m)数组则是一个样本,包含m个特征,返回一个类别值
如果是二维numpy(n*m)数组则表示n个样本,每个样本包含m个特征,返回一个numpy一维数组
'''
assert len(feature.shape) == 1 or len(feature.shape) == 2 # 只能是1维或2维
if len(feature.shape) == 1: # 如果是一个样本
return self.traverse_node(self.root, feature) # 从根节点开始路由
return np.array([self.traverse_node(self.root, f) for f in feature]) # 如果是很多个样本
def get_params(self, deep): # 要调用sklearn的cross_validate需要实现该函数返回所有参数
return {'classes': self.classes, 'features': self.features,
'max_depth': self.max_depth, 'min_samples_split': self.min_samples_split,
'impurity_t': self.impurity_t}
def set_params(self, **parameters): # 要调用sklearn的GridSearchCV需要实现该函数给类设定所有参数
for parameter, value in parameters.items():
setattr(self, parameter, value)
return self
# 定义决策树模型,传入算法参数
DT = DecisionTree(classes=[0,1], features=feature_names, max_depth=5, min_samples_split=10, impurity_t='gini')
# DT = DecisionTreeClassifier(max_depth=5, min_samples_split=10)
DT.fit(x_train, y_train) # 在训练集上训练
p_test = DT.predict(x_test) # 在测试集上预测,获得预测值
print(p_test) # 输出预测值
test_acc = accuracy_score(p_test, y_test) # 将测试预测值与测试集标签对比获得准确率
print('accuracy: {:.4f}'.format(test_acc)) # 输出准确率
[0 1 0 ... 0 1 1]
accuracy: 0.6964
模型调优
第一次模型测试结果可能不够好,可以先检查调试代码是否有bug,再尝试调整参数或者优化计算方法。 如果在一定范围内遍历模型参数,可以得到最好的参数。可以看到最佳模型的根节点也就是最重要特征是brTotalGold队伍金币差。
best = None # 记录最佳结果
for impurity_t in ['entropy', 'gini']: # 遍历不纯度的计算方式
for max_depth in range(1, 6): # 遍历最大树深度
for min_samples_split in [50, 100, 200, 500, 1000]: # 遍历节点分裂最小样本数的阈值
DT = DecisionTree(classes=[0,1], features=feature_names, # 定义决策树
max_depth=max_depth, min_samples_split=min_samples_split, impurity_t=impurity_t)
cv_result = cross_validate(DT, all_x, all_y, scoring=('accuracy'), cv=5) # 5折交叉验证
test_acc = np.mean(cv_result['test_score']) # 5折平均准确率
current = (test_acc, max_depth, min_samples_split, impurity_t) # 记录参数和结果
if best is None or test_acc > best[0]: # 如果是比当前最佳更好的结果
best = current # 记录最好结果
print('better accuracy: {:.4f}, max_depth={}, min_samples_split={}, impurity_t={}'.format(*best)) # 输出准确率和参数
else:
print('accuracy: {:.4f}, max_depth={}, min_samples_split={}, impurity_t={}'.format(*current)) # 输出准确率和参数
better accuracy: 0.7235, max_depth=1, min_samples_split=50, impurity_t=entropy
accuracy: 0.7235, max_depth=1, min_samples_split=100, impurity_t=entropy
accuracy: 0.7235, max_depth=1, min_samples_split=200, impurity_t=entropy
accuracy: 0.7235, max_depth=1, min_samples_split=500, impurity_t=entropy
accuracy: 0.7235, max_depth=1, min_samples_split=1000, impurity_t=entropy
accuracy: 0.7225, max_depth=2, min_samples_split=50, impurity_t=entropy
accuracy: 0.7225, max_depth=2, min_samples_split=100, impurity_t=entropy
accuracy: 0.7225, max_depth=2, min_samples_split=200, impurity_t=entropy
accuracy: 0.7225, max_depth=2, min_samples_split=500, impurity_t=entropy
accuracy: 0.7235, max_depth=2, min_samples_split=1000, impurity_t=entropy
better accuracy: 0.7256, max_depth=3, min_samples_split=50, impurity_t=entropy
better accuracy: 0.7264, max_depth=3, min_samples_split=100, impurity_t=entropy
better accuracy: 0.7278, max_depth=3, min_samples_split=200, impurity_t=entropy
better accuracy: 0.7281, max_depth=3, min_samples_split=500, impurity_t=entropy
accuracy: 0.7235, max_depth=3, min_samples_split=1000, impurity_t=entropy
accuracy: 0.7155, max_depth=4, min_samples_split=50, impurity_t=entropy
accuracy: 0.7195, max_depth=4, min_samples_split=100, impurity_t=entropy
accuracy: 0.7226, max_depth=4, min_samples_split=200, impurity_t=entropy
better accuracy: 0.7298, max_depth=4, min_samples_split=500, impurity_t=entropy
accuracy: 0.7235, max_depth=4, min_samples_split=1000, impurity_t=entropy
accuracy: 0.7022, max_depth=5, min_samples_split=50, impurity_t=entropy
accuracy: 0.7098, max_depth=5, min_samples_split=100, impurity_t=entropy
accuracy: 0.7155, max_depth=5, min_samples_split=200, impurity_t=entropy
accuracy: 0.7297, max_depth=5, min_samples_split=500, impurity_t=entropy
accuracy: 0.7235, max_depth=5, min_samples_split=1000, impurity_t=entropy
accuracy: 0.7235, max_depth=1, min_samples_split=50, impurity_t=gini
accuracy: 0.7235, max_depth=1, min_samples_split=100, impurity_t=gini
accuracy: 0.7235, max_depth=1, min_samples_split=200, impurity_t=gini
accuracy: 0.7235, max_depth=1, min_samples_split=500, impurity_t=gini
accuracy: 0.7235, max_depth=1, min_samples_split=1000, impurity_t=gini
accuracy: 0.7216, max_depth=2, min_samples_split=50, impurity_t=gini
accuracy: 0.7216, max_depth=2, min_samples_split=100, impurity_t=gini
accuracy: 0.7216, max_depth=2, min_samples_split=200, impurity_t=gini
accuracy: 0.7216, max_depth=2, min_samples_split=500, impurity_t=gini
accuracy: 0.7235, max_depth=2, min_samples_split=1000, impurity_t=gini
accuracy: 0.7207, max_depth=3, min_samples_split=50, impurity_t=gini
accuracy: 0.7214, max_depth=3, min_samples_split=100, impurity_t=gini
accuracy: 0.7236, max_depth=3, min_samples_split=200, impurity_t=gini
accuracy: 0.7245, max_depth=3, min_samples_split=500, impurity_t=gini
accuracy: 0.7235, max_depth=3, min_samples_split=1000, impurity_t=gini
accuracy: 0.7117, max_depth=4, min_samples_split=50, impurity_t=gini
accuracy: 0.7172, max_depth=4, min_samples_split=100, impurity_t=gini
accuracy: 0.7213, max_depth=4, min_samples_split=200, impurity_t=gini
accuracy: 0.7276, max_depth=4, min_samples_split=500, impurity_t=gini
accuracy: 0.7235, max_depth=4, min_samples_split=1000, impurity_t=gini
accuracy: 0.6961, max_depth=5, min_samples_split=50, impurity_t=gini
accuracy: 0.7083, max_depth=5, min_samples_split=100, impurity_t=gini
accuracy: 0.7167, max_depth=5, min_samples_split=200, impurity_t=gini
accuracy: 0.7275, max_depth=5, min_samples_split=500, impurity_t=gini
accuracy: 0.7235, max_depth=5, min_samples_split=1000, impurity_t=gini
也可以调用sklearn的GridSearchCV自动且多线程搜索参数,和上面的流程类似。
parameters = {'impurity_t':['entropy', 'gini'],
'max_depth': range(1, 6),
'min_samples_split': [50, 100, 200, 500, 1000]} # 定义需要遍历的参数
DT = DecisionTree(classes=[0,1], features=feature_names) # 定义决策树,可以不传参数,由GridSearchCV传入构建
grid_search = GridSearchCV(DT, parameters, scoring='accuracy', cv=5, verbose=10, n_jobs=4) # 传入模型和要遍历的参数
grid_search.fit(all_x, all_y) # 在所有数据上搜索参数
print(grid_search.best_score_, grid_search.best_params_) # 输出最佳指标和最佳参数
Fitting 5 folds for each of 50 candidates, totalling 250 fits
[Parallel(n_jobs=4)]: Using backend LokyBackend with 4 concurrent workers.
[Parallel(n_jobs=4)]: Done 5 tasks | elapsed: 1.2s
[Parallel(n_jobs=4)]: Done 10 tasks | elapsed: 1.5s
[Parallel(n_jobs=4)]: Done 17 tasks | elapsed: 2.1s
[Parallel(n_jobs=4)]: Done 24 tasks | elapsed: 2.4s
[Parallel(n_jobs=4)]: Done 33 tasks | elapsed: 3.9s
[Parallel(n_jobs=4)]: Done 42 tasks | elapsed: 5.4s
[Parallel(n_jobs=4)]: Done 53 tasks | elapsed: 7.0s
[Parallel(n_jobs=4)]: Done 64 tasks | elapsed: 9.5s
[Parallel(n_jobs=4)]: Done 77 tasks | elapsed: 12.1s
[Parallel(n_jobs=4)]: Done 90 tasks | elapsed: 15.7s
[Parallel(n_jobs=4)]: Done 105 tasks | elapsed: 20.1s
[Parallel(n_jobs=4)]: Done 120 tasks | elapsed: 23.9s
[Parallel(n_jobs=4)]: Done 137 tasks | elapsed: 25.2s
[Parallel(n_jobs=4)]: Done 154 tasks | elapsed: 26.7s
[Parallel(n_jobs=4)]: Done 173 tasks | elapsed: 29.3s
[Parallel(n_jobs=4)]: Done 192 tasks | elapsed: 33.6s
[Parallel(n_jobs=4)]: Done 213 tasks | elapsed: 38.7s
[Parallel(n_jobs=4)]: Done 234 tasks | elapsed: 44.1s
[Parallel(n_jobs=4)]: Done 250 out of 250 | elapsed: 46.8s finished
0.7298305232409164 {'impurity_t': 'entropy', 'max_depth': 4, 'min_samples_split': 500}
查看节点的特征
best_dt = grid_search.best_estimator_ # 取出最佳模型
print('root', best_dt.features[best_dt.root[0]]) # 输出根节点特征
for fv in best_dt.root[1]: # 遍历根节点的每种特征取值
print(fv, '->', best_dt.features[best_dt.root[1][fv][0]], '-> ...') # 输出下一层特征
root brTotalGold
6 -> redTowersDestroyed -> ...
1 -> blueTowersDestroyed -> ...
3 -> redEliteMonsters -> ...
2 -> redTowersDestroyed -> ...
8 -> redTowersDestroyed -> ...
5 -> redTowersDestroyed -> ...
9 -> redDragons -> ...
0 -> blueTowersDestroyed -> ...
4 -> brTowersDestroyed -> ...
7 -> blueTotalExperience -> ...
总结
一个完整的机器学习任务包括:确定任务、数据分析、特征工程、数据集划分、模型设计、模型训练和效果测试、结果分析和调优等多个阶段,本案例以英雄联盟游戏胜负预测任务为例,给出了每个阶段的一些简单例子,帮助大家入门机器学习,希望大家有所收获!