常用的沟通工具,比如手机,QQ是和已经认识的朋友交流的工具。现在出现了一大批自诩的约炮神器,非诚勿扰挺好看的,宅男腐女的问题操碎了爸妈的心,世纪佳缘都上市了,看来大家对想认识不认识的人有巨大的热情。我也听说非诚勿扰牵手成功后最后走到一起的并不多,这些方式到底靠不靠谱?如果我去参加非诚勿扰,我会不会牵手成功?这里面是不是有一些规律可循,趁着今天被毁容不能出门,我就来学习一下决策树。
高中老师隔三差五让我们填一些表,各种性格和IQ测验。这些测验里很多是十几道题目,每道题目选择“是”或者“否”,这就是决策树的基本原型了。十几道题目相当于十几种要考察的因素,每道题目的选择就是决策树的一个决策,如果题目一选择了“是”,那就跳到题目2,如果选择“否”,那就跳到题目3,决策树也就是这个样子,这个因素的判断调试是否成立,成立了就走成立的分支,不成立就走不成立的分支,这样下来,决策树就像是以树状形式排列的一系列if-else语句。
if (因素A成立): if (因素B1成立): ... if (因素N11成立): 你可以参加非诚勿扰 else: 你不可以参加非诚勿扰 else: ... if (因素N12成立): 你可以参加非诚勿扰 else: 你不可以参加非诚勿扰 else: if (因素B2成立): ... if (因素N21成立): 你可以参加非诚勿扰 else: 你不可以参加非诚勿扰 else: ... if (因素N22成立): 你可以参加非诚勿扰 else: 你不可以参加非诚勿扰
我们的目的就是根据已知的数据,构建出这棵树。
概念理清楚了,我来一点一点实现这个算法。
my_data=[['beijing','yes','yes',28,'yes'], ['hebei','no','no',23,'no'], ['henan','yes','no',24,'yes'], ['hubei','no','yes',28,'no'], ['hunan','yes','yes',39,'no']]
上面是我捏造的的数据,下面根据这五条数据来构建决策树。用户A,北京人,有房有车,28岁,牵手成功;用户B,河北人,没房没车,23岁,牵手失败;用户C,河南人,有房没车,24岁,牵手成功;用户D,湖北人,没房有车,23岁,牵手失败;用户E,湖南人,有车有房,39岁,牵手失败。
第一步:定义节点结构;第二步:对输入数据分组;第三步:选择拆分数据的评价模型;第四步:构造决策树;第五步:用决策树进行判断;第六步:剪枝;第七步:数据缺失处理。
第一步:定义节点结构:
每个节点必须表明自己代表的那个考察因素,一个成立或者不成立的判断条件,一个指向条件成立后的要执行下一个节点,一个指向条件不成立后要判断的下一个节点。如果是末级叶子节点,还需要一个结果节点,来指明要不要参加非诚勿扰。
class decisionnode: def __init__(self,col=-1,value=None,results=None,tb=None,fb=None): self.col=col self.value=value self.results=results self.tb=tb self.fb=fb
其中,col代表的要考察的因素是输入数据的那一列;valus就是判断条件;results是叶子节点保存最后判断结果的变量;tb,fb分别是条件value成立指向的分支和条件value不成立指向的分支。
第二步:分组输入数据:
我们考虑因素有四个,祖籍,房,车和年龄。祖籍是字符型,年龄是整数型,是否有车有房是布尔型;假设我们根据是否有房分组,可以将用户分为[A,C,E]和 [B,D],如果根据是否有车分组,可以将用户分为[A,D,E]和 [B,C];如果根据年龄是否大于等于39分组,可分为 [E]和[A,B,C,D];根据年龄是否大于等于28分组,可分为[A,D,E]和 [B,C]; 根据是否是北京人,分为[A]和[B,C,D,E]...
def divideset(rows,column,value): split_function=None if isinstance(value,int) or isinstance(value,float): split_function=lambda row:row[column]>=value else: split_function=lambda row:row[column]==value # Divide the rows into two sets and return them set1=[row for row in rows if split_function(row)] set2=[row for row in rows if not split_function(row)] return (set1,set2)
divideset函数,根据第column列的值是否等于value,或者大于等于value来对输入rows数据,结果分为两组,一组符合条件,一组不符合条件
第三步:拆分数据的评价模型:
第二步对数据根据因素的值来分组,每一步根据哪个因素的哪个取值来分组比较好呢?是是否有房还是年龄不能太大呢?有没有可判断的依据?有!我们要做的,就是找出合适的因素,使根据该因素divideset后生成的两组数据集的混杂程度尽可能的小。
先定义个一个小工具函数,这个函数返回rows里各种可能结果和结果的出现的次数;
def uniquecounts(rows): results={} for row in rows: # The result is the last column r=row[len(row)-1] if r not in results: results[r]=0 results[r]+=1 return results
基尼不纯度:将来自集合中某种结果随机应用于集合中某一数据项的预期误差率:
def giniimpurity(rows): total=len(rows) counts=uniquecounts(rows) imp=0 for k1 in counts: p1=float(counts[k1])/total for k2 in counts: if k1==k2: continue p2=float(counts[k2])/total imp+=p1*p2 return imp
该函数利用集合中每一项结果出现的次数除以集合的总行数来计算相应的概率,然后将所有这些概率值的乘积累加起来。这样就得到某一行数据被随机分到错误结果的总概率。这一概率的值越高,就说明对数据的拆分越不理想。
熵:代表集合的无序程度,也就是集合的混杂程度:
def entropy(rows): from math import log log2=lambda x:log(x)/log(2) results=uniquecounts(rows) # Now calculate the entropy ent=0.0 for r in results.keys(): p=float(results[r])/len(rows) ent=ent-p*log2(p) return ent
这是衡量结果之间差异程度的测度方法,如果所有结果都相同,比如按照某种方式分组后的用户都有车,那么熵就是0。群组越是混乱,熵就越高。
第四步:构建决策树:
构建决策树的过程,就是找出一个因素,根据这个因素对数据分组,使得分组后数据的熵比根据其他因素分组后的熵都要低。
def buildtree(rows,scoref=entropy): if len(rows)==0: return decisionnode() current_score=scoref(rows) # Set up some variables to track the best criteria best_gain=0.0 best_criteria=None best_sets=None column_count=len(rows[0])-1 for col in range(0,column_count): # Generate the list of different values in # this column column_values={} for row in rows: column_values[row[col]]=1 # Now try dividing the rows up for each value # in this column for value in column_values.keys(): (set1,set2)=divideset(rows,col,value) # Information gain p=float(len(set1))/len(rows) gain=current_score-p*scoref(set1)-(1-p)*scoref(set2) if gain>best_gain and len(set1)>0 and len(set2)>0: best_gain=gain best_criteria=(col,value) best_sets=(set1,set2)
# Create the sub branches if best_gain>0: trueBranch=buildtree(best_sets[0]) falseBranch=buildtree(best_sets[1]) return decisionnode(col=best_criteria[0],value=best_criteria[1], tb=trueBranch,fb=falseBranch) else: return decisionnode(results=uniquecounts(rows))
上面是通过对当前数据集选择最合适的拆分条件递归地实现决策树的构建。函数接受一个由数据构成的列表作为参数。它遍历数据集中的每一列(最后一列除外,最后一列用来存放最终结果),针对各列查找每一种可能的值,并将数据集拆分为两个子集。通过将每个子集的熵乘以子集中所含数据项在原数据集中所占的比重,函数求出了每一对新子集的加权平均熵,并记录下熵值最低的那一对子集。如果由熵最低的一对子集求得的加权平均熵比当前集合的熵要大,则拆分过程结束,针对各种可能的结果的计数所得将会被保存起来。否则,算法就会在新生成的子集上继续调用buildtree函数,并把调用结果添加到树上。我们把针对每个子集的调用结果分别附加到节点的True分支和False分支上,最终整棵树就构造出来啦。。。
第五步:对新数据进行判定:
我们根据已有数据构建好了决策树,下面就可以对新数据进行判决了:
def classify(observation,tree): if tree.results!=None: return tree.results else: v=observation[tree.col] branch=None if isinstance(v,int) or isinstance(v,float): if v>=tree.value: branch=tree.tb else: branch=tree.fb else: if v==tree.value: branch=tree.tb else: branch=tree.fb return classify(observation,branch)
classfiy以buildtree返回的结果为参数,沿着树向下遍历。每次调用后,函数会根据调用结果来判断是否达到分支的末端。如果尚未达到末端,他会对观测数据做出评估,以确认列数据是否与参考值匹配。如果匹配,则会再次在True分支上调用classify,否则会在false分支上调用classify函数。当函数到达一个包含结果信息的节点时,就会对观测数据给出结果了。
第六步:剪枝
使用第四步的方法构造决策树可能会过度拟合,也就是说,构建的决策树对过于太针对我们的训练数据。专门针对训练数据创建出来的分支,熵值与真实情况相比会有所降低。算法直到无法再降低熵的时候才会停止分支的创建过程,所以一种可能的办法是,当熵减少的数量少于某个最小值时,我们就停止分支的创建。这种方法也有一个小小的缺陷,可能某次分支的创建并不会降低很多的熵,但是随后的创建分支会大幅度降低熵。
我们使用的方法是,先构建好整棵树,然后尝试消除多余的节点,这就是剪枝。剪枝的过程就是对具有相同父节点的一组节点进行检查,判断如果将其合并,熵的增加量是否会小于某个阈值。如果确实如此,则这些叶子节点被合并成一个单一的节点。
def prune(tree,mingain): # If the branches aren't leaves, then prune them if tree.tb.results==None: prune(tree.tb,mingain) if tree.fb.results==None: prune(tree.fb,mingain) # If both the subbranches are now leaves, see if they # should merged if tree.tb.results!=None and tree.fb.results!=None: # Build a combined dataset tb,fb=[],[] for v,c in tree.tb.results.items(): tb+=[[v]]*c for v,c in tree.fb.results.items(): fb+=[[v]]*c # Test the reduction in entropy delta=entropy(tb+fb)-(entropy(tb)+entropy(fb)/2) if delta<mingain: # Merge the branches tree.tb,tree.fb=None,None tree.results=uniquecounts(tb+fb)
第七步:数据缺失处理
我们的训练数据只有五个省的用户,如果判断一个祖籍是安徽的用户呢?安徽不在我们的训练集类,决策树也就不会有安慰的信息。如果我们缺失了一些信息,而这个信息是确定分支走向所必须的,那我们可以选择两个分支都走。不过不是平均地统计各分支对应的结果值,而是对其加权统计。
def mdclassify(observation,tree): if tree.results!=None: return tree.results else: v=observation[tree.col] if v==None: tr,fr=mdclassify(observation,tree.tb),mdclassify(observation,tree.fb) tcount=sum(tr.values()) fcount=sum(fr.values()) tw=float(tcount)/(tcount+fcount) fw=float(fcount)/(tcount+fcount) result={} for k,v in tr.items(): result[k]=v*tw for k,v in fr.items(): result[k]=v*fw return result else: if isinstance(v,int) or isinstance(v,float): if v>=tree.value: branch=tree.tb else: branch=tree.fb else: if v==tree.value: branch=tree.tb else: branch=tree.fb return mdclassify(observation,branch)
好了,这个决策树的部分就到这里结束了。
看看我实验的结果
>>> tree=treepredict.buildtree(treepredict.my_data) >>> treepredict.mdclassify(['beijing', 'yes', 'yes', 22], tree) {'yes': 2} >>> treepredict.mdclassify(['beijing', 'no', 'no', 22], tree) {'no': 2} >>> treepredict.mdclassify(['beijing', 'no', 'no', 30], tree) {'no': 2} >>> treepredict.mdclassify(['beijing', 'yes', 'no', 30], tree) {'yes': 2} >>> treepredict.mdclassify(['anwei', 'yes', 'no', 30], tree) {'yes': 2} >>> treepredict.mdclassify(['anwei', 'no', 'yes', 30], tree) {'no': 2} >>> treepredict.mdclassify(['beijing', 'no', 'yes', 30], tree) {'no': 2} >>> treepredict.mdclassify(['beijing', 'no', 'yes', 20], tree) {'no': 2} >>> treepredict.mdclassify(['beijing', 'yes', 'no', 40], tree)
将构建的决策树打印出来是这样的
1:yes? T-> 0:hunan? T-> {'no': 1} F-> {'yes': 2} F-> {'no': 2}
解释一下就是这样的:
是否有房: 有: 是否武汉人: 是:不要参加非诚勿扰 不是:参加非诚勿扰 无: 不要去参加分成勿扰