本次实验承接上次实验,使用用【ID3】算法实现一棵完整的决策树。(注:如需相关函数代码,请在上一篇文章中获取)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import math
from math import log2
from collections import Counter
一、读入数据集
train_frame = pd.read_csv('train_titanic.csv')
train_frame.head()
二、实现找到数据集D中最佳划分属性
整理上次实验的代码,编写函数,【从属性集A中】寻找使得信息增益最大的属性
输入:数据集D、属性集A
输出:最佳划分的属性(维度)
计算信息增益公式:
某数据集D有若干特征值以及对应的标签值,其总样本大小为|D|,这里取其中一个特征类型feature,该特征包含V个不同的取值,特征值为第v(v=1,2,…,V)个值的数量为|
D
v
D^v
Dv|
(
∑
v
=
1
V
D
v
=
∣
D
∣
)
(\sum_{v=1}^VD^v=|D|)
(∑v=1VDv=∣D∣),则该特征值对应的信息增益为
G
a
i
n
(
D
,
f
e
a
t
u
r
e
)
=
E
n
t
(
D
)
−
∑
v
=
1
K
∣
D
v
∣
D
E
n
t
(
D
v
)
Gain(D,feature)=Ent(D)-\sum_{v=1}^K \frac{|D^v|}{D} Ent(D^v)
Gain(D,feature)=Ent(D)−v=1∑KD∣Dv∣Ent(Dv)
#找到数据集D中的最佳划分属性
def best_split(D, A):
data,dimension= np.shape(D)
label = D[:,dimension-1]
feature = []
#A=【1,1...1】
for i in range(dimension-1):
if A[i] == 1:
feature.append(D[:,i])
f = np.array(feature)
best_entropy, best_dimension = one_split_ID3(f.T,label)
#找到最佳的划分属性
b = best_dimension
for i in range(dimension-1):
if A[i] == 1:
b = b - 1
if b == -1:
best_dimension = i
return best_dimension
#测试函数best_split,并且测试当某一列属性不用时能否实现很好的划分
D = np.array(train_frame)
data,dimension= np.shape(D)
#初始化列表A
A = []
for i in range(dimension-1):
A.append(1)
print(A,best_split(D,A))
#剔除属性0
A[0]=0
print(A,best_split(D,A))
#剔除属性3
A[3]=0
print(A,best_split(D,A))
#剔除属性2
A[2]=0
print(A,best_split(D,A))
三、实现决策树的构建
完成DTree类中的TreeGenerate、train函数以完成决策树的构建。并完成DTree类中的predict函数来用构建好的决策树来对测试数据集进行预测并输出预测准确率。
构建过程主要参照决策树算法,算法伪代码在顶部。
# 树结点类
class Node:
def __init__(self, isLeaf=True, label=-1, index=-1):
self.isLeaf = isLeaf # isLeaf表示该结点是否是叶结点
self.label = label # label表示该叶结点的label(当结点为叶结点时有用)
self.index = index # index表示该分支结点的划分属性的序号(当结点为分支结点时有用)
self.children = {} # children表示该结点的所有孩子结点,dict类型,方便进行决策树的搜索
def addNode(self, val, node):
self.children[val] = node #为当前结点增加一个划分属性的值为val的孩子结点
# 决策树类
class DTree:
def __init__(self):
self.tree_root = None #决策树的根结点
self.possible_value = {} # 用于存储每个属性可能的取值
'''
TreeGenerate函数用于递归构建决策树,伪代码参照课件中的“Algorithm 1 决策树学习基本算法”
'''
def TreeGenerate(self, D, A):
# 生成结点 node
node = Node()
# if D中样本全属于同一类别C then
# 将node标记为C类叶结点并返回
# end if
data,dimension= np.shape(D)
label = D[:,dimension-1]
label_unique = np.unique(label)
if len(label_unique) == 1:
node.isLeaf = True
node.label = label_unique[0]
return node
# if A = Ø OR D中样本在A上取值相同 then
# 将node标记叶结点,其类别标记为D中样本数最多的类并返回
# end if
t1 = 0
t2 = 0
for i in range(len(A)):
if A[i] == 1:
t1 = t1 + 1
feature_unique = np.unique(D[:,i])
if len(feature_unique) != 1:
t2 = t2 + 1
if t1 == 0 or t2 == 0:
node.isLeaf = True
counter = Counter(label)
counter_values = list(counter.values())
counter_label = list(counter)
max = counter_values[0]
node.label = counter_label[0]
for i in range(len(counter_values)):
if counter_values[i] > max:
max = counter_values[i]
node.label = counter_label[i]
return node
# 从A中选择最优划分属性a_star
# (选择信息增益最大的属性,用到上面实现的best_split函数)
best_dimension = best_split(D, A)
# for a_star 的每一个值a_star_v do
# 为node 生成每一个分支;令D_v表示D中在a_star上取值为a_star_v的样本子集
# if D_v 为空 then
# 将分支结点标记为叶结点,其类别标记为D中样本最多的类
# else
# 以TreeGenerate(D_v,A-{a_star}) 为分支结点
# end if
# end for
feature_a_unique = self.possible_value[best_dimension]
for i in range(len(feature_a_unique)):
new_node = Node()
node.index = best_dimension
node.isLeaf = False
Dv = []
for j in range(data):
if D[j,best_dimension] == feature_a_unique[i]:
Dv.append(D[j])
D_v = np.array(Dv)
if len(D_v)==0:
new_node.isLeaf = True
counter = Counter(label)
counter_values = list(counter.values())
counter_label = list(counter)
max = counter_values[0]
new_node.label = counter_label[0]
for i in range(len(counter_values)):
if counter_values[i] > max:
max = counter_values[i]
new_node.label = counter_label[i]
node.addNode(feature_a_unique[i],new_node)
else:
A_new = A[:]#只将A的值赋给A_new
A_new[best_dimension] = 0
node.addNode(feature_a_unique[i],self.TreeGenerate(D_v,A_new))
return node
'''
train函数可以做一些数据预处理(比如Dataframe到numpy矩阵的转换,提取属性集等),并调用TreeGenerate函数来递归地生成决策树
'''
def train(self, D):
# D = np.array(D) # 将Dataframe对象转换为numpy矩阵(也可以不转,自行决定做法)
# A = set(range(D.shape[1] - 1)) # 属性集A
D = np.array(D)
data,dimension= np.shape(D)
A = []
for i in range(dimension-1):
A.append(1)
# #记下每个属性可能的取值
# for every in A:
# self.possible_value[every] = np.unique(D[:, every])
for i in range(len(A)):
self.possible_value[i] = np.unique(D[:, i])
# self.tree_root = self.TreeGenerate(D, A) # 递归地生成决策树,并将决策树的根结点赋值给self.tree_root
self.tree_root = self.TreeGenerate(D, A)
'''
predict函数对测试集D进行预测, 并输出预测准确率(预测正确的个数 / 总数据数量)
'''
def predict(self, D):
# D = np.array(D) # 将Dataframe对象转换为numpy矩阵(也可以不转,自行决定做法)
D = np.array(D)
data,dimension= np.shape(D)
# #对于D中的每一行数据d,从当前结点x=self.tree_root开始,当当前结点x为分支结点时,
# #则搜索x的划分属性为该行数据相应的属性值的孩子结点(即x=x.children[d[x.index]]),不断重复,
# #直至搜索到叶结点,该叶结点的label就是数据d的预测label
pre = []
t = 0
for i in range(data):
d = D[i]
x = self.tree_root
while x.isLeaf == False:
x = x.children[d[x.index]]
pre.append(x.label)
if x.label == d[dimension-1]:
t = t+1
# #输出预测准确率(预测正确的个数 / 总数据数量)
accuracy = t/data
print('预测准确率=',accuracy)
测试:
train_frame = pd.read_csv('train_titanic.csv')
dt = DTree()
# 构建决策树
dt.train(train_frame)
# 利用构建好的决策树对测试数据集进行预测,输出预测准确率(预测正确的个数 / 总数据数量)
test_frame = pd.read_csv('test_titanic.csv')
dt.predict(test_frame)
四、实验反思
①本节基于上一个实验,使用ID3算法,实现了决策树的生成,最终的预测准确率在0.83,很明显还可以进一步的对算法进行优化,可以使用更好的算法或者对决策树进行剪枝处理。
②本节的第一个函数split很好的通过上一节的方法解决,但在构建A时出现了问题,经过思考,我使用了列表中用1、0判别来实现A,终于解决了这一个困难
③第二个困难便是对于决策树的理解,在西瓜书伪代码的实现过程中,我发现最后一个return的位置并不准确,它应该被放到end for后面,开始的时候因为这个位置的错误,给我的代码带来了一定的影响。
④第三个困难是基于我对代码编程的不熟悉造成,比如字典dict的使用不太明白,还有不理解index的作用和possible_value集合的用法,不过在逐步编写的过程中,我终于理解了他们的用法,并赋予他们正确的值,实现了代码。
⑤第四个困难是基于递归算法,递归算法确实比较难于思考,编写class DTree时中途我感到很困难就在宿舍楼下中庭闲逛了一会,在这个过程中我才恍然明白了递归的一些情况,以及如何实现,最终才将递归实现,这也让我意识到有时候困难的解决需要一些灵感而不是死磕。
⑥最后便是Debug过程,还好因为我每一步都比较仔细,以及预实验的充分,没有太多问题,但是在A_new = A时,通过输出A我发现A的值改变了,查阅资料才明白,原来A_new被赋予了A的地址,这个问题来源于我没有分别C语言与python的不同才造成的,经过查阅资料,用A_new = A[:]来赋予值的操作,解决了这样的问题,让最后的预测值更准确。
决策树的理解并不很难,但是实现过程属实给我带来了各种问题,所以实践才能知道困难到底在哪里!