决策树实践

import math
import copy
import uuid
import pickle
from collections import defaultdict, namedtuple

信息量

I = -log(P(x))

信息熵

信息量的期望值

H = -ΣP(x)log(P(x))

def get_shanno_entorpy(values):
    '''
    根据给定列表中的值计算其Shanno Entropy
    '''
    uniq_vals = set(values)
    vals_num = {key: values.count(key) for key in uniq_vals}
    probs = [v/len(values) for k, v in vals_num.items()]
    entorpy = sum([-prob*math.log2(prob) for prob in probs])
    return entorpy

values = [1,2,3,4,5,1,2,1,3,4,2,3,1,5,3]
get_shanno_entorpy(values)
2.256564762130954

信息增益

将数据集划分,划分后的数据集的信息熵是每个子集的信息熵的平均期望值
HS = ΣP(D)H(D) = Σlen(D)/len(S)* H(D)

信息增益为 Gain = H - HS

ID3算法使用的就是基于信息增益的选择属性方法

def choose_best_split_feature1(dataset, classes):
    ''' 根据信息增益确定最好的划分数据的特征
    :param dataset: 待划分的数据集
    :param classes: 数据集对应的类型
    :return: 划分数据的增益最大的属性索引
    '''
    #注意:划分的数据集是classes(label)
    base_entorpy = get_shanno_entorpy(classes)
    feat_num = len(dataset[0])
    entorpy_gains = []
    for i in range(feat_num):
        splited_dict = split_dataset(dataset, classes, i)
        new_entorpy = sum([len(subclasses)/len(classes) * get_shanno_entorpy(subclasses) 
                           for _, (_, subclasses) in splited_dict.items()])
        entorpy_gains.append(base_entorpy - new_entorpy)
    return entorpy_gains.index(max(entorpy_gains))

#另一种无需计算H的实现
#因为H固定,所以最大化Gain,即最小化HS
def choose_best_split_feature2(dataset, classes):
    ''' 根据信息增益确定最好的划分数据的特征
    :param dataset: 待划分的数据集
    :param classes: 数据集对应的类型
    :return: 划分数据的增益最大的属性索引
    '''
    #注意:划分的数据集是classes(label)
    feat_num = len(dataset[0])
    entorpy_gains = []
    for i in range(feat_num):
        splited_dict = split_dataset(dataset, classes, i)
        new_entorpy = sum([len(subclasses)/len(classes) * get_shanno_entorpy(subclasses) 
                           for _, (_, subclasses) in splited_dict.items()])
        entorpy_gains.append(new_entorpy)
    return entorpy_gains.index(min(entorpy_gains))

增益比率

解决信息增益方法可能带来的无意义的分类,比如每个人有一个唯一的姓名,按姓名的feature分每类是最纯的,只有一个,这种分类无意义(无泛化能力,类似过拟合)。
方法:
引入一个分裂信息,分裂的越多,分裂信息会越大:SplitInfo = -Σlen(D)/len(S)log(len(D)/len(S))

增益比率定义为:GainRadio = Gain/SplitInfo

增益比率最大,则选择的属性最佳

问题:分裂信息有可能为0,也有可能趋近于0,此时得到的增益比率无意义

改进的措施就是在分母加一个平滑,这里加一个所有分裂信息的平均值:
GainRadio = Gain/(SplitInfo_mean+SplitInfo)

基尼指数

基尼不纯度

Gini(D) = 1 - ΣPi² (越纯值越小0)

在CART(Classification and Regression Tree)算法中利用基尼指数构造二叉决策树

属性R分裂后的基尼系数为:
Gini_s(D, R) = Σlen(D)/len(S)Gini(D) (二分。多个分法的时候取Gini_s最小的分法)

增量 ΔGini® = Gini(D) - Gini_s(D, R)

取增量最大的属性作为最佳分裂属性

若不构造二叉决策树

分裂后的不纯度为
Gini_s(D) = Σlen(D)/len(S)Gini(D)

增量计算同样。

创建决策树

根据数据集及属性不断分裂(递归)

终止条件:
1、遍历完所有的属性
2、此数据集内只有一个类别的数据了

  • 实现方法:用字典实现决策树的嵌套
def create_tree(dataset, classes, feat_names):
    ''' 根据当前数据集递归创建决策树
    :param dataset: 数据集
    :param feat_names: 数据集中数据相应的特征名称
    :param classes: 数据集中数据相应的类型
    :param tree: 以字典形式返回决策树
    '''
    #只有一种分类
    if len(set(classes)) == 1:
        return classes[0]
    #所有的feature都遍历完了,返回比例最多的类型
    if len(feat_names) == 0:
        return get_majority(classes)
    #分裂创建子树
    tree = {}
    best_feat_idx = choose_best_split_feature2(dataset, classes)
    feature = feat_names[best_feat_idx]
    tree[feature] = {}
    # 创建用于递归创建子树的子数据集
    sub_feat_names = feat_names[:]
    sub_feat_names.pop(best_feat_idx)
    split_dict = split_dataset(dataset, classes, best_feat_idx)
    for feat_val,(sub_dataset, sub_classes) in split_dict.items():
        tree[feature][feature_val] = create_tree(sub_dataset, sub_classes, sub_feat_names)
    
    #self.tree = tree
    #self.feat_names = feat_names
    
    return tree

构建决策树

#完整的类
class DicisionTreeClassifier(object):
    
    def get_shanno_entorpy(self, values):
        '''
        根据给定列表中的值计算其Shanno Entropy
        '''
        uniq_vals = set(values)
        vals_num = {key: values.count(key) for key in uniq_vals}
        probs = [v/len(values) for k, v in vals_num.items()]
        entorpy = sum([-prob*math.log2(prob) for prob in probs])
        return entorpy
    
    def choose_best_split_feature2(self, dataset, classes):
        ''' 根据信息增益确定最好的划分数据的特征
        :param dataset: 待划分的数据集
        :param classes: 数据集对应的类型
        :return: 划分数据的增益最大的属性索引
        '''
        #注意:划分的数据集是classes(label)
        feat_num = len(dataset[0])
        entorpy_gains = []
        for i in range(feat_num):
            splited_dict = self.split_dataset(dataset, classes, i)
            new_entorpy = sum([len(subclasses)/len(classes) * self.get_shanno_entorpy(subclasses) 
                               for _, (_, subclasses) in splited_dict.items()])
            entorpy_gains.append(new_entorpy)
        return entorpy_gains.index(min(entorpy_gains))
    
    def create_tree(self, dataset, classes, feat_names):
        ''' 根据当前数据集递归创建决策树
        :param dataset: 数据集
        :param feat_names: 数据集中数据相应的特征名称
        :param classes: 数据集中数据相应的类型
        :param tree: 以字典形式返回决策树
        '''
        #只有一种分类
        if len(set(classes)) == 1:
            return classes[0]
        #所有的feature都遍历完了,返回比例最多的类型
        if len(feat_names) == 0:
            return get_majority(classes)
        #分裂创建子树
        tree = {}
        best_feat_idx = self.choose_best_split_feature2(dataset, classes)
        feature = feat_names[best_feat_idx]
        tree[feature] = {}
        # 创建用于递归创建子树的子数据集
        sub_feat_names = feat_names[:]
        sub_feat_names.pop(best_feat_idx)
        splited_dict = self.split_dataset(dataset, classes, best_feat_idx)
        for feat_val,(sub_dataset, sub_classes) in splited_dict.items():
            tree[feature][feat_val] = self.create_tree(sub_dataset, sub_classes, sub_feat_names)
    
        self.tree = tree
        self.feat_names = feat_names
    
        return tree
    
    @staticmethod
    def split_dataset(dataset, classes, feat_idx):
        """
        根据某个特征以及特征值划分数据集
        :param dataset: 待划分的数据集, 有数据向量组成的列表.
        :param classes: 数据集对应的类型, 与数据集有相同的长度
        :param feat_idx: 特征在特征向量中的索引
        :return splited_dict: 保存分割后数据的字典 特征值: [子数据集, 子类型列表]
        """
        splited_dict = {}
        for data_vect, cls in zip(dataset, classes):
            #对于不同的data_vect,feat_val的值是不同的,这样对应的data就会插入到不同的key下
            feat_val = data_vect[feat_idx]
            sub_dataset, sub_classes = splited_dict.setdefault(feat_val, [[], []])
            sub_dataset.append(data_vect[: feat_idx] + data_vect[feat_idx+1: ])  
            #去除data中已经分解过的属性的值
            sub_classes.append(cls)
        
        return splited_dict
    
    def get_majority(classes):
        """
        返回类中占据最多数的类型
        """
        cls_num = defaultdict(lambda:0)
        for cls in classes:
            cls_num[cls] += 1
        #注意用法:返回字典中最大的value对应的key
        return max(cls_num, key=cls_num.get)
    
    #分类函数
    def classify(self,test_vect, tree=None, feat_names=None):
        if tree is None:
            tree = self.tree
        if feat_names is None:
            feat_names = self.feat_names
        
        #结束条件
        if type(tree) is not dict:
            return tree
        
        feature = list(tree.keys())[0]
        value = test_vect[feat_names.index(feature)]
        subtree = tree[feature][value]
        return self.classify(test_vect, subtree, feat_names)
    
    #存储决策树
    def dump_tree(self, filename, tree=None):
        if tree is None:
            tree = self.tree
        with open(filename, 'wb') as f:
            pickle.dump(tree, f)
            
    #加载决策树
    def load_tree(self, filename):
        with open(filename, 'rb') as f:
            tree = pickle.load(f)
            self.tree = tree
        return tree
        

#跑一个数据集看看

lense_labels = ['age', 'prescript', 'astigmatic', 'tearRate']
X = []
Y = []

with open('lenses.txt', 'r') as f:
    for line in f:
        data = line.strip().split('\t')
        X.append(data[:-1])
        Y.append(data[-1])

clf = DicisionTreeClassifier()
clf.create_tree(X, Y, lense_labels)
clf.tree
{'tearRate': {'normal': {'astigmatic': {'no': {'age': {'pre': 'soft',
      'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}},
      'young': 'soft'}},
    'yes': {'prescript': {'hyper': {'age': {'pre': 'no lenses',
        'presbyopic': 'no lenses',
        'young': 'hard'}},
      'myope': 'hard'}}}},
  'reduced': 'no lenses'}}
clf.classify(['pre','hyper','no','normal'])
'soft'
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值