【机器学习】【决策树】自己动手用Python实现一个类:in样本集,out特征分布、概率密度、熵、条件熵、信息增益

时间:2022-04-23 19:56:51

看懂代码的前提需要理解样本空间分布,概率密度,香农熵,条件熵,信息增益等概念,

否则代码看不懂,不理解的可以看以前博客~

1.说明

1.1要实现的类

class CSamplesTool(object)

1.2输入的样本集

输入的样本集,样例由下面的方法提供:

def create_samples():
    '''
    提供训练样本集
    
    每个example由多个特征值+1个分类标签值组成
    比如第一个example=['youth', 'no', 'no', '1', 'refuse'],此样本的含义可以解读为:
    如果一个人的条件是:youth age,no working, no house, 信誉值credit为1
    那么此类人会被分类到refuse一类中,即在相亲中被拒绝
    
    每个example的特征值类型为:
    ['age', 'working', 'house', 'credit']
    
    每个example的分类标签class_label取值范围为:'refuse'或者'agree'
    '''
    data_list = [['youth', 'no',  'no',   '1', 'refuse'],
                 ['youth', 'no',  'no',   '2', 'refuse'],
                 ['youth', 'yes', 'no',   '2', 'agree'],
                 ['youth', 'yes', 'yes',  '1', 'agree'],
                 ['youth', 'no',  'no',   '1', 'refuse'],
                 ['mid',   'no',  'no',   '1', 'refuse'],
                 ['mid',   'no',  'no',   '2', 'refuse'],
                 ['mid',   'yes', 'yes',  '2', 'agree'],
                 ['mid',   'no',  'yes',  '3', 'agree'],
                 ['mid',   'no',  'yes',  '3', 'agree'],
                 ['elder', 'no',  'yes',  '3', 'agree'],
                 ['elder', 'no',  'yes',  '2', 'agree'],
                 ['elder', 'yes', 'no',   '2', 'agree'],
                 ['elder', 'yes', 'no',   '3', 'agree'],
                 ['elder', 'no',  'no',   '1', 'refuse']]
    feat_type_list = ['age', 'working', 'house', 'credit']
    return data_list, feat_type_list

1.3输出的样本集的特征分布、概率密度、香农熵、条件熵、信息增益组成的字典

对于样例的样本集,通过类的计算,输出如下目标字典,字典内部包含了样本集的香农熵、特征分布、特征概率密度、条件熵、信息增益等信息:

1.3.1运行结果截图

【机器学习】【决策树】自己动手用Python实现一个类:in样本集,out特征分布、概率密度、熵、条件熵、信息增益

1.3.2运行结果log

feat_dict = {
         house :{
                 condition_entropy :  0.5509775004326937
                 cnt :  15
                 info_gain :  0.4199730940219749
                 yes : {'cnt': 6, 'refuse': 0, 'shannon_entropy': 0.0, 'p_agree': 1.0, 'p_refuse': 0.0, 'p_house': 0.4, 'agree': 6}
                 no : {'cnt': 9, 'refuse': 6, 'shannon_entropy': 0.9182958340544896, 'p_agree': 0.3333333333333333, 'p_refuse': 0.6666666666666666, 'p_house': 0.6, 'agree': 3}
                 }
         credit :{
                 condition_entropy :  0.6079610319175832
                 cnt :  15
                 3 : {'cnt': 4, 'refuse': 0, 'p_credit': 0.26666666666666666, 'shannon_entropy': 0.0, 'p_agree': 1.0, 'p_refuse': 0.0, 'agree': 4}
                 1 : {'cnt': 5, 'refuse': 4, 'p_credit': 0.3333333333333333, 'shannon_entropy': 0.7219280948873623, 'p_agree': 0.2, 'p_refuse': 0.8, 'agree': 1}
                 info_gain :  0.36298956253708536
                 2 : {'cnt': 6, 'refuse': 2, 'p_credit': 0.4, 'shannon_entropy': 0.9182958340544896, 'p_agree': 0.6666666666666666, 'p_refuse': 0.3333333333333333, 'agree': 4}
                 }
         working :{
                 condition_entropy :  0.6473003963031123
                 cnt :  15
                 info_gain :  0.32365019815155627
                 yes : {'cnt': 5, 'refuse': 0, 'shannon_entropy': 0.0, 'p_working': 0.3333333333333333, 'p_agree': 1.0, 'p_refuse': 0.0, 'agree': 5}
                 no : {'cnt': 10, 'refuse': 6, 'shannon_entropy': 0.9709505944546686, 'p_working': 0.6666666666666666, 'p_agree': 0.4, 'p_refuse': 0.6, 'agree': 4}
                 }
         age :{
                 condition_entropy :  0.8879430945988998
                 cnt :  15
                 info_gain :  0.08300749985576883
                 elder : {'cnt': 5, 'refuse': 1, 'p_age': 0.3333333333333333, 'shannon_entropy': 0.7219280948873623, 'p_agree': 0.8, 'p_refuse': 0.2, 'agree': 4}
                 youth : {'cnt': 5, 'refuse': 3, 'p_age': 0.3333333333333333, 'shannon_entropy': 0.9709505944546686, 'p_agree': 0.4, 'p_refuse': 0.6, 'agree': 2}
                 mid : {'cnt': 5, 'refuse': 2, 'p_age': 0.3333333333333333, 'shannon_entropy': 0.9709505944546686, 'p_agree': 0.6, 'p_refuse': 0.4, 'agree': 3}
                 }
         }

2.话不多说,直接上代码

# -*- coding: utf-8 -*-
"""
@author: 蔚蓝的天空Tom
Talk is cheap,show me the code
Aim:样本集中每种类型特征的变量分布、概率分布、香农熵、条件熵
Aim:每种类型特征都分别有多种特征值,请萃取出每个特征值的样本数据
Aim:求每种特征值被分类的概率分布
"""


import numpy as np
import math


'''Tool Function'''
varnamestr = lambda v,nms: [ vn for vn in nms if id(v)==id(nms[vn])][0]


#============================================
class CUtileTool(object):
    '''
    提供有用的方法
    比如dump_list方法,可以打印给定的list的相关信息
    '''
    def dump_list(self, src_list, src_list_namestr):
        '''
        逐行打印list
        :param self:类实例自身
        :param src_list:被打印的源list
        :return 无
        '''
        print('\n============',src_list_namestr,'================')
        list_len = len(src_list)
        list_shape = np.shape(src_list)
        print('type(',src_list_namestr,'):',type(src_list))  #<class 'list'>
        print('np.shape(',src_list_namestr,'):',np.shape(src_list))
        if 1 == len(list_shape):
            print(src_list)
        elif 2 == len(list_shape):
            for i in range(list_len):
                if 0 == i:
                    print('[',src_list[i])
                elif (list_len - 1) == i:
                    print(src_list[i],']')
                else:
                    print(src_list[i])
        else:
            print(src_list)
        print('======\n')
        return
 
    def dump_array(self, src_a, src_dict_namestr):
        '''
        逐行打印array
        :param self:类实例自身
        :param src_a:被打印的源array
        :return 无
        '''
        print('\n===============',src_dict_namestr,'===================')
        a_len = len(src_a)
        a_shape = np.shape(src_a)
        print('type(',src_dict_namestr,'):',type(src_a))  #<class 'list'>
        print('np.shape(',src_dict_namestr,'):',np.shape(src_a))
        if 1 == len(a_shape):
            print(src_a)
        elif 2 == len(a_shape):
            for i in range(a_len):
                if 0 == i:
                    print('[',src_a[i])
                elif (a_len - 1) == i:
                    print(src_a[i],']')
                else:
                    print(src_a[i])
        else:
            print(src_a)
        print('======\n')
        return


    def print_dict(self, src_dict, level, src_dict_namestr=''):
        '''
        逐行打印dict
        :param self:类实例自身
        :param src_dict:被打印的dict
        :param level:递归level,初次调用为level=0
        :param src_dict_namestr:对象变量名称字符串
        '''
        if isinstance(src_dict, dict):
            tab_str = '\t'
            for i in range(level):
                tab_str += '\t'
            if 0 == level:
                print(src_dict_namestr,'= {')
            for key, value in src_dict.items():
                if isinstance(value, dict):
                    has_dict = False
                    for k,v in value.items():
                        if isinstance(v, dict):
                            has_dict = True
                    if has_dict:
                        print(tab_str,key,":{")
                        self.print_dict(value, level + 1)
                    else:
                        print(tab_str,key,':',value)
                else:
                    print(tab_str,key,': ',value,)
            print(tab_str,'}')


    def dump_dict(self, src_dict, src_dict_namestr):
        '''
        逐行打印dict
        :param self:类实例自身
        :param src_dict:被打印的dict对象
        :return 无
        '''
        print('\n===============',src_dict_namestr,'===================')
        dict_len = len(src_dict)
        dict_shape = np.shape(src_dict)
        dict_type = type(src_dict)
        print('len(',src_dict_namestr,'):',dict_len)
        print('type(',src_dict_namestr,'):', dict_type)  #<class 'dict'>
        print('np.shape(',src_dict_namestr,'):', dict_shape)
        print('len(dict_shape):', len(dict_shape))
        
        self.print_dict(src_dict, 0, src_dict_namestr)
        print('======\n')
        return
        
    def dump(self, src_thing, src_thing_namestr):
        '''
        逐行打印变量(list, array, matrix等)
        :param self:类实例自身
        :param src_things:被打印者
        :return 无
        '''
        if isinstance(src_thing, list):
            return self.dump_list(src_thing, src_thing_namestr)
        elif isinstance(src_thing, np.ndarray):
            return self.dump_array(src_thing, src_thing_namestr)
        elif isinstance(src_thing, dict):
            return self.dump_dict(src_thing, src_thing_namestr)
        else:
            print(src_thing_namestr,':', src_thing)
        return
#===========================================


def create_samples():
    '''
    提供训练样本集
    
    每个example由多个特征值+1个分类标签值组成
    比如第一个example=['youth', 'no', 'no', '1', 'refuse'],此样本的含义可以解读为:
    如果一个人的条件是:youth age,no working, no house, 信誉值credit为1
    那么此类人会被分类到refuse一类中,即在相亲中被拒绝
    
    每个example的特征值类型为:
    ['age', 'working', 'house', 'credit']
    
    每个example的分类标签class_label取值范围为:'refuse'或者'agree'
    '''
    data_list = [['youth', 'no',  'no',   '1', 'refuse'],
                 ['youth', 'no',  'no',   '2', 'refuse'],
                 ['youth', 'yes', 'no',   '2', 'agree'],
                 ['youth', 'yes', 'yes',  '1', 'agree'],
                 ['youth', 'no',  'no',   '1', 'refuse'],
                 ['mid',   'no',  'no',   '1', 'refuse'],
                 ['mid',   'no',  'no',   '2', 'refuse'],
                 ['mid',   'yes', 'yes',  '2', 'agree'],
                 ['mid',   'no',  'yes',  '3', 'agree'],
                 ['mid',   'no',  'yes',  '3', 'agree'],
                 ['elder', 'no',  'yes',  '3', 'agree'],
                 ['elder', 'no',  'yes',  '2', 'agree'],
                 ['elder', 'yes', 'no',   '2', 'agree'],
                 ['elder', 'yes', 'no',   '3', 'agree'],
                 ['elder', 'no',  'no',   '1', 'refuse']]
    feat_type_list = ['age', 'working', 'house', 'credit']
    return data_list, feat_type_list
    
class CSamplesTool(object):
    '提供样本集的摘取目标数据的方法'
    def __init__(self, data_list, feat_type_list):
        '''
        初始化函数
        :param data_list:数据集
        :param feat_type_list:数据的特征类型列表
        :return 无
        '''
        
        ''' 
        self.data_list 样例:
        [['youth', 'no',  'no',   '1', 'refuse'],
         ['youth', 'no',  'no',   '2', 'refuse'],
         ……
         ]
        '''
        self.data_list = data_list
        
        '''
        self.feat_type_list 样例:
        ['age', 'working', 'house', 'credit']
        '''
        self.feat_type_list = feat_type_list
        
        '''self.data_cnt 就是样本集中样本的总数'''
        self.data_cnt = len(data_list)
        
        '''self.feat_type_cnt 就是每个样本的特征值类型总数'''
        self.feat_type_cnt = len(feat_type_list)
        
        '''
        self.feat_value_list 每种类型特征的取值列表, 样例:
        [ 
          ['youth', 'youth', 'youth', 'youth', 'youth', 'mid', 'mid', 'mid', 'mid', 'mid', 'elder', 'elder', 'elder', 'elder', 'elder']
          ['no', 'no', 'yes', 'yes', 'no', 'no', 'no', 'yes', 'no', 'no', 'no', 'no', 'yes', 'yes', 'no']
          ['no', 'no', 'no', 'yes', 'no', 'no', 'no', 'yes', 'yes', 'yes', 'yes', 'yes', 'no', 'no', 'no']
          ['1', '2', '2', '1', '1', '1', '2', '2', '3', '3', '3', '2', '2', '3', '1']
        ]
        '''
        self.feat_value_list = []


        '''
        self.class_value_list 样本集的分类标签取值列表,长度为样本总数,样例:
        ['refuse', 'refuse', 'agree', …… ,'agree', 'agree', 'refuse']
        '''
        self.class_value_list = []
        
        '''
        self.feat_dict 样本集的特征字典,样例:
        {
         house :{
                 condition_entropy :  0.5509775004326937
                 cnt :  15
                 info_gain :  0.4199730940219749
                 yes : {'cnt': 6, 'refuse': 0, 'shannon_entropy': 0.0, 'p_agree': 1.0, 'p_refuse': 0.0, 'p_house': 0.4, 'agree': 6}
                 no : {'cnt': 9, 'refuse': 6, 'shannon_entropy': 0.9182958340544896, 'p_agree': 0.3333333333333333, 'p_refuse': 0.6666666666666666, 'p_house': 0.6, 'agree': 3}
                 }
         credit :{
                 condition_entropy :  0.6079610319175832
                 cnt :  15
                 3 : {'cnt': 4, 'refuse': 0, 'p_credit': 0.26666666666666666, 'shannon_entropy': 0.0, 'p_agree': 1.0, 'p_refuse': 0.0, 'agree': 4}
                 1 : {'cnt': 5, 'refuse': 4, 'p_credit': 0.3333333333333333, 'shannon_entropy': 0.7219280948873623, 'p_agree': 0.2, 'p_refuse': 0.8, 'agree': 1}
                 info_gain :  0.36298956253708536
                 2 : {'cnt': 6, 'refuse': 2, 'p_credit': 0.4, 'shannon_entropy': 0.9182958340544896, 'p_agree': 0.6666666666666666, 'p_refuse': 0.3333333333333333, 'agree': 4}
                 }
         working :{
                 condition_entropy :  0.6473003963031123
                 cnt :  15
                 info_gain :  0.32365019815155627
                 yes : {'cnt': 5, 'refuse': 0, 'shannon_entropy': 0.0, 'p_working': 0.3333333333333333, 'p_agree': 1.0, 'p_refuse': 0.0, 'agree': 5}
                 no : {'cnt': 10, 'refuse': 6, 'shannon_entropy': 0.9709505944546686, 'p_working': 0.6666666666666666, 'p_agree': 0.4, 'p_refuse': 0.6, 'agree': 4}
                 }
         age :{
                 condition_entropy :  0.8879430945988998
                 cnt :  15
                 info_gain :  0.08300749985576883
                 elder : {'cnt': 5, 'refuse': 1, 'p_age': 0.3333333333333333, 'shannon_entropy': 0.7219280948873623, 'p_agree': 0.8, 'p_refuse': 0.2, 'agree': 4}
                 youth : {'cnt': 5, 'refuse': 3, 'p_age': 0.3333333333333333, 'shannon_entropy': 0.9709505944546686, 'p_agree': 0.4, 'p_refuse': 0.6, 'agree': 2}
                 mid : {'cnt': 5, 'refuse': 2, 'p_age': 0.3333333333333333, 'shannon_entropy': 0.9709505944546686, 'p_agree': 0.6, 'p_refuse': 0.4, 'agree': 3}
                 }
         }
        '''
        self.feat_dict = {}


        '''
        样本集的香农熵,H(Y={refuse, agree})
        '''
        self.samples_shanon_entropy = 1000
        
        #计算
        self.pickout_feat()
        self.pickout_class()
        self.pickout_samples_shannon_entropy()
        self.pickout_feat_dict()
            
        return


    def shan_ent_ele(self, p):
        if 0 == p:
            return 0
        else:
            return -1 * p * math.log2(p)


    def shannon_entropy(self, P):
        func = np.frompyfunc(self.shan_ent_ele, 1, 1)
        ent_ele_list = func(P)
        entropy = ent_ele_list.sum()
        return entropy
        
    def get_data_cnt(self):
        '''
        样本总数
        '''
        return self.data_cnt
       
    def get_feat_type_cnt(self):
        '''
        特征类型总数
        '''
        return self.feat_type_cnt
       
    def get_feats(self):
        '''
        每种类型特征的特征值列表
        '''
        return self.feat_value_list
    
    def get_classes(self):
        '''
        样本集的分类列表
        '''
        return self.class_value_list
        
    def get_samples_shannon_entropy(self):
        '''
        样本集的香农熵,H(Y={refuse, agree})
        '''
        return self.samples_shanon_entropy
        
    def get_feat_dict(self):
        '''
        特征分布以及概率分布
        '''
        return self.feat_dict
        
    def pickout_feat(self):
        '''
        摘取每种类型特征的特征值
        :return 无
        '''
        self.feat_value_list = []
        for dim in range(self.feat_type_cnt):
            curtype_feat = [example[dim] for example in self.data_list]
            ''' 这一句等效下面三句
            curtype_feat = []
            for i in range(self.data_cnt):
                curtype_feat.append(self.data_list[i][dim])
            '''
            self.feat_value_list.append(curtype_feat)
        return


    def pickout_class(self):
        '''
        摘取分类列表,大小一定是m×1
        '''
        self.class_value_list = []
        self.class_value_list = [example[-1] for example in self.data_list]
        '''这一句等效下面两句
        for i in range(self.data_cnt):
            self.class_value_list.append(self.data_list[i][-1])
        '''
    
    def pickout_samples_shannon_entropy(self):
        '''
        计算样本集的香农熵,H(Y={refuse, agree})
        '''
        #统计样本集的分类标签分布
        label_set = set(self.class_value_list)
        label_cnt_list = []
        for label in label_set:
            label_cnt_list.append(self.class_value_list.count(label))
        
        #统计样本集分类标签概率密度
        n_samples = len(self.class_value_list)
        label_prob_list = [label_cnt/n_samples for label_cnt in label_cnt_list]


        #计算样本集的香农熵
        self.samples_shanon_entropy = self.shannon_entropy(label_prob_list)
        return        
    
    def pickout_feat_dict(self):
        '''
        摘取特征分布以及特征概率分布
        '''
        self.feat_dict = {}
        class_label_set = set(self.class_value_list)
        print(class_label_set)
        for i in range(self.feat_type_cnt):
            #依次加入特征类型字典:age:{}, house:{}, working:{}, credit:{}
            feat_type = self.feat_type_list[i]
            self.feat_dict[feat_type] = {}
            #初始化feat_type时的条件熵, 即增加age:{'condition_entropy':0.0}
            self.feat_dict[feat_type]['condition_entropy'] = 0.0
            #初始化feat_type时的信息增益, 即增加age:{'inf_gain':0.0}
            self.feat_dict[feat_type]['info_gain'] = 0.0
            #填充feat_type的取值范围总数,如feat_type是age时,总数为3,可取值={youth, mid, elder}
            self.feat_dict[feat_type]['cnt'] = len(self.feat_value_list[i])
            #填充特征类型字典的key,例如age:{'youth':{}, 'mid:{}, 'elder':{}}
            #为具体特征值的字典填充内容
            #例如age:{elder:{填充此内容}} 填充为 age:{elder:{'cnt':5, 'p_age':5./15, 'refuse':4, 'agree':1, 'p_refuse':3./5, 'p_agree':2./5}}
            #填充具体特征值的总样本数cnt
            feat_value_set = set(self.feat_value_list[i])
            for feat_value in feat_value_set:
                #填充{'age':{'elder':{}}}其中的elder:{}
                self.feat_dict[feat_type][feat_value] = {}
                #填充'age':{'elder':{'cnt':5}}其中的'cnt':5
                self.feat_dict[feat_type][feat_value]['cnt'] = self.feat_value_list[i].count(feat_value)
                #填充'ag'e:{'elder':{'p_age':5./15}}其中的p_age:5./15
                value_prob_key = 'p_' + str(feat_type)
                self.feat_dict[feat_type][feat_value][value_prob_key] = self.feat_value_list[i].count(feat_value)*(1.0)/len(self.feat_value_list[i])
                #填充'age':{'elder':{'refuse':4, 'agree':1}}其中的'refuse':4, 'agree':1,
                for n in range(self.data_cnt):
                    for class_label in class_label_set:
                        if class_label not in self.feat_dict[feat_type][feat_value].keys():
                            self.feat_dict[feat_type][feat_value][class_label] = 0
                        if feat_value == self.feat_value_list[i][n] and class_label == self.class_value_list[n]:
                            self.feat_dict[feat_type][feat_value][class_label] += 1
                #填充'age':{'elder':{'p_refuse':3./5, 'p_agree':2./5}}其中的'p_refuse':3./5, 'p_agree':2./5
                self.feat_dict[feat_type][feat_value]['shannon_entropy'] = 0.0
                for class_label in class_label_set:
                    prob_key = 'p_'+str(class_label)
                    class_label_cnt = self.feat_dict[feat_type][feat_value][class_label]
                    value_cnt = self.feat_dict[feat_type][feat_value]['cnt']
                    self.feat_dict[feat_type][feat_value][prob_key] = float(class_label_cnt) / value_cnt
                    #填充'age':{'elder':{'shannon_entropy':2.3}}其中的'shannon_entropy':2.3
                    prob = self.feat_dict[feat_type][feat_value][prob_key]
                    if 0 != prob:
                        self.feat_dict[feat_type][feat_value]['shannon_entropy'] += -1 * prob * math.log2(prob)
                #填充feat_type时的条件熵, 即增加age:{'condition_entropy':1.0}中的'condition_entropy':1.0
                feat_value_prob = self.feat_dict[feat_type][feat_value][value_prob_key]
                feat_value_sEnt = self.feat_dict[feat_type][feat_value]['shannon_entropy']
                self.feat_dict[feat_type]['condition_entropy'] += feat_value_prob * feat_value_sEnt
            #填充feat_type时的信息增益,即计算age:{'info_gain':0.345}中的'info_gain':0.345
            print('self.samples_shanon_entropy:',self.samples_shanon_entropy)
            self.feat_dict[feat_type]['info_gain'] = self.samples_shanon_entropy - self.feat_dict[feat_type]['condition_entropy']


if __name__=='__main__':
    #创建样本集
    train_data_list, feat_type_list = create_samples()
    #创建样本集工具类实例
    samples = CSamplesTool(train_data_list, feat_type_list)
    #创建通用工具类实例
    tool = CUtileTool()
    
    tool.dump(samples.get_feats(), 'feat_value_list')
    tool.dump(samples.get_classes(), 'class_value_list')
    tool.dump(samples.get_feat_dict(), 'feat_dict')
                

3.如何使用类输出的字典?

不懂字典结构的,需要学习下python中dict的语法,很容易的,类似Json的数据结构~


3.1从输出样本集的字典,可以很容易汇总信息增益

根据类输出的样本集的字典,很容易得到,每个特征的信息增益,如下所示:

样本集的香农熵baseEntropy: 0.9709505944546686

house:
	info_gain :  0.4199730940219749
	
credit:
	info_gain :  0.36298956253708536


working:
	info_gain :  0.32365019815155627
	
age:
	info_gain :  0.08300749985576883

3.2使用信息增益来找决策树的根节点

如果以此样例样本集为训练集,其决策树的根节点应该是,信息增益最大的特征。

通过样本集的字典可以知道,条件样本空间为house时,信息增益H(Y|X=house)=0.4199730940219749是最大的。

所以选取特征house为训练集的决策树的根节点~,此时的决策树最简洁有效。


note:如果这个代码搞定了,其实决策树的生成代码,差不多已经完成一半了。

enjoy it ~

(end)