【实验一】基于决策树的英雄联盟游戏胜负预测实验报告

实验目的

根据对局前10分钟信息预测蓝方结局胜负。

方法

基于决策树模型,首先用训练集数据进行预处理,合理设计决策树结构,最后用测试机检测准确率。数据来源

过程

一,导入工具包

pandas是数据分析和处理常用的工具包,非常适合处理行列表格数据。numpy是数学运算工具包,支持高效的矩阵、向量运算。sklearn是机器学习常用工具包,包括了一些已经实现好的简单模型和一些常用数据处理方法、评价指标等函数。
由于我python仅有入门级别水平,这一过程涉及到许多我不曾了解的工具包。经过学习,个人感觉用处最大的是Counter与train_test_split。

Counter

Counter可起到计数器的作用,可统计传入参数(已使可行的有list和numpy)的各元素数量,在统计分析方面作用很大。

from collections import Counter
color = ['red','bule','white','red','bule']
c = Counter(color)
print(c)

统计过后,Counter也可以进行许多便捷操作,在本次实验中用到的是most_common,该函数可求出出现次数最多的k个元素

    def majorCnt(self, lable):
        Cnt = Counter(lable)
        return Cnt.most_common(1)[0][0]

train_test_split

X_train,X_test, y_train, y_test =sklearn.model_selection.train_test_split(train_data,train_target,
                                                                          test_size=0.4, random_state=0,stratify=y_train)
  • train_data:所要划分的样本特征集*
  • train_target:所要划分的样本结果*
  • test_size:样本占比,如果是整数的话就是样本的数量*
  • random_state:是随机数的种子。*
  • 随机数种子:其实就是该组随机数的编号,在需要重复试验的时候,保证得到一组一样的随机数。比如你每次都填1,其他参数一样的情况下你得到的随机数组是一样的。但填0或不填,每次都会不一样。*

stratify是为了保持split前类的分布。比如有100个数据,80个属于A类,20个属于B类。如果train_test_split(… test_size=0.25, stratify = y_all), 那么split之后数据如下:
training: 75个数据,其中60个属于A类,15个属于B类。
testing: 25个数据,其中20个属于A类,5个属于B类。

用了stratify参数,training集和testing集的类的比例是 A:B= 4:1,等同于split前的比例(80:20)。通常在这种类分布不平衡的情况下会用到stratify。

将stratify=X就是按照X中的比例分配

将stratify=y就是按照y中的比例分配

二,读入数据

假设数据文件放在./data/目录下,标准的csv文件可以用pandas里的read_csv()函数直接读入。文件共有40列,38个特征(红蓝方各19),1个标签列(blueWins),和一个对局标号(gameId)。对局标号不是标签也不是特征,可以舍去。
此处学习到了drop的用法

drop([ ],axis=0,inplace=True)

drop([]),默认情况下删除某一行;
如果要删除某列,需要axis=1;
参数inplace 默认情况下为False,表示保持原来的数据不变,True 则表示在原来的数据上改变。

import pandas as pd
 
import numpy as np
 
data=pd.DataFrame(np.arange(20).reshape((5,4)),columns=list('ABCD'),index=['a','b','c','d','e'])
print(data)
print('*'*40)
print(data.drop(['a'])) #删除a 行,默认inplace=False,
print('*'*40)
print(data)#  data 没有变化
print('*'*40)
print(data.drop(['A'],axis=1))#删除列
print('*'*40)
print(data.drop(['A'],axis=1,inplace=True)) #在本来的data 上删除
print('*'*40)
print(data)data 发生变化

三,数据概览

对于一个机器学习问题,在拿到任务和数据后,首先需要观察数据的情况,比如我们可以通过.iloc[0]取出数据的第一行并输出。不难看出每个特征都存成了float64浮点数,该对局蓝色方开局10分钟有小优势。同时也可以发现有些特征列是重复冗余的,比如blueGoldDiff表示蓝色队金币优势,redGoldDiff表示红色方金币优势,这两个特征是完全对称的互为相反数。blueCSPerMin是蓝色方每分钟击杀小兵数,它乘10就是10分钟所有小兵击杀数blueTotalMinionsKilled。在之后的特征处理过程中可以考虑去除这些冗余特征。
另外,pandas有非常方便的describe()函数,可以直接通过DataFrame进行调用,可以展示每一列数据的一些统计信息,对数据分布情况有大致了解,比如blueKills蓝色方击杀英雄数在前十分钟的平均数是6.14、方差为2.93,中位数是6,百分之五十以上的对局中该特征在4-8之间,等等。

四,增删特征

传统的机器学习模型大部分都是基于特征的,因此特征工程是机器学习中非常重要的一步。有时构造一个好的特征比改进一个模型带来的提升更大。这里简单展示一些特征处理的例子。首先,上面提到,特征列中有些特征信息是完全冗余的,会给模型带来不必要的计算量,可以去除。其次,相比于红蓝双方击杀、助攻的绝对值,可能双方击杀英雄的差值更能体现出当前对战的局势。因此,我们可以构造红蓝双方对应特征的差值。数据文件中已有的差值是金币差GoldDiff和经验差ExperienceDiff,实际上每个对应特征都可以构造这样的差值特征。

五,特征离散化

决策树ID3算法一般是基于离散特征的,本例中存在很多连续的数值特征,例如队伍金币。直接应用该算法每个值当作一个该特征的一个取值可能造成严重的过拟合,因此需要对特征进行离散化,即将一定范围内的值映射成一个值,例如对用户年龄特征,将0-10映射到0,11-18映射到1,19-25映射到2,25-30映射到3,等等类似,然后在决策树构建时使用映射后的值计算信息增益。
此处涉及到分类多少的问题,需要平衡提高准确率与防止过拟合之间的关系

六,数据集准备

构建机器学习模型前要构建训练和测试的数据集。在本例中首先需要分开标签和特征,标签是不能作为模型的输入特征的,就好比作业和试卷答案不能在做题和考试前就告诉学生。测试一个模型在一个任务上的效果至少需要训练集和测试集,训练集用来训练模型的参数,好比学生做作业获得知识,测试集用来测试模型效果,好比期末考试考察学生学习情况。测试集的样本不应该出现在训练集中,否则会造成模型效果估计偏高,好比考试时出的题如果是作业题中出现过的,会造成考试分数不能准确衡量学生的学习情况,估计值偏高。划分训练集和测试集有多种方法,下面首先介绍的是随机取一部分如20%作测试集,剩下作训练集。sklearn提供了相关工具函数train_test_split。sklearn的输入输出一般为numpy的array矩阵,需要先将pandas的DataFrame取出为numpy的array矩阵。

七,实现决策树

本部分为本次作业主要完成内容,期间遇到过信息增益传值错误,递归结构混乱,数据集特定分支确实等问题。本部分代码写法主要参考b站视频BV1KL4y1B7VB与github开源资源。

结果

经过调整可调的超参数,在不改变已给代码的基本上,预测准确率可达0.71,并在改变随机seed后平均准确率维持在0.7左右

思考

1.在对数据进行离散化时,将数据直接分为两类的方法会使得最后的准确率最高,而更为精细的分类反而会使准确率降低,过拟合问题比我想象中要更加容易出现
2.在预测时存在测试集中出现训练集中没有的特征分类,该问题暂未实现较好解决,在代码中只是简单地用模型已知的一个特征直接覆盖该未知特征分类,对预测准确率毫无疑问造成了影响,后续需想办法改进
3.在离散化采用较多的分类时,为平衡其带来的信息增益,我尝试使用GainRatio来代替信息增益,结果使得预测准确率由0.6775上升至0.6817,但仍不及离散化时采用减少分类的模型

结尾附代码

from collections import Counter
import pandas as pd  # 数据处理
import numpy as np  # 数学运算
from sklearn.model_selection import train_test_split, cross_validate  # 划分数据集函数
from sklearn.metrics import accuracy_score  # 准确率函数
RANDOM_SEED = 2020  # 固定随机种子
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')  # 舍去对局标号列
print(data_df.iloc[0])  # 输出第一行数据
data_df.describe()  # 每列特征的简单统计信息
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可删除

discrete_df = df.copy()  # 先复制一份数据
for c in df.columns[1:]:  # 遍历每一列特征,跳过标签列
    if c == 'blue' + 'EliteMonsters' or c == 'red' + 'EliteMonsters' or c == 'br' + 'EliteMonsters':
        continue
    if c == 'blue' + 'Dragons' or c == 'red' + 'Dragons' or c == 'br' + 'Dragons':
        continue
    if c == 'blue' + 'Heralds' or c == 'red' + 'Heralds' or c == 'br' + 'Heralds':
        continue
    if c == 'blue' + 'TowersDestroyed' or c == 'red' + 'TowersDestroyed' or c == 'br' + 'TowersDestroyed':
        continue
    if c == 'brFirstBlood':
        continue
    discrete_df[c] = pd.qcut(df[c].rank(method='first'), 2, labels=[1, 2])

all_y = discrete_df['blueWins'].values  # 所有标签数据
feature_names = np.array(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'''  # 输出数据行列信息

class DecisionTree(object):
    def __init__(self, classes, features,
                 max_depth=10, min_samples_split=10,
                 impurity_t='entropy'):
        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, feature):
        data_num = len(feature)
        cnt = Counter(feature)
        P_ele = np.array([cnt[lable] / data_num for lable in cnt])
        if self.impurity_t == 'gini':
            return 1-np.sum(np.power(P_ele, 2))
        if self.impurity_t == 'entropy':
            return -np.sum(np.multiply(P_ele, np.log2(P_ele)))

    def gain(self, feature, lable):
        numFeature = len(feature[0])
        sumimpurity = self.impurity(lable)
        bestGain = 0
        bestFeat = -1
        for i in range(numFeature):
            featlist = [example[i] for example in feature]
            uniquevals = set(featlist)
            subimpurity = 0
            for val in uniquevals:
                subfeature, sublable = self.dividefeat(feature, i, lable, val)
                a = self.impurity(sublable)
                subimpurity +=a* len(sublable) / len(feature)
            nowgain = (sumimpurity - subimpurity)
            if nowgain > bestGain:
                bestGain = nowgain
                bestFeat = i
        return bestFeat

    def dividefeat(self, feature, tagfeature, lable, value):
        refeature = []
        relable = []
        features = feature.tolist()
        lables = lable.tolist()
        for i in range(len(feature)):
            feaVec = features[i]
            if feaVec[tagfeature] == value:
                subfeat = feaVec[:tagfeature]
                subfeat.extend(feaVec[tagfeature + 1:])
                refeature.append(subfeat)
                relable.append(lables[i])
        return np.array(refeature),np.array(relable)

    def majorCnt(self, lable):
        Cnt = Counter(lable)
        return Cnt.most_common(1)[0][0]

    def expand_node(self, feature, lable, depth):
        classcount = Counter(lable)
        if len(classcount) == 1:
            return lable[0]
        if len(feature[0]) <= self.min_samples_split:
            return self.majorCnt(lable)
        if depth > self.max_depth:
            return self.majorCnt(lable)
        bestFeat = self.gain(feature, lable)
        featValue = [example[bestFeat] for example in feature]
        uniqueval = set(featValue)
        subDT = {bestFeat:{}}
        for value in uniqueval:
            subfeature,sublable = self.dividefeat(feature, bestFeat, lable, value)
            subDT[bestFeat][value] = self.expand_node(subfeature, sublable, depth=depth + 1)
        return subDT

    def fit(self, feature, label):
        assert len(self.features) == len(feature[0])  # 输入数据的特征数目应该和模型定义时的特征数目相
        self.root = self.expand_node(feature, label, depth=1)

    def traverse_node(self,node,feature):
        featname = [name for name in node.keys()][0]
        sb = [dsb for dsb in node[featname].keys()]
        if feature[featname] not in sb :
            feature[featname] = sb[0]
        if node[featname][feature[featname]] in self.classes :
            return node[featname][feature[featname]]
        return self.traverse_node(node[featname][feature[featname]],feature)

    def predict(self, feature):
        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])  # 如果是很多个样本

DT = DecisionTree(classes=[0, 1], features=feature_names, max_depth=5, min_samples_split=10, impurity_t='gini')
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))  # 输出准确率

你可能感兴趣的:(机器学习实验报告,python,决策树)