目录
实验前准备
本实验是在Anaconda下的jupyter notebook上进行的,使用的代码语言为python。在开始实验前,我们首先需要导入所需要的库与包或者模块。本实验是一个生成决策树的实验,需要处理大量的实验数据,需要处理多维数组对象,以及可能需要画图进行可视化处理,还有一些数学公式的运用,所以我们需要导入的包为numpy、pandas、math以及matplotlib.pyplot。
代码实现:
from collections import Counter
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import math
任务一——不同量化标准下的结点划分函数
导入数据集
实验要求:
代码实现:
#----your code here
# 导入训练集
df_train = pd.read_csv('train_titanic.csv')
# 将训练数据从dataframe的形式转换成array的形式,方便后续操作
df_train_array=np.array(df_train)
# 导入测试集
df_test=pd.read_csv('test_titanic.csv')
# 将测试集数据从dataframe的形式转换成array的形式,方便后续测试操作
df_test_array=np.array(df_test)
计算标签数组的信息熵
实验要求:
代码实现:
def entropy(label):
# 首先使用Counter类进行遍历,统计出标签数组中有多少个非重复的值k
counter=Counter(label)
k=len(counter)
# 获取标签数组中一共有多少个数据
total_length=len(label)
#通过for循环遍历出每一个值对应的出现次数,然后利用公式进行叠加
temp=0
for value,count in counter.items():
pk=count/total_length
temp+=(pk*math.log2(pk))
# 将累加的结果进行取反即可得到信息熵
ent=-temp
return ent
# 代码测试
l=[0,1]
entropy(l)
将数据集按照指定维度划分为若干个数据集
实验要求:
代码实现:
def split(feature, label, d):
# 创建字典用于存储划分后的子树属性集合和子树标记集合
split_feature = {}
split_label = {}
# 遍历特征集合和标签集合,根据指定维度的特征进行划分
# 通过zip内置函数,可以将特征集合和标签集合按照索引配对组合
for f, l in zip(feature, label):
value = f[d]
# 判断特征向量中这一个值有没有在子树属性集合中加入
# 如果没有,就需要先声明一个空向量
# 如果有,那么直接在该值对应的向量上添加值即可
if value not in split_feature:
split_feature[value] = []
split_label[value] = []
split_feature[value].append(f)
split_label[value].append(l)
return split_feature,split_label
# 测试代码
feature = [[0,0,0],
[0,0,1],
[1,0,2]]
label = [[0], [1],[2]]
d = 0
split(feature,label,d)
按照信息增益(ID3)进行结点划分
实验要求:
代码实现:
def one_split_ID3(x_data, y_label):
# 首先利用entropy函数计算总的不考虑属性的信息熵
Ent_D=entropy(y_label)
# 计算属性列中有多少个属性
feature_nums=len(x_data[0])
# 计算一共有多少个数据
total_nums=len(y_label)
# 创建一个空列表,存放计算得到的所有信息增益
Gain=[]
# 开始循环,计算每一个属性的信息增益
for i in range(feature_nums):
# 利用split函数将数据集按照属性进行划分,得到子树属性集合以及子树标签集合
split_feature,split_label=split(x_data,y_label,i)
# 将子树属性字典与子树特征字典进行绑定遍历
Ent_Dv=0
for feature,label in zip(split_feature.values(),split_label.values()):
# 计算子数据集的信息熵
ent_Dj=entropy(label)
# 加权求和
Ent_Dv+=(len(label)/total_nums)*ent_Dj
# 计算得到每一个属性的信息增益
Gain_i=Ent_D-Ent_Dv
# 将每一个属性的信息增益增加到数组中
Gain.append(Gain_i)
# 获取最大的信息增益值
best_entropy = np.max(Gain)
# 获取最佳的划分维度
best_dimension = np.argmax(Gain)
return best_entropy, best_dimension
# 测试代码
x_data = [[1, 2, 3],
[4, 2, 6],
[1, 5, 3],
[4, 2, 7]]
y_label = [0, 1, 1, 0]
one_split_ID3(x_data,y_label)
按照信息增益率(C4.5)进行结点划分
实验要求:
代码实现:
def one_split_C4_5(x_data, y_label):
# 首先利用entropy函数计算总的不考虑属性的信息熵
Ent_D=entropy(y_label)
# 计算属性列中有多少个属性
feature_nums=len(x_data[0])
# 计算一共有多少个数据
total_nums=len(y_label)
# 创建一个空列表,用于存放计算得到的所有信息增益率
Gain_ratio=[]
# 开始循环,计算每一个属性的信息增益率
for i in range(feature_nums):
# 利用split函数将数据集按照属性进行划分,得到子树属性集合以及子树标签集合
split_feature,split_label=split(x_data,y_label,i)
# 将子树属性字典与子树特征字典进行绑定遍历
Ent_Dv=0
for feature,label in zip(split_feature.values(),split_label.values()):
# 计算子数据集的信息熵
ent_Dj=entropy(label)
# 加权求和
Ent_Dv+=(len(label)/total_nums)*ent_Dj
# 计算得到每一个属性的信息增益
Gain_i=Ent_D-Ent_Dv
# 计算每一个属性本身的信息增益
x_data_array=np.array(x_data)
IV_i=entropy(x_data_array[:,i])
# 利用公式计算每一个属性的信息增益率
Gain_ratio_i=Gain_i/IV_i
# 将每一个属性的信息增益率添加进数组中
Gain_ratio.append(Gain_ratio_i)
# 获取最大的信息增益率
best_entropy = np.max(Gain_ratio)
# 获取最佳的划分维度
best_dimension = np.argmax(Gain_ratio)
return best_entropy, best_dimension
# 测试数据
x_data = [[1, 2, 3],
[4, 2, 6],
[1, 5, 3],
[4, 2, 7]]
y_label = [0, 1, 1, 0]
one_split_C4_5(x_data,y_label)
按照基尼系数(CART)进行结点划分
实验要求:
代码实现:
def one_split_CART(x_data, y_label):
x_data_array=np.array(x_data)
# 首先获取这一个样本中有多少个属性列
feature_nums=len(x_data[0])
# 将基尼系数初始化为无穷大
best_entropy=float('inf')
# 将对应的特征维数初始化为None
best_dimension=None
# 将分类值初始化为None
best_value=None
#然后遍历所有的属性列
for i in range (feature_nums):
# 利用函数split将特征值以及标签值进行划分
split_feature,split_label=split(x_data,y_label,i)
# 利用字典的键值将数据划分为左右子树
for index in split_feature:
# 依次将index加入到左子树
left_index={index}
# 将字典中剩余的index加入到右子树
right_index=set(split_feature.keys())-left_index
# 将index对应的label值分开到左右子树中
left_label=[]
right_label=[]
for left_key in left_index:
left_label.extend(split_label[left_key])
for right_key in right_index:
right_label.extend(split_label[right_key])
# 计算得到左右子树的权重
left_weight=len(left_label)/len(y_label)
right_weight=len(right_label)/len(y_label)
# 首先使用Counter类进行遍历,统计出左右标签子树中有多少个非重复的值k
counter_left=Counter(left_label)
counter_right=Counter(right_label)
k_left=len(counter_left)
k_right=len(counter_right)
# 获取左右标签子树中一共有多少个数据
total_left=len(left_label)
total_right=len(right_label)
#通过for循环遍历出左子树每一个值对应的出现次数,然后利用公式进行叠加
left_pk_2=0
for value,count in counter_left.items():
left_pk_2+=np.power(count/total_left,2)
# 用1减去累加的结果即可得到左子树的结果
Gini_left=1-left_pk_2
#通过for循环遍历出右子树每一个值对应的出现次数,然后利用公式进行叠加
right_pk_2=0
for value,count in counter_right.items():
right_pk_2+=np.power(count/total_right,2)
# 用1减去累加的结果即可得到右子树的结果
Gini_right=1-right_pk_2
# 最后计算基尼系数
Gini_index=left_weight*Gini_left+right_weight*Gini_right
# 如果计算得到的基尼系数比best_entropy还要小,那么更新best_entropy为计算得到的基尼系数
# 同时更新对应的特征维度,以及对应的分类值
if Gini_index<best_entropy:
best_entropy=Gini_index
best_dimension=i
best_value=index
return best_entropy, best_dimension,best_value
# 测试数据
x_data = [[1, 2, 3],
[4, 2, 6],
[1, 5, 3],
[4, 2, 7]]
y_label = [0, 1, 1, 1]
one_split_CART(x_data,y_label)
总结三种结点划分方法
实验要求:
代码实现:
#----your code here
# 首先将特征属性以及标签数据分离开来
x_data=df_train_array[:,:4]
y_data=df_train_array[:,4]
# 使用信息增益进行划分,即使用了函数one_split_ID3
best_entropy,best_dimension=one_split_ID3(x_data,y_data)
print("使用信息增益进行结点的划分,并且输出的最佳特征维度为{:},对应的信息增益值为{:}".format(best_dimension,best_entropy))
# 使用信息增益率进行划分,即使用了函数one_split_C4_5
best_entropy,best_dimension=one_split_C4_5(x_data,y_data)
print("使用了信息增益率进行结点的划分,并且输出的最佳特征维度为{:},对应的信息增益率为{:}".format(best_dimension,best_entropy))
# 使用基尼系数进行结点的划分,即使用了函数one_split_CART
best_entropy,best_dimension,best_value=one_split_CART(x_data,y_data)
print("使用了基尼系数进行结点的划分,并且输出的最佳特征维度为{:},对应的基尼系数为{:},对应的分类值为{:}"
.format(best_dimension,best_entropy,best_value))
实验结果分析:如图所示,我们综合以上的三种方法,分别对训练数据进行一次的结点划分。我们先后使用了使用信息增益方法(ID3)、信息增益率方法(C4_5)以及基尼系数方法(CART)进行结点的划分。从结果中,我们可以发现,这三种方法所得出的最佳特征维度都是0。对应的信息增益值为0.10750711887455178;对应的信息增益率为0.11339165967945304;对应的基尼系数为0.2964915724641573,而且对应的分类值为0。所以综合以上的三种方法,我们可以知道我们前面所编写的方法函数的代码应该是正确无误的。
任务二——使用ID3算法实现一棵完整的决策树
从剩余特征集中查找信息增益最大的特征
实验要求:
代码实现:
# 计算指定维度的信息增益
def Calculate_Dimension_Gain(D, dimension):
D_array=np.array(D)
# 提取标签列
labels = D_array[:, -1]
# 提取特征列的值
values = D_array[:, dimension]
# 获取特征列的唯一值
unique_values = np.unique(values)
# 计算数据集D的信息熵
entropy_D = entropy(labels)
# 初始化定义加权平均的信息熵
weighted_entropy = 0
for value in unique_values:
# 提取特征值为该值的子集
D_v = D_array[values == value]
# 计算子集的熵
entropy_D_v = entropy(D_v[:, -1])
# 累加求和
weighted_entropy += (len(D_v) / len(D)) * entropy_D_v
# 计算得到该属性的信息增益
Gain = entropy_D - weighted_entropy
return Gain
# 获取剩下的维度中最佳的划分维度
def best_split(D, A):
# 首先初始化信息增益为负无穷
best_gain = -float('inf')
# 初始化最佳的划分维度为None
best_dimension = None
# 遍历在剩下的维度中的所有维度
for dimension in A:
# 调用前面的函数计算在该维度下的信息增益值
gain = Calculate_Dimension_Gain(D, dimension)
# 如果信息增益值大于最佳的信息增益best_gain,
# 就更新best_gain为当前的信息增益值
# 同时更新最佳的划分维度
if gain > best_gain:
best_gain = gain
best_dimension = dimension
return best_dimension
# 测试用例
D = [[1, 2, 0, 0, 0],
[4, 2, 1, 0, 0],
[1, 5, 0, 1, 1],
[4, 2, 1, 1, 0],
[1, 2, 0, 1, 1]]
A = [0,1,2,3,4]
best_split(D,A)
完成决策树的构建
实验要求:
代码实现:
# 树结点类
class Node:
def __init__(self, isLeaf=True, label=-1, feature_index=-1):
self.isLeaf = isLeaf # isLeaf表示该结点是否是叶结点
self.label = label # label表示该叶结点的label(当结点为叶结点时有用)
self.feature_index = feature_index # feature_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
# 判断D中样本的最后一列,即判断D中的标签值是否只有一个值,即判断D中样本是否属于同一类别
unique_labels,counts=np.unique(D[:,-1],return_counts=True)
if len(unique_labels)==1:
# 将结点的是否为叶结点设为True
node.isLeaf=True
# 将结点的label设为label中唯一的值
node.label=unique_labels[0]
return node
# if A = Ø OR D中样本在A上取值相同 then
# 将node标记叶结点,其类别标记为D中样本数最多的类并返回
# end if
if len(A)==0 or np.all(D[:,:-1]==D[0,:-1]):
node.isLeaf=True
node.label=unique_labels[np.argmax(counts)]
return node
# 从A中选择最优划分特征a_star
# (选择信息增益最大的特征,用到上面实现的best_split函数)
a_star=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
# 首先将结点的IsLeaf设为False,因为下面是处理该节点不为叶子结点的情况
node.isLeaf=False
# 将该结点的feature_index设为上面求得的最佳划分特征
node.feature_index=a_star
# 计算该划分特征下的特征值有多少个唯一值
unique_values=np.unique(D[:,a_star])
# 遍历唯一值
for value in unique_values:
# 将等于该唯一值的数据集中的数据划分为一个单独的子集
D_v=D[D[:,a_star]==value]
if len(D_v)==0:
child_node=Node(isLeaf=True,label=unique_labels[np.argmax(counts)])
else:
child_node=self.TreeGenerate(D_v,A-{a_star})
# 创建结点
node.addNode(value,child_node)
return node
'''
train函数可以做一些数据预处理(比如Dataframe到numpy矩阵的转换,提取属性集等),并调用TreeGenerate函数来递归地生成决策树
'''
def train(self, D):
# 将Dataframe对象转换为numpy矩阵
# D = np.array(D)
# 特征集A
A = set(range(D.shape[1] - 1))
#记下每个特征可能的取值
for every in A:
self.possible_value[every] = np.unique(D[:, every])
# 递归地生成决策树,并将决策树的根结点赋值给self.tree_root
self.tree_root = self.TreeGenerate(D, A)
return
'''
predict函数对测试集D进行预测, 并输出预测准确率(预测正确的个数 / 总数据数量)
'''
def predict(self, D):
# D = np.array(D) # 将Dataframe对象转换为numpy矩阵(也可以不转,自行决定做法)
# #对于D中的每一行数据d,从当前结点x=self.tree_root开始,当当前结点x为分支结点时,
# #则搜索x的划分特征为该行数据相应的特征值的孩子结点(即x=x.children[d[x.index]]),不断重复,
# #直至搜索到叶结点,该叶结点的标签就是数据d的预测标签
# 初始化一个acc_num用于记录预测正确的结点数量
acc_num = 0
# 遍历数据中的每一行样本数据
for row in D:
# 首先初始化为决策树的根节点
current_node = self.tree_root
# 判断当前结点是否为叶子结点,如果不是叶子结点,我们就获取其对应的特征值
while not current_node.isLeaf:
feature_value = row[current_node.feature_index]
# 如果该特征值存在于当前结点中,就继续迭代,如果不存在就跳出循环,因为无法往下预测
if feature_value in current_node.children:
current_node = current_node.children[feature_value]
else:
break
# 如果当前结点为叶子结点,我们就检查叶子结点的isLeaf以及label是否与当前的数据一致
if current_node.isLeaf and current_node.label == row[-1]:
acc_num += 1
# 计算出预测正确率
acc = acc_num / len(D)
return acc
决策树对测试集的预测
实验要求:
根据上面的DTree类中的结点以及函数利用训练数据创建一个决策树,并且利用构建好的决策树对测试集进行预测准确率的判断。
代码实现:
#----your code here
# 构建决策树
dtree = DTree()
dtree.train(df_train_array)
# 利用构建好的决策树对测试数据集进行预测
accuracy = dtree.predict(df_test_array)
# 输出预测准确率(预测正确的个数 / 总数据数量)
print("Accuracy:", accuracy)
实验结果分析:如图所示,我们利用前面写好的函数,首先利用DTree创建一个树的对象,然后通过函数train来构建一个决策树,主要是利用前面导进的训练集来进行构建。最后我们再调用前面我们编写好的predict函数在测试集上来判断预测准确率为多少,并且将该准确率打印出来。从实验结果中我们可以知道,我们的预测准确率为0.8366336633663366,可以得知我们构建的决策树在预测测试集的准确率上还是比较高的,所以我们的决策树构建的是比较正确的,那么可以得知我们前面编写的代码应该也是比较正确的。
展示生成的决策树结构
实验要求:
我们需要将在前面构建好的决策树的具体形式,或者说层级结构展示出来,我们主要利用老师编写好的代码函数进行展示,我们主要调用该函数,并且将前面我们创建好的树对象传进来即可。
代码实现:
#展示生成的决策树结构
def display_tree(node, indent=''):
if node.isLeaf:
print(indent + "Leaf Node: label =", node.label)
else:
print(indent + "Branch Node: feature_index =", node.feature_index)
for value, child in node.children.items():
print(indent + "|--> Child Value:", value)
display_tree(child, indent + " ")
display_tree(dtree.tree_root)
实验结果分析:如图所示,我们可以看到我们构建的决策树的部分结构如上,首先最上面的是一个根节点,然后就到了第一个子节点,然后子节点下面还有很多的子节点,先遍历完了第一个子节点后再去遍历下一个子节点,所以我们可以知道这一个展示决策树的具体方式是深度优先的遍历顺序的。
实验总结
信息熵:信息熵是信息论中用于衡量随机变量不确定性或混乱程度的概念。而在决策树算法中,信息熵用于衡量标签集合的不确定性,以便选择最佳的划分特征。在信息熵的计算中,我们需要计算每个标签的概率。信息熵是一个随机变量不确定性的度量。对于一个标签集合,其信息熵表示为每个标签的概率乘以对应的信息量的总和取负值。而在信息熵中,信息量衡量了某个事件发生所提供的信息量大小。信息量与概率有关,当一个事件的概率越小,其信息量越大。常用的信息量度量方式是以2为底的对数函数。信息熵主要可以作为选择最佳划分特征的依据之一。通过计算不同特征的信息熵,可以选择具有最大信息增益或最大信息增益率的特征作为决策树节点的划分依据。同时信息熵可以在决策树算法中用于确定最佳划分特征,以及衡量决策树节点的纯度。通过最小化信息熵,可以构建出具有较高纯度的决策树。还有就是在决策树的预测过程中,利用决策树模型对新样本进行分类时,通过比较样本的特征值与决策树节点的划分值,根据特征的取值选择相应的分支方向,直到到达叶节点,然后将叶节点的标签作为预测结果。
当构建决策树时,可以使用三种常见的量化标准来选择最佳的划分特征:信息增益(Information Gain)、信息增益率(Gain Ratio)和基尼系数(Gini Index)。
1. 信息增益(Information Gain):
基本概念:信息增益是根据特征对标签集合的整体不确定性减少程度来选择划分特征。它衡量了使用特征划分数据集后,标签集合的不确定性减少的程度。
特征选择:计算每个特征的信息增益,选择具有最大信息增益的特征作为划分依据。
适用情况:信息增益适用于各种类型的特征和标签,但在特征取值较多的情况下,可能会偏向选择取值较多的特征。
优点:简单直观,易于实现和理解。在处理多类别问题时表现良好。
缺点:对具有大量取值的特征有偏好,忽略了特征取值的分布情况。
2. 信息增益率(Gain Ratio):
基本概念:信息增益率是在信息增益的基础上考虑特征的取值数目来纠正偏好。它解决了信息增益对取值较多特征的偏好问题。
特征选择:计算每个特征的信息增益率,选择具有最大信息增益率的特征作为划分依据。
适用情况:信息增益率适用于各种类型的特征和标签,尤其适用于处理具有大量取值的特征。
优点:纠正了信息增益对取值较多特征的偏好,能够更公平地选择特征。
缺点:对于取值较少的特征,信息增益率的值偏低,可能导致优势较大的特征被忽略。
3. 基尼系数(Gini Index):
基本概念:基尼系数是用于衡量决策树节点纯度的指标。它表示从一个数据集中随机抽取两个样本,其标签不一致的概率。基尼系数越小,节点的纯度越高。
特征选择:计算每个特征的基尼系数,选择具有最小基尼系数的特征作为划分依据。
适用情况:基尼系数适用于各种类型的特征和标签,尤其适用于处理二分类问题。
优点:计算简单,相对于信息增益和信息增益率,基尼系数在计算上更高效。
缺点:对于多分类问题,基尼系数相对于信息增益和信息增益率的表现可能较差。
这些量化标准在决策树构建中用于选择最佳划分特征,但具体选择哪种标准取决于数据集的特性和问题的要求。总体来说,信息增益适用于一般情况下,信息增益率适用于处理具有大量取值特征的情况,而基尼系数适用于处理二分类问题。我们可以根据具体情况,去选择适合的标准来构建决策树模型。
ID3决策树算法的具体步骤:
1. 数据准备:收集带有标签的训练数据集,每个训练样本都包含一组特征和对应的标签。确保数据集的特征是可测量的,并且标签是已知的。
2. 特征选择:特征选择是决策树算法中的关键步骤。在这一步,通过选择一个划分标准来确定最佳特征。常用的划分标准包括信息增益、信息增益率和基尼系数。信息增益衡量了使用特征划分数据集后,标签集合的不确定性减少的程度。选择具有最大信息增益、最高信息增益率或最低基尼系数的特征作为当前节点的划分特征。
3. 划分数据集:根据选择的划分特征,将数据集划分为多个子集。对于离散特征,每个取值将创建一个子节点;对于连续特征,选择一个划分点将数据分为两个子节点。
4. 递归构建决策树:对于每个子节点,递归地执行上述步骤,直到满足停止条件。停止条件可以是达到预定的树的深度、子集中的样本数小于某个阈值,或者子集中的样本都属于同一类别。
5. 生成叶节点:当满足停止条件时,生成叶节点并将样本中最常见的类别作为叶节点的类别标签。叶节点表示决策树的最终输出。
6. 剪枝处理(可选):为了避免过拟合,可以对生成的决策树进行剪枝处理。剪枝是通过去掉一些节点或子树来降低模型复杂度。剪枝方法包括预剪枝和后剪枝。预剪枝是在构建过程中进行剪枝,通过设定停止条件来提前停止分支的生长。后剪枝是在生成完整的决策树后进行剪枝,通过比较剪枝前后的模型性能来决定是否剪枝。
7. 预测:利用生成的决策树对新样本进行分类或回归预测。将新样本从根节点开始沿着决策树的分支进行导航,直到达到叶节点,然后将叶节点的类别或预测值作为最终的预测结果。
总体来说,ID3决策树算法易于理解和解释、可以处理离散和连续特征、能够处理多分类问题等。然而,决策树算法也存在一些问题,如容易过拟合、对噪声敏感等。为了解决这些问题,可以采用集成学习方法如随机森林或梯度提升树,或者使用剪枝等技术来改进决策树模型。