本文主要讲述了决策树的概念和代码实现,理论部分来自《统计学习方法》,实践部分来自《动手学机器学习》
决策树
组成与性质
- 分类型决策树模型是对实例进行 分类的树形结构。结点分为 内部节点(表示特征或属性),叶节点(类别)
条件概率分布(比较抽象)
- 决策树表示 特征条件下,类的条件概率分布。假设是表示 特征的随机变量,为表示类的集合,该条件概率表示为。实例看后面的例子.
- 上边的例子是二分类,当类别扩大的时候,我们取 条件概率最高的类别,认为x是这个类别
if-then规则
- 可以把决策树看成一个 if-then规则的集合——由 根节点到叶节点的每一条路径 就对应一个规则。路径上的内部节点就是规则的条件(if),而叶节点就是 结论。
- if-then规则是互斥且完备的。互斥体现在树根节点到叶子节点都是互斥的,完备体现在每个类别(叶子)在集合中。
训练过程
- 构建根结点
- 选择 最优特征,以此来分割数据集
- 若子集被正确分类,构建叶结点,否则转2
- 重复2,3,直到所有训练数据子集被正确分类
特征选择
信息增益
- 特征选择在于选取对训练数据具有分类能力,如果一个特征对分类 影响结果不大,说明这个特征没有分类能力。 评价特征好坏的准则是 信息增益/信息增益比。
- 特征对训练数据集的信息增益表示为 ,定义为经验熵 与特征A给定条件下D的经验条件熵之差 —— 。
- 表示的是数据集进行分类的不确定性,而表示在 特征A条件下信息分类的不确定性,二者之差(即信息增益)表示 由于A对 D分类不确定性减少的程度,信息增益越大,分类能力越好。然而使用 信息增益作为划分,存在偏向 取值较多的特征问题,可以使用 信息增益比作为校正.
具体计算
- 首先计算 ,D是指输出分类(**类别 是/否)——上图中的否有6个,是有9个。
- 计算各个 特征的 信息增益,用 四个特征.
- 计算年龄的信息增益 ,年龄有青年(5个),中年(5个),老年(5个),需要分别计算该条件下的 信息熵。
- 青年中,2是3否,故:
- 中年中, 3是2否,故:
- 老年中, 4是1否, 故:
- ,同理,去计算
- 比较可知,的信息增益最大,所以选择 作为最优特征
熵
- 表示 随机变量不确定性的度量。 是一个有限个值的离散随机变量,概率分布是 ,则熵为
- 随机变量的取值 等概率分布的时候,相应的熵最大。。
条件熵
- 当 熵 与 条件熵 中的概率 是通过 数据估计 得到时,则为 经验熵和 经验条件熵。数学表示为
信息增益比
- 在[[#信息增益]]中提到,单纯的增益存在偏向 取值较多的情况,我们要除以一个 训练集D关于特征A的熵之比,称为, ,
- 信息增益比写为,,以(年龄)为例子,
决策树的生成
ID3算法
输入与输出
- 输入:训练集,特征集,阈值.
- 输出:决策树T
算法
- 判断T是否需要选择特征生成决策树(特征集A的大小<=1)
- 若D中的实例都是 同一类,记录实例类别,返回T
- 若实例没有 特征,则T为单节点树,记录D中实例个数最多的类别,返回T
- 否则计算中各个特征的 [[#信息增益]],并且选择 信息增益最大的特征
- 若,则T为单节点树,返回 实例个数最多的类别,然后返回T
- 若,按照 的取值,把划分为若干的非空子集,将中实例个数最多的类别作为 标记,构建 子节点,并且和其他子集 构成一颗树,递归地构建,最后返回T.
- 对于第个结点,以作为训练集,作为特征集合,重复以上步骤
C4.5算法
- 与[[#ID3算法]]唯一不同的是,把 信息增益替换成 信息增益比。
- 能够处理 连续变量的情况,而 ID3算法不行
决策树的实现(代码部分)
- 以下代码手动实现了一颗C4.5的决策树,请注意,这个决策树限制了是 二叉树,并没有实现 多叉树。它的构造是这样的,对于每个特征,选择 为信息增益最大的点,分为了两部分,一部分是小于的部分,另一部分是 大于部分。
class Node:
def __init__(self):
# 如果是内部结点,表示特征编号
# 叶子结点表示分类结果
self.feat = None
# 分类值的列表
self.split = None
# 儿子结点
self.child = []
#%%
class DecisionTree:
def __init__(self,X,Y,feat_ranges,lbd):
self.root = Node()
self.X = X
self.Y = Y
self.feat_ranges = feat_ranges # 特征值的取值集合
self.lbd = lbd # 正则化约束强度
self.eps = 1e-8
self.T = 0 #记录叶子结点的个数
self.ID3(self.root,self.X,self.Y)
def aloga(self,a):
# 自定义的一个log2,防止出现log2(0)的情况
return a*np.log2(self.eps + a)
def entropy(self,Y):
cnt = np.unique(Y,return_counts=True)[1] # 统计每个类别的出现次数
N = len(Y)
# 要返回的熵是 每个类别-1* { log2(cnt_i/N) }
return -1 * np.sum([self.aloga(Ni/N) for Ni in cnt])
# 递归地分裂节点,构造一颗决策树
def info_gain(self,X,Y,feat,val):
# 信息增益等于 H(D) - sum { pi * log2(pi) } # 这里简单地分为两类,一类是 信息熵 <=val ,另一类是 信息熵 >= val # 通过枚举val,保存最佳的一个划分方法
N = len(Y)
if(N==0):
return 0
HX = self.entropy(Y) # 计算Y的信息熵
HXY = 0
Y_l = Y[X[:,feat]<=val]
HXY += len(Y_l) / len(Y) * self.entropy(Y_l)
Y_r = Y[X[:,feat]>val]
HXY += len(Y_r) / len(Y) * self.entropy(Y_r)
return HX - HXY
def entropy_YX(self,X,Y,feat,val):
HXY = 0
N = len(Y)
if N==0:
return 0
# 此处是计算信息增益比的分母部分
# 分母是要求一个H_D(A) = - sum{ log2(cnt_i / N)}
Y_l = Y[X[:,feat]<=val]
HXY += len(Y_l)/ N * -self.aloga(len(Y_l) / N)
Y_r = Y[X[:,feat]>val]
HXY += len(Y_r) / N * -self.aloga(len(Y_r) / N)
return HXY
def info_gain_ratio(self,X,Y,feat,val):
IG = self.info_gain(X,Y,feat,val) # 计算信息增益 Information gain HYX = self.entropy_YX(X,Y,feat,val) # 计算要除的 训练集D 除以 特征A的 熵
return IG / HYX
def ID3(self,node,X,Y):
# 对应步骤1,判断特征集中的数目是否<=1,若是,停止分裂
if len(np.unique(Y)) == 1:
node.feat = Y[0]
self.T +=1
return
best_IGR = 0
best_feat = None
best_val = None
# feat_names是特征的名字集合
# feat_ranges是 对应特征的取值集合
for feat in range(len(feat_names)):
for val in feat_ranges[feat_names[feat]]:
# 找到最优的信息增益比
IGR = self.info_gain_ratio(X,Y,feat,val)
if IGR > best_IGR:
best_IGR = IGR
best_feat = feat
best_val = val
cur_cost = len(Y) * self.entropy(Y) + self.lbd
if best_feat is None:
# 说明最优的信息增益为0,再分类也没办法增加信息了
new_cost = np.inf
else:
new_cost = 0
x_feat = X[:,best_feat]
new_Y_l = Y[x_feat<=best_val]
new_cost += len(new_Y_l) * self.entropy(new_Y_l)
new_Y_R = Y[x_feat>best_val]
new_cost += len(new_Y_R) * self.entropy(new_Y_R)
new_cost += 2 * self.lbd
if new_cost <= cur_cost:
# 如果分裂的代价更小,就执行
node.feat = best_feat
node.split = best_val
l_child = Node()
l_X = X[x_feat<=best_val]
l_Y = Y[x_feat<=best_val]
self.ID3(l_child,l_X,l_Y) # 递归构造左子树
r_child = Node()
r_X = X[x_feat>best_val]
r_Y = Y[x_feat>best_val]
self.ID3(r_child,r_X,r_Y)
node.child = [l_child,r_child]
else:
# 直接返回数目最多的类别
vals,cnt = np.unique(Y,return_counts=True)
node.feat = vals[np.argmax(cnt)]
self.T +=1
def predict(self,X):
node = self.root#从根节点往下找
while node.split is not None:
if X[node.feat] <= node.split:
node = node.child[0] # 往左子树走
else:
node = node.child[1]
# 到达叶结点,返回类别
return node.feat
def accuracy(self,X,Y):
correct = 0
for x,y in zip(X,Y):
pred = self.predict(x)
if pred == y:
correct+=1
return correct/len(Y)
下面拿这份代码跑一份什么泰坦尼克号游客生还的代码(源于书籍《动手学机器学习》)
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
# 读取数据
data = pd.read_csv('titanic/train.csv')
# 查看数据集信息和前5行具体内容,其中NaN代表数据缺失
print(data.info())
print(data[:5])
# 删去编号、姓名、船票编号3列
data.drop(columns=['PassengerId', 'Name', 'Ticket'], inplace=True)
feat_ranges = {}
cont_feat = ['Age', 'Fare'] # 连续特征
bins = 10 # 分类点数
for feat in cont_feat:
# 数据集中存在缺省值nan,需要用np.nanmin和np.nanmax
min_val = np.nanmin(data[feat])
max_val = np.nanmax(data[feat])
feat_ranges[feat] = np.linspace(min_val, max_val, bins).tolist()
print(feat, ':') # 查看分类点
for spt in feat_ranges[feat]:
print(f'{spt:.4f}')
# 只有有限取值的离散特征
cat_feat = ['Sex', 'Pclass', 'SibSp', 'Parch', 'Cabin', 'Embarked']
for feat in cat_feat:
data[feat] = data[feat].astype('category') # 数据格式转为分类格式
print(f'{feat}:{data[feat].cat.categories}') # 查看类别
data[feat] = data[feat].cat.codes.to_list() # 将类别按顺序转换为整数
ranges = list(set(data[feat]))
ranges.sort()
feat_ranges[feat] = ranges
# 将所有缺省值替换为-1
data.fillna(-1, inplace=True)
for feat in feat_ranges.keys():
feat_ranges[feat] = [-1] + feat_ranges[feat]
# 划分训练集与测试集
np.random.seed(0)
feat_names = data.columns[1:]
label_name = data.columns[0]
data = data.reindex(np.random.permutation(data.index))
ratio = 0.8
split = int(ratio * len(data))
train_x = data[:split].drop(columns=['Survived']).to_numpy()
train_y = data['Survived'][:split].to_numpy()
test_x = data[split:].drop(columns=['Survived']).to_numpy()
test_y = data['Survived'][split:].to_numpy()
print('训练集大小:', len(train_x))
print('测试集大小:', len(test_x))
print('特征数:', train_x.shape[1])
#%%
class Node:
def __init__(self):
# 如果是内部结点,表示特征编号
# 叶子结点表示分类结果
self.feat = None
# 分类值的列表
self.split = None
# 儿子结点
self.child = []
#%%
class DecisionTree:
def __init__(self,X,Y,feat_ranges,lbd):
self.root = Node()
self.X = X
self.Y = Y
self.feat_ranges = feat_ranges # 特征值的取值集合
self.lbd = lbd # 正则化约束强度
self.eps = 1e-8
self.T = 0 #记录叶子结点的个数
self.ID3(self.root,self.X,self.Y)
def aloga(self,a):
# 自定义的一个log2,防止出现log2(0)的情况
return a*np.log2(self.eps + a)
def entropy(self,Y):
cnt = np.unique(Y,return_counts=True)[1] # 统计每个类别的出现次数
N = len(Y)
# 要返回的熵是 每个类别-1* { log2(cnt_i/N) }
return -1 * np.sum([self.aloga(Ni/N) for Ni in cnt])
# 递归地分裂节点,构造一颗决策树
def info_gain(self,X,Y,feat,val):
# 信息增益等于 H(D) - sum { pi * log2(pi) } # 这里简单地分为两类,一类是 信息熵 <=val ,另一类是 信息熵 >= val # 通过枚举val,保存最佳的一个划分方法
N = len(Y)
if(N==0):
return 0
HX = self.entropy(Y) # 计算Y的信息熵
HXY = 0
Y_l = Y[X[:,feat]<=val]
HXY += len(Y_l) / len(Y) * self.entropy(Y_l)
Y_r = Y[X[:,feat]>val]
HXY += len(Y_r) / len(Y) * self.entropy(Y_r)
return HX - HXY
def entropy_YX(self,X,Y,feat,val):
HXY = 0
N = len(Y)
if N==0:
return 0
# 此处是计算信息增益比的分母部分
# 分母是要求一个H_D(A) = - sum{ log2(cnt_i / N)}
Y_l = Y[X[:,feat]<=val]
HXY += len(Y_l)/ N * -self.aloga(len(Y_l) / N)
Y_r = Y[X[:,feat]>val]
HXY += len(Y_r) / N * -self.aloga(len(Y_r) / N)
return HXY
def info_gain_ratio(self,X,Y,feat,val):
IG = self.info_gain(X,Y,feat,val) # 计算信息增益 Information gain HYX = self.entropy_YX(X,Y,feat,val) # 计算要除的 训练集D 除以 特征A的 熵
return IG / HYX
def ID3(self,node,X,Y):
# 对应步骤1,判断特征集中的数目是否<=1,若是,停止分裂
if len(np.unique(Y)) == 1:
node.feat = Y[0]
self.T +=1
return
best_IGR = 0
best_feat = None
best_val = None
# feat_names是特征的名字集合
# feat_ranges是 对应特征的取值集合
for feat in range(len(feat_names)):
for val in feat_ranges[feat_names[feat]]:
# 找到最优的信息增益比
IGR = self.info_gain_ratio(X,Y,feat,val)
if IGR > best_IGR:
best_IGR = IGR
best_feat = feat
best_val = val
cur_cost = len(Y) * self.entropy(Y) + self.lbd
if best_feat is None:
# 说明最优的信息增益为0,再分类也没办法增加信息了
new_cost = np.inf
else:
new_cost = 0
x_feat = X[:,best_feat]
new_Y_l = Y[x_feat<=best_val]
new_cost += len(new_Y_l) * self.entropy(new_Y_l)
new_Y_R = Y[x_feat>best_val]
new_cost += len(new_Y_R) * self.entropy(new_Y_R)
new_cost += 2 * self.lbd
if new_cost <= cur_cost:
# 如果分裂的代价更小,就执行
node.feat = best_feat
node.split = best_val
l_child = Node()
l_X = X[x_feat<=best_val]
l_Y = Y[x_feat<=best_val]
self.ID3(l_child,l_X,l_Y) # 递归构造左子树
r_child = Node()
r_X = X[x_feat>best_val]
r_Y = Y[x_feat>best_val]
self.ID3(r_child,r_X,r_Y)
node.child = [l_child,r_child]
else:
# 直接返回数目最多的类别
vals,cnt = np.unique(Y,return_counts=True)
node.feat = vals[np.argmax(cnt)]
self.T +=1
def predict(self,X):
node = self.root#从根节点往下找
while node.split is not None:
if X[node.feat] <= node.split:
node = node.child[0] # 往左子树走
else:
node = node.child[1]
# 到达叶结点,返回类别
return node.feat
def accuracy(self,X,Y):
correct = 0
for x,y in zip(X,Y):
pred = self.predict(x)
if pred == y:
correct+=1
return correct/len(Y)
#%%
DT = DecisionTree(train_x,train_y,feat_ranges,lbd=1.0)
print('叶结点数量: ',DT.T)
print('训练集准确率: ',DT.accuracy(train_x,train_y))
print('测试集准确率: ',DT.accuracy(test_x,test_y))
- 在测试集上表现只有70多,非常感人