决策树学习基本算法
叶节点对应决策结果,其他节点对应一个属性测试,通过属性测试的值将样本划分的子节点中
TreeGenerate(D,A){
生成节点Node;
if D只有一个类别C then
将Node标记为C类叶节点;
return;
end if
if A是空集 或 D在A上取值相同 then
将Node标记为类别最多的叶节点;
return;
end if
从A中选择最优的划分属性a*;
for a*的每一个取值v do
为Node生成一个分支;
Dv表示在a*上取值为v的样本子集;
if Dv是空集 then
将分支节点标记为叶节点,其父节点类别为样本最多的类别;
return;
else
以TreeGenerate(Dv,A\{a*})为分支节点;
end if
end for
}
2.划分选择
一般而言,随着划分过程的不断进行,我们希望决策树的分支节点所包含的样本尽可能地属于同一类别,即节点的“纯度”越来越高。
1.信息增益
信息熵: Ent(D)=−∑nk=1pklog2pk ,其中n为特征维数, pk 为第k类样本所占比例,Ent(D)越小,D的纯度越高。
信息增益: Gain(D,a)=End(D)−∑Vi=1|Dv||D|Ent(Dv) ,其中V为D中属性a的取值数, |⋅| 表示样本集中样本的个数
一般信息增益越大,所获得的纯度越高。
2.增益率
信息增益存在对取值较多的属性有所偏好的问题,使用增益率可减轻这种偏好的不利影响。
Gain_ratio(D,a)=Gain(D,a)Iv(a),Iv(a)=−∑Vi=1|Dv|Dlog2|Dv|D
增益率对取值数目少的属性有所偏好
3.基尼指数
Gini(D)=1−∑nk=11−p2k,Gini_index(D,a)=∑Vi=1|Dv||D|Gini(Dv)
基尼指数越小,纯度越高。
剪枝处理
原因:针对决策树过拟合问题,在决策树学习中,为了尽可能正确的分类训练样本,节点划分过程将不断重复,有时会造成分支过多,决策树太复杂导致过拟合。
预剪枝
在决策树的生成过程中,对每个节点在划分前进行评估,若当前结点的划分不能带来的决策树泛化性能的提升,则停止划分并将当前结点标记为叶节点。
后剪枝
先从训练集中生成一棵完整的决策树,然后自底向上地对非叶节点进行考察,若将该节点对应的子树替换为叶节点能提升泛化性能,则将其标记为叶节点。
一般情况下,后剪枝决策树的欠拟合风险很小,泛化性能往往由于预剪枝决策树。但后剪枝过程是在生成完全决策树后进行的,并且要自底向上地对树中的所有非叶节点进行逐一考察,因此其训练时间开销要大得多。
连续值与缺失值处理
连续值处理
连续属性离散化技术,比如最简单的二分法
给定样本集D和连续属性a,假定a在D上出现了n个不同的取值,将这些值排序为 {a1,a2,…,an} .基于划分点t可将D分为子 D+t 和 D−t 其中 D+t 包含那些在属性a上取值大于t的样本, D−t 则包含那些在属性a上取值小于t的样本。显然,对相邻属性取值 ai 和 ai+1 来说,t在区间 [ai,ai+1) 中取任意值产生的划分结果相同。所以n-1个候选划分点集合为: Ta={ai+ai+12|1≤i≤n−1} 。可以选取最优的划分点作为样本集合的划分,比如 Gain(D,a)=maxt∈TaGain(D,a,t)=maxt∈TaEnt(D)−∑λ∈{+,−}|Dλt||D|Ent(Dλt)
注意:与离散属性不同,若当前节点划分属性是连续属性,该属性还可作为后代节点的划分属性。
缺失值处理
两个问题:如何在属性缺失值的情况下选择划分属性?给定划分属性,若样本在该属性上的值缺失如何对样本划分?
给定训练集D和属性a,令 D~ 表示D中在属性a上没有缺失值的样本子集。假定属性a有V个取值 {a1,a2,⋯,aV} ,令$tilde{D}_v 表示 \tilde{D} 在属性a上取值为 a^v 的样本子集, \tilde{D}^k 表示 \tilde{D} 中属于第k类的样本子集。假定每个样本x被赋予一个权重 w_x ,并定义: ρ=∑x∈D~wx∑x∈Dwx,p~k=∑x∈D~kwx∑x∈D~wx,r~v=∑x∈D~vwx∑x∈D~wx ,直观来看,对于属性a,ρ表示无缺失值样本所占比例,p~k表示无缺失值样本中第k类所占比例, \tilde{r}_v$表示无缺失样本中在属性a上取值为a^v的样本所占比例。
信息增益的公式可推广为:
样本划分:若样本在属性a上的取值已知,则根据取值将其划入相应子节点中,且样本权重不变。若样本在划分属性 a上的取值缺失,则将其同时划入所有的子节点中,且样本权重在与属性值a^v对应的子节点中调整为\tilde{r}_v\cdot w_x。
ID3
特征选择:信息增益
特性:ID3不能保证得到最优解,有可能陷入局部最优;ID3容易过拟合;ID3很难处理连续属性。
#ID3实现
'''
优点:计算复杂度不高,输出结果易于理解,对中间值缺失不敏感,可处理不相关特征数据
缺点:易过拟合;ID3不能处理连续型特征数据
from math import log
def create_dataset():
dataset = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
features = ['no surfacing', 'flippers']#特征标签
return dataset, features
#计算熵
def cal_Ent(dataset):
labels = []
for d in dataset:
labels.append(d[-1])
total = len(set(labels))
label = list(set(labels))
str_labels = "".join(labels)
p = []
for l in label:
p.append(str_labels.count(str(l)))
sum = 0
for k in p:
pk = float(k) / len(dataset)
sum -= pk * log(pk, 2)
return sum
#以特征axis的值value划分数据集
def split_dataset(dataset, axis, value):
sub_dataset = []
for x in dataset:
if x[axis] == value:
t = x[0:axis]
t.extend(x[axis + 1:])
sub_dataset.append(t)
return sub_dataset
#根据信息增益选择最好的划分属性
def choose_best_feature(dataset):
EntD = cal_Ent(dataset)
best_gain = 0.0
n = len(dataset[0]) - 1 # feature num
for i in range(n):
gain_Dv = 0.0
features = [x[i] for x in dataset]
unique_features = list(set(features))
for v in unique_features:
sub_data = split_dataset(dataset, i, v)
pv = len(sub_data) / len(dataset)
gain_Dv += pv * cal_Ent(sub_data)
gain_Dv = EntD-gain_Dv
if gain_Dv > best_gain:
best_gain = gain_Dv
best_fea = i
return best_fea
#投票选择大多数的类别
from collections import Counter
def vote_label(labels):
return Counter(labels).most_common(1)[0][0]
#生成树(递归过程)
def generate_tree(dataset,features):
labels = [x[-1] for x in dataset]
if len(set(labels))==1:
return labels[0]
feature_num = len(dataset[0])-1
if feature_num==0:
return vote_label(labels)
best_fea_axis = choose_best_feature(dataset)
best_fea = features[best_fea_axis]
features.remove(best_fea)
Tree = {best_fea:{}}
best_fea_values = list(set([x[best_fea_axis] for x in dataset]))
for v in best_fea_values:
sub_dataset = split_dataset(dataset,best_fea_axis,v)
sub_features = features[:]
Tree[best_fea][v] = generate_tree(sub_dataset,sub_features)
return Tree
#使用决策树分类
def classify(tree,feature_labels,test_data):
first_key = list(tree.keys())[0]
next_dict = tree[first_key]
key_index = feature_labels.index[first_key]
for key in next_dict.keys():
if test_data[key_index] ==key:
if type(next_dict[key]).__name__ == "dict":
class_label = classify(next_dict[key],feature_labels,test_data)
else:
class_label = next_dict[key]
return class_label
import pickle
#存储树
def store_tree(tree,filename):
fw = open(filename,'wb')
pickle.dump(tree,fw)
fw.close()
#获取树
def get_tree(filename):
fr = open(filename,'rb')
return pickle.load(fr)
myData, features = create_dataset()
myTree = generate_tree(myData,features)
print(myTree)
store_tree(myTree,'tree_storge.txt')
print(get_tree('tree_storge.txt'))
C4.5
特征选择:增益率(先从候选划分属性中选择信息增益高于平均水平的属性,再从中选择增益率最高的属性)
连续值处理:二分法
确实值处理:如上介绍。
相对于ID3的改进:用信息增益率来选择属性,克服了用信息增益选择属性时偏向选择取值多的属性的不足;可处理连续属性和离散属性;可处理缺失值属性;后剪枝优化。
优点:产生的分类规则易于理解,准确率较高。其缺点是:在构造树的过程中,需要对数据集进行多次的顺序扫描和排序,因而导致算法的低效。此外,C4.5只适合于能够驻留于内存的数据集,当训练集大得无法在内存容纳时程序无法运行。
CART
既可用于分类也可用于回归;CART是二叉树
特征选择:基尼指数(分类),均方误差(回归)
分类树生成
选择最优特征和最优切分点:对每一个特征 a(xj) ,对其每一个取值 av ,根据样本点对 xj=av 的测试为“是”或“否”将样本集划分为两个子集 D1,D2 ,计算基尼指数 (|D1||D|Gini(D1)+|D2||D|Gini(D2)) 。选择最小的基尼指数对应的特征及取值作为切分特征和切分点。
回归树生成
启发式方法对输入空间进行划分:选择第j个变量x^j和它的取值s作为切分变量和切分点,并定义两个区域: R1(j,s)={x|xj≤s},R2(j,s)={x|xj>s}, 然后求解 minj,s[minc1∑xi∈R1(j,s)(yi−c1)2+minc2∑xi∈R2(j,s)(yi−c2)2] 寻找最优切分变量和最优切分点,(j,s)划分区域对应的输出值为: c^j=average(yi|xi∈Rj(j,s)),j=1,2。
在训练数据集所在的输入空间中,递归地将每个区域划分为两个子区域并决定每个子区域的输出值,构建二叉决策树。将输入空间划分为M个单元 R1,R2,…,RM ,回归树模型为: f(x)=∑Mm=1cmI(x∈Rm) ( cm 为单元 Rm 对应的输出值,利用均方误差 ∑xi∈Rm(yi−f(xi))2 优化可得最优值 c^m=average(yi|xi∈Rm) )。
#CART回归树生成代码实现
#coding= utf-8
from numpy import *
def load_data(filename):
dataset = genfromtxt(filename,delimiter='\t')
return mat(dataset)
#二元切分法
def bin_split_data(dataset,feature,value):
left_dataset =[]
right_dataset = []
#print(dataset.shape,feature)
for x in dataset.tolist():
if x[feature]>value: right_dataset.append(x)
else: left_dataset.append(x)
return mat(left_dataset), mat(right_dataset)
#选择最优切分变量和最有切分点
def choose_best_split(dataset,leaf_type,error_type,ops):
tolS = ops[0]#容许的误差下降值
tolN = ops[1]#切分的最小样本数
#只有一类,无需再划分
if len(set(dataset[:,-1].T.tolist()[0])) ==1:
return None,leaf_type(dataset)
m,n = shape(dataset)
S = error_type(dataset)
bestS= inf
best_fea = 0
best_fea_val = 0
for fea in range(n-1):
vals = set(dataset[:,fea].reshape(1,m).tolist()[0])
for v in vals:
l_mat,r_mat = bin_split_data(dataset,fea,v)
if ((shape(l_mat)[0]<tolN) or (shape(l_mat)[1]<=0)) or ((shape(r_mat)[0]<tolN) or (shape(r_mat)[1]<=0)): continue
newS = error_type(l_mat)+error_type(r_mat)
if newS <bestS:
bestS = newS
best_fea = fea
best_fea_val = v
#若切分数据集后提升效果不够大,则不进行切分而直接创建叶节点
if (S-bestS)<tolS:
return None,leaf_type(dataset)
l_mat,r_mat = bin_split_data(dataset,best_fea,best_fea_val)
if (shape(l_mat)[0] < tolN) or (shape(r_mat)[0] < tolN): return None,leaf_type(dataset)
return best_fea,best_fea_val
def reg_leaf(dataset):
return mean(dataset[:,-1])
def reg_error(dataset):
return var(dataset[:,-1])*shape(dataset)[0]
def create_tree(dataset,leaf_type=reg_leaf,error_type=reg_error,ops=(1,20)):
'''
params: leaf_type给出建立叶节点的函数;error_type代表误差计算函数;
ops是一个包含树构建所需其他参数的元组
'''
fea,val = choose_best_split(dataset,leaf_type,error_type,ops)
if fea ==None:
return val
tree = {}
tree['split_axis'] = fea
tree['split_val'] =val
left_data,right_data = bin_split_data(dataset,fea,val)
tree['left'] = create_tree(left_data,leaf_type,error_type,ops)
tree['right'] = create_tree(right_data, leaf_type, error_type, ops)
return tree
def is_tree(obj):
return (type(obj).__name__=='dict')
def get_mean(tree):
if is_tree(tree['right']): tree['right'] = get_mean(tree['right'])
if is_tree(tree['left']): tree['left'] = get_mean(tree['left'])
return (tree['left']+tree['right'])/2.0
#剪枝
def prune(tree,testData):
if shape(testData)[0]==0: return get_mean(tree)
if is_tree(tree['right']) or is_tree(tree['left']):
left_data,right_data = bin_split_data(testData,tree['split_axis'],tree['split_val'])
if is_tree(tree['left']): prune(tree['left'],left_data)
if is_tree(tree['right']): prune(tree['right'],right_data)
if not is_tree(tree['right']) and not is_tree(tree['left']):
left_data, right_data = bin_split_data(testData, tree['split_axis'], tree['split_val'])
print(left_data.shape,right_data.shape)
if (shape(left_data)[1] > 0) and (shape(right_data)[1]> 0):
#print(tree,left_data,right_data)
error_no_merge = sum(power(left_data[:,-1]-tree['left'],2))+sum(power(right_data[:,-1]-tree['right'],2))
tree_mean = (tree['left']+tree['right'])/2
error_merge = sum(power(testData[:,-1]-tree_mean,2))
if error_merge<error_no_merge:
print("merging")
return tree_mean
else:
return tree
else:
return tree
def tree_eval(model):
return float(model)
#预测函数
def tree_forecast(tree,in_data,model_eval=tree_eval):
if not is_tree(tree): return model_eval(tree)
if in_data[tree['split_axis']]>tree['split_val']:
if is_tree(tree['left']):
return tree_forecast(tree['left'],in_data,model_eval)
else:
return tree_eval(tree['left'])
else:
if is_tree(tree['right']):
return tree_forecast(tree['right'],in_data,model_eval)
else:
return tree_eval(tree['right'])
def create_forecast(tree,test_data,model_eval=tree_eval):
m = len(test_data)
pre = mat(zeros((m,1)))
for i in range(m):
pre[i] = tree_forecast(tree,test_data[i],model_eval)
return pre
myTrainData = load_data('bikeSpeedVsIq_train.txt')
myTree = create_tree(myTrainData)
myTestData = load_data('bikeSpeedVsIq_test.txt')
pre = create_forecast(myTree,myTestData[:,0])
print(corrcoef(pre,myTestData[:,1],rowvar=1)[0,1])
ID3,C4.5,CART区别
样本数据上的差异:ID3只能对分类变量进行处理,C4.5和CART可以处理连续和分类两种自变量;ID3对缺失值敏感,C4.5和CART对缺失值可以进行多种方式的处理;只从样本量考虑,小样本建议C4.5,大样本CART。
目标因变量的差异:ID3和C4.5只能做分类,CART不仅可以做分类也可以做回归;ID3和C4.5节点上可以产出多叉,而CART节点永远是二叉。
样本特征上的差异:特征变量的使用中,多分的分类变量ID3和C4.5层级之间只能单次使用,CART可多次重复使用。
多变量决策树
决策树所形成的分类边界是由若干个与轴平行的分段组成。
多变量决策树:不是为每个非叶节点寻找一个最优划分属性,而是试图建立一个合适的线性分类器。
决策树优缺点:
优点:
1:决策树易于理解和实现. 人们在通过解释后都有能力去理解决策树所表达的意义
2:对于决策树,数据的准备往往是简单或者是不必要的 其他的技术往往要求先把数据一般化,比如去掉多余的或者空白的属性。
3:决策树算法的时间复杂度(即预测数据)是用于训练决策树的数据点的对数
4:能够同时处理数据型和常规型属性。 其他的技术往往要求数据属性的单一
5:是一个白盒模型如果给定一个观察的模型,那么根据所产生的决策树很容易推出相应的逻辑表达式。相比之下,在一个黑盒子模型(例如人工神经网络),结果可能更难以解释
6:易于通过静态测试来对模型进行评测。
7:在相对短的时间内能够对大型数据源做出可行且效果良好的结果
8:效率高,决策树只需要一次构建,反复使用,每一次预测的最大计算次数不超过决策树的深度。
缺点:
1:决策树算法学习者易创建复杂的树,导致过拟合。为了避免这种问题,出现了剪枝的概念,即设置一个叶子结点所需要的最小数目或者设置树的最大深度。过拟合的原因有以下几点: a).噪音数据:训练数据中存在噪音数据,决策树的某些节点有噪音数据作为分割标准,导致决策树无法代表真实数据;b).缺少代表性数据:训练数据没有包含所有具有代表性的数据,导致某一类数据无法很好的匹配,这一点可以通过观察混淆矩阵(Confusion Matrix)分析得出;c).多重比较(Mulitple Comparition),这一情况和决策树选取分割点类似,需要在每个变量的每一个值中选取一个作为分割的代表,所以选出一个噪音分割标准的概率是很大的。
2:决策树的结果可能是不稳定的,在数据中一个很小的变化可能导致生成一个完全不同的树,这个问题可以通过使用集成决策树来解决。
3:学习一棵最优决策树是NP难的问题。实际决策树学习算法是基于启发式算法,如贪婪算法,寻求在每个节点上的局部最优决策。这样的算法不能保证返回全局最优决策树。
4:有一些概念是决策树不能很轻易的表达它们,比如说异或校验或复用的问题。
5:忽略数据集中属性之间的相关性,在处理特征关联性比较强的数据时表现得不是太好